create-op-node 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,8 +13,8 @@ That's it. The wizard walks you through:
13
13
  2. **GitHub** — creates your region's node repo from the [`OpusPopuli/opuspopuli-node`](https://github.com/OpusPopuli/opuspopuli-node) template via the GitHub API. Seeds the 5 required GitHub Secrets (Cloudflare token, account ID, zone ID, Terraform Cloud token, TFC org) for you.
14
14
  3. **Terraform Cloud** — verifies your TFC token, prepares the workspace.
15
15
  4. **First PR** — writes `environments/prod.tfvars` from your answers, commits, opens the first PR. The node repo's `cloudflare-infra.yml` workflow runs `terraform plan` against the PR; on merge to `main` it applies — Tunnel, DNS, R2 buckets, and Pages project come up automatically.
16
- 5. **pgsodium master key** — generates a fresh 64-hex root key, stores it in 1Password (via the `op` CLI if available, otherwise prompts you to paste it once).
17
- 6. **Tunnel token retrieval** — after `terraform apply` lands, fetches the Tunnel token from Terraform Cloud outputs and stores it in 1Password alongside the pgsodium key.
16
+ 5. **pgsodium master key** — generates a fresh 64-hex root key, stores it in your **macOS login Keychain** as `org.opuspopuli.<region>/pgsodium-root-key`. No third-party password manager required.
17
+ 6. **Tunnel token retrieval** — after `terraform apply` lands, fetches the Tunnel token from Terraform Cloud outputs and stores it alongside the pgsodium key in your Keychain.
18
18
 
19
19
  Then on the Mac Studio itself:
20
20
 
@@ -22,9 +22,76 @@ Then on the Mac Studio itself:
22
22
  npx create-op-node bootstrap
23
23
  ```
24
24
 
25
- Configures macOS power settings, installs Homebrew + the CLI tool list, sets up Docker Desktop + Tailscale + Ollama, clones the node repo you created, materializes the pgsodium key from 1Password, writes the LaunchAgent plist, logs into ghcr.io, pulls + warms the LLM model, and finally `docker compose pull && up -d` brings the whole stack online. Health-check loop waits until all 10 containers are `(healthy)`.
25
+ Configures macOS power settings, installs Homebrew + the CLI tool list, sets up Docker Desktop + Tailscale + Ollama, clones the node repo you created, reads the pgsodium key + Tunnel token from the Studio's Keychain (or prompts you to paste them once, then persists for re-runs), writes the LaunchAgent plist, logs into ghcr.io, pulls + warms the LLM model, and finally `docker compose pull && up -d` brings the whole stack online. Health-check loop waits until all 10 containers are `(healthy)`.
26
26
 
27
- And at any time after that:
27
+ > **Secret transport between laptop and Studio**
28
+ >
29
+ > The macOS `security` CLI writes to the local login keychain — items
30
+ > don't sync to iCloud Keychain automatically. On the Studio's first
31
+ > bootstrap, the operator pastes the pgsodium key + Tunnel token once
32
+ > (from the laptop's Keychain Access, or wherever you copied them); the
33
+ > Studio bootstrap validates the format and persists locally so re-runs
34
+ > read straight through. Use AirDrop / `security find-generic-password`
35
+ > output / Tailscale `scp` to ferry the values.
36
+
37
+ ## Resetting the Studio
38
+
39
+ To start over (e.g. before rerunning `bootstrap` against a different
40
+ region, or after a misconfiguration), reverse the Studio-side state:
41
+
42
+ ```bash
43
+ npx create-op-node reset --region us-ca
44
+ ```
45
+
46
+ Three phases run in reverse-bootstrap order. By default volumes are
47
+ preserved — the database survives so you can bring the stack back up
48
+ with `bootstrap` without losing data.
49
+
50
+ 1. **Stop the stack** — `docker compose down`. Pass `--wipe-data` to
51
+ add `-v` (destroys named volumes including the database). The wipe
52
+ mode requires retyping the region label as confirmation — and the
53
+ prompt deliberately doesn't pre-fill the answer, so you have to type
54
+ it from memory. `y` won't do it.
55
+ 2. **Unload + remove the LaunchAgent** — `launchctl unload` then `rm`
56
+ the plist and the pgsodium key file. `--keep-key-file` leaves the
57
+ key in place as a belt-and-suspenders backup before a wipe-data run.
58
+ 3. **`docker logout`** — clears the registry-credentials store entry
59
+ for `ghcr.io` (or override with `--registry`). This only clears the
60
+ store entry; if your credential helper caches the token elsewhere
61
+ (or `~/.docker/config.json` has stale entries from another host),
62
+ those need separate cleanup.
63
+
64
+ Reset does **not** touch cloud-side state: the Cloudflare resources,
65
+ the GitHub repo, and the TFC workspace remain. Keychain items on the
66
+ Studio are also left in place — `security delete-generic-password -s
67
+ org.opuspopuli.<region> -a pgsodium-root-key` etc. if you want them
68
+ gone.
69
+ `init` is idempotent against existing cloud setup, so re-running it
70
+ won't duplicate anything.
71
+
72
+ Useful flags:
73
+
74
+ - `--dry-run` — print the plan without acting. Phases that would run
75
+ show with a `?` icon; phases that are skipped show with `·`.
76
+ - `--skip-stack` / `--skip-launch-agent` / `--skip-docker-logout` —
77
+ surgical resets when only one piece needs cleaning.
78
+ - `--no-remove-orphans` — drop `--remove-orphans` from `compose down`.
79
+ Useful when you ran bootstrap with a custom `--compose-file` set and
80
+ reset without it.
81
+ - `--repo-dir <path>` — explicit path to the cloned node repo when
82
+ reset is run from outside the checkout. Passing a path that doesn't
83
+ look like a node repo is a hard error, not a silent skip.
84
+ - `--registry <reg>` — log out of a registry other than `ghcr.io`.
85
+
86
+ ```bash
87
+ # Try-before-you-buy: preview every step.
88
+ npx create-op-node reset --region us-ca --dry-run
89
+
90
+ # Nuke from orbit: containers + volumes + LaunchAgent + ghcr credentials.
91
+ npx create-op-node reset --region us-ca --wipe-data
92
+ ```
93
+
94
+ ## Verifying a live node
28
95
 
29
96
  ```bash
