comfygit 0.6.0__tar.gz → 0.7.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 (38) hide show
  1. {comfygit-0.6.0 → comfygit-0.7.0}/PKG-INFO +19 -9
  2. {comfygit-0.6.0 → comfygit-0.7.0}/README.md +15 -5
  3. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/cli.py +81 -10
  4. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/env_commands.py +69 -6
  5. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/global_commands.py +278 -235
  6. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/logging/compressed_handler.py +9 -0
  7. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/logging/environment_logger.py +21 -13
  8. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/logging/logging_config.py +6 -5
  9. comfygit-0.7.0/comfygit_cli/logging/security.py +41 -0
  10. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/utils/civitai_errors.py +1 -1
  11. comfygit-0.7.0/comfygit_cli/utils/runtime_client.py +66 -0
  12. comfygit-0.7.0/comfygit_cli/utils/workflow_result.py +40 -0
  13. {comfygit-0.6.0 → comfygit-0.7.0}/pyproject.toml +3 -3
  14. {comfygit-0.6.0 → comfygit-0.7.0}/.gitignore +0 -0
  15. {comfygit-0.6.0 → comfygit-0.7.0}/LICENSE.txt +0 -0
  16. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/__init__.py +0 -0
  17. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/__main__.py +0 -0
  18. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/cli_utils.py +0 -0
  19. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/completers.py +0 -0
  20. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/completion_commands.py +0 -0
  21. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/formatters/__init__.py +0 -0
  22. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/formatters/error_formatter.py +0 -0
  23. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/interactive/__init__.py +0 -0
  24. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/logging/log_compressor.py +0 -0
  25. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/resolution_strategies.py +0 -0
  26. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/strategies/__init__.py +0 -0
  27. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/strategies/confirmation.py +0 -0
  28. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/strategies/conflict_resolver.py +0 -0
  29. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/strategies/interactive.py +0 -0
  30. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/strategies/rollback.py +0 -0
  31. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/update_commands.py +0 -0
  32. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/utils/__init__.py +0 -0
  33. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/utils/formatting.py +0 -0
  34. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/utils/orchestrator.py +0 -0
  35. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/utils/pagination.py +0 -0
  36. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/utils/progress.py +0 -0
  37. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/utils/update_checker.py +0 -0
  38. {comfygit-0.6.0 → comfygit-0.7.0}/comfygit_cli/utils/update_notice.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: comfygit
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: ComfyGit - Git-based environment management for ComfyUI
5
5
  Project-URL: Documentation, https://docs.comfygit.org/
6
6
  Project-URL: Repository, https://github.com/comfygit-ai/comfygit
@@ -8,8 +8,8 @@ Project-URL: Issues, https://github.com/comfygit-ai/comfygit/issues
8
8
  License-File: LICENSE.txt
9
9
  Requires-Python: >=3.10
10
10
  Requires-Dist: argcomplete>=3.5.0
11
- Requires-Dist: comfygit-core==0.6.0
12
- Requires-Dist: comfygit-studio==0.6.0
11
+ Requires-Dist: comfygit-core[keyring]==0.7.0
12
+ Requires-Dist: comfygit-studio==0.7.0
13
13
  Requires-Dist: requests>=2.32.4
14
14
  Description-Content-Type: text/markdown
15
15
 
@@ -201,10 +201,17 @@ cg import [PATH|URL]
201
201
  cg export [PATH]
202
202
  --allow-issues Skip confirmation if models lack source URLs
203
203
 
204
- # Configuration
205
- cg config
206
- --civitai-key KEY Set CivitAI API key (empty string to clear)
207
- --show Show current configuration
204
+ # Provider authentication (secrets never go in argv)
205
+ cg auth status
206
+ cg auth set civitai
207
+ cg auth set huggingface
208
+ cg auth set github
209
+ cg auth login huggingface
210
+ cg auth migrate
211
+ cg auth clear PROVIDER
212
+
213
+ # Nonsecret configuration
214
+ cg config --show
208
215
 
209
216
  # Registry management
210
217
  cg registry status # Show registry cache status
@@ -243,6 +250,7 @@ cg create NAME
243
250
  --template PATH Template manifest
244
251
  --python VERSION Python version (default: 3.11)
245
252
  --comfyui VERSION ComfyUI version
253
+ --comfyui-repository URL ComfyUI Git repository (canonical by default)
246
254
  --torch-backend BACKEND PyTorch backend (auto/cpu/cu128/rocm6.3/xpu)
247
255
  --use Set as active environment
248
256
 
@@ -633,7 +641,9 @@ source ~/.bashrc # or ~/.zshrc
633
641
 
634
642
  - `COMFYGIT_HOME` - Override default workspace location (`~/comfygit`)
635
643
  - `COMFYGIT_DEV_COMPRESS_LOGS` - Enable compressed logging (dev feature: `true`/`1`/`yes`)
636
- - `CIVITAI_API_KEY` - CivitAI API key (or use `cg config --civitai-key`)
644
+ - `CIVITAI_API_TOKEN` / `CIVITAI_API_KEY` - CivitAI credential override
645
+ - `HF_TOKEN` / `HUGGING_FACE_HUB_TOKEN` - Hugging Face credential override
646
+ - `GITHUB_TOKEN` / `GH_TOKEN` - GitHub credential override
637
647
 
638
648
  Example:
639
649
  ```bash
@@ -186,10 +186,17 @@ cg import [PATH|URL]
186
186
  cg export [PATH]
187
187
  --allow-issues Skip confirmation if models lack source URLs
188
188
 
189
- # Configuration
190
- cg config
191
- --civitai-key KEY Set CivitAI API key (empty string to clear)
192
- --show Show current configuration
189
+ # Provider authentication (secrets never go in argv)
190
+ cg auth status
191
+ cg auth set civitai
192
+ cg auth set huggingface
193
+ cg auth set github
194
+ cg auth login huggingface
195
+ cg auth migrate
196
+ cg auth clear PROVIDER
197
+
198
+ # Nonsecret configuration
199
+ cg config --show
193
200
 
194
201
  # Registry management
195
202
  cg registry status # Show registry cache status
@@ -228,6 +235,7 @@ cg create NAME
228
235
  --template PATH Template manifest
229
236
  --python VERSION Python version (default: 3.11)
230
237
  --comfyui VERSION ComfyUI version
238
+ --comfyui-repository URL ComfyUI Git repository (canonical by default)
231
239
  --torch-backend BACKEND PyTorch backend (auto/cpu/cu128/rocm6.3/xpu)
232
240
  --use Set as active environment
233
241
 
@@ -618,7 +626,9 @@ source ~/.bashrc # or ~/.zshrc
618
626
 
619
627
  - `COMFYGIT_HOME` - Override default workspace location (`~/comfygit`)
620
628
  - `COMFYGIT_DEV_COMPRESS_LOGS` - Enable compressed logging (dev feature: `true`/`1`/`yes`)
621
- - `CIVITAI_API_KEY` - CivitAI API key (or use `cg config --civitai-key`)
629
+ - `CIVITAI_API_TOKEN` / `CIVITAI_API_KEY` - CivitAI credential override
630
+ - `HF_TOKEN` / `HUGGING_FACE_HUB_TOKEN` - Hugging Face credential override
631
+ - `GITHUB_TOKEN` / `GH_TOKEN` - GitHub credential override
622
632
 
623
633
  Example:
624
634
  ```bash
@@ -233,6 +233,14 @@ def _add_global_commands(subparsers: argparse._SubParsersAction) -> None:
233
233
  )
234
234
  analyze_parser.set_defaults(func=global_cmds.analyze)
235
235
 
