waygraph 0.15.51 → 0.15.52

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 (130) hide show
  1. package/dist/analysis/graph.d.ts +1 -1
  2. package/dist/analysis/graph.js +10 -15
  3. package/dist/analysis/practices-check.d.ts +0 -1
  4. package/dist/analysis/practices-check.js +0 -13
  5. package/dist/auto-session/session.d.ts +1 -1
  6. package/dist/auto-session/session.js +13 -8
  7. package/dist/blocks-select.d.ts +7 -4
  8. package/dist/blocks-select.js +0 -14
  9. package/dist/browser.d.ts +27 -0
  10. package/dist/browser.js +62 -0
  11. package/dist/cli/run-chain.js +4 -0
  12. package/dist/cli/usage.js +14 -1
  13. package/dist/cli.js +10 -1
  14. package/dist/commands/agent-dive-scaffold.d.ts +5 -0
  15. package/dist/commands/agent-dive-scaffold.js +7 -0
  16. package/dist/commands/extensions.d.ts +1 -0
  17. package/dist/commands/extensions.js +22 -0
  18. package/dist/commands/init.js +42 -4
  19. package/dist/commands/test.js +16 -2
  20. package/dist/commands/try.js +2 -1
  21. package/dist/commands/upgrade.d.ts +6 -0
  22. package/dist/commands/upgrade.js +107 -0
  23. package/dist/config.d.ts +47 -3
  24. package/dist/config.js +45 -12
  25. package/dist/core/block.d.ts +12 -0
  26. package/dist/core/block.js +36 -0
  27. package/dist/core/blocks/nav.d.ts +11 -0
  28. package/dist/core/blocks/nav.js +8 -0
  29. package/dist/core/config.d.ts +5 -0
  30. package/dist/core/engine.d.ts +17 -16
  31. package/dist/core/engine.js +3 -3
  32. package/dist/core/flow.d.ts +17 -0
  33. package/dist/core/flow.js +44 -21
  34. package/dist/core/map-builder.d.ts +3 -3
  35. package/dist/core/map-builder.js +3 -3
  36. package/dist/env-file.d.ts +7 -7
  37. package/dist/env-file.js +29 -16
  38. package/dist/extensions.d.ts +21 -0
  39. package/dist/extensions.js +178 -0
  40. package/dist/index.d.ts +6 -4
  41. package/dist/index.js +3 -2
  42. package/dist/pilot-overlay/overlay-fixtures.d.ts +0 -7
  43. package/dist/pilot-overlay/overlay-fixtures.js +0 -45
  44. package/dist/playwright-config.d.ts +4 -0
  45. package/dist/playwright-config.js +15 -0
  46. package/dist/playwright.config.d.ts +2 -0
  47. package/dist/playwright.config.js +6 -0
  48. package/dist/runner/demo-log.js +1 -7
  49. package/dist/runner/inpage/core.d.ts +1 -1
  50. package/dist/runner/inpage/core.js +8 -7
  51. package/dist/runner/main.js +18 -10
  52. package/dist/runner/rings.js +0 -49
  53. package/dist/session/auto-explore-run.js +9 -5
  54. package/dist/session/auto-explore.js +6 -4
  55. package/dist/session/step-overlay.js +3 -1
  56. package/dist/traverse/run.js +3 -1
  57. package/dist/ui/components/ring.js +1 -0
  58. package/dist/ui/overlay.css +1 -0
  59. package/dist/ui/shadow.d.ts +1 -3
  60. package/dist/ui/shadow.js +0 -4
  61. package/dist/ui/tokens.d.ts +0 -11
  62. package/dist/ui/tokens.js +0 -5
  63. package/dist/video-dir.d.ts +8 -0
  64. package/dist/video-dir.js +60 -0
  65. package/dist/webserver.js +5 -1
  66. package/package.json +1 -1
  67. package/templates/scaffold/README.md +4 -2
  68. package/templates/scaffold/STRUCTURE.md +5 -2
  69. package/templates/scaffold/config/waygraph.config.ts +26 -0
  70. package/templates/scaffold/package.json +1 -1
  71. package/templates/scaffold/src/flows/README.md +5 -5
  72. package/templates/scaffold/src/flows/example.flow.ts +31 -17
  73. package/templates/scaffold/src/flows/hoppscotch-auth.flow.ts +35 -0
  74. package/templates/scaffold/src/flows/hoppscotch-get.flow.ts +23 -0
  75. package/templates/scaffold/src/flows/hoppscotch-post.flow.ts +33 -0
  76. package/templates/scaffold/src/flows/mail-verify.flow.ts +36 -75
  77. package/templates/scaffold/src/flows/shop.flow.ts +41 -65
  78. package/templates/scaffold/src/map/(app_base)/docs/_nav.block.ts +0 -9
  79. package/templates/scaffold/src/map/(app_base)/docs/_page.block.ts +0 -5
  80. package/templates/scaffold/src/map/(app_base)/docs/_sel.ts +1 -1
  81. package/templates/scaffold/src/map/(app_base)/home/_methods/add-item.effect.block.ts +3 -19
  82. package/templates/scaffold/src/map/(app_base)/home/_methods/assert-hello.method.block.ts +2 -25
  83. package/templates/scaffold/src/map/(app_base)/home/_methods/assert-item-added.method.block.ts +2 -19
  84. package/templates/scaffold/src/map/(app_base)/home/_methods/clear-cart.method.block.ts +1 -17
  85. package/templates/scaffold/src/map/(app_base)/home/_methods/remove-item.effect.block.ts +1 -10
  86. package/templates/scaffold/src/map/(app_base)/home/_nav.block.ts +1 -12
  87. package/templates/scaffold/src/map/(app_base)/home/_page.block.ts +0 -9
  88. package/templates/scaffold/src/map/(app_base)/home/_sel.ts +3 -1
  89. package/templates/scaffold/src/map/(app_base)/home/nav-verification-link.block.ts +1 -23
  90. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/assert-response-body.method.block.ts +21 -0
  91. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/assert-response-header.method.block.ts +19 -0
  92. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/assert-status.method.block.ts +20 -0
  93. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/pick-extension-interceptor.method.block.ts +18 -0
  94. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/read-response-body.method.block.ts +21 -0
  95. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/read-response-headers.method.block.ts +21 -0
  96. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/send-request.method.block.ts +15 -0
  97. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-bearer-token.method.block.ts +20 -0
  98. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-headers.method.block.ts +23 -0
  99. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-json-body.method.block.ts +36 -0
  100. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-method.method.block.ts +17 -0
  101. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-query-params.method.block.ts +22 -0
  102. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-url.method.block.ts +18 -0
  103. package/templates/scaffold/src/map/(external)/hoppscotch/_nav.block.ts +11 -0
  104. package/templates/scaffold/src/map/(external)/hoppscotch/_page.block.ts +36 -0
  105. package/templates/scaffold/src/map/(external)/hoppscotch/_sel.ts +27 -0
  106. package/templates/scaffold/src/map/(external)/hoppscotch/settings/_nav.block.ts +10 -0
  107. package/templates/scaffold/src/map/(external)/mailpit/_methods/assert-email-content.method.block.ts +2 -27
  108. package/templates/scaffold/src/map/(external)/mailpit/_methods/assert-email-received.method.block.ts +2 -21
  109. package/templates/scaffold/src/map/(external)/mailpit/_methods/extract-email-link.method.block.ts +1 -19
  110. package/templates/scaffold/src/map/(external)/mailpit/_methods/open-message.method.block.ts +1 -11
  111. package/templates/scaffold/src/map/(external)/mailpit/_nav.block.ts +1 -9
  112. package/templates/scaffold/src/map/(external)/mailpit/_page.block.ts +0 -11
  113. package/templates/scaffold/src/states/demo.mem-keys.ts +20 -19
  114. package/templates/scaffold/src/states/demo.mem-stubs.ts +4 -0
  115. package/templates/scaffold/src/states/demo.states.ts +5 -1
  116. package/templates/scaffold/tests/example.spec.ts +2 -3
  117. package/templates/scaffold/tests/hoppscotch.spec.ts +97 -0
  118. package/templates/scaffold/tests/mail-verify.spec.ts +2 -11
  119. package/templates/scaffold/tsconfig.json +1 -1
  120. package/templates/skills/waygraph-convention.skill.md +32 -3
  121. package/templates/skills/waygraph-pilot.skill.md +4 -5
  122. package/dist/browser-path.d.ts +0 -5
  123. package/dist/browser-path.js +0 -18
  124. package/dist/runner/overlay-present.d.ts +0 -8
  125. package/dist/runner/overlay-present.js +0 -57
  126. package/dist/ui/components/banner.d.ts +0 -2
  127. package/dist/ui/components/banner.js +0 -10
  128. package/dist/ui/components/cursor.d.ts +0 -2
  129. package/dist/ui/components/cursor.js +0 -19
  130. package/templates/scaffold/playwright.config.ts +0 -12
@@ -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 {
@@ -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
+ }
@@ -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,17 @@ Primary (less is more):
184
186
  has a trace - test report opens it.
185
187
 
186
188
  Also:
189
+ --env <mode> (any command) load .env.<mode> / .env.<mode>.local on top of .env, e.g.
190
+ waygraph test --env staging (default: NODE_ENV, else development)
191
+ waygraph upgrade [project] [paths...] [--apply] [--overwrite] [--diff]
192
+ Compare a project with the scaffold in this waygraph:
193
+ lists new and differing files and a stale waygraph
194
+ dependency. Dry run unless --apply; only the paths you
195
+ name are copied, differing files only with --overwrite.
196
+ waygraph extensions install [project] [--update]
197
+ Download the browser extensions listed in
198
+ waygraph.config.* (extensions: ["hoppscotch"]) into
199
+ the user cache; runs also fetch them on first use.
187
200
  waygraph list | nav | validate | check | typecheck | graph | init <name>
188
201
  check/typecheck --no-practices Skip bad-practice warnings
189
202
  waygraph map [project] Waygraph Map convention enforcement: every static
package/dist/cli.js CHANGED
@@ -15,6 +15,7 @@
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";
18
19
  import { usage } from "./cli/usage.js";
19
20
  import { listCase, navCase } from "./commands/list.js";
