UNPKG

comfyui-mcp

Version:

Local-first, agent-native control plane for ComfyUI — MCP server + autonomous sidebar agent that drives your live graph in natural language on ANY LLM: Claude/ChatGPT/Gemini on your subscription (no API key), free local models via Ollama (fully offline),

188 lines • 9.87 kB
// Trainer bootstrap — the one-time setup that makes NATIVE (dockerless) // training possible on this machine: clone ostris ai-toolkit at the pinned // commit into <trainingRoot>/ai-toolkit, create its venv, and install torch + // requirements. Idempotent: a ready checkout+venv is left alone; a partial one // is repaired. Needed on RunPod pods (no nested docker) and dockerless rigs. // // Everything lives under the training root, so a pod whose root is the // persistent /workspace only pays the ~10min install ONCE across restarts. import childProcess from "node:child_process"; import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { delimiter, join } from "node:path"; import { AI_TOOLKIT_REF, AI_TOOLKIT_REPO, nativeToolkitReady, resolveAiToolkitDir, resolveAiToolkitPython, TRAINER_COMMAND, } from "./ai-toolkit.js"; const TORCH_PACKAGES = ["torch==2.9.1", "torchvision==0.24.1", "torchaudio==2.9.1"]; const TORCH_INDEX = "https://download.pytorch.org/whl/cu128"; export async function bootstrapStatus() { const dir = resolveAiToolkitDir(); const cloned = existsSync(join(dir, "run.py")); const venv = existsSync(resolveAiToolkitPython()); return { dir, python: resolveAiToolkitPython(), ref: AI_TOOLKIT_REF, cloned, venv, ready: await nativeToolkitReady() }; } function ok(command, data) { return { ok: true, command, data }; } function fail(command, code, message, stderr) { return { ok: false, command, error: { code, message }, stderr }; } /** Run one command, streaming output via onLog; resolves with the exit code + tail. */ function stream(cmd, args, cwd, onLog) { return new Promise((resolve) => { const child = childProcess.spawn(cmd, args, { cwd, windowsHide: true, env: { ...process.env } }); const tail = []; const onLine = (line) => { tail.push(line); if (tail.length > 200) tail.shift(); onLog?.(line); }; for (const s of [child.stdout, child.stderr]) { if (!s) continue; s.setEncoding("utf8"); let buf = ""; s.on("data", (chunk) => { buf += chunk; const parts = buf.split(/\r\n|\r|\n/); buf = parts.pop() ?? ""; for (const line of parts) if (line.trim()) onLine(line); }); s.on("end", () => { if (buf.trim()) onLine(buf); }); } child.on("close", (code) => resolve({ code: code ?? 1, tail: tail.slice(-30).join("\n") })); child.on("error", (err) => resolve({ code: 1, tail: String(err) })); }); } function basePython() { return process.platform === "win32" ? "python" : "python3"; } /** * Resolve the git executable to spawn. PATH first, then — on Windows — the * standard Git for Windows install roots. The fallback exists because a * long-lived orchestrator started BEFORE Git was installed keeps its * pre-install PATH: panel_reload rebuilds the server but the parent's * environment is inherited, so `spawn("git")` dies with ENOENT even though * Git is on the machine (issue #1640). Returns the ABSOLUTE path so the * spawn below never depends on the stale PATH again. */ export function resolveGitExecutable(env = process.env, platform = process.platform) { const name = platform === "win32" ? "git.exe" : "git"; for (const dir of (env.PATH ?? "").split(delimiter).filter(Boolean)) { const candidate = join(dir.replace(/^"|"$/g, ""), name); if (existsSync(candidate)) return candidate; } if (platform !== "win32") return undefined; const roots = [ env["ProgramFiles"] ?? "C:\\Program Files", env["ProgramFiles(x86)"], env.LOCALAPPDATA ? join(env.LOCALAPPDATA, "Programs") : undefined, ]; for (const root of roots) { if (!root) continue; const candidate = join(root, "Git", "cmd", "git.exe"); if (existsSync(candidate)) return candidate; } return undefined; } /** Why no git could be found — spelled out with the remedy (issue #1640). */ function gitNotFoundMessage() { return process.platform === "win32" ? "git is not on this process's PATH and was not found under the standard Git for Windows roots " + "(Program Files\\Git, %LOCALAPPDATA%\\Programs\\Git). If Git was installed AFTER the orchestrator " + "started, panel_reload is not enough — the long-lived parent keeps its pre-install PATH. Fully " + "restart the orchestrator (or reboot), then re-run bootstrap." : "git is not on this process's PATH. If git was installed after the orchestrator started, restart " + "the orchestrator so it inherits the new PATH, then re-run bootstrap."; } /** * Bootstrap the native trainer. Steps (each idempotent): * 1. clone ai-toolkit @ AI_TOOLKIT_REF (or `git fetch + checkout` an existing clone) * 2. create the venv * 3. pip install torch (cu128 index) + hf_transfer * 4. pip install -r requirements.txt * Long (~10 min on a fresh box, dominated by torch). Streams every command's * output via onLog. */ export async function bootstrapToolkit(opts = {}) { const dir = resolveAiToolkitDir(); const log = opts.onLog; try { const git = resolveGitExecutable(); if (!git) { return fail(TRAINER_COMMAND.bootstrap, "git_not_found", gitNotFoundMessage()); } mkdirSync(dir, { recursive: true }); if (!existsSync(join(dir, ".git"))) { // Clone into a temp sibling then move in, so a failed clone doesn't leave // a half repo at the real path. const tmp = `${dir}.clone-${process.pid}`; const r = await stream(git, ["clone", "--recurse-submodules", AI_TOOLKIT_REPO, tmp], undefined, log); if (r.code !== 0) { // Clean the failed clone so a retry isn't blocked by its leftovers // (codex finding: the stale temp dir made every retry fail instantly). const { rmSync: rmTmp } = await import("node:fs"); rmTmp(tmp, { recursive: true, force: true }); return fail(TRAINER_COMMAND.bootstrap, "clone_failed", `git clone exited ${r.code}`, r.tail); } const { renameSync, rmSync } = await import("node:fs"); rmSync(dir, { recursive: true, force: true }); renameSync(tmp, dir); } let r = await stream(git, ["fetch", "--all"], dir, log); if (r.code !== 0) return fail(TRAINER_COMMAND.bootstrap, "fetch_failed", `git fetch exited ${r.code}`, r.tail); r = await stream(git, ["checkout", AI_TOOLKIT_REF], dir, log); if (r.code !== 0) return fail(TRAINER_COMMAND.bootstrap, "checkout_failed", `git checkout ${AI_TOOLKIT_REF} exited ${r.code}`, r.tail); r = await stream(git, ["submodule", "update", "--init", "--recursive"], dir, log); if (r.code !== 0) return fail(TRAINER_COMMAND.bootstrap, "submodule_failed", `submodule update exited ${r.code}`, r.tail); // Invalidate any previous completion marker BEFORE the install steps: a // recreated venv whose pip steps now fail must NOT inherit the old env's // "ready" (codex finding — the stale marker let a partial env launch). // It is rewritten only after every step below succeeds. const { rmSync: rmMarker } = await import("node:fs"); rmMarker(join(dir, ".bootstrap-ok"), { force: true }); if (!existsSync(resolveAiToolkitPython())) { r = await stream(basePython(), ["-m", "venv", "venv"], dir, log); if (r.code !== 0) return fail(TRAINER_COMMAND.bootstrap, "venv_failed", `venv creation exited ${r.code}`, r.tail); } const pip = [resolveAiToolkitPython(), "-m", "pip", "install", "--no-cache-dir"]; r = await stream(pip[0], [...pip.slice(1), ...TORCH_PACKAGES, "--index-url", TORCH_INDEX], dir, log); if (r.code !== 0) return fail(TRAINER_COMMAND.bootstrap, "torch_failed", `torch install exited ${r.code} (index ${TORCH_INDEX})`, r.tail); r = await stream(pip[0], [...pip.slice(1), "hf_transfer"], dir, log); if (r.code !== 0) return fail(TRAINER_COMMAND.bootstrap, "hf_transfer_failed", `hf_transfer install exited ${r.code}`, r.tail); r = await stream(pip[0], [...pip.slice(1), "-r", "requirements.txt"], dir, log); if (r.code !== 0) return fail(TRAINER_COMMAND.bootstrap, "requirements_failed", `requirements install exited ${r.code}`, r.tail); const status = await bootstrapStatus(); if (!status.ready) { return fail(TRAINER_COMMAND.bootstrap, "verify_failed", "bootstrap finished but run.py/venv python are still missing"); } // Completion marker: every pip step exited 0 to get here. nativeToolkitReady // requires it (or a torch presence check for pre-marker envs) so a checkout // whose dependency install FAILED is never selected as "ready" (codex). writeFileSync(join(dir, ".bootstrap-ok"), `${AI_TOOLKIT_REF} ${new Date().toISOString()}\n`); return ok(TRAINER_COMMAND.bootstrap, status); } catch (err) { return fail(TRAINER_COMMAND.bootstrap, "error", err instanceof Error ? err.message : String(err)); } } /** homedir re-export for tests overriding the training root. */ export function _bootstrapDefaultDir() { return join(process.env.COMFYUI_MCP_DATA_DIR?.trim() || join(homedir(), ".comfyui-mcp"), "training", "ai-toolkit"); } //# sourceMappingURL=trainer-bootstrap.js.map