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.
- package/dist/commands/auth.js +1 -1
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/deploy-mcp.d.ts +3 -0
- package/dist/commands/deploy-mcp.d.ts.map +1 -0
- package/dist/commands/deploy-mcp.js +858 -0
- package/dist/commands/deploy-mcp.js.map +1 -0
- package/dist/commands/deploy.js +3 -1
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +53 -116
- package/dist/commands/doctor.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/auth.ts +1 -1
- package/src/commands/deploy-mcp.ts +882 -0
- package/src/commands/deploy.ts +3 -1
- package/src/commands/doctor.ts +64 -122
- package/src/index.ts +2 -0
|
@@ -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
|