236
+ inventory_parser = subparsers.add_parser(
237
+ "inventory",
238
+ help="Report typed workspace model, environment, and storage inventory",
239
+ )
240
+ inventory_parser.add_argument("--json", action="store_true", dest="json_output", help="Emit machine-readable JSON")
241
+ inventory_parser.add_argument("--storage", action="store_true", help="Measure environment, cache, input/output, and model storage")
242
+ inventory_parser.set_defaults(func=global_cmds.inventory)
243
+
236
244
  # update - Update ComfyGit CLI (self-update)
237
245
  update_parser = subparsers.add_parser("update", help="Update ComfyGit CLI")
238
246
  update_parser.add_argument("--check", action="store_true", help="Check for updates without upgrading")
@@ -333,6 +341,7 @@ def _add_global_commands(subparsers: argparse._SubParsersAction) -> None:
333
341
  # model index list
334
342
  model_index_list_parser = model_index_subparsers.add_parser("list", help="List all indexed models")
335
343
  model_index_list_parser.add_argument("--duplicates", action="store_true", help="Show only models with multiple locations")
344
+ model_index_list_parser.add_argument("--json", action="store_true", dest="json_output", help="Emit machine-readable JSON")
336
345
  model_index_list_parser.set_defaults(func=global_cmds.model_index_list)
337
346
 
338
347
  # model index show
@@ -370,7 +379,14 @@ def _add_global_commands(subparsers: argparse._SubParsersAction) -> None:
370
379
  # model delete
371
380
  model_delete_parser = model_subparsers.add_parser("delete", help="Delete model files and clean index entries")
372
381
  model_delete_parser.add_argument("identifier", help="Model hash, hash prefix, filename, or path")
373
- model_delete_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompt")
382
+ deletion_target = model_delete_parser.add_mutually_exclusive_group()
383
+ deletion_target.add_argument("--location-id", type=int, help="Delete one indexed location id")
384
+ deletion_target.add_argument("--all-locations", action="store_true", help="Explicitly target every indexed location")
385
+ model_delete_parser.add_argument("--apply", action="store_true", help="Apply the deletion plan; default is dry-run")
386
+ model_delete_parser.add_argument("--allow-referenced", action="store_true", help="Allow deletion despite ComfyGit environment references")
387
+ model_delete_parser.add_argument("--allow-incomplete-recovery", action="store_true", help="Allow final-copy deletion without source/hash recovery proof")
388
+ model_delete_parser.add_argument("--json", action="store_true", dest="json_output", help="Emit machine-readable JSON")
389
+ model_delete_parser.add_argument("-y", "--yes", action="store_true", help="Compatibility alias for --apply --all-locations")
374
390
  model_delete_parser.set_defaults(func=global_cmds.model_delete)
375
391
 
376
392
  # Registry management subcommands
@@ -386,14 +402,46 @@ def _add_global_commands(subparsers: argparse._SubParsersAction) -> None:
386
402
  registry_update_parser = registry_subparsers.add_parser("update", help="Update registry data from GitHub")
387
403
  registry_update_parser.set_defaults(func=global_cmds.registry_update)
388
404
 
