comfyui-node-scaffold
Bootstrap a new ComfyUI custom-node pack that matches the established
mobile-first TypeScript + bun build architecture of comfyui-gallery-loader,
comfyui-sampler-info, and comfyui-touch-numeric, leaving only the actual
node logic to implement.
Supporting material is split across references/ by the path that needs it;
see REFERENCE.md for the index.
When to Use This Skill
| Use this skill when... | Use the alternative when... |
|---|---|
| Starting a new ComfyUI usability pack repo — CI-green TS toolchain, Comfy Registry publish, and the widget-intercept/gesture skeleton before writing pack logic | You want the full pipeline (repo created + seeded + gitops-adopted) → comfy-node |
Spinning up a project:comfyui-nodes backlog idea (touch-numeric, prompt-editor, model-gallery…) |
Adding a node to an existing pack — this creates a new repo |
The architecture it scaffolds
TypeScript source in src/ (entry src/index.ts), built to web/dist/ via
bun build — typed authoring against @comfyorg/comfyui-frontend-types,
browser-ESM output, the /scripts/app.js runtime import left unbundled. This
supersedes the old vanilla-JS (web/js/*.js + copied modal primitives)
template. The generated pack starts with an ADR (docs/blueprint/adrs/0001-…)
recording the decision, mirroring sampler-info's ADR-0010.
- Type gate:
bun run typecheck→tsc --noEmit(never emits). - Emit:
bun build ./src/index.ts --target browser --format esm --outdir web/dist --external '/scripts/*'. If the pack ships a static data corpus, append&& cp -R web/data web/dist/data. - Serve:
__init__.pysetsWEB_DIRECTORY = "./web/dist"; ComfyUI serves that tree at/extensions/<name>/. - Distribute:
web/dist/is committed (tracked) so git clone/update carries the served bundle;[tool.comfy] includes = ["web/dist"]also force-ships it to the registry, andpublish.ymlrunsbun run buildfirst. - The
/scripts/app.jstype shim: apathsmapping intsconfig.jsonpoints the rooted import atsrc/comfyui-shims.d.ts(TypeScript will not match an ambientdeclare moduleagainst a/…specifier). The emitted import string stays/scripts/app.jsand--external '/scripts/*'keeps it unbundled.
The vein
A frontend extension that intercepts widget.onPointerDown (modern Vue
frontend, comfyui-frontend-package >= 1.40) and opens a touch-friendly HTML
modal in place of a clunky native LiteGraph control. Widgets are matched by
name (generic across node packs); the enhancement is additive (graceful
fallback, never breaks serialized workflows); the modal is touch-first (16px
inputs, big tap targets, momentum scroll). The modal primitives come from
@laurigates/comfy-modal-kit (openModalShell / fuzzyRank /
highlightMatches) — imported, not copied — and bun build inlines them.
Name-matching needs a gate when the modal's content is external. The emitted
TARGET_WIDGETS set matches on name alone, which is correct for a modal that
renders the widget's own options.values. If your pack instead fills the modal
from a folder_paths listing, an endpoint, or a corpus, a node that hardcodes a
combo under the same name (RIFE VFI's ckpt_name) has its only valid choices
replaced with values it rejects — see comfyui-node-authoring § "A widget name
is not proof of its option source" for the overlap gate.
Four variants
| Variant | Use when | Shape | Modal kit |
|---|---|---|---|
frontend (default) |
No Python needed — pure widget UX (seed/numeric keypad, prompt editor, tooltips, enum recipes). | Empty NODE_CLASS_MAPPINGS; widget-intercept modal in src/index.ts. Like sampler-info / touch-numeric. |
imports the kit |
backend |
Needs to read disk / serve thumbnails / add a node (model thumbnails, file listings). | Adds <module>.py (node + aiohttp endpoints, ComfyUI-bundled libs only) + a tests/conftest.py that stubs aiohttp/server so pytest is green. Like gallery-loader. |
imports the kit |
gesture |
The UX is a canvas interaction, not a widget — drag/long-press on nodes or groups (resize, move, region-box). Must be recognizable on the first pointerdown — see the contract in variants.md. |
Empty NODE_CLASS_MAPPINGS; a canvas pointer layer in src/index.ts with exported pure geometry helpers. Like touch-resize's corner grab-handles. |
no kit |
shim |
The pack's whole job is injecting scoped CSS / registering commands to paper over upstream frontend bugs — no modal, no widget hook. | Empty NODE_CLASS_MAPPINGS; a SHIMS registry in src/index.ts with applyCssShim/removeCssShim, one managed <style> per shim driven by a boolean setting, + a jsdom lifecycle smoke test. Like comfyui-touch-shim. |
no kit |
Decision rule: frontend/backend with --widgets for a per-widget
modal; frontend/backend without --widgets for a standalone modal opened
from the shared Touch Tools hub (the family's one action-bar button — a
scaffolded pack contributes a chooser row, never a button of its own); gesture
when the interaction is on the
canvas/node frame itself (no widget to hook); shim when the pack only injects
scoped CSS / registers commands to paper over an upstream frontend bug (no modal,
no widget). Add backend only when the feature
genuinely needs the server to read files or serve data. A non-bundled Python
dependency is never allowed — if you reach for one, it belongs in a separate
companion pack.
For what --widgets changes about the emitted modal, and what the gesture
and shim skeletons contain, see
references/variants.md.
How to run
scaffold.py is stdlib-only. Run from the workspace root (repos/laurigates/)
so the new repo lands as a sibling of the reference packs.
Frontend-only pack:
python3 ${CLAUDE_SKILL_DIR}/scaffold.py --name comfyui-touch-numeric --display "Touch Numeric" --desc "Touch-friendly keypad + slider modal for seed and INT/FLOAT widgets." --variant frontend --widgets seed,noise_seed,cfg,steps,denoise
The backend, standalone-modal, gesture, and shim recipes, the full flag
list, and the banner-tagline rule live in
references/invocation.md.
After scaffolding
The generator prints the exact next steps. In order:
cd comfyui-<name>
git init -b main
uv sync --group dev
bun install
pre-commit install
just check
Seed main directly (the repo is unprotected until gitops adopts it) — pushing
a feature branch first would leave main missing on origin and force a rename
- default-branch fixup later.
Then implement, and wire up infra:
- Implement the modal in
src/index.ts— tuneTARGET_WIDGETSand replace theopenPickerstub with the real modal body (import { fuzzyRank } from "@laurigates/comfy-modal-kit"for search,openModalShellfor the dialog). For the standalone-modal shape, keep the Touch Tools hub wiring intact and swap the placeholderpi pi-th-largeicon for a fitting PrimeIcon — see references/variants.md § "The Touch Tools hub contract". For thebackendvariant, fill in<module>.py's node + endpoints; widenALLOWED_EXTENSIONSexplicitly for any new file type read off disk. For thegesturevariant, tune the pointer layer (selectedNodes/nodeScreenRect/hitHandle/resizedGeometry) and re-measureCONFIG.handleHitRadiusagainst this pack's handle placement, keeping the gesture recognizable on the firstpointerdownper the contract in references/variants.md. For theshimvariant, replace the placeholderSHIMSentry — link the upstream issue, point the selector at a stabledata-testid, keep each shim fail-soft. - Add the repo to
gitops/repositories.tfwithcomfy_registry = true(andrelease_please = true). On apply, gitops pushes both the release-please App credentials and theREGISTRY_ACCESS_TOKENsecret. No per-repo secret creation is needed.
Or skip steps 1–2 entirely: run the /comfy-node orchestrator, which
chains scaffold → gh repo create → seed main → the gitops PR.
Before opening the registry PR, grade the pack's finishing pass — the artwork
gate, the --verify audit, and the registry-publishing facts are in
references/registry-readiness.md. What the
generator emits, the invariants the emitted code assumes, and what it
deliberately does not generate are in
references/implementing-the-pack.md.
Agentic Optimizations
| Context | Command |
|---|---|
| Scaffold a frontend pack | python3 ${CLAUDE_SKILL_DIR}/scaffold.py --name comfyui-X --display "X" --desc "…" --variant frontend --widgets a,b |
| Scaffold a gesture pack | python3 ${CLAUDE_SKILL_DIR}/scaffold.py --name comfyui-X --display "X" --desc "…" --variant gesture |
| Scaffold a CSS/shim pack | python3 ${CLAUDE_SKILL_DIR}/scaffold.py --name comfyui-X --display "X" --desc "…" --variant shim |
| Verify a generated pack | cd comfyui-X && bun install && just check |
| Grade one pack's finishing pass | python3 ${CLAUDE_SKILL_DIR}/scaffold.py --verify path/to/comfyui-X |
| Sweep the whole fleet for drift | python3 ${CLAUDE_SKILL_DIR}/scripts/check-fleet-drift.py |
| Sweep one pack only | python3 ${CLAUDE_SKILL_DIR}/scripts/check-fleet-drift.py --pack comfyui-X |