@aifabrix/builder 2.6.2 → 2.7.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/bin/aifabrix.js CHANGED
@@ -16,6 +16,7 @@
16
16
  const { Command } = require('commander');
17
17
  const cli = require('../lib/cli');
18
18
  const { setupAppCommands } = require('../lib/commands/app');
19
+ const { setupDatasourceCommands } = require('../lib/commands/datasource');
19
20
  const logger = require('../lib/utils/logger');
20
21
  const packageJson = require('../package.json');
21
22
 
@@ -36,6 +37,9 @@ function initializeCLI() {
36
37
  // Add application management commands
37
38
  setupAppCommands(program);
38
39
 
40
+ // Add datasource management commands
41
+ setupDatasourceCommands(program);
42
+
39
43
  // Parse command line arguments
40
44
  program.parse();
41
45
  }
package/lib/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  /**
2
3
  * AI Fabrix Builder CLI Command Definitions
3
4
  *
@@ -265,12 +266,26 @@ function setupCommands(program) {
265
266
 
266
267
  // Utility commands
267
268
  program.command('resolve <app>')
268
- .description('Generate .env file from template')
269
+ .description('Generate .env file from template and validate application files')
269
270
  .option('-f, --force', 'Generate missing secret keys in secrets file')
271
+ .option('--skip-validation', 'Skip file validation after generating .env')
270
272
  .action(async(appName, options) => {
271
273
  try {
272
- const envPath = await secrets.generateEnvFile(appName, undefined, 'local', options.force);
274
+ // builder/.env should use docker context (postgres:5432)
275
+ // apps/.env (if envOutputPath is set) will be generated with local context by processEnvVariables
276
+ const envPath = await secrets.generateEnvFile(appName, undefined, 'docker', options.force);
273
277
  logger.log(`✓ Generated .env file: ${envPath}`);
278
+
279
+ // Validate application files after generating .env
280
+ if (!options.skipValidation) {
281
+ const validate = require('./validate');
282
+ const result = await validate.validateAppOrFile(appName);
283
+ validate.displayValidationResults(result);
284
+ if (!result.valid) {
285
+ logger.log(chalk.yellow('\n⚠️ Validation found errors. Fix them before deploying.'));
286
+ process.exit(1);
287
+ }
288
+ }
274
289
  } catch (error) {
275
290
  handleCommandError(error, 'resolve');
276
291
  process.exit(1);
@@ -330,6 +345,40 @@ function setupCommands(program) {
330
345
  }
331
346
  });
332
347
 
348
+ // Validation command
349
+ program.command('validate <appOrFile>')
350
+ .description('Validate application or external integration file')
351
+ .action(async(appOrFile) => {
352
+ try {
353
+ const validate = require('./validate');
354
+ const result = await validate.validateAppOrFile(appOrFile);
355
+ validate.displayValidationResults(result);
356
+ if (!result.valid) {
357
+ process.exit(1);
358
+ }
359
+ } catch (error) {
360
+ handleCommandError(error, 'validate');
361
+ process.exit(1);
362
+ }
363
+ });
364
+
365
+ // Diff command
366
+ program.command('diff <file1> <file2>')
367
+ .description('Compare two configuration files (for deployment pipeline)')
368
+ .action(async(file1, file2) => {
369
+ try {
370
+ const diff = require('./diff');
371
+ const result = await diff.compareFiles(file1, file2);
372
+ diff.formatDiffOutput(result);
373
+ if (!result.identical) {
374
+ process.exit(1);
375
+ }
376
+ } catch (error) {
377
+ handleCommandError(error, 'diff');
378
+ process.exit(1);
379
+ }
380
+ });
381
+
333
382
  program.command('dockerfile <app>')
334
383
  .description('Generate Dockerfile for an application')
335
384
  .option('-l, --language <lang>', 'Override language detection')
@@ -0,0 +1,94 @@
1
+ /**
2
+ * AI Fabrix Builder - Datasource Commands
3
+ *
4
+ * Handles datasource validation, listing, comparison, and deployment
5
+ * Commands: datasource validate, datasource list, datasource diff, datasource deploy
6
+ *
7
+ * @fileoverview Datasource management commands for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const chalk = require('chalk');
13
+ const logger = require('../utils/logger');
14
+ const { validateDatasourceFile } = require('../datasource-validate');
15
+ const { listDatasources } = require('../datasource-list');
16
+ const { compareDatasources } = require('../datasource-diff');
17
+ const { deployDatasource } = require('../datasource-deploy');
18
+
19
+ /**
20
+ * Setup datasource management commands
21
+ * @param {Command} program - Commander program instance
22
+ */
23
+ function setupDatasourceCommands(program) {
24
+ const datasource = program
25
+ .command('datasource')
26
+ .description('Manage external data sources');
27
+
28
+ // Validate command
29
+ datasource
30
+ .command('validate <file>')
31
+ .description('Validate external datasource JSON file')
32
+ .action(async(file) => {
33
+ try {
34
+ const result = await validateDatasourceFile(file);
35
+ if (result.valid) {
36
+ logger.log(chalk.green(`\n✓ Datasource file is valid: ${file}`));
37
+ } else {
38
+ logger.log(chalk.red(`\n✗ Datasource file has errors: ${file}`));
39
+ result.errors.forEach(error => {
40
+ logger.log(chalk.red(` • ${error}`));
41
+ });
42
+ process.exit(1);
43
+ }
44
+ } catch (error) {
45
+ logger.error(chalk.red('❌ Validation failed:'), error.message);
46
+ process.exit(1);
47
+ }
48
+ });
49
+
50
+ // List command
51
+ datasource
52
+ .command('list')
53
+ .description('List datasources from environment')
54
+ .requiredOption('-e, --environment <env>', 'Environment ID or key')
55
+ .action(async(options) => {
56
+ try {
57
+ await listDatasources(options);
58
+ } catch (error) {
59
+ logger.error(chalk.red('❌ Failed to list datasources:'), error.message);
60
+ process.exit(1);
61
+ }
62
+ });
63
+
64
+ // Diff command
65
+ datasource
66
+ .command('diff <file1> <file2>')
67
+ .description('Compare two datasource configuration files (for dataplane)')
68
+ .action(async(file1, file2) => {
69
+ try {
70
+ await compareDatasources(file1, file2);
71
+ } catch (error) {
72
+ logger.error(chalk.red('❌ Diff failed:'), error.message);
73
+ process.exit(1);
74
+ }
75
+ });
76
+
77
+ // Deploy command
78
+ datasource
79
+ .command('deploy <myapp> <file>')
80
+ .description('Deploy datasource to dataplane')
81
+ .requiredOption('--controller <url>', 'Controller URL')
82
+ .requiredOption('-e, --environment <env>', 'Environment (miso, dev, tst, pro)')
83
+ .action(async(myapp, file, options) => {
84
+ try {
85
+ await deployDatasource(myapp, file, options);
86
+ } catch (error) {
87
+ logger.error(chalk.red('❌ Deployment failed:'), error.message);
88
+ process.exit(1);
89
+ }
90
+ });
91
+ }
92
+
93
+ module.exports = { setupDatasourceCommands };
94
+
package/lib/config.js CHANGED
@@ -226,13 +226,23 @@ async function setCurrentEnvironment(environment) {
226
226
  * @returns {boolean} True if token is expired
227
227
  */
228
228
  function isTokenExpired(expiresAt) {
229
- if (!expiresAt) {
230
- return true;
231
- }
229
+ if (!expiresAt) return true;
232
230
  const expirationTime = new Date(expiresAt).getTime();
233
231
  const now = Date.now();
234
- // Add 5 minute buffer to refresh before actual expiration
235
- return now >= (expirationTime - 5 * 60 * 1000);
232
+ return now >= (expirationTime - 5 * 60 * 1000); // 5 minute buffer
233
+ }
234
+
235
+ /**
236
+ * Check if token should be refreshed proactively (within 15 minutes of expiry)
237
+ * Helps keep Keycloak sessions alive by refreshing before SSO Session Idle timeout (30 minutes)
238
+ * @param {string} expiresAt - ISO timestamp string
239
+ * @returns {boolean} True if token should be refreshed proactively
240
+ */
241
+ function shouldRefreshToken(expiresAt) {
242
+ if (!expiresAt) return true;
243
+ const expirationTime = new Date(expiresAt).getTime();
244
+ const now = Date.now();
245
+ return now >= (expirationTime - 15 * 60 * 1000); // 15 minutes buffer
236
246
  }
237
247
 
238
248
  /**
@@ -242,9 +252,7 @@ function isTokenExpired(expiresAt) {
242
252
  */
243
253
  async function getDeviceToken(controllerUrl) {
244
254
  const config = await getConfig();
245
- if (!config.device || !config.device[controllerUrl]) {
246
- return null;
247
- }
255
+ if (!config.device || !config.device[controllerUrl]) return null;
248
256
  const deviceToken = config.device[controllerUrl];
249
257
  return {
250
258
  controller: controllerUrl,
@@ -262,12 +270,8 @@ async function getDeviceToken(controllerUrl) {
262
270
  */
263
271
  async function getClientToken(environment, appName) {
264
272
  const config = await getConfig();
265
- if (!config.environments || !config.environments[environment]) {
266
- return null;
267
- }
268
- if (!config.environments[environment].clients || !config.environments[environment].clients[appName]) {
269
- return null;
270
- }
273
+ if (!config.environments || !config.environments[environment]) return null;
274
+ if (!config.environments[environment].clients || !config.environments[environment].clients[appName]) return null;
271
275
  return config.environments[environment].clients[appName];
272
276
  }
273
277
 
@@ -281,14 +285,8 @@ async function getClientToken(environment, appName) {
281
285
  */
282
286
  async function saveDeviceToken(controllerUrl, token, refreshToken, expiresAt) {
283
287
  const config = await getConfig();
284
- if (!config.device) {
285
- config.device = {};
286
- }
287
- config.device[controllerUrl] = {
288
- token: token,
289
- refreshToken: refreshToken,
290
- expiresAt: expiresAt
291
- };
288
+ if (!config.device) config.device = {};
289
+ config.device[controllerUrl] = { token, refreshToken, expiresAt };
292
290
  await saveConfig(config);
293
291
  }
294
292
 
@@ -303,20 +301,10 @@ async function saveDeviceToken(controllerUrl, token, refreshToken, expiresAt) {
303
301
  */
304
302
  async function saveClientToken(environment, appName, controllerUrl, token, expiresAt) {
305
303
  const config = await getConfig();
306
- if (!config.environments) {
307
- config.environments = {};
308
- }
309
- if (!config.environments[environment]) {
310
- config.environments[environment] = { clients: {} };
311
- }
312
- if (!config.environments[environment].clients) {
313
- config.environments[environment].clients = {};
314
- }
315
- config.environments[environment].clients[appName] = {
316
- controller: controllerUrl,
317
- token: token,
318
- expiresAt: expiresAt
319
- };
304
+ if (!config.environments) config.environments = {};
305
+ if (!config.environments[environment]) config.environments[environment] = { clients: {} };
306
+ if (!config.environments[environment].clients) config.environments[environment].clients = {};
307
+ config.environments[environment].clients[appName] = { controller: controllerUrl, token, expiresAt };
320
308
  await saveConfig(config);
321
309
  }
322
310
 
@@ -468,6 +456,7 @@ const exportsObj = {
468
456
  getCurrentEnvironment,
469
457
  setCurrentEnvironment,
470
458
  isTokenExpired,
459
+ shouldRefreshToken,
471
460
  getDeviceToken,
472
461
  getClientToken,
473
462
  saveDeviceToken,
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Datasource Deployment
3
+ *
4
+ * Deploys datasource to dataplane via controller API.
5
+ * Gets dataplane URL from controller, then deploys to dataplane.
6
+ *
7
+ * @fileoverview Datasource deployment for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const chalk = require('chalk');
14
+ const { getDeploymentAuth } = require('./utils/token-manager');
15
+ const { authenticatedApiCall } = require('./utils/api');
16
+ const { formatApiError } = require('./utils/api-error-handler');
17
+ const logger = require('./utils/logger');
18
+ const { validateDatasourceFile } = require('./datasource-validate');
19
+
20
+ /**
21
+ * Gets dataplane URL from controller by fetching application details
22
+ *
23
+ * @async
24
+ * @function getDataplaneUrl
25
+ * @param {string} controllerUrl - Controller URL
26
+ * @param {string} appKey - Application key
27
+ * @param {string} environment - Environment key
28
+ * @param {Object} authConfig - Authentication configuration
29
+ * @returns {Promise<string>} Dataplane URL
30
+ * @throws {Error} If dataplane URL cannot be retrieved
31
+ */
32
+ async function getDataplaneUrl(controllerUrl, appKey, environment, authConfig) {
33
+ // Call controller API to get application details
34
+ // Expected: GET /api/v1/environments/{env}/applications/{appKey}
35
+ const endpoint = `${controllerUrl}/api/v1/environments/${environment}/applications/${appKey}`;
36
+
37
+ let response;
38
+ if (authConfig.type === 'bearer' && authConfig.token) {
39
+ response = await authenticatedApiCall(endpoint, {}, authConfig.token);
40
+ } else {
41
+ // For credentials, we'd need to use a different API call method
42
+ // For now, use bearer token approach
43
+ throw new Error('Bearer token authentication required for getting dataplane URL');
44
+ }
45
+
46
+ if (!response.success || !response.data) {
47
+ const formattedError = response.formattedError || formatApiError(response);
48
+ throw new Error(`Failed to get application from controller: ${formattedError}`);
49
+ }
50
+
51
+ // Extract dataplane URL from application response
52
+ // This is a placeholder - actual response structure may vary
53
+ const application = response.data.data || response.data;
54
+ const dataplaneUrl = application.dataplaneUrl || application.dataplane?.url || application.configuration?.dataplaneUrl;
55
+
56
+ if (!dataplaneUrl) {
57
+ logger.error(chalk.red('❌ Dataplane URL not found in application response'));
58
+ logger.error(chalk.gray('\nApplication response:'));
59
+ logger.error(chalk.gray(JSON.stringify(application, null, 2)));
60
+ throw new Error('Dataplane URL not found in application configuration');
61
+ }
62
+
63
+ return dataplaneUrl;
64
+ }
65
+
66
+ /**
67
+ * Deploys datasource to dataplane
68
+ *
69
+ * @async
70
+ * @function deployDatasource
71
+ * @param {string} appKey - Application key
72
+ * @param {string} filePath - Path to datasource JSON file
73
+ * @param {Object} options - Deployment options
74
+ * @param {string} options.controller - Controller URL
75
+ * @param {string} options.environment - Environment key
76
+ * @returns {Promise<Object>} Deployment result
77
+ * @throws {Error} If deployment fails
78
+ */
79
+ async function deployDatasource(appKey, filePath, options) {
80
+ if (!appKey || typeof appKey !== 'string') {
81
+ throw new Error('Application key is required');
82
+ }
83
+ if (!filePath || typeof filePath !== 'string') {
84
+ throw new Error('File path is required');
85
+ }
86
+ if (!options.controller) {
87
+ throw new Error('Controller URL is required (--controller)');
88
+ }
89
+ if (!options.environment) {
90
+ throw new Error('Environment is required (-e, --environment)');
91
+ }
92
+
93
+ logger.log(chalk.blue('📋 Deploying datasource...\n'));
94
+
95
+ // Validate datasource file
96
+ logger.log(chalk.blue('🔍 Validating datasource file...'));
97
+ const validation = await validateDatasourceFile(filePath);
98
+ if (!validation.valid) {
99
+ logger.error(chalk.red('❌ Datasource validation failed:'));
100
+ validation.errors.forEach(error => {
101
+ logger.error(chalk.red(` • ${error}`));
102
+ });
103
+ throw new Error('Datasource file validation failed');
104
+ }
105
+ logger.log(chalk.green('✓ Datasource file is valid'));
106
+
107
+ // Load datasource configuration
108
+ const content = fs.readFileSync(filePath, 'utf8');
109
+ let datasourceConfig;
110
+ try {
111
+ datasourceConfig = JSON.parse(content);
112
+ } catch (error) {
113
+ throw new Error(`Failed to parse datasource file: ${error.message}`);
114
+ }
115
+
116
+ // Extract systemKey
117
+ const systemKey = datasourceConfig.systemKey;
118
+ if (!systemKey) {
119
+ throw new Error('systemKey is required in datasource configuration');
120
+ }
121
+
122
+ // Get authentication
123
+ logger.log(chalk.blue('🔐 Getting authentication...'));
124
+ const authConfig = await getDeploymentAuth(options.controller, options.environment, appKey);
125
+ logger.log(chalk.green('✓ Authentication successful'));
126
+
127
+ // Get dataplane URL from controller
128
+ logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
129
+ const dataplaneUrl = await getDataplaneUrl(options.controller, appKey, options.environment, authConfig);
130
+ logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
131
+
132
+ // Deploy to dataplane
133
+ logger.log(chalk.blue('\n🚀 Deploying to dataplane...'));
134
+ const deployEndpoint = `${dataplaneUrl}/api/v1/pipeline/${systemKey}/deploy`;
135
+
136
+ // Prepare deployment request
137
+ const deployRequest = {
138
+ datasource: datasourceConfig
139
+ };
140
+
141
+ // Make API call to dataplane
142
+ // This is a placeholder - actual API structure may vary
143
+ let deployResponse;
144
+ if (authConfig.type === 'bearer' && authConfig.token) {
145
+ deployResponse = await authenticatedApiCall(
146
+ deployEndpoint,
147
+ {
148
+ method: 'POST',
149
+ body: JSON.stringify(deployRequest)
150
+ },
151
+ authConfig.token
152
+ );
153
+ } else {
154
+ throw new Error('Bearer token authentication required for dataplane deployment');
155
+ }
156
+
157
+ if (!deployResponse.success) {
158
+ const formattedError = deployResponse.formattedError || formatApiError(deployResponse);
159
+ logger.error(chalk.red('❌ Deployment failed:'));
160
+ logger.error(formattedError);
161
+ throw new Error(`Dataplane deployment failed: ${formattedError}`);
162
+ }
163
+
164
+ logger.log(chalk.green('\n✓ Datasource deployed successfully!'));
165
+ logger.log(chalk.blue(`\nDatasource: ${datasourceConfig.key || datasourceConfig.displayName}`));
166
+ logger.log(chalk.blue(`System: ${systemKey}`));
167
+ logger.log(chalk.blue(`Environment: ${options.environment}`));
168
+
169
+ return {
170
+ success: true,
171
+ datasourceKey: datasourceConfig.key,
172
+ systemKey: systemKey,
173
+ environment: options.environment,
174
+ dataplaneUrl: dataplaneUrl
175
+ };
176
+ }
177
+
178
+ module.exports = {
179
+ deployDatasource,
180
+ getDataplaneUrl
181
+ };
182
+
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Datasource Diff Command
3
+ *
4
+ * Compares two datasource configuration files.
5
+ * Specialized for dataplane deployment validation.
6
+ *
7
+ * @fileoverview Datasource comparison for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const chalk = require('chalk');
13
+ const { compareFiles, formatDiffOutput } = require('./diff');
14
+ const logger = require('./utils/logger');
15
+
16
+ /**
17
+ * Compares two datasource files with focus on dataplane-relevant fields
18
+ *
19
+ * @async
20
+ * @function compareDatasources
21
+ * @param {string} file1 - Path to first datasource file
22
+ * @param {string} file2 - Path to second datasource file
23
+ * @returns {Promise<void>}
24
+ * @throws {Error} If comparison fails
25
+ */
26
+ async function compareDatasources(file1, file2) {
27
+ const result = await compareFiles(file1, file2);
28
+
29
+ // Filter and highlight dataplane-relevant changes
30
+ const dataplaneRelevant = {
31
+ fieldMappings: result.changed.filter(c => c.path.includes('fieldMappings')),
32
+ exposed: result.changed.filter(c => c.path.includes('exposed')),
33
+ sync: result.changed.filter(c => c.path.includes('sync')),
34
+ openapi: result.changed.filter(c => c.path.includes('openapi')),
35
+ mcp: result.changed.filter(c => c.path.includes('mcp'))
36
+ };
37
+
38
+ // Display standard diff
39
+ formatDiffOutput(result);
40
+
41
+ // Display dataplane-specific highlights
42
+ const hasDataplaneChanges = Object.values(dataplaneRelevant).some(arr => arr.length > 0);
43
+
44
+ if (hasDataplaneChanges) {
45
+ logger.log(chalk.blue('\n📊 Dataplane-Relevant Changes:'));
46
+
47
+ if (dataplaneRelevant.fieldMappings.length > 0) {
48
+ logger.log(chalk.yellow(` • Field Mappings: ${dataplaneRelevant.fieldMappings.length} changes`));
49
+ }
50
+ if (dataplaneRelevant.exposed.length > 0) {
51
+ logger.log(chalk.yellow(` • Exposed Fields: ${dataplaneRelevant.exposed.length} changes`));
52
+ }
53
+ if (dataplaneRelevant.sync.length > 0) {
54
+ logger.log(chalk.yellow(` • Sync Configuration: ${dataplaneRelevant.sync.length} changes`));
55
+ }
56
+ if (dataplaneRelevant.openapi.length > 0) {
57
+ logger.log(chalk.yellow(` • OpenAPI Configuration: ${dataplaneRelevant.openapi.length} changes`));
58
+ }
59
+ if (dataplaneRelevant.mcp.length > 0) {
60
+ logger.log(chalk.yellow(` • MCP Configuration: ${dataplaneRelevant.mcp.length} changes`));
61
+ }
62
+ }
63
+
64
+ // Exit with appropriate code
65
+ if (!result.identical) {
66
+ process.exit(1);
67
+ }
68
+ }
69
+
70
+ module.exports = {
71
+ compareDatasources
72
+ };
73
+
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Datasource List Command
3
+ *
4
+ * Lists datasources from an environment via controller API.
5
+ *
6
+ * @fileoverview Datasource listing for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const chalk = require('chalk');
12
+ const { getConfig } = require('./config');
13
+ const { getOrRefreshDeviceToken } = require('./utils/token-manager');
14
+ const { authenticatedApiCall } = require('./utils/api');
15
+ const { formatApiError } = require('./utils/api-error-handler');
16
+ const logger = require('./utils/logger');
17
+
18
+ /**
19
+ * Extracts datasources array from API response
20
+ * Handles multiple response formats similar to applications list
21
+ *
22
+ * @function extractDatasources
23
+ * @param {Object} response - API response from authenticatedApiCall
24
+ * @returns {Array} Array of datasources
25
+ * @throws {Error} If response format is invalid
26
+ */
27
+ function extractDatasources(response) {
28
+ const apiResponse = response.data;
29
+ let datasources;
30
+
31
+ // Check if apiResponse.data is an array (wrapped format)
32
+ if (apiResponse && apiResponse.data && Array.isArray(apiResponse.data)) {
33
+ datasources = apiResponse.data;
34
+ } else if (Array.isArray(apiResponse)) {
35
+ // Check if apiResponse is directly an array
36
+ datasources = apiResponse;
37
+ } else if (apiResponse && Array.isArray(apiResponse.items)) {
38
+ // Check if apiResponse.items is an array (paginated format)
39
+ datasources = apiResponse.items;
40
+ } else if (apiResponse && apiResponse.data && apiResponse.data.items && Array.isArray(apiResponse.data.items)) {
41
+ // Check if apiResponse.data.items is an array (wrapped paginated format)
42
+ datasources = apiResponse.data.items;
43
+ } else {
44
+ logger.error(chalk.red('❌ Invalid response: expected data array or items array'));
45
+ logger.error(chalk.gray('\nAPI response type:'), typeof apiResponse);
46
+ logger.error(chalk.gray('API response:'), JSON.stringify(apiResponse, null, 2));
47
+ throw new Error('Invalid API response format: expected array of datasources');
48
+ }
49
+
50
+ return datasources;
51
+ }
52
+
53
+ /**
54
+ * Displays datasources in a formatted table
55
+ *
56
+ * @function displayDatasources
57
+ * @param {Array} datasources - Array of datasource objects
58
+ * @param {string} environment - Environment key
59
+ */
60
+ function displayDatasources(datasources, environment) {
61
+ if (datasources.length === 0) {
62
+ logger.log(chalk.yellow(`\nNo datasources found in environment: ${environment}`));
63
+ return;
64
+ }
65
+
66
+ logger.log(chalk.blue(`\n📋 Datasources in environment: ${environment}\n`));
67
+ logger.log(chalk.gray('Key'.padEnd(30) + 'Display Name'.padEnd(30) + 'System Key'.padEnd(20) + 'Version'.padEnd(15) + 'Status'));
68
+ logger.log(chalk.gray('-'.repeat(120)));
69
+
70
+ datasources.forEach((ds) => {
71
+ const key = (ds.key || 'N/A').padEnd(30);
72
+ const displayName = (ds.displayName || 'N/A').padEnd(30);
73
+ const systemKey = (ds.systemKey || 'N/A').padEnd(20);
74
+ const version = (ds.version || 'N/A').padEnd(15);
75
+ const status = ds.enabled !== false ? chalk.green('enabled') : chalk.red('disabled');
76
+ logger.log(`${key}${displayName}${systemKey}${version}${status}`);
77
+ });
78
+ logger.log('');
79
+ }
80
+
81
+ /**
82
+ * Lists datasources from an environment
83
+ *
84
+ * @async
85
+ * @function listDatasources
86
+ * @param {Object} options - Command options
87
+ * @param {string} options.environment - Environment ID or key
88
+ * @throws {Error} If listing fails
89
+ */
90
+ async function listDatasources(options) {
91
+ const config = await getConfig();
92
+
93
+ // Try to get device token
94
+ let controllerUrl = null;
95
+ let token = null;
96
+
97
+ if (config.device) {
98
+ const deviceUrls = Object.keys(config.device);
99
+ if (deviceUrls.length > 0) {
100
+ controllerUrl = deviceUrls[0];
101
+ const deviceToken = await getOrRefreshDeviceToken(controllerUrl);
102
+ if (deviceToken && deviceToken.token) {
103
+ token = deviceToken.token;
104
+ controllerUrl = deviceToken.controller;
105
+ }
106
+ }
107
+ }
108
+
109
+ if (!token || !controllerUrl) {
110
+ logger.error(chalk.red('❌ Not logged in. Run: aifabrix login'));
111
+ logger.error(chalk.gray(' Use device code flow: aifabrix login --method device --controller <url>'));
112
+ process.exit(1);
113
+ }
114
+
115
+ // Call controller API - using placeholder endpoint until full specs available
116
+ // Expected: GET /api/v1/environments/{env}/datasources
117
+ const endpoint = `${controllerUrl}/api/v1/environments/${options.environment}/datasources`;
118
+ const response = await authenticatedApiCall(endpoint, {}, token);
119
+
120
+ if (!response.success || !response.data) {
121
+ const formattedError = response.formattedError || formatApiError(response);
122
+ logger.error(formattedError);
123
+ logger.error(chalk.gray('\nFull response for debugging:'));
124
+ logger.error(chalk.gray(JSON.stringify(response, null, 2)));
125
+ process.exit(1);
126
+ return; // Ensure we don't continue after exit
127
+ }
128
+
129
+ const datasources = extractDatasources(response);
130
+ displayDatasources(datasources, options.environment);
131
+ }
132
+
133
+ module.exports = {
134
+ listDatasources,
135
+ displayDatasources,
136
+ extractDatasources
137
+ };
138
+