@aifabrix/builder 2.41.0 → 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.
Files changed (138) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +1 -1
  3. package/integration/hubspot/README.md +8 -4
  4. package/integration/hubspot/application.json +54 -0
  5. package/integration/hubspot/create-hubspot.js +9 -136
  6. package/integration/hubspot/env.template +3 -4
  7. package/integration/hubspot/hubspot-datasource-company.json +343 -5
  8. package/integration/hubspot/hubspot-datasource-contact.json +413 -5
  9. package/integration/hubspot/hubspot-datasource-deal.json +341 -4
  10. package/integration/hubspot/hubspot-datasource-users.json +116 -0
  11. package/integration/hubspot/hubspot-deploy.json +1250 -108
  12. package/integration/hubspot/hubspot-system.json +15 -32
  13. package/integration/hubspot/test-dataplane-down-tests.js +17 -16
  14. package/integration/hubspot/test-dataplane-down.js +2 -2
  15. package/jest.config.manual.js +2 -1
  16. package/lib/api/external-test.api.js +111 -0
  17. package/lib/api/index.js +42 -19
  18. package/lib/api/pipeline.api.js +66 -120
  19. package/lib/api/types/pipeline.types.js +37 -0
  20. package/lib/api/wizard-platform.api.js +61 -0
  21. package/lib/api/wizard.api.js +34 -1
  22. package/lib/app/config.js +23 -11
  23. package/lib/app/index.js +3 -1
  24. package/lib/app/prompts.js +44 -29
  25. package/lib/app/readme.js +8 -3
  26. package/lib/app/run-env-compose.js +64 -1
  27. package/lib/app/run-helpers.js +1 -1
  28. package/lib/app/show-display.js +1 -1
  29. package/lib/cli/setup-app.js +42 -11
  30. package/lib/cli/setup-credential-deployment.js +31 -6
  31. package/lib/cli/setup-dev.js +27 -0
  32. package/lib/cli/setup-environment.js +12 -4
  33. package/lib/cli/setup-external-system.js +19 -4
  34. package/lib/cli/setup-infra.js +54 -14
  35. package/lib/cli/setup-utility.js +117 -21
  36. package/lib/commands/credential-env.js +162 -0
  37. package/lib/commands/credential-list.js +17 -22
  38. package/lib/commands/credential-push.js +96 -0
  39. package/lib/commands/datasource.js +77 -6
  40. package/lib/commands/dev-init.js +39 -1
  41. package/lib/commands/repair-auth-config.js +99 -0
  42. package/lib/commands/repair-datasource-keys.js +208 -0
  43. package/lib/commands/repair-datasource.js +235 -0
  44. package/lib/commands/repair-env-template.js +348 -0
  45. package/lib/commands/repair-internal.js +85 -0
  46. package/lib/commands/repair-rbac.js +158 -0
  47. package/lib/commands/repair.js +507 -0
  48. package/lib/commands/test-e2e-external.js +165 -0
  49. package/lib/commands/upload.js +71 -40
  50. package/lib/commands/wizard-core-helpers.js +226 -4
  51. package/lib/commands/wizard-core.js +67 -29
  52. package/lib/commands/wizard-dataplane.js +1 -1
  53. package/lib/commands/wizard-entity-selection.js +43 -0
  54. package/lib/commands/wizard-headless.js +44 -5
  55. package/lib/commands/wizard-helpers.js +7 -3
  56. package/lib/commands/wizard.js +86 -64
  57. package/lib/core/config.js +7 -1
  58. package/lib/core/secrets.js +33 -12
  59. package/lib/datasource/deploy.js +12 -3
  60. package/lib/datasource/test-e2e.js +219 -0
  61. package/lib/datasource/test-integration.js +154 -0
  62. package/lib/deployment/deployer.js +7 -5
  63. package/lib/external-system/download.js +182 -204
  64. package/lib/external-system/generator.js +204 -56
  65. package/lib/external-system/test-execution.js +2 -1
  66. package/lib/external-system/test-system-level.js +73 -0
  67. package/lib/external-system/test.js +51 -18
  68. package/lib/generator/external-controller-manifest.js +29 -2
  69. package/lib/generator/external-schema-utils.js +1 -1
  70. package/lib/generator/external.js +10 -3
  71. package/lib/generator/index.js +4 -1
  72. package/lib/generator/split-readme.js +1 -0
  73. package/lib/generator/split-variables.js +7 -1
  74. package/lib/generator/split.js +194 -54
  75. package/lib/generator/wizard-prompts-secondary.js +294 -0
  76. package/lib/generator/wizard-prompts.js +105 -106
  77. package/lib/generator/wizard-readme.js +88 -0
  78. package/lib/generator/wizard.js +147 -158
  79. package/lib/infrastructure/compose.js +11 -1
  80. package/lib/infrastructure/index.js +11 -3
  81. package/lib/infrastructure/services.js +22 -11
  82. package/lib/schema/application-schema.json +8 -5
  83. package/lib/schema/external-datasource.schema.json +49 -26
  84. package/lib/schema/external-system.schema.json +82 -6
  85. package/lib/schema/wizard-config.schema.json +16 -0
  86. package/lib/utils/api.js +38 -10
  87. package/lib/utils/auth-headers.js +8 -7
  88. package/lib/utils/compose-generator.js +1 -1
  89. package/lib/utils/compose-handlebars-helpers.js +11 -0
  90. package/lib/utils/config-format-preference.js +51 -0
  91. package/lib/utils/config-format.js +36 -0
  92. package/lib/utils/configuration-env-resolver.js +179 -0
  93. package/lib/utils/credential-display.js +83 -0
  94. package/lib/utils/credential-secrets-env.js +115 -25
  95. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  96. package/lib/utils/deployment-validation-helpers.js +4 -4
  97. package/lib/utils/dev-ca-install.js +139 -0
  98. package/lib/utils/env-copy.js +23 -3
  99. package/lib/utils/error-formatters/http-status-errors.js +0 -1
  100. package/lib/utils/error-formatters/permission-errors.js +0 -1
  101. package/lib/utils/error-formatters/validation-errors.js +0 -1
  102. package/lib/utils/external-readme.js +56 -29
  103. package/lib/utils/external-system-display.js +59 -1
  104. package/lib/utils/external-system-test-helpers.js +21 -8
  105. package/lib/utils/external-system-validators.js +3 -0
  106. package/lib/utils/file-upload.js +20 -50
  107. package/lib/utils/help-builder.js +1 -0
  108. package/lib/utils/infra-status.js +50 -44
  109. package/lib/utils/local-secrets.js +5 -5
  110. package/lib/utils/paths.js +85 -4
  111. package/lib/utils/secrets-canonical.js +93 -0
  112. package/lib/utils/secrets-generator.js +20 -0
  113. package/lib/utils/secrets-helpers.js +75 -89
  114. package/lib/utils/test-log-writer.js +56 -0
  115. package/lib/utils/token-manager.js +24 -32
  116. package/lib/validation/env-template-auth.js +157 -0
  117. package/lib/validation/env-template-kv.js +41 -0
  118. package/lib/validation/external-manifest-validator.js +25 -0
  119. package/lib/validation/external-system-auth-rules.js +86 -0
  120. package/lib/validation/validate-batch.js +149 -0
  121. package/lib/validation/validate-datasource-keys-api.js +33 -0
  122. package/lib/validation/validate-display.js +94 -16
  123. package/lib/validation/validate.js +25 -12
  124. package/lib/validation/validator.js +7 -9
  125. package/lib/validation/wizard-datasource-validation.js +50 -0
  126. package/package.json +7 -2
  127. package/templates/applications/dataplane/application.yaml +1 -1
  128. package/templates/applications/dataplane/env.template +5 -5
  129. package/templates/applications/dataplane/rbac.yaml +2 -2
  130. package/templates/applications/miso-controller/env.template +1 -1
  131. package/templates/external-system/README.md.hbs +65 -25
  132. package/templates/external-system/deploy.js.hbs +4 -2
  133. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  134. package/templates/external-system/external-system.json.hbs +1 -18
  135. package/templates/infra/compose.yaml.hbs +6 -0
  136. package/templates/python/docker-compose.hbs +4 -4
  137. package/templates/typescript/docker-compose.hbs +4 -4
  138. package/integration/hubspot/application.yaml +0 -37
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Datasource repair helpers: align dimensions, metadataSchema, exposed, sync, testPayload
3
+ * with fieldMappings.attributes as source of truth.
4
+ *
5
+ * @fileoverview Repair datasource files for external integration
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const DEFAULT_SYNC = {
13
+ mode: 'pull',
14
+ batchSize: 500,
15
+ maxParallelRequests: 5
16
+ };
17
+
18
+ const MINIMAL_METADATA_SCHEMA = {
19
+ type: 'object',
20
+ additionalProperties: true
21
+ };
22
+
23
+ /**
24
+ * Returns the set of attribute keys from fieldMappings.attributes.
25
+ * @param {Object} parsed - Parsed datasource object
26
+ * @returns {Set<string>}
27
+ */
28
+ function getAttributeKeys(parsed) {
29
+ const attrs = parsed?.fieldMappings?.attributes;
30
+ if (!attrs || typeof attrs !== 'object') return new Set();
31
+ return new Set(Object.keys(attrs));
32
+ }
33
+
34
+ /**
35
+ * Extracts paths from attribute expressions (e.g. {{ metadata.email }}).
36
+ * Skips record_ref: expressions.
37
+ * - topLevelKeys: first segment of each path (e.g. "metadata" from "metadata.id").
38
+ * - referencedSchemaPropertyNames: for paths like "metadata.xxx" or "metadata.xxx.yyy", the name "xxx"
39
+ * (the first property under metadata). Used to prune metadataSchema.properties so we only keep
40
+ * properties that are referenced; we do not compare against "metadata" or the schema would be wiped.
41
+ *
42
+ * @param {Object} attributes - fieldMappings.attributes object
43
+ * @returns {{ paths: string[], topLevelKeys: Set<string>, referencedSchemaPropertyNames: Set<string> }}
44
+ */
45
+ function parsePathsFromExpressions(attributes) {
46
+ const paths = [];
47
+ const topLevelKeys = new Set();
48
+ const referencedSchemaPropertyNames = new Set();
49
+ if (!attributes || typeof attributes !== 'object') return { paths, topLevelKeys, referencedSchemaPropertyNames };
50
+ for (const attr of Object.values(attributes)) {
51
+ const expr = attr?.expression;
52
+ if (typeof expr !== 'string') continue;
53
+ if (/^\s*record_ref:/i.test(expr.trim())) continue;
54
+ const match = expr.match(/\{\{\s*([^}]+)\s*\}\}/);
55
+ if (!match) continue;
56
+ const path = match[1].trim().split('|')[0].trim();
57
+ if (path) {
58
+ paths.push(path);
59
+ const segments = path.split('.');
60
+ const first = segments[0];
61
+ if (first) topLevelKeys.add(first);
62
+ if (first === 'metadata' && segments.length >= 2 && segments[1]) {
63
+ referencedSchemaPropertyNames.add(segments[1]);
64
+ }
65
+ }
66
+ }
67
+ return { paths, topLevelKeys, referencedSchemaPropertyNames };
68
+ }
69
+
70
+ /**
71
+ * Removes dimension entries whose value is metadata.<attr> and attr is not in fieldMappings.attributes.
72
+ * Mutates parsed.fieldMappings.dimensions.
73
+ *
74
+ * @param {Object} parsed - Parsed datasource (mutated)
75
+ * @param {string[]} changes - Array to append change descriptions to
76
+ * @returns {boolean} True if any dimension was removed
77
+ */
78
+ function repairDimensionsFromAttributes(parsed, changes) {
79
+ const dims = parsed?.fieldMappings?.dimensions;
80
+ if (!dims || typeof dims !== 'object') return false;
81
+ const attributeKeys = getAttributeKeys(parsed);
82
+ let updated = false;
83
+ for (const [dimKey, value] of Object.entries(dims)) {
84
+ if (typeof value !== 'string') continue;
85
+ if (!value.startsWith('metadata.')) continue;
86
+ const attr = value.slice('metadata.'.length).trim();
87
+ if (!attr || attributeKeys.has(attr)) continue;
88
+ delete dims[dimKey];
89
+ changes.push(`Removed dimension '${dimKey}': ${value} not in fieldMappings.attributes`);
90
+ updated = true;
91
+ }
92
+ return updated;
93
+ }
94
+
95
+ /**
96
+ * Ensures metadataSchema exists (minimal stub if missing). If present, prunes top-level
97
+ * properties not referenced by any attribute expression. Uses the first property name under
98
+ * "metadata" in paths (e.g. metadata.id → "id") so we do not remove schema properties that
99
+ * are referenced. If no metadata.xxx paths exist, we do not prune (keep all properties).
100
+ *
101
+ * @param {Object} parsed - Parsed datasource (mutated)
102
+ * @param {string[]} changes - Array to append change descriptions to
103
+ * @returns {boolean} True if schema was added or pruned
104
+ */
105
+ function repairMetadataSchemaFromAttributes(parsed, changes) {
106
+ const { referencedSchemaPropertyNames } = parsePathsFromExpressions(parsed?.fieldMappings?.attributes ?? {});
107
+ if (!parsed.metadataSchema || typeof parsed.metadataSchema !== 'object') {
108
+ parsed.metadataSchema = { ...MINIMAL_METADATA_SCHEMA };
109
+ changes.push('Added minimal metadataSchema (was missing)');
110
+ return true;
111
+ }
112
+ const props = parsed.metadataSchema.properties;
113
+ if (!props || typeof props !== 'object') return false;
114
+ if (referencedSchemaPropertyNames.size === 0) return false;
115
+ const toRemove = Object.keys(props).filter(k => !referencedSchemaPropertyNames.has(k));
116
+ if (toRemove.length === 0) return false;
117
+ toRemove.forEach(k => delete props[k]);
118
+ changes.push(`Pruned metadataSchema.properties: removed [${toRemove.join(', ')}] (not referenced by attributes)`);
119
+ return true;
120
+ }
121
+
122
+ /**
123
+ * Sets exposed.attributes to the list of fieldMappings.attributes keys (sorted).
124
+ * Only when options.expose is true; caller should gate.
125
+ *
126
+ * @param {Object} parsed - Parsed datasource (mutated)
127
+ * @param {string[]} changes - Array to append change descriptions to
128
+ * @returns {boolean} True if exposed was updated
129
+ */
130
+ function repairExposeFromAttributes(parsed, changes) {
131
+ const keys = Array.from(getAttributeKeys(parsed)).filter(Boolean).sort();
132
+ if (keys.length === 0) return false;
133
+ if (!parsed.exposed) parsed.exposed = {};
134
+ const prev = parsed.exposed.attributes;
135
+ const same = Array.isArray(prev) && prev.length === keys.length && prev.every((v, i) => v === keys[i]);
136
+ if (same) return false;
137
+ parsed.exposed.attributes = keys;
138
+ changes.push(`Set exposed.attributes to [${keys.join(', ')}]`);
139
+ return true;
140
+ }
141
+
142
+ /**
143
+ * Adds default sync section if missing or empty.
144
+ *
145
+ * @param {Object} parsed - Parsed datasource (mutated)
146
+ * @param {string[]} changes - Array to append change descriptions to
147
+ * @returns {boolean} True if sync was added
148
+ */
149
+ function repairSyncSection(parsed, changes) {
150
+ const sync = parsed.sync;
151
+ if (sync && typeof sync === 'object' && Object.keys(sync).length > 0) return false;
152
+ parsed.sync = { ...DEFAULT_SYNC };
153
+ changes.push('Added default sync section (mode: pull, batchSize: 500, maxParallelRequests: 5)');
154
+ return true;
155
+ }
156
+
157
+ function placeholderForType(type) {
158
+ if (type === 'number' || type === 'integer') return 0;
159
+ if (type === 'boolean') return false;
160
+ if (type === 'array') return [];
161
+ if (type === 'object') return {};
162
+ return '';
163
+ }
164
+
165
+ function setNested(obj, pathParts, value) {
166
+ let cur = obj;
167
+ for (let i = 0; i < pathParts.length - 1; i++) {
168
+ const p = pathParts[i];
169
+ if (!(p in cur) || typeof cur[p] !== 'object') cur[p] = {};
170
+ cur = cur[p];
171
+ }
172
+ const last = pathParts[pathParts.length - 1];
173
+ if (last) cur[last] = value;
174
+ }
175
+
176
+ /**
177
+ * Builds minimal payloadTemplate and expectedResult from attribute expression paths.
178
+ *
179
+ * @param {Object} parsed - Parsed datasource (mutated)
180
+ * @param {string[]} changes - Array to append change descriptions to
181
+ * @returns {boolean} True if testPayload was added or updated
182
+ */
183
+ function repairTestPayload(parsed, changes) {
184
+ const attrs = parsed?.fieldMappings?.attributes;
185
+ if (!attrs || typeof attrs !== 'object') return false;
186
+ const payloadTemplate = {};
187
+ const expectedResult = {};
188
+ for (const [key, config] of Object.entries(attrs)) {
189
+ const type = config?.type || 'string';
190
+ const placeholder = placeholderForType(type);
191
+ expectedResult[key] = placeholder;
192
+ const match = config?.expression?.match(/\{\{\s*([^}|]+)/);
193
+ if (match) {
194
+ const path = match[1].trim();
195
+ setNested(payloadTemplate, path.split('.'), placeholder);
196
+ }
197
+ }
198
+ if (!parsed.testPayload) parsed.testPayload = {};
199
+ parsed.testPayload.payloadTemplate = payloadTemplate;
200
+ parsed.testPayload.expectedResult = expectedResult;
201
+ changes.push('Generated testPayload.payloadTemplate and testPayload.expectedResult from attributes');
202
+ return true;
203
+ }
204
+
205
+ /**
206
+ * Runs all requested datasource repairs. Core: dimensions + metadataSchema. Optional: expose, sync, testPayload.
207
+ *
208
+ * @param {Object} parsed - Parsed datasource object (mutated)
209
+ * @param {Object} options - { expose?: boolean, sync?: boolean, test?: boolean }
210
+ * @param {string[]} [changes] - Optional array to append change descriptions to
211
+ * @returns {{ updated: boolean, changes: string[] }}
212
+ */
213
+ function repairDatasourceFile(parsed, options = {}, changes = []) {
214
+ const out = Array.isArray(changes) ? changes : [];
215
+ let updated = false;
216
+ updated = repairDimensionsFromAttributes(parsed, out) || updated;
217
+ updated = repairMetadataSchemaFromAttributes(parsed, out) || updated;
218
+ if (options.expose) updated = repairExposeFromAttributes(parsed, out) || updated;
219
+ if (options.sync) updated = repairSyncSection(parsed, out) || updated;
220
+ if (options.test) updated = repairTestPayload(parsed, out) || updated;
221
+ return { updated, changes: out };
222
+ }
223
+
224
+ module.exports = {
225
+ getAttributeKeys,
226
+ parsePathsFromExpressions,
227
+ repairDimensionsFromAttributes,
228
+ repairMetadataSchemaFromAttributes,
229
+ repairExposeFromAttributes,
230
+ repairSyncSection,
231
+ repairTestPayload,
232
+ repairDatasourceFile,
233
+ DEFAULT_SYNC,
234
+ MINIMAL_METADATA_SCHEMA
235
+ };
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Helpers for repairing env.template from system config (KV_* names, path-style kv://).
3
+ * @fileoverview Repair env.template to match system file
4
+ * @author AI Fabrix Team
5
+ * @version 2.0.0
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+ const { systemKeyToKvPrefix, kvEnvKeyToPath, securityKeyToVar } = require('../utils/credential-secrets-env');
13
+ const { extractEnvTemplate } = require('../generator/split');
14
+
15
+ /**
16
+ * Normalizes a keyvault config entry to canonical KV_* name and path-style value.
17
+ * Path format: kv://<system-key>/<variable> (e.g. kv://microsoft-teams/clientId).
18
+ * @param {Object} entry - Config entry with name, value, location
19
+ * @param {string} prefix - KV prefix (e.g. MICROSOFT_TEAMS)
20
+ * @param {string} systemKey - System key (e.g. microsoft-teams) for path namespace
21
+ * @returns {{ name: string, value: string }|null}
22
+ */
23
+ function normalizeKeyvaultEntry(entry, prefix, systemKey) {
24
+ const afterPrefix = entry.name.startsWith(`KV_${prefix}_`)
25
+ ? entry.name.slice(`KV_${prefix}_`.length)
26
+ : entry.name.replace(/^KV_[A-Z0-9]+_/, '');
27
+ const normalizedVar = afterPrefix.replace(/_/g, '').toUpperCase();
28
+ const normalizedName = `KV_${prefix}_${normalizedVar}`;
29
+ const pathVal = kvEnvKeyToPath(normalizedName, systemKey);
30
+ if (!pathVal) return null;
31
+ return {
32
+ name: normalizedName,
33
+ value: pathVal.replace(/^kv:\/\//, ''),
34
+ location: 'keyvault'
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Adds effective config entries from system configuration array.
40
+ * @param {Array} effective - Mutable result array
41
+ * @param {Array} config - System configuration array
42
+ * @param {string} prefix - KV prefix
43
+ * @param {Set<string>} seenNames - Mutable set of names already added
44
+ * @param {string} systemKey - System key for path format kv://systemKey/variable
45
+ */
46
+ function addFromConfiguration(effective, config, prefix, seenNames, systemKey) {
47
+ for (const entry of config) {
48
+ if (!entry || !entry.name) continue;
49
+ if (entry.location === 'keyvault') {
50
+ const normalized = normalizeKeyvaultEntry(entry, prefix, systemKey);
51
+ if (normalized && !seenNames.has(normalized.name)) {
52
+ effective.push(normalized);
53
+ seenNames.add(normalized.name);
54
+ }
55
+ } else if (!seenNames.has(entry.name)) {
56
+ effective.push({
57
+ name: entry.name,
58
+ value: String(entry.value ?? ''),
59
+ location: entry.location
60
+ });
61
+ seenNames.add(entry.name);
62
+ }
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Adds effective config entries from authentication.security.
68
+ * Path format: kv://<system-key>/<variable> (e.g. kv://microsoft-teams/clientId).
69
+ * @param {Array} effective - Mutable result array
70
+ * @param {Object} systemParsed - Parsed system config
71
+ * @param {string} prefix - KV prefix
72
+ * @param {Set<string>} seenNames - Mutable set of names already added
73
+ * @param {string} systemKey - System key for path namespace
74
+ */
75
+ function addFromAuthSecurity(effective, systemParsed, prefix, seenNames, systemKey) {
76
+ const security = systemParsed.authentication?.security;
77
+ if (!security || typeof security !== 'object') return;
78
+ for (const key of Object.keys(security)) {
79
+ const envName = `KV_${prefix}_${securityKeyToVar(key)}`;
80
+ if (seenNames.has(envName)) continue;
81
+ const pathVal = kvEnvKeyToPath(envName, systemKey);
82
+ if (pathVal) {
83
+ effective.push({
84
+ name: envName,
85
+ value: pathVal.replace(/^kv:\/\//, ''),
86
+ location: 'keyvault'
87
+ });
88
+ seenNames.add(envName);
89
+ }
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Builds effective configuration array from system (KV_* names, path-style kv://).
95
+ * @param {Object} systemParsed - Parsed system config
96
+ * @param {string} systemKey - System key (e.g. 'hubspot')
97
+ * @returns {Array<{ name: string, value: string, location?: string }>}
98
+ */
99
+ function buildEffectiveConfiguration(systemParsed, systemKey) {
100
+ const effective = [];
101
+ const prefix = systemKeyToKvPrefix(systemKey);
102
+ if (!prefix) return effective;
103
+ const seenNames = new Set();
104
+ const config = Array.isArray(systemParsed.configuration) ? systemParsed.configuration : [];
105
+ addFromConfiguration(effective, config, prefix, seenNames, systemKey);
106
+ addFromAuthSecurity(effective, systemParsed, prefix, seenNames, systemKey);
107
+ return effective;
108
+ }
109
+
110
+ /**
111
+ * Builds map of env key -> full line (KEY=value) from effective config.
112
+ * @param {Array} effective - Effective configuration array
113
+ * @returns {Map<string, string>}
114
+ */
115
+ function buildExpectedByKey(effective) {
116
+ const expectedByKey = new Map();
117
+ const lines = extractEnvTemplate(effective).split('\n').filter(Boolean);
118
+ for (const line of lines) {
119
+ const eq = line.indexOf('=');
120
+ if (eq > 0) {
121
+ const key = line.substring(0, eq).trim();
122
+ expectedByKey.set(key, `${key}=${line.substring(eq + 1)}`);
123
+ }
124
+ }
125
+ return expectedByKey;
126
+ }
127
+
128
+ /**
129
+ * Creates env.template when missing. Returns true if created (and change pushed).
130
+ * @param {string} envPath - Path to env.template
131
+ * @param {Map<string, string>} expectedByKey - Expected key->line map
132
+ * @param {boolean} dryRun - If true, do not write
133
+ * @param {string[]} changes - Array to append to
134
+ * @returns {boolean}
135
+ */
136
+ function createEnvTemplateIfMissing(envPath, expectedByKey, dryRun, changes) {
137
+ if (fs.existsSync(envPath)) return false;
138
+ const lines = Array.from(expectedByKey.values());
139
+ const content = lines.join('\n');
140
+ if (!content) return false;
141
+ if (!dryRun) {
142
+ fs.writeFileSync(envPath, content + '\n', { mode: 0o644, encoding: 'utf8' });
143
+ }
144
+ changes.push('Created env.template from system configuration');
145
+ return true;
146
+ }
147
+
148
+ /**
149
+ * Extracts env key from a commented line (e.g. "# KEY=value" or "#KEY=value"). Returns null if not key=value.
150
+ * @param {string} trimmed - Trimmed line (starts with #)
151
+ * @returns {string|null} Key part after # and before =, or null
152
+ */
153
+ function keyFromCommentedLine(trimmed) {
154
+ const afterHash = trimmed.slice(1).trim();
155
+ if (!afterHash.includes('=')) return null;
156
+ const eq = afterHash.indexOf('=');
157
+ if (eq <= 0) return null;
158
+ const key = afterHash.substring(0, eq).trim();
159
+ return key || null;
160
+ }
161
+
162
+ /**
163
+ * Processes one line: keep as-is or replace with expected. Mutates updatedLines, keysWritten, changed.
164
+ * Preserves existing key=value in env.template: if a key already has a value, that value is never overwritten.
165
+ * Treats commented key=value (e.g. # KV_X=kv://...) as "already present" so repair does not add it again.
166
+ * Only missing keys from expectedByKey are added at the end.
167
+ * @param {string} line - Current line
168
+ * @param {Map<string, string>} expectedByKey - Expected key->line map
169
+ * @param {string[]} updatedLines - Mutable output lines
170
+ * @param {Set<string>} keysWritten - Mutable set of keys written
171
+ * @param {{ value: boolean }} _changedRef - Mutable changed flag (caller tracks changes)
172
+ */
173
+ function processLine(line, expectedByKey, updatedLines, keysWritten, _changedRef) {
174
+ const trimmed = line.trim();
175
+ if (!trimmed || trimmed.startsWith('#')) {
176
+ if (trimmed.startsWith('#')) {
177
+ const commentedKey = keyFromCommentedLine(trimmed);
178
+ if (commentedKey && expectedByKey.has(commentedKey)) keysWritten.add(commentedKey);
179
+ }
180
+ updatedLines.push(line);
181
+ return;
182
+ }
183
+ const eq = line.indexOf('=');
184
+ if (eq <= 0) {
185
+ updatedLines.push(line);
186
+ return;
187
+ }
188
+ const key = line.substring(0, eq).trim();
189
+ if (expectedByKey.has(key)) {
190
+ // Keep existing value; do not overwrite with expected (user may have set kv:// or custom value)
191
+ updatedLines.push(line);
192
+ keysWritten.add(key);
193
+ } else {
194
+ updatedLines.push(line);
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Merges existing env.template content with expected key=value lines.
200
+ * Preserves comments, blank lines, and vars not in expectedByKey.
201
+ * @param {string} content - Current file content
202
+ * @param {Map<string, string>} expectedByKey - Expected key->line map
203
+ * @returns {{ output: string, changed: boolean }}
204
+ */
205
+ function mergeEnvTemplateContent(content, expectedByKey) {
206
+ const lines = content.split(/\r?\n/);
207
+ const updatedLines = [];
208
+ const keysWritten = new Set();
209
+ const changedRef = { value: false };
210
+
211
+ for (const line of lines) {
212
+ processLine(line, expectedByKey, updatedLines, keysWritten, changedRef);
213
+ }
214
+ for (const key of expectedByKey.keys()) {
215
+ if (!keysWritten.has(key)) {
216
+ updatedLines.push(expectedByKey.get(key));
217
+ changedRef.value = true;
218
+ }
219
+ }
220
+
221
+ const output = updatedLines.join('\n') + (updatedLines.length > 0 ? '\n' : '');
222
+ return { output, changed: changedRef.value };
223
+ }
224
+
225
+ /**
226
+ * Repairs env.template so KV_ names and path-style kv:// values match the system file.
227
+ * @param {string} appPath - Application directory path
228
+ * @param {Object} systemParsed - Parsed system config
229
+ * @param {string} systemKey - System key
230
+ * @param {boolean} dryRun - If true, do not write
231
+ * @param {string[]} changes - Array to append change descriptions to
232
+ * @returns {boolean} True if env.template was repaired or created
233
+ */
234
+ function repairEnvTemplate(appPath, systemParsed, systemKey, dryRun, changes) {
235
+ const effective = buildEffectiveConfiguration(systemParsed, systemKey);
236
+ const expectedByKey = buildExpectedByKey(effective);
237
+ const envPath = path.join(appPath, 'env.template');
238
+
239
+ if (createEnvTemplateIfMissing(envPath, expectedByKey, dryRun, changes)) {
240
+ return true;
241
+ }
242
+ if (!fs.existsSync(envPath)) {
243
+ return false;
244
+ }
245
+
246
+ const content = fs.readFileSync(envPath, 'utf8');
247
+ const { output, changed } = mergeEnvTemplateContent(content, expectedByKey);
248
+
249
+ if (changed && !dryRun) {
250
+ fs.writeFileSync(envPath, output, { mode: 0o644, encoding: 'utf8' });
251
+ }
252
+ if (changed) {
253
+ changes.push('Repaired env.template (KV_* names and path-style kv:// values)');
254
+ return true;
255
+ }
256
+ return false;
257
+ }
258
+
259
+ /**
260
+ * Returns true if a kv value looks like legacy format (KeyVault suffix or no path segments).
261
+ * @param {string} val - Value from authentication.security or configuration
262
+ * @returns {boolean}
263
+ */
264
+ function isLegacyKvValue(val) {
265
+ if (typeof val !== 'string' || !val.trim().toLowerCase().startsWith('kv://')) return false;
266
+ const after = val.trim().slice(5); // after 'kv://'
267
+ return after.includes('KeyVault') || !after.includes('/');
268
+ }
269
+
270
+ /**
271
+ * Normalizes authentication.security values to path-style kv:// (kv://systemKey/variable).
272
+ * @param {Object} security - authentication.security object (mutated)
273
+ * @param {string} prefix - KV prefix (e.g. 'HUBSPOT')
274
+ * @param {string} systemKey - System key for path namespace
275
+ * @param {string[]} changes - Array to append change descriptions to
276
+ * @returns {boolean} True if any change was made
277
+ */
278
+ function normalizeSecuritySection(security, prefix, systemKey, changes) {
279
+ let updated = false;
280
+ for (const key of Object.keys(security)) {
281
+ const val = security[key];
282
+ if (typeof val !== 'string' || !isLegacyKvValue(val)) continue;
283
+ const envName = `KV_${prefix}_${securityKeyToVar(key)}`;
284
+ const pathVal = kvEnvKeyToPath(envName, systemKey);
285
+ if (pathVal) {
286
+ security[key] = pathVal;
287
+ changes.push(`authentication.security.${key}: normalized to path-style ${pathVal}`);
288
+ updated = true;
289
+ }
290
+ }
291
+ return updated;
292
+ }
293
+
294
+ /**
295
+ * Normalizes configuration array keyvault entries to canonical KV_* names and path-style values.
296
+ * @param {Object[]} config - configuration array (mutated)
297
+ * @param {string} prefix - KV prefix (e.g. 'HUBSPOT')
298
+ * @param {string} systemKey - System key for path namespace
299
+ * @param {string[]} changes - Array to append change descriptions to
300
+ * @returns {boolean} True if any change was made
301
+ */
302
+ function normalizeConfigurationSection(config, prefix, systemKey, changes) {
303
+ let updated = false;
304
+ for (let i = 0; i < config.length; i++) {
305
+ const entry = config[i];
306
+ if (!entry || !entry.name || (entry.location !== 'keyvault' && !String(entry.name).startsWith('KV_'))) continue;
307
+ const afterPrefix = entry.name.startsWith(`KV_${prefix}_`)
308
+ ? entry.name.slice(`KV_${prefix}_`.length)
309
+ : entry.name.replace(/^KV_[A-Z0-9]+_/, '');
310
+ const normalizedVar = afterPrefix.replace(/_/g, '').toUpperCase();
311
+ const canonicalName = `KV_${prefix}_${normalizedVar}`;
312
+ const pathVal = kvEnvKeyToPath(canonicalName, systemKey);
313
+ if (!pathVal) continue;
314
+ const pathValWithoutPrefix = pathVal.replace(/^kv:\/\//, '');
315
+ const valueLegacy = typeof entry.value === 'string' && (entry.value.includes('KeyVault') || !entry.value.includes('/'));
316
+ if (entry.name !== canonicalName || (valueLegacy && entry.value !== pathValWithoutPrefix)) {
317
+ config[i] = { ...entry, name: canonicalName, value: pathValWithoutPrefix, location: 'keyvault' };
318
+ changes.push(`configuration: normalized ${entry.name} → ${canonicalName}, value → path-style`);
319
+ updated = true;
320
+ }
321
+ }
322
+ return updated;
323
+ }
324
+
325
+ /**
326
+ * Normalizes system file authentication.security and configuration keyvault entries.
327
+ * @param {Object} systemParsed - Parsed system config (mutated)
328
+ * @param {string} systemKey - System key (e.g. 'hubspot')
329
+ * @param {string[]} changes - Array to append change descriptions to
330
+ * @returns {boolean} True if any change was made
331
+ */
332
+ function normalizeSystemFileAuthAndConfig(systemParsed, systemKey, changes) {
333
+ const prefix = systemKeyToKvPrefix(systemKey);
334
+ if (!prefix) return false;
335
+ const security = systemParsed.authentication?.security;
336
+ let updated = (security && typeof security === 'object' && normalizeSecuritySection(security, prefix, systemKey, changes));
337
+ const config = systemParsed.configuration;
338
+ if (Array.isArray(config)) {
339
+ updated = normalizeConfigurationSection(config, prefix, systemKey, changes) || updated;
340
+ }
341
+ return updated;
342
+ }
343
+
344
+ module.exports = {
345
+ buildEffectiveConfiguration,
346
+ repairEnvTemplate,
347
+ normalizeSystemFileAuthAndConfig
348
+ };
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Internal helpers for repair command: file discovery and datasource list building.
3
+ *
4
+ * @fileoverview Repair discovery and datasource helpers
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+ const chalk = require('chalk');
14
+ const logger = require('../utils/logger');
15
+
16
+ /** Matches *-datasource-*.(yaml|yml|json) or datasource-*.(yaml|yml|json) */
17
+ function isDatasourceFileName(name) {
18
+ if (!/\.(yaml|yml|json)$/i.test(name)) return false;
19
+ return /-datasource-.+\.(yaml|yml|json)$/i.test(name) || /^datasource-.+\.(yaml|yml|json)$/i.test(name);
20
+ }
21
+
22
+ /**
23
+ * Discovers system and datasource files in app directory
24
+ * @param {string} appPath - Application directory path
25
+ * @returns {{ systemFiles: string[], datasourceFiles: string[] }}
26
+ */
27
+ function discoverIntegrationFiles(appPath) {
28
+ if (!fs.existsSync(appPath)) {
29
+ return { systemFiles: [], datasourceFiles: [] };
30
+ }
31
+ const entries = fs.readdirSync(appPath);
32
+ const systemFiles = [];
33
+ const datasourceFiles = [];
34
+ for (const name of entries) {
35
+ if (!/^[a-z0-9_.-]+\.(yaml|yml|json)$/i.test(name)) continue;
36
+ if (/-system\.(yaml|yml|json)$/i.test(name)) {
37
+ systemFiles.push(name);
38
+ } else if (isDatasourceFileName(name)) {
39
+ datasourceFiles.push(name);
40
+ }
41
+ }
42
+ systemFiles.sort();
43
+ datasourceFiles.sort();
44
+ return { systemFiles, datasourceFiles };
45
+ }
46
+
47
+ /**
48
+ * Builds the effective datasource file list from application.yaml and discovered files.
49
+ * @param {string} appPath - Application directory path
50
+ * @param {string[]} discoveredDatasourceFiles - Filenames from discoverIntegrationFiles
51
+ * @param {string[]} [existingDataSources] - externalIntegration.dataSources from application.yaml
52
+ * @returns {string[]} Sorted, deduplicated list of datasource filenames
53
+ */
54
+ function buildEffectiveDatasourceFiles(appPath, discoveredDatasourceFiles, existingDataSources) {
55
+ const existing = Array.isArray(existingDataSources) ? existingDataSources : [];
56
+ const seen = new Set();
57
+ const result = [];
58
+ for (const name of existing) {
59
+ if (!name || typeof name !== 'string') continue;
60
+ const trimmed = name.trim();
61
+ if (!trimmed || seen.has(trimmed)) continue;
62
+ const filePath = path.join(appPath, trimmed);
63
+ if (fs.existsSync(filePath)) {
64
+ result.push(trimmed);
65
+ seen.add(trimmed);
66
+ } else {
67
+ logger.log(chalk.yellow(`⚠ Datasource file referenced in application.yaml not found: ${trimmed}`));
68
+ }
69
+ }
70
+ for (const name of discoveredDatasourceFiles) {
71
+ if (seen.has(name)) continue;
72
+ const filePath = path.join(appPath, name);
73
+ if (fs.existsSync(filePath)) {
74
+ result.push(name);
75
+ seen.add(name);
76
+ }
77
+ }
78
+ result.sort();
79
+ return result;
80
+ }
81
+
82
+ module.exports = {
83
+ discoverIntegrationFiles,
84
+ buildEffectiveDatasourceFiles
85
+ };