@aifabrix/builder 2.32.3 → 2.33.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 (127) 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 +12 -11
  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 +6 -2
  40. package/lib/api/types/wizard.types.js +176 -38
  41. package/lib/api/wizard.api.js +161 -23
  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 +17 -10
  46. package/lib/app/readme.js +41 -112
  47. package/lib/app/register.js +44 -9
  48. package/lib/app/rotate-secret.js +48 -31
  49. package/lib/cli.js +219 -70
  50. package/lib/commands/app.js +4 -9
  51. package/lib/commands/auth-config.js +125 -0
  52. package/lib/commands/auth-status.js +7 -8
  53. package/lib/commands/datasource.js +3 -6
  54. package/lib/commands/login-credentials.js +4 -4
  55. package/lib/commands/login-device.js +26 -17
  56. package/lib/commands/login.js +12 -10
  57. package/lib/commands/wizard-config-normalizer.js +92 -0
  58. package/lib/commands/wizard-core.js +515 -0
  59. package/lib/commands/wizard-dataplane.js +122 -0
  60. package/lib/commands/wizard-headless.js +115 -0
  61. package/lib/commands/wizard.js +110 -332
  62. package/lib/core/config.js +46 -0
  63. package/lib/core/secrets.js +3 -22
  64. package/lib/core/templates-env.js +1 -1
  65. package/lib/datasource/deploy.js +59 -23
  66. package/lib/datasource/list.js +108 -19
  67. package/lib/deployment/deployer.js +25 -0
  68. package/lib/deployment/environment.js +10 -13
  69. package/lib/external-system/delete.js +151 -0
  70. package/lib/external-system/deploy.js +53 -378
  71. package/lib/external-system/download-helpers.js +45 -65
  72. package/lib/external-system/download.js +33 -13
  73. package/lib/external-system/generator.js +11 -7
  74. package/lib/external-system/test-auth.js +4 -3
  75. package/lib/generator/builders.js +3 -1
  76. package/lib/generator/external-controller-manifest.js +157 -0
  77. package/lib/generator/external-schema-utils.js +236 -0
  78. package/lib/generator/external.js +55 -3
  79. package/lib/generator/index.js +22 -10
  80. package/lib/generator/wizard-prompts.js +33 -10
  81. package/lib/generator/wizard.js +69 -86
  82. package/lib/infrastructure/compose.js +100 -0
  83. package/lib/infrastructure/helpers.js +139 -0
  84. package/lib/infrastructure/index.js +52 -311
  85. package/lib/infrastructure/services.js +168 -0
  86. package/lib/schema/application-schema.json +23 -4
  87. package/lib/schema/external-datasource.schema.json +2 -2
  88. package/lib/schema/wizard-config.schema.json +234 -0
  89. package/lib/utils/api.js +102 -52
  90. package/lib/utils/app-existence.js +42 -0
  91. package/lib/utils/app-register-config.js +7 -2
  92. package/lib/utils/auth-config-validator.js +92 -0
  93. package/lib/utils/command-header.js +43 -0
  94. package/lib/utils/compose-generator.js +113 -70
  95. package/lib/utils/controller-url.js +65 -17
  96. package/lib/utils/dataplane-health.js +115 -0
  97. package/lib/utils/dataplane-resolver.js +29 -0
  98. package/lib/utils/dev-config.js +6 -2
  99. package/lib/utils/env-copy.js +2 -1
  100. package/lib/utils/env-ports.js +2 -1
  101. package/lib/utils/env-template.js +1 -1
  102. package/lib/utils/error-formatter.js +49 -0
  103. package/lib/utils/error-formatters/network-errors.js +13 -3
  104. package/lib/utils/external-readme.js +125 -0
  105. package/lib/utils/help-builder.js +190 -0
  106. package/lib/utils/infra-status.js +13 -3
  107. package/lib/utils/paths.js +17 -2
  108. package/lib/utils/port-resolver.js +111 -0
  109. package/lib/utils/secrets-helpers.js +3 -15
  110. package/lib/utils/secrets-utils.js +2 -2
  111. package/lib/utils/token-manager.js +9 -4
  112. package/lib/utils/variable-transformer.js +7 -2
  113. package/lib/validation/external-manifest-validator.js +202 -0
  114. package/lib/validation/validate-display.js +406 -0
  115. package/lib/validation/validate.js +159 -123
  116. package/lib/validation/validator.js +36 -3
  117. package/lib/validation/wizard-config-validator.js +267 -0
  118. package/package.json +4 -2
  119. package/templates/applications/README.md.hbs +18 -16
  120. package/templates/applications/miso-controller/env.template +1 -1
  121. package/templates/applications/miso-controller/rbac.yaml +7 -7
  122. package/templates/external-system/README.md.hbs +99 -0
  123. package/templates/github/ci.yaml.hbs +44 -1
  124. package/templates/github/release.yaml.hbs +44 -0
  125. package/templates/infra/compose.yaml.hbs +35 -0
  126. package/templates/python/docker-compose.hbs +26 -0
  127. package/templates/typescript/docker-compose.hbs +26 -0
