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

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:

  1. File under cli/commands/.
  2. Branch in Commands.kt:
    suspend fun note(dataDir: DataDir, tail: Array<String>): Int =
        NoteCommands.dispatch(dataDir, tail)
    
  3. Branch in Main.kt's top-level dispatch:
    "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.