@aifabrix/builder 2.33.6 → 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.
@@ -0,0 +1,642 @@
1
+ /**
2
+ * AI Fabrix Builder - App Show Command
3
+ *
4
+ * Displays application info from local builder/integration (offline) or from
5
+ * controller (--online). Does not run schema validation; use aifabrix validate for that.
6
+ *
7
+ * @fileoverview App show command implementation
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+ /* eslint-disable max-lines -- show: offline/online summary builders, auth, display wiring */
12
+
13
+ 'use strict';
14
+
15
+ const path = require('path');
16
+ const fs = require('fs');
17
+ const yaml = require('js-yaml');
18
+ const logger = require('../utils/logger');
19
+ const { detectAppType } = require('../utils/paths');
20
+ const generator = require('../generator');
21
+ const { getConfig, normalizeControllerUrl } = require('../core/config');
22
+ const { getOrRefreshDeviceToken } = require('../utils/token-manager');
23
+ const { resolveControllerUrl } = require('../utils/controller-url');
24
+ const { resolveEnvironment } = require('../core/config');
25
+ const { getApplication } = require('../api/applications.api');
26
+ const {
27
+ getExternalSystemConfig,
28
+ listOpenAPIFiles,
29
+ listOpenAPIEndpoints
30
+ } = require('../api/external-systems.api');
31
+ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
32
+ const { formatApiError } = require('../utils/api-error-handler');
33
+ const { formatAuthenticationError } = require('../utils/error-formatters/http-status-errors');
34
+ const { display: displayShow } = require('./show-display');
35
+
36
+ /** Truncate deployment key for display */
37
+ const DEPLOYMENT_KEY_TRUNCATE_LEN = 12;
38
+
39
+ /**
40
+ * Load and parse variables.yaml from app path (no validation).
41
+ * @param {string} appPath - Application directory path
42
+ * @returns {Object} Parsed variables
43
+ * @throws {Error} If file not found or invalid YAML
44
+ */
45
+ function loadVariablesFromPath(appPath) {
46
+ const variablesPath = path.join(appPath, 'variables.yaml');
47
+ if (!fs.existsSync(variablesPath)) {
48
+ throw new Error(`variables.yaml not found for app (path: ${variablesPath}). Use aifabrix validate to check.`);
49
+ }
50
+ const content = fs.readFileSync(variablesPath, 'utf8');
51
+ try {
52
+ const parsed = yaml.load(content);
53
+ if (!parsed || typeof parsed !== 'object') {
54
+ throw new Error('variables.yaml is empty or invalid');
55
+ }
56
+ return parsed;
57
+ } catch (error) {
58
+ if (error.message.includes('variables.yaml')) throw error;
59
+ throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Collect portal input configuration entries (label + value; masked as "(masked)" if portalInput.masked).
65
+ * @param {Object} variables - Parsed variables
66
+ * @returns {Array<{label: string, value: string}>}
67
+ */
68
+ function getPortalInputConfigurations(variables) {
69
+ const out = [];
70
+ const add = (configList) => {
71
+ if (!configList || !Array.isArray(configList)) return;
72
+ configList.forEach((item) => {
73
+ if (item.portalInput) {
74
+ const label = item.portalInput.label || item.name || item.portalInput.field || '—';
75
+ const value = item.portalInput.masked ? '(masked)' : (item.value ?? '');
76
+ out.push({ label, value });
77
+ }
78
+ });
79
+ };
80
+ add(variables.configuration);
81
+ (variables.conditionalConfiguration || []).forEach((block) => {
82
+ add(block.configuration);
83
+ });
84
+ return out;
85
+ }
86
+
87
+ function truncateDeploymentKey(key) {
88
+ if (!key || key.length <= DEPLOYMENT_KEY_TRUNCATE_LEN) return key ?? '—';
89
+ return `${key.slice(0, DEPLOYMENT_KEY_TRUNCATE_LEN)}...`;
90
+ }
91
+
92
+ function healthStrFromDeploy(deploy) {
93
+ const health = deploy.healthCheck;
94
+ if (!health || !health.path) return '—';
95
+ return `${health.path} (interval ${health.intervalSeconds ?? health.interval ?? 30}s)`;
96
+ }
97
+
98
+ function buildStrFromDeploy(deploy) {
99
+ const build = deploy.build;
100
+ if (!build || (!build.dockerfile && !build.envOutputPath)) return '—';
101
+ return `${build.dockerfile ? 'dockerfile' : '—'}, envOutputPath: ${build.envOutputPath ?? '—'}`;
102
+ }
103
+
104
+ function buildApplicationFromDeploy(deploy) {
105
+ const truncatedDeploy = truncateDeploymentKey(deploy.deploymentKey);
106
+ const application = {
107
+ key: deploy.key ?? '—',
108
+ displayName: deploy.displayName ?? '—',
109
+ description: deploy.description ?? '—',
110
+ type: deploy.type ?? 'webapp',
111
+ deploymentKey: truncatedDeploy,
112
+ image: deploy.image ?? '—',
113
+ registryMode: deploy.registryMode ?? '—',
114
+ port: (deploy.port !== undefined && deploy.port !== null) ? deploy.port : '—',
115
+ healthCheck: healthStrFromDeploy(deploy),
116
+ build: buildStrFromDeploy(deploy)
117
+ };
118
+ const extInt = deploy.externalIntegration;
119
+ if (deploy.type === 'external' && extInt) {
120
+ application.externalIntegration = {
121
+ schemaBasePath: extInt.schemaBasePath,
122
+ systems: extInt.systems || [],
123
+ dataSources: extInt.dataSources || []
124
+ };
125
+ }
126
+ return application;
127
+ }
128
+
129
+ function getDatabasesFromDeploy(deploy) {
130
+ if (Array.isArray(deploy.databases)) {
131
+ return deploy.databases.map((d) => (d && d.name) || d).filter(Boolean);
132
+ }
133
+ if (deploy.requiresDatabase && deploy.databases) {
134
+ return deploy.databases.map((d) => (d && d.name) || d).filter(Boolean);
135
+ }
136
+ return [];
137
+ }
138
+
139
+ /**
140
+ * Build offline summary from deployment manifest (same sections as online).
141
+ * @param {Object} deploy - Parsed deploy JSON
142
+ * @param {string} sourcePath - Path to deploy JSON for display
143
+ * @returns {Object} Summary with application, roles, permissions, etc.
144
+ */
145
+ function buildOfflineSummaryFromDeployJson(deploy, sourcePath) {
146
+ const application = buildApplicationFromDeploy(deploy);
147
+ const configList = deploy.configuration || deploy.system?.configuration || [];
148
+ const condConfig = deploy.conditionalConfiguration || deploy.system?.conditionalConfiguration || [];
149
+ const portalInputConfigurations = getPortalInputConfigurations(
150
+ { configuration: configList, conditionalConfiguration: Array.isArray(condConfig) ? condConfig : [] }
151
+ );
152
+ const databases = getDatabasesFromDeploy(deploy);
153
+ const authentication = deploy.authentication ?? deploy.system?.authentication ?? null;
154
+ return {
155
+ source: 'offline',
156
+ path: sourcePath,
157
+ appKey: deploy.key,
158
+ application,
159
+ roles: deploy.roles || [],
160
+ permissions: deploy.permissions || [],
161
+ authentication,
162
+ portalInputConfigurations,
163
+ databases,
164
+ isExternal: deploy.type === 'external'
165
+ };
166
+ }
167
+
168
+ function healthStrFromVariables(variables) {
169
+ const health = variables.healthCheck;
170
+ if (!health || !health.path) return '—';
171
+ return `${health.path} (interval ${health.intervalSeconds || 30}s)`;
172
+ }
173
+
174
+ function buildStrFromVariables(variables) {
175
+ const build = variables.build;
176
+ if (!build) return '—';
177
+ return `${build.language || '—'}, localPort ${build.localPort || '—'}`;
178
+ }
179
+
180
+ function buildApplicationFromVariables(variables) {
181
+ const app = variables.app || {};
182
+ const deploymentKey = app.deploymentKey;
183
+ const truncatedDeploy = (deploymentKey && deploymentKey.length > DEPLOYMENT_KEY_TRUNCATE_LEN)
184
+ ? `${deploymentKey.slice(0, DEPLOYMENT_KEY_TRUNCATE_LEN)}...`
185
+ : (deploymentKey || '—');
186
+ const application = {
187
+ key: app.key,
188
+ displayName: app.displayName,
189
+ description: app.description,
190
+ type: app.type || 'webapp',
191
+ deploymentKey: truncatedDeploy,
192
+ image: app.image || '—',
193
+ registryMode: app.registryMode || '—',
194
+ port: (app.port !== undefined && app.port !== null) ? app.port : '—',
195
+ healthCheck: healthStrFromVariables(variables),
196
+ build: buildStrFromVariables(variables)
197
+ };
198
+ const extInt = variables.externalIntegration;
199
+ if (app.type === 'external' && extInt) {
200
+ application.externalIntegration = {
201
+ schemaBasePath: extInt.schemaBasePath,
202
+ systems: extInt.systems || [],
203
+ dataSources: extInt.dataSources || []
204
+ };
205
+ }
206
+ return application;
207
+ }
208
+
209
+ /**
210
+ * Build offline summary object from variables (for display and JSON).
211
+ * @param {Object} variables - Parsed variables.yaml
212
+ * @param {string} sourcePath - Path to variables.yaml for display
213
+ * @returns {Object} Summary with application, roles, permissions, etc.
214
+ */
215
+ function buildOfflineSummary(variables, sourcePath) {
216
+ const application = buildApplicationFromVariables(variables);
217
+ const app = variables.app || {};
218
+ const databases = variables.requiresDatabase && variables.databases
219
+ ? variables.databases.map((d) => (d && d.name) || d).filter(Boolean)
220
+ : [];
221
+ return {
222
+ source: 'offline',
223
+ path: sourcePath,
224
+ appKey: app.key,
225
+ application,
226
+ roles: variables.roles || [],
227
+ permissions: variables.permissions || [],
228
+ authentication: variables.authentication || null,
229
+ portalInputConfigurations: getPortalInputConfigurations(variables),
230
+ databases,
231
+ isExternal: app.type === 'external'
232
+ };
233
+ }
234
+
235
+ /**
236
+ * Get auth token for show --online (same pattern as list).
237
+ * @param {string} controllerUrl - Controller URL
238
+ * @param {Object} config - Config from getConfig()
239
+ * @returns {Promise<{token: string, actualControllerUrl: string}|null>}
240
+ */
241
+ async function getShowAuthToken(controllerUrl, config) {
242
+ const tryController = async(url) => {
243
+ const normalized = normalizeControllerUrl(url);
244
+ const deviceToken = await getOrRefreshDeviceToken(normalized);
245
+ if (deviceToken && deviceToken.token) {
246
+ return { token: deviceToken.token, actualControllerUrl: normalized };
247
+ }
248
+ return null;
249
+ };
250
+
251
+ if (controllerUrl) {
252
+ const result = await tryController(controllerUrl);
253
+ if (result) return result;
254
+ const formatted = formatAuthenticationError({
255
+ controllerUrl,
256
+ message: 'No valid authentication found'
257
+ });
258
+ logger.error(formatted);
259
+ throw new Error('Authentication required for --online. Run aifabrix login.');
260
+ }
261
+
262
+ if (config.device && typeof config.device === 'object') {
263
+ for (const storedUrl of Object.keys(config.device)) {
264
+ const result = await tryController(storedUrl);
265
+ if (result) return result;
266
+ }
267
+ }
268
+
269
+ const formatted = formatAuthenticationError({
270
+ controllerUrl: controllerUrl || undefined,
271
+ message: 'No valid authentication found'
272
+ });
273
+ logger.error(formatted);
274
+ throw new Error('Authentication required for --online. Run aifabrix login.');
275
+ }
276
+
277
+ async function fetchOpenApiLists(dataplaneUrl, appKey, authConfig) {
278
+ let openapiFiles = [];
279
+ let openapiEndpoints = [];
280
+ try {
281
+ const filesRes = await listOpenAPIFiles(dataplaneUrl, appKey, authConfig);
282
+ const filesData = filesRes.data || filesRes;
283
+ openapiFiles = Array.isArray(filesData) ? filesData : (filesData.items || filesData.data || []);
284
+ } catch {
285
+ // optional
286
+ }
287
+ try {
288
+ const endpointsRes = await listOpenAPIEndpoints(dataplaneUrl, appKey, authConfig);
289
+ const endData = endpointsRes.data || endpointsRes;
290
+ openapiEndpoints = Array.isArray(endData) ? endData : (endData.items || endData.data || []);
291
+ } catch {
292
+ // optional
293
+ }
294
+ return { openapiFiles, openapiEndpoints };
295
+ }
296
+
297
+ function buildExternalSystemResult(configData, appKey, openapiFiles, openapiEndpoints) {
298
+ const system = configData.system || configData;
299
+ const dataSources = configData.dataSources || configData.dataSources || [];
300
+ const application = configData.application || configData.app || {};
301
+ return {
302
+ dataplaneUrl: null,
303
+ systemKey: appKey,
304
+ displayName: system.displayName || application.displayName || appKey,
305
+ type: system.type || application.type || '—',
306
+ status: system.status || '—',
307
+ version: system.version || '—',
308
+ dataSources: dataSources.map((ds) => ({
309
+ key: ds.key,
310
+ displayName: ds.displayName,
311
+ systemKey: ds.systemKey || appKey
312
+ })),
313
+ application: {
314
+ key: application.key || appKey,
315
+ displayName: application.displayName,
316
+ type: application.type,
317
+ roles: application.roles,
318
+ permissions: application.permissions
319
+ },
320
+ openapiFiles,
321
+ openapiEndpoints
322
+ };
323
+ }
324
+
325
+ /**
326
+ * Fetch external system section from dataplane (for --online and type external).
327
+ * @param {string} dataplaneUrl - Dataplane URL
328
+ * @param {string} appKey - Application key (system key)
329
+ * @param {Object} authConfig - Auth config
330
+ * @returns {Promise<Object|null>} externalSystem object or null if unreachable
331
+ */
332
+ async function fetchExternalSystemFromDataplane(dataplaneUrl, appKey, authConfig) {
333
+ try {
334
+ const configRes = await getExternalSystemConfig(dataplaneUrl, appKey, authConfig);
335
+ const data = configRes.data || configRes;
336
+ const configData = data.data || data;
337
+ const { openapiFiles, openapiEndpoints } = await fetchOpenApiLists(dataplaneUrl, appKey, authConfig);
338
+ const result = buildExternalSystemResult(configData, appKey, openapiFiles, openapiEndpoints);
339
+ result.dataplaneUrl = dataplaneUrl;
340
+ return result;
341
+ } catch (error) {
342
+ return { error: error.message || 'dataplane unreachable or not found' };
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Format healthCheck for display (same as offline manifest).
348
+ * @param {Object} health - healthCheck object
349
+ * @returns {string}
350
+ */
351
+ function formatHealthCheckForDisplay(health) {
352
+ if (!health) return '—';
353
+ const path = health.path ?? health.probePath ?? '—';
354
+ const interval = health.interval ?? health.intervalSeconds ?? health.probeIntervalInSeconds ?? 30;
355
+ return path !== '—' ? `${path} (interval ${interval}s)` : '—';
356
+ }
357
+
358
+ /**
359
+ * Format build for display (same as offline manifest).
360
+ * @param {Object} build - build object
361
+ * @returns {string}
362
+ */
363
+ function formatBuildForDisplay(build) {
364
+ if (!build) return '—';
365
+ const parts = [];
366
+ if (build.language) parts.push(build.language);
367
+ if (build.localPort !== undefined && build.localPort !== null) parts.push(`localPort ${build.localPort}`);
368
+ if (build.dockerfile) parts.push('dockerfile');
369
+ if (build.envOutputPath) parts.push(`envOutputPath: ${build.envOutputPath}`);
370
+ return parts.length ? parts.join(', ') : '—';
371
+ }
372
+
373
+ function getAppAndCfgFromApiApp(apiApp) {
374
+ const app = apiApp.data || apiApp;
375
+ const cfg = app.configuration && typeof app.configuration === 'object' ? app.configuration : {};
376
+ return { app, cfg };
377
+ }
378
+
379
+ function getPortalInputFromAppCfg(app, cfg) {
380
+ const configArray = cfg.configuration || app.configuration;
381
+ const configList = Array.isArray(configArray) ? configArray : [];
382
+ const condConfig = cfg.conditionalConfiguration || app.conditionalConfiguration || [];
383
+ return getPortalInputConfigurations(
384
+ { configuration: configList, conditionalConfiguration: Array.isArray(condConfig) ? condConfig : [] }
385
+ );
386
+ }
387
+
388
+ function resolvePortFromAppCfg(app, cfg) {
389
+ if (cfg.port !== undefined && cfg.port !== null) return cfg.port;
390
+ if (app.port !== undefined && app.port !== null) return app.port;
391
+ return '—';
392
+ }
393
+
394
+ function resolveDatabasesFromAppCfg(app, cfg) {
395
+ if (Array.isArray(cfg.databases)) return cfg.databases;
396
+ if (Array.isArray(app.databases)) return app.databases;
397
+ return [];
398
+ }
399
+
400
+ function pickAppCfg(key, app, cfg, fallback) {
401
+ const v = cfg[key] ?? app[key];
402
+ return v !== undefined && v !== null ? v : fallback;
403
+ }
404
+
405
+ function buildApplicationFromAppCfg(app, cfg, portalInputConfigurations) {
406
+ const deploymentKey = cfg.deploymentKey ?? app.deploymentKey;
407
+ const truncatedDeploy = truncateDeploymentKey(deploymentKey) || (deploymentKey ?? '—');
408
+ const application = {
409
+ key: pickAppCfg('key', app, cfg, '—'),
410
+ displayName: pickAppCfg('displayName', app, cfg, '—'),
411
+ description: pickAppCfg('description', app, cfg, '—'),
412
+ type: pickAppCfg('type', app, cfg, '—'),
413
+ deploymentKey: truncatedDeploy,
414
+ image: pickAppCfg('image', app, cfg, '—'),
415
+ registryMode: pickAppCfg('registryMode', app, cfg, '—'),
416
+ port: resolvePortFromAppCfg(app, cfg),
417
+ healthCheck: formatHealthCheckForDisplay(cfg.healthCheck ?? app.healthCheck),
418
+ build: formatBuildForDisplay(cfg.build ?? app.build),
419
+ status: pickAppCfg('status', app, cfg, '—'),
420
+ url: pickAppCfg('url', app, cfg, '—'),
421
+ roles: cfg.roles ?? app.roles,
422
+ permissions: cfg.permissions ?? app.permissions,
423
+ authentication: cfg.authentication ?? app.authentication,
424
+ portalInputConfigurations,
425
+ databases: resolveDatabasesFromAppCfg(app, cfg)
426
+ };
427
+ return application;
428
+ }
429
+
430
+ function addExternalIntegrationToApplication(application, app, cfg) {
431
+ const isExternal = (app.type ?? cfg.type) === 'external';
432
+ const extInt = cfg.externalIntegration ?? app.externalIntegration;
433
+ if (!isExternal || !extInt) return;
434
+ application.externalIntegration = {
435
+ schemaBasePath: extInt.schemaBasePath,
436
+ systems: extInt.systems || [],
437
+ dataSources: extInt.dataSources || []
438
+ };
439
+ }
440
+
441
+ function buildApplicationFromApiApp(apiApp) {
442
+ const { app, cfg } = getAppAndCfgFromApiApp(apiApp);
443
+ const portalInputConfigurations = getPortalInputFromAppCfg(app, cfg);
444
+ const application = buildApplicationFromAppCfg(app, cfg, portalInputConfigurations);
445
+ addExternalIntegrationToApplication(application, app, cfg);
446
+ return application;
447
+ }
448
+
449
+ function normalizeExternalSystemForSummary(externalSystem) {
450
+ if (!externalSystem) return null;
451
+ if (!externalSystem.error) return externalSystem;
452
+ return { error: externalSystem.error };
453
+ }
454
+
455
+ /**
456
+ * Build online summary from getApplication response (same application schema as offline).
457
+ * Controller returns Application with nested configuration (ApplicationConfig = full manifest);
458
+ * validate and use app.configuration so we get deploymentKey, healthCheck, build, roles, etc.
459
+ * @param {Object} apiApp - Application from getApplication response (e.g. response.data)
460
+ * @param {string} controllerUrl - Controller URL
461
+ * @param {Object|null} externalSystem - From dataplane when type external
462
+ * @returns {Object} Summary for display/JSON (same shape as offline)
463
+ */
464
+ function buildOnlineSummary(apiApp, controllerUrl, externalSystem) {
465
+ const application = buildApplicationFromApiApp(apiApp);
466
+ const roles = application.roles || [];
467
+ const permissions = application.permissions || [];
468
+ const isExternal = application.type === 'external';
469
+ return {
470
+ source: 'online',
471
+ controllerUrl,
472
+ appKey: application.key,
473
+ application,
474
+ roles,
475
+ permissions,
476
+ authentication: application.authentication || null,
477
+ portalInputConfigurations: application.portalInputConfigurations,
478
+ databases: application.databases || [],
479
+ isExternal,
480
+ externalSystem: normalizeExternalSystemForSummary(externalSystem)
481
+ };
482
+ }
483
+
484
+ /**
485
+ * Run show in offline mode: generate manifest (same as aifabrix json) and use it; else fall back to variables.yaml.
486
+ * @param {string} appKey - Application key
487
+ * @param {boolean} json - Output as JSON
488
+ * @throws {Error} If variables.yaml not found or invalid YAML
489
+ */
490
+ async function runOffline(appKey, json) {
491
+ let summary;
492
+
493
+ try {
494
+ const { deployment, appPath } = await generator.buildDeploymentManifestInMemory(appKey);
495
+ const sourcePath = path.relative(process.cwd(), appPath) || appPath;
496
+ summary = buildOfflineSummaryFromDeployJson(deployment, sourcePath);
497
+ } catch (_err) {
498
+ const { appPath } = await detectAppType(appKey);
499
+ const variablesPath = path.join(appPath, 'variables.yaml');
500
+ const variables = loadVariablesFromPath(appPath);
501
+ const sourcePath = path.relative(process.cwd(), variablesPath) || variablesPath;
502
+ summary = buildOfflineSummary(variables, sourcePath);
503
+ }
504
+
505
+ if (json) {
506
+ const out = {
507
+ source: summary.source,
508
+ path: summary.path,
509
+ appKey: summary.appKey,
510
+ application: {
511
+ ...summary.application,
512
+ roles: summary.roles,
513
+ permissions: summary.permissions,
514
+ authentication: summary.authentication,
515
+ portalInputConfigurations: summary.portalInputConfigurations,
516
+ databases: summary.databases
517
+ }
518
+ };
519
+ logger.log(JSON.stringify(out, null, 2));
520
+ return;
521
+ }
522
+ displayShow(summary);
523
+ }
524
+
525
+ async function resolveOnlineAuth(controllerUrl) {
526
+ const config = await getConfig();
527
+ const authResult = await getShowAuthToken(controllerUrl, config);
528
+ return { authConfig: { type: 'bearer', token: authResult.token }, authResult };
529
+ }
530
+
531
+ function ensureApplicationResponse(response, appKey, authResult) {
532
+ if (!response.success) {
533
+ if (response.status === 404) {
534
+ throw new Error(`Application "${appKey}" not found on controller.`);
535
+ }
536
+ const formatted = response.formattedError || formatApiError(response, authResult.actualControllerUrl);
537
+ logger.error(formatted);
538
+ throw new Error('Failed to get application from controller.');
539
+ }
540
+ return response.data || response;
541
+ }
542
+
543
+ async function fetchExternalSystemForOnline(controllerUrl, appKey, authConfig) {
544
+ try {
545
+ const environment = await resolveEnvironment();
546
+ const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
547
+ return await fetchExternalSystemFromDataplane(dataplaneUrl, appKey, authConfig);
548
+ } catch (err) {
549
+ return { error: err.message || 'dataplane unreachable or not found' };
550
+ }
551
+ }
552
+
553
+ function outputOnlineJson(summary) {
554
+ const out = {
555
+ source: summary.source,
556
+ controllerUrl: summary.controllerUrl,
557
+ appKey: summary.appKey,
558
+ application: {
559
+ key: summary.application.key,
560
+ displayName: summary.application.displayName,
561
+ description: summary.application.description,
562
+ type: summary.application.type,
563
+ status: summary.application.status,
564
+ url: summary.application.url,
565
+ port: summary.application.port,
566
+ configuration: summary.application.configuration,
567
+ roles: summary.application.roles,
568
+ permissions: summary.application.permissions,
569
+ authentication: summary.application.authentication,
570
+ portalInputConfigurations: summary.application.portalInputConfigurations,
571
+ databases: summary.application.databases
572
+ }
573
+ };
574
+ if (summary.externalSystem !== undefined && summary.externalSystem !== null) {
575
+ out.externalSystem = summary.externalSystem && summary.externalSystem.error
576
+ ? { error: summary.externalSystem.error }
577
+ : summary.externalSystem;
578
+ }
579
+ logger.log(JSON.stringify(out, null, 2));
580
+ }
581
+
582
+ /**
583
+ * Run show in online mode: getApplication, optionally dataplane for external, display or JSON.
584
+ * @param {string} appKey - Application key
585
+ * @param {boolean} json - Output as JSON
586
+ * @throws {Error} On auth failure, 404, or API error
587
+ */
588
+ async function runOnline(appKey, json) {
589
+ const controllerUrl = await resolveControllerUrl();
590
+ if (!controllerUrl) {
591
+ throw new Error('Controller URL is required for --online. Run aifabrix login to set the controller URL in config.yaml.');
592
+ }
593
+ const { authConfig, authResult } = await resolveOnlineAuth(controllerUrl);
594
+ const response = await getApplication(controllerUrl, appKey, authConfig);
595
+ const apiApp = ensureApplicationResponse(response, appKey, authResult);
596
+ const appData = apiApp.data || apiApp;
597
+ const externalSystem = appData.type === 'external'
598
+ ? await fetchExternalSystemForOnline(controllerUrl, appKey, authConfig)
599
+ : null;
600
+ const summary = buildOnlineSummary(apiApp, authResult.actualControllerUrl, externalSystem);
601
+ if (json) {
602
+ outputOnlineJson(summary);
603
+ return;
604
+ }
605
+ displayShow(summary);
606
+ }
607
+
608
+ /**
609
+ * Show application info (offline by default, or from controller with --online).
610
+ * @async
611
+ * @param {string} appKey - Application key
612
+ * @param {Object} options - Options
613
+ * @param {boolean} [options.online] - Fetch from controller
614
+ * @param {boolean} [options.json] - Output as JSON
615
+ * @throws {Error} If file missing/invalid (offline) or API/auth error (online)
616
+ */
617
+ async function showApp(appKey, options = {}) {
618
+ if (!appKey || typeof appKey !== 'string') {
619
+ throw new Error('appKey is required');
620
+ }
621
+
622
+ const online = Boolean(options.online);
623
+ const json = Boolean(options.json);
624
+
625
+ if (online) {
626
+ await runOnline(appKey, json);
627
+ } else {
628
+ await runOffline(appKey, json);
629
+ }
630
+ }
631
+
632
+ module.exports = {
633
+ showApp,
634
+ loadVariablesFromPath,
635
+ getPortalInputConfigurations,
636
+ buildOfflineSummary,
637
+ buildOfflineSummaryFromDeployJson,
638
+ buildOnlineSummary,
639
+ formatHealthCheckForDisplay,
640
+ formatBuildForDisplay,
641
+ getShowAuthToken
642
+ };
package/lib/cli.js CHANGED
@@ -409,22 +409,29 @@ function setupAppCommands(program) {
409
409
  }
410
410
  });
411
411
 
412
- program.command('wizard')
412
+ program.command('wizard [appName]')
413
413
  .description('Create or extend external systems (OpenAPI, MCP, or known platforms like HubSpot) via guided steps or a config file')
414
- .option('-a, --app <app>', 'Application name; skips the prompt in interactive mode')
414
+ .option('-a, --app <app>', 'Application name (synonym for positional appName)')
415
415
  .option('--config <file>', 'Run headless using a wizard.yaml file (appName, mode, source, credential, preferences)')
416
+ .option('--silent', 'Run with saved integration/<app>/wizard.yaml only; no prompts (requires app name and existing wizard.yaml)')
416
417
  .addHelpText('after', `
417
418
  Examples:
418
- $ aifabrix wizard Run interactively (prompts for app name and steps)
419
- $ aifabrix wizard -a my-integration Run interactively with app name set
419
+ $ aifabrix wizard Run interactively (mode first, then prompts)
420
+ $ aifabrix wizard my-integration Load wizard.yaml if present → show summary → "Run with saved config?" or start from step 1
421
+ $ aifabrix wizard my-integration --silent Run headless with integration/my-integration/wizard.yaml (no prompts)
422
+ $ aifabrix wizard -a my-integration Same as above (app name set)
420
423
  $ aifabrix wizard --config wizard.yaml Run headless from a wizard config file
421
424
 
422
- Headless config (wizard.yaml) must include: appName, mode (create-system|add-datasource), source (type + filePath/url/platform).
425
+ Config path: When appName is provided, integration/<appName>/wizard.yaml is used for load/save and error.log.
426
+ To change settings after a run, edit that file and run "aifabrix wizard <app>" again.
427
+ Headless config must include: appName, mode (create-system|add-datasource), source (type + filePath/url/platform).
423
428
  See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`)
424
- .action(async(options) => {
429
+ .action(async(positionalAppName, options) => {
425
430
  try {
431
+ const appName = positionalAppName || options.app;
432
+ const configPath = appName ? path.join(process.cwd(), 'integration', appName, 'wizard.yaml') : null;
426
433
  const { handleWizard } = require('./commands/wizard');
427
- await handleWizard(options);
434
+ await handleWizard({ ...options, app: appName, config: options.config, configPath });
428
435
  } catch (error) {
429
436
  handleCommandError(error, 'wizard');
430
437
  process.exit(1);
@@ -693,6 +700,20 @@ function setupUtilityCommands(program) {
693
700
  }
694
701
  });
695
702
 
703
+ program.command('show <appKey>')
704
+ .description('Show application info from local builder/ or integration/ (offline) or from controller (--online)')
705
+ .option('--online', 'Fetch application data from the controller')
706
+ .option('--json', 'Output as JSON')
707
+ .action(async(appKey, options) => {
708
+ try {
709
+ const { showApp } = require('./app/show');
710
+ await showApp(appKey, { online: options.online, json: options.json });
711
+ } catch (error) {
712
+ logger.error(chalk.red(`Error: ${error.message}`));
713
+ process.exit(1);
714
+ }
715
+ });
716
+
696
717
  program.command('validate <appOrFile>')
697
718
  .description('Validate application or external integration file')
698
719
  .option('--type <type>', 'Application type (external) - if set, only checks integration folder')