@aifabrix/builder 2.39.3 → 2.40.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.
Files changed (114) hide show
  1. package/.cursor/rules/project-rules.mdc +6 -6
  2. package/README.md +2 -2
  3. package/babel.config.js +6 -0
  4. package/integration/hubspot/README.md +53 -141
  5. package/integration/hubspot/application.yaml +37 -0
  6. package/integration/hubspot/env.template +2 -11
  7. package/integration/hubspot/hubspot-deploy.json +1 -0
  8. package/integration/hubspot/test.js +5 -5
  9. package/lib/api/credentials.api.js +5 -5
  10. package/lib/api/deployments.api.js +2 -2
  11. package/lib/api/pipeline.api.js +17 -17
  12. package/lib/api/wizard.api.js +2 -2
  13. package/lib/app/config.js +11 -6
  14. package/lib/app/deploy-config.js +13 -16
  15. package/lib/app/deploy.js +29 -22
  16. package/lib/app/display.js +1 -1
  17. package/lib/app/dockerfile.js +11 -12
  18. package/lib/app/helpers.js +51 -13
  19. package/lib/app/index.js +14 -2
  20. package/lib/app/prompts.js +37 -45
  21. package/lib/app/push.js +8 -11
  22. package/lib/app/readme.js +16 -12
  23. package/lib/app/register.js +1 -1
  24. package/lib/app/run-helpers.js +31 -22
  25. package/lib/app/run.js +44 -5
  26. package/lib/app/show-display.js +104 -44
  27. package/lib/app/show.js +123 -43
  28. package/lib/build/index.js +11 -18
  29. package/lib/cli/setup-app.js +36 -29
  30. package/lib/cli/setup-auth.js +18 -15
  31. package/lib/cli/setup-credential-deployment.js +3 -1
  32. package/lib/cli/setup-external-system.js +35 -16
  33. package/lib/cli/setup-infra.js +45 -23
  34. package/lib/cli/setup-utility.js +79 -31
  35. package/lib/commands/app-logs.js +28 -20
  36. package/lib/commands/app.js +30 -26
  37. package/lib/commands/convert.js +202 -0
  38. package/lib/commands/credential-list.js +78 -17
  39. package/lib/commands/datasource.js +24 -24
  40. package/lib/commands/deployment-list.js +13 -6
  41. package/lib/commands/up-common.js +80 -42
  42. package/lib/commands/up-dataplane.js +15 -14
  43. package/lib/commands/up-miso.js +15 -14
  44. package/lib/commands/upload.js +163 -0
  45. package/lib/commands/wizard-core.js +5 -4
  46. package/lib/core/diff.js +84 -9
  47. package/lib/core/key-generator.js +9 -12
  48. package/lib/core/secrets-docker-env.js +2 -2
  49. package/lib/core/secrets.js +3 -2
  50. package/lib/core/templates.js +2 -2
  51. package/lib/datasource/deploy.js +2 -1
  52. package/lib/deployment/deployer.js +76 -48
  53. package/lib/external-system/delete.js +0 -1
  54. package/lib/external-system/deploy-helpers.js +5 -6
  55. package/lib/external-system/deploy.js +7 -2
  56. package/lib/external-system/download-helpers.js +4 -4
  57. package/lib/external-system/download.js +11 -10
  58. package/lib/external-system/generator.js +19 -17
  59. package/lib/external-system/test.js +10 -15
  60. package/lib/generator/builders.js +1 -1
  61. package/lib/generator/external-controller-manifest.js +26 -29
  62. package/lib/generator/external-schema-utils.js +6 -18
  63. package/lib/generator/external.js +32 -27
  64. package/lib/generator/github.js +1 -1
  65. package/lib/generator/helpers.js +12 -19
  66. package/lib/generator/index.js +15 -15
  67. package/lib/generator/parse-image.js +35 -0
  68. package/lib/generator/split-readme.js +105 -0
  69. package/lib/generator/split-variables.js +149 -0
  70. package/lib/generator/split.js +86 -246
  71. package/lib/generator/wizard.js +46 -69
  72. package/lib/schema/application-schema.json +4 -4
  73. package/lib/schema/external-datasource.schema.json +5 -0
  74. package/lib/schema/external-system.schema.json +10 -0
  75. package/lib/utils/app-config-resolver.js +52 -0
  76. package/lib/utils/app-register-api.js +1 -1
  77. package/lib/utils/app-register-auth.js +1 -1
  78. package/lib/utils/app-register-config.js +16 -23
  79. package/lib/utils/app-register-validator.js +2 -2
  80. package/lib/utils/cli-utils.js +47 -3
  81. package/lib/utils/config-format.js +154 -0
  82. package/lib/utils/config-paths.js +19 -52
  83. package/lib/utils/config-tokens.js +1 -0
  84. package/lib/utils/docker-build.js +71 -94
  85. package/lib/utils/dockerfile-utils.js +1 -1
  86. package/lib/utils/env-copy.js +4 -4
  87. package/lib/utils/env-ports.js +2 -2
  88. package/lib/utils/error-formatter.js +1 -1
  89. package/lib/utils/error-formatters/validation-errors.js +1 -1
  90. package/lib/utils/external-readme.js +12 -5
  91. package/lib/utils/external-system-test-helpers.js +2 -0
  92. package/lib/utils/health-check.js +55 -66
  93. package/lib/utils/image-version.js +12 -21
  94. package/lib/utils/paths.js +39 -66
  95. package/lib/utils/port-resolver.js +8 -8
  96. package/lib/utils/schema-loader.js +22 -0
  97. package/lib/utils/schema-resolver.js +23 -33
  98. package/lib/utils/secrets-helpers.js +7 -7
  99. package/lib/utils/secrets-utils.js +10 -12
  100. package/lib/utils/template-helpers.js +13 -13
  101. package/lib/utils/token-manager.js +20 -2
  102. package/lib/utils/variable-transformer.js +2 -2
  103. package/lib/validation/validate-display.js +3 -4
  104. package/lib/validation/validate.js +33 -27
  105. package/lib/validation/validator.js +50 -30
  106. package/package.json +2 -1
  107. package/templates/README.md +1 -1
  108. package/templates/applications/README.md.hbs +3 -3
  109. package/templates/applications/miso-controller/env.template +3 -1
  110. package/templates/external-system/README.md.hbs +4 -4
  111. package/integration/hubspot/variables.yaml +0 -17
  112. /package/templates/applications/dataplane/{variables.yaml → application.yaml} +0 -0
  113. /package/templates/applications/keycloak/{variables.yaml → application.yaml} +0 -0
  114. /package/templates/applications/miso-controller/{variables.yaml → application.yaml} +0 -0
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Application config path resolution
3
+ *
4
+ * Single entry point for resolving path to application config file
5
+ * (application.yaml, application.json, or legacy variables.yaml).
6
+ *
7
+ * @fileoverview Resolve application config file path with legacy migration
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+
17
+ /**
18
+ * Resolves path to application config file (application.yaml, application.json, or legacy variables.yaml).
19
+ * If only variables.yaml exists, renames it to application.yaml and returns the new path.
20
+ *
21
+ * @param {string} appPath - Absolute path to application directory
22
+ * @returns {string} Absolute path to application config file
23
+ * @throws {Error} If no config file found
24
+ */
25
+ function resolveApplicationConfigPath(appPath) {
26
+ if (!appPath || typeof appPath !== 'string') {
27
+ throw new Error('App path is required and must be a string');
28
+ }
29
+ const applicationYaml = path.join(appPath, 'application.yaml');
30
+ const applicationYml = path.join(appPath, 'application.yml');
31
+ const applicationJson = path.join(appPath, 'application.json');
32
+ const variablesYaml = path.join(appPath, 'variables.yaml');
33
+
34
+ if (fs.existsSync(applicationYaml)) {
35
+ return applicationYaml;
36
+ }
37
+ if (fs.existsSync(applicationYml)) {
38
+ return applicationYml;
39
+ }
40
+ if (fs.existsSync(applicationJson)) {
41
+ return applicationJson;
42
+ }
43
+ if (fs.existsSync(variablesYaml)) {
44
+ fs.renameSync(variablesYaml, applicationYaml);
45
+ return applicationYaml;
46
+ }
47
+ throw new Error(
48
+ `Application config not found in ${appPath}. Expected application.yaml, application.yml, application.json, or variables.yaml.`
49
+ );
50
+ }
51
+
52
+ module.exports = { resolveApplicationConfigPath };
@@ -39,7 +39,7 @@ function handleRegistrationError(response, apiUrl, registrationData) {
39
39
  logger.error(chalk.gray('\nRequest payload:'));
40
40
  logger.error(chalk.gray(JSON.stringify(registrationData, null, 2)));
41
41
  logger.error('');
42
- logger.error(chalk.gray('Check your variables.yaml file and ensure all required fields are correctly set.'));
42
+ logger.error(chalk.gray('Check your application.yaml file and ensure all required fields are correctly set.'));
43
43
  }
