cursedops 0.9.0 → 0.9.2

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
@@ -17,7 +17,7 @@ bun add cursedops
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); 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) |
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) — and its `forge-d1-schema` bin (0.9.1), so no app carries its own schema script at all |
21
21
  | `cursedops/backups` | `BACKUP_KEEP`, the fleet's one retention for dated snapshots — six apps each exported `KEEP = 14` (0.9.0, task 2153) |
22
22
  | `cursedops/serve` | the static tier's four helpers — the path-traversal guard, the MIME table, the hashed-asset test, the crash handlers |
23
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 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedops",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
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": {
@@ -126,7 +126,8 @@
126
126
  "bin": {
127
127
  "public-surface": "./src/publicSurface.ts",
128
128
  "forge-paths": "./src/paths.ts",
129
- "forge-client": "./src/stagedClient.ts"
129
+ "forge-client": "./src/stagedClient.ts",
130
+ "forge-d1-schema": "./src/d1Schema.ts"
130
131
  },
131
132
  "files": [
132
133
  "src",
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * `forge-d1-schema --database <name> --from <module> [--note "<line>"]…` — write `db/schema.sql`,
4
+ * the shape a Worker app's D1 is given, from the module's `schemaStatements()`.
5
+ *
6
+ * ```jsonc
7
+ * // an app's package.json — the script line IS the configuration
8
+ * "schema": "forge-d1-schema --database music --from src/server/localDb.ts"
9
+ * ```
10
+ *
11
+ * ## 🔴 Why the file is GENERATED and never hand-written
12
+ *
13
+ * A Worker has no boot. An app's `migrate()` issues dozens of statements and inspects
14
+ * `PRAGMA table_info` as it goes; running that on every invocation would spend a D1 invocation's
15
+ * query budget before the request started, and D1 has no `PRAGMA table_info` to inspect with. So
16
+ * D1's schema is applied ONCE, by hand — `bunx wrangler d1 execute <name> --remote --file
17
+ * db/schema.sql` — and there are two descriptions of the app's tables. Each app's own
18
+ * schema-matches test fails its gate when they disagree, so the file is a build product with a
19
+ * checker rather than a document with a convention. {@link d1SchemaText} writes it, and refuses a
20
+ * statement that spans lines, because D1's `exec` splits on newlines.
21
+ *
22
+ * ## Why this is a bin and not a function each app calls
23
+ *
24
+ * Five apps (auth, collections, family, music, vault) each carried its own schema script until task
25
+ * 2153 (2026-09-24). A thinner wrapper around {@link d1SchemaText} was still five files that agree
26
+ * line for line on everything but two strings, and `tools/check-copies.ts` rightly priced them as
27
+ * one file forked five ways. The only thing that differs per app is data — the database name, the
28
+ * module, a note — and data fits in the one line of `package.json` that already names the command.
29
+ */
30
+ import { mkdirSync, writeFileSync } from "node:fs";
31
+ import { dirname, isAbsolute, join, resolve } from "node:path";
32
+ import { d1SchemaText } from "cursedops/d1-import";
33
+
34
+ export type D1SchemaArgs = { database: string; from: string; exportName: string; notes: string[]; out: string };
35
+
36
+ /** The flags, or the sentence saying what is missing. */
37
+ export function parseD1SchemaArgs(argv: readonly string[]): D1SchemaArgs | string {
38
+ const args: D1SchemaArgs = { database: "", from: "", exportName: "schemaStatements", notes: [], out: join("db", "schema.sql") };
39
+ for (let i = 0; i < argv.length; i++) {
40
+ const flag = argv[i];
41
+ const value = argv[i + 1];
42
+ if (value === undefined) return `${flag} needs a value`;
43
+ if (flag === "--database") args.database = value;
44
+ else if (flag === "--from") args.from = value;
45
+ else if (flag === "--export") args.exportName = value;
46
+ else if (flag === "--note") args.notes.push(value);
47
+ else if (flag === "--out") args.out = value;
48
+ else return `unknown flag ${flag}`;
49
+ i++;
50
+ }
51
+ if (!args.database) return "--database <name> is required — the D1 database, as `wrangler d1 execute` takes it";
52
+ if (!args.from) return "--from <module> is required — the module whose schemaStatements() is the schema";
53
+ return args;
54
+ }
55
+
56
+ /** Write the file under `cwd` (the app's root, where `bun run` starts a script). Exit code. */
57
+ export async function runD1SchemaCli(argv: readonly string[], cwd: string = process.cwd()): Promise<number> {
58
+ const args = parseD1SchemaArgs(argv);
59
+ if (typeof args === "string") {
60
+ console.error(`✗ forge-d1-schema: ${args}\nusage: forge-d1-schema --database <name> --from <module> [--export <fn>] [--note "<line>"]… [--out <file>]`);
61
+ return 2;
62
+ }
63
+ const module = (await import(resolve(cwd, args.from))) as Record<string, unknown>;
64
+ const read = module[args.exportName];
65
+ if (typeof read !== "function") {
66
+ console.error(`✗ forge-d1-schema: ${args.from} exports no function ${args.exportName}()`);
67
+ return 2;
68
+ }
69
+ const statements = (await read()) as string[];
70
+ const out = isAbsolute(args.out) ? args.out : join(cwd, args.out);
71
+ const text = d1SchemaText({ database: args.database, source: `${args.from}'s ${args.exportName}()`, statements, notes: args.notes });
72
+ mkdirSync(dirname(out), { recursive: true });
73
+ writeFileSync(out, text);
74
+ console.log(`[${args.database}] wrote ${statements.length} statement(s) to ${args.out}`);
75
+ return 0;
76
+ }
77
+
78
+ if (import.meta.main) {
79
+ process.exit(await runD1SchemaCli(process.argv.slice(2)));
80
+ }
@@ -243,8 +243,11 @@ export interface DeployDeps {
243
243
  }
