Four mechanical simplifications to amy with no change to the public
CLI/JSON contract:
1. Drop the Commands.kt pass-through layer. Main.kt now calls each
command object directly; the file is repurposed into Router.kt,
holding a single shared `route(name, tail, usage, routes)` helper.
2. Replace the `Context.open(dataDir)` + `try { } finally { ctx.close() }`
boilerplate (~46 sites) with `Context.open(dataDir).use { ctx -> }`
now that Context is AutoCloseable.
3. Remove the reflection-based `storeIsInitialized()` in Context; track
the lazy event store via `Lazy.isInitialized()` instead.
4. Route every `*Commands.dispatch` through the `route` helper, dropping
the repeated empty-check + unknown-verb `when` boilerplate.
Net -282 lines. Docs (cli/DEVELOPMENT.md, amy-expert skill + template)
updated to the new wiring.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjEvS812aPLZ6nM2XLobzF
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")
Context.open(dataDir).use { ctx ->
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
}
}
}
Context is AutoCloseable; wrap it in use { } so it's closed
(RunState flushed, relays disconnected) on every exit path — never a
hand-rolled try { } 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 =
route("note", tail, "note <publish|show|react>", mapOf(
"publish" to { rest -> NotePublishCommand.run(dataDir, rest) },
"show" to { rest -> NoteShowCommand.run(dataDir, rest) },
"react" to { rest -> NoteReactCommand.run(dataDir, rest) },
))
}
The shared route(name, tail, usage, routes) helper (Router.kt)
handles the empty-input and unknown-verb bad_args branches, so the
dispatch body is just the verb→handler map. 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
Main.kt's top-leveldispatch, calling the command object directly:"note" -> NoteCommands.dispatch(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
Contextinsideuse { }so it closes on every exit path. - 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.