@aifabrix/builder 2.21.0 → 2.22.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.
@@ -13,33 +13,85 @@ const chalk = require('chalk');
13
13
  /**
14
14
  * Formats authentication error
15
15
  * @param {Object} errorData - Error response data
16
+ * @param {string} [errorData.controllerUrl] - Controller URL for example command
17
+ * @param {string[]} [errorData.attemptedUrls] - Array of attempted controller URLs
18
+ * @param {string} [errorData.message] - Error message
19
+ * @param {string} [errorData.error] - Error text
20
+ * @param {string} [errorData.detail] - Error detail
21
+ * @param {string} [errorData.correlationId] - Correlation ID
16
22
  * @returns {string} Formatted authentication error message
17
23
  */
18
24
  function formatAuthenticationError(errorData) {
19
25
  const lines = [];
20
26
  lines.push(chalk.red('❌ Authentication Failed\n'));
21
- if (errorData.message) {
22
- lines.push(chalk.yellow(errorData.message));
23
- } else {
24
- lines.push(chalk.yellow('Invalid credentials or token expired.'));
27
+
28
+ // Show controller URL prominently if available
29
+ if (errorData.controllerUrl) {
30
+ lines.push(chalk.yellow(`Controller URL: ${errorData.controllerUrl}`));
31
+ lines.push('');
32
+ }
33
+
34
+ // Show attempted URLs if multiple were tried
35
+ if (errorData.attemptedUrls && errorData.attemptedUrls.length > 1) {
36
+ lines.push(chalk.gray('Attempted controller URLs:'));
37
+ errorData.attemptedUrls.forEach(url => {
38
+ lines.push(chalk.gray(` • ${url}`));
39
+ });
40
+ lines.push('');
41
+ }
42
+
43
+ // Check if error message contains specific information
44
+ const errorMessage = errorData.message || errorData.error || errorData.detail || '';
45
+ const lowerMessage = errorMessage.toLowerCase();
46
+
47
+ // Only show error message if it provides useful information beyond generic messages
48
+ const isGenericMessage = lowerMessage.includes('authentication required') ||
49
+ lowerMessage.includes('unauthorized') ||
50
+ lowerMessage === '';
51
+
52
+ if (errorMessage && !isGenericMessage) {
53
+ // Show specific error message if it provides useful details
54
+ lines.push(chalk.yellow(errorMessage));
55
+ lines.push('');
25
56
  }
57
+
58
+ // Always show general, actionable guidance
59
+ lines.push(chalk.gray('Your authentication token is invalid or has expired.'));
26
60
  lines.push('');
27
- lines.push(chalk.gray('Run: aifabrix login'));
61
+ lines.push(chalk.gray('To authenticate, run:'));
62
+
63
+ // Use real controller URL if provided, otherwise show placeholder
64
+ const controllerUrl = errorData.controllerUrl;
65
+ if (controllerUrl && controllerUrl.trim()) {
66
+ lines.push(chalk.gray(` aifabrix login --method device --controller ${controllerUrl}`));
67
+ } else {
68
+ lines.push(chalk.gray(' aifabrix login --method device --controller <url>'));
69
+ }
70
+
28
71
  if (errorData.correlationId) {
72
+ lines.push('');
29
73
  lines.push(chalk.gray(`Correlation ID: ${errorData.correlationId}`));
30
74
  }
75
+
31
76
  return lines.join('\n');
32
77
  }
33
78
 
34
79
  /**
35
80
  * Formats server error (500+)
36
81
  * @param {Object} errorData - Error response data
82
+ * @param {string} [errorData.controllerUrl] - Controller URL
37
83
  * @returns {string} Formatted server error message
38
84
  */
39
85
  function formatServerError(errorData) {
40
86
  const lines = [];
41
87
  lines.push(chalk.red('❌ Server Error\n'));
42
88
 
89
+ // Show controller URL if available
90
+ if (errorData.controllerUrl) {
91
+ lines.push(chalk.yellow(`Controller URL: ${errorData.controllerUrl}`));
92
+ lines.push('');
93
+ }
94
+
43
95
  // Check for RFC 7807 Problem Details format (detail field)
44
96
  if (errorData.detail) {
45
97
  lines.push(chalk.yellow(errorData.detail));
@@ -62,12 +114,19 @@ function formatServerError(errorData) {
62
114
  /**
63
115
  * Formats conflict error (409)
64
116
  * @param {Object} errorData - Error response data
117
+ * @param {string} [errorData.controllerUrl] - Controller URL
65
118
  * @returns {string} Formatted conflict error message
66
119
  */
67
120
  function formatConflictError(errorData) {
68
121
  const lines = [];
69
122
  lines.push(chalk.red('❌ Conflict\n'));
70
123
 
124
+ // Show controller URL if available
125
+ if (errorData.controllerUrl) {
126
+ lines.push(chalk.yellow(`Controller URL: ${errorData.controllerUrl}`));
127
+ lines.push('');
128
+ }
129
+
71
130
  // Check if it's an application already exists error
72
131
  const detail = errorData.detail || errorData.message || '';
73
132
  if (detail.toLowerCase().includes('application already exists')) {
@@ -124,12 +183,19 @@ function getNotFoundGuidance(detail) {
124
183
  /**
125
184
  * Formats not found error (404)
126
185
  * @param {Object} errorData - Error response data
186
+ * @param {string} [errorData.controllerUrl] - Controller URL
127
187
  * @returns {string} Formatted not found error message
128
188
  */
129
189
  function formatNotFoundError(errorData) {
130
190
  const lines = [];
131
191
  lines.push(chalk.red('❌ Not Found\n'));
132
192
 
193
+ // Show controller URL if available
194
+ if (errorData.controllerUrl) {
195
+ lines.push(chalk.yellow(`Controller URL: ${errorData.controllerUrl}`));
196
+ lines.push('');
197
+ }
198
+
133
199
  const detail = errorData.detail || errorData.message || '';
134
200
  if (detail) {
135
201
  lines.push(chalk.yellow(detail));
@@ -154,12 +220,19 @@ function formatNotFoundError(errorData) {
154
220
  * Formats generic error
155
221
  * @param {Object} errorData - Error response data
156
222
  * @param {number} statusCode - HTTP status code
223
+ * @param {string} [errorData.controllerUrl] - Controller URL
157
224
  * @returns {string} Formatted error message
158
225
  */
159
226
  function formatGenericError(errorData, statusCode) {
160
227
  const lines = [];
161
228
  lines.push(chalk.red(`❌ Error (HTTP ${statusCode})\n`));
162
229
 
230
+ // Show controller URL if available
231
+ if (errorData.controllerUrl) {
232
+ lines.push(chalk.yellow(`Controller URL: ${errorData.controllerUrl}`));
233
+ lines.push('');
234
+ }
235
+
163
236
  // Check for RFC 7807 Problem Details format (detail field)
164
237
  if (errorData.detail) {
165
238
  lines.push(chalk.yellow(errorData.detail));
@@ -14,26 +14,46 @@ const chalk = require('chalk');
14
14
  * Formats network error
15
15
  * @param {string} errorMessage - Error message
16
16
  * @param {Object} errorData - Error response data (optional)
17
+ * @param {string} [errorData.controllerUrl] - Controller URL
17
18
  * @returns {string} Formatted network error message
18
19
  */
19
20
  function formatNetworkError(errorMessage, errorData) {
20
21
  const lines = [];
21
22
  lines.push(chalk.red('❌ Network Error\n'));
22
23
 
23
- if (errorMessage.includes('ECONNREFUSED') || errorMessage.includes('Cannot connect')) {
24
+ // Show controller URL prominently if available
25
+ if (errorData && errorData.controllerUrl) {
26
+ lines.push(chalk.yellow(`Controller URL: ${errorData.controllerUrl}`));
27
+ lines.push('');
28
+ }
29
+
30
+ // Ensure errorMessage is a string
31
+ const message = typeof errorMessage === 'string' ? errorMessage : String(errorMessage || 'Network error');
32
+
33
+ if (message.includes('ECONNREFUSED') || message.includes('Cannot connect')) {
24
34
  lines.push(chalk.yellow('Cannot connect to controller.'));
35
+ if (errorData && errorData.controllerUrl) {
36
+ lines.push(chalk.gray(`Controller URL: ${errorData.controllerUrl}`));
37
+ }
25
38
  lines.push(chalk.gray('Check if the controller is running.'));
26
- } else if (errorMessage.includes('ENOTFOUND') || errorMessage.includes('hostname not found')) {
39
+ } else if (message.includes('ENOTFOUND') || message.includes('hostname not found')) {
27
40
  lines.push(chalk.yellow('Controller hostname not found.'));
41
+ if (errorData && errorData.controllerUrl) {
42
+ lines.push(chalk.gray(`Controller URL: ${errorData.controllerUrl}`));
43
+ }
28
44
  lines.push(chalk.gray('Check your controller URL.'));
29
- } else if (errorMessage.includes('timeout') || errorMessage.includes('timed out')) {
45
+ } else if (message.includes('timeout') || message.includes('timed out')) {
30
46
  lines.push(chalk.yellow('Request timed out.'));
47
+ if (errorData && errorData.controllerUrl) {
48
+ lines.push(chalk.gray(`Controller URL: ${errorData.controllerUrl}`));
49
+ }
31
50
  lines.push(chalk.gray('The controller may be overloaded.'));
32
51
  } else {
33
- lines.push(chalk.yellow(errorMessage));
52
+ lines.push(chalk.yellow(message));
34
53
  }
35
54
 
36
55
  if (errorData && errorData.correlationId) {
56
+ lines.push('');
37
57
  lines.push(chalk.gray(`Correlation ID: ${errorData.correlationId}`));
38
58
  }
39
59
 
package/lib/validate.js CHANGED
@@ -16,6 +16,7 @@ const validator = require('./validator');
16
16
  const { resolveExternalFiles } = require('./utils/schema-resolver');
17
17
  const { loadExternalSystemSchema, loadExternalDataSourceSchema, detectSchemaType } = require('./utils/schema-loader');
18
18
  const { formatValidationErrors } = require('./utils/error-formatter');
19
+ const { detectAppType } = require('./utils/paths');
19
20
  const logger = require('./utils/logger');
20
21
 
21
22
  /**
@@ -56,10 +57,29 @@ async function validateExternalFile(filePath, type) {
56
57
 
57
58
  const valid = validate(parsed);
58
59
 
60
+ const errors = valid ? [] : formatValidationErrors(validate.errors);
61
+ const warnings = [];
62
+
63
+ // Additional validation for external system files: check role references in permissions
64
+ if (type === 'system' && parsed.permissions && Array.isArray(parsed.permissions)) {
65
+ const roles = parsed.roles || [];
66
+ const roleValues = new Set(roles.map(r => r.value));
67
+
68
+ parsed.permissions.forEach((permission, index) => {
69
+ if (permission.roles && Array.isArray(permission.roles)) {
70
+ permission.roles.forEach(roleValue => {
71
+ if (!roleValues.has(roleValue)) {
72
+ errors.push(`Permission "${permission.name}" (index ${index}) references role "${roleValue}" which does not exist in roles array`);
73
+ }
74
+ });
75
+ }
76
+ });
77
+ }
78
+
59
79
  return {
60
- valid,
61
- errors: valid ? [] : formatValidationErrors(validate.errors),
62
- warnings: []
80
+ valid: errors.length === 0,
81
+ errors,
82
+ warnings
63
83
  };
64
84
  }
65
85
 
@@ -130,11 +150,28 @@ async function validateAppOrFile(appOrFile) {
130
150
  // Treat as app name
131
151
  const appName = appOrFile;
132
152
 
153
+ // Detect app type to support both builder/ and integration/ directories
154
+ const { appPath, isExternal } = await detectAppType(appName);
155
+
133
156
  // Validate application
134
157
  const appValidation = await validator.validateApplication(appName);
135
158
 
159
+ // Validate rbac.yaml for external systems
160
+ let rbacValidation = null;
161
+ if (isExternal) {
162
+ try {
163
+ rbacValidation = await validator.validateRbac(appName);
164
+ } catch (error) {
165
+ rbacValidation = {
166
+ valid: false,
167
+ errors: [error.message],
168
+ warnings: []
169
+ };
170
+ }
171
+ }
172
+
136
173
  // Check for externalIntegration block
137
- const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
174
+ const variablesPath = path.join(appPath, 'variables.yaml');
138
175
  if (!fs.existsSync(variablesPath)) {
139
176
  return {
140
177
  valid: appValidation.valid,
@@ -193,20 +230,25 @@ async function validateAppOrFile(appOrFile) {
193
230
  }
194
231
 
195
232
  // Aggregate results
196
- const allValid = appValidation.valid && externalValidations.every(v => v.valid);
233
+ const rbacErrors = rbacValidation ? rbacValidation.errors : [];
234
+ const rbacWarnings = rbacValidation ? rbacValidation.warnings : [];
235
+ const allValid = appValidation.valid && externalValidations.every(v => v.valid) && (!rbacValidation || rbacValidation.valid);
197
236
  const allErrors = [
198
237
  ...appValidation.errors,
199
- ...externalValidations.flatMap(v => v.errors.map(e => `${v.file}: ${e}`))
238
+ ...externalValidations.flatMap(v => v.errors.map(e => `${v.file}: ${e}`)),
239
+ ...rbacErrors.map(e => `rbac.yaml: ${e}`)
200
240
  ];
201
241
  const allWarnings = [
202
242
  ...appValidation.warnings,
203
- ...externalValidations.flatMap(v => v.warnings)
243
+ ...externalValidations.flatMap(v => v.warnings),
244
+ ...rbacWarnings
204
245
  ];
205
246
 
206
247
  return {
207
248
  valid: allValid,
208
249
  application: appValidation,
209
250
  externalFiles: externalValidations,
251
+ rbac: rbacValidation,
210
252
  errors: allErrors,
211
253
  warnings: allWarnings
212
254
  };
@@ -263,6 +305,24 @@ function displayValidationResults(result) {
263
305
  });
264
306
  }
265
307
 
308
+ // Display rbac validation (for external systems)
309
+ if (result.rbac) {
310
+ logger.log(chalk.blue('\nRBAC Configuration:'));
311
+ if (result.rbac.valid) {
312
+ logger.log(chalk.green(' ✓ RBAC configuration is valid'));
313
+ } else {
314
+ logger.log(chalk.red(' ✗ RBAC configuration has errors:'));
315
+ result.rbac.errors.forEach(error => {
316
+ logger.log(chalk.red(` • ${error}`));
317
+ });
318
+ }
319
+ if (result.rbac.warnings && result.rbac.warnings.length > 0) {
320
+ result.rbac.warnings.forEach(warning => {
321
+ logger.log(chalk.yellow(` ⚠ ${warning}`));
322
+ });
323
+ }
324
+ }
325
+
266
326
  // Display file validation (for direct file validation)
267
327
  if (result.file) {
268
328
  logger.log(chalk.blue(`\nFile: ${result.file}`));
package/lib/validator.js CHANGED
@@ -162,7 +162,9 @@ async function validateRbac(appName) {
162
162
  throw new Error('App name is required and must be a string');
163
163
  }
164
164
 
165
- const rbacPath = path.join(process.cwd(), 'builder', appName, 'rbac.yaml');
165
+ // Support both builder/ and integration/ directories using detectAppType
166
+ const { appPath } = await detectAppType(appName);
167
+ const rbacPath = path.join(appPath, 'rbac.yaml');
166
168
 
167
169
  if (!fs.existsSync(rbacPath)) {
168
170
  return { valid: true, errors: [], warnings: ['rbac.yaml not found - authentication disabled'] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.21.0",
3
+ "version": "2.22.0",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -32,6 +32,25 @@
32
32
  "serverUrl": "https://mcp.example.com",
33
33
  "toolPrefix": "{{systemKey}}"
34
34
  }{{/if}},
35
- "tags": []
35
+ "tags": []{{#if roles}},
36
+ "roles": [
37
+ {{#each roles}}
38
+ {
39
+ "name": "{{name}}",
40
+ "value": "{{value}}",
41
+ "description": "{{description}}"{{#if Groups}},
42
+ "Groups": [{{#each Groups}}"{{this}}"{{#unless @last}}, {{/unless}}{{/each}}]{{/if}}
43
+ }{{#unless @last}},{{/unless}}
44
+ {{/each}}
45
+ ]{{/if}}{{#if permissions}},
46
+ "permissions": [
47
+ {{#each permissions}}
48
+ {
49
+ "name": "{{name}}",
50
+ "roles": [{{#each roles}}"{{this}}"{{#unless @last}}, {{/unless}}{{/each}}],
51
+ "description": "{{description}}"
52
+ }{{#unless @last}},{{/unless}}
53
+ {{/each}}
54
+ ]{{/if}}
36
55
  }
37
56