diff options
| author | Clawd <ai@clawd.bot> | 2026-04-11 20:43:41 -0700 |
|---|---|---|
| committer | Clawd <ai@clawd.bot> | 2026-04-11 20:43:41 -0700 |
| commit | d0ae31c24c3c98ae89eebd67227c0c0d01606ed5 (patch) | |
| tree | c684469e0f7d3b65477cfc631ecdaafa3c6a218a /skills/ship-binary/SKILL.md | |
| parent | 5548b36e0953c17dbe30f6b63c892b7c83196b20 (diff) | |
Add ship-* Claude skills and plan
Introduces a skills/ directory with 8 Claude skills that reimagine ship
as a set of composable, human-driven deployment tools backed by Claude's
reasoning rather than a rigid CLI.
Skills:
- ship-setup: one-time VPS config, saves host to ~/.config/ship/config.json
- ship-status: derives live state from server, no local state file
- ship-env: read/write env vars with merge semantics, never overwrites
- ship-binary: deploy Go binaries with SQLite backup, correct restart behavior
- ship-caddy: manage per-app Caddyfile with validate-before-reload
- ship-service: systemd management and log inspection
- ship-static: rsync static sites with SPA routing support
- ship-deploy: orchestration runbook tying the others together
Also adds SKILLS_PLAN.md documenting the architecture and rationale.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'skills/ship-binary/SKILL.md')
| -rw-r--r-- | skills/ship-binary/SKILL.md | 168 |
1 files changed, 168 insertions, 0 deletions
diff --git a/skills/ship-binary/SKILL.md b/skills/ship-binary/SKILL.md new file mode 100644 index 0000000..16056ff --- /dev/null +++ b/skills/ship-binary/SKILL.md | |||
| @@ -0,0 +1,168 @@ | |||
| 1 | --- | ||
| 2 | name: ship-binary | ||
| 3 | description: Upload and deploy a pre-built binary to a ship VPS. Handles port allocation, systemd service, env vars, Caddy config, and SQLite backup. Use when deploying a Go binary or other compiled executable. | ||
| 4 | argument-hint: "<path-to-binary> <app-name> [host-nickname]" | ||
| 5 | --- | ||
| 6 | |||
| 7 | # ship-binary | ||
| 8 | |||
| 9 | Upload a pre-built binary and deploy it as a systemd service with Caddy reverse proxy. | ||
| 10 | |||
| 11 | ## Read Config | ||
| 12 | |||
| 13 | ```bash | ||
| 14 | python3 -c " | ||
| 15 | import json, os | ||
| 16 | cfg = json.load(open(os.path.expanduser('~/.config/ship/config.json'))) | ||
| 17 | nick = '<nickname-or-default>' | ||
| 18 | h = cfg['hosts'].get(nick, cfg['hosts'][cfg['default']]) | ||
| 19 | print(h['host']) | ||
| 20 | print(h['domain']) | ||
| 21 | " | ||
| 22 | ``` | ||
| 23 | |||
| 24 | ## Inputs | ||
| 25 | |||
| 26 | - **Binary path** — local path to the compiled binary | ||
| 27 | - **App name** — short lowercase name, becomes the service name and subdomain (e.g. `foodtracker`) | ||
| 28 | - **Domain** — defaults to `<app-name>.<base-domain>` from config, ask if different | ||
| 29 | - **Env vars** — ask if there are any env vars to set (beyond PORT/SHIP_NAME/SHIP_URL) | ||
| 30 | - **Host** — use default unless specified | ||
| 31 | |||
| 32 | ## Steps | ||
| 33 | |||
| 34 | ### 1. Check if app already exists | ||
| 35 | |||
| 36 | ```bash | ||
| 37 | ssh <host> "test -f /etc/ship/ports/<app-name> && echo exists || echo new" | ||
| 38 | ``` | ||
| 39 | |||
| 40 | This determines whether to allocate a new port or reuse the existing one. | ||
| 41 | |||
| 42 | ### 2. Backup SQLite databases (if app exists) | ||
| 43 | |||
| 44 | Before touching anything, check for SQLite files in the app's data directory: | ||
| 45 | |||
| 46 | ```bash | ||
| 47 | ssh <host> "find /var/lib/<app-name>/ -name '*.db' 2>/dev/null" | ||
| 48 | ``` | ||
| 49 | |||
| 50 | If any `.db` files are found, back them up: | ||
| 51 | |||
| 52 | ```bash | ||
| 53 | ssh <host> "sudo mkdir -p /var/lib/<app-name>/backups && sudo cp /var/lib/<app-name>/data/<name>.db /var/lib/<app-name>/backups/<name>-\$(date +%Y%m%d-%H%M%S).db" | ||
| 54 | ``` | ||
| 55 | |||
| 56 | Tell the user what was backed up before proceeding. | ||
| 57 | |||
| 58 | ### 3. Allocate or retrieve port | ||
| 59 | |||
| 60 | **New app** — find the highest port in use and add 1: | ||
| 61 | ```bash | ||
| 62 | ssh <host> "sudo bash -c 'max=9000; for f in /etc/ship/ports/*; do p=\$(cat \$f 2>/dev/null); [ \"\$p\" -gt \"\$max\" ] && max=\$p; done; port=\$((max+1)); echo \$port | tee /etc/ship/ports/<app-name>; echo \$port'" | ||
| 63 | ``` | ||
| 64 | |||
| 65 | **Existing app** — reuse the existing port: | ||
| 66 | ```bash | ||
| 67 | ssh <host> "cat /etc/ship/ports/<app-name>" | ||
| 68 | ``` | ||
| 69 | |||
| 70 | ### 4. Upload binary | ||
| 71 | |||
| 72 | ```bash | ||
| 73 | scp <binary-path> <host>:/tmp/<app-name> | ||
| 74 | ssh <host> "sudo mv /tmp/<app-name> /usr/local/bin/<app-name> && sudo chmod +x /usr/local/bin/<app-name>" | ||
| 75 | ``` | ||
| 76 | |||
| 77 | ### 5. Create work directory and service user | ||
| 78 | |||
| 79 | ```bash | ||
| 80 | ssh <host> "sudo mkdir -p /var/lib/<app-name>/data && sudo useradd -r -s /bin/false <app-name> 2>/dev/null || true && sudo chown -R <app-name>:<app-name> /var/lib/<app-name>" | ||
| 81 | ``` | ||
| 82 | |||
| 83 | ### 6. Write env file | ||
| 84 | |||
| 85 | Build the env file content merging ship-managed vars with any user-provided vars. | ||
| 86 | If the file already exists, read it first and merge (new values win, old values survive): | ||
| 87 | |||
| 88 | ```bash | ||
| 89 | ssh <host> "sudo cat /etc/ship/env/<app-name>.env 2>/dev/null" | ||
| 90 | ``` | ||
| 91 | |||
| 92 | Then write merged result: | ||
| 93 | ```bash | ||
| 94 | ssh <host> "sudo tee /etc/ship/env/<app-name>.env > /dev/null << 'EOF' | ||
| 95 | PORT=<port> | ||
| 96 | SHIP_NAME=<app-name> | ||
| 97 | SHIP_URL=https://<domain> | ||
| 98 | <user-env-vars> | ||
| 99 | EOF | ||
| 100 | sudo chmod 600 /etc/ship/env/<app-name>.env" | ||
| 101 | ``` | ||
| 102 | |||
| 103 | ### 7. Write systemd unit | ||
| 104 | |||
| 105 | ```bash | ||
| 106 | ssh <host> "sudo tee /etc/systemd/system/<app-name>.service > /dev/null << 'EOF' | ||
| 107 | [Unit] | ||
| 108 | Description=<app-name> | ||
| 109 | After=network.target | ||
| 110 | |||
| 111 | [Service] | ||
| 112 | Type=simple | ||
| 113 | User=<app-name> | ||
| 114 | WorkingDirectory=/var/lib/<app-name> | ||
| 115 | EnvironmentFile=/etc/ship/env/<app-name>.env | ||
| 116 | ExecStart=/usr/local/bin/<app-name> | ||
| 117 | Restart=always | ||
| 118 | RestartSec=5s | ||
| 119 | NoNewPrivileges=true | ||
| 120 | PrivateTmp=true | ||
| 121 | |||
| 122 | [Install] | ||
| 123 | WantedBy=multi-user.target | ||
| 124 | EOF" | ||
| 125 | ``` | ||
| 126 | |||
| 127 | ### 8. Start or restart service | ||
| 128 | |||
| 129 | ```bash | ||
| 130 | ssh <host> "sudo systemctl daemon-reload" | ||
| 131 | ``` | ||
| 132 | |||
| 133 | **New app:** | ||
| 134 | ```bash | ||
| 135 | ssh <host> "sudo systemctl enable --now <app-name>" | ||
| 136 | ``` | ||
| 137 | |||
| 138 | **Existing app:** | ||
| 139 | ```bash | ||
| 140 | ssh <host> "sudo systemctl restart <app-name>" | ||
| 141 | ``` | ||
| 142 | |||
| 143 | ### 9. Write Caddy config | ||
| 144 | |||
| 145 | ```bash | ||
| 146 | ssh <host> "sudo tee /etc/caddy/sites-enabled/<app-name>.caddy > /dev/null << 'EOF' | ||
| 147 | <domain> { | ||
| 148 | reverse_proxy 127.0.0.1:<port> | ||
| 149 | } | ||
| 150 | EOF | ||
| 151 | sudo systemctl reload caddy" | ||
| 152 | ``` | ||
| 153 | |||
| 154 | ### 10. Confirm | ||
| 155 | |||
| 156 | Tell the user: | ||
| 157 | - App name, URL, and port | ||
| 158 | - Whether it was a new deploy or update | ||
| 159 | - Any SQLite backups made | ||
| 160 | - Any env vars set | ||
| 161 | |||
| 162 | ## Notes | ||
| 163 | |||
| 164 | - Always back up SQLite before swapping the binary | ||
| 165 | - Always merge env vars — never replace the whole file | ||
| 166 | - Use `systemctl restart` for existing apps, `enable --now` for new ones | ||
| 167 | - The data directory `/var/lib/<app-name>/data/` persists across deploys | ||
| 168 | - If the user doesn't specify env vars, ask if they need any before deploying | ||
