https://github.com/peterson-umoke/spyro-cli
Intelligent SSH tunneling and remote command CLI for developers
https://github.com/peterson-umoke/spyro-cli
cli devops python remote-management ssh tunneling
Last synced: 12 days ago
JSON representation
Intelligent SSH tunneling and remote command CLI for developers
- Host: GitHub
- URL: https://github.com/peterson-umoke/spyro-cli
- Owner: peterson-umoke
- License: mit
- Created: 2026-06-08T20:19:28.000Z (15 days ago)
- Default Branch: main
- Last Pushed: 2026-06-11T16:24:34.000Z (12 days ago)
- Last Synced: 2026-06-11T18:13:23.489Z (12 days ago)
- Topics: cli, devops, python, remote-management, ssh, tunneling
- Language: Python
- Size: 185 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
- Security: SECURITY.md
Awesome Lists containing this project
README
# Spyro
Intelligent SSH tunneling and remote command CLI for developers.
Spyro 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.
## Why Spyro
Developers 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:
- **Automating tunnels** with a self-healing supervisor that survives network drops, sleep/wake cycles, and process crashes
- **Resolving credentials** from either local TOML config or remote `.env`/Rails config files
- **Running remote commands** through a PTY engine that handles sudo escalation without leaking passwords
- **Syncing files** in real-time using native OS filesystem watchers
### The problem
Every developer working with remote servers knows this workflow:
```bash
# Terminal 1: SSH tunnel for MySQL
ssh -L 3306:localhost:3306 deploy@staging.example.com -N &
# Terminal 2: SSH tunnel for Redis
ssh -L 6379:localhost:6379 deploy@staging.example.com -N &
# Terminal 3: Run migrations
ssh deploy@staging.example.com "cd /var/www/app && php artisan migrate"
# Terminal 4: Check queue workers
ssh deploy@staging.example.com "sudo supervisorctl status"
# Terminal 5: Tail logs
ssh deploy@staging.example.com "tail -f /var/www/app/storage/logs/laravel.log"
# Terminal 6: Push a file
scp .env deploy@staging.example.com:/var/www/app/.env
# Oh no, the tunnel died. Restart it.
# Wait, which port was that tunnel on?
# Did I use -N or -f?
# What's the DB password again?
```
Five terminal tabs. Three forgotten port numbers. One crashed tunnel. And you haven't written a line of code yet.
### The solution
```bash
# One command to start all tunnels
spyro up staging
# Run anything on the remote server
spyro artisan migrate -p staging
spyro supervisor status -p staging
spyro logs laravel -p staging -f
# Push files
spyro cp .env :/var/www/app/.env -p staging
# One command to stop everything
spyro down
```
One config file. One CLI. Zero terminal tabs. Tunnels that heal themselves. Passwords stored in your OS keychain, never in config files.
## Installation
### macOS / Linux (recommended)
```bash
# Install with uv (fast Python package manager)
uv tool install git+https://github.com/peterson-umoke/spyro-cli.git
# Verify installation
spyro --version
spyro --help
```
### From source (for contributors)
```bash
git clone https://github.com/peterson-umoke/spyro-cli.git
cd spyro-cli
# Create virtual environment and install
uv sync
# Run without installing globally
uv run spyro --version
# Or install globally for daily use
uv tool install .
# With filesystem watcher support (for spyro sync/watch)
uv tool install . --with watchdog
```
### Update
```bash
uv tool install --force git+https://github.com/peterson-umoke/spyro-cli.git
```
### Verify installation
```bash
spyro --version
spyro doctor
```
`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.).
## Getting Started
### Step 1: Create your config
```bash
cd ~/Projects/my-laravel-app
spyro init
```
This creates `spyro.toml` in your project root. Edit it with your server details:
```toml
[profiles.staging]
host = "staging.example.com"
user = "deploy"
port = 22
remote_path = "/var/www/app"
artisan = true
sudo = true
forwarded_ports = [3306, 6379]
[profiles.staging.db]
host = "127.0.0.1"
port = 3306
name = "myapp_staging"
user = "forge"
password = ""
driver = "mysql"
```
### Step 2: Store your password
```bash
spyro auth set -p staging
# Enter password when prompted — stored in your OS keychain
```
You'll never need to enter it again. Spyro reads it from macOS Keychain (or Linux Secret Service) automatically.
### Step 3: Start working
```bash
# Start database + Redis tunnels
spyro up staging
# Run migrations
spyro artisan migrate -p staging
# Open a MySQL shell
spyro db shell -p staging
# Check queue workers
spyro supervisor status -p staging
# When done, stop all tunnels
spyro down
```
That's the entire workflow. No more terminal tabs, no more port forwarding scripts, no more "what's the DB password?"
## Configuration
### Profile basics
Every server connection is a "profile" in `spyro.toml`:
```toml
[profiles.staging]
host = "staging.example.com" # Server IP or hostname (required)
user = "deploy" # SSH username (required)
port = 22 # SSH port (default: 22)
key = "~/.ssh/id_ed25519" # SSH key (optional, uses default if empty)
remote_path = "/var/www/app" # Working directory on server (required)
artisan = true # Enable Laravel artisan commands
wordpress = false # Enable WordPress/WP-CLI commands
sudo = true # Allow sudo when needed
forwarded_ports = [3306, 6379] # Ports to tunnel locally
env_files = [".env"] # Remote env files to scan for DB credentials
```
### Configuration fields
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `host` | string | *required* | Server hostname or IP address |
| `user` | string | `deploy` | SSH username |
| `port` | int | `22` | SSH port |
| `key` | string | `""` | Path to SSH private key (uses system default if empty) |
| `remote_path` | string | `/var/www` | Working directory on remote server |
| `forwarded_ports` | list[int] | `[]` | Remote ports to tunnel to localhost |
| `artisan` | bool | `false` | Enable `spyro artisan` commands |
| `wordpress` | bool | `false` | Enable `spyro wp` commands |
| `sudo` | bool | `false` | Enable sudo escalation for commands |
| `env_files` | list[str] | `[".env"]` | Remote env files to scan for DB credentials |
### Database configuration
```toml
[profiles.staging.db]
host = "127.0.0.1" # Always localhost (traffic goes through tunnel)
port = 3306 # Must match a port in forwarded_ports
name = "myapp_staging"
user = "forge"
password = "" # Leave empty = auto-detect from remote .env
driver = "mysql" # mysql, postgres, or sqlite
```
**Credential resolution** (dual-strategy):
1. **Explicit** — If `password` is set in `spyro.toml`, use it
2. **Auto-detect** — If empty, scan remote `.env` / config files for `DB_*` variables
### Multiple users, same server
Different services running as different system users? Create a profile per user:
```toml
[profiles.app-api]
host = "192.168.1.100"
user = "deploy"
remote_path = "/var/www/api/current"
artisan = true
sudo = true
[profiles.app-ird]
host = "192.168.1.100"
user = "app-ird"
remote_path = "/home/app-ird/uploads"
sudo = false
[profiles.app-recharge]
host = "192.168.1.100"
user = "app-recharge"
remote_path = "/var/www/recharge/current"
sudo = false
```
Each profile gets its own credential:
```bash
spyro auth set -p app-api -w 'api-password'
spyro auth set -p app-ird -w 'ird-password'
spyro auth set -p app-recharge -w 'recharge-password'
```
Use them independently:
```bash
spyro artisan migrate:status -p app-api
spyro artisan tinker -p app-ird
spyro run "ls -la" -p app-recharge
```
### Multiple servers
```toml
[profiles.staging]
host = "10.0.0.1"
user = "deploy"
remote_path = "/var/www/app"
artisan = true
sudo = true
forwarded_ports = [3306]
[profiles.production]
host = "10.0.0.2"
user = "deploy"
remote_path = "/var/www/app"
artisan = true
sudo = true
forwarded_ports = [3306]
```
```bash
spyro artisan migrate -p staging # staging only
spyro artisan migrate -p production # production only
spyro artisan migrate --all # both at once
```
### WordPress profile
```toml
[profiles.wordpress]
host = "wp.example.com"
user = "deploy"
remote_path = "/var/www/html"
wordpress = true
sudo = false
forwarded_ports = [33062]
[profiles.wordpress.db]
host = "127.0.0.1"
port = 33062
name = "wordpress"
user = "wp_user"
password = ""
driver = "mysql"
```
### Config file location
Spyro 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.
```
~/Projects/my-app/
├── spyro.toml ← Spyro finds this
├── app/
│ ├── Http/
│ └── Models/
├── config/
└── routes/
```
```bash
cd ~/Projects/my-app
spyro artisan migrate -p staging # Works
cd ~/Projects/my-app/app/Http
spyro artisan migrate -p staging # Still works — walks up to find spyro.toml
```
## Credentials
### How it works
Spyro 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.
If no keychain entry exists, Spyro prompts you interactively and stores it for next time.
### Authentication commands
```bash
# Store credentials (prompts securely)
spyro auth set -p staging
# Store non-interactively (for scripts/CI)
spyro auth set -p staging -w 'my-password'
# Overwrite existing without prompting
spyro auth set -p staging -w 'new-password' -f
# List all stored credentials
spyro auth list
# Delete a credential
spyro auth delete -p staging
```
### What happens when you run a command
```
spyro caddy restart -p dev
1. Spyro checks keychain for dev credential
2. Found → uses it for SSH login
3. Command contains "sudo" → checks if profile has sudo=true
4. sudo=true → uses same credential for sudo prompt
5. sudo=false → prints clear error: "User 'deploy' does not have sudo access"
6. Command runs, credentials zeroed from memory immediately after
```
### Security
- Passwords never leave your OS keychain
- Never stored in `spyro.toml` or any config file
- Wrapped in `SecureCredential` (bytearray) during use, zeroed with triple-pass after
- All output sanitized against terminal injection attacks
- Shell arguments passed through `shlex.quote()` to prevent injection
## Commands Reference
### Tunnel Management
```bash
spyro up # Start all tunnels (daemon mode)
spyro up staging # Start staging tunnel only
spyro up staging production # Start multiple tunnels
spyro down # Stop all tunnels
spyro down staging # Stop staging tunnel
spyro status # Show all active tunnels + health
spyro status staging # Show staging tunnel details
```
**Tunnel behavior:**
- Runs as daemon by default (survives terminal close)
- Self-healing: restarts on crash, network drop, or sleep/wake
- Exponential backoff: 1s → 2s → 4s → ... → 5min max
- Process tracking via `~/.spyro/tunnels.json` (orphan cleanup on reboot)
### Laravel Artisan
```bash
spyro artisan -p
# Examples:
spyro artisan migrate -p staging
spyro artisan migrate:status -p staging
spyro artisan queue:status -p staging
spyro artisan config:cache -p staging
spyro artisan route:cache -p staging
spyro artisan view:cache -p staging
spyro artisan optimize:clear -p staging
spyro artisan about -p staging
spyro artisan schedule:list -p staging
# Run across all environments
spyro artisan queue:status --all
```
**Tinker (interactive REPL):**
```bash
spyro tinker -p staging # Interactive shell
spyro tinker -p staging -e "User::count()" # One-shot eval
spyro tinker -p staging -f script.php # Run a PHP file
```
### Database
```bash
# Tunnel + connection
spyro db tunnel -p staging # Start tunnel, print connection URL
spyro db shell -p staging # Open MySQL/MariaDB/psql prompt
spyro db ping -p staging # Test connectivity
# Querying
spyro db query "SELECT COUNT(*) FROM users" -p staging
spyro db query "SHOW TABLES" -p staging
# Dumping
spyro db dump -p staging # Full dump
spyro db dump -p staging -t users,posts # Specific tables
spyro db dump -p staging -t users -w "id > 100" # With WHERE filter
spyro db dump -p staging -z # Gzip compressed
spyro db dump -p staging -d # Schema only (no data)
# Listing
spyro db list-databases -p staging # List all databases
# GUI tools
spyro proxy-url -p staging # Generate connection string
spyro proxy-url -p staging | pbcopy # Copy to clipboard
```
**Proxy URL output:**
```
mysql://forge:@127.0.0.1:3306/myapp_staging
```
Paste this into TablePlus, Sequel Ace, DBeaver, or any database GUI.
### Service Management
**Supervisor (queue workers, Reverb, etc.):**
```bash
spyro supervisor status -p staging # All processes
spyro supervisor restart -p staging # Restart all
spyro supervisor restart laravel-queue -p staging # Restart specific
spyro supervisor tail laravel-reverb -p staging # Tail stderr logs
```
**Redis:**
```bash
spyro redis ping -p staging # Test connection
spyro redis stats -p staging # Connections, commands/s, keyspace
spyro redis info -p staging -s server # Server info section
spyro redis info -p staging -s memory # Memory info
spyro redis cli DBSIZE -p staging # Run arbitrary redis-cli command
spyro redis cli KEYS "*" -p staging # List all keys
```
**PHP:**
```bash
spyro php version -p staging # PHP version
spyro php extensions -p staging # List loaded extensions
spyro php extensions -p staging --filter pdo # Filter extensions
spyro php info -p staging --option memory_limit # PHP config value
spyro php fpm-status -p staging # PHP-FPM pool status
spyro php restart -p staging # Restart PHP-FPM
```
**Web servers:**
```bash
# Caddy
spyro caddy version -p staging
spyro caddy status -p staging
spyro caddy restart -p staging
# Nginx
spyro nginx version -p staging
spyro nginx status -p staging
spyro nginx sites -p staging
spyro nginx restart -p staging
# Apache
spyro apache version -p staging
spyro apache modules -p staging
spyro apache sites -p staging
spyro apache status -p staging
spyro apache restart -p staging
```
### Remote Commands
```bash
# Run any command
spyro run "df -h" -p staging
spyro run "cat /var/log/syslog | tail -50" -p staging
spyro run "du -sh /var/www/app/storage" -p staging
# Run across all environments
spyro run "uptime" --all
spyro run "free -m" --all
```
### Interactive Shell
```bash
# Open an interactive SSH session for a profile
spyro ssh -p staging
spyro shell -p staging # Alias for ssh
# Auto-detects profile if only one is configured
spyro ssh
# Once connected, you're in a full interactive shell:
# - Colors, tab completion, vim, htop — all pass through
# - Ctrl+C, arrows, history work normally
# - Password or key-based auth — both handled automatically
# Exit with Ctrl+D or type "exit"
```
**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.
### File Transfer
```bash
# Upload local file to remote
spyro cp ./README.md :/var/www/app/README.md -p staging
# Download remote file to local
spyro cp :/var/www/app/.env ./.env.remote -p staging
# Upload entire directory
spyro cp -r ./public :/var/www/app/public -p staging
```
**Path convention:**
- Local paths: `/path/to/file`
- Remote paths: `:` prefix → `:/remote/path`
- Profile flag: `-p staging` always required
### Environment
```bash
# Mirror remote .env to local
spyro pull-env -p staging
# Creates .env.remote in current directory
# Useful for comparing environments or debugging config issues
```
### Logs
```bash
spyro logs laravel -p staging -n 100 # Last 100 lines
spyro logs laravel -p staging -f # Follow (tail -f)
spyro logs nginx -p staging # Nginx access log
spyro logs nginx-error -p staging # Nginx error log
spyro logs apache -p staging # Apache access log
spyro logs php -p staging # PHP-FPM error log
spyro logs supervisor -p staging # Spyro's own supervisor log
```
### Diagnostics
```bash
spyro doctor # Full audit of all profiles
# Output:
# 1. SSH connectivity
# ✓ staging: reachable
# ✓ production: reachable
#
# 2. Remote path validity
# ✓ staging: /var/www/app/current exists
# ✓ production: /var/www/app/current exists
#
# 3. Local port conflicts
# ✓ Port 3306: available
# ✓ Port 6379: available
#
# 4. Laravel artisan detection
# ✓ staging: artisan found
# ✓ production: artisan found
#
# 6. Remote services
# staging (10.0.0.1)
# ✓ Redis
# ✓ Supervisor (3 managed)
# ✓ PHP-FPM (8.3)
# ✓ Caddy
```
## Tips & Tricks
### Use `-p` everywhere
Almost every command takes `-p `. Make it muscle memory:
```bash
spyro artisan migrate -p staging
spyro db shell -p staging
spyro caddy restart -p staging
spyro logs laravel -p staging -f
```
### Quick DB access
```bash
# One-liner: open MySQL shell
spyro db shell -p staging
# Or get connection string for GUI tools
spyro proxy-url -p staging | pbcopy
# Paste into TablePlus/Sequel Ace/DBeaver
```
### Check before you deploy
```bash
spyro doctor # Full audit
spyro supervisor status -p staging # Queue workers healthy?
spyro redis ping -p staging # Redis alive?
spyro db ping -p staging # Database reachable?
spyro artisan about -p staging # App boots correctly?
```
### Run across all environments
```bash
spyro artisan queue:status --all
spyro run "uptime" --all
spyro run "df -h" --all
```
### Non-interactive auth (CI/CD)
```bash
# Store password without prompts
spyro auth set -p staging -w "$STAGING_PASSWORD" -f
# Use in GitHub Actions
- name: Setup credentials
run: spyro auth set -p staging -w "${{ secrets.STAGING_SSH_PASSWORD }}" -f
```
### Shell aliases
Add to `~/.zshrc` or `~/.bashrc`:
```bash
alias su='spyro up'
alias sd='spyro down'
alias ss='spyro status'
alias sa='spyro artisan'
alias sdsh='spyro db shell'
alias sdoc='spyro doctor'
```
### Debug tunnel issues
```bash
spyro status # What's running?
spyro logs -p staging # Supervisor logs
spyro doctor # Full audit
ssh deploy@staging.example.com # Raw SSH fallback
```
### Multiple users on one server
```toml
[profiles.app-api]
host = "192.168.1.100"
user = "deploy"
remote_path = "/var/www/api/current"
sudo = true
[profiles.app-ird]
host = "192.168.1.100"
user = "app-ird"
remote_path = "/home/app-ird/uploads"
sudo = false
```
```bash
spyro auth set -p app-api -w 'password1'
spyro auth set -p app-ird -w 'password2'
spyro artisan migrate -p app-api
spyro artisan tinker -p app-ird
```
### When sudo isn't available
If a profile has `sudo = false` and you run a command that needs sudo:
```
✗ User 'deploy' does not have sudo access on staging
Set sudo = true in your spyro.toml for this profile
```
Clear error, no cryptic failures.
### Config file location
Spyro walks up from your current directory. Put `spyro.toml` in your project root:
```bash
cd ~/Projects/my-app
spyro artisan migrate -p staging # Works from project root
cd ~/Projects/my-app/app/Http
spyro artisan migrate -p staging # Still works — walks up
```
## Architecture
```
spyro.toml ← Your config (per-project, declarative)
↓
CLI (click) ← Command routing + validation
↓
PTY Engine ← SSH with pseudo-terminal (handles sudo prompts)
│ Spawns native ssh via pty.openpty() + os.fork()
│ Matches auth prompts via regex
│ Injects credentials into PTY buffer
│ Zeroes credentials after use
↓
Keychain (keyring) ← Passwords in OS keychain (macOS/Linux native)
↓
Tunnel Supervisor ← Self-healing SSH tunnels
Process liveness + port health checks
Exponential backoff restart
Survives network drops, sleep/wake
PID tracking for orphan cleanup
```
### PTY Engine
The PTY engine (`src/core/pty_engine.py`) spawns native `ssh` in a pseudo-terminal using `pty.openpty()` and `os.fork()`. It:
- Reads stdout/stderr byte-by-byte
- Handles both **password-based** and **key-based** SSH auth automatically
- Password auth: matches prompts via regex (`password:`, `[sudo] password`, etc.), injects credentials directly into the PTY buffer
- Key auth: detects shell output (MOTD, prompt) and skips directly to raw relay mode
- Wraps credentials in `SecureCredential` for memory zeroing
- Sanitizes all output through ANSI filter before printing
- Handles SSH host key verification prompts automatically
- Uses `~/.spyro/sockets/` for connection sharing (avoids macOS Unix socket path length limits)
### Tunnel Supervisor (STS)
The STS (`src/supervisor/tunnel.py`) replaces `autossh` with a Python-native supervisor that:
- Monitors tunnel health via process liveness and port connectivity
- Restarts failed tunnels with exponential backoff (1s → 2s → 4s → ... → 5min)
- Handles network roaming and sleep/wake cycles
- Uses `psutil` for cross-platform process tree management
- Tracks PIDs/PGIDs in `~/.spyro/tunnels.json` for orphan cleanup
### Service Detection
`spyro doctor` auto-detects these remote services:
| Service | Detection Method |
|---------|-----------------|
| Redis | `redis-server` binary, process check, `redis-cli info` |
| Supervisor | `supervisorctl` binary, `supervisord` process, managed process counts |
| PHP-FPM | `php-fpm*` binary (version-aware), `php-fpm -tt` pool count |
| PHP | `php` binary, version, extension count, FPM pool children |
| Apache | `apache2`/`httpd` binary, version, module count |
| Nginx | `nginx` binary, version, worker processes, site count |
| Caddy | `caddy` binary, version, process count |
| Node.js | `node` binary, version, running processes |
| npm | `npm` binary, version |
### Smart Sync
The sync system (`spyro pin` / `spyro sync`) excludes sensitive files by default:
**Always excluded** (all frameworks):
- `.env*` — all environment files
- `*.local` — local config overrides
- `node_modules/`, `vendor/`, `__pycache__/`
- `*.swp`, `*~`, `.DS_Store`, `*.log`
**Framework-specific** (auto-detected or manual):
- **Laravel**: `.env`, `storage/logs/`, `bootstrap/cache/`, `vendor/`, `node_modules/`
- **WordPress**: `.env`, `wp-config.php`, `wp-content/cache/`, `vendor/`
- **Node.js**: `.env.local`, `.env.*.local`, `node_modules/`, `.next/`, `dist/`
- **Python**: `.env`, `__pycache__/`, `.venv/`, `*.pyc`
### Security Model
| Concern | Mitigation |
|---------|------------|
| Credential exposure | `SecureCredential` wraps passwords in `bytearray`, zeros with triple-pass (zero → random → zero) after use |
| Terminal injection | `sanitize_output()` strips all ANSI/OSC/DCS/C0 sequences, null bytes, BEL, and BS before printing |
| Shell injection | All user input passed through `shlex.quote()` |
| Config file permissions | Enforces `0600` on `spyro.toml` if it contains passwords |
| Keychain storage | Uses `keyring` library for native OS secure stores (macOS Keychain, Linux Secret Service) |
| Process isolation | PTY credentials are read into local variables and zeroed immediately after the interaction loop |
## Testing
```bash
# Unit tests
python3 -m pytest tests/unit/ -v
# Security tests — ANSI attack vectors (20 vectors)
python3 tests/security/test_ansi_attacks.py
# Security tests — Memory zeroing (8 tests)
python3 tests/security/test_memory_zeroing.py
# Phase 1 PoC — PTY engine validation
python3 tests/poc/test_pty_engine.py
# All tests including integration
python3 -m pytest tests/ -v
```
## Platform Support
- **macOS** — native (Keychain integration)
- **Linux** — native (Secret Service / kwallet)
- **Windows** — WSL only
## Planned Features
### Deployment (Capistrano-style)
Zero-downtime deployment for Laravel, React, Angular, Next.js, and Vue.js projects.
```bash
spyro deploy -p staging # Deploy to staging
spyro deploy -p staging --dry-run # Preview what would happen
spyro rollback -p staging # Rollback to previous release
```
**How it works:**
1. Sync local code → remote via rsync
2. Create timestamped release directory
3. Run build steps (composer install, npm build, etc.)
4. Swap symlink: `/var/www/app/current` → new release
5. Health check to verify deployment
6. Cleanup old releases (keeps last 5)
7. Auto-rollback on failure
**Configuration:**
```toml
[profiles.staging]
branch = "main"
deploy_strategy = "laravel" # laravel, node, static, nextjs, vuejs
[profiles.staging.deploy]
keep_releases = 5
shared_dirs = ["storage", "bootstrap/cache"]
shared_files = [".env"]
health_check = "/health"
health_timeout = 30
post_deploy = [
"php artisan migrate --force",
"php artisan config:cache",
"php artisan route:cache",
"php artisan view:cache",
"php artisan queue:restart",
]
```
**Project type strategies:**
| Strategy | Build Steps | Use Case |
|----------|-------------|----------|
| `laravel` | composer install, artisan migrate, artisan optimize | Laravel PHP apps |
| `node` | npm ci, npm run build | React, Vue, Angular |
| `static` | (none) | HTML/CSS/JS sites |
| `nextjs` | npm ci, npm run build, pm2 restart | Next.js apps |
| `vuejs` | npm ci, npm run build | Vue.js apps |
**Rollback:**
```bash
spyro rollback -p staging
# Swaps symlink to previous release
# Re-runs post-deploy hooks
```
**Health check:**
After deployment, Spyro verifies the app is healthy by hitting your health endpoint. If it fails, the deployment is rolled back automatically.
```toml
[profiles.staging.deploy]
health_check = "/health" # HTTP endpoint to check
health_timeout = 30 # Seconds to wait
```
**Status:** Planned — implementation in progress.
---
## License
MIT