@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.
- package/README.md +5 -0
- package/integration/test-hubspot/wizard.yaml +8 -0
- package/lib/api/wizard.api.js +24 -1
- package/lib/app/show-display.js +184 -0
- package/lib/app/show.js +642 -0
- package/lib/cli.js +28 -7
- package/lib/commands/wizard-core-helpers.js +278 -0
- package/lib/commands/wizard-core.js +26 -145
- package/lib/commands/wizard-headless.js +2 -2
- package/lib/commands/wizard-helpers.js +143 -0
- package/lib/commands/wizard.js +275 -68
- package/lib/generator/index.js +32 -0
- package/lib/generator/wizard-prompts.js +111 -44
- package/lib/utils/cli-utils.js +40 -1
- 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
|
+
};
|
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
|
|
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 (
|
|
419
|
-
$ aifabrix wizard
|
|
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
|
-
|
|
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')
|