@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.
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Secrets helper utilities
3
+ *
4
+ * @fileoverview Helper functions for secrets and env processing
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const yaml = require('js-yaml');
14
+ const config = require('../config');
15
+ const { buildHostnameToServiceMap, resolveUrlPort } = require('./secrets-utils');
16
+ const devConfig = require('../utils/dev-config');
17
+ const { rewriteInfraEndpoints, getEnvHosts } = require('./env-endpoints');
18
+ const { loadEnvConfig } = require('./env-config-loader');
19
+ const { processEnvVariables } = require('./env-copy');
20
+ const { updateContainerPortInEnvFile } = require('./env-ports');
21
+
22
+ /**
23
+ * Interpolate ${VAR} occurrences with values from envVars map
24
+ * @function interpolateEnvVars
25
+ * @param {string} content - Text content
26
+ * @param {Object} envVars - Map of variable name to value
27
+ * @returns {string} Interpolated content
28
+ */
29
+ function interpolateEnvVars(content, envVars) {
30
+ return content.replace(/\$\{([A-Z_]+)\}/g, (match, envVar) => {
31
+ return envVars[envVar] || match;
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Collect missing kv:// secrets referenced in content
37
+ * @function collectMissingSecrets
38
+ * @param {string} content - Text content
39
+ * @param {Object} secrets - Available secrets
40
+ * @returns {string[]} Array of missing kv://<key> references
41
+ */
42
+ function collectMissingSecrets(content, secrets) {
43
+ const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
44
+ const missing = [];
45
+ let match;
46
+ while ((match = kvPattern.exec(content)) !== null) {
47
+ const secretKey = match[1];
48
+ if (!(secretKey in secrets)) {
49
+ missing.push(`kv://${secretKey}`);
50
+ }
51
+ }
52
+ return missing;
53
+ }
54
+
55
+ /**
56
+ * Format secrets file info for error message
57
+ * @function formatMissingSecretsFileInfo
58
+ * @param {Object|string|null} secretsFilePaths - Paths or single string path
59
+ * @returns {string} Formatted file info suffix for error message
60
+ */
61
+ function formatMissingSecretsFileInfo(secretsFilePaths) {
62
+ if (!secretsFilePaths) {
63
+ return '';
64
+ }
65
+ if (typeof secretsFilePaths === 'string') {
66
+ return `\n\nSecrets file location: ${secretsFilePaths}`;
67
+ }
68
+ if (typeof secretsFilePaths === 'object' && secretsFilePaths.userPath) {
69
+ const paths = [secretsFilePaths.userPath];
70
+ if (secretsFilePaths.buildPath) {
71
+ paths.push(secretsFilePaths.buildPath);
72
+ }
73
+ return `\n\nSecrets file location: ${paths.join(' and ')}`;
74
+ }
75
+ return '';
76
+ }
77
+
78
+ /**
79
+ * Replace kv:// references with actual values, after also interpolating any ${VAR} within secret values
80
+ * @function replaceKvInContent
81
+ * @param {string} content - Text content containing kv:// references
82
+ * @param {Object} secrets - Secrets map
83
+ * @param {Object} envVars - Environment variables map for nested interpolation
84
+ * @returns {string} Content with kv:// references replaced
85
+ */
86
+ function replaceKvInContent(content, secrets, envVars) {
87
+ const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
88
+ return content.replace(kvPattern, (match, secretKey) => {
89
+ let value = secrets[secretKey];
90
+ if (typeof value === 'string') {
91
+ value = value.replace(/\$\{([A-Z_]+)\}/g, (m, envVar) => {
92
+ return envVars[envVar] || m;
93
+ });
94
+ }
95
+ return value;
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Resolve service ports inside URLs for docker environment (.env content)
101
+ * @async
102
+ * @function resolveServicePortsInEnvContent
103
+ * @param {string} envContent - .env content
104
+ * @param {string} environment - Environment name
105
+ * @returns {Promise<string>} Updated content
106
+ */
107
+ async function resolveServicePortsInEnvContent(envContent, environment) {
108
+ if (environment !== 'docker') {
109
+ return envContent;
110
+ }
111
+ const envConfig = await loadEnvConfig();
112
+ const dockerHosts = envConfig.environments.docker || {};
113
+ const hostnameToService = buildHostnameToServiceMap(dockerHosts);
114
+ const urlPattern = /(https?:\/\/)([a-zA-Z0-9-]+):(\d+)([^\s\n]*)?/g;
115
+ return envContent.replace(urlPattern, (match, protocol, hostname, port, urlPath = '') => {
116
+ return resolveUrlPort(protocol, hostname, port, urlPath || '', hostnameToService);
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Load env.template content from disk
122
+ * @function loadEnvTemplate
123
+ * @param {string} templatePath - Path to env.template
124
+ * @returns {string} Template content
125
+ * @throws {Error} If template not found
126
+ */
127
+ function loadEnvTemplate(templatePath) {
128
+ if (!fs.existsSync(templatePath)) {
129
+ throw new Error(`env.template not found: ${templatePath}`);
130
+ }
131
+ return fs.readFileSync(templatePath, 'utf8');
132
+ }
133
+
134
+ /**
135
+ * Adjust infra-related ports in resolved .env content for local environment
136
+ * Follows flow: getEnvHosts() → config.yaml override → variables.yaml override → developer-id adjustment
137
+ * @async
138
+ * @function adjustLocalEnvPortsInContent
139
+ * @param {string} envContent - Resolved .env content
140
+ * @param {string} [variablesPath] - Path to variables.yaml (to read build.localPort)
141
+ * @returns {Promise<string>} Updated content with local ports
142
+ */
143
+ async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
144
+ // Get developer-id for port adjustment
145
+ const devId = await config.getDeveloperId();
146
+ let devIdNum = 0;
147
+ if (devId !== null && devId !== undefined) {
148
+ const parsed = parseInt(devId, 10);
149
+ if (!Number.isNaN(parsed)) {
150
+ devIdNum = parsed;
151
+ }
152
+ }
153
+
154
+ // Step 1: Get base config from env-config.yaml (includes user env-config file if configured)
155
+ let localEnv = await getEnvHosts('local');
156
+
157
+ // Step 2: Apply config.yaml → environments.local override (if exists)
158
+ try {
159
+ const os = require('os');
160
+ const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
161
+ if (fs.existsSync(cfgPath)) {
162
+ const cfgContent = fs.readFileSync(cfgPath, 'utf8');
163
+ const cfg = yaml.load(cfgContent) || {};
164
+ if (cfg && cfg.environments && cfg.environments.local) {
165
+ localEnv = { ...localEnv, ...cfg.environments.local };
166
+ }
167
+ }
168
+ } catch {
169
+ // Ignore config.yaml read errors, continue with env-config values
170
+ }
171
+
172
+ // Step 3: Get PORT value following override chain
173
+ // Start with env-config value, override with variables.yaml build.localPort, then port
174
+ let baseAppPort = null;
175
+ if (localEnv.PORT !== undefined && localEnv.PORT !== null) {
176
+ const portVal = typeof localEnv.PORT === 'number' ? localEnv.PORT : parseInt(localEnv.PORT, 10);
177
+ if (!Number.isNaN(portVal)) {
178
+ baseAppPort = portVal;
179
+ }
180
+ }
181
+
182
+ // Override with variables.yaml → build.localPort (if exists)
183
+ if (variablesPath && fs.existsSync(variablesPath)) {
184
+ try {
185
+ const variablesContent = fs.readFileSync(variablesPath, 'utf8');
186
+ const variables = yaml.load(variablesContent);
187
+ const localPort = variables?.build?.localPort;
188
+ if (typeof localPort === 'number' && localPort > 0) {
189
+ baseAppPort = localPort;
190
+ } else if (baseAppPort === null || baseAppPort === undefined) {
191
+ // Fallback to variables.yaml → port if baseAppPort still not set
192
+ baseAppPort = variables?.port || 3000;
193
+ }
194
+ } catch {
195
+ // Fallback to reading from env content if variables.yaml read fails
196
+ if (baseAppPort === null || baseAppPort === undefined) {
197
+ const portMatch = envContent.match(/^PORT\s*=\s*(\d+)/m);
198
+ baseAppPort = portMatch ? parseInt(portMatch[1], 10) : 3000;
199
+ }
200
+ }
201
+ } else {
202
+ // Fallback if variablesPath not provided
203
+ if (baseAppPort === null || baseAppPort === undefined) {
204
+ const portMatch = envContent.match(/^PORT\s*=\s*(\d+)/m);
205
+ baseAppPort = portMatch ? parseInt(portMatch[1], 10) : 3000;
206
+ }
207
+ }
208
+
209
+ // Step 4: Apply developer-id adjustment: finalPort = basePort + (developerId * 100)
210
+ const appPort = devIdNum === 0 ? baseAppPort : (baseAppPort + (devIdNum * 100));
211
+
212
+ // Step 5: Get infra service ports from config and apply developer-id adjustment
213
+ // All infra ports (REDIS_PORT, DB_PORT, etc.) come from localEnv and get developer-id adjustment
214
+ const getInfraPort = (portKey, defaultValue) => {
215
+ let port = defaultValue;
216
+ if (localEnv[portKey] !== undefined && localEnv[portKey] !== null) {
217
+ const portVal = typeof localEnv[portKey] === 'number' ? localEnv[portKey] : parseInt(localEnv[portKey], 10);
218
+ if (!Number.isNaN(portVal)) {
219
+ port = portVal;
220
+ }
221
+ }
222
+ // Apply developer-id adjustment (infra ports are similar to docker)
223
+ return devIdNum === 0 ? port : (port + (devIdNum * 100));
224
+ };
225
+
226
+ // Get default ports from devConfig as last resort fallback
227
+ const basePorts = devConfig.getBasePorts();
228
+ const redisPort = getInfraPort('REDIS_PORT', basePorts.redis);
229
+ const dbPort = getInfraPort('DB_PORT', basePorts.postgres);
230
+
231
+ // Update .env content
232
+ let updated = envContent;
233
+
234
+ // Update PORT
235
+ if (/^PORT\s*=.*$/m.test(updated)) {
236
+ updated = updated.replace(/^PORT\s*=\s*.*$/m, `PORT=${appPort}`);
237
+ } else {
238
+ updated = `${updated}\nPORT=${appPort}\n`;
239
+ }
240
+
241
+ // Update DATABASE_PORT
242
+ if (/^DATABASE_PORT\s*=.*$/m.test(updated)) {
243
+ updated = updated.replace(/^DATABASE_PORT\s*=\s*.*$/m, `DATABASE_PORT=${dbPort}`);
244
+ }
245
+
246
+ // Update localhost URLs that point to the base app port to the dev-specific app port
247
+ const localhostUrlPattern = /(https?:\/\/localhost:)(\d+)(\b[^ \n]*)?/g;
248
+ updated = updated.replace(localhostUrlPattern, (match, prefix, portNum, rest = '') => {
249
+ const num = parseInt(portNum, 10);
250
+ if (num === baseAppPort) {
251
+ return `${prefix}${appPort}${rest || ''}`;
252
+ }
253
+ return match;
254
+ });
255
+
256
+ // Rewrite infra endpoints using env-config mapping for local context
257
+ // This handles REDIS_HOST, REDIS_PORT, REDIS_URL, DB_HOST, etc.
258
+ updated = await rewriteInfraEndpoints(updated, 'local', { redis: redisPort, postgres: dbPort });
259
+
260
+ return updated;
261
+ }
262
+
263
+ /**
264
+ * Read a YAML file and return parsed object
265
+ * @function readYamlAtPath
266
+ * @param {string} filePath - Absolute file path
267
+ * @returns {Object} Parsed YAML object
268
+ */
269
+ function readYamlAtPath(filePath) {
270
+ const content = fs.readFileSync(filePath, 'utf8');
271
+ return yaml.load(content);
272
+ }
273
+
274
+ /**
275
+ * Apply canonical secrets path override if configured and file exists
276
+ * @async
277
+ * @function applyCanonicalSecretsOverride
278
+ * @param {Object} currentSecrets - Current secrets map
279
+ * @returns {Promise<Object>} Possibly overridden secrets
280
+ */
281
+ async function applyCanonicalSecretsOverride(currentSecrets) {
282
+ let mergedSecrets = currentSecrets || {};
283
+ try {
284
+ const canonicalPath = await config.getSecretsPath();
285
+ if (canonicalPath) {
286
+ const resolvedCanonical = path.isAbsolute(canonicalPath)
287
+ ? canonicalPath
288
+ : path.resolve(process.cwd(), canonicalPath);
289
+ if (fs.existsSync(resolvedCanonical)) {
290
+ const configSecrets = readYamlAtPath(resolvedCanonical);
291
+ // Apply canonical secrets as a fallback source:
292
+ // - Do NOT override any existing keys from user/build
293
+ // - Add only missing keys from canonical path
294
+ if (configSecrets && typeof configSecrets === 'object') {
295
+ const result = { ...configSecrets, ...mergedSecrets };
296
+ mergedSecrets = result;
297
+ }
298
+ }
299
+ }
300
+ } catch {
301
+ // ignore and fall through
302
+ }
303
+ return mergedSecrets;
304
+ }
305
+
306
+ /**
307
+ * Ensure secrets map is non-empty or throw a friendly guidance error
308
+ * @function ensureNonEmptySecrets
309
+ * @param {Object} secrets - Secrets map
310
+ * @throws {Error} If secrets is empty
311
+ */
312
+ function ensureNonEmptySecrets(secrets) {
313
+ if (Object.keys(secrets || {}).length === 0) {
314
+ throw new Error('No secrets file found. Please create ~/.aifabrix/secrets.local.yaml or configure build.secrets in variables.yaml');
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Validate secrets against the env template, returning missing refs
320
+ * @function validateSecrets
321
+ * @param {string} envTemplate - Environment template content
322
+ * @param {Object} secrets - Available secrets
323
+ * @returns {Object} Validation result
324
+ */
325
+ function validateSecrets(envTemplate, secrets) {
326
+ const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
327
+ const missing = [];
328
+ let match;
329
+ while ((match = kvPattern.exec(envTemplate)) !== null) {
330
+ const secretKey = match[1];
331
+ if (!(secretKey in secrets)) {
332
+ missing.push(`kv://${secretKey}`);
333
+ }
334
+ }
335
+ return {
336
+ valid: missing.length === 0,
337
+ missing
338
+ };
339
+ }
340
+
341
+ module.exports = {
342
+ loadEnvConfig,
343
+ interpolateEnvVars,
344
+ collectMissingSecrets,
345
+ formatMissingSecretsFileInfo,
346
+ replaceKvInContent,
347
+ resolveServicePortsInEnvContent,
348
+ loadEnvTemplate,
349
+ processEnvVariables,
350
+ updateContainerPortInEnvFile,
351
+ adjustLocalEnvPortsInContent,
352
+ readYamlAtPath,
353
+ applyCanonicalSecretsOverride,
354
+ ensureNonEmptySecrets,
355
+ validateSecrets,
356
+ rewriteInfraEndpoints,
357
+ getEnvHosts
358
+ };
359
+
@@ -9,66 +9,42 @@
9
9
  * @version 2.0.0
10
10
  */
11
11
 
12
- const fs = require('fs');
13
12
  const path = require('path');
14
- const os = require('os');
15
- const yaml = require('js-yaml');
16
13
  const config = require('../config');
17
14
  const paths = require('./paths');
18
15
 
19
16
  /**
20
- * Resolves secrets file path (backward compatibility)
21
- * Checks common locations if path is not provided
17
+ * Resolves secrets file path when an explicit path is provided.
18
+ * If not provided, returns default fallback under <home>/secrets.yaml.
22
19
  * @function resolveSecretsPath
23
20
  * @param {string} [secretsPath] - Path to secrets file (optional)
24
21
  * @returns {string} Resolved secrets file path
25
22
  */
26
23
  function resolveSecretsPath(secretsPath) {
27
- let resolvedPath = secretsPath;
28
-
29
- if (!resolvedPath) {
30
- // Check common locations for secrets.local.yaml
31
- const commonLocations = [
32
- path.join(process.cwd(), '..', 'aifabrix-setup', 'secrets.local.yaml'),
33
- path.join(process.cwd(), '..', '..', 'aifabrix-setup', 'secrets.local.yaml'),
34
- path.join(process.cwd(), 'secrets.local.yaml'),
35
- path.join(process.cwd(), '..', 'secrets.local.yaml'),
36
- path.join(paths.getAifabrixHome(), 'secrets.yaml')
37
- ];
38
-
39
- // Find first existing file
40
- for (const location of commonLocations) {
41
- if (fs.existsSync(location)) {
42
- resolvedPath = location;
43
- break;
44
- }
45
- }
46
-
47
- // If none found, use default location
48
- if (!resolvedPath) {
49
- resolvedPath = path.join(paths.getAifabrixHome(), 'secrets.yaml');
50
- }
51
- } else if (secretsPath.startsWith('..')) {
52
- resolvedPath = path.resolve(process.cwd(), secretsPath);
24
+ if (secretsPath && secretsPath.startsWith('..')) {
25
+ return path.resolve(process.cwd(), secretsPath);
53
26
  }
54
-
55
- return resolvedPath;
27
+ if (secretsPath) {
28
+ return secretsPath;
29
+ }
30
+ // Default fallback
31
+ return path.join(paths.getAifabrixHome(), 'secrets.yaml');
56
32
  }
57
33
 
58
34
  /**
59
35
  * Determines the actual secrets file paths that loadSecrets would use
60
36
  * Mirrors the cascading lookup logic from loadSecrets
61
- * Checks config.yaml for general secrets-path as fallback
37
+ * Uses config.yaml for default secrets path as fallback
62
38
  *
63
39
  * @async
64
40
  * @function getActualSecretsPath
65
41
  * @param {string} [secretsPath] - Path to secrets file (optional)
66
- * @param {string} [appName] - Application name (optional, for variables.yaml lookup)
42
+ * @param {string} [_appName] - Application name (optional, for backward compatibility, unused)
67
43
  * @returns {Promise<Object>} Object with userPath and buildPath (if configured)
68
44
  * @returns {string} returns.userPath - User's secrets file path (~/.aifabrix/secrets.local.yaml)
69
- * @returns {string|null} returns.buildPath - App's build.secrets file path (if configured in variables.yaml or config.yaml)
45
+ * @returns {string|null} returns.buildPath - App's secrets file path (if configured in config.yaml)
70
46
  */
71
- async function getActualSecretsPath(secretsPath, appName) {
47
+ async function getActualSecretsPath(secretsPath, _appName) {
72
48
  // If explicit path provided, use it (backward compatibility)
73
49
  if (secretsPath) {
74
50
  const resolvedPath = resolveSecretsPath(secretsPath);
@@ -78,43 +54,20 @@ async function getActualSecretsPath(secretsPath, appName) {
78
54
  };
79
55
  }
80
56
 
81
- // Cascading lookup: user's file first
82
- const userSecretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.local.yaml');
57
+ // Cascading lookup: user's file first (under configured home)
58
+ const userSecretsPath = path.join(paths.getAifabrixHome(), 'secrets.local.yaml');
83
59
 
84
- // Check build.secrets from variables.yaml if appName provided
60
+ // Check config.yaml for canonical secrets path
85
61
  let buildSecretsPath = null;
86
- if (appName) {
87
- const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
88
- if (fs.existsSync(variablesPath)) {
89
- try {
90
- const variablesContent = fs.readFileSync(variablesPath, 'utf8');
91
- const variables = yaml.load(variablesContent);
92
-
93
- if (variables?.build?.secrets) {
94
- buildSecretsPath = path.resolve(
95
- path.dirname(variablesPath),
96
- variables.build.secrets
97
- );
98
- }
99
- } catch (error) {
100
- // Ignore errors, continue
101
- }
102
- }
103
- }
104
-
105
- // If no build.secrets found in variables.yaml, check config.yaml for general secrets-path
106
- if (!buildSecretsPath) {
107
- try {
108
- const generalSecretsPath = await config.getSecretsPath();
109
- if (generalSecretsPath) {
110
- // Resolve relative paths from current working directory
111
- buildSecretsPath = path.isAbsolute(generalSecretsPath)
112
- ? generalSecretsPath
113
- : path.resolve(process.cwd(), generalSecretsPath);
114
- }
115
- } catch (error) {
116
- // Ignore errors, continue
62
+ try {
63
+ const canonicalSecretsPath = await config.getAifabrixSecretsPath();
64
+ if (canonicalSecretsPath) {
65
+ buildSecretsPath = path.isAbsolute(canonicalSecretsPath)
66
+ ? canonicalSecretsPath
67
+ : path.resolve(process.cwd(), canonicalSecretsPath);
117
68
  }
69
+ } catch (error) {
70
+ // Ignore errors, continue
118
71
  }
119
72
 
120
73
  // Return both paths (even if files don't exist) for error messages
@@ -0,0 +1,38 @@
1
+ /**
2
+ * URL and service port resolution utilities
3
+ *
4
+ * @fileoverview Resolve ports in URLs using env-config and service maps (docker context)
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { buildHostnameToServiceMap, resolveUrlPort } = require('./secrets-utils');
12
+ const { loadEnvConfig } = require('./env-config-loader');
13
+
14
+ /**
15
+ * Resolve service ports inside URLs for docker environment (.env content)
16
+ * @async
17
+ * @function resolveServicePortsInEnvContent
18
+ * @param {string} envContent - .env content
19
+ * @param {string} environment - Environment name
20
+ * @returns {Promise<string>} Updated content
21
+ */
22
+ async function resolveServicePortsInEnvContent(envContent, environment) {
23
+ if (environment !== 'docker') {
24
+ return envContent;
25
+ }
26
+ const envConfig = await loadEnvConfig();
27
+ const dockerHosts = envConfig.environments.docker || {};
28
+ const hostnameToService = buildHostnameToServiceMap(dockerHosts);
29
+ const urlPattern = /(https?:\/\/)([a-zA-Z0-9-]+):(\d+)([^\s\n]*)?/g;
30
+ return envContent.replace(urlPattern, (match, protocol, hostname, port, urlPath = '') => {
31
+ return resolveUrlPort(protocol, hostname, port, urlPath || '', hostnameToService);
32
+ });
33
+ }
34
+
35
+ module.exports = {
36
+ resolveServicePortsInEnvContent
37
+ };
38
+
@@ -71,46 +71,6 @@ function loadUserSecrets() {
71
71
  }
72
72
  }
73
73
 
74
- /**
75
- * Loads build secrets from variables.yaml and merges with existing secrets
76
- * @async
77
- * @function loadBuildSecrets
78
- * @param {Object} mergedSecrets - Existing secrets to merge with
79
- * @param {string} appName - Application name
80
- * @returns {Promise<Object>} Merged secrets object
81
- */
82
- async function loadBuildSecrets(mergedSecrets, appName) {
83
- const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
84
- if (!fs.existsSync(variablesPath)) {
85
- return mergedSecrets;
86
- }
87
-
88
- try {
89
- const variablesContent = fs.readFileSync(variablesPath, 'utf8');
90
- const variables = yaml.load(variablesContent);
91
-
92
- if (variables?.build?.secrets) {
93
- const buildSecretsPath = path.resolve(
94
- path.dirname(variablesPath),
95
- variables.build.secrets
96
- );
97
-
98
- const buildSecrets = await loadSecretsFromFile(buildSecretsPath);
99
-
100
- // Merge: user's file takes priority, but use build.secrets for missing/empty values
101
- for (const [key, value] of Object.entries(buildSecrets)) {
102
- if (!(key in mergedSecrets) || !mergedSecrets[key] || mergedSecrets[key] === '') {
103
- mergedSecrets[key] = value;
104
- }
105
- }
106
- }
107
- } catch (error) {
108
- logger.warn(`Warning: Could not load build.secrets from variables.yaml: ${error.message}`);
109
- }
110
-
111
- return mergedSecrets;
112
- }
113
-
114
74
  /**
115
75
  * Loads default secrets from ~/.aifabrix/secrets.yaml
116
76
  * @function loadDefaultSecrets
@@ -198,7 +158,6 @@ function resolveUrlPort(protocol, hostname, port, urlPath, hostnameToService) {
198
158
  module.exports = {
199
159
  loadSecretsFromFile,
200
160
  loadUserSecrets,
201
- loadBuildSecrets,
202
161
  loadDefaultSecrets,
203
162
  buildHostnameToServiceMap,
204
163
  resolveUrlPort
@@ -142,9 +142,6 @@ function validateBuildConfig(build) {
142
142
  if (build.envOutputPath) {
143
143
  buildConfig.envOutputPath = build.envOutputPath;
144
144
  }
145
- if (build.secrets !== null && build.secrets !== undefined && build.secrets !== '') {
146
- buildConfig.secrets = build.secrets;
147
- }
148
145
  if (build.localPort) {
149
146
  buildConfig.localPort = build.localPort;
150
147
  }
@@ -176,12 +173,6 @@ function validateDeploymentConfig(deployment) {
176
173
  if (deployment.controllerUrl && deployment.controllerUrl.trim() !== '' && /^https:\/\/.*$/.test(deployment.controllerUrl)) {
177
174
  deploymentConfig.controllerUrl = deployment.controllerUrl;
178
175
  }
179
- if (deployment.clientId && deployment.clientId.trim() !== '' && /^[a-z0-9-]+$/.test(deployment.clientId)) {
180
- deploymentConfig.clientId = deployment.clientId;
181
- }
182
- if (deployment.clientSecret && deployment.clientSecret.trim() !== '' && /^(kv:\/\/.*|.+)$/.test(deployment.clientSecret)) {
183
- deploymentConfig.clientSecret = deployment.clientSecret;
184
- }
185
176
 
186
177
  return Object.keys(deploymentConfig).length > 0 ? deploymentConfig : null;
187
178
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.3.6",
3
+ "version": "2.5.0",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -47,7 +47,8 @@ docker logs aifabrix-{{appName}} -f
47
47
 
48
48
  **Stop:**
49
49
  ```bash
50
- docker stop aifabrix-{{appName}}
50
+ aifabrix down {{appName}}
51
+ # aifabrix down {{appName}} --volumes # also remove data volume
51
52
  ```
52
53
 
53
54
  ### 4. Deploy to Azure
@@ -86,6 +87,7 @@ aifabrix app rotate-secret {{appName}} --environment dev
86
87
  # Development
87
88
  aifabrix build {{appName}} # Build app
88
89
  aifabrix run {{appName}} # Run locally
90
+ aifabrix down {{appName}} [--volumes] # Stop app (optionally remove volume)
89
91
  aifabrix dockerfile {{appName}} --force # Generate Dockerfile
90
92
  aifabrix resolve {{appName}} # Generate .env file
91
93
 
@@ -148,11 +150,13 @@ aifabrix login --method credentials --app {{appName}} --environment dev
148
150
  aifabrix login --method credentials --app {{appName}} --client-id $CLIENT_ID --client-secret $CLIENT_SECRET --environment dev
149
151
  ```
150
152
 
151
- ### Environment Variables
153
+ ### Configuration
152
154
 
153
- ```bash
154
- export AIFABRIX_HOME=/custom/path
155
- export AIFABRIX_SECRETS=/path/to/secrets.yaml
155
+ Set overrides in `~/.aifabrix/config.yaml`:
156
+
157
+ ```yaml
158
+ aifabrix-home: "/custom/path"
159
+ aifabrix-secrets: "/path/to/secrets.yaml"
156
160
  ```
157
161
 
158
162
  ---
@@ -107,7 +107,7 @@ MOCK=true
107
107
  ENCRYPTION_KEY=kv://secrets-encryptionKeyVault
108
108
 
109
109
  # JWT Configuration (for client token generation)
110
- JWT_SECRET=kv://miso-controller-jwt-secretKeyVault
110
+ JWT_SECRET=kv://miso-controller-jwt-secretKeyVaultKeyVault
111
111
 
112
112
  # When API_KEY is set, a matching Bearer token bypasses OAuth2 validation
113
113
  API_KEY=kv://miso-controller-api-key-secretKeyVault
@@ -54,6 +54,8 @@ services:
54
54
  PGADMIN_CONFIG_SERVER_MODE: 'False'
55
55
  ports:
56
56
  - "5050:80"
57
+ volumes:
58
+ - pgadmin_data:/var/lib/pgadmin
57
59
  restart: unless-stopped
58
60
  depends_on:
59
61
  postgres:
@@ -84,6 +86,8 @@ volumes:
84
86
  driver: local
85
87
  redis_data:
86
88
  driver: local
89
+ pgadmin_data:
90
+ driver: local
87
91
 
88
92
  networks:
89
93
  infra_aifabrix-network: