@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
|
@@ -12,35 +12,41 @@ const fs = require('fs').promises;
|
|
|
12
12
|
* Prompt for wizard mode selection
|
|
13
13
|
* @async
|
|
14
14
|
* @function promptForMode
|
|
15
|
+
* @param {string} [defaultMode] - Default value ('create-system' | 'add-datasource')
|
|
15
16
|
* @returns {Promise<string>} Selected mode ('create-system' | 'add-datasource')
|
|
16
17
|
*/
|
|
17
|
-
async function promptForMode() {
|
|
18
|
+
async function promptForMode(defaultMode) {
|
|
19
|
+
const choices = [
|
|
20
|
+
{ name: 'Create a new external system', value: 'create-system' },
|
|
21
|
+
{ name: 'Add datasource to existing system', value: 'add-datasource' }
|
|
22
|
+
];
|
|
18
23
|
const { mode } = await inquirer.prompt([
|
|
19
24
|
{
|
|
20
25
|
type: 'list',
|
|
21
26
|
name: 'mode',
|
|
22
27
|
message: 'What would you like to do?',
|
|
23
|
-
choices
|
|
24
|
-
|
|
25
|
-
{ name: 'Add datasource to existing system', value: 'add-datasource' }
|
|
26
|
-
]
|
|
28
|
+
choices,
|
|
29
|
+
default: defaultMode && choices.some(c => c.value === defaultMode) ? defaultMode : undefined
|
|
27
30
|
}
|
|
28
31
|
]);
|
|
29
32
|
return mode;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
/**
|
|
33
|
-
* Prompt for existing system ID or key (for add-datasource mode)
|
|
36
|
+
* Prompt for existing system ID or key (for add-datasource mode).
|
|
37
|
+
* Only external systems (OpenAPI, MCP, custom) support add-datasource; webapps do not.
|
|
34
38
|
* @async
|
|
35
39
|
* @function promptForSystemIdOrKey
|
|
40
|
+
* @param {string} [defaultValue] - Default value (e.g. from loaded wizard.yaml)
|
|
36
41
|
* @returns {Promise<string>} System ID or key
|
|
37
42
|
*/
|
|
38
|
-
async function promptForSystemIdOrKey() {
|
|
43
|
+
async function promptForSystemIdOrKey(defaultValue) {
|
|
39
44
|
const { systemIdOrKey } = await inquirer.prompt([
|
|
40
45
|
{
|
|
41
46
|
type: 'input',
|
|
42
47
|
name: 'systemIdOrKey',
|
|
43
|
-
message: 'Enter the existing system ID or key:',
|
|
48
|
+
message: 'Enter the existing external system ID or key (not a webapp):',
|
|
49
|
+
default: defaultValue,
|
|
44
50
|
validate: (input) => {
|
|
45
51
|
if (!input || typeof input !== 'string' || input.trim().length === 0) {
|
|
46
52
|
return 'System ID or key is required';
|
|
@@ -55,20 +61,24 @@ async function promptForSystemIdOrKey() {
|
|
|
55
61
|
* Prompt for source type selection
|
|
56
62
|
* @async
|
|
57
63
|
* @function promptForSourceType
|
|
64
|
+
* @param {Array<{key: string, displayName?: string}>} [platforms] - If provided and non-empty, include "Known platform"; otherwise omit it
|
|
58
65
|
* @returns {Promise<string>} Selected source type
|
|
59
66
|
*/
|
|
60
|
-
async function promptForSourceType() {
|
|
67
|
+
async function promptForSourceType(platforms = []) {
|
|
68
|
+
const choices = [
|
|
69
|
+
{ name: 'OpenAPI file (local file)', value: 'openapi-file' },
|
|
70
|
+
{ name: 'OpenAPI URL (remote URL)', value: 'openapi-url' },
|
|
71
|
+
{ name: 'MCP server', value: 'mcp-server' }
|
|
72
|
+
];
|
|
73
|
+
if (Array.isArray(platforms) && platforms.length > 0) {
|
|
74
|
+
choices.push({ name: 'Known platform (pre-configured)', value: 'known-platform' });
|
|
75
|
+
}
|
|
61
76
|
const { sourceType } = await inquirer.prompt([
|
|
62
77
|
{
|
|
63
78
|
type: 'list',
|
|
64
79
|
name: 'sourceType',
|
|
65
80
|
message: 'What is your source type?',
|
|
66
|
-
choices
|
|
67
|
-
{ name: 'OpenAPI file (local file)', value: 'openapi-file' },
|
|
68
|
-
{ name: 'OpenAPI URL (remote URL)', value: 'openapi-url' },
|
|
69
|
-
{ name: 'MCP server', value: 'mcp-server' },
|
|
70
|
-
{ name: 'Known platform (pre-configured)', value: 'known-platform' }
|
|
71
|
-
]
|
|
81
|
+
choices
|
|
72
82
|
}
|
|
73
83
|
]);
|
|
74
84
|
return sourceType;
|
|
@@ -176,11 +186,74 @@ async function promptForMcpServer() {
|
|
|
176
186
|
};
|
|
177
187
|
}
|
|
178
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Prompt for credential action (skip / create new / use existing).
|
|
191
|
+
* Choose Skip if you don't have credentials yet; you can add them later in env.template.
|
|
192
|
+
* @async
|
|
193
|
+
* @function promptForCredentialAction
|
|
194
|
+
* @returns {Promise<Object>} Object with action ('skip'|'create'|'select') and optional credentialIdOrKey
|
|
195
|
+
*/
|
|
196
|
+
async function promptForCredentialAction() {
|
|
197
|
+
const { action } = await inquirer.prompt([
|
|
198
|
+
{
|
|
199
|
+
type: 'list',
|
|
200
|
+
name: 'action',
|
|
201
|
+
message: 'Credential (optional; choose Skip if you don\'t have credentials yet):',
|
|
202
|
+
choices: [
|
|
203
|
+
{ name: 'Skip - configure credentials later', value: 'skip' },
|
|
204
|
+
{ name: 'Create new', value: 'create' },
|
|
205
|
+
{ name: 'Use existing', value: 'select' }
|
|
206
|
+
]
|
|
207
|
+
}
|
|
208
|
+
]);
|
|
209
|
+
if (action === 'select') {
|
|
210
|
+
const { credentialIdOrKey } = await inquirer.prompt([
|
|
211
|
+
{
|
|
212
|
+
type: 'input',
|
|
213
|
+
name: 'credentialIdOrKey',
|
|
214
|
+
message: 'Enter credential ID or key (must exist on the dataplane):',
|
|
215
|
+
validate: (input) => {
|
|
216
|
+
if (!input || typeof input !== 'string' || input.trim().length === 0) {
|
|
217
|
+
return 'Credential ID or key is required (or choose Skip at the previous step)';
|
|
218
|
+
}
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
]);
|
|
223
|
+
return { action, credentialIdOrKey: credentialIdOrKey.trim() };
|
|
224
|
+
}
|
|
225
|
+
return { action };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Re-prompt for credential ID/key when validation failed (e.g. not found on dataplane).
|
|
230
|
+
* Empty input means skip.
|
|
231
|
+
* @async
|
|
232
|
+
* @function promptForCredentialIdOrKeyRetry
|
|
233
|
+
* @param {string} [previousError] - Error message from dataplane (e.g. "Credential not found")
|
|
234
|
+
* @returns {Promise<Object>} { credentialIdOrKey: string } or { skip: true } if user leaves empty
|
|
235
|
+
*/
|
|
236
|
+
async function promptForCredentialIdOrKeyRetry(previousError) {
|
|
237
|
+
const msg = previousError
|
|
238
|
+
? `Credential not found or invalid (${String(previousError).slice(0, 60)}). Enter ID/key or leave empty to skip:`
|
|
239
|
+
: 'Enter credential ID or key (or leave empty to skip):';
|
|
240
|
+
const { credentialIdOrKey } = await inquirer.prompt([
|
|
241
|
+
{
|
|
242
|
+
type: 'input',
|
|
243
|
+
name: 'credentialIdOrKey',
|
|
244
|
+
message: msg,
|
|
245
|
+
default: ''
|
|
246
|
+
}
|
|
247
|
+
]);
|
|
248
|
+
const trimmed = (credentialIdOrKey && credentialIdOrKey.trim()) || '';
|
|
249
|
+
return trimmed ? { credentialIdOrKey: trimmed } : { skip: true };
|
|
250
|
+
}
|
|
251
|
+
|
|
179
252
|
/**
|
|
180
253
|
* Prompt for known platform selection
|
|
181
254
|
* @async
|
|
182
255
|
* @function promptForKnownPlatform
|
|
183
|
-
* @param {string
|
|
256
|
+
* @param {Array<{key: string, displayName?: string}>} [platforms] - List of available platforms (if provided)
|
|
184
257
|
* @returns {Promise<string>} Selected platform key
|
|
185
258
|
*/
|
|
186
259
|
async function promptForKnownPlatform(platforms = []) {
|
|
@@ -297,7 +370,6 @@ async function promptForConfigReview(systemConfig, datasourceConfigs) {
|
|
|
297
370
|
message: 'What would you like to do?',
|
|
298
371
|
choices: [
|
|
299
372
|
{ name: 'Accept and save', value: 'accept' },
|
|
300
|
-
{ name: 'Edit configuration manually', value: 'edit' },
|
|
301
373
|
{ name: 'Cancel', value: 'cancel' }
|
|
302
374
|
]
|
|
303
375
|
}
|
|
@@ -307,32 +379,6 @@ async function promptForConfigReview(systemConfig, datasourceConfigs) {
|
|
|
307
379
|
return { action: 'cancel' };
|
|
308
380
|
}
|
|
309
381
|
|
|
310
|
-
if (action === 'edit') {
|
|
311
|
-
const { editedConfig } = await inquirer.prompt([
|
|
312
|
-
{
|
|
313
|
-
type: 'editor',
|
|
314
|
-
name: 'editedConfig',
|
|
315
|
-
message: 'Edit the configuration (JSON format):',
|
|
316
|
-
default: JSON.stringify({ systemConfig, datasourceConfigs }, null, 2),
|
|
317
|
-
validate: (input) => {
|
|
318
|
-
try {
|
|
319
|
-
JSON.parse(input);
|
|
320
|
-
return true;
|
|
321
|
-
} catch (error) {
|
|
322
|
-
return `Invalid JSON: ${error.message}`;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
]);
|
|
327
|
-
|
|
328
|
-
const parsed = JSON.parse(editedConfig);
|
|
329
|
-
return {
|
|
330
|
-
action: 'edit',
|
|
331
|
-
systemConfig: parsed.systemConfig,
|
|
332
|
-
datasourceConfigs: parsed.datasourceConfigs
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
|
|
336
382
|
return { action: 'accept' };
|
|
337
383
|
}
|
|
338
384
|
|
|
@@ -364,6 +410,24 @@ async function promptForAppName(defaultName) {
|
|
|
364
410
|
return appName.trim();
|
|
365
411
|
}
|
|
366
412
|
|
|
413
|
+
/**
|
|
414
|
+
* Prompt: Run with saved config? (Y/n). Used when resuming from existing wizard.yaml.
|
|
415
|
+
* @async
|
|
416
|
+
* @function promptForRunWithSavedConfig
|
|
417
|
+
* @returns {Promise<boolean>} True to run with saved config, false to exit
|
|
418
|
+
*/
|
|
419
|
+
async function promptForRunWithSavedConfig() {
|
|
420
|
+
const { run } = await inquirer.prompt([
|
|
421
|
+
{
|
|
422
|
+
type: 'confirm',
|
|
423
|
+
name: 'run',
|
|
424
|
+
message: 'Run with saved config?',
|
|
425
|
+
default: true
|
|
426
|
+
}
|
|
427
|
+
]);
|
|
428
|
+
return run;
|
|
429
|
+
}
|
|
430
|
+
|
|
367
431
|
module.exports = {
|
|
368
432
|
promptForMode,
|
|
369
433
|
promptForSystemIdOrKey,
|
|
@@ -371,10 +435,13 @@ module.exports = {
|
|
|
371
435
|
promptForOpenApiFile,
|
|
372
436
|
promptForOpenApiUrl,
|
|
373
437
|
promptForMcpServer,
|
|
438
|
+
promptForCredentialAction,
|
|
439
|
+
promptForCredentialIdOrKeyRetry,
|
|
374
440
|
promptForKnownPlatform,
|
|
375
441
|
promptForUserIntent,
|
|
376
442
|
promptForUserPreferences,
|
|
377
443
|
promptForConfigReview,
|
|
378
|
-
promptForAppName
|
|
444
|
+
promptForAppName,
|
|
445
|
+
promptForRunWithSavedConfig
|
|
379
446
|
};
|
|
380
447
|
|
package/lib/utils/cli-utils.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* @version 2.0.0
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs').promises;
|
|
11
13
|
const logger = require('./logger');
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -289,10 +291,47 @@ function logError(command, errorMessages) {
|
|
|
289
291
|
function handleCommandError(error, command) {
|
|
290
292
|
const errorMessages = formatError(error);
|
|
291
293
|
logError(command, errorMessages);
|
|
294
|
+
if (error.wizardResumeMessage) {
|
|
295
|
+
logger.log(error.wizardResumeMessage);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Strip ANSI escape codes for plain-text logging (ESC [...] m) */
|
|
300
|
+
// eslint-disable-next-line no-control-regex -- intentional: match ANSI CSI sequences
|
|
301
|
+
const ANSI_CODE_RE = /\x1b\[[\d;]*m/g;
|
|
302
|
+
function stripAnsi(str) {
|
|
303
|
+
return typeof str === 'string' ? str.replace(ANSI_CODE_RE, '') : str;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Appends a wizard error to integration/<appKey>/error.log (timestamp + message only; no stack or secrets).
|
|
308
|
+
* Uses full formatted message (with validation details) when error.formatted is set, stripped of ANSI.
|
|
309
|
+
* Does not throw; logs and ignores write failures.
|
|
310
|
+
* @param {string} appKey - Application/integration key (e.g. app name or system key)
|
|
311
|
+
* @param {Error} error - The error that occurred
|
|
312
|
+
* @returns {Promise<void>}
|
|
313
|
+
*/
|
|
314
|
+
async function appendWizardError(appKey, error) {
|
|
315
|
+
if (!appKey || typeof appKey !== 'string' || !/^[a-z0-9-_]+$/.test(appKey)) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const dir = path.join(process.cwd(), 'integration', appKey);
|
|
319
|
+
const logPath = path.join(dir, 'error.log');
|
|
320
|
+
const rawMessage = (error && error.message) ? String(error.message) : String(error);
|
|
321
|
+
const fullPlain = (error && error.formatted) ? stripAnsi(error.formatted) : null;
|
|
322
|
+
const message = fullPlain && fullPlain.length > rawMessage.length ? fullPlain : rawMessage;
|
|
323
|
+
const line = `${new Date().toISOString()} ${message}\n`;
|
|
324
|
+
try {
|
|
325
|
+
await fs.mkdir(dir, { recursive: true });
|
|
326
|
+
await fs.appendFile(logPath, line, 'utf8');
|
|
327
|
+
} catch (e) {
|
|
328
|
+
logger.warn(`Could not write wizard error.log: ${e.message}`);
|
|
329
|
+
}
|
|
292
330
|
}
|
|
293
331
|
|
|
294
332
|
module.exports = {
|
|
295
333
|
validateCommand,
|
|
296
|
-
handleCommandError
|
|
334
|
+
handleCommandError,
|
|
335
|
+
appendWizardError
|
|
297
336
|
};
|
|
298
337
|
|
|
@@ -114,6 +114,39 @@ async function loadWizardConfig(configPath) {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Write wizard configuration to wizard.yaml (creates directory if needed).
|
|
119
|
+
* @async
|
|
120
|
+
* @function writeWizardConfig
|
|
121
|
+
* @param {string} configPath - Path to wizard.yaml file
|
|
122
|
+
* @param {Object} config - Configuration object to write (will be dumped as YAML)
|
|
123
|
+
* @returns {Promise<void>}
|
|
124
|
+
* @throws {Error} If write fails
|
|
125
|
+
*/
|
|
126
|
+
async function writeWizardConfig(configPath, config) {
|
|
127
|
+
const resolvedPath = path.resolve(configPath);
|
|
128
|
+
const dir = path.dirname(resolvedPath);
|
|
129
|
+
await fs.mkdir(dir, { recursive: true });
|
|
130
|
+
const content = yaml.dump(config, { lineWidth: -1 });
|
|
131
|
+
await fs.writeFile(resolvedPath, content, 'utf8');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check if wizard.yaml exists at path
|
|
136
|
+
* @async
|
|
137
|
+
* @function wizardConfigExists
|
|
138
|
+
* @param {string} configPath - Path to wizard.yaml file
|
|
139
|
+
* @returns {Promise<boolean>}
|
|
140
|
+
*/
|
|
141
|
+
async function wizardConfigExists(configPath) {
|
|
142
|
+
try {
|
|
143
|
+
await fs.access(path.resolve(configPath));
|
|
144
|
+
return true;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return error.code === 'ENOENT' ? false : Promise.reject(error);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
117
150
|
/**
|
|
118
151
|
* Validate wizard configuration against schema
|
|
119
152
|
* @function validateWizardConfigSchema
|
|
@@ -258,6 +291,8 @@ function displayValidationResults(result) {
|
|
|
258
291
|
|
|
259
292
|
module.exports = {
|
|
260
293
|
loadWizardConfig,
|
|
294
|
+
writeWizardConfig,
|
|
295
|
+
wizardConfigExists,
|
|
261
296
|
validateWizardConfig,
|
|
262
297
|
validateWizardConfigSchema,
|
|
263
298
|
resolveEnvVar,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aifabrix/builder",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.36.0",
|
|
4
4
|
"description": "AI Fabrix Local Fabric & Deployment SDK",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "node tests/scripts/test-wrapper.js",
|
|
12
12
|
"test:ci": "bash tests/scripts/ci-simulate.sh",
|
|
13
|
-
"test:coverage": "
|
|
13
|
+
"test:coverage": "cross-env RUN_COVERAGE=1 node tests/scripts/test-wrapper.js",
|
|
14
14
|
"test:coverage:nyc": "nyc --reporter=text --reporter=lcov --reporter=html jest --config jest.config.coverage.js --runInBand",
|
|
15
15
|
"test:watch": "jest --watch",
|
|
16
16
|
"test:integration": "jest --config jest.config.integration.js --runInBand",
|