comfy-cli 1.23.0__tar.gz → 1.25.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 (300) hide show
  1. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/PKG-INFO +1 -1
  2. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/_lazy.py +56 -1
  3. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/builder_api.py +20 -4
  4. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cloud/command.py +44 -16
  5. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cloud/oauth.py +49 -3
  6. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cmdline.py +70 -0
  7. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/comfy_client.py +12 -1
  8. comfy_cli-1.25.0/comfy_cli/command/assets_library.py +846 -0
  9. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/build.py +97 -11
  10. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/cloud_http.py +33 -3
  11. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy.py +17 -2
  12. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy_ls.py +14 -2
  13. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy_types.py +48 -2
  14. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy_up.py +124 -18
  15. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/generate/app.py +20 -2
  16. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/generate/client.py +5 -8
  17. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/generate/emit.py +28 -8
  18. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/generate/schema.py +1 -1
  19. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/jobs.py +555 -106
  20. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/knowledge.py +14 -5
  21. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/models/models.py +201 -6
  22. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/node_deps.py +71 -16
  23. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/run/__init__.py +248 -41
  24. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/run/execution.py +70 -3
  25. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/run/preflight.py +18 -2
  26. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/templates.py +3 -1
  27. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/transfer.py +63 -7
  28. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/workflow.py +148 -10
  29. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/workflow_edit.py +78 -15
  30. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cql/engine.py +625 -88
  31. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cql/loader.py +5 -2
  32. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cql/promoted.py +942 -139
  33. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/credentials.py +235 -74
  34. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/deploy_api.py +23 -4
  35. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/deploy_api_errors.py +8 -0
  36. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/discovery.py +2 -0
  37. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/error_codes.py +122 -6
  38. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/jobs_state.py +19 -1
  39. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/knowledge.py +51 -3
  40. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/layout.py +27 -2
  41. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/link_integrity.py +2 -2
  42. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/model_variants.py +53 -6
  43. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/assets_library.json +6 -2
  44. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_push_event.json +1 -1
  45. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/deploy_ls.json +4 -0
  46. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/error_codes.md +4 -0
  47. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/jobs.json +8 -3
  48. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/node_deps.json +2 -2
  49. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/run.json +6 -1
  50. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/workflow.json +11 -0
  51. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy/SKILL.md +47 -8
  52. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-debug/SKILL.md +1 -1
  53. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-deploy/SKILL.md +10 -1
  54. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/target.py +27 -2
  55. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/tracking.py +5 -0
  56. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/workflow_ops.py +1208 -65
  57. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/workflow_print.py +676 -150
  58. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/workflow_to_api.py +923 -111
  59. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli.egg-info/PKG-INFO +1 -1
  60. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/pyproject.toml +1 -1
  61. comfy_cli-1.23.0/comfy_cli/command/assets_library.py +0 -338
  62. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/LICENSE +0 -0
  63. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/README.md +0 -0
  64. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/__init__.py +0 -0
  65. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/__main__.py +0 -0
  66. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/_safe_exec.py +0 -0
  67. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/agent/__init__.py +0 -0
  68. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/agent/command.py +0 -0
  69. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/auth/__init__.py +0 -0
  70. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/auth/command.py +0 -0
  71. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/auth/store.py +0 -0
  72. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/builder_pagination.py +0 -0
  73. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/caller.py +0 -0
  74. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cancellation.py +0 -0
  75. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cloud/__init__.py +0 -0
  76. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cloud/billing.py +0 -0
  77. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/__init__.py +0 -0
  78. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/_cloud_errors.py +0 -0
  79. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/build_diff.py +0 -0
  80. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/build_digest_cache.py +0 -0
  81. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/build_package.py +0 -0
  82. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/build_paths.py +0 -0
  83. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/build_pull.py +0 -0
  84. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/build_push.py +0 -0
  85. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/build_spec.py +0 -0
  86. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/build_targets.py +0 -0
  87. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/build_upload_progress.py +0 -0
  88. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/build_validation.py +0 -0
  89. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/code_search.py +0 -0
  90. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/custom_nodes/__init__.py +0 -0
  91. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/custom_nodes/bisect_custom_nodes.py +0 -0
  92. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/custom_nodes/cm_cli_util.py +0 -0
  93. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/custom_nodes/command.py +0 -0
  94. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy_cancel.py +0 -0
  95. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy_compute.py +0 -0
  96. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy_lifecycle.py +0 -0
  97. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy_progress.py +0 -0
  98. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy_promote.py +0 -0
  99. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy_read.py +0 -0
  100. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy_refs.py +0 -0
  101. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy_rename.py +0 -0
  102. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy_resolve.py +0 -0
  103. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy_rollback.py +0 -0
  104. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy_run.py +0 -0
  105. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy_run_assets.py +0 -0
  106. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy_run_watch.py +0 -0
  107. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy_runtime.py +0 -0
  108. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy_status.py +0 -0
  109. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/deploy_workflow.py +0 -0
  110. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/generate/__init__.py +0 -0
  111. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/generate/adapters.py +0 -0
  112. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/generate/output.py +0 -0
  113. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/generate/poll.py +0 -0
  114. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/generate/spec/openapi.yml +0 -0
  115. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/generate/spec.py +0 -0
  116. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/generate/upload.py +0 -0
  117. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/github/pr_info.py +0 -0
  118. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/install.py +0 -0
  119. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/job_watcher.py +0 -0
  120. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/launch.py +0 -0
  121. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/models/search.py +0 -0
  122. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/nodes.py +0 -0
  123. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/outdated.py +0 -0
  124. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/pack_scan.py +0 -0
  125. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/pr_command.py +0 -0
  126. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/preview.py +0 -0
  127. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/project.py +0 -0
  128. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/run/loader.py +0 -0
  129. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/run/watcher.py +0 -0
  130. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/run_cli.py +0 -0
  131. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/setup.py +0 -0
  132. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/stop_port.py +0 -0
  133. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/system.py +0 -0
  134. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/version_validators.py +0 -0
  135. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command/workflow_fragments.py +0 -0
  136. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/command_mentions.py +0 -0
  137. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/config_manager.py +0 -0
  138. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/constants.py +0 -0
  139. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cql/__init__.py +0 -0
  140. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cql/_net.py +0 -0
  141. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cql/annotations_source.py +0 -0
  142. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cql/data/__init__.py +0 -0
  143. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cql/data/cloud_disable_config.yaml +0 -0
  144. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cql/data/default_text2img.json +0 -0
  145. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cql/data/supported_nodes.yaml +0 -0
  146. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cql/default_workflow.py +0 -0
  147. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cql/errors.py +0 -0
  148. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cql/widget_catalog.py +0 -0
  149. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/cuda_detect.py +0 -0
  150. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/deploy_assets.py +0 -0
  151. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/deploy_download.py +0 -0
  152. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/deploy_events.py +0 -0
  153. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/deploy_jobs.py +0 -0
  154. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/deprecation.py +0 -0
  155. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/download_state.py +0 -0
  156. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/env_checker.py +0 -0
  157. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/execution_errors.py +0 -0
  158. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/file_utils.py +0 -0
  159. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/fragments.py +0 -0
  160. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/git_utils.py +0 -0
  161. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/hardware.py +0 -0
  162. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/hashing.py +0 -0
  163. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/help_json.py +0 -0
  164. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/host_port.py +0 -0
  165. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/http.py +0 -0
  166. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/interaction.py +0 -0
  167. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/layout_quality.py +0 -0
  168. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/local_address.py +0 -0
  169. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/locking.py +0 -0
  170. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/logging.py +0 -0
  171. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/onboarding.py +0 -0
  172. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/output/__init__.py +0 -0
  173. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/output/branding.py +0 -0
  174. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/output/glyphs.py +0 -0
  175. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/output/panels.py +0 -0
  176. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/output/preview.py +0 -0
  177. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/output/progress.py +0 -0
  178. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/output/renderer.py +0 -0
  179. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/output/rich_compat.py +0 -0
  180. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/output/sanitize.py +0 -0
  181. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/pr_cache.py +0 -0
  182. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/project.py +0 -0
  183. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/registry/__init__.py +0 -0
  184. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/registry/api.py +0 -0
  185. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/registry/config_parser.py +0 -0
  186. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/registry/types.py +0 -0
  187. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/resolve_python.py +0 -0
  188. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/__init__.py +0 -0
  189. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/agent.json +0 -0
  190. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/assets.json +0 -0
  191. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/auth.json +0 -0
  192. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_blob_ls.json +0 -0
  193. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_delete.json +0 -0
  194. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_init.json +0 -0
  195. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_ls.json +0 -0
  196. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_pull.json +0 -0
  197. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_push.json +0 -0
  198. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_refs_base_images.json +0 -0
  199. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_refs_build_targets.json +0 -0
  200. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_refs_model_dirs.json +0 -0
  201. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_refs_resolve.json +0 -0
  202. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_release_create.json +0 -0
  203. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_release_delete.json +0 -0
  204. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_release_logs.json +0 -0
  205. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_release_ls.json +0 -0
  206. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_release_manifest.json +0 -0
  207. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_release_show.json +0 -0
  208. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_show.json +0 -0
  209. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_status.json +0 -0
  210. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_update.json +0 -0
  211. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/build_validate.json +0 -0
  212. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/cloud_status.json +0 -0
  213. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/deploy_cancel.json +0 -0
  214. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/deploy_delete.json +0 -0
  215. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/deploy_events.json +0 -0
  216. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/deploy_history.json +0 -0
  217. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/deploy_logs.json +0 -0
  218. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/deploy_progress_event.json +0 -0
  219. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/deploy_promote.json +0 -0
  220. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/deploy_refs_compute.json +0 -0
  221. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/deploy_rename.json +0 -0
  222. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/deploy_rollback.json +0 -0
  223. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/deploy_run.json +0 -0
  224. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/deploy_scale.json +0 -0
  225. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/deploy_show.json +0 -0
  226. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/deploy_start.json +0 -0
  227. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/deploy_status.json +0 -0
  228. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/deploy_stop.json +0 -0
  229. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/deploy_up.json +0 -0
  230. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/discover.json +0 -0
  231. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/download.json +0 -0
  232. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/download_status.json +0 -0
  233. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/downloads.json +0 -0
  234. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/env.json +0 -0
  235. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/envelope.json +0 -0
  236. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/error.json +0 -0
  237. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/free.json +0 -0
  238. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/generate_list.json +0 -0
  239. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/generate_result.json +0 -0
  240. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/generate_schema.json +0 -0
  241. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/help.json +0 -0
  242. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/jobs_wait.json +0 -0
  243. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/knowledge.json +0 -0
  244. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/knowledge_block.json +0 -0
  245. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/launch.json +0 -0
  246. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/logs.json +0 -0
  247. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/models.json +0 -0
  248. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/nodes.json +0 -0
  249. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/outdated.json +0 -0
  250. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/preview.json +0 -0
  251. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/project.json +0 -0
  252. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/run_event.json +0 -0
  253. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/set_default.json +0 -0
  254. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/skill.json +0 -0
  255. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/stop.json +0 -0
  256. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/system_stats.json +0 -0
  257. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/templates.json +0 -0
  258. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/transfer.json +0 -0
  259. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/update.json +0 -0
  260. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/version.json +0 -0
  261. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/which.json +0 -0
  262. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/schemas/widget_catalog.json +0 -0
  263. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/selector.py +0 -0
  264. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/__init__.py +0 -0
  265. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy/__init__.py +0 -0
  266. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-agent-permissions/SKILL.md +0 -0
  267. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-build/SKILL.md +0 -0
  268. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-build-authoring/SKILL.md +0 -0
  269. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-build-failures/SKILL.md +0 -0
  270. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-build-pins/SKILL.md +0 -0
  271. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-custom-nodes/SKILL.md +0 -0
  272. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-custom-nodes-api-schema/SKILL.md +0 -0
  273. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-custom-nodes-caching/SKILL.md +0 -0
  274. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-custom-nodes-dependencies/SKILL.md +0 -0
  275. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-custom-nodes-frontend/SKILL.md +0 -0
  276. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-custom-nodes-invocation/SKILL.md +0 -0
  277. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-custom-nodes-migration/SKILL.md +0 -0
  278. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-custom-nodes-packaging/SKILL.md +0 -0
  279. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-custom-nodes-search/SKILL.md +0 -0
  280. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-custom-nodes-troubleshooting/SKILL.md +0 -0
  281. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-custom-nodes-validation/SKILL.md +0 -0
  282. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-deploy-failures/SKILL.md +0 -0
  283. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-director/SKILL.md +0 -0
  284. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/comfy-relay/SKILL.md +0 -0
  285. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/skills/command.py +0 -0
  286. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/standalone.py +0 -0
  287. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/typing.py +0 -0
  288. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/ui.py +0 -0
  289. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/update.py +0 -0
  290. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/utils.py +0 -0
  291. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/uv.py +0 -0
  292. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/where.py +0 -0
  293. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli/workspace_manager.py +0 -0
  294. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli.egg-info/SOURCES.txt +0 -0
  295. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli.egg-info/dependency_links.txt +0 -0
  296. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli.egg-info/entry_points.txt +0 -0
  297. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli.egg-info/requires.txt +0 -0
  298. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/comfy_cli.egg-info/top_level.txt +0 -0
  299. {comfy_cli-1.23.0 → comfy_cli-1.25.0}/setup.cfg +0 -0
  300. {comfy_cli-1.23.0 → comfy_cli-1.25.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.25.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
@@ -32,7 +32,7 @@ from typing import TYPE_CHECKING, Any
32
32
 
33
33
  import typer
34
34
  from typer.core import TyperGroup
35
- from typer.models import CommandInfo, TyperInfo
35
+ from typer.models import CommandInfo, DefaultPlaceholder, TyperInfo
36
36
 
37
37
  if TYPE_CHECKING:
38
38
  import click
@@ -105,6 +105,60 @@ class LazyCommand:
105
105
  hidden: bool = False
106
106
 
107
107
 
108
+ def _reject_single_callback_app(name: str, spec: LazySubcommand, sub_app: typer.Typer) -> None:
109
+ """Refuse a ``LazySubcommand`` mount of a Typer app that is only a callback.
110
+
111
+ A Typer app with no commands and no subgroups is one function wearing an
112
+ app, and ``add_typer`` turns it into a click ``Group``. A group disables
113
+ interspersed arguments, so the first non-option token ends option parsing
114
+ and everything after it is read as a nested command name. The callback's
115
+ own positional then swallows the query and the trailing flags become a
116
+ subcommand that does not exist::
117
+
118
+ $ comfy code-search KSampler --repo ComfyUI
119
+ Usage: comfy code-search [OPTIONS] {query} COMMAND [ARGS]...
120
+ Error: Missing argument 'query'.
121
+
122
+ while the same flags *before* the query parse fine — the shape a user
123
+ hits only in the installed CLI, since calling the module's app directly
124
+ never exercises the root group. ``code-search`` shipped that way and was
125
+ moved to :class:`LazyCommand`; this keeps the next one from repeating it.
126
+
127
+ A sub-app that supplies its own ``cls`` (e.g. a nested :class:`LazyTyperGroup`
128
+ with its own ``lazy_subcommands`` table) is exempt: its commands are built on
129
+ lookup rather than registered eagerly, so ``registered_commands`` and
130
+ ``registered_groups`` are empty by design, not because it is a bare callback.
131
+ ``cls`` can be set on the app itself (``typer.Typer(cls=...)``) or on its
132
+ callback (``@sub_app.callback(cls=...)``); ``typer.main.solve_typer_info_defaults``
133
+ reads the callback's value first, so this checks both in that order.
134
+ """
135
+ if not isinstance(sub_app, typer.Typer):
136
+ raise TypeError(
137
+ f"lazy_subcommands[{name!r}] names {spec.module}.{spec.attr}, which is "
138
+ f"{sub_app!r}, not a typer.Typer instance."
139
+ )
140
+ if sub_app.registered_commands or sub_app.registered_groups:
141
+ return
142
+ callback = sub_app.registered_callback
143
+ callback_cls = callback.cls if callback is not None else DefaultPlaceholder(None)
144
+ effective_cls = callback_cls if not isinstance(callback_cls, DefaultPlaceholder) else sub_app.info.cls
145
+ if not isinstance(effective_cls, DefaultPlaceholder):
146
+ return
147
+ if sub_app.registered_callback is None:
148
+ raise TypeError(
149
+ f"lazy_subcommands[{name!r}] mounts {spec.module}.{spec.attr} as a command group, but "
150
+ f"that Typer app registers no commands, no subgroups, and no callback — there is nothing "
151
+ f"to mount. Check the module actually registers the commands you expect."
152
+ )
153
+ raise TypeError(
154
+ f"lazy_subcommands[{name!r}] mounts {spec.module}.{spec.attr} as a command group, but that "
155
+ f"Typer app registers no commands and no subgroups — it is a single callback. Mounting it as "
156
+ f"a group makes click demand a nested COMMAND, so options placed after the positional "
157
+ f'argument are rejected. Declare it as LazyCommand({spec.module!r}, attr="<function>") '
158
+ f"instead, naming the callback function rather than the app."
159
+ )
160
+
161
+
108
162
  class LazyTyperGroup(TyperGroup):
109
163
  """Root group with subcommands declared in ``lazy_subcommands`` and built
110
164
  on first lookup. Set the table on a subclass::
@@ -174,6 +228,7 @@ class LazyTyperGroup(TyperGroup):
174
228
 
175
229
  def _build_group(self, name: str, spec: LazySubcommand) -> click.Command:
176
230
  sub_app = getattr(importlib.import_module(spec.module), spec.attr)
231
+ _reject_single_callback_app(name, spec, sub_app)
177
232
  # Same TyperInfo ``add_typer`` would have appended: only the keywords
178
233
  # that were given, so the sub-app's own settings fill the rest.
179
234
  kwargs: dict[str, Any] = {"name": name}
@@ -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
@@ -45,7 +45,7 @@ import urllib.request
45
45
  import webbrowser
46
46
  from dataclasses import dataclass, field
47
47
  from http.server import BaseHTTPRequestHandler
48
- from typing import Any
48
+ from typing import TYPE_CHECKING, Any
49
49
  from urllib.error import HTTPError, URLError
50
50
 
51
51
  from comfy_cli.cloud import (
@@ -56,6 +56,11 @@ from comfy_cli.cloud import (
56
56
  )
57
57
  from comfy_cli.http import NoRedirectHandler, build_http_only_opener
58
58
 
59
+ if TYPE_CHECKING:
60
+ from collections.abc import Callable
61
+
62
+ from comfy_cli.auth.store import CloudSession
63
+
59
64
  # ---------------------------------------------------------------------------
60
65
  # Error types — caller maps these to renderer.error(code=...) codes.
61
66
  # ---------------------------------------------------------------------------
@@ -92,6 +97,19 @@ class OAuthTimeout(OAuthError):
92
97
  code = "oauth_timeout"
93
98
 
94
99
 
100
+ class SessionReplaced(Exception):
101
+ """The stored sign-in is not the one ``ensure_fresh_session(expect=...)`` was asked to refresh.
102
+
103
+ Raised before anything is refreshed, persisted or cleared, so another
104
+ sign-in stored while the caller waited is left exactly as it was.
105
+ ``session`` is the sign-in found in its place.
106
+ """
107
+
108
+ def __init__(self, session: CloudSession) -> None:
109
+ super().__init__("the stored sign-in was replaced by another")
110
+ self.session = session
111
+
112
+
95
113
  # ---------------------------------------------------------------------------
96
114
  # Constants
97
115
  # ---------------------------------------------------------------------------
@@ -758,7 +776,13 @@ def take_last_fatal_refresh_reason() -> str | None:
758
776
  return reason
759
777
 
760
778
 
761
- def ensure_fresh_session(*, leeway_s: int = 60, force: bool = False, allow_clear: bool = True):
779
+ def ensure_fresh_session(
780
+ *,
781
+ leeway_s: int = 60,
782
+ force: bool = False,
783
+ allow_clear: bool = True,
784
+ expect: Callable[[CloudSession], bool] | None = None,
785
+ ):
762
786
  """Return the stored cloud session, refreshing it first when the access
763
787
  token is expired (or within ``leeway_s`` of expiring) and a refresh token
764
788
  is available.
@@ -787,6 +811,13 @@ def ensure_fresh_session(*, leeway_s: int = 60, force: bool = False, allow_clear
787
811
  session lifecycle. The refresh still returns ``None`` so the caller knows it
788
812
  could not freshen the token; it just declines to destroy the shared session.
789
813
 
814
+ ``expect`` is for a caller bound to one sign-in (an upload finished as the
815
+ account that started it). It is called with the stored session on the first
816
+ read and again under the lock, before any refresh, persist or clear; when it
817
+ returns ``False`` another sign-in replaced that one (``comfy cloud login`` in
818
+ another shell), so :class:`SessionReplaced` is raised and the store is left
819
+ untouched. Without it every stored session is refreshed, as before.
820
+
790
821
  Concurrency: the auth server rotates the refresh token on every refresh and
791
822
  invalidates the old one. A parallel ``comfy run`` fan-out (plus background
792
823
  ``jobs watch`` processes) would otherwise have N processes each load the
@@ -800,6 +831,8 @@ def ensure_fresh_session(*, leeway_s: int = 60, force: bool = False, allow_clear
800
831
  from comfy_cli.auth import store as auth_store
801
832
 
802
833
  session = auth_store.get_cloud_session()
834
+ if session is not None and expect is not None and not expect(session):
835
+ raise SessionReplaced(session)
803
836
  if session is None or not session.refresh_token:
804
837
  return session
805
838
  if not force and not session.is_expired(leeway_s=leeway_s):
@@ -821,6 +854,7 @@ def ensure_fresh_session(*, leeway_s: int = 60, force: bool = False, allow_clear
821
854
  leeway_s=leeway_s,
822
855
  force=force,
823
856
  allow_clear=allow_clear,
857
+ expect=expect,
824
858
  )
825
859
  except TimeoutError:
826
860
  # A peer has held the lock longer than a refresh should take. Don't
@@ -833,7 +867,15 @@ def ensure_fresh_session(*, leeway_s: int = 60, force: bool = False, allow_clear
833
867
  return session
834
868
 
835
869
 
836
- def _locked_refresh(auth_store, *, observed_refresh_token: str, leeway_s: int, force: bool, allow_clear: bool = True):
870
+ def _locked_refresh(
871
+ auth_store,
872
+ *,
873
+ observed_refresh_token: str,
874
+ leeway_s: int,
875
+ force: bool,
876
+ allow_clear: bool = True,
877
+ expect: Callable[[CloudSession], bool] | None = None,
878
+ ):
837
879
  """Decide → POST → persist, executed while holding the secrets lock.
838
880
 
839
881
  Re-reads the persisted session first (double-checked locking): if a peer
@@ -841,6 +883,10 @@ def _locked_refresh(auth_store, *, observed_refresh_token: str, leeway_s: int, f
841
883
  one rather than spending the now-consumed token we observed before locking.
842
884
  """
843
885
  session = auth_store.get_cloud_session()
886
+ # Checked under the lock: another login stored since the first read is
887
+ # neither refreshed nor cleared on this caller's behalf.
888
+ if session is not None and expect is not None and not expect(session):
889
+ raise SessionReplaced(session)
844
890
  if session is None or not session.refresh_token:
845
891
  return session
846
892
 
@@ -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)