UNPKG

@factorialco/shadowdog

Version:

<img src="https://raw.githubusercontent.com/factorialco/shadowdog/refs/heads/main/logo.png" alt="drawing" width="100"/>

295 lines (294 loc) β€’ 14.2 kB
"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; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.runDaemon = void 0; const chokidar = __importStar(require("chokidar")); const debounce_1 = __importDefault(require("lodash/debounce")); const path_1 = __importDefault(require("path")); const chalk_1 = __importDefault(require("chalk")); const config_1 = require("./config"); const tasks_1 = require("./tasks"); const utils_1 = require("./utils"); const plugins_1 = require("./plugins"); const task_runner_1 = require("./task-runner"); // Helper function to find command configuration for a specific artifact const findCommandForArtifact = (config, artifactOutput) => { for (const watcher of config.watchers) { for (const cmdConfig of watcher.commands) { for (const artifact of cmdConfig.artifacts) { if (artifact.output === artifactOutput) { return { commandConfig: { command: cmdConfig.command, workingDirectory: cmdConfig.workingDirectory, files: watcher.files, environment: watcher.environment, }, watcherConfig: { files: watcher.files, environment: watcher.environment, ignored: watcher.ignored, }, }; } } } } return null; }; // Helper function to execute a task with common setup const executeTask = async (config, commandConfig, watcherConfig, eventEmitter, options) => { // Pre-process files with ignores const processedFiles = (0, utils_1.processFiles)(watcherConfig.files, [ ...watcherConfig.ignored, ...config.defaultIgnoredFiles, ]); const taskRunner = new task_runner_1.TaskRunner({ files: processedFiles, environment: watcherConfig.environment, config: { command: commandConfig.command, workingDirectory: commandConfig.workingDirectory, tags: [], artifacts: options.artifacts, }, changedFilePath: options.changedFilePath, eventEmitter, }); (0, plugins_1.filterMiddlewarePlugins)(config.plugins).forEach(({ fn, options: pluginOptions }) => { taskRunner.use(fn.middleware, pluginOptions); }); taskRunner.use(() => { return (0, tasks_1.runTask)({ command: commandConfig.command, workingDirectory: path_1.default.join(process.cwd(), commandConfig.workingDirectory), changedFilePath: options.changedFilePath, onSpawn: options.onSpawn || (() => { }), onExit: options.onExit || (() => { }), }); }); await taskRunner.execute(); }; const setupWatchers = (config, eventEmitter, getIsPaused, pendingTasks, killPendingTasks) => { return Promise.all(config.watchers .filter(({ files, enabled = true }) => { if (!enabled) { (0, utils_1.logMessage)(`πŸ§ͺ Watcher for files '${(0, utils_1.chalkFiles)(files)}' is disabled. Skipping...`); } return enabled; }) .map((watcherConfig) => { return new Promise((resolve, reject) => { const ignored = [...watcherConfig.ignored, ...config.defaultIgnoredFiles].map((file) => path_1.default.join(process.cwd(), file)); const watcher = chokidar.watch(watcherConfig.files.map((file) => path_1.default.join(process.cwd(), file)), { ignoreInitial: true, ignored, }); const onFileChange = async (filePath, action) => { const changedFilePath = path_1.default.relative(process.cwd(), filePath); (0, utils_1.logMessage)(`πŸ”€ File '${chalk_1.default.blue(changedFilePath)}' has been ${chalk_1.default.cyanBright(action)}`); // Check if shadowdog is paused if (getIsPaused()) { (0, utils_1.logMessage)(`⏸️ File change ignored due to pause: '${chalk_1.default.blue(changedFilePath)}'`); // Emit changed event for plugins to track while paused const eventType = action === 'added' ? 'add' : action === 'changed' ? 'change' : 'unlink'; eventEmitter.emit('changed', { path: changedFilePath, type: eventType }); return; } killPendingTasks(); await Promise.all(watcherConfig.commands.map(async (commandConfig) => { eventEmitter.emit('begin', { artifacts: commandConfig.artifacts, }); try { await executeTask(config, { command: commandConfig.command, workingDirectory: commandConfig.workingDirectory, files: watcherConfig.files, environment: watcherConfig.environment, }, watcherConfig, eventEmitter, { changedFilePath, artifacts: commandConfig.artifacts, onSpawn: (task) => { pendingTasks.push(task); }, onExit: (task) => { pendingTasks.splice(pendingTasks.findIndex((pendingTask) => pendingTask.pid === task.pid), 1); }, }); eventEmitter.emit('end', { artifacts: commandConfig.artifacts, }); } catch (error) { eventEmitter.emit('error', { artifacts: commandConfig.artifacts, errorMessage: error.message, }); } })); }; const onReady = () => { (0, utils_1.logMessage)(`πŸ” Files ${(0, utils_1.chalkFiles)(watcherConfig.files)} are watching ${chalk_1.default.cyan(Object.keys(watcher.getWatched()).length)} folders.`); resolve(watcher); }; watcher.on('add', (0, debounce_1.default)((filePath) => onFileChange(filePath, 'added'), config.debounceTime)); watcher.on('change', (0, debounce_1.default)((filePath) => onFileChange(filePath, 'changed'), config.debounceTime)); watcher.on('unlink', (0, debounce_1.default)((filePath) => onFileChange(filePath, 'deleted'), config.debounceTime)); watcher.on('ready', onReady); watcher.on('error', reject); }); })); }; const runDaemon = async (config, configFilePath, eventEmitter) => { let currentConfig = config; let isPaused = false; let pendingTasks = []; const killPendingTasks = () => { pendingTasks.forEach((task) => { try { if (task.pid) { process.kill(-task.pid, 'SIGKILL'); (0, utils_1.logMessage)(`πŸ’€ Command (PID: ${chalk_1.default.magenta(task.pid)}) was killed because another task was started`); } } catch { (0, utils_1.logMessage)(`πŸ’€ Command (PID: ${chalk_1.default.magenta(task.pid)}) Unable to kill process.`); } }); pendingTasks = []; }; let currentWatchers = await setupWatchers(currentConfig, eventEmitter, () => isPaused, pendingTasks, killPendingTasks); const configWatcher = chokidar.watch(configFilePath, { ignoreInitial: true, }); configWatcher.on('change', (0, debounce_1.default)(async () => { (0, utils_1.logMessage)(`πŸ”ƒ Configuration file has been changed. Restarting Shadowdog...`); try { currentConfig = (0, config_1.loadConfig)(configFilePath); // Emit config loaded event for plugins that need to update eventEmitter.emit('configLoaded', { config: currentConfig }); await Promise.all(currentWatchers.map((watcher) => watcher.close())); currentWatchers = await setupWatchers(currentConfig, eventEmitter, () => isPaused, pendingTasks, killPendingTasks); (0, utils_1.logMessage)(`πŸ• Shadowdog has been restarted successfully.`); } catch (error) { (0, utils_1.logMessage)(`🚨 Error while restarting Shadowdog: ${error.message}`); } }, currentConfig.debounceTime)); // Handle pause/resume events eventEmitter.on('pause', () => { isPaused = true; (0, utils_1.logMessage)(`⏸️ Shadowdog has been paused via MCP`); }); eventEmitter.on('resume', () => { isPaused = false; (0, utils_1.logMessage)(`▢️ Shadowdog has been resumed via MCP`); }); // Handle artifact computation requests eventEmitter.on('computeArtifact', async ({ artifactOutput }) => { if (isPaused) { (0, utils_1.logMessage)(`⏸️ ${chalk_1.default.yellow('Artifact computation skipped due to pause:')} ${artifactOutput}`); return; } (0, utils_1.logMessage)(`πŸ”¨ Computing artifact via MCP: ${chalk_1.default.blue(artifactOutput)}`); // Kill any pending tasks before starting new computation killPendingTasks(); // Find the command configuration for this artifact const commandInfo = findCommandForArtifact(currentConfig, artifactOutput); if (!commandInfo) { (0, utils_1.logMessage)(`❌ ${chalk_1.default.red('No command found for artifact:')} ${artifactOutput}`); return; } try { await executeTask(currentConfig, commandInfo.commandConfig, commandInfo.watcherConfig, eventEmitter, { artifacts: [{ output: artifactOutput }], onSpawn: () => { }, // No need to track for MCP requests onExit: () => { }, // No need to track for MCP requests }); (0, utils_1.logMessage)(`βœ… Artifact computed successfully: ${chalk_1.default.blue(artifactOutput)}`); } catch (error) { (0, utils_1.logMessage)(`❌ Failed to compute artifact: ${error.message}`); } }); // Handle compute all artifacts requests eventEmitter.on('computeAllArtifacts', async ({ artifacts }) => { if (isPaused) { (0, utils_1.logMessage)(`⏸️ ${chalk_1.default.yellow('All artifacts computation skipped due to pause')}`); return; } (0, utils_1.logMessage)(`πŸ”¨ Computing all artifacts via MCP: ${chalk_1.default.blue(artifacts.length)} artifacts`); // Kill any pending tasks before starting new computation killPendingTasks(); // Process each artifact for (const artifact of artifacts) { const commandInfo = findCommandForArtifact(currentConfig, artifact.output); if (!commandInfo) { (0, utils_1.logMessage)(`❌ ${chalk_1.default.red('No command found for artifact:')} ${artifact.output}`); continue; } try { (0, utils_1.logMessage)(`πŸ”¨ Computing artifact: ${chalk_1.default.blue(artifact.output)}`); await executeTask(currentConfig, commandInfo.commandConfig, commandInfo.watcherConfig, eventEmitter, { artifacts: [{ output: artifact.output }], onSpawn: () => { }, // No need to track for MCP requests onExit: () => { }, // No need to track for MCP requests }); (0, utils_1.logMessage)(`βœ… Artifact computed successfully: ${chalk_1.default.blue(artifact.output)}`); } catch (error) { (0, utils_1.logMessage)(`❌ Failed to compute artifact ${artifact.output}: ${error.message}`); } } (0, utils_1.logMessage)(`βœ… All artifacts computation completed`); }); (0, utils_1.logMessage)(`πŸš€ Shadowdog ${chalk_1.default.blue((0, utils_1.readShadowdogVersion)())} is ready to watch your files!`); eventEmitter.emit('initialized'); let isShuttingDown = false; const shutdown = async () => { if (isShuttingDown) return; isShuttingDown = true; return (0, utils_1.exit)(eventEmitter, 0); }; process.on('beforeExit', shutdown); process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); }; exports.runDaemon = runDaemon;