@@ -58,6 +58,32 @@ function formatPatternError(field, error) {
58
58
  return `${field}: Invalid value ${invalidValue} - ${patternDesc}`;
59
59
  }
60
60
 
61
+ /**
62
+ * Formats additionalProperties validation errors
63
+ * @function formatAdditionalPropertiesError
64
+ * @param {string} field - Field name
65
+ * @param {Object} error - Validation error object
66
+ * @returns {string} Formatted error message
67
+ */
68
+ function formatAdditionalPropertiesError(field, error) {
69
+ const invalidProperty = error.params?.additionalProperty;
70
+ const parentSchema = error.parentSchema || {};
71
+ const allowedProps = parentSchema.properties ? Object.keys(parentSchema.properties) : [];
72
+ const lines = [`${field}: must NOT have additional properties`];
73
+
74
+ if (invalidProperty) {
75
+ lines.push(` Invalid property: "${invalidProperty}" (not allowed)`);
76
+ }
77
+ if (allowedProps.length > 0) {
78
+ lines.push(` Allowed properties: ${allowedProps.join(', ')}`);
79
+ }
80
+ if ((error.instancePath || '').includes('/portalInput/validation')) {
81
+ lines.push(' Example: { "minLength": 1, "maxLength": 1000, "pattern": "^[0-9]+$", "required": false }');
82
+ }
83
+
84
+ return lines.join('\n');
85
+ }
86
+
61
87
  /**
62
88
  * Creates error message formatters for each validation keyword
63
89
  * @function createKeywordFormatters
@@ -113,6 +139,9 @@ function formatSingleError(error) {
113
139
  if (error.keyword === 'pattern') {
114
140
  return formatPatternError(field, error);
115
141
  }
142
+ if (error.keyword === 'additionalProperties') {
143
+ return formatAdditionalPropertiesError(field, error);
144
+ }
116
145
 
117
146
  // Use object lookup for keyword-specific messages
118
147
  const formatters = createKeywordFormatters(field, error);
@@ -142,9 +171,29 @@ function formatValidationErrors(errors) {
142
171
  return errors.map(formatSingleError);
143
172
  }
144
173
 
174
+ /**
175
+ * Formats the error when a required DB password variable is missing.
176
+ * Supports single-db (DB_0_PASSWORD or DB_PASSWORD) and multi-db (DB_0_PASSWORD, DB_1_PASSWORD, ...).
177
+ * @param {string} appKey - Application key
178
+ * @param {Object} opts - Options
179
+ * @param {boolean} [opts.multiDb] - True when multiple databases; required passwordKey is used, no hardcoded index
180
+ * @param {string} [opts.passwordKey] - The missing variable name (e.g. 'DB_1_PASSWORD'); required when multiDb is true
181
+ * @returns {string} Error message with next steps
182
+ */
183
+ function formatMissingDbPasswordError(appKey, opts = {}) {
184
+ const { multiDb, passwordKey } = opts;
185
+ if (multiDb && passwordKey) {
186
+ return 'Missing required password variable ' + passwordKey + ' in .env file for application \'' + appKey + '\'. ' +
187
+ 'Add ' + passwordKey + '=your_secret to your .env file. For multiple databases you need DB_0_PASSWORD, DB_1_PASSWORD, etc.';
188
+ }
189
+ return 'Missing required password variable DB_0_PASSWORD or DB_PASSWORD in .env file for application \'' + appKey + '\'. ' +
190
+ 'This app has requires.database or databases in variables.yaml. Add DB_0_PASSWORD=your_secret or DB_PASSWORD=your_secret to builder/' + appKey + '/.env (or run \'aifabrix resolve ' + appKey + '\'), or set requires.database: false in variables.yaml if not needed.';
191
+ }
192
+
145
193
  module.exports = {
146
194
  formatSingleError,
147
195
  formatValidationErrors,
196
+ formatMissingDbPasswordError,
148
197
  getPatternDescription,
149
198
  PATTERN_DESCRIPTIONS
150
199
  };
