From 805541df0f22f1200692ddfae7b2f199a89a8f7b Mon Sep 17 00:00:00 2001 From: Clawd Date: Tue, 17 Feb 2026 08:13:28 -0800 Subject: Remove remaining planning docs (SHIP_V2.md, SPEC.md) --- SPEC.md | 499 ---------------------------------------------------------------- 1 file changed, 499 deletions(-) delete mode 100644 SPEC.md (limited to 'SPEC.md') diff --git a/SPEC.md b/SPEC.md deleted file mode 100644 index 50b54c2..0000000 --- a/SPEC.md +++ /dev/null @@ -1,499 +0,0 @@ -# Ship v2 Technical Specification - -## Overview - -Ship deploys code to a VPS. Input: path to code. Output: JSON with URL. - -```bash -ship ./myproject -# {"status":"ok","name":"ship-a1b2c3","url":"https://ship-a1b2c3.example.com","type":"docker","took_ms":8200} -``` - -## CLI Interface - -### Primary Command - -``` -ship [PATH] [FLAGS] -``` - -**PATH**: File or directory to deploy. Defaults to `.` (current directory). - -**FLAGS**: -| Flag | Type | Default | Description | -|------|------|---------|-------------| -| `--name` | string | auto-generated | Deploy name (becomes subdomain) | -| `--host` | string | default host | VPS host (SSH config name or user@host) | -| `--health` | string | `/` for static, none for apps | Health check endpoint | -| `--ttl` | duration | none | Auto-delete after duration (e.g., `1h`, `7d`) | -| `--env` | string[] | none | Environment variables (`KEY=VALUE`) | -| `--env-file` | string | none | Path to .env file | -| `--pretty` | bool | false | Human-readable output | - -### Other Commands - -``` -ship list [--host HOST] -ship status NAME [--host HOST] -ship logs NAME [--lines N] [--host HOST] -ship remove NAME [--host HOST] -ship host init USER@HOST --domain DOMAIN -ship host status [--host HOST] -``` - -All commands output JSON unless `--pretty` is set. - -## Output Schema - -### Success Response - -```typescript -{ - status: "ok", - name: string, - url: string, - type: "static" | "docker" | "binary", - took_ms: number, - health?: { - endpoint: string, - status: number, - latency_ms: number - }, - expires?: string // ISO 8601, only if --ttl set -} -``` - -### Error Response - -```typescript -{ - status: "error", - code: string, - message: string, - name?: string, - url?: string -} -``` - -### Exit Codes - -| Code | Meaning | -|------|---------| -| 0 | Success | -| 1 | Deploy failed | -| 2 | Invalid arguments | -| 3 | SSH connection failed | -| 4 | Health check failed | - -## Error Codes - -| Code | Description | -|------|-------------| -| `INVALID_PATH` | Path doesn't exist or isn't readable | -| `UNKNOWN_PROJECT_TYPE` | Can't detect how to deploy | -| `SSH_CONNECT_FAILED` | Can't establish SSH connection | -| `SSH_AUTH_FAILED` | SSH key rejected | -| `UPLOAD_FAILED` | SCP/rsync failed | -| `BUILD_FAILED` | Docker build or compilation failed | -| `SERVICE_FAILED` | systemd unit failed to start | -| `CADDY_FAILED` | Caddy reload failed | -| `HEALTH_CHECK_FAILED` | App didn't respond in time | -| `HEALTH_CHECK_TIMEOUT` | Health check timed out | -| `NOT_FOUND` | Named deploy doesn't exist | -| `CONFLICT` | Name already in use (if --no-update) | -| `HOST_NOT_CONFIGURED` | No default host, --host required | -| `INVALID_TTL` | Can't parse TTL duration | - -## Auto-Detection Logic - -When given a path, ship determines deploy type: - -``` -is_file(path)? - → is_executable(path)? - → BINARY - → ERROR: "Not an executable file" - -is_directory(path)? - → has_file("Dockerfile")? - → DOCKER - → has_file("index.html") OR has_file("index.htm")? - → STATIC - → has_file("go.mod") AND NOT has_file("Dockerfile")? - → ERROR: "Go project without Dockerfile. Add a Dockerfile or build a binary." - → has_file("package.json") AND NOT has_file("Dockerfile")? - → ERROR: "Node project without Dockerfile. Add a Dockerfile." - → is_empty(path)? - → ERROR: "Directory is empty" - → ERROR: "Can't detect project type. Add a Dockerfile or index.html." -``` - -## Name Generation - -If `--name` not provided: - -``` -name = "ship-" + random_alphanumeric(6) -``` - -Names must match: `^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$` - -Invalid names return `INVALID_NAME` error. - -## Port Allocation - -Ports are allocated server-side. - -### Server Files - -``` -/etc/ship/ports/ # Contains allocated port number -``` - -### Allocation Algorithm - -```python -def allocate_port(name): - port_file = f"/etc/ship/ports/{name}" - - if exists(port_file): - return int(read(port_file)) - - used_ports = [int(read(f)) for f in glob("/etc/ship/ports/*")] - next_port = 9000 - while next_port in used_ports: - next_port += 1 - - write(port_file, str(next_port)) - return next_port -``` - -### Port Range - -- Start: 9000 -- Max: 9999 -- Error if exhausted: `PORT_EXHAUSTED` - -## Server File Layout - -``` -/etc/ship/ - ports/ # Port allocation (contains port number) - env/.env # Environment variables - ttl/ # TTL expiry timestamp (Unix epoch) - -/var/www// # Static site files - -/var/lib// - src/ # Docker build context (checked out source) - data/ # Persistent data volume - -/etc/systemd/system/.service # systemd unit -/etc/caddy/sites-enabled/.caddy # Caddy config -``` - -## Deploy Flows - -### Static Site - -1. Validate path is directory with index.html -2. Generate name if needed -3. SSH: Create `/var/www//` -4. rsync: Upload directory contents to `/var/www//` -5. SSH: Set ownership to `www-data` -6. Generate Caddyfile -7. SSH: Write to `/etc/caddy/sites-enabled/.caddy` -8. SSH: Reload Caddy -9. Health check: `GET https://./` -10. If TTL: Write expiry to `/etc/ship/ttl/` -11. Return JSON - -### Docker - -1. Validate path is directory with Dockerfile -2. Generate name if needed -3. Allocate port (server-side) -4. rsync: Upload directory to `/var/lib//src/` -5. SSH: `docker build -t /var/lib//src/` -6. Generate systemd unit (runs `docker run`) -7. SSH: Write to `/etc/systemd/system/.service` -8. SSH: Write env file to `/etc/ship/env/.env` -9. SSH: `systemctl daemon-reload && systemctl restart ` -10. Generate Caddyfile (reverse proxy to port) -11. SSH: Write to `/etc/caddy/sites-enabled/.caddy` -12. SSH: Reload Caddy -13. Health check: `GET https://.` -14. If TTL: Write expiry to `/etc/ship/ttl/` -15. Return JSON - -### Binary - -1. Validate path is executable file -2. Generate name if needed -3. Allocate port (server-side) -4. SCP: Upload binary to `/usr/local/bin/` -5. SSH: `chmod +x /usr/local/bin/` -6. Generate systemd unit -7. SSH: Write to `/etc/systemd/system/.service` -8. SSH: Write env file to `/etc/ship/env/.env` -9. SSH: `systemctl daemon-reload && systemctl restart ` -10. Generate Caddyfile (reverse proxy to port) -11. SSH: Write to `/etc/caddy/sites-enabled/.caddy` -12. SSH: Reload Caddy -13. Health check: `GET https://.` -14. If TTL: Write expiry to `/etc/ship/ttl/` -15. Return JSON - -## Health Checks - -### Behavior - -After deploy, verify the app is responding: - -1. Wait 2 seconds (let app start) -2. `GET https://.` -3. If 2xx or 3xx: success -4. If error or timeout: retry after 2s -5. Max retries: 15 (total 30s) -6. If all retries fail: return `HEALTH_CHECK_FAILED` - -### Health Endpoint - -| Deploy Type | Default | Override | -|-------------|---------|----------| -| static | `/` | `--health` | -| docker | none (skip) | `--health` | -| binary | none (skip) | `--health` | - -When no health endpoint: skip health check, return success after service starts. - -## TTL (Auto-Expiry) - -### Setting TTL - -```bash -ship ./site --name preview --ttl 24h -``` - -Supported formats: `30m`, `1h`, `24h`, `7d` - -### Server-Side Cleanup - -A systemd timer runs hourly: - -```ini -# /etc/systemd/system/ship-cleanup.timer -[Timer] -OnCalendar=hourly -Persistent=true - -# /etc/systemd/system/ship-cleanup.service -[Service] -Type=oneshot -ExecStart=/usr/local/bin/ship-cleanup -``` - -Cleanup script: - -```bash -#!/bin/bash -now=$(date +%s) -for f in /etc/ship/ttl/*; do - name=$(basename "$f") - expires=$(cat "$f") - if [ "$now" -gt "$expires" ]; then - ship remove "$name" - fi -done -``` - -### TTL File Format - -``` -/etc/ship/ttl/ -``` - -Contents: Unix timestamp (seconds since epoch) - -## Templates - -### systemd Unit (Docker) - -```ini -[Unit] -Description=ship: {{.Name}} -After=docker.service -Requires=docker.service - -[Service] -Restart=always -RestartSec=5 -EnvironmentFile=/etc/ship/env/{{.Name}}.env -ExecStartPre=-/usr/bin/docker stop {{.Name}} -ExecStartPre=-/usr/bin/docker rm {{.Name}} -ExecStart=/usr/bin/docker run --rm --name {{.Name}} \ - --env-file /etc/ship/env/{{.Name}}.env \ - -p 127.0.0.1:{{.Port}}:{{.Port}} \ - -v /var/lib/{{.Name}}/data:/data \ - {{.Name}} -ExecStop=/usr/bin/docker stop {{.Name}} - -[Install] -WantedBy=multi-user.target -``` - -### systemd Unit (Binary) - -```ini -[Unit] -Description=ship: {{.Name}} -After=network.target - -[Service] -Type=simple -Restart=always -RestartSec=5 -User={{.Name}} -EnvironmentFile=/etc/ship/env/{{.Name}}.env -ExecStart=/usr/local/bin/{{.Name}} -WorkingDirectory=/var/lib/{{.Name}} - -[Install] -WantedBy=multi-user.target -``` - -### Caddyfile (Reverse Proxy) - -``` -{{.Domain}} { - reverse_proxy localhost:{{.Port}} -} -``` - -### Caddyfile (Static) - -``` -{{.Domain}} { - root * /var/www/{{.Name}} - file_server - encode gzip -} -``` - -## Environment Variables - -Always set by ship: - -| Variable | Value | -|----------|-------| -| `PORT` | Allocated port number | -| `SHIP_NAME` | Deploy name | -| `SHIP_URL` | Full URL | - -User variables from `--env` or `--env-file` are merged. - -## Host Initialization - -```bash -ship host init user@vps.example.com --domain example.com -``` - -### Steps - -1. SSH: Check connectivity -2. SSH: Install packages (`apt install -y caddy docker.io`) -3. SSH: Enable services (`systemctl enable --now caddy docker`) -4. SSH: Create directories (`/etc/ship/ports`, `/etc/ship/env`, `/etc/ship/ttl`) -5. SSH: Install cleanup timer -6. SSH: Configure Caddy base -7. Update local state with host info - -### Output - -```json -{ - "status": "ok", - "host": "vps.example.com", - "domain": "example.com", - "installed": ["caddy", "docker"] -} -``` - -## Local State - -Stored at `~/.config/ship/state.json`: - -```json -{ - "default_host": "vps", - "hosts": { - "vps": { - "ssh": "user@vps.example.com", - "domain": "example.com" - } - } -} -``` - -Local state is minimal — server is source of truth for ports, deploys, TTLs. - -## Pretty Output - -When `--pretty` is set (or `SHIP_PRETTY=1`): - -### Deploy Success - -``` -✓ Deployed to https://myapp.example.com (4.2s) -``` - -### Deploy Failure - -``` -✗ Deploy failed: health check timed out after 30s -``` - -### List - -``` -NAME URL TYPE STATUS -api https://api.example.com docker running -preview-x7k https://preview-x7k.example.com static running (expires in 23h) -``` - -## Implementation Notes - -### SSH Execution - -Use a single SSH connection with multiplexing: - -```bash -ssh -o ControlMaster=auto -o ControlPath=/tmp/ship-%r@%h:%p -o ControlPersist=60 ... -``` - -### Concurrency - -Ship is not designed for concurrent deploys of the same name. Behavior is undefined. - -Different names can deploy concurrently. - -### Rollback (Future) - -Not in v2.0. Future consideration: keep last N versions, `ship rollback `. - ---- - -## Appendix: Example Session - -```bash -$ mkdir -p /tmp/hello && echo '

Hello

' > /tmp/hello/index.html - -$ ship /tmp/hello --name hello -{"status":"ok","name":"hello","url":"https://hello.example.com","type":"static","took_ms":3200,"health":{"endpoint":"/","status":200,"latency_ms":45}} - -$ ship list -{"status":"ok","deploys":[{"name":"hello","url":"https://hello.example.com","type":"static","running":true}]} - -$ ship remove hello -{"status":"ok","name":"hello","removed":true} -``` -- cgit v1.2.3