Files
amethyst/.claude/skills/amy-expert/references/command-template.md
T
Claude 6ef0c372b1 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."
2026-04-25 16:35:10 +00:00

107 lines
3.5 KiB
Markdown

# Command-file template
Copy this shape for every new Amy verb. Resist the urge to deviate —
the uniform shape is what makes commands easy to audit and test.
## Single-verb command
```kotlin
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.Output
object NotePublishCommand {
suspend fun run(dataDir: DataDir, rest: Array<String>): Int {
val args = Args(rest)
val text = args.positional(0, "text")
val ctx = Context.open(dataDir)
try {
ctx.prepare()
val event = com.vitorpamplona.amethyst.commons.note
.buildTextNote(ctx.signer, text)
val ack = ctx.publish(event, ctx.outboxRelays())
Output.emit(mapOf(
"event_id" to event.id,
"kind" to event.kind,
"published_to" to ack.filterValues { it }.keys.map { it.url },
"rejected_by" to ack.filterValues { !it }.keys.map { it.url },
))
return 0
} finally {
ctx.close()
}
}
}
```
`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`,
`note react`), group them:
```kotlin
object NoteCommands {
suspend fun dispatch(dataDir: DataDir, tail: Array<String>): Int {
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 -> Output.error("bad_args", "note ${tail[0]}")
}
}
}
```
Each verb gets its own file. Once a single file crosses ~200 lines,
split it — see `GroupCommands.kt` and its siblings as the reference.
## Wire-up checklist
For every new command:
1. File under `cli/commands/`.
2. Branch in `Commands.kt`:
```kotlin
suspend fun note(dataDir: DataDir, tail: Array<String>): Int =
NoteCommands.dispatch(dataDir, tail)
```
3. Branch in `Main.kt`'s top-level `dispatch`:
```kotlin
"note" -> Commands.note(dataDir, tail)
```
4. Line in `printUsage()` explaining the verb.
5. Row in `cli/README.md`'s command table.
6. Status flip in `cli/ROADMAP.md` (🆕 / 📦 → ✅).
## What not to do
- No `runBlocking` in a command body — `main()` already does it.
- No `println` / `print` for command output — use
`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: …` (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
See `output-conventions.md`.