@aifabrix/builder 2.41.0 → 2.42.1

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 (142) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +2 -2
  3. package/integration/hubspot/README.md +11 -5
  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 +36 -2
  22. package/lib/app/config.js +23 -11
  23. package/lib/app/index.js +5 -3
  24. package/lib/app/prompts.js +46 -31
  25. package/lib/app/readme.js +11 -4
  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 +45 -14
  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/auth-config.js +22 -12
  37. package/lib/commands/credential-env.js +162 -0
  38. package/lib/commands/credential-list.js +17 -22
  39. package/lib/commands/credential-push.js +96 -0
  40. package/lib/commands/datasource.js +77 -6
  41. package/lib/commands/dev-init.js +39 -1
  42. package/lib/commands/repair-auth-config.js +99 -0
  43. package/lib/commands/repair-datasource-keys.js +208 -0
  44. package/lib/commands/repair-datasource.js +235 -0
  45. package/lib/commands/repair-env-template.js +348 -0
  46. package/lib/commands/repair-internal.js +85 -0
  47. package/lib/commands/repair-rbac.js +158 -0
  48. package/lib/commands/repair.js +518 -0
  49. package/lib/commands/secrets-set.js +6 -0
  50. package/lib/commands/test-e2e-external.js +165 -0
  51. package/lib/commands/up-dataplane.js +90 -6
  52. package/lib/commands/upload.js +71 -40
  53. package/lib/commands/wizard-core-helpers.js +230 -5
  54. package/lib/commands/wizard-core.js +68 -29
  55. package/lib/commands/wizard-dataplane.js +1 -1
  56. package/lib/commands/wizard-entity-selection.js +43 -0
  57. package/lib/commands/wizard-headless.js +49 -5
  58. package/lib/commands/wizard-helpers.js +7 -3
  59. package/lib/commands/wizard.js +93 -64
  60. package/lib/core/config.js +7 -1
  61. package/lib/core/secrets.js +33 -12
  62. package/lib/datasource/deploy.js +12 -3
  63. package/lib/datasource/test-e2e.js +219 -0
  64. package/lib/datasource/test-integration.js +154 -0
  65. package/lib/deployment/deployer.js +7 -5
  66. package/lib/external-system/download-helpers.js +3 -1
  67. package/lib/external-system/download.js +182 -204
  68. package/lib/external-system/generator.js +204 -56
  69. package/lib/external-system/test-execution.js +2 -1
  70. package/lib/external-system/test-system-level.js +73 -0
  71. package/lib/external-system/test.js +51 -18
  72. package/lib/generator/external-controller-manifest.js +29 -2
  73. package/lib/generator/external-schema-utils.js +4 -2
  74. package/lib/generator/external.js +10 -3
  75. package/lib/generator/index.js +4 -1
  76. package/lib/generator/split-readme.js +1 -0
  77. package/lib/generator/split-variables.js +7 -1
  78. package/lib/generator/split.js +194 -54
  79. package/lib/generator/wizard-prompts-secondary.js +326 -0
  80. package/lib/generator/wizard-prompts.js +105 -106
  81. package/lib/generator/wizard-readme.js +91 -0
  82. package/lib/generator/wizard.js +180 -179
  83. package/lib/infrastructure/compose.js +11 -1
  84. package/lib/infrastructure/index.js +11 -3
  85. package/lib/infrastructure/services.js +22 -11
  86. package/lib/schema/application-schema.json +8 -5
  87. package/lib/schema/external-datasource.schema.json +49 -26
  88. package/lib/schema/external-system.schema.json +82 -6
  89. package/lib/schema/wizard-config.schema.json +23 -1
  90. package/lib/utils/api.js +38 -10
  91. package/lib/utils/auth-headers.js +8 -7
  92. package/lib/utils/compose-generator.js +1 -1
  93. package/lib/utils/compose-handlebars-helpers.js +11 -0
  94. package/lib/utils/config-format-preference.js +51 -0
  95. package/lib/utils/config-format.js +36 -0
  96. package/lib/utils/configuration-env-resolver.js +179 -0
  97. package/lib/utils/credential-display.js +83 -0
  98. package/lib/utils/credential-secrets-env.js +115 -25
  99. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  100. package/lib/utils/deployment-validation-helpers.js +4 -4
  101. package/lib/utils/dev-ca-install.js +139 -0
  102. package/lib/utils/env-copy.js +23 -3
  103. package/lib/utils/error-formatters/http-status-errors.js +0 -1
  104. package/lib/utils/error-formatters/permission-errors.js +0 -1
  105. package/lib/utils/error-formatters/validation-errors.js +0 -1
  106. package/lib/utils/external-readme.js +89 -30
  107. package/lib/utils/external-system-display.js +59 -1
  108. package/lib/utils/external-system-test-helpers.js +21 -8
  109. package/lib/utils/external-system-validators.js +3 -0
  110. package/lib/utils/file-upload.js +20 -50
  111. package/lib/utils/help-builder.js +1 -0
  112. package/lib/utils/infra-status.js +50 -44
  113. package/lib/utils/local-secrets.js +5 -5
  114. package/lib/utils/paths.js +85 -4
  115. package/lib/utils/secrets-canonical.js +93 -0
  116. package/lib/utils/secrets-generator.js +20 -0
  117. package/lib/utils/secrets-helpers.js +75 -89
  118. package/lib/utils/test-log-writer.js +56 -0
  119. package/lib/utils/token-manager.js +24 -32
  120. package/lib/validation/env-template-auth.js +157 -0
  121. package/lib/validation/env-template-kv.js +41 -0
  122. package/lib/validation/external-manifest-validator.js +25 -0
  123. package/lib/validation/external-system-auth-rules.js +86 -0
  124. package/lib/validation/validate-batch.js +149 -0
  125. package/lib/validation/validate-datasource-keys-api.js +33 -0
  126. package/lib/validation/validate-display.js +94 -16
  127. package/lib/validation/validate.js +25 -12
  128. package/lib/validation/validator.js +7 -9
  129. package/lib/validation/wizard-datasource-validation.js +50 -0
  130. package/package.json +7 -2
  131. package/templates/applications/dataplane/application.yaml +1 -1
  132. package/templates/applications/dataplane/env.template +5 -5
  133. package/templates/applications/dataplane/rbac.yaml +2 -2
  134. package/templates/applications/miso-controller/env.template +1 -1
  135. package/templates/external-system/README.md.hbs +75 -22
  136. package/templates/external-system/deploy.js.hbs +4 -2
  137. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  138. package/templates/external-system/external-system.json.hbs +1 -18
  139. package/templates/infra/compose.yaml.hbs +6 -0
  140. package/templates/python/docker-compose.hbs +4 -4
  141. package/templates/typescript/docker-compose.hbs +4 -4
  142. package/integration/hubspot/application.yaml +0 -37
