@vizzly-testing/cli 0.38.1 → 0.39.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +222 -195
  2. package/dist/cli.js +55 -54
  3. package/dist/commands/init.js +154 -483
  4. package/dist/commands/project.js +128 -44
  5. package/dist/commands/run.js +2 -2
  6. package/dist/commands/tdd.js +89 -38
  7. package/dist/commands/upload.js +2 -2
  8. package/dist/init/agents.js +140 -0
  9. package/dist/init/core.js +31 -0
  10. package/dist/init/prompts.js +44 -0
  11. package/dist/reporter/reporter-bundle.css +1 -1
  12. package/dist/reporter/reporter-bundle.iife.js +30 -61
  13. package/dist/reporter-ssr/ssr-entry.js +3 -6
  14. package/dist/server/handlers/tdd-handler.js +33 -2
  15. package/dist/services/index.js +8 -2
  16. package/dist/tdd/services/comparison-service.js +4 -1
  17. package/dist/tdd/tdd-service.js +14 -2
  18. package/dist/test-runner/core.js +28 -3
  19. package/dist/test-runner/operations.js +41 -20
  20. package/dist/types/index.d.ts +8 -0
  21. package/dist/uploader/core.js +1 -0
  22. package/dist/utils/cli-update.js +105 -0
  23. package/dist/utils/config-discovery.js +73 -0
  24. package/dist/utils/config-loader.js +29 -8
  25. package/dist/utils/config-schema.js +5 -0
  26. package/dist/utils/file-changes.js +105 -0
  27. package/dist/utils/project-binding.js +96 -0
  28. package/dist/utils/project-link-store.js +13 -7
  29. package/dist/utils/update-check.js +2 -0
  30. package/dist/utils/update-notifications.js +175 -0
  31. package/package.json +16 -16
  32. package/skills/vizzly/SKILL.md +4 -3
  33. package/skills/vizzly/references/ci.md +33 -0
  34. package/skills/vizzly/references/cli-context.md +81 -71
  35. package/skills/vizzly/references/dynamic-content.md +16 -17
  36. package/skills/vizzly/references/sdks.md +22 -22
  37. package/skills/vizzly/references/setup-ci.md +0 -44
package/dist/cli.js CHANGED
@@ -28,15 +28,15 @@ import { whoamiCommand } from './commands/whoami.js';
28
28
  import { createPluginServices } from './plugin-api.js';
29
29
  import { loadPlugins } from './plugin-loader.js';
30
30
  import { createServices } from './services/index.js';
31
- import { generateStaticReport, getReportFileUrl } from './services/static-report-generator.js';
32
31
  import { withTimeout } from './utils/async-utils.js';
33
- import { openBrowser } from './utils/browser.js';
32
+ import { runUpdatedCli } from './utils/cli-update.js';
34
33
  import { colors } from './utils/colors.js';
35
34
  import { loadConfig } from './utils/config-loader.js';
36
35
  import { getContext } from './utils/context.js';
37
36
  import { saveUserPath } from './utils/global-config.js';
38
37
  import * as output from './utils/output.js';
39
38
  import { getPackageVersion } from './utils/package-info.js';
39
+ import { notifyUpdates } from './utils/update-notifications.js';
40
40
 
41
41
  // Custom help formatting with BearDen design system
42
42
  const formatHelp = (cmd, helper) => {
@@ -395,7 +395,7 @@ function writeCommanderError(text) {
395
395
  }
396
396
  }
397
397
  }
