@arcadiasystems/morse-cli 0.1.0 → 0.3.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/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.1.0",
151
+ version: "0.3.0",
152
152
  description: "Command-line interface for the Morse decentralized CMS on Sui.",
153
153
  license: "MIT",
154
154
  type: "module",
@@ -194,11 +194,16 @@ var package_default = {
194
194
  lint: "biome check .",
195
195
  "lint:fix": "biome check --write .",
196
196
  test: "bun test",
197
+ "test:unit": "bun test test/*.test.ts",
198
+ "test:cli": "bun test test/cli/",
197
199
  "test:coverage": "bun test --coverage",
198
- prepublishOnly: "tsc --noEmit && biome check . && bun test && bun run build"
200
+ "test:e2e": "MORSE_E2E=1 bun test test/e2e/",
201
+ coverage: "bun scripts/coverage-gate.ts",
202
+ check: "tsc --noEmit && biome check . && bun scripts/coverage-gate.ts",
203
+ prepublishOnly: "bun run check && bun run build"
199
204
  },
200
205
  dependencies: {
201
- "@arcadiasystems/morse-sdk": "^0.1.4",
206
+ "@arcadiasystems/morse-sdk": "^0.3.0",
202
207
  "@mysten/seal": "1.1.3",
203
208
  "@mysten/sui": "2.16.2",
204
209
  "@mysten/walrus": "1.1.6",
@@ -313,27 +318,41 @@ class Output {
313
318
  }
314
319
  result(human, data) {
315
320
  if (this.options.json) {
316
- process.stdout.write(`${toJson(data)}
321
+ this.out(`${toJson(data)}
317
322
  `);
318
323
  return;
319
324
  }
320
- process.stdout.write(`${human}
325
+ this.out(`${human}
321
326
  `);
322
327
  }
323
328
  info(message) {
324
329
  if (this.options.quiet || this.options.json) {
325
330
  return;
326
331
  }
327
- process.stderr.write(`${this.paint(message, DIM)}
332
+ this.err(`${this.paint(message, DIM)}
328
333
  `);
329
334
  }
330
335
  warn(message) {
331
336
  if (this.options.quiet || this.options.json) {
332
337
  return;
333
338
  }
334
- process.stderr.write(`${this.paint(message, YELLOW)}
339
+ this.err(`${this.paint(message, YELLOW)}
335
340
  `);
336
341
  }
342
+ out(text) {
343
+ if (this.options.writeOut !== undefined) {
344
+ this.options.writeOut(text);
345
+ return;
346
+ }
347
+ process.stdout.write(text);
348
+ }
349
+ err(text) {
350
+ if (this.options.writeErr !== undefined) {
351
+ this.options.writeErr(text);
352
+ return;
353
+ }
354
+ process.stderr.write(text);
355
+ }
337
356
  paint(text, code) {
338
357
  return this.options.color ? `\x1B[${code}m${text}${RESET}` : text;
339
358
  }
@@ -766,79 +785,85 @@ function accountAddress(account, env = process.env) {
766
785
  }
767
786
 
768
787
  // src/commands/account.ts
788
+ async function runAccountImport(output, gopts, env, signal) {
789
+ const secret = await readSecretToImport(env, signal);
790
+ const password = await resolvePassword("create", env, signal);
791
+ const address = await importKey(secret, password);
792
+ const profileName = await associateAccount(gopts, address);
793
+ output.info(`Imported ${address} into keystore.`);
794
+ output.result(`Imported account ${address} (profile "${profileName}").`, {
795
+ address,
796
+ profile: profileName
797
+ });
798
+ }
799
+ async function runAccountList(output, gopts) {
800
+ const addresses = await listAddresses();
801
+ const active = await resolveActiveAccount(gopts);
802
+ if (addresses.length === 0) {
803
+ output.result("No accounts. Import one with: morse account import", {
804
+ active,
805
+ accounts: []
806
+ });
807
+ return;
808
+ }
809
+ const human = addresses.map((address) => `${address === active ? "*" : " "} ${address}`).join(`
810
+ `);
811
+ output.result(human, { active, accounts: addresses });
812
+ }
813
+ async function runAccountShow(output, gopts) {
814
+ const active = await resolveActiveAccount(gopts);
815
+ if (active === undefined) {
816
+ throw new UsageError("No active account. Import one with `morse account import` or set MORSE_ADDRESS.");
817
+ }
818
+ output.result(active, { address: active });
819
+ }
820
+ async function runAccountUse(output, gopts, address) {
821
+ if (!await hasKeystore(address)) {
822
+ throw new UsageError(`No keystore for ${address}. Import it first with: morse account import`);
823
+ }
824
+ const profileName = await associateAccount(gopts, address);
825
+ output.result(`Active account for "${profileName}" set to ${address}.`, {
826
+ address,
827
+ profile: profileName
828
+ });
829
+ }
830
+ async function runAccountExport(output, gopts, address, env, signal) {
831
+ if (output.isJson) {
832
+ throw new UsageError("account export is not available in --json mode.");
833
+ }
834
+ if (!isInteractive()) {
835
+ throw new UsageError("account export requires an interactive terminal.");
836
+ }
837
+ if (gopts.yes) {
838
+ throw new UsageError("account export does not accept --yes. Confirm interactively.");
839
+ }
840
+ const proceed = await confirm(`Reveal the secret key for ${address}? Anyone who sees it controls the account.`, { signal });
841
+ if (!proceed) {
842
+ cancelled();
843
+ }
844
+ const password = await resolvePassword("unlock", env, signal);
845
+ const secret = await unlockSecret(address, password);
846
+ process.stderr.write(`Warning: secret key follows. Handle it with care.
847
+ `);
848
+ process.stdout.write(`${secret}
849
+ `);
850
+ }
769
851
  function registerAccountCommands(program) {
770
852
  const account = program.command("account").description("Import and manage encrypted signing keys");
771
853
  account.command("import").description("Import a private key into an encrypted keystore").action(async (_options, command) => {
772
- const opts = globalOptions(command);
773
- const output = outputFor(command);
774
- const signal = sigintSignal();
775
- const secret = await readSecretToImport(process.env, signal);
776
- const password = await resolvePassword("create", process.env, signal);
777
- const address = await importKey(secret, password);
778
- const profileName = await associateAccount(opts, address);
779
- output.info(`Imported ${address} into keystore.`);
780
- output.result(`Imported account ${address} (profile "${profileName}").`, {
781
- address,
782
- profile: profileName
783
- });
854
+ await runAccountImport(outputFor(command), globalOptions(command), process.env, sigintSignal());
784
855
  });
785
856
  account.command("list").description("List imported accounts").action(async (_options, command) => {
786
- const output = outputFor(command);
787
- const addresses = await listAddresses();
788
- const active = await resolveActiveAccount(globalOptions(command));
789
- if (addresses.length === 0) {
790
- output.result("No accounts. Import one with: morse account import", {
791
- active,
792
- accounts: []
793
- });
794
- return;
795
- }
796
- const human = addresses.map((address) => `${address === active ? "*" : " "} ${address}`).join(`
797
- `);
798
- output.result(human, { active, accounts: addresses });
857
+ await runAccountList(outputFor(command), globalOptions(command));
799
858
  });
800
859
  account.command("show").description("Print the active account address").action(async (_options, command) => {
801
- const output = outputFor(command);
802
- const active = await resolveActiveAccount(globalOptions(command));
803
- if (active === undefined) {
804
- throw new UsageError("No active account. Import one with `morse account import` or set MORSE_ADDRESS.");
805
- }
806
- output.result(active, { address: active });
860
+ await runAccountShow(outputFor(command), globalOptions(command));
807
861
  });
808
862
  account.command("use <address>").description("Set the active account for the current profile").action(async (address, _options, command) => {
809
- const opts = globalOptions(command);
810
- const output = outputFor(command);
811
- if (!await hasKeystore(address)) {
812
- throw new UsageError(`No keystore for ${address}. Import it first with: morse account import`);
813
- }
814
- const profileName = await associateAccount(opts, address);
815
- output.result(`Active account for "${profileName}" set to ${address}.`, {
816
- address,
817
- profile: profileName
818
- });
863
+ await runAccountUse(outputFor(command), globalOptions(command), address);
819
864
  });
820
865
  account.command("export <address>").description("Print a decrypted secret key (dangerous)").action(async (address, _options, command) => {
821
- const output = outputFor(command);
822
- if (output.isJson) {
823
- throw new UsageError("account export is not available in --json mode.");
824
- }
825
- if (!isInteractive()) {
826
- throw new UsageError("account export requires an interactive terminal.");
827
- }
828
- if (globalOptions(command).yes) {
829
- throw new UsageError("account export does not accept --yes. Confirm interactively.");
830
- }
831
- const signal = sigintSignal();
832
- const proceed = await confirm(`Reveal the secret key for ${address}? Anyone who sees it controls the account.`, { signal });
833
- if (!proceed) {
834
- cancelled();
835
- }
836
- const password = await resolvePassword("unlock", process.env, signal);
837
- const secret = await unlockSecret(address, password);
838
- process.stderr.write(`Warning: secret key follows. Handle it with care.
839
- `);
840
- process.stdout.write(`${secret}
841
- `);
866
+ await runAccountExport(outputFor(command), globalOptions(command), address, process.env, sigintSignal());
842
867
  });
843
868
  }
844
869
  async function readSecretToImport(env, signal) {
@@ -857,33 +882,38 @@ async function readSecretToImport(env, signal) {
857
882
  }
858
883
  return secret;
859
884
  }
860
- async function associateAccount(opts, address) {
861
- return updateActiveProfile(opts, { account: address });
885
+ async function associateAccount(gopts, address) {
886
+ return updateActiveProfile(gopts, { account: address });
862
887
  }
863
- async function resolveActiveAccount(opts) {
864
- return accountAddress(resolveSettings(opts, await loadConfig()).account);
888
+ async function resolveActiveAccount(gopts) {
889
+ return accountAddress(resolveSettings(gopts, await loadConfig()).account);
865
890
  }
866
891
 
867
- // src/commands/cap.ts
892
+ // src/commands/allowlist.ts
868
893
  import {
869
- destroyPublisherCap,
870
- issuePublisherCap,
871
- revokePublisherCap,
872
- toPublisherCapId as toPublisherCapId2,
894
+ addMember,
895
+ createAllowlist,
896
+ deleteAllowlist,
897
+ removeMember,
898
+ toAllowlistId,
873
899
  toSuiAddress as toSuiAddress3,
874
- transferPublisherCap
900
+ transferAllowlistCap
875
901
  } from "@arcadiasystems/morse-sdk";
876
902
 
877
903
  // src/cli/context.ts
878
904
  import {
905
+ buildFilesEventTypes,
879
906
  DefaultSealAdapter,
880
907
  DefaultWalrusReadAdapter,
881
908
  DefaultWalrusWriteAdapter,
909
+ HttpAggregatorReadAdapter,
882
910
  KeypairAdapter,
883
911
  morseConfig,
912
+ RpcFilesReader,
884
913
  RpcPublicationReader
885
914
  } from "@arcadiasystems/morse-sdk";
886
915
  import { SuiGrpcClient } from "@mysten/sui/grpc";
916
+ import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc";
887
917
  async function buildReadContext(command) {
888
918
  const opts = globalOptions(command);
889
919
  const output = outputFor(command);
@@ -920,6 +950,20 @@ async function buildWriteContext(command) {
920
950
  address
921
951
  };
922
952
  }
953
+ async function buildFilesReadContext(command) {
954
+ const base = await buildReadContext(command);
955
+ return {
956
+ ...base,
957
+ filesReader: RpcFilesReader.fromMorseConfig(base.config, base.client)
958
+ };
959
+ }
960
+ async function buildAllowlistWriteContext(command) {
961
+ const base = await buildWriteContext(command);
962
+ return {
963
+ ...base,
964
+ filesReader: RpcFilesReader.fromMorseConfig(base.config, base.client)
965
+ };
966
+ }
923
967
  async function buildContentContext(command) {
924
968
  const { base, keypair, address } = await buildSignedBase(command);
925
969
  const network = base.settings.network;
@@ -935,74 +979,71 @@ async function buildEncryptContext(command) {
935
979
  const seal = DefaultSealAdapter.fromMorseConfig(ctx.config, {}, ctx.client);
936
980
  return { ...ctx, seal };
937
981
  }
938
- async function buildReadContentContext(command) {
982
+ function walrusReadAdapter(base, network, viaAggregator) {
983
+ if (viaAggregator) {
984
+ return HttpAggregatorReadAdapter.fromMorseConfig(base.config, base.client);
985
+ }
986
+ return DefaultWalrusReadAdapter.fromConfig({
987
+ network,
988
+ suiClient: base.client
989
+ });
990
+ }
991
+ async function buildReadContentContext(command, opts = {}) {
939
992
  const base = await buildReadContext(command);
940
993
  const network = base.settings.network;
941
994
  if (network === "localnet") {
942
995
  throw new CliError("Walrus reads are not available on localnet. Use testnet or mainnet.", ExitCode.Usage);
943
996
  }
944
- const walrusRead = DefaultWalrusReadAdapter.fromConfig({
945
- network,
946
- suiClient: base.client
947
- });
948
- return { ...base, walrusRead };
997
+ return {
998
+ ...base,
999
+ walrusRead: walrusReadAdapter(base, network, Boolean(opts.viaAggregator))
1000
+ };
949
1001
  }
950
- async function buildDecryptContext(command) {
1002
+ async function buildDecryptContext(command, opts = {}) {
951
1003
  const { base, keypair, address } = await buildSignedBase(command);
952
1004
  const network = base.settings.network;
953
1005
  if (network === "localnet") {
954
1006
  throw new CliError("Seal decryption is not available on localnet. Use testnet or mainnet.", ExitCode.Usage);
955
1007
  }
956
1008
  const seal = DefaultSealAdapter.fromMorseConfig(base.config, {}, base.client);
957
- const walrusRead = DefaultWalrusReadAdapter.fromConfig({
958
- network,
959
- suiClient: base.client
960
- });
961
- return { ...base, keypair, address, seal, walrusRead };
962
- }
963
-
964
- // src/cli/target.ts
965
- import { toPublicationId } from "@arcadiasystems/morse-sdk";
966
- var OBJECT_ID = /^0x[0-9a-f]{1,64}$/i;
967
- var SLUG_PAGE_LIMIT = 50;
968
- async function resolvePublication(ctx, override) {
969
- const value = override ?? ctx.settings.publication;
970
- if (value === undefined) {
971
- throw new UsageError("No publication selected. Pass --publication <slug|id> or run `morse use <slug|id>`.");
972
- }
973
- if (OBJECT_ID.test(value)) {
974
- return toPublicationId(value.toLowerCase());
975
- }
976
- return resolveSlug(ctx, value);
1009
+ return {
1010
+ ...base,
1011
+ keypair,
1012
+ address,
1013
+ seal,
1014
+ walrusRead: walrusReadAdapter(base, network, Boolean(opts.viaAggregator))
1015
+ };
977
1016
  }
978
- function resolveCollection(ctx, override) {
979
- const value = override ?? ctx.settings.collection;
980
- if (value === undefined) {
981
- throw new UsageError("No collection selected. Pass --collection <name> or run `morse use <slug|id> <collection>`.");
1017
+ async function buildFileDownloadContext(command, opts = {}) {
1018
+ const base = await buildReadContext(command);
1019
+ const network = base.settings.network;
1020
+ if (network === "localnet") {
1021
+ throw new CliError("Walrus reads are not available on localnet. Use testnet or mainnet.", ExitCode.Usage);
982
1022
  }
983
- return value;
1023
+ return {
1024
+ ...base,
1025
+ filesReader: RpcFilesReader.fromMorseConfig(base.config, base.client),
1026
+ walrusRead: walrusReadAdapter(base, network, Boolean(opts.viaAggregator)),
1027
+ seal: DefaultSealAdapter.fromMorseConfig(base.config, {}, base.client),
1028
+ unlockSigner: () => resolveSigner(base.settings.account, process.env, base.signal)
1029
+ };
984
1030
  }
985
- async function resolveSlug(ctx, slug) {
986
- if (ctx.ownerAddress === undefined) {
987
- throw new UsageError(`Cannot resolve the slug "${slug}" without an active account. Pass the publication id, or select an account.`);
1031
+ async function buildFileListContext(command, opts = {}) {
1032
+ const base = await buildReadContext(command);
1033
+ const originPackageId = base.config.filesEventOriginPackageId;
1034
+ if (originPackageId === undefined) {
1035
+ throw new CliError("File listing is unavailable on this network: no filesEventOriginPackageId in the config. Use testnet, or supply a config that sets it.", ExitCode.Usage);
988
1036
  }
989
- ctx.output.info(`Resolving slug "${slug}" among owned publications...`);
990
- let cursor;
991
- do {
992
- const page = await ctx.reader.listPublicationsOwnedBy(ctx.ownerAddress, {
993
- limit: SLUG_PAGE_LIMIT,
994
- signal: ctx.signal,
995
- ...cursor === undefined ? {} : { cursor }
996
- });
997
- for (const owned of page.results) {
998
- const publication = await ctx.reader.getPublication(owned.publicationId, ctx.signal);
999
- if (publication.slug === slug) {
1000
- return publication.id;
1001
- }
1002
- }
1003
- cursor = page.nextCursor ?? undefined;
1004
- } while (cursor !== undefined);
1005
- throw new UsageError(`No publication with slug "${slug}" owned by the active account. Pass the publication id instead.`);
1037
+ const events = new SuiJsonRpcClient({
1038
+ network: base.settings.network,
1039
+ url: opts.indexerUrl ?? base.config.rpcUrl
1040
+ });
1041
+ return {
1042
+ ...base,
1043
+ filesReader: RpcFilesReader.fromMorseConfig(base.config, base.client),
1044
+ events,
1045
+ eventTypes: buildFilesEventTypes(originPackageId)
1046
+ };
1006
1047
  }
1007
1048
 
1008
1049
  // src/format/ids.ts
@@ -1072,6 +1113,52 @@ function renderEntryList(entries) {
1072
1113
  return entries.map((entry) => `#${entry.id} ${entry.name} (${entry.revisions.length} revisions)`).join(`
1073
1114
  `);
1074
1115
  }
1116
+ function renderAllowlist(allowlist) {
1117
+ const members = allowlist.members.length === 0 ? "(none)" : allowlist.members.map((m) => ` ${m}`).join(`
1118
+ `);
1119
+ return [
1120
+ `${allowlist.name} (${shortId(allowlist.id)})`,
1121
+ `members (${allowlist.members.length}):`,
1122
+ members
1123
+ ].join(`
1124
+ `);
1125
+ }
1126
+ function renderAllowlistCapList(caps) {
1127
+ if (caps.length === 0) {
1128
+ return "No allowlist caps held by this address.";
1129
+ }
1130
+ return caps.map((cap) => `${cap.id} allowlist ${cap.allowlistId}`).join(`
1131
+ `);
1132
+ }
1133
+ function renderFileList(items) {
1134
+ if (items.length === 0) {
1135
+ return "No files.";
1136
+ }
1137
+ const header = "id name size encrypted allowlist created";
1138
+ const rows = items.map((file) => {
1139
+ const allowlist = file.allowlistId === null ? "public" : shortId(file.allowlistId);
1140
+ const created = new Date(file.createdAtMs).toISOString().slice(0, 10);
1141
+ const encrypted = file.encrypted ? "yes" : "no";
1142
+ return `${shortId(file.id)} ${file.name} ${file.size} ${encrypted} ${allowlist} ${created}`;
1143
+ });
1144
+ return [header, ...rows].join(`
1145
+ `);
1146
+ }
1147
+ function renderEncryptedFile(file) {
1148
+ const lines = [
1149
+ `${file.name} (${shortId(file.id)})`,
1150
+ `contentType: ${file.contentType}`,
1151
+ `size: ${file.size}`,
1152
+ `encrypted: ${file.encrypted}`,
1153
+ `blobId: ${file.blobId}`,
1154
+ `owner: ${file.owner}`
1155
+ ];
1156
+ if (file.allowlistId !== null) {
1157
+ lines.push(`allowlist: ${file.allowlistId}`);
1158
+ }
1159
+ return lines.join(`
1160
+ `);
1161
+ }
1075
1162
  function headLabel(value) {
1076
1163
  return value === null ? "none" : String(value);
1077
1164
  }
@@ -1094,12 +1181,22 @@ function publisherCapOption(command) {
1094
1181
  function ownerCapOption(command) {
1095
1182
  return command.option("--owner-cap <id>", "OwnerCap ID (auto-resolved if omitted)");
1096
1183
  }
1184
+ function allowlistOption(command) {
1185
+ return command.option("-a, --allowlist <id>", "Allowlist object id");
1186
+ }
1187
+ function allowlistCapOption(command) {
1188
+ return command.option("--cap <id>", "Allowlist admin Cap id (auto-resolved from owned caps if omitted)");
1189
+ }
1190
+ function viaAggregatorOption(command) {
1191
+ return command.option("--via-aggregator", "Fetch content via the Walrus aggregator HTTP service. More reliable when storage nodes are flaky; no client-side blob verification.");
1192
+ }
1097
1193
  function contentOptions(command) {
1098
1194
  return command.option("-f, --file <path>", "File to upload (or - for stdin)").option("--stdin", "Read content from stdin").option("--content-type <type>", "MIME content type (inferred from --file if omitted)").option("--epochs <n>", "Walrus storage epochs", "3");
1099
1195
  }
1100
1196
 
1101
1197
  // src/commands/resolve.ts
1102
1198
  import {
1199
+ toAllowlistCapId,
1103
1200
  toOwnerCapId,
1104
1201
  toPublisherCapId
1105
1202
  } from "@arcadiasystems/morse-sdk";
@@ -1142,6 +1239,25 @@ async function resolvePublisherCap(reader, address, publicationId, override, sig
1142
1239
  } while (cursor !== undefined);
1143
1240
  throw new CliError(`No PublisherCap for ${publicationId} held by ${address}. Pass --publisher-cap, or check that the active account holds one.`, ExitCode.NotFound);
1144
1241
  }
1242
+ async function resolveAllowlistCap(filesReader, address, allowlistId, override, signal) {
1243
+ if (override !== undefined) {
1244
+ return toAllowlistCapId(override);
1245
+ }
1246
+ let cursor;
1247
+ do {
1248
+ const page = await filesReader.listAllowlistCapsOwnedBy(address, {
1249
+ limit: PAGE_LIMIT,
1250
+ signal,
1251
+ ...cursor === undefined ? {} : { cursor }
1252
+ });
1253
+ const match = page.results.find((c) => c.allowlistId === allowlistId);
1254
+ if (match !== undefined) {
1255
+ return match.id;
1256
+ }
1257
+ cursor = page.nextCursor ?? undefined;
1258
+ } while (cursor !== undefined);
1259
+ throw new CliError(`No allowlist Cap for ${allowlistId} held by ${address}. Pass --cap, or check that the active account administers it.`, ExitCode.NotFound);
1260
+ }
1145
1261
 
1146
1262
  // src/commands/shared.ts
1147
1263
  import { StorageMode } from "@arcadiasystems/morse-sdk";
@@ -1162,6 +1278,12 @@ function parsePositiveInt(value, name) {
1162
1278
  function parseLimit(value) {
1163
1279
  return parsePositiveInt(value, "--limit");
1164
1280
  }
1281
+ function parseByteSize(value, name) {
1282
+ if (!/^\d+$/.test(value)) {
1283
+ throw new UsageError(`${name} must be a non-negative integer, got "${value}".`);
1284
+ }
1285
+ return BigInt(value);
1286
+ }
1165
1287
  function parseId(value, name) {
1166
1288
  const id = Number(value);
1167
1289
  if (!Number.isInteger(id) || id < 0) {
@@ -1170,219 +1292,428 @@ function parseId(value, name) {
1170
1292
  return id;
1171
1293
  }
1172
1294
 
1173
- // src/commands/cap.ts
1174
- function registerCapCommands(program) {
1175
- const cap = program.command("cap").description("Manage PublisherCaps (write-access capabilities)");
1176
- cap.command("list [address]").description("List publisher caps held by an address (default: the active account)").option("--limit <n>", "Maximum results per page").option("--cursor <cursor>", "Continue from a previous page cursor").action(async (address, options, command) => {
1177
- const ctx = await buildReadContext(command);
1178
- const holder = address === undefined ? ctx.ownerAddress : toSuiAddress3(address);
1179
- if (holder === undefined) {
1180
- throw new UsageError("No address given and no active account. Pass an address or import an account.");
1181
- }
1182
- const page = await ctx.reader.listPublisherCapsOwnedBy(holder, {
1183
- signal: ctx.signal,
1184
- ...options.limit === undefined ? {} : { limit: parseLimit(options.limit) },
1185
- ...options.cursor === undefined ? {} : { cursor: options.cursor }
1186
- });
1187
- if (page.nextCursor !== null) {
1188
- ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
1189
- }
1190
- ctx.output.result(renderPublisherCapList(page.results), {
1191
- results: page.results,
1192
- nextCursor: page.nextCursor
1193
- });
1295
+ // src/commands/allowlist.ts
1296
+ function requireAllowlistId(value) {
1297
+ if (value === undefined) {
1298
+ throw new UsageError("Pass --allowlist <id>.");
1299
+ }
1300
+ return toAllowlistId(value);
1301
+ }
1302
+ async function runAllowlistCreate(ctx, options) {
1303
+ ctx.output.info(`Creating allowlist "${options.name}"...`);
1304
+ const result = await createAllowlist(ctx.adapter, ctx.config, {
1305
+ name: options.name,
1306
+ signal: ctx.signal
1194
1307
  });
1195
- const issue = cap.command("issue <holder>").description("Issue a PublisherCap bound to an address");
1196
- publicationOption(ownerCapOption(issue)).action(async (holder, options, command) => {
1197
- const ctx = await buildWriteContext(command);
1198
- const id = await resolvePublication(ctx, options.publication);
1199
- const holderAddress = toSuiAddress3(holder);
1200
- ctx.output.info("Resolving OwnerCap...");
1201
- const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
1202
- const result = await issuePublisherCap(ctx.adapter, ctx.config, {
1203
- publicationId: id,
1204
- ownerCapId,
1205
- holder: holderAddress,
1206
- signal: ctx.signal
1207
- });
1208
- ctx.output.result(`Issued PublisherCap ${result.publisherCapId} to ${holderAddress}. (tx: ${result.digest})`, result);
1308
+ const human = [
1309
+ `Created allowlist "${options.name}" (${result.allowlistId})`,
1310
+ ` cap: ${result.capId}`,
1311
+ ` tx: ${result.digest}`,
1312
+ "Save the cap id: it is the admin token for managing members."
1313
+ ].join(`
1314
+ `);
1315
+ ctx.output.result(human, result);
1316
+ }
1317
+ async function runAllowlistAddMember(ctx, member, options) {
1318
+ const allowlistId = requireAllowlistId(options.allowlist);
1319
+ const memberAddress = toSuiAddress3(member);
1320
+ const capId = await resolveAllowlistCap(ctx.filesReader, ctx.address, allowlistId, options.cap, ctx.signal);
1321
+ const result = await addMember(ctx.adapter, ctx.config, {
1322
+ allowlistId,
1323
+ capId,
1324
+ member: memberAddress,
1325
+ signal: ctx.signal
1209
1326
  });
1210
- const revoke = cap.command("revoke <publisherCapId>").description("Revoke a PublisherCap so it can no longer write");
1211
- publicationOption(ownerCapOption(revoke)).action(async (publisherCapId, options, command) => {
1212
- const ctx = await buildWriteContext(command);
1213
- const id = await resolvePublication(ctx, options.publication);
1214
- const capId = toPublisherCapId2(publisherCapId);
1215
- const proceed = await confirm(`Revoke PublisherCap ${capId}? It can no longer be used to write.`, { assumeYes: Boolean(globalOptions(command).yes), signal: ctx.signal });
1216
- if (!proceed) {
1217
- cancelled();
1218
- }
1219
- ctx.output.info("Resolving OwnerCap...");
1220
- const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
1221
- const result = await revokePublisherCap(ctx.adapter, ctx.config, {
1222
- publicationId: id,
1223
- ownerCapId,
1224
- publisherCapId: capId,
1225
- signal: ctx.signal
1226
- });
1227
- ctx.output.result(`Revoked PublisherCap ${capId}. (tx: ${result.digest})`, result);
1327
+ ctx.output.result(`Added ${memberAddress} to allowlist ${shortId(allowlistId)}. (tx: ${result.digest})`, result);
1328
+ }
1329
+ async function runAllowlistRemoveMember(ctx, member, options) {
1330
+ const allowlistId = requireAllowlistId(options.allowlist);
1331
+ const memberAddress = toSuiAddress3(member);
1332
+ const capId = await resolveAllowlistCap(ctx.filesReader, ctx.address, allowlistId, options.cap, ctx.signal);
1333
+ const result = await removeMember(ctx.adapter, ctx.config, {
1334
+ allowlistId,
1335
+ capId,
1336
+ member: memberAddress,
1337
+ signal: ctx.signal
1228
1338
  });
1229
- const destroy = cap.command("destroy <publisherCapId>").description("Destroy a PublisherCap held by the active account");
1230
- publicationOption(destroy).action(async (publisherCapId, options, command) => {
1231
- const ctx = await buildWriteContext(command);
1232
- const id = await resolvePublication(ctx, options.publication);
1233
- const capId = toPublisherCapId2(publisherCapId);
1234
- const proceed = await confirm(`Destroy PublisherCap ${capId}? This is permanent.`, { assumeYes: Boolean(globalOptions(command).yes), signal: ctx.signal });
1235
- if (!proceed) {
1236
- cancelled();
1237
- }
1238
- const result = await destroyPublisherCap(ctx.adapter, ctx.config, {
1239
- publicationId: id,
1240
- publisherCapId: capId,
1241
- signal: ctx.signal
1242
- });
1243
- ctx.output.result(`Destroyed PublisherCap ${capId}. (tx: ${result.digest})`, result);
1339
+ ctx.output.result(`Removed ${memberAddress} from allowlist ${shortId(allowlistId)}. (tx: ${result.digest})`, result);
1340
+ }
1341
+ async function runAllowlistTransferCap(ctx, recipient, options, gopts) {
1342
+ const allowlistId = requireAllowlistId(options.allowlist);
1343
+ const to = toSuiAddress3(recipient);
1344
+ const proceed = await confirm(`Transfer admin of allowlist ${shortId(allowlistId)} to ${to}? You will lose member-management rights.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
1345
+ if (!proceed) {
1346
+ cancelled();
1347
+ }
1348
+ ctx.output.info("Resolving allowlist Cap...");
1349
+ const capId = await resolveAllowlistCap(ctx.filesReader, ctx.address, allowlistId, options.cap, ctx.signal);
1350
+ const result = await transferAllowlistCap(ctx.adapter, ctx.config, {
1351
+ capId,
1352
+ recipient: to,
1353
+ signal: ctx.signal
1244
1354
  });
1245
- cap.command("transfer <publisherCapId> <recipient>").description("Transfer a PublisherCap object to another address").action(async (publisherCapId, recipient, _options, command) => {
1246
- const ctx = await buildWriteContext(command);
1247
- const capId = toPublisherCapId2(publisherCapId);
1248
- const to = toSuiAddress3(recipient);
1249
- const proceed = await confirm(`Transfer PublisherCap ${capId} to ${to}?`, {
1250
- assumeYes: Boolean(globalOptions(command).yes),
1251
- signal: ctx.signal
1252
- });
1253
- if (!proceed) {
1254
- cancelled();
1255
- }
1256
- const result = await transferPublisherCap(ctx.adapter, ctx.config, {
1257
- publisherCapId: capId,
1258
- recipient: to,
1259
- signal: ctx.signal
1260
- });
1261
- ctx.output.result(`Transferred PublisherCap ${capId} to ${to}. (tx: ${result.digest})`, result);
1355
+ ctx.output.result(`Transferred allowlist admin to ${to}. (tx: ${result.digest})`, result);
1356
+ }
1357
+ async function runAllowlistDelete(ctx, options, gopts) {
1358
+ const allowlistId = requireAllowlistId(options.allowlist);
1359
+ const proceed = await confirm(`Delete allowlist ${shortId(allowlistId)}? Files gated by it become permanently undecryptable.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
1360
+ if (!proceed) {
1361
+ cancelled();
1362
+ }
1363
+ ctx.output.info("Resolving allowlist Cap...");
1364
+ const capId = await resolveAllowlistCap(ctx.filesReader, ctx.address, allowlistId, options.cap, ctx.signal);
1365
+ const result = await deleteAllowlist(ctx.adapter, ctx.config, {
1366
+ allowlistId,
1367
+ capId,
1368
+ signal: ctx.signal
1262
1369
  });
1370
+ ctx.output.result(`Deleted allowlist ${allowlistId}. (tx: ${result.digest})`, result);
1263
1371
  }
1264
-
1265
- // src/commands/collection.ts
1266
- import { createCollection, deleteCollection } from "@arcadiasystems/morse-sdk";
1267
- function registerCollectionCommands(program) {
1268
- const collection = program.command("collection").description("Manage collections within a publication");
1269
- const list = collection.command("list").description("List the collections in a publication");
1270
- publicationOption(list).action(async (options, command) => {
1271
- const ctx = await buildReadContext(command);
1272
- const id = await resolvePublication(ctx, options.publication);
1273
- const publication = await ctx.reader.getPublication(id, ctx.signal);
1274
- ctx.output.result(renderCollectionList(publication.collections), {
1275
- publication: id,
1276
- collections: publication.collections
1277
- });
1278
- });
1279
- const create = collection.command("create <name>").description("Create a collection and select it as the active collection").option("--mode <mode>", "Storage mode: blob or quilt", "blob");
1280
- publicationOption(publisherCapOption(create)).action(async (name, options, command) => {
1281
- const ctx = await buildWriteContext(command);
1282
- const id = await resolvePublication(ctx, options.publication);
1283
- const storageMode = coerceStorageMode(options.mode);
1284
- const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
1285
- ctx.output.info(`Creating collection "${name}" (${storageMode})...`);
1286
- const result = await createCollection(ctx.adapter, ctx.config, {
1287
- publicationId: id,
1288
- publisherCapId,
1289
- name,
1290
- storageMode,
1291
- signal: ctx.signal
1292
- });
1293
- await updateActiveProfile(globalOptions(command), { collection: name });
1294
- ctx.output.result(`Created collection "${name}". Selected as the active collection. (tx: ${result.digest})`, result);
1372
+ async function runAllowlistGet(ctx, target) {
1373
+ const result = await ctx.filesReader.getAllowlist(toAllowlistId(target), ctx.signal);
1374
+ ctx.output.result(renderAllowlist(result), result);
1375
+ }
1376
+ async function runAllowlistListCaps(ctx, address, options) {
1377
+ const holder = address === undefined ? ctx.ownerAddress : toSuiAddress3(address);
1378
+ if (holder === undefined) {
1379
+ throw new UsageError("No address given and no active account. Pass an address or import an account.");
1380
+ }
1381
+ const page = await ctx.filesReader.listAllowlistCapsOwnedBy(holder, {
1382
+ signal: ctx.signal,
1383
+ ...options.limit === undefined ? {} : { limit: parseLimit(options.limit) },
1384
+ ...options.cursor === undefined ? {} : { cursor: options.cursor }
1295
1385
  });
1296
- const remove = collection.command("delete <name>").description("Delete an empty collection");
1297
- publicationOption(publisherCapOption(remove)).action(async (name, options, command) => {
1298
- const ctx = await buildWriteContext(command);
1299
- const id = await resolvePublication(ctx, options.publication);
1300
- const proceed = await confirm(`Delete collection "${name}" from ${shortId(id)}? It must be empty.`, { assumeYes: Boolean(globalOptions(command).yes), signal: ctx.signal });
1301
- if (!proceed) {
1302
- cancelled();
1303
- }
1304
- const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
1305
- const result = await deleteCollection(ctx.adapter, ctx.config, {
1306
- publicationId: id,
1307
- publisherCapId,
1308
- name,
1309
- signal: ctx.signal
1310
- });
1311
- if (ctx.settings.collection === name) {
1312
- await updateActiveProfile(globalOptions(command), {
1313
- collection: undefined
1314
- });
1315
- }
1316
- ctx.output.result(`Deleted collection "${name}". (tx: ${result.digest})`, result);
1386
+ if (page.nextCursor !== null) {
1387
+ ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
1388
+ }
1389
+ ctx.output.result(renderAllowlistCapList(page.results), {
1390
+ results: page.results,
1391
+ nextCursor: page.nextCursor
1317
1392
  });
1318
1393
  }
1319
-
1320
- // src/commands/config.ts
1321
- function registerConfigCommands(program) {
1322
- const config = program.command("config").description("Manage profiles and CLI configuration");
1323
- config.command("path").description("Print the config file path").action((_options, command) => {
1324
- const path = configFilePath();
1325
- outputFor(command).result(path, { path });
1394
+ function registerAllowlistCommands(program) {
1395
+ const allowlist = program.command("allowlist").description("Manage allowlists that gate decryption of encrypted files");
1396
+ allowlist.command("create").description("Create an allowlist and transfer its admin cap to you").requiredOption("-n, --name <name>", "Allowlist name").action(async (options, command) => {
1397
+ await runAllowlistCreate(await buildWriteContext(command), options);
1326
1398
  });
1327
- config.command("list").description("List profiles and show the default").action(async (_options, command) => {
1328
- const cfg = await loadConfig();
1329
- outputFor(command).result(renderProfiles(cfg), {
1330
- defaultProfile: cfg.defaultProfile,
1331
- profiles: cfg.profiles
1332
- });
1399
+ const addMemberCmd = allowlist.command("add-member <member>").description("Add a wallet address to an allowlist");
1400
+ allowlistCapOption(allowlistOption(addMemberCmd)).action(async (member, options, command) => {
1401
+ await runAllowlistAddMember(await buildAllowlistWriteContext(command), member, options);
1333
1402
  });
1334
- 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) => {
1335
- const network = coerceNetwork(options.network);
1336
- const cfg = await loadConfig();
1337
- const profiles = {
1338
- ...cfg.profiles,
1339
- [name]: {
1340
- network,
1341
- ...options.rpc === undefined ? {} : { rpc: options.rpc }
1342
- }
1343
- };
1344
- const defaultProfile = Object.keys(cfg.profiles).length === 0 ? name : cfg.defaultProfile;
1345
- await saveConfig({ ...cfg, profiles, defaultProfile });
1346
- outputFor(command).result(`Saved profile "${name}" (${network}).`, {
1347
- profile: name,
1348
- network,
1349
- rpc: options.rpc,
1350
- default: defaultProfile === name
1351
- });
1403
+ const removeMemberCmd = allowlist.command("remove-member <member>").description("Remove a wallet address from an allowlist");
1404
+ allowlistCapOption(allowlistOption(removeMemberCmd)).action(async (member, options, command) => {
1405
+ await runAllowlistRemoveMember(await buildAllowlistWriteContext(command), member, options);
1352
1406
  });
1353
- config.command("use <name>").description("Set the default profile").action(async (name, _options, command) => {
1354
- const cfg = await loadConfig();
1355
- requireProfile(cfg, name);
1356
- await saveConfig({ ...cfg, defaultProfile: name });
1357
- outputFor(command).result(`Default profile set to "${name}".`, {
1358
- defaultProfile: name
1359
- });
1407
+ const transferCmd = allowlist.command("transfer-cap <recipient>").description("Transfer allowlist admin rights to another address");
1408
+ allowlistCapOption(allowlistOption(transferCmd)).action(async (recipient, options, command) => {
1409
+ await runAllowlistTransferCap(await buildAllowlistWriteContext(command), recipient, options, globalOptions(command));
1360
1410
  });
1361
- config.command("remove <name>").description("Delete a profile").action(async (name, _options, command) => {
1362
- const cfg = await loadConfig();
1363
- requireProfile(cfg, name);
1364
- const { [name]: _removed, ...rest } = cfg.profiles;
1365
- const defaultProfile = cfg.defaultProfile === name ? Object.keys(rest)[0] ?? "default" : cfg.defaultProfile;
1366
- await saveConfig({ ...cfg, profiles: rest, defaultProfile });
1367
- outputFor(command).result(`Removed profile "${name}".`, {
1368
- removed: name,
1369
- defaultProfile,
1370
- profiles: Object.keys(rest)
1371
- });
1411
+ const deleteCmd = allowlist.command("delete").description("Delete an allowlist (dependent files become undecryptable)");
1412
+ allowlistCapOption(allowlistOption(deleteCmd)).action(async (options, command) => {
1413
+ await runAllowlistDelete(await buildAllowlistWriteContext(command), options, globalOptions(command));
1414
+ });
1415
+ allowlist.command("get <allowlist>").description("Fetch an allowlist's name and members").action(async (target, _options, command) => {
1416
+ await runAllowlistGet(await buildFilesReadContext(command), target);
1417
+ });
1418
+ allowlist.command("list-caps [address]").description("List allowlist admin caps held by an address (default: the active account)").option("--limit <n>", "Maximum results per page").option("--cursor <cursor>", "Continue from a previous page cursor").action(async (address, options, command) => {
1419
+ await runAllowlistListCaps(await buildFilesReadContext(command), address, options);
1372
1420
  });
1373
1421
  }
1374
- function requireProfile(config, name) {
1375
- if (!(name in config.profiles)) {
1376
- throw new UsageError(`No profile named "${name}". Create it with: morse config add ${name} --network testnet`);
1377
- }
1378
- }
1379
- function renderProfiles(config) {
1380
- const names = Object.keys(config.profiles);
1381
- if (names.length === 0) {
1382
- return "No profiles configured. Add one with: morse config add <name> --network testnet";
1383
- }
1384
- return names.map((name) => {
1385
- const profile = config.profiles[name];
1422
+
1423
+ // src/commands/cap.ts
1424
+ import {
1425
+ destroyPublisherCap,
1426
+ issuePublisherCap,
1427
+ revokePublisherCap,
1428
+ toPublisherCapId as toPublisherCapId2,
1429
+ toSuiAddress as toSuiAddress4,
1430
+ transferPublisherCap
1431
+ } from "@arcadiasystems/morse-sdk";
1432
+
1433
+ // src/cli/target.ts
1434
+ import { toPublicationId } from "@arcadiasystems/morse-sdk";
1435
+ var OBJECT_ID = /^0x[0-9a-f]{1,64}$/i;
1436
+ var SLUG_PAGE_LIMIT = 50;
1437
+ async function resolvePublication(ctx, override) {
1438
+ const value = override ?? ctx.settings.publication;
1439
+ if (value === undefined) {
1440
+ throw new UsageError("No publication selected. Pass --publication <slug|id> or run `morse use <slug|id>`.");
1441
+ }
1442
+ if (OBJECT_ID.test(value)) {
1443
+ return toPublicationId(value.toLowerCase());
1444
+ }
1445
+ return resolveSlug(ctx, value);
1446
+ }
1447
+ function resolveCollection(ctx, override) {
1448
+ const value = override ?? ctx.settings.collection;
1449
+ if (value === undefined) {
1450
+ throw new UsageError("No collection selected. Pass --collection <name> or run `morse use <slug|id> <collection>`.");
1451
+ }
1452
+ return value;
1453
+ }
1454
+ async function resolveSlug(ctx, slug) {
1455
+ if (ctx.ownerAddress === undefined) {
1456
+ throw new UsageError(`Cannot resolve the slug "${slug}" without an active account. Pass the publication id, or select an account.`);
1457
+ }
1458
+ ctx.output.info(`Resolving slug "${slug}" among owned publications...`);
1459
+ let cursor;
1460
+ do {
1461
+ const page = await ctx.reader.listPublicationsOwnedBy(ctx.ownerAddress, {
1462
+ limit: SLUG_PAGE_LIMIT,
1463
+ signal: ctx.signal,
1464
+ ...cursor === undefined ? {} : { cursor }
1465
+ });
1466
+ for (const owned of page.results) {
1467
+ const publication = await ctx.reader.getPublication(owned.publicationId, ctx.signal);
1468
+ if (publication.slug === slug) {
1469
+ return publication.id;
1470
+ }
1471
+ }
1472
+ cursor = page.nextCursor ?? undefined;
1473
+ } while (cursor !== undefined);
1474
+ throw new UsageError(`No publication with slug "${slug}" owned by the active account. Pass the publication id instead.`);
1475
+ }
1476
+
1477
+ // src/commands/cap.ts
1478
+ async function runCapList(ctx, address, options) {
1479
+ const holder = address === undefined ? ctx.ownerAddress : toSuiAddress4(address);
1480
+ if (holder === undefined) {
1481
+ throw new UsageError("No address given and no active account. Pass an address or import an account.");
1482
+ }
1483
+ const page = await ctx.reader.listPublisherCapsOwnedBy(holder, {
1484
+ signal: ctx.signal,
1485
+ ...options.limit === undefined ? {} : { limit: parseLimit(options.limit) },
1486
+ ...options.cursor === undefined ? {} : { cursor: options.cursor }
1487
+ });
1488
+ if (page.nextCursor !== null) {
1489
+ ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
1490
+ }
1491
+ ctx.output.result(renderPublisherCapList(page.results), {
1492
+ results: page.results,
1493
+ nextCursor: page.nextCursor
1494
+ });
1495
+ }
1496
+ async function runCapIssue(ctx, holder, options) {
1497
+ const id = await resolvePublication(ctx, options.publication);
1498
+ const holderAddress = toSuiAddress4(holder);
1499
+ ctx.output.info("Resolving OwnerCap...");
1500
+ const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
1501
+ const result = await issuePublisherCap(ctx.adapter, ctx.config, {
1502
+ publicationId: id,
1503
+ ownerCapId,
1504
+ holder: holderAddress,
1505
+ signal: ctx.signal
1506
+ });
1507
+ ctx.output.result(`Issued PublisherCap ${result.publisherCapId} to ${holderAddress}. (tx: ${result.digest})`, result);
1508
+ }
1509
+ async function runCapRevoke(ctx, publisherCapId, options, gopts) {
1510
+ const id = await resolvePublication(ctx, options.publication);
1511
+ const capId = toPublisherCapId2(publisherCapId);
1512
+ const proceed = await confirm(`Revoke PublisherCap ${capId}? It can no longer be used to write.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
1513
+ if (!proceed) {
1514
+ cancelled();
1515
+ }
1516
+ ctx.output.info("Resolving OwnerCap...");
1517
+ const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
1518
+ const result = await revokePublisherCap(ctx.adapter, ctx.config, {
1519
+ publicationId: id,
1520
+ ownerCapId,
1521
+ publisherCapId: capId,
1522
+ signal: ctx.signal
1523
+ });
1524
+ ctx.output.result(`Revoked PublisherCap ${capId}. (tx: ${result.digest})`, result);
1525
+ }
1526
+ async function runCapDestroy(ctx, publisherCapId, options, gopts) {
1527
+ const id = await resolvePublication(ctx, options.publication);
1528
+ const capId = toPublisherCapId2(publisherCapId);
1529
+ const proceed = await confirm(`Destroy PublisherCap ${capId}? This is permanent.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
1530
+ if (!proceed) {
1531
+ cancelled();
1532
+ }
1533
+ const result = await destroyPublisherCap(ctx.adapter, ctx.config, {
1534
+ publicationId: id,
1535
+ publisherCapId: capId,
1536
+ signal: ctx.signal
1537
+ });
1538
+ ctx.output.result(`Destroyed PublisherCap ${capId}. (tx: ${result.digest})`, result);
1539
+ }
1540
+ async function runCapTransfer(ctx, publisherCapId, recipient, gopts) {
1541
+ const capId = toPublisherCapId2(publisherCapId);
1542
+ const to = toSuiAddress4(recipient);
1543
+ const proceed = await confirm(`Transfer PublisherCap ${capId} to ${to}?`, {
1544
+ assumeYes: Boolean(gopts.yes),
1545
+ signal: ctx.signal
1546
+ });
1547
+ if (!proceed) {
1548
+ cancelled();
1549
+ }
1550
+ const result = await transferPublisherCap(ctx.adapter, ctx.config, {
1551
+ publisherCapId: capId,
1552
+ recipient: to,
1553
+ signal: ctx.signal
1554
+ });
1555
+ ctx.output.result(`Transferred PublisherCap ${capId} to ${to}. (tx: ${result.digest})`, result);
1556
+ }
1557
+ function registerCapCommands(program) {
1558
+ const cap = program.command("cap").description("Manage PublisherCaps (write-access capabilities)");
1559
+ cap.command("list [address]").description("List publisher caps held by an address (default: the active account)").option("--limit <n>", "Maximum results per page").option("--cursor <cursor>", "Continue from a previous page cursor").action(async (address, options, command) => {
1560
+ await runCapList(await buildReadContext(command), address, options);
1561
+ });
1562
+ const issue = cap.command("issue <holder>").description("Issue a PublisherCap bound to an address");
1563
+ publicationOption(ownerCapOption(issue)).action(async (holder, options, command) => {
1564
+ await runCapIssue(await buildWriteContext(command), holder, options);
1565
+ });
1566
+ const revoke = cap.command("revoke <publisherCapId>").description("Revoke a PublisherCap so it can no longer write");
1567
+ publicationOption(ownerCapOption(revoke)).action(async (publisherCapId, options, command) => {
1568
+ await runCapRevoke(await buildWriteContext(command), publisherCapId, options, globalOptions(command));
1569
+ });
1570
+ const destroy = cap.command("destroy <publisherCapId>").description("Destroy a PublisherCap held by the active account");
1571
+ publicationOption(destroy).action(async (publisherCapId, options, command) => {
1572
+ await runCapDestroy(await buildWriteContext(command), publisherCapId, options, globalOptions(command));
1573
+ });
1574
+ cap.command("transfer <publisherCapId> <recipient>").description("Transfer a PublisherCap object to another address").action(async (publisherCapId, recipient, _options, command) => {
1575
+ await runCapTransfer(await buildWriteContext(command), publisherCapId, recipient, globalOptions(command));
1576
+ });
1577
+ }
1578
+
1579
+ // src/commands/collection.ts
1580
+ import { createCollection, deleteCollection } from "@arcadiasystems/morse-sdk";
1581
+ async function runCollectionList(ctx, options) {
1582
+ const id = await resolvePublication(ctx, options.publication);
1583
+ const publication = await ctx.reader.getPublication(id, ctx.signal);
1584
+ ctx.output.result(renderCollectionList(publication.collections), {
1585
+ publication: id,
1586
+ collections: publication.collections
1587
+ });
1588
+ }
1589
+ async function runCollectionCreate(ctx, name, options, gopts) {
1590
+ const id = await resolvePublication(ctx, options.publication);
1591
+ const storageMode = coerceStorageMode(options.mode);
1592
+ const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
1593
+ ctx.output.info(`Creating collection "${name}" (${storageMode})...`);
1594
+ const result = await createCollection(ctx.adapter, ctx.config, {
1595
+ publicationId: id,
1596
+ publisherCapId,
1597
+ name,
1598
+ storageMode,
1599
+ signal: ctx.signal
1600
+ });
1601
+ await updateActiveProfile(gopts, { collection: name });
1602
+ ctx.output.result(`Created collection "${name}". Selected as the active collection. (tx: ${result.digest})`, result);
1603
+ }
1604
+ async function runCollectionDelete(ctx, name, options, gopts) {
1605
+ const id = await resolvePublication(ctx, options.publication);
1606
+ const proceed = await confirm(`Delete collection "${name}" from ${shortId(id)}? It must be empty.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
1607
+ if (!proceed) {
1608
+ cancelled();
1609
+ }
1610
+ const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
1611
+ const result = await deleteCollection(ctx.adapter, ctx.config, {
1612
+ publicationId: id,
1613
+ publisherCapId,
1614
+ name,
1615
+ signal: ctx.signal
1616
+ });
1617
+ if (ctx.settings.collection === name) {
1618
+ await updateActiveProfile(gopts, { collection: undefined });
1619
+ }
1620
+ ctx.output.result(`Deleted collection "${name}". (tx: ${result.digest})`, result);
1621
+ }
1622
+ function registerCollectionCommands(program) {
1623
+ const collection = program.command("collection").description("Manage collections within a publication");
1624
+ const list = collection.command("list").description("List the collections in a publication");
1625
+ publicationOption(list).action(async (options, command) => {
1626
+ await runCollectionList(await buildReadContext(command), options);
1627
+ });
1628
+ const create = collection.command("create <name>").description("Create a collection and select it as the active collection").option("--mode <mode>", "Storage mode: blob or quilt", "blob");
1629
+ publicationOption(publisherCapOption(create)).action(async (name, options, command) => {
1630
+ await runCollectionCreate(await buildWriteContext(command), name, options, globalOptions(command));
1631
+ });
1632
+ const remove = collection.command("delete <name>").description("Delete an empty collection");
1633
+ publicationOption(publisherCapOption(remove)).action(async (name, options, command) => {
1634
+ await runCollectionDelete(await buildWriteContext(command), name, options, globalOptions(command));
1635
+ });
1636
+ }
1637
+
1638
+ // src/commands/config.ts
1639
+ function runConfigPath(output) {
1640
+ const path = configFilePath();
1641
+ output.result(path, { path });
1642
+ }
1643
+ async function runConfigList(output) {
1644
+ const cfg = await loadConfig();
1645
+ output.result(renderProfiles(cfg), {
1646
+ defaultProfile: cfg.defaultProfile,
1647
+ profiles: cfg.profiles
1648
+ });
1649
+ }
1650
+ async function runConfigAdd(output, name, options) {
1651
+ const network = coerceNetwork(options.network);
1652
+ const cfg = await loadConfig();
1653
+ const profiles = {
1654
+ ...cfg.profiles,
1655
+ [name]: {
1656
+ network,
1657
+ ...options.rpc === undefined ? {} : { rpc: options.rpc }
1658
+ }
1659
+ };
1660
+ const defaultProfile = Object.keys(cfg.profiles).length === 0 ? name : cfg.defaultProfile;
1661
+ await saveConfig({ ...cfg, profiles, defaultProfile });
1662
+ output.result(`Saved profile "${name}" (${network}).`, {
1663
+ profile: name,
1664
+ network,
1665
+ rpc: options.rpc,
1666
+ default: defaultProfile === name
1667
+ });
1668
+ }
1669
+ async function runConfigUse(output, name) {
1670
+ const cfg = await loadConfig();
1671
+ requireProfile(cfg, name);
1672
+ await saveConfig({ ...cfg, defaultProfile: name });
1673
+ output.result(`Default profile set to "${name}".`, { defaultProfile: name });
1674
+ }
1675
+ async function runConfigRemove(output, name) {
1676
+ const cfg = await loadConfig();
1677
+ requireProfile(cfg, name);
1678
+ const { [name]: _removed, ...rest } = cfg.profiles;
1679
+ const defaultProfile = cfg.defaultProfile === name ? Object.keys(rest)[0] ?? "default" : cfg.defaultProfile;
1680
+ await saveConfig({ ...cfg, profiles: rest, defaultProfile });
1681
+ output.result(`Removed profile "${name}".`, {
1682
+ removed: name,
1683
+ defaultProfile,
1684
+ profiles: Object.keys(rest)
1685
+ });
1686
+ }
1687
+ function registerConfigCommands(program) {
1688
+ const config = program.command("config").description("Manage profiles and CLI configuration");
1689
+ config.command("path").description("Print the config file path").action((_options, command) => {
1690
+ runConfigPath(outputFor(command));
1691
+ });
1692
+ config.command("list").description("List profiles and show the default").action(async (_options, command) => {
1693
+ await runConfigList(outputFor(command));
1694
+ });
1695
+ 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) => {
1696
+ await runConfigAdd(outputFor(command), name, options);
1697
+ });
1698
+ config.command("use <name>").description("Set the default profile").action(async (name, _options, command) => {
1699
+ await runConfigUse(outputFor(command), name);
1700
+ });
1701
+ config.command("remove <name>").description("Delete a profile").action(async (name, _options, command) => {
1702
+ await runConfigRemove(outputFor(command), name);
1703
+ });
1704
+ }
1705
+ function requireProfile(config, name) {
1706
+ if (!(name in config.profiles)) {
1707
+ throw new UsageError(`No profile named "${name}". Create it with: morse config add ${name} --network testnet`);
1708
+ }
1709
+ }
1710
+ function renderProfiles(config) {
1711
+ const names = Object.keys(config.profiles);
1712
+ if (names.length === 0) {
1713
+ return "No profiles configured. Add one with: morse config add <name> --network testnet";
1714
+ }
1715
+ return names.map((name) => {
1716
+ const profile = config.profiles[name];
1386
1717
  if (profile === undefined) {
1387
1718
  return name;
1388
1719
  }
@@ -1462,333 +1793,690 @@ import {
1462
1793
  import { SessionKey } from "@mysten/seal";
1463
1794
  var SESSION_KEY_TTL_MIN = 10;
1464
1795
  var SEAL_NONCE_BYTES = 16;
1796
+ async function runEntryAddEncrypted(ctx, name, options) {
1797
+ const id = await resolvePublication(ctx, options.publication);
1798
+ const collection = resolveCollection(ctx, options.collection);
1799
+ const epochs = parsePositiveInt(options.epochs, "--epochs");
1800
+ const plaintext = await readContentBytes(options);
1801
+ const contentType = resolveContentType(options.contentType, options.stdin ? undefined : options.file);
1802
+ const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
1803
+ const sealId = buildPublisherSealId(id, crypto.getRandomValues(new Uint8Array(SEAL_NONCE_BYTES)));
1804
+ ctx.output.info(`Encrypting and uploading ${plaintext.length} bytes...`);
1805
+ const result = await addEncryptedEntryFromBytes(ctx.adapter, ctx.config, {
1806
+ walrus: ctx.walrus,
1807
+ seal: ctx.seal,
1808
+ publicationId: id,
1809
+ publisherCapId,
1810
+ collectionName: collection,
1811
+ name,
1812
+ plaintext,
1813
+ contentType,
1814
+ sealId,
1815
+ upload: { epochs, deletable: true },
1816
+ signal: ctx.signal
1817
+ });
1818
+ ctx.output.result(`Added encrypted entry #${result.entryId} "${name}". (tx: ${result.digest})`, { ...result, sealId });
1819
+ }
1820
+ async function runEntryDecrypt(ctx, entryId, revisionId, options) {
1821
+ if (ctx.output.isJson && options.out === undefined) {
1822
+ throw new UsageError("Decrypting to stdout is not supported in --json mode; pass --out <path>.");
1823
+ }
1824
+ const id = await resolvePublication(ctx, options.publication);
1825
+ const collection = resolveCollection(ctx, options.collection);
1826
+ const numericEntryId = parseId(entryId, "entryId");
1827
+ const entryData = await ctx.reader.getEntry(id, collection, numericEntryId, ctx.signal);
1828
+ if (revisionId === undefined && entryData.revisions.length === 0) {
1829
+ throw new UsageError(`Entry #${numericEntryId} has no revisions.`);
1830
+ }
1831
+ const revisionIndex = revisionId === undefined ? entryData.revisions.length - 1 : parseId(revisionId, "revision");
1832
+ const revision = entryData.revisions[revisionIndex];
1833
+ if (revision === undefined) {
1834
+ throw new UsageError(`Entry #${numericEntryId} has no revision at index ${revisionIndex}.`);
1835
+ }
1836
+ if (!revision.encrypted || revision.sealId === null) {
1837
+ throw new UsageError(`Revision #${revisionIndex} of entry #${numericEntryId} is not encrypted.`);
1838
+ }
1839
+ const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
1840
+ ctx.output.info("Fetching ciphertext from Walrus...");
1841
+ const ciphertext = await ctx.walrusRead.readBlobRef(revision.blobRef, {
1842
+ signal: ctx.signal
1843
+ });
1844
+ ctx.output.info("Signing a SessionKey with the active account...");
1845
+ const sessionKey = await SessionKey.create({
1846
+ address: ctx.address,
1847
+ packageId: ctx.config.originalPackageId ?? ctx.config.packageId,
1848
+ ttlMin: SESSION_KEY_TTL_MIN,
1849
+ signer: ctx.keypair,
1850
+ suiClient: ctx.client
1851
+ });
1852
+ const plaintext = await ctx.seal.decrypt(ciphertext, {
1853
+ sessionKey,
1854
+ sealId: revision.sealId,
1855
+ publisherCapId
1856
+ });
1857
+ if (options.out !== undefined) {
1858
+ await writeFileContents(options.out, plaintext);
1859
+ ctx.output.result(`Wrote ${plaintext.length} bytes to ${options.out}.`, {
1860
+ entryId: numericEntryId,
1861
+ revisionId: revisionIndex,
1862
+ bytes: plaintext.length,
1863
+ contentType: revision.contentType,
1864
+ out: options.out
1865
+ });
1866
+ return;
1867
+ }
1868
+ process.stdout.write(plaintext);
1869
+ }
1465
1870
  function registerEncryptedEntryCommands(entry) {
1466
1871
  const add = entry.command("add-encrypted <name>").description("Encrypt a file or stdin with Seal and add it as a new entry");
1467
1872
  collectionOption(publicationOption(publisherCapOption(contentOptions(add)))).action(async (name, options, command) => {
1468
- const ctx = await buildEncryptContext(command);
1469
- const id = await resolvePublication(ctx, options.publication);
1470
- const collection = resolveCollection(ctx, options.collection);
1471
- const epochs = parsePositiveInt(options.epochs, "--epochs");
1472
- const plaintext = await readContentBytes(options);
1473
- const contentType = resolveContentType(options.contentType, options.stdin ? undefined : options.file);
1474
- const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
1475
- const sealId = buildPublisherSealId(id, crypto.getRandomValues(new Uint8Array(SEAL_NONCE_BYTES)));
1476
- ctx.output.info(`Encrypting and uploading ${plaintext.length} bytes...`);
1477
- const result = await addEncryptedEntryFromBytes(ctx.adapter, ctx.config, {
1478
- walrus: ctx.walrus,
1479
- seal: ctx.seal,
1480
- publicationId: id,
1481
- publisherCapId,
1482
- collectionName: collection,
1483
- name,
1484
- plaintext,
1485
- contentType,
1486
- sealId,
1487
- upload: { epochs, deletable: true },
1488
- signal: ctx.signal
1489
- });
1490
- ctx.output.result(`Added encrypted entry #${result.entryId} "${name}". (tx: ${result.digest})`, { ...result, sealId });
1873
+ await runEntryAddEncrypted(await buildEncryptContext(command), name, options);
1491
1874
  });
1492
1875
  const decrypt = entry.command("decrypt <entryId> [revisionIndex]").description("Decrypt an encrypted revision by zero-based index (default: latest); signs a SessionKey with the active account").option("--out <path>", "Write plaintext to a file instead of stdout");
1493
- collectionOption(publicationOption(publisherCapOption(decrypt))).action(async (entryId, revisionId, options, command) => {
1494
- const ctx = await buildDecryptContext(command);
1495
- if (ctx.output.isJson && options.out === undefined) {
1496
- throw new UsageError("Decrypting to stdout is not supported in --json mode; pass --out <path>.");
1497
- }
1498
- const id = await resolvePublication(ctx, options.publication);
1499
- const collection = resolveCollection(ctx, options.collection);
1500
- const numericEntryId = parseId(entryId, "entryId");
1501
- const entryData = await ctx.reader.getEntry(id, collection, numericEntryId, ctx.signal);
1502
- if (revisionId === undefined && entryData.revisions.length === 0) {
1503
- throw new UsageError(`Entry #${numericEntryId} has no revisions.`);
1504
- }
1505
- const revisionIndex = revisionId === undefined ? entryData.revisions.length - 1 : parseId(revisionId, "revision");
1506
- const revision = entryData.revisions[revisionIndex];
1507
- if (revision === undefined) {
1508
- throw new UsageError(`Entry #${numericEntryId} has no revision at index ${revisionIndex}.`);
1509
- }
1510
- if (!revision.encrypted || revision.sealId === null) {
1511
- throw new UsageError(`Revision #${revisionIndex} of entry #${numericEntryId} is not encrypted.`);
1512
- }
1513
- const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
1514
- ctx.output.info("Fetching ciphertext from Walrus...");
1515
- const ciphertext = await ctx.walrusRead.readBlobRef(revision.blobRef, {
1516
- signal: ctx.signal
1517
- });
1518
- ctx.output.info("Signing a SessionKey with the active account...");
1519
- const sessionKey = await SessionKey.create({
1520
- address: ctx.address,
1521
- packageId: ctx.config.originalPackageId ?? ctx.config.packageId,
1522
- ttlMin: SESSION_KEY_TTL_MIN,
1523
- signer: ctx.keypair,
1524
- suiClient: ctx.client
1525
- });
1526
- const plaintext = await ctx.seal.decrypt(ciphertext, {
1527
- sessionKey,
1528
- sealId: revision.sealId,
1529
- publisherCapId
1530
- });
1531
- if (options.out !== undefined) {
1532
- await writeFileContents(options.out, plaintext);
1533
- ctx.output.result(`Wrote ${plaintext.length} bytes to ${options.out}.`, {
1534
- entryId: numericEntryId,
1535
- revisionId: revisionIndex,
1536
- bytes: plaintext.length,
1537
- contentType: revision.contentType,
1538
- out: options.out
1539
- });
1540
- return;
1541
- }
1542
- process.stdout.write(plaintext);
1876
+ collectionOption(publicationOption(publisherCapOption(viaAggregatorOption(decrypt)))).action(async (entryId, revisionId, options, command) => {
1877
+ await runEntryDecrypt(await buildDecryptContext(command, {
1878
+ viaAggregator: options.viaAggregator
1879
+ }), entryId, revisionId, options);
1543
1880
  });
1544
1881
  }
1545
1882
 
1546
1883
  // src/commands/entry.ts
1884
+ async function runEntryGet(ctx, entryId, options) {
1885
+ const id = await resolvePublication(ctx, options.publication);
1886
+ const collection = resolveCollection(ctx, options.collection);
1887
+ const result = await ctx.reader.getEntry(id, collection, parseId(entryId, "entryId"), ctx.signal);
1888
+ ctx.output.result(renderEntry(result), result);
1889
+ }
1890
+ async function runEntryList(ctx, options) {
1891
+ const id = await resolvePublication(ctx, options.publication);
1892
+ const collection = resolveCollection(ctx, options.collection);
1893
+ const page = await ctx.reader.listEntries(id, collection, {
1894
+ signal: ctx.signal,
1895
+ ...options.limit === undefined ? {} : { limit: parseLimit(options.limit) },
1896
+ ...options.cursor === undefined ? {} : { cursor: options.cursor }
1897
+ });
1898
+ if (page.nextCursor !== null) {
1899
+ ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
1900
+ }
1901
+ ctx.output.result(renderEntryList(page.results), {
1902
+ results: page.results,
1903
+ nextCursor: page.nextCursor
1904
+ });
1905
+ }
1906
+ async function runEntryScan(ctx, options) {
1907
+ const id = await resolvePublication(ctx, options.publication);
1908
+ const collection = resolveCollection(ctx, options.collection);
1909
+ const entries = [];
1910
+ for await (const item of ctx.reader.scanEntries(id, collection, {
1911
+ signal: ctx.signal
1912
+ })) {
1913
+ entries.push(item);
1914
+ }
1915
+ ctx.output.result(renderEntryList(entries), { results: entries });
1916
+ }
1917
+ async function runEntryAdd(ctx, name, options) {
1918
+ const id = await resolvePublication(ctx, options.publication);
1919
+ const collection = resolveCollection(ctx, options.collection);
1920
+ const epochs = parsePositiveInt(options.epochs, "--epochs");
1921
+ const bytes = await readContentBytes(options);
1922
+ const contentType = resolveContentType(options.contentType, options.stdin ? undefined : options.file);
1923
+ const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
1924
+ ctx.output.info(`Uploading ${bytes.length} bytes to Walrus...`);
1925
+ const result = await addEntryFromBytes(ctx.adapter, ctx.config, {
1926
+ walrus: ctx.walrus,
1927
+ publicationId: id,
1928
+ publisherCapId,
1929
+ collectionName: collection,
1930
+ name,
1931
+ bytes,
1932
+ contentType,
1933
+ upload: { epochs, deletable: true },
1934
+ signal: ctx.signal
1935
+ });
1936
+ const aggregator = ctx.config.walrusEndpoints.aggregator;
1937
+ const viewUrl = aggregator.length > 0 ? `${aggregator}/v1/blobs/${result.blobId}` : undefined;
1938
+ const human = viewUrl === undefined ? `Added entry #${result.entryId} "${name}". (tx: ${result.digest})` : `Added entry #${result.entryId} "${name}". (tx: ${result.digest})
1939
+ view: ${viewUrl}`;
1940
+ ctx.output.result(human, { ...result, viewUrl: viewUrl ?? null });
1941
+ }
1942
+ async function runEntryDelete(ctx, entryId, options, gopts) {
1943
+ const id = await resolvePublication(ctx, options.publication);
1944
+ const collection = resolveCollection(ctx, options.collection);
1945
+ const numericEntryId = parseId(entryId, "entryId");
1946
+ const proceed = await confirm(`Delete entry #${numericEntryId} from ${shortId(id)}/${collection}? This cannot be undone.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
1947
+ if (!proceed) {
1948
+ cancelled();
1949
+ }
1950
+ const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
1951
+ const result = await deleteEntry(ctx.adapter, ctx.config, {
1952
+ publicationId: id,
1953
+ publisherCapId,
1954
+ collectionName: collection,
1955
+ entryId: numericEntryId,
1956
+ signal: ctx.signal
1957
+ });
1958
+ ctx.output.result(`Deleted entry #${numericEntryId}. (tx: ${result.digest})`, result);
1959
+ }
1960
+ async function runEntryRead(ctx, entryId, revisionId, options) {
1961
+ if (ctx.output.isJson && options.out === undefined) {
1962
+ throw new UsageError("Reading content to stdout is not supported in --json mode; pass --out <path>.");
1963
+ }
1964
+ const id = await resolvePublication(ctx, options.publication);
1965
+ const collection = resolveCollection(ctx, options.collection);
1966
+ const numericEntryId = parseId(entryId, "entryId");
1967
+ const entryData = await ctx.reader.getEntry(id, collection, numericEntryId, ctx.signal);
1968
+ if (revisionId === undefined && entryData.revisions.length === 0) {
1969
+ throw new UsageError(`Entry #${numericEntryId} has no revisions.`);
1970
+ }
1971
+ const revisionIndex = revisionId === undefined ? entryData.revisions.length - 1 : parseId(revisionId, "revision");
1972
+ const revision = entryData.revisions[revisionIndex];
1973
+ if (revision === undefined) {
1974
+ throw new UsageError(`Entry #${numericEntryId} has no revision at index ${revisionIndex}.`);
1975
+ }
1976
+ if (revision.encrypted) {
1977
+ throw new UsageError(`Revision #${revisionIndex} of entry #${numericEntryId} is encrypted; use \`morse entry decrypt\`.`);
1978
+ }
1979
+ ctx.output.info("Fetching content from Walrus...");
1980
+ const bytes = await ctx.walrusRead.readBlobRef(revision.blobRef, {
1981
+ signal: ctx.signal
1982
+ });
1983
+ if (options.out !== undefined) {
1984
+ await writeFileContents(options.out, bytes);
1985
+ ctx.output.result(`Wrote ${bytes.length} bytes to ${options.out}.`, {
1986
+ entryId: numericEntryId,
1987
+ revisionId: revisionIndex,
1988
+ bytes: bytes.length,
1989
+ contentType: revision.contentType,
1990
+ out: options.out
1991
+ });
1992
+ return;
1993
+ }
1994
+ process.stdout.write(bytes);
1995
+ }
1547
1996
  function registerEntryCommands(program) {
1548
1997
  const entry = program.command("entry").description("Read, add, and delete entries in a collection");
1549
1998
  const get = entry.command("get <entryId>").description("Fetch a single entry");
1550
1999
  collectionOption(publicationOption(get)).action(async (entryId, options, command) => {
1551
- const ctx = await buildReadContext(command);
1552
- const id = await resolvePublication(ctx, options.publication);
1553
- const collection = resolveCollection(ctx, options.collection);
1554
- const result = await ctx.reader.getEntry(id, collection, parseId(entryId, "entryId"), ctx.signal);
1555
- ctx.output.result(renderEntry(result), result);
2000
+ await runEntryGet(await buildReadContext(command), entryId, options);
1556
2001
  });
1557
2002
  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");
1558
2003
  collectionOption(publicationOption(list)).action(async (options, command) => {
1559
- const ctx = await buildReadContext(command);
1560
- const id = await resolvePublication(ctx, options.publication);
1561
- const collection = resolveCollection(ctx, options.collection);
1562
- const page = await ctx.reader.listEntries(id, collection, {
1563
- signal: ctx.signal,
1564
- ...options.limit === undefined ? {} : { limit: parseLimit(options.limit) },
1565
- ...options.cursor === undefined ? {} : { cursor: options.cursor }
1566
- });
1567
- if (page.nextCursor !== null) {
1568
- ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
1569
- }
1570
- ctx.output.result(renderEntryList(page.results), {
1571
- results: page.results,
1572
- nextCursor: page.nextCursor
1573
- });
2004
+ await runEntryList(await buildReadContext(command), options);
1574
2005
  });
1575
2006
  const scan = entry.command("scan").description("List every entry in a collection (auto-paginated)");
1576
2007
  collectionOption(publicationOption(scan)).action(async (options, command) => {
1577
- const ctx = await buildReadContext(command);
1578
- const id = await resolvePublication(ctx, options.publication);
1579
- const collection = resolveCollection(ctx, options.collection);
1580
- const entries = [];
1581
- for await (const item of ctx.reader.scanEntries(id, collection, {
1582
- signal: ctx.signal
1583
- })) {
1584
- entries.push(item);
1585
- }
1586
- ctx.output.result(renderEntryList(entries), { results: entries });
2008
+ await runEntryScan(await buildReadContext(command), options);
1587
2009
  });
1588
2010
  const add = entry.command("add <name>").description("Upload content from a file or stdin and add it as a new entry");
1589
2011
  collectionOption(publicationOption(publisherCapOption(contentOptions(add)))).action(async (name, options, command) => {
1590
- const ctx = await buildContentContext(command);
1591
- const id = await resolvePublication(ctx, options.publication);
1592
- const collection = resolveCollection(ctx, options.collection);
1593
- const epochs = parsePositiveInt(options.epochs, "--epochs");
1594
- const bytes = await readContentBytes(options);
1595
- const contentType = resolveContentType(options.contentType, options.stdin ? undefined : options.file);
1596
- const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
1597
- ctx.output.info(`Uploading ${bytes.length} bytes to Walrus...`);
1598
- const result = await addEntryFromBytes(ctx.adapter, ctx.config, {
1599
- walrus: ctx.walrus,
1600
- publicationId: id,
1601
- publisherCapId,
1602
- collectionName: collection,
1603
- name,
1604
- bytes,
1605
- contentType,
1606
- upload: { epochs, deletable: true },
1607
- signal: ctx.signal
1608
- });
1609
- const aggregator = ctx.config.walrusEndpoints.aggregator;
1610
- const viewUrl = aggregator.length > 0 ? `${aggregator}/v1/blobs/${result.blobId}` : undefined;
1611
- const human = viewUrl === undefined ? `Added entry #${result.entryId} "${name}". (tx: ${result.digest})` : `Added entry #${result.entryId} "${name}". (tx: ${result.digest})
1612
- view: ${viewUrl}`;
1613
- ctx.output.result(human, { ...result, viewUrl: viewUrl ?? null });
2012
+ await runEntryAdd(await buildContentContext(command), name, options);
1614
2013
  });
1615
2014
  const remove = entry.command("delete <entryId>").description("Delete an entry and its revisions");
1616
2015
  collectionOption(publicationOption(publisherCapOption(remove))).action(async (entryId, options, command) => {
1617
- const ctx = await buildWriteContext(command);
1618
- const id = await resolvePublication(ctx, options.publication);
1619
- const collection = resolveCollection(ctx, options.collection);
1620
- const numericEntryId = parseId(entryId, "entryId");
1621
- const proceed = await confirm(`Delete entry #${numericEntryId} from ${shortId(id)}/${collection}? This cannot be undone.`, { assumeYes: Boolean(globalOptions(command).yes), signal: ctx.signal });
1622
- if (!proceed) {
1623
- cancelled();
1624
- }
1625
- const publisherCapId = await resolvePublisherCap(ctx.reader, ctx.address, id, options.publisherCap, ctx.signal);
1626
- const result = await deleteEntry(ctx.adapter, ctx.config, {
1627
- publicationId: id,
1628
- publisherCapId,
1629
- collectionName: collection,
1630
- entryId: numericEntryId,
1631
- signal: ctx.signal
1632
- });
1633
- ctx.output.result(`Deleted entry #${numericEntryId}. (tx: ${result.digest})`, result);
2016
+ await runEntryDelete(await buildWriteContext(command), entryId, options, globalOptions(command));
1634
2017
  });
1635
2018
  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");
1636
- collectionOption(publicationOption(read)).action(async (entryId, revisionId, options, command) => {
1637
- const ctx = await buildReadContentContext(command);
1638
- if (ctx.output.isJson && options.out === undefined) {
1639
- throw new UsageError("Reading content to stdout is not supported in --json mode; pass --out <path>.");
1640
- }
1641
- const id = await resolvePublication(ctx, options.publication);
1642
- const collection = resolveCollection(ctx, options.collection);
1643
- const numericEntryId = parseId(entryId, "entryId");
1644
- const entryData = await ctx.reader.getEntry(id, collection, numericEntryId, ctx.signal);
1645
- if (revisionId === undefined && entryData.revisions.length === 0) {
1646
- throw new UsageError(`Entry #${numericEntryId} has no revisions.`);
2019
+ collectionOption(publicationOption(viaAggregatorOption(read))).action(async (entryId, revisionId, options, command) => {
2020
+ await runEntryRead(await buildReadContentContext(command, {
2021
+ viaAggregator: options.viaAggregator
2022
+ }), entryId, revisionId, options);
2023
+ });
2024
+ registerEncryptedEntryCommands(entry);
2025
+ }
2026
+
2027
+ // src/commands/file.ts
2028
+ import {
2029
+ buildAllowlistSealId,
2030
+ createEncryptedFile,
2031
+ createPublicFile,
2032
+ deleteFile,
2033
+ reconcileFilesAccessibleBy,
2034
+ reconcileFilesOwnedBy,
2035
+ toAllowlistId as toAllowlistId2,
2036
+ toBlobObjectId,
2037
+ toEncryptedFileId,
2038
+ toSuiAddress as toSuiAddress5,
2039
+ toWalrusBlobId,
2040
+ transferFileOwnership,
2041
+ updateFileMetadata,
2042
+ uploadEncryptedFileFromBytes,
2043
+ uploadPublicFileFromBytes
2044
+ } from "@arcadiasystems/morse-sdk";
2045
+ import { SessionKey as SessionKey2 } from "@mysten/seal";
2046
+
2047
+ // src/cli/events.ts
2048
+ var PAGE_LIMIT2 = 50;
2049
+ async function fetchEventStream(client, eventType, signal) {
2050
+ const out = [];
2051
+ let cursor;
2052
+ for (;; ) {
2053
+ const page = await client.queryEvents({
2054
+ query: { MoveEventType: eventType },
2055
+ limit: PAGE_LIMIT2,
2056
+ order: "descending",
2057
+ ...cursor === undefined ? {} : { cursor },
2058
+ ...signal === undefined ? {} : { signal }
2059
+ });
2060
+ for (const event of page.data) {
2061
+ out.push({
2062
+ type: event.type,
2063
+ parsedJson: event.parsedJson,
2064
+ timestampMs: event.timestampMs
2065
+ });
1647
2066
  }
1648
- const revisionIndex = revisionId === undefined ? entryData.revisions.length - 1 : parseId(revisionId, "revision");
1649
- const revision = entryData.revisions[revisionIndex];
1650
- if (revision === undefined) {
1651
- throw new UsageError(`Entry #${numericEntryId} has no revision at index ${revisionIndex}.`);
2067
+ if (!page.hasNextPage || page.nextCursor === null || page.nextCursor === undefined) {
2068
+ break;
1652
2069
  }
1653
- if (revision.encrypted) {
1654
- throw new UsageError(`Revision #${revisionIndex} of entry #${numericEntryId} is encrypted; use \`morse entry decrypt\`.`);
2070
+ cursor = page.nextCursor;
2071
+ }
2072
+ return out;
2073
+ }
2074
+ async function fetchEventStreams(client, eventTypes, signal) {
2075
+ const streams = await Promise.all(eventTypes.map((type) => fetchEventStream(client, type, signal)));
2076
+ return streams.flat();
2077
+ }
2078
+
2079
+ // src/format/hex.ts
2080
+ var HEX = /^[0-9a-fA-F]+$/;
2081
+ function encodeHex(bytes) {
2082
+ let out = "0x";
2083
+ for (const byte of bytes) {
2084
+ out += byte.toString(16).padStart(2, "0");
2085
+ }
2086
+ return out;
2087
+ }
2088
+ function decodeHex(value) {
2089
+ const body = value.startsWith("0x") ? value.slice(2) : value;
2090
+ if (body.length === 0 || body.length % 2 !== 0 || !HEX.test(body)) {
2091
+ throw new UsageError(`Invalid hex value: expected an even number of hex digits, got ${JSON.stringify(value)}.`);
2092
+ }
2093
+ const bytes = new Uint8Array(body.length / 2);
2094
+ for (let i = 0;i < bytes.length; i += 1) {
2095
+ bytes[i] = Number.parseInt(body.slice(i * 2, i * 2 + 2), 16);
2096
+ }
2097
+ return bytes;
2098
+ }
2099
+
2100
+ // src/commands/file.ts
2101
+ var SESSION_KEY_TTL_MIN2 = 10;
2102
+ var SEAL_NONCE_BYTES2 = 16;
2103
+ function uploadProgress(output) {
2104
+ const labels = {
2105
+ encrypting: "Encrypting...",
2106
+ uploading: "Uploading to Walrus...",
2107
+ submitting: "Submitting transaction...",
2108
+ complete: ""
2109
+ };
2110
+ return (event) => {
2111
+ const label = labels[event.phase];
2112
+ if (label.length > 0) {
2113
+ output.info(label);
1655
2114
  }
1656
- ctx.output.info("Fetching content from Walrus...");
1657
- const bytes = await ctx.walrusRead.readBlobRef(revision.blobRef, {
2115
+ };
2116
+ }
2117
+ async function runFileGet(ctx, target) {
2118
+ const result = await ctx.filesReader.getEncryptedFile(toEncryptedFileId(target), ctx.signal);
2119
+ ctx.output.result(renderEncryptedFile(result), result);
2120
+ }
2121
+ async function runFileRegister(ctx, options) {
2122
+ if (options.allowlist === undefined && options.public !== true) {
2123
+ throw new UsageError("Pass --allowlist <id> to register an encrypted file, or --public for a world-readable one.");
2124
+ }
2125
+ const blobId = toWalrusBlobId(options.blobId);
2126
+ const size = parseByteSize(options.size, "--size");
2127
+ const blobObjectId = options.blobObjectId === undefined ? undefined : toBlobObjectId(options.blobObjectId);
2128
+ if (options.allowlist !== undefined) {
2129
+ const result2 = await createEncryptedFile(ctx.adapter, ctx.config, {
2130
+ allowlistId: toAllowlistId2(options.allowlist),
2131
+ blobId,
2132
+ ...blobObjectId === undefined ? {} : { blobObjectId },
2133
+ name: options.name,
2134
+ contentType: options.contentType,
2135
+ size,
1658
2136
  signal: ctx.signal
1659
2137
  });
1660
- if (options.out !== undefined) {
1661
- await writeFileContents(options.out, bytes);
1662
- ctx.output.result(`Wrote ${bytes.length} bytes to ${options.out}.`, {
1663
- entryId: numericEntryId,
1664
- revisionId: revisionIndex,
1665
- bytes: bytes.length,
1666
- contentType: revision.contentType,
1667
- out: options.out
1668
- });
1669
- return;
2138
+ ctx.output.result(`Registered encrypted file ${result2.fileId}. (tx: ${result2.digest})`, result2);
2139
+ return;
2140
+ }
2141
+ const result = await createPublicFile(ctx.adapter, ctx.config, {
2142
+ blobId,
2143
+ ...blobObjectId === undefined ? {} : { blobObjectId },
2144
+ name: options.name,
2145
+ contentType: options.contentType,
2146
+ size,
2147
+ signal: ctx.signal
2148
+ });
2149
+ ctx.output.result(`Registered public file ${result.fileId}. (tx: ${result.digest})`, result);
2150
+ }
2151
+ async function runFileUpdate(ctx, target, options) {
2152
+ const result = await updateFileMetadata(ctx.adapter, ctx.config, {
2153
+ fileId: toEncryptedFileId(target),
2154
+ name: options.name,
2155
+ contentType: options.contentType,
2156
+ signal: ctx.signal
2157
+ });
2158
+ ctx.output.result(`Updated file ${shortId(target)} metadata. (tx: ${result.digest})`, result);
2159
+ }
2160
+ async function runFileTransferOwnership(ctx, target, newOwner, gopts) {
2161
+ const fileId = toEncryptedFileId(target);
2162
+ const to = toSuiAddress5(newOwner);
2163
+ const proceed = await confirm(`Transfer metadata ownership of file ${shortId(fileId)} to ${to}? Decryption access is governed separately by the allowlist.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
2164
+ if (!proceed) {
2165
+ cancelled();
2166
+ }
2167
+ const result = await transferFileOwnership(ctx.adapter, ctx.config, {
2168
+ fileId,
2169
+ newOwner: to,
2170
+ signal: ctx.signal
2171
+ });
2172
+ ctx.output.result(`Transferred file ownership to ${to}. (tx: ${result.digest})`, result);
2173
+ }
2174
+ async function runFileDelete(ctx, target, gopts) {
2175
+ const fileId = toEncryptedFileId(target);
2176
+ const proceed = await confirm(`Delete file metadata ${shortId(fileId)}? The Walrus blob is not deleted; it expires on its own lease.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
2177
+ if (!proceed) {
2178
+ cancelled();
2179
+ }
2180
+ const result = await deleteFile(ctx.adapter, ctx.config, {
2181
+ fileId,
2182
+ signal: ctx.signal
2183
+ });
2184
+ ctx.output.result(`Deleted file ${fileId}. (tx: ${result.digest})`, result);
2185
+ }
2186
+ async function runFileUpload(ctx, path, options) {
2187
+ if (options.allowlist === undefined && options.public !== true) {
2188
+ throw new UsageError("Pass --allowlist <id> to upload an encrypted file, or --public for a world-readable one.");
2189
+ }
2190
+ const epochs = parsePositiveInt(options.epochs ?? "3", "--epochs");
2191
+ const bytes = await readContentBytes({ file: path });
2192
+ const contentType = resolveContentType(options.contentType, path);
2193
+ const upload = { epochs, deletable: true };
2194
+ const onProgress = uploadProgress(ctx.output);
2195
+ if (options.allowlist !== undefined) {
2196
+ const allowlistId = toAllowlistId2(options.allowlist);
2197
+ const sealId = buildAllowlistSealId(allowlistId, crypto.getRandomValues(new Uint8Array(SEAL_NONCE_BYTES2)));
2198
+ const result2 = await uploadEncryptedFileFromBytes(ctx.adapter, ctx.config, {
2199
+ walrus: ctx.walrus,
2200
+ seal: ctx.seal,
2201
+ allowlistId,
2202
+ sealId,
2203
+ plaintext: bytes,
2204
+ name: options.name,
2205
+ contentType,
2206
+ upload,
2207
+ signal: ctx.signal,
2208
+ onProgress
2209
+ });
2210
+ const sealIdHex = encodeHex(sealId);
2211
+ const human2 = [
2212
+ `Uploaded encrypted file ${result2.fileId}`,
2213
+ ` blobId: ${result2.blobId}`,
2214
+ ` sealId: ${sealIdHex}`,
2215
+ ` tx: ${result2.digest}`,
2216
+ "Save the seal id: decrypting later needs it plus allowlist membership."
2217
+ ].join(`
2218
+ `);
2219
+ ctx.output.result(human2, { ...result2, sealId: sealIdHex });
2220
+ return;
2221
+ }
2222
+ const result = await uploadPublicFileFromBytes(ctx.adapter, ctx.config, {
2223
+ walrus: ctx.walrus,
2224
+ bytes,
2225
+ name: options.name,
2226
+ contentType,
2227
+ upload,
2228
+ signal: ctx.signal,
2229
+ onProgress
2230
+ });
2231
+ const aggregator = ctx.config.walrusEndpoints.aggregator;
2232
+ const viewUrl = aggregator.length > 0 ? `${aggregator}/v1/blobs/${result.blobId}` : undefined;
2233
+ const human = viewUrl === undefined ? `Uploaded public file ${result.fileId}. (tx: ${result.digest})` : `Uploaded public file ${result.fileId}. (tx: ${result.digest})
2234
+ view: ${viewUrl}`;
2235
+ ctx.output.result(human, { ...result, viewUrl: viewUrl ?? null });
2236
+ }
2237
+ async function decryptFile(ctx, file, ciphertext, sealIdHex) {
2238
+ if (sealIdHex === undefined) {
2239
+ throw new UsageError("This file is encrypted; pass --seal-id <hex> (printed when the file was uploaded).");
2240
+ }
2241
+ if (file.allowlistId === null) {
2242
+ throw new UsageError(`File ${file.id} is marked encrypted but carries no allowlist; it cannot be decrypted.`);
2243
+ }
2244
+ const sealId = decodeHex(sealIdHex);
2245
+ const { keypair, address } = await ctx.unlockSigner();
2246
+ ctx.output.info("Signing a SessionKey with the active account...");
2247
+ const sessionKey = await SessionKey2.create({
2248
+ address,
2249
+ packageId: ctx.config.originalPackageId ?? ctx.config.packageId,
2250
+ ttlMin: SESSION_KEY_TTL_MIN2,
2251
+ signer: keypair,
2252
+ suiClient: ctx.client
2253
+ });
2254
+ return ctx.seal.decryptUnderAllowlist(ciphertext, {
2255
+ sealId,
2256
+ allowlistId: file.allowlistId,
2257
+ sessionKey
2258
+ });
2259
+ }
2260
+ async function runFileDownload(ctx, target, options) {
2261
+ if (ctx.output.isJson && options.out === undefined) {
2262
+ throw new UsageError("Downloading content to stdout is not supported in --json mode; pass --out <path>.");
2263
+ }
2264
+ const file = await ctx.filesReader.getEncryptedFile(toEncryptedFileId(target), ctx.signal);
2265
+ ctx.output.info("Fetching content from Walrus...");
2266
+ const bytes = await ctx.walrusRead.readBlob(file.blobId, {
2267
+ signal: ctx.signal
2268
+ });
2269
+ let content = bytes;
2270
+ if (file.encrypted) {
2271
+ content = await decryptFile(ctx, file, bytes, options.sealId);
2272
+ }
2273
+ if (options.out !== undefined) {
2274
+ await writeFileContents(options.out, content);
2275
+ ctx.output.result(`Wrote ${content.length} bytes to ${options.out}.`, {
2276
+ fileId: file.id,
2277
+ name: file.name,
2278
+ contentType: file.contentType,
2279
+ bytes: content.length,
2280
+ out: options.out
2281
+ });
2282
+ return;
2283
+ }
2284
+ process.stdout.write(content);
2285
+ }
2286
+ async function runFileList(ctx, options) {
2287
+ const limit = options.limit === undefined ? undefined : parseLimit(options.limit);
2288
+ const address = options.address === undefined ? ctx.ownerAddress : toSuiAddress5(options.address);
2289
+ if (address === undefined) {
2290
+ throw new UsageError("No address given and no active account. Pass --address or import an account.");
2291
+ }
2292
+ const types = ctx.eventTypes;
2293
+ let summaries;
2294
+ if (options.accessible) {
2295
+ ctx.output.info("Fetching membership and file events...");
2296
+ const events = await fetchEventStreams(ctx.events, [
2297
+ types.MemberAdded,
2298
+ types.MemberRemoved,
2299
+ types.AllowlistDeleted,
2300
+ types.FileCreated,
2301
+ types.FileDeleted
2302
+ ], ctx.signal);
2303
+ summaries = reconcileFilesAccessibleBy(events, address, types);
2304
+ } else {
2305
+ ctx.output.info("Fetching file events...");
2306
+ const events = await fetchEventStreams(ctx.events, [types.FileCreated, types.FileOwnershipTransferred, types.FileDeleted], ctx.signal);
2307
+ summaries = reconcileFilesOwnedBy(events, address, types);
2308
+ }
2309
+ const limited = limit === undefined ? summaries : summaries.slice(0, limit);
2310
+ let items = limited;
2311
+ if (options.hydrate) {
2312
+ ctx.output.info(`Hydrating ${limited.length} file(s)...`);
2313
+ const full = [];
2314
+ for (const summary of limited) {
2315
+ const record = await ctx.filesReader.getEncryptedFile(summary.id, ctx.signal);
2316
+ full.push({ ...record, kind: "full" });
1670
2317
  }
1671
- process.stdout.write(bytes);
2318
+ items = full;
2319
+ }
2320
+ ctx.output.result(renderFileList(items), items);
2321
+ }
2322
+ function registerFileCommands(program) {
2323
+ const file = program.command("file").description("Upload, register, read, and manage files on Walrus");
2324
+ const upload = file.command("upload <path>").description("Encrypt (or not), upload to Walrus, and register a file").requiredOption("-n, --name <name>", "File name").option("--public", "Upload a world-readable (unencrypted) file").option("--content-type <mime>", "MIME content type (inferred from the path if omitted)").option("--epochs <n>", "Walrus storage epochs", "3");
2325
+ allowlistOption(upload).action(async (path, options, command) => {
2326
+ await runFileUpload(await buildEncryptContext(command), path, options);
2327
+ });
2328
+ file.command("list").description("List files owned by (or accessible to) an address, via event indexing").option("--address <addr>", "Address to query (default: the active account)").option("--accessible", "List files you can decrypt via allowlist membership, not just owned").option("--hydrate", "Fetch full records (adds blobId; one read per file)").option("--limit <n>", "Cap the number of results").option("--indexer-url <url>", "Event source URL (default: the RPC; uses suix_queryEvents, a deprecated Sui endpoint)").action(async (options, command) => {
2329
+ await runFileList(await buildFileListContext(command, {
2330
+ indexerUrl: options.indexerUrl
2331
+ }), options);
2332
+ });
2333
+ 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("--seal-id <hex>", "Seal id from upload (required to decrypt an encrypted file)");
2334
+ viaAggregatorOption(download).action(async (target, options, command) => {
2335
+ await runFileDownload(await buildFileDownloadContext(command, {
2336
+ viaAggregator: options.viaAggregator
2337
+ }), target, options);
2338
+ });
2339
+ file.command("get <file>").description("Fetch a file's on-chain metadata").action(async (target, _options, command) => {
2340
+ await runFileGet(await buildFilesReadContext(command), target);
2341
+ });
2342
+ 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("--blob-object-id <id>", "On-chain Walrus Blob object id, if known");
2343
+ allowlistOption(register).action(async (options, command) => {
2344
+ await runFileRegister(await buildWriteContext(command), options);
2345
+ });
2346
+ file.command("update <file>").description("Update a file's name and content type (owner only)").requiredOption("-n, --name <name>", "New file name").requiredOption("--content-type <mime>", "New MIME content type").action(async (target, options, command) => {
2347
+ await runFileUpdate(await buildWriteContext(command), target, options);
2348
+ });
2349
+ file.command("transfer-ownership <file> <newOwner>").description("Transfer a file's metadata-mutation right (not decrypt access)").action(async (target, newOwner, _options, command) => {
2350
+ await runFileTransferOwnership(await buildWriteContext(command), target, newOwner, globalOptions(command));
2351
+ });
2352
+ file.command("delete <file>").description("Delete a file's metadata record (does not delete the blob)").action(async (target, _options, command) => {
2353
+ await runFileDelete(await buildWriteContext(command), target, globalOptions(command));
1672
2354
  });
1673
- registerEncryptedEntryCommands(entry);
1674
2355
  }
1675
2356
 
1676
2357
  // src/commands/publication.ts
1677
2358
  import {
1678
2359
  createPublication,
1679
2360
  deletePublication,
1680
- toSuiAddress as toSuiAddress4,
2361
+ toSuiAddress as toSuiAddress6,
1681
2362
  transferOwnership
1682
2363
  } from "@arcadiasystems/morse-sdk";
2364
+ async function runPublicationGet(ctx, target) {
2365
+ const id = await resolvePublication(ctx, target);
2366
+ const result = await ctx.reader.getPublication(id, ctx.signal);
2367
+ ctx.output.result(renderPublication(result), result);
2368
+ }
2369
+ async function runPublicationList(ctx, address, options) {
2370
+ const owner = address === undefined ? ctx.ownerAddress : toSuiAddress6(address);
2371
+ if (owner === undefined) {
2372
+ throw new UsageError("No address given and no active account. Pass an address or import an account.");
2373
+ }
2374
+ const page = await ctx.reader.listPublicationsOwnedBy(owner, {
2375
+ signal: ctx.signal,
2376
+ ...options.limit === undefined ? {} : { limit: parseLimit(options.limit) },
2377
+ ...options.cursor === undefined ? {} : { cursor: options.cursor }
2378
+ });
2379
+ if (page.nextCursor !== null) {
2380
+ ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
2381
+ }
2382
+ if (options.idsOnly) {
2383
+ ctx.output.result(renderPublicationList(page.results), {
2384
+ results: page.results,
2385
+ nextCursor: page.nextCursor
2386
+ });
2387
+ return;
2388
+ }
2389
+ const enriched = [];
2390
+ for (const owned of page.results) {
2391
+ const pub = await ctx.reader.getPublication(owned.publicationId, ctx.signal);
2392
+ enriched.push({
2393
+ slug: pub.slug,
2394
+ name: pub.name,
2395
+ publicationId: owned.publicationId,
2396
+ ownerCapId: owned.ownerCapId
2397
+ });
2398
+ }
2399
+ ctx.output.result(renderEnrichedPublicationList(enriched), {
2400
+ results: enriched,
2401
+ nextCursor: page.nextCursor
2402
+ });
2403
+ }
2404
+ async function runPublicationCreate(ctx, options, gopts) {
2405
+ ctx.output.info(`Creating "${options.name}"...`);
2406
+ const result = await createPublication(ctx.adapter, ctx.config, {
2407
+ name: options.name,
2408
+ slug: options.slug,
2409
+ signal: ctx.signal
2410
+ });
2411
+ await updateActiveProfile(gopts, {
2412
+ publication: result.publicationId,
2413
+ collection: undefined
2414
+ });
2415
+ const human = [
2416
+ `Created "${options.name}" (${result.publicationId})`,
2417
+ ` ownerCap: ${result.ownerCapId}`,
2418
+ ` publisherCap: ${result.publisherCapId}`,
2419
+ ` tx: ${result.digest}`,
2420
+ "Selected as the active publication."
2421
+ ].join(`
2422
+ `);
2423
+ ctx.output.result(human, result);
2424
+ }
2425
+ async function runPublicationDelete(ctx, target, options, gopts) {
2426
+ const id = await resolvePublication(ctx, target);
2427
+ const proceed = await confirm(`Delete publication ${shortId(id)}? This cannot be undone.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
2428
+ if (!proceed) {
2429
+ cancelled();
2430
+ }
2431
+ ctx.output.info("Resolving OwnerCap...");
2432
+ const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
2433
+ ctx.output.info("Deleting...");
2434
+ const result = await deletePublication(ctx.reader, ctx.adapter, ctx.config, {
2435
+ publicationId: id,
2436
+ ownerCapId,
2437
+ signal: ctx.signal
2438
+ });
2439
+ if (ctx.settings.publication === id) {
2440
+ await updateActiveProfile(gopts, {
2441
+ publication: undefined,
2442
+ collection: undefined
2443
+ });
2444
+ }
2445
+ ctx.output.result(`Deleted ${id}. (tx: ${result.digest})`, result);
2446
+ }
2447
+ async function runPublicationTransferOwnership(ctx, recipient, options, gopts) {
2448
+ const id = await resolvePublication(ctx, options.publication);
2449
+ const to = toSuiAddress6(recipient);
2450
+ const proceed = await confirm(`Transfer ownership of ${shortId(id)} to ${to}? You will lose owner control.`, { assumeYes: Boolean(gopts.yes), signal: ctx.signal });
2451
+ if (!proceed) {
2452
+ cancelled();
2453
+ }
2454
+ ctx.output.info("Resolving OwnerCap...");
2455
+ const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
2456
+ const result = await transferOwnership(ctx.adapter, ctx.config, {
2457
+ ownerCapId,
2458
+ recipient: to,
2459
+ signal: ctx.signal
2460
+ });
2461
+ ctx.output.result(`Transferred ownership of ${id} to ${to}. (tx: ${result.digest})`, result);
2462
+ }
1683
2463
  function registerPublicationCommands(program) {
1684
2464
  const publication = program.command("publication").alias("pub").description("Work with publications");
1685
2465
  publication.command("get [publication]").description("Fetch a publication (slug or id; default: the active publication)").action(async (target, _options, command) => {
1686
- const ctx = await buildReadContext(command);
1687
- const id = await resolvePublication(ctx, target);
1688
- const result = await ctx.reader.getPublication(id, ctx.signal);
1689
- ctx.output.result(renderPublication(result), result);
2466
+ await runPublicationGet(await buildReadContext(command), target);
1690
2467
  });
1691
2468
  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) => {
1692
- const ctx = await buildReadContext(command);
1693
- const owner = address === undefined ? ctx.ownerAddress : toSuiAddress4(address);
1694
- if (owner === undefined) {
1695
- throw new UsageError("No address given and no active account. Pass an address or import an account.");
1696
- }
1697
- const page = await ctx.reader.listPublicationsOwnedBy(owner, {
1698
- signal: ctx.signal,
1699
- ...options.limit === undefined ? {} : { limit: parseLimit(options.limit) },
1700
- ...options.cursor === undefined ? {} : { cursor: options.cursor }
1701
- });
1702
- if (page.nextCursor !== null) {
1703
- ctx.output.info(`More results: pass --cursor "${page.nextCursor}"`);
1704
- }
1705
- if (options.idsOnly) {
1706
- ctx.output.result(renderPublicationList(page.results), {
1707
- results: page.results,
1708
- nextCursor: page.nextCursor
1709
- });
1710
- return;
1711
- }
1712
- const enriched = [];
1713
- for (const owned of page.results) {
1714
- const pub = await ctx.reader.getPublication(owned.publicationId, ctx.signal);
1715
- enriched.push({
1716
- slug: pub.slug,
1717
- name: pub.name,
1718
- publicationId: owned.publicationId,
1719
- ownerCapId: owned.ownerCapId
1720
- });
1721
- }
1722
- ctx.output.result(renderEnrichedPublicationList(enriched), {
1723
- results: enriched,
1724
- nextCursor: page.nextCursor
1725
- });
2469
+ await runPublicationList(await buildReadContext(command), address, options);
1726
2470
  });
1727
2471
  publication.command("create").description("Create a publication and select it as the active publication").requiredOption("-n, --name <name>", "Publication name").requiredOption("-s, --slug <slug>", "URL slug: lowercase alphanumeric and hyphens, 1-64 chars").action(async (options, command) => {
1728
- const ctx = await buildWriteContext(command);
1729
- ctx.output.info(`Creating "${options.name}"...`);
1730
- const result = await createPublication(ctx.adapter, ctx.config, {
1731
- name: options.name,
1732
- slug: options.slug,
1733
- signal: ctx.signal
1734
- });
1735
- await updateActiveProfile(globalOptions(command), {
1736
- publication: result.publicationId,
1737
- collection: undefined
1738
- });
1739
- const human = [
1740
- `Created "${options.name}" (${result.publicationId})`,
1741
- ` ownerCap: ${result.ownerCapId}`,
1742
- ` publisherCap: ${result.publisherCapId}`,
1743
- ` tx: ${result.digest}`,
1744
- "Selected as the active publication."
1745
- ].join(`
1746
- `);
1747
- ctx.output.result(human, result);
2472
+ await runPublicationCreate(await buildWriteContext(command), options, globalOptions(command));
1748
2473
  });
1749
2474
  publication.command("delete [publication]").description("Delete an empty publication (default: the active publication)").option("--owner-cap <id>", "OwnerCap ID (auto-resolved if omitted)").action(async (target, options, command) => {
1750
- const ctx = await buildWriteContext(command);
1751
- const id = await resolvePublication(ctx, target);
1752
- const proceed = await confirm(`Delete publication ${shortId(id)}? This cannot be undone.`, {
1753
- assumeYes: Boolean(globalOptions(command).yes),
1754
- signal: ctx.signal
1755
- });
1756
- if (!proceed) {
1757
- cancelled();
1758
- }
1759
- ctx.output.info("Resolving OwnerCap...");
1760
- const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
1761
- ctx.output.info("Deleting...");
1762
- const result = await deletePublication(ctx.reader, ctx.adapter, ctx.config, {
1763
- publicationId: id,
1764
- ownerCapId,
1765
- signal: ctx.signal
1766
- });
1767
- if (ctx.settings.publication === id) {
1768
- await updateActiveProfile(globalOptions(command), {
1769
- publication: undefined,
1770
- collection: undefined
1771
- });
1772
- }
1773
- ctx.output.result(`Deleted ${id}. (tx: ${result.digest})`, result);
2475
+ await runPublicationDelete(await buildWriteContext(command), target, options, globalOptions(command));
1774
2476
  });
1775
2477
  const transfer = publication.command("transfer-ownership <recipient>").description("Transfer a publication's OwnerCap to another address");
1776
2478
  publicationOption(ownerCapOption(transfer)).action(async (recipient, options, command) => {
1777
- const ctx = await buildWriteContext(command);
1778
- const id = await resolvePublication(ctx, options.publication);
1779
- const to = toSuiAddress4(recipient);
1780
- const proceed = await confirm(`Transfer ownership of ${shortId(id)} to ${to}? You will lose owner control.`, { assumeYes: Boolean(globalOptions(command).yes), signal: ctx.signal });
1781
- if (!proceed) {
1782
- cancelled();
1783
- }
1784
- ctx.output.info("Resolving OwnerCap...");
1785
- const ownerCapId = await resolveOwnerCap(ctx.reader, ctx.address, id, options.ownerCap, ctx.signal);
1786
- const result = await transferOwnership(ctx.adapter, ctx.config, {
1787
- ownerCapId,
1788
- recipient: to,
1789
- signal: ctx.signal
1790
- });
1791
- ctx.output.result(`Transferred ownership of ${id} to ${to}. (tx: ${result.digest})`, result);
2479
+ await runPublicationTransferOwnership(await buildWriteContext(command), recipient, options, globalOptions(command));
1792
2480
  });
1793
2481
  }
1794
2482
 
@@ -1801,14 +2489,12 @@ import {
1801
2489
  function revisionOptions(command) {
1802
2490
  return collectionOption(publicationOption(publisherCapOption(contentOptions(command))));
1803
2491
  }
1804
- async function resolveTarget(command, options) {
1805
- const ctx = await buildContentContext(command);
2492
+ async function resolveTarget(ctx, options) {
1806
2493
  const publicationId = await resolvePublication(ctx, options.publication);
1807
2494
  const collection = resolveCollection(ctx, options.collection);
1808
- return { ctx, publicationId, collection };
2495
+ return { publicationId, collection };
1809
2496
  }
1810
- async function prepareContent(target, options) {
1811
- const { ctx, publicationId } = target;
2497
+ async function prepareContent(ctx, publicationId, options) {
1812
2498
  const epochs = parsePositiveInt(options.epochs, "--epochs");
1813
2499
  const bytes = await readContentBytes(options);
1814
2500
  const contentType = resolveContentType(options.contentType, options.stdin ? undefined : options.file);
@@ -1821,113 +2507,124 @@ async function prepareContent(target, options) {
1821
2507
  ctx.output.info(`Blob uploaded (${upload.blobObjectId}). Submitting transaction...`);
1822
2508
  return { blobObjectId: upload.blobObjectId, contentType, publisherCapId };
1823
2509
  }
2510
+ async function runRevisionPublishDirect(ctx, entryId, options) {
2511
+ const { publicationId, collection } = await resolveTarget(ctx, options);
2512
+ const numericEntryId = parseId(entryId, "entryId");
2513
+ const prepared = await prepareContent(ctx, publicationId, options);
2514
+ const result = await publishDirect(ctx.adapter, ctx.config, {
2515
+ publicationId,
2516
+ publisherCapId: prepared.publisherCapId,
2517
+ collectionName: collection,
2518
+ entryId: numericEntryId,
2519
+ blobObjectId: prepared.blobObjectId,
2520
+ contentType: prepared.contentType,
2521
+ signal: ctx.signal
2522
+ });
2523
+ ctx.output.result(`Published revision #${result.revisionId} on entry #${numericEntryId}. (tx: ${result.digest})`, result);
2524
+ }
2525
+ async function runRevisionAppendDraft(ctx, entryId, options) {
2526
+ const { publicationId, collection } = await resolveTarget(ctx, options);
2527
+ const numericEntryId = parseId(entryId, "entryId");
2528
+ const prepared = await prepareContent(ctx, publicationId, options);
2529
+ const result = await appendDraftRevision(ctx.adapter, ctx.config, {
2530
+ publicationId,
2531
+ publisherCapId: prepared.publisherCapId,
2532
+ collectionName: collection,
2533
+ entryId: numericEntryId,
2534
+ blobObjectId: prepared.blobObjectId,
2535
+ contentType: prepared.contentType,
2536
+ signal: ctx.signal
2537
+ });
2538
+ ctx.output.result(`Appended draft revision #${result.revisionId} on entry #${numericEntryId}. (tx: ${result.digest})`, result);
2539
+ }
2540
+ async function runRevisionPublishFromDraft(ctx, entryId, draftRevisionId, options) {
2541
+ const { publicationId, collection } = await resolveTarget(ctx, options);
2542
+ const numericEntryId = parseId(entryId, "entryId");
2543
+ const draftId = parseId(draftRevisionId, "draftRevisionId");
2544
+ const prepared = await prepareContent(ctx, publicationId, options);
2545
+ const result = await publishFromDraft(ctx.adapter, ctx.config, {
2546
+ publicationId,
2547
+ publisherCapId: prepared.publisherCapId,
2548
+ collectionName: collection,
2549
+ entryId: numericEntryId,
2550
+ draftRevisionId: draftId,
2551
+ blobObjectId: prepared.blobObjectId,
2552
+ contentType: prepared.contentType,
2553
+ signal: ctx.signal
2554
+ });
2555
+ ctx.output.result(`Published revision #${result.revisionId} from draft #${draftId} on entry #${numericEntryId}. (tx: ${result.digest})`, result);
2556
+ }
1824
2557
  function registerRevisionCommands(program) {
1825
2558
  const revision = program.command("revision").description("Append or publish revisions on an entry");
1826
2559
  revisionOptions(revision.command("publish-direct <entryId>").description("Upload content and append it as a public revision")).action(async (entryId, options, command) => {
1827
- const target = await resolveTarget(command, options);
1828
- const numericEntryId = parseId(entryId, "entryId");
1829
- const prepared = await prepareContent(target, options);
1830
- const result = await publishDirect(target.ctx.adapter, target.ctx.config, {
1831
- publicationId: target.publicationId,
1832
- publisherCapId: prepared.publisherCapId,
1833
- collectionName: target.collection,
1834
- entryId: numericEntryId,
1835
- blobObjectId: prepared.blobObjectId,
1836
- contentType: prepared.contentType,
1837
- signal: target.ctx.signal
1838
- });
1839
- target.ctx.output.result(`Published revision #${result.revisionId} on entry #${numericEntryId}. (tx: ${result.digest})`, result);
2560
+ await runRevisionPublishDirect(await buildContentContext(command), entryId, options);
1840
2561
  });
1841
2562
  revisionOptions(revision.command("append-draft <entryId>").description("Upload content and append it as a draft revision")).action(async (entryId, options, command) => {
1842
- const target = await resolveTarget(command, options);
1843
- const numericEntryId = parseId(entryId, "entryId");
1844
- const prepared = await prepareContent(target, options);
1845
- const result = await appendDraftRevision(target.ctx.adapter, target.ctx.config, {
1846
- publicationId: target.publicationId,
1847
- publisherCapId: prepared.publisherCapId,
1848
- collectionName: target.collection,
1849
- entryId: numericEntryId,
1850
- blobObjectId: prepared.blobObjectId,
1851
- contentType: prepared.contentType,
1852
- signal: target.ctx.signal
1853
- });
1854
- target.ctx.output.result(`Appended draft revision #${result.revisionId} on entry #${numericEntryId}. (tx: ${result.digest})`, result);
2563
+ await runRevisionAppendDraft(await buildContentContext(command), entryId, options);
1855
2564
  });
1856
2565
  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) => {
1857
- const target = await resolveTarget(command, options);
1858
- const numericEntryId = parseId(entryId, "entryId");
1859
- const draftId = parseId(draftRevisionId, "draftRevisionId");
1860
- const prepared = await prepareContent(target, options);
1861
- const result = await publishFromDraft(target.ctx.adapter, target.ctx.config, {
1862
- publicationId: target.publicationId,
1863
- publisherCapId: prepared.publisherCapId,
1864
- collectionName: target.collection,
1865
- entryId: numericEntryId,
1866
- draftRevisionId: draftId,
1867
- blobObjectId: prepared.blobObjectId,
1868
- contentType: prepared.contentType,
1869
- signal: target.ctx.signal
1870
- });
1871
- target.ctx.output.result(`Published revision #${result.revisionId} from draft #${draftId} on entry #${numericEntryId}. (tx: ${result.digest})`, result);
2566
+ await runRevisionPublishFromDraft(await buildContentContext(command), entryId, draftRevisionId, options);
1872
2567
  });
1873
2568
  }
1874
2569
 
1875
2570
  // src/commands/use.ts
2571
+ async function runUse(output, gopts, publication, collection, options, makeContext) {
2572
+ if (options.clear) {
2573
+ const profile2 = await updateActiveProfile(gopts, {
2574
+ publication: undefined,
2575
+ collection: undefined
2576
+ });
2577
+ output.result(`Cleared the active publication and collection (profile "${profile2}").`, { profile: profile2, publication: null, collection: null });
2578
+ return;
2579
+ }
2580
+ if (publication === undefined) {
2581
+ throw new UsageError("Provide a publication (slug or id), or pass --clear.");
2582
+ }
2583
+ const ctx = await makeContext();
2584
+ const publicationId = await resolvePublication(ctx, publication);
2585
+ let collectionName;
2586
+ if (collection !== undefined) {
2587
+ const pub = await ctx.reader.getPublication(publicationId, ctx.signal);
2588
+ if (!pub.collections.some((c) => c.name === collection)) {
2589
+ throw new UsageError(`Publication ${publicationId} has no collection "${collection}".`);
2590
+ }
2591
+ collectionName = collection;
2592
+ }
2593
+ const profile = await updateActiveProfile(gopts, {
2594
+ publication: publicationId,
2595
+ collection: collectionName
2596
+ });
2597
+ const human = collectionName === undefined ? `Active publication set to ${publicationId} (profile "${profile}").` : `Active publication set to ${publicationId}, collection "${collectionName}" (profile "${profile}").`;
2598
+ output.result(human, {
2599
+ profile,
2600
+ publication: publicationId,
2601
+ collection: collectionName ?? null
2602
+ });
2603
+ }
2604
+ function runStatus(output, settings) {
2605
+ const account = accountAddress(settings.account);
2606
+ const human = [
2607
+ `profile: ${settings.profileName}`,
2608
+ `network: ${settings.network}`,
2609
+ `account: ${account ?? "(none)"}`,
2610
+ `publication: ${settings.publication ?? "(none)"}`,
2611
+ `collection: ${settings.collection ?? "(none)"}`
2612
+ ].join(`
2613
+ `);
2614
+ output.result(human, {
2615
+ profile: settings.profileName,
2616
+ network: settings.network,
2617
+ account: account ?? null,
2618
+ publication: settings.publication ?? null,
2619
+ collection: settings.collection ?? null
2620
+ });
2621
+ }
1876
2622
  function registerContextCommands(program) {
1877
2623
  program.command("use [publication] [collection]").description("Set the active publication (slug or id) and optional collection. Omitting the collection clears any active collection.").option("--clear", "Clear the active publication and collection").action(async (publication, collection, options, command) => {
1878
- const opts = globalOptions(command);
1879
- const output = outputFor(command);
1880
- if (options.clear) {
1881
- const profile2 = await updateActiveProfile(opts, {
1882
- publication: undefined,
1883
- collection: undefined
1884
- });
1885
- output.result(`Cleared the active publication and collection (profile "${profile2}").`, { profile: profile2, publication: null, collection: null });
1886
- return;
1887
- }
1888
- if (publication === undefined) {
1889
- throw new UsageError("Provide a publication (slug or id), or pass --clear.");
1890
- }
1891
- const ctx = await buildReadContext(command);
1892
- const publicationId = await resolvePublication(ctx, publication);
1893
- let collectionName;
1894
- if (collection !== undefined) {
1895
- const pub = await ctx.reader.getPublication(publicationId, ctx.signal);
1896
- if (!pub.collections.some((c) => c.name === collection)) {
1897
- throw new UsageError(`Publication ${publicationId} has no collection "${collection}".`);
1898
- }
1899
- collectionName = collection;
1900
- }
1901
- const profile = await updateActiveProfile(opts, {
1902
- publication: publicationId,
1903
- collection: collectionName
1904
- });
1905
- const human = collectionName === undefined ? `Active publication set to ${publicationId} (profile "${profile}").` : `Active publication set to ${publicationId}, collection "${collectionName}" (profile "${profile}").`;
1906
- output.result(human, {
1907
- profile,
1908
- publication: publicationId,
1909
- collection: collectionName ?? null
1910
- });
2624
+ await runUse(outputFor(command), globalOptions(command), publication, collection, options, () => buildReadContext(command));
1911
2625
  });
1912
2626
  program.command("status").description("Show the active profile, network, account, publication, and collection").action(async (_options, command) => {
1913
- const output = outputFor(command);
1914
- const settings = resolveSettings(globalOptions(command), await loadConfig());
1915
- const account = accountAddress(settings.account);
1916
- const human = [
1917
- `profile: ${settings.profileName}`,
1918
- `network: ${settings.network}`,
1919
- `account: ${account ?? "(none)"}`,
1920
- `publication: ${settings.publication ?? "(none)"}`,
1921
- `collection: ${settings.collection ?? "(none)"}`
1922
- ].join(`
1923
- `);
1924
- output.result(human, {
1925
- profile: settings.profileName,
1926
- network: settings.network,
1927
- account: account ?? null,
1928
- publication: settings.publication ?? null,
1929
- collection: settings.collection ?? null
1930
- });
2627
+ runStatus(outputFor(command), resolveSettings(globalOptions(command), await loadConfig()));
1931
2628
  });
1932
2629
  }
1933
2630
 
@@ -1941,6 +2638,8 @@ function registerCommands(program) {
1941
2638
  registerEntryCommands(program);
1942
2639
  registerRevisionCommands(program);
1943
2640
  registerCapCommands(program);
2641
+ registerAllowlistCommands(program);
2642
+ registerFileCommands(program);
1944
2643
  }
1945
2644
 
1946
2645
  // src/index.ts