Files
PBandDev 157f7cde10 feat: Civitai Resources To Hash Metadata node, resource picker + e2e harness (#1)
* 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)
2026-07-12 15:12:03 -05:00

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();