@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.
- package/.cursor/rules/project-rules.mdc +6 -6
- package/README.md +2 -2
- package/babel.config.js +6 -0
- package/integration/hubspot/README.md +53 -141
- package/integration/hubspot/application.yaml +37 -0
- package/integration/hubspot/env.template +2 -11
- package/integration/hubspot/hubspot-deploy.json +1 -0
- package/integration/hubspot/test.js +5 -5
- package/lib/api/credentials.api.js +5 -5
- package/lib/api/deployments.api.js +2 -2
- package/lib/api/pipeline.api.js +17 -17
- package/lib/api/wizard.api.js +2 -2
- package/lib/app/config.js +11 -6
- package/lib/app/deploy-config.js +13 -16
- package/lib/app/deploy.js +29 -22
- package/lib/app/display.js +1 -1
- package/lib/app/dockerfile.js +11 -12
- package/lib/app/helpers.js +51 -13
- package/lib/app/index.js +14 -2
- package/lib/app/prompts.js +37 -45
- package/lib/app/push.js +8 -11
- package/lib/app/readme.js +16 -12
- package/lib/app/register.js +1 -1
- package/lib/app/run-helpers.js +31 -22
- package/lib/app/run.js +44 -5
- package/lib/app/show-display.js +104 -44
- package/lib/app/show.js +123 -43
- package/lib/build/index.js +11 -18
- package/lib/cli/setup-app.js +36 -29
- package/lib/cli/setup-auth.js +18 -15
- package/lib/cli/setup-credential-deployment.js +3 -1
- package/lib/cli/setup-external-system.js +35 -16
- package/lib/cli/setup-infra.js +45 -23
- package/lib/cli/setup-utility.js +79 -31
- package/lib/commands/app-logs.js +28 -20
- package/lib/commands/app.js +30 -26
- package/lib/commands/convert.js +202 -0
- package/lib/commands/credential-list.js +78 -17
- package/lib/commands/datasource.js +24 -24
- package/lib/commands/deployment-list.js +13 -6
- package/lib/commands/up-common.js +80 -42
- package/lib/commands/up-dataplane.js +15 -14
- package/lib/commands/up-miso.js +15 -14
- package/lib/commands/upload.js +163 -0
- package/lib/commands/wizard-core.js +5 -4
- package/lib/core/diff.js +84 -9
- package/lib/core/key-generator.js +9 -12
- package/lib/core/secrets-docker-env.js +2 -2
- package/lib/core/secrets.js +3 -2
- package/lib/core/templates.js +2 -2
- package/lib/datasource/deploy.js +2 -1
- package/lib/deployment/deployer.js +76 -48
- package/lib/external-system/delete.js +0 -1
- package/lib/external-system/deploy-helpers.js +5 -6
- package/lib/external-system/deploy.js +7 -2
- package/lib/external-system/download-helpers.js +4 -4
- package/lib/external-system/download.js +11 -10
- package/lib/external-system/generator.js +19 -17
- package/lib/external-system/test.js +10 -15
- package/lib/generator/builders.js +1 -1
- package/lib/generator/external-controller-manifest.js +26 -29
- package/lib/generator/external-schema-utils.js +6 -18
- package/lib/generator/external.js +32 -27
- package/lib/generator/github.js +1 -1
- package/lib/generator/helpers.js +12 -19
- package/lib/generator/index.js +15 -15
- package/lib/generator/parse-image.js +35 -0
- package/lib/generator/split-readme.js +105 -0
- package/lib/generator/split-variables.js +149 -0
- package/lib/generator/split.js +86 -246
- package/lib/generator/wizard.js +46 -69
- package/lib/schema/application-schema.json +4 -4
- package/lib/schema/external-datasource.schema.json +5 -0
- package/lib/schema/external-system.schema.json +10 -0
- package/lib/utils/app-config-resolver.js +52 -0
- package/lib/utils/app-register-api.js +1 -1
- package/lib/utils/app-register-auth.js +1 -1
- package/lib/utils/app-register-config.js +16 -23
- package/lib/utils/app-register-validator.js +2 -2
- package/lib/utils/cli-utils.js +47 -3
- package/lib/utils/config-format.js +154 -0
- package/lib/utils/config-paths.js +19 -52
- package/lib/utils/config-tokens.js +1 -0
- package/lib/utils/docker-build.js +71 -94
- package/lib/utils/dockerfile-utils.js +1 -1
- package/lib/utils/env-copy.js +4 -4
- package/lib/utils/env-ports.js +2 -2
- package/lib/utils/error-formatter.js +1 -1
- package/lib/utils/error-formatters/validation-errors.js +1 -1
- package/lib/utils/external-readme.js +12 -5
- package/lib/utils/external-system-test-helpers.js +2 -0
- package/lib/utils/health-check.js +55 -66
- package/lib/utils/image-version.js +12 -21
- package/lib/utils/paths.js +39 -66
- package/lib/utils/port-resolver.js +8 -8
- package/lib/utils/schema-loader.js +22 -0
- package/lib/utils/schema-resolver.js +23 -33
- package/lib/utils/secrets-helpers.js +7 -7
- package/lib/utils/secrets-utils.js +10 -12
- package/lib/utils/template-helpers.js +13 -13
- package/lib/utils/token-manager.js +20 -2
- package/lib/utils/variable-transformer.js +2 -2
- package/lib/validation/validate-display.js +3 -4
- package/lib/validation/validate.js +33 -27
- package/lib/validation/validator.js +50 -30
- package/package.json +2 -1
- package/templates/README.md +1 -1
- package/templates/applications/README.md.hbs +3 -3
- package/templates/applications/miso-controller/env.template +3 -1
- package/templates/external-system/README.md.hbs +4 -4
- package/integration/hubspot/variables.yaml +0 -17
- /package/templates/applications/dataplane/{variables.yaml → application.yaml} +0 -0
- /package/templates/applications/keycloak/{variables.yaml → application.yaml} +0 -0
- /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
|
|
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
|
|
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
|
|
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
|
|
34
|
-
|
|
29
|
+
const configPath = resolveApplicationConfigPath(appPath);
|
|
30
|
+
const variables = loadConfigFile(configPath);
|
|
31
|
+
return { variables, created: false };
|
|
35
32
|
} catch (error) {
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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
|
|
75
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}/
|
|
122
|
+
logger.error(chalk.red(`\n Please update ${relativePath}/application.yaml and try again.`));
|
|
123
123
|
process.exit(1);
|
|
124
124
|
}
|
|
125
125
|
|
package/lib/utils/cli-utils.js
CHANGED
|
@@ -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 (
|
|
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
|
|
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
|
-
|
|
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,
|