@arcadiasystems/morse-cli 0.6.1 → 0.7.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 +18 -0
- package/README.md +40 -2
- package/dist/index.js +67 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ 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.7.0] - 2026-09-09
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`--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.
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
Opt-in rather than default: the direct path is free and trustless, the relay costs a tip and sees your bytes.
|
|
16
|
+
- **`--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.
|
|
17
|
+
- `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.
|
|
18
|
+
|
|
19
|
+
### Notes
|
|
20
|
+
|
|
21
|
+
- 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`.
|
|
22
|
+
|
|
23
|
+
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.
|
|
24
|
+
|
|
7
25
|
## [0.6.1] - 2026-09-09
|
|
8
26
|
|
|
9
27
|
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.7.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,7 +172,7 @@ 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. |
|
|
172
176
|
| `config list` | List profiles; `*` marks the default. |
|
|
173
177
|
| `config use <name>` | Set the default profile. |
|
|
174
178
|
| `config remove <name>` | Delete a profile. |
|
|
@@ -348,6 +352,40 @@ 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
|
+
## When uploads fail: the upload relay
|
|
356
|
+
|
|
357
|
+
A Walrus upload pushes slivers to every storage node in the committee at once,
|
|
358
|
+
95 of them on mainnet. Networks that cannot sustain that burst fail with
|
|
359
|
+
`NotEnoughBlobConfirmationsError` or "Unable to connect", **even when the nodes
|
|
360
|
+
are healthy and far above write quorum**. If uploads fail while reads work, this
|
|
361
|
+
is almost certainly why, and it is not a fault in your config.
|
|
362
|
+
|
|
363
|
+
Route the upload through a relay and one connection replaces ~95:
|
|
364
|
+
|
|
365
|
+
```sh
|
|
366
|
+
morse --upload-relay auto file upload ./photo.jpg --public -n photo
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
`auto` resolves to Mysten's canonical relay for the selected network. Pass a URL
|
|
370
|
+
instead to use a different operator, or persist it on a profile:
|
|
371
|
+
|
|
372
|
+
```sh
|
|
373
|
+
morse config add mainnet --network mainnet --upload-relay auto
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Three things to know before reaching for it:
|
|
377
|
+
|
|
378
|
+
- **It costs a tip per upload**, on top of WAL and gas. Mainnet prices it
|
|
379
|
+
linearly in encoded blob size; a small blob runs about 0.0026 SUI.
|
|
380
|
+
- **The tip is capped** at `--max-tip` (default 10000000 MIST, 0.01 SUI). Over
|
|
381
|
+
the cap the upload is refused before anything is spent, rather than quietly
|
|
382
|
+
charging more than you expected.
|
|
383
|
+
- **The relay sees your bytes.** Same trade as any hosted service; encrypt first
|
|
384
|
+
if that matters.
|
|
385
|
+
|
|
386
|
+
It is opt-in on purpose. The direct path is free and trustless, and most
|
|
387
|
+
networks handle it fine.
|
|
388
|
+
|
|
351
389
|
## Publishing
|
|
352
390
|
|
|
353
391
|
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.7.0",
|
|
152
152
|
description: "Command-line interface for the Morse decentralized CMS on Sui.",
|
|
153
153
|
license: "MIT",
|
|
154
154
|
type: "module",
|
|
@@ -217,9 +217,10 @@ 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
|
|
|
@@ -462,6 +463,9 @@ function parseProfile(name, value, source) {
|
|
|
462
463
|
if (typeof value.rpc === "string") {
|
|
463
464
|
profile.rpc = value.rpc;
|
|
464
465
|
}
|
|
466
|
+
if (typeof value.uploadRelay === "string") {
|
|
467
|
+
profile.uploadRelay = value.uploadRelay;
|
|
468
|
+
}
|
|
465
469
|
if (typeof value.account === "string") {
|
|
466
470
|
profile.account = value.account;
|
|
467
471
|
}
|
|
@@ -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
|
|
@@ -991,7 +1035,7 @@ async function buildContentContext(command) {
|
|
|
991
1035
|
throw new CliError("Walrus content uploads are not available on localnet. Use testnet or mainnet.", ExitCode.Usage);
|
|
992
1036
|
}
|
|
993
1037
|
const adapter = new KeypairAdapter(keypair, base.client);
|
|
994
|
-
const walrus = DefaultWalrusWriteAdapter.fromConfig(
|
|
1038
|
+
const walrus = DefaultWalrusWriteAdapter.fromConfig(walrusWriteConfig(base, network), keypair);
|
|
995
1039
|
return {
|
|
996
1040
|
...base,
|
|
997
1041
|
output: base.output.withNetwork(network),
|
|
@@ -1006,6 +1050,17 @@ function assertSealAvailable(config) {
|
|
|
1006
1050
|
}
|
|
1007
1051
|
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
1052
|
}
|
|
1053
|
+
function walrusWriteConfig(base, network) {
|
|
1054
|
+
const host = resolveUploadRelay(base.settings.uploadRelay, network);
|
|
1055
|
+
if (host === undefined) {
|
|
1056
|
+
return { network, suiClient: base.client };
|
|
1057
|
+
}
|
|
1058
|
+
return {
|
|
1059
|
+
network,
|
|
1060
|
+
suiClient: base.client,
|
|
1061
|
+
uploadRelay: { host, sendTip: { max: parseMaxTip(base.settings.maxTip) } }
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1009
1064
|
async function buildEncryptContext(command) {
|
|
1010
1065
|
const ctx = await buildContentContext(command);
|
|
1011
1066
|
return { ...ctx, seal: lazySeal(ctx) };
|
|
@@ -1539,12 +1594,14 @@ async function runConfigList(output) {
|
|
|
1539
1594
|
}
|
|
1540
1595
|
async function runConfigAdd(output, name, options) {
|
|
1541
1596
|
const network = coerceNetwork(options.network);
|
|
1597
|
+
resolveUploadRelay(options.uploadRelay, network);
|
|
1542
1598
|
const cfg = await loadConfig();
|
|
1543
1599
|
const profiles = {
|
|
1544
1600
|
...cfg.profiles,
|
|
1545
1601
|
[name]: {
|
|
1546
1602
|
network,
|
|
1547
|
-
...options.rpc === undefined ? {} : { rpc: options.rpc }
|
|
1603
|
+
...options.rpc === undefined ? {} : { rpc: options.rpc },
|
|
1604
|
+
...options.uploadRelay === undefined ? {} : { uploadRelay: options.uploadRelay }
|
|
1548
1605
|
}
|
|
1549
1606
|
};
|
|
1550
1607
|
const defaultProfile = Object.keys(cfg.profiles).length === 0 ? name : cfg.defaultProfile;
|
|
@@ -1553,6 +1610,7 @@ async function runConfigAdd(output, name, options) {
|
|
|
1553
1610
|
profile: name,
|
|
1554
1611
|
network,
|
|
1555
1612
|
rpc: options.rpc,
|
|
1613
|
+
uploadRelay: options.uploadRelay,
|
|
1556
1614
|
default: defaultProfile === name
|
|
1557
1615
|
});
|
|
1558
1616
|
}
|
|
@@ -1582,7 +1640,7 @@ function registerConfigCommands(program) {
|
|
|
1582
1640
|
config.command("list").description("List profiles and show the default").action(async (_options, command) => {
|
|
1583
1641
|
await runConfigList(outputFor(command));
|
|
1584
1642
|
});
|
|
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) => {
|
|
1643
|
+
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
1644
|
await runConfigAdd(outputFor(command), name, options);
|
|
1587
1645
|
});
|
|
1588
1646
|
config.command("use <name>").description("Set the default profile").action(async (name, _options, command) => {
|
|
@@ -1612,6 +1670,9 @@ function renderProfiles(config) {
|
|
|
1612
1670
|
if (profile.rpc !== undefined) {
|
|
1613
1671
|
parts.push(profile.rpc);
|
|
1614
1672
|
}
|
|
1673
|
+
if (profile.uploadRelay !== undefined) {
|
|
1674
|
+
parts.push(`relay=${profile.uploadRelay}`);
|
|
1675
|
+
}
|
|
1615
1676
|
if (profile.account !== undefined) {
|
|
1616
1677
|
parts.push(profile.account);
|
|
1617
1678
|
}
|