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
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`.
- Capture UX gaps as small follow-up items: confusing errors, missing examples,
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.
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
JSON is the default output format for every command.
+2
View File
@@ -15,6 +15,7 @@ from .commands import (
runs,
schema,
sources,
status,
)
from .commands import (
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(config_commands.app, name="config")
app.command("explain")(explain.explain_command)
app.command("status")(status.status_command)
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["status"] == "completed"
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