From e17ef42e54270a3e08300df84e0854c3cddbe03b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 15:26:17 +0000 Subject: [PATCH] fix(cli): rename global --name to --account to free --name for subcommands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The marmot harness surfaced the bug on first run: amy stripped `marmot group create --name "Interop-02"` thinking the global account selector ran into the group's display-name flag. Result: amy resolved the "Interop-02" account, found no identity.json, and errored — no group ever got created. Renames the global account-selector flag to `--account`. The per- subcommand `--name` flags (`marmot group create --name "Demo"`, `profile edit --name "Alice"`, `marmot await group --name X`) are untouched — they're free of the global parser now that it doesn't claim the same name. Sweep: - `Main.kt`: GlobalFlag.NAME → ACCOUNT, long "--account". - `Config.kt`: DataDir.resolve param renamed nameFlag → accountFlag; every error message points the user at --account. - `UseCommand.kt`: error hint says `amy --account NAME init`. - All test wrappers + direct $AMY_BIN calls under cli/tests/ swap the global `--name X` for `--account X` (subcommand --name kept exactly where it appeared). - README + DEVELOPMENT updated. --- cli/DEVELOPMENT.md | 12 ++++---- cli/README.md | 30 +++++++++---------- .../com/vitorpamplona/amethyst/cli/Config.kt | 18 +++++------ .../com/vitorpamplona/amethyst/cli/Main.kt | 18 +++++------ .../amethyst/cli/commands/UseCommand.kt | 2 +- cli/tests/cache/cache-headless.sh | 4 +-- cli/tests/dm/setup.sh | 8 ++--- cli/tests/dm/tests-dm.sh | 12 ++++---- cli/tests/headless/helpers.sh | 2 +- cli/tests/marmot/tests-extras.sh | 2 +- 10 files changed, 54 insertions(+), 54 deletions(-) diff --git a/cli/DEVELOPMENT.md b/cli/DEVELOPMENT.md index ce275e433..54e421c96 100644 --- a/cli/DEVELOPMENT.md +++ b/cli/DEVELOPMENT.md @@ -201,11 +201,11 @@ Amy-specific layer still needs its own coverage: | Layer | Test approach | |---|---| -| Argument parsing (`Args`, flag forms, `--name=…` vs `--name …`) | Plain JVM unit tests in `cli/src/test/kotlin/`. | +| Argument parsing (`Args`, flag forms, `--account=…` vs `--account …`) | Plain JVM unit tests in `cli/src/test/kotlin/`. | | Error / exit-code contract (bad args → 2, await timeout → 124, runtime → 1) | Table-driven tests invoking `main(argv)` with captured stdout/stderr. | -| JSON output shape (each command's keys and types under `--json`) | Snapshot tests: run a command with `--json` against a throwaway `$HOME` (`HOME=$(mktemp -d) amy --name X …`), assert the JSON matches a golden file. The default text render has no shape contract and shouldn't be snapshotted. | +| JSON output shape (each command's keys and types under `--json`) | Snapshot tests: run a command with `--json` against a throwaway `$HOME` (`HOME=$(mktemp -d) amy --account X …`), assert the JSON matches a golden file. The default text render has no shape contract and shouldn't be snapshotted. | | File layout on disk (`identity.json`, `events-store/…`, `marmot/groups/*.mls`, `marmot/keypackages.bundle`) | Structural assertions after a command sequence. | -| Round-trip between two accounts on a local relay | End-to-end shell harnesses under `cli/tests/`. Each harness spins up a local `nostr-rs-relay` and a fresh `$HOME=$STATE_DIR` so amy sees a virgin `~/.amy/`, then bootstraps multiple identities (`--name A`, `--name D`, etc.) sharing the same `~/.amy/shared/events-store/` and drives a scenario through them (+ `wn` for Marmot interop against whitenoise-rs). Today there are two suites: `cli/tests/marmot/` (MLS scenarios vs whitenoise-rs) and `cli/tests/dm/` (NIP-17 DM round-trips between two `amy` accounts). | +| Round-trip between two accounts on a local relay | End-to-end shell harnesses under `cli/tests/`. Each harness spins up a local `nostr-rs-relay` and a fresh `$HOME=$STATE_DIR` so amy sees a virgin `~/.amy/`, then bootstraps multiple identities (`--account A`, `--account D`, etc.) sharing the same `~/.amy/shared/events-store/` and drives a scenario through them (+ `wn` for Marmot interop against whitenoise-rs). Today there are two suites: `cli/tests/marmot/` (MLS scenarios vs whitenoise-rs) and `cli/tests/dm/` (NIP-17 DM round-trips between two `amy` accounts). | | Interop with other clients | Covered by `cli/tests/marmot/marmot-interop-headless.sh` (drives Amy against whitenoise-rs `wn`/`wnd`). Add new scenarios there or start a new sibling under `cli/tests/`. | **What not to test here:** event signing, filter assembly, MLS @@ -224,12 +224,12 @@ At the byte-banging level, a minimal round-trip looks like: set -euo pipefail export HOME=$(mktemp -d) # virgin ~/.amy/ for the duration of this script -amy --name alice create -amy --name bob create +amy --account alice create +amy --account bob create # ... the scenario under test ... -amy --name bob marmot await message "$GID" --match "hello" --timeout 60 +amy --account bob marmot await message "$GID" --match "hello" --timeout 60 ``` If an Amethyst scenario cannot be scripted through Amy yet, that's diff --git a/cli/README.md b/cli/README.md index d5df6f96e..371d39c25 100644 --- a/cli/README.md +++ b/cli/README.md @@ -32,7 +32,7 @@ What every caller — user, script, agent, CI — can rely on: - **`--json` is the machine contract. One line. One object.** Stable snake_case keys. Pipe it into `jq`, parse it from Python, hand it to an agent. Pass `--json` anywhere before the subcommand: - `amy --json whoami`, `amy --name alice --json marmot group list`. + `amy --json whoami`, `amy --account alice --json marmot group list`. - **stderr is for humans.** Progress, warnings, per-relay ACK traces. Safe to discard. Errors land here too: `error: : ` by default, or JSON `{"error":"…","detail":"…"}` under `--json`. @@ -47,7 +47,7 @@ What every caller — user, script, agent, CI — can rely on: observed Nostr event lands in `~/.amy/shared/events-store/` (one cache for every account on the machine). Delete to reset; copy to move. Tests isolate by overriding `$HOME` for the amy subprocess - (`HOME=/tmp/run.123 amy --name alice …`) — same convention `git`, + (`HOME=/tmp/run.123 amy --account alice …`) — same convention `git`, `gpg`, and `npm` use. Only the `--json` shape and the exit codes are public API. The default @@ -153,19 +153,19 @@ The `installDist` tree under `cli/build/install/amy/` is self-contained # 1. Create the alice account (~/.amy/alice/) with a full Amethyst-style # bootstrap: keypair, default NIP-65 / inbox / key-package relays, # nine bootstrap events. -amy --name alice create +amy --account alice create # 2. Publish a fresh MLS KeyPackage so others can invite you. -amy --name alice marmot key-package publish +amy --account alice marmot key-package publish # 3. Create a group, invite someone, send a message. -amy --name alice marmot group create --name "Test Group" -amy --name alice marmot group add npub1...bob -amy --name alice marmot message send "hello" +amy --account alice marmot group create --name "Test Group" +amy --account alice marmot group add npub1...bob +amy --account alice marmot message send "hello" # 4. On the receiving side, in ~/.amy/bob/. -amy --name bob marmot await group --name "Test Group" --timeout 60 -amy --name bob marmot message list +amy --account bob marmot await group --name "Test Group" --timeout 60 +amy --account bob marmot message list ``` With one account you can drop the flag (auto-pick); with several, pin @@ -174,7 +174,7 @@ with `jq` by adding `--json` to switch stdout from human text to a parseable single-line JSON object: ```bash -GID=$(amy --name alice --json marmot group create --name "Test" | jq -r .group_id) +GID=$(amy --account alice --json marmot group create --name "Test" | jq -r .group_id) ``` For an interop-test script template, see @@ -235,7 +235,7 @@ itself crashed". ### Global flags -- `--name ACCOUNT` — pick which account under `~/.amy//` to +- `--account ACCOUNT` — pick which account under `~/.amy//` to operate on. Required only when more than one account exists; with a single account amy auto-picks. Override with `amy use NAME` to pin one as the default. ACCOUNT must match `[a-zA-Z0-9_-]{1,64}`. @@ -292,7 +292,7 @@ events. │ ├── addressable/… # one slot per (kind, pubkey, d-tag) for kind:30000-39999 │ ├── idx/ # hardlink indexes (kind / author / owner / tag / fts / expires_at) │ └── tombstones/ # NIP-09 / NIP-62 enforcement -├── alice/ # one dir per account (e.g. created by `amy --name alice init`) +├── alice/ # one dir per account (e.g. created by `amy --account alice init`) │ ├── identity.json # nsec/npub/hex — the account │ ├── state.json # sync cursors (giftWrapSince, groupSince) │ ├── aliases.json # local name → npub map (init writes a self-entry) @@ -321,10 +321,10 @@ those events to upstream relays. There is no `relays.json`. ## Troubleshooting - **`no identity`** — run `init`, `create`, or `login` first, or pass a - different `--name`. + different `--account`. - **`no account at ~/.amy`** — there's no account dir yet; create one - with `amy --name init`. -- **`multiple accounts in ~/.amy (...)`** — disambiguate with `--name` + with `amy --account init`. +- **`multiple accounts in ~/.amy (...)`** — disambiguate with `--account` on the next call, or pin a default via `amy use NAME`. - **`not_member`** — the group GID is unknown to this account. Run `marmot group list` to confirm, or `marmot await group --name …` to diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Config.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Config.kt index b5f912556..f8b10c6c2 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Config.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Config.kt @@ -239,7 +239,7 @@ class DataDir( fun validateName(name: String): String { require(NAME_REGEX.matches(name)) { - "--name must match [a-zA-Z0-9_-]{1,64} (got '$name')" + "--account must match [a-zA-Z0-9_-]{1,64} (got '$name')" } require(name !in RESERVED_NAMES) { "'$name' is reserved (cannot be used as an account name)" @@ -251,17 +251,17 @@ class DataDir( * Build a [DataDir] from the parsed CLI flags. * * Resolution order: - * 1. `--name X` if provided. + * 1. `--account X` if provided. * 2. `/current` marker (set by `amy use X`). * 3. Sole subdirectory of `` other than `shared/`. - * 4. Error — caller must disambiguate via `--name` or `amy use`. + * 4. Error — caller must disambiguate via `--account` or `amy use`. */ fun resolve( - nameFlag: String?, + accountFlag: String?, secrets: SecretStore, ): DataDir { val rootBase = DEFAULT_ROOT - val name = if (nameFlag != null) validateName(nameFlag) else pickAccount(rootBase) + val name = if (accountFlag != null) validateName(accountFlag) else pickAccount(rootBase) val accountRoot = File(rootBase, name).absoluteFile val sharedEvents = File(rootBase, "$SHARED_DIR_NAME/events-store").absoluteFile return DataDir( @@ -284,11 +284,11 @@ class DataDir( if (current.isFile) { val pinned = current.readText().trim() require(pinned.isNotEmpty()) { - "${current.absolutePath} is empty; rewrite with `amy use ` or pass --name" + "${current.absolutePath} is empty; rewrite with `amy use ` or pass --account" } require(File(rootBase, pinned).isDirectory) { "${current.absolutePath} pins '$pinned' but ${File(rootBase, pinned).absolutePath} doesn't exist; " + - "rewrite with `amy use ` or pass --name" + "rewrite with `amy use ` or pass --account" } return pinned } @@ -296,7 +296,7 @@ class DataDir( return when (accounts.size) { 0 -> { throw IllegalArgumentException( - "no account at ${rootBase.absolutePath}; create one with `amy --name init`", + "no account at ${rootBase.absolutePath}; create one with `amy --account init`", ) } @@ -307,7 +307,7 @@ class DataDir( else -> { throw IllegalArgumentException( "multiple accounts in ${rootBase.absolutePath} (${accounts.joinToString(", ")}); " + - "pick one with --name or `amy use `", + "pick one with --account or `amy use `", ) } } diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Main.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Main.kt index 991b37113..db8818f3d 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Main.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Main.kt @@ -82,7 +82,7 @@ private suspend fun dispatch(argv: Array): Int { // Pull global flags out of argv before subcommand parsing so subcommands see // only their own args. val filteredArgs = mutableListOf() - var nameFlag: String? = null + var accountFlag: String? = null var secretBackendFlag: String? = null var passphraseFileFlag: String? = null var i = 0 @@ -90,7 +90,7 @@ private suspend fun dispatch(argv: Array): Int { val a = argv[i] val (matched, consumed) = extractGlobalFlag(a, argv, i) when (matched) { - GlobalFlag.NAME -> nameFlag = consumed.value + GlobalFlag.ACCOUNT -> accountFlag = consumed.value GlobalFlag.SECRET_BACKEND -> secretBackendFlag = consumed.value GlobalFlag.PASSPHRASE_FILE -> passphraseFileFlag = consumed.value GlobalFlag.JSON -> Output.mode = Output.Mode.JSON @@ -116,7 +116,7 @@ private suspend fun dispatch(argv: Array): Int { } val secrets = SecretStore.from(backendFlag = secretBackendFlag, passphraseFile = passphraseFileFlag) - val dataDir = DataDir.resolve(nameFlag = nameFlag, secrets = secrets) + val dataDir = DataDir.resolve(accountFlag = accountFlag, secrets = secrets) return when (head) { "init" -> { @@ -210,7 +210,7 @@ private enum class GlobalFlag( val long: String, val takesValue: Boolean = true, ) { - NAME("--name"), + ACCOUNT("--account"), SECRET_BACKEND("--secret-backend"), PASSPHRASE_FILE("--passphrase-file"), JSON("--json", takesValue = false), @@ -253,7 +253,7 @@ private fun printUsage() { |amy — Amethyst command-line interface | |Usage: - | amy [--name ACCOUNT] + | amy [--account ACCOUNT] | [--secret-backend auto|keychain|ncryptsec|plaintext] | [--passphrase-file PATH] | [--json] @@ -267,13 +267,13 @@ private fun printUsage() { | [a-zA-Z0-9_-]{1,64} (no spaces, no slashes). | | Resolution order: - | 1. --name X if given. + | 1. --account X if given. | 2. ~/.amy/current marker (set by `amy use X`). | 3. Sole subdirectory of ~/.amy/ other than shared/. - | 4. Error — disambiguate with --name or `amy use`. + | 4. Error — disambiguate with --account or `amy use`. | | Test harnesses isolate by overriding ${'$'}HOME for the amy - | subprocess (`HOME=/tmp/run.123 amy --name alice ...`). + | subprocess (`HOME=/tmp/run.123 amy --account alice ...`). | | use NAME pin NAME as the active account | use --clear remove the pin @@ -296,7 +296,7 @@ private fun printUsage() { | |Identity: | init [--nsec NSEC] create or import a bare identity (no defaults published) - | create [--name NAME] provision a full Amethyst-style account + publish bootstrap events + | create [--name NAME] provision a full Amethyst-style account + publish bootstrap events | login KEY [--password X] import (nsec|ncryptsec|mnemonic|npub|nprofile|hex|nip05) | whoami print current identity | diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/UseCommand.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/UseCommand.kt index f1ad5c9c6..6d7c26b8d 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/UseCommand.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/UseCommand.kt @@ -74,7 +74,7 @@ object UseCommand { if (!accountDir.isDirectory) { return Output.error( "no_account", - "${accountDir.absolutePath} doesn't exist; create it with `amy --name $name init`", + "${accountDir.absolutePath} doesn't exist; create it with `amy --account $name init`", ) } SecureFileIO.secureMkdirs(rootBase) diff --git a/cli/tests/cache/cache-headless.sh b/cli/tests/cache/cache-headless.sh index 18d7fc731..df674d36d 100755 --- a/cli/tests/cache/cache-headless.sh +++ b/cli/tests/cache/cache-headless.sh @@ -83,7 +83,7 @@ source "$TESTS_DIR/headless/helpers.sh" # shellcheck source=../dm/setup.sh source "$TESTS_DIR/dm/setup.sh" -amy_b() { HOME="$STATE_DIR" "$AMY_BIN" --name B --secret-backend plaintext --json "$@"; } +amy_b() { HOME="$STATE_DIR" "$AMY_BIN" --account B --secret-backend plaintext --json "$@"; } # Helper: B-side amy_json. The dm headless's helpers.sh hardcodes A_DIR # in amy_a; we need a parallel for B without re-sourcing. @@ -251,7 +251,7 @@ fi # ---------------------------------------------------------------------- banner "T7 — store maintenance verbs (sweep/scrub/compact) on an empty store" TMP_HOME=$(mktemp -d) -T7_AMY=(${AMY_BIN} --secret-backend plaintext --name throwaway --json store) +T7_AMY=(${AMY_BIN} --secret-backend plaintext --account throwaway --json store) T7_STAT=$(HOME="$TMP_HOME" "${T7_AMY[@]}" stat) T7_SWEEP=$(HOME="$TMP_HOME" "${T7_AMY[@]}" sweep-expired) T7_SCRUB=$(HOME="$TMP_HOME" "${T7_AMY[@]}" scrub) diff --git a/cli/tests/dm/setup.sh b/cli/tests/dm/setup.sh index 677d57cc0..8693ae7f4 100644 --- a/cli/tests/dm/setup.sh +++ b/cli/tests/dm/setup.sh @@ -78,15 +78,15 @@ preflight_dm() { # `--secret-backend=plaintext` keeps these throwaway interop runs headless — # the default `auto` would try the OS keychain (not available in CI) and then # ask for a NIP-49 passphrase. Plaintext still writes 0600-owner-only. -amy_a() { HOME="$STATE_DIR" "$AMY_BIN" --name A --secret-backend plaintext --json "$@"; } -amy_d() { HOME="$STATE_DIR" "$AMY_BIN" --name D --secret-backend plaintext --json "$@"; } +amy_a() { HOME="$STATE_DIR" "$AMY_BIN" --account A --secret-backend plaintext --json "$@"; } +amy_d() { HOME="$STATE_DIR" "$AMY_BIN" --account D --secret-backend plaintext --json "$@"; } # --- identity bootstrap ------------------------------------------------------ ensure_identity_for() { local who="$1" - step "initialising Identity $who (amy at \$HOME=$STATE_DIR --name $who)" + step "initialising Identity $who (amy at \$HOME=$STATE_DIR --account $who)" local out - out=$(HOME="$STATE_DIR" "$AMY_BIN" --name "$who" --secret-backend plaintext --json init) || { + out=$(HOME="$STATE_DIR" "$AMY_BIN" --account "$who" --secret-backend plaintext --json init) || { fail_msg "amy init failed for $who: $out"; exit 1 } local npub hex diff --git a/cli/tests/dm/tests-dm.sh b/cli/tests/dm/tests-dm.sh index 0b32513aa..4fff53fbe 100644 --- a/cli/tests/dm/tests-dm.sh +++ b/cli/tests/dm/tests-dm.sh @@ -13,14 +13,14 @@ # dm-06 cursor advance (subsequent no-flag `dm list` is empty) # Wrap amy_json around either account so the per-test code stays tight. -# Both share $HOME=$STATE_DIR (set by the harness); --name picks the +# Both share $HOME=$STATE_DIR (set by the harness); --account picks the # account inside it. `--json` opts into amy's machine-readable contract; # assertions below parse with jq. amy_json_for() { local account="$1"; shift local out - if ! out=$(HOME="$STATE_DIR" "$AMY_BIN" --name "$account" --secret-backend plaintext --json "$@" 2>>"$LOG_FILE"); then - fail_msg "amy --name $account $*: exit $? (see $LOG_FILE)" + if ! out=$(HOME="$STATE_DIR" "$AMY_BIN" --account "$account" --secret-backend plaintext --json "$@" 2>>"$LOG_FILE"); then + fail_msg "amy --account $account $*: exit $? (see $LOG_FILE)" printf '%s\n' "$out" >>"$LOG_FILE" return 1 fi @@ -96,7 +96,7 @@ test_03_dm_send_rejects_no_inbox() { # test's main STATE_DIR with a third account. local ghost_home; ghost_home=$(mktemp -d "${STATE_DIR}/ghost-home.XXXXXX") local ghost_out ghost_npub - ghost_out=$(HOME="$ghost_home" "$AMY_BIN" --name ghost --secret-backend plaintext --json init) || { + ghost_out=$(HOME="$ghost_home" "$AMY_BIN" --account ghost --secret-backend plaintext --json init) || { record_result "$id" fail "ghost init failed"; rm -rf "$ghost_home"; return } ghost_npub=$(printf '%s' "$ghost_out" | jq -r '.npub') @@ -104,7 +104,7 @@ test_03_dm_send_rejects_no_inbox() { # A sends without --allow-fallback; amy should refuse. local raw rc - raw=$(HOME="$STATE_DIR" "$AMY_BIN" --name A --secret-backend plaintext --json dm send "$ghost_npub" "should be rejected" 2>&1) + raw=$(HOME="$STATE_DIR" "$AMY_BIN" --account A --secret-backend plaintext --json dm send "$ghost_npub" "should be rejected" 2>&1) rc=$? rm -rf "$ghost_home" if [[ "$rc" -ne 0 ]] && printf '%s' "$raw" | grep -q '"error":"no_dm_relays"'; then @@ -126,7 +126,7 @@ test_04_dm_send_allow_fallback() { # publish should succeed even though the ghost has no 10050. local ghost_home; ghost_home=$(mktemp -d "${STATE_DIR}/ghost-home.XXXXXX") local ghost_out ghost_npub - ghost_out=$(HOME="$ghost_home" "$AMY_BIN" --name ghost --secret-backend plaintext --json init) || { + ghost_out=$(HOME="$ghost_home" "$AMY_BIN" --account ghost --secret-backend plaintext --json init) || { record_result "$id" fail "ghost init failed"; rm -rf "$ghost_home"; return } ghost_npub=$(printf '%s' "$ghost_out" | jq -r '.npub') diff --git a/cli/tests/headless/helpers.sh b/cli/tests/headless/helpers.sh index 8521c219c..e6f6384f9 100644 --- a/cli/tests/headless/helpers.sh +++ b/cli/tests/headless/helpers.sh @@ -14,7 +14,7 @@ # # `--json` opts into amy's machine-readable output (the harness parses it # with jq); the default human-text output is for terminal use. -amy_a() { HOME="$STATE_DIR" "$AMY_BIN" --name alice --secret-backend plaintext --json "$@"; } +amy_a() { HOME="$STATE_DIR" "$AMY_BIN" --account A --secret-backend plaintext --json "$@"; } # Run amy, log stderr, surface JSON on stdout, remember last result. amy_json() { diff --git a/cli/tests/marmot/tests-extras.sh b/cli/tests/marmot/tests-extras.sh index 4730a7630..7b82a6dcf 100644 --- a/cli/tests/marmot/tests-extras.sh +++ b/cli/tests/marmot/tests-extras.sh @@ -271,7 +271,7 @@ test_14_wn_removes_a() { local deadline=$(( $(date +%s) + 120 )) removed=0 while [[ $(date +%s) -lt $deadline ]]; do local show rc - show=$(HOME="$STATE_DIR" "$AMY_BIN" --name A --secret-backend plaintext --json \ + show=$(HOME="$STATE_DIR" "$AMY_BIN" --account A --secret-backend plaintext --json \ marmot group show "$a_gid" 2>&1) rc=$? printf '%s\n' "$show" >>"$LOG_FILE"