@aifabrix/builder 2.32.2 → 2.33.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 (130) hide show
  1. package/.cursor/rules/project-rules.mdc +8 -0
  2. package/README.md +36 -8
  3. package/bin/aifabrix.js +6 -8
  4. package/integration/hubspot/README.md +8 -7
  5. package/integration/hubspot/companies.json +2048 -0
  6. package/integration/hubspot/create-hubspot.js +665 -0
  7. package/integration/hubspot/{hubspot-deploy-company.json → hubspot-datasource-company.json} +1 -1
  8. package/integration/hubspot/{hubspot-deploy-contact.json → hubspot-datasource-contact.json} +1 -1
  9. package/integration/hubspot/{hubspot-deploy-deal.json → hubspot-datasource-deal.json} +1 -1
  10. package/integration/hubspot/hubspot-deploy.json +832 -81
  11. package/integration/hubspot/hubspot-system.json +99 -0
  12. package/integration/hubspot/test-artifacts/wizard-hubspot-credential-real.yaml +20 -0
  13. package/integration/hubspot/test-artifacts/wizard-hubspot-env-vars.yaml +9 -0
  14. package/integration/hubspot/test-artifacts/wizard-invalid-add-datasource.yaml +5 -0
  15. package/integration/hubspot/test-artifacts/wizard-invalid-app-name.yaml +5 -0
  16. package/integration/hubspot/test-artifacts/wizard-invalid-credential-create.yaml +7 -0
  17. package/integration/hubspot/test-artifacts/wizard-invalid-credential-select.yaml +7 -0
  18. package/integration/hubspot/test-artifacts/wizard-invalid-known-platform.yaml +4 -0
  19. package/integration/hubspot/test-artifacts/wizard-invalid-missing-app.yaml +4 -0
  20. package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
  21. package/integration/hubspot/test-artifacts/wizard-invalid-mode.yaml +5 -0
  22. package/integration/hubspot/test-artifacts/wizard-invalid-openapi-file.yaml +5 -0
  23. package/integration/hubspot/test-artifacts/wizard-invalid-openapi-url.yaml +4 -0
  24. package/integration/hubspot/test-artifacts/wizard-invalid-source.yaml +4 -0
  25. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-array-test.yaml +5 -0
  26. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
  27. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
  28. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-test.yaml +5 -0
  29. package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-test.yaml +5 -0
  30. package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +5 -0
  31. package/integration/hubspot/test-dataplane-down-helpers.js +246 -0
  32. package/integration/hubspot/test-dataplane-down-tests.js +419 -0
  33. package/integration/hubspot/test-dataplane-down.js +157 -0
  34. package/integration/hubspot/test.js +1517 -0
  35. package/integration/hubspot/variables.yaml +4 -4
  36. package/integration/hubspot/wizard-hubspot-e2e.yaml +16 -0
  37. package/integration/hubspot/wizard-hubspot-platform.yaml +8 -0
  38. package/lib/api/applications.api.js +1 -0
  39. package/lib/api/index.js +10 -5
  40. package/lib/api/types/wizard.types.js +176 -38
  41. package/lib/api/wizard.api.js +207 -38
  42. package/lib/app/deploy.js +116 -54
  43. package/lib/app/display.js +6 -5
  44. package/lib/app/dockerfile.js +2 -1
  45. package/lib/app/list.js +78 -37
  46. package/lib/app/prompts.js +9 -5
  47. package/lib/app/readme.js +41 -112
  48. package/lib/app/register.js +44 -9
  49. package/lib/app/rotate-secret.js +50 -32
  50. package/lib/cli.js +243 -65
  51. package/lib/commands/app.js +4 -9
  52. package/lib/commands/auth-config.js +125 -0
  53. package/lib/commands/auth-status.js +261 -0
  54. package/lib/commands/datasource.js +3 -6
  55. package/lib/commands/login-credentials.js +4 -4
  56. package/lib/commands/login-device.js +43 -29
  57. package/lib/commands/login.js +22 -13
  58. package/lib/commands/wizard-config-normalizer.js +92 -0
  59. package/lib/commands/wizard-core.js +515 -0
  60. package/lib/commands/wizard-dataplane.js +122 -0
  61. package/lib/commands/wizard-headless.js +115 -0
  62. package/lib/commands/wizard.js +129 -357
  63. package/lib/core/config.js +46 -0
  64. package/lib/core/secrets.js +3 -22
  65. package/lib/core/templates-env.js +1 -1
  66. package/lib/datasource/deploy.js +34 -23
  67. package/lib/datasource/list.js +8 -6
  68. package/lib/deployment/deployer.js +25 -0
  69. package/lib/deployment/environment.js +10 -13
  70. package/lib/external-system/delete.js +151 -0
  71. package/lib/external-system/deploy.js +54 -378
  72. package/lib/external-system/download-helpers.js +45 -65
  73. package/lib/external-system/download.js +34 -13
  74. package/lib/external-system/generator.js +11 -7
  75. package/lib/external-system/test-auth.js +5 -3
  76. package/lib/generator/builders.js +3 -1
  77. package/lib/generator/external-controller-manifest.js +157 -0
  78. package/lib/generator/external-schema-utils.js +236 -0
  79. package/lib/generator/external.js +55 -3
  80. package/lib/generator/index.js +22 -10
  81. package/lib/generator/wizard-prompts.js +33 -10
  82. package/lib/generator/wizard.js +69 -86
  83. package/lib/infrastructure/compose.js +100 -0
  84. package/lib/infrastructure/helpers.js +139 -0
  85. package/lib/infrastructure/index.js +52 -311
  86. package/lib/infrastructure/services.js +168 -0
  87. package/lib/schema/application-schema.json +24 -5
  88. package/lib/schema/external-datasource.schema.json +303 -17
  89. package/lib/schema/external-system.schema.json +1 -1
  90. package/lib/schema/wizard-config.schema.json +234 -0
  91. package/lib/utils/api.js +37 -42
  92. package/lib/utils/app-existence.js +42 -0
  93. package/lib/utils/app-register-config.js +7 -2
  94. package/lib/utils/app-register-display.js +2 -1
  95. package/lib/utils/auth-config-validator.js +92 -0
  96. package/lib/utils/cli-utils.js +3 -1
  97. package/lib/utils/command-header.js +43 -0
  98. package/lib/utils/compose-generator.js +113 -70
  99. package/lib/utils/controller-url.js +115 -0
  100. package/lib/utils/dataplane-health.js +115 -0
  101. package/lib/utils/dataplane-resolver.js +29 -0
  102. package/lib/utils/dev-config.js +6 -2
  103. package/lib/utils/env-copy.js +2 -1
  104. package/lib/utils/env-map.js +2 -1
  105. package/lib/utils/env-ports.js +2 -1
  106. package/lib/utils/env-template.js +1 -1
  107. package/lib/utils/error-formatter.js +149 -28
  108. package/lib/utils/external-readme.js +125 -0
  109. package/lib/utils/help-builder.js +190 -0
  110. package/lib/utils/infra-status.js +13 -3
  111. package/lib/utils/paths.js +17 -2
  112. package/lib/utils/port-resolver.js +111 -0
  113. package/lib/utils/secrets-helpers.js +3 -15
  114. package/lib/utils/secrets-utils.js +2 -2
  115. package/lib/utils/token-manager.js +69 -4
  116. package/lib/utils/variable-transformer.js +7 -2
  117. package/lib/validation/external-manifest-validator.js +202 -0
  118. package/lib/validation/validate-display.js +406 -0
  119. package/lib/validation/validate.js +159 -123
  120. package/lib/validation/validator.js +38 -4
  121. package/lib/validation/wizard-config-validator.js +267 -0
  122. package/package.json +4 -2
  123. package/templates/applications/README.md.hbs +19 -17
  124. package/templates/applications/miso-controller/env.template +1 -1
  125. package/templates/applications/miso-controller/rbac.yaml +7 -7
  126. package/templates/external-system/README.md.hbs +99 -0
  127. package/templates/external-system/external-system.json.hbs +1 -1
  128. package/templates/infra/compose.yaml.hbs +35 -0
  129. package/templates/python/docker-compose.hbs +26 -0
  130. package/templates/typescript/docker-compose.hbs +26 -0
