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 +263 -7
- package/dist/cli.js +3581 -138
- package/dist/cli.js.map +1 -1
- package/package.json +2 -1
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
|
-
|
|
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
|
|
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
|
-
│
|
|
52
|
-
│
|
|
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
|
-
|
|
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**
|
|
81
|
-
- **v0.2.0**
|
|
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.
|