codecrypto-cli 1.0.12 → 1.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.
@@ -1,7 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import ora from 'ora';
4
- import { execSync } from 'child_process';
4
+ import { execSync, spawn } from 'child_process';
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
7
  import * as os from 'os';
@@ -12,7 +12,8 @@ export const deployCommand = new Command('deploy')
12
12
  .argument('[dest-folder]', 'Destination folder for Docker build context', './docker-build')
13
13
  .option('--skip-build', 'Skip npm run build (assume already built)', false)
14
14
  .option('--no-push', 'Build image but do not push to registry')
15
- .option('--deploy', 'Deploy to remote server after building', false)
15
+ .option('--deploy', 'Deploy to remote server after building (automatic when pushing)')
16
+ .option('--no-deploy', 'Skip deployment to remote server')
16
17
  .option('--service-name <name>', 'Service name for remote deployment')
17
18
  .option('--env-file <path>', 'Path to .env file with environment variables')
18
19
  .option('--port <port>', 'Application port', '3000')
@@ -112,7 +113,27 @@ export const deployCommand = new Command('deploy')
112
113
  // Leer versión del package.json o usar la versión especificada
113
114
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
114
115
  const version = options.imageVersion || packageJson.version || 'latest';
115
- const projectName = path.basename(resolvedProjectPath);
116
+
117
+ // Obtener el nombre del proyecto desde package.json.name
118
+ // Si no existe, usar el nombre del directorio como fallback
119
+ let projectName = packageJson.name || path.basename(resolvedProjectPath);
120
+
121
+ // Sanitizar el nombre para que sea válido para Docker
122
+ // Docker permite: letras minúsculas, números, guiones, guiones bajos y puntos
123
+ // También eliminar el scope si existe (ej: @scope/package -> package)
124
+ projectName = projectName.toLowerCase()
125
+ .replace(/^@[^/]+\//, '') // Eliminar scope npm (@scope/package -> package)
126
+ .replace(/[^a-z0-9._-]/g, '-') // Reemplazar caracteres inválidos con guiones
127
+ .replace(/^-+|-+$/g, '') // Eliminar guiones al inicio y final
128
+ .replace(/-+/g, '-') // Reemplazar múltiples guiones con uno solo
129
+ .replace(/\.+/g, '.') // Reemplazar múltiples puntos con uno solo
130
+ .substring(0, 128); // Limitar longitud (límite de Docker)
131
+
132
+ // Si después de sanitizar está vacío, usar el nombre del directorio
133
+ if (!projectName) {
134
+ projectName = path.basename(resolvedProjectPath).toLowerCase().replace(/[^a-z0-9._-]/g, '-');
135
+ }
136
+
116
137
  const imageBase = `jviejo/${projectName}`;
117
138
  const imageName = `${imageBase}:${version}`;
118
139
  const imageLatest = `${imageBase}:latest`;
@@ -151,13 +172,25 @@ export const deployCommand = new Command('deploy')
151
172
  process.exit(1);
152
173
  }
153
174
 
175
+ // Determinar ruta del archivo .env (común para ambos tipos de proyecto)
176
+ // Si no se especifica --env-file, usar .env del proyecto
177
+ const envFilePath = options.envFile
178
+ ? path.resolve(options.envFile)
179
+ : path.join(resolvedProjectPath, '.env');
180
+
154
181
  console.log(chalk.gray('Deployment Configuration:'));
155
182
  console.log(chalk.white(` Project: ${chalk.green(projectName)}`));
156
183
  console.log(chalk.white(` Type: ${chalk.green(projectType)}`));
157
184
  console.log(chalk.white(` Version: ${chalk.green(version)}`));
158
185
  console.log(chalk.white(` Image: ${chalk.green(imageName)}`));
159
186
  console.log(chalk.white(` Latest: ${chalk.green(imageLatest)}`));
160
- console.log(chalk.white(` Push to Registry: ${shouldPush ? chalk.green('Yes') : chalk.yellow('No')}\n`));
187
+ console.log(chalk.white(` Push to Registry: ${shouldPush ? chalk.green('Yes') : chalk.yellow('No')}`));
188
+ if (fs.existsSync(envFilePath)) {
189
+ console.log(chalk.white(` Env file: ${chalk.green(envFilePath)}`));
190
+ } else {
191
+ console.log(chalk.white(` Env file: ${chalk.yellow('Not found')} (${envFilePath})`));
192
+ }
193
+ console.log();
161
194
 
