ethnotary 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/index.js ADDED
@@ -0,0 +1,624 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require('commander');
4
+ const chalk = require('chalk');
5
+ const program = new Command();
6
+
7
+ // Load environment variables
8
+ require('dotenv').config();
9
+
10
+ // Package info
11
+ const pkg = require('../package.json');
12
+
13
+ // Check RPC configuration on startup - warn but don't exit
14
+ async function checkRpcConfig() {
15
+ const { listNetworks } = require('./utils/networks');
16
+ const networks = listNetworks();
17
+ const configured = networks.filter(n => n.configured);
18
+
19
+ if (configured.length === 0) {
20
+ console.log(chalk.yellow('\n⚠️ No RPC URLs configured. Some commands may not work.'));
21
+ console.log(chalk.gray('Run a transaction command to be prompted for an RPC URL.\n'));
22
+ }
23
+ }
24
+
25
+ // Run async check before starting CLI
26
+ (async () => {
27
+ await checkRpcConfig();
28
+
29
+ program
30
+ .name('ethnotary')
31
+ .description('CLI for managing MultiSig accounts, transactions, and data queries across EVM networks')
32
+ .version(pkg.version);
33
+
34
+ // Global options available to all commands
35
+ program
36
+ .option('--json', 'Output in JSON format (machine-readable)')
37
+ .option('--private-key <key>', 'Use this private key directly')
38
+ .option('--network <name>', 'Network to use (defaults to all contract networks for data commands)')
39
+ .option('--yes', 'Skip all confirmation prompts')
40
+ .option('--dry-run', 'Simulate without executing');
41
+
42
+ // Register command groups
43
+ program.addCommand(require('./commands/wallet'));
44
+ program.addCommand(require('./commands/account'));
45
+ program.addCommand(require('./commands/tx'));
46
+ program.addCommand(require('./commands/data'));
47
+ program.addCommand(require('./commands/contract'));
48
+ program.addCommand(require('./commands/contact'));
49
+ program.addCommand(require('./commands/config'));
50
+
51
+ // Top-level shortcuts (like git)
52
+ const { setDefaultContract, getDefaultContract, listContracts } = require('./utils/contracts');
53
+
54
+ // ethnotary checkout <alias> - shortcut for contract checkout
55
+ program
56
+ .command('checkout <alias>')
57
+ .description('Switch to a different contract (like git checkout)')
58
+ .action((alias) => {
59
+ try {
60
+ setDefaultContract(alias);
61
+ const current = getDefaultContract();
62
+ console.log(chalk.green(`✓ Switched to "${alias}"`));
63
+ console.log(chalk.gray(` Address: ${current.address}`));
64
+ console.log(chalk.gray(` Network: ${current.network}`));
65
+ } catch (error) {
66
+ console.log(chalk.red(`✗ ${error.message}`));
67
+ process.exit(1);
68
+ }
69
+ });
70
+
71
+ // ethnotary status - show current contract (like git status)
72
+ program
73
+ .command('status')
74
+ .description('Show the currently active contract')
75
+ .action(() => {
76
+ const current = getDefaultContract();
77
+ if (current) {
78
+ // Migrate old format if needed
79
+ const networks = current.networks || (current.network ? [current.network] : []);
80
+ console.log(chalk.cyan(`On account: ${chalk.bold(current.alias)}`));
81
+ console.log(chalk.gray(` Address: ${current.address}`));
82
+ console.log(chalk.gray(` Networks: ${networks.join(', ') || 'none'}`));
83
+ if (current.label) console.log(chalk.gray(` Label: ${current.label}`));
84
+ } else {
85
+ console.log(chalk.yellow('No contract checked out.'));
86
+ console.log(chalk.gray('Use "ethnotary checkout <alias>" to switch to a contract.'));
87
+ const contracts = listContracts();
88
+ if (contracts.length > 0) {
89
+ console.log(chalk.gray(`\nAvailable contracts: ${contracts.map(c => c.alias).join(', ')}`));
90
+ }
91
+ }
92
+ });
93
+
94
+ // ethnotary list - list all saved contracts
95
+ program
96
+ .command('list')
97
+ .description('List all saved contracts')
98
+ .action(() => {
99
+ const contracts = listContracts();
100
+ const current = getDefaultContract();
101
+ const opts = program.opts();
102
+
103
+ if (opts.json) {
104
+ console.log(JSON.stringify({
105
+ count: contracts.length,
106
+ current: current?.alias || null,
107
+ contracts
108
+ }, null, 2));
109
+ return;
110
+ }
111
+
112
+ if (contracts.length === 0) {
113
+ console.log(chalk.yellow('No contracts saved.'));
114
+ console.log(chalk.gray('Use "ethnotary add" to save a contract.'));
115
+ return;
116
+ }
117
+
118
+ console.log(chalk.bold(`\nSaved Contracts (${contracts.length}):\n`));
119
+ for (const c of contracts) {
120
+ const isActive = current && current.alias === c.alias;
121
+ const marker = isActive ? chalk.green('* ') : ' ';
122
+ const name = isActive ? chalk.green.bold(c.alias) : c.alias;
123
+ console.log(`${marker}${name}`);
124
+ console.log(chalk.gray(` Address: ${c.address}`));
125
+ // Support both old (network) and new (networks) format
126
+ const networks = c.networks || (c.network ? [c.network] : []);
127
+ console.log(chalk.gray(` Networks: ${networks.join(', ')}`));
128
+ if (c.label) console.log(chalk.gray(` Label: ${c.label}`));
129
+ if (c.decoupledFrom) console.log(chalk.yellow(` ⚠️ Decoupled from: ${c.decoupledFrom}`));
130
+ }
131
+ console.log('');
132
+ });
133
+
134
+ // ethnotary add - save a contract (shortcut for contract add)
135
+ const { saveContract, removeContract } = require('./utils/contracts');
136
+ const { validateNetwork, ensureRpcConfigured, validateNetworks, ensureRpcsConfigured } = require('./utils/networks');
137
+ const { ethers } = require('ethers');
138
+
139
+ program
140
+ .command('add')
141
+ .description('Save a contract with an alias')
142
+ .requiredOption('--alias <alias>', 'Alias for the contract')
143
+ .requiredOption('--address <address>', 'Contract address')
144
+ .option('--chain-id <chainIds>', 'Chain ID(s), comma-separated (alternative to --network)')
145
+ .option('--label <label>', 'Optional label/description')
146
+ .option('--skip-validation', 'Skip Ethnotary contract validation')
147
+ .action(async (options) => {
148
+ const opts = program.opts();
149
+ try {
150
+ if (!ethers.isAddress(options.address)) {
151
+ console.log(chalk.red(`✗ Invalid address: ${options.address}`));
152
+ process.exit(1);
153
+ }
154
+
155
+ // Resolve network(s) from --network or --chain-id (supports comma-separated)
156
+ const networkIdentifier = options.chainId || opts.network;
157
+ if (!networkIdentifier) {
158
+ console.log(chalk.red('✗ Either --network or --chain-id is required'));
159
+ process.exit(1);
160
+ }
161
+
162
+ // Check if multiple networks
163
+ const isMultiple = networkIdentifier.includes(',');
164
+
165
+ if (isMultiple) {
166
+ // Multi-network add - validates on each network, saves under single alias
167
+ const networks = validateNetworks(networkIdentifier);
168
+ if (!networks) {
169
+ process.exit(1);
170
+ }
171
+
172
+ // Ensure all RPCs are configured
173
+ const networksWithRpc = await ensureRpcsConfigured(networks, { json: opts.json });
174
+ if (!networksWithRpc) {
175
+ process.exit(1);
176
+ }
177
+
178
+ if (!opts.json) {
179
+ console.log(chalk.cyan(`\nValidating contract on ${networksWithRpc.length} networks...\n`));
180
+ }
181
+
182
+ const validNetworks = [];
183
+ const failedNetworks = [];
184
+
185
+ for (const { key, config, rpc } of networksWithRpc) {
186
+ // Validate contract on this network
187
+ if (!options.skipValidation) {
188
+ if (!opts.json) {
189
+ console.log(chalk.gray(`Validating on ${config.name}...`));
190
+ }
191
+
192
+ const provider = new ethers.JsonRpcProvider(rpc);
193
+ const contract = new ethers.Contract(
194
+ options.address,
195
+ ['function isEthnotaryMultiSig() external view returns (bool)'],
196
+ provider
197
+ );
198
+
199
+ try {
200
+ const isValid = await contract.isEthnotaryMultiSig();
201
+ if (!isValid) {
202
+ console.log(chalk.red(`✗ Contract not found or invalid on ${config.name}`));
203
+ failedNetworks.push(key);
204
+ continue;
205
+ }
206
+ if (!opts.json) {
207
+ console.log(chalk.green(`✓ Valid on ${config.name}`));
208
+ }
209
+ } catch (err) {
210
+ console.log(chalk.red(`✗ Contract not found or invalid on ${config.name}`));
211
+ failedNetworks.push(key);
212
+ continue;
213
+ }
214
+ }
215
+ validNetworks.push(key);
216
+ }
217
+
218
+ if (validNetworks.length === 0) {
219
+ console.log(chalk.red('\n✗ Contract not valid on any of the specified networks'));
220
+ process.exit(1);
221
+ }
222
+
223
+ // Save contract with all valid networks under single alias
224
+ const saved = saveContract(options.alias, options.address, validNetworks, options.label);
225
+
226
+ if (opts.json) {
227
+ console.log(JSON.stringify({
228
+ alias: options.alias,
229
+ address: options.address,
230
+ networks: validNetworks,
231
+ failedNetworks: failedNetworks,
232
+ label: options.label || '',
233
+ created: saved.created
234
+ }, null, 2));
235
+ } else {
236
+ console.log(chalk.green(`\n✓ Contract saved as "${options.alias}"`));
237
+ console.log(chalk.gray(` Address: ${options.address}`));
238
+ console.log(chalk.gray(` Networks: ${validNetworks.join(', ')}`));
239
+ if (failedNetworks.length > 0) {
240
+ console.log(chalk.yellow(` Failed: ${failedNetworks.join(', ')}`));
241
+ }
242
+ }
243
+ } else {
244
+ // Single network add
245
+ const resolved = validateNetwork(networkIdentifier);
246
+ if (!resolved) {
247
+ process.exit(1);
248
+ }
249
+
250
+ const rpc = await ensureRpcConfigured(resolved.key, resolved.config, { json: opts.json });
251
+ if (!rpc) {
252
+ process.exit(1);
253
+ }
254
+
255
+ // Validate that this is an Ethnotary MultiSig contract
256
+ if (!options.skipValidation) {
257
+ if (!opts.json) {
258
+ console.log(chalk.gray('Validating Ethnotary contract...'));
259
+ }
260
+
261
+ const provider = new ethers.JsonRpcProvider(rpc);
262
+ const contract = new ethers.Contract(
263
+ options.address,
264
+ ['function isEthnotaryMultiSig() external view returns (bool)'],
265
+ provider
266
+ );
267
+
268
+ try {
269
+ const isValid = await contract.isEthnotaryMultiSig();
270
+ if (!isValid) {
271
+ console.log(chalk.red(`✗ Contract at ${options.address} is not a valid Ethnotary MultiSig`));
272
+ console.log(chalk.yellow('\nPlease double-check your contract address.'));
273
+ console.log(chalk.gray('To create a new Ethnotary MultiSig, run:'));
274
+ console.log(chalk.white(' ethnotary create --owners <addr1,addr2,...> --required <number> --pin <pin> --name <name>'));
275
+ process.exit(1);
276
+ }
277
+ } catch (err) {
278
+ console.log(chalk.red(`✗ Contract at ${options.address} is not a valid Ethnotary MultiSig`));
279
+ console.log(chalk.yellow('\nPlease double-check your contract address.'));
280
+ console.log(chalk.gray('To create a new Ethnotary MultiSig, run:'));
281
+ console.log(chalk.white(' ethnotary create --owners <addr1,addr2,...> --required <number> --pin <pin> --name <name>'));
282
+ process.exit(1);
283
+ }
284
+ }
285
+
286
+ const saved = saveContract(options.alias, options.address, resolved.key, options.label);
287
+
288
+ if (opts.json) {
289
+ console.log(JSON.stringify({
290
+ alias: options.alias,
291
+ address: options.address,
292
+ network: resolved.key,
293
+ chainId: resolved.config.chainId,
294
+ label: options.label || '',
295
+ created: saved.created
296
+ }, null, 2));
297
+ } else {
298
+ console.log(chalk.green(`✓ Contract saved as "${options.alias}"`));
299
+ console.log(chalk.gray(` Address: ${options.address}`));
300
+ console.log(chalk.gray(` Network: ${resolved.config.name} (${resolved.key})`));
301
+ console.log(chalk.gray(` Chain ID: ${resolved.config.chainId}`));
302
+ }
303
+ }
304
+ } catch (error) {
305
+ console.log(chalk.red(`✗ ${error.message}`));
306
+ process.exit(1);
307
+ }
308
+ });
309
+
310
+ // ethnotary remove - remove a saved contract (shortcut for contract remove)
311
+ program
312
+ .command('remove <alias>')
313
+ .description('Remove a saved contract')
314
+ .action((alias) => {
315
+ const opts = program.opts();
316
+ try {
317
+ removeContract(alias);
318
+ if (opts.json) {
319
+ console.log(JSON.stringify({ alias, status: 'removed' }, null, 2));
320
+ } else {
321
+ console.log(chalk.green(`✓ Contract "${alias}" removed`));
322
+ }
323
+ } catch (error) {
324
+ console.log(chalk.red(`✗ ${error.message}`));
325
+ process.exit(1);
326
+ }
327
+ });
328
+
329
+ // ethnotary create - shortcut for account create
330
+ program
331
+ .command('create')
332
+ .description('Deploy a new MultiSig account (shortcut for account create)')
333
+ .requiredOption('--owners <addresses>', 'Comma-separated list of owner addresses')
334
+ .requiredOption('--required <number>', 'Number of required confirmations', parseInt)
335
+ .requiredOption('--pin <pin>', 'PIN for account management')
336
+ .requiredOption('--name <name>', 'Name for the MultiSig account')
337
+ .option('--chain-id <chainIds>', 'Chain ID(s), comma-separated (alternative to --network)')
338
+ .action(async (options) => {
339
+ const opts = program.opts();
340
+ const { ethers } = require('ethers');
341
+ const { getWallet } = require('./utils/auth');
342
+ const { computePinHash } = require('./utils/pin');
343
+ const { listContracts, removeContract } = require('./utils/contracts');
344
+ const { MSA_FACTORY_ABI, getFactoryAddress } = require('./utils/constants');
345
+
346
+ // Resolve network(s) from --network or --chain-id
347
+ const networkIdentifier = options.chainId || opts.network;
348
+ if (!networkIdentifier) {
349
+ console.log(chalk.red('✗ Either --network or --chain-id is required'));
350
+ process.exit(1);
351
+ }
352
+
353
+ // Parse owners
354
+ const owners = options.owners.split(',').map(addr => addr.trim());
355
+ for (const owner of owners) {
356
+ if (!ethers.isAddress(owner)) {
357
+ console.log(chalk.red(`✗ Invalid owner address: ${owner}`));
358
+ process.exit(1);
359
+ }
360
+ }
361
+
362
+ if (options.required < 1 || options.required > owners.length) {
363
+ console.log(chalk.red(`✗ Required confirmations must be between 1 and ${owners.length}`));
364
+ process.exit(1);
365
+ }
366
+
367
+ // Parse networks
368
+ const networkKeys = networkIdentifier.split(',').map(n => n.trim());
369
+ const networksToProcess = [];
370
+
371
+ for (const netKey of networkKeys) {
372
+ const resolved = validateNetwork(netKey);
373
+ if (!resolved) {
374
+ process.exit(1);
375
+ }
376
+ networksToProcess.push(resolved);
377
+ }
378
+
379
+ // Ensure all RPCs are configured
380
+ const networksWithRpc = await ensureRpcsConfigured(
381
+ networksToProcess.map(n => n),
382
+ { json: opts.json }
383
+ );
384
+ if (!networksWithRpc) {
385
+ process.exit(1);
386
+ }
387
+
388
+ // Get wallet ONCE (single password prompt)
389
+ let wallet;
390
+ try {
391
+ wallet = await getWallet(opts);
392
+ } catch (err) {
393
+ console.log(chalk.red(`✗ ${err.message}`));
394
+ process.exit(1);
395
+ }
396
+
397
+ // Compute PIN hash
398
+ const pinHash = computePinHash(options.pin);
399
+
400
+ // Run pre-flight / dry-run across all networks
401
+ if (!opts.json) {
402
+ console.log(chalk.cyan(`\n📋 Pre-flight Check for ${networksWithRpc.length} network(s)...\n`));
403
+ }
404
+
405
+ const preflightResults = [];
406
+ for (const { key, config, rpc } of networksWithRpc) {
407
+ try {
408
+ const provider = new ethers.JsonRpcProvider(rpc);
409
+ const signer = wallet.connect(provider);
410
+
411
+ const factoryAddress = getFactoryAddress(key);
412
+ const factory = new ethers.Contract(factoryAddress, MSA_FACTORY_ABI, signer);
413
+
414
+ // Predict address
415
+ let predictedAddress = null;
416
+ try {
417
+ predictedAddress = await factory.predictMSAAddress(owners, options.required, pinHash, options.name);
418
+ } catch (e) {
419
+ // Factory might not be deployed
420
+ }
421
+
422
+ // Check if already deployed
423
+ let alreadyDeployed = false;
424
+ if (predictedAddress) {
425
+ const code = await provider.getCode(predictedAddress);
426
+ alreadyDeployed = code !== '0x';
427
+ }
428
+
429
+ // Get wallet balance and estimate gas
430
+ const balance = await provider.getBalance(wallet.address);
431
+ const feeData = await provider.getFeeData();
432
+ const gasPrice = feeData.gasPrice || feeData.maxFeePerGas || ethers.parseUnits('20', 'gwei');
433
+ const estimatedGas = 500000n; // Approximate gas for deployment
434
+ const deploymentFee = 1000000000000000n; // 0.001 ETH
435
+ const estimatedCost = (estimatedGas * gasPrice) + deploymentFee;
436
+ const sufficient = balance >= estimatedCost;
437
+
438
+ preflightResults.push({
439
+ network: key,
440
+ networkName: config.name,
441
+ rpc,
442
+ predictedAddress,
443
+ alreadyDeployed,
444
+ balance: ethers.formatEther(balance),
445
+ estimatedCost: ethers.formatEther(estimatedCost),
446
+ sufficient,
447
+ ready: sufficient && !alreadyDeployed && predictedAddress !== null,
448
+ error: predictedAddress === null ? 'Factory not deployed or not responding' : null
449
+ });
450
+
451
+ if (!opts.json) {
452
+ if (alreadyDeployed) {
453
+ console.log(chalk.yellow(` ⚠ ${config.name}: Already deployed at ${predictedAddress}`));
454
+ } else if (!predictedAddress) {
455
+ console.log(chalk.red(` ✗ ${config.name}: Factory not available`));
456
+ } else if (!sufficient) {
457
+ console.log(chalk.red(` ✗ ${config.name}: Insufficient balance`));
458
+ console.log(chalk.gray(` Balance: ${ethers.formatEther(balance)} ETH`));
459
+ console.log(chalk.gray(` Required: ~${ethers.formatEther(estimatedCost)} ETH`));
460
+ } else {
461
+ console.log(chalk.green(` ✓ ${config.name}: Ready`));
462
+ console.log(chalk.gray(` Predicted: ${predictedAddress}`));
463
+ console.log(chalk.gray(` Balance: ${ethers.formatEther(balance)} ETH`));
464
+ console.log(chalk.gray(` Est. cost: ~${ethers.formatEther(estimatedCost)} ETH`));
465
+ }
466
+ }
467
+ } catch (err) {
468
+ preflightResults.push({
469
+ network: key,
470
+ networkName: config.name,
471
+ ready: false,
472
+ error: err.message
473
+ });
474
+ if (!opts.json) {
475
+ console.log(chalk.red(` ✗ ${config.name}: ${err.message}`));
476
+ }
477
+ }
478
+ }
479
+
480
+ const readyNetworks = preflightResults.filter(r => r.ready);
481
+ const notReadyNetworks = preflightResults.filter(r => !r.ready);
482
+
483
+ if (readyNetworks.length === 0) {
484
+ console.log(chalk.red('\n✗ No networks ready for deployment'));
485
+ process.exit(1);
486
+ }
487
+
488
+ // Show summary and single confirmation
489
+ if (!opts.json && !opts.yes) {
490
+ console.log(chalk.cyan('\n📝 Deployment Summary:\n'));
491
+ console.log(chalk.gray(` Name: ${options.name}`));
492
+ console.log(chalk.gray(` Owners: ${owners.join(', ')}`));
493
+ console.log(chalk.gray(` Required: ${options.required}`));
494
+ console.log(chalk.gray(` Networks ready: ${readyNetworks.map(r => r.network).join(', ')}`));
495
+ if (notReadyNetworks.length > 0) {
496
+ console.log(chalk.yellow(` Networks skipped: ${notReadyNetworks.map(r => r.network).join(', ')}`));
497
+ }
498
+ if (readyNetworks[0]?.predictedAddress) {
499
+ console.log(chalk.gray(` Predicted address: ${readyNetworks[0].predictedAddress}`));
500
+ }
501
+ console.log('');
502
+
503
+ const inquirer = require('inquirer');
504
+ const { confirm } = await inquirer.prompt([{
505
+ type: 'confirm',
506
+ name: 'confirm',
507
+ message: `Deploy to ${readyNetworks.length} network(s)?`,
508
+ default: true
509
+ }]);
510
+ if (!confirm) {
511
+ console.log(chalk.yellow('Deployment cancelled'));
512
+ process.exit(0);
513
+ }
514
+ }
515
+
516
+ // Dry run - just show results
517
+ if (opts.dryRun) {
518
+ if (opts.json) {
519
+ console.log(JSON.stringify({
520
+ dryRun: true,
521
+ name: options.name,
522
+ owners,
523
+ required: options.required,
524
+ networks: preflightResults
525
+ }, null, 2));
526
+ } else {
527
+ console.log(chalk.cyan('\n✓ Dry run complete. No deployments made.'));
528
+ }
529
+ process.exit(0);
530
+ }
531
+
532
+ // Execute deployments
533
+ const successfulNetworks = [];
534
+ const failedNetworks = [];
535
+ let deployedAddress = null;
536
+
537
+ for (const result of readyNetworks) {
538
+ const { network: key, networkName, rpc, predictedAddress } = result;
539
+
540
+ if (!opts.json) {
541
+ console.log(chalk.cyan(`\n📡 Deploying to ${networkName}...`));
542
+ }
543
+
544
+ try {
545
+ const provider = new ethers.JsonRpcProvider(rpc);
546
+ const signer = wallet.connect(provider);
547
+
548
+ const factoryAddress = getFactoryAddress(key);
549
+ const factory = new ethers.Contract(factoryAddress, MSA_FACTORY_ABI, signer);
550
+ const fee = 1000000000000000n; // 0.001 ETH
551
+
552
+ const tx = await factory.newMSA(owners, options.required, pinHash, options.name, { value: fee });
553
+
554
+ if (!opts.json) {
555
+ console.log(chalk.gray(` Transaction: ${tx.hash}`));
556
+ }
557
+
558
+ const receipt = await tx.wait();
559
+
560
+ // Get deployed address from event or use predicted
561
+ let actualAddress = predictedAddress;
562
+ for (const log of receipt.logs) {
563
+ try {
564
+ const parsed = factory.interface.parseLog(log);
565
+ if (parsed && parsed.name === 'NewMSACreated') {
566
+ actualAddress = parsed.args.msa;
567
+ break;
568
+ }
569
+ } catch (e) {}
570
+ }
571
+
572
+ if (!deployedAddress) {
573
+ deployedAddress = actualAddress;
574
+ }
575
+
576
+ successfulNetworks.push(key);
577
+
578
+ if (!opts.json) {
579
+ console.log(chalk.green(` ✓ Deployed to ${actualAddress}`));
580
+ }
581
+ } catch (err) {
582
+ failedNetworks.push({ network: key, error: err.message });
583
+ if (!opts.json) {
584
+ console.log(chalk.red(` ✗ Failed: ${err.message}`));
585
+ }
586
+ }
587
+ }
588
+
589
+ // Save contract with all successful networks
590
+ if (successfulNetworks.length > 0 && deployedAddress) {
591
+ saveContract(options.name, deployedAddress, successfulNetworks);
592
+ }
593
+
594
+ // Final summary
595
+ if (!opts.json) {
596
+ if (successfulNetworks.length > 0) {
597
+ console.log(chalk.green(`\n✓ Deployed to ${successfulNetworks.length}/${readyNetworks.length} network(s)`));
598
+ console.log(chalk.gray(` Alias: ${options.name}`));
599
+ console.log(chalk.gray(` Address: ${deployedAddress}`));
600
+ console.log(chalk.gray(` Networks: ${successfulNetworks.join(', ')}`));
601
+ if (failedNetworks.length > 0) {
602
+ console.log(chalk.yellow(` Failed: ${failedNetworks.map(f => f.network).join(', ')}`));
603
+ }
604
+ } else {
605
+ console.log(chalk.red('\n✗ Deployment failed on all networks'));
606
+ }
607
+ } else {
608
+ console.log(JSON.stringify({
609
+ address: deployedAddress,
610
+ alias: options.name,
611
+ networks: successfulNetworks,
612
+ failed: failedNetworks
613
+ }, null, 2));
614
+ }
615
+ });
616
+
617
+ // Parse arguments
618
+ program.parse(process.argv);
619
+
620
+ // Show help if no command provided
621
+ if (!process.argv.slice(2).length) {
622
+ program.outputHelp();
623
+ }
624
+ })();