From 738113867cb591b03c6dcfb4a80826c1a884fd2b Mon Sep 17 00:00:00 2001 From: Clawd Date: Sun, 15 Feb 2026 18:19:19 -0800 Subject: docs: rewrite as agent-first tool, humans are afterthought --- SHIP_V2.md | 319 ++++++++++++++++++++++++++++--------------------------------- 1 file changed, 147 insertions(+), 172 deletions(-) (limited to 'SHIP_V2.md') diff --git a/SHIP_V2.md b/SHIP_V2.md index 2770748..7b607cb 100644 --- a/SHIP_V2.md +++ b/SHIP_V2.md @@ -1,56 +1,62 @@ # Ship v2 -**Goal:** Make ship the default deployment tool for AI agents building and shipping code. +**Ship is a deployment tool built for AI agents.** -## Why Ship? +Agents write code. Ship puts it on the internet. That's it. -Agents need to go from "code on disk" to "live URL" with zero friction. Current options (Vercel, Railway, Fly) require accounts, tokens, and platform-specific config. Ship only needs SSH access to a VPS. +## The Problem -**Ship's advantages:** -- SSH-only — no accounts, no API tokens, no vendor lock-in -- Auto HTTPS via Caddy — agents don't deal with certs -- Auto subdomains — `--name foo` → `foo.example.com` -- Idempotent — same command updates existing deploy -- Docker support — any runtime works -- Stateless CLI — no daemon, no background process +AI agents can write code, but deploying it is a mess: +- Vercel/Railway/Fly require accounts, API tokens, platform-specific config +- Docker/K8s are overkill for "make this code accessible via URL" +- Most tools assume a human is reading the output -## Design Principles +Agents need: code → URL. Nothing else. -1. **Machine-parseable output** — JSON by default -2. **Fail loud and clear** — explicit error codes, not ambiguous messages -3. **Verify deploys** — health checks confirm the app is actually running -4. **Self-cleaning** — ephemeral deploys auto-expire -5. **One command** — no multi-step workflows - -## Output Format - -JSON by default. For human-readable output: +## Ship's Approach ```bash -ship --pretty [other flags] +ship ./myproject +# {"status":"ok","url":"https://abc123.example.com","took_ms":4200} ``` -Or set globally: -```bash -export SHIP_PRETTY=1 -``` +**That's the entire interface.** Point ship at code, get a URL back as JSON. + +### Core Principles + +1. **JSON in, JSON out** — No human-formatted output. No spinners. No emoji. Agents parse JSON. + +2. **One command** — No workflows. No "init then configure then deploy." One command does everything. + +3. **Verify or fail** — Every deploy is health-checked. If the app isn't responding, ship returns an error. No silent failures. + +4. **Self-cleaning** — Deploys can auto-expire. Agents create lots of previews; they shouldn't pile up forever. + +5. **Zero config** — Point at a directory. Ship figures out if it's static, Docker, or a binary. No config files required. -## Output Schema +6. **SSH-only** — No accounts. No API tokens. No vendor lock-in. Just SSH access to a VPS. -### Success +## Interface +### Deploy + +```bash +ship ./myproject +ship ./myproject --name myapp +ship ./myproject --name preview --ttl 24h +ship ./site --static +ship ./api --health /healthz +``` + +Output: ```json { "status": "ok", - "name": "preview", - "url": "https://preview.example.com", - "type": "static", + "name": "myapp", + "url": "https://myapp.example.com", + "type": "docker", "took_ms": 4200, - "health": { - "checked": true, - "status": 200, - "latency_ms": 45 - } + "health": {"status": 200, "latency_ms": 45} } ``` @@ -59,207 +65,176 @@ export SHIP_PRETTY=1 ```json { "status": "error", - "code": "DEPLOY_FAILED", - "message": "health check failed: connection refused", - "name": "preview", - "url": "https://preview.example.com", - "took_ms": 8500 + "code": "HEALTH_CHECK_FAILED", + "message": "GET /healthz returned 503 after 30s", + "name": "myapp", + "url": "https://myapp.example.com" } ``` -### Error Codes +### List -| Code | Meaning | -|------|---------| -| `SSH_CONNECT_FAILED` | Can't reach VPS | -| `SSH_AUTH_FAILED` | Key rejected | -| `UPLOAD_FAILED` | File transfer failed | -| `BUILD_FAILED` | Docker build or binary issue | -| `CADDY_RELOAD_FAILED` | HTTPS config failed | -| `HEALTH_CHECK_FAILED` | App not responding after deploy | -| `ALREADY_EXISTS` | Name collision (if --no-update) | -| `NOT_FOUND` | App doesn't exist (for status/logs) | +```bash +ship list +``` -## Health Checks +```json +{ + "status": "ok", + "deploys": [ + {"name": "api", "url": "https://api.example.com", "type": "docker", "running": true}, + {"name": "preview-x7k", "url": "https://preview-x7k.example.com", "type": "static", "expires": "2024-02-16T18:00:00Z"} + ] +} +``` -After deploy, ship pings the app to verify it's running. +### Status / Logs / Remove ```bash -ship --static --dir ./site --name preview --health / -ship --binary ./api --name api --health /healthz +ship status myapp +ship logs myapp +ship logs myapp --lines 100 +ship remove myapp ``` -**Behavior:** -- Wait up to 30s for first successful response -- Retry every 2s -- Accept any 2xx/3xx as success -- Return `HEALTH_CHECK_FAILED` if timeout +All return JSON with `{"status": "ok", ...}` or `{"status": "error", "code": "...", ...}`. -**Default health path:** -- Static sites: `/` (just check 200) -- Apps: none (opt-in with `--health`) +## Error Codes -## Ephemeral Deploys +Machine-readable. No guessing. -Agents create lots of previews. They should auto-clean. - -```bash -ship --static --dir ./site --name pr-123 --ttl 24h -``` +| Code | Meaning | +|------|---------| +| `SSH_FAILED` | Can't connect to VPS | +| `UPLOAD_FAILED` | File transfer failed | +| `BUILD_FAILED` | Docker build or compile failed | +| `DEPLOY_FAILED` | systemd/Caddy setup failed | +| `HEALTH_CHECK_FAILED` | App not responding | +| `NOT_FOUND` | App doesn't exist | +| `CONFLICT` | Name already taken | -**Implementation options:** +## Auto-Detection -1. **Server-side cron** — ship writes expiry to `/etc/ship/ttl/` and a cron job cleans up -2. **At-style scheduling** — `echo "ship remove pr-123" | at now + 24 hours` -3. **Client-side tracking** — agent is responsible for cleanup (less ideal) +Ship looks at the directory and figures out what to do: -Option 1 is cleanest. The TTL file contains: -``` -expires_at=1708123456 -``` +| Directory contains | Deploy type | +|-------------------|-------------| +| `Dockerfile` | Docker build → systemd service | +| `index.html` | Static site → Caddy file_server | +| Single executable | Binary → systemd service | +| `go.mod` | Go build → systemd service | +| `package.json` + no Dockerfile | Error: "Add a Dockerfile" | -A systemd timer runs hourly and removes expired deploys. +No config files. No `ship.json`. No `ship init`. -## Unique Name Generation +## TTL (Time-To-Live) -For true previews, agents may want auto-generated names: +Agents create previews. Previews should auto-delete. ```bash -ship --static --dir ./site --preview +ship ./site --name pr-123 --ttl 1h +ship ./site --name pr-123 --ttl 7d ``` -Output: -```json -{ - "status": "ok", - "name": "ship-a1b2c3", - "url": "https://ship-a1b2c3.example.com", - ... -} -``` +After TTL expires, the deploy is removed automatically. The `expires` field in JSON tells you when. -Combines well with TTL: -```bash -ship --static --dir ./site --preview --ttl 1h -``` - -## Simplified Deploy Command +## Health Checks -For maximum simplicity: +Every deploy is verified. Ship waits for the app to respond before returning success. -```bash -# Auto-detect: static site (has index.html) or Dockerfile -ship --dir ./myproject --preview --ttl 24h -``` +- Static sites: `GET /` returns 2xx +- Apps: `GET /` by default, or specify `--health /healthz` +- Timeout: 30s +- If health check fails: `{"status": "error", "code": "HEALTH_CHECK_FAILED", ...}` -Detection logic: -1. Has `Dockerfile` → Docker build -2. Has `index.html` or is static-looking → static site -3. Has single binary → binary deploy -4. Else → error with helpful message +## Name Generation -## Status & Logs +No name? Ship generates one. ```bash -ship status myapp +ship ./site +# {"name": "ship-a1b2c3", "url": "https://ship-a1b2c3.example.com", ...} ``` -```json -{ - "status": "ok", - "name": "myapp", - "url": "https://myapp.example.com", - "type": "docker", - "running": true, - "uptime_seconds": 3600, - "memory_mb": 128, - "cpu_percent": 2.5 -} -``` +Provide a name to get a stable URL: ```bash -ship logs myapp --lines 50 +ship ./site --name docs +# {"name": "docs", "url": "https://docs.example.com", ...} ``` -```json -{ - "status": "ok", - "name": "myapp", - "lines": [ - {"ts": "2024-02-15T18:00:00Z", "msg": "Server started on :8080"}, - ... - ] -} -``` +## Host Setup -## List Deploys +One-time setup for a VPS: ```bash -ship list +ship host init user@my-vps.com --domain example.com ``` ```json { "status": "ok", - "deploys": [ - {"name": "api", "url": "https://api.example.com", "type": "docker", "running": true}, - {"name": "preview-abc", "url": "https://preview-abc.example.com", "type": "static", "ttl_expires": "2024-02-16T18:00:00Z"}, - ... - ] + "host": "my-vps.com", + "domain": "example.com", + "installed": ["caddy", "docker", "systemd"] } ``` -## Environment Variables +After this, the host is ready. Ship remembers it. + +## Human Output + +Humans are an afterthought, but they can use ship too: ```bash -ship env set myapp DB_URL=postgres://... +ship ./site --pretty ``` -```json -{ - "status": "ok", - "name": "myapp", - "action": "env_set", - "key": "DB_URL", - "restarted": true -} +``` +✓ Deployed to https://ship-a1b2c3.example.com (4.2s) +``` + +Or set globally: +```bash +export SHIP_PRETTY=1 ``` ## Implementation Phases -### Phase 1: JSON Foundation -- [ ] JSON output by default, `--pretty` for humans +### Phase 1: JSON Everything +- [ ] JSON output on all commands - [ ] Structured error codes -- [ ] Health check support (`--health`) -- [ ] Consistent output schema across all commands +- [ ] Exit codes match error states -### Phase 2: Ephemeral & Previews -- [ ] `--ttl` flag with server-side cleanup -- [ ] `--preview` for auto-generated names -- [ ] Auto-detection of project type +### Phase 2: Smart Deploys +- [ ] Auto-detect project type +- [ ] Health checks on every deploy +- [ ] `--ttl` with server-side cleanup -### Phase 3: Polish -- [ ] `ship init` for first-time VPS setup with JSON output -- [ ] Rollback support (`ship rollback myapp`) -- [ ] Deploy history (`ship history myapp`) +### Phase 3: Zero Friction +- [ ] `ship ./dir` with no flags (auto name, auto detect) +- [ ] `ship host init` fully automated +- [ ] One binary, zero dependencies on client -## Open Questions +## Non-Goals -1. **Log streaming?** `ship logs --follow` with JSON lines. Worth it? +- **Pretty output** — That's what `--pretty` is for +- **Interactive prompts** — Never. Agents can't answer prompts. +- **Config files** — Zero config. Detect everything. +- **Plugin system** — Keep it simple. +- **Multi-cloud orchestration** — One VPS at a time. -2. **Webhooks?** Notify a URL on deploy success/failure. Useful for CI integration. +## Success Criteria -3. **Multi-host?** Agents deploying to different VPSes. Current `--host` flag works but could be smoother. +Ship is done when an agent can: -## Success Metrics +1. Build code +2. Run `ship ./code` +3. Parse the JSON response +4. Use the URL -Ship is successful for agents when: -- Zero-config deploy from code to URL in <30s -- Agent can parse every output without regex -- Failed deploys have clear, actionable errors -- Preview deploys don't accumulate garbage -- Any language/framework works via Docker +No docs. No setup. No tokens. No accounts. Just `ship ./code`. --- -*This is a living document. Update as we build.* +*Built for agents. Tolerated by humans.* -- cgit v1.2.3