@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.
Files changed (38) hide show
  1. package/README.md +3 -0
  2. package/lib/app-down.js +123 -0
  3. package/lib/app.js +4 -2
  4. package/lib/build.js +19 -13
  5. package/lib/cli.js +52 -9
  6. package/lib/commands/secure.js +5 -40
  7. package/lib/config.js +26 -4
  8. package/lib/env-reader.js +3 -2
  9. package/lib/generator.js +0 -9
  10. package/lib/infra.js +30 -3
  11. package/lib/schema/application-schema.json +0 -15
  12. package/lib/schema/env-config.yaml +8 -8
  13. package/lib/secrets.js +167 -253
  14. package/lib/templates.js +10 -18
  15. package/lib/utils/api-error-handler.js +182 -147
  16. package/lib/utils/api.js +144 -354
  17. package/lib/utils/build-copy.js +6 -13
  18. package/lib/utils/compose-generator.js +2 -1
  19. package/lib/utils/device-code.js +349 -0
  20. package/lib/utils/env-config-loader.js +102 -0
  21. package/lib/utils/env-copy.js +131 -0
  22. package/lib/utils/env-endpoints.js +209 -0
  23. package/lib/utils/env-map.js +116 -0
  24. package/lib/utils/env-ports.js +60 -0
  25. package/lib/utils/environment-checker.js +39 -6
  26. package/lib/utils/image-name.js +49 -0
  27. package/lib/utils/paths.js +22 -20
  28. package/lib/utils/secrets-generator.js +3 -3
  29. package/lib/utils/secrets-helpers.js +359 -0
  30. package/lib/utils/secrets-path.js +12 -36
  31. package/lib/utils/secrets-url.js +38 -0
  32. package/lib/utils/secrets-utils.js +1 -42
  33. package/lib/utils/variable-transformer.js +0 -9
  34. package/package.json +1 -1
  35. package/templates/applications/README.md.hbs +4 -2
  36. package/templates/applications/miso-controller/env.template +1 -1
  37. package/templates/infra/compose.yaml +4 -0
  38. package/templates/infra/compose.yaml.hbs +9 -4
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Environment endpoint rewriting utilities
3
+ *
4
+ * @fileoverview Rewrites infra endpoints using env-config and developer offsets
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 devConfig = require('../utils/dev-config');
16
+ const { loadEnvConfig } = require('./env-config-loader');
17
+
18
+ /**
19
+ * Returns the environment hosts mapping from env-config.yaml
20
+ * @async
21
+ * @param {'docker'|'local'} context
22
+ * @returns {Promise<Object>}
23
+ */
24
+ async function getEnvHosts(context) {
25
+ const envCfg = await loadEnvConfig();
26
+ const envs = envCfg && envCfg.environments ? envCfg.environments : {};
27
+ return envs[context] || {};
28
+ }
29
+
30
+ /**
31
+ * Rewrites infra endpoints (REDIS_URL/REDIS_HOST/REDIS_PORT, DB_HOST/DB_PORT, etc.) based on env-config and context
32
+ * Uses getEnvHosts() to get all service values dynamically, avoiding hardcoded values
33
+ * @async
34
+ * @function rewriteInfraEndpoints
35
+ * @param {string} envContent - .env file content
36
+ * @param {'docker'|'local'} context - Environment context
37
+ * @param {{redis:number, postgres:number}} [devPorts] - Ports object with developer-id adjusted ports (optional)
38
+ * @returns {Promise<string>} Updated content
39
+ */
40
+ async function rewriteInfraEndpoints(envContent, context, devPorts) {
41
+ // Get all service values from config system (includes env-config.yaml + user env-config file)
42
+ let hosts = await getEnvHosts(context);
43
+
44
+ // Apply config.yaml → environments.{context} override (if exists)
45
+ try {
46
+ const os = require('os');
47
+ const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
48
+ if (fs.existsSync(cfgPath)) {
49
+ const cfgContent = fs.readFileSync(cfgPath, 'utf8');
50
+ const cfg = yaml.load(cfgContent) || {};
51
+ if (cfg && cfg.environments && cfg.environments[context]) {
52
+ hosts = { ...hosts, ...cfg.environments[context] };
53
+ }
54
+ }
55
+ } catch {
56
+ // Ignore config.yaml read errors, continue with env-config values
57
+ }
58
+
59
+ // Helper to split host:port values
60
+ const splitHost = (value) => {
61
+ if (typeof value !== 'string') return { host: undefined, port: undefined };
62
+ const m = value.match(/^([^:]+):(\d+)$/);
63
+ if (m) return { host: m[1], port: parseInt(m[2], 10) };
64
+ return { host: value, port: undefined };
65
+ };
66
+
67
+ // Get aifabrix-localhost override for local context
68
+ let localhostOverride = null;
69
+ if (context === 'local') {
70
+ try {
71
+ const os = require('os');
72
+ const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
73
+ if (fs.existsSync(cfgPath)) {
74
+ const cfgContent = fs.readFileSync(cfgPath, 'utf8');
75
+ const cfg = yaml.load(cfgContent) || {};
76
+ if (typeof cfg['aifabrix-localhost'] === 'string' && cfg['aifabrix-localhost'].trim().length > 0) {
77
+ localhostOverride = cfg['aifabrix-localhost'].trim();
78
+ }
79
+ }
80
+ } catch {
81
+ // ignore override errors
82
+ }
83
+ }
84
+
85
+ // Helper to get port value from config or devPorts, with developer-id adjustment
86
+ const getServicePort = async(portKey, serviceName) => {
87
+ // If devPorts provided, use it (already has developer-id adjustment)
88
+ if (devPorts && typeof devPorts[serviceName] === 'number') {
89
+ return devPorts[serviceName];
90
+ }
91
+
92
+ // Get base port from config
93
+ let basePort = null;
94
+ if (hosts[portKey] !== undefined && hosts[portKey] !== null) {
95
+ const portVal = typeof hosts[portKey] === 'number' ? hosts[portKey] : parseInt(hosts[portKey], 10);
96
+ if (!Number.isNaN(portVal)) {
97
+ basePort = portVal;
98
+ }
99
+ }
100
+
101
+ // Last resort fallback to devConfig (only if not in config)
102
+ if (basePort === null || basePort === undefined) {
103
+ const basePorts = devConfig.getBasePorts();
104
+ basePort = basePorts[serviceName];
105
+ }
106
+
107
+ // Apply developer-id adjustment
108
+ try {
109
+ const devId = await config.getDeveloperId();
110
+ let devIdNum = 0;
111
+ if (devId !== null && devId !== undefined) {
112
+ const parsed = parseInt(devId, 10);
113
+ if (!Number.isNaN(parsed)) {
114
+ devIdNum = parsed;
115
+ }
116
+ }
117
+ return devIdNum === 0 ? basePort : (basePort + (devIdNum * 100));
118
+ } catch {
119
+ return basePort;
120
+ }
121
+ };
122
+
123
+ // Get Redis configuration
124
+ const redisParts = splitHost(hosts.REDIS_HOST);
125
+ let redisHost = redisParts.host || hosts.REDIS_HOST;
126
+ // Fallback to default if not in config
127
+ if (!redisHost) {
128
+ redisHost = context === 'docker' ? 'redis' : 'localhost';
129
+ }
130
+ if (context === 'local' && localhostOverride && redisHost === 'localhost') {
131
+ redisHost = localhostOverride;
132
+ }
133
+ const redisPort = await getServicePort('REDIS_PORT', 'redis');
134
+
135
+ // Get DB configuration
136
+ let dbHost = hosts.DB_HOST;
137
+ // Fallback to default if not in config
138
+ if (!dbHost) {
139
+ dbHost = context === 'docker' ? 'postgres' : 'localhost';
140
+ }
141
+ const finalDbHost = (context === 'local' && localhostOverride && dbHost === 'localhost') ? localhostOverride : dbHost;
142
+ const dbPort = await getServicePort('DB_PORT', 'postgres');
143
+
144
+ let updated = envContent;
145
+
146
+ // Update REDIS_URL if present
147
+ if (/^REDIS_URL\s*=.*$/m.test(updated)) {
148
+ const m = updated.match(/^REDIS_URL\s*=\s*redis:\/\/([^:\s]+):\d+/m);
149
+ const currentHost = m && m[1] ? m[1] : null;
150
+ const targetHost = redisHost || currentHost;
151
+ if (targetHost) {
152
+ updated = updated.replace(
153
+ /^REDIS_URL\s*=\s*.*$/m,
154
+ `REDIS_URL=redis://${targetHost}:${redisPort}`
155
+ );
156
+ }
157
+ }
158
+
159
+ // Update REDIS_HOST if present
160
+ if (/^REDIS_HOST\s*=.*$/m.test(updated)) {
161
+ const hostPortMatch = updated.match(/^REDIS_HOST\s*=\s*([a-zA-Z0-9_.-]+):\d+$/m);
162
+ const hasPortPattern = !!hostPortMatch;
163
+ if (hasPortPattern) {
164
+ updated = updated.replace(
165
+ /^REDIS_HOST\s*=\s*.*$/m,
166
+ `REDIS_HOST=${redisHost}:${redisPort}`
167
+ );
168
+ } else {
169
+ updated = updated.replace(/^REDIS_HOST\s*=\s*.*$/m, `REDIS_HOST=${redisHost}`);
170
+ }
171
+ }
172
+
173
+ // Update REDIS_PORT if present
174
+ if (/^REDIS_PORT\s*=.*$/m.test(updated)) {
175
+ updated = updated.replace(
176
+ /^REDIS_PORT\s*=\s*.*$/m,
177
+ `REDIS_PORT=${redisPort}`
178
+ );
179
+ }
180
+
181
+ // Update DB_HOST if present
182
+ if (/^DB_HOST\s*=.*$/m.test(updated)) {
183
+ updated = updated.replace(/^DB_HOST\s*=\s*.*$/m, `DB_HOST=${finalDbHost}`);
184
+ }
185
+
186
+ // Update DB_PORT if present
187
+ if (/^DB_PORT\s*=.*$/m.test(updated)) {
188
+ updated = updated.replace(
189
+ /^DB_PORT\s*=\s*.*$/m,
190
+ `DB_PORT=${dbPort}`
191
+ );
192
+ }
193
+
194
+ // Update DATABASE_PORT if present (some templates use DATABASE_PORT instead of DB_PORT)
195
+ if (/^DATABASE_PORT\s*=.*$/m.test(updated)) {
196
+ updated = updated.replace(
197
+ /^DATABASE_PORT\s*=\s*.*$/m,
198
+ `DATABASE_PORT=${dbPort}`
199
+ );
200
+ }
201
+
202
+ return updated;
203
+ }
204
+
205
+ module.exports = {
206
+ rewriteInfraEndpoints,
207
+ getEnvHosts
208
+ };
209
+
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Environment variable mapping utilities
3
+ *
4
+ * @fileoverview Build interpolation map from env-config.yaml and config overrides
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 { loadEnvConfig } = require('./env-config-loader');
15
+
16
+ /**
17
+ * Build environment variable map for interpolation based on env-config.yaml
18
+ * - Supports values like "host:port" by splitting into *_HOST (host) and *_PORT (port)
19
+ * - Merges overrides from ~/.aifabrix/config.yaml under environments.{env}
20
+ * - Applies aifabrix-localhost override for local context if configured
21
+ * @async
22
+ * @function buildEnvVarMap
23
+ * @param {'docker'|'local'} context - Environment context
24
+ * @param {Object} [osModule] - Optional os module (for testing). If not provided, requires 'os'
25
+ * @returns {Promise<Object>} Map of variables for interpolation
26
+ */
27
+ async function buildEnvVarMap(context, osModule = null) {
28
+ // Load env-config (base + user override if configured)
29
+ let baseVars = {};
30
+ try {
31
+ const envCfg = await loadEnvConfig();
32
+ const envs = envCfg && envCfg.environments ? envCfg.environments : {};
33
+ baseVars = { ...(envs[context] || {}) };
34
+ } catch {
35
+ baseVars = {};
36
+ }
37
+
38
+ // Get os module - use provided one or require it
39
+ const os = osModule || require('os');
40
+
41
+ // Merge overrides from ~/.aifabrix/config.yaml
42
+ let overrideVars = {};
43
+ try {
44
+ const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
45
+ if (fs.existsSync(cfgPath)) {
46
+ const cfgContent = fs.readFileSync(cfgPath, 'utf8');
47
+ const cfg = yaml.load(cfgContent) || {};
48
+ if (cfg && cfg.environments && cfg.environments[context]) {
49
+ overrideVars = { ...cfg.environments[context] };
50
+ }
51
+ }
52
+ } catch {
53
+ // ignore overrides on error
54
+ }
55
+
56
+ // Apply aifabrix-localhost override for local hostnames
57
+ let localhostOverride = null;
58
+ if (context === 'local') {
59
+ try {
60
+ const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
61
+ if (fs.existsSync(cfgPath)) {
62
+ const cfgContent = fs.readFileSync(cfgPath, 'utf8');
63
+ const cfg = yaml.load(cfgContent) || {};
64
+ if (typeof cfg['aifabrix-localhost'] === 'string' && cfg['aifabrix-localhost'].trim().length > 0) {
65
+ localhostOverride = cfg['aifabrix-localhost'].trim();
66
+ }
67
+ }
68
+ } catch {
69
+ // ignore
70
+ }
71
+ }
72
+
73
+ const merged = { ...baseVars, ...overrideVars };
74
+
75
+ // Normalize map: if VAR value is "host:port" and VAR ends with "_HOST",
76
+ // expose VAR as host only and also provide "<ROOT>_PORT"
77
+ // If VAR value is "host:port" but VAR doesn't end with "_HOST", still split to VAR_HOST/VAR_PORT
78
+ const result = {};
79
+ for (const [key, rawVal] of Object.entries(merged)) {
80
+ if (typeof rawVal !== 'string') {
81
+ result[key] = rawVal;
82
+ continue;
83
+ }
84
+ const hostPortMatch = rawVal.match(/^([A-Za-z0-9._-]+):(\d+)$/);
85
+ if (hostPortMatch) {
86
+ const host = hostPortMatch[1];
87
+ const port = hostPortMatch[2];
88
+ if (/_HOST$/.test(key)) {
89
+ // Example: DB_HOST: "postgres:5432" -> DB_HOST="postgres", DB_PORT="5432"
90
+ const root = key.replace(/_HOST$/, '');
91
+ const hostValue = context === 'local' && host === 'localhost' && localhostOverride ? localhostOverride : host;
92
+ result[key] = hostValue;
93
+ result[`${root}_PORT`] = port;
94
+ } else {
95
+ // Generic key with host:port -> expose KEY_HOST and KEY_PORT, and keep KEY as host
96
+ const hostValue = context === 'local' && host === 'localhost' && localhostOverride ? localhostOverride : host;
97
+ result[`${key}_HOST`] = hostValue;
98
+ result[`${key}_PORT`] = port;
99
+ result[key] = hostValue;
100
+ }
101
+ } else {
102
+ // Plain value
103
+ let val = rawVal;
104
+ if (context === 'local' && /_HOST$/.test(key) && rawVal === 'localhost' && localhostOverride) {
105
+ val = localhostOverride;
106
+ }
107
+ result[key] = val;
108
+ }
109
+ }
110
+ return result;
111
+ }
112
+
113
+ module.exports = {
114
+ buildEnvVarMap
115
+ };
116
+
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Environment port utilities
3
+ *
4
+ * @fileoverview Update container PORT based on variables.yaml and developer-id offset
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const yaml = require('js-yaml');
13
+ const config = require('../config');
14
+
15
+ /**
16
+ * Update PORT in the container's .env file to use variables.port (+offset)
17
+ * @function updateContainerPortInEnvFile
18
+ * @param {string} envPath - Path to .env
19
+ * @param {string} variablesPath - Path to variables.yaml
20
+ */
21
+ function updateContainerPortInEnvFile(envPath, variablesPath) {
22
+ if (!fs.existsSync(variablesPath)) {
23
+ return;
24
+ }
25
+ const variablesContent = fs.readFileSync(variablesPath, 'utf8');
26
+ const variables = yaml.load(variablesContent);
27
+ // Base port from variables
28
+ const basePort = variables?.port || 3000;
29
+ // Determine developer-id (prefer env var for sync context, fallback to config file)
30
+ let devIdNum = 0;
31
+ const devIdRaw = process.env.AIFABRIX_DEVELOPERID;
32
+ if (devIdRaw && /^[0-9]+$/.test(devIdRaw)) {
33
+ devIdNum = parseInt(devIdRaw, 10);
34
+ } else {
35
+ try {
36
+ const cfgPath = config && config.CONFIG_FILE ? config.CONFIG_FILE : null;
37
+ if (cfgPath && fs.existsSync(cfgPath)) {
38
+ const cfgContent = fs.readFileSync(cfgPath, 'utf8');
39
+ const cfg = yaml.load(cfgContent) || {};
40
+ const raw = cfg['developer-id'];
41
+ if (typeof raw === 'number') {
42
+ devIdNum = raw;
43
+ } else if (typeof raw === 'string' && /^[0-9]+$/.test(raw)) {
44
+ devIdNum = parseInt(raw, 10);
45
+ }
46
+ }
47
+ } catch {
48
+ // ignore, will use 0
49
+ }
50
+ }
51
+ const port = devIdNum > 0 ? (basePort + devIdNum * 100) : basePort;
52
+ let envContent = fs.readFileSync(envPath, 'utf8');
53
+ envContent = envContent.replace(/^PORT\s*=\s*.*$/m, `PORT=${port}`);
54
+ fs.writeFileSync(envPath, envContent, { mode: 0o600 });
55
+ }
56
+
57
+ module.exports = {
58
+ updateContainerPortInEnvFile
59
+ };
60
+
@@ -13,6 +13,7 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
  const os = require('os');
15
15
  const dockerUtils = require('./docker');
16
+ const { getActualSecretsPath } = require('./secrets-path');
16
17
 
17
18
  /**
18
19
  * Checks if Docker is installed and available
@@ -62,13 +63,42 @@ async function checkPorts() {
62
63
 
63
64
  /**
64
65
  * Checks if secrets file exists
66
+ * Checks both the default path and the path configured in config.yaml (aifabrix-secrets)
65
67
  *
68
+ * @async
66
69
  * @function checkSecrets
67
- * @returns {string} 'ok' if secrets file exists, 'missing' otherwise
70
+ * @returns {Promise<Object>} Object with status ('ok' or 'missing') and paths checked
71
+ * @returns {string} returns.status - 'ok' if secrets file exists, 'missing' otherwise
72
+ * @returns {string[]} returns.paths - Array of paths that were checked
68
73
  */
69
- function checkSecrets() {
70
- const secretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
71
- return fs.existsSync(secretsPath) ? 'ok' : 'missing';
74
+ async function checkSecrets() {
75
+ try {
76
+ const { userPath, buildPath } = await getActualSecretsPath();
77
+ const pathsChecked = [];
78
+
79
+ // Check user path (default: ~/.aifabrix/secrets.local.yaml)
80
+ if (fs.existsSync(userPath)) {
81
+ return { status: 'ok', paths: [userPath] };
82
+ }
83
+ pathsChecked.push(userPath);
84
+
85
+ // Check build path (from config.yaml aifabrix-secrets)
86
+ if (buildPath && fs.existsSync(buildPath)) {
87
+ return { status: 'ok', paths: [buildPath] };
88
+ }
89
+ if (buildPath) {
90
+ pathsChecked.push(buildPath);
91
+ }
92
+
93
+ return { status: 'missing', paths: pathsChecked };
94
+ } catch (error) {
95
+ // Fallback to default path if there's an error
96
+ const defaultPath = path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
97
+ return {
98
+ status: fs.existsSync(defaultPath) ? 'ok' : 'missing',
99
+ paths: [defaultPath]
100
+ };
101
+ }
72
102
  }
73
103
 
74
104
  /**
@@ -105,9 +135,12 @@ async function checkEnvironment() {
105
135
  }
106
136
 
107
137
  // Check secrets
108
- result.secrets = checkSecrets();
138
+ const secretsCheck = await checkSecrets();
139
+ result.secrets = secretsCheck.status;
109
140
  if (result.secrets === 'missing') {
110
- result.recommendations.push('Create secrets file: ~/.aifabrix/secrets.yaml');
141
+ // Show the actual paths that were checked
142
+ const pathsList = secretsCheck.paths.map(p => p).join(' or ');
143
+ result.recommendations.push(`Create secrets file: ${pathsList}`);
111
144
  }
112
145
 
113
146
  return result;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Image name utilities
3
+ *
4
+ * @fileoverview Helper functions for computing developer-scoped Docker image names
5
+ * based on the current developer identifier. Ensures consistent local build naming.
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ /**
13
+ * Builds a developer-scoped image name for local Docker builds.
14
+ * Format: "<base>-dev<developerId>".
15
+ * If developerId is missing, non-numeric, or 0 → "<base>-extra".
16
+ *
17
+ * @function buildDevImageName
18
+ * @param {string} baseName - Base image name (no registry), e.g., "myapp"
19
+ * @param {(string|number|null|undefined)} developerId - Developer identifier
20
+ * @returns {string} Developer-scoped image name
21
+ *
22
+ * @example
23
+ * buildDevImageName('myapp', 123) // "myapp-dev123"
24
+ * buildDevImageName('myapp', '0') // "myapp-extra"
25
+ * buildDevImageName('myapp') // "myapp-extra"
26
+ */
27
+ function buildDevImageName(baseName, developerId) {
28
+ const id =
29
+ typeof developerId === 'number'
30
+ ? developerId
31
+ : typeof developerId === 'string'
32
+ ? parseInt(developerId, 10)
33
+ : NaN;
34
+
35
+ if (!baseName || typeof baseName !== 'string') {
36
+ throw new Error('Base image name is required and must be a string');
37
+ }
38
+
39
+ if (!Number.isFinite(id) || id === 0) {
40
+ return `${baseName}-extra`;
41
+ }
42
+
43
+ return `${baseName}-dev${id}`;
44
+ }
45
+
46
+ module.exports = {
47
+ buildDevImageName
48
+ };
49
+
@@ -12,14 +12,14 @@
12
12
  'use strict';
13
13
 
14
14
  const path = require('path');
15
- const os = require('os');
16
15
  const fs = require('fs');
17
16
  const yaml = require('js-yaml');
18
17
 
19
18
  function safeHomedir() {
20
19
  try {
21
- if (typeof os.homedir === 'function') {
22
- const hd = os.homedir();
20
+ const osMod = require('os');
21
+ if (typeof osMod.homedir === 'function') {
22
+ const hd = osMod.homedir();
23
23
  if (typeof hd === 'string' && hd.length > 0) {
24
24
  return hd;
25
25
  }
@@ -38,18 +38,21 @@ function safeHomedir() {
38
38
  * @returns {string} Absolute path to the AI Fabrix home directory
39
39
  */
40
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);
41
+ const isTestEnv = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
42
+ if (!isTestEnv) {
43
+ try {
44
+ const configPath = path.join(safeHomedir(), '.aifabrix', 'config.yaml');
45
+ if (fs.existsSync(configPath)) {
46
+ const content = fs.readFileSync(configPath, 'utf8');
47
+ const config = yaml.load(content) || {};
48
+ const homeOverride = config && typeof config['aifabrix-home'] === 'string' ? config['aifabrix-home'].trim() : '';
49
+ if (homeOverride) {
50
+ return path.resolve(homeOverride);
51
+ }
49
52
  }
53
+ } catch {
54
+ // Ignore errors and fall back to default
50
55
  }
51
- } catch {
52
- // Ignore errors and fall back to default
53
56
  }
54
57
  return path.join(safeHomedir(), '.aifabrix');
55
58
  }
@@ -73,20 +76,19 @@ function getApplicationsBaseDir(developerId) {
73
76
 
74
77
  /**
75
78
  * 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}
79
+ * Dev 0: points to applications/ (root)
80
+ * Dev > 0: <home>/applications-dev-{id} (root)
78
81
  *
79
82
  * @param {string} appName - Application name
80
83
  * @param {number|string} developerId - Developer ID
81
84
  * @returns {string} Absolute path to developer-specific app directory
82
85
  */
83
86
  function getDevDirectory(appName, developerId) {
84
- const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
85
87
  const baseDir = getApplicationsBaseDir(developerId);
86
- if (idNum === 0) {
87
- return baseDir;
88
- }
89
- return path.join(baseDir, `${appName}-dev-${developerId}`);
88
+ // All files should be generated at the root of the applications folder
89
+ // Dev 0: <home>/applications
90
+ // Dev > 0: <home>/applications-dev-{id}
91
+ return baseDir;
90
92
  }
91
93
 
92
94
  module.exports = {
@@ -64,7 +64,7 @@ function generateSecretValue(key) {
64
64
  if (dbUrlMatch) {
65
65
  const appName = dbUrlMatch[1];
66
66
  const dbName = appName.replace(/-/g, '_');
67
- return `postgresql://${dbName}_user:${dbName}_pass123@\${DB_HOST}:5432/${dbName}`;
67
+ return `postgresql://${dbName}_user:${dbName}_pass123@\${DB_HOST}:\${DB_PORT}/${dbName}`;
68
68
  }
69
69
  return '';
70
70
  }
@@ -188,11 +188,11 @@ postgres-passwordKeyVault: "admin123"
188
188
 
189
189
  # Redis Secrets
190
190
  redis-passwordKeyVault: ""
191
- redis-urlKeyVault: "redis://\${REDIS_HOST}:6379"
191
+ redis-urlKeyVault: "redis://\${REDIS_HOST}:\${REDIS_PORT}"
192
192
 
193
193
  # Keycloak Secrets
194
194
  keycloak-admin-passwordKeyVault: "admin123"
195
- keycloak-auth-server-urlKeyVault: "http://\${KEYCLOAK_HOST}:8082"
195
+ keycloak-auth-server-urlKeyVault: "http://\${KEYCLOAK_HOST}:\${KEYCLOAK_PORT}"
196
196
  `;
197
197
 
198
198
  fs.writeFileSync(resolvedPath, defaultSecrets, { mode: 0o600 });