waygraph 0.15.51 → 0.15.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/README.md +8 -1
  2. package/dist/analysis/graph.d.ts +1 -1
  3. package/dist/analysis/graph.js +10 -15
  4. package/dist/analysis/map-check.d.ts +3 -2
  5. package/dist/analysis/practices-check.d.ts +0 -1
  6. package/dist/analysis/practices-check.js +0 -13
  7. package/dist/auto-session/session.d.ts +1 -1
  8. package/dist/auto-session/session.js +13 -8
  9. package/dist/blocks-select.d.ts +7 -4
  10. package/dist/blocks-select.js +0 -14
  11. package/dist/browser.d.ts +27 -0
  12. package/dist/browser.js +62 -0
  13. package/dist/cli/flags.js +3 -1
  14. package/dist/cli/run-chain.js +4 -0
  15. package/dist/cli/usage.js +17 -1
  16. package/dist/cli.js +13 -1
  17. package/dist/commands/agent-dive-scaffold.d.ts +5 -20
  18. package/dist/commands/agent-dive-scaffold.js +10 -3
  19. package/dist/commands/extensions.d.ts +1 -0
  20. package/dist/commands/extensions.js +22 -0
  21. package/dist/commands/init.js +43 -5
  22. package/dist/commands/test.js +16 -2
  23. package/dist/commands/try.js +2 -1
  24. package/dist/commands/upgrade.d.ts +6 -0
  25. package/dist/commands/upgrade.js +113 -0
  26. package/dist/config.d.ts +81 -3
  27. package/dist/config.js +91 -16
  28. package/dist/core/block.d.ts +19 -2
  29. package/dist/core/block.js +43 -3
  30. package/dist/core/blocks/nav.d.ts +11 -0
  31. package/dist/core/blocks/nav.js +8 -0
  32. package/dist/core/config.d.ts +5 -0
  33. package/dist/core/engine.d.ts +19 -16
  34. package/dist/core/engine.js +6 -4
  35. package/dist/core/flow.d.ts +27 -0
  36. package/dist/core/flow.js +75 -28
  37. package/dist/core/map-builder.d.ts +30 -4
  38. package/dist/core/map-builder.js +45 -7
  39. package/dist/env-file.d.ts +7 -7
  40. package/dist/env-file.js +29 -16
  41. package/dist/extensions.d.ts +21 -0
  42. package/dist/extensions.js +178 -0
  43. package/dist/highlights/demo-pace.d.ts +0 -1
  44. package/dist/highlights/demo-pace.js +24 -3
  45. package/dist/index.d.ts +7 -5
  46. package/dist/index.js +3 -2
  47. package/dist/pilot-overlay/action-ring.d.ts +0 -9
  48. package/dist/pilot-overlay/action-ring.js +1 -1
  49. package/dist/pilot-overlay/overlay-fixtures.d.ts +0 -7
  50. package/dist/pilot-overlay/overlay-fixtures.js +0 -45
  51. package/dist/pilot-overlay/panel.d.ts +2 -1
  52. package/dist/playwright-config.d.ts +4 -0
  53. package/dist/playwright-config.js +15 -0
  54. package/dist/playwright.config.d.ts +2 -0
  55. package/dist/playwright.config.js +6 -0
  56. package/dist/runner/demo-log.js +1 -7
  57. package/dist/runner/inpage/core.d.ts +1 -1
  58. package/dist/runner/inpage/core.js +8 -7
  59. package/dist/runner/interaction-patch.js +32 -1
  60. package/dist/runner/main.js +22 -11
  61. package/dist/runner/rings.js +0 -49
  62. package/dist/runner/step-mode.js +10 -4
  63. package/dist/session/auto-explore-run.js +9 -5
  64. package/dist/session/auto-explore.d.ts +2 -1
  65. package/dist/session/auto-explore.js +6 -4
  66. package/dist/session/auto-session-ipc.d.ts +1 -2
  67. package/dist/session/auto-session-ipc.js +1 -1
  68. package/dist/session/block-inject.d.ts +0 -13
  69. package/dist/session/block-inject.js +3 -3
  70. package/dist/session/pilot.d.ts +0 -2
  71. package/dist/session/pilot.js +1 -2
  72. package/dist/session/step-overlay.js +3 -1
  73. package/dist/traverse/coverage.d.ts +0 -4
  74. package/dist/traverse/coverage.js +1 -1
  75. package/dist/traverse/run.js +3 -1
  76. package/dist/types.d.ts +1 -1
  77. package/dist/ui/components/ring.js +1 -0
  78. package/dist/ui/overlay.css +1 -0
  79. package/dist/ui/shadow.d.ts +1 -3
  80. package/dist/ui/shadow.js +0 -4
  81. package/dist/ui/tokens.d.ts +0 -11
  82. package/dist/ui/tokens.js +0 -5
  83. package/dist/video-dir.d.ts +8 -0
  84. package/dist/video-dir.js +60 -0
  85. package/dist/webserver.js +5 -1
  86. package/package.json +11 -5
  87. package/templates/scaffold/README.md +5 -3
  88. package/templates/scaffold/STRUCTURE.md +10 -4
  89. package/templates/scaffold/config/waygraph.config.ts +45 -0
  90. package/templates/scaffold/package.json +7 -7
  91. package/templates/scaffold/src/flows/(external)/hoppscotch/auth.flow.ts +36 -0
  92. package/templates/scaffold/src/flows/(external)/hoppscotch/get.flow.ts +24 -0
  93. package/templates/scaffold/src/flows/(external)/hoppscotch/post.flow.ts +34 -0
  94. package/templates/scaffold/src/flows/(user-stories)/email-verification.flow.ts +47 -0
  95. package/templates/scaffold/src/flows/README.md +35 -14
  96. package/templates/scaffold/src/flows/home/cart.flow.ts +51 -0
  97. package/templates/scaffold/src/flows/home/hello.flow.ts +42 -0
  98. package/templates/scaffold/src/map/(app_base)/docs/_nav.block.ts +0 -9
  99. package/templates/scaffold/src/map/(app_base)/docs/_page.block.ts +0 -5
  100. package/templates/scaffold/src/map/(app_base)/docs/_sel.ts +1 -1
  101. package/templates/scaffold/src/map/(app_base)/home/_methods/add-item.effect.block.ts +3 -19
  102. package/templates/scaffold/src/map/(app_base)/home/_methods/assert-hello.method.block.ts +2 -25
  103. package/templates/scaffold/src/map/(app_base)/home/_methods/assert-item-added.method.block.ts +2 -19
  104. package/templates/scaffold/src/map/(app_base)/home/_methods/clear-cart.method.block.ts +1 -17
  105. package/templates/scaffold/src/map/(app_base)/home/_methods/remove-item.effect.block.ts +1 -10
  106. package/templates/scaffold/src/map/(app_base)/home/_nav.block.ts +1 -12
  107. package/templates/scaffold/src/map/(app_base)/home/_page.block.ts +0 -9
  108. package/templates/scaffold/src/map/(app_base)/home/_sel.ts +3 -1
  109. package/templates/scaffold/src/map/(app_base)/home/nav-verification-link.block.ts +1 -23
  110. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/assert-response-body.method.block.ts +21 -0
  111. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/assert-response-header.method.block.ts +19 -0
  112. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/assert-status.method.block.ts +20 -0
  113. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/pick-extension-interceptor.method.block.ts +18 -0
  114. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/read-response-body.method.block.ts +21 -0
  115. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/read-response-headers.method.block.ts +21 -0
  116. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/send-request.method.block.ts +15 -0
  117. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-bearer-token.method.block.ts +20 -0
  118. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-headers.method.block.ts +23 -0
  119. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-json-body.method.block.ts +36 -0
  120. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-method.method.block.ts +17 -0
  121. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-query-params.method.block.ts +22 -0
  122. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-url.method.block.ts +18 -0
  123. package/templates/scaffold/src/map/(external)/hoppscotch/_nav.block.ts +11 -0
  124. package/templates/scaffold/src/map/(external)/hoppscotch/_page.block.ts +36 -0
  125. package/templates/scaffold/src/map/(external)/hoppscotch/_sel.ts +27 -0
  126. package/templates/scaffold/src/map/(external)/hoppscotch/settings/_nav.block.ts +10 -0
  127. package/templates/scaffold/src/map/(external)/mailpit/_methods/assert-email-content.method.block.ts +2 -27
  128. package/templates/scaffold/src/map/(external)/mailpit/_methods/assert-email-received.method.block.ts +2 -21
  129. package/templates/scaffold/src/map/(external)/mailpit/_methods/extract-email-link.method.block.ts +1 -19
  130. package/templates/scaffold/src/map/(external)/mailpit/_methods/open-message.method.block.ts +1 -11
  131. package/templates/scaffold/src/map/(external)/mailpit/_nav.block.ts +1 -9
  132. package/templates/scaffold/src/map/(external)/mailpit/_page.block.ts +0 -11
  133. package/templates/scaffold/src/states/demo.mem-keys.ts +20 -19
  134. package/templates/scaffold/src/states/demo.mem-stubs.ts +4 -0
  135. package/templates/scaffold/src/states/demo.states.ts +5 -1
  136. package/templates/scaffold/tests/example.spec.ts +6 -7
  137. package/templates/scaffold/tests/hoppscotch.spec.ts +97 -0
  138. package/templates/scaffold/tests/mail-verify.spec.ts +8 -17
  139. package/templates/scaffold/tsconfig.json +1 -1
  140. package/templates/skills/waygraph-convention.skill.md +40 -3
  141. package/templates/skills/waygraph-pilot.skill.md +4 -5
  142. package/dist/browser-path.d.ts +0 -5
  143. package/dist/browser-path.js +0 -18
  144. package/dist/runner/overlay-present.d.ts +0 -8
  145. package/dist/runner/overlay-present.js +0 -57
  146. package/dist/ui/components/banner.d.ts +0 -2
  147. package/dist/ui/components/banner.js +0 -10
  148. package/dist/ui/components/cursor.d.ts +0 -2
  149. package/dist/ui/components/cursor.js +0 -19
  150. package/templates/scaffold/playwright.config.ts +0 -12
  151. package/templates/scaffold/src/flows/example.flow.ts +0 -29
  152. package/templates/scaffold/src/flows/mail-verify.flow.ts +0 -85
  153. package/templates/scaffold/src/flows/shop.flow.ts +0 -74
