mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
fix: Windows CLI Unicode encoding errors (#411)
Signed-off-by: phernandez <paul@basicmachines.co> Signed-off-by: Claude <noreply@anthropic.com> Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com> Co-authored-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
|
||||
|
||||
# Package version - updated by release automation
|
||||
__version__ = "0.15.2"
|
||||
__version__ = "0.15.3.dev42+36149d6"
|
||||
|
||||
# API version for FastAPI - independent of package version
|
||||
__api_version__ = "v0"
|
||||
|
||||
@@ -77,7 +77,7 @@ class CLIAuth:
|
||||
verification_uri = device_response["verification_uri"]
|
||||
verification_uri_complete = device_response.get("verification_uri_complete")
|
||||
|
||||
console.print("\n[bold blue]🔐 Authentication Required[/bold blue]")
|
||||
console.print("\n[bold blue]Authentication Required[/bold blue]")
|
||||
console.print("\nTo authenticate, please visit:")
|
||||
console.print(f"[bold cyan]{verification_uri}[/bold cyan]")
|
||||
console.print(f"\nAnd enter this code: [bold yellow]{user_code}[/bold yellow]")
|
||||
@@ -171,7 +171,7 @@ class CLIAuth:
|
||||
# Secure the token file
|
||||
os.chmod(self.token_file, 0o600)
|
||||
|
||||
console.print(f"[green]✓ Tokens saved to {self.token_file}[/green]")
|
||||
console.print(f"[green]Tokens saved to {self.token_file}[/green]")
|
||||
|
||||
def load_tokens(self) -> dict | None:
|
||||
"""Load tokens from .bm-auth.json file."""
|
||||
@@ -233,7 +233,7 @@ class CLIAuth:
|
||||
if new_tokens:
|
||||
# Save new tokens (may include rotated refresh token)
|
||||
self.save_tokens(new_tokens)
|
||||
console.print("[green]✓ Token refreshed successfully[/green]")
|
||||
console.print("[green]Token refreshed successfully[/green]")
|
||||
return new_tokens["access_token"]
|
||||
else:
|
||||
console.print("[yellow]Token refresh failed. Please run 'login' again.[/yellow]")
|
||||
@@ -265,13 +265,13 @@ class CLIAuth:
|
||||
# Step 4: Save tokens
|
||||
self.save_tokens(tokens)
|
||||
|
||||
console.print("\n[green]✅ Successfully authenticated with Basic Memory Cloud![/green]")
|
||||
console.print("\n[green]Successfully authenticated with Basic Memory Cloud![/green]")
|
||||
return True
|
||||
|
||||
def logout(self) -> None:
|
||||
"""Remove stored authentication tokens."""
|
||||
if self.token_file.exists():
|
||||
self.token_file.unlink()
|
||||
console.print("[green]✓ Logged out successfully[/green]")
|
||||
console.print("[green]Logged out successfully[/green]")
|
||||
else:
|
||||
console.print("[yellow]No stored authentication found[/yellow]")
|
||||
|
||||
@@ -52,11 +52,11 @@ def login():
|
||||
config.cloud_mode = True
|
||||
config_manager.save_config(config)
|
||||
|
||||
console.print("[green]✓ Cloud mode enabled[/green]")
|
||||
console.print("[green]Cloud mode enabled[/green]")
|
||||
console.print(f"[dim]All CLI commands now work against {host_url}[/dim]")
|
||||
|
||||
except SubscriptionRequiredError as e:
|
||||
console.print("\n[red]✗ Subscription Required[/red]\n")
|
||||
console.print("\n[red]Subscription Required[/red]\n")
|
||||
console.print(f"[yellow]{e.args[0]}[/yellow]\n")
|
||||
console.print(f"Subscribe at: [blue underline]{e.subscribe_url}[/blue underline]\n")
|
||||
console.print(
|
||||
@@ -77,7 +77,7 @@ def logout():
|
||||
config.cloud_mode = False
|
||||
config_manager.save_config(config)
|
||||
|
||||
console.print("[green]✓ Cloud mode disabled[/green]")
|
||||
console.print("[green]Cloud mode disabled[/green]")
|
||||
console.print("[dim]All CLI commands now work locally[/dim]")
|
||||
|
||||
|
||||
@@ -157,12 +157,12 @@ def setup() -> None:
|
||||
# Step 2: Get tenant info
|
||||
console.print("\n[blue]Step 2: Getting tenant information...[/blue]")
|
||||
tenant_info = asyncio.run(get_mount_info())
|
||||
console.print(f"[green]✓ Found tenant: {tenant_info.tenant_id}[/green]")
|
||||
console.print(f"[green]Found tenant: {tenant_info.tenant_id}[/green]")
|
||||
|
||||
# Step 3: Generate credentials
|
||||
console.print("\n[blue]Step 3: Generating sync credentials...[/blue]")
|
||||
creds = asyncio.run(generate_mount_credentials(tenant_info.tenant_id))
|
||||
console.print("[green]✓ Generated secure credentials[/green]")
|
||||
console.print("[green]Generated secure credentials[/green]")
|
||||
|
||||
# Step 4: Configure rclone remote
|
||||
console.print("\n[blue]Step 4: Configuring rclone remote...[/blue]")
|
||||
@@ -171,7 +171,7 @@ def setup() -> None:
|
||||
secret_key=creds.secret_key,
|
||||
)
|
||||
|
||||
console.print("\n[bold green]✓ Cloud setup completed successfully![/bold green]")
|
||||
console.print("\n[bold green]Cloud setup completed successfully![/bold green]")
|
||||
console.print("\n[bold]Next steps:[/bold]")
|
||||
console.print("1. Add a project with local sync path:")
|
||||
console.print(" bm project add research --local-path ~/Documents/research")
|
||||
|
||||
@@ -106,5 +106,5 @@ def configure_rclone_remote(
|
||||
# Save updated config
|
||||
save_rclone_config(config)
|
||||
|
||||
console.print(f"[green]✓ Configured rclone remote: {REMOTE_NAME}[/green]")
|
||||
console.print(f"[green]Configured rclone remote: {REMOTE_NAME}[/green]")
|
||||
return REMOTE_NAME
|
||||
|
||||
@@ -58,7 +58,7 @@ def install_rclone_macos() -> None:
|
||||
try:
|
||||
console.print("[blue]Installing rclone via Homebrew...[/blue]")
|
||||
run_command(["brew", "install", "rclone"])
|
||||
console.print("[green]✓ rclone installed via Homebrew[/green]")
|
||||
console.print("[green]rclone installed via Homebrew[/green]")
|
||||
return
|
||||
except RcloneInstallError:
|
||||
console.print(
|
||||
@@ -69,7 +69,7 @@ def install_rclone_macos() -> None:
|
||||
console.print("[blue]Installing rclone via official script...[/blue]")
|
||||
try:
|
||||
run_command(["sh", "-c", "curl https://rclone.org/install.sh | sudo bash"])
|
||||
console.print("[green]✓ rclone installed via official script[/green]")
|
||||
console.print("[green]rclone installed via official script[/green]")
|
||||
except RcloneInstallError:
|
||||
raise RcloneInstallError(
|
||||
"Failed to install rclone. Please install manually: brew install rclone"
|
||||
@@ -83,7 +83,7 @@ def install_rclone_linux() -> None:
|
||||
try:
|
||||
console.print("[blue]Installing rclone via snap...[/blue]")
|
||||
run_command(["sudo", "snap", "install", "rclone"])
|
||||
console.print("[green]✓ rclone installed via snap[/green]")
|
||||
console.print("[green]rclone installed via snap[/green]")
|
||||
return
|
||||
except RcloneInstallError:
|
||||
console.print("[yellow]Snap installation failed, trying apt...[/yellow]")
|
||||
@@ -94,7 +94,7 @@ def install_rclone_linux() -> None:
|
||||
console.print("[blue]Installing rclone via apt...[/blue]")
|
||||
run_command(["sudo", "apt", "update"])
|
||||
run_command(["sudo", "apt", "install", "-y", "rclone"])
|
||||
console.print("[green]✓ rclone installed via apt[/green]")
|
||||
console.print("[green]rclone installed via apt[/green]")
|
||||
return
|
||||
except RcloneInstallError:
|
||||
console.print("[yellow]apt installation failed, trying official script...[/yellow]")
|
||||
@@ -103,7 +103,7 @@ def install_rclone_linux() -> None:
|
||||
console.print("[blue]Installing rclone via official script...[/blue]")
|
||||
try:
|
||||
run_command(["sh", "-c", "curl https://rclone.org/install.sh | sudo bash"])
|
||||
console.print("[green]✓ rclone installed via official script[/green]")
|
||||
console.print("[green]rclone installed via official script[/green]")
|
||||
except RcloneInstallError:
|
||||
raise RcloneInstallError(
|
||||
"Failed to install rclone. Please install manually: sudo snap install rclone"
|
||||
@@ -117,7 +117,7 @@ def install_rclone_windows() -> None:
|
||||
try:
|
||||
console.print("[blue]Installing rclone via winget...[/blue]")
|
||||
run_command(["winget", "install", "Rclone.Rclone"])
|
||||
console.print("[green]✓ rclone installed via winget[/green]")
|
||||
console.print("[green]rclone installed via winget[/green]")
|
||||
return
|
||||
except RcloneInstallError:
|
||||
console.print("[yellow]winget installation failed, trying chocolatey...[/yellow]")
|
||||
@@ -127,7 +127,7 @@ def install_rclone_windows() -> None:
|
||||
try:
|
||||
console.print("[blue]Installing rclone via chocolatey...[/blue]")
|
||||
run_command(["choco", "install", "rclone", "-y"])
|
||||
console.print("[green]✓ rclone installed via chocolatey[/green]")
|
||||
console.print("[green]rclone installed via chocolatey[/green]")
|
||||
return
|
||||
except RcloneInstallError:
|
||||
console.print("[yellow]chocolatey installation failed, trying scoop...[/yellow]")
|
||||
@@ -137,7 +137,7 @@ def install_rclone_windows() -> None:
|
||||
try:
|
||||
console.print("[blue]Installing rclone via scoop...[/blue]")
|
||||
run_command(["scoop", "install", "rclone"])
|
||||
console.print("[green]✓ rclone installed via scoop[/green]")
|
||||
console.print("[green]rclone installed via scoop[/green]")
|
||||
return
|
||||
except RcloneInstallError:
|
||||
console.print("[yellow]scoop installation failed[/yellow]")
|
||||
@@ -172,7 +172,7 @@ def install_rclone(platform_override: Optional[str] = None) -> None:
|
||||
if not is_rclone_installed():
|
||||
raise RcloneInstallError("rclone installation completed but command not found in PATH")
|
||||
|
||||
console.print("[green]✓ rclone installation completed successfully[/green]")
|
||||
console.print("[green]rclone installation completed successfully[/green]")
|
||||
|
||||
except RcloneInstallError:
|
||||
raise
|
||||
|
||||
@@ -108,7 +108,7 @@ async def upload_path(
|
||||
if dry_run:
|
||||
print(f"\nTotal: {len(files_to_upload)} file(s) ({size_str})")
|
||||
else:
|
||||
print(f"✓ Upload complete: {len(files_to_upload)} file(s) ({size_str})")
|
||||
print(f"Upload complete: {len(files_to_upload)} file(s) ({size_str})")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ def upload(
|
||||
console.print(f"[blue]Creating cloud project '{project}'...[/blue]")
|
||||
try:
|
||||
await create_cloud_project(project)
|
||||
console.print(f"[green]✓ Created project '{project}'[/green]")
|
||||
console.print(f"[green]Created project '{project}'[/green]")
|
||||
except Exception as e:
|
||||
console.print(f"[red]Failed to create project: {e}[/red]")
|
||||
raise typer.Exit(1)
|
||||
@@ -109,7 +109,7 @@ def upload(
|
||||
if dry_run:
|
||||
console.print("[yellow]DRY RUN complete - no files were uploaded[/yellow]")
|
||||
else:
|
||||
console.print(f"[green]✅ Successfully uploaded to '{project}'[/green]")
|
||||
console.print(f"[green]Successfully uploaded to '{project}'[/green]")
|
||||
|
||||
# Sync project if requested (skip on dry run)
|
||||
# Force full scan after bisync to ensure database is up-to-date with synced files
|
||||
|
||||
@@ -32,9 +32,9 @@ async def run_sync(project: Optional[str] = None, force_full: bool = False):
|
||||
url += "?force_full=true"
|
||||
response = await call_post(client, url)
|
||||
data = response.json()
|
||||
console.print(f"[green]✓ {data['message']}[/green]")
|
||||
console.print(f"[green]{data['message']}[/green]")
|
||||
except (ToolError, ValueError) as e:
|
||||
console.print(f"[red]✗ Sync failed: {e}[/red]")
|
||||
console.print(f"[red]Sync failed: {e}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@@ -47,5 +47,5 @@ async def get_project_info(project: str):
|
||||
response = await call_get(client, f"{project_item.project_url}/project/info")
|
||||
return ProjectInfoResponse.model_validate(response.json())
|
||||
except (ToolError, ValueError) as e:
|
||||
console.print(f"[red]✗ Sync failed: {e}[/red]")
|
||||
console.print(f"[red]Sync failed: {e}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
@@ -78,7 +78,7 @@ def list_projects() -> None:
|
||||
table.add_column("Default", style="magenta")
|
||||
|
||||
for project in result.projects:
|
||||
is_default = "✓" if project.is_default else ""
|
||||
is_default = "[X]" if project.is_default else ""
|
||||
normalized_path = normalize_project_path(project.path)
|
||||
|
||||
# Build row based on mode
|
||||
@@ -179,7 +179,7 @@ def add_project(
|
||||
)
|
||||
ConfigManager().save_config(config)
|
||||
|
||||
console.print(f"\n[green]✓ Local sync path configured: {local_sync_path}[/green]")
|
||||
console.print(f"\n[green]Local sync path configured: {local_sync_path}[/green]")
|
||||
console.print("\nNext steps:")
|
||||
console.print(f" 1. Preview: bm project bisync --name {name} --resync --dry-run")
|
||||
console.print(f" 2. Sync: bm project bisync --name {name} --resync")
|
||||
@@ -233,7 +233,7 @@ def setup_project_sync(
|
||||
)
|
||||
config_manager.save_config(config)
|
||||
|
||||
console.print(f"[green]✓ Sync configured for project '{name}'[/green]")
|
||||
console.print(f"[green]Sync configured for project '{name}'[/green]")
|
||||
console.print(f"\nLocal sync path: {resolved_path}")
|
||||
console.print("\nNext steps:")
|
||||
console.print(f" 1. Preview: bm project bisync --name {name} --resync --dry-run")
|
||||
@@ -286,7 +286,7 @@ def remove_project(
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(local_dir)
|
||||
console.print(f"[green]✓ Removed local sync directory: {local_path}[/green]")
|
||||
console.print(f"[green]Removed local sync directory: {local_path}[/green]")
|
||||
|
||||
# Clean up bisync state if it exists
|
||||
if has_bisync_state:
|
||||
@@ -296,7 +296,7 @@ def remove_project(
|
||||
bisync_state_path = get_project_bisync_state(name)
|
||||
if bisync_state_path.exists():
|
||||
shutil.rmtree(bisync_state_path)
|
||||
console.print("[green]✓ Removed bisync state[/green]")
|
||||
console.print("[green]Removed bisync state[/green]")
|
||||
|
||||
# Clean up cloud_projects config entry
|
||||
if config.cloud_mode_enabled and name in config.cloud_projects:
|
||||
@@ -407,7 +407,7 @@ def move_project(
|
||||
"[yellow]You must manually move your project files from the old location to:[/yellow]\n"
|
||||
f"[cyan]{resolved_path}[/cyan]\n\n"
|
||||
"[dim]Basic Memory has only updated the configuration - your files remain in their original location.[/dim]",
|
||||
title="⚠️ Manual File Movement Required",
|
||||
title="Manual File Movement Required",
|
||||
border_style="yellow",
|
||||
expand=False,
|
||||
)
|
||||
@@ -477,7 +477,7 @@ def sync_project_command(
|
||||
success = project_sync(sync_project, bucket_name, dry_run=dry_run, verbose=verbose)
|
||||
|
||||
if success:
|
||||
console.print(f"[green]✓ {name} synced successfully[/green]")
|
||||
console.print(f"[green]{name} synced successfully[/green]")
|
||||
|
||||
# Trigger database sync if not a dry run
|
||||
if not dry_run:
|
||||
@@ -494,7 +494,7 @@ def sync_project_command(
|
||||
except Exception as e:
|
||||
console.print(f"[yellow]Warning: Could not trigger database sync: {e}[/yellow]")
|
||||
else:
|
||||
console.print(f"[red]✗ {name} sync failed[/red]")
|
||||
console.print(f"[red]{name} sync failed[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
except RcloneError as e:
|
||||
@@ -512,7 +512,7 @@ def bisync_project_command(
|
||||
resync: bool = typer.Option(False, "--resync", help="Force new baseline"),
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
|
||||
) -> None:
|
||||
"""Two-way sync: local ↔ cloud (bidirectional sync).
|
||||
"""Two-way sync: local <-> cloud (bidirectional sync).
|
||||
|
||||
Examples:
|
||||
bm project bisync --name research --resync # First time
|
||||
@@ -562,13 +562,13 @@ def bisync_project_command(
|
||||
)
|
||||
|
||||
# Run bisync
|
||||
console.print(f"[blue]Bisync {name} (local ↔ cloud)...[/blue]")
|
||||
console.print(f"[blue]Bisync {name} (local <-> cloud)...[/blue]")
|
||||
success = project_bisync(
|
||||
sync_project, bucket_name, dry_run=dry_run, resync=resync, verbose=verbose
|
||||
)
|
||||
|
||||
if success:
|
||||
console.print(f"[green]✓ {name} bisync completed successfully[/green]")
|
||||
console.print(f"[green]{name} bisync completed successfully[/green]")
|
||||
|
||||
# Update config
|
||||
config.cloud_projects[name].last_sync = datetime.now()
|
||||
@@ -590,7 +590,7 @@ def bisync_project_command(
|
||||
except Exception as e:
|
||||
console.print(f"[yellow]Warning: Could not trigger database sync: {e}[/yellow]")
|
||||
else:
|
||||
console.print(f"[red]✗ {name} bisync failed[/red]")
|
||||
console.print(f"[red]{name} bisync failed[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
except RcloneError as e:
|
||||
@@ -658,9 +658,9 @@ def check_project_command(
|
||||
match = project_check(sync_project, bucket_name, one_way=one_way)
|
||||
|
||||
if match:
|
||||
console.print(f"[green]✓ {name} files match[/green]")
|
||||
console.print(f"[green]{name} files match[/green]")
|
||||
else:
|
||||
console.print(f"[yellow]⚠ {name} has differences[/yellow]")
|
||||
console.print(f"[yellow]!{name} has differences[/yellow]")
|
||||
|
||||
except RcloneError as e:
|
||||
console.print(f"[red]Check error: {e}[/red]")
|
||||
@@ -691,7 +691,7 @@ def bisync_reset(
|
||||
|
||||
# Remove the entire state directory
|
||||
shutil.rmtree(state_path)
|
||||
console.print(f"[green]✓ Cleared bisync state for project '{name}'[/green]")
|
||||
console.print(f"[green]Cleared bisync state for project '{name}'[/green]")
|
||||
console.print("\nNext steps:")
|
||||
console.print(f" 1. Preview: bm project bisync --name {name} --resync --dry-run")
|
||||
console.print(f" 2. Sync: bm project bisync --name {name} --resync")
|
||||
@@ -782,13 +782,13 @@ def display_project_info(
|
||||
f"[bold]Project:[/bold] {info.project_name}\n"
|
||||
f"[bold]Path:[/bold] {info.project_path}\n"
|
||||
f"[bold]Default Project:[/bold] {info.default_project}\n",
|
||||
title="📊 Basic Memory Project Info",
|
||||
title="Basic Memory Project Info",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
|
||||
# Statistics section
|
||||
stats_table = Table(title="📈 Statistics")
|
||||
stats_table = Table(title="Statistics")
|
||||
stats_table.add_column("Metric", style="cyan")
|
||||
stats_table.add_column("Count", style="green")
|
||||
|
||||
@@ -804,7 +804,7 @@ def display_project_info(
|
||||
|
||||
# Entity types
|
||||
if info.statistics.entity_types:
|
||||
entity_types_table = Table(title="📑 Entity Types")
|
||||
entity_types_table = Table(title="Entity Types")
|
||||
entity_types_table.add_column("Type", style="blue")
|
||||
entity_types_table.add_column("Count", style="green")
|
||||
|
||||
@@ -815,7 +815,7 @@ def display_project_info(
|
||||
|
||||
# Most connected entities
|
||||
if info.statistics.most_connected_entities: # pragma: no cover
|
||||
connected_table = Table(title="🔗 Most Connected Entities")
|
||||
connected_table = Table(title="Most Connected Entities")
|
||||
connected_table.add_column("Title", style="blue")
|
||||
connected_table.add_column("Permalink", style="cyan")
|
||||
connected_table.add_column("Relations", style="green")
|
||||
@@ -829,7 +829,7 @@ def display_project_info(
|
||||
|
||||
# Recent activity
|
||||
if info.activity.recently_updated: # pragma: no cover
|
||||
recent_table = Table(title="🕒 Recent Activity")
|
||||
recent_table = Table(title="Recent Activity")
|
||||
recent_table.add_column("Title", style="blue")
|
||||
recent_table.add_column("Type", style="cyan")
|
||||
recent_table.add_column("Last Updated", style="green")
|
||||
@@ -849,7 +849,7 @@ def display_project_info(
|
||||
console.print(recent_table)
|
||||
|
||||
# Available projects
|
||||
projects_table = Table(title="📁 Available Projects")
|
||||
projects_table = Table(title="Available Projects")
|
||||
projects_table.add_column("Name", style="blue")
|
||||
projects_table.add_column("Path", style="cyan")
|
||||
projects_table.add_column("Default", style="green")
|
||||
@@ -857,7 +857,7 @@ def display_project_info(
|
||||
for name, proj_info in info.available_projects.items():
|
||||
is_default = name == info.default_project
|
||||
project_path = proj_info["path"]
|
||||
projects_table.add_row(name, project_path, "✓" if is_default else "")
|
||||
projects_table.add_row(name, project_path, "[X]" if is_default else "")
|
||||
|
||||
console.print(projects_table)
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ def display_changes(
|
||||
del_branch = tree.add("[red]Deleted[/red]")
|
||||
add_files_to_tree(del_branch, changes.deleted, "red")
|
||||
if changes.skipped_files:
|
||||
skip_branch = tree.add("[red]⚠️ Skipped (Circuit Breaker)[/red]")
|
||||
skip_branch = tree.add("[red]! Skipped (Circuit Breaker)[/red]")
|
||||
for skipped in sorted(changes.skipped_files, key=lambda x: x.path):
|
||||
skip_branch.add(
|
||||
f"[red]{skipped.path}[/red] "
|
||||
@@ -133,7 +133,7 @@ def display_changes(
|
||||
if changes.skipped_files:
|
||||
skip_count = len(changes.skipped_files)
|
||||
tree.add(
|
||||
f"[red]⚠️ {skip_count} file{'s' if skip_count != 1 else ''} "
|
||||
f"[red]! {skip_count} file{'s' if skip_count != 1 else ''} "
|
||||
f"skipped due to repeated failures[/red]"
|
||||
)
|
||||
|
||||
@@ -152,7 +152,7 @@ async def run_status(project: Optional[str] = None, verbose: bool = False): # p
|
||||
display_changes(project_item.name, "Status", sync_report, verbose)
|
||||
|
||||
except (ValueError, ToolError) as e:
|
||||
console.print(f"[red]✗ Error: {e}[/red]")
|
||||
console.print(f"[red]Error: {e}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user