waygraph 0.15.23 → 0.15.25

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 (65) hide show
  1. package/dist/cli/discover.d.ts +40 -0
  2. package/dist/cli/discover.js +148 -0
  3. package/dist/cli/flags.d.ts +91 -0
  4. package/dist/cli/flags.js +442 -0
  5. package/dist/cli/run-chain.d.ts +8 -0
  6. package/dist/cli/run-chain.js +67 -0
  7. package/dist/cli/usage.d.ts +4 -0
  8. package/dist/cli/usage.js +236 -0
  9. package/dist/cli/util.d.ts +4 -0
  10. package/dist/cli/util.js +24 -0
  11. package/dist/cli.js +45 -2448
  12. package/dist/commands/agent-dive.d.ts +1 -0
  13. package/dist/commands/agent-dive.js +52 -0
  14. package/dist/commands/check.d.ts +4 -0
  15. package/dist/commands/check.js +192 -0
  16. package/dist/commands/init.d.ts +1 -0
  17. package/dist/commands/init.js +54 -0
  18. package/dist/commands/list.d.ts +2 -0
  19. package/dist/commands/list.js +67 -0
  20. package/dist/commands/map.d.ts +1 -0
  21. package/dist/commands/map.js +29 -0
  22. package/dist/commands/run.d.ts +3 -0
  23. package/dist/commands/run.js +104 -0
  24. package/dist/commands/session.d.ts +2 -0
  25. package/dist/commands/session.js +600 -0
  26. package/dist/commands/test.d.ts +1 -0
  27. package/dist/commands/test.js +33 -0
  28. package/dist/commands/traverse.d.ts +1 -0
  29. package/dist/commands/traverse.js +177 -0
  30. package/dist/commands/try.d.ts +1 -0
  31. package/dist/commands/try.js +298 -0
  32. package/dist/engine/blocks/effect.d.ts +41 -0
  33. package/dist/engine/blocks/effect.js +23 -0
  34. package/dist/engine/blocks/method.d.ts +90 -0
  35. package/dist/engine/blocks/method.js +70 -0
  36. package/dist/engine/blocks/nav.d.ts +138 -0
  37. package/dist/engine/blocks/nav.js +110 -0
  38. package/dist/engine/blocks/page.d.ts +59 -0
  39. package/dist/engine/blocks/page.js +91 -0
  40. package/dist/engine/compose.d.ts +75 -0
  41. package/dist/engine/compose.js +76 -0
  42. package/dist/engine/config.d.ts +55 -0
  43. package/dist/engine/config.js +17 -0
  44. package/dist/engine/core.d.ts +92 -0
  45. package/dist/engine/core.js +256 -0
  46. package/dist/engine/engine-class.d.ts +289 -0
  47. package/dist/engine/engine-class.js +257 -0
  48. package/dist/engine/flow.d.ts +266 -0
  49. package/dist/engine/flow.js +387 -0
  50. package/dist/engine/locate.d.ts +26 -0
  51. package/dist/engine/locate.js +62 -0
  52. package/dist/engine/run-graph.d.ts +147 -0
  53. package/dist/engine/run-graph.js +223 -0
  54. package/dist/engine.d.ts +11 -1224
  55. package/dist/engine.js +12 -1550
  56. package/examples/saucedemo/.waygraph-auto/0ac58118.log +0 -0
  57. package/examples/saucedemo/.waygraph-auto/309c107f.log +0 -0
  58. package/examples/saucedemo/.waygraph-auto/3ab7c7bd.log +44 -0
  59. package/examples/saucedemo/.waygraph-auto/45864828.log +0 -0
  60. package/examples/saucedemo/.waygraph-auto/95bd8371.log +0 -0
  61. package/examples/saucedemo/.waygraph-auto/bcb9e31a.log +0 -0
  62. package/examples/saucedemo/.waygraph-auto/cd3e528c.log +0 -0
  63. package/examples/saucedemo/.waygraph-auto/dce1535d.log +0 -0
  64. package/examples/saucedemo/.waygraph-auto/e280bf20.log +0 -0
  65. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -14,1411 +14,20 @@
14
14
  *
15
15
  * "project" defaults to cwd. Flags beat WAYGRAPH_* env.
16
16
  */
17
- import { existsSync, readdirSync, readFileSync, writeFileSync, rmSync, cpSync, mkdirSync, mkdtempSync, statSync } from "node:fs";
18
- import { resolve, relative, join, basename, dirname } from "node:path";
19
- import { tmpdir } from "node:os";
20
- import { fileURLToPath } from "node:url";
21
- import { spawn } from "node:child_process";
22
- import { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
23
- import { checkMap } from "./map-check.js";
24
- import { runAutoExplore } from "./auto-explore-run.js";
25
- import { pilotStart, pilotAttach } from "./pilot.js";
26
- import { browserStart, listBrowserSessions, stopBrowserSession, stopAllBrowserSessions, } from "./browser.js";
27
- import { resolveInjectRoots } from "./block-inject.js";
28
- import { collectPracticeWarnings, practiceKindLabel } from "./practices-check.js";
29
- import { spawnDetachedSession, requestSession, runAttachLoop, runAutoServeCommand, resolveSessionMeta, } from "./auto-session-ipc.js";
30
- import { runAgentDive, printSkillIndex, readSkillMarkdown, SKILL_FLAG_MAP, } from "./agent-dive.js";
31
- import { runTraverse } from "./traverse-run.js";
32
- import { parseMinEdgeCoverage } from "./traverse-coverage.js";
33
- import { isFileSelectToken, parseBlocksSelect, } from "./blocks-select.js";
34
- import { parseHighlightShorthand } from "./highlight-shorthand.js";
35
- // ---------------------------------------------------------------------------
36
- // Filesystem
37
- // ---------------------------------------------------------------------------
38
- function walkDir(dir, pattern) {
39
- const results = [];
40
- let entries;
41
- try {
42
- entries = readdirSync(dir, { withFileTypes: true });
43
- }
44
- catch {
45
- return results;
46
- }
47
- for (const entry of entries) {
48
- const full = join(dir, entry.name);
49
- if (entry.isDirectory()) {
50
- if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git")
51
- continue;
52
- results.push(...walkDir(full, pattern));
53
- }
54
- else if (pattern.test(entry.name)) {
55
- results.push(full);
56
- }
57
- }
58
- return results;
59
- }
60
- function discoverFlows(projectDir) {
61
- return walkDir(projectDir, /\.flow\.ts$/);
62
- }
63
- // ---------------------------------------------------------------------------
64
- // Static analysis for `list`/`nav` (no import, regex only)
65
- // ---------------------------------------------------------------------------
66
- function extractFlowNames(filePath) {
67
- const src = readFileSync(filePath, "utf-8");
68
- const names = [];
69
- const re = /export\s+const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*[=:]/g;
70
- let m;
71
- while ((m = re.exec(src)) !== null) {
72
- const name = m[1];
73
- if (name !== undefined)
74
- names.push(name);
75
- }
76
- return names;
77
- }
78
- /** `LoginBlock` -> `"login"`; `OverviewMetricsBlock` -> `"overview-metrics"`. */
79
- function blockExportToLabel(exportId) {
80
- const base = exportId.replace(/Block$/, "");
81
- return base
82
- .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
83
- .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
84
- .toLowerCase();
85
- }
86
- /** Every `...Block` identifier a `.flow.ts` file imports, in source order. */
87
- function parseBlockImports(filePath) {
88
- const src = readFileSync(filePath, "utf-8");
89
- const names = [];
90
- const re = /import\s*\{([^}]*)\}\s*from\s*["'][^"']*\.block(?:\.js)?["']/g;
91
- let m;
92
- while ((m = re.exec(src)) !== null) {
93
- for (const part of m[1].split(",")) {
94
- const name = part.trim().split(/\s+as\s+/)[0]?.trim();
95
- if (name)
96
- names.push(name);
97
- }
98
- }
99
- return names;
100
- }
101
- /** Every `page.goto("...")` string literal in a file, in source order. */
102
- function extractGotos(filePath) {
103
- const src = readFileSync(filePath, "utf-8");
104
- const results = [];
105
- const re = /\.goto\(\s*["'`]([^"'`]*)["'`]/g;
106
- let m;
107
- while ((m = re.exec(src)) !== null) {
108
- const url = m[1];
109
- if (url !== undefined)
110
- results.push(url);
111
- }
112
- return results;
113
- }
114
- /** Every `Trait.url({ pathname: "..." })` target in a file, in source order. */
115
- function extractUrlTraits(filePath) {
116
- const src = readFileSync(filePath, "utf-8");
117
- const results = [];
118
- const re = /Trait\.url\(\s*\{[^}]*pathname:\s*["'`]([^"'`]*)["'`]/g;
119
- let m;
120
- while ((m = re.exec(src)) !== null) {
121
- const url = m[1];
122
- if (url !== undefined)
123
- results.push(url);
124
- }
125
- return results;
126
- }
127
- // ---------------------------------------------------------------------------
128
- // `list`
129
- // ---------------------------------------------------------------------------
130
- function listCommand(projectDir) {
131
- const files = discoverFlows(projectDir);
132
- if (files.length === 0) {
133
- console.log(`waygraph: no flows found under ${projectDir}`);
134
- return;
135
- }
136
- for (const file of files) {
137
- const rel = relative(projectDir, file);
138
- for (const name of extractFlowNames(file)) {
139
- console.log(`${rel} ${name}`);
140
- }
141
- }
142
- }
143
- // ---------------------------------------------------------------------------
144
- // `nav`
145
- // ---------------------------------------------------------------------------
146
- function navCommand(projectDir, flowName) {
147
- const files = discoverFlows(projectDir);
148
- const targets = flowName
149
- ? files.filter((f) => extractFlowNames(f).includes(flowName))
150
- : files;
151
- if (targets.length === 0) {
152
- console.error(flowName
153
- ? `waygraph nav: no flow named "${flowName}" found under ${projectDir}`
154
- : `waygraph: no flows found under ${projectDir}`);
155
- process.exitCode = 1;
156
- return;
157
- }
158
- for (const file of targets) {
159
- for (const name of extractFlowNames(file)) {
160
- console.log(`${name} (${relative(projectDir, file)})`);
161
- const blockDir = projectDir;
162
- for (const blockExport of parseBlockImports(file)) {
163
- console.log(` -> ${blockExportToLabel(blockExport)}`);
164
- const blockFile = walkDir(blockDir, /\.block\.ts$/).find((f) => readFileSync(f, "utf-8").includes(`export const ${blockExport}`));
165
- if (!blockFile)
166
- continue;
167
- for (const url of extractGotos(blockFile))
168
- console.log(` goto ${url}`);
169
- for (const url of extractUrlTraits(blockFile))
170
- console.log(` verify url ~ ${url}`);
171
- }
172
- }
173
- }
174
- }
175
- // ---------------------------------------------------------------------------
176
- // Dynamic import + validation
177
- // ---------------------------------------------------------------------------
178
- async function importModule(filePath) {
179
- const url = new URL(`file://${resolve(filePath)}`);
180
- return (await import(url.href));
181
- }
182
- /** Kept as a named alias - `run`/`validate` below were written against this name first. */
183
- const importFlowFile = importModule;
184
- function isFlowLike(val) {
185
- if (val === null || typeof val !== "object")
186
- return false;
187
- const obj = val;
188
- return typeof obj.run === "function";
189
- }
190
- // ---------------------------------------------------------------------------
191
- // `chain`
192
- // ---------------------------------------------------------------------------
193
- /**
194
- * `chain` cannot resolve Blocks by importing them straight into THIS process: this CLI's own
195
- * `waygraph` (this same package) already loaded - and its engine imports `@playwright/test`.
196
- * A target project installs its OWN copy of both `waygraph` and `@playwright/test`, so importing
197
- * one of its `.block.ts` files pulls in a second, physically different Playwright - which
198
- * Playwright's own runtime refuses outright ("Requiring @playwright/test second time").
199
- *
200
- * The fix: never let the two copies share a process. The runner (real, typed-by-review code in
201
- * `src/runner/`, shipped as the `waygraph/runner` export) is started from a 2-line bootstrap
202
- * written into the TARGET project directory and run as its own child process, so
203
- * `waygraph/runner` - and therefore the engine, highlights and Playwright it uses - all resolve
204
- * from the target project's own install, exclusively. This CLI's own copy never loads there.
205
- */
206
- const CHAIN_RUNNER_BOOTSTRAP = `let mod;
207
- try {
208
- mod = await import("waygraph/runner");
209
- } catch (err) {
210
- console.error(
211
- "waygraph: the waygraph installed in this project has no runner export (needs >= 0.15.23). " +
212
- "Upgrade it: npm install waygraph@latest\\n" + (err && err.message ? err.message : String(err)),
213
- );
214
- process.exit(1);
215
- }
216
- mod.startRunner();
217
- `;
218
- /**
219
- * Runs the chain runner script as a child process rooted at `projectDir` -
220
- * see {@link CHAIN_RUNNER_BOOTSTRAP} for why this can't just be imported
221
- * in-process. `WAYGRAPH_BASE_URL` (relative `page.goto()` targets, like
222
- * every real zsign-all Block uses), `WAYGRAPH_HEADED=1`, and
223
- * `WAYGRAPH_SLOWMO=<ms>` pass straight through from this process's own env.
224
- */
225
- async function runChain(projectDir, spec) {
226
- const expanded = await expandSpecFlowFiles(projectDir, spec);
227
- if (process.env.WAYGRAPH_VIDEO && !process.env.WAYGRAPH_BASE_URL) {
228
- const resolved = resolveBaseUrl(projectDir);
229
- if (resolved)
230
- process.env.WAYGRAPH_BASE_URL = resolved;
231
- }
232
- const tsxEsm = import.meta.resolve("tsx/esm");
233
- const scriptPath = join(projectDir, `.waygraph-chain-${process.pid}.mjs`);
234
- writeFileSync(scriptPath, CHAIN_RUNNER_BOOTSTRAP);
235
- try {
236
- const code = await new Promise((res, rej) => {
237
- const child = spawn(process.execPath, ["--import", tsxEsm, scriptPath, projectDir, expanded], {
238
- stdio: "inherit",
239
- env: process.env,
240
- });
241
- child.on("error", rej);
242
- child.on("exit", (code) => res(code ?? 1));
243
- });
244
- if (code !== 0) {
245
- process.exitCode = code;
246
- }
247
- }
248
- finally {
249
- rmSync(scriptPath, { force: true });
250
- }
251
- }
252
- // ---------------------------------------------------------------------------
253
- // `try` - a bundled, self-contained showcase. No .flow.ts to write, no
254
- // scenario to design - copies a small real Flow (nav/action Blocks, Trait
255
- // verify, narrate()) into the target project and runs it in step mode
256
- // against a public demo site, so a curious dev sees the real overlay/panel
257
- // experience in under a minute, then gets pointed at the plain waygraph
258
- // source that produced it.
259
- // ---------------------------------------------------------------------------
260
- /** This package's own installed root - dist/cli.js -> .. */
261
- function packageRoot() {
262
- return resolve(dirname(fileURLToPath(import.meta.url)), "..");
263
- }
264
- /**
265
- * Runs a throwaway probe script rooted at `projectDir` - the exact same
266
- * directory the real demo files get copied into, so it resolves "waygraph"
267
- * and "@playwright/test" exactly as they will - and actually launches (then
268
- * immediately closes) chromium, a real check rather than a guess at whether
269
- * the browser's installed. Never leaves the probe script behind.
270
- */
271
- async function probeTryPrereqs(projectDir) {
272
- const tsxEsm = import.meta.resolve("tsx/esm");
273
- const probeScript = [
274
- "try {",
275
- ' await import("waygraph");',
276
- "} catch {",
277
- ' console.error("WAYGRAPH_TRY_MISSING_WAYGRAPH");',
278
- " process.exit(1);",
279
- "}",
280
- "let mod;",
281
- "try {",
282
- ' mod = await import("@playwright/test");',
283
- "} catch {",
284
- ' console.error("WAYGRAPH_TRY_MISSING_PLAYWRIGHT_TEST");',
285
- " process.exit(1);",
286
- "}",
287
- "try {",
288
- " const browser = await mod.chromium.launch({ headless: true });",
289
- " await browser.close();",
290
- "} catch (err) {",
291
- " if (String(err && err.message).includes(\"Executable doesn't exist\")) {",
292
- ' console.error("WAYGRAPH_TRY_MISSING_BROWSER");',
293
- " process.exit(1);",
294
- " }",
295
- " console.error(String((err && err.stack) || err));",
296
- " process.exit(1);",
297
- "}",
298
- ].join("\n");
299
- const probePath = join(projectDir, `.waygraph-try-probe-${process.pid}.mjs`);
300
- writeFileSync(probePath, probeScript);
301
- try {
302
- const result = await new Promise((res, rej) => {
303
- const child = spawn(process.execPath, ["--import", tsxEsm, probePath], {
304
- cwd: projectDir,
305
- stdio: ["ignore", "ignore", "pipe"],
306
- env: process.env,
307
- });
308
- let stderrBuf = "";
309
- child.stderr?.on("data", (d) => { stderrBuf += d.toString(); });
310
- child.on("error", rej);
311
- child.on("exit", (code) => res({ code: code ?? 1, stderr: stderrBuf }));
312
- });
313
- if (result.code === 0)
314
- return { ok: true };
315
- const stderr = result.stderr;
316
- if (stderr.includes("WAYGRAPH_TRY_MISSING_WAYGRAPH"))
317
- return { ok: false, missing: "waygraph" };
318
- if (stderr.includes("WAYGRAPH_TRY_MISSING_PLAYWRIGHT_TEST"))
319
- return { ok: false, missing: "playwright-test" };
320
- if (stderr.includes("WAYGRAPH_TRY_MISSING_BROWSER"))
321
- return { ok: false, missing: "browser" };
322
- return { ok: false, missing: "unknown", detail: stderr.trim() || `probe exited with code ${result.code}` };
323
- }
324
- finally {
325
- rmSync(probePath, { force: true });
326
- }
327
- }
328
- /** Spawns with inherited stdio (the real npm/playwright output, visible) and resolves to its exit code. */
329
- function runInherited(cmd, args, cwd) {
330
- return new Promise((res, rej) => {
331
- const child = spawn(cmd, args, { cwd, stdio: "inherit", env: process.env });
332
- child.on("error", rej);
333
- child.on("exit", (code) => res(code ?? 1));
334
- });
335
- }
336
- /** Installs deps + chromium inside an already-copied quickstart folder. */
337
- async function ensureDirPrereqs(dir, label) {
338
- let status = await probeTryPrereqs(dir);
339
- if (status.ok)
340
- return null;
341
- if (!existsSync(join(dir, "package.json"))) {
342
- return `${label}: no package.json in ${dir}`;
343
- }
344
- if (status.missing === "waygraph" || status.missing === "playwright-test" || status.missing === "unknown") {
345
- // Copied quickstart must not keep a workspace-tainted lockfile (breaks temp-dir npm install).
346
- rmSync(join(dir, "package-lock.json"), { force: true });
347
- console.log(`${label}: installing dependencies in ${dir} ...`);
348
- const installCode = await runInherited("npm", ["install"], dir);
349
- if (installCode !== 0) {
350
- return `npm install exited with code ${installCode} - see the output above.`;
351
- }
352
- status = await probeTryPrereqs(dir);
353
- if (status.ok)
354
- return null;
355
- if (status.missing === "unknown") {
356
- return formatProbeFailure(status.detail ?? "probe failed");
357
- }
358
- }
359
- console.log(`${label}: downloading Playwright's chromium browser ...`);
360
- const localPlaywrightBin = join(dir, "node_modules", ".bin", "playwright");
361
- const browserCode = existsSync(localPlaywrightBin)
362
- ? await runInherited(localPlaywrightBin, ["install", "chromium"], dir)
363
- : await runInherited("npx", ["--yes", "playwright", "install", "chromium"], dir);
364
- if (browserCode !== 0) {
365
- return `"playwright install chromium" exited with code ${browserCode} - see the output above.`;
366
- }
367
- status = await probeTryPrereqs(dir);
368
- if (status.ok)
369
- return null;
370
- return formatProbeFailure(status.detail ?? "prerequisites still aren't ready after attempting to install them.");
371
- }
372
- /** Playwright browser binary present but OS libs missing (common in Docker/Codespaces). */
373
- function formatProbeFailure(detail) {
374
- const needsDeps = /shared libraries|libnspr4|libnss3|libatk|cannot open shared object file/i.test(detail);
375
- const hint = needsDeps
376
- ? "\n\nChromium needs OS packages too - try:\n sudo npx playwright install-deps chromium\n" +
377
- "In cloud/CI without a display, record headless:\n npx waygraph try demo --video ./out --no-step"
378
- : "";
379
- return `couldn't verify prerequisites:\n${detail}${hint}`;
380
- }
381
- const TRY_DEMO_CHAIN = 'loginFlow({"saucedemo.credentials":{"username":"standard_user","password":"secret_sauce"}}) then ' +
382
- 'shopFlow({"saucedemo.selectedItem":{"id":"sauce-labs-backpack","name":"Sauce Labs Backpack"}}) then ' +
383
- 'viewerBlockedFlow({"saucedemo.credentials":{"username":"locked_out_user","password":"secret_sauce"}})';
384
- /**
385
- * Point a copied quickstart at this checkout's waygraph build (not npm registry).
386
- * Uses `npm pack` into destDir - a file: symlink to the dev tree can double-load
387
- * @playwright/test when sibling projects also install Playwright.
388
- */
389
- async function wireQuickstartToPackageRoot(destDir) {
390
- const root = packageRoot();
391
- const version = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version;
392
- const packCode = await runInherited("npm", ["pack", "--pack-destination", destDir, "--silent"], root);
393
- if (packCode !== 0) {
394
- throw new Error(`waygraph try demo: npm pack failed with exit code ${packCode}`);
395
- }
396
- const tgz = join(destDir, `waygraph-${version}.tgz`);
397
- const pkgPath = join(destDir, "package.json");
398
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
399
- if (!pkg.dependencies)
400
- pkg.dependencies = {};
401
- pkg.dependencies.waygraph = `file:${tgz}`;
402
- writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
403
- }
404
- /**
405
- * Copy templates/quickstart into a fresh OS temp dir and wire it to this
406
- * package build. Returns destDir, or null after setting exitCode on failure.
407
- */
408
- async function prepareTryQuickstart(label) {
409
- const destDir = mkdtempSync(join(tmpdir(), `${label}-`));
410
- cpSync(join(packageRoot(), "templates", "quickstart"), destDir, { recursive: true });
411
- rmSync(join(destDir, "node_modules"), { recursive: true, force: true });
412
- rmSync(join(destDir, "test-results"), { recursive: true, force: true });
413
- rmSync(join(destDir, "package-lock.json"), { force: true });
414
- try {
415
- await wireQuickstartToPackageRoot(destDir);
416
- }
417
- catch (err) {
418
- console.error(err instanceof Error ? err.message : String(err));
419
- process.exitCode = 1;
420
- return null;
421
- }
422
- const problem = await ensureDirPrereqs(destDir, label);
423
- if (problem) {
424
- console.error(`${label}: couldn't get ready.\n${problem}`);
425
- process.exitCode = 1;
426
- return null;
427
- }
428
- return destDir;
429
- }
430
- /**
431
- * `waygraph try demo` - copy a self-contained saucedemo project into a temp
432
- * dir (never the caller's cwd), run a chainFlow step demo (Sign In + Shop +
433
- * blocked Viewer), then run the headless Playwright test, and print where
434
- * everything lives.
435
- */
436
- async function runTryDemo() {
437
- const destDir = await prepareTryQuickstart("waygraph-try-demo");
438
- if (!destDir)
439
- return;
440
- process.env.WAYGRAPH_BASE_URL ??= "https://www.saucedemo.com";
441
- process.env.WAYGRAPH_STEP ??= "1";
442
- process.env.WAYGRAPH_AUTOPLAY ??= "0";
443
- // Explicit --no-step --video = unattended headless .webm only (the
444
- // documented cloud/CI recipe above) - nobody needs to watch a live
445
- // window for Playwright's own recordVideo to capture the right thing.
446
- // This must be decided BEFORE the headed default below, or the
447
- // unconditional "??=" there always wins and the documented recipe never
448
- // actually runs headless.
449
- const unattendedVideo = Boolean(process.env.WAYGRAPH_VIDEO) && process.env.WAYGRAPH_STEP === "0";
450
- if (unattendedVideo) {
451
- process.env.WAYGRAPH_AUTOPLAY ??= "1";
452
- process.env.WAYGRAPH_HEADED ??= "0";
453
- }
454
- else {
455
- // Default: headed stepper, manual Next. --video records that session
456
- // too (not a headless rush) as long as --step is still on.
457
- process.env.WAYGRAPH_HEADED ??= "1";
458
- }
459
- const videoTo = process.env.WAYGRAPH_VIDEO;
460
- console.log(videoTo
461
- ? "waygraph try demo: step-through + record (Sign In -> Shop & Checkout -> blocked Viewer login) - click Next; video -> " +
462
- (videoTo === "1" ? ".waygraph-videos/" : videoTo)
463
- : "waygraph try demo: step-through (Sign In -> Shop & Checkout -> blocked Viewer login) - click Next for each step ...");
464
- await runChain(destDir, TRY_DEMO_CHAIN);
465
- if (process.exitCode)
466
- return;
467
- console.log("\nwaygraph try demo: running headless chainFlow test ...");
468
- const testCode = await runInherited("npm", ["test"], destDir);
469
- if (testCode !== 0) {
470
- process.exitCode = testCode;
471
- return;
472
- }
473
- const testFile = join(destDir, "tests", "chain-flow.spec.ts");
474
- const loginFlowFile = join(destDir, "src", "flows", "login.flow.ts");
475
- const shopFlowFile = join(destDir, "src", "flows", "shop.flow.ts");
476
- const viewerBlockedFlowFile = join(destDir, "src", "flows", "viewer-blocked.flow.ts");
477
- console.log("\nwaygraph try demo: done.\n\n" +
478
- "Temp project (does not touch your cwd - lives under the OS temp dir):\n" +
479
- ` ${destDir}\n\n` +
480
- "Episodes you just watched (same Flow objects the test imports):\n" +
481
- ` ${loginFlowFile}\n` +
482
- ` ${shopFlowFile}\n` +
483
- ` ${viewerBlockedFlowFile}\n\n` +
484
- "Headless chainFlow test (automated - no clicking):\n" +
485
- ` ${testFile}\n\n` +
486
- "Run again in that temp folder:\n" +
487
- ` cd ${destDir}\n` +
488
- " npm run demo # step-through, manual Next (--step --no-autoplay)\n" +
489
- " npm run auto # interactive explore (Effect Add/Remove, MemNav Open details)\n" +
490
- " npm test # playwright chainFlow test\n\n" +
491
- "Permanent full example in this package: examples/saucedemo\n" +
492
- "Keep a permanent scaffold in your tree: waygraph init my-app\n");
493
- }
494
- const TRY_AUTO_DATA = '{"saucedemo.credentials":{"username":"standard_user","password":"secret_sauce"}}';
495
- /**
496
- * `waygraph try auto` / `try auto:cli` - temp Sauce Demo explore.
497
- * Default is the **headed** browser panel. Pass `auto:cli` or `--cli` for the
498
- * terminal menu (same rows).
499
- *
500
- * Runs as a child in the temp project so Block imports share that install's
501
- * `@playwright/test`. In-process explore from the outer CLI against a packed
502
- * `file:*.tgz` copy double-loads Playwright and silently yields 0 graph edges
503
- * ("LoginPage / No moves").
504
- */
505
- async function runTryAuto(opts = { cli: false }) {
506
- const destDir = await prepareTryQuickstart("waygraph-try-auto");
507
- if (!destDir)
508
- return;
509
- process.env.WAYGRAPH_BASE_URL ??= "https://www.saucedemo.com";
510
- const cli = opts.cli === true;
511
- console.log(cli
512
- ? "waygraph try auto: CLI explore on Sauce Demo (temp dir).\n" +
513
- " Creds pre-seeded (saucedemo.credentials).\n" +
514
- " Pick [1] submit-login, then inventory Add/Remove / Open details.\n" +
515
- " Type q to quit. Headed panel: waygraph try auto\n"
516
- : "waygraph try auto: headed explore on Sauce Demo (temp dir).\n" +
517
- " Creds pre-seeded. CLI menu: waygraph try auto:cli\n" +
518
- " Pick submit-login, then inventory menus. Quit from the panel.\n");
519
- const bin = join(destDir, "node_modules", ".bin", "waygraph");
520
- const args = ["auto"];
521
- if (cli)
522
- args.push("--cli");
523
- args.push("--data", TRY_AUTO_DATA);
524
- const code = await runInherited(bin, args, destDir);
525
- if (code !== 0)
526
- process.exitCode = code;
527
- console.log("\nwaygraph try auto: explorer closed.\n\n" +
528
- `Temp project:\n ${destDir}\n\n` +
529
- "Run again:\n" +
530
- ` cd ${destDir} && npm run auto\n` +
531
- ` cd ${destDir} && npm run auto:cli\n\n` +
532
- "In-package: examples/saucedemo\n" +
533
- "Docs: https://deviate-dv8.github.io/waygraph/auto.html\n");
534
- }
535
- async function validateFlows(projectDir) {
536
- const files = discoverFlows(projectDir);
537
- const results = [];
538
- for (const file of files) {
539
- try {
540
- const mod = await importFlowFile(file);
541
- for (const [exportName, exported] of Object.entries(mod)) {
542
- if (isFlowLike(exported)) {
543
- results.push({ file, name: exportName, valid: true });
544
- }
545
- }
546
- }
547
- catch (err) {
548
- const msg = err instanceof Error ? err.message : String(err);
549
- results.push({ file, name: basename(file, ".ts"), valid: false, error: msg });
550
- }
551
- }
552
- return results;
553
- }
554
- // ---------------------------------------------------------------------------
555
- // `check` - secondary/complementary to the ActionPage @deprecated warning
556
- // (src/types.ts). That's the primary, always-on signal an editor shows the
557
- // instant a regular Block's act() calls a navigation method; this command
558
- // sweeps a whole project in one shot for contexts with no editor watching
559
- // (CI, an autonomous agent writing Block files without a language server).
560
- // See openspec/specs/nav-block-and-check/spec.md for the full rationale.
561
- // ---------------------------------------------------------------------------
562
- function discoverBlocks(projectDir) {
563
- return walkDir(projectDir, /\.block\.ts$/);
564
- }
565
- function isBlockLike(val) {
566
- if (val === null || typeof val !== "object")
567
- return false;
568
- const obj = val;
569
- if (typeof obj.name !== "string")
570
- return false;
571
- const instruction = obj.instruction;
572
- return instruction !== null && typeof instruction === "object" && typeof instruction?.act === "function";
573
- }
574
- /** Set by `defineNavBlock` (non-enumerable) - see src/engine.ts. */
575
- function isNavBlockMarked(val) {
576
- return val.__waygraphKind === "nav";
577
- }
578
- const NAV_METHOD_CALLS = ["page.goto(", "page.reload(", "page.goBack(", "page.goForward("];
579
- /**
580
- * Matches a selector-taking Trait factory call whose first argument is a
581
- * literal string/template, not an identifier or property access - the exact
582
- * shape found repeatedly in hand-written assertion Blocks
583
- * (`Trait.visible("#some-id")` instead of `Trait.visible(SomeSel.thing)`).
584
- * `Trait.url(...)` is deliberately excluded - it takes a URLPatternInit, never
585
- * a DOM selector. Same file-scoped-regex bluntness the nav-escape sweep above
586
- * already accepts (see openspec/specs/nav-block-and-check/spec.md) - a known
587
- * blind spot on string concatenation/template-built selectors, not solved
588
- * here for the same "recipe before promotion" reason that check accepts its
589
- * own shared-helper blind spot.
590
- */
591
- const INLINE_SELECTOR_CALL = /\b(?:Trait\.visible|Trait\.text|textEquals|visible)\(\s*["'`]/;
592
- /**
593
- * Walks every `*.block.ts` file under `projectDir`, imports each to find its
594
- * exported Blocks, and for any Block NOT built via `defineNavBlock`, scans
595
- * that file's own source text for a navigation call. Warning only - never
596
- * throws, never changes the caller's exit code on its own account (a Block
597
- * file that fails to import for unrelated reasons is silently skipped here;
598
- * `validate` is the command that reports import failures).
599
- */
600
- function printOrphanReport(projectDir, orphans) {
601
- if (orphans.length === 0) {
602
- console.log(`waygraph check: no orphan Blocks (every *.block.ts export is wired into a flow)`);
603
- return;
604
- }
605
- for (const o of orphans) {
606
- console.warn(`waygraph check: orphan Block ${o.file} (${o.exportName} / "${o.block}") is not referenced in any defineFlow([...]) - wire it into a .flow.ts before chain auto shorthand`);
607
- }
608
- console.log(`waygraph check: ${orphans.length} orphan Block${orphans.length === 1 ? "" : "s"}`);
609
- }
610
- async function requireNoOrphans(projectDir, forCommand) {
611
- const orphans = await findOrphanBlocks(projectDir);
612
- if (orphans.length === 0)
613
- return true;
614
- console.error(`waygraph ${forCommand}: ${orphans.length} orphan Block(s) - wire every Block into a .flow.ts before auto shorthand. Run: waygraph check ${projectDir === process.cwd() ? "." : JSON.stringify(projectDir)}`);
615
- for (const o of orphans) {
616
- console.error(` - ${o.file} (${o.exportName} / "${o.block}")`);
617
- }
618
- process.exitCode = 1;
619
- return false;
620
- }
621
- async function runChainAuto(projectDir, fromTag, toTag) {
622
- if (!(await requireNoOrphans(projectDir, "chain auto")))
623
- return;
624
- const graph = await discoverGraph(projectDir);
625
- const path = findBlockPath(graph, fromTag, toTag);
626
- if (!path) {
627
- console.error(`waygraph chain auto: no Block path from "${fromTag}" to "${toTag}" in the discovered graph`);
628
- process.exitCode = 1;
629
- return;
630
- }
631
- const spec = path.join(" then ");
632
- console.error(`waygraph chain auto: ${fromTag} -> ${toTag} via ${spec}`);
633
- if (!process.env.WAYGRAPH_BASE_URL) {
634
- const resolved = resolveBaseUrl(projectDir);
635
- if (resolved)
636
- process.env.WAYGRAPH_BASE_URL = resolved;
637
- }
638
- await runChain(projectDir, spec);
639
- }
640
- function initCommand(projectName) {
641
- if (!projectName) {
642
- console.error("waygraph init: missing <project-name>, e.g. waygraph init my-app");
643
- process.exit(1);
644
- }
645
- const targetDir = resolve(process.cwd(), projectName);
646
- const pkgName = basename(targetDir);
647
- if (existsSync(targetDir)) {
648
- const entries = readdirSync(targetDir);
649
- if (entries.length > 0) {
650
- console.error(`waygraph init: "${targetDir}" already exists and is not empty`);
651
- process.exit(1);
652
- }
653
- }
654
- else {
655
- mkdirSync(targetDir, { recursive: true });
656
- }
657
- const templateDir = join(packageRoot(), "templates", "scaffold");
658
- if (!existsSync(templateDir)) {
659
- console.error(`waygraph init: scaffold template missing at ${templateDir}`);
660
- process.exit(1);
661
- }
662
- cpSync(templateDir, targetDir, { recursive: true });
663
- const gitignoreSrc = join(targetDir, "gitignore");
664
- if (existsSync(gitignoreSrc)) {
665
- writeFileSync(join(targetDir, ".gitignore"), readFileSync(gitignoreSrc, "utf-8"));
666
- rmSync(gitignoreSrc);
667
- }
668
- const packageJsonPath = join(targetDir, "package.json");
669
- writeFileSync(packageJsonPath, readFileSync(packageJsonPath, "utf-8").replaceAll("__PROJECT_NAME__", pkgName));
670
- console.log(`Scaffolded ${pkgName}/`);
671
- console.log("");
672
- console.log(` cd ${relative(process.cwd(), targetDir) || pkgName}`);
673
- console.log(" npm install");
674
- console.log(" npx playwright install chromium");
675
- console.log(" npm test");
676
- console.log(" waygraph list # .flow.ts → export map");
677
- console.log(" waygraph check # nav hygiene + inline selectors + bad practices + orphan Blocks");
678
- console.log(" waygraph typecheck # tsc --noEmit + bad-practice warnings (use --no-practices to skip)");
679
- console.log(" waygraph auto # interactive explore (headed panel)");
680
- console.log(" waygraph auto --cli # same menus in the terminal");
681
- console.log(" waygraph demo --blocks exampleFlow");
682
- console.log(" waygraph graph # static state graph JSON");
683
- console.log("");
684
- console.log(" Layout: see STRUCTURE.md (or https://deviate-dv8.github.io/waygraph/scaffold.html)");
685
- }
686
- function printPracticeReport(projectDir, practiceWarnings) {
687
- if (practiceWarnings.length === 0) {
688
- console.log(`waygraph check: no bad-practice patterns under ${projectDir}`);
689
- return;
690
- }
691
- for (const w of practiceWarnings) {
692
- console.warn(`waygraph check: ${w.file} (${practiceKindLabel(w.kind)}) — ${w.detail}`);
693
- }
694
- console.log(`waygraph check: ${practiceWarnings.length} bad-practice warning${practiceWarnings.length === 1 ? "" : "s"}`);
695
- }
696
- async function checkCommand(projectDir, opts) {
697
- const files = discoverBlocks(projectDir);
698
- const navWarnings = [];
699
- const selWarnings = [];
700
- for (const file of files) {
701
- let mod;
702
- try {
703
- mod = await importModule(file);
704
- }
705
- catch {
706
- continue;
707
- }
708
- let src;
709
- for (const [exportName, exported] of Object.entries(mod)) {
710
- if (!isBlockLike(exported))
711
- continue;
712
- src ??= readFileSync(file, "utf-8");
713
- // Nav-escape: NavBlocks are exempt (their generated act() is the one
714
- // legitimate goto/click call site).
715
- if (!isNavBlockMarked(exported) &&
716
- NAV_METHOD_CALLS.some((needle) => src.includes(needle))) {
717
- navWarnings.push({ file, blockName: exported.name, exportName });
718
- }
719
- // Inline selector: applies to every Block kind, including NavBlocks -
720
- // a Nav's own `verify` array is just as likely to inline a selector.
721
- if (INLINE_SELECTOR_CALL.test(src)) {
722
- selWarnings.push({ file, blockName: exported.name, exportName });
723
- }
724
- }
725
- }
726
- const practiceWarnings = collectPracticeWarnings(projectDir, walkDir, {
727
- disabled: opts?.noPractices === true,
728
- });
729
- return { navWarnings, selWarnings, practiceWarnings };
730
- }
731
- async function runTypecheckCommand(projectDir, noPractices) {
732
- const { spawnSync } = await import("node:child_process");
733
- const tsc = spawnSync("npx", ["tsc", "--noEmit"], {
734
- cwd: projectDir,
735
- stdio: "inherit",
736
- shell: false,
737
- });
738
- if (tsc.status !== 0) {
739
- process.exit(tsc.status === null ? 1 : tsc.status);
740
- }
741
- if (noPractices)
742
- return;
743
- const practiceWarnings = collectPracticeWarnings(projectDir, walkDir);
744
- printPracticeReport(projectDir, practiceWarnings);
745
- }
746
- function takeFlagValue(argv, i, a, flag) {
747
- if (a.startsWith(flag + "=")) {
748
- return { value: a.slice(flag.length + 1), nextI: i };
749
- }
750
- const v = argv[i + 1];
751
- if (v === undefined || (v.startsWith("-") && !a.includes("="))) {
752
- console.error(`waygraph: ${flag} needs a value`);
753
- process.exit(1);
754
- }
755
- return { value: v, nextI: i + 1 };
756
- }
757
- /**
758
- * Looks like a Checkpoint tag for auto --blocks From To (PascalCase).
759
- * Kebab-case Block names must not trigger path-find mode.
760
- */
761
- function looksLikeCheckpointArg(s) {
762
- if (!s || s.startsWith("-"))
763
- return false;
764
- if (s.includes("then") || s.includes("(") || s.includes("{") || s.includes("/"))
765
- return false;
766
- if (s === "." || s.includes("."))
767
- return false;
768
- return /^[A-Z][A-Za-z0-9_]*$/.test(s);
769
- }
770
- /**
771
- * Pulls run/demo/auto flags out of argv. Unknown bare `--*` that are not
772
- * recognized stay as positionals only when they are project paths; known
773
- * explore flags (--cli/--mermaid/--map) are parsed here.
774
- */
775
- function parseRunFlags(argv) {
776
- const out = { positionals: [] };
777
- for (let i = 0; i < argv.length; i++) {
778
- const a = argv[i];
779
- if (a === "--step") {
780
- out.step = true;
781
- }
782
- else if (a === "--no-step") {
783
- out.step = false;
784
- }
785
- else if (a === "--auto-next" || a === "--autoplay") {
786
- out.autoplay = true;
787
- }
788
- else if (a === "--no-auto-next" || a === "--no-autoplay") {
789
- out.autoplay = false;
790
- }
791
- else if (a === "--auto-play-video") {
792
- out.autoPlayVideo = true;
793
- }
794
- else if (a === "--auto-play-video-head") {
795
- out.autoPlayVideo = true;
796
- out.nonHeadless = true;
797
- }
798
- else if (a === "--fast") {
799
- out.fast = true;
800
- }
801
- else if (a === "--full") {
802
- out.fullStepper = true;
803
- }
804
- else if (a === "--mini" || a === "--stepper-mini") {
805
- out.miniStepper = true;
806
- }
807
- else if (a === "--todo-left") {
808
- out.todoPos = "left";
809
- }
810
- else if (a === "--todo-right") {
811
- out.todoPos = "right";
812
- }
813
- else if (a === "--todo-full" || a === "--no-todo-smart") {
814
- out.todoUiFull = true;
815
- }
816
- else if (a === "--todo-smart") {
817
- out.todoUiSmart = true;
818
- }
819
- else if (a === "--ff-expand") {
820
- out.ffExpand = true;
821
- }
822
- else if (a === "--ff-disabled" || a === "--no-ff") {
823
- out.ffDisabled = true;
824
- out.ffExpand = true;
825
- }
826
- else if (a === "--non-headless") {
827
- out.nonHeadless = true;
828
- }
829
- else if (a === "--headless") {
830
- out.headlessBrowser = true;
831
- }
832
- else if (a === "--json") {
833
- out.json = true;
834
- }
835
- else if (a === "--no-practices") {
836
- out.noPractices = true;
837
- }
838
- else if (a === "--blank") {
839
- out.blank = true;
840
- }
841
- else if (a === "--goto" || a.startsWith("--goto=")) {
842
- const t = takeFlagValue(argv, i, a, "--goto");
843
- out.goto = t.value;
844
- i = t.nextI;
845
- }
846
- else if (a === "--inject" || a.startsWith("--inject=")) {
847
- const t = takeFlagValue(argv, i, a, "--inject");
848
- out.inject ??= [];
849
- out.inject.push(t.value);
850
- i = t.nextI;
851
- }
852
- else if (a === "--cli") {
853
- out.cli = true;
854
- }
855
- else if (a === "--detach") {
856
- out.detach = true;
857
- }
858
- else if (a === "--headed") {
859
- out.headed = true;
860
- }
861
- else if (a === "--mermaid") {
862
- out.mermaid = true;
863
- }
864
- else if (a === "--map") {
865
- out.map = true;
866
- }
867
- else if (a === "--base-url" || a.startsWith("--base-url=")) {
868
- const t = takeFlagValue(argv, i, a, "--base-url");
869
- out.baseUrl = t.value;
870
- i = t.nextI;
871
- }
872
- else if (a === "--title" || a.startsWith("--title=")) {
873
- const t = takeFlagValue(argv, i, a, "--title");
874
- out.title = t.value;
875
- i = t.nextI;
876
- }
877
- else if (a === "--data" || a.startsWith("--data=")) {
878
- const t = takeFlagValue(argv, i, a, "--data");
879
- out.data = t.value;
880
- i = t.nextI;
881
- }
882
- else if (a === "--mem-stub") {
883
- out.memStub = true;
884
- }
885
- else if (a === "--video" || a.startsWith("--video=")) {
886
- if (a.startsWith("--video=")) {
887
- out.video = a.slice("--video=".length);
888
- }
889
- else {
890
- const v = argv[i + 1];
891
- if (v === undefined || v.startsWith("-")) {
892
- out.video = "";
893
- }
894
- else {
895
- out.video = v;
896
- i++;
897
- }
898
- }
899
- }
900
- else if (a === "--video-viewport" || a.startsWith("--video-viewport=")) {
901
- const t = takeFlagValue(argv, i, a, "--video-viewport");
902
- out.videoViewport = t.value;
903
- i = t.nextI;
904
- }
905
- else if (a === "--blocks" || a.startsWith("--blocks=")) {
906
- if (a.startsWith("--blocks=")) {
907
- out.blocks = a.slice("--blocks=".length);
908
- }
909
- else {
910
- const v1 = argv[++i];
911
- if (!v1 || v1.startsWith("-")) {
912
- console.error("waygraph: --blocks needs a flow/spec or <from> <to> checkpoints");
913
- process.exit(1);
914
- }
915
- const v2 = argv[i + 1];
916
- if (v2 && looksLikeCheckpointArg(v1) && looksLikeCheckpointArg(v2)) {
917
- out.blocksFromTo = [v1, v2];
918
- i++;
919
- }
920
- else {
921
- out.blocks = v1;
922
- }
923
- }
924
- }
925
- else {
926
- out.positionals.push(a);
927
- }
928
- }
929
- return out;
930
- }
931
- /** Parses WAYGRAPH_VIDEO_VIEWPORT / --video-viewport: "1920x1080" or "1920,1080". */
932
- function parseVideoViewportFlag(raw) {
933
- if (!raw?.trim())
934
- return null;
935
- const m = raw.trim().match(/^(\d{3,5})[xX,](\d{3,5})$/);
936
- if (!m)
937
- return null;
938
- const width = Number(m[1]);
939
- const height = Number(m[2]);
940
- if (!Number.isFinite(width) || !Number.isFinite(height))
941
- return null;
942
- if (width < 320 || height < 240 || width > 7680 || height > 4320)
943
- return null;
944
- return { width, height };
945
- }
946
- /** Writes flag values into process.env so the chain child inherits them. Flags beat prior env. */
947
- function applyRunFlags(flags, opts) {
948
- if (flags.autoPlayVideo) {
949
- if (!opts?.allowAutoPlayVideo) {
950
- console.error("waygraph: --auto-play-video is a demo-only flag (QA watch + record)");
951
- process.exit(1);
952
- }
953
- if (flags.autoplay === undefined)
954
- flags.autoplay = true;
955
- if (flags.video === undefined)
956
- flags.video = "";
957
- // Overlay narration (captions/rings/slides) needs step mode to run, but
958
- // --auto-play-video is unattended-and-recorded by design - no human
959
- // has to watch a live window for Playwright's own recordVideo to
960
- // capture the right thing. Headless by default; --non-headless (or the
961
- // --auto-play-video-head alias, parsed above) opts back into a
962
- // visible browser for someone who wants to watch it live too.
963
- process.env.WAYGRAPH_STEP = "1";
964
- if (!flags.nonHeadless && process.env.WAYGRAPH_HEADED === undefined) {
965
- process.env.WAYGRAPH_HEADED = "0";
966
- }
967
- }
968
- if ((flags.fast || flags.fullStepper || flags.miniStepper || flags.todoPos || flags.todoUiFull || flags.todoUiSmart) &&
969
- !opts?.allowDemoUi) {
970
- console.error("waygraph: --fast / --full / --mini / --todo-left|--todo-right / --todo-full|--todo-smart are demo-only flags");
971
- process.exit(1);
972
- }
973
- if (flags.step === true) {
974
- process.env.WAYGRAPH_STEP = "1";
975
- process.env.WAYGRAPH_HEADED = "1";
976
- }
977
- else if (flags.step === false) {
978
- process.env.WAYGRAPH_STEP = "0";
979
- }
980
- if (flags.autoplay === true) {
981
- process.env.WAYGRAPH_AUTOPLAY = "1";
982
- }
983
- else if (flags.autoplay === false) {
984
- process.env.WAYGRAPH_AUTOPLAY = "0";
985
- }
986
- if (flags.nonHeadless) {
987
- process.env.WAYGRAPH_HEADED = "1";
988
- }
989
- // Any --video on demo/run: headless unless --non-headless already won.
990
- if (flags.video !== undefined && !flags.nonHeadless && process.env.WAYGRAPH_HEADED === undefined) {
991
- process.env.WAYGRAPH_HEADED = "0";
992
- }
993
- if (flags.baseUrl !== undefined) {
994
- process.env.WAYGRAPH_BASE_URL = flags.baseUrl;
995
- }
996
- if (flags.title !== undefined) {
997
- process.env.WAYGRAPH_TITLE = flags.title;
998
- }
999
- if (flags.video !== undefined) {
1000
- process.env.WAYGRAPH_VIDEO = flags.video || "1";
1001
- }
1002
- if (flags.videoViewport !== undefined) {
1003
- if (!parseVideoViewportFlag(flags.videoViewport)) {
1004
- console.error('waygraph: --video-viewport must look like "1920x1080"');
1005
- process.exit(1);
1006
- }
1007
- process.env.WAYGRAPH_VIDEO_VIEWPORT = flags.videoViewport;
1008
- }
1009
- if (flags.data !== undefined) {
1010
- process.env.WAYGRAPH_DATA = flags.data;
1011
- }
1012
- if (flags.memStub) {
1013
- process.env.WAYGRAPH_MEM_STUB = "1";
1014
- }
1015
- if (flags.fast) {
1016
- process.env.WAYGRAPH_DEMO_FAST = "1";
1017
- }
1018
- if (flags.fullStepper) {
1019
- process.env.WAYGRAPH_STEPPER = "full";
1020
- }
1021
- if (flags.miniStepper) {
1022
- process.env.WAYGRAPH_MINI = "1";
1023
- }
1024
- if (flags.todoPos === "left" || flags.todoPos === "right") {
1025
- process.env.WAYGRAPH_TODO_POS = flags.todoPos;
1026
- }
1027
- if (flags.todoUiFull) {
1028
- process.env.WAYGRAPH_TODO_UI = "full";
1029
- }
1030
- else if (flags.todoUiSmart) {
1031
- process.env.WAYGRAPH_TODO_UI = "smart";
1032
- }
1033
- if (flags.ffExpand) {
1034
- process.env.WAYGRAPH_FF_EXPAND = "1";
1035
- }
1036
- if (flags.ffDisabled) {
1037
- process.env.WAYGRAPH_FF_DISABLED = "1";
1038
- process.env.WAYGRAPH_FF_EXPAND = "1";
1039
- }
1040
- }
1041
- /**
1042
- * BASE_URL when neither --base-url nor WAYGRAPH_BASE_URL is set:
1043
- * package.json `waygraph.baseUrl` (or `baseURL`), then playwright.config `baseURL`.
1044
- */
1045
- function resolveBaseUrl(projectDir) {
1046
- const pkgPath = join(projectDir, "package.json");
1047
- if (existsSync(pkgPath)) {
1048
- try {
1049
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1050
- const fromPkg = pkg.waygraph?.baseUrl ??
1051
- pkg.waygraph?.baseURL ??
1052
- pkg.config?.waygraph?.baseUrl ??
1053
- pkg.config?.waygraph?.baseURL;
1054
- if (typeof fromPkg === "string" && fromPkg.trim())
1055
- return fromPkg.trim();
1056
- }
1057
- catch {
1058
- // ignore malformed package.json - fall through to playwright
1059
- }
1060
- }
1061
- for (const name of ["playwright.config.ts", "playwright.config.mts", "playwright.config.js", "playwright.config.mjs"]) {
1062
- const p = join(projectDir, name);
1063
- if (!existsSync(p))
1064
- continue;
1065
- const src = readFileSync(p, "utf-8");
1066
- const m = src.match(/baseURL\s*:\s*["']([^"']+)["']/);
1067
- if (m?.[1])
1068
- return m[1];
1069
- }
1070
- return undefined;
1071
- }
1072
- /**
1073
- * Friendly demo defaults: STEP on; headed only for live watch (no --video).
1074
- * --video / --auto-play-video => headless unless --non-headless.
1075
- * Call after applyRunFlags so explicit flags already won.
1076
- */
1077
- function applyDemoDefaults(projectDir) {
1078
- if (process.env.WAYGRAPH_STEP === undefined)
1079
- process.env.WAYGRAPH_STEP = "1";
1080
- if (process.env.WAYGRAPH_STEP === "1" && process.env.WAYGRAPH_HEADED === undefined) {
1081
- // Recording locks headless - a live window fights fixed video viewport /
1082
- // device shell centering. Watch live only with --non-headless.
1083
- process.env.WAYGRAPH_HEADED = process.env.WAYGRAPH_VIDEO ? "0" : "1";
1084
- }
1085
- // Manual Next by default. --auto-next / --auto-play-video flip this.
1086
- if (process.env.WAYGRAPH_AUTOPLAY === undefined) {
1087
- if (process.env.WAYGRAPH_VIDEO && process.env.WAYGRAPH_STEP === "0") {
1088
- process.env.WAYGRAPH_AUTOPLAY = "1";
1089
- }
1090
- else {
1091
- process.env.WAYGRAPH_AUTOPLAY = "0";
1092
- }
1093
- }
1094
- if (!process.env.WAYGRAPH_BASE_URL) {
1095
- const resolved = resolveBaseUrl(projectDir);
1096
- if (resolved)
1097
- process.env.WAYGRAPH_BASE_URL = resolved;
1098
- }
1099
- }
1100
- function resolveProjectDir(flags, fallbackIndex = 0) {
1101
- const cand = flags.positionals[fallbackIndex] ?? process.cwd();
1102
- return resolve(cand);
1103
- }
1104
- function resolveSpec(flags) {
1105
- return flags.blocks ?? flags.positionals[0];
1106
- }
1107
- /** `shop.flow.ts` / `src/flows/cart-bulk.flow.ts` / absolute path — not a Checkpoint or export id. */
1108
- function looksLikeFlowFileRef(ref) {
1109
- if (!ref || ref.includes("then") || ref.includes("("))
1110
- return false;
1111
- return /\.flow\.ts$/i.test(ref) || (/\.ts$/i.test(ref) && (ref.includes("/") || ref.includes("\\")));
1112
- }
1113
- function flowExportNameFromBasename(filePath) {
1114
- const base = basename(filePath).replace(/\.flow\.ts$/i, "").replace(/\.ts$/i, "");
1115
- const camel = base.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1116
- return /Flow$/i.test(camel) ? camel : `${camel}Flow`;
1117
- }
1118
- /**
1119
- * Map a `.flow.ts` path (relative to project or absolute) to its Flow export
1120
- * name so the chain runner can `findFlow(exportName)`. Prefers
1121
- * `shop.flow.ts` → `shopFlow`. Reads source only (no import) so the outer CLI
1122
- * does not dual-load `@playwright/test` against the project tree.
1123
- */
1124
- async function resolveFlowFileToExport(projectDir, fileArg) {
1125
- let abs = resolve(projectDir, fileArg);
1126
- if (!existsSync(abs)) {
1127
- const want = basename(fileArg);
1128
- const hits = walkDir(projectDir, new RegExp(`${want.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`));
1129
- if (hits.length === 1)
1130
- abs = hits[0];
1131
- else if (hits.length === 0) {
1132
- throw new Error(`waygraph: no flow file "${fileArg}" under ${projectDir}`);
1133
- }
1134
- else {
1135
- throw new Error(`waygraph: "${fileArg}" matches ${hits.length} files - use a path from \`waygraph list\``);
1136
- }
1137
- }
1138
- const expected = flowExportNameFromBasename(abs);
1139
- const src = readFileSync(abs, "utf-8");
1140
- if (new RegExp(`\\bexport\\s+(?:const|let|var|async\\s+function|function)\\s+${expected}\\b`).test(src)) {
1141
- return expected;
1142
- }
1143
- if (new RegExp(`\\bexport\\s*\\{[^}]*\\b${expected}\\b`).test(src)) {
1144
- return expected;
1145
- }
1146
- const flowNames = [
1147
- ...src.matchAll(/\bexport\s+(?:const|let|var|async\s+function|function)\s+(\w*Flow)\b/g),
1148
- ].map((m) => m[1]);
1149
- const unique = [...new Set(flowNames)];
1150
- if (unique.length === 1)
1151
- return unique[0];
1152
- if (unique.includes(expected))
1153
- return expected;
1154
- if (unique.length === 0) {
1155
- throw new Error(`waygraph: ${relative(projectDir, abs)} has no exported *Flow (expected ${expected})`);
1156
- }
1157
- throw new Error(`waygraph: ${relative(projectDir, abs)} exports ${unique.join(", ")} - pass the export name or rename to ${expected}`);
1158
- }
1159
- /**
1160
- * Expand `.flow.ts` path segments in a run/demo spec to export names.
1161
- * `src/flows/shop.flow.ts` → `shopFlow`; leaves `loginFlow then nav-cart` alone.
1162
- */
1163
- async function expandSpecFlowFiles(projectDir, spec) {
1164
- const parts = spec.split(/\bthen\b/).map((s) => s.trim()).filter((s) => s.length > 0);
1165
- const out = [];
1166
- for (const part of parts) {
1167
- const m = /^([^\s(]+)(\s*\([\s\S]*\))?$/.exec(part);
1168
- if (!m) {
1169
- out.push(part);
1170
- continue;
1171
- }
1172
- const ref = m[1];
1173
- const payload = m[2] ?? "";
1174
- if (looksLikeFlowFileRef(ref)) {
1175
- const exportName = await resolveFlowFileToExport(projectDir, ref);
1176
- out.push(exportName + payload);
1177
- }
1178
- else {
1179
- out.push(part);
1180
- }
1181
- }
1182
- return out.join(" then ");
1183
- }
17
+ import { SKILL_FLAG_MAP, printSkillIndex, readSkillMarkdown } from "./agent-dive.js";
18
+ import { usage } from "./cli/usage.js";
19
+ import { listCase, navCase } from "./commands/list.js";
20
+ import { checkCase, typecheckCase, validateCase } from "./commands/check.js";
21
+ import { chainCase, demoCase, runCase } from "./commands/run.js";
22
+ import { testCase } from "./commands/test.js";
23
+ import { initCase } from "./commands/init.js";
24
+ import { traverseCase } from "./commands/traverse.js";
25
+ import { agentDiveCase } from "./commands/agent-dive.js";
26
+ import { autoServeCase, sessionCase } from "./commands/session.js";
27
+ import { tryCase } from "./commands/try.js";
28
+ import { mapCase } from "./commands/map.js";
1184
29
  const args = process.argv.slice(2);
1185
30
  const command = args[0];
1186
- function printBrowserUsage() {
1187
- console.log(`waygraph browser — persistent Playwright sessions (headful by default)
1188
-
1189
- waygraph browser start [--inject preset|path] [--goto <url>] [--blank] [--headless] [--cli] [project]
1190
- Open a new session (about:blank by default)
1191
- waygraph browser sessions [--json] [project]
1192
- List live sessions for a project
1193
- waygraph browser stop <sessionId|--all> [project]
1194
- waygraph browser attach <sessionId> Terminal picker on a live session
1195
-
1196
- waygraph browser status <sessionId> Current menu (no side effects)
1197
- waygraph browser send <sessionId> "<pick>"
1198
- waygraph browser reach <sessionId> <Checkpoint>
1199
- waygraph browser dom|trace|console|storage|highlight <sessionId> …
1200
- waygraph browser click|type|press|goto|upload|reload|resync <sessionId> …
1201
-
1202
- auto send|status|… and pilot send|status|… use the same session ids.`);
1203
- }
1204
- function printBrowserSessionsList(sessions, projectDir) {
1205
- if (sessions.length === 0) {
1206
- console.log(`No live browser sessions for ${projectDir}.`);
1207
- console.log(" waygraph browser start");
1208
- return;
1209
- }
1210
- console.log(`Live browser sessions (${projectDir}):`);
1211
- for (const s of sessions) {
1212
- const mode = s.headless ? "headless" : "headful";
1213
- console.log(` ${s.sessionId} ${mode} pid ${s.pid}`);
1214
- console.log(` waygraph browser attach ${s.sessionId}`);
1215
- console.log(` waygraph browser stop ${s.sessionId}`);
1216
- }
1217
- }
1218
- function usage() {
1219
- console.log(`waygraph -- graph project tool + engine CLI
1220
-
1221
- Primary (less is more):
1222
- waygraph auto [project|.flow.ts] Interactive explore (picker)
1223
- --cli Terminal menu instead of browser panel
1224
- --cli --detach Run the --cli session as a background socket
1225
- server; prints {sessionId, socketPath} and exits
1226
- --blocks <From> <To> Graph path-find From->To, then run
1227
- --data '{...}' Mem seed (same as demo/run)
1228
- (pass a .flow.ts to run that flow - same as run)
1229
- --cli --detach --non-headless Detached session with a real visible browser
1230
- (same --non-headless flag run/demo already use)
1231
- waygraph auto send <sessionId> "<pick>" Send one pick to a --detach session, print
1232
- the resulting state as JSON (no TTY needed).
1233
- <pick> is a 1-based menu index, a Block name
1234
- (exact match; ambiguous names refuse rather than
1235
- guess), or "q"/"quit". Same command as
1236
- browser send <sessionId> "<pick>" (prefer that
1237
- prefix for a --detach/persistent session).
1238
- --timeout <ms> Override the 15s default wait for this one call -
1239
- a single Block can legitimately run a slow real
1240
- interaction (e.g. a multi-step mouse drag)
1241
- waygraph auto status <sessionId> Read a --detach session's state (no side effects)
1242
- waygraph auto attach <sessionId> Reopen an interactive terminal against a
1243
- running --detach session
1244
- waygraph auto dom <sessionId> Read the live page's structure (no side effects)
1245
- --mode aria|full Default aria (Playwright ariaSnapshotJSON, AI mode);
1246
- full = bounded raw DOM walk (tag/attrs/text/children)
1247
- --selector <sel> Scope either mode to one element's subtree
1248
- --depth N Limit snapshot depth (aria: native; full: caller cap)
1249
- waygraph auto trace <sessionId> Read the session's Checkpoint/Block-level history
1250
- (no side effects; not raw click/fill recording)
1251
- waygraph auto console <sessionId> Read the real browser console/pageerror messages and
1252
- failed (4xx/5xx) network responses seen so far - for
1253
- diagnosing a silent failure (e.g. a form submit that
1254
- does nothing observable in the DOM) that inspectDom
1255
- alone can't explain, since it only reads what actually
1256
- rendered, not what the page/network actually said
1257
- waygraph auto storage <sessionId> Read localStorage/sessionStorage plus registered
1258
- service workers (scope/active URL/state) - client-
1259
- side state (push-subscription/auth tokens etc.) that
1260
- never appears in the rendered DOM inspectDom reads
1261
- waygraph auto click <sessionId> <sel> Click a real element - works even with zero Blocks
1262
- (Blind Pilot: act before any Block covers this)
1263
- waygraph auto type <sessionId> <sel> <text> Fill a real input the same way
1264
- waygraph auto goto <sessionId> <url> Navigate the real live page
1265
- waygraph auto press <sessionId> <sel> <key>
1266
- Presses a real key (Playwright name, e.g. Enter,
1267
- Escape, Tab) on a real focused element - many real
1268
- inline-edit inputs (no visible Save button until you
1269
- type) commit on Enter, not blur/click-elsewhere
1270
- waygraph auto upload <sessionId> <sel> <image|pdf|video|path>
1271
- Fill a real <input type="file"> - a built-in stub
1272
- (small, real, valid: assets/stubs/stub.png|pdf|mp4)
1273
- or a caller-supplied file path
1274
- (click/type/goto/upload each re-detect the session's Checkpoint afterward,
1275
- same detection send/status already use)
1276
- waygraph auto reload <sessionId> Re-discover the project's Block library/graph
1277
- from disk without restarting the session - picks
1278
- up a Block written to disk mid-session
1279
- waygraph auto reach <sessionId> <Checkpoint> Path-find from here to Checkpoint and run
1280
- the whole route in one call - not one auto send
1281
- per step. Same findBlockPath BFS auto --blocks
1282
- <From> <To> already uses, against this session's
1283
- own live graph/position. Fails loud (not a guess)
1284
- if a step has several live options on the page
1285
- (an instanceOptions Block) - use send for that step
1286
- waygraph auto resync <sessionId> Force here to be re-detected from the real live
1287
- page right now, discarding whatever was cached -
1288
- fixes a session's tracked position going stale
1289
- after anything OUTSIDE this session changed the
1290
- page (e.g. a human clicking around in a visible
1291
- --non-headless session someone is co-driving);
1292
- send/reach never do this on their own, since they
1293
- only re-detect when the position is already unknown
1294
- waygraph auto highlight <sessionId> '<json>'
1295
- Paint agent highlight fixtures on the live page
1296
- (rings + optional todos). Same visual language as
1297
- Block stubBefore in demo. JSON: rings[{selector,
1298
- label,tone?,size?,weight?}], todos[], todoIndex?,
1299
- todoTitle?, holdMs? (0=until next), clear:true.
1300
- Missing selectors listed in response, not fatal.
1301
- Same command as browser highlight <sessionId>
1302
- '<json>' (prefer that prefix for a --detach/
1303
- persistent session).
1304
- <selector>|<label>[|<tone>] Shorthand for 1-3 rings, no JSON braces/quotes
1305
- to escape - rings separated by ";", e.g.
1306
- "#x|Login button|warning; .err|Error banner|danger".
1307
- Covers selector/label/tone only; anything else
1308
- (size/weight/zoom/focus/todos/...) needs the JSON
1309
- form above.
1310
- waygraph browser Show browser subcommands
1311
- waygraph browser start Open a new session (headful by default, about:blank)
1312
- --inject preset|path Merge an external Block library (e.g. saucedemo)
1313
- --goto <url> Navigate on start (disables blank page)
1314
- --blank Force about:blank on start (default)
1315
- --headless Headless session (default is visible browser)
1316
- --cli After start, open the terminal picker (auto attach)
1317
- waygraph browser sessions [project] List live browser sessions (attach/status/stop by id)
1318
- waygraph browser send|status|attach|… Same session control as auto (see auto send/status/…)
1319
- waygraph pilot start Bootstrap: browser session + whole-project graph +
1320
- starting snapshot in one call. Does not plan or act.
1321
- --inject / --goto / --headless / --cli / --base-url Same flags as browser start
1322
- waygraph pilot sessions [project] List live sessions (prefer attach over a new start)
1323
- waygraph pilot attach <sessionId> Graph + snapshot for an existing session (no new browser)
1324
- waygraph pilot send|status|highlight|… Same session control as browser/auto
1325
- waygraph auto Interactive explore (picker) — NOT the persistent browser
1326
- layer; use browser/pilot for agent-driven sessions.
1327
- waygraph demo [--blocks <flow|file|spec>] Watch with step overlay (QA path)
1328
- --data '{...}' Mem seed JSON (or inline flow({...}))
1329
- --mem-stub Fill any requires key --data didn't cover from
1330
- registerMemStub's registry (needs the key
1331
- registered, or the flow's own withMemStub - a
1332
- key with neither still fails preflight)
1333
- --auto-next Auto-advance steps (alias: --autoplay)
1334
- --fast Shorter auto-next / Next gates (keeps smooth cursor)
1335
- --full Classic wrap-all block chips (default: carousel)
1336
- --mini Force compact mini panel (Hide pill + Next); alias --stepper-mini
1337
- --todo-left|--todo-right Floating checklist dock side (also ctx.todoPos / WAYGRAPH_TODO_POS)
1338
- --todo-full Opt out: full checklist, no compact/collision/behind-ring
1339
- --todo-smart Opt in: compact + collision + behind-ring (default)
1340
- --ff-expand Expand fastForwardComposeBlock inners as separate steps
1341
- --ff-disabled Dispute: expand FF (alias --no-ff); blitz kept on those inners
1342
- --auto-play-video Unattended + recorded: --auto-next + --video (+ step); headless
1343
- --auto-play-video-head Same, but keep the browser visible (--non-headless)
1344
- --video [dir] Record .webm (demo: headless unless --non-headless)
1345
- --video-viewport WxH Recording size (default demo: 1920x1080)
1346
- --title / --base-url
1347
- waygraph run [--blocks <flow|file|spec>] Execute (no overlay unless --step)
1348
- --data '{...}'
1349
- --mem-stub Same as demo's --mem-stub
1350
- --non-headless Show browser
1351
- --video [dir] Record .webm
1352
- --video-viewport WxH Recording size (default run: 1280x720)
1353
- --ff-expand Same as demo (expand FFCompose inners)
1354
- --ff-disabled Same as demo (dispute: expand FF)
1355
- waygraph test Runs the project's @playwright/test suite (cwd) -
1356
- thin wrapper: forwards to the local playwright
1357
- binary (or npx playwright as a fallback)
1358
- test ui / test --ui Playwright UI Mode (interactive, watch + trace)
1359
- test report [dir] Open the last HTML report (playwright show-report)
1360
- test show-trace <file> Open one trace.zip in the trace viewer
1361
- <any other args> Forwarded verbatim (--grep, a spec path, --headed, …)
1362
- Scaffolds ship trace: "retain-on-failure" + the
1363
- html reporter, so a failed waygraph test already
1364
- has a trace - test report opens it.
1365
-
1366
- Also:
1367
- waygraph list | nav | validate | check | typecheck | graph | init <name>
1368
- check/typecheck --no-practices Skip bad-practice warnings
1369
- waygraph map [project] Waygraph Map convention enforcement: every static
1370
- Nav/Page url must verbatim-match its src/map/ folder
1371
- path ((group) segments excluded) - exits 1 on a
1372
- violation, unlike check's warnings-only stance. A
1373
- no-op (exit 0, informational) if the project has no
1374
- src/map/ directory at all.
1375
- waygraph traverse [project] Graph crawl (Phase B-E)
1376
- --blocks <glob|/re/|sub> Phase C: filter *.block.ts discovery
1377
- --parallel N Phase D: N clone workers (max 4)
1378
- --session clone|inherit Phase D: default clone
1379
- --from <Checkpoint> Seed / start checkpoint
1380
- --data '{...}' Mem seed
1381
- --max-steps N Cap Block runs (default 50)
1382
- --max-visits N Cap visits per node (default 2)
1383
- --non-headless Show browser
1384
- waygraph agent-dive [--loop claude|opencode|cursor|vscode] [--prompts]
1385
- Initialize coding-agent defs (Playwright init-agents analogue)
1386
- waygraph try [demo|auto|auto:cli] One-shot saucedemo in a temp dir
1387
- try auto Headed browser panel (default)
1388
- try auto:cli / --cli Terminal menu instead
1389
- try auto --headed Same as try auto (compat)
1390
-
1391
- Skills (print packaged skill markdown to stdout - for agents / paste):
1392
- waygraph --skill List available skills
1393
- waygraph --skill-pilot Drive an existing Block graph
1394
- waygraph --skill-pilot-blind Cold-start: raw ops + author Blocks
1395
- waygraph --skill-convention Block/Map authoring conventions
1396
-
1397
- Examples:
1398
- waygraph list # file → export map
1399
- waygraph run src/flows/shop.flow.ts --data '{...}'
1400
- waygraph run --blocks shopFlow --non-headless --video
1401
- waygraph demo --blocks src/flows/cart-bulk.flow.ts --auto-next --fast
1402
- waygraph demo src/flows/shop.flow.ts --full
1403
- waygraph try auto
1404
- waygraph try auto:cli
1405
- waygraph auto src/flows/shop.flow.ts --data '{...}' # run by file (same as run)
1406
- waygraph auto --cli --data '{"saucedemo.credentials":{...}}'
1407
- waygraph auto --blocks LoginPage OrderComplete
1408
- waygraph auto --blocks '/mailpit/' # Phase C filtered explore
1409
- waygraph traverse --blocks '**/mailpit/**/*.block.ts' # Phase C filtered crawl
1410
- waygraph traverse --parallel 2 --session clone # Phase D clone workers
1411
- waygraph run --blocks "loginFlow then add-all-to-cart" --data '{...}' --video
1412
-
1413
- Aliases (compat): \`chain <spec>\` -> run --blocks; \`chain auto A B\` -> auto --blocks A B;
1414
- --autoplay -> --auto-next. Prefer the primary verbs above.
1415
-
1416
- Project path optional (defaults to cwd). Flags beat WAYGRAPH_* env.
1417
- \`run\`/\`demo\`/\`auto\`: Flow export, .flow.ts path, or "a then b" chain (auto also explores).
1418
- Hide stepper = compact "N / M · block" pill. Collapses while a step runs (clicks hit the page). Video mode stays compact.
1419
- `);
1420
- process.exit(0);
1421
- }
1422
31
  async function main() {
1423
32
  if (!command || command === "--help" || command === "-h") {
1424
33
  usage();
@@ -1433,1073 +42,61 @@ async function main() {
1433
42
  process.exit(0);
1434
43
  }
1435
44
  switch (command) {
1436
- case "list": {
1437
- const proj = resolve(args[1] ?? process.cwd());
1438
- if (!existsSync(proj)) {
1439
- console.error(`waygraph: no such directory: ${proj}`);
1440
- process.exit(1);
1441
- }
1442
- listCommand(proj);
45
+ case "list":
46
+ await listCase(args);
47
+ break;
48
+ case "nav":
49
+ await navCase(args);
1443
50
  break;
1444
- }
1445
- case "nav": {
1446
- const flowName = args[1] && args[1] !== "." ? args[1] : undefined;
1447
- const proj = resolve(args[2] ?? process.cwd());
1448
- navCommand(proj, flowName);
51
+ case "validate":
52
+ await validateCase(args);
1449
53
  break;
1450
- }
1451
- case "validate": {
1452
- const proj = resolve(args[1] ?? process.cwd());
1453
- const results = await validateFlows(proj);
1454
- let failed = 0;
1455
- for (const r of results) {
1456
- const rel = relative(proj, r.file);
1457
- if (r.valid) {
1458
- console.log(`OK ${rel} ${r.name}`);
1459
- }
1460
- else {
1461
- failed++;
1462
- console.log(`FAIL ${rel} ${r.name} -- ${r.error}`);
1463
- }
1464
- }
1465
- if (failed > 0)
1466
- process.exitCode = 1;
54
+ case "run":
55
+ await runCase(args);
1467
56
  break;
1468
- }
1469
- case "run": {
1470
- const flags = parseRunFlags(args.slice(1));
1471
- applyRunFlags(flags);
1472
- const spec = resolveSpec(flags);
1473
- if (!spec) {
1474
- console.error("waygraph run: missing flow/spec — e.g.\n" +
1475
- " waygraph run src/flows/shop.flow.ts\n" +
1476
- " waygraph run --blocks shopFlow\n" +
1477
- " waygraph list # file → export map\n" +
1478
- " Flags: --blocks --data --mem-stub --non-headless --video [dir] --step/--no-step");
1479
- process.exit(1);
1480
- }
1481
- const proj = flags.blocks
1482
- ? resolveProjectDir(flags, 0)
1483
- : resolve(flags.positionals[1] ?? process.cwd());
1484
- if (!existsSync(proj)) {
1485
- console.error(`waygraph: no such directory: ${proj}`);
1486
- process.exit(1);
1487
- }
1488
- if (!process.env.WAYGRAPH_BASE_URL) {
1489
- const resolved = resolveBaseUrl(proj);
1490
- if (resolved)
1491
- process.env.WAYGRAPH_BASE_URL = resolved;
1492
- }
1493
- await runChain(proj, spec);
57
+ case "test":
58
+ await testCase(args);
1494
59
  break;
1495
- }
1496
- case "test": {
1497
- const rest = args.slice(1);
1498
- const proj = process.cwd();
1499
- const localPlaywrightBin = join(proj, "node_modules", ".bin", "playwright");
1500
- const runPw = (pwArgs) => existsSync(localPlaywrightBin)
1501
- ? runInherited(localPlaywrightBin, pwArgs, proj)
1502
- : runInherited("npx", ["--yes", "playwright", ...pwArgs], proj);
1503
- // report/show-trace are separate top-level `playwright` commands, not
1504
- // `playwright test` subcommands - handled before the `test` forward below.
1505
- if (rest[0] === "report") {
1506
- process.exitCode = await runPw(["show-report", ...rest.slice(1)]);
1507
- break;
1508
- }
1509
- if (rest[0] === "show-trace") {
1510
- if (!rest[1]) {
1511
- console.error("waygraph test show-trace: missing <trace.zip> (or a test-results/ dir)");
1512
- process.exit(1);
1513
- }
1514
- process.exitCode = await runPw(["show-trace", ...rest.slice(1)]);
1515
- break;
1516
- }
1517
- const wantsUi = rest[0] === "ui" || rest.includes("--ui");
1518
- const forwarded = rest[0] === "ui" ? rest.slice(1) : rest;
1519
- const pwArgs = wantsUi && !forwarded.includes("--ui")
1520
- ? ["test", "--ui", ...forwarded]
1521
- : ["test", ...forwarded];
1522
- process.exitCode = await runPw(pwArgs);
60
+ case "init":
61
+ await initCase(args);
1523
62
  break;
1524
- }
1525
- case "init": {
1526
- initCommand(args[1] ?? "");
63
+ case "traverse":
64
+ await traverseCase(args);
1527
65
  break;
1528
- }
1529
- case "traverse": {
1530
- if (args.includes("--step") || args.includes("--auto-next") || args.includes("--autoplay")) {
1531
- console.error("waygraph traverse: --step / --auto-next not supported (not a demo)");
1532
- process.exit(1);
1533
- }
1534
- const rest = args.slice(1);
1535
- let from;
1536
- let data;
1537
- let maxSteps;
1538
- let maxVisits;
1539
- let maxEdge;
1540
- let headed = false;
1541
- let baseUrl;
1542
- let blocksFilter;
1543
- let parallel;
1544
- let session;
1545
- let minEdgeCoverageRaw;
1546
- let coverageOut;
1547
- let noCoverageReport = false;
1548
- let projectDir = process.cwd();
1549
- for (let i = 0; i < rest.length; i++) {
1550
- const a = rest[i];
1551
- if (a === "--from" || a.startsWith("--from=")) {
1552
- from = a.startsWith("--from=") ? a.slice(7) : rest[++i];
1553
- continue;
1554
- }
1555
- if (a === "--data" || a.startsWith("--data=")) {
1556
- data = a.startsWith("--data=") ? a.slice(7) : rest[++i];
1557
- continue;
1558
- }
1559
- if (a === "--blocks" || a.startsWith("--blocks=")) {
1560
- if (a.startsWith("--blocks=")) {
1561
- blocksFilter = a.slice("--blocks=".length);
1562
- }
1563
- else {
1564
- const v = rest[++i];
1565
- if (!v || v.startsWith("-")) {
1566
- console.error("waygraph traverse: --blocks needs a glob, /regex/, or path substring");
1567
- process.exit(1);
1568
- }
1569
- blocksFilter = v;
1570
- }
1571
- continue;
1572
- }
1573
- if (a === "--parallel" || a.startsWith("--parallel=")) {
1574
- const v = a.startsWith("--parallel=") ? a.slice("--parallel=".length) : rest[++i];
1575
- parallel = Number(v);
1576
- continue;
1577
- }
1578
- if (a === "--session" || a.startsWith("--session=")) {
1579
- const v = a.startsWith("--session=") ? a.slice("--session=".length) : rest[++i];
1580
- if (v !== "clone" && v !== "inherit") {
1581
- console.error('waygraph traverse: --session must be "clone" or "inherit"');
1582
- process.exit(1);
1583
- }
1584
- session = v;
1585
- continue;
1586
- }
1587
- if (a === "--min-edge-coverage" || a.startsWith("--min-edge-coverage=")) {
1588
- minEdgeCoverageRaw = a.startsWith("--min-edge-coverage=")
1589
- ? a.slice("--min-edge-coverage=".length)
1590
- : rest[++i];
1591
- continue;
1592
- }
1593
- if (a === "--coverage-out" || a.startsWith("--coverage-out=")) {
1594
- coverageOut = a.startsWith("--coverage-out=")
1595
- ? a.slice("--coverage-out=".length)
1596
- : rest[++i];
1597
- continue;
1598
- }
1599
- if (a === "--no-coverage-report") {
1600
- noCoverageReport = true;
1601
- continue;
1602
- }
1603
- if (a === "--max-steps" || a.startsWith("--max-steps=")) {
1604
- const v = a.startsWith("--max-steps=") ? a.slice(12) : rest[++i];
1605
- maxSteps = Number(v);
1606
- continue;
1607
- }
1608
- if (a === "--max-visits" || a.startsWith("--max-visits=")) {
1609
- const v = a.startsWith("--max-visits=") ? a.slice(13) : rest[++i];
1610
- maxVisits = Number(v);
1611
- continue;
1612
- }
1613
- if (a === "--max-visits-per-edge" || a.startsWith("--max-visits-per-edge=")) {
1614
- const v = a.startsWith("--max-visits-per-edge=")
1615
- ? a.slice("--max-visits-per-edge=".length)
1616
- : rest[++i];
1617
- maxEdge = Number(v);
1618
- continue;
1619
- }
1620
- if (a === "--non-headless" || a === "--headed") {
1621
- headed = true;
1622
- continue;
1623
- }
1624
- if (a === "--base-url" || a.startsWith("--base-url=")) {
1625
- baseUrl = a.startsWith("--base-url=") ? a.slice(11) : rest[++i];
1626
- continue;
1627
- }
1628
- if (a === "--help" || a === "-h") {
1629
- console.log(`waygraph traverse [project] [flags]
1630
-
1631
- Graph crawl (RFC Phase B-E). Walks unused legal edges until a leaf,
1632
- budget kill, or first broken edge. --parallel N uses session clone + edge leases.
1633
-
1634
- --blocks <glob|/regex/|substr> Phase C: filter *.block.ts discovery
1635
- --parallel N Phase D: N clone workers (default 1, max 4)
1636
- --session clone|inherit Phase D: default clone; inherit refused if parallel>1
1637
- --min-edge-coverage X Phase E: suite gate 0.8 | 80% | 80 (exit 2 if below)
1638
- --coverage-out PATH Phase E: write JSON (default .waygraph-traverse/coverage.json)
1639
- --no-coverage-report Phase E: print Coverage line only (no JSON file)
1640
- --from <Checkpoint> start checkpoint (optional)
1641
- --data '{...}' Mem seed JSON
1642
- --max-steps N default 50 (per worker)
1643
- --max-visits N per-node visit cap (default 2)
1644
- --max-visits-per-edge N default 1
1645
- --non-headless show browser
1646
- --base-url URL
1647
-
1648
- Examples:
1649
- waygraph traverse --blocks '**/mailpit/**/*.block.ts'
1650
- waygraph traverse --parallel 2 --session clone --max-steps 20
1651
- waygraph traverse --min-edge-coverage 80% --coverage-out ./cov.json
1652
-
1653
- PASS: [Reached Leaf Node[traverse-1] at=... steps=N]
1654
- FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
1655
- COVER: [Coverage edges=H/T ratio=R% min=M% PASS|FAIL]
1656
- `);
1657
- process.exit(0);
1658
- }
1659
- if (!a.startsWith("-")) {
1660
- projectDir = resolve(a);
1661
- }
1662
- }
1663
- if (!existsSync(projectDir) || !statSync(projectDir).isDirectory()) {
1664
- console.error(`waygraph traverse: no such directory: ${projectDir}`);
1665
- process.exit(1);
1666
- }
1667
- const blocksSelect = blocksFilter
1668
- ? parseBlocksSelect(blocksFilter)
1669
- : undefined;
1670
- let minEdgeCoverage;
1671
- if (minEdgeCoverageRaw !== undefined) {
1672
- const parsed = parseMinEdgeCoverage(minEdgeCoverageRaw);
1673
- if (parsed === null) {
1674
- console.error(`waygraph traverse: --min-edge-coverage expects 0.8, 80%, or 80 (got ${JSON.stringify(minEdgeCoverageRaw)})`);
1675
- process.exit(1);
1676
- }
1677
- minEdgeCoverage = parsed;
1678
- }
1679
- const code = await runTraverse(projectDir, {
1680
- ...(from ? { from } : {}),
1681
- ...(data ? { data } : {}),
1682
- ...(maxSteps !== undefined && Number.isFinite(maxSteps) ? { maxSteps } : {}),
1683
- ...(maxVisits !== undefined && Number.isFinite(maxVisits)
1684
- ? { maxVisitsPerNode: maxVisits }
1685
- : {}),
1686
- ...(maxEdge !== undefined && Number.isFinite(maxEdge)
1687
- ? { maxVisitsPerEdge: maxEdge }
1688
- : {}),
1689
- headed,
1690
- ...(baseUrl ? { baseURL: baseUrl } : {}),
1691
- ...(blocksSelect ? { blocksSelect } : {}),
1692
- ...(parallel !== undefined && Number.isFinite(parallel) ? { parallel } : {}),
1693
- ...(session ? { session } : {}),
1694
- ...(minEdgeCoverage !== undefined ? { minEdgeCoverage } : {}),
1695
- ...(coverageOut ? { coverageOut: resolve(coverageOut) } : {}),
1696
- ...(noCoverageReport ? { noCoverageReport: true } : {}),
1697
- });
1698
- process.exit(code);
1699
- }
1700
66
  case "agent-dive":
1701
- case "init-agents": {
1702
- // Playwright analogue: npx playwright init-agents --loop <provider>
1703
- const rest = args.slice(1);
1704
- let loop = "claude";
1705
- let prompts = false;
1706
- let projectDir = process.cwd();
1707
- for (let i = 0; i < rest.length; i++) {
1708
- const a = rest[i];
1709
- if (a === "--prompts") {
1710
- prompts = true;
1711
- continue;
1712
- }
1713
- if (a === "--loop" || a.startsWith("--loop=")) {
1714
- const v = a.startsWith("--loop=") ? a.slice("--loop=".length) : rest[++i];
1715
- if (!v || !["claude", "opencode", "cursor", "vscode"].includes(v)) {
1716
- console.error("waygraph agent-dive: --loop must be claude | opencode | cursor | vscode");
1717
- process.exit(1);
1718
- }
1719
- loop = v;
1720
- continue;
1721
- }
1722
- if (a === "--help" || a === "-h") {
1723
- console.log(`waygraph agent-dive [--loop claude|opencode|cursor|vscode] [--prompts] [project]
1724
-
1725
- Initialize coding-agent definitions for diving an app into waygraph Blocks
1726
- (Playwright \`init-agents\` analogue).
1727
-
1728
- --loop claude write .claude/agents/*.md (default)
1729
- --loop opencode write .opencode/prompts/*.md
1730
- --loop cursor write .cursor/rules/waygraph-*.mdc
1731
- --loop vscode write .github/agents/*.md
1732
- --prompts also copy docs/waygraph-agents/*.md
1733
-
1734
- Agents shipped: waygraph-planner, waygraph-author, waygraph-healer.
1735
- `);
1736
- process.exit(0);
1737
- }
1738
- if (!a.startsWith("-")) {
1739
- projectDir = resolve(a);
1740
- }
1741
- }
1742
- if (!existsSync(projectDir) || !statSync(projectDir).isDirectory()) {
1743
- console.error(`waygraph agent-dive: no such directory: ${projectDir}`);
1744
- process.exit(1);
1745
- }
1746
- runAgentDive({ loop, projectDir, prompts });
67
+ case "init-agents":
68
+ await agentDiveCase(args);
1747
69
  break;
1748
- }
1749
- case "chain": {
1750
- // Compat alias: chain auto A B -> auto --blocks A B; else -> run --blocks.
1751
- const flags = parseRunFlags(args.slice(1));
1752
- applyRunFlags(flags);
1753
- const first = flags.positionals[0];
1754
- if (first === "auto") {
1755
- const fromTag = flags.positionals[1];
1756
- const toTag = flags.positionals[2];
1757
- if (!fromTag || !toTag) {
1758
- console.error("waygraph chain auto: missing <fromCheckpoint> <toCheckpoint> [project]");
1759
- console.error(" prefer: waygraph auto --blocks LoggedIn OrderComplete");
1760
- process.exit(1);
1761
- }
1762
- const proj = resolve(flags.positionals[3] ?? process.cwd());
1763
- if (!existsSync(proj)) {
1764
- console.error(`waygraph: no such directory: ${proj}`);
1765
- process.exit(1);
1766
- }
1767
- await runChainAuto(proj, fromTag, toTag);
1768
- break;
1769
- }
1770
- const spec = flags.blocks ?? first;
1771
- if (!spec) {
1772
- console.error('waygraph chain: missing <spec> — prefer: waygraph run --blocks "a then b"\n' +
1773
- " or: waygraph auto --blocks <fromCheckpoint> <toCheckpoint>");
1774
- process.exit(1);
1775
- }
1776
- const proj = resolve(flags.blocks ? (flags.positionals[0] ?? process.cwd()) : (flags.positionals[1] ?? process.cwd()));
1777
- if (!process.env.WAYGRAPH_BASE_URL) {
1778
- const resolved = resolveBaseUrl(proj);
1779
- if (resolved)
1780
- process.env.WAYGRAPH_BASE_URL = resolved;
1781
- }
1782
- // If caller asked for step overlay, behave like demo; else like run.
1783
- if (flags.step === true || process.env.WAYGRAPH_STEP === "1") {
1784
- applyDemoDefaults(proj);
1785
- }
1786
- await runChain(proj, spec);
70
+ case "chain":
71
+ await chainCase(args);
1787
72
  break;
1788
- }
1789
- case "demo": {
1790
- const flags = parseRunFlags(args.slice(1));
1791
- applyRunFlags(flags, { allowAutoPlayVideo: true, allowDemoUi: true });
1792
- const spec = resolveSpec(flags);
1793
- if (!spec) {
1794
- console.error("waygraph demo: missing flow/spec — e.g.\n" +
1795
- " waygraph demo src/flows/shop.flow.ts\n" +
1796
- " waygraph demo --blocks shopFlow\n" +
1797
- " Flags: --blocks --data --mem-stub --auto-next --fast --full --mini --ff-expand --ff-disabled --auto-play-video --title --base-url --video");
1798
- process.exit(1);
1799
- }
1800
- const proj = flags.blocks
1801
- ? resolveProjectDir(flags, 0)
1802
- : resolve(flags.positionals[1] ?? process.cwd());
1803
- if (!existsSync(proj)) {
1804
- console.error(`waygraph: no such directory: ${proj}`);
1805
- process.exit(1);
1806
- }
1807
- if (flags.step === undefined && process.env.WAYGRAPH_STEP === undefined) {
1808
- process.env.WAYGRAPH_STEP = "1";
1809
- }
1810
- applyDemoDefaults(proj);
1811
- console.error(`waygraph demo: STEP=${process.env.WAYGRAPH_STEP === "1" ? "on" : "off"}` +
1812
- ` AUTO_NEXT=${process.env.WAYGRAPH_AUTOPLAY === "1" ? "on" : "off"}` +
1813
- ` HEADED=${process.env.WAYGRAPH_HEADED === "0" ? "off" : "on"}` +
1814
- ` BASE_URL=${process.env.WAYGRAPH_BASE_URL ?? "(unset)"}` +
1815
- (process.env.WAYGRAPH_VIDEO ? ` VIDEO=${process.env.WAYGRAPH_VIDEO}` : "") +
1816
- (process.env.WAYGRAPH_TITLE ? ` TITLE=${JSON.stringify(process.env.WAYGRAPH_TITLE)}` : ""));
1817
- await runChain(proj, spec);
73
+ case "demo":
74
+ await demoCase(args);
1818
75
  break;
1819
- }
1820
76
  case "graph":
1821
77
  case "auto":
1822
78
  case "browser":
1823
- case "pilot": {
1824
- if (command === "browser") {
1825
- const sub = args[1];
1826
- if (sub === "sessions") {
1827
- const rest = args.slice(2);
1828
- const json = rest.includes("--json");
1829
- const positional = rest.filter((a) => a !== "--json");
1830
- const proj = resolve(positional[0] ?? process.cwd());
1831
- const sessions = listBrowserSessions(proj);
1832
- if (json) {
1833
- console.log(JSON.stringify(sessions, null, 2));
1834
- }
1835
- else {
1836
- printBrowserSessionsList(sessions, proj);
1837
- }
1838
- break;
1839
- }
1840
- if (sub === "stop") {
1841
- const proj = resolve(args[3] ?? process.cwd());
1842
- const target = args[2];
1843
- if (!target) {
1844
- console.error("waygraph browser stop: usage: waygraph browser stop <sessionId|--all> [project]");
1845
- process.exit(1);
1846
- }
1847
- if (target === "--all") {
1848
- const n = stopAllBrowserSessions(proj);
1849
- console.log(JSON.stringify({ stopped: n, projectDir: proj }));
1850
- break;
1851
- }
1852
- if (!stopBrowserSession(proj, target)) {
1853
- console.error(`waygraph browser stop: no such session "${target}"`);
1854
- process.exit(1);
1855
- }
1856
- console.log(JSON.stringify({ stopped: target }));
1857
- break;
1858
- }
1859
- if (!sub) {
1860
- printBrowserUsage();
1861
- break;
1862
- }
1863
- if (sub === "start" || sub.startsWith("-")) {
1864
- const flagArgs = sub === "start" ? args.slice(2) : args.slice(1);
1865
- const flags = parseRunFlags(flagArgs);
1866
- applyRunFlags(flags);
1867
- const proj = resolve(flags.positionals[0] ?? process.cwd());
1868
- if (!existsSync(proj)) {
1869
- console.error(`waygraph browser: no such directory: ${proj}`);
1870
- process.exit(1);
1871
- }
1872
- const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? resolveBaseUrl(proj);
1873
- try {
1874
- const inject = flags.inject?.length ? resolveInjectRoots(flags.inject, proj) : undefined;
1875
- const result = await browserStart({
1876
- projectDir: proj,
1877
- ...(inject?.length ? { inject } : {}),
1878
- ...(baseURL ? { baseURL } : {}),
1879
- ...(flags.goto
1880
- ? { startUrl: flags.goto, skipInitialNavigation: false }
1881
- : { skipInitialNavigation: flags.blank !== false }),
1882
- headless: flags.headlessBrowser === true,
1883
- });
1884
- console.log(JSON.stringify(result));
1885
- if (!result.headless) {
1886
- console.error(`Session ${result.sessionId} started. List all: waygraph browser sessions`);
1887
- }
1888
- if (flags.cli) {
1889
- await runAttachLoop(proj, result.sessionId);
1890
- }
1891
- }
1892
- catch (err) {
1893
- console.error(`waygraph browser start: ${err instanceof Error ? err.message : String(err)}`);
1894
- process.exit(1);
1895
- }
1896
- break;
1897
- }
1898
- if (sub !== "send" &&
1899
- sub !== "status" &&
1900
- sub !== "attach" &&
1901
- sub !== "dom" &&
1902
- sub !== "trace" &&
1903
- sub !== "console" &&
1904
- sub !== "storage" &&
1905
- sub !== "upload" &&
1906
- sub !== "click" &&
1907
- sub !== "type" &&
1908
- sub !== "press" &&
1909
- sub !== "goto" &&
1910
- sub !== "reload" &&
1911
- sub !== "reach" &&
1912
- sub !== "resync" &&
1913
- sub !== "highlight") {
1914
- console.error("waygraph browser: unknown subcommand — run `waygraph browser` for usage");
1915
- printBrowserUsage();
1916
- process.exit(1);
1917
- }
1918
- }
1919
- if (command === "pilot") {
1920
- if (args[1] === "sessions") {
1921
- const rest = args.slice(2);
1922
- const json = rest.includes("--json");
1923
- const positional = rest.filter((a) => a !== "--json");
1924
- const proj = resolve(positional[0] ?? process.cwd());
1925
- const sessions = listBrowserSessions(proj);
1926
- if (json) {
1927
- console.log(JSON.stringify(sessions, null, 2));
1928
- }
1929
- else {
1930
- printBrowserSessionsList(sessions, proj);
1931
- }
1932
- break;
1933
- }
1934
- if (args[1] === "attach") {
1935
- const sessionId = args[2];
1936
- if (!sessionId) {
1937
- console.error("waygraph pilot attach: usage: waygraph pilot attach <sessionId> [--inject …] [project]");
1938
- process.exit(1);
1939
- }
1940
- const flags = parseRunFlags(args.slice(3));
1941
- const proj = resolve(flags.positionals[0] ?? process.cwd());
1942
- try {
1943
- const inject = flags.inject?.length ? resolveInjectRoots(flags.inject, proj) : undefined;
1944
- const result = await pilotAttach(proj, sessionId, inject);
1945
- console.log(JSON.stringify(result));
1946
- }
1947
- catch (err) {
1948
- console.error(`waygraph pilot attach: ${err instanceof Error ? err.message : String(err)}`);
1949
- process.exit(1);
1950
- }
1951
- break;
1952
- }
1953
- if (args[1] === "start" || !args[1] || args[1].startsWith("-")) {
1954
- const flagArgs = args[1] === "start" ? args.slice(2) : args.slice(1);
1955
- const flags = parseRunFlags(flagArgs);
1956
- applyRunFlags(flags);
1957
- const proj = resolve(flags.positionals[0] ?? process.cwd());
1958
- const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? resolveBaseUrl(proj);
1959
- try {
1960
- const result = await pilotStart({
1961
- projectDir: proj,
1962
- ...(flags.inject?.length ? { injectTokens: flags.inject } : {}),
1963
- ...(baseURL ? { baseURL } : {}),
1964
- ...(flags.goto
1965
- ? { startUrl: flags.goto, skipInitialNavigation: false }
1966
- : flags.blank === true
1967
- ? { skipInitialNavigation: true }
1968
- : {}),
1969
- headless: flags.headlessBrowser === true,
1970
- });
1971
- console.log(JSON.stringify(result));
1972
- if (!result.headless) {
1973
- console.error(`Chromium window opened (session ${result.sessionId}). ` +
1974
- `Terminal menu: waygraph pilot attach ${result.sessionId}`);
1975
- }
1976
- if (flags.cli) {
1977
- await runAttachLoop(proj, result.sessionId);
1978
- }
1979
- }
1980
- catch (err) {
1981
- console.error(`waygraph pilot start: ${err instanceof Error ? err.message : String(err)}`);
1982
- process.exit(1);
1983
- }
1984
- break;
1985
- }
1986
- if (args[1] !== "send" &&
1987
- args[1] !== "status" &&
1988
- args[1] !== "dom" &&
1989
- args[1] !== "trace" &&
1990
- args[1] !== "console" &&
1991
- args[1] !== "storage" &&
1992
- args[1] !== "upload" &&
1993
- args[1] !== "click" &&
1994
- args[1] !== "type" &&
1995
- args[1] !== "press" &&
1996
- args[1] !== "goto" &&
1997
- args[1] !== "reload" &&
1998
- args[1] !== "reach" &&
1999
- args[1] !== "resync" &&
2000
- args[1] !== "highlight") {
2001
- console.error("waygraph pilot: usage:\n" +
2002
- " waygraph pilot start [--inject …] [--goto <url>] [--headless] [--cli] [--base-url <url>] [project]\n" +
2003
- " waygraph pilot sessions [project] # list live browser sessions\n" +
2004
- " waygraph pilot attach <sessionId> # graph + snapshot for an existing session\n" +
2005
- " waygraph pilot send|status|attach|highlight|… <sessionId> … (controls browser — same as browser/auto)");
2006
- process.exit(1);
2007
- }
2008
- }
2009
- // auto send|status|attach <sessionId> - session control against a
2010
- // --detach'd background session. Intercepted before the normal
2011
- // project-directory resolution below, same pattern `try demo|auto|
2012
- // auto:cli` already uses for a sub-verb positional.
2013
- if ((command === "auto" || command === "browser" || command === "pilot") &&
2014
- (args[1] === "send" ||
2015
- args[1] === "status" ||
2016
- (args[1] === "attach" && command !== "pilot") ||
2017
- args[1] === "dom" ||
2018
- args[1] === "trace" ||
2019
- args[1] === "console" ||
2020
- args[1] === "storage" ||
2021
- args[1] === "upload" ||
2022
- args[1] === "click" ||
2023
- args[1] === "type" ||
2024
- args[1] === "press" ||
2025
- args[1] === "goto" ||
2026
- args[1] === "reload" ||
2027
- args[1] === "reach" ||
2028
- args[1] === "resync" ||
2029
- args[1] === "highlight")) {
2030
- const sub = args[1];
2031
- const sessionId = args[2];
2032
- if (!sessionId) {
2033
- console.error(`waygraph ${command} ${sub}: missing <sessionId>`);
2034
- process.exit(1);
2035
- }
2036
- const projHint = resolve(process.cwd());
2037
- const meta = resolveSessionMeta(sessionId, projHint);
2038
- if (!meta) {
2039
- console.error(`waygraph ${command} ${sub}: no such session "${sessionId}" — ` +
2040
- "wrong directory? run `waygraph pilot sessions` from the project that started it");
2041
- process.exit(1);
2042
- }
2043
- const proj = meta.projectDir;
2044
- if (sub === "attach") {
2045
- await runAttachLoop(proj, sessionId);
2046
- break;
2047
- }
2048
- if (sub === "send") {
2049
- const pick = args[3];
2050
- if (pick === undefined) {
2051
- console.error('waygraph auto send: usage: waygraph auto send <sessionId> "<pick>" [--timeout <ms>]');
2052
- process.exit(1);
2053
- }
2054
- // Real, direct need found live: a single Block can legitimately
2055
- // run a genuinely slow real interaction (e.g. a multi-step mouse
2056
- // drag) that exceeds the 15s default - same reasoning `auto
2057
- // reach` already gets a 90s default for (multiple Blocks in one
2058
- // call). Unlike reach, send has no way to know ahead of time
2059
- // whether the ONE Block it's running is fast or slow, so this is
2060
- // opt-in via a flag rather than a raised default.
2061
- let timeoutMs;
2062
- for (let i = 4; i < args.length; i++) {
2063
- if (args[i] === "--timeout") {
2064
- const raw = args[++i];
2065
- const n = Number(raw);
2066
- if (!Number.isFinite(n) || n <= 0) {
2067
- console.error(`waygraph auto send: --timeout must be a positive number of ms, got "${raw}"`);
2068
- process.exit(1);
2069
- }
2070
- timeoutMs = n;
2071
- }
2072
- }
2073
- const res = await requestSession(proj, sessionId, { op: "send", pick }, timeoutMs);
2074
- console.log(JSON.stringify(res));
2075
- if (!res.ok)
2076
- process.exitCode = 1;
2077
- break;
2078
- }
2079
- if (sub === "dom") {
2080
- const domArgs = args.slice(3);
2081
- let mode;
2082
- let selector;
2083
- let depth;
2084
- for (let i = 0; i < domArgs.length; i++) {
2085
- const a = domArgs[i];
2086
- if (a === "--mode" || a.startsWith("--mode=")) {
2087
- const v = a.startsWith("--mode=") ? a.slice("--mode=".length) : domArgs[++i];
2088
- if (v !== "aria" && v !== "full") {
2089
- console.error(`waygraph auto dom: --mode must be "aria" or "full", got "${v}"`);
2090
- process.exit(1);
2091
- }
2092
- mode = v;
2093
- }
2094
- else if (a === "--selector" || a.startsWith("--selector=")) {
2095
- selector = a.startsWith("--selector=") ? a.slice("--selector=".length) : domArgs[++i];
2096
- }
2097
- else if (a === "--depth" || a.startsWith("--depth=")) {
2098
- const raw = a.startsWith("--depth=") ? a.slice("--depth=".length) : domArgs[++i];
2099
- const n = Number(raw);
2100
- if (!Number.isInteger(n) || n < 1) {
2101
- console.error(`waygraph auto dom: --depth must be a positive integer, got "${raw}"`);
2102
- process.exit(1);
2103
- }
2104
- depth = n;
2105
- }
2106
- else {
2107
- console.error(`waygraph auto dom: unrecognized argument "${a}"`);
2108
- process.exit(1);
2109
- }
2110
- }
2111
- const res = await requestSession(proj, sessionId, {
2112
- op: "dom",
2113
- ...(mode ? { mode } : {}),
2114
- ...(selector ? { selector } : {}),
2115
- ...(depth !== undefined ? { depth } : {}),
2116
- });
2117
- console.log(JSON.stringify(res));
2118
- if (!res.ok)
2119
- process.exitCode = 1;
2120
- break;
2121
- }
2122
- if (sub === "trace") {
2123
- const res = await requestSession(proj, sessionId, { op: "trace" });
2124
- console.log(JSON.stringify(res));
2125
- if (!res.ok)
2126
- process.exitCode = 1;
2127
- break;
2128
- }
2129
- if (sub === "console") {
2130
- const res = await requestSession(proj, sessionId, { op: "console" });
2131
- console.log(JSON.stringify(res));
2132
- if (!res.ok)
2133
- process.exitCode = 1;
2134
- break;
2135
- }
2136
- if (sub === "storage") {
2137
- const res = await requestSession(proj, sessionId, { op: "storage" });
2138
- console.log(JSON.stringify(res));
2139
- if (!res.ok)
2140
- process.exitCode = 1;
2141
- break;
2142
- }
2143
- if (sub === "click") {
2144
- const selector = args[3];
2145
- if (selector === undefined) {
2146
- console.error('waygraph auto click: usage: waygraph auto click <sessionId> "<selector>"');
2147
- process.exit(1);
2148
- }
2149
- const res = await requestSession(proj, sessionId, { op: "click", selector });
2150
- console.log(JSON.stringify(res));
2151
- if (!res.ok)
2152
- process.exitCode = 1;
2153
- break;
2154
- }
2155
- if (sub === "type") {
2156
- const selector = args[3];
2157
- const text = args[4];
2158
- if (selector === undefined || text === undefined) {
2159
- console.error('waygraph auto type: usage: waygraph auto type <sessionId> "<selector>" "<text>"');
2160
- process.exit(1);
2161
- }
2162
- const res = await requestSession(proj, sessionId, { op: "type", selector, text });
2163
- console.log(JSON.stringify(res));
2164
- if (!res.ok)
2165
- process.exitCode = 1;
2166
- break;
2167
- }
2168
- if (sub === "press") {
2169
- const selector = args[3];
2170
- const key = args[4];
2171
- if (selector === undefined || key === undefined) {
2172
- console.error('waygraph auto press: usage: waygraph auto press <sessionId> "<selector>" <key>');
2173
- process.exit(1);
2174
- }
2175
- const res = await requestSession(proj, sessionId, { op: "press", selector, key });
2176
- console.log(JSON.stringify(res));
2177
- if (!res.ok)
2178
- process.exitCode = 1;
2179
- break;
2180
- }
2181
- if (sub === "goto") {
2182
- const url = args[3];
2183
- if (url === undefined) {
2184
- console.error("waygraph auto goto: usage: waygraph auto goto <sessionId> <url>");
2185
- process.exit(1);
2186
- }
2187
- const res = await requestSession(proj, sessionId, { op: "goto", url });
2188
- console.log(JSON.stringify(res));
2189
- if (!res.ok)
2190
- process.exitCode = 1;
2191
- break;
2192
- }
2193
- if (sub === "upload") {
2194
- const selector = args[3];
2195
- const kindOrPath = args[4];
2196
- if (selector === undefined || kindOrPath === undefined) {
2197
- console.error('waygraph auto upload: usage: waygraph auto upload <sessionId> "<selector>" <image|pdf|video|path-to-file>');
2198
- process.exit(1);
2199
- }
2200
- const stub = kindOrPath === "image" || kindOrPath === "pdf" || kindOrPath === "video"
2201
- ? { selector, stub: kindOrPath }
2202
- : { selector, stub: { filePath: resolve(process.cwd(), kindOrPath) } };
2203
- const res = await requestSession(proj, sessionId, { op: "upload", ...stub });
2204
- console.log(JSON.stringify(res));
2205
- if (!res.ok)
2206
- process.exitCode = 1;
2207
- break;
2208
- }
2209
- if (sub === "reload") {
2210
- const res = await requestSession(proj, sessionId, { op: "reload" });
2211
- console.log(JSON.stringify(res));
2212
- if (!res.ok)
2213
- process.exitCode = 1;
2214
- break;
2215
- }
2216
- if (sub === "reach") {
2217
- const checkpoint = args[3];
2218
- if (checkpoint === undefined) {
2219
- console.error("waygraph auto reach: usage: waygraph auto reach <sessionId> <Checkpoint>");
2220
- process.exit(1);
2221
- }
2222
- // Longer than every other op's default (15s): a real multi-step
2223
- // route runs several real Blocks in sequence server-side before
2224
- // responding - a legitimately slow single request, not a hang.
2225
- const res = await requestSession(proj, sessionId, { op: "reach", checkpoint }, 90_000);
2226
- console.log(JSON.stringify(res));
2227
- if (!res.ok)
2228
- process.exitCode = 1;
2229
- break;
2230
- }
2231
- if (sub === "resync") {
2232
- const res = await requestSession(proj, sessionId, { op: "resync" });
2233
- console.log(JSON.stringify(res));
2234
- if (!res.ok)
2235
- process.exitCode = 1;
2236
- break;
2237
- }
2238
- if (sub === "highlight") {
2239
- const raw = args[3];
2240
- if (raw === undefined) {
2241
- console.error(`waygraph ${command} highlight: usage:\n` +
2242
- ` waygraph ${command} highlight <sessionId> '{"rings":[{"selector":"#x","label":"X"}]}'\n` +
2243
- ` waygraph ${command} highlight <sessionId> "#x|X" (shorthand: <selector>|<label>[|<tone>], rings separated by ";")`);
2244
- process.exit(1);
2245
- }
2246
- let fixtures;
2247
- try {
2248
- fixtures = JSON.parse(raw);
2249
- }
2250
- catch (jsonErr) {
2251
- const shorthand = parseHighlightShorthand(raw);
2252
- if (shorthand.type === "error") {
2253
- console.error(`waygraph ${command} highlight: body is neither valid JSON (${jsonErr instanceof Error ? jsonErr.message : String(jsonErr)}) nor valid shorthand (${shorthand.reason})`);
2254
- process.exit(1);
2255
- }
2256
- fixtures = shorthand.fixtures;
2257
- }
2258
- if (fixtures.op !== undefined && fixtures.op !== "highlight") {
2259
- console.error(`waygraph ${command} highlight: do not set "op" (or set it to "highlight")`);
2260
- process.exit(1);
2261
- }
2262
- const { op: _ignore, ...rest } = fixtures;
2263
- const res = await requestSession(proj, sessionId, {
2264
- op: "highlight",
2265
- ...rest,
2266
- });
2267
- console.log(JSON.stringify(res));
2268
- if (!res.ok)
2269
- process.exitCode = 1;
2270
- break;
2271
- }
2272
- const res = await requestSession(proj, sessionId, { op: "status" });
2273
- console.log(JSON.stringify(res));
2274
- if (!res.ok)
2275
- process.exitCode = 1;
2276
- break;
2277
- }
2278
- const flags = parseRunFlags(args.slice(1));
2279
- applyRunFlags(flags);
2280
- const mermaid = flags.mermaid === true;
2281
- const mapOnly = command === "graph" || flags.map === true;
2282
- const cliPicker = flags.cli === true;
2283
- const firstPos = flags.positionals[0];
2284
- // auto src/flows/shop.flow.ts → run that flow (same as `run`). Do not
2285
- // treat a .flow.ts path as a project directory (existsSync is true for files).
2286
- if (command === "auto" && firstPos && looksLikeFlowFileRef(firstPos) && !flags.blocksFromTo) {
2287
- const proj = resolve(process.cwd());
2288
- if (!existsSync(proj) || !statSync(proj).isDirectory()) {
2289
- console.error(`waygraph: no such directory: ${proj}`);
2290
- process.exit(1);
2291
- }
2292
- if (!process.env.WAYGRAPH_BASE_URL) {
2293
- const resolved = resolveBaseUrl(proj);
2294
- if (resolved)
2295
- process.env.WAYGRAPH_BASE_URL = resolved;
2296
- }
2297
- await runChain(proj, firstPos);
2298
- break;
2299
- }
2300
- const proj = resolve(firstPos ?? process.cwd());
2301
- if (!existsSync(proj)) {
2302
- console.error(`waygraph: no such directory: ${proj}`);
2303
- process.exit(1);
2304
- }
2305
- if (!statSync(proj).isDirectory()) {
2306
- console.error(`waygraph ${command}: "${firstPos}" is a file, not a project directory\n` +
2307
- " run a flow: waygraph auto src/flows/shop.flow.ts\n" +
2308
- " or explore: waygraph auto\n" +
2309
- " or path-find: waygraph auto --blocks LoginPage OrderComplete");
2310
- process.exit(1);
2311
- }
2312
- // auto --cli --detach: start the explore session as a background socket
2313
- // server instead of blocking in the interactive loop. Session control
2314
- // only applies to --cli (spec: "Headful mode is unaffected").
2315
- if (command === "auto" && flags.detach) {
2316
- if (!cliPicker) {
2317
- console.error("waygraph auto --detach requires --cli (headful has no session control)");
2318
- process.exit(1);
2319
- }
2320
- const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? resolveBaseUrl(proj);
2321
- try {
2322
- const meta = await spawnDetachedSession({
2323
- projectDir: proj,
2324
- ...(baseURL ? { baseURL } : {}),
2325
- ...(flags.nonHeadless ? { headless: false } : {}),
2326
- });
2327
- console.log(JSON.stringify({ sessionId: meta.sessionId, socketPath: meta.socketPath, headless: meta.headless }));
2328
- }
2329
- catch (err) {
2330
- console.error(`waygraph auto --detach: ${err instanceof Error ? err.message : String(err)}`);
2331
- process.exit(1);
2332
- }
2333
- break;
2334
- }
2335
- // auto --blocks From To (graph path-find + run)
2336
- // auto --blocks '/regex/' or '**/*.block.ts' (Phase C file select → explore)
2337
- if (command === "auto" && (flags.blocksFromTo || flags.blocks)) {
2338
- if (flags.blocksFromTo) {
2339
- const [fromTag, toTag] = flags.blocksFromTo;
2340
- await runChainAuto(proj, fromTag, toTag);
2341
- break;
2342
- }
2343
- if (flags.blocks && isFileSelectToken(flags.blocks)) {
2344
- const blocksSelect = parseBlocksSelect(flags.blocks);
2345
- const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? resolveBaseUrl(proj);
2346
- await runAutoExplore(proj, baseURL
2347
- ? { cli: cliPicker, baseURL, blocksSelect }
2348
- : { cli: cliPicker, blocksSelect });
2349
- break;
2350
- }
2351
- console.error("waygraph auto --blocks expects <fromCheckpoint> <toCheckpoint>\n" +
2352
- " or file select: --blocks '**/mailpit/**/*.block.ts' / --blocks '/mailpit/'\n" +
2353
- ' e.g. waygraph auto --blocks LoginPage OrderComplete\n' +
2354
- ' for a hand-named chain use: waygraph run --blocks "a then b"\n' +
2355
- " or: waygraph auto src/flows/shop.flow.ts");
2356
- process.exit(1);
2357
- }
2358
- if (mapOnly || mermaid) {
2359
- const orphans = await findOrphanBlocks(proj);
2360
- const graph = await discoverGraph(proj);
2361
- if (mermaid) {
2362
- console.log(toMermaid(graph));
2363
- }
2364
- else {
2365
- console.log(JSON.stringify({ ...graph, orphans }, null, 2));
2366
- }
2367
- console.error(`waygraph graph: ${graph.nodes.length} node(s), ${graph.edges.length} edge(s), ` +
2368
- `${graph.skipped.length} Block(s) skipped (Out not resolvable), ` +
2369
- `${orphans.length} orphan Block(s)`);
2370
- if (orphans.length > 0) {
2371
- console.error("waygraph graph: orphan Blocks block auto --blocks path-find - wire each into a .flow.ts (waygraph check)");
2372
- }
2373
- break;
2374
- }
2375
- const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? resolveBaseUrl(proj);
2376
- await runAutoExplore(proj, baseURL ? { cli: cliPicker, baseURL } : { cli: cliPicker });
79
+ case "pilot":
80
+ await sessionCase(args, command);
2377
81
  break;
2378
- }
2379
82
  // Hidden: the detached server's own entry point, spawned by
2380
83
  // `spawnDetachedSession` (auto --cli --detach). Not documented in
2381
84
  // usage() - not meant to be invoked directly by a person.
2382
- case "__auto-serve": {
2383
- const proj = resolve(args[1] ?? process.cwd());
2384
- const flags = parseRunFlags(args.slice(2));
2385
- const sessionIdIdx = args.indexOf("--session-id");
2386
- const sessionId = sessionIdIdx >= 0 ? args[sessionIdIdx + 1] : undefined;
2387
- if (!sessionId) {
2388
- console.error("waygraph __auto-serve: missing --session-id");
2389
- process.exit(1);
2390
- }
2391
- const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? resolveBaseUrl(proj);
2392
- const inject = flags.inject?.length ? resolveInjectRoots(flags.inject, proj) : undefined;
2393
- let headless;
2394
- if (flags.headlessBrowser)
2395
- headless = true;
2396
- else if (flags.nonHeadless)
2397
- headless = false;
2398
- await runAutoServeCommand({
2399
- projectDir: proj,
2400
- ...(baseURL ? { baseURL } : {}),
2401
- ...(flags.goto
2402
- ? { startUrl: flags.goto, skipInitialNavigation: false }
2403
- : flags.blank === true
2404
- ? { skipInitialNavigation: true }
2405
- : {}),
2406
- ...(headless !== undefined ? { headless } : {}),
2407
- ...(inject?.length ? { inject } : {}),
2408
- }, sessionId);
85
+ case "__auto-serve":
86
+ await autoServeCase(args);
2409
87
  break;
2410
- }
2411
- case "try": {
2412
- const flags = parseRunFlags(args.slice(1));
2413
- applyRunFlags(flags);
2414
- const modeRaw = (flags.positionals[0] ?? "demo").toLowerCase();
2415
- // auto:cli / --cli = CLI; bare try auto = headed panel
2416
- const mode = modeRaw === "auto:cli" || modeRaw === "auto-cli"
2417
- ? "auto:cli"
2418
- : modeRaw === "auto"
2419
- ? "auto"
2420
- : modeRaw;
2421
- if (mode === "auto" || mode === "auto:cli") {
2422
- const cli = mode === "auto:cli" || flags.cli === true
2423
- ? true
2424
- : flags.headed === true || flags.nonHeadless === true
2425
- ? false
2426
- : false; // try auto default = headed
2427
- await runTryAuto({ cli });
2428
- }
2429
- else if (mode === "demo" || mode === "") {
2430
- await runTryDemo();
2431
- }
2432
- else {
2433
- console.error(`waygraph try: unknown mode "${modeRaw}" - use "demo", "auto", or "auto:cli"`);
2434
- process.exitCode = 1;
2435
- }
88
+ case "try":
89
+ await tryCase(args);
2436
90
  break;
2437
- }
2438
- case "typecheck": {
2439
- const flags = parseRunFlags(args.slice(1));
2440
- const proj = resolve(flags.positionals[0] ?? process.cwd());
2441
- await runTypecheckCommand(proj, flags.noPractices === true);
91
+ case "typecheck":
92
+ await typecheckCase(args);
2442
93
  break;
2443
- }
2444
- case "check": {
2445
- const flags = parseRunFlags(args.slice(1));
2446
- const proj = resolve(flags.positionals[0] ?? process.cwd());
2447
- const { navWarnings, selWarnings, practiceWarnings } = await checkCommand(proj, flags.noPractices ? { noPractices: true } : undefined);
2448
- if (navWarnings.length === 0) {
2449
- console.log(`waygraph check: no navigation found outside NavBlocks under ${proj}`);
2450
- }
2451
- else {
2452
- for (const w of navWarnings) {
2453
- const rel = relative(proj, w.file);
2454
- console.warn(`waygraph check: ${rel} (${w.blockName}) calls page.goto/reload/goBack/goForward outside a NavBlock - consider defineNavBlock instead`);
2455
- }
2456
- console.log(`waygraph check: ${navWarnings.length} nav warning${navWarnings.length === 1 ? "" : "s"}`);
2457
- }
2458
- if (selWarnings.length === 0) {
2459
- console.log(`waygraph check: no inline selectors found in verify arrays under ${proj}`);
2460
- }
2461
- else {
2462
- for (const w of selWarnings) {
2463
- const rel = relative(proj, w.file);
2464
- console.warn(`waygraph check: ${rel} (${w.blockName}) has an inline selector literal in verify - move it into a *Sel object`);
2465
- }
2466
- console.log(`waygraph check: ${selWarnings.length} inline-selector warning${selWarnings.length === 1 ? "" : "s"}`);
2467
- }
2468
- if (!flags.noPractices) {
2469
- printPracticeReport(proj, practiceWarnings);
2470
- }
2471
- const orphans = await findOrphanBlocks(proj);
2472
- printOrphanReport(proj, orphans);
2473
- // Warnings only - never fail exit code. Orphans are reported for
2474
- // human/agent cleanup; chain auto refuses while any remain.
94
+ case "check":
95
+ await checkCase(args);
2475
96
  break;
2476
- }
2477
- case "map": {
2478
- const proj = resolve(args[1] ?? process.cwd());
2479
- const result = await checkMap(proj);
2480
- if (!result.hasMapDir) {
2481
- console.log(`waygraph map: no src/map/ directory under ${proj} - nothing to check ` +
2482
- "(this project isn't on the Waygraph Map convention, or hasn't been migrated yet)");
2483
- break;
2484
- }
2485
- console.log(`waygraph map: ${result.nodes.length} node(s) found under ${relative(proj, result.mapRoot)}`);
2486
- if (result.violations.length === 0) {
2487
- console.log("waygraph map: 0 violations - every static Nav/Page url verbatim-matches its folder path");
2488
- }
2489
- else {
2490
- for (const v of result.violations) {
2491
- console.error(`waygraph map: ${v.file} (${v.block}) - ${v.reason}`);
2492
- }
2493
- console.error(`waygraph map: ${result.violations.length} violation${result.violations.length === 1 ? "" : "s"}`);
2494
- // Unlike `check`'s warnings-only stance, a Map violation is exactly
2495
- // the class of bug this command exists to catch (a fabricated
2496
- // folder grouping that never matched the real site, e.g. the real
2497
- // (auth)/signin/ mistake this command's own header comment cites) -
2498
- // fail loud, not just warn.
2499
- process.exitCode = 1;
2500
- }
97
+ case "map":
98
+ await mapCase(args);
2501
99
  break;
2502
- }
2503
100
  default:
2504
101
  console.error(`waygraph: unknown command "${command}"`);
2505
102
  process.exit(1);