@shelby-protocol/cli 0.0.13 → 0.0.14

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.
Files changed (2) hide show
  1. package/bin/entry.js +784 -372
  2. package/package.json +8 -6
package/bin/entry.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // package.json
7
- var version = "0.0.13";
7
+ var version = "0.0.14";
8
8
 
9
9
  // src/commands/account.tsx
10
10
  import readline from "readline";
@@ -68,7 +68,7 @@ import {
68
68
 
69
69
  // ../../packages/sdk/dist/chunk-I6NG5GNL.mjs
70
70
  function sleep(ms) {
71
- return new Promise((resolve2) => setTimeout(resolve2, ms));
71
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
72
72
  }
73
73
 
74
74
  // ../../node_modules/.pnpm/tslib@2.8.1/node_modules/tslib/tslib.es6.mjs
@@ -3270,7 +3270,7 @@ var extras = {
3270
3270
  gql["default"] = gql;
3271
3271
  var lib_default = gql;
3272
3272
 
3273
- // ../../packages/sdk/dist/chunk-DLMDDEWF.mjs
3273
+ // ../../packages/sdk/dist/chunk-WJKSPJSS.mjs
3274
3274
  var GetBlobsDocument = lib_default`
3275
3275
  query getBlobs($where: blobs_bool_exp, $orderBy: [blobs_order_by!], $limit: Int, $offset: Int) {
3276
3276
  blobs(where: $where, order_by: $orderBy, limit: $limit, offset: $offset) {
@@ -3306,6 +3306,24 @@ var GetBlobActivitiesDocument = lib_default`
3306
3306
  }
3307
3307
  }
3308
3308
  `;
3309
+ var GetBlobsCountDocument = lib_default`
3310
+ query getBlobsCount($where: blobs_bool_exp) {
3311
+ blobs_aggregate(where: $where) {
3312
+ aggregate {
3313
+ count
3314
+ }
3315
+ }
3316
+ }
3317
+ `;
3318
+ var GetBlobActivitiesCountDocument = lib_default`
3319
+ query getBlobActivitiesCount($where: blob_activities_bool_exp) {
3320
+ blob_activities_aggregate(where: $where) {
3321
+ aggregate {
3322
+ count
3323
+ }
3324
+ }
3325
+ }
3326
+ `;
3309
3327
  var defaultWrapper = (action, _operationName, _operationType, _variables) => action();
3310
3328
  function getSdk(client, withWrapper = defaultWrapper) {
3311
3329
  return {
@@ -3314,11 +3332,17 @@ function getSdk(client, withWrapper = defaultWrapper) {
3314
3332
  },
3315
3333
  getBlobActivities(variables, requestHeaders, signal) {
3316
3334
  return withWrapper((wrappedRequestHeaders) => client.request({ document: GetBlobActivitiesDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), "getBlobActivities", "query", variables);
3335
+ },
3336
+ getBlobsCount(variables, requestHeaders, signal) {
3337
+ return withWrapper((wrappedRequestHeaders) => client.request({ document: GetBlobsCountDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), "getBlobsCount", "query", variables);
3338
+ },
3339
+ getBlobActivitiesCount(variables, requestHeaders, signal) {
3340
+ return withWrapper((wrappedRequestHeaders) => client.request({ document: GetBlobActivitiesCountDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), "getBlobActivitiesCount", "query", variables);
3317
3341
  }
3318
3342
  };
3319
3343
  }
3320
3344
 
3321
- // ../../packages/sdk/dist/chunk-VPT45MTZ.mjs
3345
+ // ../../packages/sdk/dist/chunk-T6TVHFJO.mjs
3322
3346
  import { Network as Network3 } from "@aptos-labs/ts-sdk";
3323
3347
 
3324
3348
  // ../../node_modules/.pnpm/graphql-request@7.2.0_graphql@16.11.0/node_modules/graphql-request/build/legacy/classes/ClientError.js
@@ -3833,7 +3857,7 @@ var parseRequestArgs = (documentOrOptions, variables, requestHeaders) => {
3833
3857
  };
3834
3858
  };
3835
3859
 
3836
- // ../../packages/sdk/dist/chunk-VPT45MTZ.mjs
3860
+ // ../../packages/sdk/dist/chunk-T6TVHFJO.mjs
3837
3861
  function createShelbyIndexerClient(baseUrl, options) {
3838
3862
  const graphqlClient = new GraphQLClient(baseUrl, options);
3839
3863
  return getSdk(graphqlClient);
@@ -3866,7 +3890,7 @@ function getShelbyIndexerClient(config) {
3866
3890
  });
3867
3891
  }
3868
3892
 
3869
- // ../../packages/sdk/dist/chunk-KBUWZXFA.mjs
3893
+ // ../../packages/sdk/dist/chunk-4JZO2D7T.mjs
3870
3894
  import { Hex as Hex2 } from "@aptos-labs/ts-sdk";
3871
3895
  async function* readInChunks(input, chunkSize) {
3872
3896
  let idx = 0;
@@ -3951,6 +3975,10 @@ function buildRequestUrl(path6, baseUrl) {
3951
3975
  const safePath = path6.replace(/^\/+/, "");
3952
3976
  return new URL(safePath, safeBase);
3953
3977
  }
3978
+ function getBlobNameSuffix(blobName) {
3979
+ const parts = blobName.split("/");
3980
+ return parts.slice(1).join("/") || "";
3981
+ }
3954
3982
 
3955
3983
  // ../../packages/sdk/dist/chunk-ZPW742E7.mjs
3956
3984
  var ERASURE_CODE_PARAMS = {
@@ -4138,7 +4166,7 @@ var BlobNameSchema = z.string().min(1, "Blob name path parameter cannot be empty
4138
4166
  message: "Blob name cannot end with a slash"
4139
4167
  });
4140
4168
 
4141
- // ../../packages/sdk/dist/chunk-TEDFWEY6.mjs
4169
+ // ../../packages/sdk/dist/chunk-L7H6EKII.mjs
4142
4170
  import { AccountAddress as AccountAddress3 } from "@aptos-labs/ts-sdk";
4143
4171
  function encodeURIComponentKeepSlashes(str) {
4144
4172
  return encodeURIComponent(str).replace(/%2F/g, "/");
@@ -4408,7 +4436,7 @@ var createBlobKey = (params) => {
4408
4436
  return `@${AccountAddress4.from(params.account).toStringLongWithoutPrefix()}/${params.blobName}`;
4409
4437
  };
4410
4438
 
4411
- // ../../packages/sdk/dist/chunk-LTV26KU4.mjs
4439
+ // ../../packages/sdk/dist/chunk-PCNLFNAT.mjs
4412
4440
  import { z as z2 } from "zod";
4413
4441
  var ChunksetCommitmentSchema = z2.object({
4414
4442
  // Chunkset root (vector commitment of child chunks)
@@ -4530,7 +4558,7 @@ function validatePrePaddedChunkset(chunkset, expectedSize, chunksetIdx) {
4530
4558
  return chunkset;
4531
4559
  }
4532
4560
 
4533
- // ../../packages/sdk/dist/chunk-MEINKPI3.mjs
4561
+ // ../../packages/sdk/dist/chunk-Y7KTNPPR.mjs
4534
4562
  import {
4535
4563
  AccountAddress as AccountAddress5,
4536
4564
  Aptos as Aptos2,
@@ -4626,6 +4654,7 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
4626
4654
  ).toUint8Array(),
4627
4655
  owner: AccountAddress5.fromString(metadata.owner),
4628
4656
  name: params.name,
4657
+ blobNameSuffix: getBlobNameSuffix(params.name),
4629
4658
  size: Number(metadata.blob_size),
4630
4659
  encoding,
4631
4660
  expirationMicros: metadata.expiration_micros,
@@ -4658,13 +4687,14 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
4658
4687
  * ```
4659
4688
  */
4660
4689
  getAccountBlobs(params) {
4690
+ const { where, ...rest } = params;
4661
4691
  return this.getBlobs({
4662
4692
  where: {
4663
- ...params.where,
4693
+ ...where ?? {},
4664
4694
  owner: { _eq: AccountAddress5.from(params.account).toString() }
4665
4695
  },
4666
- pagination: params.pagination,
4667
- orderBy: params.orderBy
4696
+ pagination: rest.pagination,
4697
+ orderBy: rest.orderBy
4668
4698
  });
4669
4699
  }
