diff --git a/comfy_cli/command/workflow.py b/comfy_cli/command/workflow.py index 850bd918..ad5cf94b 100644 --- a/comfy_cli/command/workflow.py +++ b/comfy_cli/command/workflow.py @@ -1934,6 +1934,9 @@ def validate_cmd( from comfy_cli.command import workflow_edit as _wedit # noqa: E402 +app.command("insert-workflow", help="Insert a complete workflow; emits one insert_workflow op.")( + _wedit.insert_workflow_cmd +) app.command("add-node", help="Add a node to the graph; emits an add_node op.")(_wedit.add_node_cmd) app.command("connect", help="Wire an output slot to an input slot; emits a connect op.")(_wedit.connect_cmd) app.command("set-widget", help="Set a widget by name (`.`); emits a set_widget op.")(_wedit.set_widget_cmd) diff --git a/comfy_cli/command/workflow_edit.py b/comfy_cli/command/workflow_edit.py index 39e03974..7279ccaf 100644 --- a/comfy_cli/command/workflow_edit.py +++ b/comfy_cli/command/workflow_edit.py @@ -128,6 +128,14 @@ def _finish(renderer, p, workflow: dict, op: dict, base_version: int, stdout: bo renderer.emit(payload, command=command, changed=not stdout) +def _emit_op(renderer, p: Path, op: dict, base_version: int, command: str) -> None: + """Emit an op without applying it or writing the source workflow.""" + payload = {"workflow": str(p), "op": op, "base_version": base_version, "wrote": None} + if renderer.is_pretty(): + rprint(f"[bold green]✓[/bold green] {op['op']} emitted for [dim]{p}[/dim]") + renderer.emit(payload, command=command, changed=False) + + def _graph_or_exit(input_path, host, port, renderer, where=None): return _get_graph(input_path, host, port, where=where) @@ -137,6 +145,34 @@ def _graph_or_exit(input_path, host, port, renderer, where=None): # --------------------------------------------------------------------------- +@tracking.track_command("workflow") +def insert_workflow_cmd( + file: Annotated[str, typer.Argument(help="Source frontend-format workflow JSON; emit-only, file is not modified.")], + template: Annotated[str, typer.Argument(help="Frontend-format workflow JSON to insert, or '-' for stdin.")], + actor: ActorOpt = "cli", + base_version: BaseVersionOpt = 0, +): + """Insert a workflow template and emit one atomic ``insert_workflow`` op.""" + renderer = get_renderer() + renderer.command = "workflow insert-workflow" + p, workflow = _load_workflow_or_fail(renderer, file) + try: + if template == "-": + import sys + + raw = sys.stdin.read() + else: + raw = Path(template).expanduser().read_text(encoding="utf-8") + inserted = json.loads(raw) + if not isinstance(inserted, dict): + raise ValueError("template must be a JSON object") + _, op = workflow_ops.insert_workflow(workflow, inserted, actor=actor, base_version=base_version) + except (OSError, json.JSONDecodeError, UnicodeDecodeError, ValueError) as e: + _emit_edit_error(renderer, e, hint="provide a frontend-format workflow template JSON file") + raise typer.Exit(code=1) from e + _emit_op(renderer, p, op, base_version, "workflow insert-workflow") + + @tracking.track_command("workflow") def add_node_cmd( file: Annotated[str, typer.Argument(help="Frontend-format workflow JSON.")], @@ -760,7 +796,13 @@ def apply_cmd( except workflow_ops.NotBatchableError as e: # A standalone-only op (clear) inside the batch: its own registered code, # with the hint naming the standalone command to run instead. - renderer.error(code=e.code, message=f"batch failed: {e}", hint=e.hint) + details = None + if ack == "summary": + details = { + "failed": {"index": e.spec_index, "op": e.spec_op, "code": e.code}, + "applied_count": e.applied_count, + } + renderer.error(code=e.code, message=f"batch failed: {e}", hint=e.hint, details=details) raise typer.Exit(code=1) from e except workflow_ops.DeprecatedNodeType as e: renderer.error( diff --git a/comfy_cli/discovery.py b/comfy_cli/discovery.py index d7af9805..03c0aa47 100644 --- a/comfy_cli/discovery.py +++ b/comfy_cli/discovery.py @@ -98,6 +98,7 @@ "comfy workflow notes": "workflow", "comfy workflow print": "workflow", # structured edit primitives + recipes (CRDT op-based authoring) + "comfy workflow insert-workflow": "workflow", "comfy workflow add-node": "workflow", "comfy workflow connect": "workflow", "comfy workflow set-widget": "workflow", diff --git a/comfy_cli/error_codes.py b/comfy_cli/error_codes.py index d5de7ac8..c7b81db3 100644 --- a/comfy_cli/error_codes.py +++ b/comfy_cli/error_codes.py @@ -600,6 +600,12 @@ class ErrorCode: "(docs/op-vocabulary-v1.md: batchable = no) and the batch was rejected atomically — nothing was applied.", "run the standalone `comfy workflow reset-doc --confirm` first, then apply the remaining ops as a batch", ), + ErrorCode( + "workflow_insert_workflow_not_batchable", + "A batch contained an `insert_workflow` op. A complete workflow insertion is one standalone atomic op, " + "so nesting it in the spec batch protocol is rejected and nothing is applied.", + "run `comfy workflow insert-workflow