summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorClawd <ai@clawd.bot>2026-02-17 08:13:28 -0800
committerClawd <ai@clawd.bot>2026-02-17 08:13:28 -0800
commit805541df0f22f1200692ddfae7b2f199a89a8f7b (patch)
treed5916c11b159bdd834b4a2f492ae58a418a05966
parentc50f5c5feb7e2b4c43fe3f54a31d853fb828ff04 (diff)
Remove remaining planning docs (SHIP_V2.md, SPEC.md)main
-rw-r--r--SHIP_V2.md240
-rw-r--r--SPEC.md499
2 files changed, 0 insertions, 739 deletions
diff --git a/SHIP_V2.md b/SHIP_V2.md
deleted file mode 100644
index 7b607cb..0000000
--- a/SHIP_V2.md
+++ /dev/null
@@ -1,240 +0,0 @@
1# Ship v2
2
3**Ship is a deployment tool built for AI agents.**
4
5Agents write code. Ship puts it on the internet. That's it.
6
7## The Problem
8
9AI agents can write code, but deploying it is a mess:
10- Vercel/Railway/Fly require accounts, API tokens, platform-specific config
11- Docker/K8s are overkill for "make this code accessible via URL"
12- Most tools assume a human is reading the output
13
14Agents need: code → URL. Nothing else.
15
16## Ship's Approach
17
18```bash
19ship ./myproject
20# {"status":"ok","url":"https://abc123.example.com","took_ms":4200}
21```
22
23**That's the entire interface.** Point ship at code, get a URL back as JSON.
24
25### Core Principles
26
271. **JSON in, JSON out** — No human-formatted output. No spinners. No emoji. Agents parse JSON.
28
292. **One command** — No workflows. No "init then configure then deploy." One command does everything.
30
313. **Verify or fail** — Every deploy is health-checked. If the app isn't responding, ship returns an error. No silent failures.
32
334. **Self-cleaning** — Deploys can auto-expire. Agents create lots of previews; they shouldn't pile up forever.
34
355. **Zero config** — Point at a directory. Ship figures out if it's static, Docker, or a binary. No config files required.
36
376. **SSH-only** — No accounts. No API tokens. No vendor lock-in. Just SSH access to a VPS.
38
39## Interface
40
41### Deploy
42
43```bash
44ship ./myproject
45ship ./myproject --name myapp
46ship ./myproject --name preview --ttl 24h
47ship ./site --static
48ship ./api --health /healthz
49```
50
51Output:
52```json
53{
54 "status": "ok",
55 "name": "myapp",
56 "url": "https://myapp.example.com",
57 "type": "docker",
58 "took_ms": 4200,
59 "health": {"status": 200, "latency_ms": 45}
60}
61```
62
63### Error
64
65```json
66{
67 "status": "error",
68 "code": "HEALTH_CHECK_FAILED",
69 "message": "GET /healthz returned 503 after 30s",
70 "name": "myapp",
71 "url": "https://myapp.example.com"
72}
73```
74
75### List
76
77```bash
78ship list
79```
80
81```json
82{
83 "status": "ok",
84 "deploys": [
85 {"name": "api", "url": "https://api.example.com", "type": "docker", "running": true},
86 {"name": "preview-x7k", "url": "https://preview-x7k.example.com", "type": "static", "expires": "2024-02-16T18:00:00Z"}
87 ]
88}
89```
90
91### Status / Logs / Remove
92
93```bash
94ship status myapp
95ship logs myapp
96ship logs myapp --lines 100
97ship remove myapp
98```
99
100All return JSON with `{"status": "ok", ...}` or `{"status": "error", "code": "...", ...}`.
101
102## Error Codes
103
104Machine-readable. No guessing.
105
106| Code | Meaning |
107|------|---------|
108| `SSH_FAILED` | Can't connect to VPS |
109| `UPLOAD_FAILED` | File transfer failed |
110| `BUILD_FAILED` | Docker build or compile failed |
111| `DEPLOY_FAILED` | systemd/Caddy setup failed |
112| `HEALTH_CHECK_FAILED` | App not responding |
113| `NOT_FOUND` | App doesn't exist |
114| `CONFLICT` | Name already taken |
115
116## Auto-Detection
117
118Ship looks at the directory and figures out what to do:
119
120| Directory contains | Deploy type |
121|-------------------|-------------|
122| `Dockerfile` | Docker build → systemd service |
123| `index.html` | Static site → Caddy file_server |
124| Single executable | Binary → systemd service |
125| `go.mod` | Go build → systemd service |
126| `package.json` + no Dockerfile | Error: "Add a Dockerfile" |
127
128No config files. No `ship.json`. No `ship init`.
129
130## TTL (Time-To-Live)
131
132Agents create previews. Previews should auto-delete.
133
134```bash
135ship ./site --name pr-123 --ttl 1h
136ship ./site --name pr-123 --ttl 7d
137```
138
139After TTL expires, the deploy is removed automatically. The `expires` field in JSON tells you when.
140
141## Health Checks
142
143Every deploy is verified. Ship waits for the app to respond before returning success.
144
145- Static sites: `GET /` returns 2xx
146- Apps: `GET /` by default, or specify `--health /healthz`
147- Timeout: 30s
148- If health check fails: `{"status": "error", "code": "HEALTH_CHECK_FAILED", ...}`
149
150## Name Generation
151
152No name? Ship generates one.
153
154```bash
155ship ./site
156# {"name": "ship-a1b2c3", "url": "https://ship-a1b2c3.example.com", ...}
157```
158
159Provide a name to get a stable URL:
160
161```bash
162ship ./site --name docs
163# {"name": "docs", "url": "https://docs.example.com", ...}
164```
165
166## Host Setup
167
168One-time setup for a VPS:
169
170```bash
171ship host init user@my-vps.com --domain example.com
172```
173
174```json
175{
176 "status": "ok",
177 "host": "my-vps.com",
178 "domain": "example.com",
179 "installed": ["caddy", "docker", "systemd"]
180}
181```
182
183After this, the host is ready. Ship remembers it.
184
185## Human Output
186
187Humans are an afterthought, but they can use ship too:
188
189```bash
190ship ./site --pretty
191```
192
193```
194✓ Deployed to https://ship-a1b2c3.example.com (4.2s)
195```
196
197Or set globally:
198```bash
199export SHIP_PRETTY=1
200```
201
202## Implementation Phases
203
204### Phase 1: JSON Everything
205- [ ] JSON output on all commands
206- [ ] Structured error codes
207- [ ] Exit codes match error states
208
209### Phase 2: Smart Deploys
210- [ ] Auto-detect project type
211- [ ] Health checks on every deploy
212- [ ] `--ttl` with server-side cleanup
213
214### Phase 3: Zero Friction
215- [ ] `ship ./dir` with no flags (auto name, auto detect)
216- [ ] `ship host init` fully automated
217- [ ] One binary, zero dependencies on client
218
219## Non-Goals
220
221- **Pretty output** — That's what `--pretty` is for
222- **Interactive prompts** — Never. Agents can't answer prompts.
223- **Config files** — Zero config. Detect everything.
224- **Plugin system** — Keep it simple.
225- **Multi-cloud orchestration** — One VPS at a time.
226
227## Success Criteria
228
229Ship is done when an agent can:
230
2311. Build code
2322. Run `ship ./code`
2333. Parse the JSON response
2344. Use the URL
235
236No docs. No setup. No tokens. No accounts. Just `ship ./code`.
237
238---
239
240*Built for agents. Tolerated by humans.*
diff --git a/SPEC.md b/SPEC.md
deleted file mode 100644
index 50b54c2..0000000
--- a/SPEC.md
+++ /dev/null
@@ -1,499 +0,0 @@
1# Ship v2 Technical Specification
2
3## Overview
4
5Ship deploys code to a VPS. Input: path to code. Output: JSON with URL.
6
7```bash
8ship ./myproject
9# {"status":"ok","name":"ship-a1b2c3","url":"https://ship-a1b2c3.example.com","type":"docker","took_ms":8200}
10```
11
12## CLI Interface
13
14### Primary Command
15
16```
17ship [PATH] [FLAGS]
18```
19
20**PATH**: File or directory to deploy. Defaults to `.` (current directory).
21
22**FLAGS**:
23| Flag | Type | Default | Description |
24|------|------|---------|-------------|
25| `--name` | string | auto-generated | Deploy name (becomes subdomain) |
26| `--host` | string | default host | VPS host (SSH config name or user@host) |
27| `--health` | string | `/` for static, none for apps | Health check endpoint |
28| `--ttl` | duration | none | Auto-delete after duration (e.g., `1h`, `7d`) |
29| `--env` | string[] | none | Environment variables (`KEY=VALUE`) |
30| `--env-file` | string | none | Path to .env file |
31| `--pretty` | bool | false | Human-readable output |
32
33### Other Commands
34
35```
36ship list [--host HOST]
37ship status NAME [--host HOST]
38ship logs NAME [--lines N] [--host HOST]
39ship remove NAME [--host HOST]
40ship host init USER@HOST --domain DOMAIN
41ship host status [--host HOST]
42```
43
44All commands output JSON unless `--pretty` is set.
45
46## Output Schema
47
48### Success Response
49
50```typescript
51{
52 status: "ok",
53 name: string,
54 url: string,
55 type: "static" | "docker" | "binary",
56 took_ms: number,
57 health?: {
58 endpoint: string,
59 status: number,
60 latency_ms: number
61 },
62 expires?: string // ISO 8601, only if --ttl set
63}
64```
65
66### Error Response
67
68```typescript
69{
70 status: "error",
71 code: string,
72 message: string,
73 name?: string,
74 url?: string
75}
76```
77
78### Exit Codes
79
80| Code | Meaning |
81|------|---------|
82| 0 | Success |
83| 1 | Deploy failed |
84| 2 | Invalid arguments |
85| 3 | SSH connection failed |
86| 4 | Health check failed |
87
88## Error Codes
89
90| Code | Description |
91|------|-------------|
92| `INVALID_PATH` | Path doesn't exist or isn't readable |
93| `UNKNOWN_PROJECT_TYPE` | Can't detect how to deploy |
94| `SSH_CONNECT_FAILED` | Can't establish SSH connection |
95| `SSH_AUTH_FAILED` | SSH key rejected |
96| `UPLOAD_FAILED` | SCP/rsync failed |
97| `BUILD_FAILED` | Docker build or compilation failed |
98| `SERVICE_FAILED` | systemd unit failed to start |
99| `CADDY_FAILED` | Caddy reload failed |
100| `HEALTH_CHECK_FAILED` | App didn't respond in time |
101| `HEALTH_CHECK_TIMEOUT` | Health check timed out |
102| `NOT_FOUND` | Named deploy doesn't exist |
103| `CONFLICT` | Name already in use (if --no-update) |
104| `HOST_NOT_CONFIGURED` | No default host, --host required |
105| `INVALID_TTL` | Can't parse TTL duration |
106
107## Auto-Detection Logic
108
109When given a path, ship determines deploy type:
110
111```
112is_file(path)?
113 → is_executable(path)?
114 → BINARY
115 → ERROR: "Not an executable file"
116
117is_directory(path)?
118 → has_file("Dockerfile")?
119 → DOCKER
120 → has_file("index.html") OR has_file("index.htm")?
121 → STATIC
122 → has_file("go.mod") AND NOT has_file("Dockerfile")?
123 → ERROR: "Go project without Dockerfile. Add a Dockerfile or build a binary."
124 → has_file("package.json") AND NOT has_file("Dockerfile")?
125 → ERROR: "Node project without Dockerfile. Add a Dockerfile."
126 → is_empty(path)?
127 → ERROR: "Directory is empty"
128 → ERROR: "Can't detect project type. Add a Dockerfile or index.html."
129```
130
131## Name Generation
132
133If `--name` not provided:
134
135```
136name = "ship-" + random_alphanumeric(6)
137```
138
139Names must match: `^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$`
140
141Invalid names return `INVALID_NAME` error.
142
143## Port Allocation
144
145Ports are allocated server-side.
146
147### Server Files
148
149```
150/etc/ship/ports/<name> # Contains allocated port number
151```
152
153### Allocation Algorithm
154
155```python
156def allocate_port(name):
157 port_file = f"/etc/ship/ports/{name}"
158
159 if exists(port_file):
160 return int(read(port_file))
161
162 used_ports = [int(read(f)) for f in glob("/etc/ship/ports/*")]
163 next_port = 9000
164 while next_port in used_ports:
165 next_port += 1
166
167 write(port_file, str(next_port))
168 return next_port
169```
170
171### Port Range
172
173- Start: 9000
174- Max: 9999
175- Error if exhausted: `PORT_EXHAUSTED`
176
177## Server File Layout
178
179```
180/etc/ship/
181 ports/<name> # Port allocation (contains port number)
182 env/<name>.env # Environment variables
183 ttl/<name> # TTL expiry timestamp (Unix epoch)
184
185/var/www/<name>/ # Static site files
186
187/var/lib/<name>/
188 src/ # Docker build context (checked out source)
189 data/ # Persistent data volume
190
191/etc/systemd/system/<name>.service # systemd unit
192/etc/caddy/sites-enabled/<name>.caddy # Caddy config
193```
194
195## Deploy Flows
196
197### Static Site
198
1991. Validate path is directory with index.html
2002. Generate name if needed
2013. SSH: Create `/var/www/<name>/`
2024. rsync: Upload directory contents to `/var/www/<name>/`
2035. SSH: Set ownership to `www-data`
2046. Generate Caddyfile
2057. SSH: Write to `/etc/caddy/sites-enabled/<name>.caddy`
2068. SSH: Reload Caddy
2079. Health check: `GET https://<name>.<domain>/`
20810. If TTL: Write expiry to `/etc/ship/ttl/<name>`
20911. Return JSON
210
211### Docker
212
2131. Validate path is directory with Dockerfile
2142. Generate name if needed
2153. Allocate port (server-side)
2164. rsync: Upload directory to `/var/lib/<name>/src/`
2175. SSH: `docker build -t <name> /var/lib/<name>/src/`
2186. Generate systemd unit (runs `docker run`)
2197. SSH: Write to `/etc/systemd/system/<name>.service`
2208. SSH: Write env file to `/etc/ship/env/<name>.env`
2219. SSH: `systemctl daemon-reload && systemctl restart <name>`
22210. Generate Caddyfile (reverse proxy to port)
22311. SSH: Write to `/etc/caddy/sites-enabled/<name>.caddy`
22412. SSH: Reload Caddy
22513. Health check: `GET https://<name>.<domain><health_endpoint>`
22614. If TTL: Write expiry to `/etc/ship/ttl/<name>`
22715. Return JSON
228
229### Binary
230
2311. Validate path is executable file
2322. Generate name if needed
2333. Allocate port (server-side)
2344. SCP: Upload binary to `/usr/local/bin/<name>`
2355. SSH: `chmod +x /usr/local/bin/<name>`
2366. Generate systemd unit
2377. SSH: Write to `/etc/systemd/system/<name>.service`
2388. SSH: Write env file to `/etc/ship/env/<name>.env`
2399. SSH: `systemctl daemon-reload && systemctl restart <name>`
24010. Generate Caddyfile (reverse proxy to port)
24111. SSH: Write to `/etc/caddy/sites-enabled/<name>.caddy`
24212. SSH: Reload Caddy
24313. Health check: `GET https://<name>.<domain><health_endpoint>`
24414. If TTL: Write expiry to `/etc/ship/ttl/<name>`
24515. Return JSON
246
247## Health Checks
248
249### Behavior
250
251After deploy, verify the app is responding:
252
2531. Wait 2 seconds (let app start)
2542. `GET https://<name>.<domain><health_endpoint>`
2553. If 2xx or 3xx: success
2564. If error or timeout: retry after 2s
2575. Max retries: 15 (total 30s)
2586. If all retries fail: return `HEALTH_CHECK_FAILED`
259
260### Health Endpoint
261
262| Deploy Type | Default | Override |
263|-------------|---------|----------|
264| static | `/` | `--health` |
265| docker | none (skip) | `--health` |
266| binary | none (skip) | `--health` |
267
268When no health endpoint: skip health check, return success after service starts.
269
270## TTL (Auto-Expiry)
271
272### Setting TTL
273
274```bash
275ship ./site --name preview --ttl 24h
276```
277
278Supported formats: `30m`, `1h`, `24h`, `7d`
279
280### Server-Side Cleanup
281
282A systemd timer runs hourly:
283
284```ini
285# /etc/systemd/system/ship-cleanup.timer
286[Timer]
287OnCalendar=hourly
288Persistent=true
289
290# /etc/systemd/system/ship-cleanup.service
291[Service]
292Type=oneshot
293ExecStart=/usr/local/bin/ship-cleanup
294```
295
296Cleanup script:
297
298```bash
299#!/bin/bash
300now=$(date +%s)
301for f in /etc/ship/ttl/*; do
302 name=$(basename "$f")
303 expires=$(cat "$f")
304 if [ "$now" -gt "$expires" ]; then
305 ship remove "$name"
306 fi
307done
308```
309
310### TTL File Format
311
312```
313/etc/ship/ttl/<name>
314```
315
316Contents: Unix timestamp (seconds since epoch)
317
318## Templates
319
320### systemd Unit (Docker)
321
322```ini
323[Unit]
324Description=ship: {{.Name}}
325After=docker.service
326Requires=docker.service
327
328[Service]
329Restart=always
330RestartSec=5
331EnvironmentFile=/etc/ship/env/{{.Name}}.env
332ExecStartPre=-/usr/bin/docker stop {{.Name}}
333ExecStartPre=-/usr/bin/docker rm {{.Name}}
334ExecStart=/usr/bin/docker run --rm --name {{.Name}} \
335 --env-file /etc/ship/env/{{.Name}}.env \
336 -p 127.0.0.1:{{.Port}}:{{.Port}} \
337 -v /var/lib/{{.Name}}/data:/data \
338 {{.Name}}
339ExecStop=/usr/bin/docker stop {{.Name}}
340
341[Install]
342WantedBy=multi-user.target
343```
344
345### systemd Unit (Binary)
346
347```ini
348[Unit]
349Description=ship: {{.Name}}
350After=network.target
351
352[Service]
353Type=simple
354Restart=always
355RestartSec=5
356User={{.Name}}
357EnvironmentFile=/etc/ship/env/{{.Name}}.env
358ExecStart=/usr/local/bin/{{.Name}}
359WorkingDirectory=/var/lib/{{.Name}}
360
361[Install]
362WantedBy=multi-user.target
363```
364
365### Caddyfile (Reverse Proxy)
366
367```
368{{.Domain}} {
369 reverse_proxy localhost:{{.Port}}
370}
371```
372
373### Caddyfile (Static)
374
375```
376{{.Domain}} {
377 root * /var/www/{{.Name}}
378 file_server
379 encode gzip
380}
381```
382
383## Environment Variables
384
385Always set by ship:
386
387| Variable | Value |
388|----------|-------|
389| `PORT` | Allocated port number |
390| `SHIP_NAME` | Deploy name |
391| `SHIP_URL` | Full URL |
392
393User variables from `--env` or `--env-file` are merged.
394
395## Host Initialization
396
397```bash
398ship host init user@vps.example.com --domain example.com
399```
400
401### Steps
402
4031. SSH: Check connectivity
4042. SSH: Install packages (`apt install -y caddy docker.io`)
4053. SSH: Enable services (`systemctl enable --now caddy docker`)
4064. SSH: Create directories (`/etc/ship/ports`, `/etc/ship/env`, `/etc/ship/ttl`)
4075. SSH: Install cleanup timer
4086. SSH: Configure Caddy base
4097. Update local state with host info
410
411### Output
412
413```json
414{
415 "status": "ok",
416 "host": "vps.example.com",
417 "domain": "example.com",
418 "installed": ["caddy", "docker"]
419}
420```
421
422## Local State
423
424Stored at `~/.config/ship/state.json`:
425
426```json
427{
428 "default_host": "vps",
429 "hosts": {
430 "vps": {
431 "ssh": "user@vps.example.com",
432 "domain": "example.com"
433 }
434 }
435}
436```
437
438Local state is minimal — server is source of truth for ports, deploys, TTLs.
439
440## Pretty Output
441
442When `--pretty` is set (or `SHIP_PRETTY=1`):
443
444### Deploy Success
445
446```
447✓ Deployed to https://myapp.example.com (4.2s)
448```
449
450### Deploy Failure
451
452```
453✗ Deploy failed: health check timed out after 30s
454```
455
456### List
457
458```
459NAME URL TYPE STATUS
460api https://api.example.com docker running
461preview-x7k https://preview-x7k.example.com static running (expires in 23h)
462```
463
464## Implementation Notes
465
466### SSH Execution
467
468Use a single SSH connection with multiplexing:
469
470```bash
471ssh -o ControlMaster=auto -o ControlPath=/tmp/ship-%r@%h:%p -o ControlPersist=60 ...
472```
473
474### Concurrency
475
476Ship is not designed for concurrent deploys of the same name. Behavior is undefined.
477
478Different names can deploy concurrently.
479
480### Rollback (Future)
481
482Not in v2.0. Future consideration: keep last N versions, `ship rollback <name>`.
483
484---
485
486## Appendix: Example Session
487
488```bash
489$ mkdir -p /tmp/hello && echo '<h1>Hello</h1>' > /tmp/hello/index.html
490
491$ ship /tmp/hello --name hello
492{"status":"ok","name":"hello","url":"https://hello.example.com","type":"static","took_ms":3200,"health":{"endpoint":"/","status":200,"latency_ms":45}}
493
494$ ship list
495{"status":"ok","deploys":[{"name":"hello","url":"https://hello.example.com","type":"static","running":true}]}
496
497$ ship remove hello
498{"status":"ok","name":"hello","removed":true}
499```