wf cli foundation

This commit is contained in:
lda
2026-06-01 02:27:43 +07:00 Verified
parent 6dab695b53
commit 3cd7f529e9
18 changed files with 318 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Workflow platform command-line interface."""
+44
View File
@@ -0,0 +1,44 @@
from __future__ import annotations
from typing import Annotated
import typer
from .commands import artifacts, caps, deployments, docs, drafts, explain, runs, schema
app = typer.Typer(
name="wf",
help="Workflow platform CLI.",
no_args_is_help=True,
)
@app.callback()
def root(
config: Annotated[
str,
typer.Option(
"--config",
help="Path to workflow/MCP config JSON.",
),
] = "wf_mcp.config.json",
) -> None:
"""Run workflow platform commands."""
# The root callback owns global options only. Command modules should load
# context explicitly so tests can call command functions without Typer state.
_ = config
app.add_typer(caps.app, name="cap")
app.add_typer(drafts.app, name="draft")
app.add_typer(artifacts.app, name="artifact")
app.add_typer(deployments.app, name="deploy")
app.add_typer(runs.app, name="run")
app.add_typer(docs.app, name="docs")
app.add_typer(schema.app, name="schema")
app.add_typer(explain.app, name="explain")
def main() -> None:
"""Console script entrypoint for `wf`."""
app()
+14
View File
@@ -0,0 +1,14 @@
"""Typer command groups for the wf CLI."""
from . import artifacts, caps, deployments, docs, drafts, explain, runs, schema
__all__ = [
"artifacts",
"caps",
"deployments",
"docs",
"drafts",
"explain",
"runs",
"schema",
]
+9
View File
@@ -0,0 +1,9 @@
from __future__ import annotations
import typer
app = typer.Typer(
name="artifact",
help="List and inspect saved workflow artifacts.",
no_args_is_help=True,
)
+9
View File
@@ -0,0 +1,9 @@
from __future__ import annotations
import typer
app = typer.Typer(
name="cap",
help="Inspect and call workflow capabilities.",
no_args_is_help=True,
)
+9
View File
@@ -0,0 +1,9 @@
from __future__ import annotations
import typer
app = typer.Typer(
name="deploy",
help="Save, inspect, validate, and delete workflow deployments.",
no_args_is_help=True,
)
+9
View File
@@ -0,0 +1,9 @@
from __future__ import annotations
import typer
app = typer.Typer(
name="docs",
help="List and read workflow documentation resources.",
no_args_is_help=True,
)
+9
View File
@@ -0,0 +1,9 @@
from __future__ import annotations
import typer
app = typer.Typer(
name="draft",
help="Create, inspect, patch, validate, and save draft workflows.",
no_args_is_help=True,
)
+9
View File
@@ -0,0 +1,9 @@
from __future__ import annotations
import typer
app = typer.Typer(
name="explain",
help="Explain workflow diagnostic and CLI error codes.",
no_args_is_help=True,
)
+9
View File
@@ -0,0 +1,9 @@
from __future__ import annotations
import typer
app = typer.Typer(
name="run",
help="Run workflow deployments and inspect durable runs.",
no_args_is_help=True,
)
+9
View File
@@ -0,0 +1,9 @@
from __future__ import annotations
import typer
app = typer.Typer(
name="schema",
help="Print expected input shapes for wf commands.",
no_args_is_help=True,
)
+35
View File
@@ -0,0 +1,35 @@
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from wf_mcp.broker import build_service_from_config, load_broker_config
from wf_mcp.broker.service import WfMcpService
from wf_mcp.workflow_surface import WorkflowSurfaceHandlers
@dataclass(frozen=True)
class CliContext:
"""Protocol-neutral CLI handle over the current workflow service stack.
V1 intentionally reuses wf_mcp service construction because that is where
config, store, source, artifact, draft, and run wiring currently lives. Keep
this dependency behind context.py so later extraction does not affect every
command module.
"""
config_path: Path
service: WfMcpService
handlers: WorkflowSurfaceHandlers
def load_cli_context(config_path: str | Path) -> CliContext:
"""Load config and build workflow-surface handlers for CLI commands."""
resolved_config_path = Path(config_path)
config = load_broker_config(resolved_config_path)
service = build_service_from_config(config)
return CliContext(
config_path=resolved_config_path,
service=service,
handlers=WorkflowSurfaceHandlers(service),
)
+44
View File
@@ -0,0 +1,44 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
class CliInputError(ValueError):
"""Raised when CLI JSON/file input cannot be parsed safely."""
def parse_json_input(
*,
input_json: str | None,
input_file: Path | None,
) -> dict[str, Any]:
"""Parse exactly one JSON object from inline JSON or a file path."""
if input_json is not None and input_file is not None:
raise CliInputError("--input and --input-file are mutually exclusive")
if input_json is None and input_file is None:
return {}
raw = input_json if input_json is not None else _read_input_file(input_file)
try:
payload = json.loads(raw)
except json.JSONDecodeError as exc:
raise CliInputError(f"invalid JSON input: {exc.msg}") from exc
if not isinstance(payload, dict):
raise CliInputError("JSON input must be an object")
return payload
def emit_json(payload: Any) -> None:
"""Write JSON output in the CLI default machine-readable format."""
print(json.dumps(payload, indent=2, sort_keys=True))
def _read_input_file(path: Path | None) -> str:
"""Read a required JSON input file."""
if path is None:
raise CliInputError("input file path is required")
try:
return path.read_text(encoding="utf-8")
except OSError as exc:
raise CliInputError(f"could not read input file {path!s}: {exc}") from exc