Contributing to comfy-cli
August 26, 2026 · View on GitHub
Thanks for contributing! comfy-cli is a Python/Typer
CLI for installing and running ComfyUI, published to PyPI as
comfy-cli.
- Bugs and feature requests — open an issue at Comfy-Org/comfy-cli/issues using one of the issue templates.
- Code changes — fork the repo, branch off
main, and open a pull request. - Questions — ask in Discord.
This file is the development guide as well as the contribution guide: the sections below cover environment setup, the checks CI enforces, PR conventions, and how to add a new command.
Before you open a pull request
Run the same three checks CI enforces on every PR to main:
ruff check . # lint (ruff_check.yml)
ruff format --check . # formatting (CI runs `ruff format --diff`)
pytest # unit tests (pytest.yml)
All three must pass. ruff is pinned in
.pre-commit-config.yaml and in the ruff_check
workflow — match that version locally so a formatting pass does not fight CI.
Changes under comfy_cli/** or tests/e2e/** additionally trigger
build-and-test.yml, which runs the E2E
and platform tests on Linux, Windows, and macOS.
Coverage is reported to Codecov, so new code should come with tests.
Contributor License Agreement
First-time contributors are asked to sign the CLA. The CLA Assistant bot comments on your PR with the text and the phrase to reply with; the check clears once you have.
Setup
Minimum Python version is 3.10 (the version CI targets, and the floor
declared in pyproject.toml).
The project is uv-based, and uv.lock is authoritative — CI installs with
--locked, so a dependency change must land as a deliberate lockfile bump.
uv sync --extra dev # runtime + dev deps (ruff, pytest, pytest-cov, pre-commit, jsonschema)
uv run comfy --help # check the `comfy` entry point works
If you prefer a hand-managed virtualenv or conda env, an editable install works too:
pip install -e '.[dev]'
export ENVIRONMENT=dev
comfy --help
Install the pre-commit hook so commits are checked before they reach CI:
pre-commit install
The hook runs ruff (lint + format), a few file-hygiene checks, pyproject-fmt,
and the uv lock/export/sync hooks.
Branching and pull requests
The default branch is main — branch off main and open PRs against it.
Gotcha: a local checkout may already sit on a working branch, not
main. Don't assume the current branch is the base:git fetch origin git switch -c my-feature origin/main # or, without disturbing the current checkout: git worktree add ../comfy-cli-my-feature -b my-feature origin/main
Commit messages follow Conventional Commits, scoped by the area touched — this is what the vast majority of the history looks like, and what release notes are assembled from:
fix(jobs): stream live progress in `jobs watch` by attaching as the submitting client
feat(templates): serve stale gallery cache immediately, revalidate in background
docs: ... test: ... refactor: ... ci: ... chore: ...
PRs are squash-merged, so the PR title becomes the commit subject — write it in the same form. Keep a PR to one reviewable change; describe the user-visible effect, and link the issue it closes.
Notable user-facing changes should be summarized in
CHANGELOG.md under ## [Unreleased].
Running the unit tests
uv run --extra dev pytest --cov=comfy_cli --cov-report=xml .
Or, in an environment where the dev extras are already installed:
pytest --cov=comfy_cli --cov-report=xml .
Running E2E tests
E2E tests perform real comfy install, comfy launch, and comfy node operations.
They are disabled by default and must be explicitly enabled.
TEST_E2E=true pytest tests/e2e/
For pre-release testing against alternate ComfyUI repositories (e.g. Manager v4):
TEST_E2E=true \
TEST_E2E_COMFY_URL="https://github.com/ltdrdata/ComfyUI.git@dr-bump-manager" \
pytest tests/e2e/ -v
See docs/TESTING-e2e.md for the full guide including environment variables, test suite details, and scenario descriptions.
Debugging
You can add following config to your VSCode launch.json to launch debugger.
{
"name": "Python Debugger: Run",
"type": "debugpy",
"request": "launch",
"module": "comfy_cli.__main__",
"args": [],
"console": "integratedTerminal"
}
Making changes to the code base
There is a potential need for you to reinstall the package. You can do this by
either run pip install -e . again (which will reinstall), or manually
uninstall pip uninstall comfy-cli and reinstall, or even cleaning your virtual
env and reinstalling the package (pip install -e .). With uv, uv sync --extra dev re-resolves the environment in place.
Packaging custom nodes with .comfyignore
comfy node pack and comfy node publish now read an optional .comfyignore
file in the project root. The syntax matches .gitignore (implemented with
PathSpec's gitwildmatch rules), so you can reuse familiar patterns to keep
development-only artifacts out of your published archive.
- Patterns are evaluated against paths relative to the directory you run the command from (usually the repo root).
- Files required by the pack command itself (e.g.
__init__.py,web/*) are still forced into the archive even if they match an ignore pattern. - If no
.comfyignoreis present the command falls back to the original behavior and zips every git-tracked file.
Example .comfyignore:
docs/
frontend/
tests/
*.psd
Commit the file alongside your node so teammates and CI pipelines produce the same trimmed package.
Adding a new command
Top-level commands are registered in the _RootGroup.lazy_subcommands table in
comfy_cli/cmdline.py, not with app.add_typer(...). The table maps the command
name to the module that holds it; the module is imported the first time the
command is looked up, so comfy --help and comfy --version stay fast no matter
how heavy the module is. Entries are listed in the order comfy --help shows
them, so put a new entry where it should appear, not at the end.
A command group (a command with subcommands) is a LazySubcommand:
"mything": LazySubcommand("comfy_cli.command.mything", help="One line for --help."),
attr (default "app") names the typer.Typer in that module; hidden and
callback map to the same add_typer keyword arguments.
A single top-level command is a LazyCommand naming the function:
"do-thing": LazyCommand("comfy_cli.command.mything", attr="do_thing_cmd", help="One line for --help."),
For the group, create the folder comfy_cli/command/[new_command]/ with this
boilerplate:
comfy_cli/command/[new_command]/__init__.py
from .command import app
comfy_cli/command/[new_command]/command.py
import typer
app = typer.Typer()
@app.command()
def option_a(name: str):
"""Add a new custom node"""
print(f"Adding a new custom node: {name}")
@app.command()
def remove(name: str):
"""Remove a custom node"""
print(f"Removing a custom node: {name}")
Keep heavy imports (torch, psutil, the HTTP client, ...) out of module
scope in the command module, or import them inside the command function.
tests/comfy_cli/test_import_budget.py fails if comfy --version starts
importing them.
Important notes
- Use
typerfor all command args management - Use
richfor all console output- For progress reporting, use either
rich.progress
- For progress reporting, use either
Develop comfy-cli and ComfyUI-Manager (cm-cli) together
ComfyUI-Manager is now installed as a pip package (via manager_requirements.txt
in the ComfyUI root) rather than being git-cloned into custom_nodes/.
Making changes to both
- Fork your own branches of
comfy-cliandComfyUI-Manager, make changes. - Live-install
comfy-cli:pip install -e /path/to/comfy-cli
- Live-install your fork of
ComfyUI-Managerin editable mode:pip install -e /path/to/ComfyUI-Manager
- This makes the
cm-clientry point available and points it at your local source.
Trying changes to both
- Install both packages in editable mode as described above.
- Go to a test dir and run:
comfy --here install
- The
cm-clicommand will resolve to your locally installed editable package.
Debugging both simultaneously
- Follow instructions above to get working install with changes.
- Add breakpoints directly to code:
import ipdb; ipdb.set_trace() - Execute relevant
comfy-clicommand.
Contact
If you have any questions or need further assistance, open an issue or reach us on Discord.
Happy coding!