@arcadiasystems/morse-cli 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +21 -2
- package/dist/index.js +51 -38
- package/docs/QUICKSTART.md +12 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to `@arcadiasystems/morse-cli` are documented here. The
|
|
|
4
4
|
format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.8.0] - 2026-09-10
|
|
8
|
+
|
|
9
|
+
Usability pass. Nothing is renamed or removed; every existing spelling keeps working.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Verb aliases, so the word you guess is the word that works.** The command surface had grown four inconsistencies where one concept had two names depending on the noun: fetching one thing was `get` on publication, entry and file but `show` on account; deleting was `delete` everywhere except `config remove`; transferring was `transfer-ownership` on publication and file but `transfer` on cap; and reading content was `entry read` but `file download`, despite both taking `--out` and `--via-aggregator`.
|
|
14
|
+
|
|
15
|
+
Each now accepts both spellings: `account show|get`, `publication get|show`, `entry get|show`, `file get|show`, `config remove|delete`, `cap transfer|transfer-ownership`, `entry read|download`, `file download|read`. Aliases, not renames, so nothing published breaks.
|
|
16
|
+
- **`entry list --all`** does what `entry scan` does. `scan` is accurate but undiscoverable; people look for `--all` and conclude pagination is unsupported. `scan` stays. Combining `--all` with `--limit` or `--cursor` is refused rather than silently ignoring one.
|
|
17
|
+
- **`account import` reads a key piped on stdin.** Previously it required an interactive terminal or `MORSE_PRIVATE_KEY`, so the obvious scripted form failed outright. Still never a flag: argv is visible in `ps` and shell history. The error when no key can be found now names all three routes.
|
|
18
|
+
|
|
7
19
|
## [0.7.0] - 2026-09-09
|
|
8
20
|
|
|
9
21
|
### Added
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ content entries from your terminal, signing with a locally encrypted key.
|
|
|
6
6
|
Content is stored on [Walrus](https://walrus.xyz); private entries are encrypted
|
|
7
7
|
with [Seal](https://github.com/MystenLabs/seal).
|
|
8
8
|
|
|
9
|
-
> Status: v0.
|
|
9
|
+
> Status: v0.8.0. Mainnet and testnet are both supported for public content;
|
|
10
10
|
> the command surface is stable.
|
|
11
11
|
>
|
|
12
12
|
> **Encrypted commands are testnet-only.** `entry add-encrypted`, `entry
|
|
@@ -173,9 +173,9 @@ and `-C, --collection <name>`, both defaulting to the active context.
|
|
|
173
173
|
| Command | Purpose |
|
|
174
174
|
| --- | --- |
|
|
175
175
|
| `config add <name> --network <net> [--rpc <url>] [--upload-relay <url\|auto>]` | Create or update a profile. |
|
|
176
|
+
| `config remove <name>` | Delete a profile. Also `config delete`. |
|
|
176
177
|
| `config list` | List profiles; `*` marks the default. |
|
|
177
178
|
| `config use <name>` | Set the default profile. |
|
|
178
|
-
| `config remove <name>` | Delete a profile. |
|
|
179
179
|
| `config path` | Print the config file path. |
|
|
180
180
|
|
|
181
181
|
### account
|
|
@@ -352,6 +352,25 @@ to fetch the full record per file (one read each) when you need them.
|
|
|
352
352
|
retired, so it needs `--indexer-url` pointing at a source that serves the same
|
|
353
353
|
query. This affects testnet as well as mainnet.
|
|
354
354
|
|
|
355
|
+
## Command spellings
|
|
356
|
+
|
|
357
|
+
Several commands accept two verbs for the same action, so whichever you reach
|
|
358
|
+
for works. These are aliases, not separate commands:
|
|
359
|
+
|
|
360
|
+
| Canonical | Also accepts |
|
|
361
|
+
| --- | --- |
|
|
362
|
+
| `account show` | `account get` |
|
|
363
|
+
| `publication get` | `publication show` |
|
|
364
|
+
| `entry get` | `entry show` |
|
|
365
|
+
| `file get` | `file show` |
|
|
366
|
+
| `config remove` | `config delete` |
|
|
367
|
+
| `cap transfer` | `cap transfer-ownership` |
|
|
368
|
+
| `entry read` | `entry download` |
|
|
369
|
+
| `file download` | `file read` |
|
|
370
|
+
| `entry scan` | `entry list --all` |
|
|
371
|
+
|
|
372
|
+
`publication` also answers to `pub`.
|
|
373
|
+
|
|
355
374
|
## When uploads fail: the upload relay
|
|
356
375
|
|
|
357
376
|
A Walrus upload pushes slivers to every storage node in the committee at once,
|
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.8.0",
|
|
152
152
|
description: "Command-line interface for the Morse decentralized CMS on Sui.",
|
|
153
153
|
license: "MIT",
|
|
154
154
|
type: "module",
|
|
@@ -224,6 +224,33 @@ function buildProgram() {
|
|
|
224
224
|
return program;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
// src/cli/io.ts
|
|
228
|
+
import { access, readFile, writeFile } from "node:fs/promises";
|
|
229
|
+
async function fileExists(path) {
|
|
230
|
+
try {
|
|
231
|
+
await access(path);
|
|
232
|
+
return true;
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function readBytes(path) {
|
|
238
|
+
return new Uint8Array(await readFile(path));
|
|
239
|
+
}
|
|
240
|
+
async function readJson(path) {
|
|
241
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
242
|
+
}
|
|
243
|
+
async function writeFileContents(path, data) {
|
|
244
|
+
await writeFile(path, data);
|
|
245
|
+
}
|
|
246
|
+
async function readStdin() {
|
|
247
|
+
const chunks = [];
|
|
248
|
+
for await (const chunk of process.stdin) {
|
|
249
|
+
chunks.push(chunk);
|
|
250
|
+
}
|
|
251
|
+
return new Uint8Array(Buffer.concat(chunks));
|
|
252
|
+
}
|
|
253
|
+
|
|
227
254
|
// src/cli/prompts.ts
|
|
228
255
|
import * as readline from "node:readline";
|
|
229
256
|
function isInteractive() {
|
|
@@ -487,33 +514,6 @@ function malformed(source, detail) {
|
|
|
487
514
|
// src/config/store.ts
|
|
488
515
|
import { chmod, mkdir, rename } from "node:fs/promises";
|
|
489
516
|
|
|
490
|
-
// src/cli/io.ts
|
|
491
|
-
import { access, readFile, writeFile } from "node:fs/promises";
|
|
492
|
-
async function fileExists(path) {
|
|
493
|
-
try {
|
|
494
|
-
await access(path);
|
|
495
|
-
return true;
|
|
496
|
-
} catch {
|
|
497
|
-
return false;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
async function readBytes(path) {
|
|
501
|
-
return new Uint8Array(await readFile(path));
|
|
502
|
-
}
|
|
503
|
-
async function readJson(path) {
|
|
504
|
-
return JSON.parse(await readFile(path, "utf8"));
|
|
505
|
-
}
|
|
506
|
-
async function writeFileContents(path, data) {
|
|
507
|
-
await writeFile(path, data);
|
|
508
|
-
}
|
|
509
|
-
async function readStdin() {
|
|
510
|
-
const chunks = [];
|
|
511
|
-
for await (const chunk of process.stdin) {
|
|
512
|
-
chunks.push(chunk);
|
|
513
|
-
}
|
|
514
|
-
return new Uint8Array(Buffer.concat(chunks));
|
|
515
|
-
}
|
|
516
|
-
|
|
517
517
|
// src/config/paths.ts
|
|
518
518
|
import { homedir } from "node:os";
|
|
519
519
|
import { join } from "node:path";
|
|
@@ -925,7 +925,7 @@ function registerAccountCommands(program) {
|
|
|
925
925
|
account.command("list").description("List imported accounts").action(async (_options, command) => {
|
|
926
926
|
await runAccountList(outputFor(command), globalOptions(command));
|
|
927
927
|
});
|
|
928
|
-
account.command("show").description("Print the active account address").action(async (_options, command) => {
|
|
928
|
+
account.command("show").alias("get").description("Print the active account address").action(async (_options, command) => {
|
|
929
929
|
await runAccountShow(outputFor(command), globalOptions(command));
|
|
930
930
|
});
|
|
931
931
|
account.command("use <address>").description("Set the active account for the current profile").action(async (address, _options, command) => {
|
|
@@ -940,8 +940,15 @@ async function readSecretToImport(env, signal) {
|
|
|
940
940
|
if (raw !== undefined && raw.length > 0) {
|
|
941
941
|
return raw;
|
|
942
942
|
}
|
|
943
|
+
if (!process.stdin.isTTY) {
|
|
944
|
+
const piped = new TextDecoder().decode(await readStdin()).trim();
|
|
945
|
+
if (piped.length > 0) {
|
|
946
|
+
return piped;
|
|
947
|
+
}
|
|
948
|
+
throw new UsageError("Cannot read a private key: pipe it on stdin, set MORSE_PRIVATE_KEY, or run in an interactive terminal.");
|
|
949
|
+
}
|
|
943
950
|
if (!isInteractive()) {
|
|
944
|
-
throw new UsageError("Cannot read a private key: set MORSE_PRIVATE_KEY or run in an interactive terminal.");
|
|
951
|
+
throw new UsageError("Cannot read a private key: pipe it on stdin, set MORSE_PRIVATE_KEY, or run in an interactive terminal.");
|
|
945
952
|
}
|
|
946
953
|
process.stderr.write(`Paste your Sui private key (starts with suiprivkey1...), then press Enter. Input is hidden.
|
|
947
954
|
`);
|
|
@@ -1502,7 +1509,7 @@ function registerCapCommands(program) {
|
|
|
1502
1509
|
publicationOption(destroy).action(async (publisherCapId, options, command) => {
|
|
1503
1510
|
await runCapDestroy(await buildWriteContext(command), publisherCapId, options, globalOptions(command));
|
|
1504
1511
|
});
|
|
1505
|
-
cap.command("transfer <publisherCapId> <recipient>").description("Transfer a PublisherCap object to another address").action(async (publisherCapId, recipient, _options, command) => {
|
|
1512
|
+
cap.command("transfer <publisherCapId> <recipient>").alias("transfer-ownership").description("Transfer a PublisherCap object to another address").action(async (publisherCapId, recipient, _options, command) => {
|
|
1506
1513
|
await runCapTransfer(await buildWriteContext(command), publisherCapId, recipient, globalOptions(command));
|
|
1507
1514
|
});
|
|
1508
1515
|
}
|
|
@@ -1646,7 +1653,7 @@ function registerConfigCommands(program) {
|
|
|
1646
1653
|
config.command("use <name>").description("Set the default profile").action(async (name, _options, command) => {
|
|
1647
1654
|
await runConfigUse(outputFor(command), name);
|
|
1648
1655
|
});
|
|
1649
|
-
config.command("remove <name>").description("Delete a profile").action(async (name, _options, command) => {
|
|
1656
|
+
config.command("remove <name>").alias("delete").description("Delete a profile").action(async (name, _options, command) => {
|
|
1650
1657
|
await runConfigRemove(outputFor(command), name);
|
|
1651
1658
|
});
|
|
1652
1659
|
}
|
|
@@ -1850,6 +1857,12 @@ async function runEntryGet(ctx, entryId, options) {
|
|
|
1850
1857
|
ctx.output.result(renderEntry(result), result);
|
|
1851
1858
|
}
|
|
1852
1859
|
async function runEntryList(ctx, options) {
|
|
1860
|
+
if (options.all) {
|
|
1861
|
+
if (options.limit !== undefined || options.cursor !== undefined) {
|
|
1862
|
+
throw new UsageError("--all cannot be combined with --limit or --cursor.");
|
|
1863
|
+
}
|
|
1864
|
+
return runEntryScan(ctx, options);
|
|
1865
|
+
}
|
|
1853
1866
|
const id = await resolvePublication(ctx, options.publication);
|
|
1854
1867
|
const collection = resolveCollection(ctx, options.collection);
|
|
1855
1868
|
const page = await ctx.reader.listEntries(id, collection, {
|
|
@@ -1961,11 +1974,11 @@ async function runEntryRead(ctx, entryId, revisionId, options) {
|
|
|
1961
1974
|
}
|
|
1962
1975
|
function registerEntryCommands(program) {
|
|
1963
1976
|
const entry = program.command("entry").description("Read, add, and delete entries in a collection");
|
|
1964
|
-
const get = entry.command("get <entryId>").description("Fetch a single entry");
|
|
1977
|
+
const get = entry.command("get <entryId>").alias("show").description("Fetch a single entry");
|
|
1965
1978
|
collectionOption(publicationOption(get)).action(async (entryId, options, command) => {
|
|
1966
1979
|
await runEntryGet(await buildReadContext(command), entryId, options);
|
|
1967
1980
|
});
|
|
1968
|
-
const list = entry.command("list").description("List entries in a collection").option("--limit <n>", "Maximum results per page").option("--cursor <cursor>", "Continue from a previous page cursor").option("--drafts-only", "Show only entries with a pending (unpublished) draft");
|
|
1981
|
+
const list = entry.command("list").description("List entries in a collection").option("--limit <n>", "Maximum results per page").option("--cursor <cursor>", "Continue from a previous page cursor").option("--all", "Fetch every page (same as `entry scan`)").option("--drafts-only", "Show only entries with a pending (unpublished) draft");
|
|
1969
1982
|
collectionOption(publicationOption(list)).action(async (options, command) => {
|
|
1970
1983
|
await runEntryList(await buildReadContext(command), options);
|
|
1971
1984
|
});
|
|
@@ -1981,7 +1994,7 @@ function registerEntryCommands(program) {
|
|
|
1981
1994
|
collectionOption(publicationOption(publisherCapOption(remove))).action(async (entryId, options, command) => {
|
|
1982
1995
|
await runEntryDelete(await buildWriteContext(command), entryId, options, globalOptions(command));
|
|
1983
1996
|
});
|
|
1984
|
-
const read = entry.command("read <entryId> [revisionIndex]").description("Fetch a public entry's content to stdout or a file").option("--out <path>", "Write content to a file instead of stdout");
|
|
1997
|
+
const read = entry.command("read <entryId> [revisionIndex]").alias("download").description("Fetch a public entry's content to stdout or a file").option("--out <path>", "Write content to a file instead of stdout");
|
|
1985
1998
|
collectionOption(publicationOption(viaAggregatorOption(read))).action(async (entryId, revisionId, options, command) => {
|
|
1986
1999
|
await runEntryRead(await buildReadContentContext(command, {
|
|
1987
2000
|
viaAggregator: options.viaAggregator
|
|
@@ -2411,13 +2424,13 @@ function registerFileCommands(program) {
|
|
|
2411
2424
|
indexerUrl: options.indexerUrl
|
|
2412
2425
|
}), options);
|
|
2413
2426
|
});
|
|
2414
|
-
const download = file.command("download [file]").description("Download a file's content; decrypts in place when encrypted").option("--out <path>", "Write content to a file instead of stdout").option("--share <string>", "Share string from upload (bundles file id, prefix, nonce)").option("--prefix <hex>", "Seal prefix (with --nonce) to decrypt").option("--nonce <hex>", "Seal nonce (with --prefix) to decrypt").option("--raw", "Write the raw (still-encrypted) bytes when no decrypt input is given");
|
|
2427
|
+
const download = file.command("download [file]").alias("read").description("Download a file's content; decrypts in place when encrypted").option("--out <path>", "Write content to a file instead of stdout").option("--share <string>", "Share string from upload (bundles file id, prefix, nonce)").option("--prefix <hex>", "Seal prefix (with --nonce) to decrypt").option("--nonce <hex>", "Seal nonce (with --prefix) to decrypt").option("--raw", "Write the raw (still-encrypted) bytes when no decrypt input is given");
|
|
2415
2428
|
viaAggregatorOption(download).action(async (target, options, command) => {
|
|
2416
2429
|
await runFileDownload(await buildFileDownloadContext(command, {
|
|
2417
2430
|
viaAggregator: options.viaAggregator
|
|
2418
2431
|
}), target, options);
|
|
2419
2432
|
});
|
|
2420
|
-
file.command("get <file>").description("Fetch a file's on-chain metadata").action(async (target, _options, command) => {
|
|
2433
|
+
file.command("get <file>").alias("show").description("Fetch a file's on-chain metadata").action(async (target, _options, command) => {
|
|
2421
2434
|
await runFileGet(await buildFilesReadContext(command), target);
|
|
2422
2435
|
});
|
|
2423
2436
|
const register = file.command("register").description("Register on-chain metadata for a blob already on Walrus").requiredOption("--blob-id <id>", "Walrus content id").requiredOption("-n, --name <name>", "File name").requiredOption("--content-type <mime>", "MIME content type").requiredOption("--size <bytes>", "Plaintext byte length").option("--public", "Register a world-readable (unencrypted) file").option("--encrypted", "Register an encrypted file (needs --seal-prefix)").option("--seal-prefix <hex>", "Seal prefix the blob was encrypted under").option("--blob-object-id <id>", "On-chain Walrus Blob object id, if known");
|
|
@@ -2553,7 +2566,7 @@ async function runPublicationTransferOwnership(ctx, recipient, options, gopts) {
|
|
|
2553
2566
|
}
|
|
2554
2567
|
function registerPublicationCommands(program) {
|
|
2555
2568
|
const publication = program.command("publication").alias("pub").description("Work with publications");
|
|
2556
|
-
publication.command("get [publication]").description("Fetch a publication (slug or id; default: the active publication)").action(async (target, _options, command) => {
|
|
2569
|
+
publication.command("get [publication]").alias("show").description("Fetch a publication (slug or id; default: the active publication)").action(async (target, _options, command) => {
|
|
2557
2570
|
await runPublicationGet(await buildReadContext(command), target);
|
|
2558
2571
|
});
|
|
2559
2572
|
publication.command("list [address]").description("List publications owned by an address (default: the active account)").option("--limit <n>", "Maximum results per page").option("--cursor <cursor>", "Continue from a previous page cursor").option("--ids-only", "Skip slug/name resolution (one RPC, no per-publication reads)").action(async (address, options, command) => {
|
package/docs/QUICKSTART.md
CHANGED
|
@@ -43,6 +43,18 @@ Saved profile "testnet" (testnet).
|
|
|
43
43
|
|
|
44
44
|
## 2. Import your key
|
|
45
45
|
|
|
46
|
+
Three ways to give the CLI a key. It is never accepted as a flag, because argv
|
|
47
|
+
is visible in `ps` and shell history.
|
|
48
|
+
|
|
49
|
+
- **Interactive**: run `morse account import` and paste at the hidden prompt.
|
|
50
|
+
- **Piped**: `echo "$KEY" | morse account import`
|
|
51
|
+
- **No keystore at all**: export `MORSE_PRIVATE_KEY` and skip the import; every
|
|
52
|
+
command picks it up.
|
|
53
|
+
|
|
54
|
+
The first two store the key encrypted and need `MORSE_KEYSTORE_PASSWORD` (or an
|
|
55
|
+
interactive password prompt).
|
|
56
|
+
|
|
57
|
+
|
|
46
58
|
`account import` prompts for the secret key and a keystore password, both hidden.
|
|
47
59
|
The key is encrypted at rest (scrypt + AES-256-GCM) and never stored in plaintext.
|
|
48
60
|
|