@arcadiasystems/morse-cli 0.7.0 → 0.9.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 +38 -0
- package/README.md +32 -12
- package/dist/index.js +146 -52
- package/docs/QUICKSTART.md +12 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,44 @@ 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.9.0] - 2026-09-10
|
|
8
|
+
### Fixed (config state, found in a whole-package bug hunt)
|
|
9
|
+
|
|
10
|
+
- **A one-off `--network` permanently rewrote the profile's stored network.** `updateActiveProfile` resolved the network the same way `resolveSettings` does (flag > env > stored > default) and then wrote the result back, even though the caller was only changing the account, publication or collection. So `morse --network testnet use --clear` against a mainnet profile left `"network": "testnet"` on disk, and every later command silently targeted the wrong chain with nothing said. Every active-state mutation was affected: `account import`, `account use`, `use`, `use --clear`, `publication create`, `publication delete`, `collection create`, `collection delete`. An existing profile now keeps its stored network; only a profile being created takes the resolved one. Flags and env vars are per-invocation overrides again, as `CLAUDE.md`'s precedence rule always said.
|
|
11
|
+
- **`config add` on an existing profile destroyed every field it was not given.** It replaced the profile object instead of merging, so `config add default --network testnet` on a profile that had an `rpc`, `uploadRelay`, `account`, `publication` or `collection` silently dropped all of them. The command has always been documented as "create or **update**". It merges now.
|
|
12
|
+
- **`config remove` deleted a profile with no confirmation.** Every other destructive command confirms and honours `--yes`; this one took no `GlobalOptions` at all, so there was not even a flag to skip. It deletes a profile plus its account, publication and collection links and can silently reassign the default, so it now says which profile and, when relevant, what the default becomes.
|
|
13
|
+
- **Collection selection ignored which publication a collection belonged to.** `collection create` selected the new collection as active even when created under an explicit `-P` pointing at some other publication, pairing the active publication with a collection it does not contain. `collection delete` had the mirror bug, clearing a still-valid active collection whenever a same-named one was deleted from an unrelated publication. Collection names are scoped per publication, so both now check the publication too. The existing test asserted the old behaviour and passed vacuously, with no profile written; it has been given a real one and joined by cases for both mismatch directions.
|
|
14
|
+
- `file register --public --recipient` is now refused, matching `file upload`. A public file is readable by anyone, so a recipient list on one is a contradiction rather than a no-op.
|
|
15
|
+
- `--max-tip` help text says it only applies alongside `--upload-relay`.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **`morse file list` works again, on every network.** It walked `suix_queryEvents` over JSON-RPC, which public Sui fullnodes have retired: they answer every JSON-RPC method with "Method not found". The command has been failing on testnet and mainnet alike unless the user passed `--indexer-url` at a source that still spoke the old protocol.
|
|
21
|
+
|
|
22
|
+
gRPC is the documented replacement for most Sui reads, but `@mysten/sui`'s gRPC client exposes no event API at all, so it could not be the answer here. Events now come from a Sui GraphQL endpoint, defaulting to the canonical Mysten one for the network. `--indexer-url` still overrides it and now means "a Sui GraphQL endpoint".
|
|
23
|
+
|
|
24
|
+
The querier is hand-rolled over `fetch` against one query with one cursor, rather than pulling in a GraphQL client, keeping the dependency surface as-is. A GraphQL error or a non-2xx response raises a network `CliError` (exit 5) rather than an empty page, because reporting "No files" for a broken endpoint is the worst available answer.
|
|
25
|
+
|
|
26
|
+
Verified live: a file uploaded to testnet appears in `file list` with no `--indexer-url`, `--hydrate` and `--json` both work against it, and deleting the file reconciles it back out of the listing.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `--indexer-url` now expects a Sui GraphQL endpoint rather than a `suix_queryEvents` JSON-RPC source. Anyone relying on the old flag against a JSON-RPC indexer needs a GraphQL one instead; the previous behaviour could not have been working against a public fullnode regardless.
|
|
31
|
+
- Listing on localnet requires `--indexer-url`, since there is no canonical GraphQL endpoint for it. Previously it would have used the local RPC URL and failed.
|
|
32
|
+
|
|
33
|
+
## [0.8.0] - 2026-09-10
|
|
34
|
+
|
|
35
|
+
Usability pass. Nothing is renamed or removed; every existing spelling keeps working.
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **Verb aliases, so the word you guess is the word that works.** The command surface had grown four inconsistencies where one concept had two names depending on the noun: fetching one thing was `get` on publication, entry and file but `show` on account; deleting was `delete` everywhere except `config remove`; transferring was `transfer-ownership` on publication and file but `transfer` on cap; and reading content was `entry read` but `file download`, despite both taking `--out` and `--via-aggregator`.
|
|
40
|
+
|
|
41
|
+
Each now accepts both spellings: `account show|get`, `publication get|show`, `entry get|show`, `file get|show`, `config remove|delete`, `cap transfer|transfer-ownership`, `entry read|download`, `file download|read`. Aliases, not renames, so nothing published breaks.
|
|
42
|
+
- **`entry list --all`** does what `entry scan` does. `scan` is accurate but undiscoverable; people look for `--all` and conclude pagination is unsupported. `scan` stays. Combining `--all` with `--limit` or `--cursor` is refused rather than silently ignoring one.
|
|
43
|
+
- **`account import` reads a key piped on stdin.** Previously it required an interactive terminal or `MORSE_PRIVATE_KEY`, so the obvious scripted form failed outright. Still never a flag: argv is visible in `ps` and shell history. The error when no key can be found now names all three routes.
|
|
44
|
+
|
|
7
45
|
## [0.7.0] - 2026-09-09
|
|
8
46
|
|
|
9
47
|
### Added
|
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.9.0. Mainnet and testnet are both supported for public content;
|
|
10
10
|
> the command surface is stable.
|
|
11
11
|
>
|
|
12
12
|
> **Encrypted commands are testnet-only.** `entry add-encrypted`, `entry
|
|
@@ -173,9 +173,9 @@ and `-C, --collection <name>`, both defaulting to the active context.
|
|
|
173
173
|
| Command | Purpose |
|
|
174
174
|
| --- | --- |
|
|
175
175
|
| `config add <name> --network <net> [--rpc <url>] [--upload-relay <url\|auto>]` | Create or update a profile. |
|
|
176
|
+
| `config remove <name>` | Delete a profile. Also `config delete`. |
|
|
176
177
|
| `config list` | List profiles; `*` marks the default. |
|
|
177
178
|
| `config use <name>` | Set the default profile. |
|
|
178
|
-
| `config remove <name>` | Delete a profile. |
|
|
179
179
|
| `config path` | Print the config file path. |
|
|
180
180
|
|
|
181
181
|
### account
|
|
@@ -281,13 +281,13 @@ also returns the raw `sealIdPrefix` and `sealNonce` as hex, usable as `--prefix`
|
|
|
281
281
|
|
|
282
282
|
`file list` reconstructs the file set from contract events. `RecipientFile`
|
|
283
283
|
objects have no on-chain owner index for the shared case, so listing is
|
|
284
|
-
event-derived, not a direct query.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
lag and retention). Summary rows omit `blobId`/`blobObjectId`; add `--hydrate`
|
|
284
|
+
event-derived, not a direct query. The command reads events from a Sui GraphQL
|
|
285
|
+
endpoint, defaulting to the canonical Mysten one for the network. It used to use
|
|
286
|
+
`suix_queryEvents` over JSON-RPC, which public fullnodes have since retired;
|
|
287
|
+
`@mysten/sui`'s gRPC client exposes no event API, so GraphQL is the transport
|
|
288
|
+
that remains. Point `--indexer-url <url>` at a different GraphQL endpoint (a
|
|
289
|
+
self-hosted indexer, a third-party endpoint) to stay in control. Results are
|
|
290
|
+
best-effort and eventually consistent (subject to indexer lag and retention). Summary rows omit `blobId`/`blobObjectId`; add `--hydrate`
|
|
291
291
|
to fetch the full record per file (one read each) when you need them.
|
|
292
292
|
|
|
293
293
|
## Output and scripting
|
|
@@ -348,9 +348,29 @@ to fetch the full record per file (one read each) when you need them.
|
|
|
348
348
|
an operator, use the SDK directly for now.
|
|
349
349
|
- `localnet` is accepted as a network but has no canonical deployment, so it
|
|
350
350
|
fails at startup unless you point the SDK at your own package and registry.
|
|
351
|
-
- `file list`
|
|
352
|
-
|
|
353
|
-
|
|
351
|
+
- `file list` reads events from a Sui GraphQL endpoint, defaulting to the
|
|
352
|
+
canonical one for the network. `--indexer-url` points it at a different
|
|
353
|
+
GraphQL endpoint. There is no canonical endpoint for localnet, so listing
|
|
354
|
+
there requires the flag.
|
|
355
|
+
|
|
356
|
+
## Command spellings
|
|
357
|
+
|
|
358
|
+
Several commands accept two verbs for the same action, so whichever you reach
|
|
359
|
+
for works. These are aliases, not separate commands:
|
|
360
|
+
|
|
361
|
+
| Canonical | Also accepts |
|
|
362
|
+
| --- | --- |
|
|
363
|
+
| `account show` | `account get` |
|
|
364
|
+
| `publication get` | `publication show` |
|
|
365
|
+
| `entry get` | `entry show` |
|
|
366
|
+
| `file get` | `file show` |
|
|
367
|
+
| `config remove` | `config delete` |
|
|
368
|
+
| `cap transfer` | `cap transfer-ownership` |
|
|
369
|
+
| `entry read` | `entry download` |
|
|
370
|
+
| `file download` | `file read` |
|
|
371
|
+
| `entry scan` | `entry list --all` |
|
|
372
|
+
|
|
373
|
+
`publication` also answers to `pub`.
|
|
354
374
|
|
|
355
375
|
## When uploads fail: the upload relay
|
|
356
376
|
|
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.9.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.8.0",
|
|
207
207
|
"@mysten/seal": "1.1.3",
|
|
208
208
|
"@mysten/sui": "2.16.2",
|
|
209
209
|
"@mysten/walrus": "1.1.6",
|
|
@@ -220,10 +220,37 @@ var package_default = {
|
|
|
220
220
|
var DEFAULT_MAX_TIP_MIST = 1e7;
|
|
221
221
|
function buildProgram() {
|
|
222
222
|
const program = new Command;
|
|
223
|
-
program.name("morse").description("Command-line interface for the Morse decentralized CMS on Sui.").version(package_default.version, "-V, --version", "Print the version and exit").option("--network <network>", "Sui network: mainnet, testnet, or localnet [env: MORSE_NETWORK]").option("-p, --profile <name>", "Config profile to use [env: MORSE_PROFILE]").option("--rpc <url>", "Override the Sui RPC URL [env: MORSE_RPC_URL]").option("--upload-relay <url|auto>", "Upload Walrus blobs through a relay instead of fanning out to every storage node; 'auto' picks the canonical relay for the network. Costs a tip per upload [env: MORSE_WALRUS_UPLOAD_RELAY]").option("--max-tip <mist>", `Cap the per-upload relay tip, in MIST (default: ${DEFAULT_MAX_TIP_MIST}) [env: MORSE_WALRUS_MAX_TIP]`).option("--json", "Output machine-readable JSON on stdout").option("-q, --quiet", "Suppress progress and informational output").option("-y, --yes", "Assume yes for confirmation prompts").option("--debug", "Print stack traces on error").enablePositionalOptions().showHelpAfterError().exitOverride();
|
|
223
|
+
program.name("morse").description("Command-line interface for the Morse decentralized CMS on Sui.").version(package_default.version, "-V, --version", "Print the version and exit").option("--network <network>", "Sui network: mainnet, testnet, or localnet [env: MORSE_NETWORK]").option("-p, --profile <name>", "Config profile to use [env: MORSE_PROFILE]").option("--rpc <url>", "Override the Sui RPC URL [env: MORSE_RPC_URL]").option("--upload-relay <url|auto>", "Upload Walrus blobs through a relay instead of fanning out to every storage node; 'auto' picks the canonical relay for the network. Costs a tip per upload [env: MORSE_WALRUS_UPLOAD_RELAY]").option("--max-tip <mist>", `Cap the per-upload relay tip, in MIST (default: ${DEFAULT_MAX_TIP_MIST}). Only applies with --upload-relay [env: MORSE_WALRUS_MAX_TIP]`).option("--json", "Output machine-readable JSON on stdout").option("-q, --quiet", "Suppress progress and informational output").option("-y, --yes", "Assume yes for confirmation prompts").option("--debug", "Print stack traces on error").enablePositionalOptions().showHelpAfterError().exitOverride();
|
|
224
224
|
return program;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
// src/cli/io.ts
|
|
228
|
+
import { access, readFile, writeFile } from "node:fs/promises";
|
|
229
|
+
async function fileExists(path) {
|
|
230
|
+
try {
|
|
231
|
+
await access(path);
|
|
232
|
+
return true;
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function readBytes(path) {
|
|
238
|
+
return new Uint8Array(await readFile(path));
|
|
239
|
+
}
|
|
240
|
+
async function readJson(path) {
|
|
241
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
242
|
+
}
|
|
243
|
+
async function writeFileContents(path, data) {
|
|
244
|
+
await writeFile(path, data);
|
|
245
|
+
}
|
|
246
|
+
async function readStdin() {
|
|
247
|
+
const chunks = [];
|
|
248
|
+
for await (const chunk of process.stdin) {
|
|
249
|
+
chunks.push(chunk);
|
|
250
|
+
}
|
|
251
|
+
return new Uint8Array(Buffer.concat(chunks));
|
|
252
|
+
}
|
|
253
|
+
|
|
227
254
|
// src/cli/prompts.ts
|
|
228
255
|
import * as readline from "node:readline";
|
|
229
256
|
function isInteractive() {
|
|
@@ -487,33 +514,6 @@ function malformed(source, detail) {
|
|
|
487
514
|
// src/config/store.ts
|
|
488
515
|
import { chmod, mkdir, rename } from "node:fs/promises";
|
|
489
516
|
|
|
490
|
-
// src/cli/io.ts
|
|
491
|
-
import { access, readFile, writeFile } from "node:fs/promises";
|
|
492
|
-
async function fileExists(path) {
|
|
493
|
-
try {
|
|
494
|
-
await access(path);
|
|
495
|
-
return true;
|
|
496
|
-
} catch {
|
|
497
|
-
return false;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
async function readBytes(path) {
|
|
501
|
-
return new Uint8Array(await readFile(path));
|
|
502
|
-
}
|
|
503
|
-
async function readJson(path) {
|
|
504
|
-
return JSON.parse(await readFile(path, "utf8"));
|
|
505
|
-
}
|
|
506
|
-
async function writeFileContents(path, data) {
|
|
507
|
-
await writeFile(path, data);
|
|
508
|
-
}
|
|
509
|
-
async function readStdin() {
|
|
510
|
-
const chunks = [];
|
|
511
|
-
for await (const chunk of process.stdin) {
|
|
512
|
-
chunks.push(chunk);
|
|
513
|
-
}
|
|
514
|
-
return new Uint8Array(Buffer.concat(chunks));
|
|
515
|
-
}
|
|
516
|
-
|
|
517
517
|
// src/config/paths.ts
|
|
518
518
|
import { homedir } from "node:os";
|
|
519
519
|
import { join } from "node:path";
|
|
@@ -559,7 +559,7 @@ async function updateActiveProfile(opts, patch, env = process.env) {
|
|
|
559
559
|
const config = await loadConfig();
|
|
560
560
|
const profileName = opts.profile ?? env.MORSE_PROFILE ?? config.defaultProfile;
|
|
561
561
|
const existing = config.profiles[profileName];
|
|
562
|
-
const network = coerceNetwork(opts.network ?? env.MORSE_NETWORK ??
|
|
562
|
+
const network = existing?.network ?? coerceNetwork(opts.network ?? env.MORSE_NETWORK ?? "testnet");
|
|
563
563
|
const merged = { ...existing, network, ...patch };
|
|
564
564
|
const profiles = { ...config.profiles, [profileName]: merged };
|
|
565
565
|
const defaultProfile = Object.keys(config.profiles).length === 0 ? profileName : config.defaultProfile;
|
|
@@ -925,7 +925,7 @@ function registerAccountCommands(program) {
|
|
|
925
925
|
account.command("list").description("List imported accounts").action(async (_options, command) => {
|
|
926
926
|
await runAccountList(outputFor(command), globalOptions(command));
|
|
927
927
|
});
|
|
928
|
-
account.command("show").description("Print the active account address").action(async (_options, command) => {
|
|
928
|
+
account.command("show").alias("get").description("Print the active account address").action(async (_options, command) => {
|
|
929
929
|
await runAccountShow(outputFor(command), globalOptions(command));
|
|
930
930
|
});
|
|
931
931
|
account.command("use <address>").description("Set the active account for the current profile").action(async (address, _options, command) => {
|
|
@@ -940,8 +940,15 @@ async function readSecretToImport(env, signal) {
|
|
|
940
940
|
if (raw !== undefined && raw.length > 0) {
|
|
941
941
|
return raw;
|
|
942
942
|
}
|
|
943
|
+
if (!process.stdin.isTTY) {
|
|
944
|
+
const piped = new TextDecoder().decode(await readStdin()).trim();
|
|
945
|
+
if (piped.length > 0) {
|
|
946
|
+
return piped;
|
|
947
|
+
}
|
|
948
|
+
throw new UsageError("Cannot read a private key: pipe it on stdin, set MORSE_PRIVATE_KEY, or run in an interactive terminal.");
|
|
949
|
+
}
|
|
943
950
|
if (!isInteractive()) {
|
|
944
|
-
throw new UsageError("Cannot read a private key: set MORSE_PRIVATE_KEY or run in an interactive terminal.");
|
|
951
|
+
throw new UsageError("Cannot read a private key: pipe it on stdin, set MORSE_PRIVATE_KEY, or run in an interactive terminal.");
|
|
945
952
|
}
|
|
946
953
|
process.stderr.write(`Paste your Sui private key (starts with suiprivkey1...), then press Enter. Input is hidden.
|
|
947
954
|
`);
|
|
@@ -981,7 +988,70 @@ import {
|
|
|
981
988
|
RpcRecipientFilesReader
|
|
982
989
|
} from "@arcadiasystems/morse-sdk";
|
|
983
990
|
import { SuiGrpcClient } from "@mysten/sui/grpc";
|
|
984
|
-
|
|
991
|
+
|
|
992
|
+
// src/cli/graphql-events.ts
|
|
993
|
+
function canonicalGraphqlUrl(network) {
|
|
994
|
+
if (network === "mainnet" || network === "testnet") {
|
|
995
|
+
return `https://graphql.${network}.sui.io/graphql`;
|
|
996
|
+
}
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
var QUERY = `query Events($type: String!, $first: Int!, $after: String) {
|
|
1000
|
+
events(filter: { type: $type }, first: $first, after: $after) {
|
|
1001
|
+
pageInfo { hasNextPage endCursor }
|
|
1002
|
+
nodes { timestamp contents { json type { repr } } }
|
|
1003
|
+
}
|
|
1004
|
+
}`;
|
|
1005
|
+
|
|
1006
|
+
class GraphqlEventQuerier {
|
|
1007
|
+
#url;
|
|
1008
|
+
#fetch;
|
|
1009
|
+
constructor(url, fetchImpl = fetch) {
|
|
1010
|
+
this.#url = url;
|
|
1011
|
+
this.#fetch = fetchImpl;
|
|
1012
|
+
}
|
|
1013
|
+
async queryEvents(params) {
|
|
1014
|
+
const body = JSON.stringify({
|
|
1015
|
+
query: QUERY,
|
|
1016
|
+
variables: {
|
|
1017
|
+
type: params.query.MoveEventType,
|
|
1018
|
+
first: params.limit ?? 50,
|
|
1019
|
+
after: typeof params.cursor === "string" ? params.cursor : null
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
let response;
|
|
1023
|
+
try {
|
|
1024
|
+
response = await this.#fetch(this.#url, {
|
|
1025
|
+
method: "POST",
|
|
1026
|
+
headers: { "content-type": "application/json" },
|
|
1027
|
+
body,
|
|
1028
|
+
...params.signal === undefined ? {} : { signal: params.signal }
|
|
1029
|
+
});
|
|
1030
|
+
} catch (cause) {
|
|
1031
|
+
throw new CliError(`Could not reach the event source at ${this.#url}.`, ExitCode.Network, { cause });
|
|
1032
|
+
}
|
|
1033
|
+
if (!response.ok) {
|
|
1034
|
+
throw new CliError(`Event source at ${this.#url} returned HTTP ${response.status}.`, ExitCode.Network);
|
|
1035
|
+
}
|
|
1036
|
+
const payload = await response.json();
|
|
1037
|
+
if (payload.errors && payload.errors.length > 0) {
|
|
1038
|
+
throw new CliError(`Event source rejected the query: ${payload.errors[0]?.message ?? "unknown error"}`, ExitCode.Network);
|
|
1039
|
+
}
|
|
1040
|
+
const events = payload.data?.events;
|
|
1041
|
+
const nodes = events?.nodes ?? [];
|
|
1042
|
+
return {
|
|
1043
|
+
data: nodes.map((node) => ({
|
|
1044
|
+
type: node.contents?.type?.repr ?? "",
|
|
1045
|
+
parsedJson: node.contents?.json ?? {},
|
|
1046
|
+
timestampMs: node.timestamp == null ? null : String(Date.parse(node.timestamp) || 0)
|
|
1047
|
+
})),
|
|
1048
|
+
hasNextPage: Boolean(events?.pageInfo?.hasNextPage),
|
|
1049
|
+
nextCursor: events?.pageInfo?.endCursor ?? null
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// src/cli/context.ts
|
|
985
1055
|
async function buildReadContext(command) {
|
|
986
1056
|
const opts = globalOptions(command);
|
|
987
1057
|
const output = outputFor(command);
|
|
@@ -1131,16 +1201,16 @@ async function buildFileListContext(command, opts = {}) {
|
|
|
1131
1201
|
if (originPackageId === undefined) {
|
|
1132
1202
|
throw new CliError("File listing is unavailable on this network: no recipientFileEventOriginPackageId in the config. Use testnet, or supply a config that sets it.", ExitCode.Usage);
|
|
1133
1203
|
}
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
}
|
|
1204
|
+
const indexerUrl = opts.indexerUrl ?? canonicalGraphqlUrl(base.settings.network);
|
|
1205
|
+
if (indexerUrl === undefined) {
|
|
1206
|
+
throw new CliError(`File listing has no canonical event source for ${base.settings.network}. Pass --indexer-url pointing at a Sui GraphQL endpoint.`, ExitCode.Usage);
|
|
1207
|
+
}
|
|
1138
1208
|
return {
|
|
1139
1209
|
...base,
|
|
1140
1210
|
filesReader: RpcRecipientFilesReader.fromConfig(base.client, {
|
|
1141
1211
|
packageId: base.config.packageId
|
|
1142
1212
|
}),
|
|
1143
|
-
events,
|
|
1213
|
+
events: new GraphqlEventQuerier(indexerUrl),
|
|
1144
1214
|
eventTypes: buildRecipientFileEventTypes(originPackageId)
|
|
1145
1215
|
};
|
|
1146
1216
|
}
|
|
@@ -1502,7 +1572,7 @@ function registerCapCommands(program) {
|
|
|
1502
1572
|
publicationOption(destroy).action(async (publisherCapId, options, command) => {
|
|
1503
1573
|
await runCapDestroy(await buildWriteContext(command), publisherCapId, options, globalOptions(command));
|
|
1504
1574
|
});
|
|
1505
|
-
cap.command("transfer <publisherCapId> <recipient>").description("Transfer a PublisherCap object to another address").action(async (publisherCapId, recipient, _options, command) => {
|
|
1575
|
+
cap.command("transfer <publisherCapId> <recipient>").alias("transfer-ownership").description("Transfer a PublisherCap object to another address").action(async (publisherCapId, recipient, _options, command) => {
|
|
1506
1576
|
await runCapTransfer(await buildWriteContext(command), publisherCapId, recipient, globalOptions(command));
|
|
1507
1577
|
});
|
|
1508
1578
|
}
|
|
@@ -1521,6 +1591,9 @@ async function runCollectionList(ctx, options) {
|
|
|
1521
1591
|
collections: publication.collections
|
|
1522
1592
|
});
|
|
1523
1593
|
}
|
|
1594
|
+
function isActivePublication(ctx, target) {
|
|
1595
|
+
return ctx.settings.publication === target;
|
|
1596
|
+
}
|
|
1524
1597
|
async function runCollectionCreate(ctx, name, options, gopts) {
|
|
1525
1598
|
const id = await resolvePublication(ctx, options.publication);
|
|
1526
1599
|
const storageMode = coerceStorageMode(options.mode);
|
|
@@ -1536,8 +1609,11 @@ async function runCollectionCreate(ctx, name, options, gopts) {
|
|
|
1536
1609
|
storageMode,
|
|
1537
1610
|
signal: ctx.signal
|
|
1538
1611
|
});
|
|
1539
|
-
|
|
1540
|
-
|
|
1612
|
+
const selected = isActivePublication(ctx, id);
|
|
1613
|
+
if (selected) {
|
|
1614
|
+
await updateActiveProfile(gopts, { collection: name });
|
|
1615
|
+
}
|
|
1616
|
+
ctx.output.result(`Created collection "${name}".${selected ? " Selected as the active collection." : ""} (tx: ${result.digest})`, result);
|
|
1541
1617
|
}
|
|
1542
1618
|
async function runCollectionDelete(ctx, name, options, gopts) {
|
|
1543
1619
|
const id = await resolvePublication(ctx, options.publication);
|
|
@@ -1559,7 +1635,7 @@ async function runCollectionDelete(ctx, name, options, gopts) {
|
|
|
1559
1635
|
name,
|
|
1560
1636
|
signal: ctx.signal
|
|
1561
1637
|
});
|
|
1562
|
-
if (ctx.settings.collection === name) {
|
|
1638
|
+
if (ctx.settings.collection === name && isActivePublication(ctx, id)) {
|
|
1563
1639
|
await updateActiveProfile(gopts, { collection: undefined });
|
|
1564
1640
|
}
|
|
1565
1641
|
ctx.output.result(`Deleted collection "${name}". (tx: ${result.digest})`, result);
|
|
@@ -1599,6 +1675,7 @@ async function runConfigAdd(output, name, options) {
|
|
|
1599
1675
|
const profiles = {
|
|
1600
1676
|
...cfg.profiles,
|
|
1601
1677
|
[name]: {
|
|
1678
|
+
...cfg.profiles[name],
|
|
1602
1679
|
network,
|
|
1603
1680
|
...options.rpc === undefined ? {} : { rpc: options.rpc },
|
|
1604
1681
|
...options.uploadRelay === undefined ? {} : { uploadRelay: options.uploadRelay }
|
|
@@ -1620,11 +1697,19 @@ async function runConfigUse(output, name) {
|
|
|
1620
1697
|
await saveConfig({ ...cfg, defaultProfile: name });
|
|
1621
1698
|
output.result(`Default profile set to "${name}".`, { defaultProfile: name });
|
|
1622
1699
|
}
|
|
1623
|
-
async function runConfigRemove(output, name) {
|
|
1700
|
+
async function runConfigRemove(output, name, gopts = {}, signal) {
|
|
1624
1701
|
const cfg = await loadConfig();
|
|
1625
1702
|
requireProfile(cfg, name);
|
|
1626
1703
|
const { [name]: _removed, ...rest } = cfg.profiles;
|
|
1627
1704
|
const defaultProfile = cfg.defaultProfile === name ? Object.keys(rest)[0] ?? "default" : cfg.defaultProfile;
|
|
1705
|
+
const reassigns = cfg.defaultProfile === name && defaultProfile !== name;
|
|
1706
|
+
const proceed = await confirm(`Delete profile "${name}"?${reassigns ? ` The default becomes "${defaultProfile}".` : ""}`, {
|
|
1707
|
+
assumeYes: Boolean(gopts.yes),
|
|
1708
|
+
...signal === undefined ? {} : { signal }
|
|
1709
|
+
});
|
|
1710
|
+
if (!proceed) {
|
|
1711
|
+
cancelled();
|
|
1712
|
+
}
|
|
1628
1713
|
await saveConfig({ ...cfg, profiles: rest, defaultProfile });
|
|
1629
1714
|
output.result(`Removed profile "${name}".`, {
|
|
1630
1715
|
removed: name,
|
|
@@ -1646,8 +1731,8 @@ function registerConfigCommands(program) {
|
|
|
1646
1731
|
config.command("use <name>").description("Set the default profile").action(async (name, _options, command) => {
|
|
1647
1732
|
await runConfigUse(outputFor(command), name);
|
|
1648
1733
|
});
|
|
1649
|
-
config.command("remove <name>").description("Delete a profile").action(async (name, _options, command) => {
|
|
1650
|
-
await runConfigRemove(outputFor(command), name);
|
|
1734
|
+
config.command("remove <name>").alias("delete").description("Delete a profile").action(async (name, _options, command) => {
|
|
1735
|
+
await runConfigRemove(outputFor(command), name, globalOptions(command), sigintSignal());
|
|
1651
1736
|
});
|
|
1652
1737
|
}
|
|
1653
1738
|
function requireProfile(config, name) {
|
|
@@ -1850,6 +1935,12 @@ async function runEntryGet(ctx, entryId, options) {
|
|
|
1850
1935
|
ctx.output.result(renderEntry(result), result);
|
|
1851
1936
|
}
|
|
1852
1937
|
async function runEntryList(ctx, options) {
|
|
1938
|
+
if (options.all) {
|
|
1939
|
+
if (options.limit !== undefined || options.cursor !== undefined) {
|
|
1940
|
+
throw new UsageError("--all cannot be combined with --limit or --cursor.");
|
|
1941
|
+
}
|
|
1942
|
+
return runEntryScan(ctx, options);
|
|
1943
|
+
}
|
|
1853
1944
|
const id = await resolvePublication(ctx, options.publication);
|
|
1854
1945
|
const collection = resolveCollection(ctx, options.collection);
|
|
1855
1946
|
const page = await ctx.reader.listEntries(id, collection, {
|
|
@@ -1961,11 +2052,11 @@ async function runEntryRead(ctx, entryId, revisionId, options) {
|
|
|
1961
2052
|
}
|
|
1962
2053
|
function registerEntryCommands(program) {
|
|
1963
2054
|
const entry = program.command("entry").description("Read, add, and delete entries in a collection");
|
|
1964
|
-
const get = entry.command("get <entryId>").description("Fetch a single entry");
|
|
2055
|
+
const get = entry.command("get <entryId>").alias("show").description("Fetch a single entry");
|
|
1965
2056
|
collectionOption(publicationOption(get)).action(async (entryId, options, command) => {
|
|
1966
2057
|
await runEntryGet(await buildReadContext(command), entryId, options);
|
|
1967
2058
|
});
|
|
1968
|
-
const list = entry.command("list").description("List entries in a collection").option("--limit <n>", "Maximum results per page").option("--cursor <cursor>", "Continue from a previous page cursor").option("--drafts-only", "Show only entries with a pending (unpublished) draft");
|
|
2059
|
+
const list = entry.command("list").description("List entries in a collection").option("--limit <n>", "Maximum results per page").option("--cursor <cursor>", "Continue from a previous page cursor").option("--all", "Fetch every page (same as `entry scan`)").option("--drafts-only", "Show only entries with a pending (unpublished) draft");
|
|
1969
2060
|
collectionOption(publicationOption(list)).action(async (options, command) => {
|
|
1970
2061
|
await runEntryList(await buildReadContext(command), options);
|
|
1971
2062
|
});
|
|
@@ -1981,7 +2072,7 @@ function registerEntryCommands(program) {
|
|
|
1981
2072
|
collectionOption(publicationOption(publisherCapOption(remove))).action(async (entryId, options, command) => {
|
|
1982
2073
|
await runEntryDelete(await buildWriteContext(command), entryId, options, globalOptions(command));
|
|
1983
2074
|
});
|
|
1984
|
-
const read = entry.command("read <entryId> [revisionIndex]").description("Fetch a public entry's content to stdout or a file").option("--out <path>", "Write content to a file instead of stdout");
|
|
2075
|
+
const read = entry.command("read <entryId> [revisionIndex]").alias("download").description("Fetch a public entry's content to stdout or a file").option("--out <path>", "Write content to a file instead of stdout");
|
|
1985
2076
|
collectionOption(publicationOption(viaAggregatorOption(read))).action(async (entryId, revisionId, options, command) => {
|
|
1986
2077
|
await runEntryRead(await buildReadContentContext(command, {
|
|
1987
2078
|
viaAggregator: options.viaAggregator
|
|
@@ -2116,6 +2207,9 @@ async function runFileRegister(ctx, options) {
|
|
|
2116
2207
|
throw new UsageError("Pass --public or --encrypted, not both.");
|
|
2117
2208
|
}
|
|
2118
2209
|
const recipients = (options.recipient ?? []).map((r) => toSuiAddress4(r));
|
|
2210
|
+
if (options.public && recipients.length > 0) {
|
|
2211
|
+
throw new UsageError("--recipient applies to encrypted files; a public file is readable by anyone.");
|
|
2212
|
+
}
|
|
2119
2213
|
const blobId = toWalrusBlobId(options.blobId);
|
|
2120
2214
|
const size = parseByteSize(options.size, "--size");
|
|
2121
2215
|
const blobObjectId = options.blobObjectId === undefined ? undefined : toBlobObjectId(options.blobObjectId);
|
|
@@ -2411,13 +2505,13 @@ function registerFileCommands(program) {
|
|
|
2411
2505
|
indexerUrl: options.indexerUrl
|
|
2412
2506
|
}), options);
|
|
2413
2507
|
});
|
|
2414
|
-
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("--share <string>", "Share string from upload (bundles file id, prefix, nonce)").option("--prefix <hex>", "Seal prefix (with --nonce) to decrypt").option("--nonce <hex>", "Seal nonce (with --prefix) to decrypt").option("--raw", "Write the raw (still-encrypted) bytes when no decrypt input is given");
|
|
2508
|
+
const download = file.command("download [file]").alias("read").description("Download a file's content; decrypts in place when encrypted").option("--out <path>", "Write content to a file instead of stdout").option("--share <string>", "Share string from upload (bundles file id, prefix, nonce)").option("--prefix <hex>", "Seal prefix (with --nonce) to decrypt").option("--nonce <hex>", "Seal nonce (with --prefix) to decrypt").option("--raw", "Write the raw (still-encrypted) bytes when no decrypt input is given");
|
|
2415
2509
|
viaAggregatorOption(download).action(async (target, options, command) => {
|
|
2416
2510
|
await runFileDownload(await buildFileDownloadContext(command, {
|
|
2417
2511
|
viaAggregator: options.viaAggregator
|
|
2418
2512
|
}), target, options);
|
|
2419
2513
|
});
|
|
2420
|
-
file.command("get <file>").description("Fetch a file's on-chain metadata").action(async (target, _options, command) => {
|
|
2514
|
+
file.command("get <file>").alias("show").description("Fetch a file's on-chain metadata").action(async (target, _options, command) => {
|
|
2421
2515
|
await runFileGet(await buildFilesReadContext(command), target);
|
|
2422
2516
|
});
|
|
2423
2517
|
const register = file.command("register").description("Register on-chain metadata for a blob already on Walrus").requiredOption("--blob-id <id>", "Walrus content id").requiredOption("-n, --name <name>", "File name").requiredOption("--content-type <mime>", "MIME content type").requiredOption("--size <bytes>", "Plaintext byte length").option("--public", "Register a world-readable (unencrypted) file").option("--encrypted", "Register an encrypted file (needs --seal-prefix)").option("--seal-prefix <hex>", "Seal prefix the blob was encrypted under").option("--blob-object-id <id>", "On-chain Walrus Blob object id, if known");
|
|
@@ -2553,7 +2647,7 @@ async function runPublicationTransferOwnership(ctx, recipient, options, gopts) {
|
|
|
2553
2647
|
}
|
|
2554
2648
|
function registerPublicationCommands(program) {
|
|
2555
2649
|
const publication = program.command("publication").alias("pub").description("Work with publications");
|
|
2556
|
-
publication.command("get [publication]").description("Fetch a publication (slug or id; default: the active publication)").action(async (target, _options, command) => {
|
|
2650
|
+
publication.command("get [publication]").alias("show").description("Fetch a publication (slug or id; default: the active publication)").action(async (target, _options, command) => {
|
|
2557
2651
|
await runPublicationGet(await buildReadContext(command), target);
|
|
2558
2652
|
});
|
|
2559
2653
|
publication.command("list [address]").description("List publications owned by an address (default: the active account)").option("--limit <n>", "Maximum results per page").option("--cursor <cursor>", "Continue from a previous page cursor").option("--ids-only", "Skip slug/name resolution (one RPC, no per-publication reads)").action(async (address, options, command) => {
|
package/docs/QUICKSTART.md
CHANGED
|
@@ -43,6 +43,18 @@ Saved profile "testnet" (testnet).
|
|
|
43
43
|
|
|
44
44
|
## 2. Import your key
|
|
45
45
|
|
|
46
|
+
Three ways to give the CLI a key. It is never accepted as a flag, because argv
|
|
47
|
+
is visible in `ps` and shell history.
|
|
48
|
+
|
|
49
|
+
- **Interactive**: run `morse account import` and paste at the hidden prompt.
|
|
50
|
+
- **Piped**: `echo "$KEY" | morse account import`
|
|
51
|
+
- **No keystore at all**: export `MORSE_PRIVATE_KEY` and skip the import; every
|
|
52
|
+
command picks it up.
|
|
53
|
+
|
|
54
|
+
The first two store the key encrypted and need `MORSE_KEYSTORE_PASSWORD` (or an
|
|
55
|
+
interactive password prompt).
|
|
56
|
+
|
|
57
|
+
|
|
46
58
|
`account import` prompts for the secret key and a keystore password, both hidden.
|
|
47
59
|
The key is encrypted at rest (scrypt + AES-256-GCM) and never stored in plaintext.
|
|
48
60
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcadiasystems/morse-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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.8.0",
|
|
59
59
|
"@mysten/seal": "1.1.3",
|
|
60
60
|
"@mysten/sui": "2.16.2",
|
|
61
61
|
"@mysten/walrus": "1.1.6",
|