@arcadiasystems/morse-cli 0.5.0 → 0.6.1

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,72 @@ 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.6.1] - 2026-09-09
8
+
9
+ Picks up `@arcadiasystems/morse-sdk@0.6.0` and fixes a download regression it exposed. Upgrade if you use mainnet at all.
10
+
11
+ ### Fixed
12
+
13
+ - **`file download` worked on no mainnet file, public or not.** `buildFileDownloadContext` constructed the Seal adapter eagerly, so once SDK 0.6.0 stopped pinning mainnet key servers, every download failed at context build with a Seal error before the file was even looked up. Downloading a public file never touches Seal, so the adapter is now built on first use and memoized, matching the lazy-signer pattern the same context already used. Regression covered in `test/program.test.ts` against the real context builder.
14
+ - **Encrypted commands now explain themselves on mainnet.** They previously surfaced the SDK's `ConfigurationError`, whose advice ("pass seal.serverConfigs", "start from MAINNET_SEAL_COMMITTEE") names an API the CLI does not expose: there is no flag or env var for key servers, so on mainnet these commands cannot work at all. `entry add-encrypted`, `entry decrypt`, `file upload --encrypt` / `--recipient`, and file decryption now exit 2 with a message that says that and points at testnet or the SDK.
15
+ - **`file upload --public` is not caught by that guard.** It shares the encrypt context but never encrypts, so `EncryptContext.seal` became a lazy factory too. Building the adapter eagerly there would have blocked public mainnet uploads the same way the eager build blocked public downloads. Both the refusal and the exemption are covered in `test/program.test.ts`.
16
+ - **`file download` refused every public file, on every network.** The check asked whether the file had recipients, on the assumption that "a public file has no recipients". The Move contract auto-includes the owner on creation, so `members` is non-empty for every file that exists, public or not: a public file and an encrypted one both report exactly one member. The result was that `file upload --public` produced a file the CLI would not download, telling the user their plaintext was "unusable ciphertext" and pointing at `--raw`, which then wrote the correct bytes anyway.
17
+
18
+ Encryption is now determined by the Seal id prefix via the new `getRecipientFileSealPrefix` in SDK 0.7.0, which is the only reliable signal. This bug predates the mainnet work and shipped in 0.6.0 and earlier.
19
+
20
+ It escaped the tests because the download fixture defaulted to `members: []`, modelling an object the contract cannot produce. The fixture now carries a realistic owner-member, and a regression test covers a public file that has members.
21
+ - Dependency moves to `@arcadiasystems/morse-sdk@^0.7.0`. The published 0.6.0 declared `^0.5.0`, which resolves to the SDK release whose mainnet Seal default could produce permanently unreadable ciphertext.
22
+
23
+ ### Known gaps
24
+
25
+ - `file list` still needs `--indexer-url` on every network; public Sui fullnodes have retired the JSON-RPC event query it walks.
26
+ - Encrypted mainnet stays blocked until Seal operator credentials are available and the CLI grows a way to pass them.
27
+
28
+ ## [0.6.0] - 2026-09-09
29
+
30
+ Mainnet support, consuming `@arcadiasystems/morse-sdk@0.5.0`. The Move contracts
31
+ went live on Sui mainnet on 2026-09-09; the CLI rejected `mainnet` outright until
32
+ now.
33
+
34
+ ### Added
35
+
36
+ - **`--network mainnet` works**, as do `MORSE_NETWORK=mainnet` and
37
+ `morse config add <name> --network mainnet`. `coerceNetwork` no longer
38
+ special-cases mainnet, and `localnet` is accepted too (it still fails at
39
+ startup unless you point the SDK at your own package and registry, which is
40
+ `morseConfig`'s error to raise, not the flag parser's).
41
+ - **Commands that spend gas now name the network they spent on.** The resolved
42
+ network appears as the first detail line under the result headline, and as a
43
+ `network` field in `--json` output. Applied on the write contexts only, so read
44
+ and list output is byte-for-byte unchanged. Nothing selects mainnet implicitly
45
+ (with no flag, env var, or profile the CLI targets testnet), so this is there
46
+ to make a mainnet spend visible after the fact rather than to gate it.
47
+
48
+ ### Fixed
49
+
50
+ - Status banner claimed v0.3.0, two releases stale, and said mainnet support was
51
+ pending.
52
+ - `--network` help text listed only testnet and localnet, in both the global
53
+ flag (`morse --help`) and `config add --network`, as did the `MORSE_NETWORK`
54
+ row of the README env table.
55
+ - Seven README links pointed into `examples/`, which is not in the package
56
+ `files` array, so they 404'd on the npm page. Now absolute GitHub URLs;
57
+ `docs/QUICKSTART.md` stays relative because `docs` does ship.
58
+ - Dead Walrus faucet link in `docs/QUICKSTART.md`. `docs.walrus.site` no longer
59
+ resolves; WAL acquisition now points at the working stake-wal.wal.app swap,
60
+ matching the README and morse-sdk.
61
+ - Known limitations now record that `file list` requires `--indexer-url`, since
62
+ public Sui fullnodes have retired the JSON-RPC event query it walks. This
63
+ affects testnet as well as mainnet and predates this release.
64
+
65
+ ### Known gaps
66
+
67
+ - `file list` is unusable without `--indexer-url` on every network, per above.
68
+ Porting the event source off `suix_queryEvents` is tracked separately.
69
+ - No mainnet run of the opt-in `bun run test:e2e` lifecycle; it stays pointed at
70
+ testnet, where it costs nothing real.
71
+ - The Move contracts remain unaudited.
72
+
7
73
  ## [0.5.0] - 2026-06-09
