@aifabrix/builder 2.2.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 +102 -8
- package/lib/cli.js +27 -10
- package/lib/commands/secure.js +242 -0
- package/lib/config.js +106 -19
- package/lib/infra.js +14 -7
- package/lib/push.js +34 -7
- package/lib/secrets.js +75 -24
- package/lib/templates.js +1 -1
- package/lib/utils/build-copy.js +25 -5
- package/lib/utils/cli-utils.js +28 -3
- package/lib/utils/compose-generator.js +17 -4
- package/lib/utils/dev-config.js +8 -7
- package/lib/utils/docker-build.js +24 -0
- package/lib/utils/infra-containers.js +3 -2
- package/lib/utils/secrets-encryption.js +203 -0
- package/lib/utils/secrets-path.js +22 -3
- package/lib/utils/yaml-preserve.js +214 -0
- package/package.json +2 -2
- package/test-output.txt +0 -5431
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
|
@@ -28,6 +28,52 @@ const buildCopy = require('./utils/build-copy');
|
|
|
28
28
|
|
|
29
29
|
const execAsync = promisify(exec);
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Copies application template files to dev directory
|
|
33
|
+
* Used when apps directory doesn't exist to ensure build can proceed
|
|
34
|
+
* @async
|
|
35
|
+
* @param {string} templatePath - Path to template directory
|
|
36
|
+
* @param {string} devDir - Target dev directory
|
|
37
|
+
* @param {string} _language - Language (typescript/python) - currently unused but kept for future use
|
|
38
|
+
* @throws {Error} If copying fails
|
|
39
|
+
*/
|
|
40
|
+
async function copyTemplateFilesToDevDir(templatePath, devDir, _language) {
|
|
41
|
+
if (!fsSync.existsSync(templatePath)) {
|
|
42
|
+
throw new Error(`Template path not found: ${templatePath}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const entries = await fs.readdir(templatePath);
|
|
46
|
+
|
|
47
|
+
// Copy only application files, skip Dockerfile and docker-compose templates
|
|
48
|
+
const appFiles = entries.filter(entry => {
|
|
49
|
+
const lowerEntry = entry.toLowerCase();
|
|
50
|
+
// Include .gitignore, exclude .hbs files and docker-related files
|
|
51
|
+
if (entry === '.gitignore') {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (lowerEntry.endsWith('.hbs')) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (lowerEntry.startsWith('dockerfile') || lowerEntry.includes('docker-compose')) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (entry.startsWith('.') && entry !== '.gitignore') {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
for (const entry of appFiles) {
|
|
67
|
+
const sourcePath = path.join(templatePath, entry);
|
|
68
|
+
const targetPath = path.join(devDir, entry);
|
|
69
|
+
|
|
70
|
+
const entryStats = await fs.stat(sourcePath);
|
|
71
|
+
if (entryStats.isFile()) {
|
|
72
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
31
77
|
/**
|
|
32
78
|
* Loads variables.yaml configuration for an application
|
|
33
79
|
* @param {string} appName - Application name
|
|
@@ -91,7 +137,6 @@ function detectLanguage(appPath) {
|
|
|
91
137
|
const packageJsonPath = path.join(appPath, 'package.json');
|
|
92
138
|
const requirementsPath = path.join(appPath, 'requirements.txt');
|
|
93
139
|
const pyprojectPath = path.join(appPath, 'pyproject.toml');
|
|
94
|
-
const dockerfilePath = path.join(appPath, 'Dockerfile');
|
|
95
140
|
|
|
96
141
|
// Check for package.json (TypeScript/Node.js)
|
|
97
142
|
if (fsSync.existsSync(packageJsonPath)) {
|
|
@@ -103,11 +148,6 @@ function detectLanguage(appPath) {
|
|
|
103
148
|
return 'python';
|
|
104
149
|
}
|
|
105
150
|
|
|
106
|
-
// Check for custom Dockerfile
|
|
107
|
-
if (fsSync.existsSync(dockerfilePath)) {
|
|
108
|
-
throw new Error('Custom Dockerfile found. Use --force-template to regenerate from template.');
|
|
109
|
-
}
|
|
110
|
-
|
|
111
151
|
// Default to typescript if no indicators found
|
|
112
152
|
return 'typescript';
|
|
113
153
|
}
|
|
@@ -310,16 +350,49 @@ async function buildApp(appName, options = {}) {
|
|
|
310
350
|
|
|
311
351
|
// 2. Get developer ID and copy files to dev-specific directory
|
|
312
352
|
const developerId = await config.getDeveloperId();
|
|
313
|
-
const
|
|
353
|
+
const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
|
|
354
|
+
const directoryName = idNum === 0 ? 'applications' : `dev-${developerId}`;
|
|
314
355
|
logger.log(chalk.blue(`Copying files to developer-specific directory (${directoryName})...`));
|
|
315
356
|
const devDir = await buildCopy.copyBuilderToDevDirectory(appName, developerId);
|
|
316
357
|
logger.log(chalk.green(`✓ Files copied to: ${devDir}`));
|
|
317
358
|
|
|
359
|
+
// 2a. Check if application source files exist, if not copy from templates
|
|
360
|
+
const appsPath = path.join(process.cwd(), 'apps', appName);
|
|
361
|
+
if (fsSync.existsSync(appsPath)) {
|
|
362
|
+
// Copy app source files from apps directory
|
|
363
|
+
await buildCopy.copyAppSourceFiles(appsPath, devDir);
|
|
364
|
+
logger.log(chalk.green(`✓ Copied application source files from apps/${appName}`));
|
|
365
|
+
} else {
|
|
366
|
+
// No apps directory - check if we need to copy template files
|
|
367
|
+
const language = options.language || buildConfig.language || detectLanguage(devDir);
|
|
368
|
+
const packageJsonPath = path.join(devDir, 'package.json');
|
|
369
|
+
const requirementsPath = path.join(devDir, 'requirements.txt');
|
|
370
|
+
|
|
371
|
+
if (language === 'typescript' && !fsSync.existsSync(packageJsonPath)) {
|
|
372
|
+
// Copy TypeScript template files
|
|
373
|
+
const templatePath = path.join(__dirname, '..', 'templates', 'typescript');
|
|
374
|
+
await copyTemplateFilesToDevDir(templatePath, devDir, language);
|
|
375
|
+
logger.log(chalk.green(`✓ Generated application files from ${language} template`));
|
|
376
|
+
} else if (language === 'python' && !fsSync.existsSync(requirementsPath)) {
|
|
377
|
+
// Copy Python template files
|
|
378
|
+
const templatePath = path.join(__dirname, '..', 'templates', 'python');
|
|
379
|
+
await copyTemplateFilesToDevDir(templatePath, devDir, language);
|
|
380
|
+
logger.log(chalk.green(`✓ Generated application files from ${language} template`));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
318
384
|
// 3. Prepare build context (use dev-specific directory)
|
|
319
385
|
// If buildConfig.context is relative, resolve it relative to devDir
|
|
320
386
|
// If buildConfig.context is '../..' (apps flag), keep it as is (it's relative to devDir)
|
|
321
387
|
let contextPath;
|
|
322
|
-
|
|
388
|
+
|
|
389
|
+
// Check if context is using old format (../appName) - these are incompatible with dev directory structure
|
|
390
|
+
if (buildConfig.context && buildConfig.context.startsWith('../') && buildConfig.context !== '../..') {
|
|
391
|
+
// Old format detected - always use devDir instead
|
|
392
|
+
logger.log(chalk.yellow(`⚠️ Warning: Build context uses old format: ${buildConfig.context}`));
|
|
393
|
+
logger.log(chalk.yellow(` Using dev directory instead: ${devDir}`));
|
|
394
|
+
contextPath = devDir;
|
|
395
|
+
} else if (buildConfig.context && buildConfig.context !== '../..') {
|
|
323
396
|
// Resolve relative context path from dev directory
|
|
324
397
|
contextPath = path.resolve(devDir, buildConfig.context);
|
|
325
398
|
} else if (buildConfig.context === '../..') {
|
|
@@ -333,6 +406,22 @@ async function buildApp(appName, options = {}) {
|
|
|
333
406
|
contextPath = devDir;
|
|
334
407
|
}
|
|
335
408
|
|
|
409
|
+
// Ensure context path is absolute and normalized
|
|
410
|
+
contextPath = path.resolve(contextPath);
|
|
411
|
+
|
|
412
|
+
// Validate that context path exists (skip in test environments)
|
|
413
|
+
const isTestEnv = process.env.NODE_ENV === 'test' ||
|
|
414
|
+
process.env.JEST_WORKER_ID !== undefined ||
|
|
415
|
+
typeof jest !== 'undefined';
|
|
416
|
+
|
|
417
|
+
if (!isTestEnv && !fsSync.existsSync(contextPath)) {
|
|
418
|
+
throw new Error(
|
|
419
|
+
`Build context path does not exist: ${contextPath}\n` +
|
|
420
|
+
`Expected dev directory: ${devDir}\n` +
|
|
421
|
+
'Please ensure files were copied correctly or update the context in variables.yaml.'
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
336
425
|
// 4. Check if Dockerfile exists in dev directory
|
|
337
426
|
const appDockerfilePath = path.join(devDir, 'Dockerfile');
|
|
338
427
|
const hasExistingDockerfile = fsSync.existsSync(appDockerfilePath) && !options.forceTemplate;
|
|
@@ -362,6 +451,11 @@ async function buildApp(appName, options = {}) {
|
|
|
362
451
|
|
|
363
452
|
// 6. Build Docker image
|
|
364
453
|
const tag = options.tag || 'latest';
|
|
454
|
+
|
|
455
|
+
// Log paths for debugging
|
|
456
|
+
logger.log(chalk.blue(`Using Dockerfile: ${dockerfilePath}`));
|
|
457
|
+
logger.log(chalk.blue(`Using build context: ${contextPath}`));
|
|
458
|
+
|
|
365
459
|
await executeBuild(imageName, dockerfilePath, contextPath, tag, options);
|
|
366
460
|
|
|
367
461
|
// 7. Post-build tasks
|