6ef0c372b1
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."
3.5 KiB
3.5 KiB
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
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:
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:
- File under
cli/commands/. - Branch in
Commands.kt:suspend fun note(dataDir: DataDir, tail: Array<String>): Int = NoteCommands.dispatch(dataDir, tail) - Branch in
Main.kt's top-leveldispatch:"note" -> Commands.note(dataDir, tail) - Line in
printUsage()explaining the verb. - Row in
cli/README.md's command table. - Status flip in
cli/ROADMAP.md(🆕 / 📦 → ✅).
What not to do
- No
runBlockingin a command body —main()already does it. - No
println/printfor command output — useOutput.emit(...)/Output.error(...).System.err.println(...)is fine for progress logs (they're already disposable). - No swallowing errors — let exceptions bubble;
main()translates them toerror: …(text mode) /{"error":…}(JSON mode) plus the right exit code. - No holding a connection open across invocations — every run opens
a fresh
Contextand closes it infinally. - No blocking reads for user input — take a flag.
- No global flags that collide with subcommand flags.
--nameis reserved for subcommand use (group/profile name); the global account selector is--account.
Output-shape rules
See output-conventions.md.