feat: select workflow cli target from config

This commit is contained in:
lda
2026-06-03 08:35:33 +07:00 Verified
parent 99a4d37618
commit b9b60250e8
3 changed files with 157 additions and 17 deletions
+18 -1
View File
@@ -23,9 +23,26 @@ def root(
help="Path to workflow/MCP config JSON.",
),
] = "wf_mcp.config.json",
local: Annotated[
bool,
typer.Option("--local", help="Force same-process local workflow target."),
] = False,
url: Annotated[
str | None,
typer.Option("--url", help="Override workflow JSON-RPC target URL."),
] = None,
timeout: Annotated[
float | None,
typer.Option("--timeout", min=0.1, help="Override RPC timeout seconds."),
] = None,
) -> None:
"""Run workflow platform commands."""
ctx.obj = {"config_path": config}
ctx.obj = {
"config_path": config,
"force_local": local,
"rpc_url": url,
"rpc_timeout_seconds": timeout,
}
app.add_typer(caps.app, name="cap")
+74 -10
View File
@@ -9,21 +9,18 @@ from wf_api import WorkflowApi
from wf_mcp.broker import build_service_from_config, load_broker_config
from wf_mcp.broker.service import WfMcpService
from wf_mcp.broker.service.workflow_operation_context import context_from_service
from wf_config import FilesystemStoreConfig, LocalTargetConfig, RpcHttpTargetConfig, load_workflow_config
from wf_server import build_local_static_workflow_server
from wf_transport_rpc_http import RpcWorkflowApiClient
@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.
"""
"""Protocol-neutral CLI handle over local or remote workflow operations."""
config_path: Path
service: WfMcpService
handlers: WorkflowApi
service: WfMcpService | None
handlers: WorkflowApi | RpcWorkflowApiClient
def config_path_from_context(ctx: typer.Context) -> str:
@@ -33,9 +30,16 @@ def config_path_from_context(ctx: typer.Context) -> str:
return value if isinstance(value, str) else "wf_mcp.config.json"
def load_cli_context(config_path: str | Path) -> CliContext:
def load_cli_context(
config_path: str | Path,
*,
force_local: bool = False,
rpc_url: str | None = None,
rpc_timeout_seconds: float | None = None,
) -> CliContext:
"""Load config and build workflow-surface handlers for CLI commands."""
resolved_config_path = Path(config_path)
if resolved_config_path.name == "wf_mcp.config.json":
config = load_broker_config(resolved_config_path)
service = build_service_from_config(config)
return CliContext(
@@ -43,3 +47,63 @@ def load_cli_context(config_path: str | Path) -> CliContext:
service=service,
handlers=WorkflowApi(context_from_service(service)),
)
config = load_workflow_config(resolved_config_path)
target = config.client.target
if rpc_url is not None:
timeout = rpc_timeout_seconds if rpc_timeout_seconds is not None else 30.0
return CliContext(
config_path=resolved_config_path,
service=None,
handlers=RpcWorkflowApiClient(url=rpc_url, timeout_seconds=timeout),
)
if force_local or isinstance(target, LocalTargetConfig):
store = config.server.store
if not isinstance(store, FilesystemStoreConfig):
raise ValueError("local CLI target currently requires filesystem store")
server = build_local_static_workflow_server(store.root)
return CliContext(
config_path=resolved_config_path,
service=None,
handlers=server.api,
)
if isinstance(target, RpcHttpTargetConfig):
return CliContext(
config_path=resolved_config_path,
service=None,
handlers=RpcWorkflowApiClient(
url=target.url,
timeout_seconds=(
rpc_timeout_seconds
if rpc_timeout_seconds is not None
else target.timeout_seconds
),
),
)
raise ValueError(f"unsupported workflow target {target!r}")
def force_local_from_context(ctx: typer.Context) -> bool:
obj = ctx.obj if isinstance(ctx.obj, dict) else {}
return bool(obj.get("force_local", False))
def rpc_url_from_context(ctx: typer.Context) -> str | None:
obj = ctx.obj if isinstance(ctx.obj, dict) else {}
value = obj.get("rpc_url")
return value if isinstance(value, str) else None
def rpc_timeout_from_context(ctx: typer.Context) -> float | None:
obj = ctx.obj if isinstance(ctx.obj, dict) else {}
value = obj.get("rpc_timeout_seconds")
return value if isinstance(value, float | int) else None
def load_cli_context_from_typer(ctx: typer.Context) -> CliContext:
return load_cli_context(
config_path_from_context(ctx),
force_local=force_local_from_context(ctx),
rpc_url=rpc_url_from_context(ctx),
rpc_timeout_seconds=rpc_timeout_from_context(ctx),
)
+59
View File
@@ -0,0 +1,59 @@
from __future__ import annotations
import json
from wf_cli.context import load_cli_context
from wf_transport_rpc_http import RpcWorkflowApiClient
def test_load_cli_context_uses_rpc_client_for_rpc_http_target(tmp_path) -> None:
config_path = tmp_path / "wf.json"
config_path.write_text(
json.dumps(
{
"version": 1,
"client": {
"target": {
"kind": "rpc_http",
"url": "http://127.0.0.1:8765/rpc",
"timeout_seconds": 9,
}
},
}
),
encoding="utf-8",
)
context = load_cli_context(config_path)
assert isinstance(context.handlers, RpcWorkflowApiClient)
assert context.handlers.url == "http://127.0.0.1:8765/rpc"
assert context.handlers.timeout_seconds == 9
assert context.service is None
def test_load_cli_context_local_override_beats_rpc_config(tmp_path) -> None:
config_path = tmp_path / "wf.json"
config_path.write_text(
json.dumps(
{
"version": 1,
"client": {
"target": {
"kind": "rpc_http",
"url": "http://127.0.0.1:8765/rpc",
}
},
"server": {
"store": {"kind": "filesystem", "root": ".wf_store"},
},
}
),
encoding="utf-8",
)
context = load_cli_context(config_path, force_local=True)
assert not isinstance(context.handlers, RpcWorkflowApiClient)
assert context.service is None
assert context.config_path == config_path