20
21
  import { checkCase, typecheckCase, validateCase } from "./commands/check.js";
@@ -25,8 +26,10 @@ import { traverseCase } from "./commands/traverse.js";
25
26
  import { agentDiveCase } from "./commands/agent-dive.js";
26
27
  import { autoServeCase, sessionCase } from "./commands/session.js";
27
28
  import { tryCase } from "./commands/try.js";
29
+ import { upgradeCase } from "./commands/upgrade.js";
30
+ import { extensionsCase } from "./commands/extensions.js";
28
31
  import { mapCase } from "./commands/map.js";
29
- const args = process.argv.slice(2);
32
+ const args = takeEnvFlag(process.argv.slice(2));
30
33
  const command = args[0];
31
34
  async function main() {
32
35
  if (!command || command === "--help" || command === "-h") {
@@ -97,6 +100,12 @@ async function main() {
97
100
  case "map":
98
101
  await mapCase(args);
99
102
  break;
103
+ case "upgrade":
104
+ await upgradeCase(args);
105
+ break;
106
+ case "extensions":
107
+ await extensionsCase(args);
108
+ break;
100
109
  default:
101
110
  console.error(`waygraph: unknown command "${command}"`);
102
111
  process.exit(1);
@@ -28,6 +28,11 @@ export declare const SKILL_FLAG_MAP: {
28
28
  export type SkillFlag = keyof typeof SKILL_FLAG_MAP;
29
29
  /** Raw skill markdown (frontmatter + body), as shipped in the package. */
30
30
  export declare function readSkillMarkdown(stem: string): string;
31
+ /** Every packaged skill as `.agents/skills/<name>/SKILL.md` (relative path + markdown), the cross-agent skills folder. */
32
+ export declare function agentSkillFiles(): {
33
+ rel: string;
34
+ text: string;
35
+ }[];
31
36
  /** Print `--skill` index (available flags + one-line descriptions). */
32
37
  export declare function printSkillIndex(): void;
33
38
  export interface AgentDiveOptions {
@@ -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}`);
@@ -50,6 +87,7 @@ function initCommand(projectName) {
50
87
  console.log(" waygraph auto --cli # same menus in the terminal");
51
88
  console.log(" waygraph demo --blocks exampleFlow");
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
  }
@@ -6,6 +6,7 @@ import { packageRoot, runInherited } from "../cli/util.js";
6
6
  import { tmpdir } from "node:os";
7
7
  import { runChain } from "../cli/run-chain.js";
8
8
  import { applyRunFlags, parseRunFlags } from "../cli/flags.js";
9
+ import { resolveVideoDir } from "../video-dir.js";
9
10
  /**
10
11
  * Runs a throwaway probe script rooted at `projectDir` - the exact same
11
12
  * directory the real demo files get copied into, so it resolves "waygraph"
@@ -196,7 +197,7 @@ async function runTryDemo() {
196
197
  const videoTo = process.env.WAYGRAPH_VIDEO;
197
198
  console.log(videoTo
198
199
  ? "waygraph try demo: step-through + record (Sign In -> Shop & Checkout -> blocked Viewer login) - click Next; video -> " +
199
- (videoTo === "1" ? ".waygraph-videos/" : videoTo)
200
+ (videoTo === "1" ? await resolveVideoDir(destDir) : videoTo)
200
201
  : "waygraph try demo: step-through (Sign In -> Shop & Checkout -> blocked Viewer login) - click Next for each step ...");
201
202
  await runChain(destDir, TRY_DEMO_CHAIN);
202
203
  if (process.exitCode)
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `waygraph upgrade [project] [paths...] [--apply] [--overwrite] [--diff]`
3
+ * Compares the project with the scaffold shipped in this waygraph. Dry run by default.
4
+ * Only the paths you name are copied, because a real project deliberately drops parts of the demo.
5
+ */
6
+ export declare function upgradeCase(args: string[]): Promise<void>;