| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158 |
- import fs from 'fs';
- import fsPromises from 'fs/promises';
- import path from 'path';
- import {
- configToYaml,
- getPilotDeckConfigPath,
- maskSecrets,
- rawYamlToMaskedString,
- readPilotDeckConfigFile,
- validatePilotDeckConfig,
- } from './pilotdeckConfig.js';
- import { reloadPilotDeckConfig } from './pilotdeckConfigReloader.js';
- // Watches ~/.pilotdeck/pilotdeck.yaml for external edits (vim, Cursor, other IDEs)
- // and triggers the same reload path the UI uses on save, so *any* edit takes
- // effect live. When the UI itself writes the file it calls
- // suppressNextWatchEvent() first to avoid a redundant second reload.
- let watcher = null;
- let debounceTimer = null;
- let suppressCount = 0;
- let lastSignature = '';
- let onEventHandler = null;
- function signatureForFile(filePath) {
- try {
- const stat = fs.statSync(filePath);
- return `${stat.size}:${stat.mtimeMs}`;
- } catch {
- return 'missing';
- }
- }
- export function suppressNextWatchEvent() {
- suppressCount += 1;
- setTimeout(() => {
- suppressCount = Math.max(0, suppressCount - 1);
- }, 1500);
- }
- async function handleChange(configPath) {
- if (suppressCount > 0) return;
- const signature = signatureForFile(configPath);
- if (signature === lastSignature) return;
- lastSignature = signature;
- let record;
- try {
- record = readPilotDeckConfigFile();
- } catch (error) {
- onEventHandler?.({
- source: 'watcher',
- path: configPath,
- error: error instanceof Error ? error.message : String(error),
- validation: {
- valid: false,
- errors: [error instanceof Error ? error.message : String(error)],
- warnings: [],
- },
- timestamp: new Date().toISOString(),
- });
- return;
- }
- const validation = validatePilotDeckConfig(record.config);
- // Mirror serializeConfigResponse: emit the masked disk YAML so the
- // UI's hot-reload sees full router/gateway/adapters/etc. segments
- // when the file changes from any source (UI save, vim, external tool).
- const hasDiskYaml = record.rawYaml && typeof record.rawYaml === 'object' && Object.keys(record.rawYaml).length > 0;
- const maskedRaw = hasDiskYaml ? rawYamlToMaskedString(record.rawYaml) : configToYaml(maskSecrets(record.config));
- if (!validation.valid) {
- onEventHandler?.({
- source: 'watcher',
- path: record.configPath,
- raw: maskedRaw,
- validation: { valid: false, errors: validation.errors, warnings: validation.warnings },
- reload: null,
- timestamp: new Date().toISOString(),
- });
- return;
- }
- let reloadResult = null;
- try {
- reloadResult = await reloadPilotDeckConfig(record.config);
- } catch (error) {
- onEventHandler?.({
- source: 'watcher',
- path: record.configPath,
- raw: maskedRaw,
- validation: { valid: true, errors: [], warnings: validation.warnings },
- reload: null,
- error: error instanceof Error ? error.message : String(error),
- timestamp: new Date().toISOString(),
- });
- return;
- }
- onEventHandler?.({
- source: 'watcher',
- path: record.configPath,
- raw: maskedRaw,
- validation: { valid: true, errors: [], warnings: validation.warnings },
- reload: reloadResult,
- timestamp: new Date().toISOString(),
- });
- }
- export async function startPilotDeckConfigWatcher({ onEvent } = {}) {
- stopPilotDeckConfigWatcher();
- onEventHandler = typeof onEvent === 'function' ? onEvent : null;
- const configPath = getPilotDeckConfigPath();
- const configDir = path.dirname(configPath);
- const configBase = path.basename(configPath);
- try {
- await fsPromises.mkdir(configDir, { recursive: true });
- } catch (error) {
- console.warn('[pilotdeck-config-watcher] failed to ensure config dir:', error?.message || error);
- return;
- }
- lastSignature = signatureForFile(configPath);
- try {
- watcher = fs.watch(configDir, { persistent: false }, (eventType, filename) => {
- if (filename && filename !== configBase) return;
- if (debounceTimer) clearTimeout(debounceTimer);
- debounceTimer = setTimeout(() => {
- debounceTimer = null;
- void handleChange(configPath);
- }, 250);
- });
- watcher.on('error', (error) => {
- console.warn('[pilotdeck-config-watcher] watch error:', error?.message || error);
- });
- console.log(`[pilotdeck-config-watcher] watching ${configPath}`);
- } catch (error) {
- console.warn('[pilotdeck-config-watcher] failed to start:', error?.message || error);
- }
- }
- export function stopPilotDeckConfigWatcher() {
- if (watcher) {
- try {
- watcher.close();
- } catch {
- // noop
- }
- watcher = null;
- }
- if (debounceTimer) {
- clearTimeout(debounceTimer);
- debounceTimer = null;
- }
- }
|