@desplega.ai/qa-use 2.14.0 → 2.15.0

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 (127) hide show
  1. package/README.md +23 -0
  2. package/dist/lib/env/index.d.ts +13 -0
  3. package/dist/lib/env/index.d.ts.map +1 -1
  4. package/dist/lib/env/index.js +35 -0
  5. package/dist/lib/env/index.js.map +1 -1
  6. package/dist/lib/env/localhost.d.ts +22 -0
  7. package/dist/lib/env/localhost.d.ts.map +1 -0
  8. package/dist/lib/env/localhost.js +49 -0
  9. package/dist/lib/env/localhost.js.map +1 -0
  10. package/dist/lib/env/paths.d.ts +27 -0
  11. package/dist/lib/env/paths.d.ts.map +1 -0
  12. package/dist/lib/env/paths.js +42 -0
  13. package/dist/lib/env/paths.js.map +1 -0
  14. package/dist/lib/env/sessions.d.ts +55 -0
  15. package/dist/lib/env/sessions.d.ts.map +1 -0
  16. package/dist/lib/env/sessions.js +128 -0
  17. package/dist/lib/env/sessions.js.map +1 -0
  18. package/dist/lib/tunnel/errors.d.ts +61 -0
  19. package/dist/lib/tunnel/errors.d.ts.map +1 -0
  20. package/dist/lib/tunnel/errors.js +152 -0
  21. package/dist/lib/tunnel/errors.js.map +1 -0
  22. package/dist/lib/tunnel/index.d.ts.map +1 -1
  23. package/dist/lib/tunnel/index.js +26 -11
  24. package/dist/lib/tunnel/index.js.map +1 -1
  25. package/dist/lib/tunnel/registry.d.ts +182 -0
  26. package/dist/lib/tunnel/registry.d.ts.map +1 -0
  27. package/dist/lib/tunnel/registry.js +561 -0
  28. package/dist/lib/tunnel/registry.js.map +1 -0
  29. package/dist/package.json +1 -1
  30. package/dist/src/cli/commands/browser/_detached.d.ts +27 -0
  31. package/dist/src/cli/commands/browser/_detached.d.ts.map +1 -0
  32. package/dist/src/cli/commands/browser/_detached.js +422 -0
  33. package/dist/src/cli/commands/browser/_detached.js.map +1 -0
  34. package/dist/src/cli/commands/browser/close.d.ts +7 -0
  35. package/dist/src/cli/commands/browser/close.d.ts.map +1 -1
  36. package/dist/src/cli/commands/browser/close.js +101 -5
  37. package/dist/src/cli/commands/browser/close.js.map +1 -1
  38. package/dist/src/cli/commands/browser/create.d.ts +7 -0
  39. package/dist/src/cli/commands/browser/create.d.ts.map +1 -1
  40. package/dist/src/cli/commands/browser/create.js +233 -25
  41. package/dist/src/cli/commands/browser/create.js.map +1 -1
  42. package/dist/src/cli/commands/browser/index.d.ts.map +1 -1
  43. package/dist/src/cli/commands/browser/index.js +3 -0
  44. package/dist/src/cli/commands/browser/index.js.map +1 -1
  45. package/dist/src/cli/commands/browser/run.d.ts.map +1 -1
  46. package/dist/src/cli/commands/browser/run.js +13 -6
  47. package/dist/src/cli/commands/browser/run.js.map +1 -1
  48. package/dist/src/cli/commands/browser/status.d.ts +4 -0
  49. package/dist/src/cli/commands/browser/status.d.ts.map +1 -1
  50. package/dist/src/cli/commands/browser/status.js +85 -3
  51. package/dist/src/cli/commands/browser/status.js.map +1 -1
  52. package/dist/src/cli/commands/doctor.d.ts +45 -0
  53. package/dist/src/cli/commands/doctor.d.ts.map +1 -0
  54. package/dist/src/cli/commands/doctor.js +267 -0
  55. package/dist/src/cli/commands/doctor.js.map +1 -0
  56. package/dist/src/cli/commands/test/run.d.ts.map +1 -1
  57. package/dist/src/cli/commands/test/run.js +29 -18
  58. package/dist/src/cli/commands/test/run.js.map +1 -1
  59. package/dist/src/cli/commands/tunnel/close.d.ts +18 -0
  60. package/dist/src/cli/commands/tunnel/close.d.ts.map +1 -0
  61. package/dist/src/cli/commands/tunnel/close.js +154 -0
  62. package/dist/src/cli/commands/tunnel/close.js.map +1 -0
  63. package/dist/src/cli/commands/tunnel/index.d.ts +6 -0
  64. package/dist/src/cli/commands/tunnel/index.d.ts.map +1 -0
  65. package/dist/src/cli/commands/tunnel/index.js +17 -0
  66. package/dist/src/cli/commands/tunnel/index.js.map +1 -0
  67. package/dist/src/cli/commands/tunnel/ls.d.ts +10 -0
  68. package/dist/src/cli/commands/tunnel/ls.d.ts.map +1 -0
  69. package/dist/src/cli/commands/tunnel/ls.js +89 -0
  70. package/dist/src/cli/commands/tunnel/ls.js.map +1 -0
  71. package/dist/src/cli/commands/tunnel/start.d.ts +15 -0
  72. package/dist/src/cli/commands/tunnel/start.d.ts.map +1 -0
  73. package/dist/src/cli/commands/tunnel/start.js +65 -0
  74. package/dist/src/cli/commands/tunnel/start.js.map +1 -0
  75. package/dist/src/cli/commands/tunnel/status.d.ts +8 -0
  76. package/dist/src/cli/commands/tunnel/status.d.ts.map +1 -0
  77. package/dist/src/cli/commands/tunnel/status.js +58 -0
  78. package/dist/src/cli/commands/tunnel/status.js.map +1 -0
  79. package/dist/src/cli/generated/docs-content.d.ts +1 -1
  80. package/dist/src/cli/generated/docs-content.d.ts.map +1 -1
  81. package/dist/src/cli/generated/docs-content.js +157 -100
  82. package/dist/src/cli/generated/docs-content.js.map +1 -1
  83. package/dist/src/cli/index.js +8 -0
  84. package/dist/src/cli/index.js.map +1 -1
  85. package/dist/src/cli/lib/browser.d.ts +25 -9
  86. package/dist/src/cli/lib/browser.d.ts.map +1 -1
  87. package/dist/src/cli/lib/browser.js +73 -42
  88. package/dist/src/cli/lib/browser.js.map +1 -1
  89. package/dist/src/cli/lib/cli-entry.d.ts +40 -0
  90. package/dist/src/cli/lib/cli-entry.d.ts.map +1 -0
  91. package/dist/src/cli/lib/cli-entry.js +65 -0
  92. package/dist/src/cli/lib/cli-entry.js.map +1 -0
  93. package/dist/src/cli/lib/config.d.ts.map +1 -1
  94. package/dist/src/cli/lib/config.js +8 -4
  95. package/dist/src/cli/lib/config.js.map +1 -1
  96. package/dist/src/cli/lib/startup-sweep.d.ts +45 -0
  97. package/dist/src/cli/lib/startup-sweep.d.ts.map +1 -0
  98. package/dist/src/cli/lib/startup-sweep.js +246 -0
  99. package/dist/src/cli/lib/startup-sweep.js.map +1 -0
  100. package/dist/src/cli/lib/tunnel-banner.d.ts +33 -0
  101. package/dist/src/cli/lib/tunnel-banner.d.ts.map +1 -0
  102. package/dist/src/cli/lib/tunnel-banner.js +55 -0
  103. package/dist/src/cli/lib/tunnel-banner.js.map +1 -0
  104. package/dist/src/cli/lib/tunnel-error-hint.d.ts +20 -0
  105. package/dist/src/cli/lib/tunnel-error-hint.d.ts.map +1 -0
  106. package/dist/src/cli/lib/tunnel-error-hint.js +48 -0
  107. package/dist/src/cli/lib/tunnel-error-hint.js.map +1 -0
  108. package/dist/src/cli/lib/tunnel-option.d.ts +27 -0
  109. package/dist/src/cli/lib/tunnel-option.d.ts.map +1 -0
  110. package/dist/src/cli/lib/tunnel-option.js +77 -0
  111. package/dist/src/cli/lib/tunnel-option.js.map +1 -0
  112. package/dist/src/cli/lib/tunnel-resolve.d.ts +42 -0
  113. package/dist/src/cli/lib/tunnel-resolve.d.ts.map +1 -0
  114. package/dist/src/cli/lib/tunnel-resolve.js +72 -0
  115. package/dist/src/cli/lib/tunnel-resolve.js.map +1 -0
  116. package/lib/env/index.ts +51 -0
  117. package/lib/env/localhost.test.ts +63 -0
  118. package/lib/env/localhost.ts +51 -0
  119. package/lib/env/paths.ts +46 -0
  120. package/lib/env/sessions.test.ts +109 -0
  121. package/lib/env/sessions.ts +155 -0
  122. package/lib/tunnel/errors.test.ts +105 -0
  123. package/lib/tunnel/errors.ts +169 -0
  124. package/lib/tunnel/index.ts +26 -11
  125. package/lib/tunnel/registry.test.ts +420 -0
  126. package/lib/tunnel/registry.ts +646 -0
  127. package/package.json +1 -1
@@ -4,23 +4,26 @@
4
4
  * Provides automatic localhost tunneling when tests target localhost URLs,
5
5
  * and browser WebSocket connection for remote test execution.
6
6
  */
7
- import { URL } from 'node:url';
8
7
  import { BrowserManager } from '../../../lib/browser/index.js';
9
- import { TunnelManager } from '../../../lib/tunnel/index.js';
8
+ import { classifyTunnelFailure, TunnelError } from '../../../lib/tunnel/errors.js';
9
+ import { tunnelRegistry } from '../../../lib/tunnel/registry.js';
10
10
  import { error } from './output.js';
11
+ import { printTunnelReuseBanner, printTunnelStartBanner } from './tunnel-banner.js';
12
+ import { formatTunnelFailure } from './tunnel-error-hint.js';
13
+ import { resolveTunnelMode } from './tunnel-resolve.js';
11
14
  /**
12
- * Check if a URL points to localhost
15
+ * Mirror of `TunnelManager.getWebSocketUrl` for the cross-process
16
+ * attach case where we don't have a live `TunnelManager` to call.
17
+ * Converts the tunnel's public HTTPS URL + the local browser WS path
18
+ * into a `wss://.../devtools/browser/...` URL.
13
19
  */
14
- export function isLocalhostUrl(url) {
20
+ function deriveCrossProcessWsUrl(publicHttpUrl, localWsEndpoint) {
15
21
  try {
16
- const parsed = new URL(url);
17
- return (parsed.hostname === 'localhost' ||
18
- parsed.hostname === '127.0.0.1' ||
19
- parsed.hostname === '::1' ||
20
- parsed.hostname.endsWith('.localhost'));
22
+ const wsPath = new URL(localWsEndpoint).pathname;
23
+ return publicHttpUrl.replace('https://', 'wss://').replace('http://', 'ws://') + wsPath;
21
24
  }
22
25
  catch {
23
- return false;
26
+ return null;
24
27
  }
25
28
  }
26
29
  /**
@@ -39,22 +42,6 @@ export function ensureBrowsersInstalled() {
39
42
  process.exit(1);
40
43
  }
41
44
  }
42
- /**
43
- * Get the port from a URL
44
- */
45
- export function getPortFromUrl(url) {
46
- try {
47
- const parsed = new URL(url);
48
- if (parsed.port) {
49
- return parseInt(parsed.port, 10);
50
- }
51
- // Default ports
52
- return parsed.protocol === 'https:' ? 443 : 80;
53
- }
54
- catch {
55
- return 80;
56
- }
57
- }
58
45
  /**
59
46
  * Start browser and tunnel (if needed for localhost testing)
60
47
  *
@@ -67,6 +54,7 @@ export async function startBrowserWithTunnel(testUrl, options = {}) {
67
54
  ensureBrowsersInstalled();
68
55
  const browser = new BrowserManager();
69
56
  let tunnel = null;
57
+ let tunnelHandle = null;
70
58
  let publicWsUrl = null;
71
59
  // Start browser
72
60
  console.error('Starting browser...');
@@ -74,24 +62,65 @@ export async function startBrowserWithTunnel(testUrl, options = {}) {
74
62
  headless: options.headless ?? true,
75
63
  });
76
64
  const wsUrl = browserSession.wsEndpoint;
77
- const isLocalhost = testUrl ? isLocalhostUrl(testUrl) : false;
78
- // If testing localhost, set up tunnel for browser WebSocket
79
- if (isLocalhost || !testUrl) {
80
- console.error('Localhost URL detected - starting tunnel for browser connection...');
81
- tunnel = new TunnelManager();
82
- // Extract port from WebSocket URL
83
- const wsPort = getPortFromUrl(wsUrl);
84
- const tunnelSession = await tunnel.startTunnel(wsPort, {
85
- apiKey: options.apiKey,
86
- sessionIndex: options.sessionIndex,
87
- });
88
- publicWsUrl = tunnel.getWebSocketUrl(wsUrl);
89
- console.error(`Tunnel established: ${tunnelSession.publicUrl}`);
90
- console.error(`Public WebSocket URL: ${publicWsUrl}\n`);
65
+ // Resolve the on/off decision. Default mode is 'auto' — that way
66
+ // callers that don't pass `tunnelMode` still get Phase-2 behaviour.
67
+ const mode = options.tunnelMode ?? 'auto';
68
+ const decision = resolveTunnelMode(mode, testUrl, options.apiUrl);
69
+ const isLocalhost = decision === 'on';
70
+ if (decision === 'on') {
71
+ // The tunnel target is the browser WebSocket URL — that's what the
72
+ // remote backend needs to reach. `testUrl` is retained only for the
73
+ // banner copy so users see the localhost *app* URL they typed.
74
+ const bannerTarget = testUrl ?? wsUrl;
75
+ try {
76
+ tunnelHandle = await tunnelRegistry.acquire(wsUrl, {
77
+ apiKey: options.apiKey,
78
+ sessionIndex: options.sessionIndex,
79
+ });
80
+ if (tunnelHandle.isCrossProcessAttach) {
81
+ // Another process owns the TunnelManager. We can't construct a
82
+ // new one (it would race for the same subdomain). Derive the
83
+ // public WS URL from the recorded public HTTP URL + our local
84
+ // ws path — mirrors `TunnelManager.getWebSocketUrl` without
85
+ // needing an in-process manager.
86
+ tunnel = null;
87
+ publicWsUrl = deriveCrossProcessWsUrl(tunnelHandle.publicUrl, wsUrl);
88
+ }
89
+ else {
90
+ // `tunnel` field is a back-compat shim for existing callers
91
+ // that check `session.tunnel` and reach into
92
+ // `stopTunnel`/`checkHealth`. The registry owns the lifecycle
93
+ // now — release goes through `registry.release(handle)` in
94
+ // `stopBrowserWithTunnel`.
95
+ tunnel = tunnelRegistry.getLiveManager(wsUrl);
96
+ publicWsUrl = tunnel ? tunnel.getWebSocketUrl(wsUrl) : null;
97
+ }
98
+ // Branch banner: reuse banner when this acquire landed on an
99
+ // already-running tunnel (refcount > 1 after increment OR we
100
+ // attached to a sibling process's tunnel), else the fresh-start
101
+ // banner.
102
+ const bannerOpts = {
103
+ target: bannerTarget,
104
+ publicUrl: tunnelHandle.publicUrl,
105
+ quiet: options.quiet,
106
+ };
107
+ if (tunnelHandle.refcount > 1 || tunnelHandle.isCrossProcessAttach) {
108
+ printTunnelReuseBanner(bannerOpts);
109
+ }
110
+ else {
111
+ printTunnelStartBanner(bannerOpts);
112
+ }
113
+ }
114
+ catch (err) {
115
+ const classified = err instanceof TunnelError ? err : classifyTunnelFailure(err, { target: bannerTarget });
116
+ console.error(formatTunnelFailure(classified));
117
+ throw classified;
118
+ }
91
119
  }
92
120
  return {
93
121
  browser,
94
122
  tunnel,
123
+ tunnelHandle,
95
124
  wsUrl,
96
125
  publicWsUrl,
97
126
  isLocalhost,
@@ -101,8 +130,10 @@ export async function startBrowserWithTunnel(testUrl, options = {}) {
101
130
  * Stop browser and tunnel
102
131
  */
103
132
  export async function stopBrowserWithTunnel(session) {
104
- if (session.tunnel) {
105
- await session.tunnel.stopTunnel();
133
+ if (session.tunnelHandle) {
134
+ await tunnelRegistry.release(session.tunnelHandle);
135
+ session.tunnelHandle = null;
136
+ session.tunnel = null;
106
137
  }
107
138
  await session.browser.stopBrowser();
108
139
  }
@@ -1 +1 @@
1
- {"version":3,"file":"browser.js","sourceRoot":"","sources":["../../../../src/cli/lib/browser.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../../../../src/cli/lib/browser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * CLI re-exec resolver.
3
+ *
4
+ * The detached `browser create` path spawns the CLI binary as a child
5
+ * process and runs it with the hidden `__browser-detach` subcommand. The
6
+ * parent process needs to deterministically figure out the right
7
+ * `(command, args)` to invoke, regardless of HOW the parent was invoked:
8
+ *
9
+ * 1. Installed binary — e.g. `qa-use` on PATH (maps to node or bun
10
+ * executing the compiled entry under `dist/` or a shim script).
11
+ * 2. `bun run cli ...` — `process.argv[1]` is a `.ts` file under the
12
+ * repo and `process.execPath` is the `bun` binary, which handles
13
+ * `.ts` natively.
14
+ * 3. Symlinked binary — `process.argv[1]` is a symlink; `fs.realpathSync`
15
+ * resolves it to the underlying file, so we don't re-exec through a
16
+ * broken symlink.
17
+ *
18
+ * In every case we return `{ command: process.execPath, args: [realPath, ...] }`
19
+ * so the child inherits the same runtime (node OR bun) as the parent.
20
+ */
21
+ export interface CliEntry {
22
+ /** Executable to spawn (typically `process.execPath`). */
23
+ command: string;
24
+ /** Argv to pass (first element is the real script path). */
25
+ args: string[];
26
+ }
27
+ export interface ResolveCliEntryDeps {
28
+ argv?: string[];
29
+ execPath?: string;
30
+ realpathSync?: (p: string) => string;
31
+ }
32
+ /**
33
+ * Resolve the `(command, args)` needed to re-exec the CLI binary.
34
+ *
35
+ * When `extraArgs` is provided, they are appended after the resolved
36
+ * script path — callers use this to inject `__browser-detach <session-id>`
37
+ * and related flags.
38
+ */
39
+ export declare function resolveCliEntry(extraArgs?: string[], deps?: ResolveCliEntryDeps): CliEntry;
40
+ //# sourceMappingURL=cli-entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-entry.d.ts","sourceRoot":"","sources":["../../../../src/cli/lib/cli-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,MAAM,WAAW,QAAQ;IACvB,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;CACtC;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,SAAS,GAAE,MAAM,EAAO,EACxB,IAAI,GAAE,mBAAwB,GAC7B,QAAQ,CAsCV"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * CLI re-exec resolver.
3
+ *
4
+ * The detached `browser create` path spawns the CLI binary as a child
5
+ * process and runs it with the hidden `__browser-detach` subcommand. The
6
+ * parent process needs to deterministically figure out the right
7
+ * `(command, args)` to invoke, regardless of HOW the parent was invoked:
8
+ *
9
+ * 1. Installed binary — e.g. `qa-use` on PATH (maps to node or bun
10
+ * executing the compiled entry under `dist/` or a shim script).
11
+ * 2. `bun run cli ...` — `process.argv[1]` is a `.ts` file under the
12
+ * repo and `process.execPath` is the `bun` binary, which handles
13
+ * `.ts` natively.
14
+ * 3. Symlinked binary — `process.argv[1]` is a symlink; `fs.realpathSync`
15
+ * resolves it to the underlying file, so we don't re-exec through a
16
+ * broken symlink.
17
+ *
18
+ * In every case we return `{ command: process.execPath, args: [realPath, ...] }`
19
+ * so the child inherits the same runtime (node OR bun) as the parent.
20
+ */
21
+ import fs from 'node:fs';
22
+ /**
23
+ * Resolve the `(command, args)` needed to re-exec the CLI binary.
24
+ *
25
+ * When `extraArgs` is provided, they are appended after the resolved
26
+ * script path — callers use this to inject `__browser-detach <session-id>`
27
+ * and related flags.
28
+ */
29
+ export function resolveCliEntry(extraArgs = [], deps = {}) {
30
+ const argv = deps.argv ?? process.argv;
31
+ const execPath = deps.execPath ?? process.execPath;
32
+ const realpathSync = deps.realpathSync ?? fs.realpathSync;
33
+ const rawEntry = argv[1];
34
+ if (!rawEntry) {
35
+ throw new Error('resolveCliEntry: process.argv[1] is empty — cannot re-exec the CLI');
36
+ }
37
+ let resolvedEntry;
38
+ try {
39
+ resolvedEntry = realpathSync(rawEntry);
40
+ }
41
+ catch {
42
+ // Fallback to the raw path if realpath fails (e.g. rare sandboxed
43
+ // environments). We still want the spawn to attempt with what we have
44
+ // rather than bubble an unrelated error.
45
+ resolvedEntry = rawEntry;
46
+ }
47
+ // If the entry is a TypeScript source file and the runtime is Node.js
48
+ // (not Bun), raw `node` can't execute it — we need the tsx loader. This
49
+ // happens in dev (`bun run cli` / `npm run cli` → `tsx src/cli/index.ts`).
50
+ // Under Bun, .ts is native. In production (installed binary), the entry
51
+ // is a compiled .js under dist/.
52
+ const isTsEntry = /\.(ts|tsx|mts|cts)$/.test(resolvedEntry);
53
+ const isBun = typeof globalThis.Bun !== 'undefined';
54
+ if (isTsEntry && !isBun) {
55
+ return {
56
+ command: execPath,
57
+ args: ['--import', 'tsx', resolvedEntry, ...extraArgs],
58
+ };
59
+ }
60
+ return {
61
+ command: execPath,
62
+ args: [resolvedEntry, ...extraArgs],
63
+ };
64
+ }
65
+ //# sourceMappingURL=cli-entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-entry.js","sourceRoot":"","sources":["../../../../src/cli/lib/cli-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AAezB;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC7B,YAAsB,EAAE,EACxB,OAA4B,EAAE;IAE9B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC;IACnD,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC,YAAY,CAAC;IAE1D,MAAM,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACzB,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAC;IACxF,CAAC;IAED,IAAI,aAAqB,CAAC;IAC1B,IAAI,CAAC;QACH,aAAa,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,kEAAkE;QAClE,sEAAsE;QACtE,yCAAyC;QACzC,aAAa,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED,sEAAsE;IACtE,wEAAwE;IACxE,2EAA2E;IAC3E,wEAAwE;IACxE,iCAAiC;IACjC,MAAM,SAAS,GAAG,qBAAqB,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAG,OAAQ,UAAgC,CAAC,GAAG,KAAK,WAAW,CAAC;IAC3E,IAAI,SAAS,IAAI,CAAC,KAAK,EAAE,CAAC;QACxB,OAAO;YACL,OAAO,EAAE,QAAQ;YACjB,IAAI,EAAE,CAAC,UAAU,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,SAAS,CAAC;SACvD,CAAC;IACJ,CAAC;IAED,OAAO;QACL,OAAO,EAAE,QAAQ;QACjB,IAAI,EAAE,CAAC,aAAa,EAAE,GAAG,SAAS,CAAC;KACpC,CAAC;AACJ,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../src/cli/lib/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAEtD,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,QAAQ,CAAC,EAAE;QACT,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAqCD;;;GAGG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAU7D;AAED;;;;;;;;GAQG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,SAAS,CAAC,CAqErD;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAIjE;AAED;;GAEG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC,CAErD;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,GAAG,gBAAgB,CAKvE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,CAK5D"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../src/cli/lib/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAEtD,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,QAAQ,CAAC,EAAE;QACT,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAqCD;;;GAGG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAU7D;AAED;;;;;;;;GAQG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,SAAS,CAAC,CAyErD;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAIjE;AAED;;GAEG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC,CAErD;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,GAAG,gBAAgB,CAKvE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,CAK5D"}
@@ -98,11 +98,12 @@ export async function loadConfig() {
98
98
  if (process.env.QA_USE_DEFAULT_APP_CONFIG_ID) {
99
99
  config.default_app_config_id = process.env.QA_USE_DEFAULT_APP_CONFIG_ID;
100
100
  }
101
- // Resolve custom headers: config file headers + QA_USE_HEADERS env var (env wins)
101
+ // Resolve custom headers: QA_USE_HEADERS env var seeds defaults, config-file
102
+ // headers override per-key. Headers intentionally use file-wins precedence
103
+ // (unlike api_key / api_url) so a per-session .qa-use.json can override an
104
+ // ambient env value baked in by a multi-tenant host (e.g. a sandbox gateway
105
+ // that sets QA_USE_HEADERS process-wide but writes per-session config files).
102
106
  const resolvedHeaders = {};
103
- if (config.headers) {
104
- Object.assign(resolvedHeaders, config.headers);
105
- }
106
107
  const envHeaders = process.env.QA_USE_HEADERS;
107
108
  if (envHeaders) {
108
109
  try {
@@ -115,6 +116,9 @@ export async function loadConfig() {
115
116
  console.error('Warning: QA_USE_HEADERS is not valid JSON, ignoring');
116
117
  }
117
118
  }
119
+ if (config.headers) {
120
+ Object.assign(resolvedHeaders, config.headers);
121
+ }
118
122
  if (Object.keys(resolvedHeaders).length > 0) {
119
123
  config.headers = resolvedHeaders;
120
124
  }
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../../../src/cli/lib/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../../../src/cli/lib/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Bounded startup sweep — runs on every CLI invocation except `doctor`
3
+ * itself and `__browser-detach`. Cheap, silent-on-success cleanup of
4
+ * orphaned PID files whose owning process is no longer alive.
5
+ *
6
+ * Design constraints:
7
+ * - **Budget**: 250 ms hard cap. We stop iterating early if the budget
8
+ * is exceeded, even if more stale entries remain. `doctor` picks up
9
+ * the rest on the next explicit run.
10
+ * - **Zero net/API calls**. We only remove PID files and force-close
11
+ * tunnel registry entries (in-process). Backend session-end calls
12
+ * belong to `doctor`, not to the sweep.
13
+ * - **Silent on success**. A single-line stderr notice only when we
14
+ * actually reaped something: `qa-use: cleaned up N stale session(s)`.
15
+ * - **Safe on empty state**. If `~/.qa-use/sessions/` or
16
+ * `~/.qa-use/tunnels/` doesn't exist, return immediately.
17
+ */
18
+ /**
19
+ * Determine whether to run the sweep for this CLI invocation. Looks at
20
+ * `process.argv` positionally — we don't have the parsed Commander tree
21
+ * available at this stage.
22
+ */
23
+ export declare function shouldSweep(argv?: string[]): boolean;
24
+ interface SweepResult {
25
+ reapedSessions: number;
26
+ reapedTunnels: number;
27
+ budgetExceeded: boolean;
28
+ }
29
+ /**
30
+ * Run the bounded sweep. Returns a summary object; callers may use it for
31
+ * tests. The function never throws — failures are swallowed so startup
32
+ * is never blocked.
33
+ */
34
+ export declare function runStartupSweep(): Promise<SweepResult>;
35
+ /**
36
+ * Entry point for `src/cli/index.ts`. Fire-and-forget: we kick off the
37
+ * sweep but don't await it — subsequent CLI work can run in parallel.
38
+ * The sweep's own budget guarantees it won't hold the process open for
39
+ * long.
40
+ *
41
+ * If the sweep reaped anything, we emit a single-line stderr notice.
42
+ */
43
+ export declare function kickoffStartupSweep(argv?: string[]): void;
44
+ export {};
45
+ //# sourceMappingURL=startup-sweep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"startup-sweep.d.ts","sourceRoot":"","sources":["../../../../src/cli/lib/startup-sweep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAqBH;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CA+BlE;AAED,UAAU,WAAW;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,OAAO,CAAC;CACzB;AAwGD;;;;GAIG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,WAAW,CAAC,CAmB5D;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,IAAI,CAuBvE"}
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Bounded startup sweep — runs on every CLI invocation except `doctor`
3
+ * itself and `__browser-detach`. Cheap, silent-on-success cleanup of
4
+ * orphaned PID files whose owning process is no longer alive.
5
+ *
6
+ * Design constraints:
7
+ * - **Budget**: 250 ms hard cap. We stop iterating early if the budget
8
+ * is exceeded, even if more stale entries remain. `doctor` picks up
9
+ * the rest on the next explicit run.
10
+ * - **Zero net/API calls**. We only remove PID files and force-close
11
+ * tunnel registry entries (in-process). Backend session-end calls
12
+ * belong to `doctor`, not to the sweep.
13
+ * - **Silent on success**. A single-line stderr notice only when we
14
+ * actually reaped something: `qa-use: cleaned up N stale session(s)`.
15
+ * - **Safe on empty state**. If `~/.qa-use/sessions/` or
16
+ * `~/.qa-use/tunnels/` doesn't exist, return immediately.
17
+ */
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import { sessionsDir, tunnelsDir } from '../../../lib/env/paths.js';
21
+ import { isPidAlive } from '../../../lib/env/sessions.js';
22
+ import { tunnelRegistry } from '../../../lib/tunnel/registry.js';
23
+ const BUDGET_MS = 250;
24
+ /**
25
+ * Commands that MUST NOT trigger a sweep:
26
+ * - `doctor` — runs its own fuller reap; double-reap is a no-op but
27
+ * noisy when it prints "cleaned up N" from the sweep, immediately
28
+ * followed by doctor's own report of zero.
29
+ * - `__browser-detach` — the detached child. It is itself a freshly
30
+ * spawned process whose PID file we may not have written yet; the
31
+ * sweep must not race with that write.
32
+ */
33
+ const SKIP_COMMANDS = new Set(['doctor', '__browser-detach']);
34
+ /**
35
+ * Determine whether to run the sweep for this CLI invocation. Looks at
36
+ * `process.argv` positionally — we don't have the parsed Commander tree
37
+ * available at this stage.
38
+ */
39
+ export function shouldSweep(argv = process.argv) {
40
+ // argv[0] = node, argv[1] = cli entry. Command is argv[2] unless it's
41
+ // a flag (e.g. `--version`, `--help` alone). Additionally, many nested
42
+ // commands (e.g. `browser __browser-detach`) have the sentinel in
43
+ // argv[3]; scan the first few entries.
44
+ for (let i = 2; i < Math.min(argv.length, 5); i++) {
45
+ const token = argv[i];
46
+ if (!token || token.startsWith('-'))
47
+ continue;
48
+ if (SKIP_COMMANDS.has(token))
49
+ return false;
50
+ // First non-flag token found; it's the top-level command. Return true
51
+ // if it isn't a skip command (already handled).
52
+ // We still check nested tokens because `browser __browser-detach` has
53
+ // the sentinel in position i+1.
54
+ }
55
+ // Also explicitly scan entire argv for __browser-detach (can be nested).
56
+ if (argv.includes('__browser-detach'))
57
+ return false;
58
+ // `browser status` (with or without --list / session id) must not sweep:
59
+ // the sweep would race with rendering and silently reap the very
60
+ // stale entries the user asked to see. `browser` followed (somewhere)
61
+ // by `status` is the canonical shape we skip. Other `browser`
62
+ // subcommands (create/close/snapshot/...) still sweep normally.
63
+ const browserIdx = argv.indexOf('browser');
64
+ if (browserIdx !== -1) {
65
+ for (let i = browserIdx + 1; i < argv.length; i++) {
66
+ const token = argv[i];
67
+ if (!token || token.startsWith('-'))
68
+ continue;
69
+ if (token === 'status')
70
+ return false;
71
+ break;
72
+ }
73
+ }
74
+ return true;
75
+ }
76
+ async function sweepSessions(deadline) {
77
+ const dir = sessionsDir();
78
+ let files;
79
+ try {
80
+ files = fs.readdirSync(dir);
81
+ }
82
+ catch {
83
+ return 0;
84
+ }
85
+ let reaped = 0;
86
+ for (const name of files) {
87
+ if (Date.now() >= deadline)
88
+ break;
89
+ if (!name.endsWith('.json') || name.endsWith('.tmp'))
90
+ continue;
91
+ const file = path.join(dir, name);
92
+ let parsed = null;
93
+ try {
94
+ const raw = fs.readFileSync(file, 'utf8');
95
+ parsed = JSON.parse(raw);
96
+ }
97
+ catch {
98
+ // Unreadable file — remove it.
99
+ try {
100
+ fs.unlinkSync(file);
101
+ reaped += 1;
102
+ }
103
+ catch {
104
+ /* ignore */
105
+ }
106
+ continue;
107
+ }
108
+ if (!parsed || typeof parsed.pid !== 'number') {
109
+ try {
110
+ fs.unlinkSync(file);
111
+ reaped += 1;
112
+ }
113
+ catch {
114
+ /* ignore */
115
+ }
116
+ continue;
117
+ }
118
+ if (!isPidAlive(parsed.pid)) {
119
+ try {
120
+ fs.unlinkSync(file);
121
+ }
122
+ catch {
123
+ /* already gone */
124
+ }
125
+ // Best-effort: if the session referenced a tunnel target, ensure
126
+ // any registry handle is released in-process. No net calls.
127
+ if (parsed.target) {
128
+ try {
129
+ await tunnelRegistry.forceClose(parsed.target);
130
+ }
131
+ catch {
132
+ /* best-effort */
133
+ }
134
+ }
135
+ reaped += 1;
136
+ }
137
+ }
138
+ return reaped;
139
+ }
140
+ async function sweepTunnels(deadline) {
141
+ const dir = tunnelsDir();
142
+ let files;
143
+ try {
144
+ files = fs.readdirSync(dir);
145
+ }
146
+ catch {
147
+ return 0;
148
+ }
149
+ let reaped = 0;
150
+ for (const name of files) {
151
+ if (Date.now() >= deadline)
152
+ break;
153
+ if (!name.endsWith('.json') || name.endsWith('.tmp'))
154
+ continue;
155
+ const file = path.join(dir, name);
156
+ let parsed = null;
157
+ try {
158
+ const raw = fs.readFileSync(file, 'utf8');
159
+ parsed = JSON.parse(raw);
160
+ }
161
+ catch {
162
+ try {
163
+ fs.unlinkSync(file);
164
+ reaped += 1;
165
+ }
166
+ catch {
167
+ /* ignore */
168
+ }
169
+ continue;
170
+ }
171
+ if (!parsed || typeof parsed.pid !== 'number' || !isPidAlive(parsed.pid)) {
172
+ if (parsed?.target) {
173
+ try {
174
+ await tunnelRegistry.forceClose(parsed.target);
175
+ }
176
+ catch {
177
+ /* best-effort; fall through to manual unlink */
178
+ }
179
+ }
180
+ try {
181
+ fs.unlinkSync(file);
182
+ }
183
+ catch {
184
+ /* already gone */
185
+ }
186
+ reaped += 1;
187
+ }
188
+ }
189
+ return reaped;
190
+ }
191
+ /**
192
+ * Run the bounded sweep. Returns a summary object; callers may use it for
193
+ * tests. The function never throws — failures are swallowed so startup
194
+ * is never blocked.
195
+ */
196
+ export async function runStartupSweep() {
197
+ const deadline = Date.now() + BUDGET_MS;
198
+ const result = {
199
+ reapedSessions: 0,
200
+ reapedTunnels: 0,
201
+ budgetExceeded: false,
202
+ };
203
+ try {
204
+ result.reapedSessions = await sweepSessions(deadline);
205
+ if (Date.now() < deadline) {
206
+ result.reapedTunnels = await sweepTunnels(deadline);
207
+ }
208
+ }
209
+ catch {
210
+ /* never surface sweep failures */
211
+ }
212
+ if (Date.now() >= deadline) {
213
+ result.budgetExceeded = true;
214
+ }
215
+ return result;
216
+ }
217
+ /**
218
+ * Entry point for `src/cli/index.ts`. Fire-and-forget: we kick off the
219
+ * sweep but don't await it — subsequent CLI work can run in parallel.
220
+ * The sweep's own budget guarantees it won't hold the process open for
221
+ * long.
222
+ *
223
+ * If the sweep reaped anything, we emit a single-line stderr notice.
224
+ */
225
+ export function kickoffStartupSweep(argv = process.argv) {
226
+ if (!shouldSweep(argv))
227
+ return;
228
+ void runStartupSweep()
229
+ .then((result) => {
230
+ const total = result.reapedSessions + result.reapedTunnels;
231
+ if (total > 0) {
232
+ const parts = [];
233
+ if (result.reapedSessions > 0) {
234
+ parts.push(`${result.reapedSessions} stale session${result.reapedSessions === 1 ? '' : 's'}`);
235
+ }
236
+ if (result.reapedTunnels > 0) {
237
+ parts.push(`${result.reapedTunnels} stale tunnel${result.reapedTunnels === 1 ? '' : 's'}`);
238
+ }
239
+ console.error(`qa-use: cleaned up ${parts.join(' + ')}`);
240
+ }
241
+ })
242
+ .catch(() => {
243
+ /* never surface sweep failures */
244
+ });
245
+ }
246
+ //# sourceMappingURL=startup-sweep.js.map