@arcadiasystems/morse-cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +14 -4
- package/dist/index.js +110 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,35 @@ All notable changes to `@arcadiasystems/morse-cli` are documented here. The
|
|
|
4
4
|
format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.3.0] - 2026-06-04
|
|
8
|
+
|
|
9
|
+
Event-based file listing, wrapping the SDK 0.3.0 reconcile helpers. Purely
|
|
10
|
+
additive; all existing commands are unchanged.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `morse file list`: list files owned by an address (default: the active
|
|
15
|
+
account), or, with `--accessible`, files decryptable via allowlist membership.
|
|
16
|
+
`--address <addr>` queries another address, `--hydrate` fetches the full record
|
|
17
|
+
per file (adds `blobId`; one read each), `--limit <n>` caps results, `--json`
|
|
18
|
+
emits the summary array.
|
|
19
|
+
- Event fetching via `cli/events.ts`: a paginator over `suix_queryEvents` feeding
|
|
20
|
+
the SDK's pure `reconcileFilesOwnedBy` / `reconcileFilesAccessibleBy` helpers.
|
|
21
|
+
`--indexer-url <url>` overrides the event source (any endpoint that speaks
|
|
22
|
+
`suix_queryEvents`).
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Depends on `@arcadiasystems/morse-sdk` `^0.3.0`.
|
|
27
|
+
|
|
28
|
+
### Notes
|
|
29
|
+
|
|
30
|
+
- Listing reads `suix_queryEvents`, a deprecated Sui JSON-RPC endpoint Mysten is
|
|
31
|
+
sunsetting; on the public RPC it may degrade over time. Use `--indexer-url` to
|
|
32
|
+
point at your own indexer. Results are best-effort and eventually consistent.
|
|
33
|
+
- Summaries omit `blobId`/`blobObjectId` (not in the `FileCreated` event); use
|
|
34
|
+
`--hydrate` to fetch them.
|
|
35
|
+
|
|
7
36
|
## [0.2.0] - 2026-06-04
|
|
8
37
|
|
|
9
38
|
Wraps the allowlist + encrypted-file surface from `@arcadiasystems/morse-sdk`
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ content entries from your terminal, signing with a locally encrypted key.
|
|
|
6
6
|
Content is stored on [Walrus](https://walrus.xyz); private entries are encrypted
|
|
7
7
|
with [Seal](https://github.com/MystenLabs/seal).
|
|
8
8
|
|
|
9
|
-
> Status: v0.
|
|
9
|
+
> Status: v0.3.0, targeting Sui testnet. The command surface is stable; mainnet
|
|
10
10
|
> support lands when the contracts are frozen.
|
|
11
11
|
|
|
12
12
|
## Requirements
|
|
@@ -250,6 +250,7 @@ auto-resolved from the active account when `--cap` is omitted.
|
|
|
250
250
|
| `file upload <path> --name <n> [-a <id>] [--public] [--content-type <m>] [--epochs <n>]` | Upload to Walrus and register; `-a` encrypts, `--public` is world-readable. |
|
|
251
251
|
| `file register --blob-id <id> --name <n> --content-type <m> --size <bytes> [-a <id>] [--public] [--blob-object-id <id>]` | Register metadata for a blob already on Walrus. |
|
|
252
252
|
| `file download <file> [--out <path>] [--seal-id <hex>] [--via-aggregator]` | Download content; decrypts in place when encrypted. |
|
|
253
|
+
| `file list [--address <addr>] [--accessible] [--hydrate] [--limit <n>] [--indexer-url <url>]` | List files owned by (or, with `--accessible`, decryptable by) an address. |
|
|
253
254
|
| `file get <file>` | Fetch a file's on-chain metadata. |
|
|
254
255
|
| `file update <file> --name <n> --content-type <m>` | Update name and MIME (owner only). |
|
|
255
256
|
| `file transfer-ownership <file> <newOwner> [-y]` | Transfer the metadata right (not decrypt access). |
|
|
@@ -257,9 +258,18 @@ auto-resolved from the active account when `--cap` is omitted.
|
|
|
257
258
|
|
|
258
259
|
Encrypting a file (`file upload -a <allowlist>`) prints a **seal id**. It is not
|
|
259
260
|
recoverable from the ciphertext, so save it: `file download` needs it (via
|
|
260
|
-
`--seal-id`) plus allowlist membership to decrypt.
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
`--seal-id`) plus allowlist membership to decrypt.
|
|
262
|
+
|
|
263
|
+
`file list` reconstructs the file set from contract events. Encrypted files are
|
|
264
|
+
shared objects with no on-chain owner index, so listing is event-derived, not a
|
|
265
|
+
direct query. By default the command reads events via `suix_queryEvents` on the
|
|
266
|
+
configured Sui RPC. That endpoint is **deprecated** (Mysten is sunsetting it), so
|
|
267
|
+
listing may degrade or stop working on the public RPC over time; point
|
|
268
|
+
`--indexer-url <url>` at any source that speaks `suix_queryEvents` (a self-hosted
|
|
269
|
+
indexer, a third-party endpoint) to stay in control. Results are best-effort and
|
|
270
|
+
eventually consistent (subject to indexer lag and retention). `EncryptedFileSummary`
|
|
271
|
+
rows omit `blobId`/`blobObjectId`; add `--hydrate` to fetch the full record per
|
|
272
|
+
file (one read each) when you need them.
|
|
263
273
|
|
|
264
274
|
## Output and scripting
|
|
265
275
|
|
package/dist/index.js
CHANGED
|
@@ -148,7 +148,7 @@ import { Command } from "commander";
|
|
|
148
148
|
// package.json
|
|
149
149
|
var package_default = {
|
|
150
150
|
name: "@arcadiasystems/morse-cli",
|
|
151
|
-
version: "0.
|
|
151
|
+
version: "0.3.0",
|
|
152
152
|
description: "Command-line interface for the Morse decentralized CMS on Sui.",
|
|
153
153
|
license: "MIT",
|
|
154
154
|
type: "module",
|
|
@@ -203,7 +203,7 @@ var package_default = {
|
|
|
203
203
|
prepublishOnly: "bun run check && bun run build"
|
|
204
204
|
},
|
|
205
205
|
dependencies: {
|
|
206
|
-
"@arcadiasystems/morse-sdk": "^0.
|
|
206
|
+
"@arcadiasystems/morse-sdk": "^0.3.0",
|
|
207
207
|
"@mysten/seal": "1.1.3",
|
|
208
208
|
"@mysten/sui": "2.16.2",
|
|
209
209
|
"@mysten/walrus": "1.1.6",
|
|
@@ -902,6 +902,7 @@ import {
|
|
|
902
902
|
|
|
903
903
|
// src/cli/context.ts
|
|
904
904
|
import {
|
|
905
|
+
buildFilesEventTypes,
|
|
905
906
|
DefaultSealAdapter,
|
|
906
907
|
DefaultWalrusReadAdapter,
|
|
907
908
|
DefaultWalrusWriteAdapter,
|
|
@@ -912,6 +913,7 @@ import {
|
|
|
912
913
|
RpcPublicationReader
|
|
913
914
|
} from "@arcadiasystems/morse-sdk";
|
|
914
915
|
import { SuiGrpcClient } from "@mysten/sui/grpc";
|
|
916
|
+
import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc";
|
|
915
917
|
async function buildReadContext(command) {
|
|
916
918
|
const opts = globalOptions(command);
|
|
917
919
|
const output = outputFor(command);
|
|
@@ -1026,6 +1028,23 @@ async function buildFileDownloadContext(command, opts = {}) {
|
|
|
1026
1028
|
unlockSigner: () => resolveSigner(base.settings.account, process.env, base.signal)
|
|
1027
1029
|
};
|
|
1028
1030
|
}
|
|
1031
|
+
async function buildFileListContext(command, opts = {}) {
|
|
1032
|
+
const base = await buildReadContext(command);
|
|
1033
|
+
const originPackageId = base.config.filesEventOriginPackageId;
|
|
1034
|
+
if (originPackageId === undefined) {
|
|
1035
|
+
throw new CliError("File listing is unavailable on this network: no filesEventOriginPackageId in the config. Use testnet, or supply a config that sets it.", ExitCode.Usage);
|
|
1036
|
+
}
|
|
1037
|
+
const events = new SuiJsonRpcClient({
|
|
1038
|
+
network: base.settings.network,
|
|
1039
|
+
url: opts.indexerUrl ?? base.config.rpcUrl
|
|
1040
|
+
});
|
|
1041
|
+
return {
|
|
1042
|
+
...base,
|
|
1043
|
+
filesReader: RpcFilesReader.fromMorseConfig(base.config, base.client),
|
|
1044
|
+
events,
|
|
1045
|
+
eventTypes: buildFilesEventTypes(originPackageId)
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1029
1048
|
|
|
1030
1049
|
// src/format/ids.ts
|
|
1031
1050
|
var HEAD = 6;
|
|
@@ -1111,6 +1130,20 @@ function renderAllowlistCapList(caps) {
|
|
|
1111
1130
|
return caps.map((cap) => `${cap.id} allowlist ${cap.allowlistId}`).join(`
|
|
1112
1131
|
`);
|
|
1113
1132
|
}
|
|
1133
|
+
function renderFileList(items) {
|
|
1134
|
+
if (items.length === 0) {
|
|
1135
|
+
return "No files.";
|
|
1136
|
+
}
|
|
1137
|
+
const header = "id name size encrypted allowlist created";
|
|
1138
|
+
const rows = items.map((file) => {
|
|
1139
|
+
const allowlist = file.allowlistId === null ? "public" : shortId(file.allowlistId);
|
|
1140
|
+
const created = new Date(file.createdAtMs).toISOString().slice(0, 10);
|
|
1141
|
+
const encrypted = file.encrypted ? "yes" : "no";
|
|
1142
|
+
return `${shortId(file.id)} ${file.name} ${file.size} ${encrypted} ${allowlist} ${created}`;
|
|
1143
|
+
});
|
|
1144
|
+
return [header, ...rows].join(`
|
|
1145
|
+
`);
|
|
1146
|
+
}
|
|
1114
1147
|
function renderEncryptedFile(file) {
|
|
1115
1148
|
const lines = [
|
|
1116
1149
|
`${file.name} (${shortId(file.id)})`,
|
|
@@ -1997,6 +2030,8 @@ import {
|
|
|
1997
2030
|
createEncryptedFile,
|
|
1998
2031
|
createPublicFile,
|
|
1999
2032
|
deleteFile,
|
|
2033
|
+
reconcileFilesAccessibleBy,
|
|
2034
|
+
reconcileFilesOwnedBy,
|
|
2000
2035
|
toAllowlistId as toAllowlistId2,
|
|
2001
2036
|
toBlobObjectId,
|
|
2002
2037
|
toEncryptedFileId,
|
|
@@ -2009,6 +2044,38 @@ import {
|
|
|
2009
2044
|
} from "@arcadiasystems/morse-sdk";
|
|
2010
2045
|
import { SessionKey as SessionKey2 } from "@mysten/seal";
|
|
2011
2046
|
|
|
2047
|
+
// src/cli/events.ts
|
|
2048
|
+
var PAGE_LIMIT2 = 50;
|
|
2049
|
+
async function fetchEventStream(client, eventType, signal) {
|
|
2050
|
+
const out = [];
|
|
2051
|
+
let cursor;
|
|
2052
|
+
for (;; ) {
|
|
2053
|
+
const page = await client.queryEvents({
|
|
2054
|
+
query: { MoveEventType: eventType },
|
|
2055
|
+
limit: PAGE_LIMIT2,
|
|
2056
|
+
order: "descending",
|
|
2057
|
+
...cursor === undefined ? {} : { cursor },
|
|
2058
|
+
...signal === undefined ? {} : { signal }
|
|
2059
|
+
});
|
|
2060
|
+
for (const event of page.data) {
|
|
2061
|
+
out.push({
|
|
2062
|
+
type: event.type,
|
|
2063
|
+
parsedJson: event.parsedJson,
|
|
2064
|
+
timestampMs: event.timestampMs
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
if (!page.hasNextPage || page.nextCursor === null || page.nextCursor === undefined) {
|
|
2068
|
+
break;
|
|
2069
|
+
}
|
|
2070
|
+
cursor = page.nextCursor;
|
|
2071
|
+
}
|
|
2072
|
+
return out;
|
|
2073
|
+
}
|
|
2074
|
+
async function fetchEventStreams(client, eventTypes, signal) {
|
|
2075
|
+
const streams = await Promise.all(eventTypes.map((type) => fetchEventStream(client, type, signal)));
|
|
2076
|
+
return streams.flat();
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2012
2079
|
// src/format/hex.ts
|
|
2013
2080
|
var HEX = /^[0-9a-fA-F]+$/;
|
|
2014
2081
|
function encodeHex(bytes) {
|
|
@@ -2216,12 +2283,53 @@ async function runFileDownload(ctx, target, options) {
|
|
|
2216
2283
|
}
|
|
2217
2284
|
process.stdout.write(content);
|
|
2218
2285
|
}
|
|
2286
|
+
async function runFileList(ctx, options) {
|
|
2287
|
+
const limit = options.limit === undefined ? undefined : parseLimit(options.limit);
|
|
2288
|
+
const address = options.address === undefined ? ctx.ownerAddress : toSuiAddress5(options.address);
|
|
2289
|
+
if (address === undefined) {
|
|
2290
|
+
throw new UsageError("No address given and no active account. Pass --address or import an account.");
|
|
2291
|
+
}
|
|
2292
|
+
const types = ctx.eventTypes;
|
|
2293
|
+
let summaries;
|
|
2294
|
+
if (options.accessible) {
|
|
2295
|
+
ctx.output.info("Fetching membership and file events...");
|
|
2296
|
+
const events = await fetchEventStreams(ctx.events, [
|
|
2297
|
+
types.MemberAdded,
|
|
2298
|
+
types.MemberRemoved,
|
|
2299
|
+
types.AllowlistDeleted,
|
|
2300
|
+
types.FileCreated,
|
|
2301
|
+
types.FileDeleted
|
|
2302
|
+
], ctx.signal);
|
|
2303
|
+
summaries = reconcileFilesAccessibleBy(events, address, types);
|
|
2304
|
+
} else {
|
|
2305
|
+
ctx.output.info("Fetching file events...");
|
|
2306
|
+
const events = await fetchEventStreams(ctx.events, [types.FileCreated, types.FileOwnershipTransferred, types.FileDeleted], ctx.signal);
|
|
2307
|
+
summaries = reconcileFilesOwnedBy(events, address, types);
|
|
2308
|
+
}
|
|
2309
|
+
const limited = limit === undefined ? summaries : summaries.slice(0, limit);
|
|
2310
|
+
let items = limited;
|
|
2311
|
+
if (options.hydrate) {
|
|
2312
|
+
ctx.output.info(`Hydrating ${limited.length} file(s)...`);
|
|
2313
|
+
const full = [];
|
|
2314
|
+
for (const summary of limited) {
|
|
2315
|
+
const record = await ctx.filesReader.getEncryptedFile(summary.id, ctx.signal);
|
|
2316
|
+
full.push({ ...record, kind: "full" });
|
|
2317
|
+
}
|
|
2318
|
+
items = full;
|
|
2319
|
+
}
|
|
2320
|
+
ctx.output.result(renderFileList(items), items);
|
|
2321
|
+
}
|
|
2219
2322
|
function registerFileCommands(program) {
|
|
2220
2323
|
const file = program.command("file").description("Upload, register, read, and manage files on Walrus");
|
|
2221
2324
|
const upload = file.command("upload <path>").description("Encrypt (or not), upload to Walrus, and register a file").requiredOption("-n, --name <name>", "File name").option("--public", "Upload a world-readable (unencrypted) file").option("--content-type <mime>", "MIME content type (inferred from the path if omitted)").option("--epochs <n>", "Walrus storage epochs", "3");
|
|
2222
2325
|
allowlistOption(upload).action(async (path, options, command) => {
|
|
2223
2326
|
await runFileUpload(await buildEncryptContext(command), path, options);
|
|
2224
2327
|
});
|
|
2328
|
+
file.command("list").description("List files owned by (or accessible to) an address, via event indexing").option("--address <addr>", "Address to query (default: the active account)").option("--accessible", "List files you can decrypt via allowlist membership, not just owned").option("--hydrate", "Fetch full records (adds blobId; one read per file)").option("--limit <n>", "Cap the number of results").option("--indexer-url <url>", "Event source URL (default: the RPC; uses suix_queryEvents, a deprecated Sui endpoint)").action(async (options, command) => {
|
|
2329
|
+
await runFileList(await buildFileListContext(command, {
|
|
2330
|
+
indexerUrl: options.indexerUrl
|
|
2331
|
+
}), options);
|
|
2332
|
+
});
|
|
2225
2333
|
const download = file.command("download <file>").description("Download a file's content; decrypts in place when encrypted").option("--out <path>", "Write content to a file instead of stdout").option("--seal-id <hex>", "Seal id from upload (required to decrypt an encrypted file)");
|
|
2226
2334
|
viaAggregatorOption(download).action(async (target, options, command) => {
|
|
2227
2335
|
await runFileDownload(await buildFileDownloadContext(command, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcadiasystems/morse-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Command-line interface for the Morse decentralized CMS on Sui.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"prepublishOnly": "bun run check && bun run build"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@arcadiasystems/morse-sdk": "^0.
|
|
58
|
+
"@arcadiasystems/morse-sdk": "^0.3.0",
|
|
59
59
|
"@mysten/seal": "1.1.3",
|
|
60
60
|
"@mysten/sui": "2.16.2",
|
|
61
61
|
"@mysten/walrus": "1.1.6",
|