From 88fcb675c24b33e314993fc083c4a2c68e9aa335 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 8 Jul 2025 21:55:12 -0400 Subject: [PATCH 01/30] Add Typer CLI --- README.md | 6 ++++ pyproject.toml | 7 ++++ requirements.lock | 1 + src/requirements.txt | 1 + src/seedpass/__init__.py | 0 src/seedpass/cli.py | 73 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 88 insertions(+) create mode 100644 src/seedpass/__init__.py create mode 100644 src/seedpass/cli.py diff --git a/README.md b/README.md index 011ab13..d2b591b 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,12 @@ After successfully installing the dependencies, you can run SeedPass using the f python src/main.py ``` +You can also use the new Typer-based CLI: +```bash +seedpass --help +``` +For details see [docs/advanced_cli.md](docs/advanced_cli.md). + ### Running the Application 1. **Start the Application:** diff --git a/pyproject.toml b/pyproject.toml index daed3ac..4165dd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,10 @@ +[project] +name = "seedpass" +version = "0.1.0" + +[project.scripts] +seedpass = "seedpass.cli:app" + [tool.mypy] python_version = "3.11" strict = true diff --git a/requirements.lock b/requirements.lock index 478be5c..5b74e12 100644 --- a/requirements.lock +++ b/requirements.lock @@ -61,3 +61,4 @@ varint==1.0.2 websocket-client==1.7.0 websockets==15.0.1 yarl==1.20.1 +typer==0.12.3 diff --git a/src/requirements.txt b/src/requirements.txt index 184df64..5e9e0a0 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -24,3 +24,4 @@ pyotp>=2.8.0 freezegun pyperclip qrcode>=8.2 +typer>=0.12.3 diff --git a/src/seedpass/__init__.py b/src/seedpass/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/seedpass/cli.py b/src/seedpass/cli.py new file mode 100644 index 0000000..885ae6a --- /dev/null +++ b/src/seedpass/cli.py @@ -0,0 +1,73 @@ +import typer +from typing import Optional + +app = typer.Typer(help="SeedPass command line interface") + +# Global option shared across all commands +fingerprint_option = typer.Option( + None, + "--fingerprint", + "-f", + help="Specify which seed profile to use", +) + +# Sub command groups +entry_app = typer.Typer(help="Manage individual entries") +vault_app = typer.Typer(help="Manage the entire vault") +nostr_app = typer.Typer(help="Interact with Nostr relays") +config_app = typer.Typer(help="Manage configuration values") +fingerprint_app = typer.Typer(help="Manage seed profiles") +util_app = typer.Typer(help="Utility commands") + +app.add_typer(entry_app, name="entry") +app.add_typer(vault_app, name="vault") +app.add_typer(nostr_app, name="nostr") +app.add_typer(config_app, name="config") +app.add_typer(fingerprint_app, name="fingerprint") +app.add_typer(util_app, name="util") + + +@app.callback() +def main(ctx: typer.Context, fingerprint: Optional[str] = fingerprint_option) -> None: + """SeedPass CLI entry point.""" + ctx.obj = {"fingerprint": fingerprint} + + +@entry_app.command("list") +def entry_list(ctx: typer.Context) -> None: + """List entries in the vault.""" + typer.echo(f"Listing entries for fingerprint: {ctx.obj.get('fingerprint')}") + + +@vault_app.command("export") +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}" + ) + + +@nostr_app.command("sync") +def nostr_sync(ctx: typer.Context) -> None: + """Sync with configured Nostr relays.""" + typer.echo(f"Syncing vault for fingerprint: {ctx.obj.get('fingerprint')}") + + +@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')}") + + +@fingerprint_app.command("list") +def fingerprint_list(ctx: typer.Context) -> None: + """List available seed profiles.""" + typer.echo("Listing seed profiles") + + +@util_app.command("generate-password") +def generate_password(ctx: typer.Context, length: int = 24) -> None: + """Generate a strong password.""" + typer.echo(f"Generate password of length {length} for {ctx.obj.get('fingerprint')}") From 333ff91da3e311c3917ea3d3379792af7a1446dd Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 9 Jul 2025 08:46:38 -0400 Subject: [PATCH 02/30] Implement Typer CLI commands and tests --- src/seedpass/cli.py | 114 ++++++++++++++++++++++++++++++++--- src/tests/test_typer_cli.py | 115 ++++++++++++++++++++++++++++++++++++ 2 files changed, 221 insertions(+), 8 deletions(-) create mode 100644 src/tests/test_typer_cli.py diff --git a/src/seedpass/cli.py b/src/seedpass/cli.py index 885ae6a..1b4a44a 100644 --- a/src/seedpass/cli.py +++ b/src/seedpass/cli.py @@ -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") diff --git a/src/tests/test_typer_cli.py b/src/tests/test_typer_cli.py new file mode 100644 index 0000000..d4df89f --- /dev/null +++ b/src/tests/test_typer_cli.py @@ -0,0 +1,115 @@ +from types import SimpleNamespace +from pathlib import Path + +from typer.testing import CliRunner + +from seedpass.cli import app, PasswordManager +from seedpass import cli +from password_manager.entry_types import EntryType + +runner = CliRunner() + + +def test_entry_list(monkeypatch): + called = {} + + def list_entries(sort_by="index", filter_kind=None, include_archived=False): + called["args"] = (sort_by, filter_kind, include_archived) + return [(0, "Site", "user", "", False)] + + pm = SimpleNamespace( + entry_manager=SimpleNamespace(list_entries=list_entries), + select_fingerprint=lambda fp: None, + ) + monkeypatch.setattr(cli, "PasswordManager", lambda: pm) + result = runner.invoke(app, ["entry", "list"]) + assert result.exit_code == 0 + assert "Site" in result.stdout + assert called["args"] == ("index", None, False) + + +def test_entry_search(monkeypatch): + pm = SimpleNamespace( + entry_manager=SimpleNamespace( + search_entries=lambda q: [(1, "L", None, None, False)] + ), + select_fingerprint=lambda fp: None, + ) + monkeypatch.setattr(cli, "PasswordManager", lambda: pm) + result = runner.invoke(app, ["entry", "search", "l"]) + assert result.exit_code == 0 + assert "1: L" in result.stdout + + +def test_entry_get_password(monkeypatch): + def search(q): + return [(2, "Example", "", "", False)] + + entry = {"type": EntryType.PASSWORD.value, "length": 8} + pm = SimpleNamespace( + entry_manager=SimpleNamespace( + search_entries=search, + retrieve_entry=lambda i: entry, + get_totp_code=lambda i, s: "", + ), + password_generator=SimpleNamespace(generate_password=lambda l, i: "pw"), + parent_seed="seed", + select_fingerprint=lambda fp: None, + ) + monkeypatch.setattr(cli, "PasswordManager", lambda: pm) + result = runner.invoke(app, ["entry", "get", "ex"]) + assert result.exit_code == 0 + assert "pw" in result.stdout + + +def test_vault_export(monkeypatch, tmp_path): + called = {} + + def export_db(path): + called["path"] = path + + pm = SimpleNamespace( + handle_export_database=export_db, select_fingerprint=lambda fp: None + ) + monkeypatch.setattr(cli, "PasswordManager", lambda: pm) + out_path = tmp_path / "out.json" + result = runner.invoke(app, ["vault", "export", "--file", str(out_path)]) + assert result.exit_code == 0 + assert called["path"] == out_path + + +def test_nostr_get_pubkey(monkeypatch): + pm = SimpleNamespace( + nostr_client=SimpleNamespace( + key_manager=SimpleNamespace(get_npub=lambda: "np") + ), + select_fingerprint=lambda fp: None, + ) + monkeypatch.setattr(cli, "PasswordManager", lambda: pm) + result = runner.invoke(app, ["nostr", "get-pubkey"]) + assert result.exit_code == 0 + assert "np" in result.stdout + + +def test_fingerprint_list(monkeypatch): + pm = SimpleNamespace( + fingerprint_manager=SimpleNamespace(list_fingerprints=lambda: ["a", "b"]), + select_fingerprint=lambda fp: None, + ) + monkeypatch.setattr(cli, "PasswordManager", lambda: pm) + result = runner.invoke(app, ["fingerprint", "list"]) + assert result.exit_code == 0 + assert "a" in result.stdout and "b" in result.stdout + + +def test_config_get(monkeypatch): + pm = SimpleNamespace( + config_manager=SimpleNamespace( + load_config=lambda require_pin=False: {"x": "1"} + ), + select_fingerprint=lambda fp: None, + ) + monkeypatch.setattr(cli, "PasswordManager", lambda: pm) + result = runner.invoke(app, ["config", "get", "x"]) + assert result.exit_code == 0 + assert "1" in result.stdout From a42f880ac58013f86f4ab1679e78ed2a92aca228 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 9 Jul 2025 09:07:06 -0400 Subject: [PATCH 03/30] Add FastAPI API server --- requirements.lock | 2 + src/requirements.txt | 2 + src/seedpass/api.py | 104 ++++++++++++++++++++++++++++++++++++++++++ src/seedpass/cli.py | 27 +++++++++++ src/tests/test_api.py | 50 ++++++++++++++++++++ 5 files changed, 185 insertions(+) create mode 100644 src/seedpass/api.py create mode 100644 src/tests/test_api.py diff --git a/requirements.lock b/requirements.lock index 5b74e12..7d79820 100644 --- a/requirements.lock +++ b/requirements.lock @@ -19,6 +19,7 @@ cryptography==45.0.4 ecdsa==0.19.1 ed25519-blake2b==1.4.1 execnet==2.1.1 +fastapi==0.116.0 frozenlist==1.7.0 glob2==0.7 hypothesis==6.135.20 @@ -57,6 +58,7 @@ termcolor==3.1.0 toml==0.10.2 tomli==2.2.1 urllib3==2.5.0 +uvicorn==0.35.0 varint==1.0.2 websocket-client==1.7.0 websockets==15.0.1 diff --git a/src/requirements.txt b/src/requirements.txt index 5e9e0a0..14018ab 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -25,3 +25,5 @@ freezegun pyperclip qrcode>=8.2 typer>=0.12.3 +fastapi>=0.116.0 +uvicorn>=0.35.0 diff --git a/src/seedpass/api.py b/src/seedpass/api.py new file mode 100644 index 0000000..fbe1537 --- /dev/null +++ b/src/seedpass/api.py @@ -0,0 +1,104 @@ +"""SeedPass FastAPI server.""" + +from __future__ import annotations + +import os +import secrets +from typing import Any, List, Optional + +from fastapi import FastAPI, Header, HTTPException +import asyncio +import sys +from fastapi.middleware.cors import CORSMiddleware + +from password_manager.manager import PasswordManager + + +app = FastAPI() + +_pm: Optional[PasswordManager] = None +_token: str = "" + + +def _check_token(auth: str | None) -> None: + if auth != f"Bearer {_token}": + raise HTTPException(status_code=401, detail="Unauthorized") + + +def start_server() -> str: + """Initialize global state and return the API token.""" + global _pm, _token + _pm = PasswordManager() + _token = secrets.token_urlsafe(16) + print(f"API token: {_token}") + origins = [ + o.strip() + for o in os.getenv("SEEDPASS_CORS_ORIGINS", "").split(",") + if o.strip() + ] + if origins and app.middleware_stack is None: + app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_methods=["*"], + allow_headers=["*"], + ) + return _token + + +@app.get("/api/v1/entry") +def search_entry(query: str, authorization: str | None = Header(None)) -> List[Any]: + _check_token(authorization) + assert _pm is not None + results = _pm.entry_manager.search_entries(query) + return [ + { + "id": idx, + "label": label, + "username": username, + "url": url, + "archived": archived, + } + for idx, label, username, url, archived in results + ] + + +@app.get("/api/v1/entry/{entry_id}") +def get_entry(entry_id: int, authorization: str | None = Header(None)) -> Any: + _check_token(authorization) + assert _pm is not None + entry = _pm.entry_manager.retrieve_entry(entry_id) + if entry is None: + raise HTTPException(status_code=404, detail="Not found") + return entry + + +@app.get("/api/v1/config/{key}") +def get_config(key: str, authorization: str | None = Header(None)) -> Any: + _check_token(authorization) + assert _pm is not None + value = _pm.config_manager.load_config(require_pin=False).get(key) + if value is None: + raise HTTPException(status_code=404, detail="Not found") + return {"key": key, "value": value} + + +@app.get("/api/v1/fingerprint") +def list_fingerprints(authorization: str | None = Header(None)) -> List[str]: + _check_token(authorization) + assert _pm is not None + return _pm.fingerprint_manager.list_fingerprints() + + +@app.get("/api/v1/nostr/pubkey") +def get_nostr_pubkey(authorization: str | None = Header(None)) -> Any: + _check_token(authorization) + assert _pm is not None + return {"npub": _pm.nostr_client.key_manager.get_npub()} + + +@app.post("/api/v1/shutdown") +async def shutdown_server(authorization: str | None = Header(None)) -> dict[str, str]: + _check_token(authorization) + asyncio.get_event_loop().call_soon(sys.exit, 0) + return {"status": "shutting down"} diff --git a/src/seedpass/cli.py b/src/seedpass/cli.py index 1b4a44a..b7718fc 100644 --- a/src/seedpass/cli.py +++ b/src/seedpass/cli.py @@ -5,6 +5,8 @@ import typer from password_manager.manager import PasswordManager from password_manager.entry_types import EntryType +import uvicorn +from . import api as api_module app = typer.Typer(help="SeedPass command line interface") @@ -23,6 +25,7 @@ nostr_app = typer.Typer(help="Interact with Nostr relays") config_app = typer.Typer(help="Manage configuration values") fingerprint_app = typer.Typer(help="Manage seed profiles") util_app = typer.Typer(help="Utility commands") +api_app = typer.Typer(help="Run the API server") app.add_typer(entry_app, name="entry") app.add_typer(vault_app, name="vault") @@ -30,6 +33,7 @@ app.add_typer(nostr_app, name="nostr") app.add_typer(config_app, name="config") app.add_typer(fingerprint_app, name="fingerprint") app.add_typer(util_app, name="util") +app.add_typer(api_app, name="api") def _get_pm(ctx: typer.Context) -> PasswordManager: @@ -169,3 +173,26 @@ def fingerprint_list(ctx: typer.Context) -> None: def generate_password(ctx: typer.Context, length: int = 24) -> None: """Generate a strong password.""" typer.echo(f"Generate password of length {length} for {ctx.obj.get('fingerprint')}") + + +@api_app.command("start") +def api_start(host: str = "127.0.0.1", port: int = 8000) -> None: + """Start the SeedPass API server.""" + token = api_module.start_server() + typer.echo(f"API token: {token}") + uvicorn.run(api_module.app, host=host, port=port) + + +@api_app.command("stop") +def api_stop(host: str = "127.0.0.1", port: int = 8000) -> None: + """Stop the SeedPass API server.""" + import requests + + try: + requests.post( + f"http://{host}:{port}/api/v1/shutdown", + headers={"Authorization": f"Bearer {api_module._token}"}, + timeout=2, + ) + except Exception as exc: # pragma: no cover - best effort + typer.echo(f"Failed to stop server: {exc}") diff --git a/src/tests/test_api.py b/src/tests/test_api.py new file mode 100644 index 0000000..33cf4b6 --- /dev/null +++ b/src/tests/test_api.py @@ -0,0 +1,50 @@ +from types import SimpleNamespace +from pathlib import Path +import sys + +import pytest +from fastapi.testclient import TestClient + +sys.path.append(str(Path(__file__).resolve().parents[1])) + +from seedpass import api + + +@pytest.fixture +def client(monkeypatch): + dummy = SimpleNamespace( + entry_manager=SimpleNamespace( + search_entries=lambda q: [(1, "Site", "user", "url", False)], + retrieve_entry=lambda i: {"label": "Site"}, + ), + config_manager=SimpleNamespace( + load_config=lambda require_pin=False: {"k": "v"} + ), + fingerprint_manager=SimpleNamespace(list_fingerprints=lambda: ["fp"]), + nostr_client=SimpleNamespace( + key_manager=SimpleNamespace(get_npub=lambda: "np") + ), + ) + monkeypatch.setattr(api, "PasswordManager", lambda: dummy) + monkeypatch.setenv("SEEDPASS_CORS_ORIGINS", "http://example.com") + token = api.start_server() + client = TestClient(api.app) + return client, token + + +def test_cors_and_auth(client): + cl, token = client + headers = {"Authorization": f"Bearer {token}", "Origin": "http://example.com"} + res = cl.get("/api/v1/entry", params={"query": "s"}, headers=headers) + assert res.status_code == 200 + assert res.headers.get("access-control-allow-origin") == "http://example.com" + + +def test_invalid_token(client): + cl, _token = client + res = cl.get( + "/api/v1/entry", + params={"query": "s"}, + headers={"Authorization": "Bearer bad"}, + ) + assert res.status_code == 401 From 6ed984c24e0a8b21a487f7e0aacf3f7bbeec63a8 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 9 Jul 2025 09:35:45 -0400 Subject: [PATCH 04/30] Add httpx to requirements for API tests --- requirements.lock | 1 + src/requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements.lock b/requirements.lock index 7d79820..0318410 100644 --- a/requirements.lock +++ b/requirements.lock @@ -59,6 +59,7 @@ toml==0.10.2 tomli==2.2.1 urllib3==2.5.0 uvicorn==0.35.0 +httpx==0.28.1 varint==1.0.2 websocket-client==1.7.0 websockets==15.0.1 diff --git a/src/requirements.txt b/src/requirements.txt index 14018ab..958b56a 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -27,3 +27,4 @@ qrcode>=8.2 typer>=0.12.3 fastapi>=0.116.0 uvicorn>=0.35.0 +httpx>=0.28.1 From a33f67781b09f707cbaf466df01f76021a54fddd Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 9 Jul 2025 09:51:01 -0400 Subject: [PATCH 05/30] docs: update CLI and API usage --- docs/README.md | 4 +++- docs/advanced_cli.md | 54 ++++++++++++++++++++------------------------ 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/docs/README.md b/docs/README.md index bd25730..000d23d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,4 +14,6 @@ $ seedpass totp "email" Code: 123456 ``` -See [advanced_cli.md](advanced_cli.md) (future feature set) for details on the upcoming advanced CLI. +## CLI and API Reference + +See [advanced_cli.md](advanced_cli.md) for a list of command examples and instructions on running the local API. When starting the API, set `SEEDPASS_CORS_ORIGINS` if you need to allow requests from specific web origins. diff --git a/docs/advanced_cli.md b/docs/advanced_cli.md index 0ed3070..193de5b 100644 --- a/docs/advanced_cli.md +++ b/docs/advanced_cli.md @@ -6,7 +6,7 @@ Welcome to the **Advanced CLI and API Documentation** for **SeedPass**, a secure SeedPass uses a `noun-verb` command structure (e.g., `seedpass entry get `) for a clear, scalable, and discoverable interface. You can explore the available actions for any command group with the `--help` flag (for example, `seedpass entry --help`). -> **Note:** These commands describe planned functionality. The advanced CLI is not yet part of the stable release but will follow the current SeedPass design of fingerprint-based profiles and a local API for secure integrations. +The commands in this document reflect the Typer-based CLI shipped with SeedPass. Each command accepts the optional `--fingerprint` flag to operate on a specific seed profile. --- @@ -20,8 +20,9 @@ SeedPass uses a `noun-verb` command structure (e.g., `seedpass entry get - [Config Commands](#config-commands) - [Fingerprint Commands](#fingerprint-commands) - [Utility Commands](#utility-commands) + - [API Commands](#api-commands) 3. [Detailed Command Descriptions](#detailed-command-descriptions) -4. [Planned API Integration](#planned-api-integration) +4. [API Integration](#api-integration) 5. [Usage Guidelines](#usage-guidelines) --- @@ -45,12 +46,9 @@ Manage individual entries within a vault. | Action | Command | Examples | | :--- | :--- | :--- | -| Add a new entry | `entry add` | `seedpass entry add --type password --label "GitHub" --username "user" --length 20` | -| Retrieve an entry's secret | `entry get` | `seedpass entry get "GitHub"` | | List entries | `entry list` | `seedpass entry list --sort label` | | Search for entries | `entry search` | `seedpass entry search "GitHub"` | -| Modify an entry | `entry modify` | `seedpass entry modify "GitHub" --notes "New note"` | -| Delete an entry | `entry delete` | `seedpass entry delete "GitHub"` | +| Retrieve an entry's secret | `entry get` | `seedpass entry get "GitHub"` | ### Vault Commands @@ -59,8 +57,6 @@ Manage the entire vault for a profile. | Action | Command | Examples | | :--- | :--- | :--- | | Export the vault | `vault export` | `seedpass vault export --file backup.json` | -| Import a vault | `vault import` | `seedpass vault import --file backup.json` | -| Change master password | `vault changepw` | `seedpass vault changepw` | ### Nostr Commands @@ -70,7 +66,6 @@ Interact with the Nostr network for backup and synchronization. | :--- | :--- | :--- | | Sync with relays | `nostr sync` | `seedpass nostr sync` | | Get public key | `nostr get-pubkey` | `seedpass nostr get-pubkey` | -| Manage relays | `nostr relays` | `seedpass nostr relays --add wss://relay.example.com` | ### Config Commands @@ -79,7 +74,6 @@ Manage profile‑specific settings. | Action | Command | Examples | | :--- | :--- | :--- | | Get a setting value | `config get` | `seedpass config get inactivity_timeout` | -| Set a setting value | `config set` | `seedpass config set secret_mode true` | ### Fingerprint Commands @@ -87,10 +81,7 @@ Manage seed profiles (fingerprints). | Action | Command | Examples | | :--- | :--- | :--- | -| Add a new profile | `fingerprint add` | `seedpass fingerprint add` | | List all profiles | `fingerprint list` | `seedpass fingerprint list` | -| Remove a profile | `fingerprint remove` | `seedpass fingerprint remove ` | -| Set active profile | `fingerprint use` | `seedpass fingerprint use ` | ### Utility Commands @@ -99,7 +90,15 @@ Miscellaneous helper commands. | Action | Command | Examples | | :--- | :--- | :--- | | Generate a password | `util generate-password` | `seedpass util generate-password --length 24` | -| Verify script checksum | `util verify-checksum` | `seedpass util verify-checksum` | + +### API Commands + +Run or stop the local HTTP API. + +| Action | Command | Examples | +| :--- | :--- | :--- | +| Start the API | `api start` | `seedpass api start --host 0.0.0.0 --port 8000` | +| Stop the API | `api stop` | `seedpass api stop` | --- @@ -107,47 +106,44 @@ Miscellaneous helper commands. ### `entry` Commands -- **`seedpass entry add`** – Add a new entry. Use `--type` to specify `password`, `totp`, `ssh`, `pgp`, `nostr`, `key-value`, or `managed-account`. Include `--tags tag1,tag2` to categorize the entry. -- **`seedpass entry get `** – Retrieve the primary secret for one matching entry. - **`seedpass entry list`** – List entries in the vault, optionally sorted or filtered. -- **`seedpass entry search `** – Search across labels, usernames, URLs, notes, and tags. -- **`seedpass entry modify `** – Update fields on an existing entry. Use `--archive` to hide or `--restore` to un‑archive. Specify `--tags tag1,tag2` to replace the entry's tags. -- **`seedpass entry delete `** – Permanently delete an entry after confirmation. +- **`seedpass entry search `** – Search across labels, usernames, URLs and notes. +- **`seedpass entry get `** – Retrieve the primary secret for one matching entry. ### `vault` Commands - **`seedpass vault export`** – Export the entire vault to an encrypted JSON file. -- **`seedpass vault import`** – Import entries from an exported file, replacing the current vault after creating a backup. -- **`seedpass vault changepw`** – Interactively change the master password for the current profile. ### `nostr` Commands - **`seedpass nostr sync`** – Perform a two‑way sync with configured Nostr relays. - **`seedpass nostr get-pubkey`** – Display the Nostr public key for the active profile. -- **`seedpass nostr relays`** – Manage the relay list (`--list`, `--add`, `--remove`, `--reset`). ### `config` Commands - **`seedpass config get `** – Retrieve a configuration value such as `inactivity_timeout`, `secret_mode`, or `auto_sync`. -- **`seedpass config set `** – Set a configuration value for the active profile. ### `fingerprint` Commands -- **`seedpass fingerprint add`** – Add a new seed profile (interactive or via `--import-seed`). - **`seedpass fingerprint list`** – List available profiles by fingerprint. -- **`seedpass fingerprint remove `** – Delete a profile and its data after confirmation. -- **`seedpass fingerprint use `** – Make the given fingerprint active in the current shell session. ### `util` Commands - **`seedpass util generate-password`** – Generate a strong password of the requested length. -- **`seedpass util verify-checksum`** – Verify the program checksum for integrity. --- -## Planned API Integration +## API Integration -The advanced CLI will act as a client for a locally hosted REST API. Starting the API loads the vault into memory after prompting for the master password and prints a temporary API key. Third‑party clients include this key in the `Authorization` header when calling endpoints such as `GET /api/v1/entry?query=GitHub`. The server automatically shuts down after a period of inactivity or when `seedpass api stop` is run. +SeedPass provides a small REST API for automation. Run `seedpass api start` to launch the server. The command prints a one‑time token which clients must include in the `Authorization` header. + +Set the `SEEDPASS_CORS_ORIGINS` environment variable to a comma‑separated list of allowed origins when you need cross‑origin requests: + +```bash +SEEDPASS_CORS_ORIGINS=http://localhost:3000 seedpass api start +``` + +Shut down the server with `seedpass api stop`. --- From 33f83a5a543308275da3239a90e29d3b0ca83683 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 9 Jul 2025 10:07:40 -0400 Subject: [PATCH 06/30] Document installing test dependencies --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d2b591b..108af09 100644 --- a/README.md +++ b/README.md @@ -379,7 +379,7 @@ Back in the Settings menu you can: ## Running Tests -SeedPass includes a small suite of unit tests located under `src/tests`. After activating your virtual environment and installing dependencies, run the tests with **pytest**. Use `-vv` to see INFO-level log messages from each passing test: +SeedPass includes a small suite of unit tests located under `src/tests`. **Before running `pytest`, be sure to install the test requirements.** Activate your virtual environment and run `pip install -r src/requirements.txt` to ensure all testing dependencies are available. Then run the tests with **pytest**. Use `-vv` to see INFO-level log messages from each passing test: ```bash From 3a1663ad076785b8549c02b9749e51c131ba7aa8 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 9 Jul 2025 10:29:44 -0400 Subject: [PATCH 07/30] Update CLI commands and add unit tests --- src/seedpass/cli.py | 11 +++++++++-- src/tests/test_typer_cli.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/seedpass/cli.py b/src/seedpass/cli.py index b7718fc..2d13342 100644 --- a/src/seedpass/cli.py +++ b/src/seedpass/cli.py @@ -139,7 +139,12 @@ def vault_export( @nostr_app.command("sync") def nostr_sync(ctx: typer.Context) -> None: """Sync with configured Nostr relays.""" - typer.echo(f"Syncing vault for fingerprint: {ctx.obj.get('fingerprint')}") + pm = _get_pm(ctx) + event_id = pm.sync_vault() + if event_id: + typer.echo(event_id) + else: + typer.echo("Error: Failed to sync vault") @nostr_app.command("get-pubkey") @@ -172,7 +177,9 @@ def fingerprint_list(ctx: typer.Context) -> None: @util_app.command("generate-password") def generate_password(ctx: typer.Context, length: int = 24) -> None: """Generate a strong password.""" - typer.echo(f"Generate password of length {length} for {ctx.obj.get('fingerprint')}") + pm = _get_pm(ctx) + password = pm.password_generator.generate_password(length) + typer.echo(password) @api_app.command("start") diff --git a/src/tests/test_typer_cli.py b/src/tests/test_typer_cli.py index d4df89f..aa7139c 100644 --- a/src/tests/test_typer_cli.py +++ b/src/tests/test_typer_cli.py @@ -1,6 +1,9 @@ +import sys from types import SimpleNamespace from pathlib import Path +sys.path.append(str(Path(__file__).resolve().parents[1])) + from typer.testing import CliRunner from seedpass.cli import app, PasswordManager @@ -113,3 +116,36 @@ def test_config_get(monkeypatch): result = runner.invoke(app, ["config", "get", "x"]) assert result.exit_code == 0 assert "1" in result.stdout + + +def test_nostr_sync(monkeypatch): + called = {} + + def sync_vault(): + called["called"] = True + return "evt123" + + pm = SimpleNamespace(sync_vault=sync_vault, select_fingerprint=lambda fp: None) + monkeypatch.setattr(cli, "PasswordManager", lambda: pm) + result = runner.invoke(app, ["nostr", "sync"]) + assert result.exit_code == 0 + assert called.get("called") is True + assert "evt123" in result.stdout + + +def test_generate_password(monkeypatch): + called = {} + + def gen_pw(length): + called["length"] = length + return "secretpw" + + pm = SimpleNamespace( + password_generator=SimpleNamespace(generate_password=gen_pw), + select_fingerprint=lambda fp: None, + ) + monkeypatch.setattr(cli, "PasswordManager", lambda: pm) + result = runner.invoke(app, ["util", "generate-password", "--length", "12"]) + assert result.exit_code == 0 + assert called.get("length") == 12 + assert "secretpw" in result.stdout From 39f49b27fe8e686d2a847053d391f599617888c0 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 9 Jul 2025 10:49:02 -0400 Subject: [PATCH 08/30] Allow selecting fingerprint for API server --- src/seedpass/api.py | 12 ++++++++++-- src/seedpass/cli.py | 6 +++--- src/tests/test_typer_cli.py | 16 ++++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/seedpass/api.py b/src/seedpass/api.py index fbe1537..a2bd4ee 100644 --- a/src/seedpass/api.py +++ b/src/seedpass/api.py @@ -25,10 +25,18 @@ def _check_token(auth: str | None) -> None: raise HTTPException(status_code=401, detail="Unauthorized") -def start_server() -> str: - """Initialize global state and return the API token.""" +def start_server(fingerprint: str | None = None) -> str: + """Initialize global state and return the API token. + + Parameters + ---------- + fingerprint: + Optional seed profile fingerprint to select before starting the server. + """ global _pm, _token _pm = PasswordManager() + if fingerprint: + _pm.select_fingerprint(fingerprint) _token = secrets.token_urlsafe(16) print(f"API token: {_token}") origins = [ diff --git a/src/seedpass/cli.py b/src/seedpass/cli.py index 2d13342..2d43115 100644 --- a/src/seedpass/cli.py +++ b/src/seedpass/cli.py @@ -183,15 +183,15 @@ def generate_password(ctx: typer.Context, length: int = 24) -> None: @api_app.command("start") -def api_start(host: str = "127.0.0.1", port: int = 8000) -> None: +def api_start(ctx: typer.Context, host: str = "127.0.0.1", port: int = 8000) -> None: """Start the SeedPass API server.""" - token = api_module.start_server() + token = api_module.start_server(ctx.obj.get("fingerprint")) typer.echo(f"API token: {token}") uvicorn.run(api_module.app, host=host, port=port) @api_app.command("stop") -def api_stop(host: str = "127.0.0.1", port: int = 8000) -> None: +def api_stop(ctx: typer.Context, host: str = "127.0.0.1", port: int = 8000) -> None: """Stop the SeedPass API server.""" import requests diff --git a/src/tests/test_typer_cli.py b/src/tests/test_typer_cli.py index aa7139c..b3f7d72 100644 --- a/src/tests/test_typer_cli.py +++ b/src/tests/test_typer_cli.py @@ -149,3 +149,19 @@ def test_generate_password(monkeypatch): assert result.exit_code == 0 assert called.get("length") == 12 assert "secretpw" in result.stdout + + +def test_api_start_passes_fingerprint(monkeypatch): + """Ensure the API start command forwards the selected fingerprint.""" + called = {} + + def fake_start(fp=None): + called["fp"] = fp + return "tok" + + monkeypatch.setattr(cli.api_module, "start_server", fake_start) + monkeypatch.setattr(cli, "uvicorn", SimpleNamespace(run=lambda *a, **k: None)) + + result = runner.invoke(app, ["--fingerprint", "abc", "api", "start"]) + assert result.exit_code == 0 + assert called.get("fp") == "abc" From 0fa47a1da867366dd9b902b01df204b41db0c287 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 9 Jul 2025 11:01:32 -0400 Subject: [PATCH 09/30] Add requests dependency for API stop --- src/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/requirements.txt b/src/requirements.txt index 958b56a..3a8ee6e 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -28,3 +28,4 @@ typer>=0.12.3 fastapi>=0.116.0 uvicorn>=0.35.0 httpx>=0.28.1 +requests>=2.32 From c9685b91bfd2946d63199c66357a177c763aa6d0 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 9 Jul 2025 11:13:35 -0400 Subject: [PATCH 10/30] test: expand api coverage --- src/tests/test_api.py | 89 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/src/tests/test_api.py b/src/tests/test_api.py index 33cf4b6..bc8e488 100644 --- a/src/tests/test_api.py +++ b/src/tests/test_api.py @@ -48,3 +48,92 @@ def test_invalid_token(client): headers={"Authorization": "Bearer bad"}, ) assert res.status_code == 401 + + +def test_get_entry_by_id(client): + cl, token = client + headers = { + "Authorization": f"Bearer {token}", + "Origin": "http://example.com", + } + res = cl.get("/api/v1/entry/1", headers=headers) + assert res.status_code == 200 + assert res.json() == {"label": "Site"} + assert res.headers.get("access-control-allow-origin") == "http://example.com" + + +def test_get_config_value(client): + cl, token = client + headers = { + "Authorization": f"Bearer {token}", + "Origin": "http://example.com", + } + res = cl.get("/api/v1/config/k", headers=headers) + assert res.status_code == 200 + assert res.json() == {"key": "k", "value": "v"} + assert res.headers.get("access-control-allow-origin") == "http://example.com" + + +def test_list_fingerprint(client): + cl, token = client + headers = { + "Authorization": f"Bearer {token}", + "Origin": "http://example.com", + } + res = cl.get("/api/v1/fingerprint", headers=headers) + assert res.status_code == 200 + assert res.json() == ["fp"] + assert res.headers.get("access-control-allow-origin") == "http://example.com" + + +def test_get_nostr_pubkey(client): + cl, token = client + headers = { + "Authorization": f"Bearer {token}", + "Origin": "http://example.com", + } + res = cl.get("/api/v1/nostr/pubkey", headers=headers) + assert res.status_code == 200 + assert res.json() == {"npub": "np"} + assert res.headers.get("access-control-allow-origin") == "http://example.com" + + +def test_shutdown(client, monkeypatch): + cl, token = client + + calls = {} + + class Loop: + def call_soon(self, func, *args): + calls["func"] = func + calls["args"] = args + + monkeypatch.setattr(api.asyncio, "get_event_loop", lambda: Loop()) + + headers = { + "Authorization": f"Bearer {token}", + "Origin": "http://example.com", + } + res = cl.post("/api/v1/shutdown", headers=headers) + assert res.status_code == 200 + assert res.json() == {"status": "shutting down"} + assert calls["func"] is sys.exit + assert calls["args"] == (0,) + assert res.headers.get("access-control-allow-origin") == "http://example.com" + + +@pytest.mark.parametrize( + "method,path", + [ + ("get", "/api/v1/entry/1"), + ("get", "/api/v1/config/k"), + ("get", "/api/v1/fingerprint"), + ("get", "/api/v1/nostr/pubkey"), + ("post", "/api/v1/shutdown"), + ], +) +def test_invalid_token_other_endpoints(client, method, path): + cl, _token = client + req = getattr(cl, method) + res = req(path, headers={"Authorization": "Bearer bad"}) + assert res.status_code == 401 From 1aa9447df2d641da5ffe58bc3e0c3475d2f4c02b Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 9 Jul 2025 11:45:47 -0400 Subject: [PATCH 11/30] Add REST API documentation --- README.md | 2 +- docs/README.md | 2 +- docs/api_reference.md | 50 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 docs/api_reference.md diff --git a/README.md b/README.md index 108af09..9719589 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ You can also use the new Typer-based CLI: ```bash seedpass --help ``` -For details see [docs/advanced_cli.md](docs/advanced_cli.md). +For a full list of commands see [docs/advanced_cli.md](docs/advanced_cli.md). The REST API is described in [docs/api_reference.md](docs/api_reference.md). ### Running the Application diff --git a/docs/README.md b/docs/README.md index 000d23d..eb149ff 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,4 +16,4 @@ Code: 123456 ## CLI and API Reference -See [advanced_cli.md](advanced_cli.md) for a list of command examples and instructions on running the local API. When starting the API, set `SEEDPASS_CORS_ORIGINS` if you need to allow requests from specific web origins. +See [advanced_cli.md](advanced_cli.md) for a list of command examples. Detailed information about the REST API is available in [api_reference.md](api_reference.md). When starting the API, set `SEEDPASS_CORS_ORIGINS` if you need to allow requests from specific web origins. diff --git a/docs/api_reference.md b/docs/api_reference.md new file mode 100644 index 0000000..5302d51 --- /dev/null +++ b/docs/api_reference.md @@ -0,0 +1,50 @@ +# SeedPass REST API Reference + +This guide covers how to start the SeedPass API, authenticate requests, and interact with the available endpoints. + +## Starting the API + +Run `seedpass api start` from your terminal. The command prints a one‑time token used for authentication: + +```bash +$ seedpass api start +API token: abcdef1234567890 +``` + +Keep this token secret. Every request must include it in the `Authorization` header using the `Bearer` scheme. + +## Endpoints + +- `GET /api/v1/entry?query=` – Search entries matching a query. +- `GET /api/v1/entry/{id}` – Retrieve a single entry by its index. +- `GET /api/v1/config/{key}` – Return the value for a configuration key. +- `GET /api/v1/fingerprint` – List available seed fingerprints. +- `GET /api/v1/nostr/pubkey` – Fetch the Nostr public key for the active seed. +- `POST /api/v1/shutdown` – Stop the server gracefully. + +## Example Requests + +Send requests with the token in the header: + +```bash +curl -H "Authorization: Bearer " \ + "http://127.0.0.1:8000/api/v1/entry?query=email" +``` + +### Enabling CORS + +Cross‑origin requests are disabled by default. Set `SEEDPASS_CORS_ORIGINS` to a comma‑separated list of allowed origins before starting the API: + +```bash +SEEDPASS_CORS_ORIGINS=http://localhost:3000 seedpass api start +``` + +Browsers can then call the API from the specified origins, for example using JavaScript: + +```javascript +fetch('http://127.0.0.1:8000/api/v1/entry?query=email', { + headers: { Authorization: 'Bearer ' } +}); +``` + +Without CORS enabled, only same‑origin or command‑line tools like `curl` can access the API. From aca1012aedf52daf417ccb3f37d2ebc2ad7f0848 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:07:57 -0400 Subject: [PATCH 12/30] Update README usage docs --- README.md | 6 +++++- docs/README.md | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9719589..60e8580 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,8 @@ seedpass import --file "~/seedpass_backup.json" seedpass search "github" seedpass search --tags "work,personal" seedpass get "github" -seedpass totp "email" +# Retrieve a TOTP entry +seedpass entry get "email" # The code is printed and copied to your clipboard # Sort or filter the list view @@ -186,6 +187,9 @@ seedpass list --filter totp # on an external drive. ``` +For additional command examples, see [docs/advanced_cli.md](docs/advanced_cli.md). +Details on the REST API can be found in [docs/api_reference.md](docs/api_reference.md). + ### Vault JSON Layout The encrypted index file `seedpass_entries_db.json.enc` begins with `schema_version` `2` and stores an `entries` map keyed by entry numbers. diff --git a/docs/README.md b/docs/README.md index eb149ff..bf93cc0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,12 +4,12 @@ This directory contains supplementary guides for using SeedPass. ## Quick Example: Get a TOTP Code -Run `seedpass totp ` to retrieve a time-based one-time password (TOTP). The -`` can be a label, title, or index. A progress bar shows the remaining +Run `seedpass entry get ` to retrieve a time-based one-time password (TOTP). +The `` can be a label, title, or index. A progress bar shows the remaining seconds in the current period. ```bash -$ seedpass totp "email" +$ seedpass entry get "email" [##########----------] 15s Code: 123456 ``` From 53012f466181902b7734edeca2bcd216fdec8eed Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:16:25 -0400 Subject: [PATCH 13/30] Clarify entry get behavior --- docs/advanced_cli.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/advanced_cli.md b/docs/advanced_cli.md index 193de5b..1248f55 100644 --- a/docs/advanced_cli.md +++ b/docs/advanced_cli.md @@ -48,7 +48,7 @@ Manage individual entries within a vault. | :--- | :--- | :--- | | List entries | `entry list` | `seedpass entry list --sort label` | | Search for entries | `entry search` | `seedpass entry search "GitHub"` | -| Retrieve an entry's secret | `entry get` | `seedpass entry get "GitHub"` | +| Retrieve an entry's secret (password or TOTP code) | `entry get` | `seedpass entry get "GitHub"` | ### Vault Commands @@ -108,7 +108,15 @@ Run or stop the local HTTP API. - **`seedpass entry list`** – List entries in the vault, optionally sorted or filtered. - **`seedpass entry search `** – Search across labels, usernames, URLs and notes. -- **`seedpass entry get `** – Retrieve the primary secret for one matching entry. +- **`seedpass entry get `** – Retrieve the password or TOTP code for one matching entry, depending on the entry's type. + +Example retrieving a TOTP code: + +```bash +$ seedpass entry get "email" +[##########----------] 15s +Code: 123456 +``` ### `vault` Commands From cf2009fd9abc77d382fce15c01919807b9977912 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:46:05 -0400 Subject: [PATCH 14/30] Add entry management CLI and API --- docs/advanced_cli.md | 8 ++++ docs/api_reference.md | 4 ++ src/seedpass/api.py | 58 ++++++++++++++++++++++++++ src/seedpass/cli.py | 46 +++++++++++++++++++++ src/tests/test_api.py | 42 ++++++++++++++++++- src/tests/test_typer_cli.py | 81 +++++++++++++++++++++++++++++++++++++ 6 files changed, 238 insertions(+), 1 deletion(-) diff --git a/docs/advanced_cli.md b/docs/advanced_cli.md index 1248f55..bc1087f 100644 --- a/docs/advanced_cli.md +++ b/docs/advanced_cli.md @@ -49,6 +49,10 @@ Manage individual entries within a vault. | List entries | `entry list` | `seedpass entry list --sort label` | | Search for entries | `entry search` | `seedpass entry search "GitHub"` | | Retrieve an entry's secret (password or TOTP code) | `entry get` | `seedpass entry get "GitHub"` | +| Add a password entry | `entry add` | `seedpass entry add Example --length 16` | +| Modify an entry | `entry modify` | `seedpass entry modify 1 --username alice` | +| Archive an entry | `entry archive` | `seedpass entry archive 1` | +| Unarchive an entry | `entry unarchive` | `seedpass entry unarchive 1` | ### Vault Commands @@ -109,6 +113,10 @@ Run or stop the local HTTP API. - **`seedpass entry list`** – List entries in the vault, optionally sorted or filtered. - **`seedpass entry search `** – Search across labels, usernames, URLs and notes. - **`seedpass entry get `** – Retrieve the password or TOTP code for one matching entry, depending on the entry's type. +- **`seedpass entry add