create-op-node 0.0.1 → 0.2.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
@@ -24,13 +24,261 @@ npx create-op-node bootstrap
24
24
 
25
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)`.
26
26
 
27
- And at any time after that:
27
+ ## Resetting the Studio
28
+
29
+ To start over (e.g. before rerunning `bootstrap` against a different
30
+ region, or after a misconfiguration), reverse the Studio-side state:
31
+
32
+ ```bash
33
+ npx create-op-node reset --region us-ca
34
+ ```
35
+
36
+ Three phases run in reverse-bootstrap order. By default volumes are
37
+ preserved — the database survives so you can bring the stack back up
38
+ with `bootstrap` without losing data.
39
+
40
+ 1. **Stop the stack** — `docker compose down`. Pass `--wipe-data` to
41
+ add `-v` (destroys named volumes including the database). The wipe
42
+ mode requires retyping the region label as confirmation — and the
43
+ prompt deliberately doesn't pre-fill the answer, so you have to type
44
+ it from memory. `y` won't do it.
45
+ 2. **Unload + remove the LaunchAgent** — `launchctl unload` then `rm`
46
+ the plist and the pgsodium key file. `--keep-key-file` leaves the
47
+ key in place as a belt-and-suspenders backup before a wipe-data run.
48
+ 3. **`docker logout`** — clears the registry-credentials store entry
49
+ for `ghcr.io` (or override with `--registry`). This only clears the
50
+ store entry; if your credential helper caches the token elsewhere
51
+ (or `~/.docker/config.json` has stale entries from another host),
52
+ those need separate cleanup.
53
+
54
+ Reset does **not** touch cloud-side state: the Cloudflare resources,
55
+ the GitHub repo, the TFC workspace, and the 1Password items remain.
56
+ `init` is idempotent against existing cloud setup, so re-running it
57
+ won't duplicate anything.
58
+
59
+ Useful flags:
60
+
61
+ - `--dry-run` — print the plan without acting. Phases that would run
62
+ show with a `?` icon; phases that are skipped show with `·`.
63
+ - `--skip-stack` / `--skip-launch-agent` / `--skip-docker-logout` —
64
+ surgical resets when only one piece needs cleaning.
65
+ - `--no-remove-orphans` — drop `--remove-orphans` from `compose down`.
66
+ Useful when you ran bootstrap with a custom `--compose-file` set and
67
+ reset without it.
68
+ - `--repo-dir <path>` — explicit path to the cloned node repo when
69
+ reset is run from outside the checkout. Passing a path that doesn't
70
+ look like a node repo is a hard error, not a silent skip.
71
+ - `--registry <reg>` — log out of a registry other than `ghcr.io`.
72
+
73
+ ```bash
74
+ # Try-before-you-buy: preview every step.
75
+ npx create-op-node reset --region us-ca --dry-run
76
+
77
+ # Nuke from orbit: containers + volumes + LaunchAgent + ghcr credentials.
78
+ npx create-op-node reset --region us-ca --wipe-data
79
+ ```
80
+
81
+ ## Verifying a live node
28
82
 
29
83
  ```bash
30
84
  npx create-op-node verify --domain your-domain.example
