@aifabrix/builder 2.33.5 → 2.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/integration/test-hubspot/wizard.yaml +8 -0
- package/lib/api/wizard.api.js +24 -1
- package/lib/app/run-helpers.js +7 -2
- package/lib/app/run.js +2 -2
- package/lib/app/show-display.js +184 -0
- package/lib/app/show.js +642 -0
- package/lib/cli.js +38 -9
- package/lib/commands/auth-status.js +58 -2
- package/lib/commands/up-miso.js +25 -16
- package/lib/commands/wizard-core-helpers.js +278 -0
- package/lib/commands/wizard-core.js +74 -161
- package/lib/commands/wizard-headless.js +2 -2
- package/lib/commands/wizard-helpers.js +143 -0
- package/lib/commands/wizard.js +282 -69
- package/lib/datasource/list.js +6 -3
- package/lib/generator/index.js +32 -0
- package/lib/generator/wizard-prompts.js +111 -44
- package/lib/infrastructure/services.js +6 -3
- package/lib/utils/app-register-auth.js +9 -2
- package/lib/utils/cli-utils.js +40 -1
- package/lib/utils/error-formatters/http-status-errors.js +8 -0
- package/lib/utils/error-formatters/permission-errors.js +44 -1
- package/lib/utils/infra-containers.js +19 -16
- package/lib/utils/infra-status.js +12 -3
- package/lib/validation/wizard-config-validator.js +35 -0
- package/package.json +2 -2
|
@@ -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
|
|
|
@@ -132,6 +132,8 @@ async function waitForServices(devId = null) {
|
|
|
132
132
|
* @async
|
|
133
133
|
* @function checkInfraHealth
|
|
134
134
|
* @param {number|string|null} [devId] - Developer ID (null = use current)
|
|
135
|
+
* @param {Object} [options] - Options
|
|
136
|
+
* @param {boolean} [options.strict=false] - When true, only consider current dev's containers (no fallback to dev 0); use for up-miso and status consistency
|
|
135
137
|
* @returns {Promise<Object>} Health status of each service
|
|
136
138
|
* @throws {Error} If health check fails
|
|
137
139
|
*
|
|
@@ -139,20 +141,21 @@ async function waitForServices(devId = null) {
|
|
|
139
141
|
* const health = await checkInfraHealth();
|
|
140
142
|
* // Returns: { postgres: 'healthy', redis: 'healthy', pgadmin: 'healthy', redis-commander: 'healthy' }
|
|
141
143
|
*/
|
|
142
|
-
async function checkInfraHealth(devId = null) {
|
|
144
|
+
async function checkInfraHealth(devId = null, options = {}) {
|
|
143
145
|
const developerId = devId || await config.getDeveloperId();
|
|
144
146
|
const servicesWithHealthCheck = ['postgres', 'redis'];
|
|
145
147
|
const servicesWithoutHealthCheck = ['pgadmin', 'redis-commander'];
|
|
146
148
|
const health = {};
|
|
149
|
+
const lookupOptions = options.strict ? { strict: true } : {};
|
|
147
150
|
|
|
148
151
|
// Check health status for services with health checks
|
|
149
152
|
for (const service of servicesWithHealthCheck) {
|
|
150
|
-
health[service] = await containerUtils.checkServiceWithHealthCheck(service, developerId);
|
|
153
|
+
health[service] = await containerUtils.checkServiceWithHealthCheck(service, developerId, lookupOptions);
|
|
151
154
|
}
|
|
152
155
|
|
|
153
156
|
// Check if services without health checks are running
|
|
154
157
|
for (const service of servicesWithoutHealthCheck) {
|
|
155
|
-
health[service] = await containerUtils.checkServiceWithoutHealthCheck(service, developerId);
|
|
158
|
+
health[service] = await containerUtils.checkServiceWithoutHealthCheck(service, developerId, lookupOptions);
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
return health;
|
|
@@ -138,6 +138,7 @@ async function tryFindDeviceTokenFromConfig(deviceConfig, attemptedUrls) {
|
|
|
138
138
|
|
|
139
139
|
/**
|
|
140
140
|
* Creates error data for authentication failure
|
|
141
|
+
* Deduplicates attemptedUrls so the same URL is not shown twice (e.g. when controller URL and device config URL match).
|
|
141
142
|
* @function createAuthErrorData
|
|
142
143
|
* @param {Error|null} lastError - Last error encountered
|
|
143
144
|
* @param {string|null} controllerUrl - Original controller URL
|
|
@@ -145,10 +146,16 @@ async function tryFindDeviceTokenFromConfig(deviceConfig, attemptedUrls) {
|
|
|
145
146
|
* @returns {Object} Error data object
|
|
146
147
|
*/
|
|
147
148
|
function createAuthErrorData(lastError, controllerUrl, attemptedUrls) {
|
|
149
|
+
const seen = new Set();
|
|
150
|
+
const uniqueUrls = attemptedUrls.filter(u => {
|
|
151
|
+
if (seen.has(u)) return false;
|
|
152
|
+
seen.add(u);
|
|
153
|
+
return true;
|
|
154
|
+
});
|
|
148
155
|
return {
|
|
149
156
|
message: lastError ? lastError.message : 'No valid authentication found',
|
|
150
|
-
controllerUrl: controllerUrl || (
|
|
151
|
-
attemptedUrls:
|
|
157
|
+
controllerUrl: controllerUrl || (uniqueUrls.length > 0 ? uniqueUrls[0] : undefined),
|
|
158
|
+
attemptedUrls: uniqueUrls.length > 1 ? uniqueUrls : undefined,
|
|
152
159
|
correlationId: undefined
|
|
153
160
|
};
|
|
154
161
|
}
|
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
|
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const chalk = require('chalk');
|
|
12
|
+
const { getPermissionDetailLines } = require('./permission-errors');
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Formats authentication error
|
|
@@ -108,6 +109,13 @@ function formatAuthenticationError(errorData) {
|
|
|
108
109
|
addControllerUrlInfo(lines, errorData);
|
|
109
110
|
addAttemptedUrlsInfo(lines, errorData);
|
|
110
111
|
addErrorMessageIfNotGeneric(lines, errorData);
|
|
112
|
+
|
|
113
|
+
// Show permission details when API returns them (401 "insufficient permissions" etc.)
|
|
114
|
+
const permissionLines = getPermissionDetailLines(errorData);
|
|
115
|
+
if (permissionLines.length > 0) {
|
|
116
|
+
lines.push(...permissionLines);
|
|
117
|
+
}
|
|
118
|
+
|
|
111
119
|
addAuthenticationGuidance(lines, errorData);
|
|
112
120
|
|
|
113
121
|
return lines.join('\n');
|
|
@@ -12,31 +12,44 @@ const chalk = require('chalk');
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Extracts missing permissions from error data
|
|
15
|
+
* Supports: missingPermissions, missing.permissions, data.missing.permissions
|
|
15
16
|
* @param {Object} errorData - Error response data
|
|
16
17
|
* @returns {Array<string>} Array of missing permissions
|
|
17
18
|
*/
|
|
18
19
|
function extractMissingPermissions(errorData) {
|
|
20
|
+
if (!errorData || typeof errorData !== 'object') return [];
|
|
19
21
|
if (errorData.missingPermissions && Array.isArray(errorData.missingPermissions)) {
|
|
20
22
|
return errorData.missingPermissions;
|
|
21
23
|
}
|
|
22
24
|
if (errorData.missing && errorData.missing.permissions && Array.isArray(errorData.missing.permissions)) {
|
|
23
25
|
return errorData.missing.permissions;
|
|
24
26
|
}
|
|
27
|
+
if (errorData.data?.missing?.permissions && Array.isArray(errorData.data.missing.permissions)) {
|
|
28
|
+
return errorData.data.missing.permissions;
|
|
29
|
+
}
|
|
25
30
|
return [];
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/**
|
|
29
34
|
* Extracts required permissions from error data
|
|
35
|
+
* Supports: requiredPermissions, required.permissions, permissions (array), data.required.permissions
|
|
30
36
|
* @param {Object} errorData - Error response data
|
|
31
37
|
* @returns {Array<string>} Array of required permissions
|
|
32
38
|
*/
|
|
33
39
|
function extractRequiredPermissions(errorData) {
|
|
40
|
+
if (!errorData || typeof errorData !== 'object') return [];
|
|
34
41
|
if (errorData.requiredPermissions && Array.isArray(errorData.requiredPermissions)) {
|
|
35
42
|
return errorData.requiredPermissions;
|
|
36
43
|
}
|
|
37
44
|
if (errorData.required && errorData.required.permissions && Array.isArray(errorData.required.permissions)) {
|
|
38
45
|
return errorData.required.permissions;
|
|
39
46
|
}
|
|
47
|
+
if (errorData.permissions && Array.isArray(errorData.permissions)) {
|
|
48
|
+
return errorData.permissions;
|
|
49
|
+
}
|
|
50
|
+
if (errorData.data?.required?.permissions && Array.isArray(errorData.data.required.permissions)) {
|
|
51
|
+
return errorData.data.required.permissions;
|
|
52
|
+
}
|
|
40
53
|
return [];
|
|
41
54
|
}
|
|
42
55
|
|
|
@@ -56,6 +69,33 @@ function addPermissionList(lines, perms, label) {
|
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Returns permission detail lines for appending to any error formatter (401, 403).
|
|
74
|
+
* Use when the API returns required/missing permissions in the error body.
|
|
75
|
+
* @param {Object} errorData - Error response data (may include required.permissions, missing.permissions, etc.)
|
|
76
|
+
* @returns {string[]} Array of formatted lines to append (may be empty)
|
|
77
|
+
*/
|
|
78
|
+
function getPermissionDetailLines(errorData) {
|
|
79
|
+
const lines = [];
|
|
80
|
+
const missingPerms = extractMissingPermissions(errorData);
|
|
81
|
+
const requiredPerms = extractRequiredPermissions(errorData);
|
|
82
|
+
if (missingPerms.length > 0) {
|
|
83
|
+
lines.push(chalk.yellow('Missing permissions:'));
|
|
84
|
+
missingPerms.forEach(perm => {
|
|
85
|
+
lines.push(chalk.gray(` - ${perm}`));
|
|
86
|
+
});
|
|
87
|
+
lines.push('');
|
|
88
|
+
}
|
|
89
|
+
if (requiredPerms.length > 0) {
|
|
90
|
+
lines.push(chalk.yellow('Required permissions:'));
|
|
91
|
+
requiredPerms.forEach(perm => {
|
|
92
|
+
lines.push(chalk.gray(` - ${perm}`));
|
|
93
|
+
});
|
|
94
|
+
lines.push('');
|
|
95
|
+
}
|
|
96
|
+
return lines;
|
|
97
|
+
}
|
|
98
|
+
|
|
59
99
|
/**
|
|
60
100
|
* Formats permission error with missing and required permissions
|
|
61
101
|
* @param {Object} errorData - Error response data
|
|
@@ -89,6 +129,9 @@ function formatPermissionError(errorData) {
|
|
|
89
129
|
}
|
|
90
130
|
|
|
91
131
|
module.exports = {
|
|
92
|
-
formatPermissionError
|
|
132
|
+
formatPermissionError,
|
|
133
|
+
getPermissionDetailLines,
|
|
134
|
+
extractMissingPermissions,
|
|
135
|
+
extractRequiredPermissions
|
|
93
136
|
};
|
|
94
137
|
|
|
@@ -21,24 +21,25 @@ const execAsync = promisify(exec);
|
|
|
21
21
|
* @async
|
|
22
22
|
* @param {string} serviceName - Service name
|
|
23
23
|
* @param {number|string} [devId] - Developer ID (optional, will be loaded from config if not provided)
|
|
24
|
+
* @param {Object} [options] - Options
|
|
25
|
+
* @param {boolean} [options.strict=false] - When true, only match current dev's container (no fallback to dev 0 / infra-*); use for status display
|
|
24
26
|
* @returns {Promise<string|null>} Container name or null if not found
|
|
25
27
|
*/
|
|
26
|
-
async function findContainer(serviceName, devId = null) {
|
|
28
|
+
async function findContainer(serviceName, devId = null, options = {}) {
|
|
27
29
|
try {
|
|
28
30
|
const developerId = devId || await config.getDeveloperId();
|
|
29
31
|
const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
|
|
30
32
|
// Dev 0: aifabrix-{serviceName}, Dev > 0: aifabrix-dev{id}-{serviceName}
|
|
31
33
|
const primaryPattern = idNum === 0 ? `aifabrix-${serviceName}` : `aifabrix-dev${developerId}-${serviceName}`;
|
|
32
34
|
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
];
|
|
35
|
+
// When strict (e.g. status command), only show this developer's infra; no fallback to dev 0
|
|
36
|
+
const patternsToTry = options.strict
|
|
37
|
+
? [primaryPattern]
|
|
38
|
+
: [
|
|
39
|
+
primaryPattern,
|
|
40
|
+
`infra-${serviceName}`,
|
|
41
|
+
`aifabrix-${serviceName}`
|
|
42
|
+
];
|
|
42
43
|
|
|
43
44
|
for (const pattern of patternsToTry) {
|
|
44
45
|
const { stdout } = await execAsync(`docker ps --filter "name=${pattern}" --format "{{.Names}}"`);
|
|
@@ -64,12 +65,13 @@ async function findContainer(serviceName, devId = null) {
|
|
|
64
65
|
* @private
|
|
65
66
|
* @async
|
|
66
67
|
* @param {string} serviceName - Service name
|
|
67
|
-
* @param {number} [devId] - Developer ID (optional, will be loaded from config if not provided)
|
|
68
|
+
* @param {number|string} [devId] - Developer ID (optional, will be loaded from config if not provided)
|
|
69
|
+
* @param {Object} [options] - Options (e.g. { strict: true } for current dev only)
|
|
68
70
|
* @returns {Promise<string>} Health status
|
|
69
71
|
*/
|
|
70
|
-
async function checkServiceWithHealthCheck(serviceName, devId = null) {
|
|
72
|
+
async function checkServiceWithHealthCheck(serviceName, devId = null, options = {}) {
|
|
71
73
|
try {
|
|
72
|
-
const containerName = await findContainer(serviceName, devId);
|
|
74
|
+
const containerName = await findContainer(serviceName, devId, options);
|
|
73
75
|
if (!containerName) {
|
|
74
76
|
return 'unknown';
|
|
75
77
|
}
|
|
@@ -87,12 +89,13 @@ async function checkServiceWithHealthCheck(serviceName, devId = null) {
|
|
|
87
89
|
* @private
|
|
88
90
|
* @async
|
|
89
91
|
* @param {string} serviceName - Service name
|
|
90
|
-
* @param {number} [devId] - Developer ID (optional, will be loaded from config if not provided)
|
|
92
|
+
* @param {number|string} [devId] - Developer ID (optional, will be loaded from config if not provided)
|
|
93
|
+
* @param {Object} [options] - Options (e.g. { strict: true } for current dev only)
|
|
91
94
|
* @returns {Promise<string>} Health status
|
|
92
95
|
*/
|
|
93
|
-
async function checkServiceWithoutHealthCheck(serviceName, devId = null) {
|
|
96
|
+
async function checkServiceWithoutHealthCheck(serviceName, devId = null, options = {}) {
|
|
94
97
|
try {
|
|
95
|
-
const containerName = await findContainer(serviceName, devId);
|
|
98
|
+
const containerName = await findContainer(serviceName, devId, options);
|
|
96
99
|
if (!containerName) {
|
|
97
100
|
return 'unknown';
|
|
98
101
|
}
|
|
@@ -40,7 +40,7 @@ async function getInfraStatus() {
|
|
|
40
40
|
pgadmin: { port: ports.pgadmin, url: `http://localhost:${ports.pgadmin}` },
|
|
41
41
|
'redis-commander': { port: ports.redisCommander, url: `http://localhost:${ports.redisCommander}` },
|
|
42
42
|
traefik: {
|
|
43
|
-
port: `${ports.traefikHttp}
|
|
43
|
+
port: `${ports.traefikHttp}, ${ports.traefikHttps}`,
|
|
44
44
|
url: `http://localhost:${ports.traefikHttp}, https://localhost:${ports.traefikHttps}`
|
|
45
45
|
}
|
|
46
46
|
};
|
|
@@ -49,7 +49,8 @@ async function getInfraStatus() {
|
|
|
49
49
|
|
|
50
50
|
for (const [serviceName, serviceConfig] of Object.entries(services)) {
|
|
51
51
|
try {
|
|
52
|
-
|
|
52
|
+
// Strict: only this developer's infra (no fallback to dev 0), so status reflects reality
|
|
53
|
+
const containerName = await containerUtils.findContainer(serviceName, devId, { strict: true });
|
|
53
54
|
if (containerName) {
|
|
54
55
|
const { stdout } = await execAsync(`docker inspect --format='{{.State.Status}}' ${containerName}`);
|
|
55
56
|
// Normalize status value (trim whitespace and remove quotes)
|
|
@@ -97,6 +98,9 @@ function getInfraContainerNames(devIdNum, devId) {
|
|
|
97
98
|
];
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
/** Suffixes for init/helper containers to exclude from "Running Applications" (e.g. keycloak-db-init) */
|
|
102
|
+
const INIT_CONTAINER_SUFFIXES = ['-db-init', '-init'];
|
|
103
|
+
|
|
100
104
|
/**
|
|
101
105
|
* Extracts app name from container name
|
|
102
106
|
* @param {string} containerName - Container name
|
|
@@ -107,7 +111,12 @@ function getInfraContainerNames(devIdNum, devId) {
|
|
|
107
111
|
function extractAppName(containerName, devIdNum, devId) {
|
|
108
112
|
const pattern = devIdNum === 0 ? /^aifabrix-(.+)$/ : new RegExp(`^aifabrix-dev${devId}-(.+)$`);
|
|
109
113
|
const match = containerName.match(pattern);
|
|
110
|
-
|
|
114
|
+
if (!match) return null;
|
|
115
|
+
const appName = match[1];
|
|
116
|
+
if (INIT_CONTAINER_SUFFIXES.some(suffix => appName.endsWith(suffix))) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return appName;
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
/**
|
|
@@ -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",
|