244
244
 
245
245
  export interface DeployResult {
246
- /** 0 shipped and smoked; 1 refused or failed — the step says where. */
247
- code: 0 | 1;
246
+ /**
247
+ * 0 shipped and smoked; 1 refused or failed — the step says where; {@link TAIL_UNATTACHED_EXIT}
248
+ * shipped and smoked green with its Worker CPU UNMEASURED (step `cpu-unmeasured`).
249
+ */
250
+ code: 0 | 1 | typeof TAIL_UNATTACHED_EXIT;
248
251
  /** The step it stopped at, or `"done"`. */
249
252
  step: string;
250
253
  /** Why it stopped, or the success line. */
@@ -280,6 +283,8 @@ export function runWorkerDeploy(spec: WorkerDeploySpec, deps: DeployDeps): Deplo
280
283
  return { code: 1, step, detail, commit, dirty };
281
284
  };
282
285
  const step = (message: string): void => log(`\n▸ ${message}`);
286
+ /** What passed under a CPU tail that never attached — decision 6 of the CPU check. */
287
+ const unmeasured: string[] = [];
283
288
 
284
289
  if (!commit) return stop("head", "this checkout has no readable HEAD — there is no commit to stamp, so nothing could prove the deploy.");
285
290
  if (spec.env === "production" && dirty) {
@@ -297,7 +302,9 @@ export function runWorkerDeploy(spec: WorkerDeploySpec, deps: DeployDeps): Deplo
297
302
  for (const walk of spec.stageWalk ?? []) {
298
303
  const command = spec.cpuTail ? underCpuTail(spec.cpuTail, "stage", walk) : [...walk];
299
304
  step(`walk the stage signed in${spec.cpuTail ? ", under a CPU tail" : ""}: ${command.join(" ")}`);
300
- if (deps.run(command, {}) !== 0) return stop("stage", `\`${command.join(" ")}\` failed — production was NOT touched.`);
305
+ const walked = deps.run(command, {});
306
+ if (spec.cpuTail && walked === TAIL_UNATTACHED_EXIT) unmeasured.push("the stage walk");
307
+ else if (walked !== 0) return stop("stage", `\`${command.join(" ")}\` failed — production was NOT touched.`);
301
308
  }
302
309
  }