162
195
  // Preparar directorio destino
163
196
  const resolvedDestFolder = path.resolve(destFolder);
@@ -169,11 +202,6 @@ export const deployCommand = new Command('deploy')
169
202
  let runtimeEnvVars: string[] = [];
170
203
  let nextPublicEnvVars: string[] = [];
171
204
  let dockerfileContent: string;
172
-
173
- // Determinar ruta del archivo .env (común para ambos tipos de proyecto)
174
- const envFilePath = options.envFile
175
- ? path.resolve(options.envFile)
176
- : path.join(resolvedProjectPath, '.env');
177
205
 
178
206
  if (projectType === 'nextjs') {
179
207
  // ========== LÓGICA PARA NEXT.JS ==========
@@ -630,7 +658,14 @@ CMD ${JSON.stringify(startCmd)}
630
658
  }
631
659
 
632
660
  // Paso 6: Desplegar en servidor remoto si se solicita
633
- if (options.deploy) {
661
+ // Lógica: Si se hace push, desplegar automáticamente a menos que se pase --no-deploy
662
+ // options.deploy: true si --deploy, false si --no-deploy, undefined si no se especifica
663
+ // Si shouldPush es true y no se pasa --no-deploy, desplegar automáticamente
664
+ const shouldDeploy = options.deploy !== false && (options.deploy === true || (shouldPush && options.deploy !== false));
665
+
666
+ if (shouldDeploy) {
667
+ console.log(chalk.blue('\n🚀 Starting remote deployment...\n'));
668
+
634
669
  // Para despliegue, usar la versión especificada o la del package.json
635
670
  const deployImageName = options.imageVersion
636
671
  ? `${imageBase}:${options.imageVersion}`
@@ -644,6 +679,10 @@ CMD ${JSON.stringify(startCmd)}
644
679
  envVars: runtimeEnvVars,
645
680
  envFilePath: runtimeEnvVars.length > 0 ? envFilePath : undefined,
646
681
  });
682
+ } else if (shouldPush && options.deploy === false) {
683
+ // Si se hizo push pero se pasó --no-deploy, mostrar mensaje
684
+ console.log(chalk.yellow('\n💡 Deployment skipped (--no-deploy flag was used)'));
685
+ console.log(chalk.gray(' Use --deploy to deploy to remote server after push'));
647
686
  }
648
687
 