4670
4700
  /**
@@ -4686,8 +4716,14 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
4686
4716
  async getBlobs(params = {}) {
4687
4717
  const { limit, offset } = params.pagination ?? {};
4688
4718
  const { orderBy, where } = params;
4719
+ const currentMicros = Date.now() * 1e3;
4720
+ const defaultActiveFilter = {
4721
+ expires_at: { _gte: currentMicros },
4722
+ is_deleted: { _eq: "0" }
4723
+ };
4724
+ const finalWhere = where !== void 0 ? { ...defaultActiveFilter, ...where } : defaultActiveFilter;
4689
4725
  const { blobs } = await this.indexer.getBlobs({
4690
- where,
4726
+ where: finalWhere,
4691
4727
  limit,
4692
4728
  offset,
4693
4729
  orderBy
@@ -4696,6 +4732,7 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
4696
4732
  (blob) => ({
4697
4733
  owner: AccountAddress5.from(blob.owner),
4698
4734
  name: blob.blob_name,
4735
+ blobNameSuffix: getBlobNameSuffix(blob.blob_name),
4699
4736
  blobMerkleRoot: Hex3.fromHexInput(blob.blob_commitment).toUint8Array(),
4700
4737
  size: Number(blob.size),
4701
4738
  // TODO: Add encoding when supported in NCI
@@ -4741,28 +4778,36 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
4741
4778
  );
4742
4779
  }
4743
4780
  /**
4744
- * Retrieves the blob chunks for a given blob from the blockchain. The blob chunk will contain
4745
- * the commitment, the storage provider location, and the status of the chunk (stored or pending).
4781
+ * Retrieves the total number of blobs from the blockchain.
4746
4782
  *
4747
- * @deprecated Will need to get reworked when we are acking blobs from storage providers.
4748
- *
4749
- * @param params.account - The account namespace the blob is stored in (e.g. "0x1")
4750
- * @param params.name - The name of the blob (e.g. "foo/bar")
4751
- * @returns The chunks that make up the blob.
4783
+ * @param params.where (optional) - The where clause to filter the blobs by.
4784
+ * @returns The total number of blobs.
4752
4785
  *
4753
4786
  * @example
4754
4787
  * ```typescript
4755
- * // BlobChunk[]
4756
- * const chunks = await client.getBlobChunks({
4757
- * account: AccountAddress.fromString("0x1"),
4758
- * name: "foo/bar.txt",
4759
- * });
4788
+ * const count = await client.getBlobsCount();
4789
+ * ```
4790
+ */
4791
+ async getBlobsCount(params) {
4792
+ const { where } = params;
4793
+ const { blobs_aggregate } = await this.indexer.getBlobsCount({ where });
4794
+ return blobs_aggregate?.aggregate?.count ?? 0;
4795
+ }
4796
+ /**
4797
+ * Retrieves the total number of blob activities from the blockchain.
4798
+ *
4799
+ * @param params.where (optional) - The where clause to filter the blob activities by.
4800
+ * @returns The total number of blob activities.
4760
4801
  *
4761
- * const isStored = chunks.every((c) => c.location.variant === "stored");
4802
+ * @example
4803
+ * ```typescript
4804
+ * const count = await client.getBlobActivitiesCount();
4762
4805
  * ```
4763
4806
  */
4764
- getBlobChunks(_params) {
4765
- throw new Error("Not implemented");
4807
+ async getBlobActivitiesCount(params) {
4808
+ const { where } = params;
4809
+ const { blob_activities_aggregate } = await this.indexer.getBlobActivitiesCount({ where });
4810
+ return blob_activities_aggregate?.aggregate?.count ?? 0;
4766
4811
  }
4767
4812
  /**
4768
4813
  * Registers a blob on the blockchain by writing its merkle root and metadata.
@@ -4889,9 +4934,26 @@ var ShelbyBlobClient = class _ShelbyBlobClient {
4889
4934
  ]
4890
4935
  };
4891
4936
  }
4937
+ /**
4938
+ * Creates a transaction payload to delete a blob on the blockchain.
4939
+ * This is a static helper method for constructing the Move function call payload.
4940
+ *
4941
+ * @param params.deployer - Optional deployer account address. Defaults to SHELBY_DEPLOYER.
4942
+ * @param params.blobNameSuffix - The blob name suffix (e.g. "bar.txt").
4943
+ *
4944
+ * @returns An Aptos transaction payload data object for the delete_blob Move function.
4945
+ *
4946
+ * @see https://github.com/shelby/shelby/blob/64e9d7b4f0005e586faeb1e4085c79159234b6b6/move/shelby_contract/sources/global_metadata.move#L616
4947
+ */
4948
+ static createDeleteBlobPayload(params) {
4949
+ return {
4950
+ function: `${(params.deployer ?? SHELBY_DEPLOYER).toString()}::global_metadata::delete_blob`,
4951
+ functionArguments: [params.blobNameSuffix]
4952
+ };
4953
+ }
4892
4954
  };
4893
4955
 
4894
- // ../../packages/sdk/dist/chunk-2Y3ZLUYT.mjs
4956
+ // ../../packages/sdk/dist/chunk-TPGMXZRD.mjs
4895
4957
  import {
4896
4958
  Aptos as Aptos3
4897
4959
  } from "@aptos-labs/ts-sdk";
@@ -5046,7 +5108,7 @@ var ShelbyClient = class {
5046
5108
  }
5047
5109
  };
5048
5110
 
5049
- // ../../packages/sdk/dist/chunk-X56YUBBI.mjs
5111
+ // ../../packages/sdk/dist/chunk-2DD6Q5CK.mjs
5050
5112
  var ShelbyNodeClient = class extends ShelbyClient {
5051
5113
  };
5052
5114
 
@@ -5154,6 +5216,7 @@ var DEFAULT_CHUNK_SIZE_BYTES2 = 2 * 1024 * 1024;
5154
5216
 
5155
5217
  // src/commands/account.tsx
5156
5218
  import chalk from "chalk";
5219
+ import { filesize } from "filesize";
5157
5220
  import { render } from "ink";
5158
5221
  import { z as z9 } from "zod";
5159
5222
 
