UNPKG

@sellable/install

Version:

One-command installer for Sellable MCP in Claude Code, Codex, and Hermes

134 lines (130 loc) • 4.25 kB
import { spawn } from "node:child_process"; import { fileURLToPath, pathToFileURL } from "node:url"; import { proveDefaultProfileNativeSkills } from "./fly-skills-bridge.mjs"; const WORKER = fileURLToPath(import.meta.url); const PROOF_TIMEOUT_MS = 90_000; // Native skills use synchronous Python and a profile flock. Keep both off the // lifecycle event loop so a slow native read cannot stop its lease heartbeat. export function proveDefaultProfileNativeSkillsAsync( { actions, profileId, dataRoot }, { spawnWorker = spawn, timeoutMs = PROOF_TIMEOUT_MS } = {} ) { return new Promise((resolve, reject) => { if ( !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > PROOF_TIMEOUT_MS ) { reject(new Error("skills_native_request_rejected")); return; } let child; let deadline; let failureCode; let stdoutBytes = 0; let stderrBytes = 0; const stdout = []; const stopWorkerGroup = () => { if (!child?.pid) return; try { process.kill(-child.pid, "SIGKILL"); } catch { child.kill("SIGKILL"); } }; // execFile does not forward detached to spawn. Native-proof UAT // 01a0a304 left stopped Python descendants alive after both deadlines. // Spawn the worker directly so it really owns the group we terminate. child = spawnWorker( process.execPath, [WORKER, "--prove", JSON.stringify({ actions, profileId, dataRoot })], { detached: true, stdio: ["ignore", "pipe", "pipe"], env: { PATH: process.env.PATH, HOME: dataRoot }, } ); child.stdout.on("data", (chunk) => { stdoutBytes += chunk.length; if (stdoutBytes <= 16 * 1024) stdout.push(Buffer.from(chunk)); else { failureCode ??= "skills_native_call_failed"; stopWorkerGroup(); } }); child.stderr.on("data", (chunk) => { stderrBytes += chunk.length; if (stderrBytes > 16 * 1024) { failureCode ??= "skills_native_call_failed"; stopWorkerGroup(); } }); child.once("error", () => { clearTimeout(deadline); stopWorkerGroup(); reject(new Error("skills_native_call_failed")); }); child.once("close", (code, signal) => { clearTimeout(deadline); stopWorkerGroup(); if (failureCode || code !== 0 || signal) { reject(new Error(failureCode ?? "skills_native_call_failed")); return; } try { const result = JSON.parse(Buffer.concat(stdout).toString("utf8")); if (result.ok === false && /^\w{1,80}$/.test(result.code)) { reject(new Error(result.code)); return; } const expectedStatus = (actions ?? []).some( (action) => action.type === "skill" ) ? "PROVEN" : "EMPTY"; if ( result.ok !== true || result.proof?.status !== expectedStatus || !/^[a-f0-9]{64}$/.test(result.proof?.digest ?? "") ) { throw new Error("skills_native_output_rejected"); } resolve({ status: result.proof.status, digest: result.proof.digest }); } catch { reject(new Error("skills_native_output_rejected")); } }); deadline = setTimeout(() => { failureCode ??= "skills_native_call_timeout"; stopWorkerGroup(); }, timeoutMs); deadline.unref(); }); } if ( process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url ) { let result; try { if (process.argv[2] !== "--prove") throw new Error("skills_native_request_rejected"); // cxconnect e65c62f6 timed out twice at the old synchronous 20s limit // while the new gateway was warming. This worker is already isolated // from heartbeats and its entire process group has a hard 90s deadline. const proof = proveDefaultProfileNativeSkills({ ...JSON.parse(process.argv[3]), nativeCallTimeoutMs: 60_000, }); result = { ok: true, proof }; } catch (error) { result = { ok: false, code: error?.name === "FlySkillsBridgeError" ? error.code : "skills_native_call_failed", }; } process.stdout.write(JSON.stringify(result)); }