cursedops 0.22.0 → 0.23.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/stageLook.ts +141 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedops",
3
- "version": "0.22.0",
3
+ "version": "0.23.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 check that a launchd job imports only what the LOCKED versions of its packages export (a lib-preview overlay's extra subpath or name is a job that dies at the next forced reinstall), 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/stageLook.ts CHANGED
@@ -13,6 +13,11 @@
13
13
  * to binary-server too they turn every media fetch into a CORS preflight it refuses.
14
14
  * - **Sign-in is the stage's own door**: `grantStageOwner` (auth-stage's emailed second factor) and
15
15
  * the app's `/api/auth/sso/callback?grant=…`. Never production's accounts service.
16
+ * - **An app with no sign-in is looked at signed out** (`signedIn: false`, `--signed-out`), and a stage
17
+ * with no custom domain at its own `origin` (`--origin https://<app>-stage.<account>.workers.dev`).
18
+ * patterns has neither (forge task 3918): asking auth-stage for its grant answered 400, so the one
19
+ * command could not look at it at all. `tools/stage-look.ts` decides both from auth's registry and
20
+ * the app's `wrangler.jsonc`, so nobody has to know.
16
21
  * - **A reaped binary-server sandbox is SAID, not screenshotted.** Its images answer 404, or 503 with
17
22
  * `x-bs-edge-cache: STAGE-ORIGIN-DOWN`; {@link brokenMediaOf} names them, and the CLI exits 1
18
23
  * with the recovery (`apps/binary-server` → `bun run sandbox:up`, then upload again).
@@ -22,7 +27,7 @@
22
27
  *
23
28
  * CLI, from an app's directory (or `bun $FORGE/tools/stage-look.ts <app> …` from anywhere):
24
29
  *
25
- * stage-look <app> [path] [--out <dir>] → <dir>/stage-<app>-390.png, …-1440.png
30
+ * stage-look <app> [path] [--out <dir>] [--signed-out] [--origin <url>] → <dir>/stage-<app>-390.png, …-1440.png
26
31
  */
27
32
  import { spawnSync } from "node:child_process";
28
33
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
@@ -86,6 +91,70 @@ export function brokenMediaOf(r: LookResponse): string | null {
86
91
  return null;
87
92
  }
88
93
 
