mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
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
This commit is contained in:
@@ -129,10 +129,12 @@ via `Output.emit`. The template is in `references/command-template.md`;
|
||||
copy it rather than re-deriving it.
|
||||
|
||||
Wire-up checklist:
|
||||
1. New file in `cli/commands/` with the `object` pattern.
|
||||
2. Add a branch in `Commands.kt`.
|
||||
3. Add a branch in `Main.kt`'s `dispatch` (or under `marmotDispatch`
|
||||
/ a new group dispatcher).
|
||||
1. New file in `cli/commands/` with the `object` pattern. Sub-verb
|
||||
`dispatch` functions use the shared `route(...)` helper in
|
||||
`Router.kt` rather than a hand-rolled `when (tail[0])`.
|
||||
2. Add a branch in `Main.kt`'s `dispatch` (top-level verbs call the
|
||||
command object directly, e.g. `"relay" -> RelayCommands.dispatch(…)`;
|
||||
`marmot` sub-verbs go through `marmotDispatch`'s `route` map).
|
||||
4. Extend `printUsage()` in `Main.kt`.
|
||||
5. Add the row to `cli/README.md`'s command table.
|
||||
6. Update `cli/ROADMAP.md` — move the row from 🆕 / 📦 to ✅.
|
||||
@@ -173,6 +175,7 @@ cli/
|
||||
├── secrets/ # SecretStore backends (keychain / ncryptsec / plaintext)
|
||||
└── commands/ # one file (or group) per top-level verb
|
||||
├── UseCommand.kt # `amy use NAME`
|
||||
├── Router.kt # `route(...)` shared sub-verb dispatcher
|
||||
├── InitCommands.kt # init, whoami
|
||||
├── CreateCommand.kt + LoginCommand.kt
|
||||
├── RelayCommands.kt
|
||||
|
||||
@@ -18,8 +18,7 @@ object NotePublishCommand {
|
||||
val args = Args(rest)
|
||||
val text = args.positional(0, "text")
|
||||
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
|
||||
val event = com.vitorpamplona.amethyst.commons.note
|
||||
@@ -33,13 +32,15 @@ object NotePublishCommand {
|
||||
"rejected_by" to ack.filterValues { !it }.keys.map { it.url },
|
||||
))
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`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.
|
||||
@@ -51,39 +52,34 @@ When a feature has several verbs (`note publish`, `note show`,
|
||||
|
||||
```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]}")
|
||||
}
|
||||
}
|
||||
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) },
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
Each verb gets its own file. Once a single file crosses ~200 lines,
|
||||
split it — see `GroupCommands.kt` and its siblings as the reference.
|
||||
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 `Commands.kt`:
|
||||
2. Branch in `Main.kt`'s top-level `dispatch`, calling the command
|
||||
object directly:
|
||||
```kotlin
|
||||
suspend fun note(dataDir: DataDir, tail: Array<String>): Int =
|
||||
NoteCommands.dispatch(dataDir, tail)
|
||||
"note" -> 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` (🆕 / 📦 → ✅).
|
||||
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
|
||||
|
||||
@@ -95,7 +91,7 @@ For every new command:
|
||||
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`.
|
||||
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
|
||||
|
||||
+17
-19
@@ -85,7 +85,7 @@ cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/
|
||||
├── stores/FileStores.kt # File-backed MLS / KP / message stores
|
||||
├── secrets/ # SecretStore backends (keychain / ncryptsec / plaintext)
|
||||
└── commands/
|
||||
├── Commands.kt # dispatcher tables
|
||||
├── Router.kt # `route(...)` shared sub-verb dispatcher
|
||||
├── UseCommand.kt # `amy use NAME` — pin active account
|
||||
├── InitCommands.kt # init, whoami
|
||||
├── CreateCommand.kt # full bootstrap (→ commons/account/)
|
||||
@@ -111,18 +111,19 @@ host. Never add a Gradle dependency on `:amethyst` or `:desktopApp`.
|
||||
**`Context.kt` is the backbone.** Most commands follow this template:
|
||||
|
||||
```kotlin
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx -> // .use closes the Context on exit
|
||||
ctx.prepare() // restore MLS state + connect relays
|
||||
ctx.syncIncoming() // pull new gift-wraps + group events
|
||||
// ...call into commons/ or quartz/ to build an event...
|
||||
val ack = ctx.publish(event, targets)
|
||||
Output.emit(mapOf(...))
|
||||
} finally {
|
||||
ctx.close() // flush RunState, disconnect
|
||||
}
|
||||
return 0
|
||||
} // close() flushes RunState + disconnects
|
||||
```
|
||||
|
||||
`Context` is `AutoCloseable`, so wrap it in `use { }` rather than a
|
||||
hand-rolled `try { } finally { ctx.close() }`.
|
||||
|
||||
---
|
||||
|
||||
## How to add a command
|
||||
@@ -179,20 +180,15 @@ import com.vitorpamplona.amethyst.cli.DataDir
|
||||
import com.vitorpamplona.amethyst.cli.Output
|
||||
|
||||
object NoteCommands {
|
||||
suspend fun dispatch(dataDir: DataDir, tail: Array<String>): Int {
|
||||
if (tail.isEmpty()) return Output.error("bad_args", "note <publish|read|…>")
|
||||
val rest = tail.drop(1).toTypedArray()
|
||||
return when (tail[0]) {
|
||||
"publish" -> publish(dataDir, rest)
|
||||
else -> Output.error("bad_args", "note ${tail[0]}")
|
||||
}
|
||||
}
|
||||
suspend fun dispatch(dataDir: DataDir, tail: Array<String>): Int =
|
||||
route("note", tail, "note <publish|read|…>", mapOf(
|
||||
"publish" to { rest -> publish(dataDir, rest) },
|
||||
))
|
||||
|
||||
private suspend fun publish(dataDir: DataDir, rest: Array<String>): Int {
|
||||
val args = Args(rest)
|
||||
val text = args.positional(0, "text")
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
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())
|
||||
@@ -202,13 +198,15 @@ object NoteCommands {
|
||||
"published_to" to ack.filterValues { it }.keys.map { it.url },
|
||||
))
|
||||
return 0
|
||||
} finally { ctx.close() }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Wire it into `Commands.kt`, add a top-level branch in `Main.kt`'s
|
||||
`dispatch`, and extend `printUsage()`. Keep the command tour in
|
||||
The shared `route(name, tail, usage, routes)` helper (in `Router.kt`)
|
||||
handles the empty-input and unknown-verb `bad_args` cases, so each
|
||||
`dispatch` is just the verb→handler map. Add a top-level branch in
|
||||
`Main.kt`'s `dispatch` and extend `printUsage()`. Keep the command tour in
|
||||
[README.md](./README.md) and the parity matrix in
|
||||
[ROADMAP.md](./ROADMAP.md) in sync.
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.verify
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.publishAndConfirmDetailed
|
||||
@@ -128,12 +129,14 @@ class Context(
|
||||
* humans inspect these files. Verification always re-canonicalises,
|
||||
* so the stored bytes never feed back into a signature check.
|
||||
*/
|
||||
val store: IEventStore by lazy {
|
||||
FsEventStore(
|
||||
root = dataDir.eventsDir.toPath(),
|
||||
eventToJson = com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper::toJsonPretty,
|
||||
)
|
||||
}
|
||||
private val storeDelegate: Lazy<IEventStore> =
|
||||
lazy {
|
||||
FsEventStore(
|
||||
root = dataDir.eventsDir.toPath(),
|
||||
eventToJson = JacksonMapper::toJsonPretty,
|
||||
)
|
||||
}
|
||||
val store: IEventStore by storeDelegate
|
||||
|
||||
/** Fully-wired manager. Call [prepare] once before use to load persisted state. */
|
||||
val marmot: MarmotManager = MarmotManager(signer, mlsStore, messageStore, keyPackageStore)
|
||||
@@ -611,25 +614,14 @@ class Context(
|
||||
}
|
||||
// Only close the store if it was actually opened — by-lazy
|
||||
// otherwise allocates the lock channel just to release it.
|
||||
if (storeIsInitialized()) {
|
||||
if (storeDelegate.isInitialized()) {
|
||||
try {
|
||||
store.close()
|
||||
storeDelegate.value.close()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun storeIsInitialized(): Boolean {
|
||||
// Reflect on the lazy delegate to avoid forcing initialisation in close().
|
||||
return try {
|
||||
val field = javaClass.getDeclaredField("store\$delegate").apply { isAccessible = true }
|
||||
val delegate = field.get(this) as Lazy<*>
|
||||
delegate.isInitialized()
|
||||
} catch (_: Throwable) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Lookback applied to the gift-wrap `since` filter to compensate for
|
||||
|
||||
@@ -20,7 +20,28 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.cli
|
||||
|
||||
import com.vitorpamplona.amethyst.cli.commands.Commands
|
||||
import com.vitorpamplona.amethyst.cli.commands.AwaitCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.CreateCommand
|
||||
import com.vitorpamplona.amethyst.cli.commands.DebitCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.DmCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.FollowCommand
|
||||
import com.vitorpamplona.amethyst.cli.commands.GroupCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.InitCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.KeyPackageCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.LoginCommand
|
||||
import com.vitorpamplona.amethyst.cli.commands.MarmotResetCommand
|
||||
import com.vitorpamplona.amethyst.cli.commands.MessageCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.NappletCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.NotesCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.NsiteCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.OfferCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.ProfileCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.RelayCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.SearchCommand
|
||||
import com.vitorpamplona.amethyst.cli.commands.StoreCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.UseCommand
|
||||
import com.vitorpamplona.amethyst.cli.commands.ZapCommand
|
||||
import com.vitorpamplona.amethyst.cli.commands.route
|
||||
import com.vitorpamplona.amethyst.cli.secrets.SecretStore
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.system.exitProcess
|
||||
@@ -118,86 +139,31 @@ private suspend fun dispatch(argv: Array<String>): Int {
|
||||
// resolve "multiple accounts, ambiguous" cases) — so it skips
|
||||
// DataDir.resolve. Other commands fall through to the normal path.
|
||||
if (head == "use") {
|
||||
return com.vitorpamplona.amethyst.cli.commands.UseCommand
|
||||
.run(tail)
|
||||
return UseCommand.run(tail)
|
||||
}
|
||||
|
||||
val secrets = SecretStore.from(backendFlag = secretBackendFlag, passphraseFile = passphraseFileFlag)
|
||||
val dataDir = DataDir.resolve(accountFlag = accountFlag, secrets = secrets)
|
||||
|
||||
return when (head) {
|
||||
"init" -> {
|
||||
Commands.init(dataDir, Args(tail))
|
||||
}
|
||||
|
||||
"create" -> {
|
||||
Commands.create(dataDir, tail)
|
||||
}
|
||||
|
||||
"login" -> {
|
||||
Commands.login(dataDir, tail)
|
||||
}
|
||||
|
||||
"whoami" -> {
|
||||
Commands.whoami(dataDir)
|
||||
}
|
||||
|
||||
"relay" -> {
|
||||
Commands.relay(dataDir, tail)
|
||||
}
|
||||
|
||||
"marmot" -> {
|
||||
marmotDispatch(dataDir, tail)
|
||||
}
|
||||
|
||||
"dm" -> {
|
||||
Commands.dm(dataDir, tail)
|
||||
}
|
||||
|
||||
"profile" -> {
|
||||
Commands.profile(dataDir, tail)
|
||||
}
|
||||
|
||||
"notes" -> {
|
||||
Commands.notes(dataDir, tail)
|
||||
}
|
||||
|
||||
"nsite" -> {
|
||||
Commands.nsite(dataDir, tail)
|
||||
}
|
||||
|
||||
"napplet" -> {
|
||||
Commands.napplet(dataDir, tail)
|
||||
}
|
||||
|
||||
"store" -> {
|
||||
Commands.store(dataDir, tail)
|
||||
}
|
||||
|
||||
"follow" -> {
|
||||
Commands.follow(dataDir, tail)
|
||||
}
|
||||
|
||||
"unfollow" -> {
|
||||
Commands.unfollow(dataDir, tail)
|
||||
}
|
||||
|
||||
"search" -> {
|
||||
Commands.search(dataDir, tail)
|
||||
}
|
||||
|
||||
"zap" -> {
|
||||
Commands.zap(dataDir, tail)
|
||||
}
|
||||
|
||||
"offer" -> {
|
||||
Commands.offer(dataDir, tail)
|
||||
}
|
||||
|
||||
"debit" -> {
|
||||
Commands.debit(dataDir, tail)
|
||||
}
|
||||
|
||||
"init" -> InitCommands.init(dataDir, Args(tail))
|
||||
"create" -> CreateCommand.run(dataDir, tail)
|
||||
"login" -> LoginCommand.run(dataDir, tail)
|
||||
"whoami" -> InitCommands.whoami(dataDir)
|
||||
"relay" -> RelayCommands.dispatch(dataDir, tail)
|
||||
"marmot" -> marmotDispatch(dataDir, tail)
|
||||
"dm" -> DmCommands.dispatch(dataDir, tail)
|
||||
"profile" -> ProfileCommands.dispatch(dataDir, tail)
|
||||
"notes" -> NotesCommands.dispatch(dataDir, tail)
|
||||
"nsite" -> NsiteCommands.dispatch(dataDir, tail)
|
||||
"napplet" -> NappletCommands.dispatch(dataDir, tail)
|
||||
"store" -> StoreCommands.dispatch(dataDir, tail)
|
||||
"follow" -> FollowCommand.follow(dataDir, tail)
|
||||
"unfollow" -> FollowCommand.unfollow(dataDir, tail)
|
||||
"search" -> SearchCommand.dispatch(dataDir, tail)
|
||||
"zap" -> ZapCommand.dispatch(dataDir, tail)
|
||||
"offer" -> OfferCommands.dispatch(dataDir, tail)
|
||||
"debit" -> DebitCommands.dispatch(dataDir, tail)
|
||||
else -> {
|
||||
System.err.println("unknown subcommand: $head")
|
||||
printUsage()
|
||||
@@ -209,41 +175,20 @@ private suspend fun dispatch(argv: Array<String>): Int {
|
||||
private suspend fun marmotDispatch(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int {
|
||||
if (tail.isEmpty()) {
|
||||
printUsage()
|
||||
return 2
|
||||
}
|
||||
val head = tail[0]
|
||||
val rest = tail.drop(1).toTypedArray()
|
||||
return when (head) {
|
||||
"key-package" -> {
|
||||
Commands.keyPackage(dataDir, rest)
|
||||
}
|
||||
|
||||
"group" -> {
|
||||
Commands.group(dataDir, rest)
|
||||
}
|
||||
|
||||
"message" -> {
|
||||
Commands.message(dataDir, rest)
|
||||
}
|
||||
|
||||
"await" -> {
|
||||
Commands.await(dataDir, rest)
|
||||
}
|
||||
|
||||
"reset" -> {
|
||||
Commands.reset(dataDir, rest)
|
||||
}
|
||||
|
||||
else -> {
|
||||
System.err.println("unknown marmot subcommand: $head")
|
||||
printUsage()
|
||||
2
|
||||
}
|
||||
}
|
||||
}
|
||||
): Int =
|
||||
route(
|
||||
name = "marmot",
|
||||
tail = tail,
|
||||
usage = "marmot <key-package|group|message|await|reset>",
|
||||
routes =
|
||||
mapOf(
|
||||
"key-package" to { rest -> KeyPackageCommands.dispatch(dataDir, rest) },
|
||||
"group" to { rest -> GroupCommands.dispatch(dataDir, rest) },
|
||||
"message" to { rest -> MessageCommands.dispatch(dataDir, rest) },
|
||||
"await" to { rest -> AwaitCommands.dispatch(dataDir, rest) },
|
||||
"reset" to { rest -> MarmotResetCommand.run(dataDir, rest) },
|
||||
),
|
||||
)
|
||||
|
||||
private enum class GlobalFlag(
|
||||
val long: String,
|
||||
|
||||
@@ -42,20 +42,21 @@ object AwaitCommands {
|
||||
suspend fun dispatch(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int {
|
||||
if (tail.isEmpty()) return Output.error("bad_args", "await <key-package|group|member|admin|message|rename|epoch>")
|
||||
val rest = tail.drop(1).toTypedArray()
|
||||
return when (tail[0]) {
|
||||
"key-package" -> awaitKeyPackage(dataDir, rest)
|
||||
"group" -> awaitGroup(dataDir, rest)
|
||||
"member" -> awaitMember(dataDir, rest)
|
||||
"admin" -> awaitAdmin(dataDir, rest)
|
||||
"message" -> awaitMessage(dataDir, rest)
|
||||
"rename" -> awaitRename(dataDir, rest)
|
||||
"epoch" -> awaitEpoch(dataDir, rest)
|
||||
else -> Output.error("bad_args", "await ${tail[0]}")
|
||||
}
|
||||
}
|
||||
): Int =
|
||||
route(
|
||||
"await",
|
||||
tail,
|
||||
"await <key-package|group|member|admin|message|rename|epoch>",
|
||||
mapOf(
|
||||
"key-package" to { rest -> awaitKeyPackage(dataDir, rest) },
|
||||
"group" to { rest -> awaitGroup(dataDir, rest) },
|
||||
"member" to { rest -> awaitMember(dataDir, rest) },
|
||||
"admin" to { rest -> awaitAdmin(dataDir, rest) },
|
||||
"message" to { rest -> awaitMessage(dataDir, rest) },
|
||||
"rename" to { rest -> awaitRename(dataDir, rest) },
|
||||
"epoch" to { rest -> awaitEpoch(dataDir, rest) },
|
||||
),
|
||||
)
|
||||
|
||||
private suspend fun awaitKeyPackage(
|
||||
dataDir: DataDir,
|
||||
@@ -64,8 +65,7 @@ object AwaitCommands {
|
||||
if (rest.isEmpty()) return Output.error("bad_args", "await key-package <npub>")
|
||||
val args = Args(rest.drop(1).toTypedArray())
|
||||
val timeoutSecs = args.longFlag("timeout", 30)
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val target = ctx.requireUserHex(rest[0])
|
||||
val filter = ctx.marmot.subscriptionManager.keyPackageFilter(target)
|
||||
@@ -112,8 +112,6 @@ object AwaitCommands {
|
||||
delay(2_000)
|
||||
}
|
||||
throw AwaitTimeout("no KeyPackage for $target within ${timeoutSecs}s")
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,8 +122,7 @@ object AwaitCommands {
|
||||
val args = Args(rest)
|
||||
val wantedName = args.flag("name")
|
||||
val timeoutSecs = args.longFlag("timeout", 30)
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val deadline = System.currentTimeMillis() + timeoutSecs * 1000
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
@@ -148,8 +145,6 @@ object AwaitCommands {
|
||||
delay(1_500)
|
||||
}
|
||||
throw AwaitTimeout("no group with name=$wantedName within ${timeoutSecs}s")
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,8 +192,7 @@ object AwaitCommands {
|
||||
val args = Args(rest.drop(1).toTypedArray())
|
||||
val wantedName = args.requireFlag("name")
|
||||
val timeoutSecs = args.longFlag("timeout", 30)
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val gid = ctx.resolveGroupId(rest[0])
|
||||
val deadline = System.currentTimeMillis() + timeoutSecs * 1000
|
||||
@@ -212,8 +206,6 @@ object AwaitCommands {
|
||||
delay(1_500)
|
||||
}
|
||||
throw AwaitTimeout("group $gid never renamed to $wantedName within ${timeoutSecs}s")
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,8 +217,7 @@ object AwaitCommands {
|
||||
val args = Args(rest.drop(1).toTypedArray())
|
||||
val min = args.longFlag("min", 1)
|
||||
val timeoutSecs = args.longFlag("timeout", 30)
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val gid = ctx.resolveGroupId(rest[0])
|
||||
val deadline = System.currentTimeMillis() + timeoutSecs * 1000
|
||||
@@ -240,8 +231,6 @@ object AwaitCommands {
|
||||
delay(1_500)
|
||||
}
|
||||
throw AwaitTimeout("group $gid epoch never reached $min within ${timeoutSecs}s")
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,8 +242,7 @@ object AwaitCommands {
|
||||
val args = Args(rest.drop(1).toTypedArray())
|
||||
val needle = args.requireFlag("match")
|
||||
val timeoutSecs = args.longFlag("timeout", 30)
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val gid = ctx.resolveGroupId(rest[0])
|
||||
val deadline = System.currentTimeMillis() + timeoutSecs * 1000
|
||||
@@ -285,8 +273,6 @@ object AwaitCommands {
|
||||
delay(1_500)
|
||||
}
|
||||
throw AwaitTimeout("no message matching '$needle' in $gid within ${timeoutSecs}s")
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,8 +290,7 @@ object AwaitCommands {
|
||||
if (rest.size <= targetIdx) return Output.error("bad_args", usage)
|
||||
val args = Args(rest.drop(targetIdx + 1).toTypedArray())
|
||||
val timeoutSecs = args.longFlag("timeout", 30)
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val deadline = System.currentTimeMillis() + timeoutSecs * 1000
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
@@ -318,8 +303,6 @@ object AwaitCommands {
|
||||
delay(1_500)
|
||||
}
|
||||
throw AwaitTimeout("condition never satisfied within ${timeoutSecs}s ($usage)")
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.cli.commands
|
||||
|
||||
import com.vitorpamplona.amethyst.cli.Args
|
||||
import com.vitorpamplona.amethyst.cli.DataDir
|
||||
|
||||
/**
|
||||
* Tiny dispatcher over the per-verb command groups. Each top-level subcommand
|
||||
* (`init`, `relay`, `group`, `message`, …) gets its own file so no single
|
||||
* file is too big to edit safely.
|
||||
*/
|
||||
object Commands {
|
||||
suspend fun init(
|
||||
dataDir: DataDir,
|
||||
args: Args,
|
||||
): Int = InitCommands.init(dataDir, args)
|
||||
|
||||
suspend fun create(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = CreateCommand.run(dataDir, tail)
|
||||
|
||||
suspend fun login(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = LoginCommand.run(dataDir, tail)
|
||||
|
||||
suspend fun whoami(dataDir: DataDir): Int = InitCommands.whoami(dataDir)
|
||||
|
||||
suspend fun relay(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = RelayCommands.dispatch(dataDir, tail)
|
||||
|
||||
suspend fun keyPackage(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = KeyPackageCommands.dispatch(dataDir, tail)
|
||||
|
||||
suspend fun group(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = GroupCommands.dispatch(dataDir, tail)
|
||||
|
||||
suspend fun message(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = MessageCommands.dispatch(dataDir, tail)
|
||||
|
||||
suspend fun await(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = AwaitCommands.dispatch(dataDir, tail)
|
||||
|
||||
suspend fun reset(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = MarmotResetCommand.run(dataDir, tail)
|
||||
|
||||
suspend fun dm(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = DmCommands.dispatch(dataDir, tail)
|
||||
|
||||
suspend fun profile(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = ProfileCommands.dispatch(dataDir, tail)
|
||||
|
||||
suspend fun notes(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = NotesCommands.dispatch(dataDir, tail)
|
||||
|
||||
suspend fun nsite(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = NsiteCommands.dispatch(dataDir, tail)
|
||||
|
||||
suspend fun napplet(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = NappletCommands.dispatch(dataDir, tail)
|
||||
|
||||
suspend fun store(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = StoreCommands.dispatch(dataDir, tail)
|
||||
|
||||
suspend fun follow(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = FollowCommand.follow(dataDir, tail)
|
||||
|
||||
suspend fun unfollow(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = FollowCommand.unfollow(dataDir, tail)
|
||||
|
||||
suspend fun search(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = SearchCommand.dispatch(dataDir, tail)
|
||||
|
||||
suspend fun zap(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = ZapCommand.dispatch(dataDir, tail)
|
||||
|
||||
suspend fun offer(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = OfferCommands.dispatch(dataDir, tail)
|
||||
|
||||
suspend fun debit(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int = DebitCommands.dispatch(dataDir, tail)
|
||||
}
|
||||
@@ -71,17 +71,14 @@ object CreateCommand {
|
||||
// store via verifyAndStore — so kind:10002 / 10050 / 10051 are
|
||||
// immediately readable by outboxRelays() / inboxRelays() /
|
||||
// keyPackageRelays() without a separate config file.
|
||||
val ctx = Context.open(dataDir)
|
||||
val accepted = mutableMapOf<String, List<String>>()
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
for (event in bootstrap.all()) {
|
||||
val ack = ctx.publish(event, DefaultNIP65RelaySet)
|
||||
accepted[event.kind.toString()] =
|
||||
ack.filterValues { it }.keys.map { it.url }
|
||||
}
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
|
||||
Output.emit(
|
||||
|
||||
@@ -47,16 +47,17 @@ object DebitCommands {
|
||||
suspend fun dispatch(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int {
|
||||
if (tail.isEmpty()) return Output.error("bad_args", "debit <info|pay|budget>")
|
||||
val rest = tail.drop(1).toTypedArray()
|
||||
return when (tail[0]) {
|
||||
"info" -> info(rest)
|
||||
"pay" -> pay(dataDir, rest)
|
||||
"budget" -> budget(dataDir, rest)
|
||||
else -> Output.error("bad_args", "debit ${tail[0]} (expected info|pay|budget)")
|
||||
}
|
||||
}
|
||||
): Int =
|
||||
route(
|
||||
"debit",
|
||||
tail,
|
||||
"debit <info|pay|budget>",
|
||||
mapOf(
|
||||
"info" to { rest -> info(rest) },
|
||||
"pay" to { rest -> pay(dataDir, rest) },
|
||||
"budget" to { rest -> budget(dataDir, rest) },
|
||||
),
|
||||
)
|
||||
|
||||
/** Local decode of an `ndebit` pointer — no network, no account needed. */
|
||||
private fun info(rest: Array<String>): Int {
|
||||
@@ -119,8 +120,7 @@ object DebitCommands {
|
||||
?: return Output.error("bad_args", "not a valid ndebit pointer")
|
||||
if (debit.relays.isEmpty()) return Output.error("bad_pointer", "ndebit carries no relay to reach")
|
||||
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
return when (val outcome = settle(ctx, debit, timeoutMs, buildRequest)) {
|
||||
Settle.Timeout -> {
|
||||
@@ -130,8 +130,6 @@ object DebitCommands {
|
||||
Settle.BadReply -> Output.error("bad_response", "service reply was not a kind-21002 debit event")
|
||||
is Settle.Replied -> emitDebit(outcome, debit.pubKey)
|
||||
}
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,17 +61,18 @@ object DmCommands {
|
||||
suspend fun dispatch(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int {
|
||||
if (tail.isEmpty()) return Output.error("bad_args", "dm <send|send-file|list|await> …")
|
||||
val rest = tail.drop(1).toTypedArray()
|
||||
return when (tail[0]) {
|
||||
"send" -> send(dataDir, rest)
|
||||
"send-file" -> sendFile(dataDir, rest)
|
||||
"list" -> list(dataDir, rest)
|
||||
"await" -> await(dataDir, rest)
|
||||
else -> Output.error("bad_args", "dm ${tail[0]}")
|
||||
}
|
||||
}
|
||||
): Int =
|
||||
route(
|
||||
"dm",
|
||||
tail,
|
||||
"dm <send|send-file|list|await> …",
|
||||
mapOf(
|
||||
"send" to { rest -> send(dataDir, rest) },
|
||||
"send-file" to { rest -> sendFile(dataDir, rest) },
|
||||
"list" to { rest -> list(dataDir, rest) },
|
||||
"await" to { rest -> await(dataDir, rest) },
|
||||
),
|
||||
)
|
||||
|
||||
private suspend fun send(
|
||||
dataDir: DataDir,
|
||||
@@ -82,14 +83,11 @@ object DmCommands {
|
||||
val args = Args(rest.drop(2).toTypedArray())
|
||||
val allowFallback = args.bool("allow-fallback")
|
||||
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val recipient = ctx.requireUserHex(rest[0])
|
||||
val result = DmActions.buildTextDm(ctx.signer, recipient, text)
|
||||
return publishWraps(ctx, result, allowFallback)
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,8 +116,7 @@ object DmCommands {
|
||||
val allowFallback = args.bool("allow-fallback")
|
||||
val recipientInput = rest[0]
|
||||
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val recipient = ctx.requireUserHex(recipientInput)
|
||||
|
||||
@@ -133,8 +130,6 @@ object DmCommands {
|
||||
}
|
||||
|
||||
return publishWraps(ctx, result, allowFallback, extra = summary)
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,8 +310,7 @@ object DmCommands {
|
||||
val limit = args.intFlag("limit", Int.MAX_VALUE)
|
||||
val timeoutSecs = args.longFlag("timeout", 8)
|
||||
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val peerHex = peerInput?.let { ctx.requireUserHex(it) }
|
||||
|
||||
@@ -357,8 +351,6 @@ object DmCommands {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,8 +363,7 @@ object DmCommands {
|
||||
val match = args.requireFlag("match")
|
||||
val timeoutSecs = args.longFlag("timeout", 30)
|
||||
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val peerHex = ctx.requireUserHex(peerInput)
|
||||
|
||||
@@ -408,8 +399,6 @@ object DmCommands {
|
||||
delay(2_000)
|
||||
}
|
||||
throw AwaitTimeout("no DM from $peerHex matching '$match' within ${timeoutSecs}s")
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,8 +61,7 @@ object FeedCommand {
|
||||
val until = args.flag("until")?.toLongOrNull()
|
||||
val timeoutSecs = args.longFlag("timeout", 8L)
|
||||
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
|
||||
val (authors, mode) =
|
||||
@@ -131,8 +130,6 @@ object FeedCommand {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,8 +68,7 @@ object FollowCommand {
|
||||
val args = Args(rest.drop(1).toTypedArray())
|
||||
val timeoutSecs = args.longFlag("timeout", 8L)
|
||||
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val target = ctx.requireUserHex(userArg)
|
||||
val self = ctx.identity.pubKeyHex
|
||||
@@ -148,8 +147,6 @@ object FollowCommand {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -46,8 +46,7 @@ object GroupAddMemberCommand {
|
||||
rest: Array<String>,
|
||||
): Int {
|
||||
if (rest.size < 2) return Output.error("bad_args", "group add <group_id> <npub> [<npub> ...]")
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val gid = ctx.resolveGroupId(rest[0])
|
||||
ctx.syncIncoming()
|
||||
@@ -168,8 +167,6 @@ object GroupAddMemberCommand {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,28 +21,28 @@
|
||||
package com.vitorpamplona.amethyst.cli.commands
|
||||
|
||||
import com.vitorpamplona.amethyst.cli.DataDir
|
||||
import com.vitorpamplona.amethyst.cli.Output
|
||||
|
||||
object GroupCommands {
|
||||
suspend fun dispatch(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int {
|
||||
if (tail.isEmpty()) return Output.error("bad_args", "group <create|list|show|…>")
|
||||
val rest = tail.drop(1).toTypedArray()
|
||||
return when (tail[0]) {
|
||||
"create" -> GroupCreateCommand.run(dataDir, rest)
|
||||
"list" -> GroupReadCommands.list(dataDir)
|
||||
"show" -> GroupReadCommands.show(dataDir, rest)
|
||||
"members" -> GroupReadCommands.members(dataDir, rest)
|
||||
"admins" -> GroupReadCommands.admins(dataDir, rest)
|
||||
"add" -> GroupAddMemberCommand.run(dataDir, rest)
|
||||
"rename" -> GroupMetadataCommands.rename(dataDir, rest)
|
||||
"promote" -> GroupMetadataCommands.promote(dataDir, rest)
|
||||
"demote" -> GroupMetadataCommands.demote(dataDir, rest)
|
||||
"remove" -> GroupMembershipCommands.remove(dataDir, rest)
|
||||
"leave" -> GroupMembershipCommands.leave(dataDir, rest)
|
||||
else -> Output.error("bad_args", "group ${tail[0]}")
|
||||
}
|
||||
}
|
||||
): Int =
|
||||
route(
|
||||
"group",
|
||||
tail,
|
||||
"group <create|list|show|…>",
|
||||
mapOf(
|
||||
"create" to { rest -> GroupCreateCommand.run(dataDir, rest) },
|
||||
"list" to { _ -> GroupReadCommands.list(dataDir) },
|
||||
"show" to { rest -> GroupReadCommands.show(dataDir, rest) },
|
||||
"members" to { rest -> GroupReadCommands.members(dataDir, rest) },
|
||||
"admins" to { rest -> GroupReadCommands.admins(dataDir, rest) },
|
||||
"add" to { rest -> GroupAddMemberCommand.run(dataDir, rest) },
|
||||
"rename" to { rest -> GroupMetadataCommands.rename(dataDir, rest) },
|
||||
"promote" to { rest -> GroupMetadataCommands.promote(dataDir, rest) },
|
||||
"demote" to { rest -> GroupMetadataCommands.demote(dataDir, rest) },
|
||||
"remove" to { rest -> GroupMembershipCommands.remove(dataDir, rest) },
|
||||
"leave" to { rest -> GroupMembershipCommands.leave(dataDir, rest) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,8 +35,7 @@ object GroupCreateCommand {
|
||||
): Int {
|
||||
val args = Args(rest)
|
||||
val name = args.flag("name", "")!!
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val gid = RandomInstance.bytes(32).toHexKey()
|
||||
|
||||
@@ -65,8 +64,6 @@ object GroupCreateCommand {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-8
@@ -30,8 +30,7 @@ object GroupMembershipCommands {
|
||||
rest: Array<String>,
|
||||
): Int {
|
||||
if (rest.size < 2) return Output.error("bad_args", "group remove <gid> <npub>")
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val gid = ctx.resolveGroupId(rest[0])
|
||||
val target = ctx.requireUserHex(rest[1])
|
||||
@@ -56,8 +55,6 @@ object GroupMembershipCommands {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,8 +63,7 @@ object GroupMembershipCommands {
|
||||
rest: Array<String>,
|
||||
): Int {
|
||||
if (rest.isEmpty()) return Output.error("bad_args", "group leave <gid>")
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val gid = ctx.resolveGroupId(rest[0])
|
||||
if (!ctx.marmot.isMember(gid)) return Output.error("not_member", gid)
|
||||
@@ -111,8 +107,6 @@ object GroupMembershipCommands {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-4
@@ -69,8 +69,7 @@ object GroupMetadataCommands {
|
||||
rawGid: HexKey,
|
||||
mutate: suspend (Context, MarmotGroupData) -> MarmotGroupData,
|
||||
): Int {
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val gid = ctx.resolveGroupId(rawGid)
|
||||
ctx.syncIncoming()
|
||||
@@ -100,8 +99,6 @@ object GroupMetadataCommands {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,7 @@ import com.vitorpamplona.amethyst.cli.Output
|
||||
*/
|
||||
object GroupReadCommands {
|
||||
suspend fun list(dataDir: DataDir): Int {
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
ctx.syncIncoming()
|
||||
val ids = ctx.marmot.activeGroupIds()
|
||||
@@ -46,8 +45,6 @@ object GroupReadCommands {
|
||||
}
|
||||
Output.emit(mapOf("groups" to items))
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,8 +53,7 @@ object GroupReadCommands {
|
||||
rest: Array<String>,
|
||||
): Int {
|
||||
if (rest.isEmpty()) return Output.error("bad_args", "group show <group_id>")
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val gid = ctx.resolveGroupId(rest[0])
|
||||
ctx.syncIncoming()
|
||||
@@ -81,8 +77,6 @@ object GroupReadCommands {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,8 +85,7 @@ object GroupReadCommands {
|
||||
rest: Array<String>,
|
||||
): Int {
|
||||
if (rest.isEmpty()) return Output.error("bad_args", "group members <group_id>")
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val gid = ctx.resolveGroupId(rest[0])
|
||||
ctx.syncIncoming()
|
||||
@@ -103,8 +96,6 @@ object GroupReadCommands {
|
||||
}
|
||||
Output.emit(mapOf("group_id" to gid, "members" to members))
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,8 +104,7 @@ object GroupReadCommands {
|
||||
rest: Array<String>,
|
||||
): Int {
|
||||
if (rest.isEmpty()) return Output.error("bad_args", "group admins <group_id>")
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val gid = ctx.resolveGroupId(rest[0])
|
||||
ctx.syncIncoming()
|
||||
@@ -122,8 +112,6 @@ object GroupReadCommands {
|
||||
val m = ctx.marmot.groupMetadata(gid)
|
||||
Output.emit(mapOf("group_id" to gid, "admins" to (m?.adminPubkeys ?: emptyList())))
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,18 +30,19 @@ object KeyPackageCommands {
|
||||
suspend fun dispatch(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int {
|
||||
if (tail.isEmpty()) return Output.error("bad_args", "key-package <publish|check> …")
|
||||
return when (tail[0]) {
|
||||
"publish" -> publish(dataDir)
|
||||
"check" -> check(dataDir, tail.drop(1).toTypedArray())
|
||||
else -> Output.error("bad_args", "key-package ${tail[0]}")
|
||||
}
|
||||
}
|
||||
): Int =
|
||||
route(
|
||||
"key-package",
|
||||
tail,
|
||||
"key-package <publish|check> …",
|
||||
mapOf(
|
||||
"publish" to { _ -> publish(dataDir) },
|
||||
"check" to { rest -> check(dataDir, rest) },
|
||||
),
|
||||
)
|
||||
|
||||
private suspend fun publish(dataDir: DataDir): Int {
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val relays = ctx.keyPackageRelays().ifEmpty { ctx.outboxRelays() }.ifEmpty { ctx.anyRelays() }
|
||||
if (relays.isEmpty()) return Output.error("no_relays", "configure relays first")
|
||||
@@ -57,8 +58,6 @@ object KeyPackageCommands {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,8 +66,7 @@ object KeyPackageCommands {
|
||||
rest: Array<String>,
|
||||
): Int {
|
||||
if (rest.isEmpty()) return Output.error("bad_args", "key-package check <npub>")
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val targetHex = ctx.requireUserHex(rest[0])
|
||||
// Per MIP-00: a user's KeyPackages live on the relays advertised
|
||||
@@ -109,8 +107,6 @@ object KeyPackageCommands {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,8 +46,7 @@ object MarmotResetCommand {
|
||||
rest: Array<String>,
|
||||
): Int {
|
||||
val confirmed = rest.any { it == "--yes" || it == "-y" }
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val groupIds =
|
||||
ctx.marmot
|
||||
@@ -77,8 +76,6 @@ object MarmotResetCommand {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,17 +31,18 @@ object MessageCommands {
|
||||
suspend fun dispatch(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int {
|
||||
if (tail.isEmpty()) return Output.error("bad_args", "message <send|list|react|delete> …")
|
||||
val rest = tail.drop(1).toTypedArray()
|
||||
return when (tail[0]) {
|
||||
"send" -> send(dataDir, rest)
|
||||
"list" -> list(dataDir, rest)
|
||||
"react" -> react(dataDir, rest)
|
||||
"delete" -> delete(dataDir, rest)
|
||||
else -> Output.error("bad_args", "message ${tail[0]}")
|
||||
}
|
||||
}
|
||||
): Int =
|
||||
route(
|
||||
"message",
|
||||
tail,
|
||||
"message <send|list|react|delete> …",
|
||||
mapOf(
|
||||
"send" to { rest -> send(dataDir, rest) },
|
||||
"list" to { rest -> list(dataDir, rest) },
|
||||
"react" to { rest -> react(dataDir, rest) },
|
||||
"delete" to { rest -> delete(dataDir, rest) },
|
||||
),
|
||||
)
|
||||
|
||||
private suspend fun send(
|
||||
dataDir: DataDir,
|
||||
@@ -49,8 +50,7 @@ object MessageCommands {
|
||||
): Int {
|
||||
if (rest.size < 2) return Output.error("bad_args", "message send <gid> <text>")
|
||||
val text = rest[1]
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val gid = ctx.resolveGroupId(rest[0])
|
||||
ctx.syncIncoming()
|
||||
@@ -70,8 +70,6 @@ object MessageCommands {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +80,7 @@ object MessageCommands {
|
||||
if (rest.isEmpty()) return Output.error("bad_args", "message list <gid>")
|
||||
val args = Args(rest.drop(1).toTypedArray())
|
||||
val limit = args.intFlag("limit", Int.MAX_VALUE)
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val gid = ctx.resolveGroupId(rest[0])
|
||||
ctx.syncIncoming()
|
||||
@@ -110,8 +107,6 @@ object MessageCommands {
|
||||
|
||||
Output.emit(mapOf("group_id" to gid, "messages" to items))
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,8 +117,7 @@ object MessageCommands {
|
||||
if (rest.size < 3) return Output.error("bad_args", "message react <gid> <target_event_id> <emoji>")
|
||||
val targetId = rest[1]
|
||||
val emoji = rest[2]
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val gid = ctx.resolveGroupId(rest[0])
|
||||
ctx.syncIncoming()
|
||||
@@ -146,8 +140,6 @@ object MessageCommands {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,8 +148,7 @@ object MessageCommands {
|
||||
rest: Array<String>,
|
||||
): Int {
|
||||
if (rest.size < 2) return Output.error("bad_args", "message delete <gid> <target_event_id> [target_event_id ...]")
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val gid = ctx.resolveGroupId(rest[0])
|
||||
ctx.syncIncoming()
|
||||
@@ -184,8 +175,6 @@ object MessageCommands {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,14 +53,15 @@ object NappletCommands {
|
||||
suspend fun dispatch(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int {
|
||||
if (tail.isEmpty()) return Output.error("bad_args", "napplet <fetch> …")
|
||||
val rest = tail.drop(1).toTypedArray()
|
||||
return when (tail[0]) {
|
||||
"fetch" -> fetch(dataDir, rest)
|
||||
else -> Output.error("bad_args", "napplet ${tail[0]}")
|
||||
}
|
||||
}
|
||||
): Int =
|
||||
route(
|
||||
"napplet",
|
||||
tail,
|
||||
"napplet <fetch> …",
|
||||
mapOf(
|
||||
"fetch" to { rest -> fetch(dataDir, rest) },
|
||||
),
|
||||
)
|
||||
|
||||
private suspend fun fetch(
|
||||
dataDir: DataDir,
|
||||
@@ -80,8 +81,7 @@ object NappletCommands {
|
||||
val extraServers = StaticSiteFetch.commaList(args.flag("server"))
|
||||
val extraRelays = StaticSiteFetch.commaList(args.flag("relay"))
|
||||
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val relays =
|
||||
extraRelays
|
||||
@@ -143,8 +143,6 @@ object NappletCommands {
|
||||
outFile = outFile,
|
||||
maxInlineBytes = maxInlineBytes,
|
||||
)
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
package com.vitorpamplona.amethyst.cli.commands
|
||||
|
||||
import com.vitorpamplona.amethyst.cli.DataDir
|
||||
import com.vitorpamplona.amethyst.cli.Output
|
||||
|
||||
/**
|
||||
* `amy notes <post|feed>` — NIP-10 kind:1 short text notes. Sits alongside
|
||||
@@ -32,13 +31,14 @@ object NotesCommands {
|
||||
suspend fun dispatch(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int {
|
||||
if (tail.isEmpty()) return Output.error("bad_args", "notes <post|feed> …")
|
||||
val rest = tail.drop(1).toTypedArray()
|
||||
return when (tail[0]) {
|
||||
"post" -> PostCommand.run(dataDir, rest)
|
||||
"feed" -> FeedCommand.run(dataDir, rest)
|
||||
else -> Output.error("bad_args", "notes ${tail[0]}")
|
||||
}
|
||||
}
|
||||
): Int =
|
||||
route(
|
||||
"notes",
|
||||
tail,
|
||||
"notes <post|feed> …",
|
||||
mapOf(
|
||||
"post" to { rest -> PostCommand.run(dataDir, rest) },
|
||||
"feed" to { rest -> FeedCommand.run(dataDir, rest) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -52,14 +52,15 @@ object NsiteCommands {
|
||||
suspend fun dispatch(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int {
|
||||
if (tail.isEmpty()) return Output.error("bad_args", "nsite <fetch> …")
|
||||
val rest = tail.drop(1).toTypedArray()
|
||||
return when (tail[0]) {
|
||||
"fetch" -> fetch(dataDir, rest)
|
||||
else -> Output.error("bad_args", "nsite ${tail[0]}")
|
||||
}
|
||||
}
|
||||
): Int =
|
||||
route(
|
||||
"nsite",
|
||||
tail,
|
||||
"nsite <fetch> …",
|
||||
mapOf(
|
||||
"fetch" to { rest -> fetch(dataDir, rest) },
|
||||
),
|
||||
)
|
||||
|
||||
private suspend fun fetch(
|
||||
dataDir: DataDir,
|
||||
@@ -75,8 +76,7 @@ object NsiteCommands {
|
||||
val extraServers = StaticSiteFetch.commaList(args.flag("server"))
|
||||
val extraRelays = StaticSiteFetch.commaList(args.flag("relay"))
|
||||
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val authorHex = ctx.requireUserHex(author)
|
||||
|
||||
@@ -113,8 +113,6 @@ object NsiteCommands {
|
||||
outFile = outFile,
|
||||
maxInlineBytes = maxInlineBytes,
|
||||
)
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,17 +57,18 @@ object OfferCommands {
|
||||
suspend fun dispatch(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int {
|
||||
if (tail.isEmpty()) return Output.error("bad_args", "offer <info|discover|request|pay>")
|
||||
val rest = tail.drop(1).toTypedArray()
|
||||
return when (tail[0]) {
|
||||
"info" -> info(rest)
|
||||
"discover" -> discover(dataDir, rest)
|
||||
"request" -> request(dataDir, rest)
|
||||
"pay" -> pay(dataDir, rest)
|
||||
else -> Output.error("bad_args", "offer ${tail[0]} (expected info|discover|request|pay)")
|
||||
}
|
||||
}
|
||||
): Int =
|
||||
route(
|
||||
"offer",
|
||||
tail,
|
||||
"offer <info|discover|request|pay>",
|
||||
mapOf(
|
||||
"info" to { rest -> info(rest) },
|
||||
"discover" to { rest -> discover(dataDir, rest) },
|
||||
"request" to { rest -> request(dataDir, rest) },
|
||||
"pay" to { rest -> pay(dataDir, rest) },
|
||||
),
|
||||
)
|
||||
|
||||
/**
|
||||
* Resolve a profile's advertised offer from its NIP-05 `.well-known/nostr.json` `clink_offer`
|
||||
@@ -83,8 +84,7 @@ object OfferCommands {
|
||||
Nip05Id.parse(args.positional(0, "nip05").trim())
|
||||
?: return Output.error("bad_args", "not a valid NIP-05 address (e.g. bob@example.com)")
|
||||
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val noffer = ctx.nip05Client.loadClinkOffer(id)
|
||||
if (noffer == null) {
|
||||
@@ -105,8 +105,6 @@ object OfferCommands {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,8 +150,7 @@ object OfferCommands {
|
||||
ClinkPointerParser.parse(args.positional(0, "noffer").trim()) as? NOffer
|
||||
?: return Output.error("bad_args", ERR_NOT_A_NOFFER)
|
||||
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
var hops = 0
|
||||
while (hops <= MAX_FOLLOW_HOPS) {
|
||||
@@ -200,8 +197,6 @@ object OfferCommands {
|
||||
)
|
||||
}
|
||||
return Output.error("offer_error", "too many redirects following moved offers (>$MAX_FOLLOW_HOPS)")
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,8 +227,7 @@ object OfferCommands {
|
||||
if (offerRelays.isEmpty()) return Output.error("bad_pointer", "noffer carries no relay to reach")
|
||||
if (debit.relays.isEmpty()) return Output.error("bad_pointer", "ndebit carries no relay to reach")
|
||||
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
|
||||
// 1. fetch a fresh BOLT-11 from the offer service.
|
||||
@@ -287,8 +281,6 @@ object OfferCommands {
|
||||
)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,8 +50,7 @@ object PostCommand {
|
||||
?.map { it.trim() }
|
||||
?.filter { it.isNotEmpty() } ?: emptyList()
|
||||
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val outbox = ctx.outboxRelays()
|
||||
val extraNormalized =
|
||||
@@ -78,8 +77,6 @@ object PostCommand {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,15 +45,16 @@ object ProfileCommands {
|
||||
suspend fun dispatch(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int {
|
||||
if (tail.isEmpty()) return Output.error("bad_args", "profile <show|edit> …")
|
||||
val rest = tail.drop(1).toTypedArray()
|
||||
return when (tail[0]) {
|
||||
"show" -> show(dataDir, rest)
|
||||
"edit" -> edit(dataDir, rest)
|
||||
else -> Output.error("bad_args", "profile ${tail[0]}")
|
||||
}
|
||||
}
|
||||
): Int =
|
||||
route(
|
||||
"profile",
|
||||
tail,
|
||||
"profile <show|edit> …",
|
||||
mapOf(
|
||||
"show" to { rest -> show(dataDir, rest) },
|
||||
"edit" to { rest -> edit(dataDir, rest) },
|
||||
),
|
||||
)
|
||||
|
||||
private suspend fun show(
|
||||
dataDir: DataDir,
|
||||
@@ -62,8 +63,7 @@ object ProfileCommands {
|
||||
val args = Args(rest)
|
||||
val refresh = args.bool("refresh")
|
||||
val timeoutSecs = args.longFlag("timeout", 8L)
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val pubKey =
|
||||
args.positionalOrNull(0)?.let { ctx.requireUserHex(it) }
|
||||
@@ -118,8 +118,6 @@ object ProfileCommands {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,8 +159,7 @@ object ProfileCommands {
|
||||
)
|
||||
}
|
||||
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val targets = ctx.outboxRelays().ifEmpty { ctx.bootstrapRelays() }
|
||||
val latest =
|
||||
@@ -226,8 +223,6 @@ object ProfileCommands {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,17 +51,17 @@ object RelayCommands {
|
||||
suspend fun dispatch(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int {
|
||||
if (tail.isEmpty()) return Output.error("bad_args", "relay <add|list|publish-lists> …")
|
||||
val sub = tail[0]
|
||||
val rest = tail.drop(1).toTypedArray()
|
||||
return when (sub) {
|
||||
"add" -> add(dataDir, Args(rest))
|
||||
"list" -> list(dataDir)
|
||||
"publish-lists" -> publishLists(dataDir)
|
||||
else -> Output.error("bad_args", "relay $sub")
|
||||
}
|
||||
}
|
||||
): Int =
|
||||
route(
|
||||
"relay",
|
||||
tail,
|
||||
"relay <add|list|publish-lists> …",
|
||||
mapOf(
|
||||
"add" to { rest -> add(dataDir, Args(rest)) },
|
||||
"list" to { _ -> list(dataDir) },
|
||||
"publish-lists" to { _ -> publishLists(dataDir) },
|
||||
),
|
||||
)
|
||||
|
||||
private suspend fun add(
|
||||
dataDir: DataDir,
|
||||
@@ -74,8 +74,7 @@ object RelayCommands {
|
||||
?: return Output.error("bad_args", "invalid relay url: $rawUrl")
|
||||
|
||||
val targets = if (type == "all") listOf("nip65", "inbox", "key_package") else listOf(type)
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
val addedTo = mutableListOf<String>()
|
||||
val alreadyPresent = mutableListOf<String>()
|
||||
for (t in targets) {
|
||||
@@ -89,8 +88,6 @@ object RelayCommands {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,8 +135,7 @@ object RelayCommands {
|
||||
}
|
||||
|
||||
private suspend fun list(dataDir: DataDir): Int {
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
val self = ctx.identity.pubKeyHex
|
||||
Output.emit(
|
||||
mapOf(
|
||||
@@ -149,14 +145,11 @@ object RelayCommands {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun publishLists(dataDir: DataDir): Int {
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val self = ctx.identity.pubKeyHex
|
||||
val nip65Event = ctx.relaysOf(self)
|
||||
@@ -189,8 +182,6 @@ object RelayCommands {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.cli.commands
|
||||
|
||||
import com.vitorpamplona.amethyst.cli.Output
|
||||
|
||||
/**
|
||||
* Shared sub-verb router used by every `*Commands.dispatch`.
|
||||
*
|
||||
* Maps the first token of [tail] to a handler over the remaining args.
|
||||
* Empty input emits `bad_args: <usage>`; an unrecognised verb emits
|
||||
* `bad_args: <name> <verb>`. Handlers receive the args *after* the verb,
|
||||
* mirroring the old hand-rolled `when (tail[0]) { … }` blocks.
|
||||
*/
|
||||
suspend fun route(
|
||||
name: String,
|
||||
tail: Array<String>,
|
||||
usage: String,
|
||||
routes: Map<String, suspend (Array<String>) -> Int>,
|
||||
): Int {
|
||||
if (tail.isEmpty()) return Output.error("bad_args", usage)
|
||||
val handler = routes[tail[0]] ?: return Output.error("bad_args", "$name ${tail[0]}")
|
||||
return handler(tail.drop(1).toTypedArray())
|
||||
}
|
||||
@@ -52,15 +52,16 @@ object SearchCommand {
|
||||
suspend fun dispatch(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int {
|
||||
if (tail.isEmpty()) return Output.error("bad_args", "search <user|note> <query> [--limit N] [--timeout SECS]")
|
||||
val rest = tail.drop(1).toTypedArray()
|
||||
return when (tail[0]) {
|
||||
"user" -> searchUsers(dataDir, rest)
|
||||
"note" -> searchNotes(dataDir, rest)
|
||||
else -> Output.error("bad_args", "search ${tail[0]} — expected user|note")
|
||||
}
|
||||
}
|
||||
): Int =
|
||||
route(
|
||||
"search",
|
||||
tail,
|
||||
"search <user|note> <query> [--limit N] [--timeout SECS]",
|
||||
mapOf(
|
||||
"user" to { rest -> searchUsers(dataDir, rest) },
|
||||
"note" to { rest -> searchNotes(dataDir, rest) },
|
||||
),
|
||||
)
|
||||
|
||||
private suspend fun searchUsers(
|
||||
dataDir: DataDir,
|
||||
@@ -144,8 +145,7 @@ object SearchCommand {
|
||||
timeoutMs: Long,
|
||||
render: (List<Event>) -> List<Map<String, Any?>>,
|
||||
): Int {
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val relays =
|
||||
SearchActions.resolveSearchRelays(
|
||||
@@ -172,8 +172,6 @@ object SearchCommand {
|
||||
),
|
||||
)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,18 +54,19 @@ object StoreCommands {
|
||||
suspend fun dispatch(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int {
|
||||
if (tail.isEmpty()) return Output.error("bad_args", "store <stat|sweep-expired|scrub|compact|reindex-fts>")
|
||||
val rest = tail.drop(1).toTypedArray()
|
||||
return when (tail[0]) {
|
||||
"stat" -> stat(dataDir)
|
||||
"sweep-expired" -> sweepExpired(dataDir)
|
||||
"scrub" -> scrub(dataDir)
|
||||
"compact" -> compact(dataDir)
|
||||
"reindex-fts" -> reindexFts(dataDir)
|
||||
else -> Output.error("bad_args", "store ${tail[0]}")
|
||||
}
|
||||
}
|
||||
): Int =
|
||||
route(
|
||||
"store",
|
||||
tail,
|
||||
"store <stat|sweep-expired|scrub|compact|reindex-fts>",
|
||||
mapOf(
|
||||
"stat" to { _ -> stat(dataDir) },
|
||||
"sweep-expired" to { _ -> sweepExpired(dataDir) },
|
||||
"scrub" to { _ -> scrub(dataDir) },
|
||||
"compact" to { _ -> compact(dataDir) },
|
||||
"reindex-fts" to { _ -> reindexFts(dataDir) },
|
||||
),
|
||||
)
|
||||
|
||||
private fun stat(dataDir: DataDir): Int {
|
||||
val storeRoot = dataDir.eventsDir.toPath()
|
||||
|
||||
@@ -94,8 +94,7 @@ object ZapCommand {
|
||||
?: return Output.error("bad_args", "--with must be a valid ndebit pointer with a relay")
|
||||
}
|
||||
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val recipient = ctx.requireUserHex(userArg)
|
||||
val metadata =
|
||||
@@ -117,8 +116,6 @@ object ZapCommand {
|
||||
|
||||
emitZapResult(ctx, sats, lnAddress, comment, request, zapType, timeoutMs, settleWith)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,8 +142,7 @@ object ZapCommand {
|
||||
?: return Output.error("bad_args", "--with must be a valid ndebit pointer with a relay")
|
||||
}
|
||||
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val zappedEvent =
|
||||
ctx.store.query<Event>(Filter(ids = listOf(eventId), limit = 1)).firstOrNull()
|
||||
@@ -197,8 +193,6 @@ object ZapCommand {
|
||||
|
||||
emitSplitZapResult(ctx, sats, comment, zappedEvent.id, zapType, requests, timeoutMs, settleWith)
|
||||
return 0
|
||||
} finally {
|
||||
ctx.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user