@arcadiasystems/morse-cli 0.6.1 → 0.8.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 +30 -0
- package/README.md +60 -3
- package/dist/index.js +117 -43
- package/docs/QUICKSTART.md +12 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@ 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.8.0] - 2026-09-10
|
|
8
|
+
|
|
9
|
+
Usability pass. Nothing is renamed or removed; every existing spelling keeps working.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **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`.
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
- **`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.
|
|
17
|
+
- **`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.
|
|
18
|
+
|
|
19
|
+
## [0.7.0] - 2026-09-09
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **`--upload-relay <url|auto>`** routes Walrus uploads through a relay instead of the direct fanout, with `MORSE_WALRUS_UPLOAD_RELAY` and a per-profile `uploadRelay` following the usual `flags > env > config > defaults` precedence. `auto` resolves to the canonical Mysten relay for the selected network, and is refused on localnet, which has none.
|
|
24
|
+
|
|
25
|
+
A direct upload pushes slivers to every storage node in the committee at once, 95 on mainnet. Networks that cannot sustain that burst fail with `NotEnoughBlobConfirmationsError` even when the nodes are healthy and far above write quorum, and until now the CLI had no way around it while the SDK did: `DefaultWalrusWriteAdapter.fromConfig` already accepted `uploadRelay`, the CLI just never offered a way to set it.
|
|
26
|
+
|
|
27
|
+
Opt-in rather than default: the direct path is free and trustless, the relay costs a tip and sees your bytes.
|
|
28
|
+
- **`--max-tip <mist>`** and `MORSE_WALRUS_MAX_TIP` cap the relay's per-upload tip, defaulting to 10000000 MIST (0.01 SUI). Mainnet prices the tip linearly in encoded blob size, so an uncapped relay upload has no upper bound on what it charges; over the cap `@mysten/walrus` refuses before spending. A non-integer or negative cap is rejected outright rather than falling back to the default, so a typo cannot silently authorise an unbounded tip.
|
|
29
|
+
- `config add` takes `--upload-relay` to persist the choice on a profile. The value is validated at add time the way `--network` already is, so `auto` on a network with no canonical relay is refused immediately rather than producing a profile that only fails the first time someone uploads. `config list` shows it in both the human and `--json` renderings.
|
|
30
|
+
|
|
31
|
+
### Notes
|
|
32
|
+
|
|
33
|
+
- The relay setting and its tip cap are carried raw through `resolveSettings` and only resolved on the Walrus write path. `resolveSettings` runs for every command, so parsing them eagerly would let a typo'd `MORSE_WALRUS_MAX_TIP` in a shell profile or CI environment break reads that never upload anything. A malformed cap fails the upload, not `publication get`.
|
|
34
|
+
|
|
35
|
+
Verified live: uploads through the relay succeed on both networks and round-trip byte for byte, a too-low `--max-tip` refuses before spending, and a malformed one is rejected at parse time.
|
|
36
|
+
|
|
7
37
|
## [0.6.1] - 2026-09-09
|
|
8
38
|
|
|
9
39
|
Picks up `@arcadiasystems/morse-sdk@0.6.0` and fixes a download regression it exposed. Upgrade if you use mainnet at all.
|
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.8.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
|
|
@@ -112,6 +112,8 @@ Environment variables (override the config file, overridden by flags):
|
|
|
112
112
|
| --- | --- | --- |
|
|
113
113
|
| `MORSE_PROFILE` | `--profile` | Profile to use. |
|
|
114
114
|
| `MORSE_NETWORK` | `--network` | `mainnet`, `testnet`, or `localnet`. |
|
|
115
|
+
| `MORSE_WALRUS_UPLOAD_RELAY` | `--upload-relay` | Upload through a Walrus relay instead of the direct fanout. `auto` picks the canonical relay for the network. |
|
|
116
|
+
| `MORSE_WALRUS_MAX_TIP` | `--max-tip` | Cap the relay's per-upload tip, in MIST. Defaults to 10000000 (0.01 SUI). |
|
|
115
117
|
| `MORSE_RPC_URL` | `--rpc` | Sui RPC URL override. |
|
|
116
118
|
| `MORSE_ADDRESS` | (no flag) | Active account address, selecting which keystore to use. |
|
|
117
119
|
| `MORSE_PUBLICATION` | `-P, --publication` | Active publication id. |
|
|
@@ -146,6 +148,8 @@ Global options apply to every command and must appear before the subcommand
|
|
|
146
148
|
| `--network <mainnet\|testnet\|localnet>` | Network to target (default: testnet). `localnet` needs a custom deployment. |
|
|
147
149
|
| `-p, --profile <name>` | Config profile to use. |
|
|
148
150
|
| `--rpc <url>` | Override the Sui RPC URL. |
|
|
151
|
+
| `--upload-relay <url\|auto>` | Upload Walrus blobs through a relay instead of the direct fanout. `auto` picks the canonical relay for the network. Costs a tip per upload. |
|
|
152
|
+
| `--max-tip <mist>` | Cap the relay's per-upload tip, in MIST (default: 10000000). |
|
|
149
153
|
| `--json` | Machine-readable JSON on stdout. |
|
|
150
154
|
| `-q, --quiet` | Suppress progress and informational output. |
|
|
151
155
|
| `-y, --yes` | Assume yes for confirmation prompts. |
|
|
@@ -168,10 +172,10 @@ and `-C, --collection <name>`, both defaulting to the active context.
|
|
|
168
172
|
|
|
169
173
|
| Command | Purpose |
|
|
170
174
|
| --- | --- |
|
|
171
|
-
| `config add <name> --network <net> [--rpc <url>]` | Create or update a profile. |
|
|
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`. |
|
|
172
177
|
| `config list` | List profiles; `*` marks the default. |
|
|
173
178
|
| `config use <name>` | Set the default profile. |
|
|
174
|
-
| `config remove <name>` | Delete a profile. |
|
|
175
179
|
| `config path` | Print the config file path. |
|
|
176
180
|
|
|
177
181
|
### account
|
|
@@ -348,6 +352,59 @@ to fetch the full record per file (one read each) when you need them.
|
|
|
348
352
|
retired, so it needs `--indexer-url` pointing at a source that serves the same
|
|
349
353
|
query. This affects testnet as well as mainnet.
|
|
350
354
|
|
|
355
|
+
## Command spellings
|
|
356
|
+
|
|
357
|
+
Several commands accept two verbs for the same action, so whichever you reach
|
|
358
|
+
for works. These are aliases, not separate commands:
|
|
359
|
+
|
|
360
|
+
| Canonical | Also accepts |
|
|
361
|
+
| --- | --- |
|
|
362
|
+
| `account show` | `account get` |
|
|
363
|
+
| `publication get` | `publication show` |
|
|
364
|
+
| `entry get` | `entry show` |
|
|
365
|
+
| `file get` | `file show` |
|
|
366
|
+
| `config remove` | `config delete` |
|
|
367
|
+
| `cap transfer` | `cap transfer-ownership` |
|
|
368
|
+
| `entry read` | `entry download` |
|
|
369
|
+
| `file download` | `file read` |
|
|
370
|
+
| `entry scan` | `entry list --all` |
|
|
371
|
+
|
|
372
|
+
`publication` also answers to `pub`.
|
|
373
|
+
|
|
374
|
+
## When uploads fail: the upload relay
|
|
375
|
+
|
|
376
|
+
A Walrus upload pushes slivers to every storage node in the committee at once,
|
|
377
|
+
95 of them on mainnet. Networks that cannot sustain that burst fail with
|
|
378
|
+
`NotEnoughBlobConfirmationsError` or "Unable to connect", **even when the nodes
|
|
379
|
+
are healthy and far above write quorum**. If uploads fail while reads work, this
|
|
380
|
+
is almost certainly why, and it is not a fault in your config.
|
|
381
|
+
|
|
382
|
+
Route the upload through a relay and one connection replaces ~95:
|
|
383
|
+
|
|
384
|
+
```sh
|
|
385
|
+
morse --upload-relay auto file upload ./photo.jpg --public -n photo
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
`auto` resolves to Mysten's canonical relay for the selected network. Pass a URL
|
|
389
|
+
instead to use a different operator, or persist it on a profile:
|
|
390
|
+
|
|
391
|
+
```sh
|
|
392
|
+
morse config add mainnet --network mainnet --upload-relay auto
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Three things to know before reaching for it:
|
|
396
|
+
|
|
397
|
+
- **It costs a tip per upload**, on top of WAL and gas. Mainnet prices it
|
|
398
|
+
linearly in encoded blob size; a small blob runs about 0.0026 SUI.
|
|
399
|
+
- **The tip is capped** at `--max-tip` (default 10000000 MIST, 0.01 SUI). Over
|
|
400
|
+
the cap the upload is refused before anything is spent, rather than quietly
|
|
401
|
+
charging more than you expected.
|
|
402
|
+
- **The relay sees your bytes.** Same trade as any hosted service; encrypt first
|
|
403
|
+
if that matters.
|
|
404
|
+
|
|
405
|
+
It is opt-in on purpose. The direct path is free and trustless, and most
|
|
406
|
+
networks handle it fine.
|
|
407
|
+
|
|
351
408
|
## Publishing
|
|
352
409
|
|
|
353
410
|
Published to the public npm registry as the scoped package
|
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.8.0",
|
|
152
152
|
description: "Command-line interface for the Morse decentralized CMS on Sui.",
|
|
153
153
|
license: "MIT",
|
|
154
154
|
type: "module",
|
|
@@ -217,12 +217,40 @@ var package_default = {
|
|
|
217
217
|
};
|
|
218
218
|
|
|
219
219
|
// src/cli/program.ts
|
|
220
|
+
var DEFAULT_MAX_TIP_MIST = 1e7;
|
|
220
221
|
function buildProgram() {
|
|
221
222
|
const program = new Command;
|
|
222
|
-
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("--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}) [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
224
|
return program;
|
|
224
225
|
}
|
|
225
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
|
+
|
|
226
254
|
// src/cli/prompts.ts
|
|
227
255
|
import * as readline from "node:readline";
|
|
228
256
|
function isInteractive() {
|
|
@@ -462,6 +490,9 @@ function parseProfile(name, value, source) {
|
|
|
462
490
|
if (typeof value.rpc === "string") {
|
|
463
491
|
profile.rpc = value.rpc;
|
|
464
492
|
}
|
|
493
|
+
if (typeof value.uploadRelay === "string") {
|
|
494
|
+
profile.uploadRelay = value.uploadRelay;
|
|
495
|
+
}
|
|
465
496
|
if (typeof value.account === "string") {
|
|
466
497
|
profile.account = value.account;
|
|
467
498
|
}
|
|
@@ -483,33 +514,6 @@ function malformed(source, detail) {
|
|
|
483
514
|
// src/config/store.ts
|
|
484
515
|
import { chmod, mkdir, rename } from "node:fs/promises";
|
|
485
516
|
|
|
486
|
-
// src/cli/io.ts
|
|
487
|
-
import { access, readFile, writeFile } from "node:fs/promises";
|
|
488
|
-
async function fileExists(path) {
|
|
489
|
-
try {
|
|
490
|
-
await access(path);
|
|
491
|
-
return true;
|
|
492
|
-
} catch {
|
|
493
|
-
return false;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
async function readBytes(path) {
|
|
497
|
-
return new Uint8Array(await readFile(path));
|
|
498
|
-
}
|
|
499
|
-
async function readJson(path) {
|
|
500
|
-
return JSON.parse(await readFile(path, "utf8"));
|
|
501
|
-
}
|
|
502
|
-
async function writeFileContents(path, data) {
|
|
503
|
-
await writeFile(path, data);
|
|
504
|
-
}
|
|
505
|
-
async function readStdin() {
|
|
506
|
-
const chunks = [];
|
|
507
|
-
for await (const chunk of process.stdin) {
|
|
508
|
-
chunks.push(chunk);
|
|
509
|
-
}
|
|
510
|
-
return new Uint8Array(Buffer.concat(chunks));
|
|
511
|
-
}
|
|
512
|
-
|
|
513
517
|
// src/config/paths.ts
|
|
514
518
|
import { homedir } from "node:os";
|
|
515
519
|
import { join } from "node:path";
|
|
@@ -564,6 +568,35 @@ async function updateActiveProfile(opts, patch, env = process.env) {
|
|
|
564
568
|
}
|
|
565
569
|
|
|
566
570
|
// src/config/profile.ts
|
|
571
|
+
function canonicalUploadRelay(network) {
|
|
572
|
+
if (network === "mainnet" || network === "testnet") {
|
|
573
|
+
return `https://upload-relay.${network}.walrus.space`;
|
|
574
|
+
}
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
function resolveUploadRelay(value, network) {
|
|
578
|
+
if (value === undefined || value === "") {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
if (value !== "auto") {
|
|
582
|
+
return value;
|
|
583
|
+
}
|
|
584
|
+
const canonical = canonicalUploadRelay(network);
|
|
585
|
+
if (canonical === undefined) {
|
|
586
|
+
throw new UsageError(`--upload-relay auto has no canonical relay for ${network}. Pass an explicit relay URL.`);
|
|
587
|
+
}
|
|
588
|
+
return canonical;
|
|
589
|
+
}
|
|
590
|
+
function parseMaxTip(value) {
|
|
591
|
+
if (value === undefined || value === "") {
|
|
592
|
+
return DEFAULT_MAX_TIP_MIST;
|
|
593
|
+
}
|
|
594
|
+
const parsed = /^\d+$/.test(value) ? Number(value) : Number.NaN;
|
|
595
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
596
|
+
throw new UsageError(`--max-tip must be a non-negative integer in MIST; got "${value}".`);
|
|
597
|
+
}
|
|
598
|
+
return parsed;
|
|
599
|
+
}
|
|
567
600
|
function resolveSettings(opts, config, env = process.env) {
|
|
568
601
|
const explicitProfile = opts.profile ?? env.MORSE_PROFILE;
|
|
569
602
|
const profileName = explicitProfile ?? config.defaultProfile;
|
|
@@ -574,10 +607,21 @@ function resolveSettings(opts, config, env = process.env) {
|
|
|
574
607
|
const networkValue = opts.network ?? env.MORSE_NETWORK ?? profile?.network ?? "testnet";
|
|
575
608
|
const network = coerceNetwork(networkValue);
|
|
576
609
|
const rpcUrl = opts.rpc ?? env.MORSE_RPC_URL ?? profile?.rpc;
|
|
610
|
+
const uploadRelay = opts.uploadRelay ?? env.MORSE_WALRUS_UPLOAD_RELAY ?? profile?.uploadRelay;
|
|
611
|
+
const maxTip = opts.maxTip ?? env.MORSE_WALRUS_MAX_TIP;
|
|
577
612
|
const account = env.MORSE_ADDRESS ?? profile?.account;
|
|
578
613
|
const publication = env.MORSE_PUBLICATION ?? profile?.publication;
|
|
579
614
|
const collection = env.MORSE_COLLECTION ?? profile?.collection;
|
|
580
|
-
return {
|
|
615
|
+
return {
|
|
616
|
+
profileName,
|
|
617
|
+
network,
|
|
618
|
+
rpcUrl,
|
|
619
|
+
uploadRelay,
|
|
620
|
+
maxTip,
|
|
621
|
+
account,
|
|
622
|
+
publication,
|
|
623
|
+
collection
|
|
624
|
+
};
|
|
581
625
|
}
|
|
582
626
|
|
|
583
627
|
// src/keystore/keystore.ts
|
|
@@ -881,7 +925,7 @@ function registerAccountCommands(program) {
|
|
|
881
925
|
account.command("list").description("List imported accounts").action(async (_options, command) => {
|
|
882
926
|
await runAccountList(outputFor(command), globalOptions(command));
|
|
883
927
|
});
|
|
884
|
-
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) => {
|
|
885
929
|
await runAccountShow(outputFor(command), globalOptions(command));
|
|
886
930
|
});
|
|
887
931
|
account.command("use <address>").description("Set the active account for the current profile").action(async (address, _options, command) => {
|
|
@@ -896,8 +940,15 @@ async function readSecretToImport(env, signal) {
|
|
|
896
940
|
if (raw !== undefined && raw.length > 0) {
|
|
897
941
|
return raw;
|
|
898
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
|
+
}
|
|
899
950
|
if (!isInteractive()) {
|
|
900
|
-
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.");
|
|
901
952
|
}
|
|
902
953
|
process.stderr.write(`Paste your Sui private key (starts with suiprivkey1...), then press Enter. Input is hidden.
|
|
903
954
|
`);
|
|
@@ -991,7 +1042,7 @@ async function buildContentContext(command) {
|
|
|
991
1042
|
throw new CliError("Walrus content uploads are not available on localnet. Use testnet or mainnet.", ExitCode.Usage);
|
|
992
1043
|
}
|
|
993
1044
|
const adapter = new KeypairAdapter(keypair, base.client);
|
|
994
|
-
const walrus = DefaultWalrusWriteAdapter.fromConfig(
|
|
1045
|
+
const walrus = DefaultWalrusWriteAdapter.fromConfig(walrusWriteConfig(base, network), keypair);
|
|
995
1046
|
return {
|
|
996
1047
|
...base,
|
|
997
1048
|
output: base.output.withNetwork(network),
|
|
@@ -1006,6 +1057,17 @@ function assertSealAvailable(config) {
|
|
|
1006
1057
|
}
|
|
1007
1058
|
throw new CliError(`Encrypted commands are not available on ${config.network}. Seal key servers there are operated commercially and the CLI cannot yet be pointed at one. Use --network testnet, or drive @arcadiasystems/morse-sdk directly with your own seal.serverConfigs.`, ExitCode.Usage);
|
|
1008
1059
|
}
|
|
1060
|
+
function walrusWriteConfig(base, network) {
|
|
1061
|
+
const host = resolveUploadRelay(base.settings.uploadRelay, network);
|
|
1062
|
+
if (host === undefined) {
|
|
1063
|
+
return { network, suiClient: base.client };
|
|
1064
|
+
}
|
|
1065
|
+
return {
|
|
1066
|
+
network,
|
|
1067
|
+
suiClient: base.client,
|
|
1068
|
+
uploadRelay: { host, sendTip: { max: parseMaxTip(base.settings.maxTip) } }
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1009
1071
|
async function buildEncryptContext(command) {
|
|
1010
1072
|
const ctx = await buildContentContext(command);
|
|
1011
1073
|
return { ...ctx, seal: lazySeal(ctx) };
|
|
@@ -1447,7 +1509,7 @@ function registerCapCommands(program) {
|
|
|
1447
1509
|
publicationOption(destroy).action(async (publisherCapId, options, command) => {
|
|
1448
1510
|
await runCapDestroy(await buildWriteContext(command), publisherCapId, options, globalOptions(command));
|
|
1449
1511
|
});
|
|
1450
|
-
cap.command("transfer <publisherCapId> <recipient>").description("Transfer a PublisherCap object to another address").action(async (publisherCapId, recipient, _options, command) => {
|
|
1512
|
+
cap.command("transfer <publisherCapId> <recipient>").alias("transfer-ownership").description("Transfer a PublisherCap object to another address").action(async (publisherCapId, recipient, _options, command) => {
|
|
1451
1513
|
await runCapTransfer(await buildWriteContext(command), publisherCapId, recipient, globalOptions(command));
|
|
1452
1514
|
});
|
|
1453
1515
|
}
|
|
@@ -1539,12 +1601,14 @@ async function runConfigList(output) {
|
|
|
1539
1601
|
}
|
|
1540
1602
|
async function runConfigAdd(output, name, options) {
|
|
1541
1603
|
const network = coerceNetwork(options.network);
|
|
1604
|
+
resolveUploadRelay(options.uploadRelay, network);
|
|
1542
1605
|
const cfg = await loadConfig();
|
|
1543
1606
|
const profiles = {
|
|
1544
1607
|
...cfg.profiles,
|
|
1545
1608
|
[name]: {
|
|
1546
1609
|
network,
|
|
1547
|
-
...options.rpc === undefined ? {} : { rpc: options.rpc }
|
|
1610
|
+
...options.rpc === undefined ? {} : { rpc: options.rpc },
|
|
1611
|
+
...options.uploadRelay === undefined ? {} : { uploadRelay: options.uploadRelay }
|
|
1548
1612
|
}
|
|
1549
1613
|
};
|
|
1550
1614
|
const defaultProfile = Object.keys(cfg.profiles).length === 0 ? name : cfg.defaultProfile;
|
|
@@ -1553,6 +1617,7 @@ async function runConfigAdd(output, name, options) {
|
|
|
1553
1617
|
profile: name,
|
|
1554
1618
|
network,
|
|
1555
1619
|
rpc: options.rpc,
|
|
1620
|
+
uploadRelay: options.uploadRelay,
|
|
1556
1621
|
default: defaultProfile === name
|
|
1557
1622
|
});
|
|
1558
1623
|
}
|
|
@@ -1582,13 +1647,13 @@ function registerConfigCommands(program) {
|
|
|
1582
1647
|
config.command("list").description("List profiles and show the default").action(async (_options, command) => {
|
|
1583
1648
|
await runConfigList(outputFor(command));
|
|
1584
1649
|
});
|
|
1585
|
-
config.command("add <name>").description("Create or update a profile").requiredOption("--network <network>", "Sui network: mainnet, testnet, or localnet").option("--rpc <url>", "RPC URL override for this profile").action(async (name, options, command) => {
|
|
1650
|
+
config.command("add <name>").description("Create or update a profile").requiredOption("--network <network>", "Sui network: mainnet, testnet, or localnet").option("--rpc <url>", "RPC URL override for this profile").option("--upload-relay <url|auto>", "Walrus upload relay for this profile; 'auto' picks the canonical relay for the network").action(async (name, options, command) => {
|
|
1586
1651
|
await runConfigAdd(outputFor(command), name, options);
|
|
1587
1652
|
});
|
|
1588
1653
|
config.command("use <name>").description("Set the default profile").action(async (name, _options, command) => {
|
|
1589
1654
|
await runConfigUse(outputFor(command), name);
|
|
1590
1655
|
});
|
|
1591
|
-
config.command("remove <name>").description("Delete a profile").action(async (name, _options, command) => {
|
|
1656
|
+
config.command("remove <name>").alias("delete").description("Delete a profile").action(async (name, _options, command) => {
|
|
1592
1657
|
await runConfigRemove(outputFor(command), name);
|
|
1593
1658
|
});
|
|
1594
1659
|
}
|
|
@@ -1612,6 +1677,9 @@ function renderProfiles(config) {
|
|
|
1612
1677
|
if (profile.rpc !== undefined) {
|
|
1613
1678
|
parts.push(profile.rpc);
|
|
1614
1679
|
}
|
|
1680
|
+
if (profile.uploadRelay !== undefined) {
|
|
1681
|
+
parts.push(`relay=${profile.uploadRelay}`);
|
|
1682
|
+
}
|
|
1615
1683
|
if (profile.account !== undefined) {
|
|
1616
1684
|
parts.push(profile.account);
|
|
1617
1685
|
}
|
|
@@ -1789,6 +1857,12 @@ async function runEntryGet(ctx, entryId, options) {
|
|
|
1789
1857
|
ctx.output.result(renderEntry(result), result);
|
|
1790
1858
|
}
|
|
1791
1859
|
async function runEntryList(ctx, options) {
|
|
1860
|
+
if (options.all) {
|
|
1861
|
+
if (options.limit !== undefined || options.cursor !== undefined) {
|
|
1862
|
+
throw new UsageError("--all cannot be combined with --limit or --cursor.");
|
|
1863
|
+
}
|
|
1864
|
+
return runEntryScan(ctx, options);
|
|
1865
|
+
}
|
|
1792
1866
|
const id = await resolvePublication(ctx, options.publication);
|
|
1793
1867
|
const collection = resolveCollection(ctx, options.collection);
|
|
1794
1868
|
const page = await ctx.reader.listEntries(id, collection, {
|
|
@@ -1900,11 +1974,11 @@ async function runEntryRead(ctx, entryId, revisionId, options) {
|
|
|
1900
1974
|
}
|
|
1901
1975
|
function registerEntryCommands(program) {
|
|
1902
1976
|
const entry = program.command("entry").description("Read, add, and delete entries in a collection");
|
|
1903
|
-
const get = entry.command("get <entryId>").description("Fetch a single entry");
|
|
1977
|
+
const get = entry.command("get <entryId>").alias("show").description("Fetch a single entry");
|
|
1904
1978
|
collectionOption(publicationOption(get)).action(async (entryId, options, command) => {
|
|
1905
1979
|
await runEntryGet(await buildReadContext(command), entryId, options);
|
|
1906
1980
|
});
|
|
1907
|
-
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");
|
|
1981
|
+
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");
|
|
1908
1982
|
collectionOption(publicationOption(list)).action(async (options, command) => {
|
|
1909
1983
|
await runEntryList(await buildReadContext(command), options);
|
|
1910
1984
|
});
|
|
@@ -1920,7 +1994,7 @@ function registerEntryCommands(program) {
|
|
|
1920
1994
|
collectionOption(publicationOption(publisherCapOption(remove))).action(async (entryId, options, command) => {
|
|
1921
1995
|
await runEntryDelete(await buildWriteContext(command), entryId, options, globalOptions(command));
|
|
1922
1996
|
});
|
|
1923
|
-
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");
|
|
1997
|
+
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");
|
|
1924
1998
|
collectionOption(publicationOption(viaAggregatorOption(read))).action(async (entryId, revisionId, options, command) => {
|
|
1925
1999
|
await runEntryRead(await buildReadContentContext(command, {
|
|
1926
2000
|
viaAggregator: options.viaAggregator
|
|
@@ -2350,13 +2424,13 @@ function registerFileCommands(program) {
|
|
|
2350
2424
|
indexerUrl: options.indexerUrl
|
|
2351
2425
|
}), options);
|
|
2352
2426
|
});
|
|
2353
|
-
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");
|
|
2427
|
+
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");
|
|
2354
2428
|
viaAggregatorOption(download).action(async (target, options, command) => {
|
|
2355
2429
|
await runFileDownload(await buildFileDownloadContext(command, {
|
|
2356
2430
|
viaAggregator: options.viaAggregator
|
|
2357
2431
|
}), target, options);
|
|
2358
2432
|
});
|
|
2359
|
-
file.command("get <file>").description("Fetch a file's on-chain metadata").action(async (target, _options, command) => {
|
|
2433
|
+
file.command("get <file>").alias("show").description("Fetch a file's on-chain metadata").action(async (target, _options, command) => {
|
|
2360
2434
|
await runFileGet(await buildFilesReadContext(command), target);
|
|
2361
2435
|
});
|
|
2362
2436
|
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");
|
|
@@ -2492,7 +2566,7 @@ async function runPublicationTransferOwnership(ctx, recipient, options, gopts) {
|
|
|
2492
2566
|
}
|
|
2493
2567
|
function registerPublicationCommands(program) {
|
|
2494
2568
|
const publication = program.command("publication").alias("pub").description("Work with publications");
|
|
2495
|
-
publication.command("get [publication]").description("Fetch a publication (slug or id; default: the active publication)").action(async (target, _options, command) => {
|
|
2569
|
+
publication.command("get [publication]").alias("show").description("Fetch a publication (slug or id; default: the active publication)").action(async (target, _options, command) => {
|
|
2496
2570
|
await runPublicationGet(await buildReadContext(command), target);
|
|
2497
2571
|
});
|
|
2498
2572
|
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
|
|