8
74
 
9
75
  Hardening pass from CLI testing: fail fast before paying for Walrus storage,
package/README.md CHANGED
@@ -6,16 +6,36 @@ 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.3.0, targeting Sui testnet. The command surface is stable; mainnet
10
- > support lands when the contracts are frozen.
9
+ > Status: v0.6.1. Mainnet and testnet are both supported for public content;
10
+ > the command surface is stable.
11
+ >
12
+ > **Encrypted commands are testnet-only.** `entry add-encrypted`, `entry
13
+ > decrypt`, `file upload --encrypt` (and `--recipient`, which implies it), and
14
+ > decrypting a downloaded file all need Seal key servers. Testnet has an open
15
+ > set; mainnet does not, because every mainnet Seal operator is commercial, and
16
+ > the CLI has no flag or env var for supplying one. These commands exit 2 on
17
+ > mainnet with an explanation.
18
+ >
19
+ > Everything that does not encrypt works on both networks, including
20
+ > `file upload --public`, `file download` of a public file, and the whole
21
+ > publication, collection and entry surface. All of that is verified against
22
+ > live mainnet, including a public upload and download round trip.
23
+ >
24
+ > **The Move contracts are unaudited.** On mainnet, gas and Walrus storage cost
25
+ > real SUI and WAL, there is no faucet, and deletions are irreversible. Nothing
26
+ > selects mainnet for you: with no `--network`, `MORSE_NETWORK`, or profile
27
+ > setting, the CLI targets testnet. Every command that spends gas prints the
28
+ > network it used, and `--json` output carries a `network` field.
11
29
 
12
30
  ## Requirements
13
31
 
