@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
@@ -0,0 +1,294 @@
1
+ /**
2
+ * @fileoverview Secondary wizard prompts (credential retry, platform, config review)
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const inquirer = require('inquirer');
8
+ const yaml = require('js-yaml');
9
+
10
+ let hasAutocompletePrompt = false;
11
+ try {
12
+ const AutocompletePrompt = require('inquirer-autocomplete-prompt');
13
+ inquirer.registerPrompt('autocomplete', AutocompletePrompt);
14
+ hasAutocompletePrompt = true;
15
+ } catch {
16
+ // Fallback: use 'list' if plugin not installed (no search, pageSize 10)
17
+ }
18
+
19
+ /**
20
+ * Re-prompt for credential ID/key when validation failed (e.g. not found on dataplane).
21
+ * Empty input means skip.
22
+ * @async
23
+ * @param {string} [previousError] - Error message from dataplane (e.g. "Credential not found")
24
+ * @returns {Promise<Object>} { credentialIdOrKey: string } or { skip: true } if user leaves empty
25
+ */
26
+ async function promptForCredentialIdOrKeyRetry(previousError) {
27
+ const msg = previousError
28
+ ? `Credential not found or invalid (${String(previousError).slice(0, 60)}). Enter ID/key or leave empty to skip:`
29
+ : 'Enter credential ID or key (or leave empty to skip):';
30
+ const { credentialIdOrKey } = await inquirer.prompt([
31
+ { type: 'input', name: 'credentialIdOrKey', message: msg, default: '' }
32
+ ]);
33
+ const trimmed = (credentialIdOrKey && credentialIdOrKey.trim()) || '';
34
+ return trimmed ? { credentialIdOrKey: trimmed } : { skip: true };
35
+ }
36
+
37
+ /**
38
+ * Prompt for known platform selection
39
+ * @async
40
+ * @param {Array<{key: string, displayName?: string}>} [platforms] - List of available platforms
41
+ * @returns {Promise<string>} Selected platform key
42
+ */
43
+ async function promptForKnownPlatform(platforms = []) {
44
+ const defaultPlatforms = [
45
+ { name: 'HubSpot', value: 'hubspot' },
46
+ { name: 'Salesforce', value: 'salesforce' },
47
+ { name: 'Zendesk', value: 'zendesk' },
48
+ { name: 'Slack', value: 'slack' },
49
+ { name: 'Microsoft 365', value: 'microsoft365' }
50
+ ];
51
+ const choices = platforms.length > 0
52
+ ? platforms.map(p => ({ name: p.displayName || p.key, value: p.key }))
53
+ : defaultPlatforms;
54
+ const { platform } = await inquirer.prompt([
55
+ { type: 'list', name: 'platform', message: 'Select a platform:', choices, pageSize: 10 }
56
+ ]);
57
+ return platform;
58
+ }
59
+
60
+ /**
61
+ * Format entity for display in list
62
+ * @param {Object} e - Entity { name, pathCount? }
63
+ * @returns {string} Display string
64
+ */
65
+ function _formatEntityChoice(e) {
66
+ if (!e || typeof e.name !== 'string') return 'unknown';
67
+ return e.pathCount !== undefined && e.pathCount !== null ? `${e.name} (${e.pathCount} paths)` : e.name;
68
+ }
69
+
70
+ /**
71
+ * Prompt to select an entity from discover-entities list (searchable when plugin available)
72
+ * @async
73
+ * @param {Array<{name: string, pathCount?: number, schemaMatch?: boolean}>} entities - From discover-entities
74
+ * @returns {Promise<string>} Selected entity name
75
+ */
76
+ async function promptForEntitySelection(entities = []) {
77
+ if (!Array.isArray(entities) || entities.length === 0) {
78
+ throw new Error('At least one entity is required');
79
+ }
80
+ const choices = entities.map(e => ({
81
+ name: _formatEntityChoice(e),
82
+ value: e.name
83
+ }));
84
+
85
+ const promptConfig = {
86
+ type: hasAutocompletePrompt ? 'autocomplete' : 'list',
87
+ name: 'entityName',
88
+ message: 'Select entity for datasource generation:',
89
+ choices,
90
+ pageSize: 10
91
+ };
92
+
93
+ if (hasAutocompletePrompt) {
94
+ promptConfig.source = (answers, input) => {
95
+ const q = (input || '').toLowerCase();
96
+ const filtered = entities.filter(e => (e.name || '').toLowerCase().includes(q));
97
+ return Promise.resolve(
98
+ filtered.map(e => ({ name: _formatEntityChoice(e), value: e.name }))
99
+ );
100
+ };
101
+ }
102
+
103
+ const { entityName } = await inquirer.prompt([promptConfig]);
104
+ return entityName;
105
+ }
106
+
107
+ /** @param {*} o - Value to stringify */
108
+ const _s = (o) => (o === null || o === undefined || o === '' ? '—' : String(o));
109
+
110
+ /** @param {Object} sys - systemSummary */
111
+ function _formatSystem(sys) {
112
+ return [
113
+ '\nSystem',
114
+ ` Key: ${_s(sys.key)}`,
115
+ ` Display name: ${_s(sys.displayName)}`,
116
+ ` Type: ${_s(sys.type)}`,
117
+ ` Base URL: ${_s(sys.baseUrl)}`,
118
+ ` Auth: ${_s(sys.authenticationType)}`,
119
+ ` Endpoints: ${_s(sys.endpointCount)}`
120
+ ];
121
+ }
122
+
123
+ /** @param {Object} ds - datasourceSummary */
124
+ function _formatDatasource(ds) {
125
+ return [
126
+ '\nDatasource',
127
+ ` Key: ${_s(ds.key)}`,
128
+ ` Entity: ${_s(ds.entity)}`,
129
+ ` Resource type: ${_s(ds.resourceType)}`,
130
+ ` CIP steps: ${_s(ds.cipStepCount)}`,
131
+ ` Field mappings: ${_s(ds.fieldMappingCount)}`,
132
+ ` Exposed: ${_s(ds.exposedProfileCount)} profiles`
133
+ ];
134
+ }
135
+
136
+ /** @param {Object} fm - fieldMappingsSummary */
137
+ function _formatFieldMappings(fm) {
138
+ const mapped = Array.isArray(fm.mappedFields) ? fm.mappedFields : [];
139
+ const unmapped = Array.isArray(fm.unmappedFields) ? fm.unmappedFields : [];
140
+ const mappedStr = mapped.length > 0
141
+ ? `${mapped.length} (${mapped.slice(0, 5).join(', ')}${mapped.length > 5 ? ', ...' : ''})`
142
+ : _s(fm.mappingCount);
143
+ const unmappedStr = unmapped.length > 0
144
+ ? `${unmapped.length} (${unmapped.slice(0, 3).join(', ')}${unmapped.length > 3 ? ', ...' : ''})`
145
+ : '0';
146
+ return ['\nField Mappings', ` Mapped: ${mappedStr}`, ` Unmapped: ${unmappedStr}`];
147
+ }
148
+
149
+ /**
150
+ * Derive a preview summary from systemConfig and datasourceConfigs when the dataplane
151
+ * preview API does not return summaries. Ensures a compact summary is always shown.
152
+ * @param {Object} systemConfig - System configuration
153
+ * @param {Object[]} datasourceConfigs - Array of datasource configurations
154
+ * @returns {Object} Preview object compatible with formatPreviewSummary
155
+ */
156
+ function _buildDatasourceSummary(ds) {
157
+ let fieldMappingCount = 0;
158
+ const attrs = ds.fieldMappings?.attributes ?? ds.attributes ?? {};
159
+ if (typeof attrs === 'object' && !Array.isArray(attrs)) {
160
+ fieldMappingCount = Object.keys(attrs).length;
161
+ }
162
+ const exposedCount = ds.exposed?.attributes?.length ?? 0;
163
+ return {
164
+ key: ds.key,
165
+ entity: ds.entityType ?? ds.entity ?? ds.resourceType ?? ds.key?.split('-').pop(),
166
+ resourceType: ds.resourceType,
167
+ cipStepCount: null,
168
+ fieldMappingCount: fieldMappingCount || null,
169
+ exposedProfileCount: exposedCount || null
170
+ };
171
+ }
172
+
173
+ function _buildSystemSummary(sys) {
174
+ const baseUrl = sys.openapi?.servers?.[0]?.url ?? sys.baseUrl ?? sys.openapi?.baseUrl ?? null;
175
+ const authType = sys.authentication?.type ?? sys.authenticationType ?? null;
176
+ const endpointCount = sys.openapi?.endpoints?.length ??
177
+ (sys.openapi?.operations ? Object.keys(sys.openapi.operations || {}).length : null);
178
+ return {
179
+ key: sys.key,
180
+ displayName: sys.displayName,
181
+ type: sys.type,
182
+ baseUrl,
183
+ authenticationType: authType,
184
+ endpointCount
185
+ };
186
+ }
187
+
188
+ function _buildFieldMappingsSummary(ds0) {
189
+ const attrs0 = ds0.fieldMappings?.attributes ?? ds0.attributes ?? {};
190
+ const mappedFields = (typeof attrs0 === 'object' && !Array.isArray(attrs0)) ? Object.keys(attrs0) : [];
191
+ return mappedFields.length > 0 ? { mappingCount: mappedFields.length, mappedFields, unmappedFields: [] } : null;
192
+ }
193
+
194
+ function derivePreviewFromConfig(systemConfig, datasourceConfigs) {
195
+ const sys = systemConfig || {};
196
+ const dsList = Array.isArray(datasourceConfigs) ? datasourceConfigs : (datasourceConfigs ? [datasourceConfigs] : []);
197
+
198
+ const result = {
199
+ systemSummary: _buildSystemSummary(sys),
200
+ fieldMappingsSummary: _buildFieldMappingsSummary(dsList[0] || {})
201
+ };
202
+ const datasourceSummaries = dsList.map(_buildDatasourceSummary);
203
+ if (datasourceSummaries.length === 1) {
204
+ result.datasourceSummary = datasourceSummaries[0];
205
+ } else if (datasourceSummaries.length > 1) {
206
+ result.datasourceSummaries = datasourceSummaries;
207
+ }
208
+ return result;
209
+ }
210
+
211
+ /**
212
+ * Format a preview summary for display (WizardPreviewResponse from dataplane)
213
+ * @param {Object} preview - Preview data from GET /api/v1/wizard/preview/{sessionId}
214
+ * @returns {string} Formatted summary text
215
+ */
216
+ function formatPreviewSummary(preview) {
217
+ const hasVal = (v) => v !== null && v !== undefined;
218
+ const parts = ['\n📋 Configuration Preview (what will be created)', '─'.repeat(60)];
219
+
220
+ if (preview.systemSummary) parts.push(..._formatSystem(preview.systemSummary));
221
+ if (preview.datasourceSummaries && preview.datasourceSummaries.length > 0) {
222
+ preview.datasourceSummaries.forEach((ds, i) => {
223
+ const lines = _formatDatasource(ds);
224
+ const label = preview.datasourceSummaries.length > 1 ? `Datasource ${i + 1}` : 'Datasource';
225
+ parts.push(...lines.map((line, j) => (j === 0 ? line.replace('Datasource', label) : line)));
226
+ });
227
+ } else if (preview.datasourceSummary) {
228
+ parts.push(..._formatDatasource(preview.datasourceSummary));
229
+ }
230
+ if (preview.cipPipelineSummary) {
231
+ const cip = preview.cipPipelineSummary;
232
+ parts.push('\nCIP Pipeline', ` Steps: ${_s(cip.stepCount)}`, ` Est. execution: ${_s(cip.estimatedExecutionTime) || '—'}`);
233
+ }
234
+ if (preview.fieldMappingsSummary) parts.push(..._formatFieldMappings(preview.fieldMappingsSummary));
235
+ if (hasVal(preview.estimatedRecords) || hasVal(preview.estimatedSyncTime)) {
236
+ parts.push('\nEstimates', ` Records: ${_s(preview.estimatedRecords)}`, ` Sync: ${_s(preview.estimatedSyncTime)}`);
237
+ }
238
+ parts.push('\n' + '─'.repeat(60));
239
+ return parts.join('\n');
240
+ }
241
+
242
+ /**
243
+ * Prompt for configuration review and editing
244
+ * When preview is provided, displays a compact summary; otherwise dumps full YAML.
245
+ * @async
246
+ * @param {Object} opts - Options
247
+ * @param {Object|null} [opts.preview] - Preview data from getPreview (null = fallback to YAML)
248
+ * @param {Object} opts.systemConfig - System configuration
249
+ * @param {Object[]} opts.datasourceConfigs - Array of datasource configurations
250
+ * @returns {Promise<Object>} Object with action ('accept'|'cancel') and optionally edited configs
251
+ */
252
+ async function promptForConfigReview({ preview, systemConfig, datasourceConfigs }) {
253
+ const hasSummary = preview && (preview.systemSummary || preview.datasourceSummary || (preview.datasourceSummaries && preview.datasourceSummaries.length > 0));
254
+ const summaryToShow = hasSummary ? preview : derivePreviewFromConfig(systemConfig, datasourceConfigs);
255
+ const canShowSummary = summaryToShow.systemSummary || summaryToShow.datasourceSummary ||
256
+ (summaryToShow.datasourceSummaries && summaryToShow.datasourceSummaries.length > 0);
257
+
258
+ if (canShowSummary) {
259
+ // eslint-disable-next-line no-console
260
+ console.log(formatPreviewSummary(summaryToShow));
261
+ } else {
262
+ // eslint-disable-next-line no-console
263
+ console.log('\n📋 Generated Configuration:\nSystem Configuration:');
264
+ // eslint-disable-next-line no-console
265
+ console.log(yaml.dump(systemConfig, { lineWidth: -1 }));
266
+ // eslint-disable-next-line no-console
267
+ console.log('Datasource Configurations:');
268
+ (datasourceConfigs || []).forEach((ds, index) => {
269
+ // eslint-disable-next-line no-console
270
+ console.log(`\nDatasource ${index + 1}:\n${yaml.dump(ds, { lineWidth: -1 })}`);
271
+ });
272
+ }
273
+ const { action } = await inquirer.prompt([
274
+ {
275
+ type: 'list',
276
+ name: 'action',
277
+ message: 'What would you like to do?',
278
+ choices: [
279
+ { name: 'Accept and save', value: 'accept' },
280
+ { name: 'Cancel', value: 'cancel' }
281
+ ]
282
+ }
283
+ ]);
284
+ return action === 'cancel' ? { action: 'cancel' } : { action: 'accept' };
285
+ }
286
+
287
+ module.exports = {
288
+ promptForCredentialIdOrKeyRetry,
289
+ promptForKnownPlatform,
290
+ promptForEntitySelection,
291
+ promptForConfigReview,
292
+ derivePreviewFromConfig,
293
+ formatPreviewSummary
294
+ };
@@ -7,18 +7,20 @@
7
7
  const inquirer = require('inquirer');