389
- # Config management - now with subcommands
405
+ # Provider authentication. Secret values are read from a hidden prompt or stdin,
406
+ # never from argv.
407
+ auth_parser = subparsers.add_parser("auth", help="Manage provider authentication")
408
+ auth_subparsers = auth_parser.add_subparsers(dest="auth_command", help="Authentication commands")
409
+ auth_parser.set_defaults(func=_make_help_func(auth_parser))
410
+
411
+ auth_status_parser = auth_subparsers.add_parser("status", help="Show provider authentication status")
412
+ auth_status_parser.set_defaults(func=global_cmds.auth_status)
413
+
414
+ auth_set_parser = auth_subparsers.add_parser("set", help="Save a provider credential")
415
+ auth_set_parser.add_argument("provider", choices=("civitai", "huggingface", "github"))
416
+ auth_set_parser.add_argument(
417
+ "--token-stdin",
418
+ action="store_true",
419
+ help="Read the credential from one line on standard input instead of a hidden prompt",
420
+ )
421
+ auth_set_parser.set_defaults(func=global_cmds.auth_set)
422
+
423
+ auth_clear_parser = auth_subparsers.add_parser("clear", help="Clear a saved provider credential")
424
+ auth_clear_parser.add_argument("provider", choices=("civitai", "huggingface", "github"))
425
+ auth_clear_parser.set_defaults(func=global_cmds.auth_clear)
426
+
427
+ auth_login_parser = auth_subparsers.add_parser(
428
+ "login",
429
+ help="Authenticate through a provider-native login flow",
430
+ )
431
+ auth_login_parser.add_argument("provider", choices=("huggingface",))
432
+ auth_login_parser.add_argument("--force", action="store_true", help="Force a new provider login")
433
+ auth_login_parser.set_defaults(func=global_cmds.auth_login)
434
+
435
+ auth_migrate_parser = auth_subparsers.add_parser(
436
+ "migrate",
437
+ help="Migrate verified legacy plaintext credentials into secure storage",
438
+ )
439
+ auth_migrate_parser.set_defaults(func=global_cmds.auth_migrate)
440
+
441
+ # Nonsecret workspace configuration.
390
442
  config_parser = subparsers.add_parser("config", help="Manage configuration settings")
391
443
  config_parser.add_subparsers(dest="config_command", help="Configuration commands")
392
444
 
393
- # Legacy flags - still supported at root level for backward compatibility
394
- config_parser.add_argument("--civitai-key", type=str, help="Set Civitai API key (use empty string to clear)")
395
- config_parser.add_argument("--huggingface-token", type=str, help="Set Hugging Face token for gated model downloads (use empty string to clear)")
396
- config_parser.add_argument("--github-token", type=str, help="Set GitHub token for private git repository access (use empty string to clear)")
397
445
  config_parser.add_argument("--uv-cache", type=str, help="Set external UV cache path (use empty string to clear)")
398
446
  config_parser.add_argument("--show", action="store_true", help="Show current configuration")
399
447
  config_parser.set_defaults(func=global_cmds.config)
@@ -437,22 +485,24 @@ def _add_global_commands(subparsers: argparse._SubParsersAction) -> None:
437
485
  orch_parser.set_defaults(func=_make_help_func(orch_parser))
438
486
 
439
487
  # orch status
440
- orch_status_parser = orch_subparsers.add_parser("status", help="Show orchestrator status")
488
+ orch_status_parser = orch_subparsers.add_parser("status", help="Show environment runtime status (legacy orchestrator fallback)")
441
489
  orch_status_parser.add_argument("--json", action="store_true", help="Output as JSON")
442
490
  orch_status_parser.set_defaults(func=global_cmds.orch_status)
443
491
 
444
492
  # orch restart
445
- orch_restart_parser = orch_subparsers.add_parser("restart", help="Restart ComfyUI")
493
+ orch_restart_parser = orch_subparsers.add_parser("restart", help="Restart idle ComfyUI through its cg run supervisor")
446
494
  orch_restart_parser.add_argument("--wait", action="store_true", help="Wait for restart to complete")
495
+ orch_restart_parser.add_argument("--timeout", type=float, default=180, help="Maximum readiness wait in seconds")
496
+ orch_restart_parser.add_argument("--json", action="store_true", help="Output acknowledgement or verified readiness as JSON")
447
497
  orch_restart_parser.set_defaults(func=global_cmds.orch_restart)
448
498
 
449
499
  # orch kill
450
- orch_kill_parser = orch_subparsers.add_parser("kill", help="Shutdown orchestrator")
500
+ orch_kill_parser = orch_subparsers.add_parser("kill", help="Shutdown the legacy orchestrator (not cg run)")
451
501
  orch_kill_parser.add_argument("--force", action="store_true", help="Force kill (bypass command queue)")
452
502
  orch_kill_parser.set_defaults(func=global_cmds.orch_kill)
453
503
 
454
504
  # orch clean
