@ait-co/devtools 0.1.106 → 0.1.108

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 (91) hide show
  1. package/dist/bundle-BJm5jk56.d.ts +49 -0
  2. package/dist/bundle-BJm5jk56.d.ts.map +1 -0
  3. package/dist/cdp-connection-C0AP0tH2.d.ts +277 -0
  4. package/dist/cdp-connection-C0AP0tH2.d.ts.map +1 -0
  5. package/dist/in-app/auto.d.ts +17 -0
  6. package/dist/in-app/auto.d.ts.map +1 -1
  7. package/dist/in-app/auto.js +76 -0
  8. package/dist/in-app/auto.js.map +1 -1
  9. package/dist/in-app/index.d.ts +48 -1
  10. package/dist/in-app/index.d.ts.map +1 -1
  11. package/dist/in-app/index.js +60 -1
  12. package/dist/in-app/index.js.map +1 -1
  13. package/dist/mcp/cli.js +507 -11
  14. package/dist/mcp/cli.js.map +1 -1
  15. package/dist/mcp/server.d.ts.map +1 -1
  16. package/dist/mcp/server.js +60 -3
  17. package/dist/mcp/server.js.map +1 -1
  18. package/dist/panel/index.js +1 -1
  19. package/dist/pool-Dkp7I9Bf.d.ts +14577 -0
  20. package/dist/pool-Dkp7I9Bf.d.ts.map +1 -0
  21. package/dist/{relay-secret-store-I5q2Wvvv.cjs → relay-secret-store-CPBBlV3J.cjs} +2 -2
  22. package/dist/{relay-secret-store-I5q2Wvvv.cjs.map → relay-secret-store-CPBBlV3J.cjs.map} +1 -1
  23. package/dist/{relay-secret-store-Bns5rndt.js → relay-secret-store-DBwzoCXQ.js} +2 -2
  24. package/dist/{relay-secret-store-Bns5rndt.js.map → relay-secret-store-DBwzoCXQ.js.map} +1 -1
  25. package/dist/{relay-secret-store-B0DH-8Qb.js → relay-secret-store-DhzAnnj-.js} +2 -2
  26. package/dist/{relay-secret-store-B0DH-8Qb.js.map → relay-secret-store-DhzAnnj-.js.map} +1 -1
  27. package/dist/{relay-url-store-CvmnevcO.cjs → relay-url-store-C9QLhB2p.cjs} +2 -2
  28. package/dist/{relay-url-store-CvmnevcO.cjs.map → relay-url-store-C9QLhB2p.cjs.map} +1 -1
  29. package/dist/{relay-url-store-BPeUZsiY.js → relay-url-store-CwKT7i04.js} +2 -2
  30. package/dist/{relay-url-store-BPeUZsiY.js.map → relay-url-store-CwKT7i04.js.map} +1 -1
  31. package/dist/{relay-url-store-DJHZjk8o.js → relay-url-store-j16TRTiJ.js} +2 -2
  32. package/dist/{relay-url-store-DJHZjk8o.js.map → relay-url-store-j16TRTiJ.js.map} +1 -1
  33. package/dist/relay-worker-BzFQ3fv9.d.ts +74 -0
  34. package/dist/relay-worker-BzFQ3fv9.d.ts.map +1 -0
  35. package/dist/runtime-ORdrpizY.d.ts +50 -0
  36. package/dist/runtime-ORdrpizY.d.ts.map +1 -0
  37. package/dist/test-runner/bundle.d.ts +2 -0
  38. package/dist/test-runner/bundle.js +95 -0
  39. package/dist/test-runner/bundle.js.map +1 -0
  40. package/dist/test-runner/cli.d.ts +417 -0
  41. package/dist/test-runner/cli.d.ts.map +1 -0
  42. package/dist/test-runner/cli.js +377 -0
  43. package/dist/test-runner/cli.js.map +1 -0
  44. package/dist/test-runner/config.d.ts +80 -0
  45. package/dist/test-runner/config.d.ts.map +1 -0
  46. package/dist/test-runner/config.js +54 -0
  47. package/dist/test-runner/config.js.map +1 -0
  48. package/dist/test-runner/pool.d.ts +2 -0
  49. package/dist/test-runner/pool.js +136 -0
  50. package/dist/test-runner/pool.js.map +1 -0
  51. package/dist/test-runner/relay-worker.d.ts +2 -0
  52. package/dist/test-runner/relay-worker.js +96 -0
  53. package/dist/test-runner/relay-worker.js.map +1 -0
  54. package/dist/test-runner/rpc.d.ts +53 -0
  55. package/dist/test-runner/rpc.d.ts.map +1 -0
  56. package/dist/test-runner/rpc.js +78 -0
  57. package/dist/test-runner/rpc.js.map +1 -0
  58. package/dist/test-runner/task-graph.d.ts +38 -0
  59. package/dist/test-runner/task-graph.d.ts.map +1 -0
  60. package/dist/test-runner/task-graph.js +182 -0
  61. package/dist/test-runner/task-graph.js.map +1 -0
  62. package/dist/{totp-BmKSPb5d.js → totp-95OAa20j.js} +2 -2
  63. package/dist/totp-95OAa20j.js.map +1 -0
  64. package/dist/{totp-BwDZ6dUT.cjs → totp-BjtoQNfu.cjs} +2 -2
  65. package/dist/totp-BjtoQNfu.cjs.map +1 -0
  66. package/dist/totp-D1pulXLa.js +3 -0
  67. package/dist/{totp-DYdP9N3o.js → totp-DIbrZtI7.js} +2 -2
  68. package/dist/totp-DIbrZtI7.js.map +1 -0
  69. package/dist/{totp-CNw0w89F.cjs → totp-Df252ZdA.cjs} +2 -2
  70. package/dist/totp-Df252ZdA.cjs.map +1 -0
  71. package/dist/{totp-Xq3ACwkm.js → totp-WY6l0ysP.js} +2 -2
  72. package/dist/totp-WY6l0ysP.js.map +1 -0
  73. package/dist/{tunnel-BmDcTrnU.js → tunnel-BjJROkcj.js} +2 -2
  74. package/dist/{tunnel-BmDcTrnU.js.map → tunnel-BjJROkcj.js.map} +1 -1
  75. package/dist/{tunnel-RB5zB8IK.cjs → tunnel-d_G9AIFn.cjs} +2 -2
  76. package/dist/{tunnel-RB5zB8IK.cjs.map → tunnel-d_G9AIFn.cjs.map} +1 -1
  77. package/dist/unplugin/index.cjs +4 -4
  78. package/dist/unplugin/index.d.cts +13 -32
  79. package/dist/unplugin/index.d.cts.map +1 -1
  80. package/dist/unplugin/index.d.ts +13 -32
  81. package/dist/unplugin/index.d.ts.map +1 -1
  82. package/dist/unplugin/index.js +4 -4
  83. package/dist/unplugin/tunnel.cjs +1 -1
  84. package/dist/unplugin/tunnel.js +1 -1
  85. package/package.json +13 -3
  86. package/dist/totp-BcBNRoDD.js +0 -3
  87. package/dist/totp-BmKSPb5d.js.map +0 -1
  88. package/dist/totp-BwDZ6dUT.cjs.map +0 -1
  89. package/dist/totp-CNw0w89F.cjs.map +0 -1
  90. package/dist/totp-DYdP9N3o.js.map +0 -1
  91. package/dist/totp-Xq3ACwkm.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"relay-url-store-DJHZjk8o.js","names":[],"sources":["../src/mcp/relay-url-store.ts"],"sourcesContent":["/**\n * Project-local ephemeral URL store (#424).\n *\n * Environment-2 (\"AITC Sandbox PWA\") cold-start requires two ephemeral URLs:\n * - `relayBaseUrl` — the CDP relay's https base (was `AIT_RELAY_BASE_URL`)\n * - `tunnelBaseUrl` — the app's https tunnel base (was `AIT_TUNNEL_BASE_URL`)\n *\n * Quick-tunnel URLs change every run. Previously the user had to copy-paste them\n * into env vars on every cold-start — a single typo silently broke attach. This\n * module replaces that manual hand-off with a file-based discovery pattern that\n * exactly mirrors the `.ait_relay` TOTP-secret store (relay-secret-store.ts).\n *\n * Two surfaces, intentionally split by who is allowed to write:\n *\n * - {@link writeRelayUrls} — WRITE path, called ONLY from the unplugin\n * (env-2 tunnel boot). Writes JSON to `<projectRoot>/.ait_urls` (0600) on\n * every boot (`flag: 'w'` — overwrite). A single file — no directory is\n * created.\n *\n * - {@link readRelayUrls} — READ-ONLY path, called from the MCP daemon as a\n * fallback when `AIT_RELAY_BASE_URL`/`AIT_TUNNEL_BASE_URL` are not set. It\n * NEVER writes, chmods, or creates anything: it only reads an existing\n * `.ait_urls`. On any failure (missing file / bad JSON / wrong shape) it\n * returns `null` silently, letting the downstream assertion be the single\n * fail-fast.\n *\n * - {@link deleteRelayUrls} — called from the unplugin `cleanup()` on\n * teardown (via `void deleteRelayUrls(...)`). A stale `.ait_urls` pointing\n * at a dead tunnel would cause the MCP daemon to attempt a doomed attach on\n * the next cold-start — deletion is non-negotiable. Silently swallows all\n * errors.\n *\n * Design note: the env vars (`AIT_RELAY_BASE_URL`, `AIT_TUNNEL_BASE_URL`) are\n * PRESERVED as operator overrides. The file is the fallback — env wins.\n *\n * Why `nearestPackageJsonDir` instead of `process.cwd()`: see relay-secret-store.ts.\n * The unplugin (writer) and the MCP daemon (reader) both anchor to the nearest\n * package.json from their respective `projectRoot` inputs, ensuring both sides\n * find the same file.\n *\n * SECRET-HANDLING: `relayBaseUrl` and `tunnelBaseUrl` carry the relay/tunnel host\n * — same sensitivity class as `.ait_relay`. The raw URL values, partial values,\n * and the resolved file path MUST NOT appear in any log, error message, stdout,\n * stderr, or assertion output anywhere in this module or at its call sites.\n * Only boolean pass/fail signals are safe to surface. The file is written mode\n * 0600.\n */\n\nimport { join } from 'node:path';\nimport { nearestPackageJsonDir } from './relay-secret-store.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Project-local ephemeral URL file name (single file, not a directory). */\nexport const URLS_FILE_NAME = '.ait_urls';\n\n// ---------------------------------------------------------------------------\n// Dependency injection surfaces\n// ---------------------------------------------------------------------------\n\n/** Minimal fs subset needed by {@link writeRelayUrls} — injectable for tests. */\nexport interface RelayUrlWriteFs {\n writeFileSync(path: string, data: string, options: { mode: number; flag: string }): void;\n existsSync(path: string): boolean;\n}\n\n/** Minimal fs subset needed by {@link readRelayUrls} — injectable for tests. */\nexport interface RelayUrlReadFs {\n existsSync(path: string): boolean;\n readFileSync(path: string, encoding: BufferEncoding): string;\n}\n\n/** Minimal fs subset needed by {@link deleteRelayUrls} — injectable for tests. */\nexport interface RelayUrlDeleteFs {\n existsSync(path: string): boolean;\n unlinkSync(path: string): void;\n}\n\n// ---------------------------------------------------------------------------\n// Path helper\n// ---------------------------------------------------------------------------\n\n/**\n * Absolute path to the project-local `.ait_urls` file for a given start\n * directory (resolved against the nearest package.json directory).\n *\n * Exported so tests can compute the expected path without duplicating the\n * resolution logic.\n */\nexport function urlsFilePath(start: string, existsSyncFn: (path: string) => boolean): string {\n return join(nearestPackageJsonDir(start, existsSyncFn), URLS_FILE_NAME);\n}\n\n// ---------------------------------------------------------------------------\n// WRITE path (unplugin only) — overwrite on every boot\n// ---------------------------------------------------------------------------\n\nexport interface WriteRelayUrlsDeps {\n /** Project root (typically Vite `server.config.root`). */\n projectRoot: string;\n /**\n * The CDP relay's https base URL (same value as `AIT_RELAY_BASE_URL`).\n * Omit when the relay was not started (e.g. `cdp:false`).\n * SECRET-HANDLING: never log this value.\n */\n relayBaseUrl?: string;\n /**\n * The CDP relay's LOCAL http base URL (`http://127.0.0.1:<relay-port>`).\n * Set when `cdp: true` and the local relay port is known. Used by the MCP\n * daemon's `bootExternalRelayFamily` to build the Chii inspector URL against\n * the local relay rather than the cloudflare tunnel, so front_end page load\n * and the client WS leg do not traverse the tunnel (issue #530).\n * SECRET-HANDLING: local loopback URL — no tunnel host, safe to surface.\n */\n relayLocalUrl?: string;\n /**\n * The app tunnel's https base URL (same value as `AIT_TUNNEL_BASE_URL`).\n * Omit when no tunnel URL is available.\n * SECRET-HANDLING: never log this value.\n */\n tunnelBaseUrl?: string;\n /** Filesystem operations. Defaults to node:fs synchronous functions. */\n fs?: RelayUrlWriteFs;\n /** existsSync used to resolve the nearest package.json directory. Defaults to node:fs. */\n existsSync?: (path: string) => boolean;\n}\n\n/**\n * Writes `{ relayBaseUrl, tunnelBaseUrl }` (omitting absent keys) to\n * `<projectRoot>/.ait_urls` (mode 0600). Uses `flag: 'w'` (overwrite) because\n * URLs are ephemeral — a fresh URL replaces the previous one on every boot.\n *\n * Unlike the `.ait_relay` secret store this does NOT use `O_EXCL` (`'wx'`):\n * there is no race concern here (only the unplugin writes this file) and the\n * URL must be fresh on every cold-start.\n *\n * Called ONLY from the unplugin (env-2 tunnel boot). The MCP daemon uses\n * {@link readRelayUrls} (read-only) — it must never write.\n *\n * SECRET-HANDLING: URL values are never logged.\n */\nexport async function writeRelayUrls(deps: WriteRelayUrlsDeps): Promise<void> {\n const { projectRoot, relayBaseUrl, tunnelBaseUrl, fs: fsDep, existsSync: existsSyncDep } = deps;\n\n const fs: RelayUrlWriteFs = fsDep ?? (await import('node:fs'));\n const existsSyncFn: (path: string) => boolean = existsSyncDep ?? fs.existsSync;\n\n const filePath = urlsFilePath(projectRoot, existsSyncFn);\n\n // Build the payload — omit keys whose values are absent.\n const payload: { relayBaseUrl?: string; relayLocalUrl?: string; tunnelBaseUrl?: string } = {};\n if (typeof relayBaseUrl === 'string' && relayBaseUrl !== '') {\n payload.relayBaseUrl = relayBaseUrl;\n }\n const { relayLocalUrl } = deps;\n if (typeof relayLocalUrl === 'string' && relayLocalUrl !== '') {\n payload.relayLocalUrl = relayLocalUrl;\n }\n if (typeof tunnelBaseUrl === 'string' && tunnelBaseUrl !== '') {\n payload.tunnelBaseUrl = tunnelBaseUrl;\n }\n\n // SECRET-HANDLING: JSON content (which includes URL values) is written to\n // the file only — never to any log, stdout, or stderr.\n const data = JSON.stringify(payload);\n\n // Overwrite on every boot (`flag: 'w'`) — URLs are ephemeral.\n fs.writeFileSync(filePath, data, { mode: 0o600, flag: 'w' });\n}\n\n// ---------------------------------------------------------------------------\n// READ-ONLY path (daemon only) — never writes, chmods, or creates anything\n// ---------------------------------------------------------------------------\n\nexport interface ReadRelayUrlsDeps {\n /** Project root supplied per-debug-session. When omitted, returns `null`. */\n projectRoot?: string;\n /** Read-only filesystem operations. Defaults to node:fs (existsSync + readFileSync). */\n fs?: RelayUrlReadFs;\n /** existsSync used to resolve the nearest package.json directory. Defaults to node:fs. */\n existsSync?: (path: string) => boolean;\n}\n\n/**\n * Reads `<projectRoot>/.ait_urls` and returns the stored URLs, or `null` on\n * any failure.\n *\n * Strictly READ-ONLY: only `existsSync` + `readFileSync`. Never writes,\n * chmods, or creates files/directories.\n *\n * Returns `null` (silently, no throw, no log) on:\n * - missing `projectRoot`\n * - missing `.ait_urls` file\n * - unreadable file (permissions, transient FS error)\n * - invalid JSON\n * - wrong shape (non-object, non-string values)\n *\n * Trims string values before returning. Ignores non-string fields.\n *\n * SECRET-HANDLING: URL values and the file path are never logged.\n */\nexport async function readRelayUrls(deps?: ReadRelayUrlsDeps): Promise<{\n relayBaseUrl?: string;\n relayLocalUrl?: string;\n tunnelBaseUrl?: string;\n} | null> {\n const { projectRoot, fs: fsDep, existsSync: existsSyncDep } = deps ?? {};\n\n if (projectRoot === undefined) {\n return null;\n }\n\n const fs: RelayUrlReadFs = fsDep ?? (await import('node:fs'));\n const existsSyncFn: (path: string) => boolean = existsSyncDep ?? fs.existsSync;\n\n const filePath = urlsFilePath(projectRoot, existsSyncFn);\n\n if (!fs.existsSync(filePath)) {\n return null;\n }\n\n let raw: string;\n try {\n raw = fs.readFileSync(filePath, 'utf8');\n } catch {\n // Unreadable file — silent no-op, let the downstream assert be the fail-fast.\n // SECRET-HANDLING: the error and path are not surfaced.\n return null;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n // Invalid JSON — silent no-op.\n return null;\n }\n\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n return null;\n }\n\n const obj = parsed as Record<string, unknown>;\n const result: { relayBaseUrl?: string; relayLocalUrl?: string; tunnelBaseUrl?: string } = {};\n\n const relay = obj.relayBaseUrl;\n if (typeof relay === 'string') {\n const trimmed = relay.trim();\n if (trimmed !== '') result.relayBaseUrl = trimmed;\n }\n\n const relayLocal = obj.relayLocalUrl;\n if (typeof relayLocal === 'string') {\n const trimmed = relayLocal.trim();\n if (trimmed !== '') result.relayLocalUrl = trimmed;\n }\n\n const tunnel = obj.tunnelBaseUrl;\n if (typeof tunnel === 'string') {\n const trimmed = tunnel.trim();\n if (trimmed !== '') result.tunnelBaseUrl = trimmed;\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// DELETE path (unplugin cleanup only)\n// ---------------------------------------------------------------------------\n\nexport interface DeleteRelayUrlsDeps {\n /** Project root. */\n projectRoot: string;\n /** Filesystem operations. Defaults to node:fs (existsSync + unlinkSync). */\n fs?: RelayUrlDeleteFs;\n /** existsSync used to resolve the nearest package.json directory. */\n existsSync?: (path: string) => boolean;\n}\n\n/**\n * Removes `<projectRoot>/.ait_urls` if present. Silently swallows ENOENT and\n * any other error so cleanup always succeeds.\n *\n * Called ONLY from the unplugin's `cleanup()` on `httpServer 'close'` + signals\n * (via `void deleteRelayUrls(...)`). A stale `.ait_urls` pointing at a dead\n * tunnel would cause the MCP daemon to attempt a doomed attach on the next\n * cold-start — deletion is non-negotiable.\n *\n * SECRET-HANDLING: the file path is never logged.\n */\nexport async function deleteRelayUrls(deps: DeleteRelayUrlsDeps): Promise<void> {\n const { projectRoot, fs: fsDep, existsSync: existsSyncDep } = deps;\n\n const fs: RelayUrlDeleteFs = fsDep ?? (await import('node:fs'));\n const existsSyncFn: (path: string) => boolean = existsSyncDep ?? fs.existsSync;\n\n const filePath = urlsFilePath(projectRoot, existsSyncFn);\n\n try {\n if (fs.existsSync(filePath)) {\n fs.unlinkSync(filePath);\n }\n } catch {\n // Swallow ENOENT and any other error — cleanup is best-effort.\n // SECRET-HANDLING: the path is not logged.\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,MAAa,iBAAiB;;;;;;;;AAmC9B,SAAgB,aAAa,OAAe,cAAiD;AAC3F,QAAO,KAAK,sBAAsB,OAAO,aAAa,EAAE,eAAe;;;;;;;;;;;;;;;;AAmDzE,eAAsB,eAAe,MAAyC;CAC5E,MAAM,EAAE,aAAa,cAAc,eAAe,IAAI,OAAO,YAAY,kBAAkB;CAE3F,MAAM,KAAsB,SAAU,MAAM,OAAO;CAGnD,MAAM,WAAW,aAAa,aAFkB,iBAAiB,GAAG,WAEZ;CAGxD,MAAM,UAAqF,EAAE;AAC7F,KAAI,OAAO,iBAAiB,YAAY,iBAAiB,GACvD,SAAQ,eAAe;CAEzB,MAAM,EAAE,kBAAkB;AAC1B,KAAI,OAAO,kBAAkB,YAAY,kBAAkB,GACzD,SAAQ,gBAAgB;AAE1B,KAAI,OAAO,kBAAkB,YAAY,kBAAkB,GACzD,SAAQ,gBAAgB;CAK1B,MAAM,OAAO,KAAK,UAAU,QAAQ;AAGpC,IAAG,cAAc,UAAU,MAAM;EAAE,MAAM;EAAO,MAAM;EAAK,CAAC;;;;;;;;;;;;;AA2H9D,eAAsB,gBAAgB,MAA0C;CAC9E,MAAM,EAAE,aAAa,IAAI,OAAO,YAAY,kBAAkB;CAE9D,MAAM,KAAuB,SAAU,MAAM,OAAO;CAGpD,MAAM,WAAW,aAAa,aAFkB,iBAAiB,GAAG,WAEZ;AAExD,KAAI;AACF,MAAI,GAAG,WAAW,SAAS,CACzB,IAAG,WAAW,SAAS;SAEnB"}