8
8
  const path = require('path');
9
9
  const fs = require('fs').promises;
10
+ const { formatCredentialWithStatus } = require('../utils/credential-display');
10
11
 
11
12
  /**
12
13
  * Prompt for wizard mode selection
13
14
  * @async
14
15
  * @function promptForMode
15
16
  * @param {string} [defaultMode] - Default value ('create-system' | 'add-datasource')
17
+ * @param {boolean} [allowAddDatasource=true] - If false, only show "Create a new external system"
16
18
  * @returns {Promise<string>} Selected mode ('create-system' | 'add-datasource')
17
19
  */
18
- async function promptForMode(defaultMode) {
20
+ async function promptForMode(defaultMode, allowAddDatasource = true) {
19
21
  const choices = [
20
22
  { name: 'Create a new external system', value: 'create-system' },
21
- { name: 'Add datasource to existing system', value: 'add-datasource' }
23
+ ...(allowAddDatasource ? [{ name: 'Add datasource to existing system', value: 'add-datasource' }] : [])
22
24
  ];
23
25
  const { mode } = await inquirer.prompt([
24
26
  {
@@ -26,6 +28,7 @@ async function promptForMode(defaultMode) {
26
28
  name: 'mode',
27
29
  message: 'What would you like to do?',
28
30
  choices,
31
+ pageSize: 10,
29
32
  default: defaultMode && choices.some(c => c.value === defaultMode) ? defaultMode : undefined
30
33
  }
31
34
  ]);
@@ -35,12 +38,58 @@ async function promptForMode(defaultMode) {
35
38
  /**
36
39
  * Prompt for existing system ID or key (for add-datasource mode).
37
40
  * Only external systems (OpenAPI, MCP, custom) support add-datasource; webapps do not.
41
+ * When a list is available, use promptForExistingSystem instead to show a selection list.
38
42
  * @async
39
43
  * @function promptForSystemIdOrKey
40
44
  * @param {string} [defaultValue] - Default value (e.g. from loaded wizard.yaml)
41
45
  * @returns {Promise<string>} System ID or key
42
46
  */
43
47
  async function promptForSystemIdOrKey(defaultValue) {
48
+ return promptForExistingSystemInput(defaultValue);
49
+ }
50
+
51
+ /**
52
+ * Prompt to select an existing external system: show a list from the dataplane when available, otherwise ask for ID/key.
53
+ * @async
54
+ * @function promptForExistingSystem
55
+ * @param {Array<{key?: string, id?: string, displayName?: string}>} [systemsList] - External systems from GET /api/v1/external/systems (or empty/null on error)
56
+ * @param {string} [defaultValue] - Default value (e.g. from loaded wizard.yaml)
57
+ * @returns {Promise<string>} Selected system ID or key
58
+ */
59
+ async function promptForExistingSystem(systemsList = [], defaultValue) {
60
+ const list = Array.isArray(systemsList) ? systemsList : [];
61
+ if (list.length > 0) {
62
+ const choices = list.map((s) => {
63
+ const value = s.key ?? s.id ?? '';
64
+ const displayName = s.displayName ?? s.name ?? value;
65
+ const name = displayName === value ? String(value) : `${displayName} (${value})`;
66
+ return { name: String(name), value };
67
+ }).filter((c) => c.value);
68
+ if (choices.length === 0) {
69
+ return promptForExistingSystemInput(defaultValue);
70
+ }
71
+ const { systemIdOrKey } = await inquirer.prompt([
72
+ {
73
+ type: 'list',
74
+ name: 'systemIdOrKey',
75
+ message: 'Select an existing external system (not a webapp):',
76
+ choices,
77
+ pageSize: 10
78
+ }
79
+ ]);
80
+ return systemIdOrKey;
81
+ }
82
+ return promptForExistingSystemInput(defaultValue);
83
+ }
84
+
85
+ /**
86
+ * Prompt for external system ID or key via free-text input (used when list is empty or API failed).
87
+ * @async
88
+ * @function promptForExistingSystemInput
89
+ * @param {string} [defaultValue] - Default value
90
+ * @returns {Promise<string>} System ID or key
91
+ */
92
+ async function promptForExistingSystemInput(defaultValue) {
44
93
  const { systemIdOrKey } = await inquirer.prompt([
45
94
  {
46
95
  type: 'input',
@@ -78,7 +127,8 @@ async function promptForSourceType(platforms = []) {
78
127
  type: 'list',
79
128
  name: 'sourceType',
80
129
  message: 'What is your source type?',
81
- choices
130
+ choices,
131
+ pageSize: 10
82
132
  }
83
133
  ]);
84
134
  return sourceType;
@@ -189,9 +239,10 @@ async function promptForMcpServer() {
189
239
  /**
190
240
  * Prompt for credential action (skip / create new / use existing).
191
241
  * Choose Skip if you don't have credentials yet; you can add them later in env.template.
242
+ * When "Use existing" is chosen, the caller should fetch credentials and call promptForExistingCredential.
192
243
  * @async
193
244
  * @function promptForCredentialAction
194
- * @returns {Promise<Object>} Object with action ('skip'|'create'|'select') and optional credentialIdOrKey
245
+ * @returns {Promise<Object>} Object with action ('skip'|'create'|'select'); credentialIdOrKey only when action is 'select' and chosen via promptForExistingCredential
195
246
  */
196
247
  async function promptForCredentialAction() {
197
248
  const { action } = await inquirer.prompt([
@@ -206,79 +257,65 @@ async function promptForCredentialAction() {
206
257
  ]
207
258
  }
208
259
  ]);
209
- if (action === 'select') {
260
+ return { action };
261
+ }
262
+
263
+ /**
264
+ * Prompt to select an existing credential: show a list from the dataplane when available, otherwise ask for ID/key.
265
+ * @async
266
+ * @function promptForExistingCredential
267
+ * @param {Array<{key?: string, id?: string, credentialKey?: string, displayName?: string, name?: string, status?: string}>} [credentialsList] - Credentials from GET /api/v1/wizard/credentials (or empty/null on error)
268
+ * @returns {Promise<{credentialIdOrKey: string}>} Selected credential ID or key
269
+ */
270
+ async function promptForExistingCredential(credentialsList = []) {
271
+ const list = Array.isArray(credentialsList) ? credentialsList : [];
272
+ if (list.length > 0) {
273
+ const choices = list.map((c) => {
274
+ const { name: baseName, statusFormatted, statusLabel } = formatCredentialWithStatus(c);
275
+ const name = statusFormatted
276
+ ? `${statusFormatted} ${baseName}${statusLabel}`
277
+ : String(baseName);
278
+ const value = c.key ?? c.id ?? c.credentialKey ?? '';
279
+ return { name, value };
280
+ }).filter((c) => c.value);
281
+ if (choices.length === 0) {
282
+ return promptForExistingCredentialInput();
283
+ }
210
284
  const { credentialIdOrKey } = await inquirer.prompt([
211
285
  {
212
- type: 'input',
286
+ type: 'list',
213
287
  name: 'credentialIdOrKey',
214
- message: 'Enter credential ID or key (must exist on the dataplane):',
215
- validate: (input) => {
216
- if (!input || typeof input !== 'string' || input.trim().length === 0) {
217
- return 'Credential ID or key is required (or choose Skip at the previous step)';
218
- }
219
- return true;
220
- }
288
+ message: 'Select a credential:',
289
+ choices,
290
+ pageSize: 10
221
291
  }
222
292
  ]);
223
- return { action, credentialIdOrKey: credentialIdOrKey.trim() };
293
+ return { credentialIdOrKey };
224
294
  }
225
- return { action };
295
+ return promptForExistingCredentialInput();
226
296
  }
227
297
 
228
298
  /**
229
- * Re-prompt for credential ID/key when validation failed (e.g. not found on dataplane).
230
- * Empty input means skip.
299
+ * Prompt for credential ID or key via free-text input (used when list is empty or API failed).
231
300
  * @async
232
- * @function promptForCredentialIdOrKeyRetry
233
- * @param {string} [previousError] - Error message from dataplane (e.g. "Credential not found")
234
- * @returns {Promise<Object>} { credentialIdOrKey: string } or { skip: true } if user leaves empty
301
+ * @function promptForExistingCredentialInput
302
+ * @returns {Promise<{credentialIdOrKey: string}>}
235
303
  */
236
- async function promptForCredentialIdOrKeyRetry(previousError) {
237
- const msg = previousError
238
- ? `Credential not found or invalid (${String(previousError).slice(0, 60)}). Enter ID/key or leave empty to skip:`
239
- : 'Enter credential ID or key (or leave empty to skip):';
304
+ async function promptForExistingCredentialInput() {
240
305
  const { credentialIdOrKey } = await inquirer.prompt([
241
306
  {
242
307
  type: 'input',
243
308
  name: 'credentialIdOrKey',
244
- message: msg,
245
- default: ''
246
- }
247
- ]);
248
- const trimmed = (credentialIdOrKey && credentialIdOrKey.trim()) || '';
249
- return trimmed ? { credentialIdOrKey: trimmed } : { skip: true };
250
- }
251
-
252
- /**
253
- * Prompt for known platform selection
254
- * @async
255
- * @function promptForKnownPlatform
256
- * @param {Array<{key: string, displayName?: string}>} [platforms] - List of available platforms (if provided)
257
- * @returns {Promise<string>} Selected platform key
258
- */
259
- async function promptForKnownPlatform(platforms = []) {
260
- // Default platforms if none provided
261
- const defaultPlatforms = [
262
- { name: 'HubSpot', value: 'hubspot' },
263
- { name: 'Salesforce', value: 'salesforce' },
264
- { name: 'Zendesk', value: 'zendesk' },
265
- { name: 'Slack', value: 'slack' },
266
- { name: 'Microsoft 365', value: 'microsoft365' }
267
- ];
268
-
269
- const choices = platforms.length > 0
270
- ? platforms.map(p => ({ name: p.displayName || p.key, value: p.key }))
271
- : defaultPlatforms;
272
-
273
- const { platform } = await inquirer.prompt([
274
- {
275
- type: 'list',
276
- name: 'platform',
277
- message: 'Select a platform:',
278
- choices
309
+ message: 'Enter credential ID or key (must exist on the dataplane):',
310
+ validate: (input) => {
311
+ if (!input || typeof input !== 'string' || input.trim().length === 0) {
312
+ return 'Credential ID or key is required (or choose Skip at the previous step)';
313
+ }
314
+ return true;
315
+ }
279
316
  }
280
317
  ]);
281
- return platform;
318
+ return { credentialIdOrKey: credentialIdOrKey.trim() };
282
319
  }
283
320
 
284
321
  /**
@@ -351,49 +388,6 @@ async function promptForUserPreferences() {
351
388
  };
352
389
  }
353
390
 
354
- /**
355
- * Prompt for configuration review and editing
356
- * @async
357
- * @function promptForConfigReview
358
- * @param {Object} systemConfig - System configuration
359
- * @param {Object[]} datasourceConfigs - Array of datasource configurations
360
- * @returns {Promise<Object>} Object with review decision and optionally edited configs
361
- */
362
- async function promptForConfigReview(systemConfig, datasourceConfigs) {
363
- // eslint-disable-next-line no-console
364
- console.log('\n📋 Generated Configuration:');
365
- // eslint-disable-next-line no-console
366
- console.log('\nSystem Configuration:');
367
- // eslint-disable-next-line no-console
368
- console.log(JSON.stringify(systemConfig, null, 2));
369
- // eslint-disable-next-line no-console
370
- console.log('\nDatasource Configurations:');
371
- datasourceConfigs.forEach((ds, index) => {
372
- // eslint-disable-next-line no-console
373
- console.log(`\nDatasource ${index + 1}:`);
374
- // eslint-disable-next-line no-console
375
- console.log(JSON.stringify(ds, null, 2));
376
- });
377
-
378
- const { action } = await inquirer.prompt([
379
- {
380
- type: 'list',
381
- name: 'action',
382
- message: 'What would you like to do?',
383
- choices: [
384
- { name: 'Accept and save', value: 'accept' },
385
- { name: 'Cancel', value: 'cancel' }
386
- ]
387
- }
388
- ]);
389
-
390
- if (action === 'cancel') {
391
- return { action: 'cancel' };
392
- }
393
-
394
- return { action: 'accept' };
395
- }
396
-
397
391
  /**
398
392
  * Prompt for application name
399
393
  * @async
@@ -440,19 +434,24 @@ async function promptForRunWithSavedConfig() {
440
434
  return run;
441
435
  }
442
436
 
437
+ const secondary = require('./wizard-prompts-secondary');
438
+
443
439
  module.exports = {
444
440
  promptForMode,
445
441
  promptForSystemIdOrKey,
442
+ promptForExistingSystem,
446
443
  promptForSourceType,
447
444
  promptForOpenApiFile,
448
445
  promptForOpenApiUrl,
449
446
  promptForMcpServer,
450
447
  promptForCredentialAction,
451
- promptForCredentialIdOrKeyRetry,
452
- promptForKnownPlatform,
448
+ promptForExistingCredential,
449
+ promptForCredentialIdOrKeyRetry: secondary.promptForCredentialIdOrKeyRetry,
450
+ promptForKnownPlatform: secondary.promptForKnownPlatform,
451
+ promptForEntitySelection: secondary.promptForEntitySelection,
453
452
  promptForUserIntent,
454
453
  promptForUserPreferences,
455
- promptForConfigReview,
454
+ promptForConfigReview: secondary.promptForConfigReview,
456
455
  promptForAppName,
457
456
  promptForRunWithSavedConfig
458
457
  };
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Wizard README generation - used by wizard file generator
3
+ * @fileoverview Generates README.md for external system wizard output
4
+ * @author AI Fabrix Team
5
+ * @version 2.0.0
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const fs = require('fs').promises;
11
+ const path = require('path');
12
+ const chalk = require('chalk');
13
+ const logger = require('../utils/logger');
14
+ const { generateExternalReadmeContent } = require('../utils/external-readme');
15
+
16
+ const FORMAT_EXT = { yaml: '.yaml', json: '.json' };
17
+
18
+ /**
19
+ * Converts a string to a schema-valid key segment (lowercase letters, numbers, hyphens only).
20
+ * @param {string} str - Raw entity type or key segment (may be camelCase)
21
+ * @returns {string} Segment matching ^[a-z0-9-]+$
22
+ */
23
+ function toKeySegment(str) {
24
+ if (!str || typeof str !== 'string') return 'default';
25
+ const withHyphens = str.replace(/([A-Z])/g, '-$1').toLowerCase();
26
+ const sanitized = withHyphens.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
27
+ return sanitized || 'default';
28
+ }
29
+
30
+ /**
31
+ * Generate README.md with basic documentation
32
+ * @async
33
+ * @function generateReadme
34
+ * @param {Object} options - Options for README generation
35
+ * @param {string} options.appPath - Application directory path
36
+ * @param {string} options.appName - Application name
37
+ * @param {string} options.systemKey - System key
38
+ * @param {Object} options.systemConfig - System configuration
39
+ * @param {Object[]} options.datasourceConfigs - Array of datasource configurations
40
+ * @param {string} [options.aiGeneratedContent] - Optional AI-generated README content from dataplane
41
+ * @param {string} [options.format] - Output format: 'yaml' or 'json'
42
+ * @throws {Error} If generation fails
43
+ */
44
+ async function generateReadme(options) {
45
+ const { appPath, appName, systemKey, systemConfig, datasourceConfigs, aiGeneratedContent, format } = options;
46
+ try {
47
+ const readmePath = path.join(appPath, 'README.md');
48
+
49
+ if (aiGeneratedContent) {
50
+ await fs.writeFile(readmePath, aiGeneratedContent, 'utf8');
51
+ logger.log(chalk.green('✓ Generated README.md (AI-generated from dataplane)'));
52
+ return;
53
+ }
54
+
55
+ const ext = FORMAT_EXT[format === 'json' ? 'json' : 'yaml'] || '.yaml';
56
+ const datasources = (Array.isArray(datasourceConfigs) ? datasourceConfigs : []).map((ds, index) => {
57
+ const entityType = ds.entityType || ds.entityKey || ds.key?.split('-').pop() || `datasource${index + 1}`;
58
+ const keySegment = toKeySegment(entityType);
59
+ const datasourceKey = ds.key || `${systemKey}-${keySegment}`;
60
+ const datasourceKeyOnly = datasourceKey.includes('-') && datasourceKey.startsWith(`${systemKey}-`)
61
+ ? datasourceKey.substring(systemKey.length + 1)
62
+ : keySegment;
63
+ return {
64
+ key: datasourceKey,
65
+ entityType,
66
+ displayName: ds.displayName || ds.name || ds.key || `Datasource ${index + 1}`,
67
+ fileName: `${systemKey}-datasource-${datasourceKeyOnly}${ext}`
68
+ };
69
+ });
70
+
71
+ const readmeContent = generateExternalReadmeContent({
72
+ appName,
73
+ systemKey,
74
+ systemType: systemConfig.type || systemConfig.systemType,
75
+ displayName: systemConfig.displayName,
76
+ description: systemConfig.description,
77
+ fileExt: ext,
78
+ datasources
79
+ });
80
+
81
+ await fs.writeFile(readmePath, readmeContent, 'utf8');
82
+ logger.log(chalk.green('✓ Generated README.md (template)'));
83
+ } catch (error) {
84
+ throw new Error(`Failed to generate README.md: ${error.message}`);
85
+ }
86
+ }
87
+
88
+ module.exports = { generateReadme };