diff --git a/README.md b/README.md index c899e3d..f2f8657 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ rh-envault store list rh-envault store list --prefix /production/ rh-envault store get DB_PASSWORD --store my-vault rh-envault store set DB_PASSWORD new_value --store my-vault +rh-envault store delete DB_PASSWORD --store my-vault ``` ### `rh-envault audit` diff --git a/src/envault/cli.py b/src/envault/cli.py index b3c4b7d..ab2c0b5 100644 --- a/src/envault/cli.py +++ b/src/envault/cli.py @@ -351,6 +351,27 @@ def store_set( console.print(f"[green]✓[/green] Set {key}") +@store_app.command("delete") +def store_delete( + key: str = typer.Argument(..., help="Key to delete"), + store_name: str | None = typer.Option(None, "--store", "-s", help="Store name from config"), + config_path: str = typer.Option("", "--config", "-c", help="Config file path"), +): + """Delete a secret from a secret store.""" + config = load_config(config_path) + + if store_name and store_name in config.stores: + store_instance = get_store(config.stores[store_name]) + else: + store_instance = get_store(config_path) + + if store_instance.delete(key): + console.print(f"[green]✓[/green] Deleted {key}") + else: + err_console.print(f"[red]Error:[/red] Key '{key}' not found in store") + raise typer.Exit(1) + + # ── Encrypt / Decrypt ────────────────────────────────────────────────────────── @app.command() diff --git a/src/envault/encrypt.py b/src/envault/encrypt.py index af1c9b1..2b4a512 100644 --- a/src/envault/encrypt.py +++ b/src/envault/encrypt.py @@ -5,8 +5,8 @@ envault decrypt .env.locked # Decrypt .env.locked -> .env The encryption key is derived from a master password via PBKDF2. -Key can also be stored in REVENUEHOLDINGS_LICENSE_KEY env var or -passed via --key flag for CI/CD. +Key can also be stored in ENVAULT_ENCRYPT_KEY env var or +passed via --password flag for CI/CD. """ from __future__ import annotations diff --git a/tests/test_envault.py b/tests/test_envault.py index 7818af7..6be545a 100644 --- a/tests/test_envault.py +++ b/tests/test_envault.py @@ -447,6 +447,80 @@ def test_store_factory_unknown(): get_store(config) +# ── Store Delete CLI Tests ────────────────────────────────────────────────── + + +def test_cli_store_delete_ok(tmp_path): + """store delete exits 0 and removes the key via config'd store.""" + from typer.testing import CliRunner + from envault.cli import app + import yaml + + # Create valid .envault.yml with a local store pointing at our env file + env_file = tmp_path / ".env" + env_file.write_text("MY_KEY=my_value\nOTHER=keep\n") + + envault_config = { + "project": "test", + "stores": { + "local": { + "type": "local", + "path_prefix": str(env_file), + } + } + } + config_path = tmp_path / ".envault.yml" + with open(config_path, "w") as f: + yaml.dump(envault_config, f) + + runner = CliRunner() + result = runner.invoke(app, [ + "store", "delete", "MY_KEY", + "--store", "local", + "-c", str(config_path), + ]) + assert result.exit_code == 0, f"stdout: {result.stdout}" + assert "Deleted" in result.stdout + + # Verify key was actually deleted + from envault.stores import LocalEnvStore + store = LocalEnvStore(str(env_file)) + assert store.get("MY_KEY") is None + assert store.get("OTHER") == "keep" + + +def test_cli_store_delete_not_found(tmp_path): + """store delete exits 1 when key doesn't exist.""" + from typer.testing import CliRunner + from envault.cli import app + import yaml + + env_file = tmp_path / ".env" + env_file.write_text("OTHER=value\n") + + envault_config = { + "project": "test", + "stores": { + "local": { + "type": "local", + "path_prefix": str(env_file), + } + } + } + config_path = tmp_path / ".envault.yml" + with open(config_path, "w") as f: + yaml.dump(envault_config, f) + + runner = CliRunner() + result = runner.invoke(app, [ + "store", "delete", "NONEXISTENT", + "--store", "local", + "-c", str(config_path), + ]) + assert result.exit_code == 1 + assert "not found" in result.output.lower() or "Error" in result.output + + # ── Encrypt / Decrypt ───────────────────────────────────────────────────────