@@ -17,23 +17,26 @@ const builders = require('./builders');
17
17
  const { detectAppType, getDeployJsonPath } = require('../utils/paths');
18
18
  const splitFunctions = require('./split');
19
19
  const { loadVariables, loadEnvTemplate, loadRbac, parseEnvironmentVariables } = require('./helpers');
20
- const { generateExternalSystemDeployJson, generateExternalSystemApplicationSchema } = require('./external');
20
+ const { generateExternalSystemApplicationSchema, splitExternalApplicationSchema } = require('./external');
21
+ const { generateControllerManifest } = require('./external-controller-manifest');
21
22
 
22
23
  /**
23
24
  * Generates deployment JSON from application configuration files
24
- * Creates <app-name>-deploy.json for all apps (consistent naming)
25
- * For external systems, loads the system JSON file
25
+ * Creates <app-name>-deploy.json for regular apps (consistent naming)
26
+ * For external systems, generates application-schema.json
26
27
  * For regular apps, generates deployment manifest from variables.yaml, env.template, rbac.yaml
27
28
  *
28
29
  * @async
29
30
  * @function generateDeployJson
30
31
  * @param {string} appName - Name of the application
32
+ * @param {Object} [options] - Generation options
33
+ * @param {string} [options.type] - Forced application type (external)
31
34
  * @returns {Promise<string>} Path to generated deployment JSON file
32
35
  * @throws {Error} If generation fails or configuration is invalid
33
36
  *
34
37
  * @example
35
38
  * const jsonPath = await generateDeployJson('myapp');
36
- * // Returns: './builder/myapp/myapp-deploy.json' or './integration/hubspot/hubspot-deploy.json'
39
+ * // Returns: './builder/myapp/myapp-deploy.json' or './integration/hubspot/application-schema.json'
37
40
  */
