Files
amethyst/.claude/skills/amy-expert/references/command-template.md
T
Claude 48b988d2f7 refactor(cli): simplify amy dispatch, Context lifecycle, and store handling
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
2026-06-21 17:24:00 +00:00

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:

  1. File under cli/commands/.
  2. Branch in Main.kt's top-level dispatch, calling the command object directly:
    "note" -> NoteCommands.dispatch(dataDir, tail)
    
  3. Line in printUsage() explaining the verb.
  4. Row in cli/README.md's command table.
  5. 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 inside use { } so it closes on every exit path.
  • 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.