Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
fix: move URL install confirmation prompt before spinner (#2783)
The typer.confirm() prompt inside console.status() was overwritten by
Rich's spinner animation, making extension add --from <url> appear hung.

Move URL validation and the default-deny confirmation prompt before the
spinner block so the user can see and respond to the [y/N] prompt.
  • Loading branch information
mnriem committed Jun 1, 2026
commit b0738a0ceb1b7449f3c2ab827c5b5426e500b8c0
59 changes: 32 additions & 27 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3082,6 +3082,38 @@ def extension_add(
manager = ExtensionManager(project_root)
speckit_version = get_speckit_version()

# Prompt for URL-based installs BEFORE the spinner so the user can
# actually see and respond to the confirmation (the Rich status
# spinner overwrites the typer.confirm prompt line, making it appear
# as though the command is hung).
if from_url:
from urllib.parse import urlparse

Comment thread
mnriem marked this conversation as resolved.
parsed = urlparse(from_url)
is_localhost = parsed.hostname in ("localhost", "127.0.0.1", "::1")

if parsed.scheme != "https" and not (parsed.scheme == "http" and is_localhost):
console.print("[red]Error:[/red] URL must use HTTPS for security.")
console.print("HTTP is only allowed for localhost URLs.")
raise typer.Exit(1)

# Warn about untrusted sources — default-deny confirmation
console.print()
console.print(Panel(
f"[bold]You are installing an extension from an external URL that is not\n"
f"listed in any of your configured extension catalogs.[/bold]\n\n"
f"URL: {from_url}\n\n"
f"Only install extensions from sources you trust.",
Comment thread
mnriem marked this conversation as resolved.
title="[bold yellow]⚠ Untrusted Source[/bold yellow]",
border_style="yellow",
padding=(1, 2),
))
console.print()
confirm = typer.confirm("Continue with installation?", default=False)
if not confirm:
console.print("Cancelled")
raise typer.Exit(0)

try:
with console.status(f"[cyan]Installing extension: {extension}[/cyan]"):
if dev:
Expand All @@ -3106,33 +3138,6 @@ def extension_add(
# Install from URL (ZIP file)
import urllib.request
import urllib.error
from urllib.parse import urlparse

# Validate URL
parsed = urlparse(from_url)
is_localhost = parsed.hostname in ("localhost", "127.0.0.1", "::1")

if parsed.scheme != "https" and not (parsed.scheme == "http" and is_localhost):
console.print("[red]Error:[/red] URL must use HTTPS for security.")
console.print("HTTP is only allowed for localhost URLs.")
raise typer.Exit(1)

# Warn about untrusted sources — default-deny confirmation
console.print()
console.print(Panel(
f"[bold]You are installing an extension from an external URL that is not\n"
f"listed in any of your configured extension catalogs.[/bold]\n\n"
f"URL: {from_url}\n\n"
f"Only install extensions from sources you trust.",
title="[bold yellow]⚠ Untrusted Source[/bold yellow]",
border_style="yellow",
padding=(1, 2),
))
console.print()
confirm = typer.confirm("Continue with installation?", default=False)
if not confirm:
console.print("Cancelled")
raise typer.Exit(0)

console.print(f"Downloading from {from_url}...")
Comment thread
mnriem marked this conversation as resolved.
Outdated

Expand Down
Loading