94
+ /**
95
+ * A read of the page's OWN that the stage failed — a same-origin `/api/` answer of 5xx — as
96
+ * `<status> <path>`. A look shot over one is a picture of an error card or a skeleton, not of the
97
+ * change (forge task 3914: station's `#/deploys` read 503 "station's Mac is not connected", because a
98
+ * relay stage has a Mac end only while its `worker:stage-walk` runs).
99
+ */
100
+ export function failedReadOf(r: Pick<LookResponse, "url" | "status">, origin: string): string | null {
101
+ const url = new URL(r.url());
102
+ if (url.origin !== origin || !url.pathname.startsWith("/api/") || r.status() < 500) return null;
103
+ return `${r.status()} ${url.pathname}`;
104
+ }
105
+
106
+ /** How long the network may stay busy before the look goes on without it — a player streams for good. */
107
+ export const IDLE_CEILING_MS = 15_000;
108
+ /** How long a page's pending regions (`aria-busy="true"`, cursedbelt's skeletons) may read before the look gives up. */
109
+ export const SETTLE_CEILING_MS = 90_000;
110
+
111
+ /** The slice of a playwright `Page` that {@link settle} drives. */
112
+ export interface SettlePage {
113
+ waitForLoadState(state: "networkidle", options: { timeout: number }): Promise<unknown>;
114
+ evaluate<T>(fn: () => T): Promise<T>;
115
+ }
116
+
117
+ export interface Settled {
118
+ /** False when the network never went quiet inside {@link IDLE_CEILING_MS} — said, never fatal. */
119
+ idle: boolean;
120
+ /** The pending regions still reading at the ceiling, by their text; empty when the page settled. */
121
+ busy: string[];
122
+ ms: number;
123
+ }
124
+
125
+ /**
126
+ * Wait until the page has DRAWN, not until the network is quiet: both of the looks that went wrong
127
+ * waited for `networkidle` alone. music's never came (its player keeps a stream open), so the look threw;
128
+ * station's `#/deploys` came while its read was still in flight, so the look shot four skeleton tiles
129
+ * and "Reading the apps' deploys…" (forge task 3914). So: `networkidle` with a ceiling, then every
130
+ * `aria-busy="true"` region gone on two reads in a row, up to {@link SETTLE_CEILING_MS} — and what is
131
+ * still busy then is SAID, so a look of a loading state never reads as a look of the page.
132
+ */
133
+ export async function settle(
134
+ page: SettlePage,
135
+ options: { idleMs?: number; settleMs?: number; pollMs?: number; sleep?: (ms: number) => Promise<unknown>; now?: () => number } = {},
136
+ ): Promise<Settled> {
137
+ const { idleMs = IDLE_CEILING_MS, settleMs = SETTLE_CEILING_MS, pollMs = 500, sleep = Bun.sleep, now = Date.now } = options;
138
+ const start = now();
139
+ const idle = await page.waitForLoadState("networkidle", { timeout: idleMs }).then(
140
+ () => true,
141
+ () => false,
142
+ );
143
+ const pending = () =>
144
+ page.evaluate(() =>
145
+ [...document.querySelectorAll('[aria-busy="true"]')].map((el) => (el.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 80) || el.tagName.toLowerCase()),
146
+ );
147
+ let quiet = 0;
148
+ let busy: string[] = [];
149
+ while (true) {
150
+ busy = await pending();
151
+ quiet = busy.length === 0 ? quiet + 1 : 0;
152
+ if (quiet >= 2) return { idle, busy: [], ms: now() - start };
153
+ if (now() - start >= settleMs) return { idle, busy, ms: now() - start };
154
+ await sleep(pollMs);
155
+ }
156
+ }
157
+
89
158
  /** The slices of playwright this drives. Structural, so cursedops needs no dependency on it. */
90
159
  interface LookRoute {
91
160
  request(): { headers(): Record<string, string> };
@@ -117,6 +186,10 @@ export interface StageLookSpec {
117
186
  state?: string;
118
187
  /** playwright's `chromium`; default: `playwright` resolved from the working directory. */
119
188
  chromium?: LookChromium;
189
+ /** False for an app with no sign-in: no grant is asked for and no callback is opened. Default true. */
190
+ signedIn?: boolean;
191
+ /** The stage's origin when it is not {@link stageOrigin} — a stage on workers.dev alone. */
192
+ origin?: string;
120
193
  /** Extra Chromium flags — collections' audio daemon, say. */
121
194
  args?: string[];
122
195
  /** Seams for the suite. */
@@ -140,14 +213,14 @@ async function defaultChromium(): Promise<LookChromium> {
140
213
  }
141
214
 
142
215
  /**
143
- * A page on `<app>-stage`, signed in as its owner, with Access headers on the stage's requests only.
216
+ * A page on `<app>-stage`, signed in as its owner (unless `signedIn: false`), with Access headers on the stage's requests only.
144
217
  * `Page` is the caller's playwright `Page` — the look builds it, so the cast is the caller's word.
145
218
  */
146
219
  export async function openStagePage<Page = LookPage>(spec: StageLookSpec): Promise<StageLook<Page>> {
147
220
  const cwd = process.cwd();
148
221
  const forge = spec.forge ?? forgeCodeRoot(cwd);
149
222
  if (!forge) throw new Error(`no forge.env above ${cwd} — run the look from inside the generation`);
150
- const origin = stageOrigin(spec.app);
223
+ const origin = spec.origin ? new URL(spec.origin).origin : stageOrigin(spec.app);
151
224
  const access = spec.headers ?? accessHeaders(spec.state ?? requireForgeState(cwd));
152
225
  const chromium = spec.chromium ?? (await defaultChromium());
153
226
  const browser = await chromium.launch({ args: [...(spec.args ?? []), ...pinnedResolverArgs(new URL(origin).hostname, spec.dig)] });
@@ -164,10 +237,12 @@ export async function openStagePage<Page = LookPage>(spec: StageLookSpec): Promi
164
237
  const b = brokenMediaOf(r);
165
238
  if (b) broken.push(b);
166
239
  });
167
- const granted = (spec.grant ?? (() => grantStageOwner({ forge, app: spec.app })))();
168
- if (!granted.ok) throw new Error(`auth-stage gave no grant for ${spec.app} — its output above says why`);
169
- const back = await page.goto(`${origin}/api/auth/sso/callback?grant=${encodeURIComponent(granted.grant)}`, { waitUntil: "load" });
170
- if (back && back.status() >= 400) throw new Error(`${origin}'s sign-in callback answered ${back.status()}`);
240
+ if (spec.signedIn !== false) {
241
+ const granted = (spec.grant ?? (() => grantStageOwner({ forge, app: spec.app })))();
242
+ if (!granted.ok) throw new Error(`auth-stage gave no grant for ${spec.app} — its output above says why (an app with no sign-in is looked at with --signed-out)`);
243
+ const back = await page.goto(`${origin}/api/auth/sso/callback?grant=${encodeURIComponent(granted.grant)}`, { waitUntil: "load" });
244
+ if (back && back.status() >= 400) throw new Error(`${origin}'s sign-in callback answered ${back.status()}`);
245
+ }
171
246
  return { page: page as Page, origin, brokenMedia: () => [...broken], close: () => browser.close() };
172
247
  } catch (e) {
173
248
  await browser.close();
@@ -175,27 +250,76 @@ export async function openStagePage<Page = LookPage>(spec: StageLookSpec): Promi
175
250
  }
176
251
  }
177
252
 
253
+ export interface LookArgs {
254
+ app: string;
255
+ path: string;
256
+ out?: string;
257
+ signedIn: boolean;
258
+ origin?: string;
259
+ }
260
+
261
+ /** The CLI's words, or what is wrong with them. A flag that takes a value and has none is a usage fault, never a path. */
262
+ export function parseLookArgs(argv: readonly string[]): LookArgs | { usage: string } {
263
+ const plain: string[] = [];
264
+ let out: string | undefined;
265
+ let origin: string | undefined;
266
+ let signedIn = true;
267
+ for (let i = 0; i < argv.length; i++) {
268
+ const a = argv[i] as string;
269
+ if (a === "--signed-out") signedIn = false;
270
+ else if (a === "--out" || a === "--origin") {
271
+ const v = argv[++i];
272
+ if (!v || v.startsWith("--")) return { usage: `${a} needs a value` };
273
+ if (a === "--out") out = v;
274
+ else if (!URL.canParse(v) || !/^https?:$/.test(new URL(v).protocol)) return { usage: `--origin ${v} is not an http(s) URL` };
275
+ else origin = new URL(v).origin;
276
+ } else if (a.startsWith("--")) return { usage: `unknown flag ${a}` };
277
+ else plain.push(a);
278
+ }
279
+ const [app, path = "/", extra] = plain;
280
+ if (!app) return { usage: "which app?" };
281
+ if (extra) return { usage: `one path at a time (got ${path} and ${extra})` };
282
+ return { app, path, signedIn, ...(out ? { out } : {}), ...(origin ? { origin } : {}) };
283
+ }
284
+
178
285
  /** Shoot `path` at 390 and 1440. Exit 1 when the page or its media is broken, so a look never passes on broken tiles. */
