UNPKG

accs-cli

Version:

ACCS CLI — Full-featured developer tool for scaffolding, running, building, and managing multi-language projects

249 lines (204 loc) 6.68 kB
/** * Watch command - File watcher for automatic rebuilds */ import chokidar from 'chokidar'; import path from 'path'; import chalk from 'chalk'; import { logger } from '../utils/logger.js'; import { FileUtils } from '../utils/file-utils.js'; let buildInProgress = false; let buildQueue = []; export function watchCommand(program) { program .command('watch') .option('-d, --dir <directory>', 'Directory to watch', 'src') .option('-i, --ignore <patterns...>', 'Patterns to ignore') .option('-e, --extensions <extensions...>', 'File extensions to watch') .option('-v, --verbose', 'Verbose output') .option('--delay <ms>', 'Debounce delay in milliseconds', 300) .description('Watch files for changes and rebuild automatically') .action(async (options) => { try { await watchProject(options); } catch (error) { logger.error('Watch failed:', error.message); process.exit(1); } }); } async function watchProject(options) { const projectRoot = FileUtils.getProjectRoot(); const watchDir = path.join(projectRoot, options.dir); const delay = parseInt(options.delay) || 300; if (!FileUtils.exists(watchDir)) { throw new Error(`Watch directory does not exist: ${options.dir}`); } logger.info(`🔍 Watching for changes in: ${chalk.cyan(path.relative(projectRoot, watchDir))}`); // Default patterns to ignore const defaultIgnore = [ '**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**', '**/coverage/**', '**/*.log', '**/.*' // Hidden files ]; const ignorePatterns = [ ...defaultIgnore, ...(options.ignore || []) ]; // Default extensions to watch const defaultExtensions = ['js', 'mjs', 'ts', 'tsx', 'jsx', 'css', 'scss', 'less', 'html', 'json', 'md']; const watchExtensions = options.extensions || defaultExtensions; const watchPattern = watchExtensions.length > 1 ? `**/*.{${watchExtensions.join(',')}}` : `**/*.${watchExtensions[0]}`; // Configure watcher const watcher = chokidar.watch(watchPattern, { cwd: watchDir, ignored: ignorePatterns, ignoreInitial: true, persistent: true, followSymlinks: false, depth: 10, awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 100 } }); // Debounce mechanism let debounceTimer = null; const debouncedBuild = () => { if (debounceTimer) { clearTimeout(debounceTimer); } debounceTimer = setTimeout(async () => { await triggerBuild(options); }, delay); }; // Event handlers watcher.on('ready', () => { const watchedFiles = watcher.getWatched(); const totalFiles = Object.values(watchedFiles).reduce((sum, files) => sum + files.length, 0); logger.success(`Watching ${totalFiles} files`); logger.info('Press Ctrl+C to stop watching'); if (options.verbose) { logger.info(`Extensions: ${watchExtensions.join(', ')}`); logger.info(`Ignore patterns: ${ignorePatterns.length}`); } logger.separator(); // Initial build triggerBuild(options); }); watcher.on('add', (filePath) => { logger.info(`${chalk.green('+')} ${filePath}`); debouncedBuild(); }); watcher.on('change', (filePath) => { logger.info(`${chalk.yellow('~')} ${filePath}`); debouncedBuild(); }); watcher.on('unlink', (filePath) => { logger.info(`${chalk.red('-')} ${filePath}`); debouncedBuild(); }); watcher.on('addDir', (dirPath) => { if (options.verbose) { logger.info(`${chalk.blue('+')} Directory: ${dirPath}`); } }); watcher.on('unlinkDir', (dirPath) => { if (options.verbose) { logger.info(`${chalk.red('-')} Directory: ${dirPath}`); } debouncedBuild(); }); watcher.on('error', (error) => { logger.error('Watcher error:', error.message); }); // Graceful shutdown const shutdown = () => { logger.info('\nStopping file watcher...'); if (debounceTimer) { clearTimeout(debounceTimer); } watcher.close().then(() => { logger.success('File watcher stopped'); process.exit(0); }); }; process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); // Keep process alive return new Promise(() => {}); } async function triggerBuild(options) { const timestamp = new Date().toLocaleTimeString(); if (buildInProgress) { buildQueue.push({ timestamp, options }); if (options.verbose) { logger.info(`[${timestamp}] Build queued (${buildQueue.length} in queue)`); } return; } buildInProgress = true; try { logger.info(`[${timestamp}] ${chalk.blue('Building...')}`); // Import and run build command // Create a mock program to capture build options const mockProgram = { command: () => ({ option: () => mockProgram, description: () => mockProgram, action: (handler) => { // Execute the build with default options return handler({ output: 'dist', clean: false, verbose: options.verbose }); } }) }; // This is a simplified approach - in reality you'd want to properly execute the build await executeBuild(options); logger.success(`[${timestamp}] ${chalk.green('Build completed')}`); } catch (error) { logger.error(`[${timestamp}] ${chalk.red('Build failed:')} ${error.message}`); } finally { buildInProgress = false; // Process next build in queue if (buildQueue.length > 0) { const nextBuild = buildQueue.shift(); buildQueue = []; // Clear queue to avoid spam setTimeout(() => { triggerBuild(nextBuild.options); }, 100); } } } async function executeBuild(watchOptions) { const { execa } = await import('execa'); const projectRoot = FileUtils.getProjectRoot(); // Check if there's a build script in package.json const packageJsonPath = path.join(projectRoot, 'package.json'); if (FileUtils.exists(packageJsonPath)) { const packageJson = await FileUtils.readJson(packageJsonPath); if (packageJson.scripts?.build) { // Use npm script if available await execa('npm', ['run', 'build'], { cwd: projectRoot, stdio: watchOptions.verbose ? 'inherit' : 'pipe' }); return; } } // Fallback to accs build const args = ['build']; if (watchOptions.verbose) args.push('--verbose'); await execa('node', [path.join(import.meta.url, '../../../bin/accs.js'), ...args], { cwd: projectRoot, stdio: watchOptions.verbose ? 'inherit' : 'pipe' }); }