@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.
- package/README.md +5 -0
- package/integration/test-hubspot/wizard.yaml +8 -0
- package/lib/api/wizard.api.js +24 -1
- package/lib/app/run-helpers.js +7 -2
- package/lib/app/run.js +2 -2
- package/lib/app/show-display.js +184 -0
- package/lib/app/show.js +642 -0
- package/lib/cli.js +38 -9
- package/lib/commands/auth-status.js +58 -2
- package/lib/commands/up-miso.js +25 -16
- package/lib/commands/wizard-core-helpers.js +278 -0
- package/lib/commands/wizard-core.js +74 -161
- package/lib/commands/wizard-headless.js +2 -2
- package/lib/commands/wizard-helpers.js +143 -0
- package/lib/commands/wizard.js +282 -69
- package/lib/datasource/list.js +6 -3
- package/lib/generator/index.js +32 -0
- package/lib/generator/wizard-prompts.js +111 -44
- package/lib/infrastructure/services.js +6 -3
- package/lib/utils/app-register-auth.js +9 -2
- package/lib/utils/cli-utils.js +40 -1
- package/lib/utils/error-formatters/http-status-errors.js +8 -0
- package/lib/utils/error-formatters/permission-errors.js +44 -1
- package/lib/utils/infra-containers.js +19 -16
- package/lib/utils/infra-status.js +12 -3
- package/lib/validation/wizard-config-validator.js +35 -0
- package/package.json +2 -2
package/lib/app/show.js
ADDED
|
@@ -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
|
+
};
|