codecrypto-cli 1.0.17 → 1.0.18

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.
@@ -0,0 +1,858 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.deployMcpCommand = void 0;
40
+ const commander_1 = require("commander");
41
+ const chalk_1 = __importDefault(require("chalk"));
42
+ const ora_1 = __importDefault(require("ora"));
43
+ const child_process_1 = require("child_process");
44
+ const fs = __importStar(require("fs"));
45
+ const path = __importStar(require("path"));
46
+ const os = __importStar(require("os"));
47
+ exports.deployMcpCommand = new commander_1.Command('deploy-mcp')
48
+ .description('Build and deploy Docker image for Next.js or Express application using a container')
49
+ .argument('<project-path>', 'Path to the project directory')
50
+ .argument('[dest-folder]', 'Destination folder for Docker build context', './docker-build')
51
+ .option('--skip-build', 'Skip npm run build (assume already built)', false)
52
+ .option('--no-push', 'Build image but do not push to registry')
53
+ .option('--deploy', 'Deploy to remote server after building (automatic when pushing)')
54
+ .option('--no-deploy', 'Skip deployment to remote server')
55
+ .option('--service-name <name>', 'Service name for remote deployment')
56
+ .option('--env-file <path>', 'Path to .env file with environment variables')
57
+ .option('--port <port>', 'Application port', '3000')
58
+ .option('--domain-base <domain>', 'Domain base for deployment', 'codecrypto.academy')
59
+ .option('--image-version <version>', 'Image version/tag to deploy (overrides package.json version)')
60
+ .option('--skip-git-check', 'Skip Git repository status check', false)
61
+ .option('--container-image <image>', 'Docker image to use for the build container', 'node:20-alpine')
62
+ .action(async (projectPath, destFolder, options) => {
63
+ console.log(chalk_1.default.blue('\n🐳 CodeCrypto Docker Deployment (Container Mode)\n'));
64
+ try {
65
+ // Validar que el proyecto existe
66
+ const resolvedProjectPath = path.resolve(projectPath);
67
+ if (!fs.existsSync(resolvedProjectPath)) {
68
+ console.error(chalk_1.default.red(`❌ Error: El directorio del proyecto no existe: ${resolvedProjectPath}`));
69
+ process.exit(1);
70
+ }
71
+ // Verificar que es un proyecto válido
72
+ const packageJsonPath = path.join(resolvedProjectPath, 'package.json');
73
+ if (!fs.existsSync(packageJsonPath)) {
74
+ console.error(chalk_1.default.red(`❌ Error: No se encontró package.json en el proyecto`));
75
+ process.exit(1);
76
+ }
77
+ // Leer versión del package.json o usar la versión especificada
78
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
79
+ const version = options.imageVersion || packageJson.version || 'latest';
80
+ // Obtener el nombre del proyecto desde package.json.name
81
+ let projectName = packageJson.name || path.basename(resolvedProjectPath);
82
+ // Sanitizar el nombre para que sea válido para Docker
83
+ projectName = projectName.toLowerCase()
84
+ .replace(/^@[^/]+\//, '')
85
+ .replace(/[^a-z0-9._-]/g, '-')
86
+ .replace(/^-+|-+$/g, '')
87
+ .replace(/-+/g, '-')
88
+ .replace(/\.+/g, '.')
89
+ .substring(0, 128);
90
+ if (!projectName) {
91
+ projectName = path.basename(resolvedProjectPath).toLowerCase().replace(/[^a-z0-9._-]/g, '-');
92
+ }
93
+ const imageBase = `jviejo/${projectName}`;
94
+ const imageName = `${imageBase}:${version}`;
95
+ const imageLatest = `${imageBase}:latest`;
96
+ // Determinar si hacer push
97
+ const shouldPush = options.push !== false;
98
+ // Detectar automáticamente el tipo de proyecto
99
+ const allDependencies = {
100
+ ...(packageJson.dependencies || {}),
101
+ ...(packageJson.devDependencies || {}),
102
+ };
103
+ let projectType = null;
104
+ if (allDependencies.next || allDependencies['nextjs']) {
105
+ projectType = 'nextjs';
106
+ }
107
+ else if (allDependencies.express) {
108
+ projectType = 'express';
109
+ }
110
+ if (!projectType) {
111
+ console.error(chalk_1.default.red(`❌ Error: No se pudo detectar el tipo de proyecto`));
112
+ console.error(chalk_1.default.yellow(' El proyecto debe tener "next" o "express" en las dependencias del package.json'));
113
+ process.exit(1);
114
+ }
115
+ // Determinar ruta del archivo .env
116
+ const envFilePath = options.envFile
117
+ ? path.resolve(options.envFile)
118
+ : path.join(resolvedProjectPath, '.env');
119
+ console.log(chalk_1.default.gray('Deployment Configuration:'));
120
+ console.log(chalk_1.default.white(` Project: ${chalk_1.default.green(projectName)}`));
121
+ console.log(chalk_1.default.white(` Type: ${chalk_1.default.green(projectType)}`));
122
+ console.log(chalk_1.default.white(` Version: ${chalk_1.default.green(version)}`));
123
+ console.log(chalk_1.default.white(` Image: ${chalk_1.default.green(imageName)}`));
124
+ console.log(chalk_1.default.white(` Latest: ${chalk_1.default.green(imageLatest)}`));
125
+ console.log(chalk_1.default.white(` Push to Registry: ${shouldPush ? chalk_1.default.green('Yes') : chalk_1.default.yellow('No')}`));
126
+ console.log(chalk_1.default.white(` Container Mode: ${chalk_1.default.green('Enabled')}`));
127
+ console.log(chalk_1.default.white(` Container Image: ${chalk_1.default.green(options.containerImage)}`));
128
+ if (fs.existsSync(envFilePath)) {
129
+ console.log(chalk_1.default.white(` Env file: ${chalk_1.default.green(envFilePath)}`));
130
+ }
131
+ else {
132
+ console.log(chalk_1.default.white(` Env file: ${chalk_1.default.yellow('Not found')} (${envFilePath})`));
133
+ }
134
+ console.log();
135
+ // Preparar directorio destino
136
+ const resolvedDestFolder = path.resolve(destFolder);
137
+ if (fs.existsSync(resolvedDestFolder)) {
138
+ fs.rmSync(resolvedDestFolder, { recursive: true, force: true });
139
+ }
140
+ fs.mkdirSync(resolvedDestFolder, { recursive: true });
141
+ // Crear script de deploy que se ejecutará dentro del contenedor
142
+ const deployScriptPath = path.join(resolvedDestFolder, 'deploy-script.sh');
143
+ const deployScript = `#!/bin/sh
144
+ set -e
145
+
146
+ echo "🚀 Starting deployment inside container..."
147
+
148
+ # Variables del entorno
149
+ PROJECT_PATH="/workspace/project"
150
+ DEST_FOLDER="/workspace/dest"
151
+ PROJECT_TYPE="${projectType}"
152
+ SKIP_BUILD="${options.skipBuild ? 'true' : 'false'}"
153
+ SHOULD_PUSH="${shouldPush ? 'true' : 'false'}"
154
+ IMAGE_NAME="${imageName}"
155
+ IMAGE_LATEST="${imageLatest}"
156
+ PORT="${options.port}"
157
+
158
+ cd "$PROJECT_PATH"
159
+
160
+ # Validar Git si es necesario
161
+ if [ "${options.skipGitCheck ? 'false' : 'true'}" = "true" ]; then
162
+ if git rev-parse --git-dir > /dev/null 2>&1; then
163
+ if [ -n "$(git status --porcelain)" ]; then
164
+ echo "❌ Error: Hay archivos pendientes de commit"
165
+ exit 1
166
+ fi
167
+ fi
168
+ fi
169
+
170
+ # Build del proyecto si es necesario
171
+ if [ "$SKIP_BUILD" = "false" ]; then
172
+ if [ "$PROJECT_TYPE" = "nextjs" ]; then
173
+ echo "📦 Building Next.js application..."
174
+ npm run build
175
+
176
+ # Verificar que existe .next/standalone
177
+ if [ ! -d ".next/standalone" ]; then
178
+ echo "❌ Error: No se encontró .next/standalone"
179
+ echo "Asegúrate de tener 'output: standalone' en next.config.ts"
180
+ exit 1
181
+ fi
182
+ else
183
+ echo "📦 Installing production dependencies..."
184
+ npm install --production
185
+ fi
186
+ fi
187
+
188
+ # Preparar directorio destino
189
+ mkdir -p "$DEST_FOLDER"
190
+
191
+ if [ "$PROJECT_TYPE" = "nextjs" ]; then
192
+ # Copiar standalone
193
+ mkdir -p "$DEST_FOLDER/standalone"
194
+
195
+ # Buscar directorio .next en standalone
196
+ if [ -d ".next/standalone/.next" ]; then
197
+ cp -r .next/standalone/* "$DEST_FOLDER/standalone/"
198
+ else
199
+ # Si standalone contiene el .next directamente
200
+ NEXT_DIR=$(find .next/standalone -name ".next" -type d | head -1)
201
+ if [ -n "$NEXT_DIR" ]; then
202
+ PARENT_DIR=$(dirname "$NEXT_DIR")
203
+ cp -r "$PARENT_DIR"/* "$DEST_FOLDER/standalone/"
204
+ else
205
+ cp -r .next/standalone/* "$DEST_FOLDER/standalone/"
206
+ fi
207
+ fi
208
+
209
+ # Copiar .next/static
210
+ if [ -d ".next/static" ]; then
211
+ mkdir -p "$DEST_FOLDER/standalone/.next/static"
212
+ cp -r .next/static/* "$DEST_FOLDER/standalone/.next/static/"
213
+ fi
214
+
215
+ # Copiar public
216
+ if [ -d "public" ]; then
217
+ mkdir -p "$DEST_FOLDER/standalone/public"
218
+ cp -r public/* "$DEST_FOLDER/standalone/public/"
219
+ fi
220
+
221
+ # Verificar server.js
222
+ if [ ! -f "$DEST_FOLDER/standalone/server.js" ]; then
223
+ echo "❌ Error: server.js not found in standalone"
224
+ exit 1
225
+ fi
226
+
227
+ # Leer y procesar .env si existe
228
+ RUNTIME_ENV_VARS=""
229
+ NEXT_PUBLIC_ENV_VARS=""
230
+
231
+ if [ -f ".env" ]; then
232
+ while IFS= read -r line || [ -n "$line" ]; do
233
+ trimmed=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
234
+ if [ -z "$trimmed" ] || [ "$(echo "$trimmed" | cut -c1)" = "#" ]; then
235
+ continue
236
+ fi
237
+
238
+ if echo "$trimmed" | grep -q "="; then
239
+ key=$(echo "$trimmed" | cut -d'=' -f1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
240
+ value=$(echo "$trimmed" | cut -d'=' -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
241
+
242
+ if echo "$key" | grep -q "^NEXT_PUBLIC_"; then
243
+ NEXT_PUBLIC_ENV_VARS="$NEXT_PUBLIC_ENV_VARS
244
+ ARG $key=$value
245
+ ENV $key=$value"
246
+ else
247
+ RUNTIME_ENV_VARS="$RUNTIME_ENV_VARS
248
+ ENV $key=$value"
249
+ fi
250
+ fi
251
+ done < .env
252
+ fi
253
+
254
+ # Crear Dockerfile
255
+ {
256
+ echo "FROM node:20-alpine"
257
+ echo "WORKDIR /app"
258
+ echo ""
259
+ echo "ENV NODE_ENV=production"
260
+ echo "ENV PORT=3000"
261
+ echo "$NEXT_PUBLIC_ENV_VARS"
262
+ echo ""
263
+ echo "# Copiamos el contenido de standalone"
264
+ echo "COPY standalone/ ."
265
+ echo ""
266
+ echo "# Exponemos el puerto para Traefik"
267
+ echo "EXPOSE 3000"
268
+ echo ""
269
+ echo "# Arrancamos directamente con Node"
270
+ echo 'CMD ["node", "server.js"]'
271
+ } > "$DEST_FOLDER/Dockerfile"
272
+
273
+ else
274
+ # Express: copiar archivos del proyecto
275
+ echo "📋 Copying Express project files..."
276
+
277
+ # Función para copiar archivos excluyendo ciertos patrones
278
+ copy_excluding() {
279
+ local src="$1"
280
+ local dest="$2"
281
+
282
+ # Crear directorio destino
283
+ mkdir -p "$dest"
284
+
285
+ # Copiar archivos y directorios, excluyendo los especificados
286
+ find "$src" -mindepth 1 -maxdepth 1 ! -name 'node_modules' ! -name '.git' ! -name '.next' \\
287
+ ! -name 'dist' ! -name 'build' ! -name '.env*' ! -name 'npm-debug.log*' \\
288
+ ! -name 'yarn-debug.log*' ! -name 'yarn-error.log*' ! -name '.DS_Store' \\
289
+ ! -name 'coverage' ! -name '.nyc_output' -exec cp -r {} "$dest/" \\;
290
+ }
291
+
292
+ copy_excluding "$PROJECT_PATH" "$DEST_FOLDER"
293
+
294
+ # Leer package.json para determinar comando de inicio
295
+ MAIN_FILE=\$(node -e "console.log(require('./package.json').main || 'index.js')")
296
+ START_SCRIPT=\$(node -e "const pkg=require('./package.json'); console.log(pkg.scripts?.start || '')")
297
+
298
+ # Leer .env si existe
299
+ RUNTIME_ENV_VARS=""
300
+ if [ -f ".env" ]; then
301
+ while IFS= read -r line || [ -n "$line" ]; do
302
+ trimmed=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
303
+ if [ -z "$trimmed" ] || [ "$(echo "$trimmed" | cut -c1)" = "#" ]; then
304
+ continue
305
+ fi
306
+
307
+ if echo "$trimmed" | grep -q "="; then
308
+ key=$(echo "$trimmed" | cut -d'=' -f1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
309
+ value=$(echo "$trimmed" | cut -d'=' -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
310
+ RUNTIME_ENV_VARS="$RUNTIME_ENV_VARS
311
+ ENV $key=$value"
312
+ fi
313
+ done < .env
314
+ fi
315
+
316
+ # Determinar comando de inicio
317
+ if [ -n "$START_SCRIPT" ]; then
318
+ START_CMD="npm start"
319
+ else
320
+ START_CMD="node $MAIN_FILE"
321
+ fi
322
+
323
+ # Crear Dockerfile
324
+ {
325
+ echo "FROM node:20-alpine"
326
+ echo ""
327
+ echo "WORKDIR /app"
328
+ echo ""
329
+ echo "ENV NODE_ENV=production"
330
+ echo "ENV PORT=3000"
331
+ echo "$RUNTIME_ENV_VARS"
332
+ echo ""
333
+ echo "# Copiar package files primero para aprovechar cache de Docker"
334
+ echo "COPY package*.json ./"
335
+ echo ""
336
+ echo "# Instalar solo dependencias de producción"
337
+ echo "RUN npm ci --only=production && npm cache clean --force"
338
+ echo ""
339
+ echo "# Copiar código fuente"
340
+ echo "COPY . ."
341
+ echo ""
342
+ echo "# Exponemos el puerto"
343
+ echo "EXPOSE 3000"
344
+ echo ""
345
+ echo "# Comando de inicio"
346
+ echo "CMD $START_CMD"
347
+ } > "$DEST_FOLDER/Dockerfile"
348
+ fi
349
+
350
+ echo "✅ Deployment preparation completed inside container"
351
+ `;
352
+ fs.writeFileSync(deployScriptPath, deployScript);
353
+ fs.chmodSync(deployScriptPath, 0o755);
354
+ // Ejecutar el script dentro del contenedor
355
+ const containerSpinner = (0, ora_1.default)('Running deployment inside container...').start();
356
+ try {
357
+ // Verificar que Docker está disponible
358
+ (0, child_process_1.execSync)('docker --version', { stdio: 'pipe' });
359
+ }
360
+ catch {
361
+ containerSpinner.fail('Docker is not available');
362
+ console.error(chalk_1.default.red('❌ Error: Docker is required for container mode'));
363
+ process.exit(1);
364
+ }
365
+ // Montar volúmenes necesarios
366
+ // - El proyecto como /workspace/project
367
+ // - El directorio destino como /workspace/dest
368
+ // - Docker socket para que el contenedor pueda construir imágenes
369
+ // - El script de deploy
370
+ const dockerRunArgs = [
371
+ 'run', '--rm',
372
+ '-v', `${resolvedProjectPath}:/workspace/project:ro`,
373
+ '-v', `${resolvedDestFolder}:/workspace/dest`,
374
+ '-v', '/var/run/docker.sock:/var/run/docker.sock',
375
+ '-v', `${deployScriptPath}:/workspace/deploy-script.sh:ro`,
376
+ '--workdir', '/workspace',
377
+ options.containerImage,
378
+ 'sh', '/workspace/deploy-script.sh'
379
+ ];
380
+ // Si hay archivo .env, montarlo también
381
+ if (fs.existsSync(envFilePath)) {
382
+ dockerRunArgs.splice(6, 0, '-v', `${envFilePath}:/workspace/project/.env:ro`);
383
+ }
384
+ console.log(chalk_1.default.gray('\n🐳 Container execution:'));
385
+ console.log(chalk_1.default.gray('─'.repeat(60)));
386
+ console.log(chalk_1.default.cyan(`docker ${dockerRunArgs.join(' ')}`));
387
+ console.log(chalk_1.default.gray('─'.repeat(60)));
388
+ console.log();
389
+ const dockerProcess = (0, child_process_1.spawn)('docker', dockerRunArgs, {
390
+ stdio: 'inherit',
391
+ });
392
+ await new Promise((resolve, reject) => {
393
+ dockerProcess.on('close', (code) => {
394
+ if (code === 0) {
395
+ resolve();
396
+ }
397
+ else {
398
+ reject(new Error(`Container exited with code ${code}`));
399
+ }
400
+ });
401
+ dockerProcess.on('error', (error) => {
402
+ reject(error);
403
+ });
404
+ });
405
+ containerSpinner.succeed('Container execution completed');
406
+ // Verificar que se creó el Dockerfile
407
+ const dockerfilePath = path.join(resolvedDestFolder, 'Dockerfile');
408
+ if (!fs.existsSync(dockerfilePath)) {
409
+ console.error(chalk_1.default.red('❌ Error: Dockerfile was not created inside container'));
410
+ process.exit(1);
411
+ }
412
+ // Paso 4: Verificar/crear builder multi-plataforma
413
+ const builderSpinner = (0, ora_1.default)('Setting up multi-platform builder...').start();
414
+ try {
415
+ let buildersOutput = '';
416
+ try {
417
+ buildersOutput = (0, child_process_1.execSync)('docker buildx ls 2>/dev/null', {
418
+ encoding: 'utf-8',
419
+ stdio: 'pipe',
420
+ shell: '/bin/sh',
421
+ });
422
+ }
423
+ catch (error) {
424
+ console.log(chalk_1.default.yellow('⚠️ Warning: Some builders have connection issues, continuing...'));
425
+ }
426
+ const builderName = 'multiarch-builder';
427
+ if (!buildersOutput.includes(builderName)) {
428
+ builderSpinner.text = 'Creating multi-platform builder...';
429
+ (0, child_process_1.execSync)(`docker buildx create --name ${builderName} --use --bootstrap`, {
430
+ stdio: 'pipe',
431
+ });
432
+ }
433
+ else {
434
+ builderSpinner.text = 'Using existing multi-platform builder...';
435
+ try {
436
+ (0, child_process_1.execSync)(`docker buildx use ${builderName}`, { stdio: 'pipe' });
437
+ }
438
+ catch (error) {
439
+ builderSpinner.text = 'Recreating multi-platform builder...';
440
+ try {
441
+ (0, child_process_1.execSync)(`docker buildx rm ${builderName}`, { stdio: 'pipe' });
442
+ }
443
+ catch {
444
+ // Ignorar si no existe
445
+ }
446
+ (0, child_process_1.execSync)(`docker buildx create --name ${builderName} --use --bootstrap`, { stdio: 'pipe' });
447
+ }
448
+ }
449
+ builderSpinner.succeed('Builder ready');
450
+ }
451
+ catch (error) {
452
+ builderSpinner.fail('Failed to setup builder');
453
+ console.error(chalk_1.default.red('Error setting up Docker buildx builder'));
454
+ process.exit(1);
455
+ }
456
+ // Verificar autenticación de Docker si se va a hacer push
457
+ if (shouldPush) {
458
+ const authSpinner = (0, ora_1.default)('Verifying Docker authentication...').start();
459
+ try {
460
+ (0, child_process_1.execSync)('docker info', { stdio: 'pipe' });
461
+ try {
462
+ (0, child_process_1.execSync)('docker pull hello-world:latest', { stdio: 'pipe' });
463
+ authSpinner.succeed('Docker authentication verified');
464
+ }
465
+ catch {
466
+ authSpinner.warn('Could not verify Docker Hub authentication. Make sure you are logged in with: docker login');
467
+ }
468
+ }
469
+ catch (error) {
470
+ authSpinner.fail('Docker authentication failed');
471
+ console.error(chalk_1.default.yellow('\n⚠️ Warning: Docker authentication may be required for push'));
472
+ console.error(chalk_1.default.yellow(' Run: docker login'));
473
+ }
474
+ }
475
+ // Paso 5: Construir y hacer push
476
+ const buildDockerSpinner = (0, ora_1.default)('Building Docker image for multiple platforms...').start();
477
+ try {
478
+ const tags = shouldPush
479
+ ? [`--tag ${imageName}`, `--tag ${imageLatest}`, '--push']
480
+ : [`--tag ${imageName}`, `--tag ${imageLatest}`, '--load'];
481
+ const buildCommand = `docker buildx build \
482
+ --platform linux/amd64,linux/arm64 \
483
+ ${tags.join(' \\\n ')} \
484
+ --progress=plain \
485
+ .`;
486
+ console.log(chalk_1.default.gray('\n🔨 Build command:'));
487
+ console.log(chalk_1.default.gray('─'.repeat(60)));
488
+ console.log(chalk_1.default.cyan(buildCommand));
489
+ console.log(chalk_1.default.gray('─'.repeat(60)));
490
+ console.log(chalk_1.default.gray(`\n Working directory: ${chalk_1.default.cyan(resolvedDestFolder)}`));
491
+ console.log(chalk_1.default.gray(` Platforms: ${chalk_1.default.cyan('linux/amd64, linux/arm64')}`));
492
+ console.log(chalk_1.default.gray(` Push to registry: ${shouldPush ? chalk_1.default.green('Yes') : chalk_1.default.yellow('No')}\n`));
493
+ (0, child_process_1.execSync)(buildCommand, {
494
+ cwd: resolvedDestFolder,
495
+ stdio: 'inherit'
496
+ });
497
+ buildDockerSpinner.succeed('Docker image built successfully');
498
+ console.log(chalk_1.default.green('\n✅ Image build and push completed successfully!'));
499
+ console.log(chalk_1.default.gray('📦 Image details:'));
500
+ console.log(chalk_1.default.gray('─'.repeat(60)));
501
+ console.log(chalk_1.default.white(` Platforms: ${chalk_1.default.cyan('linux/amd64, linux/arm64')}`));
502
+ console.log(chalk_1.default.white(` Version: ${chalk_1.default.cyan(version)}`));
503
+ console.log(chalk_1.default.white(` Tags:`));
504
+ console.log(chalk_1.default.white(` - ${chalk_1.default.cyan(imageName)}`));
505
+ console.log(chalk_1.default.white(` - ${chalk_1.default.cyan(imageLatest)}`));
506
+ if (shouldPush) {
507
+ console.log(chalk_1.default.white(` Registry: ${chalk_1.default.cyan('Docker Hub')}`));
508
+ console.log(chalk_1.default.white(` URL: ${chalk_1.default.cyan(`https://hub.docker.com/r/${imageBase}`)}`));
509
+ console.log(chalk_1.default.gray('─'.repeat(60)));
510
+ console.log(chalk_1.default.green(`\n✅ Image pushed to Docker Hub successfully!`));
511
+ }
512
+ else {
513
+ console.log(chalk_1.default.gray('─'.repeat(60)));
514
+ console.log(chalk_1.default.yellow(`\n⚠️ Image built locally (not pushed to registry)`));
515
+ }
516
+ }
517
+ catch (error) {
518
+ buildDockerSpinner.fail('Docker build failed');
519
+ console.error(chalk_1.default.red('\n❌ Error building Docker image'));
520
+ if (shouldPush) {
521
+ console.error(chalk_1.default.yellow('Make sure you are authenticated: docker login'));
522
+ }
523
+ process.exit(1);
524
+ }
525
+ // Paso 6: Desplegar en servidor remoto si se solicita
526
+ const shouldDeploy = options.deploy !== false && (options.deploy === true || (shouldPush && options.deploy !== false));
527
+ if (shouldDeploy) {
528
+ console.log(chalk_1.default.blue('\n🚀 Starting remote deployment...\n'));
529
+ // Leer variables de entorno del .env
530
+ let runtimeEnvVars = [];
531
+ if (fs.existsSync(envFilePath)) {
532
+ const envContent = fs.readFileSync(envFilePath, 'utf-8');
533
+ const envLines = envContent.split('\n');
534
+ for (const line of envLines) {
535
+ const trimmed = line.trim();
536
+ if (!trimmed || trimmed.startsWith('#'))
537
+ continue;
538
+ const equalIndex = trimmed.indexOf('=');
539
+ if (equalIndex === -1)
540
+ continue;
541
+ const key = trimmed.substring(0, equalIndex).trim();
542
+ const value = trimmed.substring(equalIndex + 1).trim();
543
+ if (projectType === 'nextjs' && !key.startsWith('NEXT_PUBLIC_')) {
544
+ runtimeEnvVars.push(`${key}=${value}`);
545
+ }
546
+ else if (projectType === 'express') {
547
+ runtimeEnvVars.push(`${key}=${value}`);
548
+ }
549
+ }
550
+ }
551
+ const deployImageName = options.imageVersion
552
+ ? `${imageBase}:${options.imageVersion}`
553
+ : (shouldPush ? imageName : imageLatest);
554
+ await deployToRemoteServer({
555
+ imageName: deployImageName,
556
+ serviceName: options.serviceName || projectName,
557
+ domainBase: options.domainBase,
558
+ port: options.port,
559
+ envVars: runtimeEnvVars,
560
+ envFilePath: runtimeEnvVars.length > 0 ? envFilePath : undefined,
561
+ });
562
+ }
563
+ else if (shouldPush && options.deploy === false) {
564
+ console.log(chalk_1.default.yellow('\n💡 Deployment skipped (--no-deploy flag was used)'));
565
+ console.log(chalk_1.default.gray(' Use --deploy to deploy to remote server after push'));
566
+ }
567
+ }
568
+ catch (error) {
569
+ console.error(chalk_1.default.red(`\n❌ Error: ${error.message}`));
570
+ process.exit(1);
571
+ }
572
+ });
573
+ // Función para leer certificados Docker desde token.json
574
+ function loadDockerCertificatesFromToken() {
575
+ const tokenFilePath = path.join(os.homedir(), '.codecrypto', 'token.json');
576
+ if (!fs.existsSync(tokenFilePath)) {
577
+ throw new Error(`Token file not found at ${tokenFilePath}. Please run 'codecrypto auth' first.`);
578
+ }
579
+ let tokenData;
580
+ try {
581
+ tokenData = JSON.parse(fs.readFileSync(tokenFilePath, 'utf-8'));
582
+ }
583
+ catch (error) {
584
+ throw new Error(`Failed to read token file: ${error.message}`);
585
+ }
586
+ if (!tokenData.adminGlobals || !Array.isArray(tokenData.adminGlobals)) {
587
+ throw new Error('adminGlobals not found in token.json. Please run "codecrypto auth --force" to refresh your token.');
588
+ }
589
+ const certKeys = {
590
+ ca: 'docker-client/ca-pem',
591
+ cert: 'docker-client/cert.pem',
592
+ key: 'docker-client/key.pem'
593
+ };
594
+ const certificates = {};
595
+ const foundKeys = [];
596
+ for (const global of tokenData.adminGlobals) {
597
+ if (global && global.key) {
598
+ foundKeys.push(global.key);
599
+ if (global.key === certKeys.ca) {
600
+ certificates.ca = global.value;
601
+ }
602
+ else if (global.key === certKeys.cert) {
603
+ certificates.cert = global.value;
604
+ }
605
+ else if (global.key === certKeys.key) {
606
+ certificates.key = global.value;
607
+ }
608
+ }
609
+ }
610
+ if (!certificates.ca) {
611
+ const availableKeys = foundKeys.filter(k => k.includes('docker-client')).join(', ') || 'none';
612
+ throw new Error(`Certificate not found: ${certKeys.ca} in adminGlobals.\n` +
613
+ ` Available docker-client keys: ${availableKeys || 'none'}\n` +
614
+ ` Total adminGlobals entries: ${tokenData.adminGlobals.length}\n` +
615
+ ` Please run "codecrypto auth --force" to refresh your token.`);
616
+ }
617
+ if (!certificates.cert) {
618
+ const availableKeys = foundKeys.filter(k => k.includes('docker-client')).join(', ') || 'none';
619
+ throw new Error(`Certificate not found: ${certKeys.cert} in adminGlobals.\n` +
620
+ ` Available docker-client keys: ${availableKeys || 'none'}\n` +
621
+ ` Total adminGlobals entries: ${tokenData.adminGlobals.length}\n` +
622
+ ` Please run "codecrypto auth --force" to refresh your token.`);
623
+ }
624
+ if (!certificates.key) {
625
+ const availableKeys = foundKeys.filter(k => k.includes('docker-client')).join(', ') || 'none';
626
+ throw new Error(`Certificate not found: ${certKeys.key} in adminGlobals.\n` +
627
+ ` Available docker-client keys: ${availableKeys || 'none'}\n` +
628
+ ` Total adminGlobals entries: ${tokenData.adminGlobals.length}\n` +
629
+ ` Please run "codecrypto auth --force" to refresh your token.`);
630
+ }
631
+ const dockerCertPath = path.join(os.homedir(), '.codecrypto', 'docker-client');
632
+ if (!fs.existsSync(dockerCertPath)) {
633
+ fs.mkdirSync(dockerCertPath, { recursive: true });
634
+ }
635
+ const caPemPath = path.join(dockerCertPath, 'ca.pem');
636
+ const certPemPath = path.join(dockerCertPath, 'cert.pem');
637
+ const keyPemPath = path.join(dockerCertPath, 'key.pem');
638
+ fs.writeFileSync(caPemPath, certificates.ca, 'utf-8');
639
+ fs.writeFileSync(certPemPath, certificates.cert, 'utf-8');
640
+ fs.writeFileSync(keyPemPath, certificates.key, 'utf-8');
641
+ try {
642
+ fs.chmodSync(keyPemPath, 0o600);
643
+ }
644
+ catch (error) {
645
+ // Ignorar errores de chmod en Windows
646
+ }
647
+ return {
648
+ caPem: certificates.ca,
649
+ certPem: certificates.cert,
650
+ keyPem: certificates.key,
651
+ certPath: dockerCertPath
652
+ };
653
+ }
654
+ // Función para desplegar en servidor remoto
655
+ async function deployToRemoteServer(config) {
656
+ const deploySpinner = (0, ora_1.default)('Configuring remote Docker connection...').start();
657
+ try {
658
+ let dockerCertPath;
659
+ try {
660
+ const certs = loadDockerCertificatesFromToken();
661
+ dockerCertPath = certs.certPath;
662
+ deploySpinner.succeed('Docker certificates loaded from token.json');
663
+ }
664
+ catch (error) {
665
+ deploySpinner.fail('Failed to load Docker certificates');
666
+ console.error(chalk_1.default.red(`❌ Error: ${error.message}`));
667
+ console.error(chalk_1.default.yellow('\n💡 To fix this issue:'));
668
+ console.error(chalk_1.default.yellow(' 1. Run: codecrypto auth --force'));
669
+ console.error(chalk_1.default.yellow(' 2. This will refresh your token with the latest adminGlobals'));
670
+ console.error(chalk_1.default.yellow(' 3. Then try the deploy command again\n'));
671
+ process.exit(1);
672
+ }
673
+ const requiredCerts = ['ca.pem', 'cert.pem', 'key.pem'];
674
+ for (const cert of requiredCerts) {
675
+ const certPath = path.join(dockerCertPath, cert);
676
+ if (!fs.existsSync(certPath)) {
677
+ deploySpinner.fail(`Certificate ${cert} not found`);
678
+ console.error(chalk_1.default.red(`❌ Error: Certificate ${cert} not found at ${certPath}`));
679
+ process.exit(1);
680
+ }
681
+ }
682
+ const dockerHost = 'tcp://server.codecrypto.academy:2376';
683
+ const dockerTlsPrefix = `docker -H ${dockerHost} \\
684
+ --tlsverify \\
685
+ --tlscacert="${path.join(dockerCertPath, 'ca.pem')}" \\
686
+ --tlscert="${path.join(dockerCertPath, 'cert.pem')}" \\
687
+ --tlskey="${path.join(dockerCertPath, 'key.pem')}"`;
688
+ console.log(chalk_1.default.gray('\n🔌 Remote Docker configuration:'));
689
+ console.log(chalk_1.default.gray('─'.repeat(60)));
690
+ console.log(chalk_1.default.white(` Docker Host: ${chalk_1.default.cyan(dockerHost)}`));
691
+ console.log(chalk_1.default.white(` TLS Verify: ${chalk_1.default.cyan('enabled')}`));
692
+ console.log(chalk_1.default.white(` Certificates Source: ${chalk_1.default.cyan('token.json (adminGlobals)')}`));
693
+ console.log(chalk_1.default.white(` Certificates Path: ${chalk_1.default.cyan(dockerCertPath)}`));
694
+ console.log(chalk_1.default.gray(' Certificates loaded:'));
695
+ requiredCerts.forEach(cert => {
696
+ const certPath = path.join(dockerCertPath, cert);
697
+ const exists = fs.existsSync(certPath);
698
+ console.log(chalk_1.default.gray(` ${exists ? '✓' : '✗'} ${chalk_1.default.cyan(cert)}`));
699
+ });
700
+ console.log(chalk_1.default.gray('─'.repeat(60)));
701
+ console.log(chalk_1.default.yellow('\n💡 All docker commands will use TLS parameters directly\n'));
702
+ const fullDomain = `${config.serviceName}.${config.domainBase}`;
703
+ const serviceName = config.serviceName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
704
+ const stopSpinner = (0, ora_1.default)('Checking and removing existing container...').start();
705
+ try {
706
+ const checkCmd = `${dockerTlsPrefix} ps -a --filter name=^${fullDomain}$ --format "{{.Names}}"`;
707
+ const existingContainer = (0, child_process_1.execSync)(checkCmd, {
708
+ stdio: 'pipe',
709
+ encoding: 'utf-8',
710
+ shell: '/bin/sh',
711
+ }).trim();
712
+ if (existingContainer === fullDomain) {
713
+ try {
714
+ const stopCmd = `${dockerTlsPrefix} stop ${fullDomain}`;
715
+ (0, child_process_1.execSync)(stopCmd, { stdio: 'pipe', shell: '/bin/sh' });
716
+ console.log(chalk_1.default.gray(` ✓ Container ${fullDomain} stopped`));
717
+ }
718
+ catch (error) {
719
+ console.log(chalk_1.default.gray(` ℹ Container ${fullDomain} was not running`));
720
+ }
721
+ try {
722
+ const rmCmd = `${dockerTlsPrefix} rm -f ${fullDomain}`;
723
+ (0, child_process_1.execSync)(rmCmd, { stdio: 'pipe', shell: '/bin/sh' });
724
+ stopSpinner.succeed(`Existing container ${fullDomain} removed`);
725
+ }
726
+ catch (error) {
727
+ stopSpinner.fail(`Failed to remove container ${fullDomain}`);
728
+ console.error(chalk_1.default.red(` Error: ${error.message}`));
729
+ process.exit(1);
730
+ }
731
+ }
732
+ else {
733
+ stopSpinner.succeed('No existing container found');
734
+ }
735
+ }
736
+ catch (error) {
737
+ try {
738
+ const forceRmCmd = `${dockerTlsPrefix} rm -f ${fullDomain}`;
739
+ (0, child_process_1.execSync)(forceRmCmd, { stdio: 'pipe', shell: '/bin/sh' });
740
+ stopSpinner.succeed('Existing container removed (forced)');
741
+ }
742
+ catch {
743
+ stopSpinner.succeed('No existing container found');
744
+ }
745
+ }
746
+ const envOpts = [];
747
+ const envOptsForDisplay = [];
748
+ if (config.envVars.length > 0) {
749
+ config.envVars.forEach(env => {
750
+ const [key, ...valueParts] = env.split('=');
751
+ const value = valueParts.join('=');
752
+ envOpts.push('-e', `${key}=${value}`);
753
+ const escapedValue = value.replace(/"/g, '\\"');
754
+ envOptsForDisplay.push(`-e "${key}=${escapedValue}"`);
755
+ });
756
+ }
757
+ const dockerArgs = [
758
+ '-H', dockerHost,
759
+ '--tlsverify',
760
+ '--tlscacert', path.join(dockerCertPath, 'ca.pem'),
761
+ '--tlscert', path.join(dockerCertPath, 'cert.pem'),
762
+ '--tlskey', path.join(dockerCertPath, 'key.pem'),
763
+ 'run', '-d',
764
+ '--name', fullDomain,
765
+ '--network', 'academy-network',
766
+ '--restart', 'unless-stopped',
767
+ ...envOpts,
768
+ '-l', 'traefik.enable=true',
769
+ '-l', `traefik.http.routers.${serviceName}-router.rule=Host(\`${fullDomain}\`)`,
770
+ '-l', `traefik.http.routers.${serviceName}-router.entrypoints=websecure`,
771
+ '-l', `traefik.http.routers.${serviceName}-router.tls=true`,
772
+ '-l', `traefik.http.services.${serviceName}-service.loadbalancer.server.port=${config.port}`,
773
+ '-l', `traefik.http.services.${serviceName}-service.loadbalancer.passHostHeader=true`,
774
+ config.imageName
775
+ ];
776
+ const dockerRunCmdDisplay = `docker -H ${dockerHost} \\
777
+ --tlsverify \\
778
+ --tlscacert="${path.join(dockerCertPath, 'ca.pem')}" \\
779
+ --tlscert="${path.join(dockerCertPath, 'cert.pem')}" \\
780
+ --tlskey="${path.join(dockerCertPath, 'key.pem')}" \\
781
+ run -d \\
782
+ --name ${fullDomain} \\
783
+ --network academy-network \\
784
+ --restart unless-stopped \\
785
+ ${envOptsForDisplay.length > 0 ? envOptsForDisplay.join(' \\\n ') + ' \\\n ' : ''}-l traefik.enable=true \\
786
+ -l "traefik.http.routers.${serviceName}-router.rule=Host(\\\`${fullDomain}\\\`)" \\
787
+ -l traefik.http.routers.${serviceName}-router.entrypoints=websecure \\
788
+ -l traefik.http.routers.${serviceName}-router.tls=true \\
789
+ -l traefik.http.services.${serviceName}-service.loadbalancer.server.port=${config.port} \\
790
+ -l traefik.http.services.${serviceName}-service.loadbalancer.passHostHeader=true \\
791
+ ${config.imageName}`;
792
+ console.log(chalk_1.default.gray('\n🚀 Deployment command (executing on remote Docker):'));
793
+ console.log(chalk_1.default.gray('─'.repeat(60)));
794
+ console.log(chalk_1.default.cyan(dockerRunCmdDisplay));
795
+ console.log(chalk_1.default.gray('─'.repeat(60)));
796
+ console.log(chalk_1.default.gray(`\n Remote Docker: ${chalk_1.default.cyan(dockerHost)}`));
797
+ console.log(chalk_1.default.gray(` TLS Certificates: ${chalk_1.default.cyan(dockerCertPath)}`));
798
+ console.log(chalk_1.default.gray(` Service name: ${chalk_1.default.cyan(config.serviceName)}`));
799
+ console.log(chalk_1.default.gray(` Full domain: ${chalk_1.default.cyan(fullDomain)}`));
800
+ console.log(chalk_1.default.gray(` Image: ${chalk_1.default.cyan(config.imageName)}`));
801
+ console.log(chalk_1.default.gray(` Port: ${chalk_1.default.cyan(config.port)}`));
802
+ console.log(chalk_1.default.gray(` Environment variables: ${chalk_1.default.cyan(config.envVars.length)}`));
803
+ if (config.envVars.length > 0) {
804
+ config.envVars.forEach(env => {
805
+ const [key] = env.split('=');
806
+ console.log(chalk_1.default.gray(` - ${chalk_1.default.cyan(key)}`));
807
+ });
808
+ }
809
+ console.log(chalk_1.default.gray('─'.repeat(60)));
810
+ const runSpinner = (0, ora_1.default)('Deploying container to remote server...').start();
811
+ try {
812
+ const dockerProcess = (0, child_process_1.spawn)('docker', dockerArgs, {
813
+ stdio: 'inherit',
814
+ });
815
+ await new Promise((resolve, reject) => {
816
+ dockerProcess.on('close', (code) => {
817
+ if (code === 0) {
818
+ resolve();
819
+ }
820
+ else {
821
+ reject(new Error(`Docker command exited with code ${code}`));
822
+ }
823
+ });
824
+ dockerProcess.on('error', (error) => {
825
+ reject(error);
826
+ });
827
+ });
828
+ runSpinner.succeed('Container deployed successfully');
829
+ console.log(chalk_1.default.green('\n✅ Remote deployment completed successfully!'));
830
+ console.log(chalk_1.default.gray('📋 Deployment details:'));
831
+ console.log(chalk_1.default.gray('─'.repeat(60)));
832
+ console.log(chalk_1.default.white(` Service: ${chalk_1.default.cyan(config.serviceName)}`));
833
+ console.log(chalk_1.default.white(` Domain: ${chalk_1.default.cyan(fullDomain)}`));
834
+ console.log(chalk_1.default.white(` URL: ${chalk_1.default.cyan(`https://${fullDomain}`)}`));
835
+ console.log(chalk_1.default.white(` Image: ${chalk_1.default.cyan(config.imageName)}`));
836
+ console.log(chalk_1.default.white(` Port: ${chalk_1.default.cyan(config.port)}`));
837
+ console.log(chalk_1.default.white(` Network: ${chalk_1.default.cyan('academy-network')}`));
838
+ console.log(chalk_1.default.white(` Restart policy: ${chalk_1.default.cyan('unless-stopped')}`));
839
+ console.log(chalk_1.default.gray('─'.repeat(60)));
840
+ console.log(chalk_1.default.green(`\n🌐 Your application is now available at: ${chalk_1.default.cyan.underline(`https://${fullDomain}`)}`));
841
+ }
842
+ catch (error) {
843
+ runSpinner.fail('Failed to deploy container');
844
+ console.error(chalk_1.default.red('\n❌ Error deploying container to remote server'));
845
+ console.error(chalk_1.default.yellow(' Make sure:'));
846
+ console.error(chalk_1.default.yellow(' - The image is available on the remote server'));
847
+ console.error(chalk_1.default.yellow(' - You have push the image to Docker Hub (or use --no-push and ensure image exists)'));
848
+ console.error(chalk_1.default.yellow(' - The network "academy-network" exists on the remote server'));
849
+ process.exit(1);
850
+ }
851
+ }
852
+ catch (error) {
853
+ deploySpinner.fail('Failed to configure remote deployment');
854
+ console.error(chalk_1.default.red(`\n❌ Error: ${error.message}`));
855
+ process.exit(1);
856
+ }
857
+ }
858
+ //# sourceMappingURL=deploy-mcp.js.map