@arcadiasystems/morse-cli 0.4.2 → 0.6.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 +81 -0
- package/README.md +39 -19
- package/dist/index.js +158 -42
- package/docs/QUICKSTART.md +11 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,87 @@ 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.0] - 2026-09-09
|
|
8
|
+
|
|
9
|
+
Mainnet support, consuming `@arcadiasystems/morse-sdk@0.5.0`. The Move contracts
|
|
10
|
+
went live on Sui mainnet on 2026-09-09; the CLI rejected `mainnet` outright until
|
|
11
|
+
now.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`--network mainnet` works**, as do `MORSE_NETWORK=mainnet` and
|
|
16
|
+
`morse config add <name> --network mainnet`. `coerceNetwork` no longer
|
|
17
|
+
special-cases mainnet, and `localnet` is accepted too (it still fails at
|
|
18
|
+
startup unless you point the SDK at your own package and registry, which is
|
|
19
|
+
`morseConfig`'s error to raise, not the flag parser's).
|
|
20
|
+
- **Commands that spend gas now name the network they spent on.** The resolved
|
|
21
|
+
network appears as the first detail line under the result headline, and as a
|
|
22
|
+
`network` field in `--json` output. Applied on the write contexts only, so read
|
|
23
|
+
and list output is byte-for-byte unchanged. Nothing selects mainnet implicitly
|
|
24
|
+
(with no flag, env var, or profile the CLI targets testnet), so this is there
|
|
25
|
+
to make a mainnet spend visible after the fact rather than to gate it.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Status banner claimed v0.3.0, two releases stale, and said mainnet support was
|
|
30
|
+
pending.
|
|
31
|
+
- `--network` help text listed only testnet and localnet, in both the global
|
|
32
|
+
flag (`morse --help`) and `config add --network`, as did the `MORSE_NETWORK`
|
|
33
|
+
row of the README env table.
|
|
34
|
+
- Seven README links pointed into `examples/`, which is not in the package
|
|
35
|
+
`files` array, so they 404'd on the npm page. Now absolute GitHub URLs;
|
|
36
|
+
`docs/QUICKSTART.md` stays relative because `docs` does ship.
|
|
37
|
+
- Dead Walrus faucet link in `docs/QUICKSTART.md`. `docs.walrus.site` no longer
|
|
38
|
+
resolves; WAL acquisition now points at the working stake-wal.wal.app swap,
|
|
39
|
+
matching the README and morse-sdk.
|
|
40
|
+
- Known limitations now record that `file list` requires `--indexer-url`, since
|
|
41
|
+
public Sui fullnodes have retired the JSON-RPC event query it walks. This
|
|
42
|
+
affects testnet as well as mainnet and predates this release.
|
|
43
|
+
|
|
44
|
+
### Known gaps
|
|
45
|
+
|
|
46
|
+
- `file list` is unusable without `--indexer-url` on every network, per above.
|
|
47
|
+
Porting the event source off `suix_queryEvents` is tracked separately.
|
|
48
|
+
- No mainnet run of the opt-in `bun run test:e2e` lifecycle; it stays pointed at
|
|
49
|
+
testnet, where it costs nothing real.
|
|
50
|
+
- The Move contracts remain unaudited.
|
|
51
|
+
|
|
52
|
+
## [0.5.0] - 2026-06-09
|
|
53
|
+
|
|
54
|
+
Hardening pass from CLI testing: fail fast before paying for Walrus storage,
|
|
55
|
+
clearer errors on doomed operations, and accurate listings.
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- `revision publish-from-draft` now reuses the draft's already-uploaded content
|
|
60
|
+
by default (no re-upload, no extra WAL, and the published bytes match what was
|
|
61
|
+
reviewed). Pass `--file`/`--stdin` only to publish replacement content.
|
|
62
|
+
- `entry list` / `entry scan` accept `--drafts-only` to show just entries with a
|
|
63
|
+
pending (unpublished) draft. `entry list`/`entry get` now show a draft marker /
|
|
64
|
+
`pendingDraft` line.
|
|
65
|
+
- `file download --raw` writes the still-encrypted bytes when no decrypt input is
|
|
66
|
+
given.
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
|
|
70
|
+
- `file download` now refuses to write ciphertext when a file has recipients and
|
|
71
|
+
no `--share`/`--prefix`+`--nonce` is given; pass `--raw` to opt in. Previously
|
|
72
|
+
it warned and wrote the raw bytes.
|
|
73
|
+
- `entry add` verifies the collection exists (and is blob-mode) before uploading
|
|
74
|
+
to Walrus, so a missing or quilt collection fails without burning storage.
|
|
75
|
+
- `collection delete` checks the collection is empty up front and returns a clear
|
|
76
|
+
error instead of a raw transaction abort.
|
|
77
|
+
- `file list` now reconciles metadata-update and recipient events, so renames and
|
|
78
|
+
recipient counts are current without `--hydrate`.
|
|
79
|
+
- `collection create --mode quilt` warns that quilt collections cannot yet be
|
|
80
|
+
populated with `entry add`.
|
|
81
|
+
|
|
82
|
+
### Fixed
|
|
83
|
+
|
|
84
|
+
- The `revision` commands refuse to append an unencrypted revision to an entry
|
|
85
|
+
that has encrypted revisions, instead of silently producing a mixed-encryption
|
|
86
|
+
entry.
|
|
87
|
+
|
|
7
88
|
## [0.4.2] - 2026-06-05
|
|
8
89
|
|
|
9
90
|
### Changed
|
package/README.md
CHANGED
|
@@ -6,16 +6,24 @@ 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.
|
|
10
|
-
>
|
|
9
|
+
> Status: v0.6.0. Mainnet and testnet are both supported; the command surface is
|
|
10
|
+
> stable.
|
|
11
|
+
>
|
|
12
|
+
> **The Move contracts are unaudited.** On mainnet, gas and Walrus storage cost
|
|
13
|
+
> real SUI and WAL, there is no faucet, and deletions are irreversible. Nothing
|
|
14
|
+
> selects mainnet for you: with no `--network`, `MORSE_NETWORK`, or profile
|
|
15
|
+
> setting, the CLI targets testnet. Every command that spends gas prints the
|
|
16
|
+
> network it used, and `--json` output carries a `network` field.
|
|
11
17
|
|
|
12
18
|
## Requirements
|
|
13
19
|
|
|
14
20
|
- [Node.js](https://nodejs.org) >= 18, or [Bun](https://bun.sh) >= 1.2. The
|
|
15
21
|
published CLI runs under either.
|
|
16
|
-
- A funded Sui
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
- A funded Sui address for gas, and WAL for Walrus storage when adding content.
|
|
23
|
+
On testnet, get SUI from the [Sui faucet](https://faucet.sui.io/) and swap some
|
|
24
|
+
for WAL at [stake-wal.wal.app](https://stake-wal.wal.app/?network=testnet). On
|
|
25
|
+
mainnet there is no faucet: both have to be acquired, and every write spends
|
|
26
|
+
real value.
|
|
19
27
|
|
|
20
28
|
## Install
|
|
21
29
|
|
|
@@ -91,7 +99,7 @@ Environment variables (override the config file, overridden by flags):
|
|
|
91
99
|
| Variable | Overrides | Notes |
|
|
92
100
|
| --- | --- | --- |
|
|
93
101
|
| `MORSE_PROFILE` | `--profile` | Profile to use. |
|
|
94
|
-
| `MORSE_NETWORK` | `--network` | `testnet
|
|
102
|
+
| `MORSE_NETWORK` | `--network` | `mainnet`, `testnet`, or `localnet`. |
|
|
95
103
|
| `MORSE_RPC_URL` | `--rpc` | Sui RPC URL override. |
|
|
96
104
|
| `MORSE_ADDRESS` | (no flag) | Active account address, selecting which keystore to use. |
|
|
97
105
|
| `MORSE_PUBLICATION` | `-P, --publication` | Active publication id. |
|
|
@@ -123,7 +131,7 @@ Global options apply to every command and must appear before the subcommand
|
|
|
123
131
|
|
|
124
132
|
| Global option | Purpose |
|
|
125
133
|
| --- | --- |
|
|
126
|
-
| `--network <testnet\|localnet>` | Network to target (default: testnet). |
|
|
134
|
+
| `--network <mainnet\|testnet\|localnet>` | Network to target (default: testnet). `localnet` needs a custom deployment. |
|
|
127
135
|
| `-p, --profile <name>` | Config profile to use. |
|
|
128
136
|
| `--rpc <url>` | Override the Sui RPC URL. |
|
|
129
137
|
| `--json` | Machine-readable JSON on stdout. |
|
|
@@ -188,8 +196,8 @@ and `-C, --collection <name>`, both defaulting to the active context.
|
|
|
188
196
|
| --- | --- |
|
|
189
197
|
| `entry get <entryId> [-P …] [-C …]` | Fetch a single entry's metadata. |
|
|
190
198
|
| `entry read <entryId> [revisionIndex] [--out <path>] [--via-aggregator] [-P …] [-C …]` | Fetch a public entry's content to stdout or a file. |
|
|
191
|
-
| `entry list [-P …] [-C …]` | List entries (paginated). |
|
|
192
|
-
| `entry scan [-P …] [-C …]` | List every entry (auto-paginated). |
|
|
199
|
+
| `entry list [--drafts-only] [-P …] [-C …]` | List entries (paginated); `--drafts-only` shows just entries with a pending draft. |
|
|
200
|
+
| `entry scan [--drafts-only] [-P …] [-C …]` | List every entry (auto-paginated). |
|
|
193
201
|
| `entry add <name> --file <path> [-P …] [-C …]` | Upload content and add a new entry; prints a viewable link. |
|
|
194
202
|
| `entry delete <entryId> [-P …] [-C …]` | Delete an entry. |
|
|
195
203
|
| `entry add-encrypted <name> --file <path> [-P …] [-C …]` | Encrypt, upload, and add a new entry. |
|
|
@@ -212,7 +220,7 @@ commands; pure on-chain commands (publication, collection, cap) have no epochs.
|
|
|
212
220
|
| --- | --- |
|
|
213
221
|
| `revision publish-direct <entryId> --file <path> [-P …] [-C …]` | Upload content and append a public revision. |
|
|
214
222
|
| `revision append-draft <entryId> --file <path> [-P …] [-C …]` | Upload content and append a draft revision. |
|
|
215
|
-
| `revision publish-from-draft <entryId> <draftRevisionId> --file <path> [-P …] [-C …]` | Publish a new revision
|
|
223
|
+
| `revision publish-from-draft <entryId> <draftRevisionId> [--file <path>] [-P …] [-C …]` | Publish a draft as a new revision. Reuses the draft's content by default; pass `--file`/`--stdin` to publish replacement content. |
|
|
216
224
|
|
|
217
225
|
### cap
|
|
218
226
|
|
|
@@ -238,7 +246,7 @@ There is no separate allowlist object.
|
|
|
238
246
|
| --- | --- |
|
|
239
247
|
| `file upload <path> --name <n> (--public \| --encrypt \| -r <addr>...) [--content-type <m>] [--epochs <n>]` | Upload to Walrus and register. `--public` is world-readable; `--encrypt` (or supplying `-r`) encrypts, and the sender is always a recipient. One of the three is required. |
|
|
240
248
|
| `file register --blob-id <id> --name <n> --content-type <m> --size <bytes> (--public \| --encrypted --seal-prefix <hex>) [-r <addr>...] [--blob-object-id <id>]` | Register metadata for a blob already on Walrus. |
|
|
241
|
-
| `file download [file] [--out <path>] [--share <s>] [--prefix <hex> --nonce <hex>] [--via-aggregator]` | Download content; decrypts in place with a share string (or prefix + nonce). |
|
|
249
|
+
| `file download [file] [--out <path>] [--share <s>] [--prefix <hex> --nonce <hex>] [--raw] [--via-aggregator]` | Download content; decrypts in place with a share string (or prefix + nonce). For an encrypted file with no decrypt input, it refuses unless `--raw` is given. |
|
|
242
250
|
| `file list [--address <addr>] [--accessible] [--hydrate] [--limit <n>] [--indexer-url <url>]` | List files owned by (or, with `--accessible`, decryptable by) an address. |
|
|
243
251
|
| `file get <file>` | Fetch a file's on-chain metadata and recipient list. |
|
|
244
252
|
| `file update <file> --name <n> --content-type <m>` | Update name and MIME (owner only). |
|
|
@@ -290,13 +298,13 @@ to fetch the full record per file (one read each) when you need them.
|
|
|
290
298
|
## Examples
|
|
291
299
|
|
|
292
300
|
- [docs/QUICKSTART.md](./docs/QUICKSTART.md): a full, copy-pasteable walkthrough.
|
|
293
|
-
- Runnable shell recipes in [examples/](
|
|
294
|
-
- [`lifecycle.sh`](
|
|
295
|
-
- [`content.sh`](
|
|
296
|
-
- [`encrypt-decrypt.sh`](
|
|
297
|
-
- [`delegation.sh`](
|
|
298
|
-
- [`ci-noninteractive.sh`](
|
|
299
|
-
- [`files.sh`](
|
|
301
|
+
- Runnable shell recipes in [examples/](https://github.com/arcadiasystems/morse-dcms/tree/main/morse-cli/examples):
|
|
302
|
+
- [`lifecycle.sh`](https://github.com/arcadiasystems/morse-dcms/blob/main/morse-cli/examples/lifecycle.sh): create, add an entry, read, revise, tear down.
|
|
303
|
+
- [`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.
|
|
304
|
+
- [`encrypt-decrypt.sh`](https://github.com/arcadiasystems/morse-dcms/blob/main/morse-cli/examples/encrypt-decrypt.sh): encrypt with Seal and decrypt back.
|
|
305
|
+
- [`delegation.sh`](https://github.com/arcadiasystems/morse-dcms/blob/main/morse-cli/examples/delegation.sh): issue a PublisherCap to a delegate, then revoke it.
|
|
306
|
+
- [`ci-noninteractive.sh`](https://github.com/arcadiasystems/morse-dcms/blob/main/morse-cli/examples/ci-noninteractive.sh): env-var auth, `--yes`, and `--json` parsing.
|
|
307
|
+
- [`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
308
|
|
|
301
309
|
## Limitations
|
|
302
310
|
|
|
@@ -311,7 +319,19 @@ to fetch the full record per file (one read each) when you need them.
|
|
|
311
319
|
to read through the Walrus aggregator HTTP service instead: more reliable when
|
|
312
320
|
storage nodes are flaky (common on testnet), at the cost of trusting the
|
|
313
321
|
aggregator's bytes (no client-side verification).
|
|
314
|
-
-
|
|
322
|
+
- Encrypted entries cannot be revised. The `revision` commands and `entry add`
|
|
323
|
+
only produce unencrypted content, so the CLI refuses to append to an entry that
|
|
324
|
+
already has encrypted revisions (to avoid a mixed-encryption entry). Use
|
|
325
|
+
`entry add-encrypted` for new encrypted entries.
|
|
326
|
+
- `entry add` supports blob-mode collections only. Quilt collections can be
|
|
327
|
+
created but not yet populated through the CLI; `entry add` refuses them.
|
|
328
|
+
- There is no command to query Walrus blob storage expiry; `--epochs` sets the
|
|
329
|
+
lease at upload time, but remaining lease time is not exposed by the SDK.
|
|
330
|
+
- `localnet` is accepted as a network but has no canonical deployment, so it
|
|
331
|
+
fails at startup unless you point the SDK at your own package and registry.
|
|
332
|
+
- `file list` walks a JSON-RPC event query that public Sui fullnodes have
|
|
333
|
+
retired, so it needs `--indexer-url` pointing at a source that serves the same
|
|
334
|
+
query. This affects testnet as well as mainnet.
|
|
315
335
|
|
|
316
336
|
## Publishing
|
|
317
337
|
|
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.6.0",
|
|
152
152
|
description: "Command-line interface for the Morse decentralized CMS on Sui.",
|
|
153
153
|
license: "MIT",
|
|
154
154
|
type: "module",
|
|
@@ -203,7 +203,7 @@ var package_default = {
|
|
|
203
203
|
prepublishOnly: "bun run check && bun run build"
|
|
204
204
|
},
|
|
205
205
|
dependencies: {
|
|
206
|
-
"@arcadiasystems/morse-sdk": "^0.
|
|
206
|
+
"@arcadiasystems/morse-sdk": "^0.5.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,7 +992,13 @@ 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 {
|
|
995
|
+
return {
|
|
996
|
+
...base,
|
|
997
|
+
output: base.output.withNetwork(network),
|
|
998
|
+
adapter,
|
|
999
|
+
address,
|
|
1000
|
+
walrus
|
|
1001
|
+
};
|
|
970
1002
|
}
|
|
971
1003
|
async function buildEncryptContext(command) {
|
|
972
1004
|
const ctx = await buildContentContext(command);
|
|
@@ -1136,11 +1168,18 @@ function renderPublicationList(items) {
|
|
|
1136
1168
|
return items.map((item) => `${item.publicationId} (owner cap ${shortId(item.ownerCapId)})`).join(`
|
|
1137
1169
|
`);
|
|
1138
1170
|
}
|
|
1171
|
+
function hasPendingDraft(entry) {
|
|
1172
|
+
if (entry.draftHead === null) {
|
|
1173
|
+
return false;
|
|
1174
|
+
}
|
|
1175
|
+
return entry.publicHead === null || entry.draftHead > entry.publicHead;
|
|
1176
|
+
}
|
|
1139
1177
|
function renderEntry(entry) {
|
|
1140
1178
|
const lines = [
|
|
1141
1179
|
`#${entry.id} ${entry.name}`,
|
|
1142
|
-
`publicHead:
|
|
1143
|
-
`
|
|
1180
|
+
`publicHead: ${headLabel(entry.publicHead)} draftHead: ${headLabel(entry.draftHead)}`,
|
|
1181
|
+
`pendingDraft: ${hasPendingDraft(entry) ? "yes" : "no"}`,
|
|
1182
|
+
`revisions: ${entry.revisions.length}`
|
|
1144
1183
|
];
|
|
1145
1184
|
for (const revision of entry.revisions) {
|
|
1146
1185
|
lines.push(` ${renderRevisionLine(revision)}`);
|
|
@@ -1152,7 +1191,10 @@ function renderEntryList(entries) {
|
|
|
1152
1191
|
if (entries.length === 0) {
|
|
1153
1192
|
return "No entries in this collection.";
|
|
1154
1193
|
}
|
|
1155
|
-
return entries.map((entry) =>
|
|
1194
|
+
return entries.map((entry) => {
|
|
1195
|
+
const marker = hasPendingDraft(entry) ? " [draft]" : "";
|
|
1196
|
+
return `#${entry.id} ${entry.name} (${entry.revisions.length} revisions)${marker}`;
|
|
1197
|
+
}).join(`
|
|
1156
1198
|
`);
|
|
1157
1199
|
}
|
|
1158
1200
|
function renderFileList(items) {
|
|
@@ -1397,7 +1439,11 @@ function registerCapCommands(program) {
|
|
|
1397
1439
|
}
|
|
1398
1440
|
|
|
1399
1441
|
// src/commands/collection.ts
|
|
1400
|
-
import {
|
|
1442
|
+
import {
|
|
1443
|
+
createCollection,
|
|
1444
|
+
deleteCollection,
|
|
1445
|
+
StorageMode as StorageMode2
|
|
1446
|
+
} from "@arcadiasystems/morse-sdk";
|
|
1401
1447
|
async function runCollectionList(ctx, options) {
|
|
1402
1448
|
const id = await resolvePublication(ctx, options.publication);
|
|
1403
1449
|
const publication = await ctx.reader.getPublication(id, ctx.signal);
|
|
@@ -1410,6 +1456,9 @@ async function runCollectionCreate(ctx, name, options, gopts) {
|
|
|
1410
1456
|
const id = await resolvePublication(ctx, options.publication);
|
|
1411
1457
|
const storageMode = coerceStorageMode(options.mode);
|
|
1412
1458
|
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1459
|
+
if (storageMode === StorageMode2.Quilt) {
|
|
1460
|
+
ctx.output.warn("Quilt collections cannot yet be populated with `morse entry add` (only blob collections are supported).");
|
|
1461
|
+
}
|
|
1413
1462
|
ctx.output.info(`Creating collection "${name}" (${storageMode})...`);
|
|
1414
1463
|
const result = await createCollection(ctx.adapter, ctx.config, {
|
|
1415
1464
|
publicationId: id,
|
|
@@ -1423,6 +1472,13 @@ async function runCollectionCreate(ctx, name, options, gopts) {
|
|
|
1423
1472
|
}
|
|
1424
1473
|
async function runCollectionDelete(ctx, name, options, gopts) {
|
|
1425
1474
|
const id = await resolvePublication(ctx, options.publication);
|
|
1475
|
+
const entries = await ctx.reader.listEntries(id, name, {
|
|
1476
|
+
limit: 1,
|
|
1477
|
+
signal: ctx.signal
|
|
1478
|
+
});
|
|
1479
|
+
if (entries.results.length > 0) {
|
|
1480
|
+
throw new UsageError(`Cannot delete collection "${name}": it still has entries. Delete them first.`);
|
|
1481
|
+
}
|
|
1426
1482
|
const proceed = await confirm(`Delete collection "${name}" from ${shortId(id)}? It must be empty.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
1427
1483
|
if (!proceed) {
|
|
1428
1484
|
cancelled();
|
|
@@ -1512,7 +1568,7 @@ function registerConfigCommands(program) {
|
|
|
1512
1568
|
config.command("list").description("List profiles and show the default").action(async (_options, command) => {
|
|
1513
1569
|
await runConfigList(outputFor(command));
|
|
1514
1570
|
});
|
|
1515
|
-
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) => {
|
|
1571
|
+
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) => {
|
|
1516
1572
|
await runConfigAdd(outputFor(command), name, options);
|
|
1517
1573
|
});
|
|
1518
1574
|
config.command("use <name>").description("Set the default profile").action(async (name, _options, command) => {
|
|
@@ -1553,7 +1609,8 @@ function renderProfiles(config) {
|
|
|
1553
1609
|
// src/commands/entry.ts
|
|
1554
1610
|
import {
|
|
1555
1611
|
addEntryFromBytes,
|
|
1556
|
-
deleteEntry
|
|
1612
|
+
deleteEntry,
|
|
1613
|
+
StorageMode as StorageMode3
|
|
1557
1614
|
} from "@arcadiasystems/morse-sdk";
|
|
1558
1615
|
|
|
1559
1616
|
// src/cli/input.ts
|
|
@@ -1701,6 +1758,16 @@ function registerEncryptedEntryCommands(entry) {
|
|
|
1701
1758
|
}
|
|
1702
1759
|
|
|
1703
1760
|
// src/commands/entry.ts
|
|
1761
|
+
async function assertEntryAddCollection(ctx, publicationId, collection) {
|
|
1762
|
+
const publication = await ctx.reader.getPublication(publicationId, ctx.signal);
|
|
1763
|
+
const found = publication.collections.find((c) => c.name === collection);
|
|
1764
|
+
if (found === undefined) {
|
|
1765
|
+
throw new UsageError(`Publication ${shortId(publicationId)} has no collection "${collection}".`);
|
|
1766
|
+
}
|
|
1767
|
+
if (found.storageMode === StorageMode3.Quilt) {
|
|
1768
|
+
throw new UsageError(`Collection "${collection}" uses quilt storage, which \`entry add\` does not support yet. Use a blob-mode collection.`);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1704
1771
|
async function runEntryGet(ctx, entryId, options) {
|
|
1705
1772
|
const id = await resolvePublication(ctx, options.publication);
|
|
1706
1773
|
const collection = resolveCollection(ctx, options.collection);
|
|
@@ -1718,8 +1785,9 @@ async function runEntryList(ctx, options) {
|
|
|
1718
1785
|
if (page.nextCursor !== null) {
|
|
1719
1786
|
ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
|
|
1720
1787
|
}
|
|
1721
|
-
|
|
1722
|
-
|
|
1788
|
+
const results = options.draftsOnly ? page.results.filter(hasPendingDraft) : page.results;
|
|
1789
|
+
ctx.output.result(renderEntryList(results), {
|
|
1790
|
+
results,
|
|
1723
1791
|
nextCursor: page.nextCursor
|
|
1724
1792
|
});
|
|
1725
1793
|
}
|
|
@@ -1730,7 +1798,9 @@ async function runEntryScan(ctx, options) {
|
|
|
1730
1798
|
for await (const item of ctx.reader.scanEntries(id, collection, {
|
|
1731
1799
|
signal: ctx.signal
|
|
1732
1800
|
})) {
|
|
1733
|
-
|
|
1801
|
+
if (!options.draftsOnly || hasPendingDraft(item)) {
|
|
1802
|
+
entries.push(item);
|
|
1803
|
+
}
|
|
1734
1804
|
}
|
|
1735
1805
|
ctx.output.result(renderEntryList(entries), { results: entries });
|
|
1736
1806
|
}
|
|
@@ -1740,6 +1810,7 @@ async function runEntryAdd(ctx, name, options) {
|
|
|
1740
1810
|
const epochs = parsePositiveInt(options.epochs, "--epochs");
|
|
1741
1811
|
const bytes = await readContentBytes(options);
|
|
1742
1812
|
const contentType = resolveContentType(options.contentType, options.stdin ? undefined : options.file);
|
|
1813
|
+
await assertEntryAddCollection(ctx, id, collection);
|
|
1743
1814
|
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1744
1815
|
ctx.output.info(`Uploading ${bytes.length} bytes to Walrus...`);
|
|
1745
1816
|
const result = await addEntryFromBytes(ctx.adapter, ctx.config, {
|
|
@@ -1819,11 +1890,11 @@ function registerEntryCommands(program) {
|
|
|
1819
1890
|
collectionOption(publicationOption(get)).action(async (entryId, options, command) => {
|
|
1820
1891
|
await runEntryGet(await buildReadContext(command), entryId, options);
|
|
1821
1892
|
});
|
|
1822
|
-
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");
|
|
1893
|
+
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");
|
|
1823
1894
|
collectionOption(publicationOption(list)).action(async (options, command) => {
|
|
1824
1895
|
await runEntryList(await buildReadContext(command), options);
|
|
1825
1896
|
});
|
|
1826
|
-
const scan = entry.command("scan").description("List every entry in a collection (auto-paginated)");
|
|
1897
|
+
const scan = entry.command("scan").description("List every entry in a collection (auto-paginated)").option("--drafts-only", "Show only entries with a pending (unpublished) draft");
|
|
1827
1898
|
collectionOption(publicationOption(scan)).action(async (options, command) => {
|
|
1828
1899
|
await runEntryScan(await buildReadContext(command), options);
|
|
1829
1900
|
});
|
|
@@ -2078,7 +2149,7 @@ async function runFileUpload(ctx, path, options) {
|
|
|
2078
2149
|
const upload = { epochs, deletable: true };
|
|
2079
2150
|
const onProgress = uploadProgress(ctx.output);
|
|
2080
2151
|
if (options.public) {
|
|
2081
|
-
const
|
|
2152
|
+
const result = await uploadRecipientFileFromBytes(ctx.adapter, ctx.config, {
|
|
2082
2153
|
walrus: ctx.walrus,
|
|
2083
2154
|
bytes,
|
|
2084
2155
|
recipients: [],
|
|
@@ -2089,10 +2160,10 @@ async function runFileUpload(ctx, path, options) {
|
|
|
2089
2160
|
onProgress
|
|
2090
2161
|
});
|
|
2091
2162
|
const aggregator = ctx.config.walrusEndpoints.aggregator;
|
|
2092
|
-
const viewUrl = aggregator.length > 0 ? `${aggregator}/v1/blobs/${
|
|
2093
|
-
const
|
|
2163
|
+
const viewUrl = aggregator.length > 0 ? `${aggregator}/v1/blobs/${result.blobId}` : undefined;
|
|
2164
|
+
const human = viewUrl === undefined ? `Uploaded public file ${result.fileId}. (tx: ${result.digest})` : `Uploaded public file ${result.fileId}. (tx: ${result.digest})
|
|
2094
2165
|
view: ${viewUrl}`;
|
|
2095
|
-
ctx.output.result(
|
|
2166
|
+
ctx.output.result(human, { ...result, viewUrl: viewUrl ?? null });
|
|
2096
2167
|
return;
|
|
2097
2168
|
}
|
|
2098
2169
|
const result = await uploadEncryptedRecipientFileFromBytes(ctx.adapter, ctx.config, {
|
|
@@ -2178,10 +2249,16 @@ async function runFileDownload(ctx, positional, options) {
|
|
|
2178
2249
|
throw new UsageError("Downloading content to stdout is not supported in --json mode; pass --out <path>.");
|
|
2179
2250
|
}
|
|
2180
2251
|
const target = resolveDownloadTarget(positional, options);
|
|
2252
|
+
if (options.raw && target.decrypt !== undefined) {
|
|
2253
|
+
ctx.output.warn("--raw is ignored because a decrypt input (--share/--prefix/--nonce) was given; decrypting.");
|
|
2254
|
+
}
|
|
2181
2255
|
const fileId = toRecipientFileId(target.fileId);
|
|
2182
2256
|
const file = await ctx.filesReader.getRecipientFile(fileId, ctx.signal);
|
|
2183
2257
|
if (target.decrypt === undefined && file.members.length > 0) {
|
|
2184
|
-
|
|
2258
|
+
if (!options.raw) {
|
|
2259
|
+
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.");
|
|
2260
|
+
}
|
|
2261
|
+
ctx.output.warn("Writing raw ciphertext (--raw); the output is unusable without decryption.");
|
|
2185
2262
|
}
|
|
2186
2263
|
ctx.output.info("Fetching content from Walrus...");
|
|
2187
2264
|
const bytes = await ctx.walrusRead.readBlob(file.blobId, {
|
|
@@ -2216,6 +2293,7 @@ async function runFileList(ctx, options) {
|
|
|
2216
2293
|
ctx.output.info("Fetching recipient and file events...");
|
|
2217
2294
|
const events = await fetchEventStreams(ctx.events, [
|
|
2218
2295
|
types.RecipientFileCreated,
|
|
2296
|
+
types.RecipientFileMetadataUpdated,
|
|
2219
2297
|
types.RecipientAdded,
|
|
2220
2298
|
types.RecipientRemoved,
|
|
2221
2299
|
types.RecipientFileDeleted
|
|
@@ -2225,7 +2303,10 @@ async function runFileList(ctx, options) {
|
|
|
2225
2303
|
ctx.output.info("Fetching file events...");
|
|
2226
2304
|
const events = await fetchEventStreams(ctx.events, [
|
|
2227
2305
|
types.RecipientFileCreated,
|
|
2306
|
+
types.RecipientFileMetadataUpdated,
|
|
2228
2307
|
types.RecipientFileOwnershipTransferred,
|
|
2308
|
+
types.RecipientAdded,
|
|
2309
|
+
types.RecipientRemoved,
|
|
2229
2310
|
types.RecipientFileDeleted
|
|
2230
2311
|
], ctx.signal);
|
|
2231
2312
|
summaries = reconcileRecipientFilesOwnedBy(events, address, types);
|
|
@@ -2254,7 +2335,7 @@ function registerFileCommands(program) {
|
|
|
2254
2335
|
indexerUrl: options.indexerUrl
|
|
2255
2336
|
}), options);
|
|
2256
2337
|
});
|
|
2257
|
-
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");
|
|
2338
|
+
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");
|
|
2258
2339
|
viaAggregatorOption(download).action(async (target, options, command) => {
|
|
2259
2340
|
await runFileDownload(await buildFileDownloadContext(command, {
|
|
2260
2341
|
viaAggregator: options.viaAggregator
|
|
@@ -2428,26 +2509,41 @@ async function resolveTarget(ctx, options) {
|
|
|
2428
2509
|
const collection = resolveCollection(ctx, options.collection);
|
|
2429
2510
|
return { publicationId, collection };
|
|
2430
2511
|
}
|
|
2431
|
-
async function
|
|
2512
|
+
async function loadWritableEntry(ctx, publicationId, collection, entryId) {
|
|
2513
|
+
const entry = await ctx.reader.getEntry(publicationId, collection, entryId, ctx.signal);
|
|
2514
|
+
if (entry.revisions.some((revision) => revision.encrypted)) {
|
|
2515
|
+
throw new UsageError(`Entry #${entryId} has encrypted revisions; the CLI cannot append an unencrypted revision to it. Encrypted revisions are not yet supported.`);
|
|
2516
|
+
}
|
|
2517
|
+
return entry;
|
|
2518
|
+
}
|
|
2519
|
+
async function readLocalContent(options) {
|
|
2432
2520
|
const epochs = parsePositiveInt(options.epochs, "--epochs");
|
|
2433
2521
|
const bytes = await readContentBytes(options);
|
|
2434
2522
|
const contentType = resolveContentType(options.contentType, options.stdin ? undefined : options.file);
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2523
|
+
return { bytes, contentType, epochs };
|
|
2524
|
+
}
|
|
2525
|
+
async function uploadLocal(ctx, local) {
|
|
2526
|
+
ctx.output.info(`Uploading ${local.bytes.length} bytes to Walrus...`);
|
|
2527
|
+
const upload = await ctx.walrus.uploadBlob(local.bytes, {
|
|
2528
|
+
epochs: local.epochs,
|
|
2439
2529
|
deletable: true
|
|
2440
2530
|
});
|
|
2441
2531
|
ctx.output.info(`Blob uploaded (${upload.blobObjectId}). Submitting transaction...`);
|
|
2442
|
-
return { blobObjectId: upload.blobObjectId, contentType
|
|
2532
|
+
return { blobObjectId: upload.blobObjectId, contentType: local.contentType };
|
|
2533
|
+
}
|
|
2534
|
+
function capFor(ctx, publicationId, options) {
|
|
2535
|
+
return resolvePublisherCap(ctx.reader, ctx.address, publicationId, options.publisherCap, ctx.signal);
|
|
2443
2536
|
}
|
|
2444
2537
|
async function runRevisionPublishDirect(ctx, entryId, options) {
|
|
2445
2538
|
const { publicationId, collection } = await resolveTarget(ctx, options);
|
|
2446
2539
|
const numericEntryId = parseId(entryId, "entryId");
|
|
2447
|
-
const
|
|
2540
|
+
const local = await readLocalContent(options);
|
|
2541
|
+
await loadWritableEntry(ctx, publicationId, collection, numericEntryId);
|
|
2542
|
+
const publisherCapId = await capFor(ctx, publicationId, options);
|
|
2543
|
+
const prepared = await uploadLocal(ctx, local);
|
|
2448
2544
|
const result = await publishDirect(ctx.adapter, ctx.config, {
|
|
2449
2545
|
publicationId,
|
|
2450
|
-
publisherCapId
|
|
2546
|
+
publisherCapId,
|
|
2451
2547
|
collectionName: collection,
|
|
2452
2548
|
entryId: numericEntryId,
|
|
2453
2549
|
blobObjectId: prepared.blobObjectId,
|
|
@@ -2459,10 +2555,13 @@ async function runRevisionPublishDirect(ctx, entryId, options) {
|
|
|
2459
2555
|
async function runRevisionAppendDraft(ctx, entryId, options) {
|
|
2460
2556
|
const { publicationId, collection } = await resolveTarget(ctx, options);
|
|
2461
2557
|
const numericEntryId = parseId(entryId, "entryId");
|
|
2462
|
-
const
|
|
2558
|
+
const local = await readLocalContent(options);
|
|
2559
|
+
await loadWritableEntry(ctx, publicationId, collection, numericEntryId);
|
|
2560
|
+
const publisherCapId = await capFor(ctx, publicationId, options);
|
|
2561
|
+
const prepared = await uploadLocal(ctx, local);
|
|
2463
2562
|
const result = await appendDraftRevision(ctx.adapter, ctx.config, {
|
|
2464
2563
|
publicationId,
|
|
2465
|
-
publisherCapId
|
|
2564
|
+
publisherCapId,
|
|
2466
2565
|
collectionName: collection,
|
|
2467
2566
|
entryId: numericEntryId,
|
|
2468
2567
|
blobObjectId: prepared.blobObjectId,
|
|
@@ -2471,14 +2570,31 @@ async function runRevisionAppendDraft(ctx, entryId, options) {
|
|
|
2471
2570
|
});
|
|
2472
2571
|
ctx.output.result(`Appended draft revision #${result.revisionId} on entry #${numericEntryId}. (tx: ${result.digest})`, result);
|
|
2473
2572
|
}
|
|
2573
|
+
function reuseDraftBlob(entry, draftRevisionId) {
|
|
2574
|
+
const draft = entry.revisions.find((r) => r.id === draftRevisionId);
|
|
2575
|
+
if (draft === undefined) {
|
|
2576
|
+
throw new UsageError(`Entry #${entry.id} has no revision #${draftRevisionId} to publish.`);
|
|
2577
|
+
}
|
|
2578
|
+
if (draft.blobRef.kind !== "blob") {
|
|
2579
|
+
throw new UsageError(`Draft #${draftRevisionId} uses quilt storage, which cannot be reused; pass --file or --stdin to publish new content.`);
|
|
2580
|
+
}
|
|
2581
|
+
return {
|
|
2582
|
+
blobObjectId: draft.blobRef.blobObjectId,
|
|
2583
|
+
contentType: draft.contentType
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2474
2586
|
async function runRevisionPublishFromDraft(ctx, entryId, draftRevisionId, options) {
|
|
2475
2587
|
const { publicationId, collection } = await resolveTarget(ctx, options);
|
|
2476
2588
|
const numericEntryId = parseId(entryId, "entryId");
|
|
2477
2589
|
const draftId = parseId(draftRevisionId, "draftRevisionId");
|
|
2478
|
-
const
|
|
2590
|
+
const providedNewContent = options.file !== undefined || Boolean(options.stdin);
|
|
2591
|
+
const local = providedNewContent ? await readLocalContent(options) : undefined;
|
|
2592
|
+
const entry = await loadWritableEntry(ctx, publicationId, collection, numericEntryId);
|
|
2593
|
+
const publisherCapId = await capFor(ctx, publicationId, options);
|
|
2594
|
+
const prepared = local === undefined ? reuseDraftBlob(entry, draftId) : await uploadLocal(ctx, local);
|
|
2479
2595
|
const result = await publishFromDraft(ctx.adapter, ctx.config, {
|
|
2480
2596
|
publicationId,
|
|
2481
|
-
publisherCapId
|
|
2597
|
+
publisherCapId,
|
|
2482
2598
|
collectionName: collection,
|
|
2483
2599
|
entryId: numericEntryId,
|
|
2484
2600
|
draftRevisionId: draftId,
|
|
@@ -2496,7 +2612,7 @@ function registerRevisionCommands(program) {
|
|
|
2496
2612
|
revisionOptions(revision.command("append-draft <entryId>").description("Upload content and append it as a draft revision")).action(async (entryId, options, command) => {
|
|
2497
2613
|
await runRevisionAppendDraft(await buildContentContext(command), entryId, options);
|
|
2498
2614
|
});
|
|
2499
|
-
revisionOptions(revision.command("publish-from-draft <entryId> <draftRevisionId>").description("
|
|
2615
|
+
revisionOptions(revision.command("publish-from-draft <entryId> <draftRevisionId>").description("Publish a draft as a new revision (reuses the draft's content; pass --file/--stdin to publish replacement content)")).action(async (entryId, draftRevisionId, options, command) => {
|
|
2500
2616
|
await runRevisionPublishFromDraft(await buildContentContext(command), entryId, draftRevisionId, options);
|
|
2501
2617
|
});
|
|
2502
2618
|
}
|
|
@@ -2504,11 +2620,11 @@ function registerRevisionCommands(program) {
|
|
|
2504
2620
|
// src/commands/use.ts
|
|
2505
2621
|
async function runUse(output, gopts, publication, collection, options, makeContext) {
|
|
2506
2622
|
if (options.clear) {
|
|
2507
|
-
const
|
|
2623
|
+
const profile = await updateActiveProfile(gopts, {
|
|
2508
2624
|
publication: undefined,
|
|
2509
2625
|
collection: undefined
|
|
2510
2626
|
});
|
|
2511
|
-
output.result(`Cleared the active publication and collection (profile "${
|
|
2627
|
+
output.result(`Cleared the active publication and collection (profile "${profile}").`, { profile, publication: null, collection: null });
|
|
2512
2628
|
return;
|
|
2513
2629
|
}
|
|
2514
2630
|
if (publication === undefined) {
|
package/docs/QUICKSTART.md
CHANGED
|
@@ -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
|
|
15
|
-
for content
|
|
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.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Command-line interface for the Morse decentralized CMS on Sui.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"prepublishOnly": "bun run check && bun run build"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@arcadiasystems/morse-sdk": "^0.
|
|
58
|
+
"@arcadiasystems/morse-sdk": "^0.5.0",
|
|
59
59
|
"@mysten/seal": "1.1.3",
|
|
60
60
|
"@mysten/sui": "2.16.2",
|
|
61
61
|
"@mysten/walrus": "1.1.6",
|