@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.
- package/.cursor/rules/project-rules.mdc +8 -0
- package/README.md +36 -8
- package/bin/aifabrix.js +6 -8
- package/integration/hubspot/README.md +8 -7
- 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 +10 -5
- package/lib/api/types/wizard.types.js +176 -38
- package/lib/api/wizard.api.js +207 -38
- 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 +78 -37
- package/lib/app/prompts.js +9 -5
- package/lib/app/readme.js +41 -112
- package/lib/app/register.js +44 -9
- package/lib/app/rotate-secret.js +50 -32
- package/lib/cli.js +243 -65
- package/lib/commands/app.js +4 -9
- package/lib/commands/auth-config.js +125 -0
- package/lib/commands/auth-status.js +261 -0
- package/lib/commands/datasource.js +3 -6
- package/lib/commands/login-credentials.js +4 -4
- package/lib/commands/login-device.js +43 -29
- package/lib/commands/login.js +22 -13
- 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 +129 -357
- 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 +34 -23
- package/lib/datasource/list.js +8 -6
- 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 +54 -378
- package/lib/external-system/download-helpers.js +45 -65
- package/lib/external-system/download.js +34 -13
- package/lib/external-system/generator.js +11 -7
- package/lib/external-system/test-auth.js +5 -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 +24 -5
- package/lib/schema/external-datasource.schema.json +303 -17
- package/lib/schema/external-system.schema.json +1 -1
- package/lib/schema/wizard-config.schema.json +234 -0
- package/lib/utils/api.js +37 -42
- package/lib/utils/app-existence.js +42 -0
- package/lib/utils/app-register-config.js +7 -2
- package/lib/utils/app-register-display.js +2 -1
- package/lib/utils/auth-config-validator.js +92 -0
- package/lib/utils/cli-utils.js +3 -1
- package/lib/utils/command-header.js +43 -0
- package/lib/utils/compose-generator.js +113 -70
- package/lib/utils/controller-url.js +115 -0
- 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-map.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 +149 -28
- 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 +69 -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 +38 -4
- package/lib/validation/wizard-config-validator.js +267 -0
- package/package.json +4 -2
- package/templates/applications/README.md.hbs +19 -17
- 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/external-system/external-system.json.hbs +1 -1
- package/templates/infra/compose.yaml.hbs +35 -0
- package/templates/python/docker-compose.hbs +26 -0
- package/templates/typescript/docker-compose.hbs +26 -0
package/lib/utils/api.js
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
const { parseErrorResponse } = require('./api-error-handler');
|
|
14
14
|
const auditLogger = require('../core/audit-logger');
|
|
15
15
|
|
|
16
|
+
/** Default timeout for HTTP requests (ms). Prevents hanging when the controller is unreachable. */
|
|
17
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 5000;
|
|
18
|
+
|
|
16
19
|
/**
|
|
17
20
|
* Logs API request performance metrics and errors to audit log
|
|
18
21
|
* @param {Object} params - Performance logging parameters
|
|
@@ -166,15 +169,20 @@ async function handleNetworkError(error, url, options, duration) {
|
|
|
166
169
|
|
|
167
170
|
/**
|
|
168
171
|
* Make an API call with proper error handling
|
|
172
|
+
* Uses a 15s timeout to avoid hanging when the controller is unreachable.
|
|
169
173
|
* @param {string} url - API endpoint URL
|
|
170
|
-
* @param {Object} options - Fetch options
|
|
174
|
+
* @param {Object} options - Fetch options (signal, method, headers, body, etc.)
|
|
171
175
|
* @returns {Promise<Object>} Response object with success flag
|
|
172
176
|
*/
|
|
173
177
|
async function makeApiCall(url, options = {}) {
|
|
174
178
|
const startTime = Date.now();
|
|
179
|
+
const fetchOptions = { ...options };
|
|
180
|
+
if (!fetchOptions.signal) {
|
|
181
|
+
fetchOptions.signal = AbortSignal.timeout(DEFAULT_REQUEST_TIMEOUT_MS);
|
|
182
|
+
}
|
|
175
183
|
|
|
176
184
|
try {
|
|
177
|
-
const response = await fetch(url,
|
|
185
|
+
const response = await fetch(url, fetchOptions);
|
|
178
186
|
const duration = Date.now() - startTime;
|
|
179
187
|
|
|
180
188
|
if (!response.ok) {
|
|
@@ -182,8 +190,13 @@ async function makeApiCall(url, options = {}) {
|
|
|
182
190
|
}
|
|
183
191
|
|
|
184
192
|
return await handleSuccessResponse(response, url, options, duration);
|
|
185
|
-
} catch (
|
|
193
|
+
} catch (err) {
|
|
186
194
|
const duration = Date.now() - startTime;
|
|
195
|
+
const error = err?.name === 'AbortError'
|
|
196
|
+
? new Error(
|
|
197
|
+
`Request timed out after ${DEFAULT_REQUEST_TIMEOUT_MS / 1000} seconds. The controller may be unreachable. Check the URL and network.`
|
|
198
|
+
)
|
|
199
|
+
: err;
|
|
187
200
|
return await handleNetworkError(error, url, options, duration);
|
|
188
201
|
}
|
|
189
202
|
}
|
|
@@ -209,15 +222,21 @@ function extractControllerUrl(url) {
|
|
|
209
222
|
* Automatically refreshes device token on 401 errors if refresh token is available
|
|
210
223
|
* @param {string} url - API endpoint URL
|
|
211
224
|
* @param {Object} options - Fetch options
|
|
212
|
-
* @param {string}
|
|
225
|
+
* @param {string|Object} tokenOrAuthConfig - Bearer token string or authConfig object
|
|
226
|
+
* @param {string} [tokenOrAuthConfig.token] - Bearer token (if object)
|
|
227
|
+
* @param {string} [tokenOrAuthConfig.controller] - Controller URL for token refresh (if object)
|
|
213
228
|
* @returns {Promise<Object>} Response object
|
|
214
229
|
*/
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
230
|
+
// eslint-disable-next-line max-statements
|
|
231
|
+
async function authenticatedApiCall(url, options = {}, tokenOrAuthConfig) {
|
|
232
|
+
const isStringToken = typeof tokenOrAuthConfig === 'string';
|
|
233
|
+
const token = isStringToken ? tokenOrAuthConfig : tokenOrAuthConfig?.token;
|
|
234
|
+
const authControllerUrl = isStringToken ? null : tokenOrAuthConfig?.controller;
|
|
235
|
+
const isFormData = typeof FormData !== 'undefined' && options.body instanceof FormData;
|
|
236
|
+
const headers = { ...options.headers };
|
|
237
|
+
if (!isFormData && !headers['Content-Type']) {
|
|
238
|
+
headers['Content-Type'] = 'application/json';
|
|
239
|
+
}
|
|
221
240
|
if (token) {
|
|
222
241
|
headers['Authorization'] = `Bearer ${token}`;
|
|
223
242
|
}
|
|
@@ -227,43 +246,19 @@ async function authenticatedApiCall(url, options = {}, token) {
|
|
|
227
246
|
headers
|
|
228
247
|
});
|
|
229
248
|
|
|
230
|
-
// Handle 401 errors with automatic token refresh for device tokens
|
|
231
249
|
if (!response.success && response.status === 401) {
|
|
232
250
|
try {
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
// Try to get and refresh device token
|
|
237
|
-
const { getOrRefreshDeviceToken } = require('./token-manager');
|
|
238
|
-
const refreshedToken = await getOrRefreshDeviceToken(controllerUrl);
|
|
239
|
-
|
|
240
|
-
if (refreshedToken && refreshedToken.token) {
|
|
241
|
-
// Retry request with new token
|
|
251
|
+
const { forceRefreshDeviceToken } = require('./token-manager');
|
|
252
|
+
const refreshedToken = await forceRefreshDeviceToken(authControllerUrl || extractControllerUrl(url));
|
|
253
|
+
if (refreshedToken?.token) {
|
|
242
254
|
headers['Authorization'] = `Bearer ${refreshedToken.token}`;
|
|
243
|
-
|
|
244
|
-
...options,
|
|
245
|
-
headers
|
|
246
|
-
});
|
|
247
|
-
return retryResponse;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Token refresh failed or no refresh token available
|
|
251
|
-
// Return a more helpful error message
|
|
252
|
-
if (!refreshedToken) {
|
|
253
|
-
return {
|
|
254
|
-
...response,
|
|
255
|
-
error: 'Authentication failed: Token expired and refresh failed. Please login again using: aifabrix login',
|
|
256
|
-
formattedError: 'Authentication failed: Token expired and refresh failed. Please login again using: aifabrix login'
|
|
257
|
-
};
|
|
255
|
+
return await makeApiCall(url, { ...options, headers });
|
|
258
256
|
}
|
|
257
|
+
const authError = 'Authentication failed: Token expired and refresh failed. Please login again using: aifabrix login';
|
|
258
|
+
return { ...response, error: authError, formattedError: authError };
|
|
259
259
|
} catch (refreshError) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return {
|
|
263
|
-
...response,
|
|
264
|
-
error: `Authentication failed: ${errorMessage}. Please login again using: aifabrix login`,
|
|
265
|
-
formattedError: `Authentication failed: ${errorMessage}. Please login again using: aifabrix login`
|
|
266
|
-
};
|
|
260
|
+
const authError = `Authentication failed: ${refreshError.message || String(refreshError)}. Please login again using: aifabrix login`;
|
|
261
|
+
return { ...response, error: authError, formattedError: authError };
|
|
267
262
|
}
|
|
268
263
|
}
|
|
269
264
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Existence Check Utility
|
|
3
|
+
*
|
|
4
|
+
* Checks if an application exists in an environment before deployment.
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Application existence checking for AI Fabrix Builder
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { getEnvironmentApplication } = require('../api/environments.api');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if application exists in the environment
|
|
15
|
+
* Uses device token auth to check existence (doesn't require app credentials)
|
|
16
|
+
* @async
|
|
17
|
+
* @function checkApplicationExists
|
|
18
|
+
* @param {string} appKey - Application key
|
|
19
|
+
* @param {string} controllerUrl - Controller URL
|
|
20
|
+
* @param {string} envKey - Environment key
|
|
21
|
+
* @param {Object} authConfig - Authentication configuration (device token)
|
|
22
|
+
* @returns {Promise<boolean>} True if application exists, false otherwise
|
|
23
|
+
*/
|
|
24
|
+
async function checkApplicationExists(appKey, controllerUrl, envKey, authConfig) {
|
|
25
|
+
try {
|
|
26
|
+
// Use device token auth (bearer token) to check if app exists
|
|
27
|
+
// This doesn't require app credentials, so it works even if credentials are wrong
|
|
28
|
+
const deviceAuthConfig = { type: 'bearer', token: authConfig.token };
|
|
29
|
+
const response = await getEnvironmentApplication(controllerUrl, envKey, appKey, deviceAuthConfig);
|
|
30
|
+
return response.success && response.data !== null && response.data !== undefined;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
// If 404, application doesn't exist
|
|
33
|
+
if (error.status === 404 || (error.response && error.response.status === 404)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
// For other errors (including 401 if device token is invalid), we can't determine existence
|
|
37
|
+
// Return false to avoid blocking deployment - the validation step will catch credential issues
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { checkApplicationExists };
|
|
@@ -14,6 +14,7 @@ const chalk = require('chalk');
|
|
|
14
14
|
const yaml = require('js-yaml');
|
|
15
15
|
const logger = require('./logger');
|
|
16
16
|
const { detectAppType } = require('./paths');
|
|
17
|
+
const { getContainerPort, getLocalPort } = require('./port-resolver');
|
|
17
18
|
|
|
18
19
|
// createApp is imported dynamically in createMinimalAppIfNeeded to handle test mocking
|
|
19
20
|
|
|
@@ -238,9 +239,11 @@ async function extractExternalAppConfiguration(appKey, variables, appKeyFromFile
|
|
|
238
239
|
function extractWebappConfiguration(variables, appKeyFromFile, displayName, description, options) {
|
|
239
240
|
const appType = variables.build?.language === 'typescript' ? 'webapp' : 'service';
|
|
240
241
|
const registryMode = variables.image?.registryMode || 'external';
|
|
241
|
-
const port =
|
|
242
|
+
const port = options.port ?? getContainerPort(variables, 3000);
|
|
243
|
+
const localPort = getLocalPort(variables, port);
|
|
242
244
|
const language = variables.build?.language || 'typescript';
|
|
243
245
|
const image = buildImageReference(variables, appKeyFromFile);
|
|
246
|
+
const url = variables.app?.url || variables.deployment?.dataplaneUrl || variables.deployment?.appUrl || null;
|
|
244
247
|
|
|
245
248
|
return {
|
|
246
249
|
appKey: appKeyFromFile,
|
|
@@ -249,8 +252,10 @@ function extractWebappConfiguration(variables, appKeyFromFile, displayName, desc
|
|
|
249
252
|
appType,
|
|
250
253
|
registryMode,
|
|
251
254
|
port,
|
|
255
|
+
localPort,
|
|
252
256
|
image,
|
|
253
|
-
language
|
|
257
|
+
language,
|
|
258
|
+
url
|
|
254
259
|
};
|
|
255
260
|
}
|
|
256
261
|
|
|
@@ -48,7 +48,8 @@ function displayRegistrationResults(data, apiUrl, environment) {
|
|
|
48
48
|
logger.log(chalk.bold('📋 Application Details:'));
|
|
49
49
|
logger.log(` ID: ${data.application.id}`);
|
|
50
50
|
logger.log(` Key: ${data.application.key}`);
|
|
51
|
-
logger.log(` Display Name: ${data.application.displayName}
|
|
51
|
+
logger.log(` Display Name: ${data.application.displayName}`);
|
|
52
|
+
logger.log(` Controller: ${apiUrl}\n`);
|
|
52
53
|
|
|
53
54
|
logger.log(chalk.bold.yellow('🔑 CREDENTIALS (save these immediately):'));
|
|
54
55
|
logger.log(chalk.yellow(` Client ID: ${data.credentials.clientId}`));
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication Configuration Validators
|
|
3
|
+
*
|
|
4
|
+
* Provides validation functions for authentication configuration commands
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Authentication configuration validators
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { getControllerUrlFromLoggedInUser } = require('./controller-url');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validate controller URL format
|
|
15
|
+
* @function validateControllerUrl
|
|
16
|
+
* @param {string} url - Controller URL to validate
|
|
17
|
+
* @throws {Error} If URL format is invalid
|
|
18
|
+
*/
|
|
19
|
+
function validateControllerUrl(url) {
|
|
20
|
+
if (!url || typeof url !== 'string') {
|
|
21
|
+
throw new Error('Controller URL is required and must be a string');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const trimmed = url.trim();
|
|
25
|
+
if (trimmed.length === 0) {
|
|
26
|
+
throw new Error('Controller URL cannot be empty');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Basic URL validation - must start with http:// or https://
|
|
30
|
+
if (!trimmed.match(/^https?:\/\//)) {
|
|
31
|
+
throw new Error('Controller URL must start with http:// or https://');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// Use URL constructor for more thorough validation
|
|
36
|
+
new URL(trimmed);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new Error(`Invalid controller URL format: ${error.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate environment key format
|
|
44
|
+
* @function validateEnvironment
|
|
45
|
+
* @param {string} env - Environment key to validate
|
|
46
|
+
* @throws {Error} If environment format is invalid
|
|
47
|
+
*/
|
|
48
|
+
function validateEnvironment(env) {
|
|
49
|
+
if (!env || typeof env !== 'string') {
|
|
50
|
+
throw new Error('Environment is required and must be a string');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const trimmed = env.trim();
|
|
54
|
+
if (trimmed.length === 0) {
|
|
55
|
+
throw new Error('Environment cannot be empty');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Environment key must contain only letters, numbers, hyphens, and underscores
|
|
59
|
+
if (!/^[a-z0-9-_]+$/i.test(trimmed)) {
|
|
60
|
+
throw new Error('Environment must contain only letters, numbers, hyphens, and underscores');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if user is logged in to a controller
|
|
66
|
+
* @async
|
|
67
|
+
* @function checkUserLoggedIn
|
|
68
|
+
* @param {string} controllerUrl - Controller URL to check
|
|
69
|
+
* @returns {Promise<boolean>} True if user has device token for this controller
|
|
70
|
+
*/
|
|
71
|
+
async function checkUserLoggedIn(controllerUrl) {
|
|
72
|
+
if (!controllerUrl) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const normalizedUrl = controllerUrl.trim().replace(/\/+$/, '');
|
|
77
|
+
const loggedInControllerUrl = await getControllerUrlFromLoggedInUser();
|
|
78
|
+
|
|
79
|
+
if (!loggedInControllerUrl) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Normalize both URLs for comparison
|
|
84
|
+
const normalizedLoggedIn = loggedInControllerUrl.trim().replace(/\/+$/, '');
|
|
85
|
+
return normalizedLoggedIn === normalizedUrl;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
validateControllerUrl,
|
|
90
|
+
validateEnvironment,
|
|
91
|
+
checkUserLoggedIn
|
|
92
|
+
};
|
package/lib/utils/cli-utils.js
CHANGED
|
@@ -135,7 +135,9 @@ function formatAzureError(errorMsg) {
|
|
|
135
135
|
' Use format: *.azurecr.io (e.g., myacr.azurecr.io)'
|
|
136
136
|
];
|
|
137
137
|
}
|
|
138
|
-
|
|
138
|
+
// Only match ACR-specific authentication errors, not general authentication failures
|
|
139
|
+
if (errorMsg.includes('ACR') || errorMsg.includes('azurecr.io') ||
|
|
140
|
+
(errorMsg.includes('authenticate') && (errorMsg.includes('registry') || errorMsg.includes('container')))) {
|
|
139
141
|
return [
|
|
140
142
|
' Azure Container Registry authentication failed.',
|
|
141
143
|
' Run: az acr login --name <registry-name>',
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Header Display Utility
|
|
3
|
+
*
|
|
4
|
+
* Displays active configuration (controller, environment, dataplane) at top of commands
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Command header display utility
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
const logger = require('./logger');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Display command header with active configuration
|
|
16
|
+
* @function displayCommandHeader
|
|
17
|
+
* @param {string} controllerUrl - Controller URL
|
|
18
|
+
* @param {string} environment - Environment key
|
|
19
|
+
* @param {string} [dataplaneUrl] - Dataplane URL (optional)
|
|
20
|
+
*/
|
|
21
|
+
function displayCommandHeader(controllerUrl, environment, dataplaneUrl) {
|
|
22
|
+
const parts = [];
|
|
23
|
+
|
|
24
|
+
if (controllerUrl) {
|
|
25
|
+
parts.push(`Controller: ${chalk.cyan(controllerUrl)}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (environment) {
|
|
29
|
+
parts.push(`Environment: ${chalk.cyan(environment)}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (dataplaneUrl) {
|
|
33
|
+
parts.push(`Dataplane: ${chalk.cyan(dataplaneUrl)}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (parts.length > 0) {
|
|
37
|
+
logger.log(chalk.gray(`\n${parts.join(' | ')}\n`));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
displayCommandHeader
|
|
43
|
+
};
|
|
@@ -15,6 +15,8 @@ const path = require('path');
|
|
|
15
15
|
const handlebars = require('handlebars');
|
|
16
16
|
const config = require('../core/config');
|
|
17
17
|
const buildCopy = require('./build-copy');
|
|
18
|
+
const { formatMissingDbPasswordError } = require('./error-formatter');
|
|
19
|
+
const { getContainerPort } = require('./port-resolver');
|
|
18
20
|
|
|
19
21
|
// Register commonly used helpers
|
|
20
22
|
handlebars.registerHelper('eq', (a, b) => a === b);
|
|
@@ -150,6 +152,71 @@ function buildHealthCheckConfig(config) {
|
|
|
150
152
|
};
|
|
151
153
|
}
|
|
152
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Derives base path from routing pattern by removing trailing wildcards
|
|
157
|
+
* @param {string} pattern - URL pattern (e.g., '/app/*', '/api/v1/*')
|
|
158
|
+
* @returns {string} Base path for routing
|
|
159
|
+
*/
|
|
160
|
+
function derivePathFromPattern(pattern) {
|
|
161
|
+
if (!pattern || typeof pattern !== 'string') {
|
|
162
|
+
return '/';
|
|
163
|
+
}
|
|
164
|
+
const trimmed = pattern.trim();
|
|
165
|
+
if (trimmed === '/' || trimmed === '') {
|
|
166
|
+
return '/';
|
|
167
|
+
}
|
|
168
|
+
const withoutWildcards = trimmed.replace(/\*+$/g, '');
|
|
169
|
+
const withoutTrailingSlashes = withoutWildcards.replace(/\/+$/g, '');
|
|
170
|
+
return withoutTrailingSlashes || '/';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Builds developer username from developer ID
|
|
175
|
+
* @param {string|number} devId - Developer ID
|
|
176
|
+
* @returns {string} Developer username (dev, dev01, dev02, ...)
|
|
177
|
+
*/
|
|
178
|
+
function buildDevUsername(devId) {
|
|
179
|
+
if (devId === undefined || devId === null) {
|
|
180
|
+
return 'dev';
|
|
181
|
+
}
|
|
182
|
+
const devIdString = String(devId);
|
|
183
|
+
if (devIdString === '0') {
|
|
184
|
+
return 'dev';
|
|
185
|
+
}
|
|
186
|
+
const paddedId = devIdString.length === 1 ? devIdString.padStart(2, '0') : devIdString;
|
|
187
|
+
return `dev${paddedId}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Builds Traefik ingress configuration from frontDoorRouting
|
|
192
|
+
* Resolves ${DEV_USERNAME} variable interpolation in host field
|
|
193
|
+
* @param {Object} config - Application configuration
|
|
194
|
+
* @param {string|number} devId - Developer ID
|
|
195
|
+
* @returns {Object} Traefik configuration object
|
|
196
|
+
*/
|
|
197
|
+
function buildTraefikConfig(config, devId) {
|
|
198
|
+
const frontDoor = config.frontDoorRouting;
|
|
199
|
+
if (!frontDoor || frontDoor.enabled !== true) {
|
|
200
|
+
return { enabled: false };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!frontDoor.host || typeof frontDoor.host !== 'string') {
|
|
204
|
+
throw new Error('frontDoorRouting.host is required when frontDoorRouting.enabled is true');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const devUsername = buildDevUsername(devId);
|
|
208
|
+
const host = frontDoor.host.replace(/\$\{DEV_USERNAME\}/g, devUsername);
|
|
209
|
+
const path = derivePathFromPattern(frontDoor.pattern);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
enabled: true,
|
|
213
|
+
host,
|
|
214
|
+
path,
|
|
215
|
+
tls: frontDoor.tls !== false,
|
|
216
|
+
certStore: frontDoor.certStore || null
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
153
220
|
/**
|
|
154
221
|
* Builds requires configuration section
|
|
155
222
|
* @param {Object} config - Application configuration
|
|
@@ -169,17 +236,12 @@ function buildRequiresConfig(config) {
|
|
|
169
236
|
* @param {string} appName - Application name
|
|
170
237
|
* @param {Object} config - Application configuration
|
|
171
238
|
* @param {number} port - Application port
|
|
239
|
+
* @param {string|number} devId - Developer ID
|
|
172
240
|
* @returns {Object} Service configuration
|
|
173
241
|
*/
|
|
174
|
-
function buildServiceConfig(appName, config, port) {
|
|
175
|
-
|
|
176
|
-
// Container port should remain unchanged regardless of developer ID
|
|
177
|
-
const containerPortValue = config.build?.containerPort || config.port || 3000;
|
|
178
|
-
|
|
179
|
-
// Host port: use port parameter (already calculated from CLI --port or config.port in generateDockerCompose)
|
|
180
|
-
// Note: build.localPort is ONLY used for .env file PORT variable (for local PC dev), NOT for Docker Compose
|
|
242
|
+
function buildServiceConfig(appName, config, port, devId) {
|
|
243
|
+
const containerPortValue = getContainerPort(config, 3000);
|
|
181
244
|
const hostPort = port;
|
|
182
|
-
|
|
183
245
|
return {
|
|
184
246
|
app: buildAppConfig(appName, config),
|
|
185
247
|
image: buildImageConfig(config, appName),
|
|
@@ -190,6 +252,7 @@ function buildServiceConfig(appName, config, port) {
|
|
|
190
252
|
localPort: config.build?.localPort || null // Only used for .env file PORT variable, not for Docker Compose
|
|
191
253
|
},
|
|
192
254
|
healthCheck: buildHealthCheckConfig(config),
|
|
255
|
+
traefik: buildTraefikConfig(config, devId),
|
|
193
256
|
...buildRequiresConfig(config)
|
|
194
257
|
};
|
|
195
258
|
}
|
|
@@ -200,11 +263,7 @@ function buildServiceConfig(appName, config, port) {
|
|
|
200
263
|
* @returns {Object} Volumes configuration
|
|
201
264
|
*/
|
|
202
265
|
function buildVolumesConfig(appName) {
|
|
203
|
-
|
|
204
|
-
const volumePath = path.join(process.cwd(), 'data', appName);
|
|
205
|
-
return {
|
|
206
|
-
mountVolume: volumePath.replace(/\\/g, '/')
|
|
207
|
-
};
|
|
266
|
+
return { mountVolume: path.join(process.cwd(), 'data', appName).replace(/\\/g, '/') };
|
|
208
267
|
}
|
|
209
268
|
|
|
210
269
|
/**
|
|
@@ -213,23 +272,9 @@ function buildVolumesConfig(appName) {
|
|
|
213
272
|
* @returns {Object} Networks configuration
|
|
214
273
|
*/
|
|
215
274
|
function buildNetworksConfig(config) {
|
|
216
|
-
|
|
217
|
-
const databases = config.requires?.databases || config.databases || [];
|
|
218
|
-
return {
|
|
219
|
-
databases: databases
|
|
220
|
-
};
|
|
275
|
+
return { databases: config.requires?.databases || config.databases || [] };
|
|
221
276
|
}
|
|
222
277
|
|
|
223
|
-
/**
|
|
224
|
-
* Reads database passwords from .env file
|
|
225
|
-
* Requires DB_0_PASSWORD, DB_1_PASSWORD, etc. to be set in .env file
|
|
226
|
-
* @async
|
|
227
|
-
* @param {string} envPath - Path to .env file
|
|
228
|
-
* @param {Array<Object>} databases - Array of database configurations
|
|
229
|
-
* @param {string} appKey - Application key (fallback for single database)
|
|
230
|
-
* @returns {Promise<Object>} Object with passwords array and lookup map
|
|
231
|
-
* @throws {Error} If required password variables are missing
|
|
232
|
-
*/
|
|
233
278
|
/**
|
|
234
279
|
* Reads and parses .env file
|
|
235
280
|
* @async
|
|
@@ -276,17 +321,21 @@ async function readEnvFile(envPath) {
|
|
|
276
321
|
* @function extractPassword
|
|
277
322
|
* @param {Object} envVars - Environment variables
|
|
278
323
|
* @param {string} passwordKey - Password key to look up
|
|
324
|
+
* @param {Object} [context] - Optional: { appKey, multi } for clearer error messages
|
|
279
325
|
* @returns {string} Password value
|
|
280
326
|
* @throws {Error} If password is missing or empty
|
|
281
327
|
*/
|
|
282
|
-
function extractPassword(envVars, passwordKey) {
|
|
328
|
+
function extractPassword(envVars, passwordKey, context = {}) {
|
|
329
|
+
const { appKey, multi } = context;
|
|
330
|
+
const appSuffix = appKey ? ` for application '${appKey}'` : '';
|
|
331
|
+
|
|
283
332
|
if (!(passwordKey in envVars)) {
|
|
284
|
-
throw new Error(
|
|
333
|
+
throw new Error(multi && appKey ? formatMissingDbPasswordError(appKey, { multiDb: true, passwordKey }) : 'Missing required password variable ' + passwordKey + ' in .env file' + appSuffix + '. Add ' + passwordKey + '=your_secret to your .env file.');
|
|
285
334
|
}
|
|
286
335
|
|
|
287
336
|
const password = envVars[passwordKey].trim();
|
|
288
337
|
if (!password || password.length === 0) {
|
|
289
|
-
throw new Error(
|
|
338
|
+
throw new Error('Password variable ' + passwordKey + ' is empty in .env file' + appSuffix + '. Set a non-empty value.');
|
|
290
339
|
}
|
|
291
340
|
|
|
292
341
|
return password;
|
|
@@ -303,17 +352,14 @@ function extractPassword(envVars, passwordKey) {
|
|
|
303
352
|
function processMultipleDatabases(databases, envVars, appKey) {
|
|
304
353
|
const passwords = {};
|
|
305
354
|
const passwordsArray = [];
|
|
306
|
-
|
|
307
355
|
for (let i = 0; i < databases.length; i++) {
|
|
308
356
|
const db = databases[i];
|
|
309
357
|
const dbName = db.name || appKey;
|
|
310
358
|
const passwordKey = `DB_${i}_PASSWORD`;
|
|
311
|
-
const password = extractPassword(envVars, passwordKey);
|
|
312
|
-
|
|
359
|
+
const password = extractPassword(envVars, passwordKey, { appKey, multi: true });
|
|
313
360
|
passwords[dbName] = password;
|
|
314
361
|
passwordsArray.push(password);
|
|
315
362
|
}
|
|
316
|
-
|
|
317
363
|
return { passwords, passwordsArray };
|
|
318
364
|
}
|
|
319
365
|
|
|
@@ -327,47 +373,36 @@ function processMultipleDatabases(databases, envVars, appKey) {
|
|
|
327
373
|
function processSingleDatabase(envVars, appKey) {
|
|
328
374
|
const passwords = {};
|
|
329
375
|
const passwordsArray = [];
|
|
330
|
-
|
|
331
|
-
// Single database case - use DB_0_PASSWORD or DB_PASSWORD
|
|
332
376
|
const passwordKey = ('DB_0_PASSWORD' in envVars) ? 'DB_0_PASSWORD' : 'DB_PASSWORD';
|
|
333
|
-
|
|
334
377
|
if (!(passwordKey in envVars)) {
|
|
335
|
-
throw new Error(
|
|
378
|
+
throw new Error(formatMissingDbPasswordError(appKey));
|
|
336
379
|
}
|
|
337
|
-
|
|
338
|
-
const password = extractPassword(envVars, passwordKey);
|
|
380
|
+
const password = extractPassword(envVars, passwordKey, { appKey });
|
|
339
381
|
passwords[appKey] = password;
|
|
340
382
|
passwordsArray.push(password);
|
|
341
|
-
|
|
342
383
|
return { passwords, passwordsArray };
|
|
343
384
|
}
|
|
344
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Reads database passwords from .env file
|
|
388
|
+
* @async
|
|
389
|
+
* @function readDatabasePasswords
|
|
390
|
+
* @param {string} envPath - Path to .env file
|
|
391
|
+
* @param {Array<Object>} databases - Array of database configurations
|
|
392
|
+
* @param {string} appKey - Application key (fallback for single database)
|
|
393
|
+
* @returns {Promise<Object>} Object with passwords map and array
|
|
394
|
+
* @throws {Error} If required password variables are missing
|
|
395
|
+
*/
|
|
345
396
|
async function readDatabasePasswords(envPath, databases, appKey) {
|
|
346
397
|
const envVars = await readEnvFile(envPath);
|
|
347
|
-
|
|
348
|
-
// Process each database
|
|
349
398
|
if (databases && databases.length > 0) {
|
|
350
399
|
const { passwords, passwordsArray } = processMultipleDatabases(databases, envVars, appKey);
|
|
351
|
-
return {
|
|
352
|
-
map: passwords,
|
|
353
|
-
array: passwordsArray
|
|
354
|
-
};
|
|
400
|
+
return { map: passwords, array: passwordsArray };
|
|
355
401
|
}
|
|
356
|
-
|
|
357
402
|
const { passwords, passwordsArray } = processSingleDatabase(envVars, appKey);
|
|
358
|
-
return {
|
|
359
|
-
map: passwords,
|
|
360
|
-
array: passwordsArray
|
|
361
|
-
};
|
|
403
|
+
return { map: passwords, array: passwordsArray };
|
|
362
404
|
}
|
|
363
405
|
|
|
364
|
-
/**
|
|
365
|
-
* Generates Docker Compose configuration from template
|
|
366
|
-
* @param {string} appName - Application name
|
|
367
|
-
* @param {Object} appConfig - Application configuration
|
|
368
|
-
* @param {Object} options - Run options
|
|
369
|
-
* @returns {Promise<string>} Generated compose content
|
|
370
|
-
*/
|
|
371
406
|
/**
|
|
372
407
|
* Gets developer ID and calculates numeric ID
|
|
373
408
|
* @async
|
|
@@ -376,8 +411,7 @@ async function readDatabasePasswords(envPath, databases, appKey) {
|
|
|
376
411
|
*/
|
|
377
412
|
async function getDeveloperIdAndNumeric() {
|
|
378
413
|
const devId = await config.getDeveloperId();
|
|
379
|
-
|
|
380
|
-
return { devId, idNum };
|
|
414
|
+
return { devId, idNum: typeof devId === 'string' ? parseInt(devId, 10) : devId };
|
|
381
415
|
}
|
|
382
416
|
|
|
383
417
|
/**
|
|
@@ -411,15 +445,22 @@ async function readDatabasePasswordsIfNeeded(requiresDatabase, databases, envFil
|
|
|
411
445
|
return { map: {}, array: [] };
|
|
412
446
|
}
|
|
413
447
|
|
|
448
|
+
/**
|
|
449
|
+
* Generates Docker Compose configuration from template
|
|
450
|
+
* @async
|
|
451
|
+
* @function generateDockerCompose
|
|
452
|
+
* @param {string} appName - Application name
|
|
453
|
+
* @param {Object} appConfig - Application configuration
|
|
454
|
+
* @param {Object} options - Run options
|
|
455
|
+
* @returns {Promise<string>} Generated compose content
|
|
456
|
+
*/
|
|
414
457
|
async function generateDockerCompose(appName, appConfig, options) {
|
|
415
458
|
const language = appConfig.build?.language || appConfig.language || 'typescript';
|
|
416
459
|
const template = loadDockerComposeTemplate(language);
|
|
417
460
|
const port = options.port || appConfig.port || 3000;
|
|
418
|
-
|
|
419
461
|
const { devId, idNum } = await getDeveloperIdAndNumeric();
|
|
420
462
|
const { networkName, containerName } = buildNetworkAndContainerNames(appName, devId, idNum);
|
|
421
|
-
|
|
422
|
-
const serviceConfig = buildServiceConfig(appName, appConfig, port);
|
|
463
|
+
const serviceConfig = buildServiceConfig(appName, appConfig, port, devId);
|
|
423
464
|
const volumesConfig = buildVolumesConfig(appName);
|
|
424
465
|
const networksConfig = buildNetworksConfig(appConfig);
|
|
425
466
|
|
|
@@ -439,17 +480,19 @@ async function generateDockerCompose(appName, appConfig, options) {
|
|
|
439
480
|
...volumesConfig,
|
|
440
481
|
...networksConfig,
|
|
441
482
|
envFile: envFileAbsolutePath,
|
|
442
|
-
databasePasswords
|
|
483
|
+
databasePasswords,
|
|
443
484
|
devId: idNum,
|
|
444
|
-
networkName
|
|
445
|
-
containerName
|
|
485
|
+
networkName,
|
|
486
|
+
containerName
|
|
446
487
|
};
|
|
447
|
-
|
|
448
488
|
return template(templateData);
|
|
449
489
|
}
|
|
450
490
|
|
|
451
491
|
module.exports = {
|
|
452
492
|
generateDockerCompose,
|
|
453
|
-
getImageName
|
|
493
|
+
getImageName,
|
|
494
|
+
derivePathFromPattern,
|
|
495
|
+
buildTraefikConfig,
|
|
496
|
+
buildDevUsername
|
|
454
497
|
};
|
|
455
498
|
|