@desplega.ai/qa-use 2.14.1 → 2.15.1

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 (139) hide show
  1. package/README.md +23 -0
  2. package/dist/lib/api/index.d.ts +5 -1
  3. package/dist/lib/api/index.d.ts.map +1 -1
  4. package/dist/lib/api/index.js +112 -5
  5. package/dist/lib/api/index.js.map +1 -1
  6. package/dist/lib/api/sse.d.ts +22 -2
  7. package/dist/lib/api/sse.d.ts.map +1 -1
  8. package/dist/lib/api/sse.js +77 -5
  9. package/dist/lib/api/sse.js.map +1 -1
  10. package/dist/lib/env/index.d.ts +13 -0
  11. package/dist/lib/env/index.d.ts.map +1 -1
  12. package/dist/lib/env/index.js +35 -0
  13. package/dist/lib/env/index.js.map +1 -1
  14. package/dist/lib/env/localhost.d.ts +22 -0
  15. package/dist/lib/env/localhost.d.ts.map +1 -0
  16. package/dist/lib/env/localhost.js +49 -0
  17. package/dist/lib/env/localhost.js.map +1 -0
  18. package/dist/lib/env/paths.d.ts +27 -0
  19. package/dist/lib/env/paths.d.ts.map +1 -0
  20. package/dist/lib/env/paths.js +42 -0
  21. package/dist/lib/env/paths.js.map +1 -0
  22. package/dist/lib/env/sessions.d.ts +55 -0
  23. package/dist/lib/env/sessions.d.ts.map +1 -0
  24. package/dist/lib/env/sessions.js +128 -0
  25. package/dist/lib/env/sessions.js.map +1 -0
  26. package/dist/lib/tunnel/errors.d.ts +61 -0
  27. package/dist/lib/tunnel/errors.d.ts.map +1 -0
  28. package/dist/lib/tunnel/errors.js +152 -0
  29. package/dist/lib/tunnel/errors.js.map +1 -0
  30. package/dist/lib/tunnel/index.d.ts.map +1 -1
  31. package/dist/lib/tunnel/index.js +26 -11
  32. package/dist/lib/tunnel/index.js.map +1 -1
  33. package/dist/lib/tunnel/registry.d.ts +182 -0
  34. package/dist/lib/tunnel/registry.d.ts.map +1 -0
  35. package/dist/lib/tunnel/registry.js +561 -0
  36. package/dist/lib/tunnel/registry.js.map +1 -0
  37. package/dist/package.json +1 -1
  38. package/dist/src/cli/commands/browser/_detached.d.ts +27 -0
  39. package/dist/src/cli/commands/browser/_detached.d.ts.map +1 -0
  40. package/dist/src/cli/commands/browser/_detached.js +422 -0
  41. package/dist/src/cli/commands/browser/_detached.js.map +1 -0
  42. package/dist/src/cli/commands/browser/close.d.ts +7 -0
  43. package/dist/src/cli/commands/browser/close.d.ts.map +1 -1
  44. package/dist/src/cli/commands/browser/close.js +101 -5
  45. package/dist/src/cli/commands/browser/close.js.map +1 -1
  46. package/dist/src/cli/commands/browser/create.d.ts +7 -0
  47. package/dist/src/cli/commands/browser/create.d.ts.map +1 -1
  48. package/dist/src/cli/commands/browser/create.js +233 -25
  49. package/dist/src/cli/commands/browser/create.js.map +1 -1
  50. package/dist/src/cli/commands/browser/index.d.ts.map +1 -1
  51. package/dist/src/cli/commands/browser/index.js +3 -0
  52. package/dist/src/cli/commands/browser/index.js.map +1 -1
  53. package/dist/src/cli/commands/browser/run.d.ts.map +1 -1
  54. package/dist/src/cli/commands/browser/run.js +13 -6
  55. package/dist/src/cli/commands/browser/run.js.map +1 -1
  56. package/dist/src/cli/commands/browser/status.d.ts +4 -0
  57. package/dist/src/cli/commands/browser/status.d.ts.map +1 -1
  58. package/dist/src/cli/commands/browser/status.js +85 -3
  59. package/dist/src/cli/commands/browser/status.js.map +1 -1
  60. package/dist/src/cli/commands/doctor.d.ts +45 -0
  61. package/dist/src/cli/commands/doctor.d.ts.map +1 -0
  62. package/dist/src/cli/commands/doctor.js +267 -0
  63. package/dist/src/cli/commands/doctor.js.map +1 -0
  64. package/dist/src/cli/commands/test/run.d.ts.map +1 -1
  65. package/dist/src/cli/commands/test/run.js +33 -19
  66. package/dist/src/cli/commands/test/run.js.map +1 -1
  67. package/dist/src/cli/commands/tunnel/close.d.ts +18 -0
  68. package/dist/src/cli/commands/tunnel/close.d.ts.map +1 -0
  69. package/dist/src/cli/commands/tunnel/close.js +154 -0
  70. package/dist/src/cli/commands/tunnel/close.js.map +1 -0
  71. package/dist/src/cli/commands/tunnel/index.d.ts +6 -0
  72. package/dist/src/cli/commands/tunnel/index.d.ts.map +1 -0
  73. package/dist/src/cli/commands/tunnel/index.js +17 -0
  74. package/dist/src/cli/commands/tunnel/index.js.map +1 -0
  75. package/dist/src/cli/commands/tunnel/ls.d.ts +10 -0
  76. package/dist/src/cli/commands/tunnel/ls.d.ts.map +1 -0
  77. package/dist/src/cli/commands/tunnel/ls.js +89 -0
  78. package/dist/src/cli/commands/tunnel/ls.js.map +1 -0
  79. package/dist/src/cli/commands/tunnel/start.d.ts +15 -0
  80. package/dist/src/cli/commands/tunnel/start.d.ts.map +1 -0
  81. package/dist/src/cli/commands/tunnel/start.js +65 -0
  82. package/dist/src/cli/commands/tunnel/start.js.map +1 -0
  83. package/dist/src/cli/commands/tunnel/status.d.ts +8 -0
  84. package/dist/src/cli/commands/tunnel/status.d.ts.map +1 -0
  85. package/dist/src/cli/commands/tunnel/status.js +58 -0
  86. package/dist/src/cli/commands/tunnel/status.js.map +1 -0
  87. package/dist/src/cli/generated/docs-content.d.ts +1 -1
  88. package/dist/src/cli/generated/docs-content.d.ts.map +1 -1
  89. package/dist/src/cli/generated/docs-content.js +157 -100
  90. package/dist/src/cli/generated/docs-content.js.map +1 -1
  91. package/dist/src/cli/index.js +8 -0
  92. package/dist/src/cli/index.js.map +1 -1
  93. package/dist/src/cli/lib/browser.d.ts +25 -9
  94. package/dist/src/cli/lib/browser.d.ts.map +1 -1
  95. package/dist/src/cli/lib/browser.js +73 -42
  96. package/dist/src/cli/lib/browser.js.map +1 -1
  97. package/dist/src/cli/lib/cli-entry.d.ts +40 -0
  98. package/dist/src/cli/lib/cli-entry.d.ts.map +1 -0
  99. package/dist/src/cli/lib/cli-entry.js +65 -0
  100. package/dist/src/cli/lib/cli-entry.js.map +1 -0
  101. package/dist/src/cli/lib/runner.d.ts +6 -0
  102. package/dist/src/cli/lib/runner.d.ts.map +1 -1
  103. package/dist/src/cli/lib/runner.js +2 -2
  104. package/dist/src/cli/lib/runner.js.map +1 -1
  105. package/dist/src/cli/lib/startup-sweep.d.ts +45 -0
  106. package/dist/src/cli/lib/startup-sweep.d.ts.map +1 -0
  107. package/dist/src/cli/lib/startup-sweep.js +246 -0
  108. package/dist/src/cli/lib/startup-sweep.js.map +1 -0
  109. package/dist/src/cli/lib/tunnel-banner.d.ts +33 -0
  110. package/dist/src/cli/lib/tunnel-banner.d.ts.map +1 -0
  111. package/dist/src/cli/lib/tunnel-banner.js +55 -0
  112. package/dist/src/cli/lib/tunnel-banner.js.map +1 -0
  113. package/dist/src/cli/lib/tunnel-error-hint.d.ts +20 -0
  114. package/dist/src/cli/lib/tunnel-error-hint.d.ts.map +1 -0
  115. package/dist/src/cli/lib/tunnel-error-hint.js +48 -0
  116. package/dist/src/cli/lib/tunnel-error-hint.js.map +1 -0
  117. package/dist/src/cli/lib/tunnel-option.d.ts +27 -0
  118. package/dist/src/cli/lib/tunnel-option.d.ts.map +1 -0
  119. package/dist/src/cli/lib/tunnel-option.js +77 -0
  120. package/dist/src/cli/lib/tunnel-option.js.map +1 -0
  121. package/dist/src/cli/lib/tunnel-resolve.d.ts +42 -0
  122. package/dist/src/cli/lib/tunnel-resolve.d.ts.map +1 -0
  123. package/dist/src/cli/lib/tunnel-resolve.js +72 -0
  124. package/dist/src/cli/lib/tunnel-resolve.js.map +1 -0
  125. package/lib/api/index.ts +136 -6
  126. package/lib/api/sse.test.ts +530 -0
  127. package/lib/api/sse.ts +105 -5
  128. package/lib/env/index.ts +51 -0
  129. package/lib/env/localhost.test.ts +63 -0
  130. package/lib/env/localhost.ts +51 -0
  131. package/lib/env/paths.ts +46 -0
  132. package/lib/env/sessions.test.ts +109 -0
  133. package/lib/env/sessions.ts +155 -0
  134. package/lib/tunnel/errors.test.ts +105 -0
  135. package/lib/tunnel/errors.ts +169 -0
  136. package/lib/tunnel/index.ts +26 -11
  137. package/lib/tunnel/registry.test.ts +420 -0
  138. package/lib/tunnel/registry.ts +646 -0
  139. 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"}