30
97
  npx create-op-node verify --domain your-domain.example
@@ -226,13 +293,63 @@ rather than in the regions repo itself:
226
293
  green run here, as the real signal. If the two ever disagree, the schema
227
294
  wins and this command needs updating.
228
295
 
296
+ ## What lands where
297
+
298
+ `create-op-node` touches several secret stores. Here's the full map of
299
+ what we own (the two macOS Keychain items) vs. what we just route to its
300
+ destination.
301
+
302
+ ### Stored by `create-op-node` in macOS Keychain
303
+
304
+ Two items per region, both generic-password class
305
+ (`kSecClassGenericPassword`). Visible in **Keychain Access.app**
306
+ (`/System/Applications/Utilities/Keychain Access.app`) — **not** in the
307
+ new Passwords.app, which is filtered to website-login items only.
308
+
309
+ | # | Service | Account | Label (GUI display) | Value format | Written by | Read by |
310
+ |---|---|---|---|---|---|---|
311
+ | 1 | `org.opuspopuli.<region>` | `pgsodium-root-key` | `Opus Populi (<region>) — pgsodium root key` | 64 lowercase hex chars | `init` on laptop | `bootstrap` on Studio |
312
+ | 2 | `org.opuspopuli.<region>` | `tunnel-token` | `Opus Populi (<region>) — Cloudflare Tunnel token` | JWT-style base64url string | `init` on laptop (after TFC apply) | `bootstrap` on Studio |
313
+
314
+ Both items also carry `-D 'Opus Populi secret'` (the "Kind" column in
315
+ Keychain Access) so you can filter for them at a glance.
316
+
317
+ Inspect from a shell:
318
+
319
+ ```bash
320
+ # Metadata only (safe to share output):
321
+ security find-generic-password -s org.opuspopuli.us-ca -a pgsodium-root-key
322
+
323
+ # Reveal the value (you'll be prompted to allow access on first call):
324
+ security find-generic-password -s org.opuspopuli.us-ca -a pgsodium-root-key -w
325
+ ```
326
+
327
+ ### Stored elsewhere (we don't put these in Keychain)
328
+
329
+ Everything else flows through transiently or lives in its destination
330
+ system's own credential store.
331
+
332
+ | Secret | Where it lives | Why not in Keychain |
333
+ |---|---|---|
334
+ | Cloudflare API token | Pasted into `init` prompt → forwarded to **GitHub Secrets** + **Terraform Cloud** vars | One-shot during init. Re-runs prompt again. We could store it; adds risk vs benefit. |
335
+ | Cloudflare account ID, zone ID | Same as above | Not really a "secret" but flow alongside the token |
336
+ | Terraform Cloud token | Pasted, used to verify + poll runs | Same one-shot pattern |
337
+ | GitHub PAT | Read from `gh auth token` if available, else pasted | `gh` already manages it |
338
+ | pgsodium key (Studio runtime form) | `~/.config/opuspopuli/pgsodium_root_key` (mode `0400`) | LaunchAgent reads it at every login → interpolates into the `PGSODIUM_ROOT_KEY` env var. Same value as in Keychain; file is runtime form. |
339
+ | Cloudflare Tunnel token (Studio runtime form) | Baked into `~/Library/LaunchAgents/org.opuspopuli.envloader.plist` (mode `0600`) | launchd's `launchctl setenv TUNNEL_TOKEN` injects it into the session at every boot. Same value as in Keychain; plist is runtime form. |
340
+ | ghcr.io credentials | `~/.docker/config.json` or `docker-credential-osxkeychain` | Docker manages its own credential store — it actually saves the ghcr token to a separate Keychain item under service `ghcr.io`. We just call `docker login`. |
341
+
342
+ Per region, the **only** persistent secrets `create-op-node` owns are
343
+ the two Keychain items above. Everything else is either transient
344
+ (prompted, used, forgotten) or lives in its destination system.
345
+
229
346
  ## Why this exists
