Plings Keygen Menu Implementation Plan
Plings Keygen Menu Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build an interactive menu program (scripts/plings_keygen.py) that wraps key/tag generation, master-key management, and URL validation so the operator never types long CLI commands.
Architecture: A thin questionary/rich TUI in scripts/plings_keygen.py calls a pure, testable core (scripts/plings_keygen_core.py) and reuses existing classes (HDPathKeyDerivation, load_master_key, MasterKeyGenerator, MasterKeyImporter). CSV writing moves to a shared helper (scripts/utils/output_utils.py) used by both the menu and derive_path_keys.py. No cryptographic logic is reimplemented. Execute this plan in a dedicated Plings-API git worktree (other sessions touch this repo).
Tech Stack: Python 3.10+, Poetry, pytest, questionary + rich (dev-only deps), existing crypto_utils/path_utils.
Spec: docs/specs/2026-06-13-plings-keygen-menu-design.md
File Structure
- Create
scripts/utils/output_utils.py—write_keys_csv(keys, path). Single CSV writer (cols:shortcode, decimal_path, base58_path, url). - Create
scripts/plings_keygen_core.py— pure, testable functions: filename builder, env inference, base58↔decimal path, history I/O, master-key discovery, URL parse + validate. - Create
scripts/plings_keygen.py— thequestionary/richTUI; orchestrates the core + existing classes. - Modify
scripts/derive_path_keys.py— usewrite_keys_csvinstead of inline CSV code (behaviour-preserving). - Modify
pyproject.toml— addquestionary,richto[tool.poetry.group.dev.dependencies]. - Modify
.gitignore— ignorekeys/.keygen_history.json. - Create
tests/test_output_utils.py,tests/test_keygen_core.py— unit + determinism regression.
All script imports rely on the existing sys.path insertion of scripts/utils (see top of derive_path_keys.py).
Task 1: Add dev dependencies and gitignore the history file
Files:
- Modify:
pyproject.toml([tool.poetry.group.dev.dependencies]) -
Modify:
.gitignore - Step 1: Add deps to the dev group
In pyproject.toml, under [tool.poetry.group.dev.dependencies] (create the section if absent), add:
questionary = "^2.0.1"
rich = "^13.7.0"
Do not add these to requirements.txt (Vercel deploy manifest must stay clean — this is a local-only tool).
- Step 2: Install
Run: poetry install
Expected: questionary + rich resolved into the dev environment.
- Step 3: Ignore the history file
Append to .gitignore:
# Local keygen menu state (not shared)
keys/.keygen_history.json
- Step 4: Commit
git add pyproject.toml poetry.lock .gitignore
git commit -m "chore(scripts): add questionary+rich dev deps; ignore keygen history"
Task 2: Extract the shared CSV writer
Files:
- Create:
scripts/utils/output_utils.py - Modify:
scripts/derive_path_keys.py(replace inline CSV block inmain()) -
Test:
tests/test_output_utils.py - Step 1: Write the failing test
# tests/test_output_utils.py
import csv, os, sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts', 'utils'))
from output_utils import write_keys_csv
def test_write_keys_csv_columns_and_decimal(tmp_path):
keys = [{
"short_code": "CCue",
"qr_url": "https://s.plings.io?t=q&i=CCue...&p=4.2.3.2.2",
"plings_path": "4.2.3.2.2", # base58 → decimal 3.1.2.1.1
}]
out = tmp_path / "x.csv"
write_keys_csv(keys, str(out))
rows = list(csv.reader(open(out, newline="", encoding="utf-8")))
assert rows[0] == ["shortcode", "decimal_path", "base58_path", "url"]
assert rows[1][0] == "CCue"
assert rows[1][1] == "3.1.2.1.1"
assert rows[1][2] == "4.2.3.2.2"
- Step 2: Run test to verify it fails
Run: poetry run pytest tests/test_output_utils.py -v
Expected: FAIL with ModuleNotFoundError: No module named 'output_utils'.
- Step 3: Create the writer
# scripts/utils/output_utils.py
#!/usr/bin/env python3
"""Shared output helpers for Plings key-generation scripts."""
import csv
from typing import Dict, List
from crypto_utils import CryptoUtils
def write_keys_csv(keys: List[Dict[str, str]], output_file: str) -> None:
"""Write derived keys to CSV: shortcode, decimal_path, base58_path, url."""
crypto_utils = CryptoUtils()
with open(output_file, "w", newline="", encoding="utf-8") as csvfile:
writer = csv.writer(csvfile)
writer.writerow(["shortcode", "decimal_path", "base58_path", "url"])
for key_data in keys:
base58_path = key_data.get("plings_path", "")
decimal_parts = []
for part in base58_path.split("."):
try:
decimal_parts.append(str(crypto_utils.decode_number_base58(part)))
except Exception:
decimal_parts.append(part)
writer.writerow([
key_data.get("short_code", ""),
".".join(decimal_parts),
base58_path,
key_data.get("qr_url", ""),
])
- Step 4: Run test to verify it passes
Run: poetry run pytest tests/test_output_utils.py -v
Expected: PASS.
- Step 5: Refactor derive_path_keys.py to use it
In scripts/derive_path_keys.py, add to the utils imports near the top:
from output_utils import write_keys_csv
Replace the CSV-writing block inside main() (the with open(args.csv_output, ...) loop that writes ['shortcode', 'decimal_path', 'base58_path', 'url']) with:
if args.csv_output:
try:
write_keys_csv(keys, args.csv_output)
logger.info(f"📊 CSV file generated: {args.csv_output}")
logger.info(f"📋 Total rows: {len(keys)} (plus header)")
except Exception as e:
logger.error(f"❌ CSV generation failed: {e}")
sys.exit(1)
- Step 6: Verify the CLI still produces identical CSV
Run: poetry run python scripts/derive_path_keys.py --path "3.1.2.1" --decimal-path --quantity 100 --master-key keys/dev/dev_key.json --csv-output /tmp/regen2.csv --quiet && diff keys/dev/pauls_1-100st.csv /tmp/regen2.csv && echo IDENTICAL
Expected: IDENTICAL (behaviour preserved).
- Step 7: Commit
git add scripts/utils/output_utils.py scripts/derive_path_keys.py tests/test_output_utils.py
git commit -m "refactor(scripts): extract write_keys_csv into shared output_utils"
Task 3: Core — filename builder, env inference, base58↔decimal path
Files:
- Create:
scripts/plings_keygen_core.py -
Test:
tests/test_keygen_core.py - Step 1: Write the failing test
# tests/test_keygen_core.py
import os, sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts'))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts', 'utils'))
import plings_keygen_core as core
def test_build_csv_filename():
assert core.build_csv_filename("pauls", 101, 150) == "pauls_101-150st.csv"
def test_infer_env_dir():
assert core.infer_env_dir("keys/dev/dev_key.json").replace("\\", "/") == "keys/dev"
def test_decimal_base58_roundtrip():
assert core.decimal_path_to_base58("3.1.2.1") == "4.2.3.2"
assert core.base58_path_to_decimal("4.2.3.2.2") == "3.1.2.1.1"
- Step 2: Run test to verify it fails
Run: poetry run pytest tests/test_keygen_core.py -v
Expected: FAIL with ModuleNotFoundError: No module named 'plings_keygen_core'.
- Step 3: Create the module with these functions
# scripts/plings_keygen_core.py
#!/usr/bin/env python3
"""Pure, testable core for the Plings keygen menu (no I/O prompts)."""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'app'))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'utils'))
from crypto_utils import CryptoUtils
_crypto = CryptoUtils()
def build_csv_filename(label: str, start: int, end: int) -> str:
"""e.g. ('pauls', 101, 150) -> 'pauls_101-150st.csv'."""
return f"{label}_{start}-{end}st.csv"
def infer_env_dir(master_key_path: str) -> str:
"""Directory family of the chosen master key (e.g. keys/dev/dev_key.json -> keys/dev)."""
return os.path.dirname(master_key_path)
def decimal_path_to_base58(decimal_path: str) -> str:
return ".".join(_crypto.encode_number_base58(int(p)) for p in decimal_path.split("."))
def base58_path_to_decimal(base58_path: str) -> str:
return ".".join(str(_crypto.decode_number_base58(p)) for p in base58_path.split("."))
- Step 4: Run test to verify it passes
Run: poetry run pytest tests/test_keygen_core.py -v
Expected: PASS (3 tests).
- Step 5: Commit
git add scripts/plings_keygen_core.py tests/test_keygen_core.py
git commit -m "feat(keygen): core filename/env/path helpers"
Task 4: Core — history read/write/update
Files:
- Modify:
scripts/plings_keygen_core.py -
Test:
tests/test_keygen_core.py - Step 1: Write the failing test
# append to tests/test_keygen_core.py
def test_history_roundtrip_and_update(tmp_path):
hist_file = tmp_path / ".keygen_history.json"
assert core.load_history(str(hist_file)) == {} # missing -> empty
h = core.update_history({}, "pauls", "3.1.2.1", 100, "keys/dev/dev_key.json")
assert h["pauls"]["last_instance"] == 100
assert h["pauls"]["path_decimal"] == "3.1.2.1"
core.save_history(str(hist_file), h)
assert core.load_history(str(hist_file))["pauls"]["last_instance"] == 100
def test_history_corrupt_file_is_empty(tmp_path):
hist_file = tmp_path / ".keygen_history.json"
hist_file.write_text("{ not json")
assert core.load_history(str(hist_file)) == {}
- Step 2: Run test to verify it fails
Run: poetry run pytest tests/test_keygen_core.py -k history -v
Expected: FAIL with AttributeError: module 'plings_keygen_core' has no attribute 'load_history'.
- Step 3: Add history functions
# add to scripts/plings_keygen_core.py
import json
from datetime import datetime, timezone
def load_history(history_path: str) -> dict:
"""Read the history file; missing or corrupt -> empty dict (never fatal)."""
try:
with open(history_path, "r", encoding="utf-8") as f:
data = json.load(f)
return data if isinstance(data, dict) else {}
except (FileNotFoundError, json.JSONDecodeError, OSError):
return {}
def save_history(history_path: str, history: dict) -> None:
with open(history_path, "w", encoding="utf-8") as f:
json.dump(history, f, indent=2)
def update_history(history: dict, label: str, path_decimal: str,
last_instance: int, master_key: str) -> dict:
history = dict(history)
history[label] = {
"path_decimal": path_decimal,
"last_instance": last_instance,
"master_key": master_key,
"updated_at": datetime.now(timezone.utc).isoformat(),
}
return history
- Step 4: Run test to verify it passes
Run: poetry run pytest tests/test_keygen_core.py -k history -v
Expected: PASS (2 tests).
- Step 5: Commit
git add scripts/plings_keygen_core.py tests/test_keygen_core.py
git commit -m "feat(keygen): history read/write/update"
Task 5: Core — master-key discovery
Files:
- Modify:
scripts/plings_keygen_core.py -
Test:
tests/test_keygen_core.py - Step 1: Write the failing test
# append to tests/test_keygen_core.py
import json as _json
def test_discover_master_keys(tmp_path):
(tmp_path / "dev").mkdir()
good = tmp_path / "dev" / "dev_key.json"
good.write_text(_json.dumps({"wallet_version": 3, "private_key": "x", "chain_code": "y"}))
(tmp_path / "dev" / "batch.csv").write_text("not,a,key")
(tmp_path / "dev" / "notkey.json").write_text(_json.dumps({"foo": "bar"}))
found = core.discover_master_keys(str(tmp_path))
assert any(p.endswith("dev/dev_key.json") or p.endswith("dev\\dev_key.json") for p in found)
assert all("batch.csv" not in p and "notkey.json" not in p for p in found)
- Step 2: Run test to verify it fails
Run: poetry run pytest tests/test_keygen_core.py -k discover -v
Expected: FAIL with AttributeError: ... has no attribute 'discover_master_keys'.
- Step 3: Add discovery
# add to scripts/plings_keygen_core.py
import glob
def is_master_key(path: str) -> bool:
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return isinstance(data, dict) and "private_key" in data and "chain_code" in data
except (json.JSONDecodeError, OSError):
return False
def discover_master_keys(keys_root: str) -> list:
"""All *.json under keys_root that look like master keys, sorted by path."""
candidates = glob.glob(os.path.join(keys_root, "**", "*.json"), recursive=True)
return sorted(p for p in candidates if is_master_key(p))
- Step 4: Run test to verify it passes
Run: poetry run pytest tests/test_keygen_core.py -k discover -v
Expected: PASS.
- Step 5: Commit
git add scripts/plings_keygen_core.py tests/test_keygen_core.py
git commit -m "feat(keygen): master-key discovery"
Task 6: Core — URL parse + validate against a master key
Files:
- Modify:
scripts/plings_keygen_core.py -
Test:
tests/test_keygen_core.py - Step 1: Write the failing test (uses a real row from
keys/dev/pauls_1-100st.csv)
# append to tests/test_keygen_core.py
from generate_master_key import MasterKeyGenerator
# keys/dev/dev_key.json is GITIGNORED, so regenerate it deterministically instead of
# reading the file. Dev mode is seed-based and reproduces dev_key.json byte-for-byte.
DEV_KEY = MasterKeyGenerator(production=False).generate_master_key(3)
GOOD_URL = "https://s.plings.io?t=q&i=CCuejfEAmFGKaTqhjgXjxQepiY9CZyEB463wtA4izNb9&p=4.2.3.2.2"
def test_parse_plings_url():
parsed = core.parse_plings_url(GOOD_URL)
assert parsed["i"] == "CCuejfEAmFGKaTqhjgXjxQepiY9CZyEB463wtA4izNb9"
assert parsed["p"] == "4.2.3.2.2"
assert parsed["t"] == "q"
def test_validate_url_match():
r = core.validate_url_against_master(GOOD_URL, DEV_KEY)
assert r["match"] is True
assert r["decimal_path"] == "3.1.2.1.1"
assert r["wallet_version"] == 3
def test_validate_url_mismatch():
tampered = GOOD_URL.replace("CCue", "ZZZZ")
r = core.validate_url_against_master(tampered, DEV_KEY)
assert r["match"] is False
- Step 2: Run test to verify it fails
Run: poetry run pytest tests/test_keygen_core.py -k url -v
Expected: FAIL with AttributeError: ... has no attribute 'parse_plings_url'.
- Step 3: Add URL parse + validate
# add to scripts/plings_keygen_core.py
from urllib.parse import urlparse, parse_qs
from derive_path_keys import HDPathKeyDerivation
def parse_plings_url(url: str) -> dict:
"""Extract i (instance_key), p (base58 path), cp (class pointer), t (transport)."""
qs = parse_qs(urlparse(url).query)
return {
"i": (qs.get("i") or [""])[0],
"p": (qs.get("p") or [""])[0],
"cp": (qs.get("cp") or [None])[0],
"t": (qs.get("t") or [None])[0],
}
def validate_url_against_master(url: str, master_key_data: dict) -> dict:
"""Re-derive the path against the master key and compare to the URL's instance key."""
parsed = parse_plings_url(url)
deriv = HDPathKeyDerivation(master_key_data)
derived = deriv.derive_path_key(parsed["p"])
return {
"match": derived["instance_key"] == parsed["i"],
"url_instance_key": parsed["i"],
"derived_instance_key": derived["instance_key"],
"base58_path": parsed["p"],
"decimal_path": base58_path_to_decimal(parsed["p"]),
"wallet_version": derived["wallet_version"],
"short_code": derived["short_code"],
"cp_in_url": parsed["cp"],
}
- Step 4: Run test to verify it passes
Run: poetry run pytest tests/test_keygen_core.py -k url -v
Expected: PASS (3 tests).
- Step 5: Commit
git add scripts/plings_keygen_core.py tests/test_keygen_core.py
git commit -m "feat(keygen): URL parse + validate against master key"
Task 7: Determinism regression test
Files:
-
Test:
tests/test_keygen_core.py -
Step 1: Write the test (self-contained known-answer — no gitignored fixtures)
# append to tests/test_keygen_core.py
from derive_path_keys import HDPathKeyDerivation
def test_determinism_known_answer():
# DEV_KEY is regenerated deterministically above — no gitignored fixture needed.
keys = HDPathKeyDerivation(DEV_KEY).derive_batch_keys("4.2.3.2", 100) # decimal 3.1.2.1, qty 100
# Known answers pin the BIP32 derivation (these equal rows in keys/dev/pauls_1-100st.csv).
assert keys[0]["plings_path"] == "4.2.3.2.2"
assert keys[0]["instance_key"] == "CCuejfEAmFGKaTqhjgXjxQepiY9CZyEB463wtA4izNb9"
assert keys[0]["short_code"] == "CCue"
assert keys[99]["plings_path"] == "4.2.3.2.2j"
assert keys[99]["instance_key"] == "EroLzw2j2jfXM1fydPyvRcAThMb1Uyidi6SVq6tSMfpj"
# Re-derivation is byte-stable (determinism guarantee).
keys2 = HDPathKeyDerivation(DEV_KEY).derive_batch_keys("4.2.3.2", 100)
assert [k["instance_key"] for k in keys] == [k["instance_key"] for k in keys2]
- Step 2: Run test to verify it passes (regression guard, expected to pass immediately)
Run: poetry run pytest tests/test_keygen_core.py -k determinism -v
Expected: PASS. If it FAILS, generation behaviour drifted — stop and investigate before continuing.
- Step 3: Commit
git add tests/test_keygen_core.py
git commit -m "test(keygen): determinism regression vs pauls_1-100st.csv"
Task 8: The interactive TUI entrypoint
Files:
- Create:
scripts/plings_keygen.py
This task is verified manually (interactive TUI). Write the complete file, then run the three manual checks.
- Step 1: Create
scripts/plings_keygen.py
#!/usr/bin/env python3
"""Plings Keygen — interactive menu for key/tag generation, master keys, URL validation."""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'app'))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'utils'))
sys.path.insert(0, os.path.dirname(__file__))
import questionary
from rich.console import Console
from rich.table import Table
import plings_keygen_core as core
from derive_path_keys import HDPathKeyDerivation, load_master_key
from generate_master_key import MasterKeyGenerator
from import_master_key import MasterKeyImporter
from output_utils import write_keys_csv
from path_utils import PlingsPathUtils
console = Console()
KEYS_ROOT = os.path.join(os.path.dirname(__file__), '..', 'keys')
HISTORY_FILE = os.path.join(KEYS_ROOT, '.keygen_history.json')
def choose_master_key():
keys = core.discover_master_keys(KEYS_ROOT)
if not keys:
console.print("[red]No master keys found under keys/. Create one first.[/red]")
return None
choice = questionary.select(
"Select master key:",
choices=[questionary.Choice(os.path.relpath(k), value=k) for k in keys],
).ask()
return choice
def generate_flow():
mk_path = choose_master_key()
if not mk_path:
return
master = load_master_key(mk_path)
history = core.load_history(HISTORY_FILE)
source = questionary.select(
"Path source:",
choices=["Continue from history", "New path"] if history else ["New path"],
).ask()
if source == "Continue from history":
label = questionary.select("Which batch?", choices=list(history.keys())).ask()
decimal_path = history[label]["path_decimal"]
default_start = history[label]["last_instance"] + 1
else:
label = questionary.text("Label (e.g. pauls):").ask()
decimal_path = questionary.text("Decimal path (e.g. 3.1.2.1):").ask()
if not PlingsPathUtils.validate_plings_path(core.decimal_path_to_base58(decimal_path) + ".2"):
console.print("[red]Invalid path.[/red]")
return
default_start = 1
start = int(questionary.text("Start instance:", default=str(default_start)).ask())
qty = int(questionary.text("Quantity:", default="1").ask())
cp = questionary.text("Class pointer (blank = none):", default="").ask() or None
end = start + qty - 1
env_dir = core.infer_env_dir(mk_path)
out_path = os.path.join(env_dir, core.build_csv_filename(label, start, end))
console.print(f"\n[bold]Master:[/bold] {os.path.relpath(mk_path)}")
console.print(f"[bold]Path:[/bold] {decimal_path} (base58 {core.decimal_path_to_base58(decimal_path)})")
console.print(f"[bold]Instances:[/bold] {start}–{end} ({qty} keys)")
console.print(f"[bold]Output:[/bold] {out_path}")
if os.path.exists(out_path) and not questionary.confirm(
f"{out_path} exists. Overwrite?", default=False).ask():
return
if not questionary.confirm("Generate?", default=True).ask():
return
deriv = HDPathKeyDerivation(master, class_pointer=cp)
base58_start = core.decimal_path_to_base58(decimal_path) + "." + \
deriv.crypto_utils.encode_number_base58(start)
keys = deriv.derive_batch_from_instance(base58_start, qty)
if not deriv.validate_derived_keys(keys):
console.print("[red]Validation failed.[/red]")
return
write_keys_csv(keys, out_path)
core.save_history(HISTORY_FILE,
core.update_history(history, label, decimal_path, end, mk_path))
table = Table(title=f"{len(keys)} keys → {os.path.relpath(out_path)}")
table.add_column("shortcode"); table.add_column("decimal_path"); table.add_column("url")
for k in keys[:5]:
table.add_row(k["short_code"], core.base58_path_to_decimal(k["plings_path"]), k["qr_url"])
console.print(table)
def master_key_flow():
action = questionary.select("Master keys:", choices=["Create new", "Import existing", "Back"]).ask()
if action == "Create new":
version = int(questionary.text("Wallet version:", default="1").ask())
production = questionary.confirm("Production key?", default=False).ask()
env = "production" if production else "dev"
name = questionary.text("Filename:", default=f"master_v{version}.json").ask()
out = os.path.join(KEYS_ROOT, env, name)
gen = MasterKeyGenerator(production=production)
gen.save_master_key(gen.generate_master_key(version), out)
console.print(f"[green]Created {out}[/green]")
elif action == "Import existing":
env = questionary.select("Store under:", choices=["dev", "production"]).ask()
name = questionary.text("Filename:", default="imported_key.json").ask()
out = os.path.join(KEYS_ROOT, env, name)
version = int(questionary.text("Wallet version:", default="1").ask())
importer = MasterKeyImporter()
kind = questionary.select("Import from:", choices=["Base58 private key", "JSON file"]).ask()
if kind == "Base58 private key":
data = importer.import_from_base58(questionary.text("Paste base58 private key:").ask(), version)
else:
data = importer.import_from_json(questionary.text("Path to JSON key file:").ask(), version)
importer.save_imported_key(data, out)
console.print(f"[green]Imported → {out}[/green]")
def validate_url_flow():
url = questionary.text("Paste the full s.plings.io URL:").ask()
mk_path = choose_master_key()
if not mk_path:
return
r = core.validate_url_against_master(url, load_master_key(mk_path))
status = "[green]✅ MATCH[/green]" if r["match"] else "[red]❌ MISMATCH[/red]"
console.print(f"\n{status}")
console.print(f"path: {r['decimal_path']} wallet_v: {r['wallet_version']} short_code: {r['short_code']}")
console.print(f"url i=: {r['url_instance_key']}")
console.print(f"derived: {r['derived_instance_key']}")
if r["cp_in_url"]:
console.print(f"cp in url: {r['cp_in_url']}")
def main():
console.print("[bold cyan]Plings Keygen[/bold cyan]")
while True:
action = questionary.select(
"Main menu:",
choices=["Generate keys / tags", "Master keys", "Validate a URL", "Exit"],
).ask()
if action is None or action == "Exit":
break
try:
if action == "Generate keys / tags":
generate_flow()
elif action == "Master keys":
master_key_flow()
elif action == "Validate a URL":
validate_url_flow()
except (KeyboardInterrupt, EOFError):
break
except Exception as e: # never crash the menu on one operation
console.print(f"[red]Error: {e}[/red]")
if __name__ == "__main__":
main()
- Step 2: Sanity-check the reused class APIs
The code above relies on these verified signatures — confirm they still exist before running:
MasterKeyImporter.import_from_base58(base58_key, wallet_version=1),import_from_json(file_path, wallet_version=None),save_imported_key(master_key_data, output_file)(scripts/import_master_key.py)MasterKeyGenerator(production).generate_master_key(version)+save_master_key(data, out)(scripts/generate_master_key.py)-
HDPathKeyDerivation(master, class_pointer=cp).derive_batch_from_instance(start_path, qty)+.validate_derived_keys()+.crypto_utils.encode_number_base58()(scripts/derive_path_keys.py) - Step 3: Manual check — generate
Run: poetry run python scripts/plings_keygen.py
Choose Generate → select keys/dev/dev_key.json → New path → label test_menu, path 3.1.2.1, start 1, qty 3, no cp → confirm.
Expected: writes keys/dev/test_menu_1-3st.csv, prints a rich table, and keys/.keygen_history.json now has a test_menu entry. Delete the test CSV afterward.
- Step 4: Manual check — validate URL
Run the program → Validate a URL → paste https://s.plings.io?t=q&i=CCuejfEAmFGKaTqhjgXjxQepiY9CZyEB463wtA4izNb9&p=4.2.3.2.2 → select keys/dev/dev_key.json.
Expected: ✅ MATCH, path 3.1.2.1.1, wallet_v 3.
- Step 5: Manual check — continue from history
Run the program → Generate → Continue from history → test_menu.
Expected: start instance pre-filled as 4.
- Step 6: Commit
git add scripts/plings_keygen.py
git commit -m "feat(keygen): interactive questionary/rich menu entrypoint"
Task 9: Docs + final quality control
Files:
-
Modify:
scripts/README.md -
Step 1: Document the new tool
Add a short section to scripts/README.md describing plings_keygen.py: purpose (menu-driven alternative to derive_path_keys.py), how to run (poetry run python scripts/plings_keygen.py), the three menu functions, the keys/<env>/<label>_<start>-<end>st.csv output convention, and the keys/.keygen_history.json state file. Note that questionary/rich are dev-only deps.
- Step 2: Run the full test suite
Run: poetry run pytest tests/test_keygen_core.py tests/test_output_utils.py -v
Expected: all PASS (including the determinism regression).
- Step 3: Quality control
Per CLAUDE.md, run the quality-control-enforcer agent on the new code (scripts/plings_keygen.py, scripts/plings_keygen_core.py, scripts/utils/output_utils.py) and address findings.
- Step 4: Commit
git add scripts/README.md
git commit -m "docs(scripts): document plings_keygen interactive menu"
FINAL: Use quality-control-enforcer agent to validate implementation