cursedops 0.8.0 → 0.9.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
@@ -12,12 +12,13 @@ bun add cursedops
12
12
  | `cursedops/paths` | the generation's whole-tree laws (`check-paths`, `check-doc-citations`, plus any `--also`) run over ONE repo, from a checkout or a worktree — and its `forge-paths` bin (0.5.0) |
13
13
  | `cursedops/launchd` | installing, replacing and removing a macOS launchd user agent, and the port a LIVE job serves on (`livePort`, 0.5.0) |
14
14
  | `cursedops/smoke` | the scaffolding of a deployed smoke — the ledger, the fetch, the DNS hint, the exit code — the one check no app owns (every address of a deployment serving the same built client), and since 0.5.0 its VERDICTS: the origin asked on loopback, the smoke's own environment, a network that lies about DNS, and a version that has settled |
15
- | `cursedops/worker-deploy` | the sequence that ships a Cloudflare Worker — clean tree, stage first, schema, `--var` stamp, secrets, smoke — and the readers it needs (0.5.0) |
15
+ | `cursedops/worker-deploy` | the sequence that ships a Cloudflare Worker — clean tree, stage first, schema, `--var` stamp, secrets, smoke — and the readers it needs (0.5.0); `readWranglerFile(root)` since 0.9.0, lifted from four apps' `readWrangler` (task 2153) |
16
16
  | `cursedops/worker-deploy` (CPU) | a `wrangler tail` around the traffic a deploy already sends, each request's BILLED CPU charged to its declared route, red on an undeclared, over-budget or uncapped route — and `runWorkerDeploy`'s `stageWalk` + `cpuTail` that wrap the walk and the smoke in it (0.7.0, task 2134, lifted from collections) |
17
17
  | `cursedops/worker-secrets` | a Worker holding EXACTLY its deployment's secrets, uploaded over a pipe, read back (0.5.0) |
18
18
  | `cursedops/worker-rollback` | a hostname back on its Mac origin: origin first, route second, the route found rather than typed (0.5.0) |
19
19
  | `cursedops/edge-fetch` | a request to a deployed Worker as a script must make it — curl pinned past the Mac's negative DNS cache (0.5.0) |
20
- | `cursedops/d1-import` | a cutover's data proof — SQL literals and the row-for-row comparison, never a count (0.5.0) |
20
+ | `cursedops/d1-import` | a cutover's data proof — SQL literals and the row-for-row comparison, never a count (0.5.0); and `d1SchemaText`, the generated `db/schema.sql` that refuses a statement spanning lines, because D1's exec splits on newlines (0.9.0, lifted from five apps, task 2153) |
21
+ | `cursedops/backups` | `BACKUP_KEEP`, the fleet's one retention for dated snapshots — six apps each exported `KEEP = 14` (0.9.0, task 2153) |
21
22
  | `cursedops/serve` | the static tier's four helpers — the path-traversal guard, the MIME table, the hashed-asset test, the crash handlers |
22
23
  | `cursedops/api-floor` | the rule that an unmatched `/api/...` is a phrase and never the app shell — the namespace predicates, the trailing-slash normaliser and the default 404 body. No `node:` import, so it mounts inside a Worker |
23
24
  | `cursedops/build-info` | which commit a checkout-served process is running and whether its tree was dirty — read once at load, and a `null` retried in the background rather than cached for the life of the process. `node:child_process`, so never in a Worker |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedops",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "The build-and-ops answers this generation's apps wrote independently and identically: finding a generation's roots — and printing a command that runs when pasted — without knowing a path, the generation's whole-tree laws run over one repo from a checkout or a worktree, macOS launchd agent install/replace/remove and the live port a job serves, the scaffolding and verdicts of a deployed smoke (origin probe, the smoke's own environment, a network that lies about DNS, a settled version), the static-serving helpers eight apps copied — the path-traversal guard among them — the API floor that keeps an unmatched /api/... from ever being answered with the app shell, the commit and dirty flag a checkout-served process reports, the Cloudflare Worker deploy toolkit four apps copied (the deploy sequence, exact-set secrets over a pipe, origin-first rollback, the curl edge fetch, the row-for-row D1 import proof, the billed-CPU tail check around a deploy's walk and smoke, and each app's worker:secrets and worker:smoke main as one function of its data), the Worker import-graph and await-port checks every Worker app's suite runs over its own source, and the public-surface ratchet three published libraries each carried a forked copy of. Mechanism only — no app knows its name from here. Bun, zero runtime dependencies (typescript is an optional peer, for public-surface only), ships source.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -110,6 +110,12 @@
