comfy-cli-slim 1.23.0.post2__py3-none-any.whl

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 (294) hide show
  1. comfy_cli_slim/__init__.py +0 -0
  2. comfy_cli_slim/__main__.py +172 -0
  3. comfy_cli_slim/_lazy.py +188 -0
  4. comfy_cli_slim/_safe_exec.py +344 -0
  5. comfy_cli_slim/_security_scan.py +219 -0
  6. comfy_cli_slim/agent/__init__.py +463 -0
  7. comfy_cli_slim/agent/command.py +569 -0
  8. comfy_cli_slim/auth/__init__.py +26 -0
  9. comfy_cli_slim/auth/command.py +157 -0
  10. comfy_cli_slim/auth/store.py +348 -0
  11. comfy_cli_slim/builder_api.py +465 -0
  12. comfy_cli_slim/builder_pagination.py +72 -0
  13. comfy_cli_slim/caller.py +166 -0
  14. comfy_cli_slim/cancellation.py +125 -0
  15. comfy_cli_slim/cloud/__init__.py +140 -0
  16. comfy_cli_slim/cloud/billing.py +436 -0
  17. comfy_cli_slim/cloud/command.py +728 -0
  18. comfy_cli_slim/cloud/oauth.py +1001 -0
  19. comfy_cli_slim/cmdline.py +3142 -0
  20. comfy_cli_slim/comfy_client.py +728 -0
  21. comfy_cli_slim/command/__init__.py +54 -0
  22. comfy_cli_slim/command/_cloud_errors.py +266 -0
  23. comfy_cli_slim/command/assets_library.py +338 -0
  24. comfy_cli_slim/command/build.py +3632 -0
  25. comfy_cli_slim/command/build_diff.py +342 -0
  26. comfy_cli_slim/command/build_digest_cache.py +97 -0
  27. comfy_cli_slim/command/build_package.py +269 -0
  28. comfy_cli_slim/command/build_paths.py +207 -0
  29. comfy_cli_slim/command/build_pull.py +392 -0
  30. comfy_cli_slim/command/build_push.py +369 -0
  31. comfy_cli_slim/command/build_spec.py +277 -0
  32. comfy_cli_slim/command/build_targets.py +118 -0
  33. comfy_cli_slim/command/build_upload_progress.py +369 -0
  34. comfy_cli_slim/command/build_validation.py +562 -0
  35. comfy_cli_slim/command/cloud_http.py +148 -0
  36. comfy_cli_slim/command/code_search.py +770 -0
  37. comfy_cli_slim/command/custom_nodes/__init__.py +3 -0
  38. comfy_cli_slim/command/custom_nodes/bisect_custom_nodes.py +233 -0
  39. comfy_cli_slim/command/custom_nodes/cm_cli_util.py +294 -0
  40. comfy_cli_slim/command/custom_nodes/command.py +1460 -0
  41. comfy_cli_slim/command/deploy.py +589 -0
  42. comfy_cli_slim/command/deploy_cancel.py +195 -0
  43. comfy_cli_slim/command/deploy_compute.py +48 -0
  44. comfy_cli_slim/command/deploy_lifecycle.py +161 -0
  45. comfy_cli_slim/command/deploy_ls.py +125 -0
  46. comfy_cli_slim/command/deploy_progress.py +388 -0
  47. comfy_cli_slim/command/deploy_promote.py +55 -0
  48. comfy_cli_slim/command/deploy_read.py +289 -0
  49. comfy_cli_slim/command/deploy_refs.py +120 -0
  50. comfy_cli_slim/command/deploy_rename.py +110 -0
  51. comfy_cli_slim/command/deploy_resolve.py +545 -0
  52. comfy_cli_slim/command/deploy_rollback.py +291 -0
  53. comfy_cli_slim/command/deploy_run.py +326 -0
  54. comfy_cli_slim/command/deploy_run_assets.py +90 -0
  55. comfy_cli_slim/command/deploy_run_watch.py +35 -0
  56. comfy_cli_slim/command/deploy_runtime.py +227 -0
  57. comfy_cli_slim/command/deploy_status.py +516 -0
  58. comfy_cli_slim/command/deploy_types.py +381 -0
  59. comfy_cli_slim/command/deploy_up.py +780 -0
  60. comfy_cli_slim/command/deploy_workflow.py +424 -0
  61. comfy_cli_slim/command/generate/__init__.py +3 -0
  62. comfy_cli_slim/command/generate/adapters.py +318 -0
  63. comfy_cli_slim/command/generate/app.py +1302 -0
  64. comfy_cli_slim/command/generate/client.py +226 -0
  65. comfy_cli_slim/command/generate/emit.py +674 -0
  66. comfy_cli_slim/command/generate/output.py +134 -0
  67. comfy_cli_slim/command/generate/poll.py +407 -0
  68. comfy_cli_slim/command/generate/schema.py +295 -0
  69. comfy_cli_slim/command/generate/spec/openapi.yml +1 -0
  70. comfy_cli_slim/command/generate/spec.py +1285 -0
  71. comfy_cli_slim/command/generate/upload.py +128 -0
  72. comfy_cli_slim/command/github/pr_info.py +16 -0
  73. comfy_cli_slim/command/install.py +1317 -0
  74. comfy_cli_slim/command/job_watcher.py +534 -0
  75. comfy_cli_slim/command/jobs.py +3021 -0
  76. comfy_cli_slim/command/knowledge.py +416 -0
  77. comfy_cli_slim/command/launch.py +1098 -0
  78. comfy_cli_slim/command/models/models.py +2266 -0
  79. comfy_cli_slim/command/models/search.py +843 -0
  80. comfy_cli_slim/command/node_deps.py +779 -0
  81. comfy_cli_slim/command/nodes.py +1472 -0
  82. comfy_cli_slim/command/outdated.py +550 -0
  83. comfy_cli_slim/command/pack_scan.py +44 -0
  84. comfy_cli_slim/command/pr_command.py +92 -0
  85. comfy_cli_slim/command/preview.py +323 -0
  86. comfy_cli_slim/command/project.py +369 -0
  87. comfy_cli_slim/command/run/__init__.py +1530 -0
  88. comfy_cli_slim/command/run/execution.py +686 -0
  89. comfy_cli_slim/command/run/loader.py +132 -0
  90. comfy_cli_slim/command/run/preflight.py +243 -0
  91. comfy_cli_slim/command/run/watcher.py +183 -0
  92. comfy_cli_slim/command/run_cli.py +571 -0
  93. comfy_cli_slim/command/setup.py +675 -0
  94. comfy_cli_slim/command/stop_port.py +673 -0
  95. comfy_cli_slim/command/system.py +151 -0
  96. comfy_cli_slim/command/templates.py +2161 -0
  97. comfy_cli_slim/command/transfer.py +1003 -0
  98. comfy_cli_slim/command/version_validators.py +60 -0
  99. comfy_cli_slim/command/workflow.py +2247 -0
  100. comfy_cli_slim/command/workflow_edit.py +1195 -0
  101. comfy_cli_slim/command/workflow_fragments.py +504 -0
  102. comfy_cli_slim/command_mentions.py +188 -0
  103. comfy_cli_slim/config_manager.py +215 -0
  104. comfy_cli_slim/constants.py +129 -0
  105. comfy_cli_slim/cql/__init__.py +10 -0
  106. comfy_cli_slim/cql/_net.py +28 -0
  107. comfy_cli_slim/cql/annotations_source.py +640 -0
  108. comfy_cli_slim/cql/data/__init__.py +1 -0
  109. comfy_cli_slim/cql/data/cloud_disable_config.yaml +15 -0
  110. comfy_cli_slim/cql/data/default_text2img.json +66 -0
  111. comfy_cli_slim/cql/data/supported_nodes.yaml +1107 -0
  112. comfy_cli_slim/cql/default_workflow.py +384 -0
  113. comfy_cli_slim/cql/engine.py +5549 -0
  114. comfy_cli_slim/cql/errors.py +26 -0
  115. comfy_cli_slim/cql/loader.py +271 -0
  116. comfy_cli_slim/cql/promoted.py +2362 -0
  117. comfy_cli_slim/cql/widget_catalog.py +184 -0
  118. comfy_cli_slim/credentials.py +444 -0
  119. comfy_cli_slim/cuda_detect.py +158 -0
  120. comfy_cli_slim/deploy_api.py +330 -0
  121. comfy_cli_slim/deploy_api_errors.py +279 -0
  122. comfy_cli_slim/deploy_assets.py +271 -0
  123. comfy_cli_slim/deploy_download.py +275 -0
  124. comfy_cli_slim/deploy_events.py +314 -0
  125. comfy_cli_slim/deploy_jobs.py +302 -0
  126. comfy_cli_slim/deprecation.py +117 -0
  127. comfy_cli_slim/discovery.py +336 -0
  128. comfy_cli_slim/download_state.py +1284 -0
  129. comfy_cli_slim/env_checker.py +227 -0
  130. comfy_cli_slim/error_codes.py +1864 -0
  131. comfy_cli_slim/execution_errors.py +159 -0
  132. comfy_cli_slim/file_utils.py +1109 -0
  133. comfy_cli_slim/fragments.py +1064 -0
  134. comfy_cli_slim/git_utils.py +234 -0
  135. comfy_cli_slim/hardware.py +335 -0
  136. comfy_cli_slim/hashing.py +14 -0
  137. comfy_cli_slim/help_json.py +183 -0
  138. comfy_cli_slim/host_port.py +238 -0
  139. comfy_cli_slim/http.py +487 -0
  140. comfy_cli_slim/interaction.py +338 -0
  141. comfy_cli_slim/jobs_state.py +322 -0
  142. comfy_cli_slim/knowledge.py +1296 -0
  143. comfy_cli_slim/layout.py +633 -0
  144. comfy_cli_slim/layout_quality.py +219 -0
  145. comfy_cli_slim/link_integrity.py +204 -0
  146. comfy_cli_slim/local_address.py +214 -0
  147. comfy_cli_slim/locking.py +154 -0
  148. comfy_cli_slim/logging.py +44 -0
  149. comfy_cli_slim/model_variants.py +299 -0
  150. comfy_cli_slim/onboarding.py +38 -0
  151. comfy_cli_slim/output/__init__.py +44 -0
  152. comfy_cli_slim/output/branding.py +437 -0
  153. comfy_cli_slim/output/glyphs.py +76 -0
  154. comfy_cli_slim/output/panels.py +527 -0
  155. comfy_cli_slim/output/preview.py +170 -0
  156. comfy_cli_slim/output/progress.py +68 -0
  157. comfy_cli_slim/output/renderer.py +551 -0
  158. comfy_cli_slim/output/rich_compat.py +42 -0
  159. comfy_cli_slim/output/sanitize.py +246 -0
  160. comfy_cli_slim/pr_cache.py +234 -0
  161. comfy_cli_slim/project.py +220 -0
  162. comfy_cli_slim/registry/__init__.py +15 -0
  163. comfy_cli_slim/registry/api.py +294 -0
  164. comfy_cli_slim/registry/config_parser.py +810 -0
  165. comfy_cli_slim/registry/types.py +82 -0
  166. comfy_cli_slim/resolve_python.py +76 -0
  167. comfy_cli_slim/schemas/__init__.py +6 -0
  168. comfy_cli_slim/schemas/agent.json +298 -0
  169. comfy_cli_slim/schemas/assets.json +33 -0
  170. comfy_cli_slim/schemas/assets_library.json +34 -0
  171. comfy_cli_slim/schemas/auth.json +30 -0
  172. comfy_cli_slim/schemas/build_blob_ls.json +12 -0
  173. comfy_cli_slim/schemas/build_delete.json +12 -0
  174. comfy_cli_slim/schemas/build_init.json +104 -0
  175. comfy_cli_slim/schemas/build_ls.json +22 -0
  176. comfy_cli_slim/schemas/build_pull.json +69 -0
  177. comfy_cli_slim/schemas/build_push.json +88 -0
  178. comfy_cli_slim/schemas/build_push_event.json +58 -0
  179. comfy_cli_slim/schemas/build_refs_base_images.json +9 -0
  180. comfy_cli_slim/schemas/build_refs_build_targets.json +9 -0
  181. comfy_cli_slim/schemas/build_refs_model_dirs.json +9 -0
  182. comfy_cli_slim/schemas/build_refs_resolve.json +9 -0
  183. comfy_cli_slim/schemas/build_release_create.json +27 -0
  184. comfy_cli_slim/schemas/build_release_delete.json +12 -0
  185. comfy_cli_slim/schemas/build_release_logs.json +16 -0
  186. comfy_cli_slim/schemas/build_release_ls.json +24 -0
  187. comfy_cli_slim/schemas/build_release_manifest.json +9 -0
  188. comfy_cli_slim/schemas/build_release_show.json +13 -0
  189. comfy_cli_slim/schemas/build_show.json +13 -0
  190. comfy_cli_slim/schemas/build_status.json +153 -0
  191. comfy_cli_slim/schemas/build_update.json +69 -0
  192. comfy_cli_slim/schemas/build_validate.json +14 -0
  193. comfy_cli_slim/schemas/cloud_status.json +127 -0
  194. comfy_cli_slim/schemas/deploy_cancel.json +116 -0
  195. comfy_cli_slim/schemas/deploy_delete.json +14 -0
  196. comfy_cli_slim/schemas/deploy_events.json +40 -0
  197. comfy_cli_slim/schemas/deploy_history.json +68 -0
  198. comfy_cli_slim/schemas/deploy_logs.json +14 -0
  199. comfy_cli_slim/schemas/deploy_ls.json +48 -0
  200. comfy_cli_slim/schemas/deploy_progress_event.json +98 -0
  201. comfy_cli_slim/schemas/deploy_promote.json +101 -0
  202. comfy_cli_slim/schemas/deploy_refs_compute.json +39 -0
  203. comfy_cli_slim/schemas/deploy_rename.json +39 -0
  204. comfy_cli_slim/schemas/deploy_rollback.json +86 -0
  205. comfy_cli_slim/schemas/deploy_run.json +77 -0
  206. comfy_cli_slim/schemas/deploy_scale.json +24 -0
  207. comfy_cli_slim/schemas/deploy_show.json +21 -0
  208. comfy_cli_slim/schemas/deploy_start.json +13 -0
  209. comfy_cli_slim/schemas/deploy_status.json +142 -0
  210. comfy_cli_slim/schemas/deploy_stop.json +13 -0
  211. comfy_cli_slim/schemas/deploy_up.json +116 -0
  212. comfy_cli_slim/schemas/discover.json +86 -0
  213. comfy_cli_slim/schemas/download.json +32 -0
  214. comfy_cli_slim/schemas/download_status.json +52 -0
  215. comfy_cli_slim/schemas/downloads.json +16 -0
  216. comfy_cli_slim/schemas/env.json +79 -0
  217. comfy_cli_slim/schemas/envelope.json +49 -0
  218. comfy_cli_slim/schemas/error.json +27 -0
  219. comfy_cli_slim/schemas/error_codes.md +28 -0
  220. comfy_cli_slim/schemas/free.json +21 -0
  221. comfy_cli_slim/schemas/generate_list.json +70 -0
  222. comfy_cli_slim/schemas/generate_result.json +19 -0
  223. comfy_cli_slim/schemas/generate_schema.json +96 -0
  224. comfy_cli_slim/schemas/help.json +63 -0
  225. comfy_cli_slim/schemas/jobs.json +98 -0
  226. comfy_cli_slim/schemas/jobs_wait.json +35 -0
  227. comfy_cli_slim/schemas/knowledge.json +75 -0
  228. comfy_cli_slim/schemas/knowledge_block.json +81 -0
  229. comfy_cli_slim/schemas/launch.json +15 -0
  230. comfy_cli_slim/schemas/logs.json +42 -0
  231. comfy_cli_slim/schemas/models.json +92 -0
  232. comfy_cli_slim/schemas/node_deps.json +124 -0
  233. comfy_cli_slim/schemas/nodes.json +50 -0
  234. comfy_cli_slim/schemas/outdated.json +55 -0
  235. comfy_cli_slim/schemas/preview.json +19 -0
  236. comfy_cli_slim/schemas/project.json +60 -0
  237. comfy_cli_slim/schemas/run.json +65 -0
  238. comfy_cli_slim/schemas/run_event.json +71 -0
  239. comfy_cli_slim/schemas/set_default.json +11 -0
  240. comfy_cli_slim/schemas/skill.json +34 -0
  241. comfy_cli_slim/schemas/stop.json +38 -0
  242. comfy_cli_slim/schemas/system_stats.json +35 -0
  243. comfy_cli_slim/schemas/templates.json +78 -0
  244. comfy_cli_slim/schemas/transfer.json +48 -0
  245. comfy_cli_slim/schemas/update.json +25 -0
  246. comfy_cli_slim/schemas/version.json +10 -0
  247. comfy_cli_slim/schemas/which.json +12 -0
  248. comfy_cli_slim/schemas/widget_catalog.json +87 -0
  249. comfy_cli_slim/schemas/workflow.json +87 -0
  250. comfy_cli_slim/selector.py +432 -0
  251. comfy_cli_slim/skills/__init__.py +757 -0
  252. comfy_cli_slim/skills/comfy/SKILL.md +1382 -0
  253. comfy_cli_slim/skills/comfy/__init__.py +0 -0
  254. comfy_cli_slim/skills/comfy-agent-permissions/SKILL.md +159 -0
  255. comfy_cli_slim/skills/comfy-build/SKILL.md +412 -0
  256. comfy_cli_slim/skills/comfy-build-authoring/SKILL.md +327 -0
  257. comfy_cli_slim/skills/comfy-build-failures/SKILL.md +234 -0
  258. comfy_cli_slim/skills/comfy-build-pins/SKILL.md +187 -0
  259. comfy_cli_slim/skills/comfy-custom-nodes/SKILL.md +59 -0
  260. comfy_cli_slim/skills/comfy-custom-nodes-api-schema/SKILL.md +14 -0
  261. comfy_cli_slim/skills/comfy-custom-nodes-caching/SKILL.md +14 -0
  262. comfy_cli_slim/skills/comfy-custom-nodes-dependencies/SKILL.md +11 -0
  263. comfy_cli_slim/skills/comfy-custom-nodes-frontend/SKILL.md +14 -0
  264. comfy_cli_slim/skills/comfy-custom-nodes-invocation/SKILL.md +16 -0
  265. comfy_cli_slim/skills/comfy-custom-nodes-migration/SKILL.md +14 -0
  266. comfy_cli_slim/skills/comfy-custom-nodes-packaging/SKILL.md +14 -0
  267. comfy_cli_slim/skills/comfy-custom-nodes-search/SKILL.md +12 -0
  268. comfy_cli_slim/skills/comfy-custom-nodes-troubleshooting/SKILL.md +14 -0
  269. comfy_cli_slim/skills/comfy-custom-nodes-validation/SKILL.md +15 -0
  270. comfy_cli_slim/skills/comfy-debug/SKILL.md +177 -0
  271. comfy_cli_slim/skills/comfy-deploy/SKILL.md +478 -0
  272. comfy_cli_slim/skills/comfy-deploy-failures/SKILL.md +186 -0
  273. comfy_cli_slim/skills/comfy-director/SKILL.md +130 -0
  274. comfy_cli_slim/skills/comfy-relay/SKILL.md +153 -0
  275. comfy_cli_slim/skills/command.py +476 -0
  276. comfy_cli_slim/standalone.py +207 -0
  277. comfy_cli_slim/target.py +139 -0
  278. comfy_cli_slim/tracking.py +1005 -0
  279. comfy_cli_slim/typing.py +3 -0
  280. comfy_cli_slim/ui.py +200 -0
  281. comfy_cli_slim/update.py +87 -0
  282. comfy_cli_slim/utils.py +315 -0
  283. comfy_cli_slim/uv.py +571 -0
  284. comfy_cli_slim/where.py +250 -0
  285. comfy_cli_slim/workflow_ops.py +4737 -0
  286. comfy_cli_slim/workflow_print.py +2117 -0
  287. comfy_cli_slim/workflow_to_api.py +2539 -0
  288. comfy_cli_slim/workspace_manager.py +418 -0
  289. comfy_cli_slim-1.23.0.post2.dist-info/METADATA +82 -0
  290. comfy_cli_slim-1.23.0.post2.dist-info/RECORD +294 -0
  291. comfy_cli_slim-1.23.0.post2.dist-info/WHEEL +5 -0
  292. comfy_cli_slim-1.23.0.post2.dist-info/entry_points.txt +2 -0
  293. comfy_cli_slim-1.23.0.post2.dist-info/licenses/LICENSE +674 -0
  294. comfy_cli_slim-1.23.0.post2.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,172 @@
