@aifabrix/builder 2.3.5 → 2.4.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.
@@ -14,6 +14,7 @@
14
14
  const fs = require('fs').promises;
15
15
  const path = require('path');
16
16
  const os = require('os');
17
+ const paths = require('./utils/paths');
17
18
 
18
19
  // Audit log file path (in user's home directory for compliance)
19
20
  let auditLogPath = null;
@@ -28,8 +29,7 @@ async function getAuditLogPath() {
28
29
  return auditLogPath;
29
30
  }
30
31
 
31
- const homeDir = os.homedir();
32
- const aifabrixDir = path.join(homeDir, '.aifabrix');
32
+ const aifabrixDir = paths.getAifabrixHome();
33
33
 
34
34
  try {
35
35
  await fs.mkdir(aifabrixDir, { recursive: true });
package/lib/build.js CHANGED
@@ -13,7 +13,7 @@
13
13
  const fs = require('fs').promises;
14
14
  const fsSync = require('fs');
15
15
  const path = require('path');
16
- const os = require('os');
16
+ const paths = require('./utils/paths');
17
17
  const { exec } = require('child_process');
18
18
  const { promisify } = require('util');
19
19
  const chalk = require('chalk');
@@ -198,7 +198,7 @@ async function generateDockerfile(appNameOrPath, language, config, buildConfig =
198
198
  if (devDir) {
199
199
  targetDir = devDir;
200
200
  } else {
201
- targetDir = path.join(os.homedir(), '.aifabrix', appName);
201
+ targetDir = path.join(paths.getAifabrixHome(), appName);
202
202
  }
203
203
 
204
204
  if (!fsSync.existsSync(targetDir)) {
package/lib/config.js CHANGED
@@ -11,12 +11,19 @@
11
11
 
12
12
  const fs = require('fs').promises;
13
13
  const path = require('path');
14
- const os = require('os');
15
14
  const yaml = require('js-yaml');
15
+ const os = require('os');
16
+ // Avoid importing paths here to prevent circular dependency.
17
+ // Config location is always under OS home at ~/.aifabrix/config.yaml
16
18
 
19
+ // Default (for tests and constants): always reflects OS home
17
20
  const CONFIG_DIR = path.join(os.homedir(), '.aifabrix');
18
21
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.yaml');
19
22
 
23
+ // Runtime config directory (always under OS home)
24
+ const RUNTIME_CONFIG_DIR = path.join(os.homedir(), '.aifabrix');
25
+ const RUNTIME_CONFIG_FILE = path.join(RUNTIME_CONFIG_DIR, 'config.yaml');
26
+
20
27
  // Cache for developer ID - loaded when getConfig() is first called
21
28
  let cachedDeveloperId = null;
22
29
 
@@ -27,7 +34,7 @@ let cachedDeveloperId = null;
27
34
  */
28
35
  async function getConfig() {
29
36
  try {
30
- const configContent = await fs.readFile(CONFIG_FILE, 'utf8');
37
+ const configContent = await fs.readFile(RUNTIME_CONFIG_FILE, 'utf8');
31
38
  let config = yaml.load(configContent);
32
39
 
33
40
  // Handle empty file or null/undefined result from yaml.load
@@ -91,19 +98,19 @@ async function getConfig() {
91
98
  async function saveConfig(data) {
92
99
  try {
93
100
  // Create directory if it doesn't exist
94
- await fs.mkdir(CONFIG_DIR, { recursive: true });
101
+ await fs.mkdir(RUNTIME_CONFIG_DIR, { recursive: true });
95
102
 
96
103
  // Set secure permissions
97
104
  // Force quotes to ensure numeric-like strings (e.g., "01") remain strings in YAML
98
105
  const configContent = yaml.dump(data, { forceQuotes: true });
99
106
  // Write file first
100
- await fs.writeFile(CONFIG_FILE, configContent, {
107
+ await fs.writeFile(RUNTIME_CONFIG_FILE, configContent, {
101
108
  mode: 0o600,
102
109
  flag: 'w'
103
110
  });
104
111
  // Open file descriptor and fsync to ensure write is flushed to disk
105
112
  // This is critical on Windows where file writes may be cached
106
- const fd = await fs.open(CONFIG_FILE, 'r+');
113
+ const fd = await fs.open(RUNTIME_CONFIG_FILE, 'r+');
107
114
  try {
108
115
  await fd.sync();
109
116
  } finally {
@@ -120,7 +127,7 @@ async function saveConfig(data) {
120
127
  */
121
128
  async function clearConfig() {
122
129
  try {
123
- await fs.unlink(CONFIG_FILE);
130
+ await fs.unlink(RUNTIME_CONFIG_FILE);
124
131
  } catch (error) {
125
132
  if (error.code !== 'ENOENT') {
126
133
  throw new Error(`Failed to clear config: ${error.message}`);
@@ -179,7 +186,7 @@ async function setDeveloperId(developerId) {
179
186
  // Add a small delay to ensure file system has flushed the write
180
187
  await new Promise(resolve => setTimeout(resolve, 100));
181
188
  // Read file again with fresh file handle to avoid OS caching
182
- const savedContent = await fs.readFile(CONFIG_FILE, 'utf8');
189
+ const savedContent = await fs.readFile(RUNTIME_CONFIG_FILE, 'utf8');
183
190
  const savedConfig = yaml.load(savedContent);
184
191
  // YAML may parse numbers as numbers, so convert to string for comparison
185
192
  const savedDevIdString = String(savedConfig['developer-id']);
@@ -362,7 +369,8 @@ async function setSecretsEncryptionKey(key) {
362
369
  */
363
370
  async function getSecretsPath() {
364
371
  const config = await getConfig();
365
- return config['secrets-path'] || null;
372
+ // Backward compatibility: prefer new key, fallback to legacy
373
+ return config['aifabrix-secrets'] || config['secrets-path'] || null;
366
374
  }
367
375
 
368
376
  /**
@@ -376,7 +384,54 @@ async function setSecretsPath(secretsPath) {
376
384
  }
377
385
 
378
386
  const config = await getConfig();
379
- config['secrets-path'] = secretsPath;
387
+ // Store under new canonical key
388
+ config['aifabrix-secrets'] = secretsPath;
389
+ await saveConfig(config);
390
+ }
391
+
392
+ /**
393
+ * Get aifabrix-home override from configuration
394
+ * @returns {Promise<string|null>} Home override path or null if not set
395
+ */
396
+ async function getAifabrixHomeOverride() {
397
+ const config = await getConfig();
398
+ return config['aifabrix-home'] || null;
399
+ }
400
+
401
+ /**
402
+ * Set aifabrix-home override in configuration
403
+ * @param {string} homePath - Base directory path for AI Fabrix files
404
+ * @returns {Promise<void>}
405
+ */
406
+ async function setAifabrixHomeOverride(homePath) {
407
+ if (!homePath || typeof homePath !== 'string') {
408
+ throw new Error('Home path is required and must be a string');
409
+ }
410
+ const config = await getConfig();
411
+ config['aifabrix-home'] = homePath;
412
+ await saveConfig(config);
413
+ }
414
+
415
+ /**
416
+ * Get aifabrix-secrets path from configuration (canonical)
417
+ * @returns {Promise<string|null>} Secrets path or null if not set
418
+ */
419
+ async function getAifabrixSecretsPath() {
420
+ const config = await getConfig();
421
+ return config['aifabrix-secrets'] || null;
422
+ }
423
+
424
+ /**
425
+ * Set aifabrix-secrets path in configuration (canonical)
426
+ * @param {string} secretsPath - Path to default secrets file
427
+ * @returns {Promise<void>}
428
+ */
429
+ async function setAifabrixSecretsPath(secretsPath) {
430
+ if (!secretsPath || typeof secretsPath !== 'string') {
431
+ throw new Error('Secrets path is required and must be a string');
432
+ }
433
+ const config = await getConfig();
434
+ config['aifabrix-secrets'] = secretsPath;
380
435
  await saveConfig(config);
381
436
  }
382
437
 
@@ -399,6 +454,10 @@ const exportsObj = {
399
454
  setSecretsEncryptionKey,
400
455
  getSecretsPath,
401
456
  setSecretsPath,
457
+ getAifabrixHomeOverride,
458
+ setAifabrixHomeOverride,
459
+ getAifabrixSecretsPath,
460
+ setAifabrixSecretsPath,
402
461
  CONFIG_DIR,
403
462
  CONFIG_FILE
404
463
  };
package/lib/infra.js CHANGED
@@ -13,7 +13,6 @@ const { exec } = require('child_process');
13
13
  const { promisify } = require('util');
14
14
  const path = require('path');
15
15
  const fs = require('fs');
16
- const os = require('os');
17
16
  const handlebars = require('handlebars');
18
17
  const secrets = require('./secrets');
19
18
  const config = require('./config');
@@ -21,6 +20,7 @@ const devConfig = require('./utils/dev-config');
21
20
  const logger = require('./utils/logger');
22
21
  const containerUtils = require('./utils/infra-containers');
23
22
  const dockerUtils = require('./utils/docker');
23
+ const paths = require('./utils/paths');
24
24
 
25
25
  // Register Handlebars helper for equality check
26
26
  handlebars.registerHelper('eq', (a, b) => a === b);
@@ -84,7 +84,7 @@ async function checkDockerAvailability() {
84
84
  }
85
85
 
86
86
  async function ensureAdminSecrets() {
87
- const adminSecretsPath = path.join(os.homedir(), '.aifabrix', 'admin-secrets.env');
87
+ const adminSecretsPath = path.join(paths.getAifabrixHome(), 'admin-secrets.env');
88
88
  if (!fs.existsSync(adminSecretsPath)) {
89
89
  logger.log('Generating admin-secrets.env...');
90
90
  await secrets.generateAdminSecretsEnv();
@@ -109,8 +109,8 @@ async function startInfra(developerId = null) {
109
109
  throw new Error(`Compose template not found: ${templatePath}`);
110
110
  }
111
111
 
112
- // Create infra directory in ~/.aifabrix with dev ID
113
- const aifabrixDir = path.join(os.homedir(), '.aifabrix');
112
+ // Create infra directory in AIFABRIX_HOME with dev ID
113
+ const aifabrixDir = paths.getAifabrixHome();
114
114
  const infraDirName = getInfraDirName(devId);
115
115
  const infraDir = path.join(aifabrixDir, infraDirName);
116
116
  if (!fs.existsSync(infraDir)) {
@@ -164,7 +164,7 @@ async function startInfra(developerId = null) {
164
164
  */
165
165
  async function stopInfra() {
166
166
  const devId = await config.getDeveloperId();
167
- const aifabrixDir = path.join(os.homedir(), '.aifabrix');
167
+ const aifabrixDir = paths.getAifabrixHome();
168
168
  const infraDirName = getInfraDirName(devId);
169
169
  const infraDir = path.join(aifabrixDir, infraDirName);
170
170
  const composePath = path.join(infraDir, 'compose.yaml');
@@ -201,7 +201,7 @@ async function stopInfra() {
201
201
  */
202
202
  async function stopInfraWithVolumes() {
203
203
  const devId = await config.getDeveloperId();
204
- const aifabrixDir = path.join(os.homedir(), '.aifabrix');
204
+ const aifabrixDir = paths.getAifabrixHome();
205
205
  const infraDirName = getInfraDirName(devId);
206
206
  const infraDir = path.join(aifabrixDir, infraDirName);
207
207
  const composePath = path.join(infraDir, 'compose.yaml');
@@ -337,7 +337,7 @@ async function restartService(serviceName) {
337
337
  }
338
338
 
339
339
  const devId = await config.getDeveloperId();
340
- const aifabrixDir = path.join(os.homedir(), '.aifabrix');
340
+ const aifabrixDir = paths.getAifabrixHome();
341
341
  const infraDirName = getInfraDirName(devId);
342
342
  const infraDir = path.join(aifabrixDir, infraDirName);
343
343
  const composePath = path.join(infraDir, 'compose.yaml');
package/lib/secrets.js CHANGED
@@ -12,7 +12,6 @@
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
14
  const yaml = require('js-yaml');
15
- const os = require('os');
16
15
  const chalk = require('chalk');
17
16
  const logger = require('./utils/logger');
18
17
  const config = require('./config');
@@ -33,6 +32,7 @@ const {
33
32
  resolveUrlPort
34
33
  } = require('./utils/secrets-utils');
35
34
  const { decryptSecret, isEncrypted } = require('./utils/secrets-encryption');
35
+ const pathsUtil = require('./utils/paths');
36
36
 
37
37
  /**
38
38
  * Loads environment configuration for docker/local context
@@ -414,7 +414,7 @@ async function generateAdminSecretsEnv(secretsPath) {
414
414
  secrets = await loadSecrets(secretsPath);
415
415
  } catch (error) {
416
416
  // If secrets file doesn't exist, create default secrets
417
- const defaultSecretsPath = secretsPath || path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
417
+ const defaultSecretsPath = secretsPath || path.join(pathsUtil.getAifabrixHome(), 'secrets.yaml');
418
418
 
419
419
  if (!fs.existsSync(defaultSecretsPath)) {
420
420
  logger.log('Creating default secrets file...');
@@ -425,7 +425,7 @@ async function generateAdminSecretsEnv(secretsPath) {
425
425
  }
426
426
  }
427
427
 
428
- const aifabrixDir = path.join(os.homedir(), '.aifabrix');
428
+ const aifabrixDir = pathsUtil.getAifabrixHome();
429
429
  const adminEnvPath = path.join(aifabrixDir, 'admin-secrets.env');
430
430
 
431
431
  if (!fs.existsSync(aifabrixDir)) {
@@ -13,7 +13,7 @@
13
13
  const fs = require('fs').promises;
14
14
  const fsSync = require('fs');
15
15
  const path = require('path');
16
- const os = require('os');
16
+ const paths = require('./paths');
17
17
 
18
18
  /**
19
19
  * Copies all files from builder directory to developer-specific directory
@@ -40,9 +40,7 @@ async function copyBuilderToDevDirectory(appName, developerId) {
40
40
 
41
41
  // Get base directory (applications or applications-dev-{id})
42
42
  const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
43
- const baseDir = idNum === 0
44
- ? path.join(os.homedir(), '.aifabrix', 'applications')
45
- : path.join(os.homedir(), '.aifabrix', `applications-dev-${developerId}`);
43
+ const baseDir = paths.getApplicationsBaseDir(idNum);
46
44
 
47
45
  // Clear base directory before copying (delete all files)
48
46
  if (fsSync.existsSync(baseDir)) {
@@ -117,13 +115,7 @@ async function copyDirectory(sourceDir, targetDir) {
117
115
  * @returns {string} Path to developer-specific directory
118
116
  */
119
117
  function getDevDirectory(appName, developerId) {
120
- const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
121
- if (idNum === 0) {
122
- // Dev 0: all apps go directly to applications/ (no subdirectory)
123
- return path.join(os.homedir(), '.aifabrix', 'applications');
124
- }
125
- // Dev > 0: apps go to applications-dev-{id}/{appName}-dev-{id}/
126
- return path.join(os.homedir(), '.aifabrix', `applications-dev-${developerId}`, `${appName}-dev-${developerId}`);
118
+ return paths.getDevDirectory(appName, developerId);
127
119
 
128
120
  }
129
121
 
@@ -49,7 +49,7 @@ async function getComposeCommand() {
49
49
  await execAsync('docker-compose --version');
50
50
  return 'docker-compose';
51
51
  } catch (_) {
52
- throw new Error('Docker Compose is not available (neither "docker compose" nor "docker-compose" found).');
52
+ throw new Error('Docker Compose is not available (neither "docker compose" nor "docker-compose" found). Install the Docker Compose v2 plugin (e.g., apt-get install docker-compose-plugin) or ensure docker-compose is on PATH.');
53
53
  }
54
54
  }
55
55
 
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Path Utilities for AI Fabrix Builder
3
+ *
4
+ * Centralized helpers for resolving filesystem locations with support for
5
+ * AIFABRIX_HOME override. Defaults to ~/.aifabrix when not specified.
6
+ *
7
+ * @fileoverview Path resolution utilities with environment overrides
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const path = require('path');
15
+ const os = require('os');
16
+ const fs = require('fs');
17
+ const yaml = require('js-yaml');
18
+
19
+ function safeHomedir() {
20
+ try {
21
+ if (typeof os.homedir === 'function') {
22
+ const hd = os.homedir();
23
+ if (typeof hd === 'string' && hd.length > 0) {
24
+ return hd;
25
+ }
26
+ }
27
+ } catch {
28
+ // ignore
29
+ }
30
+ return process.env.HOME || process.env.USERPROFILE || '/';
31
+ }
32
+
33
+ /**
34
+ * Returns the base AI Fabrix directory.
35
+ * Resolved from config.yaml `aifabrix-home` (stored under OS home).
36
+ * Falls back to ~/.aifabrix when not specified.
37
+ *
38
+ * @returns {string} Absolute path to the AI Fabrix home directory
39
+ */
40
+ function getAifabrixHome() {
41
+ try {
42
+ const configPath = path.join(safeHomedir(), '.aifabrix', 'config.yaml');
43
+ if (fs.existsSync(configPath)) {
44
+ const content = fs.readFileSync(configPath, 'utf8');
45
+ const config = yaml.load(content) || {};
46
+ const homeOverride = config && typeof config['aifabrix-home'] === 'string' ? config['aifabrix-home'].trim() : '';
47
+ if (homeOverride) {
48
+ return path.resolve(homeOverride);
49
+ }
50
+ }
51
+ } catch {
52
+ // Ignore errors and fall back to default
53
+ }
54
+ return path.join(safeHomedir(), '.aifabrix');
55
+ }
56
+
57
+ /**
58
+ * Returns the applications base directory for a developer.
59
+ * Dev 0: <home>/applications
60
+ * Dev > 0: <home>/applications-dev-{id}
61
+ *
62
+ * @param {number|string} developerId - Developer ID
63
+ * @returns {string} Absolute path to applications base directory
64
+ */
65
+ function getApplicationsBaseDir(developerId) {
66
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
67
+ const base = getAifabrixHome();
68
+ if (idNum === 0) {
69
+ return path.join(base, 'applications');
70
+ }
71
+ return path.join(base, `applications-dev-${developerId}`);
72
+ }
73
+
74
+ /**
75
+ * Returns the developer-specific application directory.
76
+ * Dev 0: points to applications/ (no app subdirectory)
77
+ * Dev > 0: <home>/applications-dev-{id}/{appName}-dev-{id}
78
+ *
79
+ * @param {string} appName - Application name
80
+ * @param {number|string} developerId - Developer ID
81
+ * @returns {string} Absolute path to developer-specific app directory
82
+ */
83
+ function getDevDirectory(appName, developerId) {
84
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
85
+ const baseDir = getApplicationsBaseDir(developerId);
86
+ if (idNum === 0) {
87
+ return baseDir;
88
+ }
89
+ return path.join(baseDir, `${appName}-dev-${developerId}`);
90
+ }
91
+
92
+ module.exports = {
93
+ getAifabrixHome,
94
+ getApplicationsBaseDir,
95
+ getDevDirectory
96
+ };
97
+
@@ -11,53 +11,32 @@
11
11
 
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
- const os = require('os');
15
14
  const yaml = require('js-yaml');
16
15
  const config = require('../config');
16
+ const paths = require('./paths');
17
17
 
18
18
  /**
19
- * Resolves secrets file path (backward compatibility)
20
- * Checks common locations if path is not provided
19
+ * Resolves secrets file path when an explicit path is provided.
20
+ * If not provided, returns default fallback under <home>/secrets.yaml.
21
21
  * @function resolveSecretsPath
22
22
  * @param {string} [secretsPath] - Path to secrets file (optional)
23
23
  * @returns {string} Resolved secrets file path
24
24
  */
25
25
  function resolveSecretsPath(secretsPath) {
26
- let resolvedPath = secretsPath;
27
-
28
- if (!resolvedPath) {
29
- // Check common locations for secrets.local.yaml
30
- const commonLocations = [
31
- path.join(process.cwd(), '..', 'aifabrix-setup', 'secrets.local.yaml'),
32
- path.join(process.cwd(), '..', '..', 'aifabrix-setup', 'secrets.local.yaml'),
33
- path.join(process.cwd(), 'secrets.local.yaml'),
34
- path.join(process.cwd(), '..', 'secrets.local.yaml'),
35
- path.join(os.homedir(), '.aifabrix', 'secrets.yaml')
36
- ];
37
-
38
- // Find first existing file
39
- for (const location of commonLocations) {
40
- if (fs.existsSync(location)) {
41
- resolvedPath = location;
42
- break;
43
- }
44
- }
45
-
46
- // If none found, use default location
47
- if (!resolvedPath) {
48
- resolvedPath = path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
49
- }
50
- } else if (secretsPath.startsWith('..')) {
51
- resolvedPath = path.resolve(process.cwd(), secretsPath);
26
+ if (secretsPath && secretsPath.startsWith('..')) {
27
+ return path.resolve(process.cwd(), secretsPath);
52
28
  }
53
-
54
- return resolvedPath;
29
+ if (secretsPath) {
30
+ return secretsPath;
31
+ }
32
+ // Default fallback
33
+ return path.join(paths.getAifabrixHome(), 'secrets.yaml');
55
34
  }
56
35
 
57
36
  /**
58
37
  * Determines the actual secrets file paths that loadSecrets would use
59
38
  * Mirrors the cascading lookup logic from loadSecrets
60
- * Checks config.yaml for general secrets-path as fallback
39
+ * Uses config.yaml for default secrets path as fallback
61
40
  *
62
41
  * @async
63
42
  * @function getActualSecretsPath
@@ -77,8 +56,8 @@ async function getActualSecretsPath(secretsPath, appName) {
77
56
  };
78
57
  }
79
58
 
80
- // Cascading lookup: user's file first
81
- const userSecretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.local.yaml');
59
+ // Cascading lookup: user's file first (under configured home)
60
+ const userSecretsPath = path.join(paths.getAifabrixHome(), 'secrets.local.yaml');
82
61
 
83
62
  // Check build.secrets from variables.yaml if appName provided
84
63
  let buildSecretsPath = null;
@@ -101,15 +80,14 @@ async function getActualSecretsPath(secretsPath, appName) {
101
80
  }
102
81
  }
103
82
 
104
- // If no build.secrets found in variables.yaml, check config.yaml for general secrets-path
83
+ // If no build.secrets found in variables.yaml, check config.yaml for canonical secrets path
105
84
  if (!buildSecretsPath) {
106
85
  try {
107
- const generalSecretsPath = await config.getSecretsPath();
108
- if (generalSecretsPath) {
109
- // Resolve relative paths from current working directory
110
- buildSecretsPath = path.isAbsolute(generalSecretsPath)
111
- ? generalSecretsPath
112
- : path.resolve(process.cwd(), generalSecretsPath);
86
+ const canonicalSecretsPath = await config.getAifabrixSecretsPath();
87
+ if (canonicalSecretsPath) {
88
+ buildSecretsPath = path.isAbsolute(canonicalSecretsPath)
89
+ ? canonicalSecretsPath
90
+ : path.resolve(process.cwd(), canonicalSecretsPath);
113
91
  }
114
92
  } catch (error) {
115
93
  // Ignore errors, continue
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.3.5",
3
+ "version": "2.4.0",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -148,11 +148,13 @@ aifabrix login --method credentials --app {{appName}} --environment dev
148
148
  aifabrix login --method credentials --app {{appName}} --client-id $CLIENT_ID --client-secret $CLIENT_SECRET --environment dev
149
149
  ```
150
150
 
151
- ### Environment Variables
151
+ ### Configuration
152
152
 
153
- ```bash
154
- export AIFABRIX_HOME=/custom/path
155
- export AIFABRIX_SECRETS=/path/to/secrets.yaml
153
+ Set overrides in `~/.aifabrix/config.yaml`:
154
+
155
+ ```yaml
156
+ aifabrix-home: "/custom/path"
157
+ aifabrix-secrets: "/path/to/secrets.yaml"
156
158
  ```
157
159
 
158
160
  ---