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.
- comfy_cli_slim/__init__.py +0 -0
- comfy_cli_slim/__main__.py +172 -0
- comfy_cli_slim/_lazy.py +188 -0
- comfy_cli_slim/_safe_exec.py +344 -0
- comfy_cli_slim/_security_scan.py +219 -0
- comfy_cli_slim/agent/__init__.py +463 -0
- comfy_cli_slim/agent/command.py +569 -0
- comfy_cli_slim/auth/__init__.py +26 -0
- comfy_cli_slim/auth/command.py +157 -0
- comfy_cli_slim/auth/store.py +348 -0
- comfy_cli_slim/builder_api.py +465 -0
- comfy_cli_slim/builder_pagination.py +72 -0
- comfy_cli_slim/caller.py +166 -0
- comfy_cli_slim/cancellation.py +125 -0
- comfy_cli_slim/cloud/__init__.py +140 -0
- comfy_cli_slim/cloud/billing.py +436 -0
- comfy_cli_slim/cloud/command.py +728 -0
- comfy_cli_slim/cloud/oauth.py +1001 -0
- comfy_cli_slim/cmdline.py +3142 -0
- comfy_cli_slim/comfy_client.py +728 -0
- comfy_cli_slim/command/__init__.py +54 -0
- comfy_cli_slim/command/_cloud_errors.py +266 -0
- comfy_cli_slim/command/assets_library.py +338 -0
- comfy_cli_slim/command/build.py +3632 -0
- comfy_cli_slim/command/build_diff.py +342 -0
- comfy_cli_slim/command/build_digest_cache.py +97 -0
- comfy_cli_slim/command/build_package.py +269 -0
- comfy_cli_slim/command/build_paths.py +207 -0
- comfy_cli_slim/command/build_pull.py +392 -0
- comfy_cli_slim/command/build_push.py +369 -0
- comfy_cli_slim/command/build_spec.py +277 -0
- comfy_cli_slim/command/build_targets.py +118 -0
- comfy_cli_slim/command/build_upload_progress.py +369 -0
- comfy_cli_slim/command/build_validation.py +562 -0
- comfy_cli_slim/command/cloud_http.py +148 -0
- comfy_cli_slim/command/code_search.py +770 -0
- comfy_cli_slim/command/custom_nodes/__init__.py +3 -0
- comfy_cli_slim/command/custom_nodes/bisect_custom_nodes.py +233 -0
- comfy_cli_slim/command/custom_nodes/cm_cli_util.py +294 -0
- comfy_cli_slim/command/custom_nodes/command.py +1460 -0
- comfy_cli_slim/command/deploy.py +589 -0
- comfy_cli_slim/command/deploy_cancel.py +195 -0
- comfy_cli_slim/command/deploy_compute.py +48 -0
- comfy_cli_slim/command/deploy_lifecycle.py +161 -0
- comfy_cli_slim/command/deploy_ls.py +125 -0
- comfy_cli_slim/command/deploy_progress.py +388 -0
- comfy_cli_slim/command/deploy_promote.py +55 -0
- comfy_cli_slim/command/deploy_read.py +289 -0
- comfy_cli_slim/command/deploy_refs.py +120 -0
- comfy_cli_slim/command/deploy_rename.py +110 -0
- comfy_cli_slim/command/deploy_resolve.py +545 -0
- comfy_cli_slim/command/deploy_rollback.py +291 -0
- comfy_cli_slim/command/deploy_run.py +326 -0
- comfy_cli_slim/command/deploy_run_assets.py +90 -0
- comfy_cli_slim/command/deploy_run_watch.py +35 -0
- comfy_cli_slim/command/deploy_runtime.py +227 -0
- comfy_cli_slim/command/deploy_status.py +516 -0
- comfy_cli_slim/command/deploy_types.py +381 -0
- comfy_cli_slim/command/deploy_up.py +780 -0
- comfy_cli_slim/command/deploy_workflow.py +424 -0
- comfy_cli_slim/command/generate/__init__.py +3 -0
- comfy_cli_slim/command/generate/adapters.py +318 -0
- comfy_cli_slim/command/generate/app.py +1302 -0
- comfy_cli_slim/command/generate/client.py +226 -0
- comfy_cli_slim/command/generate/emit.py +674 -0
- comfy_cli_slim/command/generate/output.py +134 -0
- comfy_cli_slim/command/generate/poll.py +407 -0
- comfy_cli_slim/command/generate/schema.py +295 -0
- comfy_cli_slim/command/generate/spec/openapi.yml +1 -0
- comfy_cli_slim/command/generate/spec.py +1285 -0
- comfy_cli_slim/command/generate/upload.py +128 -0
- comfy_cli_slim/command/github/pr_info.py +16 -0
- comfy_cli_slim/command/install.py +1317 -0
- comfy_cli_slim/command/job_watcher.py +534 -0
- comfy_cli_slim/command/jobs.py +3021 -0
- comfy_cli_slim/command/knowledge.py +416 -0
- comfy_cli_slim/command/launch.py +1098 -0
- comfy_cli_slim/command/models/models.py +2266 -0
- comfy_cli_slim/command/models/search.py +843 -0
- comfy_cli_slim/command/node_deps.py +779 -0
- comfy_cli_slim/command/nodes.py +1472 -0
- comfy_cli_slim/command/outdated.py +550 -0
- comfy_cli_slim/command/pack_scan.py +44 -0
- comfy_cli_slim/command/pr_command.py +92 -0
- comfy_cli_slim/command/preview.py +323 -0
- comfy_cli_slim/command/project.py +369 -0
- comfy_cli_slim/command/run/__init__.py +1530 -0
- comfy_cli_slim/command/run/execution.py +686 -0
- comfy_cli_slim/command/run/loader.py +132 -0
- comfy_cli_slim/command/run/preflight.py +243 -0
- comfy_cli_slim/command/run/watcher.py +183 -0
- comfy_cli_slim/command/run_cli.py +571 -0
- comfy_cli_slim/command/setup.py +675 -0
- comfy_cli_slim/command/stop_port.py +673 -0
- comfy_cli_slim/command/system.py +151 -0
- comfy_cli_slim/command/templates.py +2161 -0
- comfy_cli_slim/command/transfer.py +1003 -0
- comfy_cli_slim/command/version_validators.py +60 -0
- comfy_cli_slim/command/workflow.py +2247 -0
- comfy_cli_slim/command/workflow_edit.py +1195 -0
- comfy_cli_slim/command/workflow_fragments.py +504 -0
- comfy_cli_slim/command_mentions.py +188 -0
- comfy_cli_slim/config_manager.py +215 -0
- comfy_cli_slim/constants.py +129 -0
- comfy_cli_slim/cql/__init__.py +10 -0
- comfy_cli_slim/cql/_net.py +28 -0
- comfy_cli_slim/cql/annotations_source.py +640 -0
- comfy_cli_slim/cql/data/__init__.py +1 -0
- comfy_cli_slim/cql/data/cloud_disable_config.yaml +15 -0
- comfy_cli_slim/cql/data/default_text2img.json +66 -0
- comfy_cli_slim/cql/data/supported_nodes.yaml +1107 -0
- comfy_cli_slim/cql/default_workflow.py +384 -0
- comfy_cli_slim/cql/engine.py +5549 -0
- comfy_cli_slim/cql/errors.py +26 -0
- comfy_cli_slim/cql/loader.py +271 -0
- comfy_cli_slim/cql/promoted.py +2362 -0
- comfy_cli_slim/cql/widget_catalog.py +184 -0
- comfy_cli_slim/credentials.py +444 -0
- comfy_cli_slim/cuda_detect.py +158 -0
- comfy_cli_slim/deploy_api.py +330 -0
- comfy_cli_slim/deploy_api_errors.py +279 -0
- comfy_cli_slim/deploy_assets.py +271 -0
- comfy_cli_slim/deploy_download.py +275 -0
- comfy_cli_slim/deploy_events.py +314 -0
- comfy_cli_slim/deploy_jobs.py +302 -0
- comfy_cli_slim/deprecation.py +117 -0
- comfy_cli_slim/discovery.py +336 -0
- comfy_cli_slim/download_state.py +1284 -0
- comfy_cli_slim/env_checker.py +227 -0
- comfy_cli_slim/error_codes.py +1864 -0
- comfy_cli_slim/execution_errors.py +159 -0
- comfy_cli_slim/file_utils.py +1109 -0
- comfy_cli_slim/fragments.py +1064 -0
- comfy_cli_slim/git_utils.py +234 -0
- comfy_cli_slim/hardware.py +335 -0
- comfy_cli_slim/hashing.py +14 -0
- comfy_cli_slim/help_json.py +183 -0
- comfy_cli_slim/host_port.py +238 -0
- comfy_cli_slim/http.py +487 -0
- comfy_cli_slim/interaction.py +338 -0
- comfy_cli_slim/jobs_state.py +322 -0
- comfy_cli_slim/knowledge.py +1296 -0
- comfy_cli_slim/layout.py +633 -0
- comfy_cli_slim/layout_quality.py +219 -0
- comfy_cli_slim/link_integrity.py +204 -0
- comfy_cli_slim/local_address.py +214 -0
- comfy_cli_slim/locking.py +154 -0
- comfy_cli_slim/logging.py +44 -0
- comfy_cli_slim/model_variants.py +299 -0
- comfy_cli_slim/onboarding.py +38 -0
- comfy_cli_slim/output/__init__.py +44 -0
- comfy_cli_slim/output/branding.py +437 -0
- comfy_cli_slim/output/glyphs.py +76 -0
- comfy_cli_slim/output/panels.py +527 -0
- comfy_cli_slim/output/preview.py +170 -0
- comfy_cli_slim/output/progress.py +68 -0
- comfy_cli_slim/output/renderer.py +551 -0
- comfy_cli_slim/output/rich_compat.py +42 -0
- comfy_cli_slim/output/sanitize.py +246 -0
- comfy_cli_slim/pr_cache.py +234 -0
- comfy_cli_slim/project.py +220 -0
- comfy_cli_slim/registry/__init__.py +15 -0
- comfy_cli_slim/registry/api.py +294 -0
- comfy_cli_slim/registry/config_parser.py +810 -0
- comfy_cli_slim/registry/types.py +82 -0
- comfy_cli_slim/resolve_python.py +76 -0
- comfy_cli_slim/schemas/__init__.py +6 -0
- comfy_cli_slim/schemas/agent.json +298 -0
- comfy_cli_slim/schemas/assets.json +33 -0
- comfy_cli_slim/schemas/assets_library.json +34 -0
- comfy_cli_slim/schemas/auth.json +30 -0
- comfy_cli_slim/schemas/build_blob_ls.json +12 -0
- comfy_cli_slim/schemas/build_delete.json +12 -0
- comfy_cli_slim/schemas/build_init.json +104 -0
- comfy_cli_slim/schemas/build_ls.json +22 -0
- comfy_cli_slim/schemas/build_pull.json +69 -0
- comfy_cli_slim/schemas/build_push.json +88 -0
- comfy_cli_slim/schemas/build_push_event.json +58 -0
- comfy_cli_slim/schemas/build_refs_base_images.json +9 -0
- comfy_cli_slim/schemas/build_refs_build_targets.json +9 -0
- comfy_cli_slim/schemas/build_refs_model_dirs.json +9 -0
- comfy_cli_slim/schemas/build_refs_resolve.json +9 -0
- comfy_cli_slim/schemas/build_release_create.json +27 -0
- comfy_cli_slim/schemas/build_release_delete.json +12 -0
- comfy_cli_slim/schemas/build_release_logs.json +16 -0
- comfy_cli_slim/schemas/build_release_ls.json +24 -0
- comfy_cli_slim/schemas/build_release_manifest.json +9 -0
- comfy_cli_slim/schemas/build_release_show.json +13 -0
- comfy_cli_slim/schemas/build_show.json +13 -0
- comfy_cli_slim/schemas/build_status.json +153 -0
- comfy_cli_slim/schemas/build_update.json +69 -0
- comfy_cli_slim/schemas/build_validate.json +14 -0
- comfy_cli_slim/schemas/cloud_status.json +127 -0
- comfy_cli_slim/schemas/deploy_cancel.json +116 -0
- comfy_cli_slim/schemas/deploy_delete.json +14 -0
- comfy_cli_slim/schemas/deploy_events.json +40 -0
- comfy_cli_slim/schemas/deploy_history.json +68 -0
- comfy_cli_slim/schemas/deploy_logs.json +14 -0
- comfy_cli_slim/schemas/deploy_ls.json +48 -0
- comfy_cli_slim/schemas/deploy_progress_event.json +98 -0
- comfy_cli_slim/schemas/deploy_promote.json +101 -0
- comfy_cli_slim/schemas/deploy_refs_compute.json +39 -0
- comfy_cli_slim/schemas/deploy_rename.json +39 -0
- comfy_cli_slim/schemas/deploy_rollback.json +86 -0
- comfy_cli_slim/schemas/deploy_run.json +77 -0
- comfy_cli_slim/schemas/deploy_scale.json +24 -0
- comfy_cli_slim/schemas/deploy_show.json +21 -0
- comfy_cli_slim/schemas/deploy_start.json +13 -0
- comfy_cli_slim/schemas/deploy_status.json +142 -0
- comfy_cli_slim/schemas/deploy_stop.json +13 -0
- comfy_cli_slim/schemas/deploy_up.json +116 -0
- comfy_cli_slim/schemas/discover.json +86 -0
- comfy_cli_slim/schemas/download.json +32 -0
- comfy_cli_slim/schemas/download_status.json +52 -0
- comfy_cli_slim/schemas/downloads.json +16 -0
- comfy_cli_slim/schemas/env.json +79 -0
- comfy_cli_slim/schemas/envelope.json +49 -0
- comfy_cli_slim/schemas/error.json +27 -0
- comfy_cli_slim/schemas/error_codes.md +28 -0
- comfy_cli_slim/schemas/free.json +21 -0
- comfy_cli_slim/schemas/generate_list.json +70 -0
- comfy_cli_slim/schemas/generate_result.json +19 -0
- comfy_cli_slim/schemas/generate_schema.json +96 -0
- comfy_cli_slim/schemas/help.json +63 -0
- comfy_cli_slim/schemas/jobs.json +98 -0
- comfy_cli_slim/schemas/jobs_wait.json +35 -0
- comfy_cli_slim/schemas/knowledge.json +75 -0
- comfy_cli_slim/schemas/knowledge_block.json +81 -0
- comfy_cli_slim/schemas/launch.json +15 -0
- comfy_cli_slim/schemas/logs.json +42 -0
- comfy_cli_slim/schemas/models.json +92 -0
- comfy_cli_slim/schemas/node_deps.json +124 -0
- comfy_cli_slim/schemas/nodes.json +50 -0
- comfy_cli_slim/schemas/outdated.json +55 -0
- comfy_cli_slim/schemas/preview.json +19 -0
- comfy_cli_slim/schemas/project.json +60 -0
- comfy_cli_slim/schemas/run.json +65 -0
- comfy_cli_slim/schemas/run_event.json +71 -0
- comfy_cli_slim/schemas/set_default.json +11 -0
- comfy_cli_slim/schemas/skill.json +34 -0
- comfy_cli_slim/schemas/stop.json +38 -0
- comfy_cli_slim/schemas/system_stats.json +35 -0
- comfy_cli_slim/schemas/templates.json +78 -0
- comfy_cli_slim/schemas/transfer.json +48 -0
- comfy_cli_slim/schemas/update.json +25 -0
- comfy_cli_slim/schemas/version.json +10 -0
- comfy_cli_slim/schemas/which.json +12 -0
- comfy_cli_slim/schemas/widget_catalog.json +87 -0
- comfy_cli_slim/schemas/workflow.json +87 -0
- comfy_cli_slim/selector.py +432 -0
- comfy_cli_slim/skills/__init__.py +757 -0
- comfy_cli_slim/skills/comfy/SKILL.md +1382 -0
- comfy_cli_slim/skills/comfy/__init__.py +0 -0
- comfy_cli_slim/skills/comfy-agent-permissions/SKILL.md +159 -0
- comfy_cli_slim/skills/comfy-build/SKILL.md +412 -0
- comfy_cli_slim/skills/comfy-build-authoring/SKILL.md +327 -0
- comfy_cli_slim/skills/comfy-build-failures/SKILL.md +234 -0
- comfy_cli_slim/skills/comfy-build-pins/SKILL.md +187 -0
- comfy_cli_slim/skills/comfy-custom-nodes/SKILL.md +59 -0
- comfy_cli_slim/skills/comfy-custom-nodes-api-schema/SKILL.md +14 -0
- comfy_cli_slim/skills/comfy-custom-nodes-caching/SKILL.md +14 -0
- comfy_cli_slim/skills/comfy-custom-nodes-dependencies/SKILL.md +11 -0
- comfy_cli_slim/skills/comfy-custom-nodes-frontend/SKILL.md +14 -0
- comfy_cli_slim/skills/comfy-custom-nodes-invocation/SKILL.md +16 -0
- comfy_cli_slim/skills/comfy-custom-nodes-migration/SKILL.md +14 -0
- comfy_cli_slim/skills/comfy-custom-nodes-packaging/SKILL.md +14 -0
- comfy_cli_slim/skills/comfy-custom-nodes-search/SKILL.md +12 -0
- comfy_cli_slim/skills/comfy-custom-nodes-troubleshooting/SKILL.md +14 -0
- comfy_cli_slim/skills/comfy-custom-nodes-validation/SKILL.md +15 -0
- comfy_cli_slim/skills/comfy-debug/SKILL.md +177 -0
- comfy_cli_slim/skills/comfy-deploy/SKILL.md +478 -0
- comfy_cli_slim/skills/comfy-deploy-failures/SKILL.md +186 -0
- comfy_cli_slim/skills/comfy-director/SKILL.md +130 -0
- comfy_cli_slim/skills/comfy-relay/SKILL.md +153 -0
- comfy_cli_slim/skills/command.py +476 -0
- comfy_cli_slim/standalone.py +207 -0
- comfy_cli_slim/target.py +139 -0
- comfy_cli_slim/tracking.py +1005 -0
- comfy_cli_slim/typing.py +3 -0
- comfy_cli_slim/ui.py +200 -0
- comfy_cli_slim/update.py +87 -0
- comfy_cli_slim/utils.py +315 -0
- comfy_cli_slim/uv.py +571 -0
- comfy_cli_slim/where.py +250 -0
- comfy_cli_slim/workflow_ops.py +4737 -0
- comfy_cli_slim/workflow_print.py +2117 -0
- comfy_cli_slim/workflow_to_api.py +2539 -0
- comfy_cli_slim/workspace_manager.py +418 -0
- comfy_cli_slim-1.23.0.post2.dist-info/METADATA +82 -0
- comfy_cli_slim-1.23.0.post2.dist-info/RECORD +294 -0
- comfy_cli_slim-1.23.0.post2.dist-info/WHEEL +5 -0
- comfy_cli_slim-1.23.0.post2.dist-info/entry_points.txt +2 -0
- comfy_cli_slim-1.23.0.post2.dist-info/licenses/LICENSE +674 -0
- 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()
|
comfy_cli_slim/_lazy.py
ADDED
|
@@ -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
|
+
)
|