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."
107 lines
3.5 KiB
Markdown
107 lines
3.5 KiB
Markdown
# 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
|
|
|
|
```kotlin
|
|
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:
|
|
|
|
```kotlin
|
|
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`:
|
|
```kotlin
|
|
suspend fun note(dataDir: DataDir, tail: Array<String>): Int =
|
|
NoteCommands.dispatch(dataDir, tail)
|
|
```
|
|
3. Branch in `Main.kt`'s top-level `dispatch`:
|
|
```kotlin
|
|
"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`.
|