@arcadiasystems/morse-cli 0.4.1 → 0.5.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 +46 -0
- package/README.md +12 -4
- package/dist/index.js +111 -27
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,52 @@ 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.5.0] - 2026-06-09
|
|
8
|
+
|
|
9
|
+
Hardening pass from CLI testing: fail fast before paying for Walrus storage,
|
|
10
|
+
clearer errors on doomed operations, and accurate listings.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `revision publish-from-draft` now reuses the draft's already-uploaded content
|
|
15
|
+
by default (no re-upload, no extra WAL, and the published bytes match what was
|
|
16
|
+
reviewed). Pass `--file`/`--stdin` only to publish replacement content.
|
|
17
|
+
- `entry list` / `entry scan` accept `--drafts-only` to show just entries with a
|
|
18
|
+
pending (unpublished) draft. `entry list`/`entry get` now show a draft marker /
|
|
19
|
+
`pendingDraft` line.
|
|
20
|
+
- `file download --raw` writes the still-encrypted bytes when no decrypt input is
|
|
21
|
+
given.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- `file download` now refuses to write ciphertext when a file has recipients and
|
|
26
|
+
no `--share`/`--prefix`+`--nonce` is given; pass `--raw` to opt in. Previously
|
|
27
|
+
it warned and wrote the raw bytes.
|
|
28
|
+
- `entry add` verifies the collection exists (and is blob-mode) before uploading
|
|
29
|
+
to Walrus, so a missing or quilt collection fails without burning storage.
|
|
30
|
+
- `collection delete` checks the collection is empty up front and returns a clear
|
|
31
|
+
error instead of a raw transaction abort.
|
|
32
|
+
- `file list` now reconciles metadata-update and recipient events, so renames and
|
|
33
|
+
recipient counts are current without `--hydrate`.
|
|
34
|
+
- `collection create --mode quilt` warns that quilt collections cannot yet be
|
|
35
|
+
populated with `entry add`.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- The `revision` commands refuse to append an unencrypted revision to an entry
|
|
40
|
+
that has encrypted revisions, instead of silently producing a mixed-encryption
|
|
41
|
+
entry.
|
|
42
|
+
|
|
43
|
+
## [0.4.2] - 2026-06-05
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- Requires `@arcadiasystems/morse-sdk` `^0.4.2`. SDK 0.4.2 fixes a gRPC reader
|
|
48
|
+
bug (`Option<ID>` decoding) that prevented `morse file download` and `morse
|
|
49
|
+
file get` from reading a RecipientFile back. No CLI source changes; the fix is
|
|
50
|
+
internal to `RpcRecipientFilesReader.getRecipientFile`, which the CLI already
|
|
51
|
+
calls.
|
|
52
|
+
|
|
7
53
|
## [0.4.1] - 2026-06-05
|
|
8
54
|
|
|
9
55
|
### Changed
|
package/README.md
CHANGED
|
@@ -188,8 +188,8 @@ and `-C, --collection <name>`, both defaulting to the active context.
|
|
|
188
188
|
| --- | --- |
|
|
189
189
|
| `entry get <entryId> [-P …] [-C …]` | Fetch a single entry's metadata. |
|
|
190
190
|
| `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). |
|
|
191
|
+
| `entry list [--drafts-only] [-P …] [-C …]` | List entries (paginated); `--drafts-only` shows just entries with a pending draft. |
|
|
192
|
+
| `entry scan [--drafts-only] [-P …] [-C …]` | List every entry (auto-paginated). |
|
|
193
193
|
| `entry add <name> --file <path> [-P …] [-C …]` | Upload content and add a new entry; prints a viewable link. |
|
|
194
194
|
| `entry delete <entryId> [-P …] [-C …]` | Delete an entry. |
|
|
195
195
|
| `entry add-encrypted <name> --file <path> [-P …] [-C …]` | Encrypt, upload, and add a new entry. |
|
|
@@ -212,7 +212,7 @@ commands; pure on-chain commands (publication, collection, cap) have no epochs.
|
|
|
212
212
|
| --- | --- |
|
|
213
213
|
| `revision publish-direct <entryId> --file <path> [-P …] [-C …]` | Upload content and append a public revision. |
|
|
214
214
|
| `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
|
|
215
|
+
| `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
216
|
|
|
217
217
|
### cap
|
|
218
218
|
|
|
@@ -238,7 +238,7 @@ There is no separate allowlist object.
|
|
|
238
238
|
| --- | --- |
|
|
239
239
|
| `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
240
|
| `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). |
|
|
241
|
+
| `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
242
|
| `file list [--address <addr>] [--accessible] [--hydrate] [--limit <n>] [--indexer-url <url>]` | List files owned by (or, with `--accessible`, decryptable by) an address. |
|
|
243
243
|
| `file get <file>` | Fetch a file's on-chain metadata and recipient list. |
|
|
244
244
|
| `file update <file> --name <n> --content-type <m>` | Update name and MIME (owner only). |
|
|
@@ -311,6 +311,14 @@ to fetch the full record per file (one read each) when you need them.
|
|
|
311
311
|
to read through the Walrus aggregator HTTP service instead: more reliable when
|
|
312
312
|
storage nodes are flaky (common on testnet), at the cost of trusting the
|
|
313
313
|
aggregator's bytes (no client-side verification).
|
|
314
|
+
- Encrypted entries cannot be revised. The `revision` commands and `entry add`
|
|
315
|
+
only produce unencrypted content, so the CLI refuses to append to an entry that
|
|
316
|
+
already has encrypted revisions (to avoid a mixed-encryption entry). Use
|
|
317
|
+
`entry add-encrypted` for new encrypted entries.
|
|
318
|
+
- `entry add` supports blob-mode collections only. Quilt collections can be
|
|
319
|
+
created but not yet populated through the CLI; `entry add` refuses them.
|
|
320
|
+
- There is no command to query Walrus blob storage expiry; `--epochs` sets the
|
|
321
|
+
lease at upload time, but remaining lease time is not exposed by the SDK.
|
|
314
322
|
- Mainnet is not yet deployed; use `testnet`.
|
|
315
323
|
|
|
316
324
|
## Publishing
|
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.5.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.4.
|
|
206
|
+
"@arcadiasystems/morse-sdk": "^0.4.2",
|
|
207
207
|
"@mysten/seal": "1.1.3",
|
|
208
208
|
"@mysten/sui": "2.16.2",
|
|
209
209
|
"@mysten/walrus": "1.1.6",
|
|
@@ -1136,11 +1136,18 @@ function renderPublicationList(items) {
|
|
|
1136
1136
|
return items.map((item) => `${item.publicationId} (owner cap ${shortId(item.ownerCapId)})`).join(`
|
|
1137
1137
|
`);
|
|
1138
1138
|
}
|
|
1139
|
+
function hasPendingDraft(entry) {
|
|
1140
|
+
if (entry.draftHead === null) {
|
|
1141
|
+
return false;
|
|
1142
|
+
}
|
|
1143
|
+
return entry.publicHead === null || entry.draftHead > entry.publicHead;
|
|
1144
|
+
}
|
|
1139
1145
|
function renderEntry(entry) {
|
|
1140
1146
|
const lines = [
|
|
1141
1147
|
`#${entry.id} ${entry.name}`,
|
|
1142
|
-
`publicHead:
|
|
1143
|
-
`
|
|
1148
|
+
`publicHead: ${headLabel(entry.publicHead)} draftHead: ${headLabel(entry.draftHead)}`,
|
|
1149
|
+
`pendingDraft: ${hasPendingDraft(entry) ? "yes" : "no"}`,
|
|
1150
|
+
`revisions: ${entry.revisions.length}`
|
|
1144
1151
|
];
|
|
1145
1152
|
for (const revision of entry.revisions) {
|
|
1146
1153
|
lines.push(` ${renderRevisionLine(revision)}`);
|
|
@@ -1152,7 +1159,10 @@ function renderEntryList(entries) {
|
|
|
1152
1159
|
if (entries.length === 0) {
|
|
1153
1160
|
return "No entries in this collection.";
|
|
1154
1161
|
}
|
|
1155
|
-
return entries.map((entry) =>
|
|
1162
|
+
return entries.map((entry) => {
|
|
1163
|
+
const marker = hasPendingDraft(entry) ? " [draft]" : "";
|
|
1164
|
+
return `#${entry.id} ${entry.name} (${entry.revisions.length} revisions)${marker}`;
|
|
1165
|
+
}).join(`
|
|
1156
1166
|
`);
|
|
1157
1167
|
}
|
|
1158
1168
|
function renderFileList(items) {
|
|
@@ -1397,7 +1407,11 @@ function registerCapCommands(program) {
|
|
|
1397
1407
|
}
|
|
1398
1408
|
|
|
1399
1409
|
// src/commands/collection.ts
|
|
1400
|
-
import {
|
|
1410
|
+
import {
|
|
1411
|
+
createCollection,
|
|
1412
|
+
deleteCollection,
|
|
1413
|
+
StorageMode as StorageMode2
|
|
1414
|
+
} from "@arcadiasystems/morse-sdk";
|
|
1401
1415
|
async function runCollectionList(ctx, options) {
|
|
1402
1416
|
const id = await resolvePublication(ctx, options.publication);
|
|
1403
1417
|
const publication = await ctx.reader.getPublication(id, ctx.signal);
|
|
@@ -1410,6 +1424,9 @@ async function runCollectionCreate(ctx, name, options, gopts) {
|
|
|
1410
1424
|
const id = await resolvePublication(ctx, options.publication);
|
|
1411
1425
|
const storageMode = coerceStorageMode(options.mode);
|
|
1412
1426
|
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1427
|
+
if (storageMode === StorageMode2.Quilt) {
|
|
1428
|
+
ctx.output.warn("Quilt collections cannot yet be populated with `morse entry add` (only blob collections are supported).");
|
|
1429
|
+
}
|
|
1413
1430
|
ctx.output.info(`Creating collection "${name}" (${storageMode})...`);
|
|
1414
1431
|
const result = await createCollection(ctx.adapter, ctx.config, {
|
|
1415
1432
|
publicationId: id,
|
|
@@ -1423,6 +1440,13 @@ async function runCollectionCreate(ctx, name, options, gopts) {
|
|
|
1423
1440
|
}
|
|
1424
1441
|
async function runCollectionDelete(ctx, name, options, gopts) {
|
|
1425
1442
|
const id = await resolvePublication(ctx, options.publication);
|
|
1443
|
+
const entries = await ctx.reader.listEntries(id, name, {
|
|
1444
|
+
limit: 1,
|
|
1445
|
+
signal: ctx.signal
|
|
1446
|
+
});
|
|
1447
|
+
if (entries.results.length > 0) {
|
|
1448
|
+
throw new UsageError(`Cannot delete collection "${name}": it still has entries. Delete them first.`);
|
|
1449
|
+
}
|
|
1426
1450
|
const proceed = await confirm(`Delete collection "${name}" from ${shortId(id)}? It must be empty.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
|
|
1427
1451
|
if (!proceed) {
|
|
1428
1452
|
cancelled();
|
|
@@ -1553,7 +1577,8 @@ function renderProfiles(config) {
|
|
|
1553
1577
|
// src/commands/entry.ts
|
|
1554
1578
|
import {
|
|
1555
1579
|
addEntryFromBytes,
|
|
1556
|
-
deleteEntry
|
|
1580
|
+
deleteEntry,
|
|
1581
|
+
StorageMode as StorageMode3
|
|
1557
1582
|
} from "@arcadiasystems/morse-sdk";
|
|
1558
1583
|
|
|
1559
1584
|
// src/cli/input.ts
|
|
@@ -1701,6 +1726,16 @@ function registerEncryptedEntryCommands(entry) {
|
|
|
1701
1726
|
}
|
|
1702
1727
|
|
|
1703
1728
|
// src/commands/entry.ts
|
|
1729
|
+
async function assertEntryAddCollection(ctx, publicationId, collection) {
|
|
1730
|
+
const publication = await ctx.reader.getPublication(publicationId, ctx.signal);
|
|
1731
|
+
const found = publication.collections.find((c) => c.name === collection);
|
|
1732
|
+
if (found === undefined) {
|
|
1733
|
+
throw new UsageError(`Publication ${shortId(publicationId)} has no collection "${collection}".`);
|
|
1734
|
+
}
|
|
1735
|
+
if (found.storageMode === StorageMode3.Quilt) {
|
|
1736
|
+
throw new UsageError(`Collection "${collection}" uses quilt storage, which \`entry add\` does not support yet. Use a blob-mode collection.`);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1704
1739
|
async function runEntryGet(ctx, entryId, options) {
|
|
1705
1740
|
const id = await resolvePublication(ctx, options.publication);
|
|
1706
1741
|
const collection = resolveCollection(ctx, options.collection);
|
|
@@ -1718,8 +1753,9 @@ async function runEntryList(ctx, options) {
|
|
|
1718
1753
|
if (page.nextCursor !== null) {
|
|
1719
1754
|
ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
|
|
1720
1755
|
}
|
|
1721
|
-
|
|
1722
|
-
|
|
1756
|
+
const results = options.draftsOnly ? page.results.filter(hasPendingDraft) : page.results;
|
|
1757
|
+
ctx.output.result(renderEntryList(results), {
|
|
1758
|
+
results,
|
|
1723
1759
|
nextCursor: page.nextCursor
|
|
1724
1760
|
});
|
|
1725
1761
|
}
|
|
@@ -1730,7 +1766,9 @@ async function runEntryScan(ctx, options) {
|
|
|
1730
1766
|
for await (const item of ctx.reader.scanEntries(id, collection, {
|
|
1731
1767
|
signal: ctx.signal
|
|
1732
1768
|
})) {
|
|
1733
|
-
|
|
1769
|
+
if (!options.draftsOnly || hasPendingDraft(item)) {
|
|
1770
|
+
entries.push(item);
|
|
1771
|
+
}
|
|
1734
1772
|
}
|
|
1735
1773
|
ctx.output.result(renderEntryList(entries), { results: entries });
|
|
1736
1774
|
}
|
|
@@ -1740,6 +1778,7 @@ async function runEntryAdd(ctx, name, options) {
|
|
|
1740
1778
|
const epochs = parsePositiveInt(options.epochs, "--epochs");
|
|
1741
1779
|
const bytes = await readContentBytes(options);
|
|
1742
1780
|
const contentType = resolveContentType(options.contentType, options.stdin ? undefined : options.file);
|
|
1781
|
+
await assertEntryAddCollection(ctx, id, collection);
|
|
1743
1782
|
const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
|
|
1744
1783
|
ctx.output.info(`Uploading ${bytes.length} bytes to Walrus...`);
|
|
1745
1784
|
const result = await addEntryFromBytes(ctx.adapter, ctx.config, {
|
|
@@ -1819,11 +1858,11 @@ function registerEntryCommands(program) {
|
|
|
1819
1858
|
collectionOption(publicationOption(get)).action(async (entryId, options, command) => {
|
|
1820
1859
|
await runEntryGet(await buildReadContext(command), entryId, options);
|
|
1821
1860
|
});
|
|
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");
|
|
1861
|
+
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
1862
|
collectionOption(publicationOption(list)).action(async (options, command) => {
|
|
1824
1863
|
await runEntryList(await buildReadContext(command), options);
|
|
1825
1864
|
});
|
|
1826
|
-
const scan = entry.command("scan").description("List every entry in a collection (auto-paginated)");
|
|
1865
|
+
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
1866
|
collectionOption(publicationOption(scan)).action(async (options, command) => {
|
|
1828
1867
|
await runEntryScan(await buildReadContext(command), options);
|
|
1829
1868
|
});
|
|
@@ -2178,10 +2217,16 @@ async function runFileDownload(ctx, positional, options) {
|
|
|
2178
2217
|
throw new UsageError("Downloading content to stdout is not supported in --json mode; pass --out <path>.");
|
|
2179
2218
|
}
|
|
2180
2219
|
const target = resolveDownloadTarget(positional, options);
|
|
2220
|
+
if (options.raw && target.decrypt !== undefined) {
|
|
2221
|
+
ctx.output.warn("--raw is ignored because a decrypt input (--share/--prefix/--nonce) was given; decrypting.");
|
|
2222
|
+
}
|
|
2181
2223
|
const fileId = toRecipientFileId(target.fileId);
|
|
2182
2224
|
const file = await ctx.filesReader.getRecipientFile(fileId, ctx.signal);
|
|
2183
2225
|
if (target.decrypt === undefined && file.members.length > 0) {
|
|
2184
|
-
|
|
2226
|
+
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.");
|
|
2228
|
+
}
|
|
2229
|
+
ctx.output.warn("Writing raw ciphertext (--raw); the output is unusable without decryption.");
|
|
2185
2230
|
}
|
|
2186
2231
|
ctx.output.info("Fetching content from Walrus...");
|
|
2187
2232
|
const bytes = await ctx.walrusRead.readBlob(file.blobId, {
|
|
@@ -2216,6 +2261,7 @@ async function runFileList(ctx, options) {
|
|
|
2216
2261
|
ctx.output.info("Fetching recipient and file events...");
|
|
2217
2262
|
const events = await fetchEventStreams(ctx.events, [
|
|
2218
2263
|
types.RecipientFileCreated,
|
|
2264
|
+
types.RecipientFileMetadataUpdated,
|
|
2219
2265
|
types.RecipientAdded,
|
|
2220
2266
|
types.RecipientRemoved,
|
|
2221
2267
|
types.RecipientFileDeleted
|
|
@@ -2225,7 +2271,10 @@ async function runFileList(ctx, options) {
|
|
|
2225
2271
|
ctx.output.info("Fetching file events...");
|
|
2226
2272
|
const events = await fetchEventStreams(ctx.events, [
|
|
2227
2273
|
types.RecipientFileCreated,
|
|
2274
|
+
types.RecipientFileMetadataUpdated,
|
|
2228
2275
|
types.RecipientFileOwnershipTransferred,
|
|
2276
|
+
types.RecipientAdded,
|
|
2277
|
+
types.RecipientRemoved,
|
|
2229
2278
|
types.RecipientFileDeleted
|
|
2230
2279
|
], ctx.signal);
|
|
2231
2280
|
summaries = reconcileRecipientFilesOwnedBy(events, address, types);
|
|
@@ -2254,7 +2303,7 @@ function registerFileCommands(program) {
|
|
|
2254
2303
|
indexerUrl: options.indexerUrl
|
|
2255
2304
|
}), options);
|
|
2256
2305
|
});
|
|
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");
|
|
2306
|
+
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
2307
|
viaAggregatorOption(download).action(async (target, options, command) => {
|
|
2259
2308
|
await runFileDownload(await buildFileDownloadContext(command, {
|
|
2260
2309
|
viaAggregator: options.viaAggregator
|
|
@@ -2428,26 +2477,41 @@ async function resolveTarget(ctx, options) {
|
|
|
2428
2477
|
const collection = resolveCollection(ctx, options.collection);
|
|
2429
2478
|
return { publicationId, collection };
|
|
2430
2479
|
}
|
|
2431
|
-
async function
|
|
2480
|
+
async function loadWritableEntry(ctx, publicationId, collection, entryId) {
|
|
2481
|
+
const entry = await ctx.reader.getEntry(publicationId, collection, entryId, ctx.signal);
|
|
2482
|
+
if (entry.revisions.some((revision) => revision.encrypted)) {
|
|
2483
|
+
throw new UsageError(`Entry #${entryId} has encrypted revisions; the CLI cannot append an unencrypted revision to it. Encrypted revisions are not yet supported.`);
|
|
2484
|
+
}
|
|
2485
|
+
return entry;
|
|
2486
|
+
}
|
|
2487
|
+
async function readLocalContent(options) {
|
|
2432
2488
|
const epochs = parsePositiveInt(options.epochs, "--epochs");
|
|
2433
2489
|
const bytes = await readContentBytes(options);
|
|
2434
2490
|
const contentType = resolveContentType(options.contentType, options.stdin ? undefined : options.file);
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2491
|
+
return { bytes, contentType, epochs };
|
|
2492
|
+
}
|
|
2493
|
+
async function uploadLocal(ctx, local) {
|
|
2494
|
+
ctx.output.info(`Uploading ${local.bytes.length} bytes to Walrus...`);
|
|
2495
|
+
const upload = await ctx.walrus.uploadBlob(local.bytes, {
|
|
2496
|
+
epochs: local.epochs,
|
|
2439
2497
|
deletable: true
|
|
2440
2498
|
});
|
|
2441
2499
|
ctx.output.info(`Blob uploaded (${upload.blobObjectId}). Submitting transaction...`);
|
|
2442
|
-
return { blobObjectId: upload.blobObjectId, contentType
|
|
2500
|
+
return { blobObjectId: upload.blobObjectId, contentType: local.contentType };
|
|
2501
|
+
}
|
|
2502
|
+
function capFor(ctx, publicationId, options) {
|
|
2503
|
+
return resolvePublisherCap(ctx.reader, ctx.address, publicationId, options.publisherCap, ctx.signal);
|
|
2443
2504
|
}
|
|
2444
2505
|
async function runRevisionPublishDirect(ctx, entryId, options) {
|
|
2445
2506
|
const { publicationId, collection } = await resolveTarget(ctx, options);
|
|
2446
2507
|
const numericEntryId = parseId(entryId, "entryId");
|
|
2447
|
-
const
|
|
2508
|
+
const local = await readLocalContent(options);
|
|
2509
|
+
await loadWritableEntry(ctx, publicationId, collection, numericEntryId);
|
|
2510
|
+
const publisherCapId = await capFor(ctx, publicationId, options);
|
|
2511
|
+
const prepared = await uploadLocal(ctx, local);
|
|
2448
2512
|
const result = await publishDirect(ctx.adapter, ctx.config, {
|
|
2449
2513
|
publicationId,
|
|
2450
|
-
publisherCapId
|
|
2514
|
+
publisherCapId,
|
|
2451
2515
|
collectionName: collection,
|
|
2452
2516
|
entryId: numericEntryId,
|
|
2453
2517
|
blobObjectId: prepared.blobObjectId,
|
|
@@ -2459,10 +2523,13 @@ async function runRevisionPublishDirect(ctx, entryId, options) {
|
|
|
2459
2523
|
async function runRevisionAppendDraft(ctx, entryId, options) {
|
|
2460
2524
|
const { publicationId, collection } = await resolveTarget(ctx, options);
|
|
2461
2525
|
const numericEntryId = parseId(entryId, "entryId");
|
|
2462
|
-
const
|
|
2526
|
+
const local = await readLocalContent(options);
|
|
2527
|
+
await loadWritableEntry(ctx, publicationId, collection, numericEntryId);
|
|
2528
|
+
const publisherCapId = await capFor(ctx, publicationId, options);
|
|
2529
|
+
const prepared = await uploadLocal(ctx, local);
|
|
2463
2530
|
const result = await appendDraftRevision(ctx.adapter, ctx.config, {
|
|
2464
2531
|
publicationId,
|
|
2465
|
-
publisherCapId
|
|
2532
|
+
publisherCapId,
|
|
2466
2533
|
collectionName: collection,
|
|
2467
2534
|
entryId: numericEntryId,
|
|
2468
2535
|
blobObjectId: prepared.blobObjectId,
|
|
@@ -2471,14 +2538,31 @@ async function runRevisionAppendDraft(ctx, entryId, options) {
|
|
|
2471
2538
|
});
|
|
2472
2539
|
ctx.output.result(`Appended draft revision #${result.revisionId} on entry #${numericEntryId}. (tx: ${result.digest})`, result);
|
|
2473
2540
|
}
|
|
2541
|
+
function reuseDraftBlob(entry, draftRevisionId) {
|
|
2542
|
+
const draft = entry.revisions.find((r) => r.id === draftRevisionId);
|
|
2543
|
+
if (draft === undefined) {
|
|
2544
|
+
throw new UsageError(`Entry #${entry.id} has no revision #${draftRevisionId} to publish.`);
|
|
2545
|
+
}
|
|
2546
|
+
if (draft.blobRef.kind !== "blob") {
|
|
2547
|
+
throw new UsageError(`Draft #${draftRevisionId} uses quilt storage, which cannot be reused; pass --file or --stdin to publish new content.`);
|
|
2548
|
+
}
|
|
2549
|
+
return {
|
|
2550
|
+
blobObjectId: draft.blobRef.blobObjectId,
|
|
2551
|
+
contentType: draft.contentType
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2474
2554
|
async function runRevisionPublishFromDraft(ctx, entryId, draftRevisionId, options) {
|
|
2475
2555
|
const { publicationId, collection } = await resolveTarget(ctx, options);
|
|
2476
2556
|
const numericEntryId = parseId(entryId, "entryId");
|
|
2477
2557
|
const draftId = parseId(draftRevisionId, "draftRevisionId");
|
|
2478
|
-
const
|
|
2558
|
+
const providedNewContent = options.file !== undefined || Boolean(options.stdin);
|
|
2559
|
+
const local = providedNewContent ? await readLocalContent(options) : undefined;
|
|
2560
|
+
const entry = await loadWritableEntry(ctx, publicationId, collection, numericEntryId);
|
|
2561
|
+
const publisherCapId = await capFor(ctx, publicationId, options);
|
|
2562
|
+
const prepared = local === undefined ? reuseDraftBlob(entry, draftId) : await uploadLocal(ctx, local);
|
|
2479
2563
|
const result = await publishFromDraft(ctx.adapter, ctx.config, {
|
|
2480
2564
|
publicationId,
|
|
2481
|
-
publisherCapId
|
|
2565
|
+
publisherCapId,
|
|
2482
2566
|
collectionName: collection,
|
|
2483
2567
|
entryId: numericEntryId,
|
|
2484
2568
|
draftRevisionId: draftId,
|
|
@@ -2496,7 +2580,7 @@ function registerRevisionCommands(program) {
|
|
|
2496
2580
|
revisionOptions(revision.command("append-draft <entryId>").description("Upload content and append it as a draft revision")).action(async (entryId, options, command) => {
|
|
2497
2581
|
await runRevisionAppendDraft(await buildContentContext(command), entryId, options);
|
|
2498
2582
|
});
|
|
2499
|
-
revisionOptions(revision.command("publish-from-draft <entryId> <draftRevisionId>").description("
|
|
2583
|
+
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
2584
|
await runRevisionPublishFromDraft(await buildContentContext(command), entryId, draftRevisionId, options);
|
|
2501
2585
|
});
|
|
2502
2586
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcadiasystems/morse-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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.4.
|
|
58
|
+
"@arcadiasystems/morse-sdk": "^0.4.2",
|
|
59
59
|
"@mysten/seal": "1.1.3",
|
|
60
60
|
"@mysten/sui": "2.16.2",
|
|
61
61
|
"@mysten/walrus": "1.1.6",
|