@desplega.ai/qa-use 2.0.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 (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1003 -0
  3. package/bin/qa-use.js +7 -0
  4. package/dist/lib/api/index.d.ts +296 -0
  5. package/dist/lib/api/index.d.ts.map +1 -0
  6. package/dist/lib/api/index.js +564 -0
  7. package/dist/lib/api/index.js.map +1 -0
  8. package/dist/lib/api/sse.d.ts +33 -0
  9. package/dist/lib/api/sse.d.ts.map +1 -0
  10. package/dist/lib/api/sse.js +97 -0
  11. package/dist/lib/api/sse.js.map +1 -0
  12. package/dist/lib/browser/index.d.ts +28 -0
  13. package/dist/lib/browser/index.d.ts.map +1 -0
  14. package/dist/lib/browser/index.js +145 -0
  15. package/dist/lib/browser/index.js.map +1 -0
  16. package/dist/lib/env/index.d.ts +41 -0
  17. package/dist/lib/env/index.d.ts.map +1 -0
  18. package/dist/lib/env/index.js +125 -0
  19. package/dist/lib/env/index.js.map +1 -0
  20. package/dist/lib/tunnel/index.d.ts +38 -0
  21. package/dist/lib/tunnel/index.d.ts.map +1 -0
  22. package/dist/lib/tunnel/index.js +154 -0
  23. package/dist/lib/tunnel/index.js.map +1 -0
  24. package/dist/package.json +100 -0
  25. package/dist/src/cli/commands/info.d.ts +6 -0
  26. package/dist/src/cli/commands/info.d.ts.map +1 -0
  27. package/dist/src/cli/commands/info.js +32 -0
  28. package/dist/src/cli/commands/info.js.map +1 -0
  29. package/dist/src/cli/commands/mcp.d.ts +6 -0
  30. package/dist/src/cli/commands/mcp.d.ts.map +1 -0
  31. package/dist/src/cli/commands/mcp.js +45 -0
  32. package/dist/src/cli/commands/mcp.js.map +1 -0
  33. package/dist/src/cli/commands/setup.d.ts +6 -0
  34. package/dist/src/cli/commands/setup.d.ts.map +1 -0
  35. package/dist/src/cli/commands/setup.js +59 -0
  36. package/dist/src/cli/commands/setup.js.map +1 -0
  37. package/dist/src/cli/commands/test/index.d.ts +6 -0
  38. package/dist/src/cli/commands/test/index.d.ts.map +1 -0
  39. package/dist/src/cli/commands/test/index.js +15 -0
  40. package/dist/src/cli/commands/test/index.js.map +1 -0
  41. package/dist/src/cli/commands/test/init.d.ts +6 -0
  42. package/dist/src/cli/commands/test/init.d.ts.map +1 -0
  43. package/dist/src/cli/commands/test/init.js +64 -0
  44. package/dist/src/cli/commands/test/init.js.map +1 -0
  45. package/dist/src/cli/commands/test/list.d.ts +6 -0
  46. package/dist/src/cli/commands/test/list.d.ts.map +1 -0
  47. package/dist/src/cli/commands/test/list.js +70 -0
  48. package/dist/src/cli/commands/test/list.js.map +1 -0
  49. package/dist/src/cli/commands/test/run.d.ts +6 -0
  50. package/dist/src/cli/commands/test/run.d.ts.map +1 -0
  51. package/dist/src/cli/commands/test/run.js +95 -0
  52. package/dist/src/cli/commands/test/run.js.map +1 -0
  53. package/dist/src/cli/commands/test/validate.d.ts +6 -0
  54. package/dist/src/cli/commands/test/validate.d.ts.map +1 -0
  55. package/dist/src/cli/commands/test/validate.js +70 -0
  56. package/dist/src/cli/commands/test/validate.js.map +1 -0
  57. package/dist/src/cli/index.d.ts +6 -0
  58. package/dist/src/cli/index.d.ts.map +1 -0
  59. package/dist/src/cli/index.js +21 -0
  60. package/dist/src/cli/index.js.map +1 -0
  61. package/dist/src/cli/lib/config.d.ts +36 -0
  62. package/dist/src/cli/lib/config.d.ts.map +1 -0
  63. package/dist/src/cli/lib/config.js +89 -0
  64. package/dist/src/cli/lib/config.js.map +1 -0
  65. package/dist/src/cli/lib/loader.d.ts +49 -0
  66. package/dist/src/cli/lib/loader.d.ts.map +1 -0
  67. package/dist/src/cli/lib/loader.js +122 -0
  68. package/dist/src/cli/lib/loader.js.map +1 -0
  69. package/dist/src/cli/lib/output.d.ts +53 -0
  70. package/dist/src/cli/lib/output.d.ts.map +1 -0
  71. package/dist/src/cli/lib/output.js +133 -0
  72. package/dist/src/cli/lib/output.js.map +1 -0
  73. package/dist/src/cli/lib/runner.d.ts +23 -0
  74. package/dist/src/cli/lib/runner.d.ts.map +1 -0
  75. package/dist/src/cli/lib/runner.js +40 -0
  76. package/dist/src/cli/lib/runner.js.map +1 -0
  77. package/dist/src/http-server.d.ts +14 -0
  78. package/dist/src/http-server.d.ts.map +1 -0
  79. package/dist/src/http-server.js +145 -0
  80. package/dist/src/http-server.js.map +1 -0
  81. package/dist/src/index.d.ts +9 -0
  82. package/dist/src/index.d.ts.map +1 -0
  83. package/dist/src/index.js +21 -0
  84. package/dist/src/index.js.map +1 -0
  85. package/dist/src/server.d.ts +58 -0
  86. package/dist/src/server.d.ts.map +1 -0
  87. package/dist/src/server.js +2376 -0
  88. package/dist/src/server.js.map +1 -0
  89. package/dist/src/tunnel-mode.d.ts +13 -0
  90. package/dist/src/tunnel-mode.d.ts.map +1 -0
  91. package/dist/src/tunnel-mode.js +159 -0
  92. package/dist/src/tunnel-mode.js.map +1 -0
  93. package/dist/src/types/test-definition.d.ts +320 -0
  94. package/dist/src/types/test-definition.d.ts.map +1 -0
  95. package/dist/src/types/test-definition.js +11 -0
  96. package/dist/src/types/test-definition.js.map +1 -0
  97. package/dist/src/types.d.ts +209 -0
  98. package/dist/src/types.d.ts.map +1 -0
  99. package/dist/src/types.js +34 -0
  100. package/dist/src/types.js.map +1 -0
  101. package/dist/src/utils/package.d.ts +12 -0
  102. package/dist/src/utils/package.d.ts.map +1 -0
  103. package/dist/src/utils/package.js +36 -0
  104. package/dist/src/utils/package.js.map +1 -0
  105. package/dist/src/utils/summary.d.ts +45 -0
  106. package/dist/src/utils/summary.d.ts.map +1 -0
  107. package/dist/src/utils/summary.js +198 -0
  108. package/dist/src/utils/summary.js.map +1 -0
  109. package/lib/api/index.ts +977 -0
  110. package/lib/api/sse.ts +112 -0
  111. package/lib/browser/index.ts +181 -0
  112. package/lib/env/index.ts +156 -0
  113. package/lib/tunnel/index.test.ts +344 -0
  114. package/lib/tunnel/index.ts +197 -0
  115. package/lib/tunnel/integration.test.ts +98 -0
  116. package/package.json +100 -0
  117. package/server.json +16 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Server-Sent Events (SSE) parsing utilities
3
+ *
4
+ * Used for streaming test execution progress from /vibe-qa/cli/run endpoint
5
+ */
6
+ /**
7
+ * Parse SSE data from a chunk of text
8
+ *
9
+ * @param chunk - Raw SSE text chunk (may contain multiple events)
10
+ * @returns Array of parsed SSE events
11
+ */
12
+ export function parseSSE(chunk) {
13
+ const events = [];
14
+ const lines = chunk.split('\n');
15
+ let currentEvent = {};
16
+ for (const line of lines) {
17
+ if (line.startsWith('event: ')) {
18
+ currentEvent.event = line.slice(7).trim();
19
+ }
20
+ else if (line.startsWith('data: ')) {
21
+ const dataStr = line.slice(6);
22
+ try {
23
+ currentEvent.data = JSON.parse(dataStr);
24
+ }
25
+ catch {
26
+ currentEvent.data = dataStr;
27
+ }
28
+ }
29
+ else if (line.startsWith('id: ')) {
30
+ currentEvent.id = line.slice(4).trim();
31
+ }
32
+ else if (line === '') {
33
+ // Empty line signals end of event
34
+ if (currentEvent.event && currentEvent.data !== undefined) {
35
+ events.push(currentEvent);
36
+ }
37
+ currentEvent = {};
38
+ }
39
+ }
40
+ return events;
41
+ }
42
+ /**
43
+ * Stream SSE events from a Response object
44
+ *
45
+ * @param response - Fetch Response with SSE stream
46
+ * @yields SSE events as they arrive
47
+ */
48
+ export async function* streamSSE(response) {
49
+ if (!response.body) {
50
+ throw new Error('Response body is null');
51
+ }
52
+ const reader = response.body.getReader();
53
+ const decoder = new TextDecoder();
54
+ let buffer = '';
55
+ try {
56
+ while (true) {
57
+ const { done, value } = await reader.read();
58
+ if (done)
59
+ break;
60
+ buffer += decoder.decode(value, { stream: true });
61
+ // Find complete events (separated by double newlines)
62
+ let pos = buffer.indexOf('\n\n');
63
+ while (pos !== -1) {
64
+ const chunk = buffer.slice(0, pos + 2);
65
+ buffer = buffer.slice(pos + 2);
66
+ const events = parseSSE(chunk);
67
+ for (const event of events) {
68
+ yield event;
69
+ }
70
+ pos = buffer.indexOf('\n\n');
71
+ }
72
+ }
73
+ // Process any remaining buffer
74
+ if (buffer.trim()) {
75
+ const events = parseSSE(buffer);
76
+ for (const event of events) {
77
+ yield event;
78
+ }
79
+ }
80
+ }
81
+ finally {
82
+ reader.releaseLock();
83
+ }
84
+ }
85
+ /**
86
+ * Helper to consume an SSE stream and call a callback for each event
87
+ *
88
+ * @param response - Fetch Response with SSE stream
89
+ * @param onEvent - Callback to handle each event
90
+ * @returns Promise that resolves when stream ends
91
+ */
92
+ export async function consumeSSE(response, onEvent) {
93
+ for await (const event of streamSSE(response)) {
94
+ await onEvent(event);
95
+ }
96
+ }
97
+ //# sourceMappingURL=sse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.js","sourceRoot":"","sources":["../../../lib/api/sse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import type { BrowserServer, Browser } from 'playwright';
2
+ export interface BrowserSession {
3
+ browserServer: BrowserServer;
4
+ wsEndpoint: string;
5
+ isActive: boolean;
6
+ }
7
+ export interface BrowserOptions {
8
+ headless?: boolean;
9
+ devtools?: boolean;
10
+ args?: string[];
11
+ userDataDir?: string;
12
+ isolated?: boolean;
13
+ }
14
+ export declare class BrowserManager {
15
+ private session;
16
+ startBrowser(options?: BrowserOptions): Promise<BrowserSession>;
17
+ stopBrowser(): Promise<void>;
18
+ connectToBrowser(): Promise<Browser | null>;
19
+ getSession(): BrowserSession | null;
20
+ isActive(): boolean;
21
+ /**
22
+ * Check if browser is actually alive by attempting to connect
23
+ */
24
+ checkHealth(): Promise<boolean>;
25
+ getWebSocketEndpoint(): string | null;
26
+ installPlaywrightBrowsers(): Promise<void>;
27
+ }
28
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../lib/browser/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAMzD,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,aAAa,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,OAAO,CAA+B;IAExC,YAAY,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,cAAc,CAAC;IAmDnE,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAe5B,gBAAgB,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAYjD,UAAU,IAAI,cAAc,GAAG,IAAI;IAInC,QAAQ,IAAI,OAAO;IAInB;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAerC,oBAAoB,IAAI,MAAM,GAAG,IAAI;IAI/B,yBAAyB,IAAI,OAAO,CAAC,IAAI,CAAC;CAgDjD"}
@@ -0,0 +1,145 @@
1
+ import { chromium } from 'playwright';
2
+ import { fork } from 'child_process';
3
+ import path from 'path';
4
+ import { createRequire } from 'module';
5
+ import { fileURLToPath } from 'url';
6
+ export class BrowserManager {
7
+ session = null;
8
+ async startBrowser(options = {}) {
9
+ if (this.session) {
10
+ throw new Error('Browser session already active');
11
+ }
12
+ const defaultArgs = [
13
+ '--disable-dev-shm-usage',
14
+ '--no-sandbox',
15
+ '--disable-setuid-sandbox',
16
+ '--disable-gpu',
17
+ '--disable-background-timer-throttling',
18
+ '--disable-renderer-backgrounding',
19
+ '--disable-backgrounding-occluded-windows',
20
+ '--mute-audio',
21
+ '--disable-extensions',
22
+ '--enable-features=NetworkService,NetworkServiceInProcess',
23
+ '--disable-3d-apis',
24
+ '--remote-debugging-port=0',
25
+ ];
26
+ try {
27
+ const browserServer = await chromium.launchServer({
28
+ headless: options.headless ?? true,
29
+ devtools: options.devtools ?? false,
30
+ args: [...defaultArgs, ...(options.args || [])],
31
+ handleSIGINT: false,
32
+ handleSIGTERM: false,
33
+ });
34
+ const wsEndpoint = browserServer.wsEndpoint();
35
+ this.session = {
36
+ browserServer,
37
+ wsEndpoint,
38
+ isActive: true,
39
+ };
40
+ return this.session;
41
+ }
42
+ catch (error) {
43
+ if (error.message.includes("Executable doesn't exist") ||
44
+ error.message.includes('browserType.launch')) {
45
+ throw new Error('Chromium browser is not installed. Please run ensure_installed to install browsers.');
46
+ }
47
+ throw error;
48
+ }
49
+ }
50
+ async stopBrowser() {
51
+ if (!this.session) {
52
+ return;
53
+ }
54
+ const session = this.session;
55
+ this.session = null;
56
+ try {
57
+ await session.browserServer.close();
58
+ }
59
+ catch (error) {
60
+ // Silently handle cleanup errors
61
+ }
62
+ }
63
+ async connectToBrowser() {
64
+ if (!this.session) {
65
+ return null;
66
+ }
67
+ try {
68
+ return await chromium.connect(this.session.wsEndpoint);
69
+ }
70
+ catch (error) {
71
+ return null;
72
+ }
73
+ }
74
+ getSession() {
75
+ return this.session;
76
+ }
77
+ isActive() {
78
+ return this.session?.isActive ?? false;
79
+ }
80
+ /**
81
+ * Check if browser is actually alive by attempting to connect
82
+ */
83
+ async checkHealth() {
84
+ if (!this.session)
85
+ return false;
86
+ try {
87
+ const browser = await chromium.connect(this.session.wsEndpoint, {
88
+ timeout: 5000, // 5 second timeout
89
+ });
90
+ await browser.close();
91
+ return true;
92
+ }
93
+ catch (error) {
94
+ this.session.isActive = false;
95
+ return false;
96
+ }
97
+ }
98
+ getWebSocketEndpoint() {
99
+ return this.session?.wsEndpoint ?? null;
100
+ }
101
+ async installPlaywrightBrowsers() {
102
+ return new Promise((resolve, reject) => {
103
+ try {
104
+ // Use the LOCAL playwright installation from package.json
105
+ // This ensures consistency between install and launch
106
+ // Create require function for ES modules
107
+ const require = createRequire(import.meta.url);
108
+ const playwrightPackagePath = require.resolve('playwright/package.json');
109
+ const cliPath = path.join(path.dirname(playwrightPackagePath), 'cli.js');
110
+ console.error(`Installing browsers using local Playwright at: ${cliPath}`);
111
+ // Fork the local Playwright CLI to install chromium
112
+ const child = fork(cliPath, ['install', 'chromium'], {
113
+ stdio: 'pipe',
114
+ });
115
+ const output = [];
116
+ child.stdout?.on('data', (data) => {
117
+ const message = data.toString();
118
+ output.push(message);
119
+ console.error(message); // Log progress to stderr
120
+ });
121
+ child.stderr?.on('data', (data) => {
122
+ const message = data.toString();
123
+ output.push(message);
124
+ console.error(message); // Log errors to stderr
125
+ });
126
+ child.on('close', (code) => {
127
+ if (code === 0) {
128
+ console.error('✅ Playwright browsers installed successfully');
129
+ resolve();
130
+ }
131
+ else {
132
+ reject(new Error(`Failed to install browser: ${output.join('')}`));
133
+ }
134
+ });
135
+ child.on('error', (error) => {
136
+ reject(new Error(`Failed to start browser installation: ${error.message}`));
137
+ });
138
+ }
139
+ catch (error) {
140
+ reject(new Error(`Failed to locate Playwright CLI: ${error.message}`));
141
+ }
142
+ });
143
+ }
144
+ }
145
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/browser/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Environment variable utility with config file fallback
3
+ *
4
+ * Priority order:
5
+ * 1. Environment variables (process.env)
6
+ * 2. Config file (~/.qa-use.json) with structure: { "env": { "VAR_NAME": "value" } }
7
+ */
8
+ export type EnvSource = 'env' | 'config' | 'none';
9
+ export interface EnvResult {
10
+ value: string | undefined;
11
+ source: EnvSource;
12
+ }
13
+ /**
14
+ * Get an environment variable value with fallback to config file,
15
+ * along with the source of the value
16
+ *
17
+ * @param name - The environment variable name (e.g., 'QA_USE_API_KEY')
18
+ * @returns Object containing the value and its source
19
+ */
20
+ export declare function getEnvWithSource(name: string): EnvResult;
21
+ /**
22
+ * Get an environment variable value with fallback to config file
23
+ *
24
+ * @param name - The environment variable name (e.g., 'QA_USE_API_KEY')
25
+ * @returns The value from env or config file, or undefined if not found
26
+ */
27
+ export declare function getEnv(name: string): string | undefined;
28
+ /**
29
+ * Clear the cached config (useful for testing)
30
+ */
31
+ export declare function clearConfigCache(): void;
32
+ /**
33
+ * Check if config file exists
34
+ */
35
+ export declare function configFileExists(): boolean;
36
+ /**
37
+ * Log the source of key configuration values
38
+ * Call this at startup to inform users where their credentials are coming from
39
+ */
40
+ export declare function logConfigSources(): void;
41
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../lib/env/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA+CH,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAElD,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,EAAE,SAAS,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAcxD;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEvD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAGvC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C;AAgBD;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CA6BvC"}
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Environment variable utility with config file fallback
3
+ *
4
+ * Priority order:
5
+ * 1. Environment variables (process.env)
6
+ * 2. Config file (~/.qa-use.json) with structure: { "env": { "VAR_NAME": "value" } }
7
+ */
8
+ import { readFileSync, existsSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { homedir } from 'os';
11
+ let cachedConfig = null;
12
+ let configLoadAttempted = false;
13
+ /**
14
+ * Get the path to the config file
15
+ */
16
+ function getConfigPath() {
17
+ return join(homedir(), '.qa-use.json');
18
+ }
19
+ /**
20
+ * Load the config file from ~/.qa-use.json
21
+ * Returns null if file doesn't exist or is invalid
22
+ */
23
+ function loadConfig() {
24
+ if (configLoadAttempted) {
25
+ return cachedConfig;
26
+ }
27
+ configLoadAttempted = true;
28
+ const configPath = getConfigPath();
29
+ if (!existsSync(configPath)) {
30
+ return null;
31
+ }
32
+ try {
33
+ const content = readFileSync(configPath, 'utf-8');
34
+ cachedConfig = JSON.parse(content);
35
+ return cachedConfig;
36
+ }
37
+ catch {
38
+ // Invalid JSON or read error - silently ignore
39
+ return null;
40
+ }
41
+ }
42
+ /**
43
+ * Get an environment variable value with fallback to config file,
44
+ * along with the source of the value
45
+ *
46
+ * @param name - The environment variable name (e.g., 'QA_USE_API_KEY')
47
+ * @returns Object containing the value and its source
48
+ */
49
+ export function getEnvWithSource(name) {
50
+ // First check environment variable
51
+ const envValue = process.env[name];
52
+ if (envValue !== undefined && envValue !== '') {
53
+ return { value: envValue, source: 'env' };
54
+ }
55
+ // Fallback to config file
56
+ const config = loadConfig();
57
+ if (config?.env?.[name]) {
58
+ return { value: config.env[name], source: 'config' };
59
+ }
60
+ return { value: undefined, source: 'none' };
61
+ }
62
+ /**
63
+ * Get an environment variable value with fallback to config file
64
+ *
65
+ * @param name - The environment variable name (e.g., 'QA_USE_API_KEY')
66
+ * @returns The value from env or config file, or undefined if not found
67
+ */
68
+ export function getEnv(name) {
69
+ return getEnvWithSource(name).value;
70
+ }
71
+ /**
72
+ * Clear the cached config (useful for testing)
73
+ */
74
+ export function clearConfigCache() {
75
+ cachedConfig = null;
76
+ configLoadAttempted = false;
77
+ }
78
+ /**
79
+ * Check if config file exists
80
+ */
81
+ export function configFileExists() {
82
+ return existsSync(getConfigPath());
83
+ }
84
+ /**
85
+ * Get human-readable source description
86
+ */
87
+ function getSourceDescription(source) {
88
+ switch (source) {
89
+ case 'env':
90
+ return 'environment variable';
91
+ case 'config':
92
+ return `config file (~/.qa-use.json)`;
93
+ case 'none':
94
+ return 'not set';
95
+ }
96
+ }
97
+ /**
98
+ * Log the source of key configuration values
99
+ * Call this at startup to inform users where their credentials are coming from
100
+ */
101
+ export function logConfigSources() {
102
+ const apiKey = getEnvWithSource('QA_USE_API_KEY');
103
+ const apiUrl = getEnvWithSource('QA_USE_API_URL');
104
+ const appUrl = getEnvWithSource('QA_USE_APP_URL');
105
+ const region = getEnvWithSource('QA_USE_REGION');
106
+ const sources = [];
107
+ if (apiKey.value) {
108
+ const maskedKey = apiKey.value.slice(0, 8) + '...' + apiKey.value.slice(-4);
109
+ sources.push(` API Key: ${maskedKey} (from ${getSourceDescription(apiKey.source)})`);
110
+ }
111
+ if (apiUrl.value) {
112
+ sources.push(` API URL: ${apiUrl.value} (from ${getSourceDescription(apiUrl.source)})`);
113
+ }
114
+ if (appUrl.value) {
115
+ sources.push(` App URL: ${appUrl.value} (from ${getSourceDescription(appUrl.source)})`);
116
+ }
117
+ if (region.value) {
118
+ sources.push(` Region: ${region.value} (from ${getSourceDescription(region.source)})`);
119
+ }
120
+ if (sources.length > 0) {
121
+ console.error('Configuration loaded:');
122
+ sources.forEach((s) => console.error(s));
123
+ }
124
+ }
125
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/env/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import localtunnel from '@desplega.ai/localtunnel';
2
+ export interface TunnelSession {
3
+ tunnel: localtunnel.Tunnel;
4
+ publicUrl: string;
5
+ localPort: number;
6
+ isActive: boolean;
7
+ host: string;
8
+ region: string;
9
+ }
10
+ export interface TunnelOptions {
11
+ subdomain?: string;
12
+ localHost?: string;
13
+ apiKey?: string;
14
+ sessionIndex?: number;
15
+ }
16
+ export declare class TunnelManager {
17
+ private session;
18
+ private readonly defaultRegion;
19
+ /**
20
+ * Generate a deterministic subdomain based on API key and session index
21
+ * @param apiKey - The API key to hash
22
+ * @param sessionIndex - Index from 0-9 for concurrent sessions
23
+ * @returns Deterministic subdomain in format: qa-use-{hash}-{index}
24
+ */
25
+ static generateDeterministicSubdomain(apiKey: string, sessionIndex: number): string;
26
+ startTunnel(port: number, options?: TunnelOptions): Promise<TunnelSession>;
27
+ stopTunnel(): Promise<void>;
28
+ getSession(): TunnelSession | null;
29
+ isActive(): boolean;
30
+ /**
31
+ * Check if tunnel is actually alive by pinging the public URL
32
+ */
33
+ checkHealth(): Promise<boolean>;
34
+ getPublicUrl(): string | null;
35
+ getPublicIP(): Promise<string>;
36
+ getWebSocketUrl(originalWsEndpoint: string): string | null;
37
+ }
38
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../lib/tunnel/index.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,0BAA0B,CAAC;AAMnD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAA8B;IAC7C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkB;IAEhD;;;;;OAKG;IACH,MAAM,CAAC,8BAA8B,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;IAa7E,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,aAAa,CAAC;IAuE9E,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAejC,UAAU,IAAI,aAAa,GAAG,IAAI;IAIlC,QAAQ,IAAI,OAAO;IAInB;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAqBrC,YAAY,IAAI,MAAM,GAAG,IAAI;IAIvB,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC;IAcpC,eAAe,CAAC,kBAAkB,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;CAe3D"}
@@ -0,0 +1,154 @@
1
+ import localtunnel from '@desplega.ai/localtunnel';
2
+ import { URL } from 'url';
3
+ import https from 'https';
4
+ import crypto from 'crypto';
5
+ import { getEnv } from '../env/index.js';
6
+ export class TunnelManager {
7
+ session = null;
8
+ defaultRegion = 'auto';
9
+ /**
10
+ * Generate a deterministic subdomain based on API key and session index
11
+ * @param apiKey - The API key to hash
12
+ * @param sessionIndex - Index from 0-9 for concurrent sessions
13
+ * @returns Deterministic subdomain in format: qa-use-{hash}-{index}
14
+ */
15
+ static generateDeterministicSubdomain(apiKey, sessionIndex) {
16
+ // Hash the API key using SHA-256
17
+ const hash = crypto.createHash('sha256').update(apiKey).digest('hex');
18
+ // Take first 6 characters of hash for brevity
19
+ const shortHash = hash.substring(0, 6);
20
+ // Ensure sessionIndex is within valid range (0-9)
21
+ const validIndex = Math.max(0, Math.min(9, sessionIndex));
22
+ return `qa-use-${shortHash}-${validIndex}`;
23
+ }
24
+ async startTunnel(port, options = {}) {
25
+ if (this.session) {
26
+ throw new Error('Tunnel session already active');
27
+ }
28
+ const region = getEnv('QA_USE_REGION') || this.defaultRegion;
29
+ let host = getEnv('TUNNEL_HOST');
30
+ if (!host) {
31
+ // If no manual override, determine host based on region
32
+ if (region === 'us') {
33
+ host = 'https://lt.us.desplega.ai';
34
+ }
35
+ else {
36
+ // Default for 'auto' or unset
37
+ host = 'https://lt.desplega.ai';
38
+ }
39
+ }
40
+ // Determine subdomain: custom > deterministic > random
41
+ let subdomain = options.subdomain;
42
+ if (!subdomain && options.apiKey !== undefined && options.sessionIndex !== undefined) {
43
+ // Use deterministic subdomain based on API key and session index
44
+ subdomain = TunnelManager.generateDeterministicSubdomain(options.apiKey, options.sessionIndex);
45
+ console.log(`Using deterministic subdomain: ${subdomain}`);
46
+ }
47
+ else if (!subdomain) {
48
+ // Fallback to timestamp-based random subdomain
49
+ subdomain = `qa-use-${Date.now().toString().slice(-6)}`;
50
+ console.log(`Using random subdomain: ${subdomain}`);
51
+ }
52
+ console.log(`Starting tunnel on port ${port} with host ${host} in region ${region}`);
53
+ const tunnel = await localtunnel({
54
+ port,
55
+ host,
56
+ subdomain,
57
+ local_host: options.localHost || 'localhost',
58
+ auth: true,
59
+ });
60
+ console.log(`Tunnel started at ${tunnel.url}`);
61
+ this.session = {
62
+ tunnel,
63
+ publicUrl: tunnel.url,
64
+ localPort: port,
65
+ isActive: true,
66
+ host,
67
+ region,
68
+ };
69
+ // Handle tunnel events
70
+ tunnel.on('close', () => {
71
+ if (this.session) {
72
+ this.session.isActive = false;
73
+ }
74
+ });
75
+ tunnel.on('error', (err) => {
76
+ if (this.session) {
77
+ this.session.isActive = false;
78
+ }
79
+ throw err;
80
+ });
81
+ return this.session;
82
+ }
83
+ async stopTunnel() {
84
+ if (!this.session) {
85
+ return;
86
+ }
87
+ const session = this.session;
88
+ this.session = null;
89
+ try {
90
+ session.tunnel.close();
91
+ }
92
+ catch (error) {
93
+ // Silently handle cleanup errors
94
+ }
95
+ }
96
+ getSession() {
97
+ return this.session;
98
+ }
99
+ isActive() {
100
+ return this.session?.isActive ?? false;
101
+ }
102
+ /**
103
+ * Check if tunnel is actually alive by pinging the public URL
104
+ */
105
+ async checkHealth() {
106
+ if (!this.session)
107
+ return false;
108
+ try {
109
+ const controller = new AbortController();
110
+ const timeout = setTimeout(() => controller.abort(), 5000);
111
+ const response = await fetch(this.session.publicUrl, {
112
+ method: 'HEAD',
113
+ signal: controller.signal,
114
+ });
115
+ clearTimeout(timeout);
116
+ // 426 = Upgrade Required (expected for WebSocket endpoint)
117
+ return response.ok || response.status === 426;
118
+ }
119
+ catch (error) {
120
+ this.session.isActive = false;
121
+ return false;
122
+ }
123
+ }
124
+ getPublicUrl() {
125
+ return this.session?.publicUrl ?? null;
126
+ }
127
+ async getPublicIP() {
128
+ return new Promise((resolve, reject) => {
129
+ https
130
+ .get('https://api.ipify.org', (res) => {
131
+ let data = '';
132
+ res.on('data', (chunk) => (data += chunk));
133
+ res.on('end', () => resolve(data.trim()));
134
+ })
135
+ .on('error', (err) => {
136
+ reject(err);
137
+ });
138
+ });
139
+ }
140
+ getWebSocketUrl(originalWsEndpoint) {
141
+ if (!this.session)
142
+ return null;
143
+ try {
144
+ const localWsUrl = new URL(originalWsEndpoint);
145
+ const wsPath = localWsUrl.pathname;
146
+ // Convert HTTP/HTTPS URLs to WebSocket URLs (ws/wss)
147
+ return (this.session.publicUrl.replace('https://', 'wss://').replace('http://', 'ws://') + wsPath);
148
+ }
149
+ catch (error) {
150
+ return null;
151
+ }
152
+ }
153
+ }
154
+ //# sourceMappingURL=index.js.map