{"id":50777062,"url":"https://github.com/peterson-umoke/spyro-cli","last_synced_at":"2026-06-12T00:30:41.136Z","repository":{"id":364109290,"uuid":"1263296537","full_name":"peterson-umoke/spyro-cli","owner":"peterson-umoke","description":"Intelligent SSH tunneling and remote command CLI for developers","archived":false,"fork":false,"pushed_at":"2026-06-11T16:24:34.000Z","size":189,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-11T18:13:23.489Z","etag":null,"topics":["cli","devops","python","remote-management","ssh","tunneling"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/peterson-umoke.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-08T20:19:28.000Z","updated_at":"2026-06-11T16:32:19.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/peterson-umoke/spyro-cli","commit_stats":null,"previous_names":["peterson-umoke/spyro-cli"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/peterson-umoke/spyro-cli","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterson-umoke%2Fspyro-cli","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterson-umoke%2Fspyro-cli/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterson-umoke%2Fspyro-cli/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterson-umoke%2Fspyro-cli/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/peterson-umoke","download_url":"https://codeload.github.com/peterson-umoke/spyro-cli/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterson-umoke%2Fspyro-cli/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34224103,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-11T02:00:06.485Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","devops","python","remote-management","ssh","tunneling"],"created_at":"2026-06-12T00:30:36.086Z","updated_at":"2026-06-12T00:30:41.081Z","avatar_url":"https://github.com/peterson-umoke.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Spyro\n\nIntelligent SSH tunneling and remote command CLI for developers.\n\nSpyro automates SSH port-forwarding, remote command execution, database credential resolution, and file synchronization through a declarative `spyro.toml` configuration. It replaces manual `ssh -L` coordination, `scp` routines, and `autossh` daemons with a single, self-healing tool.\n\n## Why Spyro\n\nDevelopers working with remote servers spend significant time on repetitive SSH boilerplate: setting up port forwards, copying `.env` files, running artisan commands, syncing code. Spyro eliminates this by:\n\n- **Automating tunnels** with a self-healing supervisor that survives network drops, sleep/wake cycles, and process crashes\n- **Resolving credentials** from either local TOML config or remote `.env`/Rails config files\n- **Running remote commands** through a PTY engine that handles sudo escalation without leaking passwords\n- **Syncing files** in real-time using native OS filesystem watchers\n\n### The problem\n\nEvery developer working with remote servers knows this workflow:\n\n```bash\n# Terminal 1: SSH tunnel for MySQL\nssh -L 3306:localhost:3306 deploy@staging.example.com -N \u0026\n\n# Terminal 2: SSH tunnel for Redis\nssh -L 6379:localhost:6379 deploy@staging.example.com -N \u0026\n\n# Terminal 3: Run migrations\nssh deploy@staging.example.com \"cd /var/www/app \u0026\u0026 php artisan migrate\"\n\n# Terminal 4: Check queue workers\nssh deploy@staging.example.com \"sudo supervisorctl status\"\n\n# Terminal 5: Tail logs\nssh deploy@staging.example.com \"tail -f /var/www/app/storage/logs/laravel.log\"\n\n# Terminal 6: Push a file\nscp .env deploy@staging.example.com:/var/www/app/.env\n\n# Oh no, the tunnel died. Restart it.\n# Wait, which port was that tunnel on?\n# Did I use -N or -f?\n# What's the DB password again?\n```\n\nFive terminal tabs. Three forgotten port numbers. One crashed tunnel. And you haven't written a line of code yet.\n\n### The solution\n\n```bash\n# One command to start all tunnels\nspyro up staging\n\n# Run anything on the remote server\nspyro artisan migrate -p staging\nspyro supervisor status -p staging\nspyro logs laravel -p staging -f\n\n# Push files\nspyro cp .env :/var/www/app/.env -p staging\n\n# One command to stop everything\nspyro down\n```\n\nOne config file. One CLI. Zero terminal tabs. Tunnels that heal themselves. Passwords stored in your OS keychain, never in config files.\n\n## Installation\n\n### macOS / Linux (recommended)\n\n```bash\n# Install with uv (fast Python package manager)\nuv tool install git+https://github.com/peterson-umoke/spyro-cli.git\n\n# Verify installation\nspyro --version\nspyro --help\n```\n\n### From source (for contributors)\n\n```bash\ngit clone https://github.com/peterson-umoke/spyro-cli.git\ncd spyro-cli\n\n# Create virtual environment and install\nuv sync\n\n# Run without installing globally\nuv run spyro --version\n\n# Or install globally for daily use\nuv tool install .\n\n# With filesystem watcher support (for spyro sync/watch)\nuv tool install . --with watchdog\n```\n\n### Update\n\n```bash\nuv tool install --force git+https://github.com/peterson-umoke/spyro-cli.git\n```\n\n### Verify installation\n\n```bash\nspyro --version\nspyro doctor\n```\n\n`spyro doctor` runs a full audit: checks SSH connectivity, remote paths, port availability, and detects all running services (Redis, PHP-FPM, Supervisor, Nginx/Caddy, etc.).\n\n## Getting Started\n\n### Step 1: Create your config\n\n```bash\ncd ~/Projects/my-laravel-app\nspyro init\n```\n\nThis creates `spyro.toml` in your project root. Edit it with your server details:\n\n```toml\n[profiles.staging]\nhost = \"staging.example.com\"\nuser = \"deploy\"\nport = 22\nremote_path = \"/var/www/app\"\nartisan = true\nsudo = true\nforwarded_ports = [3306, 6379]\n\n[profiles.staging.db]\nhost = \"127.0.0.1\"\nport = 3306\nname = \"myapp_staging\"\nuser = \"forge\"\npassword = \"\"\ndriver = \"mysql\"\n```\n\n### Step 2: Store your password\n\n```bash\nspyro auth set -p staging\n# Enter password when prompted — stored in your OS keychain\n```\n\nYou'll never need to enter it again. Spyro reads it from macOS Keychain (or Linux Secret Service) automatically.\n\n### Step 3: Start working\n\n```bash\n# Start database + Redis tunnels\nspyro up staging\n\n# Run migrations\nspyro artisan migrate -p staging\n\n# Open a MySQL shell\nspyro db shell -p staging\n\n# Check queue workers\nspyro supervisor status -p staging\n\n# When done, stop all tunnels\nspyro down\n```\n\nThat's the entire workflow. No more terminal tabs, no more port forwarding scripts, no more \"what's the DB password?\"\n\n## Configuration\n\n### Profile basics\n\nEvery server connection is a \"profile\" in `spyro.toml`:\n\n```toml\n[profiles.staging]\nhost = \"staging.example.com\"     # Server IP or hostname (required)\nuser = \"deploy\"                  # SSH username (required)\nport = 22                        # SSH port (default: 22)\nkey = \"~/.ssh/id_ed25519\"       # SSH key (optional, uses default if empty)\nremote_path = \"/var/www/app\"     # Working directory on server (required)\nartisan = true                   # Enable Laravel artisan commands\nwordpress = false                # Enable WordPress/WP-CLI commands\nsudo = true                      # Allow sudo when needed\nforwarded_ports = [3306, 6379]   # Ports to tunnel locally\nenv_files = [\".env\"]             # Remote env files to scan for DB credentials\n```\n\n### Configuration fields\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `host` | string | *required* | Server hostname or IP address |\n| `user` | string | `deploy` | SSH username |\n| `port` | int | `22` | SSH port |\n| `key` | string | `\"\"` | Path to SSH private key (uses system default if empty) |\n| `remote_path` | string | `/var/www` | Working directory on remote server |\n| `forwarded_ports` | list[int] | `[]` | Remote ports to tunnel to localhost |\n| `artisan` | bool | `false` | Enable `spyro artisan` commands |\n| `wordpress` | bool | `false` | Enable `spyro wp` commands |\n| `sudo` | bool | `false` | Enable sudo escalation for commands |\n| `env_files` | list[str] | `[\".env\"]` | Remote env files to scan for DB credentials |\n\n### Database configuration\n\n```toml\n[profiles.staging.db]\nhost = \"127.0.0.1\"    # Always localhost (traffic goes through tunnel)\nport = 3306           # Must match a port in forwarded_ports\nname = \"myapp_staging\"\nuser = \"forge\"\npassword = \"\"         # Leave empty = auto-detect from remote .env\ndriver = \"mysql\"      # mysql, postgres, or sqlite\n```\n\n**Credential resolution** (dual-strategy):\n\n1. **Explicit** — If `password` is set in `spyro.toml`, use it\n2. **Auto-detect** — If empty, scan remote `.env` / config files for `DB_*` variables\n\n### Multiple users, same server\n\nDifferent services running as different system users? Create a profile per user:\n\n```toml\n[profiles.app-api]\nhost = \"192.168.1.100\"\nuser = \"deploy\"\nremote_path = \"/var/www/api/current\"\nartisan = true\nsudo = true\n\n[profiles.app-ird]\nhost = \"192.168.1.100\"\nuser = \"app-ird\"\nremote_path = \"/home/app-ird/uploads\"\nsudo = false\n\n[profiles.app-recharge]\nhost = \"192.168.1.100\"\nuser = \"app-recharge\"\nremote_path = \"/var/www/recharge/current\"\nsudo = false\n```\n\nEach profile gets its own credential:\n\n```bash\nspyro auth set -p app-api -w 'api-password'\nspyro auth set -p app-ird -w 'ird-password'\nspyro auth set -p app-recharge -w 'recharge-password'\n```\n\nUse them independently:\n\n```bash\nspyro artisan migrate:status -p app-api\nspyro artisan tinker -p app-ird\nspyro run \"ls -la\" -p app-recharge\n```\n\n### Multiple servers\n\n```toml\n[profiles.staging]\nhost = \"10.0.0.1\"\nuser = \"deploy\"\nremote_path = \"/var/www/app\"\nartisan = true\nsudo = true\nforwarded_ports = [3306]\n\n[profiles.production]\nhost = \"10.0.0.2\"\nuser = \"deploy\"\nremote_path = \"/var/www/app\"\nartisan = true\nsudo = true\nforwarded_ports = [3306]\n```\n\n```bash\nspyro artisan migrate -p staging     # staging only\nspyro artisan migrate -p production  # production only\nspyro artisan migrate --all          # both at once\n```\n\n### WordPress profile\n\n```toml\n[profiles.wordpress]\nhost = \"wp.example.com\"\nuser = \"deploy\"\nremote_path = \"/var/www/html\"\nwordpress = true\nsudo = false\nforwarded_ports = [33062]\n\n[profiles.wordpress.db]\nhost = \"127.0.0.1\"\nport = 33062\nname = \"wordpress\"\nuser = \"wp_user\"\npassword = \"\"\ndriver = \"mysql\"\n```\n\n### Config file location\n\nSpyro walks up from your current directory looking for `spyro.toml`. Put it in your project root and run commands from anywhere inside the project tree.\n\n```\n~/Projects/my-app/\n├── spyro.toml          ← Spyro finds this\n├── app/\n│   ├── Http/\n│   └── Models/\n├── config/\n└── routes/\n```\n\n```bash\ncd ~/Projects/my-app\nspyro artisan migrate -p staging    # Works\n\ncd ~/Projects/my-app/app/Http\nspyro artisan migrate -p staging    # Still works — walks up to find spyro.toml\n```\n\n## Credentials\n\n### How it works\n\nSpyro stores **one password per profile** in your OS keychain (macOS Keychain / Linux Secret Service). This single password is used for both SSH login and sudo — because in practice, they're the same.\n\nIf no keychain entry exists, Spyro prompts you interactively and stores it for next time.\n\n### Authentication commands\n\n```bash\n# Store credentials (prompts securely)\nspyro auth set -p staging\n\n# Store non-interactively (for scripts/CI)\nspyro auth set -p staging -w 'my-password'\n\n# Overwrite existing without prompting\nspyro auth set -p staging -w 'new-password' -f\n\n# List all stored credentials\nspyro auth list\n\n# Delete a credential\nspyro auth delete -p staging\n```\n\n### What happens when you run a command\n\n```\nspyro caddy restart -p dev\n\n1. Spyro checks keychain for dev credential\n2. Found → uses it for SSH login\n3. Command contains \"sudo\" → checks if profile has sudo=true\n4. sudo=true → uses same credential for sudo prompt\n5. sudo=false → prints clear error: \"User 'deploy' does not have sudo access\"\n6. Command runs, credentials zeroed from memory immediately after\n```\n\n### Security\n\n- Passwords never leave your OS keychain\n- Never stored in `spyro.toml` or any config file\n- Wrapped in `SecureCredential` (bytearray) during use, zeroed with triple-pass after\n- All output sanitized against terminal injection attacks\n- Shell arguments passed through `shlex.quote()` to prevent injection\n\n## Commands Reference\n\n### Tunnel Management\n\n```bash\nspyro up                         # Start all tunnels (daemon mode)\nspyro up staging                 # Start staging tunnel only\nspyro up staging production      # Start multiple tunnels\nspyro down                       # Stop all tunnels\nspyro down staging               # Stop staging tunnel\nspyro status                     # Show all active tunnels + health\nspyro status staging             # Show staging tunnel details\n```\n\n**Tunnel behavior:**\n- Runs as daemon by default (survives terminal close)\n- Self-healing: restarts on crash, network drop, or sleep/wake\n- Exponential backoff: 1s → 2s → 4s → ... → 5min max\n- Process tracking via `~/.spyro/tunnels.json` (orphan cleanup on reboot)\n\n### Laravel Artisan\n\n```bash\nspyro artisan \u003ccommand\u003e -p \u003cprofile\u003e\n\n# Examples:\nspyro artisan migrate -p staging\nspyro artisan migrate:status -p staging\nspyro artisan queue:status -p staging\nspyro artisan config:cache -p staging\nspyro artisan route:cache -p staging\nspyro artisan view:cache -p staging\nspyro artisan optimize:clear -p staging\nspyro artisan about -p staging\nspyro artisan schedule:list -p staging\n\n# Run across all environments\nspyro artisan queue:status --all\n```\n\n**Tinker (interactive REPL):**\n\n```bash\nspyro tinker -p staging                          # Interactive shell\nspyro tinker -p staging -e \"User::count()\"       # One-shot eval\nspyro tinker -p staging -f script.php            # Run a PHP file\n```\n\n### Database\n\n```bash\n# Tunnel + connection\nspyro db tunnel -p staging              # Start tunnel, print connection URL\nspyro db shell -p staging               # Open MySQL/MariaDB/psql prompt\nspyro db ping -p staging                # Test connectivity\n\n# Querying\nspyro db query \"SELECT COUNT(*) FROM users\" -p staging\nspyro db query \"SHOW TABLES\" -p staging\n\n# Dumping\nspyro db dump -p staging                           # Full dump\nspyro db dump -p staging -t users,posts            # Specific tables\nspyro db dump -p staging -t users -w \"id \u003e 100\"   # With WHERE filter\nspyro db dump -p staging -z                        # Gzip compressed\nspyro db dump -p staging -d                        # Schema only (no data)\n\n# Listing\nspyro db list-databases -p staging                 # List all databases\n\n# GUI tools\nspyro proxy-url -p staging              # Generate connection string\nspyro proxy-url -p staging | pbcopy     # Copy to clipboard\n```\n\n**Proxy URL output:**\n```\nmysql://forge:@127.0.0.1:3306/myapp_staging\n```\n\nPaste this into TablePlus, Sequel Ace, DBeaver, or any database GUI.\n\n### Service Management\n\n**Supervisor (queue workers, Reverb, etc.):**\n\n```bash\nspyro supervisor status -p staging                    # All processes\nspyro supervisor restart -p staging                   # Restart all\nspyro supervisor restart laravel-queue -p staging     # Restart specific\nspyro supervisor tail laravel-reverb -p staging       # Tail stderr logs\n```\n\n**Redis:**\n\n```bash\nspyro redis ping -p staging                    # Test connection\nspyro redis stats -p staging                   # Connections, commands/s, keyspace\nspyro redis info -p staging -s server          # Server info section\nspyro redis info -p staging -s memory          # Memory info\nspyro redis cli DBSIZE -p staging              # Run arbitrary redis-cli command\nspyro redis cli KEYS \"*\" -p staging            # List all keys\n```\n\n**PHP:**\n\n```bash\nspyro php version -p staging                   # PHP version\nspyro php extensions -p staging                # List loaded extensions\nspyro php extensions -p staging --filter pdo   # Filter extensions\nspyro php info -p staging --option memory_limit  # PHP config value\nspyro php fpm-status -p staging                # PHP-FPM pool status\nspyro php restart -p staging                   # Restart PHP-FPM\n```\n\n**Web servers:**\n\n```bash\n# Caddy\nspyro caddy version -p staging\nspyro caddy status -p staging\nspyro caddy restart -p staging\n\n# Nginx\nspyro nginx version -p staging\nspyro nginx status -p staging\nspyro nginx sites -p staging\nspyro nginx restart -p staging\n\n# Apache\nspyro apache version -p staging\nspyro apache modules -p staging\nspyro apache sites -p staging\nspyro apache status -p staging\nspyro apache restart -p staging\n```\n\n### Remote Commands\n\n```bash\n# Run any command\nspyro run \"df -h\" -p staging\nspyro run \"cat /var/log/syslog | tail -50\" -p staging\nspyro run \"du -sh /var/www/app/storage\" -p staging\n\n# Run across all environments\nspyro run \"uptime\" --all\nspyro run \"free -m\" --all\n```\n\n### Interactive Shell\n\n```bash\n# Open an interactive SSH session for a profile\nspyro ssh -p staging\nspyro shell -p staging       # Alias for ssh\n\n# Auto-detects profile if only one is configured\nspyro ssh\n\n# Once connected, you're in a full interactive shell:\n#   - Colors, tab completion, vim, htop — all pass through\n#   - Ctrl+C, arrows, history work normally\n#   - Password or key-based auth — both handled automatically\n# Exit with Ctrl+D or type \"exit\"\n```\n\n**How it works:** Uses the same PTY engine as `spyro run` to inject credentials from the OS keychain during auth, then hands over to a raw terminal relay. Handles both password-based and key-based SSH authentication. After auth, credentials are zeroed from memory — the interactive session has no access to them.\n\n### File Transfer\n\n```bash\n# Upload local file to remote\nspyro cp ./README.md :/var/www/app/README.md -p staging\n\n# Download remote file to local\nspyro cp :/var/www/app/.env ./.env.remote -p staging\n\n# Upload entire directory\nspyro cp -r ./public :/var/www/app/public -p staging\n```\n\n**Path convention:**\n- Local paths: `/path/to/file`\n- Remote paths: `:` prefix → `:/remote/path`\n- Profile flag: `-p staging` always required\n\n### Environment\n\n```bash\n# Mirror remote .env to local\nspyro pull-env -p staging\n\n# Creates .env.remote in current directory\n# Useful for comparing environments or debugging config issues\n```\n\n### Logs\n\n```bash\nspyro logs laravel -p staging -n 100       # Last 100 lines\nspyro logs laravel -p staging -f           # Follow (tail -f)\nspyro logs nginx -p staging                # Nginx access log\nspyro logs nginx-error -p staging          # Nginx error log\nspyro logs apache -p staging               # Apache access log\nspyro logs php -p staging                  # PHP-FPM error log\nspyro logs supervisor -p staging           # Spyro's own supervisor log\n```\n\n### Diagnostics\n\n```bash\nspyro doctor                    # Full audit of all profiles\n\n# Output:\n# 1. SSH connectivity\n#    ✓ staging: reachable\n#    ✓ production: reachable\n#\n# 2. Remote path validity\n#    ✓ staging: /var/www/app/current exists\n#    ✓ production: /var/www/app/current exists\n#\n# 3. Local port conflicts\n#    ✓ Port 3306: available\n#    ✓ Port 6379: available\n#\n# 4. Laravel artisan detection\n#    ✓ staging: artisan found\n#    ✓ production: artisan found\n#\n# 6. Remote services\n#    staging (10.0.0.1)\n#      ✓ Redis\n#      ✓ Supervisor (3 managed)\n#      ✓ PHP-FPM (8.3)\n#      ✓ Caddy\n```\n\n## Tips \u0026 Tricks\n\n### Use `-p` everywhere\n\nAlmost every command takes `-p \u003cprofile\u003e`. Make it muscle memory:\n\n```bash\nspyro artisan migrate -p staging\nspyro db shell -p staging\nspyro caddy restart -p staging\nspyro logs laravel -p staging -f\n```\n\n### Quick DB access\n\n```bash\n# One-liner: open MySQL shell\nspyro db shell -p staging\n\n# Or get connection string for GUI tools\nspyro proxy-url -p staging | pbcopy\n# Paste into TablePlus/Sequel Ace/DBeaver\n```\n\n### Check before you deploy\n\n```bash\nspyro doctor                           # Full audit\nspyro supervisor status -p staging     # Queue workers healthy?\nspyro redis ping -p staging            # Redis alive?\nspyro db ping -p staging               # Database reachable?\nspyro artisan about -p staging         # App boots correctly?\n```\n\n### Run across all environments\n\n```bash\nspyro artisan queue:status --all\nspyro run \"uptime\" --all\nspyro run \"df -h\" --all\n```\n\n### Non-interactive auth (CI/CD)\n\n```bash\n# Store password without prompts\nspyro auth set -p staging -w \"$STAGING_PASSWORD\" -f\n\n# Use in GitHub Actions\n- name: Setup credentials\n  run: spyro auth set -p staging -w \"${{ secrets.STAGING_SSH_PASSWORD }}\" -f\n```\n\n### Shell aliases\n\nAdd to `~/.zshrc` or `~/.bashrc`:\n\n```bash\nalias su='spyro up'\nalias sd='spyro down'\nalias ss='spyro status'\nalias sa='spyro artisan'\nalias sdsh='spyro db shell'\nalias sdoc='spyro doctor'\n```\n\n### Debug tunnel issues\n\n```bash\nspyro status                    # What's running?\nspyro logs -p staging           # Supervisor logs\nspyro doctor                    # Full audit\nssh deploy@staging.example.com  # Raw SSH fallback\n```\n\n### Multiple users on one server\n\n```toml\n[profiles.app-api]\nhost = \"192.168.1.100\"\nuser = \"deploy\"\nremote_path = \"/var/www/api/current\"\nsudo = true\n\n[profiles.app-ird]\nhost = \"192.168.1.100\"\nuser = \"app-ird\"\nremote_path = \"/home/app-ird/uploads\"\nsudo = false\n```\n\n```bash\nspyro auth set -p app-api -w 'password1'\nspyro auth set -p app-ird -w 'password2'\n\nspyro artisan migrate -p app-api\nspyro artisan tinker -p app-ird\n```\n\n### When sudo isn't available\n\nIf a profile has `sudo = false` and you run a command that needs sudo:\n\n```\n✗ User 'deploy' does not have sudo access on staging\n  Set sudo = true in your spyro.toml for this profile\n```\n\nClear error, no cryptic failures.\n\n### Config file location\n\nSpyro walks up from your current directory. Put `spyro.toml` in your project root:\n\n```bash\ncd ~/Projects/my-app\nspyro artisan migrate -p staging    # Works from project root\n\ncd ~/Projects/my-app/app/Http\nspyro artisan migrate -p staging    # Still works — walks up\n```\n\n## Architecture\n\n```\nspyro.toml              ← Your config (per-project, declarative)\n    ↓\nCLI (click)             ← Command routing + validation\n    ↓\nPTY Engine              ← SSH with pseudo-terminal (handles sudo prompts)\n    │                     Spawns native ssh via pty.openpty() + os.fork()\n    │                     Matches auth prompts via regex\n    │                     Injects credentials into PTY buffer\n    │                     Zeroes credentials after use\n    ↓\nKeychain (keyring)      ← Passwords in OS keychain (macOS/Linux native)\n    ↓\nTunnel Supervisor       ← Self-healing SSH tunnels\n                          Process liveness + port health checks\n                          Exponential backoff restart\n                          Survives network drops, sleep/wake\n                          PID tracking for orphan cleanup\n```\n\n### PTY Engine\n\nThe PTY engine (`src/core/pty_engine.py`) spawns native `ssh` in a pseudo-terminal using `pty.openpty()` and `os.fork()`. It:\n\n- Reads stdout/stderr byte-by-byte\n- Handles both **password-based** and **key-based** SSH auth automatically\n  - Password auth: matches prompts via regex (`password:`, `[sudo] password`, etc.), injects credentials directly into the PTY buffer\n  - Key auth: detects shell output (MOTD, prompt) and skips directly to raw relay mode\n- Wraps credentials in `SecureCredential` for memory zeroing\n- Sanitizes all output through ANSI filter before printing\n- Handles SSH host key verification prompts automatically\n- Uses `~/.spyro/sockets/` for connection sharing (avoids macOS Unix socket path length limits)\n\n### Tunnel Supervisor (STS)\n\nThe STS (`src/supervisor/tunnel.py`) replaces `autossh` with a Python-native supervisor that:\n\n- Monitors tunnel health via process liveness and port connectivity\n- Restarts failed tunnels with exponential backoff (1s → 2s → 4s → ... → 5min)\n- Handles network roaming and sleep/wake cycles\n- Uses `psutil` for cross-platform process tree management\n- Tracks PIDs/PGIDs in `~/.spyro/tunnels.json` for orphan cleanup\n\n### Service Detection\n\n`spyro doctor` auto-detects these remote services:\n\n| Service | Detection Method |\n|---------|-----------------|\n| Redis | `redis-server` binary, process check, `redis-cli info` |\n| Supervisor | `supervisorctl` binary, `supervisord` process, managed process counts |\n| PHP-FPM | `php-fpm*` binary (version-aware), `php-fpm -tt` pool count |\n| PHP | `php` binary, version, extension count, FPM pool children |\n| Apache | `apache2`/`httpd` binary, version, module count |\n| Nginx | `nginx` binary, version, worker processes, site count |\n| Caddy | `caddy` binary, version, process count |\n| Node.js | `node` binary, version, running processes |\n| npm | `npm` binary, version |\n\n### Smart Sync\n\nThe sync system (`spyro pin` / `spyro sync`) excludes sensitive files by default:\n\n**Always excluded** (all frameworks):\n- `.env*` — all environment files\n- `*.local` — local config overrides\n- `node_modules/`, `vendor/`, `__pycache__/`\n- `*.swp`, `*~`, `.DS_Store`, `*.log`\n\n**Framework-specific** (auto-detected or manual):\n- **Laravel**: `.env`, `storage/logs/`, `bootstrap/cache/`, `vendor/`, `node_modules/`\n- **WordPress**: `.env`, `wp-config.php`, `wp-content/cache/`, `vendor/`\n- **Node.js**: `.env.local`, `.env.*.local`, `node_modules/`, `.next/`, `dist/`\n- **Python**: `.env`, `__pycache__/`, `.venv/`, `*.pyc`\n\n### Security Model\n\n| Concern | Mitigation |\n|---------|------------|\n| Credential exposure | `SecureCredential` wraps passwords in `bytearray`, zeros with triple-pass (zero → random → zero) after use |\n| Terminal injection | `sanitize_output()` strips all ANSI/OSC/DCS/C0 sequences, null bytes, BEL, and BS before printing |\n| Shell injection | All user input passed through `shlex.quote()` |\n| Config file permissions | Enforces `0600` on `spyro.toml` if it contains passwords |\n| Keychain storage | Uses `keyring` library for native OS secure stores (macOS Keychain, Linux Secret Service) |\n| Process isolation | PTY credentials are read into local variables and zeroed immediately after the interaction loop |\n\n## Testing\n\n```bash\n# Unit tests\npython3 -m pytest tests/unit/ -v\n\n# Security tests — ANSI attack vectors (20 vectors)\npython3 tests/security/test_ansi_attacks.py\n\n# Security tests — Memory zeroing (8 tests)\npython3 tests/security/test_memory_zeroing.py\n\n# Phase 1 PoC — PTY engine validation\npython3 tests/poc/test_pty_engine.py\n\n# All tests including integration\npython3 -m pytest tests/ -v\n```\n\n## Platform Support\n\n- **macOS** — native (Keychain integration)\n- **Linux** — native (Secret Service / kwallet)\n- **Windows** — WSL only\n\n## Planned Features\n\n### Deployment (Capistrano-style)\n\nZero-downtime deployment for Laravel, React, Angular, Next.js, and Vue.js projects.\n\n```bash\nspyro deploy -p staging              # Deploy to staging\nspyro deploy -p staging --dry-run    # Preview what would happen\nspyro rollback -p staging            # Rollback to previous release\n```\n\n**How it works:**\n\n1. Sync local code → remote via rsync\n2. Create timestamped release directory\n3. Run build steps (composer install, npm build, etc.)\n4. Swap symlink: `/var/www/app/current` → new release\n5. Health check to verify deployment\n6. Cleanup old releases (keeps last 5)\n7. Auto-rollback on failure\n\n**Configuration:**\n\n```toml\n[profiles.staging]\nbranch = \"main\"\ndeploy_strategy = \"laravel\"    # laravel, node, static, nextjs, vuejs\n\n[profiles.staging.deploy]\nkeep_releases = 5\nshared_dirs = [\"storage\", \"bootstrap/cache\"]\nshared_files = [\".env\"]\nhealth_check = \"/health\"\nhealth_timeout = 30\npost_deploy = [\n    \"php artisan migrate --force\",\n    \"php artisan config:cache\",\n    \"php artisan route:cache\",\n    \"php artisan view:cache\",\n    \"php artisan queue:restart\",\n]\n```\n\n**Project type strategies:**\n\n| Strategy | Build Steps | Use Case |\n|----------|-------------|----------|\n| `laravel` | composer install, artisan migrate, artisan optimize | Laravel PHP apps |\n| `node` | npm ci, npm run build | React, Vue, Angular |\n| `static` | (none) | HTML/CSS/JS sites |\n| `nextjs` | npm ci, npm run build, pm2 restart | Next.js apps |\n| `vuejs` | npm ci, npm run build | Vue.js apps |\n\n**Rollback:**\n\n```bash\nspyro rollback -p staging\n# Swaps symlink to previous release\n# Re-runs post-deploy hooks\n```\n\n**Health check:**\n\nAfter deployment, Spyro verifies the app is healthy by hitting your health endpoint. If it fails, the deployment is rolled back automatically.\n\n```toml\n[profiles.staging.deploy]\nhealth_check = \"/health\"    # HTTP endpoint to check\nhealth_timeout = 30         # Seconds to wait\n```\n\n**Status:** Planned — implementation in progress.\n\n---\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpeterson-umoke%2Fspyro-cli","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpeterson-umoke%2Fspyro-cli","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpeterson-umoke%2Fspyro-cli/lists"}