110
110
  "source": "./src/stagedClient.ts",
111
111
  "import": "./src/stagedClient.ts"
112
112
  },
113
+ "./backups": {
114
+ "types": "./src/backups.ts",
115
+ "bun": "./src/backups.ts",
116
+ "source": "./src/backups.ts",
117
+ "import": "./src/backups.ts"
118
+ },
113
119
  "./deploy-tree": {
114
120
  "types": "./src/deployTree.ts",
115
121
  "bun": "./src/deployTree.ts",
package/src/backups.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The fleet's backup retention: how many dated snapshots each app's backup script keeps.
3
+ *
4
+ * Six apps (auth, collections, family, music, roms, station) each exported `KEEP = 14` until
5
+ * task 2153 (2026-09-24). A retention is a POLICY, and a policy held as six constants is six
6
+ * places a change has to reach — the one that is missed keeps a fortnight while the rest keep
7
+ * whatever was decided. So it is one number here, and each app's prune takes it.
8
+ */
9
+ export const BACKUP_KEEP = 14;
package/src/d1Import.ts CHANGED
@@ -122,3 +122,44 @@ export function wranglerRows(stdout: string): Array<Record<string, unknown>> {
122
122
  const parsed = JSON.parse(stdout.slice(start)) as Array<{ results?: Array<Record<string, unknown>> }>;
123
123
  return parsed.flatMap((result) => result.results ?? []);
124
124
  }
125
+
126
+ /**
127
+ * `db/schema.sql`'s text: the header every cut-over app wrote by hand, then one statement per line.
128
+ *
129
+ * 🔴 **Every statement is ONE LINE, and that is not formatting.** D1's `exec` splits its input on
130
+ * newlines and treats each line as a statement, so a pretty-printed `CREATE TABLE` fails there with
131
+ * `D1_EXEC_ERROR: … incomplete input` — and is green on `bun:sqlite` for ever, because that parses
132
+ * the whole string. So a statement carrying a newline THROWS here rather than being written into a
133
+ * file that only fails once a person applies it to production. Five apps (auth, collections,
134
+ * family, music, vault) carried this script as a copy until task 2153.
135
+ */
136
+ export function d1SchemaText(options: {
137
+ /** The D1 database's name, as `wrangler d1 execute` takes it. */
138
+ database: string;
139
+ /** What the statements were generated from, e.g. `src/server/localDb.ts's migrate()`. */
140
+ source: string;
141
+ statements: readonly string[];
142
+ /** App-specific lines for the header, each written as a `-- ` comment. */
143
+ notes?: readonly string[];
144
+ }): string {
145
+ const multiline = options.statements.filter((statement) => /[\r\n]/.test(statement));
146
+ if (multiline.length > 0) {
147
+ throw new Error(
148
+ `${multiline.length} statement(s) span more than one line, and D1's exec splits on newlines: ${multiline[0]?.slice(0, 120)}`,
149
+ );
150
+ }
151
+ const header = [
152
+ `-- GENERATED by \`bun run schema\` from ${options.source}. Do not edit.`,
153
+ "--",
154
+ "-- 🔴 Applied to D1 ONCE, by hand, because a Worker has no boot:",
155
+ `-- bunx wrangler d1 execute ${options.database} --remote --file db/schema.sql`,
156
+ "--",
157
+ "-- Every statement is on ONE LINE: D1's `exec` splits on newlines and treats each line as",
158
+ "-- a statement, so a formatted CREATE TABLE fails with `incomplete input` there and is",
159
+ "-- green on bun:sqlite for ever. src/server/schemaMatches.test.ts fails if this file and",
160
+ "-- migrate() ever disagree.",
161
+ ...(options.notes ?? []).map((line) => `-- ${line}`),
162
+ "",
163
+ ];
164
+ return `${header.join("\n")}${options.statements.join("\n")}\n`;
165
+ }
package/src/edgeFetch.ts CHANGED
@@ -32,6 +32,8 @@ import { spawnSync } from "node:child_process";
32
32
  import { readFileSync, rmSync, writeFileSync } from "node:fs";
33
33
  import { tmpdir } from "node:os";
34
34
  import { join } from "node:path";
35
+ // The package's own subpath, never `./roots.ts` — see `publishShape.test.ts`.
36
+ import { forgeState } from "cursedops/roots";
35
37
 
36
38
  export interface CurlRun {
37
39
  status: number | null;
@@ -172,3 +174,21 @@ export function createStageEdgeFetch(
172
174
  const { read: _read, ...rest } = options;
173
175
  return { edgeFetch: createEdgeFetch({ ...rest, headersFor: accessHeadersFor }), accessHeadersFor };
174
176
  }
177
+
178
+ /**
179
+ * THE fleet's edge fetch — {@link createStageEdgeFetch} over `$FORGE_STATE/secrets/cloudflare-access.env`,
180
+ * the one file every Worker app's smoke, stage walk and CPU tail read the Access token from. Two apps
181
+ * carried this as a one-line edge-fetch module of their own until task 2145.
182
+ *
183
+ * `$FORGE_STATE` comes from the environment or the `forge.env` above `from` (default: wherever this
184
+ * package is installed, which is inside the generation's checkout). THROWS when neither answers —
185
+ * a smoke with no idea where its token lives must not guess one.
186
+ */
187
+ export function fleetEdgeFetch(
188
+ from: string = import.meta.dir,
189
+ options: Omit<EdgeFetchOptions, "headersFor"> & { read?: (path: string) => string } = {},
190
+ ): ReturnType<typeof createStageEdgeFetch> {
191
+ const state = forgeState(from);
192
+ if (!state) throw new Error(`no generation state root above ${from}: set $FORGE_STATE or run inside a checkout with forge.env`);
193
+ return createStageEdgeFetch(join(state, "secrets", "cloudflare-access.env"), options);
194
+ }
@@ -51,6 +51,7 @@
51
51
  */
52
52
  import { spawn, spawnSync } from "node:child_process";
53
53
  import { existsSync, readFileSync } from "node:fs";
54
+ import { join } from "node:path";
54
55
 
55
56
  /** The two deployments every Worker app here has. */
56
57
  export type WorkerEnv = "production" | "stage";
@@ -89,6 +90,16 @@ export function readWranglerJsonc(text: string): Record<string, unknown> {
89
90
  return JSON.parse(stripped) as Record<string, unknown>;
90
91
  }
91
92
 
93
+ /**
94
+ * `<root>/wrangler.jsonc`, read and parsed. Four apps (auth, family, music, vault) each carried
95
+ * this as its own `readWrangler` until task 2153. It is what tells every CLI
96
+ * script whether production is the Worker or the Mac (`servedByWorker`), so a copy that drifts
97
+ * sends a script's writes to a frozen registry.
98
+ */
99
+ export function readWranglerFile(root: string): Record<string, unknown> {
100
+ return readWranglerJsonc(readFileSync(join(root, "wrangler.jsonc"), "utf8"));
101
+ }
102
+
92
103
  /**
93
104
  * `KEY=value` lines — `export ` tolerated, one layer of surrounding quotes removed (and a POSIX
94
105
  * `'\''` inside single quotes read back as `'`), `#` lines and blanks skipped. The reader every