44
44
 
45
45
  process.exit(1);
@@ -82,7 +82,7 @@ async function findDeviceTokenFromConfig(deviceConfig, attemptedUrls) {
82
82
  /**
83
83
  * Check if user is authenticated and get token
84
84
  * @async
85
- * @param {string} [controllerUrl] - Optional controller URL from variables.yaml or --controller flag
85
+ * @param {string} [controllerUrl] - Optional controller URL from application.yaml or --controller flag
86
86
  * @param {string} [environment] - Optional environment key
87
87
  * @returns {Promise<{apiUrl: string, token: string, controllerUrl: string}>} Configuration with API URL, token, and controller URL
88
88
  */
@@ -8,37 +8,35 @@
8
8
  * @version 2.0.0
9
9
  */
10
10
 
11
- const fs = require('fs').promises;
12
11
  const path = require('path');
13
12
  const chalk = require('chalk');
14
- const yaml = require('js-yaml');
15
13
  const logger = require('./logger');
16
- const { detectAppType } = require('./paths');
14
+ const { detectAppType, resolveApplicationConfigPath } = require('./paths');
15
+ const { loadConfigFile } = require('./config-format');
17
16
  const { getContainerPort, getLocalPort } = require('./port-resolver');
18
17
 
19
18
  // createApp is imported dynamically in createMinimalAppIfNeeded to handle test mocking
20
19
 
21
20
  /**
22
- * Load variables.yaml file for an application
21
+ * Load application config for an application (application.yaml, application.json, or legacy).
23
22
  * @async
24
23
  * @param {string} appKey - Application key
25
24
  * @returns {Promise<{variables: Object, created: boolean}>} Variables and creation flag
26
25
  */
27
26
  async function loadVariablesYaml(appKey) {
28
- // Detect app type and get correct path (integration or builder)
29
27
  const { appPath } = await detectAppType(appKey);
30
- const variablesPath = path.join(appPath, 'variables.yaml');
31
-
32
28
  try {
33
- const variablesContent = await fs.readFile(variablesPath, 'utf-8');
34
- return { variables: yaml.load(variablesContent), created: false };
29
+ const configPath = resolveApplicationConfigPath(appPath);
30
+ const variables = loadConfigFile(configPath);
31
+ return { variables, created: false };
35
32
  } catch (error) {
36
- if (error.code === 'ENOENT') {
37
- logger.log(chalk.yellow(`⚠️ variables.yaml not found for ${appKey}`));
33
+ const isNotFound = error.code === 'ENOENT' || (error.message && error.message.includes('not found'));
34
+ if (isNotFound) {
35
+ logger.log(chalk.yellow(`⚠️ Application config not found for ${appKey}`));
38
36
  logger.log(chalk.yellow('📝 Creating minimal configuration...\n'));
39
37
  return { variables: null, created: true };
40
38
  }
41
- throw new Error(`Failed to read variables.yaml: ${error.message}`);
39
+ throw new Error(`Failed to read application config: ${error.message}`);
42
40
  }
43
41
  }
44
42
 
@@ -65,21 +63,19 @@ async function createMinimalAppIfNeeded(appKey, options) {
65
63
  authentication: false
66
64
  });
67
65
 
68
- // Detect app type and get correct path (integration or builder)
69
66
  const appTypeResult = await detectAppType(appKey);
70
67
  if (!appTypeResult || !appTypeResult.appPath) {
71
68
  throw new Error('Failed to detect app type after creation');
72
69
  }
73
70
  const { appPath } = appTypeResult;
74
- const variablesPath = path.join(appPath, 'variables.yaml');
75
- const variablesContent = await fs.readFile(variablesPath, 'utf-8');
76
- return yaml.load(variablesContent);
71
+ const configPath = resolveApplicationConfigPath(appPath);
72
+ return loadConfigFile(configPath);
77
73
  }