179
286
  async function main(argv: string[]): Promise<number> {
180
- const outAt = argv.indexOf("--out");
181
- const out = outAt >= 0 ? argv[outAt + 1] : undefined;
182
- const [app, path = "/"] = outAt >= 0 ? argv.filter((_, i) => i !== outAt && i !== outAt + 1) : argv;
183
- if (!app || (outAt >= 0 && !out)) {
184
- console.error("usage: stage-look <app> [path] [--out <dir>] (run in the app's directory, or `bun $FORGE/tools/stage-look.ts <app> …`)");
287
+ const parsed = parseLookArgs(argv);
288
+ if ("usage" in parsed) {
289
+ console.error(`${parsed.usage}\nusage: stage-look <app> [path] [--out <dir>] [--signed-out] [--origin <url>] (run in the app's directory, or \`bun $FORGE/tools/stage-look.ts <app> …\`)`);
185
290
  return 2;
186
291
  }
292
+ const { app, path, out, signedIn, origin } = parsed;
187
293
  const dir = out ?? join(process.env.FORGE_BATCH_TMP ?? requireForgeState(process.cwd()), "stage-look");
188
294
  mkdirSync(dir, { recursive: true });
189
295
  let bad = false;
190
296
  for (const width of [390, 1440]) {
191
- type ShotPage = LookPage & { screenshot(o: { path: string; fullPage?: boolean }): Promise<unknown>; url(): string };
192
- const look = await openStagePage<ShotPage>({ app, width });
297
+ type ShotPage = LookPage & SettlePage & { screenshot(o: { path: string; fullPage?: boolean }): Promise<unknown>; url(): string };
298
+ const look = await openStagePage<ShotPage>({ app, width, signedIn, ...(origin ? { origin } : {}) });
193
299
  try {
194
- const res = await look.page.goto(`${look.origin}${path}`, { waitUntil: "networkidle" });
300
+ const failed: string[] = [];
301
+ look.page.on("response", (r) => {
302
+ const f = failedReadOf(r, look.origin);
303
+ if (f && !failed.includes(f)) failed.push(f);
304
+ });
305
+ const res = await look.page.goto(`${look.origin}${path}`, { waitUntil: "load" });
306
+ const settled = await settle(look.page);
195
307
  const shot = join(dir, `stage-${app}-${width}.png`);
196
308
  await look.page.screenshot({ path: shot });
197
- console.log(`${width}: ${res?.status() ?? "?"} ${look.page.url()} → ${shot}`);
198
- if (!res || res.status() >= 400) bad = true;
309
+ // 🔴 `null` is not a failure: the sign-in callback already landed on `/`, so a `/#/deploys` is a
310
+ // same-document hash change, which playwright answers with no response (forge task 3914's `?`).
311
+ console.log(`${width}: ${res?.status() ?? "same document"} ${look.page.url()} → ${shot} (drawn in ${(settled.ms / 1000).toFixed(1)} s)`);
312
+ if (res && res.status() >= 400) bad = true;
313
+ if (!settled.idle) console.log(` · the network never went quiet in ${IDLE_CEILING_MS / 1000} s (a stream or a poll) — the look waited on the page's own pending regions instead`);
314
+ if (settled.busy.length) {
315
+ bad = true;
316
+ console.error(` 🔴 still READING after ${SETTLE_CEILING_MS / 1000} s — this shot is the loading state, not the page: ${settled.busy.slice(0, 3).join(" · ")}`);
317
+ }
318
+ if (failed.length) {
319
+ bad = true;
320
+ console.error(` 🔴 the stage failed ${failed.length} of the page's own read(s) — the shot shows what it does without them: ${failed.slice(0, 5).join(", ")}`);
321
+ console.error(" a 503 on a relay app's stage is often its Mac end being down — that end is up only while its `worker:stage-walk` runs");
322
+ }
199
323
  const broken = look.brokenMedia();
200
324
  if (broken.length) {
201
325
  bad = true;