@aifabrix/builder 2.44.4 → 2.44.5
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/jest.projects.js +20 -15
- package/lib/api/types/wizard.types.js +2 -1
- package/lib/cli/setup-app.help.js +1 -1
- package/lib/cli/setup-app.test-commands.js +9 -5
- package/lib/cli/setup-infra.js +6 -2
- package/lib/cli/setup-utility.js +19 -0
- package/lib/commands/repair-rbac.js +25 -2
- package/lib/commands/repair.js +1 -17
- package/lib/commands/test-e2e-external.js +4 -3
- package/lib/commands/up-common.js +25 -0
- package/lib/commands/wizard-core.js +53 -11
- package/lib/commands/wizard-entity-selection.js +71 -14
- package/lib/commands/wizard-headless.js +5 -2
- package/lib/commands/wizard-helpers.js +13 -1
- package/lib/commands/wizard.js +208 -60
- package/lib/generator/wizard-prompts.js +7 -1
- package/lib/generator/wizard.js +34 -0
- package/lib/schema/wizard-config.schema.json +1 -1
- package/lib/utils/external-readme.js +47 -3
- package/lib/utils/urls-local-registry.js +52 -10
- package/package.json +1 -1
- package/templates/applications/miso-controller/env.template +6 -6
- package/templates/external-system/README.md.hbs +58 -31
package/lib/commands/wizard.js
CHANGED
|
@@ -26,6 +26,7 @@ const {
|
|
|
26
26
|
validateAndCheckAppDirectory,
|
|
27
27
|
formatDataplaneRejectedTokenMessage,
|
|
28
28
|
extractSessionId,
|
|
29
|
+
handleSourceSelection,
|
|
29
30
|
handleOpenApiParsing,
|
|
30
31
|
handleCredentialSelection,
|
|
31
32
|
handleTypeDetection,
|
|
@@ -50,6 +51,149 @@ const {
|
|
|
50
51
|
} = require('./wizard-helpers');
|
|
51
52
|
const { humanizeAppKey } = require('../generator/wizard-prompts-secondary');
|
|
52
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Map resolved source type/data onto wizard state.source.
|
|
56
|
+
* @param {Object} state - Mutable wizard state
|
|
57
|
+
* @param {string} sourceType - Source type key
|
|
58
|
+
* @param {unknown} sourceData - Raw source payload from prompts
|
|
59
|
+
*/
|
|
60
|
+
function applySourceSelectionToState(state, sourceType, sourceData) {
|
|
61
|
+
state.source = { type: sourceType };
|
|
62
|
+
if (sourceType === 'openapi-file') state.source.filePath = sourceData;
|
|
63
|
+
else if (sourceType === 'openapi-url') state.source.url = sourceData;
|
|
64
|
+
else if (sourceType === 'mcp-server') state.source.serverUrl = JSON.parse(sourceData).serverUrl;
|
|
65
|
+
else if (sourceType === 'known-platform') state.source.platform = sourceData;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Interactive or prefill source selection (Step 2).
|
|
70
|
+
* @returns {Promise<{ sourceType: string, sourceData: unknown }>}
|
|
71
|
+
*/
|
|
72
|
+
async function resolveWizardSourcePhase(dataplaneUrl, sessionId, authConfig, platforms, prefill) {
|
|
73
|
+
if (prefill?.source?.type) {
|
|
74
|
+
logger.log(chalk.gray(
|
|
75
|
+
`Using source from wizard.yaml (${prefill.source.type}). Skipping source prompts.`
|
|
76
|
+
));
|
|
77
|
+
return handleSourceSelection(dataplaneUrl, sessionId, authConfig, prefill.source);
|
|
78
|
+
}
|
|
79
|
+
return handleInteractiveSourceSelection(dataplaneUrl, sessionId, authConfig, platforms);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Credential selection with optional prefill (Step 3).
|
|
84
|
+
* @returns {Promise<string>} credentialIdOrKey for generation step
|
|
85
|
+
*/
|
|
86
|
+
async function resolveWizardCredentialPhase(dataplaneUrl, authConfig, prefill, state) {
|
|
87
|
+
if (prefill?.credential) {
|
|
88
|
+
state.credential = prefill.credential;
|
|
89
|
+
return handleCredentialSelection(dataplaneUrl, authConfig, prefill.credential);
|
|
90
|
+
}
|
|
91
|
+
const credentialAction = await promptForCredentialAction();
|
|
92
|
+
const configCredential = await resolveCredentialConfig(dataplaneUrl, authConfig, credentialAction);
|
|
93
|
+
state.credential = configCredential;
|
|
94
|
+
return handleCredentialSelection(dataplaneUrl, authConfig, configCredential);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Collect user intent and UI preferences (Step 5), with optional wizard.yaml prefill.
|
|
99
|
+
* @returns {Promise<{ userIntent: string, preferences: Object, hasPrefillIntent: boolean }>}
|
|
100
|
+
*/
|
|
101
|
+
async function collectIntentAndPreferences(preferencesPrefill) {
|
|
102
|
+
const prefillPrefs = preferencesPrefill;
|
|
103
|
+
const hasPrefillIntent =
|
|
104
|
+
prefillPrefs &&
|
|
105
|
+
typeof prefillPrefs.intent === 'string' &&
|
|
106
|
+
prefillPrefs.intent.trim().length > 0;
|
|
107
|
+
|
|
108
|
+
if (hasPrefillIntent) {
|
|
109
|
+
logger.log(chalk.gray(
|
|
110
|
+
'Using preferences from wizard.yaml (intent and toggles). Skipping preference prompts.'
|
|
111
|
+
));
|
|
112
|
+
const level = prefillPrefs.fieldOnboardingLevel;
|
|
113
|
+
const validLevel = level === 'standard' || level === 'minimal' ? level : 'full';
|
|
114
|
+
return {
|
|
115
|
+
userIntent: prefillPrefs.intent.trim(),
|
|
116
|
+
preferences: {
|
|
117
|
+
fieldOnboardingLevel: validLevel,
|
|
118
|
+
mcp: Boolean(prefillPrefs.enableMCP),
|
|
119
|
+
abac: Boolean(prefillPrefs.enableABAC),
|
|
120
|
+
rbac: Boolean(prefillPrefs.enableRBAC)
|
|
121
|
+
},
|
|
122
|
+
hasPrefillIntent: true
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const userIntent = await promptForUserIntent();
|
|
127
|
+
const preferences = await promptForUserPreferences();
|
|
128
|
+
return { userIntent, preferences, hasPrefillIntent: false };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Run Step 5 (generate), Step 6–7 (review), and save files; updates state.preferences.
|
|
133
|
+
* @param {Object} payload
|
|
134
|
+
* @param {string} payload.appKey - Application key
|
|
135
|
+
* @param {string} payload.dataplaneUrl - Dataplane URL
|
|
136
|
+
* @param {Object} payload.authConfig - Auth configuration
|
|
137
|
+
* @param {string} payload.sessionId - Wizard session ID
|
|
138
|
+
* @param {Object} payload.state - Mutable wizard state
|
|
139
|
+
* @param {Object} payload.flowOpts - Flow options (mode, systemIdOrKey, debug, prefill, etc.)
|
|
140
|
+
* @param {Object} payload.genInput - Generation inputs (openapiSpec, detectedType, credential…)
|
|
141
|
+
* @returns {Promise<Object|null>} Updated state or null if review cancelled
|
|
142
|
+
*/
|
|
143
|
+
async function completeWizardGenerateReviewSave(payload) {
|
|
144
|
+
const {
|
|
145
|
+
appKey,
|
|
146
|
+
dataplaneUrl,
|
|
147
|
+
authConfig,
|
|
148
|
+
sessionId,
|
|
149
|
+
state,
|
|
150
|
+
flowOpts,
|
|
151
|
+
genInput
|
|
152
|
+
} = payload;
|
|
153
|
+
const { mode, systemIdOrKey, debug, prefill } = flowOpts;
|
|
154
|
+
const genResult = await handleInteractiveConfigGeneration({
|
|
155
|
+
dataplaneUrl,
|
|
156
|
+
authConfig,
|
|
157
|
+
mode,
|
|
158
|
+
openapiSpec: genInput.openapiSpec,
|
|
159
|
+
detectedType: genInput.detectedType,
|
|
160
|
+
credentialIdOrKey: genInput.credentialIdOrKey,
|
|
161
|
+
systemIdOrKey,
|
|
162
|
+
sourceType: genInput.sourceType,
|
|
163
|
+
sourceData: genInput.sourceData,
|
|
164
|
+
entityName: genInput.entityName,
|
|
165
|
+
appName: appKey,
|
|
166
|
+
debug,
|
|
167
|
+
systemDisplayName: flowOpts.systemDisplayName,
|
|
168
|
+
preferencesPrefill: prefill?.preferences
|
|
169
|
+
});
|
|
170
|
+
const { systemConfig, datasourceConfigs, systemKey, preferences: savedPrefs } = genResult;
|
|
171
|
+
state.preferences = savedPrefs || {};
|
|
172
|
+
|
|
173
|
+
const finalConfigs = await handleConfigurationReview(
|
|
174
|
+
dataplaneUrl,
|
|
175
|
+
authConfig,
|
|
176
|
+
sessionId,
|
|
177
|
+
systemConfig,
|
|
178
|
+
datasourceConfigs,
|
|
179
|
+
{ appKey, debug }
|
|
180
|
+
);
|
|
181
|
+
if (!finalConfigs) return null;
|
|
182
|
+
|
|
183
|
+
await handleFileSaving(
|
|
184
|
+
appKey,
|
|
185
|
+
finalConfigs.systemConfig,
|
|
186
|
+
finalConfigs.datasourceConfigs,
|
|
187
|
+
systemKey || appKey,
|
|
188
|
+
{
|
|
189
|
+
dataplaneUrl,
|
|
190
|
+
authConfig,
|
|
191
|
+
enableRBAC: Boolean(savedPrefs?.enableRBAC)
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
return state;
|
|
195
|
+
}
|
|
196
|
+
|
|
53
197
|
/**
|
|
54
198
|
* Create wizard session with given mode and optional systemIdOrKey (no prompts)
|
|
55
199
|
* @async
|
|
@@ -125,12 +269,15 @@ async function handleInteractiveSourceSelection(dataplaneUrl, sessionId, authCon
|
|
|
125
269
|
* @param {Object} options.detectedType - Detected type info
|
|
126
270
|
* @param {string} [options.credentialIdOrKey] - Credential ID or key (optional)
|
|
127
271
|
* @param {string} [options.systemIdOrKey] - System ID or key (optional)
|
|
272
|
+
* @param {Object} [options.preferencesPrefill] - From wizard.yaml `preferences` (skip Step 5 prompts when intent is set)
|
|
128
273
|
* @returns {Promise<Object>} Generated configuration and preferences { systemConfig, datasourceConfigs, systemKey, preferences }
|
|
129
274
|
*/
|
|
130
275
|
async function handleInteractiveConfigGeneration(options) {
|
|
131
276
|
logger.log(chalk.blue('\n\uD83D\uDCCB Step 5: User Preferences'));
|
|
132
|
-
|
|
133
|
-
const preferences = await
|
|
277
|
+
|
|
278
|
+
const { userIntent, preferences, hasPrefillIntent } = await collectIntentAndPreferences(
|
|
279
|
+
options.preferencesPrefill
|
|
280
|
+
);
|
|
134
281
|
|
|
135
282
|
const configPrefs = {
|
|
136
283
|
intent: userIntent,
|
|
@@ -138,6 +285,9 @@ async function handleInteractiveConfigGeneration(options) {
|
|
|
138
285
|
enableMCP: preferences.mcp,
|
|
139
286
|
enableABAC: preferences.abac,
|
|
140
287
|
enableRBAC: preferences.rbac,
|
|
288
|
+
enableOpenAPIGeneration: hasPrefillIntent
|
|
289
|
+
? options.preferencesPrefill?.enableOpenAPIGeneration !== false
|
|
290
|
+
: true,
|
|
141
291
|
debug: options.debug === true
|
|
142
292
|
};
|
|
143
293
|
|
|
@@ -222,62 +372,43 @@ async function handleConfigurationReview(dataplaneUrl, authConfig, sessionId, sy
|
|
|
222
372
|
* @returns {Promise<Object>} Collected state (source, credential, preferences) for wizard.yaml save
|
|
223
373
|
*/
|
|
224
374
|
async function doWizardSteps(appKey, dataplaneUrl, authConfig, sessionId, flowOpts, state) {
|
|
225
|
-
const {
|
|
226
|
-
|
|
227
|
-
|
|
375
|
+
const { platforms, prefill } = flowOpts;
|
|
376
|
+
|
|
377
|
+
const { sourceType, sourceData } = await resolveWizardSourcePhase(
|
|
378
|
+
dataplaneUrl,
|
|
379
|
+
sessionId,
|
|
380
|
+
authConfig,
|
|
381
|
+
platforms,
|
|
382
|
+
prefill
|
|
228
383
|
);
|
|
229
|
-
state
|
|
230
|
-
if (sourceType === 'openapi-file') state.source.filePath = sourceData;
|
|
231
|
-
else if (sourceType === 'openapi-url') state.source.url = sourceData;
|
|
232
|
-
else if (sourceType === 'mcp-server') state.source.serverUrl = JSON.parse(sourceData).serverUrl;
|
|
233
|
-
else if (sourceType === 'known-platform') state.source.platform = sourceData;
|
|
384
|
+
applySourceSelectionToState(state, sourceType, sourceData);
|
|
234
385
|
|
|
235
386
|
const openapiSpec = await handleOpenApiParsing(dataplaneUrl, authConfig, sourceType, sourceData);
|
|
236
|
-
const
|
|
237
|
-
const configCredential = await resolveCredentialConfig(dataplaneUrl, authConfig, credentialAction);
|
|
238
|
-
state.credential = configCredential;
|
|
239
|
-
const credentialIdOrKey = await handleCredentialSelection(dataplaneUrl, authConfig, configCredential);
|
|
387
|
+
const credentialIdOrKey = await resolveWizardCredentialPhase(dataplaneUrl, authConfig, prefill, state);
|
|
240
388
|
|
|
241
389
|
const detectedType = await handleTypeDetection(dataplaneUrl, authConfig, openapiSpec);
|
|
390
|
+
const prefillEntityName = prefill?.source?.entityName;
|
|
242
391
|
const entityName = openapiSpec && sourceType !== 'known-platform'
|
|
243
|
-
? await handleEntitySelection(dataplaneUrl, authConfig, openapiSpec) : null;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
detectedType,
|
|
250
|
-
credentialIdOrKey,
|
|
251
|
-
systemIdOrKey,
|
|
252
|
-
sourceType,
|
|
253
|
-
sourceData,
|
|
254
|
-
entityName: entityName || undefined,
|
|
255
|
-
appName: appKey,
|
|
256
|
-
debug,
|
|
257
|
-
systemDisplayName: flowOpts.systemDisplayName
|
|
258
|
-
});
|
|
259
|
-
const { systemConfig, datasourceConfigs, systemKey, preferences: savedPrefs } = genResult;
|
|
260
|
-
state.preferences = savedPrefs || {};
|
|
261
|
-
|
|
262
|
-
const finalConfigs = await handleConfigurationReview(
|
|
392
|
+
? await handleEntitySelection(dataplaneUrl, authConfig, openapiSpec, prefillEntityName) : null;
|
|
393
|
+
if (entityName && state.source) {
|
|
394
|
+
state.source.entityName = entityName;
|
|
395
|
+
}
|
|
396
|
+
return completeWizardGenerateReviewSave({
|
|
397
|
+
appKey,
|
|
263
398
|
dataplaneUrl,
|
|
264
399
|
authConfig,
|
|
265
400
|
sessionId,
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
{
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
dataplaneUrl,
|
|
278
|
-
authConfig
|
|
279
|
-
);
|
|
280
|
-
return state;
|
|
401
|
+
state,
|
|
402
|
+
flowOpts,
|
|
403
|
+
genInput: {
|
|
404
|
+
openapiSpec,
|
|
405
|
+
detectedType,
|
|
406
|
+
credentialIdOrKey,
|
|
407
|
+
sourceType,
|
|
408
|
+
sourceData,
|
|
409
|
+
entityName: entityName || undefined
|
|
410
|
+
}
|
|
411
|
+
});
|
|
281
412
|
}
|
|
282
413
|
|
|
283
414
|
async function runWizardStepsAfterSession(appKey, dataplaneUrl, authConfig, sessionId, flowOpts) {
|
|
@@ -317,7 +448,7 @@ async function runWizardStepsAfterSession(appKey, dataplaneUrl, authConfig, sess
|
|
|
317
448
|
* @returns {Promise<void>} Resolves when wizard flow completes
|
|
318
449
|
*/
|
|
319
450
|
async function executeWizardFlow(appKey, dataplaneUrl, authConfig, flowOpts = {}) {
|
|
320
|
-
const { mode, systemIdOrKey, configPath, debug, systemDisplayName } = flowOpts;
|
|
451
|
+
const { mode, systemIdOrKey, configPath, debug, systemDisplayName, prefill } = flowOpts;
|
|
321
452
|
|
|
322
453
|
if (debug) {
|
|
323
454
|
logger.log(chalk.gray(`[DEBUG] Wizard debug mode enabled for app: ${appKey}`));
|
|
@@ -333,7 +464,8 @@ async function executeWizardFlow(appKey, dataplaneUrl, authConfig, flowOpts = {}
|
|
|
333
464
|
platforms,
|
|
334
465
|
configPath,
|
|
335
466
|
debug: flowOpts.debug,
|
|
336
|
-
systemDisplayName
|
|
467
|
+
systemDisplayName,
|
|
468
|
+
prefill
|
|
337
469
|
});
|
|
338
470
|
if (!state) return;
|
|
339
471
|
|
|
@@ -348,6 +480,11 @@ async function executeWizardFlow(appKey, dataplaneUrl, authConfig, flowOpts = {}
|
|
|
348
480
|
* @param {string} [appName] - App name (for log message)
|
|
349
481
|
* @returns {Promise<Object|null>} Loaded config or null
|
|
350
482
|
*/
|
|
483
|
+
/**
|
|
484
|
+
* Load wizard.yaml when present. Returns full valid config, or invalid-but-parsed config for interactive prefill.
|
|
485
|
+
*
|
|
486
|
+
* @returns {Promise<{ valid: true, config: Object } | { valid: false, config: Object, errors: string[] } | null>}
|
|
487
|
+
*/
|
|
351
488
|
async function loadWizardConfigIfExists(configPath, appName) {
|
|
352
489
|
if (!configPath) return null;
|
|
353
490
|
const displayPath = appName ? `integration/${appName}/wizard.yaml` : configPath;
|
|
@@ -360,10 +497,16 @@ async function loadWizardConfigIfExists(configPath, appName) {
|
|
|
360
497
|
const result = await validateWizardConfig(configPath, { validateFilePaths: false });
|
|
361
498
|
if (result.valid && result.config) {
|
|
362
499
|
logger.log(chalk.green(`Loaded saved state from ${displayPath}. Resuming with saved choices.`));
|
|
363
|
-
return result.config;
|
|
500
|
+
return { valid: true, config: result.config };
|
|
364
501
|
}
|
|
365
502
|
if (result.errors?.length) {
|
|
366
|
-
logger.log(chalk.yellow(
|
|
503
|
+
logger.log(chalk.yellow(
|
|
504
|
+
`Loaded ${displayPath} but it does not fully validate; prefilling from file where possible.`
|
|
505
|
+
));
|
|
506
|
+
result.errors.forEach(err => logger.log(chalk.gray(` • ${err}`)));
|
|
507
|
+
}
|
|
508
|
+
if (result.config) {
|
|
509
|
+
return { valid: false, config: result.config, errors: result.errors || [] };
|
|
367
510
|
}
|
|
368
511
|
} catch (e) {
|
|
369
512
|
logger.log(chalk.gray(`Could not load wizard config from ${displayPath}: ${e.message}`));
|
|
@@ -470,15 +613,16 @@ async function handleWizardWithSavedConfig(options, loadedConfig, displayPath) {
|
|
|
470
613
|
}
|
|
471
614
|
|
|
472
615
|
async function handleWizardInteractive(options) {
|
|
616
|
+
const prefill = options.wizardPrefill;
|
|
473
617
|
const allowAddDatasource = !options.app;
|
|
474
618
|
const mode = allowAddDatasource ? await promptForMode(undefined, true) : 'create-system';
|
|
475
619
|
const resolved = mode === 'create-system'
|
|
476
|
-
? await resolveCreateNewPath(options, null)
|
|
477
|
-
: await resolveAddDatasourcePath(options, null);
|
|
620
|
+
? await resolveCreateNewPath(options, prefill || null)
|
|
621
|
+
: await resolveAddDatasourcePath(options, prefill || null);
|
|
478
622
|
if (!resolved) return;
|
|
479
623
|
const { appKey, configPath, dataplaneUrl, authConfig } = resolved;
|
|
480
624
|
const systemIdOrKey = mode === 'add-datasource' ? resolved.systemIdOrKey : undefined;
|
|
481
|
-
const systemDisplayName = options.systemDisplayName || options.displayName ||
|
|
625
|
+
const systemDisplayName = options.systemDisplayName || options.displayName || prefill?.systemDisplayName ||
|
|
482
626
|
(mode === 'create-system' ? humanizeAppKey(appKey) : undefined);
|
|
483
627
|
try {
|
|
484
628
|
await executeWizardFlow(appKey, dataplaneUrl, authConfig, {
|
|
@@ -486,7 +630,8 @@ async function handleWizardInteractive(options) {
|
|
|
486
630
|
systemIdOrKey,
|
|
487
631
|
configPath,
|
|
488
632
|
debug: options.debug,
|
|
489
|
-
systemDisplayName
|
|
633
|
+
systemDisplayName,
|
|
634
|
+
prefill: prefill || undefined
|
|
490
635
|
});
|
|
491
636
|
logger.log(chalk.gray(`To change settings, edit integration/${appKey}/wizard.yaml and run: aifabrix wizard ${appKey}`));
|
|
492
637
|
} catch (error) {
|
|
@@ -504,9 +649,12 @@ async function handleWizard(options = {}) {
|
|
|
504
649
|
return await handleWizardSilent(options);
|
|
505
650
|
}
|
|
506
651
|
logger.log(chalk.blue('\n\uD83E\uDDD9 AI Fabrix External System Wizard\n'));
|
|
507
|
-
const
|
|
508
|
-
if (
|
|
509
|
-
return await handleWizardWithSavedConfig(options,
|
|
652
|
+
const loadResult = await loadWizardConfigIfExists(options.configPath, options.app);
|
|
653
|
+
if (loadResult?.valid && loadResult.config) {
|
|
654
|
+
return await handleWizardWithSavedConfig(options, loadResult.config, displayPath);
|
|
655
|
+
}
|
|
656
|
+
if (loadResult?.config && loadResult.valid === false) {
|
|
657
|
+
return await handleWizardInteractive({ ...options, wizardPrefill: loadResult.config });
|
|
510
658
|
}
|
|
511
659
|
return await handleWizardInteractive(options);
|
|
512
660
|
}
|
|
@@ -324,17 +324,22 @@ async function promptForExistingCredentialInput() {
|
|
|
324
324
|
* @function promptForUserIntent
|
|
325
325
|
* @returns {Promise<string>} User intent
|
|
326
326
|
*/
|
|
327
|
+
const WIZARD_INTENT_MAX_LENGTH = 1000;
|
|
328
|
+
|
|
327
329
|
async function promptForUserIntent() {
|
|
328
330
|
const { intent } = await inquirer.prompt([
|
|
329
331
|
{
|
|
330
332
|
type: 'input',
|
|
331
333
|
name: 'intent',
|
|
332
|
-
message:
|
|
334
|
+
message: `Describe your primary use case (max ${WIZARD_INTENT_MAX_LENGTH} characters):`,
|
|
333
335
|
default: 'general integration',
|
|
334
336
|
validate: (input) => {
|
|
335
337
|
if (!input || typeof input !== 'string' || input.trim().length === 0) {
|
|
336
338
|
return 'Intent is required';
|
|
337
339
|
}
|
|
340
|
+
if (input.length > WIZARD_INTENT_MAX_LENGTH) {
|
|
341
|
+
return `Intent must be ${WIZARD_INTENT_MAX_LENGTH} characters or fewer`;
|
|
342
|
+
}
|
|
338
343
|
return true;
|
|
339
344
|
}
|
|
340
345
|
}
|
|
@@ -437,6 +442,7 @@ async function promptForRunWithSavedConfig() {
|
|
|
437
442
|
const secondary = require('./wizard-prompts-secondary');
|
|
438
443
|
|
|
439
444
|
module.exports = {
|
|
445
|
+
WIZARD_INTENT_MAX_LENGTH,
|
|
440
446
|
promptForMode,
|
|
441
447
|
promptForSystemIdOrKey,
|
|
442
448
|
promptForExistingSystem,
|
package/lib/generator/wizard.js
CHANGED
|
@@ -32,6 +32,35 @@ function toKeySegment(str) {
|
|
|
32
32
|
return sanitized || 'default';
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Align authentication.security kv:// namespaces and credentialKey with the final system key.
|
|
37
|
+
* Dataplane normalizes this before respond; when appName overrides a spec-derived key (e.g.
|
|
38
|
+
* OpenAPI title "Companies"), the builder still must rewrite nested auth so it matches env.template.
|
|
39
|
+
*
|
|
40
|
+
* @param {Object|null|undefined} authentication - authentication block from dataplane
|
|
41
|
+
* @param {string} systemKey - Final external system key (integration app name)
|
|
42
|
+
* @param {string} [authDisplayName] - Credential display name (typically title-cased app name)
|
|
43
|
+
*/
|
|
44
|
+
function normalizeAuthenticationToSystemKey(authentication, systemKey, authDisplayName) {
|
|
45
|
+
if (!authentication || typeof authentication !== 'object') return;
|
|
46
|
+
authentication.credentialKey = `${systemKey}-cred`;
|
|
47
|
+
const security = authentication.security;
|
|
48
|
+
if (security && typeof security === 'object') {
|
|
49
|
+
for (const k of Object.keys(security)) {
|
|
50
|
+
const v = security[k];
|
|
51
|
+
if (typeof v === 'string' && v.startsWith('kv://')) {
|
|
52
|
+
const rest = v.slice(5);
|
|
53
|
+
const idx = rest.indexOf('/');
|
|
54
|
+
const suffix = idx >= 0 ? rest.slice(idx + 1) : '';
|
|
55
|
+
security[k] = suffix ? `kv://${systemKey}/${suffix}` : `kv://${systemKey}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (authDisplayName) {
|
|
60
|
+
authentication.displayName = authDisplayName;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
35
64
|
/**
|
|
36
65
|
* Generate files from dataplane-generated wizard configurations
|
|
37
66
|
* @async
|
|
@@ -182,6 +211,11 @@ async function prepareWizardContext(appName, systemConfig, datasourceConfigs) {
|
|
|
182
211
|
const originalSystemKey = systemConfig.key || finalSystemKey;
|
|
183
212
|
const appDisplayName = appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
184
213
|
const updatedSystemConfig = { ...systemConfig, key: finalSystemKey, displayName: appDisplayName };
|
|
214
|
+
normalizeAuthenticationToSystemKey(
|
|
215
|
+
updatedSystemConfig.authentication,
|
|
216
|
+
finalSystemKey,
|
|
217
|
+
appDisplayName
|
|
218
|
+
);
|
|
185
219
|
const originalPrefix = `${originalSystemKey}-`;
|
|
186
220
|
const updatedDatasourceConfigs = datasourceConfigs.map(ds => {
|
|
187
221
|
let newKey;
|
|
@@ -137,11 +137,43 @@ function rbacOptionalFilename(normalizedExt) {
|
|
|
137
137
|
return normalizedExt === '.yaml' || normalizedExt === '.yml' ? 'rbac.yaml' : 'rbac.json';
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Word-wraps plain description text for Markdown (MD013 ~80 columns). Keeps blank
|
|
142
|
+
* lines between paragraphs.
|
|
143
|
+
* @param {string} text - Raw description
|
|
144
|
+
* @param {number} [maxLen=80] - Target max line length
|
|
145
|
+
* @returns {string}
|
|
146
|
+
*/
|
|
147
|
+
function wrapPlainTextForMarkdown(text, maxLen = 80) {
|
|
148
|
+
if (!text || typeof text !== 'string') return text;
|
|
149
|
+
const blocks = text.split(/\n\s*\n/);
|
|
150
|
+
const wrapped = blocks.map((block) => {
|
|
151
|
+
const flat = block.replace(/\s+/g, ' ').trim();
|
|
152
|
+
if (!flat) return '';
|
|
153
|
+
const words = flat.split(' ');
|
|
154
|
+
const lines = [];
|
|
155
|
+
let current = '';
|
|
156
|
+
for (const w of words) {
|
|
157
|
+
const candidate = current ? `${current} ${w}` : w;
|
|
158
|
+
if (candidate.length <= maxLen) {
|
|
159
|
+
current = candidate;
|
|
160
|
+
} else {
|
|
161
|
+
if (current) lines.push(current);
|
|
162
|
+
current = w;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (current) lines.push(current);
|
|
166
|
+
return lines.join('\n');
|
|
167
|
+
});
|
|
168
|
+
return wrapped.filter(Boolean).join('\n\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
140
171
|
function buildExternalReadmeContext(params = {}) {
|
|
141
172
|
const appName = params.appName || params.systemKey || 'external-system';
|
|
142
173
|
const systemKey = params.systemKey || appName;
|
|
143
174
|
const displayName = params.displayName || formatDisplayName(systemKey);
|
|
144
|
-
const
|
|
175
|
+
const rawDescription = params.description || `External system integration for ${systemKey}`;
|
|
176
|
+
const description = wrapPlainTextForMarkdown(rawDescription);
|
|
145
177
|
const systemType = params.systemType || 'openapi';
|
|
146
178
|
const fileExt = params.fileExt !== undefined ? params.fileExt : '.json';
|
|
147
179
|
const normalizedExt = fileExt && fileExt.startsWith('.') ? fileExt : `.${fileExt || 'json'}`;
|
|
@@ -186,13 +218,25 @@ function loadExternalReadmeTemplate() {
|
|
|
186
218
|
* @param {Object} params - Context parameters
|
|
187
219
|
* @returns {string} README content
|
|
188
220
|
*/
|
|
221
|
+
/**
|
|
222
|
+
* Collapses 3+ consecutive newlines to 2 (fixes MD012 from Handlebars spacing).
|
|
223
|
+
* @param {string} md - Markdown body
|
|
224
|
+
* @returns {string}
|
|
225
|
+
*/
|
|
226
|
+
function collapseConsecutiveBlankLines(md) {
|
|
227
|
+
if (!md || typeof md !== 'string') return md;
|
|
228
|
+
return md.replace(/\n{3,}/g, '\n\n');
|
|
229
|
+
}
|
|
230
|
+
|
|
189
231
|
function generateExternalReadmeContent(params = {}) {
|
|
190
232
|
const template = loadExternalReadmeTemplate();
|
|
191
233
|
const context = buildExternalReadmeContext(params);
|
|
192
|
-
return template(context);
|
|
234
|
+
return collapseConsecutiveBlankLines(template(context));
|
|
193
235
|
}
|
|
194
236
|
|
|
195
237
|
module.exports = {
|
|
196
238
|
buildExternalReadmeContext,
|
|
197
|
-
generateExternalReadmeContent
|
|
239
|
+
generateExternalReadmeContent,
|
|
240
|
+
wrapPlainTextForMarkdown,
|
|
241
|
+
collapseConsecutiveBlankLines
|
|
198
242
|
};
|
|
@@ -175,6 +175,54 @@ function mergeBuilderDirIntoRegistry(merged, builderDir) {
|
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* True when getBuilderRoot() resolves to the same path as AIFABRIX_BUILDER_DIR (authoritative override).
|
|
180
|
+
* @param {string|null} resolvedEffective
|
|
181
|
+
* @param {string|null} envResolved
|
|
182
|
+
* @returns {boolean}
|
|
183
|
+
*/
|
|
184
|
+
function effectiveBuilderMatchesEnvVar(resolvedEffective, envResolved) {
|
|
185
|
+
return Boolean(envResolved && resolvedEffective && resolvedEffective === envResolved);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Builder dirs to scan in order; later merges overwrite registry keys from earlier dirs.
|
|
190
|
+
* When `AIFABRIX_BUILDER_DIR` is set and differs from projectRoot/builder, env builder root is merged last.
|
|
191
|
+
* Otherwise projectRoot/builder is merged last so explicit refresh roots override getBuilderRoot().
|
|
192
|
+
*
|
|
193
|
+
* @param {string} root - Resolved project root passed to refresh
|
|
194
|
+
* @param {string|null} effectiveBuilderDir - pathsUtil.getBuilderRoot()
|
|
195
|
+
* @returns {string[]}
|
|
196
|
+
*/
|
|
197
|
+
function getOrderedBuilderDirsForRegistryScan(root, effectiveBuilderDir) {
|
|
198
|
+
const legacyBuilderDir = path.join(root, 'builder');
|
|
199
|
+
let resolvedLegacy;
|
|
200
|
+
let resolvedEffective;
|
|
201
|
+
try {
|
|
202
|
+
resolvedLegacy = path.resolve(legacyBuilderDir);
|
|
203
|
+
resolvedEffective = effectiveBuilderDir ? path.resolve(effectiveBuilderDir) : null;
|
|
204
|
+
} catch {
|
|
205
|
+
return [legacyBuilderDir];
|
|
206
|
+
}
|
|
207
|
+
const envRaw = process.env.AIFABRIX_BUILDER_DIR && String(process.env.AIFABRIX_BUILDER_DIR).trim();
|
|
208
|
+
const envResolved = envRaw ? path.resolve(envRaw) : null;
|
|
209
|
+
// Only treat env as authoritative when getBuilderRoot() is actually that path. Otherwise a stray
|
|
210
|
+
// AIFABRIX_BUILDER_DIR on CI (or Jest mocking getBuilderRoot to a temp dir) must not force
|
|
211
|
+
// [legacy, effective] — that order lets the real checkout builder overwrite the mocked root last.
|
|
212
|
+
const effectiveMatchesEnvVar = effectiveBuilderMatchesEnvVar(resolvedEffective, envResolved);
|
|
213
|
+
|
|
214
|
+
if (effectiveBuilderDir && resolvedEffective && resolvedEffective === resolvedLegacy) {
|
|
215
|
+
return [legacyBuilderDir];
|
|
216
|
+
}
|
|
217
|
+
if (effectiveMatchesEnvVar && effectiveBuilderDir && resolvedEffective && resolvedEffective !== resolvedLegacy) {
|
|
218
|
+
return [legacyBuilderDir, effectiveBuilderDir];
|
|
219
|
+
}
|
|
220
|
+
if (effectiveBuilderDir && resolvedEffective && resolvedEffective !== resolvedLegacy) {
|
|
221
|
+
return [effectiveBuilderDir, legacyBuilderDir];
|
|
222
|
+
}
|
|
223
|
+
return [legacyBuilderDir];
|
|
224
|
+
}
|
|
225
|
+
|
|
178
226
|
/**
|
|
179
227
|
* Merge scan results into registry (does not remove stale keys).
|
|
180
228
|
* @param {string|null} projectRoot - getProjectRoot() or null (same semantics as projectRoot || getProjectRoot())
|
|
@@ -188,19 +236,13 @@ function refreshUrlsLocalRegistryFromBuilder(projectRoot) {
|
|
|
188
236
|
}
|
|
189
237
|
// Published npm tarball omits builder/ under the package root (.npmignore). Global installs must
|
|
190
238
|
// still refresh from the real builder tree (AIFABRIX_BUILDER_DIR or integration base + builder).
|
|
191
|
-
|
|
192
|
-
const effectiveBuilderDir = pathsUtil.getBuilderRoot();
|
|
193
|
-
const builderDirs = [legacyBuilderDir];
|
|
239
|
+
let effectiveBuilderDir = null;
|
|
194
240
|
try {
|
|
195
|
-
|
|
196
|
-
effectiveBuilderDir &&
|
|
197
|
-
path.resolve(effectiveBuilderDir) !== path.resolve(legacyBuilderDir)
|
|
198
|
-
) {
|
|
199
|
-
builderDirs.push(effectiveBuilderDir);
|
|
200
|
-
}
|
|
241
|
+
effectiveBuilderDir = pathsUtil.getBuilderRoot();
|
|
201
242
|
} catch {
|
|
202
|
-
|
|
243
|
+
effectiveBuilderDir = null;
|
|
203
244
|
}
|
|
245
|
+
const builderDirs = getOrderedBuilderDirsForRegistryScan(root, effectiveBuilderDir);
|
|
204
246
|
for (const builderDir of builderDirs) {
|
|
205
247
|
mergeBuilderDirIntoRegistry(merged, builderDir);
|
|
206
248
|
}
|
package/package.json
CHANGED
|
@@ -28,8 +28,12 @@ SKIP_FIRST_TIME_SETUP=false
|
|
|
28
28
|
# Optional custom controller key for onboarding (default: miso-controller)
|
|
29
29
|
ONBOARDING_CONTROLLER_KEY=miso-controller
|
|
30
30
|
|
|
31
|
-
#
|
|
32
|
-
|
|
31
|
+
# Infrastructure/service name used by controller/onboarding flows
|
|
32
|
+
INFRASTRUCTURE_NAME=aifabrix
|
|
33
|
+
|
|
34
|
+
# Azure region (required for first-time onboarding: controller record / provisioning metadata).
|
|
35
|
+
# Use the region you deploy to (e.g. westeurope, eastus, northeurope). Single env name: LOCATION.
|
|
36
|
+
LOCATION=westeurope
|
|
33
37
|
|
|
34
38
|
# Required for admin user creation during onboarding
|
|
35
39
|
# Password for the initial administrator user (username: admin)
|
|
@@ -319,10 +323,6 @@ MISO_ENVIRONMENT=miso
|
|
|
319
323
|
MISO_CLIENTID=kv://miso-controller-client-idKeyVault
|
|
320
324
|
MISO_CLIENTSECRET=kv://miso-controller-client-secretKeyVault
|
|
321
325
|
|
|
322
|
-
# Allowed origins for CORS validation (comma-separated)
|
|
323
|
-
# Use wildcards for ports: http://localhost:*
|
|
324
|
-
MISO_ALLOWED_ORIGINS=http://localhost:*,url://host-public,url://host-private,url://dataplane-host-public,url://dataplane-host-private
|
|
325
|
-
|
|
326
326
|
# Evaluation mode (optional .env override of DB controller.configuration.evaluation):
|
|
327
327
|
# When true (default if DB omits flag), infra deploy may coerce :envKey to `miso` — e2e poll on `dev` can 404.
|
|
328
328
|
# Set false locally to force path envKey to match deploy + GET .../deployments/:id.
|