@aifabrix/builder 2.44.4 → 2.44.6
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/cli-layout.mdc +1 -1
- package/.cursor/rules/project-rules.mdc +1 -1
- package/.npmrc.token +1 -1
- package/README.md +15 -23
- package/integration/hubspot-test/README.md +2 -0
- package/integration/hubspot-test/test.js +5 -3
- package/jest.projects.js +68 -17
- package/lib/api/controller-health.api.js +49 -0
- package/lib/api/dimension-values.api.js +82 -0
- package/lib/api/dimensions.api.js +114 -0
- package/lib/api/external-systems.api.js +1 -0
- package/lib/api/integration-clients.api.js +168 -0
- package/lib/api/types/dimension-values.types.js +28 -0
- package/lib/api/types/dimensions.types.js +31 -0
- package/lib/api/types/integration-clients.types.js +45 -0
- package/lib/api/types/wizard.types.js +2 -1
- package/lib/api/validation-runner.js +46 -25
- package/lib/app/deploy-config.js +11 -1
- package/lib/app/deploy-status-display.js +3 -3
- package/lib/app/deploy.js +36 -14
- package/lib/app/display.js +15 -11
- package/lib/app/push.js +46 -23
- package/lib/app/register.js +1 -1
- package/lib/app/restart-display.js +95 -0
- package/lib/app/rotate-secret.js +1 -1
- package/lib/app/run-container-start.js +12 -6
- package/lib/app/run-env-compose.js +30 -1
- package/lib/app/run-helpers.js +44 -12
- package/lib/app/run-reload-sync.js +148 -0
- package/lib/app/run-resolve-image.js +51 -1
- package/lib/app/run.js +99 -73
- package/lib/build/index.js +75 -45
- package/lib/cli/doctor-check.js +117 -0
- package/lib/cli/index.js +8 -2
- package/lib/cli/infra-guided.js +445 -0
- package/lib/cli/setup-app.help.js +1 -1
- package/lib/cli/setup-app.js +20 -2
- package/lib/cli/setup-app.test-commands.js +9 -5
- package/lib/cli/setup-auth.js +26 -0
- package/lib/cli/setup-dev-path-commands.js +50 -3
- package/lib/cli/setup-infra.js +138 -61
- package/lib/cli/setup-integration-client.js +182 -0
- package/lib/cli/setup-parameters.js +21 -2
- package/lib/cli/setup-platform.js +102 -0
- package/lib/cli/setup-secrets.js +18 -6
- package/lib/cli/setup-utility.js +97 -33
- package/lib/commands/datasource-capability-dimension-cli.js +128 -0
- package/lib/commands/datasource-capability-output.js +29 -0
- package/lib/commands/datasource-capability-relate-cli.js +140 -0
- package/lib/commands/datasource-capability.js +411 -0
- package/lib/commands/datasource-unified-test-cli.options.js +1 -1
- package/lib/commands/datasource.js +53 -13
- package/lib/commands/dev-down.js +3 -3
- package/lib/commands/dev-infra-gate.js +32 -0
- package/lib/commands/dev-init.js +13 -7
- package/lib/commands/dimension-value.js +179 -0
- package/lib/commands/dimension.js +330 -0
- package/lib/commands/integration-client.js +430 -0
- package/lib/commands/login-device.js +65 -30
- package/lib/commands/login.js +21 -10
- package/lib/commands/parameters-validate.js +78 -13
- package/lib/commands/repair-datasource-auto-rbac.js +166 -0
- package/lib/commands/repair-datasource-keys.js +10 -5
- package/lib/commands/repair-datasource.js +19 -7
- package/lib/commands/repair-env-template.js +4 -1
- package/lib/commands/repair-openapi-sync.js +172 -0
- package/lib/commands/repair-persist.js +102 -0
- package/lib/commands/repair-rbac-extract.js +27 -0
- package/lib/commands/repair-rbac-migrate.js +186 -0
- package/lib/commands/repair-rbac.js +225 -19
- package/lib/commands/repair-system-alignment.js +246 -0
- package/lib/commands/repair-system-permissions.js +168 -0
- package/lib/commands/repair.js +120 -354
- package/lib/commands/secure.js +1 -1
- package/lib/commands/setup-modes.js +455 -0
- package/lib/commands/setup-prompts.js +388 -0
- package/lib/commands/setup.js +149 -0
- package/lib/commands/teardown.js +228 -0
- package/lib/commands/test-e2e-external.js +4 -3
- package/lib/commands/up-common.js +97 -12
- package/lib/commands/up-dataplane.js +33 -11
- package/lib/commands/up-miso.js +7 -11
- package/lib/commands/upload.js +109 -23
- package/lib/commands/wizard-core-helpers.js +14 -11
- package/lib/commands/wizard-core.js +58 -15
- package/lib/commands/wizard-dataplane.js +2 -2
- package/lib/commands/wizard-entity-selection.js +72 -14
- package/lib/commands/wizard-headless.js +7 -3
- package/lib/commands/wizard-helpers.js +13 -1
- package/lib/commands/wizard.js +210 -61
- package/lib/constants/infra-compose-service-names.js +40 -0
- package/lib/core/env-reader.js +16 -3
- package/lib/core/secrets-admin-env.js +101 -0
- package/lib/core/secrets-ensure-infra.js +34 -1
- package/lib/core/secrets-ensure.js +88 -66
- package/lib/core/secrets-env-content.js +432 -0
- package/lib/core/secrets-env-write.js +27 -1
- package/lib/core/secrets-load.js +248 -0
- package/lib/core/secrets-names.js +32 -0
- package/lib/core/secrets.js +17 -757
- package/lib/datasource/capability/basic-exposure.js +76 -0
- package/lib/datasource/capability/capability-diff-slice.js +41 -0
- package/lib/datasource/capability/capability-key.js +34 -0
- package/lib/datasource/capability/capability-resolve.js +172 -0
- package/lib/datasource/capability/capability-storage-keys.js +22 -0
- package/lib/datasource/capability/copy-operations.js +348 -0
- package/lib/datasource/capability/copy-test-payload.js +139 -0
- package/lib/datasource/capability/create-operations.js +235 -0
- package/lib/datasource/capability/dimension-operations.js +151 -0
- package/lib/datasource/capability/dimension-validate.js +219 -0
- package/lib/datasource/capability/json-pointer.js +31 -0
- package/lib/datasource/capability/reference-rewrite.js +51 -0
- package/lib/datasource/capability/relate-operations.js +325 -0
- package/lib/datasource/capability/relate-validate.js +219 -0
- package/lib/datasource/capability/remove-operations.js +275 -0
- package/lib/datasource/capability/run-capability-copy.js +152 -0
- package/lib/datasource/capability/run-capability-diff.js +135 -0
- package/lib/datasource/capability/run-capability-dimension.js +291 -0
- package/lib/datasource/capability/run-capability-edit.js +377 -0
- package/lib/datasource/capability/run-capability-relate.js +193 -0
- package/lib/datasource/capability/run-capability-remove.js +105 -0
- package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
- package/lib/datasource/capability/validate-capability-slice.js +35 -0
- package/lib/datasource/list.js +136 -23
- package/lib/datasource/log-viewer.js +2 -4
- package/lib/datasource/unified-validation-run.js +51 -16
- package/lib/datasource/validate.js +53 -1
- package/lib/deployment/deploy-poll-ui.js +60 -0
- package/lib/deployment/deployer-status.js +29 -3
- package/lib/deployment/deployer.js +48 -30
- package/lib/deployment/environment.js +7 -2
- package/lib/deployment/poll-interval.js +72 -0
- package/lib/deployment/push.js +11 -9
- package/lib/external-system/deploy.js +4 -1
- package/lib/external-system/download.js +61 -32
- package/lib/external-system/sync-deploy-manifest.js +33 -0
- package/lib/generator/wizard-prompts.js +7 -1
- package/lib/generator/wizard.js +34 -0
- package/lib/infrastructure/index.js +49 -19
- package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
- package/lib/parameters/infra-kv-discovery.js +29 -4
- package/lib/parameters/infra-parameter-catalog.js +6 -3
- package/lib/parameters/infra-parameter-validate.js +67 -19
- package/lib/resolvers/datasource-resolver.js +53 -0
- package/lib/resolvers/dimension-file.js +52 -0
- package/lib/resolvers/manifest-resolver.js +133 -0
- package/lib/schema/external-datasource.schema.json +183 -53
- package/lib/schema/external-system.schema.json +23 -10
- package/lib/schema/infra.parameter.yaml +26 -11
- package/lib/schema/wizard-config.schema.json +2 -2
- package/lib/utils/aifabrix-config-dir-walk.js +40 -0
- package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
- package/lib/utils/app-run-containers.js +2 -2
- package/lib/utils/bash-secret-env.js +59 -0
- package/lib/utils/cli-secrets-error-format.js +78 -0
- package/lib/utils/cli-test-layout-chalk.js +31 -9
- package/lib/utils/cli-utils.js +4 -36
- package/lib/utils/datasource-test-run-display.js +8 -0
- package/lib/utils/dev-hosts-helper.js +3 -2
- package/lib/utils/dev-init-ssh-merge.js +2 -1
- package/lib/utils/docker-build.js +17 -9
- package/lib/utils/docker-reload-mount.js +127 -0
- package/lib/utils/external-readme.js +117 -4
- package/lib/utils/external-system-local-test-tty.js +3 -2
- package/lib/utils/external-system-readiness-core.js +45 -12
- package/lib/utils/external-system-readiness-deploy-display.js +3 -3
- package/lib/utils/external-system-readiness-display-internals.js +33 -3
- package/lib/utils/external-system-readiness-display.js +10 -1
- package/lib/utils/file-upload.js +40 -3
- package/lib/utils/health-check-db-init.js +107 -0
- package/lib/utils/health-check-public-warn.js +69 -0
- package/lib/utils/health-check-url.js +19 -4
- package/lib/utils/health-check.js +135 -105
- package/lib/utils/help-builder.js +5 -1
- package/lib/utils/image-name.js +34 -7
- package/lib/utils/integration-file-backup.js +74 -0
- package/lib/utils/mutagen-install.js +30 -3
- package/lib/utils/paths.js +108 -25
- package/lib/utils/postgres-wipe.js +212 -0
- package/lib/utils/register-aifabrix-shell-env.js +15 -0
- package/lib/utils/remote-dev-auth.js +21 -5
- package/lib/utils/remote-docker-env.js +9 -1
- package/lib/utils/remote-secrets-loader.js +42 -3
- package/lib/utils/resolve-docker-image-ref.js +9 -3
- package/lib/utils/secrets-ancestor-paths.js +47 -0
- package/lib/utils/secrets-helpers.js +17 -10
- package/lib/utils/secrets-kv-refs.js +42 -0
- package/lib/utils/secrets-kv-scope.js +19 -2
- package/lib/utils/secrets-materialize-local.js +134 -0
- package/lib/utils/secrets-path.js +24 -10
- package/lib/utils/secrets-utils.js +2 -2
- package/lib/utils/system-builder-root.js +34 -0
- package/lib/utils/url-declarative-resolve-build.js +6 -1
- package/lib/utils/url-declarative-runtime-base-path.js +32 -0
- package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
- package/lib/utils/urls-local-registry.js +73 -20
- package/lib/utils/validation-poll-ui.js +81 -0
- package/lib/utils/validation-run-poll.js +29 -5
- package/lib/utils/with-muted-logger.js +53 -0
- package/package.json +1 -1
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/rbac.yaml +10 -10
- package/templates/applications/keycloak/env.template +8 -6
- package/templates/applications/miso-controller/application.yaml +7 -0
- package/templates/applications/miso-controller/env.template +7 -7
- package/templates/applications/miso-controller/rbac.yaml +9 -9
- package/templates/external-system/README.md.hbs +89 -102
- package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
- package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
- package/.nyc_output/processinfo/index.json +0 -1
- package/lib/api/service-users.api.js +0 -150
- package/lib/api/types/service-users.types.js +0 -65
- package/lib/cli/setup-service-user.js +0 -187
- package/lib/commands/service-user.js +0 -429
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {
|
|
2
|
+
formatBlockingError,
|
|
3
|
+
formatIssue,
|
|
4
|
+
formatSuccessLine,
|
|
5
|
+
sectionTitle
|
|
6
|
+
} = require('../utils/cli-test-layout-chalk');
|
|
2
7
|
/**
|
|
3
8
|
* @fileoverview CLI handler: parameters validate (kv:// vs infra.parameter.yaml)
|
|
4
9
|
* @author AI Fabrix Team
|
|
5
10
|
* @version 2.0.0
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
|
-
const chalk = require('chalk');
|
|
9
13
|
const logger = require('../utils/logger');
|
|
10
14
|
const pathsUtil = require('../utils/paths');
|
|
11
15
|
const {
|
|
@@ -17,37 +21,98 @@ const {
|
|
|
17
21
|
validateCatalogRequiredGenerators
|
|
18
22
|
} = require('../parameters/infra-parameter-validate');
|
|
19
23
|
|
|
24
|
+
function _loadCatalog(catalogPath) {
|
|
25
|
+
try {
|
|
26
|
+
const catalog = catalogPath
|
|
27
|
+
? loadInfraParameterCatalog(catalogPath)
|
|
28
|
+
: getInfraParameterCatalog();
|
|
29
|
+
return { catalog, catalogPath: catalogPath || 'lib/schema/infra.parameter.yaml' };
|
|
30
|
+
} catch (e) {
|
|
31
|
+
logger.log(formatBlockingError(`Could not load infra parameter catalog: ${e.message}`));
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function _printScanSummary(kv) {
|
|
37
|
+
logger.log(sectionTitle('Scan summary:'));
|
|
38
|
+
logger.log(
|
|
39
|
+
` Apps scanned: ${kv.summary.scannedApps.length} (builder/* only; integration/* is skipped)`
|
|
40
|
+
);
|
|
41
|
+
logger.log(` env.template files: ${kv.summary.scannedEnvTemplates.length}`);
|
|
42
|
+
logger.log(` kv:// keys (unique): ${kv.summary.kvKeysCount}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _printCatalogHint(catalogPath) {
|
|
46
|
+
logger.log(
|
|
47
|
+
formatIssue(
|
|
48
|
+
`Catalog: ${catalogPath}`,
|
|
49
|
+
'Use --catalog to validate against a different infra.parameter.yaml.'
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function _printKvErrors(kv) {
|
|
55
|
+
logger.log(sectionTitle('Missing catalog entries:'));
|
|
56
|
+
kv.errors.forEach((err) => {
|
|
57
|
+
if (err.key === '__read_error__') {
|
|
58
|
+
logger.log(
|
|
59
|
+
formatIssue(
|
|
60
|
+
`Could not read ${err.envTemplatePath}`,
|
|
61
|
+
err.message || 'Check file permissions and retry.'
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
logger.log(
|
|
67
|
+
formatIssue(
|
|
68
|
+
`Unknown kv:// key "${err.key}" in ${err.envTemplatePath}`,
|
|
69
|
+
'Add a matching entry (or keyPattern) in lib/schema/infra.parameter.yaml.'
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function _printSuccess(kv, catalogPath, verbose) {
|
|
76
|
+
logger.log(formatSuccessLine('parameters validate: catalog OK; workspace kv:// keys covered.'));
|
|
77
|
+
logger.log(` Catalog: ${catalogPath}`);
|
|
78
|
+
_printScanSummary(kv);
|
|
79
|
+
if (verbose) {
|
|
80
|
+
logger.log(sectionTitle('Files scanned:'));
|
|
81
|
+
kv.summary.scannedEnvTemplates.forEach((p) => logger.log(` - ${p}`));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
20
85
|
/**
|
|
21
86
|
* Run catalog + workspace kv:// validation.
|
|
22
87
|
* @param {Object} [options] - CLI options
|
|
23
88
|
* @returns {Promise<{ valid: boolean }>}
|
|
24
89
|
*/
|
|
25
90
|
async function handleParametersValidate(options = {}) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
catalog = options.catalogPath
|
|
29
|
-
? loadInfraParameterCatalog(options.catalogPath)
|
|
30
|
-
: getInfraParameterCatalog();
|
|
31
|
-
} catch (e) {
|
|
32
|
-
logger.log(formatBlockingError(`Could not load infra parameter catalog: ${e.message}`));
|
|
91
|
+
const loaded = _loadCatalog(options.catalogPath);
|
|
92
|
+
if (!loaded) {
|
|
33
93
|
return { valid: false };
|
|
34
94
|
}
|
|
95
|
+
const { catalog, catalogPath } = loaded;
|
|
35
96
|
|
|
36
97
|
const reqGen = validateCatalogRequiredGenerators(catalog.data);
|
|
37
98
|
if (!reqGen.valid) {
|
|
38
99
|
logger.log(formatBlockingError('Catalog requiredForLocal / generator issues:'));
|
|
39
|
-
reqGen.errors.forEach((err) =>
|
|
100
|
+
reqGen.errors.forEach((err) =>
|
|
101
|
+
logger.log(formatIssue(err, 'Fix infra.parameter.yaml generator for requiredForLocal entry.'))
|
|
102
|
+
);
|
|
40
103
|
return { valid: false };
|
|
41
104
|
}
|
|
42
105
|
|
|
43
106
|
const kv = validateWorkspaceKvRefsAgainstCatalog(catalog, pathsUtil);
|
|
44
107
|
if (!kv.valid) {
|
|
45
|
-
logger.log(formatBlockingError('
|
|
46
|
-
|
|
108
|
+
logger.log(formatBlockingError('Missing infra.parameter.yaml coverage for kv:// keys.'));
|
|
109
|
+
_printCatalogHint(catalogPath);
|
|
110
|
+
_printScanSummary(kv);
|
|
111
|
+
_printKvErrors(kv);
|
|
47
112
|
return { valid: false };
|
|
48
113
|
}
|
|
49
114
|
|
|
50
|
-
|
|
115
|
+
_printSuccess(kv, catalogPath, Boolean(options.verbose));
|
|
51
116
|
return { valid: true };
|
|
52
117
|
}
|
|
53
118
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-RBAC operation key normalization for datasource OpenAPI sections.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Normalize operation keys for RBAC safety and consistency
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.2.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
function safeString(v) {
|
|
12
|
+
return typeof v === 'string' && v.trim() ? v.trim() : '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function toCamelCaseKey(opKey) {
|
|
16
|
+
const s = safeString(opKey);
|
|
17
|
+
if (!s) return '';
|
|
18
|
+
// Split on common separators, keep only alphanumerics
|
|
19
|
+
const parts = s
|
|
20
|
+
.split(/[^a-zA-Z0-9]+/g)
|
|
21
|
+
.map((p) => p.trim())
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
if (parts.length === 0) return '';
|
|
24
|
+
const first = parts[0].charAt(0).toLowerCase() + parts[0].slice(1);
|
|
25
|
+
const rest = parts
|
|
26
|
+
.slice(1)
|
|
27
|
+
.map((p) => (p.charAt(0).toUpperCase() + p.slice(1)));
|
|
28
|
+
const out = [first, ...rest].join('');
|
|
29
|
+
// Must match ^[a-z][a-zA-Z0-9]*$
|
|
30
|
+
return /^[a-z][a-zA-Z0-9]*$/.test(out) ? out : '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildRenameMap(operationMap) {
|
|
34
|
+
if (!operationMap || typeof operationMap !== 'object' || Array.isArray(operationMap)) return {};
|
|
35
|
+
const renameMap = {};
|
|
36
|
+
for (const k of Object.keys(operationMap)) {
|
|
37
|
+
// If it's already schema-valid but contains capitals, normalize to lowercase to align
|
|
38
|
+
// with RBAC permission name restrictions (external-system schema forbids A-Z).
|
|
39
|
+
if (/^[a-z][a-zA-Z0-9]*$/.test(k)) {
|
|
40
|
+
if (/[A-Z]/.test(k)) {
|
|
41
|
+
renameMap[k] = k.toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const camel = toCamelCaseKey(k);
|
|
46
|
+
if (!camel || camel === k) continue;
|
|
47
|
+
// Use lowercase key to keep RBAC permission names schema-valid.
|
|
48
|
+
renameMap[k] = camel.toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
return renameMap;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toKebabAliasKey(opKey) {
|
|
54
|
+
const s = safeString(opKey);
|
|
55
|
+
if (!s) return '';
|
|
56
|
+
const withHyphens = s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
57
|
+
const cleaned = withHyphens.replace(/[^a-z0-9-]+/g, '-');
|
|
58
|
+
return cleaned.replace(/-+/g, '-').replace(/^-+|-+$/g, '');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildAliasMapFromCanonicalOps(canonicalOps) {
|
|
62
|
+
if (!canonicalOps || typeof canonicalOps !== 'object' || Array.isArray(canonicalOps)) return {};
|
|
63
|
+
const aliasMap = {};
|
|
64
|
+
for (const k of Object.keys(canonicalOps)) {
|
|
65
|
+
// If canonical is createBasic, allow alias create-basic to map back to createBasic
|
|
66
|
+
const alias = toKebabAliasKey(k);
|
|
67
|
+
if (!alias || alias === k) continue;
|
|
68
|
+
if (aliasMap[alias]) continue;
|
|
69
|
+
aliasMap[alias] = k;
|
|
70
|
+
}
|
|
71
|
+
return aliasMap;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renameKeysInObject(obj, renameMap) {
|
|
75
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return false;
|
|
76
|
+
let updated = false;
|
|
77
|
+
for (const [oldKey, newKey] of Object.entries(renameMap)) {
|
|
78
|
+
if (!oldKey || !newKey || oldKey === newKey) continue;
|
|
79
|
+
if (obj[oldKey] === undefined) continue;
|
|
80
|
+
if (obj[newKey] !== undefined) continue; // avoid collisions
|
|
81
|
+
obj[newKey] = obj[oldKey];
|
|
82
|
+
delete obj[oldKey];
|
|
83
|
+
updated = true;
|
|
84
|
+
}
|
|
85
|
+
return updated;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function mergeAndDeleteAliasKeys(obj, renameMap) {
|
|
89
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return false;
|
|
90
|
+
let updated = false;
|
|
91
|
+
for (const [oldKey, newKey] of Object.entries(renameMap)) {
|
|
92
|
+
if (!oldKey || !newKey || oldKey === newKey) continue;
|
|
93
|
+
if (obj[oldKey] === undefined) continue;
|
|
94
|
+
if (obj[newKey] === undefined) continue;
|
|
95
|
+
|
|
96
|
+
const oldVal = obj[oldKey];
|
|
97
|
+
const newVal = obj[newKey];
|
|
98
|
+
if (Array.isArray(oldVal) && Array.isArray(newVal)) {
|
|
99
|
+
const merged = [...new Set([...newVal, ...oldVal])];
|
|
100
|
+
obj[newKey] = merged;
|
|
101
|
+
}
|
|
102
|
+
delete obj[oldKey];
|
|
103
|
+
updated = true;
|
|
104
|
+
}
|
|
105
|
+
return updated;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function renameScenarioOperations(parsed, renameMap) {
|
|
109
|
+
const scenarios = parsed?.testPayload?.scenarios;
|
|
110
|
+
if (!Array.isArray(scenarios)) return false;
|
|
111
|
+
let updated = false;
|
|
112
|
+
for (const sc of scenarios) {
|
|
113
|
+
const op = safeString(sc?.operation);
|
|
114
|
+
if (!op) continue;
|
|
115
|
+
const newOp = renameMap[op];
|
|
116
|
+
if (!newOp) continue;
|
|
117
|
+
sc.operation = newOp;
|
|
118
|
+
updated = true;
|
|
119
|
+
}
|
|
120
|
+
return updated;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* When OpenAPI autoRbac is enabled, ensure operation keys are schema-valid and consistent across:
|
|
125
|
+
* - openapi.operations
|
|
126
|
+
* - execution.cip.operations
|
|
127
|
+
* - testPayload.scenarios[].operation
|
|
128
|
+
*/
|
|
129
|
+
function normalizeAutoRbacOperationKeys(parsed, changes) {
|
|
130
|
+
const openapi = parsed?.openapi;
|
|
131
|
+
if (!openapi || typeof openapi !== 'object') return false;
|
|
132
|
+
if (openapi.autoRbac !== true) return false;
|
|
133
|
+
|
|
134
|
+
const renameMap = {
|
|
135
|
+
...buildRenameMap(openapi.operations),
|
|
136
|
+
...buildAliasMapFromCanonicalOps(openapi.operations)
|
|
137
|
+
};
|
|
138
|
+
const keysToRename = Object.keys(renameMap);
|
|
139
|
+
if (keysToRename.length === 0) return false;
|
|
140
|
+
|
|
141
|
+
const cipOps = parsed?.execution?.cip?.operations;
|
|
142
|
+
let updated = false;
|
|
143
|
+
updated = renameKeysInObject(openapi.operations, renameMap) || updated;
|
|
144
|
+
updated = renameKeysInObject(cipOps, renameMap) || updated;
|
|
145
|
+
updated = renameScenarioOperations(parsed, renameMap) || updated;
|
|
146
|
+
|
|
147
|
+
const wsAllowed = parsed?.fieldMappings?.writeSurface?.allowed;
|
|
148
|
+
updated = renameKeysInObject(wsAllowed, renameMap) || updated;
|
|
149
|
+
// If both alias + canonical exist, merge and drop alias (schema requires canonical camelCase keys).
|
|
150
|
+
updated = mergeAndDeleteAliasKeys(wsAllowed, renameMap) || updated;
|
|
151
|
+
|
|
152
|
+
if (updated && Array.isArray(changes)) {
|
|
153
|
+
const pairs = keysToRename.map((k) => `${k}→${renameMap[k]}`).join(', ');
|
|
154
|
+
changes.push(
|
|
155
|
+
`Normalized autoRbac operation keys (permission names are lowercase per schema; kebab aliases fold into canonical keys): ${pairs}`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return updated;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
normalizeAutoRbacOperationKeys,
|
|
164
|
+
toCamelCaseKey
|
|
165
|
+
};
|
|
166
|
+
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
|
|
18
|
+
const { backupIntegrationFile } = require('../utils/integration-file-backup');
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Returns suffix from a canonical-format filename: <systemKey>-datasource-<suffix>.<ext>.
|
|
@@ -112,13 +113,12 @@ function isFilenameAlreadyCanonical(fileName, systemKey) {
|
|
|
112
113
|
* @param {string} appPath - Application directory path
|
|
113
114
|
* @param {string[]} datasourceFiles - Current list of datasource filenames
|
|
114
115
|
* @param {string} systemKey - System key
|
|
115
|
-
* @param {
|
|
116
|
-
* @param {boolean} dryRun - If true, do not write or rename
|
|
117
|
-
* @param {string[]} changes - Array to append change descriptions to
|
|
116
|
+
* @param {{ variables: Object, dryRun: boolean, changes: string[], backupCtx?: Object }} writeOpts
|
|
118
117
|
* @returns {{ updated: boolean, datasourceFiles: string[] }} Updated flag and new list of datasource filenames
|
|
119
118
|
*/
|
|
120
119
|
/* eslint-disable max-lines-per-function, max-statements, complexity -- Normalization loops and branching per file */
|
|
121
|
-
function normalizeDatasourceKeysAndFilenames(appPath, datasourceFiles, systemKey,
|
|
120
|
+
function normalizeDatasourceKeysAndFilenames(appPath, datasourceFiles, systemKey, writeOpts) {
|
|
121
|
+
const { variables, dryRun, changes, backupCtx } = writeOpts;
|
|
122
122
|
if (!datasourceFiles || datasourceFiles.length === 0) {
|
|
123
123
|
return { updated: false, datasourceFiles: datasourceFiles || [] };
|
|
124
124
|
}
|
|
@@ -174,7 +174,11 @@ function normalizeDatasourceKeysAndFilenames(appPath, datasourceFiles, systemKey
|
|
|
174
174
|
|
|
175
175
|
if (parsed.key !== canonicalKey) {
|
|
176
176
|
parsed.key = canonicalKey;
|
|
177
|
-
if (!dryRun)
|
|
177
|
+
if (!dryRun) {
|
|
178
|
+
const fullPath = path.join(appPath, fileName);
|
|
179
|
+
backupIntegrationFile(fullPath, backupCtx);
|
|
180
|
+
writeConfigFile(fullPath, parsed);
|
|
181
|
+
}
|
|
178
182
|
changes.push(`${fileName}: key → ${canonicalKey}`);
|
|
179
183
|
updated = true;
|
|
180
184
|
}
|
|
@@ -182,6 +186,7 @@ function normalizeDatasourceKeysAndFilenames(appPath, datasourceFiles, systemKey
|
|
|
182
186
|
const oldPath = path.join(appPath, fileName);
|
|
183
187
|
const newPath = path.join(appPath, canonicalFileName);
|
|
184
188
|
if (!dryRun && fs.existsSync(oldPath) && !fs.existsSync(newPath)) {
|
|
189
|
+
backupIntegrationFile(oldPath, backupCtx);
|
|
185
190
|
fs.renameSync(oldPath, newPath);
|
|
186
191
|
}
|
|
187
192
|
changes.push(`Renamed ${fileName} → ${canonicalFileName}`);
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
'use strict';
|
|
11
11
|
|
|
12
12
|
const { repairOpenapiSection } = require('./repair-datasource-openapi');
|
|
13
|
+
const { normalizeAutoRbacOperationKeys } = require('./repair-datasource-auto-rbac');
|
|
13
14
|
|
|
14
15
|
const DEFAULT_SYNC = {
|
|
15
16
|
mode: 'pull',
|
|
@@ -442,13 +443,8 @@ function repairDatasourceFile(parsed, options = {}, changes = []) {
|
|
|
442
443
|
let updated = false;
|
|
443
444
|
const none = isNoneEntityType(parsed?.entityType);
|
|
444
445
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
updated = repairRootDimensionsFromAttributes(parsed, out) || updated;
|
|
448
|
-
updated = repairMetadataSchemaFromAttributes(parsed, out) || updated;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
updated = repairOpenapiSection(parsed, out) || updated;
|
|
446
|
+
updated = applyBaseDatasourceRepairs(parsed, none, out) || updated;
|
|
447
|
+
updated = applyOpenapiDatasourceRepairs(parsed, out) || updated;
|
|
452
448
|
|
|
453
449
|
if (options.expose) {
|
|
454
450
|
updated = repairExposeFromAttributes(parsed, out) || updated;
|
|
@@ -467,6 +463,22 @@ function repairDatasourceFile(parsed, options = {}, changes = []) {
|
|
|
467
463
|
return { updated, changes: out };
|
|
468
464
|
}
|
|
469
465
|
|
|
466
|
+
function applyBaseDatasourceRepairs(parsed, none, changes) {
|
|
467
|
+
if (none) return false;
|
|
468
|
+
let updated = false;
|
|
469
|
+
updated = repairDimensionBindingShape(parsed, changes) || updated;
|
|
470
|
+
updated = repairRootDimensionsFromAttributes(parsed, changes) || updated;
|
|
471
|
+
updated = repairMetadataSchemaFromAttributes(parsed, changes) || updated;
|
|
472
|
+
return updated;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function applyOpenapiDatasourceRepairs(parsed, changes) {
|
|
476
|
+
let updated = false;
|
|
477
|
+
updated = repairOpenapiSection(parsed, changes) || updated;
|
|
478
|
+
updated = normalizeAutoRbacOperationKeys(parsed, changes) || updated;
|
|
479
|
+
return updated;
|
|
480
|
+
}
|
|
481
|
+
|
|
470
482
|
module.exports = {
|
|
471
483
|
getAttributeKeys,
|
|
472
484
|
parsePathsFromExpressions,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const fs = require('fs');
|
|
12
|
+
const { backupIntegrationFile } = require('../utils/integration-file-backup');
|
|
12
13
|
const { systemKeyToKvPrefix, kvEnvKeyToPath, securityKeyToVar } = require('../utils/credential-secrets-env');
|
|
13
14
|
const { extractEnvTemplate } = require('../generator/split');
|
|
14
15
|
const { generateExternalEnvTemplateContent } = require('../utils/external-env-template');
|
|
@@ -235,9 +236,10 @@ function mergeEnvTemplateContent(content, expectedByKey) {
|
|
|
235
236
|
* @param {string} systemKey - System key
|
|
236
237
|
* @param {boolean} dryRun - If true, do not write
|
|
237
238
|
* @param {string[]} changes - Array to append change descriptions to
|
|
239
|
+
* @param {Object} [backupCtx] - Optional backup context for backupIntegrationFile
|
|
238
240
|
* @returns {boolean} True if env.template was repaired or created
|
|
239
241
|
*/
|
|
240
|
-
function repairEnvTemplate(appPath, systemParsed, systemKey, dryRun, changes) {
|
|
242
|
+
function repairEnvTemplate(appPath, systemParsed, systemKey, dryRun, changes, backupCtx) {
|
|
241
243
|
const effective = buildEffectiveConfiguration(systemParsed, systemKey);
|
|
242
244
|
const expectedByKey = buildExpectedByKey(effective);
|
|
243
245
|
const envPath = path.join(appPath, 'env.template');
|
|
@@ -253,6 +255,7 @@ function repairEnvTemplate(appPath, systemParsed, systemKey, dryRun, changes) {
|
|
|
253
255
|
const { output, changed } = mergeEnvTemplateContent(content, expectedByKey);
|
|
254
256
|
|
|
255
257
|
if (changed && !dryRun) {
|
|
258
|
+
backupIntegrationFile(envPath, backupCtx);
|
|
256
259
|
fs.writeFileSync(envPath, output, { mode: 0o644, encoding: 'utf8' });
|
|
257
260
|
}
|
|
258
261
|
if (changed) {
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload OpenAPI specs (integration/<systemKey>/openapi/*.json) to dataplane so MCP generation
|
|
3
|
+
* can resolve `openapi.documentKey` and store mcpContract per datasource.
|
|
4
|
+
*
|
|
5
|
+
* This is a *repair* action: upload should remain non-mutating.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Repair action: sync OpenAPI files for MCP
|
|
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
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
18
|
+
const { getDeploymentAuth, requireBearerForDataplanePipeline } = require('../utils/token-manager');
|
|
19
|
+
const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
|
|
20
|
+
const { uploadFileAs } = require('../utils/file-upload');
|
|
21
|
+
const { listOpenAPIFiles } = require('../api/external-systems.api');
|
|
22
|
+
|
|
23
|
+
async function fileExistsAsync(filePath) {
|
|
24
|
+
try {
|
|
25
|
+
await fs.promises.access(filePath);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function resolveDataplaneAndAuth(systemKey) {
|
|
33
|
+
const { resolveEnvironment } = require('../core/config');
|
|
34
|
+
const environment = await resolveEnvironment();
|
|
35
|
+
const controllerUrl = await resolveControllerUrl();
|
|
36
|
+
const authConfig = await getDeploymentAuth(controllerUrl, environment, systemKey);
|
|
37
|
+
requireBearerForDataplanePipeline(authConfig);
|
|
38
|
+
const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig, { silent: true });
|
|
39
|
+
return { dataplaneUrl, authConfig };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function documentKeyToLocalOpenApiPath(appPath, systemKey, documentKey) {
|
|
43
|
+
const openapiDir = path.join(appPath, 'openapi');
|
|
44
|
+
const suffix = documentKey.startsWith(systemKey + '-') ? documentKey.slice(systemKey.length + 1) : documentKey;
|
|
45
|
+
return path.join(openapiDir, `${suffix}.json`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function uploadOneOpenApiFile(dataplaneUrl, authConfig, systemKey, localPath, documentKey) {
|
|
49
|
+
const url =
|
|
50
|
+
`${dataplaneUrl.replace(/\/$/, '')}` +
|
|
51
|
+
`/api/v1/specs/upload?systemIdOrKey=${encodeURIComponent(systemKey)}`;
|
|
52
|
+
const res = await uploadFileAs(url, localPath, `${documentKey}.json`, 'file', authConfig);
|
|
53
|
+
if (!res || res.success !== true) {
|
|
54
|
+
const msg = res && typeof res.formattedError === 'string'
|
|
55
|
+
? res.formattedError
|
|
56
|
+
: (res && typeof res.error === 'string' ? res.error : 'OpenAPI upload failed');
|
|
57
|
+
throw new Error(msg);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {Object|null|undefined} res
|
|
63
|
+
* @param {string} fallback
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
function failureMessageFromApiResult(res, fallback) {
|
|
67
|
+
if (res && typeof res.formattedError === 'string') return res.formattedError;
|
|
68
|
+
if (res && typeof res.error === 'string') return res.error;
|
|
69
|
+
return fallback;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {Object} res
|
|
74
|
+
* @returns {Array<unknown>}
|
|
75
|
+
*/
|
|
76
|
+
function extractOpenApiFileListItems(res) {
|
|
77
|
+
const data = res && res.data;
|
|
78
|
+
if (data && Array.isArray(data.data)) return data.data;
|
|
79
|
+
if (data && Array.isArray(data.items)) return data.items;
|
|
80
|
+
if (res && Array.isArray(res.items)) return res.items;
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function getExistingOpenApiKeys(dataplaneUrl, systemKey, authConfig) {
|
|
85
|
+
// Note: this endpoint is system-scoped and does not require guessing externalSystemId.
|
|
86
|
+
// Dataplane caps pageSize at 100 (OpenAPI route schema); keep within bounds.
|
|
87
|
+
const res = await listOpenAPIFiles(dataplaneUrl, systemKey, authConfig, { pageSize: 100 });
|
|
88
|
+
if (!res || res.success !== true) {
|
|
89
|
+
throw new Error(failureMessageFromApiResult(res, 'Failed to list OpenAPI files'));
|
|
90
|
+
}
|
|
91
|
+
const keys = new Set();
|
|
92
|
+
for (const it of extractOpenApiFileListItems(res)) {
|
|
93
|
+
if (it && typeof it.key === 'string') keys.add(it.key);
|
|
94
|
+
}
|
|
95
|
+
return keys;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readDatasourceDocumentKey(appPath, datasourceFileName) {
|
|
99
|
+
const dsPath = path.join(appPath, datasourceFileName);
|
|
100
|
+
if (!fs.existsSync(dsPath)) return null;
|
|
101
|
+
const parsed = require('../utils/config-format').loadConfigFile(dsPath);
|
|
102
|
+
const openapi = parsed && typeof parsed.openapi === 'object' && !Array.isArray(parsed.openapi) ? parsed.openapi : null;
|
|
103
|
+
return openapi && typeof openapi.documentKey === 'string' ? openapi.documentKey : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {Object} opts
|
|
108
|
+
* @param {string} opts.appPath
|
|
109
|
+
* @param {string} opts.systemKey
|
|
110
|
+
* @param {string[]} opts.datasourceFiles
|
|
111
|
+
* @returns {Promise<{ uploaded: number, skipped: number }>}
|
|
112
|
+
*/
|
|
113
|
+
async function syncOpenApiFilesForMcp(opts) {
|
|
114
|
+
const { appPath, systemKey, datasourceFiles } = opts;
|
|
115
|
+
const openapiDir = path.join(appPath, 'openapi');
|
|
116
|
+
if (!(await fileExistsAsync(openapiDir))) return { uploaded: 0, skipped: 0 };
|
|
117
|
+
|
|
118
|
+
const { dataplaneUrl, authConfig } = await resolveDataplaneAndAuth(systemKey);
|
|
119
|
+
const existingKeys = await getExistingOpenApiKeys(dataplaneUrl, systemKey, authConfig);
|
|
120
|
+
const uploadedKeys = new Set(existingKeys);
|
|
121
|
+
let uploaded = 0;
|
|
122
|
+
let skipped = 0;
|
|
123
|
+
|
|
124
|
+
for (const fileName of datasourceFiles || []) {
|
|
125
|
+
const documentKey = readDatasourceDocumentKey(appPath, fileName);
|
|
126
|
+
if (!documentKey || uploadedKeys.has(documentKey)) continue;
|
|
127
|
+
|
|
128
|
+
const localPath = documentKeyToLocalOpenApiPath(appPath, systemKey, documentKey);
|
|
129
|
+
if (!(await fileExistsAsync(localPath))) {
|
|
130
|
+
skipped += 1;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await uploadOneOpenApiFile(dataplaneUrl, authConfig, systemKey, localPath, documentKey);
|
|
135
|
+
uploadedKeys.add(documentKey);
|
|
136
|
+
uploaded += 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { uploaded, skipped };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Repair wrapper that returns change-log lines (or []).
|
|
144
|
+
* @param {Object} opts
|
|
145
|
+
* @param {boolean} opts.enabled
|
|
146
|
+
* @param {boolean} opts.dryRun
|
|
147
|
+
* @param {string} opts.appPath
|
|
148
|
+
* @param {string} opts.systemKey
|
|
149
|
+
* @param {string[]} opts.datasourceFiles
|
|
150
|
+
* @returns {Promise<string[]>}
|
|
151
|
+
*/
|
|
152
|
+
async function maybeSyncOpenApiFilesForMcp(opts) {
|
|
153
|
+
if (!opts.enabled || opts.dryRun) return [];
|
|
154
|
+
const r = await syncOpenApiFilesForMcp(opts);
|
|
155
|
+
const lines = [];
|
|
156
|
+
if (r.uploaded > 0) {
|
|
157
|
+
lines.push(`Uploaded ${r.uploaded} OpenAPI file(s) for MCP (keyed by openapi.documentKey)`);
|
|
158
|
+
}
|
|
159
|
+
if (r.skipped > 0) {
|
|
160
|
+
lines.push(
|
|
161
|
+
`Skipped ${r.skipped} OpenAPI upload(s) (missing local integration/<systemKey>/openapi/<name>.json)`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return lines;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = {
|
|
168
|
+
documentKeyToLocalOpenApiPath,
|
|
169
|
+
maybeSyncOpenApiFilesForMcp,
|
|
170
|
+
syncOpenApiFilesForMcp
|
|
171
|
+
};
|
|
172
|
+
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence helpers for repair: write config files, regenerate manifest, regenerate README.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from repair.js to keep file size under 500 lines.
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview External integration repair helpers (persistence)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const chalk = require('chalk');
|
|
14
|
+
|
|
15
|
+
const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
|
|
16
|
+
const { getDeployJsonPath } = require('../utils/paths');
|
|
17
|
+
const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
|
|
18
|
+
const { writeConfigFile, writeYamlPreservingComments, isYamlPath } = require('../utils/config-format');
|
|
19
|
+
const { backupIntegrationFile } = require('../utils/integration-file-backup');
|
|
20
|
+
const logger = require('../utils/logger');
|
|
21
|
+
const generator = require('../generator');
|
|
22
|
+
const { generateReadmeFromDeployJson } = require('../generator/split-readme');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* README "Files" section should match integration config format on disk (YAML vs JSON).
|
|
26
|
+
* @param {string} appPath - Integration directory
|
|
27
|
+
* @returns {string} '.yaml' or '.json'
|
|
28
|
+
*/
|
|
29
|
+
function inferExternalReadmeFileExt(appPath) {
|
|
30
|
+
try {
|
|
31
|
+
const configPath = resolveApplicationConfigPath(appPath);
|
|
32
|
+
const ext = path.extname(configPath).toLowerCase();
|
|
33
|
+
if (ext === '.yaml' || ext === '.yml') return '.yaml';
|
|
34
|
+
} catch {
|
|
35
|
+
/* use default */
|
|
36
|
+
}
|
|
37
|
+
return '.json';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function regenerateManifest(appName, appPath, changes, backupCtx) {
|
|
41
|
+
try {
|
|
42
|
+
const deployPath = getDeployJsonPath(appName, 'external', true);
|
|
43
|
+
backupIntegrationFile(deployPath, backupCtx);
|
|
44
|
+
const outPath = await generator.generateDeployJson(appName, { appPath });
|
|
45
|
+
changes.push(`Regenerated ${path.basename(outPath)}`);
|
|
46
|
+
return true;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
logger.log(chalk.yellow(`⚠ Manifest regeneration skipped: ${err.message}`));
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Regenerates README.md from deployment manifest when options.doc is set.
|
|
55
|
+
* @param {string} appName - Application name
|
|
56
|
+
* @param {string} appPath - Application path
|
|
57
|
+
* @param {Object} options - Options (doc, dryRun)
|
|
58
|
+
* @param {string[]} changes - Array to append change messages to
|
|
59
|
+
* @returns {Promise<boolean>} True if README was regenerated
|
|
60
|
+
*/
|
|
61
|
+
async function regenerateReadmeIfRequested(appName, appPath, options, changes) {
|
|
62
|
+
if (!options.doc) return false;
|
|
63
|
+
const deployJsonPath = getDeployJsonPath(appName, 'external', true);
|
|
64
|
+
if (!fs.existsSync(deployJsonPath) && !options.dryRun) {
|
|
65
|
+
await regenerateManifest(appName, appPath, changes, options.backupCtx);
|
|
66
|
+
}
|
|
67
|
+
if (!fs.existsSync(deployJsonPath)) return false;
|
|
68
|
+
try {
|
|
69
|
+
const deployment = JSON.parse(fs.readFileSync(deployJsonPath, 'utf8'));
|
|
70
|
+
const fileExt = inferExternalReadmeFileExt(appPath);
|
|
71
|
+
const readmeContent = generateReadmeFromDeployJson(deployment, { fileExt });
|
|
72
|
+
const readmePath = path.join(appPath, 'README.md');
|
|
73
|
+
if (!options.dryRun) {
|
|
74
|
+
backupIntegrationFile(readmePath, options.backupCtx);
|
|
75
|
+
fs.writeFileSync(readmePath, readmeContent, { mode: 0o644, encoding: 'utf8' });
|
|
76
|
+
}
|
|
77
|
+
changes.push('Regenerated README.md from deployment manifest');
|
|
78
|
+
return true;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
logger.log(chalk.yellow(`⚠ Could not regenerate README: ${err.message}`));
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function persistChangesAndRegenerate(opts) {
|
|
86
|
+
const { configPath, variables, appName, appPath, changes, originalYamlContent, backupCtx } = opts;
|
|
87
|
+
backupIntegrationFile(configPath, backupCtx);
|
|
88
|
+
if (originalYamlContent !== null && originalYamlContent !== undefined && typeof originalYamlContent === 'string' && isYamlPath(configPath)) {
|
|
89
|
+
writeYamlPreservingComments(configPath, originalYamlContent, variables);
|
|
90
|
+
} else {
|
|
91
|
+
writeConfigFile(configPath, variables);
|
|
92
|
+
}
|
|
93
|
+
logger.log(formatSuccessLine(`Updated ${path.basename(configPath)}`));
|
|
94
|
+
changes.forEach(c => logger.log(chalk.gray(` ${c}`)));
|
|
95
|
+
return regenerateManifest(appName, appPath, changes, backupCtx);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
persistChangesAndRegenerate,
|
|
100
|
+
regenerateReadmeIfRequested
|
|
101
|
+
};
|
|
102
|
+
|