comfy-cli 1.23.0__tar.gz → 1.24.0__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 (299) hide show
  1. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/PKG-INFO +1 -1
  2. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/builder_api.py +20 -4
  3. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cloud/command.py +44 -16
  4. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cmdline.py +70 -0
  5. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/comfy_client.py +12 -1
  6. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/build.py +97 -11
  7. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy.py +17 -2
  8. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_ls.py +14 -2
  9. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_types.py +48 -2
  10. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_up.py +124 -18
  11. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/app.py +1 -1
  12. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/client.py +5 -8
  13. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/schema.py +1 -1
  14. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/jobs.py +555 -106
  15. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/node_deps.py +71 -16
  16. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/run/__init__.py +248 -41
  17. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/run/execution.py +70 -3
  18. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/run/preflight.py +18 -2
  19. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/templates.py +3 -1
  20. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/transfer.py +63 -7
  21. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/workflow.py +139 -8
  22. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/workflow_edit.py +8 -5
  23. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cql/engine.py +230 -27
  24. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cql/loader.py +5 -2
  25. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cql/promoted.py +907 -130
  26. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/credentials.py +114 -62
  27. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/deploy_api.py +23 -4
  28. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/deploy_api_errors.py +8 -0
  29. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/error_codes.py +63 -4
  30. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/jobs_state.py +19 -1
  31. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/layout.py +27 -2
  32. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/model_variants.py +48 -5
  33. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_ls.json +4 -0
  34. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/error_codes.md +4 -0
  35. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/jobs.json +8 -3
  36. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/node_deps.json +2 -2
  37. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/run.json +6 -1
  38. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/workflow.json +11 -0
  39. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy/SKILL.md +43 -5
  40. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-debug/SKILL.md +1 -1
  41. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-deploy/SKILL.md +10 -1
  42. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/target.py +3 -2
  43. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/tracking.py +5 -0
  44. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/workflow_ops.py +560 -20
  45. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/workflow_print.py +647 -146
  46. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/workflow_to_api.py +907 -108
  47. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli.egg-info/PKG-INFO +1 -1
  48. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/pyproject.toml +1 -1
  49. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/LICENSE +0 -0
  50. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/README.md +0 -0
  51. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/__init__.py +0 -0
  52. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/__main__.py +0 -0
  53. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/_lazy.py +0 -0
  54. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/_safe_exec.py +0 -0
  55. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/agent/__init__.py +0 -0
  56. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/agent/command.py +0 -0
  57. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/auth/__init__.py +0 -0
  58. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/auth/command.py +0 -0
  59. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/auth/store.py +0 -0
  60. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/builder_pagination.py +0 -0
  61. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/caller.py +0 -0
  62. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cancellation.py +0 -0
  63. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cloud/__init__.py +0 -0
  64. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cloud/billing.py +0 -0
  65. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cloud/oauth.py +0 -0
  66. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/__init__.py +0 -0
  67. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/_cloud_errors.py +0 -0
  68. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/assets_library.py +0 -0
  69. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/build_diff.py +0 -0
  70. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/build_digest_cache.py +0 -0
  71. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/build_package.py +0 -0
  72. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/build_paths.py +0 -0
  73. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/build_pull.py +0 -0
  74. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/build_push.py +0 -0
  75. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/build_spec.py +0 -0
  76. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/build_targets.py +0 -0
  77. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/build_upload_progress.py +0 -0
  78. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/build_validation.py +0 -0
  79. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/cloud_http.py +0 -0
  80. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/code_search.py +0 -0
  81. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/custom_nodes/__init__.py +0 -0
  82. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/custom_nodes/bisect_custom_nodes.py +0 -0
  83. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/custom_nodes/cm_cli_util.py +0 -0
  84. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/custom_nodes/command.py +0 -0
  85. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_cancel.py +0 -0
  86. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_compute.py +0 -0
  87. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_lifecycle.py +0 -0
  88. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_progress.py +0 -0
  89. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_promote.py +0 -0
  90. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_read.py +0 -0
  91. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_refs.py +0 -0
  92. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_rename.py +0 -0
  93. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_resolve.py +0 -0
  94. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_rollback.py +0 -0
  95. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_run.py +0 -0
  96. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_run_assets.py +0 -0
  97. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_run_watch.py +0 -0
  98. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_runtime.py +0 -0
  99. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_status.py +0 -0
  100. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_workflow.py +0 -0
  101. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/__init__.py +0 -0
  102. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/adapters.py +0 -0
  103. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/emit.py +0 -0
  104. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/output.py +0 -0
  105. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/poll.py +0 -0
  106. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/spec/openapi.yml +0 -0
  107. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/spec.py +0 -0
  108. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/upload.py +0 -0
  109. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/github/pr_info.py +0 -0
  110. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/install.py +0 -0
  111. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/job_watcher.py +0 -0
  112. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/knowledge.py +0 -0
  113. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/launch.py +0 -0
  114. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/models/models.py +0 -0
  115. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/models/search.py +0 -0
  116. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/nodes.py +0 -0
  117. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/outdated.py +0 -0
  118. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/pack_scan.py +0 -0
  119. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/pr_command.py +0 -0
  120. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/preview.py +0 -0
  121. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/project.py +0 -0
  122. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/run/loader.py +0 -0
  123. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/run/watcher.py +0 -0
  124. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/run_cli.py +0 -0
  125. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/setup.py +0 -0
  126. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/stop_port.py +0 -0
  127. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/system.py +0 -0
  128. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/version_validators.py +0 -0
  129. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command/workflow_fragments.py +0 -0
  130. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/command_mentions.py +0 -0
  131. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/config_manager.py +0 -0
  132. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/constants.py +0 -0
  133. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cql/__init__.py +0 -0
  134. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cql/_net.py +0 -0
  135. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cql/annotations_source.py +0 -0
  136. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cql/data/__init__.py +0 -0
  137. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cql/data/cloud_disable_config.yaml +0 -0
  138. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cql/data/default_text2img.json +0 -0
  139. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cql/data/supported_nodes.yaml +0 -0
  140. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cql/default_workflow.py +0 -0
  141. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cql/errors.py +0 -0
  142. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cql/widget_catalog.py +0 -0
  143. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/cuda_detect.py +0 -0
  144. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/deploy_assets.py +0 -0
  145. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/deploy_download.py +0 -0
  146. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/deploy_events.py +0 -0
  147. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/deploy_jobs.py +0 -0
  148. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/deprecation.py +0 -0
  149. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/discovery.py +0 -0
  150. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/download_state.py +0 -0
  151. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/env_checker.py +0 -0
  152. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/execution_errors.py +0 -0
  153. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/file_utils.py +0 -0
  154. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/fragments.py +0 -0
  155. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/git_utils.py +0 -0
  156. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/hardware.py +0 -0
  157. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/hashing.py +0 -0
  158. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/help_json.py +0 -0
  159. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/host_port.py +0 -0
  160. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/http.py +0 -0
  161. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/interaction.py +0 -0
  162. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/knowledge.py +0 -0
  163. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/layout_quality.py +0 -0
  164. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/link_integrity.py +0 -0
  165. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/local_address.py +0 -0
  166. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/locking.py +0 -0
  167. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/logging.py +0 -0
  168. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/onboarding.py +0 -0
  169. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/output/__init__.py +0 -0
  170. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/output/branding.py +0 -0
  171. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/output/glyphs.py +0 -0
  172. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/output/panels.py +0 -0
  173. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/output/preview.py +0 -0
  174. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/output/progress.py +0 -0
  175. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/output/renderer.py +0 -0
  176. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/output/rich_compat.py +0 -0
  177. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/output/sanitize.py +0 -0
  178. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/pr_cache.py +0 -0
  179. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/project.py +0 -0
  180. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/registry/__init__.py +0 -0
  181. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/registry/api.py +0 -0
  182. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/registry/config_parser.py +0 -0
  183. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/registry/types.py +0 -0
  184. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/resolve_python.py +0 -0
  185. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/__init__.py +0 -0
  186. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/agent.json +0 -0
  187. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/assets.json +0 -0
  188. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/assets_library.json +0 -0
  189. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/auth.json +0 -0
  190. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_blob_ls.json +0 -0
  191. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_delete.json +0 -0
  192. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_init.json +0 -0
  193. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_ls.json +0 -0
  194. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_pull.json +0 -0
  195. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_push.json +0 -0
  196. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_push_event.json +0 -0
  197. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_refs_base_images.json +0 -0
  198. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_refs_build_targets.json +0 -0
  199. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_refs_model_dirs.json +0 -0
  200. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_refs_resolve.json +0 -0
  201. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_release_create.json +0 -0
  202. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_release_delete.json +0 -0
  203. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_release_logs.json +0 -0
  204. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_release_ls.json +0 -0
  205. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_release_manifest.json +0 -0
  206. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_release_show.json +0 -0
  207. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_show.json +0 -0
  208. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_status.json +0 -0
  209. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_update.json +0 -0
  210. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_validate.json +0 -0
  211. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/cloud_status.json +0 -0
  212. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_cancel.json +0 -0
  213. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_delete.json +0 -0
  214. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_events.json +0 -0
  215. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_history.json +0 -0
  216. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_logs.json +0 -0
  217. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_progress_event.json +0 -0
  218. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_promote.json +0 -0
  219. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_refs_compute.json +0 -0
  220. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_rename.json +0 -0
  221. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_rollback.json +0 -0
  222. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_run.json +0 -0
  223. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_scale.json +0 -0
  224. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_show.json +0 -0
  225. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_start.json +0 -0
  226. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_status.json +0 -0
  227. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_stop.json +0 -0
  228. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_up.json +0 -0
  229. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/discover.json +0 -0
  230. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/download.json +0 -0
  231. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/download_status.json +0 -0
  232. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/downloads.json +0 -0
  233. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/env.json +0 -0
  234. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/envelope.json +0 -0
  235. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/error.json +0 -0
  236. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/free.json +0 -0
  237. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/generate_list.json +0 -0
  238. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/generate_result.json +0 -0
  239. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/generate_schema.json +0 -0
  240. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/help.json +0 -0
  241. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/jobs_wait.json +0 -0
  242. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/knowledge.json +0 -0
  243. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/knowledge_block.json +0 -0
  244. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/launch.json +0 -0
  245. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/logs.json +0 -0
  246. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/models.json +0 -0
  247. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/nodes.json +0 -0
  248. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/outdated.json +0 -0
  249. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/preview.json +0 -0
  250. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/project.json +0 -0
  251. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/run_event.json +0 -0
  252. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/set_default.json +0 -0
  253. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/skill.json +0 -0
  254. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/stop.json +0 -0
  255. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/system_stats.json +0 -0
  256. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/templates.json +0 -0
  257. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/transfer.json +0 -0
  258. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/update.json +0 -0
  259. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/version.json +0 -0
  260. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/which.json +0 -0
  261. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/schemas/widget_catalog.json +0 -0
  262. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/selector.py +0 -0
  263. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/__init__.py +0 -0
  264. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy/__init__.py +0 -0
  265. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-agent-permissions/SKILL.md +0 -0
  266. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-build/SKILL.md +0 -0
  267. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-build-authoring/SKILL.md +0 -0
  268. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-build-failures/SKILL.md +0 -0
  269. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-build-pins/SKILL.md +0 -0
  270. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-custom-nodes/SKILL.md +0 -0
  271. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-custom-nodes-api-schema/SKILL.md +0 -0
  272. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-custom-nodes-caching/SKILL.md +0 -0
  273. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-custom-nodes-dependencies/SKILL.md +0 -0
  274. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-custom-nodes-frontend/SKILL.md +0 -0
  275. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-custom-nodes-invocation/SKILL.md +0 -0
  276. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-custom-nodes-migration/SKILL.md +0 -0
  277. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-custom-nodes-packaging/SKILL.md +0 -0
  278. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-custom-nodes-search/SKILL.md +0 -0
  279. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-custom-nodes-troubleshooting/SKILL.md +0 -0
  280. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-custom-nodes-validation/SKILL.md +0 -0
  281. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-deploy-failures/SKILL.md +0 -0
  282. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-director/SKILL.md +0 -0
  283. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-relay/SKILL.md +0 -0
  284. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/skills/command.py +0 -0
  285. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/standalone.py +0 -0
  286. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/typing.py +0 -0
  287. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/ui.py +0 -0
  288. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/update.py +0 -0
  289. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/utils.py +0 -0
  290. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/uv.py +0 -0
  291. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/where.py +0 -0
  292. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli/workspace_manager.py +0 -0
  293. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli.egg-info/SOURCES.txt +0 -0
  294. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli.egg-info/dependency_links.txt +0 -0
  295. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli.egg-info/entry_points.txt +0 -0
  296. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli.egg-info/requires.txt +0 -0
  297. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/comfy_cli.egg-info/top_level.txt +0 -0
  298. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/setup.cfg +0 -0
  299. {comfy_cli-1.23.0 → comfy_cli-1.24.0}/tests/test_file_utils_network.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comfy-cli
3
- Version: 1.23.0
3
+ Version: 1.24.0
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
@@ -116,7 +116,14 @@ class BuilderClient:
116
116
  ``X-API-Key``, via the shared authed HTTP path (HTTPS-enforced, no credential
117
117
  replay on redirect, response-size capped)."""
118
118
 
119
- def __init__(self, base_url: str, token: str | None = None, *, api_key: Credential | None = None):
119
+ def __init__(
120
+ self,
121
+ base_url: str,
122
+ token: str | None = None,
123
+ *,
124
+ api_key: Credential | None = None,
125
+ token_env_var: str = "COMFY_BUILDER_TOKEN",
126
+ ):
120
127
  # kind="cloud" is what makes the shared http layer attach the credential;
121
128
  # path_prefix carries the /v1 base so target.url("builds") is right.
122
129
  self.target = Target(
@@ -127,18 +134,27 @@ class BuilderClient:
127
134
  api_key=api_key.value if api_key is not None else None,
128
135
  )
129
136
  # Only a client built from the stored sign-in may swap its token; one
130
- # handed a token (COMFY_BUILDER_TOKEN) or a key keeps it and lets a 401 surface.
137
+ # handed a token (COMFY_BUILDER_TOKEN, COMFY_CLOUD_AUTH_TOKEN) or a key keeps
138
+ # it and lets a 401 surface.
131
139
  self._refreshes_on_401 = False
132
140
  # What to replace once the builder refuses this client's credential.
133
141
  self._refused_hint = (
134
142
  credentials.refused_key_hint(api_key)
135
143
  if api_key is not None
136
- else "replace COMFY_BUILDER_TOKEN with a fresh Cloud JWT"
144
+ else f"replace {token_env_var} with a fresh Cloud JWT"
137
145
  )
138
146
 
139
147
  @classmethod
140
148
  def from_credentials(cls, base_url: str) -> BuilderClient:
141
- """Build a client from the workspace API key when one is set, else from the sign-in."""
149
+ """Build a client from a credential handed in by env when one is set, else from the sign-in.
150
+
151
+ ``COMFY_CLOUD_AUTH_TOKEN``, then ``COMFY_CLOUD_API_KEY``, outrank the
152
+ stored sign-in, which is then not refreshed; the saved set-key key comes
153
+ only after the sign-in.
154
+ """
155
+ token = credentials.platform_env_token()
156
+ if token:
157
+ return cls(base_url, token, token_env_var=credentials.CLOUD_BEARER_ENV_VAR)
142
158
  key = credentials.platform_api_key()
143
159
  if key is None:
144
160
  try:
@@ -182,12 +182,16 @@ def logout_cmd():
182
182
  @app.command("whoami", help="Show the current Comfy Cloud sign-in status.")
183
183
  @tracking.track_command("cloud")
184
184
  def whoami_cmd():
185
- from comfy_cli.credentials import find_api_key, get_session
185
+ from comfy_cli.credentials import env_credential, find_api_key, get_session
186
186
 
187
187
  renderer = get_renderer()
188
+ # A credential handed in by env outranks the stored sign-in for every cloud
189
+ # call, so whoami reports that one as the identity in effect.
190
+ handed_in = env_credential("cloud")
188
191
  # Refresh first so we report (and persist) a live token rather than a
189
- # token that quietly lapsed since the last command.
190
- session = get_session(refresh=True)
192
+ # token that quietly lapsed since the last command — but only when the
193
+ # sign-in is what cloud calls will send. Otherwise it is read as stored.
194
+ session = get_session(refresh=handed_in is None)
191
195
  configured_base_url = get_base_url()
192
196
 
193
197
  # Ambient API key (env / store), reported even when an OAuth session
@@ -198,28 +202,47 @@ def whoami_cmd():
198
202
  if ambient_key is not None:
199
203
  api_key_source = "env" if ambient_key.source.startswith("env:") else "store"
200
204
 
201
- # `signed_in` reflects *any* valid auth path — OAuth session or API key.
202
- # When both are present, OAuth wins (it's the credential the client sends);
203
- # the API key is only used as a fallback when no live session exists.
205
+ # `signed_in` reflects *any* valid auth path. Precedence matches what the
206
+ # client sends: a credential handed in by env (COMFY_CLOUD_AUTH_TOKEN, then
207
+ # COMFY_CLOUD_API_KEY), then a live OAuth session, then the stored key.
208
+ # `credential_source` names the one in effect ("env:<VAR>", "session",
209
+ # "stored:<provider>").
204
210
  has_api_key = api_key_source is not None
205
211
  has_oauth = session is not None
206
212
  expired = session.is_expired() if session else None
207
- oauth_usable = has_oauth and not expired
213
+ # A session minted for another base URL is never sent to the configured
214
+ # one (the replay-guard in resolve_target), so it is not usable here.
215
+ stale_base_url = (session.base_url != configured_base_url) if session else False
216
+ oauth_usable = has_oauth and not expired and not stale_base_url
208
217
  # `signed_in` must reflect whether we can actually authenticate right now:
209
- # a live (non-expired) OAuth session, or an API key. An expired,
210
- # unrefreshable OAuth session is NOT signed in on its own.
211
- signed_in = oauth_usable or has_api_key
218
+ # a handed-in credential, a live (non-expired) OAuth session, or an API
219
+ # key. An expired, unrefreshable OAuth session is NOT signed in on its own.
220
+ signed_in = handed_in is not None or oauth_usable or has_api_key
212
221
  auth_method: str | None
213
- if oauth_usable:
222
+ credential_source: str | None
223
+ if handed_in is not None:
224
+ auth_method = handed_in.kind
225
+ credential_source = handed_in.source
226
+ elif oauth_usable:
214
227
  auth_method = "oauth"
215
- elif has_api_key:
228
+ credential_source = "session"
229
+ elif ambient_key is not None:
216
230
  auth_method = "api_key"
231
+ credential_source = ambient_key.source
217
232
  else:
218
233
  auth_method = None
219
- stale_base_url = (session.base_url != configured_base_url) if session else False
234
+ credential_source = None
220
235
 
221
236
  if renderer.is_pretty():
222
- if has_oauth:
237
+ if handed_in is not None:
238
+ env_var = handed_in.source.removeprefix("env:")
239
+ rprint(f"[bold]Authenticated via {env_var}[/bold] [dim](handed in by the environment)[/dim]")
240
+ if has_oauth:
241
+ rprint(
242
+ f"[dim]A stored `comfy cloud login` session ({session.base_url}) is present "
243
+ f"but unused while {env_var} is set.[/dim]"
244
+ )
245
+ elif has_oauth:
223
246
  from comfy_cli.output.branding import whoami_banner
224
247
 
225
248
  renderer.console().print(
@@ -244,7 +267,8 @@ def whoami_cmd():
244
267
  f"[dim]OAuth session is preferred; X-API-Key from {api_key_source} present but unused.[/dim]"
245
268
  )
246
269
  else:
247
- rprint(f"[dim]OAuth session expired — falling back to X-API-Key from {api_key_source}.[/dim]")
270
+ reason = "is for another base URL" if stale_base_url else "expired"
271
+ rprint(f"[dim]OAuth session {reason} — falling back to X-API-Key from {api_key_source}.[/dim]")
248
272
  else:
249
273
  from comfy_cli.output.branding import signed_out_banner
250
274
 
@@ -260,9 +284,13 @@ def whoami_cmd():
260
284
  payload: dict[str, object | None] = {
261
285
  "signed_in": signed_in,
262
286
  "auth_method": auth_method,
263
- "base_url": session.base_url if session else configured_base_url,
287
+ # The URL the credential in effect goes to. Cloud calls always go to the
288
+ # configured URL (a session minted for another one is not used), so
289
+ # this is the configured URL; the session's own URL is in `session`.
290
+ "base_url": configured_base_url,
264
291
  "configured_base_url": configured_base_url,
265
292
  "api_key_source": api_key_source,
293
+ "credential_source": credential_source,
266
294
  }
267
295
  if session is not None:
268
296
  payload["expired"] = expired
@@ -2002,6 +2002,33 @@ def run(
2002
2002
  ),
2003
2003
  ),
2004
2004
  ] = False,
2005
+ output_node_ids: Annotated[
2006
+ list[str] | None,
2007
+ typer.Option(
2008
+ "--output-node-ids",
2009
+ show_default=False,
2010
+ help=(
2011
+ "Comma-separated ids of output nodes to run (e.g. 9,12; repeatable); only they and "
2012
+ "the nodes they depend on execute, the rest of the graph is skipped. Ids are the "
2013
+ "API-format graph's node ids and must be output nodes. Sent to /prompt as "
2014
+ "partial_execution_targets. Omit to run the whole graph."
2015
+ ),
2016
+ ),
2017
+ ] = None,
2018
+ client_id: Annotated[
2019
+ str | None,
2020
+ typer.Option(
2021
+ "--client-id",
2022
+ show_default=False,
2023
+ help=(
2024
+ "Local-only: submit as this already-connected WebSocket clientId instead of a fresh "
2025
+ "one. ComfyUI sends a prompt's execution events only to the socket that submitted it, "
2026
+ "so passing a browser tab's clientId is what makes that tab show node highlights, "
2027
+ "progress and outputs for a run it did not start. This invocation then cannot watch "
2028
+ "the run itself, so it cannot be combined with --wait."
2029
+ ),
2030
+ ),
2031
+ ] = None,
2005
2032
  ):
2006
2033
  # Snapshot kwargs before the body mutates api_key/host/port — analytics should record what user actually supplied.
2007
2034
  _track_props = tracking.filter_command_kwargs(dict(locals()))
@@ -2047,11 +2074,51 @@ def run(
2047
2074
  # `where` kwarg is None then). Rides on the execution_success/_error events.
2048
2075
  _track_props["target"] = "cloud" if decision.target is where_module.WhereTarget.CLOUD else "local"
2049
2076
 
2077
+ if client_id is not None and not client_id.strip():
2078
+ renderer.error(
2079
+ code="client_id_rejected",
2080
+ message="--client-id must not be empty",
2081
+ hint="pass the connected browser client's id, or omit --client-id",
2082
+ details={"reason": "empty"},
2083
+ )
2084
+ raise typer.Exit(code=1)
2085
+ client_id = client_id.strip() if client_id is not None else None
2086
+ if client_id:
2087
+ rejection = None
2088
+ if decision.target is where_module.WhereTarget.CLOUD:
2089
+ rejection = ("cloud", "--client-id addresses a local ComfyUI socket and has no meaning on cloud")
2090
+ elif wait:
2091
+ rejection = (
2092
+ "wait",
2093
+ "--client-id hands this run's events to another client, so --wait would watch a silent socket",
2094
+ )
2095
+ if rejection is not None:
2096
+ reason, message = rejection
2097
+ renderer.error(
2098
+ code="client_id_rejected",
2099
+ message=message,
2100
+ hint="drop --client-id, or drop the conflicting flag",
2101
+ details={"reason": reason},
2102
+ )
2103
+ raise typer.Exit(code=1)
2104
+
2050
2105
  # Default for --notify: on when a human is at the terminal, off for
2051
2106
  # agents (they shouldn't get surprise side-channel processes they didn't
2052
2107
  # ask for). The user can override either way with --notify/--no-notify.
2053
2108
  effective_notify = notify if notify is not None else (renderer.is_pretty() and not wait)
2054
2109
 
2110
+ target_ids: list[str] | None = None
2111
+ if output_node_ids:
2112
+ target_ids = run_inner.parse_output_node_ids(output_node_ids)
2113
+ if not target_ids:
2114
+ renderer.error(
2115
+ code="invalid_output_node_ids",
2116
+ message="--output-node-ids was given but names no node ids",
2117
+ hint="pass comma-separated output node ids, e.g. --output-node-ids 9,12; omit it to run the whole graph",
2118
+ details={"unknown_node_ids": [], "non_output_node_ids": []},
2119
+ )
2120
+ raise typer.Exit(code=1)
2121
+
2055
2122
  # --prompt/--set build an in-memory API-format graph from the bundled
2056
2123
  # default text2img workflow (no --workflow file). The aliases resolve
2057
2124
  # against OUR pinned node ids, so mixing them with a user --workflow —
@@ -2106,6 +2173,7 @@ def run(
2106
2173
  workflow_id=workflow_id,
2107
2174
  preloaded=preloaded,
2108
2175
  allow_spend=allow_spend,
2176
+ output_node_ids=target_ids,
2109
2177
  )
2110
2178
  else:
2111
2179
  # Both targets must fall off the END of this try suite: Python skips
@@ -2144,6 +2212,8 @@ def run(
2144
2212
  print_prompt=print_prompt,
2145
2213
  preloaded=preloaded,
2146
2214
  allow_spend=allow_spend,
2215
+ output_node_ids=target_ids,
2216
+ client_id=client_id,
2147
2217
  )
2148
2218
  except typer.Exit as e:
2149
2219
  if (e.exit_code or 0) == 0:
@@ -175,7 +175,12 @@ class Client:
175
175
  if self.target.api_key:
176
176
  return False
177
177
 
178
- from comfy_cli.credentials import get_session
178
+ from comfy_cli.credentials import env_credential, get_session
179
+
180
+ # A token handed in by env is what was refused; the stored sign-in was
181
+ # never used, so it is not refreshed (or rewritten) on this token's behalf.
182
+ if env_credential("cloud") is not None:
183
+ return False
179
184
 
180
185
  old_token = self.target.auth_token
181
186
  # Was there a stored OAuth session backing this token? If not (e.g. a
@@ -347,6 +352,7 @@ class Client:
347
352
  timeout: float | None = None,
348
353
  extra_data: dict | None = None,
349
354
  workflow_id: str | None = None,
355
+ output_node_ids: list[str] | None = None,
350
356
  ) -> SubmitResult:
351
357
  """POST {prefix}/prompt — submit a workflow for execution.
