@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.
@@ -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
- logger.log(chalk.gray(' Run onboarding and register Keycloak from the miso-controller repo if needed.'));
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
- const appPath = generatedFiles.appPath;
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
  }
@@ -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
- return config['aifabrix-secrets'] || config['secrets-path'] || null;
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) {
@@ -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) return null;
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
- if (!fs.existsSync(resolvedConfigPath)) return null;
148
- const configSecrets = readYamlAtPath(resolvedConfigPath);
149
- if (!configSecrets || typeof configSecrets !== 'object') return null;
150
- const merged = { ...configSecrets };
151
- const userSecrets = loadUserSecrets();
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
- // Use the same path resolution logic as loadSecrets
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
- * Loads and validates environment deploy config from a JSON file
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
- const valid = validate(parsed);
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
-
@@ -31,11 +31,15 @@ function safeHomedir() {
31
31
  }
32
32
 
33
33
  /**
34
- * Returns the path to the config file (AIFABRIX_HOME env or ~/.aifabrix).
35
- * Used so getAifabrixHome can read from the same location as config.js.
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.37.5",
3
+ "version": "2.37.9",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {