@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.
- package/LICENSE +21 -0
- package/README.md +1003 -0
- package/bin/qa-use.js +7 -0
- package/dist/lib/api/index.d.ts +296 -0
- package/dist/lib/api/index.d.ts.map +1 -0
- package/dist/lib/api/index.js +564 -0
- package/dist/lib/api/index.js.map +1 -0
- package/dist/lib/api/sse.d.ts +33 -0
- package/dist/lib/api/sse.d.ts.map +1 -0
- package/dist/lib/api/sse.js +97 -0
- package/dist/lib/api/sse.js.map +1 -0
- package/dist/lib/browser/index.d.ts +28 -0
- package/dist/lib/browser/index.d.ts.map +1 -0
- package/dist/lib/browser/index.js +145 -0
- package/dist/lib/browser/index.js.map +1 -0
- package/dist/lib/env/index.d.ts +41 -0
- package/dist/lib/env/index.d.ts.map +1 -0
- package/dist/lib/env/index.js +125 -0
- package/dist/lib/env/index.js.map +1 -0
- package/dist/lib/tunnel/index.d.ts +38 -0
- package/dist/lib/tunnel/index.d.ts.map +1 -0
- package/dist/lib/tunnel/index.js +154 -0
- package/dist/lib/tunnel/index.js.map +1 -0
- package/dist/package.json +100 -0
- package/dist/src/cli/commands/info.d.ts +6 -0
- package/dist/src/cli/commands/info.d.ts.map +1 -0
- package/dist/src/cli/commands/info.js +32 -0
- package/dist/src/cli/commands/info.js.map +1 -0
- package/dist/src/cli/commands/mcp.d.ts +6 -0
- package/dist/src/cli/commands/mcp.d.ts.map +1 -0
- package/dist/src/cli/commands/mcp.js +45 -0
- package/dist/src/cli/commands/mcp.js.map +1 -0
- package/dist/src/cli/commands/setup.d.ts +6 -0
- package/dist/src/cli/commands/setup.d.ts.map +1 -0
- package/dist/src/cli/commands/setup.js +59 -0
- package/dist/src/cli/commands/setup.js.map +1 -0
- package/dist/src/cli/commands/test/index.d.ts +6 -0
- package/dist/src/cli/commands/test/index.d.ts.map +1 -0
- package/dist/src/cli/commands/test/index.js +15 -0
- package/dist/src/cli/commands/test/index.js.map +1 -0
- package/dist/src/cli/commands/test/init.d.ts +6 -0
- package/dist/src/cli/commands/test/init.d.ts.map +1 -0
- package/dist/src/cli/commands/test/init.js +64 -0
- package/dist/src/cli/commands/test/init.js.map +1 -0
- package/dist/src/cli/commands/test/list.d.ts +6 -0
- package/dist/src/cli/commands/test/list.d.ts.map +1 -0
- package/dist/src/cli/commands/test/list.js +70 -0
- package/dist/src/cli/commands/test/list.js.map +1 -0
- package/dist/src/cli/commands/test/run.d.ts +6 -0
- package/dist/src/cli/commands/test/run.d.ts.map +1 -0
- package/dist/src/cli/commands/test/run.js +95 -0
- package/dist/src/cli/commands/test/run.js.map +1 -0
- package/dist/src/cli/commands/test/validate.d.ts +6 -0
- package/dist/src/cli/commands/test/validate.d.ts.map +1 -0
- package/dist/src/cli/commands/test/validate.js +70 -0
- package/dist/src/cli/commands/test/validate.js.map +1 -0
- package/dist/src/cli/index.d.ts +6 -0
- package/dist/src/cli/index.d.ts.map +1 -0
- package/dist/src/cli/index.js +21 -0
- package/dist/src/cli/index.js.map +1 -0
- package/dist/src/cli/lib/config.d.ts +36 -0
- package/dist/src/cli/lib/config.d.ts.map +1 -0
- package/dist/src/cli/lib/config.js +89 -0
- package/dist/src/cli/lib/config.js.map +1 -0
- package/dist/src/cli/lib/loader.d.ts +49 -0
- package/dist/src/cli/lib/loader.d.ts.map +1 -0
- package/dist/src/cli/lib/loader.js +122 -0
- package/dist/src/cli/lib/loader.js.map +1 -0
- package/dist/src/cli/lib/output.d.ts +53 -0
- package/dist/src/cli/lib/output.d.ts.map +1 -0
- package/dist/src/cli/lib/output.js +133 -0
- package/dist/src/cli/lib/output.js.map +1 -0
- package/dist/src/cli/lib/runner.d.ts +23 -0
- package/dist/src/cli/lib/runner.d.ts.map +1 -0
- package/dist/src/cli/lib/runner.js +40 -0
- package/dist/src/cli/lib/runner.js.map +1 -0
- package/dist/src/http-server.d.ts +14 -0
- package/dist/src/http-server.d.ts.map +1 -0
- package/dist/src/http-server.js +145 -0
- package/dist/src/http-server.js.map +1 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +21 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/server.d.ts +58 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +2376 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/tunnel-mode.d.ts +13 -0
- package/dist/src/tunnel-mode.d.ts.map +1 -0
- package/dist/src/tunnel-mode.js +159 -0
- package/dist/src/tunnel-mode.js.map +1 -0
- package/dist/src/types/test-definition.d.ts +320 -0
- package/dist/src/types/test-definition.d.ts.map +1 -0
- package/dist/src/types/test-definition.js +11 -0
- package/dist/src/types/test-definition.js.map +1 -0
- package/dist/src/types.d.ts +209 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +34 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/package.d.ts +12 -0
- package/dist/src/utils/package.d.ts.map +1 -0
- package/dist/src/utils/package.js +36 -0
- package/dist/src/utils/package.js.map +1 -0
- package/dist/src/utils/summary.d.ts +45 -0
- package/dist/src/utils/summary.d.ts.map +1 -0
- package/dist/src/utils/summary.js +198 -0
- package/dist/src/utils/summary.js.map +1 -0
- package/lib/api/index.ts +977 -0
- package/lib/api/sse.ts +112 -0
- package/lib/browser/index.ts +181 -0
- package/lib/env/index.ts +156 -0
- package/lib/tunnel/index.test.ts +344 -0
- package/lib/tunnel/index.ts +197 -0
- package/lib/tunnel/integration.test.ts +98 -0
- package/package.json +100 -0
- 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
|