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.
- package/README.md +8 -5
- package/dist/commands/auth.js +1 -1
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/deploy-sc.d.ts.map +1 -1
- package/dist/commands/deploy-sc.js +2 -3
- package/dist/commands/deploy-sc.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +91 -18
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +252 -25
- package/dist/commands/doctor.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/auth.ts +1 -1
- package/src/commands/deploy-sc.ts +2 -3
- package/src/commands/deploy.ts +99 -20
- package/src/commands/doctor.ts +256 -25
package/src/commands/deploy.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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')}
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
930
|
+
envOptsForDisplay.push(`-e "${key}=${escapedValue}"`);
|
|
890
931
|
});
|
|
891
932
|
}
|
|
892
933
|
|
|
893
934
|
// Construir comando docker run con parámetros TLS
|
|
894
|
-
|
|
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
|
-
${
|
|
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(
|
|
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
|
|
929
|
-
|
|
994
|
+
// Ejecutar comando docker usando spawn para mejor manejo de argumentos
|
|
995
|
+
const dockerProcess = spawn('docker', dockerArgs, {
|
|
930
996
|
stdio: 'inherit',
|
|
931
|
-
|
|
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');
|
package/src/commands/doctor.ts
CHANGED
|
@@ -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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
343
|
-
const anvilRunningCheck = ora('Checking if Anvil is running (port
|
|
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:
|
|
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 ===
|
|
361
|
-
anvilRunningCheck.succeed(`Anvil running on port
|
|
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
|
|
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
|
|
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
|
|
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
|
|
384
|
-
details: 'Start Anvil with: anvil --chain-id
|
|
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
|
|
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
|
|
529
|
+
details: `Error: ${errorMsg}. Start Anvil with: anvil --chain-id 31337 --port 8545`
|
|
401
530
|
});
|
|
402
531
|
}
|
|
403
532
|
}
|
|
404
533
|
|
|
405
|
-
//
|
|
534
|
+
// 15. Check Anvil account 0
|
|
406
535
|
const anvilAccountCheck = ora('Checking Anvil account 0...').start();
|
|
407
536
|
try {
|
|
408
|
-
const rpcUrl = 'http://localhost:
|
|
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
|
// ============================================
|