docs(cli): make USAGE.md the README; move contract material to DEVELOPMENT
USAGE.md was the better README — entry-point users want examples and quick start, not the public-API contract. Flip them and refresh the amy-expert skill so it matches the post-refactor reality. cli/README.md (was USAGE.md): - Install, quick start, seven worked examples, full command reference, output modes, multi-account workflows, agent recipes, troubleshooting. - Cross-refs point at DEVELOPMENT.md for the contract / architecture and ROADMAP.md for what's coming. cli/DEVELOPMENT.md absorbs the old README's architecture sections: - New "Public contract" section at the top — the stable promises (text-default + --json contract, stderr for humans, exit codes, ~/.amy/ as the world). - "Local event store" deep-dive with the cache-helper API. - "Relay routing" rules table. - "Full on-disk layout" tree with annotations. cli/ROADMAP.md, cli/USAGE.md: - ROADMAP cross-refs collapsed (no more USAGE.md row). - USAGE.md deleted — content lives in README now. .claude/skills/amy-expert refreshed end-to-end: - SKILL.md description + Rules 2 and 4 rewritten for the dual-output contract (text default, --json opt-in) and the ~/.amy/ layout. - "Where things live" listing matches the current source tree (Output.kt, Aliases.kt, UseCommand.kt, secrets/, all the new command files). - "Common mistakes" lists the new traps: don't read user.home directly, don't add a global flag that collides with subcommand --name, don't use Json.writeLine (it's gone). - references/command-template.md uses Output.emit / Output.error (Json.writeLine / Json.error helpers no longer exist). - references/output-conventions.md rewritten around the dual-mode contract — same JSON shape rules, but framed as "this is what --json emits" rather than "this is stdout."
This commit is contained in:
@@ -11,7 +11,7 @@ package com.vitorpamplona.amethyst.cli.commands
|
||||
import com.vitorpamplona.amethyst.cli.Args
|
||||
import com.vitorpamplona.amethyst.cli.Context
|
||||
import com.vitorpamplona.amethyst.cli.DataDir
|
||||
import com.vitorpamplona.amethyst.cli.Json
|
||||
import com.vitorpamplona.amethyst.cli.Output
|
||||
|
||||
object NotePublishCommand {
|
||||
suspend fun run(dataDir: DataDir, rest: Array<String>): Int {
|
||||
@@ -26,7 +26,7 @@ object NotePublishCommand {
|
||||
.buildTextNote(ctx.signer, text)
|
||||
val ack = ctx.publish(event, ctx.outboxRelays())
|
||||
|
||||
Json.writeLine(mapOf(
|
||||
Output.emit(mapOf(
|
||||
"event_id" to event.id,
|
||||
"kind" to event.kind,
|
||||
"published_to" to ack.filterValues { it }.keys.map { it.url },
|
||||
@@ -40,6 +40,10 @@ object NotePublishCommand {
|
||||
}
|
||||
```
|
||||
|
||||
`Output.emit(...)` handles the text-vs-JSON mode automatically. The
|
||||
result map IS the `--json` shape; the human-readable text default is
|
||||
derived from the same map by `Output.kt`'s renderer.
|
||||
|
||||
## Multi-verb group
|
||||
|
||||
When a feature has several verbs (`note publish`, `note show`,
|
||||
@@ -48,13 +52,13 @@ When a feature has several verbs (`note publish`, `note show`,
|
||||
```kotlin
|
||||
object NoteCommands {
|
||||
suspend fun dispatch(dataDir: DataDir, tail: Array<String>): Int {
|
||||
if (tail.isEmpty()) return Json.error("bad_args", "note <publish|show|react>")
|
||||
if (tail.isEmpty()) return Output.error("bad_args", "note <publish|show|react>")
|
||||
val rest = tail.drop(1).toTypedArray()
|
||||
return when (tail[0]) {
|
||||
"publish" -> NotePublishCommand.run(dataDir, rest)
|
||||
"show" -> NoteShowCommand.run(dataDir, rest)
|
||||
"react" -> NoteReactCommand.run(dataDir, rest)
|
||||
else -> Json.error("bad_args", "note ${tail[0]}")
|
||||
else -> Output.error("bad_args", "note ${tail[0]}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,13 +89,17 @@ For every new command:
|
||||
|
||||
- No `runBlocking` in a command body — `main()` already does it.
|
||||
- No `println` / `print` for command output — use
|
||||
`Json.writeLine(...)`. `System.err.println(...)` is fine for
|
||||
progress logs (they're already disposable).
|
||||
`Output.emit(...)` / `Output.error(...)`. `System.err.println(...)`
|
||||
is fine for progress logs (they're already disposable).
|
||||
- No swallowing errors — let exceptions bubble; `main()` translates
|
||||
them to `{"error":...}` + exit code.
|
||||
them to `error: …` (text mode) / `{"error":…}` (JSON mode) plus the
|
||||
right exit code.
|
||||
- No holding a connection open across invocations — every run opens
|
||||
a fresh `Context` and closes it in `finally`.
|
||||
- No blocking reads for user input — take a flag.
|
||||
- No global flags that collide with subcommand flags. `--name` is
|
||||
reserved for subcommand use (group/profile name); the global
|
||||
account selector is `--account`.
|
||||
|
||||
## Output-shape rules
|
||||
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
# Output conventions
|
||||
|
||||
Amy's JSON contract is its public API. Follow these rules.
|
||||
amy ships a dual-output contract. Default stdout is human-readable
|
||||
text (a YAML-ish render of the underlying result map); `--json` flips
|
||||
stdout to a single JSON object per success. The text shape can drift;
|
||||
the `--json` shape is the public API.
|
||||
|
||||
Commands always emit via `Output.emit(mapOf(...))`. The map IS the
|
||||
JSON shape — the renderer in `Output.kt` derives the text from the
|
||||
same map. Don't write two render paths; write one map and let
|
||||
`Output` pick.
|
||||
|
||||
## Channels
|
||||
|
||||
| Stream | What goes here |
|
||||
|---|---|
|
||||
| **stdout** | Exactly one JSON object per successful invocation. Nothing else. |
|
||||
| **stderr** | Human progress logs, warnings, per-relay ACK traces, stack traces, `printUsage()` output. Safe to discard. Not machine-consumed. |
|
||||
| Stream | Default mode | `--json` mode |
|
||||
|---|---|---|
|
||||
| **stdout** | YAML-ish text from `Output.emit(...)` | Exactly one JSON object per successful invocation |
|
||||
| **stderr** | Human progress logs, warnings, per-relay ACK traces, stack traces, `printUsage()` output, errors as `error: <code>: <detail>` | Same logs, plus errors as `{"error":...,"detail":...}` |
|
||||
|
||||
If a command needs to emit structured data for machines, it goes on
|
||||
stdout. If it needs to explain what it's doing to a human watching,
|
||||
stderr.
|
||||
stdout and is automatically JSON under `--json`. If it needs to
|
||||
explain what it's doing to a human watching, stderr.
|
||||
|
||||
## Exit codes
|
||||
|
||||
| Code | Meaning |
|
||||
|---|---|
|
||||
| `0` | Success. Stdout has a JSON object. |
|
||||
| `1` | Runtime error. Stderr has `{"error":"...","detail":"..."}`. |
|
||||
| `2` | Bad arguments. Stderr has a JSON error object and/or usage. |
|
||||
| `0` | Success. |
|
||||
| `1` | Runtime error. |
|
||||
| `2` | Bad arguments. |
|
||||
| `124` | `await` timed out. |
|
||||
|
||||
Throw the right exception type in commands:
|
||||
@@ -32,7 +40,7 @@ The top-level `main()` in `Main.kt` handles the translation. Don't
|
||||
try-catch at the command level unless you're converting a third-party
|
||||
exception into one of the above.
|
||||
|
||||
## Object shape
|
||||
## `--json` object shape
|
||||
|
||||
### Top-level
|
||||
|
||||
@@ -58,8 +66,9 @@ newline-delimited stream.
|
||||
| Pubkey (primary subject) | hex **and** bech32. Keys: `pubkey` + `npub`. |
|
||||
| Pubkey (secondary reference) | hex only. Key: `pubkey`. |
|
||||
| Relay URL | Normalized string (`wss://…`). Never an object. |
|
||||
| Timestamps | Unix seconds, integer. Key names end in `_at`. |
|
||||
| Timestamps | Unix seconds, integer. Key names end in `_at`. The text renderer auto-formats these as `2026-04-25 13:42:11Z (8m ago)`. |
|
||||
| Group ID (Marmot) | Hex string. Key: `group_id`. |
|
||||
| Byte counts | Integer. Key names end in `_bytes`. The text renderer auto-formats these as `8.7 KiB`. |
|
||||
|
||||
### Collections
|
||||
|
||||
@@ -70,7 +79,8 @@ newline-delimited stream.
|
||||
|
||||
### Booleans
|
||||
|
||||
- Use `true`/`false`, not `0`/`1`, not `"yes"`.
|
||||
- Use `true`/`false` in the result map. The text renderer prints them
|
||||
as `yes`/`no` (green/red); `--json` keeps the literal booleans.
|
||||
- Name keys so `true` is the expected/successful state:
|
||||
`is_member`, `published`, `accepted`.
|
||||
|
||||
@@ -93,22 +103,40 @@ appear in neither — add `timed_out_on` if you need to surface them.
|
||||
|
||||
### Error shape
|
||||
|
||||
Default mode (text):
|
||||
|
||||
```text
|
||||
error: not_member: <gid>
|
||||
```
|
||||
|
||||
Under `--json`:
|
||||
|
||||
```json
|
||||
{ "error": "code", "detail": "free-form explanation" }
|
||||
{ "error": "not_member", "detail": "<gid>" }
|
||||
```
|
||||
|
||||
- `error` is a short, stable, lower_snake code. Agents can branch on
|
||||
it.
|
||||
- `detail` is free text — OK to change between versions.
|
||||
- Common codes today: `bad_args`, `no_identity`, `exists`, `bad_key`,
|
||||
`not_member`, `timeout`, `runtime`. Reuse before inventing.
|
||||
- Common codes today: `bad_args`, `no_identity`, `no_account`,
|
||||
`exists`, `bad_key`, `not_member`, `no_dm_relays`, `timeout`,
|
||||
`runtime`. Reuse before inventing.
|
||||
|
||||
Use `Output.error("code", "detail")` from commands; it picks the
|
||||
right channel and format based on the active mode.
|
||||
|
||||
## Never
|
||||
|
||||
- `println(...)` of anything except `Json.writeLine(...)`.
|
||||
- Multi-line JSON (pretty-printed). One line, always.
|
||||
- Mixing stdout lines — one command invocation emits one stdout line.
|
||||
If you need progress updates, they go on stderr.
|
||||
- `println(...)` of anything except `Output.emit(...)`.
|
||||
- `Json.writeLine` / `Json.error` — that helper is gone; use the
|
||||
`Output` object instead.
|
||||
- Multi-line JSON (pretty-printed) under `--json`. One line, always.
|
||||
- Mixing stdout lines — one command invocation emits one stdout line
|
||||
in `--json` mode. If you need progress updates, they go on stderr.
|
||||
- Machine output to stderr. The whole point is clean separation.
|
||||
- Silent fallbacks — if a relay rejects your publish, say so in the
|
||||
JSON.
|
||||
result map.
|
||||
- Building text rendering by hand. Trust the `Output.kt` renderer:
|
||||
it handles alignment, colour, byte/timestamp formatting, nested
|
||||
maps and lists. If you need a bespoke render for one command,
|
||||
pass a custom render lambda — don't go around `Output`.
|
||||
|
||||
Reference in New Issue
Block a user