@vizzly-testing/cli
Version:
Visual review platform for UI developers and designers
261 lines (243 loc) • 13.6 kB
JavaScript
import 'dotenv/config';
import { program } from 'commander';
import { init } from './commands/init.js';
import { uploadCommand, validateUploadOptions } from './commands/upload.js';
import { runCommand, validateRunOptions } from './commands/run.js';
import { tddCommand, validateTddOptions } from './commands/tdd.js';
import { tddStartCommand, tddStopCommand, tddStatusCommand, runDaemonChild } from './commands/tdd-daemon.js';
import { statusCommand, validateStatusOptions } from './commands/status.js';
import { finalizeCommand, validateFinalizeOptions } from './commands/finalize.js';
import { doctorCommand, validateDoctorOptions } from './commands/doctor.js';
import { loginCommand, validateLoginOptions } from './commands/login.js';
import { logoutCommand, validateLogoutOptions } from './commands/logout.js';
import { whoamiCommand, validateWhoamiOptions } from './commands/whoami.js';
import { projectSelectCommand, projectListCommand, projectTokenCommand, projectRemoveCommand, validateProjectOptions } from './commands/project.js';
import { getPackageVersion } from './utils/package-info.js';
import { loadPlugins } from './plugin-loader.js';
import { loadConfig } from './utils/config-loader.js';
import { createComponentLogger } from './utils/logger-factory.js';
import { createServiceContainer } from './container/index.js';
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').option('--json', 'Machine-readable output').option('--no-color', 'Disable colored output');
// Load plugins before defining commands
// We need to manually parse to get the config option early
let configPath = null;
let verboseMode = false;
for (let i = 0; i < process.argv.length; i++) {
if ((process.argv[i] === '-c' || process.argv[i] === '--config') && process.argv[i + 1]) {
configPath = process.argv[i + 1];
}
if (process.argv[i] === '-v' || process.argv[i] === '--verbose') {
verboseMode = true;
}
}
let config = await loadConfig(configPath, {});
let logger = createComponentLogger('CLI', {
level: config.logLevel || (verboseMode ? 'debug' : 'warn'),
verbose: verboseMode || false
});
let container = await createServiceContainer(config);
let plugins = [];
try {
plugins = await loadPlugins(configPath, config, logger);
for (let plugin of plugins) {
try {
// Add timeout protection for plugin registration (5 seconds)
let registerPromise = plugin.register(program, {
config,
logger,
services: container
});
let timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Plugin registration timeout (5s)')), 5000));
await Promise.race([registerPromise, timeoutPromise]);
logger.debug(`Registered plugin: ${plugin.name}`);
} catch (error) {
logger.warn(`Failed to register plugin ${plugin.name}: ${error.message}`);
}
}
} catch (error) {
logger.debug(`Plugin loading failed: ${error.message}`);
}
program.command('init').description('Initialize Vizzly in your project').option('--force', 'Overwrite existing configuration').action(async options => {
const globalOptions = program.opts();
await init({
...globalOptions,
...options,
plugins
});
});
program.command('upload').description('Upload screenshots to Vizzly').argument('<path>', 'Path to screenshots directory or file').option('-b, --build-name <name>', 'Build name for grouping').option('-m, --metadata <json>', 'Additional metadata as JSON').option('--batch-size <n>', 'Upload batch size', v => parseInt(v, 10)).option('--upload-timeout <ms>', 'Upload timeout in milliseconds', v => parseInt(v, 10)).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', parseFloat).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) => {
const globalOptions = program.opts();
// Validate options
const validationErrors = validateUploadOptions(path, options);
if (validationErrors.length > 0) {
console.error('Validation errors:');
validationErrors.forEach(error => console.error(` - ${error}`));
process.exit(1);
}
await uploadCommand(path, options, globalOptions);
});
// TDD command with subcommands
const tddCmd = program.command('tdd').description('Run tests in TDD mode with local visual comparisons');
// TDD Start - Background server
tddCmd.command('start').description('Start background TDD server').option('--port <port>', 'Port for screenshot server', '47392').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', parseFloat).option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--token <token>', 'API token override').option('--daemon-child', 'Internal: run as daemon child process').action(async options => {
const globalOptions = program.opts();
// If this is a daemon child process, run the server directly
if (options.daemonChild) {
await runDaemonChild(options, globalOptions);
return;
}
await tddStartCommand(options, globalOptions);
});
// TDD Stop - Kill background server
tddCmd.command('stop').description('Stop background TDD server').action(async options => {
const globalOptions = program.opts();
await tddStopCommand(options, globalOptions);
});
// TDD Status - Check server status
tddCmd.command('status').description('Check TDD server status').action(async options => {
const globalOptions = program.opts();
await tddStatusCommand(options, globalOptions);
});
// TDD Run - One-off test run (primary workflow)
tddCmd.command('run <command>').description('Run tests once in TDD mode with local visual comparisons').option('--port <port>', 'Port for screenshot server', '47392').option('--branch <branch>', 'Git branch override').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', parseFloat).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)').action(async (command, options) => {
const globalOptions = program.opts();
// Validate options
const validationErrors = validateTddOptions(command, options);
if (validationErrors.length > 0) {
console.error('Validation errors:');
validationErrors.forEach(error => console.error(` - ${error}`));
process.exit(1);
}
const {
result,
cleanup
} = await tddCommand(command, options, globalOptions);
// Set up cleanup on process signals
const handleCleanup = async () => {
await cleanup();
};
process.once('SIGINT', () => {
handleCleanup().then(() => process.exit(1));
});
process.once('SIGTERM', () => {
handleCleanup().then(() => process.exit(1));
});
if (result && !result.success && result.exitCode > 0) {
await cleanup();
process.exit(result.exitCode);
}
await cleanup();
});
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('--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) => {
const globalOptions = program.opts();
// Validate options
const validationErrors = validateRunOptions(command, options);
if (validationErrors.length > 0) {
console.error('Validation errors:');
validationErrors.forEach(error => console.error(` - ${error}`));
process.exit(1);
}
try {
const result = await runCommand(command, options, globalOptions);
if (result && !result.success && result.exitCode > 0) {
process.exit(result.exitCode);
}
} catch (error) {
console.error('Command failed:', error.message);
if (globalOptions.verbose) {
console.error('Stack trace:', error.stack);
}
process.exit(1);
}
});
program.command('status').description('Check the status of a build').argument('<build-id>', 'Build ID to check status for').action(async (buildId, options) => {
const globalOptions = program.opts();
// Validate options
const validationErrors = validateStatusOptions(buildId, options);
if (validationErrors.length > 0) {
console.error('Validation errors:');
validationErrors.forEach(error => console.error(` - ${error}`));
process.exit(1);
}
await statusCommand(buildId, options, globalOptions);
});
program.command('finalize').description('Finalize a parallel build after all shards complete').argument('<parallel-id>', 'Parallel ID to finalize').action(async (parallelId, options) => {
const globalOptions = program.opts();
// Validate options
const validationErrors = validateFinalizeOptions(parallelId, options);
if (validationErrors.length > 0) {
console.error('Validation errors:');
validationErrors.forEach(error => console.error(` - ${error}`));
process.exit(1);
}
await finalizeCommand(parallelId, options, globalOptions);
});
program.command('doctor').description('Run diagnostics to check your environment and configuration').option('--api', 'Include API connectivity checks').action(async options => {
const globalOptions = program.opts();
// Validate options
const validationErrors = validateDoctorOptions(options);
if (validationErrors.length > 0) {
console.error('Validation errors:');
validationErrors.forEach(error => console.error(` - ${error}`));
process.exit(1);
}
await doctorCommand(options, globalOptions);
});
program.command('login').description('Authenticate with your Vizzly account').option('--api-url <url>', 'API URL override').action(async options => {
const globalOptions = program.opts();
// Validate options
const validationErrors = validateLoginOptions(options);
if (validationErrors.length > 0) {
console.error('Validation errors:');
validationErrors.forEach(error => console.error(` - ${error}`));
process.exit(1);
}
await loginCommand(options, globalOptions);
});
program.command('logout').description('Clear stored authentication tokens').option('--api-url <url>', 'API URL override').action(async options => {
const globalOptions = program.opts();
// Validate options
const validationErrors = validateLogoutOptions(options);
if (validationErrors.length > 0) {
console.error('Validation errors:');
validationErrors.forEach(error => console.error(` - ${error}`));
process.exit(1);
}
await logoutCommand(options, globalOptions);
});
program.command('whoami').description('Show current authentication status and user information').option('--api-url <url>', 'API URL override').action(async options => {
const globalOptions = program.opts();
// Validate options
const validationErrors = validateWhoamiOptions(options);
if (validationErrors.length > 0) {
console.error('Validation errors:');
validationErrors.forEach(error => console.error(` - ${error}`));
process.exit(1);
}
await whoamiCommand(options, globalOptions);
});
program.command('project:select').description('Configure project for current directory').option('--api-url <url>', 'API URL override').action(async options => {
const globalOptions = program.opts();
// Validate options
const validationErrors = validateProjectOptions(options);
if (validationErrors.length > 0) {
console.error('Validation errors:');
validationErrors.forEach(error => console.error(` - ${error}`));
process.exit(1);
}
await projectSelectCommand(options, globalOptions);
});
program.command('project:list').description('Show all configured projects').action(async options => {
const globalOptions = program.opts();
await projectListCommand(options, globalOptions);
});
program.command('project:token').description('Show project token for current directory').action(async options => {
const globalOptions = program.opts();
await projectTokenCommand(options, globalOptions);
});
program.command('project:remove').description('Remove project configuration for current directory').action(async options => {
const globalOptions = program.opts();
await projectRemoveCommand(options, globalOptions);
});
program.parse();