waygraph 0.15.23 → 0.15.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/discover.d.ts +40 -0
- package/dist/cli/discover.js +148 -0
- package/dist/cli/flags.d.ts +91 -0
- package/dist/cli/flags.js +442 -0
- package/dist/cli/run-chain.d.ts +8 -0
- package/dist/cli/run-chain.js +67 -0
- package/dist/cli/usage.d.ts +4 -0
- package/dist/cli/usage.js +236 -0
- package/dist/cli/util.d.ts +4 -0
- package/dist/cli/util.js +24 -0
- package/dist/cli.js +45 -2448
- package/dist/commands/agent-dive.d.ts +1 -0
- package/dist/commands/agent-dive.js +52 -0
- package/dist/commands/check.d.ts +4 -0
- package/dist/commands/check.js +192 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +54 -0
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/list.js +67 -0
- package/dist/commands/map.d.ts +1 -0
- package/dist/commands/map.js +29 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +104 -0
- package/dist/commands/session.d.ts +2 -0
- package/dist/commands/session.js +600 -0
- package/dist/commands/test.d.ts +1 -0
- package/dist/commands/test.js +33 -0
- package/dist/commands/traverse.d.ts +1 -0
- package/dist/commands/traverse.js +177 -0
- package/dist/commands/try.d.ts +1 -0
- package/dist/commands/try.js +298 -0
- package/examples/saucedemo/.waygraph-auto/0ac58118.log +0 -0
- package/examples/saucedemo/.waygraph-auto/309c107f.log +0 -0
- package/examples/saucedemo/.waygraph-auto/3ab7c7bd.log +44 -0
- package/examples/saucedemo/.waygraph-auto/45864828.log +0 -0
- package/examples/saucedemo/.waygraph-auto/95bd8371.log +0 -0
- package/examples/saucedemo/.waygraph-auto/bcb9e31a.log +0 -0
- package/examples/saucedemo/.waygraph-auto/cd3e528c.log +0 -0
- package/examples/saucedemo/.waygraph-auto/dce1535d.log +0 -0
- package/examples/saucedemo/.waygraph-auto/e280bf20.log +0 -0
- 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 {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
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
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|