@steipete/terminator-mcp
Version:
MCP plugin to manage macOS terminal sessions.
149 lines • 7.72 kB
JavaScript
// Handles the direct invocation and process management of the Swift CLI 'terminator' binary.
// Includes logic for spawning the process, handling stdout/stderr, cancellation, and timeouts.
import { spawn } from 'node:child_process';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { debugLog } from './config.js'; // For logging
import * as fs from 'node:fs'; // Import fs
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export const SWIFT_CLI_NAME = 'terminator';
export const SWIFT_CLI_PATH = path.resolve(__dirname, '..', 'bin', SWIFT_CLI_NAME);
export function invokeSwiftCLI(cliArgs, terminatorEnv, mcpContext, wrapperTimeoutMs) {
debugLog(`Invoking Swift CLI: ${SWIFT_CLI_PATH} ${cliArgs.join(' ')} with env:`, terminatorEnv);
let swiftProcess = null;
let internalTimeoutId = null;
let mcpCancellationListener = null;
const executionPromise = new Promise((resolve) => {
debugLog(`[invokeSwiftCLI] About to spawn. Resolved SWIFT_CLI_PATH: ${SWIFT_CLI_PATH}`);
debugLog(`[invokeSwiftCLI] Does SWIFT_CLI_PATH exist according to fs.existsSync? ${fs.existsSync(SWIFT_CLI_PATH)}`);
swiftProcess = spawn(SWIFT_CLI_PATH, cliArgs, {
env: { ...process.env, ...terminatorEnv },
cwd: path.resolve(__dirname, '..') // Set CWD to project root
});
let stdoutData = '';
let stderrData = '';
let mcpCancelled = false;
let internalTimeoutHit = false;
mcpCancellationListener = () => {
if (mcpCancelled)
return;
debugLog('MCP Host signalled cancellation.');
mcpCancelled = true;
if (internalTimeoutId)
clearTimeout(internalTimeoutId);
if (swiftProcess && !swiftProcess.killed) {
debugLog('Attempting to SIGKILL Swift CLI process due to MCP cancellation.');
swiftProcess.kill('SIGKILL');
}
// Resolve directly, on('close') might not fire or might be delayed
resolve({ stdout: stdoutData, stderr: stderrData, exitCode: null, cancelled: true });
};
if (mcpContext.signal) {
if (mcpContext.signal.aborted) {
mcpCancellationListener(); // Call immediately if already aborted
return;
}
mcpContext.signal.addEventListener('abort', mcpCancellationListener);
}
swiftProcess.stdout?.on('data', (data) => {
stdoutData += data.toString();
debugLog('Swift CLI stdout:', data.toString().trim());
});
swiftProcess.stderr?.on('data', (data) => {
stderrData += data.toString();
debugLog('Swift CLI stderr:', data.toString().trim());
});
swiftProcess.on('error', (err) => {
if (mcpCancelled || internalTimeoutHit)
return;
if (internalTimeoutId)
clearTimeout(internalTimeoutId);
debugLog('Failed to start Swift CLI.', err);
resolve({ stdout: stdoutData, stderr: stderrData, exitCode: null, cancelled: false, internalTimeoutHit });
});
swiftProcess.on('close', (code) => {
if (mcpCancelled || internalTimeoutHit)
return;
if (internalTimeoutId)
clearTimeout(internalTimeoutId);
debugLog(`Swift CLI exited with code ${code}.`);
let processedStdout = stdoutData;
// Check if this was a command expected to produce JSON
// A more robust check might involve inspecting cliArgs more deeply or passing a flag
if (cliArgs.includes('info') && cliArgs.includes('--json')) {
const jsonStartIndex = stdoutData.indexOf('{');
if (jsonStartIndex !== -1) {
processedStdout = stdoutData.substring(jsonStartIndex);
debugLog('Extracted JSON from Swift CLI stdout:', processedStdout.trim());
}
else {
debugLog("Could not find start of JSON ('{') in info command stdout. Using raw.");
}
}
else if (cliArgs.includes('list') && cliArgs.includes('--json')) {
const jsonStartIndex = stdoutData.indexOf('['); // list command outputs a JSON array
if (jsonStartIndex !== -1) {
processedStdout = stdoutData.substring(jsonStartIndex);
debugLog('Extracted JSON array from Swift CLI stdout:', processedStdout.trim());
}
else {
debugLog("Could not find start of JSON array ('[') in list command stdout. Using raw.");
}
}
resolve({ stdout: processedStdout, stderr: stderrData, exitCode: code, cancelled: false, internalTimeoutHit });
});
// Internal timeout for the Swift CLI process itself
internalTimeoutId = setTimeout(() => {
if (mcpCancelled || internalTimeoutHit)
return;
internalTimeoutHit = true;
if (swiftProcess && !swiftProcess.killed) {
debugLog(`Swift CLI process exceeded internal wrapper timeout of ${wrapperTimeoutMs}ms. Killing.`);
swiftProcess.kill('SIGKILL');
}
// The 'close' event should eventually fire and resolve the promise.
// To be safe, if close doesn't fire after a short delay, resolve with timeout status.
// This secondary timeout is to prevent hangs if SIGKILL + close event fails to trigger 'close' promptly.
setTimeout(() => {
if (!mcpCancelled) { // Check again in case MCP cancellation happened during this small delay
// Process stdout for JSON extraction here as well, in case of timeout before 'close'
let processedStdoutOnTimeout = stdoutData;
if (cliArgs.includes('info') && cliArgs.includes('--json')) {
const jsonStartIndex = stdoutData.indexOf('{');
if (jsonStartIndex !== -1) {
processedStdoutOnTimeout = stdoutData.substring(jsonStartIndex);
}
}
else if (cliArgs.includes('list') && cliArgs.includes('--json')) {
const jsonStartIndex = stdoutData.indexOf('[');
if (jsonStartIndex !== -1) {
processedStdoutOnTimeout = stdoutData.substring(jsonStartIndex);
}
}
resolve({
stdout: processedStdoutOnTimeout,
stderr: stderrData,
exitCode: null, // Or last known code if any
cancelled: false,
internalTimeoutHit: true
});
}
}, 1000); // Give 1 sec for SIGKILL to result in a 'close' event
}, wrapperTimeoutMs);
});
return executionPromise.finally(() => {
if (mcpContext.signal && mcpCancellationListener) {
try {
mcpContext.signal.removeEventListener('abort', mcpCancellationListener);
}
catch (e) {
debugLog("Minor error removing abort listener, possibly due to it not being standard on this Node version's AbortSignal.");
}
}
if (internalTimeoutId) {
clearTimeout(internalTimeoutId);
}
});
}
//# sourceMappingURL=swift-cli.js.map