@ait-co/devtools 0.1.121 → 0.1.123

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 (86) hide show
  1. package/dist/{attach-orchestrator-DEJfLm7V.js → attach-orchestrator-CPMQWkGV.js} +3 -3
  2. package/dist/{attach-orchestrator-DEJfLm7V.js.map → attach-orchestrator-CPMQWkGV.js.map} +1 -1
  3. package/dist/{attach-orchestrator-qFEMBUwb.js → attach-orchestrator-CpE0dMew.js} +2 -2
  4. package/dist/{attach-orchestrator-qFEMBUwb.js.map → attach-orchestrator-CpE0dMew.js.map} +1 -1
  5. package/dist/{attach-orchestrator-CFotueL3.js → attach-orchestrator-RhOdO3DS.js} +2 -2
  6. package/dist/{attach-orchestrator-CFotueL3.js.map → attach-orchestrator-RhOdO3DS.js.map} +1 -1
  7. package/dist/{bundle-CA1TCXED.d.ts → bundle-Cb3GFuw6.d.ts} +1 -1
  8. package/dist/{bundle-CA1TCXED.d.ts.map → bundle-Cb3GFuw6.d.ts.map} +1 -1
  9. package/dist/{capture-BmK_cBqQ.d.ts → capture-B1zfGyTo.d.ts} +1 -1
  10. package/dist/{capture-BmK_cBqQ.d.ts.map → capture-B1zfGyTo.d.ts.map} +1 -1
  11. package/dist/{cdp-connection-DB2zgthr.d.ts → cdp-connection-Dxw_EY8s.d.ts} +1 -1
  12. package/dist/{cdp-connection-DB2zgthr.d.ts.map → cdp-connection-Dxw_EY8s.d.ts.map} +1 -1
  13. package/dist/{cell-Cv3e_qwn.js → cell-Ba9lCI3B.js} +3 -3
  14. package/dist/{cell-BPWZUm_X.js.map → cell-Ba9lCI3B.js.map} +1 -1
  15. package/dist/{cell-BPWZUm_X.js → cell-CX92lDI0.js} +3 -3
  16. package/dist/{cell-Cv3e_qwn.js.map → cell-CX92lDI0.js.map} +1 -1
  17. package/dist/{cell-nrWFeqvG.js → cell-D0dJSGHi.js} +3 -3
  18. package/dist/{cell-Cj-u6JTG.js.map → cell-D0dJSGHi.js.map} +1 -1
  19. package/dist/{debug-server-BhHVy3XT.js → debug-server-Bup2AJ5H.js} +7 -226
  20. package/dist/debug-server-Bup2AJ5H.js.map +1 -0
  21. package/dist/{debug-server-Buc8yoa7.js → debug-server-C0A_Kujq.js} +8 -945
  22. package/dist/debug-server-C0A_Kujq.js.map +1 -0
  23. package/dist/{debug-server-C_5ag9jj.js → debug-server-DywH5GU-.js} +5 -4
  24. package/dist/{debug-server-C_5ag9jj.js.map → debug-server-DywH5GU-.js.map} +1 -1
  25. package/dist/mcp/cli.js +8204 -4
  26. package/dist/mcp/cli.js.map +1 -1
  27. package/dist/mcp/server.js +1 -1
  28. package/dist/panel/index.js +1 -1
  29. package/dist/{pool-D5QH6Ydv.d.ts → pool-B9wSRPOU.d.ts} +3 -3
  30. package/dist/{pool-D5QH6Ydv.d.ts.map → pool-B9wSRPOU.d.ts.map} +1 -1
  31. package/dist/qr-http-server-CKbtqvKy.js +1505 -0
  32. package/dist/qr-http-server-CKbtqvKy.js.map +1 -0
  33. package/dist/qr-http-server-DDmFS7fX.js +1506 -0
  34. package/dist/qr-http-server-DDmFS7fX.js.map +1 -0
  35. package/dist/{relay-factory-DVI7TrX4.js → relay-factory-DnHMDxMA.js} +25 -5
  36. package/dist/relay-factory-DnHMDxMA.js.map +1 -0
  37. package/dist/{relay-secret-store-DNPJKHNs.js → relay-secret-store-DKxs7zwq.js} +1 -1
  38. package/dist/{relay-secret-store-DNPJKHNs.js.map → relay-secret-store-DKxs7zwq.js.map} +1 -1
  39. package/dist/{relay-secret-store-CmDDfZMh.js → relay-secret-store-DhzAnnj-.js} +3 -3
  40. package/dist/{relay-secret-store-CmDDfZMh.js.map → relay-secret-store-DhzAnnj-.js.map} +1 -1
  41. package/dist/{relay-url-store-CnL2zwbH.js → relay-url-store-CwKT7i04.js} +2 -2
  42. package/dist/{relay-url-store-CnL2zwbH.js.map → relay-url-store-CwKT7i04.js.map} +1 -1
  43. package/dist/{relay-url-store-Dv5HSzwb.js → relay-url-store-xmUuTjXA.js} +2 -2
  44. package/dist/{relay-url-store-Dv5HSzwb.js.map → relay-url-store-xmUuTjXA.js.map} +1 -1
  45. package/dist/{relay-worker-BBu1oEqC.d.ts → relay-worker-7biDlEjo.d.ts} +5 -5
  46. package/dist/{relay-worker-BBu1oEqC.d.ts.map → relay-worker-7biDlEjo.d.ts.map} +1 -1
  47. package/dist/{runtime-Bm_sT1C9.d.ts → runtime-ozyPnRbU.d.ts} +1 -1
  48. package/dist/{runtime-Bm_sT1C9.d.ts.map → runtime-ozyPnRbU.d.ts.map} +1 -1
  49. package/dist/test-runner/bin.d.ts +2 -0
  50. package/dist/{cli-RhzASQef.js → test-runner/bin.js} +50 -7
  51. package/dist/test-runner/bin.js.map +1 -0
  52. package/dist/test-runner/bundle.d.ts +1 -1
  53. package/dist/test-runner/capture.d.ts +1 -1
  54. package/dist/test-runner/config.d.ts +1 -1
  55. package/dist/test-runner/config.d.ts.map +1 -1
  56. package/dist/test-runner/config.js +1 -1
  57. package/dist/test-runner/pool.d.ts +1 -1
  58. package/dist/test-runner/relay-factory.d.ts.map +1 -1
  59. package/dist/test-runner/relay-factory.js +24 -4
  60. package/dist/test-runner/relay-factory.js.map +1 -1
  61. package/dist/test-runner/relay-worker.d.ts +1 -1
  62. package/dist/test-runner/report.d.ts +3 -3
  63. package/dist/test-runner/rpc.d.ts +2 -2
  64. package/dist/test-runner/runtime.d.ts +1 -1
  65. package/dist/test-runner/task-graph.d.ts +1 -1
  66. package/dist/totp-D1pulXLa.js +3 -0
  67. package/dist/{totp-0bvlo9Yb.js → totp-WY6l0ysP.js} +1 -1
  68. package/dist/{totp-0bvlo9Yb.js.map → totp-WY6l0ysP.js.map} +1 -1
  69. package/package.json +2 -2
  70. package/dist/attach-orchestrator-BTsEHXfq.js +0 -3
  71. package/dist/attach-orchestrator-DPMBN06p.js +0 -2797
  72. package/dist/attach-orchestrator-DPMBN06p.js.map +0 -1
  73. package/dist/cell-Bca_k5ne.js +0 -3
  74. package/dist/cell-Cj-u6JTG.js +0 -85
  75. package/dist/cell-nrWFeqvG.js.map +0 -1
  76. package/dist/cli-RhzASQef.js.map +0 -1
  77. package/dist/debug-server-BhHVy3XT.js.map +0 -1
  78. package/dist/debug-server-Buc8yoa7.js.map +0 -1
  79. package/dist/debug-server-m9lIC9YW.js +0 -5758
  80. package/dist/debug-server-m9lIC9YW.js.map +0 -1
  81. package/dist/relay-factory-DVI7TrX4.js.map +0 -1
  82. package/dist/relay-secret-store-BlFEhqwb.js +0 -3
  83. package/dist/test-runner/cli.d.ts +0 -547
  84. package/dist/test-runner/cli.d.ts.map +0 -1
  85. package/dist/test-runner/cli.js +0 -3
  86. package/dist/totp-zGIlDsZS.js +0 -3
@@ -1,783 +1,18 @@
1
1
  import { n as buildRelayVerifyAuth, r as generateTotp, t as assertRelayAuthConfigured } from "./totp-DfekTBk3.js";
2
- import { createRelayConnectionFactory } from "./test-runner/relay-factory.js";
3
- import { a as startTunnelHealthProbe, i as startQuickTunnel, n as makeTunnelStatus, o as logError, r as printAttachBanner, s as logInfo, t as generateAttachToken } from "./attach-orchestrator-DEJfLm7V.js";
4
- import "./cell-Cv3e_qwn.js";
5
- import "./relay-secret-store-DNPJKHNs.js";
2
+ import "./test-runner/relay-worker.js";
3
+ import "./relay-factory-DnHMDxMA.js";
4
+ import { a as startTunnelHealthProbe, i as startQuickTunnel, n as makeTunnelStatus, o as logError, r as printAttachBanner, s as logInfo, t as generateAttachToken } from "./attach-orchestrator-CPMQWkGV.js";
5
+ import "./cell-D0dJSGHi.js";
6
+ import "./qr-http-server-CKbtqvKy.js";
7
+ import "./relay-secret-store-DKxs7zwq.js";
6
8
  import { createRequire } from "node:module";
7
- import { accessSync } from "node:fs";
8
- import * as path$1 from "node:path";
9
- import path, { isAbsolute, resolve } from "node:path";
10
9
  import "@modelcontextprotocol/sdk/server/index.js";
11
10
  import "@modelcontextprotocol/sdk/server/stdio.js";
12
11
  import "@modelcontextprotocol/sdk/types.js";
13
- import { parseArgs } from "node:util";
14
- import * as fs from "node:fs/promises";
15
- import { glob, mkdir, writeFile } from "node:fs/promises";
16
- import { fileURLToPath } from "node:url";
17
12
  import { EventEmitter } from "node:events";
18
13
  import { WebSocket, WebSocketServer } from "ws";
19
14
  import { createServer } from "node:http";
20
- //#region src/test-runner/discover.ts
21
- /**
22
- * Test-file discovery shared by the `devtools-test` CLI and the `run_tests`
23
- * MCP tool, so both expand glob patterns with identical semantics.
24
- *
25
- * Uses Node's built-in `fs/promises` `glob` (Node 22+) — no extra dependency,
26
- * which keeps the MCP daemon install graph lean (a plain glob lib would land in
27
- * the `npx … devtools-mcp` path for no benefit).
28
- *
29
- * Pure Node IO only (`node:fs/promises` + `node:path`) — react-free, so it is
30
- * safe to import from the MCP daemon graph.
31
- */
32
- /**
33
- * Expands `patterns` (globs or plain paths) into a sorted, de-duplicated list of
34
- * ABSOLUTE test file paths, resolved relative to `cwd`.
35
- *
36
- * A plain (non-glob) path passes through when it matches a real file; a glob
37
- * expands against `cwd`. Absolute matches are kept as-is; relative matches are
38
- * resolved against `cwd`. `bundleTestFile` requires an absolute path, so the
39
- * absolute output feeds it directly.
40
- *
41
- * @param patterns Glob patterns or file paths (e.g. `['src/**\/*.ait.test.ts']`).
42
- * @param cwd Base directory for relative patterns/results.
43
- * @returns Sorted, de-duplicated absolute file paths. Empty when nothing matches.
44
- */
45
- async function discoverTestFiles(patterns, cwd) {
46
- const out = /* @__PURE__ */ new Set();
47
- for await (const match of glob(patterns, { cwd })) out.add(isAbsolute(match) ? match : resolve(cwd, match));
48
- return [...out].sort();
49
- }
50
- //#endregion
51
- //#region src/test-runner/bundle.ts
52
- /**
53
- * esbuild-based bundler for user test files.
54
- *
55
- * Bundles a single test file into a self-contained IIFE string that can be
56
- * injected into a WebView via `Runtime.evaluate`. The bundle includes the
57
- * test runtime (`runtime.ts`), which provides `describe/it/test/expect` and
58
- * the `runTestModule(factory)` entry point.
59
- *
60
- * ## How the wiring works
61
- *
62
- * The bundle exposes two exports on `globalThis.__testBundle`:
63
- * - `runTestModule` — the runtime's entry function.
64
- * - `__userFactory` — an async function whose body is the user's top-level
65
- * test registration code (describe/it/test calls).
66
- *
67
- * The Node-side RPC (`rpc.ts`) calls:
68
- * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`
69
- *
70
- * `runTestModule` then installs `describe/it/test/expect` as globals, invokes
71
- * the factory (which registers all tests), runs them, and returns a `RunReport`.
72
- *
73
- * ## Why a factory wrapper is needed
74
- *
75
- * Naively adding the runtime to `entryPoints` and bundling the user file would
76
- * fail for two reasons:
77
- * 1. `describe/it/test/expect` from the runtime are module-local in the IIFE
78
- * scope. The user's top-level `describe(...)` calls expect them as globals —
79
- * they are not globals until `runTestModule` installs them.
80
- * 2. Even with globals pre-installed, the user file runs at IIFE-evaluation
81
- * time, before the RPC layer calls `runTestModule` to reset state and start
82
- * the test clock.
83
- *
84
- * The factory approach solves both: the user's registration code is deferred
85
- * into a function that `runTestModule` calls AFTER installing the globals.
86
- *
87
- * ## Factory extraction algorithm
88
- *
89
- * The `userFactoryPlugin` reads the user file and splits lines into:
90
- * - **top-level**: `import …` and re-export lines — kept at module scope
91
- * (the only valid position for static `import` in ESM).
92
- * - **body**: all other statements — moved into the body of the exported
93
- * `__userFactory` async function.
94
- *
95
- * esbuild processes the re-generated module, following each static import
96
- * through the normal dependency graph (including the SDK-redirect plugin).
97
- *
98
- * ## SDK redirect
99
- *
100
- * Imports of `@apps-in-toss/web-framework` (and sub-paths) are intercepted via
101
- * the `sdkRedirectPlugin` and replaced with a virtual `window.__sdk` proxy that
102
- * `src/in-app/auto.ts` installs at runtime. This works for both 2.x and 3.x SDK.
103
- *
104
- * SECRET-HANDLING: the returned bundle code is caller-managed; never log it.
105
- */
106
- /** The SDK package name that mini-app test code imports from. */
107
- const SDK_PACKAGE = "@apps-in-toss/web-framework";
108
- /**
109
- * Names the runtime installs as globals before invoking the user factory.
110
- * The `vitest` virtual module re-exports each as a lazy getter that reads from
111
- * `globalThis` at access time. Keep in sync with the globals installed in
112
- * `runtime.ts#runTestModule`.
113
- */
114
- const VITEST_GLOBAL_NAMES = [
115
- "describe",
116
- "it",
117
- "test",
118
- "expect",
119
- "beforeAll",
120
- "afterAll",
121
- "beforeEach",
122
- "afterEach",
123
- "vi"
124
- ];
125
- /**
126
- * Matches the bare SDK package and any sub-path import
127
- * (`@apps-in-toss/web-framework`, `@apps-in-toss/web-framework/foo`).
128
- * Built from {@link SDK_PACKAGE} so the package name has a single source.
129
- */
130
- const SDK_IMPORT_FILTER = new RegExp(`^${SDK_PACKAGE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
131
- /**
132
- * esbuild plugin that intercepts SDK imports and redirects them to the
133
- * `window.__sdk` proxy that `src/in-app/auto.ts` installs at runtime.
134
- *
135
- * Strategy: for every import of `@apps-in-toss/web-framework` (or sub-paths),
136
- * esbuild resolves it to a virtual module that re-exports all named exports
137
- * via `window.__sdk[name]`. This avoids bundling the real SDK (which may not
138
- * be available in the test environment) while still making named imports work.
139
- *
140
- * If `window.__sdk` is absent (non-dog-food build), every access throws a
141
- * descriptive error rather than returning `undefined` silently.
142
- */
143
- function sdkRedirectPlugin() {
144
- return {
145
- name: "sdk-redirect",
146
- setup(build) {
147
- build.onResolve({ filter: SDK_IMPORT_FILTER }, (args) => ({
148
- path: args.path,
149
- namespace: "sdk-redirect"
150
- }));
151
- build.onLoad({
152
- filter: /.*/,
153
- namespace: "sdk-redirect"
154
- }, () => ({
155
- contents: `
156
- var __proxy = (typeof window !== 'undefined' && window.__sdk)
157
- ? window.__sdk
158
- : new Proxy({}, {
159
- get: function(_t, p) {
160
- throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));
161
- }
162
- });
163
- module.exports = __proxy;
164
- `,
165
- loader: "js"
166
- }));
167
- }
168
- };
169
- }
170
- /**
171
- * esbuild plugin that intercepts `import … from 'vitest'` and replaces it with
172
- * a virtual module that delegates every named import to `globalThis` at ACCESS
173
- * time (not at bundle-evaluation time).
174
- *
175
- * The runtime installs `describe/it/test/expect/beforeAll/afterAll/beforeEach/
176
- * afterEach/vi` as globals inside `runTestModule`, which runs AFTER the bundle
177
- * IIFE is evaluated. A value-copy redirect (`export var describe =
178
- * globalThis.describe`) would therefore capture `undefined` at evaluation time
179
- * and the user's `describe(...)` calls would be no-ops — registering zero tests.
180
- *
181
- * The fix defers the lookup to call time using per-name **getter** exports.
182
- * We emit a CommonJS module that:
183
- * 1. sets `__esModule = true` so esbuild's `__toESM` interop maps each named
184
- * import directly to a property access on the module (NOT wrapped under a
185
- * `default` shim — which is what happens for a bare Proxy whose own-keys
186
- * are empty, leaving every named import `undefined`);
187
- * 2. defines each global name as a getter that reads `globalThis[name]` on
188
- * every access. So `import { describe } from 'vitest'` compiles to
189
- * `import_vitest.describe`, whose getter returns the real `describe` only
190
- * when the factory calls it — after `runTestModule` installs the globals.
191
- *
192
- * A plain `module.exports = new Proxy(...)` does NOT work here: esbuild routes
193
- * the virtual module through `__toESM`, which enumerates own-keys (none on an
194
- * empty Proxy target) and therefore exposes zero named exports. Explicit getter
195
- * properties give `__toESM` real keys to map while keeping access lazy.
196
- */
197
- function vitestRedirectPlugin() {
198
- return {
199
- name: "vitest-redirect",
200
- setup(build) {
201
- build.onResolve({ filter: /^vitest$/ }, () => ({
202
- path: "vitest",
203
- namespace: "vitest-redirect"
204
- }));
205
- build.onLoad({
206
- filter: /^vitest$/,
207
- namespace: "vitest-redirect"
208
- }, () => {
209
- return {
210
- contents: `Object.defineProperty(exports, '__esModule', { value: true });\n${VITEST_GLOBAL_NAMES.map((name) => `Object.defineProperty(exports, ${JSON.stringify(name)}, { enumerable: true, get: function() { return globalThis[${JSON.stringify(name)}]; } });`).join("\n")}\n`,
211
- loader: "js"
212
- };
213
- });
214
- }
215
- };
216
- }
217
- /**
218
- * esbuild plugin that transforms the user test file into a module that exports
219
- * an async `__userFactory` function. The factory defers the user's top-level
220
- * test registration code (describe/it/test calls) so it only runs when
221
- * `runTestModule(__userFactory)` explicitly invokes it — AFTER the runtime has
222
- * installed describe/it/test/expect as globals.
223
- *
224
- * Algorithm:
225
- * - Import declarations and re-export statements are kept at module top-level
226
- * (the only valid ESM position for static `import`). A statement that spans
227
- * multiple lines — e.g. a named import with one member per line:
228
- * import {
229
- * appLogin,
230
- * getAnonymousKey,
231
- * } from '@apps-in-toss/web-framework';
232
- * is tracked as a single block: every line from the opening `import {` /
233
- * `export {` through the closing `from '…'` (or side-effect `'…'`) line is
234
- * kept together at top-level. This prevents the member lines and the
235
- * closing `} from '…'` line from leaking into the factory body, which would
236
- * leave an unterminated `import {` at module scope (the #678 env3 failure:
237
- * esbuild threw `Expected "as" but found "{"` on multi-line SDK imports).
238
- * - All other lines (describe/it/test calls, local declarations, etc.) are
239
- * moved into the body of the exported async factory function.
240
- *
241
- * This preserves SDK import resolution (the sdk-redirect plugin processes
242
- * top-level imports normally) while deferring test registration to the factory.
243
- */
244
- function userFactoryPlugin(absPath) {
245
- const NAMESPACE = "user-test-factory";
246
- return {
247
- name: "user-test-factory",
248
- setup(build) {
249
- build.onResolve({ filter: /^user-test-factory$/ }, () => ({
250
- path: absPath,
251
- namespace: NAMESPACE
252
- }));
253
- build.onLoad({
254
- filter: /.*/,
255
- namespace: NAMESPACE
256
- }, async (args) => {
257
- const lines = (await fs.readFile(args.path, "utf8")).split("\n");
258
- const topLevelLines = [];
259
- const bodyLines = [];
260
- const EXPORT_DECLARATION_RE = /^(export\s+)(default\s+|async\s+function\s+|function\s+|class\s+|const\s+|let\s+|var\s+)/;
261
- const isImportStart = (trimmed) => trimmed.startsWith("import ") || trimmed.startsWith("import{") || trimmed.startsWith("import'") || trimmed.startsWith("import\"");
262
- const endsStatement = (trimmed) => /['"]\s*;?\s*$/.test(trimmed.replace(/\/\/.*$/, "").trimEnd());
263
- let inImportBlock = false;
264
- for (const line of lines) {
265
- const trimmed = line.trimStart();
266
- const indent = line.slice(0, line.length - trimmed.length);
267
- if (inImportBlock) {
268
- topLevelLines.push(line);
269
- if (endsStatement(trimmed)) inImportBlock = false;
270
- continue;
271
- }
272
- if (isImportStart(trimmed)) {
273
- topLevelLines.push(line);
274
- if (!endsStatement(trimmed)) inImportBlock = true;
275
- } else if (trimmed.startsWith("export ")) if (trimmed.match(EXPORT_DECLARATION_RE)) bodyLines.push(indent + trimmed.slice(7));
276
- else {
277
- topLevelLines.push(line);
278
- if (/\bfrom\b/.test(trimmed) ? !endsStatement(trimmed) : trimmed.endsWith("{")) inImportBlock = true;
279
- }
280
- else bodyLines.push(line);
281
- }
282
- return {
283
- contents: [
284
- ...topLevelLines,
285
- "",
286
- "// biome-ignore lint: generated factory wrapper",
287
- "export default async function __userFactory(): Promise<void> {",
288
- ...bodyLines.map((l) => ` ${l}`),
289
- "}"
290
- ].join("\n"),
291
- loader: "ts",
292
- resolveDir: path$1.dirname(absPath)
293
- };
294
- });
295
- }
296
- };
297
- }
298
- /**
299
- * Returns the absolute filesystem path to the test-runner runtime module
300
- * (dist/test-runner/runtime.js — a fully self-contained page-side bundle).
301
- *
302
- * Rolldown code-splitting duplicates this bundling logic into shared chunks
303
- * emitted at ARBITRARY dist depths: the `devtools-test` CLI pulls it from
304
- * dist/test-runner/bundle.js (dir = dist/test-runner/), while the `devtools-mcp`
305
- * daemon (dist/mcp/cli.js) pulls it through a ROOT chunk
306
- * (dist/debug-server-<hash>.js, dir = dist/). A fixed `..`-hop candidate list
307
- * is therefore wrong from at least one chunk — the live #697 regression.
308
- *
309
- * This resolves WITHOUT assuming chunk depth: from `import.meta.url`'s dir it
310
- * probes the co-located `runtime.js` and the nested `test-runner/runtime.js`,
311
- * then ascends one directory at a time (bounded) repeating both probes. The
312
- * nested probe catches dist/test-runner/runtime.js from the dist/ root level no
313
- * matter which depth the chunk was hoisted to (root, dist/mcp/, or a future
314
- * relocation). The build always emits dist/test-runner/runtime.js (tsdown entry
315
- * `'test-runner/runtime'`; guarded by scripts/check-test-runner-dist.sh).
316
- *
317
- * An ABSOLUTE path is returned deliberately: esbuild loads it as a literal file
318
- * read, bypassing Node module resolution entirely, so this works identically in
319
- * the npx-daemon context (its own dist tree) and the consumer-CLI context
320
- * (the mini-app's installed @ait-co/devtools dist) — neither needs the package
321
- * to be node-resolvable from the caller.
322
- */
323
- function getRuntimePath() {
324
- const startDir = path$1.dirname(fileURLToPath(import.meta.url));
325
- const RELATIVE_PROBES = [
326
- ["runtime.js"],
327
- ["test-runner", "runtime.js"],
328
- ["runtime.ts"],
329
- ["test-runner", "runtime.ts"]
330
- ];
331
- let dir = startDir;
332
- for (let i = 0; i < 12; i++) {
333
- for (const segs of RELATIVE_PROBES) {
334
- const candidate = path$1.join(dir, ...segs);
335
- try {
336
- accessSync(candidate);
337
- return candidate;
338
- } catch {}
339
- }
340
- const parent = path$1.dirname(dir);
341
- if (parent === dir) break;
342
- dir = parent;
343
- }
344
- return path$1.join(startDir, "runtime.js");
345
- }
346
- /**
347
- * Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.
348
- *
349
- * The IIFE installs `window.__testBundle` (or the custom `globalName`) with:
350
- * - `runTestModule` — the runtime entry (from `runtime.ts`).
351
- * - `__userFactory` — an async function wrapping the user's test registration
352
- * code so it runs AFTER `runTestModule` installs the globals.
353
- *
354
- * Callers (rpc.ts) invoke:
355
- * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`
356
- *
357
- * @param absPath - Absolute path to the user test file.
358
- * @param opts - Optional bundling overrides.
359
- */
360
- async function bundleTestFile(absPath, opts) {
361
- const globalName = opts?.globalName ?? "__testBundle";
362
- const extraExternals = opts?.extraExternals ?? [];
363
- const esbuild = await import("esbuild");
364
- const runtimePath = getRuntimePath();
365
- const wrapperContent = [
366
- `import { runTestModule } from ${JSON.stringify(runtimePath)};`,
367
- `import __userFactory from "user-test-factory";`,
368
- `export { runTestModule, __userFactory };`
369
- ].join("\n");
370
- const result = await esbuild.build({
371
- stdin: {
372
- contents: wrapperContent,
373
- loader: "ts",
374
- resolveDir: path$1.dirname(absPath)
375
- },
376
- bundle: true,
377
- format: "iife",
378
- globalName,
379
- platform: "browser",
380
- target: "es2022",
381
- write: false,
382
- plugins: [
383
- userFactoryPlugin(absPath),
384
- vitestRedirectPlugin(),
385
- sdkRedirectPlugin()
386
- ],
387
- external: extraExternals,
388
- treeShaking: true,
389
- footer: { js: `globalThis[${JSON.stringify(globalName)}] = ${globalName};` }
390
- });
391
- const warnings = result.warnings.map((w) => `${path$1.relative(process.cwd(), w.location?.file ?? "")}:${w.location?.line ?? "?"}: ${w.text}`);
392
- const outputFile = result.outputFiles?.[0];
393
- if (!outputFile) throw new Error("bundleTestFile: esbuild produced no output — check entryPoints");
394
- return {
395
- code: outputFile.text,
396
- warnings
397
- };
398
- }
399
- //#endregion
400
- //#region src/test-runner/capture.ts
401
- /** The exact console-line prefix sdk-example's `flushCapture` emits. */
402
- const CAPTURE_PREFIX = "__AIT_CAPTURE__ ";
403
- /**
404
- * Parses raw console line texts into {@link AitCaptureLine}s.
405
- *
406
- * Filtering rules (each independently drops a line — never throws):
407
- * - the line text must `startsWith(CAPTURE_PREFIX)` exactly (allowlist);
408
- * - there must be a non-empty category token (up to the next space);
409
- * - the remaining payload must be valid JSON (`JSON.parse` succeeds).
410
- *
411
- * Lines that fail any rule (wss/scheme noise, truncated, broken JSON) are
412
- * silently discarded — capture harvesting is best-effort and must never fail a
413
- * run or leak a malformed/secret-bearing line.
414
- *
415
- * @param raw - Console line objects (only `.text` is read).
416
- * @returns The captured lines, in input order.
417
- */
418
- function parseCaptureLines(raw) {
419
- const out = [];
420
- for (const { text } of raw) {
421
- if (!text.startsWith(CAPTURE_PREFIX)) continue;
422
- const body = text.slice(16);
423
- const spaceIdx = body.indexOf(" ");
424
- if (spaceIdx === -1) continue;
425
- const category = body.slice(0, spaceIdx);
426
- const json = body.slice(spaceIdx + 1);
427
- if (category === "" || json === "") continue;
428
- try {
429
- JSON.parse(json);
430
- } catch {
431
- continue;
432
- }
433
- out.push({
434
- category,
435
- json
436
- });
437
- }
438
- return out;
439
- }
440
- //#endregion
441
- //#region src/test-runner/rpc.ts
442
- /** Maximum milliseconds to wait for a single evaluate round-trip. */
443
- const DEFAULT_TIMEOUT_MS = 3e4;
444
- /**
445
- * Wraps bundle code in a self-executing IIFE that:
446
- * 1. Evaluates the bundle (registering describe/it/test).
447
- * 2. Calls `__testBundle.runTestModule(...)` — the entry the runtime exports.
448
- * 3. Returns a JSON-serialised `RunReport` string.
449
- *
450
- * The double-serialisation (RunReport → JSON string → returnByValue string)
451
- * is intentional: CDP `returnByValue` reliably transports strings; deeply
452
- * nested objects can lose fidelity across the Chii relay.
453
- *
454
- * SECRET-HANDLING: `bundleCode` MUST NOT be logged by callers.
455
- */
456
- function buildRunTestsExpression(bundleCode) {
457
- return `(async () => { try { ${bundleCode} } catch(e) { return JSON.stringify({ok:false,error:'bundle-eval: ' + String(e && e.message || e)}); } if (typeof globalThis.__testBundle !== 'object' || typeof globalThis.__testBundle.runTestModule !== 'function' || typeof globalThis.__testBundle.__userFactory !== 'function') { return JSON.stringify({ok:false,error:'bundle-missing-export: __testBundle.runTestModule or __userFactory is not a function'}); } try { const report = await globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory); return JSON.stringify({ok:true,value:report}); } catch(e) { return JSON.stringify({ok:false,error:'test-run: ' + String(e && e.message || e)}); }})()`;
458
- }
459
- /**
460
- * Parses the raw CDP `returnByValue` result from a `buildRunTestsExpression`
461
- * evaluate call into a typed `RpcRunResult`.
462
- *
463
- * Throws only on parse failure — an `ok:false` envelope is a normal result.
464
- *
465
- * SECRET-HANDLING: `rawValue` is not included in error messages.
466
- */
467
- function parseRunTestsResult(rawValue) {
468
- if (typeof rawValue !== "string") throw new Error(`rpc.parseRunTestsResult: unexpected return type "${typeof rawValue}" — expected JSON string`);
469
- let parsed;
470
- try {
471
- parsed = JSON.parse(rawValue);
472
- } catch {
473
- throw new Error("rpc.parseRunTestsResult: bridge returned non-JSON string");
474
- }
475
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("rpc.parseRunTestsResult: parsed result is not an object");
476
- const obj = parsed;
477
- if (obj.ok === true) return {
478
- ok: true,
479
- report: obj.value
480
- };
481
- if (obj.ok === false) return {
482
- ok: false,
483
- error: typeof obj.error === "string" ? obj.error : String(obj.error)
484
- };
485
- throw new Error("rpc.parseRunTestsResult: result missing \"ok\" field");
486
- }
487
- /**
488
- * Injects `bundleCode` into the attached page and awaits test execution.
489
- *
490
- * Uses `Runtime.evaluate` with `awaitPromise: true` to wait for the
491
- * async IIFE to settle. The 30-second CDP command timeout covers even
492
- * long-running test suites; split into smaller files if you hit it.
493
- *
494
- * @param connection - Active CDP connection (relay or local).
495
- * @param bundleCode - IIFE bundle string from `bundleTestFile`.
496
- * @param timeoutMs - Override the default 30 s timeout.
497
- *
498
- * SECRET-HANDLING: `bundleCode` and the raw CDP result value are never logged.
499
- */
500
- async function injectAndRunBundle(connection, bundleCode, timeoutMs = DEFAULT_TIMEOUT_MS) {
501
- const expression = buildRunTestsExpression(bundleCode);
502
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error(`rpc: evaluate timed out after ${timeoutMs}ms`)), timeoutMs));
503
- const evalPromise = connection.send("Runtime.evaluate", {
504
- expression,
505
- returnByValue: true,
506
- awaitPromise: true
507
- });
508
- const cdpResult = await Promise.race([evalPromise, timeoutPromise]);
509
- if (cdpResult.exceptionDetails) {
510
- const msg = cdpResult.exceptionDetails.exception?.description ?? cdpResult.exceptionDetails.text ?? "Runtime.evaluate threw an exception";
511
- throw new Error(`rpc.injectAndRunBundle: ${msg}`);
512
- }
513
- return parseRunTestsResult(cdpResult.result.value);
514
- }
515
- //#endregion
516
- //#region src/test-runner/relay-worker.ts
517
- /**
518
- * Runs all `files` sequentially over the given CDP `connection`.
519
- *
520
- * For each file:
521
- * 1. Bundle with esbuild (includes SDK shim + runtime).
522
- * 2. Inject into the attached page via `Runtime.evaluate`.
523
- * 3. Await the `RunReport` JSON response.
524
- * 4. Accumulate results.
525
- *
526
- * Returns a `RelayRunReport` with per-file results and flattened totals.
527
- *
528
- * This function does NOT open or manage the relay connection — the caller
529
- * is responsible for attaching and closing it.
530
- *
531
- * TODO (#645): implement the Vitest `PoolRunnerInitializer` interface here
532
- * so that `runTestFilesOverRelay` can be used as a Vitest pool entry.
533
- *
534
- * @param connection - Active CDP connection (relay or local kind).
535
- * @param files - Absolute paths to test files, run in order.
536
- * @param opts - Optional per-run overrides.
537
- */
538
- async function runTestFilesOverRelay(connection, files, opts) {
539
- const wallStart = Date.now();
540
- const startedAt = new Date(wallStart).toISOString();
541
- const fileResults = [];
542
- let domainsEnabled = false;
543
- try {
544
- await connection.enableDomains();
545
- domainsEnabled = true;
546
- } catch (e) {
547
- process.stderr.write(`relay-worker: enableDomains() failed before run — console capture may be empty (${e instanceof Error ? e.message : String(e)})\n`);
548
- }
549
- const collectCaptures = opts?.collectCaptures === true;
550
- const liveConsole = [];
551
- let unsubscribeConsole;
552
- if (collectCaptures && domainsEnabled) unsubscribeConsole = connection.on("Runtime.consoleAPICalled", (event) => {
553
- liveConsole.push(event);
554
- });
555
- try {
556
- for (const file of files) {
557
- let fileEntry;
558
- try {
559
- const { code } = await bundleTestFile(file, opts?.bundleOptions);
560
- const rpcResult = await injectAndRunBundle(connection, code, opts?.timeoutMs);
561
- if (rpcResult.ok) fileEntry = {
562
- file,
563
- result: rpcResult.report
564
- };
565
- else fileEntry = {
566
- file,
567
- result: { error: rpcResult.error }
568
- };
569
- } catch (e) {
570
- fileEntry = {
571
- file,
572
- result: { error: e instanceof Error ? e.message : String(e) }
573
- };
574
- }
575
- fileResults.push(fileEntry);
576
- }
577
- } finally {
578
- unsubscribeConsole?.();
579
- }
580
- const captures = collectCaptures ? parseCaptureLines(liveConsole.map((e) => ({ text: renderConsoleLineText(e) }))) : [];
581
- const totals = fileResults.reduce((acc, { result }) => {
582
- if ("error" in result) {
583
- acc.failed += 1;
584
- acc.total += 1;
585
- } else {
586
- acc.passed += result.passed;
587
- acc.failed += result.failed;
588
- acc.skipped += result.skipped;
589
- acc.total += result.passed + result.failed + result.skipped;
590
- }
591
- return acc;
592
- }, {
593
- passed: 0,
594
- failed: 0,
595
- skipped: 0,
596
- total: 0
597
- });
598
- return {
599
- startedAt,
600
- duration: Date.now() - wallStart,
601
- files: fileResults,
602
- totals,
603
- captures
604
- };
605
- }
606
- /**
607
- * Renders one `Runtime.consoleAPICalled` event to a single line of text, the
608
- * same way `tools.ts#normalizeConsoleMessage` does (args rendered + space-
609
- * joined). Inlined here (≈8 lines) so this module avoids importing `tools.ts`,
610
- * which would drag the heavy MCP/Node graph (server-lock, parent-watcher, …)
611
- * onto the test-runner entry.
612
- *
613
- * SECRET-HANDLING: this only stringifies console args; the caller's
614
- * allowlist-prefix parser then discards everything that is not a genuine
615
- * `__AIT_CAPTURE__` line.
616
- */
617
- function renderConsoleLineText(event) {
618
- return event.args.map((arg) => {
619
- if (arg.value !== void 0) {
620
- if (typeof arg.value === "string") return arg.value;
621
- try {
622
- return JSON.stringify(arg.value);
623
- } catch {
624
- return String(arg.value);
625
- }
626
- }
627
- if (arg.description !== void 0) return arg.description;
628
- if (arg.className !== void 0) return arg.className;
629
- return arg.subtype ?? arg.type;
630
- }).join(" ");
631
- }
632
- //#endregion
633
- //#region src/test-runner/report.ts
634
- /**
635
- * Runner-agnostic report serialisation for env3 test runs (devtools#696).
636
- *
637
- * Both env3 execution paths — the Vitest custom pool (`pool.ts`) and the
638
- * standalone `devtools-test` CLI (`cli.ts`) — call the same core
639
- * `runTestFilesOverRelay` and so produce the same {@link RelayRunReport}. This
640
- * module is the single, runner-neutral place that turns that in-memory report
641
- * into a stable on-disk artifact so a 2.x run and a 3.0 run can be diffed
642
- * cell-by-cell after the fact.
643
- *
644
- * The serialised schema is deliberately MINIMAL and secret-free:
645
- *
646
- * - file paths are stored RELATIVE to `projectRoot` (no absolute `/Users/...`
647
- * leakage — see {@link RunnerAgnosticReport.files});
648
- * - the cell metadata (sdkLine/platform) is baked INTO the body, not only the
649
- * filename, so a moved artifact never loses its provenance;
650
- * - NO relay wss / scheme / TOTP / relayUrl fields exist in the schema at all
651
- * (enforced by the type + this comment) — error strings are the matcher
652
- * message only, inherited from rpc.ts which already strips expression/value.
653
- *
654
- * react-free — depends only on the type-level `RelayRunReport` and `node:fs` /
655
- * `node:path`. Safe to bundle without pulling the chii/cloudflared graph.
656
- */
657
- /**
658
- * Converts an absolute (or already-relative) file path to a projectRoot-relative
659
- * one. `path.relative` returns `''` when the paths are equal — guard that to the
660
- * basename so the field is never empty.
661
- *
662
- * SECRET-HANDLING: this is the single choke point that strips absolute project
663
- * paths from the artifact.
664
- */
665
- function relativise(projectRoot, file) {
666
- const rel = path.relative(projectRoot, file);
667
- if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return path.basename(file);
668
- return rel;
669
- }
670
- /**
671
- * Serialises a {@link RelayRunReport} into the runner-agnostic, secret-free
672
- * on-disk shape. Pure — no IO; testable with a plain report + meta.
673
- *
674
- * @param report - The core relay run report.
675
- * @param meta - Cell axes + projectRoot (projectRoot is consumed, not stored).
676
- */
677
- function serializeRelayReport(report, meta) {
678
- return {
679
- cell: {
680
- sdkLine: meta.sdkLine,
681
- platform: meta.platform
682
- },
683
- startedAt: report.startedAt,
684
- duration: report.duration,
685
- totals: report.totals,
686
- files: report.files.map((f) => {
687
- const file = relativise(meta.projectRoot, f.file);
688
- if ("error" in f.result) return {
689
- file,
690
- error: f.result.error
691
- };
692
- return {
693
- file,
694
- duration: f.result.duration,
695
- passed: f.result.passed,
696
- failed: f.result.failed,
697
- skipped: f.result.skipped,
698
- tests: f.result.tests
699
- };
700
- })
701
- };
702
- }
703
- /**
704
- * Writes the serialised report to `<dir>/<sdkLine>.<platform>.json`, creating
705
- * `dir` if needed. Returns the absolute path written.
706
- *
707
- * The cell-suffixed filename keeps 2.x and 3.0 (and per-platform) runs as
708
- * distinct artifacts in the same directory; the same cell metadata is also baked
709
- * into the body so a renamed/moved file still carries its provenance.
710
- *
711
- * SECRET-HANDLING: the written body contains no relay/secret fields (the schema
712
- * has none). `dir`/`projectRoot` are local filesystem paths, never logged here.
713
- *
714
- * @param report - The core relay run report.
715
- * @param dir - Output directory (created recursively if missing).
716
- * @param meta - Cell axes + projectRoot.
717
- * @returns The absolute path of the written file.
718
- */
719
- async function writeReportArtifact(report, dir, meta) {
720
- const serialised = serializeRelayReport(report, meta);
721
- await mkdir(dir, { recursive: true });
722
- const outFile = path.join(dir, `${meta.sdkLine}.${meta.platform}.json`);
723
- await writeFile(outFile, `${JSON.stringify(serialised, null, 2)}\n`, "utf8");
724
- return outFile;
725
- }
726
- /**
727
- * Writes harvested `__AIT_CAPTURE__` lines to per-category files under `dir`,
728
- * named `<category>.<sdkLine>.<platform>.json` — the SAME convention
729
- * sdk-example's env1 `flushCapture` uses on the filesystem, so env1 and env3
730
- * capture artifacts line up for diffing.
731
- *
732
- * Each line's `json` payload is an opaque JSON array of capture records. Lines
733
- * sharing a category are concatenated into one array, in harvest order.
734
- *
735
- * SECRET-HANDLING: only allowlist-prefixed capture lines reach here (the parser
736
- * dropped wss/scheme noise); the `json` payload is written verbatim but is a
737
- * capture record array, not a relay/secret.
738
- *
739
- * @param captures - Parsed capture lines (from `RelayRunReport.captures`).
740
- * @param dir - Output directory (created recursively if missing).
741
- * @param cell - Cell axes for the filename suffix.
742
- * @returns The absolute paths written (one per category), in category order.
743
- */
744
- async function writeCaptureArtifacts(captures, dir, cell) {
745
- if (captures.length === 0) return [];
746
- const byCategory = /* @__PURE__ */ new Map();
747
- for (const { category, json } of captures) {
748
- let merged = byCategory.get(category);
749
- if (!merged) {
750
- merged = [];
751
- byCategory.set(category, merged);
752
- }
753
- const parsed = JSON.parse(json);
754
- if (Array.isArray(parsed)) merged.push(...parsed);
755
- else merged.push(parsed);
756
- }
757
- await mkdir(dir, { recursive: true });
758
- const written = [];
759
- for (const [category, records] of byCategory) {
760
- const outFile = path.join(dir, `${category}.${cell.sdkLine}.${cell.platform}.json`);
761
- await writeFile(outFile, `${JSON.stringify(records, null, 2)}\n`, "utf8");
762
- written.push(outFile);
763
- }
764
- return written;
765
- }
766
- //#endregion
767
- //#region src/test-runner/cli.ts
768
- /**
769
- * `devtools-test` CLI.
770
- *
771
- * Shares test-file discovery with the `run_tests` MCP tool (`discoverTestFiles`)
772
- * and exposes `runWithConnection` — the pure run core that bundles, injects, and
773
- * collects each file over a CDP connection. The CLI's `main()` performs a
774
- * standalone relay attach (boot relay → QR → phone scan → cell inject → run).
775
- *
776
- * NOTE: no shebang in this source file — the tsdown entry's `banner` option
777
- * injects `#!/usr/bin/env node` into the compiled output (same pattern as
778
- * `src/mcp/cli.ts`).
779
- */
780
- const USAGE = `
15
+ `
781
16
  devtools-test — run mini-app tests on a real device WebView over the CDP relay
782
17
 
783
18
  USAGE
@@ -824,178 +59,6 @@ EXAMPLE
824
59
  --timeout 60000
825
60
 
826
61
  `.trimStart();
827
- /**
828
- * Runs `files` over `connection` and returns the aggregate report.
829
- * This pure function is the testable core of the CLI (and is what the
830
- * `run_tests` MCP tool calls against the daemon's attached connection); it is
831
- * separate from `main()` so tests can call it without spawning a subprocess.
832
- */
833
- async function runWithConnection(connection, files, opts) {
834
- const report = await runTestFilesOverRelay(connection, files, opts);
835
- if (opts?.printSummary) {
836
- const { totals } = report;
837
- process.stdout.write(`\ndevtools-test: ${totals.passed} passed, ${totals.failed} failed, ${totals.skipped} skipped (${report.duration}ms)\n`);
838
- }
839
- return report;
840
- }
841
- /**
842
- * Decides whether to suppress the QR/attach block on stdout.
843
- *
844
- * Suppress when EITHER the user passed `--no-qr-stdout`, OR stdout is not a TTY
845
- * / `CI` is set / `AIT_NO_QR_STDOUT` is set (non-interactive — a captured stdout
846
- * must not leak the relay wss + TOTP `at=` code that the QR block encodes). The
847
- * suppression is whole-chunk: `attachUrl` AND `relayUrl` ride in the same block.
848
- *
849
- * Exported for unit testing.
850
- */
851
- function shouldSuppressQr(noQrFlag) {
852
- return noQrFlag || !process.stdout.isTTY || process.env.CI !== void 0 || process.env.AIT_NO_QR_STDOUT !== void 0;
853
- }
854
- /**
855
- * CLI entry point.
856
- *
857
- * Performs a standalone relay attach → run lifecycle, sharing the attach
858
- * assembly with the Vitest pool via `createRelayConnectionFactory` (single
859
- * source — no drift):
860
- *
861
- * 1. Parse args: globs, --timeout, --cell-sdk-line, --cell-platform,
862
- * --scheme-url (required for env3), --report-dir, --no-qr-stdout,
863
- * --headless, --project-root.
864
- * 2. Discover test files; exit 1 if none.
865
- * 3. factory.open() — boot relay → render QR (suppressed on non-interactive
866
- * stdout) → wait for phone → inject cell → enableDomains. Returns the conn.
867
- * 4. runWithConnection(conn, files, { timeoutMs, collectCaptures, printSummary }).
868
- * 5. With --report-dir: write the runner-agnostic report + capture files.
869
- * 6. factory.close(); process.exitCode = failed > 0 ? 1 : 0.
870
- *
871
- * The CLI is not a daemon — no lock, router, SSE, or tools_list is needed.
872
- * Attach timeout exits with code 1; test failures exit with code 1.
873
- *
874
- * SECRET-HANDLING: scheme_url / relay wssUrl / TOTP codes are never written to
875
- * stdout/stderr directly. The QR block (which encodes the TOTP `at=` code) is
876
- * printed only when stdout is interactive AND not suppressed.
877
- */
878
- async function main(argv = process.argv.slice(2)) {
879
- let parsed;
880
- try {
881
- parsed = parseArgs({
882
- args: argv,
883
- options: {
884
- help: {
885
- type: "boolean",
886
- short: "h"
887
- },
888
- timeout: { type: "string" },
889
- "scheme-url": { type: "string" },
890
- "cell-sdk-line": { type: "string" },
891
- "cell-platform": { type: "string" },
892
- "report-dir": { type: "string" },
893
- "no-qr-stdout": { type: "boolean" },
894
- headless: { type: "boolean" },
895
- "project-root": { type: "string" }
896
- },
897
- allowPositionals: true
898
- });
899
- } catch (e) {
900
- process.stderr.write(`devtools-test: ${e instanceof Error ? e.message : String(e)}\n`);
901
- process.exitCode = 1;
902
- return;
903
- }
904
- if (parsed.values.help || argv.length === 0) {
905
- process.stdout.write(USAGE);
906
- return;
907
- }
908
- const vals = parsed.values;
909
- const rawTimeout = typeof vals.timeout === "string" ? vals.timeout : void 0;
910
- const timeoutMs = rawTimeout !== void 0 ? parseInt(rawTimeout, 10) : 3e4;
911
- if (Number.isNaN(timeoutMs) || timeoutMs <= 0) {
912
- process.stderr.write(`devtools-test: --timeout must be a positive integer\n`);
913
- process.exitCode = 1;
914
- return;
915
- }
916
- const schemeUrl = typeof vals["scheme-url"] === "string" ? vals["scheme-url"] : "";
917
- if (schemeUrl === "") {
918
- process.stderr.write("devtools-test: --scheme-url is required for standalone relay attach.\n Pass the intoss-private:// URL from `ait deploy --scheme-only`.\n");
919
- process.exitCode = 1;
920
- return;
921
- }
922
- const headless = vals.headless === true;
923
- const projectRoot = typeof vals["project-root"] === "string" ? vals["project-root"] : process.cwd();
924
- const reportDir = typeof vals["report-dir"] === "string" ? vals["report-dir"] : void 0;
925
- const suppressQr = shouldSuppressQr(vals["no-qr-stdout"] === true);
926
- const cellSdkLine = typeof vals["cell-sdk-line"] === "string" ? vals["cell-sdk-line"] : void 0;
927
- const cellPlatform = typeof vals["cell-platform"] === "string" ? vals["cell-platform"] : process.env.AIT_CELL_PLATFORM;
928
- const hasCell = cellSdkLine !== void 0 || cellPlatform !== void 0;
929
- const cell = {
930
- sdkLine: cellSdkLine ?? "2.x",
931
- platform: cellPlatform ?? "mock"
932
- };
933
- const globs = parsed.positionals;
934
- if (globs.length === 0) {
935
- process.stderr.write(`devtools-test: at least one glob pattern is required\n`);
936
- process.stdout.write(USAGE);
937
- process.exitCode = 1;
938
- return;
939
- }
940
- const files = await discoverTestFiles(globs, process.cwd());
941
- if (files.length === 0) {
942
- process.stderr.write(`devtools-test: no test files matched ${globs.join(", ")}\n`);
943
- process.exitCode = 1;
944
- return;
945
- }
946
- process.stderr.write(`devtools-test: found ${files.length} test file(s)\n`);
947
- if (hasCell) process.stderr.write(`devtools-test: injecting __AIT_CELL__ = ${JSON.stringify(cell)}\n`);
948
- const factory = createRelayConnectionFactory({
949
- schemeUrl,
950
- projectRoot,
951
- timeoutMs,
952
- headless,
953
- cell: hasCell ? cell : void 0,
954
- onQrContent: (chunks) => {
955
- if (suppressQr) {
956
- process.stdout.write("QR suppressed (non-interactive)\n");
957
- return;
958
- }
959
- for (const chunk of chunks) process.stdout.write(`${chunk}\n`);
960
- }
961
- });
962
- let connection;
963
- try {
964
- connection = await factory.open();
965
- } catch (e) {
966
- process.stderr.write(`devtools-test: ${e instanceof Error ? e.message : String(e)}\n`);
967
- process.exitCode = 1;
968
- return;
969
- }
970
- let exitCode = 0;
971
- try {
972
- const report = await runWithConnection(connection, files, {
973
- timeoutMs,
974
- printSummary: true,
975
- collectCaptures: reportDir !== void 0
976
- });
977
- if (reportDir !== void 0) try {
978
- const reportPath = await writeReportArtifact(report, reportDir, {
979
- sdkLine: cell.sdkLine,
980
- platform: cell.platform,
981
- projectRoot
982
- });
983
- process.stderr.write(`devtools-test: wrote report ${reportPath}\n`);
984
- const capturePaths = await writeCaptureArtifacts(report.captures, `${reportDir}/.ait-capture`, cell);
985
- if (capturePaths.length > 0) process.stderr.write(`devtools-test: wrote ${capturePaths.length} capture file(s)\n`);
986
- } catch (e) {
987
- process.stderr.write(`devtools-test: failed to write report artifacts: ${e instanceof Error ? e.message : String(e)}\n`);
988
- }
989
- exitCode = report.totals.failed > 0 ? 1 : 0;
990
- } finally {
991
- await factory.close(connection);
992
- process.exitCode = exitCode;
993
- }
994
- }
995
- if (import.meta.url === new URL(process.argv[1], "file://").href) main().catch((e) => {
996
- process.stderr.write(`devtools-test: unexpected error: ${e instanceof Error ? e.message : String(e)}\n`);
997
- process.exitCode = 1;
998
- });
999
62
  //#endregion
1000
63
  //#region src/shared/relay-auth-close.ts
1001
64
  /**
@@ -1906,4 +969,4 @@ async function bootRelayFamily(options = {}) {
1906
969
  //#endregion
1907
970
  export { bootRelayFamily, buildRelayVerifyAuth };
1908
971
 
1909
- //# sourceMappingURL=debug-server-Buc8yoa7.js.map
972
+ //# sourceMappingURL=debug-server-C0A_Kujq.js.map