@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.
Files changed (198) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +7 -5
  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/integration/hubspot/test.js +1 -1
  16. package/jest.config.manual.js +2 -1
  17. package/lib/api/credential.api.js +40 -0
  18. package/lib/api/dev.api.js +423 -0
  19. package/lib/api/external-test.api.js +111 -0
  20. package/lib/api/index.js +42 -19
  21. package/lib/api/pipeline.api.js +66 -120
  22. package/lib/api/types/credential.types.js +23 -0
  23. package/lib/api/types/dev.types.js +140 -0
  24. package/lib/api/types/pipeline.types.js +37 -0
  25. package/lib/api/wizard-platform.api.js +61 -0
  26. package/lib/api/wizard.api.js +34 -1
  27. package/lib/app/config.js +44 -11
  28. package/lib/app/down.js +2 -1
  29. package/lib/app/index.js +12 -1
  30. package/lib/app/prompts.js +44 -29
  31. package/lib/app/push.js +36 -12
  32. package/lib/app/readme.js +9 -6
  33. package/lib/app/run-env-compose.js +264 -0
  34. package/lib/app/run-helpers.js +121 -118
  35. package/lib/app/run.js +148 -28
  36. package/lib/app/show-display.js +1 -1
  37. package/lib/app/show.js +5 -2
  38. package/lib/build/index.js +11 -3
  39. package/lib/cli/setup-app.js +172 -15
  40. package/lib/cli/setup-credential-deployment.js +31 -6
  41. package/lib/cli/setup-dev.js +206 -16
  42. package/lib/cli/setup-environment.js +16 -6
  43. package/lib/cli/setup-external-system.js +89 -24
  44. package/lib/cli/setup-infra.js +82 -15
  45. package/lib/cli/setup-secrets.js +52 -5
  46. package/lib/cli/setup-utility.js +129 -24
  47. package/lib/commands/app-install.js +172 -0
  48. package/lib/commands/app-shell.js +75 -0
  49. package/lib/commands/app-test.js +282 -0
  50. package/lib/commands/app.js +1 -1
  51. package/lib/commands/credential-env.js +162 -0
  52. package/lib/commands/credential-list.js +17 -22
  53. package/lib/commands/credential-push.js +96 -0
  54. package/lib/commands/datasource.js +77 -6
  55. package/lib/commands/dev-cli-handlers.js +141 -0
  56. package/lib/commands/dev-down.js +114 -0
  57. package/lib/commands/dev-init.js +347 -0
  58. package/lib/commands/repair-auth-config.js +99 -0
  59. package/lib/commands/repair-datasource-keys.js +208 -0
  60. package/lib/commands/repair-datasource.js +235 -0
  61. package/lib/commands/repair-env-template.js +348 -0
  62. package/lib/commands/repair-internal.js +85 -0
  63. package/lib/commands/repair-rbac.js +158 -0
  64. package/lib/commands/repair.js +507 -0
  65. package/lib/commands/secrets-list.js +118 -0
  66. package/lib/commands/secrets-remove.js +97 -0
  67. package/lib/commands/secrets-set.js +30 -17
  68. package/lib/commands/secrets-validate.js +50 -0
  69. package/lib/commands/test-e2e-external.js +165 -0
  70. package/lib/commands/up-dataplane.js +2 -2
  71. package/lib/commands/up-miso.js +0 -25
  72. package/lib/commands/upload.js +96 -40
  73. package/lib/commands/wizard-core-helpers.js +226 -4
  74. package/lib/commands/wizard-core.js +67 -29
  75. package/lib/commands/wizard-dataplane.js +1 -1
  76. package/lib/commands/wizard-entity-selection.js +43 -0
  77. package/lib/commands/wizard-headless.js +44 -5
  78. package/lib/commands/wizard-helpers.js +7 -3
  79. package/lib/commands/wizard.js +86 -64
  80. package/lib/core/admin-secrets.js +96 -0
  81. package/lib/core/config.js +7 -1
  82. package/lib/core/secrets-ensure.js +378 -0
  83. package/lib/core/secrets-env-write.js +157 -0
  84. package/lib/core/secrets.js +176 -89
  85. package/lib/datasource/deploy.js +12 -3
  86. package/lib/datasource/field-reference-validator.js +91 -0
  87. package/lib/datasource/test-e2e.js +219 -0
  88. package/lib/datasource/test-integration.js +154 -0
  89. package/lib/datasource/validate.js +21 -3
  90. package/lib/deployment/deployer.js +7 -5
  91. package/lib/deployment/environment-config.js +137 -0
  92. package/lib/deployment/environment.js +21 -98
  93. package/lib/deployment/push.js +32 -2
  94. package/lib/external-system/download.js +188 -203
  95. package/lib/external-system/generator.js +204 -56
  96. package/lib/external-system/test-auth.js +7 -3
  97. package/lib/external-system/test-execution.js +2 -1
  98. package/lib/external-system/test-system-level.js +73 -0
  99. package/lib/external-system/test.js +56 -19
  100. package/lib/generator/external-controller-manifest.js +29 -2
  101. package/lib/generator/external-schema-utils.js +1 -1
  102. package/lib/generator/external.js +10 -3
  103. package/lib/generator/index.js +177 -25
  104. package/lib/generator/split-readme.js +1 -0
  105. package/lib/generator/split-variables.js +7 -1
  106. package/lib/generator/split.js +194 -54
  107. package/lib/generator/wizard-prompts-secondary.js +294 -0
  108. package/lib/generator/wizard-prompts.js +105 -106
  109. package/lib/generator/wizard-readme.js +88 -0
  110. package/lib/generator/wizard.js +155 -158
  111. package/lib/infrastructure/compose.js +11 -1
  112. package/lib/infrastructure/helpers.js +103 -20
  113. package/lib/infrastructure/index.js +98 -12
  114. package/lib/infrastructure/services.js +88 -22
  115. package/lib/schema/application-schema.json +32 -8
  116. package/lib/schema/external-datasource.schema.json +49 -26
  117. package/lib/schema/external-system.schema.json +509 -411
  118. package/lib/schema/wizard-config.schema.json +16 -0
  119. package/lib/utils/api.js +41 -13
  120. package/lib/utils/app-register-auth.js +25 -3
  121. package/lib/utils/auth-headers.js +8 -7
  122. package/lib/utils/cli-utils.js +20 -0
  123. package/lib/utils/compose-generator.js +77 -76
  124. package/lib/utils/compose-handlebars-helpers.js +54 -0
  125. package/lib/utils/compose-vector-helper.js +18 -0
  126. package/lib/utils/config-format-preference.js +51 -0
  127. package/lib/utils/config-format.js +36 -0
  128. package/lib/utils/config-paths.js +127 -2
  129. package/lib/utils/configuration-env-resolver.js +179 -0
  130. package/lib/utils/credential-display.js +83 -0
  131. package/lib/utils/credential-secrets-env.js +357 -0
  132. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  133. package/lib/utils/deployment-validation-helpers.js +4 -4
  134. package/lib/utils/dev-ca-install.js +139 -0
  135. package/lib/utils/dev-cert-helper.js +122 -0
  136. package/lib/utils/device-code-helpers.js +224 -0
  137. package/lib/utils/device-code.js +37 -336
  138. package/lib/utils/docker-build.js +40 -8
  139. package/lib/utils/env-copy.js +103 -13
  140. package/lib/utils/env-map.js +35 -5
  141. package/lib/utils/env-template.js +6 -5
  142. package/lib/utils/error-formatters/http-status-errors.js +20 -2
  143. package/lib/utils/error-formatters/permission-errors.js +0 -1
  144. package/lib/utils/error-formatters/validation-errors.js +0 -1
  145. package/lib/utils/external-readme.js +56 -29
  146. package/lib/utils/external-system-display.js +59 -1
  147. package/lib/utils/external-system-test-helpers.js +21 -8
  148. package/lib/utils/external-system-validators.js +3 -0
  149. package/lib/utils/file-upload.js +20 -50
  150. package/lib/utils/help-builder.js +16 -2
  151. package/lib/utils/infra-status.js +80 -45
  152. package/lib/utils/local-secrets.js +7 -52
  153. package/lib/utils/mutagen-install.js +195 -0
  154. package/lib/utils/mutagen.js +146 -0
  155. package/lib/utils/paths.js +128 -37
  156. package/lib/utils/port-resolver.js +28 -16
  157. package/lib/utils/remote-dev-auth.js +38 -0
  158. package/lib/utils/remote-docker-env.js +43 -0
  159. package/lib/utils/remote-secrets-loader.js +60 -0
  160. package/lib/utils/secrets-canonical.js +93 -0
  161. package/lib/utils/secrets-generator.js +114 -6
  162. package/lib/utils/secrets-helpers.js +108 -114
  163. package/lib/utils/secrets-path.js +2 -2
  164. package/lib/utils/secrets-utils.js +52 -1
  165. package/lib/utils/secrets-validation.js +84 -0
  166. package/lib/utils/ssh-key-helper.js +116 -0
  167. package/lib/utils/test-log-writer.js +56 -0
  168. package/lib/utils/token-manager-messages.js +90 -0
  169. package/lib/utils/token-manager.js +29 -36
  170. package/lib/utils/variable-transformer.js +3 -3
  171. package/lib/validation/env-template-auth.js +157 -0
  172. package/lib/validation/env-template-kv.js +41 -0
  173. package/lib/validation/external-manifest-validator.js +25 -0
  174. package/lib/validation/external-system-auth-rules.js +86 -0
  175. package/lib/validation/validate-batch.js +149 -0
  176. package/lib/validation/validate-datasource-keys-api.js +33 -0
  177. package/lib/validation/validate-display.js +94 -16
  178. package/lib/validation/validate.js +25 -12
  179. package/lib/validation/validator.js +72 -9
  180. package/lib/validation/wizard-datasource-validation.js +50 -0
  181. package/package.json +8 -3
  182. package/scripts/install-local.js +34 -15
  183. package/templates/README.md +0 -1
  184. package/templates/applications/README.md.hbs +4 -4
  185. package/templates/applications/dataplane/application.yaml +6 -5
  186. package/templates/applications/dataplane/env.template +15 -10
  187. package/templates/applications/dataplane/rbac.yaml +2 -2
  188. package/templates/applications/keycloak/env.template +2 -0
  189. package/templates/applications/miso-controller/application.yaml +1 -0
  190. package/templates/applications/miso-controller/env.template +12 -10
  191. package/templates/external-system/README.md.hbs +65 -25
  192. package/templates/external-system/deploy.js.hbs +4 -2
  193. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  194. package/templates/external-system/external-system.json.hbs +1 -18
  195. package/templates/infra/compose.yaml.hbs +6 -0
  196. package/templates/python/docker-compose.hbs +49 -23
  197. package/templates/typescript/docker-compose.hbs +48 -22
  198. package/integration/hubspot/application.yaml +0 -37
