@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
@@ -0,0 +1,561 @@
1
+ /**
2
+ * TunnelRegistry — a shared, refcount-managed layer over `TunnelManager`.
3
+ *
4
+ * Goals:
5
+ * - Two CLI commands targeting the same localhost base URL share one
6
+ * remote tunnel (one public URL, one provider connection).
7
+ * - State is visible and editable across sibling processes via
8
+ * `~/.qa-use/tunnels/<hash>.json`, so a SECOND process targeting the
9
+ * same localhost picks up the FIRST process's public URL rather than
10
+ * spinning up its own tunnel.
11
+ * - Last-releaser (in the OWNER process) keeps the tunnel alive for
12
+ * `GRACE_MS` (30 s default) so rapid-fire invocations do not thrash
13
+ * the provider.
14
+ *
15
+ * Cross-process coordination model:
16
+ * - The process that first acquires a given target becomes the OWNER
17
+ * and runs the in-process `TunnelManager`. Its PID is recorded in
18
+ * the registry file.
19
+ * - Later acquirers in OTHER processes read the file, see an alive
20
+ * owner PID, increment refcount under a lockfile, and return an
21
+ * "attach" handle (`isCrossProcessAttach: true`) with the owner's
22
+ * `publicUrl`. They do NOT construct a `TunnelManager`.
23
+ * - Read-modify-write of the record is guarded by a lockfile
24
+ * (`<hash>.lock`, `O_EXCL | O_CREAT`) with bounded retry.
25
+ *
26
+ * TTL grace limitation:
27
+ * - The grace window is bounded by the OWNER process's lifetime. Since
28
+ * `TunnelManager` is a per-process localtunnel client, the remote
29
+ * tunnel dies when the owner exits. For long-lived owners
30
+ * (`tunnel start --hold`, a running `test run`, a detached
31
+ * `browser create` from Phase 4) grace works as designed: a new
32
+ * acquirer within `GRACE_MS` cancels tear-down. For short-lived
33
+ * commands that release-and-exit immediately, grace collapses to
34
+ * zero (the owner process is gone, so there is no tunnel to keep
35
+ * alive anyway).
36
+ *
37
+ * Non-goals (this phase):
38
+ * - Daemonised tunnel hosts. Phase 4 adds detach so short-lived
39
+ * commands can leave a long-lived owner behind.
40
+ * - Retries on provider failure. Zero retries, consistent with Phase 2.
41
+ */
42
+ import crypto from 'node:crypto';
43
+ import fs from 'node:fs';
44
+ import path from 'node:path';
45
+ import { URL } from 'node:url';
46
+ import { getPortFromUrl } from '../env/localhost.js';
47
+ import { ensureDir, tunnelsDir } from '../env/paths.js';
48
+ import { TunnelQuotaError } from './errors.js';
49
+ import { TunnelManager } from './index.js';
50
+ /**
51
+ * How long (ms) to keep a tunnel alive after its refcount hits zero.
52
+ * A new `acquire()` within this window reuses the existing tunnel.
53
+ * Overridable via `QA_USE_TUNNEL_GRACE_MS` env var (primarily a
54
+ * test-friendly knob; production callers should stick with the default).
55
+ */
56
+ export const GRACE_MS = 30_000;
57
+ function resolveGraceMsFromEnv(fallback) {
58
+ const raw = process.env.QA_USE_TUNNEL_GRACE_MS;
59
+ if (!raw)
60
+ return fallback;
61
+ const parsed = Number.parseInt(raw, 10);
62
+ if (Number.isFinite(parsed) && parsed >= 0)
63
+ return parsed;
64
+ return fallback;
65
+ }
66
+ /**
67
+ * Max concurrent tunnels per API key. Mirrors the sessionIndex clamp at
68
+ * `lib/tunnel/index.ts:41` so we surface a clear error instead of silently
69
+ * colliding on subdomains.
70
+ */
71
+ export const MAX_CONCURRENT_TUNNELS = 10;
72
+ /**
73
+ * Canonical target key — used both as the map key and to derive the
74
+ * filename hash. We lowercase the hostname and drop any path/query so
75
+ * `http://Localhost:3000/foo` and `http://localhost:3000/` dedupe.
76
+ */
77
+ export function canonicalTarget(target) {
78
+ try {
79
+ const u = new URL(target);
80
+ return `${u.protocol}//${u.hostname.toLowerCase()}${u.port ? `:${u.port}` : ''}`;
81
+ }
82
+ catch {
83
+ return target.toLowerCase();
84
+ }
85
+ }
86
+ /** Filename hash for a target. First 10 hex chars of sha256. */
87
+ export function targetHash(target) {
88
+ return crypto.createHash('sha256').update(canonicalTarget(target)).digest('hex').slice(0, 10);
89
+ }
90
+ /**
91
+ * Atomic write: write to `<path>.tmp` then rename. `rename` is atomic on
92
+ * POSIX, so readers never see a half-written file.
93
+ */
94
+ function atomicWriteJson(filePath, data) {
95
+ ensureDir(path.dirname(filePath));
96
+ const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
97
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2));
98
+ fs.renameSync(tmp, filePath);
99
+ }
100
+ function readRecord(filePath) {
101
+ try {
102
+ const raw = fs.readFileSync(filePath, 'utf8');
103
+ const parsed = JSON.parse(raw);
104
+ if (typeof parsed.id !== 'string' || typeof parsed.target !== 'string')
105
+ return null;
106
+ return parsed;
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ }
112
+ function safeUnlink(filePath) {
113
+ try {
114
+ fs.unlinkSync(filePath);
115
+ }
116
+ catch {
117
+ /* ignore */
118
+ }
119
+ }
120
+ function isPidAlive(pid) {
121
+ if (!pid || pid <= 0)
122
+ return false;
123
+ try {
124
+ process.kill(pid, 0);
125
+ return true;
126
+ }
127
+ catch (err) {
128
+ // ESRCH = no process with that pid. EPERM = exists but we can't
129
+ // signal it (still counts as alive).
130
+ if (err.code === 'EPERM')
131
+ return true;
132
+ return false;
133
+ }
134
+ }
135
+ /**
136
+ * Acquire a per-target advisory lockfile. Cross-process exclusion for
137
+ * read-modify-write of the `<hash>.json` file. Implemented via
138
+ * `O_EXCL | O_CREAT` with bounded retry; releases via `unlinkSync`.
139
+ *
140
+ * Returns an `unlock` callback. Always invoke it in a `finally` block.
141
+ */
142
+ const LOCK_RETRY_INTERVAL_MS = 15;
143
+ const LOCK_MAX_WAIT_MS = 2_000;
144
+ const LOCK_STALE_THRESHOLD_MS = 5_000;
145
+ async function withLock(lockPath, fn) {
146
+ ensureDir(path.dirname(lockPath));
147
+ const deadline = Date.now() + LOCK_MAX_WAIT_MS;
148
+ let fd = null;
149
+ while (true) {
150
+ try {
151
+ // O_EXCL | O_CREAT — fails with EEXIST if another holder is live.
152
+ fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_RDWR);
153
+ fs.writeSync(fd, String(process.pid));
154
+ break;
155
+ }
156
+ catch (err) {
157
+ if (err.code !== 'EEXIST')
158
+ throw err;
159
+ // Check for stale lock (older than threshold) and reap.
160
+ try {
161
+ const stat = fs.statSync(lockPath);
162
+ if (Date.now() - stat.mtimeMs > LOCK_STALE_THRESHOLD_MS) {
163
+ safeUnlink(lockPath);
164
+ continue;
165
+ }
166
+ }
167
+ catch {
168
+ // Lock file vanished between exist check and stat — loop again.
169
+ continue;
170
+ }
171
+ if (Date.now() >= deadline) {
172
+ throw new Error(`Timed out waiting for tunnel registry lock: ${lockPath}`);
173
+ }
174
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_INTERVAL_MS));
175
+ }
176
+ }
177
+ try {
178
+ return await fn();
179
+ }
180
+ finally {
181
+ if (fd !== null) {
182
+ try {
183
+ fs.closeSync(fd);
184
+ }
185
+ catch {
186
+ /* ignore */
187
+ }
188
+ }
189
+ safeUnlink(lockPath);
190
+ }
191
+ }
192
+ /**
193
+ * Acquire / release / list API for tunnels.
194
+ *
195
+ * A singleton is exported as `tunnelRegistry` below for convenience;
196
+ * callers that need isolation (tests) construct their own instance.
197
+ */
198
+ export class TunnelRegistry {
199
+ live = new Map();
200
+ graceMs;
201
+ maxConcurrent;
202
+ managerFactory;
203
+ constructor(opts = {}) {
204
+ this.graceMs = resolveGraceMsFromEnv(opts.graceMs ?? GRACE_MS);
205
+ this.maxConcurrent = opts.maxConcurrent ?? MAX_CONCURRENT_TUNNELS;
206
+ this.managerFactory = opts.managerFactory ?? (() => new TunnelManager());
207
+ }
208
+ /**
209
+ * Start a tunnel for `target` (or reuse an existing one — in-process
210
+ * OR in a sibling process), returning a handle. Caller must pair each
211
+ * acquire with exactly one `release(handle)`.
212
+ */
213
+ async acquire(target, opts = {}) {
214
+ const canon = canonicalTarget(target);
215
+ const hash = targetHash(canon);
216
+ const file = path.join(tunnelsDir(), `${hash}.json`);
217
+ const lock = path.join(tunnelsDir(), `${hash}.lock`);
218
+ // Fast path: we already own a live manager in this process.
219
+ const existing = this.live.get(canon);
220
+ if (existing) {
221
+ if (existing.graceTimer) {
222
+ clearTimeout(existing.graceTimer);
223
+ existing.graceTimer = undefined;
224
+ }
225
+ return withLock(lock, () => {
226
+ existing.record.refcount += 1;
227
+ existing.record.ttlExpiresAt = null;
228
+ this.writeRecord(existing.record);
229
+ return { ...existing.record, isCrossProcessAttach: false, _released: false };
230
+ });
231
+ }
232
+ // Slow path: consult the on-disk registry. Everything from here
233
+ // runs under the per-target lock.
234
+ const result = await withLock(lock, async () => {
235
+ const existingRecord = readRecord(file);
236
+ if (existingRecord) {
237
+ if (isPidAlive(existingRecord.pid) && existingRecord.pid !== process.pid) {
238
+ // Another process owns this tunnel; attach.
239
+ existingRecord.refcount += 1;
240
+ existingRecord.ttlExpiresAt = null;
241
+ this.writeRecord(existingRecord);
242
+ return { kind: 'attach', record: existingRecord };
243
+ }
244
+ // Either same pid (rare — map miss means first acquire in
245
+ // this process; treat as stale) or dead pid. Reap and fall
246
+ // through.
247
+ safeUnlink(file);
248
+ }
249
+ return { kind: 'fresh' };
250
+ });
251
+ if (result.kind === 'attach') {
252
+ return { ...result.record, isCrossProcessAttach: true, _released: false };
253
+ }
254
+ // Fresh start — cap check + launch + write under a fresh lock
255
+ // acquisition (startTunnel can take seconds; don't hold the lock
256
+ // that whole time, but DO re-validate nothing raced us).
257
+ const activeList = this.list();
258
+ if (activeList.length >= this.maxConcurrent) {
259
+ throw new TunnelQuotaError(`Concurrent tunnel cap reached (${this.maxConcurrent}). Close an existing tunnel with \`qa-use tunnel close <target>\` and try again.`, { target: canon });
260
+ }
261
+ const port = getPortFromUrl(canon);
262
+ const manager = this.managerFactory();
263
+ const session = await manager.startTunnel(port, opts);
264
+ return withLock(lock, async () => {
265
+ // Race resolution — in-process: a concurrent acquire may have
266
+ // landed in `this.live` while we were booting.
267
+ const existingLocal = this.live.get(canon);
268
+ if (existingLocal) {
269
+ try {
270
+ await manager.stopTunnel();
271
+ }
272
+ catch {
273
+ /* best-effort */
274
+ }
275
+ if (existingLocal.graceTimer) {
276
+ clearTimeout(existingLocal.graceTimer);
277
+ existingLocal.graceTimer = undefined;
278
+ }
279
+ existingLocal.record.refcount += 1;
280
+ existingLocal.record.ttlExpiresAt = null;
281
+ this.writeRecord(existingLocal.record);
282
+ return {
283
+ ...existingLocal.record,
284
+ isCrossProcessAttach: false,
285
+ _released: false,
286
+ };
287
+ }
288
+ // Race resolution — cross-process: if another process wrote a
289
+ // record while we were booting, prefer theirs and tear our
290
+ // tunnel down.
291
+ const raced = readRecord(file);
292
+ if (raced && isPidAlive(raced.pid) && raced.pid !== process.pid) {
293
+ try {
294
+ await manager.stopTunnel();
295
+ }
296
+ catch {
297
+ /* best-effort */
298
+ }
299
+ raced.refcount += 1;
300
+ raced.ttlExpiresAt = null;
301
+ this.writeRecord(raced);
302
+ return { ...raced, isCrossProcessAttach: true, _released: false };
303
+ }
304
+ const record = {
305
+ id: hash,
306
+ target: canon,
307
+ publicUrl: session.publicUrl,
308
+ pid: process.pid,
309
+ refcount: 1,
310
+ ttlExpiresAt: null,
311
+ startedAt: Date.now(),
312
+ };
313
+ this.live.set(canon, { record, manager });
314
+ this.writeRecord(record);
315
+ return { ...record, isCrossProcessAttach: false, _released: false };
316
+ });
317
+ }
318
+ /**
319
+ * Release a handle. Decrements refcount (under lock). When the
320
+ * refcount hits zero AND we are the owner, schedule a tear-down
321
+ * `graceMs` later. A subsequent `acquire()` within the grace window
322
+ * cancels the tear-down.
323
+ *
324
+ * Grace window is bounded by owner process lifetime — short-lived
325
+ * commands exit before grace expires and will tear down immediately.
326
+ */
327
+ async release(handle) {
328
+ if (handle._released)
329
+ return;
330
+ handle._released = true;
331
+ const canon = canonicalTarget(handle.target);
332
+ const hash = targetHash(canon);
333
+ const file = path.join(tunnelsDir(), `${hash}.json`);
334
+ const lock = path.join(tunnelsDir(), `${hash}.lock`);
335
+ await withLock(lock, async () => {
336
+ const record = readRecord(file);
337
+ if (!record)
338
+ return; // Already torn down.
339
+ record.refcount = Math.max(0, record.refcount - 1);
340
+ if (record.refcount > 0) {
341
+ record.ttlExpiresAt = null;
342
+ this.writeRecord(record);
343
+ // If we ARE the owner, keep the in-memory bookkeeping in sync.
344
+ const entry = this.live.get(canon);
345
+ if (entry) {
346
+ entry.record.refcount = record.refcount;
347
+ entry.record.ttlExpiresAt = null;
348
+ }
349
+ return;
350
+ }
351
+ // refcount === 0
352
+ record.ttlExpiresAt = Date.now() + this.graceMs;
353
+ this.writeRecord(record);
354
+ const entry = this.live.get(canon);
355
+ if (entry && record.pid === process.pid) {
356
+ // We are the owner and last releaser — schedule tear-down.
357
+ entry.record.refcount = 0;
358
+ entry.record.ttlExpiresAt = record.ttlExpiresAt;
359
+ if (entry.graceTimer) {
360
+ clearTimeout(entry.graceTimer);
361
+ }
362
+ entry.graceTimer = setTimeout(() => {
363
+ void this.maybeTeardown(canon).catch(() => {
364
+ /* best-effort */
365
+ });
366
+ }, this.graceMs);
367
+ if (typeof entry.graceTimer.unref === 'function') {
368
+ entry.graceTimer.unref();
369
+ }
370
+ }
371
+ // If we are NOT the owner (cross-process attach), just leave the
372
+ // zeroed refcount + ttl on disk. The owner's next release or its
373
+ // own exit path will honour the grace timer.
374
+ });
375
+ }
376
+ /**
377
+ * Force teardown of a tunnel regardless of refcount. Used by
378
+ * `qa-use tunnel close`. Safe to call when no such tunnel exists.
379
+ *
380
+ * If the owner is this process, tears down the in-memory manager.
381
+ * Otherwise just removes the registry file (and leaves the orphan
382
+ * remote tunnel to die with its owner).
383
+ */
384
+ async forceClose(target) {
385
+ const canon = canonicalTarget(target);
386
+ const hash = targetHash(canon);
387
+ const lock = path.join(tunnelsDir(), `${hash}.lock`);
388
+ await withLock(lock, async () => {
389
+ await this.teardown(canon);
390
+ });
391
+ }
392
+ /**
393
+ * Look up a single entry by canonical target. Scans the on-disk
394
+ * registry; returns `null` if no record exists or the owning pid is
395
+ * dead.
396
+ */
397
+ get(target) {
398
+ const canon = canonicalTarget(target);
399
+ const hash = targetHash(canon);
400
+ const file = path.join(tunnelsDir(), `${hash}.json`);
401
+ const record = readRecord(file);
402
+ if (!record)
403
+ return null;
404
+ if (!isPidAlive(record.pid)) {
405
+ safeUnlink(file);
406
+ return null;
407
+ }
408
+ return record;
409
+ }
410
+ /**
411
+ * List all live entries. Reconciles against owning PID; stale entries
412
+ * are removed as a side-effect.
413
+ */
414
+ list() {
415
+ const dir = tunnelsDir();
416
+ let files;
417
+ try {
418
+ files = fs.readdirSync(dir);
419
+ }
420
+ catch {
421
+ return [];
422
+ }
423
+ const out = [];
424
+ for (const name of files) {
425
+ if (!name.endsWith('.json') || name.endsWith('.tmp'))
426
+ continue;
427
+ const file = path.join(dir, name);
428
+ const record = readRecord(file);
429
+ if (!record) {
430
+ safeUnlink(file);
431
+ continue;
432
+ }
433
+ if (!isPidAlive(record.pid)) {
434
+ safeUnlink(file);
435
+ continue;
436
+ }
437
+ out.push(record);
438
+ }
439
+ return out;
440
+ }
441
+ /**
442
+ * Returns the live `TunnelManager` instance for `target` IF this process
443
+ * currently owns it. Used by callers that need health-check / WS-URL
444
+ * helpers on the underlying manager. Returns `null` for targets owned
445
+ * by a different process (file visible via `list()` / `get()` but not
446
+ * live in-memory here).
447
+ */
448
+ getLiveManager(target) {
449
+ const canon = canonicalTarget(target);
450
+ const entry = this.live.get(canon);
451
+ return entry ? entry.manager : null;
452
+ }
453
+ /**
454
+ * Look up by filename hash (e.g. output of `tunnel ls`).
455
+ */
456
+ getByHash(hash) {
457
+ const file = path.join(tunnelsDir(), `${hash}.json`);
458
+ const record = readRecord(file);
459
+ if (!record)
460
+ return null;
461
+ if (!isPidAlive(record.pid)) {
462
+ safeUnlink(file);
463
+ return null;
464
+ }
465
+ return record;
466
+ }
467
+ // -------------------------------------------------------------------
468
+ // Internals
469
+ // -------------------------------------------------------------------
470
+ writeRecord(record) {
471
+ const file = path.join(tunnelsDir(), `${record.id}.json`);
472
+ atomicWriteJson(file, record);
473
+ }
474
+ /**
475
+ * Timer callback: re-check under lock whether we should tear down. A
476
+ * concurrent acquire may have bumped refcount back above zero.
477
+ */
478
+ async maybeTeardown(canon) {
479
+ const hash = targetHash(canon);
480
+ const file = path.join(tunnelsDir(), `${hash}.json`);
481
+ const lock = path.join(tunnelsDir(), `${hash}.lock`);
482
+ await withLock(lock, async () => {
483
+ const record = readRecord(file);
484
+ if (!record) {
485
+ // Someone else cleaned up already — drop our in-memory entry.
486
+ const stale = this.live.get(canon);
487
+ if (stale) {
488
+ this.live.delete(canon);
489
+ try {
490
+ await stale.manager.stopTunnel();
491
+ }
492
+ catch {
493
+ /* best-effort */
494
+ }
495
+ }
496
+ return;
497
+ }
498
+ // If another consumer joined, refcount is back above zero.
499
+ if (record.refcount > 0) {
500
+ // Clear our grace timer bookkeeping; caller will rearm on next
501
+ // release.
502
+ const entry = this.live.get(canon);
503
+ if (entry) {
504
+ entry.record.refcount = record.refcount;
505
+ entry.record.ttlExpiresAt = null;
506
+ if (entry.graceTimer) {
507
+ clearTimeout(entry.graceTimer);
508
+ entry.graceTimer = undefined;
509
+ }
510
+ }
511
+ return;
512
+ }
513
+ // ttlExpiresAt guard: may have been bumped by another release,
514
+ // e.g. a cross-process release wrote a fresh grace window.
515
+ if (record.ttlExpiresAt && record.ttlExpiresAt > Date.now()) {
516
+ // Reschedule.
517
+ const entry = this.live.get(canon);
518
+ if (entry) {
519
+ if (entry.graceTimer)
520
+ clearTimeout(entry.graceTimer);
521
+ const delay = Math.max(0, record.ttlExpiresAt - Date.now());
522
+ entry.graceTimer = setTimeout(() => {
523
+ void this.maybeTeardown(canon).catch(() => {
524
+ /* best-effort */
525
+ });
526
+ }, delay);
527
+ if (typeof entry.graceTimer.unref === 'function') {
528
+ entry.graceTimer.unref();
529
+ }
530
+ }
531
+ return;
532
+ }
533
+ await this.teardown(canon);
534
+ });
535
+ }
536
+ /**
537
+ * Unconditional tear-down. Callers must hold the per-target lock.
538
+ */
539
+ async teardown(canon) {
540
+ const entry = this.live.get(canon);
541
+ const hash = targetHash(canon);
542
+ const file = path.join(tunnelsDir(), `${hash}.json`);
543
+ if (entry) {
544
+ if (entry.graceTimer) {
545
+ clearTimeout(entry.graceTimer);
546
+ entry.graceTimer = undefined;
547
+ }
548
+ this.live.delete(canon);
549
+ try {
550
+ await entry.manager.stopTunnel();
551
+ }
552
+ catch {
553
+ /* best-effort */
554
+ }
555
+ }
556
+ safeUnlink(file);
557
+ }
558
+ }
559
+ /** Module-level singleton for CLI use. */
560
+ export const tunnelRegistry = new TunnelRegistry();
561
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../lib/tunnel/registry.ts"],"names":[],"mappings":"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"}
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/qa-use",
3
- "version": "2.14.0",
3
+ "version": "2.15.0",
4
4
  "packageManager": "bun@^1.3.4",
