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
JavaScript
/**
* 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'
});
}