31
85
  ```
32
86
 
33
- Off-LAN health probe of a live node — TLS, GraphQL reachability, cosign signature check on the running images.
87
+ Off-LAN health probe of a live node, runnable from anywhere with internet
88
+ access. Five phases:
89
+
90
+ 1. **TLS handshake** to `api.<domain>:443` — surfaces cert subject, issuer,
91
+ and days-to-expiry. Warns when the cert is within `--cert-warn-days`
92
+ of expiring (default 14d). Negative expiries render as
93
+ `expired Nd ago`.
94
+ 2. **`GET https://api.<domain>/health`** must return 200.
95
+ 3. **`POST https://api.<domain>/api`** with `{ __typename }` must return a
96
+ valid GraphQL envelope (catches the "TLS green, but a misconfigured
97
+ proxy returns HTML" case).
98
+ 4. **Cloudflare Tunnel status** (optional) — looks up `connections` via
99
+ the CF API. Zero connectors registered → warning that cloudflared on
100
+ the Studio is offline. Requires all three of `--cf-token` (or
101
+ `--cf-token-file`), `--cf-account-id`, `--tunnel-id`; partial
102
+ configuration warns + names the missing flag.
103
+ 5. **`cosign verify`** (optional, repeatable `--image`) — keyless
104
+ verification against the GitHub Actions OIDC issuer + Fulcio +
105
+ the Rekor transparency log. Silently skipped when `cosign` isn't on
106
+ `PATH` (install with `brew install cosign` to enable).
107
+
108
+ No phase short-circuits the others — verify always runs the full pass so
109
+ the operator sees the whole landscape in one report. Exits non-zero only
110
+ when at least one phase failed; warnings are reported but don't fail the
111
+ run. Skipped phases are hidden by default; add `--show-skipped` to see them.
112
+
113
+ Full flag set:
114
+
115
+ ```bash
116
+ npx create-op-node verify \
117
+ --domain yournode.example.org \
118
+ --cf-token-file ~/.config/opuspopuli/cf-token \
119
+ --cf-account-id $CF_ACCOUNT_ID \
120
+ --tunnel-id $TUNNEL_ID \
121
+ --image ghcr.io/opuspopuli/api:latest \
122
+ --image ghcr.io/opuspopuli/users:latest \
123
+ --cert-warn-days 21
124
+ ```
125
+
126
+ `--cf-token-file` is preferred over `--cf-token` for cron / systemd
127
+ invocations — the latter ends up in `ps` output, the former doesn't.
128
+ Use `--api-host <host>` to override the default `api.<domain>`
129
+ construction when your node exposes the API at a different subdomain.
130
+
131
+ ## Bootstrapping a region config
132
+
133
+ A node serves data; **what** data it serves is defined by a declarative region
134
+ config in [`OpusPopuli/opuspopuli-regions`](https://github.com/OpusPopuli/opuspopuli-regions).
135
+ Hand-writing one of those JSON files against the schema is the same kind of
136
+ fiddly, error-prone step the rest of this CLI exists to remove — so there's a
137
+ subcommand for it. Run it from the root of your `opuspopuli-regions` checkout:
138
+
139
+ ```bash
140
+ npx create-op-node region
141
+ ```
142
+
143
+ The wizard walks you through level (state or county), names, the two-letter
144
+ state code, FIPS code, timezone, and at least one data source (URL, data type,
145
+ source type, content goal). It then:
146
+
147
+ 1. Derives the `regionId` and keeps `name === config.regionId` (the invariant
148
+ the regions repo enforces).
149
+ 2. Validates the generated file against the **vendored copy of
150
+ `region-plugin.schema.json`** (the canonical contract) using an ESM-native
151
+ JSON Schema validator, then layers on the cross-field rules the repo's
152
+ `pnpm test` adds in code: semver shape, FIPS length per level
153
+ (2 digits for state, 5 for county), county-id-prefixed-by-parent, no
154
+ duplicate data sources keyed by `(dataType, url)`. All checks run **before**
155
+ writing, so the file lands green instead of bouncing off CI.
156
+ 3. Writes it to the canonical path
157
+ (`regions/<state>/<state>.json` or
158
+ `regions/<state>/counties/<county>/<county>.json`).
159
+
160
+ > **Conventions you may not expect**
161
+ >
162
+ > - New configs are stamped at **version `0.1.0`** — the documented starting
163
+ > point in `opuspopuli-regions/CLAUDE.md`. Bump manually as the config
164
+ > matures (additions → minor, breaking changes → major).
165
+ > - `boundarySources` is **not** prompted for. It's optional per the schema, so
166
+ > the scaffolded file is valid without it — but if your region has TIGER /
167
+ > ArcGIS boundary coverage and you want PostGIS point-in-polygon district
168
+ > lookups, you'll need to add the block by hand after scaffolding (see
169
+ > `regions/california/california.json` for a worked example).
170
+ > - `civics_blocks` is **not** part of the region config schema. It's a
171
+ > per-region taxonomy that lives elsewhere in the platform — don't look for
172
+ > a prompt for it here.
173
+
174
+ Then it's just `pnpm test` + a PR. Non-interactive flags (`--level`, `--name`,
175
+ `--parent`, `--state-code`, `--fips`, `--timezone`, `--out-dir`, `--force`) are
176
+ available for scripting; run `create-op-node region --help` for the list.
177
+
178
+ ### What it looks like
179
+
180
+ ```text
181
+ ┌ create-op-node region
182
+
183
+ ◇ What level is this region?
184
+ │ County
185
+
186
+ ◇ County name?
187
+ │ Alameda
188
+
189
+ ◇ Parent state slug?
190
+ │ california
191
+
192
+ ◇ Display name?
193
+ │ Alameda County
194
+
195
+ ◇ One-line description of the data coverage?
196
+ │ Civic data for Alameda County, California
197
+
198
+ ◇ Two-letter state code?
199
+ │ CA
200
+
201
+ ◇ County FIPS (5 digits)?
202
+ │ 06001
203
+
204
+ ◇ IANA timezone?
205
+ │ America/Los_Angeles
206
+
207
+ ◇ Data source #1 — URL?
208
+ │ https://bos.acgov.org/
209
+
210
+ ◇ Data type?
211
+ │ meetings
212
+
213
+ ◇ Source type?
214
+ │ html_scrape
215
+
216
+ ◇ Content goal (what should the scraper extract)?
217
+ │ Fetch Board of Supervisors agendas, minutes, and votes
218
+
219
+ ◇ Category label (optional)?
220
+ │ Board of Supervisors
221
+
222
+ ◇ Add another data source?
223
+ │ No
224
+
225
+ ◆ regions/california/counties/alameda/alameda.json (preview) ──────────╮
226
+ │ { │
227
+ │ "name": "california-alameda", │
228
+ │ "displayName": "Alameda County", │
229
+ │ "description": "Civic data for Alameda County, California", │
230
+ │ "version": "0.1.0", │
231
+ │ "config": { │
232
+ │ "regionId": "california-alameda", │
233
+ │ "regionName": "Alameda County", │
234
+ │ "description": "Civic data for Alameda County, California", │
235
+ │ "timezone": "America/Los_Angeles", │
236
+ │ "stateCode": "CA", │
237
+ │ "fipsCode": "06001", │
238
+ │ "dataSources": [ │
239
+ │ { │
240
+ │ "url": "https://bos.acgov.org/", │
241
+ │ "dataType": "meetings", │
242
+ │ "sourceType": "html_scrape", │
243
+ │ "contentGoal": "Fetch Board of Supervisors agendas, ...", │
244
+ │ "category": "Board of Supervisors" │
245
+ │ } │
246
+ │ ] │
247
+ │ }, │
248
+ │ "parentRegionId": "california" │
249
+ │ } │
250
+ ├────────────────────────────────────────────────────────────────────────╯
251
+
252
+ ◇ Write regions/california/counties/alameda/alameda.json?
253
+ │ Yes
254
+
255
+ ◆ Done ────────────────────────────────────────────────────╮
256
+ │ ✓ Wrote regions/california/counties/alameda/alameda.json │
257
+ │ │
258
+ │ Next steps in your opuspopuli-regions checkout: │
259
+ │ pnpm test # schema + hierarchy │
260
+ │ pnpm test:connectivity # URL reachability │
261
+ │ git add … && git commit && open a PR │
262
+ ├──────────────────────────────────────────────────────────────╯
263
+
264
+ └ Region scaffolded: california-alameda
265
+ ```
266
+
267
+ ### Caveats
268
+
269
+ A couple of honest sharp edges, since this command lives in the *node* CLI
270
+ rather than in the regions repo itself:
271
+
272
+ - **Run it from a `opuspopuli-regions` checkout.** The file is written relative
273
+ to `--out-dir` (default: current directory) at the canonical `regions/…`
274
+ path. Run it anywhere else and the file lands in the wrong tree.
275
+ - **The validation here mirrors the regions schema; it does not import it.**
276
+ `create-op-node` can't see `region-plugin.schema.json` at runtime, so its
277
+ pre-write checks are a hand-maintained copy of the rules. They can drift if
278
+ the schema changes. The regions repo's own `pnpm test` is the source of
279
+ truth — **always run it after scaffolding**; treat a green run there, not a
280
+ green run here, as the real signal. If the two ever disagree, the schema
281
+ wins and this command needs updating.
34
282
 
