Full Code of yoland68/comfy-cli for AI

main 0f8298c7ec3c cached
145 files
2.3 MB
615.1k tokens
1845 symbols
1 requests
Download .txt
Showing preview only (2,462K chars total). Download the full file or copy to clipboard to get everything.
Repository: yoland68/comfy-cli
Branch: main
Commit: 0f8298c7ec3c
Files: 145
Total size: 2.3 MB

Directory structure:
gitextract_3k1yj69h/

├── .coveragerc
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── codecov.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── build-and-test.yml
│       ├── publish_package.yml
│       ├── pytest.yml
│       ├── ruff_check.yml
│       ├── run-on-gpu.yml
│       ├── test-mac.yml
│       └── test-windows.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .pylintrc
├── DEV_README.md
├── LICENSE
├── README.md
├── comfy_cli/
│   ├── __init__.py
│   ├── __main__.py
│   ├── cmdline.py
│   ├── command/
│   │   ├── __init__.py
│   │   ├── code_search.py
│   │   ├── custom_nodes/
│   │   │   ├── __init__.py
│   │   │   ├── bisect_custom_nodes.py
│   │   │   ├── cm_cli_util.py
│   │   │   └── command.py
│   │   ├── generate/
│   │   │   ├── __init__.py
│   │   │   ├── adapters.py
│   │   │   ├── app.py
│   │   │   ├── client.py
│   │   │   ├── output.py
│   │   │   ├── poll.py
│   │   │   ├── schema.py
│   │   │   ├── spec/
│   │   │   │   └── openapi.yml
│   │   │   ├── spec.py
│   │   │   └── upload.py
│   │   ├── github/
│   │   │   └── pr_info.py
│   │   ├── install.py
│   │   ├── launch.py
│   │   ├── models/
│   │   │   └── models.py
│   │   ├── pr_command.py
│   │   └── run.py
│   ├── config_manager.py
│   ├── constants.py
│   ├── cuda_detect.py
│   ├── env_checker.py
│   ├── file_utils.py
│   ├── git_utils.py
│   ├── logging.py
│   ├── pr_cache.py
│   ├── registry/
│   │   ├── __init__.py
│   │   ├── api.py
│   │   ├── config_parser.py
│   │   └── types.py
│   ├── resolve_python.py
│   ├── standalone.py
│   ├── tracking.py
│   ├── typing.py
│   ├── ui.py
│   ├── update.py
│   ├── utils.py
│   ├── uv.py
│   ├── workflow_to_api.py
│   └── workspace_manager.py
├── conda.listing.txt
├── docs/
│   ├── DESIGN-uv-compile.md
│   ├── PRD-uv-compile.md
│   └── TESTING-e2e.md
├── pylock.toml
├── pyproject.toml
├── pyrightconfig.json
└── tests/
    ├── comfy_cli/
    │   ├── command/
    │   │   ├── generate/
    │   │   │   ├── __init__.py
    │   │   │   ├── test_adapters.py
    │   │   │   ├── test_app.py
    │   │   │   ├── test_client.py
    │   │   │   ├── test_output.py
    │   │   │   ├── test_poll.py
    │   │   │   ├── test_schema.py
    │   │   │   ├── test_spec.py
    │   │   │   ├── test_upload.py
    │   │   │   └── test_video_poll.py
    │   │   ├── github/
    │   │   │   └── test_pr.py
    │   │   ├── models/
    │   │   │   └── test_models.py
    │   │   ├── nodes/
    │   │   │   ├── test_bisect_custom_nodes.py
    │   │   │   ├── test_node_init.py
    │   │   │   ├── test_node_install.py
    │   │   │   ├── test_pack.py
    │   │   │   └── test_publish.py
    │   │   ├── test_bisect_parse.py
    │   │   ├── test_cm_cli_util.py
    │   │   ├── test_code_search.py
    │   │   ├── test_command.py
    │   │   ├── test_frontend_pr.py
    │   │   ├── test_launch_frontend_pr.py
    │   │   ├── test_manager_gui.py
    │   │   ├── test_npm_help.py
    │   │   └── test_run.py
    │   ├── conftest.py
    │   ├── fixtures/
    │   │   ├── sd15_expected_api.json
    │   │   ├── sd15_object_info.json
    │   │   └── sd15_ui_workflow.json
    │   ├── registry/
    │   │   ├── test_api.py
    │   │   └── test_config_parser.py
    │   ├── test_aria2_download.py
    │   ├── test_cm_cli_python_resolution.py
    │   ├── test_cmdline_python_resolution.py
    │   ├── test_config_manager.py
    │   ├── test_cuda_detect.py
    │   ├── test_cuda_detect_real.py
    │   ├── test_custom_nodes_python_resolution.py
    │   ├── test_env_checker.py
    │   ├── test_file_utils.py
    │   ├── test_global_python_install.py
    │   ├── test_install.py
    │   ├── test_install_python_resolution.py
    │   ├── test_launch_python_resolution.py
    │   ├── test_models_python_resolution.py
    │   ├── test_resolve_python.py
    │   ├── test_standalone.py
    │   ├── test_tracking.py
    │   ├── test_ui.py
    │   ├── test_update.py
    │   ├── test_utils.py
    │   ├── test_workflow_to_api.py
    │   └── test_workspace_manager.py
    ├── e2e/
    │   ├── test_e2e.py
    │   ├── test_e2e_uv_compile.py
    │   └── workflow.json
    ├── test_file_utils_network.py
    └── uv/
        ├── mock_comfy/
        │   ├── custom_nodes/
        │   │   ├── x/
        │   │   │   ├── pyproject.toml
        │   │   │   ├── requirements.txt
        │   │   │   ├── setup.cfg
        │   │   │   └── setup.py
        │   │   ├── y/
        │   │   │   ├── setup.cfg
        │   │   │   └── setup.py
        │   │   └── z/
        │   │       └── setup.py
        │   ├── pyproject.toml
        │   ├── setup.cfg
        │   └── setup.py
        ├── mock_requirements/
        │   ├── core_reqs.txt
        │   ├── x_reqs.txt
        │   └── y_reqs.txt
        ├── test_torch_backend_compile.py
        └── test_uv.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .coveragerc
================================================
[run]
source = comfy_cli
omit = tests/*

[report]
exclude_lines =
    pragma: no cover
    def __repr__
    if self.debug:
    if __name__ == .__main__.:
    raise NotImplementedError
    pass
    except ImportError:
    def parse_args
    @abstractmethod

ignore_errors = True

[html]
directory = coverage_html_report

[xml]
output = coverage.xml


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a bug report to help us improve.
title: ''
labels: bug
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior

**Expected behavior**
A clear and concise description of what you expected to happen.

**Nice to have**
- [ ] Terminal output
- [ ] Screenshots

**Additional context**
Add any other context about the problem here.

================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Submit a feature request for this repo.
title: ''
labels: enhancement
assignees: ''

---

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/codecov.yml
================================================
comment:
  layout: "diff, files"

coverage:
  status:
    project:
      default:
        threshold: 0.1%
    patch: off


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/.github/workflows"
    schedule:
      interval: "monthly"
    open-pull-requests-limit: 1
    groups:
      ci-dependencies:
        patterns:
          - "*"


================================================
FILE: .github/workflows/build-and-test.yml
================================================
name: "Test CLI Tool on Multiple Platforms"
on:
  push:
    branches:
      - main
    paths:
      - "comfy_cli/**"
      - "tests/e2e/**"
      - "!.github/**"
      - "!.coveragerc"
      - "!.gitignore"
  pull_request:
    branches:
      - main
    paths:
      - "comfy_cli/**"
      - "tests/e2e/**"
      - "!.github/**"
      - "!.coveragerc"
      - "!.gitignore"

permissions:
  contents: read

jobs:
  test:
    name: "Run Tests on Multiple Platforms"
    runs-on: ${{ matrix.os }}
    env:
      PYTHONIOENCODING: "utf8"

    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        python-version: ["3.10"]

    steps:
      - name: Check out code
        uses: actions/checkout@v6

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install Dependencies
        run: |
          python -m pip install --upgrade pip
          pip install pytest
          pip install -e .

      - name: Test CUDA auto-detection (skips if no GPU)
        run: |
          pytest tests/comfy_cli/test_cuda_detect_real.py -v

      - name: Test e2e
        env:
          PYTHONPATH: ${{ github.workspace }}
          TEST_E2E: true
        run: |
          pytest tests/e2e

      - name: Test torch backend compilation
        env:
          TEST_TORCH_BACKEND: "true"
        run: |
          pytest tests/uv/test_torch_backend_compile.py -xvs


================================================
FILE: .github/workflows/publish_package.yml
================================================
name: Publish to PyPI

on:
  release:
    types: [ created ]

permissions:
  contents: read

jobs:
  build-n-publish-pypi:
    name: Build and publish Python distributions to PyPI
    runs-on: ubuntu-latest
    permissions:
      id-token: write  # IMPORTANT: this permission is mandatory for trusted publishing
    steps:
      - uses: actions/checkout@v6

      - name: Set up Python 3.12
        uses: actions/setup-python@v6
        with:
          python-version: '3.12'

      - name: Install build and tomlkit dependencies
        run: python -m pip install --upgrade pip build tomlkit

      - name: Extract version from tag
        id: get_version
        run: |
          VERSION="${GITHUB_REF#refs/tags/}"
          VERSION="${VERSION#v}"
          echo "VERSION=${VERSION}" >> $GITHUB_ENV

      - name: Update version in pyproject.toml
        run: |
          python -c "
          import tomlkit
          with open('pyproject.toml', 'r') as f:
              content = tomlkit.load(f)
          content['project']['version'] = '${{ env.VERSION }}'
          with open('pyproject.toml', 'w') as f:
              tomlkit.dump(content, f)
          "

      - name: Build distribution
        run: python -m build --sdist --wheel --outdir dist/

      # - name: Publish distribution to TestPyPI for Validation
      #   uses: pypa/gh-action-pypi-publish@v1.8.14
      #   with:
      #     repository_url: https://test.pypi.org/legacy/

      # - name: Clear pip cache
      #   run: pip cache purge

      # - name: Install Comfy CLI from Test Pypi and Test
      #   run: |
      #     for i in {1..3}; do
      #       pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple comfy-cli==${{env.VERSION}} && break || sleep 5
      #     done
      #     comfy --help

      - name: Publish distribution to Official PyPI
        uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0

  test-pip-installation:
    name: Test Comfy CLI Installation via pip
    needs: build-n-publish-pypi  # This job runs after build-n-publish completes successfully
    runs-on: ubuntu-latest
    steps:
      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: '3.10'

      - name: Extract version from tag
        id: get_version
        run: |
          VERSION="${GITHUB_REF#refs/tags/}"
          VERSION="${VERSION#v}"
          echo "VERSION=${VERSION}" >> $GITHUB_ENV

      - name: Install Comfy CLI via pip and Test
        run: |
          # PyPI's index can lag behind a successful upload by a minute or
          # two, so retry before failing the job.
          for i in 1 2 3 4 5 6 7 8; do
            pip install --no-cache-dir "comfy-cli==${VERSION}" && exit 0
            echo "Attempt $i: package not yet available on PyPI, waiting 15s..."
            sleep 15
          done
          echo "::error::Failed to install comfy-cli==${VERSION} after 8 attempts"
          exit 1

      - name: Test Comfy CLI Help
        run: comfy --help


================================================
FILE: .github/workflows/pytest.yml
================================================
name: Run pytest

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

permissions:
  contents: read
  statuses: write
  pull-requests: write

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: '3.10'  # Follow the min version in pyproject.toml

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install pytest pytest-cov
          pip install -e .

      - name: Run tests
        env:
          PYTHONPATH: ${{ github.workspace }}
        run: |
          pytest --cov=comfy_cli --cov-report=xml .

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          fail_ci_if_error: true
          verbose: true


================================================
FILE: .github/workflows/ruff_check.yml
================================================
name: ruff_check

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

permissions:
  contents: read

jobs:
  ruff_check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: "3.x"
      - name: install ruff
        run: |
          python -m pip install --upgrade pip
          pip install ruff
      - name: lint check and then format check with ruff
        run: |
          ruff check
          ruff format --diff


================================================
FILE: .github/workflows/run-on-gpu.yml
================================================
name: "Test CLI Tool on GPU runners"

on:
  push:
    branches:
      - main
    paths:
      - "comfy_cli/**"
      - "!comfy_cli/test_**"
      - "!.github/**"
      - "!tests/**"
      - "!.coveragerc"
      - "!.gitignore"
  pull_request:
    branches:
      - main
    paths:
      - "comfy_cli/**"
      - "!comfy_cli/test_**"
      - "!.github/**"
      - "!tests/**"
      - "!.coveragerc"
      - "!.gitignore"

permissions:
  contents: read

jobs:
  test-cli-gpu:
    name: "Run Tests on GPU Runners"
    runs-on:
      group: gpu-runners
      labels: ${{ matrix.os }}-x64-gpu #
    strategy:
      fail-fast: false
      matrix:
        os: [linux]

    steps:
      - name: Check out code
        uses: actions/checkout@v6

      - name: Check Nvidia
        run: |
          nvidia-smi

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: 3.12

      - name: Check disk space
        run: |
          df -h

      - name: Install Dependencies
        run: |
          python -m pip install --upgrade pip
          pip install pytest
          pip install -e .

      - name: Test CUDA auto-detection (real hardware)
        run: |
          pytest tests/comfy_cli/test_cuda_detect_real.py -v

      - name: Test e2e
        id: test-e2e
        env:
          PYTHONPATH: ${{ github.workspace }}
          TEST_E2E: true
          TEST_E2E_COMFY_INSTALL_FLAGS: --nvidia
          TEST_E2E_COMFY_LAUNCH_FLAGS_EXTRA: ""
        run: |
          pytest tests/e2e

      - name: Retry test e2e but without gpu
        if: ${{ failure() && steps.test-e2e.conclusion == 'failure' }}
        env:
          PYTHONPATH: ${{ github.workspace }}
          TEST_E2E: true
        run: |
          pytest tests/e2e


================================================
FILE: .github/workflows/test-mac.yml
================================================
name: "Mac Specific Commands"
on:
  pull_request:
    branches:
      - main
    paths:
      - comfy_cli/**

permissions:
  contents: read

jobs:
  test:
    runs-on: macos-latest
    env:
      PYTHONIOENCODING: "utf8"

    steps:
      - name: Check out code
        uses: actions/checkout@v6

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: 3.12

      - name: Install Dependencies
        run: |
          python -m venv venv
          source venv/bin/activate
          python -m pip install --upgrade pip
          pip install -e .
          comfy --skip-prompt --workspace ./ComfyUI install --fast-deps --m-series --skip-manager
          comfy --here launch -- --cpu --quick-test-for-ci


================================================
FILE: .github/workflows/test-windows.yml
================================================
name: "Windows Specific Commands"
on:
  pull_request:
    branches:
      - main
    paths:
      - comfy_cli/**

permissions:
  contents: read

jobs:
  test:
    runs-on: windows-latest
    env:
      PYTHONIOENCODING: "utf8"

    steps:
      - name: Check out code
        uses: actions/checkout@v6

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: 3.12

      - name: Install Dependencies
        run: |
          python -m venv venv
          .\venv\Scripts\Activate.ps1
          Get-Command python
          python -m pip install --upgrade pip
          pip install pytest
          pip install -e .
          comfy --skip-prompt --workspace ./ComfyUI install --fast-deps --nvidia --cuda-version 12.6 --skip-manager
          comfy --here launch -- --cpu --quick-test-for-ci


================================================
FILE: .gitignore
================================================
__pycache__/
*.py[cod]

#COMMON CONFIGs
.DS_Store
.src_port
.webpack_watch.log
*.swp
*.swo
.vscode/settings.json
.idea/
.vscode/
*.code-workspace
.history

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# temporary files created by linting, tests, etc
.pytest_cache/
.ruff_cache/
tests/temp/

venv/

bisect_state.json
python*
cpython*
requirements.compiled
override.txt
.coverage
coverage.xml


================================================
FILE: .pre-commit-config.yaml
================================================
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
    -   id: check-yaml
        exclude: ^tests/.*$
    -   id: check-toml
        exclude: ^tests/.*$
    -   id: end-of-file-fixer
        exclude: >-
          (^.*\.(json|txt)$)|(^tests/.*\.toml$)|(.github/.*TEMPLATE)
    -   id: trailing-whitespace
        exclude: >-
          (^.*\.(json|txt)$)

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.12.4
    hooks:
      # Run the linter.
      - id: ruff
        args: [ --fix ]
      # Run the formatter.
      - id: ruff-format

  - repo: https://github.com/tox-dev/pyproject-fmt
    rev: v2.6.0
    hooks:
      - id: pyproject-fmt
        exclude: ^tests/.*$

  - repo: https://github.com/astral-sh/uv-pre-commit
    rev: 0.8.5
    hooks:
      - id: uv-lock
      - id: uv-export
        args: [ "--output-file=pylock.toml" ]
      - id: uv-sync
        args: [ "--all-extras" ]


================================================
FILE: .pylintrc
================================================
# .pylintrc or pylintrc

[MAIN]
max-line-length=120


================================================
FILE: DEV_README.md
================================================
# Development Guide

This guide provides an overview of how to develop in this repository.

## General guide

1. Clone the repo, create and activate a conda env. Minimum Python version is 3.9.

2. Install the package to your conda env.

`pip install -e .`

3. Set ENVIRONMENT variable to DEV.

`export ENVIRONMENT=dev`

4. Check if the "comfy" package can run.

`comfy --help`

5. Install the pre-commit hook to ensure that your code won't need reformatting later.

`pre-commit install`

6. To save time during code review, it's recommended that you also manually run
   the unit tests before submitting a pull request (see below).

## Running the unit tests

1. Install pytest into your conda env. You should preferably be using Python 3.9
   in your conda env, since it's the version we are targeting for compatibility.

`pip install pytest pytest-cov`

2. Verify that all unit tests run successfully.

`pytest --cov=comfy_cli --cov-report=xml .`

## Debugging

You can add following config to your VSCode `launch.json` to launch debugger.

```json
{
  "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 conda
env and reinstalling the package (`pip install -e .`)

## 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 `.comfyignore` is present the command falls back to the original
  behavior and zips every git-tracked file.

Example `.comfyignore`:

```gitignore
docs/
frontend/
tests/
*.psd
```

Commit the file alongside your node so teammates and CI pipelines produce the
same trimmed package.

## Adding a new command

- Register it under `comfy_cli/cmdline.py`

If it's contains subcommand, create folder under comfy_cli/command/[new_command] and
add the following 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}")

```

## Important notes

- Use `typer` for all command args management
- Use `rich` for all console output
  - For progress reporting, use either [`rich.progress`](https://rich.readthedocs.io/en/stable/progress.html)

## 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
1. Fork your own branches of `comfy-cli` and `ComfyUI-Manager`, make changes.
2. Live-install `comfy-cli`:
   - `pip install -e /path/to/comfy-cli`
3. Live-install your fork of `ComfyUI-Manager` in editable mode:
   - `pip install -e /path/to/ComfyUI-Manager`
4. This makes the `cm-cli` entry point available and points it at your local source.

### Trying changes to both
1. Install both packages in editable mode as described above.
2. Go to a test dir and run:
   - `comfy --here install`
3. The `cm-cli` command will resolve to your locally installed editable package.

### Debugging both simultaneously
1. Follow instructions above to get working install with changes.
2. Add breakpoints directly to code: `import ipdb; ipdb.set_trace()`
3. Execute relevant `comfy-cli` command.


## 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.

```bash
TEST_E2E=true pytest tests/e2e/
```

For pre-release testing against alternate ComfyUI repositories (e.g. Manager v4):

```bash
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](docs/TESTING-e2e.md) for the full guide including
environment variables, test suite details, and scenario descriptions.

## Contact

If you have any questions or need further assistance, please contact the project maintainer at [???](mailto:???@drip.art).

Happy coding!


================================================
FILE: LICENSE
================================================
                    GNU GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU General Public License is a free, copyleft license for
software and other kinds of works.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.  We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors.  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights.  Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received.  You must make sure that they, too, receive
or can get the source code.  And you must show them these terms so they
know their rights.

  Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.

  For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software.  For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.

  Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so.  This is fundamentally incompatible with the aim of
protecting users' freedom to change the software.  The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable.  Therefore, we
have designed this version of the GPL to prohibit the practice for those
products.  If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.

  Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary.  To prevent this, the GPL assures that
patents cannot be used to render the program non-free.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Use with the GNU Affero General Public License.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

  If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:

    <program>  Copyright (C) <year>  <name of author>
    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.

  The GNU General Public License does not permit incorporating your program
into proprietary programs.  If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.  But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.


================================================
FILE: README.md
================================================
# comfy-cli: A Command Line Tool for ComfyUI

[![Run pytest](https://github.com/Comfy-Org/comfy-cli/actions/workflows/pytest.yml/badge.svg)](https://github.com/Comfy-Org/comfy-cli/actions/workflows/pytest.yml)
[![codecov](https://codecov.io/github/Comfy-Org/comfy-cli/graph/badge.svg?token=S64WJWD2ZX)](https://codecov.io/github/Comfy-Org/comfy-cli)
[![PyPI](https://img.shields.io/pypi/v/comfy-cli.svg)](https://pypi.org/project/comfy-cli/)
[![Downloads](https://static.pepy.tech/badge/comfy-cli/month)](https://pepy.tech/project/comfy-cli)
[![Python](https://img.shields.io/pypi/pyversions/comfy-cli)](https://pypi.org/project/comfy-cli/)
[![License](https://img.shields.io/pypi/l/comfy-cli)](https://github.com/Comfy-Org/comfy-cli/blob/main/LICENSE)

comfy-cli is a command-line tool for installing, running, and extending
[ComfyUI](https://github.com/comfyanonymous/ComfyUI) — the open-source
generative-media engine. Set up ComfyUI, install custom nodes and models, run
workflows, and call hosted partner image models, all from your terminal.

## Demo

<img src="https://github.com/yoland68/comfy-cli/raw/main/assets/comfy-demo.gif" width="400" alt="Comfy Command Demo">

## Features

- 🚀 One-command ComfyUI install and launch
- 🎨 Direct calls to partner image and video nodes (Flux, Ideogram, DALL·E, Recraft, Stability, Gemini/nano-banana, Kling, Luma, Runway, Pika, Vidu, Hailuo, Seedance, …) via `comfy generate`, no workflow JSON required
- 🔧 Custom node management — install, update, snapshot, bisect
- 📦 Fast dependency resolution with `uv` (`--fast-deps`, `--uv-compile`)
- 🗄️ Model downloads from CivitAI, Hugging Face, and direct URLs
- 🎬 Run workflows against a local ComfyUI server, including auto-conversion of UI-format JSON
- 🧪 Test ComfyUI and frontend pull requests with one flag
- 💻 Cross-platform: Windows, macOS, Linux

## Installation

1. (Recommended) Activate a virtual environment ([venv](https://docs.python.org/3/library/venv.html) or [conda](https://conda.io/projects/conda/en/latest/user-guide/getting-started.html)).

2. Install with `pip` (requires Python 3.10+):

   ```bash
   pip install comfy-cli
   ```

### Shell Autocomplete

Install shell completion so `comfy <TAB>` expands commands and options:

```bash
comfy --install-completion
```

## Usage

### Installing ComfyUI

To install ComfyUI using comfy, simply run:

`comfy install`

This command will download and set up the latest version of ComfyUI and ComfyUI-Manager on your
system. If you run in a ComfyUI repo that has already been setup. The command
will simply update the comfy.yaml file to reflect the local setup

- `comfy install --skip-manager`: Install ComfyUI without ComfyUI-Manager.
  To use a custom Manager fork or specific version, skip the default installation
  and install your own into the workspace venv:
  ```bash
  comfy install --skip-manager
  # Then install your custom Manager:
  pip install -e /path/to/your-manager-fork   # editable install
  # or
  pip install comfyui-manager==4.1b8          # specific version
  ```
- `comfy --workspace=<path> install`: Install ComfyUI into `<path>/ComfyUI`.
- `comfy install --fast-deps`: Use `uv` instead of `pip` for faster dependency resolution
  during initial ComfyUI installation. comfy-cli's built-in resolver compiles all requirements (core + custom nodes)
  into a single lockfile and installs from it. Also handles GPU-specific PyTorch wheel selection automatically.
- For `comfy install`, if no path specification like `--workspace, --recent, or --here` is provided, it will be implicitly installed in `<HOME>/comfy`.

#### Python environment handling

When you run `comfy install`, comfy-cli picks a Python environment for ComfyUI
dependencies using the following precedence:

1. An **active virtualenv or conda** environment (`VIRTUAL_ENV` / `CONDA_PREFIX`) is used as-is.
2. An **existing `.venv` or `venv`** directory inside the workspace is reused.
3. Otherwise the choice depends on how comfy-cli was installed:
   - **`pip install comfy-cli`** (global / system Python): dependencies go
     directly into the same Python environment. This is the typical Docker setup.
   - **`pipx install comfy-cli`** or **`uv tool install comfy-cli`** (isolated
     tool environment): a `.venv` is created inside the ComfyUI workspace.
     Use `comfy launch` to start ComfyUI with the correct Python.

### Specifying execution path

- You can specify the path of ComfyUI where the command will be applied through path indicators as follows:
  - `comfy --workspace=<path>`: Run from the ComfyUI installed in the specified workspace.
  - `comfy --recent`: Run from the recently executed or installed ComfyUI.
  - `comfy --here`: Run from the ComfyUI located in the current directory.
- --workspace, --recent, and --here options cannot be used simultaneously.
- If there is no path indicator, the following priority applies:

  - Run from the default ComfyUI at the path specified by `comfy set-default <path>`.
  - Run from the recently executed or installed ComfyUI.
  - Run from the ComfyUI located in the current directory.

- Example 1: To run the recently executed ComfyUI:
  - `comfy --recent launch`
- Example 2: To install a package on the ComfyUI in the current directory:
  - `comfy --here node install comfyui-impact-pack`
- Example 3: To update the automatically selected path of ComfyUI and custom nodes based on priority:

  - `comfy node update all`

- You can use the `comfy which` command to check the path of the target workspace.
  - e.g `comfy --recent which`, `comfy --here which`, `comfy which`, ...

### Default Setup

The default sets the option that will be executed by default when no specific workspace's ComfyUI has been set for the command.

`comfy set-default <workspace path> ?[--launch-extras="<extra args>"]`

- `--launch-extras` option specifies extra args that are applied only during launch by default. However, if extras are specified at the time of launch, this setting is ignored.

### Launch ComfyUI

Comfy provides commands that allow you to easily run the installed ComfyUI.

`comfy launch`

- To run with default ComfyUI options:

  `comfy launch -- <extra args...>`

  `comfy launch -- --cpu --listen 0.0.0.0`

  - When you manually configure the extra options, the extras set by set-default will be overridden.

- To run background

  `comfy launch --background`

  `comfy --workspace=~/comfy launch --background -- --listen 10.0.0.10 --port 8000`

  - Instances launched with `--background` are displayed in the "Background ComfyUI" section of `comfy env`, providing management functionalities for a single background instance only.
  - Since "Comfy Server Running" in `comfy env` only shows the default port 8188, it doesn't display ComfyUI running on a different port.
  - Background-running ComfyUI can be stopped with `comfy stop`.

- to run ComfyUI with a specific pull request:

  `comfy install --pr "#1234"`

  `comfy install --pr "jtydhr88:load-3d-nodes"`

  `comfy install --pr "https://github.com/comfyanonymous/ComfyUI/pull/1234"`

  - If you want to run ComfyUI with a specific pull request, you can use the `--pr` option. This will automatically install the specified pull request and run ComfyUI with it.
  - Important: The --pr option cannot be combined with --version or --commit and will be rejected if used together.

- To test a frontend pull request:

  ```
  comfy launch --frontend-pr "#456"
  comfy launch --frontend-pr "username:branch-name"
  comfy launch --frontend-pr "https://github.com/Comfy-Org/ComfyUI_frontend/pull/456"
  ```

  - The `--frontend-pr` option allows you to test frontend PRs by automatically cloning, building, and using the frontend for that session.
  - Requirements: Node.js and npm must be installed to build the frontend.
  - Builds are cached for quick switching between PRs - subsequent uses of the same PR are instant.
  - Each PR is used only for that launch session. Normal launches use the default frontend.

  **Managing PR cache**:
  ```
  comfy pr-cache list              # List cached PR builds
  comfy pr-cache clean             # Clean all cached builds
  comfy pr-cache clean 456         # Clean specific PR cache
  ```

  - Cache automatically expires after 7 days
  - Maximum of 10 PR builds are kept (oldest are removed automatically)
  - Cache limits help manage disk space while keeping recent builds available

### Managing Custom Nodes

comfy provides a convenient way to manage custom nodes for extending ComfyUI's functionality. Here are some examples:

- Show custom nodes' information:

```
comfy node [show|simple-show] [installed|enabled|not-installed|disabled|all|snapshot|snapshot-list]
                             ?[--channel <channel name>]
                             ?[--mode [remote|local|cache]]
```

- `comfy node show all --channel recent`

  `comfy node simple-show installed`

  `comfy node update all`

  `comfy node install comfyui-impact-pack`

- Managing snapshot:

  `comfy node save-snapshot`

  `comfy node restore-snapshot <snapshot name>`

- Install dependencies:

  `comfy node install-deps --deps=<deps .json file>`

  `comfy node install-deps --workflow=<workflow .json/.png file>`

- Generate deps:

  `comfy node deps-in-workflow --workflow=<workflow .json/.png file> --output=<output deps .json file>`

#### Unified Dependency Resolution (--uv-compile)

Requires ComfyUI-Manager v4.1+. Instead of installing dependencies per-node with
`pip install`, `--uv-compile` delegates to ComfyUI-Manager's unified resolver which batch-resolves
all custom node dependencies via `uv pip compile` with **cross-node conflict detection** —
it can identify which node packs have incompatible dependencies and why.

- Install with unified resolution:

  `comfy node install comfyui-impact-pack --uv-compile`

- Available on: `install`, `reinstall`, `update`, `fix`, `restore-snapshot`,
  `restore-dependencies`, `install-deps`

- Run standalone (resolve all existing custom node dependencies):

  `comfy node uv-sync`

- `--uv-compile` is mutually exclusive with `--fast-deps` and `--no-deps`.

- To make `--uv-compile` the default for all commands, see
  [uv-compile default](#uv-compile-default) below.

- Use `--no-uv-compile` to override the default for a single command:

  `comfy node install comfyui-impact-pack --no-uv-compile`

#### --fast-deps vs --uv-compile

Both flags use `uv` for faster dependency resolution, but they work differently:

|                       | `--fast-deps`                                   | `--uv-compile`                                |
|-----------------------|-------------------------------------------------|-----------------------------------------------|
| **Resolver**          | comfy-cli built-in (`DependencyCompiler`)       | ComfyUI-Manager (`UnifiedDepResolver`)        |
| **Scope**             | `comfy install`, `comfy node install/reinstall` | Custom node commands only                     |
| **Conflict handling** | Interactive prompt to pick a version            | Automatic detection with node attribution     |
| **Config default**    | No                                              | Yes (`comfy manager uv-compile-default true`) |
| **Requires**          | Only `uv`                                       | ComfyUI-Manager v4.1+                         |

**When to use which:**
- For initial ComfyUI installation with uv: `comfy install --fast-deps`
- For custom node management with Manager v4.1+: `--uv-compile` (recommended)
- For custom node management with older Manager: `--fast-deps`

#### Bisect custom nodes

If you encounter bugs only with custom nodes enabled, and want to find out which custom node(s) causes the bug,
the bisect tool can help you pinpoint the custom node that causes the issue.

- `comfy node bisect start`: Start a new bisect session with optional ComfyUI launch args. It automatically marks the starting state as bad, and takes all enabled nodes when the command executes as the test set.
- `comfy node bisect good`: Mark the current active set as good, indicating the problem is not within the test set.
- `comfy node bisect bad`: Mark the current active set as bad, indicating the problem is within the test set.
- `comfy node bisect reset`: Reset the current bisect session.

### Managing Models

- Model downloading

  `comfy model download --url <URL> ?[--relative-path <PATH>] ?[--set-civitai-api-token <TOKEN>] ?[--set-hf-api-token <TOKEN>]`

  - URL: CivitAI page, Hugging Face file URL, etc...
  - You can also specify your API tokens via the `CIVITAI_API_TOKEN` and `HF_API_TOKEN` environment variables. The order of priority is `--set-X-token` (always highest priority), then the environment variables if they exist, and lastly your config's stored tokens from previous `--set-X-token` usage (which remembers your most recently set token values).
  - Tokens provided via the environment variables are never stored persistently in your config file. They are intended as a way to easily and safely provide transient secrets.

- Model remove

  `comfy model remove ?[--relative-path <PATH>] --model-names <model names>`

- Model list

  `comfy model list ?[--relative-path <PATH>]`

### Calling partner nodes (`comfy generate`)

`comfy generate` calls Comfy's partner nodes directly from the terminal — no
local ComfyUI or workflow JSON required. It hits the same hosted partner nodes
you'd otherwise wire into a ComfyUI workflow, but as one-shot CLI calls. Image
models (Flux, Ideogram, DALL·E, Recraft, Stability, Runway, Reve, xAI Grok,
Google Gemini Flash Image aka **nano-banana**, …) and video models (Kling,
Luma, Runway Gen-3, Pika, Vidu, Moonvalley, Hailuo, Grok video, ByteDance
**Seedance**) are all covered; video jobs run async and the CLI polls until
the result is ready.

Prerequisites — a Comfy API key and a credit balance:

- [Create an API key](https://docs.comfy.org/development/comfyui-server/api-key-integration)
- [Browse partner nodes and per-call credit costs](https://docs.comfy.org/tutorials/partner-nodes/overview) · [pricing table](https://docs.comfy.org/tutorials/partner-nodes/pricing)
- [Add credits](https://docs.comfy.org/interface/credits)

Set the key once, then go:

```bash
export COMFY_API_KEY=comfyui-...   # or pass --api-key on each call

comfy generate list                                  # browse available models
comfy generate schema flux-pro                       # see params for one model
comfy generate flux-pro --prompt "a cat on the moon" \
    --width 1024 --height 1024 --download cat.png
```

Reference images can be passed as local paths — the CLI uploads them through
the cloud's storage endpoint (or base64-encodes inline, as each partner
requires):

```bash
comfy generate flux-kontext --prompt "add a top hat" \
    --input_image ./photo.jpg --download out.png

comfy generate upload ./photo.jpg                    # explicit upload
```

Async models (every video model plus the Flux family) block until ready by
default. Pass `--async` to return immediately with a job id, then resume later
with `comfy generate resume <model> <job_id>`. Examples:

```bash
comfy generate kling --prompt "a paper boat drifting on a river at dusk" \
    --duration 5 --download boat.mp4

comfy generate luma --prompt "..." --aspect_ratio 16:9 --async
# → prints job id; resume with:
comfy generate resume luma <job_id> --download out.mp4
```

**Gemini Flash Image (nano-banana)** — text-to-image and image edits in one
alias. Pass `--image` (repeatable) for reference images. The response is
inline base64, so `--download` is required to save:

```bash
comfy generate nano-banana --prompt "a watercolor of a sleeping fox" \
    --download fox.png

# Image edit — reference accepted as a local path, http(s) URL, or data URI:
comfy generate nano-banana --prompt "add a top hat" \
    --image ./cat.png --download out.png

# Switch model variants:
comfy generate nano-banana --prompt "..." --model gemini-3-pro-image-preview \
    --download out.png
```

**Seedance** — text-to-video and image-to-video, up to 1080p / 12s clips.
Resolution, ratio, duration, fps, etc. get passed through as flags; the CLI
inlines them into Seedance's prompt syntax for you:

```bash
comfy generate seedance --prompt "a hummingbird hovering over a flower" \
    --resolution 1080p --duration 5 --download bird.mp4

# Image-to-video: pick a lite/i2v variant and pass a first frame.
comfy generate seedance --model seedance-1-0-lite-i2v-250428 \
    --prompt "the wave crests and crashes" \
    --image ./still.jpg --download wave.mp4
```

### Managing ComfyUI-Manager

- Disable ComfyUI-Manager completely (no manager flags passed to ComfyUI):

  `comfy manager disable`

- Enable ComfyUI-Manager with new GUI:

  `comfy manager enable-gui`

- Enable ComfyUI-Manager without GUI (manager runs but UI is hidden):

  `comfy manager disable-gui`

- Enable ComfyUI-Manager with legacy GUI:

  `comfy manager enable-legacy-gui`

- Clear reserved startup action:

  `comfy manager clear`

- Migrate legacy git-cloned ComfyUI-Manager to pip package:

  `comfy manager migrate-legacy`

#### uv-compile default

Set `--uv-compile` as the default behavior for all custom node operations:

  `comfy manager uv-compile-default true`

When enabled, all node commands (`install`, `reinstall`, `update`, `fix`,
`restore-snapshot`, `restore-dependencies`, `install-deps`) will automatically
use `--uv-compile`. Use `--no-uv-compile` on any individual command to override.

To disable:

  `comfy manager uv-compile-default false`

## Beta Feature: format of comfy-lock.yaml (WIP)

```
basic:

models:
  - model: [name of the model]
    url: [url of the source, e.g. https://huggingface.co/...]
    paths: [list of paths to the model]
      - path: [path to the model]
      - path: [path to the model]
    hashes: [hashes for the model]
      - hash: [hash]
        type: [AutoV1, AutoV2, SHA256, CRC32, and Blake3]
    type: [type of the model, e.g. diffuser, lora, etc.]

  - model:
  ...

# compatible with ComfyUI-Manager's .yaml snapshot
custom_nodes:
  comfyui: [commit hash]
  file_custom_nodes:
  - disabled: [bool]
    filename: [.py filename]
    ...
  git_custom_nodes:
    [git-url]:
      disabled: [bool]
      hash: [commit hash]
    ...
```

## Analytics

We track analytics using Mixpanel to help us understand usage patterns and know where to prioritize our efforts. When you first download the cli, it will ask you to give consent. If at any point you wish to opt out:

```
comfy tracking disable
```

Check out the usage here: [Mixpanel Board](https://mixpanel.com/p/13hGfPfEPdRkjPtNaS7BYQ)

## Contributing

We welcome contributions to comfy-cli! For ideas, suggestions, or bug reports,
open an issue at [Comfy-Org/comfy-cli](https://github.com/Comfy-Org/comfy-cli/issues).
For code changes, fork the repo and open a pull request.

See the [Dev Guide](/DEV_README.md) for setup details.

## License

Released under the [GNU General Public License v3.0](https://github.com/Comfy-Org/comfy-cli/blob/main/LICENSE).

## Support

Questions or issues? [Open an issue](https://github.com/Comfy-Org/comfy-cli/issues)
or reach us on [Discord](https://discord.com/invite/comfyorg).

Happy diffusing with ComfyUI and comfy-cli! 🎉


================================================
FILE: comfy_cli/__init__.py
================================================


================================================
FILE: comfy_cli/__main__.py
================================================
from comfy_cli.cmdline import main

if __name__ == "__main__":  # pragma: nocover
    main()


================================================
FILE: comfy_cli/cmdline.py
================================================
import os
import subprocess
import sys
import webbrowser
from typing import Annotated

import questionary
import typer
from rich import print as rprint
from rich.console import Console

from comfy_cli import constants, env_checker, logging, tracking, ui, utils
from comfy_cli.command import code_search, custom_nodes, pr_command
from comfy_cli.command import generate as generate_command
from comfy_cli.command import install as install_inner
from comfy_cli.command import run as run_inner
from comfy_cli.command.install import validate_version
from comfy_cli.command.launch import launch as launch_command
from comfy_cli.command.models import models as models_command
from comfy_cli.config_manager import ConfigManager
from comfy_cli.constants import GPU_OPTION, CUDAVersion, ROCmVersion
from comfy_cli.cuda_detect import DEFAULT_CUDA_TAG, detect_cuda_driver_version, resolve_cuda_wheel
from comfy_cli.env_checker import EnvChecker
from comfy_cli.resolve_python import resolve_workspace_python
from comfy_cli.standalone import StandalonePython
from comfy_cli.update import check_for_updates
from comfy_cli.uv import DependencyCompiler
from comfy_cli.workspace_manager import WorkspaceManager, check_comfy_repo

logging.setup_logging()
app = typer.Typer()
workspace_manager = WorkspaceManager()

console = Console()


def main():
    app()


class MutuallyExclusiveValidator:
    def __init__(self):
        self.group = []

    def reset_for_testing(self):
        self.group.clear()

    def validate(self, _ctx: typer.Context, param: typer.CallbackParam, value: str):
        # Add cli option to group if it was called with a value
        if value is not None and param.name not in self.group:
            self.group.append(param.name)
        if len(self.group) > 1:
            raise typer.BadParameter(f"option `{param.name}` is mutually exclusive with option `{self.group.pop()}`")
        return value


g_exclusivity = MutuallyExclusiveValidator()
g_gpu_exclusivity = MutuallyExclusiveValidator()


@app.command(help="Display help for commands")
def help(ctx: typer.Context):
    rprint(ctx.find_root().get_help())
    ctx.exit(0)


@app.callback(invoke_without_command=True)
def entry(
    ctx: typer.Context,
    workspace: Annotated[
        str | None,
        typer.Option(
            show_default=False,
            help="Path to ComfyUI workspace",
            callback=g_exclusivity.validate,
        ),
    ] = None,
    recent: Annotated[
        bool | None,
        typer.Option(
            show_default=False,
            help="Execute from recent path",
            callback=g_exclusivity.validate,
        ),
    ] = None,
    here: Annotated[
        bool | None,
        typer.Option(
            show_default=False,
            help="Execute from current path",
            callback=g_exclusivity.validate,
        ),
    ] = None,
    skip_prompt: Annotated[
        bool,
        typer.Option(
            show_default=False,
            help="Do not prompt user for input, use default options",
        ),
    ] = False,
    enable_telemetry: Annotated[
        bool,
        typer.Option(
            show_default=False,
            hidden=True,
            help="Enable tracking",
        ),
    ] = False,
    version: bool = typer.Option(
        False,
        "--version",
        "-v",
        help="Print version and exit",
    ),
):
    if version:
        rprint(ConfigManager().get_cli_version())
        ctx.exit(0)

    workspace_manager.setup_workspace_manager(workspace, here, recent, skip_prompt)

    tracking.prompt_tracking_consent(skip_prompt, default_value=enable_telemetry)

    if ctx.invoked_subcommand is None:
        rprint("[bold yellow]Welcome to Comfy CLI![/bold yellow]: https://github.com/Comfy-Org/comfy-cli")
        rprint(ctx.get_help())
        ctx.exit()

    # TODO: Move this to proper place
    # start_time = time.time()
    # workspace_manager.scan_dir()
    # end_time = time.time()
    #
    # logging.info(f"scan_dir took {end_time - start_time:.2f} seconds to run")


def validate_commit_and_version(commit: str | None, ctx: typer.Context) -> str | None:
    """
    Validate that the commit is not specified unless the version is 'nightly'.
    """
    version = ctx.params.get("version")
    if commit and version != "nightly":
        raise typer.BadParameter("You can only specify the commit if the version is 'nightly'.")
    return commit


def _resolve_cuda(
    gpu: GPU_OPTION | None,
    cuda_version: CUDAVersion | None,
) -> tuple[CUDAVersion | None, str | None]:
    """Resolve the CUDA wheel tag for an NVIDIA install.

    Returns (cuda_version_enum_or_None, cuda_tag_string_or_None).
    When the user passed an explicit --cuda-version, that is used as-is.
    Otherwise auto-detection is attempted.
    """
    if gpu != GPU_OPTION.NVIDIA:
        return cuda_version, None

    if cuda_version is not None:
        tag = f"cu{cuda_version.value.replace('.', '')}"
        rprint(f"[bold]Using explicit CUDA version:[/bold] {cuda_version.value} ({tag})")
        return cuda_version, tag

    drv = detect_cuda_driver_version()
    if drv is not None:
        tag = resolve_cuda_wheel(drv)
        if tag is not None:
            rprint(f"[bold green]Detected CUDA driver version:[/bold green] {drv[0]}.{drv[1]} → using {tag}")
            return None, tag
        rprint(
            f"[bold yellow]Warning:[/bold yellow] CUDA driver {drv[0]}.{drv[1]} is too old for any known PyTorch wheel. "
            f"Falling back to {DEFAULT_CUDA_TAG}. Use `--cuda-version` to override."
        )
        return None, DEFAULT_CUDA_TAG

    rprint(
        f"[bold yellow]Warning:[/bold yellow] Could not detect CUDA driver version. "
        f"Falling back to {DEFAULT_CUDA_TAG}. Use `--cuda-version` to override."
    )
    return None, DEFAULT_CUDA_TAG


@app.command(help="Download and install ComfyUI and ComfyUI-Manager")
@tracking.track_command()
def install(
    url: Annotated[
        str,
        typer.Option(
            show_default=False,
            help="url or local path pointing to the ComfyUI core git repo to be installed. A specific branch can optionally be specified using a setuptools-like syntax, eg https://foo.git@bar",
        ),
    ] = constants.COMFY_GITHUB_URL,
    version: Annotated[
        str,
        typer.Option(
            show_default=False,
            help="Specify version of ComfyUI to install. Default is nightl, which is the latest commit on master branch. Other options include: latest, which is the latest stable release. Or a specific version number, eg. 0.2.0",
            callback=validate_version,
        ),
    ] = "nightly",
    restore: Annotated[
        bool,
        typer.Option(
            show_default=False,
            help="Restore dependencies for installed ComfyUI if not installed",
        ),
    ] = False,
    skip_manager: Annotated[
        bool,
        typer.Option(show_default=False, help="Skip installing the manager component"),
    ] = False,
    skip_torch_or_directml: Annotated[
        bool,
        typer.Option(show_default=False, help="Skip installing PyTorch Or DirectML"),
    ] = False,
    skip_requirement: Annotated[
        bool, typer.Option(show_default=False, help="Skip installing requirements.txt")
    ] = False,
    nvidia: Annotated[
        bool | None,
        typer.Option(
            show_default=False,
            help="Install for Nvidia gpu",
            callback=g_gpu_exclusivity.validate,
        ),
    ] = None,
    cuda_version: Annotated[CUDAVersion | None, typer.Option(show_default=False)] = None,
    rocm_version: Annotated[ROCmVersion, typer.Option(show_default=True)] = ROCmVersion.v6_3,
    amd: Annotated[
        bool | None,
        typer.Option(
            show_default=False,
            help="Install for AMD gpu",
            callback=g_gpu_exclusivity.validate,
        ),
    ] = None,
    m_series: Annotated[
        bool | None,
        typer.Option(
            show_default=False,
            help="Install for Mac M-Series gpu",
            callback=g_gpu_exclusivity.validate,
        ),
    ] = None,
    intel_arc: Annotated[
        bool | None,
        typer.Option(
            hidden=True,
            show_default=False,
            help="Install for Intel Arc gpu",
            callback=g_gpu_exclusivity.validate,
        ),
    ] = None,
    cpu: Annotated[
        bool | None,
        typer.Option(
            show_default=False,
            help="Install for CPU",
            callback=g_gpu_exclusivity.validate,
        ),
    ] = None,
    commit: Annotated[
        str | None, typer.Option(help="Specify commit hash for ComfyUI", callback=validate_commit_and_version)
    ] = None,
    fast_deps: Annotated[
        bool,
        typer.Option(
            "--fast-deps",
            show_default=False,
            help="Use uv instead of pip for dependency resolution (comfy-cli built-in resolver)",
        ),
    ] = False,
    pr: Annotated[
        str | None,
        typer.Option(
            show_default=False,
            help="Install from a specific PR. Supports formats: username:branch, #123, or PR URL",
        ),
    ] = None,
):
    check_for_updates()
    checker = EnvChecker()

    comfy_path, _ = workspace_manager.get_workspace_path()

    is_comfy_installed_at_path, resolved_path = check_comfy_repo(comfy_path)
    if is_comfy_installed_at_path and not restore:
        rprint(f"[bold red]ComfyUI is already installed at the specified path:[/bold red] {comfy_path}\n")
        rprint(
            "[bold yellow]If you want to restore dependencies, add the '--restore' option.[/bold yellow]",
        )
        raise typer.Exit(code=1)

    if resolved_path is not None:
        comfy_path = resolved_path

    if checker.python_version.major < 3 or checker.python_version.minor < 9:
        rprint("[bold red]Python version 3.9 or higher is required to run ComfyUI.[/bold red]")
        rprint(f"You are currently using Python version {env_checker.format_python_version(checker.python_version)}.")
    platform = utils.get_os()

    if pr and (version not in {None, "nightly"} or commit):
        rprint("--pr cannot be used with --version or --commit")
        raise typer.Exit(code=1)

    if cpu:
        rprint("[bold yellow]Installing for CPU[/bold yellow]")
        install_inner.execute(
            url,
            comfy_path,
            restore,
            skip_manager,
            commit=commit,
            version=version,
            gpu=None,
            cuda_version=cuda_version,
            cuda_tag=None,
            rocm_version=rocm_version,
            plat=platform,
            skip_torch_or_directml=skip_torch_or_directml,
            skip_requirement=skip_requirement,
            fast_deps=fast_deps,
            pr=pr,
        )
        rprint(f"ComfyUI is installed at: {comfy_path}")
        return None

    if nvidia and platform == constants.OS.MACOS:
        rprint("[bold red]Nvidia GPU is never on MacOS. What are you smoking? 🤔[/bold red]")
        raise typer.Exit(code=1)

    if platform != constants.OS.MACOS and m_series:
        rprint(f"[bold red]You are on {platform} bruh [/bold red]")

    gpu = None

    if nvidia:
        gpu = GPU_OPTION.NVIDIA
    elif amd:
        gpu = GPU_OPTION.AMD
    elif m_series:
        gpu = GPU_OPTION.MAC_M_SERIES
    elif intel_arc:
        gpu = GPU_OPTION.INTEL_ARC
    else:
        if platform == constants.OS.MACOS:
            gpu = ui.prompt_select_enum(
                "What type of Mac do you have?",
                [GPU_OPTION.MAC_M_SERIES, GPU_OPTION.MAC_INTEL],
            )
        else:
            gpu = ui.prompt_select_enum(
                "What GPU do you have?",
                [GPU_OPTION.NVIDIA, GPU_OPTION.AMD, GPU_OPTION.INTEL_ARC],
            )

    if gpu is None and not cpu:
        rprint(
            "[bold red]No GPU option selected or `--cpu` enabled, use --\\[gpu option] flag (e.g. --nvidia) to pick GPU. use `--cpu` to install for CPU. Exiting...[/bold red]"
        )
        raise typer.Exit(code=1)

    cuda_version, cuda_tag = _resolve_cuda(gpu, cuda_version) if not skip_torch_or_directml else (cuda_version, None)

    install_inner.execute(
        url,
        comfy_path,
        restore,
        skip_manager,
        commit=commit,
        gpu=gpu,
        version=version,
        cuda_version=cuda_version,
        cuda_tag=cuda_tag,
        rocm_version=rocm_version,
        plat=platform,
        skip_torch_or_directml=skip_torch_or_directml,
        skip_requirement=skip_requirement,
        fast_deps=fast_deps,
        pr=pr,
    )

    rprint(f"ComfyUI is installed at: {comfy_path}")


@app.command(help="Update ComfyUI Environment [all|comfy]")
@tracking.track_command()
def update(
    target: str = typer.Argument(
        "comfy",
        help="[all|comfy]",
        autocompletion=utils.create_choice_completer(["all", "comfy"]),
    ),
):
    if target not in ["all", "comfy"]:
        typer.echo(
            f"Invalid target: {target}. Allowed targets are 'all', 'comfy'.",
            err=True,
        )
        raise typer.Exit(code=1)

    comfy_path = workspace_manager.workspace_path

    if "all" == target:
        custom_nodes.command.execute_cm_cli(["update", "all"])
    else:
        rprint(f"Updating ComfyUI in {comfy_path}...")
        if comfy_path is None:
            rprint("ComfyUI path is not found.")
            raise typer.Exit(code=1)
        os.chdir(comfy_path)
        subprocess.run(["git", "pull"], check=True)
        python = resolve_workspace_python(comfy_path)
        subprocess.run(
            [python, "-m", "pip", "install", "-r", "requirements.txt"],
            check=True,
        )

    try:
        custom_nodes.command.update_node_id_cache()
    except (FileNotFoundError, subprocess.CalledProcessError) as e:
        rprint(f"[yellow]Failed to update node id cache: {e}[/yellow]")


@app.command(help="Run API workflow file using the ComfyUI launched by `comfy launch --background`")
@tracking.track_command()
def run(
    workflow: Annotated[str, typer.Option(help="Path to the workflow API json file.")],
    wait: Annotated[
        bool,
        typer.Option(help="If the command should wait until execution completes."),
    ] = True,
    verbose: Annotated[
        bool,
        typer.Option(help="Enables verbose output of the execution process."),
    ] = False,
    host: Annotated[
        str | None,
        typer.Option(help="The IP/hostname where the ComfyUI instance is running, e.g. 127.0.0.1 or localhost."),
    ] = None,
    port: Annotated[
        int | None,
        typer.Option(help="The port where the ComfyUI instance is running, e.g. 8188."),
    ] = None,
    timeout: Annotated[
        int | None,
        typer.Option(help="The timeout in seconds for the workflow execution."),
    ] = 30,
    api_key: Annotated[
        str | None,
        typer.Option(
            "--api-key",
            envvar="COMFY_API_KEY",
            help=(
                "Comfy API key for API Nodes (Partner Nodes). "
                "Embedded in the prompt body as extra_data.api_key_comfy_org on POST /prompt. "
                "For scripting, prefer the COMFY_API_KEY environment variable so the secret "
                "stays out of shell history."
            ),
        ),
    ] = None,
):
    if api_key:
        api_key = api_key.strip() or None

    config = ConfigManager()

    if host:
        s = host.split(":")
        host = s[0]
        if not port and len(s) == 2:
            port = int(s[1])

    local_paths = False
    if config.background:
        if not host:
            host = config.background[0]
            local_paths = True
        if port:
            local_paths = False
        else:
            port = config.background[1]

    if not host:
        host = "127.0.0.1"
    if not port:
        port = 8188

    run_inner.execute(workflow, host, port, wait, verbose, local_paths, timeout, api_key=api_key)


def validate_comfyui(_env_checker):
    if _env_checker.comfy_repo is None:
        rprint("[bold red]If ComfyUI is not installed, this feature cannot be used.[/bold red]")
        raise typer.Exit(code=1)


@app.command(help="Stop background ComfyUI")
@tracking.track_command()
def stop():
    if constants.CONFIG_KEY_BACKGROUND not in ConfigManager().config["DEFAULT"]:
        rprint("[bold red]No ComfyUI is running in the background.[/bold red]\n")
        raise typer.Exit(code=1)

    bg_info = ConfigManager().background
    if not bg_info:
        rprint("[bold red]No ComfyUI is running in the background.[/bold red]\n")
        raise typer.Exit(code=1)
    is_killed = utils.kill_all(bg_info[2])

    if not is_killed:
        rprint("[bold red]Failed to stop ComfyUI in the background.[/bold red]\n")
    else:
        rprint(f"[bold yellow]Background ComfyUI is stopped.[/bold yellow] ({bg_info[0]}:{bg_info[1]})")

    ConfigManager().remove_background()


@app.command(help="Launch ComfyUI: ?[--background] ?[-- <extra args ...>]")
@tracking.track_command()
def launch(
    extra: list[str] = typer.Argument(None),
    background: Annotated[bool, typer.Option(help="Launch ComfyUI in background")] = False,
    frontend_pr: Annotated[
        str | None,
        typer.Option(
            "--frontend-pr",
            show_default=False,
            help="Use a specific frontend PR. Supports formats: username:branch, #123, or PR URL",
        ),
    ] = None,
):
    launch_command(background, extra, frontend_pr)


@app.command("set-default", help="Set default ComfyUI path")
@tracking.track_command()
def set_default(
    workspace_path: str,
    launch_extras: Annotated[str, typer.Option(help="Specify extra options for launch")] = "",
):
    comfy_path = os.path.abspath(os.path.expanduser(workspace_path))

    if not os.path.exists(comfy_path):
        rprint(
            f"\nPath not found: {comfy_path}.\n",
            file=sys.stderr,
        )
        raise typer.Exit(code=1)

    is_comfy_repo, resolved_path = check_comfy_repo(comfy_path)
    if not is_comfy_repo:
        rprint(
            f"\nSpecified path is not a ComfyUI path: {comfy_path}.\n",
            file=sys.stderr,
        )
        raise typer.Exit(code=1)

    comfy_path = resolved_path

    rprint(f"Specified path is set as default ComfyUI path: {comfy_path} ")
    workspace_manager.set_default_workspace(comfy_path)
    workspace_manager.set_default_launch_extras(launch_extras)


@app.command(help="Show which ComfyUI is selected.")
@tracking.track_command()
def which():
    comfy_path = workspace_manager.workspace_path
    if comfy_path is None:
        rprint(
            "ComfyUI not found, please run 'comfy install', run 'comfy' in a ComfyUI directory, or specify the workspace path with '--workspace'."
        )
        raise typer.Exit(code=1)

    rprint(f"Target ComfyUI path: {comfy_path}")


@app.command(help="Print out current environment variables.")
@tracking.track_command()
def env():
    check_for_updates()
    env_data = EnvChecker().fill_print_table()
    workspace_data = workspace_manager.fill_print_table()
    all_data = env_data + workspace_data
    ui.display_table(
        data=all_data,
        column_names=[":laptop_computer: Environment", "Value"],
        title="Environment Information",
    )


@app.command(hidden=True)
@tracking.track_command()
def nodes():
    rprint("\n[bold red] No such command, did you mean 'comfy node' instead?[/bold red]\n")


@app.command(hidden=True)
@tracking.track_command()
def models():
    rprint("\n[bold red] No such command, did you mean 'comfy model' instead?[/bold red]\n")


@app.command(help="Provide feedback on the Comfy CLI tool.")
@tracking.track_command()
def feedback():
    rprint("Feedback Collection for Comfy CLI Tool\n")

    # General Satisfaction
    general_satisfaction_score = ui.prompt_select(
        question="On a scale of 1 to 5, how satisfied are you with the Comfy CLI tool? (1 being very dissatisfied and 5 being very satisfied)",
        choices=["1", "2", "3", "4", "5"],
        force_prompting=True,
    )
    tracking.track_event("feedback_general_satisfaction", {"score": general_satisfaction_score})

    # Usability and User Experience
    usability_satisfaction_score = ui.prompt_select(
        question="On a scale of 1 to 5,  how satisfied are you with the usability and user experience of the Comfy CLI tool? (1 being very dissatisfied and 5 being very satisfied)",
        choices=["1", "2", "3", "4", "5"],
        force_prompting=True,
    )
    tracking.track_event("feedback_usability_satisfaction", {"score": usability_satisfaction_score})

    # Additional Feature-Specific Feedback
    if questionary.confirm("Do you want to provide additional feature-specific feedback on our GitHub page?").ask():
        tracking.track_event("feedback_additional")
        webbrowser.open("https://github.com/Comfy-Org/comfy-cli/issues/new/choose")

    rprint("Thank you for your feedback!")


@app.command(hidden=True)
@app.command(
    help="Given an existing installation of comfy core and any custom nodes, installs any needed python dependencies"
)
@tracking.track_command()
def dependency():
    comfy_path, _ = workspace_manager.get_workspace_path()

    python = resolve_workspace_python(comfy_path)
    depComp = DependencyCompiler(cwd=comfy_path, executable=python)
    depComp.compile_deps()
    depComp.install_deps()


@app.command(help="Download a standalone Python interpreter and dependencies based on an existing comfyui workspace")
@tracking.track_command()
def standalone(
    cli_spec: Annotated[
        str,
        typer.Option(
            show_default=False,
            help="setuptools-style requirement specificer pointing to an instance of comfy-cli",
        ),
    ] = "comfy-cli",
    pack_wheels: Annotated[
        bool,
        typer.Option(
            show_default=False,
            help="Pack requirement wheels in archive when creating standalone bundle",
        ),
    ] = False,
    platform: Annotated[
        constants.OS | None,
        typer.Option(
            show_default=False,
            help="Create standalone Python for specified platform",
        ),
    ] = None,
    proc: Annotated[
        constants.PROC | None,
        typer.Option(
            show_default=False,
            help="Create standalone Python for specified processor",
        ),
    ] = None,
    rehydrate: Annotated[
        bool,
        typer.Option(
            show_default=False,
            help="Create standalone Python for CPU",
        ),
    ] = False,
):
    comfy_path, _ = workspace_manager.get_workspace_path()

    platform = utils.get_os() if platform is None else platform
    proc = utils.get_proc() if proc is None else proc

    if rehydrate:
        sty = StandalonePython.FromTarball(fpath="python.tgz")
        sty.rehydrate_comfy_deps(packWheels=pack_wheels)
    else:
        sty = StandalonePython.FromDistro(platform=platform, proc=proc)
        sty.dehydrate_comfy_deps(comfyDir=comfy_path, extraSpecs=[], packWheels=pack_wheels)
        sty.to_tarball()


generate_command.register_with(app)
app.add_typer(models_command.app, name="model", help="Manage models.")
app.add_typer(custom_nodes.app, name="node", help="Manage custom nodes.")
app.add_typer(custom_nodes.manager_app, name="manager", help="Manage ComfyUI-Manager.")

app.add_typer(pr_command.app, name="pr-cache", help="Manage PR cache.")

app.add_typer(code_search.app, name="code-search", help="Search code across ComfyUI repositories.")
app.add_typer(code_search.app, name="cs", hidden=True)

app.add_typer(tracking.app, name="tracking", help="Manage analytics tracking settings.")


================================================
FILE: comfy_cli/command/__init__.py
================================================
from . import custom_nodes, install

__all__ = ["custom_nodes", "install"]


================================================
FILE: comfy_cli/command/code_search.py
================================================
"""CLI commands for searching code across ComfyUI repositories."""

import json
import re
import sys
from typing import Annotated
from urllib.parse import quote

import requests
import typer
from rich.console import Console
from rich.text import Text

from comfy_cli import tracking

app = typer.Typer()
console = Console()

API_URL = "https://comfy-codesearch.vercel.app/api/search/code"
DEFAULT_COUNT = 20
REQUEST_TIMEOUT = 30


_TYPE_FILTER_RE = re.compile(r"(^|\s)type:")


def _build_query(query: str, repo: str | None, count: int) -> str:
    parts = []
    if repo:
        if "/" not in repo:
            repo = f"Comfy-Org/{repo}"
        parts.append(f"repo:^{re.escape(repo)}$")
    # Only default to file matches when the user hasn't specified their own
    # type: filter — otherwise respect whatever they passed (e.g. type:commit).
    if not _TYPE_FILTER_RE.search(query):
        parts.append("type:file")
    parts.append(f"count:{count}")
    parts.append(query)
    return " ".join(parts)


def _fetch_results(query: str) -> dict:
    response = requests.get(API_URL, params={"query": query}, timeout=REQUEST_TIMEOUT)
    response.raise_for_status()
    return response.json()


def _format_results(search: dict) -> list[dict]:
    raw_results = search.get("results", {}).get("results", [])
    formatted = []
    for result in raw_results:
        repo_info = result.get("repository") or {}
        repo_name = repo_info.get("name", "")
        clean_name = repo_name.removeprefix("github.com/")

        file_info = result.get("file") or {}
        file_path = file_info.get("path", "")

        if not clean_name or not file_path:
            continue

        default_branch = repo_info.get("defaultBranch") or {}
        branch_name = default_branch.get("displayName", "main")
        commit_hash = (default_branch.get("target") or {}).get("commit", {}).get("oid", "")
        ref = commit_hash or branch_name

        encoded_path = quote(file_path, safe="/")
        file_url = f"https://github.com/{clean_name}/blob/{ref}/{encoded_path}"

        line_matches = result.get("lineMatches") or []
        matches = []
        for m in line_matches:
            line = m.get("lineNumber", 0) + 1
            preview = m.get("preview", "").rstrip()
            matches.append({"line": line, "preview": preview, "url": f"{file_url}#L{line}"})

        formatted.append(
            {
                "repository": clean_name,
                "file": file_path,
                "file_url": file_url,
                "branch": branch_name,
                "commit": commit_hash,
                "matches": matches,
            }
        )

    return formatted


def _get_stats(search: dict) -> dict:
    return {
        "approximate_count": search.get("stats", {}).get("approximateResultCount", "0"),
        "match_count": search.get("results", {}).get("matchCount", 0),
        "limit_hit": search.get("results", {}).get("limitHit", False),
    }


def _print_results(results: list[dict], stats: dict, json_output: bool) -> None:
    if json_output:
        print(json.dumps({"stats": stats, "results": results}, indent=2))
        return

    if not results:
        console.print("[yellow]No results found.[/yellow]")
        return

    # Use raw isatty() rather than Rich's console.is_terminal: Rich treats
    # FORCE_COLOR=1 / TTY_COMPATIBLE=1 as terminal-capable even when stdout
    # is redirected, but OSC 8 escapes in a piped stream defeat the whole
    # point of this branch (hiding URLs from humans, exposing them to AI).
    is_tty = sys.stdout.isatty()

    for file_result in results:
        repo = file_result["repository"]
        path = file_result["file"]
        file_url = file_result["file_url"]

        header = Text()
        if is_tty:
            # Humans: clickable OSC 8 hyperlink, URL hidden from visible output.
            header.append(f"{repo} / {path}", style=f"bold cyan link {file_url}")
        else:
            # Non-TTY (pipes, AI agents): print the raw URL once per file so
            # agents can synthesize #L<line> anchors themselves.
            header.append(f"{repo} / {path}\n")
            header.append(f"  {file_url}", style="dim")
        console.print(header)

        for match in file_result["matches"]:
            line_text = Text("  ")
            line_style = f"green link {match['url']}" if is_tty else "green"
            line_text.append(f"L{match['line']:>5}", style=line_style)
            line_text.append(f"  {match['preview']}")
            console.print(line_text)

        console.print()

    limit_msg = " (limit hit — use --count to fetch more)" if stats.get("limit_hit") else ""
    console.print(
        f"[dim]{stats['approximate_count']} approximate results, {stats['match_count']} matches returned{limit_msg}[/dim]"
    )


@app.callback(invoke_without_command=True)
@tracking.track_command()
def code_search(
    query: Annotated[
        str,
        typer.Argument(
            help=(
                "Search query (supports Sourcegraph syntax). Defaults to file matches; "
                "pass your own `type:` filter (e.g. `type:commit`) to override."
            ),
        ),
    ],
    repo: Annotated[
        str | None,
        typer.Option("--repo", "-r", help="Filter by repository (e.g. ComfyUI, Comfy-Org/ComfyUI)"),
    ] = None,
    count: Annotated[
        int,
        typer.Option("--count", "-n", help="Maximum number of results"),
    ] = DEFAULT_COUNT,
    json_output: Annotated[
        bool,
        typer.Option("--json", "-j", help="Output results as JSON"),
    ] = False,
):
    """Search code across ComfyUI repositories."""
    built_query = _build_query(query, repo, count)

    try:
        data = _fetch_results(built_query)
    except requests.ConnectionError:
        console.print("[bold red]Error: Could not connect to the code search service.[/bold red]")
        raise typer.Exit(code=1)
    except requests.Timeout:
        console.print("[bold red]Error: Request timed out.[/bold red]")
        raise typer.Exit(code=1)
    except requests.HTTPError as e:
        status = e.response.status_code if e.response is not None else "unknown"
        console.print(f"[bold red]Error: HTTP {status}[/bold red]")
        raise typer.Exit(code=1)

    search = data.get("data", {}).get("search", {})
    results = _format_results(search)
    stats = _get_stats(search)
    _print_results(results, stats, json_output=json_output)


================================================
FILE: comfy_cli/command/custom_nodes/__init__.py
================================================
from .command import app, manager_app

__all__ = ["app", "manager_app"]


================================================
FILE: comfy_cli/command/custom_nodes/bisect_custom_nodes.py
================================================
from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Annotated, Literal, NamedTuple

import typer

from comfy_cli.command.custom_nodes.cm_cli_util import execute_cm_cli
from comfy_cli.command.launch import launch as launch_command

bisect_app = typer.Typer()

# File to store the state of bisect
default_state_file = Path("bisect_state.json")


class BisectState(NamedTuple):
    status: Literal["idle", "running", "resolved"]

    # All nodes in the current bisect session
    all: list[str]

    # The range of nodes that contains the bad node
    range: list[str]

    # The active set of nodes to test
    active: list[str]

    # The arguments to pass to the ComfyUI launch command
    launch_args: list[str] = []

    def good(self) -> BisectState:
        """The active set of nodes is good, narrowing down the potential problem area."""
        if self.status != "running":
            raise ValueError("No bisect session running.")

        new_range = list(set(self.range) - set(self.active))

        if len(new_range) == 1:
            return BisectState(
                status="resolved",
                all=self.all,
                launch_args=self.launch_args,
                range=new_range,
                active=[],
            )

        return BisectState(
            status="running",
            all=self.all,
            launch_args=self.launch_args,
            range=new_range,
            active=new_range[len(new_range) // 2 :],
        )

    def bad(self) -> BisectState:
        """The active set of nodes is bad, indicating the problem is within this set."""
        if self.status != "running":
            raise ValueError("No bisect session running.")

        new_range = self.active

        if len(new_range) == 1:
            return BisectState(
                status="resolved",
                all=self.all,
                launch_args=self.launch_args,
                range=new_range,
                active=[],
            )

        return BisectState(
            status="running",
            all=self.all,
            launch_args=self.launch_args,
            range=new_range,
            active=new_range[len(new_range) // 2 :],
        )

    def save(self, state_file=None):
        self.set_custom_node_enabled_states()
        state_file = state_file or default_state_file
        with state_file.open("w") as f:
            json.dump(self._asdict(), f)  # pylint: disable=no-member

    def reset(self):
        BisectState(
            "idle",
            all=self.all,
            launch_args=self.launch_args,
            range=self.all,
            active=self.all,
        ).set_custom_node_enabled_states()
        return BisectState("idle", self.all, self.all, self.all, self.launch_args)

    @classmethod
    def load(cls, state_file=None) -> BisectState:
        state_file = state_file or default_state_file
        if state_file.exists():
            with state_file.open() as f:
                return BisectState(**json.load(f))
        return BisectState("idle", [], [], [])

    @property
    def inactive_nodes(self) -> list[str]:
        return list(set(self.all) - set(self.active))

    def set_custom_node_enabled_states(self):
        if self.active:
            execute_cm_cli(["enable", *self.active])
        if self.inactive_nodes:
            execute_cm_cli(["disable", *self.inactive_nodes])

    def __str__(self):
        active_list = "\n".join([f"{i + 1:3}. {node}" for i, node in enumerate(self.active)])
        return f"""BisectState(status={self.status})
set of nodes with culprit: {len(self.range)}
set of nodes to test: {len(self.active)}
--------------------------
{active_list}"""


def parse_cm_output(cm_output: str, pinned_nodes: set[str] | None = None) -> list[str]:
    """Parse cm_cli simple-show output into a list of node names.

    cm_cli simple-show always formats node entries as ``name@version``
    (see ComfyUI-Manager cm_cli show_list).  We whitelist on the ``@``
    separator so any progress/status lines are ignored regardless of
    their prefix.
    """
    pinned = pinned_nodes or set()
    return [
        stripped
        for line in cm_output.strip().split("\n")
        if (stripped := line.strip()) and "@" in stripped and stripped not in pinned
    ]


@bisect_app.command(
    help="Start a new bisect session with optionally pinned nodes to always enable, and optional ComfyUI launch args."
    + "?[--pinned-nodes PINNED_NODES]"
    + "?[-- <extra args ...>]"
)
def start(
    pinned_nodes: Annotated[str, typer.Option(help="Pinned nodes always enable during the bisect")] = "",
    extra: list[str] = typer.Argument(None),
):
    """Start a new bisect session. The initial state is bad with all custom nodes
    enabled, good with all custom nodes disabled."""

    if BisectState.load().status != "idle":
        typer.echo("A bisect session is already running.")
        raise typer.Exit()

    pinned_nodes = {s.strip() for s in pinned_nodes.split(",") if s}

    cm_output: str | None = execute_cm_cli(["simple-show", "enabled"])
    if cm_output is None:
        typer.echo("Failed to fetch the list of nodes.")
        raise typer.Exit()

    nodes_list = parse_cm_output(cm_output, pinned_nodes)
    state = BisectState(
        status="running",
        all=nodes_list,
        range=nodes_list,
        active=nodes_list,
        launch_args=extra or [],
    )
    state.save()

    typer.echo(f"Bisect session started.\n{state}")
    if pinned_nodes:
        typer.echo(f"Pinned nodes: {', '.join(pinned_nodes)}")

    bad()


@bisect_app.command(help="Mark the current active set as good, indicating the problem is outside the test set.")
def good():
    state = BisectState.load()
    if state.status != "running":
        typer.echo("No bisect session running or no active nodes to process.")
        raise typer.Exit()

    new_state = state.good()

    if new_state.status == "resolved":
        assert len(new_state.range) == 1
        typer.echo(f"Problematic node identified: {new_state.range[0]}")
        reset()
    else:
        new_state.save()
        typer.echo(new_state)
        launch_command(background=False, extra=state.launch_args)


@bisect_app.command(help="Mark the current active set as bad, indicating the problem is within the test set.")
def bad():
    state = BisectState.load()
    if state.status != "running":
        typer.echo("No bisect session running or no active nodes to process.")
        raise typer.Exit()

    new_state = state.bad()

    if new_state.status == "resolved":
        assert len(new_state.range) == 1
        typer.echo(f"Problematic node identified: {new_state.range[0]}")
        reset()
    else:
        new_state.save()
        typer.echo(new_state)
        launch_command(background=False, extra=state.launch_args)


@bisect_app.command(help="Reset the current bisect session.")
def reset():
    if default_state_file.exists():
        BisectState.load().reset()
        os.unlink(default_state_file)
        typer.echo("Bisect session reset.")
    else:
        typer.echo("No bisect session to reset.")


================================================
FILE: comfy_cli/command/custom_nodes/cm_cli_util.py
================================================
from __future__ import annotations

import importlib.util
import os
import subprocess
import sys
import threading
import uuid
from functools import lru_cache

import typer
from rich import print

from comfy_cli.config_manager import ConfigManager
from comfy_cli.resolve_python import resolve_workspace_python
from comfy_cli.uv import DependencyCompiler
from comfy_cli.workspace_manager import WorkspaceManager, check_comfy_repo

workspace_manager = WorkspaceManager()

# set of commands that invalidate (ie require an update of) dependencies after they are run
_dependency_cmds = {
    "install",
    "reinstall",
}


@lru_cache(maxsize=1)
def find_cm_cli() -> bool:
    """Check if cm_cli module is available in the workspace Python.

    First checks the workspace venv Python (primary path — matches the Python
    used by execute_cm_cli). Falls back to the current Python environment only
    when the workspace Python is the same as sys.executable.

    Results are cached for the session lifetime.
    """
    ws = workspace_manager.workspace_path
    if ws:
        python = resolve_workspace_python(ws)
        if python != sys.executable:
            # Workspace uses a different Python — check that one
            try:
                result = subprocess.run(
                    [python, "-c", "import cm_cli"],
                    capture_output=True,
                    timeout=10,
                )
                return result.returncode == 0
            except (subprocess.TimeoutExpired, OSError):
                return False

    # Same Python or no workspace — check current environment
    return importlib.util.find_spec("cm_cli") is not None


def resolve_manager_gui_mode(not_installed_value: str | None = None) -> str | None:
    """Resolve manager GUI mode from config, with legacy migration.

    Priority: CONFIG_KEY_MANAGER_GUI_MODE > CONFIG_KEY_MANAGER_GUI_ENABLED > auto-detect.

    Args:
        not_installed_value: Value to return when manager is not installed and no config exists.
            Callers use None (launch — means "no flags") or "not-installed" (display).
    """
    from comfy_cli import constants

    config_manager = ConfigManager()
    mode = config_manager.get(constants.CONFIG_KEY_MANAGER_GUI_MODE)

    if mode is not None:
        return mode

    # Legacy migration
    old_value = config_manager.get(constants.CONFIG_KEY_MANAGER_GUI_ENABLED)
    if old_value is not None:
        old_str = str(old_value).lower()
        if old_str in ("false", "0", "off"):
            return "disable"
        if old_str in ("true", "1", "on"):
            return "enable-gui"

    # No config at all — check manager availability
    if not find_cm_cli():
        return not_installed_value
    return "enable-gui"


def execute_cm_cli(
    args, channel=None, fast_deps=False, no_deps=False, uv_compile=False, mode=None, raise_on_error=False
) -> str | None:
    _config_manager = ConfigManager()

    workspace_path = workspace_manager.workspace_path

    if not workspace_path:
        print("\n[bold red]ComfyUI path is not resolved.[/bold red]\n", file=sys.stderr)
        raise typer.Exit(code=1)

    if not check_comfy_repo(workspace_path)[0]:
        print(
            f"\n[bold red]'{workspace_path}' is not a valid ComfyUI workspace.[/bold red]\n"
            "Run [bold]comfy install[/bold] to set up ComfyUI, or use [bold]--workspace <path>[/bold] to specify a valid path.\n",
            file=sys.stderr,
        )
        raise typer.Exit(code=1)

    if not find_cm_cli():
        print(
            "\n[bold red]ComfyUI-Manager not found. 'cm-cli' command is not available.[/bold red]\n",
            file=sys.stderr,
        )
        raise typer.Exit(code=1)

    python = resolve_workspace_python(workspace_path)
    cmd = [python, "-m", "cm_cli"] + args

    if channel is not None:
        cmd += ["--channel", channel]

    if uv_compile:
        cmd += ["--uv-compile"]
    elif fast_deps or no_deps:
        cmd += ["--no-deps"]

    if mode is not None:
        cmd += ["--mode", mode]

    new_env = os.environ.copy()
    session_path = os.path.join(_config_manager.get_config_path(), "tmp", str(uuid.uuid4()))
    new_env["__COMFY_CLI_SESSION__"] = session_path
    new_env["COMFYUI_PATH"] = workspace_path
    new_env["PYTHONUNBUFFERED"] = "1"

    print(f"Execute from: {workspace_path}")
    print(f"Command: {cmd}")
    try:
        process = subprocess.Popen(
            cmd,
            env=new_env,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            encoding="utf-8",
            errors="replace",
        )

        # Read stderr in a background thread to avoid pipe deadlock on Windows.
        # Windows pipe buffers are small (4 KB); if stderr fills up while the main
        # thread is blocked reading stdout line-by-line, the child process blocks
        # on stderr writes and never closes stdout — classic deadlock.
        stderr_lines: list[str] = []

        def _drain_stderr():
            for line in process.stderr:
                sys.stderr.write(line)
                sys.stderr.flush()
                stderr_lines.append(line)

        stderr_thread = threading.Thread(target=_drain_stderr, daemon=True)
        stderr_thread.start()

        stdout_lines = []
        for line in process.stdout:
            sys.stdout.write(line)
            sys.stdout.flush()
            stdout_lines.append(line)

        stderr_thread.join(timeout=10)
        return_code = process.wait()
        stdout_output = "".join(stdout_lines)
        stderr_output = "".join(stderr_lines)
        if return_code != 0:
            raise subprocess.CalledProcessError(return_code, cmd, output=stdout_output, stderr=stderr_output)

        if fast_deps and args[0] in _dependency_cmds:
            # we're using the fast_deps behavior and just ran a command that invalidated the dependencies
            depComp = DependencyCompiler(cwd=workspace_path, executable=python)
            depComp.compile_deps()
            depComp.install_deps()

        workspace_manager.set_recent_workspace(workspace_path)
        return stdout_output
    except subprocess.CalledProcessError as e:
        if raise_on_error:
            raise e

        if e.returncode == 1:
            print(f"\n[bold red]Execution error: {cmd}[/bold red]\n", file=sys.stderr)
            return None

        if e.returncode == 2:
            return None

        raise e


================================================
FILE: comfy_cli/command/custom_nodes/command.py
================================================
import os
import pathlib
import platform
import shutil
import subprocess
import sys
import uuid
from enum import Enum
from typing import Annotated

import typer
from rich import print
from rich.console import Console

from comfy_cli import constants, logging, tracking, ui, utils
from comfy_cli.command.custom_nodes.bisect_custom_nodes import bisect_app
from comfy_cli.command.custom_nodes.cm_cli_util import execute_cm_cli, find_cm_cli
from comfy_cli.config_manager import ConfigManager
from comfy_cli.constants import NODE_ZIP_FILENAME
from comfy_cli.file_utils import (
    DownloadException,
    download_file,
    extract_package_as_zip,
    upload_file_to_signed_url,
    zip_files,
)
from comfy_cli.registry import (
    RegistryAPI,
    extract_node_configuration,
    initialize_project_config,
)
from comfy_cli.resolve_python import resolve_workspace_python
from comfy_cli.workspace_manager import WorkspaceManager

console = Console()
app = typer.Typer()
app.add_typer(bisect_app, name="bisect", help="Bisect custom nodes for culprit node.")
manager_app = typer.Typer()
workspace_manager = WorkspaceManager()
registry_api = RegistryAPI()


# Enum for show command target
class ShowTarget(str, Enum):
    INSTALLED = "installed"
    ENABLED = "enabled"
    NOT_INSTALLED = "not-installed"
    DISABLED = "disabled"
    ALL = "all"
    SNAPSHOT = "snapshot"
    SNAPSHOT_LIST = "snapshot-list"


def _resolve_uv_compile(uv_compile: bool | None, fast_deps: bool = False, no_deps: bool = False) -> bool:
    """Resolve effective uv_compile value from explicit flag, config default, and conflicting flags.

    Priority: explicit --uv-compile/--no-uv-compile > config default > False.
    When config default is True, explicit --fast-deps or --no-deps silently override it.
    """
    if uv_compile is not None:
        return uv_compile

    config_manager = ConfigManager()
    config_value = config_manager.get(constants.CONFIG_KEY_UV_COMPILE_DEFAULT)
    if config_value is not None and config_value.lower() == "true":
        if fast_deps or no_deps:
            return False
        return True
    return False


def validate_comfyui_manager():
    if not find_cm_cli():
        print("[bold red]ComfyUI-Manager is not installed. 'cm-cli' command is not available.[/bold red]")
        raise typer.Exit(code=1)


def run_script(cmd, cwd="."):
    if len(cmd) > 0 and cmd[0].startswith("#"):
        print(f"[ComfyUI-Manager] Unexpected behavior: `{cmd}`")
        return 0

    subprocess.check_call(cmd, cwd=cwd)

    return 0


pip_map = None


def get_installed_packages():
    global pip_map

    if pip_map is None:
        try:
            python = resolve_workspace_python(workspace_manager.workspace_path)
            result = subprocess.check_output([python, "-m", "pip", "list"], universal_newlines=True)

            pip_map = {}
            for line in result.split("\n"):
                x = line.strip()
                if x:
                    y = line.split()
                    if y[0] == "Package" or y[0].startswith("-"):
                        continue

                    pip_map[y[0]] = y[1]
        except subprocess.CalledProcessError:
            print("[ComfyUI-Manager] Failed to retrieve the information of installed pip packages.")
            return set()

    return pip_map


def try_install_script(repo_path, install_cmd, instant_execution=False):
    startup_script_path = os.path.join(workspace_manager.workspace_path, "startup-scripts")
    if not instant_execution and (
        (len(install_cmd) > 0 and install_cmd[0].startswith("#"))
        or (
            platform.system() == "Windows"
            # From Yoland: disable commit compare
            # and comfy_ui_commit_datetime.date()
            # >= comfy_ui_required_commit_datetime.date()
        )
    ):
        if not os.path.exists(startup_script_path):
            os.makedirs(startup_script_path)

        script_path = os.path.join(startup_script_path, "install-scripts.txt")
        with open(script_path, "a", encoding="utf-8") as file:
            obj = [repo_path] + install_cmd
            file.write(f"{obj}\n")

        return True
    else:
        # From Yoland: Disable blacklisting
        # if len(install_cmd) == 5 and install_cmd[2:4] == ['pip', 'install']:
        #     if is_blacklisted(install_cmd[4]):
        #         print(f"[ComfyUI-Manager] skip black listed pip installation: '{install_cmd[4]}'")
        #         return True

        print(f"\n## ComfyUI-Manager: EXECUTE => {install_cmd}")
        code = run_script(install_cmd, cwd=repo_path)

        # From Yoland: Disable warning
        # if platform.system() != "Windows":
        #     try:
        #         if comfy_ui_commit_datetime.date() < comfy_ui_required_commit_datetime.date():
        #             print("\n\n###################################################################")
        #             print(f"[WARN] ComfyUI-Manager: Your ComfyUI version ({comfy_ui_revision})[{comfy_ui_commit_datetime.date()}] is too old. Please update to the latest version.")
        #             print(f"[WARN] The extension installation feature may not work properly in the current installed ComfyUI version on Windows environment.")
        #             print("###################################################################\n\n")
        #     except:
        #         pass

        if code != 0:
            print("install script failed")
            return False


def execute_install_script(repo_path):
    install_script_path = os.path.join(repo_path, "install.py")
    requirements_path = os.path.join(repo_path, "requirements.txt")

    # From Yoland: disable lazy mode
    # if lazy_mode:
    #     install_cmd = ["#LAZY-INSTALL-SCRIPT",  sys.executable]
    #     try_install_script(repo_path, install_cmd)
    # else:

    if os.path.exists(requirements_path):
        print("Install: pip packages")
        python = resolve_workspace_python(workspace_manager.workspace_path)
        # Absolute path so pip doesn't re-resolve it against cwd=repo_path
        # in try_install_script, which would double the path if repo_path
        # is relative.
        install_cmd = [python, "-m", "pip", "install", "-r", os.path.abspath(requirements_path)]
        try_install_script(repo_path, install_cmd)

    if os.path.exists(install_script_path):
        print("Install: install script")
        python = resolve_workspace_python(workspace_manager.workspace_path)
        install_cmd = [python, "install.py"]
        try_install_script(repo_path, install_cmd)


@app.command("save-snapshot", help="Save a snapshot of the current ComfyUI environment")
@tracking.track_command("node")
def save_snapshot(
    output: Annotated[
        str | None,
        typer.Option(show_default=False, help="Specify the output file path. (.json/.yaml)"),
    ] = None,
):
    if output is None:
        execute_cm_cli(["save-snapshot"])
    else:
        output = os.path.abspath(output)  # to compensate chdir
        execute_cm_cli(["save-snapshot", "--output", output])


@app.command("restore-snapshot", help="Restore snapshot from snapshot file")
@tracking.track_command("node")
def restore_snapshot(
    path: str,
    pip_non_url: bool | None = typer.Option(
        default=None,
        show_default=False,
        help="Restore for pip packages registered on PyPI.",
    ),
    pip_non_local_url: bool | None = typer.Option(
        default=None,
        show_default=False,
        help="Restore for pip packages registered at web URLs.",
    ),
    pip_local_url: bool | None = typer.Option(
        default=None,
        show_default=False,
        help="Restore for pip packages specified by local paths.",
    ),
    uv_compile: Annotated[
        bool | None,
        typer.Option(
            "--uv-compile/--no-uv-compile",
            show_default=False,
            help="After restoring, batch-resolve all dependencies via uv pip compile (requires ComfyUI-Manager v4.1+)",
        ),
    ] = None,
):
    extras = []

    if pip_non_url:
        extras += ["--pip-non-url"]

    if pip_non_local_url:
        extras += ["--pip-non-local-url"]

    if pip_local_url:
        extras += ["--pip-local-url"]

    path = os.path.abspath(path)
    execute_cm_cli(["restore-snapshot", path] + extras, uv_compile=_resolve_uv_compile(uv_compile))


@app.command("restore-dependencies", help="Restore dependencies from installed custom nodes")
@tracking.track_command("node")
def restore_dependencies(
    uv_compile: Annotated[
        bool | None,
        typer.Option(
            "--uv-compile/--no-uv-compile",
            show_default=False,
            help="After restoring, batch-resolve all dependencies via uv pip compile (requires ComfyUI-Manager v4.1+)",
        ),
    ] = None,
):
    execute_cm_cli(["restore-dependencies"], uv_compile=_resolve_uv_compile(uv_compile))


@manager_app.command("disable", help="Disable ComfyUI-Manager completely")
@tracking.track_command("node")
def disable_manager():
    """Disable ComfyUI-Manager. No manager flags will be passed to ComfyUI."""
    config_manager = ConfigManager()
    config_manager.set(constants.CONFIG_KEY_MANAGER_GUI_MODE, "disable")
    print("[bold yellow]ComfyUI-Manager has been disabled.[/bold yellow]")
    print("No manager flags will be passed to ComfyUI on next launch.")


@manager_app.command("enable-gui", help="Enable ComfyUI-Manager with new GUI")
@tracking.track_command("node")
def enable_gui():
    """Enable ComfyUI-Manager with new GUI."""
    config_manager = ConfigManager()
    config_manager.set(constants.CONFIG_KEY_MANAGER_GUI_MODE, "enable-gui")
    print("[bold green]ComfyUI-Manager GUI has been enabled.[/bold green]")
    print("[dim]ComfyUI will launch with: --enable-manager[/dim]")


@manager_app.command("disable-gui", help="Enable ComfyUI-Manager without GUI")
@tracking.track_command("node")
def disable_gui():
    """Enable ComfyUI-Manager but disable its GUI."""
    config_manager = ConfigManager()
    config_manager.set(constants.CONFIG_KEY_MANAGER_GUI_MODE, "disable-gui")
    print("[bold green]ComfyUI-Manager enabled with GUI disabled.[/bold green]")
    print("[dim]ComfyUI will launch with: --enable-manager --disable-manager-ui[/dim]")


@manager_app.command("enable-legacy-gui", help="Enable ComfyUI-Manager with legacy GUI")
@tracking.track_command("node")
def enable_legacy_gui():
    """Enable ComfyUI-Manager with legacy GUI."""
    config_manager = ConfigManager()
    config_manager.set(constants.CONFIG_KEY_MANAGER_GUI_MODE, "enable-legacy-gui")
    print("[bold green]ComfyUI-Manager legacy GUI has been enabled.[/bold green]")
    print("[dim]ComfyUI will launch with: --enable-manager --enable-manager-legacy-ui[/dim]")


@manager_app.command("migrate-legacy", help="Migrate legacy git-cloned ComfyUI-Manager to .disabled")
@tracking.track_command("node")
def migrate_legacy(
    yes: Annotated[
        bool,
        typer.Option("--yes", "-y", help="Skip confirmation prompt"),
    ] = False,
):
    """
    Migrate legacy ComfyUI-Manager from custom_nodes/ to custom_nodes/.disabled/

    Detects .enable-cli-only-mode file to set appropriate mode:
    - If .enable-cli-only-mode exists → mode = disable
    - Otherwise → mode = enable-gui
    """
    if not workspace_manager.workspace_path:
        print("[bold red]ComfyUI workspace is not set.[/bold red]")
        print("[dim]Use --workspace or run from a ComfyUI directory.[/dim]")
        raise typer.Exit(code=1)

    custom_nodes_path = pathlib.Path(workspace_manager.workspace_path) / "custom_nodes"

    # Find legacy manager with case-insensitive matching (must be a real directory, not symlink)
    legacy_manager_path = None
    if custom_nodes_path.exists():
        for item in custom_nodes_path.iterdir():
            if item.is_dir() and not item.is_symlink() and item.name.lower() == "comfyui-manager":
                legacy_manager_path = item
                break

    # Check if legacy manager exists
    if legacy_manager_path is None:
        print("[bold yellow]No legacy ComfyUI-Manager found in custom_nodes/[/bold yellow]")
        print("Nothing to migrate.")
        return

    # Verify it's a git-cloned repository
    git_dir = legacy_manager_path / ".git"
    if not git_dir.exists():
        print(f"[bold yellow]Warning: {legacy_manager_path.name} does not appear to be a git repository.[/bold yellow]")
        print("[dim]Expected a git-cloned ComfyUI-Manager. Skipping migration.[/dim]")
        return

    # Detect CLI-only mode before any changes
    cli_only_mode_file = legacy_manager_path / ".enable-cli-only-mode"
    cli_only_mode = cli_only_mode_file.exists()

    # Show what will happen and ask for confirmation
    print(f"[bold]Found legacy ComfyUI-Manager:[/bold] {legacy_manager_path}")
    print(f"[dim]CLI-only mode: {cli_only_mode}[/dim]")
    print()
    print("[bold]This will:[/bold]")
    print(f"  1. Move {legacy_manager_path.name} to custom_nodes/.disabled/")
    print(f"  2. Set manager mode to: {'disable' if cli_only_mode else 'enable-gui'}")
    print("  3. Install manager_requirements.txt (if present)")
    print()

    if not yes:
        confirm = ui.prompt_confirm_action("Proceed with migration?", False)
        if not confirm:
            print("[dim]Migration cancelled.[/dim]")
            return

    # Create .disabled directory
    disabled_path = custom_nodes_path / ".disabled"
    disabled_path.mkdir(exist_ok=True)

    # Check if target already exists (case-insensitive)
    existing_target = None
    for item in disabled_path.iterdir():
        if item.is_dir() and item.name.lower() == "comfyui-manager":
            existing_target = item
            break

    if existing_target is not None:
        print(f"[bold red]Target path already exists: {existing_target}[/bold red]")
        print("Please remove it manually and try again.")
        raise typer.Exit(code=1)

    # Move legacy manager (preserve original directory name)
    target_path = disabled_path / legacy_manager_path.name
    try:
        shutil.move(str(legacy_manager_path), str(target_path))
    except OSError as e:
        print(f"[bold red]Failed to move legacy manager: {e}[/bold red]")
        raise typer.Exit(code=1)

    # Install manager_requirements.txt if present
    workspace_path = pathlib.Path(workspace_manager.workspace_path)
    manager_req_path = workspace_path / constants.MANAGER_REQUIREMENTS_FILE
    python = resolve_workspace_python(str(workspace_path))
    install_success = False  # Default to failure, set True only on success
    if manager_req_path.exists():
        print("[dim]Installing ComfyUI-Manager dependencies...[/dim]")
        result = subprocess.run(
            [python, "-m", "pip", "install", "-r", str(manager_req_path)],
            check=False,
        )
        if result.returncode != 0:
            print("[bold yellow]Warning: Failed to install ComfyUI-Manager dependencies.[/bold yellow]")
            print("[dim]You may need to run: pip install -r manager_requirements.txt[/dim]")
        else:
            install_success = True
    else:
        print("[bold yellow]Warning: manager_requirements.txt not found (older ComfyUI version?).[/bold yellow]")
        print("[dim]ComfyUI-Manager pip package not installed.[/dim]")

    # Set config mode
    config_manager = ConfigManager()
    if cli_only_mode or not install_success:
        config_manager.set(constants.CONFIG_KEY_MANAGER_GUI_MODE, "disable")
        print("[bold green]Legacy ComfyUI-Manager migrated to .disabled/[/bold green]")
        if cli_only_mode:
            print("[dim]Detected .enable-cli-only-mode → Manager set to: disable[/dim]")
        else:
            print("[dim]Manager installation failed → Manager set to: disable[/dim]")
            print("[dim]After fixing installation, run: comfy manager enable-gui[/dim]")
    else:
        config_manager.set(constants.CONFIG_KEY_MANAGER_GUI_MODE, "enable-gui")
        print("[bold green]Legacy ComfyUI-Manager migrated to .disabled/[/bold green]")
        print("[dim]Manager set to: enable-gui (new GUI)[/dim]")

    print("\n[bold]The new pip-installed ComfyUI-Manager will be used on next launch.[/bold]")


@manager_app.command(
    "uv-compile-default", help="Set whether --uv-compile is used by default for custom node operations"
)
@tracking.track_command("node")
def uv_compile_default(
    enabled: Annotated[
        bool,
        typer.Argument(help="true to enable, false to disable"),
    ],
):
    config_manager = ConfigManager()
    config_manager.set(constants.CONFIG_KEY_UV_COMPILE_DEFAULT, str(enabled))
    if enabled:
        print("[bold green]uv-compile is now enabled by default.[/bold green]")
        print("[dim]Use --no-uv-compile to override for individual commands.[/dim]")
    else:
        print("[bold yellow]uv-compile default has been disabled.[/bold yellow]")
        print("[dim]Use --uv-compile to enable for individual commands.[/dim]")


@manager_app.command(help="Clear reserved startup action in ComfyUI-Manager")
@tracking.track_command("node")
def clear():
    execute_cm_cli(["clear"])


@app.command("update-cache", help="Force-fetch remote data and populate local Manager cache (blocking)")
@tracking.track_command("node")
def update_cache():
    execute_cm_cli(["update-cache"])


# completers
mode_completer = utils.create_choice_completer(["remote", "local", "cache"])


channel_completer = utils.create_choice_completer(["default", "recent", "dev", "forked", "tutorial", "legacy"])


def node_completer(incomplete: str) -> list[str]:
    try:
        config_manager = ConfigManager()
        tmp_path = os.path.join(config_manager.get_config_path(), "tmp", "node-cache.list")

        with open(tmp_path, encoding="UTF-8", errors="ignore") as cache_file:
            return [node_id for node_id in cache_file.readlines() if node_id.startswith(incomplete)]

    except Exception:
        return []


def node_or_all_completer(incomplete: str) -> list[str]:
    try:
        config_manager = ConfigManager()
        tmp_path = os.path.join(config_manager.get_config_path(), "tmp", "node-cache.list")

        all_opt = []
        if "all".startswith(incomplete):
            all_opt = ["all"]

        with open(tmp_path, encoding="UTF-8", errors="ignore") as cache_file:
            return [node_id for node_id in cache_file.readlines() if node_id.startswith(incomplete)] + all_opt

    except Exception:
        return []


def validate_mode(mode):
    valid_modes = ["remote", "local", "cache"]
    if mode and mode.lower() not in valid_modes:
        typer.echo(
            f"Invalid mode: {mode}. Allowed modes are 'remote', 'local', 'cache'.",
            err=True,
        )
        raise typer.Exit(code=1)


@app.command(help="Show node list")
@tracking.track_command("node")
def show(
    arg: ShowTarget = typer.Argument(
        help="Target to display",
    ),
    channel: Annotated[
        str | None,
        typer.Option(
            show_default=False,
            help="Specify the operation mode",
            autocompletion=channel_completer,
        ),
    ] = None,
    mode: str = typer.Option(
        None,
        help="[remote|local|cache]",
        autocompletion=mode_completer,
    ),
):
    validate_mode(mode)

    execute_cm_cli(["show", arg.value], channel=channel, mode=mode)


@app.command("simple-show", help="Show node list (simple mode)")
@tracking.track_command("node")
def simple_show(
    arg: ShowTarget = typer.Argument(
        help="Target to display",
    ),
    channel: Annotated[
        str | None,
        typer.Option(
            show_default=False,
            help="Specify the operation mode",
            autocompletion=channel_completer,
        ),
    ] = None,
    mode: str = typer.Option(
        None,
        help="[remote|local|cache]",
        autocompletion=mode_completer,
    ),
):
    validate_mode(mode)

    execute_cm_cli(["simple-show", arg.value], channel=channel, mode=mode)


# install, reinstall, uninstall
@app.command(help="Install custom nodes")
@tracking.track_command("node")
def install(
    nodes: list[str] = typer.Argument(..., help="List of custom nodes to install", autocompletion=node_completer),
    channel: Annotated[
        str | None,
        typer.Option(
            show_default=False,
            help="Specify the operation mode",
            autocompletion=channel_completer,
        ),
    ] = None,
    fast_deps: Annotated[
        bool,
        typer.Option(
            "--fast-deps",
            show_default=False,
            help="Use new fast dependency installer",
        ),
    ] = False,
    no_deps: Annotated[
        bool,
        typer.Option(
            "--no-deps",
            show_default=False,
            help="Skip dependency installation",
        ),
    ] = False,
    uv_compile: Annotated[
        bool | None,
        typer.Option(
            "--uv-compile/--no-uv-compile",
            show_default=False,
            help="After installing, batch-resolve all dependencies via uv pip compile (requires ComfyUI-Manager v4.1+)",
        ),
    ] = None,
    exit_on_fail: Annotated[
        bool,
        typer.Option(
            "--exit-on-fail",
            help="Exit on failure",
        ),
    ] = False,
    mode: str = typer.Option(
        None,
        help="[remote|local|cache]",
        autocompletion=mode_completer,
    ),
):
    if "all" in nodes:
        typer.echo("`install all` is not allowed", err=True)
        raise typer.Exit(code=1)

    exclusive_flags = [
        name for name, val in [("--fast-deps", fast_deps), ("--no-deps", no_deps), ("--uv-compile", uv_compile)] if val
    ]
    if len(exclusive_flags) > 1:
        typer.echo(f"Cannot use {' and '.join(exclusive_flags)} together", err=True)
        raise typer.Exit(code=1)

    effective_uv_compile = _resolve_uv_compile(uv_compile, fast_deps=fast_deps, no_deps=no_deps)

    validate_mode(mode)

    if exit_on_fail:
        cmd = ["install", "--exit-on-fail"] + nodes
    else:
        cmd = ["install"] + nodes

    try:
        execute_cm_cli(
            cmd,
            channel=channel,
            fast_deps=fast_deps,
            no_deps=no_deps,
            uv_compile=effective_uv_compile,
            mode=mode,
            raise_on_error=exit_on_fail,
        )
    except subprocess.CalledProcessError as e:
        if exit_on_fail:
            raise typer.Exit(code=e.returncode)


@app.command(help="Reinstall custom nodes")
@tracking.track_command("node")
def reinstall(
    nodes: list[str] = typer.Argument(..., help="List of custom nodes to reinstall", autocompletion=node_completer),
    channel: Annotated[
        str | None,
        typer.Option(
            show_default=False,
            help="Specify the operation mode",
            autocompletion=channel_completer,
        ),
    ] = None,
    fast_deps: Annotated[
        bool,
        typer.Option(
            "--fast-deps",
            show_default=False,
            help="Use new fast dependency installer",
        ),
    ] = False,
    uv_compile: Annotated[
        bool | None,
        typer.Option(
            "--uv-compile/--no-uv-compile",
            show_default=False,
            help="After reinstalling, batch-resolve all dependencies via uv pip compile (requires ComfyUI-Manager v4.1+)",
        ),
    ] = None,
    mode: str = typer.Option(
        None,
        help="[remote|local|cache]",
        autocompletion=mode_completer,
    ),
):
    if "all" in nodes:
        typer.echo("`reinstall all` is not allowed", err=True)
        raise typer.Exit(code=1)

    exclusive_flags = [name for name, val in [("--fast-deps", fast_deps), ("--uv-compile", uv_compile)] if val]
    if len(exclusive_flags) > 1:
        typer.echo(f"Cannot use {' and '.join(exclusive_flags)} together", err=True)
        raise typer.Exit(code=1)

    effective_uv_compile = _resolve_uv_compile(uv_compile, fast_deps=fast_deps)

    validate_mode(mode)

    execute_cm_cli(
        ["reinstall"] + nodes, channel=channel, fast_deps=fast_deps, uv_compile=effective_uv_compile, mode=mode
    )


@app.command(
    "uv-sync",
    help="Batch-resolve and install all custom node dependencies via uv (requires ComfyUI-Manager v4.1+)",
)
@tracking.track_command("node")
def uv_sync():
    execute_cm_cli(["uv-sync"])


@app.command(help="Uninstall custom nodes")
@tracking.track_command("node")
def uninstall(
    nodes: list[str] = typer.Argument(..., help="List of custom nodes to uninstall", autocompletion=node_completer),
    channel: Annotated[
        str | None,
        typer.Option(
            show_default=False,
            help="Specify the operation mode",
            autocompletion=channel_completer,
        ),
    ] = None,
    mode: str = typer.Option(
        None,
        help="[remote|local|cache]",
        autocompletion=mode_completer,
    ),
):
    if "all" in nodes:
        typer.echo("`uninstall all` is not allowed", err=True)
        raise typer.Exit(code=1)

    validate_mode(mode)

    execute_cm_cli(["uninstall"] + nodes, channel=channel, mode=mode)


def update_node_id_cache():
    config_manager = ConfigManager()
    workspace_path = workspace_manager.workspace_path

    if not find_cm_cli():
        raise FileNotFoundError("cm-cli not found")

    tmp_path = os.path.join(config_manager.get_config_path(), "tmp")
    if not os.path.exists(tmp_path):
        os.makedirs(tmp_path)

    cache_path = os.path.join(tmp_path, "node-cache.list")
    python = resolve_workspace_python(workspace_path)
    cmd = [python, "-m", "cm_cli", "export-custom-node-ids", cache_path]

    new_env = os.environ.copy()
    new_env["COMFYUI_PATH"] = workspace_path
    subprocess.run(cmd, env=new_env, check=True)


# `update, disable, enable, fix` allows `all` param
@app.command(help="Update custom nodes or ComfyUI")
@tracking.track_command("node")
def update(
    nodes: list[str] = typer.Argument(
        ...,
        help="[all|List of custom nodes to update]",
        autocompletion=node_or_all_completer,
    ),
    channel: Annotated[
        str | None,
        typer.Option(
            show_default=False,
            help="Specify the operation mode",
            autocompletion=channel_completer,
        ),
    ] = None,
    uv_compile: Annotated[
        bool | None,
        typer.Option(
            "--uv-compile/--no-uv-compile",
            show_default=False,
            help="After updating, batch-resolve all dependencies via uv pip compile (requires ComfyUI-Manager v4.1+)",
        ),
    ] = None,
    mode: str = typer.Option(
        None,
        help="[remote|local|cache]",
        autocompletion=mode_completer,
    ),
):
    validate_mode(mode)

    execute_cm_cli(["update"] + nodes, channel=channel, uv_compile=_resolve_uv_compile(uv_compile), mode=mode)

    update_node_id_cache()


@app.command(help="Disable custom nodes")
@tracking.track_command("node")
def disable(
    nodes: list[str] = typer.Argument(
        ...,
        help="[all|List of custom nodes to disable]",
        autocompletion=node_or_all_completer,
    ),
    channel: Annotated[
        str | None,
        typer.Option(
            show_default=False,
            help="Specify the operation mode",
            autocompletion=channel_completer,
        ),
    ] = None,
    mode: str = typer.Option(
        None,
        help="[remote|local|cache]",
        autocompletion=mode_completer,
    ),
):
    validate_mode(mode)

    execute_cm_cli(["disable"] + nodes, channel=channel, mode=mode)


@app.command(help="Enable custom nodes")
@tracking.track_command("node")
def enable(
    nodes: list[str] = typer.Argument(
        ...,
        help="[all|List of custom nodes to enable]",
        autocompletion=node_or_all_completer,
    ),
    channel: Annotated[
        str | None,
        typer.Option(
            show_default=False,
            help="Specify the operation mode",
            autocompletion=channel_completer,
        ),
    ] = None,
    mode: str = typer.Option(
        None,
        help="[remote|local|cache]",
        autocompletion=mode_completer,
    ),
):
    validate_mode(mode)

    execute_cm_cli(["enable"] + nodes, channel=channel, mode=mode)


@app.command(help="Fix dependencies of custom nodes")
@tracking.track_command("node")
def fix(
    nodes: list[str] = typer.Argument(
        ...,
        help="[all|List of custom nodes to fix]",
        autocompletion=node_or_all_completer,
    ),
    channel: Annotated[
        str | None,
        typer.Option(
            show_default=False,
            help="Specify the operation mode",
            autocompletion=channel_completer,
        ),
    ] = None,
    uv_compile: Annotated[
        bool | None,
        typer.Option(
            "--uv-compile/--no-uv-compile",
            show_default=False,
            help="After fixing, batch-resolve all dependencies via uv pip compile (requires ComfyUI-Manager v4.1+)",
        ),
    ] = None,
    mode: str = typer.Option(
        None,
        help="[remote|local|cache]",
        autocompletion=mode_completer,
    ),
):
    validate_mode(mode)

    execute_cm_cli(["fix"] + nodes, channel=channel, uv_compile=_resolve_uv_compile(uv_compile), mode=mode)


@app.command(
    "install-deps",
    help="Install dependencies from dependencies file(.json) or workflow(.png/.json)",
)
@tracking.track_command("node")
def install_deps(
    deps: Annotated[
        str | None,
        typer.Option(show_default=False, help="Dependency spec file (.json)"),
    ] = None,
    workflow: Annotated[
        str | None,
        typer.Option(show_default=False, help="Workflow file (.json/.png)"),
    ] = None,
    channel: Annotated[
        str | None,
        typer.Option(
            show_default=False,
            help="Specify the operation mode",
            autocompletion=channel_completer,
        ),
    ] = None,
    uv_compile: Annotated[
        bool | None,
        typer.Option(
            "--uv-compile/--no-uv-compile",
            show_default=False,
            help="After installing, batch-resolve all dependencies via uv pip compile (requires ComfyUI-Manager v4.1+)",
        ),
    ] = None,
    mode: str = typer.Option(
        None,
        help="[remote|local|cache]",
        autocompletion=mode_completer,
    ),
):
    validate_mode(mode)

    if deps is None and workflow is None:
        print("[bold red]One of --deps or --workflow must be provided as an argument.[/bold red]\n")

    effective_uv_compile = _resolve_uv_compile(uv_compile)

    tmp_path = None
    if workflow is not None:
        workflow = os.path.abspath(os.path.expanduser(workflow))
        tmp_path = os.path.join(workspace_manager.config_manager.get_config_path(), "tmp")
        if not os.path.exists(tmp_path):
            os.makedirs(tmp_path)
        tmp_path = os.path.join(tmp_path, str(uuid.uuid4())) + ".json"

        execute_cm_cli(
            ["deps-in-workflow", "--workflow", workflow, "--output", tmp_path],
            channel,
            mode=mode,
        )

        deps_file = tmp_path
    else:
        deps_file = os.path.abspath(os.path.expanduser(deps))

    execute_cm_cli(["install-deps", deps_file], channel=channel, uv_compile=effective_uv_compile, mode=mode)

    if tmp_path is not None and os.path.exists(tmp_path):
        os.remove(tmp_path)


@app.command("deps-in-workflow", help="Generate dependencies file from workflow (.json/.png)")
@tracking.track_command("node")
def deps_in_workflow(
    workflow: Annotated[str, typer.Option(show_default=False, help="Workflow file (.json/.png)")],
    output: Annotated[str, typer.Option(show_default=False, help="Output file (.json)")],
    channel: Annotated[
        str | None,
        typer.Option(
            show_default=False,
            help="Specify the operation mode",
            autocompletion=channel_completer,
        ),
    ] = None,
    mode: str = typer.Option(
        None,
        help="[remote|local|cache]",
        autocompletion=mode_completer,
    ),
):
    validate_mode(mode)

    workflow = os.path.abspath(os.path.expanduser(workflow))
    output = os.path.abspath(os.path.expanduser(output))

    execute_cm_cli(
        ["deps-in-workflow", "--workflow", workflow, "--output", output],
        channel,
        mode=mode,
    )


def validate_node_for_publishing():
    """
    Validates node configuration and runs security checks.
    Returns the validated config if successful, raises typer.Exit if validation fails.
    """
    # Perform some validation logic here
    typer.echo("Validating node configuration...")
    config = extract_node_configuration()
    if config is None:
        raise typer.Exit(code=1)

    if not config.project.version:
        # Escape `[` chars so rich doesn't parse `[tool.comfy.version]` and
        # `["version"]` as markup tags; `]` doesn't need escaping.
        print(
            "[red]Error: project version is empty. Set `project.version` in pyproject.toml, "
            r'or configure `\[tool.comfy.version].path` if using `dynamic = \["version"]`.[/red]'
        )
        raise typer.Exit(code=1)

    # Run security checks first
    typer.echo("Running security checks...")
    try:
        # Run ruff check with security rules and --exit-zero to only warn
        cmd = [sys.executable, "-m", "ruff", "check", ".", "-q", "--select", "S102,S307,E702", "--exit-zero"]
        result = subprocess.run(cmd, capture_output=True, text=True)

        if result.stdout:
            print("[yellow]Security warnings found:[/yellow]")
            print(result.stdout)
            print(
                "[bold yellow]We will soon disable exec and eval, and multiple statements in a single line, so this will be an error soon.[/bold yellow]"
            )
        else:
            print("[green]✓ All validation checks passed successfully[/green]")

    except FileNotFoundError:
        print("[red]Ruff is not installed. Please install it with 'pip install ruff'[/red]")
        raise typer.Exit(code=1)
    except Exception as e:
        print(f"[red]Error running security check: {e}[/red]")
        raise typer.Exit(code=1)

    return config


@app.command("validate", help="Run validation checks for publishing")
@tracking.track_command("publish")
def validate():
    """
    Run validation checks that would be performed during publishing.
    """
    validate_node_for_publishing()
    # print("[green]✓ All validation checks passed successfully[/green]")


@app.command("publish", help="Publish node to registry")
@tracking.track_command("publish")
def publish(
    token: str | None = typer.Option(None, "--token", help="Personal Access Token for publishing", hide_input=True),
):
    """
    Publish a node with optional validation.
    """
    config = validate_node_for_publishing()

    # Prompt for API Key
    if not token:
        token = typer.prompt(
            "Please enter your API Key (can be created on https://registry.comfy.org)",
            hide_input=True,
        )

    # Call API to fetch node version with the token in the body
    typer.echo("Publishing node version...")
    try:
        response = registry_api.publish_node_version(config, token)
        # Zip up all files in the current directory, respecting .gitignore files.
        signed_url = response.signedUrl
        zip_filename = NODE_ZIP_FILENAME
        typer.echo("Creating zip file...")

        includes = config.tool_comfy.includes if config and config.tool_comfy else []

        if includes:
            typer.echo(f"Including additional directories: {', '.join(includes)}")

        zip_files(zip_filename, includes=includes)

        # Upload the zip file to the signed URL
        typer.echo("Uploading zip file...")
        upload_file_to_signed_url(signed_url, zip_filename)
    except Exception as e:
        ui.display_error_message({str(e)})
        raise typer.Exit(code=1)


@app.command("init", help="Init scaffolding for custom node")
@tracking.track_command("node")
def scaffold():
    if os.path.exists("pyproject.toml"):
        typer.echo("Warning: 'pyproject.toml' already exists. Will not overwrite.")
        raise typer.Exit(code=1)

    typer.echo("Initializing metadata...")
    initialize_project_config()
    typer.echo("pyproject.toml created successfully. Defaults were filled in. Please check before publishing.")


@app.command("registry-list", help="List all nodes in the registry", hidden=True)
@tracking.track_command("node")
def display_all_nodes():
    """
    Display all nodes in the registry.
    """

    nodes = None
    try:
        nodes = registry_api.list_all_nodes()
    except Exception as e:
        logging.error(f"Failed to fetch nodes from the registry: {str(e)}")
        ui.display_error_message("Failed to fetch nodes from the registry.")

    # Map Node data class instances to tuples for display
    node_data = [
        (
            node.id,
            node.name,
            node.description,
            node.author or "N/A",
            node.license or "N/A",
            ", ".join(node.tags),
            node.latest_version.version if node.latest_version else "N/A",
        )
        for node in nodes
    ]
    ui.display_table(
        node_data,
        [
            "ID",
            "Name",
            "Description",
            "Author",
            "License",
            "Tags",
            "Latest Version",
        ],
        title="List of All Nodes",
    )


@app.command(
    "registry-install",
    help="Install a node from the registry",
    hidden=True,
)
@tracking.track_command("node")
def registry_install(
    node_id: str,
    version: str | None = None,
    force_download: Annotated[
        bool,
        typer.Option(
            "--force-download",
            help="Force download the node even if it is already installed",
        ),
    ] = False,
):
    """
    Install a node from the registry.
    Args:
      node_id: The ID of the node to install.
      version: The version of the node to install. If not provided, the latest version will be installed.
    """

    # If the node ID is not provided, prompt the user to enter it
    if not node_id:
        node_id = typer.prompt("Enter the ID of the node you want to install")

    node_version = None
    try:
        # Call the API to install the node
        node_version = registry_api.install_node(node_id, version)
        if not node_version.download_url:
            logging.error("Download URL not provided from the registry.")
            ui.display_error_message(f"Failed to download the custom node {node_id}.")
            return

    except Exception as e:
        logging.error(f"Encountered an error while installing the node. error: {str(e)}")
        ui.display_error_message(f"Failed to download the custom node {node_id}.")
        return

    # Download the node archive
    custom_nodes_path = pathlib.Path(workspace_manager.workspace_path) / "custom_nodes"
    node_specific_path = custom_nodes_path / node_id  # Subdirectory for the node
    if node_specific_path.exists():
        print(
            f"[bold red] The node {node_id} already exists in the workspace. This might delete any model files in the node.[/bold red]"
        )

        confirm = ui.prompt_confirm_action(
            "Do you want to overwrite it?",
            force_download,
        )
        if not confirm:
            return
    node_specific_path.mkdir(parents=True, exist_ok=True)  # Create the directory if it doesn't exist

    local_filename = node_specific_path / f"{node_id}-{node_version.version}.zip"
    logging.debug(f"Start downloading the node {node_id} version {node_version.version} to {local_filename}")
    try:
        download_file(node_version.download_url, local_filename)
    except DownloadException as e:
        logging.error(f"Failed to download node {node_id} version {node_version.version}: {e}")
        ui.display_error_message(f"Failed to download the custom node {node_id}: {e}")
        raise typer.Exit(code=1) from None

    # Extract the downloaded archive to the custom_node directory on the workspace.
    logging.debug(f"Start extracting the node {node_id} version {node_version.version} to {custom_nodes_path}")
    extract_package_as_zip(local_filename, node_specific_path)

    # TODO: temoporary solution to run requirement.txt and install script
    execute_install_script(node_specific_path)

    # Delete the downloaded archive
    logging.debug(f"Deleting the downloaded archive {local_filename}")
    os.remove(local_filename)

    logging.info(f"Node {node_id} version {node_version.version} has been successfully installed.")


@app.command(
    "pack",
    help="Pack the current node into a zip file using git-tracked files and honoring .comfyignore patterns.",
)
@tracking.track_command("pack")
def pack():
    typer.echo("Validating node configuration...")
    config = extract_node_configuration()
    if not config:
        raise typer.Exit(code=1)

    zip_filename = NODE_ZIP_FILENAME
    includes = config.tool_comfy.includes if config and config.tool_comfy else []

    if includes:
        typer.echo(f"Including additional directories: {', '.join(includes)}")

    zip_files(zip_filename, includes=includes)

    typer.echo(f"Created zip file: {NODE_ZIP_FILENAME}")
    logging.info("Node has been packed successfully.")


@app.command("scaffold", help="Create a new ComfyUI custom node project using cookiecutter")
@tracking.track_command("node")
def scaffold_cookiecutter():
    """Create a new ComfyUI custom node project using cookiecutter."""
    import cookiecutter.main

    try:
        cookiecutter.main.cookiecutter(
            "gh:comfy-org/cookiecutter-comfy-extension",
            overwrite_if_exists=True,
        )
        console.print("[bold green]✓ Custom node project created successfully![/bold green]")
    except Exception as e:
        console.print(f"[bold red]Error creating project: {str(e)}[/bold red]")
        raise typer.Exit(code=1)


================================================
FILE: comfy_cli/command/generate/__init__.py
================================================
from comfy_cli.command.generate.app import register_with

__all__ = ["register_with"]


================================================
FILE: comfy_cli/command/generate/adapters.py
================================================
"""Per-endpoint adapters for partners whose request/response shapes don't fit
the generic schema-driven flag→JSON mold.

Two endpoints today:

- **Gemini Flash Image (nano-banana)** — Vertex AI's ``contents``/``parts``
  body, inline base64 image input, and inline base64 image output. The model
  variant lives in the URL path, not the body.
- **Seedance** (ByteDance) — assembles a ``content`` array of typed parts
  (``text`` + optional ``image_url``) and inlines its own knobs (resolution,
  duration, …) into the prompt string.

An adapter contributes three optional pieces:

- ``flags`` — replaces the schema-derived flag list for the model
- ``build_body`` — produces the JSON body from parsed flag values
- ``decode_sync`` — handles a sync response that ships inline blobs (Gemini)
- ``path_param`` — name of a flag whose value gets substituted into the URL
  path's ``{placeholder}`` (e.g. ``model`` for Gemini's templated path)
"""

from __future__ import annotations

import base64
import mimetypes
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from typing import Any

import httpx

from comfy_cli.command.generate.client import ApiError
from comfy_cli.command.generate.schema import FlagDef


@dataclass(frozen=True)
class Adapter:
    flags: list[FlagDef]
    build_body: Callable[[dict, str], dict]
    decode_sync: Callable[[dict, str, str], list[Path]] | None = None
    path_param: str | None = None


# ── Gemini / nano-banana ──────────────────────────────────────────────────

GEMINI_IMAGE_MODELS = (
    "gemini-2.5-flash-image",
    "gemini-2.5-flash-image-preview",
    "gemini-3-pro-image-preview",
)


def _inline_image(value: str) -> tuple[str, str]:
    """Return ``(mime_type, base64_str)`` for a local path, http(s) URL, or
    ``data:`` URI. Gemini accepts inline-only — there's no signed-URL path
    here, so we pull bytes locally rather than going through ``upload.py``."""
    if value.startswith("data:"):
        head, _, b64 = value.partition(",")
        mime = head.split(";", 1)[0].removeprefix("data:") or "image/png"
        return mime, b64
    if value.startswith(("http://", "https://")):
        with httpx.Client(timeout=60.0, follow_redirects=True) as c:
            r = c.get(value)
            r.raise_for_status()
            mime = (r.headers.get("content-type") or "image/png").split(";", 1)[0].strip()
            return mime, base64.b64encode(r.content).decode("ascii")
    path = Path(value).expanduser()
    if not path.is_file():
        raise ApiError(0, "", f"Image not found: {path}")
    mime, _ = mimetypes.guess_type(path.name)
    return mime or "image/png", base64.b64encode(path.read_bytes()).decode("ascii")


def _gemini_build_body(values: dict, api_key: str) -> dict[str, Any]:
    parts: list[dict[str, Any]] = [{"text": str(values["prompt"])}]
    images = values.get("image") or []
    if isinstance(images, str):
        images = [images]
    for img in images:
        mime, b64 = _inline_image(str(img))
        parts.append({"inlineData": {"mimeType": mime, "data": b64}})
    return {
        "contents": [{"role": "user", "parts": parts}],
        "generationConfig": {"responseModalities": ["IMAGE"]},
    }


def _gemini_decode_sync(body: dict, download: str, request_id: str) -> list[Path]:
    """Walk candidates[*].content.parts[*].inlineData; save each blob."""
    from comfy_cli.command.generate import output

    blobs: list[tuple[str, bytes]] = []
    for cand in body.get("candidates") or []:
        content = cand.get("content") or {}
        for part in content.get("parts") or []:
            inline = part.get("inlineData") or part.get("inline_data")
            if not inline:
                continue
            data_b64 = inline.get("data") or ""
            mime = inline.get("mimeType") or inline.get("mime_type") or "image/png"
            try:
                raw = base64.b64decode(data_b64, validate=False)
            except (ValueError, TypeError):
                continue
            blobs.append((mime, raw))
    if not blobs:
        return []
    return output.save_inline_blobs(blobs, download, request_id)


_gemini_adapter = Adapter(
    flags=[
        FlagDef(
            name="prompt",
            kind="string",
            required=True,
            description="Text instruction. For edits, describe the change.",
        ),
        FlagDef(
            name="image",
            kind="array",
            item_kind="string",
            required=False,
            description="Optional reference image(s): local path, http(s) URL, or data URI.",
        ),
        FlagDef(
            name="model",
            kind="enum",
            required=False,
            default="gemini-2.5-flash-image",
            description="Gemini image-model variant.",
            enum=list(GEMINI_IMAGE_MODELS),
        ),
    ],
    build_body=_gemini_build_body,
    decode_sync=_gemini_decode_sync,
    path_param="model",
)


# ── Seedance ──────────────────────────────────────────────────────────────

SEEDANCE_MODELS = (
    "seedance-1-0-pro-250528",
    "seedance-1-0-pro-fast-251015",
    "seedance-1-5-pro-251215",
    "seedance-1-0-lite-t2v-250428",
    "seedance-1-0-lite-i2v-250428",
)

_SEEDANCE_INLINE_KEYS = ("resolution", "ratio", "duration", "fps", "seed", "camerafixed", "watermark")


def _seedance_text(values: dict) -> str:
    """Compose the ``text`` field, appending Seedance's inline ``--rs/--rt/…``
    style overrides for any flags the user set."""
    prompt = str(values["prompt"])
    extras: list[str] = []
    for key in _SEEDANCE_INLINE_KEYS:
        v = values.get(key)
        if v is None or v == "":
            continue
        if isinstance(v, bool):
            v = "true" if v else "false"
        extras.append(f"--{key} {v}")
    return f"{prompt} {' '.join(extras)}".strip()


def _seedance_image_url(value: str, api_key: str) -> str:
    """Local paths get uploaded; data: and http(s) pass through verbatim."""
    if value.startswith(("http://", "https://", "data:")):
        return value
    from comfy_cli.command.generate import upload

    return upload.upload_path(Path(value).expanduser(), api_key).url


def _seedance_build_body(values: dict, api_key: str) -> dict[str, Any]:
    content: list[dict[str, Any]] = [{"type": "text", "text": _seedance_text(values)}]
    image = values.get("image")
    if image:
        content.append({"type": "image_url", "image_url": {"url": _seedance_image_url(str(image), api_key)}})
    body: dict[str, Any] = {
        "model": values.get("model") or SEEDANCE_MODELS[0],
        "content": content,
    }
    if "generate_audio" in values:
        body["generate_audio"] = bool(values["generate_audio"])
    if "return_last_frame" in values:
        body["return_last_frame"] = bool(values["return_last_frame"])
    return body


_seedance_adapter = Adapter(
    flags=[
        FlagDef(name="prompt", kind="string", required=True, description="Text prompt for the video."),
        FlagDef(
            name="image",
            kind="string",
            required=False,
            description="Optional first-frame image (URL, local path, or data URI). "
            "Local paths are auto-uploaded via /customers/storage.",
        ),
        FlagDef(
            name="model",
            kind="enum",
            required=False,
            default="seedance-1-0-pro-250528",
            description="Seedance model variant.",
            enum=list(SEEDANCE_MODELS),
        ),
        FlagDef(name="resolution", kind="enum", required=False, enum=["480p", "720p", "1080p"]),
        FlagDef(
            name="ratio",
            kind="enum",
            required=False,
            enum=["21:9", "16:9", "4:3", "1:1", "3:4", "9:16", "9:21", "adaptive"],
        ),
        FlagDef(name="duration", kind="integer", required=False, description="Length in seconds (3–12)."),
        FlagDef(name="fps", kind="integer", required=False, description="Frames per second (default 24)."),
        FlagDef(name="seed", kind="integer", required=False, description="RNG seed (-1 to 2^32-1)."),
        FlagDef(name="camerafixed", kind="boolean", required=False, description="Lock camera position."),
        FlagDef(name="watermark", kind="boolean", required=False, description="Include a watermark."),
        FlagDef(
            name="generate_audio",
            kind="boolean",
            required=False,
            description="Synthesize matching audio (Seedance 1.5 pro only).",
        ),
        FlagDef(
            name="return_last_frame",
            kind="boolean",
            required=False,
            description="Return the last-frame image alongside the video.",
        ),
    ],
    build_body=_seedance_build_body,
    decode_sync=None,
    path_param=None,
)


_ADAPTERS: dict[str, Adapter] = {
    "vertexai/gemini/{model}": _gemini_adapter,
    "byteplus/api/v3/contents/generations/tasks": _seedance_adapter,
}


def get(endpoint_id: str) -> Adapter | None:
    return _ADAPTERS.get(endpoint_id)


def resolve_path(template: str, values: dict, adapter: Adapter) -> str:
    """Substitute ``adapter.path_param`` into the URL template, falling back to
    the flag's ``default`` when the user didn't pass it."""
    if not adapter.path_param:
        return template
    val = values.get(adapter.path_param)
    if not val:
        for f in adapter.flags:
            if f.name == adapter.path_param:
                val = f.default
                break
    if not val:
        raise ApiError(0, "", f"Missing --{adapter.path_param}: required to fill in the URL path.")
    return template.replace("{" + adapter.path_param + "}", str(val))


================================================
FILE: comfy_cli/command/generate/app.py
================================================
"""``comfy generate`` — call ComfyUI partner nodes from the CLI.

UX shape, modeled on fal-ai's genmedia but creative-user-first:

    comfy generate <model> [--<param> value]... [--download P] [--async]
    comfy generate list [--partner P] [--style S]
    comfy generate schema <model>
    comfy generate refresh
    comfy generate resume <model> <job_id> [--download P]

The first positional is either a reserved action (``list``/``schema``/
``refresh``/``resume``) or a model alias (``flux-pro``, ``ideogram-edit``, …).
Anything not in the reserved set falls through to the generate path.
"""

from __future__ import annotations

import uuid
from pathlib import Path
from typing import Annotated

import httpx
import typer
from rich import print as rprint
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn

from comfy_cli import tracking, ui
from comfy_cli.command.generate import adapters, client, output, poll, schema, spec, upload

_HELP = "Generate images via ComfyUI partner nodes (Flux, Ideogram, DALL·E, Recraft, Stability, …)."

_CONTEXT_SETTINGS = {
    "allow_extra_args": True,
    "ignore_unknown_options": True,
    "help_option_names": [],
}


def register_with(parent: typer.Typer) -> None:
    """Wire the ``generate`` command into a Typer app. We register directly
    (rather than as a sub-app via ``add_typer``) so the first positional after
    ``generate`` can be a model alias — Click groups would treat that as a
    subcommand name and error."""

    @parent.command(name="generate", help=_HELP, context_settings=_CONTEXT_SETTINGS)
    @tracking.track_command()
    def _generate_entry(
        ctx: typer.Context,
        target: Annotated[
            str | None,
            typer.Argument(
                help="A model alias (e.g. flux-pro, ideogram-edit, dalle) "
                "or one of: list, schema, refresh, upload, resume.",
            ),
        ] = None,
    ) -> None:
        if target is None or target in {"-h", "--help"}:
            _print_top_help()
            raise typer.Exit(code=0)
        if target == "list":
            return _list_models(list(ctx.args))
        if target == "schema":
            return _schema(list(ctx.args))
        if target == "refresh":
            return _refresh()
        if target == "upload":
            return _upload(list(ctx.args))
        if target == "resume":
            return _resume(list(ctx.args))
        _generate(target, list(ctx.args))


def _separate_meta_flags(extra_args: list[str]) -> tuple[list[str], dict[str, str | bool]]:
    """Pull run-level flags out of the user's argv tail."""
    meta_names = {"download", "async", "json", "timeout", "api-key"}
    meta: dict[str, str | bool] = {}
    remaining: list[str] = []
    i = 0
    while i < len(extra_args):
        tok = extra_args[i]
        if tok.startswith("--"):
            body = tok[2:]
            raw: str | None = None
            if "=" in body:
                body, raw = body.split("=", 1)
            if body in meta_names:
                if body in {"async", "json"}:
                    meta[body] = True if raw is None else raw.lower() not in {"false", "0", "no"}
                    i += 1
                    continue
                if raw is None:
                    if i + 1 >= len(extra_args):
                        raise schema.SchemaError(f"--{body}: missing value")
                    raw = extra_args[i + 1]
                    i += 2
                else:
                    i += 1
                meta[body] = raw
                continue
        remaining.append(tok)
        i += 1
    return remaining, meta


def _show_schema_help(endpoint: spec.Endpoint) -> None:
    """Print the schema-driven help block for a model."""
    flags = schema.flags_for(endpoint)
    alias = spec.preferred_alias(endpoint.id)
    name = alias or endpoint.id
    if alias:
        rprint(f"[bold]Model:[/bold] {alias}  [dim]({endpoint.id})[/dim]")
    else:
        rprint(f"[bold]Model:[/bold] {endpoint.id}")
    body = schema.help_text(endpoint, flags)
    rprint(body)
    rprint("")
    rprint("[dim]Example:[/dim]")
    rprint(f"  {schema.example_invocation(endpoint, flags, display_name=name)}")


def _spinner() -> Progress:
    return Progress(
        SpinnerColumn(),
        TextColumn("[bold blue]{task.description}"),
        TimeElapsedColumn(),
        transient=True,
    )


def _emit_result(result: poll.PollResult, *, request_id: str, download: str | None, as_json: bool) -> None:
    if as_json:
        output.print_json(result.raw)
        return
    if result.status != "succeeded":
        rprint(f"[bold red]Job {result.status}: {result.error or 'unknown error'}[/bold red]")
        output.print_json(result.raw)
        raise typer.Exit(code=1)
    if download and result.image_urls:
        saved = output.save_urls(result.image_urls, download, request_id)
        output.print_urls(result.image_urls, request_id=request_id)
        output.print_saved(saved)
    else:
        output.print_urls(result.image_urls, request_id=request_id)
        if download and not result.image_urls:
            rprint("[yellow]--download requested but no image URLs found in response.[/yellow]")


def _generate(model: str, extra_args: list[str]) -> None:
    try:
        ep = spec.get_endpoint(model)
    except spec.SpecError as e:
        rprint(f"[bold red]{e}[/bold red]")
        raise typer.Exit(code=1)

    if any(a in {"--help", "-h"} for a in extra_args):
        _show_schema_help(ep)
        raise typer.Exit(code=0)

    try:
        remaining, meta = _separate_meta_flags(extra_args)
    except schema.SchemaError as e:
        rprint(f"[bold red]{e}[/bold red]")
        raise typer.Exit(code=1)

    flags = schema.flags_for(ep)
    try:
        values = schema.parse_args(flags, remaining)
    except schema.SchemaError as e:
        rprint(f"[bold red]{e}[/bold red]")
        name = spec.preferred_alias(ep.id) or ep.id
        rprint(f"[dim]Run `comfy generate schema {name}` for the full parameter list.[/dim]")
        raise typer.Exit(code=1)

    try:
        api_key = client.resolve_api_key(meta.get("api-key") if isinstance(meta.get("api-key"), str) else None)
    except client.ApiError as e:
        rprint(f"[bold red]{e}[/bold red]")
        raise typer.Exit(code=1)

    timeout_raw = meta.get("timeout", "300")
    try:
        timeout = float(timeout_raw) if isinstance(timeout_raw, str) else 300.0
    except ValueError:
        rprint(f"[bold red]--timeout: expected number, got {timeout_raw!r}[/bold red]")
        raise typer.Exit(code=1)

    do_async = bool(meta.get("async", False))
    download = meta.get("download") if isinstance(meta.get("download"), str) else None
    as_json = bool(meta.get("json", False))

    try:
        _apply_upload_transforms(values, flags, ep, api_key)
    except (client.ApiError, httpx.HTTPError) as e:
        rprint(f"[bold red]Upload failed: {e}[/bold red]")
        raise typer.Exit(code=1)

    request_id = str(uuid.uuid4())[:8]
    try:
        resp = client.send_request(ep, values, flags, api_key, timeout=timeout)
    except httpx.HTTPError as e:
        rprint(f"[bold red]Network error contacting {spec.base_url()}: {e}[/bold red]")
        raise typer.Exit(code=1) from e

    try:
        client.raise_for_status(resp)
    except client.ApiError as e:
        rprint(f"[bold red]API error {e.status}[/bold red]\n{e.body}")
        raise typer.Exit(code=1) from e

    if resp.headers.get("content-type", "").startswith("image/"):
        if download:
            saved = output.save_binary_response(resp, download, request_id)
            output.print_saved([saved])
        else:
            rprint("[yellow]Binary image response; nothing saved. Pass --download <path> to write it to disk.[/yellow]")
        return

    try:
        body = resp.json()
    except ValueError:
        rprint("[bold red]Unexpected non-JSON response.[/bold red]")
        rprint(resp.text[:500])
        raise typer.Exit(code=1)

    if ep.polling:
        job_id = poll.extract_job_id(ep.polling, body) or request_id
        name = spec.preferred_alias(ep.id) or ep.id
        if do_async:
            if as_json:
                output.print_json(body)
            else:
                rprint(f"[bold green]Submitted:[/bold green] {name}")
                rprint(f"  job id: {job_id}")
                rprint(f"  resume: comfy generate resume {name} {job_id}")
            return

        poller = poll.get_poller(ep.polling)
        with _spinner() as prog:
            task = prog.add_task(f"Generating with {name} (job {job_id})", total=None)

            def _on_progress(p: float) -> None:
                prog.update(task, description=f"Generating ({p * 100:.0f}%)")

            result = poller(
                body,
                api_key=api_key,
                timeout=timeout,
                on_progress=_on_progress,
                create_path=ep.path,
            )
        _emit_result(result, request_id=job_id, download=download, as_json=as_json)
        return

    adapter = adapters.get(ep.id)
    if adapter is not None and adapter.decode_sync is not None:
        body = resp.json()
        if as_json:
            output.print_json(body)
            return
        if not download:
            rprint("[yellow]Image data returned inline. Pass --download <path> to save.[/yellow]")
            return
        saved = adapter.decode_sync(body, download, request_id)
        if saved:
            output.print_saved(saved)
        else:
            rprint("[yellow]No image data found in response.[/yellow]")
            output.print_json(body)
        return

    result = poll.sync_result_from_response(resp)
    _emit_result(result, request_id=request_id, download=download, as_json=as_json)


def _arg_value(args: list[str], *names: str) -> str | None:
    for i, tok in enumerate(args):
        for n in names:
            if tok == n and i + 1 < len(args):
                return args[i + 1]
            if tok.startswith(n + "="):
                return tok.split("=", 1)[1]
    return None


def _list_models(extra_args: list[str]) -> None:
    """`comfy generate list` — show available models with their short aliases."""
    partner = _arg_value(extra_args, "--partner", "-p")
    category = _arg_value(extra_args, "--category", "--style", "-c")
    query = _arg_value(extra_args, "--query", "-q")
    eps = spec.list_endpoints(partner=partner, category=category, query=query)
    if not eps:
        rprint("[yellow]No models match those filters.[/yellow]")
        raise typer.Exit(code=0)
    rows = [
        (
            spec.preferred_alias(e.id) or e.id,
            e.partner,
            e.category,
            "async" if e.polling else "sync",
            (e.summary[:60] + "…") if len(e.summary) > 61 else e.summary,
        )
        for e in eps
    ]
    ui.display_table(rows, ["Model", "Partner", "Style", "Mode", "Summary"], title="Comfy Generate — Models")
    rprint("\n[dim]Run `comfy generate schema <model>` to see parameters for a model.[/dim]")


def _schema(extra_args: list[str]) -> None:
    """`comfy generate schema <model>` — show params for a model (fal-style)."""
    if not extra_args or extra_args[0].startswith("-"):
        rprint("[bold red]Usage: comfy generate schema <model>[/bold red]")
        raise typer.Exit(code=1)
    try:
        ep = spec.get_endpoint(extra_args[0])
    except spec.SpecError as e:
        rprint(f"[bold red]{e}[/bold red]")
        raise typer.Exit(code=1)
    _show_schema_help(ep)


def _refresh() -> None:
    url = spec.base_url() + "/openapi.yml"
    try:
        with httpx.Client(timeout=30.0, follow_redirects=True) as cli:
            r = cli.get(url, headers={"Comfy-Env": "comfy-cli", "User-Agent": "comfy-cli/api"})
            r.raise_for_status()
    except httpx.HTTPError as e:
        rprint(f"[bold red]Failed to fetch {url}: {e}[/bold red]")
        raise typer.Exit(code=1)
    path = spec.write_cache(r.text)
    rprint(f"[bold green]Refreshed model catalog at {path}[/bold green]")


def _upload(extra_args: list[str]) -> None:
    """`comfy generate upload <file-or-url> [--json] [--api-key K]`."""
    try:
        remaining, meta = _separate_meta_flags(extra_args)
    except schema.SchemaError as e:
        rprint(f"[bold red]{e}[/bold red]")
        raise typer.Exit(code=1)
    # `remaining` already excludes recognized --meta flags AND their values, so
    # `comfy generate upload --api-key KEY ./img.png` correctly resolves to "./img.png".
    if not remaining:
        rprint("[bold red]Usage: comfy generate upload <file-or-url> [--json][/bold red]")
        raise typer.Exit(code=1)
    target = remaining[0]
    try:
        api_key = client.resolve_api_key(meta.get("api-key") if isinstance(meta.get("api-key"), str) else None)
    except client.ApiError as e:
        rprint(f"[bold red]{e}[/bold red]")
        raise typer.Exit(code=1)
    as_json = bool(meta.get("json", False))
    try:
        result = upload.upload_target(target, api_key)
    except (client.ApiError, httpx.HTTPError) as e:
        rprint(f"[bold red]Upload failed: {e}[/bold red]")
        raise typer.Exit(code=1)
    if as_json:
        output.print_json(
            {
                "url": result.url,
                "expires_at": result.expires_at,
                "existing_file": result.existing_file,
                "hint": "Pass this URL as the model's image/input_image field.",
            }
        )
        return
    rprint(f"[bold green]Uploaded:[/bold green] {result.url}")
    if result.expires_at:
        rprint(f"  expires: {result.expires_at}")
    if result.existing_file:
        rprint("  [dim](server already had a hash-match; no bytes transferred)[/dim]")


def _apply_upload_transforms(values: dict, flags: list[schema.FlagDef], endpoint: spec.Endpoint, api_key: str) -> None:
    """When the user supplies a local file path for a field that expects a
    base64 blob or a URL, transform it transparently.

    This only applies to JSON endpoints — multipart endpoints already stream
    file paths natively via httpx and don't need pre-uploading. Endpoints with
    a custom adapter handle their own asset shaping inside ``build_body``.
    """
    if adapters.get(endpoint.id) is not None:
        return
    if endpoint.request_content_type != "application/json":
        return
    flag_by_name = {f.name: f for f in flags}
    for name, value in list(values.items()):
        flag = flag_by_name.get(name)
        if flag is None or flag.upload_mode is None or not isinstance(value, str):
            continue
        if value.startswith(("http://", "https://", "data:")):
            continue
        path = Path(value).expanduser()
        if not path.is_file():
            continue
        if flag.upload_mode == "base64":
            import base64 as _base64

            try:
                data = path.read_bytes()
            except OSError as e:
                raise client.ApiError(0, "", f"Unable to read file for --{name}: {path} ({e})") from e
            values[name] = _base64.b64encode(data).decode("ascii")
            rprint(f"[dim]base64-encoded {path.name} for --{name}[/dim]")
        elif flag.upload_mode == "url":
            rprint(f"[dim]uploading {path.name} for --{name}…[/dim]")
            result = upload.upload_path(path, api_key)
            values[name] = result.url


def _resume(extra_args: list[str]) -> None:
    if len(extra_args) < 2:
        rprint("[bold red]Usage: comfy generate resume <model> <job_id> [--download PATH] [--json][/bold red]")
        raise typer.Exit(code=1)
    model, job_id = extra_args[0], extra_args[1]
    tail = extra_args[2:]
    try:
        ep = spec.get_endpoint(model)
    except spec.SpecError as e:
        rprint(f"[bold red]{e}[/bold red]")
        raise typer.Exit(code=1)
    if not ep.polling:
        rprint(f"[bold red]{model} is a sync model; nothing to resume.[/bold red]")
        raise typer.Exit(code=1)
    try:
        _, meta = _separate_meta_flags(tail)
    except schema.SchemaError as e:
        rprint(f"[bold red]{e}[/bold red]")
        raise typer.Exit(code=1)
    try:
        api_key = client.resolve_api_key(meta.get("api-key") if isinstance(meta.get("api-key"), str) else None)
    except client.ApiError as e:
        rprint(f"[bold red]{e}[/bold red]")
        raise typer.Exit(code=1)
    timeout = float(meta.get("timeout") or 300.0) if isinstance(meta.get("timeout"), str) else 300.0
    download = meta.get("download") if isinstance(meta.get("download"), str) else None
    as_json = bool(meta.get("json", False))

    try:
        initial = poll.build_synthetic_initial(ep.polling, job_id, base_url=spec.base_url())
    except client.ApiError as e:
        rprint(f"[bold red]{e}[/bold red]")
        raise typer.Exit(code=1)

    poller = poll.get_poller(ep.polling)
    with _spinner() as prog:
        task = prog.add_task(f"Resuming job {job_id}", total=None)

        def _on_progress(p: float) -> None:
            prog.update(task, description=f"Job {job_id} ({p * 100:.0f}%)")

        result = poller(
            initial,
            api_key=api_key,
            timeout=timeout,
            on_progress=_on_progress,
            create_path=ep.path,
        )
    _emit_result(result, request_id=job_id, download=download, as_json=as_json)


def _print_top_help() -> None:
    """Custom help that emphasizes the model-first UX over Typer's auto-help."""
    rprint("[bold]comfy generate[/bold] — call ComfyUI partner nodes")
    rprint("")
    rprint("[bold]Usage:[/bold]")
    rprint("  comfy generate <model> [--<param> value]... [--download PATH] [--async] [--api-key KEY]")
    rprint("")
    rprint("[bold]Examples:[/bold]")
    rprint('  comfy generate flux-pro --prompt "a cat on the moon" --width 1024 --height 1024 --download cat.png')
    rprint(
        '  comfy generate ideogram-edit --image cat.png --mask m.png --prompt "add sunglasses" --rendering_speed TURBO'
    )
    rprint('  comfy generate dalle --prompt "a watercolor whale" --download whale.png')
    rprint("")
    rprint("[bold]Actions:[/bold]")
    rprint("  comfy generate list                    Browse available models")
    rprint("  comfy generate schema <model>          Show parameters for a model")
    rprint("  comfy generate refresh                 Refresh the model catalog")
    rprint("  comfy generate upload <file-or-url>    Host a local file or remote URL and print its signed URL")
    rprint("  comfy generate resume <model> <job>    Resume an async job")
    rprint("")
    rprint("[dim]Auth: set COMFY_API_KEY or pass --api-key. Get one at https://platform.comfy.org.[/dim]")


================================================
FILE: comfy_cli/command/generate/client.py
================================================
"""HTTP client for the Comfy cloud API.

A thin wrapper around httpx that:
- attaches ``Authorization: Bearer $COMFY_API_KEY`` to every request,
- targets ``$COMFY_API_BASE_URL`` (defaulting to ``https://api.comfy.org``),
- splits a request payload into JSON or multipart based on the endpoint's
  declared content-type, streaming any ``format: binary`` fields as files.
"""

from __future__ import annotations

import os
from pathlib import Path
from typing import Any

import httpx

from comfy_cli.command.generate import spec
from comfy_cli.command.generate.schema import FlagDef


class ApiError(RuntimeError):
    def __init__(self, status: int, body: str, message: str | None = None) -> None:
        super().__init__(message or f"HTTP {status}: {body}")
        self.status = status
        self.body = body


def resolve_api_key(explicit: str | None = None) -> str:
    """Order: explicit flag → COMFY_API_KEY env var. Raise if neither set."""
    key = explicit.strip() if isinstance(explicit, str) and explicit.strip() else os.environ.get("COMFY_API_KEY", "")
    key = key.strip()
    if not key:
        raise ApiError(
            401,
            "",
            "No API key. Pass --api-key or set COMFY_API_KEY in your environment. "
            "Generate one at https://platform.comfy.org/api-keys.",
        )
    return key


def _split_payload(
    values: dict[str, Any], flags: list[FlagDef], content_type: str
) -> tuple[dict[str, Any] | None, list[tuple[str, Any]] | None, dict[str, Any] | None]:
    """Return (json_body, multipart_files, multipart_data).

    For JSON endpoints: json_body is the dict, others are None.
    For multipart: files is a list of (field_name, (filename, fileobj, mime)) tuples
    and data is the non-file form fields (stringified or JSON-encoded as needed).
    """
    flag_by_name = {f.name: f for f in flags}
    if content_type != "multipart/form-data":
        return values, None, None

    files: list[tuple[str, Any]] = []
    data: dict[str, Any] = {}
    for name, value in values.items():
        flag = flag_by_name.get(name)
        if flag and flag.kind == "binary":
            path = Path(value) if not isinstance(value, Path) else value
            if not path.is_file():
                raise ApiError(0, "", f"--{name}: file not found: {path}")
            files.append((name, (path.name, path.open("rb"), "application/octet-stream")))
        elif flag and flag.kind == "array" and flag.item_kind == "binary":
            for p in value:
                p = Path(p) if not isinstance(p, Path) else p
                if not p.is_file():
                    raise ApiError(0, "", f"--{name}: file not found: {p}")
                files.append((name, (p.name, p.open("rb"), "application/octet-stream")))
        elif flag and flag.kind in ("object", "array"):
            # Multipart form fields are scalar — JSON-encode complex values.
            import json as _json

            data[name] = _json.dumps(value)
        elif flag and flag.kind == "boolean":
            data[name] = "true" if value else "false"
        else:
            data[name] = str(value)
    return None, files, data


def _auth_headers(api_key: str, extra: dict[str, str] | None = None) -> dict[str, str]:
    # The server accepts two key types on different headers:
    #   - "comfyui-..." API keys → X-API-Key (validated by sha256 lookup)
    #   - Firebase ID tokens     → Authorization: Bearer (validated as a JWT)
    # See comfy-api server/middleware/authentication/comfy_firebase_auth.go.
    headers = {"User-Agent": "comfy-cli/api", "Comfy-Env": "comfy-cli"}
    if api_key.startswith("comfyui-"):
        headers["X-API-Key"] = api_key
    else:
        headers["Authorization"] = f"Bearer {api_key}"
    if extra:
        headers.update(extra)
    return headers


def send_request(
    endpoint: spec.Endpoint,
    values: dict[str, Any],
    flags: list[FlagDef],
    api_key: str,
    timeout: float = 120.0,
) -> httpx.Response:
    """Send the initial request for `endpoint` with the given typed values."""
    from comfy_cli.command.generate import adapters as _adapters

    adapter = _adapters.get(endpoint.id)
    url_path = _adapters.resolve_path(endpoint.path, values, adapter) if adapter else endpoint.path
    url = spec.base_url() + url_path
    if adapter is not None:
        json_body, files, data = adapter.build_body(values, api_key), None, None
    else:
        json_body, files, data = _split_payload(values, flags, endpoint.request_content_type)
    headers = _auth_headers(api_key)
    try:
        if endpoint.method.lower() == "get":
            return httpx.get(url, params=values, headers=headers, timeout=timeout)
        if endpoint.request_content_type == "application/json":
            return httpx.post(url, json=json_body, headers=headers, timeout=timeout)
        return httpx.post(url, files=files, data=data, headers=headers, timeout=timeout)
    finally:
        # Ensure file handles from multipart are closed even on httpx errors.
        if files:
            for _name, payload in files:
                fileobj = payload[1]
                try:
                    fileobj.close()
                except Exception:  # noqa: BLE001
                    pass


def get(url: str, api_key: str, timeout: float = 60.0) -> httpx.Response:
    """GET helper for polling sibling endpoints and downloading result URLs."""
    if url.startswith("/"):
        url = spec.base_url() + url
    return httpx.get(url, headers=_auth_headers(api_key), timeout=timeout)


def download_bytes(url: str, timeout: float = 120.0) -> bytes:
    """Fetch result media. These URLs are usually pre-signed and not Comfy-hosted,
    so we don't send the Comfy bearer token."""
    with httpx.Client(timeout=timeout, follow_redirects=True) as client:
        r = client.get(url)
        r.raise_for_status()
        return r.content


def raise_for_status(resp: httpx.Response) -> None:
    if resp.status_code < 400:
        return
    try:
        body = resp.json()
        import json as _json

        body_str = _json.dumps(body, indent=2)
    except Exception:  # noqa: BLE001
        body_str = resp.text
    raise ApiError(resp.status_code, body_str)


================================================
FILE: comfy_cli/command/generate/output.py
================================================
"""Output handling: --download templating, URL printing, binary response writes.

Templating tokens: ``{request_id}``, ``{index}``, ``{ext}``. A trailing ``/``
on the template means "use a default filename in this directory."
"""

from __future__ import annotations

import json
import mimetypes
from pathlib import Path

import httpx
from rich import print as rprint

from comfy_cli.command.generate import client

_EXT_FROM_MIME = {
    "image/png": "png",
    "image/jpeg": "jpg",
    "image/jpg": "jpg",
    "image/webp": "webp",
    "image/gif": "gif",
    "image/svg+xml": "svg",
}


def _ext_from_url(url: str) -> str:
    suffix = Path(url.split("?", 1)[0]).suffix.lstrip(".").lower()
    return suffix or "png"


def _ext_from_response(resp: httpx.Response) -> str:
    ct = resp.headers.get("content-type", "").split(";", 1)[0].strip().lower()
    if ct in _EXT_FROM_MIME:
        return _EXT_FROM_MIME[ct]
    guess = mimetypes.guess_extension(ct) or ""
    return guess.lstrip(".") or "bin"


def _resolve_template(template: str, request_id: str, index: int, ext: str) -> Path:
    if template.endswith(("/", "\\")) or Path(template).is_dir():
        # Directory shorthand.
        path = Path(template) / f"{request_id}_{index}.{ext}"
    else:
        path = Path(template.format(request_id=request_id, index=index, ext=ext))
    return path.expanduser()


def save_urls(urls: list[str], template: str, request_id: str) -> list[Path]:
    """Download each URL and save under the resolved template path. Returns saved paths.

    Multi-URL responses (a video + thumbnail from Luma, for example) need a
    per-output filename. If the
Download .txt
gitextract_3k1yj69h/

├── .coveragerc
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── codecov.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── build-and-test.yml
│       ├── publish_package.yml
│       ├── pytest.yml
│       ├── ruff_check.yml
│       ├── run-on-gpu.yml
│       ├── test-mac.yml
│       └── test-windows.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .pylintrc
├── DEV_README.md
├── LICENSE
├── README.md
├── comfy_cli/
│   ├── __init__.py
│   ├── __main__.py
│   ├── cmdline.py
│   ├── command/
│   │   ├── __init__.py
│   │   ├── code_search.py
│   │   ├── custom_nodes/
│   │   │   ├── __init__.py
│   │   │   ├── bisect_custom_nodes.py
│   │   │   ├── cm_cli_util.py
│   │   │   └── command.py
│   │   ├── generate/
│   │   │   ├── __init__.py
│   │   │   ├── adapters.py
│   │   │   ├── app.py
│   │   │   ├── client.py
│   │   │   ├── output.py
│   │   │   ├── poll.py
│   │   │   ├── schema.py
│   │   │   ├── spec/
│   │   │   │   └── openapi.yml
│   │   │   ├── spec.py
│   │   │   └── upload.py
│   │   ├── github/
│   │   │   └── pr_info.py
│   │   ├── install.py
│   │   ├── launch.py
│   │   ├── models/
│   │   │   └── models.py
│   │   ├── pr_command.py
│   │   └── run.py
│   ├── config_manager.py
│   ├── constants.py
│   ├── cuda_detect.py
│   ├── env_checker.py
│   ├── file_utils.py
│   ├── git_utils.py
│   ├── logging.py
│   ├── pr_cache.py
│   ├── registry/
│   │   ├── __init__.py
│   │   ├── api.py
│   │   ├── config_parser.py
│   │   └── types.py
│   ├── resolve_python.py
│   ├── standalone.py
│   ├── tracking.py
│   ├── typing.py
│   ├── ui.py
│   ├── update.py
│   ├── utils.py
│   ├── uv.py
│   ├── workflow_to_api.py
│   └── workspace_manager.py
├── conda.listing.txt
├── docs/
│   ├── DESIGN-uv-compile.md
│   ├── PRD-uv-compile.md
│   └── TESTING-e2e.md
├── pylock.toml
├── pyproject.toml
├── pyrightconfig.json
└── tests/
    ├── comfy_cli/
    │   ├── command/
    │   │   ├── generate/
    │   │   │   ├── __init__.py
    │   │   │   ├── test_adapters.py
    │   │   │   ├── test_app.py
    │   │   │   ├── test_client.py
    │   │   │   ├── test_output.py
    │   │   │   ├── test_poll.py
    │   │   │   ├── test_schema.py
    │   │   │   ├── test_spec.py
    │   │   │   ├── test_upload.py
    │   │   │   └── test_video_poll.py
    │   │   ├── github/
    │   │   │   └── test_pr.py
    │   │   ├── models/
    │   │   │   └── test_models.py
    │   │   ├── nodes/
    │   │   │   ├── test_bisect_custom_nodes.py
    │   │   │   ├── test_node_init.py
    │   │   │   ├── test_node_install.py
    │   │   │   ├── test_pack.py
    │   │   │   └── test_publish.py
    │   │   ├── test_bisect_parse.py
    │   │   ├── test_cm_cli_util.py
    │   │   ├── test_code_search.py
    │   │   ├── test_command.py
    │   │   ├── test_frontend_pr.py
    │   │   ├── test_launch_frontend_pr.py
    │   │   ├── test_manager_gui.py
    │   │   ├── test_npm_help.py
    │   │   └── test_run.py
    │   ├── conftest.py
    │   ├── fixtures/
    │   │   ├── sd15_expected_api.json
    │   │   ├── sd15_object_info.json
    │   │   └── sd15_ui_workflow.json
    │   ├── registry/
    │   │   ├── test_api.py
    │   │   └── test_config_parser.py
    │   ├── test_aria2_download.py
    │   ├── test_cm_cli_python_resolution.py
    │   ├── test_cmdline_python_resolution.py
    │   ├── test_config_manager.py
    │   ├── test_cuda_detect.py
    │   ├── test_cuda_detect_real.py
    │   ├── test_custom_nodes_python_resolution.py
    │   ├── test_env_checker.py
    │   ├── test_file_utils.py
    │   ├── test_global_python_install.py
    │   ├── test_install.py
    │   ├── test_install_python_resolution.py
    │   ├── test_launch_python_resolution.py
    │   ├── test_models_python_resolution.py
    │   ├── test_resolve_python.py
    │   ├── test_standalone.py
    │   ├── test_tracking.py
    │   ├── test_ui.py
    │   ├── test_update.py
    │   ├── test_utils.py
    │   ├── test_workflow_to_api.py
    │   └── test_workspace_manager.py
    ├── e2e/
    │   ├── test_e2e.py
    │   ├── test_e2e_uv_compile.py
    │   └── workflow.json
    ├── test_file_utils_network.py
    └── uv/
        ├── mock_comfy/
        │   ├── custom_nodes/
        │   │   ├── x/
        │   │   │   ├── pyproject.toml
        │   │   │   ├── requirements.txt
        │   │   │   ├── setup.cfg
        │   │   │   └── setup.py
        │   │   ├── y/
        │   │   │   ├── setup.cfg
        │   │   │   └── setup.py
        │   │   └── z/
        │   │       └── setup.py
        │   ├── pyproject.toml
        │   ├── setup.cfg
        │   └── setup.py
        ├── mock_requirements/
        │   ├── core_reqs.txt
        │   ├── x_reqs.txt
        │   └── y_reqs.txt
        ├── test_torch_backend_compile.py
        └── test_uv.py
Download .txt
SYMBOL INDEX (1845 symbols across 94 files)

FILE: comfy_cli/cmdline.py
  function main (line 37) | def main():
  class MutuallyExclusiveValidator (line 41) | class MutuallyExclusiveValidator:
    method __init__ (line 42) | def __init__(self):
    method reset_for_testing (line 45) | def reset_for_testing(self):
    method validate (line 48) | def validate(self, _ctx: typer.Context, param: typer.CallbackParam, va...
  function help (line 62) | def help(ctx: typer.Context):
  function entry (line 68) | def entry(
  function validate_commit_and_version (line 137) | def validate_commit_and_version(commit: str | None, ctx: typer.Context) ...
  function _resolve_cuda (line 147) | def _resolve_cuda(
  function install (line 186) | def install(
  function update (line 389) | def update(
  function run (line 428) | def run(
  function validate_comfyui (line 493) | def validate_comfyui(_env_checker):
  function stop (line 501) | def stop():
  function launch (line 522) | def launch(
  function set_default (line 539) | def set_default(
  function which (line 569) | def which():
  function env (line 582) | def env():
  function nodes (line 596) | def nodes():
  function models (line 602) | def models():
  function feedback (line 608) | def feedback():
  function dependency (line 640) | def dependency():
  function standalone (line 651) | def standalone(

FILE: comfy_cli/command/code_search.py
  function _build_query (line 27) | def _build_query(query: str, repo: str | None, count: int) -> str:
  function _fetch_results (line 42) | def _fetch_results(query: str) -> dict:
  function _format_results (line 48) | def _format_results(search: dict) -> list[dict]:
  function _get_stats (line 91) | def _get_stats(search: dict) -> dict:
  function _print_results (line 99) | def _print_results(results: list[dict], stats: dict, json_output: bool) ...
  function code_search (line 147) | def code_search(

FILE: comfy_cli/command/custom_nodes/bisect_custom_nodes.py
  class BisectState (line 19) | class BisectState(NamedTuple):
    method good (line 34) | def good(self) -> BisectState:
    method bad (line 58) | def bad(self) -> BisectState:
    method save (line 82) | def save(self, state_file=None):
    method reset (line 88) | def reset(self):
    method load (line 99) | def load(cls, state_file=None) -> BisectState:
    method inactive_nodes (line 107) | def inactive_nodes(self) -> list[str]:
    method set_custom_node_enabled_states (line 110) | def set_custom_node_enabled_states(self):
    method __str__ (line 116) | def __str__(self):
  function parse_cm_output (line 125) | def parse_cm_output(cm_output: str, pinned_nodes: set[str] | None = None...
  function start (line 146) | def start(
  function good (line 182) | def good():
  function bad (line 201) | def bad():
  function reset (line 220) | def reset():

FILE: comfy_cli/command/custom_nodes/cm_cli_util.py
  function find_cm_cli (line 29) | def find_cm_cli() -> bool:
  function resolve_manager_gui_mode (line 57) | def resolve_manager_gui_mode(not_installed_value: str | None = None) -> ...
  function execute_cm_cli (line 89) | def execute_cm_cli(

FILE: comfy_cli/command/custom_nodes/command.py
  class ShowTarget (line 44) | class ShowTarget(str, Enum):
  function _resolve_uv_compile (line 54) | def _resolve_uv_compile(uv_compile: bool | None, fast_deps: bool = False...
  function validate_comfyui_manager (line 72) | def validate_comfyui_manager():
  function run_script (line 78) | def run_script(cmd, cwd="."):
  function get_installed_packages (line 91) | def get_installed_packages():
  function try_install_script (line 115) | def try_install_script(repo_path, install_cmd, instant_execution=False):
  function execute_install_script (line 161) | def execute_install_script(repo_path):
  function save_snapshot (line 189) | def save_snapshot(
  function restore_snapshot (line 204) | def restore_snapshot(
  function restore_dependencies (line 247) | def restore_dependencies(
  function disable_manager (line 262) | def disable_manager():
  function enable_gui (line 272) | def enable_gui():
  function disable_gui (line 282) | def disable_gui():
  function enable_legacy_gui (line 292) | def enable_legacy_gui():
  function migrate_legacy (line 302) | def migrate_legacy(
  function uv_compile_default (line 429) | def uv_compile_default(
  function clear (line 447) | def clear():
  function update_cache (line 453) | def update_cache():
  function node_completer (line 464) | def node_completer(incomplete: str) -> list[str]:
  function node_or_all_completer (line 476) | def node_or_all_completer(incomplete: str) -> list[str]:
  function validate_mode (line 492) | def validate_mode(mode):
  function show (line 504) | def show(
  function simple_show (line 529) | def simple_show(
  function install (line 555) | def install(
  function reinstall (line 639) | def reinstall(
  function uv_sync (line 694) | def uv_sync():
  function uninstall (line 700) | def uninstall(
  function update_node_id_cache (line 725) | def update_node_id_cache():
  function update (line 748) | def update(
  function disable (line 785) | def disable(
  function enable (line 812) | def enable(
  function fix (line 839) | def fix(
  function install_deps (line 877) | def install_deps(
  function deps_in_workflow (line 941) | def deps_in_workflow(
  function validate_node_for_publishing (line 970) | def validate_node_for_publishing():
  function validate (line 1018) | def validate():
  function publish (line 1028) | def publish(
  function scaffold (line 1069) | def scaffold():
  function display_all_nodes (line 1081) | def display_all_nodes():
  function registry_install (line 1127) | def registry_install(
  function pack (line 1207) | def pack():
  function scaffold_cookiecutter (line 1227) | def scaffold_cookiecutter():

FILE: comfy_cli/command/generate/adapters.py
  class Adapter (line 38) | class Adapter:
  function _inline_image (line 54) | def _inline_image(value: str) -> tuple[str, str]:
  function _gemini_build_body (line 75) | def _gemini_build_body(values: dict, api_key: str) -> dict[str, Any]:
  function _gemini_decode_sync (line 89) | def _gemini_decode_sync(body: dict, download: str, request_id: str) -> l...
  function _seedance_text (line 155) | def _seedance_text(values: dict) -> str:
  function _seedance_image_url (line 170) | def _seedance_image_url(value: str, api_key: str) -> str:
  function _seedance_build_body (line 179) | def _seedance_build_body(values: dict, api_key: str) -> dict[str, Any]:
  function get (line 250) | def get(endpoint_id: str) -> Adapter | None:
  function resolve_path (line 254) | def resolve_path(template: str, values: dict, adapter: Adapter) -> str:

FILE: comfy_cli/command/generate/app.py
  function register_with (line 39) | def register_with(parent: typer.Typer) -> None:
  function _separate_meta_flags (line 73) | def _separate_meta_flags(extra_args: list[str]) -> tuple[list[str], dict...
  function _show_schema_help (line 105) | def _show_schema_help(endpoint: spec.Endpoint) -> None:
  function _spinner (line 121) | def _spinner() -> Progress:
  function _emit_result (line 130) | def _emit_result(result: poll.PollResult, *, request_id: str, download: ...
  function _generate (line 148) | def _generate(model: str, extra_args: list[str]) -> None:
  function _arg_value (line 275) | def _arg_value(args: list[str], *names: str) -> str | None:
  function _list_models (line 285) | def _list_models(extra_args: list[str]) -> None:
  function _schema (line 308) | def _schema(extra_args: list[str]) -> None:
  function _refresh (line 321) | def _refresh() -> None:
  function _upload (line 334) | def _upload(extra_args: list[str]) -> None:
  function _apply_upload_transforms (line 375) | def _apply_upload_transforms(values: dict, flags: list[schema.FlagDef], ...
  function _resume (line 412) | def _resume(extra_args: list[str]) -> None:
  function _print_top_help (line 463) | def _print_top_help() -> None:

FILE: comfy_cli/command/generate/client.py
  class ApiError (line 22) | class ApiError(RuntimeError):
    method __init__ (line 23) | def __init__(self, status: int, body: str, message: str | None = None)...
  function resolve_api_key (line 29) | def resolve_api_key(explicit: str | None = None) -> str:
  function _split_payload (line 43) | def _split_payload(
  function _auth_headers (line 83) | def _auth_headers(api_key: str, extra: dict[str, str] | None = None) -> ...
  function send_request (line 98) | def send_request(
  function get (line 133) | def get(url: str, api_key: str, timeout: float = 60.0) -> httpx.Response:
  function download_bytes (line 140) | def download_bytes(url: str, timeout: float = 120.0) -> bytes:
  function raise_for_status (line 149) | def raise_for_status(resp: httpx.Response) -> None:

FILE: comfy_cli/command/generate/output.py
  function _ext_from_url (line 28) | def _ext_from_url(url: str) -> str:
  function _ext_from_response (line 33) | def _ext_from_response(resp: httpx.Response) -> str:
  function _resolve_template (line 41) | def _resolve_template(template: str, request_id: str, index: int, ext: s...
  function save_urls (line 50) | def save_urls(urls: list[str], template: str, request_id: str) -> list[P...
  function save_inline_blobs (line 73) | def save_inline_blobs(blobs: list[tuple[str, bytes]], template: str, req...
  function save_binary_response (line 92) | def save_binary_response(resp: httpx.Response, template: str, request_id...
  function print_urls (line 101) | def print_urls(urls: list[str], request_id: str | None = None) -> None:
  function print_json (line 112) | def print_json(body: dict | list | str) -> None:
  function print_saved (line 119) | def print_saved(paths: list[Path]) -> None:

FILE: comfy_cli/command/generate/poll.py
  class PollResult (line 30) | class PollResult:
  function _now (line 62) | def _now() -> float:
  function _extract_urls (line 66) | def _extract_urls(node: Any) -> list[str]:
  function _dotget (line 90) | def _dotget(body: Any, path: str) -> Any:
  function _first (line 101) | def _first(body: Any, paths: tuple[str, ...]) -> Any:
  function _sleep (line 109) | def _sleep(seconds: float) -> None:
  function poll_bfl (line 113) | def poll_bfl(
  class PollSpec (line 148) | class PollSpec:
  function _build_poll_url (line 249) | def _build_poll_url(spec: PollSpec, job_id: str, create_path: str | None...
  function poll_generic (line 258) | def poll_generic(
  function extract_job_id (line 313) | def extract_job_id(name: str, body: dict[str, Any]) -> str | None:
  function build_synthetic_initial (line 324) | def build_synthetic_initial(name: str, job_id: str, base_url: str | None...
  function get_poller (line 347) | def get_poller(name: str) -> Callable[..., PollResult]:
  function sync_result_from_response (line 380) | def sync_result_from_response(resp: httpx.Response) -> PollResult:

FILE: comfy_cli/command/generate/schema.py
  class FlagDef (line 22) | class FlagDef:
  class SchemaError (line 33) | class SchemaError(ValueError):
  function _classify (line 37) | def _classify(prop: dict[str, Any]) -> tuple[str, str | None]:
  function _detect_upload_mode (line 66) | def _detect_upload_mode(name: str, prop: dict[str, Any]) -> str | None:
  function flags_for (line 89) | def flags_for(endpoint: Endpoint) -> list[FlagDef]:
  function _coerce (line 120) | def _coerce(flag: FlagDef, raw: str) -> Any:
  function parse_args (line 167) | def parse_args(flags: list[FlagDef], argv: list[str]) -> dict[str, Any]:
  function help_text (line 226) | def help_text(endpoint: Endpoint, flags: list[FlagDef]) -> str:
  function example_invocation (line 263) | def example_invocation(endpoint: Endpoint, flags: list[FlagDef], display...

FILE: comfy_cli/command/generate/spec.py
  class _YamlLoader (line 24) | class _YamlLoader(yaml.SafeLoader):
  class Endpoint (line 54) | class Endpoint:
  function aliases (line 162) | def aliases() -> dict[str, str]:
  function preferred_alias (line 167) | def preferred_alias(endpoint_id: str) -> str | None:
  function resolve_alias (line 172) | def resolve_alias(target: str) -> str:
  class SpecError (line 259) | class SpecError(RuntimeError):
  function _select_spec_path (line 263) | def _select_spec_path() -> Path:
  function load_raw_spec (line 274) | def load_raw_spec() -> dict[str, Any]:
  function base_url (line 280) | def base_url() -> str:
  function _resolve_ref (line 289) | def _resolve_ref(spec: dict[str, Any], ref: str) -> dict[str, Any]:
  function _resolve (line 299) | def _resolve(spec: dict[str, Any], node: Any, seen: frozenset[str] = fro...
  function _detect_polling (line 314) | def _detect_polling(partner: str, response_schema: dict[str, Any]) -> st...
  function _registry (line 329) | def _registry() -> dict[str, Endpoint]:
  function list_endpoints (line 377) | def list_endpoints(
  function get_endpoint (line 394) | def get_endpoint(endpoint_id: str) -> Endpoint:
  function _unknown_endpoint_message (line 402) | def _unknown_endpoint_message(endpoint_id: str) -> str:
  function write_cache (line 415) | def write_cache(yaml_text: str) -> Path:
  function active_spec_path (line 425) | def active_spec_path() -> Path:

FILE: comfy_cli/command/generate/upload.py
  class UploadResult (line 33) | class UploadResult:
  function _guess_content_type (line 39) | def _guess_content_type(name: str) -> str:
  function _sha256_hex (line 44) | def _sha256_hex(data: bytes) -> str:
  function _request_signed_url (line 48) | def _request_signed_url(
  function _put_bytes (line 68) | def _put_bytes(upload_url: str, data: bytes, content_type: str) -> None:
  function upload_bytes (line 76) | def upload_bytes(data: bytes, file_name: str, api_key: str, content_type...
  function upload_path (line 98) | def upload_path(path: Path | str, api_key: str) -> UploadResult:
  function upload_remote_url (line 109) | def upload_remote_url(url: str, api_key: str) -> UploadResult:
  function upload_target (line 123) | def upload_target(target: str | Path, api_key: str) -> UploadResult:

FILE: comfy_cli/command/github/pr_info.py
  class PRInfo (line 4) | class PRInfo(NamedTuple):
    method is_fork (line 15) | def is_fork(self) -> bool:

FILE: comfy_cli/command/install.py
  function get_os_details (line 32) | def get_os_details():
  function _pip_install_torch (line 38) | def _pip_install_torch(python: str, index_args: list[str]) -> subprocess...
  function pip_install_comfyui_dependencies (line 46) | def pip_install_comfyui_dependencies(
  function pip_install_manager (line 117) | def pip_install_manager(repo_dir, python=sys.executable):
  function execute (line 146) | def execute(
  function handle_pr_checkout (line 302) | def handle_pr_checkout(pr_ref: str, comfy_path: str) -> str:
  function validate_version (line 362) | def validate_version(version: str) -> str | None:
  class GitHubRateLimitError (line 392) | class GitHubRateLimitError(Exception):
  function handle_github_rate_limit (line 396) | def handle_github_rate_limit(response):
  class GithubRelease (line 411) | class GithubRelease(TypedDict):
  function clone_comfyui (line 426) | def clone_comfyui(url: str, repo_dir: str):
  function _resolve_latest_tag_from_local (line 438) | def _resolve_latest_tag_from_local(repo_dir: str) -> tuple[str | None, b...
  function _parse_github_owner_repo (line 511) | def _parse_github_owner_repo(url: str | None) -> tuple[str, str] | None:
  function checkout_stable_comfyui (line 529) | def checkout_stable_comfyui(version: str, repo_dir: str, url: str | None...
  function get_latest_release (line 589) | def get_latest_release(repo_owner: str, repo_name: str) -> GithubRelease...
  function _parse_pr_reference (line 633) | def _parse_pr_reference(
  function parse_pr_reference (line 669) | def parse_pr_reference(pr_ref: str) -> tuple[str, str, int | None]:
  function fetch_pr_info (line 673) | def fetch_pr_info(repo_owner: str, repo_name: str, pr_number: int) -> PR...
  function find_pr_by_branch (line 707) | def find_pr_by_branch(repo_owner: str, repo_name: str, username: str, br...
  function _print_npm_not_found_help (line 739) | def _print_npm_not_found_help(node_version: str) -> None:
  function verify_node_tools (line 825) | def verify_node_tools() -> bool:
  function handle_temporary_frontend_pr (line 923) | def handle_temporary_frontend_pr(frontend_pr: str) -> str | None:
  function parse_frontend_pr_reference (line 1030) | def parse_frontend_pr_reference(pr_ref: str) -> tuple[str, str, int | No...

FILE: comfy_cli/command/launch.py
  function _get_manager_flags (line 27) | def _get_manager_flags() -> list[str]:
  function launch_comfyui (line 53) | def launch_comfyui(extra, frontend_pr=None, python=sys.executable):
  function launch (line 149) | def launch(
  function background_launch (line 191) | def background_launch(extra, frontend_pr=None):
  function launch_and_monitor (line 249) | async def launch_and_monitor(cmd, listen, port):

FILE: comfy_cli/command/models/models.py
  function get_workspace (line 36) | def get_workspace() -> pathlib.Path:
  function _format_elapsed (line 40) | def _format_elapsed(seconds: float) -> str:
  function potentially_strip_param_url (line 52) | def potentially_strip_param_url(path_name: str) -> str:
  function check_huggingface_url (line 56) | def check_huggingface_url(url: str) -> tuple[bool, str | None, str | Non...
  function check_civitai_url (line 93) | def check_civitai_url(url: str) -> tuple[bool, bool, int | None, int | N...
  function request_civitai_model_version_api (line 154) | def request_civitai_model_version_api(version_id: int, headers: dict | N...
  function request_civitai_model_api (line 173) | def request_civitai_model_api(model_id: int, version_id: int = None, hea...
  function download (line 202) | def download(
  function remove (line 374) | def remove(
  function list_models (line 442) | def list_models(path: pathlib.Path) -> list[pathlib.Path]:
  function list_command (line 451) | def list_command(

FILE: comfy_cli/command/pr_command.py
  function list_cached (line 23) | def list_cached() -> None:
  function clean_cache (line 60) | def clean_cache(

FILE: comfy_cli/command/run.py
  function is_ui_workflow (line 23) | def is_ui_workflow(workflow) -> bool:
  function _validate_api_workflow (line 31) | def _validate_api_workflow(workflow):
  function fetch_object_info (line 41) | def fetch_object_info(host: str, port: int, timeout: int) -> dict:
  function execute (line 68) | def execute(
  class ExecutionProgress (line 176) | class ExecutionProgress(Progress):
    method get_renderables (line 177) | def get_renderables(self):
  class WorkflowExecution (line 193) | class WorkflowExecution:
    method __init__ (line 194) | def __init__(self, workflow, host, port, verbose, progress, local_path...
    method connect (line 215) | def connect(self):
    method queue (line 219) | def queue(self):
    method watch_execution (line 248) | def watch_execution(self):
    method update_overall_progress (line 257) | def update_overall_progress(self):
    method get_node_title (line 260) | def get_node_title(self, node_id):
    method log_node (line 268) | def log_node(self, type, node_id):
    method format_image_path (line 285) | def format_image_path(self, img):
    method on_message (line 299) | def on_message(self, message):
    method on_executing (line 318) | def on_executing(self, data):
    method on_cached (line 333) | def on_cached(self, data):
    method on_progress (line 340) | def on_progress(self, data):
    method on_executed (line 352) | def on_executed(self, data):
    method on_error (line 367) | def on_error(self, data):

FILE: comfy_cli/config_manager.py
  class ConfigManager (line 10) | class ConfigManager:
    method __init__ (line 11) | def __init__(self):
    method get_config_path (line 17) | def get_config_path():
    method get_config_file_path (line 20) | def get_config_file_path(self):
    method write_config (line 23) | def write_config(self):
    method set (line 32) | def set(self, key, value):
    method get (line 39) | def get(self, key):
    method get_bool (line 45) | def get_bool(self, key) -> bool | None:
    method get_or_override (line 56) | def get_or_override(self, env_key: str, config_key: str, set_value: st...
    method load (line 77) | def load(self):
    method get_env_data (line 96) | def get_env_data(self):
    method remove_background (line 156) | def remove_background(self):
    method get_cli_version (line 161) | def get_cli_version(self):

FILE: comfy_cli/constants.py
  class OS (line 5) | class OS(str, Enum):
  class PROC (line 11) | class PROC(str, Enum):
  class CUDAVersion (line 79) | class CUDAVersion(str, Enum):
  class ROCmVersion (line 89) | class ROCmVersion(str, Enum):
  class GPU_OPTION (line 97) | class GPU_OPTION(str, Enum):

FILE: comfy_cli/cuda_detect.py
  function _load_libcuda (line 27) | def _load_libcuda() -> ctypes.CDLL:
  function _detect_via_ctypes (line 53) | def _detect_via_ctypes() -> int | None:
  function _detect_via_nvidia_smi (line 79) | def _detect_via_nvidia_smi() -> tuple[int, int] | None:
  function detect_cuda_driver_version (line 98) | def detect_cuda_driver_version() -> tuple[int, int] | None:
  function resolve_cuda_wheel (line 121) | def resolve_cuda_wheel(driver_version: tuple[int, int]) -> str | None:

FILE: comfy_cli/env_checker.py
  function format_python_version (line 17) | def format_python_version(version_info):
  function check_comfy_server_running (line 35) | def check_comfy_server_running(port=8188, host="localhost"):
  class EnvChecker (line 50) | class EnvChecker:
    method __init__ (line 69) | def __init__(self):
    method is_isolated_env (line 75) | def is_isolated_env(self):
    method get_isolated_env (line 78) | def get_isolated_env(self):
    method check (line 87) | def check(self):
    method fill_print_table (line 91) | def fill_print_table(self):

FILE: comfy_cli/file_utils.py
  class DownloadException (line 16) | class DownloadException(Exception):
  function guess_status_code_reason (line 20) | def guess_status_code_reason(status_code: int, message: str) -> str:
  function check_unauthorized (line 49) | def check_unauthorized(url: str, headers: dict | None = None) -> bool:
  function _poll_aria2_download (line 68) | def _poll_aria2_download(download) -> None:
  function _download_file_aria2 (line 113) | def _download_file_aria2(url: str, local_filepath: pathlib.Path, headers...
  class _TransientHTTPStatusError (line 183) | class _TransientHTTPStatusError(Exception):
    method __init__ (line 186) | def __init__(self, status_code: int, reason: str):
  function _cleanup_partial (line 195) | def _cleanup_partial(filepath: pathlib.Path) -> None:
  function _friendly_network_error (line 203) | def _friendly_network_error(exc: Exception) -> str:
  function _download_file_httpx (line 228) | def _download_file_httpx(
  function download_file (line 272) | def download_file(url: str, local_filepath: pathlib.Path, headers: dict ...
  function _load_comfyignore_spec (line 329) | def _load_comfyignore_spec(ignore_filename: str = ".comfyignore") -> Pat...
  function list_git_tracked_files (line 344) | def list_git_tracked_files(base_path: str | os.PathLike = ".") -> list[s...
  function _normalize_path (line 356) | def _normalize_path(path: str) -> str:
  function _is_force_included (line 363) | def _is_force_included(rel_path: str, include_prefixes: list[str]) -> bool:
  function zip_files (line 367) | def zip_files(zip_filename, includes=None):
  function upload_file_to_signed_url (line 464) | def upload_file_to_signed_url(signed_url: str, file_path: str):
  function extract_package_as_zip (line 475) | def extract_package_as_zip(file_path: pathlib.Path, extract_path: pathli...

FILE: comfy_cli/git_utils.py
  function sanitize_for_local_branch (line 13) | def sanitize_for_local_branch(branch_name: str) -> str:
  function git_checkout_tag (line 27) | def git_checkout_tag(repo_path: str, tag: str) -> bool:
  function checkout_pr (line 94) | def checkout_pr(repo_path: str, pr_info: PRInfo) -> bool:

FILE: comfy_cli/logging.py
  function setup_logging (line 12) | def setup_logging():
  function debug (line 30) | def debug(message):
  function info (line 34) | def info(message):
  function warning (line 38) | def warning(message):
  function error (line 42) | def error(message):

FILE: comfy_cli/pr_cache.py
  class PRCache (line 19) | class PRCache:
    method __init__ (line 33) | def __init__(self) -> None:
    method get_frontend_cache_path (line 40) | def get_frontend_cache_path(self, pr_info) -> Path:
    method get_cache_info_path (line 48) | def get_cache_info_path(self, cache_path: Path) -> Path:
    method is_cache_valid (line 52) | def is_cache_valid(self, pr_info, cache_path: Path) -> bool:
    method save_cache_info (line 81) | def save_cache_info(self, pr_info, cache_path: Path) -> None:
    method get_cached_frontend_path (line 99) | def get_cached_frontend_path(self, pr_info) -> Path | None:
    method _load_cache_info (line 108) | def _load_cache_info(self, cache_dir: Path) -> dict | None:
    method _clean_specific_pr_cache (line 120) | def _clean_specific_pr_cache(self, frontend_cache: Path, pr_number: in...
    method clean_frontend_cache (line 131) | def clean_frontend_cache(self, pr_number: int | None = None) -> None:
    method _calculate_cache_size_mb (line 144) | def _calculate_cache_size_mb(self, cache_dir: Path) -> float:
    method _get_cache_info_with_metadata (line 149) | def _get_cache_info_with_metadata(self, cache_dir: Path) -> dict | None:
    method list_cached_frontends (line 157) | def list_cached_frontends(self) -> list[dict]:
    method _is_cache_expired (line 173) | def _is_cache_expired(self, cached_at: str) -> bool:
    method _get_expired_items (line 181) | def _get_expired_items(self, cached_items: list[dict]) -> list[dict]:
    method _get_excess_items (line 190) | def _get_excess_items(self, cached_items: list[dict], expired_items: l...
    method _remove_cache_item (line 198) | def _remove_cache_item(self, item: dict) -> None:
    method enforce_cache_limits (line 206) | def enforce_cache_limits(self) -> None:
    method get_cache_age (line 219) | def get_cache_age(self, cached_at: str) -> str:

FILE: comfy_cli/registry/api.py
  class RegistryAPI (line 17) | class RegistryAPI:
    method __init__ (line 18) | def __init__(self):
    method determine_base_url (line 21) | def determine_base_url(self):
    method publish_node_version (line 30) | def publish_node_version(self, node_config: PyProjectConfig, token) ->...
    method list_all_nodes (line 88) | def list_all_nodes(self):
    method install_node (line 103) | def install_node(self, node_id, version=None):
  function map_node_version (line 128) | def map_node_version(api_node_version):
  function map_node_to_node_class (line 150) | def map_node_to_node_class(api_node_data):
  function serialize_license (line 175) | def serialize_license(license: License) -> str:

FILE: comfy_cli/registry/config_parser.py
  function create_comfynode_config (line 47) | def create_comfynode_config():
  function sanitize_node_name (line 98) | def sanitize_node_name(name: str) -> str:
  function validate_and_extract_os_classifiers (line 122) | def validate_and_extract_os_classifiers(classifiers: list) -> list:
  function validate_and_extract_accelerator_classifiers (line 142) | def validate_and_extract_accelerator_classifiers(classifiers: list) -> l...
  function validate_version (line 170) | def validate_version(version: str, field_name: str) -> str:
  function _strip_url_credentials (line 192) | def _strip_url_credentials(url: str) -> str:
  function initialize_project_config (line 204) | def initialize_project_config():
  function _resolve_dynamic_version (line 305) | def _resolve_dynamic_version(pyproject_dir: pathlib.Path, rel_path: str)...
  function _parse_dynamic_fields (line 379) | def _parse_dynamic_fields(project_data) -> list[str]:
  function _extract_version (line 398) | def _extract_version(project_data, comfy_data, pyproject_dir: pathlib.Pa...
  function extract_node_configuration (line 461) | def extract_node_configuration(

FILE: comfy_cli/registry/types.py
  class NodeVersion (line 5) | class NodeVersion:
  class Node (line 15) | class Node:
  class PublishNodeVersionResponse (line 28) | class PublishNodeVersionResponse:
  class URLs (line 34) | class URLs:
  class Model (line 42) | class Model:
  class ComfyConfig (line 48) | class ComfyConfig:
  class License (line 59) | class License:
  class ProjectConfig (line 65) | class ProjectConfig:
  class PyProjectConfig (line 80) | class PyProjectConfig:

FILE: comfy_cli/resolve_python.py
  function _get_python_binary (line 12) | def _get_python_binary(env_path: str) -> str:
  function _is_externally_managed (line 18) | def _is_externally_managed() -> bool:
  function resolve_workspace_python (line 24) | def resolve_workspace_python(workspace_path: str | None = None) -> str:
  function create_workspace_venv (line 46) | def create_workspace_venv(workspace_path: str) -> str:
  function ensure_workspace_python (line 56) | def ensure_workspace_python(workspace_path: str) -> str:

FILE: comfy_cli/standalone.py
  function _resolve_python_version (line 31) | def _resolve_python_version(asset_url_prefix: str, minor_version: str) -...
  function download_standalone_python (line 59) | def download_standalone_python(
  class StandalonePython (line 99) | class StandalonePython:
    method FromDistro (line 101) | def FromDistro(
    method FromTarball (line 123) | def FromTarball(fpath: PathLike, name: PathLike = "python", show_progr...
    method __init__ (line 131) | def __init__(self, rpath: PathLike):
    method clean (line 150) | def clean(self):
    method run_module (line 154) | def run_module(self, mod: str, *args: str):
    method pip_install (line 164) | def pip_install(self, *args: str):
    method uv_install (line 167) | def uv_install(self, *args: str):
    method install_comfy_cli (line 170) | def install_comfy_cli(self, dev: bool = False):
    method run_comfy_cli (line 176) | def run_comfy_cli(self, *args: str):
    method install_comfy (line 179) | def install_comfy(self, *args: str, gpu_arg: str = "--nvidia"):
    method dehydrate_comfy_deps (line 182) | def dehydrate_comfy_deps(
    method rehydrate_comfy_deps (line 200) | def rehydrate_comfy_deps(self, packWheels: bool = False):
    method to_tarball (line 210) | def to_tarball(self, outPath: PathLike | None = None, show_progress: b...

FILE: comfy_cli/tracking.py
  function enable (line 36) | def enable():
  function disable (line 43) | def disable():
  function track_event (line 48) | def track_event(event_name: str, properties: any = None):
  function track_command (line 64) | def track_command(sub_command: str = None):
  function prompt_tracking_consent (line 92) | def prompt_tracking_consent(skip_prompt: bool = False, default_value: bo...
  function init_tracking (line 104) | def init_tracking(enable_tracking: bool):

FILE: comfy_cli/ui.py
  function show_progress (line 17) | def show_progress(iterable, total, description="Downloading..."):
  function prompt_autocomplete (line 41) | def prompt_autocomplete(
  function prompt_select (line 61) | def prompt_select(
  function prompt_select_enum (line 84) | def prompt_select_enum(question: str, choices: list[E], force_prompting:...
  function prompt_input (line 107) | def prompt_input(question: str, default: str = "", force_prompting: bool...
  function prompt_multi_select (line 126) | def prompt_multi_select(prompt: str, choices: list[str]) -> list[str]:
  function prompt_confirm_action (line 141) | def prompt_confirm_action(prompt: str, default: bool) -> bool:
  function display_table (line 157) | def display_table(data: list[tuple], column_names: list[str], title: str...
  function display_error_message (line 177) | def display_error_message(message: str) -> None:

FILE: comfy_cli/update.py
  function check_for_newer_pypi_version (line 14) | def check_for_newer_pypi_version(package_name, current_version):
  function check_for_updates (line 30) | def check_for_updates():
  function get_version_from_pyproject (line 38) | def get_version_from_pyproject():
  function notify_update (line 43) | def notify_update(current_version: str, newer_version: str):

FILE: comfy_cli/utils.py
  function singleton (line 23) | def singleton(cls):
  function get_os (line 43) | def get_os():
  function get_proc (line 56) | def get_proc():
  function install_conda_package (line 67) | def install_conda_package(package_name):
  function get_not_user_set_default_workspace (line 76) | def get_not_user_set_default_workspace():
  function kill_all (line 80) | def kill_all(pid):
  function is_running (line 91) | def is_running(pid):
  function create_choice_completer (line 99) | def create_choice_completer(opts: list[str]):
  function download_url (line 106) | def download_url(
  function extract_tarball (line 137) | def extract_tarball(
  function create_tarball (line 192) | def create_tarball(

FILE: comfy_cli/uv.py
  function _run (line 14) | def _run(cmd: list[str], cwd: PathLike, check: bool = True) -> subproces...
  function _check_call (line 18) | def _check_call(cmd: list[str], cwd: PathLike | None = None):
  function _req_re_closure (line 46) | def _req_re_closure(name: str) -> re.Pattern[str]:
  function parse_uv_compile_error (line 50) | def parse_uv_compile_error(err: str) -> tuple[str, list[str]]:
  function parse_req_file (line 65) | def parse_req_file(rf: PathLike, skips: list[str] | None = None):
  class DependencyCompiler (line 85) | class DependencyCompiler:
    method Find_Req_Files (line 112) | def Find_Req_Files(*ders: PathLike) -> list[Path]:
    method Install_Build_Deps (line 128) | def Install_Build_Deps(executable: PathLike = sys.executable):
    method Compile (line 134) | def Compile(
    method Install (line 208) | def Install(
    method Sync (line 261) | def Sync(
    method Download (line 290) | def Download(
    method Wheel (line 326) | def Wheel(
    method Resolve_Gpu (line 362) | def Resolve_Gpu(gpu: GPU_OPTION | None):
    method __init__ (line 377) | def __init__(
    method find_core_reqs (line 436) | def find_core_reqs(self):
    method find_ext_reqs (line 439) | def find_ext_reqs(self):
    method make_override (line 443) | def make_override(self):
    method compile_core_plus_ext (line 474) | def compile_core_plus_ext(self):
    method handle_opencv (line 507) | def handle_opencv(self):
    method compile_deps (line 528) | def compile_deps(self):
    method install_deps (line 533) | def install_deps(self):
    method install_dists (line 542) | def install_dists(self):
    method install_wheels (line 552) | def install_wheels(self):
    method install_wheels_directly (line 562) | def install_wheels_directly(self):
    method sync_core_plus_ext (line 571) | def sync_core_plus_ext(self):
    method fetch_dep_dists (line 579) | def fetch_dep_dists(self, skip_uv: bool = False):
    method fetch_dep_wheels (line 594) | def fetch_dep_wheels(self, skip_uv: bool = False):

FILE: comfy_cli/workflow_to_api.py
  class WorkflowConversionError (line 59) | class WorkflowConversionError(Exception):
  function is_api_format (line 63) | def is_api_format(workflow: Any) -> bool:
  function is_subgraph_uuid (line 77) | def is_subgraph_uuid(node_type: Any) -> bool:
  function convert_ui_to_api (line 87) | def convert_ui_to_api(workflow: dict, object_info: dict) -> dict:
  function _has_group_nodes (line 175) | def _has_group_nodes(workflow: dict) -> bool:
  function _strip_orphan_link_inputs (line 189) | def _strip_orphan_link_inputs(api_prompt: dict[str, dict]) -> None:
  class _SubgraphCtx (line 211) | class _SubgraphCtx:
    method __init__ (line 214) | def __init__(self) -> None:
  function _collect_subgraph_defs (line 223) | def _collect_subgraph_defs(workflow: dict) -> dict[str, dict]:
  function _expand_subgraphs (line 242) | def _expand_subgraphs(
  function _outer_slot_to_input_idx (line 283) | def _outer_slot_to_input_idx(outer_node: dict, sg_def: dict) -> dict[int...
  function _expand_one_subgraph (line 299) | def _expand_one_subgraph(
  function _rewrite_internal_input (line 396) | def _rewrite_internal_input(
  function _rewrite_links_for_subgraphs (line 417) | def _rewrite_links_for_subgraphs(links: list, ctx: _SubgraphCtx, nodes: ...
  function _resolve_subgraph_output (line 456) | def _resolve_subgraph_output(node_id_str: str, slot: Any, ctx: _Subgraph...
  function _resolve_subgraph_input_all (line 469) | def _resolve_subgraph_input_all(
  function _is_valid_connection (line 499) | def _is_valid_connection(type_a: Any, type_b: Any) -> bool:
  function _build_link_map (line 522) | def _build_link_map(links: list) -> dict[int, dict]:
  function _collect_primitive_values (line 538) | def _collect_primitive_values(nodes: list[dict]) -> dict[str, Any]:
  function _collect_bypassed (line 549) | def _collect_bypassed(nodes: list[dict]) -> set[str]:
  function _collect_reroute_sources (line 553) | def _collect_reroute_sources(nodes: list[dict], link_map: dict[int, dict...
  function _collect_get_set_mappings (line 573) | def _collect_get_set_mappings(
  function _collect_excluded (line 607) | def _collect_excluded(nodes: list[dict]) -> set[str]:
  class _Tracers (line 628) | class _Tracers:
    method __init__ (line 631) | def __init__(
    method trace_reroute (line 652) | def trace_reroute(self, src_id: Any, src_slot: Any) -> tuple[Any, Any]:
    method trace_get_set (line 664) | def trace_get_set(self, src_id: Any, src_slot: Any) -> tuple[Any, Any]:
    method trace_bypassed (line 679) | def trace_bypassed(self, src_id: Any, src_slot: Any) -> tuple[Any, Any]:
  function _wrap_widget_value (line 769) | def _wrap_widget_value(value: Any) -> Any:
  function process_dynamic_prompt (line 780) | def process_dynamic_prompt(value: str) -> str:
  function _resolve_dynamic_prompt (line 797) | def _resolve_dynamic_prompt(value: str) -> str:
  function _parse_dynamic_prompt_block (line 817) | def _parse_dynamic_prompt_block(value: str, i: int) -> tuple[str, int]:
  function _dynamic_prompt_input_names (line 852) | def _dynamic_prompt_input_names(node_type: str | None, node: dict | None...
  function _build_api_node (line 874) | def _build_api_node(
  function _schema_for (line 964) | def _schema_for(node_type: str, node: dict, object_info: dict) -> dict |...
  function _schema_input_def (line 973) | def _schema_input_def(schema: Any) -> dict:
  function _get_ordered_input_names (line 988) | def _get_ordered_input_names(node_type: str, node: dict, object_info: di...
  function _is_widget_input (line 1011) | def _is_widget_input(input_spec: Any) -> tuple[bool, bool]:
  function _dynamic_combo_sub_inputs (line 1044) | def _dynamic_combo_sub_inputs(
  function _get_widget_name_order (line 1074) | def _get_widget_name_order(node_type: str, node: dict, object_info: dict...
  function _fallback_widget_names (line 1103) | def _fallback_widget_names(node: dict, widget_values: list[Any]) -> list...
  function _filter_control_values (line 1139) | def _filter_control_values(
  function _has_control_after_generate_companion (line 1202) | def _has_control_after_generate_companion(input_name: str, input_spec: A...
  function _collect_widget_inputs (line 1225) | def _collect_widget_inputs(
  function _absorb_dict_widget_values (line 1280) | def _absorb_dict_widget_values(widget_values: list[Any], out: dict[str, ...
  function _collect_default_inputs (line 1302) | def _collect_default_inputs(
  function _extract_default (line 1328) | def _extract_default(input_spec: Any) -> Any:
  function _normalize_combo_values (line 1344) | def _normalize_combo_values(schema: dict | None, inputs: dict[str, Any])...

FILE: comfy_cli/workspace_manager.py
  class ModelPath (line 19) | class ModelPath:
  class Model (line 24) | class Model:
  class Basics (line 33) | class Basics:
  class CustomNode (line 39) | class CustomNode:
  class ComfyLockYAMLStruct (line 45) | class ComfyLockYAMLStruct:
  function _paths_match (line 51) | def _paths_match(path_a: str, path_b: str) -> bool:
  function _has_comfyui_markers (line 58) | def _has_comfyui_markers(path: str) -> bool:
  function _find_comfyui_root (line 64) | def _find_comfyui_root(path: str) -> str | None:
  function check_comfy_repo (line 78) | def check_comfy_repo(path) -> tuple[bool, str | None]:
  function save_yaml (line 146) | def save_yaml(file_path: str, metadata: ComfyLockYAMLStruct):
  function check_file_is_model (line 169) | def check_file_is_model(path):
  class WorkspaceType (line 174) | class WorkspaceType(Enum):
  class WorkspaceManager (line 182) | class WorkspaceManager:
    method __init__ (line 183) | def __init__(
    method setup_workspace_manager (line 195) | def setup_workspace_manager(
    method set_recent_workspace (line 208) | def set_recent_workspace(self, path: str):
    method set_default_workspace (line 214) | def set_default_workspace(self, path: str):
    method set_default_launch_extras (line 220) | def set_default_launch_extras(self, extras: str):
    method __get_specified_workspace (line 226) | def __get_specified_workspace(self) -> str | None:
    method get_workspace_path (line 232) | def get_workspace_path(self) -> tuple[str, WorkspaceType]:
    method scan_dir (line 306) | def scan_dir(self):
    method scan_dir_concur (line 318) | def scan_dir_concur(self):
    method load_metadata (line 331) | def load_metadata(self):
    method save_metadata (line 339) | def save_metadata(self):
    method fill_print_table (line 343) | def fill_print_table(self):

FILE: tests/comfy_cli/command/generate/test_adapters.py
  function test_nano_banana_alias_resolves (line 15) | def test_nano_banana_alias_resolves():
  function test_gemini_adapter_overrides_schema_flags (line 22) | def test_gemini_adapter_overrides_schema_flags():
  function test_gemini_build_body_text_only (line 30) | def test_gemini_build_body_text_only():
  function test_gemini_build_body_inlines_local_image (line 38) | def test_gemini_build_body_inlines_local_image(tmp_path):
  function test_gemini_build_body_inlines_remote_url (line 52) | def test_gemini_build_body_inlines_remote_url(monkeypatch):
  function test_gemini_build_body_inlines_data_uri (line 81) | def test_gemini_build_body_inlines_data_uri():
  function test_gemini_inline_image_missing_path_raises (line 92) | def test_gemini_inline_image_missing_path_raises(tmp_path):
  function test_gemini_decode_sync_saves_inline_blobs (line 97) | def test_gemini_decode_sync_saves_inline_blobs(tmp_path):
  function test_gemini_decode_sync_handles_snake_case_keys (line 106) | def test_gemini_decode_sync_handles_snake_case_keys(tmp_path):
  function test_gemini_decode_sync_returns_empty_when_blocked (line 117) | def test_gemini_decode_sync_returns_empty_when_blocked(tmp_path):
  function test_gemini_resolve_path_substitutes_model (line 123) | def test_gemini_resolve_path_substitutes_model():
  function test_gemini_send_request_hits_substituted_path (line 130) | def test_gemini_send_request_hits_substituted_path(monkeypatch):
  function test_seedance_alias_resolves (line 154) | def test_seedance_alias_resolves():
  function test_seedance_adapter_overrides_flags (line 161) | def test_seedance_adapter_overrides_flags():
  function test_seedance_build_body_text_only (line 171) | def test_seedance_build_body_text_only():
  function test_seedance_build_body_inlines_knobs_into_text (line 177) | def test_seedance_build_body_inlines_knobs_into_text():
  function test_seedance_build_body_uploads_local_image (line 198) | def test_seedance_build_body_uploads_local_image(monkeypatch, tmp_path):
  function test_seedance_build_body_keeps_remote_url_verbatim (line 218) | def test_seedance_build_body_keeps_remote_url_verbatim(monkeypatch):
  function test_seedance_build_body_includes_audio_flag (line 233) | def test_seedance_build_body_includes_audio_flag():
  function test_seedance_send_request_passes_through_body (line 242) | def test_seedance_send_request_passes_through_body(monkeypatch):
  function test_seedance_poll_url_and_success_extraction (line 262) | def test_seedance_poll_url_and_success_extraction(monkeypatch):
  function test_seedance_poll_failure (line 283) | def test_seedance_poll_failure(monkeypatch):
  function test_seedance_resume_helper_round_trip (line 296) | def test_seedance_resume_helper_round_trip():
  function test_save_inline_blobs_auto_indexes_multi (line 308) | def test_save_inline_blobs_auto_indexes_multi(tmp_path):
  function test_save_inline_blobs_picks_extension_from_mime (line 318) | def test_save_inline_blobs_picks_extension_from_mime(tmp_path):

FILE: tests/comfy_cli/command/generate/test_app.py
  function disable_tracking_prompt (line 19) | def disable_tracking_prompt(monkeypatch):
  function runner (line 27) | def runner():
  function api_key (line 32) | def api_key(monkeypatch):
  function test_no_args_prints_top_help (line 40) | def test_no_args_prints_top_help(runner):
  function test_top_help_via_dash_help (line 47) | def test_top_help_via_dash_help(runner):
  function test_list_shows_aliases (line 56) | def test_list_shows_aliases(runner):
  function test_list_partner_filter (line 62) | def test_list_partner_filter(runner):
  function test_list_partner_eq_form (line 69) | def test_list_partner_eq_form(runner):
  function test_list_style_filter (line 75) | def test_list_style_filter(runner):
  function test_list_query_filter (line 81) | def test_list_query_filter(runner):
  function test_list_no_matches (line 87) | def test_list_no_matches(runner):
  function test_schema_alias (line 96) | def test_schema_alias(runner):
  function test_schema_full_path (line 103) | def test_schema_full_path(runner):
  function test_schema_missing_arg (line 109) | def test_schema_missing_arg(runner):
  function test_schema_unknown_model (line 115) | def test_schema_unknown_model(runner):
  function test_per_model_help (line 124) | def test_per_model_help(runner):
  function test_generate_missing_api_key (line 134) | def test_generate_missing_api_key(runner, monkeypatch):
  function test_generate_bad_int_suggests_schema (line 144) | def test_generate_bad_int_suggests_schema(runner, api_key):
  function test_generate_unknown_model (line 154) | def test_generate_unknown_model(runner, api_key):
  function test_generate_missing_required (line 160) | def test_generate_missing_required(runner, api_key):
  function test_generate_bad_timeout (line 166) | def test_generate_bad_timeout(runner, api_key, monkeypatch):
  function test_generate_async_sync_poll_to_ready (line 183) | def test_generate_async_sync_poll_to_ready(runner, api_key, monkeypatch):
  function test_generate_async_returns_job_id (line 202) | def test_generate_async_returns_job_id(runner, api_key, monkeypatch):
  function test_generate_async_failure_status (line 215) | def test_generate_async_failure_status(runner, api_key, monkeypatch):
  function test_generate_sync_prints_url (line 229) | def test_generate_sync_prints_url(runner, api_key, monkeypatch):
  function test_generate_sync_with_download (line 237) | def test_generate_sync_with_download(runner, api_key, tmp_path, monkeypa...
  function test_generate_json_flag (line 249) | def test_generate_json_flag(runner, api_key, monkeypatch):
  function test_generate_download_no_urls (line 259) | def test_generate_download_no_urls(runner, api_key, monkeypatch):
  function test_generate_binary_response_with_download (line 270) | def test_generate_binary_response_with_download(runner, api_key, tmp_pat...
  function test_generate_binary_response_no_download (line 279) | def test_generate_binary_response_no_download(runner, api_key, monkeypat...
  function test_generate_api_error_surface (line 290) | def test_generate_api_error_surface(runner, api_key, monkeypatch):
  function test_generate_network_error_surface (line 299) | def test_generate_network_error_surface(runner, api_key, monkeypatch):
  function test_generate_non_json_response (line 309) | def test_generate_non_json_response(runner, api_key, monkeypatch):
  function test_resume_missing_args (line 320) | def test_resume_missing_args(runner, api_key):
  function test_resume_sync_model_rejected (line 326) | def test_resume_sync_model_rejected(runner, api_key):
  function test_resume_unknown_model (line 332) | def test_resume_unknown_model(runner, api_key):
  function test_resume_async_succeeds (line 338) | def test_resume_async_succeeds(runner, api_key, monkeypatch):
  function test_resume_with_download (line 350) | def test_resume_with_download(runner, api_key, tmp_path, monkeypatch):
  function test_refresh_writes_cache (line 367) | def test_refresh_writes_cache(runner, monkeypatch, tmp_path):
  function test_refresh_network_failure (line 399) | def test_refresh_network_failure(runner, monkeypatch):
  function test_upload_missing_arg (line 422) | def test_upload_missing_arg(runner, api_key):
  function test_upload_local_file (line 428) | def test_upload_local_file(runner, api_key, tmp_path, monkeypatch):
  function test_upload_json_output (line 443) | def test_upload_json_output(runner, api_key, tmp_path, monkeypatch):
  function test_upload_does_not_mistake_meta_value_for_target (line 459) | def test_upload_does_not_mistake_meta_value_for_target(runner, monkeypat...
  function test_upload_propagates_api_error (line 478) | def test_upload_propagates_api_error(runner, api_key, tmp_path, monkeypa...
  function test_generate_auto_base64_for_kontext (line 494) | def test_generate_auto_base64_for_kontext(runner, api_key, tmp_path, mon...
  function test_generate_auto_upload_leaves_url_alone (line 515) | def test_generate_auto_upload_leaves_url_alone(runner, api_key, monkeypa...
  function test_generate_auto_upload_skipped_for_multipart (line 548) | def test_generate_auto_upload_skipped_for_multipart(runner, api_key, tmp...
  function test_video_kling_async_path (line 584) | def test_video_kling_async_path(runner, api_key, monkeypatch):
  function test_video_luma_async_path (line 605) | def test_video_luma_async_path(runner, api_key, monkeypatch):
  function test_video_runway_failure_surfaces (line 633) | def test_video_runway_failure_surfaces(runner, api_key, monkeypatch):
  function test_video_async_submission_shows_resume_alias (line 661) | def test_video_async_submission_shows_resume_alias(runner, api_key, monk...
  function test_video_resume_kling (line 670) | def test_video_resume_kling(runner, api_key, monkeypatch):
  function test_list_video_filter (line 687) | def test_list_video_filter(runner):
  function test_arg_value_long_and_eq (line 698) | def test_arg_value_long_and_eq():
  function test_arg_value_alternatives (line 704) | def test_arg_value_alternatives():
  function test_separate_meta_flags_typical (line 708) | def test_separate_meta_flags_typical():
  function test_separate_meta_flags_eq_form (line 716) | def test_separate_meta_flags_eq_form():
  function test_separate_meta_flags_missing_value_raises (line 721) | def test_separate_meta_flags_missing_value_raises():

FILE: tests/comfy_cli/command/generate/test_client.py
  function test_resolve_api_key_from_env (line 9) | def test_resolve_api_key_from_env(monkeypatch):
  function test_resolve_api_key_explicit_wins (line 14) | def test_resolve_api_key_explicit_wins(monkeypatch):
  function test_resolve_api_key_missing (line 19) | def test_resolve_api_key_missing(monkeypatch):
  function test_split_payload_json_pass_through (line 25) | def test_split_payload_json_pass_through():
  function test_split_payload_multipart_separates_files (line 37) | def test_split_payload_multipart_separates_files(tmp_path):
  function _capture_post (line 57) | def _capture_post(monkeypatch):
  function test_send_request_uses_x_api_key_for_comfyui_keys (line 70) | def test_send_request_uses_x_api_key_for_comfyui_keys(monkeypatch):
  function test_send_request_uses_bearer_for_firebase_tokens (line 80) | def test_send_request_uses_bearer_for_firebase_tokens(monkeypatch):
  function test_raise_for_status_includes_body (line 90) | def test_raise_for_status_includes_body():

FILE: tests/comfy_cli/command/generate/test_output.py
  function test_resolve_template_directory_shorthand (line 8) | def test_resolve_template_directory_shorthand(tmp_path):
  function test_resolve_template_placeholders (line 13) | def test_resolve_template_placeholders(tmp_path):
  function test_ext_from_response_known_mime (line 19) | def test_ext_from_response_known_mime():
  function test_ext_from_url_strips_query (line 24) | def test_ext_from_url_strips_query():

FILE: tests/comfy_cli/command/generate/test_poll.py
  function _resp (line 10) | def _resp(body):
  function test_poll_bfl_extracts_sample_url (line 14) | def test_poll_bfl_extracts_sample_url():
  function test_poll_bfl_reports_failure (line 46) | def test_poll_bfl_reports_failure():

FILE: tests/comfy_cli/command/generate/test_schema.py
  function test_flags_for_bfl_classifies_types (line 8) | def test_flags_for_bfl_classifies_types():
  function test_flags_for_multipart_finds_binary_fields (line 19) | def test_flags_for_multipart_finds_binary_fields():
  function test_parse_args_basic_coercion (line 28) | def test_parse_args_basic_coercion():
  function test_parse_args_eq_form_and_enum (line 43) | def test_parse_args_eq_form_and_enum():
  function test_parse_args_rejects_unknown_flag (line 53) | def test_parse_args_rejects_unknown_flag():
  function test_parse_args_rejects_bad_int (line 60) | def test_parse_args_rejects_bad_int():
  function test_parse_args_missing_required (line 67) | def test_parse_args_missing_required():
  function test_parse_args_enum_value_validated (line 74) | def test_parse_args_enum_value_validated():
  function test_parse_args_object_accepts_json (line 84) | def test_parse_args_object_accepts_json():

FILE: tests/comfy_cli/command/generate/test_spec.py
  function test_registry_loads_and_has_entries (line 7) | def test_registry_loads_and_has_entries():
  function test_get_endpoint_round_trip (line 12) | def test_get_endpoint_round_trip():
  function test_unknown_endpoint_suggests_close_match (line 21) | def test_unknown_endpoint_suggests_close_match():
  function test_request_schema_resolved_no_refs (line 31) | def test_request_schema_resolved_no_refs():
  function test_multipart_endpoints_detected (line 39) | def test_multipart_endpoints_detected():
  function test_json_endpoints_detected (line 44) | def test_json_endpoints_detected():
  function test_sync_endpoints_have_no_polling (line 49) | def test_sync_endpoints_have_no_polling():
  function test_filter_by_partner_and_category (line 54) | def test_filter_by_partner_and_category():
  function test_proxy_prefix_accepted (line 61) | def test_proxy_prefix_accepted():

FILE: tests/comfy_cli/command/generate/test_upload.py
  function test_request_signed_url_posts_hash (line 9) | def test_request_signed_url_posts_hash(monkeypatch):
  function test_upload_bytes_dedupe_skips_put (line 35) | def test_upload_bytes_dedupe_skips_put(monkeypatch):
  function test_upload_bytes_new_file_puts (line 57) | def test_upload_bytes_new_file_puts(monkeypatch):
  function test_upload_path_reads_file (line 83) | def test_upload_path_reads_file(monkeypatch, tmp_path):
  function test_upload_path_missing_file (line 100) | def test_upload_path_missing_file(tmp_path):
  function test_upload_remote_url_rehosts (line 105) | def test_upload_remote_url_rehosts(monkeypatch):
  function test_upload_target_dispatches_on_scheme (line 137) | def test_upload_target_dispatches_on_scheme(monkeypatch, tmp_path):
  function test_put_bytes_raises_on_error (line 147) | def test_put_bytes_raises_on_error(monkeypatch):

FILE: tests/comfy_cli/command/generate/test_video_poll.py
  function _resp (line 9) | def _resp(body):
  function no_sleep (line 14) | def no_sleep(monkeypatch):
  function _make_runner (line 18) | def _make_runner(get_responses):
  function test_kling_sibling_poll_path (line 24) | def test_kling_sibling_poll_path(no_sleep, monkeypatch):
  function test_luma_succeeds (line 44) | def test_luma_succeeds(no_sleep, monkeypatch):
  function test_runway_progress_normalized (line 59) | def test_runway_progress_normalized(no_sleep, monkeypatch):
  function test_runway_failure_states (line 75) | def test_runway_failure_states(no_sleep, monkeypatch):
  function test_minimax_redeems_file_id (line 85) | def test_minimax_redeems_file_id(no_sleep, monkeypatch):
  function test_pika_polls_videos_endpoint (line 102) | def test_pika_polls_videos_endpoint(no_sleep, monkeypatch):
  function test_vidu_polls_creations_path (line 115) | def test_vidu_polls_creations_path(no_sleep, monkeypatch):
  function test_xai_video_polls_request_id (line 127) | def test_xai_video_polls_request_id(no_sleep, monkeypatch):
  function test_moonvalley_polls_prompts (line 139) | def test_moonvalley_polls_prompts(no_sleep, monkeypatch):
  function test_missing_id_raises (line 151) | def test_missing_id_raises(monkeypatch):
  function test_kling_without_create_path_raises (line 157) | def test_kling_without_create_path_raises():
  function test_build_synthetic_initial_for_each_partner (line 162) | def test_build_synthetic_initial_for_each_partner():
  function test_build_synthetic_initial_for_bfl (line 169) | def test_build_synthetic_initial_for_bfl():
  function test_extract_urls_recognizes_video_extensions (line 175) | def test_extract_urls_recognizes_video_extensions():
  function test_extract_urls_recognizes_query_strings (line 181) | def test_extract_urls_recognizes_query_strings():
  function test_extract_job_id_from_nested_paths (line 187) | def test_extract_job_id_from_nested_paths():
  function test_existing_bfl_poller_still_works (line 194) | def test_existing_bfl_poller_still_works(no_sleep, monkeypatch):

FILE: tests/comfy_cli/command/github/test_pr.py
  function runner (line 28) | def runner():
  function sample_pr_info (line 35) | def sample_pr_info():
  class TestPRReferenceParsing (line 48) | class TestPRReferenceParsing:
    method test_parse_pr_number_format (line 49) | def test_parse_pr_number_format(self):
    method test_parse_user_branch_format (line 56) | def test_parse_user_branch_format(self):
    method test_parse_github_url_format (line 63) | def test_parse_github_url_format(self):
    method test_parse_invalid_format (line 71) | def test_parse_invalid_format(self):
    method test_parse_empty_string (line 76) | def test_parse_empty_string(self):
  class TestGitHubAPIIntegration (line 82) | class TestGitHubAPIIntegration:
    method test_fetch_pr_info_success (line 86) | def test_fetch_pr_info_success(self, mock_get, sample_pr_info):
    method test_fetch_pr_info_not_found (line 112) | def test_fetch_pr_info_not_found(self, mock_get):
    method test_fetch_pr_info_rate_limit (line 123) | def test_fetch_pr_info_rate_limit(self, mock_get):
    method test_find_pr_by_branch_success (line 134) | def test_find_pr_by_branch_success(self, mock_get):
    method test_find_pr_by_branch_not_found (line 161) | def test_find_pr_by_branch_not_found(self, mock_get):
    method test_find_pr_by_branch_error (line 172) | def test_find_pr_by_branch_error(self, mock_get):
  class TestGitOperations (line 180) | class TestGitOperations:
    method test_checkout_pr_fork_success (line 186) | def test_checkout_pr_fork_success(self, mock_getcwd, mock_chdir, mock_...
    method test_checkout_pr_non_fork_success (line 211) | def test_checkout_pr_non_fork_success(self, mock_getcwd, mock_chdir, m...
    method test_checkout_pr_git_failure (line 239) | def test_checkout_pr_git_failure(self, mock_getcwd, mock_chdir, mock_s...
  class TestGitCheckoutTag (line 251) | class TestGitCheckoutTag:
    method _init_repo (line 261) | def _init_repo(path):
    method test_succeeds_offline_when_tag_already_local (line 270) | def test_succeeds_offline_when_tag_already_local(self, tmp_path):
    method test_fetches_when_tag_missing_locally (line 297) | def test_fetches_when_tag_missing_locally(self, tmp_path):
  class TestHandlePRCheckout (line 311) | class TestHandlePRCheckout:
    method test_handle_pr_checkout_success (line 321) | def test_handle_pr_checkout_success(
  class TestCommandLineIntegration (line 349) | class TestCommandLineIntegration:
    method test_install_with_pr_parameter (line 353) | def test_install_with_pr_parameter(self, mock_execute, runner):
    method test_pr_and_version_conflict (line 363) | def test_pr_and_version_conflict(self, runner):
    method test_pr_and_commit_conflict (line 369) | def test_pr_and_commit_conflict(self, runner):
    method test_commit_without_pr_does_not_conflict (line 379) | def test_commit_without_pr_does_not_conflict(self, mock_track, mock_ws...
    method test_cpu_pr_conflict_with_version (line 393) | def test_cpu_pr_conflict_with_version(self, mock_track, mock_ws, mock_...
    method test_cpu_pr_conflict_with_commit (line 406) | def test_cpu_pr_conflict_with_commit(self, mock_track, mock_ws, mock_c...
    method test_cpu_pr_passes_pr_to_execute (line 421) | def test_cpu_pr_passes_pr_to_execute(self, mock_track, mock_ws, mock_c...
  class TestPRInfoDataClass (line 431) | class TestPRInfoDataClass:
    method test_pr_info_is_fork_true (line 434) | def test_pr_info_is_fork_true(self):
    method test_pr_info_is_fork_false (line 448) | def test_pr_info_is_fork_false(self):
  class TestEdgeCases (line 463) | class TestEdgeCases:
    method test_parse_pr_reference_whitespace (line 466) | def test_parse_pr_reference_whitespace(self):
    method test_fetch_pr_info_with_github_token (line 474) | def test_fetch_pr_info_with_github_token(self, mock_get):
    method test_checkout_pr_remote_already_exists (line 498) | def test_checkout_pr_remote_already_exists(self, mock_getcwd, mock_chd...
  class TestGetLatestRelease (line 514) | class TestGetLatestRelease:
    method test_sends_auth_header_when_token_set (line 518) | def test_sends_auth_header_when_token_set(self, mock_get):
    method test_no_auth_header_without_token (line 537) | def test_no_auth_header_without_token(self, mock_get):
    method test_rate_limit_raises_error (line 554) | def test_rate_limit_raises_error(self, mock_get):
    method test_non_semver_tag_returns_release_with_version_none (line 565) | def test_non_semver_tag_returns_release_with_version_none(self, mock_g...
  class TestHandleGithubRateLimit (line 583) | class TestHandleGithubRateLimit:
    method test_primary_rate_limit_message_format (line 584) | def test_primary_rate_limit_message_format(self):
    method test_retry_after_header (line 596) | def test_retry_after_header(self):
    method test_no_rate_limit_does_not_raise (line 603) | def test_no_rate_limit_does_not_raise(self):
  class TestResolveLatestTagFromLocal (line 610) | class TestResolveLatestTagFromLocal:
    method _init_repo (line 615) | def _init_repo(path):
    method _make_repo (line 625) | def _make_repo(cls, path, tags):
    method test_picks_highest_stable_semver (line 630) | def test_picks_highest_stable_semver(self, tmp_path):
    method test_skips_pre_release_tags (line 635) | def test_skips_pre_release_tags(self, tmp_path):
    method test_skips_non_semver_tags (line 641) | def test_skips_non_semver_tags(self, tmp_path):
    method test_returns_none_when_no_tags (line 646) | def test_returns_none_when_no_tags(self, tmp_path):
    method test_returns_none_when_only_prereleases (line 651) | def test_returns_none_when_only_prereleases(self, tmp_path):
    method test_returns_none_when_only_non_semver (line 656) | def test_returns_none_when_only_non_semver(self, tmp_path):
    method test_returns_none_for_non_git_directory (line 661) | def test_returns_none_for_non_git_directory(self, tmp_path):
    method test_tolerates_fetch_exception (line 666) | def test_tolerates_fetch_exception(self, tmp_path):
    method test_tolerates_fetch_nonzero_exit (line 683) | def test_tolerates_fetch_nonzero_exit(self, tmp_path):
    method test_tag_with_v_prefix_normalized (line 701) | def test_tag_with_v_prefix_normalized(self, tmp_path):
  class TestParseGithubOwnerRepo (line 708) | class TestParseGithubOwnerRepo:
    method test_parses_github_urls (line 732) | def test_parses_github_urls(self, url, expected):
    method test_returns_none_for_non_github_urls (line 747) | def test_returns_none_for_non_github_urls(self, url):
  class TestCheckoutStableComfyUI (line 759) | class TestCheckoutStableComfyUI:
    method test_latest_uses_local_tag_no_api_call (line 767) | def test_latest_uses_local_tag_no_api_call(self, mock_local, mock_api,...
    method test_latest_warns_on_stale_tag_when_fetch_failed (line 778) | def test_latest_warns_on_stale_tag_when_fetch_failed(self, mock_local,...
    method test_latest_no_warning_when_fetch_succeeded (line 797) | def test_latest_no_warning_when_fetch_succeeded(self, mock_local, mock...
    method test_latest_falls_back_to_api_when_local_empty (line 808) | def test_latest_falls_back_to_api_when_local_empty(self, mock_local, m...
    method test_latest_fallback_uses_fork_owner_repo_from_url (line 821) | def test_latest_fallback_uses_fork_owner_repo_from_url(self, mock_loca...
    method test_latest_fallback_strips_branch_suffix_from_url (line 837) | def test_latest_fallback_strips_branch_suffix_from_url(self, mock_loca...
    method test_latest_fallback_defaults_to_upstream_for_non_github_url (line 849) | def test_latest_fallback_defaults_to_upstream_for_non_github_url(self,...
    method test_latest_fallback_defaults_to_upstream_when_url_omitted (line 861) | def test_latest_fallback_defaults_to_upstream_when_url_omitted(self, m...
    method test_latest_warns_when_fetch_failed_before_api_fallback (line 873) | def test_latest_warns_when_fetch_failed_before_api_fallback(self, mock...
    method test_latest_exits_when_both_local_and_api_fail (line 888) | def test_latest_exits_when_both_local_and_api_fail(self, mock_local, m...
    method test_specific_version_skips_both_local_and_api (line 896) | def test_specific_version_skips_both_local_and_api(self, mock_local, m...
    method test_specific_version_with_v_prefix_passes_through (line 907) | def test_specific_version_with_v_prefix_passes_through(self, mock_loca...
    method test_latest_with_rate_limited_api_when_no_local_tags (line 916) | def test_latest_with_rate_limited_api_when_no_local_tags(self, mock_co...
    method test_latest_with_local_tags_no_network_at_all (line 938) | def test_latest_with_local_tags_no_network_at_all(self, mock_co, mock_...
  class TestInstallExecuteWithLatest (line 951) | class TestInstallExecuteWithLatest:
    method _make_comfy_repo (line 967) | def _make_comfy_repo(path):
    method test_full_execute_resolves_latest_locally_no_api_call (line 983) | def test_full_execute_resolves_latest_locally_no_api_call(self, tmp_pa...
    method test_full_execute_with_specific_version_no_api_no_resolver (line 1034) | def test_full_execute_with_specific_version_no_api_no_resolver(self, t...

FILE: tests/comfy_cli/command/models/test_models.py
  function _make_model_tree (line 10) | def _make_model_tree(tmp_path: pathlib.Path) -> pathlib.Path:
  function test_list_models_finds_files_in_subdirectories (line 23) | def test_list_models_finds_files_in_subdirectories(tmp_path):
  function test_list_models_finds_root_level_files (line 33) | def test_list_models_finds_root_level_files(tmp_path):
  function test_list_models_returns_empty_for_missing_directory (line 40) | def test_list_models_returns_empty_for_missing_directory(tmp_path):
  function test_list_models_ignores_directories (line 44) | def test_list_models_ignores_directories(tmp_path):
  function test_list_command_shows_type_column (line 56) | def test_list_command_shows_type_column(tmp_path):
  function test_remove_with_path_traversal_is_rejected (line 67) | def test_remove_with_path_traversal_is_rejected(tmp_path):
  function test_remove_deletes_model_in_subdirectory (line 84) | def test_remove_deletes_model_in_subdirectory(tmp_path):
  function test_remove_rejects_directory_name (line 98) | def test_remove_rejects_directory_name(tmp_path):
  function test_remove_deletes_root_level_model (line 110) | def test_remove_deletes_root_level_model(tmp_path):
  function test_remove_interactive_shows_relative_paths (line 124) | def test_remove_interactive_shows_relative_paths(tmp_path):
  function test_valid_model_url (line 142) | def test_valid_model_url():
  function test_valid_model_url_with_version (line 147) | def test_valid_model_url_with_version():
  function test_valid_model_url_with_version_and_additional_segments (line 152) | def test_valid_model_url_with_version_and_additional_segments():
  function test_valid_model_url_with_query (line 157) | def test_valid_model_url_with_query():
  function test_valid_api_url (line 162) | def test_valid_api_url():
  function test_invalid_url (line 167) | def test_invalid_url():
  function test_malformed_url (line 172) | def test_malformed_url():
  function test_invalid_model_id_url (line 177) | def test_invalid_model_id_url():
  function test_malformed_query_url (line 182) | def test_malformed_query_url():
  function test_model_url_with_model_version_id_query (line 187) | def test_model_url_with_model_version_id_query():
  function test_model_url_with_model_version_id_invalid (line 192) | def test_model_url_with_model_version_id_invalid():
  function test_valid_api_v1_model_versions_url (line 197) | def test_valid_api_v1_model_versions_url():
  function test_valid_api_v1_model_versions_camelcase_segment (line 202) | def test_valid_api_v1_model_versions_camelcase_segment():
  function test_valid_api_download_with_query_params (line 207) | def test_valid_api_download_with_query_params():
  function test_api_download_trailing_slash_is_ok (line 212) | def test_api_download_trailing_slash_is_ok():
  function test_api_download_non_numeric_id_models_version (line 217) | def test_api_download_non_numeric_id_models_version():
  function test_api_download_non_numeric_id (line 222) | def test_api_download_non_numeric_id():
  function test_model_url_with_slug_and_query (line 227) | def test_model_url_with_slug_and_query():
  function test_www_subdomain_is_accepted (line 232) | def test_www_subdomain_is_accepted():
  function test_completly_mailformed_civitai_url (line 237) | def test_completly_mailformed_civitai_url():
  function test_non_evil_civitai_url (line 242) | def test_non_evil_civitai_url():
  function test_valid_model_url_red_domain (line 247) | def test_valid_model_url_red_domain():
  function test_valid_model_url_red_with_query (line 252) | def test_valid_model_url_red_with_query():
  function test_valid_api_download_url_red_domain (line 257) | def test_valid_api_download_url_red_domain():
  function test_valid_api_v1_model_versions_url_red_domain (line 262) | def test_valid_api_v1_model_versions_url_red_domain():
  function test_www_subdomain_red_is_accepted (line 267) | def test_www_subdomain_red_is_accepted():
  function test_non_evil_civitai_red_url (line 272) | def test_non_evil_civitai_red_url():
  function test_red_as_spoofed_subdomain_of_other_tld (line 277) | def test_red_as_spoofed_subdomain_of_other_tld():
  function test_valid_huggingface_url (line 282) | def test_valid_huggingface_url():
  function test_valid_huggingface_url_sd_audio (line 287) | def test_valid_huggingface_url_sd_audio():
  function test_valid_huggingface_url_with_folder (line 292) | def test_valid_huggingface_url_with_folder():
  function test_valid_huggingface_url_with_subfolder (line 303) | def test_valid_huggingface_url_with_subfolder():
  function test_valid_huggingface_url_with_encoded_filename (line 314) | def test_valid_huggingface_url_with_encoded_filename():
  function test_invalid_huggingface_url (line 319) | def test_invalid_huggingface_url():
  function test_invalid_huggingface_url_structure (line 324) | def test_invalid_huggingface_url_structure():
  function test_huggingface_url_with_com_domain (line 329) | def test_huggingface_url_with_com_domain():
  function test_huggingface_url_with_folder_structure (line 334) | def test_huggingface_url_with_folder_structure():
  class TestFormatElapsed (line 345) | class TestFormatElapsed:
    method test_under_one_minute (line 346) | def test_under_one_minute(self):
    method test_fractional_seconds (line 349) | def test_fractional_seconds(self):
    method test_rounds_up_to_minute_boundary (line 352) | def test_rounds_up_to_minute_boundary(self):
    method test_exactly_sixty_seconds (line 355) | def test_exactly_sixty_seconds(self):
    method test_minutes_and_seconds (line 358) | def test_minutes_and_seconds(self):
    method test_over_one_hour (line 361) | def test_over_one_hour(self):
    method test_large_duration (line 364) | def test_large_duration(self):
  class TestDownloadCommandDownloaderOption (line 373) | class TestDownloadCommandDownloaderOption:
    method test_downloader_flag_forwarded (line 374) | def test_downloader_flag_forwarded(self, tmp_path):
    method test_default_from_config (line 406) | def test_default_from_config(self, tmp_path):
    method test_cli_flag_overrides_config (line 439) | def test_cli_flag_overrides_config(self, tmp_path):
  class TestDownloadCommandErrorHandling (line 475) | class TestDownloadCommandErrorHandling:
    method _run_with_download_error (line 478) | def _run_with_download_error(self, tmp_path, exc):
    method test_download_exception_exits_with_code_1 (line 503) | def test_download_exception_exits_with_code_1(self, tmp_path):
    method test_download_exception_does_not_show_traceback (line 511) | def test_download_exception_does_not_show_traceback(self, tmp_path):
    method test_download_exception_skips_done_message (line 522) | def test_download_exception_skips_done_message(self, tmp_path):
    method test_download_exception_with_markup_chars_does_not_crash (line 529) | def test_download_exception_with_markup_chars_does_not_crash(self, tmp...

FILE: tests/comfy_cli/command/nodes/test_bisect_custom_nodes.py
  function bisect_state (line 10) | def bisect_state():
  function test_good (line 19) | def test_good():
  function test_good_resolved (line 33) | def test_good_resolved(bisect_state: BisectState):
  function test_bad (line 41) | def test_bad(bisect_state):
  function test_bad_resolved (line 49) | def test_bad_resolved():
  function test_save (line 64) | def test_save(mock_execute_cm_cli, bisect_state, tmp_path):
  function test_reset (line 75) | def test_reset(mock_execute_cm_cli, bisect_state):
  function test_load_existing_state (line 84) | def test_load_existing_state(tmp_path):
  function test_load_nonexistent_state (line 102) | def test_load_nonexistent_state(tmp_path):
  function test_set_custom_node_enabled_states (line 112) | def test_set_custom_node_enabled_states(mock_execute_cm_cli, bisect_state):
  function test_set_custom_node_enabled_states_no_active_nodes (line 118) | def test_set_custom_node_enabled_states_no_active_nodes(mock_execute_cm_...

FILE: tests/comfy_cli/command/nodes/test_node_init.py
  function test_node_init_strips_credentials (line 11) | def test_node_init_strips_credentials(tmp_path, monkeypatch):
  function test_node_init_refuses_overwrite (line 32) | def test_node_init_refuses_overwrite(tmp_path, monkeypatch):

FILE: tests/comfy_cli/command/nodes/test_node_install.py
  function strip_ansi (line 13) | def strip_ansi(text):
  function test_install_no_deps_option_exists (line 18) | def test_install_no_deps_option_exists():
  function test_install_fast_deps_and_no_deps_mutually_exclusive (line 26) | def test_install_fast_deps_and_no_deps_mutually_exclusive():
  function test_install_no_deps_alone_works (line 32) | def test_install_no_deps_alone_works():
  function test_install_fast_deps_alone_works (line 42) | def test_install_fast_deps_alone_works():
  function test_install_neither_deps_option (line 52) | def test_install_neither_deps_option():
  function test_multiple_commands_work_independently (line 62) | def test_multiple_commands_work_independently():
  function test_install_uv_compile_passes_to_execute (line 72) | def test_install_uv_compile_passes_to_execute():
  function test_install_no_uv_compile_passes_false (line 83) | def test_install_no_uv_compile_passes_false():
  function test_install_uv_compile_and_fast_deps_mutually_exclusive (line 92) | def test_install_uv_compile_and_fast_deps_mutually_exclusive():
  function test_install_uv_compile_and_no_deps_mutually_exclusive (line 98) | def test_install_uv_compile_and_no_deps_mutually_exclusive():
  function test_uv_sync_calls_execute_cm_cli (line 104) | def test_uv_sync_calls_execute_cm_cli():
  function test_reinstall_uv_compile_passes_to_execute (line 113) | def test_reinstall_uv_compile_passes_to_execute():
  function test_reinstall_uv_compile_and_fast_deps_mutually_exclusive (line 122) | def test_reinstall_uv_compile_and_fast_deps_mutually_exclusive():
  function test_reinstall_no_uv_compile_passes_false (line 128) | def test_reinstall_no_uv_compile_passes_false():
  function test_install_exit_on_fail_reraises_and_propagates_code (line 137) | def test_install_exit_on_fail_reraises_and_propagates_code():
  function test_save_snapshot_no_output (line 148) | def test_save_snapshot_no_output():
  function test_save_snapshot_with_output (line 157) | def test_save_snapshot_with_output():
  function test_restore_snapshot_with_uv_compile (line 167) | def test_restore_snapshot_with_uv_compile():
  function test_restore_snapshot_with_pip_flags (line 176) | def test_restore_snapshot_with_pip_flags():
  function test_restore_dependencies_with_uv_compile (line 186) | def test_restore_dependencies_with_uv_compile():
  function test_update_with_uv_compile (line 195) | def test_update_with_uv_compile():
  function test_fix_with_uv_compile (line 207) | def test_fix_with_uv_compile():
  function test_uninstall_rejects_all (line 216) | def test_uninstall_rejects_all():
  function test_reinstall_rejects_all (line 223) | def test_reinstall_rejects_all():
  function test_validate_mode_rejects_invalid (line 230) | def test_validate_mode_rejects_invalid():
  function test_install_deps_with_deps_file (line 236) | def test_install_deps_with_deps_file():
  function test_install_deps_with_uv_compile (line 245) | def test_install_deps_with_uv_compile():
  function test_install_deps_no_args_shows_error (line 254) | def test_install_deps_no_args_shows_error():
  function test_restore_snapshot_with_pip_non_local_url (line 260) | def test_restore_snapshot_with_pip_non_local_url():
  function test_update_calls_update_node_id_cache (line 269) | def test_update_calls_update_node_id_cache():
  function test_uninstall_calls_execute (line 280) | def test_uninstall_calls_execute():
  function test_show_installed (line 289) | def test_show_installed():
  function test_install_deps_with_workflow (line 298) | def test_install_deps_with_workflow(tmp_path):
  function test_install_rejects_all (line 315) | def test_install_rejects_all():
  function test_simple_show_installed (line 322) | def test_simple_show_installed():
  function test_show_with_channel (line 331) | def test_show_with_channel():
  class TestRegistryInstallDownloadError (line 340) | class TestRegistryInstallDownloadError:
    method _invoke (line 344) | def _invoke(self, tmp_path, download_side_effect):
    method test_download_exception_caught_and_reported (line 360) | def test_download_exception_caught_and_reported(self, tmp_path):
    method test_no_extract_or_install_script_after_failure (line 373) | def test_no_extract_or_install_script_after_failure(self, tmp_path):
    method test_no_traceback_in_output (line 381) | def test_no_traceback_in_output(self, tmp_path):

FILE: tests/comfy_cli/command/nodes/test_pack.py
  function test_pack_creates_zip_with_correct_contents (line 24) | def test_pack_creates_zip_with_correct_contents(tmp_path, monkeypatch):

FILE: tests/comfy_cli/command/nodes/test_publish.py
  function create_mock_config (line 11) | def create_mock_config(includes_list=None):
  function test_publish_fails_on_security_violations (line 34) | def test_publish_fails_on_security_violations():
  function test_publish_continues_on_no_security_violations (line 52) | def test_publish_continues_on_no_security_violations():
  function test_publish_handles_missing_ruff (line 82) | def test_publish_handles_missing_ruff():
  function test_publish_with_token_option (line 90) | def test_publish_with_token_option():
  function test_publish_exits_on_upload_failure (line 118) | def test_publish_exits_on_upload_failure():
  function test_publish_fails_when_config_is_none (line 149) | def test_publish_fails_when_config_is_none():
  function test_publish_fails_when_version_is_empty (line 161) | def test_publish_fails_when_version_is_empty():
  function test_publish_with_includes_parameter (line 179) | def test_publish_with_includes_parameter():

FILE: tests/comfy_cli/command/test_bisect_parse.py
  class TestParseCmOutput (line 110) | class TestParseCmOutput:
    method test_real_output_filters_fetch_lines (line 111) | def test_real_output_filters_fetch_lines(self):
    method test_no_fetch_lines_in_result (line 116) | def test_no_fetch_lines_in_result(self):
    method test_pinned_nodes_excluded (line 121) | def test_pinned_nodes_excluded(self):
    method test_empty_output (line 128) | def test_empty_output(self):
    method test_only_fetch_lines (line 132) | def test_only_fetch_lines(self):
    method test_no_fetch_lines (line 136) | def test_no_fetch_lines(self):
    method test_arbitrary_status_lines_filtered (line 140) | def test_arbitrary_status_lines_filtered(self):

FILE: tests/comfy_cli/command/test_cm_cli_util.py
  function _make_mock_proc (line 11) | def _make_mock_proc(returncode, stdout_lines=None, stderr_lines=None):
  function _clear_find_cm_cli_cache (line 21) | def _clear_find_cm_cli_cache():
  function _cm_cli_env (line 28) | def _cm_cli_env(tmp_path):
  class TestFindCmCli (line 48) | class TestFindCmCli:
    method test_returns_true_when_module_exists_same_python (line 49) | def test_returns_true_when_module_exists_same_python(self):
    method test_returns_true_no_workspace_module_exists (line 61) | def test_returns_true_no_workspace_module_exists(self):
    method test_returns_false_when_module_missing (line 69) | def test_returns_false_when_module_missing(self):
    method test_returns_true_when_found_in_workspace_venv (line 76) | def test_returns_true_when_found_in_workspace_venv(self):
    method test_returns_false_when_missing_from_workspace_venv (line 88) | def test_returns_false_when_missing_from_workspace_venv(self):
    method test_workspace_python_checked_first_not_cli_interpreter (line 100) | def test_workspace_python_checked_first_not_cli_interpreter(self):
    method test_result_is_cached (line 124) | def test_result_is_cached(self):
    method test_cache_clear_allows_recheck (line 135) | def test_cache_clear_allows_recheck(self):
    method test_returns_false_on_subprocess_timeout (line 147) | def test_returns_false_on_subprocess_timeout(self):
  class TestResolveManagerGuiMode (line 163) | class TestResolveManagerGuiMode:
    method test_returns_config_mode_when_set (line 164) | def test_returns_config_mode_when_set(self):
    method test_legacy_false_returns_disable (line 169) | def test_legacy_false_returns_disable(self):
    method test_legacy_true_returns_enable_gui (line 174) | def test_legacy_true_returns_enable_gui(self):
    method test_legacy_boolean_0_returns_disable (line 179) | def test_legacy_boolean_0_returns_disable(self):
    method test_no_config_manager_available_returns_enable_gui (line 184) | def test_no_config_manager_available_returns_enable_gui(self):
    method test_no_config_no_manager_returns_not_installed_value (line 192) | def test_no_config_no_manager_returns_not_installed_value(self):
    method test_no_config_no_manager_returns_none_by_default (line 200) | def test_no_config_no_manager_returns_none_by_default(self):
  class TestExecuteCmCli (line 209) | class TestExecuteCmCli:
    method test_no_workspace_raises_exit (line 210) | def test_no_workspace_raises_exit(self):
    method test_no_cm_cli_raises_exit (line 217) | def test_no_cm_cli_raises_exit(self):
    method test_happy_path_returns_stdout (line 225) | def test_happy_path_returns_stdout(self, _cm_cli_env):
    method test_cmd_uses_python_m_cm_cli (line 229) | def test_cmd_uses_python_m_cm_cli(self, _cm_cli_env):
    method test_channel_appended (line 234) | def test_channel_appended(self, _cm_cli_env):
    method test_uv_compile_flag (line 240) | def test_uv_compile_flag(self, _cm_cli_env):
    method test_fast_deps_adds_no_deps (line 245) | def test_fast_deps_adds_no_deps(self, _cm_cli_env):
    method test_no_deps_adds_no_deps (line 250) | def test_no_deps_adds_no_deps(self, _cm_cli_env):
    method test_uv_compile_takes_precedence_over_fast_deps (line 255) | def test_uv_compile_takes_precedence_over_fast_deps(self, _cm_cli_env):
    method test_mode_appended (line 261) | def test_mode_appended(self, _cm_cli_env):
    method test_error_returncode_1_returns_none (line 267) | def test_error_returncode_1_returns_none(self, tmp_path):
    method test_error_returncode_2_returns_none (line 282) | def test_error_returncode_2_returns_none(self, tmp_path):
    method test_error_other_returncode_raises (line 297) | def test_error_other_returncode_raises(self, tmp_path):
    method test_raise_on_error_reraises (line 312) | def test_raise_on_error_reraises(self, tmp_path):
    method test_fast_deps_triggers_dependency_compiler (line 327) | def test_fast_deps_triggers_dependency_compiler(self, tmp_path):
    method test_fast_deps_non_dependency_cmd_skips_compiler (line 350) | def test_fast_deps_non_dependency_cmd_skips_compiler(self, tmp_path):
    method test_sets_comfyui_path_env (line 369) | def test_sets_comfyui_path_env(self, _cm_cli_env):
    method test_captures_stderr_via_pipe (line 374) | def test_captures_stderr_via_pipe(self, _cm_cli_env):

FILE: tests/comfy_cli/command/test_code_search.py
  function search_response (line 40) | def search_response():
  function raw_api_response (line 80) | def raw_api_response(search_response):
  function empty_search (line 86) | def empty_search():
  function empty_api_response (line 101) | def empty_api_response(empty_search):
  function limit_hit_search (line 106) | def limit_hit_search(search_response):
  function limit_hit_response (line 112) | def limit_hit_response(limit_hit_search):
  class TestBuildQuery (line 121) | class TestBuildQuery:
    method test_simple_query (line 122) | def test_simple_query(self):
    method test_with_repo_short_name (line 125) | def test_with_repo_short_name(self):
    method test_with_repo_full_name (line 129) | def test_with_repo_full_name(self):
    method test_with_custom_count (line 133) | def test_with_custom_count(self):
    method test_with_repo_and_count (line 137) | def test_with_repo_and_count(self):
    method test_user_type_filter_preserved (line 141) | def test_user_type_filter_preserved(self):
    method test_user_type_file_not_duplicated (line 147) | def test_user_type_file_not_duplicated(self):
  class TestFormatResults (line 157) | class TestFormatResults:
    method test_formats_valid_results (line 158) | def test_formats_valid_results(self, search_response):
    method test_empty_results (line 175) | def test_empty_results(self, empty_search):
    method test_skips_results_without_repo (line 178) | def test_skips_results_without_repo(self):
    method test_skips_results_without_file (line 182) | def test_skips_results_without_file(self):
    method test_handles_missing_branch_info (line 192) | def test_handles_missing_branch_info(self):
    method test_handles_completely_empty_response (line 211) | def test_handles_completely_empty_response(self):
    method test_handles_no_line_matches (line 214) | def test_handles_no_line_matches(self):
  class TestGetStats (line 237) | class TestGetStats:
    method test_extracts_stats (line 238) | def test_extracts_stats(self, search_response):
    method test_empty_response (line 244) | def test_empty_response(self):
    method test_limit_hit (line 250) | def test_limit_hit(self, limit_hit_search):
  class TestFetchResults (line 260) | class TestFetchResults:
    method test_successful_fetch (line 262) | def test_successful_fetch(self, mock_get, raw_api_response):
    method test_http_error_propagates (line 274) | def test_http_error_propagates(self, mock_get):
    method test_timeout_propagates (line 283) | def test_timeout_propagates(self, mock_get):
    method test_connection_error_propagates (line 290) | def test_connection_error_propagates(self, mock_get):
  class TestPrintResults (line 302) | class TestPrintResults:
    method test_json_output (line 303) | def test_json_output(self, capsys, search_response):
    method test_empty_results_message (line 314) | def test_empty_results_message(self, capsys):
    method test_formatted_output_contains_file_info (line 319) | def test_formatted_output_contains_file_info(self, capsys, search_resp...
    method test_limit_hit_message (line 329) | def test_limit_hit_message(self, capsys, limit_hit_search):
    method test_non_tty_prints_file_url_once_and_no_per_line_urls (line 337) | def test_non_tty_prints_file_url_once_and_no_per_line_urls(self, capsy...
    method test_tty_emits_osc8_and_hides_urls (line 355) | def test_tty_emits_osc8_and_hides_urls(self, search_response):
    method test_non_tty_ignores_force_color_env (line 387) | def test_non_tty_ignores_force_color_env(self, capsys, search_response...
  class TestCodeSearchCLI (line 405) | class TestCodeSearchCLI:
    method test_basic_search (line 407) | def test_basic_search(self, mock_fetch, raw_api_response):
    method test_search_with_repo (line 417) | def test_search_with_repo(self, mock_fetch, raw_api_response):
    method test_search_with_count (line 426) | def test_search_with_count(self, mock_fetch, raw_api_response):
    method test_search_json_output (line 435) | def test_search_json_output(self, mock_fetch, raw_api_response):
    method test_search_no_results (line 445) | def test_search_no_results(self, mock_fetch, empty_api_response):
    method test_connection_error (line 454) | def test_connection_error(self, mock_fetch):
    method test_timeout_error (line 463) | def test_timeout_error(self, mock_fetch):
    method test_http_error (line 472) | def test_http_error(self, mock_fetch):
    method test_http_error_no_response (line 483) | def test_http_error_no_response(self, mock_fetch):
    method test_short_options (line 492) | def test_short_options(self, mock_fetch, raw_api_response):
  class TestRootCLIWiring (line 508) | class TestRootCLIWiring:
    method test_code_search_registered (line 514) | def test_code_search_registered(self, mock_fetch, mock_ws, mock_track,...
    method test_cs_alias_registered (line 526) | def test_cs_alias_registered(self, mock_fetch, mock_ws, mock_track, ra...

FILE: tests/comfy_cli/command/test_command.py
  function runner (line 11) | def runner():
  function mock_execute (line 18) | def mock_execute():
  function mock_prompt_select_enum (line 24) | def mock_prompt_select_enum():
  function mock_tracking_consent (line 36) | def mock_tracking_consent():
  function test_install_here (line 48) | def test_install_here(cmd, runner, mock_execute, mock_prompt_select_enum):
  function test_version (line 58) | def test_version(runner):
  function mock_run_execute (line 65) | def mock_run_execute():
  function _write_workflow (line 70) | def _write_workflow(tmp_path):
  class TestRunApiKeyResolution (line 76) | class TestRunApiKeyResolution:
    method test_envvar_is_picked_up (line 79) | def test_envvar_is_picked_up(self, runner, mock_run_execute, tmp_path):
    method test_flag_overrides_envvar (line 85) | def test_flag_overrides_envvar(self, runner, mock_run_execute, tmp_path):
    method test_absent_resolves_to_none (line 95) | def test_absent_resolves_to_none(self, runner, mock_run_execute, tmp_p...
    method test_envvar_trailing_whitespace_is_stripped (line 102) | def test_envvar_trailing_whitespace_is_stripped(self, runner, mock_run...
    method test_whitespace_only_collapses_to_none (line 108) | def test_whitespace_only_collapses_to_none(self, runner, mock_run_exec...

FILE: tests/comfy_cli/command/test_frontend_pr.py
  function runner (line 14) | def runner():
  function sample_frontend_pr_info (line 19) | def sample_frontend_pr_info():
  class TestFrontendPRReferenceParsing (line 32) | class TestFrontendPRReferenceParsing:
    method test_parse_frontend_pr_number_format (line 35) | def test_parse_frontend_pr_number_format(self):
    method test_parse_frontend_user_branch_format (line 42) | def test_parse_frontend_user_branch_format(self):
    method test_parse_frontend_github_url_format (line 49) | def test_parse_frontend_github_url_format(self):
    method test_parse_frontend_custom_repo_url (line 57) | def test_parse_frontend_custom_repo_url(self):
    method test_parse_frontend_invalid_format (line 65) | def test_parse_frontend_invalid_format(self):
    method test_parse_frontend_empty_string (line 70) | def test_parse_frontend_empty_string(self):
  class TestNodeToolsVerification (line 76) | class TestNodeToolsVerification:
    method test_verify_node_tools_success (line 80) | def test_verify_node_tools_success(self, mock_run):
    method test_verify_node_tools_missing_node (line 101) | def test_verify_node_tools_missing_node(self, mock_run):
    method test_verify_node_tools_missing_npm (line 112) | def test_verify_node_tools_missing_npm(self, mock_run):
    method test_verify_node_tools_auto_install_pnpm (line 128) | def test_verify_node_tools_auto_install_pnpm(self, mock_run, mock_conf...
    method test_verify_node_tools_user_declines_pnpm_install (line 161) | def test_verify_node_tools_user_declines_pnpm_install(self, mock_run, ...
    method test_verify_node_tools_file_not_found (line 184) | def test_verify_node_tools_file_not_found(self, mock_run):

FILE: tests/comfy_cli/command/test_launch_frontend_pr.py
  function runner (line 17) | def runner():
  function mock_tracking_consent (line 22) | def mock_tracking_consent():
  function sample_frontend_pr_info (line 28) | def sample_frontend_pr_info():
  function mock_pr_cache (line 42) | def mock_pr_cache():
  class TestLaunchWithFrontendPR (line 49) | class TestLaunchWithFrontendPR:
    method test_launch_frontend_pr_without_node (line 53) | def test_launch_frontend_pr_without_node(self, mock_verify):
    method test_launch_frontend_pr_with_cache_hit (line 64) | def test_launch_frontend_pr_with_cache_hit(
    method test_launch_frontend_pr_cache_miss_builds (line 91) | def test_launch_frontend_pr_cache_miss_builds(
  class TestPRCacheManagement (line 133) | class TestPRCacheManagement:
    method test_pr_cache_get_frontend_path (line 136) | def test_pr_cache_get_frontend_path(self, sample_frontend_pr_info):
    method test_pr_cache_list_empty (line 145) | def test_pr_cache_list_empty(self):
    method test_pr_cache_clean_specific (line 152) | def test_pr_cache_clean_specific(self, tmp_path):
    method test_pr_cache_age_check (line 168) | def test_pr_cache_age_check(self, sample_frontend_pr_info, tmp_path):
    method test_pr_cache_enforce_limits (line 192) | def test_pr_cache_enforce_limits(self, tmp_path):
    method test_get_cache_age (line 220) | def test_get_cache_age(self):
  class TestPRCacheCommands (line 238) | class TestPRCacheCommands:
    method test_pr_cache_list_command (line 241) | def test_pr_cache_list_command(self, runner):
    method test_pr_cache_clean_command_with_confirmation (line 252) | def test_pr_cache_clean_command_with_confirmation(self, runner):
    method test_pr_cache_clean_command_with_yes_flag (line 267) | def test_pr_cache_clean_command_with_yes_flag(self, runner):

FILE: tests/comfy_cli/command/test_manager_gui.py
  function mock_config_manager (line 11) | def mock_config_manager():
  function mock_launch_config_manager (line 19) | def mock_launch_config_manager():
  class TestManagerCommands (line 26) | class TestManagerCommands:
    method test_disable_manager_sets_config (line 27) | def test_disable_manager_sets_config(self, mock_config_manager):
    method test_enable_gui_sets_config (line 34) | def test_enable_gui_sets_config(self, mock_config_manager):
    method test_disable_gui_sets_config (line 41) | def test_disable_gui_sets_config(self, mock_config_manager):
    method test_enable_legacy_gui_sets_config (line 48) | def test_enable_legacy_gui_sets_config(self, mock_config_manager):
  class TestGetManagerFlags (line 56) | class TestGetManagerFlags:
    method test_disable_mode_returns_empty (line 58) | def test_disable_mode_returns_empty(self, mock_resolve):
    method test_enable_gui_mode_returns_enable_manager (line 64) | def test_enable_gui_mode_returns_enable_manager(self, mock_resolve, mo...
    method test_disable_gui_mode_returns_both_flags (line 70) | def test_disable_gui_mode_returns_both_flags(self, mock_resolve, mock_...
    method test_enable_legacy_gui_mode_returns_legacy_flags (line 76) | def test_enable_legacy_gui_mode_returns_legacy_flags(self, mock_resolv...
    method test_unknown_mode_returns_default_with_warning (line 82) | def test_unknown_mode_returns_default_with_warning(self, mock_resolve,...
    method test_enable_mode_without_cmcli_returns_empty (line 90) | def test_enable_mode_without_cmcli_returns_empty(self, mock_resolve, m...
    method test_not_installed_returns_empty (line 97) | def test_not_installed_returns_empty(self, mock_resolve):
  class TestResolveManagerGuiMode (line 103) | class TestResolveManagerGuiMode:
    method test_returns_configured_mode (line 107) | def test_returns_configured_mode(self, mock_cm_cls):
    method test_old_config_false_migrates_to_disable (line 118) | def test_old_config_false_migrates_to_disable(self, mock_cm_cls, mock_...
    method test_old_config_true_migrates_to_enable_gui (line 132) | def test_old_config_true_migrates_to_enable_gui(self, mock_cm_cls, moc...
    method test_no_config_with_cmcli_defaults_to_enable_gui (line 146) | def test_no_config_with_cmcli_defaults_to_enable_gui(self, mock_cm_cls...
    method test_no_config_no_cmcli_returns_not_installed_value (line 157) | def test_no_config_no_cmcli_returns_not_installed_value(self, mock_cm_...
    method test_old_config_boolean_false_migrates_to_disable (line 168) | def test_old_config_boolean_false_migrates_to_disable(self, mock_cm_cls):
    method test_old_config_boolean_true_migrates_to_enable_gui (line 183) | def test_old_config_boolean_true_migrates_to_enable_gui(self, mock_cm_...
  class TestLaunchManagerFlagInjection (line 197) | class TestLaunchManagerFlagInjection:
    method test_launch_injects_enable_manager (line 203) | def test_launch_injects_enable_manager(
    method test_launch_no_inject_when_disabled (line 224) | def test_launch_no_inject_when_disabled(
    method test_launch_injects_when_extra_is_none (line 244) | def test_launch_injects_when_extra_is_none(
    method test_launch_injects_disable_gui_flags (line 264) | def test_launch_injects_disable_gui_flags(
    method test_launch_injects_legacy_gui_flags (line 287) | def test_launch_injects_legacy_gui_flags(
  class TestMigrateLegacy (line 304) | class TestMigrateLegacy:
    method test_migrate_legacy_no_workspace_exits (line 306) | def test_migrate_legacy_no_workspace_exits(self, mock_ws, mock_config_...
    method test_migrate_legacy_with_cli_only_mode (line 319) | def test_migrate_legacy_with_cli_only_mode(self, mock_ws, mock_config_...
    method test_migrate_legacy_without_cli_only_mode (line 340) | def test_migrate_legacy_without_cli_only_mode(self, mock_ws, mock_subp...
    method test_migrate_legacy_no_legacy_manager (line 362) | def test_migrate_legacy_no_legacy_manager(self, mock_ws, mock_config_m...
    method test_migrate_legacy_target_exists (line 377) | def test_migrate_legacy_target_exists(self, mock_ws, mock_config_manag...
    method test_migrate_legacy_lowercase_directory (line 394) | def test_migrate_legacy_lowercase_directory(self, mock_ws, mock_subpro...
    method test_migrate_legacy_installs_manager_requirements (line 418) | def test_migrate_legacy_installs_manager_requirements(
    method test_migrate_legacy_no_requirements_file (line 449) | def test_migrate_legacy_no_requirements_file(self, mock_ws, mock_subpr...
    method test_migrate_legacy_not_git_repo (line 468) | def test_migrate_legacy_not_git_repo(self, mock_ws, mock_config_manage...
    method test_migrate_legacy_skips_symlink (line 487) | def test_migrate_legacy_skips_symlink(self, mock_ws, mock_config_manag...
    method test_migrate_legacy_move_error (line 510) | def test_migrate_legacy_move_error(self, mock_ws, mock_move, mock_conf...
    method test_migrate_legacy_user_cancels (line 531) | def test_migrate_legacy_user_cancels(self, mock_ws, mock_confirm, mock...
  class TestInstallSkipManager (line 551) | class TestInstallSkipManager:
    method test_skip_manager_sets_disable_config (line 567) | def test_skip_manager_sets_disable_config(
  class TestInstallManagerFailure (line 607) | class TestInstallManagerFailure:
    method test_manager_install_failure_sets_disable_config (line 623) | def test_manager_install_failure_sets_disable_config(
    method test_manager_install_success_does_not_set_disable (line 676) | def test_manager_install_success_does_not_set_disable(
    method test_fast_deps_manager_failure_sets_disable_config (line 730) | def test_fast_deps_manager_failure_sets_disable_config(
  class TestPipInstallManagerCacheClear (line 775) | class TestPipInstallManagerCacheClear:
    method test_pip_install_manager_clears_cache_on_success (line 781) | def test_pip_install_manager_clears_cache_on_success(self, mock_exists...
    method test_pip_install_manager_no_cache_clear_on_failure (line 798) | def test_pip_install_manager_no_cache_clear_on_failure(self, mock_exis...
  class TestFillPrintTable (line 812) | class TestFillPrintTable:
    method mock_workspace_config_manager (line 816) | def mock_workspace_config_manager(self):
    method test_fill_print_table_disable_mode (line 823) | def test_fill_print_table_disable_mode(self, mock_resolve, mock_worksp...
    method test_fill_print_table_enable_gui_mode (line 839) | def test_fill_print_table_enable_gui_mode(self, mock_resolve, mock_wor...
    method test_fill_print_table_disable_gui_mode (line 852) | def test_fill_print_table_disable_gui_mode(self, mock_resolve, mock_wo...
    method test_fill_print_table_enable_legacy_gui_mode (line 865) | def test_fill_print_table_enable_legacy_gui_mode(self, mock_resolve, m...
    method test_fill_print_table_not_installed (line 878) | def test_fill_print_table_not_installed(self, mock_resolve, mock_works...
    method test_fill_print_table_unknown_mode_defaults_to_enable (line 891) | def test_fill_print_table_unknown_mode_defaults_to_enable(self, mock_r...
    method test_fill_print_table_uv_compile_enabled (line 904) | def test_fill_print_table_uv_compile_enabled(self, mock_resolve, mock_...
    method test_fill_print_table_uv_compile_disabled (line 921) | def test_fill_print_table_uv_compile_disabled(self, mock_resolve, mock...
    method test_fill_print_table_uv_compile_lowercase_true (line 936) | def test_fill_print_table_uv_compile_lowercase_true(self, mock_resolve...
    method test_fill_print_table_uv_compile_explicit_false (line 951) | def test_fill_print_table_uv_compile_explicit_false(self, mock_resolve...
  class TestResolveUvCompile (line 966) | class TestResolveUvCompile:
    method mock_resolve_config_manager (line 970) | def mock_resolve_config_manager(self):
    method test_explicit_true_returns_true (line 976) | def test_explicit_true_returns_true(self, mock_resolve_config_manager):
    method test_explicit_false_returns_false (line 982) | def test_explicit_false_returns_false(self, mock_resolve_config_manager):
    method test_explicit_true_ignores_config (line 988) | def test_explicit_true_ignores_config(self, mock_resolve_config_manager):
    method test_none_with_config_true (line 996) | def test_none_with_config_true(self, mock_resolve_config_manager):
    method test_none_with_config_false (line 1003) | def test_none_with_config_false(self, mock_resolve_config_manager):
    method test_none_with_no_config (line 1010) | def test_none_with_no_config(self, mock_resolve_config_manager):
    method test_config_true_overridden_by_fast_deps (line 1017) | def test_config_true_overridden_by_fast_deps(self, mock_resolve_config...
    method test_config_true_overridden_by_no_deps (line 1024) | def test_config_true_overridden_by_no_deps(self, mock_resolve_config_m...
    method test_config_false_with_fast_deps_stays_false (line 1031) | def test_config_false_with_fast_deps_stays_false(self, mock_resolve_co...
    method test_explicit_true_not_affected_by_fast_deps (line 1038) | def test_explicit_true_not_affected_by_fast_deps(self, mock_resolve_co...
  class TestUvCompileDefaultCommand (line 1045) | class TestUvCompileDefaultCommand:
    method test_uv_compile_default_enable (line 1048) | def test_uv_compile_default_enable(self, mock_config_manager):
    method test_uv_compile_default_disable (line 1055) | def test_uv_compile_default_disable(self, mock_config_manager):
  class TestFindCmCli (line 1063) | class TestFindCmCli:
    method test_find_cm_cli_module_found (line 1066) | def test_find_cm_cli_module_found(self):
    method test_find_cm_cli_module_not_found (line 1080) | def test_find_cm_cli_module_not_found(self):
    method test_find_cm_cli_cache_behavior (line 1098) | def test_find_cm_cli_cache_behavior(self):
  class TestPipInstallManagerEdgeCases (line 1119) | class TestPipInstallManagerEdgeCases:
    method test_pip_install_manager_requirements_not_found (line 1124) | def test_pip_install_manager_requirements_not_found(self, mock_exists,...
  class TestValidateComfyuiManager (line 1135) | class TestValidateComfyuiManager:
    method test_validate_comfyui_manager_exits_when_not_found (line 1139) | def test_validate_comfyui_manager_exits_when_not_found(self, mock_find...
    method test_validate_comfyui_manager_passes_when_found (line 1150) | def test_validate_comfyui_manager_passes_when_found(self, mock_find_cm...

FILE: tests/comfy_cli/command/test_npm_help.py
  class TestPrintNpmNotFoundHelp (line 11) | class TestPrintNpmNotFoundHelp:
    method capture_output (line 15) | def capture_output(self):
    method test_npm_not_found_help_shows_common_message (line 24) | def test_npm_not_found_help_shows_common_message(self, capture_output):
    method test_npm_not_found_help_windows (line 35) | def test_npm_not_found_help_windows(self, capture_output):
    method test_npm_not_found_help_macos (line 45) | def test_npm_not_found_help_macos(self, capture_output):
    method test_npm_not_found_help_linux (line 57) | def test_npm_not_found_help_linux(self, capture_output):
    method test_npm_not_found_help_unknown_os_falls_back_to_linux (line 69) | def test_npm_not_found_help_unknown_os_falls_back_to_linux(self, captu...

FILE: tests/comfy_cli/command/test_run.py
  function workflow (line 21) | def workflow():
  function workflow_file (line 37) | def workflow_file(workflow):
  function mock_execution (line 46) | def mock_execution(workflow):
  function _make_msg (line 60) | def _make_msg(msg_type, prompt_id, **data_fields):
  class TestIsUiWorkflow (line 64) | class TestIsUiWorkflow:
    method test_detects_ui_workflow (line 65) | def test_detects_ui_workflow(self):
    method test_rejects_api_workflow (line 68) | def test_rejects_api_workflow(self):
    method test_rejects_non_dict (line 71) | def test_rejects_non_dict(self):
    method test_requires_both_keys (line 75) | def test_requires_both_keys(self):
    method test_rejects_api_workflow_with_nodes_and_links_as_keys (line 79) | def test_rejects_api_workflow_with_nodes_and_links_as_keys(self):
    method test_rejects_when_values_are_not_lists (line 88) | def test_rejects_when_values_are_not_lists(self):
  function _make_http_error (line 93) | def _make_http_error(code: int, body: bytes = b"") -> urllib.error.HTTPE...
  function _ok_response (line 103) | def _ok_response(body: bytes) -> MagicMock:
  class TestFetchObjectInfo (line 111) | class TestFetchObjectInfo:
    method test_returns_parsed_json_on_success (line 112) | def test_returns_parsed_json_on_success(self):
    method test_http_error_exits_cleanly (line 122) | def test_http_error_exits_cleanly(self):
    method test_network_error_exits_cleanly (line 131) | def test_network_error_exits_cleanly(self):
    method test_timeout_exits_cleanly (line 140) | def test_timeout_exits_cleanly(self):
    method test_invalid_json_exits_cleanly (line 146) | def test_invalid_json_exits_cleanly(self):
  class TestWorkflowExecutionAuth (line 156) | class TestWorkflowExecutionAuth:
    method _make_exec (line 159) | def _make_exec(self, workflow, api_key=None):
    method test_queue_embeds_api_key_in_extra_data (line 173) | def test_queue_embeds_api_key_in_extra_data(self, workflow):
    method test_queue_does_not_send_x_api_key_header (line 182) | def test_queue_does_not_send_x_api_key_header(self, workflow):
    method test_queue_omits_extra_data_when_no_api_key (line 190) | def test_queue_omits_extra_data_when_no_api_key(self, workflow):
  class TestWatchExecution (line 201) | class TestWatchExecution:
    method test_successful_execution (line 202) | def test_successful_execution(self, mock_execution):
    method test_skips_other_prompt_messages (line 220) | def test_skips_other_prompt_messages(self, mock_execution):
    method test_unknown_node_ids_do_not_crash (line 235) | def test_unknown_node_ids_do_not_crash(self, mock_execution):
    method test_unknown_node_ids_verbose (line 255) | def test_unknown_node_ids_verbose(self, workflow):
    method test_collects_image_outputs (line 281) | def test_collects_image_outputs(self, mock_execution):
  class TestExecuteErrorHandling (line 311) | class TestExecuteErrorHandling:
    method _run_execute_expect_exit (line 312) | def _run_execute_expect_exit(self, workflow_file, **overrides):
    method test_timeout_exits_with_code_1 (line 319) | def test_timeout_exits_with_code_1(self, workflow_file):
    method test_connection_error_exits_with_code_1 (line 332) | def test_connection_error_exits_with_code_1(self, workflow_file):
    method test_websocket_exception_exits_with_code_1 (line 345) | def test_websocket_exception_exits_with_code_1(self, workflow_file):
    method test_successful_execution (line 358) | def test_successful_execution(self, workflow_file):
    method test_file_not_found_exits (line 375) | def test_file_not_found_exits(self):
    method test_rejects_invalid_workflow_format (line 380) | def test_rejects_invalid_workflow_format(self):
    method test_rejects_malformed_json (line 394) | def test_rejects_malformed_json(self):
    method test_rejects_unreadable_file (line 407) | def test_rejects_unreadable_file(self):
    method test_progress_stopped_on_error (line 429) | def test_progress_stopped_on_error(self, workflow_file):
  class TestExecuteUiWorkflow (line 446) | class TestExecuteUiWorkflow:
    method ui_workflow_file (line 489) | def ui_workflow_file(self):
    method test_ui_workflow_is_converted_then_executed (line 497) | def test_ui_workflow_is_converted_then_executed(self, ui_workflow_file):
    method test_ui_workflow_exits_when_server_not_running (line 517) | def test_ui_workflow_exits_when_server_not_running(self, ui_workflow_f...
    method test_ui_workflow_exits_cleanly_on_unexpected_converter_crash (line 527) | def test_ui_workflow_exits_cleanly_on_unexpected_converter_crash(self,...
    method test_ui_workflow_plumbs_api_key_through_to_execution (line 545) | def test_ui_workflow_plumbs_api_key_through_to_execution(self, ui_work...
    method test_ui_workflow_exits_when_conversion_yields_nothing (line 561) | def test_ui_workflow_exits_when_conversion_yields_nothing(self):

FILE: tests/comfy_cli/conftest.py
  function _preserve_cwd (line 7) | def _preserve_cwd():

FILE: tests/comfy_cli/registry/test_api.py
  class TestRegistryAPI (line 9) | class TestRegistryAPI(unittest.TestCase):
    method setUp (line 10) | def setUp(self):
    method test_determine_base_url_dev (line 31) | def test_determine_base_url_dev(self, mock_getenv):
    method test_determine_base_url_prod (line 36) | def test_determine_base_url_prod(self, mock_getenv):
    method test_publish_node_version_success (line 41) | def test_publish_node_version_success(self, mock_post):
    method test_publish_node_version_failure (line 63) | def test_publish_node_version_failure(self, mock_post):
    method test_list_all_nodes_success (line 74) | def test_list_all_nodes_success(self, mock_get):
    method test_list_all_nodes_failure (line 107) | def test_list_all_nodes_failure(self, mock_get):
    method test_install_node_success (line 118) | def test_install_node_success(self, mock_get):
    method test_install_node_failure (line 136) | def test_install_node_failure(self, mock_get):

FILE: tests/comfy_cli/registry/test_config_parser.py
  function mock_toml_data (line 24) | def mock_toml_data():
  function test_extract_node_configuration_success (line 61) | def test_extract_node_configuration_success(mock_toml_data):
  function test_extract_node_configuration_license_spdx_string (line 94) | def test_extract_node_configuration_license_spdx_string(license_str):
  function test_extract_node_configuration_license_text_dict (line 111) | def test_extract_node_configuration_license_text_dict():
  function test_extract_node_configuration_with_os_classifiers (line 131) | def test_extract_node_configuration_with_os_classifiers():
  function test_extract_node_configuration_with_accelerator_classifiers (line 155) | def test_extract_node_configuration_with_accelerator_classifiers():
  function test_extract_node_configuration_with_comfyui_version (line 185) | def test_extract_node_configuration_with_comfyui_version():
  function test_extract_node_configuration_with_requires_comfyui (line 202) | def test_extract_node_configuration_with_requires_comfyui():
  function _write_pyproject (line 215) | def _write_pyproject(tmp_path, body: str) -> str:
  function test_dynamic_version_resolved_from_double_quoted_literal (line 222) | def test_dynamic_version_resolved_from_double_quoted_literal(tmp_path):
  function test_dynamic_version_resolved_from_VERSION_name (line 234) | def test_dynamic_version_resolved_from_VERSION_name(tmp_path):
  function test_dynamic_version_resolved_from_single_quotes (line 245) | def test_dynamic_version_resolved_from_single_quotes(tmp_path):
  function test_dynamic_version_resolved_with_type_annotation (line 256) | def test_dynamic_version_resolved_with_type_annotation(tmp_path):
  function test_dynamic_version_ignores_commented_line (line 267) | def test_dynamic_version_ignores_commented_line(tmp_path):
  function test_dynamic_version_first_match_wins (line 279) | def test_dynamic_version_first_match_wins(tmp_path):
  function test_static_version_wins_over_tool_comfy_version (line 290) | def test_static_version_wins_over_tool_comfy_version(tmp_path):
  function test_dynamic_version_without_tool_comfy_version_warns (line 304) | def test_dynamic_version_without_tool_comfy_version_warns(mock_echo, tmp...
  function test_dynamic_version_absolute_path_rejected (line 313) | def test_dynamic_version_absolute_path_rejected(mock_echo, tmp_path):
  function test_dynamic_version_windows_absolute_path_rejected (line 325) | def test_dynamic_version_windows_absolute_path_rejected(mock_echo, tmp_p...
  function test_dynamic_version_path_traversal_rejected (line 339) | def test_dynamic_version_path_traversal_rejected(mock_echo, tmp_path):
  function test_dynamic_version_missing_file_warns (line 351) | def test_dynamic_version_missing_file_warns(mock_echo, tmp_path):
  function test_dynamic_version_no_match_warns (line 363) | def test_dynamic_version_no_match_warns(mock_echo, tmp_path):
  function test_dynamic_version_handles_utf8_bom (line 375) | def test_dynamic_version_handles_utf8_bom(tmp_path):
  function test_dynamic_version_invalid_utf8_warns (line 388) | def test_dynamic_version_invalid_utf8_warns(mock_echo, tmp_path):
  function test_dynamic_version_scalar_tool_comfy_version_warns (line 403) | def test_dynamic_version_scalar_tool_comfy_version_warns(mock_echo, tmp_...
  function test_malformed_dynamic_scalar_string_warns (line 417) | def test_malformed_dynamic_scalar_string_warns(mock_echo, tmp_path):
  function test_dynamic_version_indented_only_does_not_match (line 431) | def test_dynamic_version_indented_only_does_not_match(tmp_path):
  function test_dynamic_version_trailing_inline_comment_resolves (line 445) | def test_dynamic_version_trailing_inline_comment_resolves(tmp_path):
  function test_dynamic_version_path_is_directory_warns (line 459) | def test_dynamic_version_path_is_directory_warns(mock_echo, tmp_path):
  function test_padded_static_version_is_stripped (line 473) | def test_padded_static_version_is_stripped(tmp_path):
  function test_dynamic_version_non_string_path_warns_as_type_error (line 483) | def test_dynamic_version_non_string_path_warns_as_type_error(mock_echo, ...
  function test_static_version_happy_path_emits_no_version_warnings (line 499) | def test_static_version_happy_path_emits_no_version_warnings(mock_echo, ...
  function test_malformed_toml_does_not_crash (line 518) | def test_malformed_toml_does_not_crash(tmp_path):
  function test_pyproject_with_utf8_bom_parses_successfully (line 526) | def test_pyproject_with_utf8_bom_parses_successfully(tmp_path):
  function test_pyproject_with_invalid_utf8_returns_none_gracefully (line 538) | def test_pyproject_with_invalid_utf8_returns_none_gracefully(tmp_path):
  function test_static_version_non_string_scalar_rejected (line 549) | def test_static_version_non_string_scalar_rejected(mock_echo, tmp_path):
  function test_static_version_array_rejected (line 561) | def test_static_version_array_rejected(mock_echo, tmp_path):
  function test_static_version_inline_table_rejected (line 573) | def test_static_version_inline_table_rejected(mock_echo, tmp_path):
  function test_project_scalar_at_root_does_not_crash (line 586) | def test_project_scalar_at_root_does_not_crash(mock_echo, tmp_path):
  function test_static_version_falsy_non_string_rejected (line 598) | def test_static_version_falsy_non_string_rejected(mock_echo, tmp_path, v...
  function test_dynamic_version_padded_literal_is_stripped (line 618) | def test_dynamic_version_padded_literal_is_stripped(tmp_path):
  function test_dynamic_version_empty_path_string_warns_as_not_set (line 635) | def test_dynamic_version_empty_path_string_warns_as_not_set(mock_echo, t...
  function test_dynamic_version_table_missing_path_key_warns_as_not_set (line 648) | def test_dynamic_version_table_missing_path_key_warns_as_not_set(mock_ec...
  function test_falsy_nonstring_path_values_warn_as_type_mismatch (line 661) | def test_falsy_nonstring_path_values_warn_as_type_mismatch(mock_echo, tm...
  function test_dynamic_version_backslash_in_value_not_matched (line 682) | def test_dynamic_version_backslash_in_value_not_matched(mock_echo, tmp_p...
  function test_dynamic_version_adjacent_literals_double_quote_warns (line 704) | def test_dynamic_version_adjacent_literals_double_quote_warns(mock_echo,...
  function test_dynamic_version_adjacent_literals_no_whitespace_warns (line 724) | def test_dynamic_version_adjacent_literals_no_whitespace_warns(mock_echo...
  function test_dynamic_version_adjacent_literals_single_quote_warns (line 739) | def test_dynamic_version_adjacent_literals_single_quote_warns(mock_echo,...
  function test_dynamic_version_adjacent_literals_mixed_quotes_warns (line 754) | def test_dynamic_version_adjacent_literals_mixed_quotes_warns(mock_echo,...
  function test_dynamic_version_semicolon_after_literal_still_resolves (line 770) | def test_dynamic_version_semicolon_after_literal_still_resolves(tmp_path):
  function test_validate_and_extract_os_classifiers_valid (line 785) | def test_validate_and_extract_os_classifiers_valid():
  function test_validate_and_extract_os_classifiers_invalid (line 800) | def test_validate_and_extract_os_classifiers_invalid(mock_echo):
  function test_validate_and_extract_accelerator_classifiers_valid (line 813) | def test_validate_and_extract_accelerator_classifiers_valid():
  function test_validate_and_extract_accelerator_classifiers_invalid (line 835) | def test_validate_and_extract_accelerator_classifiers_invalid(mock_echo):
  function test_validate_version_valid (line 848) | def test_validate_version_valid():
  function test_validate_version_invalid (line 876) | def test_validate_version_invalid(mock_echo):
  function test_strip_url_credentials (line 913) | def test_strip_url_credentials(url, expected):
  function test_initialize_project_config_strips_credentials (line 917) | def test_initialize_project_config_strips_credentials(tmp_path, monkeypa...
  function test_initialize_project_config_clean_https (line 936) | def test_initialize_project_config_clean_https(tmp_path, monkeypatch):
  function test_initialize_project_config_ssh_remote (line 954) | def test_initialize_project_config_ssh_remote(tmp_path, monkeypatch):
  function _init_git_repo_with_reqs (line 979) | def _init_git_repo_with_reqs(tmp_path, requirements_content: str) -> None:
  function test_initialize_project_config_strips_inline_comments (line 990) | def test_initialize_project_config_strips_inline_comments(tmp_path, monk...
  function test_initialize_project_config_skips_full_line_comments (line 1003) | def test_initialize_project_config_skips_full_line_comments(tmp_path, mo...
  function test_initialize_project_config_skips_pip_options (line 1016) | def test_initialize_project_config_skips_pip_options(tmp_path, monkeypat...
  function test_initialize_project_config_preserves_vcs_subdirectory_fragment (line 1042) | def test_initialize_project_config_preserves_vcs_subdirectory_fragment(t...
  function test_initialize_project_config_vcs_with_inline_comment (line 1058) | def test_initialize_project_config_vcs_with_inline_comment(tmp_path, mon...

FILE: tests/comfy_cli/test_aria2_download.py
  function aria2_env (line 18) | def aria2_env(monkeypatch):
  function fake_aria2p (line 25) | def fake_aria2p():
  function mock_aria2_success (line 40) | def mock_aria2_success(aria2_env, fake_aria2p):
  class TestAria2Download (line 79) | class TestAria2Download:
    method test_success (line 80) | def test_success(self, tmp_path, mock_aria2_success):
    method test_passes_headers (line 92) | def test_passes_headers(self, tmp_path, mock_aria2_success):
    method test_no_headers (line 103) | def test_no_headers(self, tmp_path, mock_aria2_success):
    method test_missing_server_env_raises (line 111) | def test_missing_server_env_raises(self, tmp_path, fake_aria2p, monkey...
    method test_import_error_raises (line 118) | def test_import_error_raises(self, tmp_path, aria2_env):
    method test_download_failure_raises (line 124) | def test_download_failure_raises(self, tmp_path, aria2_env, fake_aria2p):
    method test_download_removed_raises (line 143) | def test_download_removed_raises(self, tmp_path, aria2_env, fake_aria2p):
    method test_server_url_parsing (line 160) | def test_server_url_parsing(self, tmp_path, fake_aria2p, monkeypatch):
    method test_server_url_default_port (line 182) | def test_server_url_default_port(self, tmp_path, fake_aria2p, monkeypa...
    method test_server_url_without_scheme (line 204) | def test_server_url_without_scheme(self, tmp_path, fake_aria2p, monkey...
    method test_secret_passed_to_client (line 226) | def test_secret_passed_to_client(self, tmp_path, fake_aria2p, monkeypa...
    method test_malformed_server_url_raises (line 248) | def test_malformed_server_url_raises(self, tmp_path, fake_aria2p, monk...
    method test_update_connection_error_raises (line 256) | def test_update_connection_error_raises(self, tmp_path, aria2_env, fak...
    method test_file_missing_after_download_raises (line 273) | def test_file_missing_after_download_raises(self, tmp_path, aria2_env,...
  class TestDownloadFileDispatch (line 298) | class TestDownloadFileDispatch:
    method test_default_downloader_uses_httpx (line 299) | def test_default_downloader_uses_httpx(self, tmp_path):
    method test_downloader_httpx_explicit (line 312) | def test_downloader_httpx_explicit(self, tmp_path):
    method test_downloader_aria2_dispatches (line 325) | def test_downloader_aria2_dispatches(self, tmp_path):
    method test_invalid_downloader_raises (line 331) | def test_invalid_downloader_raises(self, tmp_path):

FILE: tests/comfy_cli/test_cm_cli_python_resolution.py
  function _setup_cm_cli (line 12) | def _setup_cm_cli(tmp_path, script_body):
  function _run (line 20) | def _run(tmp_path, args, *, fast_deps=False, raise_on_error=False):
  class TestExecuteCmCli (line 50) | class TestExecuteCmCli:
    method test_uses_resolved_python (line 51) | def test_uses_resolved_python(self, tmp_path):
    method test_fast_deps_passes_python_to_compiler (line 76) | def test_fast_deps_passes_python_to_compiler(self, tmp_path):
    method test_stdout_returned_and_streamed (line 104) | def test_stdout_returned_and_streamed(self, tmp_path, capsys):
    method test_expected_error_codes_return_none (line 120) | def test_expected_error_codes_return_none(self, tmp_path, returncode):
    method test_unexpected_error_code_raises (line 131) | def test_unexpected_error_code_raises(self, tmp_path):
    method test_raise_on_error_overrides_silent_return (line 143) | def test_raise_on_error_overrides_silent_return(self, tmp_path):
    method test_output_streams_incrementally (line 157) | def test_output_streams_incrementally(self, tmp_path):
    method test_pythonunbuffered_set_in_env (line 183) | def test_pythonunbuffered_set_in_env(self, tmp_path):

FILE: tests/comfy_cli/test_cmdline_python_resolution.py
  class TestUpdateComfy (line 6) | class TestUpdateComfy:
    method test_uses_resolved_python (line 7) | def test_uses_resolved_python(self, tmp_path):
    method test_update_comfy_succeeds_when_cm_cli_missing (line 28) | def test_update_comfy_succeeds_when_cm_cli_missing(self, tmp_path):
  class TestDependency (line 44) | class TestDependency:
    method test_passes_python_to_compiler (line 45) | def test_passes_python_to_compiler(self, tmp_path):

FILE: tests/comfy_cli/test_config_manager.py
  function _make_config_manager (line 16) | def _make_config_manager(config_dir, is_running_val=True):
  function config_mgr (line 25) | def config_mgr(tmp_path):
  class TestLoad (line 31) | class TestLoad:
    method test_creates_tmp_directory (line 32) | def test_creates_tmp_directory(self, tmp_path):
    method test_reads_existing_config (line 38) | def test_reads_existing_config(self, tmp_path):
    method test_parses_background_info (line 47) | def test_parses_background_info(self, tmp_path):
    method test_removes_background_when_stale_pid (line 56) | def test_removes_background_when_stale_pid(self, tmp_path):
  class TestWriteConfig (line 67) | class TestWriteConfig:
    method test_creates_directory_if_missing (line 68) | def test_creates_directory_if_missing(self, tmp_path):
    method test_set_persists_to_file (line 77) | def test_set_persists_to_file(self, config_mgr):
  class TestGetBool (line 84) | class TestGetBool:
    method test_missing_key_returns_none (line 85) | def test_missing_key_returns_none(self, config_mgr):
  class TestGetOrOverride (line 89) | class TestGetOrOverride:
    method test_set_value_wins (line 90) | def test_set_value_wins(self, config_mgr):
    method test_env_var_wins_over_config (line 95) | def test_env_var_wins_over_config(self, config_mgr):
    method test_config_is_fallback (line 100) | def test_config_is_fallback(self, config_mgr):
    method test_empty_set_value_returns_none (line 107) | def test_empty_set_value_returns_none(self, config_mgr):
    method test_empty_env_var_returns_none (line 110) | def test_empty_env_var_returns_none(self, config_mgr):
    method test_set_value_is_persisted (line 114) | def test_set_value_is_persisted(self, config_mgr):
    method test_all_missing_returns_none (line 118) | def test_all_missing_returns_none(self, config_mgr):
  class TestGetEnvData (line 125) | class TestGetEnvData:
    method test_full_config (line 126) | def test_full_config(self, config_mgr):
    method test_empty_config (line 142) | def test_empty_config(self, config_mgr):
    method test_launch_extras_only_read_when_workspace_set (line 149) | def test_launch_extras_only_read_when_workspace_set(self, config_mgr):
  class TestRemoveBackground (line 155) | class TestRemoveBackground:
    method test_clears_background (line 156) | def test_clears_background(self, config_mgr):

FILE: tests/comfy_cli/test_cuda_detect.py
  class TestDetectViaCtypes (line 17) | class TestDetectViaCtypes:
    method test_happy_path (line 18) | def test_happy_path(self):
    method test_version_decoding (line 41) | def test_version_decoding(self, raw, expected):
    method test_library_not_found (line 58) | def test_library_not_found(self):
    method test_cuinit_fails (line 62) | def test_cuinit_fails(self):
  class TestDetectViaNvidiaSmi (line 70) | class TestDetectViaNvidiaSmi:
    method test_happy_path (line 71) | def test_happy_path(self):
    method test_cuda_13 (line 80) | def test_cuda_13(self):
    method test_not_found (line 85) | def test_not_found(self):
    method test_parse_failure (line 89) | def test_parse_failure(self):
    method test_timeout (line 93) | def test_timeout(self):
  class TestDetectCudaDriverVersion (line 101) | class TestDetectCudaDriverVersion:
    method test_ctypes_success_skips_smi (line 102) | def test_ctypes_success_skips_smi(self):
    method test_ctypes_fails_falls_back_to_smi (line 120) | def test_ctypes_fails_falls_back_to_smi(self):
    method test_both_fail (line 127) | def test_both_fail(self):
    method test_cuda_visible_devices_restored (line 134) | def test_cuda_visible_devices_restored(self):
    method test_cuda_visible_devices_empty_string (line 145) | def test_cuda_visible_devices_empty_string(self):
  class TestResolveCudaWheel (line 173) | class TestResolveCudaWheel:
    method test_mapping (line 189) | def test_mapping(self, driver_version, expected):
    method test_driver_too_old (line 192) | def test_driver_too_old(self):
    method test_very_new_driver (line 196) | def test_very_new_driver(self):
    method test_exact_match_preferred (line 200) | def test_exact_match_preferred(self):
  class TestLoadLibcuda (line 205) | class TestLoadLibcuda:
    method test_linux_paths_tried_in_order (line 206) | def test_linux_paths_tried_in_order(self):
    method test_windows_path (line 227) | def test_windows_path(self):
    method test_first_success_wins (line 243) | def test_first_success_wins(self):
  class TestConstants (line 259) | class TestConstants:
    method test_wheels_in_descending_order (line 260) | def test_wheels_in_descending_order(self):
    method test_default_tag_is_in_wheel_list (line 268) | def test_default_tag_is_in_wheel_list(self):

FILE: tests/comfy_cli/test_cuda_detect_real.py
  function _nvidia_smi_cuda_version (line 30) | def _nvidia_smi_cuda_version() -> tuple[int, int] | None:
  class TestRealDetection (line 42) | class TestRealDetection:
    method test_detect_returns_valid_tuple (line 43) | def test_detect_returns_valid_tuple(self):
    method test_detect_matches_nvidia_smi (line 51) | def test_detect_matches_nvidia_smi(self):
    method test_nvidia_smi_fallback_works (line 61) | def test_nvidia_smi_fallback_works(self):
    method test_resolve_wheel_for_detected_driver (line 67) | def test_resolve_wheel_for_detected_driver(self):
    method test_resolved_wheel_version_not_greater_than_driver (line 75) | def test_resolved_wheel_version_not_greater_than_driver(self):

FILE: tests/comfy_cli/test_custom_nodes_python_resolution.py
  class TestGetInstalledPackages (line 7) | class TestGetInstalledPackages:
    method test_uses_resolved_python (line 8) | def test_uses_resolved_python(self):
  class TestExecuteInstallScript (line 31) | class TestExecuteInstallScript:
    method test_pip_uses_resolved_python (line 32) | def test_pip_uses_resolved_python(self, tmp_path):
    method test_install_py_uses_resolved_python (line 50) | def test_install_py_uses_resolved_python(self, tmp_path):
    method test_inline_comment_not_passed_to_pip (line 67) | def test_inline_comment_not_passed_to_pip(self, tmp_path):
    method test_uses_pip_install_r (line 88) | def test_uses_pip_install_r(self, tmp_path):
    method test_requirements_path_is_absolute_when_repo_path_is_relative (line 109) | def test_requirements_path_is_absolute_when_repo_path_is_relative(self...
  class TestUpdateNodeIdCache (line 134) | class TestUpdateNodeIdCache:
    method test_uses_resolved_python (line 135) | def test_uses_resolved_python(self, tmp_path):

FILE: tests/comfy_cli/test_env_checker.py
  class TestFormatPythonVersion (line 14) | class TestFormatPythonVersion:
    method test_modern_python (line 15) | def test_modern_python(self):
    method test_python_39_is_modern (line 19) | def test_python_39_is_modern(self):
    method test_python_38_is_old (line 23) | def test_python_38_is_old(self):
    method test_python_37_is_old (line 29) | def test_python_37_is_old(self):
  class TestCheckComfyServerRunning (line 35) | class TestCheckComfyServerRunning:
    method test_server_running (line 37) | def test_server_running(self, mock_get):
    method test_server_not_running (line 42) | def test_server_not_running(self, mock_get):
    method test_non_200_status (line 47) | def test_non_200_status(self, mock_get):
    method test_custom_port_and_host (line 52) | def test_custom_port_and_host(self, mock_get):
  class TestEnvChecker (line 58) | class TestEnvChecker:
    method checker (line 60) | def checker(self):
    method test_check_detects_virtualenv (line 67) | def test_check_detects_virtualenv(self, checker):
    method test_check_detects_conda (line 72) | def test_check_detects_conda(self, checker):
    method test_check_no_isolated_env (line 77) | def test_check_no_isolated_env(self, checker):
    method test_get_isolated_env_prefers_venv (line 86) | def test_get_isolated_env_prefers_venv(self, checker):
    method test_get_isolated_env_falls_back_to_conda (line 91) | def test_get_isolated_env_falls_back_to_conda(self, checker):
    method test_fill_print_table_server_running (line 97) | def test_fill_print_table_server_running(self, mock_cm, mock_server, c...
    method test_fill_print_table_server_not_running (line 104) | def test_fill_print_table_server_not_running(self, mock_cm, mock_serve...

FILE: tests/comfy_cli/test_file_utils.py
  function test_zip_files_respects_comfyignore (line 6) | def test_zip_files_respects_comfyignore(tmp_path, monkeypatch):
  function test_zip_files_force_include_overrides_ignore (line 39) | def test_zip_files_force_include_overrides_ignore(tmp_path, monkeypatch):
  function test_zip_files_without_git_falls_back_to_walk (line 69) | def test_zip_files_without_git_falls_back_to_walk(tmp_path, monkeypatch):

FILE: tests/comfy_cli/test_global_python_install.py
  function _clean_env (line 23) | def _clean_env():
  class TestGlobalPythonDetection (line 40) | class TestGlobalPythonDetection:
    method test_global_python_skips_venv_creation (line 44) | def test_global_python_skips_venv_creation(self, tmp_path):
    method test_isolated_env_creates_venv (line 62) | def test_isolated_env_creates_venv(self, tmp_path):
  class TestGlobalPythonInstallExecute (line 76) | class TestGlobalPythonInstallExecute:
    method _run_execute (line 80) | def _run_execute(self, tmp_path, *, fast_deps, python="/usr/bin/python...
    method test_fast_deps_global_python_skips_install_build_deps (line 108) | def test_fast_deps_global_python_skips_install_build_deps(self, tmp_pa...
    method test_fast_deps_venv_python_calls_install_build_deps (line 116) | def test_fast_deps_venv_python_calls_install_build_deps(self, tmp_path):
    method test_non_fast_deps_uses_global_python (line 125) | def test_non_fast_deps_uses_global_python(self, tmp_path):
  class TestDependencyCompilerGlobalPython (line 138) | class TestDependencyCompilerGlobalPython:
    method workspace (line 145) | def workspace(self, tmp_path):
    method test_compile_produces_complete_output (line 152) | def test_compile_produces_complete_output(self, workspace):
    method test_compile_nvidia_resolves_torch (line 174) | def test_compile_nvidia_resolves_torch(self, workspace):
    method test_install_targets_correct_python (line 195) | def test_install_targets_correct_python(self, workspace):

FILE: tests/comfy_cli/test_install.py
  function test_validate_version_nightly (line 8) | def test_validate_version_nightly():
  function test_validate_version_latest (line 13) | def test_validate_version_latest():
  function test_validate_version_valid_semver (line 18) | def test_validate_version_valid_semver():
  function test_validate_version_invalid (line 24) | def test_validate_version_invalid():
  function test_validate_version_empty (line 29) | def test_validate_version_empty():
  class TestPipInstallManager (line 34) | class TestPipInstallManager:
    method test_success (line 38) | def test_success(self, mock_exists, mock_run, mock_find):
    method test_missing_requirements_file (line 45) | def test_missing_requirements_file(self, mock_exists):
    method test_pip_failure (line 51) | def test_pip_failure(self, mock_exists, mock_run):
    method test_pip_failure_no_stderr (line 58) | def test_pip_failure_no_stderr(self, mock_exists, mock_run):

FILE: tests/comfy_cli/test_install_python_resolution.py
  class TestPipInstallComfyuiDependencies (line 11) | class TestPipInstallComfyuiDependencies:
    method test_uses_python_param_cpu (line 12) | def test_uses_python_param_cpu(self, tmp_path):
  class TestPipInstallManager (line 33) | class TestPipInstallManager:
    method test_uses_python_param (line 34) | def test_uses_python_param(self, tmp_path):
  class TestExecute (line 48) | class TestExecute:
    method test_calls_ensure_and_passes_resolved_python (line 49) | def test_calls_ensure_and_passes_resolved_python(self, tmp_path):
    method test_fast_deps_passes_python_to_dependency_compiler (line 74) | def test_fast_deps_passes_python_to_dependency_compiler(self, tmp_path):
    method test_fast_deps_forwards_skip_torch (line 105) | def test_fast_deps_forwards_skip_torch(self, tmp_path):
    method test_fast_deps_cuda_tag_converted_to_dotted_version (line 135) | def test_fast_deps_cuda_tag_converted_to_dotted_version(self, tmp_path):
    method test_fast_deps_explicit_cuda_version_no_tag (line 164) | def test_fast_deps_explicit_cuda_version_no_tag(self, tmp_path):
  class TestAutoDetectIntegration (line 194) | class TestAutoDetectIntegration:
    method test_auto_detected_cuda_tag_used (line 195) | def test_auto_detected_cuda_tag_used(self, tmp_path):
    method test_auto_detect_failure_falls_back (line 214) | def test_auto_detect_failure_falls_back(self, tmp_path):
    method test_explicit_cuda_version_used_when_no_tag (line 233) | def test_explicit_cuda_version_used_when_no_tag(self, tmp_path):
    method test_cuda_tag_takes_precedence_over_enum (line 252) | def test_cuda_tag_takes_precedence_over_enum(self, tmp_path):
  function _get_torch_install_cmd (line 272) | def _get_torch_install_cmd(calls):
  class TestTorchInstallCommands (line 281) | class TestTorchInstallCommands:
    method test_amd_uses_index_url_with_rocm_version (line 292) | def test_amd_uses_index_url_with_rocm_version(self, tmp_path, rocm_ver...
    method test_nvidia_uses_index_url_with_cuda_version (line 323) | def test_nvidia_uses_index_url_with_cuda_version(self, tmp_path, cuda_...
    method test_nvidia_linux_uses_index_url (line 343) | def test_nvidia_linux_uses_index_url(self, tmp_path):

FILE: tests/comfy_cli/test_launch_python_resolution.py
  class TestLaunchComfyui (line 10) | class TestLaunchComfyui:
    method test_uses_python_param (line 11) | def test_uses_python_param(self):
    method test_foreground_exit_code_matches_subprocess (line 27) | def test_foreground_exit_code_matches_subprocess(self, returncode):
  class TestLaunchResolvesWorkspacePython (line 41) | class TestLaunchResolvesWorkspacePython:
    method test_resolves_and_passes_python (line 42) | def test_resolves_and_passes_python(self):

FILE: tests/comfy_cli/test_models_python_resolution.py
  function _block_huggingface_hub (line 13) | def _block_huggingface_hub(name, *args, **kwargs):
  class TestDownloadHuggingfacePipInstall (line 19) | class TestDownloadHuggingfacePipInstall:
    method test_uses_resolved_python (line 20) | def test_uses_resolved_python(self, tmp_path):

FILE: tests/comfy_cli/test_resolve_python.py
  function _clean_env (line 17) | def _clean_env(**overrides):
  function _make_fake_python (line 42) | def _make_fake_python(base_dir, name="bin/python"):
  function _make_real_venv (line 50) | def _make_real_venv(base_dir):
  class TestIsExternallyManaged (line 58) | class TestIsExternallyManaged:
    method test_true_when_marker_exists (line 59) | def test_true_when_marker_exists(self, tmp_path):
    method test_false_when_no_marker (line 65) | def test_false_when_no_marker(self, tmp_path):
    method test_false_when_stdlib_is_none (line 69) | def test_false_when_stdlib_is_none(self):
  class TestGetPythonBinary (line 74) | class TestGetPythonBinary:
    method test_unix (line 76) | def test_unix(self, _mock):
    method test_macos (line 80) | def test_macos(self, _mock):
    method test_windows (line 84) | def test_windows(self, _mock):
  class TestResolveWorkspacePython (line 89) | class TestResolveWorkspacePython:
    method test_virtual_env_takes_precedence_over_workspace_venv (line 90) | def test_virtual_env_takes_precedence_over_workspace_venv(self, tmp_pa...
    method test_virtual_env_takes_precedence_over_conda (line 100) | def test_virtual_env_takes_precedence_over_conda(self, tmp_path):
    method test_conda_prefix_when_no_virtual_env (line 110) | def test_conda_prefix_when_no_virtual_env(self, tmp_path):
    method test_conda_prefix_python_missing_falls_through (line 118) | def test_conda_prefix_python_missing_falls_through(self, tmp_path):
    method test_virtual_env_missing_falls_to_conda (line 126) | def test_virtual_env_missing_falls_to_conda(self, tmp_path):
    method test_workspace_dot_venv_found (line 136) | def test_workspace_dot_venv_found(self, tmp_path):
    method test_workspace_venv_found (line 144) | def test_workspace_venv_found(self, tmp_path):
    method test_dot_venv_preferred_over_venv (line 152) | def test_dot_venv_preferred_over_venv(self, tmp_path):
    method test_workspace_dot_venv_dir_exists_but_python_missing (line 161) | def test_workspace_dot_venv_dir_exists_but_python_missing(self, tmp_pa...
    method test_workspace_dot_venv_broken_falls_to_venv (line 169) | def test_workspace_dot_venv_broken_falls_to_venv(self, tmp_path):
    method test_workspace_venv_dir_exists_but_python_missing (line 178) | def test_workspace_venv_dir_exists_but_python_missing(self, tmp_path):
    method test_fallback_to_sys_executable (line 186) | def test_fallback_to_sys_executable(self, tmp_path):
    method test_none_workspace_path (line 191) | def test_none_workspace_path(self):
    method test_virtual_env_python_missing_falls_through (line 196) | def test_virtual_env_python_missing_falls_through(self, tmp_path):
    method test_with_real_venv (line 204) | def test_with_real_venv(self, tmp_path):
  class TestEnsureWorkspacePython (line 217) | class TestEnsureWorkspacePython:
    method test_with_virtual_env_does_not_create_venv (line 218) | def test_with_virtual_env_does_not_create_venv(self, tmp_path):
    method test_with_conda_does_not_create_venv (line 229) | def test_with_conda_does_not_create_venv(self, tmp_path):
    method test_with_both_env_vars_uses_virtual_env (line 240) | def test_with_both_env_vars_uses_virtual_env(self, tmp_path):
    method test_global_python_returns_sys_executable (line 253) | def test_global_python_returns_sys_executable(self, tmp_path):
    method test_global_python_pep668_creates_venv (line 270) | def test_global_python_pep668_creates_venv(self, tmp_path):
    method test_creates_venv_when_isolated_env (line 288) | def test_creates_venv_when_isolated_env(self, tmp_path):
    method test_existing_dot_venv_reused (line 306) | def test_existing_dot_venv_reused(self, tmp_path):
    method test_existing_venv_reused (line 314) | def test_existing_venv_reused(self, tmp_path):
    method test_broken_dot_venv_falls_to_venv (line 322) | def test_broken_dot_venv_falls_to_venv(self, tmp_path):
    method test_broken_dot_venv_global_python_returns_sys_executable (line 331) | def test_broken_dot_venv_global_python_returns_sys_executable(self, tm...
    method test_broken_dot_venv_isolated_env_creates_new (line 346) | def test_broken_dot_venv_isolated_env_creates_new(self, tmp_path):
  class TestCreateWorkspaceVenv (line 361) | class TestCreateWorkspaceVenv:
    method test_creates_working_venv (line 362) | def test_creates_working_venv(self, tmp_path):
    method test_created_venv_has_pip (line 373) | def test_created_venv_has_pip(self, tmp_path):
    method test_created_venv_is_isolated (line 382) | def test_created_venv_is_isolated(self, tmp_path):
    method test_returns_platform_specific_path (line 392) | def test_returns_platform_specific_path(self, tmp_path):
    method test_idempotent (line 402) | def test_idempotent(self, tmp_path):
    method test_failure_raises (line 413) | def test_failure_raises(self, tmp_path):

FILE: tests/comfy_cli/test_standalone.py
  function _mock_response (line 25) | def _mock_response(text, status_code=200):
  class TestResolvePythonVersion (line 35) | class TestResolvePythonVersion:
    method test_resolves_312 (line 37) | def test_resolves_312(self, mock_get):
    method test_resolves_310 (line 43) | def test_resolves_310(self, mock_get):
    method test_resolves_313 (line 49) | def test_resolves_313(self, mock_get):
    method test_missing_version_raises (line 55) | def test_missing_version_raises(self, mock_get):
    method test_http_error_propagates (line 61) | def test_http_error_propagates(self, mock_get):
    method test_picks_highest_patch (line 67) | def test_picks_highest_patch(self, mock_get):
    method test_url_construction (line 79) | def test_url_construction(self, mock_get):
    method test_no_false_match_across_minor (line 85) | def test_no_false_match_across_minor(self, mock_get):
  class TestDownloadStandalonePython (line 92) | class TestDownloadStandalonePython:
    method test_minor_version_triggers_resolution (line 95) | def test_minor_version_triggers_resolution(self, mock_get, mock_downlo...
    method test_full_version_skips_resolution (line 113) | def test_full_version_skips_resolution(self, mock_get, mock_download):
  class TestResolveVersionIntegration (line 131) | class TestResolveVersionIntegration:
    method test_latest_release_json_is_reachable (line 134) | def test_latest_release_json_is_reachable(self):
    method test_resolve_312_from_real_release (line 143) | def test_resolve_312_from_real_release(self):
    method test_sha256sums_contains_expected_platforms (line 153) | def test_sha256sums_contains_expected_platforms(self):

FILE: tests/comfy_cli/test_tracking.py
  function tracking_module (line 13) | def tracking_module(tmp_path):
  class TestTrackEvent (line 32) | class TestTrackEvent:
    method test_short_circuits_when_disabled (line 33) | def test_short_circuits_when_disabled(self, tracking_module):
    method test_short_circuits_when_not_configured (line 38) | def test_short_circuits_when_not_configured(self, tracking_module):
    method test_fires_when_enabled (line 42) | def test_fires_when_enabled(self, tracking_module):
    method test_properties_default_to_empty_dict (line 52) | def test_properties_default_to_empty_dict(self, tracking_module):
    method test_swallows_mixpanel_errors (line 59) | def test_swallows_mixpanel_errors(self, tracking_module):
  class TestTrackCommandRedaction (line 66) | class TestTrackCommandRedaction:
    method test_api_key_value_is_redacted (line 69) | def test_api_key_value_is_redacted(self, tracking_module):
    method test_api_key_none_stays_none (line 85) | def test_api_key_none_stays_none(self, tracking_module):
  class TestInitTrackingRoundTrip (line 101) | class TestInitTrackingRoundTrip:
    method test_disable_is_respected_by_track_event (line 108) | def test_disable_is_respected_by_track_event(self, tracking_module):
    method test_enable_is_respected_by_track_event (line 113) | def test_enable_is_respected_by_track_event(self, tracking_module):
    method test_disable_persists_as_parseable_bool (line 119) | def test_disable_persists_as_parseable_bool(self, tracking_module):
    method test_enable_generates_user_id (line 123) | def test_enable_generates_user_id(self, tracking_module):
    method test_disable_does_not_generate_user_id (line 132) | def test_disable_does_not_generate_user_id(self, tracking_module):
    method test_install_event_fires_once_across_calls (line 136) | def test_install_event_fires_once_across_calls(self, tracking_module):

FILE: tests/comfy_cli/test_ui.py
  function _capture (line 9) | def _capture(fn, *args, **kwargs):
  class TestDisplayErrorMessageMarkup (line 21) | class TestDisplayErrorMessageMarkup:
    method test_plain_message_rendered (line 26) | def test_plain_message_rendered(self):
    method test_closing_tag_alone_does_not_crash (line 30) | def test_closing_tag_alone_does_not_crash(self):
    method test_bracketed_substring_preserved (line 35) | def test_bracketed_substring_preserved(self):
    method test_multiple_markup_like_tokens (line 41) | def test_multiple_markup_like_tokens(self):
    method test_unbalanced_opening_bracket (line 47) | def test_unbalanced_opening_bracket(self):

FILE: tests/comfy_cli/test_update.py
  function _mock_pypi_response (line 8) | def _mock_pypi_response(latest_version):
  class TestCheckForNewerPypiVersion (line 15) | class TestCheckForNewerPypiVersion:
    method test_newer_version_available (line 17) | def test_newer_version_available(self, mock_get):
    method test_no_update_when_current (line 24) | def test_no_update_when_current(self, mock_get):
    method test_network_failure_returns_false (line 31) | def test_network_failure_returns_false(self, mock_get):
    method test_timeout_value_is_passed (line 38) | def test_timeout_value_is_passed(self, mock_get):
  class TestCheckForUpdates (line 44) | class TestCheckForUpdates:
    method test_notifies_when_update_available (line 48) | def test_notifies_when_update_available(self, mock_get, _mock_ver, moc...
    method test_no_notification_on_network_error (line 56) | def test_no_notification_on_network_error(self, mock_get, _mock_ver, m...

FILE: tests/comfy_cli/test_utils.py
  class _FakeRaw (line 7) | class _FakeRaw(io.BytesIO):
    method read (line 16) | def read(self, amt=-1, decode_content=False):
  class TestDownloadUrl (line 20) | class TestDownloadUrl:
    method test_writes_file (line 22) | def test_writes_file(self, mock_get, tmp_path):
  class TestTarballRoundTrip (line 35) | class TestTarballRoundTrip:
    method test_create_and_extract (line 36) | def test_create_and_extract(self, tmp_path, monkeypatch):

FILE: tests/comfy_cli/test_workflow_to_api.py
  function object_info (line 27) | def object_info():
  function _node (line 104) | def _node(node_id, node_type, *, inputs=None, outputs=None, widgets=None...
  class TestIsApiFormat (line 124) | class TestIsApiFormat:
    method test_recognizes_api (line 125) | def test_recognizes_api(self):
    method test_ui_is_not_api (line 128) | def test_ui_is_not_api(self):
    method test_non_dict_is_not_api (line 131) | def test_non_dict_is_not_api(self):
    method test_empty_dict_is_not_api (line 136) | def test_empty_dict_is_not_api(self):
    method test_metadata_only_is_not_api (line 139) | def test_metadata_only_is_not_api(self):
  class TestIsSubgraphUuid (line 144) | class TestIsSubgraphUuid:
    method test_real_uuid (line 145) | def test_real_uuid(self):
    method test_class_name_is_not_uuid (line 148) | def test_class_name_is_not_uuid(self):
    method test_wrong_length (line 151) | def test_wrong_length(self):
    method test_wrong_dash_count (line 154) | def test_wrong_dash_count(self):
    method test_non_string (line 157) | def test_non_string(self):
  class TestConvertCore (line 167) | class TestConvertCore:
    method test_already_api_is_returned_unchanged (line 168) | def test_already_api_is_returned_unchanged(self, object_info):
    method test_minimal_workflow (line 172) | def test_minimal_workflow(self, object_info):
    method test_input_order_follows_schema (line 199) | def test_input_order_follows_schema(self, object_info):
    method test_unknown_node_type_uses_class_name_as_title (line 246) | def test_unknown_node_type_uses_class_name_as_title(self, object_info):
    method test_node_title_overrides_display_name (line 266) | def test_node_title_overrides_display_name(self, object_info):
    method test_invalid_workflow_raises (line 283) | def test_invalid_workflow_raises(self, object_info):
  class TestSpecialNodes (line 293) | class TestSpecialNodes:
    method test_primitive_node_inlines_value (line 294) | def test_primitive_node_inlines_value(self, object_info):
    method test_reroute_is_transparent (line 323) | def test_reroute_is_transparent(self, object_info):
    method test_get_set_node_pair (line 345) | def test_get_set_node_pair(self, object_info):
    method test_muted_node_is_excluded (line 375) | def test_muted_node_is_excluded(self, object_info):
    method test_bypassed_node_passes_through (line 393) | def test_bypassed_node_passes_through(self, object_info):
    method test_load_image_output_excluded (line 419) | def test_load_image_output_excluded(self, object_info):
    method test_note_node_excluded (line 440) | def test_note_node_excluded(self, object_info):
    method test_output_node_kept_even_without_outgoing_links (line 451) | def test_output_node_kept_even_without_outgoing_links(self, object_info):
    method test_unwired_node_still_emitted (line 463) | def test_unwired_node_still_emitted(self, object_info):
    method test_unwired_load_node_still_emitted (line 486) | def test_unwired_load_node_still_emitted(self, object_info):
    method test_markdown_note_excluded (line 521) | def test_markdown_note_excluded(self, object_info):
  class TestSchemaAwareBehavior (line 549) | class TestSchemaAwareBehavior:
    method test_combo_value_normalized_case_insensitively (line 550) | def test_combo_value_normalized_case_insensitively(self, object_info):
    method test_defaults_filled_when_widget_values_absent (line 575) | def test_defaults_filled_when_widget_values_absent(self, object_info):
  class TestMalformedInputHardening (line 601) | class TestMalformedInputHardening:
    method test_rejects_non_dict_workflow (line 609) | def test_rejects_non_dict_workflow(self, object_info):
    method test_rejects_non_dict_object_info (line 615) | def test_rejects_non_dict_object_info(self):
    method test_rejects_missing_nodes_or_links (line 619) | def test_rejects_missing_nodes_or_links(self, object_info):
    method test_skips_non_dict_node_entries (line 625) | def test_skips_non_dict_node_entries(self, object_info):
    method test_tolerates_garbage_in_inputs_and_outputs (line 641) | def test_tolerates_garbage_in_inputs_and_outputs(self, object_info):
    method test_tolerates_non_list_widgets_values (line 662) | def test_tolerates_non_list_widgets_values(self, object_info):
    method test_tolerates_non_numeric_slot_in_link (line 681) | def test_tolerates_non_numeric_slot_in_link(self, object_info):
    method test_tolerates_garbage_definitions (line 702) | def test_tolerates_garbage_definitions(self, object_info):
    method test_set_get_node_with_unhashable_var_name_does_not_crash (line 716) | def test_set_get_node_with_unhashable_var_name_does_not_crash(self, ob...
    method test_unhashable_link_value_in_global_helpers_does_not_crash (line 739) | def test_unhashable_link_value_in_global_helpers_does_not_crash(self, ...
    method test_subgraph_link_with_unhashable_id_is_skipped (line 806) | def test_subgraph_link_with_unhashable_id_is_skipped(self, object_info):
    method test_inner_node_with_unhashable_link_id_does_not_crash (line 830) | def test_inner_node_with_unhashable_link_id_does_not_crash(self, objec...
    method test_malformed_subgraph_definition_does_not_crash (line 860) | def test_malformed_subgraph_definition_does_not_crash(self, object_info):
    method test_outer_subgraph_node_with_non_dict_inputs_does_not_crash (line 884) | def test_outer_subgraph_node_with_non_dict_inputs_does_not_crash(self,...
    method test_v3_combo_option_with_non_dict_inputs_keeps_node (line 903) | def test_v3_combo_option_with_non_dict_inputs_keeps_node(self):
    method test_malformed_schema_input_does_not_crash (line 942) | def test_malformed_schema_input_does_not_crash(self):
    method test_malformed_schema_input_order_does_not_crash (line 973) | def test_malformed_schema_input_order_does_not_crash(self):
    method test_single_bad_node_does_not_abort_conversion (line 1000) | def test_single_bad_node_does_not_abort_conversion(self, object_info, ...
  class TestControlAfterGenerate (line 1034) | class TestControlAfterGenerate:
    method test_seed_widget_with_control_marker_strips_correctly (line 1040) | def test_seed_widget_with_control_marker_strips_correctly(self):
    method test_legitimate_value_named_fixed_is_preserved (line 1073) | def test_legitimate_value_named_fixed_is_preserved(self):
    method test_unknown_node_falls_back_to_legacy_filter (line 1105) | def test_unknown_node_falls_back_to_legacy_filter(self):
  class TestWildcardInputType (line 1134) | class TestWildcardInputType:
    method test_star_wildcard_not_treated_as_widget (line 1174) | def test_star_wildcard_not_treated_as_widget(self):
    method test_empty_string_wildcard_does_not_consume_widget_slot (line 1192) | def test_empty_string_wildcard_does_not_consume_widget_slot(self):
  class TestImplicitSeedCompanion (line 1214) | class TestImplicitSeedCompanion:
    method test_seed_named_input_strips_implicit_companion (line 1259) | def test_seed_named_input_strips_implicit_companion(self):
    method test_noise_seed_named_input_strips_implicit_companion (line 1276) | def test_noise_seed_named_input_strips_implicit_companion(self):
    method test_seed_input_without_companion_still_works (line 1293) | def test_seed_input_without_companion_still_works(self):
    method test_regular_int_input_does_not_strip_control_value (line 1313) | def test_regular_int_input_does_not_strip_control_value(self):
  class TestNodeNameForSAndRAlias (line 1336) | class TestNodeNameForSAndRAlias:
    method _aliased_workflow (line 1367) | def _aliased_workflow(self, *, widgets_values):
    method test_meta_title_uses_aliased_schema (line 1391) | def test_meta_title_uses_aliased_schema(self):
    method test_combo_normalization_uses_aliased_schema (line 1395) | def test_combo_normalization_uses_aliased_schema(self):
    method test_defaults_filled_from_aliased_schema (line 1400) | def test_defaults_filled_from_aliased_schema(self):
    method test_aliased_node_with_no_connections_still_emits (line 1405) | def test_aliased_node_with_no_connections_still_emits(self):
  class TestForceInputHandling (line 1431) | class TestForceInputHandling:
    method test_forceinput_widget_does_not_consume_value_slot (line 1440) | def test_forceinput_widget_does_not_consume_value_slot(self):
    method test_legacy_defaultinput_alias_works_the_same (line 1481) | def test_legacy_defaultinput_alias_works_the_same(self):
  class TestFrontendParity (line 1523) | class TestFrontendParity:
    method test_list_widget_value_is_wrapped_to_disambiguate_from_link (line 1526) | def test_list_widget_value_is_wrapped_to_disambiguate_from_link(self, ...
    method test_orphan_link_inputs_are_stripped (line 1553) | def test_orphan_link_inputs_are_stripped(self, object_info):
    method test_bypass_matches_any_type_wildcard (line 1591) | def test_bypass_matches_any_type_wildcard(self, object_info):
    method test_bypass_falls_back_to_first_linked_input_when_types_mismatch (line 1619) | def test_bypass_falls_back_to_first_linked_input_when_types_mismatch(s...
    method test_muted_node_does_not_leave_dangling_reference (line 1648) | def test_muted_node_does_not_leave_dangling_reference(self, object_info):
    method test_bypass_matches_comma_separated_types (line 1674) | def test_bypass_matches_comma_separated_types(self, object_info):
    method test_group_node_workflow_emits_warning (line 1700) | def test_group_node_workflow_emits_warning(self, object_info, caplog):
  class TestTracerChainDepth (line 1718) | class TestTracerChainDepth:
    method _consumer_id (line 1729) | def _consumer_id(self):
    method test_long_reroute_chain (line 1732) | def test_long_reroute_chain(self):
    method test_long_bypass_chain (line 1754) | def test_long_bypass_chain(self):
    method test_long_getset_chain (line 1782) | def test_long_getset_chain(self):
  class TestMutedBypassedSubgraph (line 1818) | class TestMutedBypassedSubgraph:
    method _workflow (line 1827) | def _workflow(self, mode, with_external_wires=False):
    method test_muted_subgraph_drops_inner_nodes (line 1881) | def test_muted_subgraph_drops_inner_nodes(self, object_info):
    method test_bypassed_subgraph_drops_inner_nodes (line 1885) | def test_bypassed_subgraph_drops_inner_nodes(self, object_info):
    method test_normal_subgraph_still_expands (line 1889) | def test_normal_subgraph_still_expands(self, object_info):
    method test_bypassed_subgraph_passes_external_input_through (line 1895) | def test_bypassed_subgraph_passes_external_input_through(self, object_...
  class TestDynamicComboAfterControlMarker (line 1904) | class TestDynamicComboAfterControlMarker:
    method test_dynamic_combo_selector_reads_from_filtered_slot (line 1916) | def test_dynamic_combo_selector_reads_from_filtered_slot(self):
  class TestDynamicPrompts (line 1960) | class TestDynamicPrompts:
    method test_no_braces_passes_through (line 1969) | def test_no_braces_passes_through(self):
    method test_strips_line_comments (line 1973) | def test_strips_line_comments(self):
    method test_strips_block_comments (line 1977) | def test_strips_block_comments(self):
    method test_picks_one_option_per_group (line 1981) | def test_picks_one_option_per_group(self):
    method test_handles_empty_alternatives (line 1987) | def test_handles_empty_alternatives(self):
    method test_handles_nested_groups (line 1998) | def test_handles_nested_groups(self):
    method test_escapes_preserve_literal_characters (line 2006) | def test_escapes_preserve_literal_characters(self):
    method test_unterminated_group_degrades_gracefully (line 2015) | def test_unterminated_group_degrades_gracefully(self):
    method test_multiple_groups_in_one_string (line 2020) | def test_multiple_groups_in_one_string(self):
    method test_clip_text_encode_resolves_groups (line 2053) | def test_clip_text_encode_resolves_groups(self):
    method test_widget_without_dynamic_prompts_flag_left_alone (line 2078) | def test_widget_without_dynamic_prompts_flag_left_alone(self):
    method test_non_string_value_passes_through_unchanged (line 2096) | def test_non_string_value_passes_through_unchanged(self):
    method test_random_choice_is_deterministic_under_seed (line 2121) | def test_random_choice_is_deterministic_under_seed(self):
  class TestFixtureParity (line 2132) | class TestFixtureParity:
    method test_sd15_workflow_matches_reference (line 2140) | def test_sd15_workflow_matches_reference(self):
  class TestSubgraphExpansion (line 2147) | class TestSubgraphExpansion:
    method test_simple_subgraph_expansion (line 2148) | def test_simple_subgraph_expansion(self, object_info):

FILE: tests/comfy_cli/test_workspace_manager.py
  class TestPathsMatch (line 15) | class TestPathsMatch:
    method test_identical_paths (line 16) | def test_identical_paths(self, tmp_path):
    method test_symlink_to_same_dir (line 21) | def test_symlink_to_same_dir(self, tmp_path):
    method test_different_paths (line 28) | def test_different_paths(self, tmp_path):
    method test_nonexistent_paths_same (line 35) | def test_nonexistent_paths_same(self):
    method test_nonexistent_paths_different (line 38) | def test_nonexistent_paths_different(self):
    method test_trailing_slash (line 41) | def test_trailing_slash(self, tmp_path):
    method test_dot_components (line 46) | def test_dot_components(self, tmp_path):
    method test_parent_component (line 51) | def test_parent_component(self, tmp_path):
    method test_one_exists_one_not (line 58) | def test_one_exists_one_not(self, tmp_path):
    method test_double_symlink (line 65) | def test_double_symlink(self, tmp_path):
  function _create_comfyui_markers (line 80) | def _create_comfyui_markers(path, markers=None):
  class TestHasComfyuiMarkers (line 92) | class TestHasComfyuiMarkers:
    method test_all_five_markers (line 93) | def test_all_five_markers(self, tmp_path):
    method test_any_four_of_five_sufficient (line 98) | def test_any_four_of_five_sufficient(self, tmp_path, omit):
    method test_three_markers_insufficient (line 111) | def test_three_markers_insufficient(self, tmp_path, present):
    method test_empty_directory (line 115) | def test_empty_directory(self, tmp_path):
    method test_nonexistent_path (line 118) | def test_nonexistent_path(self):
  function _make_manager (line 122) | def _make_manager(*, use_here=None, specified_workspace=None, use_recent...
  function _mock_config (line 134) | def _mock_config(mgr, default_workspace=None, recent_workspace=None):
  class TestFindComfyuiRoot (line 152) | class TestFindComfyuiRoot:
    method test_markers_at_given_path (line 155) | def test_markers_at_given_path(self, tmp_path):
    method test_walks_up_to_parent_with_markers (line 159) | def test_walks_up_to_parent_with_markers(self, tmp_path):
    method test_walks_up_multiple_levels (line 165) | def test_walks_up_multiple_levels(self, tmp_path):
    method test_no_markers_anywhere (line 171) | def test_no_markers_anywhere(self, tmp_path):
    method test_returns_nearest_root (line 176) | def test_returns_nearest_root(self, tmp_path):
  class TestCheckComfyRepoFallback (line 188) | class TestCheckComfyRepoFallback:
    method test_nonexistent_path (line 191) | def test_nonexistent_path(self):
    method test_non_git_dir_with_all_markers (line 196) | def test_non_git_dir_with_all_markers(self, tmp_path):
    method test_non_git_dir_with_four_markers (line 202) | def test_non_git_dir_with_four_markers(self, tmp_path):
    method test_non_git_dir_insufficient_markers (line 208) | def test_non_git_dir_insufficient_markers(self, tmp_path):
    method test_non_git_empty_dir (line 214) | def test_non_git_empty_dir(self, tmp_path):
    method test_returned_path_is_absolute (line 219) | def test_returned_path_is_absolute(self, tmp_path):
    method test_subdirectory_walks_up_to_root (line 230) | def test_subdirectory_walks_up_to_root(self, tmp_path):
    method test_fork_repo_with_markers_detected (line 239) | def test_fork_repo_with_markers_detected(self, tmp_path):
    method test_fork_repo_without_markers_not_detected (line 255) | def test_fork_repo_without_markers_not_detected(self, tmp_path):
  class TestStep1Workspace (line 270) | class TestStep1Workspace:
    method test_workspace_flag_takes_priority (line 271) | def test_workspace_flag_takes_priority(self):
    method test_workspace_overrides_cwd_matching_default (line 280) | def test_workspace_overrides_cwd_matching_default(self, mock_getcwd, m...
  class TestStep3Here (line 293) | class TestStep3Here:
    method test_here_flag_forces_current_dir_even_if_matches_default (line 296) | def test_here_flag_forces_current_dir_even_if_matches_default(self, mo...
    method test_here_flag_non_comfy_dir_appends_comfyui (line 310) | def test_here_flag_non_comfy_dir_appends_comfyui(self, mock_getcwd, mo...
  class TestStep4AutoDetect (line 323) | class TestStep4AutoDetect:
    method test_cwd_matches_default_returns_default_type (line 326) | def test_cwd_matches_default_returns_default_type(self, mock_getcwd, m...
    method test_cwd_different_repo_returns_current_dir (line 342) | def test_cwd_different_repo_returns_current_dir(self, mock_getcwd, moc...
    method test_cwd_repo_no_default_configured (line 358) | def test_cwd_repo_no_default_configured(self, mock_getcwd, mock_check):
    method test_cwd_repo_empty_default_returns_current_dir (line 372) | def test_cwd_repo_empty_default_returns_current_dir(self, mock_getcwd,...
    method test_paths_match_called_with_correct_args (line 385) | def test_paths_match_called_with_correct_args(self, mock_getcwd, mock_...
  class TestNoHereSkipsStep4 (line 399) | class TestNoHereSkipsStep4:
    method test_no_here_skips_cwd_detection (line 402) | def test_no_here_skips_cwd_detection(self, mock_getcwd, mock_check):
  class TestStep5ConfiguredDefault (line 418) | class TestStep5ConfiguredDefault:
    method test_not_comfy_repo_falls_through_to_default (line 421) | def test_not_comfy_repo_falls_through_to_default(self, mock_getcwd, mo...
  class TestStep6RecentFallback (line 436) | class TestStep6RecentFallback:
    method test_no_default_falls_to_recent (line 439) | def test_no_default_falls_to_recent(self, mock_getcwd, mock_check):
  class TestStep7FallbackDefault (line 458) | class TestStep7FallbackDefault:
    method test_all_fallbacks_exhausted (line 462) | def test_all_fallbacks_exhausted(self, _cwd, _check, mock_fallback):
  class TestFullIntegration (line 473) | class TestFullIntegration:
    method _create_comfy_repo (line 479) | def _create_comfy_repo(path):
    method test_cwd_is_default_workspace_real_repo (line 493) | def test_cwd_is_default_workspace_real_repo(self, tmp_path):
    method test_cwd_is_default_workspace_via_symlink (line 507) | def test_cwd_is_default_workspace_via_symlink(self, tmp_path):
    method test_cwd_is_subdir_of_default_workspace (line 522) | def test_cwd_is_subdir_of_default_workspace(self, tmp_path):
    method test_two_repos_cwd_in_non_default (line 538) | def test_two_repos_cwd_in_non_default(self, tmp_path):
    method test_default_workspace_trailing_slash (line 554) | def test_default_workspace_trailing_slash(self, tmp_path):
    method test_here_flag_overrides_even_with_real_repo (line 567) | def test_here_flag_overrides_even_with_real_repo(self, tmp_path):
    method test_not_in_any_repo_falls_to_configured_default (line 580) | def test_not_in_any_repo_falls_to_configured_default(self, tmp_path):
    method test_non_git_comfyui_detected_as_cwd (line 596) | def test_non_git_comfyui_detected_as_cwd(self, tmp_path):
    method test_non_git_comfyui_as_configured_default (line 611) | def test_non_git_comfyui_as_configured_default(self, tmp_path):
    method test_non_git_comfyui_from_subdirectory (line 628) | def test_non_git_comfyui_from_subdirectory(self, tmp_path):
    method test_fork_repo_detected_as_comfyui (line 645) | def test_fork_repo_detected_as_comfyui(self, tmp_path):

FILE: tests/e2e/test_e2e.py
  function e2e_test (line 11) | def e2e_test(func):
  function exec (line 18) | def exec(cmd: str, **kwargs) -> subprocess.CompletedProcess[str]:
  function workspace (line 36) | def workspace():
  function comfy_cli (line 72) | def comfy_cli(workspace):
  function test_model (line 78) | def test_model(comfy_cli):
  function test_node (line 105) | def test_node(comfy_cli, workspace):
  function test_manager_installed (line 175) | def test_manager_installed(comfy_cli, workspace):
  function test_node_uv_compile (line 196) | def test_node_uv_compile(comfy_cli):
  function test_uv_compile_default_config (line 216) | def test_uv_compile_default_config(comfy_cli):
  function test_install_version_latest_no_github_api (line 247) | def test_install_version_latest_no_github_api(tmp_path):
  function test_run (line 304) | def test_run(comfy_cli):

FILE: tests/e2e/test_e2e_uv_compile.py
  function _e2e_enabled (line 39) | def _e2e_enabled():
  function exec (line 48) | def exec(cmd: str, timeout: int = 600, **kwargs) -> subprocess.Completed...
  function _rmtree_retry (line 75) | def _rmtree_retry(path, retries=5, delay=2.0):
  function workspace (line 108) | def workspace():
  function comfy_cli (line 166) | def comfy_cli(workspace):
  function _clean_test_packs (line 171) | def _clean_test_packs(workspace):
  function test_real_packs_sequential_no_conflict (line 194) | def test_real_packs_sequential_no_conflict(comfy_cli):
  function test_real_packs_simultaneous_no_conflict (line 210) | def test_real_packs_simultaneous_no_conflict(comfy_cli):
  function test_progressive_conflict (line 225) | def test_progressive_conflict(comfy_cli):
  function test_node_reinstall_uv_compile (line 252) | def test_node_reinstall_uv_compile(comfy_cli):
  function test_node_update_uv_compile (line 264) | def test_node_update_uv_compile(comfy_cli):
  function test_node_fix_uv_compile (line 276) | def test_node_fix_uv_compile(comfy_cli):
  function test_node_restore_deps_uv_compile (line 288) | def test_node_restore_deps_uv_compile(comfy_cli):
  function test_node_uv_sync_standalone (line 305) | def test_node_uv_sync_standalone(comfy_cli):
  function test_node_uv_sync_standalone_conflict (line 317) | def test_node_uv_sync_standalone_conflict(comfy_cli):
  function test_uv_compile_config_default (line 337) | def test_uv_compile_config_default(comfy_cli):
  function test_no_uv_compile_overrides_config (line 352) | def test_no_uv_compile_overrides_config(comfy_cli):
  function test_uv_compile_mutual_exclusivity (line 372) | def test_uv_compile_mutual_exclusivity(comfy_cli):

FILE: tests/test_file_utils_network.py
  function test_guess_status_code_reason_401_with_json (line 22) | def test_guess_status_code_reason_401_with_json():
  function test_guess_status_code_reason_401_without_json (line 29) | def test_guess_status_code_reason_401_without_json():
  function test_guess_status_code_reason_403 (line 35) | def test_guess_status_code_reason_403():
  function test_guess_status_code_reason_404 (line 40) | def test_guess_status_code_reason_404():
  function test_guess_status_code_reason_unknown (line 45) | def test_guess_status_code_reason_unknown():
  function test_check_unauthorized_true (line 51) | def test_check_unauthorized_true(mock_get):
  function test_check_unauthorized_false (line 60) | def test_check_unauthorized_false(mock_get):
  function test_check_unauthorized_exception (line 69) | def test_check_unauthorized_exception(mock_get):
  function test_download_file_success (line 76) | def test_download_file_success(mock_stream, tmp_path):
  function test_download_file_success_without_content_length (line 93) | def test_download_file_success_without_content_length(mock_stream, tmp_p...
  function test_download_file_failure (line 111) | def test_download_file_failure(mock_stream):
  function test_upload_file_success (line 126) | def test_upload_file_success(mock_put, tmp_path):
  function test_upload_file_failure (line 140) | def test_upload_file_failure(mock_put, tmp_path):
  function test_extract_package_as_zip (line 155) | def test_extract_package_as_zip(tmp_path):
  function _make_ok_response (line 171) | def _make_ok_response(content=b"data", content_length=None):
  function _make_failing_iter (line 184) | def _make_failing_iter(data=b"partial", exc=None):
  function _make_status_response (line 196) | def _make_status_response(status_code, body=b""):
  class TestCleanupPartial (line 206) | class TestCleanupPartial:
    method test_removes_existing_file (line 207) | def test_removes_existing_file(self, tmp_path):
    method test_noop_when_file_missing (line 213) | def test_noop_when_file_missing(self, tmp_path):
  class TestFriendlyNetworkError (line 219) | class TestFriendlyNetworkError:
    method test_read_timeout (line 220) | def test_read_timeout(self):
    method test_connect_timeout (line 224) | def test_connect_timeout(self):
    method test_generic_timeout (line 228) | def test_generic_timeout(self):
    method test_network_error (line 232) | def test_network_error(self):
    method test_protocol_error (line 236) | def test_protocol_error(self):
    method test_proxy_error (line 241) | def test_proxy_error(self):
    method test_other_exception (line 246) | def test_other_exception(self):
    method test_transient_http_status_known_code_includes_phrase (line 250) | def test_transient_http_status_known_code_includes_phrase(self):
    method test_transient_http_status_500_includes_phrase (line 256) | def test_transient_http_status_500_includes_phrase(self):
    method test_transient_http_status_unknown_code_falls_back (line 261) | def test_transient_http_status_unknown_code_falls_back(self):
    method test_invalid_url (line 267) | def test_invalid_url(self):
  class TestDownloadTimeout (line 273) | class TestDownloadTimeout:
    method test_uses_generous_timeout (line 275) | def test_uses_generous_timeout(self, mock_stream, tmp_path):
  class TestDownloadRetry (line 287) | class TestDownloadRetry:
    method test_succeeds_after_transient_timeout (line 290) | def test_succeeds_after_transient_timeout(self, mock_stream, mock_slee...
    method test_succeeds_after_network_error (line 306) | def test_succeeds_after_network_error(self, mock_stream, mock_sleep, t...
    method test_succeeds_after_protocol_error (line 322) | def test_succeeds_after_protocol_error(self, mock_stream, mock_sleep, ...
    method test_succeeds_after_proxy_error (line 337) | def test_succeeds_after_proxy_error(self, mock_stream, mock_sleep, tmp...
    method test_all_retries_exhausted_read_timeout (line 352) | def test_all_retries_exhausted_read_timeout(self, mock_stream, mock_sl...
    method test_all_retries_exhausted_connect_error (line 367) | def test_all_retries_exhausted_connect_error(self, mock_stream, mock_s...
    method test_http_error_not_retried (line 380) | def test_http_error_not_retried(self, mock_stream, mock_sleep, tmp_path):
    method test_backoff_increases_with_attempts (line 397) | def test_backoff_increases_with_attempts(self, mock_stream, mock_sleep...
    method test_original_exception_chained (line 411) | def test_original_exception_chained(self, mock_stream, mock_sleep, tmp...
  class TestDownloadPartialCleanup (line 421) | class TestDownloadPartialCleanup:
    method test_partial_file_removed_after_midstream_timeout (line 424) | def test_partial_file_removed_after_midstream_timeout(self, mock_strea...
    method test_partial_file_removed_between_retries (line 442) | def test_partial_file_removed_between_retries(self, mock_stream, mock_...
    method test_preexisting_file_preserved_on_http_error (line 465) | def test_preexisting_file_preserved_on_http_error(self, mock_stream, m...
    method test_preexisting_file_preserved_on_connect_error (line 489) | def test_preexisting_file_preserved_on_connect_error(self, mock_stream...
    method test_preexisting_file_preserved_on_interrupt_before_open (line 509) | def test_preexisting_file_preserved_on_interrupt_before_open(self, moc...
    method test_keyboard_interrupt_cleans_up_when_user_confirms (line 528) | def test_keyboard_interrupt_cleans_up_when_user_confirms(self, mock_st...
    method test_keyboard_interrupt_keeps_partial_when_user_declines (line 547) | def test_keyboard_interrupt_keeps_partial_when_user_declines(self, moc...
  class TestDownloadHTTPStatusRetry (line 566) | class TestDownloadHTTPStatusRetry:
    method test_500_retried_and_succeeds (line 571) | def test_500_retried_and_succeeds(self, mock_stream, mock_sleep, tmp_p...
    method test_502_retried (line 587) | def test_502_retried(self, mock_stream, mock_sleep, tmp_path):
    method test_503_retried (line 599) | def test_503_retried(self, mock_stream, mock_sleep, tmp_path):
    method test_504_retried (line 611) | def test_504_retried(self, mock_stream, mock_sleep, tmp_path):
    method test_429_retried (line 623) | def test_429_retried(self, mock_stream, mock_sleep, tmp_path):
    method test_408_retried (line 635) | def test_408_retried(self, mock_stream, mock_sleep, tmp_path):
    method test_all_retries_exhausted_on_500 (line 647) | def test_all_retries_exhausted_on_500(self, mock_stream, mock_sleep, t...
    method test_retry_body_read_timeout_still_retries (line 669) | def test_retry_body_read_timeout_still_retries(self, mock_stream, mock...
    method test_mixed_transient_errors_eventually_succeed (line 687) | def test_mixed_transient_errors_eventually_succeed(self, mock_stream, ...
    method test_404_not_retried (line 703) | def test_404_not_retried(self, mock_stream, mock_sleep, tmp_path):
    method test_401_not_retried (line 715) | def test_401_not_retried(self, mock_stream, mock_sleep, tmp_path):
    method test_403_not_retried (line 727) | def test_403_not_retried(self, mock_stream, mock_sleep, tmp_path):
    method test_preexisting_file_preserved_on_http_status_retry_exhaust (line 739) | def test_preexisting_file_preserved_on_http_status_retry_exhaust(self,...
  class TestDownloadNonRetriableHTTPError (line 760) | class TestDownloadNonRetriableHTTPError:
    method test_unsupported_protocol_wrapped (line 767) | def test_unsupported_protocol_wrapped(self, mock_stream, mock_sleep, t...
    method test_too_many_redirects_wrapped (line 779) | def test_too_many_redirects_wrapped(self, mock_stream, mock_sleep, tmp...
    method test_decoding_error_wrapped (line 791) | def test_decoding_error_wrapped(self, mock_stream, mock_sleep, tmp_path):
    method test_invalid_url_wrapped (line 803) | def test_invalid_url_wrapped(self, mock_stream, mock_sleep, tmp_path):
    method test_invalid_url_preserves_preexisting_file (line 817) | def test_invalid_url_preserves_preexisting_file(self, mock_stream, tmp...
    method test_preexisting_file_preserved_on_non_retriable_error (line 832) | def test_preexisting_file_preserved_on_non_retriable_error(self, mock_...
    method test_partial_file_cleaned_up_on_mid_stream_non_retriable (line 847) | def test_partial_file_cleaned_up_on_mid_stream_non_retriable(self, moc...

FILE: tests/uv/test_torch_backend_compile.py
  function _setup_temp (line 35) | def _setup_temp():
  function _compile_for (line 41) | def _compile_for(gpu):
  function test_compile_nvidia (line 55) | def test_compile_nvidia():
  function test_compile_amd (line 63) | def test_compile_amd():
  function test_compile_cpu (line 71) | def test_compile_cpu():
  function test_compile_nvidia_cu130_preserves_cuda_runtime (line 81) | def test_compile_nvidia_cu130_preserves_cuda_runtime():
  function test_compile_mac (line 116) | def test_compile_mac():

FILE: tests/uv/test_uv.py
  function mock_prompt_select (line 24) | def mock_prompt_select(monkeypatch):
  function test_find_req_files (line 33) | def test_find_req_files():
  function test_compile (line 54) | def test_compile(mock_prompt_select):
  function test_torch_backend_nvidia (line 84) | def test_torch_backend_nvidia():
  function test_torch_backend_amd (line 90) | def test_torch_backend_amd():
  function test_torch_backend_cpu (line 96) | def test_torch_backend_cpu():
  function test_torch_backend_none (line 102) | def test_torch_backend_none():
  function test_compile_passes_torch_backend (line 109) | def test_compile_passes_torch_backend():
  function test_compile_omits_torch_backend_when_none (line 123) | def test_compile_omits_torch_backend_when_none():
  function test_compiled_output_has_no_extra_index_url (line 136) | def test_compiled_output_has_no_extra_index_url(mock_prompt_select):
  function test_override_file_has_no_extra_index_url (line 152) | def test_override_file_has_no_extra_index_url():
  function test_make_override_does_not_strip_cuda_toolkit_extras (line 167) | def test_make_override_does_not_strip_cuda_toolkit_extras():
  function test_nvidia_custom_cuda_version (line 236) | def test_nvidia_custom_cuda_version():
  function test_nvidia_cuda_13 (line 244) | def test_nvidia_cuda_13():
  function test_amd_custom_rocm_version (line 252) | def test_amd_custom_rocm_version():
  function test_nvidia_auto_detected_tag (line 260) | def test_nvidia_auto_detected_tag():
  function test_nvidia_no_cuda_version_uses_default (line 268) | def test_nvidia_no_cuda_version_uses_default():
  function test_skip_torch_disables_gpu_url_and_backend (line 277) | def test_skip_torch_disables_gpu_url_and_backend(gpu):
  function test_skip_torch_override_has_no_torch (line 283) | def test_skip_torch_override_has_no_torch():
  function test_skip_torch_install_deps_no_extra_index_url (line 297) | def test_skip_torch_install_deps_no_extra_index_url():
  function test_check_call_prints_nfs_hint_on_uv_install_failure (line 308) | def test_check_call_prints_nfs_hint_on_uv_install_failure(capsys):
  function test_check_call_prints_nfs_hint_on_uv_sync_failure (line 321) | def test_check_call_prints_nfs_hint_on_uv_sync_failure(capsys):
  function test_check_call_no_hint_for_non_uv_failure (line 332) | def test_check_call_no_hint_for_non_uv_failure(capsys):
  function test_check_call_no_hint_on_uv_compile_failure (line 343) | def test_check_call_no_hint_on_uv_compile_failure(capsys):
  function test_check_call_no_hint_for_pip_install_uv (line 354) | def test_check_call_no_hint_for_pip_install_uv(capsys):
  function test_parse_req_file_strips_inline_comments (line 370) | def test_parse_req_file_strips_inline_comments(tmp_path):
  function test_parse_req_file_strips_inline_comment_with_single_space (line 376) | def test_parse_req_file_strips_inline_comment_with_single_space(tmp_path):
  function test_parse_req_file_skips_full_line_comments (line 382) | def test_parse_req_file_skips_full_line_comments(tmp_path):
  function test_parse_req_file_preserves_vcs_subdirectory_fragment (line 388) | def test_parse_req_file_preserves_vcs_subdirectory_fragment(tmp_path):
  function test_parse_req_file_preserves_vcs_egg_fragment (line 396) | def test_parse_req_file_preserves_vcs_egg_fragment(tmp_path):
  function test_parse_req_file_preserves_direct_url_hash (line 402) | def test_parse_req_file_preserves_direct_url_hash(tmp_path):
  function test_parse_req_file_vcs_with_inline_comment_strips_only_comment (line 408) | def test_parse_req_file_vcs_with_inline_comment_strips_only_comment(tmp_...
  function test_parse_req_file_preserves_double_dash_options (line 417) | def test_parse_req_file_preserves_double_dash_options(tmp_path):
  function test_parse_req_file_handles_crlf_line_endings (line 423) | def test_parse_req_file_handles_crlf_line_endings(tmp_path):
Condensed preview — 145 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,555K chars).
[
  {
    "path": ".coveragerc",
    "chars": 348,
    "preview": "[run]\nsource = comfy_cli\nomit = tests/*\n\n[report]\nexclude_lines =\n    pragma: no cover\n    def __repr__\n    if self.debu"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 447,
    "preview": "---\nname: Bug report\nabout: Create a bug report to help us improve.\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 450,
    "preview": "---\nname: Feature request\nabout: Submit a feature request for this repo.\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n--"
  },
  {
    "path": ".github/codecov.yml",
    "chars": 121,
    "preview": "comment:\n  layout: \"diff, files\"\n\ncoverage:\n  status:\n    project:\n      default:\n        threshold: 0.1%\n    patch: off"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 237,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/.github/workflows\"\n    schedule:\n      inte"
  },
  {
    "path": ".github/workflows/build-and-test.yml",
    "chars": 1497,
    "preview": "name: \"Test CLI Tool on Multiple Platforms\"\non:\n  push:\n    branches:\n      - main\n    paths:\n      - \"comfy_cli/**\"\n   "
  },
  {
    "path": ".github/workflows/publish_package.yml",
    "chars": 3062,
    "preview": "name: Publish to PyPI\n\non:\n  release:\n    types: [ created ]\n\npermissions:\n  contents: read\n\njobs:\n  build-n-publish-pyp"
  },
  {
    "path": ".github/workflows/pytest.yml",
    "chars": 980,
    "preview": "name: Run pytest\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\npermissions:\n  cont"
  },
  {
    "path": ".github/workflows/ruff_check.yml",
    "chars": 573,
    "preview": "name: ruff_check\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\npermissions:\n  cont"
  },
  {
    "path": ".github/workflows/run-on-gpu.yml",
    "chars": 1771,
    "preview": "name: \"Test CLI Tool on GPU runners\"\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - \"comfy_cli/**\"\n      - \""
  },
  {
    "path": ".github/workflows/test-mac.yml",
    "chars": 754,
    "preview": "name: \"Mac Specific Commands\"\non:\n  pull_request:\n    branches:\n      - main\n    paths:\n      - comfy_cli/**\n\npermission"
  },
  {
    "path": ".github/workflows/test-windows.yml",
    "chars": 839,
    "preview": "name: \"Windows Specific Commands\"\non:\n  pull_request:\n    branches:\n      - main\n    paths:\n      - comfy_cli/**\n\npermis"
  },
  {
    "path": ".gitignore",
    "chars": 823,
    "preview": "__pycache__/\n*.py[cod]\n\n#COMMON CONFIGs\n.DS_Store\n.src_port\n.webpack_watch.log\n*.swp\n*.swo\n.vscode/settings.json\n.idea/\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 947,
    "preview": "repos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v5.0.0\n    hooks:\n    -   id: check-yaml\n      "
  },
  {
    "path": ".pylintrc",
    "chars": 52,
    "preview": "# .pylintrc or pylintrc\n\n[MAIN]\nmax-line-length=120\n"
  },
  {
    "path": "DEV_README.md",
    "chars": 4988,
    "preview": "# Development Guide\n\nThis guide provides an overview of how to develop in this repository.\n\n## General guide\n\n1. Clone t"
  },
  {
    "path": "LICENSE",
    "chars": 35823,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\r\n                       Version 3, 29 June 2007\r\n\r\n Copyright (C) 2007 Fr"
  },
  {
    "path": "README.md",
    "chars": 19258,
    "preview": "# comfy-cli: A Command Line Tool for ComfyUI\n\n[![Run pytest](https://github.com/Comfy-Org/comfy-cli/actions/workflows/py"
  },
  {
    "path": "comfy_cli/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "comfy_cli/__main__.py",
    "chars": 93,
    "preview": "from comfy_cli.cmdline import main\n\nif __name__ == \"__main__\":  # pragma: nocover\n    main()\n"
  },
  {
    "path": "comfy_cli/cmdline.py",
    "chars": 23770,
    "preview": "import os\nimport subprocess\nimport sys\nimport webbrowser\nfrom typing import Annotated\n\nimport questionary\nimport typer\nf"
  },
  {
    "path": "comfy_cli/command/__init__.py",
    "chars": 75,
    "preview": "from . import custom_nodes, install\n\n__all__ = [\"custom_nodes\", \"install\"]\n"
  },
  {
    "path": "comfy_cli/command/code_search.py",
    "chars": 6471,
    "preview": "\"\"\"CLI commands for searching code across ComfyUI repositories.\"\"\"\n\nimport json\nimport re\nimport sys\nfrom typing import "
  },
  {
    "path": "comfy_cli/command/custom_nodes/__init__.py",
    "chars": 72,
    "preview": "from .command import app, manager_app\n\n__all__ = [\"app\", \"manager_app\"]\n"
  },
  {
    "path": "comfy_cli/command/custom_nodes/bisect_custom_nodes.py",
    "chars": 7162,
    "preview": "from __future__ import annotations\n\nimport json\nimport os\nfrom pathlib import Path\nfrom typing import Annotated, Literal"
  },
  {
    "path": "comfy_cli/command/custom_nodes/cm_cli_util.py",
    "chars": 6484,
    "preview": "from __future__ import annotations\n\nimport importlib.util\nimport os\nimport subprocess\nimport sys\nimport threading\nimport"
  },
  {
    "path": "comfy_cli/command/custom_nodes/command.py",
    "chars": 42127,
    "preview": "import os\nimport pathlib\nimport platform\nimport shutil\nimport subprocess\nimport sys\nimport uuid\nfrom enum import Enum\nfr"
  },
  {
    "path": "comfy_cli/command/generate/__init__.py",
    "chars": 86,
    "preview": "from comfy_cli.command.generate.app import register_with\n\n__all__ = [\"register_with\"]\n"
  },
  {
    "path": "comfy_cli/command/generate/adapters.py",
    "chars": 9718,
    "preview": "\"\"\"Per-endpoint adapters for partners whose request/response shapes don't fit\nthe generic schema-driven flag→JSON mold.\n"
  },
  {
    "path": "comfy_cli/command/generate/app.py",
    "chars": 18756,
    "preview": "\"\"\"``comfy generate`` — call ComfyUI partner nodes from the CLI.\n\nUX shape, modeled on fal-ai's genmedia but creative-us"
  },
  {
    "path": "comfy_cli/command/generate/client.py",
    "chars": 6233,
    "preview": "\"\"\"HTTP client for the Comfy cloud API.\n\nA thin wrapper around httpx that:\n- attaches ``Authorization: Bearer $COMFY_API"
  },
  {
    "path": "comfy_cli/command/generate/output.py",
    "chars": 4621,
    "preview": "\"\"\"Output handling: --download templating, URL printing, binary response writes.\n\nTemplating tokens: ``{request_id}``, `"
  },
  {
    "path": "comfy_cli/command/generate/poll.py",
    "chars": 13451,
    "preview": "\"\"\"Async-job polling for partner endpoints.\n\nThere are two flavors:\n\n1. **BFL** — the server returns ``{id, polling_url}"
  },
  {
    "path": "comfy_cli/command/generate/schema.py",
    "chars": 11074,
    "preview": "\"\"\"Convert an openapi requestBody schema into CLI flag definitions, and parse\nuser-supplied argv against those flags.\n\nT"
  },
  {
    "path": "comfy_cli/command/generate/spec/openapi.yml",
    "chars": 1043913,
    "preview": "openapi: \"3.0.2\"\ninfo:\n  title: Comfy API\n  version: \"1.0\"\nservers:\n  - url: https://api.comfy.org\npaths:\n  /users:\n    "
  },
  {
    "path": "comfy_cli/command/generate/spec.py",
    "chars": 16931,
    "preview": "\"\"\"Load the bundled openapi.yml and expose the curated image-endpoint registry.\n\nLookup order on disk:\n1. ``~/.comfy/ope"
  },
  {
    "path": "comfy_cli/command/generate/upload.py",
    "chars": 5311,
    "preview": "\"\"\"Upload reference assets via ``/customers/storage``.\n\nThe cloud endpoint issues short-lived signed URLs:\n\n1. POST ``/c"
  },
  {
    "path": "comfy_cli/command/github/pr_info.py",
    "chars": 313,
    "preview": "from typing import NamedTuple\n\n\nclass PRInfo(NamedTuple):\n    number: int\n    head_repo_url: str\n    head_branch: str\n  "
  },
  {
    "path": "comfy_cli/command/install.py",
    "chars": 39895,
    "preview": "import os\nimport platform\nimport re\nimport subprocess\nimport sys\nfrom typing import TypedDict\nfrom urllib.parse import u"
  },
  {
    "path": "comfy_cli/command/launch.py",
    "chars": 10324,
    "preview": "from __future__ import annotations\n\nimport asyncio\nimport os\nimport subprocess\nimport sys\nimport threading\nimport uuid\n\n"
  },
  {
    "path": "comfy_cli/command/models/models.py",
    "chars": 18026,
    "preview": "import contextlib\nimport os\nimport pathlib\nimport time\nfrom typing import Annotated\nfrom urllib.parse import parse_qs, u"
  },
  {
    "path": "comfy_cli/command/pr_command.py",
    "chars": 2798,
    "preview": "\"\"\"PR cache management commands.\n\nThis module provides CLI commands for managing the PR cache, including:\n- Listing cach"
  },
  {
    "path": "comfy_cli/command/run.py",
    "chars": 13519,
    "preview": "import json\nimport os\nimport sys\nimport time\nimport urllib.error\nimport urllib.parse\nimport uuid\nfrom datetime import ti"
  },
  {
    "path": "comfy_cli/config_manager.py",
    "chars": 6074,
    "preview": "import configparser\nimport os\nfrom importlib.metadata import version\n\nfrom comfy_cli import constants, logging\nfrom comf"
  },
  {
    "path": "comfy_cli/constants.py",
    "chars": 3653,
    "preview": "import os\nfrom enum import Enum\n\n\nclass OS(str, Enum):\n    WINDOWS = \"windows\"\n    MACOS = \"macos\"\n    LINUX = \"linux\"\n\n"
  },
  {
    "path": "comfy_cli/cuda_detect.py",
    "chars": 3606,
    "preview": "\"\"\"Auto-detect CUDA driver version and resolve the best PyTorch wheel suffix.\"\"\"\n\nfrom __future__ import annotations\n\nim"
  },
  {
    "path": "comfy_cli/env_checker.py",
    "chars": 3890,
    "preview": "\"\"\"\nModule for checking various env and state conditions.\n\"\"\"\n\nimport os\nimport sys\n\nimport requests\nfrom rich.console i"
  },
  {
    "path": "comfy_cli/file_utils.py",
    "chars": 18777,
    "preview": "import json\nimport os\nimport pathlib\nimport subprocess\nimport time\nimport zipfile\nfrom http import HTTPStatus\n\nimport ht"
  },
  {
    "path": "comfy_cli/git_utils.py",
    "chars": 5902,
    "preview": "import os\nimport subprocess\n\nfrom rich.console import Console\nfrom rich.panel import Panel\nfrom rich.text import Text\n\nf"
  },
  {
    "path": "comfy_cli/logging.py",
    "chars": 1121,
    "preview": "\"\"\"\nThis module provides logging utilities for the CLI.\n\nNote: we could potentially change the logging library or the wa"
  },
  {
    "path": "comfy_cli/pr_cache.py",
    "chars": 9289,
    "preview": "\"\"\"PR Cache Management for temporary PR testing.\n\nThis module provides functionality for caching built frontend PRs to e"
  },
  {
    "path": "comfy_cli/registry/__init__.py",
    "chars": 381,
    "preview": "from .api import RegistryAPI\nfrom .config_parser import extract_node_configuration, initialize_project_config\nfrom .type"
  },
  {
    "path": "comfy_cli/registry/api.py",
    "chars": 7051,
    "preview": "import json\nimport logging\nimport os\n\nimport requests\n\n# Reduced global imports from comfy_cli.registry\nfrom comfy_cli.r"
  },
  {
    "path": "comfy_cli/registry/config_parser.py",
    "chars": 23587,
    "preview": "import os\nimport pathlib\nimport re\nimport subprocess\nfrom urllib.parse import urlparse, urlunparse\n\nimport tomlkit\nimpor"
  },
  {
    "path": "comfy_cli/registry/types.py",
    "chars": 1776,
    "preview": "from dataclasses import dataclass, field\n\n\n@dataclass\nclass NodeVersion:\n    changelog: str\n    dependencies: list[str]\n"
  },
  {
    "path": "comfy_cli/resolve_python.py",
    "chars": 2769,
    "preview": "from __future__ import annotations\n\nimport os\nimport platform\nimport subprocess\nimport sys\nimport sysconfig\n\nfrom rich i"
  },
  {
    "path": "comfy_cli/standalone.py",
    "chars": 7462,
    "preview": "import logging\nimport re\nimport shutil\nimport subprocess\nfrom pathlib import Path\n\nimport requests\n\nfrom comfy_cli.const"
  },
  {
    "path": "comfy_cli/tracking.py",
    "chars": 4613,
    "preview": "import functools\nimport logging as logginglib\nimport uuid\n\nimport typer\nfrom mixpanel import Mixpanel\n\nfrom comfy_cli im"
  },
  {
    "path": "comfy_cli/typing.py",
    "chars": 45,
    "preview": "import os\n\nPathLike = os.PathLike[str] | str\n"
  },
  {
    "path": "comfy_cli/ui.py",
    "chars": 6197,
    "preview": "from enum import Enum\nfrom typing import Any, TypeVar\n\nimport questionary\nimport typer\nfrom questionary import Choice\nfr"
  },
  {
    "path": "comfy_cli/update.py",
    "chars": 1979,
    "preview": "import logging\nimport sys\nfrom importlib.metadata import metadata\n\nimport requests\nfrom packaging import version\nfrom ri"
  },
  {
    "path": "comfy_cli/utils.py",
    "chars": 6861,
    "preview": "\"\"\"\nModule for utility functions.\n\"\"\"\n\nimport functools\nimport platform\nimport shutil\nimport subprocess\nimport tarfile\nf"
  },
  {
    "path": "comfy_cli/uv.py",
    "chars": 20610,
    "preview": "import re\nimport subprocess\nimport sys\nfrom importlib import metadata\nfrom pathlib import Path\nfrom textwrap import dede"
  },
  {
    "path": "comfy_cli/workflow_to_api.py",
    "chars": 55809,
    "preview": "\"\"\"Convert ComfyUI UI-format workflows to API (\"prompt\") format.\n\nThe UI format is what the ComfyUI frontend saves by de"
  },
  {
    "path": "comfy_cli/workspace_manager.py",
    "chars": 13614,
    "preview": "import concurrent.futures\nimport os\nfrom dataclasses import dataclass, field\nfrom datetime import datetime\nfrom enum imp"
  },
  {
    "path": "conda.listing.txt",
    "chars": 48537,
    "preview": "Loading channels: ...working... done\npython 3.8.11 hbdb9e5c_5\n------------------------\nfile name   : python-3.8.11-hbdb9"
  },
  {
    "path": "docs/DESIGN-uv-compile.md",
    "chars": 8139,
    "preview": "# DESIGN: Unified Dependency Resolution (--uv-compile) Implementation\n\n## Architecture Decision: Pass-Through\n\ncm_cli al"
  },
  {
    "path": "docs/PRD-uv-compile.md",
    "chars": 3295,
    "preview": "# PRD: Unified Dependency Resolution (--uv-compile) Support\n\n## Overview\n\nAdd `--uv-compile` flag to comfy-cli to integr"
  },
  {
    "path": "docs/TESTING-e2e.md",
    "chars": 5955,
    "preview": "# E2E Testing Guide\n\nE2E tests perform real `comfy install`, `comfy launch`, and `comfy node` operations.\nThey are **dis"
  },
  {
    "path": "pylock.toml",
    "chars": 95404,
    "preview": "# This file was autogenerated by uv via the following command:\n#    uv export --output-file=pylock.toml\nlock-version = \""
  },
  {
    "path": "pyproject.toml",
    "chars": 2150,
    "preview": "[build-system]\nbuild-backend = \"setuptools.build_meta\"\n\nrequires = [ \"setuptools>=61\" ]\n\n[project]\nname = \"comfy-cli\"\nve"
  },
  {
    "path": "pyrightconfig.json",
    "chars": 33,
    "preview": "{\n    \"pythonPlatform\": \"All\", \n}"
  },
  {
    "path": "tests/comfy_cli/command/generate/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/comfy_cli/command/generate/test_adapters.py",
    "chars": 11436,
    "preview": "\"\"\"Tests for the per-endpoint adapters: Gemini (nano-banana) and Seedance.\"\"\"\n\nfrom __future__ import annotations\n\nimpor"
  },
  {
    "path": "tests/comfy_cli/command/generate/test_app.py",
    "chars": 25859,
    "preview": "\"\"\"End-to-end tests for ``comfy generate`` via Typer's CliRunner.\n\nThese cover the dispatch table (list/schema/refresh/r"
  },
  {
    "path": "tests/comfy_cli/command/generate/test_client.py",
    "chars": 3412,
    "preview": "\"\"\"Tests for the httpx client wrapper — auth header, payload split.\"\"\"\n\nimport httpx\nimport pytest\n\nfrom comfy_cli.comma"
  },
  {
    "path": "tests/comfy_cli/command/generate/test_output.py",
    "chars": 754,
    "preview": "\"\"\"Tests for download templating.\"\"\"\n\nimport httpx\n\nfrom comfy_cli.command.generate import output\n\n\ndef test_resolve_tem"
  },
  {
    "path": "tests/comfy_cli/command/generate/test_poll.py",
    "chars": 1725,
    "preview": "\"\"\"Tests for the BFL polling adapter.\"\"\"\n\nfrom unittest.mock import patch\n\nimport httpx\n\nfrom comfy_cli.command.generate"
  },
  {
    "path": "tests/comfy_cli/command/generate/test_schema.py",
    "chars": 3307,
    "preview": "\"\"\"Tests for openapi schema → CLI flag conversion and argv parsing.\"\"\"\n\nimport pytest\n\nfrom comfy_cli.command.generate i"
  },
  {
    "path": "tests/comfy_cli/command/generate/test_spec.py",
    "chars": 2117,
    "preview": "\"\"\"Tests for the openapi registry — verify the curated image allowlist resolves\nagainst the vendored spec and classifies"
  },
  {
    "path": "tests/comfy_cli/command/generate/test_upload.py",
    "chars": 5364,
    "preview": "\"\"\"Tests for /customers/storage upload helpers.\"\"\"\n\nimport httpx\nimport pytest\n\nfrom comfy_cli.command.generate import c"
  },
  {
    "path": "tests/comfy_cli/command/generate/test_video_poll.py",
    "chars": 7486,
    "preview": "\"\"\"Tests for the generic config-driven poller and per-partner specs.\"\"\"\n\nimport httpx\nimport pytest\n\nfrom comfy_cli.comm"
  },
  {
    "path": "tests/comfy_cli/command/github/test_pr.py",
    "chars": 46502,
    "preview": "import subprocess\nimport sys\nfrom unittest.mock import Mock, patch\n\nimport pytest\nimport requests\nfrom typer.testing imp"
  },
  {
    "path": "tests/comfy_cli/command/models/test_models.py",
    "chars": 20911,
    "preview": "import pathlib\nfrom unittest.mock import Mock, patch\n\nimport typer.testing\n\nfrom comfy_cli import constants\nfrom comfy_c"
  },
  {
    "path": "tests/comfy_cli/command/nodes/test_bisect_custom_nodes.py",
    "chars": 4067,
    "preview": "import json\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom comfy_cli.command.custom_nodes.bisect_custom_nodes impo"
  },
  {
    "path": "tests/comfy_cli/command/nodes/test_node_init.py",
    "chars": 1204,
    "preview": "import subprocess\n\nimport tomlkit\nfrom typer.testing import CliRunner\n\nfrom comfy_cli.command.custom_nodes.command impor"
  },
  {
    "path": "tests/comfy_cli/command/nodes/test_node_install.py",
    "chars": 16126,
    "preview": "import re\nimport subprocess\nfrom unittest.mock import MagicMock, patch\n\nfrom typer.testing import CliRunner\n\nfrom comfy_"
  },
  {
    "path": "tests/comfy_cli/command/nodes/test_pack.py",
    "chars": 3137,
    "preview": "import subprocess\nimport zipfile\nfrom unittest.mock import patch\n\nfrom typer.testing import CliRunner\n\nfrom comfy_cli.cm"
  },
  {
    "path": "tests/comfy_cli/command/nodes/test_publish.py",
    "chars": 7936,
    "preview": "from unittest.mock import MagicMock, patch\n\nfrom typer.testing import CliRunner\n\nfrom comfy_cli.command.custom_nodes.com"
  },
  {
    "path": "tests/comfy_cli/command/test_bisect_parse.py",
    "chars": 4533,
    "preview": "from comfy_cli.command.custom_nodes.bisect_custom_nodes import parse_cm_output\n\nCM_OUTPUT_REAL = \"\"\"\\\nFETCH ComfyRegistr"
  },
  {
    "path": "tests/comfy_cli/command/test_cm_cli_util.py",
    "chars": 19454,
    "preview": "import subprocess\nimport sys\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\nimport typer\n\nfrom comfy_cli.comm"
  },
  {
    "path": "tests/comfy_cli/command/test_code_search.py",
    "chars": 19897,
    "preview": "\"\"\"Tests for the code-search command.\"\"\"\n\nimport json\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\nimport r"
  },
  {
    "path": "tests/comfy_cli/command/test_command.py",
    "chars": 3801,
    "preview": "import os\nfrom unittest.mock import patch\n\nimport pytest\nfrom typer.testing import CliRunner\n\nfrom comfy_cli.cmdline imp"
  },
  {
    "path": "tests/comfy_cli/command/test_frontend_pr.py",
    "chars": 6354,
    "preview": "from unittest.mock import Mock, patch\n\nimport pytest\nfrom typer.testing import CliRunner\n\nfrom comfy_cli.command.install"
  },
  {
    "path": "tests/comfy_cli/command/test_launch_frontend_pr.py",
    "chars": 9934,
    "preview": "\"\"\"Tests for launch-time frontend PR functionality\"\"\"\n\nimport json\nfrom datetime import datetime, timedelta\nfrom pathlib"
  },
  {
    "path": "tests/comfy_cli/command/test_manager_gui.py",
    "chars": 48541,
    "preview": "from unittest.mock import MagicMock, patch\n\nimport pytest\nimport typer\n\nfrom comfy_cli import constants\nfrom comfy_cli.c"
  },
  {
    "path": "tests/comfy_cli/command/test_npm_help.py",
    "chars": 3074,
    "preview": "\"\"\"Tests for install command functionality\"\"\"\n\nfrom io import StringIO\nfrom unittest.mock import patch\n\nimport pytest\n\nf"
  },
  {
    "path": "tests/comfy_cli/command/test_run.py",
    "chars": 22794,
    "preview": "import io\nimport json\nimport os\nimport tempfile\nimport urllib.error\nfrom unittest.mock import MagicMock, patch\n\nimport p"
  },
  {
    "path": "tests/comfy_cli/conftest.py",
    "chars": 446,
    "preview": "import os\n\nimport pytest\n\n\n@pytest.fixture(autouse=True)\ndef _preserve_cwd():\n    \"\"\"Restore the working directory after"
  },
  {
    "path": "tests/comfy_cli/fixtures/sd15_expected_api.json",
    "chars": 1761,
    "preview": "{\n  \"4\": {\n    \"inputs\": {\n      \"ckpt_name\": \"v1-5-pruned-emaonly-fp16.safetensors\"\n    },\n    \"class_type\": \"Checkpoin"
  },
  {
    "path": "tests/comfy_cli/fixtures/sd15_object_info.json",
    "chars": 11224,
    "preview": "{\n  \"CLIPTextEncode\": {\n    \"input\": {\n      \"required\": {\n        \"text\": [\n          \"STRING\",\n          {\n           "
  },
  {
    "path": "tests/comfy_cli/fixtures/sd15_ui_workflow.json",
    "chars": 11006,
    "preview": "{\n  \"id\": \"2ba0b800-2f13-4f21-b8d6-c6cdb0152cae\",\n  \"revision\": 0,\n  \"last_node_id\": 16,\n  \"last_link_id\": 9,\n  \"nodes\":"
  },
  {
    "path": "tests/comfy_cli/registry/test_api.py",
    "chars": 5541,
    "preview": "import unittest\nfrom unittest.mock import MagicMock, patch\n\nfrom comfy_cli.registry import PyProjectConfig\nfrom comfy_cl"
  },
  {
    "path": "tests/comfy_cli/registry/test_config_parser.py",
    "chars": 43818,
    "preview": "import subprocess\nfrom unittest.mock import mock_open, patch\n\nimport pytest\nimport tomlkit\n\nfrom comfy_cli.registry.conf"
  },
  {
    "path": "tests/comfy_cli/test_aria2_download.py",
    "chars": 13813,
    "preview": "\"\"\"Tests for aria2 RPC download support.\"\"\"\n\nimport sys\nfrom types import ModuleType\nfrom unittest.mock import MagicMock"
  },
  {
    "path": "tests/comfy_cli/test_cm_cli_python_resolution.py",
    "chars": 8396,
    "preview": "import subprocess\nimport sys\nimport textwrap\nimport time\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom"
  },
  {
    "path": "tests/comfy_cli/test_cmdline_python_resolution.py",
    "chars": 2453,
    "preview": "from unittest.mock import MagicMock, patch\n\nfrom comfy_cli import cmdline\n\n\nclass TestUpdateComfy:\n    def test_uses_res"
  },
  {
    "path": "tests/comfy_cli/test_config_manager.py",
    "chars": 6715,
    "preview": "import configparser\nimport os\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom comfy_cli import constants\nfrom comfy"
  },
  {
    "path": "tests/comfy_cli/test_cuda_detect.py",
    "chars": 8592,
    "preview": "import subprocess\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom comfy_cli.cuda_detect import (\n    DEF"
  },
  {
    "path": "tests/comfy_cli/test_cuda_detect_real.py",
    "chars": 3119,
    "preview": "\"\"\"Real-hardware integration tests for CUDA auto-detection.\n\nThese tests call the detection functions without mocks, exe"
  },
  {
    "path": "tests/comfy_cli/test_custom_nodes_python_resolution.py",
    "chars": 6711,
    "preview": "import os\nfrom unittest.mock import patch\n\nfrom comfy_cli.command.custom_nodes import command\n\n\nclass TestGetInstalledPa"
  },
  {
    "path": "tests/comfy_cli/test_env_checker.py",
    "chars": 3992,
    "preview": "import os\nimport sys\nfrom types import SimpleNamespace\nfrom unittest.mock import patch\n\nimport pytest\nimport requests\n\nf"
  },
  {
    "path": "tests/comfy_cli/test_file_utils.py",
    "chars": 2558,
    "preview": "import zipfile\r\n\r\nfrom comfy_cli import file_utils\r\n\r\n\r\ndef test_zip_files_respects_comfyignore(tmp_path, monkeypatch):\r"
  },
  {
    "path": "tests/comfy_cli/test_global_python_install.py",
    "chars": 7785,
    "preview": "\"\"\"Integration tests for the global-Python (Docker / bare-metal) install path.\n\nCovers the scenario where comfy-cli is i"
  },
  {
    "path": "tests/comfy_cli/test_install.py",
    "chars": 2178,
    "preview": "from unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom comfy_cli.command.install import pip_install_manager, va"
  },
  {
    "path": "tests/comfy_cli/test_install_python_resolution.py",
    "chars": 15291,
    "preview": "import sys\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom comfy_cli import constants\nfrom comfy_cli.com"
  },
  {
    "path": "tests/comfy_cli/test_launch_python_resolution.py",
    "chars": 2419,
    "preview": "import subprocess\nimport sys\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom comfy_cli.command import la"
  },
  {
    "path": "tests/comfy_cli/test_models_python_resolution.py",
    "chars": 1919,
    "preview": "import builtins\nfrom unittest.mock import patch\n\nimport typer.testing\n\nfrom comfy_cli.command.models.models import app\n\n"
  },
  {
    "path": "tests/comfy_cli/test_resolve_python.py",
    "chars": 15888,
    "preview": "import os\nimport subprocess\nimport sys\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom comfy_cli.resolve_python imp"
  },
  {
    "path": "tests/comfy_cli/test_standalone.py",
    "chars": 7215,
    "preview": "import os\nimport re\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\nimport requests\n\nfrom comfy_cli.standalone"
  },
  {
    "path": "tests/comfy_cli/test_tracking.py",
    "chars": 6141,
    "preview": "from unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom comfy_cli import constants\nfrom comfy_cli.config_manager"
  },
  {
    "path": "tests/comfy_cli/test_ui.py",
    "chars": 1917,
    "preview": "import io\n\nfrom rich.console import Console\n\nimport comfy_cli.ui as ui_module\nfrom comfy_cli.ui import display_error_mes"
  },
  {
    "path": "tests/comfy_cli/test_update.py",
    "chars": 2425,
    "preview": "from unittest.mock import MagicMock, patch\n\nimport requests\n\nfrom comfy_cli.update import check_for_newer_pypi_version, "
  },
  {
    "path": "tests/comfy_cli/test_utils.py",
    "chars": 1943,
    "preview": "import io\nfrom unittest.mock import MagicMock, patch\n\nfrom comfy_cli.utils import create_tarball, download_url, extract_"
  },
  {
    "path": "tests/comfy_cli/test_workflow_to_api.py",
    "chars": 88609,
    "preview": "\"\"\"Unit tests for the UI -> API workflow converter.\"\"\"\n\nimport json\nimport random\nfrom pathlib import Path\nfrom unittest"
  },
  {
    "path": "tests/comfy_cli/test_workspace_manager.py",
    "chars": 25825,
    "preview": "import os\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom comfy_cli.workspace_manager import (\n    Works"
  },
  {
    "path": "tests/e2e/test_e2e.py",
    "chars": 9879,
    "preview": "import os\nimport subprocess\nfrom datetime import datetime\nfrom textwrap import dedent\n\nimport pytest\n\nfrom comfy_cli.res"
  },
  {
    "path": "tests/e2e/test_e2e_uv_compile.py",
    "chars": 14279,
    "preview": "\"\"\"E2E tests for comfy-cli uv-compile support (requires Manager v4.1+).\n\nTests the full stack: comfy node → execute_cm_c"
  },
  {
    "path": "tests/e2e/workflow.json",
    "chars": 1760,
    "preview": "{\n  \"3\": {\n    \"inputs\": {\n      \"seed\": 156680208700286,\n      \"steps\": 20,\n      \"cfg\": 8,\n      \"sampler_name\": \"eule"
  },
  {
    "path": "tests/test_file_utils_network.py",
    "chars": 33439,
    "preview": "import json\nimport pathlib\nfrom unittest.mock import Mock, patch\n\nimport httpx\nimport pytest\nimport requests\n\nfrom comfy"
  },
  {
    "path": "tests/uv/mock_comfy/custom_nodes/x/pyproject.toml",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "tests/uv/mock_comfy/custom_nodes/x/requirements.txt",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/uv/mock_comfy/custom_nodes/x/setup.cfg",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/uv/mock_comfy/custom_nodes/x/setup.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/uv/mock_comfy/custom_nodes/y/setup.cfg",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/uv/mock_comfy/custom_nodes/y/setup.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/uv/mock_comfy/custom_nodes/z/setup.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/uv/mock_comfy/pyproject.toml",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "tests/uv/mock_comfy/setup.cfg",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/uv/mock_comfy/setup.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/uv/mock_requirements/core_reqs.txt",
    "chars": 13,
    "preview": "tqdm==4.66.4\n"
  },
  {
    "path": "tests/uv/mock_requirements/x_reqs.txt",
    "chars": 37,
    "preview": "numpy>=2.0.0\nsympy<=1.10.1\ntqdm==1.0\n"
  },
  {
    "path": "tests/uv/mock_requirements/y_reqs.txt",
    "chars": 53,
    "preview": "mpmath==1.3.0\nnumpy<=2.0.2\nsympy>=1.13.0\ntqdm==2.0.0\n"
  },
  {
    "path": "tests/uv/test_torch_backend_compile.py",
    "chars": 4080,
    "preview": "\"\"\"Integration tests for torch backend compilation.\n\nThese tests do real uv pip compile with a torch requirement and ver"
  },
  {
    "path": "tests/uv/test_uv.py",
    "chars": 16465,
    "preview": "import shutil\nimport subprocess\nfrom pathlib import Path\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom comfy_cli "
  }
]

About this extraction

This page contains the full source code of the yoland68/comfy-cli GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 145 files (2.3 MB), approximately 615.1k tokens, and a symbol index with 1845 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!