create-op-node 0.0.1 → 0.1.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
@@ -30,7 +30,201 @@ And at any time after that:
30
30
  npx create-op-node verify --domain your-domain.example
31
31
  ```
32
32
 
33
- Off-LAN health probe of a live node — TLS, GraphQL reachability, cosign signature check on the running images.
33
+ Off-LAN health probe of a live node, runnable from anywhere with internet
34
+ access. Five phases:
35
+
36
+ 1. **TLS handshake** to `api.<domain>:443` — surfaces cert subject, issuer,
37
+ and days-to-expiry. Warns when the cert is within `--cert-warn-days`
38
+ of expiring (default 14d). Negative expiries render as
39
+ `expired Nd ago`.
40
+ 2. **`GET https://api.<domain>/health`** must return 200.
41
+ 3. **`POST https://api.<domain>/api`** with `{ __typename }` must return a
42
+ valid GraphQL envelope (catches the "TLS green, but a misconfigured
43
+ proxy returns HTML" case).
44
+ 4. **Cloudflare Tunnel status** (optional) — looks up `connections` via
45
+ the CF API. Zero connectors registered → warning that cloudflared on
46
+ the Studio is offline. Requires all three of `--cf-token` (or
47
+ `--cf-token-file`), `--cf-account-id`, `--tunnel-id`; partial
48
+ configuration warns + names the missing flag.
49
+ 5. **`cosign verify`** (optional, repeatable `--image`) — keyless
50
+ verification against the GitHub Actions OIDC issuer + Fulcio +
51
+ the Rekor transparency log. Silently skipped when `cosign` isn't on
52
+ `PATH` (install with `brew install cosign` to enable).
53
+
54
+ No phase short-circuits the others — verify always runs the full pass so
55
+ the operator sees the whole landscape in one report. Exits non-zero only
56
+ when at least one phase failed; warnings are reported but don't fail the
57
+ run. Skipped phases are hidden by default; add `--show-skipped` to see them.
58
+
59
+ Full flag set:
60
+
61
+ ```bash
62
+ npx create-op-node verify \
63
+ --domain yournode.example.org \
64
+ --cf-token-file ~/.config/opuspopuli/cf-token \
65
+ --cf-account-id $CF_ACCOUNT_ID \
66
+ --tunnel-id $TUNNEL_ID \
67
+ --image ghcr.io/opuspopuli/api:latest \
68
+ --image ghcr.io/opuspopuli/users:latest \
69
+ --cert-warn-days 21
70
+ ```
71
+
72
+ `--cf-token-file` is preferred over `--cf-token` for cron / systemd
73
+ invocations — the latter ends up in `ps` output, the former doesn't.
74
+ Use `--api-host <host>` to override the default `api.<domain>`
75
+ construction when your node exposes the API at a different subdomain.
76
+
77
+ ## Bootstrapping a region config
78
+
79
+ A node serves data; **what** data it serves is defined by a declarative region
80
+ config in [`OpusPopuli/opuspopuli-regions`](https://github.com/OpusPopuli/opuspopuli-regions).
81
+ Hand-writing one of those JSON files against the schema is the same kind of
82
+ fiddly, error-prone step the rest of this CLI exists to remove — so there's a
83
+ subcommand for it. Run it from the root of your `opuspopuli-regions` checkout:
84
+
85
+ ```bash
86
+ npx create-op-node region
87
+ ```
88
+
89
+ The wizard walks you through level (state or county), names, the two-letter
90
+ state code, FIPS code, timezone, and at least one data source (URL, data type,
91
+ source type, content goal). It then:
92
+
93
+ 1. Derives the `regionId` and keeps `name === config.regionId` (the invariant
94
+ the regions repo enforces).
95
+ 2. Validates the generated file against the **vendored copy of
96
+ `region-plugin.schema.json`** (the canonical contract) using an ESM-native
97
+ JSON Schema validator, then layers on the cross-field rules the repo's
98
+ `pnpm test` adds in code: semver shape, FIPS length per level
99
+ (2 digits for state, 5 for county), county-id-prefixed-by-parent, no
100
+ duplicate data sources keyed by `(dataType, url)`. All checks run **before**
101
+ writing, so the file lands green instead of bouncing off CI.
102
+ 3. Writes it to the canonical path
103
+ (`regions/<state>/<state>.json` or
104
+ `regions/<state>/counties/<county>/<county>.json`).
105
+
106
+ > **Conventions you may not expect**
107
+ >
108
+ > - New configs are stamped at **version `0.1.0`** — the documented starting
109
+ > point in `opuspopuli-regions/CLAUDE.md`. Bump manually as the config
110
+ > matures (additions → minor, breaking changes → major).
111
+ > - `boundarySources` is **not** prompted for. It's optional per the schema, so
112
+ > the scaffolded file is valid without it — but if your region has TIGER /
113
+ > ArcGIS boundary coverage and you want PostGIS point-in-polygon district
114
+ > lookups, you'll need to add the block by hand after scaffolding (see
115
+ > `regions/california/california.json` for a worked example).
116
+ > - `civics_blocks` is **not** part of the region config schema. It's a
117
+ > per-region taxonomy that lives elsewhere in the platform — don't look for
118
+ > a prompt for it here.
119
+
120
+ Then it's just `pnpm test` + a PR. Non-interactive flags (`--level`, `--name`,
121
+ `--parent`, `--state-code`, `--fips`, `--timezone`, `--out-dir`, `--force`) are
122
+ available for scripting; run `create-op-node region --help` for the list.
123
+
124
+ ### What it looks like
125
+
126
+ ```text
127
+ ┌ create-op-node region
128
+
129
+ ◇ What level is this region?
130
+ │ County
131
+
132
+ ◇ County name?
133
+ │ Alameda
134
+
135
+ ◇ Parent state slug?
136
+ │ california
137
+
138
+ ◇ Display name?
139
+ │ Alameda County
140
+
141
+ ◇ One-line description of the data coverage?
142
+ │ Civic data for Alameda County, California
143
+
144
+ ◇ Two-letter state code?
145
+ │ CA
146
+
147
+ ◇ County FIPS (5 digits)?
148
+ │ 06001
149
+
150
+ ◇ IANA timezone?
151
+ │ America/Los_Angeles
152
+
153
+ ◇ Data source #1 — URL?
154
+ │ https://bos.acgov.org/
155
+
156
+ ◇ Data type?
157
+ │ meetings
158
+
159
+ ◇ Source type?
160
+ │ html_scrape
161
+
162
+ ◇ Content goal (what should the scraper extract)?
163
+ │ Fetch Board of Supervisors agendas, minutes, and votes
164
+
165
+ ◇ Category label (optional)?
166
+ │ Board of Supervisors
167
+
168
+ ◇ Add another data source?
169
+ │ No
170
+
171
+ ◆ regions/california/counties/alameda/alameda.json (preview) ──────────╮
172
+ │ { │
173
+ │ "name": "california-alameda", │
174
+ │ "displayName": "Alameda County", │
175
+ │ "description": "Civic data for Alameda County, California", │
176
+ │ "version": "0.1.0", │
177
+ │ "config": { │
178
+ │ "regionId": "california-alameda", │
179
+ │ "regionName": "Alameda County", │
180
+ │ "description": "Civic data for Alameda County, California", │
181
+ │ "timezone": "America/Los_Angeles", │
182
+ │ "stateCode": "CA", │
183
+ │ "fipsCode": "06001", │
184
+ │ "dataSources": [ │
185
+ │ { │
186
+ │ "url": "https://bos.acgov.org/", │
187
+ │ "dataType": "meetings", │
188
+ │ "sourceType": "html_scrape", │
189
+ │ "contentGoal": "Fetch Board of Supervisors agendas, ...", │
190
+ │ "category": "Board of Supervisors" │
191
+ │ } │
192
+ │ ] │
193
+ │ }, │
194
+ │ "parentRegionId": "california" │
195
+ │ } │
196
+ ├────────────────────────────────────────────────────────────────────────╯
197
+
198
+ ◇ Write regions/california/counties/alameda/alameda.json?
199
+ │ Yes
200
+
201
+ ◆ Done ────────────────────────────────────────────────────╮
202
+ │ ✓ Wrote regions/california/counties/alameda/alameda.json │
203
+ │ │
204
+ │ Next steps in your opuspopuli-regions checkout: │
205
+ │ pnpm test # schema + hierarchy │
206
+ │ pnpm test:connectivity # URL reachability │
207
+ │ git add … && git commit && open a PR │
208
+ ├──────────────────────────────────────────────────────────────╯
209
+
210
+ └ Region scaffolded: california-alameda
211
+ ```
212
+
213
+ ### Caveats
214
+
215
+ A couple of honest sharp edges, since this command lives in the *node* CLI
216
+ rather than in the regions repo itself:
217
+
218
+ - **Run it from a `opuspopuli-regions` checkout.** The file is written relative
219
+ to `--out-dir` (default: current directory) at the canonical `regions/…`
220
+ path. Run it anywhere else and the file lands in the wrong tree.
221
+ - **The validation here mirrors the regions schema; it does not import it.**
222
+ `create-op-node` can't see `region-plugin.schema.json` at runtime, so its
223
+ pre-write checks are a hand-maintained copy of the rules. They can drift if
224
+ the schema changes. The regions repo's own `pnpm test` is the source of
225
+ truth — **always run it after scaffolding**; treat a green run there, not a
226
+ green run here, as the real signal. If the two ever disagree, the schema
227
+ wins and this command needs updating.
34
228
 
