@aifabrix/builder 2.3.6 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/lib/app-down.js +123 -0
- package/lib/app.js +4 -2
- package/lib/build.js +19 -13
- package/lib/cli.js +52 -9
- package/lib/config.js +83 -8
- package/lib/env-reader.js +3 -2
- package/lib/generator.js +0 -9
- package/lib/infra.js +30 -3
- package/lib/schema/application-schema.json +0 -15
- package/lib/schema/env-config.yaml +8 -8
- package/lib/secrets.js +167 -253
- package/lib/templates.js +10 -18
- package/lib/utils/api-error-handler.js +182 -147
- package/lib/utils/api.js +144 -354
- package/lib/utils/build-copy.js +6 -13
- package/lib/utils/compose-generator.js +2 -1
- package/lib/utils/device-code.js +349 -0
- package/lib/utils/env-config-loader.js +102 -0
- package/lib/utils/env-copy.js +131 -0
- package/lib/utils/env-endpoints.js +209 -0
- package/lib/utils/env-map.js +116 -0
- package/lib/utils/env-ports.js +60 -0
- package/lib/utils/environment-checker.js +39 -6
- package/lib/utils/image-name.js +49 -0
- package/lib/utils/paths.js +40 -18
- package/lib/utils/secrets-generator.js +3 -3
- package/lib/utils/secrets-helpers.js +359 -0
- package/lib/utils/secrets-path.js +24 -71
- package/lib/utils/secrets-url.js +38 -0
- package/lib/utils/secrets-utils.js +0 -41
- package/lib/utils/variable-transformer.js +0 -9
- package/package.json +1 -1
- package/templates/applications/README.md.hbs +9 -5
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/infra/compose.yaml +4 -0
- package/templates/infra/compose.yaml.hbs +9 -4
package/README.md
CHANGED
|
@@ -18,6 +18,9 @@ aifabrix up # Start Postgres + Redis
|
|
|
18
18
|
aifabrix create myapp # Create your app
|
|
19
19
|
aifabrix build myapp # Build Docker image
|
|
20
20
|
aifabrix run myapp # Run locally
|
|
21
|
+
# Stop the app (optionally remove its data volume)
|
|
22
|
+
aifabrix down myapp
|
|
23
|
+
# aifabrix down myapp --volumes
|
|
21
24
|
```
|
|
22
25
|
|
|
23
26
|
→ [Full Guide](docs/QUICK-START.md) | [CLI Commands](docs/CLI-REFERENCE.md)
|
package/lib/app-down.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Down Management
|
|
3
|
+
*
|
|
4
|
+
* Provides functionality to stop and remove a specific application's container,
|
|
5
|
+
* and optionally remove its associated Docker volume.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Application stop/remove utilities for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const chalk = require('chalk');
|
|
15
|
+
const { exec } = require('child_process');
|
|
16
|
+
const logger = require('./utils/logger');
|
|
17
|
+
const config = require('./config');
|
|
18
|
+
const helpers = require('./app-run-helpers');
|
|
19
|
+
const { validateAppName } = require('./app-push');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Executes a shell command asynchronously.
|
|
23
|
+
* Wraps child_process.exec with a Promise interface.
|
|
24
|
+
*
|
|
25
|
+
* @async
|
|
26
|
+
* @param {string} command - Command to execute
|
|
27
|
+
* @param {Object} [options] - Exec options
|
|
28
|
+
* @returns {Promise<{stdout: string, stderr: string}>} Exec result
|
|
29
|
+
*/
|
|
30
|
+
function execAsync(command, options = {}) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
exec(command, options, (error, stdout, stderr) => {
|
|
33
|
+
if (error) {
|
|
34
|
+
return reject(error);
|
|
35
|
+
}
|
|
36
|
+
resolve({ stdout, stderr });
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Computes the Docker volume name for an application, based on developer ID.
|
|
43
|
+
* Dev 0: aifabrix_{app}_data
|
|
44
|
+
* Dev > 0: aifabrix_dev{developerId}_{app}_data
|
|
45
|
+
*
|
|
46
|
+
* @param {string} appName - Application name
|
|
47
|
+
* @param {number|string} developerId - Developer ID
|
|
48
|
+
* @returns {string} Docker volume name
|
|
49
|
+
*/
|
|
50
|
+
function getAppVolumeName(appName, developerId) {
|
|
51
|
+
const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
|
|
52
|
+
if (idNum === 0) {
|
|
53
|
+
return `aifabrix_${appName}_data`;
|
|
54
|
+
}
|
|
55
|
+
return `aifabrix_dev${developerId}_${appName}_data`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Stops and removes a specific application's container.
|
|
60
|
+
* If --volumes is passed, attempts to remove the app's named Docker volume.
|
|
61
|
+
*
|
|
62
|
+
* This does NOT delete any files under builder/<app> or apps/<app>.
|
|
63
|
+
*
|
|
64
|
+
* @async
|
|
65
|
+
* @function downApp
|
|
66
|
+
* @param {string} appName - Application name
|
|
67
|
+
* @param {Object} options - Options
|
|
68
|
+
* @param {boolean} [options.volumes=false] - Remove Docker volume data
|
|
69
|
+
* @returns {Promise<void>} Resolves when operation completes
|
|
70
|
+
* @throws {Error} If validation fails or Docker operations error (other than missing volume)
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* await downApp('myapp', { volumes: true });
|
|
74
|
+
*/
|
|
75
|
+
async function downApp(appName, options = {}) {
|
|
76
|
+
try {
|
|
77
|
+
// Input validation
|
|
78
|
+
if (!appName || typeof appName !== 'string') {
|
|
79
|
+
throw new Error('Application name is required and must be a string');
|
|
80
|
+
}
|
|
81
|
+
validateAppName(appName);
|
|
82
|
+
|
|
83
|
+
const volumes = !!options.volumes;
|
|
84
|
+
|
|
85
|
+
// Load developer ID
|
|
86
|
+
const developerId = await config.getDeveloperId();
|
|
87
|
+
|
|
88
|
+
// Stop and remove container (idempotent handling in helper logs when not running)
|
|
89
|
+
await helpers.stopAndRemoveContainer(appName, developerId, false);
|
|
90
|
+
|
|
91
|
+
// Optionally remove named Docker volume (ignore if it doesn't exist)
|
|
92
|
+
if (volumes) {
|
|
93
|
+
const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
|
|
94
|
+
// Primary expected name
|
|
95
|
+
const primaryName = getAppVolumeName(appName, developerId);
|
|
96
|
+
// Legacy name used when devId \"0\" was treated as string in templates
|
|
97
|
+
const legacyDev0Name = idNum === 0 ? `aifabrix_dev0_${appName}_data` : null;
|
|
98
|
+
// Build list of candidates, unique and non-empty
|
|
99
|
+
const candidates = Array.from(
|
|
100
|
+
new Set([primaryName, legacyDev0Name].filter(Boolean))
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
for (const volumeName of candidates) {
|
|
104
|
+
logger.log(chalk.yellow(`Removing volume ${volumeName}...`));
|
|
105
|
+
try {
|
|
106
|
+
await execAsync(`docker volume rm -f ${volumeName}`);
|
|
107
|
+
logger.log(chalk.green(`✓ Volume ${volumeName} removed`));
|
|
108
|
+
} catch (volErr) {
|
|
109
|
+
// Swallow errors for missing volume; provide neutral message
|
|
110
|
+
logger.log(chalk.gray(`Volume ${volumeName} not found or already removed`));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// Provide meaningful error while avoiding sensitive info
|
|
116
|
+
throw new Error(`Failed to stop application: ${error.message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
downApp
|
|
122
|
+
};
|
|
123
|
+
|
package/lib/app.js
CHANGED
|
@@ -23,6 +23,7 @@ const { generateDockerfileForApp } = require('./app-dockerfile');
|
|
|
23
23
|
const { loadTemplateVariables, updateTemplateVariables, mergeTemplateVariables } = require('./utils/template-helpers');
|
|
24
24
|
const logger = require('./utils/logger');
|
|
25
25
|
const auditLogger = require('./audit-logger');
|
|
26
|
+
const { downApp } = require('./app-down');
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Displays success message after app creation
|
|
@@ -170,11 +171,11 @@ async function updateVariablesForAppFlag(appPath, appName) {
|
|
|
170
171
|
|
|
171
172
|
if (variables.build) {
|
|
172
173
|
variables.build.context = '../..';
|
|
173
|
-
variables.build.envOutputPath =
|
|
174
|
+
variables.build.envOutputPath = `../../apps/${appName}/.env`;
|
|
174
175
|
} else {
|
|
175
176
|
variables.build = {
|
|
176
177
|
context: '../..',
|
|
177
|
-
envOutputPath:
|
|
178
|
+
envOutputPath: `../../apps/${appName}/.env`
|
|
178
179
|
};
|
|
179
180
|
}
|
|
180
181
|
|
|
@@ -403,6 +404,7 @@ module.exports = {
|
|
|
403
404
|
createApp,
|
|
404
405
|
buildApp,
|
|
405
406
|
runApp,
|
|
407
|
+
downApp,
|
|
406
408
|
detectLanguage,
|
|
407
409
|
generateDockerfile,
|
|
408
410
|
generateDockerfileForApp,
|
package/lib/build.js
CHANGED
|
@@ -25,6 +25,7 @@ const validator = require('./validator');
|
|
|
25
25
|
const dockerfileUtils = require('./utils/dockerfile-utils');
|
|
26
26
|
const dockerBuild = require('./utils/docker-build');
|
|
27
27
|
const buildCopy = require('./utils/build-copy');
|
|
28
|
+
const { buildDevImageName } = require('./utils/image-name');
|
|
28
29
|
|
|
29
30
|
const execAsync = promisify(exec);
|
|
30
31
|
|
|
@@ -306,18 +307,10 @@ async function executeBuild(imageName, dockerfilePath, contextPath, tag, options
|
|
|
306
307
|
|
|
307
308
|
async function postBuildTasks(appName, buildConfig) {
|
|
308
309
|
try {
|
|
309
|
-
const envPath = await secrets.generateEnvFile(appName, buildConfig.secrets);
|
|
310
|
+
const envPath = await secrets.generateEnvFile(appName, buildConfig.secrets, 'docker');
|
|
310
311
|
logger.log(chalk.green(`✓ Generated .env file: ${envPath}`));
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const outputPath = path.resolve(builderPath, buildConfig.envOutputPath);
|
|
314
|
-
const outputDir = path.dirname(outputPath);
|
|
315
|
-
if (!fsSync.existsSync(outputDir)) {
|
|
316
|
-
await fs.mkdir(outputDir, { recursive: true });
|
|
317
|
-
}
|
|
318
|
-
await fs.copyFile(envPath, outputPath);
|
|
319
|
-
logger.log(chalk.green(`✓ Copied .env to: ${buildConfig.envOutputPath}`));
|
|
320
|
-
}
|
|
312
|
+
// Note: processEnvVariables is already called by generateEnvFile to generate local .env
|
|
313
|
+
// at the envOutputPath, so we don't need to manually copy the docker .env file
|
|
321
314
|
} catch (error) {
|
|
322
315
|
logger.log(chalk.yellow(`⚠️ Warning: Could not generate .env file: ${error.message}`));
|
|
323
316
|
}
|
|
@@ -355,6 +348,7 @@ async function buildApp(appName, options = {}) {
|
|
|
355
348
|
logger.log(chalk.blue(`Copying files to developer-specific directory (${directoryName})...`));
|
|
356
349
|
const devDir = await buildCopy.copyBuilderToDevDirectory(appName, developerId);
|
|
357
350
|
logger.log(chalk.green(`✓ Files copied to: ${devDir}`));
|
|
351
|
+
const effectiveImageName = buildDevImageName(imageName, developerId);
|
|
358
352
|
|
|
359
353
|
// 2a. Check if application source files exist, if not copy from templates
|
|
360
354
|
const appsPath = path.join(process.cwd(), 'apps', appName);
|
|
@@ -456,7 +450,18 @@ async function buildApp(appName, options = {}) {
|
|
|
456
450
|
logger.log(chalk.blue(`Using Dockerfile: ${dockerfilePath}`));
|
|
457
451
|
logger.log(chalk.blue(`Using build context: ${contextPath}`));
|
|
458
452
|
|
|
459
|
-
await executeBuild(
|
|
453
|
+
await executeBuild(effectiveImageName, dockerfilePath, contextPath, tag, options);
|
|
454
|
+
// Back-compat: also tag the built dev image as the base image name
|
|
455
|
+
try {
|
|
456
|
+
// Use runtime promisify so tests can capture this call reliably
|
|
457
|
+
const { promisify } = require('util');
|
|
458
|
+
const { exec } = require('child_process');
|
|
459
|
+
const run = promisify(exec);
|
|
460
|
+
await run(`docker tag ${effectiveImageName}:${tag} ${imageName}:${tag}`);
|
|
461
|
+
logger.log(chalk.green(`✓ Tagged image: ${imageName}:${tag}`));
|
|
462
|
+
} catch (err) {
|
|
463
|
+
logger.log(chalk.yellow(`⚠️ Warning: Could not create compatibility tag ${imageName}:${tag} - ${err.message}`));
|
|
464
|
+
}
|
|
460
465
|
|
|
461
466
|
// 7. Post-build tasks
|
|
462
467
|
await postBuildTasks(appName, buildConfig);
|
|
@@ -475,5 +480,6 @@ module.exports = {
|
|
|
475
480
|
executeDockerBuild: dockerBuild.executeDockerBuild,
|
|
476
481
|
detectLanguage,
|
|
477
482
|
generateDockerfile,
|
|
478
|
-
buildApp
|
|
483
|
+
buildApp,
|
|
484
|
+
postBuildTasks
|
|
479
485
|
};
|
package/lib/cli.js
CHANGED
|
@@ -70,15 +70,21 @@ function setupCommands(program) {
|
|
|
70
70
|
}
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
program.command('down')
|
|
74
|
-
.description('Stop and remove local infrastructure services')
|
|
73
|
+
program.command('down [app]')
|
|
74
|
+
.description('Stop and remove local infrastructure services or a specific application')
|
|
75
75
|
.option('-v, --volumes', 'Remove volumes (deletes all data)')
|
|
76
|
-
.action(async(options) => {
|
|
76
|
+
.action(async(appName, options) => {
|
|
77
77
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
// If app name is provided, stop/remove that application (optionally volumes)
|
|
79
|
+
if (typeof appName === 'string' && appName.trim().length > 0) {
|
|
80
|
+
await app.downApp(appName, { volumes: !!options.volumes });
|
|
80
81
|
} else {
|
|
81
|
-
|
|
82
|
+
// Otherwise, stop/remove infrastructure
|
|
83
|
+
if (options.volumes) {
|
|
84
|
+
await infra.stopInfraWithVolumes();
|
|
85
|
+
} else {
|
|
86
|
+
await infra.stopInfra();
|
|
87
|
+
}
|
|
82
88
|
}
|
|
83
89
|
} catch (error) {
|
|
84
90
|
handleCommandError(error, 'down');
|
|
@@ -340,11 +346,12 @@ function setupCommands(program) {
|
|
|
340
346
|
if (!digitsOnly) {
|
|
341
347
|
throw new Error('Developer ID must be a non-negative digit string (0 = default infra, > 0 = developer-specific)');
|
|
342
348
|
}
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
await config.setDeveloperId(devIdNum);
|
|
349
|
+
// Preserve the original string value to maintain leading zeros (e.g., "01")
|
|
350
|
+
await config.setDeveloperId(setIdValue);
|
|
346
351
|
process.env.AIFABRIX_DEVELOPERID = setIdValue;
|
|
347
352
|
logger.log(chalk.green(`✓ Developer ID set to ${setIdValue}`));
|
|
353
|
+
// Convert to number only for getDevPorts (which requires a number)
|
|
354
|
+
const devIdNum = parseInt(setIdValue, 10);
|
|
348
355
|
// Use the ID we just set instead of reading from file to avoid race conditions
|
|
349
356
|
const ports = devConfig.getDevPorts(devIdNum);
|
|
350
357
|
logger.log('\n🔧 Developer Configuration\n');
|
|
@@ -355,6 +362,24 @@ function setupCommands(program) {
|
|
|
355
362
|
logger.log(` Redis: ${ports.redis}`);
|
|
356
363
|
logger.log(` pgAdmin: ${ports.pgadmin}`);
|
|
357
364
|
logger.log(` Redis Commander: ${ports.redisCommander}`);
|
|
365
|
+
|
|
366
|
+
// Display configuration variables if set
|
|
367
|
+
const aifabrixHome = await config.getAifabrixHomeOverride();
|
|
368
|
+
const aifabrixSecrets = await config.getAifabrixSecretsPath();
|
|
369
|
+
const aifabrixEnvConfig = await config.getAifabrixEnvConfigPath();
|
|
370
|
+
|
|
371
|
+
if (aifabrixHome || aifabrixSecrets || aifabrixEnvConfig) {
|
|
372
|
+
logger.log('\nConfiguration:');
|
|
373
|
+
if (aifabrixHome) {
|
|
374
|
+
logger.log(` aifabrix-home: ${aifabrixHome}`);
|
|
375
|
+
}
|
|
376
|
+
if (aifabrixSecrets) {
|
|
377
|
+
logger.log(` aifabrix-secrets: ${aifabrixSecrets}`);
|
|
378
|
+
}
|
|
379
|
+
if (aifabrixEnvConfig) {
|
|
380
|
+
logger.log(` aifabrix-env-config: ${aifabrixEnvConfig}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
358
383
|
logger.log('');
|
|
359
384
|
return;
|
|
360
385
|
}
|
|
@@ -371,6 +396,24 @@ function setupCommands(program) {
|
|
|
371
396
|
logger.log(` Redis: ${ports.redis}`);
|
|
372
397
|
logger.log(` pgAdmin: ${ports.pgadmin}`);
|
|
373
398
|
logger.log(` Redis Commander: ${ports.redisCommander}`);
|
|
399
|
+
|
|
400
|
+
// Display configuration variables if set
|
|
401
|
+
const aifabrixHome = await config.getAifabrixHomeOverride();
|
|
402
|
+
const aifabrixSecrets = await config.getAifabrixSecretsPath();
|
|
403
|
+
const aifabrixEnvConfig = await config.getAifabrixEnvConfigPath();
|
|
404
|
+
|
|
405
|
+
if (aifabrixHome || aifabrixSecrets || aifabrixEnvConfig) {
|
|
406
|
+
logger.log('\nConfiguration:');
|
|
407
|
+
if (aifabrixHome) {
|
|
408
|
+
logger.log(` aifabrix-home: ${aifabrixHome}`);
|
|
409
|
+
}
|
|
410
|
+
if (aifabrixSecrets) {
|
|
411
|
+
logger.log(` aifabrix-secrets: ${aifabrixSecrets}`);
|
|
412
|
+
}
|
|
413
|
+
if (aifabrixEnvConfig) {
|
|
414
|
+
logger.log(` aifabrix-env-config: ${aifabrixEnvConfig}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
374
417
|
logger.log('');
|
|
375
418
|
} catch (error) {
|
|
376
419
|
handleCommandError(error, 'dev config');
|
package/lib/config.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AI Fabrix Builder Configuration Management
|
|
3
|
-
*
|
|
4
3
|
* Manages stored authentication configuration for CLI
|
|
5
4
|
* Stores controller URL and auth tokens securely
|
|
6
5
|
*
|
|
@@ -13,14 +12,15 @@ const fs = require('fs').promises;
|
|
|
13
12
|
const path = require('path');
|
|
14
13
|
const yaml = require('js-yaml');
|
|
15
14
|
const os = require('os');
|
|
16
|
-
|
|
15
|
+
// Avoid importing paths here to prevent circular dependency.
|
|
16
|
+
// Config location is always under OS home at ~/.aifabrix/config.yaml
|
|
17
17
|
|
|
18
18
|
// Default (for tests and constants): always reflects OS home
|
|
19
19
|
const CONFIG_DIR = path.join(os.homedir(), '.aifabrix');
|
|
20
20
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.yaml');
|
|
21
21
|
|
|
22
|
-
// Runtime (
|
|
23
|
-
const RUNTIME_CONFIG_DIR =
|
|
22
|
+
// Runtime config directory (always under OS home)
|
|
23
|
+
const RUNTIME_CONFIG_DIR = path.join(os.homedir(), '.aifabrix');
|
|
24
24
|
const RUNTIME_CONFIG_FILE = path.join(RUNTIME_CONFIG_DIR, 'config.yaml');
|
|
25
25
|
|
|
26
26
|
// Cache for developer ID - loaded when getConfig() is first called
|
|
@@ -368,7 +368,8 @@ async function setSecretsEncryptionKey(key) {
|
|
|
368
368
|
*/
|
|
369
369
|
async function getSecretsPath() {
|
|
370
370
|
const config = await getConfig();
|
|
371
|
-
|
|
371
|
+
// Backward compatibility: prefer new key, fallback to legacy
|
|
372
|
+
return config['aifabrix-secrets'] || config['secrets-path'] || null;
|
|
372
373
|
}
|
|
373
374
|
|
|
374
375
|
/**
|
|
@@ -382,7 +383,77 @@ async function setSecretsPath(secretsPath) {
|
|
|
382
383
|
}
|
|
383
384
|
|
|
384
385
|
const config = await getConfig();
|
|
385
|
-
|
|
386
|
+
// Store under new canonical key
|
|
387
|
+
config['aifabrix-secrets'] = secretsPath;
|
|
388
|
+
await saveConfig(config);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Get aifabrix-home override from configuration
|
|
393
|
+
* @returns {Promise<string|null>} Home override path or null if not set
|
|
394
|
+
*/
|
|
395
|
+
async function getAifabrixHomeOverride() {
|
|
396
|
+
const config = await getConfig();
|
|
397
|
+
return config['aifabrix-home'] || null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Set aifabrix-home override in configuration
|
|
402
|
+
* @param {string} homePath - Base directory path for AI Fabrix files
|
|
403
|
+
* @returns {Promise<void>}
|
|
404
|
+
*/
|
|
405
|
+
async function setAifabrixHomeOverride(homePath) {
|
|
406
|
+
if (!homePath || typeof homePath !== 'string') {
|
|
407
|
+
throw new Error('Home path is required and must be a string');
|
|
408
|
+
}
|
|
409
|
+
const config = await getConfig();
|
|
410
|
+
config['aifabrix-home'] = homePath;
|
|
411
|
+
await saveConfig(config);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Get aifabrix-secrets path from configuration (canonical)
|
|
416
|
+
* @returns {Promise<string|null>} Secrets path or null if not set
|
|
417
|
+
*/
|
|
418
|
+
async function getAifabrixSecretsPath() {
|
|
419
|
+
const config = await getConfig();
|
|
420
|
+
return config['aifabrix-secrets'] || null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Set aifabrix-secrets path in configuration (canonical)
|
|
425
|
+
* @param {string} secretsPath - Path to default secrets file
|
|
426
|
+
* @returns {Promise<void>}
|
|
427
|
+
*/
|
|
428
|
+
async function setAifabrixSecretsPath(secretsPath) {
|
|
429
|
+
if (!secretsPath || typeof secretsPath !== 'string') {
|
|
430
|
+
throw new Error('Secrets path is required and must be a string');
|
|
431
|
+
}
|
|
432
|
+
const config = await getConfig();
|
|
433
|
+
config['aifabrix-secrets'] = secretsPath;
|
|
434
|
+
await saveConfig(config);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Get aifabrix-env-config path from configuration
|
|
439
|
+
* @returns {Promise<string|null>} Env config path or null if not set
|
|
440
|
+
*/
|
|
441
|
+
async function getAifabrixEnvConfigPath() {
|
|
442
|
+
const config = await getConfig();
|
|
443
|
+
return config['aifabrix-env-config'] || null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Set aifabrix-env-config path in configuration
|
|
448
|
+
* @param {string} envConfigPath - Path to user env-config file
|
|
449
|
+
* @returns {Promise<void>}
|
|
450
|
+
*/
|
|
451
|
+
async function setAifabrixEnvConfigPath(envConfigPath) {
|
|
452
|
+
if (!envConfigPath || typeof envConfigPath !== 'string') {
|
|
453
|
+
throw new Error('Env config path is required and must be a string');
|
|
454
|
+
}
|
|
455
|
+
const config = await getConfig();
|
|
456
|
+
config['aifabrix-env-config'] = envConfigPath;
|
|
386
457
|
await saveConfig(config);
|
|
387
458
|
}
|
|
388
459
|
|
|
@@ -405,6 +476,12 @@ const exportsObj = {
|
|
|
405
476
|
setSecretsEncryptionKey,
|
|
406
477
|
getSecretsPath,
|
|
407
478
|
setSecretsPath,
|
|
479
|
+
getAifabrixHomeOverride,
|
|
480
|
+
setAifabrixHomeOverride,
|
|
481
|
+
getAifabrixSecretsPath,
|
|
482
|
+
setAifabrixSecretsPath,
|
|
483
|
+
getAifabrixEnvConfigPath,
|
|
484
|
+
setAifabrixEnvConfigPath,
|
|
408
485
|
CONFIG_DIR,
|
|
409
486
|
CONFIG_FILE
|
|
410
487
|
};
|
|
@@ -419,6 +496,4 @@ Object.defineProperty(exportsObj, 'developerId', {
|
|
|
419
496
|
enumerable: true,
|
|
420
497
|
configurable: true
|
|
421
498
|
});
|
|
422
|
-
|
|
423
499
|
module.exports = exportsObj;
|
|
424
|
-
|
package/lib/env-reader.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require('fs').promises;
|
|
9
9
|
const path = require('path');
|
|
10
|
+
const { getCanonicalSecretName } = require('./secrets');
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Read existing .env file from application folder
|
|
@@ -147,8 +148,8 @@ function generateSecretsFromEnv(envVars) {
|
|
|
147
148
|
|
|
148
149
|
Object.entries(envVars).forEach(([key, value]) => {
|
|
149
150
|
if (detectSensitiveValue(key, value)) {
|
|
150
|
-
//
|
|
151
|
-
const secretName = key
|
|
151
|
+
// Use centralized resolver for canonical secret names
|
|
152
|
+
const secretName = getCanonicalSecretName(key);
|
|
152
153
|
secrets[secretName] = value;
|
|
153
154
|
}
|
|
154
155
|
});
|
package/lib/generator.js
CHANGED
|
@@ -185,9 +185,6 @@ function validateBuildFields(build) {
|
|
|
185
185
|
if (build.envOutputPath) {
|
|
186
186
|
buildConfig.envOutputPath = build.envOutputPath;
|
|
187
187
|
}
|
|
188
|
-
if (build.secrets && typeof build.secrets === 'string') {
|
|
189
|
-
buildConfig.secrets = build.secrets;
|
|
190
|
-
}
|
|
191
188
|
if (build.dockerfile && build.dockerfile.trim()) {
|
|
192
189
|
buildConfig.dockerfile = build.dockerfile;
|
|
193
190
|
}
|
|
@@ -210,12 +207,6 @@ function validateDeploymentFields(deployment) {
|
|
|
210
207
|
if (deployment.controllerUrl && deployment.controllerUrl.trim() && deployment.controllerUrl.startsWith('https://')) {
|
|
211
208
|
deploymentConfig.controllerUrl = deployment.controllerUrl;
|
|
212
209
|
}
|
|
213
|
-
if (deployment.clientId && deployment.clientId.trim()) {
|
|
214
|
-
deploymentConfig.clientId = deployment.clientId;
|
|
215
|
-
}
|
|
216
|
-
if (deployment.clientSecret && deployment.clientSecret.trim()) {
|
|
217
|
-
deploymentConfig.clientSecret = deployment.clientSecret;
|
|
218
|
-
}
|
|
219
210
|
|
|
220
211
|
return Object.keys(deploymentConfig).length > 0 ? deploymentConfig : null;
|
|
221
212
|
}
|
package/lib/infra.js
CHANGED
|
@@ -23,7 +23,22 @@ const dockerUtils = require('./utils/docker');
|
|
|
23
23
|
const paths = require('./utils/paths');
|
|
24
24
|
|
|
25
25
|
// Register Handlebars helper for equality check
|
|
26
|
-
|
|
26
|
+
// Handles both strict equality and numeric string comparisons
|
|
27
|
+
// Treats null/undefined as equivalent to "0" (default infrastructure)
|
|
28
|
+
handlebars.registerHelper('eq', (a, b) => {
|
|
29
|
+
// Handle null/undefined - treat as "0" for default infrastructure
|
|
30
|
+
if (a === null || a === undefined) a = '0';
|
|
31
|
+
if (b === null || b === undefined) b = '0';
|
|
32
|
+
|
|
33
|
+
// If both are numeric strings or one is number and other is numeric string, compare as numbers
|
|
34
|
+
const aNum = typeof a === 'string' && /^\d+$/.test(a) ? parseInt(a, 10) : a;
|
|
35
|
+
const bNum = typeof b === 'string' && /^\d+$/.test(b) ? parseInt(b, 10) : b;
|
|
36
|
+
// Use numeric comparison if both are numbers, otherwise strict equality
|
|
37
|
+
if (typeof aNum === 'number' && typeof bNum === 'number') {
|
|
38
|
+
return aNum === bNum;
|
|
39
|
+
}
|
|
40
|
+
return a === b;
|
|
41
|
+
});
|
|
27
42
|
const execAsync = promisify(exec);
|
|
28
43
|
|
|
29
44
|
/**
|
|
@@ -118,12 +133,25 @@ async function startInfra(developerId = null) {
|
|
|
118
133
|
}
|
|
119
134
|
|
|
120
135
|
// Generate compose file from template
|
|
136
|
+
// Register helper right before compiling to ensure it's available
|
|
137
|
+
handlebars.registerHelper('eq', (a, b) => {
|
|
138
|
+
if (a === null || a === undefined) a = '0';
|
|
139
|
+
if (b === null || b === undefined) b = '0';
|
|
140
|
+
const aNum = typeof a === 'string' && /^\d+$/.test(a) ? parseInt(a, 10) : a;
|
|
141
|
+
const bNum = typeof b === 'string' && /^\d+$/.test(b) ? parseInt(b, 10) : b;
|
|
142
|
+
if (typeof aNum === 'number' && typeof bNum === 'number') {
|
|
143
|
+
return aNum === bNum;
|
|
144
|
+
}
|
|
145
|
+
return a === b;
|
|
146
|
+
});
|
|
121
147
|
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
|
122
148
|
const template = handlebars.compile(templateContent);
|
|
123
149
|
// Dev 0: infra-aifabrix-network, Dev > 0: infra-dev{id}-aifabrix-network
|
|
124
150
|
const networkName = idNum === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
|
|
151
|
+
// Pass string devId to preserve leading zeros (e.g., "01") in container names
|
|
152
|
+
// The eq helper will handle numeric comparisons correctly
|
|
125
153
|
const composeContent = template({
|
|
126
|
-
devId:
|
|
154
|
+
devId: devId,
|
|
127
155
|
postgresPort: ports.postgres,
|
|
128
156
|
redisPort: ports.redis,
|
|
129
157
|
pgadminPort: ports.pgadmin,
|
|
@@ -466,7 +494,6 @@ async function getAppStatus() {
|
|
|
466
494
|
|
|
467
495
|
return apps;
|
|
468
496
|
}
|
|
469
|
-
|
|
470
497
|
module.exports = {
|
|
471
498
|
startInfra, stopInfra, stopInfraWithVolumes, checkInfraHealth,
|
|
472
499
|
getInfraStatus, getAppStatus, restartService, ensureAdminSecrets
|
|
@@ -633,11 +633,6 @@
|
|
|
633
633
|
"description": "Path where .env file is copied for local development (relative to builder/)",
|
|
634
634
|
"pattern": "^[^/].*"
|
|
635
635
|
},
|
|
636
|
-
"secrets": {
|
|
637
|
-
"type": "string",
|
|
638
|
-
"description": "Path to secrets file (defaults to ~/.aifabrix/secrets.yaml if empty)",
|
|
639
|
-
"pattern": "^[^/].*"
|
|
640
|
-
},
|
|
641
636
|
"localPort": {
|
|
642
637
|
"type": "integer",
|
|
643
638
|
"description": "Port for local development (different from Docker port)",
|
|
@@ -676,16 +671,6 @@
|
|
|
676
671
|
"type": "string",
|
|
677
672
|
"description": "Controller API URL for deployment",
|
|
678
673
|
"pattern": "^https://.*$"
|
|
679
|
-
},
|
|
680
|
-
"clientId": {
|
|
681
|
-
"type": "string",
|
|
682
|
-
"description": "Pipeline ClientId for automated deployment",
|
|
683
|
-
"pattern": "^[a-z0-9-]+$"
|
|
684
|
-
},
|
|
685
|
-
"clientSecret": {
|
|
686
|
-
"type": "string",
|
|
687
|
-
"description": "Pipeline ClientSecret (use kv:// reference)",
|
|
688
|
-
"pattern": "^(kv://.*|.+)$"
|
|
689
674
|
}
|
|
690
675
|
},
|
|
691
676
|
"additionalProperties": false
|
|
@@ -4,20 +4,20 @@
|
|
|
4
4
|
environments:
|
|
5
5
|
docker:
|
|
6
6
|
DB_HOST: postgres
|
|
7
|
+
DB_PORT: 5432
|
|
7
8
|
REDIS_HOST: redis
|
|
9
|
+
REDIS_PORT: 6379
|
|
8
10
|
MISO_HOST: miso-controller
|
|
11
|
+
MISO_PORT: 3000
|
|
9
12
|
KEYCLOAK_HOST: keycloak
|
|
10
|
-
|
|
11
|
-
OPENWEBUI_HOST: openwebui
|
|
12
|
-
FLOWISE_HOST: flowise
|
|
13
|
-
DATAPLANE_HOST: dataplane
|
|
13
|
+
KEYCLOAK_PORT: 8082
|
|
14
14
|
|
|
15
15
|
local:
|
|
16
16
|
DB_HOST: localhost
|
|
17
|
+
DB_PORT: 5432
|
|
17
18
|
REDIS_HOST: localhost
|
|
19
|
+
REDIS_PORT: 6379
|
|
18
20
|
MISO_HOST: localhost
|
|
21
|
+
MISO_PORT: 3010
|
|
19
22
|
KEYCLOAK_HOST: localhost
|
|
20
|
-
|
|
21
|
-
OPENWEBUI_HOST: localhost
|
|
22
|
-
FLOWISE_HOST: localhost
|
|
23
|
-
DATAPLANE_HOST: localhost
|
|
23
|
+
KEYCLOAK_PORT: 8082
|