waygraph 0.16.0 → 0.16.2
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/commands/browser.d.ts +2 -0
- package/dist/cli/commands/browser.js +109 -0
- package/dist/cli/commands/codemod-stubs.js +90 -2
- package/dist/cli/commands/pilot.d.ts +2 -0
- package/dist/cli/commands/pilot.js +102 -0
- package/dist/cli/commands/session-ops.d.ts +3 -0
- package/dist/cli/commands/session-ops.js +257 -0
- package/dist/cli/commands/session.js +20 -461
- package/dist/cli/run-chain.js +2 -2
- package/dist/cli/usage.d.ts +1 -1
- package/dist/cli/util.js +1 -1
- package/dist/demo/narration/demo-pace.d.ts +2 -2
- package/dist/demo/narration/demo-pace.js +1 -1
- package/dist/demo/narration/device.d.ts +2 -2
- package/dist/demo/narration/merge.d.ts +5 -3
- package/dist/demo/narration/merge.js +3 -7
- package/dist/demo/narration/narration.d.ts +9 -13
- package/dist/demo/narration/narration.js +5 -17
- package/dist/demo/narration/stub-ctx.js +3 -3
- package/dist/demo/narration/todo-dock.d.ts +5 -5
- package/dist/demo/overlay/cursor.d.ts +9 -2
- package/dist/demo/overlay/cursor.js +1 -1
- package/dist/demo/overlay/device-stage.d.ts +15 -2
- package/dist/demo/overlay/inpage/banner.d.ts +2 -1
- package/dist/demo/overlay/inpage/banner.js +3 -3
- package/dist/demo/overlay/inpage/caption.d.ts +1 -1
- package/dist/demo/overlay/inpage/core.d.ts +2 -1
- package/dist/demo/overlay/inpage/core.js +1 -7
- package/dist/demo/overlay/inpage/device-chip.d.ts +1 -1
- package/dist/demo/overlay/inpage/device-chip.js +6 -6
- package/dist/demo/overlay/inpage/network.d.ts +3 -2
- package/dist/demo/overlay/inpage/network.js +1 -12
- package/dist/demo/overlay/inpage/panel-chrome.d.ts +1 -1
- package/dist/demo/overlay/inpage/panel-chrome.js +1 -1
- package/dist/demo/overlay/inpage/stamp.d.ts +1 -1
- package/dist/demo/overlay/inpage/stamp.js +1 -1
- package/dist/demo/overlay/inpage/swipe-zoom.d.ts +1 -1
- package/dist/demo/overlay/inpage/swipe-zoom.js +1 -1
- package/dist/demo/overlay/inpage/todos.d.ts +2 -1
- package/dist/demo/overlay/inpage/todos.js +1 -2
- package/dist/demo/overlay/network-capture.d.ts +2 -2
- package/dist/demo/overlay/network-capture.js +1 -1
- package/dist/demo/overlay/overlay-css.js +1 -55
- package/dist/demo/overlay/overlay-stage.d.ts +19 -11
- package/dist/demo/overlay/overlay.d.ts +2 -2
- package/dist/demo/overlay/overlay.js +3 -2
- package/dist/demo/overlay/rings.d.ts +38 -18
- package/dist/demo/overlay/rings.js +21 -62
- package/dist/demo/overlay/ui/overlay.css +40 -0
- package/dist/demo/stepper/after.js +11 -34
- package/dist/demo/stepper/banner-log.d.ts +4 -6
- package/dist/demo/stepper/banner-log.js +3 -1
- package/dist/demo/stepper/before.js +6 -4
- package/dist/demo/stepper/demo-log.d.ts +26 -9
- package/dist/demo/stepper/demo-log.js +10 -27
- package/dist/demo/stepper/interaction-patch.d.ts +3 -2
- package/dist/demo/stepper/interaction-patch.js +22 -34
- package/dist/demo/stepper/panel-after.d.ts +17 -0
- package/dist/demo/stepper/panel-after.js +166 -0
- package/dist/demo/stepper/panel-before.d.ts +12 -0
- package/dist/demo/stepper/panel-before.js +241 -0
- package/dist/demo/stepper/slides.d.ts +16 -1
- package/dist/demo/stepper/slides.js +21 -5
- package/dist/demo/stepper/step-panels.d.ts +57 -9
- package/dist/demo/stepper/step-panels.js +10 -427
- package/dist/demo/stepper/stepper.js +1 -1
- package/dist/demo/stepper/teardown.d.ts +4 -5
- package/dist/demo/stepper/teardown.js +4 -6
- package/dist/demo/stepper/todo-advance.d.ts +3 -2
- package/dist/demo/stepper/todo-advance.js +2 -2
- package/dist/demo/stepper/view.d.ts +4 -14
- package/dist/demo/types.d.ts +69 -1
- package/dist/demo/verbose-log.js +1 -1
- package/dist/engine/block/action-page.d.ts +53 -0
- package/dist/engine/block/action-page.js +1 -0
- package/dist/engine/block/block.d.ts +24 -90
- package/dist/engine/block/block.js +2 -77
- package/dist/engine/block/branch.d.ts +17 -0
- package/dist/engine/block/branch.js +40 -0
- package/dist/engine/block/dialog.d.ts +13 -0
- package/dist/engine/block/dialog.js +37 -0
- package/dist/engine/block/kinds/method.d.ts +0 -2
- package/dist/engine/block/kinds/method.js +0 -1
- package/dist/engine/block/kinds/nav.js +1 -1
- package/dist/engine/flow/map-branch.d.ts +9 -0
- package/dist/engine/flow/map-branch.js +57 -0
- package/dist/engine/flow/map-builder.d.ts +7 -22
- package/dist/engine/flow/map-builder.js +6 -145
- package/dist/engine/flow/map-steps.d.ts +30 -0
- package/dist/engine/flow/map-steps.js +87 -0
- package/dist/engine/run/run.d.ts +1 -19
- package/dist/engine/run/run.js +0 -88
- package/dist/engine/stub/ring.d.ts +2 -4
- package/dist/engine/stub/ring.js +1 -0
- package/dist/engine/stub/stub.d.ts +16 -8
- package/dist/engine/trait/trait.js +1 -1
- package/dist/index.d.ts +15 -12
- package/dist/index.js +9 -7
- package/dist/live/auto/auto.d.ts +3 -15
- package/dist/live/auto/auto.js +10 -420
- package/dist/live/auto/explore.d.ts +2 -2
- package/dist/live/auto/mem.d.ts +11 -0
- package/dist/live/auto/mem.js +85 -0
- package/dist/live/auto/menu.d.ts +30 -0
- package/dist/live/auto/menu.js +335 -0
- package/dist/live/auto-session/auto-session.d.ts +23 -103
- package/dist/live/auto-session/auto-session.js +100 -370
- package/dist/live/auto-session/console.d.ts +4 -0
- package/dist/live/auto-session/console.js +41 -0
- package/dist/live/auto-session/files.d.ts +44 -0
- package/dist/live/auto-session/files.js +145 -0
- package/dist/live/auto-session/helpers.d.ts +5 -2
- package/dist/live/auto-session/helpers.js +17 -0
- package/dist/live/auto-session/inspect.d.ts +4 -0
- package/dist/live/auto-session/inspect.js +75 -0
- package/dist/live/auto-session/ipc.d.ts +3 -22
- package/dist/live/auto-session/ipc.js +7 -147
- package/dist/live/auto-session/open-browser.d.ts +6 -0
- package/dist/live/auto-session/open-browser.js +47 -0
- package/dist/live/pilot/pilot.d.ts +1 -1
- package/dist/live/pilot/pilot.js +2 -1
- package/dist/live/traverse/traverse.js +6 -200
- package/dist/live/traverse/worker.d.ts +37 -0
- package/dist/live/traverse/worker.js +200 -0
- package/dist/runner/plan/find.js +2 -2
- package/dist/runner/plan/plan.js +1 -1
- package/dist/runner/plan/seed-mem.js +5 -5
- package/dist/runner/plan/spec.js +1 -1
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
- package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +2 -1
- package/examples/saucedemo/src/flows/checkout.flow.ts +2 -1
- package/examples/saucedemo/src/flows/shop.flow.ts +2 -1
- package/examples/saucedemo/src/stubs/device-showcase.ts +37 -43
- package/examples/saucedemo/src/stubs/finish-order.ts +38 -0
- package/package.json +5 -1
- package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
- package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +2 -1
- package/templates/quickstart/src/flows/checkout.flow.ts +2 -1
- package/templates/quickstart/src/flows/shop.flow.ts +2 -1
- package/templates/quickstart/src/stubs/device-showcase.ts +37 -43
- package/templates/quickstart/src/stubs/finish-order.ts +38 -0
- package/templates/scaffold/src/flows/home/hello.flow.ts +3 -0
- package/templates/scaffold/src/map/(app_base)/home/_methods/assert-hello.method.block.ts +1 -9
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// `waygraph browser start | sessions | stop`: start a detached session, list them, stop them. The session ops
|
|
2
|
+
// (send, status, ...) are session-ops.ts.
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { envString } from "../../engine/env.js";
|
|
6
|
+
import { listBrowserSessions, stopAllBrowserSessions, stopBrowserSession } from "../../live/auto-session/files.js";
|
|
7
|
+
import { runAttachLoop } from "../../live/auto-session/ipc.js";
|
|
8
|
+
import { browserStart } from "../../live/auto-session/start.js";
|
|
9
|
+
import { resolveInjectRoots } from "../../manifest/roots.js";
|
|
10
|
+
import { resolveBaseUrl } from "../../runtime/waygraph-config.js";
|
|
11
|
+
import { parseCommand } from "../args.js";
|
|
12
|
+
import { applyRunFlags, parseRunFlags } from "../flags.js";
|
|
13
|
+
import { printBrowserSessionsList, printBrowserUsage } from "../usage.js";
|
|
14
|
+
/** Runs a `browser` subcommand; false when it is a session op for session-ops.ts. */
|
|
15
|
+
export async function browserCase(args) {
|
|
16
|
+
const sub = args[1];
|
|
17
|
+
if (sub === "sessions") {
|
|
18
|
+
const { values, positionals: positional } = parseCommand("sessions", { json: { type: "boolean", help: "Machine-readable JSON" } }, args.slice(2));
|
|
19
|
+
const json = values.json === true;
|
|
20
|
+
const proj = resolve(positional[0] ?? process.cwd());
|
|
21
|
+
const sessions = listBrowserSessions(proj);
|
|
22
|
+
if (json) {
|
|
23
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
printBrowserSessionsList(sessions, proj);
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (sub === "stop") {
|
|
31
|
+
const proj = resolve(args[3] ?? process.cwd());
|
|
32
|
+
const target = args[2];
|
|
33
|
+
if (!target) {
|
|
34
|
+
console.error("waygraph browser stop: usage: waygraph browser stop <sessionId|--all> [project]");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
if (target === "--all") {
|
|
38
|
+
const n = stopAllBrowserSessions(proj);
|
|
39
|
+
console.log(JSON.stringify({ stopped: n, projectDir: proj }));
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (!stopBrowserSession(proj, target)) {
|
|
43
|
+
console.error(`waygraph browser stop: no such session "${target}"`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
console.log(JSON.stringify({ stopped: target }));
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (!sub) {
|
|
50
|
+
printBrowserUsage();
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (sub === "start" || sub.startsWith("-")) {
|
|
54
|
+
const flagArgs = sub === "start" ? args.slice(2) : args.slice(1);
|
|
55
|
+
const flags = parseRunFlags(flagArgs, "browser");
|
|
56
|
+
applyRunFlags(flags);
|
|
57
|
+
const proj = resolve(flags.positionals[0] ?? process.cwd());
|
|
58
|
+
if (!existsSync(proj)) {
|
|
59
|
+
console.error(`waygraph browser: no such directory: ${proj}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const baseURL = flags.baseUrl ?? envString("WAYGRAPH_BASE_URL") ?? (await resolveBaseUrl(proj));
|
|
63
|
+
try {
|
|
64
|
+
const inject = flags.inject?.length ? resolveInjectRoots(flags.inject, proj) : undefined;
|
|
65
|
+
const result = await browserStart({
|
|
66
|
+
projectDir: proj,
|
|
67
|
+
...(inject?.length ? { inject } : {}),
|
|
68
|
+
...(baseURL ? { baseURL } : {}),
|
|
69
|
+
...(flags.goto
|
|
70
|
+
? { startUrl: flags.goto, skipInitialNavigation: false }
|
|
71
|
+
: { skipInitialNavigation: flags.blank !== false }),
|
|
72
|
+
headless: flags.headlessBrowser === true,
|
|
73
|
+
});
|
|
74
|
+
console.log(JSON.stringify(result));
|
|
75
|
+
if (!result.headless) {
|
|
76
|
+
console.error(`Session ${result.sessionId} started. List all: waygraph browser sessions`);
|
|
77
|
+
}
|
|
78
|
+
if (flags.cli) {
|
|
79
|
+
await runAttachLoop(proj, result.sessionId);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
console.error(`waygraph browser start: ${err instanceof Error ? err.message : String(err)}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
if (sub !== "send" &&
|
|
89
|
+
sub !== "status" &&
|
|
90
|
+
sub !== "attach" &&
|
|
91
|
+
sub !== "dom" &&
|
|
92
|
+
sub !== "trace" &&
|
|
93
|
+
sub !== "console" &&
|
|
94
|
+
sub !== "storage" &&
|
|
95
|
+
sub !== "upload" &&
|
|
96
|
+
sub !== "click" &&
|
|
97
|
+
sub !== "type" &&
|
|
98
|
+
sub !== "press" &&
|
|
99
|
+
sub !== "goto" &&
|
|
100
|
+
sub !== "reload" &&
|
|
101
|
+
sub !== "reach" &&
|
|
102
|
+
sub !== "resync" &&
|
|
103
|
+
sub !== "highlight") {
|
|
104
|
+
console.error("waygraph browser: unknown subcommand — run `waygraph browser` for usage");
|
|
105
|
+
printBrowserUsage();
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// aliases (`banner`, `bannerPos`, `clearDevice`, `clearTodos`, `landscape`, `portrait`, `todoDockFull`) become the
|
|
5
5
|
// names they stood for. A stub written as a slot map (`{ email: { selector, label } }`) becomes a function of
|
|
6
6
|
// `ctx.ring` calls, and a stub set in a `define*Block({ ... })` (options or instruction) moves out to a chained
|
|
7
|
-
// `.stubBefore(...)` on the Block
|
|
7
|
+
// `.stubBefore(...)` on the Block; a Block's `slides` become `ctx.slides(...)` in its chained `.stubAfter(...)`;
|
|
8
|
+
// `ctx.highlights({ a: ring })` becomes one `ctx.ring("a", ring)` per ring; `narrate(locator, caption, action)` in an
|
|
9
|
+
// `act` becomes the action, reported so the caption can move to a stubBefore ring. Works on the TypeScript AST (ts-morph).
|
|
8
10
|
import { Node, SyntaxKind } from "ts-morph";
|
|
9
11
|
const RENAMED = {
|
|
10
12
|
banner: (a) => `title(${a.join(", ")})`,
|
|
@@ -127,7 +129,7 @@ function convertSlotMaps(sf) {
|
|
|
127
129
|
}
|
|
128
130
|
return changed;
|
|
129
131
|
}
|
|
130
|
-
/** `defineXBlock({ ..., stubBefore: fn })` (or in its instruction) -> `defineXBlock({ ... }).stubBefore(fn)`. */
|
|
132
|
+
/** `defineXBlock({ ..., stubBefore: fn, slides })` (or in its instruction) -> `defineXBlock({ ... }).stubBefore(fn).stubAfter(slides)`. */
|
|
131
133
|
function hoistDefinitionStubs(sf) {
|
|
132
134
|
let changed = false;
|
|
133
135
|
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression).reverse()) {
|
|
@@ -141,9 +143,17 @@ function hoistDefinitionStubs(sf) {
|
|
|
141
143
|
if (Node.isPropertyAssignment(instruction) && Node.isObjectLiteralExpression(instruction.getInitializer()))
|
|
142
144
|
holders.push(instruction.getInitializer());
|
|
143
145
|
const chained = [];
|
|
146
|
+
let slides;
|
|
144
147
|
for (const holder of holders) {
|
|
145
148
|
for (const prop of holder.getProperties()) {
|
|
146
149
|
const name = Node.isPropertyAssignment(prop) || Node.isMethodDeclaration(prop) ? prop.getName() : "";
|
|
150
|
+
if (name === "slides" && Node.isPropertyAssignment(prop)) {
|
|
151
|
+
const init = prop.getInitializerOrThrow();
|
|
152
|
+
// `slides: (out) => [...]` read the Checkpoint the step reached; in stubAfter that is ctx.out.
|
|
153
|
+
slides = Node.isArrowFunction(init) || Node.isFunctionExpression(init) ? `(${init.getText()})(ctx.out!)` : init.getText();
|
|
154
|
+
prop.remove();
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
147
157
|
if (!PHASES.has(name))
|
|
148
158
|
continue;
|
|
149
159
|
let fn;
|
|
@@ -157,6 +167,15 @@ function hoistDefinitionStubs(sf) {
|
|
|
157
167
|
prop.remove();
|
|
158
168
|
}
|
|
159
169
|
}
|
|
170
|
+
if (slides !== undefined) {
|
|
171
|
+
const after = chained.findIndex((c) => c.startsWith(".stubAfter("));
|
|
172
|
+
if (after < 0)
|
|
173
|
+
chained.push(`.stubAfter((ctx) => ctx.slides(${slides}))`);
|
|
174
|
+
else {
|
|
175
|
+
const fn = chained[after].slice(".stubAfter(".length, -1);
|
|
176
|
+
chained[after] = `.stubAfter(async (ctx) => {\n await (${fn})(ctx);\n ctx.slides(${slides});\n})`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
160
179
|
if (chained.length) {
|
|
161
180
|
call.replaceWithText(call.getText() + chained.join(""));
|
|
162
181
|
changed = true;
|
|
@@ -164,6 +183,71 @@ function hoistDefinitionStubs(sf) {
|
|
|
164
183
|
}
|
|
165
184
|
return changed;
|
|
166
185
|
}
|
|
186
|
+
/** `ctx.highlights({ a: ring, b: ring })` -> `ctx.ring("a", ring); ctx.ring("b", ring);`, one call at a time. */
|
|
187
|
+
function convertHighlights(sf, problems) {
|
|
188
|
+
let changed = false;
|
|
189
|
+
const skipped = new Set();
|
|
190
|
+
for (;;) {
|
|
191
|
+
// Raw text replacement forgets the file's nodes, so look the next call up again each time.
|
|
192
|
+
const statement = sf.getDescendantsOfKind(SyntaxKind.ExpressionStatement).find((st) => {
|
|
193
|
+
const c = ctxCall(st);
|
|
194
|
+
return c && c.method === "highlights" && !skipped.has(st.getStart()) && isStubCtx(c.call, c.receiver);
|
|
195
|
+
});
|
|
196
|
+
if (!statement)
|
|
197
|
+
return changed;
|
|
198
|
+
const c = ctxCall(statement);
|
|
199
|
+
const map = c.call.getArguments()[0];
|
|
200
|
+
if (!Node.isObjectLiteralExpression(map)) {
|
|
201
|
+
problems.push({ line: c.call.getStartLineNumber(), reason: "ctx.highlights(...) was removed and its argument is not an object literal: call ctx.ring(id, ring) for each ring" });
|
|
202
|
+
skipped.add(statement.getStart());
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
// Each ring keeps its own lines, moved to the statement's indent (replaceWithText would re-indent them).
|
|
206
|
+
const column = (n) => n.getStart() - n.getStartLinePos();
|
|
207
|
+
const indent = " ".repeat(column(statement));
|
|
208
|
+
const rings = map.getProperties().flatMap((p) => {
|
|
209
|
+
if (!Node.isPropertyAssignment(p))
|
|
210
|
+
return [];
|
|
211
|
+
const from = column(p);
|
|
212
|
+
const value = p.getInitializerOrThrow().getText().split("\n").map((l, n) => (n === 0 ? l : indent + l.slice(Math.min(from, l.search(/\S|$/))))).join("\n");
|
|
213
|
+
return [`${c.receiver}.ring(${JSON.stringify(p.getName().replace(/^["']|["']$/g, ""))}, ${value});`];
|
|
214
|
+
});
|
|
215
|
+
sf.replaceText([statement.getStart(), statement.getEnd()], rings.join("\n" + indent));
|
|
216
|
+
changed = true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* `narrate(locator, caption, () => action)` in an `act` -> just the action. The caption was narration inside the
|
|
221
|
+
* Block; it belongs in a `stubBefore` ring where the flow uses the Block, which the codemod can't place, so it is
|
|
222
|
+
* reported.
|
|
223
|
+
*/
|
|
224
|
+
function unwrapNarrate(sf, problems) {
|
|
225
|
+
let changed = false;
|
|
226
|
+
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression).reverse()) {
|
|
227
|
+
if (call.wasForgotten() || call.getExpression().getText() !== "narrate" || call.getArguments().length !== 3)
|
|
228
|
+
continue;
|
|
229
|
+
const [, captionNode, action] = call.getArguments();
|
|
230
|
+
const caption = captionNode.getText();
|
|
231
|
+
const line = call.getStartLineNumber();
|
|
232
|
+
let body = `(${action.getText()})()`;
|
|
233
|
+
if (Node.isArrowFunction(action) && !Node.isBlock(action.getBody()))
|
|
234
|
+
body = action.getBody().getText();
|
|
235
|
+
call.replaceWithText(body);
|
|
236
|
+
problems.push({ line, reason: `narrate(...) was removed; its caption ${caption} is gone: ring the element in a stubBefore where the flow uses this Block` });
|
|
237
|
+
changed = true;
|
|
238
|
+
}
|
|
239
|
+
if (changed) {
|
|
240
|
+
for (const decl of sf.getImportDeclarations()) {
|
|
241
|
+
const spec = decl.getNamedImports().find((n) => n.getName() === "narrate");
|
|
242
|
+
if (!spec)
|
|
243
|
+
continue;
|
|
244
|
+
spec.remove();
|
|
245
|
+
if (decl.getNamedImports().length === 0 && !decl.getDefaultImport() && !decl.getNamespaceImport())
|
|
246
|
+
decl.remove();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return changed;
|
|
250
|
+
}
|
|
167
251
|
/** Rewrites the old stub forms in every file; returns the files it changed and the calls left for a person. */
|
|
168
252
|
export function convertStubs(project) {
|
|
169
253
|
const changed = [];
|
|
@@ -171,6 +255,10 @@ export function convertStubs(project) {
|
|
|
171
255
|
for (const sf of project.getSourceFiles()) {
|
|
172
256
|
const left = [];
|
|
173
257
|
let touched = hoistDefinitionStubs(sf);
|
|
258
|
+
if (unwrapNarrate(sf, left))
|
|
259
|
+
touched = true;
|
|
260
|
+
if (convertHighlights(sf, left))
|
|
261
|
+
touched = true;
|
|
174
262
|
if (convertSlotMaps(sf))
|
|
175
263
|
touched = true;
|
|
176
264
|
for (const block of sf.getDescendantsOfKind(SyntaxKind.Block).reverse()) {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// `waygraph pilot start | sessions | attach`: a session plus the whole graph and a starting snapshot in one call.
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { envString } from "../../engine/env.js";
|
|
4
|
+
import { listBrowserSessions } from "../../live/auto-session/files.js";
|
|
5
|
+
import { runAttachLoop } from "../../live/auto-session/ipc.js";
|
|
6
|
+
import { pilotAttach, pilotStart } from "../../live/pilot/pilot.js";
|
|
7
|
+
import { resolveInjectRoots } from "../../manifest/roots.js";
|
|
8
|
+
import { resolveBaseUrl } from "../../runtime/waygraph-config.js";
|
|
9
|
+
import { parseCommand } from "../args.js";
|
|
10
|
+
import { applyRunFlags, parseRunFlags } from "../flags.js";
|
|
11
|
+
import { printBrowserSessionsList } from "../usage.js";
|
|
12
|
+
/** Runs a `pilot` subcommand; false when it is a session op for session-ops.ts. */
|
|
13
|
+
export async function pilotCase(args) {
|
|
14
|
+
if (args[1] === "sessions") {
|
|
15
|
+
const { values, positionals: positional } = parseCommand("sessions", { json: { type: "boolean", help: "Machine-readable JSON" } }, args.slice(2));
|
|
16
|
+
const json = values.json === true;
|
|
17
|
+
const proj = resolve(positional[0] ?? process.cwd());
|
|
18
|
+
const sessions = listBrowserSessions(proj);
|
|
19
|
+
if (json) {
|
|
20
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
printBrowserSessionsList(sessions, proj);
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (args[1] === "attach") {
|
|
28
|
+
const sessionId = args[2];
|
|
29
|
+
if (!sessionId) {
|
|
30
|
+
console.error("waygraph pilot attach: usage: waygraph pilot attach <sessionId> [--inject …] [project]");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const flags = parseRunFlags(args.slice(3), "pilot");
|
|
34
|
+
const proj = resolve(flags.positionals[0] ?? process.cwd());
|
|
35
|
+
try {
|
|
36
|
+
const inject = flags.inject?.length ? resolveInjectRoots(flags.inject, proj) : undefined;
|
|
37
|
+
const result = await pilotAttach(proj, sessionId, inject);
|
|
38
|
+
console.log(JSON.stringify(result));
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.error(`waygraph pilot attach: ${err instanceof Error ? err.message : String(err)}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (args[1] === "start" || !args[1] || args[1].startsWith("-")) {
|
|
47
|
+
const flagArgs = args[1] === "start" ? args.slice(2) : args.slice(1);
|
|
48
|
+
const flags = parseRunFlags(flagArgs, "pilot");
|
|
49
|
+
applyRunFlags(flags);
|
|
50
|
+
const proj = resolve(flags.positionals[0] ?? process.cwd());
|
|
51
|
+
const baseURL = flags.baseUrl ?? envString("WAYGRAPH_BASE_URL") ?? (await resolveBaseUrl(proj));
|
|
52
|
+
try {
|
|
53
|
+
const result = await pilotStart({
|
|
54
|
+
projectDir: proj,
|
|
55
|
+
...(flags.inject?.length ? { injectTokens: flags.inject } : {}),
|
|
56
|
+
...(baseURL ? { baseURL } : {}),
|
|
57
|
+
...(flags.goto
|
|
58
|
+
? { startUrl: flags.goto, skipInitialNavigation: false }
|
|
59
|
+
: flags.blank === true
|
|
60
|
+
? { skipInitialNavigation: true }
|
|
61
|
+
: {}),
|
|
62
|
+
headless: flags.headlessBrowser === true,
|
|
63
|
+
});
|
|
64
|
+
console.log(JSON.stringify(result));
|
|
65
|
+
if (!result.headless) {
|
|
66
|
+
console.error(`Chromium window opened (session ${result.sessionId}). ` +
|
|
67
|
+
`Terminal menu: waygraph pilot attach ${result.sessionId}`);
|
|
68
|
+
}
|
|
69
|
+
if (flags.cli) {
|
|
70
|
+
await runAttachLoop(proj, result.sessionId);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
console.error(`waygraph pilot start: ${err instanceof Error ? err.message : String(err)}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (args[1] !== "send" &&
|
|
80
|
+
args[1] !== "status" &&
|
|
81
|
+
args[1] !== "dom" &&
|
|
82
|
+
args[1] !== "trace" &&
|
|
83
|
+
args[1] !== "console" &&
|
|
84
|
+
args[1] !== "storage" &&
|
|
85
|
+
args[1] !== "upload" &&
|
|
86
|
+
args[1] !== "click" &&
|
|
87
|
+
args[1] !== "type" &&
|
|
88
|
+
args[1] !== "press" &&
|
|
89
|
+
args[1] !== "goto" &&
|
|
90
|
+
args[1] !== "reload" &&
|
|
91
|
+
args[1] !== "reach" &&
|
|
92
|
+
args[1] !== "resync" &&
|
|
93
|
+
args[1] !== "highlight") {
|
|
94
|
+
console.error("waygraph pilot: usage:\n" +
|
|
95
|
+
" waygraph pilot start [--inject …] [--goto <url>] [--headless] [--cli] [--base-url <url>] [project]\n" +
|
|
96
|
+
" waygraph pilot sessions [project] # list live browser sessions\n" +
|
|
97
|
+
" waygraph pilot attach <sessionId> # graph + snapshot for an existing session\n" +
|
|
98
|
+
" waygraph pilot send|status|attach|highlight|… <sessionId> … (controls browser — same as browser/auto)");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Whether `sub` is a session op (`attach` is pilot's own subcommand, not an op, under `pilot`). */
|
|
2
|
+
export declare function isSessionOp(command: string, sub: string | undefined): boolean;
|
|
3
|
+
export declare function sessionOpCase(args: string[], command: string): Promise<void>;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// Session ops against a running session by id: `waygraph auto|browser|pilot <op> <sessionId> ...` (send, status,
|
|
2
|
+
// attach, dom, trace, console, storage, click, type, press, goto, upload, reload, reach, resync, highlight).
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { parseHighlightShorthand } from "../../demo/narration/shorthand.js";
|
|
5
|
+
import { resolveSessionMeta } from "../../live/auto-session/files.js";
|
|
6
|
+
import { requestSession, runAttachLoop } from "../../live/auto-session/ipc.js";
|
|
7
|
+
import { parseCommand } from "../args.js";
|
|
8
|
+
const SEND_OPTIONS = {
|
|
9
|
+
timeout: { type: "string", value: "<ms>", help: "How long one Block may run (default 15000)" },
|
|
10
|
+
};
|
|
11
|
+
const DOM_OPTIONS = {
|
|
12
|
+
mode: { type: "string", value: "aria|full", help: "Accessibility tree (default) or the full DOM" },
|
|
13
|
+
selector: { type: "string", value: "<css>", help: "Only this element's subtree" },
|
|
14
|
+
depth: { type: "string", value: "N", help: "Levels to read" },
|
|
15
|
+
};
|
|
16
|
+
const OPS = new Set(["send", "status", "attach", "dom", "trace", "console", "storage", "upload", "click", "type", "press", "goto", "reload", "reach", "resync", "highlight"]);
|
|
17
|
+
/** Whether `sub` is a session op (`attach` is pilot's own subcommand, not an op, under `pilot`). */
|
|
18
|
+
export function isSessionOp(command, sub) {
|
|
19
|
+
return sub !== undefined && OPS.has(sub) && !(sub === "attach" && command === "pilot");
|
|
20
|
+
}
|
|
21
|
+
export async function sessionOpCase(args, command) {
|
|
22
|
+
const sub = args[1];
|
|
23
|
+
const sessionId = args[2];
|
|
24
|
+
if (!sessionId) {
|
|
25
|
+
console.error(`waygraph ${command} ${sub}: missing <sessionId>`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const projHint = resolve(process.cwd());
|
|
29
|
+
const meta = resolveSessionMeta(sessionId, projHint);
|
|
30
|
+
if (!meta) {
|
|
31
|
+
console.error(`waygraph ${command} ${sub}: no such session "${sessionId}" — ` +
|
|
32
|
+
"wrong directory? run `waygraph pilot sessions` from the project that started it");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const proj = meta.projectDir;
|
|
36
|
+
if (sub === "attach") {
|
|
37
|
+
await runAttachLoop(proj, sessionId);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (sub === "send") {
|
|
41
|
+
const pick = args[3];
|
|
42
|
+
if (pick === undefined) {
|
|
43
|
+
console.error('waygraph auto send: usage: waygraph auto send <sessionId> "<pick>" [--timeout <ms>]');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
// Real, direct need found live: a single Block can legitimately
|
|
47
|
+
// run a genuinely slow real interaction (e.g. a multi-step mouse
|
|
48
|
+
// drag) that exceeds the 15s default - same reasoning `auto
|
|
49
|
+
// reach` already gets a 90s default for (multiple Blocks in one
|
|
50
|
+
// call). Unlike reach, send has no way to know ahead of time
|
|
51
|
+
// whether the ONE Block it's running is fast or slow, so this is
|
|
52
|
+
// opt-in via a flag rather than a raised default.
|
|
53
|
+
let timeoutMs;
|
|
54
|
+
const raw = parseCommand("auto send", SEND_OPTIONS, args.slice(4)).values.timeout;
|
|
55
|
+
if (raw !== undefined) {
|
|
56
|
+
const n = Number(raw);
|
|
57
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
58
|
+
console.error(`waygraph auto send: --timeout must be a positive number of ms, got "${raw}"`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
timeoutMs = n;
|
|
62
|
+
}
|
|
63
|
+
const res = await requestSession(proj, sessionId, { op: "send", pick }, timeoutMs);
|
|
64
|
+
console.log(JSON.stringify(res));
|
|
65
|
+
if (!res.ok)
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (sub === "dom") {
|
|
70
|
+
const domArgs = args.slice(3);
|
|
71
|
+
let mode;
|
|
72
|
+
let selector;
|
|
73
|
+
let depth;
|
|
74
|
+
const dom = parseCommand("auto dom", DOM_OPTIONS, domArgs).values;
|
|
75
|
+
if (dom.mode !== undefined) {
|
|
76
|
+
if (dom.mode !== "aria" && dom.mode !== "full") {
|
|
77
|
+
console.error(`waygraph auto dom: --mode must be "aria" or "full", got "${dom.mode}"`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
mode = dom.mode;
|
|
81
|
+
}
|
|
82
|
+
selector = dom.selector;
|
|
83
|
+
if (dom.depth !== undefined) {
|
|
84
|
+
const n = Number(dom.depth);
|
|
85
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
86
|
+
console.error(`waygraph auto dom: --depth must be a positive integer, got "${dom.depth}"`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
depth = n;
|
|
90
|
+
}
|
|
91
|
+
const res = await requestSession(proj, sessionId, {
|
|
92
|
+
op: "dom",
|
|
93
|
+
...(mode ? { mode } : {}),
|
|
94
|
+
...(selector ? { selector } : {}),
|
|
95
|
+
...(depth !== undefined ? { depth } : {}),
|
|
96
|
+
});
|
|
97
|
+
console.log(JSON.stringify(res));
|
|
98
|
+
if (!res.ok)
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (sub === "trace") {
|
|
103
|
+
const res = await requestSession(proj, sessionId, { op: "trace" });
|
|
104
|
+
console.log(JSON.stringify(res));
|
|
105
|
+
if (!res.ok)
|
|
106
|
+
process.exitCode = 1;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (sub === "console") {
|
|
110
|
+
const res = await requestSession(proj, sessionId, { op: "console" });
|
|
111
|
+
console.log(JSON.stringify(res));
|
|
112
|
+
if (!res.ok)
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (sub === "storage") {
|
|
117
|
+
const res = await requestSession(proj, sessionId, { op: "storage" });
|
|
118
|
+
console.log(JSON.stringify(res));
|
|
119
|
+
if (!res.ok)
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (sub === "click") {
|
|
124
|
+
const selector = args[3];
|
|
125
|
+
if (selector === undefined) {
|
|
126
|
+
console.error('waygraph auto click: usage: waygraph auto click <sessionId> "<selector>"');
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
const res = await requestSession(proj, sessionId, { op: "click", selector });
|
|
130
|
+
console.log(JSON.stringify(res));
|
|
131
|
+
if (!res.ok)
|
|
132
|
+
process.exitCode = 1;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (sub === "type") {
|
|
136
|
+
const selector = args[3];
|
|
137
|
+
const text = args[4];
|
|
138
|
+
if (selector === undefined || text === undefined) {
|
|
139
|
+
console.error('waygraph auto type: usage: waygraph auto type <sessionId> "<selector>" "<text>"');
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
const res = await requestSession(proj, sessionId, { op: "type", selector, text });
|
|
143
|
+
console.log(JSON.stringify(res));
|
|
144
|
+
if (!res.ok)
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (sub === "press") {
|
|
149
|
+
const selector = args[3];
|
|
150
|
+
const key = args[4];
|
|
151
|
+
if (selector === undefined || key === undefined) {
|
|
152
|
+
console.error('waygraph auto press: usage: waygraph auto press <sessionId> "<selector>" <key>');
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
const res = await requestSession(proj, sessionId, { op: "press", selector, key });
|
|
156
|
+
console.log(JSON.stringify(res));
|
|
157
|
+
if (!res.ok)
|
|
158
|
+
process.exitCode = 1;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (sub === "goto") {
|
|
162
|
+
const url = args[3];
|
|
163
|
+
if (url === undefined) {
|
|
164
|
+
console.error("waygraph auto goto: usage: waygraph auto goto <sessionId> <url>");
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
const res = await requestSession(proj, sessionId, { op: "goto", url });
|
|
168
|
+
console.log(JSON.stringify(res));
|
|
169
|
+
if (!res.ok)
|
|
170
|
+
process.exitCode = 1;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (sub === "upload") {
|
|
174
|
+
const selector = args[3];
|
|
175
|
+
const kindOrPath = args[4];
|
|
176
|
+
if (selector === undefined || kindOrPath === undefined) {
|
|
177
|
+
console.error('waygraph auto upload: usage: waygraph auto upload <sessionId> "<selector>" <image|pdf|video|path-to-file>');
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
const stub = kindOrPath === "image" || kindOrPath === "pdf" || kindOrPath === "video"
|
|
181
|
+
? { selector, stub: kindOrPath }
|
|
182
|
+
: { selector, stub: { filePath: resolve(process.cwd(), kindOrPath) } };
|
|
183
|
+
const res = await requestSession(proj, sessionId, { op: "upload", ...stub });
|
|
184
|
+
console.log(JSON.stringify(res));
|
|
185
|
+
if (!res.ok)
|
|
186
|
+
process.exitCode = 1;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (sub === "reload") {
|
|
190
|
+
const res = await requestSession(proj, sessionId, { op: "reload" });
|
|
191
|
+
console.log(JSON.stringify(res));
|
|
192
|
+
if (!res.ok)
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (sub === "reach") {
|
|
197
|
+
const checkpoint = args[3];
|
|
198
|
+
if (checkpoint === undefined) {
|
|
199
|
+
console.error("waygraph auto reach: usage: waygraph auto reach <sessionId> <Checkpoint>");
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
// Longer than every other op's default (15s): a real multi-step
|
|
203
|
+
// route runs several real Blocks in sequence server-side before
|
|
204
|
+
// responding - a legitimately slow single request, not a hang.
|
|
205
|
+
const res = await requestSession(proj, sessionId, { op: "reach", checkpoint }, 90_000);
|
|
206
|
+
console.log(JSON.stringify(res));
|
|
207
|
+
if (!res.ok)
|
|
208
|
+
process.exitCode = 1;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (sub === "resync") {
|
|
212
|
+
const res = await requestSession(proj, sessionId, { op: "resync" });
|
|
213
|
+
console.log(JSON.stringify(res));
|
|
214
|
+
if (!res.ok)
|
|
215
|
+
process.exitCode = 1;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (sub === "highlight") {
|
|
219
|
+
const raw = args[3];
|
|
220
|
+
if (raw === undefined) {
|
|
221
|
+
console.error(`waygraph ${command} highlight: usage:\n` +
|
|
222
|
+
` waygraph ${command} highlight <sessionId> '{"rings":[{"selector":"#x","label":"X"}]}'\n` +
|
|
223
|
+
` waygraph ${command} highlight <sessionId> "#x|X" (shorthand: <selector>|<label>[|<tone>], rings separated by ";")`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
let fixtures;
|
|
227
|
+
try {
|
|
228
|
+
fixtures = JSON.parse(raw);
|
|
229
|
+
}
|
|
230
|
+
catch (jsonErr) {
|
|
231
|
+
const shorthand = parseHighlightShorthand(raw);
|
|
232
|
+
if (shorthand.type === "error") {
|
|
233
|
+
console.error(`waygraph ${command} highlight: body is neither valid JSON (${jsonErr instanceof Error ? jsonErr.message : String(jsonErr)}) nor valid shorthand (${shorthand.reason})`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
fixtures = shorthand.fixtures;
|
|
237
|
+
}
|
|
238
|
+
if (fixtures.op !== undefined && fixtures.op !== "highlight") {
|
|
239
|
+
console.error(`waygraph ${command} highlight: do not set "op" (or set it to "highlight")`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
const { op: _ignore, ...rest } = fixtures;
|
|
243
|
+
const res = await requestSession(proj, sessionId, {
|
|
244
|
+
op: "highlight",
|
|
245
|
+
...rest,
|
|
246
|
+
});
|
|
247
|
+
console.log(JSON.stringify(res));
|
|
248
|
+
if (!res.ok)
|
|
249
|
+
process.exitCode = 1;
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const res = await requestSession(proj, sessionId, { op: "status" });
|
|
253
|
+
console.log(JSON.stringify(res));
|
|
254
|
+
if (!res.ok)
|
|
255
|
+
process.exitCode = 1;
|
|
256
|
+
return;
|
|
257
|
+
}
|