38
41
  /**
39
42
  * Loads configuration files for deployment generation
@@ -88,17 +91,25 @@ function buildAndValidateDeployment(appName, variables, envTemplate, rbac) {
88
91
  return deployment;
89
92
  }
90
93
 
91
- async function generateDeployJson(appName) {
94
+ async function generateDeployJson(appName, options = {}) {
92
95
  if (!appName || typeof appName !== 'string') {
93
96
  throw new Error('App name is required and must be a string');
94
97
  }
95
98
 
96
99
  // Detect app type and get correct path (integration or builder)
97
- const { isExternal, appPath, appType } = await detectAppType(appName);
100
+ const { isExternal, appPath, appType } = await detectAppType(appName, options);
98
101
 
99
102
  // Check if app type is external
100
103
  if (isExternal) {
101
- return await generateExternalSystemDeployJson(appName, appPath);
104
+ // Generate controller-compatible manifest format
105
+ const manifest = await generateControllerManifest(appName);
106
+
107
+ // Determine system key for file naming
108
+ const systemKey = manifest.key || appName;
109
+ const deployJsonPath = path.join(appPath, `${systemKey}-deploy.json`);
110
+
111
+ await fs.promises.writeFile(deployJsonPath, JSON.stringify(manifest, null, 2), { mode: 0o644, encoding: 'utf8' });
112
+ return deployJsonPath;
102
113
  }
103
114
 
104
115
  // Regular app: generate deployment manifest
@@ -112,13 +123,13 @@ async function generateDeployJson(appName) {
112
123
  return jsonPath;
113
124
  }
114
125
 
115
- async function generateDeployJsonWithValidation(appName) {
116
- const jsonPath = await generateDeployJson(appName);
126
+ async function generateDeployJsonWithValidation(appName, options = {}) {
127
+ const jsonPath = await generateDeployJson(appName, options);
117
128
  const jsonContent = fs.readFileSync(jsonPath, 'utf8');
118
129
  const deployment = JSON.parse(jsonContent);
119
130
 
120
131
  // Detect if this is an external system
121
- const { isExternal } = await detectAppType(appName);
132
+ const { isExternal } = await detectAppType(appName, options);
122
133
 
123
134
  // For external systems, skip deployment JSON validation (they use external system JSON structure)
124
135
  if (isExternal) {
@@ -143,6 +154,7 @@ module.exports = {
143
154
  generateDeployJson,
144
155
  generateDeployJsonWithValidation,
145
156
  generateExternalSystemApplicationSchema,
157
+ splitExternalApplicationSchema,
146
158
  parseEnvironmentVariables,
147
159
  splitDeployJson: splitFunctions.splitDeployJson,
148
160
  extractEnvTemplate: splitFunctions.extractEnvTemplate,
@@ -29,6 +29,28 @@ async function promptForMode() {
29
29
  return mode;
30
30
  }
31
31
 
32
+ /**
33
+ * Prompt for existing system ID or key (for add-datasource mode)
34
+ * @async
35
+ * @function promptForSystemIdOrKey
36
+ * @returns {Promise<string>} System ID or key
37
+ */
38
+ async function promptForSystemIdOrKey() {
39
+ const { systemIdOrKey } = await inquirer.prompt([
40
+ {
41
+ type: 'input',
42
+ name: 'systemIdOrKey',
43
+ message: 'Enter the existing system ID or key:',
44
+ validate: (input) => {
45
+ if (!input || typeof input !== 'string' || input.trim().length === 0) {
46
+ return 'System ID or key is required';
47
+ }
48
+ return true;
49
+ }
50
+ }
51
+ ]);
52
+ return systemIdOrKey.trim();
53
+ }
32
54
  /**
33
55
  * Prompt for source type selection
34
56
  * @async
@@ -195,19 +217,19 @@ async function promptForKnownPlatform(platforms = []) {
195
217
  async function promptForUserIntent() {
196
218
  const { intent } = await inquirer.prompt([
197
219
  {
198
- type: 'list',
220
+ type: 'input',
199
221
  name: 'intent',
200
- message: 'What is your primary use case?',
201
- choices: [
202
- { name: 'Sales-focused (CRM, leads, deals)', value: 'sales-focused' },
203
- { name: 'Support-focused (tickets, customers)', value: 'support-focused' },
204
- { name: 'Marketing-focused (campaigns, contacts)', value: 'marketing-focused' },
205
- { name: 'General integration', value: 'general' }
206
- ],
207
- default: 'general'
222
+ message: 'Describe your primary use case (any text):',
223
+ default: 'general integration',
224
+ validate: (input) => {
225
+ if (!input || typeof input !== 'string' || input.trim().length === 0) {
226
+ return 'Intent is required';
227
+ }
228
+ return true;
229
+ }
208
230
  }
209
231
  ]);
210
- return intent;
232
+ return intent.trim();
211
233
  }
212
234
 
213
235
  /**
@@ -344,6 +366,7 @@ async function promptForAppName(defaultName) {
344
366
 
345
367
  module.exports = {
346
368
  promptForMode,
369
+ promptForSystemIdOrKey,
347
370
  promptForSourceType,
348
371
  promptForOpenApiFile,
349
372
  promptForOpenApiUrl,
@@ -10,7 +10,7 @@ const yaml = require('js-yaml');
10
10
  const Handlebars = require('handlebars');
11
11
  const chalk = require('chalk');
12
12
  const logger = require('../utils/logger');
13
- const { generateExternalSystemApplicationSchema } = require('./external');
13
+ const { generateExternalReadmeContent } = require('../utils/external-readme');
14
14
 
15
15
  /**
16
16
  * Generate files from dataplane-generated wizard configurations
@@ -33,7 +33,7 @@ const { generateExternalSystemApplicationSchema } = require('./external');
33
33
  * @returns {Promise<string>} System file path
34
34
  */
