cursedops 0.10.25 → 0.10.26
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/package.json +1 -1
- package/src/relayLink.ts +1 -1
- package/src/relayStage.ts +34 -2
- package/src/workerDeploy.ts +100 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedops",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.26",
|
|
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 relay a Worker fronts a Mac-bound app with (the Durable Object, the frames, the Mac's dialer and key rotation — lifted from station for roms — and the signed-in stage walk's skeleton and the relay app's whole worker:deploy), 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": {
|
package/src/relayLink.ts
CHANGED
|
@@ -318,7 +318,7 @@ export async function rotateLinkKey(spec: RotateSpec): Promise<{ ok: true; file:
|
|
|
318
318
|
const put =
|
|
319
319
|
spec.putSecret ??
|
|
320
320
|
((name: string, value: string) => {
|
|
321
|
-
const r = spawnSync("bunx", ["wrangler
|
|
321
|
+
const r = spawnSync("bunx", ["wrangler", "secret", "put", name, ...(spec.env ? ["--env", spec.env] : [])], {
|
|
322
322
|
cwd: spec.appDir,
|
|
323
323
|
input: value,
|
|
324
324
|
env: { ...process.env, ...spec.credential },
|
package/src/relayStage.ts
CHANGED
|
@@ -54,6 +54,37 @@ export async function until(ms: number, ok: () => Promise<boolean>, clock: Clock
|
|
|
54
54
|
return false;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* True once `isLinked` has said yes on every ask for a whole `windowMs` (default 15 s) — false if that
|
|
59
|
+
* never happens within `maxMs` (default 2 min). A throw counts as no, and restarts the window.
|
|
60
|
+
*
|
|
61
|
+
* 🔴 Why not a sleep and then {@link until}: a digest put is a new Worker version that reaches the
|
|
62
|
+
* edge in its own time, and `until` passes the first instant the link is up. Measured 2026-09-28 on
|
|
63
|
+
* station, beside three other deploys: the walk slept its fixed 10 s, the link came up, then flapped
|
|
64
|
+
* (`1002 Expected 101` — an edge still on the old digest refusing the new key) and the first
|
|
65
|
+
* signed-in read got the offline 503. `oldKeyRefused` already records that 15 s was not always
|
|
66
|
+
* enough. So the walk waits for a link that STAYS up, and how long that took is printed.
|
|
67
|
+
*/
|
|
68
|
+
export async function linkedSteadily(spec: {
|
|
69
|
+
isLinked: () => Promise<boolean>;
|
|
70
|
+
windowMs?: number;
|
|
71
|
+
maxMs?: number;
|
|
72
|
+
clock?: Clock;
|
|
73
|
+
}): Promise<{ steady: boolean; seconds: number }> {
|
|
74
|
+
const clock = spec.clock ?? realClock;
|
|
75
|
+
const window = spec.windowMs ?? 15_000;
|
|
76
|
+
const started = clock.now();
|
|
77
|
+
let upSince: number | null = null;
|
|
78
|
+
while (clock.now() - started < (spec.maxMs ?? 120_000)) {
|
|
79
|
+
const up = await spec.isLinked().catch(() => false);
|
|
80
|
+
if (!up) upSince = null;
|
|
81
|
+
else if (upSince === null) upSince = clock.now();
|
|
82
|
+
else if (clock.now() - upSince >= window) return { steady: true, seconds: Math.round((clock.now() - started) / 1000) };
|
|
83
|
+
await clock.sleep(1_000);
|
|
84
|
+
}
|
|
85
|
+
return { steady: false, seconds: Math.round((clock.now() - started) / 1000) };
|
|
86
|
+
}
|
|
87
|
+
|
|
57
88
|
/** The Access service token, as the pair a Mac end's two variables and a roster socket carry. */
|
|
58
89
|
export function accessPair(headers: Record<string, string>): { id: string; secret: string } {
|
|
59
90
|
return { id: headers["CF-Access-Client-Id"] ?? "", secret: headers["CF-Access-Client-Secret"] ?? "" };
|
|
@@ -142,10 +173,11 @@ export interface DigestPut {
|
|
|
142
173
|
/**
|
|
143
174
|
* Put `key`'s digest on the STAGE Worker (`--env stage`, always — this module has no production
|
|
144
175
|
* path). A per-run key for the walk, and again for the rotation leg. `ok: false` carries wrangler's
|
|
145
|
-
* stderr. A secret put is a new version
|
|
176
|
+
* stderr. A secret put is a new version that reaches the edge in its own time: the caller waits for
|
|
177
|
+
* a link that stays up ({@link linkedSteadily}), never a fixed sleep.
|
|
146
178
|
*/
|
|
147
179
|
export async function putStageLinkDigest(put: DigestPut): Promise<{ ok: boolean; stderr: string }> {
|
|
148
|
-
const done = (put.run ?? realRun)(["bunx", "wrangler
|
|
180
|
+
const done = (put.run ?? realRun)(["bunx", "wrangler", "secret", "put", put.secretName, "--env", "stage"], {
|
|
149
181
|
cwd: put.cwd,
|
|
150
182
|
input: await sha256Hex(put.key),
|
|
151
183
|
env: put.credential,
|
package/src/workerDeploy.ts
CHANGED
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
* refusal without wrangler, git or a network.
|
|
54
54
|
*/
|
|
55
55
|
import { spawn, spawnSync } from "node:child_process";
|
|
56
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
56
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
|
|
57
57
|
import { dirname, join } from "node:path";
|
|
58
58
|
import { forgeState } from "cursedops/roots";
|
|
59
59
|
|
|
@@ -308,6 +308,10 @@ export interface DeployDeps {
|
|
|
308
308
|
error?: (line: string) => void;
|
|
309
309
|
/** The clock the legs are timed on. Default `Date.now`. */
|
|
310
310
|
now?: () => number;
|
|
311
|
+
/** Whether a path in the checkout exists — the local wrangler. Default `existsSync` against the process's cwd. */
|
|
312
|
+
exists?: (path: string) => boolean;
|
|
313
|
+
/** The unpublished library builds in `node_modules` ({@link previewMarks}). Default: read the process's cwd. */
|
|
314
|
+
previews?: () => PreviewMark[];
|
|
311
315
|
/**
|
|
312
316
|
* Where the run's {@link DeployEventLine} goes, on EVERY exit — refused, red or green.
|
|
313
317
|
* {@link workerDeployDeps} appends it to the generation's ledger; absent, nothing is recorded.
|
|
@@ -548,6 +552,90 @@ export function judgeSchemaApply(code: number, stdout: string): { applied: boole
|
|
|
548
552
|
* Run the sequence in the header's order, stopping at the first red with a sentence. Never
|
|
549
553
|
* exits — the caller does, with {@link DeployResult.code}.
|
|
550
554
|
*/
|
|
555
|
+
/** Where an app's OWN wrangler is, once `bun install` has run. */
|
|
556
|
+
export const LOCAL_WRANGLER = "node_modules/.bin/wrangler";
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Refuse a deploy whose app has no wrangler of its own — `null` when it has one.
|
|
560
|
+
*
|
|
561
|
+
* 🔴 Why (task 2260): with no local install, `bunx wrangler` resolves into ONE directory shared by
|
|
562
|
+
* every process on the Mac (`$TMPDIR/bunx-<uid>-wrangler@latest`), and bunx refreshes it in place.
|
|
563
|
+
* On 2026-09-28 it was rewritten at 12:17 while four deploys ran, and station's stage walk died on
|
|
564
|
+
* `Cannot find module './lib/websocket-server'` — a half-extracted `ws`. A version spec
|
|
565
|
+
* (`wrangler@4`) is worse: bunx then skips a local install even when there is one. So every app
|
|
566
|
+
* declares `wrangler` in its own `devDependencies`, locked by its own `bun.lock`, and nothing here
|
|
567
|
+
* passes bunx a version (`workerDeploy.test.ts` scans `src/` for one).
|
|
568
|
+
*/
|
|
569
|
+
export function localWranglerRefusal(exists: (path: string) => boolean): string | null {
|
|
570
|
+
if (exists(LOCAL_WRANGLER)) return null;
|
|
571
|
+
return `this app has no wrangler of its own (${LOCAL_WRANGLER} is missing), so \`bunx wrangler\` would use the one install every deploy on this Mac shares — and concurrent deploys have corrupted it mid-run. Run \`bun add -d wrangler@^4\` in the app and commit package.json and bun.lock. Nothing was deployed.`;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** The file `tools/lib-preview.ts` writes INSIDE a package it overlaid with an unpublished build. */
|
|
575
|
+
export const PREVIEW_MARK = ".lib-preview.json";
|
|
576
|
+
|
|
577
|
+
/** One unpublished library build sitting in `node_modules` — what its {@link PREVIEW_MARK} says. */
|
|
578
|
+
export interface PreviewMark {
|
|
579
|
+
lib: string;
|
|
580
|
+
/** The version in the library's own tree — never the one `bun.lock` names. */
|
|
581
|
+
version: string;
|
|
582
|
+
/** The library tree's HEAD when it was packed, `-dirty` when it had uncommitted changes. */
|
|
583
|
+
commit: string;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Every package under `nodeModules` (top level and `@scope/name`) carrying a {@link PREVIEW_MARK}.
|
|
588
|
+
*
|
|
589
|
+
* The mark lives inside the package's own directory, never beside it, so it cannot outlive the bytes
|
|
590
|
+
* it describes: an install that replaces the package (a lockfile bump, `--force`) removes it too.
|
|
591
|
+
* `bun install --frozen-lockfile` leaves an overlay whose `package.json` version matches the lock, so
|
|
592
|
+
* the preview writes the locked version there and its real one here (measured 2026-09-28).
|
|
593
|
+
*/
|
|
594
|
+
export function previewMarks(nodeModules = "node_modules"): PreviewMark[] {
|
|
595
|
+
const marks: PreviewMark[] = [];
|
|
596
|
+
const read = (dir: string): void => {
|
|
597
|
+
const file = join(dir, PREVIEW_MARK);
|
|
598
|
+
if (!existsSync(file)) return;
|
|
599
|
+
try {
|
|
600
|
+
const mark = JSON.parse(readFileSync(file, "utf8")) as Partial<PreviewMark>;
|
|
601
|
+
marks.push({ lib: mark.lib ?? dir, version: mark.version ?? "?", commit: mark.commit ?? "?" });
|
|
602
|
+
} catch {
|
|
603
|
+
marks.push({ lib: dir, version: "?", commit: "unreadable mark" });
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
let names: string[] = [];
|
|
607
|
+
try {
|
|
608
|
+
names = readdirSync(nodeModules);
|
|
609
|
+
} catch {
|
|
610
|
+
return marks;
|
|
611
|
+
}
|
|
612
|
+
for (const name of names) {
|
|
613
|
+
if (name.startsWith("@")) {
|
|
614
|
+
let scoped: string[] = [];
|
|
615
|
+
try {
|
|
616
|
+
scoped = readdirSync(join(nodeModules, name));
|
|
617
|
+
} catch {}
|
|
618
|
+
for (const inner of scoped) read(join(nodeModules, name, inner));
|
|
619
|
+
} else if (!name.startsWith(".")) read(join(nodeModules, name));
|
|
620
|
+
}
|
|
621
|
+
return marks;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** `<lib>@<version> (<commit>)`, joined — how a deploy names what it would ship unpublished. */
|
|
625
|
+
export function describePreviews(marks: readonly PreviewMark[]): string {
|
|
626
|
+
return marks.map((m) => `${m.lib}@${m.version} (${m.commit})`).join(", ");
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Production never ships an unpublished library (task 2252) — `null` when nothing is previewed. A
|
|
631
|
+
* stage deploy ships it, says so, and stamps itself dirty: `/healthz` then says the bytes are not
|
|
632
|
+
* the commit's, which is true.
|
|
633
|
+
*/
|
|
634
|
+
export function previewRefusal(env: WorkerEnv, marks: readonly PreviewMark[]): string | null {
|
|
635
|
+
if (env !== "production" || marks.length === 0) return null;
|
|
636
|
+
return `node_modules holds an UNPUBLISHED build of ${describePreviews(marks)} — production ships only published libraries. Publish it (\`tools/publish-libs.sh\`), bump this app to it, or put the locked version back with \`bun $FORGE/tools/lib-preview.ts --restore\`. Nothing was deployed.`;
|
|
637
|
+
}
|
|
638
|
+
|
|
551
639
|
/** What {@link runWorkerDeploy} runs first: `node_modules` exactly as `bun.lock` says. */
|
|
552
640
|
export const INSTALL: readonly string[] = ["bun", "install", "--frozen-lockfile"];
|
|
553
641
|
|
|
@@ -631,6 +719,13 @@ export function runWorkerDeploy(spec: WorkerDeploySpec, deps: DeployDeps): Deplo
|
|
|
631
719
|
if (deps.run(INSTALL, {}) !== 0) {
|
|
632
720
|
return stop("install", `\`${INSTALL.join(" ")}\` failed — node_modules cannot be made to match bun.lock, so nothing was built or deployed.`);
|
|
633
721
|
}
|
|
722
|
+
const noWrangler = localWranglerRefusal(deps.exists ?? existsSync);
|
|
723
|
+
if (noWrangler) return stop("wrangler", noWrangler);
|
|
724
|
+
const previews = (deps.previews ?? previewMarks)();
|
|
725
|
+
const previewed = previewRefusal(spec.env, previews);
|
|
726
|
+
if (previewed) return stop("preview", previewed);
|
|
727
|
+
if (previews.length > 0) log(`\n🟡 ${tag} this stage deploy ships an UNPUBLISHED ${describePreviews(previews)} — stamped dirty; production will refuse it until it is published`);
|
|
728
|
+
const shipsDirty = dirty || previews.length > 0;
|
|
634
729
|
if (spec.env === "production") {
|
|
635
730
|
for (const command of spec.stageFirst ?? []) {
|
|
636
731
|
step(`the stage first — production waits on it: ${command.join(" ")}`);
|
|
@@ -683,9 +778,9 @@ export function runWorkerDeploy(spec: WorkerDeploySpec, deps: DeployDeps): Deplo
|
|
|
683
778
|
log(` ${verdict.line}`);
|
|
684
779
|
}
|
|
685
780
|
}
|
|
686
|
-
step(`deploy ${spec.workerName} @ ${commit.slice(0, 8)}${
|
|
781
|
+
step(`deploy ${spec.workerName} @ ${commit.slice(0, 8)}${shipsDirty ? " (dirty — stage only)" : ""}`);
|
|
687
782
|
const stamp = spec.stampVars
|
|
688
|
-
? ["--var", `${spec.stampVars.commit}:${commit}`, "--var", `${spec.stampVars.dirty}:${
|
|
783
|
+
? ["--var", `${spec.stampVars.commit}:${commit}`, "--var", `${spec.stampVars.dirty}:${shipsDirty ? "true" : "false"}`]
|
|
689
784
|
: [];
|
|
690
785
|
enter("upload");
|
|
691
786
|
if (deps.run(["bunx", "wrangler", "deploy", ...envArgs, ...stamp], spec.credential) !== 0) return stop("deploy", "wrangler deploy failed.");
|
|
@@ -1144,13 +1239,13 @@ export interface WorkerCpuDeps {
|
|
|
1144
1239
|
error: (line: string) => void;
|
|
1145
1240
|
}
|
|
1146
1241
|
|
|
1147
|
-
/** The real {@link WorkerCpuDeps}: `bunx wrangler
|
|
1242
|
+
/** The real {@link WorkerCpuDeps}: `bunx wrangler tail` — the APP's own wrangler, whose major (4) its `devDependencies` pin: {@link localWranglerRefusal}. */
|
|
1148
1243
|
export function workerCpuDeps(): WorkerCpuDeps {
|
|
1149
1244
|
return {
|
|
1150
1245
|
startTail: (workerName, cwd, env) => {
|
|
1151
1246
|
let out = "";
|
|
1152
1247
|
let err = "";
|
|
1153
|
-
const child = spawn("bunx", ["wrangler
|
|
1248
|
+
const child = spawn("bunx", ["wrangler", "tail", workerName, "--format", "json"], {
|
|
1154
1249
|
cwd,
|
|
1155
1250
|
env: { ...process.env, ...env },
|
|
1156
1251
|
stdio: ["ignore", "pipe", "pipe"],
|