@@ -11,6 +11,7 @@
11
11
  * @version 2.0.0
12
12
  */
13
13
 
14
+ const readline = require('readline');
14
15
  const chalk = require('chalk');
15
16
  const pathsUtil = require('../utils/paths');
16
17
  const { loadConfigFile } = require('../utils/config-format');
@@ -18,13 +19,87 @@ const logger = require('../utils/logger');
18
19
  const config = require('../core/config');
19
20
  const { checkAuthentication } = require('../utils/app-register-auth');
20
21
  const { resolveControllerUrl } = require('../utils/controller-url');
21
- const { resolveEnvironment } = require('../core/config');
22
+ const { resolveEnvironment, setControllerUrl } = require('../core/config');
22
23
  const { registerApplication } = require('../app/register');
23
24
  const { rotateSecret } = require('../app/rotate-secret');
24
25
  const { checkApplicationExists } = require('../utils/app-existence');
26
+ const { checkHealthEndpoint } = require('../utils/health-check');
27
+ const { validateControllerUrl } = require('../utils/auth-config-validator');
25
28
  const app = require('../app');
26
29
  const { ensureAppFromTemplate, validateEnvOutputPathFolderOrNull } = require('./up-common');
27
30
 
31
+ const CONTROLLER_HEALTH_PATH = '/health';
32
+
33
+ /**
34
+ * Check if controller is reachable (health endpoint).
35
+ * @param {string} baseUrl - Controller base URL (no trailing slash)
36
+ * @returns {Promise<boolean>} True if healthy
37
+ */
38
+ async function isControllerHealthy(baseUrl) {
39
+ const healthUrl = `${baseUrl.replace(/\/+$/, '')}${CONTROLLER_HEALTH_PATH}`;
40
+ try {
41
+ return await checkHealthEndpoint(healthUrl, false);
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Prompt user for controller URL when current controller is not available.
49
+ * @param {string} currentUrl - Current controller URL that failed health check
50
+ * @returns {Promise<string|null>} New URL or null if user aborted (empty input)
51
+ */
52
+ function promptForControllerUrl(currentUrl) {
53
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
54
+ return new Promise((resolve) => {
55
+ rl.question(
56
+ chalk.yellow(`Controller at ${currentUrl} is not available. Enter controller URL (or press Enter to abort): `),
57
+ (answer) => {
58
+ rl.close();
59
+ const trimmed = (answer || '').trim();
60
+ resolve(trimmed === '' ? null : trimmed);
61
+ }
62
+ );
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Resolve controller URL and ensure it is healthy; if not, prompt once for new URL.
68
+ * @returns {Promise<string>} Controller URL to use
69
+ * @throws {Error} If controller unavailable and user aborts or new URL still unhealthy
70
+ */
71
+ async function resolveControllerUrlWithHealthCheck() {
72
+ let controllerUrl = await resolveControllerUrl();
73
+ controllerUrl = controllerUrl.replace(/\/+$/, '');
74
+
75
+ let healthy = await isControllerHealthy(controllerUrl);
76
+ if (healthy) {
77
+ return controllerUrl;
78
+ }
79
+
80
+ logger.log(chalk.yellow(`\nController at ${controllerUrl} is not responding (health check failed).\n`));
81
+ const newUrl = await promptForControllerUrl(controllerUrl);
82
+ if (!newUrl) {
83
+ throw new Error('Controller URL is required. Run "aifabrix up-dataplane" again and enter a valid controller URL, or set it with: aifabrix auth config --set-controller <url>');
84
+ }
85
+
86
+ try {
87
+ validateControllerUrl(newUrl);
88
+ } catch (err) {
89
+ throw new Error(`Invalid controller URL: ${err.message}`);
90
+ }
91
+
92
+ await setControllerUrl(newUrl);
93
+ const normalizedNew = newUrl.trim().replace(/\/+$/, '');
94
+ healthy = await isControllerHealthy(normalizedNew);
95
+ if (!healthy) {
96
+ throw new Error(`Controller at ${normalizedNew} is not responding. Ensure the controller is running and reachable, then run "aifabrix up-dataplane" again.`);
97
+ }
98
+
99
+ logger.log(chalk.green(`✓ Using controller: ${normalizedNew}`));
100
+ return normalizedNew;
101
+ }
102
+
28
103
  /**
29
104
  * Register or rotate dataplane: if app exists in controller, rotate secret; otherwise register.
30
105
  * @async
@@ -45,6 +120,17 @@ async function registerOrRotateDataplane(options, controllerUrl, environmentKey,
45
120
  }
46
121
  }
47
122
 
123
+ /**
124
+ * Deploy dataplane app (send manifest to controller).
125
+ * @param {Object} options - Commander options (registry, registryMode, image)
126
+ * @returns {Promise<void>}
127
+ */
128
+ async function deployDataplaneToController(options) {
129
+ const imageOverride = options.image || (options.registry ? buildDataplaneImageRef(options.registry) : undefined);
130
+ const deployOpts = { imageOverride, image: imageOverride, registryMode: options.registryMode };
131
+ await app.deployApp('dataplane', deployOpts);
132
+ }
133
+
48
134
  /**
49
135
  * Build full image ref from registry and dataplane config (registry/name:tag)
50
136
  * @param {string} registry - Registry URL
@@ -85,7 +171,8 @@ async function handleUpDataplane(options = {}) {
85
171
  }
86
172
  logger.log(chalk.blue('Starting up-dataplane (register/rotate, deploy, then run dataplane locally)...\n'));
87
173
 
88
- const [controllerUrl, environmentKey] = await Promise.all([resolveControllerUrl(), resolveEnvironment()]);
174
+ const controllerUrl = await resolveControllerUrlWithHealthCheck();
175
+ const environmentKey = await resolveEnvironment();
89
176
  const authConfig = await checkAuthentication(controllerUrl, environmentKey, { throwOnFailure: true });
90
177
 
91
178
  const cfg = await config.getConfig();
@@ -103,10 +190,7 @@ async function handleUpDataplane(options = {}) {
103
190
 
104
191
  await registerOrRotateDataplane(options, controllerUrl, environmentKey, authConfig);
105
192
 
106
- const imageOverride = options.image || (options.registry ? buildDataplaneImageRef(options.registry) : undefined);
107
- const deployOpts = { imageOverride, image: imageOverride, registryMode: options.registryMode };
108
-
109
- await app.deployApp('dataplane', deployOpts);
193
+ await deployDataplaneToController(options);
110
194
  logger.log('');
111
195
  await app.runApp('dataplane', { skipEnvOutputPath: true });
112
196
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Upload external system to dataplane (upload → validate → publish).
2
+ * Upload external system to dataplane (single pipeline upload: upload → validate → publish).
3
3
  *
4
4
  * @fileoverview Upload command handler for aifabrix upload <system-key>
5
5
  * @author AI Fabrix Team
@@ -14,15 +14,16 @@ const { getDeploymentAuth, requireBearerForDataplanePipeline } = require('../uti
14
14
  const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
15
15
  const { getIntegrationPath } = require('../utils/paths');
16
16
  const { pushCredentialSecrets } = require('../utils/credential-secrets-env');
17
+ const {
18
+ buildResolvedEnvMapForIntegration,
19
+ resolveConfigurationValues
20
+ } = require('../utils/configuration-env-resolver');
17
21
  const { validateExternalSystemComplete } = require('../validation/validate');
18
22
  const { displayValidationResults } = require('../validation/validate-display');
19
23
  const { generateControllerManifest } = require('../generator/external-controller-manifest');
20
- const {
21
- uploadApplicationViaPipeline,
22
- validateUploadViaPipeline,
23
- publishUploadViaPipeline
24
- } = require('../api/pipeline.api');
24
+ const { uploadApplicationViaPipeline } = require('../api/pipeline.api');
25
25
  const { formatApiError } = require('../utils/api-error-handler');
26
+ const { logDataplanePipelineWarning } = require('../utils/dataplane-pipeline-warning');
26
27
 
27
28
  /**
28
29
  * Validates system-key format (same as download).
@@ -40,25 +41,25 @@ function validateSystemKeyFormat(systemKey) {
40
41
 
41
42
  /**
42
43
  * Builds pipeline upload payload from controller manifest.
43
- * Payload: { version, application, dataSources }; application = system with RBAC.
44
+ * Payload: { version, application, dataSources, status }; Builder always uses status "draft".
44
45
  * @param {Object} manifest - Controller manifest from generateControllerManifest
45
- * @returns {Object} { version, application, dataSources }
46
+ * @returns {Object} { version, application, dataSources, status: "draft" }
46
47
  */
47
48
  function buildUploadPayload(manifest) {
48
49
  return {
49
50
  version: manifest.version || '1.0.0',
50
51
  application: manifest.system,
51
- dataSources: manifest.dataSources || []
52
+ dataSources: manifest.dataSources || [],
53
+ status: 'draft'
52
54
  };
53
55
  }
54
56
 
55
57
  /**
56
58
  * Resolves dataplane URL and auth (same pattern as download).
57
59
  * @param {string} systemKey - System key
58
- * @param {Object} options - Options with optional dataplane override
59
60
  * @returns {Promise<{ dataplaneUrl: string, authConfig: Object, environment: string }>}
60
61
  */
61
- async function resolveDataplaneAndAuth(systemKey, options) {
62
+ async function resolveDataplaneAndAuth(systemKey) {
62
63
  const { resolveEnvironment } = require('../core/config');
63
64
  const environment = await resolveEnvironment();
64
65
  const controllerUrl = await resolveControllerUrl();
@@ -68,42 +69,42 @@ async function resolveDataplaneAndAuth(systemKey, options) {
68
69
  throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register <system-key>" first.');
69
70
  }
70
71
 
71
- let dataplaneUrl;
72
- if (options.dataplane) {
73
- dataplaneUrl = options.dataplane.replace(/\/$/, '');
74
- } else {
75
- logger.log(chalk.blue('Resolving dataplane URL...'));
76
- dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
77
- }
78
-
72
+ logger.log(chalk.blue('Resolving dataplane URL...'));
73
+ const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
79
74
  return { dataplaneUrl, authConfig, environment };
80
75
  }
81
76
 
82
77
  /**
83
- * Runs upload → validate → publish on the dataplane.
78
+ * Runs single pipeline upload (upload → validate → publish) on the dataplane.
84
79
  * @param {string} dataplaneUrl - Dataplane base URL
85
80
  * @param {Object} authConfig - Auth config
86
- * @param {Object} payload - { version, application, dataSources }
87
- * @returns {Promise<{ uploadId: string }>}
81
+ * @param {Object} payload - { version, application, dataSources, status: "draft" }
82
+ * @returns {Promise<Object>} Publication result from Dataplane
88
83
  */
89
84
  async function runUploadValidatePublish(dataplaneUrl, authConfig, payload) {
90
- const uploadRes = await uploadApplicationViaPipeline(dataplaneUrl, authConfig, payload);
91
- const uploadId = uploadRes?.data?.uploadId ?? uploadRes?.data?.id ?? uploadRes?.uploadId;
92
- if (!uploadId) {
93
- const msg = uploadRes?.success === false
94
- ? formatApiError(uploadRes, dataplaneUrl)
95
- : 'Upload did not return an upload ID';
85
+ const res = await uploadApplicationViaPipeline(dataplaneUrl, authConfig, payload);
86
+ if (res?.success === false) {
87
+ const msg = formatApiError(res, dataplaneUrl);
96
88
  throw new Error(msg);
97
89
  }
90
+ return res;
91
+ }
98
92
 
99
- const validateRes = await validateUploadViaPipeline(dataplaneUrl, uploadId, authConfig);
100
- if (validateRes?.success === false) {
101
- const msg = formatApiError(validateRes, dataplaneUrl);
102
- throw new Error(`Upload validation failed: ${msg}`);
93
+ /**
94
+ * Builds a short summary of validation errors for the thrown message.
95
+ * @param {Object} validationResult - Result from validateExternalSystemComplete
96
+ * @returns {string} First few errors joined for the error message
97
+ */
98
+ function formatValidationErrorSummary(validationResult) {
99
+ const errors = validationResult.errors || [];
100
+ if (errors.length === 0) {
101
+ return 'Validation failed. Fix errors before uploading.';
103
102
  }
104
-
105
- await publishUploadViaPipeline(dataplaneUrl, uploadId, authConfig);
106
- return { uploadId };
103
+ const maxShow = 3;
104
+ const shown = errors.slice(0, maxShow).map(e => (typeof e === 'string' ? e : String(e)));
105
+ const summary = shown.join('; ');
106
+ const more = errors.length > maxShow ? ` (and ${errors.length - maxShow} more)` : '';
107
+ return `Validation failed: ${summary}${more}. Fix errors above and run the command again.`;
107
108
  }
108
109
 
109
110
  /**
@@ -114,7 +115,24 @@ async function runUploadValidatePublish(dataplaneUrl, authConfig, payload) {
114
115
  function throwIfValidationFailed(validationResult) {
115
116
  if (!validationResult.valid) {
116
117
  displayValidationResults(validationResult);
117
- throw new Error('Validation failed. Fix errors before uploading.');
118
+ throw new Error(formatValidationErrorSummary(validationResult));
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Resolves configuration arrays in the upload payload (application + dataSources) from env and secrets.
124
+ * @param {string} systemKey - System key
125
+ * @param {Object} payload - Upload payload (mutated)
126
+ */
127
+ async function resolvePayloadConfiguration(systemKey, payload) {
128
+ const { envMap, secrets } = await buildResolvedEnvMapForIntegration(systemKey);
129
+ if (Array.isArray(payload.application?.configuration) && payload.application.configuration.length > 0) {
130
+ resolveConfigurationValues(payload.application.configuration, envMap, secrets, systemKey);
131
+ }
132
+ for (const ds of payload.dataSources || []) {
133
+ if (Array.isArray(ds?.configuration) && ds.configuration.length > 0) {
134
+ resolveConfigurationValues(ds.configuration, envMap, secrets, systemKey);
135
+ }
118
136
  }
119
137
  }
120
138
 
@@ -133,7 +151,10 @@ async function pushAndLogCredentialSecrets(dataplaneUrl, authConfig, systemKey,
133
151
  payload
134
152
  });
135
153
  if (pushResult.pushed > 0) {
136
- logger.log(chalk.green(`Pushed ${pushResult.pushed} credential secret(s) to dataplane.`));
154
+ const keyList = pushResult.keys?.length ? ` (${pushResult.keys.join(', ')})` : '';
155
+ logger.log(chalk.green(`Pushed ${pushResult.pushed} credential secret(s) to dataplane${keyList}.`));
156
+ } else {
157
+ logger.log(chalk.yellow('Secret push skipped'));
137
158
  }
138
159
  if (pushResult.warning) {
139
160
  logger.log(chalk.yellow(`Warning: ${pushResult.warning}`));
@@ -141,11 +162,10 @@ async function pushAndLogCredentialSecrets(dataplaneUrl, authConfig, systemKey,
141
162
  }
142
163
 
143
164
  /**
144
- * Uploads external system to dataplane (upload validate → publish). No controller deploy.
165
+ * Uploads external system to dataplane (single pipeline upload). No controller deploy.
145
166
  * @param {string} systemKey - External system key (integration/<system-key>/)
146
167
  * @param {Object} [options] - Options
147
168
  * @param {boolean} [options.dryRun] - Validate and build payload only; no API calls
148
- * @param {string} [options.dataplane] - Override dataplane URL
149
169
  * @returns {Promise<void>}
150
170
  * @throws {Error} If validation or API calls fail
151
171
  */
@@ -159,6 +179,7 @@ async function uploadExternalSystem(systemKey, options = {}) {
159
179
 
160
180
  const manifest = await generateControllerManifest(systemKey, { type: 'external' });
161
181
  const payload = buildUploadPayload(manifest);
182
+ await resolvePayloadConfiguration(systemKey, payload);
162
183
 
163
184
  if (options.dryRun) {
164
185
  logger.log(chalk.yellow('Dry run: would upload payload (no API calls).'));
@@ -166,13 +187,23 @@ async function uploadExternalSystem(systemKey, options = {}) {
166
187
  return;
167
188
  }
168
189
 
169
- const { dataplaneUrl, authConfig, environment } = await resolveDataplaneAndAuth(systemKey, options);
190
+ const { dataplaneUrl, authConfig, environment } = await resolveDataplaneAndAuth(systemKey);
170
191
  requireBearerForDataplanePipeline(authConfig);
171
192
  logger.log(chalk.blue(`Dataplane: ${dataplaneUrl}`));
193
+ logDataplanePipelineWarning();
172
194
 
173
195
  await pushAndLogCredentialSecrets(dataplaneUrl, authConfig, systemKey, payload);
174
196
  await runUploadValidatePublish(dataplaneUrl, authConfig, payload);
197
+ logUploadSuccess(environment, systemKey, dataplaneUrl);
198
+ }
175
199
 
200
+ /**
201
+ * Logs upload success summary.
202
+ * @param {string} environment - Environment key
203
+ * @param {string} systemKey - System key
204
+ * @param {string} dataplaneUrl - Dataplane URL
205
+ */
206
+ function logUploadSuccess(environment, systemKey, dataplaneUrl) {
176
207
  logger.log(chalk.green('\nUpload validated and published to dataplane.'));
177
208
  logger.log(chalk.blue(`Environment: ${environment}`));
178
209
  logger.log(chalk.blue(`System: ${systemKey}`));
@@ -3,11 +3,18 @@
3
3
  * @author AI Fabrix Team
4
4
  * @version 2.0.0
5
5
  */
6
+ /* eslint-disable max-lines -- 502 lines; wizard payload and helpers */
6
7
 
7
8
  const chalk = require('chalk');
8
9
  const ora = require('ora');
10
+ const path = require('path');
11
+ const fs = require('fs').promises;
12
+ const yaml = require('js-yaml');
9
13
  const logger = require('../utils/logger');
14
+ const { getIntegrationPath } = require('../utils/paths');
10
15
  const { parseOpenApi, testMcpConnection, credentialSelection } = require('../api/wizard.api');
16
+ const { listCredentials } = require('../api/credentials.api');
17
+ const { listExternalSystems, getExternalSystem } = require('../api/external-systems.api');
11
18
 
12
19
  /**
13
20
  * Parse OpenAPI file or URL
@@ -158,6 +165,7 @@ function buildConfigPreferences(configPrefs) {
158
165
  intent: configPrefs?.intent || 'general integration',
159
166
  fieldOnboardingLevel: configPrefs?.fieldOnboardingLevel || 'full',
160
167
  enableOpenAPIGeneration: configPrefs?.enableOpenAPIGeneration !== false,
168
+ debug: configPrefs?.debug === true,
161
169
  userPreferences: {
162
170
  enableMCP: configPrefs?.enableMCP || false,
163
171
  enableABAC: configPrefs?.enableABAC || false,
@@ -174,10 +182,12 @@ function buildConfigPreferences(configPrefs) {
174
182
  * @param {string} params.mode - Selected mode
175
183
  * @param {Object} params.prefs - Configuration preferences
176
184
  * @param {string} [params.credentialIdOrKey] - Credential ID or key
177
- * @param {string} [params.systemIdOrKey] - System ID or key
185
+ * @param {string} [params.systemIdOrKey] - System ID or key (application/system key, not datasource/entity key)
186
+ * @param {string} [params.entityName] - Entity name for multi-entity OpenAPI (from discover-entities)
187
+ * @param {string|null} [params.systemDisplayName] - System-level display name for credential (e.g. 'Hubspot Demo')
178
188
  * @returns {Object} Configuration payload
179
189
  */
180
- function buildConfigPayload({ openapiSpec, detectedType, mode, prefs, credentialIdOrKey, systemIdOrKey }) {
190
+ function buildConfigPayload({ openapiSpec, detectedType, mode, prefs, credentialIdOrKey, systemIdOrKey, entityName, systemDisplayName }) {
181
191
  const detectedTypeValue = detectedType?.recommendedType || detectedType?.apiType || detectedType?.selectedType || 'record-based';
182
192
  const payload = {
183
193
  openapiSpec,
@@ -190,6 +200,33 @@ function buildConfigPayload({ openapiSpec, detectedType, mode, prefs, credential
190
200
  };
191
201
  if (credentialIdOrKey) payload.credentialIdOrKey = credentialIdOrKey;
192
202
  if (systemIdOrKey) payload.systemIdOrKey = systemIdOrKey;
203
+ if (entityName) payload.entityName = entityName;
204
+ if (systemDisplayName) payload.systemDisplayName = systemDisplayName;
205
+ if (prefs.debug) payload.debug = true;
206
+ return payload;
207
+ }
208
+
209
+ /**
210
+ * Build payload for platform config endpoint.
211
+ * Schema allows only: datasourceKeys?, credentialIdOrKey?, configurationValues?, debug?
212
+ * (additionalProperties: false - do NOT send intent, mode, fieldOnboardingLevel, etc.)
213
+ * @param {Object} params - Parameters object
214
+ * @param {string} [params.credentialIdOrKey] - Credential ID or key
215
+ * @param {string[]} [params.datasourceKeys] - Datasource keys to include (defaults to all)
216
+ * @param {Object} [params.configurationValues] - Configuration value overrides
217
+ * @param {boolean} [params.debug] - When true, capture debug log
218
+ * @returns {Object} Platform config payload
219
+ */
220
+ function buildPlatformConfigPayload({ credentialIdOrKey, datasourceKeys, configurationValues, debug }) {
221
+ const payload = {};
222
+ if (credentialIdOrKey) payload.credentialIdOrKey = credentialIdOrKey;
223
+ if (datasourceKeys && Array.isArray(datasourceKeys) && datasourceKeys.length > 0) {
224
+ payload.datasourceKeys = datasourceKeys;
225
+ }
226
+ if (configurationValues && typeof configurationValues === 'object' && Object.keys(configurationValues).length > 0) {
227
+ payload.configurationValues = configurationValues;
228
+ }
229
+ if (debug) payload.debug = true;
193
230
  return payload;
194
231
  }
195
232
 
@@ -250,14 +287,19 @@ function formatValidationDetailsPlain(errorData) {
250
287
  /**
251
288
  * Create and throw config generation error with optional formatted message
252
289
  * @param {Object} generateResponse - API response (error)
290
+ * @param {Object} [options] - Optional options
291
+ * @param {string} [options.debugManifestHint] - Hint to append when debug manifest was saved (e.g. review debug.log and fix manually)
253
292
  * @throws {Error}
254
293
  */
255
- function throwConfigGenerationError(generateResponse) {
294
+ function throwConfigGenerationError(generateResponse, options = {}) {
256
295
  const summary = generateResponse.error || generateResponse.formattedError || 'Unknown error';
257
296
  const detailsPlain = formatValidationDetailsPlain(generateResponse.errorData);
258
- const message = detailsPlain
297
+ let message = detailsPlain
259
298
  ? `Configuration generation failed: ${summary}\n${detailsPlain}`
260
299
  : `Configuration generation failed: ${summary}`;
300
+ if (options.debugManifestHint) {
301
+ message += `\n\n${options.debugManifestHint}`;
302
+ }
261
303
  const err = new Error(message);
262
304
  if (generateResponse.formattedError) {
263
305
  err.formatted = generateResponse.formattedError;
@@ -265,6 +307,181 @@ function throwConfigGenerationError(generateResponse) {
265
307
  throw err;
266
308
  }
267
309
 
310
+ /**
311
+ * Write debug log to integration/<appName>/debug.log
312
+ * @async
313
+ * @param {string} appName - Application name
314
+ * @param {string} content - Debug log content
315
+ */
316
+ async function writeDebugLog(appName, content) {
317
+ try {
318
+ const dir = getIntegrationPath(appName);
319
+ await fs.mkdir(dir, { recursive: true });
320
+ const debugPath = path.join(dir, 'debug.log');
321
+ await fs.writeFile(debugPath, content, 'utf8');
322
+ logger.log(chalk.gray(` Debug log saved to integration/${appName}/debug.log`));
323
+ } catch (e) {
324
+ logger.warn(`Could not save debug.log: ${e.message}`);
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Write systemConfig and datasourceConfig from error response for manual fix
330
+ * @async
331
+ * @param {string} appName - Application name
332
+ * @param {Object} [systemConfig] - System config from errorData
333
+ * @param {Object|Object[]} [datasourceConfig] - Datasource config(s) from errorData
334
+ * @returns {Promise<string[]>} Names of saved files
335
+ */
336
+ async function writeDebugManifest(appName, systemConfig, datasourceConfig) {
337
+ const saved = [];
338
+ try {
339
+ const dir = getIntegrationPath(appName);
340
+ await fs.mkdir(dir, { recursive: true });
341
+ if (systemConfig && typeof systemConfig === 'object') {
342
+ const systemPath = path.join(dir, 'debug-system.yaml');
343
+ await fs.writeFile(systemPath, yaml.dump(systemConfig, { lineWidth: -1 }), 'utf8');
344
+ saved.push('debug-system.yaml');
345
+ logger.log(chalk.gray(` Debug manifest saved to integration/${appName}/debug-system.yaml`));
346
+ }
347
+ if (datasourceConfig !== undefined && datasourceConfig !== null) {
348
+ const configs = Array.isArray(datasourceConfig) ? datasourceConfig : [datasourceConfig];
349
+ if (configs.length > 0 && configs.every(c => c && typeof c === 'object')) {
350
+ const datasourcePath = path.join(dir, 'debug-datasource.yaml');
351
+ const toWrite = configs.length === 1 ? configs[0] : configs;
352
+ await fs.writeFile(datasourcePath, yaml.dump(toWrite, { lineWidth: -1 }), 'utf8');
353
+ saved.push('debug-datasource.yaml');
354
+ logger.log(chalk.gray(` Debug manifest saved to integration/${appName}/debug-datasource.yaml`));
355
+ }
356
+ }
357
+ } catch (e) {
358
+ logger.warn(`Could not save debug manifest: ${e.message}`);
359
+ }
360
+ return saved;
361
+ }
362
+
363
+ /**
364
+ * Save debug manifest on error and throw
365
+ * @param {Object} generateResponse - API error response
366
+ * @param {Object} opts - Options
367
+ * @param {boolean} opts.enableDebug - Whether debug was enabled
368
+ * @param {string} [opts.appName] - App name for writing files
369
+ */
370
+ async function saveDebugManifestOnErrorAndThrow(generateResponse, opts) {
371
+ const { enableDebug, appName } = opts;
372
+ let debugManifestHint = null;
373
+ if (enableDebug && appName) {
374
+ const errorData = generateResponse.errorData || {};
375
+ const debugLog = errorData.debugLog;
376
+ if (debugLog && typeof debugLog === 'string') {
377
+ await writeDebugLog(appName, debugLog);
378
+ }
379
+ const systemConfig = errorData.systemConfig;
380
+ const datasourceConfig = errorData.datasourceConfig || errorData.datasourceConfigs;
381
+ const savedManifest = await writeDebugManifest(appName, systemConfig, datasourceConfig);
382
+ if (debugLog || savedManifest.length > 0) {
383
+ const files = [debugLog && 'debug.log', ...savedManifest].filter(Boolean).join(', ');
384
+ debugManifestHint = `Debug manifest saved to integration/${appName}/ (${files}). Review the log and fix the manifest manually, then run: aifabrix wizard ${appName}`;
385
+ }
386
+ }
387
+ throwConfigGenerationError(generateResponse, { debugManifestHint });
388
+ }
389
+
390
+ /** Throws with validation error; saves debug manifest when options.debug and options.appName are set. */
391
+ async function throwValidationFailureWithDebug(validateResponse, systemConfig, configs, errorMsg, options) {
392
+ if (!options.debug || !options.appName) throw new Error(`Configuration validation failed: ${errorMsg}`);
393
+ const errorData = validateResponse.errorData || validateResponse.data || {};
394
+ const debugLog = errorData.debugLog;
395
+ if (debugLog && typeof debugLog === 'string') await writeDebugLog(options.appName, debugLog);
396
+ const savedManifest = await writeDebugManifest(
397
+ options.appName, errorData.systemConfig || systemConfig, errorData.datasourceConfig || errorData.datasourceConfigs || configs
398
+ );
399
+ if (!debugLog && savedManifest.length === 0) throw new Error(`Configuration validation failed: ${errorMsg}`);
400
+ const files = [debugLog && 'debug.log', ...savedManifest].filter(Boolean).join(', ');
401
+ throw new Error(`Configuration validation failed: ${errorMsg}\n\nDebug manifest saved to integration/${options.appName}/ (${files}). Review the log and fix the manifest manually, then run: aifabrix wizard ${options.appName}`);
402
+ }
403
+
404
+ /**
405
+ * Resolve credential config from user action (select/skip/create)
406
+ * @async
407
+ * @param {string} dataplaneUrl - Dataplane URL
408
+ * @param {Object} authConfig - Auth config
409
+ * @param {Object} credentialAction - Result from promptForCredentialAction
410
+ * @returns {Promise<Object>} configCredential for handleCredentialSelection
411
+ */
412
+ async function resolveCredentialConfig(dataplaneUrl, authConfig, credentialAction) {
413
+ const { promptForExistingCredential } = require('../generator/wizard-prompts');
414
+ if (credentialAction.action !== 'select') {
415
+ return credentialAction.action === 'skip' ? { action: 'skip' } : { action: credentialAction.action };
416
+ }
417
+ let credentialsList = [];
418
+ try {
419
+ const listResponse = await listCredentials(dataplaneUrl, authConfig, {
420
+ activeOnly: true,
421
+ pageSize: 100
422
+ });
423
+ const body = listResponse?.data ?? listResponse;
424
+ const inner = Array.isArray(body) ? body : (body?.data ?? body);
425
+ credentialsList = Array.isArray(inner) ? inner : (inner?.credentials ?? inner?.items ?? []) || [];
426
+ } catch (_) {
427
+ credentialsList = [];
428
+ }
429
+ const selected = await promptForExistingCredential(Array.isArray(credentialsList) ? credentialsList : []);
430
+ return { action: 'select', credentialIdOrKey: selected.credentialIdOrKey };
431
+ }
432
+
433
+ /**
434
+ * Fetch external systems list from dataplane
435
+ * @async
436
+ * @param {string} dataplaneUrl - Dataplane URL
437
+ * @param {Object} authConfig - Auth config
438
+ * @returns {Promise<Object[]>} Systems list
439
+ */
440
+ async function fetchSystemsListForAddDatasource(dataplaneUrl, authConfig) {
441
+ try {
442
+ const listResponse = await listExternalSystems(dataplaneUrl, authConfig, { pageSize: 100 });
443
+ const body = listResponse?.data ?? listResponse;
444
+ const inner = Array.isArray(body) ? body : (body?.data ?? body);
445
+ const list = Array.isArray(inner) ? inner : (inner?.items ?? inner?.systems ?? []) || [];
446
+ return Array.isArray(list) ? list : [];
447
+ } catch (_) {
448
+ return [];
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Resolve and validate external system for add-datasource (prompt until valid)
454
+ * @async
455
+ * @param {string} dataplaneUrl - Dataplane URL
456
+ * @param {Object} authConfig - Auth config
457
+ * @param {Object[]} systemsList - Systems list
458
+ * @param {string} initialSystemIdOrKey - Initial system ID or key
459
+ * @returns {Promise<{systemResponse: Object, systemIdOrKey: string}>}
460
+ */
461
+ async function resolveExternalSystemForAddDatasource(dataplaneUrl, authConfig, systemsList, initialSystemIdOrKey) {
462
+ const { promptForExistingSystem } = require('../generator/wizard-prompts');
463
+ const { isExternalSystemForAddDatasource } = require('./wizard-helpers');
464
+ let systemIdOrKey = initialSystemIdOrKey;
465
+ let systemResponse;
466
+ for (;;) {
467
+ try {
468
+ systemResponse = await getExternalSystem(dataplaneUrl, systemIdOrKey, authConfig);
469
+ const sys = systemResponse?.data || systemResponse;
470
+ if (sys && (systemResponse?.data || systemResponse?.success)) {
471
+ if (!isExternalSystemForAddDatasource(sys)) {
472
+ logger.log(chalk.red('Cannot add datasource to a webapp. Please select an external system.'));
473
+ } else {
474
+ break;
475
+ }
476
+ }
477
+ } catch (err) {
478
+ logger.log(chalk.red(`System not found or error: ${err.message}`));
479
+ }
480
+ systemIdOrKey = await promptForExistingSystem(systemsList, systemIdOrKey);
481
+ }
482
+ return { systemResponse, systemIdOrKey };
483
+ }
484
+
268
485
  module.exports = {
269
486
  parseOpenApiSource,
270
487
  testMcpServerConnection,
@@ -272,7 +489,15 @@ module.exports = {
272
489
  runCredentialSelectionLoop,
273
490
  buildConfigPreferences,
274
491
  buildConfigPayload,
492
+ buildPlatformConfigPayload,
275
493
  extractConfigurationFromResponse,
276
494
  formatValidationDetailsPlain,
277
- throwConfigGenerationError
495
+ throwConfigGenerationError,
496
+ writeDebugLog,
497
+ writeDebugManifest,
498
+ saveDebugManifestOnErrorAndThrow,
499
+ throwValidationFailureWithDebug,
500
+ resolveCredentialConfig,
501
+ fetchSystemsListForAddDatasource,
502
+ resolveExternalSystemForAddDatasource
278
503
  };