@aifabrix/builder 2.1.7 → 2.3.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/lib/app-deploy.js +73 -29
- package/lib/app-list.js +132 -0
- package/lib/app-readme.js +11 -4
- package/lib/app-register.js +435 -0
- package/lib/app-rotate-secret.js +164 -0
- package/lib/app-run.js +98 -84
- package/lib/app.js +13 -0
- package/lib/audit-logger.js +195 -15
- package/lib/build.js +155 -42
- package/lib/cli.js +104 -8
- package/lib/commands/app.js +8 -391
- package/lib/commands/login.js +130 -36
- package/lib/commands/secure.js +260 -0
- package/lib/config.js +315 -4
- package/lib/deployer.js +221 -183
- package/lib/infra.js +177 -112
- package/lib/push.js +34 -7
- package/lib/secrets.js +89 -23
- package/lib/templates.js +1 -1
- package/lib/utils/api-error-handler.js +465 -0
- package/lib/utils/api.js +165 -16
- package/lib/utils/auth-headers.js +84 -0
- package/lib/utils/build-copy.js +162 -0
- package/lib/utils/cli-utils.js +49 -3
- package/lib/utils/compose-generator.js +57 -16
- package/lib/utils/deployment-errors.js +90 -0
- package/lib/utils/deployment-validation.js +60 -0
- package/lib/utils/dev-config.js +83 -0
- package/lib/utils/docker-build.js +24 -0
- package/lib/utils/env-template.js +30 -10
- package/lib/utils/health-check.js +18 -1
- package/lib/utils/infra-containers.js +101 -0
- package/lib/utils/local-secrets.js +0 -2
- package/lib/utils/secrets-encryption.js +203 -0
- package/lib/utils/secrets-path.js +22 -3
- package/lib/utils/token-manager.js +381 -0
- package/package.json +2 -2
- package/templates/applications/README.md.hbs +155 -23
- package/templates/applications/miso-controller/Dockerfile +7 -119
- package/templates/infra/compose.yaml.hbs +93 -0
- package/templates/python/docker-compose.hbs +25 -17
- package/templates/typescript/docker-compose.hbs +25 -17
- package/test-output.txt +0 -5431
package/lib/secrets.js
CHANGED
|
@@ -15,6 +15,8 @@ const yaml = require('js-yaml');
|
|
|
15
15
|
const os = require('os');
|
|
16
16
|
const chalk = require('chalk');
|
|
17
17
|
const logger = require('./utils/logger');
|
|
18
|
+
const config = require('./config');
|
|
19
|
+
const devConfig = require('./utils/dev-config');
|
|
18
20
|
const {
|
|
19
21
|
generateMissingSecrets,
|
|
20
22
|
createDefaultSecrets
|
|
@@ -30,6 +32,7 @@ const {
|
|
|
30
32
|
buildHostnameToServiceMap,
|
|
31
33
|
resolveUrlPort
|
|
32
34
|
} = require('./utils/secrets-utils');
|
|
35
|
+
const { decryptSecret, isEncrypted } = require('./utils/secrets-encryption');
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
38
|
* Loads environment configuration for docker/local context
|
|
@@ -41,16 +44,59 @@ function loadEnvConfig() {
|
|
|
41
44
|
return yaml.load(content);
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Decrypts encrypted values in secrets object
|
|
49
|
+
* Checks for secure:// prefix and decrypts using encryption key from config
|
|
50
|
+
*
|
|
51
|
+
* @async
|
|
52
|
+
* @function decryptSecretsObject
|
|
53
|
+
* @param {Object} secrets - Secrets object with potentially encrypted values
|
|
54
|
+
* @returns {Promise<Object>} Secrets object with decrypted values
|
|
55
|
+
* @throws {Error} If decryption fails or encryption key is missing
|
|
56
|
+
*/
|
|
57
|
+
async function decryptSecretsObject(secrets) {
|
|
58
|
+
if (!secrets || typeof secrets !== 'object') {
|
|
59
|
+
return secrets;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const encryptionKey = await config.getSecretsEncryptionKey();
|
|
63
|
+
if (!encryptionKey) {
|
|
64
|
+
// No encryption key set, check if any values are encrypted
|
|
65
|
+
const hasEncrypted = Object.values(secrets).some(value => isEncrypted(value));
|
|
66
|
+
if (hasEncrypted) {
|
|
67
|
+
throw new Error('Encrypted secrets found but no encryption key configured. Run "aifabrix secure --secrets-encryption <key>" to set encryption key.');
|
|
68
|
+
}
|
|
69
|
+
// No encrypted values, return as-is
|
|
70
|
+
return secrets;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const decryptedSecrets = {};
|
|
74
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
75
|
+
if (isEncrypted(value)) {
|
|
76
|
+
try {
|
|
77
|
+
decryptedSecrets[key] = decryptSecret(value, encryptionKey);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new Error(`Failed to decrypt secret '${key}': ${error.message}`);
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
decryptedSecrets[key] = value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return decryptedSecrets;
|
|
87
|
+
}
|
|
88
|
+
|
|
44
89
|
/**
|
|
45
90
|
* Loads secrets with cascading lookup
|
|
46
91
|
* Supports both user secrets (~/.aifabrix/secrets.local.yaml) and project overrides
|
|
47
92
|
* User's file takes priority, then falls back to build.secrets from variables.yaml
|
|
93
|
+
* Automatically decrypts values with secure:// prefix
|
|
48
94
|
*
|
|
49
95
|
* @async
|
|
50
96
|
* @function loadSecrets
|
|
51
97
|
* @param {string} [secretsPath] - Path to secrets file (optional, for explicit override)
|
|
52
98
|
* @param {string} [appName] - Application name (optional, for variables.yaml lookup)
|
|
53
|
-
* @returns {Promise<Object>} Loaded secrets object
|
|
99
|
+
* @returns {Promise<Object>} Loaded secrets object with decrypted values
|
|
54
100
|
* @throws {Error} If no secrets file found and no fallback available
|
|
55
101
|
*
|
|
56
102
|
* @example
|
|
@@ -58,6 +104,8 @@ function loadEnvConfig() {
|
|
|
58
104
|
* // Returns: { 'postgres-passwordKeyVault': 'admin123', ... }
|
|
59
105
|
*/
|
|
60
106
|
async function loadSecrets(secretsPath, appName) {
|
|
107
|
+
let secrets;
|
|
108
|
+
|
|
61
109
|
// If explicit path provided, use it (backward compatibility)
|
|
62
110
|
if (secretsPath) {
|
|
63
111
|
const resolvedPath = resolveSecretsPath(secretsPath);
|
|
@@ -66,34 +114,35 @@ async function loadSecrets(secretsPath, appName) {
|
|
|
66
114
|
}
|
|
67
115
|
|
|
68
116
|
const content = fs.readFileSync(resolvedPath, 'utf8');
|
|
69
|
-
|
|
117
|
+
secrets = yaml.load(content);
|
|
70
118
|
|
|
71
119
|
if (!secrets || typeof secrets !== 'object') {
|
|
72
120
|
throw new Error(`Invalid secrets file format: ${resolvedPath}`);
|
|
73
121
|
}
|
|
122
|
+
} else {
|
|
123
|
+
// Cascading lookup: user's file first
|
|
124
|
+
let mergedSecrets = loadUserSecrets();
|
|
74
125
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
let mergedSecrets = loadUserSecrets();
|
|
126
|
+
// Then check build.secrets from variables.yaml if appName provided
|
|
127
|
+
if (appName) {
|
|
128
|
+
mergedSecrets = await loadBuildSecrets(mergedSecrets, appName);
|
|
129
|
+
}
|
|
80
130
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
131
|
+
// If still no secrets found, try default location
|
|
132
|
+
if (Object.keys(mergedSecrets).length === 0) {
|
|
133
|
+
mergedSecrets = loadDefaultSecrets();
|
|
134
|
+
}
|
|
85
135
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
136
|
+
// If still empty, throw error
|
|
137
|
+
if (Object.keys(mergedSecrets).length === 0) {
|
|
138
|
+
throw new Error('No secrets file found. Please create ~/.aifabrix/secrets.local.yaml or configure build.secrets in variables.yaml');
|
|
139
|
+
}
|
|
90
140
|
|
|
91
|
-
|
|
92
|
-
if (Object.keys(mergedSecrets).length === 0) {
|
|
93
|
-
throw new Error('No secrets file found. Please create ~/.aifabrix/secrets.local.yaml or configure build.secrets in variables.yaml');
|
|
141
|
+
secrets = mergedSecrets;
|
|
94
142
|
}
|
|
95
143
|
|
|
96
|
-
|
|
144
|
+
// Decrypt encrypted values
|
|
145
|
+
return await decryptSecretsObject(secrets);
|
|
97
146
|
}
|
|
98
147
|
|
|
99
148
|
/**
|
|
@@ -108,6 +157,7 @@ async function loadSecrets(secretsPath, appName) {
|
|
|
108
157
|
* @param {Object|string|null} [secretsFilePaths] - Paths object with userPath and buildPath, or string path (for backward compatibility)
|
|
109
158
|
* @param {string} [secretsFilePaths.userPath] - User's secrets file path
|
|
110
159
|
* @param {string|null} [secretsFilePaths.buildPath] - App's build.secrets file path (if configured)
|
|
160
|
+
* @param {string} [appName] - Application name (optional, for error messages)
|
|
111
161
|
* @returns {Promise<string>} Resolved environment content
|
|
112
162
|
* @throws {Error} If kv:// reference cannot be resolved
|
|
113
163
|
*
|
|
@@ -115,7 +165,7 @@ async function loadSecrets(secretsPath, appName) {
|
|
|
115
165
|
* const resolved = await resolveKvReferences(template, secrets, 'local');
|
|
116
166
|
* // Returns: 'DATABASE_URL=postgresql://user:pass@localhost:5432/db'
|
|
117
167
|
*/
|
|
118
|
-
async function resolveKvReferences(envTemplate, secrets, environment = 'local', secretsFilePaths = null) {
|
|
168
|
+
async function resolveKvReferences(envTemplate, secrets, environment = 'local', secretsFilePaths = null, appName = null) {
|
|
119
169
|
const envConfig = loadEnvConfig();
|
|
120
170
|
const envVars = envConfig.environments[environment] || envConfig.environments.local;
|
|
121
171
|
|
|
@@ -150,7 +200,8 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local',
|
|
|
150
200
|
fileInfo = `\n\nSecrets file location: ${paths.join(' and ')}`;
|
|
151
201
|
}
|
|
152
202
|
}
|
|
153
|
-
|
|
203
|
+
const resolveCommand = appName ? `aifabrix resolve ${appName}` : 'aifabrix resolve <app-name>';
|
|
204
|
+
throw new Error(`Missing secrets: ${missingSecrets.join(', ')}${fileInfo}\n\nRun "${resolveCommand}" to generate missing secrets.`);
|
|
154
205
|
}
|
|
155
206
|
|
|
156
207
|
// Now replace kv:// references, and handle ${VAR} inside the secret values
|
|
@@ -288,7 +339,7 @@ async function generateEnvFile(appName, secretsPath, environment = 'local', forc
|
|
|
288
339
|
const template = loadEnvTemplate(templatePath);
|
|
289
340
|
|
|
290
341
|
// Resolve secrets paths to show in error messages (use actual paths that loadSecrets would use)
|
|
291
|
-
const secretsPaths = getActualSecretsPath(secretsPath, appName);
|
|
342
|
+
const secretsPaths = await getActualSecretsPath(secretsPath, appName);
|
|
292
343
|
|
|
293
344
|
if (force) {
|
|
294
345
|
// Use userPath for generating missing secrets (priority file)
|
|
@@ -296,13 +347,28 @@ async function generateEnvFile(appName, secretsPath, environment = 'local', forc
|
|
|
296
347
|
}
|
|
297
348
|
|
|
298
349
|
const secrets = await loadSecrets(secretsPath, appName);
|
|
299
|
-
let resolved = await resolveKvReferences(template, secrets, environment, secretsPaths);
|
|
350
|
+
let resolved = await resolveKvReferences(template, secrets, environment, secretsPaths, appName);
|
|
300
351
|
|
|
301
352
|
// Resolve service ports in URLs for docker environment
|
|
302
353
|
if (environment === 'docker') {
|
|
303
354
|
resolved = resolveServicePortsInEnvContent(resolved, environment);
|
|
304
355
|
}
|
|
305
356
|
|
|
357
|
+
// For local environment, update infrastructure ports to use dev-specific ports
|
|
358
|
+
if (environment === 'local') {
|
|
359
|
+
const devId = await config.getDeveloperId();
|
|
360
|
+
const ports = devConfig.getDevPorts(devId);
|
|
361
|
+
|
|
362
|
+
// Update DATABASE_PORT if present
|
|
363
|
+
resolved = resolved.replace(/^DATABASE_PORT\s*=\s*.*$/m, `DATABASE_PORT=${ports.postgres}`);
|
|
364
|
+
|
|
365
|
+
// Update REDIS_URL if present (format: redis://localhost:port)
|
|
366
|
+
resolved = resolved.replace(/^REDIS_URL\s*=\s*redis:\/\/localhost:\d+/m, `REDIS_URL=redis://localhost:${ports.redis}`);
|
|
367
|
+
|
|
368
|
+
// Update REDIS_HOST if it contains a port
|
|
369
|
+
resolved = resolved.replace(/^REDIS_HOST\s*=\s*localhost:\d+/m, `REDIS_HOST=localhost:${ports.redis}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
306
372
|
fs.writeFileSync(envPath, resolved, { mode: 0o600 });
|
|
307
373
|
|
|
308
374
|
// Update PORT variable in container .env file to use port (from variables.yaml)
|
package/lib/templates.js
CHANGED
|
@@ -49,7 +49,7 @@ function generateVariablesYaml(appName, config) {
|
|
|
49
49
|
build: {
|
|
50
50
|
language: config.language || 'typescript',
|
|
51
51
|
envOutputPath: null,
|
|
52
|
-
context:
|
|
52
|
+
context: null, // Defaults to dev directory in build process
|
|
53
53
|
dockerfile: '',
|
|
54
54
|
secrets: null
|
|
55
55
|
},
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder API Error Handler
|
|
3
|
+
*
|
|
4
|
+
* Parses and formats structured error responses from the controller API
|
|
5
|
+
* Handles permission errors, validation errors, authentication errors,
|
|
6
|
+
* network errors, and server errors with user-friendly formatting
|
|
7
|
+
*
|
|
8
|
+
* @fileoverview API error handling utilities for AI Fabrix Builder
|
|
9
|
+
* @author AI Fabrix Team
|
|
10
|
+
* @version 2.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const chalk = require('chalk');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Formats permission error with missing and required permissions
|
|
17
|
+
* @param {Object} errorData - Error response data
|
|
18
|
+
* @returns {string} Formatted permission error message
|
|
19
|
+
*/
|
|
20
|
+
function formatPermissionError(errorData) {
|
|
21
|
+
const lines = [];
|
|
22
|
+
lines.push(chalk.red('❌ Permission Denied\n'));
|
|
23
|
+
|
|
24
|
+
// Handle detail message if present
|
|
25
|
+
if (errorData.detail) {
|
|
26
|
+
lines.push(chalk.yellow(errorData.detail));
|
|
27
|
+
lines.push('');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Extract missing permissions (support both flat and nested structures)
|
|
31
|
+
let missingPerms = [];
|
|
32
|
+
if (errorData.missingPermissions && Array.isArray(errorData.missingPermissions)) {
|
|
33
|
+
missingPerms = errorData.missingPermissions;
|
|
34
|
+
} else if (errorData.missing && errorData.missing.permissions && Array.isArray(errorData.missing.permissions)) {
|
|
35
|
+
missingPerms = errorData.missing.permissions;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (missingPerms.length > 0) {
|
|
39
|
+
lines.push(chalk.yellow('Missing permissions:'));
|
|
40
|
+
missingPerms.forEach(perm => {
|
|
41
|
+
lines.push(chalk.gray(` - ${perm}`));
|
|
42
|
+
});
|
|
43
|
+
lines.push('');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Extract required permissions (support both flat and nested structures)
|
|
47
|
+
let requiredPerms = [];
|
|
48
|
+
if (errorData.requiredPermissions && Array.isArray(errorData.requiredPermissions)) {
|
|
49
|
+
requiredPerms = errorData.requiredPermissions;
|
|
50
|
+
} else if (errorData.required && errorData.required.permissions && Array.isArray(errorData.required.permissions)) {
|
|
51
|
+
requiredPerms = errorData.required.permissions;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (requiredPerms.length > 0) {
|
|
55
|
+
lines.push(chalk.yellow('Required permissions:'));
|
|
56
|
+
requiredPerms.forEach(perm => {
|
|
57
|
+
lines.push(chalk.gray(` - ${perm}`));
|
|
58
|
+
});
|
|
59
|
+
lines.push('');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Use instance (from RFC 7807) or url field
|
|
63
|
+
const requestUrl = errorData.instance || errorData.url;
|
|
64
|
+
const method = errorData.method || 'POST';
|
|
65
|
+
if (requestUrl) {
|
|
66
|
+
lines.push(chalk.gray(`Request: ${method} ${requestUrl}`));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (errorData.correlationId) {
|
|
70
|
+
lines.push(chalk.gray(`Correlation ID: ${errorData.correlationId}`));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Formats validation error with field-level details
|
|
78
|
+
* Handles unified error API response format (RFC 7807 Problem Details)
|
|
79
|
+
* @param {Object} errorData - Error response data
|
|
80
|
+
* @returns {string} Formatted validation error message
|
|
81
|
+
*/
|
|
82
|
+
function formatValidationError(errorData) {
|
|
83
|
+
const lines = [];
|
|
84
|
+
lines.push(chalk.red('❌ Validation Error\n'));
|
|
85
|
+
|
|
86
|
+
// Handle RFC 7807 Problem Details format
|
|
87
|
+
// Priority: detail > title > message
|
|
88
|
+
if (errorData.detail) {
|
|
89
|
+
lines.push(chalk.yellow(errorData.detail));
|
|
90
|
+
lines.push('');
|
|
91
|
+
} else if (errorData.title) {
|
|
92
|
+
lines.push(chalk.yellow(errorData.title));
|
|
93
|
+
lines.push('');
|
|
94
|
+
} else if (errorData.message) {
|
|
95
|
+
lines.push(chalk.yellow(errorData.message));
|
|
96
|
+
lines.push('');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Handle errors array - this is the most important part
|
|
100
|
+
if (errorData.errors && Array.isArray(errorData.errors) && errorData.errors.length > 0) {
|
|
101
|
+
lines.push(chalk.yellow('Validation errors:'));
|
|
102
|
+
errorData.errors.forEach(err => {
|
|
103
|
+
const field = err.field || err.path || 'validation';
|
|
104
|
+
const message = err.message || 'Invalid value';
|
|
105
|
+
// If field is 'validation' or generic, just show the message
|
|
106
|
+
if (field === 'validation' || field === 'unknown') {
|
|
107
|
+
lines.push(chalk.gray(` • ${message}`));
|
|
108
|
+
} else {
|
|
109
|
+
lines.push(chalk.gray(` • ${field}: ${message}`));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
lines.push('');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Show instance (endpoint) if available (RFC 7807)
|
|
116
|
+
if (errorData.instance) {
|
|
117
|
+
lines.push(chalk.gray(`Endpoint: ${errorData.instance}`));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Show correlation ID if available
|
|
121
|
+
if (errorData.correlationId) {
|
|
122
|
+
lines.push(chalk.gray(`Correlation ID: ${errorData.correlationId}`));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return lines.join('\n');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Formats authentication error
|
|
130
|
+
* @param {Object} errorData - Error response data
|
|
131
|
+
* @returns {string} Formatted authentication error message
|
|
132
|
+
*/
|
|
133
|
+
function formatAuthenticationError(errorData) {
|
|
134
|
+
const lines = [];
|
|
135
|
+
lines.push(chalk.red('❌ Authentication Failed\n'));
|
|
136
|
+
|
|
137
|
+
if (errorData.message) {
|
|
138
|
+
lines.push(chalk.yellow(errorData.message));
|
|
139
|
+
} else {
|
|
140
|
+
lines.push(chalk.yellow('Invalid credentials or token expired.'));
|
|
141
|
+
}
|
|
142
|
+
lines.push('');
|
|
143
|
+
lines.push(chalk.gray('Run: aifabrix login'));
|
|
144
|
+
|
|
145
|
+
if (errorData.correlationId) {
|
|
146
|
+
lines.push(chalk.gray(`Correlation ID: ${errorData.correlationId}`));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return lines.join('\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Formats network error
|
|
154
|
+
* @param {string} errorMessage - Error message
|
|
155
|
+
* @param {Object} errorData - Error response data (optional)
|
|
156
|
+
* @returns {string} Formatted network error message
|
|
157
|
+
*/
|
|
158
|
+
function formatNetworkError(errorMessage, errorData) {
|
|
159
|
+
const lines = [];
|
|
160
|
+
lines.push(chalk.red('❌ Network Error\n'));
|
|
161
|
+
|
|
162
|
+
if (errorMessage.includes('ECONNREFUSED') || errorMessage.includes('Cannot connect')) {
|
|
163
|
+
lines.push(chalk.yellow('Cannot connect to controller.'));
|
|
164
|
+
lines.push(chalk.gray('Check if the controller is running.'));
|
|
165
|
+
} else if (errorMessage.includes('ENOTFOUND') || errorMessage.includes('hostname not found')) {
|
|
166
|
+
lines.push(chalk.yellow('Controller hostname not found.'));
|
|
167
|
+
lines.push(chalk.gray('Check your controller URL.'));
|
|
168
|
+
} else if (errorMessage.includes('timeout') || errorMessage.includes('timed out')) {
|
|
169
|
+
lines.push(chalk.yellow('Request timed out.'));
|
|
170
|
+
lines.push(chalk.gray('The controller may be overloaded.'));
|
|
171
|
+
} else {
|
|
172
|
+
lines.push(chalk.yellow(errorMessage));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (errorData && errorData.correlationId) {
|
|
176
|
+
lines.push(chalk.gray(`Correlation ID: ${errorData.correlationId}`));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return lines.join('\n');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Formats server error (500+)
|
|
184
|
+
* @param {Object} errorData - Error response data
|
|
185
|
+
* @returns {string} Formatted server error message
|
|
186
|
+
*/
|
|
187
|
+
function formatServerError(errorData) {
|
|
188
|
+
const lines = [];
|
|
189
|
+
lines.push(chalk.red('❌ Server Error\n'));
|
|
190
|
+
|
|
191
|
+
// Check for RFC 7807 Problem Details format (detail field)
|
|
192
|
+
if (errorData.detail) {
|
|
193
|
+
lines.push(chalk.yellow(errorData.detail));
|
|
194
|
+
} else if (errorData.message) {
|
|
195
|
+
lines.push(chalk.yellow(errorData.message));
|
|
196
|
+
} else {
|
|
197
|
+
lines.push(chalk.yellow('An internal server error occurred.'));
|
|
198
|
+
}
|
|
199
|
+
lines.push('');
|
|
200
|
+
lines.push(chalk.gray('Please try again later or contact support.'));
|
|
201
|
+
|
|
202
|
+
if (errorData.correlationId) {
|
|
203
|
+
lines.push('');
|
|
204
|
+
lines.push(chalk.gray(`Correlation ID: ${errorData.correlationId}`));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return lines.join('\n');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Formats conflict error (409)
|
|
212
|
+
* @param {Object} errorData - Error response data
|
|
213
|
+
* @returns {string} Formatted conflict error message
|
|
214
|
+
*/
|
|
215
|
+
function formatConflictError(errorData) {
|
|
216
|
+
const lines = [];
|
|
217
|
+
lines.push(chalk.red('❌ Conflict\n'));
|
|
218
|
+
|
|
219
|
+
// Check if it's an application already exists error
|
|
220
|
+
const detail = errorData.detail || errorData.message || '';
|
|
221
|
+
if (detail.toLowerCase().includes('application already exists')) {
|
|
222
|
+
lines.push(chalk.yellow('This application already exists in this environment.'));
|
|
223
|
+
lines.push('');
|
|
224
|
+
lines.push(chalk.gray('Options:'));
|
|
225
|
+
lines.push(chalk.gray(' • Use a different environment'));
|
|
226
|
+
lines.push(chalk.gray(' • Check existing applications: aifabrix app list -e <environment>'));
|
|
227
|
+
lines.push(chalk.gray(' • Update the existing application if needed'));
|
|
228
|
+
} else if (detail) {
|
|
229
|
+
lines.push(chalk.yellow(detail));
|
|
230
|
+
} else if (errorData.message) {
|
|
231
|
+
lines.push(chalk.yellow(errorData.message));
|
|
232
|
+
} else {
|
|
233
|
+
lines.push(chalk.yellow('A conflict occurred. The resource may already exist.'));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (errorData.correlationId) {
|
|
237
|
+
lines.push('');
|
|
238
|
+
lines.push(chalk.gray(`Correlation ID: ${errorData.correlationId}`));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return lines.join('\n');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Formats not found error (404)
|
|
246
|
+
* @param {Object} errorData - Error response data
|
|
247
|
+
* @returns {string} Formatted not found error message
|
|
248
|
+
*/
|
|
249
|
+
function formatNotFoundError(errorData) {
|
|
250
|
+
const lines = [];
|
|
251
|
+
lines.push(chalk.red('❌ Not Found\n'));
|
|
252
|
+
|
|
253
|
+
// Extract detail from RFC 7807 Problem Details format
|
|
254
|
+
const detail = errorData.detail || errorData.message || '';
|
|
255
|
+
|
|
256
|
+
if (detail) {
|
|
257
|
+
lines.push(chalk.yellow(detail));
|
|
258
|
+
lines.push('');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Provide actionable guidance based on error context
|
|
262
|
+
if (detail.toLowerCase().includes('environment')) {
|
|
263
|
+
lines.push(chalk.gray('Options:'));
|
|
264
|
+
lines.push(chalk.gray(' • Check the environment key spelling'));
|
|
265
|
+
lines.push(chalk.gray(' • List available environments: aifabrix app list -e <environment>'));
|
|
266
|
+
lines.push(chalk.gray(' • Verify you have access to this environment'));
|
|
267
|
+
} else if (detail.toLowerCase().includes('application')) {
|
|
268
|
+
lines.push(chalk.gray('Options:'));
|
|
269
|
+
lines.push(chalk.gray(' • Check the application key spelling'));
|
|
270
|
+
lines.push(chalk.gray(' • List applications: aifabrix app list -e <environment>'));
|
|
271
|
+
lines.push(chalk.gray(' • Verify the application exists in this environment'));
|
|
272
|
+
} else {
|
|
273
|
+
lines.push(chalk.gray('Options:'));
|
|
274
|
+
lines.push(chalk.gray(' • Check the resource identifier'));
|
|
275
|
+
lines.push(chalk.gray(' • Verify the resource exists'));
|
|
276
|
+
lines.push(chalk.gray(' • Check your permissions'));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (errorData.correlationId) {
|
|
280
|
+
lines.push('');
|
|
281
|
+
lines.push(chalk.gray(`Correlation ID: ${errorData.correlationId}`));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return lines.join('\n');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Formats generic error
|
|
289
|
+
* @param {Object} errorData - Error response data
|
|
290
|
+
* @param {number} statusCode - HTTP status code
|
|
291
|
+
* @returns {string} Formatted error message
|
|
292
|
+
*/
|
|
293
|
+
function formatGenericError(errorData, statusCode) {
|
|
294
|
+
const lines = [];
|
|
295
|
+
lines.push(chalk.red(`❌ Error (HTTP ${statusCode})\n`));
|
|
296
|
+
|
|
297
|
+
// Check for RFC 7807 Problem Details format (detail field)
|
|
298
|
+
if (errorData.detail) {
|
|
299
|
+
lines.push(chalk.yellow(errorData.detail));
|
|
300
|
+
} else if (errorData.message) {
|
|
301
|
+
lines.push(chalk.yellow(errorData.message));
|
|
302
|
+
} else if (errorData.error) {
|
|
303
|
+
lines.push(chalk.yellow(errorData.error));
|
|
304
|
+
} else {
|
|
305
|
+
lines.push(chalk.yellow('An error occurred while processing your request.'));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (errorData.correlationId) {
|
|
309
|
+
lines.push('');
|
|
310
|
+
lines.push(chalk.gray(`Correlation ID: ${errorData.correlationId}`));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return lines.join('\n');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Parses error response and determines error type
|
|
318
|
+
* @param {string|Object} errorResponse - Error response (string or parsed JSON)
|
|
319
|
+
* @param {number} statusCode - HTTP status code
|
|
320
|
+
* @param {boolean} isNetworkError - Whether this is a network error
|
|
321
|
+
* @returns {Object} Parsed error object with type, message, and formatted output
|
|
322
|
+
*/
|
|
323
|
+
function parseErrorResponse(errorResponse, statusCode, isNetworkError) {
|
|
324
|
+
let errorData = {};
|
|
325
|
+
|
|
326
|
+
// Handle undefined or null error response
|
|
327
|
+
if (errorResponse === undefined || errorResponse === null) {
|
|
328
|
+
errorData = { message: 'Unknown error occurred' };
|
|
329
|
+
} else if (typeof errorResponse === 'string') {
|
|
330
|
+
// Parse error response
|
|
331
|
+
try {
|
|
332
|
+
errorData = JSON.parse(errorResponse);
|
|
333
|
+
} catch {
|
|
334
|
+
errorData = { message: errorResponse };
|
|
335
|
+
}
|
|
336
|
+
} else if (typeof errorResponse === 'object') {
|
|
337
|
+
errorData = errorResponse;
|
|
338
|
+
} else {
|
|
339
|
+
// Fallback for unexpected types
|
|
340
|
+
errorData = { message: String(errorResponse) };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Handle nested response structure (some APIs wrap errors in data field)
|
|
344
|
+
if (errorData.data && typeof errorData.data === 'object') {
|
|
345
|
+
errorData = errorData.data;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Handle network errors
|
|
349
|
+
if (isNetworkError) {
|
|
350
|
+
const errorMessage = errorData.message || errorResponse || 'Network error';
|
|
351
|
+
return {
|
|
352
|
+
type: 'network',
|
|
353
|
+
message: errorMessage,
|
|
354
|
+
formatted: formatNetworkError(errorMessage, errorData),
|
|
355
|
+
data: errorData
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Handle different HTTP status codes
|
|
360
|
+
if (statusCode === 403) {
|
|
361
|
+
// Permission error
|
|
362
|
+
return {
|
|
363
|
+
type: 'permission',
|
|
364
|
+
message: 'Permission denied',
|
|
365
|
+
formatted: formatPermissionError(errorData),
|
|
366
|
+
data: errorData
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (statusCode === 401) {
|
|
371
|
+
// Authentication error
|
|
372
|
+
return {
|
|
373
|
+
type: 'authentication',
|
|
374
|
+
message: 'Authentication failed',
|
|
375
|
+
formatted: formatAuthenticationError(errorData),
|
|
376
|
+
data: errorData
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (statusCode === 400) {
|
|
381
|
+
// Validation error
|
|
382
|
+
// Extract message from unified error format (RFC 7807)
|
|
383
|
+
const errorMessage = errorData.detail || errorData.title || errorData.message || 'Validation error';
|
|
384
|
+
return {
|
|
385
|
+
type: 'validation',
|
|
386
|
+
message: errorMessage,
|
|
387
|
+
formatted: formatValidationError(errorData),
|
|
388
|
+
data: errorData
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (statusCode === 404) {
|
|
393
|
+
// Not found error
|
|
394
|
+
return {
|
|
395
|
+
type: 'notfound',
|
|
396
|
+
message: errorData.detail || errorData.message || 'Not found',
|
|
397
|
+
formatted: formatNotFoundError(errorData),
|
|
398
|
+
data: errorData
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (statusCode === 409) {
|
|
403
|
+
// Conflict error
|
|
404
|
+
return {
|
|
405
|
+
type: 'conflict',
|
|
406
|
+
message: errorData.detail || errorData.message || 'Conflict',
|
|
407
|
+
formatted: formatConflictError(errorData),
|
|
408
|
+
data: errorData
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (statusCode >= 500) {
|
|
413
|
+
// Server error
|
|
414
|
+
return {
|
|
415
|
+
type: 'server',
|
|
416
|
+
message: 'Server error',
|
|
417
|
+
formatted: formatServerError(errorData),
|
|
418
|
+
data: errorData
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Generic error
|
|
423
|
+
return {
|
|
424
|
+
type: 'generic',
|
|
425
|
+
message: errorData.message || errorData.error || 'Unknown error',
|
|
426
|
+
formatted: formatGenericError(errorData, statusCode),
|
|
427
|
+
data: errorData
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Formats error for display in CLI
|
|
433
|
+
* @param {Object} apiResponse - API response object from makeApiCall
|
|
434
|
+
* @returns {string} Formatted error message
|
|
435
|
+
*/
|
|
436
|
+
function formatApiError(apiResponse) {
|
|
437
|
+
if (!apiResponse || apiResponse.success !== false) {
|
|
438
|
+
return chalk.red('❌ Unknown error occurred');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Use formattedError if already available
|
|
442
|
+
if (apiResponse.formattedError) {
|
|
443
|
+
return apiResponse.formattedError;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const errorResponse = apiResponse.error || apiResponse.data || '';
|
|
447
|
+
const statusCode = apiResponse.status || 0;
|
|
448
|
+
const isNetworkError = apiResponse.network === true;
|
|
449
|
+
|
|
450
|
+
const parsed = parseErrorResponse(errorResponse, statusCode, isNetworkError);
|
|
451
|
+
return parsed.formatted;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
module.exports = {
|
|
455
|
+
parseErrorResponse,
|
|
456
|
+
formatApiError,
|
|
457
|
+
formatPermissionError,
|
|
458
|
+
formatValidationError,
|
|
459
|
+
formatAuthenticationError,
|
|
460
|
+
formatConflictError,
|
|
461
|
+
formatNetworkError,
|
|
462
|
+
formatServerError,
|
|
463
|
+
formatGenericError
|
|
464
|
+
};
|
|
465
|
+
|