@@ -10,6 +10,21 @@
10
10
 
11
11
  const path = require('path');
12
12
 
13
+ /**
14
+ * GET /api/dev/settings response parameter names (builder-cli.md §1).
15
+ * Single source of truth so CLI and config merge stay aligned with the contract.
16
+ */
17
+ const SETTINGS_RESPONSE_KEYS = [
18
+ 'user-mutagen-folder',
19
+ 'secrets-encryption',
20
+ 'aifabrix-secrets',
21
+ 'aifabrix-env-config',
22
+ 'remote-server',
23
+ 'docker-endpoint',
24
+ 'sync-ssh-user',
25
+ 'sync-ssh-host'
26
+ ];
27
+
13
28
  /**
14
29
  * Get path configuration value
15
30
  * @async
@@ -73,6 +88,114 @@ function createEnvConfigPathFunctions(getConfigFn, saveConfigFn) {
73
88
  };
74
89
  }
75
90
 
91
+ function createRemoteConfigGetters(getConfigFn) {
92
+ return {
93
+ async getRemoteServer() {
94
+ return getPathConfig(getConfigFn, 'remote-server');
95
+ },
96
+ async getDockerEndpoint() {
97
+ return getPathConfig(getConfigFn, 'docker-endpoint');
98
+ },
99
+ async getUserMutagenFolder() {
100
+ return getPathConfig(getConfigFn, 'user-mutagen-folder');
101
+ },
102
+ async getSyncSshUser() {
103
+ return getPathConfig(getConfigFn, 'sync-ssh-user');
104
+ },
105
+ async getSyncSshHost() {
106
+ return getPathConfig(getConfigFn, 'sync-ssh-host');
107
+ }
108
+ };
109
+ }
110
+
111
+ function createRemoteConfigSetters(getConfigFn, saveConfigFn) {
112
+ return {
113
+ async setRemoteServer(value) {
114
+ if (value !== null && value !== undefined && typeof value !== 'string') {
115
+ throw new Error('remote-server must be a string');
116
+ }
117
+ const config = await getConfigFn();
118
+ config['remote-server'] = value ? value.trim().replace(/\/+$/, '') : undefined;
119
+ await saveConfigFn(config);
120
+ },
121
+ async setDockerEndpoint(value) {
122
+ if (value !== null && value !== undefined && typeof value !== 'string') {
123
+ throw new Error('docker-endpoint must be a string');
124
+ }
125
+ const config = await getConfigFn();
126
+ config['docker-endpoint'] = value || undefined;
127
+ await saveConfigFn(config);
128
+ }
129
+ };
130
+ }
131
+
132
+ function isHttpUrl(value) {
133
+ return typeof value === 'string' && (value.startsWith('http://') || value.startsWith('https://'));
134
+ }
135
+
136
+ /**
137
+ * Derive hostname from a URL (e.g. https://builder.aifabrix.dev -> builder.aifabrix.dev).
138
+ * @param {string} url - URL string
139
+ * @returns {string|null} Hostname or null if invalid
140
+ */
141
+ function hostnameFromUrl(url) {
142
+ if (!url || typeof url !== 'string') return null;
143
+ const s = url.trim().replace(/\/+$/, '');
144
+ if (!s) return null;
145
+ const withProtocol = s.match(/^https?:\/\//) ? s : `https://${s}`;
146
+ try {
147
+ return new URL(withProtocol).hostname || null;
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+
153
+ function applySecretsUrlFromRemote(config) {
154
+ const remoteServer = config['remote-server'];
155
+ const secretsPath = config['aifabrix-secrets'];
156
+ if (!remoteServer || !secretsPath || isHttpUrl(secretsPath)) return;
157
+ const base = typeof remoteServer === 'string' ? remoteServer.trim().replace(/\/+$/, '') : '';
158
+ if (base) config['aifabrix-secrets'] = `${base}/api/dev/secrets`;
159
+ }
160
+
161
+ function applySyncAndDockerFromHost(config) {
162
+ const host = hostnameFromUrl(config['remote-server']);
163
+ if (!host) return;
164
+ if (!config['sync-ssh-host']) config['sync-ssh-host'] = host;
165
+ if (!config['docker-endpoint']) config['docker-endpoint'] = `tcp://${host}:2376`;
166
+ }
167
+
168
+ async function mergeRemoteSettingsImpl(getConfigFn, saveConfigFn, settings) {
169
+ if (!settings || typeof settings !== 'object') return;
170
+ const config = await getConfigFn();
171
+ for (const key of SETTINGS_RESPONSE_KEYS) {
172
+ const raw = settings[key];
173
+ if (raw === undefined || raw === null) continue;
174
+ const value = typeof raw === 'string' ? raw.trim() : raw;
175
+ if (value === '') continue;
176
+ config[key] = value;
177
+ }
178
+ applySecretsUrlFromRemote(config);
179
+ applySyncAndDockerFromHost(config);
180
+ await saveConfigFn(config);
181
+ }
182
+
183
+ /**
184
+ * Remote Docker / Builder Server config. Used when remote-server is set.
185
+ * @param {Function} getConfigFn - Function to get config
186
+ * @param {Function} saveConfigFn - Function to save config
187
+ * @returns {Object} Remote config getters, setters, and mergeRemoteSettings
188
+ */
189
+ function createRemoteConfigFunctions(getConfigFn, saveConfigFn) {
190
+ return {
191
+ ...createRemoteConfigGetters(getConfigFn),
192
+ ...createRemoteConfigSetters(getConfigFn, saveConfigFn),
193
+ async mergeRemoteSettings(settings) {
194
+ return mergeRemoteSettingsImpl(getConfigFn, saveConfigFn, settings);
195
+ }
196
+ };
197
+ }
198
+
76
199
  /**
77
200
  * Create path configuration functions with config access
78
201
  * @param {Function} getConfigFn - Function to get config
@@ -82,13 +205,15 @@ function createEnvConfigPathFunctions(getConfigFn, saveConfigFn) {
82
205
  function createPathConfigFunctions(getConfigFn, saveConfigFn) {
83
206
  return {
84
207
  ...createHomeAndSecretsPathFunctions(getConfigFn, saveConfigFn),
85
- ...createEnvConfigPathFunctions(getConfigFn, saveConfigFn)
208
+ ...createEnvConfigPathFunctions(getConfigFn, saveConfigFn),
209
+ ...createRemoteConfigFunctions(getConfigFn, saveConfigFn)
86
210
  };
87
211
  }
88
212
 
89
213
  module.exports = {
90
214
  getPathConfig,
91
215
  setPathConfig,
92
- createPathConfigFunctions
216
+ createPathConfigFunctions,
217
+ SETTINGS_RESPONSE_KEYS
93
218
  };
94
219
 
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Resolves configuration section values for upload (variable → .env, keyvault → secrets)
3
+ * and re-templates configuration on download from env.template.
4
+ *
5
+ * @fileoverview Configuration env resolution for external system upload/download
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+ const { getIntegrationPath } = require('./paths');
13
+ const { parseEnvToMap, resolveKvValue } = require('./credential-secrets-env');
14
+ const { loadSecrets, resolveKvReferences } = require('../core/secrets');
15
+ const { loadEnvTemplate } = require('./secrets-helpers');
16
+ const { getActualSecretsPath } = require('./secrets-path');
17
+
18
+ const VAR_PLACEHOLDER_REGEX = /\{\{([^}]+)\}\}/g;
19
+
20
+ /**
21
+ * Builds resolved env map and secrets for an integration app (for configuration resolution).
22
+ * If .env exists, parses it; otherwise resolves env.template with secrets and parses the result.
23
+ *
24
+ * @param {string} systemKey - External system key (e.g. 'my-sharepoint')
25
+ * @returns {Promise<{ envMap: Object.<string, string>, secrets: Object }>} envMap for {{VAR}} substitution, secrets for kv://
26
+ * @throws {Error} If env.template is missing when .env is missing (only when building from template)
27
+ */
28
+ async function buildResolvedEnvMapForIntegration(systemKey) {
29
+ if (!systemKey || typeof systemKey !== 'string') {
30
+ throw new Error('systemKey is required and must be a string');
31
+ }
32
+ const integrationPath = getIntegrationPath(systemKey);
33
+ const envPath = path.join(integrationPath, '.env');
34
+ const envTemplatePath = path.join(integrationPath, 'env.template');
35
+
36
+ let secrets = {};
37
+ try {
38
+ secrets = await loadSecrets(undefined, systemKey);
39
+ } catch {
40
+ secrets = {};
41
+ }
42
+
43
+ let envMap = {};
44
+ if (fs.existsSync(envPath)) {
45
+ const content = fs.readFileSync(envPath, 'utf8');
46
+ envMap = parseEnvToMap(content);
47
+ } else if (fs.existsSync(envTemplatePath)) {
48
+ const templateContent = loadEnvTemplate(envTemplatePath);
49
+ const secretsPaths = await getActualSecretsPath(undefined, systemKey);
50
+ const resolvedContent = await resolveKvReferences(
51
+ templateContent,
52
+ secrets,
53
+ 'local',
54
+ secretsPaths,
55
+ systemKey
56
+ );
57
+ envMap = parseEnvToMap(resolvedContent);
58
+ }
59
+ return { envMap, secrets };
60
+ }
61
+
62
+ /**
63
+ * Resolves {{VAR}} in a string using envMap. Throws if any variable is missing.
64
+ *
65
+ * @param {string} value - Value that may contain {{VAR}}
66
+ * @param {Object.<string, string>} envMap - Resolved env key-value map
67
+ * @param {string} [systemKey] - System key for error message
68
+ * @returns {string} Value with {{VAR}} replaced
69
+ * @throws {Error} If a {{VAR}} is missing from envMap
70
+ */
71
+ function substituteVarPlaceholders(value, envMap, systemKey) {
72
+ const hint = systemKey ? ` Run 'aifabrix resolve ${systemKey}' or set the variable in .env.` : '';
73
+ return value.replace(VAR_PLACEHOLDER_REGEX, (match, varName) => {
74
+ const key = varName.trim();
75
+ if (envMap[key] === undefined || envMap[key] === null) {
76
+ throw new Error(`Missing configuration env var: ${key}.${hint}`);
77
+ }
78
+ return String(envMap[key]);
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Resolves configuration array values in place by location: variable → {{VAR}} from envMap;
84
+ * keyvault → kv:// from secrets. Does not log or expose secret values.
85
+ *
86
+ * @param {Array<{ name?: string, value?: string, location?: string }>} configArray - Configuration array (mutated)
87
+ * @param {Object.<string, string>} envMap - Resolved env map from buildResolvedEnvMapForIntegration
88
+ * @param {Object} secrets - Loaded secrets for kv:// resolution
89
+ * @param {string} [systemKey] - System key for error messages
90
+ * @throws {Error} If variable env is missing or keyvault secret unresolved (message never contains secret values)
91
+ */
92
+ function resolveConfigurationValues(configArray, envMap, secrets, systemKey) {
93
+ if (!Array.isArray(configArray)) return;
94
+ const hint = systemKey ? ` Run 'aifabrix resolve ${systemKey}' and ensure the key exists in the secrets file.` : '';
95
+ for (const item of configArray) {
96
+ if (!item || typeof item.value !== 'string') continue;
97
+ const location = (item.location || '').toLowerCase();
98
+ if (location === 'variable') {
99
+ if (item.value.trim().startsWith('kv://')) {
100
+ throw new Error(`Configuration entry '${item.name || 'unknown'}' has location 'variable' but value is kv://. Use location 'keyvault' for secrets.`);
101
+ }
102
+ item.value = substituteVarPlaceholders(item.value, envMap, systemKey);
103
+ } else if (location === 'keyvault') {
104
+ const resolved = resolveKvValue(secrets, item.value);
105
+ if (resolved === null || resolved === undefined) {
106
+ throw new Error(`Unresolved keyvault reference for configuration '${item.name || 'unknown'}'.${hint}`);
107
+ }
108
+ item.value = resolved;
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Returns the set of variable names (keys) defined in env.template content.
115
+ *
116
+ * @param {string} envTemplateContent - Raw env.template content
117
+ * @returns {Set<string>} Set of variable names
118
+ */
119
+ function getEnvTemplateVariableNames(envTemplateContent) {
120
+ const names = new Set();
121
+ if (!envTemplateContent || typeof envTemplateContent !== 'string') return names;
122
+ const lines = envTemplateContent.split(/\r?\n/);
123
+ for (const line of lines) {
124
+ const trimmed = line.trim();
125
+ if (!trimmed || trimmed.startsWith('#')) continue;
126
+ const eq = trimmed.indexOf('=');
127
+ if (eq > 0) {
128
+ const key = trimmed.substring(0, eq).trim();
129
+ if (key) names.add(key);
130
+ }
131
+ }
132
+ return names;
133
+ }
134
+
135
+ /**
136
+ * Re-templates configuration from env.template: for each entry with location === 'variable'
137
+ * whose name matches a key in env.template, sets value to {{name}}. Mutates configArray in place.
138
+ *
139
+ * @param {Array<{ name?: string, value?: string, location?: string }>} configArray - Configuration array (mutated)
140
+ * @param {Set<string>} envTemplateVariableNames - Variable names present in env.template
141
+ */
142
+ function retemplateConfigurationFromEnvTemplate(configArray, envTemplateVariableNames) {
143
+ if (!Array.isArray(configArray) || !envTemplateVariableNames || !envTemplateVariableNames.size) return;
144
+ for (const item of configArray) {
145
+ if (!item || (item.location || '').toLowerCase() !== 'variable') continue;
146
+ const name = item.name && String(item.name).trim();
147
+ if (name && envTemplateVariableNames.has(name)) {
148
+ item.value = `{{${name}}}`;
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Reads env.template at integration path, re-templates the given configuration array,
155
+ * and returns the updated array (mutates in place). If env.template is missing, does nothing.
156
+ *
157
+ * @param {string} systemKey - External system key
158
+ * @param {Array<{ name?: string, value?: string, location?: string }>} configArray - Configuration array (mutated)
159
+ * @returns {Promise<boolean>} True if re-templating was applied (env.template existed)
160
+ */
161
+ async function retemplateConfigurationForDownload(systemKey, configArray) {
162
+ if (!systemKey || typeof systemKey !== 'string' || !Array.isArray(configArray)) return false;
163
+ const integrationPath = getIntegrationPath(systemKey);
164
+ const envTemplatePath = path.join(integrationPath, 'env.template');
165
+ if (!fs.existsSync(envTemplatePath)) return false;
166
+ const content = fs.readFileSync(envTemplatePath, 'utf8');
167
+ const names = getEnvTemplateVariableNames(content);
168
+ retemplateConfigurationFromEnvTemplate(configArray, names);
169
+ return true;
170
+ }
171
+
172
+ module.exports = {
173
+ buildResolvedEnvMapForIntegration,
174
+ resolveConfigurationValues,
175
+ getEnvTemplateVariableNames,
176
+ retemplateConfigurationFromEnvTemplate,
177
+ retemplateConfigurationForDownload,
178
+ substituteVarPlaceholders
179
+ };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Credential display utilities – status icons and formatting for CLI output
3
+ * Aligns with dataplane credential status lifecycle (pending, verified, failed, expired).
4
+ *
5
+ * @fileoverview Credential status formatter with icons and chalk colors
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ const chalk = require('chalk');
11
+
12
+ /** @type {{ verified: string, pending: string, failed: string, expired: string }} */
13
+ const STATUS_ICONS = {
14
+ verified: ' ✓',
15
+ pending: ' ○',
16
+ failed: ' ✗',
17
+ expired: ' ⊘'
18
+ };
19
+
20
+ /** @type {{ verified: string, pending: string, failed: string, expired: string }} */
21
+ const STATUS_LABELS = {
22
+ verified: 'Valid',
23
+ pending: 'Not tested',
24
+ failed: 'Connection failed',
25
+ expired: 'Token expired'
26
+ };
27
+
28
+ /**
29
+ * Chalk color functions per status
30
+ * @type {{ verified: Function, pending: Function, failed: Function, expired: Function }}
31
+ */
32
+ const STATUS_CHALK = {
33
+ verified: chalk.green,
34
+ pending: chalk.gray,
35
+ failed: chalk.red,
36
+ expired: chalk.yellow
37
+ };
38
+
39
+ const VALID_STATUSES = ['verified', 'pending', 'failed', 'expired'];
40
+
41
+ /**
42
+ * Format credential status for display (icon + optional label)
43
+ * @param {string} [status] - Credential status (pending | verified | failed | expired)
44
+ * @returns {{ icon: string, color: Function, label: string } | null} Status info or null when missing/invalid
45
+ */
46
+ function formatCredentialStatus(status) {
47
+ if (!status || typeof status !== 'string') return null;
48
+ const s = status.toLowerCase();
49
+ if (!VALID_STATUSES.includes(s)) return null;
50
+ return {
51
+ icon: STATUS_ICONS[s],
52
+ color: STATUS_CHALK[s],
53
+ label: STATUS_LABELS[s]
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Format credential with status for CLI display
59
+ * @param {Object} credential - Credential object from API
60
+ * @param {string} [credential.key]
61
+ * @param {string} [credential.id]
62
+ * @param {string} [credential.credentialKey]
63
+ * @param {string} [credential.displayName]
64
+ * @param {string} [credential.name]
65
+ * @param {string} [credential.status]
66
+ * @returns {{ key: string, name: string, statusFormatted: string, statusLabel: string }}
67
+ */
68
+ function formatCredentialWithStatus(credential) {
69
+ const key = credential?.key ?? credential?.id ?? credential?.credentialKey ?? '-';
70
+ const name = credential?.displayName ?? credential?.name ?? key;
71
+ const statusInfo = formatCredentialStatus(credential?.status);
72
+ const statusFormatted = statusInfo ? statusInfo.color(statusInfo.icon) : '';
73
+ const statusLabel = statusInfo ? ` (${statusInfo.label})` : '';
74
+ return { key, name, statusFormatted, statusLabel };
75
+ }
76
+
77
+ module.exports = {
78
+ STATUS_ICONS,
79
+ STATUS_LABELS,
80
+ STATUS_CHALK,
81
+ formatCredentialStatus,
82
+ formatCredentialWithStatus
83
+ };