@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
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { homedir } from 'os';
|
|
4
|
-
import {
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
5
|
import * as output from '../utils/output.js';
|
|
6
6
|
import { tddCommand } from './tdd.js';
|
|
7
7
|
|
|
@@ -19,7 +19,7 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
19
19
|
|
|
20
20
|
// Check if server already running
|
|
21
21
|
if (await isServerRunning(options.port || 47392)) {
|
|
22
|
-
|
|
22
|
+
const port = options.port || 47392;
|
|
23
23
|
output.info(`TDD server already running at http://localhost:${port}`);
|
|
24
24
|
output.info(`Dashboard: http://localhost:${port}/dashboard`);
|
|
25
25
|
if (options.open) {
|
|
@@ -29,13 +29,13 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
29
29
|
}
|
|
30
30
|
try {
|
|
31
31
|
// Ensure .vizzly directory exists
|
|
32
|
-
|
|
32
|
+
const vizzlyDir = join(process.cwd(), '.vizzly');
|
|
33
33
|
if (!existsSync(vizzlyDir)) {
|
|
34
34
|
mkdirSync(vizzlyDir, {
|
|
35
35
|
recursive: true
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
const port = options.port || 47392;
|
|
39
39
|
|
|
40
40
|
// Show loading indicator if downloading baselines (but not in verbose mode since child shows progress)
|
|
41
41
|
if (options.baselineBuild && !globalOptions.verbose) {
|
|
@@ -43,7 +43,7 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Spawn child process with stdio inherited during init for direct error visibility
|
|
46
|
-
|
|
46
|
+
const child = spawn(process.execPath, [process.argv[1],
|
|
47
47
|
// CLI entry point
|
|
48
48
|
'tdd', 'start', '--daemon-child',
|
|
49
49
|
// Special flag for child process
|
|
@@ -72,7 +72,7 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
// Timeout after 30 seconds to prevent indefinite wait
|
|
75
|
-
|
|
75
|
+
const timeoutId = setTimeout(() => {
|
|
76
76
|
if (!initComplete && !initFailed) {
|
|
77
77
|
initFailed = true;
|
|
78
78
|
resolve();
|
|
@@ -95,8 +95,8 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
95
95
|
child.unref();
|
|
96
96
|
|
|
97
97
|
// Verify server started with retries
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
const maxRetries = 10;
|
|
99
|
+
const retryDelay = 200; // Start with 200ms
|
|
100
100
|
let running = false;
|
|
101
101
|
for (let i = 0; i < maxRetries && !running; i++) {
|
|
102
102
|
await new Promise(resolve => setTimeout(resolve, retryDelay * (i + 1)));
|
|
@@ -113,14 +113,14 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
113
113
|
|
|
114
114
|
// Write server info to global location for SDK discovery (iOS/Swift can read this)
|
|
115
115
|
try {
|
|
116
|
-
|
|
116
|
+
const globalVizzlyDir = join(homedir(), '.vizzly');
|
|
117
117
|
if (!existsSync(globalVizzlyDir)) {
|
|
118
118
|
mkdirSync(globalVizzlyDir, {
|
|
119
119
|
recursive: true
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
const globalServerFile = join(globalVizzlyDir, 'server.json');
|
|
123
|
+
const serverInfo = {
|
|
124
124
|
pid: child.pid,
|
|
125
125
|
port: port.toString(),
|
|
126
126
|
startTime: Date.now()
|
|
@@ -160,11 +160,11 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
160
160
|
* @private
|
|
161
161
|
*/
|
|
162
162
|
export async function runDaemonChild(options = {}, globalOptions = {}) {
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
const vizzlyDir = join(process.cwd(), '.vizzly');
|
|
164
|
+
const port = options.port || 47392;
|
|
165
165
|
try {
|
|
166
166
|
// Use existing tddCommand but with daemon mode
|
|
167
|
-
|
|
167
|
+
const {
|
|
168
168
|
cleanup
|
|
169
169
|
} = await tddCommand(null,
|
|
170
170
|
// No test command - server only
|
|
@@ -179,9 +179,9 @@ export async function runDaemonChild(options = {}, globalOptions = {}) {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
// Store our PID for the stop command
|
|
182
|
-
|
|
182
|
+
const pidFile = join(vizzlyDir, 'server.pid');
|
|
183
183
|
writeFileSync(pidFile, process.pid.toString());
|
|
184
|
-
|
|
184
|
+
const serverInfo = {
|
|
185
185
|
pid: process.pid,
|
|
186
186
|
port: port,
|
|
187
187
|
startTime: Date.now()
|
|
@@ -189,16 +189,16 @@ export async function runDaemonChild(options = {}, globalOptions = {}) {
|
|
|
189
189
|
writeFileSync(join(vizzlyDir, 'server.json'), JSON.stringify(serverInfo, null, 2));
|
|
190
190
|
|
|
191
191
|
// Set up graceful shutdown
|
|
192
|
-
|
|
192
|
+
const handleShutdown = async () => {
|
|
193
193
|
try {
|
|
194
194
|
// Clean up PID files
|
|
195
195
|
if (existsSync(pidFile)) unlinkSync(pidFile);
|
|
196
|
-
|
|
196
|
+
const serverFile = join(vizzlyDir, 'server.json');
|
|
197
197
|
if (existsSync(serverFile)) unlinkSync(serverFile);
|
|
198
198
|
|
|
199
199
|
// Clean up global server file
|
|
200
200
|
try {
|
|
201
|
-
|
|
201
|
+
const globalServerFile = join(homedir(), '.vizzly', 'server.json');
|
|
202
202
|
if (existsSync(globalServerFile)) unlinkSync(globalServerFile);
|
|
203
203
|
} catch {
|
|
204
204
|
// Non-fatal
|
|
@@ -236,9 +236,9 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
236
236
|
verbose: globalOptions.verbose,
|
|
237
237
|
color: !globalOptions.noColor
|
|
238
238
|
});
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
const vizzlyDir = join(process.cwd(), '.vizzly');
|
|
240
|
+
const pidFile = join(vizzlyDir, 'server.pid');
|
|
241
|
+
const serverFile = join(vizzlyDir, 'server.json');
|
|
242
242
|
|
|
243
243
|
// First try to find process by PID file
|
|
244
244
|
let pid = null;
|
|
@@ -251,10 +251,10 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
// If no PID file or invalid, try to find by port using lsof
|
|
254
|
-
|
|
254
|
+
const port = options.port || 47392;
|
|
255
255
|
if (!pid) {
|
|
256
256
|
try {
|
|
257
|
-
|
|
257
|
+
const lsofProcess = spawn('lsof', ['-ti', `:${port}`], {
|
|
258
258
|
stdio: 'pipe'
|
|
259
259
|
});
|
|
260
260
|
let lsofOutput = '';
|
|
@@ -264,8 +264,8 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
264
264
|
await new Promise(resolve => {
|
|
265
265
|
lsofProcess.on('close', code => {
|
|
266
266
|
if (code === 0 && lsofOutput.trim()) {
|
|
267
|
-
|
|
268
|
-
if (foundPid && !isNaN(foundPid)) {
|
|
267
|
+
const foundPid = parseInt(lsofOutput.trim().split('\n')[0], 10);
|
|
268
|
+
if (foundPid && !Number.isNaN(foundPid)) {
|
|
269
269
|
pid = foundPid;
|
|
270
270
|
}
|
|
271
271
|
}
|
|
@@ -327,21 +327,21 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
327
327
|
* @param {Object} options - Command options
|
|
328
328
|
* @param {Object} globalOptions - Global CLI options
|
|
329
329
|
*/
|
|
330
|
-
export async function tddStatusCommand(
|
|
330
|
+
export async function tddStatusCommand(_options, globalOptions = {}) {
|
|
331
331
|
output.configure({
|
|
332
332
|
json: globalOptions.json,
|
|
333
333
|
verbose: globalOptions.verbose,
|
|
334
334
|
color: !globalOptions.noColor
|
|
335
335
|
});
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
336
|
+
const vizzlyDir = join(process.cwd(), '.vizzly');
|
|
337
|
+
const pidFile = join(vizzlyDir, 'server.pid');
|
|
338
|
+
const serverFile = join(vizzlyDir, 'server.json');
|
|
339
339
|
if (!existsSync(pidFile)) {
|
|
340
340
|
output.info('TDD server not running');
|
|
341
341
|
return;
|
|
342
342
|
}
|
|
343
343
|
try {
|
|
344
|
-
|
|
344
|
+
const pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
|
|
345
345
|
|
|
346
346
|
// Check if process is actually running
|
|
347
347
|
process.kill(pid, 0); // Signal 0 just checks if process exists
|
|
@@ -354,7 +354,7 @@ export async function tddStatusCommand(options, globalOptions = {}) {
|
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
// Try to check health endpoint
|
|
357
|
-
|
|
357
|
+
const health = await checkServerHealth(serverInfo.port);
|
|
358
358
|
if (health.running) {
|
|
359
359
|
output.success(`TDD server running (PID: ${pid})`);
|
|
360
360
|
output.info(`Dashboard: http://localhost:${serverInfo.port}/`);
|
|
@@ -365,10 +365,10 @@ export async function tddStatusCommand(options, globalOptions = {}) {
|
|
|
365
365
|
output.info(` Settings: http://localhost:${serverInfo.port}/settings`);
|
|
366
366
|
output.info(` Projects: http://localhost:${serverInfo.port}/projects`);
|
|
367
367
|
if (serverInfo.startTime) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
368
|
+
const uptime = Math.floor((Date.now() - serverInfo.startTime) / 1000);
|
|
369
|
+
const hours = Math.floor(uptime / 3600);
|
|
370
|
+
const minutes = Math.floor(uptime % 3600 / 60);
|
|
371
|
+
const seconds = uptime % 60;
|
|
372
372
|
let uptimeStr = '';
|
|
373
373
|
if (hours > 0) uptimeStr += `${hours}h `;
|
|
374
374
|
if (minutes > 0 || hours > 0) uptimeStr += `${minutes}m `;
|
|
@@ -398,7 +398,7 @@ export async function tddStatusCommand(options, globalOptions = {}) {
|
|
|
398
398
|
*/
|
|
399
399
|
async function isServerRunning(port = 47392) {
|
|
400
400
|
try {
|
|
401
|
-
|
|
401
|
+
const health = await checkServerHealth(port);
|
|
402
402
|
return health.running;
|
|
403
403
|
} catch {
|
|
404
404
|
return false;
|
|
@@ -411,8 +411,8 @@ async function isServerRunning(port = 47392) {
|
|
|
411
411
|
*/
|
|
412
412
|
async function checkServerHealth(port = 47392) {
|
|
413
413
|
try {
|
|
414
|
-
|
|
415
|
-
|
|
414
|
+
const response = await fetch(`http://localhost:${port}/health`);
|
|
415
|
+
const data = await response.json();
|
|
416
416
|
return {
|
|
417
417
|
running: response.ok,
|
|
418
418
|
port: data.port,
|
|
@@ -430,7 +430,7 @@ async function checkServerHealth(port = 47392) {
|
|
|
430
430
|
* @private
|
|
431
431
|
*/
|
|
432
432
|
function openDashboard(port = 47392) {
|
|
433
|
-
|
|
433
|
+
const url = `http://localhost:${port}/dashboard`;
|
|
434
434
|
|
|
435
435
|
// Cross-platform open command
|
|
436
436
|
let openCmd;
|
package/dist/commands/tdd.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 } from '../utils/git.js';
|
|
4
|
+
import * as output from '../utils/output.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* TDD command implementation
|
|
@@ -20,7 +20,7 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
20
20
|
let isCleanedUp = false;
|
|
21
21
|
|
|
22
22
|
// Create cleanup function that can be called by the caller
|
|
23
|
-
|
|
23
|
+
const cleanup = async () => {
|
|
24
24
|
if (isCleanedUp) return;
|
|
25
25
|
isCleanedUp = true;
|
|
26
26
|
output.cleanup();
|
|
@@ -30,14 +30,14 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
30
30
|
};
|
|
31
31
|
try {
|
|
32
32
|
// Load configuration with CLI overrides
|
|
33
|
-
|
|
33
|
+
const allOptions = {
|
|
34
34
|
...globalOptions,
|
|
35
35
|
...options
|
|
36
36
|
};
|
|
37
|
-
|
|
37
|
+
const config = await loadConfig(globalOptions.config, allOptions);
|
|
38
38
|
|
|
39
39
|
// Dev mode works locally by default - only needs token for baseline download
|
|
40
|
-
|
|
40
|
+
const needsToken = options.baselineBuild || options.baselineComparison;
|
|
41
41
|
if (!config.apiKey && needsToken) {
|
|
42
42
|
throw new Error('API token required when using --baseline-build or --baseline-comparison flags');
|
|
43
43
|
}
|
|
@@ -46,12 +46,12 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
46
46
|
config.allowNoToken = true;
|
|
47
47
|
|
|
48
48
|
// Collect git metadata
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
const branch = await detectBranch(options.branch);
|
|
50
|
+
const commit = await detectCommit(options.commit);
|
|
51
51
|
|
|
52
52
|
// Show header (skip in daemon mode)
|
|
53
53
|
if (!options.daemon) {
|
|
54
|
-
|
|
54
|
+
const mode = config.apiKey ? 'local' : 'local';
|
|
55
55
|
output.header('tdd', mode);
|
|
56
56
|
|
|
57
57
|
// Show config in verbose mode
|
|
@@ -64,17 +64,17 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
64
64
|
|
|
65
65
|
// Create services
|
|
66
66
|
output.startSpinner('Initializing TDD server...');
|
|
67
|
-
|
|
67
|
+
const configWithVerbose = {
|
|
68
68
|
...config,
|
|
69
69
|
verbose: globalOptions.verbose
|
|
70
70
|
};
|
|
71
|
-
|
|
71
|
+
const services = createServices(configWithVerbose, 'tdd');
|
|
72
72
|
testRunner = services.testRunner;
|
|
73
73
|
output.stopSpinner();
|
|
74
74
|
|
|
75
75
|
// Set up event handlers for user feedback
|
|
76
76
|
testRunner.on('progress', progressData => {
|
|
77
|
-
|
|
77
|
+
const {
|
|
78
78
|
message: progressMessage
|
|
79
79
|
} = progressData;
|
|
80
80
|
output.progress(progressMessage || 'Running tests...');
|
|
@@ -96,7 +96,7 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
96
96
|
output.debug('capture', screenshotInfo.name);
|
|
97
97
|
});
|
|
98
98
|
testRunner.on('comparison-result', comparisonInfo => {
|
|
99
|
-
|
|
99
|
+
const {
|
|
100
100
|
name,
|
|
101
101
|
status,
|
|
102
102
|
pixelDifference
|
|
@@ -112,7 +112,7 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
112
112
|
testRunner.on('error', error => {
|
|
113
113
|
output.error('Test runner error', error);
|
|
114
114
|
});
|
|
115
|
-
|
|
115
|
+
const runOptions = {
|
|
116
116
|
testCommand,
|
|
117
117
|
port: config.server.port,
|
|
118
118
|
timeout: config.server.timeout,
|
|
@@ -149,19 +149,19 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
149
149
|
|
|
150
150
|
// Normal dev mode - run tests
|
|
151
151
|
output.debug('run', testCommand);
|
|
152
|
-
|
|
152
|
+
const runResult = await testRunner.run(runOptions);
|
|
153
153
|
|
|
154
154
|
// Show summary
|
|
155
|
-
|
|
155
|
+
const {
|
|
156
156
|
screenshotsCaptured,
|
|
157
157
|
comparisons
|
|
158
158
|
} = runResult;
|
|
159
159
|
|
|
160
160
|
// Determine success based on comparison results
|
|
161
|
-
|
|
161
|
+
const hasFailures = runResult.failed || runResult.comparisons?.some(c => c.status === 'failed');
|
|
162
162
|
if (comparisons && comparisons.length > 0) {
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
const passed = comparisons.filter(c => c.status === 'passed').length;
|
|
164
|
+
const failed = comparisons.filter(c => c.status === 'failed').length;
|
|
165
165
|
if (hasFailures) {
|
|
166
166
|
output.error(`${failed} visual difference${failed !== 1 ? 's' : ''} detected`);
|
|
167
167
|
output.info(`Check .vizzly/diffs/ for diff images`);
|
|
@@ -200,25 +200,25 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
200
200
|
* @param {Object} options - Command options
|
|
201
201
|
*/
|
|
202
202
|
export function validateTddOptions(testCommand, options) {
|
|
203
|
-
|
|
203
|
+
const errors = [];
|
|
204
204
|
if (!testCommand || testCommand.trim() === '') {
|
|
205
205
|
errors.push('Test command is required');
|
|
206
206
|
}
|
|
207
207
|
if (options.port) {
|
|
208
|
-
|
|
209
|
-
if (isNaN(port) || port < 1 || port > 65535) {
|
|
208
|
+
const port = parseInt(options.port, 10);
|
|
209
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
210
210
|
errors.push('Port must be a valid number between 1 and 65535');
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
if (options.timeout) {
|
|
214
|
-
|
|
215
|
-
if (isNaN(timeout) || timeout < 1000) {
|
|
214
|
+
const timeout = parseInt(options.timeout, 10);
|
|
215
|
+
if (Number.isNaN(timeout) || timeout < 1000) {
|
|
216
216
|
errors.push('Timeout must be at least 1000 milliseconds');
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
if (options.threshold !== undefined) {
|
|
220
|
-
|
|
221
|
-
if (isNaN(threshold) || threshold < 0) {
|
|
220
|
+
const threshold = parseFloat(options.threshold);
|
|
221
|
+
if (Number.isNaN(threshold) || threshold < 0) {
|
|
222
222
|
errors.push('Threshold must be a non-negative number (CIEDE2000 Delta E)');
|
|
223
223
|
}
|
|
224
224
|
}
|
package/dist/commands/upload.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import * as output from '../utils/output.js';
|
|
1
|
+
import { ApiService } from '../services/api-service.js';
|
|
3
2
|
import { createServices } from '../services/index.js';
|
|
3
|
+
import { loadConfig } from '../utils/config-loader.js';
|
|
4
4
|
import { detectBranch, detectCommit, detectCommitMessage, detectPullRequestNumber, generateBuildNameWithGit } from '../utils/git.js';
|
|
5
|
-
import
|
|
5
|
+
import * as output from '../utils/output.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Construct proper build URL with org/project context
|
|
@@ -13,13 +13,13 @@ import { ApiService } from '../services/api-service.js';
|
|
|
13
13
|
*/
|
|
14
14
|
async function constructBuildUrl(buildId, apiUrl, apiToken) {
|
|
15
15
|
try {
|
|
16
|
-
|
|
16
|
+
const apiService = new ApiService({
|
|
17
17
|
baseUrl: apiUrl,
|
|
18
18
|
token: apiToken,
|
|
19
19
|
command: 'upload'
|
|
20
20
|
});
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const tokenContext = await apiService.getTokenContext();
|
|
22
|
+
const baseUrl = apiUrl.replace(/\/api.*$/, '');
|
|
23
23
|
if (tokenContext.organization?.slug && tokenContext.project?.slug) {
|
|
24
24
|
return `${baseUrl}/${tokenContext.organization.slug}/${tokenContext.project.slug}/builds/${buildId}`;
|
|
25
25
|
}
|
|
@@ -31,7 +31,7 @@ async function constructBuildUrl(buildId, apiUrl, apiToken) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// Fallback URL construction
|
|
34
|
-
|
|
34
|
+
const baseUrl = apiUrl.replace(/\/api.*$/, '');
|
|
35
35
|
return `${baseUrl}/builds/${buildId}`;
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -49,12 +49,12 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
49
49
|
});
|
|
50
50
|
let buildId = null;
|
|
51
51
|
let config = null;
|
|
52
|
-
|
|
52
|
+
const uploadStartTime = Date.now();
|
|
53
53
|
try {
|
|
54
54
|
output.info('Starting upload process...');
|
|
55
55
|
|
|
56
56
|
// Load configuration with CLI overrides
|
|
57
|
-
|
|
57
|
+
const allOptions = {
|
|
58
58
|
...globalOptions,
|
|
59
59
|
...options
|
|
60
60
|
};
|
|
@@ -67,11 +67,11 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// Collect git metadata if not provided
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
const branch = await detectBranch(options.branch);
|
|
71
|
+
const commit = await detectCommit(options.commit);
|
|
72
|
+
const message = options.message || (await detectCommitMessage());
|
|
73
|
+
const buildName = await generateBuildNameWithGit(options.buildName);
|
|
74
|
+
const pullRequestNumber = detectPullRequestNumber();
|
|
75
75
|
output.info(`Uploading screenshots from: ${screenshotsPath}`);
|
|
76
76
|
if (globalOptions.verbose) {
|
|
77
77
|
output.info('Configuration loaded');
|
|
@@ -85,11 +85,11 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
85
85
|
|
|
86
86
|
// Get uploader service
|
|
87
87
|
output.startSpinner('Initializing uploader...');
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
const services = createServices(config, 'upload');
|
|
89
|
+
const uploader = services.uploader;
|
|
90
90
|
|
|
91
91
|
// Prepare upload options with progress callback
|
|
92
|
-
|
|
92
|
+
const uploadOptions = {
|
|
93
93
|
screenshotsDir: screenshotsPath,
|
|
94
94
|
buildName,
|
|
95
95
|
branch,
|
|
@@ -102,7 +102,7 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
102
102
|
pullRequestNumber,
|
|
103
103
|
parallelId: config.parallelId,
|
|
104
104
|
onProgress: progressData => {
|
|
105
|
-
|
|
105
|
+
const {
|
|
106
106
|
message: progressMessage,
|
|
107
107
|
current,
|
|
108
108
|
total,
|
|
@@ -128,19 +128,19 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
128
128
|
|
|
129
129
|
// Start upload
|
|
130
130
|
output.progress('Starting upload...');
|
|
131
|
-
|
|
131
|
+
const result = await uploader.upload(uploadOptions);
|
|
132
132
|
buildId = result.buildId; // Ensure we have the buildId
|
|
133
133
|
|
|
134
134
|
// Mark build as completed
|
|
135
135
|
if (result.buildId) {
|
|
136
136
|
output.progress('Finalizing build...');
|
|
137
137
|
try {
|
|
138
|
-
|
|
138
|
+
const apiService = new ApiService({
|
|
139
139
|
baseUrl: config.apiUrl,
|
|
140
140
|
token: config.apiKey,
|
|
141
141
|
command: 'upload'
|
|
142
142
|
});
|
|
143
|
-
|
|
143
|
+
const executionTime = Date.now() - uploadStartTime;
|
|
144
144
|
await apiService.finalizeBuild(result.buildId, true, executionTime);
|
|
145
145
|
} catch (error) {
|
|
146
146
|
output.warn(`Failed to finalize build: ${error.message}`);
|
|
@@ -152,7 +152,7 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
152
152
|
if (result.buildId) {
|
|
153
153
|
output.info(`🐻 Vizzly: Uploaded ${result.stats.uploaded} of ${result.stats.total} screenshots to build ${result.buildId}`);
|
|
154
154
|
// Use API-provided URL or construct proper URL with org/project context
|
|
155
|
-
|
|
155
|
+
const buildUrl = result.url || (await constructBuildUrl(result.buildId, config.apiUrl, config.apiKey));
|
|
156
156
|
output.info(`🔗 Vizzly: View results at ${buildUrl}`);
|
|
157
157
|
}
|
|
158
158
|
|
|
@@ -160,7 +160,7 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
160
160
|
if (options.wait && result.buildId) {
|
|
161
161
|
output.info('Waiting for build completion...');
|
|
162
162
|
output.startSpinner('Processing comparisons...');
|
|
163
|
-
|
|
163
|
+
const buildResult = await uploader.waitForBuild(result.buildId);
|
|
164
164
|
output.success('Build processing completed');
|
|
165
165
|
|
|
166
166
|
// Show build processing results
|
|
@@ -170,7 +170,7 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
170
170
|
output.success(`All ${buildResult.passedComparisons} visual comparisons passed`);
|
|
171
171
|
}
|
|
172
172
|
// Use API-provided URL or construct proper URL with org/project context
|
|
173
|
-
|
|
173
|
+
const buildUrl = buildResult.url || (await constructBuildUrl(result.buildId, config.apiUrl, config.apiKey));
|
|
174
174
|
output.info(`🔗 Vizzly: View results at ${buildUrl}`);
|
|
175
175
|
}
|
|
176
176
|
output.cleanup();
|
|
@@ -178,19 +178,19 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
178
178
|
// Mark build as failed if we have a buildId and config
|
|
179
179
|
if (buildId && config) {
|
|
180
180
|
try {
|
|
181
|
-
|
|
181
|
+
const apiService = new ApiService({
|
|
182
182
|
baseUrl: config.apiUrl,
|
|
183
183
|
token: config.apiKey,
|
|
184
184
|
command: 'upload'
|
|
185
185
|
});
|
|
186
|
-
|
|
186
|
+
const executionTime = Date.now() - uploadStartTime;
|
|
187
187
|
await apiService.finalizeBuild(buildId, false, executionTime);
|
|
188
188
|
} catch {
|
|
189
189
|
// Silent fail on cleanup
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
// Use user-friendly error message if available
|
|
193
|
-
|
|
193
|
+
const errorMessage = error?.getUserMessage ? error.getUserMessage() : error.message;
|
|
194
194
|
output.error(errorMessage || 'Upload failed', error);
|
|
195
195
|
process.exit(1);
|
|
196
196
|
}
|
|
@@ -202,7 +202,7 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
202
202
|
* @param {Object} options - Command options
|
|
203
203
|
*/
|
|
204
204
|
export function validateUploadOptions(screenshotsPath, options) {
|
|
205
|
-
|
|
205
|
+
const errors = [];
|
|
206
206
|
if (!screenshotsPath) {
|
|
207
207
|
errors.push('Screenshots path is required');
|
|
208
208
|
}
|
|
@@ -214,19 +214,19 @@ export function validateUploadOptions(screenshotsPath, options) {
|
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
if (options.threshold !== undefined) {
|
|
217
|
-
|
|
218
|
-
if (isNaN(threshold) || threshold < 0) {
|
|
217
|
+
const threshold = parseFloat(options.threshold);
|
|
218
|
+
if (Number.isNaN(threshold) || threshold < 0) {
|
|
219
219
|
errors.push('Threshold must be a non-negative number (CIEDE2000 Delta E)');
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
if (options.batchSize !== undefined) {
|
|
223
|
-
|
|
223
|
+
const n = parseInt(options.batchSize, 10);
|
|
224
224
|
if (!Number.isFinite(n) || n <= 0) {
|
|
225
225
|
errors.push('Batch size must be a positive integer');
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
if (options.uploadTimeout !== undefined) {
|
|
229
|
-
|
|
229
|
+
const n = parseInt(options.uploadTimeout, 10);
|
|
230
230
|
if (!Number.isFinite(n) || n <= 0) {
|
|
231
231
|
errors.push('Upload timeout must be a positive integer (milliseconds)');
|
|
232
232
|
}
|
package/dist/commands/whoami.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Shows current user and authentication status
|
|
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
|
* Whoami command implementation
|
|
@@ -21,7 +21,7 @@ export async function whoamiCommand(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
|
if (globalOptions.json) {
|
|
27
27
|
output.data({
|
|
@@ -38,10 +38,10 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
|
|
|
38
38
|
|
|
39
39
|
// Get current user info
|
|
40
40
|
output.startSpinner('Fetching user information...');
|
|
41
|
-
|
|
41
|
+
const authService = new AuthService({
|
|
42
42
|
baseUrl: options.apiUrl || getApiUrl()
|
|
43
43
|
});
|
|
44
|
-
|
|
44
|
+
const response = await authService.whoami();
|
|
45
45
|
output.stopSpinner();
|
|
46
46
|
|
|
47
47
|
// Output in JSON mode
|
|
@@ -76,7 +76,7 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
|
|
|
76
76
|
if (response.organizations && response.organizations.length > 0) {
|
|
77
77
|
output.blank();
|
|
78
78
|
output.info('Organizations:');
|
|
79
|
-
for (
|
|
79
|
+
for (const org of response.organizations) {
|
|
80
80
|
let orgInfo = ` - ${org.name}`;
|
|
81
81
|
if (org.slug) {
|
|
82
82
|
orgInfo += ` (@${org.slug})`;
|
|
@@ -94,12 +94,12 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
|
|
|
94
94
|
// Show token expiry info
|
|
95
95
|
if (auth.expiresAt) {
|
|
96
96
|
output.blank();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
const expiresAt = new Date(auth.expiresAt);
|
|
98
|
+
const now = new Date();
|
|
99
|
+
const msUntilExpiry = expiresAt.getTime() - now.getTime();
|
|
100
|
+
const daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
|
|
101
|
+
const hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
|
|
102
|
+
const minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
|
|
103
103
|
if (msUntilExpiry <= 0) {
|
|
104
104
|
output.warn('Token has expired');
|
|
105
105
|
output.blank();
|
|
@@ -149,7 +149,7 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
|
|
|
149
149
|
* @param {Object} options - Command options
|
|
150
150
|
*/
|
|
151
151
|
export function validateWhoamiOptions() {
|
|
152
|
-
|
|
152
|
+
const errors = [];
|
|
153
153
|
|
|
154
154
|
// No specific validation needed for whoami command
|
|
155
155
|
|