@desplega.ai/qa-use 2.14.1 → 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 (124) 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/startup-sweep.d.ts +45 -0
  94. package/dist/src/cli/lib/startup-sweep.d.ts.map +1 -0
  95. package/dist/src/cli/lib/startup-sweep.js +246 -0
  96. package/dist/src/cli/lib/startup-sweep.js.map +1 -0
  97. package/dist/src/cli/lib/tunnel-banner.d.ts +33 -0
  98. package/dist/src/cli/lib/tunnel-banner.d.ts.map +1 -0
  99. package/dist/src/cli/lib/tunnel-banner.js +55 -0
  100. package/dist/src/cli/lib/tunnel-banner.js.map +1 -0
  101. package/dist/src/cli/lib/tunnel-error-hint.d.ts +20 -0
  102. package/dist/src/cli/lib/tunnel-error-hint.d.ts.map +1 -0
  103. package/dist/src/cli/lib/tunnel-error-hint.js +48 -0
  104. package/dist/src/cli/lib/tunnel-error-hint.js.map +1 -0
  105. package/dist/src/cli/lib/tunnel-option.d.ts +27 -0
  106. package/dist/src/cli/lib/tunnel-option.d.ts.map +1 -0
  107. package/dist/src/cli/lib/tunnel-option.js +77 -0
  108. package/dist/src/cli/lib/tunnel-option.js.map +1 -0
  109. package/dist/src/cli/lib/tunnel-resolve.d.ts +42 -0
  110. package/dist/src/cli/lib/tunnel-resolve.d.ts.map +1 -0
  111. package/dist/src/cli/lib/tunnel-resolve.js +72 -0
  112. package/dist/src/cli/lib/tunnel-resolve.js.map +1 -0
  113. package/lib/env/index.ts +51 -0
  114. package/lib/env/localhost.test.ts +63 -0
  115. package/lib/env/localhost.ts +51 -0
  116. package/lib/env/paths.ts +46 -0
  117. package/lib/env/sessions.test.ts +109 -0
  118. package/lib/env/sessions.ts +155 -0
  119. package/lib/tunnel/errors.test.ts +105 -0
  120. package/lib/tunnel/errors.ts +169 -0
  121. package/lib/tunnel/index.ts +26 -11
  122. package/lib/tunnel/registry.test.ts +420 -0
  123. package/lib/tunnel/registry.ts +646 -0
  124. package/package.json +1 -1
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Auto-tunnel banner helpers (stderr-only, TTY-aware).
3
+ *
4
+ * `printTunnelStartBanner` is the multi-line boxed notice shown when a
5
+ * new tunnel is started. `printTunnelReuseBanner` is the short
6
+ * single-line notice used when an existing tunnel is reused (Phase 3
7
+ * consumer, but lands here so all banner copy lives in one place).
8
+ *
9
+ * Suppression rules — banner prints nothing when ANY of the following
10
+ * hold:
11
+ * - `process.stderr.isTTY` is falsy (piped / redirected stderr)
12
+ * - `process.env.QA_USE_QUIET === '1'`
13
+ * - caller passes `{ quiet: true }`
14
+ */
15
+ function shouldSuppress(quiet) {
16
+ if (quiet)
17
+ return true;
18
+ if (process.env.QA_USE_QUIET === '1')
19
+ return true;
20
+ if (!process.stderr.isTTY)
21
+ return true;
22
+ return false;
23
+ }
24
+ /**
25
+ * Multi-line boxed banner printed on a fresh tunnel start.
26
+ */
27
+ export function printTunnelStartBanner(options) {
28
+ if (shouldSuppress(options.quiet))
29
+ return;
30
+ const lines = [
31
+ '╭─ Auto-tunnel active ──────────────────────────────',
32
+ `│ Detected localhost target: ${options.target}`,
33
+ '│ Remote backend cannot reach your machine directly,',
34
+ '│ so qa-use is exposing it via:',
35
+ '│',
36
+ `│ ${options.publicUrl}`,
37
+ '│',
38
+ '│ Disable with --no-tunnel (or use a public URL).',
39
+ '╰───────────────────────────────────────────────────',
40
+ ];
41
+ for (const line of lines) {
42
+ console.error(line);
43
+ }
44
+ }
45
+ /**
46
+ * Single-line notice printed when an existing tunnel is reused.
47
+ * Used by the Phase-3 registry layer; lives here so all banner copy
48
+ * stays co-located.
49
+ */
50
+ export function printTunnelReuseBanner(options) {
51
+ if (shouldSuppress(options.quiet))
52
+ return;
53
+ console.error(`↻ Auto-tunnel reuse: ${options.target} → ${options.publicUrl}`);
54
+ }
55
+ //# sourceMappingURL=tunnel-banner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel-banner.js","sourceRoot":"","sources":["../../../../src/cli/lib/tunnel-banner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAWH,SAAS,cAAc,CAAC,KAA0B;IAChD,IAAI,KAAK;QAAE,OAAO,IAAI,CAAC;IACvB,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAClD,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACvC,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAA4B;IACjE,IAAI,cAAc,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO;IAE1C,MAAM,KAAK,GAAG;QACZ,sDAAsD;QACtD,gCAAgC,OAAO,CAAC,MAAM,EAAE;QAChD,sDAAsD;QACtD,iCAAiC;QACjC,GAAG;QACH,OAAO,OAAO,CAAC,SAAS,EAAE;QAC1B,GAAG;QACH,mDAAmD;QACnD,sDAAsD;KACvD,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACtB,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAA4B;IACjE,IAAI,cAAc,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO;IAE1C,OAAO,CAAC,KAAK,CAAC,wBAAwB,OAAO,CAAC,MAAM,MAAM,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;AACjF,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Triage-hint formatter for structured tunnel errors.
3
+ *
4
+ * The brainstorm defines the user-facing copy:
5
+ * × Auto-tunnel failed for <target>
6
+ * Attempted: start tunnel via <provider>
7
+ * Likely cause: <subclass-specific>
8
+ * Next steps:
9
+ * • Retry: ...
10
+ * • Skip tunnel: --no-tunnel ...
11
+ * • Check tunnel status: `qa-use tunnel status`
12
+ *
13
+ * Each tunnel-error subclass picks its own "likely cause" line.
14
+ */
15
+ import { type TunnelError } from '../../../lib/tunnel/errors.js';
16
+ /**
17
+ * Return a multi-line string ready to print to stderr.
18
+ */
19
+ export declare function formatTunnelFailure(err: TunnelError): string;
20
+ //# sourceMappingURL=tunnel-error-hint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel-error-hint.d.ts","sourceRoot":"","sources":["../../../../src/cli/lib/tunnel-error-hint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAEL,KAAK,WAAW,EAGjB,MAAM,+BAA+B,CAAC;AAevC;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAoB5D"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Triage-hint formatter for structured tunnel errors.
3
+ *
4
+ * The brainstorm defines the user-facing copy:
5
+ * × Auto-tunnel failed for <target>
6
+ * Attempted: start tunnel via <provider>
7
+ * Likely cause: <subclass-specific>
8
+ * Next steps:
9
+ * • Retry: ...
10
+ * • Skip tunnel: --no-tunnel ...
11
+ * • Check tunnel status: `qa-use tunnel status`
12
+ *
13
+ * Each tunnel-error subclass picks its own "likely cause" line.
14
+ */
15
+ import { TunnelAuthError, TunnelNetworkError, TunnelQuotaError, } from '../../../lib/tunnel/errors.js';
16
+ function likelyCause(err) {
17
+ if (err instanceof TunnelNetworkError) {
18
+ return 'network timeout or connectivity issue reaching the tunnel provider';
19
+ }
20
+ if (err instanceof TunnelAuthError) {
21
+ return 'authentication rejected (bad or expired API key)';
22
+ }
23
+ if (err instanceof TunnelQuotaError) {
24
+ return 'tunnel quota / rate-limit exceeded, or subdomain already in use';
25
+ }
26
+ return 'unknown failure from the tunnel provider';
27
+ }
28
+ /**
29
+ * Return a multi-line string ready to print to stderr.
30
+ */
31
+ export function formatTunnelFailure(err) {
32
+ const target = err.target ?? '<unknown target>';
33
+ const provider = err.provider ?? 'localtunnel';
34
+ const lines = [
35
+ `× Auto-tunnel failed for ${target}`,
36
+ ` Attempted: start tunnel via ${provider}`,
37
+ ` Likely cause: ${likelyCause(err)}`,
38
+ ];
39
+ if (err.message) {
40
+ lines.push(` Error: ${err.message}`);
41
+ }
42
+ lines.push(' Next steps:');
43
+ lines.push(' • Retry: the failure may be transient');
44
+ lines.push(' • Skip tunnel: rerun with --no-tunnel (only works if the backend can reach your target)');
45
+ lines.push(' • Check tunnel status: `qa-use tunnel status`');
46
+ return lines.join('\n');
47
+ }
48
+ //# sourceMappingURL=tunnel-error-hint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel-error-hint.js","sourceRoot":"","sources":["../../../../src/cli/lib/tunnel-error-hint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,eAAe,EAEf,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,+BAA+B,CAAC;AAEvC,SAAS,WAAW,CAAC,GAAgB;IACnC,IAAI,GAAG,YAAY,kBAAkB,EAAE,CAAC;QACtC,OAAO,oEAAoE,CAAC;IAC9E,CAAC;IACD,IAAI,GAAG,YAAY,eAAe,EAAE,CAAC;QACnC,OAAO,kDAAkD,CAAC;IAC5D,CAAC;IACD,IAAI,GAAG,YAAY,gBAAgB,EAAE,CAAC;QACpC,OAAO,iEAAiE,CAAC;IAC3E,CAAC;IACD,OAAO,0CAA0C,CAAC;AACpD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAgB;IAClD,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,kBAAkB,CAAC;IAChD,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,IAAI,aAAa,CAAC;IAE/C,MAAM,KAAK,GAAa;QACtB,4BAA4B,MAAM,EAAE;QACpC,iCAAiC,QAAQ,EAAE;QAC3C,mBAAmB,WAAW,CAAC,GAAG,CAAC,EAAE;KACtC,CAAC;IACF,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;QAChB,KAAK,CAAC,IAAI,CAAC,YAAY,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;IACxC,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAC5B,KAAK,CAAC,IAAI,CAAC,2CAA2C,CAAC,CAAC;IACxD,KAAK,CAAC,IAAI,CACR,6FAA6F,CAC9F,CAAC;IACF,KAAK,CAAC,IAAI,CAAC,mDAAmD,CAAC,CAAC;IAEhE,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Tri-state `--tunnel` option helper for Commander commands.
3
+ *
4
+ * Shape:
5
+ * --tunnel [auto|on|off] (default: auto; bare --tunnel = on)
6
+ * --no-tunnel (alias for --tunnel off)
7
+ *
8
+ * Invalid values are rejected at parse time with a clear error.
9
+ *
10
+ * Bare `--tunnel` (no value) is preserved as sugar for `--tunnel on`, which
11
+ * matches the pre-tri-state UX. Commander represents the bare form by passing
12
+ * the literal string `"true"` (or `undefined` depending on argv shape) through
13
+ * to the parser, so we map both to `'on'`.
14
+ */
15
+ import { type Command } from 'commander';
16
+ export type TunnelMode = 'auto' | 'on' | 'off';
17
+ /**
18
+ * Attach the tri-state `--tunnel` option to a Commander command.
19
+ *
20
+ * The option is stored under the property name `tunnel` on the options
21
+ * object. Callers can read it as a `TunnelMode`.
22
+ *
23
+ * Also registers `--no-tunnel` as an alias that resolves to `'off'`, and
24
+ * accepts bare `--tunnel` (no value) as sugar for `--tunnel on`.
25
+ */
26
+ export declare function addTunnelOption(command: Command): Command;
27
+ //# sourceMappingURL=tunnel-option.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel-option.d.ts","sourceRoot":"","sources":["../../../../src/cli/lib/tunnel-option.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAgC,MAAM,WAAW,CAAC;AAEvE,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,IAAI,GAAG,KAAK,CAAC;AAwC/C;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CA4BzD"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Tri-state `--tunnel` option helper for Commander commands.
3
+ *
4
+ * Shape:
5
+ * --tunnel [auto|on|off] (default: auto; bare --tunnel = on)
6
+ * --no-tunnel (alias for --tunnel off)
7
+ *
8
+ * Invalid values are rejected at parse time with a clear error.
9
+ *
10
+ * Bare `--tunnel` (no value) is preserved as sugar for `--tunnel on`, which
11
+ * matches the pre-tri-state UX. Commander represents the bare form by passing
12
+ * the literal string `"true"` (or `undefined` depending on argv shape) through
13
+ * to the parser, so we map both to `'on'`.
14
+ */
15
+ import { InvalidArgumentError, Option } from 'commander';
16
+ const VALID_MODES = ['auto', 'on', 'off'];
17
+ /**
18
+ * Parser for --tunnel [mode]. Accepts 'auto' | 'on' | 'off'.
19
+ * - Bare `--tunnel` (no value) → 'on' (backward-compat sugar).
20
+ * - `--no-tunnel` (boolean `false`) → 'off'.
21
+ * - Invalid strings are rejected.
22
+ */
23
+ function parseTunnelMode(value) {
24
+ // `--no-tunnel` arrives here as the boolean `false` (Commander's
25
+ // --no-<flag> negation semantics). Map it to 'off'.
26
+ if (value === false) {
27
+ return 'off';
28
+ }
29
+ // Bare `--tunnel` (no value). With the `[mode]` optional-arg form,
30
+ // Commander substitutes the literal string `"true"` as a placeholder
31
+ // when the flag is present without an explicit value. Treat that and
32
+ // `undefined` as the backward-compat sugar → 'on'.
33
+ if (value === undefined || value === true || value === 'true') {
34
+ return 'on';
35
+ }
36
+ if (typeof value !== 'string') {
37
+ throw new InvalidArgumentError(`Invalid --tunnel value. Expected one of: ${VALID_MODES.join(', ')}.`);
38
+ }
39
+ const normalized = value.toLowerCase();
40
+ if (!VALID_MODES.includes(normalized)) {
41
+ throw new InvalidArgumentError(`Invalid --tunnel value: "${value}". Expected one of: ${VALID_MODES.join(', ')}.`);
42
+ }
43
+ return normalized;
44
+ }
45
+ /**
46
+ * Attach the tri-state `--tunnel` option to a Commander command.
47
+ *
48
+ * The option is stored under the property name `tunnel` on the options
49
+ * object. Callers can read it as a `TunnelMode`.
50
+ *
51
+ * Also registers `--no-tunnel` as an alias that resolves to `'off'`, and
52
+ * accepts bare `--tunnel` (no value) as sugar for `--tunnel on`.
53
+ */
54
+ export function addTunnelOption(command) {
55
+ const option = new Option('--tunnel [mode]', 'Tunnel mode: auto (localhost-only), on (force, default when flag present without value), off (never)')
56
+ .default('auto')
57
+ .argParser(parseTunnelMode);
58
+ command.addOption(option);
59
+ // `--no-tunnel` as an alias for `--tunnel off`. Commander treats this as a
60
+ // separate boolean option that, when present, sets the `tunnel` property to
61
+ // `false`. Normalise that to `'off'` in a preAction hook so downstream
62
+ // consumers only ever see a `TunnelMode` string.
63
+ command.option('--no-tunnel', 'Disable tunnel (alias for --tunnel off)');
64
+ command.hook('preAction', (cmd) => {
65
+ const opts = cmd.opts();
66
+ if (opts.tunnel === false) {
67
+ cmd.setOptionValue('tunnel', 'off');
68
+ }
69
+ else if (opts.tunnel === true || opts.tunnel === 'true') {
70
+ // Bare `--tunnel` with no argParser invocation path (belt-and-braces):
71
+ // some Commander versions skip argParser when the optional arg is absent.
72
+ cmd.setOptionValue('tunnel', 'on');
73
+ }
74
+ });
75
+ return command;
76
+ }
77
+ //# sourceMappingURL=tunnel-option.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel-option.js","sourceRoot":"","sources":["../../../../src/cli/lib/tunnel-option.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAgB,oBAAoB,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAIvE,MAAM,WAAW,GAA0B,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,CAAU,CAAC;AAE1E;;;;;GAKG;AACH,SAAS,eAAe,CAAC,KAAc;IACrC,iEAAiE;IACjE,oDAAoD;IACpD,IAAI,KAAK,KAAK,KAAK,EAAE,CAAC;QACpB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,mEAAmE;IACnE,qEAAqE;IACrE,qEAAqE;IACrE,mDAAmD;IACnD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;QAC9D,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,IAAI,oBAAoB,CAC5B,4CAA4C,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACtE,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,KAAK,CAAC,WAAW,EAAgB,CAAC;IACrD,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,oBAAoB,CAC5B,4BAA4B,KAAK,uBAAuB,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAClF,CAAC;IACJ,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,OAAgB;IAC9C,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB,iBAAiB,EACjB,sGAAsG,CACvG;SACE,OAAO,CAAC,MAAoB,CAAC;SAC7B,SAAS,CAAC,eAAe,CAAC,CAAC;IAE9B,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAE1B,2EAA2E;IAC3E,4EAA4E;IAC5E,uEAAuE;IACvE,iDAAiD;IACjD,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,yCAAyC,CAAC,CAAC;IAEzE,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,GAAG,EAAE,EAAE;QAChC,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QACxB,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;YAC1B,GAAG,CAAC,cAAc,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QACtC,CAAC;aAAM,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC1D,uEAAuE;YACvE,0EAA0E;YAC1E,GAAG,CAAC,cAAc,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACrC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Resolve the effective tunnel mode from CLI flag + config file, and
3
+ * turn that mode into a concrete on/off decision using the base URL
4
+ * and API URL.
5
+ *
6
+ * Precedence for `resolveTunnelFlag`:
7
+ * 1. CLI flag (if the user explicitly passed --tunnel / --no-tunnel)
8
+ * 2. Config file (`tunnel` key in ~/.qa-use.json)
9
+ * 3. Default: 'auto'
10
+ *
11
+ * `resolveTunnelMode` is the Phase-2 auto-inference: `'auto'` maps to
12
+ * `'on'` iff `isLocalhostUrl(baseUrl) && !isLocalhostUrl(apiUrl)`, else
13
+ * `'off'`. `'on'` and `'off'` are passed through untouched.
14
+ */
15
+ import type { TunnelMode } from './tunnel-option.js';
16
+ export type { TunnelMode } from './tunnel-option.js';
17
+ /**
18
+ * Resolve the tunnel mode from CLI + config inputs.
19
+ *
20
+ * @param cliFlag - value of the `--tunnel` option after Commander parsing;
21
+ * may be `undefined` when the caller hasn't wired the flag yet, or when
22
+ * the option has no default (but in practice we default to 'auto', so
23
+ * callers will usually pass 'auto' here)
24
+ * @param configFile - value of `~/.qa-use.json`'s `tunnel` key
25
+ */
26
+ export declare function resolveTunnelFlag(cliFlag: TunnelMode | undefined, configFile: TunnelMode | undefined): TunnelMode;
27
+ /**
28
+ * Resolve the concrete on/off decision for the tunnel.
29
+ *
30
+ * Matrix:
31
+ * - `mode === 'on'` → `'on'` (force tunnel, even in dev mode)
32
+ * - `mode === 'off'` → `'off'` (never tunnel)
33
+ * - `mode === 'auto'` → `'on'` iff base URL is localhost and API URL is
34
+ * *not* localhost; otherwise `'off'`.
35
+ *
36
+ * If either `baseUrl` or `apiUrl` is unknown in `'auto'` mode, we stay
37
+ * conservative and return `'off'`: without a localhost base URL there's
38
+ * nothing worth tunnelling, and without a known API URL we can't tell
39
+ * whether we're in dev mode (where auto-tunnel should be skipped).
40
+ */
41
+ export declare function resolveTunnelMode(mode: TunnelMode, baseUrl: string | undefined, apiUrl: string | undefined): 'on' | 'off';
42
+ //# sourceMappingURL=tunnel-resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel-resolve.d.ts","sourceRoot":"","sources":["../../../../src/cli/lib/tunnel-resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,UAAU,GAAG,SAAS,EAC/B,UAAU,EAAE,UAAU,GAAG,SAAS,GACjC,UAAU,CAgBZ;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB,IAAI,GAAG,KAAK,CAWd"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Resolve the effective tunnel mode from CLI flag + config file, and
3
+ * turn that mode into a concrete on/off decision using the base URL
4
+ * and API URL.
5
+ *
6
+ * Precedence for `resolveTunnelFlag`:
7
+ * 1. CLI flag (if the user explicitly passed --tunnel / --no-tunnel)
8
+ * 2. Config file (`tunnel` key in ~/.qa-use.json)
9
+ * 3. Default: 'auto'
10
+ *
11
+ * `resolveTunnelMode` is the Phase-2 auto-inference: `'auto'` maps to
12
+ * `'on'` iff `isLocalhostUrl(baseUrl) && !isLocalhostUrl(apiUrl)`, else
13
+ * `'off'`. `'on'` and `'off'` are passed through untouched.
14
+ */
15
+ import { isLocalhostUrl } from '../../../lib/env/localhost.js';
16
+ /**
17
+ * Resolve the tunnel mode from CLI + config inputs.
18
+ *
19
+ * @param cliFlag - value of the `--tunnel` option after Commander parsing;
20
+ * may be `undefined` when the caller hasn't wired the flag yet, or when
21
+ * the option has no default (but in practice we default to 'auto', so
22
+ * callers will usually pass 'auto' here)
23
+ * @param configFile - value of `~/.qa-use.json`'s `tunnel` key
24
+ */
25
+ export function resolveTunnelFlag(cliFlag, configFile) {
26
+ // Phase 1: we can't reliably distinguish "user passed --tunnel auto" from
27
+ // "Commander filled in the 'auto' default" without extra bookkeeping on the
28
+ // command. So the contract here is: the caller gives us the post-parse
29
+ // value. If they want config precedence to apply, they should pass
30
+ // `undefined` when the CLI value is the unchanged default. To keep this
31
+ // phase simple and predictable — and aligned with the plan's "CLI > config
32
+ // > default 'auto'" precedence — we treat a CLI-passed 'auto' as
33
+ // "no explicit override" so the config value can take effect.
34
+ if (cliFlag !== undefined && cliFlag !== 'auto') {
35
+ return cliFlag;
36
+ }
37
+ if (configFile !== undefined) {
38
+ return configFile;
39
+ }
40
+ return 'auto';
41
+ }
42
+ /**
43
+ * Resolve the concrete on/off decision for the tunnel.
44
+ *
45
+ * Matrix:
46
+ * - `mode === 'on'` → `'on'` (force tunnel, even in dev mode)
47
+ * - `mode === 'off'` → `'off'` (never tunnel)
48
+ * - `mode === 'auto'` → `'on'` iff base URL is localhost and API URL is
49
+ * *not* localhost; otherwise `'off'`.
50
+ *
51
+ * If either `baseUrl` or `apiUrl` is unknown in `'auto'` mode, we stay
52
+ * conservative and return `'off'`: without a localhost base URL there's
53
+ * nothing worth tunnelling, and without a known API URL we can't tell
54
+ * whether we're in dev mode (where auto-tunnel should be skipped).
55
+ */
56
+ export function resolveTunnelMode(mode, baseUrl, apiUrl) {
57
+ if (mode === 'on')
58
+ return 'on';
59
+ if (mode === 'off')
60
+ return 'off';
61
+ // mode === 'auto'
62
+ if (!baseUrl)
63
+ return 'off';
64
+ if (!isLocalhostUrl(baseUrl))
65
+ return 'off';
66
+ // Dev-mode skip: if the API is itself local, the backend can reach
67
+ // localhost directly — no tunnel needed.
68
+ if (apiUrl && isLocalhostUrl(apiUrl))
69
+ return 'off';
70
+ return 'on';
71
+ }
72
+ //# sourceMappingURL=tunnel-resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel-resolve.js","sourceRoot":"","sources":["../../../../src/cli/lib/tunnel-resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAK/D;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAA+B,EAC/B,UAAkC;IAElC,0EAA0E;IAC1E,4EAA4E;IAC5E,uEAAuE;IACvE,mEAAmE;IACnE,wEAAwE;IACxE,2EAA2E;IAC3E,iEAAiE;IACjE,8DAA8D;IAC9D,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;QAChD,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B,OAAO,UAAU,CAAC;IACpB,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAAgB,EAChB,OAA2B,EAC3B,MAA0B;IAE1B,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC/B,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IAEjC,kBAAkB;IAClB,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IAC3B,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3C,mEAAmE;IACnE,yCAAyC;IACzC,IAAI,MAAM,IAAI,cAAc,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAC;IACnD,OAAO,IAAI,CAAC;AACd,CAAC"}
package/lib/env/index.ts CHANGED
@@ -11,11 +11,24 @@ import { existsSync, readFileSync } from 'node:fs';
11
11
  import { homedir } from 'node:os';
12
12
  import { join } from 'node:path';
13
13
 
14
+ export { getPortFromUrl, isLocalhostUrl } from './localhost.js';
15
+
16
+ /**
17
+ * Tunnel mode stored in ~/.qa-use.json.
18
+ * Kept as a string literal here (instead of importing `TunnelMode` from
19
+ * `src/cli/lib/tunnel-option.ts`) to avoid pulling CLI code into the
20
+ * shared env loader.
21
+ */
22
+ export type QaUseTunnelMode = 'auto' | 'on' | 'off';
23
+
24
+ const VALID_TUNNEL_MODES: readonly QaUseTunnelMode[] = ['auto', 'on', 'off'] as const;
25
+
14
26
  interface QaUseConfig {
15
27
  api_key?: string;
16
28
  api_url?: string;
17
29
  app_url?: string;
18
30
  region?: string;
31
+ tunnel?: QaUseTunnelMode;
19
32
  env?: Record<string, string>;
20
33
  }
21
34
 
@@ -215,6 +228,44 @@ export function getCustomHeaders(): Record<string, string> | null {
215
228
  return Object.keys(headers).length > 0 ? headers : null;
216
229
  }
217
230
 
231
+ /**
232
+ * Read the `tunnel` key from `~/.qa-use.json`.
233
+ *
234
+ * Returns one of `'auto' | 'on' | 'off'`, or `undefined` if unset.
235
+ * On an invalid value, logs a one-line stderr warning and returns `undefined`
236
+ * (caller will fall back to the default, typically `'auto'`).
237
+ *
238
+ * Phase 1: config-only (no env-var override layer for tunnel mode).
239
+ */
240
+ let tunnelWarningLogged = false;
241
+ export function getTunnelModeFromConfig(): QaUseTunnelMode | undefined {
242
+ const config = loadConfig();
243
+ if (!config) return undefined;
244
+
245
+ const raw = config.tunnel;
246
+ if (raw === undefined || raw === null) return undefined;
247
+
248
+ if (typeof raw === 'string' && VALID_TUNNEL_MODES.includes(raw as QaUseTunnelMode)) {
249
+ return raw as QaUseTunnelMode;
250
+ }
251
+
252
+ if (!tunnelWarningLogged) {
253
+ console.error(
254
+ `qa-use: invalid "tunnel" value in ~/.qa-use.json: ${JSON.stringify(raw)}. ` +
255
+ `Expected one of: ${VALID_TUNNEL_MODES.join(', ')}. Falling back to "auto".`
256
+ );
257
+ tunnelWarningLogged = true;
258
+ }
259
+ return undefined;
260
+ }
261
+
262
+ /**
263
+ * Reset the tunnel-warning latch (for tests).
264
+ */
265
+ export function clearTunnelWarningLatch(): void {
266
+ tunnelWarningLogged = false;
267
+ }
268
+
218
269
  /**
219
270
  * Get agent session ID from environment if available.
220
271
  * Used for auto-linking browser sessions and test runs to agent sessions.
@@ -0,0 +1,63 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { getPortFromUrl, isLocalhostUrl } from './localhost.js';
3
+
4
+ describe('isLocalhostUrl', () => {
5
+ test('returns true for localhost', () => {
6
+ expect(isLocalhostUrl('http://localhost:3000')).toBe(true);
7
+ expect(isLocalhostUrl('http://localhost')).toBe(true);
8
+ expect(isLocalhostUrl('https://localhost:8080/path')).toBe(true);
9
+ });
10
+
11
+ test('returns true for 127.0.0.1', () => {
12
+ expect(isLocalhostUrl('http://127.0.0.1:5000')).toBe(true);
13
+ expect(isLocalhostUrl('http://127.0.0.1')).toBe(true);
14
+ });
15
+
16
+ test('returns true for ::1 (IPv6)', () => {
17
+ expect(isLocalhostUrl('http://[::1]:3000')).toBe(true);
18
+ expect(isLocalhostUrl('http://[::1]')).toBe(true);
19
+ });
20
+
21
+ test('returns true for *.localhost', () => {
22
+ expect(isLocalhostUrl('http://foo.localhost:3000')).toBe(true);
23
+ expect(isLocalhostUrl('http://api.localhost')).toBe(true);
24
+ });
25
+
26
+ test('returns true for 0.0.0.0', () => {
27
+ expect(isLocalhostUrl('http://0.0.0.0:3000')).toBe(true);
28
+ });
29
+
30
+ test('returns false for example.com', () => {
31
+ expect(isLocalhostUrl('https://example.com')).toBe(false);
32
+ expect(isLocalhostUrl('http://example.com:8080')).toBe(false);
33
+ });
34
+
35
+ test('returns false for private IPs (not actually localhost)', () => {
36
+ expect(isLocalhostUrl('http://192.168.1.1')).toBe(false);
37
+ expect(isLocalhostUrl('http://10.0.0.1')).toBe(false);
38
+ });
39
+
40
+ test('returns false for invalid URLs', () => {
41
+ expect(isLocalhostUrl('not-a-url')).toBe(false);
42
+ expect(isLocalhostUrl('')).toBe(false);
43
+ });
44
+ });
45
+
46
+ describe('getPortFromUrl', () => {
47
+ test('returns explicit port', () => {
48
+ expect(getPortFromUrl('http://localhost:3000')).toBe(3000);
49
+ expect(getPortFromUrl('https://example.com:8443')).toBe(8443);
50
+ });
51
+
52
+ test('defaults to 443 for https without port', () => {
53
+ expect(getPortFromUrl('https://example.com')).toBe(443);
54
+ });
55
+
56
+ test('defaults to 80 for http without port', () => {
57
+ expect(getPortFromUrl('http://example.com')).toBe(80);
58
+ });
59
+
60
+ test('returns 80 for invalid URL', () => {
61
+ expect(getPortFromUrl('not-a-url')).toBe(80);
62
+ });
63
+ });
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Canonical localhost URL helpers.
3
+ *
4
+ * These live in `lib/env/` so they are usable from both the CLI layer and
5
+ * library code (tunnel, browser) without forcing a dependency on `src/cli`.
6
+ */
7
+
8
+ import { URL } from 'node:url';
9
+
10
+ /**
11
+ * Check if a URL points to localhost.
12
+ *
13
+ * Matches:
14
+ * - `localhost`
15
+ * - `127.0.0.1`
16
+ * - `::1`
17
+ * - `*.localhost` (e.g., `foo.localhost`)
18
+ * - `0.0.0.0`
19
+ */
20
+ export function isLocalhostUrl(url: string): boolean {
21
+ try {
22
+ const parsed = new URL(url);
23
+ // Node/Bun's URL parser returns IPv6 hosts wrapped in brackets
24
+ // (e.g. "[::1]"). Strip them for comparison.
25
+ const host = parsed.hostname.replace(/^\[|\]$/g, '');
26
+ return (
27
+ host === 'localhost' ||
28
+ host === '127.0.0.1' ||
29
+ host === '::1' ||
30
+ host === '0.0.0.0' ||
31
+ host.endsWith('.localhost')
32
+ );
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Get the port from a URL, defaulting to 443 for https and 80 otherwise.
40
+ */
41
+ export function getPortFromUrl(url: string): number {
42
+ try {
43
+ const parsed = new URL(url);
44
+ if (parsed.port) {
45
+ return parseInt(parsed.port, 10);
46
+ }
47
+ return parsed.protocol === 'https:' ? 443 : 80;
48
+ } catch {
49
+ return 80;
50
+ }
51
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Filesystem paths used by qa-use for CLI-side state.
3
+ *
4
+ * All paths are relative to `os.homedir()` and created lazily on first
5
+ * write. Reading these directories does not create them — that's important
6
+ * for commands like `tunnel ls` that report an empty state cleanly when
7
+ * nothing has ever been written.
8
+ */
9
+
10
+ import fs from 'node:fs';
11
+ import os from 'node:os';
12
+ import path from 'node:path';
13
+
14
+ /**
15
+ * Base dir for all qa-use CLI-side state. Defaults to `~/.qa-use` but may
16
+ * be overridden via `QA_USE_HOME` (test-friendly).
17
+ */
18
+ export function qaUseDir(): string {
19
+ const override = process.env.QA_USE_HOME;
20
+ if (override && override.length > 0) {
21
+ return override;
22
+ }
23
+ return path.join(os.homedir(), '.qa-use');
24
+ }
25
+
26
+ /**
27
+ * Where persisted tunnel registry entries live. Each active tunnel is a
28
+ * single JSON file named `<sha256(target)[0..10]>.json`.
29
+ */
30
+ export function tunnelsDir(): string {
31
+ return path.join(qaUseDir(), 'tunnels');
32
+ }
33
+
34
+ /**
35
+ * Where detached browser-session PID files live (used by Phase 4 onward).
36
+ */
37
+ export function sessionsDir(): string {
38
+ return path.join(qaUseDir(), 'sessions');
39
+ }
40
+
41
+ /**
42
+ * Ensure a directory exists (recursively). No-op if it already does.
43
+ */
44
+ export function ensureDir(dir: string): void {
45
+ fs.mkdirSync(dir, { recursive: true });
46
+ }