352
358
 
@@ -355,6 +361,9 @@ class Client:
355
361
  forwarded as a top-level ``workflow_id`` field so the server can associate
356
362
  the job with an existing workflow and auto-promote a draft on run. Omitted
357
363
  from the body entirely when unset.
364
+ ``output_node_ids`` is sent as top-level ``partial_execution_targets`` so
365
+ the server runs only those output nodes and what they depend on; omitted
366
+ from the body entirely when unset, which queues the whole graph.
358
367
  For cloud submissions, the user's OAuth token is injected as
359
368
  ``auth_token_comfy_org`` so partner-API nodes (BFL Flux Pro, Gemini
360
369
  Nano Banana, etc.) can call out to comfy.org — matching what the web
@@ -385,6 +394,8 @@ class Client:
385
394
  # workflow (auto-promotes a draft on run). Only sent when provided.
386
395
  if workflow_id:
387
396
  request_payload["workflow_id"] = workflow_id
397
+ if output_node_ids:
398
+ request_payload["partial_execution_targets"] = list(output_node_ids)
388
399
  return request_payload
389
400
 
390
401
  resp = self._request("POST", ("prompt",), body_factory=payload, timeout=timeout)
@@ -26,6 +26,7 @@ from __future__ import annotations
26
26
 
27
27
  import contextlib
28
28
  import hashlib
29
+ import http.client
29
30
  import io
30
31
  import json
31
32
  import os
@@ -2487,13 +2488,14 @@ def _builder_client(renderer, builder_url: str | None):
2487
2488
  A caller that already holds a Cloud JWT, such as the Developer Platform agent
2488
2489
  service forwarding the request's token, injects it via ``COMFY_BUILDER_TOKEN``,
2489
2490
  and that explicit token comes first. Otherwise ``from_credentials`` uses a
2490
- workspace API key from ``COMFY_CLOUD_API_KEY``, which a CI job sets, then the
2491
- stored sign-in.
2491
+ Cloud JWT from ``COMFY_CLOUD_AUTH_TOKEN`` or a workspace API key from
2492
+ ``COMFY_CLOUD_API_KEY``, which a CI job sets, then the stored sign-in. Empty
2493
+ or whitespace-only values count as unset.
2492
2494
  """
2493
2495
  from comfy_cli.builder_api import BuilderAuthError, BuilderClient
2494
2496
 
2495
2497
  base_url = builder_url or os.environ.get("COMFY_BUILDER_URL") or DEFAULT_BUILDER_URL
2496
- token = os.environ.get("COMFY_BUILDER_TOKEN")
2498
+ token = (os.environ.get("COMFY_BUILDER_TOKEN") or "").strip()
2497
2499
  if token:
2498
2500
  return BuilderClient(base_url, token)
2499
2501
  try:
@@ -2978,6 +2980,10 @@ def show_cmd(
2978
2980
 
2979
2981
 
2980
2982
  _RELEASE_POLL_SECONDS = 2.0
2983
+ #: Failed polls in a row a watch rides out before it ends: transport failures and
2984
+ #: answers too damaged to read, counted together. With the backoff in
2985
+ #: ``_poll_call``, about a minute of outage.
2986
+ _POLL_TRANSPORT_MISSES = 5
2981
2987
 
2982
2988
 
2983
2989
  #: Sorts below every usable timestamp, so a release the builder dated badly loses
@@ -3133,9 +3139,71 @@ def _selected_release_id(renderer, client, scope: _BuildScope, release: str | No
3133
3139
  return _newest_release_id(renderer, client, _resolve_build_id(renderer, client, scope))
3134
3140
 
3135
3141
 
3142
+ def _poll_call(renderer, fn, usable=None):
3143
+ """``_builder_call`` for one poll of a watch, which a single timed-out or
3144
+ dropped request must not end: the release keeps building either way. A
3145
+ transport failure sleeps and polls again, up to ``_POLL_TRANSPORT_MISSES`` in
3146
+ a row; the builder's own answer (an ``HTTPError``) and every other failure
3147
+ still end the watch on the first one, through the same envelope.
3148
+
3149
+ The builder client is urllib-only: a connect timeout arrives wrapped in a
3150
+ ``URLError``, a timeout or reset while awaiting the response arrives bare, and
3151
+ a connection closed partway through the body arrives as ``IncompleteRead``.
3152
+ A certificate the CA store refuses also arrives as a ``URLError``, but it
3153
+ will be refused again, so it ends the watch at once with its trust hint.
3154
+ A fixed-length body cut short arrives as no error at all: the HTTP layer
3155
+ returns the partial bytes, which parse as ``{}``. So an answer ``usable``
3156
+ rejects counts as a miss in the same run, and the last one ends the watch."""
3157
+ from comfy_cli.http import tls_verification_failed
3158
+
3159
+ def attempt():
3160
+ for miss in range(1, _POLL_TRANSPORT_MISSES + 1):
3161
+ try:
3162
+ answer = fn()
3163
+ except (urllib.error.URLError, TimeoutError, ConnectionError, http.client.IncompleteRead) as e:
3164
+ if isinstance(e, urllib.error.HTTPError) or tls_verification_failed(e):
3165
+ raise
3166
+ if miss == _POLL_TRANSPORT_MISSES:
3167
+ if isinstance(e, urllib.error.URLError):
3168
+ raise
3169
+ # Wrapped, or a bare one would escape ``_builder_call`` as a traceback.
3170
+ raise urllib.error.URLError(e) from e
3171
+ renderer.warn(f"builder unreachable ({e}); polling again ({miss}/{_POLL_TRANSPORT_MISSES - 1})")
3172
+ else:
3173
+ if usable is None or usable(answer):
3174
+ return answer
3175
+ if miss == _POLL_TRANSPORT_MISSES:
3176
+ renderer.error(
3177
+ code="build_builder_error",
3178
+ message=f"the builder sent no readable answer in {miss} polls in a row.",
3179
+ )
3180
+ raise typer.Exit(code=1)
3181
+ renderer.warn(f"builder answer unreadable; polling again ({miss}/{_POLL_TRANSPORT_MISSES - 1})")
3182
+ time.sleep(min(_RELEASE_POLL_SECONDS * 2**miss, 30.0))
3183
+
3184
+ return _builder_call(renderer, attempt)
3185
+
3186
+
3187
+ def _readable_log(answer) -> bool:
3188
+ """The builder always sends ``log``, empty when none was captured yet, so an
3189
+ answer without one is a body cut short."""
3190
+ return isinstance(answer, dict) and isinstance(answer.get("log"), str)
3191
+
3192
+
3193
+ def _poll_status(renderer, client, release_id: str) -> dict:
3194
+ """One status read of a watch. An answer with no status string is unreadable
3195
+ and polled again; a whole answer naming an unknown status is the builder's
3196
+ word and ends the watch at the caller."""
3197
+ return _poll_call(
3198
+ renderer,
3199
+ lambda: client.get_release(release_id),
3200
+ usable=lambda release: isinstance(release, dict) and isinstance(release.get("status"), str),
3201
+ )
3202
+
3203
+
3136
3204
  def _poll_release(renderer, client, release_id: str) -> dict:
3137
3205
  while True:
3138
- release = _builder_call(renderer, lambda: client.get_release(release_id))
3206
+ release = _poll_status(renderer, client, release_id)
3139
3207
  status = release.get("status")
3140
3208
  if renderer.is_pretty():
3141
3209
  renderer.info(f"release {release_id}: {status}")
@@ -3323,21 +3391,39 @@ def release_logs(
3323
3391
  release_id = _selected_release_id(renderer, client, _BuildScope(ctx, path, build_id), release)
3324
3392
  previous_log = ""
3325
3393
  content: dict = {}
3394
+ complete = False
3395
+
3396
+ def read_log() -> dict:
3397
+ return client.get_release_logs(release_id, os=selected_target.os, gpu=selected_target.gpu)
3398
+
3326
3399
  while True:
3327
- content = _builder_call(
3328
- renderer,
3329
- lambda: client.get_release_logs(release_id, os=selected_target.os, gpu=selected_target.gpu),
3330
- )
3400
+ # Only a follow is a watch; a single read fails fast like every other command.
3401
+ # A body cut short inside its Content-Length parses as `{}` rather than
3402
+ # raising. Before any whole log is in hand the follow polls again, as for
3403
+ # a status read; after, publishing it would drop the log already read and
3404
+ # fail `build_release_logs.json`, so the last whole log stands until a
3405
+ # later read replaces it.
3406
+ if follow:
3407
+ had_log = _readable_log(content)
3408
+ read = _poll_call(renderer, read_log, usable=lambda answer: had_log or _readable_log(answer))
3409
+ else:
3410
+ read = _builder_call(renderer, read_log)
3411
+ if _readable_log(read) or not content:
3412
+ content = read
3413
+ elif complete:
3414
+ renderer.warn("the final log read came back incomplete; showing the log read before the release completed")
3331
3415
  log = content.get("log", "")
3332
3416
  if renderer.is_pretty() and isinstance(log, str) and log:
3333
3417
  renderer.print(log[len(previous_log) :] if log.startswith(previous_log) else log)
3334
3418
  previous_log = log
3335
- if not follow:
3419
+ if not follow or complete:
3336
3420
  break
3337
- detail = _builder_call(renderer, lambda: client.get_release(release_id))
3421
+ detail = _poll_status(renderer, client, release_id)
3338
3422
  match detail.get("status"):
3339
3423
  case "complete":
3340
- break
3424
+ # The log above may predate completion (a retried status read
3425
+ # widens that gap), so read it once more for the final lines.
3426
+ complete = True
3341
3427
  case "queued" | "building":
3342
3428
  time.sleep(_RELEASE_POLL_SECONDS)
3343
3429
  case status:
@@ -52,6 +52,7 @@ from comfy_cli.command.deploy_up import (
52
52
  landed_result,
53
53
  move_line,
54
54
  move_text,
55
+ observe_editor,
55
56
  reconcile_up,
56
57
  release_or_id,
57
58
  replaced_message,
@@ -358,6 +359,15 @@ def up_cmd(
358
359
  help="Name the deployment this creates. Default: production for a Build's first, else deployment-N.",
359
360
  ),
360
361
  ] = None,
362
+ editor: Annotated[
363
+ bool | None,
364
+ typer.Option(
365
+ "--editor/--no-editor",
366
+ help="Keep a CPU server for opening the deployment in the editor. Use --no-editor for one only called "
367
+ "through the API. Omitted, a new deployment gets comfy-deploy's default, on, and an existing one keeps "
368
+ "what it has.",
369
+ ),
370
+ ] = None,
361
371
  # Watching is what someone who just asked for a deployment wants: the command
362
372
  # that starts a several-minute wait should say how the wait is going. Ctrl-C
363
373
  # and --no-watch both leave the deploy running and print how to re-attach.
@@ -382,6 +392,7 @@ def up_cmd(
382
392
  deployment_id=deployment_id,
383
393
  create=create,
384
394
  name=name,
395
+ editor=editor,
385
396
  watch=watch,
386
397
  )
387
398
  try:
@@ -418,12 +429,16 @@ def up_cmd(
418
429
  # process: report where it had got to, and leave it be.
419
430
  _warn_unapplied_bounds(renderer, result)
420
431
  if last is not None:
421
- _render_result(renderer, replace(result, deployment=last, pending_bounds=None), watch=False)
432
+ abandoned_at = replace(result, deployment=last, pending_bounds=None)
433
+ _render_result(renderer, observe_editor(abandoned_at, last), watch=False)
422
434
 
423
435
  move = watched_move(result.release, result.previous_release, result.deployment) if moving else None
424
436
  outcomes = MoveOutcomes(client, watched_id, move) if move is not None else None
425
437
  watched = _watch(renderer, client, watched_id, outcomes=outcomes, on_abandon=abandoned)
426
- result = finish_move(client, result, watched, outcomes) if outcomes else replace(result, deployment=watched)
438
+ if outcomes is not None:
439
+ result = finish_move(client, result, watched, outcomes)
440
+ else:
441
+ result = observe_editor(replace(result, deployment=watched), watched)
427
442
  _render_result(renderer, result, watch=watch)
428
443
  except (BuildSpecNotFoundError, BuildSpecInvalidError) as error:
429
444
  _render_spec_error(renderer, error)
@@ -54,6 +54,17 @@ def _release_column(deployment: JsonObject) -> str:
54
54
  return f" {release_id}" if isinstance(release_id, str) and release_id else ""
55
55
 
56
56
 
57
+ def _editor_of(row: JsonObject) -> bool | None:
58
+ # A comfy-deploy older than the editor switch sends no `editor`.
59
+ editor = row.get("editor")
60
+ return editor if isinstance(editor, bool) else None
61
+
62
+
63
+ def _editor_column(deployment: JsonObject) -> str:
64
+ editor = deployment.get("editor")
65
+ return "" if editor is None else f" editor {'on' if editor else 'off'}"
66
+
67
+
57
68
  def run_ls(request: ListRequest) -> None:
58
69
  renderer = get_renderer()
59
70
  try:
@@ -90,7 +101,7 @@ def run_ls(request: ListRequest) -> None:
90
101
  version = versions[release_id]
91
102
  if not request.include_deleted and row.get("deletedAt") is not None:
92
103
  continue
93
- named = {**row, "name": deployment_name(row)}
104
+ named = {**row, "name": deployment_name(row), "editor": _editor_of(row)}
94
105
  deployments.append(named if version is None else {**named, "releaseVersion": version})
95
106
  if len(deployments) == request.limit:
96
107
  break
@@ -104,7 +115,8 @@ def run_ls(request: ListRequest) -> None:
104
115
  marker = " (deleted)" if deployment.get("deletedAt") is not None else ""
105
116
  renderer.print(
106
117
  f" {sanitize_markup(deployment['name'] or '-')} {required_string(deployment, 'id')}"
107
- f" {required_string(deployment, 'status')}{_release_column(deployment)}{marker}"
118
+ f" {required_string(deployment, 'status')}{_release_column(deployment)}"
119
+ f"{_editor_column(deployment)}{marker}"
108
120
  )
109
121
  payload_rows: list[JsonValue] = [*deployments]
110
122
  renderer.emit({"deployments": payload_rows}, command="deploy ls", changed=False)
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections.abc import Callable
6
- from dataclasses import dataclass
6
+ from dataclasses import dataclass, replace
7
7
  from typing import Final, Protocol
8
8
 
9
9
  from comfy_cli.command.build_spec import JsonObject, JsonValue
@@ -21,6 +21,7 @@ class DeployUpClient(Protocol):
21
21
  *,
22
22
  idempotency_key: str | None = None,
23
23
  name: str | None = None,
24
+ editor: bool | None = None,
24
25
  ) -> JsonObject: ...
25
26
 
26
27
  def get_deployment(self, deployment_id: str) -> JsonObject: ...
@@ -29,7 +30,11 @@ class DeployUpClient(Protocol):
29
30
 
30
31
  def start_deployment(self, deployment_id: str) -> JsonObject: ...
31
32
 
32
- def move_deployment(self, deployment_id: str, base_revision: int, release_id: str) -> JsonObject: ...
33
+ def set_deployment_editor(self, deployment_id: str, editor: bool) -> JsonObject: ...
34
+
35
+ def move_deployment(
36
+ self, deployment_id: str, base_revision: int, release_id: str, *, editor: bool | None = None
37
+ ) -> JsonObject: ...
33
38
 
34
39
  def promote_deployment(self, deployment_id: str, base_revision: int, from_deployment_id: str) -> JsonObject: ...
35
40
 
@@ -59,12 +64,51 @@ class UpRequest:
59
64
  # `--name`: the name a created deployment gets, where comfy-deploy would
60
65
  # otherwise pick one.
61
66
  name: str | None = None
67
+ # `--editor/--no-editor`: whether the deployment keeps an editor CPU server.
68
+ # ``None`` is "flag omitted" and sends nothing: a create gets comfy-deploy's
69
+ # default, and an existing deployment keeps what it has.
70
+ editor: bool | None = None
62
71
  # Whether the caller follows the deployment once the request is accepted.
63
72
  # A move cannot carry new worker bounds, so they are applied after it lands,
64
73
  # and only a watch is there to see it land.
65
74
  watch: bool = True
66
75
 
67
76
 
77
+ @dataclass(frozen=True, slots=True)
78
+ class EditorCheck:
79
+ """Where `--editor/--no-editor` stands, judged apart from what else `up` did.
80
+
81
+ comfy-deploy keeps a move it took when the editor write fails, and leaves
82
+ `editor` out of an answer where its lookup of it fails, so neither ever
83
+ undoes the change it is judged beside.
84
+ """
85
+
86
+ requested: bool
87
+ # The value the latest answer that had one showed; None while none has.
88
+ seen: bool | None = None
89
+ # Set on an edit of the editor alone, of a deployment that had the other
90
+ # value: the value confirmed is then the change.
91
+ turns: bool = False
92
+ # Why the read made to confirm the value failed, until an answer shows it.
93
+ read_error: str | None = None
94
+
95
+ def observe(self, answer: JsonObject) -> EditorCheck:
96
+ """The check with the editor value ``answer`` shows, where it shows one."""
97
+ seen = answer.get("editor")
98
+ if not isinstance(seen, bool):
99
+ return self
100
+ return replace(self, seen=seen, read_error=None)
101
+
102
+ @property
103
+ def held(self) -> bool:
104
+ return self.seen is self.requested
105
+
106
+ @property
107
+ def made(self) -> bool:
108
+ """Whether the edit of the editor alone changed the deployment."""
109
+ return self.turns and self.held
110
+
111
+
68
112
  @dataclass(frozen=True, slots=True)
69
113
  class UpResult:
70
114
  deployment: JsonObject
@@ -91,6 +135,8 @@ class UpResult:
91
135
  pending_bounds: JsonObject | None = None
92
136
  # The name `--name` asked a create for, to say so where comfy-deploy took none.
93
137
  requested_name: str | None = None
138
+ # `--editor/--no-editor` as asked and as last read; None when neither was passed.
139
+ editor: EditorCheck | None = None
94
140
 
95
141
  def payload(self) -> JsonObject:
96
142
  supersedes: list[JsonValue] = [*self.supersedes]