@aifabrix/builder 2.44.6 → 2.45.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/.cursor/rules/cli-layout.mdc +7 -3
- package/jest.projects.js +56 -0
- package/lib/app/helpers.js +3 -3
- package/lib/app/index.js +3 -3
- package/lib/app/register.js +7 -6
- package/lib/app/restart-display.js +52 -21
- package/lib/app/rotate-secret.js +7 -6
- package/lib/app/run-helpers.js +15 -8
- package/lib/app/run.js +57 -9
- package/lib/app/show-display.js +7 -0
- package/lib/app/show.js +87 -5
- package/lib/build/index.js +9 -5
- package/lib/cli/infra-guided.js +42 -27
- package/lib/cli/installation-log-command.js +73 -0
- package/lib/cli/setup-app.js +11 -1
- package/lib/cli/setup-auth.js +94 -49
- package/lib/cli/setup-infra-up-dataplane-action.js +111 -0
- package/lib/cli/setup-infra-up-platform-action.js +131 -0
- package/lib/cli/setup-infra.js +60 -119
- package/lib/cli/setup-platform.js +1 -1
- package/lib/cli/setup-utility-resolve.js +132 -0
- package/lib/cli/setup-utility.js +65 -51
- package/lib/commands/app-logs.js +81 -33
- package/lib/commands/auth-config.js +116 -18
- package/lib/commands/setup-modes.js +19 -6
- package/lib/commands/setup-prompts.js +41 -8
- package/lib/commands/setup.js +114 -9
- package/lib/commands/teardown.js +54 -5
- package/lib/commands/up-common.js +48 -14
- package/lib/commands/up-dataplane.js +21 -18
- package/lib/commands/up-miso.js +12 -8
- package/lib/commands/upload.js +5 -3
- package/lib/core/audit-logger.js +1 -34
- package/lib/core/config-admin-email.js +56 -0
- package/lib/core/config-normalize.js +60 -0
- package/lib/core/config-registered-controller-urls.js +54 -0
- package/lib/core/config.js +33 -50
- package/lib/core/secrets-ensure-infra.js +1 -1
- package/lib/core/secrets-env-content.js +86 -90
- package/lib/core/secrets-env-declarative-expand.js +170 -0
- package/lib/core/secrets-env-write.js +2 -0
- package/lib/core/secrets-load.js +106 -102
- package/lib/external-system/deploy.js +5 -1
- package/lib/internal/node-fs.js +2 -0
- package/lib/schema/application-schema.json +4 -0
- package/lib/schema/infra.parameter.yaml +10 -0
- package/lib/utils/app-config-resolver.js +24 -1
- package/lib/utils/applications-config-defaults.js +206 -0
- package/lib/utils/auth-config-validator.js +2 -12
- package/lib/utils/bash-secret-env.js +1 -1
- package/lib/utils/compose-generate-docker-compose.js +111 -6
- package/lib/utils/compose-generator.js +17 -8
- package/lib/utils/controller-url.js +50 -7
- package/lib/utils/env-copy.js +99 -14
- package/lib/utils/env-template.js +5 -1
- package/lib/utils/health-check-url.js +18 -15
- package/lib/utils/health-check.js +7 -5
- package/lib/utils/infra-optional-service-flags.js +69 -0
- package/lib/utils/installation-log-core.js +282 -0
- package/lib/utils/installation-log-record.js +237 -0
- package/lib/utils/installation-log.js +123 -0
- package/lib/utils/log-redaction.js +105 -0
- package/lib/utils/manifest-location.js +164 -0
- package/lib/utils/manifest-source-emit.js +162 -0
- package/lib/utils/paths.js +238 -89
- package/lib/utils/remote-secrets-loader.js +7 -1
- package/lib/utils/run-cli-flags.js +29 -0
- package/lib/utils/secrets-canonical.js +10 -3
- package/lib/utils/secrets-path.js +3 -4
- package/lib/utils/secrets-utils.js +20 -10
- package/lib/utils/system-builder-root.js +10 -2
- package/lib/utils/url-declarative-public-base.js +80 -12
- package/lib/utils/url-declarative-resolve-build-urls.js +238 -0
- package/lib/utils/url-declarative-resolve-build.js +24 -393
- package/lib/utils/url-declarative-resolve-expand-token.js +189 -0
- package/lib/utils/url-declarative-resolve-load-doc.js +12 -3
- package/lib/utils/url-declarative-resolve-surface-state.js +102 -0
- package/lib/utils/url-declarative-resolve.js +47 -7
- package/lib/utils/url-declarative-runtime-base-path.js +21 -1
- package/lib/utils/urls-local-registry-scan.js +103 -0
- package/lib/utils/urls-local-registry.js +161 -90
- package/package.json +3 -1
- package/templates/applications/dataplane/application.yaml +4 -0
- package/templates/applications/miso-controller/application.yaml +2 -0
- package/templates/applications/miso-controller/env.template +27 -29
- package/.npmrc.token +0 -1
package/lib/cli/setup-utility.js
CHANGED
|
@@ -10,11 +10,11 @@ const { formatSuccessParagraph } = require('../utils/cli-test-layout-chalk');
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const chalk = require('chalk');
|
|
13
|
-
const secrets = require('../core/secrets');
|
|
14
13
|
const generator = require('../generator');
|
|
15
14
|
const logger = require('../utils/logger');
|
|
16
15
|
const { handleCommandError, logOfflinePathWhenType } = require('../utils/cli-utils');
|
|
17
16
|
const { detectAppType, getDeployJsonPath, getResolveAppPath } = require('../utils/paths');
|
|
17
|
+
const { setupResolveCommand } = require('./setup-utility-resolve');
|
|
18
18
|
|
|
19
19
|
const JSON_HELP_AFTER = `
|
|
20
20
|
Example:
|
|
@@ -121,46 +121,20 @@ function logSplitJsonResult(result) {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
function setupResolveCommand(program) {
|
|
125
|
-
program.command('resolve <app>')
|
|
126
|
-
.description('Generate .env from template; optional validate after')
|
|
127
|
-
.option('-f, --force', 'Generate missing secret keys in secrets file')
|
|
128
|
-
.option('--skip-validation', 'Skip file validation after generating .env')
|
|
129
|
-
.action(async(appName, options) => {
|
|
130
|
-
try {
|
|
131
|
-
const { appPath, envOnly } = await getResolveAppPath(appName);
|
|
132
|
-
const envPath = await secrets.generateEnvFile(
|
|
133
|
-
appName,
|
|
134
|
-
undefined,
|
|
135
|
-
'docker',
|
|
136
|
-
options.force,
|
|
137
|
-
{ appPath, envOnly, skipOutputPath: false, preserveFromPath: null }
|
|
138
|
-
);
|
|
139
|
-
logger.log(`✔ Generated .env file: ${envPath}`);
|
|
140
|
-
if (envOnly) {
|
|
141
|
-
logger.log(chalk.gray(' (env-only mode: validation skipped; no application config file)'));
|
|
142
|
-
} else if (!options.skipValidation) {
|
|
143
|
-
const validate = require('../validation/validate');
|
|
144
|
-
const result = await validate.validateAppOrFile(appName);
|
|
145
|
-
validate.displayValidationResults(result);
|
|
146
|
-
if (!result.valid) {
|
|
147
|
-
logger.log(chalk.yellow('\n⚠ Validation found errors. Fix them before deploying.'));
|
|
148
|
-
process.exit(1);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
} catch (error) {
|
|
152
|
-
handleCommandError(error, 'resolve');
|
|
153
|
-
process.exit(1);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
124
|
function setupJsonCommand(program) {
|
|
159
125
|
program.command('json <app>')
|
|
160
126
|
.description('Write deployment JSON to disk for version control')
|
|
161
127
|
.addHelpText('after', JSON_HELP_AFTER)
|
|
162
128
|
.action(async(appName, options) => {
|
|
163
129
|
try {
|
|
130
|
+
const resolved = await getResolveAppPath(appName);
|
|
131
|
+
const { emitManifestMetadataLineIfTTY } = require('../utils/manifest-source-emit');
|
|
132
|
+
emitManifestMetadataLineIfTTY(logger, {
|
|
133
|
+
appKey: appName,
|
|
134
|
+
appPath: resolved.appPath,
|
|
135
|
+
envOnly: resolved.envOnly,
|
|
136
|
+
json: false
|
|
137
|
+
});
|
|
164
138
|
const result = await generator.generateDeployJsonWithValidation(appName, options);
|
|
165
139
|
if (result.success) {
|
|
166
140
|
const fileName = result.path.includes('application-schema.json') ? 'application-schema.json' : 'deployment JSON';
|
|
@@ -353,28 +327,56 @@ function setupSplitJsonConvertShowCommands(program) {
|
|
|
353
327
|
setupShowCommand(program);
|
|
354
328
|
}
|
|
355
329
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
330
|
+
function emitManifestAfterValidateIfApplicable(appOrFile, result, outFormat) {
|
|
331
|
+
if (outFormat === 'json' || !result?.appPath || !appOrFile || typeof appOrFile !== 'string') {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
if (fs.existsSync(appOrFile) && fs.statSync(appOrFile).isFile()) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const { emitManifestMetadataLineIfTTY } = require('../utils/manifest-source-emit');
|
|
339
|
+
emitManifestMetadataLineIfTTY(logger, {
|
|
340
|
+
appKey: appOrFile,
|
|
341
|
+
appPath: result.appPath,
|
|
342
|
+
envOnly: false,
|
|
343
|
+
json: false
|
|
344
|
+
});
|
|
345
|
+
} catch {
|
|
346
|
+
/* ignore emit failures */
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function runValidateBatchBranch(validate, opts) {
|
|
359
351
|
const integration = opts.integration === true;
|
|
360
352
|
const builder = opts.builder === true;
|
|
353
|
+
if (!integration && !builder) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
361
356
|
const outFormat = (opts.format || 'default').toLowerCase();
|
|
357
|
+
const batchResult = integration && builder
|
|
358
|
+
? await validate.validateAll(opts)
|
|
359
|
+
: integration
|
|
360
|
+
? await validate.validateAllIntegrations(opts)
|
|
361
|
+
: await validate.validateAllBuilderApps(opts);
|
|
362
|
+
if (outFormat === 'json') {
|
|
363
|
+
logger.log(JSON.stringify(batchResult, null, 2));
|
|
364
|
+
} else {
|
|
365
|
+
validate.displayBatchValidationResults(batchResult);
|
|
366
|
+
}
|
|
367
|
+
if (!batchResult.valid) process.exit(1);
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
362
370
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
? await validate.validateAllIntegrations(opts)
|
|
368
|
-
: await validate.validateAllBuilderApps(opts);
|
|
369
|
-
if (outFormat === 'json') {
|
|
370
|
-
logger.log(JSON.stringify(batchResult, null, 2));
|
|
371
|
-
} else {
|
|
372
|
-
validate.displayBatchValidationResults(batchResult);
|
|
373
|
-
}
|
|
374
|
-
if (!batchResult.valid) process.exit(1);
|
|
371
|
+
async function runValidateCommand(appOrFile, options) {
|
|
372
|
+
const validate = require('../validation/validate');
|
|
373
|
+
const opts = options.opts ? options.opts() : options;
|
|
374
|
+
if (await runValidateBatchBranch(validate, opts)) {
|
|
375
375
|
return;
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
const outFormat = (opts.format || 'default').toLowerCase();
|
|
379
|
+
|
|
378
380
|
if (!appOrFile || typeof appOrFile !== 'string') {
|
|
379
381
|
logger.log(chalk.red('App name or file path is required, or use --integration / --builder'));
|
|
380
382
|
process.exit(1);
|
|
@@ -384,8 +386,20 @@ async function runValidateCommand(appOrFile, options) {
|
|
|
384
386
|
...opts,
|
|
385
387
|
certSync: opts.certSync === true
|
|
386
388
|
});
|
|
389
|
+
emitManifestAfterValidateIfApplicable(appOrFile, result, outFormat);
|
|
387
390
|
if (outFormat === 'json') {
|
|
388
|
-
|
|
391
|
+
let payload = result;
|
|
392
|
+
try {
|
|
393
|
+
if (result?.appPath && appOrFile && typeof appOrFile === 'string') {
|
|
394
|
+
if (!(fs.existsSync(appOrFile) && fs.statSync(appOrFile).isFile())) {
|
|
395
|
+
const { getManifestSourcePayload } = require('../utils/manifest-source-emit');
|
|
396
|
+
payload = { ...result, manifestSource: getManifestSourcePayload(appOrFile, result.appPath) };
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
} catch {
|
|
400
|
+
/* keep plain result */
|
|
401
|
+
}
|
|
402
|
+
logger.log(JSON.stringify(payload, null, 2));
|
|
389
403
|
} else {
|
|
390
404
|
validate.displayValidationResults(result);
|
|
391
405
|
}
|
package/lib/commands/app-logs.js
CHANGED
|
@@ -15,6 +15,7 @@ const containerHelpers = require('../utils/app-run-containers');
|
|
|
15
15
|
const { validateAppName } = require('../app/push');
|
|
16
16
|
|
|
17
17
|
const { execWithDockerEnv } = require('../utils/docker-exec');
|
|
18
|
+
const { maskEnvLine } = require('../utils/log-redaction');
|
|
18
19
|
|
|
19
20
|
/** Default number of log lines */
|
|
20
21
|
const DEFAULT_TAIL_LINES = 100;
|
|
@@ -43,36 +44,11 @@ const LEVEL_JSON_NUMERIC_REGEX = /"level"\s*:\s*(\d+)/;
|
|
|
43
44
|
/** Fallback: line contains whole-word "error" or "Error" when no other level detected (catches stack traces, "Error: msg", etc.) */
|
|
44
45
|
const ERROR_WORD_FALLBACK_REGEX = /\berror\b/i;
|
|
45
46
|
|
|
46
|
-
/**
|
|
47
|
-
const
|
|
47
|
+
/** Keycloak / Java style: validation failure without the word "error" (would be dropped by `-l error` otherwise) */
|
|
48
|
+
const VALIDATION_FAILURE_LINE_REGEX = /\bvalidation\s+failed\b/i;
|
|
48
49
|
|
|
49
|
-
/**
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
/** URL with embedded credentials: scheme://user:password@host → scheme://user:***@host */
|
|
53
|
-
const URL_CREDENTIAL_PATTERN = /(\w+:\/\/)([^:@]*):([^@]+)@/g;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Masks a single env line if the key looks like a secret or value contains URL credentials
|
|
57
|
-
* @param {string} line - Line in form KEY=value
|
|
58
|
-
* @returns {string} Same line or KEY=*** or value with masked URL credentials
|
|
59
|
-
*/
|
|
60
|
-
function maskEnvLine(line) {
|
|
61
|
-
const eq = line.indexOf('=');
|
|
62
|
-
if (eq <= 0) return line;
|
|
63
|
-
const key = line.slice(0, eq);
|
|
64
|
-
const value = line.slice(eq + 1);
|
|
65
|
-
|
|
66
|
-
const keyForCheck = key.replace(KEY_PREFIXES_TO_STRIP, '');
|
|
67
|
-
const isSecretKey = SECRET_KEY_PATTERN.test(keyForCheck);
|
|
68
|
-
|
|
69
|
-
const maskedValue = value.replace(URL_CREDENTIAL_PATTERN, '$1$2:***@');
|
|
70
|
-
const hasUrlCredentials = maskedValue !== value;
|
|
71
|
-
|
|
72
|
-
if (isSecretKey) return `${key}=***`;
|
|
73
|
-
if (hasUrlCredentials) return `${key}=${maskedValue}`;
|
|
74
|
-
return line;
|
|
75
|
-
}
|
|
50
|
+
/** Other failure phrases treated as error severity for level filtering */
|
|
51
|
+
const FAILURE_PHRASE_AS_ERROR_REGEX = /\b(fatal|failed to run|unable to (start|run)|exception in thread)\b/i;
|
|
76
52
|
|
|
77
53
|
/** Normalize level string to canonical 'debug'|'info'|'warn'|'error'. */
|
|
78
54
|
function normalizeLevel(raw) {
|
|
@@ -104,9 +80,66 @@ function getLogLevel(line) {
|
|
|
104
80
|
const jsonNum = line.match(LEVEL_JSON_NUMERIC_REGEX);
|
|
105
81
|
if (jsonNum) return numericLevelToName(parseInt(jsonNum[1], 10));
|
|
106
82
|
if (ERROR_WORD_FALLBACK_REGEX.test(line)) return 'error';
|
|
83
|
+
if (VALIDATION_FAILURE_LINE_REGEX.test(line)) return 'error';
|
|
84
|
+
if (FAILURE_PHRASE_AS_ERROR_REGEX.test(line)) return 'error';
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Lines after an `error`-level line often have no level prefix (e.g. Keycloak config validation details). */
|
|
89
|
+
const ERROR_FILTER_CONTEXT_UNCLASSIFIED_MAX = 50;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether to show a log line when `--level` filtering is on. After an emitted error line, includes
|
|
93
|
+
* up to {@link ERROR_FILTER_CONTEXT_UNCLASSIFIED_MAX} following lines with no parseable level so
|
|
94
|
+
* stack traces and multi-line validation output are not dropped.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} line
|
|
97
|
+
* @param {string|null} minLevel - normalized LOG_LEVELS value or null
|
|
98
|
+
* @param {{ pendingAfterError: number }} state - mutable; only used when minLevel is `error`
|
|
99
|
+
* @returns {boolean}
|
|
100
|
+
*/
|
|
101
|
+
/**
|
|
102
|
+
* @param {string|null} level
|
|
103
|
+
* @param {string} minLevel
|
|
104
|
+
* @param {{ pendingAfterError: number }} state
|
|
105
|
+
* @returns {boolean|null} true if line shown by sticky rule; null to continue
|
|
106
|
+
*/
|
|
107
|
+
function tryStickyErrorContinuation(level, minLevel, state) {
|
|
108
|
+
if (minLevel !== 'error' || state.pendingAfterError <= 0) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
if (level === null || level === undefined) {
|
|
112
|
+
state.pendingAfterError -= 1;
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (level !== 'error') {
|
|
116
|
+
state.pendingAfterError = 0;
|
|
117
|
+
}
|
|
107
118
|
return null;
|
|
108
119
|
}
|
|
109
120
|
|
|
121
|
+
function shouldShowFilteredLogLine(line, minLevel, state) {
|
|
122
|
+
if (minLevel === null || minLevel === undefined || minLevel === '' || !LOG_LEVELS.includes(minLevel)) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (line.trim() === '' && minLevel === 'error' && state.pendingAfterError > 0) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
const level = getLogLevel(line);
|
|
129
|
+
const sticky = tryStickyErrorContinuation(level, minLevel, state);
|
|
130
|
+
if (sticky === true) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (passesLevelFilter(level, minLevel)) {
|
|
135
|
+
if (minLevel === 'error' && level === 'error') {
|
|
136
|
+
state.pendingAfterError = ERROR_FILTER_CONTEXT_UNCLASSIFIED_MAX;
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
110
143
|
/**
|
|
111
144
|
* Whether a line's level passes the minimum level filter (show this level and above).
|
|
112
145
|
* @param {string|null} lineLevel - Level from getLogLevel (null treated as 'info')
|
|
@@ -143,7 +176,12 @@ async function dumpMaskedEnv(containerName) {
|
|
|
143
176
|
lines.forEach((line) => logger.log(maskEnvLine(line)));
|
|
144
177
|
logger.log(chalk.gray('\n--- Logs ---\n'));
|
|
145
178
|
} catch (err) {
|
|
146
|
-
logger.log(
|
|
179
|
+
logger.log(
|
|
180
|
+
chalk.gray(
|
|
181
|
+
'(Could not read container env — the container may be stopped, restarting, or not ready yet. ' +
|
|
182
|
+
'Docker log output below is still shown.)\n'
|
|
183
|
+
)
|
|
184
|
+
);
|
|
147
185
|
}
|
|
148
186
|
}
|
|
149
187
|
|
|
@@ -175,8 +213,9 @@ async function runDockerLogs(containerName, options) {
|
|
|
175
213
|
const proc = spawn('docker', args, { stdio: ['inherit', 'pipe', 'pipe'], env: dockerEnv });
|
|
176
214
|
proc.on('error', reject);
|
|
177
215
|
|
|
216
|
+
const filterState = { pendingAfterError: 0 };
|
|
178
217
|
function onLine(line) {
|
|
179
|
-
if (
|
|
218
|
+
if (shouldShowFilteredLogLine(line, minLevel, filterState)) {
|
|
180
219
|
process.stdout.write(line + '\n');
|
|
181
220
|
}
|
|
182
221
|
}
|
|
@@ -241,8 +280,11 @@ async function runDockerLogsFollow(containerName, tail, minLevel) {
|
|
|
241
280
|
logger.log(chalk.red(`Error: ${err.message}`));
|
|
242
281
|
process.exit(1);
|
|
243
282
|
});
|
|
283
|
+
const filterState = { pendingAfterError: 0 };
|
|
244
284
|
function onLine(line) {
|
|
245
|
-
if (
|
|
285
|
+
if (shouldShowFilteredLogLine(line, level, filterState)) {
|
|
286
|
+
process.stdout.write(line + '\n');
|
|
287
|
+
}
|
|
246
288
|
}
|
|
247
289
|
const rlOut = readline.createInterface({ input: proc.stdout, crlfDelay: Infinity });
|
|
248
290
|
rlOut.on('line', onLine);
|
|
@@ -301,4 +343,10 @@ async function runAppLogs(appKey, options = {}) {
|
|
|
301
343
|
}
|
|
302
344
|
}
|
|
303
345
|
|
|
304
|
-
module.exports = {
|
|
346
|
+
module.exports = {
|
|
347
|
+
runAppLogs,
|
|
348
|
+
maskEnvLine,
|
|
349
|
+
getLogLevel,
|
|
350
|
+
passesLevelFilter,
|
|
351
|
+
shouldShowFilteredLogLine
|
|
352
|
+
};
|
|
@@ -8,53 +8,144 @@
|
|
|
8
8
|
* @version 2.0.0
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
const chalk = require('chalk');
|
|
11
12
|
const { formatBlockingError, formatSuccessLine } = require('../utils/cli-test-layout-chalk');
|
|
12
13
|
const {
|
|
13
14
|
setControllerUrl,
|
|
14
15
|
setCurrentEnvironment,
|
|
15
|
-
getControllerUrl
|
|
16
|
+
getControllerUrl,
|
|
17
|
+
getRegisteredControllerUrls,
|
|
18
|
+
getConfig
|
|
16
19
|
} = require('../core/config');
|
|
17
20
|
const {
|
|
18
21
|
validateControllerUrl,
|
|
19
22
|
validateEnvironment,
|
|
20
23
|
checkUserLoggedIn
|
|
21
24
|
} = require('../utils/auth-config-validator');
|
|
22
|
-
const {
|
|
25
|
+
const { hasStoredDeviceTokenForController } = require('../utils/controller-url');
|
|
23
26
|
const logger = require('../utils/logger');
|
|
24
27
|
|
|
28
|
+
/**
|
|
29
|
+
* True when user ran --set-controller with no URL (pick from config.yaml).
|
|
30
|
+
* @param {unknown} setController - Commander option value
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
function isInteractiveControllerPick(setController) {
|
|
34
|
+
return (
|
|
35
|
+
setController === true ||
|
|
36
|
+
(typeof setController === 'string' && setController.trim() === '')
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* True when a non-empty controller URL string was passed.
|
|
42
|
+
* @param {unknown} setController - Commander option value
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
function hasExplicitControllerUrl(setController) {
|
|
46
|
+
return typeof setController === 'string' && setController.trim() !== '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Normalize controller URL for equality checks (trailing slashes).
|
|
51
|
+
* @param {string|null|undefined} url - URL or empty
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
function normalizeForCompare(url) {
|
|
55
|
+
if (!url || typeof url !== 'string') return '';
|
|
56
|
+
return url.trim().replace(/\/+$/, '');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function throwNoRegisteredControllers() {
|
|
60
|
+
const msg =
|
|
61
|
+
'No controllers are registered in config. Run "aifabrix login" first, or set a controller with "aifabrix auth --set-controller <url>".';
|
|
62
|
+
logger.error(formatBlockingError(msg));
|
|
63
|
+
throw new Error(msg);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function throwNonInteractiveControllerPick() {
|
|
67
|
+
const msg =
|
|
68
|
+
'Cannot choose a controller without a URL in non-interactive mode. Run: aifabrix auth --set-controller <url>';
|
|
69
|
+
logger.error(formatBlockingError(msg));
|
|
70
|
+
throw new Error(msg);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Pick default controller from URLs in config (`controller` + `device` keys), or set the only one.
|
|
75
|
+
* @async
|
|
76
|
+
* @returns {Promise<void>}
|
|
77
|
+
*/
|
|
78
|
+
async function handleSelectRegisteredController() {
|
|
79
|
+
const urls = await getRegisteredControllerUrls();
|
|
80
|
+
|
|
81
|
+
if (urls.length === 0) {
|
|
82
|
+
throwNoRegisteredControllers();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!process.stdin.isTTY) {
|
|
86
|
+
throwNonInteractiveControllerPick();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (urls.length === 1) {
|
|
90
|
+
const sole = urls[0];
|
|
91
|
+
const current = await getControllerUrl();
|
|
92
|
+
if (normalizeForCompare(current) === normalizeForCompare(sole)) {
|
|
93
|
+
logger.log(formatSuccessLine(`Default controller is already set to ${sole}.`));
|
|
94
|
+
logger.log(
|
|
95
|
+
chalk.white('To add another controller, run: aifabrix auth --set-controller <url>')
|
|
96
|
+
);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await handleSetController(sole);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const inquirer = require('inquirer');
|
|
104
|
+
const { controllerUrl } = await inquirer.prompt([
|
|
105
|
+
{
|
|
106
|
+
type: 'list',
|
|
107
|
+
name: 'controllerUrl',
|
|
108
|
+
message: 'Select default controller:',
|
|
109
|
+
choices: urls
|
|
110
|
+
}
|
|
111
|
+
]);
|
|
112
|
+
await handleSetController(controllerUrl);
|
|
113
|
+
}
|
|
114
|
+
|
|
25
115
|
/**
|
|
26
116
|
* Handle set-controller command
|
|
27
|
-
* Allows setting the default controller when no
|
|
28
|
-
*
|
|
117
|
+
* Allows setting the default controller when there are no device tokens, or when a token exists
|
|
118
|
+
* for the target URL (including switching among multiple logged-in controllers). If device tokens
|
|
119
|
+
* exist only for other controller URLs, throws with a clear message.
|
|
29
120
|
*
|
|
30
121
|
* @async
|
|
31
122
|
* @function handleSetController
|
|
32
123
|
* @param {string} url - Controller URL to set
|
|
33
124
|
* @returns {Promise<void>}
|
|
34
|
-
* @throws {Error} If validation fails or credentials exist for
|
|
125
|
+
* @throws {Error} If validation fails or credentials exist only for other controllers
|
|
35
126
|
*/
|
|
36
127
|
async function handleSetController(url) {
|
|
37
128
|
try {
|
|
38
129
|
validateControllerUrl(url);
|
|
39
130
|
const normalizedUrl = url.trim().replace(/\/+$/, '');
|
|
40
131
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
132
|
+
const userConfig = await getConfig();
|
|
133
|
+
const device =
|
|
134
|
+
userConfig.device && typeof userConfig.device === 'object' ? userConfig.device : {};
|
|
135
|
+
const deviceKeys = Object.keys(device);
|
|
136
|
+
const hasTokenForTarget = await hasStoredDeviceTokenForController(normalizedUrl);
|
|
48
137
|
|
|
49
|
-
|
|
50
|
-
if (normalizedLoggedIn === normalizedUrl) {
|
|
138
|
+
if (deviceKeys.length === 0 || hasTokenForTarget) {
|
|
51
139
|
await setControllerUrl(url);
|
|
52
140
|
logger.log(formatSuccessLine(`Controller URL set to: ${url}`));
|
|
53
141
|
return;
|
|
54
142
|
}
|
|
55
143
|
|
|
144
|
+
const otherKey =
|
|
145
|
+
deviceKeys.find((k) => normalizeForCompare(k) !== normalizeForCompare(normalizedUrl)) ||
|
|
146
|
+
deviceKeys[0];
|
|
56
147
|
throw new Error(
|
|
57
|
-
`You have credentials for another controller (${
|
|
148
|
+
`You have credentials for another controller (${otherKey.trim().replace(/\/+$/, '')}).\n` +
|
|
58
149
|
'To use a different controller either run "aifabrix login" with that controller, or run "aifabrix logout" first to clear credentials, then set the new controller with "aifabrix auth --set-controller <url>".'
|
|
59
150
|
);
|
|
60
151
|
} catch (error) {
|
|
@@ -107,20 +198,27 @@ async function handleSetEnvironment(environment) {
|
|
|
107
198
|
* @async
|
|
108
199
|
* @function handleAuthConfig
|
|
109
200
|
* @param {Object} options - Command options
|
|
110
|
-
* @param {string} [options.setController] - Controller URL
|
|
201
|
+
* @param {string|boolean} [options.setController] - Controller URL, or true when flag has no value
|
|
111
202
|
* @param {string} [options.setEnvironment] - Environment to set
|
|
112
203
|
* @returns {Promise<void>}
|
|
113
204
|
* @throws {Error} If command fails
|
|
114
205
|
*/
|
|
115
206
|
async function handleAuthConfig(options) {
|
|
116
|
-
|
|
207
|
+
const pick = isInteractiveControllerPick(options.setController);
|
|
208
|
+
const hasUrl = hasExplicitControllerUrl(options.setController);
|
|
209
|
+
|
|
210
|
+
if (!pick && !hasUrl && !options.setEnvironment) {
|
|
117
211
|
throw new Error(
|
|
118
212
|
'No action specified. Use "aifabrix auth --set-controller <url>" or "aifabrix auth --set-environment <env>".'
|
|
119
213
|
);
|
|
120
214
|
}
|
|
121
|
-
|
|
215
|
+
|
|
216
|
+
if (pick) {
|
|
217
|
+
await handleSelectRegisteredController();
|
|
218
|
+
} else if (hasUrl) {
|
|
122
219
|
await handleSetController(options.setController);
|
|
123
220
|
}
|
|
221
|
+
|
|
124
222
|
if (options.setEnvironment) {
|
|
125
223
|
await handleSetEnvironment(options.setEnvironment);
|
|
126
224
|
}
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* - {@link runUpdateImages} — Mode 4: docker pull infra + platform images, up-infra, up-platform.
|
|
10
10
|
*
|
|
11
11
|
* Every handler ends with `up-infra` (idempotent, reads flags from
|
|
12
|
-
* `config.yaml`) followed by `up-platform`.
|
|
12
|
+
* `config.yaml`) followed by `up-platform`. Optional service keys
|
|
13
|
+
* (`traefik`, `pgadmin`, `redisCommander`) are written when missing so the
|
|
14
|
+
* file matches effective compose defaults. Modes 1/2/3 also remove
|
|
13
15
|
* `secrets.local.yaml` so the next platform bootstrap re-resolves
|
|
14
16
|
* service secrets from the catalog and re-prompts the AI tool wizard.
|
|
15
17
|
*
|
|
@@ -49,6 +51,10 @@ const postgresWipe = require('../utils/postgres-wipe');
|
|
|
49
51
|
const setupPrompts = require('./setup-prompts');
|
|
50
52
|
const { handleLogin } = require('./login');
|
|
51
53
|
const infraGuided = require('../cli/infra-guided');
|
|
54
|
+
const {
|
|
55
|
+
computeEffectiveInfraOptionalFlags,
|
|
56
|
+
persistMissingInfraOptionalServiceFlags
|
|
57
|
+
} = require('../utils/infra-optional-service-flags');
|
|
52
58
|
|
|
53
59
|
/** Builder app keys touched by `up-platform --force`. */
|
|
54
60
|
const PLATFORM_APPS = ['keycloak', 'miso-controller', 'dataplane'];
|
|
@@ -203,7 +209,9 @@ function stopSpinnerSuccess(spinner, text) {
|
|
|
203
209
|
/**
|
|
204
210
|
* Start infrastructure using the developer's `config.yaml` flags.
|
|
205
211
|
* Mirrors the relevant slice of `runUpInfraCommand` from `lib/cli/setup-infra.js`
|
|
206
|
-
* without re-applying CLI flag persistence (
|
|
212
|
+
* without re-applying CLI flag persistence (setup passes no Commander flags).
|
|
213
|
+
* After a successful start, missing `traefik` / `pgadmin` / `redisCommander`
|
|
214
|
+
* keys are written to match effective {@link infra.startInfra} options.
|
|
207
215
|
*
|
|
208
216
|
* @async
|
|
209
217
|
* @param {Object} [overrides] - Optional admin overrides (fresh install only)
|
|
@@ -214,17 +222,22 @@ function stopSpinnerSuccess(spinner, text) {
|
|
|
214
222
|
async function startInfraFromConfig(overrides = {}) {
|
|
215
223
|
await config.ensureSecretsEncryptionKey();
|
|
216
224
|
const cfg = await config.getConfig();
|
|
225
|
+
const emailOverride = String(overrides.adminEmail || '').trim();
|
|
226
|
+
const emailFromConfig = typeof cfg.adminEmail === 'string' ? cfg.adminEmail.trim() : '';
|
|
227
|
+
const adminEmailMerged = emailOverride || emailFromConfig || undefined;
|
|
228
|
+
const effective = computeEffectiveInfraOptionalFlags(cfg, {});
|
|
217
229
|
await withMutedLogger(async() => {
|
|
218
230
|
await infra.startInfra(null, {
|
|
219
|
-
traefik:
|
|
220
|
-
pgadmin:
|
|
221
|
-
redisCommander:
|
|
231
|
+
traefik: effective.traefik,
|
|
232
|
+
pgadmin: effective.pgadmin,
|
|
233
|
+
redisCommander: effective.redisCommander,
|
|
222
234
|
adminPassword: overrides.adminPassword,
|
|
223
235
|
adminPwd: overrides.adminPassword,
|
|
224
|
-
adminEmail:
|
|
236
|
+
adminEmail: adminEmailMerged,
|
|
225
237
|
tlsEnabled: cfg.tlsEnabled === true
|
|
226
238
|
});
|
|
227
239
|
});
|
|
240
|
+
await persistMissingInfraOptionalServiceFlags(cfg, effective);
|
|
228
241
|
}
|
|
229
242
|
|
|
230
243
|
/**
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Three small prompt helpers:
|
|
5
5
|
* 1. {@link promptModeSelection} - Mode menu shown when infra is already up.
|
|
6
|
-
* 2. {@link promptAdminCredentials} - Admin email + password (fresh install only)
|
|
6
|
+
* 2. {@link promptAdminCredentials} - Admin email + password (fresh install only); email default
|
|
7
|
+
* from {@link module:lib/core/config.getAdminEmail} when `adminEmail` exists in config.yaml.
|
|
7
8
|
* 3. {@link promptAiTool} - AI tool provider + keys, only when the merged
|
|
8
9
|
* secret cascade (user-local + shared) does not already provide the keys.
|
|
9
10
|
*
|
|
@@ -23,9 +24,11 @@
|
|
|
23
24
|
|
|
24
25
|
'use strict';
|
|
25
26
|
|
|
27
|
+
const path = require('path');
|
|
26
28
|
const inquirer = require('inquirer');
|
|
27
29
|
const chalk = require('chalk');
|
|
28
30
|
const logger = require('../utils/logger');
|
|
31
|
+
const config = require('../core/config');
|
|
29
32
|
const secretsCore = require('../core/secrets');
|
|
30
33
|
const secretsEnsure = require('../core/secrets-ensure');
|
|
31
34
|
const { formatSuccessLine, infoLine } = require('../utils/cli-test-layout-chalk');
|
|
@@ -133,6 +136,8 @@ function validatePassword(input) {
|
|
|
133
136
|
|
|
134
137
|
/**
|
|
135
138
|
* Prompt for admin email + password (fresh install only).
|
|
139
|
+
* Reads {@link module:lib/core/config.getAdminEmail} so a previous `aifabrix setup` can prefill the
|
|
140
|
+
* email field (user may change it before continuing).
|
|
136
141
|
* Asks once for the password and once for confirmation; returns trimmed email
|
|
137
142
|
* and the verbatim password (never logged).
|
|
138
143
|
*
|
|
@@ -140,13 +145,24 @@ function validatePassword(input) {
|
|
|
140
145
|
* @returns {Promise<{ adminEmail: string, adminPassword: string }>}
|
|
141
146
|
*/
|
|
142
147
|
async function promptAdminCredentials() {
|
|
148
|
+
let defaultEmail;
|
|
149
|
+
try {
|
|
150
|
+
const saved = await config.getAdminEmail();
|
|
151
|
+
defaultEmail = saved && saved.trim() ? saved.trim() : undefined;
|
|
152
|
+
} catch {
|
|
153
|
+
defaultEmail = undefined;
|
|
154
|
+
}
|
|
155
|
+
const emailQuestion = {
|
|
156
|
+
type: 'input',
|
|
157
|
+
name: 'adminEmail',
|
|
158
|
+
message: 'Admin email (Keycloak / pgAdmin login):',
|
|
159
|
+
validate: validateEmail
|
|
160
|
+
};
|
|
161
|
+
if (defaultEmail !== undefined) {
|
|
162
|
+
emailQuestion.default = defaultEmail;
|
|
163
|
+
}
|
|
143
164
|
const answers = await inquirer.prompt([
|
|
144
|
-
|
|
145
|
-
type: 'input',
|
|
146
|
-
name: 'adminEmail',
|
|
147
|
-
message: 'Admin email (Keycloak / pgAdmin login):',
|
|
148
|
-
validate: validateEmail
|
|
149
|
-
},
|
|
165
|
+
emailQuestion,
|
|
150
166
|
{
|
|
151
167
|
type: 'password',
|
|
152
168
|
name: 'adminPassword',
|
|
@@ -323,6 +339,22 @@ async function promptAiTool(options = {}) {
|
|
|
323
339
|
);
|
|
324
340
|
}
|
|
325
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Absolute directory paths (with trailing sep) shown when setup will replace platform apps.
|
|
344
|
+
*
|
|
345
|
+
* @param {string} builderRoot - Absolute or relative builder root
|
|
346
|
+
* @param {string[]} platformApps - App folder names (e.g. `keycloak`)
|
|
347
|
+
* @returns {string[]}
|
|
348
|
+
*/
|
|
349
|
+
function formatBuilderPlatformReplaceLines(builderRoot, platformApps) {
|
|
350
|
+
const root = builderRoot && typeof builderRoot === 'string' ? builderRoot : 'builder/';
|
|
351
|
+
const apps = Array.isArray(platformApps) ? platformApps : [];
|
|
352
|
+
return apps.map((a) => {
|
|
353
|
+
const full = path.resolve(path.join(root, a));
|
|
354
|
+
return full.endsWith(path.sep) ? full : `${full}${path.sep}`;
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
326
358
|
/**
|
|
327
359
|
* When `aifabrix setup` is about to run `up-platform --force`, it will wipe
|
|
328
360
|
* platform app directories under `builder/`. If the builder root already
|
|
@@ -356,7 +388,7 @@ async function promptBuilderDirConflict(info) {
|
|
|
356
388
|
systemNote +
|
|
357
389
|
'\n\n' +
|
|
358
390
|
'This setup path will REPLACE the platform app folders under:\n' +
|
|
359
|
-
` ${
|
|
391
|
+
` ${formatBuilderPlatformReplaceLines(builderRoot, platformApps).join('\n ')}`;
|
|
360
392
|
|
|
361
393
|
const { action } = await inquirer.prompt([
|
|
362
394
|
{
|
|
@@ -379,6 +411,7 @@ module.exports = {
|
|
|
379
411
|
MODE,
|
|
380
412
|
AI_KEYS,
|
|
381
413
|
BUILDER_DIR_ACTION,
|
|
414
|
+
formatBuilderPlatformReplaceLines,
|
|
382
415
|
promptModeSelection,
|
|
383
416
|
confirmDestructiveMode,
|
|
384
417
|
promptAdminCredentials,
|