35
35
  async function writeSystemJsonFile(appPath, finalSystemKey, systemConfig) {
36
- const systemFileName = `${finalSystemKey}-deploy.json`;
36
+ const systemFileName = `${finalSystemKey}-system.json`;
37
37
  const systemFilePath = path.join(appPath, systemFileName);
38
38
  await fs.writeFile(systemFilePath, JSON.stringify(systemConfig, null, 2), 'utf8');
39
39
  logger.log(chalk.green(`✓ Generated system file: ${systemFileName}`));
@@ -53,7 +53,12 @@ async function writeDatasourceJsonFiles(appPath, finalSystemKey, datasourceConfi
53
53
  const datasourceFileNames = [];
54
54
  for (const datasourceConfig of datasourceConfigs) {
55
55
  const entityType = datasourceConfig.entityType || datasourceConfig.entityKey || datasourceConfig.key?.split('-').pop() || 'default';
56
- const datasourceFileName = `${finalSystemKey}-deploy-${entityType}.json`;
56
+ const datasourceKey = datasourceConfig.key || `${finalSystemKey}-${entityType}`;
57
+ // Extract datasource key (remove system key prefix if present)
58
+ const datasourceKeyOnly = datasourceKey.includes('-') && datasourceKey.startsWith(`${finalSystemKey}-`)
59
+ ? datasourceKey.substring(finalSystemKey.length + 1)
60
+ : entityType;
61
+ const datasourceFileName = `${finalSystemKey}-datasource-${datasourceKeyOnly}.json`;
57
62
  const datasourceFilePath = path.join(appPath, datasourceFileName);
58
63
  await fs.writeFile(datasourceFilePath, JSON.stringify(datasourceConfig, null, 2), 'utf8');
59
64
  datasourceFileNames.push(datasourceFileName);
@@ -99,17 +104,18 @@ async function generateConfigFilesForWizard(params) {
99
104
  // Generate deployment scripts
100
105
  const deployScripts = await generateDeployScripts(appPath, finalSystemKey, systemFileName, datasourceFileNames);
101
106
 
102
- // Generate application-schema.json
103
- const applicationSchema = await generateExternalSystemApplicationSchema(appName);
104
- const applicationSchemaPath = path.join(appPath, 'application-schema.json');
105
- await fs.writeFile(applicationSchemaPath, JSON.stringify(applicationSchema, null, 2), 'utf8');
106
- logger.log(chalk.green('✓ Generated application-schema.json'));
107
+ // Generate deployment manifest (<systemKey>-deploy.json) using controller format
108
+ const { generateControllerManifest } = require('./external-controller-manifest');
109
+ const manifest = await generateControllerManifest(appName, { appPath });
110
+ const deployManifestPath = path.join(appPath, `${finalSystemKey}-deploy.json`);
111
+ await fs.writeFile(deployManifestPath, JSON.stringify(manifest, null, 2), 'utf8');
112
+ logger.log(chalk.green(`✓ Generated deployment manifest: ${finalSystemKey}-deploy.json`));
107
113
 
108
114
  return {
109
115
  variablesPath: path.join(appPath, 'variables.yaml'),
110
116
  envTemplatePath: path.join(appPath, 'env.template'),
111
117
  readmePath: path.join(appPath, 'README.md'),
112
- applicationSchemaPath,
118
+ applicationSchemaPath: deployManifestPath,
113
119
  ...deployScripts
114
120
  };
115
121
  }
@@ -123,23 +129,46 @@ async function generateWizardFiles(appName, systemConfig, datasourceConfigs, sys
123
129
  // Create directory if it doesn't exist
124
130
  await fs.mkdir(appPath, { recursive: true });
125
131
 
126
- // Extract system key from config if not provided
127
- const finalSystemKey = systemKey || systemConfig.key || appName;
132
+ // Use appName as the system key to ensure consistent naming
133
+ // Priority: appName > systemKey parameter > systemConfig.key
134
+ const finalSystemKey = appName;
135
+
136
+ // Generate displayName from appName (e.g., "my-hubspot" -> "My Hubspot")
137
+ const appDisplayName = appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
138
+
139
+ // Update system config to use the appName as key and displayName
140
+ const updatedSystemConfig = {
141
+ ...systemConfig,
142
+ key: finalSystemKey,
143
+ displayName: appDisplayName
144
+ };
145
+
146
+ // Update datasource configs to use appName-based keys and systemKey
147
+ const updatedDatasourceConfigs = datasourceConfigs.map(ds => {
148
+ const entityType = ds.entityType || ds.entityKey || ds.key?.split('-').pop() || 'default';
149
+ const entityDisplayName = entityType.charAt(0).toUpperCase() + entityType.slice(1).replace(/-/g, ' ');
150
+ return {
151
+ ...ds,
152
+ key: `${finalSystemKey}-${entityType}`,
153
+ systemKey: finalSystemKey,
154
+ displayName: `${appDisplayName} ${entityDisplayName}`
155
+ };
156
+ });
128
157
 
129
158
  // Write system and datasource JSON files
130
- const systemFilePath = await writeSystemJsonFile(appPath, finalSystemKey, systemConfig);
131
- const datasourceFileNames = await writeDatasourceJsonFiles(appPath, finalSystemKey, datasourceConfigs);
159
+ const systemFilePath = await writeSystemJsonFile(appPath, finalSystemKey, updatedSystemConfig);
160
+ const datasourceFileNames = await writeDatasourceJsonFiles(appPath, finalSystemKey, updatedDatasourceConfigs);
132
161
 
133
162
  // Generate configuration files
134
- const systemFileName = `${finalSystemKey}-deploy.json`;
163
+ const systemFileName = `${finalSystemKey}-system.json`;
135
164
  const configFiles = await generateConfigFilesForWizard({
136
165
  appPath,
137
166
  appName,
138
167
  finalSystemKey,
139
168
  systemFileName,
140
169
  datasourceFileNames,
141
- systemConfig,
142
- datasourceConfigs,
170
+ systemConfig: updatedSystemConfig,
171
+ datasourceConfigs: updatedDatasourceConfigs,
143
172
  aiGeneratedReadme
144
173
  });
145
174
 
@@ -408,76 +437,30 @@ async function generateReadme(appPath, appName, systemKey, systemConfig, datasou
408
437
  return;
409
438
  }
410
439
 
411
- const displayName = systemConfig.displayName || appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
412
- const description = systemConfig.description || `External system integration for ${appName}`;
413
-
414
- const lines = [
415
- `# ${displayName}`,
416
- '',
417
- description,
418
- '',
419
- '## Overview',
420
- '',
421
- 'This integration was created using the AI Fabrix wizard.',
422
- '',
423
- '## Files',
424
- '',
425
- `- \`${systemKey}-deploy.json\` - External system configuration`,
426
- ...datasourceConfigs.map((ds, index) => {
427
- const entityType = ds.entityType || ds.entityKey || ds.key?.split('-').pop() || `datasource${index + 1}`;
428
- return `- \`${systemKey}-deploy-${entityType}.json\` - Datasource configuration`;
429
- }),
430
- '- `variables.yaml` - Application variables and external integration configuration',
431
- '- `env.template` - Environment variable template',
432
- '- `application-schema.json` - Single deployment file',
433
- '- `deploy.sh` - Bash deployment script',
434
- '- `deploy.ps1` - PowerShell deployment script',
435
- '',
436
- '## Deployment',
437
- '',
438
- '### Using Deployment Scripts',
439
- '',
440
- 'You can deploy using the provided scripts:',
441
- '',
442
- '**Bash (Linux/macOS):**',
443
- '```bash',
444
- './deploy.sh',
445
- '```',
446
- '',
447
- '**PowerShell (Windows):**',
448
- '```powershell',
449
- '.\\deploy.ps1',
450
- '```',
451
- '',
452
- 'The scripts support environment variables:',
453
- '- `ENVIRONMENT` - Environment key (default: dev)',
454
- '- `CONTROLLER` - Controller URL (default: http://localhost:3000)',
455
- '- `RUN_TESTS` - Set to "true" to run integration tests after deployment',
456
- '',
457
- '**Example:**',
458
- '```bash',
459
- 'ENVIRONMENT=prod CONTROLLER=https://controller.example.com ./deploy.sh',
460
- '```',
461
- '',
462
- '### Using CLI Directly',
463
- '',
464
- 'To deploy this external system:',
465
- '',
466
- '```bash',
467
- `aifabrix deploy ${appName}`,
468
- '```',
469
- '',
470
- '## Configuration',
471
- '',
472
- 'Update the environment variables in `env.template` and set the values in your secrets store.',
473
- '',
474
- '## Documentation',
475
- '',
476
- 'For more information, see the [External Systems Documentation](../../docs/external-systems.md).'
477
- ];
478
-
479
- await fs.writeFile(readmePath, lines.join('\n'), 'utf8');
480
- logger.log(chalk.green('✓ Generated README.md'));
440
+ const datasources = (Array.isArray(datasourceConfigs) ? datasourceConfigs : []).map((ds, index) => {
441
+ const entityType = ds.entityType || ds.entityKey || ds.key?.split('-').pop() || `datasource${index + 1}`;
442
+ const datasourceKey = ds.key || `${systemKey}-${entityType}`;
443
+ const datasourceKeyOnly = datasourceKey.includes('-') && datasourceKey.startsWith(`${systemKey}-`)
444
+ ? datasourceKey.substring(systemKey.length + 1)
445
+ : entityType;
446
+ return {
447
+ entityType,
448
+ displayName: ds.displayName || ds.name || ds.key || `Datasource ${index + 1}`,
449
+ fileName: `${systemKey}-datasource-${datasourceKeyOnly}.json`
450
+ };
451
+ });
452
+
453
+ const readmeContent = generateExternalReadmeContent({
454
+ appName,
455
+ systemKey,
456
+ systemType: systemConfig.type || systemConfig.systemType,
457
+ displayName: systemConfig.displayName,
458
+ description: systemConfig.description,
459
+ datasources
460
+ });
461
+
462
+ await fs.writeFile(readmePath, readmeContent, 'utf8');
463
+ logger.log(chalk.green(' Generated README.md (template)'));
481
464
  } catch (error) {
482
465
  throw new Error(`Failed to generate README.md: ${error.message}`);
483
466
  }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * AI Fabrix Builder Infrastructure Compose File Generation
3
+ *
4
+ * Handles Docker Compose file generation from templates and Traefik configuration.
5
+ *
6
+ * @fileoverview Compose file generation for infrastructure
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const handlebars = require('handlebars');
13
+ const path = require('path');
14
+
15
+ /**
16
+ * Builds Traefik configuration from environment variables
17
+ * @param {boolean} enabled - Whether Traefik should be included
18
+ * @returns {Object} Traefik configuration
19
+ */
20
+ function buildTraefikConfig(enabled) {
21
+ return {
22
+ enabled: !!enabled,
23
+ certStore: process.env.TRAEFIK_CERT_STORE || null,
24
+ certFile: process.env.TRAEFIK_CERT_FILE || null,
25
+ keyFile: process.env.TRAEFIK_KEY_FILE || null
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Validates Traefik configuration when enabled
31
+ * @param {Object} traefikConfig - Traefik configuration
32
+ * @returns {{valid: boolean, errors: string[]}} Validation result
33
+ */
34
+ function validateTraefikConfig(traefikConfig) {
35
+ if (!traefikConfig || !traefikConfig.enabled) {
36
+ return { valid: true, errors: [] };
37
+ }
38
+
39
+ const errors = [];
40
+
41
+ if (traefikConfig.certStore) {
42
+ if (!traefikConfig.certFile || !traefikConfig.keyFile) {
43
+ errors.push('TRAEFIK_CERT_FILE and TRAEFIK_KEY_FILE are required when TRAEFIK_CERT_STORE is set');
44
+ } else {
45
+ if (!fs.existsSync(traefikConfig.certFile)) {
46
+ errors.push(`Certificate file not found: ${traefikConfig.certFile}`);
47
+ }
48
+ if (!fs.existsSync(traefikConfig.keyFile)) {
49
+ errors.push(`Private key file not found: ${traefikConfig.keyFile}`);
50
+ }
51
+ }
52
+ }
53
+
54
+ return { valid: errors.length === 0, errors };
55
+ }
56
+
57
+ /**
58
+ * Generate docker-compose file from template
59
+ * @param {string} templatePath - Path to compose template
60
+ * @param {string} devId - Developer ID
61
+ * @param {number} idNum - Developer ID number
62
+ * @param {Object} ports - Port configuration
63
+ * @param {string} infraDir - Infrastructure directory
64
+ * @param {Object} [options] - Additional options
65
+ * @param {Object|boolean} [options.traefik] - Traefik configuration
66
+ * @returns {string} Path to generated compose file
67
+ */
68
+ function generateComposeFile(templatePath, devId, idNum, ports, infraDir, options = {}) {
69
+ const templateContent = fs.readFileSync(templatePath, 'utf8');
70
+ const template = handlebars.compile(templateContent);
71
+ const networkName = idNum === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
72
+ const serversJsonPath = path.join(infraDir, 'servers.json');
73
+ const pgpassPath = path.join(infraDir, 'pgpass');
74
+ const traefikConfig = typeof options.traefik === 'object'
75
+ ? options.traefik
76
+ : buildTraefikConfig(!!options.traefik);
77
+ const composeContent = template({
78
+ devId: devId,
79
+ postgresPort: ports.postgres,
80
+ redisPort: ports.redis,
81
+ pgadminPort: ports.pgadmin,
82
+ redisCommanderPort: ports.redisCommander,
83
+ traefikHttpPort: ports.traefikHttp,
84
+ traefikHttpsPort: ports.traefikHttps,
85
+ networkName: networkName,
86
+ serversJsonPath: serversJsonPath,
87
+ pgpassPath: pgpassPath,
88
+ infraDir: infraDir,
89
+ traefik: traefikConfig
90
+ });
91
+ const composePath = path.join(infraDir, 'compose.yaml');
92
+ fs.writeFileSync(composePath, composeContent);
93
+ return composePath;
94
+ }
95
+
96
+ module.exports = {
97
+ buildTraefikConfig,
98
+ validateTraefikConfig,
99
+ generateComposeFile
100
+ };
@@ -0,0 +1,139 @@
1
+ /**
2
+ * AI Fabrix Builder Infrastructure Helpers
3
+ *
4
+ * Helper functions for infrastructure management including directory names,
5
+ * Docker availability checks, and pgAdmin configuration.
6
+ *
7
+ * @fileoverview Helper functions for infrastructure management
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const path = require('path');
13
+ const fs = require('fs');
14
+ const handlebars = require('handlebars');
15
+ const secrets = require('../core/secrets');
16
+ const logger = require('../utils/logger');
17
+ const dockerUtils = require('../utils/docker');
18
+ const paths = require('../utils/paths');
19
+
20
+ /**
21
+ * Gets infrastructure directory name based on developer ID
22
+ * Dev 0: infra (no dev-0 suffix), Dev > 0: infra-dev{id}
23
+ * @param {number|string} devId - Developer ID
24
+ * @returns {string} Infrastructure directory name
25
+ */
26
+ function getInfraDirName(devId) {
27
+ const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
28
+ return idNum === 0 ? 'infra' : `infra-dev${devId}`;
29
+ }
30
+
31
+ /**
32
+ * Gets Docker Compose project name based on developer ID
33
+ * Dev 0: infra (no dev-0 suffix), Dev > 0: infra-dev{id}
34
+ * @param {number|string} devId - Developer ID
35
+ * @returns {string} Docker Compose project name
36
+ */
37
+ function getInfraProjectName(devId) {
38
+ const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
39
+ return idNum === 0 ? 'infra' : `infra-dev${devId}`;
40
+ }
41
+
42
+ /**
43
+ * Check Docker availability
44
+ * @async
45
+ * @returns {Promise<void>}
46
+ * @throws {Error} If Docker is not available
47
+ */
48
+ async function checkDockerAvailability() {
49
+ try {
50
+ await dockerUtils.ensureDockerAndCompose();
51
+ } catch (error) {
52
+ throw new Error('Docker or Docker Compose is not available. Please install and start Docker.');
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Ensure admin secrets file exists
58
+ * @async
59
+ * @returns {Promise<string>} Path to admin secrets file
60
+ */
61
+ async function ensureAdminSecrets() {
62
+ const adminSecretsPath = path.join(paths.getAifabrixHome(), 'admin-secrets.env');
63
+ if (!fs.existsSync(adminSecretsPath)) {
64
+ logger.log('Generating admin-secrets.env...');
65
+ await secrets.generateAdminSecretsEnv();
66
+ }
67
+ return adminSecretsPath;
68
+ }
69
+
70
+ /**
71
+ * Generates pgAdmin4 configuration files (servers.json and pgpass)
72
+ * @param {string} infraDir - Infrastructure directory path
73
+ * @param {string} postgresPassword - PostgreSQL password
74
+ */
75
+ function generatePgAdminConfig(infraDir, postgresPassword) {
76
+ const serversJsonTemplatePath = path.join(__dirname, '..', 'templates', 'infra', 'servers.json.hbs');
77
+ if (!fs.existsSync(serversJsonTemplatePath)) {
78
+ return;
79
+ }
80
+
81
+ const serversJsonTemplateContent = fs.readFileSync(serversJsonTemplatePath, 'utf8');
82
+ const serversJsonTemplate = handlebars.compile(serversJsonTemplateContent);
83
+ const serversJsonContent = serversJsonTemplate({ postgresPassword });
84
+ const serversJsonPath = path.join(infraDir, 'servers.json');
85
+ fs.writeFileSync(serversJsonPath, serversJsonContent, { mode: 0o644 });
86
+
87
+ const pgpassContent = `postgres:5432:postgres:pgadmin:${postgresPassword}\n`;
88
+ const pgpassPath = path.join(infraDir, 'pgpass');
89
+ fs.writeFileSync(pgpassPath, pgpassContent, { mode: 0o600 });
90
+ }
91
+
92
+ /**
93
+ * Prepare infrastructure directory and extract postgres password
94
+ * @param {string} devId - Developer ID
95
+ * @param {string} adminSecretsPath - Path to admin secrets file
96
+ * @returns {Object} Object with infraDir and postgresPassword
97
+ */
98
+ function prepareInfraDirectory(devId, adminSecretsPath) {
99
+ const aifabrixDir = paths.getAifabrixHome();
100
+ const infraDirName = getInfraDirName(devId);
101
+ const infraDir = path.join(aifabrixDir, infraDirName);
102
+ if (!fs.existsSync(infraDir)) {
103
+ fs.mkdirSync(infraDir, { recursive: true });
104
+ }
105
+
106
+ const adminSecretsContent = fs.readFileSync(adminSecretsPath, 'utf8');
107
+ const postgresPasswordMatch = adminSecretsContent.match(/^POSTGRES_PASSWORD=(.+)$/m);
108
+ const postgresPassword = postgresPasswordMatch ? postgresPasswordMatch[1] : '';
109
+ generatePgAdminConfig(infraDir, postgresPassword);
110
+
111
+ return { infraDir, postgresPassword };
112
+ }
113
+
114
+ /**
115
+ * Register Handlebars helper for equality comparison
116
+ */
117
+ function registerHandlebarsHelper() {
118
+ handlebars.registerHelper('eq', (a, b) => {
119
+ // Handle null/undefined - treat as "0" for default infrastructure
120
+ if (a === null || a === undefined) a = '0';
121
+ if (b === null || b === undefined) b = '0';
122
+ const aNum = typeof a === 'string' && /^\d+$/.test(a) ? parseInt(a, 10) : a;
123
+ const bNum = typeof b === 'string' && /^\d+$/.test(b) ? parseInt(b, 10) : b;
124
+ if (typeof aNum === 'number' && typeof bNum === 'number') {
125
+ return aNum === bNum;
126
+ }
127
+ return a === b;
128
+ });
129
+ }
130
+
131
+ module.exports = {
132
+ getInfraDirName,
133
+ getInfraProjectName,
134
+ checkDockerAvailability,
135
+ ensureAdminSecrets,
136
+ generatePgAdminConfig,
137
+ prepareInfraDirectory,
138
+ registerHandlebarsHelper
139
+ };