@vizzly-testing/cli 0.34.0 → 0.35.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/README.md +67 -7
- package/dist/api/core.js +18 -4
- package/dist/api/endpoints.js +38 -15
- package/dist/api/index.js +1 -1
- package/dist/cli.js +270 -194
- package/dist/client/index.js +62 -21
- package/dist/commands/api.js +9 -3
- package/dist/commands/preview.js +30 -26
- package/dist/commands/project.js +1 -1
- package/dist/commands/run.js +25 -10
- package/dist/commands/status.js +14 -1
- package/dist/commands/tdd-daemon-process.js +5 -1
- package/dist/commands/tdd-daemon.js +217 -139
- package/dist/commands/tdd.js +26 -5
- package/dist/commands/upload.js +31 -13
- package/dist/reporter/reporter-bundle.iife.js +6 -6
- package/dist/sdk/index.js +23 -9
- package/dist/server/handlers/api-handler.js +3 -2
- package/dist/server/handlers/tdd-handler.js +5 -1
- package/dist/server/routers/screenshot.js +11 -4
- package/dist/services/build-manager.js +6 -1
- package/dist/tdd/tdd-service.js +6 -2
- package/dist/test-runner/core.js +8 -3
- package/dist/test-runner/operations.js +19 -3
- package/dist/types/client.d.ts +32 -5
- package/dist/types/index.d.ts +33 -5
- package/dist/uploader/core.js +41 -9
- package/dist/uploader/operations.js +19 -17
- package/dist/utils/config-loader.js +13 -6
- package/dist/utils/context.js +46 -76
- package/dist/utils/environment-config.js +20 -0
- package/dist/utils/output.js +8 -5
- package/dist/utils/screenshot-options.js +60 -10
- package/package.json +1 -1
- package/skills/vizzly/SKILL.md +12 -4
- package/skills/vizzly/references/cli-context.md +8 -2
- package/skills/vizzly/references/setup-ci.md +3 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import 'dotenv/config';
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync, statSync } from 'node:fs';
|
|
4
|
+
import { Option, program } from 'commander';
|
|
4
5
|
import { apiCommand, validateApiOptions } from './commands/api.js';
|
|
5
6
|
import { baselinesCommand, validateBaselinesOptions } from './commands/baselines.js';
|
|
6
7
|
import { buildsCommand, validateBuildsOptions } from './commands/builds.js';
|
|
@@ -52,7 +53,7 @@ const formatHelp = (cmd, helper) => {
|
|
|
52
53
|
lines.push(` ${c.gray('Visual regression testing from your terminal')}`);
|
|
53
54
|
} else {
|
|
54
55
|
// Compact header for subcommands
|
|
55
|
-
lines.push(` ${c.brand.amber(c.bold(
|
|
56
|
+
lines.push(` ${c.brand.amber(c.bold(commandPath(cmd)))}`);
|
|
56
57
|
let desc = cmd.description();
|
|
57
58
|
if (desc) {
|
|
58
59
|
lines.push(` ${c.gray(desc)}`);
|
|
@@ -84,7 +85,7 @@ const formatHelp = (cmd, helper) => {
|
|
|
84
85
|
key: 'setup',
|
|
85
86
|
icon: '▸',
|
|
86
87
|
title: 'Setup',
|
|
87
|
-
names: ['init', 'doctor', 'config', 'baselines']
|
|
88
|
+
names: ['init', 'doctor', 'config', 'baselines', 'project']
|
|
88
89
|
}, {
|
|
89
90
|
key: 'advanced',
|
|
90
91
|
icon: '▸',
|
|
@@ -174,7 +175,7 @@ const formatHelp = (cmd, helper) => {
|
|
|
174
175
|
lines.push(` ${c.brand.amber('▸')} ${c.bold('Quick Start')}`);
|
|
175
176
|
lines.push('');
|
|
176
177
|
lines.push(` ${c.dim('# Local visual review')}`);
|
|
177
|
-
lines.push(` ${c.gray('$')} ${c.white('vizzly tdd start')}`);
|
|
178
|
+
lines.push(` ${c.gray('$')} ${c.white('vizzly tdd start --open')}`);
|
|
178
179
|
lines.push('');
|
|
179
180
|
lines.push(` ${c.dim('# CI pipeline')}`);
|
|
180
181
|
lines.push(` ${c.gray('$')} ${c.white('vizzly run "pnpm test" --wait')}`);
|
|
@@ -205,8 +206,21 @@ const formatHelp = (cmd, helper) => {
|
|
|
205
206
|
lines.push('');
|
|
206
207
|
return lines.join('\n');
|
|
207
208
|
};
|
|
209
|
+
function commandPath(cmd) {
|
|
210
|
+
let names = [];
|
|
211
|
+
let current = cmd;
|
|
212
|
+
while (current) {
|
|
213
|
+
let name = current.name();
|
|
214
|
+
if (name) {
|
|
215
|
+
names.unshift(name);
|
|
216
|
+
}
|
|
217
|
+
current = current.parent;
|
|
218
|
+
}
|
|
219
|
+
return names.join(' ');
|
|
220
|
+
}
|
|
208
221
|
function extractGlobalOptionsFromArgv(argv, commandNames = null) {
|
|
209
222
|
let configPath = null;
|
|
223
|
+
let tokenArg = null;
|
|
210
224
|
let verboseMode = false;
|
|
211
225
|
let logLevelArg = null;
|
|
212
226
|
let jsonArg = null;
|
|
@@ -214,6 +228,11 @@ function extractGlobalOptionsFromArgv(argv, commandNames = null) {
|
|
|
214
228
|
if ((argv[i] === '-c' || argv[i] === '--config') && argv[i + 1]) {
|
|
215
229
|
configPath = argv[i + 1];
|
|
216
230
|
}
|
|
231
|
+
if (argv[i] === '--token' && argv[i + 1]) {
|
|
232
|
+
tokenArg = argv[i + 1];
|
|
233
|
+
} else if (argv[i].startsWith('--token=')) {
|
|
234
|
+
tokenArg = argv[i].substring('--token='.length);
|
|
235
|
+
}
|
|
217
236
|
if (argv[i] === '-v' || argv[i] === '--verbose') {
|
|
218
237
|
verboseMode = true;
|
|
219
238
|
}
|
|
@@ -234,6 +253,7 @@ function extractGlobalOptionsFromArgv(argv, commandNames = null) {
|
|
|
234
253
|
}
|
|
235
254
|
return {
|
|
236
255
|
configPath,
|
|
256
|
+
tokenArg,
|
|
237
257
|
verboseMode,
|
|
238
258
|
logLevelArg,
|
|
239
259
|
jsonArg
|
|
@@ -252,14 +272,139 @@ function normalizeJsonArgv(argv, commandNames) {
|
|
|
252
272
|
}
|
|
253
273
|
return normalizedArgv;
|
|
254
274
|
}
|
|
275
|
+
function findNestedCommandRequest(args, startIndex, subcommandNames, parentName) {
|
|
276
|
+
if (!subcommandNames?.size) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
let valueOptions = new Set(['-c', '--config', '--token', '--log-level']);
|
|
280
|
+
for (let i = startIndex; i < args.length; i++) {
|
|
281
|
+
let arg = args[i];
|
|
282
|
+
if (arg === '--') {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
if (valueOptions.has(arg)) {
|
|
286
|
+
i++;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (arg.startsWith('--')) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (arg.startsWith('-')) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
name: arg,
|
|
297
|
+
known: subcommandNames.has(arg),
|
|
298
|
+
helpCommand: `vizzly ${parentName} --help`
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
function findRequestedCommand(argv, commandNames, nestedCommandNames = new Map()) {
|
|
304
|
+
let valueOptions = new Set(['-c', '--config', '--token', '--log-level']);
|
|
305
|
+
let args = argv.slice(2);
|
|
306
|
+
let jsonValueCandidate = null;
|
|
307
|
+
for (let i = 0; i < args.length; i++) {
|
|
308
|
+
let arg = args[i];
|
|
309
|
+
if (arg === '--') {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
if (arg === '--json') {
|
|
313
|
+
let nextArg = args[i + 1];
|
|
314
|
+
if (nextArg && !nextArg.startsWith('-') && !commandNames.has(nextArg)) {
|
|
315
|
+
jsonValueCandidate = jsonValueCandidate || nextArg;
|
|
316
|
+
i++;
|
|
317
|
+
}
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (valueOptions.has(arg)) {
|
|
321
|
+
i++;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (arg.startsWith('--json=')) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (arg.startsWith('--')) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (arg.startsWith('-')) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
let known = commandNames.has(arg);
|
|
334
|
+
if (known) {
|
|
335
|
+
let nestedRequest = findNestedCommandRequest(args, i + 1, nestedCommandNames.get(arg), arg);
|
|
336
|
+
if (nestedRequest && !nestedRequest.known) {
|
|
337
|
+
return nestedRequest;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
name: arg,
|
|
342
|
+
known
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
if (jsonValueCandidate) {
|
|
346
|
+
return {
|
|
347
|
+
name: jsonValueCandidate,
|
|
348
|
+
known: false
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
function getGlobalOptions() {
|
|
354
|
+
let options = program.opts();
|
|
355
|
+
return {
|
|
356
|
+
...options,
|
|
357
|
+
noColor: options.noColor || options.color === false
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
function reportValidationErrors(errors) {
|
|
361
|
+
if (output.isJson()) {
|
|
362
|
+
output.error('Validation errors', null, {
|
|
363
|
+
errors
|
|
364
|
+
});
|
|
365
|
+
} else {
|
|
366
|
+
output.error('Validation errors:');
|
|
367
|
+
for (let error of errors) {
|
|
368
|
+
output.printErr(` - ${error}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
function formatCommanderError(message) {
|
|
374
|
+
return message.replace(/^error:\s*/, '').trim();
|
|
375
|
+
}
|
|
376
|
+
function writeCommanderError(text) {
|
|
377
|
+
let message = formatCommanderError(text);
|
|
378
|
+
if (!message) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (output.isJson()) {
|
|
382
|
+
if (message.startsWith('(')) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
output.error(message);
|
|
386
|
+
} else {
|
|
387
|
+
process.stderr.write(text);
|
|
388
|
+
if (!text.endsWith('\n')) {
|
|
389
|
+
process.stderr.write('\n');
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
255
393
|
program.name('vizzly').description('Vizzly CLI for visual regression testing').version(getPackageVersion()).option('-c, --config <path>', 'Config file path').option('--token <token>', 'Vizzly API token').option('-v, --verbose', 'Verbose output (shorthand for --log-level debug)').option('--log-level <level>', 'Log level: debug, info, warn, error (default: info, or VIZZLY_LOG_LEVEL env var)').option('--json [fields]', 'JSON output, optionally specify fields (e.g., --json id,status,branch)').option('--color', 'Force colored output (even in non-TTY)').option('--no-color', 'Disable colored output').option('--strict', 'Fail on any error (default: be resilient, warn on non-critical issues)').configureHelp({
|
|
256
394
|
formatHelp
|
|
257
395
|
});
|
|
396
|
+
program.configureOutput({
|
|
397
|
+
writeErr: writeCommanderError
|
|
398
|
+
});
|
|
399
|
+
program.showHelpAfterError('(Run vizzly --help for available commands)');
|
|
400
|
+
program.showSuggestionAfterError();
|
|
401
|
+
program.exitOverride();
|
|
258
402
|
|
|
259
403
|
// Load plugins before defining commands
|
|
260
404
|
// We need to manually parse to get the config option early
|
|
261
405
|
let {
|
|
262
406
|
configPath,
|
|
407
|
+
tokenArg,
|
|
263
408
|
verboseMode,
|
|
264
409
|
logLevelArg,
|
|
265
410
|
jsonArg
|
|
@@ -280,9 +425,11 @@ output.configure({
|
|
|
280
425
|
color: colorOverride,
|
|
281
426
|
json: jsonArg
|
|
282
427
|
});
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
428
|
+
let config = await loadConfig(configPath, {
|
|
429
|
+
token: tokenArg
|
|
430
|
+
});
|
|
431
|
+
let services = createServices(config);
|
|
432
|
+
let pluginServices = createPluginServices(services);
|
|
286
433
|
let plugins = [];
|
|
287
434
|
try {
|
|
288
435
|
plugins = await loadPlugins(configPath, config);
|
|
@@ -304,34 +451,41 @@ try {
|
|
|
304
451
|
output.debug(`Plugin loading failed: ${error.message}`);
|
|
305
452
|
}
|
|
306
453
|
program.command('init').description('Initialize Vizzly in your project').option('--force', 'Overwrite existing configuration').option('--agent-skill', 'Install the repo-local Vizzly agent skill').option('--agent-guidance', 'Add Vizzly guidance to this project AGENTS.md and install the agent skill').option('--skip-agent-skill', 'Skip the Vizzly agent skill prompt').action(async options => {
|
|
307
|
-
let globalOptions =
|
|
454
|
+
let globalOptions = getGlobalOptions();
|
|
308
455
|
await init({
|
|
309
456
|
...globalOptions,
|
|
310
457
|
...options,
|
|
311
458
|
plugins
|
|
312
459
|
});
|
|
313
460
|
});
|
|
314
|
-
program.command('upload').description('Upload screenshots to Vizzly').argument('<path>', 'Path to screenshots directory
|
|
315
|
-
|
|
461
|
+
program.command('upload').description('Upload screenshots to Vizzly').argument('<path>', 'Path to screenshots directory').option('-b, --build-name <name>', 'Build name for grouping').option('-m, --metadata <json>', 'Additional metadata as JSON').option('--batch-size <n>', 'Upload batch size', Number).option('--upload-timeout <ms>', 'Upload timeout in milliseconds', Number).option('--branch <branch>', 'Git branch').option('--commit <sha>', 'Git commit SHA').option('--message <msg>', 'Commit message').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', Number).option('--min-cluster-size <pixels>', 'Minimum changed-pixel cluster size', Number).option('--token <token>', 'API token override').option('--wait', 'Wait for build completion').option('--upload-all', 'Upload all screenshots without SHA deduplication').option('--parallel-id <id>', 'Unique identifier for parallel test execution').action(async (path, options) => {
|
|
462
|
+
let globalOptions = getGlobalOptions();
|
|
316
463
|
|
|
317
464
|
// Validate options
|
|
318
465
|
const validationErrors = validateUploadOptions(path, options);
|
|
319
466
|
if (validationErrors.length > 0) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
467
|
+
reportValidationErrors(validationErrors);
|
|
468
|
+
}
|
|
469
|
+
if (!existsSync(path)) {
|
|
470
|
+
output.error(`Path does not exist: ${path}`);
|
|
324
471
|
process.exit(1);
|
|
325
472
|
}
|
|
326
|
-
|
|
473
|
+
if (!statSync(path).isDirectory()) {
|
|
474
|
+
output.error(`Path is not a directory: ${path}`);
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
let result = await uploadCommand(path, options, globalOptions);
|
|
478
|
+
if (result?.exitCode) {
|
|
479
|
+
process.exit(result.exitCode);
|
|
480
|
+
}
|
|
327
481
|
});
|
|
328
482
|
|
|
329
483
|
// TDD command with subcommands - local visual review with an interactive dashboard
|
|
330
|
-
const tddCmd = program.command('tdd').description('Run tests in TDD mode with local visual review');
|
|
484
|
+
const tddCmd = program.command('tdd').description('Run tests in TDD mode with local visual review').showHelpAfterError('(Run vizzly tdd --help for available commands)').showSuggestionAfterError();
|
|
331
485
|
|
|
332
486
|
// TDD Start - Background server
|
|
333
|
-
tddCmd.command('start').description('Start background TDD server with dashboard').option('--port <port>', 'Port for TDD server'
|
|
334
|
-
|
|
487
|
+
tddCmd.command('start').description('Start background TDD server with dashboard').option('--port <port>', 'Port for TDD server').option('--open', 'Open dashboard in browser').option('--baseline-build <id>', 'Use specific build as baseline').option('--baseline-comparison <id>', 'Use specific comparison as baseline').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', Number).option('--min-cluster-size <pixels>', 'Minimum changed-pixel cluster size', Number).option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--fail-on-diff', 'Fail tests when visual differences are detected').option('--token <token>', 'API token override').addOption(new Option('--daemon-child', 'Internal: run as daemon child process').hideHelp()).action(async options => {
|
|
488
|
+
let globalOptions = getGlobalOptions();
|
|
335
489
|
|
|
336
490
|
// If this is a daemon child process, run the server directly
|
|
337
491
|
if (options.daemonChild) {
|
|
@@ -340,45 +494,45 @@ tddCmd.command('start').description('Start background TDD server with dashboard'
|
|
|
340
494
|
}
|
|
341
495
|
let validationErrors = validateTddStartOptions(options);
|
|
342
496
|
if (validationErrors.length > 0) {
|
|
343
|
-
|
|
344
|
-
for (let error of validationErrors) {
|
|
345
|
-
output.printErr(` - ${error}`);
|
|
346
|
-
}
|
|
347
|
-
process.exit(1);
|
|
497
|
+
reportValidationErrors(validationErrors);
|
|
348
498
|
}
|
|
349
499
|
await tddStartCommand(options, globalOptions);
|
|
350
500
|
});
|
|
351
501
|
|
|
352
502
|
// TDD Stop - Kill background server
|
|
353
|
-
tddCmd.command('stop').description('Stop background TDD server').action(async options => {
|
|
354
|
-
|
|
503
|
+
tddCmd.command('stop').description('Stop background TDD server').option('--port <port>', 'Port for TDD server').action(async options => {
|
|
504
|
+
let globalOptions = getGlobalOptions();
|
|
505
|
+
let validationErrors = validateTddStartOptions(options);
|
|
506
|
+
if (validationErrors.length > 0) {
|
|
507
|
+
reportValidationErrors(validationErrors);
|
|
508
|
+
}
|
|
355
509
|
await tddStopCommand(options, globalOptions);
|
|
356
510
|
});
|
|
357
511
|
|
|
358
512
|
// TDD Status - Check server status
|
|
359
|
-
tddCmd.command('status').description('Check TDD server status').action(async options => {
|
|
360
|
-
|
|
513
|
+
tddCmd.command('status').description('Check TDD server status').option('--port <port>', 'Port for TDD server').action(async options => {
|
|
514
|
+
let globalOptions = getGlobalOptions();
|
|
515
|
+
let validationErrors = validateTddStartOptions(options);
|
|
516
|
+
if (validationErrors.length > 0) {
|
|
517
|
+
reportValidationErrors(validationErrors);
|
|
518
|
+
}
|
|
361
519
|
await tddStatusCommand(options, globalOptions);
|
|
362
520
|
});
|
|
363
521
|
|
|
364
522
|
// TDD List - List all running servers (for menubar app integration)
|
|
365
523
|
tddCmd.command('list').description('List all running TDD servers').action(async options => {
|
|
366
|
-
|
|
524
|
+
let globalOptions = getGlobalOptions();
|
|
367
525
|
await tddListCommand(options, globalOptions);
|
|
368
526
|
});
|
|
369
527
|
|
|
370
528
|
// TDD Run - One-off test run with ephemeral server (generates static report)
|
|
371
|
-
tddCmd.command('run <command>').description('Run tests once in TDD mode with local visual review').option('--port <port>', 'Port for TDD server'
|
|
372
|
-
|
|
529
|
+
tddCmd.command('run <command>').description('Run tests once in TDD mode with local visual review').option('--port <port>', 'Port for TDD server').option('--branch <branch>', 'Git branch override').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', Number).option('--min-cluster-size <pixels>', 'Minimum changed-pixel cluster size', Number).option('--token <token>', 'API token override').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--baseline-build <id>', 'Use specific build as baseline').option('--baseline-comparison <id>', 'Use specific comparison as baseline').option('--set-baseline', 'Accept current screenshots as new baseline (overwrites existing)').option('--fail-on-diff', 'Fail tests when visual differences are detected').option('--no-open', 'Skip opening report in browser').action(async (command, options) => {
|
|
530
|
+
let globalOptions = getGlobalOptions();
|
|
373
531
|
|
|
374
532
|
// Validate options
|
|
375
533
|
const validationErrors = validateTddOptions(command, options);
|
|
376
534
|
if (validationErrors.length > 0) {
|
|
377
|
-
|
|
378
|
-
for (let error of validationErrors) {
|
|
379
|
-
output.printErr(` - ${error}`);
|
|
380
|
-
}
|
|
381
|
-
process.exit(1);
|
|
535
|
+
reportValidationErrors(validationErrors);
|
|
382
536
|
}
|
|
383
537
|
const {
|
|
384
538
|
result,
|
|
@@ -403,8 +557,8 @@ tddCmd.command('run <command>').description('Run tests once in TDD mode with loc
|
|
|
403
557
|
process.once('SIGINT', sigintHandler);
|
|
404
558
|
process.once('SIGTERM', sigtermHandler);
|
|
405
559
|
|
|
406
|
-
// If
|
|
407
|
-
|
|
560
|
+
// If the run captured screenshots, generate a static report for review.
|
|
561
|
+
let hasComparisons = result?.screenshotsCaptured > 0 || result?.comparisons?.length > 0 || result?.summary?.total > 0;
|
|
408
562
|
if (hasComparisons) {
|
|
409
563
|
// Note: Tests have completed at this point, so report-data.json is stable.
|
|
410
564
|
// The report reflects the final state of all comparisons.
|
|
@@ -430,16 +584,12 @@ tddCmd.command('run <command>').description('Run tests once in TDD mode with loc
|
|
|
430
584
|
process.exit(result?.exitCode || 0);
|
|
431
585
|
});
|
|
432
586
|
program.command('run').description('Run tests with Vizzly integration').argument('<command>', 'Test command to run').option('--port <port>', 'Port for screenshot server', '47392').option('-b, --build-name <name>', 'Custom build name').option('--branch <branch>', 'Git branch override').option('--commit <sha>', 'Git commit SHA').option('--message <msg>', 'Commit message').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', Number).option('--min-cluster-size <pixels>', 'Minimum changed-pixel cluster size', Number).option('--batch-size <n>', 'Upload batch size', Number).option('--upload-timeout <ms>', 'Upload timeout in milliseconds', Number).option('--token <token>', 'API token override').option('--wait', 'Wait for build completion').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--allow-no-token', 'Allow running without API token').option('--upload-all', 'Upload all screenshots without SHA deduplication').option('--parallel-id <id>', 'Unique identifier for parallel test execution').action(async (command, options) => {
|
|
433
|
-
|
|
587
|
+
let globalOptions = getGlobalOptions();
|
|
434
588
|
|
|
435
589
|
// Validate options
|
|
436
590
|
const validationErrors = validateRunOptions(command, options);
|
|
437
591
|
if (validationErrors.length > 0) {
|
|
438
|
-
|
|
439
|
-
for (let error of validationErrors) {
|
|
440
|
-
output.printErr(` - ${error}`);
|
|
441
|
-
}
|
|
442
|
-
process.exit(1);
|
|
592
|
+
reportValidationErrors(validationErrors);
|
|
443
593
|
}
|
|
444
594
|
try {
|
|
445
595
|
const result = await runCommand(command, options, globalOptions);
|
|
@@ -452,16 +602,12 @@ program.command('run').description('Run tests with Vizzly integration').argument
|
|
|
452
602
|
}
|
|
453
603
|
});
|
|
454
604
|
program.command('status').description('Check the status of a build').argument('<build-id>', 'Build ID to check status for').action(async (buildId, options) => {
|
|
455
|
-
|
|
605
|
+
let globalOptions = getGlobalOptions();
|
|
456
606
|
|
|
457
607
|
// Validate options
|
|
458
608
|
const validationErrors = validateStatusOptions(buildId, options);
|
|
459
609
|
if (validationErrors.length > 0) {
|
|
460
|
-
|
|
461
|
-
for (let error of validationErrors) {
|
|
462
|
-
output.printErr(` - ${error}`);
|
|
463
|
-
}
|
|
464
|
-
process.exit(1);
|
|
610
|
+
reportValidationErrors(validationErrors);
|
|
465
611
|
}
|
|
466
612
|
await statusCommand(buildId, options, globalOptions);
|
|
467
613
|
});
|
|
@@ -476,16 +622,12 @@ Examples:
|
|
|
476
622
|
$ vizzly builds -b abc123 --comparisons # Include comparisons
|
|
477
623
|
$ vizzly builds --json # Output as JSON for scripting
|
|
478
624
|
`).action(async options => {
|
|
479
|
-
|
|
625
|
+
let globalOptions = getGlobalOptions();
|
|
480
626
|
|
|
481
627
|
// Validate options
|
|
482
628
|
const validationErrors = validateBuildsOptions(options);
|
|
483
629
|
if (validationErrors.length > 0) {
|
|
484
|
-
|
|
485
|
-
for (let error of validationErrors) {
|
|
486
|
-
output.printErr(` - ${error}`);
|
|
487
|
-
}
|
|
488
|
-
process.exit(1);
|
|
630
|
+
reportValidationErrors(validationErrors);
|
|
489
631
|
}
|
|
490
632
|
await buildsCommand(options, globalOptions);
|
|
491
633
|
});
|
|
@@ -499,20 +641,16 @@ Examples:
|
|
|
499
641
|
$ vizzly comparisons --status changed # Only changed comparisons
|
|
500
642
|
$ vizzly comparisons --json # Output as JSON for scripting
|
|
501
643
|
`).action(async options => {
|
|
502
|
-
|
|
644
|
+
let globalOptions = getGlobalOptions();
|
|
503
645
|
|
|
504
646
|
// Validate options
|
|
505
647
|
const validationErrors = validateComparisonsOptions(options);
|
|
506
648
|
if (validationErrors.length > 0) {
|
|
507
|
-
|
|
508
|
-
for (let error of validationErrors) {
|
|
509
|
-
output.printErr(` - ${error}`);
|
|
510
|
-
}
|
|
511
|
-
process.exit(1);
|
|
649
|
+
reportValidationErrors(validationErrors);
|
|
512
650
|
}
|
|
513
651
|
await comparisonsCommand(options, globalOptions);
|
|
514
652
|
});
|
|
515
|
-
let contextCmd = program.command('context').description('Fetch build and diff context');
|
|
653
|
+
let contextCmd = program.command('context').description('Fetch build and diff context').showHelpAfterError('(Run vizzly context --help for available commands)').showSuggestionAfterError();
|
|
516
654
|
contextCmd.command('build').description('Fetch build context').argument('<build-id>', 'Build ID to fetch context for').option('--source <source>', 'Context source: auto, cloud, or local', 'auto').option('--agent', 'Output compact context for LLM agents').option('--full', 'Return the full build context payload with --agent --json').option('--include <items>', 'Add detail to compact agent JSON: screenshots,diffs,comments').addHelpText('after', `
|
|
517
655
|
Examples:
|
|
518
656
|
$ vizzly context build abc123
|
|
@@ -522,14 +660,10 @@ Examples:
|
|
|
522
660
|
$ vizzly context build abc123 --agent --json --include diffs,comments
|
|
523
661
|
$ vizzly context build abc123 --agent --json --full
|
|
524
662
|
`).action(async (buildId, options) => {
|
|
525
|
-
|
|
663
|
+
let globalOptions = getGlobalOptions();
|
|
526
664
|
const validationErrors = validateContextBuildOptions(options);
|
|
527
665
|
if (validationErrors.length > 0) {
|
|
528
|
-
|
|
529
|
-
for (let error of validationErrors) {
|
|
530
|
-
output.printErr(` - ${error}`);
|
|
531
|
-
}
|
|
532
|
-
process.exit(1);
|
|
666
|
+
reportValidationErrors(validationErrors);
|
|
533
667
|
}
|
|
534
668
|
await contextBuildCommand(buildId, options, globalOptions);
|
|
535
669
|
});
|
|
@@ -540,14 +674,10 @@ Examples:
|
|
|
540
674
|
$ vizzly context comparison def456 --similar-limit 5 --recent-limit 5
|
|
541
675
|
$ vizzly context comparison def456 --json
|
|
542
676
|
`).action(async (comparisonId, options) => {
|
|
543
|
-
|
|
677
|
+
let globalOptions = getGlobalOptions();
|
|
544
678
|
const validationErrors = validateContextComparisonOptions(options);
|
|
545
679
|
if (validationErrors.length > 0) {
|
|
546
|
-
|
|
547
|
-
for (let error of validationErrors) {
|
|
548
|
-
output.printErr(` - ${error}`);
|
|
549
|
-
}
|
|
550
|
-
process.exit(1);
|
|
680
|
+
reportValidationErrors(validationErrors);
|
|
551
681
|
}
|
|
552
682
|
await contextComparisonCommand(comparisonId, options, globalOptions);
|
|
553
683
|
});
|
|
@@ -558,14 +688,10 @@ Examples:
|
|
|
558
688
|
$ vizzly context screenshot Dashboard --project storybook --org acme
|
|
559
689
|
$ vizzly context screenshot Dashboard --json
|
|
560
690
|
`).action(async (name, options) => {
|
|
561
|
-
|
|
691
|
+
let globalOptions = getGlobalOptions();
|
|
562
692
|
const validationErrors = validateContextScreenshotOptions(options);
|
|
563
693
|
if (validationErrors.length > 0) {
|
|
564
|
-
|
|
565
|
-
for (let error of validationErrors) {
|
|
566
|
-
output.printErr(` - ${error}`);
|
|
567
|
-
}
|
|
568
|
-
process.exit(1);
|
|
694
|
+
reportValidationErrors(validationErrors);
|
|
569
695
|
}
|
|
570
696
|
await contextScreenshotCommand(name, options, globalOptions);
|
|
571
697
|
});
|
|
@@ -575,14 +701,10 @@ Examples:
|
|
|
575
701
|
$ vizzly context similar fp-dashboard --project storybook --org acme
|
|
576
702
|
$ vizzly context similar fp-dashboard --json
|
|
577
703
|
`).action(async (fingerprintHash, options) => {
|
|
578
|
-
|
|
704
|
+
let globalOptions = getGlobalOptions();
|
|
579
705
|
const validationErrors = validateContextSimilarOptions(options);
|
|
580
706
|
if (validationErrors.length > 0) {
|
|
581
|
-
|
|
582
|
-
for (let error of validationErrors) {
|
|
583
|
-
output.printErr(` - ${error}`);
|
|
584
|
-
}
|
|
585
|
-
process.exit(1);
|
|
707
|
+
reportValidationErrors(validationErrors);
|
|
586
708
|
}
|
|
587
709
|
await contextSimilarCommand(fingerprintHash, options, globalOptions);
|
|
588
710
|
});
|
|
@@ -593,28 +715,20 @@ Examples:
|
|
|
593
715
|
$ vizzly context review-queue --limit 10
|
|
594
716
|
$ vizzly context review-queue --json
|
|
595
717
|
`).action(async options => {
|
|
596
|
-
|
|
718
|
+
let globalOptions = getGlobalOptions();
|
|
597
719
|
const validationErrors = validateContextReviewQueueOptions(options);
|
|
598
720
|
if (validationErrors.length > 0) {
|
|
599
|
-
|
|
600
|
-
for (let error of validationErrors) {
|
|
601
|
-
output.printErr(` - ${error}`);
|
|
602
|
-
}
|
|
603
|
-
process.exit(1);
|
|
721
|
+
reportValidationErrors(validationErrors);
|
|
604
722
|
}
|
|
605
723
|
await contextReviewQueueCommand(options, globalOptions);
|
|
606
724
|
});
|
|
607
725
|
program.command('config').description('Display current configuration').argument('[key]', 'Specific config key to get (dot notation, e.g., comparison.threshold)').action(async (key, options) => {
|
|
608
|
-
|
|
726
|
+
let globalOptions = getGlobalOptions();
|
|
609
727
|
|
|
610
728
|
// Validate options
|
|
611
729
|
const validationErrors = validateConfigOptions(options);
|
|
612
730
|
if (validationErrors.length > 0) {
|
|
613
|
-
|
|
614
|
-
for (let error of validationErrors) {
|
|
615
|
-
output.printErr(` - ${error}`);
|
|
616
|
-
}
|
|
617
|
-
process.exit(1);
|
|
731
|
+
reportValidationErrors(validationErrors);
|
|
618
732
|
}
|
|
619
733
|
await configCommand(key, options, globalOptions);
|
|
620
734
|
});
|
|
@@ -627,16 +741,12 @@ Examples:
|
|
|
627
741
|
|
|
628
742
|
Note: Baselines are stored locally in .vizzly/baselines/ during TDD mode.
|
|
629
743
|
`).action(async options => {
|
|
630
|
-
|
|
744
|
+
let globalOptions = getGlobalOptions();
|
|
631
745
|
|
|
632
746
|
// Validate options
|
|
633
747
|
const validationErrors = validateBaselinesOptions(options);
|
|
634
748
|
if (validationErrors.length > 0) {
|
|
635
|
-
|
|
636
|
-
for (let error of validationErrors) {
|
|
637
|
-
output.printErr(` - ${error}`);
|
|
638
|
-
}
|
|
639
|
-
process.exit(1);
|
|
749
|
+
reportValidationErrors(validationErrors);
|
|
640
750
|
}
|
|
641
751
|
await baselinesCommand(options, globalOptions);
|
|
642
752
|
});
|
|
@@ -651,20 +761,16 @@ Examples:
|
|
|
651
761
|
Note: POST is restricted to approve, reject, and comment endpoints.
|
|
652
762
|
Most operations have dedicated commands (builds, comparisons, approve, etc.).
|
|
653
763
|
`).action(async (endpoint, options) => {
|
|
654
|
-
|
|
764
|
+
let globalOptions = getGlobalOptions();
|
|
655
765
|
|
|
656
766
|
// Validate options
|
|
657
767
|
const validationErrors = validateApiOptions(endpoint, options);
|
|
658
768
|
if (validationErrors.length > 0) {
|
|
659
|
-
|
|
660
|
-
for (let error of validationErrors) {
|
|
661
|
-
output.printErr(` - ${error}`);
|
|
662
|
-
}
|
|
663
|
-
process.exit(1);
|
|
769
|
+
reportValidationErrors(validationErrors);
|
|
664
770
|
}
|
|
665
771
|
await apiCommand(endpoint, options, globalOptions);
|
|
666
772
|
});
|
|
667
|
-
program.command('approve').description('Approve a comparison').argument('<comparison-id>', 'Comparison ID to approve
|
|
773
|
+
program.command('approve').description('Approve a comparison').argument('<comparison-id>', 'Comparison ID to approve').option('-m, --comment <message>', 'Optional comment explaining the approval').addHelpText('after', `
|
|
668
774
|
Examples:
|
|
669
775
|
$ vizzly approve abc123-def456-7890 # Approve a comparison
|
|
670
776
|
$ vizzly approve abc123 -m "LGTM" # Approve with comment
|
|
@@ -675,18 +781,14 @@ Workflow:
|
|
|
675
781
|
2. Review the changes in the web UI or via URLs in the output
|
|
676
782
|
3. Approve: vizzly approve <comparison-id>
|
|
677
783
|
`).action(async (comparisonId, options) => {
|
|
678
|
-
|
|
784
|
+
let globalOptions = getGlobalOptions();
|
|
679
785
|
const validationErrors = validateApproveOptions(comparisonId, options);
|
|
680
786
|
if (validationErrors.length > 0) {
|
|
681
|
-
|
|
682
|
-
for (let error of validationErrors) {
|
|
683
|
-
output.printErr(` - ${error}`);
|
|
684
|
-
}
|
|
685
|
-
process.exit(1);
|
|
787
|
+
reportValidationErrors(validationErrors);
|
|
686
788
|
}
|
|
687
789
|
await approveCommand(comparisonId, options, globalOptions);
|
|
688
790
|
});
|
|
689
|
-
program.command('reject').description('Reject a comparison').argument('<comparison-id>', 'Comparison ID to reject
|
|
791
|
+
program.command('reject').description('Reject a comparison').argument('<comparison-id>', 'Comparison ID to reject').option('-r, --reason <message>', 'Required reason for rejection').addHelpText('after', `
|
|
690
792
|
Examples:
|
|
691
793
|
$ vizzly reject abc123 -r "Button color is wrong"
|
|
692
794
|
$ vizzly reject abc123 --reason "Needs design review"
|
|
@@ -697,18 +799,14 @@ Workflow:
|
|
|
697
799
|
2. Review the changes in the web UI or via URLs in the output
|
|
698
800
|
3. Reject with reason: vizzly reject <comparison-id> -r "reason"
|
|
699
801
|
`).action(async (comparisonId, options) => {
|
|
700
|
-
|
|
802
|
+
let globalOptions = getGlobalOptions();
|
|
701
803
|
const validationErrors = validateRejectOptions(comparisonId, options);
|
|
702
804
|
if (validationErrors.length > 0) {
|
|
703
|
-
|
|
704
|
-
for (let error of validationErrors) {
|
|
705
|
-
output.printErr(` - ${error}`);
|
|
706
|
-
}
|
|
707
|
-
process.exit(1);
|
|
805
|
+
reportValidationErrors(validationErrors);
|
|
708
806
|
}
|
|
709
807
|
await rejectCommand(comparisonId, options, globalOptions);
|
|
710
808
|
});
|
|
711
|
-
program.command('comment').description('Add a comment to a build').argument('<build-id>', 'Build ID to comment on
|
|
809
|
+
program.command('comment').description('Add a comment to a build').argument('<build-id>', 'Build ID to comment on').argument('<message>', 'Comment message').option('-t, --type <type>', 'Comment type: general, approval, rejection', 'general').addHelpText('after', `
|
|
712
810
|
Examples:
|
|
713
811
|
$ vizzly comment abc123 "Looks good overall"
|
|
714
812
|
$ vizzly comment abc123 "Approved" -t approval
|
|
@@ -719,14 +817,10 @@ Workflow:
|
|
|
719
817
|
1. Get build ID: vizzly builds --branch main
|
|
720
818
|
2. Add comment: vizzly comment <build-id> "Your message"
|
|
721
819
|
`).action(async (buildId, message, options) => {
|
|
722
|
-
|
|
820
|
+
let globalOptions = getGlobalOptions();
|
|
723
821
|
const validationErrors = validateCommentOptions(buildId, message, options);
|
|
724
822
|
if (validationErrors.length > 0) {
|
|
725
|
-
|
|
726
|
-
for (let error of validationErrors) {
|
|
727
|
-
output.printErr(` - ${error}`);
|
|
728
|
-
}
|
|
729
|
-
process.exit(1);
|
|
823
|
+
reportValidationErrors(validationErrors);
|
|
730
824
|
}
|
|
731
825
|
await commentCommand(buildId, message, options, globalOptions);
|
|
732
826
|
});
|
|
@@ -738,14 +832,10 @@ Examples:
|
|
|
738
832
|
Note: Shows organizations from your user account (via vizzly login)
|
|
739
833
|
or the single organization for a project token.
|
|
740
834
|
`).action(async options => {
|
|
741
|
-
|
|
835
|
+
let globalOptions = getGlobalOptions();
|
|
742
836
|
const validationErrors = validateOrgsOptions(options);
|
|
743
837
|
if (validationErrors.length > 0) {
|
|
744
|
-
|
|
745
|
-
for (let error of validationErrors) {
|
|
746
|
-
output.printErr(` - ${error}`);
|
|
747
|
-
}
|
|
748
|
-
process.exit(1);
|
|
838
|
+
reportValidationErrors(validationErrors);
|
|
749
839
|
}
|
|
750
840
|
await orgsCommand(options, globalOptions);
|
|
751
841
|
});
|
|
@@ -760,18 +850,14 @@ Workflow:
|
|
|
760
850
|
2. List projects: vizzly projects --org <org-slug>
|
|
761
851
|
3. Query builds: vizzly builds
|
|
762
852
|
`).action(async options => {
|
|
763
|
-
|
|
853
|
+
let globalOptions = getGlobalOptions();
|
|
764
854
|
const validationErrors = validateProjectsOptions(options);
|
|
765
855
|
if (validationErrors.length > 0) {
|
|
766
|
-
|
|
767
|
-
for (let error of validationErrors) {
|
|
768
|
-
output.printErr(` - ${error}`);
|
|
769
|
-
}
|
|
770
|
-
process.exit(1);
|
|
856
|
+
reportValidationErrors(validationErrors);
|
|
771
857
|
}
|
|
772
858
|
await projectsCommand(options, globalOptions);
|
|
773
859
|
});
|
|
774
|
-
let projectCommand = program.command('project').description('Manage the project linked to this local checkout');
|
|
860
|
+
let projectCommand = program.command('project').description('Manage the project linked to this local checkout').showHelpAfterError('(Run vizzly project --help for available commands)').showSuggestionAfterError();
|
|
775
861
|
projectCommand.command('link [selector]').description('Link a Vizzly project for cloud uploads').option('--org <slug>', 'Organization slug').option('--project <slug>', 'Project slug').option('--name <name>', 'Credential name shown in Vizzly').option('--expires-at <iso>', 'Optional token expiration timestamp').addHelpText('after', `
|
|
776
862
|
Examples:
|
|
777
863
|
$ vizzly project link vizzly/storybook
|
|
@@ -780,111 +866,85 @@ Examples:
|
|
|
780
866
|
Note: run "vizzly login" first. The linked credential is project-scoped and is
|
|
781
867
|
used for cloud uploads; your user login remains separate for review actions.
|
|
782
868
|
`).action(async (selector, options) => {
|
|
783
|
-
|
|
869
|
+
let globalOptions = getGlobalOptions();
|
|
784
870
|
const validationErrors = validateProjectLinkOptions(selector, options);
|
|
785
871
|
if (validationErrors.length > 0) {
|
|
786
|
-
|
|
787
|
-
for (let error of validationErrors) {
|
|
788
|
-
output.printErr(` - ${error}`);
|
|
789
|
-
}
|
|
790
|
-
process.exit(1);
|
|
872
|
+
reportValidationErrors(validationErrors);
|
|
791
873
|
}
|
|
792
874
|
await projectLinkCommand(selector, options, globalOptions);
|
|
793
875
|
});
|
|
794
876
|
program.command('finalize').description('Finalize a parallel build after all shards complete').argument('<parallel-id>', 'Parallel ID to finalize').action(async (parallelId, options) => {
|
|
795
|
-
|
|
877
|
+
let globalOptions = getGlobalOptions();
|
|
796
878
|
|
|
797
879
|
// Validate options
|
|
798
880
|
const validationErrors = validateFinalizeOptions(parallelId, options);
|
|
799
881
|
if (validationErrors.length > 0) {
|
|
800
|
-
|
|
801
|
-
for (let error of validationErrors) {
|
|
802
|
-
output.printErr(` - ${error}`);
|
|
803
|
-
}
|
|
804
|
-
process.exit(1);
|
|
882
|
+
reportValidationErrors(validationErrors);
|
|
805
883
|
}
|
|
806
884
|
await finalizeCommand(parallelId, options, globalOptions);
|
|
807
885
|
});
|
|
808
886
|
program.command('preview').description('Upload static files as a preview for a build').argument('[path]', 'Path to static files (dist/, build/, out/)').option('-b, --build <id>', 'Build ID to attach preview to').option('--base <path>', 'Override auto-detected base path').option('--open', 'Open preview URL in browser after upload').option('--dry-run', 'Show what would be uploaded without uploading').option('--public-link', 'Acknowledge that preview URL grants access to anyone with the link (required for private projects)').option('-x, --exclude <pattern>', 'Exclude files/dirs (repeatable, e.g. -x "*.log" -x "temp/")', (val, prev) => prev ? [...prev, val] : [val]).option('-i, --include <pattern>', 'Override default exclusions (repeatable, e.g. -i package.json -i tests/)', (val, prev) => prev ? [...prev, val] : [val]).action(async (path, options) => {
|
|
809
|
-
|
|
887
|
+
let globalOptions = getGlobalOptions();
|
|
810
888
|
|
|
811
889
|
// Show helpful error if path is missing
|
|
812
890
|
if (!path) {
|
|
813
891
|
output.error('Path to static files is required');
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
892
|
+
if (!globalOptions.json) {
|
|
893
|
+
output.blank();
|
|
894
|
+
output.print(' Upload your build output directory:');
|
|
895
|
+
output.blank();
|
|
896
|
+
output.print(' vizzly preview ./dist');
|
|
897
|
+
output.print(' vizzly preview ./build');
|
|
898
|
+
output.print(' vizzly preview ./out');
|
|
899
|
+
output.blank();
|
|
900
|
+
}
|
|
821
901
|
process.exit(1);
|
|
822
902
|
}
|
|
823
903
|
|
|
824
904
|
// Validate options
|
|
825
905
|
const validationErrors = validatePreviewOptions(path, options);
|
|
826
906
|
if (validationErrors.length > 0) {
|
|
827
|
-
|
|
828
|
-
for (let error of validationErrors) {
|
|
829
|
-
output.printErr(` - ${error}`);
|
|
830
|
-
}
|
|
831
|
-
process.exit(1);
|
|
907
|
+
reportValidationErrors(validationErrors);
|
|
832
908
|
}
|
|
833
909
|
await previewCommand(path, options, globalOptions);
|
|
834
910
|
});
|
|
835
911
|
program.command('doctor').description('Run diagnostics to check your environment and configuration').option('--api', 'Include API connectivity checks').action(async options => {
|
|
836
|
-
|
|
912
|
+
let globalOptions = getGlobalOptions();
|
|
837
913
|
|
|
838
914
|
// Validate options
|
|
839
915
|
const validationErrors = validateDoctorOptions(options);
|
|
840
916
|
if (validationErrors.length > 0) {
|
|
841
|
-
|
|
842
|
-
for (let error of validationErrors) {
|
|
843
|
-
output.printErr(` - ${error}`);
|
|
844
|
-
}
|
|
845
|
-
process.exit(1);
|
|
917
|
+
reportValidationErrors(validationErrors);
|
|
846
918
|
}
|
|
847
919
|
await doctorCommand(options, globalOptions);
|
|
848
920
|
});
|
|
849
921
|
program.command('login').description('Authenticate with your Vizzly account').option('--api-url <url>', 'API URL override').action(async options => {
|
|
850
|
-
|
|
922
|
+
let globalOptions = getGlobalOptions();
|
|
851
923
|
|
|
852
924
|
// Validate options
|
|
853
925
|
const validationErrors = validateLoginOptions(options);
|
|
854
926
|
if (validationErrors.length > 0) {
|
|
855
|
-
|
|
856
|
-
for (let error of validationErrors) {
|
|
857
|
-
output.printErr(` - ${error}`);
|
|
858
|
-
}
|
|
859
|
-
process.exit(1);
|
|
927
|
+
reportValidationErrors(validationErrors);
|
|
860
928
|
}
|
|
861
929
|
await loginCommand(options, globalOptions);
|
|
862
930
|
});
|
|
863
931
|
program.command('logout').description('Clear stored authentication tokens').option('--api-url <url>', 'API URL override').action(async options => {
|
|
864
|
-
|
|
932
|
+
let globalOptions = getGlobalOptions();
|
|
865
933
|
|
|
866
934
|
// Validate options
|
|
867
935
|
const validationErrors = validateLogoutOptions(options);
|
|
868
936
|
if (validationErrors.length > 0) {
|
|
869
|
-
|
|
870
|
-
for (let error of validationErrors) {
|
|
871
|
-
output.printErr(` - ${error}`);
|
|
872
|
-
}
|
|
873
|
-
process.exit(1);
|
|
937
|
+
reportValidationErrors(validationErrors);
|
|
874
938
|
}
|
|
875
939
|
await logoutCommand(options, globalOptions);
|
|
876
940
|
});
|
|
877
941
|
program.command('whoami').description('Show current authentication status and user information').option('--api-url <url>', 'API URL override').action(async options => {
|
|
878
|
-
|
|
942
|
+
let globalOptions = getGlobalOptions();
|
|
879
943
|
|
|
880
944
|
// Validate options
|
|
881
945
|
const validationErrors = validateWhoamiOptions(options);
|
|
882
946
|
if (validationErrors.length > 0) {
|
|
883
|
-
|
|
884
|
-
for (let error of validationErrors) {
|
|
885
|
-
output.printErr(` - ${error}`);
|
|
886
|
-
}
|
|
887
|
-
process.exit(1);
|
|
947
|
+
reportValidationErrors(validationErrors);
|
|
888
948
|
}
|
|
889
949
|
await whoamiCommand(options, globalOptions);
|
|
890
950
|
});
|
|
@@ -893,6 +953,7 @@ program.command('whoami').description('Show current authentication status and us
|
|
|
893
953
|
// This auto-configures the menubar app so it can find package runners/node
|
|
894
954
|
saveUserPath().catch(() => {});
|
|
895
955
|
let commandNames = new Set(program.commands.map(command => command.name()));
|
|
956
|
+
let nestedCommandNames = new Map(program.commands.map(command => [command.name(), new Set(command.commands.map(subcommand => subcommand.name()))]));
|
|
896
957
|
let normalizedArgv = normalizeJsonArgv(process.argv, commandNames);
|
|
897
958
|
let normalizedGlobals = extractGlobalOptionsFromArgv(normalizedArgv, commandNames);
|
|
898
959
|
output.configure({
|
|
@@ -902,4 +963,19 @@ output.configure({
|
|
|
902
963
|
json: normalizedGlobals.jsonArg,
|
|
903
964
|
resetTimer: false
|
|
904
965
|
});
|
|
905
|
-
|
|
966
|
+
let requestedCommand = findRequestedCommand(normalizedArgv, commandNames, nestedCommandNames);
|
|
967
|
+
if (requestedCommand && !requestedCommand.known) {
|
|
968
|
+
output.error(`unknown command '${requestedCommand.name}'`);
|
|
969
|
+
if (!output.isJson() && requestedCommand.helpCommand) {
|
|
970
|
+
output.printErr(`(Run ${requestedCommand.helpCommand} for available commands)`);
|
|
971
|
+
}
|
|
972
|
+
process.exit(1);
|
|
973
|
+
}
|
|
974
|
+
try {
|
|
975
|
+
program.parse(normalizedArgv);
|
|
976
|
+
} catch (error) {
|
|
977
|
+
if (error.exitCode == null) {
|
|
978
|
+
output.error('Unexpected CLI error', error);
|
|
979
|
+
}
|
|
980
|
+
process.exit(error.exitCode ?? 1);
|
|
981
|
+
}
|