14
32
  - [Node.js](https://nodejs.org) >= 18, or [Bun](https://bun.sh) >= 1.2. The
15
33
  published CLI runs under either.
16
- - A funded Sui testnet address for gas, and testnet WAL for Walrus storage when
17
- adding content. Get SUI from the [Sui faucet](https://faucet.sui.io/) and WAL
18
- from the [Walrus testnet faucet](https://docs.walrus.site/usage/web-tool.html).
34
+ - A funded Sui address for gas, and WAL for Walrus storage when adding content.
35
+ On testnet, get SUI from the [Sui faucet](https://faucet.sui.io/) and swap some
36
+ for WAL at [stake-wal.wal.app](https://stake-wal.wal.app/?network=testnet). On
37
+ mainnet there is no faucet: both have to be acquired, and every write spends
38
+ real value.
19
39
 
20
40
  ## Install
21
41
 
@@ -91,7 +111,7 @@ Environment variables (override the config file, overridden by flags):
91
111
  | Variable | Overrides | Notes |
92
112
  | --- | --- | --- |
93
113
  | `MORSE_PROFILE` | `--profile` | Profile to use. |
94
- | `MORSE_NETWORK` | `--network` | `testnet` or `localnet`. |
114
+ | `MORSE_NETWORK` | `--network` | `mainnet`, `testnet`, or `localnet`. |
95
115
  | `MORSE_RPC_URL` | `--rpc` | Sui RPC URL override. |
96
116
  | `MORSE_ADDRESS` | (no flag) | Active account address, selecting which keystore to use. |
97
117
  | `MORSE_PUBLICATION` | `-P, --publication` | Active publication id. |
@@ -123,7 +143,7 @@ Global options apply to every command and must appear before the subcommand
123
143
 
124
144
  | Global option | Purpose |
125
145
  | --- | --- |
126
- | `--network <testnet\|localnet>` | Network to target (default: testnet). |
146
+ | `--network <mainnet\|testnet\|localnet>` | Network to target (default: testnet). `localnet` needs a custom deployment. |
127
147
  | `-p, --profile <name>` | Config profile to use. |
128
148
  | `--rpc <url>` | Override the Sui RPC URL. |
129
149
  | `--json` | Machine-readable JSON on stdout. |
@@ -290,13 +310,13 @@ to fetch the full record per file (one read each) when you need them.
290
310
  ## Examples
291
311
 
292
312
  - [docs/QUICKSTART.md](./docs/QUICKSTART.md): a full, copy-pasteable walkthrough.
293
- - Runnable shell recipes in [examples/](./examples/):
294
- - [`lifecycle.sh`](./examples/lifecycle.sh): create, add an entry, read, revise, tear down.
295
- - [`content.sh`](./examples/content.sh): upload an image and a post, publish a revision, fetch content back, get a link, remove a collection.
296
- - [`encrypt-decrypt.sh`](./examples/encrypt-decrypt.sh): encrypt with Seal and decrypt back.
297
- - [`delegation.sh`](./examples/delegation.sh): issue a PublisherCap to a delegate, then revoke it.
298
- - [`ci-noninteractive.sh`](./examples/ci-noninteractive.sh): env-var auth, `--yes`, and `--json` parsing.
299
- - [`files.sh`](./examples/files.sh): RecipientFile round-trip (upload an encrypted file, download/decrypt via its share string, manage recipients, plus a public file).
313
+ - Runnable shell recipes in [examples/](https://github.com/arcadiasystems/morse-dcms/tree/main/morse-cli/examples):
314
+ - [`lifecycle.sh`](https://github.com/arcadiasystems/morse-dcms/blob/main/morse-cli/examples/lifecycle.sh): create, add an entry, read, revise, tear down.
315
+ - [`content.sh`](https://github.com/arcadiasystems/morse-dcms/blob/main/morse-cli/examples/content.sh): upload an image and a post, publish a revision, fetch content back, get a link, remove a collection.
316
+ - [`encrypt-decrypt.sh`](https://github.com/arcadiasystems/morse-dcms/blob/main/morse-cli/examples/encrypt-decrypt.sh): encrypt with Seal and decrypt back.
317
+ - [`delegation.sh`](https://github.com/arcadiasystems/morse-dcms/blob/main/morse-cli/examples/delegation.sh): issue a PublisherCap to a delegate, then revoke it.
318
+ - [`ci-noninteractive.sh`](https://github.com/arcadiasystems/morse-dcms/blob/main/morse-cli/examples/ci-noninteractive.sh): env-var auth, `--yes`, and `--json` parsing.
319
+ - [`files.sh`](https://github.com/arcadiasystems/morse-dcms/blob/main/morse-cli/examples/files.sh): RecipientFile round-trip (upload an encrypted file, download/decrypt via its share string, manage recipients, plus a public file).
300
320
 
301
321
  ## Limitations
302
322
 
@@ -319,7 +339,14 @@ to fetch the full record per file (one read each) when you need them.
319
339
  created but not yet populated through the CLI; `entry add` refuses them.
320
340
  - There is no command to query Walrus blob storage expiry; `--epochs` sets the
321
341
  lease at upload time, but remaining lease time is not exposed by the SDK.
322
- - Mainnet is not yet deployed; use `testnet`.
342
+ - Encrypted commands do not work on mainnet, per the status note above. There
343
+ is no way to supply Seal key servers to the CLI; if you have credentials from
344
+ an operator, use the SDK directly for now.
345
+ - `localnet` is accepted as a network but has no canonical deployment, so it
346
+ fails at startup unless you point the SDK at your own package and registry.
347
+ - `file list` walks a JSON-RPC event query that public Sui fullnodes have
348
+ retired, so it needs `--indexer-url` pointing at a source that serves the same
349
+ query. This affects testnet as well as mainnet.
323
350
 
324
351
  ## Publishing
325
352
 
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.5.0",
151
+ version: "0.6.1",
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.4.2",
206
+ "@arcadiasystems/morse-sdk": "^0.7.0",
207
207
  "@mysten/seal": "1.1.3",
208
208
  "@mysten/sui": "2.16.2",
209
209
  "@mysten/walrus": "1.1.6",
@@ -219,7 +219,7 @@ var package_default = {
219
219
  // src/cli/program.ts
220
220
  function buildProgram() {
221
221
  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: 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();
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
223
  return program;
224
224
  }
225
225
 
@@ -316,14 +316,39 @@ class Output {
316
316
  get isJson() {
317
317
  return this.options.json;
318
318
  }
319
+ withNetwork(network) {
320
+ return new Output({ ...this.options, network });
321
+ }
319
322
  result(human, data) {
320
323
  if (this.options.json) {
321
- this.out(`${toJson(data)}
324
+ this.out(`${toJson(this.stampJson(data))}
322
325
  `);
323
326
  return;
324
327
  }
325
- this.out(`${human}
328
+ this.out(`${this.stampHuman(human)}
329
+ `);
330
+ }
331
+ stampJson(data) {
332
+ const network = this.options.network;
333
+ if (network === undefined || !isPlainObject(data) || "network" in data) {
334
+ return data;
335
+ }
336
+ return { network, ...data };
337
+ }
338
+ stampHuman(human) {
339
+ const network = this.options.network;
340
+ if (network === undefined) {
341
+ return human;
342
+ }
343
+ const line = ` network: ${network}`;
344
+ const breakAt = human.indexOf(`
326
345
  `);
346
+ if (breakAt === -1) {
347
+ return `${human}
348
+ ${line}`;
349
+ }
350
+ return `${human.slice(0, breakAt)}
351
+ ${line}${human.slice(breakAt)}`;
327
352
  }
328
353
  info(message) {
329
354
  if (this.options.quiet || this.options.json) {
@@ -369,6 +394,9 @@ function resolveColor(json) {
369
394
  }
370
395
  return Boolean(process.stderr.isTTY);
371
396
  }
397
+ function isPlainObject(value) {
398
+ return typeof value === "object" && value !== null && !Array.isArray(value);
399
+ }
372
400
  function createOutput(opts) {
373
401
  const json = Boolean(opts.json);
374
402
  return new Output({
@@ -397,13 +425,10 @@ function isNetwork(value) {
397
425
  return typeof value === "string" && NETWORKS.has(value);
398
426
  }
399
427
  function coerceNetwork(value) {
400
- if (value === Network.Mainnet) {
401
- throw new UsageError("Morse is not yet deployed on mainnet. Use testnet or localnet.");
402
- }
403
428
  if (isNetwork(value)) {
404
429
  return value;
405
430
  }
406
- throw new UsageError(`Unknown network "${value}". Use one of: testnet, localnet.`);
431
+ throw new UsageError(`Unknown network "${value}". Use one of: mainnet, testnet, localnet.`);
407
432
  }
408
433
  function parseConfig(raw, source) {
409
434
  if (!isRecord(raw)) {
@@ -945,6 +970,7 @@ async function buildWriteContext(command) {
945
970
  const { base, keypair, address } = await buildSignedBase(command);
946
971
  return {
947
972
  ...base,
973
+ output: base.output.withNetwork(base.settings.network),
948
974
  adapter: new KeypairAdapter(keypair, base.client),
949
975
  address
950
976
  };
@@ -966,12 +992,23 @@ async function buildContentContext(command) {
966
992
  }
967
993
  const adapter = new KeypairAdapter(keypair, base.client);
968
994
  const walrus = DefaultWalrusWriteAdapter.fromConfig({ network, suiClient: base.client }, keypair);
969
- return { ...base, adapter, address, walrus };
995
+ return {
996
+ ...base,
997
+ output: base.output.withNetwork(network),
998
+ adapter,
999
+ address,
1000
+ walrus
1001
+ };
1002
+ }
1003
+ function assertSealAvailable(config) {
1004
+ if (config.sealKeyServers.length > 0) {
1005
+ return;
1006
+ }
1007
+ 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);
970
1008
  }
971
1009
  async function buildEncryptContext(command) {
972
1010
  const ctx = await buildContentContext(command);
973
- const seal = DefaultSealAdapter.fromMorseConfig(ctx.config, {}, ctx.client);
974
- return { ...ctx, seal };
1011
+ return { ...ctx, seal: lazySeal(ctx) };
975
1012
  }
976
1013
  function walrusReadAdapter(base, network, viaAggregator) {
977
1014
  if (viaAggregator) {
@@ -999,6 +1036,7 @@ async function buildDecryptContext(command, opts = {}) {
999
1036
  if (network === "localnet") {
1000
1037
  throw new CliError("Seal decryption is not available on localnet. Use testnet or mainnet.", ExitCode.Usage);
1001
1038
  }
1039
+ assertSealAvailable(base.config);
1002
1040
  const seal = DefaultSealAdapter.fromMorseConfig(base.config, {}, base.client);
1003
1041
  return {
1004
1042
  ...base,
@@ -1020,10 +1058,18 @@ async function buildFileDownloadContext(command, opts = {}) {
1020
1058
  packageId: base.config.packageId
1021
1059
  }),
1022
1060
  walrusRead: walrusReadAdapter(base, network, Boolean(opts.viaAggregator)),
1023
- seal: DefaultSealAdapter.fromMorseConfig(base.config, {}, base.client),
1061
+ seal: lazySeal(base),
1024
1062
  unlockSigner: () => resolveSigner(base.settings.account, process.env, base.signal)
1025
1063
  };
1026
1064
  }
1065
+ function lazySeal(base) {
1066
+ let adapter;
1067
+ return () => {
1068
+ assertSealAvailable(base.config);
1069
+ adapter ??= DefaultSealAdapter.fromMorseConfig(base.config, {}, base.client);
1070
+ return adapter;
1071
+ };
1072
+ }
1027
1073
  async function buildFileListContext(command, opts = {}) {
1028
1074
  const base = await buildReadContext(command);
1029
1075
  const originPackageId = base.config.recipientFileEventOriginPackageId;
@@ -1536,7 +1582,7 @@ function registerConfigCommands(program) {
1536
1582
  config.command("list").description("List profiles and show the default").action(async (_options, command) => {
1537
1583
  await runConfigList(outputFor(command));
1538
1584
  });
1539
- config.command("add <name>").description("Create or update a profile").requiredOption("--network <network>", "Sui network: testnet or localnet").option("--rpc <url>", "RPC URL override for this profile").action(async (name, options, command) => {
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) => {
1540
1586
  await runConfigAdd(outputFor(command), name, options);
1541
1587
  });
1542
1588
  config.command("use <name>").description("Set the default profile").action(async (name, _options, command) => {
@@ -1649,7 +1695,7 @@ async function runEntryAddEncrypted(ctx, name, options) {
1649
1695
  ctx.output.info(`Encrypting and uploading ${plaintext.length} bytes...`);
1650
1696
  const result = await addEncryptedEntryFromBytes(ctx.adapter, ctx.config, {
1651
1697
  walrus: ctx.walrus,
1652
- seal: ctx.seal,
1698
+ seal: ctx.seal(),
1653
1699
  publicationId: id,
1654
1700
  publisherCapId,
1655
1701
  collectionName: collection,
@@ -2117,7 +2163,7 @@ async function runFileUpload(ctx, path, options) {
2117
2163
  const upload = { epochs, deletable: true };
2118
2164
  const onProgress = uploadProgress(ctx.output);
2119
2165
  if (options.public) {
2120
- const result2 = await uploadRecipientFileFromBytes(ctx.adapter, ctx.config, {
2166
+ const result = await uploadRecipientFileFromBytes(ctx.adapter, ctx.config, {
2121
2167
  walrus: ctx.walrus,
2122
2168
  bytes,
2123
2169
  recipients: [],
@@ -2128,15 +2174,15 @@ async function runFileUpload(ctx, path, options) {
2128
2174
  onProgress
2129
2175
  });
2130
2176
  const aggregator = ctx.config.walrusEndpoints.aggregator;
2131
- const viewUrl = aggregator.length > 0 ? `${aggregator}/v1/blobs/${result2.blobId}` : undefined;
2132
- const human2 = viewUrl === undefined ? `Uploaded public file ${result2.fileId}. (tx: ${result2.digest})` : `Uploaded public file ${result2.fileId}. (tx: ${result2.digest})
2177
+ const viewUrl = aggregator.length > 0 ? `${aggregator}/v1/blobs/${result.blobId}` : undefined;
2178
+ const human = viewUrl === undefined ? `Uploaded public file ${result.fileId}. (tx: ${result.digest})` : `Uploaded public file ${result.fileId}. (tx: ${result.digest})
2133
2179
  view: ${viewUrl}`;
2134
- ctx.output.result(human2, { ...result2, viewUrl: viewUrl ?? null });
2180
+ ctx.output.result(human, { ...result, viewUrl: viewUrl ?? null });
2135
2181
  return;
2136
2182
  }
2137
2183
  const result = await uploadEncryptedRecipientFileFromBytes(ctx.adapter, ctx.config, {
2138
2184
  walrus: ctx.walrus,
2139
- seal: ctx.seal,
2185
+ seal: ctx.seal(),
2140
2186
  plaintext: bytes,
2141
2187
  recipients,
2142
2188
  name: options.name,
@@ -2206,7 +2252,7 @@ async function decryptRecipientFile(ctx, fileId, ciphertext, prefix, nonce) {
2206
2252
  signer: keypair,
2207
2253
  suiClient: ctx.client
2208
2254
  });
2209
- return ctx.seal.decryptUnderRecipientFile(ciphertext, {
2255
+ return ctx.seal().decryptUnderRecipientFile(ciphertext, {
2210
2256
  sessionKey,
2211
2257
  sealId,
2212
2258
  fileId
@@ -2222,9 +2268,10 @@ async function runFileDownload(ctx, positional, options) {
2222
2268
  }
2223
2269
  const fileId = toRecipientFileId(target.fileId);
2224
2270
  const file = await ctx.filesReader.getRecipientFile(fileId, ctx.signal);
2225
- if (target.decrypt === undefined && file.members.length > 0) {
2271
+ const encrypted = target.decrypt !== undefined || await ctx.filesReader.getRecipientFileSealPrefix(fileId, ctx.signal) !== null;
2272
+ if (target.decrypt === undefined && encrypted) {
2226
2273
  if (!options.raw) {
2227
- throw new UsageError("This file has a recipient list, so its bytes are encrypted. Pass --share (or --prefix and --nonce) to decrypt, or --raw to write the raw ciphertext anyway.");
2274
+ throw new UsageError("This file is encrypted. Pass --share (or --prefix and --nonce) to decrypt, or --raw to write the raw ciphertext anyway.");
2228
2275
  }
2229
2276
  ctx.output.warn("Writing raw ciphertext (--raw); the output is unusable without decryption.");
2230
2277
  }
@@ -2588,11 +2635,11 @@ function registerRevisionCommands(program) {
2588
2635
  // src/commands/use.ts
2589
2636
  async function runUse(output, gopts, publication, collection, options, makeContext) {
2590
2637
  if (options.clear) {
2591
- const profile2 = await updateActiveProfile(gopts, {
2638
+ const profile = await updateActiveProfile(gopts, {
2592
2639
  publication: undefined,
2593
2640
  collection: undefined
2594
2641
  });
2595
- output.result(`Cleared the active publication and collection (profile "${profile2}").`, { profile: profile2, publication: null, collection: null });
2642
+ output.result(`Cleared the active publication and collection (profile "${profile}").`, { profile, publication: null, collection: null });
2596
2643
  return;
2597
2644
  }
2598
2645
  if (publication === undefined) {
@@ -5,14 +5,21 @@ Sui testnet, then tears it down. Every command is copy-pasteable; expected
5
5
  output is shown beneath each step. Replace IDs in later steps with the ones your
6
6
  own runs print.
7
7
 
8
+ The walkthrough uses testnet on purpose: it creates a publication, uploads
9
+ content to Walrus, and deletes it all again. On mainnet that sequence costs real
10
+ SUI and WAL. Once it works for you, the only change needed is the network on the
11
+ profile in step 1 (`--network mainnet`); every later command reads the network
12
+ from the profile and prints which one it spent on.
13
+
8
14
  ## 0. Prerequisites
9
15
 
10
16
  - Install [Bun](https://bun.sh) >= 1.2.
11
17
  - Have a Sui testnet keypair. If you use the Sui CLI, export one with
12
18
  `sui keytool export --key-identity <alias>` to get a `suiprivkey1...` string.
13
19
  - Fund the address with testnet SUI ([faucet](https://faucet.sui.io/)) for gas,
14
- and with WAL ([Walrus faucet](https://docs.walrus.site/usage/web-tool.html))
15
- for content uploads.
20
+ and with WAL by swapping some SUI at
21
+ [stake-wal.wal.app](https://stake-wal.wal.app/?network=testnet) for content
22
+ uploads.
16
23
 
17
24
  Install the CLI:
18
25
 
@@ -24,6 +31,8 @@ morse --version
24
31
  ## 1. Create a profile
25
32
 
26
33
  A profile pins the network. The first profile you add becomes the default.
34
+ `--network` takes `mainnet`, `testnet`, or `localnet`; with no profile, flag, or
35
+ `MORSE_NETWORK`, the CLI targets testnet.
27
36
 
28
37
  ```sh
29
38
  morse config add testnet --network testnet
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadiasystems/morse-cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
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.4.2",
58
+ "@arcadiasystems/morse-sdk": "^0.7.0",
59
59
  "@mysten/seal": "1.1.3",
60
60
  "@mysten/sui": "2.16.2",
61
61
  "@mysten/walrus": "1.1.6",