Implement Typer CLI commands and tests

This commit is contained in:
thePR0M3TH3AN
2025-07-09 08:46:38 -04:00
parent a3015aac0d
commit 333ff91da3
2 changed files with 221 additions and 8 deletions

View File

@@ -1,6 +1,11 @@
import typer
from pathlib import Path
from typing import Optional
import typer
from password_manager.manager import PasswordManager
from password_manager.entry_types import EntryType
app = typer.Typer(help="SeedPass command line interface")
# Global option shared across all commands
@@ -27,6 +32,16 @@ app.add_typer(fingerprint_app, name="fingerprint")
app.add_typer(util_app, name="util")
def _get_pm(ctx: typer.Context) -> PasswordManager:
"""Return a PasswordManager optionally selecting a fingerprint."""
pm = PasswordManager()
fp = ctx.obj.get("fingerprint")
if fp:
# `select_fingerprint` will initialize managers
pm.select_fingerprint(fp)
return pm
@app.callback()
def main(ctx: typer.Context, fingerprint: Optional[str] = fingerprint_option) -> None:
"""SeedPass CLI entry point."""
@@ -34,9 +49,77 @@ def main(ctx: typer.Context, fingerprint: Optional[str] = fingerprint_option) ->
@entry_app.command("list")
def entry_list(ctx: typer.Context) -> None:
def entry_list(
ctx: typer.Context,
sort: str = typer.Option(
"index", "--sort", help="Sort by 'index', 'label', or 'username'"
),
kind: Optional[str] = typer.Option(None, "--kind", help="Filter by entry type"),
archived: bool = typer.Option(False, "--archived", help="Include archived"),
) -> None:
"""List entries in the vault."""
typer.echo(f"Listing entries for fingerprint: {ctx.obj.get('fingerprint')}")
pm = _get_pm(ctx)
entries = pm.entry_manager.list_entries(
sort_by=sort, filter_kind=kind, include_archived=archived
)
for idx, label, username, url, is_archived in entries:
line = f"{idx}: {label}"
if username:
line += f" ({username})"
if url:
line += f" {url}"
if is_archived:
line += " [archived]"
typer.echo(line)
@entry_app.command("search")
def entry_search(ctx: typer.Context, query: str) -> None:
"""Search entries."""
pm = _get_pm(ctx)
results = pm.entry_manager.search_entries(query)
if not results:
typer.echo("No matching entries found")
return
for idx, label, username, url, _arch in results:
line = f"{idx}: {label}"
if username:
line += f" ({username})"
if url:
line += f" {url}"
typer.echo(line)
@entry_app.command("get")
def entry_get(ctx: typer.Context, query: str) -> None:
"""Retrieve a single entry's secret."""
pm = _get_pm(ctx)
matches = pm.entry_manager.search_entries(query)
if len(matches) == 0:
typer.echo("No matching entries found")
raise typer.Exit(code=1)
if len(matches) > 1:
typer.echo("Matches:")
for idx, label, username, _url, _arch in matches:
name = f"{idx}: {label}"
if username:
name += f" ({username})"
typer.echo(name)
raise typer.Exit(code=1)
index = matches[0][0]
entry = pm.entry_manager.retrieve_entry(index)
etype = entry.get("type", entry.get("kind"))
if etype == EntryType.PASSWORD.value:
length = int(entry.get("length", 12))
password = pm.password_generator.generate_password(length, index)
typer.echo(password)
elif etype == EntryType.TOTP.value:
code = pm.entry_manager.get_totp_code(index, pm.parent_seed)
typer.echo(code)
else:
typer.echo("Unsupported entry type")
raise typer.Exit(code=1)
@vault_app.command("export")
@@ -44,9 +127,9 @@ def vault_export(
ctx: typer.Context, file: str = typer.Option(..., help="Output file")
) -> None:
"""Export the vault."""
typer.echo(
f"Exporting vault for fingerprint {ctx.obj.get('fingerprint')} to {file}"
)
pm = _get_pm(ctx)
pm.handle_export_database(Path(file))
typer.echo(str(file))
@nostr_app.command("sync")
@@ -55,16 +138,31 @@ def nostr_sync(ctx: typer.Context) -> None:
typer.echo(f"Syncing vault for fingerprint: {ctx.obj.get('fingerprint')}")
@nostr_app.command("get-pubkey")
def nostr_get_pubkey(ctx: typer.Context) -> None:
"""Display the active profile's npub."""
pm = _get_pm(ctx)
npub = pm.nostr_client.key_manager.get_npub()
typer.echo(npub)
@config_app.command("get")
def config_get(ctx: typer.Context, key: str) -> None:
"""Get a configuration value."""
typer.echo(f"Get config '{key}' for fingerprint: {ctx.obj.get('fingerprint')}")
pm = _get_pm(ctx)
value = pm.config_manager.load_config(require_pin=False).get(key)
if value is None:
typer.echo("Key not found")
else:
typer.echo(str(value))
@fingerprint_app.command("list")
def fingerprint_list(ctx: typer.Context) -> None:
"""List available seed profiles."""
typer.echo("Listing seed profiles")
pm = _get_pm(ctx)
for fp in pm.fingerprint_manager.list_fingerprints():
typer.echo(fp)
@util_app.command("generate-password")