@aifabrix/builder 2.31.1 → 2.32.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 (118) hide show
  1. package/README.md +9 -9
  2. package/integration/hubspot/README.md +2 -2
  3. package/integration/hubspot/hubspot-deploy-company.json +17 -14
  4. package/integration/hubspot/hubspot-deploy-contact.json +19 -16
  5. package/integration/hubspot/hubspot-deploy-deal.json +21 -18
  6. package/lib/api/types/datasources.types.js +31 -5
  7. package/lib/api/types/wizard.types.js +142 -0
  8. package/lib/api/wizard.api.js +177 -0
  9. package/lib/{app-config.js → app/config.js} +4 -4
  10. package/lib/{app-deploy.js → app/deploy.js} +8 -8
  11. package/lib/app/display.js +90 -0
  12. package/lib/{app-dockerfile.js → app/dockerfile.js} +4 -4
  13. package/lib/{app-down.js → app/down.js} +4 -4
  14. package/lib/app/helpers.js +218 -0
  15. package/lib/app/index.js +298 -0
  16. package/lib/{app-list.js → app/list.js} +6 -6
  17. package/lib/{app-push.js → app/push.js} +4 -4
  18. package/lib/{app-readme.js → app/readme.js} +34 -13
  19. package/lib/{app-register.js → app/register.js} +9 -9
  20. package/lib/{app-rotate-secret.js → app/rotate-secret.js} +10 -10
  21. package/lib/{app-run-helpers.js → app/run-helpers.js} +10 -10
  22. package/lib/{app-run.js → app/run.js} +6 -6
  23. package/lib/{build.js → build/index.js} +59 -32
  24. package/lib/build/package.json +7 -0
  25. package/lib/cli.js +245 -179
  26. package/lib/commands/app.js +3 -3
  27. package/lib/commands/datasource.js +4 -4
  28. package/lib/commands/login-credentials.js +209 -0
  29. package/lib/commands/login-device.js +254 -0
  30. package/lib/commands/login.js +67 -378
  31. package/lib/commands/logout.js +1 -1
  32. package/lib/commands/secrets-set.js +1 -1
  33. package/lib/commands/secure.js +2 -2
  34. package/lib/commands/wizard.js +498 -0
  35. package/lib/{audit-logger.js → core/audit-logger.js} +1 -1
  36. package/lib/{config.js → core/config.js} +28 -26
  37. package/lib/{diff.js → core/diff.js} +157 -72
  38. package/lib/{secrets.js → core/secrets.js} +86 -49
  39. package/lib/{templates.js → core/templates-env.js} +14 -222
  40. package/lib/core/templates.js +279 -0
  41. package/lib/{datasource-deploy.js → datasource/deploy.js} +6 -6
  42. package/lib/{datasource-diff.js → datasource/diff.js} +2 -2
  43. package/lib/datasource/list.js +223 -0
  44. package/lib/{datasource-validate.js → datasource/validate.js} +2 -2
  45. package/lib/{deployer.js → deployment/deployer.js} +48 -18
  46. package/lib/{environment-deploy.js → deployment/environment.js} +163 -84
  47. package/lib/{push.js → deployment/push.js} +1 -1
  48. package/lib/external-system/deploy-helpers.js +145 -0
  49. package/lib/{external-system-deploy.js → external-system/deploy.js} +156 -111
  50. package/lib/external-system/download-helpers.js +114 -0
  51. package/lib/{external-system-download.js → external-system/download.js} +92 -135
  52. package/lib/{external-system-generator.js → external-system/generator.js} +15 -11
  53. package/lib/external-system/test-auth.js +40 -0
  54. package/lib/external-system/test-execution.js +84 -0
  55. package/lib/external-system/test-helpers.js +109 -0
  56. package/lib/{external-system-test.js → external-system/test.js} +174 -192
  57. package/lib/{generator-builders.js → generator/builders.js} +87 -10
  58. package/lib/{generator-external.js → generator/external.js} +115 -52
  59. package/lib/{github-generator.js → generator/github.js} +116 -15
  60. package/lib/{generator-helpers.js → generator/helpers.js} +92 -42
  61. package/lib/{generator.js → generator/index.js} +49 -22
  62. package/lib/{generator-split.js → generator/split.js} +108 -55
  63. package/lib/generator/wizard-prompts.js +357 -0
  64. package/lib/generator/wizard.js +490 -0
  65. package/lib/{infra.js → infrastructure/index.js} +49 -22
  66. package/lib/schema/external-datasource.schema.json +145 -133
  67. package/lib/schema/external-system.schema.json +42 -0
  68. package/lib/utils/api.js +9 -5
  69. package/lib/utils/app-register-api.js +60 -32
  70. package/lib/utils/app-register-auth.js +172 -47
  71. package/lib/utils/app-register-config.js +130 -59
  72. package/lib/utils/app-run-containers.js +29 -8
  73. package/lib/utils/build-helpers.js +1 -1
  74. package/lib/utils/cli-utils.js +78 -30
  75. package/lib/utils/compose-generator.js +145 -65
  76. package/lib/utils/config-paths.js +2 -0
  77. package/lib/utils/deployment-errors.js +1 -1
  78. package/lib/utils/device-code.js +99 -41
  79. package/lib/utils/env-config-loader.js +1 -1
  80. package/lib/utils/env-copy.js +21 -18
  81. package/lib/utils/env-endpoints.js +115 -67
  82. package/lib/utils/env-map.js +13 -14
  83. package/lib/utils/env-ports.js +45 -25
  84. package/lib/utils/env-template.js +84 -42
  85. package/lib/utils/error-formatter.js +26 -9
  86. package/lib/utils/error-formatters/error-parser.js +90 -4
  87. package/lib/utils/error-formatters/http-status-errors.js +54 -17
  88. package/lib/utils/error-formatters/network-errors.js +103 -26
  89. package/lib/utils/external-system-display.js +184 -90
  90. package/lib/utils/external-system-validators.js +164 -42
  91. package/lib/utils/file-upload.js +109 -0
  92. package/lib/utils/health-check.js +199 -83
  93. package/lib/utils/infra-containers.js +1 -1
  94. package/lib/utils/infra-status.js +66 -15
  95. package/lib/utils/local-secrets.js +45 -25
  96. package/lib/utils/paths.js +45 -33
  97. package/lib/utils/schema-loader.js +42 -25
  98. package/lib/utils/schema-resolver.js +123 -74
  99. package/lib/utils/secrets-encryption.js +62 -25
  100. package/lib/utils/secrets-helpers.js +126 -63
  101. package/lib/utils/secrets-path.js +1 -1
  102. package/lib/utils/secrets-url.js +1 -1
  103. package/lib/utils/token-manager-refresh.js +181 -0
  104. package/lib/utils/token-manager.js +76 -123
  105. package/lib/utils/variable-transformer.js +154 -77
  106. package/lib/utils/yaml-preserve.js +41 -47
  107. package/lib/{template-validator.js → validation/template.js} +54 -23
  108. package/lib/{validate.js → validation/validate.js} +205 -125
  109. package/lib/{validator.js → validation/validator.js} +58 -39
  110. package/package.json +31 -2
  111. package/templates/external-system/deploy.ps1.hbs +34 -0
  112. package/templates/external-system/deploy.sh.hbs +34 -0
  113. package/templates/external-system/external-datasource.json.hbs +31 -12
  114. package/lib/app.js +0 -467
  115. package/lib/datasource-list.js +0 -141
  116. /package/lib/{app-prompts.js → app/prompts.js} +0 -0
  117. /package/lib/{env-reader.js → core/env-reader.js} +0 -0
  118. /package/lib/{key-generator.js → core/key-generator.js} +0 -0
@@ -12,7 +12,7 @@
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
14
  const chalk = require('chalk');
15
- const logger = require('./utils/logger');
15
+ const logger = require('../utils/logger');
16
16
 
17
17
  /**
18
18
  * Handle added field in comparison
@@ -83,6 +83,37 @@ function isNestedObject(value) {
83
83
  * @param {string} [path=''] - Current path in object (for nested fields)
84
84
  * @returns {Object} Comparison result with added, removed, changed fields
85
85
  */
86
+ /**
87
+ * Compares a single key between two objects
88
+ * @function compareSingleKey
89
+ * @param {string} key - Key to compare
90
+ * @param {Object} obj1 - First object
91
+ * @param {Object} obj2 - Second object
92
+ * @param {string} newPath - Current path in object
93
+ * @param {Object} result - Comparison result object
94
+ */
95
+ function compareSingleKey(key, obj1, obj2, newPath, result) {
96
+ const val1 = obj1 && obj1[key];
97
+ const val2 = obj2 && obj2[key];
98
+
99
+ if (!(key in obj1)) {
100
+ handleAddedField(key, val2, newPath, result);
101
+ } else if (!(key in obj2)) {
102
+ handleRemovedField(key, val1, newPath, result);
103
+ } else if (isNestedObject(val1) && isNestedObject(val2)) {
104
+ // Recursively compare nested objects
105
+ const nestedResult = compareObjects(val1, val2, newPath);
106
+ result.added.push(...nestedResult.added);
107
+ result.removed.push(...nestedResult.removed);
108
+ result.changed.push(...nestedResult.changed);
109
+ if (!nestedResult.identical) {
110
+ result.identical = false;
111
+ }
112
+ } else if (JSON.stringify(val1) !== JSON.stringify(val2)) {
113
+ handleChangedField(val1, val2, newPath, result);
114
+ }
115
+ }
116
+
86
117
  function compareObjects(obj1, obj2, currentPath = '') {
87
118
  const result = {
88
119
  added: [],
@@ -95,25 +126,7 @@ function compareObjects(obj1, obj2, currentPath = '') {
95
126
 
96
127
  for (const key of allKeys) {
97
128
  const newPath = currentPath ? `${currentPath}.${key}` : key;
98
- const val1 = obj1 && obj1[key];
99
- const val2 = obj2 && obj2[key];
100
-
101
- if (!(key in obj1)) {
102
- handleAddedField(key, val2, newPath, result);
103
- } else if (!(key in obj2)) {
104
- handleRemovedField(key, val1, newPath, result);
105
- } else if (isNestedObject(val1) && isNestedObject(val2)) {
106
- // Recursively compare nested objects
107
- const nestedResult = compareObjects(val1, val2, newPath);
108
- result.added.push(...nestedResult.added);
109
- result.removed.push(...nestedResult.removed);
110
- result.changed.push(...nestedResult.changed);
111
- if (!nestedResult.identical) {
112
- result.identical = false;
113
- }
114
- } else if (JSON.stringify(val1) !== JSON.stringify(val2)) {
115
- handleChangedField(val1, val2, newPath, result);
116
- }
129
+ compareSingleKey(key, obj1, obj2, newPath, result);
117
130
  }
118
131
 
119
132
  return result;
@@ -170,49 +183,68 @@ function identifyBreakingChanges(comparison) {
170
183
  * const result = await compareFiles('./old.json', './new.json');
171
184
  * // Returns: { identical: false, added: [...], removed: [...], changed: [...] }
172
185
  */
173
- async function compareFiles(file1, file2) {
186
+ /**
187
+ * Validates file paths
188
+ * @function validateFilePaths
189
+ * @param {string} file1 - First file path
190
+ * @param {string} file2 - Second file path
191
+ * @throws {Error} If paths are invalid
192
+ */
193
+ function validateFilePaths(file1, file2) {
174
194
  if (!file1 || typeof file1 !== 'string') {
175
195
  throw new Error('First file path is required');
176
196
  }
177
197
  if (!file2 || typeof file2 !== 'string') {
178
198
  throw new Error('Second file path is required');
179
199
  }
180
-
181
- // Validate files exist
182
200
  if (!fs.existsSync(file1)) {
183
201
  throw new Error(`File not found: ${file1}`);
184
202
  }
185
203
  if (!fs.existsSync(file2)) {
186
204
  throw new Error(`File not found: ${file2}`);
187
205
  }
206
+ }
188
207
 
189
- // Read and parse files
190
- let content1, content2;
191
- let parsed1, parsed2;
192
-
193
- try {
194
- content1 = fs.readFileSync(file1, 'utf8');
195
- parsed1 = JSON.parse(content1);
196
- } catch (error) {
197
- throw new Error(`Failed to parse ${file1}: ${error.message}`);
198
- }
199
-
208
+ /**
209
+ * Reads and parses a JSON file
210
+ * @function readAndParseFile
211
+ * @param {string} filePath - File path
212
+ * @returns {Object} Parsed JSON object
213
+ * @throws {Error} If file cannot be read or parsed
214
+ */
215
+ function readAndParseFile(filePath) {
200
216
  try {
201
- content2 = fs.readFileSync(file2, 'utf8');
202
- parsed2 = JSON.parse(content2);
217
+ const content = fs.readFileSync(filePath, 'utf8');
218
+ return JSON.parse(content);
203
219
  } catch (error) {
204
- throw new Error(`Failed to parse ${file2}: ${error.message}`);
220
+ throw new Error(`Failed to parse ${filePath}: ${error.message}`);
205
221
  }
222
+ }
206
223
 
207
- // Compare objects
208
- const comparison = compareObjects(parsed1, parsed2);
224
+ /**
225
+ * Extracts version from parsed object
226
+ * @function extractVersion
227
+ * @param {Object} parsed - Parsed JSON object
228
+ * @returns {string} Version string
229
+ */
230
+ function extractVersion(parsed) {
231
+ return parsed.version || parsed.metadata?.version || 'unknown';
232
+ }
209
233
 
210
- // Check for version changes
211
- const version1 = parsed1.version || parsed1.metadata?.version || 'unknown';
212
- const version2 = parsed2.version || parsed2.metadata?.version || 'unknown';
234
+ /**
235
+ * Builds comparison result object
236
+ * @function buildComparisonResult
237
+ * @param {Object} comparison - Comparison result from compareObjects
238
+ * @param {Object} parsed1 - First parsed object
239
+ * @param {Object} parsed2 - Second parsed object
240
+ * @param {string} file1 - First file path
241
+ * @param {string} file2 - Second file path
242
+ * @returns {Object} Complete comparison result
243
+ */
244
+ function buildComparisonResult(comparison, parsed1, parsed2, file1, file2) {
245
+ const version1 = extractVersion(parsed1);
246
+ const version2 = extractVersion(parsed2);
213
247
  const versionChanged = version1 !== version2;
214
-
215
- // Identify breaking changes
216
248
  const breakingChanges = identifyBreakingChanges(comparison);
217
249
 
218
250
  return {
@@ -235,6 +267,16 @@ async function compareFiles(file1, file2) {
235
267
  };
236
268
  }
237
269
 
270
+ async function compareFiles(file1, file2) {
271
+ validateFilePaths(file1, file2);
272
+
273
+ const parsed1 = readAndParseFile(file1);
274
+ const parsed2 = readAndParseFile(file2);
275
+
276
+ const comparison = compareObjects(parsed1, parsed2);
277
+ return buildComparisonResult(comparison, parsed1, parsed2, file1, file2);
278
+ }
279
+
238
280
  /**
239
281
  * Formats and displays diff output
240
282
  * Shows differences in a user-friendly format with color coding
@@ -242,61 +284,104 @@ async function compareFiles(file1, file2) {
242
284
  * @function formatDiffOutput
243
285
  * @param {Object} diffResult - Comparison result from compareFiles
244
286
  */
245
- function formatDiffOutput(diffResult) {
246
- logger.log(chalk.blue(`\nComparing: ${diffResult.file1} ${diffResult.file2}`));
247
-
248
- if (diffResult.identical) {
249
- logger.log(chalk.green('\n✓ Files are identical'));
250
- return;
251
- }
252
-
253
- logger.log(chalk.yellow('\nFiles are different'));
254
-
255
- // Version information
287
+ /**
288
+ * Displays version information if changed
289
+ * @function displayVersionInfo
290
+ * @param {Object} diffResult - Comparison result
291
+ */
292
+ function displayVersionInfo(diffResult) {
256
293
  if (diffResult.versionChanged) {
257
294
  logger.log(chalk.blue(`\nVersion: ${diffResult.version1} → ${diffResult.version2}`));
258
295
  }
296
+ }
259
297
 
260
- // Breaking changes
261
- if (diffResult.breakingChanges.length > 0) {
298
+ /**
299
+ * Displays breaking changes
300
+ * @function displayBreakingChanges
301
+ * @param {Object[]} breakingChanges - Array of breaking changes
302
+ */
303
+ function displayBreakingChanges(breakingChanges) {
304
+ if (breakingChanges.length > 0) {
262
305
  logger.log(chalk.red('\n⚠️ Breaking Changes:'));
263
- diffResult.breakingChanges.forEach(change => {
306
+ breakingChanges.forEach(change => {
264
307
  logger.log(chalk.red(` • ${change.description}`));
265
308
  });
266
309
  }
310
+ }
267
311
 
268
- // Added fields
269
- if (diffResult.added.length > 0) {
312
+ /**
313
+ * Displays added fields
314
+ * @function displayAddedFields
315
+ * @param {Object[]} added - Array of added fields
316
+ */
317
+ function displayAddedFields(added) {
318
+ if (added.length > 0) {
270
319
  logger.log(chalk.green('\nAdded Fields:'));
271
- diffResult.added.forEach(field => {
320
+ added.forEach(field => {
272
321
  logger.log(chalk.green(` + ${field.path}: ${JSON.stringify(field.value)}`));
273
322
  });
274
323
  }
324
+ }
275
325
 
276
- // Removed fields
277
- if (diffResult.removed.length > 0) {
326
+ /**
327
+ * Displays removed fields
328
+ * @function displayRemovedFields
329
+ * @param {Object[]} removed - Array of removed fields
330
+ */
331
+ function displayRemovedFields(removed) {
332
+ if (removed.length > 0) {
278
333
  logger.log(chalk.red('\nRemoved Fields:'));
279
- diffResult.removed.forEach(field => {
334
+ removed.forEach(field => {
280
335
  logger.log(chalk.red(` - ${field.path}: ${JSON.stringify(field.value)}`));
281
336
  });
282
337
  }
338
+ }
283
339
 
284
- // Changed fields
285
- if (diffResult.changed.length > 0) {
340
+ /**
341
+ * Displays changed fields
342
+ * @function displayChangedFields
343
+ * @param {Object[]} changed - Array of changed fields
344
+ */
345
+ function displayChangedFields(changed) {
346
+ if (changed.length > 0) {
286
347
  logger.log(chalk.yellow('\nChanged Fields:'));
287
- diffResult.changed.forEach(change => {
348
+ changed.forEach(change => {
288
349
  logger.log(chalk.yellow(` ~ ${change.path}:`));
289
350
  logger.log(chalk.gray(` Old: ${JSON.stringify(change.oldValue)}`));
290
351
  logger.log(chalk.gray(` New: ${JSON.stringify(change.newValue)}`));
291
352
  });
292
353
  }
354
+ }
293
355
 
294
- // Summary
356
+ /**
357
+ * Displays summary statistics
358
+ * @function displaySummary
359
+ * @param {Object} summary - Summary object
360
+ */
361
+ function displaySummary(summary) {
295
362
  logger.log(chalk.blue('\nSummary:'));
296
- logger.log(chalk.blue(` Added: ${diffResult.summary.totalAdded}`));
297
- logger.log(chalk.blue(` Removed: ${diffResult.summary.totalRemoved}`));
298
- logger.log(chalk.blue(` Changed: ${diffResult.summary.totalChanged}`));
299
- logger.log(chalk.blue(` Breaking: ${diffResult.summary.totalBreaking}`));
363
+ logger.log(chalk.blue(` Added: ${summary.totalAdded}`));
364
+ logger.log(chalk.blue(` Removed: ${summary.totalRemoved}`));
365
+ logger.log(chalk.blue(` Changed: ${summary.totalChanged}`));
366
+ logger.log(chalk.blue(` Breaking: ${summary.totalBreaking}`));
367
+ }
368
+
369
+ function formatDiffOutput(diffResult) {
370
+ logger.log(chalk.blue(`\nComparing: ${diffResult.file1} ↔ ${diffResult.file2}`));
371
+
372
+ if (diffResult.identical) {
373
+ logger.log(chalk.green('\n✓ Files are identical'));
374
+ return;
375
+ }
376
+
377
+ logger.log(chalk.yellow('\nFiles are different'));
378
+
379
+ displayVersionInfo(diffResult);
380
+ displayBreakingChanges(diffResult.breakingChanges);
381
+ displayAddedFields(diffResult.added);
382
+ displayRemovedFields(diffResult.removed);
383
+ displayChangedFields(diffResult.changed);
384
+ displaySummary(diffResult.summary);
300
385
  }
301
386
 
302
387
  module.exports = {
@@ -12,7 +12,7 @@
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
14
  const yaml = require('js-yaml');
15
- const logger = require('./utils/logger');
15
+ const logger = require('../utils/logger');
16
16
  const config = require('./config');
17
17
  const {
18
18
  interpolateEnvVars,
@@ -26,24 +26,24 @@ const {
26
26
  applyCanonicalSecretsOverride,
27
27
  ensureNonEmptySecrets,
28
28
  validateSecrets
29
- } = require('./utils/secrets-helpers');
30
- const { processEnvVariables } = require('./utils/env-copy');
31
- const { buildEnvVarMap } = require('./utils/env-map');
32
- const { resolveServicePortsInEnvContent } = require('./utils/secrets-url');
29
+ } = require('../utils/secrets-helpers');
30
+ const { processEnvVariables } = require('../utils/env-copy');
31
+ const { buildEnvVarMap } = require('../utils/env-map');
32
+ const { resolveServicePortsInEnvContent } = require('../utils/secrets-url');
33
33
  const {
34
34
  generateMissingSecrets,
35
35
  createDefaultSecrets
36
- } = require('./utils/secrets-generator');
36
+ } = require('../utils/secrets-generator');
37
37
  const {
38
38
  resolveSecretsPath,
39
39
  getActualSecretsPath
40
- } = require('./utils/secrets-path');
40
+ } = require('../utils/secrets-path');
41
41
  const {
42
42
  loadUserSecrets,
43
43
  loadDefaultSecrets
44
- } = require('./utils/secrets-utils');
45
- const { decryptSecret, isEncrypted } = require('./utils/secrets-encryption');
46
- const pathsUtil = require('./utils/paths');
44
+ } = require('../utils/secrets-utils');
45
+ const { decryptSecret, isEncrypted } = require('../utils/secrets-encryption');
46
+ const pathsUtil = require('../utils/paths');
47
47
 
48
48
  /**
49
49
  * Generates a canonical secret name from an environment variable key.
@@ -58,7 +58,10 @@ function getCanonicalSecretName(key) {
58
58
  if (!key || typeof key !== 'string') {
59
59
  return '';
60
60
  }
61
- const lower = key.toLowerCase();
61
+ // Insert hyphens before capital letters (camelCase -> kebab-case)
62
+ // Then convert to lowercase and replace non-alphanumeric with hyphens
63
+ const withHyphens = key.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
64
+ const lower = withHyphens.toLowerCase();
62
65
  const hyphenated = lower.replace(/[^a-z0-9]/g, '-');
63
66
  const collapsed = hyphenated.replace(/-+/g, '-');
64
67
  return collapsed.replace(/^-+|-+$/g, '');
@@ -217,21 +220,23 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local',
217
220
  * // Returns: './builder/myapp/.env'
218
221
  */
219
222
  /**
220
- * Updates PORT in resolved content for docker environment
221
- * Sets PORT to container port (build.containerPort or port from variables.yaml)
222
- * NOT the host port (which includes developer-id offset)
223
+ * Gets base docker environment config
223
224
  * @async
224
- * @function updatePortForDocker
225
- * @param {string} resolved - Resolved environment content
226
- * @param {string} variablesPath - Path to variables.yaml file
227
- * @returns {Promise<string>} Updated content with PORT set
225
+ * @function getBaseDockerEnv
226
+ * @returns {Promise<Object>} Docker environment config
228
227
  */
229
- async function updatePortForDocker(resolved, variablesPath) {
230
- // Step 1: Get base config from env-config.yaml (includes user env-config file if configured)
231
- const { getEnvHosts } = require('./utils/env-endpoints');
232
- let dockerEnv = await getEnvHosts('docker');
228
+ async function getBaseDockerEnv() {
229
+ const { getEnvHosts } = require('../utils/env-endpoints');
230
+ return await getEnvHosts('docker');
231
+ }
233
232
 
234
- // Step 2: Apply config.yaml → environments.docker override (if exists)
233
+ /**
234
+ * Applies config.yaml override to docker environment
235
+ * @function applyDockerEnvOverride
236
+ * @param {Object} dockerEnv - Base docker environment config
237
+ * @returns {Object} Updated docker environment config
238
+ */
239
+ function applyDockerEnvOverride(dockerEnv) {
235
240
  try {
236
241
  const os = require('os');
237
242
  const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
@@ -239,38 +244,70 @@ async function updatePortForDocker(resolved, variablesPath) {
239
244
  const cfgContent = fs.readFileSync(cfgPath, 'utf8');
240
245
  const cfg = yaml.load(cfgContent) || {};
241
246
  if (cfg && cfg.environments && cfg.environments.docker) {
242
- dockerEnv = { ...dockerEnv, ...cfg.environments.docker };
247
+ return { ...dockerEnv, ...cfg.environments.docker };
243
248
  }
244
249
  }
245
250
  } catch {
246
251
  // Ignore config.yaml read errors, continue with env-config values
247
252
  }
253
+ return dockerEnv;
254
+ }
255
+
256
+ /**
257
+ * Gets container port from variables.yaml
258
+ * @function getContainerPortFromVariables
259
+ * @param {string} variablesPath - Path to variables.yaml
260
+ * @returns {number|null} Container port or null
261
+ */
262
+ function getContainerPortFromVariables(variablesPath) {
263
+ if (!variablesPath || !fs.existsSync(variablesPath)) {
264
+ return null;
265
+ }
266
+ try {
267
+ const variablesContent = fs.readFileSync(variablesPath, 'utf8');
268
+ const variables = yaml.load(variablesContent);
269
+ // Use containerPort if specified, otherwise use base port (no developer-id offset)
270
+ return variables?.build?.containerPort || variables?.port || null;
271
+ } catch {
272
+ return null;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Gets container port from docker environment config
278
+ * @function getContainerPortFromDockerEnv
279
+ * @param {Object} dockerEnv - Docker environment config
280
+ * @returns {number} Container port (defaults to 3000)
281
+ */
282
+ function getContainerPortFromDockerEnv(dockerEnv) {
283
+ if (dockerEnv.PORT === undefined || dockerEnv.PORT === null) {
284
+ return 3000;
285
+ }
286
+ const portVal = typeof dockerEnv.PORT === 'number' ? dockerEnv.PORT : parseInt(dockerEnv.PORT, 10);
287
+ return Number.isNaN(portVal) ? 3000 : portVal;
288
+ }
289
+
290
+ /**
291
+ * Updates PORT in resolved content for docker environment
292
+ * Sets PORT to container port (build.containerPort or port from variables.yaml)
293
+ * NOT the host port (which includes developer-id offset)
294
+ * @async
295
+ * @function updatePortForDocker
296
+ * @param {string} resolved - Resolved environment content
297
+ * @param {string} variablesPath - Path to variables.yaml file
298
+ * @returns {Promise<string>} Updated content with PORT set
299
+ */
300
+ async function updatePortForDocker(resolved, variablesPath) {
301
+ // Step 1: Get base config from env-config.yaml
302
+ let dockerEnv = await getBaseDockerEnv();
303
+
304
+ // Step 2: Apply config.yaml → environments.docker override (if exists)
305
+ dockerEnv = applyDockerEnvOverride(dockerEnv);
248
306
 
249
307
  // Step 3: Get PORT value for container (should be container port, NOT host port)
250
- // For Docker containers, PORT should be the container port (build.containerPort or port)
251
- // NOT the host port (which includes developer-id offset)
252
- let containerPort = null;
253
- if (variablesPath && fs.existsSync(variablesPath)) {
254
- try {
255
- const variablesContent = fs.readFileSync(variablesPath, 'utf8');
256
- const variables = yaml.load(variablesContent);
257
- // Use containerPort if specified, otherwise use base port (no developer-id offset)
258
- containerPort = variables?.build?.containerPort || variables?.port || 3000;
259
- } catch {
260
- // Fallback to default
261
- containerPort = 3000;
262
- }
263
- } else {
264
- // Fallback: check dockerEnv.PORT (but this should be container port, not host port)
265
- if (dockerEnv.PORT !== undefined && dockerEnv.PORT !== null) {
266
- const portVal = typeof dockerEnv.PORT === 'number' ? dockerEnv.PORT : parseInt(dockerEnv.PORT, 10);
267
- if (!Number.isNaN(portVal)) {
268
- containerPort = portVal;
269
- }
270
- }
271
- if (containerPort === null || containerPort === undefined) {
272
- containerPort = 3000;
273
- }
308
+ let containerPort = getContainerPortFromVariables(variablesPath);
309
+ if (containerPort === null) {
310
+ containerPort = getContainerPortFromDockerEnv(dockerEnv);
274
311
  }
275
312
 
276
313
  // PORT in container should be the container port (no developer-id adjustment)
@@ -294,7 +331,7 @@ async function applyEnvironmentTransformations(resolved, environment, variablesP
294
331
  // Interpolate ${VAR} references created by rewriteInfraEndpoints
295
332
  // Get the actual host and port values from env-endpoints.js directly
296
333
  // to ensure they are correctly populated in envVars for interpolation
297
- const { getEnvHosts, getServiceHost, getServicePort, getLocalhostOverride } = require('./utils/env-endpoints');
334
+ const { getEnvHosts, getServiceHost, getServicePort, getLocalhostOverride } = require('../utils/env-endpoints');
298
335
  const hosts = await getEnvHosts('docker');
299
336
  const localhostOverride = getLocalhostOverride('docker');
300
337
  const redisHost = getServiceHost(hosts.REDIS_HOST, 'docker', 'redis', localhostOverride);