This commit is contained in:
nose
2025-12-07 00:21:30 -08:00
parent f29709d951
commit 6b05dc5552
23 changed files with 2196 additions and 1133 deletions

143
CLI.py
View File

@@ -231,9 +231,25 @@ def _get_table_title_for_command(cmd_name: str, emitted_items: Optional[List[Any
'delete_file': 'Results',
'check-file-status': 'Status',
'check_file_status': 'Status',
'get-metadata': None,
'get_metadata': None,
}
return title_map.get(cmd_name, 'Results')
mapped = title_map.get(cmd_name, 'Results')
if mapped is not None:
return mapped
# For metadata, derive title from first item if available
if emitted_items:
first = emitted_items[0]
try:
if isinstance(first, dict) and first.get('title'):
return str(first.get('title'))
if hasattr(first, 'title') and getattr(first, 'title'):
return str(getattr(first, 'title'))
except Exception:
pass
return 'Results'
def _close_cli_worker_manager() -> None:
@@ -409,9 +425,20 @@ def _get_cmdlet_names() -> List[str]:
def _import_cmd_module(mod_name: str):
"""Import a cmdlet/native module from cmdlets or cmdnats packages."""
for package in ("cmdlets", "cmdnats", None):
# Normalize leading punctuation used in aliases (e.g., .pipe)
normalized = (mod_name or "").strip()
if normalized.startswith('.'):
normalized = normalized.lstrip('.')
# Convert hyphens to underscores to match module filenames
normalized = normalized.replace("-", "_")
if not normalized:
return None
# Prefer native cmdnats modules first so editable installs of this package
# don't shadow the in-repo implementations (e.g., .pipe autocomplete flags).
for package in ("cmdnats", "cmdlets", None):
try:
qualified = f"{package}.{mod_name}" if package else mod_name
qualified = f"{package}.{normalized}" if package else normalized
return import_module(qualified)
except ModuleNotFoundError:
continue
@@ -495,6 +522,15 @@ def _get_arg_choices(cmd_name: str, arg_name: str) -> List[str]:
merged = sorted(set(provider_choices + meta_choices))
if merged:
return merged
if normalized_arg == "scrape":
try:
from helper.metadata_search import list_metadata_providers
meta_providers = list_metadata_providers(_load_cli_config())
if meta_providers:
return sorted(meta_providers.keys())
except Exception:
pass
mod = _import_cmd_module(mod_name)
data = getattr(mod, "CMDLET", None) if mod else None
if data:
@@ -536,36 +572,48 @@ if (
text = document.text_before_cursor
tokens = text.split()
if not tokens:
# Respect pipeline stages: only use tokens after the last '|'
last_pipe = -1
for idx, tok in enumerate(tokens):
if tok == "|":
last_pipe = idx
stage_tokens = tokens[last_pipe + 1:] if last_pipe >= 0 else tokens
if not stage_tokens:
for cmd in self.cmdlet_names:
yield CompletionType(cmd, start_position=0)
elif len(tokens) == 1:
current = tokens[0].lower()
return
# Single token at this stage -> suggest command names/keywords
if len(stage_tokens) == 1:
current = stage_tokens[0].lower()
for cmd in self.cmdlet_names:
if cmd.startswith(current):
yield CompletionType(cmd, start_position=-len(current))
for keyword in ["help", "exit", "quit"]:
if keyword.startswith(current):
yield CompletionType(keyword, start_position=-len(current))
else:
cmd_name = tokens[0].replace("_", "-").lower()
current_token = tokens[-1].lower()
prev_token = tokens[-2].lower() if len(tokens) > 1 else ""
return
choices = _get_arg_choices(cmd_name, prev_token)
if choices:
for choice in choices:
if choice.lower().startswith(current_token):
yield CompletionType(choice, start_position=-len(current_token))
return
# Otherwise treat first token of stage as command and complete its args
cmd_name = stage_tokens[0].replace("_", "-").lower()
current_token = stage_tokens[-1].lower()
prev_token = stage_tokens[-2].lower() if len(stage_tokens) > 1 else ""
arg_names = _get_cmdlet_args(cmd_name)
for arg in arg_names:
if arg.lower().startswith(current_token):
yield CompletionType(arg, start_position=-len(current_token))
choices = _get_arg_choices(cmd_name, prev_token)
if choices:
for choice in choices:
if choice.lower().startswith(current_token):
yield CompletionType(choice, start_position=-len(current_token))
return
if "--help".startswith(current_token):
yield CompletionType("--help", start_position=-len(current_token))
arg_names = _get_cmdlet_args(cmd_name)
for arg in arg_names:
if arg.lower().startswith(current_token):
yield CompletionType(arg, start_position=-len(current_token))
if "--help".startswith(current_token):
yield CompletionType("--help", start_position=-len(current_token))
async def get_completions_async(self, document: Document, complete_event): # type: ignore[override]
for completion in self.get_completions(document, complete_event):
@@ -689,6 +737,7 @@ def _create_cmdlet_cli():
|246813579|JKLMNOPQR|
|369369369|STUVWXYZ0|
|483726159|ABCDEFGHI|
|=========+=========|
|516273849|JKLMNOPQR|
|639639639|STUVWXYZ0|
|753186429|ABCDEFGHI|
@@ -699,7 +748,7 @@ def _create_cmdlet_cli():
print(banner)
# Configurable prompt
prompt_text = ">>>|"
prompt_text = "🜂🜄🜁🜃|"
# Pre-acquire Hydrus session key at startup (like hub-ui does)
try:
@@ -840,7 +889,6 @@ def _create_cmdlet_cli():
return input(prompt)
while True:
print("#-------------------------------------------------------------------------#")
try:
user_input = get_input(prompt_text).strip()
except (EOFError, KeyboardInterrupt):
@@ -971,6 +1019,19 @@ def _execute_pipeline(tokens: list):
if not stages:
print("Invalid pipeline syntax\n")
return
# If a previous stage paused for selection, attach its remaining stages when the user runs only @N
pending_tail = ctx.get_pending_pipeline_tail() if hasattr(ctx, 'get_pending_pipeline_tail') else []
pending_source = ctx.get_pending_pipeline_source() if hasattr(ctx, 'get_pending_pipeline_source') else None
current_source = ctx.get_current_stage_table_source_command() if hasattr(ctx, 'get_current_stage_table_source_command') else None
selection_only = len(stages) == 1 and stages[0] and stages[0][0].startswith('@')
if pending_tail and selection_only:
if pending_source and current_source and current_source == pending_source:
stages.extend(pending_tail)
if hasattr(ctx, 'clear_pending_pipeline_tail'):
ctx.clear_pending_pipeline_tail()
elif hasattr(ctx, 'clear_pending_pipeline_tail'):
ctx.clear_pending_pipeline_tail()
# Load config relative to CLI root
config = _load_cli_config()
@@ -1044,7 +1105,9 @@ def _execute_pipeline(tokens: list):
command_expanded = False
selected_row_args = []
if source_cmd:
skip_pipe_expansion = source_cmd == '.pipe' and len(stages) > 0
if source_cmd and not skip_pipe_expansion:
# Try to find row args for the selected indices
for idx in first_stage_selection_indices:
row_args = ctx.get_current_stage_table_row_selection_args(idx)
@@ -1151,6 +1214,9 @@ def _execute_pipeline(tokens: list):
if source_cmd == '.pipe' or source_cmd == '.adjective':
should_expand_to_command = True
if source_cmd == '.pipe' and (stage_index + 1 < len(stages) or stage_args):
# When piping playlist rows to another cmdlet, prefer item-based selection
should_expand_to_command = False
elif source_cmd == 'search-file' and source_args and 'youtube' in source_args:
# Special case for youtube search results: @N expands to .pipe
if stage_index + 1 >= len(stages):
@@ -1170,6 +1236,10 @@ def _execute_pipeline(tokens: list):
# Single format object
if source_cmd:
should_expand_to_command = True
# If we have a source command but no piped data (paused for selection), expand to command
if not should_expand_to_command and source_cmd and selection is not None and piped_result is None:
should_expand_to_command = True
# If expanding to command, replace this stage and re-execute
if should_expand_to_command and selection is not None:
@@ -1360,6 +1430,27 @@ def _execute_pipeline(tokens: list):
# Intermediate stage - thread to next stage
piped_result = pipeline_ctx.emits
ctx.set_last_result_table(None, pipeline_ctx.emits)
else:
# No output from this stage. If it presented a selectable table (e.g., format list), pause
# and stash the remaining pipeline so @N can resume with the selection applied.
if not is_last_stage:
stage_table_source = ctx.get_current_stage_table_source_command()
row_has_selection = ctx.get_current_stage_table_row_selection_args(0) is not None
if stage_table_source and row_has_selection:
pending_tail = stages[stage_index + 1:]
if pending_tail and pending_tail[0] and pending_tail[0][0].startswith('@'):
pending_tail = pending_tail[1:]
if hasattr(ctx, 'set_pending_pipeline_tail') and pending_tail:
ctx.set_pending_pipeline_tail(pending_tail, stage_table_source)
elif hasattr(ctx, 'clear_pending_pipeline_tail'):
ctx.clear_pending_pipeline_tail()
if pipeline_session and worker_manager:
try:
worker_manager.log_step(pipeline_session.worker_id, "Pipeline paused for @N selection")
except Exception:
pass
print("Pipeline paused: select a format with @N to continue remaining stages")
return
if ret_code != 0:
stage_status = "failed"