78
74
 
79
75
  /**
80
76
  * Builds image reference string from variables
81
77
  * Format: repository:tag (e.g., aifabrix/miso-controller:latest or myregistry.azurecr.io/miso-controller:v1.0.0)
82
- * @param {Object} variables - Variables from YAML file
78
+ * @param {Object} variables - Variables from application config
83
79
  * @param {string} appKey - Application key (fallback)
84
80
  * @returns {string} Image reference string
85
81
  */
@@ -94,7 +90,7 @@ function buildImageReference(variables, appKey) {
94
90
  * Extract URL from external system JSON file for registration
95
91
  * @async
96
92
  * @param {string} appKey - Application key
97
- * @param {Object} externalIntegration - External integration config from variables.yaml
93
+ * @param {Object} externalIntegration - External integration config from application config
98
94
  * @returns {Promise<{url: string, apiKey?: string}>} URL and optional API key
99
95
  */
100
96
  /**
@@ -171,12 +167,9 @@ async function extractExternalIntegrationUrl(appKey, externalIntegration) {
171
167
  const systemFilePath = resolveSystemFilePath(appPath, schemaBasePath, systemFileName);
172
168
 
173
169
  try {
174
- const systemContent = await fs.readFile(systemFilePath, 'utf-8');
175
- const systemJson = JSON.parse(systemContent);
176
-
170
+ const systemJson = loadConfigFile(systemFilePath);
177
171
  const url = extractUrlFromSystemJson(systemJson, systemFileName);
178
172
  const apiKey = extractApiKeyFromSystemJson(systemJson);
179
-
180
173
  return { url, apiKey };
181
174
  } catch (error) {
182
175
  handleFileReadError(error, systemFilePath);
@@ -184,7 +177,7 @@ async function extractExternalIntegrationUrl(appKey, externalIntegration) {
184
177
  }
185
178
 
186
179
  /**
187
- * Extract application configuration from variables.yaml
180
+ * Extract application configuration from application config
188
181
  * @async
189
182
  * @param {Object} variables - Variables from YAML file
190
183
  * @param {string} appKey - Application key
@@ -114,12 +114,12 @@ async function validateAppRegistrationData(config, originalAppKey) {
114
114
  if (!config.displayName) missingFields.push('app.name');
115
115
 
116
116
  if (missingFields.length > 0) {
117
- logger.error(chalk.red('❌ Missing required fields in variables.yaml:'));
117
+ logger.error(chalk.red('❌ Missing required fields in application.yaml:'));
118
118
  missingFields.forEach(field => logger.error(chalk.red(` - ${field}`)));
119
119
  // Detect app type to show correct path
120
120
  const { appPath } = await detectAppType(originalAppKey);
121
121
  const relativePath = path.relative(process.cwd(), appPath);
122
- logger.error(chalk.red(`\n Please update ${relativePath}/variables.yaml and try again.`));
122
+ logger.error(chalk.red(`\n Please update ${relativePath}/application.yaml and try again.`));
123
123
  process.exit(1);
124
124
  }
125
125
 
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  const path = require('path');
12
+ const chalk = require('chalk');
12
13
  const fs = require('fs').promises;
13
14
  const logger = require('./logger');
14
15
 
@@ -85,6 +86,19 @@ function isPermissionDeniedError(errorMsg) {
85
86
  !errorMsg.includes('Field "permissions');
86
87
  }
87
88
 
89
+ /**
90
+ * Checks if permission denied error is Docker-related (daemon socket / CLI), not API auth.
91
+ * Used to avoid showing Docker hints when the error is from Controller/Dataplane "Permission denied".
92
+ * @function isDockerPermissionDeniedError
93
+ * @param {string} errorMsg - Error message
94
+ * @returns {boolean} True if Docker permission denied error
95
+ */
96
+ function isDockerPermissionDeniedError(errorMsg) {
97
+ if (!isPermissionDeniedError(errorMsg)) return false;
98
+ const lower = errorMsg.toLowerCase();
99
+ return lower.includes('docker') || lower.includes('socket') || errorMsg.includes('EACCES');
100
+ }
101
+
88
102
  /**
89
103
  * Format Docker-related errors
90
104
  * @param {string} errorMsg - Error message
@@ -109,7 +123,7 @@ function formatDockerError(errorMsg) {
109
123
  ' Run "aifabrix doctor" to check which ports are in use.'
110
124
  ];
111
125
  }
112
- if (isPermissionDeniedError(errorMsg)) {
126
+ if (isDockerPermissionDeniedError(errorMsg)) {
113
127
  return [
114
128
  ' Permission denied.',
115
129
  ' Make sure you have the necessary permissions to run Docker commands.'
@@ -149,7 +163,7 @@ function formatAzureError(errorMsg) {
149
163
  if (errorMsg.includes('Registry URL is required')) {
150
164
  return [
151
165
  ' Registry URL is required.',
152
- ' Provide via --registry flag or configure in variables.yaml under image.registry'
166
+ ' Provide via --registry flag or configure in application.yaml under image.registry'
153
167
  ];
154
168
  }
155
169
  return null;
@@ -222,6 +236,21 @@ function formatValidationError(errorMsg) {
222
236
  return null;
223
237
  }
224
238
 
239
+ /**
240
+ * Format API/Controller/Dataplane permission errors (403-style "Permission denied").
241
+ * Keeps the real message and adds a hint; avoids mis-classifying as Docker.
242
+ * @param {string} errorMsg - Error message
243
+ * @returns {string[]|null} Array of error message lines or null if not an API permission error
244
+ */
245
+ function formatApiPermissionError(errorMsg) {
246
+ if (!isPermissionDeniedError(errorMsg)) return null;
247
+ if (isDockerPermissionDeniedError(errorMsg)) return null;
248
+ return [
249
+ ` ${errorMsg}`,
250
+ ' Ensure your token has the required permission (e.g. external-system:delete for delete).'
251
+ ];
252
+ }
253
+
225
254
  /**
226
255
  * Formats error message based on error type
227
256
  * @function formatError
@@ -236,6 +265,7 @@ function formatValidationError(errorMsg) {
236
265
  */
237
266
  function tryFormatErrorWithFormatters(errorMsg) {
238
267
  const formatters = [
268
+ formatApiPermissionError,
239
269
  formatDockerError,
240
270
  formatAzureError,
241
271
  formatSecretsError,
@@ -283,6 +313,19 @@ function logError(command, errorMessages) {
283
313
  logger.error('\n💡 Run "aifabrix doctor" for environment diagnostics.\n');
284
314
  }
285
315
 
316
+ /**
317
+ * Logs the resolved app path so the user can see which directory (integration/<app> or builder/<app>) is used.
318
+ * Path resolution order is always integration first, then builder; no CLI flag overrides this.
319
+ *
320
+ * @param {string} appPath - Resolved application directory path
321
+ * @param {Object} [_options] - Reserved for backward compatibility; ignored
322
+ */
323
+ function logOfflinePathWhenType(appPath, options) {
324
+ if (!appPath || !options || (options.type !== 'app' && options.type !== 'external')) return;
325
+ const displayPath = path.relative(process.cwd(), appPath) || appPath;
326
+ logger.log(chalk.gray(`Using: ${displayPath}`));
327
+ }
328
+
286
329
  /**
287
330
  * Handles command errors with user-friendly messages
288
331
  * @param {Error} error - The error that occurred
@@ -332,6 +375,7 @@ async function appendWizardError(appKey, error) {
332
375
  module.exports = {
333
376
  validateCommand,
334
377
  handleCommandError,
335
- appendWizardError
378
+ appendWizardError,
379
+ logOfflinePathWhenType
336
380
  };
337
381
 
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Config Format Converter Layer
3
+ *
4
+ * Single place for YAML/JSON config I/O. All config loaders and writers use this
5
+ * layer; internal code works with plain JS objects and JSON Schema only.
6
+ *
7
+ * @fileoverview Config format conversion (YAML/JSON) at I/O boundary
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const yaml = require('js-yaml');
17
+
18
+ const YAML_EXTENSIONS = ['.yaml', '.yml'];
19
+ const JSON_EXTENSIONS = ['.json'];
20
+
21
+ const DEFAULT_YAML_OPTIONS = { indent: 2, lineWidth: 120, noRefs: true };
22
+
23
+ /**
24
+ * Parses YAML string to plain JS object (same shape as JSON).
25
+ * Used when reading .yaml / .yml files.
26
+ *
27
+ * @param {string} content - YAML string content
28
+ * @returns {Object} Plain JS object
29
+ * @throws {Error} If YAML syntax is invalid
30
+ */
31
+ function yamlToJson(content) {
32
+ if (typeof content !== 'string') {
33
+ throw new Error('yamlToJson expects a string');
34
+ }
35
+ try {
36
+ const parsed = yaml.load(content);
37
+ return parsed === undefined || parsed === null ? {} : parsed;
38
+ } catch (error) {
39
+ throw new Error(`Invalid YAML syntax: ${error.message}`);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Serializes JS object to YAML string.
45
+ * Used when writing human-editable config as YAML.
46
+ *
47
+ * @param {Object} object - Plain JS object (config)
48
+ * @param {Object} [options] - js-yaml dump options
49
+ * @returns {string} YAML string
50
+ */
51
+ function jsonToYaml(object, options = {}) {
52
+ if (object === undefined || object === null) {
53
+ return '';
54
+ }
55
+ const opts = { ...DEFAULT_YAML_OPTIONS, ...options };
56
+ return yaml.dump(object, opts);
57
+ }
58
+
59
+ /**
60
+ * Returns whether the file path is treated as YAML by extension.
61
+ *
62
+ * @param {string} filePath - File path
63
+ * @returns {boolean} True if .yaml or .yml
64
+ */
65
+ function isYamlPath(filePath) {
66
+ const ext = path.extname(filePath).toLowerCase();
67
+ return YAML_EXTENSIONS.includes(ext);
68
+ }
69
+
70
+ /**
71
+ * Returns whether the file path is treated as JSON by extension.
72
+ *
73
+ * @param {string} filePath - File path
74
+ * @returns {boolean} True if .json
75
+ */
76
+ function isJsonPath(filePath) {
77
+ const ext = path.extname(filePath).toLowerCase();
78
+ return JSON_EXTENSIONS.includes(ext);
79
+ }
80
+
81
+ /**
82
+ * Reads config file at path; by extension uses yamlToJson or JSON.parse.
83
+ * Single entry point for "read config file regardless of format".
84
+ *
85
+ * @param {string} filePath - Absolute path to config file
86
+ * @returns {Object} Parsed config object
87
+ * @throws {Error} If file not found, unreadable, or invalid format
88
+ */
89
+ function loadConfigFile(filePath) {
90
+ if (!filePath || typeof filePath !== 'string') {
91
+ throw new Error('loadConfigFile requires a non-empty file path');
92
+ }
93
+ if (!fs.existsSync(filePath)) {
94
+ throw new Error(`Config file not found: ${filePath}`);
95
+ }
96
+ const content = fs.readFileSync(filePath, 'utf8');
97
+ const ext = path.extname(filePath).toLowerCase();
98
+ if (YAML_EXTENSIONS.includes(ext)) {
99
+ return yamlToJson(content);
100
+ }
101
+ if (JSON_EXTENSIONS.includes(ext)) {
102
+ try {
103
+ const parsed = JSON.parse(content);
104
+ return parsed === undefined || parsed === null ? {} : parsed;
105
+ } catch (error) {
106
+ throw new Error(`Invalid JSON syntax in ${path.basename(filePath)}: ${error.message}`);
107
+ }
108
+ }
109
+ throw new Error(`Unsupported config file extension: ${ext}. Use .yaml, .yml, or .json`);
110
+ }
111
+
112
+ /**
113
+ * Writes config object to path as YAML or JSON based on format or path extension.
114
+ *
115
+ * @param {string} filePath - Absolute path to write (extension determines format if format omitted)
116
+ * @param {Object} object - Config object to write
117
+ * @param {string} [format] - 'yaml' or 'json'; if omitted, inferred from filePath extension
118
+ * @throws {Error} If format is invalid or write fails
119
+ */
120
+ function writeConfigFile(filePath, object, format) {
121
+ if (!filePath || typeof filePath !== 'string') {
122
+ throw new Error('writeConfigFile requires a non-empty file path');
123
+ }
124
+ let targetFormat = format;
125
+ if (!targetFormat) {
126
+ const ext = path.extname(filePath).toLowerCase();
127
+ if (YAML_EXTENSIONS.includes(ext)) {
128
+ targetFormat = 'yaml';
129
+ } else if (JSON_EXTENSIONS.includes(ext)) {
130
+ targetFormat = 'json';
131
+ } else {
132
+ throw new Error(`Cannot infer format from path ${filePath}. Use .yaml, .yml, or .json, or pass format.`);
133
+ }
134
+ }
135
+ const normalized = targetFormat.toLowerCase();
136
+ let content;
137
+ if (normalized === 'yaml' || normalized === 'yml') {
138
+ content = jsonToYaml(object);
139
+ } else if (normalized === 'json') {
140
+ content = JSON.stringify(object, null, 2);
141
+ } else {
142
+ throw new Error(`Invalid format: ${format}. Use 'yaml' or 'json'.`);
143
+ }
144
+ fs.writeFileSync(filePath, content, 'utf8');
145
+ }
146
+
147
+ module.exports = {
148
+ yamlToJson,
149
+ jsonToYaml,
150
+ loadConfigFile,
151
+ writeConfigFile,
152
+ isYamlPath,
153
+ isJsonPath
154
+ };
@@ -41,77 +41,31 @@ async function setPathConfig(getConfigFn, saveConfigFn, key, value, errorMsg) {
41
41
  await saveConfigFn(config);
42
42
  }
43
43
 
44
- /**
45
- * Create path configuration functions with config access
46
- * @param {Function} getConfigFn - Function to get config
47
- * @param {Function} saveConfigFn - Function to save config
48
- * @returns {Object} Path configuration functions
49
- */
50
- function createPathConfigFunctions(getConfigFn, saveConfigFn) {
44
+ function createHomeAndSecretsPathFunctions(getConfigFn, saveConfigFn) {
51
45
  return {
52
- /**
53
- * Get aifabrix-home override path
54
- * @async
55
- * @returns {Promise<string|null>} Home path or null
56
- */
57
46
  async getAifabrixHomeOverride() {
58
47
  return getPathConfig(getConfigFn, 'aifabrix-home');
59
48
  },
60
-
61
- /**
62
- * Set aifabrix-home override path
63
- * @async
64
- * @param {string} homePath - Home path
65
- * @returns {Promise<void>}
66
- */
67
49
  async setAifabrixHomeOverride(homePath) {
68
50
  await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-home', homePath, 'Home path is required and must be a string');
69
51
  },
70
-
71
- /**
72
- * Get aifabrix-secrets path
73
- * @async
74
- * @returns {Promise<string|null>} Secrets path or null
75
- */
76
52
  async getAifabrixSecretsPath() {
77
53
  return getPathConfig(getConfigFn, 'aifabrix-secrets');
78
54
  },
79
-
80
- /**
81
- * Set aifabrix-secrets path
82
- * @async
83
- * @param {string} secretsPath - Secrets path
84
- * @returns {Promise<void>}
85
- */
86
55
  async setAifabrixSecretsPath(secretsPath) {
87
56
  await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-secrets', secretsPath, 'Secrets path is required and must be a string');
88
- },
57
+ }
58
+ };
59
+ }
89
60
 
90
- /**
91
- * Get aifabrix-env-config path
92
- * @async
93
- * @returns {Promise<string|null>} Env config path or null
94
- */
61
+ function createEnvConfigPathFunctions(getConfigFn, saveConfigFn) {
62
+ return {
95
63
  async getAifabrixEnvConfigPath() {
96
64
  return getPathConfig(getConfigFn, 'aifabrix-env-config');
97
65
  },
98
-
99
- /**
100
- * Set aifabrix-env-config path
101
- * @async
102
- * @param {string} envConfigPath - Env config path
103
- * @returns {Promise<void>}
104
- */
105
66
  async setAifabrixEnvConfigPath(envConfigPath) {
106
67
  await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-env-config', envConfigPath, 'Env config path is required and must be a string');
107
68
  },
108
-
109
- /**
110
- * Get builder root directory (dirname of aifabrix-env-config when set).
111
- * When set, app dirs and generated .env use this instead of cwd/builder.
112
- * @async
113
- * @returns {Promise<string|null>} Builder root path or null to use cwd/builder
114
- */
115
69
  async getAifabrixBuilderDir() {
116
70
  const envConfigPath = await getPathConfig(getConfigFn, 'aifabrix-env-config');
117
71
  return envConfigPath && typeof envConfigPath === 'string' ? path.dirname(envConfigPath) : null;
@@ -119,6 +73,19 @@ function createPathConfigFunctions(getConfigFn, saveConfigFn) {
119
73
  };
120
74
  }
121
75
 
76
+ /**
77
+ * Create path configuration functions with config access
78
+ * @param {Function} getConfigFn - Function to get config
79
+ * @param {Function} saveConfigFn - Function to save config
80
+ * @returns {Object} Path configuration functions
81
+ */
82
+ function createPathConfigFunctions(getConfigFn, saveConfigFn) {
83
+ return {
84
+ ...createHomeAndSecretsPathFunctions(getConfigFn, saveConfigFn),
85
+ ...createEnvConfigPathFunctions(getConfigFn, saveConfigFn)
86
+ };
87
+ }
88
+
122
89
  module.exports = {
123
90
  getPathConfig,
124
91
  setPathConfig,
@@ -39,6 +39,7 @@ function normalizeControllerUrl(url) {
39
39
  * @param {Function} params.isTokenEncryptedFn - Function to check if token is encrypted
40
40
  * @returns {Object} Token management functions
41
41
  */
42
+ /* eslint-disable max-lines-per-function -- factory returns many bound helpers; splitting would break encapsulation */
42
43
  function createTokenManagementFunctions({
43
44
  getConfigFn,
44
45
  saveConfigFn,