9b9084ffaf
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
3.0 KiB
3.0 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.Json
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())
Json.writeLine(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()
}
}
}
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 Json.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]}")
}
}
}
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 — useJson.writeLine(...).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. - 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.
Output-shape rules
See output-conventions.md.