@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 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.6.1. Mainnet and testnet are both supported for public content;
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.6.1",
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 { profileName, network, rpcUrl, account, publication, collection };
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({ network, suiClient: base.client }, keypair);
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) => {
@@ -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.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "Command-line interface for the Morse decentralized CMS on Sui.",
5
5
  "license": "MIT",
6
6
  "type": "module",