#!/usr/bin/env python3 """ Copy runtime-relevant Koolook files into a live ComfyUI custom_nodes folder so a fix can be tested without a tag-and-publish round-trip. USER-INITIATED ONLY. This script overwrites a live ComfyUI install. Agents must NEVER run it automatically - not after a commit, not after a PR merge or ship-pr, not at session end, not from any "task complete" cleanup. The maintainer typically has multiple parallel sessions across worktrees, and an unsolicited sync from one silently destroys what another is reviewing. See project CLAUDE.md `dev-sync` section for the full policy. Run only on the explicit user trigger phrase. The target path is read from the KOLOOK_COMFYUI_DEV_PATH environment variable (loaded from `.env` at the repo root if present). The variable is intentionally kept out of the committed tree - see `.env.example`. `KOLOOK_COMFYUI_DEV_PATH` should point at the eventual Koolook subdirectory inside `custom_nodes/`, NOT at the `custom_nodes/` parent. Target ``custom_nodes/koolook/`` — that's where ComfyUI-Manager and the Comfy Registry install (derived from ``[project].name`` in ``pyproject.toml``), so dev-sync overwrites the Manager install in place. Targeting ``custom_nodes/ComfyUI-Koolook/`` instead spawns a parallel install; ``__init__.py``'s duplicate-install guard logs a critical message and disables the non-winning copy (issue #162). Example layouts: macOS: /Volumes/Data/ComfyUI/custom_nodes/koolook Windows: C:/ComfyUI_portable/ComfyUI/custom_nodes/koolook Usage: python scripts/sync_to_dev.py # copy files python scripts/sync_to_dev.py --dry-run # show what would copy python scripts/sync_to_dev.py --init # first-run: create the # target folder if missing # (parent custom_nodes/ must # already exist), then sync After copying Python files, restart ComfyUI manually so custom-node modules are re-imported. This script only copies files. Exit codes: 0 success 2 KOLOOK_COMFYUI_DEV_PATH unset, parent missing, or target missing (without --init) 3 --init refused: parent is not an existing directory or doesn't resemble a ComfyUI custom_nodes/ folder This script never reaches outside the repo, never deletes anything in the source, and only touches paths under the configured target. It does overwrite files in the target - that's the point. """ from __future__ import annotations import argparse import json import os import shutil import subprocess import sys from datetime import datetime from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent def _get_short_sha() -> str | None: """Best-effort short commit SHA of the source tree being synced. Returns ``None`` if git isn't reachable or the call fails - the summary line just omits the SHA in that case. """ try: r = subprocess.run( ["git", "rev-parse", "--short", "HEAD"], cwd=str(REPO_ROOT), capture_output=True, text=True, timeout=2, ) if r.returncode == 0: sha = r.stdout.strip() if sha: return sha except (OSError, subprocess.TimeoutExpired): pass return None def _get_worktree_name() -> str: """Returns the basename of the source tree being synced - useful when the maintainer is running multiple parallel ComfyUI installs and needs to know which checkout fed the most recent sync. For a worktree at ``.../ComfyUI-Koolook/.claude/worktrees/foo`` this returns ``foo``; for the main repo at ``.../ComfyUI-Koolook`` it returns ``ComfyUI-Koolook``. Either is informative enough to disambiguate.""" return REPO_ROOT.name def build_line() -> str: """Composes the two-piece header line consumed by the chat-report convention defined in project CLAUDE.md: - SHA falls back to ``unknown`` if git is unreachable (we always need SOMETHING in slot 1 - the line shape is part of the convention). Worktree name comes from ``REPO_ROOT.name`` and is always present. Public - consumed by scoped per-module wrappers like ``sync_to_dev_audio.py`` so every dev-sync variant emits the same chat-report header. """ sha = _get_short_sha() or "unknown" return f"{sha} - {_get_worktree_name()}" # Backwards-compatible alias (the function was private until the audio # wrapper landed). Drop after the next release cycle once we're sure no # downstream caller imports the underscore name. _build_line = build_line def write_build_info(target: Path, scope: str | None) -> None: """Drop a tiny JSON next to the sidebar JS so the in-browser footer can render `dev *