1
+ {"version":3,"file":"relay-url-store-j16TRTiJ.js","names":[],"sources":["../src/mcp/relay-url-store.ts"],"sourcesContent":["/**\n * Project-local ephemeral URL store (#424).\n *\n * Environment-2 (\"AITC Sandbox PWA\") cold-start requires two ephemeral URLs:\n * - `relayBaseUrl` — the CDP relay's https base (was `AIT_RELAY_BASE_URL`)\n * - `tunnelBaseUrl` — the app's https tunnel base (was `AIT_TUNNEL_BASE_URL`)\n *\n * Quick-tunnel URLs change every run. Previously the user had to copy-paste them\n * into env vars on every cold-start — a single typo silently broke attach. This\n * module replaces that manual hand-off with a file-based discovery pattern that\n * exactly mirrors the `.ait_relay` TOTP-secret store (relay-secret-store.ts).\n *\n * Two surfaces, intentionally split by who is allowed to write:\n *\n * - {@link writeRelayUrls} — WRITE path, called ONLY from the unplugin\n * (env-2 tunnel boot). Writes JSON to `<projectRoot>/.ait_urls` (0600) on\n * every boot (`flag: 'w'` — overwrite). A single file — no directory is\n * created.\n *\n * - {@link readRelayUrls} — READ-ONLY path, called from the MCP daemon as a\n * fallback when `AIT_RELAY_BASE_URL`/`AIT_TUNNEL_BASE_URL` are not set. It\n * NEVER writes, chmods, or creates anything: it only reads an existing\n * `.ait_urls`. On any failure (missing file / bad JSON / wrong shape) it\n * returns `null` silently, letting the downstream assertion be the single\n * fail-fast.\n *\n * - {@link deleteRelayUrls} — called from the unplugin `cleanup()` on\n * teardown (via `void deleteRelayUrls(...)`). A stale `.ait_urls` pointing\n * at a dead tunnel would cause the MCP daemon to attempt a doomed attach on\n * the next cold-start — deletion is non-negotiable. Silently swallows all\n * errors.\n *\n * Design note: the env vars (`AIT_RELAY_BASE_URL`, `AIT_TUNNEL_BASE_URL`) are\n * PRESERVED as operator overrides. The file is the fallback — env wins.\n *\n * Why `nearestPackageJsonDir` instead of `process.cwd()`: see relay-secret-store.ts.\n * The unplugin (writer) and the MCP daemon (reader) both anchor to the nearest\n * package.json from their respective `projectRoot` inputs, ensuring both sides\n * find the same file.\n *\n * SECRET-HANDLING: `relayBaseUrl` and `tunnelBaseUrl` carry the relay/tunnel host\n * — same sensitivity class as `.ait_relay`. The raw URL values, partial values,\n * and the resolved file path MUST NOT appear in any log, error message, stdout,\n * stderr, or assertion output anywhere in this module or at its call sites.\n * Only boolean pass/fail signals are safe to surface. The file is written mode\n * 0600.\n */\n\nimport { join } from 'node:path';\nimport { nearestPackageJsonDir } from './relay-secret-store.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Project-local ephemeral URL file name (single file, not a directory). */\nexport const URLS_FILE_NAME = '.ait_urls';\n\n// ---------------------------------------------------------------------------\n// Dependency injection surfaces\n// ---------------------------------------------------------------------------\n\n/** Minimal fs subset needed by {@link writeRelayUrls} — injectable for tests. */\nexport interface RelayUrlWriteFs {\n writeFileSync(path: string, data: string, options: { mode: number; flag: string }): void;\n existsSync(path: string): boolean;\n}\n\n/** Minimal fs subset needed by {@link readRelayUrls} — injectable for tests. */\nexport interface RelayUrlReadFs {\n existsSync(path: string): boolean;\n readFileSync(path: string, encoding: BufferEncoding): string;\n}\n\n/** Minimal fs subset needed by {@link deleteRelayUrls} — injectable for tests. */\nexport interface RelayUrlDeleteFs {\n existsSync(path: string): boolean;\n unlinkSync(path: string): void;\n}\n\n// ---------------------------------------------------------------------------\n// Path helper\n// ---------------------------------------------------------------------------\n\n/**\n * Absolute path to the project-local `.ait_urls` file for a given start\n * directory (resolved against the nearest package.json directory).\n *\n * Exported so tests can compute the expected path without duplicating the\n * resolution logic.\n */\nexport function urlsFilePath(start: string, existsSyncFn: (path: string) => boolean): string {\n return join(nearestPackageJsonDir(start, existsSyncFn), URLS_FILE_NAME);\n}\n\n// ---------------------------------------------------------------------------\n// WRITE path (unplugin only) — overwrite on every boot\n// ---------------------------------------------------------------------------\n\nexport interface WriteRelayUrlsDeps {\n /** Project root (typically Vite `server.config.root`). */\n projectRoot: string;\n /**\n * The CDP relay's https base URL (same value as `AIT_RELAY_BASE_URL`).\n * Omit when the relay was not started (e.g. `cdp:false`).\n * SECRET-HANDLING: never log this value.\n */\n relayBaseUrl?: string;\n /**\n * The CDP relay's LOCAL http base URL (`http://127.0.0.1:<relay-port>`).\n * Set when `cdp: true` and the local relay port is known. Used by the MCP\n * daemon's `bootExternalRelayFamily` to build the Chii inspector URL against\n * the local relay rather than the cloudflare tunnel, so front_end page load\n * and the client WS leg do not traverse the tunnel (issue #530).\n * SECRET-HANDLING: local loopback URL — no tunnel host, safe to surface.\n */\n relayLocalUrl?: string;\n /**\n * The app tunnel's https base URL (same value as `AIT_TUNNEL_BASE_URL`).\n * Omit when no tunnel URL is available.\n * SECRET-HANDLING: never log this value.\n */\n tunnelBaseUrl?: string;\n /** Filesystem operations. Defaults to node:fs synchronous functions. */\n fs?: RelayUrlWriteFs;\n /** existsSync used to resolve the nearest package.json directory. Defaults to node:fs. */\n existsSync?: (path: string) => boolean;\n}\n\n/**\n * Writes `{ relayBaseUrl, tunnelBaseUrl }` (omitting absent keys) to\n * `<projectRoot>/.ait_urls` (mode 0600). Uses `flag: 'w'` (overwrite) because\n * URLs are ephemeral — a fresh URL replaces the previous one on every boot.\n *\n * Unlike the `.ait_relay` secret store this does NOT use `O_EXCL` (`'wx'`):\n * there is no race concern here (only the unplugin writes this file) and the\n * URL must be fresh on every cold-start.\n *\n * Called ONLY from the unplugin (env-2 tunnel boot). The MCP daemon uses\n * {@link readRelayUrls} (read-only) — it must never write.\n *\n * SECRET-HANDLING: URL values are never logged.\n */\nexport async function writeRelayUrls(deps: WriteRelayUrlsDeps): Promise<void> {\n const { projectRoot, relayBaseUrl, tunnelBaseUrl, fs: fsDep, existsSync: existsSyncDep } = deps;\n\n const fs: RelayUrlWriteFs = fsDep ?? (await import('node:fs'));\n const existsSyncFn: (path: string) => boolean = existsSyncDep ?? fs.existsSync;\n\n const filePath = urlsFilePath(projectRoot, existsSyncFn);\n\n // Build the payload — omit keys whose values are absent.\n const payload: { relayBaseUrl?: string; relayLocalUrl?: string; tunnelBaseUrl?: string } = {};\n if (typeof relayBaseUrl === 'string' && relayBaseUrl !== '') {\n payload.relayBaseUrl = relayBaseUrl;\n }\n const { relayLocalUrl } = deps;\n if (typeof relayLocalUrl === 'string' && relayLocalUrl !== '') {\n payload.relayLocalUrl = relayLocalUrl;\n }\n if (typeof tunnelBaseUrl === 'string' && tunnelBaseUrl !== '') {\n payload.tunnelBaseUrl = tunnelBaseUrl;\n }\n\n // SECRET-HANDLING: JSON content (which includes URL values) is written to\n // the file only — never to any log, stdout, or stderr.\n const data = JSON.stringify(payload);\n\n // Overwrite on every boot (`flag: 'w'`) — URLs are ephemeral.\n fs.writeFileSync(filePath, data, { mode: 0o600, flag: 'w' });\n}\n\n// ---------------------------------------------------------------------------\n// READ-ONLY path (daemon only) — never writes, chmods, or creates anything\n// ---------------------------------------------------------------------------\n\nexport interface ReadRelayUrlsDeps {\n /** Project root supplied per-debug-session. When omitted, returns `null`. */\n projectRoot?: string;\n /** Read-only filesystem operations. Defaults to node:fs (existsSync + readFileSync). */\n fs?: RelayUrlReadFs;\n /** existsSync used to resolve the nearest package.json directory. Defaults to node:fs. */\n existsSync?: (path: string) => boolean;\n}\n\n/**\n * Reads `<projectRoot>/.ait_urls` and returns the stored URLs, or `null` on\n * any failure.\n *\n * Strictly READ-ONLY: only `existsSync` + `readFileSync`. Never writes,\n * chmods, or creates files/directories.\n *\n * Returns `null` (silently, no throw, no log) on:\n * - missing `projectRoot`\n * - missing `.ait_urls` file\n * - unreadable file (permissions, transient FS error)\n * - invalid JSON\n * - wrong shape (non-object, non-string values)\n *\n * Trims string values before returning. Ignores non-string fields.\n *\n * SECRET-HANDLING: URL values and the file path are never logged.\n */\nexport async function readRelayUrls(deps?: ReadRelayUrlsDeps): Promise<{\n relayBaseUrl?: string;\n relayLocalUrl?: string;\n tunnelBaseUrl?: string;\n} | null> {\n const { projectRoot, fs: fsDep, existsSync: existsSyncDep } = deps ?? {};\n\n if (projectRoot === undefined) {\n return null;\n }\n\n const fs: RelayUrlReadFs = fsDep ?? (await import('node:fs'));\n const existsSyncFn: (path: string) => boolean = existsSyncDep ?? fs.existsSync;\n\n const filePath = urlsFilePath(projectRoot, existsSyncFn);\n\n if (!fs.existsSync(filePath)) {\n return null;\n }\n\n let raw: string;\n try {\n raw = fs.readFileSync(filePath, 'utf8');\n } catch {\n // Unreadable file — silent no-op, let the downstream assert be the fail-fast.\n // SECRET-HANDLING: the error and path are not surfaced.\n return null;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n // Invalid JSON — silent no-op.\n return null;\n }\n\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n return null;\n }\n\n const obj = parsed as Record<string, unknown>;\n const result: { relayBaseUrl?: string; relayLocalUrl?: string; tunnelBaseUrl?: string } = {};\n\n const relay = obj.relayBaseUrl;\n if (typeof relay === 'string') {\n const trimmed = relay.trim();\n if (trimmed !== '') result.relayBaseUrl = trimmed;\n }\n\n const relayLocal = obj.relayLocalUrl;\n if (typeof relayLocal === 'string') {\n const trimmed = relayLocal.trim();\n if (trimmed !== '') result.relayLocalUrl = trimmed;\n }\n\n const tunnel = obj.tunnelBaseUrl;\n if (typeof tunnel === 'string') {\n const trimmed = tunnel.trim();\n if (trimmed !== '') result.tunnelBaseUrl = trimmed;\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// DELETE path (unplugin cleanup only)\n// ---------------------------------------------------------------------------\n\nexport interface DeleteRelayUrlsDeps {\n /** Project root. */\n projectRoot: string;\n /** Filesystem operations. Defaults to node:fs (existsSync + unlinkSync). */\n fs?: RelayUrlDeleteFs;\n /** existsSync used to resolve the nearest package.json directory. */\n existsSync?: (path: string) => boolean;\n}\n\n/**\n * Removes `<projectRoot>/.ait_urls` if present. Silently swallows ENOENT and\n * any other error so cleanup always succeeds.\n *\n * Called ONLY from the unplugin's `cleanup()` on `httpServer 'close'` + signals\n * (via `void deleteRelayUrls(...)`). A stale `.ait_urls` pointing at a dead\n * tunnel would cause the MCP daemon to attempt a doomed attach on the next\n * cold-start — deletion is non-negotiable.\n *\n * SECRET-HANDLING: the file path is never logged.\n */\nexport async function deleteRelayUrls(deps: DeleteRelayUrlsDeps): Promise<void> {\n const { projectRoot, fs: fsDep, existsSync: existsSyncDep } = deps;\n\n const fs: RelayUrlDeleteFs = fsDep ?? (await import('node:fs'));\n const existsSyncFn: (path: string) => boolean = existsSyncDep ?? fs.existsSync;\n\n const filePath = urlsFilePath(projectRoot, existsSyncFn);\n\n try {\n if (fs.existsSync(filePath)) {\n fs.unlinkSync(filePath);\n }\n } catch {\n // Swallow ENOENT and any other error — cleanup is best-effort.\n // SECRET-HANDLING: the path is not logged.\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,MAAa,iBAAiB;;;;;;;;AAmC9B,SAAgB,aAAa,OAAe,cAAiD;AAC3F,QAAO,KAAK,sBAAsB,OAAO,aAAa,EAAE,eAAe;;;;;;;;;;;;;;;;AAmDzE,eAAsB,eAAe,MAAyC;CAC5E,MAAM,EAAE,aAAa,cAAc,eAAe,IAAI,OAAO,YAAY,kBAAkB;CAE3F,MAAM,KAAsB,SAAU,MAAM,OAAO;CAGnD,MAAM,WAAW,aAAa,aAFkB,iBAAiB,GAAG,WAEZ;CAGxD,MAAM,UAAqF,EAAE;AAC7F,KAAI,OAAO,iBAAiB,YAAY,iBAAiB,GACvD,SAAQ,eAAe;CAEzB,MAAM,EAAE,kBAAkB;AAC1B,KAAI,OAAO,kBAAkB,YAAY,kBAAkB,GACzD,SAAQ,gBAAgB;AAE1B,KAAI,OAAO,kBAAkB,YAAY,kBAAkB,GACzD,SAAQ,gBAAgB;CAK1B,MAAM,OAAO,KAAK,UAAU,QAAQ;AAGpC,IAAG,cAAc,UAAU,MAAM;EAAE,MAAM;EAAO,MAAM;EAAK,CAAC;;;;;;;;;;;;;AA2H9D,eAAsB,gBAAgB,MAA0C;CAC9E,MAAM,EAAE,aAAa,IAAI,OAAO,YAAY,kBAAkB;CAE9D,MAAM,KAAuB,SAAU,MAAM,OAAO;CAGpD,MAAM,WAAW,aAAa,aAFkB,iBAAiB,GAAG,WAEZ;AAExD,KAAI;AACF,MAAI,GAAG,WAAW,SAAS,CACzB,IAAG,WAAW,SAAS;SAEnB"}
@@ -0,0 +1,74 @@
1
+ import { t as BundleOptions } from "./bundle-BJm5jk56.js";
2
+ import { t as CdpConnection } from "./cdp-connection-C0AP0tH2.js";
3
+ import { n as TestResult, t as RunReport } from "./runtime-ORdrpizY.js";
4
+
5
+ //#region src/test-runner/relay-worker.d.ts
6
+ /** Per-file result in the aggregate `RunReport`. */
7
+ interface FileResult {
8
+ /** Absolute or relative path to the test file. */
9
+ file: string;
10
+ /** Full run report for this file, or an error if bundling/injection failed. */
11
+ result: RunReport | {
12
+ error: string;
13
+ };
14
+ }
15
+ /** Aggregate report returned by `runTestFilesOverRelay`. */
16
+ interface RelayRunReport {
17
+ /** ISO timestamp of when the run started. */
18
+ startedAt: string;
19
+ /** Total elapsed wall-clock milliseconds. */
20
+ duration: number;
21
+ /** Per-file results in execution order. */
22
+ files: FileResult[];
23
+ /** Flattened totals across all files. */
24
+ totals: {
25
+ passed: number;
26
+ failed: number;
27
+ skipped: number;
28
+ total: number;
29
+ };
30
+ }
31
+ /** Options for `runTestFilesOverRelay`. */
32
+ interface RelayRunOptions {
33
+ /**
34
+ * Options forwarded to `bundleTestFile` for each file.
35
+ */
36
+ bundleOptions?: BundleOptions;
37
+ /**
38
+ * Per-file evaluate timeout in milliseconds. Defaults to 30 000.
39
+ * Increase for long-running suites or split the file.
40
+ */
41
+ timeoutMs?: number;
42
+ }
43
+ /**
44
+ * Runs all `files` sequentially over the given CDP `connection`.
45
+ *
46
+ * For each file:
47
+ * 1. Bundle with esbuild (includes SDK shim + runtime).
48
+ * 2. Inject into the attached page via `Runtime.evaluate`.
49
+ * 3. Await the `RunReport` JSON response.
50
+ * 4. Accumulate results.
51
+ *
52
+ * Returns a `RelayRunReport` with per-file results and flattened totals.
53
+ *
54
+ * This function does NOT open or manage the relay connection — the caller
55
+ * is responsible for attaching and closing it.
56
+ *
57
+ * TODO (#645): implement the Vitest `PoolRunnerInitializer` interface here
58
+ * so that `runTestFilesOverRelay` can be used as a Vitest pool entry.
59
+ *
60
+ * @param connection - Active CDP connection (relay or local kind).
61
+ * @param files - Absolute paths to test files, run in order.
62
+ * @param opts - Optional per-run overrides.
63
+ */
64
+ declare function runTestFilesOverRelay(connection: CdpConnection, files: string[], opts?: RelayRunOptions): Promise<RelayRunReport>;
65
+ /**
66
+ * Flattens all test results from a `RelayRunReport` into a single array.
67
+ * Files that errored during bundle/inject produce a synthetic failed entry.
68
+ */
69
+ declare function flattenResults(report: RelayRunReport): Array<TestResult & {
70
+ file: string;
71
+ }>;
72
+ //#endregion
73
+ export { runTestFilesOverRelay as a, flattenResults as i, RelayRunOptions as n, RelayRunReport as r, FileResult as t };
74
+ //# sourceMappingURL=relay-worker-BzFQ3fv9.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay-worker-BzFQ3fv9.d.ts","names":[],"sources":["../src/test-runner/relay-worker.ts"],"mappings":";;;;;;UAsBiB,UAAA;EAYf;EAVA,IAAA;EAYO;EAVP,MAAA,EAAQ,SAAA;IAAc,KAAA;EAAA;AAAA;;UAIP,cAAA;EAYR;EAVP,SAAA;EAe8B;EAb9B,QAAA;EAiB6B;EAf7B,KAAA,EAAO,UAAA;EAeS;EAbhB,MAAA;IACE,MAAA;IACA,MAAA;IACA,OAAA;IACA,KAAA;EAAA;AAAA;;UAKa,eAAA;EAqCd;;;EAjCD,aAAA,GAAgB,aAAA;EA8BhB;;;;EAzBA,SAAA;AAAA;;;AAoFF;;;;;;;;;;;;;;;;;;;iBA5DsB,qBAAA,CACpB,UAAA,EAAY,aAAA,EACZ,KAAA,YACA,IAAA,GAAO,eAAA,GACN,OAAA,CAAQ,cAAA;;;;;iBAwDK,cAAA,CAAe,MAAA,EAAQ,cAAA,GAAiB,KAAA,CAAM,UAAA;EAAe,IAAA;AAAA"}
@@ -0,0 +1,50 @@
1
+ //#region src/test-runner/runtime.d.ts
2
+ /**
3
+ * Thin test runtime for WebView execution.
4
+ *
5
+ * This file is bundled by `bundle.ts` together with the user's test file
6
+ * into a single IIFE injected into the WebView via `Runtime.evaluate`.
7
+ * It MUST stay browser-compatible — no Node.js APIs allowed.
8
+ *
9
+ * Design: rather than shipping @vitest/runner verbatim into a WebView (where
10
+ * its Node-side internals cause issues), this runtime provides a minimal
11
+ * compatible API surface — describe/it/test/expect globals — collects results
12
+ * into a plain JSON-safe object, and exports `runTestModule` as the entry point.
13
+ *
14
+ * @vitest/runner and @vitest/expect are listed as dependencies so that the
15
+ * package's type contracts are available and so the browser-compatible subsets
16
+ * can be referenced. Full pool integration (PoolRunnerInitializer) is tracked
17
+ * in issue #645.
18
+ *
19
+ * NOTE: this file is imported by type from Node-side code (rpc.ts / relay-worker.ts)
20
+ * for the RunReport / TestResult type shapes. The runtime ITSELF is not imported
21
+ * at runtime on the Node side — only the types are used.
22
+ */
23
+ /**
24
+ * Result of a single test case.
25
+ * All fields are JSON-serialisable.
26
+ */
27
+ interface TestResult {
28
+ /** Full dot-joined test name including nested suite names. */
29
+ name: string;
30
+ /** `'pass'` or `'fail'`. `'skip'` for skipped tests. */
31
+ status: 'pass' | 'fail' | 'skip';
32
+ /** Duration in milliseconds. */
33
+ duration: number;
34
+ /** Error message (fail only). Does NOT include the expression/secret. */
35
+ error?: string;
36
+ }
37
+ /** Aggregate report returned by `runTestModule`. */
38
+ interface RunReport {
39
+ /** ISO timestamp of when `runTestModule` was called. */
40
+ startedAt: string;
41
+ /** Total elapsed milliseconds (wall-clock). */
42
+ duration: number;
43
+ passed: number;
44
+ failed: number;
45
+ skipped: number;
46
+ tests: TestResult[];
47
+ }
48
+ //#endregion
49
+ export { TestResult as n, RunReport as t };
50
+ //# sourceMappingURL=runtime-ORdrpizY.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-ORdrpizY.d.ts","names":[],"sources":["../src/test-runner/runtime.ts"],"mappings":";;AA8BA;;;;;;;;;;AAYA;;;;;;;;;;;;;;UAZiB,UAAA;;EAEf,IAAA;;EAEA,MAAA;;EAEA,QAAA;;EAEA,KAAA;AAAA;;UAIe,SAAA;;EAEf,SAAA;;EAEA,QAAA;EACA,MAAA;EACA,MAAA;EACA,OAAA;EACA,KAAA,EAAO,UAAA;AAAA"}
@@ -0,0 +1,2 @@
1
+ import { n as BundleResult, r as bundleTestFile, t as BundleOptions } from "../bundle-BJm5jk56.js";
2
+ export { BundleOptions, BundleResult, bundleTestFile };
@@ -0,0 +1,95 @@
1
+ import * as path from "node:path";
2
+ //#region src/test-runner/bundle.ts
3
+ /**
4
+ * esbuild-based bundler for user test files.
5
+ *
6
+ * Bundles a single test file into a self-contained IIFE string that can be
7
+ * injected into a WebView via `Runtime.evaluate`. The user's SDK imports
8
+ * (`@apps-in-toss/web-framework` and sub-paths) are intercepted via an
9
+ * esbuild plugin that redirects them to `window.__sdk`, which the in-app
10
+ * debug gate (`src/in-app/auto.ts`) installs as a namespace mirror of the
11
+ * SDK exports (works for both 2.x and 3.x SDK).
12
+ *
13
+ * SECRET-HANDLING: the returned bundle code is caller-managed; never log it.
14
+ */
15
+ /**
16
+ * Matches the bare SDK package and any sub-path import
17
+ * (`@apps-in-toss/web-framework`, `@apps-in-toss/web-framework/foo`).
18
+ * Built from {@link SDK_PACKAGE} so the package name has a single source.
19
+ */
20
+ const SDK_IMPORT_FILTER = new RegExp(`^${"@apps-in-toss/web-framework".replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
21
+ /**
22
+ * esbuild plugin that intercepts SDK imports and redirects them to the
23
+ * `window.__sdk` proxy that `src/in-app/auto.ts` installs at runtime.
24
+ *
25
+ * Strategy: for every import of `@apps-in-toss/web-framework` (or sub-paths),
26
+ * esbuild resolves it to a virtual module that re-exports all named exports
27
+ * via `window.__sdk[name]`. This avoids bundling the real SDK (which may not
28
+ * be available in the test environment) while still making named imports work.
29
+ *
30
+ * If `window.__sdk` is absent (non-dog-food build), every access throws a
31
+ * descriptive error rather than returning `undefined` silently.
32
+ */
33
+ function sdkRedirectPlugin() {
34
+ return {
35
+ name: "sdk-redirect",
36
+ setup(build) {
37
+ build.onResolve({ filter: SDK_IMPORT_FILTER }, (args) => ({
38
+ path: args.path,
39
+ namespace: "sdk-redirect"
40
+ }));
41
+ build.onLoad({
42
+ filter: /.*/,
43
+ namespace: "sdk-redirect"
44
+ }, () => ({
45
+ contents: `
46
+ var __proxy = (typeof window !== 'undefined' && window.__sdk)
47
+ ? window.__sdk
48
+ : new Proxy({}, {
49
+ get: function(_t, p) {
50
+ throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));
51
+ }
52
+ });
53
+ module.exports = __proxy;
54
+ `,
55
+ loader: "js"
56
+ }));
57
+ }
58
+ };
59
+ }
60
+ /**
61
+ * Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.
62
+ *
63
+ * The IIFE installs `window.__testBundle` (or the custom `globalName`) with
64
+ * `runTestModule` as the callable entry point.
65
+ *
66
+ * @param absPath - Absolute path to the user test file.
67
+ * @param opts - Optional bundling overrides.
68
+ */
69
+ async function bundleTestFile(absPath, opts) {
70
+ const globalName = opts?.globalName ?? "__testBundle";
71
+ const extraExternals = opts?.extraExternals ?? [];
72
+ const result = await (await import("esbuild")).build({
73
+ entryPoints: [absPath],
74
+ bundle: true,
75
+ format: "iife",
76
+ globalName,
77
+ platform: "browser",
78
+ target: "es2022",
79
+ write: false,
80
+ plugins: [sdkRedirectPlugin()],
81
+ external: extraExternals,
82
+ treeShaking: true
83
+ });
84
+ const warnings = result.warnings.map((w) => `${path.relative(process.cwd(), w.location?.file ?? "")}:${w.location?.line ?? "?"}: ${w.text}`);
85
+ const outputFile = result.outputFiles?.[0];
86
+ if (!outputFile) throw new Error("bundleTestFile: esbuild produced no output — check entryPoints");
87
+ return {
88
+ code: outputFile.text,
89
+ warnings
90
+ };
91
+ }
92
+ //#endregion
93
+ export { bundleTestFile };
94
+
95
+ //# sourceMappingURL=bundle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle.js","names":[],"sources":["../../src/test-runner/bundle.ts"],"sourcesContent":["/**\n * esbuild-based bundler for user test files.\n *\n * Bundles a single test file into a self-contained IIFE string that can be\n * injected into a WebView via `Runtime.evaluate`. The user's SDK imports\n * (`@apps-in-toss/web-framework` and sub-paths) are intercepted via an\n * esbuild plugin that redirects them to `window.__sdk`, which the in-app\n * debug gate (`src/in-app/auto.ts`) installs as a namespace mirror of the\n * SDK exports (works for both 2.x and 3.x SDK).\n *\n * SECRET-HANDLING: the returned bundle code is caller-managed; never log it.\n */\n\nimport * as path from 'node:path';\n// esbuild is imported for TYPES only at module scope; the runtime module is\n// loaded lazily inside `bundleTestFile` via dynamic import. esbuild runs a\n// startup invariant check (`TextEncoder().encode('') instanceof Uint8Array`)\n// that fails in a jsdom realm — a static import would break every MCP/test\n// module that merely *imports* this file's transitive graph (e.g. debug-server →\n// run_tests). Lazy load keeps esbuild off the import graph until a bundle is\n// actually built, and mirrors the cloudflared/chii dynamic-import precedent.\nimport type * as esbuild from 'esbuild';\n\n/** Options accepted by `bundleTestFile`. */\nexport interface BundleOptions {\n /**\n * Additional esbuild `external` patterns. The SDK package\n * (`@apps-in-toss/web-framework` and `@apps-in-toss/web-framework/*`) is\n * always handled by the SDK redirect plugin — callers may add more patterns\n * to be left as globals.\n */\n extraExternals?: string[];\n /**\n * Global name for the IIFE output object. Defaults to `__testBundle`.\n * The runtime entry uses this to call `__testBundle.runTestModule()`.\n */\n globalName?: string;\n}\n\n/**\n * The result of bundling a test file.\n * `code` is a self-contained IIFE string ready for `Runtime.evaluate`.\n */\nexport interface BundleResult {\n code: string;\n warnings: string[];\n}\n\n/** The SDK package name that mini-app test code imports from. */\nconst SDK_PACKAGE = '@apps-in-toss/web-framework';\n\n/**\n * Matches the bare SDK package and any sub-path import\n * (`@apps-in-toss/web-framework`, `@apps-in-toss/web-framework/foo`).\n * Built from {@link SDK_PACKAGE} so the package name has a single source.\n */\nconst SDK_IMPORT_FILTER = new RegExp(`^${SDK_PACKAGE.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}`);\n\n/**\n * esbuild plugin that intercepts SDK imports and redirects them to the\n * `window.__sdk` proxy that `src/in-app/auto.ts` installs at runtime.\n *\n * Strategy: for every import of `@apps-in-toss/web-framework` (or sub-paths),\n * esbuild resolves it to a virtual module that re-exports all named exports\n * via `window.__sdk[name]`. This avoids bundling the real SDK (which may not\n * be available in the test environment) while still making named imports work.\n *\n * If `window.__sdk` is absent (non-dog-food build), every access throws a\n * descriptive error rather than returning `undefined` silently.\n */\nfunction sdkRedirectPlugin(): esbuild.Plugin {\n return {\n name: 'sdk-redirect',\n setup(build) {\n // Match the bare package and any sub-path imports\n build.onResolve({ filter: SDK_IMPORT_FILTER }, (args) => ({\n path: args.path,\n namespace: 'sdk-redirect',\n }));\n\n build.onLoad({ filter: /.*/, namespace: 'sdk-redirect' }, () => ({\n // Generate a virtual CommonJS-style module so that esbuild does NOT perform\n // strict named-export matching. When `format:'iife'` bundles a CJS module,\n // it wraps it with its own __toCommonJS helper and satisfies named imports\n // via property access on the module.exports object — which is our Proxy.\n // This means `import { getPlatformOS } from '...'` becomes\n // `__proxy.getPlatformOS` at runtime, which correctly reads from window.__sdk.\n contents: `\nvar __proxy = (typeof window !== 'undefined' && window.__sdk)\n ? window.__sdk\n : new Proxy({}, {\n get: function(_t, p) {\n throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));\n }\n });\nmodule.exports = __proxy;\n`,\n loader: 'js',\n }));\n },\n };\n}\n\n/**\n * Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.\n *\n * The IIFE installs `window.__testBundle` (or the custom `globalName`) with\n * `runTestModule` as the callable entry point.\n *\n * @param absPath - Absolute path to the user test file.\n * @param opts - Optional bundling overrides.\n */\nexport async function bundleTestFile(absPath: string, opts?: BundleOptions): Promise<BundleResult> {\n const globalName = opts?.globalName ?? '__testBundle';\n const extraExternals = opts?.extraExternals ?? [];\n\n // Lazy load esbuild at call time (see the module-scope import note).\n const esbuild = await import('esbuild');\n\n const result = await esbuild.build({\n entryPoints: [absPath],\n bundle: true,\n format: 'iife',\n globalName,\n platform: 'browser',\n target: 'es2022',\n write: false,\n plugins: [sdkRedirectPlugin()],\n // Extra externals are left as global references (caller's responsibility\n // to ensure they exist in the WebView context).\n external: extraExternals,\n // Keep bundle self-contained; no dynamic require/import at runtime.\n treeShaking: true,\n });\n\n const warnings = result.warnings.map(\n (w) =>\n `${path.relative(process.cwd(), w.location?.file ?? '')}:${w.location?.line ?? '?'}: ${w.text}`,\n );\n\n const outputFile = result.outputFiles?.[0];\n if (!outputFile) {\n throw new Error('bundleTestFile: esbuild produced no output — check entryPoints');\n }\n\n return { code: outputFile.text, warnings };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAwDA,MAAM,oBAAoB,IAAI,OAAO,IAPjB,8BAOiC,QAAQ,uBAAuB,OAAO,GAAG;;;;;;;;;;;;;AAc9F,SAAS,oBAAoC;AAC3C,QAAO;EACL,MAAM;EACN,MAAM,OAAO;AAEX,SAAM,UAAU,EAAE,QAAQ,mBAAmB,GAAG,UAAU;IACxD,MAAM,KAAK;IACX,WAAW;IACZ,EAAE;AAEH,SAAM,OAAO;IAAE,QAAQ;IAAM,WAAW;IAAgB,SAAS;IAO/D,UAAU;;;;;;;;;;IAUV,QAAQ;IACT,EAAE;;EAEN;;;;;;;;;;;AAYH,eAAsB,eAAe,SAAiB,MAA6C;CACjG,MAAM,aAAa,MAAM,cAAc;CACvC,MAAM,iBAAiB,MAAM,kBAAkB,EAAE;CAKjD,MAAM,SAAS,OAFC,MAAM,OAAO,YAEA,MAAM;EACjC,aAAa,CAAC,QAAQ;EACtB,QAAQ;EACR,QAAQ;EACR;EACA,UAAU;EACV,QAAQ;EACR,OAAO;EACP,SAAS,CAAC,mBAAmB,CAAC;EAG9B,UAAU;EAEV,aAAa;EACd,CAAC;CAEF,MAAM,WAAW,OAAO,SAAS,KAC9B,MACC,GAAG,KAAK,SAAS,QAAQ,KAAK,EAAE,EAAE,UAAU,QAAQ,GAAG,CAAC,GAAG,EAAE,UAAU,QAAQ,IAAI,IAAI,EAAE,OAC5F;CAED,MAAM,aAAa,OAAO,cAAc;AACxC,KAAI,CAAC,WACH,OAAM,IAAI,MAAM,iEAAiE;AAGnF,QAAO;EAAE,MAAM,WAAW;EAAM;EAAU"}