Add CLI subcommands and tests

This commit is contained in:
thePR0M3TH3AN
2025-07-03 16:09:13 -04:00
parent 8fd571e26f
commit 9a22d537a5
3 changed files with 175 additions and 38 deletions

View File

@@ -16,10 +16,12 @@ import traceback
from password_manager.manager import PasswordManager
from nostr.client import NostrClient
from password_manager.entry_types import EntryType
from constants import INACTIVITY_TIMEOUT, initialize_app
from utils.password_prompt import PasswordPromptError
from utils import timed_input
from local_bip85.bip85 import Bip85Error
import pyperclip
colorama_init()
@@ -233,6 +235,22 @@ def handle_display_stats(password_manager: PasswordManager) -> None:
print(colored(f"Error: Failed to display stats: {e}", "red"))
def print_matches(matches: list[tuple[int, str, str | None, str | None, bool]]) -> None:
"""Print a list of search matches."""
print(colored("\n[+] Matches:\n", "green"))
for entry in matches:
idx, website, username, url, blacklisted = entry
print(colored(f"Index: {idx}", "cyan"))
if website:
print(colored(f" Website: {website}", "cyan"))
if username:
print(colored(f" Username: {username}", "cyan"))
if url:
print(colored(f" URL: {url}", "cyan"))
print(colored(f" Blacklisted: {'Yes' if blacklisted else 'No'}", "cyan"))
print("-" * 40)
def handle_post_to_nostr(
password_manager: PasswordManager, alt_summary: str | None = None
):
@@ -702,15 +720,14 @@ def display_menu(
print(colored("Invalid choice. Please select a valid option.", "red"))
if __name__ == "__main__":
# Configure logging with both file and console handlers
def main(argv: list[str] | None = None) -> int:
"""Entry point for the SeedPass CLI."""
configure_logging()
initialize_app()
logger = logging.getLogger(__name__)
logger.info("Starting SeedPass Password Manager")
# Load config from disk and parse command-line arguments
cfg = load_global_config()
load_global_config()
parser = argparse.ArgumentParser()
sub = parser.add_subparsers(dest="command")
@@ -720,48 +737,96 @@ if __name__ == "__main__":
imp = sub.add_parser("import")
imp.add_argument("--file")
args = parser.parse_args()
search_p = sub.add_parser("search")
search_p.add_argument("query")
get_p = sub.add_parser("get")
get_p.add_argument("query")
totp_p = sub.add_parser("totp")
totp_p.add_argument("query")
args = parser.parse_args(argv)
# Initialize PasswordManager and proceed with application logic
try:
password_manager = PasswordManager()
logger.info("PasswordManager initialized successfully.")
except (PasswordPromptError, Bip85Error) as e:
logger.error(f"Failed to initialize PasswordManager: {e}", exc_info=True)
print(colored(f"Error: Failed to initialize PasswordManager: {e}", "red"))
sys.exit(1)
return 1
except Exception as e:
logger.error(f"Failed to initialize PasswordManager: {e}", exc_info=True)
print(colored(f"Error: Failed to initialize PasswordManager: {e}", "red"))
sys.exit(1)
return 1
if args.command == "export":
password_manager.handle_export_database(Path(args.file))
sys.exit(0)
elif args.command == "import":
return 0
if args.command == "import":
password_manager.handle_import_database(Path(args.file))
sys.exit(0)
return 0
if args.command == "search":
matches = password_manager.entry_manager.search_entries(args.query)
if matches:
print_matches(matches)
else:
print(colored("No matching entries found.", "yellow"))
return 0
if args.command == "get":
matches = password_manager.entry_manager.search_entries(args.query)
if len(matches) != 1:
if not matches:
print(colored("No matching entries found.", "yellow"))
else:
print_matches(matches)
return 1
idx = matches[0][0]
entry = password_manager.entry_manager.retrieve_entry(idx)
if entry.get("type", EntryType.PASSWORD.value) != EntryType.PASSWORD.value:
print(colored("Entry is not a password entry.", "red"))
return 1
length = int(entry.get("length", 0))
pw = password_manager.password_generator.generate_password(length, idx)
print(pw)
return 0
if args.command == "totp":
matches = password_manager.entry_manager.search_entries(args.query)
if len(matches) != 1:
if not matches:
print(colored("No matching entries found.", "yellow"))
else:
print_matches(matches)
return 1
idx = matches[0][0]
entry = password_manager.entry_manager.retrieve_entry(idx)
if entry.get("type") != EntryType.TOTP.value:
print(colored("Entry is not a TOTP entry.", "red"))
return 1
code = password_manager.entry_manager.get_totp_code(
idx, password_manager.parent_seed
)
print(code)
try:
pyperclip.copy(code)
except Exception as exc:
logging.warning(f"Clipboard copy failed: {exc}")
return 0
# Register signal handlers for graceful shutdown
def signal_handler(sig, frame):
"""
Handles termination signals to gracefully shutdown the NostrClient.
"""
def signal_handler(sig, _frame):
print(colored("\nReceived shutdown signal. Exiting gracefully...", "yellow"))
logging.info(f"Received shutdown signal: {sig}. Initiating graceful shutdown.")
try:
password_manager.nostr_client.close_client_pool() # Gracefully close the ClientPool
password_manager.nostr_client.close_client_pool()
logging.info("NostrClient closed successfully.")
except Exception as e:
logging.error(f"Error during shutdown: {e}")
print(colored(f"Error during shutdown: {e}", "red"))
except Exception as exc:
logging.error(f"Error during shutdown: {exc}")
print(colored(f"Error during shutdown: {exc}", "red"))
sys.exit(0)
# Register the signal handlers
signal.signal(signal.SIGINT, signal_handler) # Handle Ctrl+C
signal.signal(signal.SIGTERM, signal_handler) # Handle termination signals
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# Display the interactive menu to the user
try:
display_menu(
password_manager, inactivity_timeout=password_manager.inactivity_timeout
@@ -770,29 +835,34 @@ if __name__ == "__main__":
logger.info("Program terminated by user via KeyboardInterrupt.")
print(colored("\nProgram terminated by user.", "yellow"))
try:
password_manager.nostr_client.close_client_pool() # Gracefully close the ClientPool
password_manager.nostr_client.close_client_pool()
logging.info("NostrClient closed successfully.")
except Exception as e:
logging.error(f"Error during shutdown: {e}")
print(colored(f"Error during shutdown: {e}", "red"))
sys.exit(0)
except Exception as exc:
logging.error(f"Error during shutdown: {exc}")
print(colored(f"Error during shutdown: {exc}", "red"))
return 0
except (PasswordPromptError, Bip85Error) as e:
logger.error(f"A user-related error occurred: {e}", exc_info=True)
print(colored(f"Error: {e}", "red"))
try:
password_manager.nostr_client.close_client_pool()
logging.info("NostrClient closed successfully.")
except Exception as close_error:
logging.error(f"Error during shutdown: {close_error}")
print(colored(f"Error during shutdown: {close_error}", "red"))
sys.exit(1)
except Exception as exc:
logging.error(f"Error during shutdown: {exc}")
print(colored(f"Error during shutdown: {exc}", "red"))
return 1
except Exception as e:
logger.error(f"An unexpected error occurred: {e}", exc_info=True)
print(colored(f"Error: An unexpected error occurred: {e}", "red"))
try:
password_manager.nostr_client.close_client_pool() # Attempt to close the ClientPool
password_manager.nostr_client.close_client_pool()
logging.info("NostrClient closed successfully.")
except Exception as close_error:
logging.error(f"Error during shutdown: {close_error}")
print(colored(f"Error during shutdown: {close_error}", "red"))
sys.exit(1)
except Exception as exc:
logging.error(f"Error during shutdown: {exc}")
print(colored(f"Error during shutdown: {exc}", "red"))
return 1
return 0
if __name__ == "__main__":
sys.exit(main())