@@ -15,6 +15,12 @@ export interface RunTestOptions {
15
15
  testId?: string;
16
16
  /** Run ID for organizing downloads (set automatically) */
17
17
  runId?: string;
18
+ /**
19
+ * Idle timeout in seconds. If no SSE events arrive for this long, the
20
+ * underlying fetch is aborted and `runCliTest` rejects with a `timed out`
21
+ * error. `0` (or undefined) disables the watchdog.
22
+ */
23
+ idleTimeoutSec?: number;
18
24
  }
19
25
  /**
20
26
  * Run a test with real-time progress output
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../../src/cli/lib/runner.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAChG,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAGxD,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,8CAA8C;IAC9C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mCAAmC;IACnC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;GAQG;AACH,wBAAsB,OAAO,CAC3B,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,iBAAiB,EAC1B,UAAU,GAAE,cAAmB,EAC/B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,GAClC,OAAO,CAAC,gBAAgB,CAAC,CAgB3B;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,iBAAiB,EAAE,GACzB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAU7B"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../../src/cli/lib/runner.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAChG,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAGxD,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,8CAA8C;IAC9C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mCAAmC;IACnC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAsB,OAAO,CAC3B,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,iBAAiB,EAC1B,UAAU,GAAE,cAAmB,EAC/B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,GAClC,OAAO,CAAC,gBAAgB,CAAC,CA4B3B;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,iBAAiB,EAAE,GACzB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAU7B"}
@@ -12,7 +12,7 @@ import { printSSEProgress } from './output.js';
12
12
  * @returns Test result
13
13
  */
