diff --git a/aai_cli/app/init_exec.py b/aai_cli/app/init_exec.py index 87fc29e7..7c43aa40 100644 --- a/aai_cli/app/init_exec.py +++ b/aai_cli/app/init_exec.py @@ -45,11 +45,9 @@ class InitOptions: def _pick_template() -> str: """Interactive picker; raises a usage error when there's no TTY to prompt on.""" if not stdio.interactive_stdio(): - raise CLIError( + raise UsageError( "No template given and not running interactively. " f"Pass one of: {', '.join(templates.TEMPLATE_ORDER)}.", - error_type="usage_error", - exit_code=1, ) try: import questionary @@ -86,10 +84,8 @@ def _resolve_template(template: str | None) -> str: """Resolve the template name: the picker when omitted, else validate the arg.""" chosen = template if template is not None else _pick_template() if not templates.is_template(chosen): - raise CLIError( + raise UsageError( f"Unknown template {chosen!r}. Choose one of: {', '.join(templates.TEMPLATE_ORDER)}.", - error_type="usage_error", - exit_code=1, ) return chosen @@ -165,22 +161,16 @@ def _resolve_target( """Resolve the target directory, rejecting --here+DIRECTORY, an existing file, or a non-empty conflict. Returns the target and whether --force is overlaying it.""" if here and directory: - raise CLIError( - "Pass either a DIRECTORY or --here, not both.", - error_type="usage_error", - exit_code=1, - ) + raise UsageError("Pass either a DIRECTORY or --here, not both.") target = _resolve_dir(directory, chosen, here=here) if target.exists() and not target.is_dir(): raise UsageError(f"{target} exists and is not a directory.") _reject_file_ancestor(target) conflict = scaffold.target_conflict(target) if conflict and not force: - raise CLIError( + raise UsageError( f"{target} already exists and is not empty. " f"Use --force to overwrite or pick another directory.", - error_type="usage_error", - exit_code=1, ) return target, conflict diff --git a/aai_cli/app/transcribe/run.py b/aai_cli/app/transcribe/run.py index c303b928..3ec3d189 100644 --- a/aai_cli/app/transcribe/run.py +++ b/aai_cli/app/transcribe/run.py @@ -315,6 +315,10 @@ def run_transcribe(opts: TranscribeOptions, state: AppState, *, json_mode: bool) transcribe_validate.validate_out_path(opts.out) transcribe_validate.validate_json_with_output(opts.output_field, json_mode=json_mode) client.validate_chars_per_caption(opts.chars_per_caption, opts.output_field) + # --download-sections only slices a downloadable-URL fetch; for a local file, + # stdin, remote bucket, or directory batch it would be dropped silently — reject + # it up front like `clip`/`dub` rather than billing a full-file transcription. + youtube.validate_sections_flag(opts.source, list(opts.download_sections or [])) merged = config_builder.merge_transcribe_config( flags=flags, overrides=opts.config_kv, config_file=opts.config_file diff --git a/aai_cli/commands/dev/__init__.py b/aai_cli/commands/dev/__init__.py index 42e269df..2c52fb78 100644 --- a/aai_cli/commands/dev/__init__.py +++ b/aai_cli/commands/dev/__init__.py @@ -32,7 +32,7 @@ ) def dev( ctx: typer.Context, - port: int = typer.Option(3000, "--port", help="Local server port"), + port: int = typer.Option(3000, "--port", help="Local server port", min=0, max=65535), host: str = typer.Option( devserver.LOCAL_HOST, "--host", diff --git a/aai_cli/commands/init.py b/aai_cli/commands/init.py index ec159123..5784644c 100644 --- a/aai_cli/commands/init.py +++ b/aai_cli/commands/init.py @@ -65,7 +65,9 @@ def init( ), ), here: bool = typer.Option(False, "--here", help="Scaffold into the current directory"), - port: int = typer.Option(init_exec.DEFAULT_PORT, "--port", help="Local server port"), + port: int = typer.Option( + init_exec.DEFAULT_PORT, "--port", help="Local server port", min=0, max=65535 + ), json_out: bool = options.json_option(), ) -> None: """Scaffold a new app from a template and launch it diff --git a/aai_cli/commands/share/__init__.py b/aai_cli/commands/share/__init__.py index 6301e923..cc7f9db2 100644 --- a/aai_cli/commands/share/__init__.py +++ b/aai_cli/commands/share/__init__.py @@ -29,7 +29,7 @@ ) def share( ctx: typer.Context, - port: int = typer.Option(3000, "--port", help="Local server port"), + port: int = typer.Option(3000, "--port", help="Local server port", min=0, max=65535), no_install: bool = typer.Option( False, "--no-install", help="Skip dependency install; launch directly" ), diff --git a/aai_cli/commands/stream/__init__.py b/aai_cli/commands/stream/__init__.py index f3dd2638..531643ab 100644 --- a/aai_cli/commands/stream/__init__.py +++ b/aai_cli/commands/stream/__init__.py @@ -128,12 +128,14 @@ def stream( None, "--min-turn-silence", help="Min silence to end a turn (ms)", + min=1, rich_help_panel=help_panels.OPT_TURNS, ), max_turn_silence: int | None = typer.Option( None, "--max-turn-silence", help="Max silence before ending a turn (ms)", + min=1, rich_help_panel=help_panels.OPT_TURNS, ), vad_threshold: float | None = typer.Option( @@ -190,6 +192,7 @@ def stream( None, "--inactivity-timeout", help="Auto-close after N seconds idle", + min=1, rich_help_panel=help_panels.OPT_FEATURES, ), # guardrails @@ -255,6 +258,7 @@ def stream( llm.DEFAULT_MAX_TOKENS, "--max-tokens", help="Max tokens", + min=1, rich_help_panel=help_panels.OPT_LLM, ), # escape hatch diff --git a/aai_cli/core/youtube.py b/aai_cli/core/youtube.py index 08ef06aa..3a956e7f 100644 --- a/aai_cli/core/youtube.py +++ b/aai_cli/core/youtube.py @@ -158,7 +158,7 @@ def validate_video_flag(source: str, *, video: bool) -> None: ) -def validate_sections_flag(source: str, sections: list[str]) -> None: +def validate_sections_flag(source: str | None, sections: list[str]) -> None: """Reject ``--download-sections`` for a source that isn't a downloadable URL. The specs select which parts of a media-page download yt-dlp fetches; a local diff --git a/tests/__snapshots__/test_snapshots_help_build.ambr b/tests/__snapshots__/test_snapshots_help_build.ambr index 2d42f2f4..3437689b 100644 --- a/tests/__snapshots__/test_snapshots_help_build.ambr +++ b/tests/__snapshots__/test_snapshots_help_build.ambr @@ -47,14 +47,18 @@ browser. ╭─ Options ────────────────────────────────────────────────────────────────────╮ - │ --port INTEGER Local server port [default: 3000] │ - │ --host TEXT Interface to bind. Loopback by default; pass │ - │ 0.0.0.0 to expose on your network. │ - │ [default: 127.0.0.1] │ - │ --no-open Launch, but don't open the browser │ - │ --no-install Skip dependency install; launch directly │ - │ --json -j Output raw JSON │ - │ --help Show this message and exit. │ + │ --port INTEGER RANGE Local server port │ + │ [0<=x<=65535] [default: 3000] │ + │ --host TEXT Interface to bind. Loopback │ + │ by default; pass 0.0.0.0 to │ + │ expose on your network. │ + │ [default: 127.0.0.1] │ + │ --no-open Launch, but don't open the │ + │ browser │ + │ --no-install Skip dependency install; │ + │ launch directly │ + │ --json -j Output raw JSON │ + │ --help Show this message and exit. │ ╰──────────────────────────────────────────────────────────────────────────────╯ Examples @@ -90,15 +94,20 @@ │ directory [DIRECTORY] Target directory (default: