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),
152 lines (143 loc) • 7.24 kB
JavaScript
#!/usr/bin/env node
// Generate one-click install-windows.bat + install-runpod.sh for each pack from
// its pack.yaml + manifest.yaml. The manifest is the single source of truth
// (also consumed by apply_manifest); never hand-edit the generated scripts.
//
// node scripts/gen-pack-installers.mjs # all packs/*
// node scripts/gen-pack-installers.mjs packs/anima # one pack
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from "node:fs";
import { join, basename, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { parse } from "yaml";
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const packsRoot = join(repoRoot, "packs");
function packDirsFromArgs() {
const arg = process.argv[2];
if (arg) return [join(repoRoot, arg)];
return readdirSync(packsRoot)
.map((n) => join(packsRoot, n))
.filter((p) => statSync(p).isDirectory() && existsSync(join(p, "manifest.yaml")));
}
// Folder a `git clone <url>` lands in (basename, minus .git / trailing ref).
function repoFolder(url) {
const noRef = url.replace(/(?<!^)@[^@/]+$/, "").replace(/[?#].*$/, "").replace(/\/+$/, "");
return basename(noRef).replace(/\.git$/i, "");
}
// Target path of a model, relative to models/.
function modelRel(m) {
if (m.local_path) return m.local_path;
const fn = m.filename || basename(new URL(m.url).pathname);
return `${m.model_type || "checkpoints"}/${fn}`;
}
function genBat(pack, manifest) {
const nodes = manifest.custom_nodes || [];
const models = manifest.models || [];
const pip = manifest.pip || [];
const workflow = pack.workflow || "workflow.json";
const L = [];
L.push("@echo off");
L.push("setlocal EnableExtensions EnableDelayedExpansion");
L.push("chcp 65001 >nul");
L.push(`rem ===== Auto-generated by scripts/gen-pack-installers.mjs — do not edit =====`);
L.push(`rem Pack: ${pack.display_name || pack.name}`);
L.push("rem Run from your ComfyUI root (the folder containing custom_nodes\\ and models\\).");
L.push('if not exist "custom_nodes" ( echo [ERROR] Run from your ComfyUI root ^(custom_nodes\\ not found^). & pause & exit /b 1 )');
L.push('where git >nul 2>&1 || ( echo [ERROR] git not found in PATH. & pause & exit /b 1 )');
L.push('where curl >nul 2>&1 || ( echo [ERROR] curl not found in PATH. & pause & exit /b 1 )');
L.push("rem Resolve the ComfyUI python: venv first, then portable embed, then PATH python.");
L.push('set "PY=%CD%\\.venv\\Scripts\\python.exe"');
L.push('if not exist "%PY%" set "PY=%CD%\\venv\\Scripts\\python.exe"');
L.push('if not exist "%PY%" set "PY=%CD%\\..\\python_embeded\\python.exe"');
L.push('if not exist "%PY%" set "PY=python"');
L.push('echo using python: %PY%');
L.push("");
L.push("echo -------- custom nodes --------");
for (const url of nodes) L.push(`call :clone "${repoFolder(url)}" "${url}"`);
if (pip.length) {
L.push("");
L.push("echo -------- pip (manifest extras) --------");
for (const p of pip) L.push(`"%PY%" -m pip install "${p}"`);
}
L.push("");
L.push("echo -------- models --------");
for (const m of models) L.push(`call :grab "models\\${modelRel(m).replace(/\//g, "\\")}" "${m.url}"`);
L.push("");
L.push(`echo DONE. Restart ComfyUI, then load ${workflow}.`);
L.push("pause");
L.push("exit /b");
L.push("");
L.push(":clone");
L.push('if not exist "custom_nodes\\%~1" (');
L.push(" echo cloning %~1");
L.push(' git clone --depth 1 "%~2" "custom_nodes\\%~1"');
L.push(' if exist "custom_nodes\\%~1\\requirements.txt" ( echo installing %~1 requirements.txt & "%PY%" -m pip install -r "custom_nodes\\%~1\\requirements.txt" )');
L.push(") else ( echo %~1 present - skip )");
L.push("goto :eof");
L.push("");
L.push(":grab");
L.push('if not exist "%~dp1" mkdir "%~dp1"');
L.push('if not exist "%~1" ( echo downloading %~nx1 & curl -L --ssl-no-revoke -o "%~1" "%~2" ) else ( echo %~nx1 present - skip )');
L.push("goto :eof");
return L.join("\r\n") + "\r\n";
}
function genSh(pack, manifest) {
const nodes = manifest.custom_nodes || [];
const models = manifest.models || [];
const pip = manifest.pip || [];
const workflow = pack.workflow || "workflow.json";
const L = [];
L.push("#!/usr/bin/env bash");
L.push("set -euo pipefail");
L.push("# ===== Auto-generated by scripts/gen-pack-installers.mjs — do not edit =====");
L.push(`# Pack: ${pack.display_name || pack.name} (Linux / RunPod)`);
L.push("# Run from your ComfyUI root (the folder containing custom_nodes/ and models/).");
L.push('[ -d custom_nodes ] || { echo "[ERROR] run from your ComfyUI root (custom_nodes/ not found)"; exit 1; }');
L.push("# Resolve the ComfyUI python: its venv first, then portable embed, then \\$PYTHON/python3.");
L.push('# Installing requirements into the wrong interpreter is why nodes silently fail to load.');
L.push('if [ -n "${PYTHON:-}" ]; then PY="$PYTHON";');
L.push('elif [ -x ".venv/bin/python" ]; then PY=".venv/bin/python";');
L.push('elif [ -x "venv/bin/python" ]; then PY="venv/bin/python";');
L.push('elif [ -x "../python_embeded/python" ]; then PY="../python_embeded/python";');
L.push('else PY="python3"; fi');
L.push('echo "using python: $PY"');
L.push("");
L.push("clone() { # folder url");
L.push(' if [ ! -d "custom_nodes/$1" ]; then');
L.push(' echo " cloning $1"; git clone --depth 1 "$2" "custom_nodes/$1"');
L.push(' if [ -f "custom_nodes/$1/requirements.txt" ]; then echo " installing $1 requirements.txt"; "$PY" -m pip install -r "custom_nodes/$1/requirements.txt"; fi');
L.push(' else echo " $1 present - skip"; fi');
L.push("}");
L.push("grab() { # relpath url");
L.push(' mkdir -p "$(dirname "$1")"');
L.push(' if [ ! -f "$1" ]; then echo " downloading $(basename "$1")"; curl -L -o "$1" "$2"; else echo " $(basename "$1") present - skip"; fi');
L.push("}");
L.push("");
L.push('echo "-------- custom nodes --------"');
for (const url of nodes) L.push(`clone "${repoFolder(url)}" "${url}"`);
if (pip.length) {
L.push("");
L.push('echo "-------- pip (manifest extras) --------"');
for (const p of pip) L.push(`"$PY" -m pip install "${p}"`);
}
L.push("");
L.push('echo "-------- models --------"');
for (const m of models) L.push(`grab "models/${modelRel(m)}" "${m.url}"`);
L.push("");
L.push(`echo "DONE. Restart ComfyUI, then load ${workflow}."`);
return L.join("\n") + "\n";
}
const dirs = packDirsFromArgs();
if (!dirs.length) {
console.error("No packs found under packs/.");
process.exit(1);
}
for (const dir of dirs) {
const manifest = parse(readFileSync(join(dir, "manifest.yaml"), "utf8")) || {};
const packPath = join(dir, "pack.yaml");
const pack = existsSync(packPath) ? parse(readFileSync(packPath, "utf8")) || {} : { name: basename(dir) };
writeFileSync(join(dir, "install-windows.bat"), genBat(pack, manifest));
writeFileSync(join(dir, "install-runpod.sh"), genSh(pack, manifest));
const n = (manifest.custom_nodes || []).length;
const m = (manifest.models || []).length;
console.log(`${basename(dir)}: wrote install-windows.bat + install-runpod.sh (${n} nodes, ${m} models)`);
}