feat(amy): list an author's napplets / nsites

Add `amy napplet list <author>` and `amy nsite list <author>`: fetch the
author's root + named manifests (15129/35129 for napplets, 15128/35128 for
nsites), keep the latest per identifier, and emit a summary of each (kind, d,
title, description, path count, servers, requires/aggregate, event id,
created_at). Thin assembly over ctx.drain + the quartz manifest accessors.

Harness README notes `amy napplet list` for enumerating what you've published.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
This commit is contained in:
Claude
2026-06-22 15:38:21 +00:00
parent 7bc95d836c
commit d60a618653
3 changed files with 119 additions and 2 deletions
@@ -57,14 +57,66 @@ object NappletCommands {
route(
"napplet",
tail,
"napplet <fetch|publish|serve> …",
"napplet <fetch|publish|serve|list> …",
mapOf(
"fetch" to { rest -> fetch(dataDir, rest) },
"publish" to { rest -> publish(dataDir, rest) },
"serve" to { rest -> serve(dataDir, rest) },
"list" to { rest -> list(dataDir, rest) },
),
)
/**
* `amy napplet list <author> [--relay R] [--timeout S]` — enumerate an author's published
* napplets: the root (kind 15129) and every named one (kind 35129), latest per identifier.
*/
private suspend fun list(
dataDir: DataDir,
rest: Array<String>,
): Int {
val args = Args(rest)
val author = args.positionalOrNull(0) ?: return Output.error("bad_args", "napplet list <author> [--relay R] [--timeout S]")
val extraRelays = StaticSiteFetch.commaList(args.flag("relay"))
val timeoutSecs = args.longFlag("timeout", 8L)
Context.open(dataDir).use { ctx ->
ctx.prepare()
val authorHex = ctx.requireUserHex(author)
val relays =
extraRelays
.mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) }
.toSet()
.ifEmpty { ctx.bootstrapRelays() }
val filter = Filter(kinds = listOf(RootNappletEvent.KIND, NamedNappletEvent.KIND), authors = listOf(authorHex))
val items =
ctx
.drain(relays.associateWith { listOf(filter) }, timeoutSecs * 1000)
.map { (_, ev) -> ev }
.filter { it.pubKey == authorHex && it is NappletManifest }
.groupBy { it.kind to (it as? NamedNappletEvent)?.identifier() }
.mapNotNull { (_, dupes) -> dupes.maxByOrNull { it.createdAt } }
.sortedByDescending { it.createdAt }
.map { ev ->
val m = ev as NappletManifest
mapOf(
"kind" to ev.kind,
"d" to (ev as? NamedNappletEvent)?.identifier(),
"title" to m.title(),
"description" to m.description(),
"paths" to m.paths().size,
"servers" to m.servers(),
"requires" to m.requires(),
"aggregate_sha256" to m.declaredAggregateHash(),
"aggregate_verified" to m.verifyAggregate(),
"event_id" to ev.id,
"created_at" to ev.createdAt,
)
}
Output.emit(mapOf("pubkey" to authorHex, "count" to items.size, "napplets" to items))
return 0
}
}
/**
* `amy napplet serve <author> [--d ID] [--port N]` — fetch + aggregate-verify the manifest and
* serve its static content over a local HTTP server. NOTE: the napplet's window.napplet.* runtime
@@ -57,14 +57,78 @@ object NsiteCommands {
route(
"nsite",
tail,
"nsite <fetch|publish|serve> …",
"nsite <fetch|publish|serve|list> …",
mapOf(
"fetch" to { rest -> fetch(dataDir, rest) },
"publish" to { rest -> publish(dataDir, rest) },
"serve" to { rest -> serve(dataDir, rest) },
"list" to { rest -> list(dataDir, rest) },
),
)
/**
* `amy nsite list <author> [--relay R] [--timeout S]` — enumerate an author's static websites:
* the root (kind 15128) and every named one (kind 35128), latest per identifier.
*/
private suspend fun list(
dataDir: DataDir,
rest: Array<String>,
): Int {
val args = Args(rest)
val author = args.positionalOrNull(0) ?: return Output.error("bad_args", "nsite list <author> [--relay R] [--timeout S]")
val extraRelays = StaticSiteFetch.commaList(args.flag("relay"))
val timeoutSecs = args.longFlag("timeout", 8L)
Context.open(dataDir).use { ctx ->
ctx.prepare()
val authorHex = ctx.requireUserHex(author)
val relays =
extraRelays
.mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) }
.toSet()
.ifEmpty { ctx.bootstrapRelays() }
val filter = Filter(kinds = listOf(RootSiteEvent.KIND, NamedSiteEvent.KIND), authors = listOf(authorHex))
val items =
ctx
.drain(relays.associateWith { listOf(filter) }, timeoutSecs * 1000)
.map { (_, ev) -> ev }
.filter { it.pubKey == authorHex && (it is RootSiteEvent || it is NamedSiteEvent) }
.groupBy { it.kind to (it as? NamedSiteEvent)?.identifier() }
.mapNotNull { (_, dupes) -> dupes.maxByOrNull { it.createdAt } }
.sortedByDescending { it.createdAt }
.map { ev -> describe(ev) }
Output.emit(mapOf("pubkey" to authorHex, "count" to items.size, "nsites" to items))
return 0
}
}
private fun describe(ev: Event): Map<String, Any?> =
when (ev) {
is NamedSiteEvent ->
mapOf(
"kind" to ev.kind,
"d" to ev.identifier(),
"title" to ev.title(),
"description" to ev.description(),
"paths" to ev.paths().size,
"servers" to ev.servers(),
"event_id" to ev.id,
"created_at" to ev.createdAt,
)
is RootSiteEvent ->
mapOf(
"kind" to ev.kind,
"d" to null,
"title" to ev.title(),
"description" to ev.description(),
"paths" to ev.paths().size,
"servers" to ev.servers(),
"event_id" to ev.id,
"created_at" to ev.createdAt,
)
else -> mapOf("kind" to ev.kind, "event_id" to ev.id)
}
/**
* `amy nsite serve <author> [--d ID] [--port N]` — fetch the manifest and serve it over a local
* HTTP server (sha256-verified per request) so you can open it in a browser.
+1
View File
@@ -28,6 +28,7 @@ amy napplet publish tools/napplet-test \
the identity reads (`getProfile`, `getFollows`, …) have data.
Verify it resolves (optional): `amy napplet fetch <your-pubkey-hex> --d napplet-test`.
List everything you've published: `amy napplet list <your-pubkey-hex>` (or `amy nsite list <pubkey>`).
## Preview in a browser (optional)