303
310
  if (spec.build) {
@@ -329,7 +336,9 @@ export function runWorkerDeploy(spec: WorkerDeploySpec, deps: DeployDeps): Deplo
329
336
  if (spec.smoke) {
330
337
  step(`smoke every address this deployment answers on${spec.cpuTail ? ", under a CPU tail" : ""}`);
331
338
  const smoke = spec.cpuTail ? underCpuTail(spec.cpuTail, spec.env, spec.smoke) : spec.smoke;
332
- if (deps.run(smoke, {}) !== 0) {
339
+ const smoked = deps.run(smoke, {});
340
+ if (spec.cpuTail && smoked === TAIL_UNATTACHED_EXIT) unmeasured.push("the smoke");
341
+ else if (smoked !== 0) {
333
342
  return stop(
334
343
  "smoke",
335
344
  "the Worker is deployed and its smoke FAILED. Nothing was rolled back: `bunx wrangler rollback` puts the previous version back in one command.",
@@ -337,6 +346,13 @@ export function runWorkerDeploy(spec: WorkerDeploySpec, deps: DeployDeps): Deplo
337
346
  }
338
347
  }
339
348
  const detail = `${spec.workerName} deployed${spec.secrets ? ", attached" : ""}${spec.smoke ? " and smoked" : ""} @ ${commit.slice(0, 8)}.`;
349
+ if (unmeasured.length > 0) {
350
+ const why =
351
+ `${detail} Worker CPU UNMEASURED — the tail never attached around ${unmeasured.join(" and ")}, which PASSED. ` +
352
+ "Nothing is wrong with the deployment and nothing needs rolling back; rerun the smoke under the app's CPU wrapper to measure it.";
353
+ error(`\n🟡 ${tag} ${why}`);
354
+ return { code: TAIL_UNATTACHED_EXIT, step: "cpu-unmeasured", detail: why, commit, dirty };
355
+ }
340
356
  log(`\n✅ ${detail}`);
341
357
  return { code: 0, step: "done", detail, commit, dirty };
342
358
  }
@@ -400,6 +416,12 @@ export function runWorkerDeploy(spec: WorkerDeploySpec, deps: DeployDeps): Deplo
400
416
  * excused by production's traffic on a shared tail, or the reverse.
401
417
  * 5. **A tail that never connects is a failure, never an empty pass**; neither is a runtime that
402
418
  * stops reporting `cpuTime` (`recordTailTraces` skips it; the empty report is `no-samples`).
419
+ * 6. 🔴 **…but it is not the COMMAND failing (0.9.2, task 2157).** A tail that does not attach is
420
+ * restarted ONCE with a fresh marker; if the second never attaches either, the command still
421
+ * runs (unmeasured, so the deploy is still smoked) and the result is {@link TAIL_UNATTACHED_EXIT},
422
+ * which {@link runWorkerDeploy} reports as "CPU unmeasured", never "smoke FAILED". Measured
423
+ * 2026-09-24: collections' production tail never delivered its marker within 60 s, the deploy
424
+ * said the smoke failed, and the same smoke rerun two minutes later passed 30 checks.
403
425
  *
404
426
  * ## How it knows the tail is listening, and that it has heard everything
405
427
  *
@@ -682,11 +704,21 @@ export interface WorkerCpuTailSpec extends Omit<JudgeOpts, "scriptName"> {
682
704
  markerWaitMs?: number;
683
705
  }
684
706
 
707
+ /**
708
+ * The exit code of a CPU tail that never attached, twice, around a command that PASSED (`EX_UNAVAILABLE`):
709
+ * nothing was measured, and nothing was wrong with the deployment. Distinct from the command's own
710
+ * codes so {@link runWorkerDeploy} can say which of the two happened — decision 6.
711
+ */
712
+ export const TAIL_UNATTACHED_EXIT = 69;
713
+
685
714
  export interface WorkerCpuTailResult {
686
- /** 0 green; the command's own code when it failed; 1 for any CPU or tail failure. */
715
+ /**
716
+ * 0 green; the command's own code when it failed; {@link TAIL_UNATTACHED_EXIT} when the command
717
+ * passed and the tail never attached; 1 for any other CPU or tail failure.
718
+ */
687
719
  code: number;
688
- /** Where it stopped: `connect`, `command`, `drain`, `budget` or `done`. */
689
- step: "connect" | "command" | "drain" | "budget" | "done";
720
+ /** Where it stopped: `unmeasured`, `command`, `drain`, `budget` or `done`. */
721
+ step: "unmeasured" | "command" | "drain" | "budget" | "done";
690
722
  verdict: TailVerdict | null;
691
723
  }
692
724
 
@@ -700,7 +732,7 @@ export async function runWorkerCpuTail(spec: WorkerCpuTailSpec, deps: WorkerCpuD
700
732
  const base = spec.base.replace(/\/$/, "");
701
733
 
702
734
  deps.log(`\n▸ ${tag} Worker CPU: tailing ${spec.workerName} around \`${spec.command.join(" ")}\``);
703
- const tail = deps.startTail(spec.workerName, spec.cwd, spec.credential);
735
+ let tail = deps.startTail(spec.workerName, spec.cwd, spec.credential);
704
736
 
705
737
  /** Send the marker until the tail has carried it back, or give up. */
706
738
  const awaitMarker = async (marker: string, withinMs: number): Promise<boolean> => {
@@ -718,16 +750,27 @@ export async function runWorkerCpuTail(spec: WorkerCpuTailSpec, deps: WorkerCpuD
718
750
  return false;
719
751
  };
720
752
 
721
- if (!(await awaitMarker(nonce, connectMs))) {
753
+ // Decision 6: a tail that never attaches is restarted ONCE, with a fresh marker, before anything is judged.
754
+ let attached = await awaitMarker(nonce, connectMs);
755
+ let marker = nonce;
756
+ if (!attached) {
757
+ tail.stop();
758
+ deps.error(`🟡 ${tag} wrangler tail never delivered a request from ${base} within ${Math.round(connectMs / 1000)}s — restarting it once.\n${tail.errors()}`);
759
+ marker = `${nonce}r`;
760
+ tail = deps.startTail(spec.workerName, spec.cwd, spec.credential);
761
+ attached = await awaitMarker(marker, connectMs);
762
+ }
763
+ if (!attached) {
722
764
  tail.stop();
723
765
  deps.error(
724
- `🔴 ${tag} wrangler tail never delivered a request from ${base} within ${Math.round(connectMs / 1000)}s — nothing could be measured.\n${tail.errors()}`,
766
+ `🔴 ${tag} wrangler tail never attached, twice, within ${Math.round(connectMs / 1000)}s each — the command runs UNMEASURED.\n${tail.errors()}`,
725
767
  );
726
- return { code: 1, step: "connect", verdict: null };
768
+ const ran = deps.run(spec.command, spec.cwd);
769
+ return { code: ran !== 0 ? ran : TAIL_UNATTACHED_EXIT, step: ran !== 0 ? "command" : "unmeasured", verdict: null };
727
770
  }
728
771
 
729
772
  const ran = deps.run(spec.command, spec.cwd);
730
- const drained = await awaitMarker(`${nonce}-end`, drainMs);
773
+ const drained = await awaitMarker(`${marker}-end`, drainMs);
731
774
  // Traces are not strictly ordered; a moment more lets a straggler behind the end marker land.
732
775
  await deps.sleep(2_000);
733
776
  tail.stop();