398
- 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({
398
+ 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('--no-update-notifier', 'Skip update checks and notices').option('--strict', 'Fail on any error (default: be resilient, warn on non-critical issues)').configureHelp({
399
399
  formatHelp
400
400
  });
401
401
  program.configureOutput({
@@ -404,6 +404,13 @@ program.configureOutput({
404
404
  program.showHelpAfterError('(Run vizzly --help for available commands)');
405
405
  program.showSuggestionAfterError();
406
406
  program.exitOverride();
407
+ program.hook('preAction', async (_command, actionCommand) => {
408
+ if (actionCommand.name() === 'init') return;
409
+ let updatedCli = await notifyUpdates(actionCommand.optsWithGlobals());
410
+ if (!updatedCli) return;
411
+ let code = await runUpdatedCli(updatedCli, process.argv.slice(2), process.cwd());
412
+ process.exit(code);
413
+ });
407
414
 
408
415
  // Load plugins before defining commands
409
416
  // We need to manually parse to get the config option early
@@ -430,38 +437,49 @@ output.configure({
430
437
  color: colorOverride,
431
438
  json: jsonArg
432
439
  });
433
- let config = await loadConfig(configPath, {
434
- token: tokenArg
435
- });
436
- let services = createServices(config);
437
- let pluginServices = createPluginServices(services);
438
- let plugins = [];
439
- try {
440
- plugins = await loadPlugins(configPath, config);
441
- for (let plugin of plugins) {
442
- try {
443
- // Add timeout protection for plugin registration (5 seconds)
444
- let registerPromise = plugin.register(program, {
445
- config,
446
- services: pluginServices,
447
- output
448
- });
449
- await withTimeout(registerPromise, 5000, 'Plugin registration timeout (5s)');
450
- output.debug(`Registered plugin: ${plugin.name}`);
451
- } catch (error) {
452
- output.warn(`Failed to register plugin ${plugin.name}: ${error.message}`);
440
+ let setupCommand = findRequestedCommand(process.argv, new Set(['init', 'project']))?.name;
441
+ let needsRuntimeBootstrap = !['init', 'project'].includes(setupCommand);
442
+ if (needsRuntimeBootstrap) {
443
+ let config = await loadConfig(configPath, {
444
+ token: tokenArg
445
+ });
446
+ let services = createServices(config);
447
+ let pluginServices = createPluginServices(services);
448
+ try {
449
+ let plugins = await loadPlugins(configPath, config);
450
+ for (let plugin of plugins) {
451
+ try {
452
+ // Add timeout protection for plugin registration (5 seconds)
453
+ let registerPromise = plugin.register(program, {
454
+ config,
455
+ services: pluginServices,
456
+ output
457
+ });
458
+ await withTimeout(registerPromise, 5000, 'Plugin registration timeout (5s)');
459
+ output.debug(`Registered plugin: ${plugin.name}`);
460
+ } catch (error) {
461
+ output.warn(`Failed to register plugin ${plugin.name}: ${error.message}`);
462
+ }
453
463
  }
464
+ } catch (error) {
465
+ output.debug(`Plugin loading failed: ${error.message}`);
454
466
  }
455
- } catch (error) {
456
- output.debug(`Plugin loading failed: ${error.message}`);
457
467
  }
458
- program.command('init').description('Initialize Vizzly in your project').option('--force', 'Overwrite existing configuration').option('--agent-skill', 'Install or refresh the repo-local Vizzly agent skill').option('--agent-guidance', 'Install or refresh the repo-local Vizzly skill and AGENTS.md guidance').option('--skip-agent-skill', 'Skip the Vizzly agent skill prompt').action(async options => {
468
+ program.command('init').description('Initialize Vizzly in your project').option('--quick', 'Create an editable config without the setup wizard').option('--force', 'Overwrite existing configuration').option('--agent-skill', 'Install or refresh the Vizzly agent skill').option('--agent-guidance', 'Install or refresh the Vizzly skill and AGENTS.md guidance').option('--skip-agent-skill', 'Skip the Vizzly agent skill prompt').action(async options => {
459
469
  let globalOptions = getGlobalOptions();
460
- await init({
461
- ...globalOptions,
462
- ...options,
463
- plugins
464
- });
470
+ try {
471
+ let result = await init({
472
+ ...globalOptions,
473
+ ...options
474
+ });
475
+ process.exitCode = result.exitCode || 0;
476
+ } catch (error) {
477
+ output.error(error.message, error, {
478
+ changes: error.completed || []
479
+ });
480
+ if (error.completed?.length) output.list(error.completed.map(change => change.path));
481
+ process.exitCode = error.code === 'INIT_INPUT_REQUIRED' ? 2 : 1;
482
+ }
465
483
  });
466
484
  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) => {
467
485
  let globalOptions = getGlobalOptions();
@@ -555,7 +573,10 @@ tddCmd.command('run <command>').description('Run tests once in TDD mode with loc
555
573
  const {
556
574
  result,
557
575
  cleanup
558
- } = await tddCommand(command, options, globalOptions);
576
+ } = await tddCommand(command, {
577
+ ...options,
578
+ autoPort: options.port === undefined
579
+ }, globalOptions);
559
580
 
560
581
  // Track cleanup state to prevent double cleanup
561
582
  let isCleaningUp = false;
@@ -575,26 +596,6 @@ tddCmd.command('run <command>').description('Run tests once in TDD mode with loc
575
596
  process.once('SIGINT', sigintHandler);
576
597
  process.once('SIGTERM', sigtermHandler);
577
598
 
578
- // If the run captured screenshots, generate a static report for review.
579
- let hasComparisons = result?.screenshotsCaptured > 0 || result?.comparisons?.length > 0 || result?.summary?.total > 0;
580
- if (hasComparisons) {
581
- // Note: Tests have completed at this point, so report-data.json is stable.
582
- // The report reflects the final state of all comparisons.
583
- const reportResult = await generateStaticReport(process.cwd());
584
- if (reportResult.success) {
585
- const reportUrl = getReportFileUrl(reportResult.reportPath);
586
- output.print(` ${colors.brand.textTertiary('→')} Report: ${colors.info(reportUrl)}`);
587
- output.blank();
588
-
589
- // Open report in browser unless --no-open
590
- if (options.open !== false) {
591
- await openBrowser(reportUrl);
592
- }
593
- } else {
594
- output.warn(`Failed to generate static report: ${reportResult.error}`);
595
- }
596
- }
597
-
598
599
  // Remove signal handlers before normal cleanup to prevent double cleanup
599
600
  process.off('SIGINT', sigintHandler);
600
601
  process.off('SIGTERM', sigtermHandler);
@@ -950,7 +951,7 @@ program.command('whoami').description('Show current authentication status and us
950
951
 
951
952
  // Save PATH for the menubar app before commands read or update credentials
952
953
  // in the same config file.
953
- await saveUserPath().catch(() => {});
954
+ if (needsRuntimeBootstrap) await saveUserPath().catch(() => {});
954
955
  let commandNames = new Set(program.commands.map(command => command.name()));
955
956
  let nestedCommandNames = new Map(program.commands.map(command => [command.name(), new Set(command.registeredArguments.length ? [] : command.commands.map(subcommand => subcommand.name()))]));
956
957
  let normalizedArgv = normalizeJsonArgv(process.argv, commandNames);
@@ -971,7 +972,7 @@ if (requestedCommand && !requestedCommand.known) {
971
972
  process.exit(1);
972
973
  }
973
974
  try {
974
- program.parse(normalizedArgv);
975
+ await program.parseAsync(normalizedArgv);
975
976
  } catch (error) {
976
977
  if (error.exitCode == null) {
977
978
  output.error('Unexpected CLI error', error);