@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
JavaScript
;
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;