@aifabrix/builder 2.41.0 → 2.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +1 -1
  3. package/integration/hubspot/README.md +8 -4
  4. package/integration/hubspot/application.json +54 -0
  5. package/integration/hubspot/create-hubspot.js +9 -136
  6. package/integration/hubspot/env.template +3 -4
  7. package/integration/hubspot/hubspot-datasource-company.json +343 -5
  8. package/integration/hubspot/hubspot-datasource-contact.json +413 -5
  9. package/integration/hubspot/hubspot-datasource-deal.json +341 -4
  10. package/integration/hubspot/hubspot-datasource-users.json +116 -0
  11. package/integration/hubspot/hubspot-deploy.json +1250 -108
  12. package/integration/hubspot/hubspot-system.json +15 -32
  13. package/integration/hubspot/test-dataplane-down-tests.js +17 -16
  14. package/integration/hubspot/test-dataplane-down.js +2 -2
  15. package/jest.config.manual.js +2 -1
  16. package/lib/api/external-test.api.js +111 -0
  17. package/lib/api/index.js +42 -19
  18. package/lib/api/pipeline.api.js +66 -120
  19. package/lib/api/types/pipeline.types.js +37 -0
  20. package/lib/api/wizard-platform.api.js +61 -0
  21. package/lib/api/wizard.api.js +34 -1
  22. package/lib/app/config.js +23 -11
  23. package/lib/app/index.js +3 -1
  24. package/lib/app/prompts.js +44 -29
  25. package/lib/app/readme.js +8 -3
  26. package/lib/app/run-env-compose.js +64 -1
  27. package/lib/app/run-helpers.js +1 -1
  28. package/lib/app/show-display.js +1 -1
  29. package/lib/cli/setup-app.js +42 -11
  30. package/lib/cli/setup-credential-deployment.js +31 -6
  31. package/lib/cli/setup-dev.js +27 -0
  32. package/lib/cli/setup-environment.js +12 -4
  33. package/lib/cli/setup-external-system.js +19 -4
  34. package/lib/cli/setup-infra.js +54 -14
  35. package/lib/cli/setup-utility.js +117 -21
  36. package/lib/commands/credential-env.js +162 -0
  37. package/lib/commands/credential-list.js +17 -22
  38. package/lib/commands/credential-push.js +96 -0
  39. package/lib/commands/datasource.js +77 -6
  40. package/lib/commands/dev-init.js +39 -1
  41. package/lib/commands/repair-auth-config.js +99 -0
  42. package/lib/commands/repair-datasource-keys.js +208 -0
  43. package/lib/commands/repair-datasource.js +235 -0
  44. package/lib/commands/repair-env-template.js +348 -0
  45. package/lib/commands/repair-internal.js +85 -0
  46. package/lib/commands/repair-rbac.js +158 -0
  47. package/lib/commands/repair.js +507 -0
  48. package/lib/commands/test-e2e-external.js +165 -0
  49. package/lib/commands/upload.js +71 -40
  50. package/lib/commands/wizard-core-helpers.js +226 -4
  51. package/lib/commands/wizard-core.js +67 -29
  52. package/lib/commands/wizard-dataplane.js +1 -1
  53. package/lib/commands/wizard-entity-selection.js +43 -0
  54. package/lib/commands/wizard-headless.js +44 -5
  55. package/lib/commands/wizard-helpers.js +7 -3
  56. package/lib/commands/wizard.js +86 -64
  57. package/lib/core/config.js +7 -1
  58. package/lib/core/secrets.js +33 -12
  59. package/lib/datasource/deploy.js +12 -3
  60. package/lib/datasource/test-e2e.js +219 -0
  61. package/lib/datasource/test-integration.js +154 -0
  62. package/lib/deployment/deployer.js +7 -5
  63. package/lib/external-system/download.js +182 -204
  64. package/lib/external-system/generator.js +204 -56
  65. package/lib/external-system/test-execution.js +2 -1
  66. package/lib/external-system/test-system-level.js +73 -0
  67. package/lib/external-system/test.js +51 -18
  68. package/lib/generator/external-controller-manifest.js +29 -2
  69. package/lib/generator/external-schema-utils.js +1 -1
  70. package/lib/generator/external.js +10 -3
  71. package/lib/generator/index.js +4 -1
  72. package/lib/generator/split-readme.js +1 -0
  73. package/lib/generator/split-variables.js +7 -1
  74. package/lib/generator/split.js +194 -54
  75. package/lib/generator/wizard-prompts-secondary.js +294 -0
  76. package/lib/generator/wizard-prompts.js +105 -106
  77. package/lib/generator/wizard-readme.js +88 -0
  78. package/lib/generator/wizard.js +147 -158
  79. package/lib/infrastructure/compose.js +11 -1
  80. package/lib/infrastructure/index.js +11 -3
  81. package/lib/infrastructure/services.js +22 -11
  82. package/lib/schema/application-schema.json +8 -5
  83. package/lib/schema/external-datasource.schema.json +49 -26
  84. package/lib/schema/external-system.schema.json +82 -6
  85. package/lib/schema/wizard-config.schema.json +16 -0
  86. package/lib/utils/api.js +38 -10
  87. package/lib/utils/auth-headers.js +8 -7
  88. package/lib/utils/compose-generator.js +1 -1
  89. package/lib/utils/compose-handlebars-helpers.js +11 -0
  90. package/lib/utils/config-format-preference.js +51 -0
  91. package/lib/utils/config-format.js +36 -0
  92. package/lib/utils/configuration-env-resolver.js +179 -0
  93. package/lib/utils/credential-display.js +83 -0
  94. package/lib/utils/credential-secrets-env.js +115 -25
  95. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  96. package/lib/utils/deployment-validation-helpers.js +4 -4
  97. package/lib/utils/dev-ca-install.js +139 -0
  98. package/lib/utils/env-copy.js +23 -3
  99. package/lib/utils/error-formatters/http-status-errors.js +0 -1
  100. package/lib/utils/error-formatters/permission-errors.js +0 -1
  101. package/lib/utils/error-formatters/validation-errors.js +0 -1
  102. package/lib/utils/external-readme.js +56 -29
  103. package/lib/utils/external-system-display.js +59 -1
  104. package/lib/utils/external-system-test-helpers.js +21 -8
  105. package/lib/utils/external-system-validators.js +3 -0
  106. package/lib/utils/file-upload.js +20 -50
  107. package/lib/utils/help-builder.js +1 -0
  108. package/lib/utils/infra-status.js +50 -44
  109. package/lib/utils/local-secrets.js +5 -5
  110. package/lib/utils/paths.js +85 -4
  111. package/lib/utils/secrets-canonical.js +93 -0
  112. package/lib/utils/secrets-generator.js +20 -0
  113. package/lib/utils/secrets-helpers.js +75 -89
  114. package/lib/utils/test-log-writer.js +56 -0
  115. package/lib/utils/token-manager.js +24 -32
  116. package/lib/validation/env-template-auth.js +157 -0
  117. package/lib/validation/env-template-kv.js +41 -0
  118. package/lib/validation/external-manifest-validator.js +25 -0
  119. package/lib/validation/external-system-auth-rules.js +86 -0
  120. package/lib/validation/validate-batch.js +149 -0
  121. package/lib/validation/validate-datasource-keys-api.js +33 -0
  122. package/lib/validation/validate-display.js +94 -16
  123. package/lib/validation/validate.js +25 -12
  124. package/lib/validation/validator.js +7 -9
  125. package/lib/validation/wizard-datasource-validation.js +50 -0
  126. package/package.json +7 -2
  127. package/templates/applications/dataplane/application.yaml +1 -1
  128. package/templates/applications/dataplane/env.template +5 -5
  129. package/templates/applications/dataplane/rbac.yaml +2 -2
  130. package/templates/applications/miso-controller/env.template +1 -1
  131. package/templates/external-system/README.md.hbs +65 -25
  132. package/templates/external-system/deploy.js.hbs +4 -2
  133. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  134. package/templates/external-system/external-system.json.hbs +1 -18
  135. package/templates/infra/compose.yaml.hbs +6 -0
  136. package/templates/python/docker-compose.hbs +4 -4
  137. package/templates/typescript/docker-compose.hbs +4 -4
  138. package/integration/hubspot/application.yaml +0 -37
