cursedops 0.4.0 → 0.5.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 +138 -5
- package/package.json +41 -4
- package/src/d1Import.ts +124 -0
- package/src/edgeFetch.ts +152 -0
- package/src/launchd.ts +40 -0
- package/src/paths.ts +173 -0
- package/src/roots.ts +126 -0
- package/src/smoke.ts +476 -4
- package/src/workerDeploy.ts +268 -0
- package/src/workerRollback.ts +133 -0
- package/src/workerSecrets.ts +195 -0
package/src/paths.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* `cursedops/paths` — the generation's whole-tree laws, run over ONE repo's tree, found the same
|
|
4
|
+
* way from a primary checkout and from a git worktree.
|
|
5
|
+
*
|
|
6
|
+
* ```jsonc
|
|
7
|
+
* // package.json — the whole wiring; no scripts/paths.ts at all
|
|
8
|
+
* "paths": "forge-paths" // check-paths + check-doc-citations
|
|
9
|
+
* "paths": "forge-paths --also check-gate-graph.ts" // cursedbelt's extra law, as an OPTION
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* // or, for a repo that wants a file: scripts/paths.ts
|
|
14
|
+
* import { runGenerationLaws } from "cursedops/paths";
|
|
15
|
+
* process.exit(runGenerationLaws(import.meta.dir, { also: ["check-no-clock.ts"] }));
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* ## Why this is a library entry and not a file every repo copies
|
|
19
|
+
*
|
|
20
|
+
* Measured 2026-09-22 (task 2089): `scripts/paths.ts` was in EIGHTEEN packages — every app,
|
|
21
|
+
* `autopilot`, and every library — sixteen of them byte-identical, and three had already forked:
|
|
22
|
+
* `libs/cursedbelt` added `check-gate-graph.ts`, `apps/station` added `check-no-clock.ts`, and
|
|
23
|
+
* `autopilot` split the walk into a `scripts/generation.ts` of its own. `check-copies` prices
|
|
24
|
+
* exported definitions, and a script that exports nothing registered as ZERO duplication — the
|
|
25
|
+
* same blindness that let three `publicSurface.ts` bodies fork unremarked. The generation's own
|
|
26
|
+
* `check-paths.ts` named one of the copies as the template to copy, so the count could only grow.
|
|
27
|
+
*
|
|
28
|
+
* ## 🔴 Why a wrapper at all, and not `bun ../../tools/check-paths.ts .`
|
|
29
|
+
*
|
|
30
|
+
* That spelling assumes the checkout sits at a fixed depth under the generation root, and a
|
|
31
|
+
* WORKTREE never does: the working agreement puts worktrees outside every generation by
|
|
32
|
+
* construction. Measured 2026-09-17 in a worktree — `error: Module not found
|
|
33
|
+
* "../../tools/check-paths.ts"`, and the gate stopped on its FIRST step in seventeen packages.
|
|
34
|
+
* The failure mode is not the error but the workaround: an agent that cannot run its gate moves
|
|
35
|
+
* back into a checkout another agent holds, or lands without a green gate.
|
|
36
|
+
*
|
|
37
|
+
* So the generation is found by walking up for `forge.env` — the marker `check-paths` and the
|
|
38
|
+
* runner use, so the three cannot disagree — and, in a worktree, by following
|
|
39
|
+
* `git rev-parse --git-common-dir` back to the primary checkout. The walk is inlined here rather
|
|
40
|
+
* than imported from `cursedops/roots`: a shipped file imports no sibling (`publishShape.test.ts`).
|
|
41
|
+
*
|
|
42
|
+
* 🔴 **No generation, no pass.** A lone clone has no generation above it and is not a worktree of
|
|
43
|
+
* one, and answering "fine" because the laws could not be located is how a rule stops being
|
|
44
|
+
* enforced without anybody deciding to stop enforcing it. Every failure to locate is exit 1 with
|
|
45
|
+
* a sentence saying which walk came back empty — the same posture the eighteen copies had.
|
|
46
|
+
*/
|
|
47
|
+
import { spawnSync } from "node:child_process";
|
|
48
|
+
import { existsSync } from "node:fs";
|
|
49
|
+
import { dirname, join, resolve } from "node:path";
|
|
50
|
+
|
|
51
|
+
/** The laws every repo runs over itself, in the order they should fail. */
|
|
52
|
+
export const DEFAULT_LAWS: readonly string[] = ["check-paths.ts", "check-doc-citations.ts"];
|
|
53
|
+
|
|
54
|
+
const MAX_HOPS = 16;
|
|
55
|
+
|
|
56
|
+
function walkUp(from: string, marker: string): string | null {
|
|
57
|
+
let dir = resolve(from);
|
|
58
|
+
for (let hops = 0; hops < MAX_HOPS; hops++) {
|
|
59
|
+
if (existsSync(join(dir, marker))) return dir;
|
|
60
|
+
const up = dirname(dir);
|
|
61
|
+
if (up === dir) return null;
|
|
62
|
+
dir = up;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function viaWorktree(from: string): string | null {
|
|
68
|
+
try {
|
|
69
|
+
const git = spawnSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
|
|
70
|
+
cwd: from,
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
timeout: 5_000,
|
|
73
|
+
});
|
|
74
|
+
if (git.status !== 0) return null;
|
|
75
|
+
const common = (git.stdout ?? "").trim();
|
|
76
|
+
if (!common.startsWith("/")) return null;
|
|
77
|
+
return walkUp(dirname(common), "forge.env");
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The checkout `from` is in, and the generation its laws live in — either may be `null`. */
|
|
84
|
+
export function locateLaws(from: string): { repo: string | null; generation: string | null } {
|
|
85
|
+
return { repo: walkUp(from, "package.json"), generation: walkUp(from, "forge.env") ?? viaWorktree(from) };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface LawsOptions {
|
|
89
|
+
/** Laws on top of {@link DEFAULT_LAWS} — a file name under the generation's `tools/`. */
|
|
90
|
+
also?: readonly string[];
|
|
91
|
+
/** Replace the list entirely. Rarely right: a repo that drops a law has stopped checking it. */
|
|
92
|
+
laws?: readonly string[];
|
|
93
|
+
/** Passed through to every law, e.g. `--prune`. */
|
|
94
|
+
argv?: readonly string[];
|
|
95
|
+
/** Injected for tests. Runs one law and returns its exit code. */
|
|
96
|
+
run?: (runtime: string, args: readonly string[]) => number;
|
|
97
|
+
error?: (line: string) => void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A law is a file NAME under `tools/`, never a path — a path is a second place that knows one. */
|
|
101
|
+
function checkedLaw(name: string): string {
|
|
102
|
+
if (!/^[A-Za-z0-9._-]+\.(?:ts|js|mjs)$/.test(name)) throw new Error(`not a law's file name: ${JSON.stringify(name)} — e.g. "check-gate-graph.ts"`);
|
|
103
|
+
return name;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The runtime to hand each law: this bun when running under bun, else `bun` from PATH. */
|
|
107
|
+
function runtime(): string {
|
|
108
|
+
return process.versions.bun ? process.execPath : "bun";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Run every law over the checkout `from` is in. Returns the first non-zero exit code, or 0.
|
|
113
|
+
*
|
|
114
|
+
* Laws run in order and stop at the first red, exactly as the copies did — a later law's output
|
|
115
|
+
* under an earlier red is noise the reader has to scroll past.
|
|
116
|
+
*/
|
|
117
|
+
export function runGenerationLaws(from: string, options: LawsOptions = {}): number {
|
|
118
|
+
const error = options.error ?? ((line: string) => console.error(line));
|
|
119
|
+
const run =
|
|
120
|
+
options.run ?? ((bin: string, args: readonly string[]) => spawnSync(bin, [...args], { stdio: "inherit" }).status ?? 1);
|
|
121
|
+
let laws: string[];
|
|
122
|
+
try {
|
|
123
|
+
laws = [...new Set([...(options.laws ?? DEFAULT_LAWS), ...(options.also ?? [])].map(checkedLaw))];
|
|
124
|
+
} catch (thrown) {
|
|
125
|
+
error(`✗ ${(thrown as Error).message}`);
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
const { repo, generation } = locateLaws(from);
|
|
129
|
+
if (!repo) {
|
|
130
|
+
error(`✗ no package.json above ${from}, so there is no repo to check.`);
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
if (!generation) {
|
|
134
|
+
error("✗ this checkout is not inside a generation and is not a worktree of one, so");
|
|
135
|
+
error(` tools/{${laws.join(",")}} cannot be located. Run it from a checkout under a`);
|
|
136
|
+
error(" generation, or from a worktree cut from one — a skipped check is not a passed one.");
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
for (const name of laws) {
|
|
140
|
+
const tool = join(generation, "tools", name);
|
|
141
|
+
if (!existsSync(tool)) {
|
|
142
|
+
error(`✗ ${tool} does not exist. The generation at ${generation} has no ${name},`);
|
|
143
|
+
error(" so this repo cannot prove it obeys that law. That is a failure, not a skip.");
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
const code = run(runtime(), [tool, repo, ...(options.argv ?? [])]);
|
|
147
|
+
if (code !== 0) return code;
|
|
148
|
+
}
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The bin's arguments: `--also <law>` / `--also=<law>` (repeatable) are this wrapper's; every
|
|
154
|
+
* other argument goes through to the laws untouched, so `bun run paths --prune` still prunes.
|
|
155
|
+
*/
|
|
156
|
+
export function parseLawsArgv(argv: readonly string[]): { also: string[]; argv: string[] } {
|
|
157
|
+
const also: string[] = [];
|
|
158
|
+
const rest: string[] = [];
|
|
159
|
+
for (let i = 0; i < argv.length; i++) {
|
|
160
|
+
const arg = argv[i] as string;
|
|
161
|
+
if (arg === "--also") {
|
|
162
|
+
const next = argv[i + 1];
|
|
163
|
+
if (next !== undefined) also.push(next);
|
|
164
|
+
i++;
|
|
165
|
+
} else if (arg.startsWith("--also=")) also.push(arg.slice("--also=".length));
|
|
166
|
+
else rest.push(arg);
|
|
167
|
+
}
|
|
168
|
+
return { also, argv: rest };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (import.meta.main) {
|
|
172
|
+
process.exit(runGenerationLaws(process.cwd(), parseLawsArgv(process.argv.slice(2))));
|
|
173
|
+
}
|
package/src/roots.ts
CHANGED
|
@@ -218,3 +218,129 @@ export function requireForgeState(from: string, env: NodeJS.ProcessEnv = process
|
|
|
218
218
|
` Source the generation's ${MARKER} first, or run this from inside its checkout.`,
|
|
219
219
|
);
|
|
220
220
|
}
|
|
221
|
+
|
|
222
|
+
// ── A printed command that RUNS when it is pasted ─────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Where a repo is, as a person's shell should reach it: the directory, and the `forge.env` that
|
|
226
|
+
* makes `$FORGE` real in the reader's shell. `forgeEnv` is non-null **exactly when** `dir` is the
|
|
227
|
+
* repo's home inside a located generation, so the two can never describe different places.
|
|
228
|
+
*/
|
|
229
|
+
export interface RepoLocation {
|
|
230
|
+
readonly dir: string;
|
|
231
|
+
readonly forgeEnv: string | null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* A repo's place inside a generation — `apps/auth`, `libs/cwip`, `autopilot` — checked, never
|
|
236
|
+
* trusted: relative, no `..`, no shell metacharacter.
|
|
237
|
+
*
|
|
238
|
+
* 🔴 The segment is always the CALLER's. This library is published and may not know a repo's
|
|
239
|
+
* name any more than a generation's (the header's third 🔴), and a segment that could carry
|
|
240
|
+
* `"` or `$` would be interpolated into a line a person pastes into a shell.
|
|
241
|
+
*/
|
|
242
|
+
function checkedSegment(segment: string): string {
|
|
243
|
+
const clean = segment.trim().replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
244
|
+
if (!/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(clean) || clean.split("/").some((part) => part === ".." || part === ".")) {
|
|
245
|
+
throw new Error(`not a repo segment: ${JSON.stringify(segment)} — pass the repo's place in the generation, e.g. "apps/auth"`);
|
|
246
|
+
}
|
|
247
|
+
return clean;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** `"…"` for a POSIX shell: the four characters a double-quoted string still interprets are escaped. */
|
|
251
|
+
function shellDoubleQuoted(value: string): string {
|
|
252
|
+
return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The generation root a printed command should name — `$FORGE` when the directory it names
|
|
257
|
+
* really holds a `forge.env`, otherwise the walk up from `from` (worktree-aware, see
|
|
258
|
+
* {@link findForgeRoot}).
|
|
259
|
+
*
|
|
260
|
+
* 🔴 **Verified, never trusted.** A `$FORGE` pointing at a directory with no `forge.env` is a
|
|
261
|
+
* stale export from a shell that outlived a move, and honouring it re-creates the exact defect
|
|
262
|
+
* {@link repoCd} exists to end: a command naming a directory that is not there.
|
|
263
|
+
* {@link forgeCodeRoot} deliberately keeps its older contract (the variable wins unchecked); this
|
|
264
|
+
* is the one for text a person will paste.
|
|
265
|
+
*/
|
|
266
|
+
export function verifiedForgeRoot(from: string, env: NodeJS.ProcessEnv = process.env): string | null {
|
|
267
|
+
const named = env.FORGE?.trim();
|
|
268
|
+
if (named && existsSync(join(named, MARKER))) return resolve(named);
|
|
269
|
+
return findForgeRoot(from);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* {@link RepoLocation} for the repo at `segment`, as seen from `from` (pass `import.meta.dir`).
|
|
274
|
+
*
|
|
275
|
+
* `from` is REQUIRED: a default here would be this library's own directory — inside somebody's
|
|
276
|
+
* `node_modules` — and the fallback would print a `cd` into it.
|
|
277
|
+
*
|
|
278
|
+
* Never throws on a location (only on a malformed segment), and `dir` always EXISTS: the repo's
|
|
279
|
+
* home in the located generation, else the checkout `from` is in (a clone, or a checkout
|
|
280
|
+
* somebody moved — the one thing always true there is that a command run from it runs against
|
|
281
|
+
* this code), else `from` itself.
|
|
282
|
+
*/
|
|
283
|
+
export function locateRepo(segment: string, from: string, env: NodeJS.ProcessEnv = process.env): RepoLocation {
|
|
284
|
+
const clean = checkedSegment(segment);
|
|
285
|
+
const forge = verifiedForgeRoot(from, env);
|
|
286
|
+
if (forge) {
|
|
287
|
+
const home = join(forge, clean);
|
|
288
|
+
if (existsSync(home)) return { dir: home, forgeEnv: join(forge, MARKER) };
|
|
289
|
+
}
|
|
290
|
+
return { dir: findPackageRoot(from) ?? resolve(from), forgeEnv: null };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The prefix every command a repo prints should carry — pasteable into any shell, from any
|
|
295
|
+
* directory. The owner's standing rule: *"Commands you show me must RUN. I paste them into a
|
|
296
|
+
* terminal whose directory you do not know."*
|
|
297
|
+
*
|
|
298
|
+
* With a generation located it sources `forge.env` FIRST and then goes through the variable, so
|
|
299
|
+
* the reader's shell ends up with `$FORGE`/`$FORGE_STATE` set the way every CLI here expects and
|
|
300
|
+
* the line shows where the value comes from instead of asserting a path. Without one it is the
|
|
301
|
+
* plain `cd` into a directory that exists. It never spells `~/`: `check-paths` reds a `cd ~/…`
|
|
302
|
+
* in source, and cannot see one composed at runtime — which is why this is the one place that
|
|
303
|
+
* composes it.
|
|
304
|
+
*
|
|
305
|
+
* Measured 2026-09-19 by `check-paths`'s `deadCd` rule: twelve printed commands in five repos
|
|
306
|
+
* `cd`'d into directories two generations gone, each read at a moment something was already
|
|
307
|
+
* wrong. Four repos then wrote this function; `apps/binary-server/src/repoCommand.ts` (the
|
|
308
|
+
* original), `apps/auth/src/kit/repoCommand.ts`, and `scripts/forgeRoot.ts` in `family` and `roms`.
|
|
309
|
+
*/
|
|
310
|
+
export function repoCd(segment: string, from: string, env: NodeJS.ProcessEnv = process.env): string {
|
|
311
|
+
const clean = checkedSegment(segment);
|
|
312
|
+
const { dir, forgeEnv } = locateRepo(clean, from, env);
|
|
313
|
+
return forgeEnv ? `source ${shellDoubleQuoted(forgeEnv)} && cd "$FORGE/${clean}"` : `cd ${shellDoubleQuoted(dir)}`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** {@link repoCd} joined to `rest` with `&&`, so a failed `cd` stops the line. */
|
|
317
|
+
export function repoCommand(segment: string, rest: string, from: string, env: NodeJS.ProcessEnv = process.env): string {
|
|
318
|
+
return `${repoCd(segment, from, env)} && ${rest}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* The same prefix aimed at ANOTHER repo in this generation — what a guard shared by several
|
|
323
|
+
* apps needs when it is told the other app's place and nothing else (`apps/auth`'s
|
|
324
|
+
* artifact-boot refusal, which names the app that would not boot).
|
|
325
|
+
*
|
|
326
|
+
* Falls back to {@link repoCd} for `selfSegment` when `segment` is not in the located
|
|
327
|
+
* generation: a line aimed at the wrong-but-real repo is runnable, and a line naming a
|
|
328
|
+
* directory that is not there is the whole defect.
|
|
329
|
+
*/
|
|
330
|
+
export function otherRepoCd(segment: string, selfSegment: string, from: string, env: NodeJS.ProcessEnv = process.env): string {
|
|
331
|
+
const clean = checkedSegment(segment);
|
|
332
|
+
const forge = verifiedForgeRoot(from, env);
|
|
333
|
+
if (forge && existsSync(join(forge, clean))) return `source ${shellDoubleQuoted(join(forge, MARKER))} && cd "$FORGE/${clean}"`;
|
|
334
|
+
return repoCd(selfSegment, from, env);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** {@link otherRepoCd} joined to `rest` with `&&`. */
|
|
338
|
+
export function otherRepoCommand(
|
|
339
|
+
segment: string,
|
|
340
|
+
selfSegment: string,
|
|
341
|
+
rest: string,
|
|
342
|
+
from: string,
|
|
343
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
344
|
+
): string {
|
|
345
|
+
return `${otherRepoCd(segment, selfSegment, from, env)} && ${rest}`;
|
|
346
|
+
}
|