UNPKG

@debugg-ai/cli

Version:
600 lines 29.2 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const commander_1 = require("commander"); const path = __importStar(require("path")); const fs = __importStar(require("fs-extra")); const dotenv_1 = require("dotenv"); const test_manager_1 = require("./lib/test-manager"); const workflow_orchestrator_1 = require("./lib/workflow-orchestrator"); const system_logger_1 = require("./util/system-logger"); const crypto_1 = require("crypto"); const telemetry_1 = require("./services/telemetry"); // Load environment variables (0, dotenv_1.config)(); const program = new commander_1.Command(); program .name('@debugg-ai/cli') .description('CLI tool for running DebuggAI tests in CI/CD environments') .version('1.0.0'); program .command('test') .description('Run E2E tests based on git changes') .option('-k, --api-key <key>', 'DebuggAI API key (can also use DEBUGGAI_API_KEY env var)') .option('-u, --base-url <url>', 'API base URL (default: https://api.debugg.ai)') .option('-r, --repo-path <path>', 'Repository path (default: current directory)') .option('-o, --output-dir <dir>', 'Test output directory (default: tests/debugg-ai)') .option('-c, --commit <hash>', 'Specific commit hash to analyze (instead of working changes)') .option('--commit-range <range>', 'Commit range to analyze (e.g., HEAD~3..HEAD, main..feature-branch)') .option('--since <date>', 'Analyze commits since date/time (e.g., "2024-01-01", "2 days ago")') .option('--last <number>', 'Analyze last N commits (e.g., --last 3)') .option('--pr <number>', 'PR number for GitHub App-based testing (requires GitHub App integration)') .option('--pr-sequence', 'Enable PR commit sequence testing (sends individual test requests for each commit in PR)') .option('--base-branch <branch>', 'Base branch for PR testing (auto-detected from GitHub env if not provided)') .option('--head-branch <branch>', 'Head branch for PR testing (auto-detected from GitHub env if not provided)') .option('--wait-for-server', 'Wait for local development server to be ready') .option('--server-port <port>', 'Local server port to wait for (default: 3000)', '3000') .option('--server-timeout <ms>', 'Server wait timeout in milliseconds (default: 60000)', '60000') .option('--max-test-time <ms>', 'Maximum test wait time in milliseconds (default: 600000)', '600000') .option('--tunnel-uuid <uuid>', 'Create ngrok tunnel with custom UUID endpoint (e.g., <uuid>.debugg.ai)') .option('--tunnel-port <port>', 'Port to tunnel (default: 3000)', '3000') .option('--download-artifacts', 'Download test artifacts (scripts, recordings, JSON results) to local filesystem') .option('-v, --verbose', 'Enable verbose logging for debugging') .option('--dev', 'Enable development logging (shows all technical details, tunnel info, API calls, git details, timing)') .option('--no-color', 'Disable colored output') .action(async (options) => { try { // Track command start telemetry_1.telemetry.trackCommandStart('test', options); // Set up logging mode based on flags if (options.dev) { system_logger_1.systemLogger.setDevMode(true); system_logger_1.systemLogger.debug('Development mode enabled - showing all technical details'); } else if (options.verbose) { system_logger_1.systemLogger.setDevMode(true); system_logger_1.systemLogger.debug('Verbose logging enabled'); } // Disable colors if requested (now handled by loggers) if (options.noColor) { // Color handling is now managed by the logger system } system_logger_1.systemLogger.info('DebuggAI Test Runner'); if (!system_logger_1.systemLogger.getDevMode()) { console.log('='.repeat(50)); } // Get API key const apiKey = options.apiKey || process.env.DEBUGGAI_API_KEY; if (!apiKey) { system_logger_1.systemLogger.error('API key is required. Provide it via --api-key or DEBUGGAI_API_KEY environment variable.'); process.exit(1); } // Get repository path const repoPath = options.repoPath ? path.resolve(options.repoPath) : process.cwd(); // Validate repository path exists if (!await fs.pathExists(repoPath)) { system_logger_1.systemLogger.error(`Repository path does not exist: ${repoPath}`); process.exit(1); } // Validate it's a git repository const gitDir = path.join(repoPath, '.git'); if (!await fs.pathExists(gitDir)) { system_logger_1.systemLogger.error(`Not a git repository: ${repoPath}`); process.exit(1); } system_logger_1.systemLogger.debug('CLI configuration', { category: 'cli', details: { repoPath, apiKey: `${apiKey.substring(0, 8)}...` } }); // Generate a tunnel key for backend to create tunnel endpoints const tunnelKey = (0, crypto_1.randomUUID)(); system_logger_1.systemLogger.debug('Generated tunnel key', { category: 'tunnel', details: { key: tunnelKey.substring(0, 8) + '...' } }); // Initialize test manager (after all validations pass) let testManager; if (options.tunnelUuid) { // Use new auto-tunnel flow where backend provides ngrok token after commit suite creation system_logger_1.systemLogger.debug('Using auto-tunnel mode', { category: 'tunnel', details: { uuid: options.tunnelUuid } }); system_logger_1.systemLogger.info(`Using auto-tunnel mode with UUID: ${options.tunnelUuid}`); testManager = test_manager_1.TestManager.withAutoTunnel({ apiKey, repoPath, baseUrl: options.baseUrl, ...(options.outputDir && { testOutputDir: options.outputDir }), serverTimeout: parseInt(options.serverTimeout) || 60000, maxTestWaitTime: parseInt(options.maxTestTime) || 600000, downloadArtifacts: options.downloadArtifacts || false, tunnelKey, // Pass the generated tunnel key // Commit analysis options commit: options.commit, commitRange: options.commitRange, since: options.since, ...(options.last && { last: parseInt(options.last) }), // PR sequence options prSequence: options.prSequence || false, baseBranch: options.baseBranch, headBranch: options.headBranch, // GitHub App PR testing ...(options.pr && { pr: parseInt(options.pr) }) }, options.tunnelUuid, parseInt(options.tunnelPort) || 3000); } else { // Standard mode with tunnel key for backend tunnel creation testManager = new test_manager_1.TestManager({ apiKey, repoPath, baseUrl: options.baseUrl, ...(options.outputDir && { testOutputDir: options.outputDir }), serverTimeout: parseInt(options.serverTimeout) || 60000, maxTestWaitTime: parseInt(options.maxTestTime) || 600000, downloadArtifacts: options.downloadArtifacts || false, tunnelKey, // Pass the generated tunnel key createTunnel: true, // Enable tunnel creation tunnelPort: parseInt(options.serverPort) || 3000, // Use server port for tunnel // Commit analysis options commit: options.commit, commitRange: options.commitRange, since: options.since, ...(options.last && { last: parseInt(options.last) }), // PR sequence options prSequence: options.prSequence || false, baseBranch: options.baseBranch, headBranch: options.headBranch, // GitHub App PR testing ...(options.pr && { pr: parseInt(options.pr) }) }); } // Wait for server if requested if (options.waitForServer) { const serverPort = parseInt(options.serverPort); const serverTimeout = parseInt(options.serverTimeout) || 60000; system_logger_1.systemLogger.debug('Waiting for development server', { category: 'server', details: { port: serverPort, timeout: serverTimeout } }); if (system_logger_1.systemLogger.getDevMode()) { system_logger_1.systemLogger.debug(`Waiting for development server on port ${serverPort}`, { category: 'server' }); } else { system_logger_1.systemLogger.progress.start(`Waiting for development server on port ${serverPort}`); } const serverReady = await testManager.waitForServer(serverPort, serverTimeout); if (!serverReady) { system_logger_1.systemLogger.error(`Server on port ${serverPort} did not start within ${serverTimeout}ms`); process.exit(1); } } // Run the tests if (system_logger_1.systemLogger.getDevMode()) { system_logger_1.systemLogger.debug('Starting test analysis and generation', { category: 'test' }); } else { system_logger_1.systemLogger.progress.start('Starting test analysis and generation'); } const result = await testManager.runCommitTests(); if (result.success) { system_logger_1.systemLogger.success('Tests completed successfully!'); if (result.testFiles && result.testFiles.length > 0) { system_logger_1.systemLogger.displayFileList(result.testFiles, repoPath); } system_logger_1.systemLogger.debug('Test suite completed', { category: 'test', details: { suiteUuid: result.suiteUuid } }); system_logger_1.systemLogger.info(`Test suite ID: ${result.suiteUuid}`); // Check if any tests failed (process.exitCode may have been set by reportResults) if (process.exitCode === 1) { system_logger_1.systemLogger.error('Some tests failed - see results above'); process.exit(1); } else { process.exit(0); } } else { system_logger_1.systemLogger.error(`Tests failed: ${result.error}`); process.exit(1); } } catch (error) { // Re-throw test exit errors to prevent them from being handled if (error instanceof Error && error.isSuccessExit) { throw error; } const errorMsg = error instanceof Error ? error.message : String(error); system_logger_1.systemLogger.error('Unexpected error: ' + errorMsg); if (process.env.DEBUG) { system_logger_1.systemLogger.error('Stack trace: ' + error?.stack); } telemetry_1.telemetry.trackCommandComplete('test', false, errorMsg); await telemetry_1.telemetry.shutdown(); process.exit(1); } }); program .command('status') .description('Check the status of a test suite') .requiredOption('-s, --suite-id <id>', 'Test suite UUID') .option('-k, --api-key <key>', 'DebuggAI API key (can also use DEBUGGAI_API_KEY env var)') .option('-u, --base-url <url>', 'API base URL (default: https://api.debugg.ai)') .option('--dev', 'Enable development logging (shows all technical details)') .option('--no-color', 'Disable colored output') .action(async (options) => { try { // Track command start telemetry_1.telemetry.trackCommandStart('status', options); // Set up development mode if (options.dev) { process.env.DEBUGGAI_LOG_LEVEL = 'DEBUG'; process.env.DEBUGGAI_DEV_MODE = 'true'; system_logger_1.systemLogger.debug('Development mode enabled'); } // Disable colors if requested (now handled by loggers) if (options.noColor) { // Color handling is now managed by the logger system } system_logger_1.systemLogger.info('DebuggAI Test Status'); console.log('='.repeat(50)); // Get API key const apiKey = options.apiKey || process.env.DEBUGGAI_API_KEY; if (!apiKey) { system_logger_1.systemLogger.error('API key is required.'); process.exit(1); } // Create a basic test manager just for API access const testManager = new test_manager_1.TestManager({ apiKey, repoPath: process.cwd(), // Not used for status check baseUrl: options.baseUrl }); // Get test suite status const suite = await testManager.client.getCommitTestSuiteStatus(options.suiteId); if (!suite) { system_logger_1.systemLogger.error(`Test suite not found: ${options.suiteId}`); process.exit(1); } system_logger_1.systemLogger.info(`Suite ID: ${suite.uuid}`); system_logger_1.systemLogger.info(`Name: ${suite.name || 'Unnamed'}`); system_logger_1.systemLogger.info(`Status: ${getStatusColor(suite.status || 'unknown')}`); system_logger_1.systemLogger.info(`Tests: ${suite.tests?.length || 0}`); if (suite.tests && suite.tests.length > 0) { system_logger_1.systemLogger.info('\nTest Details:'); for (const test of suite.tests) { const status = test.curRun?.status || 'unknown'; console.log(` • ${test.name || test.uuid}: ${getStatusColor(status)}`); } } // Track successful completion telemetry_1.telemetry.trackCommandComplete('status', true); await telemetry_1.telemetry.shutdown(); } catch (error) { // Re-throw test exit errors to prevent them from being handled if (error instanceof Error && error.isSuccessExit) { throw error; } const errorMsg = error instanceof Error ? error.message : String(error); system_logger_1.systemLogger.error('Error checking status: ' + errorMsg); telemetry_1.telemetry.trackCommandComplete('status', false, errorMsg); await telemetry_1.telemetry.shutdown(); process.exit(1); } }); program .command('list') .description('List test suites for a repository') .option('-k, --api-key <key>', 'DebuggAI API key (can also use DEBUGGAI_API_KEY env var)') .option('-u, --base-url <url>', 'API base URL (default: https://api.debugg.ai)') .option('-r, --repo <name>', 'Repository name filter') .option('-b, --branch <name>', 'Branch name filter') .option('-l, --limit <number>', 'Limit number of results (default: 20)', '20') .option('-p, --page <number>', 'Page number (default: 1)', '1') .option('--dev', 'Enable development logging (shows all technical details)') .option('--no-color', 'Disable colored output') .action(async (options) => { try { // Track command start telemetry_1.telemetry.trackCommandStart('list', options); // Set up development mode if (options.dev) { process.env.DEBUGGAI_LOG_LEVEL = 'DEBUG'; process.env.DEBUGGAI_DEV_MODE = 'true'; system_logger_1.systemLogger.debug('Development mode enabled'); } // Disable colors if requested (now handled by loggers) if (options.noColor) { // Color handling is now managed by the logger system } system_logger_1.systemLogger.info('DebuggAI Test Suites'); console.log('='.repeat(50)); // Get API key const apiKey = options.apiKey || process.env.DEBUGGAI_API_KEY; if (!apiKey) { system_logger_1.systemLogger.error('API key is required.'); process.exit(1); } // Create a basic test manager just for API access const testManager = new test_manager_1.TestManager({ apiKey, repoPath: process.cwd(), // Not used for listing baseUrl: options.baseUrl }); // List test suites const result = await testManager.client.listTestSuites({ repoName: options.repo, branchName: options.branch, limit: parseInt(options.limit), page: parseInt(options.page) }); if (result.suites.length === 0) { system_logger_1.systemLogger.warn('No test suites found.'); return; } system_logger_1.systemLogger.info(`Found ${result.total} test suites (showing ${result.suites.length}):`); console.log(''); for (const suite of result.suites) { console.log(`${suite.name || suite.uuid}`); console.log(` Status: ${getStatusColor(suite.status || 'unknown')}`); console.log(` Tests: ${suite.tests?.length || 0}`); console.log(` UUID: ${suite.uuid}`); console.log(''); } // Track successful completion telemetry_1.telemetry.trackCommandComplete('list', true); await telemetry_1.telemetry.shutdown(); } catch (error) { // Re-throw test exit errors to prevent them from being handled if (error instanceof Error && error.isSuccessExit) { throw error; } const errorMsg = error instanceof Error ? error.message : String(error); system_logger_1.systemLogger.error('Error listing test suites: ' + errorMsg); telemetry_1.telemetry.trackCommandComplete('list', false, errorMsg); await telemetry_1.telemetry.shutdown(); process.exit(1); } }); program .command('workflow') .description('Run complete E2E testing workflow with server management and tunnel setup') .option('-k, --api-key <key>', 'DebuggAI API key (can also use DEBUGGAI_API_KEY env var)') .option('-u, --base-url <url>', 'API base URL (default: https://api.debugg.ai)') .option('-r, --repo-path <path>', 'Repository path (default: current directory)') .option('-o, --output-dir <dir>', 'Test output directory (default: tests/debugg-ai)') .option('-p, --port <port>', 'Server port (default: 3000)', '3000') .option('-c, --command <cmd>', 'Server start command (default: npm start)', 'npm start') .option('--server-args <args>', 'Server command arguments (comma-separated)') .option('--server-cwd <path>', 'Server working directory') .option('--server-env <env>', 'Server environment variables (KEY=value,KEY2=value2)') .option('--ngrok-token <token>', 'Ngrok auth token (can also use NGROK_AUTH_TOKEN env var)') .option('--ngrok-subdomain <subdomain>', 'Custom ngrok subdomain') .option('--ngrok-domain <domain>', 'Custom ngrok domain') .option('--base-domain <domain>', 'Base domain for tunnels (default: ngrok.debugg.ai)') .option('--max-test-time <ms>', 'Maximum test wait time in milliseconds (default: 600000)', '600000') .option('--server-timeout <ms>', 'Server startup timeout in milliseconds (default: 60000)', '60000') .option('--cleanup-on-success', 'Cleanup resources after successful completion (default: true)', true) .option('--cleanup-on-error', 'Cleanup resources after errors (default: true)', true) .option('--download-artifacts', 'Download test artifacts (scripts, recordings, JSON results) to local filesystem') .option('--pr-sequence', 'Enable PR commit sequence testing (sends individual test requests for each commit in PR)') .option('--base-branch <branch>', 'Base branch for PR testing (auto-detected from GitHub env if not provided)') .option('--head-branch <branch>', 'Head branch for PR testing (auto-detected from GitHub env if not provided)') .option('--verbose', 'Verbose logging') .option('--dev', 'Enable development logging (shows all technical details, server logs, tunnel info)') .option('--no-color', 'Disable colored output') .action(async (options) => { try { // Set up development mode if (options.dev) { process.env.DEBUGGAI_LOG_LEVEL = 'DEBUG'; process.env.DEBUGGAI_DEV_MODE = 'true'; system_logger_1.systemLogger.debug('Development mode enabled'); } // Disable colors if requested (now handled by loggers) if (options.noColor) { // Color handling is now managed by the logger system } system_logger_1.systemLogger.info('DebuggAI Workflow Runner'); console.log('='.repeat(50)); // Get API key const apiKey = options.apiKey || process.env.DEBUGGAI_API_KEY; if (!apiKey) { system_logger_1.systemLogger.error('API key is required. Provide it via --api-key or DEBUGGAI_API_KEY environment variable.'); process.exit(1); } // Get repository path const repoPath = options.repoPath ? path.resolve(options.repoPath) : process.cwd(); // Validate repository path exists if (!await fs.pathExists(repoPath)) { system_logger_1.systemLogger.error(`Repository path does not exist: ${repoPath}`); process.exit(1); } // Validate it's a git repository const gitDir = path.join(repoPath, '.git'); if (!await fs.pathExists(gitDir)) { system_logger_1.systemLogger.error(`Not a git repository: ${repoPath}`); process.exit(1); } system_logger_1.systemLogger.debug('Workflow configuration', { category: 'workflow', details: { repoPath, apiKey: `${apiKey.substring(0, 8)}...` } }); // Parse server command and args const [command, ...defaultArgs] = options.command.split(' '); const serverArgs = options.serverArgs ? options.serverArgs.split(',').map((arg) => arg.trim()) : defaultArgs; // Parse environment variables const serverEnv = {}; if (options.serverEnv) { options.serverEnv.split(',').forEach((pair) => { const [key, value] = pair.trim().split('='); if (key && value) { serverEnv[key] = value; } }); } // Generate a tunnel key for backend to create tunnel endpoints const tunnelKey = (0, crypto_1.randomUUID)(); system_logger_1.systemLogger.debug('Generated tunnel key for workflow', { category: 'tunnel', details: { key: tunnelKey.substring(0, 8) + '...' } }); // Initialize workflow orchestrator const orchestrator = new workflow_orchestrator_1.WorkflowOrchestrator({ ngrokAuthToken: options.ngrokToken || process.env.NGROK_AUTH_TOKEN, baseDomain: options.baseDomain, verbose: options.verbose || options.dev, // Dev mode implies verbose devMode: options.dev }); // Configure workflow const workflowConfig = { server: { command, args: serverArgs, port: parseInt(options.port), cwd: options.serverCwd || repoPath, env: serverEnv, startupTimeout: parseInt(options.serverTimeout) }, tunnel: { port: parseInt(options.port), subdomain: options.ngrokSubdomain, customDomain: options.ngrokDomain, authtoken: options.ngrokToken || process.env.NGROK_AUTH_TOKEN }, test: { apiKey, baseUrl: options.baseUrl, repoPath, testOutputDir: options.outputDir, maxTestWaitTime: parseInt(options.maxTestTime), downloadArtifacts: options.downloadArtifacts || false, tunnelKey, // Add the generated tunnel key createTunnel: true, // Enable tunnel creation tunnelPort: parseInt(options.port) || 3000, // Use server port for tunnel // PR sequence options prSequence: options.prSequence || false, baseBranch: options.baseBranch, headBranch: options.headBranch, // GitHub App PR testing ...(options.pr && { pr: parseInt(options.pr) }) }, cleanup: { onSuccess: options.cleanupOnSuccess, onError: options.cleanupOnError } }; system_logger_1.systemLogger.debug('Starting workflow orchestrator'); system_logger_1.systemLogger.progress.start('Starting complete testing workflow'); const result = await orchestrator.executeWorkflow(workflowConfig); if (result.success) { system_logger_1.systemLogger.success('Workflow completed successfully!'); if (result.tunnelInfo) { system_logger_1.systemLogger.info(`Tunnel URL: ${result.tunnelInfo.url}`); } if (result.serverUrl) { system_logger_1.systemLogger.info(`Local Server: ${result.serverUrl}`); } if (result.testResult?.testFiles && result.testResult.testFiles.length > 0) { system_logger_1.systemLogger.displayFileList(result.testResult.testFiles, repoPath); } if (result.testResult?.suiteUuid) { system_logger_1.systemLogger.info(`Test suite ID: ${result.testResult.suiteUuid}`); } // Check if any tests failed (process.exitCode may have been set by reportResults) if (process.exitCode === 1) { system_logger_1.systemLogger.error('Some tests failed - see results above'); process.exit(1); } else { process.exit(0); } } else { system_logger_1.systemLogger.error(`Workflow failed: ${result.error}`); process.exit(1); } } catch (error) { // Re-throw test exit errors to prevent them from being handled if (error instanceof Error && error.isSuccessExit) { throw error; } system_logger_1.systemLogger.error('Unexpected workflow error: ' + (error instanceof Error ? error.message : String(error))); if (process.env.DEBUG) { system_logger_1.systemLogger.error('Stack trace: ' + error?.stack); } process.exit(1); } }); /** * Get colored status text */ function getStatusColor(status) { switch (status) { case 'completed': return '✓ COMPLETED'; case 'failed': return '✗ FAILED'; case 'running': return '⏳ RUNNING'; case 'pending': return '⏸ PENDING'; default: return '❓ UNKNOWN'; } } // Handle unhandled promise rejections and uncaught exceptions // Only add these handlers if we're not in a test environment if (process.env.NODE_ENV !== 'test') { process.on('unhandledRejection', (reason, promise) => { system_logger_1.systemLogger.error('Unhandled Rejection at: promise=' + promise + ', reason=' + reason); process.exit(1); }); process.on('uncaughtException', (error) => { system_logger_1.systemLogger.error('Uncaught Exception: ' + error); process.exit(1); }); } // Parse command line arguments program.parse(); //# sourceMappingURL=cli.js.map