@aifabrix/builder 2.40.2 → 2.42.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/docs-rules.mdc +30 -0
- package/README.md +7 -5
- package/integration/hubspot/README.md +8 -4
- package/integration/hubspot/application.json +54 -0
- package/integration/hubspot/create-hubspot.js +9 -136
- package/integration/hubspot/env.template +3 -4
- package/integration/hubspot/hubspot-datasource-company.json +343 -5
- package/integration/hubspot/hubspot-datasource-contact.json +413 -5
- package/integration/hubspot/hubspot-datasource-deal.json +341 -4
- package/integration/hubspot/hubspot-datasource-users.json +116 -0
- package/integration/hubspot/hubspot-deploy.json +1250 -108
- package/integration/hubspot/hubspot-system.json +15 -32
- package/integration/hubspot/test-dataplane-down-tests.js +17 -16
- package/integration/hubspot/test-dataplane-down.js +2 -2
- package/integration/hubspot/test.js +1 -1
- package/jest.config.manual.js +2 -1
- package/lib/api/credential.api.js +40 -0
- package/lib/api/dev.api.js +423 -0
- package/lib/api/external-test.api.js +111 -0
- package/lib/api/index.js +42 -19
- package/lib/api/pipeline.api.js +66 -120
- package/lib/api/types/credential.types.js +23 -0
- package/lib/api/types/dev.types.js +140 -0
- package/lib/api/types/pipeline.types.js +37 -0
- package/lib/api/wizard-platform.api.js +61 -0
- package/lib/api/wizard.api.js +34 -1
- package/lib/app/config.js +44 -11
- package/lib/app/down.js +2 -1
- package/lib/app/index.js +12 -1
- package/lib/app/prompts.js +44 -29
- package/lib/app/push.js +36 -12
- package/lib/app/readme.js +9 -6
- package/lib/app/run-env-compose.js +264 -0
- package/lib/app/run-helpers.js +121 -118
- package/lib/app/run.js +148 -28
- package/lib/app/show-display.js +1 -1
- package/lib/app/show.js +5 -2
- package/lib/build/index.js +11 -3
- package/lib/cli/setup-app.js +172 -15
- package/lib/cli/setup-credential-deployment.js +31 -6
- package/lib/cli/setup-dev.js +206 -16
- package/lib/cli/setup-environment.js +16 -6
- package/lib/cli/setup-external-system.js +89 -24
- package/lib/cli/setup-infra.js +82 -15
- package/lib/cli/setup-secrets.js +52 -5
- package/lib/cli/setup-utility.js +129 -24
- package/lib/commands/app-install.js +172 -0
- package/lib/commands/app-shell.js +75 -0
- package/lib/commands/app-test.js +282 -0
- package/lib/commands/app.js +1 -1
- package/lib/commands/credential-env.js +162 -0
- package/lib/commands/credential-list.js +17 -22
- package/lib/commands/credential-push.js +96 -0
- package/lib/commands/datasource.js +77 -6
- package/lib/commands/dev-cli-handlers.js +141 -0
- package/lib/commands/dev-down.js +114 -0
- package/lib/commands/dev-init.js +347 -0
- package/lib/commands/repair-auth-config.js +99 -0
- package/lib/commands/repair-datasource-keys.js +208 -0
- package/lib/commands/repair-datasource.js +235 -0
- package/lib/commands/repair-env-template.js +348 -0
- package/lib/commands/repair-internal.js +85 -0
- package/lib/commands/repair-rbac.js +158 -0
- package/lib/commands/repair.js +507 -0
- package/lib/commands/secrets-list.js +118 -0
- package/lib/commands/secrets-remove.js +97 -0
- package/lib/commands/secrets-set.js +30 -17
- package/lib/commands/secrets-validate.js +50 -0
- package/lib/commands/test-e2e-external.js +165 -0
- package/lib/commands/up-dataplane.js +2 -2
- package/lib/commands/up-miso.js +0 -25
- package/lib/commands/upload.js +96 -40
- package/lib/commands/wizard-core-helpers.js +226 -4
- package/lib/commands/wizard-core.js +67 -29
- package/lib/commands/wizard-dataplane.js +1 -1
- package/lib/commands/wizard-entity-selection.js +43 -0
- package/lib/commands/wizard-headless.js +44 -5
- package/lib/commands/wizard-helpers.js +7 -3
- package/lib/commands/wizard.js +86 -64
- package/lib/core/admin-secrets.js +96 -0
- package/lib/core/config.js +7 -1
- package/lib/core/secrets-ensure.js +378 -0
- package/lib/core/secrets-env-write.js +157 -0
- package/lib/core/secrets.js +176 -89
- package/lib/datasource/deploy.js +12 -3
- package/lib/datasource/field-reference-validator.js +91 -0
- package/lib/datasource/test-e2e.js +219 -0
- package/lib/datasource/test-integration.js +154 -0
- package/lib/datasource/validate.js +21 -3
- package/lib/deployment/deployer.js +7 -5
- package/lib/deployment/environment-config.js +137 -0
- package/lib/deployment/environment.js +21 -98
- package/lib/deployment/push.js +32 -2
- package/lib/external-system/download.js +188 -203
- package/lib/external-system/generator.js +204 -56
- package/lib/external-system/test-auth.js +7 -3
- package/lib/external-system/test-execution.js +2 -1
- package/lib/external-system/test-system-level.js +73 -0
- package/lib/external-system/test.js +56 -19
- package/lib/generator/external-controller-manifest.js +29 -2
- package/lib/generator/external-schema-utils.js +1 -1
- package/lib/generator/external.js +10 -3
- package/lib/generator/index.js +177 -25
- package/lib/generator/split-readme.js +1 -0
- package/lib/generator/split-variables.js +7 -1
- package/lib/generator/split.js +194 -54
- package/lib/generator/wizard-prompts-secondary.js +294 -0
- package/lib/generator/wizard-prompts.js +105 -106
- package/lib/generator/wizard-readme.js +88 -0
- package/lib/generator/wizard.js +155 -158
- package/lib/infrastructure/compose.js +11 -1
- package/lib/infrastructure/helpers.js +103 -20
- package/lib/infrastructure/index.js +98 -12
- package/lib/infrastructure/services.js +88 -22
- package/lib/schema/application-schema.json +32 -8
- package/lib/schema/external-datasource.schema.json +49 -26
- package/lib/schema/external-system.schema.json +509 -411
- package/lib/schema/wizard-config.schema.json +16 -0
- package/lib/utils/api.js +41 -13
- package/lib/utils/app-register-auth.js +25 -3
- package/lib/utils/auth-headers.js +8 -7
- package/lib/utils/cli-utils.js +20 -0
- package/lib/utils/compose-generator.js +77 -76
- package/lib/utils/compose-handlebars-helpers.js +54 -0
- package/lib/utils/compose-vector-helper.js +18 -0
- package/lib/utils/config-format-preference.js +51 -0
- package/lib/utils/config-format.js +36 -0
- package/lib/utils/config-paths.js +127 -2
- package/lib/utils/configuration-env-resolver.js +179 -0
- package/lib/utils/credential-display.js +83 -0
- package/lib/utils/credential-secrets-env.js +357 -0
- package/lib/utils/dataplane-pipeline-warning.js +28 -0
- package/lib/utils/deployment-validation-helpers.js +4 -4
- package/lib/utils/dev-ca-install.js +139 -0
- package/lib/utils/dev-cert-helper.js +122 -0
- package/lib/utils/device-code-helpers.js +224 -0
- package/lib/utils/device-code.js +37 -336
- package/lib/utils/docker-build.js +40 -8
- package/lib/utils/env-copy.js +103 -13
- package/lib/utils/env-map.js +35 -5
- package/lib/utils/env-template.js +6 -5
- package/lib/utils/error-formatters/http-status-errors.js +20 -2
- package/lib/utils/error-formatters/permission-errors.js +0 -1
- package/lib/utils/error-formatters/validation-errors.js +0 -1
- package/lib/utils/external-readme.js +56 -29
- package/lib/utils/external-system-display.js +59 -1
- package/lib/utils/external-system-test-helpers.js +21 -8
- package/lib/utils/external-system-validators.js +3 -0
- package/lib/utils/file-upload.js +20 -50
- package/lib/utils/help-builder.js +16 -2
- package/lib/utils/infra-status.js +80 -45
- package/lib/utils/local-secrets.js +7 -52
- package/lib/utils/mutagen-install.js +195 -0
- package/lib/utils/mutagen.js +146 -0
- package/lib/utils/paths.js +128 -37
- package/lib/utils/port-resolver.js +28 -16
- package/lib/utils/remote-dev-auth.js +38 -0
- package/lib/utils/remote-docker-env.js +43 -0
- package/lib/utils/remote-secrets-loader.js +60 -0
- package/lib/utils/secrets-canonical.js +93 -0
- package/lib/utils/secrets-generator.js +114 -6
- package/lib/utils/secrets-helpers.js +108 -114
- package/lib/utils/secrets-path.js +2 -2
- package/lib/utils/secrets-utils.js +52 -1
- package/lib/utils/secrets-validation.js +84 -0
- package/lib/utils/ssh-key-helper.js +116 -0
- package/lib/utils/test-log-writer.js +56 -0
- package/lib/utils/token-manager-messages.js +90 -0
- package/lib/utils/token-manager.js +29 -36
- package/lib/utils/variable-transformer.js +3 -3
- package/lib/validation/env-template-auth.js +157 -0
- package/lib/validation/env-template-kv.js +41 -0
- package/lib/validation/external-manifest-validator.js +25 -0
- package/lib/validation/external-system-auth-rules.js +86 -0
- package/lib/validation/validate-batch.js +149 -0
- package/lib/validation/validate-datasource-keys-api.js +33 -0
- package/lib/validation/validate-display.js +94 -16
- package/lib/validation/validate.js +25 -12
- package/lib/validation/validator.js +72 -9
- package/lib/validation/wizard-datasource-validation.js +50 -0
- package/package.json +8 -3
- package/scripts/install-local.js +34 -15
- package/templates/README.md +0 -1
- package/templates/applications/README.md.hbs +4 -4
- package/templates/applications/dataplane/application.yaml +6 -5
- package/templates/applications/dataplane/env.template +15 -10
- package/templates/applications/dataplane/rbac.yaml +2 -2
- package/templates/applications/keycloak/env.template +2 -0
- package/templates/applications/miso-controller/application.yaml +1 -0
- package/templates/applications/miso-controller/env.template +12 -10
- package/templates/external-system/README.md.hbs +65 -25
- package/templates/external-system/deploy.js.hbs +4 -2
- package/templates/external-system/external-datasource.yaml.hbs +217 -0
- package/templates/external-system/external-system.json.hbs +1 -18
- package/templates/infra/compose.yaml.hbs +6 -0
- package/templates/python/docker-compose.hbs +49 -23
- package/templates/typescript/docker-compose.hbs +48 -22
- package/integration/hubspot/application.yaml +0 -37
|
@@ -59,6 +59,17 @@
|
|
|
59
59
|
"type": "string",
|
|
60
60
|
"description": "Known platform identifier (for known-platform type)",
|
|
61
61
|
"enum": ["hubspot", "salesforce", "zendesk", "slack", "microsoft365"]
|
|
62
|
+
},
|
|
63
|
+
"datasourceKeys": {
|
|
64
|
+
"type": "array",
|
|
65
|
+
"description": "Datasource keys to include (validated against platform; omit for all)",
|
|
66
|
+
"items": { "type": "string", "minLength": 1 },
|
|
67
|
+
"minItems": 1
|
|
68
|
+
},
|
|
69
|
+
"entityName": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"description": "Entity for multi-entity OpenAPI (validated against discover-entities; for openapi-file and openapi-url)",
|
|
72
|
+
"minLength": 1
|
|
62
73
|
}
|
|
63
74
|
},
|
|
64
75
|
"allOf": [
|
|
@@ -194,6 +205,11 @@
|
|
|
194
205
|
"type": "boolean",
|
|
195
206
|
"description": "Enable Role-Based Access Control",
|
|
196
207
|
"default": false
|
|
208
|
+
},
|
|
209
|
+
"debug": {
|
|
210
|
+
"type": "boolean",
|
|
211
|
+
"description": "When true, capture detailed generation steps and save to debug.log (dataplane returns debugLog)",
|
|
212
|
+
"default": false
|
|
197
213
|
}
|
|
198
214
|
}
|
|
199
215
|
},
|
package/lib/utils/api.js
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
const { parseErrorResponse } = require('./api-error-handler');
|
|
14
14
|
const auditLogger = require('../core/audit-logger');
|
|
15
15
|
|
|
16
|
-
/** Default timeout for HTTP requests (ms). Prevents hanging when the controller is unreachable. */
|
|
17
|
-
const DEFAULT_REQUEST_TIMEOUT_MS =
|
|
16
|
+
/** Default timeout for HTTP requests (ms). Prevents hanging when the controller is unreachable. 30s allows Azure Web App cold start to complete. */
|
|
17
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Logs API request performance metrics and errors to audit log
|
|
@@ -121,10 +121,22 @@ async function handleSuccessResponse(response, url, options, duration) {
|
|
|
121
121
|
success: true
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
// 204 No Content or empty body: nothing to parse (avoids "Unexpected end of JSON input")
|
|
125
|
+
if (response.status === 204) {
|
|
126
|
+
return { success: true, data: null, status: response.status };
|
|
127
|
+
}
|
|
128
|
+
|
|
124
129
|
const contentType = response.headers.get('content-type');
|
|
125
130
|
if (contentType && contentType.includes('application/json')) {
|
|
126
|
-
|
|
127
|
-
|
|
131
|
+
try {
|
|
132
|
+
const data = await response.json();
|
|
133
|
+
return { success: true, data, status: response.status };
|
|
134
|
+
} catch (e) {
|
|
135
|
+
if (e instanceof SyntaxError && e.message && e.message.includes('JSON')) {
|
|
136
|
+
return { success: true, data: null, status: response.status };
|
|
137
|
+
}
|
|
138
|
+
throw e;
|
|
139
|
+
}
|
|
128
140
|
}
|
|
129
141
|
|
|
130
142
|
const text = await response.text();
|
|
@@ -229,7 +241,7 @@ async function handleNetworkError(error, url, options, duration) {
|
|
|
229
241
|
|
|
230
242
|
/**
|
|
231
243
|
* Make an API call with proper error handling
|
|
232
|
-
* Uses a
|
|
244
|
+
* Uses a 30s timeout to avoid hanging when the controller is unreachable (Azure cold start can exceed 5s).
|
|
233
245
|
* @param {string} url - API endpoint URL
|
|
234
246
|
* @param {Object} options - Fetch options (signal, method, headers, body, etc.)
|
|
235
247
|
* @returns {Promise<Object>} Response object with success flag
|
|
@@ -286,12 +298,28 @@ function extractControllerUrl(url) {
|
|
|
286
298
|
}
|
|
287
299
|
|
|
288
300
|
/**
|
|
289
|
-
*
|
|
290
|
-
*
|
|
301
|
+
* Set auth header on headers object: Bearer for user token, x-client-token for application token.
|
|
302
|
+
* @param {Object} headers - Headers object to mutate
|
|
303
|
+
* @param {string} token - Token value
|
|
304
|
+
* @param {string} authType - 'bearer' or 'client-token'
|
|
305
|
+
*/
|
|
306
|
+
function setAuthHeader(headers, token, authType) {
|
|
307
|
+
if (!token) return;
|
|
308
|
+
if (authType === 'client-token') {
|
|
309
|
+
headers['x-client-token'] = token;
|
|
310
|
+
} else {
|
|
311
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Make an authenticated API call with user token (Bearer) or application token (x-client-token).
|
|
317
|
+
* Automatically refreshes device token on 401 when user Bearer was used.
|
|
291
318
|
* @param {string} url - API endpoint URL
|
|
292
319
|
* @param {Object} options - Fetch options
|
|
293
|
-
* @param {string|Object} tokenOrAuthConfig -
|
|
294
|
-
* @param {string} [tokenOrAuthConfig.
|
|
320
|
+
* @param {string|Object} tokenOrAuthConfig - User token string (Bearer), or authConfig object with type 'bearer'|'client-token'
|
|
321
|
+
* @param {string} [tokenOrAuthConfig.type] - 'bearer' (user token) or 'client-token' (application token)
|
|
322
|
+
* @param {string} [tokenOrAuthConfig.token] - Token (if object)
|
|
295
323
|
* @param {string} [tokenOrAuthConfig.controller] - Controller URL for token refresh (if object)
|
|
296
324
|
* @returns {Promise<Object>} Response object
|
|
297
325
|
*/
|
|
@@ -299,22 +327,22 @@ function extractControllerUrl(url) {
|
|
|
299
327
|
async function authenticatedApiCall(url, options = {}, tokenOrAuthConfig) {
|
|
300
328
|
const isStringToken = typeof tokenOrAuthConfig === 'string';
|
|
301
329
|
const token = isStringToken ? tokenOrAuthConfig : tokenOrAuthConfig?.token;
|
|
330
|
+
const authType = isStringToken ? 'bearer' : tokenOrAuthConfig?.type;
|
|
302
331
|
const authControllerUrl = isStringToken ? null : tokenOrAuthConfig?.controller;
|
|
303
332
|
const isFormData = typeof FormData !== 'undefined' && options.body instanceof FormData;
|
|
304
333
|
const headers = { ...options.headers };
|
|
305
334
|
if (!isFormData && !headers['Content-Type']) {
|
|
306
335
|
headers['Content-Type'] = 'application/json';
|
|
307
336
|
}
|
|
308
|
-
|
|
309
|
-
headers['Authorization'] = `Bearer ${token}`;
|
|
310
|
-
}
|
|
337
|
+
setAuthHeader(headers, token, authType);
|
|
311
338
|
|
|
312
339
|
const response = await makeApiCall(url, {
|
|
313
340
|
...options,
|
|
314
341
|
headers
|
|
315
342
|
});
|
|
316
343
|
|
|
317
|
-
|
|
344
|
+
// Only attempt device token refresh on 401 when user Bearer token was used (not for client-token)
|
|
345
|
+
if (!response.success && response.status === 401 && authType !== 'client-token') {
|
|
318
346
|
try {
|
|
319
347
|
const { forceRefreshDeviceToken } = require('./token-manager');
|
|
320
348
|
const refreshedToken = await forceRefreshDeviceToken(authControllerUrl || extractControllerUrl(url));
|
|
@@ -257,7 +257,22 @@ async function attemptGetAuthenticationToken(normalizedControllerUrl, config, at
|
|
|
257
257
|
return { token, finalControllerUrl, lastError };
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
|
|
260
|
+
/**
|
|
261
|
+
* When auth fails and throwOnFailure is true, throws an error with controllerUrl so caller can run login and retry.
|
|
262
|
+
* @param {string|null} controllerUrl - Controller URL to attach to the thrown error
|
|
263
|
+
* @param {Error|null} lastError - Last error from token attempt
|
|
264
|
+
* @param {string[]} attemptedUrls - URLs that were tried
|
|
265
|
+
* @throws {Error} Error with .controllerUrl and .authFailure set
|
|
266
|
+
*/
|
|
267
|
+
function throwAuthFailureError(controllerUrl, lastError, attemptedUrls) {
|
|
268
|
+
const message = lastError ? lastError.message : 'No valid authentication found';
|
|
269
|
+
const err = new Error(message);
|
|
270
|
+
err.controllerUrl = controllerUrl || (attemptedUrls && attemptedUrls[0]) || null;
|
|
271
|
+
err.authFailure = true;
|
|
272
|
+
throw err;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function checkAuthentication(controllerUrl, _environment, options = {}) {
|
|
261
276
|
try {
|
|
262
277
|
const config = await getConfig();
|
|
263
278
|
const normalizedControllerUrl = normalizeControllerUrlIfProvided(controllerUrl);
|
|
@@ -269,8 +284,12 @@ async function checkAuthentication(controllerUrl, _environment) {
|
|
|
269
284
|
attemptedUrls
|
|
270
285
|
);
|
|
271
286
|
|
|
272
|
-
|
|
273
|
-
|
|
287
|
+
if (!token || !finalControllerUrl) {
|
|
288
|
+
if (options.throwOnFailure) {
|
|
289
|
+
throwAuthFailureError(controllerUrl || finalControllerUrl, lastError, attemptedUrls);
|
|
290
|
+
}
|
|
291
|
+
validateAuthenticationResult(token, finalControllerUrl, lastError, controllerUrl, attemptedUrls);
|
|
292
|
+
}
|
|
274
293
|
|
|
275
294
|
return {
|
|
276
295
|
apiUrl: finalControllerUrl,
|
|
@@ -278,6 +297,9 @@ async function checkAuthentication(controllerUrl, _environment) {
|
|
|
278
297
|
controllerUrl: finalControllerUrl
|
|
279
298
|
};
|
|
280
299
|
} catch (error) {
|
|
300
|
+
if (error.authFailure) {
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
281
303
|
displayAuthenticationError(error, { controllerUrl: controllerUrl });
|
|
282
304
|
}
|
|
283
305
|
}
|
|
@@ -25,11 +25,12 @@ function createBearerTokenHeaders(token) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Creates authentication headers for
|
|
28
|
+
* Creates authentication headers for the token-issuing endpoint only (e.g. POST /api/v1/auth/token).
|
|
29
|
+
* Do not use for Controller or Dataplane app endpoints—those require Bearer token (use createBearerTokenHeaders).
|
|
29
30
|
*
|
|
30
31
|
* @param {string} clientId - Application client ID
|
|
31
32
|
* @param {string} clientSecret - Application client secret
|
|
32
|
-
* @returns {Object} Headers object with
|
|
33
|
+
* @returns {Object} Headers object with x-client-id and x-client-secret
|
|
33
34
|
* @throws {Error} If credentials are missing
|
|
34
35
|
*/
|
|
35
36
|
function createClientCredentialsHeaders(clientId, clientSecret) {
|
|
@@ -43,14 +44,14 @@ function createClientCredentialsHeaders(clientId, clientSecret) {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
|
-
* Creates authentication headers based on auth configuration
|
|
47
|
-
*
|
|
47
|
+
* Creates authentication headers based on auth configuration.
|
|
48
|
+
* For app endpoints use type 'bearer' only. Use 'client-credentials' only when calling the token-issuing endpoint (e.g. /api/v1/auth/token).
|
|
48
49
|
*
|
|
49
50
|
* @param {Object} authConfig - Authentication configuration
|
|
50
|
-
* @param {string} authConfig.type - Auth type: 'bearer' or 'client-credentials'
|
|
51
|
+
* @param {string} authConfig.type - Auth type: 'bearer' (for app endpoints) or 'client-credentials' (token endpoint only)
|
|
51
52
|
* @param {string} [authConfig.token] - Bearer token (for type 'bearer')
|
|
52
|
-
* @param {string} [authConfig.clientId] - Client ID (for type 'client-credentials')
|
|
53
|
-
* @param {string} [authConfig.clientSecret] - Client secret (for type 'client-credentials')
|
|
53
|
+
* @param {string} [authConfig.clientId] - Client ID (for type 'client-credentials', token endpoint only)
|
|
54
|
+
* @param {string} [authConfig.clientSecret] - Client secret (for type 'client-credentials', token endpoint only)
|
|
54
55
|
* @returns {Object} Headers object with authentication
|
|
55
56
|
* @throws {Error} If auth config is invalid
|
|
56
57
|
*/
|
package/lib/utils/cli-utils.js
CHANGED
|
@@ -106,6 +106,10 @@ function isDockerPermissionDeniedError(errorMsg) {
|
|
|
106
106
|
*/
|
|
107
107
|
function formatDockerError(errorMsg) {
|
|
108
108
|
if (isDockerImageNotFoundError(errorMsg)) {
|
|
109
|
+
// Preserve custom hint for template apps (keycloak, miso-controller) from up-miso/up-dataplane
|
|
110
|
+
if (errorMsg.includes('use --image') || errorMsg.includes('Pull the image')) {
|
|
111
|
+
return errorMsg.split('\n').map(line => (line.trim() ? ` ${line.trim()}` : ''));
|
|
112
|
+
}
|
|
109
113
|
return [
|
|
110
114
|
' Docker image not found.',
|
|
111
115
|
' Run: aifabrix build <app> first'
|
|
@@ -326,6 +330,21 @@ function logOfflinePathWhenType(appPath, options) {
|
|
|
326
330
|
logger.log(chalk.gray(`Using: ${displayPath}`));
|
|
327
331
|
}
|
|
328
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Returns true if the error is likely due to authentication failure (e.g. 401, token expired, login required).
|
|
335
|
+
* Used to show "Run: aifabrix login" for commands that require Controller auth (e.g. up-dataplane).
|
|
336
|
+
* @param {Error} error - The error that occurred
|
|
337
|
+
* @returns {boolean} True if the error appears to be auth-related
|
|
338
|
+
*/
|
|
339
|
+
function isAuthenticationError(error) {
|
|
340
|
+
if (!error) return false;
|
|
341
|
+
if (error.authFailure === true) return true;
|
|
342
|
+
const msg = (error.message || '').toLowerCase();
|
|
343
|
+
const formatted = (typeof error.formatted === 'string' ? error.formatted : '').toLowerCase();
|
|
344
|
+
const combined = `${msg} ${formatted}`;
|
|
345
|
+
return /401|unauthorized|authentication|token expired|login required|aifabrix login|no authentication|device token|refresh token/.test(combined);
|
|
346
|
+
}
|
|
347
|
+
|
|
329
348
|
/**
|
|
330
349
|
* Handles command errors with user-friendly messages
|
|
331
350
|
* @param {Error} error - The error that occurred
|
|
@@ -375,6 +394,7 @@ async function appendWizardError(appKey, error) {
|
|
|
375
394
|
module.exports = {
|
|
376
395
|
validateCommand,
|
|
377
396
|
handleCommandError,
|
|
397
|
+
isAuthenticationError,
|
|
378
398
|
appendWizardError,
|
|
379
399
|
logOfflinePathWhenType
|
|
380
400
|
};
|
|
@@ -18,58 +18,12 @@ const buildCopy = require('./build-copy');
|
|
|
18
18
|
const { formatMissingDbPasswordError } = require('./error-formatter');
|
|
19
19
|
const { getContainerPort } = require('./port-resolver');
|
|
20
20
|
const { parseImageOverride } = require('./parse-image-ref');
|
|
21
|
+
const { registerComposeHelpers } = require('./compose-handlebars-helpers');
|
|
22
|
+
const { isVectorDatabaseName } = require('./compose-vector-helper');
|
|
23
|
+
const paths = require('./paths');
|
|
24
|
+
const { getInfraDirName } = require('../infrastructure/helpers');
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
handlebars.registerHelper('eq', (a, b) => a === b);
|
|
24
|
-
|
|
25
|
-
// Register Handlebars helper for quoting PostgreSQL identifiers
|
|
26
|
-
// PostgreSQL requires identifiers with hyphens or special characters to be quoted
|
|
27
|
-
handlebars.registerHelper('pgQuote', (identifier) => {
|
|
28
|
-
if (!identifier) {
|
|
29
|
-
return '';
|
|
30
|
-
}
|
|
31
|
-
// Always quote identifiers to handle hyphens and special characters
|
|
32
|
-
// Return SafeString to prevent HTML escaping
|
|
33
|
-
return new handlebars.SafeString(`"${String(identifier).replace(/"/g, '""')}"`);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// Helper to generate quoted PostgreSQL user name from database name
|
|
37
|
-
// User names must use underscores (not hyphens) for PostgreSQL compatibility
|
|
38
|
-
handlebars.registerHelper('pgUser', (dbName) => {
|
|
39
|
-
if (!dbName) {
|
|
40
|
-
return '';
|
|
41
|
-
}
|
|
42
|
-
// Replace hyphens with underscores in user name (database names can have hyphens, but user names should not)
|
|
43
|
-
const userName = `${String(dbName).replace(/-/g, '_')}_user`;
|
|
44
|
-
// Return SafeString to prevent HTML escaping
|
|
45
|
-
return new handlebars.SafeString(`"${userName.replace(/"/g, '""')}"`);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// Helper to generate old user name format (for migration - drops old users with hyphens)
|
|
49
|
-
// This is used to drop legacy users that were created with hyphens before the fix
|
|
50
|
-
// Returns unquoted name (quotes should be added in template where needed)
|
|
51
|
-
handlebars.registerHelper('pgUserOld', (dbName) => {
|
|
52
|
-
if (!dbName) {
|
|
53
|
-
return '';
|
|
54
|
-
}
|
|
55
|
-
// Old format: database name + _user (preserving hyphens)
|
|
56
|
-
const userName = `${String(dbName)}_user`;
|
|
57
|
-
// Return unquoted name - template will add quotes where needed
|
|
58
|
-
return new handlebars.SafeString(userName);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// Helper to generate unquoted PostgreSQL user name (for SQL WHERE clauses)
|
|
62
|
-
// Returns the user name without quotes for use in SQL queries
|
|
63
|
-
handlebars.registerHelper('pgUserName', (dbName) => {
|
|
64
|
-
if (!dbName) {
|
|
65
|
-
return '';
|
|
66
|
-
}
|
|
67
|
-
// Replace hyphens with underscores in user name
|
|
68
|
-
const userName = `${String(dbName).replace(/-/g, '_')}_user`;
|
|
69
|
-
// Return unquoted name for SQL queries
|
|
70
|
-
return new handlebars.SafeString(userName);
|
|
71
|
-
});
|
|
72
|
-
|
|
26
|
+
registerComposeHelpers();
|
|
73
27
|
/**
|
|
74
28
|
* Loads and compiles Docker Compose template
|
|
75
29
|
* @param {string} language - Language type
|
|
@@ -95,7 +49,6 @@ function loadDockerComposeTemplate(language) {
|
|
|
95
49
|
const templateContent = fsSync.readFileSync(templatePath, 'utf8');
|
|
96
50
|
return handlebars.compile(templateContent);
|
|
97
51
|
}
|
|
98
|
-
|
|
99
52
|
/**
|
|
100
53
|
* Extracts image name from configuration (same logic as build.js)
|
|
101
54
|
* @param {Object} config - Application configuration
|
|
@@ -112,7 +65,6 @@ function getImageName(config, appName) {
|
|
|
112
65
|
}
|
|
113
66
|
return appName;
|
|
114
67
|
}
|
|
115
|
-
|
|
116
68
|
/**
|
|
117
69
|
* Builds app configuration section
|
|
118
70
|
* @param {string} appName - Application name
|
|
@@ -125,7 +77,6 @@ function buildAppConfig(appName, config) {
|
|
|
125
77
|
name: config.displayName || appName
|
|
126
78
|
};
|
|
127
79
|
}
|
|
128
|
-
|
|
129
80
|
/**
|
|
130
81
|
* Builds image configuration section
|
|
131
82
|
* @param {Object} config - Application configuration
|
|
@@ -255,9 +206,6 @@ function buildServiceConfig(appName, config, port, devId, imageOverride) {
|
|
|
255
206
|
port: containerPortValue, // Container port (for health check and template)
|
|
256
207
|
containerPort: containerPortValue, // Container port (always set, equals containerPort if exists, else port)
|
|
257
208
|
hostPort: hostPort, // Host port (options.port if provided, else config.port)
|
|
258
|
-
build: {
|
|
259
|
-
localPort: config.build?.localPort || null // Only used for .env file PORT variable, not for Docker Compose
|
|
260
|
-
},
|
|
261
209
|
healthCheck: buildHealthCheckConfig(config),
|
|
262
210
|
traefik: buildTraefikConfig(config, devId),
|
|
263
211
|
...buildRequiresConfig(config)
|
|
@@ -276,7 +224,7 @@ function buildVolumesConfig(appName) {
|
|
|
276
224
|
/**
|
|
277
225
|
* Builds networks configuration for template data
|
|
278
226
|
* @param {Object} config - Application configuration
|
|
279
|
-
* @returns {Object} Networks configuration
|
|
227
|
+
* @returns {Object} Networks configuration with databases array
|
|
280
228
|
*/
|
|
281
229
|
function buildNetworksConfig(config) {
|
|
282
230
|
return { databases: config.requires?.databases || config.databases || [] };
|
|
@@ -445,6 +393,52 @@ async function readDatabasePasswordsIfNeeded(requiresDatabase, databases, envFil
|
|
|
445
393
|
return { map: {}, array: [] };
|
|
446
394
|
}
|
|
447
395
|
|
|
396
|
+
/**
|
|
397
|
+
* Resolves image override from options (--image, --tag, or null).
|
|
398
|
+
* @param {Object} options - Run options
|
|
399
|
+
* @param {Object} appConfig - Application configuration
|
|
400
|
+
* @param {string} appName - Application name
|
|
401
|
+
* @returns {string|null} Full image reference or null
|
|
402
|
+
*/
|
|
403
|
+
function resolveImageOverride(options, appConfig, appName) {
|
|
404
|
+
if (options.image) return options.image;
|
|
405
|
+
if (options.imageOverride) return options.imageOverride;
|
|
406
|
+
if (options.tag) return `${getImageName(appConfig, appName)}:${options.tag}`;
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Resolves Miso environment from options (tst, pro, or dev).
|
|
412
|
+
* @param {Object} options - Run options
|
|
413
|
+
* @returns {string} 'dev' | 'tst' | 'pro'
|
|
414
|
+
*/
|
|
415
|
+
function resolveMisoEnvironment(options) {
|
|
416
|
+
const env = (options.env && typeof options.env === 'string') ? options.env.toLowerCase() : 'dev';
|
|
417
|
+
return (env === 'tst' || env === 'pro') ? env : 'dev';
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Resolves dev mount path from options.
|
|
422
|
+
* @param {Object} options - Run options
|
|
423
|
+
* @returns {string|null} Trimmed path or null
|
|
424
|
+
*/
|
|
425
|
+
function resolveDevMountPath(options) {
|
|
426
|
+
return (options.devMountPath && typeof options.devMountPath === 'string') ? options.devMountPath.trim() : null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Resolves env file path from options or default dev dir (forward slashes).
|
|
431
|
+
* @param {Object} options - Run options
|
|
432
|
+
* @param {string} devDir - Default dev directory
|
|
433
|
+
* @returns {string} Absolute env file path
|
|
434
|
+
*/
|
|
435
|
+
function resolveEnvFilePath(options, devDir) {
|
|
436
|
+
const envFilePath = (options.envFilePath && typeof options.envFilePath === 'string')
|
|
437
|
+
? path.resolve(options.envFilePath)
|
|
438
|
+
: path.join(devDir, '.env');
|
|
439
|
+
return envFilePath.replace(/\\/g, '/');
|
|
440
|
+
}
|
|
441
|
+
|
|
448
442
|
/**
|
|
449
443
|
* Generates Docker Compose configuration from template
|
|
450
444
|
* @async
|
|
@@ -458,8 +452,7 @@ async function generateDockerCompose(appName, appConfig, options) {
|
|
|
458
452
|
const language = appConfig.build?.language || appConfig.language || 'typescript';
|
|
459
453
|
const template = loadDockerComposeTemplate(language);
|
|
460
454
|
const port = options.port || appConfig.port || 3000;
|
|
461
|
-
const imageOverride = options
|
|
462
|
-
(options.tag ? `${getImageName(appConfig, appName)}:${options.tag}` : null);
|
|
455
|
+
const imageOverride = resolveImageOverride(options, appConfig, appName);
|
|
463
456
|
const { devId, idNum } = await getDeveloperIdAndNumeric();
|
|
464
457
|
const { networkName, containerName } = buildNetworkAndContainerNames(appName, devId, idNum);
|
|
465
458
|
const serviceConfig = buildServiceConfig(appName, appConfig, port, devId, imageOverride);
|
|
@@ -467,33 +460,41 @@ async function generateDockerCompose(appName, appConfig, options) {
|
|
|
467
460
|
const networksConfig = buildNetworksConfig(appConfig);
|
|
468
461
|
|
|
469
462
|
const devDir = buildCopy.getDevDirectory(appName, devId);
|
|
470
|
-
const
|
|
471
|
-
const
|
|
463
|
+
const envFileAbsolutePath = resolveEnvFilePath(options, devDir);
|
|
464
|
+
const dbInitEnvFileAbsolutePath = (options.dbInitEnvFilePath && typeof options.dbInitEnvFilePath === 'string')
|
|
465
|
+
? path.resolve(options.dbInitEnvFilePath).replace(/\\/g, '/')
|
|
466
|
+
: null;
|
|
472
467
|
|
|
473
468
|
const databasePasswords = await readDatabasePasswordsIfNeeded(
|
|
474
469
|
serviceConfig.requiresDatabase || false,
|
|
475
470
|
networksConfig.databases || [],
|
|
476
|
-
|
|
471
|
+
envFileAbsolutePath,
|
|
477
472
|
appName
|
|
478
473
|
);
|
|
479
|
-
|
|
480
|
-
const
|
|
474
|
+
const devMountPath = resolveDevMountPath(options);
|
|
475
|
+
const reloadStartRaw = appConfig.build?.reloadStart;
|
|
476
|
+
const reloadStartCommand =
|
|
477
|
+
devMountPath && typeof reloadStartRaw === 'string' && reloadStartRaw.trim().length > 0
|
|
478
|
+
? reloadStartRaw.trim()
|
|
479
|
+
: null;
|
|
480
|
+
|
|
481
|
+
const infraPgpassPath = path.join(paths.getAifabrixHome(), getInfraDirName(devId), 'pgpass');
|
|
482
|
+
const useInfraPgpass = serviceConfig.requiresDatabase && fsSync.existsSync(infraPgpassPath);
|
|
483
|
+
return template({
|
|
481
484
|
...serviceConfig,
|
|
482
485
|
...volumesConfig,
|
|
483
486
|
...networksConfig,
|
|
484
487
|
envFile: envFileAbsolutePath,
|
|
488
|
+
dbInitEnvFile: dbInitEnvFileAbsolutePath,
|
|
485
489
|
databasePasswords,
|
|
486
490
|
devId: idNum,
|
|
487
491
|
networkName,
|
|
488
|
-
containerName
|
|
489
|
-
|
|
490
|
-
|
|
492
|
+
containerName,
|
|
493
|
+
misoEnvironment: resolveMisoEnvironment(options),
|
|
494
|
+
devMountPath,
|
|
495
|
+
reloadStartCommand,
|
|
496
|
+
infraPgpassPath: useInfraPgpass ? infraPgpassPath : null,
|
|
497
|
+
useInfraPgpass: !!useInfraPgpass
|
|
498
|
+
});
|
|
491
499
|
}
|
|
492
|
-
|
|
493
|
-
module.exports = {
|
|
494
|
-
generateDockerCompose,
|
|
495
|
-
getImageName,
|
|
496
|
-
derivePathFromPattern,
|
|
497
|
-
buildTraefikConfig,
|
|
498
|
-
buildDevUsername
|
|
499
|
-
};
|
|
500
|
+
module.exports = { generateDockerCompose, getImageName, derivePathFromPattern, buildTraefikConfig, buildDevUsername, isVectorDatabaseName };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handlebars helpers for Docker Compose templates.
|
|
3
|
+
* @fileoverview Compose template helpers (pgQuote, pgUser, isVectorDatabase, etc.)
|
|
4
|
+
* @author AI Fabrix Team
|
|
5
|
+
* @version 2.0.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const handlebars = require('handlebars');
|
|
9
|
+
const { isVectorDatabaseName } = require('./compose-vector-helper');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Registers Handlebars helpers used by Docker Compose templates.
|
|
13
|
+
*/
|
|
14
|
+
function registerComposeHelpers() {
|
|
15
|
+
handlebars.registerHelper('eq', (a, b) => a === b);
|
|
16
|
+
|
|
17
|
+
handlebars.registerHelper('pgQuote', (identifier) => {
|
|
18
|
+
if (!identifier) return '';
|
|
19
|
+
return new handlebars.SafeString(`"${String(identifier).replace(/"/g, '""')}"`);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
handlebars.registerHelper('pgUser', (dbName) => {
|
|
23
|
+
if (!dbName) return '';
|
|
24
|
+
const userName = `${String(dbName).replace(/-/g, '_')}_user`;
|
|
25
|
+
return new handlebars.SafeString(`"${userName.replace(/"/g, '""')}"`);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
handlebars.registerHelper('pgUserOld', (dbName) => {
|
|
29
|
+
if (!dbName) return '';
|
|
30
|
+
const userName = `${String(dbName)}_user`;
|
|
31
|
+
return new handlebars.SafeString(userName);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
handlebars.registerHelper('pgUserName', (dbName) => {
|
|
35
|
+
if (!dbName) return '';
|
|
36
|
+
const userName = `${String(dbName).replace(/-/g, '_')}_user`;
|
|
37
|
+
return new handlebars.SafeString(userName);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
handlebars.registerHelper('isVectorDatabase', (name) => isVectorDatabaseName(name));
|
|
41
|
+
|
|
42
|
+
/** Returns list of extension names for this database (config extensions + vector if name ends with "vector"). */
|
|
43
|
+
handlebars.registerHelper('extensionsForDb', (db) => {
|
|
44
|
+
if (!db) return [];
|
|
45
|
+
const explicit = Array.isArray(db.extensions) ? db.extensions : [];
|
|
46
|
+
const list = [...explicit];
|
|
47
|
+
if (isVectorDatabaseName(db.name) && !list.includes('vector')) {
|
|
48
|
+
list.push('vector');
|
|
49
|
+
}
|
|
50
|
+
return list;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { registerComposeHelpers };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vector database name predicate for Docker Compose generation.
|
|
3
|
+
* Used so db-init can run CREATE EXTENSION vector on vector-store databases.
|
|
4
|
+
* @fileoverview Vector database name helper for compose-generator
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns true when the database name ends with "vector" (case-insensitive).
|
|
11
|
+
* @param {string} name - Database name
|
|
12
|
+
* @returns {boolean}
|
|
13
|
+
*/
|
|
14
|
+
function isVectorDatabaseName(name) {
|
|
15
|
+
return name !== null && name !== undefined && String(name).toLowerCase().endsWith('vector');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { isVectorDatabaseName };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config format preference utilities (json/yaml)
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Format preference get/set for config
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validate and normalize format (json or yaml)
|
|
11
|
+
* @param {*} format - Format value
|
|
12
|
+
* @returns {string} Normalized format ('json' or 'yaml')
|
|
13
|
+
* @throws {Error} If format is invalid
|
|
14
|
+
*/
|
|
15
|
+
function validateAndNormalizeFormat(format) {
|
|
16
|
+
if (!format || typeof format !== 'string') {
|
|
17
|
+
throw new Error('Option --format must be \'json\' or \'yaml\'');
|
|
18
|
+
}
|
|
19
|
+
const normalized = format.trim().toLowerCase();
|
|
20
|
+
if (normalized !== 'json' && normalized !== 'yaml') {
|
|
21
|
+
throw new Error('Option --format must be \'json\' or \'yaml\'');
|
|
22
|
+
}
|
|
23
|
+
return normalized;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create format preference functions
|
|
28
|
+
* @param {Function} getConfigFn - Async function to get config
|
|
29
|
+
* @param {Function} saveConfigFn - Async function to save config
|
|
30
|
+
* @returns {{ getFormat: Function, setFormat: Function, validateAndNormalizeFormat: Function }}
|
|
31
|
+
*/
|
|
32
|
+
function createFormatFunctions(getConfigFn, saveConfigFn) {
|
|
33
|
+
return {
|
|
34
|
+
async getFormat() {
|
|
35
|
+
const config = await getConfigFn();
|
|
36
|
+
const raw = config.format;
|
|
37
|
+
if (!raw || typeof raw !== 'string') return null;
|
|
38
|
+
const normalized = raw.trim().toLowerCase();
|
|
39
|
+
return normalized === 'json' || normalized === 'yaml' ? normalized : null;
|
|
40
|
+
},
|
|
41
|
+
async setFormat(format) {
|
|
42
|
+
const normalized = validateAndNormalizeFormat(format);
|
|
43
|
+
const config = await getConfigFn();
|
|
44
|
+
config.format = normalized;
|
|
45
|
+
await saveConfigFn(config);
|
|
46
|
+
},
|
|
47
|
+
validateAndNormalizeFormat
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { createFormatFunctions, validateAndNormalizeFormat };
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const yaml = require('js-yaml');
|
|
17
|
+
const YAML = require('yaml');
|
|
17
18
|
|
|
18
19
|
const YAML_EXTENSIONS = ['.yaml', '.yml'];
|
|
19
20
|
const JSON_EXTENSIONS = ['.json'];
|
|
@@ -144,11 +145,46 @@ function writeConfigFile(filePath, object, format) {
|
|
|
144
145
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
145
146
|
}
|
|
146
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Writes application config YAML by updating only repaired keys in the original content,
|
|
150
|
+
* so comments and formatting on other keys are preserved. Use for repair flows that
|
|
151
|
+
* only change externalIntegration and/or app.key.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} filePath - Absolute path to the YAML file
|
|
154
|
+
* @param {string} originalContent - Original file content (with comments)
|
|
155
|
+
* @param {Object} repairedVariables - Repaired config object; only externalIntegration and app are written
|
|
156
|
+
* @throws {Error} If parsing or write fails
|
|
157
|
+
*/
|
|
158
|
+
function writeYamlPreservingComments(filePath, originalContent, repairedVariables) {
|
|
159
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
160
|
+
throw new Error('writeYamlPreservingComments requires a non-empty file path');
|
|
161
|
+
}
|
|
162
|
+
if (typeof originalContent !== 'string') {
|
|
163
|
+
throw new Error('writeYamlPreservingComments requires original content string');
|
|
164
|
+
}
|
|
165
|
+
const doc = YAML.parseDocument(originalContent);
|
|
166
|
+
if (doc.errors && doc.errors.length > 0) {
|
|
167
|
+
const first = doc.errors[0];
|
|
168
|
+
throw new Error(`Invalid YAML: ${first.message}`);
|
|
169
|
+
}
|
|
170
|
+
if (!doc.contents) {
|
|
171
|
+
doc.contents = doc.createNode({});
|
|
172
|
+
}
|
|
173
|
+
if (repairedVariables.externalIntegration !== undefined) {
|
|
174
|
+
doc.set('externalIntegration', doc.createNode(repairedVariables.externalIntegration));
|
|
175
|
+
}
|
|
176
|
+
if (repairedVariables.app !== undefined) {
|
|
177
|
+
doc.set('app', doc.createNode(repairedVariables.app));
|
|
178
|
+
}
|
|
179
|
+
fs.writeFileSync(filePath, String(doc), 'utf8');
|
|
180
|
+
}
|
|
181
|
+
|
|
147
182
|
module.exports = {
|
|
148
183
|
yamlToJson,
|
|
149
184
|
jsonToYaml,
|
|
150
185
|
loadConfigFile,
|
|
151
186
|
writeConfigFile,
|
|
187
|
+
writeYamlPreservingComments,
|
|
152
188
|
isYamlPath,
|
|
153
189
|
isJsonPath
|
|
154
190
|
};
|