@@ -121,5 +121,42 @@
121
121
  * @property {string} data.environment - Environment key
122
122
  */
123
123
 
124
+ /**
125
+ * Dataplane pipeline upload request body (single upload → validate → publish).
126
+ * @typedef {Object} PipelineUploadRequest
127
+ * @property {string} version - Config version (e.g. "1.0.0")
128
+ * @property {Object} application - Application/system config (external-system schema)
129
+ * @property {Object[]} dataSources - Data source configs
130
+ * @property {string} [status="draft"] - "draft" or "published"; Builder uses "draft"
131
+ */
132
+
133
+ /**
134
+ * Dataplane pipeline upload response (publication result; no uploadId).
135
+ * @typedef {Object} PipelineUploadResponse
136
+ * @property {boolean} success - Request success flag
137
+ * @property {Object} [data] - Publication result (system, datasources, warnings)
138
+ * @property {string} [data.systemKey] - Published system key
139
+ * @property {string[]} [data.datasourceKeys] - Published datasource keys
140
+ * @property {string[]} [data.warnings] - Warnings if any
141
+ * @property {string} [formattedError] - Formatted error message on failure
142
+ */
143
+
144
+ /**
145
+ * Dataplane pipeline validate request body (dry-run validation only).
146
+ * @typedef {Object} PipelineValidateConfigRequest
147
+ * @property {Object} config - Full config to validate
148
+ * @property {string} config.version - Config version
149
+ * @property {Object} config.application - Application config
150
+ * @property {Object[]} config.dataSources - Data source configs
151
+ */
152
+
153
+ /**
154
+ * Dataplane pipeline validate response.
155
+ * @typedef {Object} PipelineValidateConfigResponse
156
+ * @property {boolean} isValid - Whether config is valid
157
+ * @property {string[]} [errors] - Validation errors
158
+ * @property {string[]} [warnings] - Validation warnings
159
+ */
160
+
124
161
  module.exports = {};