35
229
  ## Why this exists
36
230
 
@@ -48,8 +242,9 @@ The CLI itself never holds any credentials beyond the scope of a single command
48
242
  │ │
49
243
  npx create-op-node ─┤ ┌─ init ──────► Cloudflare API │
50
244
  │ ├─ bootstrap GitHub API │
51
- └─ verify Terraform Cloud │
52
- `op` CLI (optional)│
245
+ ├─ verify Terraform Cloud │
246
+ └─ region `op` CLI (optional)│
247
+ │ (writes a regions repo config) │
53
248
  └──────────────────────────────────────┘
54
249
 
55
250
 
@@ -73,12 +268,18 @@ The CLI itself never holds any credentials beyond the scope of a single command
73
268
 
74
269
  ## Status
75
270
 
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.
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.
272
+
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)`.
274
+
275
+ **`verify` — scaffold stub.** Type-safe argument parsing only; prints a roadmap-style message and exits.
276
+
277
+ **`region`** — fully wired. Scaffolds schema-valid region configs for the `OpusPopuli/opuspopuli-regions` repo.
77
278
 
78
279
  ### Roadmap
79
280
 
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`.
281
+ - **v0.1.0** `init` end-to-end + `region` scaffolder.
282
+ - **v0.2.0** `bootstrap` fully wired on the Studio side.
82
283
  - **v0.3.0** — `verify` fully wired: TLS + GraphQL + cosign signature checks.
83
284
  - **v0.4.0** — Resend domain + DKIM automation, drift detection, automated backup-restore drill.
84
285
 
@@ -117,5 +318,6 @@ node dist/cli.js --help # test the built binary
117
318
  ## Related
118
319
 
119
320
  - [`OpusPopuli/opuspopuli-node`](https://github.com/OpusPopuli/opuspopuli-node) — the per-region deployment template this CLI creates from.
321
+ - [`OpusPopuli/opuspopuli-regions`](https://github.com/OpusPopuli/opuspopuli-regions) — declarative region configs; `create-op-node region` scaffolds one.
120
322
  - [`OpusPopuli/opuspopuli`](https://github.com/OpusPopuli/opuspopuli) — the central monorepo that builds + publishes `ghcr.io/opuspopuli/*` images.
121
323
  - [`OpusPopuli/prompt-service`](https://github.com/OpusPopuli/prompt-service) — private prompt-template service consumed by every node.