Files
Medios-Macina/SYS/cmdlet_api.py
2026-01-02 02:28:59 -08:00

221 lines
6.2 KiB
Python

from __future__ import annotations
import contextlib
import io
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, Sequence
from SYS import pipeline as ctx
from SYS.models import PipelineStageContext
from SYS.rich_display import capture_rich_output
CmdletCallable = Callable[[Any, Sequence[str], Dict[str, Any]], int]
@dataclass(slots=True)
class CmdletRunResult:
"""Programmatic result for a single cmdlet invocation."""
name: str
args: Sequence[str]
exit_code: int = 0
emitted: List[Any] = field(default_factory=list)
# Best-effort: cmdlets can publish tables/items via pipeline state even when
# they don't emit pipeline items.
result_table: Optional[Any] = None
result_items: List[Any] = field(default_factory=list)
result_subject: Optional[Any] = None
stdout: str = ""
stderr: str = ""
error: Optional[str] = None
def _normalize_cmd_name(name: str) -> str:
return str(name or "").replace("_", "-").strip().lower()
def resolve_cmdlet(cmd_name: str) -> Optional[CmdletCallable]:
"""Resolve a cmdlet callable by name from the registry (aliases supported)."""
try:
from SYS.cmdlet_catalog import ensure_registry_loaded
ensure_registry_loaded()
except Exception:
pass
try:
import cmdlet as cmdlet_pkg
return cmdlet_pkg.get(cmd_name)
except Exception:
return None
def run_cmdlet(
cmd: str | CmdletCallable,
args: Sequence[str] | None,
config: Dict[str, Any],
*,
piped: Any = None,
isolate: bool = True,
capture_output: bool = True,
stage_index: int = 0,
total_stages: int = 1,
pipe_index: Optional[int] = None,
worker_id: Optional[str] = None,
) -> CmdletRunResult:
"""Run a single cmdlet programmatically and return structured results.
This is intended for TUI/webapp consumers that want cmdlet behavior without
going through the interactive CLI loop.
Notes:
- When `isolate=True` (default) this runs inside `ctx.new_pipeline_state()` so
global CLI pipeline state is not mutated.
- Output capturing covers both normal `print()` and Rich output via
`capture_rich_output()`.
"""
normalized_args: Sequence[str] = list(args or [])
if isinstance(cmd, str):
name = _normalize_cmd_name(cmd)
cmd_fn = resolve_cmdlet(name)
else:
name = getattr(cmd, "__name__", "cmdlet")
cmd_fn = cmd
result = CmdletRunResult(name=name, args=normalized_args)
if not callable(cmd_fn):
result.exit_code = 1
result.error = f"Unknown command: {name}"
result.stderr = result.error
return result
stage_ctx = PipelineStageContext(
stage_index=int(stage_index),
total_stages=int(total_stages),
pipe_index=pipe_index,
worker_id=worker_id,
)
stdout_buffer = io.StringIO()
stderr_buffer = io.StringIO()
stage_text = " ".join([name, *list(normalized_args)]).strip()
state_cm = ctx.new_pipeline_state() if isolate else contextlib.nullcontext()
with state_cm:
# Keep behavior predictable: start from a clean slate.
try:
ctx.reset()
except Exception:
pass
try:
ctx.set_stage_context(stage_ctx)
except Exception:
pass
try:
ctx.set_current_cmdlet_name(name)
except Exception:
pass
try:
ctx.set_current_stage_text(stage_text)
except Exception:
pass
try:
ctx.set_current_command_text(stage_text)
except Exception:
pass
try:
run_cm = (
capture_rich_output(stdout=stdout_buffer, stderr=stderr_buffer)
if capture_output
else contextlib.nullcontext()
)
with run_cm:
with (
contextlib.redirect_stdout(stdout_buffer)
if capture_output
else contextlib.nullcontext()
):
with (
contextlib.redirect_stderr(stderr_buffer)
if capture_output
else contextlib.nullcontext()
):
result.exit_code = int(cmd_fn(piped, list(normalized_args), config))
except Exception as exc:
result.exit_code = 1
result.error = f"{type(exc).__name__}: {exc}"
finally:
result.stdout = stdout_buffer.getvalue()
result.stderr = stderr_buffer.getvalue()
# Prefer cmdlet emits (pipeline semantics).
try:
result.emitted = list(stage_ctx.emits or [])
except Exception:
result.emitted = []
# Mirror CLI behavior: if cmdlet emitted items and there is no overlay table,
# make emitted items the last result items for downstream consumers.
try:
has_overlay = bool(ctx.get_display_table())
except Exception:
has_overlay = False
if result.emitted and not has_overlay:
try:
ctx.set_last_result_items_only(list(result.emitted))
except Exception:
pass
# Best-effort snapshot of visible results.
try:
result.result_table = (
ctx.get_display_table() or ctx.get_current_stage_table() or ctx.get_last_result_table()
)
except Exception:
result.result_table = None
try:
result.result_items = list(ctx.get_last_result_items() or [])
except Exception:
result.result_items = []
try:
result.result_subject = ctx.get_last_result_subject()
except Exception:
result.result_subject = None
# Cleanup stage-local markers.
try:
ctx.clear_current_stage_text()
except Exception:
pass
try:
ctx.clear_current_cmdlet_name()
except Exception:
pass
try:
ctx.clear_current_command_text()
except Exception:
pass
try:
ctx.set_stage_context(None)
except Exception:
pass
return result