@aifabrix/builder 2.37.5 → 2.37.9
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/commands/up-miso.js +2 -2
- package/lib/commands/wizard-core.js +39 -27
- package/lib/core/config.js +16 -1
- package/lib/core/secrets.js +42 -50
- package/lib/deployment/environment.js +32 -21
- package/lib/utils/paths.js +6 -3
- package/package.json +1 -1
package/lib/commands/up-miso.js
CHANGED
|
@@ -140,8 +140,8 @@ async function handleUpMiso(options = {}) {
|
|
|
140
140
|
const devIdNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
|
|
141
141
|
await setMisoSecretsAndResolve(devIdNum);
|
|
142
142
|
await runMisoApps(options);
|
|
143
|
-
logger.log(chalk.green('\n✓ up-miso complete. Keycloak, miso-controller, and dataplane are running.')
|
|
144
|
-
|
|
143
|
+
logger.log(chalk.green('\n✓ up-miso complete. Keycloak, miso-controller, and dataplane are running.') +
|
|
144
|
+
chalk.gray('\n Run onboarding and register Keycloak from the miso-controller repo if needed.'));
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
module.exports = { handleUpMiso, parseImageOptions };
|
|
@@ -338,6 +338,44 @@ async function validateWizardConfiguration(dataplaneUrl, authConfig, systemConfi
|
|
|
338
338
|
}
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Fetches deployment docs and writes README.md when variables.yaml and deploy JSON are available.
|
|
343
|
+
* @async
|
|
344
|
+
* @param {string} appPath - Application path
|
|
345
|
+
* @param {string} appName - Application name
|
|
346
|
+
* @param {string} dataplaneUrl - Dataplane URL
|
|
347
|
+
* @param {Object} authConfig - Authentication configuration
|
|
348
|
+
* @param {string} systemKey - System key
|
|
349
|
+
*/
|
|
350
|
+
async function tryUpdateReadmeFromDeploymentDocs(appPath, appName, dataplaneUrl, authConfig, systemKey) {
|
|
351
|
+
const variablesPath = path.join(appPath, 'variables.yaml');
|
|
352
|
+
const deployPath = path.join(appPath, `${appName}-deploy.json`);
|
|
353
|
+
let variablesYaml = null;
|
|
354
|
+
let deployJson = null;
|
|
355
|
+
try {
|
|
356
|
+
variablesYaml = await fs.readFile(variablesPath, 'utf8');
|
|
357
|
+
} catch {
|
|
358
|
+
// optional
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
const deployContent = await fs.readFile(deployPath, 'utf8');
|
|
362
|
+
deployJson = JSON.parse(deployContent);
|
|
363
|
+
} catch {
|
|
364
|
+
// optional
|
|
365
|
+
}
|
|
366
|
+
const hasBody = variablesYaml !== null || deployJson !== null;
|
|
367
|
+
const body = hasBody ? { variablesYaml: variablesYaml || null, deployJson: deployJson || null } : null;
|
|
368
|
+
const docsResponse = body
|
|
369
|
+
? await postDeploymentDocs(dataplaneUrl, authConfig, systemKey, body)
|
|
370
|
+
: await getDeploymentDocs(dataplaneUrl, authConfig, systemKey);
|
|
371
|
+
const content = docsResponse?.data?.content ?? docsResponse?.content;
|
|
372
|
+
if (content && typeof content === 'string') {
|
|
373
|
+
const readmePath = path.join(appPath, 'README.md');
|
|
374
|
+
await fs.writeFile(readmePath, content, 'utf8');
|
|
375
|
+
logger.log(chalk.gray(' Updated README.md from deployment-docs API (variables.yaml + deploy JSON).'));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
341
379
|
/**
|
|
342
380
|
* Handle file saving step
|
|
343
381
|
* @async
|
|
@@ -357,33 +395,7 @@ async function handleFileSaving(appName, systemConfig, datasourceConfigs, system
|
|
|
357
395
|
const generatedFiles = await generateWizardFiles(appName, systemConfig, datasourceConfigs, systemKey, { aiGeneratedReadme: null });
|
|
358
396
|
if (systemKey && dataplaneUrl && authConfig && generatedFiles.appPath) {
|
|
359
397
|
try {
|
|
360
|
-
|
|
361
|
-
const deployKey = appName;
|
|
362
|
-
const variablesPath = path.join(appPath, 'variables.yaml');
|
|
363
|
-
const deployPath = path.join(appPath, `${deployKey}-deploy.json`);
|
|
364
|
-
let variablesYaml = null;
|
|
365
|
-
let deployJson = null;
|
|
366
|
-
try {
|
|
367
|
-
variablesYaml = await fs.readFile(variablesPath, 'utf8');
|
|
368
|
-
} catch {
|
|
369
|
-
// optional
|
|
370
|
-
}
|
|
371
|
-
try {
|
|
372
|
-
const deployContent = await fs.readFile(deployPath, 'utf8');
|
|
373
|
-
deployJson = JSON.parse(deployContent);
|
|
374
|
-
} catch {
|
|
375
|
-
// optional
|
|
376
|
-
}
|
|
377
|
-
const body = (variablesYaml !== null && variablesYaml !== undefined) || (deployJson !== null && deployJson !== undefined) ? { variablesYaml: variablesYaml || null, deployJson: deployJson || null } : null;
|
|
378
|
-
const docsResponse = body
|
|
379
|
-
? await postDeploymentDocs(dataplaneUrl, authConfig, systemKey, body)
|
|
380
|
-
: await getDeploymentDocs(dataplaneUrl, authConfig, systemKey);
|
|
381
|
-
const content = docsResponse?.data?.content ?? docsResponse?.content;
|
|
382
|
-
if (content && typeof content === 'string') {
|
|
383
|
-
const readmePath = path.join(appPath, 'README.md');
|
|
384
|
-
await fs.writeFile(readmePath, content, 'utf8');
|
|
385
|
-
logger.log(chalk.gray(' Updated README.md from deployment-docs API (variables.yaml + deploy JSON).'));
|
|
386
|
-
}
|
|
398
|
+
await tryUpdateReadmeFromDeploymentDocs(generatedFiles.appPath, appName, dataplaneUrl, authConfig, systemKey);
|
|
387
399
|
} catch (e) {
|
|
388
400
|
logger.log(chalk.gray(` Could not fetch AI-generated README: ${e.message}`));
|
|
389
401
|
}
|
package/lib/core/config.js
CHANGED
|
@@ -405,9 +405,24 @@ async function ensureSecretsEncryptionKey() {
|
|
|
405
405
|
await run({ getSecretsEncryptionKey, setSecretsEncryptionKey, getSecretsPath });
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Expand leading ~ to home directory so config paths like ~/.aifabrix/secrets.local.yaml resolve correctly.
|
|
410
|
+
* @param {string} filePath - Path that may start with ~ or ~/
|
|
411
|
+
* @returns {string} Path with ~ expanded, or unchanged if no leading ~
|
|
412
|
+
*/
|
|
413
|
+
function expandTilde(filePath) {
|
|
414
|
+
if (!filePath || typeof filePath !== 'string') return filePath;
|
|
415
|
+
if (filePath === '~') return os.homedir();
|
|
416
|
+
if (filePath.startsWith('~/') || filePath.startsWith('~' + path.sep)) {
|
|
417
|
+
return path.join(os.homedir(), filePath.slice(2));
|
|
418
|
+
}
|
|
419
|
+
return filePath;
|
|
420
|
+
}
|
|
421
|
+
|
|
408
422
|
async function getSecretsPath() {
|
|
409
423
|
const config = await getConfig();
|
|
410
|
-
|
|
424
|
+
const raw = config['aifabrix-secrets'] || config['secrets-path'] || null;
|
|
425
|
+
return raw ? expandTilde(raw) : null;
|
|
411
426
|
}
|
|
412
427
|
|
|
413
428
|
async function setSecretsPath(secretsPath) {
|
package/lib/core/secrets.js
CHANGED
|
@@ -132,28 +132,57 @@ async function decryptSecretsObject(secrets) {
|
|
|
132
132
|
* const secrets = await loadSecrets(undefined, 'myapp');
|
|
133
133
|
*/
|
|
134
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Merges config file secrets into user secrets (user wins). Returns null if path missing or config empty.
|
|
137
|
+
* @param {Object} userSecrets - User secrets object
|
|
138
|
+
* @param {string} resolvedConfigPath - Absolute path to config secrets file
|
|
139
|
+
* @returns {Object|null} Merged secrets or null
|
|
140
|
+
*/
|
|
141
|
+
function mergeUserWithConfigFile(userSecrets, resolvedConfigPath) {
|
|
142
|
+
if (!fs.existsSync(resolvedConfigPath)) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
let configSecrets;
|
|
146
|
+
try {
|
|
147
|
+
configSecrets = readYamlAtPath(resolvedConfigPath);
|
|
148
|
+
} catch (loadError) {
|
|
149
|
+
throw new Error(`Failed to load secrets file ${resolvedConfigPath}: ${loadError.message}`);
|
|
150
|
+
}
|
|
151
|
+
if (!configSecrets || typeof configSecrets !== 'object') {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const merged = { ...userSecrets };
|
|
155
|
+
for (const key of Object.keys(configSecrets)) {
|
|
156
|
+
if (!(key in merged) || merged[key] === undefined || merged[key] === null || merged[key] === '') {
|
|
157
|
+
merged[key] = configSecrets[key];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return merged;
|
|
161
|
+
}
|
|
162
|
+
|
|
135
163
|
/**
|
|
136
164
|
* Loads config secrets path, merges with user secrets (user overrides). Used by loadSecrets cascade.
|
|
137
165
|
* @async
|
|
138
166
|
* @returns {Promise<Object|null>} Merged secrets object or null
|
|
139
167
|
*/
|
|
140
168
|
async function loadMergedConfigAndUserSecrets() {
|
|
169
|
+
const userSecrets = loadUserSecrets();
|
|
170
|
+
const hasKeys = (obj) => obj && Object.keys(obj).length > 0;
|
|
171
|
+
const userOrNull = () => (hasKeys(userSecrets) ? userSecrets : null);
|
|
141
172
|
try {
|
|
142
173
|
const configSecretsPath = await config.getSecretsPath();
|
|
143
|
-
if (!configSecretsPath)
|
|
174
|
+
if (!configSecretsPath) {
|
|
175
|
+
return userOrNull();
|
|
176
|
+
}
|
|
144
177
|
const resolvedConfigPath = path.isAbsolute(configSecretsPath)
|
|
145
178
|
? configSecretsPath
|
|
146
179
|
: path.resolve(process.cwd(), configSecretsPath);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
for (const key of Object.keys(userSecrets)) {
|
|
153
|
-
merged[key] = userSecrets[key];
|
|
180
|
+
const merged = mergeUserWithConfigFile(userSecrets, resolvedConfigPath);
|
|
181
|
+
return merged !== null ? merged : userOrNull();
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (error.message && error.message.startsWith('Failed to load secrets file')) {
|
|
184
|
+
throw error;
|
|
154
185
|
}
|
|
155
|
-
return merged;
|
|
156
|
-
} catch {
|
|
157
186
|
return null;
|
|
158
187
|
}
|
|
159
188
|
}
|
|
@@ -229,22 +258,11 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local',
|
|
|
229
258
|
return replaceKvInContent(resolved, secrets, envVars);
|
|
230
259
|
}
|
|
231
260
|
|
|
232
|
-
/**
|
|
233
|
-
* Applies environment-specific transformations to resolved content
|
|
234
|
-
* @async
|
|
235
|
-
* @function applyEnvironmentTransformations
|
|
236
|
-
* @param {string} resolved - Resolved environment content
|
|
237
|
-
* @param {string} environment - Environment context
|
|
238
|
-
* @param {string} variablesPath - Path to variables.yaml file
|
|
239
|
-
* @returns {Promise<string>} Transformed content
|
|
240
|
-
*/
|
|
261
|
+
/** Applies environment-specific transformations to resolved content. */
|
|
241
262
|
async function applyEnvironmentTransformations(resolved, environment, variablesPath) {
|
|
242
263
|
if (environment === 'docker') {
|
|
243
264
|
resolved = await resolveServicePortsInEnvContent(resolved, environment);
|
|
244
265
|
resolved = await rewriteInfraEndpoints(resolved, 'docker');
|
|
245
|
-
// Interpolate ${VAR} references created by rewriteInfraEndpoints
|
|
246
|
-
// Get the actual host and port values from env-endpoints.js directly
|
|
247
|
-
// to ensure they are correctly populated in envVars for interpolation
|
|
248
266
|
const { getEnvHosts, getServiceHost, getServicePort, getLocalhostOverride } = require('../utils/env-endpoints');
|
|
249
267
|
const hosts = await getEnvHosts('docker');
|
|
250
268
|
const localhostOverride = getLocalhostOverride('docker');
|
|
@@ -252,10 +270,7 @@ async function applyEnvironmentTransformations(resolved, environment, variablesP
|
|
|
252
270
|
const redisPort = await getServicePort('REDIS_PORT', 'redis', hosts, 'docker', null);
|
|
253
271
|
const dbHost = getServiceHost(hosts.DB_HOST, 'docker', 'postgres', localhostOverride);
|
|
254
272
|
const dbPort = await getServicePort('DB_PORT', 'postgres', hosts, 'docker', null);
|
|
255
|
-
|
|
256
|
-
// Build envVars map and ensure it has the correct values
|
|
257
273
|
const envVars = await buildEnvVarMap('docker');
|
|
258
|
-
// Override with the actual values that were just set by rewriteInfraEndpoints
|
|
259
274
|
envVars.REDIS_HOST = redisHost;
|
|
260
275
|
envVars.REDIS_PORT = String(redisPort);
|
|
261
276
|
envVars.DB_HOST = dbHost;
|
|
@@ -269,35 +284,15 @@ async function applyEnvironmentTransformations(resolved, environment, variablesP
|
|
|
269
284
|
return resolved;
|
|
270
285
|
}
|
|
271
286
|
|
|
272
|
-
/**
|
|
273
|
-
* Generates .env file content from template and secrets (without writing to disk)
|
|
274
|
-
* @async
|
|
275
|
-
* @function generateEnvContent
|
|
276
|
-
* @param {string} appName - Name of the application
|
|
277
|
-
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
278
|
-
* @param {string} [environment='local'] - Environment context
|
|
279
|
-
* @param {boolean} [force=false] - Generate missing secret keys in secrets file
|
|
280
|
-
* @returns {Promise<string>} Generated .env file content
|
|
281
|
-
* @throws {Error} If generation fails
|
|
282
|
-
*/
|
|
287
|
+
/** Generates .env file content from template and secrets (without writing to disk). */
|
|
283
288
|
async function generateEnvContent(appName, secretsPath, environment = 'local', force = false) {
|
|
284
289
|
const builderPath = pathsUtil.getBuilderPath(appName);
|
|
285
290
|
const templatePath = path.join(builderPath, 'env.template');
|
|
286
291
|
const variablesPath = path.join(builderPath, 'variables.yaml');
|
|
287
|
-
|
|
288
292
|
const template = loadEnvTemplate(templatePath);
|
|
289
293
|
const secretsPaths = await getActualSecretsPath(secretsPath, appName);
|
|
290
|
-
|
|
291
294
|
if (force) {
|
|
292
|
-
|
|
293
|
-
// If explicit path provided, use it; otherwise use the path that loadUserSecrets() would use
|
|
294
|
-
let secretsFileForGeneration;
|
|
295
|
-
if (secretsPath) {
|
|
296
|
-
secretsFileForGeneration = resolveSecretsPath(secretsPath);
|
|
297
|
-
} else {
|
|
298
|
-
// Use the same path that loadUserSecrets() would use (now uses paths.getAifabrixHome())
|
|
299
|
-
secretsFileForGeneration = secretsPaths.userPath;
|
|
300
|
-
}
|
|
295
|
+
const secretsFileForGeneration = secretsPath ? resolveSecretsPath(secretsPath) : secretsPaths.userPath;
|
|
301
296
|
await generateMissingSecrets(template, secretsFileForGeneration);
|
|
302
297
|
}
|
|
303
298
|
|
|
@@ -472,9 +467,6 @@ REDIS_COMMANDER_PASSWORD=${postgresPassword}
|
|
|
472
467
|
fs.writeFileSync(adminEnvPath, adminSecrets, { mode: 0o600 });
|
|
473
468
|
return adminEnvPath;
|
|
474
469
|
}
|
|
475
|
-
|
|
476
|
-
// validateSecrets is imported from ./utils/secrets-helpers
|
|
477
|
-
|
|
478
470
|
module.exports = {
|
|
479
471
|
loadSecrets,
|
|
480
472
|
resolveKvReferences,
|
|
@@ -82,20 +82,8 @@ async function getEnvironmentAuth(controllerUrl) {
|
|
|
82
82
|
* @returns {Promise<Object>} Deployment result
|
|
83
83
|
* @throws {Error} If deployment fails
|
|
84
84
|
*/
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
* @param {string} configPath - Absolute or relative path to config JSON
|
|
88
|
-
* @returns {Object} Valid deploy request { environmentConfig, dryRun? }
|
|
89
|
-
* @throws {Error} If file missing, invalid JSON, or validation fails
|
|
90
|
-
*/
|
|
91
|
-
function loadAndValidateEnvironmentDeployConfig(configPath) {
|
|
92
|
-
const resolvedPath = path.isAbsolute(configPath) ? configPath : path.resolve(process.cwd(), configPath);
|
|
93
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
94
|
-
throw new Error(
|
|
95
|
-
`Environment config file not found: ${resolvedPath}\n` +
|
|
96
|
-
'Use --config <file> with a JSON file containing "environmentConfig" (e.g. templates/infra/environment-dev.json).'
|
|
97
|
-
);
|
|
98
|
-
}
|
|
85
|
+
/** Reads and parses config file; throws if missing, unreadable, or invalid structure. */
|
|
86
|
+
function parseEnvironmentConfigFile(resolvedPath) {
|
|
99
87
|
let raw;
|
|
100
88
|
try {
|
|
101
89
|
raw = fs.readFileSync(resolvedPath, 'utf8');
|
|
@@ -124,14 +112,21 @@ function loadAndValidateEnvironmentDeployConfig(configPath) {
|
|
|
124
112
|
);
|
|
125
113
|
}
|
|
126
114
|
if (typeof parsed.environmentConfig !== 'object' || parsed.environmentConfig === null) {
|
|
127
|
-
throw new Error(
|
|
128
|
-
`"environmentConfig" must be an object. File: ${resolvedPath}`
|
|
129
|
-
);
|
|
115
|
+
throw new Error(`"environmentConfig" must be an object. File: ${resolvedPath}`);
|
|
130
116
|
}
|
|
117
|
+
return parsed;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validates parsed config against schema and returns deploy request.
|
|
122
|
+
* @param {Object} parsed - Parsed config object
|
|
123
|
+
* @param {string} resolvedPath - Path for error messages
|
|
124
|
+
* @returns {Object} { environmentConfig, dryRun? }
|
|
125
|
+
*/
|
|
126
|
+
function validateEnvironmentDeployParsed(parsed, resolvedPath) {
|
|
131
127
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
132
128
|
const validate = ajv.compile(environmentDeployRequestSchema);
|
|
133
|
-
|
|
134
|
-
if (!valid) {
|
|
129
|
+
if (!validate(parsed)) {
|
|
135
130
|
const messages = formatValidationErrors(validate.errors);
|
|
136
131
|
throw new Error(
|
|
137
132
|
`Environment config validation failed (${resolvedPath}):\n • ${messages.join('\n • ')}\n` +
|
|
@@ -144,6 +139,24 @@ function loadAndValidateEnvironmentDeployConfig(configPath) {
|
|
|
144
139
|
};
|
|
145
140
|
}
|
|
146
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Loads and validates environment deploy config from a JSON file
|
|
144
|
+
* @param {string} configPath - Absolute or relative path to config JSON
|
|
145
|
+
* @returns {Object} Valid deploy request { environmentConfig, dryRun? }
|
|
146
|
+
* @throws {Error} If file missing, invalid JSON, or validation fails
|
|
147
|
+
*/
|
|
148
|
+
function loadAndValidateEnvironmentDeployConfig(configPath) {
|
|
149
|
+
const resolvedPath = path.isAbsolute(configPath) ? configPath : path.resolve(process.cwd(), configPath);
|
|
150
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Environment config file not found: ${resolvedPath}\n` +
|
|
153
|
+
'Use --config <file> with a JSON file containing "environmentConfig" (e.g. templates/infra/environment-dev.json).'
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const parsed = parseEnvironmentConfigFile(resolvedPath);
|
|
157
|
+
return validateEnvironmentDeployParsed(parsed, resolvedPath);
|
|
158
|
+
}
|
|
159
|
+
|
|
147
160
|
/**
|
|
148
161
|
* Builds environment deployment request from options (config file required)
|
|
149
162
|
* @function buildEnvironmentDeploymentRequest
|
|
@@ -479,8 +492,6 @@ async function deployEnvironment(envKey, options = {}) {
|
|
|
479
492
|
throw error;
|
|
480
493
|
}
|
|
481
494
|
}
|
|
482
|
-
|
|
483
495
|
module.exports = {
|
|
484
496
|
deployEnvironment
|
|
485
497
|
};
|
|
486
|
-
|
package/lib/utils/paths.js
CHANGED
|
@@ -31,11 +31,15 @@ function safeHomedir() {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Returns the path to the config
|
|
35
|
-
*
|
|
34
|
+
* Returns the path to the config directory (same precedence as config.js so both read the same config).
|
|
35
|
+
* Priority: AIFABRIX_CONFIG (dirname) → AIFABRIX_HOME → ~/.aifabrix.
|
|
36
36
|
* @returns {string} Absolute path to config directory
|
|
37
37
|
*/
|
|
38
38
|
function getConfigDirForPaths() {
|
|
39
|
+
const configFile = process.env.AIFABRIX_CONFIG && typeof process.env.AIFABRIX_CONFIG === 'string';
|
|
40
|
+
if (configFile) {
|
|
41
|
+
return path.dirname(path.resolve(process.env.AIFABRIX_CONFIG.trim()));
|
|
42
|
+
}
|
|
39
43
|
if (process.env.AIFABRIX_HOME && typeof process.env.AIFABRIX_HOME === 'string') {
|
|
40
44
|
return path.resolve(process.env.AIFABRIX_HOME.trim());
|
|
41
45
|
}
|
|
@@ -480,7 +484,6 @@ async function detectAppType(appName, options = {}) {
|
|
|
480
484
|
// Check builder folder (backward compatibility)
|
|
481
485
|
return checkBuilderFolder(appName);
|
|
482
486
|
}
|
|
483
|
-
|
|
484
487
|
module.exports = {
|
|
485
488
|
getAifabrixHome,
|
|
486
489
|
getConfigDirForPaths,
|