@vizzly-testing/cli 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +87 -59
- package/dist/client/index.js +6 -6
- package/dist/commands/doctor.js +15 -15
- package/dist/commands/finalize.js +7 -7
- package/dist/commands/init.js +28 -28
- package/dist/commands/login.js +23 -23
- package/dist/commands/logout.js +4 -4
- package/dist/commands/project.js +36 -36
- package/dist/commands/run.js +33 -33
- package/dist/commands/status.js +14 -14
- package/dist/commands/tdd-daemon.js +43 -43
- package/dist/commands/tdd.js +26 -26
- package/dist/commands/upload.js +32 -32
- package/dist/commands/whoami.js +12 -12
- package/dist/index.js +9 -14
- package/dist/plugin-api.js +43 -0
- package/dist/plugin-loader.js +28 -28
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +19 -19
- package/dist/sdk/index.js +33 -35
- package/dist/server/handlers/api-handler.js +4 -4
- package/dist/server/handlers/tdd-handler.js +22 -21
- package/dist/server/http-server.js +21 -22
- package/dist/server/middleware/json-parser.js +1 -1
- package/dist/server/routers/assets.js +14 -14
- package/dist/server/routers/auth.js +14 -14
- package/dist/server/routers/baseline.js +8 -8
- package/dist/server/routers/cloud-proxy.js +15 -15
- package/dist/server/routers/config.js +11 -11
- package/dist/server/routers/dashboard.js +11 -11
- package/dist/server/routers/health.js +4 -4
- package/dist/server/routers/projects.js +19 -19
- package/dist/server/routers/screenshot.js +9 -9
- package/dist/services/api-service.js +16 -16
- package/dist/services/auth-service.js +17 -17
- package/dist/services/build-manager.js +3 -3
- package/dist/services/config-service.js +32 -32
- package/dist/services/html-report-generator.js +8 -8
- package/dist/services/index.js +11 -11
- package/dist/services/project-service.js +19 -19
- package/dist/services/report-generator/report.css +3 -3
- package/dist/services/report-generator/viewer.js +25 -23
- package/dist/services/screenshot-server.js +1 -1
- package/dist/services/server-manager.js +5 -5
- package/dist/services/static-report-generator.js +14 -14
- package/dist/services/tdd-service.js +152 -110
- package/dist/services/test-runner.js +3 -3
- package/dist/services/uploader.js +10 -8
- package/dist/types/config.d.ts +2 -1
- package/dist/types/index.d.ts +95 -1
- package/dist/types/sdk.d.ts +1 -1
- package/dist/utils/browser.js +3 -3
- package/dist/utils/build-history.js +12 -12
- package/dist/utils/config-loader.js +17 -17
- package/dist/utils/config-schema.js +6 -6
- package/dist/utils/environment-config.js +11 -0
- package/dist/utils/fetch-utils.js +2 -2
- package/dist/utils/file-helpers.js +2 -2
- package/dist/utils/git.js +3 -6
- package/dist/utils/global-config.js +28 -25
- package/dist/utils/output.js +136 -28
- package/dist/utils/package-info.js +3 -3
- package/dist/utils/security.js +12 -12
- package/docs/api-reference.md +52 -23
- package/docs/plugins.md +60 -25
- package/package.json +9 -13
package/dist/commands/login.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Authenticates user via OAuth device flow
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import * as output from '../utils/output.js';
|
|
7
6
|
import { AuthService } from '../services/auth-service.js';
|
|
8
|
-
import { getApiUrl } from '../utils/environment-config.js';
|
|
9
7
|
import { openBrowser } from '../utils/browser.js';
|
|
8
|
+
import { getApiUrl } from '../utils/environment-config.js';
|
|
9
|
+
import * as output from '../utils/output.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Login command implementation using OAuth device flow
|
|
@@ -19,31 +19,31 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
19
19
|
verbose: globalOptions.verbose,
|
|
20
20
|
color: !globalOptions.noColor
|
|
21
21
|
});
|
|
22
|
-
|
|
22
|
+
const colors = output.getColors();
|
|
23
23
|
try {
|
|
24
24
|
output.info('Starting Vizzly authentication...');
|
|
25
25
|
output.blank();
|
|
26
26
|
|
|
27
27
|
// Create auth service
|
|
28
|
-
|
|
28
|
+
const authService = new AuthService({
|
|
29
29
|
baseUrl: options.apiUrl || getApiUrl()
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
// Initiate device flow
|
|
33
33
|
output.startSpinner('Connecting to Vizzly...');
|
|
34
|
-
|
|
34
|
+
const deviceFlow = await authService.initiateDeviceFlow();
|
|
35
35
|
output.stopSpinner();
|
|
36
36
|
|
|
37
37
|
// Handle both snake_case and camelCase field names
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const verificationUri = deviceFlow.verification_uri || deviceFlow.verificationUri;
|
|
39
|
+
const userCode = deviceFlow.user_code || deviceFlow.userCode;
|
|
40
|
+
const deviceCode = deviceFlow.device_code || deviceFlow.deviceCode;
|
|
41
41
|
if (!verificationUri || !userCode || !deviceCode) {
|
|
42
42
|
throw new Error('Invalid device flow response from server');
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Build URL with pre-filled code
|
|
46
|
-
|
|
46
|
+
const urlWithCode = `${verificationUri}?code=${userCode}`;
|
|
47
47
|
|
|
48
48
|
// Display user code prominently
|
|
49
49
|
output.blank();
|
|
@@ -61,7 +61,7 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
61
61
|
output.blank();
|
|
62
62
|
|
|
63
63
|
// Try to open browser with pre-filled code
|
|
64
|
-
|
|
64
|
+
const browserOpened = await openBrowser(urlWithCode);
|
|
65
65
|
if (browserOpened) {
|
|
66
66
|
output.info('Opening browser...');
|
|
67
67
|
} else {
|
|
@@ -83,12 +83,12 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
83
83
|
|
|
84
84
|
// Check authorization status
|
|
85
85
|
output.startSpinner('Checking authorization status...');
|
|
86
|
-
|
|
86
|
+
const pollResponse = await authService.pollDeviceAuthorization(deviceCode);
|
|
87
87
|
output.stopSpinner();
|
|
88
88
|
let tokenData = null;
|
|
89
89
|
|
|
90
90
|
// Check if authorization was successful by looking for tokens
|
|
91
|
-
if (pollResponse.tokens
|
|
91
|
+
if (pollResponse.tokens?.accessToken) {
|
|
92
92
|
// Success! We got tokens
|
|
93
93
|
tokenData = pollResponse;
|
|
94
94
|
} else if (pollResponse.status === 'pending') {
|
|
@@ -103,10 +103,10 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
103
103
|
|
|
104
104
|
// Complete device flow and save tokens
|
|
105
105
|
// Handle both snake_case and camelCase for token data, and nested tokens object
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
const tokensData = tokenData.tokens || tokenData;
|
|
107
|
+
const tokenExpiresIn = tokensData.expiresIn || tokensData.expires_in;
|
|
108
|
+
const tokenExpiresAt = tokenExpiresIn ? new Date(Date.now() + tokenExpiresIn * 1000).toISOString() : tokenData.expires_at || tokenData.expiresAt;
|
|
109
|
+
const tokens = {
|
|
110
110
|
accessToken: tokensData.accessToken || tokensData.access_token,
|
|
111
111
|
refreshToken: tokensData.refreshToken || tokensData.refresh_token,
|
|
112
112
|
expiresAt: tokenExpiresAt,
|
|
@@ -129,7 +129,7 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
129
129
|
if (tokens.organizations && tokens.organizations.length > 0) {
|
|
130
130
|
output.blank();
|
|
131
131
|
output.info('Organizations:');
|
|
132
|
-
for (
|
|
132
|
+
for (const org of tokens.organizations) {
|
|
133
133
|
output.print(` - ${org.name}${org.slug ? ` (@${org.slug})` : ''}`);
|
|
134
134
|
}
|
|
135
135
|
}
|
|
@@ -137,11 +137,11 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
137
137
|
// Show token expiry info
|
|
138
138
|
if (tokens.expiresAt) {
|
|
139
139
|
output.blank();
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
const expiresAt = new Date(tokens.expiresAt);
|
|
141
|
+
const msUntilExpiry = expiresAt.getTime() - Date.now();
|
|
142
|
+
const daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
|
|
143
|
+
const hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
|
|
144
|
+
const minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
|
|
145
145
|
if (daysUntilExpiry > 0) {
|
|
146
146
|
output.info(`Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''} (${expiresAt.toLocaleDateString()})`);
|
|
147
147
|
} else if (hoursUntilExpiry > 0) {
|
|
@@ -180,7 +180,7 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
180
180
|
* @param {Object} options - Command options
|
|
181
181
|
*/
|
|
182
182
|
export function validateLoginOptions() {
|
|
183
|
-
|
|
183
|
+
const errors = [];
|
|
184
184
|
|
|
185
185
|
// No specific validation needed for login command
|
|
186
186
|
// OAuth device flow handles everything via browser
|
package/dist/commands/logout.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Clears stored authentication tokens
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import * as output from '../utils/output.js';
|
|
7
6
|
import { AuthService } from '../services/auth-service.js';
|
|
8
7
|
import { getApiUrl } from '../utils/environment-config.js';
|
|
9
8
|
import { getAuthTokens } from '../utils/global-config.js';
|
|
9
|
+
import * as output from '../utils/output.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Logout command implementation
|
|
@@ -21,7 +21,7 @@ export async function logoutCommand(options = {}, globalOptions = {}) {
|
|
|
21
21
|
});
|
|
22
22
|
try {
|
|
23
23
|
// Check if user is logged in
|
|
24
|
-
|
|
24
|
+
const auth = await getAuthTokens();
|
|
25
25
|
if (!auth || !auth.accessToken) {
|
|
26
26
|
output.info('You are not logged in');
|
|
27
27
|
output.cleanup();
|
|
@@ -30,7 +30,7 @@ export async function logoutCommand(options = {}, globalOptions = {}) {
|
|
|
30
30
|
|
|
31
31
|
// Logout
|
|
32
32
|
output.startSpinner('Logging out...');
|
|
33
|
-
|
|
33
|
+
const authService = new AuthService({
|
|
34
34
|
baseUrl: options.apiUrl || getApiUrl()
|
|
35
35
|
});
|
|
36
36
|
await authService.logout();
|
|
@@ -58,7 +58,7 @@ export async function logoutCommand(options = {}, globalOptions = {}) {
|
|
|
58
58
|
* @param {Object} options - Command options
|
|
59
59
|
*/
|
|
60
60
|
export function validateLogoutOptions() {
|
|
61
|
-
|
|
61
|
+
const errors = [];
|
|
62
62
|
|
|
63
63
|
// No specific validation needed for logout command
|
|
64
64
|
|
package/dist/commands/project.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* Select, list, and manage project tokens
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import readline from 'node:readline';
|
|
7
8
|
import { AuthService } from '../services/auth-service.js';
|
|
8
9
|
import { getApiUrl } from '../utils/environment-config.js';
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import readline from 'readline';
|
|
10
|
+
import { deleteProjectMapping, getAuthTokens, getProjectMapping, getProjectMappings, saveProjectMapping } from '../utils/global-config.js';
|
|
11
|
+
import * as output from '../utils/output.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Project select command - configure project for current directory
|
|
@@ -23,20 +23,20 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
|
|
|
23
23
|
});
|
|
24
24
|
try {
|
|
25
25
|
// Check authentication
|
|
26
|
-
|
|
26
|
+
const auth = await getAuthTokens();
|
|
27
27
|
if (!auth || !auth.accessToken) {
|
|
28
28
|
output.error('Not authenticated');
|
|
29
29
|
output.blank();
|
|
30
30
|
output.info('Run "vizzly login" to authenticate first');
|
|
31
31
|
process.exit(1);
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
const authService = new AuthService({
|
|
34
34
|
baseUrl: options.apiUrl || getApiUrl()
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
// Get user info to show organizations
|
|
38
38
|
output.startSpinner('Fetching organizations...');
|
|
39
|
-
|
|
39
|
+
const userInfo = await authService.whoami();
|
|
40
40
|
output.stopSpinner();
|
|
41
41
|
if (!userInfo.organizations || userInfo.organizations.length === 0) {
|
|
42
42
|
output.error('No organizations found');
|
|
@@ -53,12 +53,12 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
|
|
|
53
53
|
output.print(` ${index + 1}. ${org.name} (@${org.slug})`);
|
|
54
54
|
});
|
|
55
55
|
output.blank();
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
const orgChoice = await promptNumber('Enter number', 1, userInfo.organizations.length);
|
|
57
|
+
const selectedOrg = userInfo.organizations[orgChoice - 1];
|
|
58
58
|
|
|
59
59
|
// List projects for organization
|
|
60
60
|
output.startSpinner(`Fetching projects for ${selectedOrg.name}...`);
|
|
61
|
-
|
|
61
|
+
const response = await makeAuthenticatedRequest(`${options.apiUrl || getApiUrl()}/api/project`, {
|
|
62
62
|
headers: {
|
|
63
63
|
Authorization: `Bearer ${auth.accessToken}`,
|
|
64
64
|
'X-Organization': selectedOrg.slug
|
|
@@ -67,7 +67,7 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
|
|
|
67
67
|
output.stopSpinner();
|
|
68
68
|
|
|
69
69
|
// Handle both array response and object with projects property
|
|
70
|
-
|
|
70
|
+
const projects = Array.isArray(response) ? response : response.projects || [];
|
|
71
71
|
if (projects.length === 0) {
|
|
72
72
|
output.error('No projects found');
|
|
73
73
|
output.blank();
|
|
@@ -83,12 +83,12 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
|
|
|
83
83
|
output.print(` ${index + 1}. ${project.name} (${project.slug})`);
|
|
84
84
|
});
|
|
85
85
|
output.blank();
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
const projectChoice = await promptNumber('Enter number', 1, projects.length);
|
|
87
|
+
const selectedProject = projects[projectChoice - 1];
|
|
88
88
|
|
|
89
89
|
// Create API token for project
|
|
90
90
|
output.startSpinner(`Creating API token for ${selectedProject.name}...`);
|
|
91
|
-
|
|
91
|
+
const tokenResponse = await makeAuthenticatedRequest(`${options.apiUrl || getApiUrl()}/api/project/${selectedProject.slug}/tokens`, {
|
|
92
92
|
method: 'POST',
|
|
93
93
|
headers: {
|
|
94
94
|
Authorization: `Bearer ${auth.accessToken}`,
|
|
@@ -103,7 +103,7 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
|
|
|
103
103
|
output.stopSpinner();
|
|
104
104
|
|
|
105
105
|
// Save project mapping
|
|
106
|
-
|
|
106
|
+
const currentDir = resolve(process.cwd());
|
|
107
107
|
await saveProjectMapping(currentDir, {
|
|
108
108
|
token: tokenResponse.token,
|
|
109
109
|
projectSlug: selectedProject.slug,
|
|
@@ -135,8 +135,8 @@ export async function projectListCommand(_options = {}, globalOptions = {}) {
|
|
|
135
135
|
color: !globalOptions.noColor
|
|
136
136
|
});
|
|
137
137
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
const mappings = await getProjectMappings();
|
|
139
|
+
const paths = Object.keys(mappings);
|
|
140
140
|
if (paths.length === 0) {
|
|
141
141
|
output.info('No projects configured');
|
|
142
142
|
output.blank();
|
|
@@ -151,14 +151,14 @@ export async function projectListCommand(_options = {}, globalOptions = {}) {
|
|
|
151
151
|
}
|
|
152
152
|
output.info('Configured projects:');
|
|
153
153
|
output.blank();
|
|
154
|
-
|
|
155
|
-
for (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
const currentDir = resolve(process.cwd());
|
|
155
|
+
for (const path of paths) {
|
|
156
|
+
const mapping = mappings[path];
|
|
157
|
+
const isCurrent = path === currentDir;
|
|
158
|
+
const marker = isCurrent ? '→' : ' ';
|
|
159
159
|
|
|
160
160
|
// Extract token string (handle both string and object formats)
|
|
161
|
-
|
|
161
|
+
const tokenStr = typeof mapping.token === 'string' ? mapping.token : mapping.token?.token || '[invalid token]';
|
|
162
162
|
output.print(`${marker} ${path}`);
|
|
163
163
|
output.print(` Project: ${mapping.projectName} (${mapping.projectSlug})`);
|
|
164
164
|
output.print(` Organization: ${mapping.organizationSlug}`);
|
|
@@ -187,8 +187,8 @@ export async function projectTokenCommand(_options = {}, globalOptions = {}) {
|
|
|
187
187
|
color: !globalOptions.noColor
|
|
188
188
|
});
|
|
189
189
|
try {
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
const currentDir = resolve(process.cwd());
|
|
191
|
+
const mapping = await getProjectMapping(currentDir);
|
|
192
192
|
if (!mapping) {
|
|
193
193
|
output.error('No project configured for this directory');
|
|
194
194
|
output.blank();
|
|
@@ -197,7 +197,7 @@ export async function projectTokenCommand(_options = {}, globalOptions = {}) {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
// Extract token string (handle both string and object formats)
|
|
200
|
-
|
|
200
|
+
const tokenStr = typeof mapping.token === 'string' ? mapping.token : mapping.token?.token || '[invalid token]';
|
|
201
201
|
if (globalOptions.json) {
|
|
202
202
|
output.data({
|
|
203
203
|
token: tokenStr,
|
|
@@ -224,11 +224,11 @@ export async function projectTokenCommand(_options = {}, globalOptions = {}) {
|
|
|
224
224
|
* Helper to make authenticated API request
|
|
225
225
|
*/
|
|
226
226
|
async function makeAuthenticatedRequest(url, options = {}) {
|
|
227
|
-
|
|
227
|
+
const response = await fetch(url, options);
|
|
228
228
|
if (!response.ok) {
|
|
229
229
|
let errorText = '';
|
|
230
230
|
try {
|
|
231
|
-
|
|
231
|
+
const errorData = await response.json();
|
|
232
232
|
errorText = errorData.error || errorData.message || '';
|
|
233
233
|
} catch {
|
|
234
234
|
errorText = await response.text();
|
|
@@ -243,14 +243,14 @@ async function makeAuthenticatedRequest(url, options = {}) {
|
|
|
243
243
|
*/
|
|
244
244
|
function promptNumber(message, min, max) {
|
|
245
245
|
return new Promise(resolve => {
|
|
246
|
-
|
|
246
|
+
const rl = readline.createInterface({
|
|
247
247
|
input: process.stdin,
|
|
248
248
|
output: process.stdout
|
|
249
249
|
});
|
|
250
|
-
|
|
250
|
+
const ask = () => {
|
|
251
251
|
rl.question(`${message} (${min}-${max}): `, answer => {
|
|
252
|
-
|
|
253
|
-
if (isNaN(num) || num < min || num > max) {
|
|
252
|
+
const num = parseInt(answer, 10);
|
|
253
|
+
if (Number.isNaN(num) || num < min || num > max) {
|
|
254
254
|
output.print(`Please enter a number between ${min} and ${max}`);
|
|
255
255
|
ask();
|
|
256
256
|
} else {
|
|
@@ -275,8 +275,8 @@ export async function projectRemoveCommand(_options = {}, globalOptions = {}) {
|
|
|
275
275
|
color: !globalOptions.noColor
|
|
276
276
|
});
|
|
277
277
|
try {
|
|
278
|
-
|
|
279
|
-
|
|
278
|
+
const currentDir = resolve(process.cwd());
|
|
279
|
+
const mapping = await getProjectMapping(currentDir);
|
|
280
280
|
if (!mapping) {
|
|
281
281
|
output.info('No project configured for this directory');
|
|
282
282
|
output.cleanup();
|
|
@@ -290,7 +290,7 @@ export async function projectRemoveCommand(_options = {}, globalOptions = {}) {
|
|
|
290
290
|
output.print(` Organization: ${mapping.organizationSlug}`);
|
|
291
291
|
output.print(` Directory: ${currentDir}`);
|
|
292
292
|
output.blank();
|
|
293
|
-
|
|
293
|
+
const confirmed = await promptConfirm('Remove this project configuration?');
|
|
294
294
|
if (!confirmed) {
|
|
295
295
|
output.info('Cancelled');
|
|
296
296
|
output.cleanup();
|
|
@@ -312,7 +312,7 @@ export async function projectRemoveCommand(_options = {}, globalOptions = {}) {
|
|
|
312
312
|
*/
|
|
313
313
|
function promptConfirm(message) {
|
|
314
314
|
return new Promise(resolve => {
|
|
315
|
-
|
|
315
|
+
const rl = readline.createInterface({
|
|
316
316
|
input: process.stdin,
|
|
317
317
|
output: process.stdout
|
|
318
318
|
});
|
package/dist/commands/run.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { loadConfig } from '../utils/config-loader.js';
|
|
2
|
-
import * as output from '../utils/output.js';
|
|
3
1
|
import { createServices } from '../services/index.js';
|
|
2
|
+
import { loadConfig } from '../utils/config-loader.js';
|
|
4
3
|
import { detectBranch, detectCommit, detectCommitMessage, detectPullRequestNumber, generateBuildNameWithGit } from '../utils/git.js';
|
|
4
|
+
import * as output from '../utils/output.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Run command implementation
|
|
@@ -21,7 +21,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
21
21
|
let isTddMode = false;
|
|
22
22
|
|
|
23
23
|
// Ensure cleanup on exit
|
|
24
|
-
|
|
24
|
+
const cleanup = async () => {
|
|
25
25
|
output.cleanup();
|
|
26
26
|
|
|
27
27
|
// Cancel test runner (kills process and stops server)
|
|
@@ -36,40 +36,40 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
36
36
|
// Finalize build if we have one
|
|
37
37
|
if (testRunner && buildId) {
|
|
38
38
|
try {
|
|
39
|
-
|
|
39
|
+
const executionTime = Date.now() - (startTime || Date.now());
|
|
40
40
|
await testRunner.finalizeBuild(buildId, isTddMode, false, executionTime);
|
|
41
41
|
} catch {
|
|
42
42
|
// Silent fail on cleanup
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
|
-
|
|
46
|
+
const sigintHandler = async () => {
|
|
47
47
|
await cleanup();
|
|
48
48
|
process.exit(1);
|
|
49
49
|
};
|
|
50
|
-
|
|
50
|
+
const exitHandler = () => output.cleanup();
|
|
51
51
|
process.on('SIGINT', sigintHandler);
|
|
52
52
|
process.on('exit', exitHandler);
|
|
53
53
|
try {
|
|
54
54
|
// Load configuration with CLI overrides
|
|
55
|
-
|
|
55
|
+
const allOptions = {
|
|
56
56
|
...globalOptions,
|
|
57
57
|
...options
|
|
58
58
|
};
|
|
59
59
|
output.debug('[RUN] Loading config', {
|
|
60
60
|
hasToken: !!allOptions.token
|
|
61
61
|
});
|
|
62
|
-
|
|
62
|
+
const config = await loadConfig(globalOptions.config, allOptions);
|
|
63
63
|
output.debug('[RUN] Config loaded', {
|
|
64
64
|
hasApiKey: !!config.apiKey,
|
|
65
|
-
apiKeyPrefix: config.apiKey ? config.apiKey.substring(0, 8)
|
|
65
|
+
apiKeyPrefix: config.apiKey ? `${config.apiKey.substring(0, 8)}***` : 'NONE'
|
|
66
66
|
});
|
|
67
67
|
if (globalOptions.verbose) {
|
|
68
68
|
output.info('Token check:');
|
|
69
69
|
output.debug('Token details', {
|
|
70
70
|
hasApiKey: !!config.apiKey,
|
|
71
71
|
apiKeyType: typeof config.apiKey,
|
|
72
|
-
apiKeyPrefix: typeof config.apiKey === 'string' && config.apiKey ? config.apiKey.substring(0, 10)
|
|
72
|
+
apiKeyPrefix: typeof config.apiKey === 'string' && config.apiKey ? `${config.apiKey.substring(0, 10)}...` : 'none',
|
|
73
73
|
projectSlug: config.projectSlug || 'none',
|
|
74
74
|
organizationSlug: config.organizationSlug || 'none'
|
|
75
75
|
});
|
|
@@ -82,11 +82,11 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// Collect git metadata and build info
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
const branch = await detectBranch(options.branch);
|
|
86
|
+
const commit = await detectCommit(options.commit);
|
|
87
|
+
const message = options.message || (await detectCommitMessage());
|
|
88
|
+
const buildName = await generateBuildNameWithGit(options.buildName);
|
|
89
|
+
const pullRequestNumber = detectPullRequestNumber();
|
|
90
90
|
if (globalOptions.verbose) {
|
|
91
91
|
output.info('Configuration loaded');
|
|
92
92
|
output.debug('Config details', {
|
|
@@ -104,7 +104,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
104
104
|
|
|
105
105
|
// Create service container and get test runner service
|
|
106
106
|
output.startSpinner('Initializing test runner...');
|
|
107
|
-
|
|
107
|
+
const configWithVerbose = {
|
|
108
108
|
...config,
|
|
109
109
|
verbose: globalOptions.verbose,
|
|
110
110
|
uploadAll: options.uploadAll || false
|
|
@@ -112,7 +112,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
112
112
|
output.debug('[RUN] Creating services', {
|
|
113
113
|
hasApiKey: !!configWithVerbose.apiKey
|
|
114
114
|
});
|
|
115
|
-
|
|
115
|
+
const services = createServices(configWithVerbose, 'run');
|
|
116
116
|
testRunner = services.testRunner;
|
|
117
117
|
output.stopSpinner();
|
|
118
118
|
|
|
@@ -121,7 +121,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
121
121
|
|
|
122
122
|
// Set up event handlers
|
|
123
123
|
testRunner.on('progress', progressData => {
|
|
124
|
-
|
|
124
|
+
const {
|
|
125
125
|
message: progressMessage
|
|
126
126
|
} = progressData;
|
|
127
127
|
output.progress(progressMessage || 'Running tests...');
|
|
@@ -164,7 +164,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
164
164
|
});
|
|
165
165
|
|
|
166
166
|
// Prepare run options
|
|
167
|
-
|
|
167
|
+
const runOptions = {
|
|
168
168
|
testCommand,
|
|
169
169
|
port: config.server.port,
|
|
170
170
|
timeout: config.server.timeout,
|
|
@@ -210,8 +210,8 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
210
210
|
// Check if it's a test command failure (as opposed to setup failure)
|
|
211
211
|
if (error.code === 'TEST_COMMAND_FAILED' || error.code === 'TEST_COMMAND_INTERRUPTED') {
|
|
212
212
|
// Extract exit code from error message if available
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
const exitCodeMatch = error.message.match(/exited with code (\d+)/);
|
|
214
|
+
const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 1;
|
|
215
215
|
output.error('Test run failed');
|
|
216
216
|
return {
|
|
217
217
|
success: false,
|
|
@@ -233,10 +233,10 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
233
233
|
if (runOptions.wait) {
|
|
234
234
|
output.info('Waiting for build completion...');
|
|
235
235
|
output.startSpinner('Processing comparisons...');
|
|
236
|
-
|
|
236
|
+
const {
|
|
237
237
|
uploader
|
|
238
238
|
} = services;
|
|
239
|
-
|
|
239
|
+
const buildResult = await uploader.waitForBuild(result.buildId);
|
|
240
240
|
output.success('Build processing completed');
|
|
241
241
|
|
|
242
242
|
// Exit with appropriate code based on comparison results
|
|
@@ -255,11 +255,11 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
255
255
|
|
|
256
256
|
// Provide more context about where the error occurred
|
|
257
257
|
let errorContext = 'Test run failed';
|
|
258
|
-
if (error.message
|
|
258
|
+
if (error.message?.includes('build')) {
|
|
259
259
|
errorContext = 'Build creation failed';
|
|
260
|
-
} else if (error.message
|
|
260
|
+
} else if (error.message?.includes('screenshot')) {
|
|
261
261
|
errorContext = 'Screenshot processing failed';
|
|
262
|
-
} else if (error.message
|
|
262
|
+
} else if (error.message?.includes('server')) {
|
|
263
263
|
errorContext = 'Server startup failed';
|
|
264
264
|
}
|
|
265
265
|
output.error(errorContext, error);
|
|
@@ -277,30 +277,30 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
277
277
|
* @param {Object} options - Command options
|
|
278
278
|
*/
|
|
279
279
|
export function validateRunOptions(testCommand, options) {
|
|
280
|
-
|
|
280
|
+
const errors = [];
|
|
281
281
|
if (!testCommand || testCommand.trim() === '') {
|
|
282
282
|
errors.push('Test command is required');
|
|
283
283
|
}
|
|
284
284
|
if (options.port) {
|
|
285
|
-
|
|
286
|
-
if (isNaN(port) || port < 1 || port > 65535) {
|
|
285
|
+
const port = parseInt(options.port, 10);
|
|
286
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
287
287
|
errors.push('Port must be a valid number between 1 and 65535');
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
290
|
if (options.timeout) {
|
|
291
|
-
|
|
292
|
-
if (isNaN(timeout) || timeout < 1000) {
|
|
291
|
+
const timeout = parseInt(options.timeout, 10);
|
|
292
|
+
if (Number.isNaN(timeout) || timeout < 1000) {
|
|
293
293
|
errors.push('Timeout must be at least 1000 milliseconds');
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
if (options.batchSize !== undefined) {
|
|
297
|
-
|
|
297
|
+
const n = parseInt(options.batchSize, 10);
|
|
298
298
|
if (!Number.isFinite(n) || n <= 0) {
|
|
299
299
|
errors.push('Batch size must be a positive integer');
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
302
|
if (options.uploadTimeout !== undefined) {
|
|
303
|
-
|
|
303
|
+
const n = parseInt(options.uploadTimeout, 10);
|
|
304
304
|
if (!Number.isFinite(n) || n <= 0) {
|
|
305
305
|
errors.push('Upload timeout must be a positive integer (milliseconds)');
|
|
306
306
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { loadConfig } from '../utils/config-loader.js';
|
|
2
|
-
import * as output from '../utils/output.js';
|
|
3
1
|
import { createServices } from '../services/index.js';
|
|
2
|
+
import { loadConfig } from '../utils/config-loader.js';
|
|
4
3
|
import { getApiUrl } from '../utils/environment-config.js';
|
|
4
|
+
import * as output from '../utils/output.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Status command implementation
|
|
@@ -19,11 +19,11 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
19
19
|
output.info(`Checking status for build: ${buildId}`);
|
|
20
20
|
|
|
21
21
|
// Load configuration with CLI overrides
|
|
22
|
-
|
|
22
|
+
const allOptions = {
|
|
23
23
|
...globalOptions,
|
|
24
24
|
...options
|
|
25
25
|
};
|
|
26
|
-
|
|
26
|
+
const config = await loadConfig(globalOptions.config, allOptions);
|
|
27
27
|
|
|
28
28
|
// Validate API token
|
|
29
29
|
if (!config.apiKey) {
|
|
@@ -33,17 +33,17 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
33
33
|
|
|
34
34
|
// Get API service
|
|
35
35
|
output.startSpinner('Fetching build status...');
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const services = createServices(config, 'status');
|
|
37
|
+
const {
|
|
38
38
|
apiService
|
|
39
39
|
} = services;
|
|
40
40
|
|
|
41
41
|
// Get build details via unified ApiService
|
|
42
|
-
|
|
42
|
+
const buildStatus = await apiService.getBuild(buildId);
|
|
43
43
|
output.stopSpinner();
|
|
44
44
|
|
|
45
45
|
// Extract build data from API response
|
|
46
|
-
|
|
46
|
+
const build = buildStatus.build || buildStatus;
|
|
47
47
|
|
|
48
48
|
// Display build summary
|
|
49
49
|
output.success(`Build: ${build.name || build.id}`);
|
|
@@ -77,15 +77,15 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// Show build URL if we can construct it
|
|
80
|
-
|
|
80
|
+
const baseUrl = config.baseUrl || getApiUrl();
|
|
81
81
|
if (baseUrl && build.project_id) {
|
|
82
|
-
|
|
82
|
+
const buildUrl = baseUrl.replace('/api', '') + `/projects/${build.project_id}/builds/${build.id}`;
|
|
83
83
|
output.info(`View Build: ${buildUrl}`);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
// Output JSON data for --json mode
|
|
87
87
|
if (globalOptions.json) {
|
|
88
|
-
|
|
88
|
+
const statusData = {
|
|
89
89
|
buildId: build.id,
|
|
90
90
|
status: build.status,
|
|
91
91
|
name: build.name,
|
|
@@ -131,9 +131,9 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
131
131
|
|
|
132
132
|
// Show progress if build is still processing
|
|
133
133
|
if (build.status === 'processing' || build.status === 'pending') {
|
|
134
|
-
|
|
134
|
+
const totalJobs = build.completed_jobs + build.failed_jobs + build.processing_screenshots;
|
|
135
135
|
if (totalJobs > 0) {
|
|
136
|
-
|
|
136
|
+
const progress = (build.completed_jobs + build.failed_jobs) / totalJobs;
|
|
137
137
|
output.info(`Progress: ${Math.round(progress * 100)}% complete`);
|
|
138
138
|
}
|
|
139
139
|
}
|
|
@@ -155,7 +155,7 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
155
155
|
* @param {Object} options - Command options
|
|
156
156
|
*/
|
|
157
157
|
export function validateStatusOptions(buildId) {
|
|
158
|
-
|
|
158
|
+
const errors = [];
|
|
159
159
|
if (!buildId || buildId.trim() === '') {
|
|
160
160
|
errors.push('Build ID is required');
|
|
161
161
|
}
|