1
+ """Console-script entrypoint for `comfy` / `comfy-cli` / `comfycli`.
2
+
3
+ A thin wrapper around `comfy_cli_slim.cmdline.main` whose only job is to make the
4
+ process a well-behaved pipe producer. When the consumer of our stdout goes away
5
+ early — `comfy run --wait … | head`, `comfy jobs ls | head -5` — the next write
6
+ raises `BrokenPipeError`. The right answer is a silent exit 0: the consumer got
7
+ exactly what it asked for. Without this wrapper the process instead exits 1 (or,
8
+ when the failing write is the interpreter's own shutdown flush, prints an
9
+ "Exception ignored" traceback), which reads as a command failure to every
10
+ shell/CI/agent downstream.
11
+
12
+ The exit 0 is only ever granted to a command that *succeeded*. A closed reader
13
+ on a command that failed still exits with the failure's code — `comfy --json
14
+ <failing-cmd> | head` is a failure that happened to be piped, not a success.
15
+ Absorbing the broken pipe there would launder a genuine error into 0, which is
16
+ the one outcome worse than the exit 1 this wrapper exists to remove.
17
+
18
+ Output written before the pipe closed is of course truncated — a half-written
19
+ `--json` envelope stays half-written. That is the consumer's problem by
20
+ construction; the exit code is ours.
21
+ """
22
+
23
+ import os
24
+ import sys
25
+
26
+ from comfy_cli_slim.cmdline import main as _cli_main
27
+
28
+
29
+ def _silence_stdout() -> None:
30
+ """Point fd 1 at /dev/null so nothing later can hit EPIPE a second time.
31
+
32
+ Between here and process death an atexit hook, a `finally` that prints, or
33
+ the interpreter's own shutdown flush can all write to stdout. Once its
34
+ reader is gone each of those turns into a fresh `BrokenPipeError` — an
35
+ "Exception ignored" traceback on stderr, and (for the shutdown flush) exit
36
+ 120 in place of whatever code we meant to report.
37
+ """
38
+ try:
39
+ devnull_fd = os.open(os.devnull, os.O_WRONLY)
40
+ except OSError: # pragma: no cover — /dev/null is not openable
41
+ return
42
+ try:
43
+ os.dup2(devnull_fd, sys.stdout.fileno())
44
+ except (OSError, ValueError, AttributeError):
45
+ # No usable fd 1 (already closed, or stdout was replaced by something
46
+ # without a real file descriptor). Nothing to protect.
47
+ pass
48
+ finally:
49
+ # dup2 gives fd 1 its own reference, so this one is ours to release.
50
+ os.close(devnull_fd)
51
+
52
+
53
+ def _exit_quietly_on_broken_pipe() -> None:
54
+ """Absorb a closed stdout and exit 0. Never returns."""
55
+ _silence_stdout()
56
+ # os._exit rather than sys.exit: it skips atexit handlers and stdio
57
+ # flushing entirely, so no buffered-but-unwritable stdout can resurrect the
58
+ # error we just absorbed. The cost is that the telemetry atexit flush in
59
+ # comfy_cli_slim.tracking is skipped for this invocation, which is the intended
60
+ # trade — a truncated pipe is not worth risking a second failure or a
61
+ # shutdown hang on the way out.
62
+ os._exit(0)
63
+
64
+
65
+ def _broken_pipe_swallowed_by_click() -> bool:
66
+ """Report whether the `SystemExit` we just caught is click's EPIPE bail-out.
67
+
68
+ click's `BaseCommand.main` handles EPIPE itself: it swaps `sys.stdout` and
69
+ `sys.stderr` for a `PacifyFlushWrapper` and raises `SystemExit(1)`. So on
70
+ the common path a broken pipe never reaches us as a `BrokenPipeError` at
71
+ all, and the exit 1 is otherwise indistinguishable from a genuine command
72
+ failure. That wrapper — which click installs in this branch and nowhere
73
+ else — is the one reliable in-process signal.
74
+
75
+ The class is matched by name and owning package because there is no single
76
+ class to point at: typer >= 0.24 runs on a vendored click, so the wrapper the
77
+ CLI actually meets is `typer._click.utils.PacifyFlushWrapper` rather than the
78
+ one in the installed `click`, and click 8.5 renamed its own copy to a private
79
+ name whose public alias warns on access. The package test keeps a same-named
80
+ class from anywhere else — `my_click_adapter.PacifyFlushWrapper` — out, since
81
+ a false positive here reports a failed command as a success.
82
+ """
83
+ cls = type(sys.stdout)
84
+ if cls.__name__.removeprefix("_") != "PacifyFlushWrapper":
85
+ return False
86
+ return cls.__module__.partition(".")[0] in ("click", "typer")
87
+
88
+
89
+ def _flush_stdout(*, unwinding: bool) -> bool:
90
+ """Flush stdout; return False iff the flush hit a broken pipe.
91
+
92
+ Reporting the broken pipe by return value instead of raising is what lets
93
+ `main` keep the two cases apart. This flush runs at a point where a real
94
+ exception may already be on its way out, and a `BrokenPipeError` raised
95
+ from here would silently *replace* it — turning a failed command's non-zero
96
+ exit into 0.
97
+
98
+ `unwinding` says whether that is the case. While unwinding, every other
99
+ flush failure is swallowed too, for the same reason: a `finally`-time flush
100
+ must never mask the exception it is running underneath. On the clean path
101
+ there is nothing to mask, so a non-broken-pipe `OSError` (a full disk, EIO)
102
+ is allowed to propagate — output was genuinely lost and a real traceback
103
+ beats the interpreter's opaque exit 120. Only the nothing-to-flush shapes
104
+ stay quiet either way.
105
+ """
106
+ stream = sys.stdout
107
+ if stream is None: # stdout detached (pythonw and friends)
108
+ return True
109
+ try:
110
+ stream.flush()
111
+ except BrokenPipeError:
112
+ return False
113
+ except (ValueError, AttributeError):
114
+ # Closed or non-file-like stdout (`comfy … >&-`) — nothing to flush,
115
+ # and not the broken-pipe case we are guarding against.
116
+ pass
117
+ except OSError:
118
+ if not unwinding:
119
+ raise
120
+ return True
121
+
122
+
123
+ def _is_clean_exit(exc: SystemExit) -> bool:
124
+ """Whether this `SystemExit` reports success (`sys.exit(0)` / `sys.exit()`)."""
125
+ return exc.code is None or exc.code == 0
126
+
127
+
128
+ def main() -> None:
129
+ try:
130
+ _cli_main()
131
+ except BrokenPipeError:
132
+ # click's standalone `main` converts every EPIPE raised inside a command
133
+ # into the pacified `SystemExit(1)` handled just below, so a raw
134
+ # `BrokenPipeError` reaching here is stdout dying outside that guard.
135
+ _exit_quietly_on_broken_pipe()
136
+ except SystemExit as exc:
137
+ # `exc.code == 1` narrows the shortcut to the only code click's EPIPE
138
+ # branch raises, so no other exit status can be laundered into a 0 by a
139
+ # stdout that merely looks pacified.
140
+ if exc.code == 1 and _broken_pipe_swallowed_by_click():
141
+ _exit_quietly_on_broken_pipe()
142
+ # stdout is block-buffered when it is a pipe, so the write that actually
143
+ # lands on the closed reader is frequently the interpreter's shutdown
144
+ # flush — long after any handler here could run. Flushing now pulls that
145
+ # failure forward to where we can still decide what it means.
146
+ clean = _is_clean_exit(exc)
147
+ if _flush_stdout(unwinding=not clean):
148
+ raise
149
+ if clean:
150
+ # The command succeeded and its reader left early: that is the whole
151
+ # story, so exit 0.
152
+ _exit_quietly_on_broken_pipe()
153
+ # The command failed. Its exit code is the truth and outranks the closed
154
+ # reader; only silence fd 1 so the shutdown flush cannot re-raise what
155
+ # we just absorbed (which would replace that code with 120).
156
+ _silence_stdout()
157
+ raise
158
+ except BaseException:
159
+ # A crash is already on its way out. Same rule: flush what we can, let
160
+ # nothing from the flush displace it.
161
+ if not _flush_stdout(unwinding=True):
162
+ _silence_stdout()
163
+ raise
164
+ else:
165
+ # `_cli_main` returned instead of exiting. click always raises in
166
+ # standalone mode, but `cmdline.main` can fall through on rc == 0.
167
+ if not _flush_stdout(unwinding=False):
168
+ _exit_quietly_on_broken_pipe()
169
+
170
+
171
+ if __name__ == "__main__": # pragma: nocover
172
+ main()
@@ -0,0 +1,188 @@
1
+ """Lazy-import helpers for the CLI's startup path.
2
+
3
+ Every ``comfy`` invocation pays ``cmdline.py``'s import graph before doing
4
+ any work, and agents pay it on every tool call. Most of that graph is the
5
+ subcommand modules, of which a single invocation uses one. These helpers let
6
+ ``cmdline.py`` keep its flat, module-level names (tests patch
7
+ ``comfy_cli_slim.cmdline.run_inner`` and friends) while deferring the actual import
8
+ to first use.
9
+
10
+ Two pieces:
11
+
12
+ * :class:`LazyModule` — a module proxy. Attribute reads import the module on
13
+ first touch; reads, writes and deletes all forward to the real module, so
14
+ ``patch("comfy_cli_slim.cmdline.run_inner.execute")`` patches the real function
15
+ exactly as it did when ``run_inner`` was the module itself.
16
+ * :class:`LazyTyperGroup` — a ``TyperGroup`` whose subgroups are declared as
17
+ ``(module, attr)`` pairs and built on first lookup with typer's own
18
+ ``get_group_from_info``, so help text, hidden flags and callbacks behave
19
+ exactly as ``app.add_typer(...)`` did. ``list_commands`` keeps the eager
20
+ commands first and the lazy groups in table order, so ``comfy --help`` is
21
+ byte-identical to the eager wiring.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import importlib
27
+ import inspect
28
+ from collections.abc import Callable
29
+ from dataclasses import dataclass
30
+ from types import ModuleType
31
+ from typing import TYPE_CHECKING, Any
32
+
33
+ import typer
34
+ from typer.core import TyperGroup
35
+ from typer.models import CommandInfo, TyperInfo
36
+
37
+ if TYPE_CHECKING:
38
+ import click
39
+
40
+
41
+ class LazyModule:
42
+ """Proxy for a module that is imported on first attribute access.
43
+
44
+ ``get``/``set``/``del`` of attributes all forward to the real module, and
45
+ ``__dict__`` is the real module's namespace, so ``unittest.mock.patch``
46
+ and ``monkeypatch.setattr`` treat the proxy as the module.
47
+ """
48
+
49
+ __slots__ = ("_lazy_path", "_lazy_target")
50
+
51
+ def __init__(self, path: str) -> None:
52
+ object.__setattr__(self, "_lazy_path", path)
53
+ object.__setattr__(self, "_lazy_target", None)
54
+
55
+ def _lazy_resolve(self) -> ModuleType:
56
+ target = object.__getattribute__(self, "_lazy_target")
57
+ if target is None:
58
+ target = importlib.import_module(object.__getattribute__(self, "_lazy_path"))
59
+ object.__setattr__(self, "_lazy_target", target)
60
+ return target
61
+
62
+ @property
63
+ def __dict__(self) -> dict[str, Any]: # type: ignore[override]
64
+ return self._lazy_resolve().__dict__
65
+
66
+ def __getattr__(self, name: str) -> Any:
67
+ return getattr(self._lazy_resolve(), name)
68
+
69
+ def __setattr__(self, name: str, value: Any) -> None:
70
+ setattr(self._lazy_resolve(), name, value)
71
+
72
+ def __delattr__(self, name: str) -> None:
73
+ delattr(self._lazy_resolve(), name)
74
+
75
+ def __dir__(self) -> list[str]:
76
+ return dir(self._lazy_resolve())
77
+
78
+ def __repr__(self) -> str:
79
+ return f"<LazyModule {object.__getattribute__(self, '_lazy_path')!r}>"
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class LazySubcommand:
84
+ """One deferred ``app.add_typer(...)``: the module holding the sub-``Typer``
85
+ and the ``add_typer`` keyword arguments that were passed with it."""
86
+
87
+ module: str
88
+ attr: str = "app"
89
+ help: str | None = None
90
+ hidden: bool = False
91
+ callback: Callable[..., Any] | None = None
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class LazyCommand:
96
+ """One deferred ``app.command(name, ...)(fn)``. ``attr`` names the function
97
+ in ``module``. ``register`` instead names a ``register_with(app)`` hook on
98
+ the module that registers the command onto a Typer app itself (``generate``
99
+ does this because its command is a closure)."""
100
+
101
+ module: str
102
+ attr: str | None = None
103
+ register: str | None = None
104
+ help: str | None = None
105
+ hidden: bool = False
106
+
107
+
108
+ class LazyTyperGroup(TyperGroup):
109
+ """Root group with subcommands declared in ``lazy_subcommands`` and built
110
+ on first lookup. Set the table on a subclass::
111
+
112
+ class RootGroup(LazyTyperGroup):
113
+ lazy_subcommands = {"nodes": LazySubcommand("comfy_cli_slim.command.nodes", help="...")}
114
+
115
+ app = typer.Typer(cls=RootGroup)
116
+ """
117
+
118
+ lazy_subcommands: dict[str, LazySubcommand | LazyCommand] = {}
119
+ # Assigned by the module that owns the root app
120
+ # (``_RootGroup.pretty_exceptions_short = app.pretty_exceptions_short`` in
121
+ # cmdline.py). The fallback is read off ``typer.Typer`` rather than
122
+ # restated here, so it cannot drift from typer.
123
+ pretty_exceptions_short: bool = (
124
+ inspect.signature(typer.Typer.__init__).parameters["pretty_exceptions_short"].default
125
+ )
126
+
127
+ def __init__(self, **attrs: Any) -> None:
128
+ super().__init__(**attrs)
129
+ self._lazy_loaded: dict[str, click.Command] = {}
130
+
131
+ def list_commands(self, ctx: click.Context) -> list[str]:
132
+ eager = list(super().list_commands(ctx))
133
+ return eager + [name for name in self.lazy_subcommands if name not in self.commands]
134
+
135
+ def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
136
+ command = super().get_command(ctx, cmd_name)
137
+ if command is not None:
138
+ return command
139
+ spec = self.lazy_subcommands.get(cmd_name)
140
+ if spec is None:
141
+ return None
142
+ if cmd_name not in self._lazy_loaded:
143
+ build = self._build_command if isinstance(spec, LazyCommand) else self._build_group
144
+ self._lazy_loaded[cmd_name] = build(cmd_name, spec)
145
+ return self._lazy_loaded[cmd_name]
146
+
147
+ def _factory_kwargs(self, factory: Callable[..., Any]) -> dict[str, Any]:
148
+ """The settings typer's own wiring passes to ``factory``, narrowed to the
149
+ keywords this typer version actually accepts. ``suggest_commands`` only
150
+ exists from typer 0.20 on, where it is required and has no default, while
151
+ pyproject still allows ``typer>=0.12.5``."""
152
+ candidates = {
153
+ "pretty_exceptions_short": self.pretty_exceptions_short,
154
+ "rich_markup_mode": self.rich_markup_mode,
155
+ "suggest_commands": getattr(self, "suggest_commands", True),
156
+ }
157
+ accepted = inspect.signature(factory).parameters
158
+ return {name: value for name, value in candidates.items() if name in accepted}
159
+
160
+ def _build_command(self, name: str, spec: LazyCommand) -> click.Command:
161
+ module = importlib.import_module(spec.module)
162
+ if spec.register is not None:
163
+ scratch = typer.Typer()
164
+ getattr(module, spec.register)(scratch)
165
+ infos = [info for info in scratch.registered_commands if info.name == name]
166
+ assert len(infos) == 1, f"{spec.module}.{spec.register} did not register exactly one {name!r} command"
167
+ info = infos[0]
168
+ if spec.hidden:
169
+ info.hidden = True
170
+ else:
171
+ assert spec.attr, "LazyCommand needs attr or register"
172
+ info = CommandInfo(name=name, callback=getattr(module, spec.attr), help=spec.help, hidden=spec.hidden)
173
+ return typer.main.get_command_from_info(info, **self._factory_kwargs(typer.main.get_command_from_info))
174
+
175
+ def _build_group(self, name: str, spec: LazySubcommand) -> click.Command:
176
+ sub_app = getattr(importlib.import_module(spec.module), spec.attr)
177
+ # Same TyperInfo ``add_typer`` would have appended: only the keywords
178
+ # that were given, so the sub-app's own settings fill the rest.
179
+ kwargs: dict[str, Any] = {"name": name}
180
+ if spec.help is not None:
181
+ kwargs["help"] = spec.help
182
+ if spec.hidden:
183
+ kwargs["hidden"] = True
184
+ if spec.callback is not None:
185
+ kwargs["callback"] = spec.callback
186
+ return typer.main.get_group_from_info(
187
+ TyperInfo(sub_app, **kwargs), **self._factory_kwargs(typer.main.get_group_from_info)
188
+ )