@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
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
+ }
@@ -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
+ }