@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.
- package/.cursor/rules/project-rules.mdc +8 -0
- package/README.md +36 -8
- package/bin/aifabrix.js +6 -8
- package/integration/hubspot/README.md +12 -11
- package/integration/hubspot/companies.json +2048 -0
- package/integration/hubspot/create-hubspot.js +665 -0
- package/integration/hubspot/{hubspot-deploy-company.json → hubspot-datasource-company.json} +1 -1
- package/integration/hubspot/{hubspot-deploy-contact.json → hubspot-datasource-contact.json} +1 -1
- package/integration/hubspot/{hubspot-deploy-deal.json → hubspot-datasource-deal.json} +1 -1
- package/integration/hubspot/hubspot-deploy.json +832 -81
- package/integration/hubspot/hubspot-system.json +99 -0
- package/integration/hubspot/test-artifacts/wizard-hubspot-credential-real.yaml +20 -0
- package/integration/hubspot/test-artifacts/wizard-hubspot-env-vars.yaml +9 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-add-datasource.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-app-name.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-credential-create.yaml +7 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-credential-select.yaml +7 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-known-platform.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-app.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-mode.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-openapi-file.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-openapi-url.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-source.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-array-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +5 -0
- package/integration/hubspot/test-dataplane-down-helpers.js +246 -0
- package/integration/hubspot/test-dataplane-down-tests.js +419 -0
- package/integration/hubspot/test-dataplane-down.js +157 -0
- package/integration/hubspot/test.js +1517 -0
- package/integration/hubspot/variables.yaml +4 -4
- package/integration/hubspot/wizard-hubspot-e2e.yaml +16 -0
- package/integration/hubspot/wizard-hubspot-platform.yaml +8 -0
- package/lib/api/applications.api.js +1 -0
- package/lib/api/index.js +6 -2
- package/lib/api/types/wizard.types.js +176 -38
- package/lib/api/wizard.api.js +161 -23
- package/lib/app/deploy.js +116 -54
- package/lib/app/display.js +6 -5
- package/lib/app/dockerfile.js +2 -1
- package/lib/app/list.js +17 -10
- package/lib/app/readme.js +41 -112
- package/lib/app/register.js +44 -9
- package/lib/app/rotate-secret.js +48 -31
- package/lib/cli.js +219 -70
- package/lib/commands/app.js +4 -9
- package/lib/commands/auth-config.js +125 -0
- package/lib/commands/auth-status.js +7 -8
- package/lib/commands/datasource.js +3 -6
- package/lib/commands/login-credentials.js +4 -4
- package/lib/commands/login-device.js +26 -17
- package/lib/commands/login.js +12 -10
- package/lib/commands/wizard-config-normalizer.js +92 -0
- package/lib/commands/wizard-core.js +515 -0
- package/lib/commands/wizard-dataplane.js +122 -0
- package/lib/commands/wizard-headless.js +115 -0
- package/lib/commands/wizard.js +110 -332
- package/lib/core/config.js +46 -0
- package/lib/core/secrets.js +3 -22
- package/lib/core/templates-env.js +1 -1
- package/lib/datasource/deploy.js +59 -23
- package/lib/datasource/list.js +108 -19
- package/lib/deployment/deployer.js +25 -0
- package/lib/deployment/environment.js +10 -13
- package/lib/external-system/delete.js +151 -0
- package/lib/external-system/deploy.js +53 -378
- package/lib/external-system/download-helpers.js +45 -65
- package/lib/external-system/download.js +33 -13
- package/lib/external-system/generator.js +11 -7
- package/lib/external-system/test-auth.js +4 -3
- package/lib/generator/builders.js +3 -1
- package/lib/generator/external-controller-manifest.js +157 -0
- package/lib/generator/external-schema-utils.js +236 -0
- package/lib/generator/external.js +55 -3
- package/lib/generator/index.js +22 -10
- package/lib/generator/wizard-prompts.js +33 -10
- package/lib/generator/wizard.js +69 -86
- package/lib/infrastructure/compose.js +100 -0
- package/lib/infrastructure/helpers.js +139 -0
- package/lib/infrastructure/index.js +52 -311
- package/lib/infrastructure/services.js +168 -0
- package/lib/schema/application-schema.json +23 -4
- package/lib/schema/external-datasource.schema.json +2 -2
- package/lib/schema/wizard-config.schema.json +234 -0
- package/lib/utils/api.js +102 -52
- package/lib/utils/app-existence.js +42 -0
- package/lib/utils/app-register-config.js +7 -2
- package/lib/utils/auth-config-validator.js +92 -0
- package/lib/utils/command-header.js +43 -0
- package/lib/utils/compose-generator.js +113 -70
- package/lib/utils/controller-url.js +65 -17
- package/lib/utils/dataplane-health.js +115 -0
- package/lib/utils/dataplane-resolver.js +29 -0
- package/lib/utils/dev-config.js +6 -2
- package/lib/utils/env-copy.js +2 -1
- package/lib/utils/env-ports.js +2 -1
- package/lib/utils/env-template.js +1 -1
- package/lib/utils/error-formatter.js +49 -0
- package/lib/utils/error-formatters/network-errors.js +13 -3
- package/lib/utils/external-readme.js +125 -0
- package/lib/utils/help-builder.js +190 -0
- package/lib/utils/infra-status.js +13 -3
- package/lib/utils/paths.js +17 -2
- package/lib/utils/port-resolver.js +111 -0
- package/lib/utils/secrets-helpers.js +3 -15
- package/lib/utils/secrets-utils.js +2 -2
- package/lib/utils/token-manager.js +9 -4
- package/lib/utils/variable-transformer.js +7 -2
- package/lib/validation/external-manifest-validator.js +202 -0
- package/lib/validation/validate-display.js +406 -0
- package/lib/validation/validate.js +159 -123
- package/lib/validation/validator.js +36 -3
- package/lib/validation/wizard-config-validator.js +267 -0
- package/package.json +4 -2
- package/templates/applications/README.md.hbs +18 -16
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/applications/miso-controller/rbac.yaml +7 -7
- package/templates/external-system/README.md.hbs +99 -0
- package/templates/github/ci.yaml.hbs +44 -1
- package/templates/github/release.yaml.hbs +44 -0
- package/templates/infra/compose.yaml.hbs +35 -0
- package/templates/python/docker-compose.hbs +26 -0
- 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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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 [
|
|
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
|
/**
|
package/lib/utils/paths.js
CHANGED
|
@@ -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
|
-
* @
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
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');
|