comfy-cli 1.7.0__tar.gz → 1.7.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/PKG-INFO +24 -3
  2. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/README.md +23 -2
  3. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/cmdline.py +57 -9
  4. comfy_cli-1.7.2/comfy_cli/command/code_search.py +157 -0
  5. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/command/custom_nodes/bisect_custom_nodes.py +17 -5
  6. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/command/install.py +26 -5
  7. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/command/models/models.py +29 -1
  8. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/constants.py +6 -0
  9. comfy_cli-1.7.2/comfy_cli/cuda_detect.py +137 -0
  10. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/file_utils.py +220 -23
  11. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/uv.py +22 -2
  12. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/workspace_manager.py +46 -13
  13. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli.egg-info/PKG-INFO +24 -3
  14. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli.egg-info/SOURCES.txt +2 -0
  15. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/pyproject.toml +1 -1
  16. comfy_cli-1.7.2/tests/test_file_utils_network.py +530 -0
  17. comfy_cli-1.7.0/tests/test_file_utils_network.py +0 -164
  18. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/LICENSE +0 -0
  19. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/__init__.py +0 -0
  20. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/__main__.py +0 -0
  21. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/command/__init__.py +0 -0
  22. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/command/custom_nodes/__init__.py +0 -0
  23. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/command/custom_nodes/cm_cli_util.py +0 -0
  24. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/command/custom_nodes/command.py +0 -0
  25. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/command/github/pr_info.py +0 -0
  26. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/command/launch.py +0 -0
  27. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/command/pr_command.py +0 -0
  28. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/command/run.py +0 -0
  29. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/config_manager.py +0 -0
  30. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/env_checker.py +0 -0
  31. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/git_utils.py +0 -0
  32. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/logging.py +0 -0
  33. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/pr_cache.py +0 -0
  34. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/registry/__init__.py +0 -0
  35. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/registry/api.py +0 -0
  36. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/registry/config_parser.py +0 -0
  37. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/registry/types.py +0 -0
  38. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/resolve_python.py +0 -0
  39. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/standalone.py +0 -0
  40. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/tracking.py +0 -0
  41. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/typing.py +0 -0
  42. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/ui.py +0 -0
  43. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/update.py +0 -0
  44. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli/utils.py +0 -0
  45. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli.egg-info/dependency_links.txt +0 -0
  46. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli.egg-info/entry_points.txt +0 -0
  47. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli.egg-info/requires.txt +0 -0
  48. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/comfy_cli.egg-info/top_level.txt +0 -0
  49. {comfy_cli-1.7.0 → comfy_cli-1.7.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comfy-cli
3
- Version: 1.7.0
3
+ Version: 1.7.2
4
4
  Summary: A CLI tool for installing and using ComfyUI.
5
5
  Maintainer-email: Yoland Yan <yoland@drip.art>, James Kwon <hongilkwon316@gmail.com>, Robin Huang <robin@drip.art>, "Dr.Lt.Data" <dr.lt.data@gmail.com>
6
6
  License: GPL-3.0-only
@@ -111,6 +111,9 @@ will simply update the comfy.yaml file to reflect the local setup
111
111
  pip install comfyui-manager==4.1b8 # specific version
112
112
  ```
113
113
  - `comfy --workspace=<path> install`: Install ComfyUI into `<path>/ComfyUI`.
114
+ - `comfy install --fast-deps`: Use `uv` instead of `pip` for faster dependency resolution
115
+ during initial ComfyUI installation. comfy-cli's built-in resolver compiles all requirements (core + custom nodes)
116
+ into a single lockfile and installs from it. Also handles GPU-specific PyTorch wheel selection automatically.
114
117
  - For `comfy install`, if no path specification like `--workspace, --recent, or --here` is provided, it will be implicitly installed in `<HOME>/comfy`.
115
118
 
116
119
  #### Python environment handling
@@ -257,8 +260,9 @@ comfy node [show|simple-show] [installed|enabled|not-installed|disabled|all|snap
257
260
  #### Unified Dependency Resolution (--uv-compile)
258
261
 
259
262
  Requires ComfyUI-Manager v4.1+. Instead of installing dependencies per-node with
260
- `pip install`, `--uv-compile` batch-resolves all custom node dependencies via
261
- `uv pip compile` to avoid version conflicts.
263
+ `pip install`, `--uv-compile` delegates to ComfyUI-Manager's unified resolver which batch-resolves
264
+ all custom node dependencies via `uv pip compile` with **cross-node conflict detection** —
265
+ it can identify which node packs have incompatible dependencies and why.
262
266
 
263
267
  - Install with unified resolution:
264
268
 
@@ -280,6 +284,23 @@ Requires ComfyUI-Manager v4.1+. Instead of installing dependencies per-node with
280
284
 
281
285
  `comfy node install comfyui-impact-pack --no-uv-compile`
282
286
 
287
+ #### --fast-deps vs --uv-compile
288
+
289
+ Both flags use `uv` for faster dependency resolution, but they work differently:
290
+
291
+ | | `--fast-deps` | `--uv-compile` |
292
+ |-----------------------|-------------------------------------------------|-----------------------------------------------|
293
+ | **Resolver** | comfy-cli built-in (`DependencyCompiler`) | ComfyUI-Manager (`UnifiedDepResolver`) |
294
+ | **Scope** | `comfy install`, `comfy node install/reinstall` | Custom node commands only |
295
+ | **Conflict handling** | Interactive prompt to pick a version | Automatic detection with node attribution |
296
+ | **Config default** | No | Yes (`comfy manager uv-compile-default true`) |
297
+ | **Requires** | Only `uv` | ComfyUI-Manager v4.1+ |
298
+
299
+ **When to use which:**
300
+ - For initial ComfyUI installation with uv: `comfy install --fast-deps`
301
+ - For custom node management with Manager v4.1+: `--uv-compile` (recommended)
302
+ - For custom node management with older Manager: `--fast-deps`
303
+
283
304
  #### Bisect custom nodes
284
305
 
285
306
  If you encounter bugs only with custom nodes enabled, and want to find out which custom node(s) causes the bug,
@@ -66,6 +66,9 @@ will simply update the comfy.yaml file to reflect the local setup
66
66
  pip install comfyui-manager==4.1b8 # specific version
67
67
  ```
68
68
  - `comfy --workspace=<path> install`: Install ComfyUI into `<path>/ComfyUI`.
69
+ - `comfy install --fast-deps`: Use `uv` instead of `pip` for faster dependency resolution
70
+ during initial ComfyUI installation. comfy-cli's built-in resolver compiles all requirements (core + custom nodes)
71
+ into a single lockfile and installs from it. Also handles GPU-specific PyTorch wheel selection automatically.
69
72
  - For `comfy install`, if no path specification like `--workspace, --recent, or --here` is provided, it will be implicitly installed in `<HOME>/comfy`.
70
73
 
71
74
  #### Python environment handling
@@ -212,8 +215,9 @@ comfy node [show|simple-show] [installed|enabled|not-installed|disabled|all|snap
212
215
  #### Unified Dependency Resolution (--uv-compile)
213
216
 
214
217
  Requires ComfyUI-Manager v4.1+. Instead of installing dependencies per-node with
215
- `pip install`, `--uv-compile` batch-resolves all custom node dependencies via
216
- `uv pip compile` to avoid version conflicts.
218
+ `pip install`, `--uv-compile` delegates to ComfyUI-Manager's unified resolver which batch-resolves
219
+ all custom node dependencies via `uv pip compile` with **cross-node conflict detection** —
220
+ it can identify which node packs have incompatible dependencies and why.
217
221
 
218
222
  - Install with unified resolution:
219
223
 
@@ -235,6 +239,23 @@ Requires ComfyUI-Manager v4.1+. Instead of installing dependencies per-node with
235
239
 
236
240
  `comfy node install comfyui-impact-pack --no-uv-compile`
237
241
 
242
+ #### --fast-deps vs --uv-compile
243
+
244
+ Both flags use `uv` for faster dependency resolution, but they work differently:
245
+
246
+ | | `--fast-deps` | `--uv-compile` |
247
+ |-----------------------|-------------------------------------------------|-----------------------------------------------|
248
+ | **Resolver** | comfy-cli built-in (`DependencyCompiler`) | ComfyUI-Manager (`UnifiedDepResolver`) |
249
+ | **Scope** | `comfy install`, `comfy node install/reinstall` | Custom node commands only |
250
+ | **Conflict handling** | Interactive prompt to pick a version | Automatic detection with node attribution |
251
+ | **Config default** | No | Yes (`comfy manager uv-compile-default true`) |
252
+ | **Requires** | Only `uv` | ComfyUI-Manager v4.1+ |
253
+
254
+ **When to use which:**
255
+ - For initial ComfyUI installation with uv: `comfy install --fast-deps`
256
+ - For custom node management with Manager v4.1+: `--uv-compile` (recommended)
257
+ - For custom node management with older Manager: `--fast-deps`
258
+
238
259
  #### Bisect custom nodes
239
260
 
240
261
  If you encounter bugs only with custom nodes enabled, and want to find out which custom node(s) causes the bug,
@@ -10,7 +10,7 @@ from rich import print as rprint
10
10
  from rich.console import Console
11
11
 
12
12
  from comfy_cli import constants, env_checker, logging, tracking, ui, utils
13
- from comfy_cli.command import custom_nodes, pr_command
13
+ from comfy_cli.command import code_search, custom_nodes, pr_command
14
14
  from comfy_cli.command import install as install_inner
15
15
  from comfy_cli.command import run as run_inner
16
16
  from comfy_cli.command.install import validate_version
@@ -18,6 +18,7 @@ from comfy_cli.command.launch import launch as launch_command
18
18
  from comfy_cli.command.models import models as models_command
19
19
  from comfy_cli.config_manager import ConfigManager
20
20
  from comfy_cli.constants import GPU_OPTION, CUDAVersion, ROCmVersion
21
+ from comfy_cli.cuda_detect import DEFAULT_CUDA_TAG, detect_cuda_driver_version, resolve_cuda_wheel
21
22
  from comfy_cli.env_checker import EnvChecker
22
23
  from comfy_cli.resolve_python import resolve_workspace_python
23
24
  from comfy_cli.standalone import StandalonePython
@@ -142,6 +143,43 @@ def validate_commit_and_version(commit: str | None, ctx: typer.Context) -> str |
142
143
  return commit
143
144
 
144
145
 
146
+ def _resolve_cuda(
147
+ gpu: GPU_OPTION | None,
148
+ cuda_version: CUDAVersion | None,
149
+ ) -> tuple[CUDAVersion | None, str | None]:
150
+ """Resolve the CUDA wheel tag for an NVIDIA install.
151
+
152
+ Returns (cuda_version_enum_or_None, cuda_tag_string_or_None).
153
+ When the user passed an explicit --cuda-version, that is used as-is.
154
+ Otherwise auto-detection is attempted.
155
+ """
156
+ if gpu != GPU_OPTION.NVIDIA:
157
+ return cuda_version, None
158
+
159
+ if cuda_version is not None:
160
+ tag = f"cu{cuda_version.value.replace('.', '')}"
161
+ rprint(f"[bold]Using explicit CUDA version:[/bold] {cuda_version.value} ({tag})")
162
+ return cuda_version, tag
163
+
164
+ drv = detect_cuda_driver_version()
165
+ if drv is not None:
166
+ tag = resolve_cuda_wheel(drv)
167
+ if tag is not None:
168
+ rprint(f"[bold green]Detected CUDA driver version:[/bold green] {drv[0]}.{drv[1]} → using {tag}")
169
+ return None, tag
170
+ rprint(
171
+ f"[bold yellow]Warning:[/bold yellow] CUDA driver {drv[0]}.{drv[1]} is too old for any known PyTorch wheel. "
172
+ f"Falling back to {DEFAULT_CUDA_TAG}. Use `--cuda-version` to override."
173
+ )
174
+ return None, DEFAULT_CUDA_TAG
175
+
176
+ rprint(
177
+ f"[bold yellow]Warning:[/bold yellow] Could not detect CUDA driver version. "
178
+ f"Falling back to {DEFAULT_CUDA_TAG}. Use `--cuda-version` to override."
179
+ )
180
+ return None, DEFAULT_CUDA_TAG
181
+
182
+
145
183
  @app.command(help="Download and install ComfyUI and ComfyUI-Manager")
146
184
  @tracking.track_command()
147
185
  def install(
@@ -186,7 +224,7 @@ def install(
186
224
  callback=g_gpu_exclusivity.validate,
187
225
  ),
188
226
  ] = None,
189
- cuda_version: Annotated[CUDAVersion, typer.Option(show_default=True)] = CUDAVersion.v12_6,
227
+ cuda_version: Annotated[CUDAVersion | None, typer.Option(show_default=False)] = None,
190
228
  rocm_version: Annotated[ROCmVersion, typer.Option(show_default=True)] = ROCmVersion.v6_3,
191
229
  amd: Annotated[
192
230
  bool | None,
@@ -229,7 +267,7 @@ def install(
229
267
  typer.Option(
230
268
  "--fast-deps",
231
269
  show_default=False,
232
- help="Use new fast dependency installer",
270
+ help="Use uv instead of pip for dependency resolution (comfy-cli built-in resolver)",
233
271
  ),
234
272
  ] = False,
235
273
  pr: Annotated[
@@ -245,7 +283,7 @@ def install(
245
283
 
246
284
  comfy_path, _ = workspace_manager.get_workspace_path()
247
285
 
248
- is_comfy_installed_at_path, repo_dir = check_comfy_repo(comfy_path)
286
+ is_comfy_installed_at_path, resolved_path = check_comfy_repo(comfy_path)
249
287
  if is_comfy_installed_at_path and not restore:
250
288
  rprint(f"[bold red]ComfyUI is already installed at the specified path:[/bold red] {comfy_path}\n")
251
289
  rprint(
@@ -253,8 +291,8 @@ def install(
253
291
  )
254
292
  raise typer.Exit(code=1)
255
293
 
256
- if repo_dir is not None:
257
- comfy_path = str(repo_dir.working_dir)
294
+ if resolved_path is not None:
295
+ comfy_path = resolved_path
258
296
 
259
297
  if checker.python_version.major < 3 or checker.python_version.minor < 9:
260
298
  rprint("[bold red]Python version 3.9 or higher is required to run ComfyUI.[/bold red]")
@@ -276,6 +314,7 @@ def install(
276
314
  version=version,
277
315
  gpu=None,
278
316
  cuda_version=cuda_version,
317
+ cuda_tag=None,
279
318
  rocm_version=rocm_version,
280
319
  plat=platform,
281
320
  skip_torch_or_directml=skip_torch_or_directml,
@@ -321,6 +360,8 @@ def install(
321
360
  )
322
361
  raise typer.Exit(code=1)
323
362
 
363
+ cuda_version, cuda_tag = _resolve_cuda(gpu, cuda_version) if not skip_torch_or_directml else (cuda_version, None)
364
+
324
365
  install_inner.execute(
325
366
  url,
326
367
  comfy_path,
@@ -330,6 +371,7 @@ def install(
330
371
  gpu=gpu,
331
372
  version=version,
332
373
  cuda_version=cuda_version,
374
+ cuda_tag=cuda_tag,
333
375
  rocm_version=rocm_version,
334
376
  plat=platform,
335
377
  skip_torch_or_directml=skip_torch_or_directml,
@@ -374,7 +416,10 @@ def update(
374
416
  check=True,
375
417
  )
376
418
 
377
- custom_nodes.command.update_node_id_cache()
419
+ try:
420
+ custom_nodes.command.update_node_id_cache()
421
+ except (FileNotFoundError, subprocess.CalledProcessError) as e:
422
+ rprint(f"[yellow]Failed to update node id cache: {e}[/yellow]")
378
423
 
379
424
 
380
425
  @app.command(help="Run API workflow file using the ComfyUI launched by `comfy launch --background`")
@@ -487,7 +532,7 @@ def set_default(
487
532
  )
488
533
  raise typer.Exit(code=1)
489
534
 
490
- is_comfy_repo, comfy_repo = check_comfy_repo(comfy_path)
535
+ is_comfy_repo, resolved_path = check_comfy_repo(comfy_path)
491
536
  if not is_comfy_repo:
492
537
  rprint(
493
538
  f"\nSpecified path is not a ComfyUI path: {comfy_path}.\n",
@@ -495,7 +540,7 @@ def set_default(
495
540
  )
496
541
  raise typer.Exit(code=1)
497
542
 
498
- comfy_path = comfy_repo.working_dir
543
+ comfy_path = resolved_path
499
544
 
500
545
  rprint(f"Specified path is set as default ComfyUI path: {comfy_path} ")
501
546
  workspace_manager.set_default_workspace(comfy_path)
@@ -643,4 +688,7 @@ app.add_typer(custom_nodes.manager_app, name="manager", help="Manage ComfyUI-Man
643
688
 
644
689
  app.add_typer(pr_command.app, name="pr-cache", help="Manage PR cache.")
645
690
 
691
+ app.add_typer(code_search.app, name="code-search", help="Search code across ComfyUI repositories.")
692
+ app.add_typer(code_search.app, name="cs", hidden=True)
693
+
646
694
  app.add_typer(tracking.app, name="tracking", help="Manage analytics tracking settings.")
@@ -0,0 +1,157 @@
1
+ """CLI commands for searching code across ComfyUI repositories."""
2
+
3
+ import json
4
+ import re
5
+ from typing import Annotated
6
+ from urllib.parse import quote
7
+
8
+ import requests
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.text import Text
12
+
13
+ from comfy_cli import tracking
14
+
15
+ app = typer.Typer()
16
+ console = Console()
17
+
18
+ API_URL = "https://comfy-codesearch.vercel.app/api/search/code"
19
+ DEFAULT_COUNT = 20
20
+ REQUEST_TIMEOUT = 30
21
+
22
+
23
+ def _build_query(query: str, repo: str | None, count: int) -> str:
24
+ parts = []
25
+ if repo:
26
+ if "/" not in repo:
27
+ repo = f"Comfy-Org/{repo}"
28
+ parts.append(f"repo:^{re.escape(repo)}$")
29
+ parts.append(f"count:{count}")
30
+ parts.append(query)
31
+ return " ".join(parts)
32
+
33
+
34
+ def _fetch_results(query: str) -> dict:
35
+ response = requests.get(API_URL, params={"query": query}, timeout=REQUEST_TIMEOUT)
36
+ response.raise_for_status()
37
+ return response.json()
38
+
39
+
40
+ def _format_results(search: dict) -> list[dict]:
41
+ raw_results = search.get("results", {}).get("results", [])
42
+ formatted = []
43
+ for result in raw_results:
44
+ repo_info = result.get("repository") or {}
45
+ repo_name = repo_info.get("name", "")
46
+ clean_name = repo_name.removeprefix("github.com/")
47
+
48
+ file_info = result.get("file") or {}
49
+ file_path = file_info.get("path", "")
50
+
51
+ if not clean_name or not file_path:
52
+ continue
53
+
54
+ default_branch = repo_info.get("defaultBranch") or {}
55
+ branch_name = default_branch.get("displayName", "main")
56
+ commit_hash = (default_branch.get("target") or {}).get("commit", {}).get("oid", "")
57
+ ref = commit_hash or branch_name
58
+
59
+ line_matches = result.get("lineMatches") or []
60
+ matches = []
61
+ if line_matches:
62
+ encoded_path = quote(file_path, safe="/")
63
+ base_url = f"https://github.com/{clean_name}/blob/{ref}/{encoded_path}"
64
+ for m in line_matches:
65
+ line = m.get("lineNumber", 0) + 1
66
+ preview = m.get("preview", "").rstrip()
67
+ matches.append({"line": line, "preview": preview, "url": f"{base_url}#L{line}"})
68
+
69
+ formatted.append(
70
+ {
71
+ "repository": clean_name,
72
+ "file": file_path,
73
+ "branch": branch_name,
74
+ "commit": commit_hash,
75
+ "matches": matches,
76
+ }
77
+ )
78
+
79
+ return formatted
80
+
81
+
82
+ def _get_stats(search: dict) -> dict:
83
+ return {
84
+ "approximate_count": search.get("stats", {}).get("approximateResultCount", "0"),
85
+ "match_count": search.get("results", {}).get("matchCount", 0),
86
+ "limit_hit": search.get("results", {}).get("limitHit", False),
87
+ }
88
+
89
+
90
+ def _print_results(results: list[dict], stats: dict, json_output: bool) -> None:
91
+ if json_output:
92
+ print(json.dumps({"stats": stats, "results": results}, indent=2))
93
+ return
94
+
95
+ if not results:
96
+ console.print("[yellow]No results found.[/yellow]")
97
+ return
98
+
99
+ for file_result in results:
100
+ header = Text()
101
+ header.append(file_result["repository"], style="bold cyan")
102
+ header.append(" / ", style="dim")
103
+ header.append(file_result["file"], style="bold")
104
+ console.print(header)
105
+
106
+ for match in file_result["matches"]:
107
+ line_text = Text()
108
+ line_text.append(f" L{match['line']:>5}", style="green")
109
+ line_text.append(f" {match['preview']}")
110
+ console.print(line_text)
111
+ console.print(f" [dim]{match['url']}[/dim]")
112
+
113
+ console.print()
114
+
115
+ limit_msg = " (limit hit — use --count to fetch more)" if stats.get("limit_hit") else ""
116
+ console.print(
117
+ f"[dim]{stats['approximate_count']} approximate results, {stats['match_count']} matches returned{limit_msg}[/dim]"
118
+ )
119
+
120
+
121
+ @app.callback(invoke_without_command=True)
122
+ @tracking.track_command()
123
+ def code_search(
124
+ query: Annotated[str, typer.Argument(help="Search query (supports Sourcegraph syntax)")],
125
+ repo: Annotated[
126
+ str | None,
127
+ typer.Option("--repo", "-r", help="Filter by repository (e.g. ComfyUI, Comfy-Org/ComfyUI)"),
128
+ ] = None,
129
+ count: Annotated[
130
+ int,
131
+ typer.Option("--count", "-n", help="Maximum number of results"),
132
+ ] = DEFAULT_COUNT,
133
+ json_output: Annotated[
134
+ bool,
135
+ typer.Option("--json", "-j", help="Output results as JSON"),
136
+ ] = False,
137
+ ):
138
+ """Search code across ComfyUI repositories."""
139
+ built_query = _build_query(query, repo, count)
140
+
141
+ try:
142
+ data = _fetch_results(built_query)
143
+ except requests.ConnectionError:
144
+ console.print("[bold red]Error: Could not connect to the code search service.[/bold red]")
145
+ raise typer.Exit(code=1)
146
+ except requests.Timeout:
147
+ console.print("[bold red]Error: Request timed out.[/bold red]")
148
+ raise typer.Exit(code=1)
149
+ except requests.HTTPError as e:
150
+ status = e.response.status_code if e.response is not None else "unknown"
151
+ console.print(f"[bold red]Error: HTTP {status}[/bold red]")
152
+ raise typer.Exit(code=1)
153
+
154
+ search = data.get("data", {}).get("search", {})
155
+ results = _format_results(search)
156
+ stats = _get_stats(search)
157
+ _print_results(results, stats, json_output=json_output)
@@ -122,6 +122,22 @@ set of nodes to test: {len(self.active)}
122
122
  {active_list}"""
123
123
 
124
124
 
125
+ def parse_cm_output(cm_output: str, pinned_nodes: set[str] | None = None) -> list[str]:
126
+ """Parse cm_cli simple-show output into a list of node names.
127
+
128
+ cm_cli simple-show always formats node entries as ``name@version``
129
+ (see ComfyUI-Manager cm_cli show_list). We whitelist on the ``@``
130
+ separator so any progress/status lines are ignored regardless of
131
+ their prefix.
132
+ """
133
+ pinned = pinned_nodes or set()
134
+ return [
135
+ stripped
136
+ for line in cm_output.strip().split("\n")
137
+ if (stripped := line.strip()) and "@" in stripped and stripped not in pinned
138
+ ]
139
+
140
+
125
141
  @bisect_app.command(
126
142
  help="Start a new bisect session with optionally pinned nodes to always enable, and optional ComfyUI launch args."
127
143
  + "?[--pinned-nodes PINNED_NODES]"
@@ -145,11 +161,7 @@ def start(
145
161
  typer.echo("Failed to fetch the list of nodes.")
146
162
  raise typer.Exit()
147
163
 
148
- nodes_list = [
149
- line.strip()
150
- for line in cm_output.strip().split("\n")
151
- if not line.startswith("FETCH DATA") and line.strip() not in pinned_nodes
152
- ]
164
+ nodes_list = parse_cm_output(cm_output, pinned_nodes)
153
165
  state = BisectState(
154
166
  status="running",
155
167
  all=nodes_list,
@@ -18,6 +18,7 @@ from comfy_cli import constants, ui
18
18
  from comfy_cli.command.custom_nodes.command import update_node_id_cache
19
19
  from comfy_cli.command.github.pr_info import PRInfo
20
20
  from comfy_cli.constants import GPU_OPTION
21
+ from comfy_cli.cuda_detect import DEFAULT_CUDA_TAG
21
22
  from comfy_cli.git_utils import checkout_pr, git_checkout_tag
22
23
  from comfy_cli.resolve_python import ensure_workspace_python
23
24
  from comfy_cli.uv import DependencyCompiler
@@ -45,11 +46,12 @@ def pip_install_comfyui_dependencies(
45
46
  repo_dir,
46
47
  gpu: GPU_OPTION,
47
48
  plat: constants.OS,
48
- cuda_version: constants.CUDAVersion,
49
+ cuda_version: constants.CUDAVersion | None,
49
50
  skip_torch_or_directml: bool,
50
51
  skip_requirement: bool,
51
52
  python: str = sys.executable,
52
53
  rocm_version: constants.ROCmVersion = constants.ROCmVersion.v6_3,
54
+ cuda_tag: str | None = None,
53
55
  ):
54
56
  os.chdir(repo_dir)
55
57
 
@@ -63,7 +65,8 @@ def pip_install_comfyui_dependencies(
63
65
 
64
66
  # install torch for NVIDIA
65
67
  if gpu == GPU_OPTION.NVIDIA:
66
- cuda_tag = f"cu{cuda_version.value.replace('.', '')}"
68
+ if cuda_tag is None:
69
+ cuda_tag = f"cu{cuda_version.value.replace('.', '')}" if cuda_version else DEFAULT_CUDA_TAG
67
70
  result = _pip_install_torch(python, ["--index-url", f"https://download.pytorch.org/whl/{cuda_tag}"])
68
71
 
69
72
  # install torch for Intel Arc GPUs (upstream torch xpu)
@@ -147,7 +150,8 @@ def execute(
147
150
  version: str,
148
151
  commit: str | None = None,
149
152
  gpu: constants.GPU_OPTION = None,
150
- cuda_version: constants.CUDAVersion = constants.CUDAVersion.v12_6,
153
+ cuda_version: constants.CUDAVersion | None = None,
154
+ cuda_tag: str | None = None,
151
155
  rocm_version: constants.ROCmVersion = constants.ROCmVersion.v6_3,
152
156
  plat: constants.OS = None,
153
157
  skip_torch_or_directml: bool = False,
@@ -227,6 +231,7 @@ def execute(
227
231
  skip_requirement,
228
232
  python=python,
229
233
  rocm_version=rocm_version,
234
+ cuda_tag=cuda_tag,
230
235
  )
231
236
 
232
237
  WorkspaceManager().set_recent_workspace(repo_dir)
@@ -256,11 +261,19 @@ def execute(
256
261
  # Workspace venv needs uv bootstrapped; for the global Python
257
262
  # uv is already available as a comfy-cli dependency.
258
263
  DependencyCompiler.Install_Build_Deps(executable=python)
264
+ if cuda_tag:
265
+ # DependencyCompiler expects a dotted version like "13.0", not a tag like "cu130"
266
+ digits = cuda_tag[2:]
267
+ resolved_cuda = f"{digits[:2]}.{digits[2:]}"
268
+ elif cuda_version:
269
+ resolved_cuda = cuda_version.value
270
+ else:
271
+ resolved_cuda = None
259
272
  depComp = DependencyCompiler(
260
273
  cwd=repo_dir,
261
274
  executable=python,
262
275
  gpu=gpu,
263
- cuda_version=cuda_version.value,
276
+ cuda_version=resolved_cuda,
264
277
  rocm_version=rocm_version.value,
265
278
  skip_torch=skip_torch_or_directml,
266
279
  )
@@ -463,8 +476,16 @@ def get_latest_release(repo_owner: str, repo_name: str) -> GithubRelease | None:
463
476
  """
464
477
  url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest"
465
478
 
479
+ headers = {}
480
+ if github_token := os.getenv("GITHUB_TOKEN"):
481
+ headers["Authorization"] = f"Bearer {github_token}"
482
+
466
483
  try:
467
- response = requests.get(url, timeout=5)
484
+ response = requests.get(url, headers=headers, timeout=5)
485
+
486
+ if response.status_code in (403, 429):
487
+ handle_github_rate_limit(response)
488
+
468
489
  response.raise_for_status()
469
490
 
470
491
  data = response.json()
@@ -1,6 +1,7 @@
1
1
  import contextlib
2
2
  import os
3
3
  import pathlib
4
+ import time
4
5
  from typing import Annotated
5
6
  from urllib.parse import parse_qs, unquote, urlparse
6
7
 
@@ -33,6 +34,18 @@ def get_workspace() -> pathlib.Path:
33
34
  return pathlib.Path(workspace_manager.workspace_path)
34
35
 
35
36
 
37
+ def _format_elapsed(seconds: float) -> str:
38
+ """Format elapsed seconds into a human-readable string."""
39
+ rounded = round(seconds, 1)
40
+ if rounded < 60:
41
+ return f"{rounded:.1f}s"
42
+ minutes, secs = divmod(int(rounded), 60)
43
+ if minutes < 60:
44
+ return f"{minutes}m {secs}s"
45
+ hours, minutes = divmod(minutes, 60)
46
+ return f"{hours}h {minutes}m {secs}s"
47
+
48
+
36
49
  def potentially_strip_param_url(path_name: str) -> str:
37
50
  return path_name.split("?")[0]
38
51
 
@@ -219,6 +232,14 @@ def download(
219
232
  show_default=False,
220
233
  ),
221
234
  ] = None,
235
+ downloader: Annotated[
236
+ str | None,
237
+ typer.Option(
238
+ "--downloader",
239
+ help="Download backend: 'httpx' (default) or 'aria2' (requires aria2 RPC server).",
240
+ show_default=False,
241
+ ),
242
+ ] = None,
222
243
  ):
223
244
  if relative_path is not None:
224
245
  relative_path = os.path.expanduser(relative_path)
@@ -233,6 +254,8 @@ def download(
233
254
  constants.HF_API_TOKEN_ENV_KEY, constants.HF_API_TOKEN_KEY, set_hf_api_token
234
255
  )
235
256
 
257
+ resolved_downloader = downloader or config_manager.get(constants.CONFIG_KEY_DEFAULT_DOWNLOADER) or "httpx"
258
+
236
259
  is_civitai_model_url, is_civitai_api_url, model_id, version_id = check_civitai_url(url)
237
260
  is_huggingface_url, repo_id, hf_filename, hf_folder_name, hf_branch_name = check_huggingface_url(url)
238
261
 
@@ -297,6 +320,8 @@ def download(
297
320
  print(f"[bold red]File already exists: {local_filepath}[/bold red]")
298
321
  return
299
322
 
323
+ start_time = time.monotonic()
324
+
300
325
  if is_huggingface_url and check_unauthorized(url, headers):
301
326
  if hf_api_token is None:
302
327
  print(
@@ -329,7 +354,10 @@ def download(
329
354
  print(f"Model downloaded successfully to: {output_path}")
330
355
  else:
331
356
  print(f"Start downloading URL: {url} into {local_filepath}")
332
- download_file(url, local_filepath, headers)
357
+ download_file(url, local_filepath, headers, downloader=resolved_downloader)
358
+
359
+ elapsed = time.monotonic() - start_time
360
+ print(f"Done in {_format_elapsed(elapsed)}")
333
361
 
334
362
 
335
363
  @app.command()
@@ -50,6 +50,10 @@ CIVITAI_API_TOKEN_ENV_KEY = "CIVITAI_API_TOKEN"
50
50
  HF_API_TOKEN_KEY = "hf_api_token"
51
51
  HF_API_TOKEN_ENV_KEY = "HF_API_TOKEN"
52
52
 
53
+ ARIA2_SERVER_ENV_KEY = "COMFYUI_MANAGER_ARIA2_SERVER"
54
+ ARIA2_SECRET_ENV_KEY = "COMFYUI_MANAGER_ARIA2_SECRET"
55
+ CONFIG_KEY_DEFAULT_DOWNLOADER = "default_downloader"
56
+
53
57
  DEFAULT_TRACKING_VALUE = True
54
58
 
55
59
  COMFY_LOCK_YAML_FILE = "comfy.lock.yaml"
@@ -72,7 +76,9 @@ COMFY_ORIGIN_URL_CHOICES = {
72
76
 
73
77
 
74
78
  class CUDAVersion(str, Enum):
79
+ v13_0 = "13.0"
75
80
  v12_9 = "12.9"
81
+ v12_8 = "12.8"
76
82
  v12_6 = "12.6"
77
83
  v12_4 = "12.4"
78
84
  v12_1 = "12.1"