This commit is contained in:
nose
2025-12-20 23:57:44 -08:00
parent b75faa49a2
commit 8ca5783970
39 changed files with 4294 additions and 1722 deletions

345
CLI.py
View File

@@ -28,6 +28,26 @@ from prompt_toolkit.document import Document
from prompt_toolkit.lexers import Lexer
from prompt_toolkit.styles import Style
from rich_display import stderr_console, stdout_console
def _install_rich_traceback(*, show_locals: bool = False) -> None:
"""Install Rich traceback handler as the default excepthook.
This keeps uncaught exceptions readable in the terminal.
"""
try:
from rich.traceback import install as rich_traceback_install
rich_traceback_install(show_locals=bool(show_locals))
except Exception:
# Fall back to the standard Python traceback if Rich isn't available.
return
# Default to Rich tracebacks for the whole process.
_install_rich_traceback(show_locals=False)
from SYS.background_notifier import ensure_background_notifier
from SYS.logger import debug, set_debug
from SYS.worker_manager import WorkerManager
@@ -530,6 +550,32 @@ class CmdletCompleter(Completer):
self._config_loader = config_loader
self.cmdlet_names = CmdletIntrospection.cmdlet_names()
@staticmethod
def _used_arg_logicals(cmd_name: str, stage_tokens: List[str]) -> Set[str]:
"""Return logical argument names already used in this cmdlet stage.
Example: if the user has typed `download-media -url ...`, then `url`
is considered used and should not be suggested again (even as `--url`).
"""
arg_flags = CmdletIntrospection.cmdlet_args(cmd_name)
allowed = {a.lstrip("-").strip().lower() for a in arg_flags if a}
if not allowed:
return set()
used: Set[str] = set()
for tok in stage_tokens[1:]:
if not tok or not tok.startswith("-"):
continue
if tok in {"-", "--"}:
continue
# Handle common `-arg=value` form.
raw = tok.split("=", 1)[0]
logical = raw.lstrip("-").strip().lower()
if logical and logical in allowed:
used.add(logical)
return used
def get_completions(self, document: Document, complete_event): # type: ignore[override]
text = document.text_before_cursor
tokens = text.split()
@@ -600,6 +646,7 @@ class CmdletCompleter(Completer):
return
arg_names = CmdletIntrospection.cmdlet_args(cmd_name)
used_logicals = self._used_arg_logicals(cmd_name, stage_tokens)
logical_seen: Set[str] = set()
for arg in arg_names:
arg_low = arg.lower()
@@ -607,6 +654,8 @@ class CmdletCompleter(Completer):
if prefer_single_dash and arg_low.startswith("--"):
continue
logical = arg.lstrip("-").lower()
if logical in used_logicals:
continue
if prefer_single_dash and logical in logical_seen:
continue
if arg_low.startswith(current_token):
@@ -751,26 +800,32 @@ class CmdletHelp:
def show_cmdlet_list() -> None:
try:
metadata = list_cmdlet_metadata() or {}
print("\nAvailable cmdlet:")
from rich.box import SIMPLE
from rich.panel import Panel
from rich.table import Table as RichTable
table = RichTable(show_header=True, header_style="bold", box=SIMPLE, expand=True)
table.add_column("Cmdlet", no_wrap=True)
table.add_column("Aliases")
table.add_column("Args")
table.add_column("Summary")
for cmd_name in sorted(metadata.keys()):
info = metadata[cmd_name]
aliases = info.get("aliases", [])
args = info.get("args", [])
summary = info.get("summary") or ""
alias_str = ", ".join([str(a) for a in (aliases or []) if str(a).strip()])
arg_names = [a.get("name") for a in (args or []) if isinstance(a, dict) and a.get("name")]
args_str = ", ".join([str(a) for a in arg_names if str(a).strip()])
table.add_row(str(cmd_name), alias_str, args_str, str(summary))
display = f" cmd:{cmd_name}"
if aliases:
display += f" alias:{', '.join(aliases)}"
if args:
arg_names = [a.get("name") for a in args if a.get("name")]
if arg_names:
display += f" args:{', '.join(arg_names)}"
summary = info.get("summary")
if summary:
display += f" - {summary}"
print(display)
print()
stdout_console().print(Panel(table, title="Cmdlets", expand=False))
except Exception as exc:
print(f"Error: {exc}\n")
from rich.panel import Panel
from rich.text import Text
stderr_console().print(Panel(Text(f"Error: {exc}"), title="Error", expand=False))
@staticmethod
def show_cmdlet_help(cmd_name: str) -> None:
@@ -787,7 +842,10 @@ class CmdletHelp:
def _print_metadata(cmd_name: str, data: Any) -> None:
d = data.to_dict() if hasattr(data, "to_dict") else data
if not isinstance(d, dict):
print(f"Invalid metadata for {cmd_name}\n")
from rich.panel import Panel
from rich.text import Text
stderr_console().print(Panel(Text(f"Invalid metadata for {cmd_name}"), title="Error", expand=False))
return
name = d.get("name", cmd_name)
@@ -797,45 +855,48 @@ class CmdletHelp:
args = d.get("args", [])
details = d.get("details", [])
print("\nNAME")
print(f" {name}")
from rich.box import SIMPLE
from rich.console import Group
from rich.panel import Panel
from rich.table import Table as RichTable
from rich.text import Text
print("\nSYNOPSIS")
print(f" {usage or name}")
header = Text.assemble((str(name), "bold"))
synopsis = Text(str(usage or name))
stdout_console().print(Panel(Group(header, synopsis), title="Help", expand=False))
if summary or description:
print("\nDESCRIPTION")
desc_bits: List[Text] = []
if summary:
print(f" {summary}")
desc_bits.append(Text(str(summary)))
if description:
print(f" {description}")
desc_bits.append(Text(str(description)))
stdout_console().print(Panel(Group(*desc_bits), title="Description", expand=False))
if args and isinstance(args, list):
print("\nPARAMETERS")
param_table = RichTable(show_header=True, header_style="bold", box=SIMPLE, expand=True)
param_table.add_column("Arg", no_wrap=True)
param_table.add_column("Type", no_wrap=True)
param_table.add_column("Required", no_wrap=True)
param_table.add_column("Description")
for arg in args:
if isinstance(arg, dict):
name_str = arg.get("name", "?")
typ = arg.get("type", "string")
required = arg.get("required", False)
required = bool(arg.get("required", False))
desc = arg.get("description", "")
else:
name_str = getattr(arg, "name", "?")
typ = getattr(arg, "type", "string")
required = getattr(arg, "required", False)
required = bool(getattr(arg, "required", False))
desc = getattr(arg, "description", "")
req_marker = "[required]" if required else "[optional]"
print(f" -{name_str} <{typ}>")
if desc:
print(f" {desc}")
print(f" {req_marker}")
print()
param_table.add_row(f"-{name_str}", str(typ), "yes" if required else "no", str(desc or ""))
stdout_console().print(Panel(param_table, title="Parameters", expand=False))
if details:
print("REMARKS")
for detail in details:
print(f" {detail}")
print()
stdout_console().print(Panel(Group(*[Text(str(x)) for x in details]), title="Remarks", expand=False))
class CmdletExecutor:
@@ -1044,6 +1105,26 @@ class CmdletExecutor:
ctx.set_last_selection(selected_indices)
try:
try:
if hasattr(ctx, "set_current_cmdlet_name"):
ctx.set_current_cmdlet_name(cmd_name)
except Exception:
pass
try:
if hasattr(ctx, "set_current_stage_text"):
raw_stage = ""
try:
raw_stage = ctx.get_current_command_text("") if hasattr(ctx, "get_current_command_text") else ""
except Exception:
raw_stage = ""
if raw_stage:
ctx.set_current_stage_text(raw_stage)
else:
ctx.set_current_stage_text(" ".join([cmd_name, *filtered_args]).strip() or cmd_name)
except Exception:
pass
ret_code = cmd_fn(result, filtered_args, config)
if getattr(pipeline_ctx, "emits", None):
@@ -1113,8 +1194,8 @@ class CmdletExecutor:
else:
ctx.set_last_result_items_only(emits)
print()
print(table.format_plain())
stdout_console().print()
stdout_console().print(table)
if ret_code != 0:
stage_status = "failed"
@@ -1125,6 +1206,16 @@ class CmdletExecutor:
stage_error = f"{type(exc).__name__}: {exc}"
print(f"[error] {type(exc).__name__}: {exc}\n")
finally:
try:
if hasattr(ctx, "clear_current_cmdlet_name"):
ctx.clear_current_cmdlet_name()
except Exception:
pass
try:
if hasattr(ctx, "clear_current_stage_text"):
ctx.clear_current_stage_text()
except Exception:
pass
ctx.clear_last_selection()
if stage_session:
stage_session.close(status=stage_status, error_msg=stage_error)
@@ -1322,6 +1413,13 @@ class PipelineExecutor:
pipeline_text = " | ".join(" ".join(stage) for stage in stages)
pipeline_session = WorkerStages.begin_pipeline(worker_manager, pipeline_text=pipeline_text, config=config)
raw_stage_texts: List[str] = []
try:
if hasattr(ctx, "get_current_command_stages"):
raw_stage_texts = ctx.get_current_command_stages() or []
except Exception:
raw_stage_texts = []
if pipeline_session and worker_manager and isinstance(config, dict):
session_worker_ids = config.get("_session_worker_ids")
if session_worker_ids:
@@ -1452,6 +1550,9 @@ class PipelineExecutor:
if table_type == "youtube":
print("Auto-running YouTube selection via download-media")
stages.append(["download-media"])
elif table_type == "bandcamp":
print("Auto-running Bandcamp selection via download-media")
stages.append(["download-media"])
elif table_type in {"soulseek", "openlibrary", "libgen"}:
print("Auto-piping selection to download-file")
stages.append(["download-file"])
@@ -1473,6 +1574,14 @@ class PipelineExecutor:
):
print("Auto-inserting download-media after YouTube selection")
stages.insert(0, ["download-media"])
if table_type == "bandcamp" and first_cmd not in (
"download-media",
"download_media",
"download-file",
".pipe",
):
print("Auto-inserting download-media after Bandcamp selection")
stages.insert(0, ["download-media"])
if table_type == "libgen" and first_cmd not in (
"download-file",
"download-media",
@@ -1645,6 +1754,32 @@ class PipelineExecutor:
except Exception:
pass
try:
if hasattr(ctx, "set_current_cmdlet_name"):
ctx.set_current_cmdlet_name(cmd_name)
except Exception:
pass
try:
if hasattr(ctx, "set_current_stage_text"):
stage_text = ""
if raw_stage_texts and stage_index < len(raw_stage_texts):
candidate = str(raw_stage_texts[stage_index] or "").strip()
if candidate:
try:
cand_tokens = shlex.split(candidate)
except Exception:
cand_tokens = candidate.split()
if cand_tokens:
first = str(cand_tokens[0]).replace("_", "-").lower()
if first == cmd_name:
stage_text = candidate
if not stage_text:
stage_text = " ".join(stage_tokens).strip()
ctx.set_current_stage_text(stage_text)
except Exception:
pass
ret_code = cmd_fn(piped_result, list(stage_args), config)
stage_is_last = stage_index + 1 >= len(stages)
@@ -1676,7 +1811,6 @@ class PipelineExecutor:
and (not emits)
and cmd_name in {"download-media", "download_media"}
and stage_table is not None
and hasattr(stage_table, "format_plain")
and stage_table_type in {"ytdlp.formatlist", "download-media", "download_media"}
):
try:
@@ -1691,8 +1825,8 @@ class PipelineExecutor:
already_rendered = False
if not already_rendered:
print()
print(stage_table.format_plain())
stdout_console().print()
stdout_console().print(stage_table)
try:
remaining = stages[stage_index + 1 :]
@@ -1719,15 +1853,15 @@ class PipelineExecutor:
if final_table is None:
final_table = stage_table
if final_table is not None and hasattr(final_table, "format_plain"):
if final_table is not None:
try:
already_rendered = bool(getattr(final_table, "_rendered_by_cmdlet", False))
except Exception:
already_rendered = False
if not already_rendered:
print()
print(final_table.format_plain())
stdout_console().print()
stdout_console().print(final_table)
# Fallback: if a cmdlet emitted results but did not provide a table,
# render a standard ResultTable so last-stage pipelines still show output.
@@ -1739,8 +1873,8 @@ class PipelineExecutor:
table = ResultTable(table_title)
for item in emits:
table.add_result(item)
print()
print(table.format_plain())
stdout_console().print()
stdout_console().print(table)
if isinstance(ret_code, int) and ret_code != 0:
stage_status = "failed"
@@ -1757,6 +1891,16 @@ class PipelineExecutor:
pipeline_error = f"{stage_label} error: {exc}"
return
finally:
try:
if hasattr(ctx, "clear_current_cmdlet_name"):
ctx.clear_current_cmdlet_name()
except Exception:
pass
try:
if hasattr(ctx, "clear_current_stage_text"):
ctx.clear_current_stage_text()
except Exception:
pass
if stage_session:
stage_session.close(status=stage_status, error_msg=stage_error)
elif pipeline_session and worker_manager:
@@ -1774,8 +1918,8 @@ class PipelineExecutor:
for item in items:
table.add_result(item)
ctx.set_last_result_items_only(items)
print()
print(table.format_plain())
stdout_console().print()
stdout_console().print(table)
except Exception as exc:
pipeline_status = "failed"
pipeline_error = str(exc)
@@ -1786,7 +1930,20 @@ class PipelineExecutor:
except Exception as exc:
print(f"[error] Failed to execute pipeline: {exc}\n")
Welcome = """
# MEDIOS-MACINA
Rich can do a pretty *decent* job of rendering markdown.
1. This is a list item
2. This is another list item
"""
from rich.markdown import Markdown
from rich.console import Console
console = Console()
md = Markdown(Welcome)
console.print(md)
class MedeiaCLI:
"""Main CLI application object."""
@@ -1892,25 +2049,20 @@ class MedeiaCLI:
return app
def run(self) -> None:
# Ensure Rich tracebacks are active even when invoking subcommands.
try:
config = self._config_loader.load()
debug_enabled = bool(config.get("debug", False)) if isinstance(config, dict) else False
except Exception:
debug_enabled = False
set_debug(debug_enabled)
_install_rich_traceback(show_locals=debug_enabled)
self.build_app()()
def run_repl(self) -> None:
banner = r"""
Medeia-Macina
=====================
|123456789|ABCDEFGHI|
|246813579|JKLMNOPQR|
|369369369|STUVWXYZ0|
|483726159|ABCDEFGHI|
|=========+=========|
|516273849|JKLMNOPQR|
|639639639|STUVWXYZ0|
|753186429|ABCDEFGHI|
|876543219|JKLMNOPQR|
|999999999|STUVWXYZ0|
=====================
"""
print(banner)
# (Startup banner is optional; keep the REPL quiet by default.)
prompt_text = "🜂🜄🜁🜃|"
@@ -1918,6 +2070,11 @@ class MedeiaCLI:
"*********<IGNITIO>*********<NOUSEMPEH>*********<RUGRAPOG>*********<OMEGHAU>*********"
)
startup_table.set_no_choice(True).set_preserve_order(True)
startup_table.set_value_case("upper")
def _upper(value: Any) -> str:
text = "" if value is None else str(value)
return text.upper()
def _add_startup_check(
status: str,
@@ -1929,12 +2086,12 @@ class MedeiaCLI:
detail: str = "",
) -> None:
row = startup_table.add_row()
row.add_column("Status", status)
row.add_column("Name", name)
row.add_column("Provider", provider or "")
row.add_column("Store", store or "")
row.add_column("Files", "" if files is None else str(files))
row.add_column("Detail", detail or "")
row.add_column("STATUS", _upper(status))
row.add_column("NAME", _upper(name))
row.add_column("PROVIDER", _upper(provider or ""))
row.add_column("STORE", _upper(store or ""))
row.add_column("FILES", "" if files is None else str(files))
row.add_column("DETAIL", _upper(detail or ""))
def _has_store_subtype(cfg: dict, subtype: str) -> bool:
store_cfg = cfg.get("store")
@@ -1967,8 +2124,8 @@ class MedeiaCLI:
config = self._config_loader.load()
debug_enabled = bool(config.get("debug", False))
set_debug(debug_enabled)
if debug_enabled:
debug("✓ Debug logging enabled")
_install_rich_traceback(show_locals=debug_enabled)
_add_startup_check("ENABLED" if debug_enabled else "DISABLED", "DEBUGGING")
try:
try:
@@ -2226,8 +2383,8 @@ class MedeiaCLI:
_add_startup_check("ERROR", "Cookies", detail=str(exc))
if startup_table.rows:
print()
print(startup_table.format_plain())
stdout_console().print()
stdout_console().print(startup_table)
except Exception as exc:
if debug_enabled:
debug(f"⚠ Could not check service availability: {exc}")
@@ -2349,9 +2506,9 @@ class MedeiaCLI:
if last_table is None:
last_table = ctx.get_last_result_table()
if last_table:
print()
stdout_console().print()
ctx.set_current_stage_table(last_table)
print(last_table.format_plain())
stdout_console().print(last_table)
else:
items = ctx.get_last_result_items()
if items:
@@ -2370,10 +2527,44 @@ class MedeiaCLI:
last_table = ctx.get_display_table() if hasattr(ctx, "get_display_table") else None
if last_table is None:
last_table = ctx.get_last_result_table()
# Auto-refresh search-store tables when navigating back,
# so row payloads (titles/tags) reflect latest store state.
try:
src_cmd = getattr(last_table, "source_command", None) if last_table else None
if isinstance(src_cmd, str) and src_cmd.lower().replace("_", "-") == "search-store":
src_args = getattr(last_table, "source_args", None) if last_table else None
base_args = list(src_args) if isinstance(src_args, list) else []
cleaned_args = [
str(a)
for a in base_args
if str(a).strip().lower() not in {"--refresh", "-refresh"}
]
if hasattr(ctx, "set_current_command_text"):
try:
title_text = getattr(last_table, "title", None) if last_table else None
if isinstance(title_text, str) and title_text.strip():
ctx.set_current_command_text(title_text.strip())
else:
ctx.set_current_command_text(" ".join(["search-store", *cleaned_args]).strip())
except Exception:
pass
try:
self._cmdlet_executor.execute("search-store", cleaned_args + ["--refresh"])
finally:
if hasattr(ctx, "clear_current_command_text"):
try:
ctx.clear_current_command_text()
except Exception:
pass
continue
except Exception as exc:
print(f"Error refreshing search-store table: {exc}", file=sys.stderr)
if last_table:
print()
stdout_console().print()
ctx.set_current_stage_table(last_table)
print(last_table.format_plain())
stdout_console().print(last_table)
else:
items = ctx.get_last_result_items()
if items: