@aifabrix/builder 2.33.5 → 2.36.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,14 +13,17 @@ const {
13
13
  promptForOpenApiFile,
14
14
  promptForOpenApiUrl,
15
15
  promptForMcpServer,
16
+ promptForCredentialAction,
16
17
  promptForKnownPlatform,
17
18
  promptForUserIntent,
18
19
  promptForUserPreferences,
19
20
  promptForConfigReview,
20
- promptForAppName
21
+ promptForAppName,
22
+ promptForRunWithSavedConfig
21
23
  } = require('../generator/wizard-prompts');
22
24
  const {
23
25
  validateAndCheckAppDirectory,
26
+ formatDataplaneRejectedTokenMessage,
24
27
  handleOpenApiParsing,
25
28
  handleCredentialSelection,
26
29
  handleTypeDetection,
@@ -30,7 +33,17 @@ const {
30
33
  setupDataplaneAndAuth
31
34
  } = require('./wizard-core');
32
35
  const { handleWizardHeadless } = require('./wizard-headless');
33
- const { createWizardSession, updateWizardSession } = require('../api/wizard.api');
36
+ const { createWizardSession, updateWizardSession, getWizardPlatforms } = require('../api/wizard.api');
37
+ const { getExternalSystem } = require('../api/external-systems.api');
38
+ const { writeWizardConfig, wizardConfigExists, validateWizardConfig } = require('../validation/wizard-config-validator');
39
+ const { appendWizardError } = require('../utils/cli-utils');
40
+ const {
41
+ buildPreferencesForSave,
42
+ buildWizardStateForSave,
43
+ showWizardConfigSummary,
44
+ ensureIntegrationDir,
45
+ isExternalSystemForAddDatasource
46
+ } = require('./wizard-helpers');
34
47
 
35
48
  /**
36
49
  * Extract session ID from response data
@@ -52,29 +65,29 @@ function extractSessionId(responseData) {
52
65
  }
53
66
 
54
67
  /**
55
- * Handle interactive mode selection step
68
+ * Create wizard session with given mode and optional systemIdOrKey (no prompts)
56
69
  * @async
57
- * @function handleInteractiveModeSelection
70
+ * @function createSessionFromParams
58
71
  * @param {string} dataplaneUrl - Dataplane URL
59
72
  * @param {Object} authConfig - Authentication configuration
60
- * @returns {Promise<Object>} Object with mode and sessionId
73
+ * @param {string} mode - Mode ('create-system' | 'add-datasource')
74
+ * @param {string} [systemIdOrKey] - System ID or key (for add-datasource)
75
+ * @param {string} [appName] - Application name (for 401 hint)
76
+ * @returns {Promise<string>} Session ID
61
77
  */
62
- async function handleInteractiveModeSelection(dataplaneUrl, authConfig) {
63
- logger.log(chalk.blue('\n\uD83D\uDCCB Step 1: Mode Selection'));
64
- const mode = await promptForMode();
65
- let systemIdOrKey = null;
66
- if (mode === 'add-datasource') {
67
- systemIdOrKey = await promptForSystemIdOrKey();
68
- }
69
- const sessionResponse = await createWizardSession(dataplaneUrl, authConfig, mode, systemIdOrKey);
78
+ async function createSessionFromParams(dataplaneUrl, authConfig, mode, systemIdOrKey, appName) {
79
+ const sessionResponse = await createWizardSession(dataplaneUrl, authConfig, mode, systemIdOrKey || null);
70
80
  if (!sessionResponse.success || !sessionResponse.data) {
71
81
  const errorMsg = sessionResponse.formattedError || sessionResponse.error ||
72
82
  sessionResponse.errorData?.detail || sessionResponse.message ||
73
83
  (sessionResponse.status ? `HTTP ${sessionResponse.status}` : 'Unknown error');
74
- throw new Error(`Failed to create wizard session: ${errorMsg}`);
84
+ const apiMessage = sessionResponse.errorData?.message || sessionResponse.errorData?.detail || sessionResponse.error || '';
85
+ const fullMsg = sessionResponse.status === 401
86
+ ? formatDataplaneRejectedTokenMessage(dataplaneUrl, appName, apiMessage)
87
+ : `Failed to create wizard session: ${errorMsg}`;
88
+ throw new Error(fullMsg);
75
89
  }
76
- const sessionId = extractSessionId(sessionResponse.data);
77
- return { mode, sessionId, systemIdOrKey };
90
+ return extractSessionId(sessionResponse.data);
78
91
  }
79
92
 
80
93
  /**
@@ -84,11 +97,12 @@ async function handleInteractiveModeSelection(dataplaneUrl, authConfig) {
84
97
  * @param {string} dataplaneUrl - Dataplane URL
85
98
  * @param {string} sessionId - Wizard session ID
86
99
  * @param {Object} authConfig - Authentication configuration
100
+ * @param {Array<{key: string, displayName?: string}>} [platforms] - Known platforms from dataplane (empty = hide "Known platform")
87
101
  * @returns {Promise<Object>} Object with sourceType and sourceData
88
102
  */
89
- async function handleInteractiveSourceSelection(dataplaneUrl, sessionId, authConfig) {
103
+ async function handleInteractiveSourceSelection(dataplaneUrl, sessionId, authConfig, platforms = []) {
90
104
  logger.log(chalk.blue('\n\uD83D\uDCCB Step 2: Source Selection'));
91
- const sourceType = await promptForSourceType();
105
+ const sourceType = await promptForSourceType(platforms);
92
106
  let sourceData = null;
93
107
  const updateData = { currentStep: 1 };
94
108
 
@@ -102,7 +116,7 @@ async function handleInteractiveSourceSelection(dataplaneUrl, sessionId, authCon
102
116
  sourceData = JSON.stringify(mcpDetails);
103
117
  updateData.mcpServerUrl = mcpDetails.url || null;
104
118
  } else if (sourceType === 'known-platform') {
105
- sourceData = await promptForKnownPlatform();
119
+ sourceData = await promptForKnownPlatform(platforms);
106
120
  }
107
121
 
108
122
  const updateResponse = await updateWizardSession(dataplaneUrl, sessionId, authConfig, updateData);
@@ -125,7 +139,7 @@ async function handleInteractiveSourceSelection(dataplaneUrl, sessionId, authCon
125
139
  * @param {Object} options.detectedType - Detected type info
126
140
  * @param {string} [options.credentialIdOrKey] - Credential ID or key (optional)
127
141
  * @param {string} [options.systemIdOrKey] - System ID or key (optional)
128
- * @returns {Promise<Object>} Generated configuration
142
+ * @returns {Promise<Object>} Generated configuration and preferences { systemConfig, datasourceConfigs, systemKey, preferences }
129
143
  */
130
144
  async function handleInteractiveConfigGeneration(options) {
131
145
  logger.log(chalk.blue('\n\uD83D\uDCCB Step 5: User Preferences'));
@@ -139,7 +153,7 @@ async function handleInteractiveConfigGeneration(options) {
139
153
  enableRBAC: preferences.rbac
140
154
  };
141
155
 
142
- return await handleConfigurationGeneration(options.dataplaneUrl, options.authConfig, {
156
+ const result = await handleConfigurationGeneration(options.dataplaneUrl, options.authConfig, {
143
157
  mode: options.mode,
144
158
  openapiSpec: options.openapiSpec,
145
159
  detectedType: options.detectedType,
@@ -147,6 +161,11 @@ async function handleInteractiveConfigGeneration(options) {
147
161
  credentialIdOrKey: options.credentialIdOrKey,
148
162
  systemIdOrKey: options.systemIdOrKey
149
163
  });
164
+
165
+ return {
166
+ ...result,
167
+ preferences: buildPreferencesForSave(userIntent, preferences)
168
+ };
150
169
  }
151
170
 
152
171
  /**
@@ -177,32 +196,37 @@ async function handleConfigurationReview(dataplaneUrl, authConfig, systemConfig,
177
196
  }
178
197
 
179
198
  /**
180
- * Execute wizard flow steps (interactive mode)
199
+ * Run steps 2–7 after session is created (source, credential, type, generate, review, save).
200
+ * On any error, saves partial wizard.yaml with all collected state so far, appends to error.log, then rethrows.
181
201
  * @async
182
- * @function executeWizardFlow
183
- * @param {string} appName - Application name
202
+ * @param {string} appKey - Application key
184
203
  * @param {string} dataplaneUrl - Dataplane URL
185
204
  * @param {Object} authConfig - Authentication configuration
186
- * @returns {Promise<void>} Resolves when wizard flow completes
205
+ * @param {string} sessionId - Wizard session ID
206
+ * @param {Object} flowOpts - Mode, systemIdOrKey, platforms, configPath
207
+ * @returns {Promise<Object>} Collected state (source, credential, preferences) for wizard.yaml save
187
208
  */
188
- async function executeWizardFlow(appName, dataplaneUrl, authConfig) {
189
- // Step 1: Mode Selection
190
- const { mode, sessionId, systemIdOrKey } = await handleInteractiveModeSelection(dataplaneUrl, authConfig);
191
-
192
- // Step 2: Source Selection
193
- const { sourceType, sourceData } = await handleInteractiveSourceSelection(dataplaneUrl, sessionId, authConfig);
209
+ async function doWizardSteps(appKey, dataplaneUrl, authConfig, sessionId, flowOpts, state) {
210
+ const { mode, systemIdOrKey, platforms } = flowOpts;
211
+ const { sourceType, sourceData } = await handleInteractiveSourceSelection(
212
+ dataplaneUrl, sessionId, authConfig, platforms
213
+ );
214
+ state.source = { type: sourceType };
215
+ if (sourceType === 'openapi-file') state.source.filePath = sourceData;
216
+ else if (sourceType === 'openapi-url') state.source.url = sourceData;
217
+ else if (sourceType === 'mcp-server') state.source.serverUrl = JSON.parse(sourceData).serverUrl;
218
+ else if (sourceType === 'known-platform') state.source.platform = sourceData;
194
219
 
195
- // Parse OpenAPI (part of step 2)
196
220
  const openapiSpec = await handleOpenApiParsing(dataplaneUrl, authConfig, sourceType, sourceData);
221
+ const credentialAction = await promptForCredentialAction();
222
+ const configCredential = credentialAction.action === 'skip'
223
+ ? { action: 'skip' }
224
+ : { action: credentialAction.action, credentialIdOrKey: credentialAction.credentialIdOrKey };
225
+ state.credential = configCredential;
226
+ const credentialIdOrKey = await handleCredentialSelection(dataplaneUrl, authConfig, configCredential);
197
227
 
198
- // Step 3: Credential Selection (optional)
199
- const credentialIdOrKey = await handleCredentialSelection(dataplaneUrl, authConfig);
200
-
201
- // Step 4: Detect Type
202
228
  const detectedType = await handleTypeDetection(dataplaneUrl, authConfig, openapiSpec);
203
-
204
- // Step 5: Generate Configuration
205
- const { systemConfig, datasourceConfigs, systemKey } = await handleInteractiveConfigGeneration({
229
+ const genResult = await handleInteractiveConfigGeneration({
206
230
  dataplaneUrl,
207
231
  authConfig,
208
232
  mode,
@@ -211,60 +235,249 @@ async function executeWizardFlow(appName, dataplaneUrl, authConfig) {
211
235
  credentialIdOrKey,
212
236
  systemIdOrKey
213
237
  });
238
+ const { systemConfig, datasourceConfigs, systemKey, preferences: savedPrefs } = genResult;
239
+ state.preferences = savedPrefs || {};
214
240
 
215
- // Step 6-7: Review & Validate
216
241
  const finalConfigs = await handleConfigurationReview(dataplaneUrl, authConfig, systemConfig, datasourceConfigs);
217
- if (!finalConfigs) return;
242
+ if (!finalConfigs) return null;
218
243
 
219
- // Step 7: Save Files
220
244
  await handleFileSaving(
221
- appName,
245
+ appKey,
222
246
  finalConfigs.systemConfig,
223
247
  finalConfigs.datasourceConfigs,
224
- systemKey || appName,
248
+ systemKey || appKey,
225
249
  dataplaneUrl,
226
250
  authConfig
227
251
  );
252
+ return state;
253
+ }
254
+
255
+ async function runWizardStepsAfterSession(appKey, dataplaneUrl, authConfig, sessionId, flowOpts) {
256
+ const { mode, systemIdOrKey, configPath } = flowOpts;
257
+ const state = { appKey, mode, systemIdOrKey: mode === 'add-datasource' ? systemIdOrKey : undefined };
258
+
259
+ const savePartialOnError = async(err) => {
260
+ state.preferences = state.preferences || {};
261
+ if (configPath) {
262
+ try {
263
+ await writeWizardConfig(configPath, buildWizardStateForSave(state));
264
+ } catch (e) {
265
+ logger.warn(`Could not save partial wizard.yaml: ${e.message}`);
266
+ }
267
+ }
268
+ await appendWizardError(appKey, err);
269
+ err.wizardResumeMessage = `To resume: aifabrix wizard ${appKey}\nSee integration/${appKey}/error.log for details.`;
270
+ err.wizardPartialSaved = true;
271
+ };
272
+
273
+ try {
274
+ return await doWizardSteps(appKey, dataplaneUrl, authConfig, sessionId, flowOpts, state);
275
+ } catch (err) {
276
+ await savePartialOnError(err);
277
+ throw err;
278
+ }
228
279
  }
229
280
 
230
281
  /**
231
- * Handle wizard command
282
+ * Execute wizard flow steps (interactive mode). Mode and systemIdOrKey are already set; creates session then runs steps 2–7.
232
283
  * @async
233
- * @function handleWizard
284
+ * @function executeWizardFlow
285
+ * @param {string} appKey - Application/integration key (folder name under integration/)
286
+ * @param {string} dataplaneUrl - Dataplane URL
287
+ * @param {Object} authConfig - Authentication configuration
288
+ * @param {Object} flowOpts - Flow options (mode, systemIdOrKey, configPath)
289
+ * @returns {Promise<void>} Resolves when wizard flow completes
290
+ */
291
+ async function executeWizardFlow(appKey, dataplaneUrl, authConfig, flowOpts = {}) {
292
+ const { mode, systemIdOrKey, configPath } = flowOpts;
293
+
294
+ logger.log(chalk.blue('\n\uD83D\uDCCB Step 1: Create Session'));
295
+ const sessionId = await createSessionFromParams(dataplaneUrl, authConfig, mode, systemIdOrKey, appKey);
296
+ logger.log(chalk.green('\u2713 Session created'));
297
+
298
+ const platforms = await getWizardPlatforms(dataplaneUrl, authConfig);
299
+ const state = await runWizardStepsAfterSession(appKey, dataplaneUrl, authConfig, sessionId, {
300
+ mode,
301
+ systemIdOrKey,
302
+ platforms,
303
+ configPath
304
+ });
305
+ if (!state) return;
306
+
307
+ if (configPath) {
308
+ await writeWizardConfig(configPath, buildWizardStateForSave(state));
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Load wizard config from configPath if it exists (for prefill)
314
+ * @param {string} [configPath] - Path to wizard.yaml (e.g. integration/<app>/wizard.yaml)
315
+ * @param {string} [appName] - App name (for log message)
316
+ * @returns {Promise<Object|null>} Loaded config or null
317
+ */
318
+ async function loadWizardConfigIfExists(configPath, appName) {
319
+ if (!configPath) return null;
320
+ const displayPath = appName ? `integration/${appName}/wizard.yaml` : configPath;
321
+ try {
322
+ const exists = await wizardConfigExists(configPath);
323
+ if (!exists) {
324
+ logger.log(chalk.gray(`No saved state at ${displayPath}; starting from step 1.`));
325
+ return null;
326
+ }
327
+ const result = await validateWizardConfig(configPath, { validateFilePaths: false });
328
+ if (result.valid && result.config) {
329
+ logger.log(chalk.green(`Loaded saved state from ${displayPath}. Resuming with saved choices.`));
330
+ return result.config;
331
+ }
332
+ if (result.errors?.length) {
333
+ logger.log(chalk.yellow(`Loaded ${displayPath} but it has errors; prompting for missing fields.`));
334
+ }
335
+ } catch (e) {
336
+ logger.log(chalk.gray(`Could not load wizard config from ${displayPath}: ${e.message}`));
337
+ }
338
+ return null;
339
+ }
340
+
341
+ /**
342
+ * Resolve appKey, configPath, dataplane and auth for "create-system" mode
234
343
  * @param {Object} options - Command options
235
- * @param {string} [options.app] - Application name
236
- * @param {string} [options.controller] - Controller URL
237
- * @param {string} [options.environment] - Environment key
238
- * @param {string} [options.dataplane] - Dataplane URL (overrides controller lookup)
239
- * @param {string} [options.config] - Path to wizard.yaml config file (headless mode)
240
- * @returns {Promise<void>} Resolves when wizard completes
241
- * @throws {Error} If wizard fails
344
+ * @param {Object} [loadedConfig] - Loaded wizard config
345
+ * @returns {Promise<Object|null>} { appKey, configPath, dataplaneUrl, authConfig } or null if cancelled
242
346
  */
243
- async function handleWizard(options = {}) {
244
- // Check if headless mode (config file provided)
245
- if (options.config) {
246
- return await handleWizardHeadless(options);
347
+ async function resolveCreateNewPath(options, loadedConfig) {
348
+ const appName = options.app || loadedConfig?.appName || (await promptForAppName(loadedConfig?.appName));
349
+ const shouldContinue = await validateAndCheckAppDirectory(appName, true);
350
+ if (!shouldContinue) return null;
351
+ const appKey = appName;
352
+ const configPath = await ensureIntegrationDir(appKey);
353
+ const { dataplaneUrl, authConfig } = await setupDataplaneAndAuth(options, appName);
354
+ return { appKey, configPath, dataplaneUrl, authConfig };
355
+ }
356
+
357
+ /**
358
+ * Resolve appKey, configPath, dataplane and auth for "add-datasource" mode (validates system)
359
+ * @param {Object} options - Command options
360
+ * @param {Object} [loadedConfig] - Loaded wizard config
361
+ * @returns {Promise<Object>} { appKey, configPath, dataplaneUrl, authConfig, systemIdOrKey }
362
+ */
363
+ async function resolveAddDatasourcePath(options, loadedConfig) {
364
+ let systemIdOrKey = loadedConfig?.systemIdOrKey || (await promptForSystemIdOrKey(loadedConfig?.systemIdOrKey));
365
+ const { dataplaneUrl, authConfig } = await setupDataplaneAndAuth(options, systemIdOrKey || 'wizard');
366
+ let systemResponse;
367
+ for (;;) {
368
+ try {
369
+ systemResponse = await getExternalSystem(dataplaneUrl, systemIdOrKey, authConfig);
370
+ const sys = systemResponse?.data || systemResponse;
371
+ if (sys && (systemResponse?.data || systemResponse?.success)) {
372
+ if (!isExternalSystemForAddDatasource(sys)) {
373
+ logger.log(chalk.red('Cannot add datasource to a webapp. Please enter an external system ID or key.'));
374
+ systemIdOrKey = await promptForSystemIdOrKey(systemIdOrKey);
375
+ continue;
376
+ }
377
+ break;
378
+ }
379
+ } catch (err) {
380
+ logger.log(chalk.red(`System not found or error: ${err.message}`));
381
+ }
382
+ systemIdOrKey = await promptForSystemIdOrKey(systemIdOrKey);
247
383
  }
384
+ const sys = systemResponse?.data || systemResponse;
385
+ const appKey = sys?.key || sys?.systemKey || systemIdOrKey;
386
+ const configPath = await ensureIntegrationDir(appKey);
387
+ return { appKey, configPath, dataplaneUrl, authConfig, systemIdOrKey };
388
+ }
248
389
 
249
- logger.log(chalk.blue('\n\uD83E\uDDD9 AI Fabrix External System Wizard\n'));
390
+ /**
391
+ * On wizard error: append to error.log, save partial wizard.yaml, set resume message
392
+ * @param {string} appKey - Application key
393
+ * @param {string} [configPath] - Path to wizard.yaml
394
+ * @param {string} mode - Wizard mode
395
+ * @param {string} [systemIdOrKey] - System ID (add-datasource)
396
+ * @param {Error} error - The error
397
+ */
398
+ async function handleWizardError(appKey, configPath, mode, systemIdOrKey, error) {
399
+ await appendWizardError(appKey, error);
400
+ if (!error.wizardPartialSaved && configPath) {
401
+ const partial = buildWizardStateForSave({
402
+ appKey,
403
+ mode,
404
+ systemIdOrKey: mode === 'add-datasource' ? systemIdOrKey : undefined
405
+ });
406
+ try {
407
+ await writeWizardConfig(configPath, partial);
408
+ } catch (e) {
409
+ logger.warn(`Could not save partial wizard.yaml: ${e.message}`);
410
+ }
411
+ }
412
+ error.wizardResumeMessage = `To resume: aifabrix wizard ${appKey}\nSee integration/${appKey}/error.log for details.`;
413
+ }
250
414
 
251
- // Get or prompt for app name
252
- let appName = options.app;
253
- if (!appName) {
254
- appName = await promptForAppName();
415
+ /**
416
+ * Handle wizard command (mode-first, load/save wizard.yaml, error.log on failure)
417
+ * @async
418
+ * @function handleWizard
419
+ * @param {Object} options - Command options
420
+ * @param {string} [options.app] - Application name (from positional or -a)
421
+ * @param {string} [options.config] - Path to wizard.yaml (headless mode)
422
+ * @param {string} [options.configPath] - Resolved path integration/<app>/wizard.yaml for load/save
423
+ * @returns {Promise<void>} Resolves when wizard completes
424
+ * @throws {Error} If wizard fails (wizardResumeMessage set when appKey known)
425
+ */
426
+ async function handleWizardSilent(options) {
427
+ if (!options.configPath) {
428
+ throw new Error('--silent requires an app name (e.g. aifabrix wizard test --silent)');
429
+ }
430
+ const result = await validateWizardConfig(options.configPath, { validateFilePaths: false });
431
+ if (!result.valid || !result.config) {
432
+ const displayPath = options.app ? `integration/${options.app}/wizard.yaml` : '';
433
+ const errMsg = result.errors?.length ? result.errors.join('; ') : 'Invalid or missing wizard.yaml';
434
+ throw new Error(`Cannot run --silent: ${displayPath} is invalid or missing. ${errMsg}`);
255
435
  }
436
+ return await handleWizardHeadless({ ...options, config: options.configPath });
437
+ }
256
438
 
257
- // Validate app name and check directory
258
- const shouldContinue = await validateAndCheckAppDirectory(appName, true);
259
- if (!shouldContinue) {
439
+ async function handleWizardWithSavedConfig(options, loadedConfig, displayPath) {
440
+ showWizardConfigSummary(loadedConfig, displayPath);
441
+ const runWithSaved = await promptForRunWithSavedConfig();
442
+ if (!runWithSaved) {
443
+ logger.log(chalk.gray(`To change settings, edit ${displayPath} and run: aifabrix wizard ${options.app}`));
260
444
  return;
261
445
  }
446
+ logger.log(chalk.gray(`Running with saved config from ${displayPath}...\n`));
447
+ return await handleWizardHeadless({ ...options, config: options.configPath });
448
+ }
262
449
 
263
- // Get dataplane URL and authentication
264
- const { dataplaneUrl, authConfig } = await setupDataplaneAndAuth(options, appName);
450
+ async function handleWizardInteractive(options) {
451
+ const mode = await promptForMode();
452
+ const resolved = mode === 'create-system'
453
+ ? await resolveCreateNewPath(options, null)
454
+ : await resolveAddDatasourcePath(options, null);
455
+ if (!resolved) return;
456
+ const { appKey, configPath, dataplaneUrl, authConfig } = resolved;
457
+ const systemIdOrKey = mode === 'add-datasource' ? resolved.systemIdOrKey : undefined;
458
+ try {
459
+ await executeWizardFlow(appKey, dataplaneUrl, authConfig, { mode, systemIdOrKey, configPath });
460
+ logger.log(chalk.gray(`To change settings, edit integration/${appKey}/wizard.yaml and run: aifabrix wizard ${appKey}`));
461
+ } catch (error) {
462
+ await handleWizardError(appKey, configPath, mode, systemIdOrKey, error);
463
+ throw error;
464
+ }
465
+ }
265
466
 
266
- // Execute wizard flow
267
- await executeWizardFlow(appName, dataplaneUrl, authConfig);
467
+ async function handleWizard(options = {}) {
468
+ if (options.config) {
469
+ return await handleWizardHeadless(options);
470
+ }
471
+ const displayPath = options.app ? `integration/${options.app}/wizard.yaml` : '';
472
+ if (options.silent && options.app) {
473
+ return await handleWizardSilent(options);
474
+ }
475
+ logger.log(chalk.blue('\n\uD83E\uDDD9 AI Fabrix External System Wizard\n'));
476
+ const loadedConfig = await loadWizardConfigIfExists(options.configPath, options.app);
477
+ if (loadedConfig) {
478
+ return await handleWizardWithSavedConfig(options, loadedConfig, displayPath);
479
+ }
480
+ return await handleWizardInteractive(options);
268
481
  }
269
482
 
270
483
  module.exports = { handleWizard, handleWizardHeadless };
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Datasource List Command
3
3
  *
4
- * Lists datasources from an environment via dataplane API.
5
- * Gets dataplane URL from controller, then lists datasources from dataplane.
4
+ * Lists datasources from the dataplane (GET /api/v1/external/).
5
+ * Resolves dataplane URL from the controller, then calls the dataplane list API.
6
6
  *
7
7
  * @fileoverview Datasource listing for AI Fabrix Builder
8
8
  * @author AI Fabrix Team
@@ -12,7 +12,7 @@
12
12
  const chalk = require('chalk');
13
13
  const { getConfig, resolveEnvironment } = require('../core/config');
14
14
  const { getOrRefreshDeviceToken } = require('../utils/token-manager');
15
- const { listDatasources: listDatasourcesFromDataplane } = require('../api/datasources-core.api');
15
+ const { listDatasources: listDatasourcesFromDataplane } = require('../api/datasources.api');
16
16
  const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
17
17
  const { formatApiError } = require('../utils/api-error-handler');
18
18
  const logger = require('../utils/logger');
@@ -291,8 +291,11 @@ async function listDatasources(_options) {
291
291
 
292
292
  const controllerUrl = validateControllerUrl(authInfo.controllerUrl);
293
293
  const authConfig = setupAuthConfig(authInfo.token, controllerUrl);
294
+
295
+ // Resolve dataplane URL first (required for list call)
294
296
  const dataplaneUrl = await resolveAndValidateDataplaneUrl(controllerUrl, environment, authConfig);
295
297
 
298
+ // List datasources from dataplane (GET /api/v1/external/)
296
299
  const response = await listDatasourcesFromDataplane(dataplaneUrl, authConfig);
297
300
 
298
301
  if (!response.success || !response.data) {
@@ -91,6 +91,37 @@ function buildAndValidateDeployment(appName, variables, envTemplate, rbac) {
91
91
  return deployment;
92
92
  }
93
93
 
94
+ /**
95
+ * Builds deployment manifest in memory (no file write, no schema validation).
96
+ * Same structure as generateDeployJson output; used by "aifabrix show" offline.
97
+ * @async
98
+ * @function buildDeploymentManifestInMemory
99
+ * @param {string} appName - Application name
100
+ * @param {Object} [options] - Options (e.g. type for external)
101
+ * @returns {Promise<{ deployment: Object, appPath: string }>} Manifest and app path
102
+ * @throws {Error} If variables.yaml/env.template missing or generation fails
103
+ */
104
+ async function buildDeploymentManifestInMemory(appName, options = {}) {
105
+ if (!appName || typeof appName !== 'string') {
106
+ throw new Error('App name is required and must be a string');
107
+ }
108
+
109
+ const { isExternal, appPath, appType } = await detectAppType(appName, options);
110
+
111
+ if (isExternal) {
112
+ const manifest = await generateControllerManifest(appName);
113
+ return { deployment: manifest, appPath };
114
+ }
115
+
116
+ const { variables, envTemplate, rbac } = loadDeploymentConfigFiles(appPath, appType, appName);
117
+ const configuration = parseEnvironmentVariables(envTemplate, variables);
118
+ const deployment = builders.buildManifestStructure(appName, variables, null, configuration, rbac);
119
+ const deploymentKey = _keyGenerator.generateDeploymentKeyFromJson(deployment);
120
+ deployment.deploymentKey = deploymentKey;
121
+
122
+ return { deployment, appPath };
123
+ }
124
+
94
125
  async function generateDeployJson(appName, options = {}) {
95
126
  if (!appName || typeof appName !== 'string') {
96
127
  throw new Error('App name is required and must be a string');
@@ -153,6 +184,7 @@ async function generateDeployJsonWithValidation(appName, options = {}) {
153
184
  module.exports = {
154
185
  generateDeployJson,
155
186
  generateDeployJsonWithValidation,
187
+ buildDeploymentManifestInMemory,
156
188
  generateExternalSystemApplicationSchema,
157
189
  splitExternalApplicationSchema,
158
190
  parseEnvironmentVariables,