35
283
  ## Why this exists
36
284
 
@@ -48,8 +296,9 @@ The CLI itself never holds any credentials beyond the scope of a single command
48
296
  │ │
49
297
  npx create-op-node ─┤ ┌─ init ──────► Cloudflare API │
50
298
  │ ├─ bootstrap GitHub API │
51
- └─ verify Terraform Cloud │
52
- `op` CLI (optional)│
299
+ ├─ verify Terraform Cloud │
300
+ └─ region `op` CLI (optional)│
301
+ │ (writes a regions repo config) │
53
302
  └──────────────────────────────────────┘
54
303
 
55
304
 
@@ -73,12 +322,18 @@ The CLI itself never holds any credentials beyond the scope of a single command
73
322
 
74
323
  ## Status
75
324
 
76
- **v0.0.1scaffold only.** The 3 subcommands exist with type-safe argument parsing and the Cloudflare 5-scope probe. The interactive prompts collect inputs but stop before doing destructive work. Iterating in the open against the OpusPopuli/opuspopuli-node template as it stabilizes.
325
+ **`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.
326
+
327
+ **`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)`.
328
+
329
+ **`verify` — scaffold stub.** Type-safe argument parsing only; prints a roadmap-style message and exits.
330
+
331
+ **`region`** — fully wired. Scaffolds schema-valid region configs for the `OpusPopuli/opuspopuli-regions` repo.
77
332
 
78
333
  ### Roadmap
79
334
 
80
- - **v0.1.0** `init` fully wired: GitHub template clone, secret seeding, prod.tfvars generation, pgsodium key flow, first PR, Tunnel token retrieval.
81
- - **v0.2.0** `bootstrap` fully wired: macOS config, brew installs, Docker Desktop setup, Tailscale, repo clone, LaunchAgent, ghcr.io login, Ollama, `docker compose up`.
335
+ - **v0.1.0** `init` end-to-end + `region` scaffolder.
336
+ - **v0.2.0** `bootstrap` fully wired on the Studio side.
82
337
  - **v0.3.0** — `verify` fully wired: TLS + GraphQL + cosign signature checks.
83
338
  - **v0.4.0** — Resend domain + DKIM automation, drift detection, automated backup-restore drill.
84
339
 
@@ -117,5 +372,6 @@ node dist/cli.js --help # test the built binary
117
372
  ## Related
118
373
 
119
374
  - [`OpusPopuli/opuspopuli-node`](https://github.com/OpusPopuli/opuspopuli-node) — the per-region deployment template this CLI creates from.
375
+ - [`OpusPopuli/opuspopuli-regions`](https://github.com/OpusPopuli/opuspopuli-regions) — declarative region configs; `create-op-node region` scaffolds one.
120
376
  - [`OpusPopuli/opuspopuli`](https://github.com/OpusPopuli/opuspopuli) — the central monorepo that builds + publishes `ghcr.io/opuspopuli/*` images.
121
377
  - [`OpusPopuli/prompt-service`](https://github.com/OpusPopuli/prompt-service) — private prompt-template service consumed by every node.