/** * CLI Executor for Obsidian commands * Constitutional Principle IV: Defensive Programming * Uses spawn for better streaming and timeout control */ import { spawn } from 'child_process'; import { CLICommand, CLIResult } from '../utils/types.js'; import { logger } from '../utils/logger.js'; import { getCommandTimeout } from '../config/timeouts.js'; /** * Execute an Obsidian CLI command with timeout */ export async function executeCommand(cmd: CLICommand): Promise { const timeout = cmd.timeout || getCommandTimeout(cmd.command); logger.debug('Executing CLI command', { command: cmd.command, argCount: cmd.args.length, timeout, }); return new Promise((resolve) => { const child = spawn(cmd.command, cmd.args, { cwd: cmd.cwd || process.cwd(), shell: true, }); let stdout = ''; let stderr = ''; let timedOut = false; // Set timeout const timeoutId = setTimeout(() => { timedOut = true; child.kill('SIGTERM'); logger.warn('CLI command timed out', { command: cmd.command, timeout, }); }, timeout); // Collect stdout child.stdout?.on('data', (data) => { stdout += data.toString(); }); // Collect stderr child.stderr?.on('data', (data) => { stderr += data.toString(); }); // Handle completion child.on('close', (code) => { clearTimeout(timeoutId); const result: CLIResult = { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code || 0, timedOut, }; if (result.exitCode === 0) { logger.debug('CLI command succeeded', { command: cmd.command, outputLength: result.stdout.length, }); } else { logger.warn('CLI command failed', { command: cmd.command, exitCode: result.exitCode, timedOut: result.timedOut, }); } resolve(result); }); // Handle spawn errors child.on('error', (error) => { clearTimeout(timeoutId); logger.error('CLI command spawn error', { error: error.message }); resolve({ stdout: '', stderr: error.message, exitCode: 1, timedOut: false, }); }); }); } /** * Execute Obsidian CLI command with vault context */ export async function executeObsidianCommand( subcommand: string, args: string[] = [], options?: { timeout?: number } ): Promise { const vaultName = process.env.OBSIDIAN_VAULT; if (!vaultName) { throw new Error('OBSIDIAN_VAULT environment variable not set'); } // Build full command: obsidian --vault const fullArgs = [subcommand, '--vault', vaultName, ...args]; return executeCommand({ command: '/Applications/Obsidian.app/Contents/MacOS/obsidian', args: fullArgs, timeout: options?.timeout, }); }