Files
Medios-Macina/cmdnat/help.py

447 lines
15 KiB
Python
Raw Normal View History

2025-12-11 12:47:30 -08:00
from __future__ import annotations
2026-01-02 02:28:59 -08:00
from typing import Any, Dict, Sequence, List, Optional, Tuple
2025-12-11 12:47:30 -08:00
import shlex
import sys
2025-12-12 21:55:38 -08:00
from cmdlet._shared import Cmdlet, CmdletArg, parse_cmdlet_args
2026-01-02 02:28:59 -08:00
from cmdlet import REGISTRY as CMDLET_REGISTRY, ensure_cmdlet_modules_loaded
2025-12-11 19:04:02 -08:00
from SYS.logger import log
from SYS.result_table import ResultTable
from SYS import pipeline as ctx
2025-12-11 12:47:30 -08:00
def _normalize_choice_list(arg_names: Optional[List[str]]) -> List[str]:
return sorted(set(arg_names or []))
2026-01-03 03:37:48 -08:00
_HELP_EXAMPLE_SOURCE_COMMAND = ".help-example"
def _example_for_cmd(name: str) -> List[str]:
2025-12-11 12:47:30 -08:00
"""Return example invocations for a given command (best-effort)."""
lookup = {
".adjective": [
'.adjective -add "example"',
'.adjective -delete "example"',
],
}
key = name.replace("_", "-").lower()
return lookup.get(key, [])
2026-01-03 03:37:48 -08:00
def _parse_example_tokens(example: str) -> List[str]:
"""Split an example string into CLI tokens suitable for @N selection."""
text = str(example or "").strip()
if not text:
return []
try:
tokens = shlex.split(text)
except Exception:
tokens = text.split()
return [token for token in tokens if token]
2026-01-02 02:28:59 -08:00
def _normalize_cmdlet_key(name: Optional[str]) -> str:
return str(name or "").replace("_", "-").lower().strip()
def _cmdlet_aliases(cmdlet_obj: Cmdlet) -> List[str]:
aliases: List[str] = []
for attr in ("alias", "aliases"):
raw_aliases = getattr(cmdlet_obj, attr, None)
if isinstance(raw_aliases, (list, tuple, set)):
candidates = raw_aliases
else:
candidates = (raw_aliases,)
for alias in candidates or ():
text = str(alias or "").strip()
if text:
aliases.append(text)
seen: set[str] = set()
deduped: List[str] = []
for alias in aliases:
key = alias.lower()
if key in seen:
continue
seen.add(key)
deduped.append(alias)
return deduped
def _cmdlet_arg_to_dict(arg: CmdletArg) -> Dict[str, Any]:
return {
"name": str(getattr(arg, "name", "") or ""),
"type": str(getattr(arg, "type", "") or ""),
"required": bool(getattr(arg, "required", False)),
"description": str(getattr(arg, "description", "") or ""),
"choices": [str(c) for c in list(getattr(arg, "choices", []) or [])],
"alias": str(getattr(arg, "alias", "") or ""),
"variadic": bool(getattr(arg, "variadic", False)),
"usage": str(getattr(arg, "usage", "") or ""),
"query_key": getattr(arg, "query_key", None),
"query_aliases": [str(c) for c in list(getattr(arg, "query_aliases", []) or [])],
"query_only": bool(getattr(arg, "query_only", False)),
"requires_db": bool(getattr(arg, "requires_db", False)),
}
def _build_alias_map_from_metadata(metadata: Dict[str, Dict[str, Any]]) -> Dict[str, str]:
mapping: Dict[str, str] = {}
for name, meta in metadata.items():
canonical = _normalize_cmdlet_key(name)
if canonical:
mapping[canonical] = name
for alias in meta.get("aliases", []) or []:
alias_key = _normalize_cmdlet_key(alias)
if alias_key:
mapping[alias_key] = name
return mapping
def _gather_metadata_from_cmdlet_classes() -> Tuple[Dict[str, Dict[str, Any]], Dict[str, str]]:
metadata: Dict[str, Dict[str, Any]] = {}
alias_map: Dict[str, str] = {}
try:
ensure_cmdlet_modules_loaded()
except Exception:
pass
for module in list(sys.modules.values()):
mod_name = getattr(module, "__name__", "") or ""
if not (mod_name.startswith("cmdlet.") or mod_name == "cmdlet" or mod_name.startswith("cmdnat.")):
continue
cmdlet_obj = getattr(module, "CMDLET", None)
if not isinstance(cmdlet_obj, Cmdlet):
continue
canonical_key = _normalize_cmdlet_key(getattr(cmdlet_obj, "name", None) or "")
if not canonical_key:
continue
2026-01-03 03:37:48 -08:00
example_entries: List[str] = []
seen_example_entries: set[str] = set()
for attr in ("examples", "example"):
for value in (getattr(cmdlet_obj, attr, []) or []):
text = str(value or "").strip()
if not text or text in seen_example_entries:
continue
seen_example_entries.add(text)
example_entries.append(text)
2026-01-02 02:28:59 -08:00
entry = {
"name": str(getattr(cmdlet_obj, "name", "") or canonical_key),
"summary": str(getattr(cmdlet_obj, "summary", "") or ""),
"usage": str(getattr(cmdlet_obj, "usage", "") or ""),
"aliases": _cmdlet_aliases(cmdlet_obj),
"details": list(getattr(cmdlet_obj, "detail", []) or []),
"args": [_cmdlet_arg_to_dict(a) for a in getattr(cmdlet_obj, "arg", []) or []],
2026-01-03 03:37:48 -08:00
"examples": example_entries,
2026-01-02 02:28:59 -08:00
"raw": getattr(cmdlet_obj, "raw", None),
}
metadata[canonical_key] = entry
alias_map[canonical_key] = canonical_key
for alias in entry["aliases"]:
alias_key = _normalize_cmdlet_key(alias)
if alias_key:
alias_map[alias_key] = canonical_key
for registry_name in CMDLET_REGISTRY.keys():
normalized = _normalize_cmdlet_key(registry_name)
if not normalized or normalized in alias_map:
continue
alias_map[normalized] = normalized
metadata.setdefault(
normalized,
{
"name": normalized,
"aliases": [],
"usage": "",
"summary": "",
"details": [],
"args": [],
"raw": None,
},
)
return metadata, alias_map
def _find_cmd_metadata(name: str,
metadata: Dict[str,
Dict[str,
Any]]) -> Optional[Dict[str,
Any]]:
2025-12-11 12:47:30 -08:00
target = name.replace("_", "-").lower()
for cmd_name, meta in metadata.items():
if target == cmd_name:
return meta
aliases = meta.get("aliases", []) or []
if target in aliases:
return meta
return None
2025-12-29 17:05:03 -08:00
def _render_list(
metadata: Dict[str,
Dict[str,
Any]],
filter_text: Optional[str],
args: Sequence[str]
2025-12-29 17:05:03 -08:00
) -> None:
2025-12-11 12:47:30 -08:00
table = ResultTable("Help")
table.set_source_command(".help", list(args))
items: List[Dict[str, Any]] = []
needle = (filter_text or "").lower().strip()
for name in sorted(metadata.keys()):
meta = metadata[name]
summary = meta.get("summary", "") or ""
if needle and needle not in name.lower() and needle not in summary.lower():
continue
row = table.add_row()
row.add_column("Cmd", name)
aliases = ", ".join(meta.get("aliases", []) or [])
row.add_column("Aliases", aliases)
arg_names = [a.get("name") for a in meta.get("args", []) if a.get("name")]
row.add_column("Args", ", ".join(f"-{a}" for a in arg_names))
table.set_row_selection_args(len(table.rows) - 1, ["-cmd", name])
items.append(meta)
ctx.set_last_result_table(table, items)
ctx.set_current_stage_table(table)
2026-01-03 03:37:48 -08:00
setattr(table, "_rendered_by_cmdlet", True)
from SYS.rich_display import stdout_console
2025-12-20 23:57:44 -08:00
stdout_console().print(table)
2025-12-11 12:47:30 -08:00
2026-01-03 03:37:48 -08:00
def _render_detail(meta: Dict[str, Any], _args: Sequence[str]) -> None:
cmd_name = str(meta.get("name", "") or "cmd")
title = f"Help: {cmd_name}"
2025-12-11 12:47:30 -08:00
summary = meta.get("summary", "")
usage = meta.get("usage", "")
aliases = meta.get("aliases", []) or []
2026-01-03 03:37:48 -08:00
details = meta.get("details", []) or []
seen_examples: set[str] = set()
explicit_example: List[str] = []
for attr in ("examples", "example"):
for value in (meta.get(attr, []) or []):
text = str(value or "").strip()
if not text or text in seen_examples:
continue
seen_examples.add(text)
explicit_example.append(text)
fallback_example = _example_for_cmd(cmd_name)
for fallback in fallback_example:
text = str(fallback or "").strip()
if not text or text in seen_examples:
continue
seen_examples.add(text)
explicit_example.append(text)
2025-12-11 12:47:30 -08:00
2026-01-03 03:37:48 -08:00
header_lines: List[str] = []
2025-12-11 12:47:30 -08:00
if summary:
header_lines.append(summary)
if usage:
header_lines.append(f"Usage: {usage}")
if aliases:
header_lines.append("Aliases: " + ", ".join(aliases))
2026-01-03 03:37:48 -08:00
if details:
header_lines.extend(str(line) for line in details if str(line).strip())
if explicit_example:
header_lines.append("Examples available below")
2025-12-11 12:47:30 -08:00
args_meta = meta.get("args", []) or []
2026-01-03 03:37:48 -08:00
args_table = ResultTable(title)
if header_lines:
args_table.set_header_lines(header_lines)
args_table.set_preserve_order(True)
args_table.set_no_choice(True)
2025-12-11 12:47:30 -08:00
if not args_meta:
2026-01-03 03:37:48 -08:00
row = args_table.add_row()
2025-12-11 12:47:30 -08:00
row.add_column("Arg", "(none)")
row.add_column("Type", "")
row.add_column("Req", "")
row.add_column("Description", "")
else:
for arg in args_meta:
2026-01-03 03:37:48 -08:00
row = args_table.add_row()
2025-12-11 12:47:30 -08:00
name = arg.get("name") or ""
row.add_column("Arg", f"-{name}" if name else "")
row.add_column("Type", arg.get("type", ""))
row.add_column("Req", "yes" if arg.get("required") else "")
desc = arg.get("description", "") or ""
choices = arg.get("choices", []) or []
if choices:
choice_text = f"choices: {', '.join(choices)}"
desc = f"{desc} ({choice_text})" if desc else choice_text
row.add_column("Description", desc)
2026-01-03 03:37:48 -08:00
example_table = ResultTable(f"{cmd_name} Examples")
example_table.set_preserve_order(True)
example_table.set_header_line("Select @N to insert the example command into the REPL.")
example_items: List[str] = []
if explicit_example:
for idx, example_cmd in enumerate(explicit_example):
example_text = str(example_cmd or "").strip()
row = example_table.add_row()
row.add_column("Example", example_text or "(empty example)")
example_items.append(example_text)
if example_text:
tokens = _parse_example_tokens(example_text)
if tokens:
example_table.set_row_selection_args(idx, tokens)
else:
example_table.set_no_choice(True)
row = example_table.add_row()
row.add_column("Example", "(no examples available)")
ctx.set_last_result_table(example_table, example_items)
ctx.set_current_stage_table(example_table)
setattr(example_table, "_rendered_by_cmdlet", True)
example_table.set_source_command(_HELP_EXAMPLE_SOURCE_COMMAND)
from SYS.rich_display import stdout_console
2025-12-20 23:57:44 -08:00
2026-01-03 03:37:48 -08:00
stdout_console().print()
stdout_console().print(args_table)
stdout_console().print()
stdout_console().print(example_table)
stdout_console().print()
2025-12-11 12:47:30 -08:00
def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
2026-01-02 02:28:59 -08:00
catalog: Any | None = None
cmdlet_names: List[str] = []
metadata: Dict[str, Dict[str, Any]] = {}
alias_map: Dict[str, str] = {}
2025-12-11 12:47:30 -08:00
try:
2025-12-16 23:23:43 -08:00
import cmdlet_catalog as _catalog
2025-12-11 12:47:30 -08:00
2026-01-02 02:28:59 -08:00
catalog = _catalog
2025-12-11 12:47:30 -08:00
except Exception:
2026-01-02 02:28:59 -08:00
catalog = None
2025-12-11 12:47:30 -08:00
2026-01-02 02:28:59 -08:00
if catalog is not None:
try:
cmdlet_names = catalog.list_cmdlet_names(config=config)
except Exception:
cmdlet_names = []
try:
metadata = catalog.list_cmdlet_metadata(config=config)
except Exception:
metadata = {}
if metadata:
alias_map = _build_alias_map_from_metadata(metadata)
else:
metadata, alias_map = _gather_metadata_from_cmdlet_classes()
if not metadata:
fallback_names = sorted(set(cmdlet_names or list(CMDLET_REGISTRY.keys())))
if fallback_names:
base_meta: Dict[str, Dict[str, Any]] = {}
for cmdname in fallback_names:
canonical = str(cmdname or "").replace("_", "-").lower()
entry: Dict[str, Any]
candidate: Dict[str, Any] | None = None
if catalog is not None:
try:
candidate = catalog.get_cmdlet_metadata(cmdname, config=config)
except Exception:
candidate = None
if candidate:
canonical = candidate.get("name", canonical)
entry = candidate
else:
entry = {
"name": canonical,
"aliases": [],
"usage": "",
"summary": "",
"details": [],
"args": [],
"raw": None,
}
base = base_meta.setdefault(
canonical,
{
"name": canonical,
"aliases": [],
"usage": "",
"summary": "",
"details": [],
"args": [],
"raw": entry.get("raw"),
},
)
if entry.get("aliases"):
base_aliases = set(base.get("aliases", []))
base_aliases.update([a for a in entry.get("aliases", []) if a])
base["aliases"] = sorted(base_aliases)
if not base.get("usage") and entry.get("usage"):
base["usage"] = entry["usage"]
if not base.get("summary") and entry.get("summary"):
base["summary"] = entry["summary"]
if not base.get("details") and entry.get("details"):
base["details"] = entry["details"]
if not base.get("args") and entry.get("args"):
base["args"] = entry["args"]
if not base.get("raw") and entry.get("raw"):
base["raw"] = entry["raw"]
metadata = base_meta
alias_map = _build_alias_map_from_metadata(metadata)
choice_candidates = list(alias_map.keys()) if alias_map else list(metadata.keys())
CMDLET.arg[0].choices = _normalize_choice_list(choice_candidates)
2025-12-11 12:47:30 -08:00
parsed = parse_cmdlet_args(args, CMDLET)
filter_text = parsed.get("filter")
cmd_arg = parsed.get("cmd")
if cmd_arg:
target_meta = _find_cmd_metadata(str(cmd_arg), metadata)
if not target_meta:
log(f"Unknown command: {cmd_arg}", file=sys.stderr)
return 1
_render_detail(target_meta, args)
return 0
_render_list(metadata, filter_text, args)
return 0
CMDLET = Cmdlet(
name=".help",
alias=["help",
"?"],
2025-12-12 21:55:38 -08:00
summary="Show cmdlet or detailed help",
2025-12-11 12:47:30 -08:00
usage=".help [cmd] [-filter text]",
arg=[
CmdletArg(
name="cmd",
type="string",
description="Cmdlet name to show detailed help",
required=False,
choices=[],
),
CmdletArg(
name="-filter",
type="string",
2025-12-12 21:55:38 -08:00
description="Filter cmdlet by substring",
2025-12-11 12:47:30 -08:00
required=False,
),
],
)
2025-12-11 23:21:45 -08:00
CMDLET.exec = _run