125
162
 
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @fileoverview Wizard platform API - getPlatformDetails, discoverEntities
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const { ApiClient } = require('./index');
8
+
9
+ /**
10
+ * Get platform details including available datasources
11
+ * GET /api/v1/wizard/platforms/{platformKey}
12
+ * @requiresPermission {Dataplane} external-system:read
13
+ * @async
14
+ * @function getPlatformDetails
15
+ * @param {string} dataplaneUrl - Dataplane base URL
16
+ * @param {Object} authConfig - Authentication configuration
17
+ * @param {string} platformKey - Platform key (e.g. 'hubspot')
18
+ * @returns {Promise<Object>} Platform details including datasources: [{ key, displayName, entity }]
19
+ * @throws {Error} If request fails or platform not found (404)
20
+ */
21
+ async function getPlatformDetails(dataplaneUrl, authConfig, platformKey) {
22
+ const client = new ApiClient(dataplaneUrl, authConfig);
23
+ const response = await client.get(`/api/v1/wizard/platforms/${encodeURIComponent(platformKey)}`);
24
+ if (!response.success) {
25
+ const msg = response.status === 404
26
+ ? `Platform '${platformKey}' not found`
27
+ : response.formattedError || response.error || 'Failed to get platform details';
28
+ const err = new Error(msg);
29
+ err.status = response.status;
30
+ throw err;
31
+ }
32
+ return response;
33
+ }
34
+
35
+ /**
36
+ * Discover entities from OpenAPI spec (for multi-entity flows)
37
+ * POST /api/v1/wizard/discover-entities
38
+ * @requiresPermission {Dataplane} external-system:create
39
+ * @async
40
+ * @function discoverEntities
41
+ * @param {string} dataplaneUrl - Dataplane base URL
42
+ * @param {Object} authConfig - Authentication configuration
43
+ * @param {Object} openapiSpec - OpenAPI specification object
44
+ * @returns {Promise<Object>} Response with entities: [{ name, pathCount, schemaMatch }]
45
+ * @throws {Error} If request fails
46
+ */
47
+ async function discoverEntities(dataplaneUrl, authConfig, openapiSpec) {
48
+ const client = new ApiClient(dataplaneUrl, authConfig);
49
+ const response = await client.post('/api/v1/wizard/discover-entities', {
50
+ body: { openapiSpec }
51
+ });
52
+ if (!response.success) {
53
+ const msg = response.formattedError || response.error || 'Failed to discover entities';
54
+ const err = new Error(msg);
55
+ err.status = response.status;
56
+ throw err;
57
+ }
58
+ return response;
59
+ }
60
+
61
+ module.exports = { getPlatformDetails, discoverEntities };
@@ -6,6 +6,7 @@
6
6
 
7
7
  const { ApiClient } = require('./index');
8
8
  const { uploadFile } = require('../utils/file-upload');
9
+ const { getPlatformDetails, discoverEntities } = require('./wizard-platform.api');
9
10
 
10
11
  /**
11
12
  * Create wizard session
@@ -178,8 +179,36 @@ async function detectType(dataplaneUrl, authConfig, openapiSpec) {
178
179
  }
179
180
 
180
181
  /**
181
- * Generate configuration via AI
182
+ * Get platform configuration for a known platform (no OpenAPI parsing)
183
+ * POST /api/v1/wizard/platforms/{platformKey}/config
184
+ * Use this when sourceType=known-platform; do NOT use generate-config which requires openapiSpec.
185
+ * @requiresPermission {Dataplane} external-system:create
186
+ * @async
187
+ * @function getPlatformConfig
188
+ * @param {string} dataplaneUrl - Dataplane base URL
189
+ * @param {Object} authConfig - Authentication configuration
190
+ * @param {string} platformKey - Platform key (e.g. 'hubspot')
191
+ * @param {Object} config - Configuration payload (no openapiSpec)
192
+ * @param {string} config.mode - Wizard mode ('create-system' | 'add-datasource')
193
+ * @param {string} [config.systemIdOrKey] - Existing system ID/key (required for add-datasource)
194
+ * @param {string} [config.credentialIdOrKey] - Credential ID or key
195
+ * @param {string} [config.intent] - User intent
196
+ * @param {string} [config.fieldOnboardingLevel] - Field onboarding level ('full' | 'standard' | 'minimal')
197
+ * @param {Object} [config.userPreferences] - User preferences
198
+ * @returns {Promise<Object>} Generated configuration response
199
+ * @throws {Error} If request fails
200
+ */
201
+ async function getPlatformConfig(dataplaneUrl, authConfig, platformKey, config) {
202
+ const client = new ApiClient(dataplaneUrl, authConfig);
203
+ return await client.post(`/api/v1/wizard/platforms/${encodeURIComponent(platformKey)}/config`, {
204
+ body: config
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Generate configuration via AI (OpenAPI-based)
182
210
  * POST /api/v1/wizard/generate-config
211
+ * Do NOT use for sourceType=known-platform; use getPlatformConfig instead.
183
212
  * @requiresPermission {Dataplane} external-system:create
184
213
  * @async
185
214
  * @function generateConfig
@@ -198,6 +227,7 @@ async function detectType(dataplaneUrl, authConfig, openapiSpec) {
198
227
  * @param {boolean} [config.userPreferences.enableMCP] - Enable MCP
199
228
  * @param {boolean} [config.userPreferences.enableABAC] - Enable ABAC
200
229
  * @param {boolean} [config.userPreferences.enableRBAC] - Enable RBAC
230
+ * @param {string} [config.entityName] - Entity for multi-entity OpenAPI (from discover-entities)
201
231
  * @returns {Promise<Object>} Generated configuration response
202
232
  * @throws {Error} If request fails
203
233
  */
@@ -417,6 +447,9 @@ module.exports = {
417
447
  parseOpenApi,
418
448
  credentialSelection,
419
449
  detectType,
450
+ getPlatformDetails,
451
+ discoverEntities,
452
+ getPlatformConfig,
420
453
  generateConfig,
421
454
  generateConfigStream,
422
455
  validateWizardConfig,
package/lib/app/config.js CHANGED
@@ -15,6 +15,7 @@ const { generateVariablesYaml, generateEnvTemplate, generateRbacYaml } = require
15
15
  const { generateEnvTemplate: generateEnvTemplateFromReader } = require('../core/env-reader');
16
16
  const { generateReadmeMdFile } = require('./readme');
17
17
  const logger = require('../utils/logger');
18
+ const { systemKeyToKvPrefix } = require('../utils/credential-secrets-env');
18
19
 
19
20
  /**
20
21
  * Checks if a file exists
@@ -73,7 +74,8 @@ async function generateVariablesYamlFile(appPath, appName, config) {
73
74
  }
74
75
 
75
76
  /**
76
- * Generates env.template content for external systems based on authentication type
77
+ * Generates env.template content for external systems based on authentication type.
78
+ * Uses KV_<APPKEY>_<VAR> convention (e.g. KV_HUBSPOT_CLIENTID) for credential push.
77
79
  * @param {Object} config - Application configuration with authType and systemKey
78
80
  * @param {string} appName - Application name (used as fallback for systemKey)
79
81
  * @returns {string} Environment template content
@@ -81,23 +83,33 @@ async function generateVariablesYamlFile(appPath, appName, config) {
81
83
  function generateExternalSystemEnvTemplate(config, appName) {
82
84
  const systemKey = config.systemKey || appName;
83
85
  const authType = config.authType || 'apikey';
86
+ const prefix = systemKeyToKvPrefix(systemKey);
87
+ if (!prefix) return '';
88
+
84
89
  const lines = [
85
- `# ${systemKey} ${authType.toUpperCase()} Configuration`,
86
- '# These values are set via the Miso Controller interface or Dataplane portal',
87
- '# Values are stored in Key Vault automatically by the platform',
90
+ `# ${systemKey} ${String(authType).toUpperCase()} Configuration`,
91
+ '# Use KV_* variables for credential push (aifabrix credential push).',
92
+ '# Values are stored in Key Vault automatically by the platform.',
88
93
  ''
89
94
  ];
90
95
 
91
- if (authType === 'oauth2') {
92
- lines.push('CLIENTID=kv://' + systemKey + '-clientidKeyVault');
93
- lines.push('CLIENTSECRET=kv://' + systemKey + '-clientsecretKeyVault');
94
- lines.push('TOKENURL=https://api.example.com/oauth/token');
96
+ if (authType === 'oauth2' || authType === 'aad') {
97
+ lines.push(`KV_${prefix}_CLIENTID=`);
98
+ lines.push(`KV_${prefix}_CLIENTSECRET=`);
99
+ lines.push('TOKEN_URL=https://api.example.com/oauth/token');
95
100
  } else if (authType === 'apikey') {
96
- lines.push('API_KEY=kv://' + systemKey + '-api-keyKeyVault');
101
+ lines.push(`KV_${prefix}_APIKEY=`);
97
102
  } else if (authType === 'basic') {
98
- lines.push('USERNAME=kv://' + systemKey + '-usernameKeyVault');
99
- lines.push('PASSWORD=kv://' + systemKey + '-passwordKeyVault');
103
+ lines.push(`KV_${prefix}_USERNAME=`);
104
+ lines.push(`KV_${prefix}_PASSWORD=`);
105
+ } else if (authType === 'queryParam') {
106
+ lines.push(`KV_${prefix}_PARAMVALUE=`);
107
+ } else if (authType === 'oidc') {
108
+ lines.push('# OIDC: variables only (openIdConfigUrl, clientId); no security keys');
109
+ } else if (authType === 'hmac') {
110
+ lines.push(`KV_${prefix}_SIGNINGSECRET=`);
100
111
  }
112
+ // none: no security keys
101
113
 
102
114
  return lines.join('\n');
103
115
  }
package/lib/app/index.js CHANGED
@@ -141,8 +141,10 @@ async function generateApplicationFiles(finalAppPath, appName, config, options)
141
141
 
142
142
  // Generate external system files if type is external
143
143
  if (config.type === 'external') {
144
+ const configModule = require('../core/config');
145
+ const format = (await configModule.getFormat()) || 'yaml';
144
146
  const externalGenerator = require('../external-system/generator');
145
- await externalGenerator.generateExternalSystemFiles(finalAppPath, appName, config);
147
+ await externalGenerator.generateExternalSystemFiles(finalAppPath, appName, config, format);
146
148
  }
147
149
 
148
150
  if (options.app) {
@@ -176,8 +176,13 @@ function buildExternalSystemTypeQuestions(options) {
176
176
  message: 'What authentication type does the system use?',
177
177
  choices: [
178
178
  { name: 'OAuth2', value: 'oauth2' },
179
+ { name: 'Azure AD', value: 'aad' },
179
180
  { name: 'API Key', value: 'apikey' },
180
- { name: 'Basic Auth', value: 'basic' }
181
+ { name: 'Basic Auth', value: 'basic' },
182
+ { name: 'Query Parameter', value: 'queryParam' },
183
+ { name: 'OpenID Connect', value: 'oidc' },
184
+ { name: 'HMAC Signature', value: 'hmac' },
185
+ { name: 'None', value: 'none' }
181
186
  ],
182
187
  default: 'apikey'
183
188
  });
@@ -185,6 +190,28 @@ function buildExternalSystemTypeQuestions(options) {
185
190
  return questions;
186
191
  }
187
192
 
193
+ /**
194
+ * Build entityType question for external system datasources
195
+ * @param {Object} options - Provided options
196
+ * @returns {Array} Array of question objects
197
+ */
198
+ function buildEntityTypeQuestion(options) {
199
+ if (options.entityType) return [];
200
+ return [{
201
+ type: 'list',
202
+ name: 'entityType',
203
+ message: 'What entity type do the datasources represent?',
204
+ choices: [
205
+ { name: 'Record storage (CRM, deals, contacts)', value: 'recordStorage' },
206
+ { name: 'Document storage (with vector)', value: 'documentStorage' },
207
+ { name: 'Vector store', value: 'vectorStore' },
208
+ { name: 'Message service', value: 'messageService' },
209
+ { name: 'None', value: 'none' }
210
+ ],
211
+ default: 'recordStorage'
212
+ }];
213
+ }
214
+
188
215
  function buildExternalSystemDatasourceQuestion(options) {
189
216
  if (options.datasourceCount) return [];
190
217
  return [{
@@ -210,6 +237,7 @@ function buildExternalSystemQuestions(options, appName) {
210
237
  return [
211
238
  ...buildExternalSystemIdentityQuestions(options, appName),
212
239
  ...buildExternalSystemTypeQuestions(options),
240
+ ...buildEntityTypeQuestion(options),
213
241
  ...buildExternalSystemDatasourceQuestion(options)
214
242
  ];
215
243
  }
@@ -325,6 +353,16 @@ function resolveExternalSystemField(options, answers, fieldName, defaultValue) {
325
353
  return null;
326
354
  }
327
355
 
356
+ const EXTERNAL_SYSTEM_FIELD_SPECS = [
357
+ { key: 'systemKey', default: undefined },
358
+ { key: 'systemDisplayName', default: undefined },
359
+ { key: 'systemDescription', default: undefined },
360
+ { key: 'systemType', default: 'openapi' },
361
+ { key: 'authType', default: 'apikey' },
362
+ { key: 'entityType', default: 'recordStorage' },
363
+ { key: 'datasourceCount', default: 1, transform: (v) => parseInt(v, 10) }
364
+ ];
365
+
328
366
  /**
329
367
  * Resolve external system fields and add to config
330
368
  * @function resolveExternalSystemFields
@@ -334,34 +372,11 @@ function resolveExternalSystemField(options, answers, fieldName, defaultValue) {
334
372
  * @returns {void}
335
373
  */
336
374
  function resolveExternalSystemFields(options, answers, config) {
337
- const systemKey = resolveExternalSystemField(options, answers, 'systemKey', undefined);
338
- if (systemKey !== null) {
339
- config.systemKey = systemKey;
340
- }
341
-
342
- const systemDisplayName = resolveExternalSystemField(options, answers, 'systemDisplayName', undefined);
343
- if (systemDisplayName !== null) {
344
- config.systemDisplayName = systemDisplayName;
345
- }
346
-
347
- const systemDescription = resolveExternalSystemField(options, answers, 'systemDescription', undefined);
348
- if (systemDescription !== null) {
349
- config.systemDescription = systemDescription;
350
- }
351
-
352
- const systemType = resolveExternalSystemField(options, answers, 'systemType', 'openapi');
353
- if (systemType !== null) {
354
- config.systemType = systemType;
355
- }
356
-
357
- const authType = resolveExternalSystemField(options, answers, 'authType', 'apikey');
358
- if (authType !== null) {
359
- config.authType = authType;
360
- }
361
-
362
- const datasourceCount = resolveExternalSystemField(options, answers, 'datasourceCount', 1);
363
- if (datasourceCount !== null) {
364
- config.datasourceCount = parseInt(datasourceCount, 10);
375
+ for (const { key, default: defaultValue, transform } of EXTERNAL_SYSTEM_FIELD_SPECS) {
376
+ const value = resolveExternalSystemField(options, answers, key, defaultValue);
377
+ if (value !== null) {
378
+ config[key] = transform ? transform(value) : value;
379
+ }
365
380
  }
366
381
  }
367
382
 
package/lib/app/readme.js CHANGED
@@ -92,20 +92,23 @@ function extractServiceFlags(config) {
92
92
  /**
93
93
  * Builds placeholder datasources for external README generation
94
94
  * @function buildExternalDatasourcePlaceholders
95
+ * @param {string} systemKey - System key
95
96
  * @param {number} datasourceCount - Datasource count
97
+ * @param {string} [fileExt='.json'] - File extension (e.g. '.json', '.yaml')
96
98
  * @returns {Array<Object>} Datasource placeholders
97
99
  */
98
- function buildExternalDatasourcePlaceholders(systemKey, datasourceCount) {
100
+ function buildExternalDatasourcePlaceholders(systemKey, datasourceCount, fileExt = '.json') {
99
101
  const normalizedCount = Number.isInteger(datasourceCount)
100
102
  ? datasourceCount
101
103
  : parseInt(datasourceCount, 10);
102
104
  const total = Number.isFinite(normalizedCount) && normalizedCount > 0 ? normalizedCount : 0;
105
+ const ext = fileExt && fileExt.startsWith('.') ? fileExt : `.${fileExt || 'json'}`;
103
106
  return Array.from({ length: total }, (_value, index) => {
104
107
  const entityType = `entity${index + 1}`;
105
108
  return {
106
109
  entityType,
107
110
  displayName: `Datasource ${index + 1}`,
108
- fileName: `${systemKey}-datasource-${entityType}.yaml`
111
+ fileName: `${systemKey}-datasource-${entityType}${ext}`
109
112
  };
110
113
  });
111
114
  }
@@ -143,15 +146,17 @@ function buildReadmeContext(appName, config) {
143
146
  function generateReadmeMd(appName, config) {
144
147
  if (config.type === 'external') {
145
148
  const systemKey = config.systemKey || appName;
149
+ const fileExt = config.fileExt !== undefined ? config.fileExt : '.json';
146
150
  const datasources = Array.isArray(config.datasources) && config.datasources.length > 0
147
151
  ? config.datasources
148
- : buildExternalDatasourcePlaceholders(systemKey, config.datasourceCount);
152
+ : buildExternalDatasourcePlaceholders(systemKey, config.datasourceCount, fileExt);
149
153
  return generateExternalReadmeContent({
150
154
  appName,
151
155
  systemKey,
152
156
  systemType: config.systemType,
153
157
  displayName: config.systemDisplayName,
154
158
  description: config.systemDescription,
159
+ fileExt: config.fileExt,
155
160
  datasources
156
161
  });
157
162
  }
@@ -16,6 +16,7 @@ const pathsUtil = require('../utils/paths');
16
16
  const adminSecrets = require('../core/admin-secrets');
17
17
  const secretsEnvWrite = require('../core/secrets-env-write');
18
18
  const { getContainerPort } = require('../utils/port-resolver');
19
+ const { getInfraDirName } = require('../infrastructure/helpers');
19
20
 
20
21
  /**
21
22
  * Clean applications directory: remove generated docker-compose.yaml and .env.* files.
@@ -145,16 +146,74 @@ function buildDbInitOnlyEnv(merged) {
145
146
  return dbInit;
146
147
  }
147
148
 
149
+ /**
150
+ * Return pgpass paths under infra-dev* directories in aifabrix home (for fallback lookup).
151
+ * @param {string} aifabrixDir - Aifabrix home directory
152
+ * @returns {string[]} Paths to pgpass files
153
+ */
154
+ function getInfraDevPgpassPaths(aifabrixDir) {
155
+ if (!fsSync.existsSync(aifabrixDir)) return [];
156
+ let entries;
157
+ try {
158
+ entries = fsSync.readdirSync(aifabrixDir).sort();
159
+ } catch {
160
+ return [];
161
+ }
162
+ return entries
163
+ .filter((name) => name.startsWith('infra-dev'))
164
+ .map((name) => path.join(aifabrixDir, name, 'pgpass'));
165
+ }
166
+
167
+ /**
168
+ * Read first password from a pgpass file (format host:port:db:user:password).
169
+ * @param {string} pgpassPath - Path to pgpass file
170
+ * @returns {Promise<string|undefined>} Password or undefined
171
+ */
172
+ async function readPasswordFromPgpassFile(pgpassPath) {
173
+ const content = await fs.readFile(pgpassPath, 'utf8');
174
+ const line = content.split('\n')[0];
175
+ if (!line) return undefined;
176
+ const parts = line.split(':');
177
+ return parts.length >= 5 ? parts[4].trim() : undefined;
178
+ }
179
+
180
+ /**
181
+ * Read POSTGRES_PASSWORD from an existing infra pgpass so db-init uses the same password as running Postgres.
182
+ * Tries dev-specific, then default infra, then any infra-dev* dir (e.g. dev 1 run when only infra-dev06 has pgpass).
183
+ * @param {number|string} developerId - Developer ID
184
+ * @returns {Promise<string|undefined>} Password or undefined
185
+ */
186
+ async function readPostgresPasswordFromPgpass(developerId) {
187
+ const home = pathsUtil.getAifabrixHome();
188
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
189
+ const candidates = [path.join(home, getInfraDirName(developerId), 'pgpass')];
190
+ if (idNum !== 0) candidates.push(path.join(home, getInfraDirName(0), 'pgpass'));
191
+ const extra = getInfraDevPgpassPaths(home).filter((p) => !candidates.includes(p));
192
+ candidates.push(...extra);
193
+ for (const pgpassPath of candidates) {
194
+ if (!fsSync.existsSync(pgpassPath)) continue;
195
+ try {
196
+ const pwd = await readPasswordFromPgpassFile(pgpassPath);
197
+ if (pwd !== undefined) return pwd;
198
+ } catch {
199
+ // ignore
200
+ }
201
+ }
202
+ return undefined;
203
+ }
204
+
148
205
  /**
149
206
  * Build two run env files: .env.run (app-only, no admin secrets) and .env.run.admin (start-only, for db-init).
150
207
  * Admin password is never set in the app container; .env.run.admin is used only for start and then deleted.
208
+ * When an infra pgpass exists, POSTGRES_PASSWORD is taken from it so db-init matches the running Postgres.
151
209
  * @async
152
210
  * @param {string} appName - Application name
153
211
  * @param {Object} appConfig - Application configuration
154
212
  * @param {string} devDir - Applications directory path
213
+ * @param {number|string} [developerId] - Developer ID (for pgpass lookup)
155
214
  * @returns {Promise<{ runEnvPath: string, runEnvAdminPath: string }>} Paths to .env.run and .env.run.admin
156
215
  */
157
- async function buildMergedRunEnvAndWrite(appName, appConfig, devDir) {
216
+ async function buildMergedRunEnvAndWrite(appName, appConfig, devDir, developerId) {
158
217
  const infra = require('../infrastructure');
159
218
  const ensureAdminSecretsFn = typeof infra.ensureAdminSecrets === 'function'
160
219
  ? infra.ensureAdminSecrets
@@ -167,6 +226,10 @@ async function buildMergedRunEnvAndWrite(appName, appConfig, devDir) {
167
226
  force: false
168
227
  });
169
228
  const merged = { ...adminObj, ...appObj };
229
+ if (developerId !== undefined) {
230
+ const pgpassPwd = await readPostgresPasswordFromPgpass(developerId);
231
+ if (pgpassPwd !== undefined) merged.POSTGRES_PASSWORD = pgpassPwd;
232
+ }
170
233
  injectDatabaseNamesAndUsers(merged, appConfig);
171
234
  injectContainerPortForRun(merged, appConfig, appName);
172
235
 
@@ -337,7 +337,7 @@ async function prepareEnvironment(appName, appConfig, options) {
337
337
 
338
338
  runEnvCompose.cleanApplicationsDir(developerId);
339
339
  logger.log(chalk.blue('Building merged .env (admin + app secrets)...'));
340
- const { runEnvPath, runEnvAdminPath } = await runEnvCompose.buildMergedRunEnvAndWrite(appName, appConfig, devDir);
340
+ const { runEnvPath, runEnvAdminPath } = await runEnvCompose.buildMergedRunEnvAndWrite(appName, appConfig, devDir, developerId);
341
341
 
342
342
  const composeOptions = {
343
343
  ...options,
@@ -161,7 +161,7 @@ function logExternalSystemMain(ext) {
161
161
  logger.log(` Credential: ${ext.credentialId ?? '—'}`);
162
162
  logger.log(` Status: ${ext.status ?? '—'}`);
163
163
  logger.log(` API docs: ${ext.openApiDocsPageUrl ?? ext.apiDocumentUrl ?? '—'}`);
164
- logger.log(` MCP server: ${ext.mcpServerUrl ?? '—'}`);
164
+ logger.log(` MCP server: ${ext.mcpServerUrl ?? '—'}`);
165
165
  logger.log(` OpenAPI spec: ${ext.apiDocumentUrl ?? '—'}`);
166
166
  }
167
167
 
@@ -17,12 +17,20 @@ const { handleCommandError } = require('../utils/cli-utils');
17
17
  * @param {Object} options - Raw CLI options
18
18
  * @returns {Object} Normalized options
19
19
  */
20
+ const VALID_ENTITY_TYPES = ['recordStorage', 'documentStorage', 'vectorStore', 'messageService', 'none'];
21
+
20
22
  function normalizeExternalOptions(options) {
21
23
  const normalized = { ...options };
22
24
  if (options.displayName) normalized.systemDisplayName = options.displayName;
23
25
  if (options.description) normalized.systemDescription = options.description;
24
26
  if (options.systemType) normalized.systemType = options.systemType;
25
27
  if (options.authType) normalized.authType = options.authType;
28
+ if (options.entityType) {
29
+ if (!VALID_ENTITY_TYPES.includes(options.entityType)) {
30
+ throw new Error(`Invalid --entity-type. Must be one of: ${VALID_ENTITY_TYPES.join(', ')}`);
31
+ }
32
+ normalized.entityType = options.entityType;
33
+ }
26
34
  if (options.datasources !== undefined) {
27
35
  const parsedCount = parseInt(options.datasources, 10);
28
36
  if (Number.isNaN(parsedCount) || parsedCount < 1 || parsedCount > 10) {
@@ -48,6 +56,7 @@ function validateNonInteractiveExternalOptions(normalizedOptions) {
48
56
  if (!normalizedOptions.systemDescription) missing.push('--description');
49
57
  if (!normalizedOptions.systemType) missing.push('--system-type');
50
58
  if (!normalizedOptions.authType) missing.push('--auth-type');
59
+ if (!normalizedOptions.entityType) missing.push('--entity-type');
51
60
  if (!normalizedOptions.datasourceCount) missing.push('--datasources');
52
61
  if (missing.length > 0) {
53
62
  throw new Error(`Missing required options for non-interactive external create: ${missing.join(', ')}`);
@@ -110,7 +119,8 @@ function setupCreateCommand(program) {
110
119
  .option('--display-name <name>', 'External system display name')
111
120
  .option('--description <desc>', 'External system description')
112
121
  .option('--system-type <type>', 'External system type (openapi, mcp, custom)')
113
- .option('--auth-type <type>', 'External system auth type (oauth2, apikey, basic)')
122
+ .option('--auth-type <type>', 'External system auth type (oauth2, aad, apikey, basic, queryParam, oidc, hmac, none)')
123
+ .option('--entity-type <type>', 'Entity type for datasources (recordStorage, documentStorage, vectorStore, messageService, none)')
114
124
  .option('--datasources <count>', 'Number of datasources to create')
115
125
  .action(async(appName, options) => {
116
126
  try {
@@ -130,6 +140,7 @@ Examples:
130
140
  $ aifabrix wizard my-integration --silent Run headless with integration/my-integration/wizard.yaml (no prompts)
131
141
  $ aifabrix wizard -a my-integration Same as above (app name set)
132
142
  $ aifabrix wizard --config wizard.yaml Run headless from a wizard config file
143
+ $ aifabrix wizard hubspot-test-v2 --debug Enable debug output and save debug manifests on validation failure
133
144
 
134
145
  Config path: When appName is provided, integration/<appName>/wizard.yaml is used for load/save and error.log.
135
146
  To change settings after a run, edit that file and run "aifabrix wizard <app>" again.
@@ -140,6 +151,7 @@ See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`;
140
151
  .option('-a, --app <app>', 'Application name (synonym for positional appName)')
141
152
  .option('--config <file>', 'Run headless using a wizard.yaml file (appName, mode, source, credential, preferences)')
142
153
  .option('--silent', 'Run with saved integration/<app>/wizard.yaml only; no prompts (requires app name and existing wizard.yaml)')
154
+ .option('--debug', 'Enable debug output and save debug manifests on validation failure')
143
155
  .addHelpText('after', wizardHelp)
144
156
  .action(async(positionalAppName, options) => {
145
157
  try {
@@ -280,6 +292,29 @@ function setupShellTestStopCommands(program) {
280
292
  });
281
293
  }
282
294
 
295
+ async function runTestE2ECommand(appName, options) {
296
+ const pathsUtil = require('../utils/paths');
297
+ const appType = await pathsUtil.detectAppType(appName).catch(() => null);
298
+ if (appType && appType.baseDir === 'integration') {
299
+ const { runTestE2EForExternalSystem } = require('../commands/test-e2e-external');
300
+ const { success, results } = await runTestE2EForExternalSystem(appName, {
301
+ env: options.env,
302
+ debug: options.debug,
303
+ verbose: options.verbose,
304
+ async: options.async !== false
305
+ });
306
+ results.forEach(r => {
307
+ const icon = r.success ? chalk.green('✓') : chalk.red('✗');
308
+ const msg = r.error ? `${r.key}: ${r.error}` : r.key;
309
+ logger.log(` ${icon} ${msg}`);
310
+ });
311
+ if (!success) process.exit(1);
312
+ return;
313
+ }
314
+ const { runAppTestE2e } = require('../commands/app-test');
315
+ await runAppTestE2e(appName, { env: options.env });
316
+ }
317
+
283
318
  function setupInstallTestE2eLintCommands(program) {
284
319
  program.command('install <app>')
285
320
  .description('Install dependencies in container (builder apps only)')
@@ -301,18 +336,14 @@ function setupInstallTestE2eLintCommands(program) {
301
336
  });
302
337
 
303
338
  program.command('test-e2e <app>')
304
- .description('Run e2e tests in container (builder apps only)')
305
- .option('--env <env>', 'dev (running container) or tst (ephemeral with .env)', 'dev')
339
+ .description('Run e2e tests (builder: in container; external system: all datasources via dataplane)')
340
+ .option('-e, --env <env>', 'Environment: dev, tst, or pro (builder: dev/tst for container)')
341
+ .option('-v, --verbose', 'Show detailed step output and poll progress')
342
+ .option('--debug', 'Include debug output and write log to integration/<app>/logs/')
343
+ .option('--no-async', 'Use sync mode (no polling); single POST per datasource')
306
344
  .action(async(appName, options) => {
307
345
  try {
308
- const pathsUtil = require('../utils/paths');
309
- const appType = await pathsUtil.detectAppType(appName).catch(() => null);
310
- if (appType && appType.baseDir === 'integration') {
311
- logger.log(chalk.gray('test-e2e is for builder applications only. Use aifabrix shell <app> then make test:e2e or pnpm test:e2e.'));
312
- return;
313
- }
314
- const { runAppTestE2e } = require('../commands/app-test');
315
- await runAppTestE2e(appName, { env: options.env });
346
+ await runTestE2ECommand(appName, options);
316
347
  } catch (error) {
317
348
  handleCommandError(error, 'test-e2e');
318
349
  process.exit(1);