Ctrl K
ComunidadGuías…

install/windows-script-host.mjs

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
import { spawn, spawnSync } from "node:child_process";
import { dirname, isAbsolute, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { randomBytes } from "node:crypto";

const installRoot = realpathSync.native(dirname(fileURLToPath(import.meta.url)));
const envPath = join(installRoot, ".env");
const args = process.argv.slice(2);
const allowed = new Set(["--open", "--stop"]);
if (args.some((arg) => !allowed.has(arg))) {
  console.error("Usage: windows-script-host.mjs [--open|--stop]");
  process.exitCode = 2;
  process.exit();
}
const wantsOpen = args.includes("--open");
const wantsStop = args.includes("--stop");
let stopping = false;

function parseEnv(text) {
  const result = {};
  for (const raw of text.split(/\r?\n/u)) {
    const line = raw.replace(/^\uFEFF/u, "").trim();
    if (!line || line.startsWith("#")) continue;
    const at = line.indexOf("=");
    if (at <= 0) continue;
    let value = line.slice(at + 1).trim();
    if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
    result[line.slice(0, at).trim()] = value;
  }
  return result;
}
const config = existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {};
for (const [key, value] of Object.entries(config)) process.env[key] = value;
process.env.NODE_ENV = "production";

const dataDir = resolve(config.CHAT_DATA_DIR || config.METIS_DATA_DIR || join(installRoot, "data"));
mkdirSync(dataDir, { recursive: true });
const paths = {
  lock: join(dataDir, ".windows-script-host.lock"),
  stop: join(dataDir, ".windows-script-host.stop"),
  hostLog: join(dataDir, "host.log"),
};
const token = randomBytes(24).toString("hex");

function log(line) {
  appendFileSync(paths.hostLog, new Date().toISOString() + " " + line + "\n");
}
function windowsHidden(options = {}) {
  return { windowsHide: true, stdio: options.stdio || "ignore", cwd: installRoot, ...options };
}
function runHidden(command, commandArgs, options = {}) {
  return spawnSync(command, commandArgs, { ...windowsHidden(options), encoding: "utf8", timeout: options.timeout });
}
function processExists(pid) {
  if (!Number.isInteger(pid) || pid <= 0) return false;
  const result = runHidden("tasklist.exe", ["/FI", "PID eq " + pid, "/FO", "CSV", "/NH"], { stdio: ["ignore", "pipe", "ignore"] });
  return result.status === 0 && new RegExp('"[^"]+","\\s*' + pid + '","', "u").test(result.stdout || "");
}
function atomicWrite(file, value) {
  const temp = file + "." + process.pid + "." + Date.now() + ".tmp";
  writeFileSync(temp, value, { encoding: "utf8", mode: 0o600 });
  renameSync(temp, file);
}
function readLock() {
  try { return JSON.parse(readFileSync(paths.lock, "utf8")); } catch { return null; }
}
function staleLock() {
  const current = readLock();
  if (!current) {
    try { return Date.now() - statSync(paths.lock).mtimeMs > 15000; } catch { return false; }
  }
  return !processExists(Number(current.pid));
}
function acquireLock() {
  for (let attempt = 0; attempt < 3; attempt += 1) {
    try {
      const fd = openSync(paths.lock, "wx", 0o600);
      try { writeFileSync(fd, JSON.stringify({ pid: process.pid, root: installRoot, token, heartbeat: Date.now() }) + "\n"); } finally { closeSync(fd); }
      return true;
    } catch (error) {
      if (error.code !== "EEXIST" || !staleLock()) return false;
      try { renameSync(paths.lock, paths.lock + ".stale." + Date.now()); } catch {}
    }
  }
  return false;
}
function releaseLock() {
  const current = readLock();
  if (current?.pid === process.pid && current.token === token) rmSync(paths.lock, { force: true });
}
function requestStop() {
  const current = readLock();
  if (!current || current.root !== installRoot || !processExists(Number(current.pid))) return;
  atomicWrite(paths.stop, JSON.stringify({ root: installRoot, pid: current.pid, token: current.token, requestedAt: Date.now() }) + "\n");
  for (let i = 0; i < 75; i += 1) {
    if (!existsSync(paths.lock)) return;
    const latest = readLock();
    if (!latest || latest.root !== installRoot) return;
    Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
  }
  throw new Error("Metis background host did not stop within 15 seconds.");
}
function removeOwnStop() { try { rmSync(paths.stop, { force: true }); } catch {} }
function appendChildLog(name, chunk) {
  if (chunk) appendFileSync(join(dataDir, name + ".log"), chunk);
}
function childCommand(name) {
  const node = config.METIS_NODE_BIN || "node.exe";
  const rootPath = (value) => isAbsolute(value) ? value : join(installRoot, value);
  if (name === "app") return { command: node, args: ["--import", "tsx", rootPath("server.mjs")] };
  if (name === "worker") return { command: node, args: ["--import", "tsx", rootPath("worker.ts")] };
  return { command: node, args: [rootPath("lib/mcp-core/gateway-core.mjs")] };
}
function startChild(name) {
  const command = childCommand(name);
  const child = spawn(command.command, command.args, {
    cwd: installRoot, env: { ...process.env, NODE_ENV: "production" }, windowsHide: true, stdio: ["ignore", "pipe", "pipe"],
  });
  child.stdout.on("data", (chunk) => appendChildLog(name, chunk));
  child.stderr.on("data", (chunk) => appendChildLog(name, chunk));
  child.on("error", (error) => log(name + " spawn error: " + error.message));
  return child;
}
function stopChild(child, name) {
  if (!child?.pid || child.exitCode !== null || child.signalCode !== null) return;
  log("stopping " + name + " tree pid=" + child.pid);
  runHidden("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { timeout: 10000 });
}
function healthyUrl() { return config.AI_CHAT_PUBLIC_URL || "http://127.0.0.1:" + (config.PORT || "3100"); }
async function isHealthy() {
  try {
    const response = await fetch("http://127.0.0.1:" + (config.PORT || "3100") + "/api/status", { signal: AbortSignal.timeout(3000) });
    const status = await response.json();
    return response.ok && typeof status.authenticated === "boolean" && status.worker?.ok === true && status.mcp?.ok === true;
  } catch { return false; }
}
async function openBrowser() {
  const url = healthyUrl();
  for (let i = 0; i < 60; i += 1) {
    if (stopping) return;
    if (await isHealthy()) {
      runHidden("powershell.exe", ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command",
        "Start-Process -FilePath '" + url.replaceAll("'", "''") + "'"], { timeout: 5000 });
      return;
    }
    await new Promise((resolveSleep) => setTimeout(resolveSleep, 1000));
  }
  throw new Error("Metis did not become ready at " + url + ". Check " + dataDir + " for startup errors.");
}
function showError(error) {
  log(error.stack || String(error));
  if (!wantsOpen) return;
  const message = String(error.message || error).replaceAll("'", "''");
  runHidden("powershell.exe", ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command",
    "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('" + message + "','Metis AI','OK','Error') | Out-Null"], { timeout: 10000 });
}
function dockerEnabled() { return config.METIS_DOCKER === "1" || config.METIS_DOCKER === "true"; }
function dockerStart() {
  const result = runHidden("docker.exe", ["compose", "--env-file", envPath, "up", "-d", "--remove-orphans"],
    { stdio: ["ignore", "pipe", "pipe"], timeout: 120000 });
  appendChildLog("host", result.stdout || "");
  appendChildLog("host", result.stderr || "");
  if (result.status !== 0) throw new Error("Docker Compose failed. Check host.log.");
  log("docker compose started; stop leaves the compose stack running.");
}

async function main() {
  if (wantsStop) { requestStop(); return; }
  if (!acquireLock()) {
    if (wantsOpen) await openBrowser();
    return;
  }
  removeOwnStop();
  const children = [];
  const docker = dockerEnabled();
  try {
    if (config.PORT && config.MCP_PORT && config.PORT === config.MCP_PORT) throw new Error("PORT and MCP_PORT must be different.");
    if (!docker) {
      for (const portName of ["PORT", "MCP_PORT"]) {
        const port = Number(config[portName] || (portName === "PORT" ? 3100 : 8787));
        const probe = runHidden("powershell.exe", ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command",
          "$l=New-Object Net.Sockets.TcpListener([Net.IPAddress]::Any," + port + "); try{$l.Start();$l.Stop();exit 0}catch{exit 1}"], { timeout: 5000 });
        if (probe.status !== 0) throw new Error("Port " + port + " (" + portName + ") is occupied or unavailable.");
      }
      for (const name of ["app", "worker", "mcp"]) children.push({ name, child: startChild(name) });
    } else dockerStart();
    let openPromise;
    if (wantsOpen) { openPromise = openBrowser().catch(showError); }
    while (true) {
      const stopRequest = readLock();
      let command = null;
      try { command = JSON.parse(readFileSync(paths.stop, "utf8")); } catch {}
      if (command && command.root === installRoot && command.token === token && stopRequest?.token === token) break;
      for (const entry of children) {
        if (entry.child.exitCode !== null) {
          log(entry.name + " exited code=" + entry.child.exitCode + "; restarting");
          entry.child = startChild(entry.name);
        }
      }
      atomicWrite(paths.lock, JSON.stringify({ pid: process.pid, root: installRoot, token, heartbeat: Date.now() }) + "\n");
      await new Promise((resolveSleep) => setTimeout(resolveSleep, 500));
    }

  } finally {
    stopping = true;
    for (const entry of children) stopChild(entry.child, entry.name);
    removeOwnStop();
    releaseLock();
  }
}
main().catch((error) => { showError(error); process.exitCode = 1; });