649
688
  } catch (error: any) {
@@ -879,24 +918,51 @@ async function deployToRemoteServer(config: {
879
918
  // Para despliegue remoto, pasamos las variables directamente con -e
880
919
  // ya que --env-file requiere que el archivo esté en el servidor remoto
881
920
  const envOpts: string[] = [];
921
+ const envOptsForDisplay: string[] = [];
882
922
  if (config.envVars.length > 0) {
883
923
  config.envVars.forEach(env => {
884
- // Escapar comillas y caracteres especiales en el valor
885
924
  const [key, ...valueParts] = env.split('=');
886
925
  const value = valueParts.join('=');
887
- // Escapar comillas dobles en el valor
926
+ // Para spawn, pasamos como argumentos separados
927
+ envOpts.push('-e', `${key}=${value}`);
928
+ // Para display, mantenemos el formato con comillas escapadas
888
929
  const escapedValue = value.replace(/"/g, '\\"');
889
- envOpts.push(`-e "${key}=${escapedValue}"`);
930
+ envOptsForDisplay.push(`-e "${key}=${escapedValue}"`);
890
931
  });
891
932
  }
892
933
 
893
934
  // Construir comando docker run con parámetros TLS
894
- const dockerRunCmd = `${dockerTlsPrefix} run -d \\
935
+ // Usar spawn con argumentos separados para evitar problemas con shell escaping
936
+ const dockerArgs = [
937
+ '-H', dockerHost,
938
+ '--tlsverify',
939
+ '--tlscacert', path.join(dockerCertPath, 'ca.pem'),
940
+ '--tlscert', path.join(dockerCertPath, 'cert.pem'),
941
+ '--tlskey', path.join(dockerCertPath, 'key.pem'),
942
+ 'run', '-d',
943
+ '--name', fullDomain,
944
+ '--network', 'academy-network',
945
+ '--restart', 'unless-stopped',
946
+ ...envOpts,
947
+ '-l', 'traefik.enable=true',
948
+ '-l', `traefik.http.routers.${serviceName}-router.rule=Host(\`${fullDomain}\`)`,
949
+ '-l', `traefik.http.routers.${serviceName}-router.entrypoints=websecure`,
950
+ '-l', `traefik.http.routers.${serviceName}-router.tls=true`,
951
+ '-l', `traefik.http.services.${serviceName}-service.loadbalancer.server.port=${config.port}`,
952
+ config.imageName
953
+ ];
954
+
955
+ // Construir comando para mostrar (formato legible)
956
+ const dockerRunCmdDisplay = `docker -H ${dockerHost} \\
957
+ --tlsverify \\
958
+ --tlscacert="${path.join(dockerCertPath, 'ca.pem')}" \\
959
+ --tlscert="${path.join(dockerCertPath, 'cert.pem')}" \\
960
+ --tlskey="${path.join(dockerCertPath, 'key.pem')}" \\
961
+ run -d \\
895
962
  --name ${fullDomain} \\
896
963
  --network academy-network \\
897
964
  --restart unless-stopped \\
898
- ${envOpts.length > 0 ? envOpts.join(' \\\n ') + ' \\' : ''}
899
- -l traefik.enable=true \\
965
+ ${envOptsForDisplay.length > 0 ? envOptsForDisplay.join(' \\\n ') + ' \\\n ' : ''}-l traefik.enable=true \\
900
966
  -l "traefik.http.routers.${serviceName}-router.rule=Host(\\\`${fullDomain}\\\`)" \\
901
967
  -l traefik.http.routers.${serviceName}-router.entrypoints=websecure \\
902
968
  -l traefik.http.routers.${serviceName}-router.tls=true \\
@@ -905,7 +971,7 @@ async function deployToRemoteServer(config: {
905
971
 
906
972
  console.log(chalk.gray('\n🚀 Deployment command (executing on remote Docker):'));
907
973
  console.log(chalk.gray('─'.repeat(60)));
908
- console.log(chalk.cyan(dockerRunCmd));
974
+ console.log(chalk.cyan(dockerRunCmdDisplay));
909
975
  console.log(chalk.gray('─'.repeat(60)));
910
976
  console.log(chalk.gray(`\n Remote Docker: ${chalk.cyan(dockerHost)}`));
911
977
  console.log(chalk.gray(` TLS Certificates: ${chalk.cyan(dockerCertPath)}`));
@@ -925,10 +991,23 @@ async function deployToRemoteServer(config: {
925
991
  const runSpinner = ora('Deploying container to remote server...').start();
926
992
 
927
993
  try {
928
- // Ejecutar comando docker con parámetros TLS directamente
929
- execSync(dockerRunCmd, {
994
+ // Ejecutar comando docker usando spawn para mejor manejo de argumentos
995
+ const dockerProcess = spawn('docker', dockerArgs, {
930
996
  stdio: 'inherit',
931
- shell: '/bin/sh',
997
+ });
998
+
999
+ await new Promise<void>((resolve, reject) => {
1000
+ dockerProcess.on('close', (code: number) => {
1001
+ if (code === 0) {
1002
+ resolve();
1003
+ } else {
1004
+ reject(new Error(`Docker command exited with code ${code}`));
1005
+ }
1006
+ });
1007
+
1008
+ dockerProcess.on('error', (error: Error) => {
1009
+ reject(error);
1010
+ });
932
1011
  });
933
1012
 
934
1013
  runSpinner.succeed('Container deployed successfully');
@@ -154,12 +154,79 @@ export const doctorCommand = new Command('doctor')
154
154
  try {
155
155
  const tokenData = JSON.parse(fs.readFileSync(tokenFilePath, 'utf-8'));
156
156
  if (tokenData.token && tokenData.email) {
157
- tokenCheck.succeed(`Token found for: ${tokenData.email}`);
158
- checks.push({
159
- name: 'CodeCrypto Token',
160
- status: 'success',
161
- message: `Token found for: ${tokenData.email}`
162
- });
157
+ // Validar el token con el servidor
158
+ const serverUrl = tokenData.serverUrl || process.env.CODECRYPTO_API_URL || process.env.API_URL || 'https://proyectos.codecrypto.academy';
159
+ const token = String(tokenData.token).trim();
160
+
161
+ try {
162
+ const verifyUrl = `${serverUrl}/api/tokens/verify`;
163
+ const controller = new AbortController();
164
+ const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 seconds timeout
165
+
166
+ const response = await fetch(verifyUrl, {
167
+ method: 'GET',
168
+ headers: {
169
+ 'Authorization': `Bearer ${token}`,
170
+ },
171
+ signal: controller.signal,
172
+ });
173
+
174
+ clearTimeout(timeoutId);
175
+
176
+ if (response.ok) {
177
+ const responseData = await response.json();
178
+ tokenCheck.succeed(`Token valid for: ${tokenData.email}`);
179
+ checks.push({
180
+ name: 'CodeCrypto Token',
181
+ status: 'success',
182
+ message: `Token valid for: ${tokenData.email}`
183
+ });
184
+ } else {
185
+ const errorText = await response.text();
186
+ let errorMessage = 'Token validation failed';
187
+ try {
188
+ const errorJson = JSON.parse(errorText);
189
+ errorMessage = errorJson.error || errorJson.message || errorMessage;
190
+ } catch {
191
+ errorMessage = errorText || errorMessage;
192
+ }
193
+
194
+ tokenCheck.fail(`Token validation failed: ${errorMessage}`);
195
+ checks.push({
196
+ name: 'CodeCrypto Token',
197
+ status: 'error',
198
+ message: `Token validation failed: ${errorMessage}`,
199
+ details: 'Run "codecrypto auth --force" to refresh your token'
200
+ });
201
+ }
202
+ } catch (fetchError: any) {
203
+ const errorMsg = fetchError.message || String(fetchError);
204
+ if (fetchError.name === 'AbortError' || errorMsg.includes('timeout')) {
205
+ tokenCheck.fail('Token validation timeout');
206
+ checks.push({
207
+ name: 'CodeCrypto Token',
208
+ status: 'error',
209
+ message: 'Token validation timeout',
210
+ details: 'Cannot reach server to validate token. Check your internet connection.'
211
+ });
212
+ } else if (errorMsg.includes('ECONNREFUSED') || errorMsg.includes('ENOTFOUND')) {
213
+ tokenCheck.fail('Cannot reach server to validate token');
214
+ checks.push({
215
+ name: 'CodeCrypto Token',
216
+ status: 'error',
217
+ message: 'Cannot reach server to validate token',
218
+ details: `Cannot reach ${serverUrl}. Check your network connection.`
219
+ });
220
+ } else {
221
+ tokenCheck.fail(`Token validation error: ${errorMsg}`);
222
+ checks.push({
223
+ name: 'CodeCrypto Token',
224
+ status: 'error',
225
+ message: `Token validation error: ${errorMsg}`,
226
+ details: 'Run "codecrypto auth --force" to refresh your token'
227
+ });
228
+ }
229
+ }
163
230
  } else {
164
231
  tokenCheck.fail('Token file exists but is invalid');
165
232
  checks.push({
@@ -188,7 +255,69 @@ export const doctorCommand = new Command('doctor')
188
255
  });
189
256
  }
190
257
 
191
- // 8. Check Docker certificates in token.json
258
+ // 8. Check server access (proyectos.codecrypto.academy)
259
+ const serverCheck = ora('Checking access to proyectos.codecrypto.academy...').start();
260
+ try {
261
+ const serverUrl = 'https://proyectos.codecrypto.academy';
262
+ const controller = new AbortController();
263
+ const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 seconds timeout
264
+
265
+ const response = await fetch(serverUrl, {
266
+ method: 'GET',
267
+ signal: controller.signal,
268
+ headers: {
269
+ 'User-Agent': 'CodeCrypto-CLI/1.0'
270
+ }
271
+ });
272
+
273
+ clearTimeout(timeoutId);
274
+
275
+ if (response.ok || response.status < 500) {
276
+ serverCheck.succeed(`Server accessible: ${serverUrl}`);
277
+ checks.push({
278
+ name: 'Server Access',
279
+ status: 'success',
280
+ message: `Server accessible: ${serverUrl} (status: ${response.status})`
281
+ });
282
+ } else {
283
+ serverCheck.fail(`Server returned error: ${response.status}`);
284
+ checks.push({
285
+ name: 'Server Access',
286
+ status: 'error',
287
+ message: `Server returned error: ${response.status}`,
288
+ details: `Check if ${serverUrl} is accessible from your network`
289
+ });
290
+ }
291
+ } catch (error: any) {
292
+ const errorMsg = error.message || String(error);
293
+ if (error.name === 'AbortError' || errorMsg.includes('timeout')) {
294
+ serverCheck.fail('Server connection timeout');
295
+ checks.push({
296
+ name: 'Server Access',
297
+ status: 'error',
298
+ message: 'Server connection timeout',
299
+ details: `Cannot reach https://proyectos.codecrypto.academy. Check your internet connection.`
300
+ });
301
+ } else if (errorMsg.includes('ECONNREFUSED') || errorMsg.includes('ENOTFOUND') || errorMsg.includes('getaddrinfo')) {
302
+ serverCheck.fail('Cannot resolve server address');
303
+ checks.push({
304
+ name: 'Server Access',
305
+ status: 'error',
306
+ message: 'Cannot resolve server address',
307
+ details: `Cannot reach https://proyectos.codecrypto.academy. Check DNS resolution and network connectivity.`
308
+ });
309
+ } else {
310
+ serverCheck.fail('Cannot access server');
311
+ checks.push({
312
+ name: 'Server Access',
313
+ status: 'error',
314
+ message: 'Cannot access server',
315
+ details: `Error: ${errorMsg}. Check if https://proyectos.codecrypto.academy is accessible.`
316
+ });
317
+ }
318
+ }
319
+
320
+ // 9. Check Docker certificates in token.json
192
321
  const certCheck = ora('Checking Docker certificates in token.json...').start();
193
322
  try {
194
323
  if (fs.existsSync(tokenFilePath)) {
@@ -254,7 +383,7 @@ export const doctorCommand = new Command('doctor')
254
383
  });
255
384
  }
256
385
 
257
- // 9. Check Git (optional but recommended)
386
+ // 10. Check Git (optional but recommended)
258
387
  const gitCheck = ora('Checking Git...').start();
259
388
  try {
260
389
  const gitVersion = execSync('git --version', { encoding: 'utf-8', stdio: 'pipe' }).trim();
@@ -279,7 +408,7 @@ export const doctorCommand = new Command('doctor')
279
408
  // ============================================
280
409
  console.log(chalk.cyan('\n🔷 Foundry / Smart Contracts Deployment Checks\n'));
281
410
 
282
- // 10. Check Foundry (forge)
411
+ // 11. Check Foundry (forge)
283
412
  const forgeCheck = ora('Checking Foundry (forge)...').start();
284
413
  try {
285
414
  const forgeVersion = execSync('forge --version', { encoding: 'utf-8', stdio: 'pipe' }).trim();
@@ -299,7 +428,7 @@ export const doctorCommand = new Command('doctor')
299
428
  });
300
429
  }
301
430
 
302
- // 11. Check cast
431
+ // 12. Check cast
303
432
  const castCheck = ora('Checking cast...').start();
