@aifabrix/builder 2.3.0 → 2.3.2
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/lib/app-run-helpers.js +381 -0
- package/lib/app-run.js +17 -392
- package/lib/build.js +2 -1
- package/lib/cli.js +13 -10
- package/lib/commands/secure.js +17 -35
- package/lib/config.js +48 -19
- package/lib/infra.js +14 -7
- package/lib/secrets.js +3 -1
- package/lib/utils/build-copy.js +7 -5
- package/lib/utils/compose-generator.js +3 -2
- package/lib/utils/dev-config.js +8 -7
- package/lib/utils/infra-containers.js +3 -2
- package/lib/utils/yaml-preserve.js +214 -0
- package/package.json +1 -1
package/lib/app-run.js
CHANGED
|
@@ -9,390 +9,13 @@
|
|
|
9
9
|
* @version 2.0.0
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
const fs = require('fs').promises;
|
|
13
|
-
const fsSync = require('fs');
|
|
14
|
-
const path = require('path');
|
|
15
12
|
const chalk = require('chalk');
|
|
16
|
-
const yaml = require('js-yaml');
|
|
17
|
-
const { exec } = require('child_process');
|
|
18
|
-
const { promisify } = require('util');
|
|
19
|
-
const validator = require('./validator');
|
|
20
|
-
const infra = require('./infra');
|
|
21
|
-
const secrets = require('./secrets');
|
|
22
13
|
const config = require('./config');
|
|
23
|
-
const buildCopy = require('./utils/build-copy');
|
|
24
14
|
const logger = require('./utils/logger');
|
|
25
|
-
const {
|
|
15
|
+
const { checkPortAvailable, waitForHealthCheck } = require('./utils/health-check');
|
|
26
16
|
const composeGenerator = require('./utils/compose-generator');
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Checks if Docker image exists for the application
|
|
32
|
-
* @param {string} imageName - Image name (can include repository prefix)
|
|
33
|
-
* @param {string} tag - Image tag (default: latest)
|
|
34
|
-
* @param {boolean} [debug=false] - Enable debug logging
|
|
35
|
-
* @returns {Promise<boolean>} True if image exists
|
|
36
|
-
*/
|
|
37
|
-
async function checkImageExists(imageName, tag = 'latest', debug = false) {
|
|
38
|
-
try {
|
|
39
|
-
const fullImageName = `${imageName}:${tag}`;
|
|
40
|
-
const cmd = `docker images --format "{{.Repository}}:{{.Tag}}" --filter "reference=${fullImageName}"`;
|
|
41
|
-
if (debug) {
|
|
42
|
-
logger.log(chalk.gray(`[DEBUG] Executing: ${cmd}`));
|
|
43
|
-
}
|
|
44
|
-
// Use Docker's native filtering for cross-platform compatibility (Windows-safe)
|
|
45
|
-
const { stdout } = await execAsync(cmd);
|
|
46
|
-
const lines = stdout.trim().split('\n').filter(line => line.trim() !== '');
|
|
47
|
-
const exists = lines.some(line => line.trim() === fullImageName);
|
|
48
|
-
if (debug) {
|
|
49
|
-
logger.log(chalk.gray(`[DEBUG] Image ${fullImageName} exists: ${exists}`));
|
|
50
|
-
}
|
|
51
|
-
return exists;
|
|
52
|
-
} catch (error) {
|
|
53
|
-
if (debug) {
|
|
54
|
-
logger.log(chalk.gray(`[DEBUG] Image check failed: ${error.message}`));
|
|
55
|
-
}
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Checks if container is already running
|
|
62
|
-
* @param {string} appName - Application name
|
|
63
|
-
* @param {number} developerId - Developer ID (0 = default infra, > 0 = developer-specific)
|
|
64
|
-
* @param {boolean} [debug=false] - Enable debug logging
|
|
65
|
-
* @returns {Promise<boolean>} True if container is running
|
|
66
|
-
*/
|
|
67
|
-
async function checkContainerRunning(appName, developerId, debug = false) {
|
|
68
|
-
try {
|
|
69
|
-
// Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
|
|
70
|
-
const containerName = developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${developerId}-${appName}`;
|
|
71
|
-
const cmd = `docker ps --filter "name=${containerName}" --format "{{.Names}}"`;
|
|
72
|
-
if (debug) {
|
|
73
|
-
logger.log(chalk.gray(`[DEBUG] Executing: ${cmd}`));
|
|
74
|
-
}
|
|
75
|
-
const { stdout } = await execAsync(cmd);
|
|
76
|
-
const isRunning = stdout.trim() === containerName;
|
|
77
|
-
if (debug) {
|
|
78
|
-
logger.log(chalk.gray(`[DEBUG] Container ${containerName} running: ${isRunning}`));
|
|
79
|
-
if (isRunning) {
|
|
80
|
-
// Get container status details
|
|
81
|
-
const statusCmd = `docker ps --filter "name=${containerName}" --format "{{.Status}}"`;
|
|
82
|
-
const { stdout: status } = await execAsync(statusCmd);
|
|
83
|
-
const portsCmd = `docker ps --filter "name=${containerName}" --format "{{.Ports}}"`;
|
|
84
|
-
const { stdout: ports } = await execAsync(portsCmd);
|
|
85
|
-
logger.log(chalk.gray(`[DEBUG] Container status: ${status.trim()}`));
|
|
86
|
-
logger.log(chalk.gray(`[DEBUG] Container ports: ${ports.trim()}`));
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return isRunning;
|
|
90
|
-
} catch (error) {
|
|
91
|
-
if (debug) {
|
|
92
|
-
logger.log(chalk.gray(`[DEBUG] Container check failed: ${error.message}`));
|
|
93
|
-
}
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Stops and removes existing container
|
|
100
|
-
* @param {string} appName - Application name
|
|
101
|
-
* @param {number} developerId - Developer ID (0 = default infra, > 0 = developer-specific)
|
|
102
|
-
* @param {boolean} [debug=false] - Enable debug logging
|
|
103
|
-
*/
|
|
104
|
-
async function stopAndRemoveContainer(appName, developerId, debug = false) {
|
|
105
|
-
try {
|
|
106
|
-
// Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
|
|
107
|
-
const containerName = developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${developerId}-${appName}`;
|
|
108
|
-
logger.log(chalk.yellow(`Stopping existing container ${containerName}...`));
|
|
109
|
-
const stopCmd = `docker stop ${containerName}`;
|
|
110
|
-
if (debug) {
|
|
111
|
-
logger.log(chalk.gray(`[DEBUG] Executing: ${stopCmd}`));
|
|
112
|
-
}
|
|
113
|
-
await execAsync(stopCmd);
|
|
114
|
-
const rmCmd = `docker rm ${containerName}`;
|
|
115
|
-
if (debug) {
|
|
116
|
-
logger.log(chalk.gray(`[DEBUG] Executing: ${rmCmd}`));
|
|
117
|
-
}
|
|
118
|
-
await execAsync(rmCmd);
|
|
119
|
-
logger.log(chalk.green(`✓ Container ${containerName} stopped and removed`));
|
|
120
|
-
} catch (error) {
|
|
121
|
-
if (debug) {
|
|
122
|
-
logger.log(chalk.gray(`[DEBUG] Stop/remove container error: ${error.message}`));
|
|
123
|
-
}
|
|
124
|
-
// Container might not exist, which is fine
|
|
125
|
-
// Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
|
|
126
|
-
const containerName = developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${developerId}-${appName}`;
|
|
127
|
-
logger.log(chalk.gray(`Container ${containerName} was not running`));
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Validates app name and loads configuration
|
|
133
|
-
* @async
|
|
134
|
-
* @param {string} appName - Application name
|
|
135
|
-
* @returns {Promise<Object>} Application configuration
|
|
136
|
-
* @throws {Error} If validation fails
|
|
137
|
-
*/
|
|
138
|
-
async function validateAppConfiguration(appName) {
|
|
139
|
-
// Validate app name
|
|
140
|
-
if (!appName || typeof appName !== 'string') {
|
|
141
|
-
throw new Error('Application name is required');
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Check if we're running from inside the builder directory
|
|
145
|
-
const currentDir = process.cwd();
|
|
146
|
-
const normalizedPath = currentDir.replace(/\\/g, '/');
|
|
147
|
-
const expectedBuilderPath = `builder/${appName}`;
|
|
148
|
-
|
|
149
|
-
// If inside builder/{appName}, suggest moving to project root
|
|
150
|
-
if (normalizedPath.endsWith(expectedBuilderPath)) {
|
|
151
|
-
const projectRoot = path.resolve(currentDir, '../..');
|
|
152
|
-
throw new Error(
|
|
153
|
-
'You\'re running from inside the builder directory.\n' +
|
|
154
|
-
`Current directory: ${currentDir}\n` +
|
|
155
|
-
'Please change to the project root and try again:\n' +
|
|
156
|
-
` cd ${projectRoot}\n` +
|
|
157
|
-
` aifabrix run ${appName}`
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Load and validate app configuration
|
|
162
|
-
const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
163
|
-
if (!fsSync.existsSync(configPath)) {
|
|
164
|
-
const expectedDir = path.join(currentDir, 'builder', appName);
|
|
165
|
-
throw new Error(
|
|
166
|
-
`Application configuration not found: ${configPath}\n` +
|
|
167
|
-
`Current directory: ${currentDir}\n` +
|
|
168
|
-
`Expected location: ${expectedDir}\n` +
|
|
169
|
-
'Make sure you\'re running from the project root (where \'builder\' directory exists)\n' +
|
|
170
|
-
`Run 'aifabrix create ${appName}' first if configuration doesn't exist`
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const configContent = fsSync.readFileSync(configPath, 'utf8');
|
|
175
|
-
const config = yaml.load(configContent);
|
|
176
|
-
|
|
177
|
-
// Validate configuration
|
|
178
|
-
const validation = await validator.validateApplication(appName);
|
|
179
|
-
if (!validation.valid) {
|
|
180
|
-
const allErrors = [];
|
|
181
|
-
|
|
182
|
-
if (validation.variables && validation.variables.errors && validation.variables.errors.length > 0) {
|
|
183
|
-
allErrors.push('variables.yaml:');
|
|
184
|
-
allErrors.push(...validation.variables.errors.map(err => ` ${err}`));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (validation.rbac && validation.rbac.errors && validation.rbac.errors.length > 0) {
|
|
188
|
-
allErrors.push('rbac.yaml:');
|
|
189
|
-
allErrors.push(...validation.rbac.errors.map(err => ` ${err}`));
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (validation.env && validation.env.errors && validation.env.errors.length > 0) {
|
|
193
|
-
allErrors.push('env.template:');
|
|
194
|
-
allErrors.push(...validation.env.errors.map(err => ` ${err}`));
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (allErrors.length === 0) {
|
|
198
|
-
throw new Error('Configuration validation failed');
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
throw new Error(`Configuration validation failed:\n${allErrors.join('\n')}`);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return config;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Checks prerequisites: Docker image and infrastructure
|
|
209
|
-
* @async
|
|
210
|
-
* @param {string} appName - Application name
|
|
211
|
-
* @param {Object} config - Application configuration
|
|
212
|
-
* @param {boolean} [debug=false] - Enable debug logging
|
|
213
|
-
* @throws {Error} If prerequisites are not met
|
|
214
|
-
*/
|
|
215
|
-
async function checkPrerequisites(appName, config, debug = false) {
|
|
216
|
-
// Extract image name from configuration (same logic as build process)
|
|
217
|
-
const imageName = composeGenerator.getImageName(config, appName);
|
|
218
|
-
const imageTag = config.image?.tag || 'latest';
|
|
219
|
-
const fullImageName = `${imageName}:${imageTag}`;
|
|
220
|
-
|
|
221
|
-
if (debug) {
|
|
222
|
-
logger.log(chalk.gray(`[DEBUG] Image name: ${imageName}, tag: ${imageTag}`));
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Check if Docker image exists
|
|
226
|
-
logger.log(chalk.blue(`Checking if image ${fullImageName} exists...`));
|
|
227
|
-
const imageExists = await checkImageExists(imageName, imageTag, debug);
|
|
228
|
-
if (!imageExists) {
|
|
229
|
-
throw new Error(`Docker image ${fullImageName} not found\nRun 'aifabrix build ${appName}' first`);
|
|
230
|
-
}
|
|
231
|
-
logger.log(chalk.green(`✓ Image ${fullImageName} found`));
|
|
232
|
-
|
|
233
|
-
// Check infrastructure health
|
|
234
|
-
logger.log(chalk.blue('Checking infrastructure health...'));
|
|
235
|
-
const infraHealth = await infra.checkInfraHealth();
|
|
236
|
-
if (debug) {
|
|
237
|
-
logger.log(chalk.gray(`[DEBUG] Infrastructure health: ${JSON.stringify(infraHealth, null, 2)}`));
|
|
238
|
-
}
|
|
239
|
-
const unhealthyServices = Object.entries(infraHealth)
|
|
240
|
-
.filter(([_, status]) => status !== 'healthy')
|
|
241
|
-
.map(([service, _]) => service);
|
|
242
|
-
|
|
243
|
-
if (unhealthyServices.length > 0) {
|
|
244
|
-
throw new Error(`Infrastructure services not healthy: ${unhealthyServices.join(', ')}\nRun 'aifabrix up' first`);
|
|
245
|
-
}
|
|
246
|
-
logger.log(chalk.green('✓ Infrastructure is running'));
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* Prepares environment: ensures .env file and generates Docker Compose
|
|
250
|
-
* @async
|
|
251
|
-
* @param {string} appName - Application name
|
|
252
|
-
* @param {Object} appConfig - Application configuration
|
|
253
|
-
* @param {Object} options - Run options
|
|
254
|
-
* @returns {Promise<string>} Path to generated compose file
|
|
255
|
-
*/
|
|
256
|
-
async function prepareEnvironment(appName, appConfig, options) {
|
|
257
|
-
// Get developer ID and dev-specific directory
|
|
258
|
-
const developerId = await config.getDeveloperId();
|
|
259
|
-
const devDir = buildCopy.getDevDirectory(appName, developerId);
|
|
260
|
-
|
|
261
|
-
// Ensure dev directory exists (should exist from build, but check anyway)
|
|
262
|
-
if (!fsSync.existsSync(devDir)) {
|
|
263
|
-
await buildCopy.copyBuilderToDevDirectory(appName, developerId);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Ensure .env file exists with 'docker' environment context (for running in Docker)
|
|
267
|
-
// Generate in builder directory first, then copy to dev directory
|
|
268
|
-
const builderEnvPath = path.join(process.cwd(), 'builder', appName, '.env');
|
|
269
|
-
if (!fsSync.existsSync(builderEnvPath)) {
|
|
270
|
-
logger.log(chalk.yellow('Generating .env file from template...'));
|
|
271
|
-
await secrets.generateEnvFile(appName, null, 'docker');
|
|
272
|
-
} else {
|
|
273
|
-
// Re-generate with 'docker' context to ensure correct hostnames for Docker
|
|
274
|
-
logger.log(chalk.blue('Updating .env file for Docker environment...'));
|
|
275
|
-
await secrets.generateEnvFile(appName, null, 'docker');
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Copy .env to dev directory
|
|
279
|
-
const devEnvPath = path.join(devDir, '.env');
|
|
280
|
-
if (fsSync.existsSync(builderEnvPath)) {
|
|
281
|
-
await fs.copyFile(builderEnvPath, devEnvPath);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Also ensure .env file in apps/ directory is updated (for Docker build context)
|
|
285
|
-
const variablesPath = path.join(devDir, 'variables.yaml');
|
|
286
|
-
if (fsSync.existsSync(variablesPath)) {
|
|
287
|
-
const variablesContent = fsSync.readFileSync(variablesPath, 'utf8');
|
|
288
|
-
const variables = yaml.load(variablesContent);
|
|
289
|
-
|
|
290
|
-
if (variables?.build?.envOutputPath && variables.build.envOutputPath !== null) {
|
|
291
|
-
// The generateEnvFile already copies to apps/, but ensure it's using docker context
|
|
292
|
-
logger.log(chalk.blue('Ensuring .env file in apps/ directory is updated for Docker...'));
|
|
293
|
-
await secrets.generateEnvFile(appName, null, 'docker');
|
|
294
|
-
// Copy .env to dev directory again after regeneration
|
|
295
|
-
if (fsSync.existsSync(builderEnvPath)) {
|
|
296
|
-
await fs.copyFile(builderEnvPath, devEnvPath);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Generate Docker Compose configuration
|
|
302
|
-
logger.log(chalk.blue('Generating Docker Compose configuration...'));
|
|
303
|
-
const composeOptions = { ...options };
|
|
304
|
-
if (!composeOptions.port) {
|
|
305
|
-
const basePort = appConfig.port || 3000;
|
|
306
|
-
composeOptions.port = developerId === 0 ? basePort : basePort + (developerId * 100);
|
|
307
|
-
}
|
|
308
|
-
const composeContent = await composeGenerator.generateDockerCompose(appName, appConfig, composeOptions);
|
|
309
|
-
|
|
310
|
-
// Write compose file to dev-specific directory (devDir already defined above)
|
|
311
|
-
// Ensure dev directory exists (should exist from build, but check anyway)
|
|
312
|
-
if (!fsSync.existsSync(devDir)) {
|
|
313
|
-
await buildCopy.copyBuilderToDevDirectory(appName, developerId);
|
|
314
|
-
}
|
|
315
|
-
const tempComposePath = path.join(devDir, 'docker-compose.yaml');
|
|
316
|
-
await fs.writeFile(tempComposePath, composeContent);
|
|
317
|
-
|
|
318
|
-
return tempComposePath;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Starts the container and waits for health check
|
|
323
|
-
* @async
|
|
324
|
-
* @param {string} appName - Application name
|
|
325
|
-
* @param {string} composePath - Path to Docker Compose file
|
|
326
|
-
* @param {number} port - Application port
|
|
327
|
-
* @param {Object} config - Application configuration
|
|
328
|
-
* @param {boolean} [debug=false] - Enable debug logging
|
|
329
|
-
* @throws {Error} If container fails to start or become healthy
|
|
330
|
-
*/
|
|
331
|
-
async function startContainer(appName, composePath, port, config = null, debug = false) {
|
|
332
|
-
logger.log(chalk.blue(`Starting ${appName}...`));
|
|
333
|
-
|
|
334
|
-
// Ensure ADMIN_SECRETS_PATH is set for db-init service
|
|
335
|
-
const adminSecretsPath = await infra.ensureAdminSecrets();
|
|
336
|
-
if (debug) {
|
|
337
|
-
logger.log(chalk.gray(`[DEBUG] Admin secrets path: ${adminSecretsPath}`));
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Load POSTGRES_PASSWORD from admin-secrets.env
|
|
341
|
-
const adminSecretsContent = fsSync.readFileSync(adminSecretsPath, 'utf8');
|
|
342
|
-
const postgresPasswordMatch = adminSecretsContent.match(/^POSTGRES_PASSWORD=(.+)$/m);
|
|
343
|
-
const postgresPassword = postgresPasswordMatch ? postgresPasswordMatch[1] : '';
|
|
344
|
-
|
|
345
|
-
// Set environment variables for docker-compose
|
|
346
|
-
const env = {
|
|
347
|
-
...process.env,
|
|
348
|
-
ADMIN_SECRETS_PATH: adminSecretsPath,
|
|
349
|
-
POSTGRES_PASSWORD: postgresPassword
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
if (debug) {
|
|
353
|
-
logger.log(chalk.gray(`[DEBUG] Environment variables: ADMIN_SECRETS_PATH=${adminSecretsPath}, POSTGRES_PASSWORD=${postgresPassword ? '***' : '(not set)'}`));
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const composeCmd = `docker-compose -f "${composePath}" up -d`;
|
|
357
|
-
if (debug) {
|
|
358
|
-
logger.log(chalk.gray(`[DEBUG] Executing: ${composeCmd}`));
|
|
359
|
-
logger.log(chalk.gray(`[DEBUG] Compose file: ${composePath}`));
|
|
360
|
-
}
|
|
361
|
-
await execAsync(composeCmd, { env });
|
|
362
|
-
// Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
|
|
363
|
-
const containerName = config.developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${config.developerId}-${appName}`;
|
|
364
|
-
logger.log(chalk.green(`✓ Container ${containerName} started`));
|
|
365
|
-
if (debug) {
|
|
366
|
-
// Get container status after start
|
|
367
|
-
const statusCmd = `docker ps --filter "name=${containerName}" --format "{{.Status}}"`;
|
|
368
|
-
const { stdout: status } = await execAsync(statusCmd);
|
|
369
|
-
const portsCmd = `docker ps --filter "name=${containerName}" --format "{{.Ports}}"`;
|
|
370
|
-
const { stdout: ports } = await execAsync(portsCmd);
|
|
371
|
-
logger.log(chalk.gray(`[DEBUG] Container status: ${status.trim()}`));
|
|
372
|
-
logger.log(chalk.gray(`[DEBUG] Container ports: ${ports.trim()}`));
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Wait for health check using host port (CLI --port or dev-specific port, NOT localPort)
|
|
376
|
-
const healthCheckPath = config?.healthCheck?.path || '/health';
|
|
377
|
-
logger.log(chalk.blue(`Waiting for application to be healthy at http://localhost:${port}${healthCheckPath}...`));
|
|
378
|
-
await waitForHealthCheck(appName, 90, port, config, debug);
|
|
379
|
-
}
|
|
380
|
-
/**
|
|
381
|
-
* Displays run status after successful start
|
|
382
|
-
* @param {string} appName - Application name
|
|
383
|
-
* @param {number} port - Application port
|
|
384
|
-
* @param {Object} config - Application configuration (with developerId property)
|
|
385
|
-
*/
|
|
386
|
-
async function displayRunStatus(appName, port, config) {
|
|
387
|
-
// Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
|
|
388
|
-
const containerName = config.developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${config.developerId}-${appName}`;
|
|
389
|
-
const healthCheckPath = config?.healthCheck?.path || '/health';
|
|
390
|
-
const healthCheckUrl = `http://localhost:${port}${healthCheckPath}`;
|
|
391
|
-
|
|
392
|
-
logger.log(chalk.green(`\n✓ App running at http://localhost:${port}`));
|
|
393
|
-
logger.log(chalk.blue(`Health check: ${healthCheckUrl}`));
|
|
394
|
-
logger.log(chalk.gray(`Container: ${containerName}`));
|
|
395
|
-
}
|
|
17
|
+
// Helper functions extracted to reduce file size and complexity
|
|
18
|
+
const helpers = require('./app-run-helpers');
|
|
396
19
|
|
|
397
20
|
/**
|
|
398
21
|
* Runs the application locally using Docker
|
|
@@ -421,7 +44,7 @@ async function runApp(appName, options = {}) {
|
|
|
421
44
|
|
|
422
45
|
try {
|
|
423
46
|
// Validate app name and load configuration
|
|
424
|
-
const appConfig = await validateAppConfiguration(appName);
|
|
47
|
+
const appConfig = await helpers.validateAppConfiguration(appName);
|
|
425
48
|
|
|
426
49
|
// Load developer ID once from config module - it's now cached and available as config.developerId
|
|
427
50
|
// Developer ID: 0 = default infra, > 0 = developer-specific
|
|
@@ -433,21 +56,23 @@ async function runApp(appName, options = {}) {
|
|
|
433
56
|
}
|
|
434
57
|
|
|
435
58
|
// Check prerequisites: image and infrastructure
|
|
436
|
-
await checkPrerequisites(appName, appConfig, debug);
|
|
59
|
+
await helpers.checkPrerequisites(appName, appConfig, debug);
|
|
437
60
|
|
|
438
61
|
// Check if container is already running
|
|
439
|
-
const containerRunning = await checkContainerRunning(appName, appConfig.developerId, debug);
|
|
62
|
+
const containerRunning = await helpers.checkContainerRunning(appName, appConfig.developerId, debug);
|
|
440
63
|
if (containerRunning) {
|
|
441
64
|
// Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
|
|
442
|
-
const
|
|
65
|
+
const idNum2 = typeof appConfig.developerId === 'string' ? parseInt(appConfig.developerId, 10) : appConfig.developerId;
|
|
66
|
+
const containerName = idNum2 === 0 ? `aifabrix-${appName}` : `aifabrix-dev${appConfig.developerId}-${appName}`;
|
|
443
67
|
logger.log(chalk.yellow(`Container ${containerName} is already running`));
|
|
444
|
-
await stopAndRemoveContainer(appName, appConfig.developerId, debug);
|
|
68
|
+
await helpers.stopAndRemoveContainer(appName, appConfig.developerId, debug);
|
|
445
69
|
}
|
|
446
70
|
|
|
447
71
|
// Calculate host port: use dev-specific port offset if not overridden
|
|
448
72
|
// IMPORTANT: Container port (for Dockerfile) stays unchanged from appConfig.port
|
|
449
73
|
const basePort = appConfig.port || 3000;
|
|
450
|
-
const
|
|
74
|
+
const idNum3 = typeof appConfig.developerId === 'string' ? parseInt(appConfig.developerId, 10) : appConfig.developerId;
|
|
75
|
+
const hostPort = options.port || (idNum3 === 0 ? basePort : basePort + (idNum3 * 100));
|
|
451
76
|
if (debug) {
|
|
452
77
|
logger.log(chalk.gray(`[DEBUG] Host port: ${hostPort} (${options.port ? 'CLI override' : 'dev-specific'}), Container port: ${appConfig.build?.containerPort || appConfig.port || 3000} (unchanged)`));
|
|
453
78
|
}
|
|
@@ -460,17 +85,17 @@ async function runApp(appName, options = {}) {
|
|
|
460
85
|
}
|
|
461
86
|
|
|
462
87
|
// Prepare environment: ensure .env file and generate Docker Compose
|
|
463
|
-
const tempComposePath = await prepareEnvironment(appName, appConfig, options);
|
|
88
|
+
const tempComposePath = await helpers.prepareEnvironment(appName, appConfig, options);
|
|
464
89
|
if (debug) {
|
|
465
90
|
logger.log(chalk.gray(`[DEBUG] Compose file generated: ${tempComposePath}`));
|
|
466
91
|
}
|
|
467
92
|
|
|
468
93
|
try {
|
|
469
94
|
// Start container and wait for health check
|
|
470
|
-
await startContainer(appName, tempComposePath, hostPort, appConfig, debug);
|
|
95
|
+
await helpers.startContainer(appName, tempComposePath, hostPort, appConfig, debug);
|
|
471
96
|
|
|
472
97
|
// Display success message
|
|
473
|
-
await displayRunStatus(appName, hostPort, appConfig);
|
|
98
|
+
await helpers.displayRunStatus(appName, hostPort, appConfig);
|
|
474
99
|
|
|
475
100
|
} catch (error) {
|
|
476
101
|
// Keep the compose file for debugging - don't delete on error
|
|
@@ -491,9 +116,9 @@ async function runApp(appName, options = {}) {
|
|
|
491
116
|
}
|
|
492
117
|
module.exports = {
|
|
493
118
|
runApp,
|
|
494
|
-
checkImageExists,
|
|
495
|
-
checkContainerRunning,
|
|
496
|
-
stopAndRemoveContainer,
|
|
119
|
+
checkImageExists: helpers.checkImageExists,
|
|
120
|
+
checkContainerRunning: helpers.checkContainerRunning,
|
|
121
|
+
stopAndRemoveContainer: helpers.stopAndRemoveContainer,
|
|
497
122
|
checkPortAvailable,
|
|
498
123
|
generateDockerCompose: composeGenerator.generateDockerCompose,
|
|
499
124
|
waitForHealthCheck
|
package/lib/build.js
CHANGED
|
@@ -350,7 +350,8 @@ async function buildApp(appName, options = {}) {
|
|
|
350
350
|
|
|
351
351
|
// 2. Get developer ID and copy files to dev-specific directory
|
|
352
352
|
const developerId = await config.getDeveloperId();
|
|
353
|
-
const
|
|
353
|
+
const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
|
|
354
|
+
const directoryName = idNum === 0 ? 'applications' : `dev-${developerId}`;
|
|
354
355
|
logger.log(chalk.blue(`Copying files to developer-specific directory (${directoryName})...`));
|
|
355
356
|
const devDir = await buildCopy.copyBuilderToDevDirectory(appName, developerId);
|
|
356
357
|
logger.log(chalk.green(`✓ Files copied to: ${devDir}`));
|
package/lib/cli.js
CHANGED
|
@@ -336,18 +336,19 @@ function setupCommands(program) {
|
|
|
336
336
|
// Commander.js converts --set-id to setId in options object
|
|
337
337
|
const setIdValue = options.setId || options['set-id'];
|
|
338
338
|
if (setIdValue) {
|
|
339
|
-
const
|
|
340
|
-
if (
|
|
341
|
-
throw new Error('Developer ID must be a non-negative
|
|
339
|
+
const digitsOnly = /^[0-9]+$/.test(setIdValue);
|
|
340
|
+
if (!digitsOnly) {
|
|
341
|
+
throw new Error('Developer ID must be a non-negative digit string (0 = default infra, > 0 = developer-specific)');
|
|
342
342
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
343
|
+
// Convert to number for setDeveloperId and getDevPorts (they expect numbers)
|
|
344
|
+
const devIdNum = parseInt(setIdValue, 10);
|
|
345
|
+
await config.setDeveloperId(devIdNum);
|
|
346
|
+
process.env.AIFABRIX_DEVELOPERID = setIdValue;
|
|
347
|
+
logger.log(chalk.green(`✓ Developer ID set to ${setIdValue}`));
|
|
346
348
|
// Use the ID we just set instead of reading from file to avoid race conditions
|
|
347
|
-
const
|
|
348
|
-
const ports = devConfig.getDevPorts(devId);
|
|
349
|
+
const ports = devConfig.getDevPorts(devIdNum);
|
|
349
350
|
logger.log('\n🔧 Developer Configuration\n');
|
|
350
|
-
logger.log(`Developer ID: ${
|
|
351
|
+
logger.log(`Developer ID: ${setIdValue}`);
|
|
351
352
|
logger.log('\nPorts:');
|
|
352
353
|
logger.log(` App: ${ports.app}`);
|
|
353
354
|
logger.log(` Postgres: ${ports.postgres}`);
|
|
@@ -359,7 +360,9 @@ function setupCommands(program) {
|
|
|
359
360
|
}
|
|
360
361
|
|
|
361
362
|
const devId = await config.getDeveloperId();
|
|
362
|
-
|
|
363
|
+
// Convert string developer ID to number for getDevPorts
|
|
364
|
+
const devIdNum = parseInt(devId, 10);
|
|
365
|
+
const ports = devConfig.getDevPorts(devIdNum);
|
|
363
366
|
logger.log('\n🔧 Developer Configuration\n');
|
|
364
367
|
logger.log(`Developer ID: ${devId}`);
|
|
365
368
|
logger.log('\nPorts:');
|
package/lib/commands/secure.js
CHANGED
|
@@ -17,7 +17,8 @@ const inquirer = require('inquirer');
|
|
|
17
17
|
const chalk = require('chalk');
|
|
18
18
|
const logger = require('../utils/logger');
|
|
19
19
|
const { setSecretsEncryptionKey, getSecretsEncryptionKey } = require('../config');
|
|
20
|
-
const {
|
|
20
|
+
const { validateEncryptionKey } = require('../utils/secrets-encryption');
|
|
21
|
+
const { encryptYamlValues } = require('../utils/yaml-preserve');
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Finds all secrets.local.yaml files to encrypt
|
|
@@ -92,7 +93,8 @@ async function findSecretsFiles() {
|
|
|
92
93
|
|
|
93
94
|
/**
|
|
94
95
|
* Encrypts all non-encrypted values in a secrets file
|
|
95
|
-
* Preserves YAML structure and
|
|
96
|
+
* Preserves YAML structure, comments, and formatting
|
|
97
|
+
* Skips URLs (http:// and https://) as they are not secrets
|
|
96
98
|
*
|
|
97
99
|
* @async
|
|
98
100
|
* @function encryptSecretsFile
|
|
@@ -102,45 +104,25 @@ async function findSecretsFiles() {
|
|
|
102
104
|
*/
|
|
103
105
|
async function encryptSecretsFile(filePath, encryptionKey) {
|
|
104
106
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
105
|
-
const secrets = yaml.load(content);
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
let totalCount = 0;
|
|
113
|
-
const updatedSecrets = {};
|
|
114
|
-
|
|
115
|
-
for (const [key, value] of Object.entries(secrets)) {
|
|
116
|
-
totalCount++;
|
|
117
|
-
if (typeof value === 'string' && value.trim() !== '') {
|
|
118
|
-
if (isEncrypted(value)) {
|
|
119
|
-
// Already encrypted, keep as-is
|
|
120
|
-
updatedSecrets[key] = value;
|
|
121
|
-
} else {
|
|
122
|
-
// Encrypt the value
|
|
123
|
-
updatedSecrets[key] = encryptSecret(value, encryptionKey);
|
|
124
|
-
encryptedCount++;
|
|
125
|
-
}
|
|
126
|
-
} else {
|
|
127
|
-
// Non-string or empty value, keep as-is
|
|
128
|
-
updatedSecrets[key] = value;
|
|
108
|
+
// Validate that file contains valid YAML structure (optional check)
|
|
109
|
+
try {
|
|
110
|
+
const secrets = yaml.load(content);
|
|
111
|
+
if (!secrets || typeof secrets !== 'object') {
|
|
112
|
+
throw new Error(`Invalid secrets file format: ${filePath}`);
|
|
129
113
|
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// If YAML parsing fails, still try to encrypt (might have syntax issues but could be fixable)
|
|
116
|
+
// The line-by-line parser will handle it gracefully
|
|
130
117
|
}
|
|
131
118
|
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
const yamlContent = yaml.dump(updatedSecrets, {
|
|
135
|
-
indent: 2,
|
|
136
|
-
lineWidth: -1,
|
|
137
|
-
noRefs: true,
|
|
138
|
-
sortKeys: false
|
|
139
|
-
});
|
|
119
|
+
// Use line-by-line encryption to preserve comments and formatting
|
|
120
|
+
const result = encryptYamlValues(content, encryptionKey);
|
|
140
121
|
|
|
141
|
-
|
|
122
|
+
// Write back to file preserving all formatting
|
|
123
|
+
fs.writeFileSync(filePath, result.content, { mode: 0o600 });
|
|
142
124
|
|
|
143
|
-
return { encrypted:
|
|
125
|
+
return { encrypted: result.encrypted, total: result.total };
|
|
144
126
|
}
|
|
145
127
|
|
|
146
128
|
/**
|