@btc-vision/cli 1.0.8 → 1.0.10

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.
@@ -1,6 +1,6 @@
1
1
  import { confirm, input } from '@inquirer/prompts';
2
2
  import { BaseCommand } from './BaseCommand.js';
3
- import { getPackage, getRegistryContract, getVersion, isVersionImmutable, } from '../lib/registry.js';
3
+ import { getPackage, getRegistryContract, getVersion, isVersionImmutable } from '../lib/registry.js';
4
4
  import { canSign, loadCredentials } from '../lib/credentials.js';
5
5
  import { CLIWallet } from '../lib/wallet.js';
6
6
  import { buildTransactionParams, checkBalance, formatSats, getWalletAddress, } from '../lib/transaction.js';
@@ -3,7 +3,7 @@ import { confirm } from '@inquirer/prompts';
3
3
  import { Logger } from '@btc-vision/logger';
4
4
  import { CLIWallet } from '../lib/wallet.js';
5
5
  import { canSign, loadCredentials } from '../lib/credentials.js';
6
- import { getContenthash, getContenthashTypeName, getDomain, getDomainPrice, getResolverContract, getTreasuryAddress, parseDomainName, validateDomainName, } from '../lib/resolver.js';
6
+ import { getResolverContract, getDomain, getContenthash, getContenthashTypeName, validateDomainName, getTreasuryAddress, getDomainPrice, parseDomainName, } from '../lib/resolver.js';
7
7
  import { buildTransactionParams, checkBalance, DEFAULT_FEE_RATE, DEFAULT_MAX_SAT_TO_SPEND, formatSats, getWalletAddress, waitForTransactionConfirmation, } from '../lib/transaction.js';
8
8
  import { TransactionOutputFlags } from 'opnet';
9
9
  const logger = new Logger();
@@ -67,7 +67,6 @@ async function registerDomain(domain, options) {
67
67
  logger.log(`Treasury: ${treasuryAddr}`);
68
68
  logger.log(`Network: ${network}`);
69
69
  logger.log(`Your wallet: ${wallet.p2trAddress}`);
70
- logger.log(`MLDSA Public Key Hash: ${wallet.address.toHex()}`);
71
70
  logger.log('');
72
71
  if (options.dryRun) {
73
72
  logger.warn('Dry run - no changes made.');
@@ -223,7 +222,8 @@ async function domainInfo(domain, options) {
223
222
  process.exit(1);
224
223
  }
225
224
  }
226
- const domainCommand = new Command('domain').description('Manage .btc domains');
225
+ const domainCommand = new Command('domain')
226
+ .description('Manage .btc domains');
227
227
  domainCommand
228
228
  .command('register')
229
229
  .description('Register a new .btc domain')
@@ -3,5 +3,7 @@ export declare class InstallCommand extends BaseCommand {
3
3
  constructor();
4
4
  protected configure(): void;
5
5
  private execute;
6
+ private removeOldVersions;
7
+ private escapeRegex;
6
8
  }
7
9
  export declare const installCommand: import("commander").Command;
@@ -106,8 +106,10 @@ export class InstallCommand extends BaseCommand {
106
106
  }
107
107
  const outputDir = options?.output || path.join(process.cwd(), 'plugins');
108
108
  fs.mkdirSync(outputDir, { recursive: true });
109
- const fileName = `${packageName.replace(/^@/, '').replace(/\//g, '-')}-${version}.opnet`;
109
+ const packageBaseName = packageName.replace(/^@/, '').replace(/\//g, '-');
110
+ const fileName = `${packageBaseName}-${version}.opnet`;
110
111
  const outputPath = path.join(outputDir, fileName);
112
+ this.removeOldVersions(outputDir, packageBaseName, version);
111
113
  this.logger.info('Saving plugin...');
112
114
  fs.writeFileSync(outputPath, result.data);
113
115
  this.logger.success('Plugin installed');
@@ -126,5 +128,31 @@ export class InstallCommand extends BaseCommand {
126
128
  this.exitWithError(this.formatError(error));
127
129
  }
128
130
  }
131
+ removeOldVersions(outputDir, packageBaseName, newVersion) {
132
+ try {
133
+ const entries = fs.readdirSync(outputDir, { withFileTypes: true });
134
+ for (const entry of entries) {
135
+ if (!entry.isFile() || !entry.name.endsWith('.opnet')) {
136
+ continue;
137
+ }
138
+ const pattern = new RegExp(`^${this.escapeRegex(packageBaseName)}-(.+)\\.opnet$`);
139
+ const match = entry.name.match(pattern);
140
+ if (match) {
141
+ const oldVersion = match[1];
142
+ if (oldVersion !== newVersion) {
143
+ const oldFilePath = path.join(outputDir, entry.name);
144
+ this.logger.info(`Removing old version: ${entry.name}`);
145
+ fs.unlinkSync(oldFilePath);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ catch (error) {
151
+ this.logger.warn(`Could not clean up old versions: ${this.formatError(error)}`);
152
+ }
153
+ }
154
+ escapeRegex(str) {
155
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
156
+ }
129
157
  }
130
158
  export const installCommand = new InstallCommand().getCommand();
@@ -76,9 +76,9 @@ export class LoginCommand extends BaseCommand {
76
76
  network: options.network,
77
77
  };
78
78
  }
79
- return this.interactiveLogin(options.network, mldsaLevel);
79
+ return this.interactiveLogin(options.network);
80
80
  }
81
- async interactiveLogin(defaultNetwork, defaultLevel) {
81
+ async interactiveLogin(defaultNetwork) {
82
82
  this.logger.info('OPNet Wallet Configuration\n');
83
83
  const loginMethod = await select({
84
84
  message: 'How would you like to authenticate?',
@@ -104,27 +104,8 @@ export class LoginCommand extends BaseCommand {
104
104
  ],
105
105
  default: defaultNetwork,
106
106
  }));
107
- const selectedLevel = (await select({
108
- message: 'Select MLDSA security level:',
109
- choices: [
110
- {
111
- name: 'MLDSA-44 (Level 2, fastest)',
112
- value: 44,
113
- description: '1312 byte public key',
114
- },
115
- {
116
- name: 'MLDSA-65 (Level 3, balanced)',
117
- value: 65,
118
- description: '1952 byte public key',
119
- },
120
- {
121
- name: 'MLDSA-87 (Level 5, most secure)',
122
- value: 87,
123
- description: '2592 byte public key',
124
- },
125
- ],
126
- default: defaultLevel,
127
- }));
107
+ const selectedLevel = 44;
108
+ this.logger.info('Using MLDSA-44 (only supported level on OPNet)');
128
109
  if (loginMethod === 'mnemonic') {
129
110
  const mnemonic = await password({
130
111
  message: 'Enter your mnemonic phrase (12 or 24 words):',
@@ -1,6 +1,6 @@
1
1
  import { confirm } from '@inquirer/prompts';
2
2
  import { BaseCommand } from './BaseCommand.js';
3
- import { getPackage, getRegistryContract, getVersion, isVersionImmutable, } from '../lib/registry.js';
3
+ import { getPackage, getRegistryContract, getVersion, isVersionImmutable } from '../lib/registry.js';
4
4
  import { canSign, loadCredentials } from '../lib/credentials.js';
5
5
  import { CLIWallet } from '../lib/wallet.js';
6
6
  import { buildTransactionParams, checkBalance, formatSats, getWalletAddress, } from '../lib/transaction.js';
@@ -6,7 +6,7 @@ import { CLIWallet } from '../lib/wallet.js';
6
6
  import { canSign, loadCredentials } from '../lib/credentials.js';
7
7
  import { uploadDirectory, uploadFile } from '../lib/ipfs.js';
8
8
  import { formatFileSize } from '../lib/binary.js';
9
- import { getContenthash, getContenthashTypeName, getDomain, getResolverContract, getSubdomain, isSubdomain, parseDomainName, } from '../lib/resolver.js';
9
+ import { getResolverContract, getDomain, getSubdomain, getContenthash, getContenthashTypeName, isSubdomain, parseDomainName, } from '../lib/resolver.js';
10
10
  import { buildTransactionParams, checkBalance, DEFAULT_FEE_RATE, DEFAULT_MAX_SAT_TO_SPEND, formatSats, getWalletAddress, waitForTransactionConfirmation, } from '../lib/transaction.js';
11
11
  export class WebsiteDeployCommand extends BaseCommand {
12
12
  constructor() {
@@ -17,7 +17,7 @@ export class WebsiteDeployCommand extends BaseCommand {
17
17
  .argument('<domain>', 'Domain name (e.g., mysite or mysite.btc)')
18
18
  .argument('<path>', 'Path to website directory or HTML file')
19
19
  .option('-n, --network <network>', 'Network to use', 'mainnet')
20
- .option('--dry-run', "Upload to IPFS but don't update on-chain")
20
+ .option('--dry-run', 'Upload to IPFS but don\'t update on-chain')
21
21
  .option('-y, --yes', 'Skip confirmation prompts')
22
22
  .action((domain, websitePath, options) => this.execute(domain, websitePath, options || { network: 'mainnet' }));
23
23
  }
@@ -84,7 +84,7 @@ export class WebsiteDeployCommand extends BaseCommand {
84
84
  if (wallet.address.toHex() !== ownerAddress) {
85
85
  this.logger.fail('You are not the owner of this domain');
86
86
  this.logger.log(`Domain owner: ${ownerAddress}`);
87
- this.logger.log(`Your address: ${wallet.p2trAddress}`);
87
+ this.logger.log(`Your address: ${wallet.address.toHex()}`);
88
88
  process.exit(1);
89
89
  }
90
90
  this.logger.success('Ownership verified');
@@ -2,7 +2,7 @@ import { confirm } from '@inquirer/prompts';
2
2
  import { BaseCommand } from './BaseCommand.js';
3
3
  import { CLIWallet } from '../lib/wallet.js';
4
4
  import { canSign, loadCredentials } from '../lib/credentials.js';
5
- import { detectContenthashType, getContenthash, getContenthashTypeName, getDomain, getResolverContract, getSubdomain, isSubdomain, parseDomainName, validateCIDv0, validateCIDv1, validateIPNS, } from '../lib/resolver.js';
5
+ import { getResolverContract, getDomain, getSubdomain, getContenthash, detectContenthashType, validateCIDv0, validateCIDv1, validateIPNS, getContenthashTypeName, isSubdomain, parseDomainName, } from '../lib/resolver.js';
6
6
  import { buildTransactionParams, checkBalance, DEFAULT_FEE_RATE, DEFAULT_MAX_SAT_TO_SPEND, formatSats, getWalletAddress, waitForTransactionConfirmation, } from '../lib/transaction.js';
7
7
  import { CONTENTHASH_TYPE_CIDv0, CONTENTHASH_TYPE_CIDv1, CONTENTHASH_TYPE_IPNS, CONTENTHASH_TYPE_SHA256, } from '../types/BtcResolver.js';
8
8
  export class WebsitePublishCommand extends BaseCommand {
@@ -110,8 +110,7 @@ export class WebsitePublishCommand extends BaseCommand {
110
110
  if (wallet.address.toHex() !== ownerAddress) {
111
111
  this.logger.fail('You are not the owner of this domain');
112
112
  this.logger.log(`Domain owner: ${ownerAddress}`);
113
- this.logger.log(`Your address: ${wallet.p2trAddress}`);
114
- this.logger.log(`MLDSA Public Key Hash: ${wallet.address.toHex()}`);
113
+ this.logger.log(`Your address: ${wallet.address.toHex()}`);
115
114
  process.exit(1);
116
115
  }
117
116
  this.logger.success('Ownership verified');
@@ -10,6 +10,9 @@ export declare function pinToIPFS(data: Buffer, name?: string): Promise<PinResul
10
10
  export declare function fetchFromIPFS(cid: string): Promise<FetchResult>;
11
11
  export declare function buildGatewayUrl(gateway: string, cid: string): string;
12
12
  export declare function isValidCid(cid: string): boolean;
13
+ export declare function isCIDv0(cid: string): boolean;
14
+ export declare function isCIDv1(cid: string): boolean;
15
+ export declare function cidV0toV1(cidv0: string): string;
13
16
  export declare function downloadPlugin(cid: string, outputPath: string): Promise<number>;
14
17
  export declare function uploadPlugin(filePath: string): Promise<PinResult>;
15
18
  export interface DirectoryPinResult {
@@ -17,5 +20,5 @@ export interface DirectoryPinResult {
17
20
  files: number;
18
21
  totalSize: number;
19
22
  }
20
- export declare function uploadDirectory(dirPath: string, wrapWithDirectory?: boolean): Promise<DirectoryPinResult>;
23
+ export declare function uploadDirectory(dirPath: string, _wrapWithDirectory?: boolean): Promise<DirectoryPinResult>;
21
24
  export declare function uploadFile(filePath: string): Promise<PinResult>;
package/build/lib/ipfs.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as https from 'https';
2
2
  import * as http from 'http';
3
3
  import * as fs from 'fs';
4
+ import ora from 'ora';
4
5
  import { loadConfig } from './config.js';
5
6
  const DEFAULT_MAX_REDIRECTS = 10;
6
7
  async function httpRequest(url, options, redirectCount = 0) {
@@ -23,13 +24,17 @@ async function httpRequest(url, options, redirectCount = 0) {
23
24
  };
24
25
  const req = lib.request(reqOptions, (res) => {
25
26
  const statusCode = res.statusCode ?? 0;
26
- if (followRedirect && statusCode >= 300 && statusCode < 400 && res.headers.location) {
27
- const redirectUrl = new URL(res.headers.location, url).href;
28
- res.resume();
29
- httpRequest(redirectUrl, options, redirectCount + 1)
30
- .then(resolve)
31
- .catch(reject);
32
- return;
27
+ if (followRedirect && statusCode >= 300 && statusCode < 400) {
28
+ const locationHeader = res.headers.location || res.headers['Location'];
29
+ const location = Array.isArray(locationHeader) ? locationHeader[0] : locationHeader;
30
+ if (location) {
31
+ const redirectUrl = new URL(location, url).href;
32
+ res.resume();
33
+ httpRequest(redirectUrl, options, redirectCount + 1)
34
+ .then(resolve)
35
+ .catch(reject);
36
+ return;
37
+ }
33
38
  }
34
39
  const chunks = [];
35
40
  res.on('data', (chunk) => {
@@ -37,6 +42,17 @@ async function httpRequest(url, options, redirectCount = 0) {
37
42
  });
38
43
  res.on('end', () => {
39
44
  const body = Buffer.concat(chunks);
45
+ if (followRedirect && statusCode >= 300 && statusCode < 400) {
46
+ const bodyStr = body.toString();
47
+ const hrefMatch = bodyStr.match(/href="([^"]+)"/);
48
+ if (hrefMatch && hrefMatch[1]) {
49
+ const redirectUrl = new URL(hrefMatch[1], url).href;
50
+ httpRequest(redirectUrl, options, redirectCount + 1)
51
+ .then(resolve)
52
+ .catch(reject);
53
+ return;
54
+ }
55
+ }
40
56
  if (statusCode >= 200 && statusCode < 300) {
41
57
  resolve(body);
42
58
  }
@@ -53,9 +69,37 @@ async function httpRequest(url, options, redirectCount = 0) {
53
69
  reject(new Error('Request timeout'));
54
70
  });
55
71
  if (options.body) {
56
- req.write(options.body);
72
+ const bodyBuffer = Buffer.isBuffer(options.body) ? options.body : Buffer.from(options.body);
73
+ const totalBytes = bodyBuffer.length;
74
+ const chunkSize = 64 * 1024;
75
+ let bytesSent = 0;
76
+ const writeChunk = () => {
77
+ while (bytesSent < totalBytes) {
78
+ const end = Math.min(bytesSent + chunkSize, totalBytes);
79
+ const chunk = bodyBuffer.subarray(bytesSent, end);
80
+ const canContinue = req.write(chunk);
81
+ bytesSent += chunk.length;
82
+ if (options.onProgress) {
83
+ options.onProgress(bytesSent, totalBytes);
84
+ }
85
+ if (!canContinue) {
86
+ req.once('drain', writeChunk);
87
+ return;
88
+ }
89
+ }
90
+ if (options.onUploadComplete) {
91
+ options.onUploadComplete();
92
+ }
93
+ req.end();
94
+ };
95
+ writeChunk();
96
+ }
97
+ else {
98
+ if (options.onUploadComplete) {
99
+ options.onUploadComplete();
100
+ }
101
+ req.end();
57
102
  }
58
- req.end();
59
103
  });
60
104
  }
61
105
  export async function pinToIPFS(data, name) {
@@ -141,6 +185,9 @@ export async function pinToIPFS(data, name) {
141
185
  if (!cid) {
142
186
  throw new Error(`Failed to extract CID from pinning response: ${JSON.stringify(result)}`);
143
187
  }
188
+ if (isCIDv0(cid)) {
189
+ cid = cidV0toV1(cid);
190
+ }
144
191
  return {
145
192
  cid,
146
193
  size: data.length,
@@ -204,6 +251,73 @@ export function isValidCid(cid) {
204
251
  }
205
252
  return false;
206
253
  }
254
+ export function isCIDv0(cid) {
255
+ return cid.startsWith('Qm') && cid.length === 46;
256
+ }
257
+ export function isCIDv1(cid) {
258
+ return cid.startsWith('baf') && cid.length >= 50;
259
+ }
260
+ const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
261
+ const BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567';
262
+ function decodeBase58(str) {
263
+ const bytes = [];
264
+ for (const char of str) {
265
+ const value = BASE58_ALPHABET.indexOf(char);
266
+ if (value === -1) {
267
+ throw new Error(`Invalid base58 character: ${char}`);
268
+ }
269
+ let carry = value;
270
+ for (let i = 0; i < bytes.length; i++) {
271
+ carry += bytes[i] * 58;
272
+ bytes[i] = carry & 0xff;
273
+ carry >>= 8;
274
+ }
275
+ while (carry > 0) {
276
+ bytes.push(carry & 0xff);
277
+ carry >>= 8;
278
+ }
279
+ }
280
+ for (const char of str) {
281
+ if (char === '1') {
282
+ bytes.push(0);
283
+ }
284
+ else {
285
+ break;
286
+ }
287
+ }
288
+ return new Uint8Array(bytes.reverse());
289
+ }
290
+ function encodeBase32(bytes) {
291
+ let result = '';
292
+ let bits = 0;
293
+ let value = 0;
294
+ for (const byte of bytes) {
295
+ value = (value << 8) | byte;
296
+ bits += 8;
297
+ while (bits >= 5) {
298
+ bits -= 5;
299
+ result += BASE32_ALPHABET[(value >> bits) & 0x1f];
300
+ }
301
+ }
302
+ if (bits > 0) {
303
+ result += BASE32_ALPHABET[(value << (5 - bits)) & 0x1f];
304
+ }
305
+ return result;
306
+ }
307
+ export function cidV0toV1(cidv0) {
308
+ if (!isCIDv0(cidv0)) {
309
+ return cidv0;
310
+ }
311
+ const multihash = decodeBase58(cidv0);
312
+ if (multihash[0] !== 0x12 || multihash[1] !== 0x20 || multihash.length !== 34) {
313
+ throw new Error('Invalid CIDv0: not a valid sha2-256 multihash');
314
+ }
315
+ const cidv1Bytes = new Uint8Array(2 + multihash.length);
316
+ cidv1Bytes[0] = 0x01;
317
+ cidv1Bytes[1] = 0x70;
318
+ cidv1Bytes.set(multihash, 2);
319
+ return 'b' + encodeBase32(cidv1Bytes);
320
+ }
207
321
  export async function downloadPlugin(cid, outputPath) {
208
322
  const result = await fetchFromIPFS(cid);
209
323
  fs.writeFileSync(outputPath, result.data);
@@ -257,74 +371,176 @@ function getMimeType(filePath) {
257
371
  };
258
372
  return mimeTypes[ext] || 'application/octet-stream';
259
373
  }
260
- export async function uploadDirectory(dirPath, wrapWithDirectory = true) {
261
- try {
262
- const config = loadConfig();
263
- const endpoint = config.ipfsPinningEndpoint;
264
- if (!endpoint) {
265
- throw new Error('IPFS pinning endpoint not configured. Run `opnet config set ipfsPinningEndpoint <url>`');
266
- }
267
- const files = getAllFiles(dirPath);
268
- if (files.length === 0) {
269
- throw new Error('Directory is empty');
270
- }
374
+ function generateSessionId() {
375
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
376
+ const r = (Math.random() * 16) | 0;
377
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
378
+ return v.toString(16);
379
+ });
380
+ }
381
+ function sleep(ms) {
382
+ return new Promise(resolve => setTimeout(resolve, ms));
383
+ }
384
+ async function mfsCall(endpoint, apiPath, params, body, headers, maxRetries = 3) {
385
+ const url = new URL(endpoint);
386
+ url.pathname = apiPath;
387
+ for (const [key, value] of Object.entries(params)) {
388
+ url.searchParams.set(key, value);
389
+ }
390
+ const reqHeaders = {
391
+ ...headers,
392
+ };
393
+ let formBody;
394
+ if (body) {
271
395
  const boundary = '----FormBoundary' + Math.random().toString(36).substring(2);
272
396
  const formParts = [];
273
- let totalSize = 0;
274
- for (const file of files) {
275
- const data = fs.readFileSync(file.fullPath);
276
- totalSize += data.length;
277
- const mimeType = getMimeType(file.path);
278
- formParts.push(Buffer.from(`--${boundary}\r\n` +
279
- `Content-Disposition: form-data; name="file"; filename="${file.path}"\r\n` +
280
- `Content-Type: ${mimeType}\r\n\r\n`));
281
- formParts.push(data);
282
- formParts.push(Buffer.from('\r\n'));
283
- }
284
- formParts.push(Buffer.from(`--${boundary}--\r\n`));
285
- const body = Buffer.concat(formParts);
286
- const headers = {
287
- 'Content-Type': `multipart/form-data; boundary=${boundary}`,
288
- 'Content-Length': body.length.toString(),
289
- };
290
- if (config.ipfsPinningApiKey) {
291
- headers['Authorization'] = `Bearer ${config.ipfsPinningApiKey}`;
292
- }
293
- const url = new URL(endpoint);
294
- if (wrapWithDirectory) {
295
- url.searchParams.set('wrap-with-directory', 'true');
397
+ formParts.push(Buffer.from(`--${boundary}\r\n` +
398
+ `Content-Disposition: form-data; name="file"\r\n` +
399
+ `Content-Type: application/octet-stream\r\n\r\n`));
400
+ formParts.push(body);
401
+ formParts.push(Buffer.from(`\r\n--${boundary}--\r\n`));
402
+ formBody = Buffer.concat(formParts);
403
+ reqHeaders['Content-Type'] = `multipart/form-data; boundary=${boundary}`;
404
+ reqHeaders['Content-Length'] = formBody.length.toString();
405
+ }
406
+ let lastError;
407
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
408
+ try {
409
+ return await httpRequest(url.toString(), {
410
+ method: 'POST',
411
+ headers: reqHeaders,
412
+ body: formBody,
413
+ timeout: body ? 120000 : 60000,
414
+ followRedirect: true,
415
+ });
296
416
  }
297
- url.searchParams.set('cid-version', '1');
298
- const response = await httpRequest(url.toString(), {
299
- method: 'POST',
300
- headers,
301
- body,
302
- timeout: 300000,
303
- followRedirect: true,
304
- });
305
- const lines = response.toString().trim().split('\n');
306
- let rootCid;
307
- for (const line of lines) {
308
- if (!line.trim())
417
+ catch (e) {
418
+ lastError = e instanceof Error ? e : new Error(String(e));
419
+ if (lastError.message.includes('429')) {
420
+ const backoffMs = Math.pow(2, attempt) * 1000;
421
+ await sleep(backoffMs);
309
422
  continue;
310
- const result = JSON.parse(line);
311
- if (result.Hash && (result.Name === '' || !result.Name)) {
312
- rootCid = result.Hash;
313
423
  }
314
- else if (result.Hash && !rootCid) {
315
- rootCid = result.Hash;
424
+ throw lastError;
425
+ }
426
+ }
427
+ throw lastError || new Error('Max retries exceeded');
428
+ }
429
+ export async function uploadDirectory(dirPath, _wrapWithDirectory = true) {
430
+ const config = loadConfig();
431
+ const endpoint = config.ipfsPinningEndpoint;
432
+ if (!endpoint) {
433
+ throw new Error('IPFS pinning endpoint not configured. Run `opnet config set ipfsPinningEndpoint <url>`');
434
+ }
435
+ const baseUrl = endpoint.replace(/\/api\/v0\/add\/?$/, '');
436
+ const files = getAllFiles(dirPath);
437
+ if (files.length === 0) {
438
+ throw new Error('Directory is empty');
439
+ }
440
+ let totalSize = 0;
441
+ for (const file of files) {
442
+ const stat = fs.statSync(file.fullPath);
443
+ totalSize += stat.size;
444
+ }
445
+ const sessionId = generateSessionId();
446
+ const mfsPath = `/uploads/${sessionId}`;
447
+ const headers = {};
448
+ if (config.ipfsPinningApiKey) {
449
+ headers['Authorization'] = `Bearer ${config.ipfsPinningApiKey}`;
450
+ }
451
+ const formatBytes = (bytes) => {
452
+ if (bytes < 1024)
453
+ return `${bytes} B`;
454
+ if (bytes < 1024 * 1024)
455
+ return `${(bytes / 1024).toFixed(1)} KB`;
456
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
457
+ };
458
+ let uploadedBytes = 0;
459
+ let spinner = null;
460
+ try {
461
+ await mfsCall(baseUrl, '/api/v0/files/mkdir', {
462
+ arg: mfsPath,
463
+ parents: 'true',
464
+ }, undefined, headers);
465
+ for (let i = 0; i < files.length; i++) {
466
+ const file = files[i];
467
+ const data = fs.readFileSync(file.fullPath);
468
+ const fileMfsPath = `${mfsPath}/${file.path}`;
469
+ const parentDir = fileMfsPath.substring(0, fileMfsPath.lastIndexOf('/'));
470
+ if (parentDir !== mfsPath) {
471
+ await mfsCall(baseUrl, '/api/v0/files/mkdir', {
472
+ arg: parentDir,
473
+ parents: 'true',
474
+ }, undefined, headers);
316
475
  }
476
+ await mfsCall(baseUrl, '/api/v0/files/write', {
477
+ arg: fileMfsPath,
478
+ create: 'true',
479
+ parents: 'true',
480
+ truncate: 'true',
481
+ }, data, headers);
482
+ uploadedBytes += data.length;
483
+ const percent = Math.round((uploadedBytes / totalSize) * 100);
484
+ const barWidth = 30;
485
+ const filled = Math.round((percent / 100) * barWidth);
486
+ const empty = barWidth - filled;
487
+ const bar = '█'.repeat(filled) + '░'.repeat(empty);
488
+ process.stdout.write(`\r Uploading: [${bar}] ${percent}% - ${i + 1}/${files.length} files (${formatBytes(uploadedBytes)}/${formatBytes(totalSize)})`);
317
489
  }
318
- if (!rootCid) {
319
- throw new Error(`Failed to extract root CID from response`);
490
+ process.stdout.write('\n');
491
+ spinner = ora({
492
+ text: 'Getting directory CID...',
493
+ spinner: 'dots',
494
+ }).start();
495
+ const statResponse = await mfsCall(baseUrl, '/api/v0/files/stat', {
496
+ arg: mfsPath,
497
+ hash: 'true',
498
+ 'cid-base': 'base32',
499
+ }, undefined, headers);
500
+ const statResult = JSON.parse(statResponse.toString());
501
+ let cid = statResult.Hash;
502
+ if (!cid) {
503
+ spinner.fail('Failed to get directory CID');
504
+ throw new Error('MFS stat did not return a Hash');
505
+ }
506
+ if (isCIDv0(cid)) {
507
+ cid = cidV0toV1(cid);
508
+ }
509
+ spinner.text = 'Pinning content...';
510
+ try {
511
+ await mfsCall(baseUrl, '/api/v0/pin/add', {
512
+ arg: cid,
513
+ }, undefined, headers);
514
+ }
515
+ catch {
516
+ }
517
+ spinner.succeed(`Upload complete: ${cid}`);
518
+ try {
519
+ await mfsCall(baseUrl, '/api/v0/files/rm', {
520
+ arg: mfsPath,
521
+ recursive: 'true',
522
+ }, undefined, headers);
523
+ }
524
+ catch {
320
525
  }
321
526
  return {
322
- cid: rootCid,
527
+ cid,
323
528
  files: files.length,
324
529
  totalSize,
325
530
  };
326
531
  }
327
532
  catch (e) {
533
+ if (spinner) {
534
+ spinner.fail('Upload failed');
535
+ }
536
+ try {
537
+ await mfsCall(baseUrl, '/api/v0/files/rm', {
538
+ arg: mfsPath,
539
+ recursive: 'true',
540
+ }, undefined, headers);
541
+ }
542
+ catch {
543
+ }
328
544
  throw new Error(`IPFS directory upload failed: ${e instanceof Error ? e.message : String(e)}`);
329
545
  }
330
546
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btc-vision/cli",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "type": "module",
5
5
  "description": "CLI for the OPNet plugin ecosystem - scaffolding, compilation, signing, and registry interaction",
6
6
  "author": "OP_NET",