@aifabrix/builder 2.4.0 → 2.5.1
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/commands/secure.js +5 -40
- package/lib/config.js +26 -4
- 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 +22 -20
- package/lib/utils/secrets-generator.js +3 -3
- package/lib/utils/secrets-helpers.js +359 -0
- package/lib/utils/secrets-path.js +12 -36
- package/lib/utils/secrets-url.js +38 -0
- package/lib/utils/secrets-utils.js +1 -42
- package/lib/utils/variable-transformer.js +0 -9
- package/package.json +1 -1
- package/templates/applications/README.md.hbs +4 -2
- 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/commands/secure.js
CHANGED
|
@@ -22,7 +22,7 @@ const { encryptYamlValues } = require('../utils/yaml-preserve');
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Finds all secrets.local.yaml files to encrypt
|
|
25
|
-
* Includes user secrets file and
|
|
25
|
+
* Includes user secrets file and general secrets from config.yaml
|
|
26
26
|
*
|
|
27
27
|
* @async
|
|
28
28
|
* @function findSecretsFiles
|
|
@@ -37,45 +37,10 @@ async function findSecretsFiles() {
|
|
|
37
37
|
files.push({ path: userSecretsPath, type: 'user' });
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
//
|
|
41
|
-
// Scan builder directory for apps
|
|
40
|
+
// Check config.yaml for aifabrix-secrets
|
|
42
41
|
try {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
const entries = fs.readdirSync(builderDir, { withFileTypes: true });
|
|
46
|
-
for (const entry of entries) {
|
|
47
|
-
if (entry.isDirectory()) {
|
|
48
|
-
const appName = entry.name;
|
|
49
|
-
const variablesPath = path.join(builderDir, appName, 'variables.yaml');
|
|
50
|
-
if (fs.existsSync(variablesPath)) {
|
|
51
|
-
try {
|
|
52
|
-
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
53
|
-
const variables = yaml.load(variablesContent);
|
|
54
|
-
|
|
55
|
-
if (variables?.build?.secrets) {
|
|
56
|
-
const buildSecretsPath = path.resolve(
|
|
57
|
-
path.dirname(variablesPath),
|
|
58
|
-
variables.build.secrets
|
|
59
|
-
);
|
|
60
|
-
if (fs.existsSync(buildSecretsPath)) {
|
|
61
|
-
files.push({ path: buildSecretsPath, type: `app:${appName}` });
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
} catch (error) {
|
|
65
|
-
// Ignore errors, continue
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
} catch (error) {
|
|
72
|
-
// Ignore errors, continue
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Check config.yaml for general secrets-path
|
|
76
|
-
try {
|
|
77
|
-
const { getSecretsPath } = require('../config');
|
|
78
|
-
const generalSecretsPath = await getSecretsPath();
|
|
42
|
+
const { getAifabrixSecretsPath } = require('../config');
|
|
43
|
+
const generalSecretsPath = await getAifabrixSecretsPath();
|
|
79
44
|
if (generalSecretsPath) {
|
|
80
45
|
const resolvedPath = path.isAbsolute(generalSecretsPath)
|
|
81
46
|
? generalSecretsPath
|
|
@@ -205,7 +170,7 @@ async function handleSecure(options) {
|
|
|
205
170
|
|
|
206
171
|
if (secretsFiles.length === 0) {
|
|
207
172
|
logger.log(chalk.yellow('⚠️ No secrets files found to encrypt'));
|
|
208
|
-
logger.log(chalk.gray(' Create ~/.aifabrix/secrets.local.yaml or configure
|
|
173
|
+
logger.log(chalk.gray(' Create ~/.aifabrix/secrets.local.yaml or configure aifabrix-secrets in config.yaml'));
|
|
209
174
|
return;
|
|
210
175
|
}
|
|
211
176
|
|
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
|
*
|
|
@@ -364,7 +363,7 @@ async function setSecretsEncryptionKey(key) {
|
|
|
364
363
|
|
|
365
364
|
/**
|
|
366
365
|
* Get general secrets path from configuration
|
|
367
|
-
*
|
|
366
|
+
* Returns aifabrix-secrets path from config.yaml if configured
|
|
368
367
|
* @returns {Promise<string|null>} Secrets path or null if not set
|
|
369
368
|
*/
|
|
370
369
|
async function getSecretsPath() {
|
|
@@ -435,6 +434,29 @@ async function setAifabrixSecretsPath(secretsPath) {
|
|
|
435
434
|
await saveConfig(config);
|
|
436
435
|
}
|
|
437
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;
|
|
457
|
+
await saveConfig(config);
|
|
458
|
+
}
|
|
459
|
+
|
|
438
460
|
// Create exports object
|
|
439
461
|
const exportsObj = {
|
|
440
462
|
getConfig,
|
|
@@ -458,6 +480,8 @@ const exportsObj = {
|
|
|
458
480
|
setAifabrixHomeOverride,
|
|
459
481
|
getAifabrixSecretsPath,
|
|
460
482
|
setAifabrixSecretsPath,
|
|
483
|
+
getAifabrixEnvConfigPath,
|
|
484
|
+
setAifabrixEnvConfigPath,
|
|
461
485
|
CONFIG_DIR,
|
|
462
486
|
CONFIG_FILE
|
|
463
487
|
};
|
|
@@ -472,6 +496,4 @@ Object.defineProperty(exportsObj, 'developerId', {
|
|
|
472
496
|
enumerable: true,
|
|
473
497
|
configurable: true
|
|
474
498
|
});
|
|
475
|
-
|
|
476
499
|
module.exports = exportsObj;
|
|
477
|
-
|
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
|