@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
package/lib/api/sse.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
export interface SSEEvent {
|
|
8
|
+
event: string;
|
|
9
|
+
data: any;
|
|
10
|
+
id?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse SSE data from a chunk of text
|
|
15
|
+
*
|
|
16
|
+
* @param chunk - Raw SSE text chunk (may contain multiple events)
|
|
17
|
+
* @returns Array of parsed SSE events
|
|
18
|
+
*/
|
|
19
|
+
export function parseSSE(chunk: string): SSEEvent[] {
|
|
20
|
+
const events: SSEEvent[] = [];
|
|
21
|
+
const lines = chunk.split('\n');
|
|
22
|
+
|
|
23
|
+
let currentEvent: Partial<SSEEvent> = {};
|
|
24
|
+
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
if (line.startsWith('event: ')) {
|
|
27
|
+
currentEvent.event = line.slice(7).trim();
|
|
28
|
+
} else if (line.startsWith('data: ')) {
|
|
29
|
+
const dataStr = line.slice(6);
|
|
30
|
+
try {
|
|
31
|
+
currentEvent.data = JSON.parse(dataStr);
|
|
32
|
+
} catch {
|
|
33
|
+
currentEvent.data = dataStr;
|
|
34
|
+
}
|
|
35
|
+
} else if (line.startsWith('id: ')) {
|
|
36
|
+
currentEvent.id = line.slice(4).trim();
|
|
37
|
+
} else if (line === '') {
|
|
38
|
+
// Empty line signals end of event
|
|
39
|
+
if (currentEvent.event && currentEvent.data !== undefined) {
|
|
40
|
+
events.push(currentEvent as SSEEvent);
|
|
41
|
+
}
|
|
42
|
+
currentEvent = {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return events;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Stream SSE events from a Response object
|
|
51
|
+
*
|
|
52
|
+
* @param response - Fetch Response with SSE stream
|
|
53
|
+
* @yields SSE events as they arrive
|
|
54
|
+
*/
|
|
55
|
+
export async function* streamSSE(response: Response): AsyncGenerator<SSEEvent, void, unknown> {
|
|
56
|
+
if (!response.body) {
|
|
57
|
+
throw new Error('Response body is null');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const reader = response.body.getReader();
|
|
61
|
+
const decoder = new TextDecoder();
|
|
62
|
+
let buffer = '';
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
while (true) {
|
|
66
|
+
const { done, value } = await reader.read();
|
|
67
|
+
if (done) break;
|
|
68
|
+
|
|
69
|
+
buffer += decoder.decode(value, { stream: true });
|
|
70
|
+
|
|
71
|
+
// Find complete events (separated by double newlines)
|
|
72
|
+
let pos = buffer.indexOf('\n\n');
|
|
73
|
+
while (pos !== -1) {
|
|
74
|
+
const chunk = buffer.slice(0, pos + 2);
|
|
75
|
+
buffer = buffer.slice(pos + 2);
|
|
76
|
+
|
|
77
|
+
const events = parseSSE(chunk);
|
|
78
|
+
for (const event of events) {
|
|
79
|
+
yield event;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
pos = buffer.indexOf('\n\n');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Process any remaining buffer
|
|
87
|
+
if (buffer.trim()) {
|
|
88
|
+
const events = parseSSE(buffer);
|
|
89
|
+
for (const event of events) {
|
|
90
|
+
yield event;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} finally {
|
|
94
|
+
reader.releaseLock();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Helper to consume an SSE stream and call a callback for each event
|
|
100
|
+
*
|
|
101
|
+
* @param response - Fetch Response with SSE stream
|
|
102
|
+
* @param onEvent - Callback to handle each event
|
|
103
|
+
* @returns Promise that resolves when stream ends
|
|
104
|
+
*/
|
|
105
|
+
export async function consumeSSE(
|
|
106
|
+
response: Response,
|
|
107
|
+
onEvent: (event: SSEEvent) => void | Promise<void>
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
for await (const event of streamSSE(response)) {
|
|
110
|
+
await onEvent(event);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
import type { BrowserServer, Browser } from 'playwright';
|
|
3
|
+
import { fork } from 'child_process';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { createRequire } from 'module';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
export interface BrowserSession {
|
|
9
|
+
browserServer: BrowserServer;
|
|
10
|
+
wsEndpoint: string;
|
|
11
|
+
isActive: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface BrowserOptions {
|
|
15
|
+
headless?: boolean;
|
|
16
|
+
devtools?: boolean;
|
|
17
|
+
args?: string[];
|
|
18
|
+
userDataDir?: string;
|
|
19
|
+
isolated?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class BrowserManager {
|
|
23
|
+
private session: BrowserSession | null = null;
|
|
24
|
+
|
|
25
|
+
async startBrowser(options: BrowserOptions = {}): Promise<BrowserSession> {
|
|
26
|
+
if (this.session) {
|
|
27
|
+
throw new Error('Browser session already active');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const defaultArgs = [
|
|
31
|
+
'--disable-dev-shm-usage',
|
|
32
|
+
'--no-sandbox',
|
|
33
|
+
'--disable-setuid-sandbox',
|
|
34
|
+
'--disable-gpu',
|
|
35
|
+
'--disable-background-timer-throttling',
|
|
36
|
+
'--disable-renderer-backgrounding',
|
|
37
|
+
'--disable-backgrounding-occluded-windows',
|
|
38
|
+
'--mute-audio',
|
|
39
|
+
'--disable-extensions',
|
|
40
|
+
'--enable-features=NetworkService,NetworkServiceInProcess',
|
|
41
|
+
'--disable-3d-apis',
|
|
42
|
+
'--remote-debugging-port=0',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const browserServer = await chromium.launchServer({
|
|
47
|
+
headless: options.headless ?? true,
|
|
48
|
+
devtools: options.devtools ?? false,
|
|
49
|
+
args: [...defaultArgs, ...(options.args || [])],
|
|
50
|
+
handleSIGINT: false,
|
|
51
|
+
handleSIGTERM: false,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const wsEndpoint = browserServer.wsEndpoint();
|
|
55
|
+
|
|
56
|
+
this.session = {
|
|
57
|
+
browserServer,
|
|
58
|
+
wsEndpoint,
|
|
59
|
+
isActive: true,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return this.session;
|
|
63
|
+
} catch (error: any) {
|
|
64
|
+
if (
|
|
65
|
+
error.message.includes("Executable doesn't exist") ||
|
|
66
|
+
error.message.includes('browserType.launch')
|
|
67
|
+
) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
'Chromium browser is not installed. Please run ensure_installed to install browsers.'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async stopBrowser(): Promise<void> {
|
|
77
|
+
if (!this.session) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const session = this.session;
|
|
82
|
+
this.session = null;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await session.browserServer.close();
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// Silently handle cleanup errors
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async connectToBrowser(): Promise<Browser | null> {
|
|
92
|
+
if (!this.session) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
return await chromium.connect(this.session.wsEndpoint);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
getSession(): BrowserSession | null {
|
|
104
|
+
return this.session;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
isActive(): boolean {
|
|
108
|
+
return this.session?.isActive ?? false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if browser is actually alive by attempting to connect
|
|
113
|
+
*/
|
|
114
|
+
async checkHealth(): Promise<boolean> {
|
|
115
|
+
if (!this.session) return false;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const browser = await chromium.connect(this.session.wsEndpoint, {
|
|
119
|
+
timeout: 5000, // 5 second timeout
|
|
120
|
+
});
|
|
121
|
+
await browser.close();
|
|
122
|
+
return true;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
this.session.isActive = false;
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
getWebSocketEndpoint(): string | null {
|
|
130
|
+
return this.session?.wsEndpoint ?? null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async installPlaywrightBrowsers(): Promise<void> {
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
try {
|
|
136
|
+
// Use the LOCAL playwright installation from package.json
|
|
137
|
+
// This ensures consistency between install and launch
|
|
138
|
+
// Create require function for ES modules
|
|
139
|
+
const require = createRequire(import.meta.url);
|
|
140
|
+
const playwrightPackagePath = require.resolve('playwright/package.json');
|
|
141
|
+
const cliPath = path.join(path.dirname(playwrightPackagePath), 'cli.js');
|
|
142
|
+
|
|
143
|
+
console.error(`Installing browsers using local Playwright at: ${cliPath}`);
|
|
144
|
+
|
|
145
|
+
// Fork the local Playwright CLI to install chromium
|
|
146
|
+
const child = fork(cliPath, ['install', 'chromium'], {
|
|
147
|
+
stdio: 'pipe',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const output: string[] = [];
|
|
151
|
+
|
|
152
|
+
child.stdout?.on('data', (data) => {
|
|
153
|
+
const message = data.toString();
|
|
154
|
+
output.push(message);
|
|
155
|
+
console.error(message); // Log progress to stderr
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
child.stderr?.on('data', (data) => {
|
|
159
|
+
const message = data.toString();
|
|
160
|
+
output.push(message);
|
|
161
|
+
console.error(message); // Log errors to stderr
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
child.on('close', (code) => {
|
|
165
|
+
if (code === 0) {
|
|
166
|
+
console.error('✅ Playwright browsers installed successfully');
|
|
167
|
+
resolve();
|
|
168
|
+
} else {
|
|
169
|
+
reject(new Error(`Failed to install browser: ${output.join('')}`));
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
child.on('error', (error) => {
|
|
174
|
+
reject(new Error(`Failed to start browser installation: ${error.message}`));
|
|
175
|
+
});
|
|
176
|
+
} catch (error: any) {
|
|
177
|
+
reject(new Error(`Failed to locate Playwright CLI: ${error.message}`));
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
package/lib/env/index.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
|
|
9
|
+
import { readFileSync, existsSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
|
|
13
|
+
interface QaUseConfig {
|
|
14
|
+
env?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let cachedConfig: QaUseConfig | null = null;
|
|
18
|
+
let configLoadAttempted = false;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get the path to the config file
|
|
22
|
+
*/
|
|
23
|
+
function getConfigPath(): string {
|
|
24
|
+
return join(homedir(), '.qa-use.json');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load the config file from ~/.qa-use.json
|
|
29
|
+
* Returns null if file doesn't exist or is invalid
|
|
30
|
+
*/
|
|
31
|
+
function loadConfig(): QaUseConfig | null {
|
|
32
|
+
if (configLoadAttempted) {
|
|
33
|
+
return cachedConfig;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
configLoadAttempted = true;
|
|
37
|
+
|
|
38
|
+
const configPath = getConfigPath();
|
|
39
|
+
|
|
40
|
+
if (!existsSync(configPath)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
46
|
+
cachedConfig = JSON.parse(content) as QaUseConfig;
|
|
47
|
+
return cachedConfig;
|
|
48
|
+
} catch {
|
|
49
|
+
// Invalid JSON or read error - silently ignore
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type EnvSource = 'env' | 'config' | 'none';
|
|
55
|
+
|
|
56
|
+
export interface EnvResult {
|
|
57
|
+
value: string | undefined;
|
|
58
|
+
source: EnvSource;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get an environment variable value with fallback to config file,
|
|
63
|
+
* along with the source of the value
|
|
64
|
+
*
|
|
65
|
+
* @param name - The environment variable name (e.g., 'QA_USE_API_KEY')
|
|
66
|
+
* @returns Object containing the value and its source
|
|
67
|
+
*/
|
|
68
|
+
export function getEnvWithSource(name: string): EnvResult {
|
|
69
|
+
// First check environment variable
|
|
70
|
+
const envValue = process.env[name];
|
|
71
|
+
if (envValue !== undefined && envValue !== '') {
|
|
72
|
+
return { value: envValue, source: 'env' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Fallback to config file
|
|
76
|
+
const config = loadConfig();
|
|
77
|
+
if (config?.env?.[name]) {
|
|
78
|
+
return { value: config.env[name], source: 'config' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { value: undefined, source: 'none' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get an environment variable value with fallback to config file
|
|
86
|
+
*
|
|
87
|
+
* @param name - The environment variable name (e.g., 'QA_USE_API_KEY')
|
|
88
|
+
* @returns The value from env or config file, or undefined if not found
|
|
89
|
+
*/
|
|
90
|
+
export function getEnv(name: string): string | undefined {
|
|
91
|
+
return getEnvWithSource(name).value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Clear the cached config (useful for testing)
|
|
96
|
+
*/
|
|
97
|
+
export function clearConfigCache(): void {
|
|
98
|
+
cachedConfig = null;
|
|
99
|
+
configLoadAttempted = false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if config file exists
|
|
104
|
+
*/
|
|
105
|
+
export function configFileExists(): boolean {
|
|
106
|
+
return existsSync(getConfigPath());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get human-readable source description
|
|
111
|
+
*/
|
|
112
|
+
function getSourceDescription(source: EnvSource): string {
|
|
113
|
+
switch (source) {
|
|
114
|
+
case 'env':
|
|
115
|
+
return 'environment variable';
|
|
116
|
+
case 'config':
|
|
117
|
+
return `config file (~/.qa-use.json)`;
|
|
118
|
+
case 'none':
|
|
119
|
+
return 'not set';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Log the source of key configuration values
|
|
125
|
+
* Call this at startup to inform users where their credentials are coming from
|
|
126
|
+
*/
|
|
127
|
+
export function logConfigSources(): void {
|
|
128
|
+
const apiKey = getEnvWithSource('QA_USE_API_KEY');
|
|
129
|
+
const apiUrl = getEnvWithSource('QA_USE_API_URL');
|
|
130
|
+
const appUrl = getEnvWithSource('QA_USE_APP_URL');
|
|
131
|
+
const region = getEnvWithSource('QA_USE_REGION');
|
|
132
|
+
|
|
133
|
+
const sources: string[] = [];
|
|
134
|
+
|
|
135
|
+
if (apiKey.value) {
|
|
136
|
+
const maskedKey = apiKey.value.slice(0, 8) + '...' + apiKey.value.slice(-4);
|
|
137
|
+
sources.push(` API Key: ${maskedKey} (from ${getSourceDescription(apiKey.source)})`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (apiUrl.value) {
|
|
141
|
+
sources.push(` API URL: ${apiUrl.value} (from ${getSourceDescription(apiUrl.source)})`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (appUrl.value) {
|
|
145
|
+
sources.push(` App URL: ${appUrl.value} (from ${getSourceDescription(appUrl.source)})`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (region.value) {
|
|
149
|
+
sources.push(` Region: ${region.value} (from ${getSourceDescription(region.source)})`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (sources.length > 0) {
|
|
153
|
+
console.error('Configuration loaded:');
|
|
154
|
+
sources.forEach((s) => console.error(s));
|
|
155
|
+
}
|
|
156
|
+
}
|