* feat: port template e2e harness * feat: civitai resource line parser * feat: e2e packs, fixtures, dummy model * feat: full-chain e2e via dummy model * feat: cached civitai resolver * feat: emission, sanitize, dedup, structured entries * feat: v2 node + ui payload + registration * feat: in-node civitai status list * test: v2 url-box + widget e2e; fix cloudflare 403 via user-agent; output node * feat: e2e serve mode; nicer status wrapping * docs: v2 node + testing guide; ci runs full suite with caches * chore: ignore e2e scratch dir * fix: codex review findings (validation, cache hardening, breaker, harness guards) * feat: status widget redesign — themed cards, preview thumbnails, bounded sizing * feat: drop nsfw gating from previews; readme example + screenshot * feat!: drop CIVITAI_API_TOKEN support; all endpoints are public * feat: civitai search client for resource picker * feat: textbox line model for resource picker * feat: lora manager soft-dep client * feat: resource picker overlay modal * feat: add-resource button opens picker on v2 node * test: picker e2e (live search, remove staging, LM local tab) * docs: resource picker usage * fix: picker review findings (stale cursor/error races, staged-control locking, LM page size, e2e de-vacuousing, setup timeout) * feat: /clth/preview route — run-parity resource preview endpoint * feat: status list row remove + refresh toolbar + preview client * feat: live status preview on picker apply, refresh, row remove, workflow load * feat: picker ux — zero-results site link, weight labels, card links * test: live-preview e2e (refresh, row remove, apply, reload) * docs: live preview, row remove, zero-results behavior * fix: r2 review findings (route caps + dedicated worker, stale-run guard, configure-timer cleanup, /api prefix, id-digit guard, empty-note, deterministic load e2e) * fix: refresh as native widget, keep loaded_loras rows in previews, note copy * fix: retry civitai search once on 5xx (live api flaps) * fix: preview reads upstream loaded_loras link (LM loader) — lora rows without a run * docs: live loaded_loras preview via upstream link * feat: status dot tooltips (resolved/unverified/duplicate/missing) * fix: search retries twice on 5xx (match python resolver policy)
136 lines
4.0 KiB
JavaScript
136 lines
4.0 KiB
JavaScript
import { execFileSync } from "node:child_process";
|
|
import {
|
|
existsSync,
|
|
lstatSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
readlinkSync,
|
|
rmSync,
|
|
symlinkSync,
|
|
} from "node:fs";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { e2eConfig } from "../e2e.config.mjs";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const projectRoot = resolve(__dirname, "..");
|
|
const workspaceDir = resolve(projectRoot, e2eConfig.workspaceDir);
|
|
const comfyDir = resolve(projectRoot, e2eConfig.comfyDir);
|
|
const workspaceVenvDir = resolve(projectRoot, e2eConfig.venvDir);
|
|
const customNodesDir = resolve(comfyDir, "custom_nodes");
|
|
const packageJsonPath = resolve(projectRoot, "package.json");
|
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
const mountedNodeDir = resolve(customNodesDir, packageJson.name);
|
|
const pythonBinary =
|
|
process.platform === "win32"
|
|
? resolve(workspaceVenvDir, "Scripts", "python.exe")
|
|
: resolve(workspaceVenvDir, "bin", "python");
|
|
const comfyBinary =
|
|
process.platform === "win32"
|
|
? resolve(workspaceVenvDir, "Scripts", "comfy.exe")
|
|
: resolve(workspaceVenvDir, "bin", "comfy");
|
|
|
|
function run(command, args, label, options = {}) {
|
|
console.log(`[setup:e2e] ${label}`);
|
|
console.log(`[setup:e2e] $ ${command} ${args.join(" ")}`);
|
|
execFileSync(command, args, {
|
|
cwd: options.cwd ?? projectRoot,
|
|
stdio: "inherit",
|
|
});
|
|
}
|
|
|
|
// A symlink/junction here would redirect installs (and later deletions) to a
|
|
// real ComfyUI outside the repo — refuse to operate through one.
|
|
function assertNotSymlink(target, label) {
|
|
if (!existsSync(target)) {
|
|
return;
|
|
}
|
|
if (lstatSync(target).isSymbolicLink()) {
|
|
throw new Error(`${label} (${target}) is a symlink/junction — refusing to use a redirected path.`);
|
|
}
|
|
}
|
|
|
|
function ensureWorkspaceVenv() {
|
|
if (existsSync(pythonBinary)) {
|
|
return;
|
|
}
|
|
|
|
mkdirSync(resolve(projectRoot, e2eConfig.workspaceDir, ".."), { recursive: true });
|
|
run("uv", ["venv", workspaceVenvDir], "Create repo-local E2E virtual environment");
|
|
}
|
|
|
|
function ensureComfyCli() {
|
|
if (existsSync(comfyBinary)) {
|
|
return;
|
|
}
|
|
|
|
run(
|
|
"uv",
|
|
[
|
|
"pip",
|
|
"install",
|
|
"--python",
|
|
pythonBinary,
|
|
`comfy-cli==${e2eConfig.comfyCliVersion}`,
|
|
],
|
|
"Install comfy-cli into the E2E virtual environment",
|
|
);
|
|
}
|
|
|
|
function ensurePinnedComfyInstall() {
|
|
const mainPyPath = resolve(comfyDir, "main.py");
|
|
if (existsSync(mainPyPath)) {
|
|
return;
|
|
}
|
|
|
|
mkdirSync(dirname(comfyDir), { recursive: true });
|
|
run(
|
|
comfyBinary,
|
|
[
|
|
"--skip-prompt",
|
|
`--workspace=${workspaceDir}`,
|
|
"install",
|
|
"--skip-manager",
|
|
"--fast-deps",
|
|
"--cpu",
|
|
"--url",
|
|
`https://github.com/comfyanonymous/ComfyUI.git@${e2eConfig.comfyRevision}`,
|
|
],
|
|
"Install pinned ComfyUI workspace",
|
|
{ cwd: dirname(comfyDir) },
|
|
);
|
|
}
|
|
|
|
function ensureMountedCustomNode() {
|
|
mkdirSync(customNodesDir, { recursive: true });
|
|
|
|
if (!existsSync(mountedNodeDir)) {
|
|
symlinkSync(projectRoot, mountedNodeDir, process.platform === "win32" ? "junction" : "dir");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const stats = lstatSync(mountedNodeDir);
|
|
if (!stats.isSymbolicLink()) {
|
|
throw new Error("Mounted custom node path already exists and is not a symlink");
|
|
}
|
|
|
|
const currentTarget = resolve(customNodesDir, readlinkSync(mountedNodeDir));
|
|
if (currentTarget !== projectRoot) {
|
|
rmSync(mountedNodeDir, { recursive: true, force: true });
|
|
symlinkSync(projectRoot, mountedNodeDir, process.platform === "win32" ? "junction" : "dir");
|
|
}
|
|
} catch (error) {
|
|
rmSync(mountedNodeDir, { recursive: true, force: true });
|
|
symlinkSync(projectRoot, mountedNodeDir, process.platform === "win32" ? "junction" : "dir");
|
|
}
|
|
}
|
|
|
|
assertNotSymlink(resolve(projectRoot, ".e2e"), "e2e root");
|
|
assertNotSymlink(comfyDir, "e2e ComfyUI workspace");
|
|
assertNotSymlink(customNodesDir, "e2e custom_nodes dir");
|
|
ensureWorkspaceVenv();
|
|
ensureComfyCli();
|
|
ensurePinnedComfyInstall();
|
|
ensureMountedCustomNode();
|