304
433
  try {
305
434
  const castVersion = execSync('cast --version', { encoding: 'utf-8', stdio: 'pipe' }).trim();
@@ -319,7 +448,7 @@ export const doctorCommand = new Command('doctor')
319
448
  });
320
449
  }
321
450
 
322
- // 12. Check anvil
451
+ // 13. Check anvil
323
452
  const anvilCheck = ora('Checking anvil...').start();
324
453
  try {
325
454
  const anvilVersion = execSync('anvil --version', { encoding: 'utf-8', stdio: 'pipe' }).trim();
@@ -339,11 +468,11 @@ export const doctorCommand = new Command('doctor')
339
468
  });
340
469
  }
341
470
 
342
- // 13. Check if Anvil is running on correct port and chain-id
343
- const anvilRunningCheck = ora('Checking if Anvil is running (port 55556, chain-id 3133731337)...').start();
471
+ // 14. Check if Anvil is running on correct port and chain-id
472
+ const anvilRunningCheck = ora('Checking if Anvil is running (port 8545, chain-id 31337)...').start();
344
473
  try {
345
474
  // Try to get chain id from the RPC
346
- const rpcUrl = 'http://localhost:55556';
475
+ const rpcUrl = 'http://localhost:8545';
347
476
  const chainIdResponse = execSync(
348
477
  `cast chain-id --rpc-url ${rpcUrl}`,
349
478
  { encoding: 'utf-8', stdio: 'pipe', timeout: 5000 }
@@ -357,20 +486,20 @@ export const doctorCommand = new Command('doctor')
357
486
  chainId = parseInt(chainIdResponse, 10);
358
487
  }
359
488
 
360
- if (chainId === 3133731337) {
361
- anvilRunningCheck.succeed(`Anvil running on port 55556 with chain-id ${chainId}`);
489
+ if (chainId === 31337) {
490
+ anvilRunningCheck.succeed(`Anvil running on port 8545 with chain-id ${chainId}`);
362
491
  checks.push({
363
492
  name: 'Anvil Running',
364
493
  status: 'success',
365
- message: `Anvil running on port 55556 with chain-id ${chainId}`
494
+ message: `Anvil running on port 8545 with chain-id ${chainId}`
366
495
  });
367
496
  } else {
368
- anvilRunningCheck.fail(`Anvil running but wrong chain-id: ${chainId} (expected 3133731337)`);
497
+ anvilRunningCheck.fail(`Anvil running but wrong chain-id: ${chainId} (expected 31337)`);
369
498
  checks.push({
370
499
  name: 'Anvil Running',
371
500
  status: 'error',
372
501
  message: `Anvil running but wrong chain-id: ${chainId}`,
373
- details: 'Start Anvil with: anvil --chain-id 3133731337 --port 55556'
502
+ details: 'Start Anvil with: anvil --chain-id 31337 --port 8545'
374
503
  });
375
504
  }
376
505
  } catch (error: any) {
@@ -380,8 +509,8 @@ export const doctorCommand = new Command('doctor')
380
509
  checks.push({
381
510
  name: 'Anvil Running',
382
511
  status: 'error',
383
- message: 'Anvil is not running on port 55556',
384
- details: 'Start Anvil with: anvil --chain-id 3133731337 --port 55556'
512
+ message: 'Anvil is not running on port 8545',
513
+ details: 'Start Anvil with: anvil --chain-id 31337 --port 8545'
385
514
  });
386
515
  } else if (errorMsg.includes('timeout') || errorMsg.includes('ETIMEDOUT')) {
387
516
  anvilRunningCheck.fail('Anvil connection timeout');
@@ -389,7 +518,7 @@ export const doctorCommand = new Command('doctor')
389
518
  name: 'Anvil Running',
390
519
  status: 'error',
391
520
  message: 'Anvil connection timeout',
392
- details: 'Check if Anvil is running: anvil --chain-id 3133731337 --port 55556'
521
+ details: 'Check if Anvil is running: anvil --chain-id 31337 --port 8545'
393
522
  });