@@ -40,7 +40,10 @@ function addControllerUrlHeader(lines, errorData) {
40
40
  * @param {Object} errorData - Error response data
41
41
  */
42
42
  function addControllerUrlToMessage(lines, errorData) {
43
- if (errorData && errorData.controllerUrl) {
43
+ // Prefer showing full endpoint URL if available
44
+ if (errorData && errorData.endpointUrl) {
45
+ lines.push(chalk.gray(`Endpoint URL: ${errorData.endpointUrl}`));
46
+ } else if (errorData && errorData.controllerUrl) {
44
47
  lines.push(chalk.gray(`Controller URL: ${errorData.controllerUrl}`));
45
48
  }
46
49
  }
@@ -77,8 +80,15 @@ function formatHostnameNotFoundError(lines, errorData) {
77
80
  */
78
81
  function formatTimeoutError(lines, errorData) {
79
82
  lines.push(chalk.yellow('Request timed out.'));
80
- addControllerUrlToMessage(lines, errorData);
81
- lines.push(chalk.gray('The controller may be overloaded.'));
83
+
84
+ // Show full endpoint URL if available, otherwise show controller URL
85
+ if (errorData && errorData.endpointUrl) {
86
+ lines.push(chalk.gray(`Endpoint URL: ${errorData.endpointUrl}`));
87
+ } else if (errorData && errorData.controllerUrl) {
88
+ lines.push(chalk.gray(`Controller URL: ${errorData.controllerUrl}`));
89
+ }
90
+
91
+ lines.push(chalk.gray('The endpoint may not exist, the controller may be overloaded, or there may be a network issue.'));
82
92
  }
83
93
 
84
94
  /**
@@ -0,0 +1,125 @@
1
+ /**
2
+ * External System README Generation
3
+ *
4
+ * Provides a shared Handlebars-based README generator for external systems.
5
+ *
6
+ * @fileoverview External system README generation utilities
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const handlebars = require('handlebars');
16
+ const { getProjectRoot } = require('./paths');
17
+
18
+ /**
19
+ * Formats a display name from a key
20
+ * @param {string} key - System or app key
21
+ * @returns {string} Display name
22
+ */
23
+ function formatDisplayName(key) {
24
+ if (!key || typeof key !== 'string') {
25
+ return 'External System';
26
+ }
27
+ return key
28
+ .split('-')
29
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
30
+ .join(' ');
31
+ }
32
+
33
+ /**
34
+ * Normalizes datasource entries for template use
35
+ * @param {Array} datasources - Datasource objects
36
+ * @param {string} systemKey - System key for filename generation
37
+ * @returns {Array<{entityType: string, displayName: string, fileName: string}>} Normalized entries
38
+ */
39
+ function normalizeDatasources(datasources, systemKey) {
40
+ if (!Array.isArray(datasources)) {
41
+ return [];
42
+ }
43
+ return datasources.map((datasource, index) => {
44
+ const entityType = datasource.entityType ||
45
+ datasource.entityKey ||
46
+ datasource.key?.split('-').pop() ||
47
+ `entity${index + 1}`;
48
+ const displayName = datasource.displayName ||
49
+ datasource.name ||
50
+ `Datasource ${index + 1}`;
51
+ let fileName = datasource.fileName || datasource.file;
52
+ if (!fileName) {
53
+ const key = datasource.key || '';
54
+ // Extract entity from keys like "hubspot-deploy-company" -> "company"
55
+ const entity = (systemKey && key.startsWith(`${systemKey}-deploy-`))
56
+ ? key.slice(`${systemKey}-deploy-`.length)
57
+ : entityType;
58
+ fileName = systemKey ? `${systemKey}-datasource-${entity}.json` : `${entity}.json`;
59
+ }
60
+ return { entityType, displayName, fileName };
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Builds the external system README template context
66
+ * @function buildExternalReadmeContext
67
+ * @param {Object} params - Context parameters
68
+ * @param {string} [params.appName] - Application name
69
+ * @param {string} [params.systemKey] - System key
70
+ * @param {string} [params.systemType] - System type
71
+ * @param {string} [params.displayName] - Display name
72
+ * @param {string} [params.description] - Description
73
+ * @param {Array} [params.datasources] - Datasource objects
74
+ * @returns {Object} Template context
75
+ */
76
+ function buildExternalReadmeContext(params = {}) {
77
+ const appName = params.appName || params.systemKey || 'external-system';
78
+ const systemKey = params.systemKey || appName;
79
+ const displayName = params.displayName || formatDisplayName(systemKey);
80
+ const description = params.description || `External system integration for ${systemKey}`;
81
+ const systemType = params.systemType || 'openapi';
82
+ const datasources = normalizeDatasources(params.datasources, systemKey);
83
+
84
+ return {
85
+ appName,
86
+ systemKey,
87
+ displayName,
88
+ description,
89
+ systemType,
90
+ datasourceCount: datasources.length,
91
+ datasources
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Loads and compiles the external system README template
97
+ * @returns {Function} Compiled template
98
+ * @throws {Error} If template is missing
99
+ */
100
+ function loadExternalReadmeTemplate() {
101
+ const projectRoot = getProjectRoot();
102
+ const templatePath = path.join(projectRoot, 'templates', 'external-system', 'README.md.hbs');
103
+ if (!fs.existsSync(templatePath)) {
104
+ throw new Error(`External system README template not found at ${templatePath}`);
105
+ }
106
+ const content = fs.readFileSync(templatePath, 'utf8');
107
+ return handlebars.compile(content);
108
+ }
109
+
110
+ /**
111
+ * Generates README content for an external system
112
+ * @function generateExternalReadmeContent
113
+ * @param {Object} params - Context parameters
114
+ * @returns {string} README content
115
+ */
116
+ function generateExternalReadmeContent(params = {}) {
117
+ const template = loadExternalReadmeTemplate();
118
+ const context = buildExternalReadmeContext(params);
119
+ return template(context);
120
+ }
121
+
122
+ module.exports = {
123
+ buildExternalReadmeContext,
124
+ generateExternalReadmeContent
125
+ };
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Categorized help builder for AI Fabrix Builder CLI
3
+ *
4
+ * Groups commands into logical categories and outputs a user-friendly help.
5
+ *
6
+ * @fileoverview Categorized CLI help
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const { Help } = require('commander');
12
+
13
+ /**
14
+ * Command categories and order. Each command can have an optional `term` override
15
+ * for the help line (e.g. "down [app]"); otherwise the command name is used.
16
+ *
17
+ * @type {Array<{ name: string, commands: Array<{ name: string, term?: string }> }>}
18
+ */
19
+ const CATEGORIES = [
20
+ {
21
+ name: 'Infrastructure (Local Development)',
22
+ commands: [
23
+ { name: 'up' },
24
+ { name: 'down', term: 'down [app]' },
25
+ { name: 'doctor' },
26
+ { name: 'status' },
27
+ { name: 'restart', term: 'restart <service>' }
28
+ ]
29
+ },
30
+ {
31
+ name: 'Authentication',
32
+ commands: [
33
+ { name: 'login' },
34
+ { name: 'logout' },
35
+ { name: 'auth' }
36
+ ]
37
+ },
38
+ {
39
+ name: 'Applications (Create & Develop)',
40
+ commands: [
41
+ { name: 'create', term: 'create <app>' },
42
+ { name: 'wizard' },
43
+ { name: 'build', term: 'build <app>' },
44
+ { name: 'run', term: 'run <app>' },
45
+ { name: 'dockerfile', term: 'dockerfile <app>' }
46
+ ]
47
+ },
48
+ {
49
+ name: 'Deployment',
50
+ commands: [
51
+ { name: 'push', term: 'push <app>' },
52
+ { name: 'deploy', term: 'deploy <app>' }
53
+ ]
54
+ },
55
+ {
56
+ name: 'Environments',
57
+ commands: [
58
+ { name: 'environment' },
59
+ { name: 'env' }
60
+ ]
61
+ },
62
+ {
63
+ name: 'Application & Datasource Management',
64
+ commands: [
65
+ { name: 'app' },
66
+ { name: 'datasource' }
67
+ ]
68
+ },
69
+ {
70
+ name: 'Configuration & Validation',
71
+ commands: [
72
+ { name: 'resolve', term: 'resolve <app>' },
73
+ { name: 'json', term: 'json <app>' },
74
+ { name: 'split-json', term: 'split-json <app>' },
75
+ { name: 'genkey', term: 'genkey <app>' },
76
+ { name: 'validate', term: 'validate <appOrFile>' },
77
+ { name: 'diff', term: 'diff <file1> <file2>' }
78
+ ]
79
+ },
80
+ {
81
+ name: 'External Systems',
82
+ commands: [
83
+ { name: 'download', term: 'download <system-key>' },
84
+ { name: 'delete', term: 'delete <system-key>' },
85
+ { name: 'test', term: 'test <app>' },
86
+ { name: 'test-integration', term: 'test-integration <app>' }
87
+ ]
88
+ },
89
+ {
90
+ name: 'Developer & Secrets',
91
+ commands: [
92
+ { name: 'dev' },
93
+ { name: 'secrets' },
94
+ { name: 'secure' }
95
+ ]
96
+ }
97
+ ];
98
+
99
+ /**
100
+ * @param {object} helper - Commander Help instance
101
+ * @param {import('commander').Command} program
102
+ * @returns {string[]}
103
+ */
104
+ function formatHeader(helper, program) {
105
+ const out = [`Usage: ${helper.commandUsage(program)}`, ''];
106
+ const desc = helper.commandDescription(program);
107
+ if (desc && String(desc).length > 0) {
108
+ out.push(helper.wrap(String(desc), helper.helpWidth || 80, 0), '');
109
+ }
110
+ return out;
111
+ }
112
+
113
+ /**
114
+ * @param {object} helper - Commander Help instance
115
+ * @param {import('commander').Command} program
116
+ * @returns {string[]}
117
+ */
118
+ function formatOptions(helper, program) {
119
+ const options = helper.visibleOptions(program);
120
+ if (options.length === 0) return [];
121
+ const optTermWidth = options.reduce((max, o) => Math.max(max, helper.optionTerm(o).length), 0);
122
+ const out = ['Options:'];
123
+ for (const opt of options) {
124
+ out.push(` ${helper.optionTerm(opt).padEnd(optTermWidth + 2)}${helper.optionDescription(opt)}`);
125
+ }
126
+ out.push('');
127
+ return out;
128
+ }
129
+
130
+ /**
131
+ * @param {object} helper - Commander Help instance
132
+ * @param {import('commander').Command} program
133
+ * @returns {{ categorized: Array<{ name: string, lines: Array<{ term: string, desc: string }> }>, pad: number }}
134
+ */
135
+ function buildCategorizedWithPad(helper, program) {
136
+ const nameToCmd = new Map(program.commands.map((c) => [c.name(), c]));
137
+ const categorized = [];
138
+ let maxTermLen = 'help [command]'.length;
139
+
140
+ for (const cat of CATEGORIES) {
141
+ const lines = [];
142
+ for (const spec of cat.commands) {
143
+ const cmd = nameToCmd.get(spec.name);
144
+ if (!cmd) continue;
145
+ const term = spec.term || cmd.name();
146
+ maxTermLen = Math.max(maxTermLen, term.length);
147
+ const descText = helper.subcommandDescription(cmd) || cmd.description() || '';
148
+ lines.push({ term, desc: descText });
149
+ }
150
+ if (lines.length > 0) categorized.push({ name: cat.name, lines });
151
+ }
152
+ return { categorized, pad: maxTermLen + 2 };
153
+ }
154
+
155
+ /**
156
+ * @param {object} helper - Commander Help instance
157
+ * @param {import('commander').Command} program
158
+ * @returns {string[]}
159
+ */
160
+ function formatCommandCategories(helper, program) {
161
+ const { categorized, pad } = buildCategorizedWithPad(helper, program);
162
+ const out = [];
163
+ for (const { name, lines } of categorized) {
164
+ out.push(name + ':');
165
+ for (const { term, desc: d } of lines) out.push(` ${term.padEnd(pad)}${d}`);
166
+ out.push('');
167
+ }
168
+ out.push('Help:');
169
+ out.push(` ${'help [command]'.padEnd(pad)}display help for command`);
170
+ out.push('');
171
+ return out;
172
+ }
173
+
174
+ /**
175
+ * Build the full categorized help string for the program.
176
+ *
177
+ * @param {import('commander').Command} program - Commander program
178
+ * @returns {string} Formatted help text
179
+ */
180
+ function buildCategorizedHelp(program) {
181
+ const helper = new Help();
182
+ const output = [
183
+ ...formatHeader(helper, program),
184
+ ...formatOptions(helper, program),
185
+ ...formatCommandCategories(helper, program)
186
+ ];
187
+ return output.join('\n');
188
+ }
189
+
190
+ module.exports = { buildCategorizedHelp, CATEGORIES };
@@ -38,7 +38,11 @@ async function getInfraStatus() {
38
38
  postgres: { port: ports.postgres, url: `localhost:${ports.postgres}` },
39
39
  redis: { port: ports.redis, url: `localhost:${ports.redis}` },
40
40
  pgadmin: { port: ports.pgadmin, url: `http://localhost:${ports.pgadmin}` },
41
- 'redis-commander': { port: ports.redisCommander, url: `http://localhost:${ports.redisCommander}` }
41
+ 'redis-commander': { port: ports.redisCommander, url: `http://localhost:${ports.redisCommander}` },
42
+ traefik: {
43
+ port: `${ports.traefikHttp}/${ports.traefikHttps}`,
44
+ url: `http://localhost:${ports.traefikHttp}, https://localhost:${ports.traefikHttps}`
45
+ }
42
46
  };
43
47
 
44
48
  const status = {};
@@ -82,9 +86,15 @@ async function getInfraStatus() {
82
86
  */
83
87
  function getInfraContainerNames(devIdNum, devId) {
84
88
  if (devIdNum === 0) {
85
- return ['aifabrix-postgres', 'aifabrix-redis', 'aifabrix-pgadmin', 'aifabrix-redis-commander'];
89
+ return ['aifabrix-postgres', 'aifabrix-redis', 'aifabrix-pgadmin', 'aifabrix-redis-commander', 'aifabrix-traefik'];
86
90
  }
87
- return [`aifabrix-dev${devId}-postgres`, `aifabrix-dev${devId}-redis`, `aifabrix-dev${devId}-pgadmin`, `aifabrix-dev${devId}-redis-commander`];
91
+ return [
92
+ `aifabrix-dev${devId}-postgres`,
93
+ `aifabrix-dev${devId}-redis`,
94
+ `aifabrix-dev${devId}-pgadmin`,
95
+ `aifabrix-dev${devId}-redis-commander`,
96
+ `aifabrix-dev${devId}-traefik`
97
+ ];
88
98
  }
89
99
 
90
100
  /**
@@ -408,13 +408,28 @@ function checkBuilderFolder(appName) {
408
408
  * Checks both integration/ and builder/ folders for backward compatibility
409
409
  *
410
410
  * @param {string} appName - Application name
411
- * @returns {Promise<{isExternal: boolean, appPath: string, appType: string}>}
411
+ * @param {Object} [options] - Detection options
412
+ * @param {string} [options.type] - Forced application type (external)
413
+ * @returns {Promise<{isExternal: boolean, appPath: string, appType: string, baseDir?: string}>}
412
414
  */
413
- async function detectAppType(appName) {
415
+ async function detectAppType(appName, options = {}) {
414
416
  if (!appName || typeof appName !== 'string') {
415
417
  throw new Error('App name is required and must be a string');
416
418
  }
417
419
 
420
+ if (options.type === 'external') {
421
+ const integrationPath = getIntegrationPath(appName);
422
+ if (!fs.existsSync(integrationPath)) {
423
+ throw new Error(`External system not found in integration/${appName}`);
424
+ }
425
+ return {
426
+ isExternal: true,
427
+ appPath: integrationPath,
428
+ appType: 'external',
429
+ baseDir: 'integration'
430
+ };
431
+ }
432
+
418
433
  // Check integration folder first (new structure)
419
434
  const integrationResult = checkIntegrationFolder(appName);
420
435
  if (integrationResult) {
@@ -0,0 +1,111 @@
1
+ /**
2
+ * AI Fabrix Builder - Centralized Port Resolution
3
+ *
4
+ * Single source of truth for resolving application port from variables.yaml.
5
+ * Use getContainerPort for container/Docker/deployment/registration; use getLocalPort
6
+ * for local .env and dev-id–adjusted host port.
7
+ *
8
+ * @fileoverview Port resolution from variables (port, build.containerPort, build.localPort)
9
+ * @author AI Fabrix Team
10
+ * @version 2.0.0
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const yaml = require('js-yaml');
17
+
18
+ /**
19
+ * Resolve container port from variables object.
20
+ * Precedence: build.containerPort → port → defaultPort.
21
+ * Used for: Dockerfile, container .env PORT, compose, deployment, app register, variable-transformer, builders, secrets-utils.
22
+ *
23
+ * @param {Object} variables - Parsed variables.yaml (or subset with build, port)
24
+ * @param {number} [defaultPort=3000] - Default when neither build.containerPort nor port is set
25
+ * @returns {number} Resolved container port
26
+ */
27
+ function getContainerPort(variables, defaultPort = 3000) {
28
+ const v = variables || {};
29
+ return v.build?.containerPort ?? v.port ?? defaultPort;
30
+ }
31
+
32
+ /**
33
+ * Resolve local (development) port from variables object.
34
+ * Precedence: build.localPort (if number and > 0) → port → defaultPort.
35
+ * Used for: env-copy, env-ports, and as base for getLocalPortFromPath (secrets-helpers).
36
+ *
37
+ * @param {Object} variables - Parsed variables.yaml
38
+ * @param {number} [defaultPort=3000] - Default when neither build.localPort nor port is set
39
+ * @returns {number} Resolved local port
40
+ */
41
+ function getLocalPort(variables, defaultPort = 3000) {
42
+ const v = variables || {};
43
+ const local = v.build?.localPort;
44
+ if (typeof local === 'number' && local > 0) {
45
+ return local;
46
+ }
47
+ return v.port ?? defaultPort;
48
+ }
49
+
50
+ /**
51
+ * Load variables from path. Returns null if path missing, not found, or parse error.
52
+ *
53
+ * @param {string} variablesPath - Path to variables.yaml
54
+ * @returns {Object|null} Parsed variables or null
55
+ */
56
+ function loadVariablesFromPath(variablesPath) {
57
+ if (!variablesPath || !fs.existsSync(variablesPath)) {
58
+ return null;
59
+ }
60
+ try {
61
+ const content = fs.readFileSync(variablesPath, 'utf8');
62
+ return yaml.load(content) || null;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Resolve container port from variables.yaml path.
70
+ * Returns null when file is missing or neither build.containerPort nor port is set (for chaining with other sources).
71
+ *
72
+ * @param {string} variablesPath - Path to variables.yaml
73
+ * @returns {number|null} Container port or null
74
+ */
75
+ function getContainerPortFromPath(variablesPath) {
76
+ const v = loadVariablesFromPath(variablesPath);
77
+ if (!v) {
78
+ return null;
79
+ }
80
+ const p = v.build?.containerPort ?? v.port;
81
+ return (p !== undefined && p !== null) ? p : null;
82
+ }
83
+
84
+ /**
85
+ * Resolve local port from variables.yaml path.
86
+ * Matches legacy getPortFromVariablesFile: build.localPort (if number and > 0) else variables.port or null.
87
+ * Returns null when file is missing or neither is set (for calculateAppPort chain).
88
+ *
89
+ * @param {string} variablesPath - Path to variables.yaml
90
+ * @returns {number|null} Local port or null
91
+ */
92
+ function getLocalPortFromPath(variablesPath) {
93
+ const v = loadVariablesFromPath(variablesPath);
94
+ if (!v) {
95
+ return null;
96
+ }
97
+ const local = v.build?.localPort;
98
+ if (typeof local === 'number' && local > 0) {
99
+ return local;
100
+ }
101
+ const p = v.port;
102
+ return (p !== undefined && p !== null) ? p : null;
103
+ }
104
+
105
+ module.exports = {
106
+ getContainerPort,
107
+ getLocalPort,
108
+ getContainerPortFromPath,
109
+ getLocalPortFromPath,
110
+ loadVariablesFromPath
111
+ };
@@ -17,6 +17,7 @@ const { rewriteInfraEndpoints, getEnvHosts, getServicePort, getServiceHost, getL
17
17
  const { loadEnvConfig } = require('./env-config-loader');
18
18
  const { updateContainerPortInEnvFile } = require('./env-ports');
19
19
  const { buildEnvVarMap } = require('./env-map');
20
+ const { getLocalPortFromPath } = require('./port-resolver');
20
21
 
21
22
  /**
22
23
  * Interpolate ${VAR} occurrences with values from envVars map
@@ -145,26 +146,13 @@ function getPortFromLocalEnv(localEnv) {
145
146
  }
146
147
 
147
148
  /**
148
- * Gets port from variables.yaml file
149
+ * Gets port from variables.yaml file (build.localPort if positive, else port). Uses port-resolver.
149
150
  * @function getPortFromVariablesFile
150
151
  * @param {string} variablesPath - Path to variables.yaml
151
152
  * @returns {number|null} Port value or null
152
153
  */
153
154
  function getPortFromVariablesFile(variablesPath) {
154
- if (!variablesPath || !fs.existsSync(variablesPath)) {
155
- return null;
156
- }
157
- try {
158
- const variablesContent = fs.readFileSync(variablesPath, 'utf8');
159
- const variables = yaml.load(variablesContent);
160
- const localPort = variables?.build?.localPort;
161
- if (typeof localPort === 'number' && localPort > 0) {
162
- return localPort;
163
- }
164
- return variables?.port || null;
165
- } catch {
166
- return null;
167
- }
155
+ return getLocalPortFromPath(variablesPath);
168
156
  }
169
157
 
170
158
  /**
@@ -14,6 +14,7 @@ const path = require('path');
14
14
  const yaml = require('js-yaml');
15
15
  const logger = require('./logger');
16
16
  const pathsUtil = require('./paths');
17
+ const { getContainerPort } = require('./port-resolver');
17
18
 
18
19
  /**
19
20
  * Loads secrets from file with cascading lookup support
@@ -145,8 +146,7 @@ function resolveUrlPort(protocol, hostname, port, urlPath, hostnameToService) {
145
146
  const variablesContent = fs.readFileSync(serviceVariablesPath, 'utf8');
146
147
  const variables = yaml.load(variablesContent);
147
148
 
148
- // Get containerPort or fall back to port
149
- const containerPort = variables?.build?.containerPort || variables?.port || port;
149
+ const containerPort = getContainerPort(variables, port);
150
150
 
151
151
  // Replace port in URL
152
152
  return `${protocol}${hostname}:${containerPort}${urlPath}`;
@@ -321,7 +321,7 @@ async function getDeploymentAuth(controllerUrl, environment, appName) {
321
321
  async function extractClientCredentials(authConfig, appKey, envKey, _options = {}) {
322
322
  if (authConfig.type === 'client-credentials') {
323
323
  if (!authConfig.clientId || !authConfig.clientSecret) {
324
- throw new Error('Client ID and Client Secret are required');
324
+ throw new Error('Client ID and Client Secret are required for client-credentials authentication');
325
325
  }
326
326
  return {
327
327
  clientId: authConfig.clientId,
@@ -349,9 +349,14 @@ async function extractClientCredentials(authConfig, appKey, envKey, _options = {
349
349
  };
350
350
  }
351
351
 
352
- // Construct clientId from controller, environment, and application key
353
- // (not used, but shown in error message for reference)
354
- throw new Error(`Client ID and Client Secret are required. Add credentials to ~/.aifabrix/secrets.local.yaml as '${appKey}-client-idKeyVault' and '${appKey}-client-secretKeyVault', or use credentials authentication.`);
352
+ // No credentials found - provide helpful error message
353
+ throw new Error(
354
+ 'Client ID and Client Secret are required for deployment.\n' +
355
+ 'Add credentials to ~/.aifabrix/secrets.local.yaml as:\n' +
356
+ ` '${appKey}-client-idKeyVault': <client-id>\n` +
357
+ ` '${appKey}-client-secretKeyVault': <client-secret>\n\n` +
358
+ 'Or use credentials authentication with --client-id and --client-secret flags.'
359
+ );
355
360
  }
356
361
 
357
362
  throw new Error('Invalid authentication type');