From d0ae31c24c3c98ae89eebd67227c0c0d01606ed5 Mon Sep 17 00:00:00 2001 From: Clawd Date: Sat, 11 Apr 2026 20:43:41 -0700 Subject: 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 --- SKILLS_PLAN.md | 135 ++++++++++++++++++++++++++++++++++ skills/ship-binary/SKILL.md | 168 +++++++++++++++++++++++++++++++++++++++++++ skills/ship-caddy/SKILL.md | 135 ++++++++++++++++++++++++++++++++++ skills/ship-deploy/SKILL.md | 126 ++++++++++++++++++++++++++++++++ skills/ship-env/SKILL.md | 84 ++++++++++++++++++++++ skills/ship-service/SKILL.md | 133 ++++++++++++++++++++++++++++++++++ skills/ship-setup/SKILL.md | 122 +++++++++++++++++++++++++++++++ skills/ship-static/SKILL.md | 121 +++++++++++++++++++++++++++++++ skills/ship-status/SKILL.md | 85 ++++++++++++++++++++++ 9 files changed, 1109 insertions(+) create mode 100644 SKILLS_PLAN.md create mode 100644 skills/ship-binary/SKILL.md create mode 100644 skills/ship-caddy/SKILL.md create mode 100644 skills/ship-deploy/SKILL.md create mode 100644 skills/ship-env/SKILL.md create mode 100644 skills/ship-service/SKILL.md create mode 100644 skills/ship-setup/SKILL.md create mode 100644 skills/ship-static/SKILL.md create mode 100644 skills/ship-status/SKILL.md diff --git a/SKILLS_PLAN.md b/SKILLS_PLAN.md new file mode 100644 index 0000000..ded2b38 --- /dev/null +++ b/SKILLS_PLAN.md @@ -0,0 +1,135 @@ +# Ship Skills — Reimagining Ship as Claude Skills + +## The Idea + +Rather than a monolithic CLI that bakes in rigid assumptions, ship becomes a family of +narrow, composable Claude skills. Each skill knows how to do one thing well. Claude +provides the reasoning and orchestration. The server is the source of truth. + +Skills are completely generic — no hostnames, app names, or passwords baked in. The +same skills work for anyone. Share them with a friend, point them at a different VPS, +they just work. + +## Shared Configuration + +A single static file at `~/.config/ship/config.json` holds the VPS host (and little +else). All skills read from this file. No vault dependency — works for anyone. + +```json +{ + "host": "ubuntu@1.2.3.4", + "domain": "example.com" +} +``` + +The server itself is the source of truth for everything else — what services are +running, what ports are allocated, what Caddy configs exist. No local state file that +can go stale. + +## The Skills + +### `ship-setup` +One-time setup. Asks for VPS host if not configured, saves to `~/.config/ship/config.json`, +SSHes in and installs server dependencies (Caddy, directory structure, etc). +All other skills depend on this having been run once. + +### `ship-status` +Derives current state entirely from the server at runtime: +- Running apps → `systemctl list-units --type=service` +- Ports → `/etc/ship/ports/` or env files +- Domains → parse Caddy configs in `sites-enabled/` +- Static sites → list `/var/www/` + +No state file needed. Always accurate. Replaces the need for any local tracking. + +### `ship-env` +Read and write env vars with merge semantics. Never overwrites — reads existing file +first, merges new values on top, writes result. Old vars survive redeployments. + +### `ship-caddy` +Manage per-app Caddyfile config. Knows Caddy syntax. Diffs before writing. Validates +before reloading. Never regenerates from scratch — only touches what needs changing. + +### `ship-service` +Systemd management. Handles the difference between a new service (enable + start) and +an existing one (restart). Status, logs, restart, stop — all covered. + +### `ship-binary` +Upload and install a pre-built binary. SCP to `/tmp`, move to `/usr/local/bin/`, +chmod +x, set up work directory and service user. Calls `ship-service` and `ship-env` +to complete the deployment. + +### `ship-static` +Rsync a local dist folder to `/var/www/{name}` on the server. Calls `ship-caddy` to +configure serving. + +### `ship-deploy` +A runbook skill that orchestrates the others in the right order for a full deployment. +Not imperative code — just a checklist of steps with enough context for Claude to +reason about what to do. Adapts based on what the user tells it (binary vs static, +what env vars are needed, etc). + +## What the Server Knows + +All persistent state lives on the server in conventional locations: + +``` +/etc/caddy/sites-enabled/{name}.caddy # per-app Caddy config +/etc/ship/env/{name}.env # environment variables +/etc/ship/ports/{name} # allocated port number +/etc/systemd/system/{name}.service # systemd unit +/var/www/{name}/ # static site files +/var/lib/{name}/ # app work directory (binary, data) +/usr/local/bin/{name} # binary executable +``` + +## Why This Is Better Than the CLI + +- **Transparent** — Claude tells you what it's about to do before doing it +- **Flexible** — no rigid assumptions, Claude reasons about edge cases +- **Mergeable** — env files, Caddy configs never blindly overwritten +- **Debuggable** — if something goes wrong, just ask Claude to fix it +- **Shareable** — no app-specific knowledge baked in, works for anyone +- **No stale state** — server is always the source of truth + +## Per-App Notes (Optional) + +The server can't know things like "this app needs FOODTRACKER_PASSWORD on redeploy" +or "this app has SQLite at /var/lib/foodtracker/data/". That's documentation, not +state. Users can keep these as plain notes in whatever system they prefer — a vault, +a README, a comment in a script. The skills don't depend on it. + +## SQLite Backup + +Before swapping a binary, `ship-binary` checks `/var/lib/{name}/` for any `.db` files +and backs them up to `/var/lib/{name}/backups/{timestamp}.db` before proceeding. Silent +and automatic — you never lose data from a bad deploy. + +## Multi-Host Support + +Config supports multiple named hosts. One is marked as default. All skills use the +default unless told otherwise. + +```json +{ + "default": "prod", + "hosts": { + "prod": { + "host": "ubuntu@1.2.3.4", + "domain": "example.com" + }, + "staging": { + "host": "ubuntu@5.6.7.8", + "domain": "staging.example.com" + } + } +} +``` + +Usage is natural — "deploy foodtracker to staging" and Claude picks the right host. +`ship-setup` can be run multiple times to add new hosts. The default can be changed +at any time. + +## Out of Scope (For Now) + +- Health checks — skipping initially, can add later if needed 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 @@ +--- +name: ship-binary +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. +argument-hint: " [host-nickname]" +--- + +# ship-binary + +Upload a pre-built binary and deploy it as a systemd service with Caddy reverse proxy. + +## Read Config + +```bash +python3 -c " +import json, os +cfg = json.load(open(os.path.expanduser('~/.config/ship/config.json'))) +nick = '' +h = cfg['hosts'].get(nick, cfg['hosts'][cfg['default']]) +print(h['host']) +print(h['domain']) +" +``` + +## Inputs + +- **Binary path** — local path to the compiled binary +- **App name** — short lowercase name, becomes the service name and subdomain (e.g. `foodtracker`) +- **Domain** — defaults to `.` from config, ask if different +- **Env vars** — ask if there are any env vars to set (beyond PORT/SHIP_NAME/SHIP_URL) +- **Host** — use default unless specified + +## Steps + +### 1. Check if app already exists + +```bash +ssh "test -f /etc/ship/ports/ && echo exists || echo new" +``` + +This determines whether to allocate a new port or reuse the existing one. + +### 2. Backup SQLite databases (if app exists) + +Before touching anything, check for SQLite files in the app's data directory: + +```bash +ssh "find /var/lib// -name '*.db' 2>/dev/null" +``` + +If any `.db` files are found, back them up: + +```bash +ssh "sudo mkdir -p /var/lib//backups && sudo cp /var/lib//data/.db /var/lib//backups/-\$(date +%Y%m%d-%H%M%S).db" +``` + +Tell the user what was backed up before proceeding. + +### 3. Allocate or retrieve port + +**New app** — find the highest port in use and add 1: +```bash +ssh "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/; echo \$port'" +``` + +**Existing app** — reuse the existing port: +```bash +ssh "cat /etc/ship/ports/" +``` + +### 4. Upload binary + +```bash +scp :/tmp/ +ssh "sudo mv /tmp/ /usr/local/bin/ && sudo chmod +x /usr/local/bin/" +``` + +### 5. Create work directory and service user + +```bash +ssh "sudo mkdir -p /var/lib//data && sudo useradd -r -s /bin/false 2>/dev/null || true && sudo chown -R : /var/lib/" +``` + +### 6. Write env file + +Build the env file content merging ship-managed vars with any user-provided vars. +If the file already exists, read it first and merge (new values win, old values survive): + +```bash +ssh "sudo cat /etc/ship/env/.env 2>/dev/null" +``` + +Then write merged result: +```bash +ssh "sudo tee /etc/ship/env/.env > /dev/null << 'EOF' +PORT= +SHIP_NAME= +SHIP_URL=https:// + +EOF +sudo chmod 600 /etc/ship/env/.env" +``` + +### 7. Write systemd unit + +```bash +ssh "sudo tee /etc/systemd/system/.service > /dev/null << 'EOF' +[Unit] +Description= +After=network.target + +[Service] +Type=simple +User= +WorkingDirectory=/var/lib/ +EnvironmentFile=/etc/ship/env/.env +ExecStart=/usr/local/bin/ +Restart=always +RestartSec=5s +NoNewPrivileges=true +PrivateTmp=true + +[Install] +WantedBy=multi-user.target +EOF" +``` + +### 8. Start or restart service + +```bash +ssh "sudo systemctl daemon-reload" +``` + +**New app:** +```bash +ssh "sudo systemctl enable --now " +``` + +**Existing app:** +```bash +ssh "sudo systemctl restart " +``` + +### 9. Write Caddy config + +```bash +ssh "sudo tee /etc/caddy/sites-enabled/.caddy > /dev/null << 'EOF' + { + reverse_proxy 127.0.0.1: +} +EOF +sudo systemctl reload caddy" +``` + +### 10. Confirm + +Tell the user: +- App name, URL, and port +- Whether it was a new deploy or update +- Any SQLite backups made +- Any env vars set + +## Notes + +- Always back up SQLite before swapping the binary +- Always merge env vars — never replace the whole file +- Use `systemctl restart` for existing apps, `enable --now` for new ones +- The data directory `/var/lib//data/` persists across deploys +- If the user doesn't specify env vars, ask if they need any before deploying diff --git a/skills/ship-caddy/SKILL.md b/skills/ship-caddy/SKILL.md new file mode 100644 index 0000000..df79e39 --- /dev/null +++ b/skills/ship-caddy/SKILL.md @@ -0,0 +1,135 @@ +--- +name: ship-caddy +description: Manage Caddy configuration for a deployed app. Add, update, or remove site configs. Use when you need to change how Caddy serves an app — custom domains, redirects, auth, headers, etc. +argument-hint: " [host-nickname]" +--- + +# ship-caddy + +Manage per-app Caddy configuration on a ship VPS. + +## Read Config + +```bash +python3 -c " +import json, os +cfg = json.load(open(os.path.expanduser('~/.config/ship/config.json'))) +nick = '' +h = cfg['hosts'].get(nick, cfg['hosts'][cfg['default']]) +print(h['host']) +" +``` + +## Usage Patterns + +### View current Caddy config for an app + +```bash +ssh "sudo cat /etc/caddy/sites-enabled/.caddy" +``` + +### Add or update a site config + +Read the current config first if it exists, then write the new one. Always reload +Caddy after writing — validate first by checking syntax: + +```bash +ssh "sudo caddy validate --config /etc/caddy/Caddyfile 2>&1" +``` + +If validation passes: +```bash +ssh "sudo systemctl reload caddy" +``` + +If validation fails, show the error and do NOT reload. Tell the user what the +problem is. + +### Standard reverse proxy config (most apps) + +```bash +ssh "sudo tee /etc/caddy/sites-enabled/.caddy > /dev/null << 'EOF' + { + reverse_proxy 127.0.0.1: +} +EOF" +``` + +### Custom domain (in addition to default) + +```bash +ssh "sudo tee /etc/caddy/sites-enabled/.caddy > /dev/null << 'EOF' +, { + reverse_proxy 127.0.0.1: +} +EOF" +``` + +### Basic auth + +```bash +ssh "sudo tee /etc/caddy/sites-enabled/.caddy > /dev/null << 'EOF' + { + basicauth { + + } + reverse_proxy 127.0.0.1: +} +EOF" +``` + +To generate a bcrypt hash for a password: +```bash +ssh "caddy hash-password --plaintext ''" +``` + +### Redirect www to non-www + +```bash +ssh "sudo tee /etc/caddy/sites-enabled/.caddy > /dev/null << 'EOF' +www. { + redir https://{uri} permanent +} + + { + reverse_proxy 127.0.0.1: +} +EOF" +``` + +### Static site + +```bash +ssh "sudo tee /etc/caddy/sites-enabled/.caddy > /dev/null << 'EOF' + { + root * /var/www/ + file_server + encode gzip +} +EOF" +``` + +### Remove a site config + +```bash +ssh "sudo rm /etc/caddy/sites-enabled/.caddy && sudo systemctl reload caddy" +``` + +Confirm with the user before removing. + +### View Caddy status and logs + +```bash +ssh "sudo systemctl status caddy --no-pager" +ssh "sudo journalctl -u caddy -n 50 --no-pager" +``` + +## Notes + +- Always validate before reloading — never reload with a broken config +- The port for an app can be found at `/etc/ship/ports/` +- Caddy handles HTTPS automatically — no need to configure certificates +- If the user asks for something not covered here, write the appropriate Caddy + directives — Caddy's config language is flexible and well documented +- Main Caddyfile is at `/etc/caddy/Caddyfile` and imports all files in + `/etc/caddy/sites-enabled/` diff --git a/skills/ship-deploy/SKILL.md b/skills/ship-deploy/SKILL.md new file mode 100644 index 0000000..60cc263 --- /dev/null +++ b/skills/ship-deploy/SKILL.md @@ -0,0 +1,126 @@ +--- +name: ship-deploy +description: Deploy an app to a ship VPS. Orchestrates ship-binary or ship-static depending on what you're deploying. Use when you want to deploy or redeploy an app and want Claude to guide the process. +argument-hint: " [host-nickname]" +--- + +# ship-deploy + +Orchestration runbook for deploying apps to a ship VPS. Guides the full deployment +process by calling the appropriate ship skills in the right order. + +## Prerequisites + +- `/ship-setup` must have been run at least once +- `~/.config/ship/config.json` must exist and contain at least one host +- The app must already be built locally (binary compiled or dist folder ready) + +## Read Config + +```bash +python3 -c " +import json, os +cfg = json.load(open(os.path.expanduser('~/.config/ship/config.json'))) +nick = '' +h = cfg['hosts'].get(nick, cfg['hosts'][cfg['default']]) +print(h['host']) +print(h['domain']) +" +``` + +## Step 1 — Understand what we're deploying + +Ask the user (or infer from context): + +- **What is the app name?** (e.g. `foodtracker`) +- **What type?** Binary or static site? +- **Where is the artifact?** Path to binary or dist folder +- **What host?** Default unless specified +- **Any env vars needed?** Especially for first-time deploys +- **Custom domain?** Or use `.` + +If any of these are unclear, ask before proceeding. + +## Step 2 — Check if app already exists + +```bash +ssh "test -f /etc/ship/ports/ && echo exists || echo new" +``` + +Tell the user whether this is a fresh deploy or an update to an existing app. + +## Step 3 — Deploy + +### For a binary app → follow ship-binary + +Key steps in order: +1. Backup any SQLite databases in `/var/lib//` +2. Allocate or retrieve port +3. Upload binary via scp +4. Write/merge env file +5. Write systemd unit +6. `systemctl restart` (existing) or `systemctl enable --now` (new) +7. Write Caddy config and reload + +### For a static site → follow ship-static + +Key steps in order: +1. Validate `index.html` exists in dist folder +2. Rsync dist folder to `/var/www//` +3. Fix ownership to `www-data` +4. Write Caddy config and reload + +## Step 4 — Verify + +After deploying, confirm the service came up: + +**Binary:** +```bash +ssh "sudo systemctl is-active " +``` + +If not active, immediately check logs: +```bash +ssh "sudo journalctl -u -n 30 --no-pager" +``` + +**Static:** +```bash +ssh "curl -sI https:// | head -5" +``` + +## Step 5 — Confirm to user + +Report: +- App name and live URL +- Type (binary / static) +- New deploy or update +- Port (binary only) +- Any SQLite backups made +- Any env vars that were set +- Whether the service is running + +## Checklist (reference) + +Use this to make sure nothing is missed: + +- [ ] Config file read, host resolved +- [ ] App type confirmed (binary / static) +- [ ] Artifact path confirmed and exists locally +- [ ] App name and domain confirmed +- [ ] Existing app check done +- [ ] SQLite backed up (binary, if db files exist) +- [ ] Port allocated or retrieved +- [ ] Artifact uploaded +- [ ] Env file written with merge semantics +- [ ] Systemd unit written and service started/restarted (binary) +- [ ] Caddy config written and reloaded +- [ ] Service confirmed running + +## Notes + +- Never skip the SQLite backup step for binary apps — always check even if you don't expect a db +- Always merge env vars — never overwrite the whole env file +- If anything fails mid-deploy, tell the user exactly where it failed and what state the server is in +- Use `systemctl restart` for existing apps, `enable --now` for new ones — not the other way around +- If the user says "deploy X" without more context, ask the minimum necessary questions before starting diff --git a/skills/ship-env/SKILL.md b/skills/ship-env/SKILL.md new file mode 100644 index 0000000..692904d --- /dev/null +++ b/skills/ship-env/SKILL.md @@ -0,0 +1,84 @@ +--- +name: ship-env +description: Read or write environment variables for a deployed app. Always merges — never overwrites existing vars. Use when you need to set, update, or view env vars for an app without redeploying. +argument-hint: " [KEY=VALUE ...]" +--- + +# ship-env + +Read and write environment variables for a deployed app on a ship VPS. +Always merges new values into the existing file — existing vars are never lost. + +## Read Config + +Load the host from `~/.config/ship/config.json`: + +```bash +python3 -c " +import json, os +cfg = json.load(open(os.path.expanduser('~/.config/ship/config.json'))) +nick = '' +h = cfg['hosts'].get(nick, cfg['hosts'][cfg['default']]) +print(h['host']) +" +``` + +## Usage Patterns + +### View current env vars for an app + +```bash +ssh "sudo cat /etc/ship/env/.env" +``` + +Show them to the user clearly. Remind them that PORT, SHIP_NAME, and SHIP_URL are +managed by ship and shouldn't be manually changed. + +### Set or update one or more vars + +Given vars like `FOO=bar BAZ=qux`, merge them into the existing env file: + +```bash +# Read current env +ssh "sudo cat /etc/ship/env/.env 2>/dev/null" +``` + +Then write the merged result back: + +```bash +ssh "sudo tee /etc/ship/env/.env > /dev/null << 'EOF' +PORT=9013 +SHIP_NAME=myapp +SHIP_URL=https://myapp.example.com +FOO=bar +BAZ=qux +EOF" +``` + +**Merge rules:** +- Start with all existing vars +- Overwrite any keys that appear in the new set +- Add any new keys that didn't exist before +- Never remove existing keys unless explicitly asked + +### Delete a var + +Only delete a var if the user explicitly asks to remove it by name. Show them the +current value and confirm before removing. + +### Restart after changes + +After writing new env vars, ask the user if they want to restart the service to apply +them. If yes, use ship-service to restart: + +```bash +ssh "sudo systemctl restart " +``` + +## Notes + +- Env file lives at `/etc/ship/env/.env` +- PORT, SHIP_NAME, and SHIP_URL are written by ship-binary/ship-deploy — don't remove them +- If the env file doesn't exist, the app probably isn't deployed yet — say so +- Use the default host unless a host nickname was specified (e.g. "set FOO=bar on staging") +- After updating, remind the user the service needs a restart to pick up changes diff --git a/skills/ship-service/SKILL.md b/skills/ship-service/SKILL.md new file mode 100644 index 0000000..e4d7510 --- /dev/null +++ b/skills/ship-service/SKILL.md @@ -0,0 +1,133 @@ +--- +name: ship-service +description: Manage systemd services for deployed apps. Start, stop, restart, view status and logs. Use when you need to control a running service or diagnose problems. +argument-hint: " [start|stop|restart|status|logs] [host-nickname]" +--- + +# ship-service + +Manage systemd services for apps deployed on a ship VPS. + +## Read Config + +```bash +python3 -c " +import json, os +cfg = json.load(open(os.path.expanduser('~/.config/ship/config.json'))) +nick = '' +h = cfg['hosts'].get(nick, cfg['hosts'][cfg['default']]) +print(h['host']) +" +``` + +## Usage Patterns + +### Status + +```bash +ssh "sudo systemctl status --no-pager" +``` + +Shows whether the service is running, its PID, memory usage, and recent log lines. + +### Restart + +```bash +ssh "sudo systemctl restart " +``` + +Use after changing env vars or replacing the binary. + +### Stop + +```bash +ssh "sudo systemctl stop " +``` + +### Start + +```bash +ssh "sudo systemctl start " +``` + +### View logs + +Recent logs (last 50 lines): +```bash +ssh "sudo journalctl -u -n 50 --no-pager" +``` + +Follow live logs: +```bash +ssh "sudo journalctl -u -f" +``` + +Logs since last boot: +```bash +ssh "sudo journalctl -u -b --no-pager" +``` + +### Enable (start on boot) + +```bash +ssh "sudo systemctl enable " +``` + +### Disable (don't start on boot) + +```bash +ssh "sudo systemctl disable " +``` + +### View the systemd unit file + +```bash +ssh "sudo cat /etc/systemd/system/.service" +``` + +### Remove a service entirely + +Only do this if the user explicitly asks to remove/uninstall an app: + +```bash +ssh "sudo systemctl stop && sudo systemctl disable && sudo rm /etc/systemd/system/.service && sudo systemctl daemon-reload" +``` + +Confirm with the user before removing. Note that this does not delete the binary, +data directory, env file, or Caddy config — those are managed separately. + +## Diagnosing Problems + +If a service is failing, check: + +1. Service status for the error: +```bash +ssh "sudo systemctl status --no-pager" +``` + +2. Full logs for context: +```bash +ssh "sudo journalctl -u -n 100 --no-pager" +``` + +3. Whether the binary exists and is executable: +```bash +ssh "ls -la /usr/local/bin/" +``` + +4. Whether the env file exists and looks correct: +```bash +ssh "sudo cat /etc/ship/env/.env" +``` + +5. Whether the port is already in use by something else: +```bash +ssh "sudo ss -tlnp | grep " +``` + +## Notes + +- If the user just says "restart foodtracker" or "check the logs for myapp", infer the action +- After a restart, give it a moment then check status to confirm it came up +- If a service repeatedly crashes, look at the logs before suggesting a fix +- Use default host unless another is specified diff --git a/skills/ship-setup/SKILL.md b/skills/ship-setup/SKILL.md new file mode 100644 index 0000000..e3d08ee --- /dev/null +++ b/skills/ship-setup/SKILL.md @@ -0,0 +1,122 @@ +--- +name: ship-setup +description: Set up ship for the first time or add a new VPS host. Saves host config to ~/.config/ship/config.json and installs server dependencies. Use when configuring ship for the first time, or adding/changing a host. +argument-hint: "[host-nickname]" +--- + +# ship-setup + +Configure ship and prepare a VPS for deployments. + +## Config File + +All ship skills read from `~/.config/ship/config.json`. This skill creates or updates it. + +Structure: +```json +{ + "default": "prod", + "hosts": { + "prod": { + "host": "ubuntu@1.2.3.4", + "domain": "example.com" + }, + "staging": { + "host": "ubuntu@5.6.7.8", + "domain": "staging.example.com" + } + } +} +``` + +## Steps + +### 1. Read existing config + +Check if `~/.config/ship/config.json` exists: + +```bash +cat ~/.config/ship/config.json 2>/dev/null +``` + +If it exists, show the user the current hosts so they know what's already configured. + +### 2. Get host details + +If no nickname was provided as an argument, ask the user: +- **Nickname** — a short name for this host (e.g. `prod`, `staging`, `vps`) +- **SSH connection string** — e.g. `ubuntu@1.2.3.4` or an SSH config alias like `alaskav6` +- **Base domain** — the domain pointing to this server (e.g. `example.com`) + +If this is the first host, ask if it should be the default. If hosts already exist, ask if this should replace the current default. + +### 3. Test SSH connection + +Verify the connection works before saving anything: + +```bash +ssh -o ConnectTimeout=5 "echo ok" +``` + +If it fails, tell the user and stop. Don't save config for an unreachable host. + +### 4. Save config + +Write or update `~/.config/ship/config.json` with the new host. Merge with existing hosts — never overwrite the whole file. Use Python to safely read/write JSON: + +```bash +python3 -c " +import json, os +path = os.path.expanduser('~/.config/ship/config.json') +os.makedirs(os.path.dirname(path), exist_ok=True) +cfg = json.load(open(path)) if os.path.exists(path) else {'default': None, 'hosts': {}} +cfg['hosts'][''] = {'host': '', 'domain': ''} +if not cfg['default']: + cfg['default'] = '' +json.dump(cfg, open(path, 'w'), indent=2) +print('saved') +" +``` + +### 5. Install server dependencies + +SSH in and ensure the required directories and software exist. This is idempotent — safe to run multiple times. + +**Install Caddy** (if not present): +```bash +ssh "which caddy || (sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl && curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg && curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list && sudo apt-get update && sudo apt-get install -y caddy)" +``` + +**Create directory structure:** +```bash +ssh "sudo mkdir -p /etc/ship/env /etc/ship/ports /etc/caddy/sites-enabled /var/www && sudo chmod 755 /etc/ship" +``` + +**Configure main Caddyfile** (only if not already set up): +```bash +ssh "sudo test -f /etc/caddy/Caddyfile && echo exists || echo '{ +} + +import /etc/caddy/sites-enabled/*' | sudo tee /etc/caddy/Caddyfile" +``` + +**Enable and start Caddy:** +```bash +ssh "sudo systemctl enable caddy && sudo systemctl start caddy" +``` + +### 6. Confirm + +Tell the user: +- Host nickname and SSH target saved +- Whether it's the default host +- That the server is ready for deployments +- How to add another host: `/ship-setup ` +- How to deploy: `/ship-deploy` + +## Notes + +- Never overwrite the entire config file — always merge +- If a nickname already exists in config, confirm before overwriting it +- The SSH host can be an alias from `~/.ssh/config` — no need to require raw IP +- Default host is used by all other ship skills when no host is specified diff --git a/skills/ship-static/SKILL.md b/skills/ship-static/SKILL.md new file mode 100644 index 0000000..1ef74d3 --- /dev/null +++ b/skills/ship-static/SKILL.md @@ -0,0 +1,121 @@ +--- +name: ship-static +description: Deploy a static site to a ship VPS. Rsyncs a local dist folder to the server and configures Caddy to serve it. Use when deploying a built frontend, docs site, or any folder of static files. +argument-hint: " [host-nickname]" +--- + +# ship-static + +Deploy a static site by rsyncing a local directory to the server and configuring Caddy. + +## Read Config + +```bash +python3 -c " +import json, os +cfg = json.load(open(os.path.expanduser('~/.config/ship/config.json'))) +nick = '' +h = cfg['hosts'].get(nick, cfg['hosts'][cfg['default']]) +print(h['host']) +print(h['domain']) +" +``` + +## Inputs + +- **Dist path** — local directory containing the built static files (e.g. `./dist`, `./out`) +- **App name** — short lowercase name, becomes the subdomain (e.g. `mysite`) +- **Domain** — defaults to `.`, ask if different +- **Host** — use default unless specified + +## Steps + +### 1. Validate local path + +Check that the dist directory exists and contains an `index.html`: + +```bash +ls /index.html +``` + +If not found, tell the user — they may need to build first. + +### 2. Create remote directory + +```bash +ssh "sudo mkdir -p /var/www/ && sudo chown $USER:$USER /var/www/" +``` + +### 3. Sync files + +```bash +rsync -avz --delete / :/var/www// +``` + +The `--delete` flag removes files on the server that no longer exist locally, keeping +the deployment clean. Tell the user how many files were transferred. + +### 4. Fix ownership + +After rsync, ensure Caddy can read the files: + +```bash +ssh "sudo chown -R www-data:www-data /var/www/" +``` + +### 5. Write Caddy config + +Check if a config already exists: + +```bash +ssh "cat /etc/caddy/sites-enabled/.caddy 2>/dev/null" +``` + +Write (or overwrite) the config: + +```bash +ssh "sudo tee /etc/caddy/sites-enabled/.caddy > /dev/null << 'EOF' + { + root * /var/www/ + file_server + encode gzip +} +EOF" +``` + +### 6. Validate and reload Caddy + +```bash +ssh "sudo caddy validate --config /etc/caddy/Caddyfile 2>&1" +``` + +If valid: +```bash +ssh "sudo systemctl reload caddy" +``` + +If invalid, show the error and do not reload. + +### 7. Confirm + +Tell the user: +- URL the site is live at +- Number of files synced +- Whether this was a new deployment or an update + +## Notes + +- Build before deploying — this skill does not run build commands +- `--delete` in rsync means files removed locally will be removed from the server too +- If the user wants a custom domain, use ship-caddy to update the config after deploying +- For SPAs with client-side routing, the Caddy config may need a `try_files` directive: + ``` + { + root * /var/www/ + try_files {path} /index.html + file_server + encode gzip + } + ``` + Ask the user if their site uses client-side routing. +- Use default host unless another is specified diff --git a/skills/ship-status/SKILL.md b/skills/ship-status/SKILL.md new file mode 100644 index 0000000..f47e081 --- /dev/null +++ b/skills/ship-status/SKILL.md @@ -0,0 +1,85 @@ +--- +name: ship-status +description: Show what's running on a ship VPS. Derives state from the server — no local state file. Use when you want to know what apps are deployed, what ports they use, and whether they're running. +argument-hint: "[host-nickname]" +--- + +# ship-status + +Show the current state of all deployments on a ship VPS by reading directly from the server. + +## Read Config + +Load the host from `~/.config/ship/config.json`. Use the default host unless a nickname was specified: + +```bash +python3 -c " +import json, os, sys +cfg = json.load(open(os.path.expanduser('~/.config/ship/config.json'))) +nick = sys.argv[1] if len(sys.argv) > 1 else cfg['default'] +h = cfg['hosts'][nick] +print(h['host']) +print(h['domain']) +" +``` + +## Gather Server State + +Run all of these in a single SSH session to minimize round trips: + +**Systemd services (binary/docker apps):** +```bash +ssh "systemctl list-units --type=service --state=active --no-pager --no-legend | grep -v '@' | awk '{print \$1}' | xargs -I{} sh -c 'name=\$(echo {} | sed s/.service//); port=\$(cat /etc/ship/ports/\$name 2>/dev/null); env=\$(cat /etc/ship/env/\$name.env 2>/dev/null | grep SHIP_URL | cut -d= -f2); echo \"\$name|\$port|\$env\"' 2>/dev/null | grep '|'" +``` + +**Static sites:** +```bash +ssh "ls /var/www/ 2>/dev/null" +``` + +**Caddy configs (domains):** +```bash +ssh "ls /etc/caddy/sites-enabled/ 2>/dev/null | grep -v '^$'" +``` + +**Caddy status:** +```bash +ssh "systemctl is-active caddy" +``` + +**Disk usage for app data dirs:** +```bash +ssh "du -sh /var/lib/*/ 2>/dev/null" +``` + +## Present Results + +Format as a clean summary, for example: + +``` +HOST: prod (ubuntu@1.2.3.4) + +SERVICES + foodtracker running :9013 https://foodtracker.example.com + myapi running :9014 https://api.example.com + +STATIC SITES + mysite https://mysite.example.com + +CADDY: running + +DATA + /var/lib/foodtracker/ 48M + /var/lib/myapi/ 2M +``` + +If a service appears in `/etc/ship/ports/` but is not active in systemd, flag it as **stopped**. + +If a Caddy config exists for a name but no service or static site matches, flag it as **orphaned config**. + +## Notes + +- No local state file is consulted — everything comes from the server +- If `~/.config/ship/config.json` doesn't exist, tell the user to run `/ship-setup` first +- If a nickname is given that doesn't exist in config, list available nicknames +- Use default host if no argument given -- cgit v1.2.3