455
- orch_clean_parser = orch_subparsers.add_parser("clean", help="Clean orchestrator state")
505
+ orch_clean_parser = orch_subparsers.add_parser("clean", help="Clean legacy orchestrator state")
456
506
  orch_clean_parser.add_argument("--dry-run", action="store_true", help="Show what would be deleted")
457
507
  orch_clean_parser.add_argument("--force", action="store_true", help="Skip confirmation")
458
508
  orch_clean_parser.add_argument("--kill", action="store_true", help="Also kill orchestrator process")
@@ -497,6 +547,13 @@ def _add_env_commands(subparsers: argparse._SubParsersAction) -> None:
497
547
  create_parser.add_argument("--template", type=Path, help="Template manifest")
498
548
  create_parser.add_argument("--python", default="3.11", help="Python version")
499
549
  create_parser.add_argument("--comfyui", help="ComfyUI version")
550
+ create_parser.add_argument(
551
+ "--comfyui-repository",
552
+ help=(
553
+ "ComfyUI Git repository URL. Use with --comfyui to pin a fork "
554
+ "or alternate upstream; defaults to canonical ComfyUI."
555
+ ),
556
+ )
500
557
  create_parser.add_argument(
501
558
  "--torch-backend",
502
559
  default="auto",
@@ -1065,10 +1122,24 @@ def _add_env_commands(subparsers: argparse._SubParsersAction) -> None:
1065
1122
  workflow_resolve_parser = workflow_subparsers.add_parser("resolve", help="Resolve workflow dependencies (nodes & models)")
1066
1123
  workflow_resolve_parser.add_argument("name", help="Workflow name to resolve").completer = workflow_completer # type: ignore[attr-defined]
1067
1124
  workflow_resolve_parser.add_argument("--auto", action="store_true", help="Auto-resolve without interaction")
1125
+ workflow_resolve_parser.add_argument("--json", action="store_true", help="JSON result on stdout; requires --auto and --install/--no-install")
1126
+ workflow_resolve_parser.add_argument("--strict", action="store_true", help="Exit 1 when dependencies remain unresolved or uninstalled")
1068
1127
  workflow_resolve_parser.add_argument("--install", action="store_true", help="Auto-install missing nodes without prompting")
1069
1128
  workflow_resolve_parser.add_argument("--no-install", action="store_true", help="Skip node installation prompt")
1070
1129
  workflow_resolve_parser.set_defaults(func=env_cmds.workflow_resolve)
1071
1130
 
1131
+ workflow_node_parser = workflow_subparsers.add_parser("node", help="Manage explicit node ownership mappings")
1132
+ mapping_parsers = workflow_node_parser.add_subparsers(dest="mapping_command", required=True)
1133
+ for operation in ("map", "unmap", "list"):
1134
+ mapping_parser = mapping_parsers.add_parser(operation)
1135
+ mapping_parser.add_argument("name", help="Workflow name")
1136
+ if operation != "list":
1137
+ mapping_parser.add_argument("node_type", help="ComfyUI registered node class name")
1138
+ if operation == "map":
1139
+ mapping_parser.add_argument("package", help="Tracked node package identifier")
1140
+ mapping_parser.add_argument("--json", action="store_true")
1141
+ mapping_parser.set_defaults(func=env_cmds.workflow_node_mapping)
1142
+
1072
1143
  # workflow model importance
1073
1144
  workflow_importance_parser = workflow_subparsers.add_parser(
1074
1145
  "model",
@@ -20,8 +20,8 @@ from comfygit_core.models import (
20
20
  )
21
21
  from comfygit_core.runtime import (
22
22
  ACTIVE_TORCH_BACKEND_OVERRIDE_ENV,
23
+ ManagedRuntimeController,
23
24
  SwitchObserverServer,
24
- cleanup_supervisor_advertisement,
25
25
  cleanup_switch_status,
26
26
  read_switch_status,
27
27
  resolve_comfyui_endpoint,
@@ -259,6 +259,7 @@ class EnvironmentCommands:
259
259
  workspace.create_environment(
260
260
  name=args.name,
261
261
  comfyui_version=args.comfyui,
262
+ comfyui_repository=getattr(args, "comfyui_repository", None),
262
263
  python_version=args.python,
263
264
  template_path=args.template,
264
265
  torch_backend=args.torch_backend,
@@ -595,6 +596,9 @@ class EnvironmentCommands:
595
596
  extras = getattr(args, 'extra', None) or []
596
597
  all_extras = getattr(args, 'all_extras', False)
597
598
  overlay_names = getattr(args, 'overlay', None) or []
599
+ self._managed_runtime = ManagedRuntimeController(
600
+ env.name, resolve_comfyui_endpoint(base_comfyui_args), self.workspace.path,
601
+ )
598
602
  supervisor_control = self._start_supervisor_control(base_comfyui_args)
599
603
  if supervisor_control:
600
604
  atexit.register(supervisor_control.stop)
@@ -611,6 +615,9 @@ class EnvironmentCommands:
611
615
  # replacing `env`, so derive backend-specific launch args per target.
612
616
  torch_backend, was_probed = self._get_or_probe_backend(env, torch_backend_override)
613
617
  comfyui_args = self._comfyui_args_for_backend(base_comfyui_args, torch_backend)
618
+ self._managed_runtime.configure(env.name, resolve_comfyui_endpoint(comfyui_args))
619
+ if supervisor_control:
620
+ supervisor_control.advertise_runtime()
614
621
 
615
622
  if torch_backend_override:
616
623
  print(f"🔧 Using PyTorch backend override: {torch_backend}")
@@ -653,6 +660,7 @@ class EnvironmentCommands:
653
660
  if comfyui_args:
654
661
  print(f" Arguments: {' '.join(comfyui_args)}")
655
662
 
663
+ self._managed_runtime.launching()
656
664
  if switch_source_env:
657
665
  result = self._run_switched_comfyui(
658
666
  env,
@@ -668,6 +676,7 @@ class EnvironmentCommands:
668
676
  backend_override=torch_backend_override,
669
677
  )
670
678
 
679
+ self._managed_runtime.exited()
671
680
  if result.returncode == RESTART_EXIT_CODE:
672
681
  print("\n🔄 Restart requested, syncing dependencies...\n")
673
682
  no_sync = False # Ensure sync runs on restart
@@ -696,7 +705,6 @@ class EnvironmentCommands:
696
705
  explicit_port = port_text is not None
697
706
 
698
707
  if port_text and port_text.lower() in {"0", "off", "false", "disabled"}:
699
- cleanup_supervisor_advertisement(self.workspace.path)
700
708
  return None
701
709
 
702
710
  if port_text:
@@ -704,7 +712,6 @@ class EnvironmentCommands:
704
712
  candidate_ports = [int(port_text)]
705
713
  except ValueError:
706
714
  print(f"⚠️ Invalid COMFYGIT_SUPERVISOR_CONTROL_PORT={port_text!r}; supervisor control disabled")
707
- cleanup_supervisor_advertisement(self.workspace.path)
708
715
  return None
709
716
  else:
710
717
  start_port = min(max(endpoint.port + 1, 1), 65535)
@@ -720,6 +727,7 @@ class EnvironmentCommands:
720
727
  host,
721
728
  port,
722
729
  public_origin=public_origin,
730
+ runtime_controller=getattr(self, "_managed_runtime", None),
723
731
  )
724
732
  try:
725
733
  control.start()
@@ -738,7 +746,6 @@ class EnvironmentCommands:
738
746
  "⚠️ Could not start supervisor control server on "
739
747
  f"{host}:{candidate_ports[0]}-{candidate_ports[-1]}: {last_error}"
740
748
  )
741
- cleanup_supervisor_advertisement(self.workspace.path)
742
749
  return None
743
750
 
744
751
  def _append_switch_log(
@@ -3732,8 +3739,64 @@ class EnvironmentCommands:
3732
3739
  print(f"✗ Invalid selection: {choice}")
3733
3740
  return None
3734
3741
 
3735
- @with_env_logging("workflow resolve", get_env_name=lambda self, args: self._get_env(args).name)
3736
3742
  def workflow_resolve(self, args: argparse.Namespace, logger=None) -> None:
3743
+ """Resolve with optional machine-readable, strict completion reporting."""
3744
+ import contextlib
3745
+ import json
3746
+
3747
+ json_output = getattr(args, "json", False)
3748
+ strict = getattr(args, "strict", False)
3749
+ if json_output and not args.auto:
3750
+ print("--json requires --auto (use explicit mappings for unknown nodes)", file=sys.stderr)
3751
+ raise SystemExit(2)
3752
+ if json_output and not (args.install or args.no_install):
3753
+ print("--json requires --install or --no-install", file=sys.stderr)
3754
+ raise SystemExit(2)
3755
+
3756
+ try:
3757
+ with contextlib.redirect_stdout(sys.stderr) if json_output else contextlib.nullcontext():
3758
+ self._workflow_resolve(args)
3759
+ if not (json_output or strict):
3760
+ return
3761
+ # Re-analyze AFTER downloads/installs, rather than reporting the stale plan.
3762
+ env = self._get_env(args)
3763
+ _, result = env.analyze_workflow_dependencies(args.name)
3764
+ missing = env.get_uninstalled_nodes(workflow_name=args.name)
3765
+ failed = env.get_workflow_failed_downloads(args.name)
3766
+ from .utils.workflow_result import resolution_report
3767
+ report = resolution_report(result, missing, failed)
3768
+ except (Exception, SystemExit) as exc:
3769
+ if json_output:
3770
+ print(json.dumps({"schema_version": 1, "workflow": args.name,
3771
+ "complete": False, "error": "resolution_failed"}))
3772
+ raise exc
3773
+ if json_output:
3774
+ print(json.dumps(report, indent=2))
3775
+ if strict and not report["complete"]:
3776
+ raise SystemExit(1)
3777
+
3778
+ def workflow_node_mapping(self, args: argparse.Namespace) -> None:
3779
+ """Explicit ownership edits through the public Environment facade."""
3780
+ import json
3781
+
3782
+ env = self._get_env(args)
3783
+ if not env.get_workflow_path(args.name).is_file():
3784
+ raise ValueError(f"Workflow not found: {args.name}")
3785
+ if args.mapping_command == "map":
3786
+ if env.get_manifest_node(args.package) is None:
3787
+ raise ValueError(f"Package is not tracked: {args.package}. Add or dev-link it first.")
3788
+ env.set_workflow_custom_node_mapping(args.name, args.node_type, args.package)
3789
+ elif args.mapping_command == "unmap":
3790
+ env.remove_workflow_custom_node_mapping(args.name, args.node_type)
3791
+ mappings = dict(env.get_workflow_custom_node_map(args.name))
3792
+ if args.json:
3793
+ print(json.dumps({"workflow": args.name, "custom_node_map": mappings}, indent=2))
3794
+ else:
3795
+ for node_type, package in sorted(mappings.items()):
3796
+ print(f"{node_type}: {package}")
3797
+
3798
+ @with_env_logging("workflow resolve", get_env_name=lambda self, args: self._get_env(args).name)
3799
+ def _workflow_resolve(self, args: argparse.Namespace, logger=None) -> None:
3737
3800
  """Resolve workflow dependencies interactively."""
3738
3801
  env = self._get_env(args)
3739
3802
 
@@ -3923,7 +3986,7 @@ class EnvironmentCommands:
3923
3986
  print(f" • {m.filename}")
3924
3987
 
3925
3988
  print("\n💡 Next:")
3926
- print(" Add Civitai API key: cg config --civitai-key <your-token>")
3989
+ print(" Add Civitai API key: cg auth set civitai")
3927
3990
  print(f" Try again: cg workflow resolve \"{args.name}\"")
3928
3991
  print(" Or commit anyway: cg commit -m \"...\" --allow-issues")
3929
3992
  else: