@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 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, referencing a draft. |
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.4.1",
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.1",
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: ${headLabel(entry.publicHead)} draftHead: ${headLabel(entry.draftHead)}`,
1143
- `revisions: ${entry.revisions.length}`
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) => `#${entry.id} ${entry.name} (${entry.revisions.length} revisions)`).join(`
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 { createCollection, deleteCollection } from "@arcadiasystems/morse-sdk";
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
- ctx.output.result(renderEntryList(page.results), {
1722
- results: page.results,
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
- entries.push(item);
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
- ctx.output.warn("This file has a recipient list, so its bytes are likely encrypted. Pass --share (or --prefix and --nonce) to decrypt; writing the raw bytes as-is.");
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 prepareContent(ctx, publicationId, options) {
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
- const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, publicationId, options.publisherCap, ctx.signal);
2436
- ctx.output.info(`Uploading ${bytes.length} bytes to Walrus...`);
2437
- const upload = await ctx.walrus.uploadBlob(bytes, {
2438
- epochs,
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, publisherCapId };
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 prepared = await prepareContent(ctx, publicationId, options);
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: prepared.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 prepared = await prepareContent(ctx, publicationId, options);
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: prepared.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 prepared = await prepareContent(ctx, publicationId, options);
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: prepared.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("Upload content and publish it as a new revision, referencing a draft")).action(async (entryId, draftRevisionId, options, command) => {
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.4.1",
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.1",
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",