waygraph 0.16.0 → 0.16.1
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/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/usage.d.ts +1 -1
- package/dist/demo/narration/demo-pace.d.ts +1 -1
- package/dist/demo/narration/device.d.ts +2 -2
- package/dist/demo/narration/merge.d.ts +4 -2
- package/dist/demo/narration/merge.js +2 -6
- package/dist/demo/narration/narration.d.ts +8 -8
- 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 -1
- 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-stage.d.ts +19 -11
- package/dist/demo/overlay/overlay.d.ts +2 -2
- package/dist/demo/overlay/rings.d.ts +38 -10
- package/dist/demo/overlay/rings.js +21 -47
- package/dist/demo/stepper/after.js +9 -19
- 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 +3 -3
- package/dist/demo/stepper/demo-log.d.ts +26 -9
- package/dist/demo/stepper/demo-log.js +9 -26
- package/dist/demo/stepper/interaction-patch.d.ts +3 -2
- package/dist/demo/stepper/interaction-patch.js +19 -19
- 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 +13 -1
- package/dist/demo/stepper/slides.js +3 -3
- 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 -5
- 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/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 -79
- 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/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/trait/trait.js +1 -1
- package/dist/index.d.ts +13 -9
- package/dist/index.js +7 -5
- 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/package.json +5 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface SessionMeta {
|
|
2
|
+
sessionId: string;
|
|
3
|
+
socketPath: string;
|
|
4
|
+
pid: number;
|
|
5
|
+
projectDir: string;
|
|
6
|
+
startedAt: string;
|
|
7
|
+
headless: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function sessionDir(projectDir: string): string;
|
|
10
|
+
export declare function metaPath(projectDir: string, sessionId: string): string;
|
|
11
|
+
/** Global index so session control works even when cwd ≠ the project that spawned the session. */
|
|
12
|
+
export declare function globalMetaPath(sessionId: string): string;
|
|
13
|
+
export declare function isPidAlive(pid: number): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Deliberately NOT under the project directory (unlike meta/log files) -
|
|
16
|
+
* a real bug found while proving out openspec/changes/waygraph-map's own
|
|
17
|
+
* "waypack" loading claim: a project loaded through a consumer's own deeply
|
|
18
|
+
* nested `node_modules/<pkg>` path can push the socket's absolute path past
|
|
19
|
+
* the OS's AF_UNIX `sun_path` limit (~108 bytes on Linux), failing with
|
|
20
|
+
* `listen EINVAL`. A short, fixed-location directory sidesteps this
|
|
21
|
+
* regardless of how deep `projectDir` itself is nested. `sessionId` alone
|
|
22
|
+
* (8 hex chars, `generateSessionId()`) is enough to keep this collision-safe
|
|
23
|
+
* across every project on the machine sharing this one flat directory - the
|
|
24
|
+
* same randomness space this package already relied on per-project.
|
|
25
|
+
*/
|
|
26
|
+
export declare function socketDir(): string;
|
|
27
|
+
export declare function socketPathFor(sessionId: string): string;
|
|
28
|
+
export declare function logPathFor(projectDir: string, sessionId: string): string;
|
|
29
|
+
export declare function generateSessionId(): string;
|
|
30
|
+
export declare function readSessionMeta(projectDir: string, sessionId: string): SessionMeta | null;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve session metadata from a project hint (cwd) or the global registry.
|
|
33
|
+
* Fixes "session unreachable" when the agent calls status/send from a different
|
|
34
|
+
* directory or pane than the one that ran `pilot start`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveSessionMeta(sessionId: string, projectDirHint?: string): SessionMeta | null;
|
|
37
|
+
export declare function writeGlobalSessionMeta(meta: SessionMeta): void;
|
|
38
|
+
export declare function removeSessionFiles(projectDir: string, sessionId: string): void;
|
|
39
|
+
/** Stop one detached session (SIGTERM + cleanup metadata/socket). */
|
|
40
|
+
export declare function stopBrowserSession(projectDir: string, sessionId: string): boolean;
|
|
41
|
+
/** Stop every live session registered for a project. */
|
|
42
|
+
export declare function stopAllBrowserSessions(projectDir: string): number;
|
|
43
|
+
/** Live browser sessions for a project (metadata + socket + pid still present). */
|
|
44
|
+
export declare function listBrowserSessions(projectDir: string): SessionMeta[];
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Where a detached session lives on disk: its metadata (per project, plus a global copy keyed by id), its short
|
|
2
|
+
// socket path under the OS temp folder, its log; and listing, resolving and stopping sessions by id.
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
export function sessionDir(projectDir) {
|
|
8
|
+
return join(projectDir, ".waygraph-auto");
|
|
9
|
+
}
|
|
10
|
+
export function metaPath(projectDir, sessionId) {
|
|
11
|
+
return join(sessionDir(projectDir), `${sessionId}.json`);
|
|
12
|
+
}
|
|
13
|
+
/** Global index so session control works even when cwd ≠ the project that spawned the session. */
|
|
14
|
+
export function globalMetaPath(sessionId) {
|
|
15
|
+
return join(socketDir(), `${sessionId}.meta.json`);
|
|
16
|
+
}
|
|
17
|
+
export function isPidAlive(pid) {
|
|
18
|
+
try {
|
|
19
|
+
process.kill(pid, 0);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Deliberately NOT under the project directory (unlike meta/log files) -
|
|
28
|
+
* a real bug found while proving out openspec/changes/waygraph-map's own
|
|
29
|
+
* "waypack" loading claim: a project loaded through a consumer's own deeply
|
|
30
|
+
* nested `node_modules/<pkg>` path can push the socket's absolute path past
|
|
31
|
+
* the OS's AF_UNIX `sun_path` limit (~108 bytes on Linux), failing with
|
|
32
|
+
* `listen EINVAL`. A short, fixed-location directory sidesteps this
|
|
33
|
+
* regardless of how deep `projectDir` itself is nested. `sessionId` alone
|
|
34
|
+
* (8 hex chars, `generateSessionId()`) is enough to keep this collision-safe
|
|
35
|
+
* across every project on the machine sharing this one flat directory - the
|
|
36
|
+
* same randomness space this package already relied on per-project.
|
|
37
|
+
*/
|
|
38
|
+
export function socketDir() {
|
|
39
|
+
return join(tmpdir(), "waygraph-auto");
|
|
40
|
+
}
|
|
41
|
+
export function socketPathFor(sessionId) {
|
|
42
|
+
return join(socketDir(), `${sessionId}.sock`);
|
|
43
|
+
}
|
|
44
|
+
export function logPathFor(projectDir, sessionId) {
|
|
45
|
+
return join(sessionDir(projectDir), `${sessionId}.log`);
|
|
46
|
+
}
|
|
47
|
+
export function generateSessionId() {
|
|
48
|
+
return randomBytes(4).toString("hex");
|
|
49
|
+
}
|
|
50
|
+
export function readSessionMeta(projectDir, sessionId) {
|
|
51
|
+
const p = metaPath(projectDir, sessionId);
|
|
52
|
+
if (!existsSync(p))
|
|
53
|
+
return null;
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(readFileSync(p, "utf-8"));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolve session metadata from a project hint (cwd) or the global registry.
|
|
63
|
+
* Fixes "session unreachable" when the agent calls status/send from a different
|
|
64
|
+
* directory or pane than the one that ran `pilot start`.
|
|
65
|
+
*/
|
|
66
|
+
export function resolveSessionMeta(sessionId, projectDirHint) {
|
|
67
|
+
if (projectDirHint) {
|
|
68
|
+
const local = readSessionMeta(projectDirHint, sessionId);
|
|
69
|
+
if (local)
|
|
70
|
+
return local;
|
|
71
|
+
}
|
|
72
|
+
const globalPath = globalMetaPath(sessionId);
|
|
73
|
+
if (existsSync(globalPath)) {
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(readFileSync(globalPath, "utf-8"));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
export function writeGlobalSessionMeta(meta) {
|
|
84
|
+
mkdirSync(socketDir(), { recursive: true });
|
|
85
|
+
writeFileSync(globalMetaPath(meta.sessionId), JSON.stringify(meta, null, 2));
|
|
86
|
+
}
|
|
87
|
+
export function removeSessionFiles(projectDir, sessionId) {
|
|
88
|
+
for (const p of [
|
|
89
|
+
metaPath(projectDir, sessionId),
|
|
90
|
+
globalMetaPath(sessionId),
|
|
91
|
+
socketPathFor(sessionId),
|
|
92
|
+
]) {
|
|
93
|
+
try {
|
|
94
|
+
rmSync(p, { force: true });
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
/* ignore */
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Stop one detached session (SIGTERM + cleanup metadata/socket). */
|
|
102
|
+
export function stopBrowserSession(projectDir, sessionId) {
|
|
103
|
+
const meta = resolveSessionMeta(sessionId, projectDir);
|
|
104
|
+
if (!meta)
|
|
105
|
+
return false;
|
|
106
|
+
try {
|
|
107
|
+
process.kill(meta.pid, "SIGTERM");
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
/* already gone */
|
|
111
|
+
}
|
|
112
|
+
removeSessionFiles(meta.projectDir, sessionId);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
/** Stop every live session registered for a project. */
|
|
116
|
+
export function stopAllBrowserSessions(projectDir) {
|
|
117
|
+
const sessions = listBrowserSessions(projectDir);
|
|
118
|
+
for (const meta of sessions) {
|
|
119
|
+
stopBrowserSession(meta.projectDir, meta.sessionId);
|
|
120
|
+
}
|
|
121
|
+
return sessions.length;
|
|
122
|
+
}
|
|
123
|
+
/** Live browser sessions for a project (metadata + socket + pid still present). */
|
|
124
|
+
export function listBrowserSessions(projectDir) {
|
|
125
|
+
const dir = sessionDir(projectDir);
|
|
126
|
+
if (!existsSync(dir))
|
|
127
|
+
return [];
|
|
128
|
+
const out = [];
|
|
129
|
+
for (const f of readdirSync(dir)) {
|
|
130
|
+
if (!f.endsWith(".json"))
|
|
131
|
+
continue;
|
|
132
|
+
const id = f.slice(0, -".json".length);
|
|
133
|
+
const meta = readSessionMeta(projectDir, id);
|
|
134
|
+
if (!meta)
|
|
135
|
+
continue;
|
|
136
|
+
if (!existsSync(socketPathFor(id)))
|
|
137
|
+
continue;
|
|
138
|
+
if (!isPidAlive(meta.pid)) {
|
|
139
|
+
removeSessionFiles(projectDir, id);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
out.push(meta);
|
|
143
|
+
}
|
|
144
|
+
return out.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
145
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { PilotHighlightFixtures } from "../../demo/overlay/pilot/fixtures.js";
|
|
1
2
|
import type { StubPhaseResult } from "../../engine/stub/stub.js";
|
|
2
|
-
import type { ExploreMenu } from "../auto/explore.js";
|
|
3
3
|
import type { BlockEntry } from "../../manifest/project.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { ExploreMenu } from "../auto/explore.js";
|
|
5
|
+
import type { PickResult } from "../auto/menu.js";
|
|
5
6
|
/** True when a stub-phase result actually carries authored content worth keeping. */
|
|
6
7
|
export declare function stubPhaseHasContent(r: StubPhaseResult): boolean;
|
|
7
8
|
/** One available move in a session snapshot: a Block and the Checkpoint it leads to. */
|
|
@@ -211,4 +212,6 @@ export interface StorageSnapshot {
|
|
|
211
212
|
serviceWorkers: string[];
|
|
212
213
|
serviceWorkerRegistrations: ServiceWorkerRegistrationInfo[];
|
|
213
214
|
}
|
|
215
|
+
/** What a highlight call paints, in a few words, for the session's activity line. */
|
|
216
|
+
export declare function describeHighlight(fixtures: PilotHighlightFixtures): string;
|
|
214
217
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// The session's shared shapes and pure helpers: snapshots, picks, trace / console entries, DOM walking, stub files.
|
|
1
2
|
import { dirname, join, resolve } from "node:path";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
4
|
/** True when a stub-phase result actually carries authored content worth keeping. */
|
|
@@ -146,3 +147,19 @@ export function stubFilePath(kind) {
|
|
|
146
147
|
return join(packageRoot, "assets", "stubs", names[kind]);
|
|
147
148
|
}
|
|
148
149
|
export const CONSOLE_LOG_MAX_ENTRIES = 200;
|
|
150
|
+
/** What a highlight call paints, in a few words, for the session's activity line. */
|
|
151
|
+
export function describeHighlight(fixtures) {
|
|
152
|
+
if (fixtures.clear === true)
|
|
153
|
+
return "clear";
|
|
154
|
+
return ([
|
|
155
|
+
fixtures.rings?.length ? `${fixtures.rings.length} ring(s)` : null,
|
|
156
|
+
fixtures.todos?.length ? `${fixtures.todos.length} todo(s)` : null,
|
|
157
|
+
fixtures.zoom && fixtures.zoom > 1.001 ? `zoom ${fixtures.zoom}` : null,
|
|
158
|
+
fixtures.device
|
|
159
|
+
? `device ${typeof fixtures.device === "string" ? fixtures.device : fixtures.device.preset || "custom"}`
|
|
160
|
+
: null,
|
|
161
|
+
fixtures.rings?.some((r) => r.focus) ? "focus" : null,
|
|
162
|
+
]
|
|
163
|
+
.filter(Boolean)
|
|
164
|
+
.join(", ") || "fixtures");
|
|
165
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from "@playwright/test";
|
|
2
|
+
import type { InspectDomOptions, InspectDomResult, StorageSnapshot } from "./helpers.js";
|
|
3
|
+
export declare function inspectDom(page: Page, opts: InspectDomOptions): Promise<InspectDomResult>;
|
|
4
|
+
export declare function inspectStorage(page: Page, context: BrowserContext): Promise<StorageSnapshot>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { showPilotActivity, showPilotVision } from "../../demo/overlay/pilot/shell.js";
|
|
2
|
+
import { ignorePageGone } from "../../engine/page-gone.js";
|
|
3
|
+
import { FULL_MODE_DEFAULT_DEPTH, FULL_MODE_MAX_NODES, FULL_MODE_MAX_TEXT_LENGTH, walkFullDom } from "./helpers.js";
|
|
4
|
+
export async function inspectDom(page, opts) {
|
|
5
|
+
const mode = opts.mode ?? "aria";
|
|
6
|
+
const ariaOptions = opts.depth !== undefined ? { mode: "ai", depth: opts.depth } : { mode: "ai" };
|
|
7
|
+
if (opts.selector) {
|
|
8
|
+
const locator = page.locator(opts.selector).first();
|
|
9
|
+
if ((await locator.count()) === 0) {
|
|
10
|
+
return { ok: false, error: `no element matches selector "${opts.selector}"` };
|
|
11
|
+
}
|
|
12
|
+
await showPilotVision(page, opts.selector, `dom (${mode}): ${opts.selector}`, "orange");
|
|
13
|
+
if (mode === "full") {
|
|
14
|
+
const caps = {
|
|
15
|
+
depth: opts.depth ?? FULL_MODE_DEFAULT_DEPTH,
|
|
16
|
+
maxNodes: FULL_MODE_MAX_NODES,
|
|
17
|
+
maxTextLength: FULL_MODE_MAX_TEXT_LENGTH,
|
|
18
|
+
};
|
|
19
|
+
const { tree, truncated } = await locator.evaluate(walkFullDom, caps);
|
|
20
|
+
return { ok: true, snapshot: { mode: "full", selector: opts.selector, truncated, tree } };
|
|
21
|
+
}
|
|
22
|
+
const tree = await locator.ariaSnapshotJSON(ariaOptions);
|
|
23
|
+
return { ok: true, snapshot: { mode: "aria", selector: opts.selector, truncated: false, tree } };
|
|
24
|
+
}
|
|
25
|
+
await showPilotActivity(page, `Running (Dom): ${mode}, whole page`);
|
|
26
|
+
if (mode === "full") {
|
|
27
|
+
// page.evaluate only ships the one function passed to it - it can't
|
|
28
|
+
// reach another named function by closure. Routing the whole-page case
|
|
29
|
+
// through the same `html` Locator lets both cases share one
|
|
30
|
+
// self-contained walkFullDom instead of two near-duplicate callbacks.
|
|
31
|
+
const caps = {
|
|
32
|
+
depth: opts.depth ?? FULL_MODE_DEFAULT_DEPTH,
|
|
33
|
+
maxNodes: FULL_MODE_MAX_NODES,
|
|
34
|
+
maxTextLength: FULL_MODE_MAX_TEXT_LENGTH,
|
|
35
|
+
};
|
|
36
|
+
const { tree, truncated } = await page.locator("html").first().evaluate(walkFullDom, caps);
|
|
37
|
+
return { ok: true, snapshot: { mode: "full", truncated, tree } };
|
|
38
|
+
}
|
|
39
|
+
const tree = await page.ariaSnapshotJSON(ariaOptions);
|
|
40
|
+
return { ok: true, snapshot: { mode: "aria", truncated: false, tree } };
|
|
41
|
+
}
|
|
42
|
+
export async function inspectStorage(page, context) {
|
|
43
|
+
const [localStorageEntries, sessionStorageEntries] = await page.evaluate(() => [
|
|
44
|
+
Object.entries(window.localStorage),
|
|
45
|
+
Object.entries(window.sessionStorage),
|
|
46
|
+
]);
|
|
47
|
+
const cookies = await context.cookies();
|
|
48
|
+
const workers = context.serviceWorkers();
|
|
49
|
+
const registrations = await page
|
|
50
|
+
.evaluate(async () => {
|
|
51
|
+
if (!("serviceWorker" in navigator))
|
|
52
|
+
return [];
|
|
53
|
+
const regs = await navigator.serviceWorker.getRegistrations();
|
|
54
|
+
return regs.map((r) => ({
|
|
55
|
+
scope: r.scope,
|
|
56
|
+
...(r.active?.scriptURL ? { activeUrl: r.active.scriptURL } : {}),
|
|
57
|
+
...(r.active?.state ? { state: r.active.state } : {}),
|
|
58
|
+
}));
|
|
59
|
+
})
|
|
60
|
+
.catch(ignorePageGone);
|
|
61
|
+
return {
|
|
62
|
+
localStorage: Object.fromEntries(localStorageEntries),
|
|
63
|
+
sessionStorage: Object.fromEntries(sessionStorageEntries),
|
|
64
|
+
cookies: cookies.map((c) => ({
|
|
65
|
+
name: c.name,
|
|
66
|
+
value: c.value,
|
|
67
|
+
domain: c.domain,
|
|
68
|
+
path: c.path,
|
|
69
|
+
httpOnly: c.httpOnly,
|
|
70
|
+
secure: c.secure,
|
|
71
|
+
})),
|
|
72
|
+
serviceWorkers: workers.map((w) => w.url()),
|
|
73
|
+
serviceWorkerRegistrations: registrations ?? [],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
import { AutoSession } from "./auto-session.js";
|
|
2
|
-
import { type AutoSessionInit, type SessionSnapshot, type InspectDomOptions, type DomSnapshot, type TraceStep, type ApplyPathResult, type ConsoleLogEntry, type StorageSnapshot, type StubFileKind } from "./helpers.js";
|
|
3
1
|
import type { PilotHighlightFixtures } from "../../demo/overlay/pilot/fixtures.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
pid: number;
|
|
8
|
-
projectDir: string;
|
|
9
|
-
startedAt: string;
|
|
10
|
-
headless: boolean;
|
|
11
|
-
}
|
|
2
|
+
import { AutoSession } from "./auto-session.js";
|
|
3
|
+
import { SessionMeta } from "./files.js";
|
|
4
|
+
import { type ApplyPathResult, type AutoSessionInit, type ConsoleLogEntry, type DomSnapshot, type InspectDomOptions, type SessionSnapshot, type StorageSnapshot, type StubFileKind, type TraceStep } from "./helpers.js";
|
|
12
5
|
export type StatusOrSendResponse = {
|
|
13
6
|
ok: true;
|
|
14
7
|
snapshot: SessionSnapshot;
|
|
@@ -54,12 +47,6 @@ export type HighlightResponse = {
|
|
|
54
47
|
ok: false;
|
|
55
48
|
error: string;
|
|
56
49
|
};
|
|
57
|
-
/**
|
|
58
|
-
* Resolve session metadata from a project hint (cwd) or the global registry.
|
|
59
|
-
* Fixes "session unreachable" when the agent calls status/send from a different
|
|
60
|
-
* directory or pane than the one that ran `pilot start`.
|
|
61
|
-
*/
|
|
62
|
-
export declare function resolveSessionMeta(sessionId: string, projectDirHint?: string): SessionMeta | null;
|
|
63
50
|
/**
|
|
64
51
|
* Client-side: sends one request to a running session and returns its
|
|
65
52
|
* response. Reports a stale/unreachable session as an error within
|
|
@@ -137,12 +124,6 @@ export declare function runAutoServeCommand(init: AutoSessionInit, sessionId: st
|
|
|
137
124
|
* than re-implementing that resolution here.
|
|
138
125
|
*/
|
|
139
126
|
export declare function spawnDetachedSession(init: AutoSessionInit, readyTimeoutMs?: number): Promise<SessionMeta>;
|
|
140
|
-
/** Stop one detached session (SIGTERM + cleanup metadata/socket). */
|
|
141
|
-
export declare function stopBrowserSession(projectDir: string, sessionId: string): boolean;
|
|
142
|
-
/** Stop every live session registered for a project. */
|
|
143
|
-
export declare function stopAllBrowserSessions(projectDir: string): number;
|
|
144
|
-
/** Live browser sessions for a project (metadata + socket + pid still present). */
|
|
145
|
-
export declare function listBrowserSessions(projectDir: string): SessionMeta[];
|
|
146
127
|
/**
|
|
147
128
|
* Client-only interactive loop against a running detached session - reopens
|
|
148
129
|
* the same rendering/parsing the foreground `--cli` loop uses, driven purely
|
|
@@ -4,112 +4,17 @@
|
|
|
4
4
|
* directory, mirroring the existing `.waygraph-traverse/` dotdir convention.
|
|
5
5
|
* See openspec/changes/waygraph-auto-cli-session-control.
|
|
6
6
|
*/
|
|
7
|
-
import { createServer, connect } from "node:net";
|
|
8
7
|
import { spawn } from "node:child_process";
|
|
8
|
+
import { mkdirSync, openSync, rmSync, writeFileSync, } from "node:fs";
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
13
|
-
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { randomBytes } from "node:crypto";
|
|
15
|
-
import { createInterface } from "node:readline/promises";
|
|
10
|
+
import { connect, createServer } from "node:net";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
16
12
|
import { stdin as input, stdout as output } from "node:process";
|
|
17
|
-
import {
|
|
13
|
+
import { createInterface } from "node:readline/promises";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
18
15
|
import { ignorePageGone } from "../../engine/page-gone.js";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
function metaPath(projectDir, sessionId) {
|
|
23
|
-
return join(sessionDir(projectDir), `${sessionId}.json`);
|
|
24
|
-
}
|
|
25
|
-
/** Global index so session control works even when cwd ≠ the project that spawned the session. */
|
|
26
|
-
function globalMetaPath(sessionId) {
|
|
27
|
-
return join(socketDir(), `${sessionId}.meta.json`);
|
|
28
|
-
}
|
|
29
|
-
function isPidAlive(pid) {
|
|
30
|
-
try {
|
|
31
|
-
process.kill(pid, 0);
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Deliberately NOT under the project directory (unlike meta/log files) -
|
|
40
|
-
* a real bug found while proving out openspec/changes/waygraph-map's own
|
|
41
|
-
* "waypack" loading claim: a project loaded through a consumer's own deeply
|
|
42
|
-
* nested `node_modules/<pkg>` path can push the socket's absolute path past
|
|
43
|
-
* the OS's AF_UNIX `sun_path` limit (~108 bytes on Linux), failing with
|
|
44
|
-
* `listen EINVAL`. A short, fixed-location directory sidesteps this
|
|
45
|
-
* regardless of how deep `projectDir` itself is nested. `sessionId` alone
|
|
46
|
-
* (8 hex chars, `generateSessionId()`) is enough to keep this collision-safe
|
|
47
|
-
* across every project on the machine sharing this one flat directory - the
|
|
48
|
-
* same randomness space this package already relied on per-project.
|
|
49
|
-
*/
|
|
50
|
-
function socketDir() {
|
|
51
|
-
return join(tmpdir(), "waygraph-auto");
|
|
52
|
-
}
|
|
53
|
-
function socketPathFor(sessionId) {
|
|
54
|
-
return join(socketDir(), `${sessionId}.sock`);
|
|
55
|
-
}
|
|
56
|
-
function logPathFor(projectDir, sessionId) {
|
|
57
|
-
return join(sessionDir(projectDir), `${sessionId}.log`);
|
|
58
|
-
}
|
|
59
|
-
function generateSessionId() {
|
|
60
|
-
return randomBytes(4).toString("hex");
|
|
61
|
-
}
|
|
62
|
-
function readSessionMeta(projectDir, sessionId) {
|
|
63
|
-
const p = metaPath(projectDir, sessionId);
|
|
64
|
-
if (!existsSync(p))
|
|
65
|
-
return null;
|
|
66
|
-
try {
|
|
67
|
-
return JSON.parse(readFileSync(p, "utf-8"));
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Resolve session metadata from a project hint (cwd) or the global registry.
|
|
75
|
-
* Fixes "session unreachable" when the agent calls status/send from a different
|
|
76
|
-
* directory or pane than the one that ran `pilot start`.
|
|
77
|
-
*/
|
|
78
|
-
export function resolveSessionMeta(sessionId, projectDirHint) {
|
|
79
|
-
if (projectDirHint) {
|
|
80
|
-
const local = readSessionMeta(projectDirHint, sessionId);
|
|
81
|
-
if (local)
|
|
82
|
-
return local;
|
|
83
|
-
}
|
|
84
|
-
const globalPath = globalMetaPath(sessionId);
|
|
85
|
-
if (existsSync(globalPath)) {
|
|
86
|
-
try {
|
|
87
|
-
return JSON.parse(readFileSync(globalPath, "utf-8"));
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
function writeGlobalSessionMeta(meta) {
|
|
96
|
-
mkdirSync(socketDir(), { recursive: true });
|
|
97
|
-
writeFileSync(globalMetaPath(meta.sessionId), JSON.stringify(meta, null, 2));
|
|
98
|
-
}
|
|
99
|
-
function removeSessionFiles(projectDir, sessionId) {
|
|
100
|
-
for (const p of [
|
|
101
|
-
metaPath(projectDir, sessionId),
|
|
102
|
-
globalMetaPath(sessionId),
|
|
103
|
-
socketPathFor(sessionId),
|
|
104
|
-
]) {
|
|
105
|
-
try {
|
|
106
|
-
rmSync(p, { force: true });
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
/* ignore */
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
16
|
+
import { AutoSession } from "./auto-session.js";
|
|
17
|
+
import { generateSessionId, isPidAlive, logPathFor, metaPath, readSessionMeta, removeSessionFiles, resolveSessionMeta, sessionDir, socketDir, socketPathFor, writeGlobalSessionMeta } from "./files.js";
|
|
113
18
|
/** Reads newline-delimited JSON off a socket, one message at a time. */
|
|
114
19
|
function readOneMessage(sock, timeoutMs) {
|
|
115
20
|
return new Promise((resolvePromise, reject) => {
|
|
@@ -412,51 +317,6 @@ export async function spawnDetachedSession(init, readyTimeoutMs = 20_000) {
|
|
|
412
317
|
throw new Error(`session "${sessionId}" said it was ready but wrote no metadata - check ${log}`);
|
|
413
318
|
return meta;
|
|
414
319
|
}
|
|
415
|
-
/** Stop one detached session (SIGTERM + cleanup metadata/socket). */
|
|
416
|
-
export function stopBrowserSession(projectDir, sessionId) {
|
|
417
|
-
const meta = resolveSessionMeta(sessionId, projectDir);
|
|
418
|
-
if (!meta)
|
|
419
|
-
return false;
|
|
420
|
-
try {
|
|
421
|
-
process.kill(meta.pid, "SIGTERM");
|
|
422
|
-
}
|
|
423
|
-
catch {
|
|
424
|
-
/* already gone */
|
|
425
|
-
}
|
|
426
|
-
removeSessionFiles(meta.projectDir, sessionId);
|
|
427
|
-
return true;
|
|
428
|
-
}
|
|
429
|
-
/** Stop every live session registered for a project. */
|
|
430
|
-
export function stopAllBrowserSessions(projectDir) {
|
|
431
|
-
const sessions = listBrowserSessions(projectDir);
|
|
432
|
-
for (const meta of sessions) {
|
|
433
|
-
stopBrowserSession(meta.projectDir, meta.sessionId);
|
|
434
|
-
}
|
|
435
|
-
return sessions.length;
|
|
436
|
-
}
|
|
437
|
-
/** Live browser sessions for a project (metadata + socket + pid still present). */
|
|
438
|
-
export function listBrowserSessions(projectDir) {
|
|
439
|
-
const dir = sessionDir(projectDir);
|
|
440
|
-
if (!existsSync(dir))
|
|
441
|
-
return [];
|
|
442
|
-
const out = [];
|
|
443
|
-
for (const f of readdirSync(dir)) {
|
|
444
|
-
if (!f.endsWith(".json"))
|
|
445
|
-
continue;
|
|
446
|
-
const id = f.slice(0, -".json".length);
|
|
447
|
-
const meta = readSessionMeta(projectDir, id);
|
|
448
|
-
if (!meta)
|
|
449
|
-
continue;
|
|
450
|
-
if (!existsSync(socketPathFor(id)))
|
|
451
|
-
continue;
|
|
452
|
-
if (!isPidAlive(meta.pid)) {
|
|
453
|
-
removeSessionFiles(projectDir, id);
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
out.push(meta);
|
|
457
|
-
}
|
|
458
|
-
return out.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
459
|
-
}
|
|
460
320
|
function printSnapshotMenu(snapshot) {
|
|
461
321
|
console.log("");
|
|
462
322
|
console.log(`You are here: ${snapshot.here ?? "Unknown (pick Start here)"}`);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from "@playwright/test";
|
|
2
|
+
export declare function openSessionBrowser(projectDir: string, headless: boolean, baseURL: string | undefined, startUrl: string | undefined): Promise<{
|
|
3
|
+
closeBrowser: () => Promise<void>;
|
|
4
|
+
context: BrowserContext;
|
|
5
|
+
page: Page;
|
|
6
|
+
}>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { instrumentPilotActions } from "../../demo/overlay/pilot/actions.js";
|
|
2
|
+
import { installPersistentPilotOverlay } from "../../demo/overlay/pilot/shell.js";
|
|
3
|
+
import { ignorePageGone, ignoreTimeout } from "../../engine/page-gone.js";
|
|
4
|
+
import { chromiumExecutablePath, launchContext } from "../../runtime/browser.js";
|
|
5
|
+
import { configuredExtensions } from "../../runtime/waygraph-config.js";
|
|
6
|
+
export async function openSessionBrowser(projectDir, headless, baseURL, startUrl) {
|
|
7
|
+
const { chromium } = await import("@playwright/test");
|
|
8
|
+
const executablePath = chromiumExecutablePath();
|
|
9
|
+
// Real gap found live: a headed (--non-headless) session kept a fixed
|
|
10
|
+
// 1280x720 content area even when the user maximized/fullscreened the
|
|
11
|
+
// actual window - `viewport: null` alone only makes the PAGE track the
|
|
12
|
+
// window; the window itself still launches at Chromium's own small
|
|
13
|
+
// default size unless told to start maximized. Same fix cli.ts's own
|
|
14
|
+
// --step/headed launch already uses (see its own comment there) -
|
|
15
|
+
// headless keeps the fixed viewport since there's no real window to size.
|
|
16
|
+
const launchOpts = {
|
|
17
|
+
headless,
|
|
18
|
+
// Keep Playwright's default --no-startup-window for headful: without it
|
|
19
|
+
// Chromium opens its own "New Tab" window AND our context.newPage() opens
|
|
20
|
+
// a second about:blank window. bringToFront() below surfaces the real page.
|
|
21
|
+
args: headless ? [] : ["--start-maximized"],
|
|
22
|
+
};
|
|
23
|
+
if (executablePath)
|
|
24
|
+
launchOpts.executablePath = executablePath;
|
|
25
|
+
const contextOpts = {
|
|
26
|
+
viewport: headless ? { width: 1280, height: 720 } : null,
|
|
27
|
+
};
|
|
28
|
+
if (baseURL)
|
|
29
|
+
contextOpts.baseURL = baseURL;
|
|
30
|
+
const { close: closeBrowser, context } = await launchContext({
|
|
31
|
+
launcher: chromium,
|
|
32
|
+
launch: launchOpts,
|
|
33
|
+
context: contextOpts,
|
|
34
|
+
extensions: await configuredExtensions(projectDir),
|
|
35
|
+
});
|
|
36
|
+
await installPersistentPilotOverlay(context);
|
|
37
|
+
const page = await context.newPage();
|
|
38
|
+
instrumentPilotActions(page, headless ? 0 : 450);
|
|
39
|
+
if (startUrl) {
|
|
40
|
+
await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(ignorePageGone);
|
|
41
|
+
await page.waitForLoadState("load").catch(ignoreTimeout);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
await page.goto("about:blank").catch(ignorePageGone);
|
|
45
|
+
}
|
|
46
|
+
return { closeBrowser, context, page };
|
|
47
|
+
}
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
* new execution primitive.
|
|
30
30
|
*/
|
|
31
31
|
import { type WaygraphGraph } from "../../analysis/graph.js";
|
|
32
|
-
import { type BrowserStartInit } from "../auto-session/start.js";
|
|
33
32
|
import type { SessionSnapshot } from "../auto-session/helpers.js";
|
|
33
|
+
import { type BrowserStartInit } from "../auto-session/start.js";
|
|
34
34
|
/** What `pilotStart` returns: the session id, socket path and first snapshot. */
|
|
35
35
|
export interface PilotStartResult {
|
|
36
36
|
sessionId: string;
|
package/dist/live/pilot/pilot.js
CHANGED
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
* new execution primitive.
|
|
30
30
|
*/
|
|
31
31
|
import { discoverGraph } from "../../analysis/graph.js";
|
|
32
|
-
import {
|
|
32
|
+
import { resolveSessionMeta } from "../auto-session/files.js";
|
|
33
|
+
import { requestSession, } from "../auto-session/ipc.js";
|
|
33
34
|
import { browserStart } from "../auto-session/start.js";
|
|
34
35
|
/**
|
|
35
36
|
* Starts a detached session and reads back its own graph + starting
|