230
347
 
231
348
  Each Opus Populi region is operated independently by a local maintainer — its own Cloudflare account, its own Mac Studio, its own domain. The full bootstrap is a few hours of manual steps across Cloudflare, GitHub, Terraform Cloud, macOS Setup Assistant, Docker Desktop, Tailscale, Ollama, and the node's own Docker Compose stack. Doable from the runbook, but error-prone.
232
349
 
233
350
  This CLI exists to make that bootstrap **foolproof** — every prompt validates immediately, every secret is retrieved from a secure source (never echoed, never written to disk in plaintext), and every step has an explicit "what happens next" message. The goal is zero documentation reading required to get a node running.
234
351
 
235
- The CLI itself never holds any credentials beyond the scope of a single command — secrets flow from your 1Password vault → through the CLI → directly into the destination (GitHub Secrets, Terraform Cloud workspace variables, Mac Studio LaunchAgent). Nothing persists in this process.
352
+ The CLI itself never holds any credentials beyond the scope of a single command — secrets flow from your macOS Keychain → through the CLI → directly into the destination (GitHub Secrets, Terraform Cloud workspace variables, Mac Studio LaunchAgent). Nothing persists in this process.
236
353
 
237
354
  ## Architecture
238
355
 
@@ -268,9 +385,9 @@ The CLI itself never holds any credentials beyond the scope of a single command
268
385
 
269
386
  ## Status
270
387
 
271
- **`init` — fully wired.** Full Phase 1 of the runbook: prompts → Cloudflare 5-scope probe → Terraform Cloud verify → GitHub template clone → 5 repo secrets seeded → branch + prod.tfvars committed → PR opened → pgsodium key generated → (after operator merges PR) Terraform apply polled → Tunnel token retrieved + saved to 1Password.
388
+ **`init` — fully wired.** Full Phase 1 of the runbook: prompts → Cloudflare 5-scope probe → Terraform Cloud verify → GitHub template clone → 5 repo secrets seeded → branch + prod.tfvars committed → PR opened → pgsodium key generated → (after operator merges PR) Terraform apply polled → Tunnel token retrieved + saved to the macOS Keychain.
272
389
 
273
- **`bootstrap` — fully wired.** Phase 2 on the Mac Studio: macOS sanity (auto-restart, disk sleep), Homebrew + tool installs (gh, pnpm, jq, cloudflared, rclone, ollama, docker, tailscale), GitHub + Tailscale signin prompts, pgsodium key + Tunnel token read from 1Password, LaunchAgent written + loaded, ghcr.io login, Ollama models pulled + warmed, region repo located or cloned, `docker compose pull && up -d`, health-check loop until everything reports `(healthy)`.
390
+ **`bootstrap` — fully wired.** Phase 2 on the Mac Studio: macOS sanity (auto-restart, disk sleep), Homebrew + tool installs (gh, pnpm, jq, cloudflared, rclone, ollama, docker, tailscale), GitHub + Tailscale signin prompts, pgsodium key + Tunnel token read from the Studio's Keychain (or pasted in once if first run on that machine, then persisted), LaunchAgent written + loaded, ghcr.io login, Ollama models pulled + warmed, region repo located or cloned, `docker compose pull && up -d`, health-check loop until everything reports `(healthy)`.
274
391
 
275
392
  **`verify` — scaffold stub.** Type-safe argument parsing only; prints a roadmap-style message and exits.
276
393