dsf
This commit is contained in:
329
CLI.py
329
CLI.py
@@ -1024,6 +1024,94 @@ class CmdletExecutor:
|
||||
|
||||
config = self._config_loader.load()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Single-command Live pipeline progress (match REPL behavior)
|
||||
# ------------------------------------------------------------------
|
||||
progress_ui = None
|
||||
pipe_idx: Optional[int] = None
|
||||
|
||||
def _maybe_start_single_live_progress(
|
||||
*,
|
||||
cmd_name_norm: str,
|
||||
filtered_args: List[str],
|
||||
piped_input: Any,
|
||||
config: Any,
|
||||
) -> None:
|
||||
nonlocal progress_ui, pipe_idx
|
||||
|
||||
# Keep behavior consistent with pipeline runner exclusions.
|
||||
if cmd_name_norm in {"get-relationship", "get-rel", ".pipe", ".matrix"}:
|
||||
return
|
||||
|
||||
try:
|
||||
quiet_mode = bool(config.get("_quiet_background_output")) if isinstance(config, dict) else False
|
||||
except Exception:
|
||||
quiet_mode = False
|
||||
if quiet_mode:
|
||||
return
|
||||
|
||||
try:
|
||||
import sys as _sys
|
||||
|
||||
if not bool(getattr(_sys.stderr, "isatty", lambda: False)()):
|
||||
return
|
||||
except Exception:
|
||||
return
|
||||
|
||||
try:
|
||||
from models import PipelineLiveProgress
|
||||
|
||||
progress_ui = PipelineLiveProgress([cmd_name_norm], enabled=True)
|
||||
progress_ui.start()
|
||||
try:
|
||||
if hasattr(ctx, "set_live_progress"):
|
||||
ctx.set_live_progress(progress_ui)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
pipe_idx = 0
|
||||
|
||||
# Estimate per-item task count for the single pipe.
|
||||
total_items = 1
|
||||
preview_items: Optional[List[Any]] = None
|
||||
try:
|
||||
if isinstance(piped_input, list):
|
||||
total_items = max(1, int(len(piped_input)))
|
||||
preview_items = list(piped_input)
|
||||
elif piped_input is not None:
|
||||
total_items = 1
|
||||
preview_items = [piped_input]
|
||||
else:
|
||||
preview: List[Any] = []
|
||||
toks = list(filtered_args or [])
|
||||
i = 0
|
||||
while i < len(toks):
|
||||
t = str(toks[i])
|
||||
low = t.lower().strip()
|
||||
if low in {"-url", "--url"} and i + 1 < len(toks):
|
||||
nxt = str(toks[i + 1])
|
||||
if nxt and not nxt.startswith("-"):
|
||||
preview.append(nxt)
|
||||
i += 2
|
||||
continue
|
||||
if (not t.startswith("-")) and ("://" in low or low.startswith(("magnet:", "torrent:"))):
|
||||
preview.append(t)
|
||||
i += 1
|
||||
preview_items = preview if preview else None
|
||||
total_items = max(1, int(len(preview)) if preview else 1)
|
||||
except Exception:
|
||||
total_items = 1
|
||||
preview_items = None
|
||||
|
||||
try:
|
||||
progress_ui.begin_pipe(0, total_items=int(total_items), items_preview=preview_items)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
progress_ui = None
|
||||
pipe_idx = None
|
||||
|
||||
|
||||
filtered_args: List[str] = []
|
||||
selected_indices: List[int] = []
|
||||
select_all = False
|
||||
@@ -1099,7 +1187,35 @@ class CmdletExecutor:
|
||||
)
|
||||
|
||||
stage_worker_id = stage_session.worker_id if stage_session else None
|
||||
pipeline_ctx = ctx.PipelineStageContext(stage_index=0, total_stages=1, pipe_index=0, worker_id=stage_worker_id)
|
||||
|
||||
# Start live progress after we know the effective cmd + args + piped input.
|
||||
cmd_norm = str(cmd_name or "").replace("_", "-").strip().lower()
|
||||
_maybe_start_single_live_progress(
|
||||
cmd_name_norm=cmd_norm or str(cmd_name or "").strip().lower(),
|
||||
filtered_args=filtered_args,
|
||||
piped_input=result,
|
||||
config=config,
|
||||
)
|
||||
|
||||
on_emit = None
|
||||
if progress_ui is not None and pipe_idx is not None:
|
||||
_ui = progress_ui
|
||||
|
||||
def _on_emit(obj: Any, _progress=_ui) -> None:
|
||||
try:
|
||||
_progress.on_emit(0, obj)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
on_emit = _on_emit
|
||||
|
||||
pipeline_ctx = ctx.PipelineStageContext(
|
||||
stage_index=0,
|
||||
total_stages=1,
|
||||
pipe_index=pipe_idx if pipe_idx is not None else 0,
|
||||
worker_id=stage_worker_id,
|
||||
on_emit=on_emit,
|
||||
)
|
||||
ctx.set_stage_context(pipeline_ctx)
|
||||
stage_status = "completed"
|
||||
stage_error = ""
|
||||
@@ -1131,6 +1247,19 @@ class CmdletExecutor:
|
||||
if getattr(pipeline_ctx, "emits", None):
|
||||
emits = list(pipeline_ctx.emits)
|
||||
|
||||
# Shared `-path` behavior: if the cmdlet emitted temp/PATH file artifacts,
|
||||
# move them to the user-specified destination and update emitted paths.
|
||||
try:
|
||||
from cmdlet import _shared as sh
|
||||
|
||||
emits = sh.apply_output_path_from_pipeobjects(cmd_name=cmd_name, args=filtered_args, emits=emits)
|
||||
try:
|
||||
pipeline_ctx.emits = list(emits)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Detect format-selection emits and skip printing (user selects with @N).
|
||||
is_format_selection = False
|
||||
if emits:
|
||||
@@ -1195,9 +1324,64 @@ class CmdletExecutor:
|
||||
else:
|
||||
ctx.set_last_result_items_only(emits)
|
||||
|
||||
# Stop Live progress before printing tables.
|
||||
if progress_ui is not None:
|
||||
try:
|
||||
if pipe_idx is not None:
|
||||
progress_ui.finish_pipe(int(pipe_idx), force_complete=(stage_status == "completed"))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
progress_ui.stop()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if hasattr(ctx, "set_live_progress"):
|
||||
ctx.set_live_progress(None)
|
||||
except Exception:
|
||||
pass
|
||||
progress_ui = None
|
||||
pipe_idx = None
|
||||
|
||||
stdout_console().print()
|
||||
stdout_console().print(table)
|
||||
|
||||
# If the cmdlet produced a current-stage table without emits (e.g. format selection),
|
||||
# render it here for parity with REPL pipeline runner.
|
||||
if (not getattr(pipeline_ctx, "emits", None)) and hasattr(ctx, "get_current_stage_table"):
|
||||
try:
|
||||
stage_table = ctx.get_current_stage_table()
|
||||
except Exception:
|
||||
stage_table = None
|
||||
if stage_table is not None:
|
||||
try:
|
||||
already_rendered = bool(getattr(stage_table, "_rendered_by_cmdlet", False))
|
||||
except Exception:
|
||||
already_rendered = False
|
||||
|
||||
if already_rendered:
|
||||
return
|
||||
|
||||
if progress_ui is not None:
|
||||
try:
|
||||
if pipe_idx is not None:
|
||||
progress_ui.finish_pipe(int(pipe_idx), force_complete=(stage_status == "completed"))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
progress_ui.stop()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if hasattr(ctx, "set_live_progress"):
|
||||
ctx.set_live_progress(None)
|
||||
except Exception:
|
||||
pass
|
||||
progress_ui = None
|
||||
pipe_idx = None
|
||||
stdout_console().print()
|
||||
stdout_console().print(stage_table)
|
||||
|
||||
if ret_code != 0:
|
||||
stage_status = "failed"
|
||||
stage_error = f"exit code {ret_code}"
|
||||
@@ -1207,6 +1391,21 @@ class CmdletExecutor:
|
||||
stage_error = f"{type(exc).__name__}: {exc}"
|
||||
print(f"[error] {type(exc).__name__}: {exc}\n")
|
||||
finally:
|
||||
if progress_ui is not None:
|
||||
try:
|
||||
if pipe_idx is not None:
|
||||
progress_ui.finish_pipe(int(pipe_idx), force_complete=(stage_status == "completed"))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
progress_ui.stop()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if hasattr(ctx, "set_live_progress"):
|
||||
ctx.set_live_progress(None)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if hasattr(ctx, "clear_current_cmdlet_name"):
|
||||
ctx.clear_current_cmdlet_name()
|
||||
@@ -1245,6 +1444,50 @@ class PipelineExecutor:
|
||||
stages.append(current)
|
||||
return stages
|
||||
|
||||
@staticmethod
|
||||
def _validate_download_media_relationship_order(stages: List[List[str]]) -> bool:
|
||||
"""Guard against running add-relationship on unstored download-media results.
|
||||
|
||||
Intended UX:
|
||||
download-media ... | add-file -store <store> | add-relationship
|
||||
|
||||
Rationale:
|
||||
download-media outputs items that may not yet have a stable store+hash.
|
||||
add-relationship is designed to operate in store/hash mode.
|
||||
"""
|
||||
|
||||
def _norm(name: str) -> str:
|
||||
return str(name or "").replace("_", "-").strip().lower()
|
||||
|
||||
names: List[str] = []
|
||||
for stage in stages or []:
|
||||
if not stage:
|
||||
continue
|
||||
names.append(_norm(stage[0]))
|
||||
|
||||
dl_idxs = [i for i, n in enumerate(names) if n == "download-media"]
|
||||
rel_idxs = [i for i, n in enumerate(names) if n == "add-relationship"]
|
||||
add_file_idxs = [i for i, n in enumerate(names) if n == "add-file"]
|
||||
|
||||
if not dl_idxs or not rel_idxs:
|
||||
return True
|
||||
|
||||
# If download-media is upstream of add-relationship, require an add-file in between.
|
||||
for rel_i in rel_idxs:
|
||||
dl_before = [d for d in dl_idxs if d < rel_i]
|
||||
if not dl_before:
|
||||
continue
|
||||
dl_i = max(dl_before)
|
||||
if not any(dl_i < a < rel_i for a in add_file_idxs):
|
||||
print(
|
||||
"Pipeline order error: when using download-media with add-relationship, "
|
||||
"add-relationship must come after add-file (so items are stored and have store+hash).\n"
|
||||
"Example: download-media <...> | add-file -store <store> | add-relationship\n"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _try_clear_pipeline_stop(ctx: Any) -> None:
|
||||
try:
|
||||
@@ -1714,6 +1957,11 @@ class PipelineExecutor:
|
||||
name = str(stage_tokens[0]).replace("_", "-").lower()
|
||||
if name == "@" or name.startswith("@"):
|
||||
continue
|
||||
# Display-only: avoid Live progress for relationship viewing.
|
||||
# This keeps `@1 | get-relationship` clean and prevents progress UI
|
||||
# from interfering with Rich tables/panels.
|
||||
if name in {"get-relationship", "get-rel"}:
|
||||
continue
|
||||
# `.pipe` (MPV) is an interactive launcher; disable pipeline Live progress
|
||||
# for it because it doesn't meaningfully "complete" (mpv may keep running)
|
||||
# and Live output interferes with MPV playlist UI.
|
||||
@@ -1792,6 +2040,12 @@ class PipelineExecutor:
|
||||
if initial_piped is not None:
|
||||
piped_result = initial_piped
|
||||
|
||||
# REPL guard: prevent add-relationship before add-file for download-media pipelines.
|
||||
if not self._validate_download_media_relationship_order(stages):
|
||||
pipeline_status = "failed"
|
||||
pipeline_error = "Invalid pipeline order"
|
||||
return
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Multi-level pipeline progress (pipes = stages, tasks = items)
|
||||
# ------------------------------------------------------------------
|
||||
@@ -2069,6 +2323,23 @@ class PipelineExecutor:
|
||||
emits: List[Any] = []
|
||||
if getattr(pipeline_ctx, "emits", None) is not None:
|
||||
emits = list(pipeline_ctx.emits or [])
|
||||
|
||||
# Shared `-path` behavior: persist temp/PATH artifacts to destination.
|
||||
if emits:
|
||||
try:
|
||||
from cmdlet import _shared as sh
|
||||
|
||||
emits = sh.apply_output_path_from_pipeobjects(
|
||||
cmd_name=cmd_name,
|
||||
args=list(stage_args),
|
||||
emits=emits,
|
||||
)
|
||||
try:
|
||||
pipeline_ctx.emits = list(emits)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
if emits:
|
||||
# If the cmdlet already installed an overlay table (e.g. get-tag),
|
||||
# don't overwrite it: set_last_result_items_only() would clear the
|
||||
@@ -2286,25 +2557,12 @@ class PipelineExecutor:
|
||||
except Exception as exc:
|
||||
print(f"[error] Failed to execute pipeline: {exc}\n")
|
||||
|
||||
Welcome = """
|
||||
# MEDIOS-MACINA
|
||||
|
||||
[red]Romans 1:22[/red] Professing themselves to be wise, they became fools,
|
||||
|
||||
|
||||
dfd
|
||||
==
|
||||
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."""
|
||||
|
||||
@@ -2422,6 +2680,32 @@ class MedeiaCLI:
|
||||
|
||||
_ = (search_provider, pipeline, repl, main_callback)
|
||||
|
||||
# Dynamically register all cmdlets as top-level Typer commands so users can
|
||||
# invoke `mm <cmdlet> [args]` directly from the shell. We use Click/Typer
|
||||
# context settings to allow arbitrary flags and options to pass through to
|
||||
# the cmdlet system without Typer trying to parse them.
|
||||
try:
|
||||
names = list_cmdlet_names()
|
||||
skip = {"search-provider", "pipeline", "repl"}
|
||||
for nm in names:
|
||||
if not nm or nm in skip:
|
||||
continue
|
||||
# create a scoped handler to capture the command name
|
||||
def _make_handler(cmd_name: str):
|
||||
@app.command(cmd_name, context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
|
||||
def _handler(ctx: typer.Context):
|
||||
try:
|
||||
args = list(ctx.args or [])
|
||||
except Exception:
|
||||
args = []
|
||||
self._cmdlet_executor.execute(cmd_name, args)
|
||||
return _handler
|
||||
|
||||
_make_handler(nm)
|
||||
except Exception:
|
||||
# Don't let failure to register dynamic commands break startup
|
||||
pass
|
||||
|
||||
return app
|
||||
|
||||
def run(self) -> None:
|
||||
@@ -2438,8 +2722,21 @@ class MedeiaCLI:
|
||||
self.build_app()()
|
||||
|
||||
def run_repl(self) -> None:
|
||||
# (Startup banner is optional; keep the REPL quiet by default.)
|
||||
Welcome = """
|
||||
# MEDIOS-MACINA
|
||||
|
||||
[red]Romans 1:22[/red] Professing themselves to be wise, they became fools,
|
||||
|
||||
|
||||
dfd
|
||||
==
|
||||
Rich can do a pretty *decent* job of rendering markdown.
|
||||
|
||||
1. This is a list item
|
||||
2. This is another list item
|
||||
"""
|
||||
md = Markdown(Welcome)
|
||||
console.print(md)
|
||||
prompt_text = "<🜂🜄|🜁🜃>"
|
||||
|
||||
startup_table = ResultTable(
|
||||
|
||||
Reference in New Issue
Block a user