@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
@@ -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
@@ -190,14 +219,16 @@ async function detectType(dataplaneUrl, authConfig, openapiSpec) {
190
219
  * @param {string} config.detectedType - Detected API type (required, e.g., 'record-based')
191
220
  * @param {string} config.intent - User intent (required, any descriptive text)
192
221
  * @param {string} config.mode - Wizard mode (required, 'create-system' | 'add-datasource')
193
- * @param {string} [config.systemIdOrKey] - Existing system ID/key (required for add-datasource)
222
+ * @param {string} [config.systemIdOrKey] - Existing system ID/key (required for add-datasource; must be application/system key, not entity/datasource key)
194
223
  * @param {string} [config.credentialIdOrKey] - Credential ID or key
224
+ * @param {string|null} [config.systemDisplayName] - System-level display name for credential (e.g. 'Hubspot Demo'); when OpenAPI title is entity-specific, pass system name here
195
225
  * @param {string} [config.fieldOnboardingLevel] - Field onboarding level ('full' | 'standard' | 'minimal')
196
226
  * @param {boolean} [config.enableOpenAPIGeneration] - Enable OpenAPI operation generation
197
227
  * @param {Object} [config.userPreferences] - User preferences
198
228
  * @param {boolean} [config.userPreferences.enableMCP] - Enable MCP
199
229
  * @param {boolean} [config.userPreferences.enableABAC] - Enable ABAC
200
230
  * @param {boolean} [config.userPreferences.enableRBAC] - Enable RBAC
231
+ * @param {string} [config.entityName] - Entity for multi-entity OpenAPI (from discover-entities)
201
232
  * @returns {Promise<Object>} Generated configuration response
202
233
  * @throws {Error} If request fails
203
234
  */
@@ -417,6 +448,9 @@ module.exports = {
417
448
  parseOpenApi,
418
449
  credentialSelection,
419
450
  detectType,
451
+ getPlatformDetails,
452
+ discoverEntities,
453
+ getPlatformConfig,
420
454
  generateConfig,
421
455
  generateConfigStream,
422
456
  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
@@ -68,7 +68,7 @@ function validateAppNameAndSetup(appName, options) {
68
68
  throw new Error('Application name is required');
69
69
  }
70
70
 
71
- const initialType = options.type || 'webapp';
71
+ const initialType = options.type || 'external';
72
72
  const baseDir = getBaseDirForAppType(initialType);
73
73
  const appPath = getAppPath(appName, initialType);
74
74
 
@@ -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) {
@@ -177,7 +179,7 @@ async function logApplicationCreation(appName, config, options) {
177
179
  async function createApp(appName, options = {}) {
178
180
  try {
179
181
  const { appPath } = validateAppNameAndSetup(appName, options);
180
- await validateAppCreation(appName, options, appPath, getBaseDirForAppType(options.type || 'webapp'));
182
+ await validateAppCreation(appName, options, appPath, getBaseDirForAppType(options.type || 'external'));
181
183
 
182
184
  const mergedOptions = await handleTemplateSetup(options);
183
185
  const config = await promptForOptions(appName, mergedOptions);
@@ -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
 
@@ -411,8 +426,8 @@ function mergePromptAnswers(appName, options, answers) {
411
426
  * @returns {Promise<Object>} Complete configuration
412
427
  */
413
428
  async function promptForOptions(appName, options) {
414
- // Get app type from options (default to webapp)
415
- const appType = options.type || 'webapp';
429
+ // Get app type from options (default to external)
430
+ const appType = options.type || 'external';
416
431
 
417
432
  // Build questions based on app type
418
433
  let questions = [];
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,16 +146,20 @@ 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);
153
+ const authType = config.authentication?.type || config.authentication?.method || config.authType;
149
154
  return generateExternalReadmeContent({
150
155
  appName,
151
156
  systemKey,
152
157
  systemType: config.systemType,
153
158
  displayName: config.systemDisplayName,
154
159
  description: config.systemDescription,
155
- datasources
160
+ fileExt: config.fileExt,
161
+ datasources,
162
+ authType
156
163
  });
157
164
  }
158
165
  const context = buildReadmeContext(appName, config);
@@ -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