@skyramp/mcp
Version:
Skyramp MCP (Model Context Protocol) Server - AI-powered test generation and execution
440 lines (439 loc) • 17.8 kB
JavaScript
import { spawn, exec } from "child_process";
import path from "path";
import fs from "fs";
import os from "os";
import { promisify } from "util";
import { logger } from "./logger.js";
const execAsync = promisify(exec);
// Mapping of environment variables
const shellEnvVariables = {
http_proxy: "http://localhost:8888",
HTTPS_PROXY: "http://localhost:8888",
HTTP_PROXY: "http://localhost:8888",
https_proxy: "http://localhost:8888",
no_proxy: "",
NO_PROXY: "",
};
let terminalProcess;
let terminalPID;
function getTerminalConfig() {
const platform = os.platform();
switch (platform) {
case "win32":
return {
command: "cmd",
args: ["/c", "start", "cmd", "/k"],
scriptExtension: ".bat",
scriptHeader: "@echo off",
pidCommand: "echo %$",
};
case "darwin":
return {
command: "open",
args: ["-a", "Terminal"],
scriptExtension: ".sh",
scriptHeader: "#!/bin/bash",
pidCommand: "echo $$",
};
case "linux":
default:
// Try common Linux terminals in order of preference
const linuxTerminals = [
"gnome-terminal",
"konsole",
"xfce4-terminal",
"xterm",
"terminator",
"alacritty",
];
// For now, default to gnome-terminal, but this could be enhanced
// to detect which terminal is available
return {
command: "gnome-terminal",
args: ["--"],
scriptExtension: ".sh",
scriptHeader: "#!/bin/bash",
pidCommand: "echo $$",
};
}
}
function createEnvironmentScript(config) {
const tempDir = os.tmpdir();
const scriptPath = path.join(tempDir, `skyramp-launch${config.scriptExtension}`);
const pidFile = path.join(tempDir, "skyramp-terminal.pid");
const infoMessage = os.platform() === "win32"
? "A new terminal has been spawned here by Skyramp to enable trace collection.\\nAsk skyramp agent to stop trace to terminate trace collection and return to where you were.\\nFor more information, visit skyramp.dev/docs."
: "\\033[33mA new terminal has been spawned here by Skyramp to enable trace collection.\\nAsk skyramp agent to stop trace to terminate trace collection and return to where you were.\\nFor more information, visit skyramp.dev/docs.\\033[0m";
let scriptContent;
if (os.platform() === "win32") {
// Windows batch script
const setStatements = Object.entries(shellEnvVariables)
.map(([key, value]) => `set ${key}=${value}`)
.join("\n");
scriptContent = `${config.scriptHeader}
${setStatements}
echo %^%^% > "${pidFile}"
echo ${infoMessage}
cmd /k`;
}
else {
// Unix-like script (macOS/Linux)
const exportStatements = Object.entries(shellEnvVariables)
.map(([key, value]) => `export ${key}="${value}"`)
.join("\n");
scriptContent = `${config.scriptHeader}
${exportStatements}
${config.pidCommand} > "${pidFile}"
echo -e "${infoMessage}"
exec $SHELL`;
}
try {
// Remove existing script if it exists
if (fs.existsSync(scriptPath)) {
try {
fs.unlinkSync(scriptPath);
logger.debug("Removed existing script file", { scriptPath });
}
catch (removeError) {
logger.warning("Could not remove existing script file", {
scriptPath,
error: removeError instanceof Error
? removeError.message
: String(removeError),
});
}
}
// Create the script
fs.writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
// Verify the script was created successfully
if (!fs.existsSync(scriptPath)) {
throw new Error("Script file was not created successfully");
}
// Check if the script is readable and executable
try {
fs.accessSync(scriptPath, fs.constants.R_OK | fs.constants.X_OK);
}
catch (accessError) {
logger.warning("Script may not have correct permissions", {
scriptPath,
error: accessError instanceof Error
? accessError.message
: String(accessError),
});
}
logger.debug("Cross-platform script created and verified", {
platform: os.platform(),
scriptPath,
scriptSize: fs.statSync(scriptPath).size,
});
return scriptPath;
}
catch (error) {
logger.error("Failed to create terminal script", {
platform: os.platform(),
scriptPath,
tempDir,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
async function findAvailableTerminal() {
if (os.platform() !== "linux") {
return null; // Use default for non-Linux platforms
}
const terminals = [
"gnome-terminal",
"konsole",
"xfce4-terminal",
"xterm",
"terminator",
"alacritty",
"kitty",
];
for (const terminal of terminals) {
try {
await execAsync(`which ${terminal}`);
logger.debug("Found available terminal", { terminal });
return terminal;
}
catch {
// Terminal not found, try next
}
}
logger.warning("No common Linux terminal found, falling back to xterm");
return "xterm";
}
export default async function openProxyTerminal() {
logger.info("Starting cross-platform proxy terminal creation", {
platform: os.platform(),
arch: os.arch(),
});
try {
const config = getTerminalConfig();
// For Linux, try to find an available terminal
if (os.platform() === "linux") {
const availableTerminal = await findAvailableTerminal();
if (availableTerminal && availableTerminal !== "gnome-terminal") {
config.command = availableTerminal;
// Adjust args based on terminal
if (availableTerminal === "xterm") {
config.args = ["-e"];
}
else if (availableTerminal === "konsole") {
config.args = ["-e"];
}
}
}
const scriptPath = createEnvironmentScript(config);
const pidFile = path.join(os.tmpdir(), "skyramp-terminal.pid");
// Verify script exists before launching terminal
if (!fs.existsSync(scriptPath)) {
throw new Error(`Script file does not exist: ${scriptPath}`);
}
// Verify script is accessible
try {
fs.accessSync(scriptPath, fs.constants.R_OK | fs.constants.X_OK);
}
catch (accessError) {
throw new Error(`Script file is not accessible: ${scriptPath} - ${accessError instanceof Error
? accessError.message
: String(accessError)}`);
}
logger.debug("Script verified before terminal launch", {
scriptPath,
scriptSize: fs.statSync(scriptPath).size,
});
// Launch terminal with script
const spawnArgs = [...config.args, scriptPath];
terminalProcess = spawn(config.command, spawnArgs, {
env: {
...process.env,
...shellEnvVariables,
},
detached: true,
stdio: "ignore",
});
logger.debug("Spawned cross-platform terminal", {
platform: os.platform(),
command: config.command,
args: spawnArgs,
pid: terminalProcess.pid,
});
// Handle process events
terminalProcess.on("exit", (code) => {
logger.info("Terminal launch command completed", {
platform: os.platform(),
exitCode: code,
});
// Wait for terminal to write PID and execute script
// On macOS, Terminal.app takes longer to start and execute the script
const cleanupDelay = os.platform() === "darwin" ? 10000 : 3000;
setTimeout(() => {
try {
if (fs.existsSync(pidFile)) {
const pidContent = fs.readFileSync(pidFile, "utf8").trim();
const pid = parseInt(pidContent);
if (!isNaN(pid)) {
terminalPID = pid;
logger.info("Terminal PID captured", { terminalPID: pid });
}
// Cleanup PID file
try {
fs.unlinkSync(pidFile);
}
catch (cleanupError) {
logger.debug("PID file cleanup completed with minor issues", {
error: cleanupError instanceof Error
? cleanupError.message
: String(cleanupError),
});
}
}
// Cleanup script file with additional delay and verification
setTimeout(() => {
try {
if (fs.existsSync(scriptPath)) {
fs.unlinkSync(scriptPath);
logger.debug("Script file cleaned up", { scriptPath });
}
}
catch (scriptCleanupError) {
logger.debug("Script cleanup completed with minor issues", {
error: scriptCleanupError instanceof Error
? scriptCleanupError.message
: String(scriptCleanupError),
});
}
}, 2000);
}
catch (err) {
logger.warning("Could not read terminal PID", {
error: err instanceof Error ? err.message : String(err),
});
// Even if PID reading fails, still cleanup script after delay
setTimeout(() => {
try {
if (fs.existsSync(scriptPath)) {
fs.unlinkSync(scriptPath);
logger.debug("Script file cleaned up (fallback)", {
scriptPath,
});
}
}
catch (scriptCleanupError) {
logger.debug("Fallback script cleanup completed with minor issues", {
error: scriptCleanupError instanceof Error
? scriptCleanupError.message
: String(scriptCleanupError),
});
}
}, 2000);
}
}, cleanupDelay);
});
terminalProcess.on("error", (error) => {
logger.error("Terminal spawn error", {
platform: os.platform(),
command: config.command,
error: error.message,
});
});
logger.info("Cross-platform proxy terminal opened successfully", {
platform: os.platform(),
arch: os.arch(),
});
}
catch (error) {
logger.error("Failed to open proxy terminal", {
platform: os.platform(),
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
export async function closeProxyTerminal() {
logger.info("Starting cross-platform proxy terminal closure", {
platform: os.platform(),
});
// Kill the spawn process if it exists
if (terminalProcess) {
const killResult = terminalProcess.kill();
logger.debug("Terminal spawn process kill result", { killResult });
}
// Handle platform-specific terminal closing
if (terminalPID) {
try {
const platform = os.platform();
if (platform === "darwin") {
// macOS: First try to kill the specific PID and its process group
try {
// Kill the process group to ensure child processes are also terminated
process.kill(-terminalPID, "SIGTERM");
logger.info("macOS terminal process group killed via SIGTERM", {
terminalPID,
});
// Give processes time to terminate gracefully
await new Promise((resolve) => setTimeout(resolve, 1000));
// Check if process still exists and force kill if needed
try {
process.kill(terminalPID, 0); // Check if process exists
process.kill(-terminalPID, "SIGKILL");
logger.info("macOS terminal process group force killed", {
terminalPID,
});
}
catch (killErr) {
// Process is already dead, which is what we want
logger.debug("Terminal process already terminated", {
terminalPID,
});
}
}
catch (pidErr) {
logger.debug("PID-based kill failed, trying AppleScript fallback", {
error: pidErr instanceof Error ? pidErr.message : String(pidErr),
});
// Fallback: Use AppleScript to close terminal windows with our proxy environment
const closeScript = `tell application "Terminal"
repeat with w in windows
repeat with t in tabs of w
try
-- Send exit command to close the shell in this tab
do script "exit" in t
end try
end repeat
end repeat
end tell`;
await execAsync(`osascript -e '${closeScript.replace(/'/g, "'\\''")}'`);
logger.info("macOS terminal closed via AppleScript fallback");
}
}
else if (platform === "win32") {
// Windows: Use taskkill with /T flag to kill process tree
await execAsync(`taskkill /PID ${terminalPID} /T /F`);
logger.info("Windows terminal and children closed via taskkill", {
terminalPID,
});
}
else {
// Linux: Send SIGTERM to process group
try {
process.kill(-terminalPID, "SIGTERM");
logger.info("Linux terminal process group closed via SIGTERM", {
terminalPID,
});
// Give processes time to terminate gracefully
await new Promise((resolve) => setTimeout(resolve, 1000));
// Check if process still exists and force kill if needed
try {
process.kill(terminalPID, 0); // Check if process exists
process.kill(-terminalPID, "SIGKILL");
logger.info("Linux terminal process group force killed", {
terminalPID,
});
}
catch (killErr) {
// Process is already dead, which is what we want
logger.debug("Terminal process already terminated", {
terminalPID,
});
}
}
catch (pidErr) {
// Fallback to regular process kill
try {
process.kill(terminalPID, "SIGTERM");
logger.info("Linux terminal closed via fallback SIGTERM", {
terminalPID,
});
}
catch (fallbackErr) {
logger.warning("Could not kill terminal process", {
terminalPID,
error: fallbackErr instanceof Error
? fallbackErr.message
: String(fallbackErr),
});
}
}
}
}
catch (err) {
logger.warning("Error during platform-specific terminal closure", {
platform: os.platform(),
terminalPID,
error: err instanceof Error ? err.message : String(err),
});
}
}
else {
logger.warning("No terminal PID available for cleanup - terminal may not have been properly tracked");
}
// Reset state
terminalProcess = undefined;
terminalPID = undefined;
logger.info("Cross-platform proxy terminal closure completed", {
platform: os.platform(),
});
}