@@ -6266,6 +6329,9 @@ function getErasureCodingProvider() {
6266
6329
 
6267
6330
  // src/utils/paths.ts
6268
6331
  import path2 from "path";
6332
+ function endsWithDirectorySeparator(filePath) {
6333
+ return filePath.endsWith("/") || filePath.endsWith("\\") || filePath.endsWith(path2.sep);
6334
+ }
6269
6335
  function normBlobName(pathModule, inputDirectoryName, filename, blobPrefix) {
6270
6336
  const blobNameWithoutInputDir = pathModule.relative(
6271
6337
  inputDirectoryName,
@@ -7029,10 +7095,10 @@ function askQuestion(question) {
7029
7095
  input: process.stdin,
7030
7096
  output: process.stdout
7031
7097
  });
7032
- return new Promise((resolve2) => {
7098
+ return new Promise((resolve3) => {
7033
7099
  rl.question(question, (answer) => {
7034
7100
  rl.close();
7035
- resolve2(answer.trim());
7101
+ resolve3(answer.trim());
7036
7102
  });
7037
7103
  });
7038
7104
  }
@@ -7235,25 +7301,33 @@ function accountCommand(program) {
7235
7301
  });
7236
7302
  const activeAccount = getCurrentAccount(config, accountName).account;
7237
7303
  const shelbyClient = new ShelbyNodeClient(shelbyConfig);
7238
- console.log(
7239
- `\u{1F50D} Retrieving blobs for ${accountName} (${activeAccount.accountAddress.toString()})`
7304
+ console.log(`\u{1F50D} Retrieving blobs for ${accountName}`);
7305
+ console.log(`\u{1F464} Address: ${activeAccount.accountAddress.toString()}`);
7306
+ const shelbyExplorerUrl = getShelbyAccountExplorerUrl(
7307
+ shelbyConfig.network,
7308
+ activeAccount.accountAddress.toString()
7240
7309
  );
7310
+ console.log(`\u{1F5C2}\uFE0F Shelby Explorer: ${shelbyExplorerUrl}`);
7241
7311
  const blobs = await shelbyClient.coordination.getBlobs({
7242
7312
  where: {
7243
7313
  owner: { _eq: activeAccount.accountAddress.toString() }
7244
7314
  }
7245
7315
  });
7246
7316
  console.log(
7247
- `\u2705 Retrieved ${blobs.length} blob${blobs.length === 1 ? "" : "s"} for ${accountName} (${activeAccount.accountAddress.toString()})`
7317
+ `
7318
+ \u2705 Retrieved ${blobs.length} blob${blobs.length === 1 ? "" : "s"}`
7248
7319
  );
7320
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
7249
7321
  if (blobs.length === 0) {
7250
7322
  console.log("No blobs found.");
7323
+ console.log("\u2728 Done!");
7251
7324
  return;
7252
7325
  }
7326
+ console.log("\u{1F4E6} Stored Blobs");
7253
7327
  const { table, wrapCell } = createResponsiveTable({
7254
7328
  columns: [
7255
7329
  { header: "Name", width: 30, flex: true },
7256
- { header: "Size (bytes)", width: 15 },
7330
+ { header: "Size", width: 12 },
7257
7331
  { header: "Expires", width: 25 }
7258
7332
  ],
7259
7333
  padding: 12
@@ -7262,14 +7336,20 @@ function accountCommand(program) {
7262
7336
  const expirationDate = new Date(blob.expirationMicros / 1e3);
7263
7337
  const blobNameMatch = blob.name.match(/@[^/]+\/(.+)/);
7264
7338
  const displayName = blobNameMatch ? blobNameMatch[1] : blob.name;
7265
- const row = [
7266
- displayName,
7267
- blob.size.toString(),
7268
- expirationDate.toLocaleString()
7269
- ];
7339
+ const formattedSize = filesize(blob.size, { standard: "jedec" });
7340
+ const formattedDate = expirationDate.toLocaleDateString("en-US", {
7341
+ month: "short",
7342
+ day: "numeric",
7343
+ year: "numeric",
7344
+ hour: "numeric",
7345
+ minute: "2-digit",
7346
+ hour12: true
7347
+ });
7348
+ const row = [displayName, formattedSize, formattedDate];
7270
7349
  table.push(row.map((cell, i) => wrapCell(cell, i)));
7271
7350
  }
7272
7351
  console.log(table.toString());
7352
+ console.log("\u2728 Done!");
7273
7353
  } catch (error) {
7274
7354
  let displayMessage;
7275
7355
  if (error instanceof Error && error.message.includes("GraphQL Error (Code: 401)")) {
@@ -7776,52 +7856,99 @@ import { pipeline } from "stream/promises";
7776
7856
  import ora from "ora";
7777
7857
  import { z as z13 } from "zod";
7778
7858
  var denormBlobName2 = (a, b, c) => denormBlobName(path3, a, b, c);
7779
- var endsWithDirectorySeparator = (filePath) => {
7780
- return filePath.endsWith("/") || filePath.endsWith("\\") || filePath.endsWith(path3.sep);
7781
- };
7782
7859
  var DownloadOptionsSchema = z13.object({
7783
- src: z13.string().min(1, "`src`must be a valid blob name or directory prefix").describe("Blob name or directory prefix to download"),
7784
- dst: z13.string().min(1, "`dst` must be a valid filepath").describe("Local path where to save the downloaded content"),
7860
+ source: z13.string({
7861
+ required_error: "\n\u274C Missing Required Argument\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\u26A0\uFE0F Missing source blob name or prefix (first argument)\n\n\u{1F4A1} Usage:\n shelby download <source-blob-name> <destination-path> [options]\n\n\u{1F4DD} Examples:\n shelby download my-blob.txt ./myfile.txt\n shelby download my-folder/ ./my-folder/ -r\n"
7862
+ }).min(1, "Source blob name or directory prefix is required").describe("Blob name or directory prefix to download"),
7863
+ destination: z13.string({
7864
+ required_error: "\n\u274C Missing Required Argument\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\u26A0\uFE0F Missing destination path (second argument)\n\n\u{1F4A1} Usage:\n shelby download <source-blob-name> <destination-path> [options]\n\n\u{1F4DD} Example:\n shelby download my-blob.txt ./myfile.txt\n"
7865
+ }).min(1, "`destination` must be a valid filepath").describe("Local path where to save the downloaded content"),
7785
7866
  recursive: z13.boolean().default(false).describe("Download assuming canonical directory layout and recurse"),
7786
- force: z13.boolean().default(false).describe("Overwrite the dst if it already exists")
7867
+ force: z13.boolean().default(false).describe("Overwrite the destination if it already exists")
7787
7868
  }).refine(
7788
7869
  (data) => {
7789
7870
  if (data.recursive) {
7790
- if (!data.src.endsWith("/") || !endsWithDirectorySeparator(data.dst))
7871
+ if (!data.source.endsWith("/") || !endsWithDirectorySeparator(data.destination))
7791
7872
  return false;
7792
7873
  return true;
7793
7874
  }
7794
7875
  return true;
7795
7876
  },
7796
- {
7797
- message: "Recursive downloads require src prefix and dst name to end in directory separator ('/' or '\\')"
7798
- }
7877
+ (data) => ({
7878
+ message: `
7879
+ \u274C Invalid Download Options
7880
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
7881
+
7882
+ \u26A0\uFE0F Recursive downloads require:
7883
+ - The source prefix must end with '/'
7884
+ - The destination path must end with '/' or '\\'
7885
+
7886
+ \u{1F9FE} Received:
7887
+ Source: ${data.source}
7888
+ Destination: ${data.destination}
7889
+
7890
+ \u{1F4A1} Tip: Add trailing slashes to both paths:
7891
+ \u{1F449} shelby download --recursive ${data.source.endsWith("/") ? data.source : `${data.source}/`} ${endsWithDirectorySeparator(data.destination) ? data.destination : `${data.destination}/`}
7892
+ `
7893
+ })
7799
7894
  ).refine(
7800
7895
  (data) => {
7801
7896
  if (!data.recursive) {
7802
- if (!BlobNameSchema.safeParse(data.src).success) return false;
7803
- if (endsWithDirectorySeparator(data.dst)) return false;
7897
+ if (!BlobNameSchema.safeParse(data.source).success) return false;
7898
+ if (endsWithDirectorySeparator(data.destination)) return false;
7804
7899
  return true;
7805
7900
  }
7806
7901
  return true;
7807
7902
  },
7808
- {
7809
- message: "Blob downloads, non-recursive, require that src blob name and dst path not end in directory separator ('/' or '\\')"
7903
+ (data) => {
7904
+ const sourceEndsWithSlash = data.source.endsWith("/") || data.source.endsWith("\\");
7905
+ const destEndsWithSlash = endsWithDirectorySeparator(data.destination);
7906
+ let tipMessage = "";
7907
+ if (sourceEndsWithSlash && destEndsWithSlash) {
7908
+ tipMessage = `\u{1F4A1} Tip: For downloading a directory, use the --recursive flag:
7909
+ \u{1F449} shelby download --recursive ${data.source} ${data.destination}`;
7910
+ } else if (sourceEndsWithSlash && !destEndsWithSlash) {
7911
+ tipMessage = `\u{1F4A1} Tip: For downloading a directory, both paths need trailing slashes:
7912
+ \u{1F449} shelby download --recursive ${data.source} ${data.destination}/`;
7913
+ } else if (!sourceEndsWithSlash && destEndsWithSlash) {
7914
+ tipMessage = `\u{1F4A1} Choose one of the following:
7915
+
7916
+ To download a single blob (remove trailing slash from destination):
7917
+ \u{1F449} shelby download ${data.source} ${data.destination.replace(/[\\/]+$/, "")}
7918
+
7919
+ To download a directory recursively (add trailing slash to source):
7920
+ \u{1F449} shelby download --recursive ${data.source}/ ${data.destination}`;
7921
+ }
7922
+ return {
7923
+ message: `
7924
+ \u274C Invalid Download Options
7925
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
7926
+
7927
+ \u26A0\uFE0F Blob downloads (non-recursive) require:
7928
+ - The source blob name must NOT end with '/' or '\\'
7929
+ - The destination path must NOT end with '/' or '\\'
7930
+
7931
+ \u{1F9FE} Received:
7932
+ Source: ${data.source}
7933
+ Destination: ${data.destination}
7934
+
7935
+ ` + tipMessage + "\n"
7936
+ };
7810
7937
  }
7811
7938
  );
7812
7939
  async function validateOutput(options) {
7813
- const parentDir = path3.dirname(options.dst);
7940
+ const parentDir = path3.dirname(options.destination);
7814
7941
  try {
7815
7942
  const parentStats = await fs3.stat(parentDir);
7816
7943
  if (!parentStats.isDirectory()) {
7817
7944
  throw new Error(
7818
- `Parent path '${parentDir}' exists but is not a directory. Cannot create dst ${options.recursive ? "directory" : "file"} '${options.dst}'.`
7945
+ `Parent path '${parentDir}' exists but is not a directory. Cannot create destination ${options.recursive ? "directory" : "file"} '${options.destination}'.`
7819
7946
  );
7820
7947
  }
7821
7948
  } catch (error) {
7822
7949
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
7823
7950
  throw new Error(
7824
- `Parent directory '${parentDir}' of dst '${options.dst}' does not exist. Create it first or use a different dst path.`
7951
+ `Parent directory '${parentDir}' of destination '${options.destination}' does not exist. Create it first or use a different destination path.`
7825
7952
  );
7826
7953
  }
7827
7954
  throw error;
@@ -7829,7 +7956,7 @@ async function validateOutput(options) {
7829
7956
  if (options.force) return;
7830
7957
  let outputStats;
7831
7958
  try {
7832
- outputStats = await fs3.stat(options.dst);
7959
+ outputStats = await fs3.stat(options.destination);
7833
7960
  } catch (error) {
7834
7961
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
7835
7962
  return;
@@ -7839,64 +7966,73 @@ async function validateOutput(options) {
7839
7966
  const isDirectory = outputStats.isDirectory();
7840
7967
  if (!options.recursive && isDirectory) {
7841
7968
  throw new Error(
7842
- `dst path '${options.dst}' is a directory but expected a file path to download a single blob (--recursive not provided).`
7969
+ `destination path '${options.destination}' is a directory but expected a file path to download a single blob (--recursive not provided).`
7843
7970
  );
7844
7971
  }
7845
7972
  if (options.recursive && !isDirectory) {
7846
7973
  throw new Error(
7847
- `dst path '${options.dst}' exists but is not a directory. Directory downloads require a directory path.`
7974
+ `destination path '${options.destination}' exists but is not a directory. Directory downloads require a directory path.`
7848
7975
  );
7849
7976
  }
7850
7977
  if (options.recursive) {
7851
- const entries = await fs3.readdir(options.dst);
7978
+ const entries = await fs3.readdir(options.destination);
7852
7979
  if (entries.length > 0) {
7853
7980
  throw new Error(
7854
- `Directory '${options.dst}' exists and is not empty. Use --force to overwrite or choose an empty directory.`
7981
+ `Directory '${options.destination}' exists and is not empty. Use --force to overwrite or choose an empty directory.`
7855
7982
  );
7856
7983
  }
7857
7984
  } else {
7858
7985
  throw new Error(
7859
- `File '${options.dst}' already exists. Use --force to overwrite.`
7986
+ `File '${options.destination}' already exists. Use --force to overwrite.`
7860
7987
  );
7861
7988
  }
7862
7989
  }
7863
7990
  async function createFileList(options, nodeClient, account) {
7864
7991
  const matchingBlobList = [];
7992
+ const stripAccountPrefix = (fullName) => {
7993
+ const blobNameMatch = fullName.match(/^@[^/]+\/(.+)$/);
7994
+ return blobNameMatch ? blobNameMatch[1] : fullName;
7995
+ };
7865
7996
  if (options.recursive) {
7866
- const allBlobs = await nodeClient.coordination.getAccountBlobs({
7997
+ const activeBlobs = await nodeClient.coordination.getAccountBlobs({
7867
7998
  account
7868
7999
  });
7869
- for (const blobMd of allBlobs) {
7870
- const blobNameWithoutAccount = blobMd.name.slice(1 + 64 + 1);
7871
- if (!blobNameWithoutAccount.startsWith(options.src)) continue;
8000
+ for (const blobMd of activeBlobs) {
8001
+ const blobNameWithoutAccount = stripAccountPrefix(blobMd.name);
8002
+ if (!blobNameWithoutAccount.startsWith(options.source)) continue;
7872
8003
  matchingBlobList.push(blobMd);
7873
8004
  }
7874
8005
  if (matchingBlobList.length === 0) {
7875
8006
  throw new Error(
7876
- `No blobs matching prefix ${options.src} were found in account ${account}`
8007
+ `No active blobs matching prefix '${options.source}' were found for account ${account.toString()}. They may have expired or been deleted.`
7877
8008
  );
7878
8009
  }
7879
8010
  } else {
7880
- const md = await nodeClient.coordination.getBlobMetadata({
7881
- account,
7882
- name: options.src
8011
+ const activeBlobs = await nodeClient.coordination.getAccountBlobs({
8012
+ account
7883
8013
  });
7884
- if (md === void 0) {
7885
- throw new Error(`Could not find a blob with name ${options.src}`);
8014
+ const md = activeBlobs.find(
8015
+ (blob) => stripAccountPrefix(blob.name) === options.source
8016
+ );
8017
+ if (!md) {
8018
+ throw new Error(
8019
+ `No active blob named '${options.source}' was found for account ${account.toString()}. It may have expired or been deleted.
8020
+ `
8021
+ );
7886
8022
  }
7887
8023
  matchingBlobList.push(md);
7888
8024
  }
7889
8025
  const ret = [];
7890
8026
  for (const blobMd of matchingBlobList) {
7891
- let blobname;
7892
- if (options.recursive) {
7893
- blobname = blobMd.name.slice(1 + 64 + 1);
7894
- } else {
7895
- blobname = blobMd.name;
7896
- }
8027
+ const blobNameWithoutAccount = stripAccountPrefix(blobMd.name);
7897
8028
  const entry = {
7898
- filename: denormBlobName2(options.src, blobname, options.dst),
7899
- blobname,
8029
+ filename: denormBlobName2(
8030
+ options.source,
8031
+ blobNameWithoutAccount,
8032
+ options.destination
8033
+ ),
8034
+ blobname: blobNameWithoutAccount,
8035
+ // FIXME this is nasty
7900
8036
  sizeBytes: blobMd.size
7901
8037
  };
7902
8038
  ret.push(entry);
@@ -7935,96 +8071,170 @@ function createProgressTransform(totalBytes, reporter) {
7935
8071
  });
7936
8072
  }
7937
8073
  function downloadCommand(program) {
7938
- program.command("download <src> <dst>").description("Download a file or directory from Shelby RPC").option(
8074
+ program.command("download").description("Download a file or directory from Shelby RPC").argument("[source]", "Source blob name or prefix").argument("[destination]", "Destination path").option(
7939
8075
  "-r, --recursive",
7940
8076
  "Download assuming canonical directory layout using '/' as separators. Produces a directory."
7941
- ).option("-f, --force", "Overwrite the dst").action(
7942
- async (src, dst, rawOptions) => {
7943
- const options = DownloadOptionsSchema.parse({
7944
- ...rawOptions,
7945
- src,
7946
- dst
7947
- });
7948
- options.dst = path3.resolve(options.dst);
7949
- await validateOutput(options);
7950
- if (options.recursive) {
7951
- console.log(
7952
- `Downloading directory: ${options.src} -> ${options.dst}`
7953
- );
7954
- } else {
7955
- console.log(`Downloading blob: ${options.src} -> ${options.dst}`);
7956
- }
7957
- const configPath = program.opts().configFile;
7958
- let config;
8077
+ ).option("-f, --force", "Overwrite the destination").action(
8078
+ async (source, destination, rawOptions) => {
8079
+ let options;
7959
8080
  try {
7960
- config = loadConfig(configPath);
8081
+ options = DownloadOptionsSchema.parse({
8082
+ ...rawOptions,
8083
+ source,
8084
+ destination
8085
+ });
7961
8086
  } catch (error) {
7962
- console.error(`Error: ${error.message}`);
8087
+ if (error instanceof z13.ZodError) {
8088
+ const firstIssue = error.issues[0];
8089
+ if (firstIssue) {
8090
+ console.log(firstIssue.message);
8091
+ } else {
8092
+ console.error("\u26A0\uFE0F Invalid download options provided");
8093
+ }
8094
+ } else {
8095
+ console.error(
8096
+ `\u26A0\uFE0F ${error instanceof Error ? error.message : String(error)}`
8097
+ );
8098
+ }
7963
8099
  process.exit(1);
7964
8100
  }
7965
- const shelbyConfig = getCurrentShelbyConfig(config, {
7966
- context: program.opts().context
7967
- });
7968
- const activeAccount = getCurrentAccount(
7969
- config,
7970
- program.opts().account
7971
- ).account;
7972
- const nodeClient = new ShelbyNodeClient(shelbyConfig);
7973
- const fileList = await createFileList(
7974
- options,
7975
- nodeClient,
7976
- activeAccount.accountAddress
7977
- );
7978
- for (const fileEntry of fileList) {
7979
- console.log(
7980
- `Downloading ${fileEntry.blobname} -> ${fileEntry.filename}`
7981
- );
7982
- }
7983
- if (options.force) {
7984
- console.log(`--force was set, so deleting ${options.dst}`);
7985
- await fs3.rm(options.dst, { recursive: true, force: true });
7986
- }
8101
+ options.destination = path3.resolve(options.destination);
8102
+ let config;
8103
+ let spinner;
7987
8104
  const handleSigint = () => {
7988
- spinner.fail("Quitting due to ctrl-C / SIGINT...");
8105
+ spinner?.fail("Quitting due to ctrl-C / SIGINT...");
7989
8106
  process.exit(1);
7990
8107
  };
7991
- process.removeAllListeners("SIGINT");
7992
- process.on("SIGINT", handleSigint);
7993
- const spinner = ora({
7994
- text: "Creating dst directory tree...",
7995
- discardStdin: false
7996
- }).start();
7997
- await createOutputDirectories(fileList);
7998
- let totalSize = 0;
7999
- for (const fileEntry of fileList) totalSize += fileEntry.sizeBytes;
8000
- const startTime = performance.now();
8001
- let amountDownloaded = 0;
8002
- const formatProgressPercent = () => (100 * (amountDownloaded / totalSize)).toFixed(2);
8003
- const formatProgressRate = () => {
8004
- const mib = amountDownloaded / 1024 / 1024;
8005
- const sec = (performance.now() - startTime) / 1e3;
8006
- return (mib / sec).toFixed(2);
8007
- };
8008
- for (const fileEntry of fileList) {
8009
- const out = fsS.createWriteStream(fileEntry.filename);
8010
- const { readable } = await nodeClient.rpc.getBlob({
8011
- account: activeAccount.accountAddress,
8012
- blobName: fileEntry.blobname
8108
+ try {
8109
+ const configPath = program.opts().configFile;
8110
+ config = loadConfig(configPath);
8111
+ const shelbyConfig = getCurrentShelbyConfig(config, {
8112
+ context: program.opts().context
8013
8113
  });
8014
- const reporter = (filePercent, rateMiB) => {
8015
- spinner.text = `Downloading ${fileEntry.blobname}, ${filePercent}% at ${rateMiB} MiB/s .. (Overall: ${formatProgressPercent()}%, ${formatProgressRate()} MiB/s)`;
8016
- };
8017
- const nodeStream = Readable2.fromWeb(readable);
8018
- await pipeline(
8019
- nodeStream,
8020
- createProgressTransform(fileEntry.sizeBytes, reporter),
8021
- out
8114
+ const activeAccount = getCurrentAccount(
8115
+ config,
8116
+ program.opts().account
8117
+ ).account;
8118
+ const nodeClient = new ShelbyNodeClient(shelbyConfig);
8119
+ const fileList = await createFileList(
8120
+ options,
8121
+ nodeClient,
8122
+ activeAccount.accountAddress
8123
+ );
8124
+ await validateOutput(options);
8125
+ console.log(`
8126
+ \u{1F4E5} Downloading Files (${fileList.length} total)`);
8127
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
8128
+ for (let i = 0; i < fileList.length; i++) {
8129
+ const fileEntry = fileList[i];
8130
+ const blobBaseName = path3.basename(fileEntry.blobname);
8131
+ console.log(`${i + 1}. ${blobBaseName} \u2192 ${fileEntry.filename}`);
8132
+ }
8133
+ console.log();
8134
+ if (options.force) {
8135
+ console.log(`--force was set, so deleting ${options.destination}`);
8136
+ await fs3.rm(options.destination, { recursive: true, force: true });
8137
+ }
8138
+ process.removeAllListeners("SIGINT");
8139
+ process.on("SIGINT", handleSigint);
8140
+ spinner = ora({
8141
+ text: "Creating destination directory tree...",
8142
+ discardStdin: false
8143
+ }).start();
8144
+ await createOutputDirectories(fileList);
8145
+ let totalSize = 0;
8146
+ for (const fileEntry of fileList) totalSize += fileEntry.sizeBytes;
8147
+ const startTime = performance.now();
8148
+ let amountDownloaded = 0;
8149
+ let filesDownloaded = 0;
8150
+ const isSingleFile = fileList.length === 1;
8151
+ const formatProgressPercent = () => totalSize > 0 ? (100 * (amountDownloaded / totalSize)).toFixed(2) : "0.00";
8152
+ for (const fileEntry of fileList) {
8153
+ const out = fsS.createWriteStream(fileEntry.filename);
8154
+ const fileName = path3.basename(fileEntry.blobname);
8155
+ const { readable } = await nodeClient.rpc.getBlob({
8156
+ account: activeAccount.accountAddress,
8157
+ blobName: fileEntry.blobname
8158
+ });
8159
+ const reporter = (filePercent, _rateMiB) => {
8160
+ if (spinner) {
8161
+ if (isSingleFile) {
8162
+ spinner.text = `Downloading ${fileName}... (${filePercent}%)`;
8163
+ } else {
8164
+ spinner.text = `Downloading ${fileName}... (${filesDownloaded}/${fileList.length} files, ${formatProgressPercent()}%)`;
8165
+ }
8166
+ }
8167
+ };
8168
+ const nodeStream = Readable2.fromWeb(readable);
8169
+ await pipeline(
8170
+ nodeStream,
8171
+ createProgressTransform(fileEntry.sizeBytes, reporter),
8172
+ out
8173
+ );
8174
+ amountDownloaded += fileEntry.sizeBytes;
8175
+ filesDownloaded++;
8176
+ }
8177
+ const totalTime = (performance.now() - startTime) / 1e3;
8178
+ spinner?.stop();
8179
+ console.log(
8180
+ `
8181
+ \u2705 All downloads complete \u2014 took ${totalTime.toFixed(1)} seconds`
8022
8182
  );
8023
- amountDownloaded += fileEntry.sizeBytes;
8183
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
8184
+ console.log("\u2728 Done!");
8185
+ } catch (error) {
8186
+ spinner?.stop();
8187
+ console.log("\n\u274C Download Failed");
8188
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
8189
+ if (options.recursive) {
8190
+ console.log(`\u{1F9FE} Directory: ${options.source}`);
8191
+ } else {
8192
+ console.log(`\u{1F9FE} File: ${options.source}`);
8193
+ }
8194
+ if (config) {
8195
+ const activeAccount = getCurrentAccount(
8196
+ config,
8197
+ program.opts().account
8198
+ ).account;
8199
+ console.log(
8200
+ `\u{1F464} Account: ${activeAccount.accountAddress.toString()}`
8201
+ );
8202
+ }
8203
+ console.log();
8204
+ if (error instanceof Error) {
8205
+ if (error.message.includes("GraphQL Error (Code: 401)")) {
8206
+ const contextName = program.opts().context || config?.default_context;
8207
+ console.error("\u26A0\uFE0F Authentication required.");
8208
+ console.error(
8209
+ " Please add an indexer API key to your context configuration."
8210
+ );
8211
+ console.error();
8212
+ console.error("\u{1F4A1} Tip: Update your context with an API key:");
8213
+ console.error(
8214
+ ` \u{1F449} shelby context update ${contextName ?? "default"} --indexer-api-key YOUR_KEY`
8215
+ );
8216
+ } else if (error.message.includes("No active blob") || error.message.includes("may have expired or been deleted")) {
8217
+ const blobName = options.source;
8218
+ console.error(
8219
+ `\u26A0\uFE0F No active blob${options.recursive ? "s" : ""} named '${blobName}' ${options.recursive ? "were" : "was"} found.`
8220
+ );
8221
+ console.error(" It may have expired or been deleted.");
8222
+ console.error();
8223
+ console.error(
8224
+ "\u{1F4A1} Tip: Try listing available blobs before downloading:"
8225
+ );
8226
+ console.error(" \u{1F449} shelby account blobs");
8227
+ } else {
8228
+ console.error(`\u26A0\uFE0F ${error.message}`);
8229
+ }
8230
+ } else {
8231
+ console.error(`\u26A0\uFE0F ${String(error)}`);
8232
+ }
8233
+ console.log("\n");
8234
+ process.exit(1);
8235
+ } finally {
8236
+ process.removeListener("SIGINT", handleSigint);
8024
8237
  }
8025
- const totalTime = (performance.now() - startTime) / 1e3;
8026
- const totalRate = amountDownloaded / 1024 / 1024 / totalTime;
8027
- spinner.succeed(`Final rate ${totalRate.toFixed(2)} MiB/s`);
8028
8238
  }
8029
8239
  );
8030
8240
  }
@@ -8246,13 +8456,63 @@ import * as path5 from "path";
8246
8456
  import { Aptos as Aptos5, AptosConfig as AptosConfig4 } from "@aptos-labs/ts-sdk";
8247
8457
  import * as chrono from "chrono-node";
8248
8458
  import { glob } from "glob";
8459
+ import ignore from "ignore";
8249
8460
  import { Box as Box7, render as render4, Text as Text7 } from "ink";
8250
8461
  import SelectInput4 from "ink-select-input";
8251
8462
  import ora2 from "ora";
8252
8463
  import { z as z15 } from "zod";
8464
+
8465
+ // src/utils/commander-helpers.ts
8466
+ function createExitOverrideHandler(commandName, requiredArgs, requiredOptions, exampleUsage, warningMessage) {
8467
+ return (err) => {
8468
+ if (err.code === "commander.helpDisplayed") {
8469
+ process.exit(0);
8470
+ }
8471
+ const args = process.argv.slice(process.argv.indexOf(commandName) + 1);
8472
+ const hasArguments = args.some((arg) => !arg.startsWith("-"));
8473
+ if (err.code === "commander.missingMandatoryOptionValue") {
8474
+ if (!hasArguments && requiredOptions) {
8475
+ console.error("\n\u274C Missing Required Arguments");
8476
+ console.error("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
8477
+ const warning = warningMessage || "Missing required arguments or options";
8478
+ console.error(`\u26A0\uFE0F ${warning}
8479
+ `);
8480
+ console.error("\u{1F4A1} Usage:");
8481
+ console.error(
8482
+ ` shelby ${commandName} ${requiredArgs} ${requiredOptions}
8483
+ `
8484
+ );
8485
+ if (exampleUsage) {
8486
+ console.error("\u{1F4DD} Examples:");
8487
+ console.error(` ${exampleUsage}`);
8488
+ }
8489
+ console.error("\n");
8490
+ } else {
8491
+ console.error(err.message);
8492
+ }
8493
+ process.exit(1);
8494
+ }
8495
+ if (err.code === "commander.optionMissingArgument") {
8496
+ console.error("\n\u274C Missing Option Value");
8497
+ console.error("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
8498
+ const cleanMessage = err.message.replace(/^error:\s*/i, "");
8499
+ console.error(`\u26A0\uFE0F ${cleanMessage}
8500
+ `);
8501
+ if (exampleUsage) {
8502
+ console.error("\u{1F4A1} Example:");
8503
+ console.error(` ${exampleUsage}`);
8504
+ }
8505
+ console.error("\n");
8506
+ process.exit(1);
8507
+ }
8508
+ throw err;
8509
+ };
8510
+ }
8511
+
8512
+ // src/commands/upload.tsx
8253
8513
  import { jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
8254
8514
  var normBlobName2 = (i, f, b) => normBlobName(path5, i, f, b);
8255
- var flexibleDateSchema = z15.string().transform((val) => {
8515
+ var flexibleDateSchema = z15.string().transform((val, ctx) => {
8256
8516
  const now = /* @__PURE__ */ new Date();
8257
8517
  let parsedDate = null;
8258
8518
  if (/^\d+$/.test(val)) {
@@ -8274,77 +8534,207 @@ var flexibleDateSchema = z15.string().transform((val) => {
8274
8534
  }
8275
8535
  }
8276
8536
  if (!parsedDate || Number.isNaN(parsedDate.getTime())) {
8277
- throw new Error(
8278
- `Invalid date format: "${val}". Try formats like "tomorrow", "in 2 days", "next Friday", "2025-12-31", or a UNIX timestamp.`
8279
- );
8537
+ ctx.addIssue({
8538
+ code: z15.ZodIssueCode.custom,
8539
+ message: `
8540
+ \u274C Upload Failed
8541
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8542
+
8543
+ \u26A0\uFE0F Invalid date format: "${val}"
8544
+
8545
+ \u{1F4A1} Try formats like:
8546
+ \u2022 "tomorrow", "in 2 days", "next Friday"
8547
+ \u2022 "2025-12-31"
8548
+ \u2022 UNIX timestamp
8549
+
8550
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`
8551
+ });
8552
+ return z15.NEVER;
8280
8553
  }
8281
8554
  if (parsedDate.getTime() <= now.getTime()) {
8282
- throw new Error(
8283
- `Expiration date must be in the future. "${val}" resolves to ${parsedDate.toLocaleString()} which is in the past.`
8284
- );
8555
+ ctx.addIssue({
8556
+ code: z15.ZodIssueCode.custom,
8557
+ message: `
8558
+ \u274C Upload Failed
8559
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8560
+
8561
+ \u26A0\uFE0F Expiration date must be in the future
8562
+
8563
+ \u{1F4C5} "${val}" resolves to ${parsedDate.toLocaleString()}
8564
+ which is in the past.
8565
+
8566
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`
8567
+ });
8568
+ return z15.NEVER;
8285
8569
  }
8286
8570
  return parsedDate;
8287
8571
  });
8288
8572
  var UploadOptionsSchema = z15.object({
8289
- src: z15.string().nonempty("`src` positional arg is required"),
8290
- dst: z15.string().nonempty("`dst` position arg is required"),
8573
+ source: z15.string({
8574
+ required_error: '\n\u274C Missing Required Argument\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\u26A0\uFE0F Missing source file or directory path (first argument)\n\n\u{1F4A1} Usage:\n shelby upload <source-file-or-directory> <destination-blob-name> [options]\n\n\u{1F4DD} Examples:\n shelby upload ./myfile.txt my-blob.txt -e tomorrow\n shelby upload ./my-folder/ my-folder/ -r -e "next week"\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
8575
+ }).min(1, "Source file or directory path is required"),
8576
+ destination: z15.string({
8577
+ required_error: "\n\u274C Missing Required Argument\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\u26A0\uFE0F Missing destination blob name (second argument)\n\n\u{1F4A1} Usage:\n shelby upload <source-file-or-directory> <destination-blob-name> [options]\n\n\u{1F4DD} Example:\n shelby upload ./myfile.txt files/my-blob.txt -e tomorrow\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"
8578
+ }).min(1, "Destination blob name is required"),
8291
8579
  expiration: flexibleDateSchema,
8292
8580
  recursive: z15.boolean().optional().default(false),
8293
8581
  assumeYes: z15.boolean().optional().default(false),
8294
8582
  outputCommitments: z15.string().optional()
8295
8583
  }).superRefine(async (data, ctx) => {
8296
- const stats = await fs5.stat(data.src);
8584
+ const stats = await fs5.stat(data.source);
8297
8585
  if (!stats.isFile() && !stats.isDirectory()) {
8298
8586
  ctx.addIssue({
8299
8587
  code: z15.ZodIssueCode.custom,
8300
- message: "`src` must be a file or a directory",
8301
- path: ["src"]
8588
+ message: "\n\u274C Upload Failed\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\u26A0\uFE0F Source path must be a file or directory\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
8589
+ path: ["source"]
8302
8590
  });
8303
8591
  return;
8304
8592
  }
8305
8593
  if (stats.isDirectory()) {
8306
- if (!data.dst.endsWith("/")) {
8594
+ if (!data.destination.endsWith("/")) {
8307
8595
  ctx.addIssue({
8308
8596
  code: z15.ZodIssueCode.custom,
8309
- message: "When src is a directory, dst must end with '/'",
8310
- path: ["dst"]
8597
+ message: `
8598
+ \u274C Upload Failed
8599
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8600
+
8601
+ \u26A0\uFE0F When source is a directory, destination must end with '/'
8602
+
8603
+ \u{1F4A1} Tip: When uploading a directory, add a trailing slash:
8604
+ \u{1F449} shelby upload -r ${data.source} ${data.destination}/
8605
+
8606
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`,
8607
+ path: ["destination"]
8311
8608
  });
8312
8609
  }
8313
8610
  } else if (stats.isFile()) {
8314
- const blobNameResult = BlobNameSchema.safeParse(data.dst);
8611
+ const blobNameResult = BlobNameSchema.safeParse(data.destination);
8315
8612
  if (!blobNameResult.success) {
8316
8613
  ctx.addIssue({
8317
8614
  code: z15.ZodIssueCode.custom,
8318
- message: "When src is a file, dst must be a valid blob name (cannot end with '/')",
8319
- path: ["dst"]
8615
+ message: `
8616
+ \u274C Upload Failed
8617
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8618
+
8619
+ \u26A0\uFE0F When source is a file, destination must be a valid blob name (cannot end with '/')
8620
+
8621
+ \u{1F4A1} Tip: When uploading a file, remove the trailing slash:
8622
+ \u{1F449} shelby upload ${data.source} ${data.destination.replace(/\/$/, "")}
8623
+
8624
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`,
8625
+ path: ["destination"]
8320
8626
  });
8321
8627
  }
8322
8628
  }
8323
8629
  });
8630
+ async function buildGitignoreMap(root) {
8631
+ const resolvedRoot = path5.resolve(root);
8632
+ const gitignoreMap = /* @__PURE__ */ new Map();
8633
+ const ensureMatcher = (dir) => {
8634
+ const existing = gitignoreMap.get(dir);
8635
+ if (existing) {
8636
+ return existing;
8637
+ }
8638
+ const instance = ignore();
8639
+ gitignoreMap.set(dir, instance);
8640
+ return instance;
8641
+ };
8642
+ ensureMatcher(resolvedRoot).add(".git/");
8643
+ const gitignoreFiles = await glob("**/.gitignore", {
8644
+ cwd: resolvedRoot,
8645
+ dot: true,
8646
+ absolute: true,
8647
+ nodir: true
8648
+ });
8649
+ for (const gitignorePath of gitignoreFiles) {
8650
+ try {
8651
+ const contents = await fs5.readFile(gitignorePath, "utf8");
8652
+ const directory = path5.dirname(gitignorePath);
8653
+ ensureMatcher(directory).add(contents);
8654
+ } catch (error) {
8655
+ if (error instanceof Error && Object.hasOwn(error, "code") && // biome-ignore lint/suspicious/noExplicitAny: node fs error type
8656
+ error.code === "ENOENT") {
8657
+ continue;
8658
+ }
8659
+ throw error;
8660
+ }
8661
+ }
8662
+ return gitignoreMap;
8663
+ }
8664
+ function collectDirectoriesFromRoot(root, filePath) {
8665
+ const directories = [];
8666
+ let current = path5.dirname(filePath);
8667
+ while (true) {
8668
+ directories.push(current);
8669
+ if (current === root) {
8670
+ break;
8671
+ }
8672
+ const parent = path5.dirname(current);
8673
+ if (parent === current) {
8674
+ break;
8675
+ }
8676
+ current = parent;
8677
+ }
8678
+ return directories.reverse();
8679
+ }
8680
+ function shouldIgnorePath(gitignoreMap, root, filePath) {
8681
+ const directories = collectDirectoriesFromRoot(root, filePath);
8682
+ let ignored = false;
8683
+ for (const directory of directories) {
8684
+ const matcher = gitignoreMap.get(directory);
8685
+ if (!matcher) {
8686
+ continue;
8687
+ }
8688
+ const relative2 = path5.relative(directory, filePath).split(path5.sep).join("/");
8689
+ const result = matcher.test(relative2);
8690
+ if (result.ignored) {
8691
+ ignored = true;
8692
+ }
8693
+ if (result.unignored) {
8694
+ ignored = false;
8695
+ }
8696
+ }
8697
+ return ignored;
8698
+ }
8324
8699
  async function createFilelist(options) {
8325
- const stats = await fs5.stat(options.src);
8700
+ const stats = await fs5.stat(options.source);
8326
8701
  if (stats.isFile()) {
8327
- const blobname = normBlobName2(options.src, options.src, options.dst);
8702
+ const blobname = normBlobName2(
8703
+ options.source,
8704
+ options.source,
8705
+ options.destination
8706
+ );
8328
8707
  const blobNameValidation = BlobNameSchema.safeParse(blobname);
8329
8708
  if (!blobNameValidation.success) {
8330
8709
  throw new Error(
8331
- `File ${options.src} as blobname ${blobname} would form an invalid blob name: ${blobNameValidation.error.message}`
8710
+ `File ${options.source} as blobname ${blobname} would form an invalid blob name: ${blobNameValidation.error.message}`
8332
8711
  );
8333
8712
  }
8334
- return [{ filename: options.src, blobname, sizeBytes: stats.size }];
8713
+ return [{ filename: options.source, blobname, sizeBytes: stats.size }];
8335
8714
  }
8336
8715
  if (!options.recursive) {
8337
8716
  throw new Error(
8338
- `${options.src} is a directory. Use --recursive to upload directories.`
8717
+ `${options.source} is a directory. Use --recursive to upload directories.`
8339
8718
  );
8340
8719
  }
8720
+ const sourceRoot = path5.resolve(options.source);
8721
+ const gitignoreMap = await buildGitignoreMap(sourceRoot);
8341
8722
  const fileList = [];
8342
- const result = await glob(path5.join(options.src, "**", "*"), {
8343
- nodir: true
8723
+ const result = await glob("**/*", {
8724
+ cwd: sourceRoot,
8725
+ absolute: true,
8726
+ nodir: true,
8727
+ dot: true
8344
8728
  });
8345
8729
  for await (const file of result) {
8730
+ if (shouldIgnorePath(gitignoreMap, sourceRoot, file)) {
8731
+ continue;
8732
+ }
8346
8733
  const stats2 = await fs5.stat(file);
8347
- const blobname = normBlobName2(options.src, file, options.dst);
8734
+ if (!stats2.isFile()) {
8735
+ continue;
8736
+ }
8737
+ const blobname = normBlobName2(sourceRoot, file, options.destination);
8348
8738
  const blobNameValidation = BlobNameSchema.safeParse(blobname);
8349
8739
  if (!blobNameValidation.success) {
8350
8740
  throw new Error(
@@ -8360,7 +8750,7 @@ async function createFilelist(options) {
8360
8750
  return fileList;
8361
8751
  }
8362
8752
  function uploadCommand(program) {
8363
- program.command("upload <src> <dst>").description("Upload a file or directory to the shelby RPC in the config.").requiredOption(
8753
+ const uploadCmd = program.command("upload").description("Upload a file or directory to the shelby RPC in the config.").argument("[source]", "Source file or directory path").argument("[destination]", "Destination blob name").requiredOption(
8364
8754
  "-e, --expiration <datetime>",
8365
8755
  'Expiration date/time (required). Examples: "tomorrow", "in 2 days", "next Friday", "2025-12-31", UNIX timestamp'
8366
8756
  ).option("-r, --recursive", "If uploading a directory, recurse").option(
@@ -8369,205 +8759,227 @@ function uploadCommand(program) {
8369
8759
  ).option(
8370
8760
  "--output-commitments <filename>",
8371
8761
  "Location to store commitments computed as as part of the upload"
8372
- ).action(async (src, dst, options) => {
8373
- let validatedOptions;
8374
- try {
8375
- validatedOptions = await UploadOptionsSchema.parseAsync({
8376
- ...options,
8377
- src,
8378
- dst
8379
- });
8380
- } catch (error) {
8381
- const { displayMessage } = handleError(error);
8382
- console.error(`\u274C ${displayMessage}`);
8383
- process.exit(1);
8384
- }
8385
- const configPath = program.opts().configFile;
8386
- let config;
8387
- try {
8388
- config = loadConfig(configPath);
8389
- } catch (error) {
8390
- console.error(`Error: ${error.message}`);
8391
- process.exit(1);
8762
+ ).configureOutput({
8763
+ writeErr: (_str) => {
8392
8764
  }
8393
- const start = performance.now();
8394
- const filelist = await createFilelist(validatedOptions);
8395
- const timeToCreateFilelist = ((performance.now() - start) / 1e3).toFixed(
8396
- 5
8397
- );
8398
- console.log("\n\u{1F680} Upload Summary");
8399
- console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
8400
- if (filelist.length === 1) {
8401
- console.log(`\u{1F4E6} File: ${filelist[0].filename}`);
8402
- console.log(`\u{1F4C1} Blob Name: ${filelist[0].blobname}`);
8403
- } else {
8404
- console.log(`\u{1F4E6} Files: ${filelist.length} files`);
8405
- }
8406
- console.log("");
8407
- console.log(
8408
- `\u{1F9EE} Filelist created (${filelist.length} ${filelist.length === 1 ? "entry" : "entries"})`
8409
- );
8410
- console.log(`\u23F1\uFE0F Took: ${timeToCreateFilelist}s`);
8411
- if (validatedOptions.assumeYes) {
8412
- console.log("\u2699\uFE0F Flag: --assume-yes (auto-confirmed)");
8413
- }
8414
- if (!validatedOptions.assumeYes) {
8415
- const shouldContinue = await new Promise((resolve2) => {
8416
- const { unmount } = render4(
8417
- /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
8418
- /* @__PURE__ */ jsx13(Text7, { children: "Continue?" }),
8419
- /* @__PURE__ */ jsx13(
8420
- SelectInput4,
8421
- {
8422
- items: [
8423
- { label: "Yes", value: true },
8424
- { label: "No", value: false }
8425
- ],
8426
- onSelect: (item) => {
8427
- unmount();
8428
- resolve2(item.value);
8429
- }
8430
- }
8431
- )
8432
- ] })
8433
- );
8434
- });
8435
- if (!shouldContinue) {
8436
- console.log("Upload cancelled.");
8437
- process.exit(0);
8438
- }
8439
- }
8440
- const shelbyConfig = getCurrentShelbyConfig(config, {
8441
- context: program.opts().context
8442
- });
8443
- const activeAccount = getCurrentAccount(
8444
- config,
8445
- program.opts().account
8446
- ).account;
8447
- const aptos = new Aptos5(new AptosConfig4(shelbyConfig.aptos));
8448
- const shelbyClient = new ShelbyNodeClient(shelbyConfig);
8449
- const handleSigint = () => {
8450
- spinner.fail("Quitting due to ctrl-C / SIGINT...");
8451
- process.exit(1);
8452
- };
8453
- process.removeAllListeners("SIGINT");
8454
- process.on("SIGINT", handleSigint);
8455
- const expireUsec = validatedOptions.expiration.getTime() * 1e3;
8456
- const expirationDate = new Date(expireUsec / 1e3);
8457
- const formattedExpiration = expirationDate.toLocaleDateString("en-US", {
8458
- year: "numeric",
8459
- month: "short",
8460
- day: "numeric",
8461
- hour: "numeric",
8462
- minute: "2-digit",
8463
- second: "2-digit",
8464
- hour12: true
8465
- });
8466
- console.log("");
8467
- console.log(`\u{1F552} Expires: ${formattedExpiration}`);
8468
- const spinner = ora2({
8469
- text: "Uploading...",
8470
- discardStdin: false
8471
- }).start();
8472
- const startTime = performance.now();
8473
- let totalSize = 0;
8474
- let amountUploaded = 0;
8475
- const outputCommitments = {};
8476
- let firstTransactionHash;
8477
- const formatProgressPercent = () => (100 * (amountUploaded / totalSize)).toFixed(2);
8478
- for (const entry of filelist) {
8479
- totalSize += entry.sizeBytes;
8480
- }
8481
- let hasUploadedBlob = false;
8482
- for (const entry of filelist) {
8483
- const fileName = path5.basename(entry.filename);
8484
- spinner.text = `\u{1F4D6} Reading ${fileName}... (${formatProgressPercent()}%)`;
8765
+ });
8766
+ uploadCmd.exitOverride(
8767
+ createExitOverrideHandler(
8768
+ "upload",
8769
+ "<source> <destination>",
8770
+ "-e, --expiration <datetime>",
8771
+ 'shelby upload ./file.txt myblob -e "in 2 days"\n shelby upload ./my-folder/ my-folder/ -r -e tomorrow',
8772
+ "Missing source, destination, and expiration"
8773
+ )
8774
+ );
8775
+ uploadCmd.action(
8776
+ async (source, destination, options) => {
8777
+ let validatedOptions;
8485
8778
  try {
8486
- const blobData = await fs5.readFile(entry.filename);
8487
- if (blobData.length !== entry.sizeBytes) {
8488
- throw new Error(
8489
- `Size of file ${entry.filename} changed after initial scan. Original size was ${entry.sizeBytes} but it is now ${blobData.length}`
8490
- );
8491
- }
8492
- spinner.text = `\u{1F517} Checking if blob exists... (${formatProgressPercent()}%)`;
8493
- const existingBlobMetadata = await shelbyClient.coordination.getBlobMetadata({
8494
- account: activeAccount.accountAddress,
8495
- name: entry.blobname
8779
+ validatedOptions = await UploadOptionsSchema.parseAsync({
8780
+ ...options,
8781
+ source,
8782
+ destination
8496
8783
  });
8497
- if (existingBlobMetadata) {
8498
- spinner.fail(
8499
- `Blob '${entry.blobname}' already exists. Please use a different name or delete the existing blob first.`
8500
- );
8501
- process.exit(1);
8502
- }
8503
- spinner.text = `\u{1F517} Generating commitments for ${fileName}... (${formatProgressPercent()}%)`;
8504
- const provider = await getErasureCodingProvider();
8505
- const blobCommitments = await generateCommitments(provider, blobData);
8506
- if (validatedOptions.outputCommitments) {
8507
- outputCommitments[entry.filename] = blobCommitments;
8508
- }
8509
- spinner.text = `\u{1F517} Registering ${fileName} on L1... (${formatProgressPercent()}%)`;
8510
- const { transaction: pendingRegisterBlobTransaction } = await shelbyClient.coordination.registerBlob({
8511
- account: activeAccount,
8512
- blobName: entry.blobname,
8513
- blobMerkleRoot: blobCommitments.blob_merkle_root,
8514
- size: blobData.length,
8515
- expirationMicros: validatedOptions.expiration.getTime() * 1e3
8516
- });
8517
- const registerTransactionHash = pendingRegisterBlobTransaction.hash;
8518
- if (!firstTransactionHash) {
8519
- firstTransactionHash = registerTransactionHash;
8784
+ } catch (error) {
8785
+ if (error instanceof z15.ZodError) {
8786
+ const firstIssue = error.issues[0];
8787
+ if (firstIssue) {
8788
+ console.log(firstIssue.message);
8789
+ } else {
8790
+ console.error("\u26A0\uFE0F Invalid options provided");
8791
+ }
8792
+ } else {
8793
+ const { displayMessage } = handleError(error);
8794
+ console.error(`\u26A0\uFE0F ${displayMessage}`);
8520
8795
  }
8521
- await aptos.waitForTransaction({
8522
- transactionHash: pendingRegisterBlobTransaction.hash
8523
- });
8524
- spinner.text = `\u{1F4E4} Uploading ${fileName} to Shelby RPC... (${formatProgressPercent()}%)`;
8525
- await shelbyClient.rpc.putBlob({
8526
- account: activeAccount.accountAddress,
8527
- blobName: entry.blobname,
8528
- blobData
8529
- });
8530
- hasUploadedBlob = true;
8531
- amountUploaded += blobData.length;
8796
+ process.exit(1);
8797
+ }
8798
+ const configPath = program.opts().configFile;
8799
+ let config;
8800
+ try {
8801
+ config = loadConfig(configPath);
8532
8802
  } catch (error) {
8533
- const { displayMessage } = handleError(error);
8534
- spinner.fail(displayMessage);
8803
+ console.error(`Error: ${error.message}`);
8535
8804
  process.exit(1);
8536
8805
  }
8537
- }
8538
- if (validatedOptions.outputCommitments) {
8539
- await fs5.writeFile(
8540
- validatedOptions.outputCommitments,
8541
- JSON.stringify(outputCommitments)
8806
+ const start = performance.now();
8807
+ const filelist = await createFilelist(validatedOptions);
8808
+ const timeToCreateFilelist = ((performance.now() - start) / 1e3).toFixed(
8809
+ 5
8542
8810
  );
8543
- }
8544
- if (hasUploadedBlob) {
8811
+ console.log("\n\u{1F680} Upload Summary");
8812
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
8813
+ if (filelist.length === 1) {
8814
+ console.log(`\u{1F4E6} File: ${filelist[0].filename}`);
8815
+ console.log(`\u{1F4C1} Blob Name: ${filelist[0].blobname}`);
8816
+ } else {
8817
+ console.log(`\u{1F4E6} Files: ${filelist.length} files`);
8818
+ }
8819
+ console.log("");
8820
+ console.log(
8821
+ `\u{1F9EE} Filelist created (${filelist.length} ${filelist.length === 1 ? "entry" : "entries"})`
8822
+ );
8823
+ console.log(`\u23F1\uFE0F Took: ${timeToCreateFilelist}s`);
8824
+ if (validatedOptions.assumeYes) {
8825
+ console.log("\u2699\uFE0F Flag: --assume-yes (auto-confirmed)");
8826
+ }
8827
+ if (!validatedOptions.assumeYes) {
8828
+ const shouldContinue = await new Promise((resolve3) => {
8829
+ const { unmount } = render4(
8830
+ /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
8831
+ /* @__PURE__ */ jsx13(Text7, { children: "Continue?" }),
8832
+ /* @__PURE__ */ jsx13(
8833
+ SelectInput4,
8834
+ {
8835
+ items: [
8836
+ { label: "Yes", value: true },
8837
+ { label: "No", value: false }
8838
+ ],
8839
+ onSelect: (item) => {
8840
+ unmount();
8841
+ resolve3(item.value);
8842
+ }
8843
+ }
8844
+ )
8845
+ ] })
8846
+ );
8847
+ });
8848
+ if (!shouldContinue) {
8849
+ console.log("Upload cancelled.");
8850
+ process.exit(0);
8851
+ }
8852
+ }
8853
+ const shelbyConfig = getCurrentShelbyConfig(config, {
8854
+ context: program.opts().context
8855
+ });
8856
+ const activeAccount = getCurrentAccount(
8857
+ config,
8858
+ program.opts().account
8859
+ ).account;
8860
+ const aptos = new Aptos5(new AptosConfig4(shelbyConfig.aptos));
8861
+ const shelbyClient = new ShelbyNodeClient(shelbyConfig);
8862
+ const handleSigint = () => {
8863
+ spinner.fail("Quitting due to ctrl-C / SIGINT...");
8864
+ process.exit(1);
8865
+ };
8866
+ process.removeAllListeners("SIGINT");
8867
+ process.on("SIGINT", handleSigint);
8868
+ const expireUsec = validatedOptions.expiration.getTime() * 1e3;
8869
+ const expirationDate = new Date(expireUsec / 1e3);
8870
+ const formattedExpiration = expirationDate.toLocaleDateString("en-US", {
8871
+ year: "numeric",
8872
+ month: "short",
8873
+ day: "numeric",
8874
+ hour: "numeric",
8875
+ minute: "2-digit",
8876
+ second: "2-digit",
8877
+ hour12: true
8878
+ });
8879
+ console.log("");
8880
+ console.log(`\u{1F552} Expires: ${formattedExpiration}`);
8881
+ const spinner = ora2({
8882
+ text: "Uploading...",
8883
+ discardStdin: false
8884
+ }).start();
8885
+ const startTime = performance.now();
8886
+ let totalSize = 0;
8887
+ let amountUploaded = 0;
8888
+ const outputCommitments = {};
8889
+ let firstTransactionHash;
8890
+ for (const entry of filelist) {
8891
+ totalSize += entry.sizeBytes;
8892
+ }
8893
+ const isSingleFile = filelist.length === 1;
8894
+ const formatProgressPercent = () => totalSize > 0 ? (100 * (amountUploaded / totalSize)).toFixed(2) : "0.00";
8895
+ let filesProcessed = 0;
8896
+ for (const entry of filelist) {
8897
+ const fileName = path5.basename(entry.filename);
8898
+ const fileProgress = isSingleFile ? `(${formatProgressPercent()}%)` : `(${filesProcessed}/${filelist.length} files, ${formatProgressPercent()}%)`;
8899
+ spinner.text = `\u{1F4D6} Reading ${fileName}... ${fileProgress}`;
8900
+ try {
8901
+ const blobData = await fs5.readFile(entry.filename);
8902
+ if (blobData.length !== entry.sizeBytes) {
8903
+ throw new Error(
8904
+ `Size of file ${entry.filename} changed after initial scan. Original size was ${entry.sizeBytes} but it is now ${blobData.length}`
8905
+ );
8906
+ }
8907
+ spinner.text = `\u{1F517} Checking if blob exists... ${fileProgress}`;
8908
+ const existingBlobMetadata = await shelbyClient.coordination.getBlobMetadata({
8909
+ account: activeAccount.accountAddress,
8910
+ name: entry.blobname
8911
+ });
8912
+ if (existingBlobMetadata) {
8913
+ spinner.fail(
8914
+ `Blob '${entry.blobname}' already exists. Please use a different name or delete the existing blob first.`
8915
+ );
8916
+ process.exit(1);
8917
+ }
8918
+ spinner.text = `\u{1F517} Generating commitments for ${fileName}... ${fileProgress}`;
8919
+ const provider = await getErasureCodingProvider();
8920
+ const blobCommitments = await generateCommitments(provider, blobData);
8921
+ if (validatedOptions.outputCommitments) {
8922
+ outputCommitments[entry.filename] = blobCommitments;
8923
+ }
8924
+ spinner.text = `\u{1F517} Registering ${fileName} on L1... ${fileProgress}`;
8925
+ const { transaction: pendingRegisterBlobTransaction } = await shelbyClient.coordination.registerBlob({
8926
+ account: activeAccount,
8927
+ blobName: entry.blobname,
8928
+ blobMerkleRoot: blobCommitments.blob_merkle_root,
8929
+ size: blobData.length,
8930
+ expirationMicros: validatedOptions.expiration.getTime() * 1e3
8931
+ });
8932
+ const registerTransactionHash = pendingRegisterBlobTransaction.hash;
8933
+ if (!firstTransactionHash) {
8934
+ firstTransactionHash = registerTransactionHash;
8935
+ }
8936
+ await aptos.waitForTransaction({
8937
+ transactionHash: pendingRegisterBlobTransaction.hash
8938
+ });
8939
+ spinner.text = `\u{1F4E4} Uploading ${fileName} to Shelby RPC... ${fileProgress}`;
8940
+ await shelbyClient.rpc.putBlob({
8941
+ account: activeAccount.accountAddress,
8942
+ blobName: entry.blobname,
8943
+ blobData
8944
+ });
8945
+ amountUploaded += blobData.length;
8946
+ filesProcessed++;
8947
+ } catch (error) {
8948
+ const { displayMessage } = handleError(error);
8949
+ spinner.fail(displayMessage);
8950
+ process.exit(1);
8951
+ }
8952
+ }
8953
+ if (validatedOptions.outputCommitments) {
8954
+ await fs5.writeFile(
8955
+ validatedOptions.outputCommitments,
8956
+ JSON.stringify(outputCommitments)
8957
+ );
8958
+ }
8545
8959
  const elapsedSec = ((performance.now() - startTime) / 1e3).toFixed(2);
8546
8960
  spinner.succeed(`Upload complete \u2014 took ${elapsedSec}s`);
8547
- } else {
8548
- spinner.succeed("Skipped upload, blob has already been uploaded.");
8549
- }
8550
- console.log("");
8551
- if (firstTransactionHash) {
8552
- const explorerUrl = getAptosTransactionExplorerUrl(
8961
+ console.log("");
8962
+ if (firstTransactionHash) {
8963
+ const explorerUrl = getAptosTransactionExplorerUrl(
8964
+ shelbyConfig.network,
8965
+ firstTransactionHash
8966
+ );
8967
+ console.log("\u{1F310} Aptos Explorer:");
8968
+ console.log(` ${explorerUrl}`);
8969
+ console.log("");
8970
+ }
8971
+ const shelbyExplorerUrl = getShelbyAccountExplorerUrl(
8553
8972
  shelbyConfig.network,
8554
- firstTransactionHash
8973
+ activeAccount.accountAddress.toString()
8555
8974
  );
8556
- console.log("\u{1F310} Aptos Explorer:");
8557
- console.log(` ${explorerUrl}`);
8975
+ console.log("\u{1F5C2}\uFE0F Shelby Explorer:");
8976
+ console.log(` ${shelbyExplorerUrl}`);
8977
+ console.log("");
8978
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
8979
+ console.log("\u2728 Done!");
8558
8980
  console.log("");
8559
8981
  }
8560
- const shelbyExplorerUrl = getShelbyAccountExplorerUrl(
8561
- shelbyConfig.network,
8562
- activeAccount.accountAddress.toString()
8563
- );
8564
- console.log("\u{1F5C2}\uFE0F Shelby Explorer:");
8565
- console.log(` ${shelbyExplorerUrl}`);
8566
- console.log("");
8567
- console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
8568
- console.log("\u2728 Done!");
8569
- console.log("");
8570
- });
8982
+ );
8571
8983
  }
8572
8984
 
8573
8985
  // src/cli.tsx