docs(cli): split amy docs by responsibility + add amy-expert skill
Separate the single DEVELOPMENT.md into focused docs per audience: - cli/README.md — trim agent/interop sub-sections; user-facing contract, commands, data-dir, troubleshooting only. - cli/DEVELOPMENT.md — pared down to architecture, how-to-add-a-command, output conventions, testing, housekeeping. - cli/ROADMAP.md (new) — north-star, parity matrix, ordered milestones, non-goals. The live checklist for CLI feature parity. - cli/plans/2026-04-21-cli-distribution.md (new) — packaging strategy (Homebrew / winget / Scoop / deb / rpm / AUR / AppImage). - commons/plans/2026-04-21-event-renderer.md (new) — cross-cutting renderer design owned by commons, consumed by cli + desktop + android. Agentic routing: - .claude/skills/amy-expert/SKILL.md (new) — routing triggers + the five hard rules (thin-layer, JSON contract, non-interactive, data-dir-is-world, extract-before-adding). - references/command-template.md, extraction-recipe.md, output-conventions.md — bundled copy-paste references. Root .claude/CLAUDE.md: - Adds cli/ to the module list with its sharing rule. - Registers amy-expert in the skills table. - Documents per-module plans/ convention; freezes docs/plans/. https://claude.ai/code/session_01BQ5ZHwa8BAgEQ9zeM4CKhW
This commit is contained in:
+120
-269
@@ -1,94 +1,74 @@
|
||||
# Developing Amy
|
||||
|
||||
This doc is the north-star plan for growing `amy` from a Marmot
|
||||
test-bed into a full command-line mirror of Amethyst. It is meant to
|
||||
be picked up by any future contributor (human or agent) who needs to
|
||||
add a feature, extract logic out of `amethyst/`, or close an interop
|
||||
gap.
|
||||
How to touch the `cli/` module without breaking its public contract.
|
||||
|
||||
User-facing reference lives in [README.md](./README.md). This file
|
||||
covers **why** things are shaped the way they are and **what to build
|
||||
next**.
|
||||
- What Amy is and what's already shipped: [README.md](./README.md).
|
||||
- What to build next and in what order: [ROADMAP.md](./ROADMAP.md).
|
||||
- Plans for cross-cutting work: see this module's `plans/` folder
|
||||
and `commons/plans/` for shared-code work consumed by Amy.
|
||||
|
||||
The rule this doc defends: **`cli/` is a thin assembly layer**. If
|
||||
you're writing Nostr protocol logic, filter building, state machines,
|
||||
or encryption in here, stop — that code belongs in `quartz/` or
|
||||
`commons/`.
|
||||
|
||||
---
|
||||
|
||||
## 1. North-star goal
|
||||
## Design principles
|
||||
|
||||
> For every feature of the Amethyst Android app, there is a way to
|
||||
> exercise it through `amy`, with byte-identical on-relay behaviour.
|
||||
|
||||
This matters because:
|
||||
|
||||
- Interop tests against the ~100 other Nostr clients need a reproducible
|
||||
harness that does not require running Android.
|
||||
- Agents and LLMs can script real Amethyst flows without touching a GUI.
|
||||
- Regressions in shared logic (signing, encryption, filter building,
|
||||
event parsing) become shell-scriptable.
|
||||
- Power users get a command-line Amethyst for free.
|
||||
|
||||
The non-goal: Amy is **not** a second Nostr client implementation. It
|
||||
is a thin assembly layer over `quartz` + `commons`. If you find yourself
|
||||
writing protocol or business logic inside `cli/`, stop — that code
|
||||
belongs in `commons/` or `quartz/`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Design principles
|
||||
|
||||
1. **Non-interactive.** One verb = one JSON object on stdout = one exit
|
||||
code. No REPL, no daemon, no prompts.
|
||||
2. **Thin command layer.** Each file in `cli/src/.../commands/` is glue:
|
||||
parse args → call into `commons/` or `quartz/` → print JSON. A file
|
||||
longer than ~200 lines is a code smell; the logic is probably living
|
||||
in the wrong module.
|
||||
1. **Non-interactive.** One verb = one JSON object on stdout = one
|
||||
exit code. No REPL, no daemon, no prompts. Any network wait is an
|
||||
explicit `await` verb with a `--timeout`.
|
||||
2. **Thin command layer.** Each file in `commands/` parses args,
|
||||
calls into `commons/` or `quartz/`, and prints JSON. A file longer
|
||||
than ~200 lines is a code smell — the logic is living in the wrong
|
||||
module.
|
||||
3. **Everything persistent is on-disk.** No in-memory caches that
|
||||
survive between invocations. Cursors, MLS state, identity, relay
|
||||
config — all reload on every run. This is what makes `amy` safe to
|
||||
run from CI and from 100 parallel interop scenarios.
|
||||
4. **Shared defaults.** When the Android app sets a default relay,
|
||||
picks a kind, derives a tag — Amy calls the same helper. No hand-
|
||||
rolled duplicates. If such a helper doesn't exist yet, the fix is to
|
||||
extract it to `commons/`, not to copy its body into `cli/`.
|
||||
5. **JSON is the public API.** Output shape changes are breaking
|
||||
changes. Treat them as such, version them explicitly in commit
|
||||
messages, and update interop fixtures.
|
||||
survive between invocations. Every run reloads cursors, MLS state,
|
||||
identity, and relay config. This is what makes Amy safe to run
|
||||
from CI and from 100 parallel interop scenarios.
|
||||
4. **Shared defaults.** When Amethyst picks a default relay, kind, or
|
||||
tag — Amy calls the same helper. No hand-rolled duplicates. If the
|
||||
helper doesn't exist yet, extract it to `commons/` first.
|
||||
5. **JSON is the public API.** Output-shape changes are breaking
|
||||
changes. Version them explicitly in commit messages; update interop
|
||||
fixtures.
|
||||
|
||||
---
|
||||
|
||||
## 3. Current architecture
|
||||
## Architecture
|
||||
|
||||
```
|
||||
cli/
|
||||
└── src/main/kotlin/com/vitorpamplona/amethyst/cli/
|
||||
├── Main.kt # argv → subcommand dispatch
|
||||
├── Args.kt # tiny flag parser (no framework)
|
||||
├── Json.kt # single-line stdout + error printer
|
||||
├── Config.kt # Identity, RelayConfig, RunState, DataDir
|
||||
├── Context.kt # per-run wiring (signer + NostrClient +
|
||||
│ # MarmotManager + publish/drain/sync helpers)
|
||||
├── stores/FileStores.kt # File-backed MLS / KP / message stores
|
||||
└── commands/
|
||||
├── Commands.kt # dispatcher
|
||||
├── InitCommands.kt # init, whoami
|
||||
├── CreateCommand.kt # full bootstrap (→ commons/account/)
|
||||
├── LoginCommand.kt # nsec/ncryptsec/mnemonic/npub/nprofile/hex/nip05
|
||||
├── RelayCommands.kt # add/list/publish-lists
|
||||
├── KeyPackageCommands.kt # marmot key-package publish / check
|
||||
├── GroupCommands.kt # marmot group create/list/show/…
|
||||
├── GroupCreateCommand.kt
|
||||
├── GroupReadCommands.kt
|
||||
├── GroupAddMemberCommand.kt
|
||||
├── GroupMembershipCommands.kt
|
||||
├── GroupMetadataCommands.kt
|
||||
├── MessageCommands.kt # marmot message send / list
|
||||
└── AwaitCommands.kt # poll-until-condition helpers
|
||||
cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/
|
||||
├── Main.kt # argv → subcommand dispatch
|
||||
├── Args.kt # tiny flag parser (no framework)
|
||||
├── Json.kt # single-line stdout + error printer
|
||||
├── Config.kt # Identity, RelayConfig, RunState, DataDir
|
||||
├── Context.kt # per-run wiring: signer + NostrClient +
|
||||
│ # MarmotManager + publish/drain/sync helpers
|
||||
├── stores/FileStores.kt # File-backed MLS / KP / message stores
|
||||
└── commands/
|
||||
├── Commands.kt # dispatcher
|
||||
├── InitCommands.kt # init, whoami
|
||||
├── CreateCommand.kt # full bootstrap (→ commons/account/)
|
||||
├── LoginCommand.kt # nsec/ncryptsec/mnemonic/npub/nprofile/hex/nip05
|
||||
├── RelayCommands.kt # add/list/publish-lists
|
||||
├── KeyPackageCommands.kt # marmot key-package publish / check
|
||||
├── GroupCommands.kt # marmot group create/list/show/…
|
||||
├── GroupCreateCommand.kt
|
||||
├── GroupReadCommands.kt
|
||||
├── GroupAddMemberCommand.kt
|
||||
├── GroupMembershipCommands.kt
|
||||
├── GroupMetadataCommands.kt
|
||||
├── MessageCommands.kt # marmot message send / list
|
||||
└── AwaitCommands.kt # poll-until-condition helpers
|
||||
```
|
||||
|
||||
**Dependencies:** `:quartz` + `:commons` + kotlinx-coroutines + OkHttp +
|
||||
Jackson. No Android, no Compose. This is intentional — `amy` compiles
|
||||
on any JDK 21 target, including CI runners and headless interop hosts.
|
||||
**Dependencies:** `:quartz` + `:commons` + kotlinx-coroutines + OkHttp
|
||||
+ Jackson. **No Android, no Compose.** Amy compiles on any JDK 21
|
||||
host. Never add a Gradle dependency on `:amethyst` or `:desktopApp`.
|
||||
|
||||
**Context.kt is the backbone.** Most commands follow this template:
|
||||
**`Context.kt` is the backbone.** Most commands follow this template:
|
||||
|
||||
```kotlin
|
||||
val ctx = Context.open(dataDir)
|
||||
@@ -103,88 +83,41 @@ try {
|
||||
}
|
||||
```
|
||||
|
||||
Keep new commands in that shape.
|
||||
|
||||
---
|
||||
|
||||
## 4. Current surface vs. Amethyst surface
|
||||
|
||||
What Amy can drive today (✅), what's clearly gap (🆕), and where the
|
||||
logic already lives in `commons/` ready to be called (📦).
|
||||
|
||||
| Area | Status | Notes |
|
||||
|---|---|---|
|
||||
| Identity create / import (`nsec`, `ncryptsec`, mnemonic, `npub`, `nprofile`, hex, NIP-05) | ✅ | `LoginCommand` + Quartz NIP-05 / NIP-06 / NIP-49 |
|
||||
| Account bootstrap (nine events) | ✅ | `commons/account/AccountBootstrapEvents.kt` |
|
||||
| Relay config + NIP-65 / NIP-10050 publish | ✅ | `RelayCommands` |
|
||||
| MLS KeyPackage publish + fetch | ✅ | `commons/marmot/MarmotManager` |
|
||||
| Marmot group create / add / rename / promote / demote / remove / leave | ✅ | `commons/marmot/` |
|
||||
| Marmot message send / list | ✅ | `commons/marmot/` |
|
||||
| `await` polling for KP / group / member / admin / message / rename / epoch | ✅ | `AwaitCommands` |
|
||||
| NIP-01 note publish (`amy note publish TEXT`) | 🆕 | Needs a `commons/` builder wrapper. |
|
||||
| NIP-01 feed read (`amy feed home`, `amy feed hashtag #X`, `amy feed profile NPUB`) | 🆕 | Extract `FeedFilter` usage from `amethyst/ui/dal/` into `commons/` entry points. |
|
||||
| NIP-02 follow list add / remove / list | 🆕 | Logic in `amethyst/model/nip02FollowLists/`. |
|
||||
| NIP-09 event deletion | 🆕 | Builder exists in quartz. |
|
||||
| NIP-17 DMs send / list / read | 🆕 | Gift-wrap path is already in `commons/` via Marmot — generalise. |
|
||||
| NIP-18 reposts / quotes | 🆕 | |
|
||||
| NIP-25 reactions | 🆕 | |
|
||||
| NIP-51 lists (bookmarks, mute, follow sets) | 🆕 | `amethyst/model/nip51Lists/` |
|
||||
| NIP-57 zaps (send + verify) | 🆕 | Needs LN-URL plumbing; `amethyst/service/lnurl/`. |
|
||||
| NIP-65 outbox model queries | 🆕 | |
|
||||
| NIP-72 communities | 🆕 | |
|
||||
| NIP-78 app-specific data (settings sync) | 🆕 | |
|
||||
| Long-form (NIP-23) publish / read | 🆕 | |
|
||||
| Live activities / chess (NIP-53 / NIP-64) | 🆕 | |
|
||||
| Blossom uploads (NIP-B7) | 🆕 | |
|
||||
| NIP-47 Wallet Connect | 🆕 | |
|
||||
| NIP-46 bunker signer | 🆕 | Would need a `signers` abstraction in Amy. |
|
||||
| Profile view (`amy profile show NPUB`) | 🆕 | Renderer work — see §6. |
|
||||
| Thread view (`amy thread show EVENT_ID`) | 🆕 | |
|
||||
| Notifications feed | 🆕 | |
|
||||
| Search (NIP-50) | 🆕 | |
|
||||
|
||||
Treat this as a live checklist — update it in the same PR that closes
|
||||
a gap.
|
||||
|
||||
---
|
||||
|
||||
## 5. How to add a command
|
||||
## How to add a command
|
||||
|
||||
Rule of thumb: **no new logic in `cli/`**. Every command is an
|
||||
assembly of things that already work elsewhere. Follow these steps.
|
||||
assembly of things that already work elsewhere.
|
||||
|
||||
### 5.1. Audit (mandatory)
|
||||
### 1. Audit (mandatory)
|
||||
|
||||
Before you write anything, answer three questions:
|
||||
Before writing anything, answer three questions:
|
||||
|
||||
1. Is the Nostr-protocol piece (event kind, tags, encryption) already
|
||||
in `quartz/`? If not, add it there first.
|
||||
1. Is the Nostr-protocol piece (event kind, tags, encryption)
|
||||
already in `quartz/`? If not, add it there first.
|
||||
2. Is the business logic (state, default values, ordering, filter
|
||||
assembly) already in `commons/`? If not, **extract it from
|
||||
`amethyst/` into `commons/`** in a preceding commit.
|
||||
assembly) already in `commons/`? If not, extract it from
|
||||
`amethyst/` into `commons/` in a preceding commit. See the
|
||||
[extraction recipe](#extract-from-android) below.
|
||||
3. What is the smallest signed event or query this command has to
|
||||
produce? Write that down — it is the JSON your command will echo.
|
||||
produce? That shape is the JSON your command will echo.
|
||||
|
||||
If `commons/` doesn't have the helper you need, see §5.2 before
|
||||
touching `cli/`.
|
||||
### 2. Extract from Android
|
||||
|
||||
### 5.2. Extract-from-Android checklist
|
||||
|
||||
This is the single most important recurring task for Amy's growth.
|
||||
Most Amethyst features today live in `amethyst/src/main/java/…/model/`
|
||||
or `…/service/` with Android-only imports (Context, SharedPreferences,
|
||||
WorkManager). Amy cannot call those directly — they have to move.
|
||||
|
||||
Extraction recipe:
|
||||
The single most important recurring task for Amy's growth. Most
|
||||
Amethyst features today live in `amethyst/src/main/java/…/model/` or
|
||||
`…/service/` with Android-only imports (`Context`, `SharedPreferences`,
|
||||
`WorkManager`, `Log`, `Bitmap`). Amy cannot call those directly — they
|
||||
have to move.
|
||||
|
||||
1. Identify the class in `amethyst/` (e.g. `ReactionPost.kt`).
|
||||
2. List its Android dependencies. The usual offenders: `Context`,
|
||||
`SharedPreferences`, `WorkManager`, `Log`, `Bitmap`.
|
||||
3. For each one, choose:
|
||||
- **Inline-able** (one call, trivial): delete it.
|
||||
- **Platform-abstractable**: add an `expect`/`actual` in
|
||||
`commons/commonMain/…` + `commons/androidMain/…` + `commons/jvmMain/…`.
|
||||
(See `kotlin-multiplatform` skill.)
|
||||
2. List its Android dependencies.
|
||||
3. For each dependency, choose:
|
||||
- **Inline-able** (one call, trivial): delete.
|
||||
- **Platform-abstractable**: add `expect`/`actual` in
|
||||
`commons/commonMain/…` + `commons/androidMain/…` +
|
||||
`commons/jvmMain/…`. (See `kotlin-multiplatform` skill.)
|
||||
- **Inversion-of-control**: take it as a constructor arg. Amy
|
||||
supplies a JVM flavour.
|
||||
4. Move the file to `commons/commonMain/…`.
|
||||
@@ -195,7 +128,7 @@ Extraction recipe:
|
||||
side-effects (notifications, background services, camera, Intents).
|
||||
Everything else is a candidate to move.
|
||||
|
||||
### 5.3. Command file template
|
||||
### 3. Command file template
|
||||
|
||||
```kotlin
|
||||
package com.vitorpamplona.amethyst.cli.commands
|
||||
@@ -234,155 +167,73 @@ object NoteCommands {
|
||||
}
|
||||
```
|
||||
|
||||
Then wire it in `Commands.kt` and add a top-level branch in `Main.kt`'s
|
||||
`dispatch`. Extend `printUsage()`.
|
||||
Wire it into `Commands.kt`, add a top-level branch in `Main.kt`'s
|
||||
`dispatch`, and extend `printUsage()`. Keep [README.md](./README.md)'s
|
||||
command table and [ROADMAP.md](./ROADMAP.md)'s parity matrix in sync.
|
||||
|
||||
### 5.4. Output-shape conventions
|
||||
### 4. Output-shape conventions
|
||||
|
||||
- Top-level object always.
|
||||
- Stable snake_case keys.
|
||||
- Event IDs as hex strings (not npub-style).
|
||||
- Pubkeys as hex (`"pubkey":…`) **and** bech32 when it's the primary
|
||||
subject of the output (`"npub":…`).
|
||||
- Pubkeys as hex (`"pubkey":…`) **and** bech32 when the pubkey is the
|
||||
primary subject (`"npub":…`).
|
||||
- Relay URLs as strings, normalized, never objects.
|
||||
- Lists of events are under a plural key (`"messages"`, `"members"`).
|
||||
- Lists of events under a plural key (`"messages"`, `"members"`).
|
||||
- Errors via `Json.error("code","detail")` — single lower_snake code,
|
||||
free-form detail.
|
||||
|
||||
---
|
||||
|
||||
## 6. Rendering Nostr events
|
||||
## Testing
|
||||
|
||||
Amy has to render every Nostr kind Amethyst understands, in a way that:
|
||||
|
||||
- a human reading a terminal can parse at a glance,
|
||||
- an agent can parse mechanically,
|
||||
- an interop test can diff against a fixture.
|
||||
|
||||
**Plan:**
|
||||
|
||||
1. Build an `EventRenderer` interface in
|
||||
`commons/commonMain/.../rendering/` with one `render(event, ctx)`
|
||||
call returning a structured `RenderedEvent` (title, summary, body,
|
||||
mentions, media refs, etc) — not a string.
|
||||
2. Register a renderer per kind, keyed by `event.kind`. Default
|
||||
renderer dumps raw tags + content. Specialised renderers cover the
|
||||
kinds Amethyst displays specially (kind:0 metadata, kind:1 notes,
|
||||
kind:6 reposts, kind:7 reactions, kind:9/445 Marmot, kind:1059
|
||||
gift-wraps once unwrapped, kind:10002, kind:30023, kind:30043,
|
||||
kind:30311 live activities, etc).
|
||||
3. Reuse these renderers from the Desktop app too — the `RenderedEvent`
|
||||
structure is the input to Compose views and to Amy's JSON, via two
|
||||
different formatters.
|
||||
4. Amy's formatter serialises `RenderedEvent` to JSON with a stable
|
||||
schema. A second formatter produces a human pretty-print (`amy note
|
||||
show EID --format text`).
|
||||
|
||||
This is the single biggest cross-cutting work item. It benefits the
|
||||
Desktop app and Amy simultaneously, and it unlocks a huge chunk of the
|
||||
"🆕" rows in §4 cheaply, because once the renderer exists, a feed
|
||||
command is just "query, render each, emit list".
|
||||
|
||||
---
|
||||
|
||||
## 7. Testing Amy
|
||||
|
||||
Most of what Amy does is exercised by tests in `quartz` and `commons`
|
||||
already — the protocol, the builders, the state machines. The thin
|
||||
Most of what Amy does is already exercised by tests in `quartz` and
|
||||
`commons` — the protocol, the builders, the state machines. The thin
|
||||
Amy-specific layer still needs its own coverage:
|
||||
|
||||
| Layer | Test approach |
|
||||
|---|---|
|
||||
| Argument parsing (`Args`, flag forms, `--data-dir=…` vs `--data-dir …`) | Plain JVM unit tests in `cli/src/test/kotlin/`. |
|
||||
| Error / exit-code contract (bad args → 2, await timeout → 124, runtime → 1) | Table-driven unit tests invoking `main(argv)` with a captured stdout/stderr. |
|
||||
| JSON output shape (each command's keys and types) | Snapshot tests: run a command against a throwaway data-dir, assert the emitted JSON matches a golden file. |
|
||||
| File layout on disk (`identity.json`, `relays.json`, `groups/*.mls`, `keypackages.bundle`) | Structural assertions after running a sequence of commands. |
|
||||
| Round-trip between two data-dirs on a local ephemeral relay | End-to-end shell tests under `cli/src/test/resources/scripts/`. Spin up a local relay (e.g. `nostr-rs-relay`), run Alice + Bob, assert await verbs resolve. |
|
||||
| Interop with other clients | A separate harness repo consumes `amy` as a binary; out of scope for this module's own test suite but the JSON contract is what keeps it stable. |
|
||||
| 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) | Snapshot tests: run a command against a throwaway data-dir, assert the JSON matches a golden file. |
|
||||
| File layout on disk (`identity.json`, `relays.json`, `groups/*.mls`, `keypackages.bundle`) | Structural assertions after a command sequence. |
|
||||
| Round-trip between two data-dirs on a local relay | End-to-end shell scripts under `cli/src/test/resources/scripts/`. Spin up `nostr-rs-relay`, run Alice + Bob, assert await verbs resolve. |
|
||||
| Interop with other clients | External harness consumes Amy as a binary; out of scope here but the JSON contract is what keeps it stable. |
|
||||
|
||||
**What not to test here:** event signing, filter assembly, MLS
|
||||
correctness, NIP-44 encryption. Those belong in `quartz`/`commons`.
|
||||
If an Amy bug can only be caught by a test here, it's likely a
|
||||
contract violation (wrong key name, wrong exit code) rather than a
|
||||
protocol bug.
|
||||
If an Amy bug can only be caught here, it's a contract violation
|
||||
(wrong key name, wrong exit code), not a protocol bug.
|
||||
|
||||
**Setup hooks for a test suite:**
|
||||
- Add a `testImplementation` block to `cli/build.gradle.kts`
|
||||
(`kotlin("test")`, `junit5`, `jackson`).
|
||||
- Launch the CLI via `main(argv)` in-process for fast tests; launch
|
||||
the built `installDist` launcher for end-to-end tests.
|
||||
- Use `@TempDir` for the data-dir so tests can run in parallel.
|
||||
**Interop-test script template:**
|
||||
|
||||
```bash
|
||||
set -euo pipefail
|
||||
TMP=$(mktemp -d)
|
||||
A=$TMP/alice; B=$TMP/bob
|
||||
|
||||
amy --data-dir "$A" create --name Alice
|
||||
amy --data-dir "$B" create --name Bob
|
||||
|
||||
# ... the scenario under test ...
|
||||
|
||||
amy --data-dir "$B" marmot await message "$GID" --match "hello" --timeout 60
|
||||
```
|
||||
|
||||
If an Amethyst scenario cannot be scripted through Amy yet, that's
|
||||
a gap — add it to [ROADMAP.md](./ROADMAP.md).
|
||||
|
||||
---
|
||||
|
||||
## 8. Distribution
|
||||
|
||||
Today Amy runs via `./gradlew :cli:run` or `installDist`. That is fine
|
||||
for dogfooding but not for the interop-test audience, which needs a
|
||||
single-binary install on every OS. Target matrix:
|
||||
|
||||
| OS / channel | Strategy | Notes |
|
||||
|---|---|---|
|
||||
| macOS (arm64 + x64) | `brew install amy-nostr` | Homebrew formula pointing at a tarball of the `installDist` tree + a native `jlink` runtime. Mirrors `amethyst-nostr` cask. |
|
||||
| Windows | `winget install VitorPamplona.Amy` + `scoop install amy` | `.zip` with `amy.bat` launcher + jlink runtime. |
|
||||
| Debian / Ubuntu | `.deb` via `jpackage --type deb` | Depends on libc only; jlink runtime bundled. |
|
||||
| Fedora / RHEL / openSUSE | `.rpm` via `jpackage --type rpm` | Same. |
|
||||
| Arch | AUR `amy-nostr-bin` | Wrap the `.tar.gz`. |
|
||||
| Any Linux | `.tar.gz` + AppImage | AppImage for users without a package manager. |
|
||||
| Nix / NixOS | `nixpkgs` entry | Straightforward wrapper around `installDist`. |
|
||||
| Zapstore | `zapstore.yaml` entry (already exists for Amethyst) | Signed by the same Nostr key as the Android app. |
|
||||
| GitHub Release | Every version ships the above as release assets | Use `scripts/asset-name.sh` for consistent naming. |
|
||||
|
||||
**Shortest path:** extend the existing desktop `jpackage` flow in
|
||||
`desktopApp/` to also produce an `amy-<version>-<os>-<arch>` artefact.
|
||||
No native image yet — GraalVM `native-image` is tempting for startup
|
||||
time but loses FFI to `secp256k1-kmp-jni-*`; revisit once Quartz has a
|
||||
pure-Kotlin signer fallback.
|
||||
|
||||
**Auto-update:** out of scope for v1. Package managers handle it.
|
||||
|
||||
**Size budget:** target < 80 MB installed with a jlink'd runtime. If
|
||||
we cross that, audit transitive deps — Amy should not pull in Compose
|
||||
or Android libs.
|
||||
|
||||
---
|
||||
|
||||
## 9. Roadmap (order of operations)
|
||||
|
||||
A rough proposed sequencing. Each step is a PR.
|
||||
|
||||
1. **Event rendering core** (`commons/commonMain/.../rendering/`) with
|
||||
renderers for kind:0 / 1 / 3 / 6 / 7 / 10002 / 10050. Unblocks
|
||||
everything in §4 marked 🆕.
|
||||
2. **`amy note publish` / `amy note show` / `amy note react`.** Smallest
|
||||
possible end-to-end write+read loop outside of Marmot.
|
||||
3. **`amy feed home|profile|hashtag|thread`** reading through the
|
||||
renderer.
|
||||
4. **`amy follow add|remove|list`** (NIP-02) — proves extraction of
|
||||
list-building logic from `amethyst/model/`.
|
||||
5. **`amy dm send|list`** (NIP-17) — reuses the gift-wrap path already
|
||||
exercised by Marmot.
|
||||
6. **`amy list bookmarks|mute|pin …`** (NIP-51).
|
||||
7. **`amy zap send|verify`** (NIP-57).
|
||||
8. **Distribution** — Homebrew + Scoop + `.deb` in the same release
|
||||
pipeline as desktop.
|
||||
9. **Test suite** end-to-end against a local relay.
|
||||
10. **Everything else** in §4.
|
||||
|
||||
Each step should extract at least one file from `amethyst/` into
|
||||
`commons/`. If a step doesn't move anything, it's probably duplicating
|
||||
logic — re-audit.
|
||||
|
||||
---
|
||||
|
||||
## 10. House-keeping
|
||||
## Housekeeping
|
||||
|
||||
- Run `./gradlew spotlessApply` before every commit.
|
||||
- Keep `printUsage()` in `Main.kt` in sync with the command table in
|
||||
[README.md](./README.md) and §4 above — the three drift apart fast.
|
||||
- Never add a Gradle dependency on `amethyst/` or `desktopApp/`. If you
|
||||
need something from there, move it to `commons/`.
|
||||
- Keep three things in sync: `printUsage()` in `Main.kt`, the command
|
||||
table in [README.md](./README.md), and the parity matrix in
|
||||
[ROADMAP.md](./ROADMAP.md). They drift fast.
|
||||
- Never add a Gradle dependency on `:amethyst` or `:desktopApp`. If
|
||||
you need something from there, move it to `commons/` first.
|
||||
- Never introduce a blocking prompt (`readLine()`, interactive
|
||||
password input). Take it as a flag.
|
||||
- Keep each command file small. When one grows past ~200 lines,
|
||||
split it — the Marmot `group` verbs are already a cautionary tale.
|
||||
- Keep each command file small. Past ~200 lines, split — the Marmot
|
||||
`group` verbs are already a cautionary tale.
|
||||
|
||||
+39
-84
@@ -5,11 +5,11 @@ same Nostr protocol as the Android and Desktop apps, shares the same
|
||||
`quartz` and `commons` code, and aims to eventually expose every feature
|
||||
the GUI offers as a command you can script.
|
||||
|
||||
Amy exists to serve three audiences:
|
||||
Amy exists for three audiences at once:
|
||||
|
||||
1. **Humans** who want to use Amethyst from a terminal or remote shell.
|
||||
2. **Agents / LLMs** that need a deterministic, JSON-typed interface to
|
||||
drive a Nostr account.
|
||||
1. **Humans** using Amethyst from a terminal or remote shell.
|
||||
2. **Agents / LLMs** driving a Nostr account through a deterministic,
|
||||
JSON-typed interface — no interactive prompts, no screen scraping.
|
||||
3. **Interop test harnesses** that put Amethyst side-by-side with the
|
||||
other ~100 Nostr clients publishing and consuming the same events.
|
||||
Any flow that is tested in the Amethyst app should be reproducible
|
||||
@@ -17,43 +17,45 @@ Amy exists to serve three audiences:
|
||||
|
||||
> Today Amy covers identity, relay config, account bootstrap, and
|
||||
> Marmot / MLS group chat (MIP-00 / NIP-445). Everything else from the
|
||||
> Android app is on the roadmap — see [DEVELOPMENT.md](./DEVELOPMENT.md).
|
||||
> Android app is on the roadmap — see [ROADMAP.md](./ROADMAP.md).
|
||||
>
|
||||
> To extend Amy, see [DEVELOPMENT.md](./DEVELOPMENT.md).
|
||||
|
||||
---
|
||||
|
||||
## Design contract
|
||||
## Output contract
|
||||
|
||||
- **Non-interactive.** One invocation, one job, exits cleanly. No REPL,
|
||||
no daemon, no hidden prompts. If a subcommand would need to block
|
||||
waiting for something on the network, it is an explicit `await` verb
|
||||
with a `--timeout`.
|
||||
- **stdout is JSON. One line. One object.** Always. Pipe it into `jq`,
|
||||
parse it from Python, feed it to an agent — the shape is stable.
|
||||
- **stderr is for humans.** Progress logs, warnings, `[cli] ingest …`
|
||||
traces all go to stderr and are safe to discard.
|
||||
What every caller — user, script, agent, CI — can rely on:
|
||||
|
||||
- **stdout is JSON. One line. One object.** Stable snake_case keys.
|
||||
Pipe it into `jq`, parse it from Python, hand it to an agent.
|
||||
- **stderr is for humans.** Progress, warnings, per-relay ACK traces.
|
||||
Safe to discard.
|
||||
- **Exit codes are the real signal.**
|
||||
- `0` — success
|
||||
- `1` — runtime error (JSON `{"error":"…","detail":"…"}` on stderr)
|
||||
- `2` — bad arguments
|
||||
- `124` — `await` timed out
|
||||
- **Data-dir is the whole world.** All persisted state (identity,
|
||||
relays, MLS epochs, message archives, run cursors) lives under
|
||||
`--data-dir PATH`. Delete it to reset; copy it to move; `AMETHYST_CLI_DATA`
|
||||
env var overrides the default `./amethyst-cli-data`.
|
||||
- **No interactive prompts, ever.** Passwords, names, keys — all flags.
|
||||
- **Data-dir is the whole world.** All state (identity, relays, MLS
|
||||
epochs, message archives, run cursors) lives under `--data-dir PATH`.
|
||||
Delete to reset; copy to move; `AMETHYST_CLI_DATA` env var overrides
|
||||
the default `./amethyst-cli-data`.
|
||||
|
||||
Those rules are not incidental — they make `amy` cleanly consumable
|
||||
from CI, from agents, and from other Nostr clients running the same
|
||||
test matrix.
|
||||
The rationale behind each of these lives in
|
||||
[DEVELOPMENT.md](./DEVELOPMENT.md). Breaking any of them is a breaking
|
||||
change to Amy's public API.
|
||||
|
||||
---
|
||||
|
||||
## Install
|
||||
|
||||
Until Amy ships as a signed native binary (see the "Distribution" section
|
||||
in [DEVELOPMENT.md](./DEVELOPMENT.md)), run it from source:
|
||||
Until Amy ships as a signed native binary (see
|
||||
[cli/plans/2026-04-21-cli-distribution.md](./plans/2026-04-21-cli-distribution.md)),
|
||||
run it from source:
|
||||
|
||||
```bash
|
||||
# One-shot run — positional args go after `--args`, quoted as a single string
|
||||
# One-shot run — positional args go after `--args`, quoted as one string
|
||||
./gradlew :cli:run --quiet --args="whoami"
|
||||
|
||||
# Or build a runnable distribution and use the generated launch script
|
||||
@@ -62,8 +64,7 @@ in [DEVELOPMENT.md](./DEVELOPMENT.md)), run it from source:
|
||||
```
|
||||
|
||||
The `installDist` tree under `cli/build/install/amy/` is self-contained
|
||||
(just a JVM launcher + jars) and is what downstream packaging
|
||||
(Homebrew formula, `.deb`, `.msi`, etc.) will wrap.
|
||||
(JVM launcher + jars) and is what downstream packaging will wrap.
|
||||
|
||||
**Requirements:** JDK 21.
|
||||
|
||||
@@ -74,11 +75,10 @@ The `installDist` tree under `cli/build/install/amy/` is self-contained
|
||||
```bash
|
||||
# 1. Create a data-dir with a full Amethyst-style account.
|
||||
# Generates a keypair, seeds default NIP-65 / inbox / key-package
|
||||
# relays, and publishes the nine bootstrap events (profile, contacts,
|
||||
# relay lists, etc).
|
||||
# relays, and publishes the nine bootstrap events.
|
||||
amy --data-dir ./alice create --name "Alice"
|
||||
|
||||
# 2. Publish a fresh MLS KeyPackage so other users can invite you.
|
||||
# 2. Publish a fresh MLS KeyPackage so others can invite you.
|
||||
amy --data-dir ./alice marmot key-package publish
|
||||
|
||||
# 3. Create a group, invite someone, send a message.
|
||||
@@ -91,16 +91,18 @@ amy --data-dir ./bob marmot await group --name "Test Group" --timeout 60
|
||||
amy --data-dir ./bob marmot message list <GID>
|
||||
```
|
||||
|
||||
Every line above prints a single JSON object on success. Compose them
|
||||
with `jq` to extract IDs:
|
||||
Compose with `jq` to chain commands:
|
||||
|
||||
```bash
|
||||
GID=$(amy --data-dir ./alice marmot group create --name "Test" | jq -r .group_id)
|
||||
```
|
||||
|
||||
For an interop-test script template, see
|
||||
[DEVELOPMENT.md § Testing](./DEVELOPMENT.md#testing).
|
||||
|
||||
---
|
||||
|
||||
## Full command reference
|
||||
## Command reference
|
||||
|
||||
Run `amy --help` for the canonical list. As of today:
|
||||
|
||||
@@ -116,7 +118,7 @@ Run `amy --help` for the canonical list. As of today:
|
||||
| `marmot key-package publish` | Publish a fresh MLS KeyPackage (kind:30443). |
|
||||
| `marmot key-package check NPUB` | Fetch someone else's KeyPackage from their advertised relays. |
|
||||
| `marmot group create [--name NAME]` | New empty group with you as sole admin. |
|
||||
| `marmot group list` | All groups you're currently a member of. |
|
||||
| `marmot group list` | All groups you're a member of. |
|
||||
| `marmot group show GID` | Full group state (members, admins, epoch, metadata). |
|
||||
| `marmot group members GID` | Members only. |
|
||||
| `marmot group admins GID` | Admins only. |
|
||||
@@ -137,8 +139,8 @@ Run `amy --help` for the canonical list. As of today:
|
||||
| `marmot await epoch GID --min N` | Block until GID's MLS epoch is ≥ N. |
|
||||
|
||||
All `await` verbs accept `--timeout SECS` (default 30). Timeout exits 124
|
||||
so scripts can tell the difference between "condition never happened"
|
||||
and "the command itself crashed".
|
||||
so scripts can distinguish "condition never happened" from "the command
|
||||
itself crashed".
|
||||
|
||||
### Global flags
|
||||
|
||||
@@ -162,54 +164,7 @@ and "the command itself crashed".
|
||||
```
|
||||
|
||||
All files are plain JSON or framed binary — human-inspectable, easy to
|
||||
diff across two accounts in a test run.
|
||||
|
||||
---
|
||||
|
||||
## Use from agents
|
||||
|
||||
Amy is intentionally easy for an LLM to drive:
|
||||
|
||||
1. Every stdout is one JSON object — `jq`-ready, schema-stable.
|
||||
2. Errors are JSON too, so "did it work?" is a machine question.
|
||||
3. No interactive prompts — even password input takes `--password`.
|
||||
4. `await` verbs mean an agent can say "send this, then wait for the
|
||||
other account to see it" without implementing its own polling.
|
||||
|
||||
Recommended agent loop:
|
||||
|
||||
```text
|
||||
plan → amy <cmd> --data-dir A → parse JSON → amy <cmd> --data-dir B …
|
||||
```
|
||||
|
||||
When Amy prints `{"error":"bad_args",…}` or exits 2, the agent should
|
||||
re-read `--help` rather than retry blindly.
|
||||
|
||||
---
|
||||
|
||||
## Use from interop tests
|
||||
|
||||
The test goal that drives Amy's roadmap: every canonical Amethyst
|
||||
user-flow should be expressible as a short shell script that can also
|
||||
be aimed at any other Nostr client with a similar harness.
|
||||
|
||||
Template:
|
||||
|
||||
```bash
|
||||
set -euo pipefail
|
||||
TMP=$(mktemp -d)
|
||||
A=$TMP/alice; B=$TMP/bob
|
||||
|
||||
amy --data-dir "$A" create --name Alice
|
||||
amy --data-dir "$B" create --name Bob
|
||||
|
||||
# ... the scenario under test ...
|
||||
|
||||
amy --data-dir "$B" marmot await message "$GID" --match "hello" --timeout 60
|
||||
```
|
||||
|
||||
If an Amethyst scenario cannot be scripted through `amy` yet, that is a
|
||||
gap to close — see the roadmap in [DEVELOPMENT.md](./DEVELOPMENT.md).
|
||||
diff across two data-dirs in a test run.
|
||||
|
||||
---
|
||||
|
||||
@@ -225,4 +180,4 @@ gap to close — see the roadmap in [DEVELOPMENT.md](./DEVELOPMENT.md).
|
||||
operation has a timeout — use `--timeout` for `await`, or wrap the
|
||||
whole command in `timeout(1)` if you're scripting.
|
||||
- **Nothing seems to publish** — inspect stderr; each publish prints
|
||||
per-relay `OK` / `REJECT` via the `[cli] ingest …` traces.
|
||||
per-relay `OK` / `REJECT` via the `[cli] …` traces.
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
# Amy Roadmap
|
||||
|
||||
The live checklist for growing `amy` from a Marmot test-bed into a
|
||||
full command-line mirror of Amethyst.
|
||||
|
||||
**How to use this file:** update it in the same PR that ships a
|
||||
feature. Move rows between tables, adjust ordering, add non-goals.
|
||||
This is the single source of truth for "what's left".
|
||||
|
||||
- How the CLI is used today: [README.md](./README.md)
|
||||
- How to implement an item: [DEVELOPMENT.md](./DEVELOPMENT.md)
|
||||
- Ongoing design plans: [plans/](./plans/)
|
||||
- Shared work consumed here: [../commons/plans/](../commons/plans/)
|
||||
|
||||
---
|
||||
|
||||
## North-star goal
|
||||
|
||||
> For every feature of the Amethyst Android app, there is a way to
|
||||
> exercise it through `amy`, with byte-identical on-relay behaviour.
|
||||
|
||||
Why:
|
||||
|
||||
- Interop tests against the ~100 other Nostr clients need a
|
||||
reproducible harness that does not require running Android.
|
||||
- Agents and LLMs can script real Amethyst flows without a GUI.
|
||||
- Regressions in shared logic (signing, encryption, filter building,
|
||||
event parsing) become shell-scriptable.
|
||||
- Power users get a command-line Amethyst for free.
|
||||
|
||||
**Non-goal:** Amy is not a second Nostr client implementation. It is
|
||||
a thin assembly layer over `quartz` + `commons`. Protocol and business
|
||||
logic in `cli/` are bugs.
|
||||
|
||||
---
|
||||
|
||||
## Parity matrix
|
||||
|
||||
Status legend: ✅ shipped · 📦 logic lives in `commons/`, needs a command ·
|
||||
🆕 needs extraction from `amethyst/` first · ⚠️ blocked (see Notes).
|
||||
|
||||
| Area | Status | Notes |
|
||||
|---|---|---|
|
||||
| Identity create / import (`nsec`, `ncryptsec`, mnemonic, `npub`, `nprofile`, hex, NIP-05) | ✅ | `LoginCommand` + Quartz NIP-05 / NIP-06 / NIP-49 |
|
||||
| Account bootstrap (nine events) | ✅ | `commons/account/AccountBootstrapEvents.kt` |
|
||||
| Relay config + NIP-65 / NIP-10050 publish | ✅ | `RelayCommands` |
|
||||
| MLS KeyPackage publish + fetch | ✅ | `commons/marmot/MarmotManager` |
|
||||
| Marmot group create / add / rename / promote / demote / remove / leave | ✅ | `commons/marmot/` |
|
||||
| Marmot message send / list | ✅ | `commons/marmot/` |
|
||||
| `await` polling (KP / group / member / admin / message / rename / epoch) | ✅ | `AwaitCommands` |
|
||||
| NIP-01 note publish (`amy note publish TEXT`) | 🆕 | Needs a `commons/` builder wrapper. |
|
||||
| NIP-01 feed read (`amy feed home`, `amy feed hashtag #X`, `amy feed profile NPUB`) | 🆕 | Extract `FeedFilter` usage from `amethyst/ui/dal/` into `commons/` entry points. |
|
||||
| NIP-02 follow list add / remove / list | 🆕 | Logic in `amethyst/model/nip02FollowLists/`. |
|
||||
| NIP-09 event deletion | 🆕 | Builder exists in quartz. |
|
||||
| NIP-17 DMs send / list / read | 🆕 | Gift-wrap path is already in `commons/` via Marmot — generalise. |
|
||||
| NIP-18 reposts / quotes | 🆕 | |
|
||||
| NIP-25 reactions | 🆕 | |
|
||||
| NIP-51 lists (bookmarks, mute, follow sets) | 🆕 | `amethyst/model/nip51Lists/` |
|
||||
| NIP-57 zaps (send + verify) | 🆕 | Needs LN-URL plumbing; `amethyst/service/lnurl/`. |
|
||||
| NIP-65 outbox model queries | 🆕 | |
|
||||
| NIP-72 communities | 🆕 | |
|
||||
| NIP-78 app-specific data (settings sync) | 🆕 | |
|
||||
| Long-form (NIP-23) publish / read | 🆕 | |
|
||||
| Live activities / chess (NIP-53 / NIP-64) | 🆕 | |
|
||||
| Blossom uploads (NIP-B7) | 🆕 | |
|
||||
| NIP-47 Wallet Connect | 🆕 | |
|
||||
| NIP-46 bunker signer | 🆕 | Needs a signers abstraction in Amy. |
|
||||
| Profile view (`amy profile show NPUB`) | ⚠️ | Blocked on event-renderer plan in `commons/plans/`. |
|
||||
| Thread view (`amy thread show EVENT_ID`) | ⚠️ | Same. |
|
||||
| Notifications feed | 🆕 | |
|
||||
| Search (NIP-50) | 🆕 | |
|
||||
|
||||
---
|
||||
|
||||
## Order of operations
|
||||
|
||||
Proposed sequencing. Each step is one PR. Each step should extract
|
||||
at least one file from `amethyst/` into `commons/`; if it doesn't
|
||||
move anything, re-audit — you're probably duplicating logic.
|
||||
|
||||
1. **Event rendering core** in `commons/commonMain/.../rendering/`
|
||||
with renderers for kinds 0 / 1 / 3 / 6 / 7 / 10002 / 10050.
|
||||
Unblocks all the 🆕 and ⚠️ read-path rows below.
|
||||
Design: `commons/plans/2026-04-21-event-renderer.md`.
|
||||
2. **`amy note publish` / `amy note show` / `amy note react`** —
|
||||
smallest end-to-end write+read loop outside Marmot.
|
||||
3. **`amy feed home|profile|hashtag|thread`** reading through the
|
||||
renderer.
|
||||
4. **`amy follow add|remove|list`** (NIP-02) — proves extraction of
|
||||
list-building logic from `amethyst/model/`.
|
||||
5. **`amy dm send|list`** (NIP-17) — reuses the gift-wrap path already
|
||||
exercised by Marmot.
|
||||
6. **`amy list bookmarks|mute|pin …`** (NIP-51).
|
||||
7. **`amy zap send|verify`** (NIP-57).
|
||||
8. **Distribution** — Homebrew + Scoop + `.deb` in the same release
|
||||
pipeline as desktop. Plan: `cli/plans/2026-04-21-cli-distribution.md`.
|
||||
9. **Test suite** — end-to-end against a local relay.
|
||||
10. **Everything else in the matrix.**
|
||||
|
||||
---
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Interactive TUI or REPL.
|
||||
- Native image (GraalVM) until Quartz has a pure-Kotlin signer
|
||||
fallback — `secp256k1-kmp-jni-*` needs JNI today.
|
||||
- A Gradle dependency on `:amethyst` or `:desktopApp`. Ever.
|
||||
- Re-implementing any Nostr protocol piece that's already in
|
||||
`quartz/` or in another client's library.
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: "feat(cli): native distribution across macOS / Windows / Linux"
|
||||
type: feat
|
||||
status: proposed
|
||||
date: 2026-04-21
|
||||
owner: cli
|
||||
---
|
||||
|
||||
# feat(cli): native distribution
|
||||
|
||||
## Overview
|
||||
|
||||
Today `amy` ships only as `./gradlew :cli:run` or a raw `installDist`
|
||||
tree. Fine for dogfooding, not fine for the interop-test audience,
|
||||
who need a single-binary install on every OS.
|
||||
|
||||
Goal: publish `amy-<version>-<os>-<arch>` artefacts on every GitHub
|
||||
release, wrapped in the native package managers users already have.
|
||||
|
||||
## Target matrix
|
||||
|
||||
| OS / channel | Strategy | Notes |
|
||||
|---|---|---|
|
||||
| macOS (arm64 + x64) | `brew install amy-nostr` | Homebrew formula pointing at a tarball of `installDist` + a jlink'd JRE. Mirrors the existing `amethyst-nostr` cask. |
|
||||
| Windows | `winget install VitorPamplona.Amy` + `scoop install amy` | `.zip` with `amy.bat` launcher + jlink JRE. |
|
||||
| Debian / Ubuntu | `.deb` via `jpackage --type deb` | Depends on libc only; jlink JRE bundled. |
|
||||
| Fedora / RHEL / openSUSE | `.rpm` via `jpackage --type rpm` | Same. |
|
||||
| Arch | AUR `amy-nostr-bin` | Wrap the `.tar.gz`. |
|
||||
| Any Linux | `.tar.gz` + AppImage | AppImage for users without a package manager. |
|
||||
| Nix / NixOS | `nixpkgs` entry | Wrapper around `installDist`. |
|
||||
| Zapstore | Extend `zapstore.yaml` | Signed by the same Nostr key as the Android app. |
|
||||
| GitHub Release | All of the above attached as assets | Use `scripts/asset-name.sh` for consistent naming. |
|
||||
|
||||
## Shortest path to first release
|
||||
|
||||
1. Extend the existing `desktopApp` `jpackage` flow to also produce an
|
||||
`amy-<version>-<os>-<arch>` artefact. Reuse the signing and
|
||||
notarisation already configured for the desktop build.
|
||||
2. CI matrix: macos-14 (arm64), macos-13 (x64), windows-latest,
|
||||
ubuntu-latest. Each runner produces one tarball + one native
|
||||
installer (`.dmg` / `.msi` / `.deb`).
|
||||
3. Publish as release assets; no package-manager submission yet.
|
||||
4. Only after the artefacts are stable (no path churn, no JRE
|
||||
incompatibilities): submit Homebrew formula, winget manifest,
|
||||
Scoop bucket, AUR PKGBUILD.
|
||||
|
||||
## Why not GraalVM native-image
|
||||
|
||||
Tempting for startup time but loses FFI to `secp256k1-kmp-jni-*`,
|
||||
which is how Quartz signs today. Revisit when Quartz ships a
|
||||
pure-Kotlin fallback signer.
|
||||
|
||||
## Auto-update
|
||||
|
||||
Out of scope for v1. Package managers handle it (`brew upgrade`,
|
||||
`winget upgrade`, etc). Revisit if manual-install users complain.
|
||||
|
||||
## Size budget
|
||||
|
||||
Target: **< 80 MB installed** with a jlink'd runtime. If we cross
|
||||
that, audit transitive deps — Amy should not pull in Compose or
|
||||
Android libs. Verify with `./gradlew :cli:installDist && du -sh
|
||||
cli/build/install/amy/` on CI and fail the build over a threshold.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Code signing on macOS and Windows** costs real money and requires
|
||||
secrets rotation. Reuse the desktop app's existing signing setup
|
||||
rather than standing up a separate keychain.
|
||||
- **Package-manager review latency.** Homebrew / winget reviews can
|
||||
take days. Ship tarballs first; submissions later.
|
||||
- **JRE size.** A jlink'd JDK 21 image is ~40 MB; adding OkHttp +
|
||||
Jackson + Quartz should land well under the 80 MB budget, but a
|
||||
bad transitive dep can double it overnight.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Single brew cask `amethyst-nostr` with an `amy` formula alongside,
|
||||
or a separate `amy-nostr` tap? Probably separate — different update
|
||||
cadence, different audience.
|
||||
- Should Amy ship in the same release as the desktop app, or on its
|
||||
own cadence? Leaning same release — shared CI, shared version —
|
||||
until the surfaces diverge.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- In-app auto-update UX (no CLI auto-update).
|
||||
- macOS app notarization of the CLI (only signing). Notarization is
|
||||
for bundled `.app` GUIs.
|
||||
Reference in New Issue
Block a user