feat: add workflow status command

This commit is contained in:
lda
2026-06-09 02:40:24 +07:00 Verified
parent 04122e12ac
commit 39a524aee4
6 changed files with 247 additions and 1 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ MCP entrypoint and compatibility surface.
The platform is usable enough to test as a product. Next work should focus on The platform is usable enough to test as a product. Next work should focus on
clear operator feedback before adding more architecture. clear operator feedback before adding more architecture.
- Add `wf status` as a compact target/server status command. - Completed: `wf status` is a compact read-only target/server status command.
- Run a real CLI smoke script against `wf-rpc-server --config wf.config.json`. - Run a real CLI smoke script against `wf-rpc-server --config wf.config.json`.
- Capture UX gaps as small follow-up items: confusing errors, missing examples, - Capture UX gaps as small follow-up items: confusing errors, missing examples,
poor command help, and target/config ambiguity. poor command help, and target/config ambiguity.
+11
View File
@@ -92,6 +92,17 @@ wf --url http://127.0.0.1:8765/rpc admin registry apply
Apply updates the running server's source graph from desired registry state. Apply updates the running server's source graph from desired registry state.
It is explicit in v1; registry mutations are not auto-applied. It is explicit in v1; registry mutations are not auto-applied.
Check the selected target:
```bash
wf status
wf --url http://127.0.0.1:8765/rpc status
```
`status` is read-only. It reports the selected target, capability/source
availability, admin counts, auth record count, and desired registry count when
the target exposes those admin surfaces. It does not return auth payload values.
## Output Policy ## Output Policy
JSON is the default output format for every command. JSON is the default output format for every command.
+2
View File
@@ -15,6 +15,7 @@ from .commands import (
runs, runs,
schema, schema,
sources, sources,
status,
) )
from .commands import ( from .commands import (
config as config_commands, config as config_commands,
@@ -80,6 +81,7 @@ app.add_typer(docs.app, name="docs")
app.add_typer(schema.app, name="schema") app.add_typer(schema.app, name="schema")
app.add_typer(config_commands.app, name="config") app.add_typer(config_commands.app, name="config")
app.command("explain")(explain.explain_command) app.command("explain")(explain.explain_command)
app.command("status")(status.status_command)
def main() -> None: def main() -> None:
+123
View File
@@ -0,0 +1,123 @@
from __future__ import annotations
from typing import Any
import typer
from wf_cli.context import CliContext, CliTyperState, load_cli_context_from_typer
from wf_cli.io import emit_json
from wf_cli.remote_errors import run_cli_operation
def status_command(ctx: typer.Context) -> None:
"""Print a compact read-only summary of the selected workflow target."""
state = CliTyperState.from_context(ctx)
context = load_cli_context_from_typer(ctx)
target_url = _target_url(context, state)
target: dict[str, Any] = {
"mode": "remote" if target_url is not None else "local",
"config_path": str(context.config_path),
"url": target_url,
}
status_data = run_cli_operation(context, _fetch_status_data(context))
payload: dict[str, Any] = {"target": target, **status_data}
emit_json(payload)
def _target_url(context: CliContext, state: CliTyperState) -> str | None:
"""Return the resolved RPC URL whether it came from --url or config."""
if state.rpc_url is not None:
return state.rpc_url
url = getattr(context.handlers, "url", None)
return url if isinstance(url, str) else None
async def _fetch_status_data(context: CliContext) -> dict[str, Any]:
capabilities = await context.handlers.list_capabilities(limit=20)
items = capabilities.get("capabilities", [])
names = [
item.get("name")
for item in items
if isinstance(item, dict) and isinstance(item.get("name"), str)
]
workflow = {
"capability_count": len(items),
"sample_capabilities": names[:5],
}
sources = await _fetch_sources(context)
admin = await _fetch_admin(context)
registry = await _fetch_registry(context)
return {
"workflow": workflow,
"sources": sources,
"admin": admin,
"registry": registry,
}
async def _fetch_sources(context: CliContext) -> dict[str, Any]:
# Intentionally broad: graceful degradation when source admin is unavailable
try:
payload = await context.source_admin.list_sources(limit=20)
except Exception as exc:
return _unavailable(exc)
sources = payload.get("sources", [])
source_ids = [
item.get("id")
for item in sources
if isinstance(item, dict) and isinstance(item.get("id"), str)
]
return {
"available": True,
"source_count": len(sources),
"sample_sources": source_ids[:5],
}
async def _fetch_admin(context: CliContext) -> dict[str, Any]:
# Intentionally broad: graceful degradation when admin surfaces are unavailable
try:
connections = await context.admin.list_connections()
statuses = await context.admin.get_connection_statuses()
events = await context.admin.list_events()
except Exception as exc:
return _unavailable(exc)
# Auth is optional - some targets (local/static) don't have auth admin
auth_count = 0
try:
auth = await context.admin.list_auth_records()
auth_count = len(auth.get("auth_records", []))
except Exception:
pass
return {
"available": True,
"connection_count": len(connections.get("connections", [])),
"status_count": len(statuses.get("statuses", [])),
"event_count": len(events.get("events", [])),
"auth_count": auth_count,
}
async def _fetch_registry(context: CliContext) -> dict[str, Any]:
admin = context.source_registry_admin
if admin is None:
return {"available": False, "reason": "source registry admin is not configured"}
# Intentionally broad: graceful degradation when registry is unavailable
try:
payload = await admin.list_registry_entries(limit=20)
except Exception as exc:
return _unavailable(exc)
return {
"available": True,
"entry_count": len(payload.get("entries", [])),
}
def _unavailable(exc: Exception) -> dict[str, Any]:
return {
"available": False,
"reason": str(exc),
}
+62
View File
@@ -643,3 +643,65 @@ def test_wf_remote_run_resume_interrupted_deployment(monkeypatch, tmp_path) -> N
assert resumed_payload["run_id"] == started_payload["run_id"] assert resumed_payload["run_id"] == started_payload["run_id"]
assert resumed_payload["status"] == "completed" assert resumed_payload["status"] == "completed"
assert resumed_payload["outcome"] == "submitted" assert resumed_payload["outcome"] == "submitted"
def test_wf_status_uses_rpc_url_override(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
result = CliRunner().invoke(
app,
[
"--config",
str(config_path),
"--url",
"http://test/rpc",
"status",
],
)
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert payload["target"]["mode"] == "remote"
assert payload["target"]["url"] == "http://test/rpc"
assert payload["workflow"]["capability_count"] >= 1
assert payload["sources"]["available"] is True
assert payload["admin"]["available"] is True
assert payload["registry"]["available"] is False
def test_wf_status_reports_rpc_config_target(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text(
json.dumps(
{
"version": 1,
"client": {
"target": {
"kind": "rpc_http",
"url": "http://test/rpc",
}
},
}
),
encoding="utf-8",
)
result = CliRunner().invoke(
app,
[
"--config",
str(config_path),
"status",
],
)
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert payload["target"]["mode"] == "remote"
assert payload["target"]["url"] == "http://test/rpc"
assert payload["workflow"]["capability_count"] >= 1
+48
View File
@@ -0,0 +1,48 @@
from __future__ import annotations
import json
from typer.testing import CliRunner
from wf_cli.app import app
def test_wf_status_local_static_target(tmp_path) -> None:
config_path = tmp_path / "wf.json"
config_path.write_text(
json.dumps(
{
"version": 1,
"client": {"target": {"kind": "local"}},
"server": {
"store": {
"kind": "filesystem",
"root": str(tmp_path / "store"),
}
},
}
),
encoding="utf-8",
)
result = CliRunner().invoke(
app,
[
"--config",
str(config_path),
"--local",
"status",
],
)
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert payload["target"]["mode"] == "local"
assert payload["target"]["config_path"] == str(config_path)
assert payload["target"]["url"] is None
assert payload["workflow"]["capability_count"] >= 1
assert "wf.std.constant" in payload["workflow"]["sample_capabilities"]
assert payload["sources"]["available"] is True
assert payload["sources"]["source_count"] >= 1
assert payload["admin"]["available"] is True
assert payload["registry"]["available"] is False