14
14
  export async function runTest(client, options, runOptions = {}, onEvent) {
15
- const { verbose = false, sourceFile, download, downloadBaseDir, testId, runId } = runOptions;
15
+ const { verbose = false, sourceFile, download, downloadBaseDir, testId, runId, idleTimeoutSec, } = runOptions;
16
16
  // Build context for SSE progress handler
17
17
  const context = sourceFile || download ? { sourceFile, download, downloadBaseDir, testId, runId } : undefined;
18
18
  return await client.runCliTest(options, (event) => {
@@ -22,7 +22,7 @@ export async function runTest(client, options, runOptions = {}, onEvent) {
22
22
  if (onEvent) {
23
23
  onEvent(event);
24
24
  }
25
- });
25
+ }, { idleTimeoutSec });
26
26
  }
27
27
  /**
28
28
  * Run multiple tests sequentially
@@ -1 +1 @@
1
- {"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../../src/cli/lib/runner.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,gBAAgB,EAA2B,MAAM,aAAa,CAAC;AAgBxE;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,MAAiB,EACjB,OAA0B,EAC1B,aAA6B,EAAE,EAC/B,OAAmC;IAEnC,MAAM,EAAE,OAAO,GAAG,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,UAAU,CAAC;IAE7F,yCAAyC;IACzC,MAAM,OAAO,GACX,UAAU,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAEhG,OAAO,MAAM,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;QAChD,4BAA4B;QAC5B,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;QAE1C,uCAAuC;QACvC,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,MAAiB,EACjB,KAA0B;IAE1B,MAAM,OAAO,GAAuB,EAAE,CAAC;IAEvC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,OAAO,CAAC,CAAC;QAC5D,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/C,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACvB,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../../src/cli/lib/runner.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,gBAAgB,EAA2B,MAAM,aAAa,CAAC;AAsBxE;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,MAAiB,EACjB,OAA0B,EAC1B,aAA6B,EAAE,EAC/B,OAAmC;IAEnC,MAAM,EACJ,OAAO,GAAG,KAAK,EACf,UAAU,EACV,QAAQ,EACR,eAAe,EACf,MAAM,EACN,KAAK,EACL,cAAc,GACf,GAAG,UAAU,CAAC;IAEf,yCAAyC;IACzC,MAAM,OAAO,GACX,UAAU,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAEhG,OAAO,MAAM,MAAM,CAAC,UAAU,CAC5B,OAAO,EACP,CAAC,KAAK,EAAE,EAAE;QACR,4BAA4B;QAC5B,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;QAE1C,uCAAuC;QACvC,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC;IACH,CAAC,EACD,EAAE,cAAc,EAAE,CACnB,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,MAAiB,EACjB,KAA0B;IAE1B,MAAM,OAAO,GAAuB,EAAE,CAAC;IAEvC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,OAAO,CAAC,CAAC;QAC5D,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/C,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACvB,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -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