package/README.md CHANGED
@@ -16,7 +16,14 @@ export const SubmitLoginBlock = defineMethodBlock<LoginPage, LoggedIn>({
16
16
  });
17
17
 
18
18
  // A Flow = Blocks in order.
19
- export const loginFlow = engine.defineFlow([start, NavLoginBlock, FillUsernameBlock, FillPasswordBlock, SubmitLoginBlock, end]);
19
+ export const loginFlow = engine
20
+ .map()
21
+ .start()
22
+ .gotoPage(NavLoginBlock)
23
+ .method(FillUsernameBlock)
24
+ .method(FillPasswordBlock)
25
+ .method(SubmitLoginBlock)
26
+ .end();
20
27
  ```
21
28
 
22
29
  ## Try it in 60 seconds
@@ -13,7 +13,7 @@ export interface WaygraphEdge {
13
13
  block: string;
14
14
  /** Path the Block was discovered in, relative to the project root. */
15
15
  file: string;
16
- /** `"*"` for a NavBlock - reachable from anywhere, by design. */
16
+ /** `"*"` for a NavBlock - reachable from anywhere, unless it declares `from` (then one edge per tag). */
17
17
  from: string;
18
18
  to: string;
19
19
  kind: "nav" | "action";
@@ -53,21 +53,10 @@ function discoverBlocks(projectDir) {
53
53
  function discoverFlows(projectDir) {
54
54
  return walkDir(projectDir, /\.flow\.ts$/);
55
55
  }
56
- /** Block export ids listed inside any `defineFlow([...])` array in flow files. */
56
+ /** Block export ids a flow file uses: every identifier outside its import lines (a Block can sit in the array or in a local like `const AddShown = AddBlock.stubAfter(...)`). */
57
57
  function extractFlowBlockRefs(flowSrc) {
58
- const refs = [];
59
- const re = /defineFlow(?:<[\s\S]*?>)?\s*\(\s*\[([\s\S]*?)\]\s*\)/g;
60
- let m;
61
- while ((m = re.exec(flowSrc)) !== null) {
62
- for (const part of m[1].split(",")) {
63
- const trimmed = part.trim().replace(/\/\/.*$/, "").trim();
64
- const id = trimmed.split(/\s+/)[0];
65
- if (!id || id === "start" || id === "end")
66
- continue;
67
- refs.push(id);
68
- }
69
- }
70
- return refs;
58
+ const body = flowSrc.replace(/^\s*import[\s\S]*?;\s*$/gm, "").replace(/\/\/.*$/gm, "");
59
+ return body.match(/[A-Za-z_$][\w$]*/g) ?? [];
71
60
  }
72
61
  function exportNameToBlockName(exportName) {
73
62
  const base = exportName.replace(/Block$/, "");
@@ -420,7 +409,13 @@ export async function discoverGraph(projectDir, opts) {
420
409
  const deepLink = !isPage ||
421
410
  exported.__waygraphPageDeepLink === true;
422
411
  if (deepLink) {
423
- edges.push({ block: blockName, file: relFile, from: "*", to, kind: "nav" });
412
+ // `from: [...]` on the Nav = only those Checkpoints; otherwise reachable from anywhere.
413
+ const navFrom = exported.__waygraphNavFrom;
414
+ for (const from of navFrom ?? ["*"]) {
415
+ if (from !== "*")
416
+ nodeTags.add(from);
417
+ edges.push({ block: blockName, file: relFile, from, to, kind: "nav" });
418
+ }
424
419
  }
425
420
  }
426
421
  catch {
@@ -1,9 +1,9 @@
1
- export interface MapViolation {
1
+ interface MapViolation {
2
2
  file: string;
3
3
  block: string;
4
4
  reason: string;
5
5
  }
6
- export interface MapNode {
6
+ interface MapNode {
7
7
  block: string;
8
8
  checkpoint: string | undefined;
9
9
  file: string;
@@ -16,3 +16,4 @@ export interface MapCheckResult {
16
16
  violations: MapViolation[];
17
17
  }
18
18
  export declare function checkMap(projectDir: string): Promise<MapCheckResult>;
19
+ export {};
@@ -11,4 +11,3 @@ export interface PracticeCheckOptions {
11
11
  }
12
12
  export declare function collectPracticeWarnings(projectDir: string, walkDir: (dir: string, re: RegExp) => string[], opts?: PracticeCheckOptions): PracticeWarning[];
13
13
  export declare function practiceKindLabel(kind: PracticeKind): string;
14
- export declare function formatPracticeWarnings(projectDir: string, warnings: PracticeWarning[]): string[];
@@ -193,16 +193,3 @@ export function practiceKindLabel(kind) {
193
193
  }
194
194
  }
195
195
  }
196
- export function formatPracticeWarnings(projectDir, warnings) {
197
- const lines = [];
198
- for (const w of warnings) {
199
- lines.push(`waygraph practices: ${w.file} (${practiceKindLabel(w.kind)}) — ${w.detail}`);
200
- }
201
- if (warnings.length === 0) {
202
- lines.push(`waygraph practices: no bad-practice patterns under ${projectDir}`);
203
- }
204
- else {
205
- lines.push(`waygraph practices: ${warnings.length} bad-practice warning${warnings.length === 1 ? "" : "s"}`);
206
- }
207
- return lines;
208
- }
@@ -16,7 +16,7 @@ export declare class AutoSession {
16
16
  private library;
17
17
  private readonly mem;
18
18
  private readonly engine;
19
- private readonly browser;
19
+ private readonly closeBrowser;
20
20
  private readonly context;
21
21
  private page;
22
22
  private readonly startUrl;
@@ -9,7 +9,8 @@ import { collectKnownInteractionSelectors, unmappedInteractionsPageScript } from
9
9
  import { runStubPhase } from "../highlights.js";
10
10
  import { verboseLog } from "./verbose-log.js";
11
11
  import { findBlockPathDetailed } from "../analysis/graph.js";
12
- import { chromiumExecutablePath } from "../browser-path.js";
12
+ import { configuredExtensions } from "../config.js";
13
+ import { chromiumExecutablePath, launchContext } from "../browser.js";
13
14
  /**
14
15
  * Non-interactive explore session: same graph/library/mem/browser setup
15
16
  * `runAutoExplore` runs in `--cli` mode, driven by `applyPick` instead of a
@@ -26,7 +27,7 @@ export class AutoSession {
26
27
  library;
27
28
  mem;
28
29
  engine;
29
- browser;
30
+ closeBrowser;
30
31
  context;
31
32
  page;
32
33
  startUrl;
@@ -38,7 +39,7 @@ export class AutoSession {
38
39
  knownInteractionSelectors;
39
40
  headless;
40
41
  lastOverlayInfo = null;
41
- constructor(projectDir, inject, blocksSelect, graph, library, mem, engine, browser, context, page, startUrl, sessionId, knownInteractionSelectors, headless) {
42
+ constructor(projectDir, inject, blocksSelect, graph, library, mem, engine, closeBrowser, context, page, startUrl, sessionId, knownInteractionSelectors, headless) {
42
43
  this.projectDir = projectDir;
43
44
  this.inject = inject;
44
45
  this.blocksSelect = blocksSelect;
@@ -46,7 +47,7 @@ export class AutoSession {
46
47
  this.library = library;
47
48
  this.mem = mem;
48
49
  this.engine = engine;
49
- this.browser = browser;
50
+ this.closeBrowser = closeBrowser;
50
51
  this.context = context;
51
52
  this.page = page;
52
53
  this.startUrl = startUrl;
@@ -85,13 +86,17 @@ export class AutoSession {
85
86
  };
86
87
  if (executablePath)
87
88
  launchOpts.executablePath = executablePath;
88
- const browser = await chromium.launch(launchOpts);
89
89
  const contextOpts = {
90
90
  viewport: headless ? { width: 1280, height: 720 } : null,
91
91
  };
92
92
  if (baseURL)
93
93
  contextOpts.baseURL = baseURL;
94
- const context = await browser.newContext(contextOpts);
94
+ const { close: closeBrowser, context } = await launchContext({
95
+ launcher: chromium,
96
+ launch: launchOpts,
97
+ context: contextOpts,
98
+ extensions: await configuredExtensions(init.projectDir),
99
+ });
95
100
  await installPersistentPilotOverlay(context);
96
101
  const page = await context.newPage();
97
102
  instrumentPilotActions(page, headless ? 0 : 450);
@@ -103,7 +108,7 @@ export class AutoSession {
103
108
  await page.goto("about:blank").catch(() => { });
104
109
  }
105
110
  const knownInteractionSelectors = collectKnownInteractionSelectors(init.projectDir, library, init.inject);
106
- const session = new AutoSession(init.projectDir, init.inject, init.blocksSelect, graph, library, mem, engine, browser, context, page, startUrl, init.sessionId, knownInteractionSelectors, headless);
111
+ const session = new AutoSession(init.projectDir, init.inject, init.blocksSelect, graph, library, mem, engine, closeBrowser, context, page, startUrl, init.sessionId, knownInteractionSelectors, headless);
107
112
  page.on("load", () => {
108
113
  void session.repaintOverlayAfterNavigation();
109
114
  });
@@ -709,6 +714,6 @@ export class AutoSession {
709
714
  }
710
715
  async close() {
711
716
  await this.context.close();
712
- await this.browser.close();
717
+ await this.closeBrowser();
713
718
  }
714
719
  }
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Shared `--blocks` select grammar (RFC Phase C).
3
+ *
4
+ * - `/pattern/` -> regex on relative file path OR block name
5
+ * - has `*`/`**` -> glob on relative file path (POSIX-ish)
6
+ * - otherwise -> bare token (flow export / checkpoint / block name) — caller decides
7
+ */
1
8
  export type BlocksSelectKind = "regex" | "glob" | "bare";
2
9
  export interface BlocksSelect {
3
10
  kind: BlocksSelectKind;
@@ -16,7 +23,3 @@ export declare function matchBlocksSelect(select: BlocksSelect, opts: {
16
23
  relFile: string;
17
24
  blockName?: string;
18
25
  }): boolean;
19
- export declare function filterByBlocksSelect<T extends {
20
- file: string;
21
- block?: string;
22
- }>(items: T[], select: BlocksSelect | undefined, projectDir: string): T[];
@@ -5,7 +5,6 @@
5
5
  * - has `*`/`**` -> glob on relative file path (POSIX-ish)
6
6
  * - otherwise -> bare token (flow export / checkpoint / block name) — caller decides
7
7
  */
8
- import { relative } from "node:path";
9
8
  /** True when the token is file-select (glob/regex), not a bare flow/checkpoint name. */
10
9
  export function isFileSelectToken(token) {
11
10
  if (!token)
@@ -70,16 +69,3 @@ export function matchBlocksSelect(select, opts) {
70
69
  return true;
71
70
  return false;
72
71
  }
73
- export function filterByBlocksSelect(items, select, projectDir) {
74
- if (!select)
75
- return items;
76
- return items.filter((it) => {
77
- const relFile = it.file.startsWith(projectDir)
78
- ? relative(projectDir, it.file)
79
- : it.file;
80
- return matchBlocksSelect(select, {
81
- relFile: relFile.replace(/\\/g, "/"),
82
- ...(it.block !== undefined ? { blockName: it.block } : {}),
83
- });
84
- });
85
- }
@@ -0,0 +1,27 @@
1
+ import type { Browser, BrowserContext, BrowserContextOptions, BrowserType, LaunchOptions } from "@playwright/test";
2
+ /**
3
+ * CHROME_PATH / CHROMIUM_PATH, if set and the file exists. A stale path (browser uninstalled)
4
+ * falls back to Playwright's bundled Chromium with a one-time warning instead of failing the run.
5
+ */
6
+ export declare function chromiumExecutablePath(): string | undefined;
7
+ /** Unpacked extension directories, as resolved by `resolveExtensions` - also what `WAYGRAPH_EXTENSIONS` carries to child processes. */
8
+ export declare function extensionPathsFromEnv(): string[];
9
+ export interface LaunchedContext {
10
+ /** Null for a persistent (extension) context. */
11
+ browser: Browser | null;
12
+ context: BrowserContext;
13
+ /** Closes the context and the browser (and the throwaway profile when extensions are loaded). */
14
+ close(): Promise<void>;
15
+ }
16
+ /**
17
+ * Launches a browser and its first context. With `extensions` it uses a persistent context, the only
18
+ * way Chromium loads extensions: new headless (`channel: "chromium"`; the default headless shell
19
+ * ignores them) and Playwright's own Chromium (branded Chrome 137+ ignores `--load-extension`).
20
+ * Extensions are not active in extra contexts made with `browser.newContext()`.
21
+ */
22
+ export declare function launchContext(opts: {
23
+ launcher: BrowserType;
24
+ launch: LaunchOptions;
25
+ context?: BrowserContextOptions;
26
+ extensions?: readonly string[];
27
+ }): Promise<LaunchedContext>;
@@ -0,0 +1,62 @@
1
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ let warned = false;
5
+ /**
6
+ * CHROME_PATH / CHROMIUM_PATH, if set and the file exists. A stale path (browser uninstalled)
7
+ * falls back to Playwright's bundled Chromium with a one-time warning instead of failing the run.
8
+ */
9
+ export function chromiumExecutablePath() {
10
+ const path = process.env.CHROME_PATH || process.env.CHROMIUM_PATH;
11
+ if (!path)
12
+ return undefined;
13
+ if (existsSync(path))
14
+ return path;
15
+ if (!warned) {
16
+ warned = true;
17
+ console.error(`waygraph: CHROME_PATH/CHROMIUM_PATH "${path}" does not exist - using Playwright's bundled Chromium`);
18
+ }
19
+ return undefined;
20
+ }
21
+ /** Unpacked extension directories, as resolved by `resolveExtensions` - also what `WAYGRAPH_EXTENSIONS` carries to child processes. */
22
+ export function extensionPathsFromEnv() {
23
+ return (process.env.WAYGRAPH_EXTENSIONS ?? "").split(",").filter(Boolean);
24
+ }
25
+ /**
26
+ * Launches a browser and its first context. With `extensions` it uses a persistent context, the only
27
+ * way Chromium loads extensions: new headless (`channel: "chromium"`; the default headless shell
28
+ * ignores them) and Playwright's own Chromium (branded Chrome 137+ ignores `--load-extension`).
29
+ * Extensions are not active in extra contexts made with `browser.newContext()`.
30
+ */
31
+ export async function launchContext(opts) {
32
+ const extensions = opts.extensions ?? [];
33
+ if (extensions.length === 0) {
34
+ const browser = await opts.launcher.launch(opts.launch);
35
+ const context = await browser.newContext(opts.context);
36
+ return { browser, context, close: () => browser.close() };
37
+ }
38
+ if (opts.launcher.name() !== "chromium")
39
+ throw new Error("waygraph: extensions only work in Chromium");
40
+ const list = extensions.join(",");
41
+ const { executablePath: _ignored, ...launch } = opts.launch;
42
+ const profile = mkdtempSync(join(tmpdir(), "waygraph-profile-"));
43
+ const context = await opts.launcher.launchPersistentContext(profile, {
44
+ ...launch,
45
+ ...opts.context,
46
+ ...(launch.headless === false ? {} : { channel: "chromium" }),
47
+ args: [...(launch.args ?? []), `--disable-extensions-except=${list}`, `--load-extension=${list}`],
48
+ });
49
+ // An extension's background worker registers its page hooks, so wait for it before the first navigation.
50
+ const hasWorker = extensions.some((dir) => JSON.parse(readFileSync(join(dir, "manifest.json"), "utf-8")).background?.service_worker !== undefined);
51
+ if (hasWorker && context.serviceWorkers().length === 0) {
52
+ await context.waitForEvent("serviceworker", { timeout: 10_000 }).catch(() => undefined);
53
+ }
54
+ return {
55
+ browser: context.browser(),
56
+ context,
57
+ close: async () => {
58
+ await context.close();
59
+ rmSync(profile, { recursive: true, force: true });
60
+ },
61
+ };
62
+ }
package/dist/cli/flags.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { basename, relative, resolve } from "node:path";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { walkDir } from "./discover.js";
5
- import { resolveBaseUrl } from "../config.js";
5
+ import { loadWaygraphConfig, resolveBaseUrl } from "../config.js";
6
6
  function takeFlagValue(argv, i, a, flag) {
7
7
  if (a.startsWith(flag + "=")) {
8
8
  return { value: a.slice(flag.length + 1), nextI: i };
@@ -323,6 +323,8 @@ export { resolveBaseUrl };
323
323
  * Call after applyRunFlags so explicit flags already won.
324
324
  */
325
325
  export async function applyDemoDefaults(projectDir) {
326
+ // Config first: its defaults (headed, autoNext, pace...) must land before the demo defaults below fill the same variables.
327
+ await loadWaygraphConfig(projectDir);
326
328
  if (process.env.WAYGRAPH_STEP === undefined)
327
329
  process.env.WAYGRAPH_STEP = "1";
328
330
  if (process.env.WAYGRAPH_STEP === "1" && process.env.WAYGRAPH_HEADED === undefined) {
@@ -1,4 +1,5 @@
1
1
  // Split out of the former 8,700-line cli.ts (see src/ARCHITECTURE.md). Behavior unchanged.
2
+ import { configuredExtensions } from "../config.js";
2
3
  import { expandSpecFlowFiles, resolveBaseUrl } from "./flags.js";
3
4
  import { join } from "node:path";
4
5
  import { rmSync, writeFileSync } from "node:fs";
@@ -45,6 +46,9 @@ export async function runChain(projectDir, spec) {
45
46
  if (resolved)
46
47
  process.env.WAYGRAPH_BASE_URL = resolved;
47
48
  }
49
+ const extensions = await configuredExtensions(projectDir);
50
+ if (extensions.length > 0)
51
+ process.env.WAYGRAPH_EXTENSIONS = extensions.join(",");
48
52
  const tsxEsm = import.meta.resolve("tsx/esm");
49
53
  const scriptPath = join(projectDir, `.waygraph-chain-${process.pid}.mjs`);
50
54
  writeFileSync(scriptPath, CHAIN_RUNNER_BOOTSTRAP);
package/dist/cli/usage.js CHANGED
@@ -156,7 +156,9 @@ Primary (less is more):
156
156
  --ff-disabled Dispute: expand FF (alias --no-ff); blitz kept on those inners
157
157
  --auto-play-video Unattended + recorded: --auto-next + --video (+ step); headless
158
158
  --auto-play-video-head Same, but keep the browser visible (--non-headless)
159
- --video [dir] Record .webm (demo: headless unless --non-headless)
159
+ --video [dir] Record .webm (demo: headless unless --non-headless);
160
+ default folder ~/Downloads/waygraph-videos/<project>,
161
+ or video.dir in the config / WAYGRAPH_VIDEO_DIR
160
162
  --video-viewport WxH Recording size (default demo: 1920x1080)
161
163
  --title / --base-url
162
164
  waygraph run [--blocks <flow|file|spec>] Execute (no overlay unless --step)
@@ -184,6 +186,20 @@ Primary (less is more):
184
186
  has a trace - test report opens it.
185
187
 
186
188
  Also:
189
+ --config <name|file> (any command) use another config instead of waygraph.config.ts, e.g.
190
+ waygraph test --config staging (config/waygraph.staging.config.ts)
191
+ --env <mode> (any command) load .env.<mode> / .env.<mode>.local on top of .env, e.g.
192
+ waygraph test --env staging (default: NODE_ENV, else development)
193
+ waygraph upgrade [project] [paths...] [--apply] [--overwrite] [--diff] [--skills]
194
+ Compare a project with the scaffold in this waygraph:
195
+ lists new and differing files and a stale waygraph
196
+ dependency. Dry run unless --apply; only the paths you
197
+ name are copied, differing files only with --overwrite.
198
+ --skills refreshes .agents/skills to this waygraph's.
199
+ waygraph extensions install [project] [--update]
200
+ Download the browser extensions listed in
201
+ waygraph.config.* (extensions: ["hoppscotch"]) into
202
+ the user cache; runs also fetch them on first use.
187
203
  waygraph list | nav | validate | check | typecheck | graph | init <name>
188
204
  check/typecheck --no-practices Skip bad-practice warnings
189
205
  waygraph map [project] Waygraph Map convention enforcement: every static
package/dist/cli.js CHANGED
@@ -15,6 +15,8 @@
15
15
  * "project" defaults to cwd. Flags beat WAYGRAPH_* env.
16
16
  */
17
17
  import { SKILL_FLAG_MAP, printSkillIndex, readSkillMarkdown } from "./commands/agent-dive-scaffold.js";
18
+ import { takeEnvFlag } from "./env-file.js";
19
+ import { takeConfigFlag } from "./config.js";
18
20
  import { usage } from "./cli/usage.js";
19
21
  import { listCase, navCase } from "./commands/list.js";
20
22
  import { checkCase, typecheckCase, validateCase } from "./commands/check.js";
@@ -25,9 +27,13 @@ import { traverseCase } from "./commands/traverse.js";
25
27
  import { agentDiveCase } from "./commands/agent-dive.js";
26
28
  import { autoServeCase, sessionCase } from "./commands/session.js";
27
29
  import { tryCase } from "./commands/try.js";
30
+ import { upgradeCase } from "./commands/upgrade.js";
31
+ import { extensionsCase } from "./commands/extensions.js";
28
32
  import { mapCase } from "./commands/map.js";
29
- const args = process.argv.slice(2);
33
+ const args = takeConfigFlag(takeEnvFlag(process.argv.slice(2)));
30
34
  const command = args[0];
35
+ if (command)
36
+ process.env.WAYGRAPH_COMMAND = command;
31
37
  async function main() {
32
38
  if (!command || command === "--help" || command === "-h") {
33
39
  usage();
@@ -97,6 +103,12 @@ async function main() {
97
103
  case "map":
98
104
  await mapCase(args);
99
105
  break;
106
+ case "upgrade":
107
+ await upgradeCase(args);
108
+ break;
109
+ case "extensions":
110
+ await extensionsCase(args);
111
+ break;
100
112
  default:
101
113
  console.error(`waygraph: unknown command "${command}"`);
102
114
  process.exit(1);
@@ -1,24 +1,4 @@
1
1
  export type AgentDiveLoop = "claude" | "opencode" | "cursor" | "vscode";
2
- export interface AgentSpec {
3
- name: string;
4
- description: string;
5
- model?: string;
6
- color?: string;
7
- tools: string[];
8
- instructions: string;
9
- }
10
- /** Minimal YAML-ish frontmatter parser for `*.agent.md` templates. */
11
- export declare function parseAgentSpec(filePath: string): AgentSpec;
12
- export declare function loadAgentSpecs(): AgentSpec[];
13
- /**
14
- * Claude Code Skills (`.claude/skills/<name>/SKILL.md`) - a different
15
- * mechanism than the agent persona files above: loadable instructions
16
- * triggered by task shape, not a persona. Claude-Code-specific (no
17
- * equivalent in the opencode/cursor/vscode loops), additive to the
18
- * `claude` loop only. Reuses the same `name: .../description: ...` +
19
- * body frontmatter shape `parseAgentSpec` already parses.
20
- */
21
- export declare function loadSkillSpecs(): AgentSpec[];
22
2
  /** CLI flag -> packaged skill file stem (under templates/skills/). */
23
3
  export declare const SKILL_FLAG_MAP: {
24
4
  readonly "--skill-pilot": "waygraph-pilot";
@@ -28,6 +8,11 @@ export declare const SKILL_FLAG_MAP: {
28
8
  export type SkillFlag = keyof typeof SKILL_FLAG_MAP;
29
9
  /** Raw skill markdown (frontmatter + body), as shipped in the package. */
30
10
  export declare function readSkillMarkdown(stem: string): string;
11
+ /** Every packaged skill as `.agents/skills/<name>/SKILL.md` (relative path + markdown), the cross-agent skills folder. */
12
+ export declare function agentSkillFiles(): {
13
+ rel: string;
14
+ text: string;
15
+ }[];
31
16
  /** Print `--skill` index (available flags + one-line descriptions). */
32
17
  export declare function printSkillIndex(): void;
33
18
  export interface AgentDiveOptions {
@@ -16,7 +16,7 @@ function skillsTemplateDir() {
16
16
  return join(packageRoot(), "templates", "skills");
17
17
  }
18
18
  /** Minimal YAML-ish frontmatter parser for `*.agent.md` templates. */
19
- export function parseAgentSpec(filePath) {
19
+ function parseAgentSpec(filePath) {
20
20
  const raw = readFileSync(filePath, "utf-8");
21
21
  if (!raw.startsWith("---\n")) {
22
22
  throw new Error(`agent-dive: missing frontmatter in ${filePath}`);
@@ -58,7 +58,7 @@ export function parseAgentSpec(filePath) {
58
58
  instructions: body,
59
59
  };
60
60
  }
61
- export function loadAgentSpecs() {
61
+ function loadAgentSpecs() {
62
62
  const dir = agentsTemplateDir();
63
63
  if (!existsSync(dir)) {
64
64
  throw new Error(`agent-dive: templates missing at ${dir}`);
@@ -76,7 +76,7 @@ export function loadAgentSpecs() {
76
76
  * `claude` loop only. Reuses the same `name: .../description: ...` +
77
77
  * body frontmatter shape `parseAgentSpec` already parses.
78
78
  */
79
- export function loadSkillSpecs() {
79
+ function loadSkillSpecs() {
80
80
  const dir = skillsTemplateDir();
81
81
  if (!existsSync(dir))
82
82
  return [];
@@ -100,6 +100,13 @@ export function readSkillMarkdown(stem) {
100
100
  const raw = readFileSync(path, "utf-8");
101
101
  return raw.endsWith("\n") ? raw : raw + "\n";
102
102
  }
103
+ /** Every packaged skill as `.agents/skills/<name>/SKILL.md` (relative path + markdown), the cross-agent skills folder. */
104
+ export function agentSkillFiles() {
105
+ return Object.values(SKILL_FLAG_MAP).map((stem) => ({
106
+ rel: join(".agents", "skills", stem, "SKILL.md"),
107
+ text: readSkillMarkdown(stem),
108
+ }));
109
+ }
103
110
  /** Print `--skill` index (available flags + one-line descriptions). */
104
111
  export function printSkillIndex() {
105
112
  const skills = loadSkillSpecs();
@@ -0,0 +1 @@
1
+ export declare function extensionsCase(args: string[]): Promise<void>;
@@ -0,0 +1,22 @@
1
+ import { resolve } from "node:path";
2
+ import { loadWaygraphConfig } from "../config.js";
3
+ import { extensionInfo, resolveExtension } from "../extensions.js";
4
+ export async function extensionsCase(args) {
5
+ const update = args.includes("--update");
6
+ const rest = args.slice(2).filter((a) => a !== "--update");
7
+ if (args[1] !== "install") {
8
+ console.error("usage: waygraph extensions install [project] [--update]");
9
+ process.exit(1);
10
+ }
11
+ const proj = resolve(rest[0] ?? process.cwd());
12
+ const specs = (await loadWaygraphConfig(proj))?.extensions ?? [];
13
+ if (specs.length === 0) {
14
+ console.log("waygraph extensions: no `extensions` in waygraph.config.* - nothing to install");
15
+ return;
16
+ }
17
+ for (const spec of specs) {
18
+ const dir = await resolveExtension(spec, proj, { update });
19
+ const { name, version } = extensionInfo(dir);
20
+ console.log(`waygraph extensions: ${name} ${version} -> ${dir}`);
21
+ }
22
+ }
@@ -1,8 +1,34 @@
1
1
  // Split out of the former 8,700-line cli.ts (see src/ARCHITECTURE.md). Behavior unchanged.
2
2
  import { packageRoot } from "../cli/util.js";
3
- import { basename, join, relative, resolve } from "node:path";
3
+ import { agentSkillFiles } from "./agent-dive-scaffold.js";
4
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
5
+ import { spawnSync } from "node:child_process";
4
6
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
5
7
  import { pathToFileURL } from "node:url";
8
+ /**
9
+ * Running from a checkout (not an installed package), npm doesn't have this build's code, so a plain `^version` would install an
10
+ * older waygraph and break the scaffold's types. Pack this checkout into the project and depend on that file instead.
11
+ */
12
+ function packLocalBuild(targetDir) {
13
+ const root = packageRoot();
14
+ if (root.split(sep).includes("node_modules"))
15
+ return undefined;
16
+ const dir = join(targetDir, ".waygraph-local");
17
+ mkdirSync(dir, { recursive: true });
18
+ const packed = spawnSync("npm", ["pack", "--pack-destination", dir, "--silent"], {
19
+ cwd: root,
20
+ encoding: "utf-8",
21
+ shell: process.platform === "win32",
22
+ });
23
+ const file = packed.stdout?.trim().split("\n").pop();
24
+ if (packed.status !== 0 || !file) {
25
+ console.error("waygraph init: could not pack this checkout; the project will depend on the published waygraph");
26
+ rmSync(dir, { recursive: true, force: true });
27
+ return undefined;
28
+ }
29
+ writeFileSync(join(targetDir, ".gitignore"), readFileSync(join(targetDir, ".gitignore"), "utf-8") + ".waygraph-local/\n");
30
+ return `file:.waygraph-local/${file}`;
31
+ }
6
32
  function initCommand(projectName) {
7
33
  if (!projectName) {
8
34
  console.error("waygraph init: missing <project-name>, e.g. waygraph init my-app");
@@ -34,9 +60,20 @@ function initCommand(projectName) {
34
60
  // Demo pages open via file://; the absolute path is only known once copied, so it's filled in here.
35
61
  const appBaseUrl = pathToFileURL(join(targetDir, "src", "map", "(app_base)", "index.html")).href;
36
62
  const packageJsonPath = join(targetDir, "package.json");
37
- writeFileSync(packageJsonPath, readFileSync(packageJsonPath, "utf-8").replaceAll("__PROJECT_NAME__", pkgName));
38
- const playwrightConfigPath = join(targetDir, "playwright.config.ts");
39
- writeFileSync(playwrightConfigPath, readFileSync(playwrightConfigPath, "utf-8").replaceAll("__APP_BASE_URL__", appBaseUrl));
63
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8").replaceAll("__PROJECT_NAME__", pkgName));
64
+ // Pin the project to the waygraph that created it - a stale range here installs an old engine.
65
+ const ownVersion = JSON.parse(readFileSync(join(packageRoot(), "package.json"), "utf-8")).version;
66
+ const local = packLocalBuild(targetDir);
67
+ pkg.dependencies.waygraph = local ?? `^${ownVersion}`;
68
+ if (local)
69
+ console.log(`(running from a waygraph checkout: depending on its local build, ${local})`);
70
+ writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + "\n");
71
+ const configPath = join(targetDir, "config", "waygraph.config.ts");
72
+ writeFileSync(configPath, readFileSync(configPath, "utf-8").replaceAll("__APP_BASE_URL__", appBaseUrl));
73
+ for (const { rel, text } of agentSkillFiles()) {
74
+ mkdirSync(dirname(join(targetDir, rel)), { recursive: true });
75
+ writeFileSync(join(targetDir, rel), text);
76
+ }
40
77
  console.log(`Scaffolded ${pkgName}/`);
41
78
  console.log("");
42
79
  console.log(` cd ${relative(process.cwd(), targetDir) || pkgName}`);
@@ -48,8 +85,9 @@ function initCommand(projectName) {
48
85
  console.log(" waygraph typecheck # tsc --noEmit + bad-practice warnings (use --no-practices to skip)");
49
86
  console.log(" waygraph auto # interactive explore (headed panel)");
50
87
  console.log(" waygraph auto --cli # same menus in the terminal");
51
- console.log(" waygraph demo --blocks exampleFlow");
88
+ console.log(" waygraph demo --blocks homeHelloFlow");
52
89
  console.log(" waygraph graph # static state graph JSON");
90
+ console.log(" .agents/skills/ # waygraph skills for coding agents (waygraph agent-dive adds agent personas)");
53
91
  console.log("");
54
92
  console.log(" Layout: see STRUCTURE.md (or https://deviate-dv8.github.io/waygraph/scaffold.html)");
55
93
  }
@@ -1,7 +1,9 @@
1
1
  // Split out of the former 8,700-line cli.ts (see src/ARCHITECTURE.md). Behavior unchanged.
2
+ import { configuredExtensions } from "../config.js";
2
3
  import { runInherited } from "../cli/util.js";
3
4
  import { join } from "node:path";
4
5
  import { existsSync } from "node:fs";
6
+ import { packageRoot } from "../cli/util.js";
5
7
  export async function testCase(args) {
6
8
  const rest = args.slice(1);
7
9
  const proj = process.cwd();
@@ -23,11 +25,23 @@ export async function testCase(args) {
23
25
  process.exitCode = await runPw(["show-trace", ...rest.slice(1)]);
24
26
  return;
25
27
  }
28
+ const extensions = await configuredExtensions(proj);
29
+ if (extensions.length > 0)
30
+ process.env.WAYGRAPH_EXTENSIONS = extensions.join(",");
31
+ // No playwright.config in the project: waygraph builds one from config/waygraph.config.ts.
32
+ const hasOwnConfig = ["ts", "mts", "js", "mjs", "cjs"].some((e) => existsSync(join(proj, `playwright.config.${e}`)));
33
+ if (hasOwnConfig) {
34
+ console.error("waygraph: playwright.config is deprecated - move its settings into config/waygraph.config.ts");
35
+ }
36
+ else {
37
+ process.env.WAYGRAPH_PROJECT_DIR = proj;
38
+ }
39
+ const configArgs = hasOwnConfig ? [] : ["-c", join(packageRoot(), "dist", "playwright.config.js")];
26
40
  const wantsUi = rest[0] === "ui" || rest.includes("--ui");
27
41
  const forwarded = rest[0] === "ui" ? rest.slice(1) : rest;
28
42
  const pwArgs = wantsUi && !forwarded.includes("--ui")
29
- ? ["test", "--ui", ...forwarded]
30
- : ["test", ...forwarded];
43
+ ? ["test", ...configArgs, "--ui", ...forwarded]
44
+ : ["test", ...configArgs, ...forwarded];
31
45
  process.exitCode = await runPw(pwArgs);
32
46
  return;
33
47
  }