Merge branch 'development' into development_builders
* development: minor example fixes feat: add comprehensive SSH agent management command refactor: Harden and improve SSH agent module ... feat: add editable ttyd dashboard mode feat: add CLI for dashboard management and 4-pane layout fix: Fix build ... refactor: update SSH agent examples and module structure feat: add tmux dashboard with ttyd integration refactor: Remove is_tmux_server_not_running_error function wip: pushing the code to sync in other branch refactor: Improve tmux API consistency and formatting # Conflicts: # lib/osal/core/net.v # lib/virt/podman/factory.v
This commit is contained in:
@@ -86,6 +86,7 @@ fn do() ! {
|
|||||||
herocmds.cmd_generator(mut cmd)
|
herocmds.cmd_generator(mut cmd)
|
||||||
herocmds.cmd_docusaurus(mut cmd)
|
herocmds.cmd_docusaurus(mut cmd)
|
||||||
herocmds.cmd_web(mut cmd)
|
herocmds.cmd_web(mut cmd)
|
||||||
|
herocmds.cmd_sshagent(mut cmd)
|
||||||
|
|
||||||
cmd.setup()
|
cmd.setup()
|
||||||
cmd.parse(os.args)
|
cmd.parse(os.args)
|
||||||
|
|||||||
1519
debug.logs
1519
debug.logs
File diff suppressed because it is too large
Load Diff
0
examples/osal/sshagent.vsh → examples/osal/sshagent/sshagent.vsh
Normal file → Executable file
0
examples/osal/sshagent.vsh → examples/osal/sshagent/sshagent.vsh
Normal file → Executable file
@@ -1,51 +0,0 @@
|
|||||||
module main
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.osal.sshagent
|
|
||||||
import freeflowuniverse.herolib.osal.linux
|
|
||||||
|
|
||||||
fn do1() ! {
|
|
||||||
mut agent := sshagent.new()!
|
|
||||||
println(agent)
|
|
||||||
k := agent.get(name: 'kds') or { panic('notgound') }
|
|
||||||
println(k)
|
|
||||||
|
|
||||||
mut k2 := agent.get(name: 'books') or { panic('notgound') }
|
|
||||||
k2.load()!
|
|
||||||
println(k2.agent)
|
|
||||||
|
|
||||||
println(agent)
|
|
||||||
|
|
||||||
k2.forget()!
|
|
||||||
println(k2.agent)
|
|
||||||
|
|
||||||
// println(agent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_user_mgmt() ! {
|
|
||||||
mut lf := linux.new()!
|
|
||||||
// Test user creation
|
|
||||||
lf.user_create(
|
|
||||||
name: 'testuser'
|
|
||||||
sshkey: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM3/2K7R8A/l0kM0/d'
|
|
||||||
)!
|
|
||||||
|
|
||||||
// Test ssh key creation
|
|
||||||
lf.sshkey_create(
|
|
||||||
username: 'testuser'
|
|
||||||
sshkey_name: 'testkey'
|
|
||||||
)!
|
|
||||||
|
|
||||||
// Test ssh key deletion
|
|
||||||
lf.sshkey_delete(
|
|
||||||
username: 'testuser'
|
|
||||||
sshkey_name: 'testkey'
|
|
||||||
)!
|
|
||||||
|
|
||||||
// Test user deletion
|
|
||||||
lf.user_delete(name: 'testuser')!
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
do1() or { panic(err) }
|
|
||||||
test_user_mgmt() or { panic(err) }
|
|
||||||
}
|
|
||||||
168
examples/osal/sshagent/sshagent_example.vsh
Executable file
168
examples/osal/sshagent/sshagent_example.vsh
Executable file
@@ -0,0 +1,168 @@
|
|||||||
|
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||||
|
|
||||||
|
import freeflowuniverse.herolib.osal.sshagent
|
||||||
|
import freeflowuniverse.herolib.osal.linux
|
||||||
|
import freeflowuniverse.herolib.ui.console
|
||||||
|
|
||||||
|
fn demo_sshagent_basic() ! {
|
||||||
|
console.print_header('SSH Agent Basic Demo')
|
||||||
|
|
||||||
|
// Create SSH agent
|
||||||
|
mut agent := sshagent.new()!
|
||||||
|
console.print_debug('SSH Agent initialized')
|
||||||
|
|
||||||
|
// Show current status
|
||||||
|
console.print_header('Current SSH Agent Status:')
|
||||||
|
println(agent)
|
||||||
|
|
||||||
|
// Show diagnostics
|
||||||
|
diag := agent.diagnostics()
|
||||||
|
console.print_header('SSH Agent Diagnostics:')
|
||||||
|
for key, value in diag {
|
||||||
|
console.print_item('${key}: ${value}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn demo_sshagent_key_management() ! {
|
||||||
|
console.print_header('SSH Agent Key Management Demo')
|
||||||
|
|
||||||
|
mut agent := sshagent.new()!
|
||||||
|
|
||||||
|
// Generate a test key if it doesn't exist
|
||||||
|
test_key_name := 'herolib_demo_key'
|
||||||
|
|
||||||
|
// Clean up any existing test key first
|
||||||
|
if existing_key := agent.get(name: test_key_name) {
|
||||||
|
console.print_debug('Removing existing test key...')
|
||||||
|
// Remove existing key files
|
||||||
|
mut key_file := agent.homepath.file_get_new('${test_key_name}')!
|
||||||
|
mut pub_key_file := agent.homepath.file_get_new('${test_key_name}.pub')!
|
||||||
|
key_file.delete() or {}
|
||||||
|
pub_key_file.delete() or {}
|
||||||
|
} else {
|
||||||
|
console.print_debug('No existing test key found')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new key with empty passphrase
|
||||||
|
console.print_debug('Generating new SSH key: ${test_key_name}')
|
||||||
|
mut new_key := agent.generate(test_key_name, '')!
|
||||||
|
console.print_green('✓ Generated new SSH key: ${test_key_name}')
|
||||||
|
|
||||||
|
// Show key information
|
||||||
|
console.print_item('Key name: ${new_key.name}')
|
||||||
|
console.print_item('Key type: ${new_key.cat}')
|
||||||
|
console.print_item('Key loaded: ${new_key.loaded}')
|
||||||
|
|
||||||
|
// Demonstrate key operations without loading (to avoid passphrase issues)
|
||||||
|
console.print_header('Key file operations:')
|
||||||
|
mut key_path := new_key.keypath()!
|
||||||
|
mut pub_key_path := new_key.keypath_pub()!
|
||||||
|
console.print_item('Private key path: ${key_path.path}')
|
||||||
|
console.print_item('Public key path: ${pub_key_path.path}')
|
||||||
|
|
||||||
|
// Show public key content
|
||||||
|
pub_key_content := new_key.keypub()!
|
||||||
|
preview_len := if pub_key_content.len > 60 { 60 } else { pub_key_content.len }
|
||||||
|
console.print_item('Public key: ${pub_key_content[0..preview_len]}...')
|
||||||
|
|
||||||
|
// Show agent status
|
||||||
|
console.print_header('Agent status after key generation:')
|
||||||
|
println(agent)
|
||||||
|
|
||||||
|
// Clean up test key
|
||||||
|
console.print_debug('Cleaning up test key...')
|
||||||
|
key_path.delete()!
|
||||||
|
pub_key_path.delete()!
|
||||||
|
console.print_green('✓ Test key cleaned up')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn demo_sshagent_with_existing_keys() ! {
|
||||||
|
console.print_header('SSH Agent with Existing Keys Demo')
|
||||||
|
|
||||||
|
mut agent := sshagent.new()!
|
||||||
|
|
||||||
|
if agent.keys.len == 0 {
|
||||||
|
console.print_debug('No SSH keys found. Generating example key...')
|
||||||
|
mut key := agent.generate('example_demo_key', '')!
|
||||||
|
key.load()!
|
||||||
|
console.print_green('✓ Created and loaded example key')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.print_header('Available SSH keys:')
|
||||||
|
for key in agent.keys {
|
||||||
|
status := if key.loaded { 'LOADED' } else { 'NOT LOADED' }
|
||||||
|
console.print_item('${key.name} - ${status} (${key.cat})')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to work with the first available key
|
||||||
|
if agent.keys.len > 0 {
|
||||||
|
mut first_key := agent.keys[0]
|
||||||
|
console.print_header('Working with key: ${first_key.name}')
|
||||||
|
|
||||||
|
if first_key.loaded {
|
||||||
|
console.print_debug('Key is loaded, showing public key info...')
|
||||||
|
pubkey := first_key.keypub() or { 'Could not read public key' }
|
||||||
|
preview_len := if pubkey.len > 50 { 50 } else { pubkey.len }
|
||||||
|
console.print_item('Public key preview: ${pubkey[0..preview_len]}...')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_user_mgmt() ! {
|
||||||
|
console.print_header('User Management Test')
|
||||||
|
|
||||||
|
// Note: This requires root privileges and should be run carefully
|
||||||
|
console.print_debug('User management test requires root privileges')
|
||||||
|
console.print_debug('Skipping user management test in this demo')
|
||||||
|
|
||||||
|
// Uncomment below to test user management (requires root)
|
||||||
|
/*
|
||||||
|
mut lf := linux.new()!
|
||||||
|
// Test user creation
|
||||||
|
lf.user_create(
|
||||||
|
name: 'testuser'
|
||||||
|
sshkey: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM3/2K7R8A/l0kM0/d'
|
||||||
|
)!
|
||||||
|
|
||||||
|
// Test ssh key creation
|
||||||
|
lf.sshkey_create(
|
||||||
|
username: 'testuser'
|
||||||
|
sshkey_name: 'testkey'
|
||||||
|
)!
|
||||||
|
|
||||||
|
// Test ssh key deletion
|
||||||
|
lf.sshkey_delete(
|
||||||
|
username: 'testuser'
|
||||||
|
sshkey_name: 'testkey'
|
||||||
|
)!
|
||||||
|
|
||||||
|
// Test user deletion
|
||||||
|
lf.user_delete(name: 'testuser')!
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
console.print_header('🔑 SSH Agent Example - HeroLib')
|
||||||
|
|
||||||
|
demo_sshagent_basic() or {
|
||||||
|
console.print_stderr('❌ Basic demo failed: ${err}')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
demo_sshagent_key_management() or {
|
||||||
|
console.print_stderr('❌ Key management demo failed: ${err}')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
demo_sshagent_with_existing_keys() or {
|
||||||
|
console.print_stderr('❌ Existing keys demo failed: ${err}')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
test_user_mgmt() or {
|
||||||
|
console.print_stderr('❌ User management test failed: ${err}')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.print_header('🎉 All SSH Agent demos completed successfully!')
|
||||||
|
}
|
||||||
85
examples/osal/sshagent/sshagent_example2.vsh
Executable file
85
examples/osal/sshagent/sshagent_example2.vsh
Executable file
@@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||||
|
|
||||||
|
import freeflowuniverse.herolib.osal.sshagent
|
||||||
|
import freeflowuniverse.herolib.ui.console
|
||||||
|
|
||||||
|
console.print_header('SSH Agent Management Example')
|
||||||
|
|
||||||
|
// Create SSH agent with single instance guarantee
|
||||||
|
mut agent := sshagent.new_single()!
|
||||||
|
println('SSH Agent initialized and ensured single instance')
|
||||||
|
|
||||||
|
// Show diagnostics
|
||||||
|
diag := agent.diagnostics()
|
||||||
|
console.print_header('SSH Agent Diagnostics:')
|
||||||
|
for key, value in diag {
|
||||||
|
console.print_item('${key}: ${value}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show current agent status
|
||||||
|
println(agent)
|
||||||
|
|
||||||
|
// Example: Generate a test key if no keys exist
|
||||||
|
if agent.keys.len == 0 {
|
||||||
|
console.print_header('No keys found, generating example key...')
|
||||||
|
mut key := agent.generate('example_key', '')!
|
||||||
|
console.print_debug('Generated key: ${key}')
|
||||||
|
|
||||||
|
// Load the generated key
|
||||||
|
key.load()!
|
||||||
|
console.print_debug('Key loaded into agent')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Working with existing keys
|
||||||
|
if agent.keys.len > 0 {
|
||||||
|
console.print_header('Working with existing keys...')
|
||||||
|
|
||||||
|
for i, key in agent.keys {
|
||||||
|
console.print_debug('Key ${i + 1}: ${key.name}')
|
||||||
|
console.print_debug(' Type: ${key.cat}')
|
||||||
|
console.print_debug(' Loaded: ${key.loaded}')
|
||||||
|
console.print_debug(' Email: ${key.email}')
|
||||||
|
|
||||||
|
if !key.loaded {
|
||||||
|
console.print_debug(' Loading key...')
|
||||||
|
mut key_mut := key
|
||||||
|
key_mut.load() or {
|
||||||
|
console.print_debug(' Failed to load: ${err}')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
console.print_debug(' ✓ Key loaded successfully')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Add a key from private key content
|
||||||
|
console.print_header('Example: Adding a key from content...')
|
||||||
|
console.print_debug('Note: This would normally use real private key content')
|
||||||
|
console.print_debug('For security, we skip this in the example')
|
||||||
|
|
||||||
|
// Example: Generate and manage a new key
|
||||||
|
console.print_header('Example: Generate a new test key...')
|
||||||
|
test_key_name := 'test_key_example'
|
||||||
|
|
||||||
|
// Check if test key already exists
|
||||||
|
existing_key := agent.get(name: test_key_name) or {
|
||||||
|
console.print_debug('Test key does not exist, generating...')
|
||||||
|
|
||||||
|
// Generate new key
|
||||||
|
mut new_key := agent.generate(test_key_name, '')!
|
||||||
|
console.print_debug('✓ Generated new key: ${new_key.name}')
|
||||||
|
|
||||||
|
// Load it
|
||||||
|
new_key.load()!
|
||||||
|
console.print_debug('✓ Key loaded into agent')
|
||||||
|
|
||||||
|
new_key
|
||||||
|
}
|
||||||
|
|
||||||
|
console.print_debug('Test key exists: ${existing_key.name}')
|
||||||
|
|
||||||
|
// Show final agent status
|
||||||
|
console.print_header('Final SSH Agent Status:')
|
||||||
|
println(agent)
|
||||||
|
|
||||||
|
console.print_header('SSH Agent example completed successfully')
|
||||||
255
examples/sshagent/hero_sshagent_examples.md
Normal file
255
examples/sshagent/hero_sshagent_examples.md
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
# Hero SSH Agent Management Tool
|
||||||
|
|
||||||
|
The Hero SSH Agent Management Tool provides comprehensive SSH agent lifecycle management with cross-platform compatibility for macOS and Linux. It integrates seamlessly with shell profiles and implements intelligent SSH agent lifecycle management, automatic key discovery and loading, and remote key deployment capabilities.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🔐 **Single SSH Agent Instance Enforcement**: Ensures exactly one SSH agent is running with persistent socket management
|
||||||
|
- 🚀 **Smart Key Loading**: Auto-loads single keys from ~/.ssh/ directory with passphrase prompting
|
||||||
|
- 🌐 **Remote Key Deployment**: Interactive SSH key deployment to remote machines
|
||||||
|
- 🔄 **Agent Health Verification**: Comprehensive health checks through ssh-agent -l functionality
|
||||||
|
- 📝 **Shell Profile Integration**: Automatic initialization in shell profiles
|
||||||
|
- 🎯 **Cross-Platform**: Works on macOS and Linux
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
The SSH agent functionality is built into the hero binary. After compiling hero:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./cli/compile.vsh
|
||||||
|
```
|
||||||
|
|
||||||
|
The hero binary will be available at `/Users/mahmoud/hero/bin/hero`
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### `hero sshagent profile`
|
||||||
|
|
||||||
|
Primary initialization command that ensures exactly one SSH agent is running on a consistent socket, performs health checks, manages agent lifecycle without losing existing keys, and automatically loads SSH keys when only one is present in ~/.ssh/ directory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Initialize SSH agent with smart key loading
|
||||||
|
hero sshagent profile
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
- Ensures single SSH agent instance
|
||||||
|
- Verifies agent health and responsiveness
|
||||||
|
- Auto-loads single SSH keys
|
||||||
|
- Updates shell profile for automatic initialization
|
||||||
|
- Preserves existing loaded keys
|
||||||
|
|
||||||
|
### `hero sshagent list`
|
||||||
|
|
||||||
|
Lists all available SSH keys and their current status.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all SSH keys
|
||||||
|
hero sshagent list
|
||||||
|
```
|
||||||
|
|
||||||
|
### `hero sshagent status`
|
||||||
|
|
||||||
|
Shows comprehensive SSH agent status and diagnostics.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show agent status
|
||||||
|
hero sshagent status
|
||||||
|
```
|
||||||
|
|
||||||
|
### `hero sshagent generate`
|
||||||
|
|
||||||
|
Generates a new SSH key pair.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate new SSH key
|
||||||
|
hero sshagent generate -n my_new_key
|
||||||
|
|
||||||
|
# Generate and immediately load
|
||||||
|
hero sshagent generate -n my_new_key -l
|
||||||
|
```
|
||||||
|
|
||||||
|
### `hero sshagent load`
|
||||||
|
|
||||||
|
Loads a specific SSH key into the agent.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Load specific key
|
||||||
|
hero sshagent load -n my_key
|
||||||
|
```
|
||||||
|
|
||||||
|
### `hero sshagent forget`
|
||||||
|
|
||||||
|
Removes a specific SSH key from the agent.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remove key from agent
|
||||||
|
hero sshagent forget -n my_key
|
||||||
|
```
|
||||||
|
|
||||||
|
### `hero sshagent reset`
|
||||||
|
|
||||||
|
Removes all loaded SSH keys from the agent.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Reset agent (remove all keys)
|
||||||
|
hero sshagent reset
|
||||||
|
```
|
||||||
|
|
||||||
|
### `hero sshagent push`
|
||||||
|
|
||||||
|
Interactive SSH key deployment to remote machines with automatic key selection when multiple keys exist, target specification via user@hostname format, and streamlined single-key auto-selection.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Deploy SSH key to remote machine
|
||||||
|
hero sshagent push -t user@hostname
|
||||||
|
|
||||||
|
# Deploy specific key to remote machine
|
||||||
|
hero sshagent push -t user@hostname -k my_key
|
||||||
|
|
||||||
|
# Deploy to custom port
|
||||||
|
hero sshagent push -t user@hostname:2222
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
- Automatic key selection when only one key exists
|
||||||
|
- Interactive key selection for multiple keys
|
||||||
|
- Support for custom SSH ports
|
||||||
|
- Uses ssh-copy-id when available, falls back to manual deployment
|
||||||
|
|
||||||
|
### `hero sshagent auth`
|
||||||
|
|
||||||
|
Remote SSH key authorization ensuring proper key installation on target machines, support for explicit key specification via -key parameter, and verification of successful key addition.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify SSH key authorization
|
||||||
|
hero sshagent auth -t user@hostname
|
||||||
|
|
||||||
|
# Verify specific key authorization
|
||||||
|
hero sshagent auth -t user@hostname -k my_key
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Basic Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Initialize SSH agent
|
||||||
|
hero sshagent profile
|
||||||
|
|
||||||
|
# 2. Check status
|
||||||
|
hero sshagent status
|
||||||
|
|
||||||
|
# 3. Generate a new key if needed
|
||||||
|
hero sshagent generate -n production_key
|
||||||
|
|
||||||
|
# 4. Load the key
|
||||||
|
hero sshagent load -n production_key
|
||||||
|
|
||||||
|
# 5. Deploy to remote server
|
||||||
|
hero sshagent push -t user@production-server.com
|
||||||
|
|
||||||
|
# 6. Verify authorization
|
||||||
|
hero sshagent auth -t user@production-server.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Deploy specific key to multiple servers
|
||||||
|
hero sshagent push -t user@server1.com -k production_key
|
||||||
|
hero sshagent push -t user@server2.com:2222 -k production_key
|
||||||
|
|
||||||
|
# Verify access to all servers
|
||||||
|
hero sshagent auth -t user@server1.com -k production_key
|
||||||
|
hero sshagent auth -t user@server2.com:2222 -k production_key
|
||||||
|
|
||||||
|
# List all keys and their status
|
||||||
|
hero sshagent list
|
||||||
|
|
||||||
|
# Reset agent if needed
|
||||||
|
hero sshagent reset
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technical Specifications
|
||||||
|
|
||||||
|
### Cross-Platform Compatibility
|
||||||
|
|
||||||
|
- **Socket Management**: Uses ~/.ssh/hero-agent.sock for consistent socket location
|
||||||
|
- **Shell Integration**: Supports ~/.profile, ~/.bash_profile, ~/.bashrc, and ~/.zshrc
|
||||||
|
- **Process Management**: Robust SSH agent lifecycle management
|
||||||
|
- **Platform Support**: macOS and Linux (Windows not supported)
|
||||||
|
|
||||||
|
### Security Features
|
||||||
|
|
||||||
|
- **Single Agent Enforcement**: Prevents multiple conflicting agents
|
||||||
|
- **Secure Socket Paths**: Uses user home directory for socket files
|
||||||
|
- **Proper Permissions**: Ensures correct file permissions (0700 for .ssh, 0600 for keys)
|
||||||
|
- **Input Validation**: Validates all user inputs and target specifications
|
||||||
|
- **Connection Testing**: Verifies SSH connections before reporting success
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
- **Network Connectivity**: Handles network failures gracefully
|
||||||
|
- **Authentication Failures**: Provides clear error messages for auth issues
|
||||||
|
- **Key Management**: Validates key existence and format
|
||||||
|
- **Target Validation**: Ensures proper target format (user@hostname[:port])
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
- `HERO_DEBUG=1`: Enable debug output for troubleshooting
|
||||||
|
|
||||||
|
## Integration with Development Environments
|
||||||
|
|
||||||
|
The tool is designed to work seamlessly with development environments:
|
||||||
|
|
||||||
|
- **Preserves existing SSH agent state** during initialization
|
||||||
|
- **Non-destructive operations** that don't interfere with existing workflows
|
||||||
|
- **Shell profile integration** for automatic initialization
|
||||||
|
- **Compatible with existing SSH configurations**
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Multiple SSH agents running**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hero sshagent profile # Will cleanup and ensure single agent
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Keys not loading**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hero sshagent status # Check agent status
|
||||||
|
hero sshagent reset # Reset if needed
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Remote deployment failures**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hero sshagent auth -t user@hostname # Verify connectivity
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Enable debug output for detailed troubleshooting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
HERO_DEBUG=1 hero sshagent profile
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shell Profile Integration
|
||||||
|
|
||||||
|
The `hero sshagent profile` command automatically adds initialization code to your shell profile:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Hero SSH Agent initialization
|
||||||
|
if [ -f "/Users/username/.ssh/hero-agent.sock" ]; then
|
||||||
|
export SSH_AUTH_SOCK="/Users/username/.ssh/hero-agent.sock"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures the SSH agent is available in new shell sessions.
|
||||||
105
examples/sshagent/test_hero_sshagent.vsh
Executable file
105
examples/sshagent/test_hero_sshagent.vsh
Executable file
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||||
|
|
||||||
|
import os
|
||||||
|
import freeflowuniverse.herolib.ui.console
|
||||||
|
|
||||||
|
// Test script for Hero SSH Agent functionality
|
||||||
|
// This script demonstrates the key features of the hero sshagent command
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
console.print_header('🔑 Hero SSH Agent Test Suite')
|
||||||
|
os.execute('${os.dir(os.dir(@FILE))}/cli/compile.vsh')
|
||||||
|
|
||||||
|
hero_bin := '${os.home_dir()}/hero/bin/hero'
|
||||||
|
|
||||||
|
// Check if hero binary exists
|
||||||
|
if !os.exists(hero_bin) {
|
||||||
|
console.print_stderr('Hero binary not found at ${hero_bin}')
|
||||||
|
console.print_stderr('Please compile hero first with: ./cli/compile.vsh')
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.print_green('✓ Hero binary found at ${hero_bin}')
|
||||||
|
|
||||||
|
// Test 1: Profile initialization
|
||||||
|
console.print_header('Test 1: Profile Initialization')
|
||||||
|
result1 := os.execute('${hero_bin} sshagent profile')
|
||||||
|
if result1.exit_code == 0 && result1.output.contains('Hero SSH Agent Profile Initialization') {
|
||||||
|
console.print_green('✓ Profile initialization successful')
|
||||||
|
} else {
|
||||||
|
console.print_stderr('❌ Profile initialization failed: ${result1.output}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: Status check
|
||||||
|
console.print_header('Test 2: Status Check')
|
||||||
|
result2 := os.execute('${hero_bin} sshagent status')
|
||||||
|
if result2.exit_code == 0 && result2.output.contains("- SSH Agent Status") {
|
||||||
|
console.print_green('✓ Status check successful')
|
||||||
|
println(result2.output)
|
||||||
|
} else {
|
||||||
|
console.print_stderr('❌ Status check failed: ${result2.output}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: List keys
|
||||||
|
console.print_header('Test 3: List SSH Keys')
|
||||||
|
result3 := os.execute('${hero_bin} sshagent list')
|
||||||
|
if result3.exit_code == 0 && result3.output.contains('SSH Keys Status') {
|
||||||
|
console.print_green('✓ List keys successful')
|
||||||
|
println(result3.output)
|
||||||
|
} else {
|
||||||
|
console.print_stderr('❌ List keys failed: ${result3.output}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 4: Generate test key
|
||||||
|
console.print_header('Test 4: Generate Test Key')
|
||||||
|
test_key_name := 'hero_test_${os.getpid()}'
|
||||||
|
result4 := os.execute('${hero_bin} sshagent generate -n ${test_key_name}')
|
||||||
|
if result4.exit_code == 0 && result4.output.contains('Generating SSH key') {
|
||||||
|
console.print_green('✓ Key generation successful')
|
||||||
|
println(result4.output)
|
||||||
|
|
||||||
|
// Cleanup: remove test key files
|
||||||
|
test_key_path := '${os.home_dir()}/.ssh/${test_key_name}'
|
||||||
|
test_pub_path := '${test_key_path}.pub'
|
||||||
|
|
||||||
|
if os.exists(test_key_path) {
|
||||||
|
os.rm(test_key_path) or {}
|
||||||
|
console.print_debug('Cleaned up test private key')
|
||||||
|
}
|
||||||
|
if os.exists(test_pub_path) {
|
||||||
|
os.rm(test_pub_path) or {}
|
||||||
|
console.print_debug('Cleaned up test public key')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.print_stderr('❌ Key generation failed: ${result4.output}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 5: Help output
|
||||||
|
console.print_header('Test 5: Help Output')
|
||||||
|
result5 := os.execute('${hero_bin} sshagent')
|
||||||
|
if result5.exit_code == 1 && result5.output.contains('Hero SSH Agent Management Tool') {
|
||||||
|
console.print_green('✓ Help output is correct')
|
||||||
|
} else {
|
||||||
|
console.print_stderr('❌ Help output unexpected')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.print_header('🎉 Test Suite Complete')
|
||||||
|
console.print_green('Hero SSH Agent is ready for use!')
|
||||||
|
|
||||||
|
// Show usage examples
|
||||||
|
console.print_header('Usage Examples:')
|
||||||
|
println('')
|
||||||
|
println('Initialize SSH agent:')
|
||||||
|
println(' ${hero_bin} sshagent profile')
|
||||||
|
println('')
|
||||||
|
println('Check status:')
|
||||||
|
println(' ${hero_bin} sshagent status')
|
||||||
|
println('')
|
||||||
|
println('Deploy key to remote server:')
|
||||||
|
println(' ${hero_bin} sshagent push -t user@server.com')
|
||||||
|
println('')
|
||||||
|
println('Verify authorization:')
|
||||||
|
println(' ${hero_bin} sshagent auth -t user@server.com')
|
||||||
|
println('')
|
||||||
|
println('For more examples, see: examples/sshagent/hero_sshagent_examples.md')
|
||||||
|
}
|
||||||
389
examples/tmux/server_dashboard.vsh
Executable file
389
examples/tmux/server_dashboard.vsh
Executable file
@@ -0,0 +1,389 @@
|
|||||||
|
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||||
|
|
||||||
|
import freeflowuniverse.herolib.osal.tmux
|
||||||
|
import freeflowuniverse.herolib.osal.core as osal
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const session_name = 'server_dashboard'
|
||||||
|
const window_name = 'dashboard'
|
||||||
|
const python_port = 8000
|
||||||
|
const ttyd_port = 7890
|
||||||
|
|
||||||
|
// Command line argument handling
|
||||||
|
fn show_help() {
|
||||||
|
println('=== Tmux Server Dashboard ===')
|
||||||
|
println('Usage:')
|
||||||
|
println(' ${os.args[0]} # Start the dashboard')
|
||||||
|
println(' ${os.args[0]} -editable # Start dashboard with editable ttyd')
|
||||||
|
println(' ${os.args[0]} -down # Stop dashboard and cleanup')
|
||||||
|
println(' ${os.args[0]} -status # Show dashboard status')
|
||||||
|
println(' ${os.args[0]} -restart # Restart the dashboard')
|
||||||
|
println(' ${os.args[0]} -help # Show this help')
|
||||||
|
println('')
|
||||||
|
println('Dashboard includes:')
|
||||||
|
println(' • Python HTTP Server (port ${python_port})')
|
||||||
|
println(' • Counter service (updates every 5 seconds)')
|
||||||
|
println(' • Hero Web (compile and run hero web server)')
|
||||||
|
println(' • CPU Monitor (htop)')
|
||||||
|
println(' • Web access via ttyd (port ${ttyd_port})')
|
||||||
|
println('')
|
||||||
|
println('ttyd modes:')
|
||||||
|
println(' • Default: read-only access to terminal')
|
||||||
|
println(' • -editable: allows writing/editing in the terminal')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop_dashboard() ! {
|
||||||
|
println('=== Stopping Dashboard ===')
|
||||||
|
|
||||||
|
// Kill ttyd processes
|
||||||
|
println('Stopping ttyd processes...')
|
||||||
|
os.execute('pkill ttyd')
|
||||||
|
|
||||||
|
// Kill tmux session
|
||||||
|
println('Stopping tmux session...')
|
||||||
|
mut t := tmux.new()!
|
||||||
|
if t.session_exist(session_name) {
|
||||||
|
mut session := t.session_get(session_name)!
|
||||||
|
session.stop()!
|
||||||
|
println('✓ Tmux session "${session_name}" stopped')
|
||||||
|
} else {
|
||||||
|
println('• Session "${session_name}" not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for any remaining processes on our ports
|
||||||
|
println('Checking for processes on ports...')
|
||||||
|
|
||||||
|
// Check Python server port
|
||||||
|
python_check := os.execute('lsof -i :${python_port}')
|
||||||
|
if python_check.exit_code == 0 {
|
||||||
|
println('• Found processes on port ${python_port}')
|
||||||
|
println(python_check.output)
|
||||||
|
} else {
|
||||||
|
println('✓ Port ${python_port} is free')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ttyd port
|
||||||
|
ttyd_check := os.execute('lsof -i :${ttyd_port}')
|
||||||
|
if ttyd_check.exit_code == 0 {
|
||||||
|
println('• Found processes on port ${ttyd_port}')
|
||||||
|
println(ttyd_check.output)
|
||||||
|
} else {
|
||||||
|
println('✓ Port ${ttyd_port} is free')
|
||||||
|
}
|
||||||
|
|
||||||
|
println('=== Dashboard stopped ===')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_status() ! {
|
||||||
|
println('=== Dashboard Status ===')
|
||||||
|
|
||||||
|
mut t := tmux.new()!
|
||||||
|
|
||||||
|
// Check tmux session
|
||||||
|
if t.session_exist(session_name) {
|
||||||
|
println('✓ Tmux session "${session_name}" is running')
|
||||||
|
|
||||||
|
mut session := t.session_get(session_name)!
|
||||||
|
mut window := session.window_get(name: window_name) or {
|
||||||
|
println('✗ Window "${window_name}" not found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
println('✓ Window "${window_name}" exists with ${window.panes.len} panes')
|
||||||
|
|
||||||
|
// Show pane details
|
||||||
|
for i, pane in window.panes {
|
||||||
|
service_name := match i {
|
||||||
|
0 { 'Python HTTP Server' }
|
||||||
|
1 { 'Counter Service' }
|
||||||
|
2 { 'Hero Web Service' }
|
||||||
|
3 { 'CPU Monitor' }
|
||||||
|
else { 'Service ${i + 1}' }
|
||||||
|
}
|
||||||
|
|
||||||
|
mut pane_mut := pane
|
||||||
|
stats := pane_mut.stats() or {
|
||||||
|
println(' Pane ${i + 1} (${service_name}): ID=%${pane.id}, PID=${pane.pid} (stats unavailable)')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
memory_mb := f64(stats.memory_bytes) / (1024.0 * 1024.0)
|
||||||
|
println(' Pane ${i + 1} (${service_name}): ID=%${pane.id}, CPU=${stats.cpu_percent:.1f}%, Memory=${memory_mb:.1f}MB')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println('✗ Tmux session "${session_name}" not running')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ports
|
||||||
|
python_check := os.execute('lsof -i :${python_port}')
|
||||||
|
if python_check.exit_code == 0 {
|
||||||
|
println('✓ Python server running on port ${python_port}')
|
||||||
|
} else {
|
||||||
|
println('✗ No process on port ${python_port}')
|
||||||
|
}
|
||||||
|
|
||||||
|
ttyd_check := os.execute('lsof -i :${ttyd_port}')
|
||||||
|
if ttyd_check.exit_code == 0 {
|
||||||
|
println('✓ ttyd running on port ${ttyd_port}')
|
||||||
|
} else {
|
||||||
|
println('✗ No process on port ${ttyd_port}')
|
||||||
|
}
|
||||||
|
|
||||||
|
println('')
|
||||||
|
println('Access URLs:')
|
||||||
|
println(' • Python Server: http://localhost:${python_port}')
|
||||||
|
println(' • Web Terminal: http://localhost:${ttyd_port}')
|
||||||
|
println(' • Tmux attach: tmux attach-session -t ${session_name}')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restart_dashboard() ! {
|
||||||
|
println('=== Restarting Dashboard ===')
|
||||||
|
stop_dashboard()!
|
||||||
|
time.sleep(2000 * time.millisecond) // Wait 2 seconds
|
||||||
|
start_dashboard()!
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_dashboard_with_mode(ttyd_editable bool) ! {
|
||||||
|
println('=== Server Dashboard with 4 Panes ===')
|
||||||
|
println('Setting up tmux session with:')
|
||||||
|
println(' 1. Python HTTP Server (port ${python_port})')
|
||||||
|
println(' 2. Counter Service (updates every 5 seconds)')
|
||||||
|
println(' 3. Hero Web (compile and run hero web server)')
|
||||||
|
println(' 4. CPU Monitor (htop)')
|
||||||
|
println('')
|
||||||
|
|
||||||
|
// Initialize tmux
|
||||||
|
mut t := tmux.new()!
|
||||||
|
|
||||||
|
if !t.is_running()! {
|
||||||
|
println('Starting tmux server...')
|
||||||
|
t.start()!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up existing session if it exists
|
||||||
|
if t.session_exist(session_name) {
|
||||||
|
println('Cleaning up existing ${session_name} session...')
|
||||||
|
t.session_delete(session_name)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new session
|
||||||
|
println('Creating ${session_name} session...')
|
||||||
|
mut session := t.session_create(name: session_name)!
|
||||||
|
|
||||||
|
// Create main window with initial bash shell
|
||||||
|
println('Creating dashboard window...')
|
||||||
|
mut window := session.window_new(name: window_name, cmd: 'bash', reset: true)!
|
||||||
|
|
||||||
|
// Wait for initial setup
|
||||||
|
time.sleep(500 * time.millisecond)
|
||||||
|
t.scan()!
|
||||||
|
|
||||||
|
println('\n=== Setting up 4-pane layout ===')
|
||||||
|
|
||||||
|
// Get the main window
|
||||||
|
window = session.window_get(name: window_name)!
|
||||||
|
|
||||||
|
// Split horizontally first (left and right halves)
|
||||||
|
println('1. Splitting horizontally for left/right layout...')
|
||||||
|
mut right_pane := window.pane_split_horizontal('bash')!
|
||||||
|
time.sleep(300 * time.millisecond)
|
||||||
|
window.scan()!
|
||||||
|
|
||||||
|
// Split left pane vertically (top-left and bottom-left)
|
||||||
|
println('2. Splitting left pane vertically...')
|
||||||
|
window.scan()!
|
||||||
|
if window.panes.len >= 2 {
|
||||||
|
mut left_pane := window.panes[0] // First pane should be the left one
|
||||||
|
left_pane.select()!
|
||||||
|
time.sleep(200 * time.millisecond)
|
||||||
|
mut bottom_left_pane := window.pane_split_vertical('bash')!
|
||||||
|
time.sleep(300 * time.millisecond)
|
||||||
|
window.scan()!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split right pane vertically (top-right and bottom-right)
|
||||||
|
println('3. Splitting right pane vertically...')
|
||||||
|
window.scan()!
|
||||||
|
if window.panes.len >= 3 {
|
||||||
|
// Find the rightmost pane (should be the last one after horizontal split)
|
||||||
|
mut right_pane_current := window.panes[window.panes.len - 1]
|
||||||
|
right_pane_current.select()!
|
||||||
|
time.sleep(200 * time.millisecond)
|
||||||
|
mut bottom_right_pane := window.pane_split_vertical('bash')!
|
||||||
|
time.sleep(300 * time.millisecond)
|
||||||
|
window.scan()!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a proper 2x2 tiled layout using tmux command
|
||||||
|
println('4. Setting 2x2 tiled layout...')
|
||||||
|
os.execute('tmux select-layout -t ${session_name}:${window_name} tiled')
|
||||||
|
time.sleep(500 * time.millisecond)
|
||||||
|
window.scan()!
|
||||||
|
|
||||||
|
println('5. Layout complete! We now have 4 panes in 2x2 grid.')
|
||||||
|
|
||||||
|
// Refresh to get all panes
|
||||||
|
window.scan()!
|
||||||
|
println('\nCurrent panes: ${window.panes.len}')
|
||||||
|
for i, pane in window.panes {
|
||||||
|
println(' Pane ${i}: ID=%${pane.id}, PID=${pane.pid}')
|
||||||
|
}
|
||||||
|
|
||||||
|
if window.panes.len < 4 {
|
||||||
|
eprintln('Expected 4 panes, but got ${window.panes.len}')
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
println('\n=== Starting services in each pane ===')
|
||||||
|
|
||||||
|
// Pane 1 (top-left): Python HTTP Server
|
||||||
|
println('Starting Python HTTP Server in pane 1...')
|
||||||
|
mut pane1 := window.panes[0]
|
||||||
|
pane1.select()!
|
||||||
|
pane1.send_command('echo "=== Python HTTP Server Port 8000 ==="')!
|
||||||
|
pane1.send_command('cd /tmp && python3 -m http.server ${python_port}')!
|
||||||
|
|
||||||
|
time.sleep(500 * time.millisecond)
|
||||||
|
|
||||||
|
// Pane 2 (bottom-left): Counter Service
|
||||||
|
println('Starting Counter Service in pane 2...')
|
||||||
|
mut pane2 := window.panes[1]
|
||||||
|
pane2.select()!
|
||||||
|
pane2.send_command('echo "=== Counter Service - Updates every 5 seconds ==="')!
|
||||||
|
pane2.send_command('while true; do echo "Count: $(date)"; sleep 5; done')!
|
||||||
|
|
||||||
|
time.sleep(500 * time.millisecond)
|
||||||
|
|
||||||
|
// Pane 3 (top-right): Hero Web
|
||||||
|
println('Starting Hero Web in pane 3...')
|
||||||
|
mut pane3 := window.panes[2]
|
||||||
|
pane3.select()!
|
||||||
|
pane3.send_command('echo "=== Hero Web Server ==="')!
|
||||||
|
pane3.send_command('hero web')!
|
||||||
|
|
||||||
|
time.sleep(500 * time.millisecond)
|
||||||
|
|
||||||
|
// Pane 4 (bottom-right): CPU Monitor
|
||||||
|
println('Starting CPU Monitor in pane 4...')
|
||||||
|
mut pane4 := window.panes[3]
|
||||||
|
pane4.select()!
|
||||||
|
pane4.send_command('echo "=== CPU Monitor ==="')!
|
||||||
|
pane4.send_command('htop')!
|
||||||
|
|
||||||
|
println('\n=== All services started! ===')
|
||||||
|
|
||||||
|
// Wait a moment for services to initialize
|
||||||
|
time.sleep(2000 * time.millisecond)
|
||||||
|
|
||||||
|
// Refresh and show current state
|
||||||
|
t.scan()!
|
||||||
|
window = session.window_get(name: window_name)!
|
||||||
|
|
||||||
|
println('\n=== Current Dashboard State ===')
|
||||||
|
for i, mut pane in window.panes {
|
||||||
|
stats := pane.stats() or {
|
||||||
|
println(' Pane ${i + 1}: ID=%${pane.id}, PID=${pane.pid} (stats unavailable)')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
memory_mb := f64(stats.memory_bytes) / (1024.0 * 1024.0)
|
||||||
|
service_name := match i {
|
||||||
|
0 { 'Python Server' }
|
||||||
|
1 { 'Counter Service' }
|
||||||
|
2 { 'Hero Web' }
|
||||||
|
3 { 'CPU Monitor' }
|
||||||
|
else { 'Unknown' }
|
||||||
|
}
|
||||||
|
println(' Pane ${i + 1} (${service_name}): ID=%${pane.id}, CPU=${stats.cpu_percent:.1f}%, Memory=${memory_mb:.1f}MB')
|
||||||
|
}
|
||||||
|
|
||||||
|
println('\n=== Access Information ===')
|
||||||
|
println('• Python HTTP Server: http://localhost:${python_port}')
|
||||||
|
println('• Tmux Session: tmux attach-session -t ${session_name}')
|
||||||
|
println('')
|
||||||
|
println('=== Pane Resize Commands ===')
|
||||||
|
println('To resize panes, attach to the session and use:')
|
||||||
|
println(' Ctrl+B then Arrow Keys (hold Ctrl+B and press arrow keys)')
|
||||||
|
println(' Or programmatically:')
|
||||||
|
for i, pane in window.panes {
|
||||||
|
service_name := match i {
|
||||||
|
0 { 'Python Server' }
|
||||||
|
1 { 'Counter Service' }
|
||||||
|
2 { 'Hero Web' }
|
||||||
|
3 { 'CPU Monitor' }
|
||||||
|
else { 'Unknown' }
|
||||||
|
}
|
||||||
|
println(' # Resize ${service_name} pane:')
|
||||||
|
println(' tmux resize-pane -t ${session_name}:${window_name}.%${pane.id} -U 5 # Up')
|
||||||
|
println(' tmux resize-pane -t ${session_name}:${window_name}.%${pane.id} -D 5 # Down')
|
||||||
|
println(' tmux resize-pane -t ${session_name}:${window_name}.%${pane.id} -L 5 # Left')
|
||||||
|
println(' tmux resize-pane -t ${session_name}:${window_name}.%${pane.id} -R 5 # Right')
|
||||||
|
}
|
||||||
|
|
||||||
|
println('\n=== Dashboard is running! ===')
|
||||||
|
println('Attach to view: tmux attach-session -t ${session_name}')
|
||||||
|
println('Press Ctrl+B then d to detach from session')
|
||||||
|
println('To stop all services: tmux kill-session -t ${session_name}')
|
||||||
|
println('Running the browser-based dashboard: TTYD')
|
||||||
|
|
||||||
|
mode_str := if ttyd_editable { 'editable' } else { 'read-only' }
|
||||||
|
println('Starting ttyd in ${mode_str} mode...')
|
||||||
|
|
||||||
|
window.run_ttyd(port: ttyd_port, editable: ttyd_editable) or {
|
||||||
|
println('Failed to start ttyd: ${err}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_dashboard() ! {
|
||||||
|
start_dashboard_with_mode(false)!
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
mut ttyd_editable := false // Local flag for ttyd editable mode
|
||||||
|
|
||||||
|
// Main execution with argument handling
|
||||||
|
if os.args.len > 1 {
|
||||||
|
command := os.args[1]
|
||||||
|
match command {
|
||||||
|
'-editable' {
|
||||||
|
ttyd_editable = true
|
||||||
|
start_dashboard_with_mode(ttyd_editable) or {
|
||||||
|
eprintln('Error starting dashboard: ${err}')
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'-down' {
|
||||||
|
stop_dashboard() or {
|
||||||
|
eprintln('Error stopping dashboard: ${err}')
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'-status' {
|
||||||
|
show_status() or {
|
||||||
|
eprintln('Error getting status: ${err}')
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'-restart' {
|
||||||
|
restart_dashboard() or {
|
||||||
|
eprintln('Error restarting dashboard: ${err}')
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'-help', '--help', '-h' {
|
||||||
|
show_help()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
eprintln('Unknown command: ${command}')
|
||||||
|
show_help()
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No arguments - start the dashboard
|
||||||
|
start_dashboard_with_mode(ttyd_editable) or {
|
||||||
|
eprintln('Error starting dashboard: ${err}')
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
120
examples/tmux/tmux.vsh
Executable file
120
examples/tmux/tmux.vsh
Executable file
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||||
|
|
||||||
|
import freeflowuniverse.herolib.osal.tmux
|
||||||
|
import freeflowuniverse.herolib.osal.core as osal
|
||||||
|
import time
|
||||||
|
|
||||||
|
// Constants for display formatting
|
||||||
|
const bytes_to_mb = 1024.0 * 1024.0
|
||||||
|
const cpu_precision = 1
|
||||||
|
const memory_precision = 3
|
||||||
|
|
||||||
|
println('=== Tmux Pane Example ===')
|
||||||
|
|
||||||
|
mut t := tmux.new()!
|
||||||
|
|
||||||
|
if !t.is_running()! {
|
||||||
|
println('Starting tmux server...')
|
||||||
|
t.start()!
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.session_exist('demo') {
|
||||||
|
println('Deleting existing demo session...')
|
||||||
|
t.session_delete('demo')!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create session and window
|
||||||
|
println('Creating demo session...')
|
||||||
|
mut session := t.session_create(name: 'demo')!
|
||||||
|
|
||||||
|
println('Creating main window with htop...')
|
||||||
|
mut window := session.window_new(name: 'main', cmd: 'htop', reset: true)!
|
||||||
|
|
||||||
|
// Wait a moment for the window to be created
|
||||||
|
time.sleep(500 * time.millisecond)
|
||||||
|
|
||||||
|
// Refresh to get current state
|
||||||
|
t.scan()!
|
||||||
|
|
||||||
|
println('\n=== Current Tmux State ===')
|
||||||
|
println(t)
|
||||||
|
|
||||||
|
// Get the window and demonstrate pane functionality
|
||||||
|
mut main_window := session.window_get(name: 'main')!
|
||||||
|
|
||||||
|
println('\n=== Window Pane Information ===')
|
||||||
|
println('Window: ${main_window.name} (ID: ${main_window.id})')
|
||||||
|
println('Number of panes: ${main_window.panes.len}')
|
||||||
|
|
||||||
|
for i, mut pane in main_window.panes {
|
||||||
|
println('Pane ${i}: ID=%${pane.id}, PID=${pane.pid}, Active=${pane.active}, Cmd="${pane.cmd}"')
|
||||||
|
|
||||||
|
// Get pane stats
|
||||||
|
stats := pane.stats() or {
|
||||||
|
println(' Could not get stats: ${err}')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
memory_mb := f64(stats.memory_bytes) / bytes_to_mb
|
||||||
|
println(' CPU: ${stats.cpu_percent:.1f}%, Memory: ${stats.memory_percent:.3f}% (${memory_mb:.1f} MB)')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the active pane
|
||||||
|
if mut active_pane := main_window.pane_active() {
|
||||||
|
println('\n=== Active Pane Details ===')
|
||||||
|
println('Active pane ID: %${active_pane.id}')
|
||||||
|
println('Process ID: ${active_pane.pid}')
|
||||||
|
println('Command: ${active_pane.cmd}')
|
||||||
|
|
||||||
|
// Get process information
|
||||||
|
process_info := active_pane.processinfo_main() or {
|
||||||
|
println('Could not get process info: ${err}')
|
||||||
|
osal.ProcessInfo{}
|
||||||
|
}
|
||||||
|
if process_info.pid > 0 {
|
||||||
|
println('Process info: PID=${process_info.pid}, Command=${process_info.cmd}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get recent logs
|
||||||
|
println('\n=== Recent Pane Output ===')
|
||||||
|
logs := active_pane.logs_all() or {
|
||||||
|
println('Could not get logs: ${err}')
|
||||||
|
''
|
||||||
|
}
|
||||||
|
if logs.len > 0 {
|
||||||
|
lines := logs.split_into_lines()
|
||||||
|
// Show last 5 lines
|
||||||
|
start_idx := if lines.len > 5 { lines.len - 5 } else { 0 }
|
||||||
|
for i in start_idx .. lines.len {
|
||||||
|
if lines[i].trim_space().len > 0 {
|
||||||
|
println(' ${lines[i]}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println('No active pane found')
|
||||||
|
}
|
||||||
|
|
||||||
|
println('\n=== Creating Additional Windows ===')
|
||||||
|
|
||||||
|
// Create more windows to demonstrate multiple panes
|
||||||
|
mut monitor_window := session.window_new(name: 'monitor', cmd: 'top', reset: true)!
|
||||||
|
mut logs_window := session.window_new(name: 'logs', cmd: 'tail -f /var/log/system.log', reset: true)!
|
||||||
|
|
||||||
|
time.sleep(500 * time.millisecond)
|
||||||
|
t.scan()!
|
||||||
|
|
||||||
|
println('\n=== Final Tmux State ===')
|
||||||
|
println(t)
|
||||||
|
|
||||||
|
println('\n=== Window Statistics ===')
|
||||||
|
for mut win in session.windows {
|
||||||
|
println('Window: ${win.name}')
|
||||||
|
window_stats := win.stats() or {
|
||||||
|
println(' Could not get window stats: ${err}')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
memory_mb := f64(window_stats.memory_bytes) / bytes_to_mb
|
||||||
|
println(' Total CPU: ${window_stats.cpu_percent:.1f}%')
|
||||||
|
println(' Total Memory: ${window_stats.memory_percent:.3f}% (${memory_mb:.1f} MB)')
|
||||||
|
println(' Panes: ${win.panes.len}')
|
||||||
|
}
|
||||||
143
examples/tmux/tmux_pane_resize.vsh
Executable file
143
examples/tmux/tmux_pane_resize.vsh
Executable file
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||||
|
|
||||||
|
import freeflowuniverse.herolib.osal.tmux
|
||||||
|
import time
|
||||||
|
|
||||||
|
println('=== Tmux Pane Resizing Example ===')
|
||||||
|
|
||||||
|
mut t := tmux.new()!
|
||||||
|
|
||||||
|
if !t.is_running()! {
|
||||||
|
println('Starting tmux server...')
|
||||||
|
t.start()!
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.session_exist('resize_demo') {
|
||||||
|
println('Deleting existing resize_demo session...')
|
||||||
|
t.session_delete('resize_demo')!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create session and window
|
||||||
|
println('Creating resize_demo session...')
|
||||||
|
mut session := t.session_create(name: 'resize_demo')!
|
||||||
|
|
||||||
|
println('Creating main window...')
|
||||||
|
mut window := session.window_new(name: 'main', cmd: 'bash', reset: true)!
|
||||||
|
|
||||||
|
time.sleep(500 * time.millisecond)
|
||||||
|
t.scan()!
|
||||||
|
|
||||||
|
// Create a 2x2 grid of panes
|
||||||
|
println('\n=== Creating 2x2 Grid of Panes ===')
|
||||||
|
|
||||||
|
// Split horizontally first (left | right)
|
||||||
|
mut right_pane := window.pane_split_horizontal('htop')!
|
||||||
|
time.sleep(300 * time.millisecond)
|
||||||
|
|
||||||
|
// Split left pane vertically (top-left, bottom-left)
|
||||||
|
window.scan()!
|
||||||
|
if window.panes.len > 1 {
|
||||||
|
mut left_pane := window.panes[1] // The original bash pane
|
||||||
|
left_pane.select()!
|
||||||
|
time.sleep(200 * time.millisecond)
|
||||||
|
}
|
||||||
|
mut bottom_left_pane := window.pane_split_vertical('top')!
|
||||||
|
time.sleep(300 * time.millisecond)
|
||||||
|
|
||||||
|
// Split right pane vertically (top-right, bottom-right)
|
||||||
|
window.scan()!
|
||||||
|
for mut pane in window.panes {
|
||||||
|
if pane.cmd.contains('htop') {
|
||||||
|
pane.select()!
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.sleep(200 * time.millisecond)
|
||||||
|
mut bottom_right_pane := window.pane_split_vertical('tail -f /var/log/system.log')!
|
||||||
|
time.sleep(500 * time.millisecond)
|
||||||
|
|
||||||
|
window.scan()!
|
||||||
|
println('Created 2x2 grid with ${window.panes.len} panes:')
|
||||||
|
for i, pane in window.panes {
|
||||||
|
println(' Pane ${i}: ID=%${pane.id}, Cmd="${pane.cmd}"')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demonstrate resizing operations
|
||||||
|
println('\n=== Demonstrating Pane Resizing ===')
|
||||||
|
|
||||||
|
// Get references to panes for resizing
|
||||||
|
window.scan()!
|
||||||
|
if window.panes.len >= 4 {
|
||||||
|
mut top_left := window.panes[1] // bash
|
||||||
|
mut top_right := window.panes[0] // htop
|
||||||
|
mut bottom_left := window.panes[2] // top
|
||||||
|
mut bottom_right := window.panes[3] // tail
|
||||||
|
|
||||||
|
println('Resizing top-left pane (bash) to be wider...')
|
||||||
|
top_left.select()!
|
||||||
|
time.sleep(200 * time.millisecond)
|
||||||
|
top_left.resize_right(10)!
|
||||||
|
time.sleep(1000 * time.millisecond)
|
||||||
|
|
||||||
|
println('Resizing top-right pane (htop) to be taller...')
|
||||||
|
top_right.select()!
|
||||||
|
time.sleep(200 * time.millisecond)
|
||||||
|
top_right.resize_down(5)!
|
||||||
|
time.sleep(1000 * time.millisecond)
|
||||||
|
|
||||||
|
println('Resizing bottom-left pane (top) to be narrower...')
|
||||||
|
bottom_left.select()!
|
||||||
|
time.sleep(200 * time.millisecond)
|
||||||
|
bottom_left.resize_left(5)!
|
||||||
|
time.sleep(1000 * time.millisecond)
|
||||||
|
|
||||||
|
println('Resizing bottom-right pane (tail) to be shorter...')
|
||||||
|
bottom_right.select()!
|
||||||
|
time.sleep(200 * time.millisecond)
|
||||||
|
bottom_right.resize_up(3)!
|
||||||
|
time.sleep(1000 * time.millisecond)
|
||||||
|
|
||||||
|
// Demonstrate using the generic resize method
|
||||||
|
println('Using generic resize method to make top-left pane taller...')
|
||||||
|
top_left.select()!
|
||||||
|
time.sleep(200 * time.millisecond)
|
||||||
|
top_left.resize(direction: 'down', cells: 3)!
|
||||||
|
time.sleep(1000 * time.millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send some commands to make the panes more interesting
|
||||||
|
println('\n=== Adding Content to Panes ===')
|
||||||
|
window.scan()!
|
||||||
|
if window.panes.len >= 4 {
|
||||||
|
// Send commands to bash pane
|
||||||
|
mut bash_pane := window.panes[1]
|
||||||
|
bash_pane.send_command('echo "=== Bash Pane ==="')!
|
||||||
|
bash_pane.send_command('ls -la')!
|
||||||
|
bash_pane.send_command('pwd')!
|
||||||
|
time.sleep(500 * time.millisecond)
|
||||||
|
|
||||||
|
// Send command to top pane
|
||||||
|
mut top_pane := window.panes[2]
|
||||||
|
top_pane.send_command('echo "=== Top Pane ==="')!
|
||||||
|
time.sleep(500 * time.millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
println('\n=== Final Layout ===')
|
||||||
|
t.scan()!
|
||||||
|
println('Session: ${session.name}')
|
||||||
|
println('Window: ${window.name} (${window.panes.len} panes)')
|
||||||
|
for i, pane in window.panes {
|
||||||
|
println(' ${i + 1}. Pane %${pane.id} - ${pane.cmd}')
|
||||||
|
}
|
||||||
|
|
||||||
|
println('\n=== Pane Resize Operations Available ===')
|
||||||
|
println('✓ resize_up(cells) - Make pane taller by shrinking pane above')
|
||||||
|
println('✓ resize_down(cells) - Make pane taller by shrinking pane below')
|
||||||
|
println('✓ resize_left(cells) - Make pane wider by shrinking pane to the left')
|
||||||
|
println('✓ resize_right(cells) - Make pane wider by shrinking pane to the right')
|
||||||
|
println('✓ resize(direction: "up/down/left/right", cells: N) - Generic resize method')
|
||||||
|
|
||||||
|
println('\nExample completed! You can attach to the session with:')
|
||||||
|
println(' tmux attach-session -t resize_demo')
|
||||||
|
println('\nThen use Ctrl+B followed by arrow keys to manually resize panes,')
|
||||||
|
println('or Ctrl+B followed by Alt+arrow keys for larger resize steps.')
|
||||||
170
examples/tmux/tmux_panes.vsh
Executable file
170
examples/tmux/tmux_panes.vsh
Executable file
@@ -0,0 +1,170 @@
|
|||||||
|
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||||
|
|
||||||
|
import freeflowuniverse.herolib.osal.tmux
|
||||||
|
import time
|
||||||
|
|
||||||
|
println('=== Tmux Pane Splitting Example ===')
|
||||||
|
|
||||||
|
mut t := tmux.new()!
|
||||||
|
|
||||||
|
if !t.is_running()! {
|
||||||
|
println('Starting tmux server...')
|
||||||
|
t.start()!
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.session_exist('panes_demo') {
|
||||||
|
println('Deleting existing panes_demo session...')
|
||||||
|
t.session_delete('panes_demo')!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create session and initial window
|
||||||
|
println('Creating panes_demo session...')
|
||||||
|
mut session := t.session_create(name: 'panes_demo')!
|
||||||
|
|
||||||
|
println('Creating main window...')
|
||||||
|
mut window := session.window_new(name: 'main', cmd: 'bash', reset: true)!
|
||||||
|
|
||||||
|
// Wait for initial setup
|
||||||
|
time.sleep(500 * time.millisecond)
|
||||||
|
t.scan()!
|
||||||
|
|
||||||
|
println('\n=== Initial State ===')
|
||||||
|
println('Window: ${window.name} (ID: ${window.id})')
|
||||||
|
println('Number of panes: ${window.panes.len}')
|
||||||
|
|
||||||
|
// Split the window horizontally (side by side)
|
||||||
|
println('\n=== Splitting Horizontally (Side by Side) ===')
|
||||||
|
mut right_pane := window.pane_split_horizontal('htop')!
|
||||||
|
time.sleep(500 * time.millisecond)
|
||||||
|
window.scan()!
|
||||||
|
|
||||||
|
println('After horizontal split:')
|
||||||
|
println('Number of panes: ${window.panes.len}')
|
||||||
|
for i, mut pane in window.panes {
|
||||||
|
println(' Pane ${i}: ID=%${pane.id}, PID=${pane.pid}, Active=${pane.active}, Cmd="${pane.cmd}"')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the right pane vertically (top and bottom)
|
||||||
|
println('\n=== Splitting Right Pane Vertically (Top and Bottom) ===')
|
||||||
|
// Get a fresh reference to the right pane after the first split
|
||||||
|
window.scan()!
|
||||||
|
if window.panes.len > 0 {
|
||||||
|
// Find the pane with htop command (the one we just created)
|
||||||
|
mut right_pane_fresh := &window.panes[0]
|
||||||
|
for mut pane in window.panes {
|
||||||
|
if pane.cmd.contains('htop') {
|
||||||
|
right_pane_fresh = pane
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select the right pane to make it active
|
||||||
|
right_pane_fresh.select()!
|
||||||
|
time.sleep(200 * time.millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
mut bottom_pane := window.pane_split_vertical('top')!
|
||||||
|
time.sleep(500 * time.millisecond)
|
||||||
|
window.scan()!
|
||||||
|
|
||||||
|
println('After vertical split of right pane:')
|
||||||
|
println('Number of panes: ${window.panes.len}')
|
||||||
|
for i, mut pane in window.panes {
|
||||||
|
println(' Pane ${i}: ID=%${pane.id}, PID=${pane.pid}, Active=${pane.active}, Cmd="${pane.cmd}"')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send commands to different panes
|
||||||
|
println('\n=== Sending Commands to Panes ===')
|
||||||
|
|
||||||
|
// Get the first pane (left side) and send some commands
|
||||||
|
if window.panes.len > 0 {
|
||||||
|
mut left_pane := window.panes[0]
|
||||||
|
println('Sending commands to left pane (ID: %${left_pane.id})')
|
||||||
|
|
||||||
|
left_pane.send_command('echo "Hello from left pane!"')!
|
||||||
|
time.sleep(200 * time.millisecond)
|
||||||
|
|
||||||
|
left_pane.send_command('ls -la')!
|
||||||
|
time.sleep(200 * time.millisecond)
|
||||||
|
|
||||||
|
left_pane.send_command('pwd')!
|
||||||
|
time.sleep(200 * time.millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send command to bottom pane
|
||||||
|
if window.panes.len > 2 {
|
||||||
|
mut bottom_pane_ref := window.panes[2]
|
||||||
|
println('Sending command to bottom pane (ID: %${bottom_pane_ref.id})')
|
||||||
|
bottom_pane_ref.send_command('echo "Hello from bottom pane!"')!
|
||||||
|
time.sleep(200 * time.millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture output from panes
|
||||||
|
println('\n=== Capturing Pane Output ===')
|
||||||
|
for i, mut pane in window.panes {
|
||||||
|
println('Output from Pane ${i} (ID: %${pane.id}):')
|
||||||
|
logs := pane.logs_all() or {
|
||||||
|
println(' Could not get logs: ${err}')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if logs.len > 0 {
|
||||||
|
lines := logs.split_into_lines()
|
||||||
|
// Show last 3 lines
|
||||||
|
start_idx := if lines.len > 3 { lines.len - 3 } else { 0 }
|
||||||
|
for j in start_idx .. lines.len {
|
||||||
|
if lines[j].trim_space().len > 0 {
|
||||||
|
println(' ${lines[j]}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println('')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demonstrate pane selection
|
||||||
|
println('\n=== Demonstrating Pane Selection ===')
|
||||||
|
for i, mut pane in window.panes {
|
||||||
|
println('Selecting pane ${i} (ID: %${pane.id})')
|
||||||
|
pane.select()!
|
||||||
|
time.sleep(300 * time.millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final state
|
||||||
|
println('\n=== Final Tmux State ===')
|
||||||
|
t.scan()!
|
||||||
|
println(t)
|
||||||
|
|
||||||
|
println('\n=== Pane Management Summary ===')
|
||||||
|
println('Created ${window.panes.len} panes in window "${window.name}":')
|
||||||
|
for i, pane in window.panes {
|
||||||
|
println(' ${i + 1}. Pane %${pane.id} - PID: ${pane.pid} - Command: ${pane.cmd}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demonstrate killing a pane
|
||||||
|
println('\n=== Demonstrating Pane Killing ===')
|
||||||
|
if window.panes.len > 2 {
|
||||||
|
mut pane_to_kill := window.panes[2] // Kill the bottom pane
|
||||||
|
println('Killing pane %${pane_to_kill.id} (${pane_to_kill.cmd})')
|
||||||
|
pane_to_kill.kill()!
|
||||||
|
time.sleep(500 * time.millisecond)
|
||||||
|
window.scan()!
|
||||||
|
|
||||||
|
println('After killing pane:')
|
||||||
|
println('Number of panes: ${window.panes.len}')
|
||||||
|
for i, pane in window.panes {
|
||||||
|
println(' Pane ${i}: ID=%${pane.id}, PID=${pane.pid}, Cmd="${pane.cmd}"')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println('\n=== Available Pane Operations ===')
|
||||||
|
println('✓ Split panes horizontally (side by side)')
|
||||||
|
println('✓ Split panes vertically (top and bottom)')
|
||||||
|
println('✓ Send commands to specific panes')
|
||||||
|
println('✓ Send raw keys to panes')
|
||||||
|
println('✓ Select/activate panes')
|
||||||
|
println('✓ Capture pane output')
|
||||||
|
println('✓ Get pane process information')
|
||||||
|
println('✓ Kill individual panes')
|
||||||
|
|
||||||
|
println('\nExample completed! You can attach to the session with:')
|
||||||
|
println(' tmux attach-session -t panes_demo')
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
module base
|
module base
|
||||||
|
|
||||||
import freeflowuniverse.herolib.data.paramsparser
|
import freeflowuniverse.herolib.data.paramsparser
|
||||||
import freeflowuniverse.herolib.ui
|
|
||||||
import freeflowuniverse.herolib.ui.console
|
|
||||||
import crypto.md5
|
|
||||||
|
|
||||||
@[params]
|
@[params]
|
||||||
pub struct ContextConfigArgs {
|
pub struct ContextConfigArgs {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
module herocmds
|
module herocmds
|
||||||
|
|
||||||
|
import os
|
||||||
import freeflowuniverse.herolib.osal.sshagent
|
import freeflowuniverse.herolib.osal.sshagent
|
||||||
import freeflowuniverse.herolib.ui.console
|
import freeflowuniverse.herolib.ui.console
|
||||||
import freeflowuniverse.herolib.ui
|
import freeflowuniverse.herolib.ui
|
||||||
@@ -7,33 +8,88 @@ import cli { Command, Flag }
|
|||||||
|
|
||||||
pub fn cmd_sshagent(mut cmdroot Command) {
|
pub fn cmd_sshagent(mut cmdroot Command) {
|
||||||
mut cmd_run := Command{
|
mut cmd_run := Command{
|
||||||
name: 'sshagent'
|
name: 'sshagent'
|
||||||
description: 'Work with SSHAgent'
|
description: 'Comprehensive SSH Agent Management'
|
||||||
// required_args: 1
|
usage: '
|
||||||
usage: 'sub commands of generate are list, generate, unload, load'
|
Hero SSH Agent Management Tool
|
||||||
|
|
||||||
|
COMMANDS:
|
||||||
|
profile Initialize SSH agent with smart key loading
|
||||||
|
list List available SSH keys
|
||||||
|
generate <name> Generate new SSH key
|
||||||
|
load <name> Load SSH key into agent
|
||||||
|
forget <name> Remove SSH key from agent
|
||||||
|
reset Remove all loaded SSH keys
|
||||||
|
push <target> [key] Deploy SSH key to remote machine
|
||||||
|
auth <target> [key] Verify SSH key authorization
|
||||||
|
status Show SSH agent status
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
hero sshagent profile
|
||||||
|
hero sshagent push user@server.com
|
||||||
|
hero sshagent push user@server.com:2222 my_key
|
||||||
|
hero sshagent auth user@server.com
|
||||||
|
hero sshagent load my_key
|
||||||
|
hero sshagent status
|
||||||
|
|
||||||
|
TARGET FORMAT:
|
||||||
|
user@hostname[:port] # Port defaults to 22
|
||||||
|
'
|
||||||
execute: cmd_sshagent_execute
|
execute: cmd_sshagent_execute
|
||||||
sort_commands: true
|
sort_commands: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Profile command - primary initialization
|
||||||
|
mut sshagent_command_profile := Command{
|
||||||
|
sort_flags: true
|
||||||
|
name: 'profile'
|
||||||
|
execute: cmd_sshagent_execute
|
||||||
|
description: 'Initialize SSH agent with smart key loading and shell integration'
|
||||||
|
}
|
||||||
|
|
||||||
mut sshagent_command_list := Command{
|
mut sshagent_command_list := Command{
|
||||||
sort_flags: true
|
sort_flags: true
|
||||||
name: 'list'
|
name: 'list'
|
||||||
execute: cmd_sshagent_execute
|
execute: cmd_sshagent_execute
|
||||||
description: 'list ssh-keys.'
|
description: 'List available SSH keys and their status'
|
||||||
}
|
}
|
||||||
|
|
||||||
mut sshagent_command_generate := Command{
|
mut sshagent_command_generate := Command{
|
||||||
sort_flags: true
|
sort_flags: true
|
||||||
name: 'generate'
|
name: 'generate'
|
||||||
execute: cmd_sshagent_execute
|
execute: cmd_sshagent_execute
|
||||||
description: 'generate ssh-key.'
|
description: 'Generate new SSH key pair'
|
||||||
}
|
}
|
||||||
|
|
||||||
mut sshagent_command_add := Command{
|
mut sshagent_command_add := Command{
|
||||||
sort_flags: true
|
sort_flags: true
|
||||||
name: 'add'
|
name: 'add'
|
||||||
execute: cmd_sshagent_execute
|
execute: cmd_sshagent_execute
|
||||||
description: 'add a key starting from private key, only works interactive for nows.'
|
description: 'Add existing private key to SSH agent'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status command
|
||||||
|
mut sshagent_command_status := Command{
|
||||||
|
sort_flags: true
|
||||||
|
name: 'status'
|
||||||
|
execute: cmd_sshagent_execute
|
||||||
|
description: 'Show SSH agent status and diagnostics'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push command for remote deployment
|
||||||
|
mut sshagent_command_push := Command{
|
||||||
|
sort_flags: true
|
||||||
|
name: 'push'
|
||||||
|
execute: cmd_sshagent_execute
|
||||||
|
description: 'Deploy SSH key to remote machine'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth command for verification
|
||||||
|
mut sshagent_command_auth := Command{
|
||||||
|
sort_flags: true
|
||||||
|
name: 'auth'
|
||||||
|
execute: cmd_sshagent_execute
|
||||||
|
description: 'Verify SSH key authorization on remote machine'
|
||||||
}
|
}
|
||||||
|
|
||||||
sshagent_command_generate.add_flag(Flag{
|
sshagent_command_generate.add_flag(Flag{
|
||||||
@@ -43,6 +99,37 @@ pub fn cmd_sshagent(mut cmdroot Command) {
|
|||||||
description: 'should key be loaded'
|
description: 'should key be loaded'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Add target flag for push and auth commands
|
||||||
|
sshagent_command_push.add_flag(Flag{
|
||||||
|
flag: .string
|
||||||
|
name: 'target'
|
||||||
|
abbrev: 't'
|
||||||
|
required: true
|
||||||
|
description: 'target in format user@hostname[:port]'
|
||||||
|
})
|
||||||
|
|
||||||
|
sshagent_command_push.add_flag(Flag{
|
||||||
|
flag: .string
|
||||||
|
name: 'key'
|
||||||
|
abbrev: 'k'
|
||||||
|
description: 'specific key name to deploy (optional)'
|
||||||
|
})
|
||||||
|
|
||||||
|
sshagent_command_auth.add_flag(Flag{
|
||||||
|
flag: .string
|
||||||
|
name: 'target'
|
||||||
|
abbrev: 't'
|
||||||
|
required: true
|
||||||
|
description: 'target in format user@hostname[:port]'
|
||||||
|
})
|
||||||
|
|
||||||
|
sshagent_command_auth.add_flag(Flag{
|
||||||
|
flag: .string
|
||||||
|
name: 'key'
|
||||||
|
abbrev: 'k'
|
||||||
|
description: 'specific key name to verify (optional)'
|
||||||
|
})
|
||||||
|
|
||||||
mut sshagent_command_load := Command{
|
mut sshagent_command_load := Command{
|
||||||
sort_flags: true
|
sort_flags: true
|
||||||
name: 'load'
|
name: 'load'
|
||||||
@@ -64,6 +151,7 @@ pub fn cmd_sshagent(mut cmdroot Command) {
|
|||||||
description: 'Reset all keys, means unload them all.'
|
description: 'Reset all keys, means unload them all.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commands that require a name parameter
|
||||||
mut allcmdsref_gen0 := [&sshagent_command_generate, &sshagent_command_load, &sshagent_command_unload,
|
mut allcmdsref_gen0 := [&sshagent_command_generate, &sshagent_command_load, &sshagent_command_unload,
|
||||||
&sshagent_command_reset, &sshagent_command_add]
|
&sshagent_command_reset, &sshagent_command_add]
|
||||||
for mut d in allcmdsref_gen0 {
|
for mut d in allcmdsref_gen0 {
|
||||||
@@ -76,63 +164,486 @@ pub fn cmd_sshagent(mut cmdroot Command) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commands that support script mode
|
||||||
mut allcmdsref_gen := [&sshagent_command_list, &sshagent_command_generate, &sshagent_command_load,
|
mut allcmdsref_gen := [&sshagent_command_list, &sshagent_command_generate, &sshagent_command_load,
|
||||||
&sshagent_command_unload, &sshagent_command_reset]
|
&sshagent_command_unload, &sshagent_command_reset, &sshagent_command_status]
|
||||||
|
|
||||||
for mut c in allcmdsref_gen {
|
for mut c in allcmdsref_gen {
|
||||||
// c.add_flag(Flag{
|
|
||||||
// flag: .bool
|
|
||||||
// name: 'reset'
|
|
||||||
// abbrev: 'r'
|
|
||||||
// description: 'do you want to reset all? Dangerous!'
|
|
||||||
// })
|
|
||||||
c.add_flag(Flag{
|
c.add_flag(Flag{
|
||||||
flag: .bool
|
flag: .bool
|
||||||
name: 'script'
|
name: 'script'
|
||||||
abbrev: 's'
|
abbrev: 's'
|
||||||
description: 'runs non interactive!'
|
description: 'runs non interactive!'
|
||||||
})
|
})
|
||||||
|
|
||||||
cmd_run.add_command(*c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add all commands to the main sshagent command
|
||||||
|
cmd_run.add_command(sshagent_command_profile)
|
||||||
|
cmd_run.add_command(sshagent_command_list)
|
||||||
|
cmd_run.add_command(sshagent_command_generate)
|
||||||
|
cmd_run.add_command(sshagent_command_add)
|
||||||
|
cmd_run.add_command(sshagent_command_load)
|
||||||
|
cmd_run.add_command(sshagent_command_unload)
|
||||||
|
cmd_run.add_command(sshagent_command_reset)
|
||||||
|
cmd_run.add_command(sshagent_command_status)
|
||||||
|
cmd_run.add_command(sshagent_command_push)
|
||||||
|
cmd_run.add_command(sshagent_command_auth)
|
||||||
|
|
||||||
cmdroot.add_command(cmd_run)
|
cmdroot.add_command(cmd_run)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_sshagent_execute(cmd Command) ! {
|
fn cmd_sshagent_execute(cmd Command) ! {
|
||||||
// mut reset := cmd.flags.get_bool('reset') or {false }
|
|
||||||
mut isscript := cmd.flags.get_bool('script') or { false }
|
mut isscript := cmd.flags.get_bool('script') or { false }
|
||||||
mut load := cmd.flags.get_bool('load') or { false }
|
mut load := cmd.flags.get_bool('load') or { false }
|
||||||
mut name := cmd.flags.get_string('name') or { '' }
|
mut name := cmd.flags.get_string('name') or { '' }
|
||||||
|
mut target := cmd.flags.get_string('target') or { '' }
|
||||||
|
mut key := cmd.flags.get_string('key') or { '' }
|
||||||
|
|
||||||
mut agent := sshagent.new()!
|
mut agent := sshagent.new()!
|
||||||
|
|
||||||
if cmd.name == 'list' {
|
match cmd.name {
|
||||||
if !isscript {
|
'profile' {
|
||||||
console.clear()
|
cmd_profile_execute(mut agent, isscript)!
|
||||||
}
|
}
|
||||||
console.print_debug(agent.str())
|
'list' {
|
||||||
} else if cmd.name == 'generate' {
|
cmd_list_execute(mut agent, isscript)!
|
||||||
agent.generate(name, '')!
|
}
|
||||||
if load {
|
'generate' {
|
||||||
agent.load(name)!
|
cmd_generate_execute(mut agent, name, load)!
|
||||||
|
}
|
||||||
|
'load' {
|
||||||
|
cmd_load_execute(mut agent, name)!
|
||||||
|
}
|
||||||
|
'forget' {
|
||||||
|
cmd_forget_execute(mut agent, name)!
|
||||||
|
}
|
||||||
|
'reset' {
|
||||||
|
cmd_reset_execute(mut agent, isscript)!
|
||||||
|
}
|
||||||
|
'add' {
|
||||||
|
cmd_add_execute(mut agent, name)!
|
||||||
|
}
|
||||||
|
'status' {
|
||||||
|
cmd_status_execute(mut agent)!
|
||||||
|
}
|
||||||
|
'push' {
|
||||||
|
cmd_push_execute(mut agent, target, key)!
|
||||||
|
}
|
||||||
|
'auth' {
|
||||||
|
cmd_auth_execute(mut agent, target, key)!
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return error(cmd.help_message())
|
||||||
}
|
}
|
||||||
} else if cmd.name == 'load' {
|
|
||||||
agent.load(name)!
|
|
||||||
} else if cmd.name == 'forget' {
|
|
||||||
agent.forget(name)!
|
|
||||||
} else if cmd.name == 'reset' {
|
|
||||||
agent.reset()!
|
|
||||||
} else if cmd.name == 'add' {
|
|
||||||
panic("can't work, no support for multiline yet")
|
|
||||||
mut myui := ui.new()!
|
|
||||||
privkey := myui.ask_question(
|
|
||||||
question: 'private key of your ssh key'
|
|
||||||
)!
|
|
||||||
agent.add(name, privkey)!
|
|
||||||
} else {
|
|
||||||
// console.print_debug(1)
|
|
||||||
return error(cmd.help_message())
|
|
||||||
// console.print_debug(" Supported commands are: ${gentools.gencmds}")
|
|
||||||
// return error('unknown subcmd')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Profile command - comprehensive SSH agent initialization
|
||||||
|
fn cmd_profile_execute(mut agent sshagent.SSHAgent, isscript bool) ! {
|
||||||
|
console.print_header('🔑 Hero SSH Agent Profile Initialization')
|
||||||
|
|
||||||
|
// Ensure single agent instance
|
||||||
|
agent.ensure_single_agent()!
|
||||||
|
console.print_green('✓ SSH agent instance verified')
|
||||||
|
|
||||||
|
// Smart key loading
|
||||||
|
available_keys := agent.keys
|
||||||
|
loaded_keys := agent.keys_loaded()!
|
||||||
|
|
||||||
|
console.print_debug('Found ${available_keys.len} available keys, ${loaded_keys.len} loaded')
|
||||||
|
|
||||||
|
// If only one key and none loaded, auto-load it
|
||||||
|
if available_keys.len == 1 && loaded_keys.len == 0 {
|
||||||
|
key_name := available_keys[0].name
|
||||||
|
console.print_debug('Auto-loading single key: ${key_name}')
|
||||||
|
|
||||||
|
mut key := agent.get(name: key_name) or {
|
||||||
|
console.print_stderr('Failed to get key: ${err}')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key.load() or { console.print_debug('Key loading failed (may need passphrase): ${err}') }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update shell profile
|
||||||
|
update_shell_profile()!
|
||||||
|
|
||||||
|
console.print_green('✅ SSH agent profile initialized successfully')
|
||||||
|
cmd_status_execute(mut agent)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update shell profile with SSH agent initialization
|
||||||
|
fn update_shell_profile() ! {
|
||||||
|
home := os.home_dir()
|
||||||
|
ssh_dir := '${home}/.ssh'
|
||||||
|
socket_path := '${ssh_dir}/hero-agent.sock'
|
||||||
|
|
||||||
|
// Find appropriate profile file
|
||||||
|
profile_candidates := [
|
||||||
|
'${home}/.profile',
|
||||||
|
'${home}/.bash_profile',
|
||||||
|
'${home}/.bashrc',
|
||||||
|
'${home}/.zshrc',
|
||||||
|
]
|
||||||
|
|
||||||
|
mut profile_file := '${home}/.profile'
|
||||||
|
for candidate in profile_candidates {
|
||||||
|
if os.exists(candidate) {
|
||||||
|
profile_file = candidate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
profile_content := if os.exists(profile_file) {
|
||||||
|
os.read_file(profile_file)!
|
||||||
|
} else {
|
||||||
|
''
|
||||||
|
}
|
||||||
|
|
||||||
|
hero_init_block := '
|
||||||
|
# Hero SSH Agent initialization
|
||||||
|
if [ -f "${socket_path}" ]; then
|
||||||
|
export SSH_AUTH_SOCK="${socket_path}"
|
||||||
|
fi'
|
||||||
|
|
||||||
|
// Check if already present
|
||||||
|
if profile_content.contains('Hero SSH Agent initialization') {
|
||||||
|
console.print_debug('Hero initialization already present in profile')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add hero initialization
|
||||||
|
updated_content := profile_content + hero_init_block
|
||||||
|
os.write_file(profile_file, updated_content)!
|
||||||
|
|
||||||
|
console.print_green('✓ Updated shell profile: ${profile_file}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// List command
|
||||||
|
fn cmd_list_execute(mut agent sshagent.SSHAgent, isscript bool) ! {
|
||||||
|
if !isscript {
|
||||||
|
console.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
console.print_header('SSH Keys Status')
|
||||||
|
println(agent.str())
|
||||||
|
|
||||||
|
loaded_keys := agent.keys_loaded()!
|
||||||
|
if loaded_keys.len > 0 {
|
||||||
|
console.print_header('Currently Loaded Keys:')
|
||||||
|
for key in loaded_keys {
|
||||||
|
console.print_item('- ${key.name} (${key.cat})')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.print_debug('No keys currently loaded in agent')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate command
|
||||||
|
fn cmd_generate_execute(mut agent sshagent.SSHAgent, name string, load bool) ! {
|
||||||
|
if name == '' {
|
||||||
|
return error('Key name is required for generate command')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.print_debug('Generating SSH key: ${name}')
|
||||||
|
mut key := agent.generate(name, '')!
|
||||||
|
console.print_green('✓ Generated SSH key: ${name}')
|
||||||
|
|
||||||
|
if load {
|
||||||
|
console.print_debug('Loading key into agent...')
|
||||||
|
key.load() or {
|
||||||
|
console.print_stderr('Failed to load key: ${err}')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.print_green('✓ Key loaded into agent')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load command
|
||||||
|
fn cmd_load_execute(mut agent sshagent.SSHAgent, name string) ! {
|
||||||
|
if name == '' {
|
||||||
|
return error('Key name is required for load command')
|
||||||
|
}
|
||||||
|
|
||||||
|
mut key := agent.get(name: name) or { return error('SSH key "${name}" not found') }
|
||||||
|
|
||||||
|
console.print_debug('Loading SSH key: ${name}')
|
||||||
|
key.load()!
|
||||||
|
console.print_green('✓ SSH key "${name}" loaded successfully')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forget command
|
||||||
|
fn cmd_forget_execute(mut agent sshagent.SSHAgent, name string) ! {
|
||||||
|
if name == '' {
|
||||||
|
return error('Key name is required for forget command')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.print_debug('Removing SSH key from agent: ${name}')
|
||||||
|
agent.forget(name)!
|
||||||
|
console.print_green('✓ SSH key "${name}" removed from agent')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset command
|
||||||
|
fn cmd_reset_execute(mut agent sshagent.SSHAgent, isscript bool) ! {
|
||||||
|
if !isscript {
|
||||||
|
print('This will remove all loaded SSH keys. Continue? (y/N): ')
|
||||||
|
input := os.input('')
|
||||||
|
if input.trim_space().to_lower() != 'y' {
|
||||||
|
console.print_debug('Reset cancelled')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.print_debug('Resetting SSH agent - removing all keys')
|
||||||
|
agent.reset()!
|
||||||
|
console.print_green('✓ All SSH keys removed from agent')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add command
|
||||||
|
fn cmd_add_execute(mut agent sshagent.SSHAgent, name string) ! {
|
||||||
|
if name == '' {
|
||||||
|
return error('Key name is required for add command')
|
||||||
|
}
|
||||||
|
|
||||||
|
mut myui := ui.new()!
|
||||||
|
privkey := myui.ask_question(
|
||||||
|
question: 'Enter the private key content:'
|
||||||
|
)!
|
||||||
|
|
||||||
|
console.print_debug('Adding SSH key: ${name}')
|
||||||
|
agent.add(name, privkey)!
|
||||||
|
console.print_green('✓ SSH key "${name}" added successfully')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status command
|
||||||
|
fn cmd_status_execute(mut agent sshagent.SSHAgent) ! {
|
||||||
|
console.print_header('SSH Agent Status')
|
||||||
|
|
||||||
|
diag := agent.diagnostics()
|
||||||
|
for key, value in diag {
|
||||||
|
console.print_item('${key}: ${value}')
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded_keys := agent.keys_loaded()!
|
||||||
|
if loaded_keys.len > 0 {
|
||||||
|
console.print_header('Loaded Keys:')
|
||||||
|
for key in loaded_keys {
|
||||||
|
console.print_item('- ${key.name} (${key.cat})')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.print_debug('No keys currently loaded')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push command - deploy SSH key to remote machine
|
||||||
|
fn cmd_push_execute(mut agent sshagent.SSHAgent, target string, key_name string) ! {
|
||||||
|
if target == '' {
|
||||||
|
return error('Target is required for push command (format: user@hostname[:port])')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.print_header('🚀 SSH Key Deployment')
|
||||||
|
|
||||||
|
// Parse target
|
||||||
|
parsed_target := parse_target(target)!
|
||||||
|
console.print_debug('Target: ${parsed_target.user}@${parsed_target.hostname}:${parsed_target.port}')
|
||||||
|
|
||||||
|
// Select key to deploy
|
||||||
|
mut selected_key := select_key_for_deployment(mut agent, key_name)!
|
||||||
|
console.print_debug('Selected key: ${selected_key.name}')
|
||||||
|
|
||||||
|
// Deploy key
|
||||||
|
deploy_key_to_target(mut selected_key, parsed_target)!
|
||||||
|
|
||||||
|
console.print_green('✅ SSH key deployed successfully to ${target}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth command - verify SSH key authorization
|
||||||
|
fn cmd_auth_execute(mut agent sshagent.SSHAgent, target string, key_name string) ! {
|
||||||
|
if target == '' {
|
||||||
|
return error('Target is required for auth command (format: user@hostname[:port])')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.print_header('🔐 SSH Key Authorization Verification')
|
||||||
|
|
||||||
|
// Parse target
|
||||||
|
parsed_target := parse_target(target)!
|
||||||
|
|
||||||
|
// Select key to verify
|
||||||
|
mut selected_key := select_key_for_deployment(mut agent, key_name)!
|
||||||
|
|
||||||
|
// Verify authorization
|
||||||
|
verify_key_authorization(mut selected_key, parsed_target)!
|
||||||
|
|
||||||
|
console.print_green('✅ SSH key authorization verified for ${target}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper structures and functions for remote operations
|
||||||
|
struct RemoteTarget {
|
||||||
|
mut:
|
||||||
|
user string
|
||||||
|
hostname string
|
||||||
|
port int = 22
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse target string in format user@hostname[:port]
|
||||||
|
fn parse_target(target_str string) !RemoteTarget {
|
||||||
|
if !target_str.contains('@') {
|
||||||
|
return error('Target must be in format user@hostname[:port]')
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := target_str.split('@')
|
||||||
|
if parts.len != 2 {
|
||||||
|
return error('Invalid target format: ${target_str}')
|
||||||
|
}
|
||||||
|
|
||||||
|
user := parts[0]
|
||||||
|
mut hostname := parts[1]
|
||||||
|
mut port := 22
|
||||||
|
|
||||||
|
// Check for port specification
|
||||||
|
if hostname.contains(':') {
|
||||||
|
host_port := hostname.split(':')
|
||||||
|
if host_port.len != 2 {
|
||||||
|
return error('Invalid hostname:port format: ${hostname}')
|
||||||
|
}
|
||||||
|
hostname = host_port[0]
|
||||||
|
port = host_port[1].int()
|
||||||
|
}
|
||||||
|
|
||||||
|
return RemoteTarget{
|
||||||
|
user: user
|
||||||
|
hostname: hostname
|
||||||
|
port: port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select appropriate key for deployment
|
||||||
|
fn select_key_for_deployment(mut agent sshagent.SSHAgent, key_name string) !sshagent.SSHKey {
|
||||||
|
available_keys := agent.keys
|
||||||
|
|
||||||
|
if available_keys.len == 0 {
|
||||||
|
return error('No SSH keys found. Generate a key first with: hero sshagent generate <name>')
|
||||||
|
}
|
||||||
|
|
||||||
|
// If specific key requested
|
||||||
|
if key_name.len > 0 {
|
||||||
|
for key in available_keys {
|
||||||
|
if key.name == key_name {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return error('SSH key "${key_name}" not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-select if only one key
|
||||||
|
if available_keys.len == 1 {
|
||||||
|
console.print_debug('Auto-selecting single available key: ${available_keys[0].name}')
|
||||||
|
return available_keys[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interactive selection for multiple keys
|
||||||
|
return interactive_key_selection(available_keys)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interactive key selection when multiple keys are available
|
||||||
|
fn interactive_key_selection(keys []sshagent.SSHKey) !sshagent.SSHKey {
|
||||||
|
console.print_header('Multiple SSH keys available:')
|
||||||
|
|
||||||
|
for i, key in keys {
|
||||||
|
console.print_item('${i + 1}. ${key.name} (${key.cat})')
|
||||||
|
}
|
||||||
|
|
||||||
|
print('Select key number (1-${keys.len}): ')
|
||||||
|
input := os.input('')
|
||||||
|
|
||||||
|
selection := input.trim_space().int() - 1
|
||||||
|
if selection < 0 || selection >= keys.len {
|
||||||
|
return error('Invalid selection: ${input}')
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys[selection]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deploy key to remote target
|
||||||
|
fn deploy_key_to_target(mut key sshagent.SSHKey, target RemoteTarget) ! {
|
||||||
|
console.print_debug('Deploying key ${key.name} to ${target.user}@${target.hostname}')
|
||||||
|
|
||||||
|
// Get public key content
|
||||||
|
pub_key_content := key.keypub()!
|
||||||
|
|
||||||
|
// Use ssh-copy-id if available, otherwise manual deployment
|
||||||
|
if has_ssh_copy_id() {
|
||||||
|
deploy_with_ssh_copy_id(mut key, target)!
|
||||||
|
} else {
|
||||||
|
deploy_manually(pub_key_content, target)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if ssh-copy-id is available
|
||||||
|
fn has_ssh_copy_id() bool {
|
||||||
|
result := os.execute('which ssh-copy-id')
|
||||||
|
return result.exit_code == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deploy using ssh-copy-id
|
||||||
|
fn deploy_with_ssh_copy_id(mut key sshagent.SSHKey, target RemoteTarget) ! {
|
||||||
|
mut key_path := key.keypath()!
|
||||||
|
|
||||||
|
mut cmd := 'ssh-copy-id -i ${key_path.path}'
|
||||||
|
if target.port != 22 {
|
||||||
|
cmd += ' -p ${target.port}'
|
||||||
|
}
|
||||||
|
cmd += ' ${target.user}@${target.hostname}'
|
||||||
|
|
||||||
|
console.print_debug('Executing: ${cmd}')
|
||||||
|
result := os.execute(cmd)
|
||||||
|
|
||||||
|
if result.exit_code != 0 {
|
||||||
|
return error('ssh-copy-id failed: ${result.output}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual deployment by appending to authorized_keys
|
||||||
|
fn deploy_manually(pub_key_content string, target RemoteTarget) ! {
|
||||||
|
mut ssh_cmd := 'ssh'
|
||||||
|
if target.port != 22 {
|
||||||
|
ssh_cmd += ' -p ${target.port}'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command to append key to authorized_keys
|
||||||
|
remote_cmd := 'mkdir -p ~/.ssh && echo "${pub_key_content}" >> ~/.ssh/authorized_keys && chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys'
|
||||||
|
|
||||||
|
full_cmd := '${ssh_cmd} ${target.user}@${target.hostname} "${remote_cmd}"'
|
||||||
|
|
||||||
|
console.print_debug('Executing manual deployment')
|
||||||
|
result := os.execute(full_cmd)
|
||||||
|
|
||||||
|
if result.exit_code != 0 {
|
||||||
|
return error('Manual key deployment failed: ${result.output}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that key is properly authorized on remote target
|
||||||
|
fn verify_key_authorization(mut key sshagent.SSHKey, target RemoteTarget) ! {
|
||||||
|
console.print_debug('Verifying key authorization for ${key.name}')
|
||||||
|
|
||||||
|
// Test SSH connection
|
||||||
|
mut ssh_cmd := 'ssh -o BatchMode=yes -o ConnectTimeout=10'
|
||||||
|
if target.port != 22 {
|
||||||
|
ssh_cmd += ' -p ${target.port}'
|
||||||
|
}
|
||||||
|
ssh_cmd += ' ${target.user}@${target.hostname} "echo SSH_CONNECTION_SUCCESS"'
|
||||||
|
|
||||||
|
console.print_debug('Testing SSH connection...')
|
||||||
|
result := os.execute(ssh_cmd)
|
||||||
|
|
||||||
|
if result.exit_code != 0 {
|
||||||
|
return error('SSH connection failed: ${result.output}')
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.output.contains('SSH_CONNECTION_SUCCESS') {
|
||||||
|
return error('SSH connection test failed - unexpected output')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.print_green('✓ SSH key is properly authorized and working')
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
module core
|
module core
|
||||||
|
|
||||||
import base
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
// check path is accessible, e.g. do we need sudo and are we sudo
|
// check path is accessible, e.g. do we need sudo and are we sudo
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
module ipaddress
|
module ipaddress
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import freeflowuniverse.herolib.osal.core as osal
|
|
||||||
import freeflowuniverse.herolib.core
|
import freeflowuniverse.herolib.core
|
||||||
import freeflowuniverse.herolib.ui.console
|
import freeflowuniverse.herolib.ui.console
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
module gittools
|
module gittools
|
||||||
|
|
||||||
import freeflowuniverse.herolib.osal.sshagent
|
|
||||||
import freeflowuniverse.herolib.core.pathlib
|
import freeflowuniverse.herolib.core.pathlib
|
||||||
import freeflowuniverse.herolib.ui.console
|
import freeflowuniverse.herolib.ui.console
|
||||||
import freeflowuniverse.herolib.develop.vscode
|
import freeflowuniverse.herolib.develop.vscode
|
||||||
@@ -106,7 +105,9 @@ fn (self GitRepo) get_repo_url_for_clone() !string {
|
|||||||
// return url
|
// return url
|
||||||
// }
|
// }
|
||||||
|
|
||||||
if sshagent.loaded() {
|
// Check if SSH agent is loaded (avoid importing sshagent to prevent circular dependency)
|
||||||
|
ssh_check := os.execute('ssh-add -l')
|
||||||
|
if ssh_check.exit_code == 0 {
|
||||||
return self.get_ssh_url()!
|
return self.get_ssh_url()!
|
||||||
} else {
|
} else {
|
||||||
return self.get_http_url()!
|
return self.get_http_url()!
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import net
|
|||||||
import time
|
import time
|
||||||
import freeflowuniverse.herolib.ui.console
|
import freeflowuniverse.herolib.ui.console
|
||||||
import freeflowuniverse.herolib.core
|
import freeflowuniverse.herolib.core
|
||||||
|
<<<<<<< HEAD
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
=======
|
||||||
|
>>>>>>> development
|
||||||
|
|
||||||
pub enum PingResult {
|
pub enum PingResult {
|
||||||
ok
|
ok
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ module core
|
|||||||
import freeflowuniverse.herolib.ui.console
|
import freeflowuniverse.herolib.ui.console
|
||||||
import freeflowuniverse.herolib.core.texttools
|
import freeflowuniverse.herolib.core.texttools
|
||||||
import freeflowuniverse.herolib.core
|
import freeflowuniverse.herolib.core
|
||||||
import os
|
|
||||||
|
|
||||||
// update the package list
|
// update the package list
|
||||||
pub fn package_refresh() ! {
|
pub fn package_refresh() ! {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ module linux
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import freeflowuniverse.herolib.core.pathlib
|
|
||||||
import freeflowuniverse.herolib.osal.core as osal
|
import freeflowuniverse.herolib.osal.core as osal
|
||||||
import freeflowuniverse.herolib.ui.console
|
import freeflowuniverse.herolib.ui.console
|
||||||
|
|
||||||
@@ -288,9 +287,43 @@ fn (mut lf LinuxFactory) create_ssh_agent_profile(username string) ! {
|
|||||||
user_home := '/home/${username}'
|
user_home := '/home/${username}'
|
||||||
profile_script := '${user_home}/.profile_sshagent'
|
profile_script := '${user_home}/.profile_sshagent'
|
||||||
|
|
||||||
// script_content := ''
|
// Create SSH agent auto-start script content
|
||||||
|
script_content := '#!/bin/bash
|
||||||
|
# Auto-start ssh-agent if not running
|
||||||
|
SSH_AGENT_SOCKET="/tmp/ssh-agent-${username}.sock"
|
||||||
|
|
||||||
panic('implement')
|
# Check if agent is already running and responsive
|
||||||
|
if [ -n "\$SSH_AUTH_SOCK" ] && ssh-add -l >/dev/null 2>&1; then
|
||||||
|
# Agent is running and responsive
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if our socket exists and is responsive
|
||||||
|
if [ -S "\$SSH_AGENT_SOCKET" ]; then
|
||||||
|
export SSH_AUTH_SOCK="\$SSH_AGENT_SOCKET"
|
||||||
|
if ssh-add -l >/dev/null 2>&1; then
|
||||||
|
# Socket is responsive
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up any orphaned agents
|
||||||
|
pkill -u ${username} ssh-agent >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Remove stale socket
|
||||||
|
rm -f "\$SSH_AGENT_SOCKET"
|
||||||
|
|
||||||
|
# Start new ssh-agent with consistent socket
|
||||||
|
ssh-agent -a "\$SSH_AGENT_SOCKET" >/dev/null 2>&1
|
||||||
|
|
||||||
|
# Export the socket path
|
||||||
|
export SSH_AUTH_SOCK="\$SSH_AGENT_SOCKET"
|
||||||
|
|
||||||
|
# Verify agent is responsive
|
||||||
|
if ! ssh-add -l >/dev/null 2>&1; then
|
||||||
|
echo "Warning: SSH agent started but is not responsive" >&2
|
||||||
|
fi
|
||||||
|
'
|
||||||
|
|
||||||
osal.file_write(profile_script, script_content)!
|
osal.file_write(profile_script, script_content)!
|
||||||
osal.exec(cmd: 'chown ${username}:${username} ${profile_script}')!
|
osal.exec(cmd: 'chown ${username}:${username} ${profile_script}')!
|
||||||
|
|||||||
33
lib/osal/netns/instructions.md
Normal file
33
lib/osal/netns/instructions.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
### 1. **Network namespaces**
|
||||||
|
|
||||||
|
* Each namespace can have its own interfaces, routing table, firewall rules, etc.
|
||||||
|
* You can move the user’s processes into a network namespace that only has access to a given bridge.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a new netns for user "alice"
|
||||||
|
ip netns add alice
|
||||||
|
|
||||||
|
# Add a veth pair
|
||||||
|
ip link add veth-alice type veth peer name veth-alice-br
|
||||||
|
|
||||||
|
# Attach one side to the bridge
|
||||||
|
ip link set veth-alice-br master br0
|
||||||
|
ip link set veth-alice-br up
|
||||||
|
|
||||||
|
# Move the other side into the netns
|
||||||
|
ip link set veth-alice netns alice
|
||||||
|
|
||||||
|
# Configure inside namespace
|
||||||
|
ip netns exec alice ip addr add 192.168.100.2/24 dev veth-alice
|
||||||
|
ip netns exec alice ip link set veth-alice up
|
||||||
|
ip netns exec alice ip route add default via 192.168.100.1
|
||||||
|
|
||||||
|
# Now run a shell in alice’s namespace
|
||||||
|
sudo -u alice ip netns exec alice bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Now all processes run by `alice` will use only that veth → bridge → network.
|
||||||
|
|
||||||
|
|
||||||
@@ -74,10 +74,10 @@ pub fn sshkey_delete(mut agent SSHAgent, name string) ! {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from agent if loaded (temporarily disabled due to reset_ssh panic)
|
// Remove from agent if loaded
|
||||||
// if key.loaded {
|
if key.loaded {
|
||||||
// key.forget()!
|
key.forget()!
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Delete key files
|
// Delete key files
|
||||||
if key_path.exists() {
|
if key_path.exists() {
|
||||||
@@ -148,62 +148,3 @@ pub fn sshkey_check(mut agent SSHAgent, name string) ! {
|
|||||||
|
|
||||||
console.print_green('✓ SSH key "${name}" is valid')
|
console.print_green('✓ SSH key "${name}" is valid')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy private key to remote node
|
|
||||||
pub fn remote_copy(mut agent SSHAgent, node_addr string, key_name string) ! {
|
|
||||||
console.print_header('Copying SSH key "${key_name}" to ${node_addr}')
|
|
||||||
|
|
||||||
// Get the key
|
|
||||||
mut key := agent.get(name: key_name) or { return error('SSH key "${key_name}" not found') }
|
|
||||||
|
|
||||||
// Create builder node
|
|
||||||
mut b := builder.new() or { return error('Failed to create builder') }
|
|
||||||
mut node := b.node_new(ipaddr: node_addr) or { return error('Failed to create node') }
|
|
||||||
|
|
||||||
// Get private key content
|
|
||||||
mut key_path := key.keypath()!
|
|
||||||
if !key_path.exists() {
|
|
||||||
return error('Private key file not found: ${key_path.path}')
|
|
||||||
}
|
|
||||||
|
|
||||||
private_key_content := key_path.read()!
|
|
||||||
|
|
||||||
// Get home directory on remote
|
|
||||||
home_dir_map := node.environ_get() or {
|
|
||||||
return error('Could not get environment on remote node')
|
|
||||||
}
|
|
||||||
home_dir := home_dir_map['HOME'] or {
|
|
||||||
return error('Could not determine HOME directory on remote node')
|
|
||||||
}
|
|
||||||
|
|
||||||
remote_ssh_dir := '${home_dir}/.ssh'
|
|
||||||
remote_key_path := '${remote_ssh_dir}/${key_name}'
|
|
||||||
|
|
||||||
// Ensure .ssh directory exists with correct permissions
|
|
||||||
node.exec_silent('mkdir -p ${remote_ssh_dir}')!
|
|
||||||
node.exec_silent('chmod 700 ${remote_ssh_dir}')!
|
|
||||||
|
|
||||||
// Copy private key to remote
|
|
||||||
node.file_write(remote_key_path, private_key_content)!
|
|
||||||
node.exec_silent('chmod 600 ${remote_key_path}')!
|
|
||||||
|
|
||||||
// Generate public key on remote
|
|
||||||
node.exec_silent('ssh-keygen -y -f ${remote_key_path} > ${remote_key_path}.pub')!
|
|
||||||
node.exec_silent('chmod 644 ${remote_key_path}.pub')!
|
|
||||||
|
|
||||||
console.print_green('✓ SSH key "${key_name}" copied to ${node_addr}')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add public key to authorized_keys on remote node
|
|
||||||
pub fn remote_auth(mut agent SSHAgent, node_addr string, key_name string) ! {
|
|
||||||
console.print_header('Adding SSH key "${key_name}" to authorized_keys on ${node_addr}')
|
|
||||||
|
|
||||||
// Create builder node
|
|
||||||
mut b := builder.new() or { return error('Failed to create builder') }
|
|
||||||
mut node := b.node_new(ipaddr: node_addr) or { return error('Failed to create node') }
|
|
||||||
|
|
||||||
// Use existing builder integration
|
|
||||||
agent.push_key_to_node(mut node, key_name)!
|
|
||||||
|
|
||||||
console.print_green('✓ SSH key "${key_name}" added to authorized_keys on ${node_addr}')
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -96,3 +96,59 @@ pub fn (mut agent SSHAgent) verify_key_access(mut node builder.Node, key_name st
|
|||||||
|
|
||||||
return result.contains('SSH key verification successful')
|
return result.contains('SSH key verification successful')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy private key to remote node
|
||||||
|
pub fn remote_copy(mut agent SSHAgent, node_addr string, key_name string) ! {
|
||||||
|
console.print_header('Copying SSH key "${key_name}" to ${node_addr}')
|
||||||
|
|
||||||
|
// Get the key
|
||||||
|
mut key := agent.get(name: key_name) or { return error('SSH key "${key_name}" not found') }
|
||||||
|
|
||||||
|
// Create builder node
|
||||||
|
mut b := builder.new()!
|
||||||
|
mut node := b.node_new(ipaddr: node_addr)!
|
||||||
|
|
||||||
|
// Get private key content
|
||||||
|
mut key_path := key.keypath()!
|
||||||
|
if !key_path.exists() {
|
||||||
|
return error('Private key file not found: ${key_path.path}')
|
||||||
|
}
|
||||||
|
|
||||||
|
private_key_content := key_path.read()!
|
||||||
|
|
||||||
|
// Get home directory on remote
|
||||||
|
home_dir := node.environ_get()!['HOME'] or {
|
||||||
|
return error('Could not determine HOME directory on remote node')
|
||||||
|
}
|
||||||
|
|
||||||
|
remote_ssh_dir := '${home_dir}/.ssh'
|
||||||
|
remote_key_path := '${remote_ssh_dir}/${key_name}'
|
||||||
|
|
||||||
|
// Ensure .ssh directory exists with correct permissions
|
||||||
|
node.exec_silent('mkdir -p ${remote_ssh_dir}')!
|
||||||
|
node.exec_silent('chmod 700 ${remote_ssh_dir}')!
|
||||||
|
|
||||||
|
// Copy private key to remote
|
||||||
|
node.file_write(remote_key_path, private_key_content)!
|
||||||
|
node.exec_silent('chmod 600 ${remote_key_path}')!
|
||||||
|
|
||||||
|
// Generate public key on remote
|
||||||
|
node.exec_silent('ssh-keygen -y -f ${remote_key_path} > ${remote_key_path}.pub')!
|
||||||
|
node.exec_silent('chmod 644 ${remote_key_path}.pub')!
|
||||||
|
|
||||||
|
console.print_green('✓ SSH key "${key_name}" copied to ${node_addr}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add public key to authorized_keys on remote node
|
||||||
|
pub fn remote_auth(mut agent SSHAgent, node_addr string, key_name string) ! {
|
||||||
|
console.print_header('Adding SSH key "${key_name}" to authorized_keys on ${node_addr}')
|
||||||
|
|
||||||
|
// Create builder node
|
||||||
|
mut b := builder.new()!
|
||||||
|
mut node := b.node_new(ipaddr: node_addr)!
|
||||||
|
|
||||||
|
// Use existing builder integration
|
||||||
|
agent.push_key_to_node(mut node, key_name)!
|
||||||
|
|
||||||
|
console.print_green('✓ SSH key "${key_name}" added to authorized_keys on ${node_addr}')
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ pub fn new(args_ SSHAgentNewArgs) !SSHAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn loaded() bool {
|
pub fn loaded() bool {
|
||||||
mut agent := new() or { panic(err) }
|
mut agent := new() or { return false }
|
||||||
return agent.active
|
return agent.active
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ fn (mut agent SSHAgent) pop(pubkey_ string) {
|
|||||||
if agent.keys.len > result {
|
if agent.keys.len > result {
|
||||||
agent.keys.delete(x)
|
agent.keys.delete(x)
|
||||||
} else {
|
} else {
|
||||||
panic('bug')
|
// This should not happen, but handle gracefully
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
82
lib/osal/sshagent/play.v
Normal file
82
lib/osal/sshagent/play.v
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
module sshagent
|
||||||
|
|
||||||
|
import freeflowuniverse.herolib.core.playbook { PlayBook }
|
||||||
|
|
||||||
|
pub fn play(mut plbook PlayBook) ! {
|
||||||
|
if !plbook.exists(filter: 'sshagent.') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create a single SSH agent instance
|
||||||
|
mut agent := new_single()!
|
||||||
|
|
||||||
|
// Process sshagent.check actions
|
||||||
|
mut check_actions := plbook.find(filter: 'sshagent.check')!
|
||||||
|
for mut action in check_actions {
|
||||||
|
agent_check(mut agent)!
|
||||||
|
action.done = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process sshagent.sshkey_create actions
|
||||||
|
mut create_actions := plbook.find(filter: 'sshagent.sshkey_create')!
|
||||||
|
for mut action in create_actions {
|
||||||
|
mut p := action.params
|
||||||
|
name := p.get('name')!
|
||||||
|
passphrase := p.get_default('passphrase', '')!
|
||||||
|
|
||||||
|
sshkey_create(mut agent, name, passphrase)!
|
||||||
|
action.done = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process sshagent.sshkey_delete actions
|
||||||
|
mut delete_actions := plbook.find(filter: 'sshagent.sshkey_delete')!
|
||||||
|
for mut action in delete_actions {
|
||||||
|
mut p := action.params
|
||||||
|
name := p.get('name')!
|
||||||
|
|
||||||
|
sshkey_delete(mut agent, name)!
|
||||||
|
action.done = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process sshagent.sshkey_load actions
|
||||||
|
mut load_actions := plbook.find(filter: 'sshagent.sshkey_load')!
|
||||||
|
for mut action in load_actions {
|
||||||
|
mut p := action.params
|
||||||
|
name := p.get('name')!
|
||||||
|
|
||||||
|
sshkey_load(mut agent, name)!
|
||||||
|
action.done = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process sshagent.sshkey_check actions
|
||||||
|
mut check_key_actions := plbook.find(filter: 'sshagent.sshkey_check')!
|
||||||
|
for mut action in check_key_actions {
|
||||||
|
mut p := action.params
|
||||||
|
name := p.get('name')!
|
||||||
|
|
||||||
|
sshkey_check(mut agent, name)!
|
||||||
|
action.done = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process sshagent.remote_copy actions
|
||||||
|
mut remote_copy_actions := plbook.find(filter: 'sshagent.remote_copy')!
|
||||||
|
for mut action in remote_copy_actions {
|
||||||
|
mut p := action.params
|
||||||
|
node_addr := p.get('node')!
|
||||||
|
key_name := p.get('name')!
|
||||||
|
|
||||||
|
remote_copy(mut agent, node_addr, key_name)!
|
||||||
|
action.done = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process sshagent.remote_auth actions
|
||||||
|
mut remote_auth_actions := plbook.find(filter: 'sshagent.remote_auth')!
|
||||||
|
for mut action in remote_auth_actions {
|
||||||
|
mut p := action.params
|
||||||
|
node_addr := p.get('node')!
|
||||||
|
key_name := p.get('name')!
|
||||||
|
|
||||||
|
remote_auth(mut agent, node_addr, key_name)!
|
||||||
|
action.done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +1,81 @@
|
|||||||
## ssh agent
|
# SSH Agent Module
|
||||||
|
|
||||||
|
SSH agent management library for V language. Provides secure key handling, agent lifecycle control, and remote integration.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* Manage SSH keys (generate, load, import)
|
||||||
|
* Single agent per user with auto-cleanup
|
||||||
|
* Start/stop/reset agent easily
|
||||||
|
* Diagnostics and status checks
|
||||||
|
* Push keys to remote nodes & verify access
|
||||||
|
* Security-first (file permissions, socket handling)
|
||||||
|
|
||||||
|
## Platform Support
|
||||||
|
|
||||||
|
* Linux, macOS
|
||||||
|
* Windows (not yet supported)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
```v
|
```v
|
||||||
import freeflowuniverse.herolib.osal.sshagent
|
import freeflowuniverse.herolib.osal.sshagent
|
||||||
|
|
||||||
mut agent := sshagent.new()!
|
mut agent := sshagent.new()!
|
||||||
|
mut key := agent.generate('my_key', '')!
|
||||||
privkey:='
|
key.load()!
|
||||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
println(agent)
|
||||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
|
||||||
QyNTUxOQAAACDXf9Z/2AH8/8a1ppagCplQdhWyQ8wZAieUw3nNcxsDiQAAAIhb3ybRW98m
|
|
||||||
0QAAAAtzc2gtZWQyNTUxOQAAACDXf9Z/2AH8/8a1ppagCplQdhWyQ8wZAieUw3nNcxsDiQ
|
|
||||||
AAAEC+fcDBPqdJHlJOQJ2zXhU2FztKAIl3TmWkaGCPnyts49d/1n/YAfz/xrWmlqAKmVB2
|
|
||||||
FbJDzBkCJ5TDec1zGwOJAAAABWJvb2tz
|
|
||||||
-----END OPENSSH PRIVATE KEY-----
|
|
||||||
'
|
|
||||||
|
|
||||||
mut sshkey:=agent.add("mykey:,privkey)!
|
|
||||||
|
|
||||||
|
|
||||||
sshkey.forget()!
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### hero
|
## Usage
|
||||||
|
|
||||||
there is also a hero command
|
### Agent
|
||||||
|
|
||||||
```js
|
|
||||||
//will add the key and load (at this stage no support for passphrases)
|
|
||||||
!!sshagent.key_add name:'myname'
|
|
||||||
privkey:'
|
|
||||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
|
||||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
|
||||||
QyNTUxOQAAACDXf9Z/2AH8/8a1ppagCplQdhWyQ8wZAieUw3nNcxsDiQAAAIhb3ybRW98m
|
|
||||||
0QAAAAtzc2gtZWQysdsdsddsdsdsdsdsdsd8/8a1ppagCplQdhWyQ8wZAieUw3nNcxsDiQ
|
|
||||||
AAAEC+fcDBPqdJHlJOQJ2zXhU2FztKAIl3TmWkaGCPnyts49d/1n/YAfz/xrWmlqAKmVB2
|
|
||||||
FbJDzBkCJ5TDec1zGwOJAAAABWJvb2tz
|
|
||||||
-----END OPENSSH PRIVATE KEY-----
|
|
||||||
'
|
|
||||||
|
|
||||||
|
```v
|
||||||
|
mut agent := sshagent.new()!
|
||||||
|
mut agent := sshagent.new(homepath: '/custom/ssh/path')!
|
||||||
|
mut agent := sshagent.new_single()!
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Keys
|
||||||
|
|
||||||
|
```v
|
||||||
|
mut key := agent.generate('my_key', '')!
|
||||||
|
agent.add('imported_key', privkey)!
|
||||||
|
key.load()!
|
||||||
|
if agent.exists(name: 'my_key') { println('Key exists') }
|
||||||
|
agent.forget('my_key')!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Agent Ops
|
||||||
|
|
||||||
|
```v
|
||||||
|
println(agent.diagnostics())
|
||||||
|
println(agent.keys_loaded()!)
|
||||||
|
agent.reset()!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remote
|
||||||
|
|
||||||
|
```v
|
||||||
|
import freeflowuniverse.herolib.builder
|
||||||
|
|
||||||
|
mut node := builder.node_new(ipaddr: 'user@remote:22')!
|
||||||
|
agent.push_key_to_node(mut node, 'my_key')!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
* Private keys set to `0600`
|
||||||
|
* Secure sockets & user isolation
|
||||||
|
* Validated inputs & safe memory handling
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See `examples/osal/sshagent/` for demos.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
v test lib/osal/sshagent/
|
||||||
|
```
|
||||||
|
|||||||
241
lib/osal/sshagent/security.v
Normal file
241
lib/osal/sshagent/security.v
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
module sshagent
|
||||||
|
|
||||||
|
import os
|
||||||
|
import freeflowuniverse.herolib.core.texttools
|
||||||
|
|
||||||
|
// Security validation functions for SSH agent operations
|
||||||
|
|
||||||
|
// validate_key_name ensures SSH key names are safe and follow conventions
|
||||||
|
pub fn validate_key_name(name string) !string {
|
||||||
|
if name.len == 0 {
|
||||||
|
return error('SSH key name cannot be empty')
|
||||||
|
}
|
||||||
|
|
||||||
|
if name.len > 255 {
|
||||||
|
return error('SSH key name too long (max 255 characters)')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for dangerous characters
|
||||||
|
dangerous_chars := ['/', '\\', '..', '~', '$', '`', ';', '|', '&', '>', '<', '*', '?', '[',
|
||||||
|
']', '{', '}', '(', ')', '"', "'", ' ']
|
||||||
|
for dangerous_char in dangerous_chars {
|
||||||
|
if name.contains(dangerous_char) {
|
||||||
|
return error('SSH key name contains invalid character: ${dangerous_char}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure it starts with alphanumeric
|
||||||
|
if !name[0].is_alnum() {
|
||||||
|
return error('SSH key name must start with alphanumeric character')
|
||||||
|
}
|
||||||
|
|
||||||
|
return texttools.name_fix(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate_private_key checks if the provided string is a valid SSH private key
|
||||||
|
pub fn validate_private_key(privkey string) !string {
|
||||||
|
if privkey.len == 0 {
|
||||||
|
return error('Private key cannot be empty')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for valid private key headers
|
||||||
|
valid_headers := [
|
||||||
|
'-----BEGIN OPENSSH PRIVATE KEY-----',
|
||||||
|
'-----BEGIN RSA PRIVATE KEY-----',
|
||||||
|
'-----BEGIN DSA PRIVATE KEY-----',
|
||||||
|
'-----BEGIN EC PRIVATE KEY-----',
|
||||||
|
'-----BEGIN PRIVATE KEY-----',
|
||||||
|
]
|
||||||
|
|
||||||
|
mut has_valid_header := false
|
||||||
|
for header in valid_headers {
|
||||||
|
if privkey.contains(header) {
|
||||||
|
has_valid_header = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has_valid_header {
|
||||||
|
return error('Invalid private key format - missing valid header')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for corresponding footer
|
||||||
|
valid_footers := [
|
||||||
|
'-----END OPENSSH PRIVATE KEY-----',
|
||||||
|
'-----END RSA PRIVATE KEY-----',
|
||||||
|
'-----END DSA PRIVATE KEY-----',
|
||||||
|
'-----END EC PRIVATE KEY-----',
|
||||||
|
'-----END PRIVATE KEY-----',
|
||||||
|
]
|
||||||
|
|
||||||
|
mut has_valid_footer := false
|
||||||
|
for footer in valid_footers {
|
||||||
|
if privkey.contains(footer) {
|
||||||
|
has_valid_footer = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has_valid_footer {
|
||||||
|
return error('Invalid private key format - missing valid footer')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic length check (private keys should be substantial)
|
||||||
|
if privkey.len < 200 {
|
||||||
|
return error('Private key appears to be too short')
|
||||||
|
}
|
||||||
|
|
||||||
|
return privkey
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate_file_path ensures file paths are safe and within expected directories
|
||||||
|
pub fn validate_file_path(path string, base_dir string) !string {
|
||||||
|
if path.len == 0 {
|
||||||
|
return error('File path cannot be empty')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve absolute path
|
||||||
|
abs_path := os.abs_path(path)
|
||||||
|
abs_base := os.abs_path(base_dir)
|
||||||
|
|
||||||
|
// Ensure path is within base directory (prevent directory traversal)
|
||||||
|
if !abs_path.starts_with(abs_base) {
|
||||||
|
return error('File path outside of allowed directory: ${path}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for dangerous path components
|
||||||
|
dangerous_components := ['..', './', '~/', '$']
|
||||||
|
for component in dangerous_components {
|
||||||
|
if path.contains(component) {
|
||||||
|
return error('File path contains dangerous component: ${component}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return abs_path
|
||||||
|
}
|
||||||
|
|
||||||
|
// secure_file_permissions sets appropriate permissions for SSH key files
|
||||||
|
pub fn secure_file_permissions(file_path string, is_private bool) ! {
|
||||||
|
if !os.exists(file_path) {
|
||||||
|
return error('File does not exist: ${file_path}')
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_private {
|
||||||
|
// Private keys should be readable/writable only by owner
|
||||||
|
os.chmod(file_path, 0o600)!
|
||||||
|
} else {
|
||||||
|
// Public keys can be readable by others
|
||||||
|
os.chmod(file_path, 0o644)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get_secure_socket_path returns a secure socket path for the given user
|
||||||
|
pub fn get_secure_socket_path(user string) !string {
|
||||||
|
if user.len == 0 {
|
||||||
|
return error('User cannot be empty')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate user name
|
||||||
|
validated_user := validate_key_name(user)!
|
||||||
|
|
||||||
|
// Use more secure temporary directory if available
|
||||||
|
mut temp_dir := '/tmp'
|
||||||
|
|
||||||
|
// Check for user-specific temp directory
|
||||||
|
user_temp := os.getenv('XDG_RUNTIME_DIR')
|
||||||
|
if user_temp.len > 0 && os.exists(user_temp) {
|
||||||
|
temp_dir = user_temp
|
||||||
|
}
|
||||||
|
|
||||||
|
socket_path := '${temp_dir}/ssh-agent-${validated_user}.sock'
|
||||||
|
|
||||||
|
// Ensure parent directory exists and has correct permissions
|
||||||
|
parent_dir := os.dir(socket_path)
|
||||||
|
if !os.exists(parent_dir) {
|
||||||
|
os.mkdir_all(parent_dir)!
|
||||||
|
os.chmod(parent_dir, 0o700)! // Only owner can access
|
||||||
|
}
|
||||||
|
|
||||||
|
return socket_path
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitize_environment_variables cleans SSH-related environment variables
|
||||||
|
pub fn sanitize_environment_variables() {
|
||||||
|
// List of SSH-related environment variables that might need cleaning
|
||||||
|
ssh_env_vars := ['SSH_AUTH_SOCK', 'SSH_AGENT_PID', 'SSH_CLIENT', 'SSH_CONNECTION']
|
||||||
|
|
||||||
|
for var in ssh_env_vars {
|
||||||
|
env_val := os.getenv(var)
|
||||||
|
if env_val.len > 0 {
|
||||||
|
// Basic validation of environment variable values
|
||||||
|
if env_val.contains('..') || env_val.contains(';') || env_val.contains('|') {
|
||||||
|
// Unset potentially dangerous environment variables
|
||||||
|
os.unsetenv(var)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate_passphrase checks passphrase strength (basic validation)
|
||||||
|
pub fn validate_passphrase(passphrase string) !string {
|
||||||
|
// Allow empty passphrase (user choice)
|
||||||
|
if passphrase.len == 0 {
|
||||||
|
return passphrase
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic length check
|
||||||
|
if passphrase.len < 8 {
|
||||||
|
return error('Passphrase should be at least 8 characters long')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for common weak passphrases
|
||||||
|
weak_passphrases := ['password', '12345678', 'qwerty', 'admin', 'root', 'test']
|
||||||
|
for weak in weak_passphrases {
|
||||||
|
if passphrase.to_lower() == weak {
|
||||||
|
return error('Passphrase is too weak - avoid common passwords')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return passphrase
|
||||||
|
}
|
||||||
|
|
||||||
|
// check_system_security performs basic system security checks
|
||||||
|
pub fn check_system_security() !map[string]string {
|
||||||
|
mut security_status := map[string]string{}
|
||||||
|
|
||||||
|
// Check if running as root (generally not recommended)
|
||||||
|
if os.getuid() == 0 {
|
||||||
|
security_status['root_user'] = 'WARNING: Running as root user'
|
||||||
|
} else {
|
||||||
|
security_status['root_user'] = 'OK: Not running as root'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check SSH directory permissions
|
||||||
|
ssh_dir := '${os.home_dir()}/.ssh'
|
||||||
|
if os.exists(ssh_dir) {
|
||||||
|
// Get directory permissions (simplified check)
|
||||||
|
if os.is_readable(ssh_dir) && os.is_writable(ssh_dir) {
|
||||||
|
security_status['ssh_dir_permissions'] = 'OK: SSH directory accessible'
|
||||||
|
} else {
|
||||||
|
security_status['ssh_dir_permissions'] = 'WARNING: SSH directory permission issues'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
security_status['ssh_dir_permissions'] = 'INFO: SSH directory does not exist'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for SSH agent processes
|
||||||
|
user := os.getenv('USER')
|
||||||
|
res := os.execute('pgrep -u ${user} ssh-agent | wc -l')
|
||||||
|
if res.exit_code == 0 {
|
||||||
|
agent_count := res.output.trim_space().int()
|
||||||
|
if agent_count == 0 {
|
||||||
|
security_status['ssh_agents'] = 'INFO: No SSH agents running'
|
||||||
|
} else if agent_count == 1 {
|
||||||
|
security_status['ssh_agents'] = 'OK: One SSH agent running'
|
||||||
|
} else {
|
||||||
|
security_status['ssh_agents'] = 'WARNING: Multiple SSH agents running (${agent_count})'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return security_status
|
||||||
|
}
|
||||||
@@ -34,9 +34,18 @@ pub fn (mut agent SSHAgent) ensure_single_agent() ! {
|
|||||||
agent.active = true
|
agent.active = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// get consistent socket path per user
|
// get consistent socket path per user in home directory
|
||||||
fn get_agent_socket_path(user string) string {
|
fn get_agent_socket_path(user string) string {
|
||||||
return '/tmp/ssh-agent-${user}.sock'
|
home := os.home_dir()
|
||||||
|
ssh_dir := '${home}/.ssh'
|
||||||
|
|
||||||
|
// Ensure SSH directory exists with correct permissions
|
||||||
|
if !os.exists(ssh_dir) {
|
||||||
|
os.mkdir_all(ssh_dir) or { return '/tmp/ssh-agent-${user}.sock' }
|
||||||
|
os.chmod(ssh_dir, 0o700) or {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '${ssh_dir}/hero-agent.sock'
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if current agent is responsive
|
// check if current agent is responsive
|
||||||
@@ -155,7 +164,7 @@ pub fn (mut agent SSHAgent) init() ! {
|
|||||||
if line.contains(' ') {
|
if line.contains(' ') {
|
||||||
splitted := line.split(' ')
|
splitted := line.split(' ')
|
||||||
if splitted.len < 2 {
|
if splitted.len < 2 {
|
||||||
panic('bug')
|
return error('Invalid SSH key format in agent output: ${line}')
|
||||||
}
|
}
|
||||||
pubkey := splitted[1]
|
pubkey := splitted[1]
|
||||||
mut sshkey := SSHKey{
|
mut sshkey := SSHKey{
|
||||||
@@ -166,12 +175,12 @@ pub fn (mut agent SSHAgent) init() ! {
|
|||||||
if splitted[0].contains('ed25519') {
|
if splitted[0].contains('ed25519') {
|
||||||
sshkey.cat = .ed25519
|
sshkey.cat = .ed25519
|
||||||
if splitted.len > 2 {
|
if splitted.len > 2 {
|
||||||
sshkey.email = splitted[2] or { panic('bug') }
|
sshkey.email = splitted[2] or { '' }
|
||||||
}
|
}
|
||||||
} else if splitted[0].contains('rsa') {
|
} else if splitted[0].contains('rsa') {
|
||||||
sshkey.cat = .rsa
|
sshkey.cat = .rsa
|
||||||
} else {
|
} else {
|
||||||
panic('bug: implement other cat for ssh-key.\n${line}')
|
return error('Unsupported SSH key type in line: ${line}')
|
||||||
}
|
}
|
||||||
|
|
||||||
if !(agent.exists(pubkey: pubkey)) {
|
if !(agent.exists(pubkey: pubkey)) {
|
||||||
@@ -191,7 +200,7 @@ pub fn (mut agent SSHAgent) init() ! {
|
|||||||
c = c.replace(' ', ' ').replace(' ', ' ') // deal with double spaces, or tripple (need to do this 2x
|
c = c.replace(' ', ' ').replace(' ', ' ') // deal with double spaces, or tripple (need to do this 2x
|
||||||
splitted := c.trim_space().split(' ')
|
splitted := c.trim_space().split(' ')
|
||||||
if splitted.len < 2 {
|
if splitted.len < 2 {
|
||||||
panic('bug')
|
return error('Invalid public key format in file: ${pkp.path}')
|
||||||
}
|
}
|
||||||
mut name := pkp.name()
|
mut name := pkp.name()
|
||||||
name = name[0..(name.len - 4)]
|
name = name[0..(name.len - 4)]
|
||||||
@@ -211,7 +220,7 @@ pub fn (mut agent SSHAgent) init() ! {
|
|||||||
} else if splitted[0].contains('rsa') {
|
} else if splitted[0].contains('rsa') {
|
||||||
sshkey2.cat = .rsa
|
sshkey2.cat = .rsa
|
||||||
} else {
|
} else {
|
||||||
panic('bug: implement other cat for ssh-key')
|
return error('Unsupported SSH key type in file: ${pkp.path}')
|
||||||
}
|
}
|
||||||
if splitted.len > 2 {
|
if splitted.len > 2 {
|
||||||
sshkey2.email = splitted[2]
|
sshkey2.email = splitted[2]
|
||||||
@@ -223,53 +232,74 @@ pub fn (mut agent SSHAgent) init() ! {
|
|||||||
|
|
||||||
// returns path to sshkey
|
// returns path to sshkey
|
||||||
pub fn (mut agent SSHAgent) generate(name string, passphrase string) !SSHKey {
|
pub fn (mut agent SSHAgent) generate(name string, passphrase string) !SSHKey {
|
||||||
dest := '${agent.homepath.path}/${name}'
|
// Validate inputs
|
||||||
|
validated_name := validate_key_name(name)!
|
||||||
|
validated_passphrase := validate_passphrase(passphrase)!
|
||||||
|
|
||||||
|
dest := '${agent.homepath.path}/${validated_name}'
|
||||||
if os.exists(dest) {
|
if os.exists(dest) {
|
||||||
os.rm(dest)!
|
os.rm(dest)!
|
||||||
}
|
}
|
||||||
cmd := 'ssh-keygen -t ed25519 -f ${dest} -P ${passphrase} -q'
|
cmd := 'ssh-keygen -t ed25519 -f ${dest} -P ${validated_passphrase} -q'
|
||||||
// console.print_debug(cmd)
|
// console.print_debug(cmd)
|
||||||
rc := os.execute(cmd)
|
rc := os.execute(cmd)
|
||||||
if !(rc.exit_code == 0) {
|
if !(rc.exit_code == 0) {
|
||||||
return error('Could not generated sshkey,\n${rc}')
|
return error('Could not generate SSH key: ${rc.output}')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set secure permissions
|
||||||
|
secure_file_permissions(dest, true)! // private key
|
||||||
|
secure_file_permissions('${dest}.pub', false)! // public key
|
||||||
|
|
||||||
agent.init()!
|
agent.init()!
|
||||||
return agent.get(name: name) or { panic(err) }
|
return agent.get(name: validated_name) or {
|
||||||
|
return error("Generated SSH key '${validated_name}' not found in agent after creation: ${err}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// unload all ssh keys
|
// unload all ssh keys
|
||||||
pub fn (mut agent SSHAgent) reset() ! {
|
pub fn (mut agent SSHAgent) reset() ! {
|
||||||
if true {
|
console.print_debug('Resetting SSH agent - removing all loaded keys')
|
||||||
panic('reset_ssh')
|
|
||||||
}
|
|
||||||
res := os.execute('ssh-add -D')
|
res := os.execute('ssh-add -D')
|
||||||
if res.exit_code > 0 {
|
if res.exit_code > 0 {
|
||||||
return error('cannot reset sshkeys.')
|
return error('cannot reset sshkeys: ${res.output}')
|
||||||
}
|
}
|
||||||
agent.init()! // should now be empty for loaded keys
|
agent.init()! // should now be empty for loaded keys
|
||||||
|
console.print_green('✓ All SSH keys removed from agent')
|
||||||
}
|
}
|
||||||
|
|
||||||
// load the key, they key is content (private key) .
|
// load the key, they key is content (private key) .
|
||||||
// a name is required
|
// a name is required
|
||||||
pub fn (mut agent SSHAgent) add(name string, privkey_ string) !SSHKey {
|
pub fn (mut agent SSHAgent) add(name string, privkey_ string) !SSHKey {
|
||||||
mut privkey := privkey_
|
// Validate inputs
|
||||||
path := '${agent.homepath.path}/${name}'
|
validated_name := validate_key_name(name)!
|
||||||
if os.exists(path) {
|
validated_privkey := validate_private_key(privkey_)!
|
||||||
os.rm(path)!
|
|
||||||
|
mut privkey := validated_privkey
|
||||||
|
path := '${agent.homepath.path}/${validated_name}'
|
||||||
|
|
||||||
|
// Validate file path
|
||||||
|
validated_path := validate_file_path(path, agent.homepath.path)!
|
||||||
|
|
||||||
|
if os.exists(validated_path) {
|
||||||
|
os.rm(validated_path)!
|
||||||
}
|
}
|
||||||
if os.exists('${path}.pub') {
|
if os.exists('${validated_path}.pub') {
|
||||||
os.rm('${path}.pub')!
|
os.rm('${validated_path}.pub')!
|
||||||
}
|
}
|
||||||
if !privkey.ends_with('\n') {
|
if !privkey.ends_with('\n') {
|
||||||
privkey += '\n'
|
privkey += '\n'
|
||||||
}
|
}
|
||||||
os.write_file(path, privkey)!
|
os.write_file(validated_path, privkey)!
|
||||||
os.chmod(path, 0o600)!
|
secure_file_permissions(validated_path, true)! // private key
|
||||||
res4 := os.execute('ssh-keygen -y -f ${path} > ${path}.pub')
|
|
||||||
|
res4 := os.execute('ssh-keygen -y -f ${validated_path} > ${validated_path}.pub')
|
||||||
if res4.exit_code > 0 {
|
if res4.exit_code > 0 {
|
||||||
return error('cannot generate pubkey ${path}.\n${res4.output}')
|
return error('Cannot generate public key from private key: ${res4.output}')
|
||||||
}
|
}
|
||||||
return agent.load(path)!
|
secure_file_permissions('${validated_path}.pub', false)! // public key
|
||||||
|
|
||||||
|
return agent.load(validated_path)!
|
||||||
}
|
}
|
||||||
|
|
||||||
// load key starting from path to private key
|
// load key starting from path to private key
|
||||||
@@ -288,18 +318,17 @@ pub fn (mut agent SSHAgent) load(keypath string) !SSHKey {
|
|||||||
}
|
}
|
||||||
agent.init()!
|
agent.init()!
|
||||||
return agent.get(name: name) or {
|
return agent.get(name: name) or {
|
||||||
panic("can't find sshkey with name:'${name}' from agent.\n${err}")
|
return error("Cannot find SSH key '${name}' in agent after loading from '${keypath}': ${err}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// forget the specified key
|
// forget the specified key
|
||||||
pub fn (mut agent SSHAgent) forget(name string) ! {
|
pub fn (mut agent SSHAgent) forget(name string) ! {
|
||||||
if true {
|
console.print_debug('Forgetting SSH key: ${name}')
|
||||||
panic('reset_ssh')
|
mut key := agent.get(name: name) or { return error('SSH key "${name}" not found in agent') }
|
||||||
}
|
|
||||||
mut key := agent.get(name: name) or { return }
|
|
||||||
agent.pop(key.pubkey)
|
agent.pop(key.pubkey)
|
||||||
key.forget()!
|
key.forget()!
|
||||||
|
console.print_green('✓ SSH key "${name}" removed from agent')
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn (mut agent SSHAgent) str() string {
|
pub fn (mut agent SSHAgent) str() string {
|
||||||
|
|||||||
216
lib/osal/sshagent/sshagent_test.v
Normal file
216
lib/osal/sshagent/sshagent_test.v
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
module sshagent
|
||||||
|
|
||||||
|
import os
|
||||||
|
import freeflowuniverse.herolib.ui.console
|
||||||
|
|
||||||
|
// Test helper to create temporary directory for testing
|
||||||
|
fn setup_test_env() !string {
|
||||||
|
test_dir := '/tmp/sshagent_test_${os.getpid()}'
|
||||||
|
os.mkdir_all(test_dir)!
|
||||||
|
return test_dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test helper to cleanup test environment
|
||||||
|
fn cleanup_test_env(test_dir string) {
|
||||||
|
os.rmdir_all(test_dir) or {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test SSH agent creation
|
||||||
|
fn test_sshagent_new() ! {
|
||||||
|
test_dir := setup_test_env()!
|
||||||
|
defer { cleanup_test_env(test_dir) }
|
||||||
|
|
||||||
|
mut agent := new(homepath: test_dir)!
|
||||||
|
assert agent.homepath.path == test_dir
|
||||||
|
assert agent.keys.len >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test SSH agent with single instance
|
||||||
|
fn test_sshagent_new_single() ! {
|
||||||
|
test_dir := setup_test_env()!
|
||||||
|
defer { cleanup_test_env(test_dir) }
|
||||||
|
|
||||||
|
mut agent := new_single(homepath: test_dir)!
|
||||||
|
assert agent.homepath.path == test_dir
|
||||||
|
|
||||||
|
// Test that agent is responsive
|
||||||
|
// Note: This might fail in CI environments without SSH agent
|
||||||
|
// agent.is_agent_responsive() // Commented out for CI compatibility
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test SSH key generation
|
||||||
|
fn test_sshkey_generation() ! {
|
||||||
|
test_dir := setup_test_env()!
|
||||||
|
defer { cleanup_test_env(test_dir) }
|
||||||
|
|
||||||
|
mut agent := new(homepath: test_dir)!
|
||||||
|
|
||||||
|
// Generate a test key
|
||||||
|
key_name := 'test_key'
|
||||||
|
mut key := agent.generate(key_name, '')!
|
||||||
|
|
||||||
|
assert key.name == key_name
|
||||||
|
assert key.cat == .ed25519
|
||||||
|
|
||||||
|
// Verify key files exist
|
||||||
|
mut key_path := key.keypath()!
|
||||||
|
mut pub_key_path := key.keypath_pub()!
|
||||||
|
|
||||||
|
assert key_path.exists()
|
||||||
|
assert pub_key_path.exists()
|
||||||
|
|
||||||
|
// Verify key content
|
||||||
|
private_content := key_path.read()!
|
||||||
|
public_content := key.keypub()!
|
||||||
|
|
||||||
|
assert private_content.contains('PRIVATE KEY')
|
||||||
|
assert public_content.starts_with('ssh-ed25519')
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
key_path.delete()!
|
||||||
|
pub_key_path.delete()!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test SSH key operations
|
||||||
|
fn test_sshkey_operations() ! {
|
||||||
|
test_dir := setup_test_env()!
|
||||||
|
defer { cleanup_test_env(test_dir) }
|
||||||
|
|
||||||
|
mut agent := new(homepath: test_dir)!
|
||||||
|
|
||||||
|
// Test key existence check
|
||||||
|
assert !agent.exists(name: 'nonexistent_key')
|
||||||
|
|
||||||
|
// Generate key
|
||||||
|
key_name := 'ops_test_key'
|
||||||
|
mut key := agent.generate(key_name, '')!
|
||||||
|
|
||||||
|
// Test key retrieval
|
||||||
|
retrieved_key := agent.get(name: key_name) or {
|
||||||
|
assert false, 'Key should exist after generation'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert retrieved_key.name == key_name
|
||||||
|
|
||||||
|
// Test key existence after generation
|
||||||
|
assert agent.exists(name: key_name)
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
mut cleanup_key_path := key.keypath()!
|
||||||
|
mut cleanup_pub_path := key.keypath_pub()!
|
||||||
|
cleanup_key_path.delete()!
|
||||||
|
cleanup_pub_path.delete()!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test SSH agent diagnostics
|
||||||
|
fn test_sshagent_diagnostics() ! {
|
||||||
|
test_dir := setup_test_env()!
|
||||||
|
defer { cleanup_test_env(test_dir) }
|
||||||
|
|
||||||
|
mut agent := new(homepath: test_dir)!
|
||||||
|
|
||||||
|
diag := agent.diagnostics()
|
||||||
|
|
||||||
|
// Check that all expected diagnostic keys are present
|
||||||
|
expected_keys := ['socket_path', 'socket_exists', 'agent_responsive', 'loaded_keys_count',
|
||||||
|
'total_keys_count', 'agent_processes']
|
||||||
|
|
||||||
|
for key in expected_keys {
|
||||||
|
assert key in diag, 'Missing diagnostic key: ${key}'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify diagnostic values are reasonable
|
||||||
|
assert diag['loaded_keys_count'].int() >= 0
|
||||||
|
assert diag['total_keys_count'].int() >= 0
|
||||||
|
assert diag['agent_processes'].int() >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test error handling
|
||||||
|
fn test_error_handling() ! {
|
||||||
|
test_dir := setup_test_env()!
|
||||||
|
defer { cleanup_test_env(test_dir) }
|
||||||
|
|
||||||
|
mut agent := new(homepath: test_dir)!
|
||||||
|
|
||||||
|
// Test loading non-existent key
|
||||||
|
if _ := agent.load('/nonexistent/path') {
|
||||||
|
assert false, 'Should fail to load non-existent key'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test getting non-existent key
|
||||||
|
if _ := agent.get(name: 'nonexistent') {
|
||||||
|
assert false, 'Should return none for non-existent key'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test forgetting non-existent key
|
||||||
|
if _ := agent.forget('nonexistent') {
|
||||||
|
assert false, 'Should fail to forget non-existent key'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test key string representation
|
||||||
|
fn test_sshkey_string() ! {
|
||||||
|
test_dir := setup_test_env()!
|
||||||
|
defer { cleanup_test_env(test_dir) }
|
||||||
|
|
||||||
|
mut agent := new(homepath: test_dir)!
|
||||||
|
|
||||||
|
// Generate key for testing
|
||||||
|
key_name := 'string_test_key'
|
||||||
|
mut key := agent.generate(key_name, '')!
|
||||||
|
|
||||||
|
// Test key string representation
|
||||||
|
key_str := key.str()
|
||||||
|
assert key_str.contains(key_name)
|
||||||
|
assert key_str.contains('ed25519')
|
||||||
|
|
||||||
|
// Test agent string representation
|
||||||
|
agent_str := agent.str()
|
||||||
|
assert agent_str.contains('SSHAGENT')
|
||||||
|
assert agent_str.contains(key_name)
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
mut cleanup_key_path2 := key.keypath()!
|
||||||
|
mut cleanup_pub_path2 := key.keypath_pub()!
|
||||||
|
cleanup_key_path2.delete()!
|
||||||
|
cleanup_pub_path2.delete()!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test private key addition (simplified - just test file creation)
|
||||||
|
fn test_add_private_key() ! {
|
||||||
|
test_dir := setup_test_env()!
|
||||||
|
defer { cleanup_test_env(test_dir) }
|
||||||
|
|
||||||
|
mut agent := new(homepath: test_dir)!
|
||||||
|
|
||||||
|
// Create a simple test private key content (not a real key, just for testing file operations)
|
||||||
|
test_private_key := '-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||||
|
QyNTUxOQAAACDXf9Z/2AH8/8a1ppagCplQdhWyQ8wZAieUw3nNcxsDiQAAAIhb3ybRW98m
|
||||||
|
0QAAAAtzc2gtZWQyNTUxOQAAACDXf9Z/2AH8/8a1ppagCplQdhWyQ8wZAieUw3nNcxsDiQ
|
||||||
|
AAAEC+fcDBPqdJHlJOQJ2zXhU2FztKAIl3TmWkaGCPnyts49d/1n/YAfz/xrWmlqAKmVB2
|
||||||
|
FbJDzBkCJ5TDec1zGwOJAAAABWJvb2tz
|
||||||
|
-----END OPENSSH PRIVATE KEY-----'
|
||||||
|
|
||||||
|
// Test input validation
|
||||||
|
key_name := 'test_added_key'
|
||||||
|
|
||||||
|
// This should work for file creation but may fail on public key generation
|
||||||
|
// which is expected since this is not a real private key
|
||||||
|
if mut added_key := agent.add(key_name, test_private_key) {
|
||||||
|
// If it succeeds, verify files were created
|
||||||
|
mut added_key_path := added_key.keypath()!
|
||||||
|
assert added_key_path.exists()
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
added_key_path.delete()!
|
||||||
|
if pub_path := added_key.keypath_pub() {
|
||||||
|
mut pub_file := pub_path
|
||||||
|
pub_file.delete() or {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Expected to fail with invalid key, which is fine for this test
|
||||||
|
// We're mainly testing the validation and file handling logic
|
||||||
|
console.print_debug('Add private key failed as expected with test key')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -177,7 +177,7 @@ pub fn (mut t Tmux) windows_get() []&Window {
|
|||||||
pub fn (mut t Tmux) is_running() !bool {
|
pub fn (mut t Tmux) is_running() !bool {
|
||||||
res := os.execute('tmux info')
|
res := os.execute('tmux info')
|
||||||
if res.exit_code != 0 {
|
if res.exit_code != 0 {
|
||||||
if res.output.contains('no server running') {
|
if is_tmux_server_not_running_error(res.output) {
|
||||||
// console.print_debug(" TMUX NOT RUNNING")
|
// console.print_debug(" TMUX NOT RUNNING")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,83 @@ import time
|
|||||||
import os
|
import os
|
||||||
import freeflowuniverse.herolib.ui.console
|
import freeflowuniverse.herolib.ui.console
|
||||||
|
|
||||||
|
// Constants for memory calculations
|
||||||
|
const kb_to_bytes_factor = 1024
|
||||||
|
const memory_display_precision = 3
|
||||||
|
const memory_cache_ttl_seconds = 300 // Cache system memory for 5 minutes
|
||||||
|
|
||||||
|
// Global cache for system memory to avoid repeated syscalls
|
||||||
|
struct MemoryCache {
|
||||||
|
mut:
|
||||||
|
total_bytes u64
|
||||||
|
cached_at time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
__global (
|
||||||
|
memory_cache MemoryCache
|
||||||
|
)
|
||||||
|
|
||||||
|
// Platform-specific memory detection
|
||||||
|
fn get_total_system_memory() !u64 {
|
||||||
|
$if macos {
|
||||||
|
result := osal.execute_silent('sysctl -n hw.memsize') or {
|
||||||
|
return error('Failed to get system memory on macOS: ${err}')
|
||||||
|
}
|
||||||
|
return result.trim_space().u64()
|
||||||
|
} $else $if linux {
|
||||||
|
// Read from /proc/meminfo
|
||||||
|
content := os.read_file('/proc/meminfo') or {
|
||||||
|
return error('Failed to read /proc/meminfo on Linux: ${err}')
|
||||||
|
}
|
||||||
|
for line in content.split_into_lines() {
|
||||||
|
if line.starts_with('MemTotal:') {
|
||||||
|
parts := line.split_any(' \t').filter(it.len > 0)
|
||||||
|
if parts.len >= 2 {
|
||||||
|
kb_value := parts[1].u64()
|
||||||
|
return kb_value * kb_to_bytes_factor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return error('Could not parse MemTotal from /proc/meminfo')
|
||||||
|
} $else {
|
||||||
|
return error('Unsupported platform for memory detection')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get cached or fresh system memory
|
||||||
|
fn get_system_memory_cached() u64 {
|
||||||
|
now := time.now()
|
||||||
|
|
||||||
|
// Check if cache is valid
|
||||||
|
if memory_cache.total_bytes > 0
|
||||||
|
&& now.unix() - memory_cache.cached_at.unix() < memory_cache_ttl_seconds {
|
||||||
|
return memory_cache.total_bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh cache
|
||||||
|
total_memory := get_total_system_memory() or {
|
||||||
|
console.print_debug('Failed to get system memory: ${err}')
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
memory_cache.total_bytes = total_memory
|
||||||
|
memory_cache.cached_at = now
|
||||||
|
|
||||||
|
return total_memory
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate accurate memory percentage
|
||||||
|
fn calculate_memory_percentage(memory_bytes u64, ps_fallback_percent f64) f64 {
|
||||||
|
total_memory := get_system_memory_cached()
|
||||||
|
|
||||||
|
if total_memory > 0 {
|
||||||
|
return (f64(memory_bytes) / f64(total_memory)) * 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to ps value if system memory detection fails
|
||||||
|
return ps_fallback_percent
|
||||||
|
}
|
||||||
|
|
||||||
@[heap]
|
@[heap]
|
||||||
struct Pane {
|
struct Pane {
|
||||||
pub mut:
|
pub mut:
|
||||||
@@ -22,28 +99,47 @@ pub mut:
|
|||||||
|
|
||||||
pub fn (mut p Pane) stats() !ProcessStats {
|
pub fn (mut p Pane) stats() !ProcessStats {
|
||||||
if p.pid == 0 {
|
if p.pid == 0 {
|
||||||
return ProcessStats{}
|
return ProcessStats{
|
||||||
|
cpu_percent: 0.0
|
||||||
|
memory_percent: 0.0
|
||||||
|
memory_bytes: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use ps command to get CPU and memory stats
|
// Use ps command to get CPU and memory stats (cross-platform compatible)
|
||||||
cmd := 'ps -p ${p.pid} -o %cpu,%mem,rss --no-headers'
|
cmd := 'ps -p ${p.pid} -o %cpu,%mem,rss'
|
||||||
result := osal.execute_silent(cmd) or {
|
result := osal.execute_silent(cmd) or {
|
||||||
return error('Cannot get stats for PID ${p.pid}: ${err}')
|
return error('Cannot get stats for PID ${p.pid}: ${err}')
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.trim_space() == '' {
|
lines := result.split_into_lines()
|
||||||
|
if lines.len < 2 {
|
||||||
return error('Process ${p.pid} not found')
|
return error('Process ${p.pid} not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := result.trim_space().split_any(' \t').filter(it != '')
|
// Skip header line, get data line
|
||||||
if parts.len < 3 {
|
data_line := lines[1].trim_space()
|
||||||
return error('Invalid ps output: ${result}')
|
if data_line == '' {
|
||||||
|
return error('Process ${p.pid} not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parts := data_line.split_any(' \t').filter(it != '')
|
||||||
|
if parts.len < 3 {
|
||||||
|
return error('Invalid ps output: ${data_line}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse values from ps output
|
||||||
|
cpu_percent := parts[0].f64()
|
||||||
|
ps_memory_percent := parts[1].f64()
|
||||||
|
memory_bytes := parts[2].u64() * kb_to_bytes_factor
|
||||||
|
|
||||||
|
// Calculate accurate memory percentage using cached system memory
|
||||||
|
memory_percent := calculate_memory_percentage(memory_bytes, ps_memory_percent)
|
||||||
|
|
||||||
return ProcessStats{
|
return ProcessStats{
|
||||||
cpu_percent: parts[0].f64()
|
cpu_percent: cpu_percent
|
||||||
memory_percent: parts[1].f64()
|
memory_percent: memory_percent
|
||||||
memory_bytes: parts[2].u64() * 1024 // ps returns KB, convert to bytes
|
memory_bytes: memory_bytes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,8 +150,14 @@ pub mut:
|
|||||||
offset int
|
offset int
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn (mut p Pane) logs_get_new(reset bool) ![]TMuxLogEntry {
|
pub struct LogsGetArgs {
|
||||||
if reset {
|
pub mut:
|
||||||
|
reset bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// get new logs since last call
|
||||||
|
pub fn (mut p Pane) logs_get_new(args LogsGetArgs) ![]TMuxLogEntry {
|
||||||
|
if args.reset {
|
||||||
p.last_output_offset = 0
|
p.last_output_offset = 0
|
||||||
}
|
}
|
||||||
// Capture pane content with line numbers
|
// Capture pane content with line numbers
|
||||||
@@ -141,3 +243,66 @@ pub fn (mut p Pane) processinfo_main() !osal.ProcessInfo {
|
|||||||
|
|
||||||
return osal.processinfo_get(p.pid)!
|
return osal.processinfo_get(p.pid)!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send a command to this pane
|
||||||
|
pub fn (mut p Pane) send_command(command string) ! {
|
||||||
|
cmd := 'tmux send-keys -t ${p.window.session.name}:@${p.window.id}.%${p.id} "${command}" Enter'
|
||||||
|
osal.execute_silent(cmd) or { return error('Cannot send command to pane %${p.id}: ${err}') }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send raw keys to this pane (without Enter)
|
||||||
|
pub fn (mut p Pane) send_keys(keys string) ! {
|
||||||
|
cmd := 'tmux send-keys -t ${p.window.session.name}:@${p.window.id}.%${p.id} "${keys}"'
|
||||||
|
osal.execute_silent(cmd) or { return error('Cannot send keys to pane %${p.id}: ${err}') }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill this specific pane
|
||||||
|
pub fn (mut p Pane) kill() ! {
|
||||||
|
cmd := 'tmux kill-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id}'
|
||||||
|
osal.execute_silent(cmd) or { return error('Cannot kill pane %${p.id}: ${err}') }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select/activate this pane
|
||||||
|
pub fn (mut p Pane) select() ! {
|
||||||
|
cmd := 'tmux select-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id}'
|
||||||
|
osal.execute_silent(cmd) or { return error('Cannot select pane %${p.id}: ${err}') }
|
||||||
|
p.active = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@[params]
|
||||||
|
pub struct PaneResizeArgs {
|
||||||
|
pub mut:
|
||||||
|
direction string = 'right' // 'up', 'down', 'left', 'right'
|
||||||
|
cells int = 5 // number of cells to resize by
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize this pane
|
||||||
|
pub fn (mut p Pane) resize(args PaneResizeArgs) ! {
|
||||||
|
direction_flag := match args.direction.to_lower() {
|
||||||
|
'up', 'u' { '-U' }
|
||||||
|
'down', 'd' { '-D' }
|
||||||
|
'left', 'l' { '-L' }
|
||||||
|
'right', 'r' { '-R' }
|
||||||
|
else { return error('Invalid resize direction: ${args.direction}. Use up, down, left, or right') }
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := 'tmux resize-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id} ${direction_flag} ${args.cells}'
|
||||||
|
osal.execute_silent(cmd) or { return error('Cannot resize pane %${p.id}: ${err}') }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience methods for resizing
|
||||||
|
pub fn (mut p Pane) resize_up(cells int) ! {
|
||||||
|
p.resize(direction: 'up', cells: cells)!
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (mut p Pane) resize_down(cells int) ! {
|
||||||
|
p.resize(direction: 'down', cells: cells)!
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (mut p Pane) resize_left(cells int) ! {
|
||||||
|
p.resize(direction: 'left', cells: cells)!
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (mut p Pane) resize_right(cells int) ! {
|
||||||
|
p.resize(direction: 'right', cells: cells)!
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,9 +5,30 @@ import freeflowuniverse.herolib.core.texttools
|
|||||||
import freeflowuniverse.herolib.ui.console
|
import freeflowuniverse.herolib.ui.console
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
// Check if error message indicates tmux server is not running
|
||||||
|
fn is_tmux_server_not_running_error(error_msg string) bool {
|
||||||
|
// Common tmux server not running error patterns
|
||||||
|
tmux_not_running_patterns := [
|
||||||
|
'no server running',
|
||||||
|
'error connecting to',
|
||||||
|
'No such file or directory', // when socket doesn't exist
|
||||||
|
]
|
||||||
|
|
||||||
|
error_lower := error_msg.to_lower()
|
||||||
|
for pattern in tmux_not_running_patterns {
|
||||||
|
if error_lower.contains(pattern.to_lower()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
fn (mut t Tmux) scan_add(line string) !&Pane {
|
fn (mut t Tmux) scan_add(line string) !&Pane {
|
||||||
// Parse the line to get session, window, and pane info
|
// Parse the line to get session, window, and pane info
|
||||||
line_arr := line.split('|')
|
line_arr := line.split('|')
|
||||||
|
if line_arr.len < 6 {
|
||||||
|
return error('Invalid tmux pane line format: ${line}')
|
||||||
|
}
|
||||||
session_name := line_arr[0]
|
session_name := line_arr[0]
|
||||||
window_name := line_arr[1]
|
window_name := line_arr[1]
|
||||||
window_id := line_arr[2]
|
window_id := line_arr[2]
|
||||||
@@ -16,6 +37,11 @@ fn (mut t Tmux) scan_add(line string) !&Pane {
|
|||||||
pane_pid := line_arr[5]
|
pane_pid := line_arr[5]
|
||||||
pane_start_command := line_arr[6] or { '' }
|
pane_start_command := line_arr[6] or { '' }
|
||||||
|
|
||||||
|
// Skip if window name is empty
|
||||||
|
if window_name.len == 0 {
|
||||||
|
return error('Window name is empty in line: ${line}')
|
||||||
|
}
|
||||||
|
|
||||||
wid := (window_id.replace('@', '')).int()
|
wid := (window_id.replace('@', '')).int()
|
||||||
pid := (pane_id.replace('%', '')).int()
|
pid := (pane_id.replace('%', '')).int()
|
||||||
|
|
||||||
@@ -71,7 +97,7 @@ pub fn (mut t Tmux) scan() ! {
|
|||||||
|
|
||||||
cmd_list_session := "tmux list-sessions -F '#{session_name}'"
|
cmd_list_session := "tmux list-sessions -F '#{session_name}'"
|
||||||
exec_list := osal.exec(cmd: cmd_list_session, stdout: false, name: 'tmux_list') or {
|
exec_list := osal.exec(cmd: cmd_list_session, stdout: false, name: 'tmux_list') or {
|
||||||
if err.msg().contains('no server running') {
|
if is_tmux_server_not_running_error(err.msg()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return error('could not execute list sessions.\n${err}')
|
return error('could not execute list sessions.\n${err}')
|
||||||
|
|||||||
@@ -63,8 +63,11 @@ pub fn (mut s Session) scan() ! {
|
|||||||
for line in result.split_into_lines() {
|
for line in result.split_into_lines() {
|
||||||
if line.contains('|') {
|
if line.contains('|') {
|
||||||
parts := line.split('|')
|
parts := line.split('|')
|
||||||
if parts.len >= 2 {
|
if parts.len >= 3 && parts[0].len > 0 && parts[1].len > 0 {
|
||||||
window_name := texttools.name_fix(parts[0])
|
window_name := texttools.name_fix(parts[0])
|
||||||
|
if window_name.len == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
window_id := parts[1].replace('@', '').int()
|
window_id := parts[1].replace('@', '').int()
|
||||||
window_active := parts[2] == '1'
|
window_active := parts[2] == '1'
|
||||||
|
|
||||||
@@ -73,7 +76,7 @@ pub fn (mut s Session) scan() ! {
|
|||||||
// Update existing window or create new one
|
// Update existing window or create new one
|
||||||
mut found := false
|
mut found := false
|
||||||
for mut w in s.windows {
|
for mut w in s.windows {
|
||||||
if w.name == window_name {
|
if w.name.len > 0 && window_name.len > 0 && w.name == window_name {
|
||||||
w.id = window_id
|
w.id = window_id
|
||||||
w.active = window_active
|
w.active = window_active
|
||||||
w.scan()! // Scan panes for this window
|
w.scan()! // Scan panes for this window
|
||||||
@@ -99,7 +102,7 @@ pub fn (mut s Session) scan() ! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove windows that no longer exist in tmux
|
// Remove windows that no longer exist in tmux
|
||||||
s.windows = s.windows.filter(current_windows[it.name] == true)
|
s.windows = s.windows.filter(it.name.len > 0 && current_windows[it.name] == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// window_name is the name of the window in session main (will always be called session main)
|
// window_name is the name of the window in session main (will always be called session main)
|
||||||
@@ -115,7 +118,7 @@ pub fn (mut s Session) scan() ! {
|
|||||||
// reset bool
|
// reset bool
|
||||||
// }
|
// }
|
||||||
// ```
|
// ```
|
||||||
pub fn (mut s Session) window_new(args WindowArgs) !Window {
|
pub fn (mut s Session) window_new(args WindowArgs) !&Window {
|
||||||
$if debug {
|
$if debug {
|
||||||
console.print_header(' start window: \n${args}')
|
console.print_header(' start window: \n${args}')
|
||||||
}
|
}
|
||||||
@@ -127,7 +130,7 @@ pub fn (mut s Session) window_new(args WindowArgs) !Window {
|
|||||||
return error('cannot create new window it already exists, window ${namel} in session:${s.name}')
|
return error('cannot create new window it already exists, window ${namel} in session:${s.name}')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mut w := Window{
|
mut w := &Window{
|
||||||
session: &s
|
session: &s
|
||||||
name: namel
|
name: namel
|
||||||
panes: []&Pane{}
|
panes: []&Pane{}
|
||||||
@@ -211,9 +214,12 @@ fn (mut s Session) window_exist(args_ WindowGetArgs) bool {
|
|||||||
|
|
||||||
pub fn (mut s Session) window_get(args_ WindowGetArgs) !&Window {
|
pub fn (mut s Session) window_get(args_ WindowGetArgs) !&Window {
|
||||||
mut args := args_
|
mut args := args_
|
||||||
|
if args.name.len == 0 {
|
||||||
|
return error('Window name cannot be empty')
|
||||||
|
}
|
||||||
args.name = texttools.name_fix(args.name)
|
args.name = texttools.name_fix(args.name)
|
||||||
for w in s.windows {
|
for w in s.windows {
|
||||||
if w.name == args.name {
|
if w.name.len > 0 && w.name == args.name {
|
||||||
if (args.id > 0 && w.id == args.id) || args.id == 0 {
|
if (args.id > 0 && w.id == args.id) || args.id == 0 {
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
@@ -252,3 +258,29 @@ pub fn (mut s Session) stop() ! {
|
|||||||
return error("Can't delete session ${s.name} - This may happen when session is not found: ${err}")
|
return error("Can't delete session ${s.name} - This may happen when session is not found: ${err}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run ttyd for this session so it can be accessed in the browser
|
||||||
|
pub fn (mut s Session) run_ttyd(args TtydArgs) ! {
|
||||||
|
target := '${s.name}'
|
||||||
|
|
||||||
|
// Add -W flag for write access if editable mode is enabled
|
||||||
|
mut ttyd_flags := '-p ${args.port}'
|
||||||
|
if args.editable {
|
||||||
|
ttyd_flags += ' -W'
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := 'nohup ttyd ${ttyd_flags} tmux attach -t ${target} >/dev/null 2>&1 &'
|
||||||
|
|
||||||
|
code := os.system(cmd)
|
||||||
|
if code != 0 {
|
||||||
|
return error('Failed to start ttyd on port ${args.port} for session ${s.name}')
|
||||||
|
}
|
||||||
|
|
||||||
|
mode_str := if args.editable { 'editable' } else { 'read-only' }
|
||||||
|
println('ttyd started for session ${s.name} at http://localhost:${args.port} (${mode_str} mode)')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward compatibility method - runs ttyd in read-only mode
|
||||||
|
pub fn (mut s Session) run_ttyd_readonly(port int) ! {
|
||||||
|
s.run_ttyd(port: port, editable: false)!
|
||||||
|
}
|
||||||
|
|||||||
@@ -175,3 +175,117 @@ pub fn (mut w Window) pane_active() ?&Pane {
|
|||||||
}
|
}
|
||||||
return none
|
return none
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@[params]
|
||||||
|
pub struct PaneSplitArgs {
|
||||||
|
pub mut:
|
||||||
|
cmd string // command to run in new pane
|
||||||
|
horizontal bool // true for horizontal split, false for vertical
|
||||||
|
env map[string]string // environment variables
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the active pane horizontally or vertically
|
||||||
|
pub fn (mut w Window) pane_split(args PaneSplitArgs) !&Pane {
|
||||||
|
mut cmd_to_run := args.cmd
|
||||||
|
if cmd_to_run == '' {
|
||||||
|
cmd_to_run = '/bin/bash'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build environment arguments
|
||||||
|
mut env_args := ''
|
||||||
|
for key, value in args.env {
|
||||||
|
env_args += ' -e ${key}="${value}"'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose split direction
|
||||||
|
split_flag := if args.horizontal { '-h' } else { '-v' }
|
||||||
|
|
||||||
|
// Execute tmux split-window command
|
||||||
|
res_opt := "-P -F '#{session_name}|#{window_name}|#{window_id}|#{pane_active}|#{pane_id}|#{pane_pid}|#{pane_start_command}'"
|
||||||
|
cmd := 'tmux split-window ${split_flag} ${res_opt}${env_args} -t ${w.session.name}:@${w.id} \'${cmd_to_run}\''
|
||||||
|
|
||||||
|
console.print_debug('Splitting pane: ${cmd}')
|
||||||
|
|
||||||
|
res := osal.exec(cmd: cmd, stdout: false, name: 'tmux_pane_split') or {
|
||||||
|
return error("Can't split pane in window ${w.name}: ${err}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the result to get new pane info
|
||||||
|
line_arr := res.output.split('|')
|
||||||
|
if line_arr.len < 7 {
|
||||||
|
return error('Invalid tmux split-window output: ${res.output}')
|
||||||
|
}
|
||||||
|
|
||||||
|
pane_id := line_arr[4].replace('%', '').int()
|
||||||
|
pane_pid := line_arr[5].int()
|
||||||
|
pane_active := line_arr[3] == '1'
|
||||||
|
pane_cmd := line_arr[6] or { '' }
|
||||||
|
|
||||||
|
// Create new pane object
|
||||||
|
mut new_pane := Pane{
|
||||||
|
window: &w
|
||||||
|
id: pane_id
|
||||||
|
pid: pane_pid
|
||||||
|
active: pane_active
|
||||||
|
cmd: pane_cmd
|
||||||
|
env: args.env
|
||||||
|
created_at: time.now()
|
||||||
|
last_output_offset: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to window's panes and rescan to get current state
|
||||||
|
w.panes << &new_pane
|
||||||
|
w.scan()!
|
||||||
|
|
||||||
|
// Return reference to the new pane
|
||||||
|
for mut pane in w.panes {
|
||||||
|
if pane.id == pane_id {
|
||||||
|
return pane
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return error('Could not find newly created pane with ID ${pane_id}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split pane horizontally (side by side)
|
||||||
|
pub fn (mut w Window) pane_split_horizontal(cmd string) !&Pane {
|
||||||
|
return w.pane_split(cmd: cmd, horizontal: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split pane vertically (top and bottom)
|
||||||
|
pub fn (mut w Window) pane_split_vertical(cmd string) !&Pane {
|
||||||
|
return w.pane_split(cmd: cmd, horizontal: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@[params]
|
||||||
|
pub struct TtydArgs {
|
||||||
|
pub mut:
|
||||||
|
port int
|
||||||
|
editable bool // if true, allows write access to the terminal
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run ttyd for this window so it can be accessed in the browser
|
||||||
|
pub fn (mut w Window) run_ttyd(args TtydArgs) ! {
|
||||||
|
target := '${w.session.name}:@${w.id}'
|
||||||
|
|
||||||
|
// Add -W flag for write access if editable mode is enabled
|
||||||
|
mut ttyd_flags := '-p ${args.port}'
|
||||||
|
if args.editable {
|
||||||
|
ttyd_flags += ' -W'
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := 'nohup ttyd ${ttyd_flags} tmux attach -t ${target} >/dev/null 2>&1 &'
|
||||||
|
|
||||||
|
code := os.system(cmd)
|
||||||
|
if code != 0 {
|
||||||
|
return error('Failed to start ttyd on port ${args.port} for window ${w.name}')
|
||||||
|
}
|
||||||
|
|
||||||
|
mode_str := if args.editable { 'editable' } else { 'read-only' }
|
||||||
|
println('ttyd started for window ${w.name} at http://localhost:${args.port} (${mode_str} mode)')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward compatibility method - runs ttyd in read-only mode
|
||||||
|
pub fn (mut w Window) run_ttyd_readonly(port int) ! {
|
||||||
|
w.run_ttyd(port: port, editable: false)!
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
module podman
|
module herocontainers
|
||||||
|
|
||||||
import os
|
|
||||||
import freeflowuniverse.herolib.osal.core as osal { exec }
|
import freeflowuniverse.herolib.osal.core as osal { exec }
|
||||||
import freeflowuniverse.herolib.core
|
import freeflowuniverse.herolib.core
|
||||||
import freeflowuniverse.herolib.installers.virt.podman as podman_installer
|
import freeflowuniverse.herolib.installers.virt.podman as podman_installer
|
||||||
import freeflowuniverse.herolib.installers.lang.herolib
|
|
||||||
import freeflowuniverse.herolib.ui.console
|
|
||||||
import json
|
|
||||||
import rand
|
|
||||||
|
|
||||||
@[heap]
|
@[heap]
|
||||||
pub struct PodmanFactory {
|
pub struct PodmanFactory {
|
||||||
pub mut:
|
pub mut:
|
||||||
// sshkeys_allowed []string // all keys here have access over ssh into the machine, when ssh enabled
|
// sshkeys_allowed []string // all keys here have access over ssh into the machine, when ssh enabled
|
||||||
images []Image
|
images []Image
|
||||||
containers []Container
|
containers []Container
|
||||||
builders []Builder
|
buildpath string
|
||||||
buildpath string
|
|
||||||
// cache bool = true
|
// cache bool = true
|
||||||
// push bool
|
// push bool
|
||||||
// platform []BuildPlatformType // used to build
|
// platform []BuildPlatformType // used to build
|
||||||
@@ -24,30 +18,6 @@ pub mut:
|
|||||||
prefix string
|
prefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildPlatformType represents different build platforms
|
|
||||||
pub enum BuildPlatformType {
|
|
||||||
linux_amd64
|
|
||||||
linux_arm64
|
|
||||||
darwin_amd64
|
|
||||||
darwin_arm64
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainerRuntimeConfig represents container runtime configuration
|
|
||||||
pub struct ContainerRuntimeConfig {
|
|
||||||
pub mut:
|
|
||||||
name string
|
|
||||||
image string
|
|
||||||
command []string
|
|
||||||
env map[string]string
|
|
||||||
ports []string
|
|
||||||
volumes []string
|
|
||||||
detach bool = true
|
|
||||||
remove bool
|
|
||||||
interactive bool
|
|
||||||
tty bool
|
|
||||||
working_dir string
|
|
||||||
entrypoint string
|
|
||||||
}
|
|
||||||
|
|
||||||
@[params]
|
@[params]
|
||||||
pub struct NewArgs {
|
pub struct NewArgs {
|
||||||
@@ -57,38 +27,12 @@ pub mut:
|
|||||||
herocompile bool
|
herocompile bool
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(args_ NewArgs) !PodmanFactory {
|
|
||||||
mut args := args_
|
|
||||||
|
|
||||||
// Support both Linux and macOS
|
|
||||||
if !core.is_linux()! && !core.is_osx()! {
|
|
||||||
return error('only linux and macOS supported as host for now')
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.install {
|
if args.install {
|
||||||
mut podman_installer0 := podman_installer.get()!
|
mut podman_installer0 := podman_installer.get()!
|
||||||
podman_installer0.install()!
|
podman_installer0.install()!
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure podman machine is available (macOS/Windows)
|
|
||||||
ensure_machine_available() or {
|
|
||||||
console.print_debug('Warning: Failed to ensure podman machine availability: ${err}')
|
|
||||||
console.print_debug('Continuing anyway - podman operations may fail if machine is not running')
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.herocompile {
|
|
||||||
herolib.check()! // will check if install, if not will do
|
|
||||||
herolib.hero_compile(reset: true)!
|
|
||||||
}
|
|
||||||
|
|
||||||
mut factory := PodmanFactory{}
|
|
||||||
factory.init()!
|
|
||||||
if args.reset {
|
|
||||||
factory.reset_all()!
|
|
||||||
}
|
|
||||||
|
|
||||||
return factory
|
|
||||||
}
|
|
||||||
|
|
||||||
fn (mut e PodmanFactory) init() ! {
|
fn (mut e PodmanFactory) init() ! {
|
||||||
if e.buildpath == '' {
|
if e.buildpath == '' {
|
||||||
@@ -118,294 +62,31 @@ pub fn (mut e PodmanFactory) reset_all() ! {
|
|||||||
exec(cmd: 'podman rmi -a -f', stdout: false)!
|
exec(cmd: 'podman rmi -a -f', stdout: false)!
|
||||||
e.builders_delete_all()!
|
e.builders_delete_all()!
|
||||||
osal.done_reset()!
|
osal.done_reset()!
|
||||||
// Only check systemctl on Linux
|
if core.platform()! == core.PlatformType.arch {
|
||||||
if core.is_linux()! && core.platform()! == core.PlatformType.arch {
|
|
||||||
exec(cmd: 'systemctl status podman.socket', stdout: false)!
|
exec(cmd: 'systemctl status podman.socket', stdout: false)!
|
||||||
}
|
}
|
||||||
e.load()!
|
e.load()!
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get free port - simple implementation
|
// Get free port
|
||||||
pub fn (mut e PodmanFactory) get_free_port() ?int {
|
pub fn (mut e PodmanFactory) get_free_port() ?int {
|
||||||
// Simple implementation - return a random port in the range
|
mut used_ports := []int{}
|
||||||
// In a real implementation, you'd check for port availability
|
mut range := []int{}
|
||||||
return 20000 + (rand.int() % 20000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create_from_buildah_image creates a podman container from a buildah image
|
for c in e.containers {
|
||||||
pub fn (mut e PodmanFactory) create_from_buildah_image(image_name string, config ContainerRuntimeConfig) !string {
|
for p in c.forwarded_ports {
|
||||||
// Check if image exists in podman
|
used_ports << p.split(':')[0].int()
|
||||||
image_exists := e.image_exists(repo: image_name) or { false }
|
|
||||||
|
|
||||||
if !image_exists {
|
|
||||||
// Try to transfer from buildah to podman
|
|
||||||
exec(cmd: 'buildah push ${image_name} containers-storage:${image_name}') or {
|
|
||||||
return new_image_error('create_from_buildah', image_name, 1, 'Failed to transfer image from buildah',
|
|
||||||
err.msg())
|
|
||||||
}
|
|
||||||
// Reload images after transfer
|
|
||||||
e.images_load()!
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create container using the image
|
|
||||||
args := ContainerCreateArgs{
|
|
||||||
name: config.name
|
|
||||||
image_repo: image_name
|
|
||||||
command: config.command.join(' ')
|
|
||||||
env: config.env
|
|
||||||
forwarded_ports: config.ports
|
|
||||||
mounted_volumes: config.volumes
|
|
||||||
detach: config.detach
|
|
||||||
remove_when_done: config.remove
|
|
||||||
interactive: config.interactive
|
|
||||||
}
|
|
||||||
|
|
||||||
container := e.container_create(args)!
|
|
||||||
return container.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// build_and_run_workflow performs a complete buildah build to podman run workflow
|
|
||||||
pub fn (mut e PodmanFactory) build_and_run_workflow(build_config ContainerRuntimeConfig, run_config ContainerRuntimeConfig, image_name string) !string {
|
|
||||||
// Simple implementation - just create a container from the image
|
|
||||||
// In a full implementation, this would coordinate with buildah
|
|
||||||
return e.create_from_buildah_image(image_name, run_config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple API functions (from client.v) - these use a default factory instance
|
|
||||||
|
|
||||||
// run_container runs a container with the specified image and options.
|
|
||||||
// Returns the container ID of the created container.
|
|
||||||
pub fn run_container(image string, options RunOptions) !string {
|
|
||||||
mut factory := new(install: false)!
|
|
||||||
|
|
||||||
// Convert RunOptions to ContainerCreateArgs
|
|
||||||
args := ContainerCreateArgs{
|
|
||||||
name: options.name
|
|
||||||
image_repo: image
|
|
||||||
command: options.command.join(' ')
|
|
||||||
env: options.env
|
|
||||||
forwarded_ports: options.ports
|
|
||||||
mounted_volumes: options.volumes
|
|
||||||
detach: options.detach
|
|
||||||
interactive: options.interactive
|
|
||||||
remove_when_done: options.remove
|
|
||||||
// Map other options as needed
|
|
||||||
}
|
|
||||||
|
|
||||||
container := factory.container_create(args)!
|
|
||||||
return container.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// exec_podman executes a podman command with the given arguments
|
|
||||||
fn exec_podman(args []string) !string {
|
|
||||||
cmd := 'podman ' + args.join(' ')
|
|
||||||
result := exec(cmd: cmd, stdout: false)!
|
|
||||||
return result.output
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse_json_output parses JSON output into the specified type
|
|
||||||
fn parse_json_output[T](output string) ![]T {
|
|
||||||
if output.trim_space() == '' {
|
|
||||||
return []T{}
|
|
||||||
}
|
|
||||||
return json.decode([]T, output)!
|
|
||||||
}
|
|
||||||
|
|
||||||
// list_containers lists running containers, or all containers if all=true.
|
|
||||||
pub fn list_containers(all bool) ![]PodmanContainer {
|
|
||||||
mut args := ['ps', '--format', 'json']
|
|
||||||
if all {
|
|
||||||
args << '--all'
|
|
||||||
}
|
|
||||||
|
|
||||||
output := exec_podman(args)!
|
|
||||||
return parse_json_output[PodmanContainer](output) or {
|
|
||||||
return new_container_error('list', 'containers', 1, err.msg(), err.msg())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// list_images lists all available images.
|
|
||||||
pub fn list_images() ![]PodmanImage {
|
|
||||||
output := exec_podman(['images', '--format', 'json'])!
|
|
||||||
return parse_json_output[PodmanImage](output) or {
|
|
||||||
return new_image_error('list', 'images', 1, err.msg(), err.msg())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// inspect_container returns detailed information about a container.
|
|
||||||
pub fn inspect_container(id string) !PodmanContainer {
|
|
||||||
output := exec_podman(['inspect', '--format', 'json', id])!
|
|
||||||
|
|
||||||
containers := parse_json_output[PodmanContainer](output) or {
|
|
||||||
return new_container_error('inspect', id, 1, err.msg(), err.msg())
|
|
||||||
}
|
|
||||||
|
|
||||||
if containers.len == 0 {
|
|
||||||
return new_container_error('inspect', id, 1, 'Container not found', 'Container ${id} not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
return containers[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// stop_container stops a running container.
|
|
||||||
pub fn stop_container(id string) ! {
|
|
||||||
exec_podman(['stop', id]) or { return new_container_error('stop', id, 1, err.msg(), err.msg()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove_container removes a container.
|
|
||||||
// If force=true, the container will be forcefully removed even if running.
|
|
||||||
pub fn remove_container(id string, force bool) ! {
|
|
||||||
mut args := ['rm']
|
|
||||||
if force {
|
|
||||||
args << '-f'
|
|
||||||
}
|
|
||||||
args << id
|
|
||||||
|
|
||||||
exec_podman(args) or { return new_container_error('remove', id, 1, err.msg(), err.msg()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove_image removes an image by ID or name.
|
|
||||||
// If force=true, the image will be forcefully removed even if in use.
|
|
||||||
pub fn remove_image(id string, force bool) ! {
|
|
||||||
mut args := ['rmi']
|
|
||||||
if force {
|
|
||||||
args << '-f'
|
|
||||||
}
|
|
||||||
args << id
|
|
||||||
|
|
||||||
exec_podman(args) or { return new_image_error('remove', id, 1, err.msg(), err.msg()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// MACHINE MANAGEMENT (macOS/Windows support)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
// Machine represents a podman machine (VM)
|
|
||||||
pub struct Machine {
|
|
||||||
pub:
|
|
||||||
name string
|
|
||||||
vm_type string
|
|
||||||
created string
|
|
||||||
last_up string
|
|
||||||
cpus string
|
|
||||||
memory string
|
|
||||||
disk string
|
|
||||||
running bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure_machine_available ensures a podman machine is available and running
|
|
||||||
// This is required on macOS and Windows where podman runs in a VM
|
|
||||||
pub fn ensure_machine_available() ! {
|
|
||||||
// Only needed on macOS and Windows
|
|
||||||
if os.user_os() !in ['macos', 'windows'] {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if any machine exists
|
|
||||||
machines := list_machines() or { []Machine{} }
|
|
||||||
|
|
||||||
if machines.len == 0 {
|
|
||||||
console.print_debug('No podman machine found, initializing...')
|
|
||||||
machine_init() or { return error('Failed to initialize podman machine: ${err}') }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if a machine is running
|
|
||||||
if !is_any_machine_running() {
|
|
||||||
console.print_debug('Starting podman machine...')
|
|
||||||
machine_start() or { return error('Failed to start podman machine: ${err}') }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// list_machines returns all available podman machines
|
|
||||||
pub fn list_machines() ![]Machine {
|
|
||||||
return parse_machine_list_text()!
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse_machine_list_text parses text format output as fallback
|
|
||||||
fn parse_machine_list_text() ![]Machine {
|
|
||||||
job := exec(cmd: 'podman machine list', stdout: false) or {
|
|
||||||
return error('Failed to list podman machines: ${err}')
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := job.output.split_into_lines()
|
|
||||||
if lines.len <= 1 {
|
|
||||||
return []Machine{} // No machines or only header
|
|
||||||
}
|
|
||||||
|
|
||||||
mut machines := []Machine{}
|
|
||||||
for i in 1 .. lines.len {
|
|
||||||
line := lines[i].trim_space()
|
|
||||||
if line == '' {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
fields := line.split_any(' \t').filter(it.trim_space() != '')
|
|
||||||
if fields.len >= 6 {
|
|
||||||
machine := Machine{
|
|
||||||
name: fields[0]
|
|
||||||
vm_type: fields[1]
|
|
||||||
created: fields[2]
|
|
||||||
last_up: fields[3]
|
|
||||||
cpus: fields[4]
|
|
||||||
memory: fields[5]
|
|
||||||
disk: if fields.len > 6 { fields[6] } else { '' }
|
|
||||||
running: line.contains('Currently running') || line.contains('Running')
|
|
||||||
}
|
|
||||||
machines << machine
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return machines
|
for i in 20000 .. 40000 {
|
||||||
}
|
if i !in used_ports {
|
||||||
|
range << i
|
||||||
// is_any_machine_running checks if any podman machine is currently running
|
}
|
||||||
pub fn is_any_machine_running() bool {
|
|
||||||
machines := list_machines() or { return false }
|
|
||||||
return machines.any(it.running)
|
|
||||||
}
|
|
||||||
|
|
||||||
// machine_init initializes a new podman machine with default settings
|
|
||||||
pub fn machine_init() ! {
|
|
||||||
machine_init_named('podman-machine-default')!
|
|
||||||
}
|
|
||||||
|
|
||||||
// machine_init_named initializes a new podman machine with specified name
|
|
||||||
pub fn machine_init_named(name string) ! {
|
|
||||||
console.print_debug('Initializing podman machine: ${name}')
|
|
||||||
exec(cmd: 'podman machine init ${name}', stdout: false) or {
|
|
||||||
return error('Failed to initialize podman machine: ${err}')
|
|
||||||
}
|
}
|
||||||
console.print_debug('✅ Podman machine initialized: ${name}')
|
// arrays.shuffle<int>(mut range, 0)
|
||||||
}
|
if range.len == 0 {
|
||||||
|
return none
|
||||||
// machine_start starts the default podman machine
|
|
||||||
pub fn machine_start() ! {
|
|
||||||
machine_start_named('')!
|
|
||||||
}
|
|
||||||
|
|
||||||
// machine_start_named starts a specific podman machine
|
|
||||||
pub fn machine_start_named(name string) ! {
|
|
||||||
mut cmd := 'podman machine start'
|
|
||||||
if name != '' {
|
|
||||||
cmd += ' ${name}'
|
|
||||||
}
|
}
|
||||||
|
return range[0]
|
||||||
console.print_debug('Starting podman machine...')
|
|
||||||
exec(cmd: cmd, stdout: false) or { return error('Failed to start podman machine: ${err}') }
|
|
||||||
console.print_debug('✅ Podman machine started')
|
|
||||||
}
|
|
||||||
|
|
||||||
// machine_stop stops the default podman machine
|
|
||||||
pub fn machine_stop() ! {
|
|
||||||
machine_stop_named('')!
|
|
||||||
}
|
|
||||||
|
|
||||||
// machine_stop_named stops a specific podman machine
|
|
||||||
pub fn machine_stop_named(name string) ! {
|
|
||||||
mut cmd := 'podman machine stop'
|
|
||||||
if name != '' {
|
|
||||||
cmd += ' ${name}'
|
|
||||||
}
|
|
||||||
|
|
||||||
exec(cmd: cmd, stdout: false) or { return error('Failed to stop podman machine: ${err}') }
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user