389 lines
14 KiB
Python
389 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, Sequence, List, Optional, Tuple
|
|
import shlex
|
|
import sys
|
|
|
|
from cmdlet._shared import Cmdlet, CmdletArg, parse_cmdlet_args
|
|
from cmdlet import REGISTRY as CMDLET_REGISTRY, ensure_cmdlet_modules_loaded
|
|
from SYS.logger import log
|
|
from SYS.result_table import ResultTable
|
|
from SYS import pipeline as ctx
|
|
|
|
|
|
def _normalize_choice_list(arg_names: Optional[List[str]]) -> List[str]:
|
|
return sorted(set(arg_names or []))
|
|
|
|
|
|
def _examples_for_cmd(name: str) -> List[str]:
|
|
"""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, [])
|
|
|
|
|
|
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
|
|
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 []],
|
|
"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]]:
|
|
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
|
|
|
|
|
|
def _render_list(
|
|
metadata: Dict[str,
|
|
Dict[str,
|
|
Any]],
|
|
filter_text: Optional[str],
|
|
args: Sequence[str]
|
|
) -> None:
|
|
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)
|
|
from SYS.rich_display import stdout_console
|
|
|
|
stdout_console().print(table)
|
|
|
|
|
|
def _render_detail(meta: Dict[str, Any], args: Sequence[str]) -> None:
|
|
title = f"Help: {meta.get('name', '') or 'cmd'}"
|
|
table = ResultTable(title)
|
|
table.set_source_command(".help", list(args))
|
|
|
|
header_lines: List[str] = []
|
|
summary = meta.get("summary", "")
|
|
usage = meta.get("usage", "")
|
|
aliases = meta.get("aliases", []) or []
|
|
examples = _examples_for_cmd(meta.get("name", ""))
|
|
first_example_tokens: List[str] = []
|
|
first_example_cmd: Optional[str] = None
|
|
if examples:
|
|
try:
|
|
split_tokens = shlex.split(examples[0])
|
|
if split_tokens:
|
|
first_example_cmd = split_tokens[0]
|
|
first_example_tokens = split_tokens[1:]
|
|
except Exception:
|
|
pass
|
|
|
|
if summary:
|
|
header_lines.append(summary)
|
|
if usage:
|
|
header_lines.append(f"Usage: {usage}")
|
|
if aliases:
|
|
header_lines.append("Aliases: " + ", ".join(aliases))
|
|
if examples:
|
|
header_lines.append("Examples: " + " | ".join(examples))
|
|
if header_lines:
|
|
table.set_header_lines(header_lines)
|
|
|
|
args_meta = meta.get("args", []) or []
|
|
example_text = " | ".join(examples)
|
|
# If we have an example, use it as the source command so @N runs that example
|
|
if first_example_cmd:
|
|
table.set_source_command(first_example_cmd, [])
|
|
if not args_meta:
|
|
row = table.add_row()
|
|
row.add_column("Arg", "(none)")
|
|
row.add_column("Type", "")
|
|
row.add_column("Req", "")
|
|
row.add_column("Description", "")
|
|
row.add_column("Example", example_text)
|
|
if first_example_tokens:
|
|
table.set_row_selection_args(len(table.rows) - 1, first_example_tokens)
|
|
else:
|
|
for arg in args_meta:
|
|
row = table.add_row()
|
|
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)
|
|
row.add_column("Example", example_text)
|
|
if first_example_tokens:
|
|
table.set_row_selection_args(len(table.rows) - 1, first_example_tokens)
|
|
|
|
ctx.set_last_result_table_overlay(table, [meta])
|
|
ctx.set_current_stage_table(table)
|
|
from SYS.rich_display import stdout_console
|
|
|
|
stdout_console().print(table)
|
|
|
|
|
|
def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
|
catalog: Any | None = None
|
|
cmdlet_names: List[str] = []
|
|
metadata: Dict[str, Dict[str, Any]] = {}
|
|
alias_map: Dict[str, str] = {}
|
|
|
|
try:
|
|
import cmdlet_catalog as _catalog
|
|
|
|
catalog = _catalog
|
|
except Exception:
|
|
catalog = None
|
|
|
|
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)
|
|
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",
|
|
"?"],
|
|
summary="Show cmdlet or detailed help",
|
|
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",
|
|
description="Filter cmdlet by substring",
|
|
required=False,
|
|
),
|
|
],
|
|
)
|
|
|
|
CMDLET.exec = _run
|