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 [])) _HELP_EXAMPLE_SOURCE_COMMAND = ".help-example" def _example_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 _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] 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 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) 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 []], "examples": example_entries, "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) setattr(table, "_rendered_by_cmdlet", True) from SYS.rich_display import stdout_console stdout_console().print(table) def _render_detail(meta: Dict[str, Any], _args: Sequence[str]) -> None: cmd_name = str(meta.get("name", "") or "cmd") title = f"Help: {cmd_name}" summary = meta.get("summary", "") usage = meta.get("usage", "") aliases = meta.get("aliases", []) or [] 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) header_lines: List[str] = [] if summary: header_lines.append(summary) if usage: header_lines.append(f"Usage: {usage}") if aliases: header_lines.append("Aliases: " + ", ".join(aliases)) if details: header_lines.extend(str(line) for line in details if str(line).strip()) if explicit_example: header_lines.append("Examples available below") args_meta = meta.get("args", []) or [] 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) if not args_meta: row = args_table.add_row() row.add_column("Arg", "(none)") row.add_column("Type", "") row.add_column("Req", "") row.add_column("Description", "") else: for arg in args_meta: row = args_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) 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 stdout_console().print() stdout_console().print(args_table) stdout_console().print() stdout_console().print(example_table) stdout_console().print() 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