394
523
  } else {
395
524
  anvilRunningCheck.fail('Cannot connect to Anvil');
@@ -397,15 +526,15 @@ export const doctorCommand = new Command('doctor')
397
526
  name: 'Anvil Running',
398
527
  status: 'error',
399
528
  message: 'Cannot connect to Anvil',
400
- details: `Error: ${errorMsg}. Start Anvil with: anvil --chain-id 3133731337 --port 55556`
529
+ details: `Error: ${errorMsg}. Start Anvil with: anvil --chain-id 31337 --port 8545`
401
530
  });
402
531
  }
403
532
  }
404
533
 
405
- // 14. Check Anvil account 0
534
+ // 15. Check Anvil account 0
406
535
  const anvilAccountCheck = ora('Checking Anvil account 0...').start();
407
536
  try {
408
- const rpcUrl = 'http://localhost:55556';
537
+ const rpcUrl = 'http://localhost:8545';
409
538
  const balance = execSync(
410
539
  `cast balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --rpc-url ${rpcUrl}`,
411
540
  { encoding: 'utf-8', stdio: 'pipe', timeout: 5000 }
@@ -427,6 +556,108 @@ export const doctorCommand = new Command('doctor')
427
556
  });
428
557
  }
429
558
 
559
+ // 16. Check Besu1 network access
560
+ const besu1Check = ora('Checking Besu1 network (https://besu1.proyectos.codecrypto.academy)...').start();
561
+ try {
562
+ const rpcUrl = 'https://besu1.proyectos.codecrypto.academy';
563
+ const chainIdResponse = execSync(
564
+ `cast chain-id --rpc-url ${rpcUrl}`,
565
+ { encoding: 'utf-8', stdio: 'pipe', timeout: 10000 }
566
+ ).trim();
567
+
568
+ let chainId: number;
569
+ if (chainIdResponse.startsWith('0x') || chainIdResponse.startsWith('0X')) {
570
+ chainId = parseInt(chainIdResponse, 16);
571
+ } else {
572
+ chainId = parseInt(chainIdResponse, 10);
573
+ }
574
+
575
+ besu1Check.succeed(`Besu1 network accessible (chain-id: ${chainId})`);
576
+ checks.push({
577
+ name: 'Besu1 Network',
578
+ status: 'success',
579
+ message: `Besu1 network accessible: ${rpcUrl} (chain-id: ${chainId})`
580
+ });
581
+ } catch (error: any) {
582
+ const errorMsg = error.message || String(error);
583
+ if (errorMsg.includes('ECONNREFUSED') || errorMsg.includes('Connection refused')) {
584
+ besu1Check.fail('Cannot connect to Besu1 network');
585
+ checks.push({
586
+ name: 'Besu1 Network',
587
+ status: 'error',
588
+ message: 'Cannot connect to Besu1 network',
589
+ details: 'Check if https://besu1.proyectos.codecrypto.academy is accessible from your network'
590
+ });
591
+ } else if (errorMsg.includes('timeout') || errorMsg.includes('ETIMEDOUT')) {
592
+ besu1Check.fail('Besu1 network connection timeout');
593
+ checks.push({
594
+ name: 'Besu1 Network',
595
+ status: 'error',
596
+ message: 'Besu1 network connection timeout',
597
+ details: 'Check your internet connection and network access to https://besu1.proyectos.codecrypto.academy'
598
+ });
599
+ } else {
600
+ besu1Check.fail('Cannot access Besu1 network');
601
+ checks.push({
602
+ name: 'Besu1 Network',
603
+ status: 'error',
604
+ message: 'Cannot access Besu1 network',
605
+ details: `Error: ${errorMsg}. Check if https://besu1.proyectos.codecrypto.academy is accessible.`
606
+ });
607
+ }
608
+ }
609
+
610
+ // 17. Check Besu2 network access
611
+ const besu2Check = ora('Checking Besu2 network (https://besu2.proyectos.codecrypto.academy)...').start();
612
+ try {
613
+ const rpcUrl = 'https://besu2.proyectos.codecrypto.academy';
614
+ const chainIdResponse = execSync(
615
+ `cast chain-id --rpc-url ${rpcUrl}`,
616
+ { encoding: 'utf-8', stdio: 'pipe', timeout: 10000 }
617
+ ).trim();
618
+
619
+ let chainId: number;
620
+ if (chainIdResponse.startsWith('0x') || chainIdResponse.startsWith('0X')) {
621
+ chainId = parseInt(chainIdResponse, 16);
622
+ } else {
623
+ chainId = parseInt(chainIdResponse, 10);
624
+ }
625
+
626
+ besu2Check.succeed(`Besu2 network accessible (chain-id: ${chainId})`);
627
+ checks.push({
628
+ name: 'Besu2 Network',
629
+ status: 'success',
630
+ message: `Besu2 network accessible: ${rpcUrl} (chain-id: ${chainId})`
631
+ });
632
+ } catch (error: any) {
633
+ const errorMsg = error.message || String(error);
634
+ if (errorMsg.includes('ECONNREFUSED') || errorMsg.includes('Connection refused')) {
635
+ besu2Check.fail('Cannot connect to Besu2 network');
636
+ checks.push({
637
+ name: 'Besu2 Network',
638
+ status: 'error',
639
+ message: 'Cannot connect to Besu2 network',
640
+ details: 'Check if https://besu2.proyectos.codecrypto.academy is accessible from your network'
641
+ });
642
+ } else if (errorMsg.includes('timeout') || errorMsg.includes('ETIMEDOUT')) {
643
+ besu2Check.fail('Besu2 network connection timeout');
644
+ checks.push({
645
+ name: 'Besu2 Network',
646
+ status: 'error',
647
+ message: 'Besu2 network connection timeout',
648
+ details: 'Check your internet connection and network access to https://besu2.proyectos.codecrypto.academy'
649
+ });
650
+ } else {
651
+ besu2Check.fail('Cannot access Besu2 network');
652
+ checks.push({
653
+ name: 'Besu2 Network',
654
+ status: 'error',
655
+ message: 'Cannot access Besu2 network',
656
+ details: `Error: ${errorMsg}. Check if https://besu2.proyectos.codecrypto.academy is accessible.`
657
+ });
658
+ }
659
+ }
660
+
430
661
  // ============================================
431
662
  // SUMMARY
432
663
  // ============================================