Files
OllaHub/lib/utils/system.ts
T
Evandro Fonseca Junior f19bf10ef3 feat(mcp): enhance MCP installation with status tracking and custom configs
- Add /mcp-servers/ to .gitignore for MCP server installations
- Extend mcp_installations table with status, logs, and validation fields
- Refactor MCP install route to support custom configs, enable logs, and async installation
- Improve MCPSyncService with promise-based syncing to prevent concurrent operations

This update improves MCP management by adding robust installation tracking, custom configuration support, and better error handling for a more reliable user experience.
2025-10-28 21:59:00 -03:00

156 lines
3.9 KiB
TypeScript

/**
* Utilitários para operações de sistema
*/
import { exec, spawn } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
/**
* Verifica se um comando existe no sistema
*/
export async function commandExists(command: string): Promise<boolean> {
try {
const isWindows = process.platform === "win32";
const checkCommand = isWindows ? "where" : "which";
await execAsync(`${checkCommand} ${command}`);
return true;
} catch {
return false;
}
}
/**
* Executa um comando e retorna stdout/stderr
*/
export async function execCommand(
cmd: string,
args: string[],
options?: {
cwd?: string;
env?: Record<string, string>;
timeout?: number;
}
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
try {
const fullCommand = `${cmd} ${args.join(" ")}`;
const { stdout, stderr } = await execAsync(fullCommand, {
cwd: options?.cwd,
env: options?.env ? { ...process.env, ...options.env } : process.env,
timeout: options?.timeout || 300000, // 5 minutos default
});
return { stdout, stderr, exitCode: 0 };
} catch (error: any) {
return {
stdout: error.stdout || "",
stderr: error.stderr || error.message,
exitCode: error.code || 1,
};
}
}
/**
* Spawna um processo e captura output com progresso
*/
export async function spawnWithProgress(
cmd: string,
args: string[],
options: {
cwd?: string;
env?: Record<string, string>;
onOutput?: (data: string, isError: boolean) => void;
timeout?: number;
}
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
let stdout = "";
let stderr = "";
let hasTimedOut = false;
const childProcess = spawn(cmd, args, {
cwd: options.cwd,
env: options.env ? { ...process.env, ...options.env } : process.env,
stdio: ["pipe", "pipe", "pipe"],
});
// Timeout handler
const timeout = options.timeout || 300000; // 5 minutos default
const timeoutId = setTimeout(() => {
hasTimedOut = true;
childProcess.kill("SIGTERM");
// Force kill após 5 segundos
setTimeout(() => {
if (!childProcess.killed) {
childProcess.kill("SIGKILL");
}
}, 5000);
}, timeout);
// Capturar stdout
childProcess.stdout?.on("data", (data: Buffer) => {
const text = data.toString();
stdout += text;
if (options.onOutput) {
options.onOutput(text, false);
}
});
// Capturar stderr
childProcess.stderr?.on("data", (data: Buffer) => {
const text = data.toString();
stderr += text;
if (options.onOutput) {
options.onOutput(text, true);
}
});
// Error handler
childProcess.on("error", (error) => {
clearTimeout(timeoutId);
reject(new Error(`Failed to spawn process: ${error.message}`));
});
// Exit handler
childProcess.on("close", (code) => {
clearTimeout(timeoutId);
if (hasTimedOut) {
reject(
new Error(
`Process timed out after ${timeout}ms. stdout: ${stdout}, stderr: ${stderr}`
)
);
} else {
resolve({
exitCode: code || 0,
stdout,
stderr,
});
}
});
});
}
/**
* Sanitiza nome de pacote para evitar command injection
*/
export function sanitizePackageName(packageName: string): string {
// Remove caracteres perigosos, mantém apenas alphanumeric, @, /, -, _
return packageName.replace(/[^a-zA-Z0-9@/\-_.]/g, "");
}
/**
* Valida se um caminho está dentro de um diretório base (previne path traversal)
*/
export function isPathSafe(basePath: string, targetPath: string): boolean {
const path = require("path");
const resolvedBase = path.resolve(basePath);
const resolvedTarget = path.resolve(targetPath);
return resolvedTarget.startsWith(resolvedBase);
}