@aifabrix/builder 2.33.4 → 2.33.6
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/lib/app/run-helpers.js +7 -2
- package/lib/app/run.js +2 -2
- package/lib/cli.js +14 -6
- package/lib/commands/auth-status.js +58 -2
- package/lib/commands/up-miso.js +25 -16
- package/lib/commands/wizard-core.js +48 -16
- package/lib/commands/wizard.js +9 -3
- package/lib/datasource/list.js +6 -3
- package/lib/infrastructure/services.js +6 -3
- package/lib/utils/app-register-auth.js +9 -2
- package/lib/utils/env-config-loader.js +16 -5
- package/lib/utils/env-map.js +11 -9
- 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/package.json +1 -1
package/lib/app/run-helpers.js
CHANGED
|
@@ -139,14 +139,15 @@ async function validateAppConfiguration(appName) {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/**
|
|
142
|
-
* Checks prerequisites: Docker image and infrastructure
|
|
142
|
+
* Checks prerequisites: Docker image and (optionally) infrastructure
|
|
143
143
|
* @async
|
|
144
144
|
* @param {string} appName - Application name
|
|
145
145
|
* @param {Object} appConfig - Application configuration
|
|
146
146
|
* @param {boolean} [debug=false] - Enable debug logging
|
|
147
|
+
* @param {boolean} [skipInfraCheck=false] - When true, skip infra health check (e.g. when caller already verified, e.g. up-miso)
|
|
147
148
|
* @throws {Error} If prerequisites are not met
|
|
148
149
|
*/
|
|
149
|
-
async function checkPrerequisites(appName, appConfig, debug = false) {
|
|
150
|
+
async function checkPrerequisites(appName, appConfig, debug = false, skipInfraCheck = false) {
|
|
150
151
|
const imageName = composeGenerator.getImageName(appConfig, appName);
|
|
151
152
|
const imageTag = appConfig.image?.tag || 'latest';
|
|
152
153
|
const fullImageName = `${imageName}:${imageTag}`;
|
|
@@ -162,6 +163,10 @@ async function checkPrerequisites(appName, appConfig, debug = false) {
|
|
|
162
163
|
}
|
|
163
164
|
logger.log(chalk.green(`✓ Image ${fullImageName} found`));
|
|
164
165
|
|
|
166
|
+
if (skipInfraCheck) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
165
170
|
logger.log(chalk.blue('Checking infrastructure health...'));
|
|
166
171
|
const infraHealth = await infra.checkInfraHealth();
|
|
167
172
|
if (debug) {
|
package/lib/app/run.js
CHANGED
|
@@ -177,8 +177,8 @@ async function runApp(appName, options = {}) {
|
|
|
177
177
|
// Load and configure application
|
|
178
178
|
const appConfig = await loadAndConfigureApp(appName, debug);
|
|
179
179
|
|
|
180
|
-
// Check prerequisites: image and infrastructure
|
|
181
|
-
await helpers.checkPrerequisites(appName, appConfig, debug);
|
|
180
|
+
// Check prerequisites: image and (unless skipped) infrastructure
|
|
181
|
+
await helpers.checkPrerequisites(appName, appConfig, debug, options.skipInfraCheck === true);
|
|
182
182
|
|
|
183
183
|
// Check if container is already running and stop it if needed
|
|
184
184
|
await checkAndStopContainer(appName, appConfig.developerId, debug);
|
package/lib/cli.js
CHANGED
|
@@ -155,10 +155,10 @@ function setupInfraCommands(program) {
|
|
|
155
155
|
});
|
|
156
156
|
|
|
157
157
|
program.command('up-miso')
|
|
158
|
-
.description('Install keycloak
|
|
159
|
-
.option('-r, --registry <url>', 'Override registry for
|
|
158
|
+
.description('Install keycloak, miso-controller, and dataplane from images (no build). Infra must be up. Uses auto-generated secrets for testing.')
|
|
159
|
+
.option('-r, --registry <url>', 'Override registry for all apps (e.g. myacr.azurecr.io)')
|
|
160
160
|
.option('--registry-mode <mode>', 'Override registry mode (acr|external)')
|
|
161
|
-
.option('-i, --image <key>=<value>', 'Override image (e.g. keycloak=myreg/
|
|
161
|
+
.option('-i, --image <key>=<value>', 'Override image (e.g. keycloak=myreg/k:v1, miso-controller=myreg/m:v1, dataplane=myreg/d:v1); can be repeated', (v, prev) => (prev || []).concat([v]))
|
|
162
162
|
.action(async(options) => {
|
|
163
163
|
try {
|
|
164
164
|
await handleUpMiso(options);
|
|
@@ -410,9 +410,17 @@ function setupAppCommands(program) {
|
|
|
410
410
|
});
|
|
411
411
|
|
|
412
412
|
program.command('wizard')
|
|
413
|
-
.description('
|
|
414
|
-
.option('-a, --app <app>', 'Application name
|
|
415
|
-
.option('--config <file>', '
|
|
413
|
+
.description('Create or extend external systems (OpenAPI, MCP, or known platforms like HubSpot) via guided steps or a config file')
|
|
414
|
+
.option('-a, --app <app>', 'Application name; skips the prompt in interactive mode')
|
|
415
|
+
.option('--config <file>', 'Run headless using a wizard.yaml file (appName, mode, source, credential, preferences)')
|
|
416
|
+
.addHelpText('after', `
|
|
417
|
+
Examples:
|
|
418
|
+
$ aifabrix wizard Run interactively (prompts for app name and steps)
|
|
419
|
+
$ aifabrix wizard -a my-integration Run interactively with app name set
|
|
420
|
+
$ aifabrix wizard --config wizard.yaml Run headless from a wizard config file
|
|
421
|
+
|
|
422
|
+
Headless config (wizard.yaml) must include: appName, mode (create-system|add-datasource), source (type + filePath/url/platform).
|
|
423
|
+
See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`)
|
|
416
424
|
.action(async(options) => {
|
|
417
425
|
try {
|
|
418
426
|
const { handleWizard } = require('./commands/wizard');
|
|
@@ -15,6 +15,9 @@ const { getConfig } = config;
|
|
|
15
15
|
const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
16
16
|
const { getAuthUser } = require('../api/auth.api');
|
|
17
17
|
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
18
|
+
const { findDataplaneServiceAppKey } = require('./wizard-dataplane');
|
|
19
|
+
const { getDataplaneUrl } = require('../datasource/deploy');
|
|
20
|
+
const { checkDataplaneHealth } = require('../utils/dataplane-health');
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Format expiration date for display
|
|
@@ -212,13 +215,53 @@ function displayTokenInfo(tokenInfo) {
|
|
|
212
215
|
displayUserInfo(tokenInfo.user);
|
|
213
216
|
}
|
|
214
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Resolve dataplane URL from controller without progress logs (for auth status display)
|
|
220
|
+
* @async
|
|
221
|
+
* @param {string} controllerUrl - Controller URL
|
|
222
|
+
* @param {string} environment - Environment key
|
|
223
|
+
* @param {Object} authConfig - Authentication configuration
|
|
224
|
+
* @returns {Promise<string|null>} Dataplane URL or null if not discoverable
|
|
225
|
+
*/
|
|
226
|
+
async function resolveDataplaneUrlSilent(controllerUrl, environment, authConfig) {
|
|
227
|
+
try {
|
|
228
|
+
const dataplaneAppKey = await findDataplaneServiceAppKey(controllerUrl, environment, authConfig);
|
|
229
|
+
if (dataplaneAppKey) {
|
|
230
|
+
return await getDataplaneUrl(controllerUrl, dataplaneAppKey, environment, authConfig);
|
|
231
|
+
}
|
|
232
|
+
return await getDataplaneUrl(controllerUrl, 'dataplane', environment, authConfig);
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Display dataplane section when authenticated
|
|
240
|
+
* @param {string|null} dataplaneUrl - Dataplane URL or null
|
|
241
|
+
* @param {boolean} dataplaneConnected - Whether dataplane health check passed
|
|
242
|
+
*/
|
|
243
|
+
function displayDataplaneSection(dataplaneUrl, dataplaneConnected) {
|
|
244
|
+
if (dataplaneUrl) {
|
|
245
|
+
logger.log(`Dataplane: ${chalk.cyan(dataplaneUrl)}`);
|
|
246
|
+
const statusIcon = dataplaneConnected ? chalk.green('✓') : chalk.red('✗');
|
|
247
|
+
const statusText = dataplaneConnected ? 'Connected' : 'Not reachable';
|
|
248
|
+
logger.log(`Status: ${statusIcon} ${statusText}`);
|
|
249
|
+
} else {
|
|
250
|
+
logger.log(`Dataplane: ${chalk.gray('—')}`);
|
|
251
|
+
logger.log(`Status: ${chalk.gray('Not discovered')}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
215
255
|
/**
|
|
216
256
|
* Display authentication status
|
|
217
257
|
* @param {string} controllerUrl - Controller URL
|
|
218
258
|
* @param {string} environment - Environment key
|
|
219
259
|
* @param {Object|null} tokenInfo - Token information
|
|
260
|
+
* @param {Object} [dataplaneInfo] - Optional dataplane URL and health
|
|
261
|
+
* @param {string|null} [dataplaneInfo.url] - Dataplane URL
|
|
262
|
+
* @param {boolean} [dataplaneInfo.connected] - Whether dataplane is reachable
|
|
220
263
|
*/
|
|
221
|
-
function displayStatus(controllerUrl, environment, tokenInfo) {
|
|
264
|
+
function displayStatus(controllerUrl, environment, tokenInfo, dataplaneInfo) {
|
|
222
265
|
logger.log(chalk.bold('\n🔐 Authentication Status\n'));
|
|
223
266
|
logger.log(`Controller: ${chalk.cyan(controllerUrl)}`);
|
|
224
267
|
logger.log(`Environment: ${chalk.cyan(environment || 'Not specified')}\n`);
|
|
@@ -231,6 +274,11 @@ function displayStatus(controllerUrl, environment, tokenInfo) {
|
|
|
231
274
|
}
|
|
232
275
|
|
|
233
276
|
displayTokenInfo(tokenInfo);
|
|
277
|
+
|
|
278
|
+
if (tokenInfo.authenticated && dataplaneInfo) {
|
|
279
|
+
displayDataplaneSection(dataplaneInfo.url, dataplaneInfo.connected);
|
|
280
|
+
}
|
|
281
|
+
|
|
234
282
|
logger.log('');
|
|
235
283
|
}
|
|
236
284
|
|
|
@@ -255,7 +303,15 @@ async function handleAuthStatus(_options) {
|
|
|
255
303
|
tokenInfo = await checkClientToken(controllerUrl, environment);
|
|
256
304
|
}
|
|
257
305
|
|
|
258
|
-
|
|
306
|
+
let dataplaneInfo = null;
|
|
307
|
+
if (tokenInfo && tokenInfo.authenticated && tokenInfo.token) {
|
|
308
|
+
const authConfig = { type: 'bearer', token: tokenInfo.token };
|
|
309
|
+
const dataplaneUrl = await resolveDataplaneUrlSilent(controllerUrl, environment, authConfig);
|
|
310
|
+
const connected = dataplaneUrl ? await checkDataplaneHealth(dataplaneUrl, 5000) : false;
|
|
311
|
+
dataplaneInfo = { url: dataplaneUrl, connected };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
displayStatus(controllerUrl, environment, tokenInfo, dataplaneInfo);
|
|
259
315
|
}
|
|
260
316
|
|
|
261
317
|
module.exports = { handleAuthStatus };
|
package/lib/commands/up-miso.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AI Fabrix Builder - Up Miso Command
|
|
3
3
|
*
|
|
4
|
-
* Installs miso-controller and
|
|
4
|
+
* Installs keycloak, miso-controller, and dataplane from images (no build).
|
|
5
5
|
* Assumes infra is up; sets dev secrets and resolves (no force; existing .env values preserved).
|
|
6
6
|
*
|
|
7
7
|
* @fileoverview up-miso command implementation
|
|
@@ -25,11 +25,13 @@ const { ensureAppFromTemplate } = require('./up-common');
|
|
|
25
25
|
const KEYCLOAK_BASE_PORT = 8082;
|
|
26
26
|
/** Miso controller base port (dev-config app base) */
|
|
27
27
|
const MISO_BASE_PORT = 3000;
|
|
28
|
+
/** Dataplane base port (from templates/applications/dataplane/variables.yaml) */
|
|
29
|
+
const _DATAPLANE_BASE_PORT = 3001;
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
|
-
* Parse --image options array into map { keycloak?: string, 'miso-controller'?: string }
|
|
31
|
-
* @param {string[]|string} imageOpts - Option value(s) e.g. ['keycloak=reg/k:v1', 'miso-controller=reg/m:v1']
|
|
32
|
-
* @returns {{ keycloak?: string, 'miso-controller'?: string }}
|
|
32
|
+
* Parse --image options array into map { keycloak?: string, 'miso-controller'?: string, dataplane?: string }
|
|
33
|
+
* @param {string[]|string} imageOpts - Option value(s) e.g. ['keycloak=reg/k:v1', 'miso-controller=reg/m:v1', 'dataplane=reg/d:v1']
|
|
34
|
+
* @returns {{ keycloak?: string, 'miso-controller'?: string, dataplane?: string }}
|
|
33
35
|
*/
|
|
34
36
|
function parseImageOptions(imageOpts) {
|
|
35
37
|
const map = {};
|
|
@@ -48,7 +50,7 @@ function parseImageOptions(imageOpts) {
|
|
|
48
50
|
|
|
49
51
|
/**
|
|
50
52
|
* Build full image ref from registry and app variables (registry/name:tag)
|
|
51
|
-
* @param {string} appName - keycloak
|
|
53
|
+
* @param {string} appName - keycloak, miso-controller, or dataplane
|
|
52
54
|
* @param {string} registry - Registry URL
|
|
53
55
|
* @returns {string} Full image reference
|
|
54
56
|
*/
|
|
@@ -65,7 +67,7 @@ function buildImageRefFromRegistry(appName, registry) {
|
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/**
|
|
68
|
-
* Set URL secrets and resolve keycloak + miso-controller (no force; existing .env preserved)
|
|
70
|
+
* Set URL secrets and resolve keycloak + miso-controller + dataplane (no force; existing .env preserved)
|
|
69
71
|
* @async
|
|
70
72
|
* @param {number} devIdNum - Developer ID number
|
|
71
73
|
*/
|
|
@@ -77,11 +79,12 @@ async function setMisoSecretsAndResolve(devIdNum) {
|
|
|
77
79
|
logger.log(chalk.green('✓ Set keycloak and miso-controller URL secrets'));
|
|
78
80
|
await secrets.generateEnvFile('keycloak', undefined, 'docker', false, true);
|
|
79
81
|
await secrets.generateEnvFile('miso-controller', undefined, 'docker', false, true);
|
|
80
|
-
|
|
82
|
+
await secrets.generateEnvFile('dataplane', undefined, 'docker', false, true);
|
|
83
|
+
logger.log(chalk.green('✓ Resolved keycloak, miso-controller, and dataplane'));
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
/**
|
|
84
|
-
* Build run options and run keycloak
|
|
87
|
+
* Build run options and run keycloak, miso-controller, then dataplane
|
|
85
88
|
* @async
|
|
86
89
|
* @param {Object} options - Commander options (image, registry, registryMode)
|
|
87
90
|
*/
|
|
@@ -89,23 +92,27 @@ async function runMisoApps(options) {
|
|
|
89
92
|
const imageMap = parseImageOptions(options.image);
|
|
90
93
|
const keycloakImage = imageMap.keycloak || (options.registry ? buildImageRefFromRegistry('keycloak', options.registry) : undefined);
|
|
91
94
|
const misoImage = imageMap['miso-controller'] || (options.registry ? buildImageRefFromRegistry('miso-controller', options.registry) : undefined);
|
|
92
|
-
const
|
|
93
|
-
const
|
|
95
|
+
const dataplaneImage = imageMap.dataplane || (options.registry ? buildImageRefFromRegistry('dataplane', options.registry) : undefined);
|
|
96
|
+
const keycloakRunOpts = { image: keycloakImage, registry: options.registry, registryMode: options.registryMode, skipEnvOutputPath: true, skipInfraCheck: true };
|
|
97
|
+
const misoRunOpts = { image: misoImage, registry: options.registry, registryMode: options.registryMode, skipEnvOutputPath: true, skipInfraCheck: true };
|
|
98
|
+
const dataplaneRunOpts = { image: dataplaneImage, registry: options.registry, registryMode: options.registryMode, skipEnvOutputPath: true, skipInfraCheck: true };
|
|
94
99
|
logger.log(chalk.blue('Starting keycloak...'));
|
|
95
100
|
await app.runApp('keycloak', keycloakRunOpts);
|
|
96
101
|
logger.log(chalk.blue('Starting miso-controller...'));
|
|
97
102
|
await app.runApp('miso-controller', misoRunOpts);
|
|
103
|
+
logger.log(chalk.blue('Starting dataplane...'));
|
|
104
|
+
await app.runApp('dataplane', dataplaneRunOpts);
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
/**
|
|
101
|
-
* Handle up-miso command: ensure infra, ensure app dirs, set secrets, resolve (preserve existing .env), run keycloak
|
|
108
|
+
* Handle up-miso command: ensure infra, ensure app dirs, set secrets, resolve (preserve existing .env), run keycloak, miso-controller, then dataplane.
|
|
102
109
|
*
|
|
103
110
|
* @async
|
|
104
111
|
* @function handleUpMiso
|
|
105
112
|
* @param {Object} options - Commander options
|
|
106
|
-
* @param {string} [options.registry] - Override registry for
|
|
113
|
+
* @param {string} [options.registry] - Override registry for all apps
|
|
107
114
|
* @param {string} [options.registryMode] - Override registry mode (acr|external)
|
|
108
|
-
* @param {string[]|string} [options.image] - Override images e.g. keycloak=reg/k:v1
|
|
115
|
+
* @param {string[]|string} [options.image] - Override images e.g. keycloak=reg/k:v1, miso-controller=reg/m:v1, dataplane=reg/d:v1
|
|
109
116
|
* @returns {Promise<void>}
|
|
110
117
|
* @throws {Error} If infra not up or any step fails
|
|
111
118
|
*/
|
|
@@ -114,8 +121,9 @@ async function handleUpMiso(options = {}) {
|
|
|
114
121
|
if (builderDir) {
|
|
115
122
|
process.env.AIFABRIX_BUILDER_DIR = builderDir;
|
|
116
123
|
}
|
|
117
|
-
logger.log(chalk.blue('Starting up-miso (keycloak + miso-controller from images)...\n'));
|
|
118
|
-
|
|
124
|
+
logger.log(chalk.blue('Starting up-miso (keycloak + miso-controller + dataplane from images)...\n'));
|
|
125
|
+
// Strict: only this developer's infra (same as status), so up-miso and status agree
|
|
126
|
+
const health = await infra.checkInfraHealth(undefined, { strict: true });
|
|
119
127
|
const allHealthy = Object.values(health).every(status => status === 'healthy');
|
|
120
128
|
if (!allHealthy) {
|
|
121
129
|
throw new Error('Infrastructure is not up. Run \'aifabrix up\' first.');
|
|
@@ -123,11 +131,12 @@ async function handleUpMiso(options = {}) {
|
|
|
123
131
|
logger.log(chalk.green('✓ Infrastructure is up'));
|
|
124
132
|
await ensureAppFromTemplate('keycloak');
|
|
125
133
|
await ensureAppFromTemplate('miso-controller');
|
|
134
|
+
await ensureAppFromTemplate('dataplane');
|
|
126
135
|
const developerId = await config.getDeveloperId();
|
|
127
136
|
const devIdNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
|
|
128
137
|
await setMisoSecretsAndResolve(devIdNum);
|
|
129
138
|
await runMisoApps(options);
|
|
130
|
-
logger.log(chalk.green('\n✓ up-miso complete. Keycloak
|
|
139
|
+
logger.log(chalk.green('\n✓ up-miso complete. Keycloak, miso-controller, and dataplane are running.'));
|
|
131
140
|
logger.log(chalk.gray(' Run onboarding and register Keycloak from the miso-controller repo if needed.'));
|
|
132
141
|
}
|
|
133
142
|
|
|
@@ -9,7 +9,7 @@ const ora = require('ora');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const fs = require('fs').promises;
|
|
11
11
|
const logger = require('../utils/logger');
|
|
12
|
-
const { getDeploymentAuth
|
|
12
|
+
const { getDeploymentAuth } = require('../utils/token-manager');
|
|
13
13
|
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
14
14
|
const { normalizeWizardConfigs } = require('./wizard-config-normalizer');
|
|
15
15
|
const {
|
|
@@ -75,6 +75,32 @@ function extractSessionId(responseData) {
|
|
|
75
75
|
return sessionId;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Full error message when dataplane returns 401 (controller accepts token, dataplane rejects it).
|
|
80
|
+
* Avoids misleading "token invalid or expired" / "run login" text from the API formatter.
|
|
81
|
+
* @param {string} [dataplaneUrl] - Dataplane URL (for context)
|
|
82
|
+
* @param {string} [appName] - Application name for credential hint
|
|
83
|
+
* @param {string} [apiMessage] - Raw message from API (e.g. "Invalid token or insufficient permissions")
|
|
84
|
+
* @returns {string} Full message explaining the actual problem
|
|
85
|
+
*/
|
|
86
|
+
function formatDataplaneRejectedTokenMessage(dataplaneUrl = '', appName = null, apiMessage = '') {
|
|
87
|
+
const app = appName || '<app>';
|
|
88
|
+
const where = dataplaneUrl ? ` the dataplane at ${dataplaneUrl}` : ' the dataplane';
|
|
89
|
+
const apiLine = apiMessage ? `\n\nResponse: ${apiMessage}` : '';
|
|
90
|
+
return (
|
|
91
|
+
'Failed to create wizard session.' +
|
|
92
|
+
apiLine +
|
|
93
|
+
`\n\nYour token is valid for the controller (aifabrix auth status shows you as authenticated), but${where} rejected the request.` +
|
|
94
|
+
' This usually means:\n' +
|
|
95
|
+
' • The dataplane is configured to accept only client credentials, not device tokens, or\n' +
|
|
96
|
+
' • There is a permission or configuration issue on the dataplane side.\n\n' +
|
|
97
|
+
'What you can do:\n' +
|
|
98
|
+
` • Add client credentials to ~/.aifabrix/secrets.local.yaml as "${app}-client-idKeyVault" and "${app}-client-secretKeyVault" if the dataplane accepts them.\n` +
|
|
99
|
+
' • Contact your administrator to have the dataplane accept your token or to get the required client credentials.\n' +
|
|
100
|
+
' • Run "aifabrix doctor" for environment diagnostics.'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
78
104
|
/**
|
|
79
105
|
* Handle mode selection step - create wizard session
|
|
80
106
|
* @async
|
|
@@ -92,7 +118,11 @@ async function handleModeSelection(dataplaneUrl, authConfig, configMode = null,
|
|
|
92
118
|
if (!sessionResponse.success || !sessionResponse.data) {
|
|
93
119
|
const errorMsg = sessionResponse.formattedError || sessionResponse.error ||
|
|
94
120
|
sessionResponse.errorData?.detail || 'Unknown error';
|
|
95
|
-
|
|
121
|
+
const apiMessage = sessionResponse.errorData?.message || sessionResponse.errorData?.detail || sessionResponse.error || '';
|
|
122
|
+
const fullMsg = sessionResponse.status === 401
|
|
123
|
+
? formatDataplaneRejectedTokenMessage(dataplaneUrl, null, apiMessage)
|
|
124
|
+
: `Failed to create wizard session: ${errorMsg}`;
|
|
125
|
+
throw new Error(fullMsg);
|
|
96
126
|
}
|
|
97
127
|
const sessionId = extractSessionId(sessionResponse.data);
|
|
98
128
|
logger.log(chalk.green(`\u2713 Session created: ${sessionId}`));
|
|
@@ -478,20 +508,13 @@ async function setupDataplaneAndAuth(options, appName) {
|
|
|
478
508
|
const { resolveEnvironment } = require('../core/config');
|
|
479
509
|
const environment = await resolveEnvironment();
|
|
480
510
|
const controllerUrl = await resolveControllerUrl();
|
|
511
|
+
// Prefer device token; use client token or client credentials when available.
|
|
512
|
+
// Some dataplanes accept only client credentials; getDeploymentAuth tries all.
|
|
481
513
|
let authConfig;
|
|
482
514
|
try {
|
|
483
|
-
|
|
484
|
-
// since the app doesn't exist yet. Device token is sufficient for
|
|
485
|
-
// discovering the dataplane URL and running the wizard.
|
|
486
|
-
authConfig = await getDeviceOnlyAuth(controllerUrl);
|
|
515
|
+
authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
|
|
487
516
|
} catch (error) {
|
|
488
|
-
|
|
489
|
-
// (e.g., for add-datasource mode where app might exist)
|
|
490
|
-
try {
|
|
491
|
-
authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
|
|
492
|
-
} catch (fallbackError) {
|
|
493
|
-
throw new Error(`Authentication failed: ${error.message}`);
|
|
494
|
-
}
|
|
517
|
+
throw new Error(`Authentication failed: ${error.message}`);
|
|
495
518
|
}
|
|
496
519
|
const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
|
|
497
520
|
let dataplaneUrl;
|
|
@@ -509,7 +532,16 @@ async function setupDataplaneAndAuth(options, appName) {
|
|
|
509
532
|
}
|
|
510
533
|
|
|
511
534
|
module.exports = {
|
|
512
|
-
validateAndCheckAppDirectory,
|
|
513
|
-
|
|
514
|
-
|
|
535
|
+
validateAndCheckAppDirectory,
|
|
536
|
+
extractSessionId,
|
|
537
|
+
formatDataplaneRejectedTokenMessage,
|
|
538
|
+
handleModeSelection,
|
|
539
|
+
handleSourceSelection,
|
|
540
|
+
handleOpenApiParsing,
|
|
541
|
+
handleCredentialSelection,
|
|
542
|
+
handleTypeDetection,
|
|
543
|
+
handleConfigurationGeneration,
|
|
544
|
+
validateWizardConfiguration,
|
|
545
|
+
handleFileSaving,
|
|
546
|
+
setupDataplaneAndAuth
|
|
515
547
|
};
|
package/lib/commands/wizard.js
CHANGED
|
@@ -21,6 +21,7 @@ const {
|
|
|
21
21
|
} = require('../generator/wizard-prompts');
|
|
22
22
|
const {
|
|
23
23
|
validateAndCheckAppDirectory,
|
|
24
|
+
formatDataplaneRejectedTokenMessage,
|
|
24
25
|
handleOpenApiParsing,
|
|
25
26
|
handleCredentialSelection,
|
|
26
27
|
handleTypeDetection,
|
|
@@ -57,9 +58,10 @@ function extractSessionId(responseData) {
|
|
|
57
58
|
* @function handleInteractiveModeSelection
|
|
58
59
|
* @param {string} dataplaneUrl - Dataplane URL
|
|
59
60
|
* @param {Object} authConfig - Authentication configuration
|
|
61
|
+
* @param {string} [appName] - Application name (for 401 hint)
|
|
60
62
|
* @returns {Promise<Object>} Object with mode and sessionId
|
|
61
63
|
*/
|
|
62
|
-
async function handleInteractiveModeSelection(dataplaneUrl, authConfig) {
|
|
64
|
+
async function handleInteractiveModeSelection(dataplaneUrl, authConfig, appName) {
|
|
63
65
|
logger.log(chalk.blue('\n\uD83D\uDCCB Step 1: Mode Selection'));
|
|
64
66
|
const mode = await promptForMode();
|
|
65
67
|
let systemIdOrKey = null;
|
|
@@ -71,7 +73,11 @@ async function handleInteractiveModeSelection(dataplaneUrl, authConfig) {
|
|
|
71
73
|
const errorMsg = sessionResponse.formattedError || sessionResponse.error ||
|
|
72
74
|
sessionResponse.errorData?.detail || sessionResponse.message ||
|
|
73
75
|
(sessionResponse.status ? `HTTP ${sessionResponse.status}` : 'Unknown error');
|
|
74
|
-
|
|
76
|
+
const apiMessage = sessionResponse.errorData?.message || sessionResponse.errorData?.detail || sessionResponse.error || '';
|
|
77
|
+
const fullMsg = sessionResponse.status === 401
|
|
78
|
+
? formatDataplaneRejectedTokenMessage(dataplaneUrl, appName, apiMessage)
|
|
79
|
+
: `Failed to create wizard session: ${errorMsg}`;
|
|
80
|
+
throw new Error(fullMsg);
|
|
75
81
|
}
|
|
76
82
|
const sessionId = extractSessionId(sessionResponse.data);
|
|
77
83
|
return { mode, sessionId, systemIdOrKey };
|
|
@@ -187,7 +193,7 @@ async function handleConfigurationReview(dataplaneUrl, authConfig, systemConfig,
|
|
|
187
193
|
*/
|
|
188
194
|
async function executeWizardFlow(appName, dataplaneUrl, authConfig) {
|
|
189
195
|
// Step 1: Mode Selection
|
|
190
|
-
const { mode, sessionId, systemIdOrKey } = await handleInteractiveModeSelection(dataplaneUrl, authConfig);
|
|
196
|
+
const { mode, sessionId, systemIdOrKey } = await handleInteractiveModeSelection(dataplaneUrl, authConfig, appName);
|
|
191
197
|
|
|
192
198
|
// Step 2: Source Selection
|
|
193
199
|
const { sourceType, sourceData } = await handleInteractiveSourceSelection(dataplaneUrl, sessionId, authConfig);
|
package/lib/datasource/list.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Datasource List Command
|
|
3
3
|
*
|
|
4
|
-
* Lists datasources from
|
|
5
|
-
*
|
|
4
|
+
* Lists datasources from the dataplane (GET /api/v1/external/).
|
|
5
|
+
* Resolves dataplane URL from the controller, then calls the dataplane list API.
|
|
6
6
|
*
|
|
7
7
|
* @fileoverview Datasource listing for AI Fabrix Builder
|
|
8
8
|
* @author AI Fabrix Team
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
const chalk = require('chalk');
|
|
13
13
|
const { getConfig, resolveEnvironment } = require('../core/config');
|
|
14
14
|
const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
15
|
-
const { listDatasources: listDatasourcesFromDataplane } = require('../api/datasources
|
|
15
|
+
const { listDatasources: listDatasourcesFromDataplane } = require('../api/datasources.api');
|
|
16
16
|
const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
|
|
17
17
|
const { formatApiError } = require('../utils/api-error-handler');
|
|
18
18
|
const logger = require('../utils/logger');
|
|
@@ -291,8 +291,11 @@ async function listDatasources(_options) {
|
|
|
291
291
|
|
|
292
292
|
const controllerUrl = validateControllerUrl(authInfo.controllerUrl);
|
|
293
293
|
const authConfig = setupAuthConfig(authInfo.token, controllerUrl);
|
|
294
|
+
|
|
295
|
+
// Resolve dataplane URL first (required for list call)
|
|
294
296
|
const dataplaneUrl = await resolveAndValidateDataplaneUrl(controllerUrl, environment, authConfig);
|
|
295
297
|
|
|
298
|
+
// List datasources from dataplane (GET /api/v1/external/)
|
|
296
299
|
const response = await listDatasourcesFromDataplane(dataplaneUrl, authConfig);
|
|
297
300
|
|
|
298
301
|
if (!response.success || !response.data) {
|
|
@@ -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
|
}
|
|
@@ -75,6 +75,19 @@ function mergeEnvConfigs(baseConfig, userConfig) {
|
|
|
75
75
|
return merged;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Load schema-only env-config (no user merge). Used for *_PUBLIC_PORT calculation
|
|
80
|
+
* so public ports always use canonical base (e.g. KEYCLOAK_PUBLIC_PORT = 8082 + devId*100).
|
|
81
|
+
*
|
|
82
|
+
* @function loadSchemaEnvConfig
|
|
83
|
+
* @returns {Object} Parsed schema env-config (environments.docker / .local only)
|
|
84
|
+
*/
|
|
85
|
+
function loadSchemaEnvConfig() {
|
|
86
|
+
const envConfigPath = path.join(__dirname, '..', 'schema', 'env-config.yaml');
|
|
87
|
+
const content = fs.readFileSync(envConfigPath, 'utf8');
|
|
88
|
+
return yaml.load(content) || {};
|
|
89
|
+
}
|
|
90
|
+
|
|
78
91
|
/**
|
|
79
92
|
* Load env config YAML used for environment variable interpolation
|
|
80
93
|
* Loads base config from lib/schema/env-config.yaml and merges with user config if configured
|
|
@@ -84,10 +97,7 @@ function mergeEnvConfigs(baseConfig, userConfig) {
|
|
|
84
97
|
* @throws {Error} If base file cannot be read or parsed
|
|
85
98
|
*/
|
|
86
99
|
async function loadEnvConfig() {
|
|
87
|
-
|
|
88
|
-
const envConfigPath = path.join(__dirname, '..', 'schema', 'env-config.yaml');
|
|
89
|
-
const content = fs.readFileSync(envConfigPath, 'utf8');
|
|
90
|
-
const baseConfig = yaml.load(content) || {};
|
|
100
|
+
const baseConfig = loadSchemaEnvConfig();
|
|
91
101
|
|
|
92
102
|
// Load user env-config if configured
|
|
93
103
|
const userConfig = await loadUserEnvConfig();
|
|
@@ -97,6 +107,7 @@ async function loadEnvConfig() {
|
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
module.exports = {
|
|
100
|
-
loadEnvConfig
|
|
110
|
+
loadEnvConfig,
|
|
111
|
+
loadSchemaEnvConfig
|
|
101
112
|
};
|
|
102
113
|
|
package/lib/utils/env-map.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const yaml = require('js-yaml');
|
|
13
|
-
const { loadEnvConfig } = require('./env-config-loader');
|
|
13
|
+
const { loadEnvConfig, loadSchemaEnvConfig } = require('./env-config-loader');
|
|
14
14
|
const config = require('../core/config');
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -233,15 +233,15 @@ function applyLocalPortAdjustment(result, devIdNum) {
|
|
|
233
233
|
|
|
234
234
|
/**
|
|
235
235
|
* Calculate public ports for docker context
|
|
236
|
-
* Uses
|
|
237
|
-
* (e.g. KEYCLOAK_PUBLIC_PORT = 8082 + 600 = 8682 for dev 6,
|
|
236
|
+
* Uses schema env-config ports only so *_PUBLIC_PORT is always canonical base + devId*100
|
|
237
|
+
* (e.g. KEYCLOAK_PUBLIC_PORT = 8082 + 600 = 8682 for dev 6, even if user env-config or config overrides KEYCLOAK_PORT to 8080)
|
|
238
238
|
*
|
|
239
239
|
* @function calculateDockerPublicPorts
|
|
240
240
|
* @param {Object} result - Environment variable map (merged base + overrides)
|
|
241
241
|
* @param {number} devIdNum - Developer ID number
|
|
242
|
-
* @param {Object} [
|
|
242
|
+
* @param {Object} [schemaBaseVars] - Schema-only env-config vars (lib/schema/env-config.yaml) for *_PUBLIC_PORT
|
|
243
243
|
*/
|
|
244
|
-
function calculateDockerPublicPorts(result, devIdNum,
|
|
244
|
+
function calculateDockerPublicPorts(result, devIdNum, schemaBaseVars = {}) {
|
|
245
245
|
if (devIdNum <= 0) {
|
|
246
246
|
return;
|
|
247
247
|
}
|
|
@@ -249,9 +249,9 @@ function calculateDockerPublicPorts(result, devIdNum, baseVars = {}) {
|
|
|
249
249
|
// Match any variable ending with _PORT (e.g., MISO_PORT, KEYCLOAK_PORT, DB_PORT)
|
|
250
250
|
if (/_PORT$/.test(key) && !/_PUBLIC_PORT$/.test(key)) {
|
|
251
251
|
const publicPortKey = key.replace(/_PORT$/, '_PUBLIC_PORT');
|
|
252
|
-
// Use
|
|
253
|
-
const
|
|
254
|
-
const sourceVal =
|
|
252
|
+
// Use schema port when available so PUBLIC_PORT is canonical (e.g. 8082), not overridden (e.g. 8080)
|
|
253
|
+
const schemaPort = schemaBaseVars[key];
|
|
254
|
+
const sourceVal = schemaPort !== undefined && schemaPort !== null ? schemaPort : value;
|
|
255
255
|
let portVal;
|
|
256
256
|
if (typeof sourceVal === 'string') {
|
|
257
257
|
portVal = parseInt(sourceVal, 10);
|
|
@@ -294,7 +294,9 @@ async function buildEnvVarMap(context, osModule = null, developerId = null) {
|
|
|
294
294
|
applyLocalPortAdjustment(result, devIdNum);
|
|
295
295
|
} else if (context === 'docker') {
|
|
296
296
|
const devIdNum = await getDeveloperIdNumber(developerId);
|
|
297
|
-
|
|
297
|
+
const schemaCfg = loadSchemaEnvConfig();
|
|
298
|
+
const schemaBaseVars = (schemaCfg && schemaCfg.environments && schemaCfg.environments[context]) ? schemaCfg.environments[context] : {};
|
|
299
|
+
calculateDockerPublicPorts(result, devIdNum, schemaBaseVars);
|
|
298
300
|
}
|
|
299
301
|
|
|
300
302
|
return result;
|
|
@@ -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
|
/**
|