@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.
@@ -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
- { name: 'Create a new external system', value: 'create-system' },
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[]} [platforms] - List of available platforms (if provided)
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
 
@@ -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.33.6",
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": "jest --config jest.config.coverage.js --coverage --runInBand",
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",