5
5
  "description": "QA automation tool for browser testing with MCP server support",
6
6
  "type": "module",
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Hidden `__browser-detach` subcommand — runs inside the detached child
3
+ * spawned by the parent `browser create` command.
4
+ *
5
+ * Responsibilities:
6
+ * 1. Start the local browser (Playwright).
7
+ * 2. If tunnel mode is 'on', acquire a tunnel via `TunnelRegistry`.
8
+ * 3. Register a backend session via the API.
9
+ * 4. Write a PID file at `~/.qa-use/sessions/<spawn-id>.json` for the
10
+ * parent to poll. Updated in place as milestones complete.
11
+ * 5. Install SIGTERM/SIGINT handlers that tear down cleanly:
12
+ * API session end → tunnel release → browser close → remove PID file.
13
+ * 6. Run a 30s heartbeat that verifies API session + tunnel health; on
14
+ * external close, self-terminate.
15
+ *
16
+ * This subcommand is `.hidden()` on the Commander definition so it does
17
+ * not appear in `qa-use browser --help`. It is ONLY invoked by the
18
+ * parent `browser create` bootstrap via `child_process.spawn`.
19
+ */
20
+ import { Command } from 'commander';
21
+ export declare const detachedCommand: Command;
22
+ /**
23
+ * Convenience export for the lookup table used by PID-file consumers.
24
+ * Kept in sync with `DetachedSessionRecord` shape.
25
+ */
26
+ export declare const __detachedPhaseFileName: (spawnId: string) => string;
27
+ //# sourceMappingURL=_detached.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_detached.d.ts","sourceRoot":"","sources":["../../../../../src/cli/commands/browser/_detached.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAibpC,eAAO,MAAM,eAAe,SA+BxB,CAAC;AASL;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAAI,SAAS,MAAM,KAAG,MAAkC,CAAC"}