@aifabrix/builder 2.39.2 → 2.40.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/project-rules.mdc +6 -6
- package/README.md +2 -2
- package/babel.config.js +6 -0
- package/integration/hubspot/README.md +53 -141
- package/integration/hubspot/application.yaml +37 -0
- package/integration/hubspot/env.template +2 -11
- package/integration/hubspot/hubspot-deploy.json +1 -0
- package/integration/hubspot/test.js +5 -5
- package/lib/api/credentials.api.js +5 -5
- package/lib/api/deployments.api.js +2 -2
- package/lib/api/pipeline.api.js +17 -17
- package/lib/api/wizard.api.js +2 -2
- package/lib/app/config.js +11 -6
- package/lib/app/deploy-config.js +13 -16
- package/lib/app/deploy.js +29 -22
- package/lib/app/display.js +1 -1
- package/lib/app/dockerfile.js +11 -12
- package/lib/app/helpers.js +51 -13
- package/lib/app/index.js +14 -2
- package/lib/app/prompts.js +37 -45
- package/lib/app/push.js +8 -11
- package/lib/app/readme.js +16 -12
- package/lib/app/register.js +3 -3
- package/lib/app/run-helpers.js +31 -22
- package/lib/app/run.js +44 -5
- package/lib/app/show-display.js +104 -44
- package/lib/app/show.js +123 -43
- package/lib/build/index.js +11 -18
- package/lib/cli/setup-app.js +38 -28
- package/lib/cli/setup-auth.js +18 -15
- package/lib/cli/setup-credential-deployment.js +3 -1
- package/lib/cli/setup-external-system.js +35 -16
- package/lib/cli/setup-infra.js +45 -23
- package/lib/cli/setup-utility.js +79 -31
- package/lib/commands/app-logs.js +165 -10
- package/lib/commands/app.js +30 -26
- package/lib/commands/convert.js +202 -0
- package/lib/commands/credential-list.js +78 -17
- package/lib/commands/datasource.js +24 -24
- package/lib/commands/deployment-list.js +13 -6
- package/lib/commands/up-common.js +80 -42
- package/lib/commands/up-dataplane.js +15 -14
- package/lib/commands/up-miso.js +15 -14
- package/lib/commands/upload.js +163 -0
- package/lib/commands/wizard-core.js +5 -4
- package/lib/core/diff.js +84 -9
- package/lib/core/key-generator.js +9 -12
- package/lib/core/secrets-docker-env.js +2 -2
- package/lib/core/secrets.js +3 -2
- package/lib/core/templates.js +2 -2
- package/lib/datasource/deploy.js +2 -1
- package/lib/deployment/deployer.js +76 -48
- package/lib/external-system/delete.js +0 -1
- package/lib/external-system/deploy-helpers.js +5 -6
- package/lib/external-system/deploy.js +7 -2
- package/lib/external-system/download-helpers.js +4 -4
- package/lib/external-system/download.js +11 -10
- package/lib/external-system/generator.js +19 -17
- package/lib/external-system/test.js +10 -15
- package/lib/generator/builders.js +1 -1
- package/lib/generator/external-controller-manifest.js +26 -29
- package/lib/generator/external-schema-utils.js +6 -18
- package/lib/generator/external.js +32 -27
- package/lib/generator/github.js +1 -1
- package/lib/generator/helpers.js +12 -19
- package/lib/generator/index.js +15 -15
- package/lib/generator/parse-image.js +35 -0
- package/lib/generator/split-readme.js +105 -0
- package/lib/generator/split-variables.js +149 -0
- package/lib/generator/split.js +86 -246
- package/lib/generator/wizard.js +46 -69
- package/lib/schema/application-schema.json +4 -4
- package/lib/schema/deployment-rules.yaml +0 -4
- package/lib/schema/external-datasource.schema.json +5 -0
- package/lib/schema/external-system.schema.json +10 -0
- package/lib/utils/app-config-resolver.js +52 -0
- package/lib/utils/app-register-api.js +1 -1
- package/lib/utils/app-register-auth.js +1 -1
- package/lib/utils/app-register-config.js +16 -23
- package/lib/utils/app-register-display.js +22 -3
- package/lib/utils/app-register-validator.js +2 -2
- package/lib/utils/cli-utils.js +47 -3
- package/lib/utils/config-format.js +154 -0
- package/lib/utils/config-paths.js +19 -52
- package/lib/utils/config-tokens.js +1 -0
- package/lib/utils/docker-build.js +71 -94
- package/lib/utils/dockerfile-utils.js +1 -1
- package/lib/utils/env-copy.js +4 -4
- package/lib/utils/env-ports.js +2 -2
- package/lib/utils/error-formatter.js +1 -1
- package/lib/utils/error-formatters/validation-errors.js +1 -1
- package/lib/utils/external-readme.js +12 -5
- package/lib/utils/external-system-test-helpers.js +2 -0
- package/lib/utils/health-check.js +55 -66
- package/lib/utils/image-version.js +12 -21
- package/lib/utils/paths.js +39 -66
- package/lib/utils/port-resolver.js +8 -8
- package/lib/utils/schema-loader.js +22 -0
- package/lib/utils/schema-resolver.js +23 -33
- package/lib/utils/secrets-helpers.js +7 -7
- package/lib/utils/secrets-utils.js +10 -12
- package/lib/utils/template-helpers.js +13 -13
- package/lib/utils/token-manager.js +20 -2
- package/lib/utils/variable-transformer.js +2 -2
- package/lib/validation/validate-display.js +3 -4
- package/lib/validation/validate.js +33 -27
- package/lib/validation/validator.js +50 -30
- package/package.json +2 -1
- package/templates/README.md +1 -1
- package/templates/applications/README.md.hbs +3 -3
- package/templates/applications/miso-controller/env.template +3 -1
- package/templates/external-system/README.md.hbs +4 -4
- package/integration/hubspot/variables.yaml +0 -17
- /package/templates/applications/dataplane/{variables.yaml → application.yaml} +0 -0
- /package/templates/applications/keycloak/{variables.yaml → application.yaml} +0 -0
- /package/templates/applications/miso-controller/{variables.yaml → application.yaml} +0 -0
package/lib/commands/app-logs.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const chalk = require('chalk');
|
|
10
10
|
const { exec, spawn } = require('child_process');
|
|
11
|
+
const readline = require('readline');
|
|
11
12
|
const { promisify } = require('util');
|
|
12
13
|
const logger = require('../utils/logger');
|
|
13
14
|
const config = require('../core/config');
|
|
@@ -19,6 +20,30 @@ const execAsync = promisify(exec);
|
|
|
19
20
|
/** Default number of log lines */
|
|
20
21
|
const DEFAULT_TAIL_LINES = 100;
|
|
21
22
|
|
|
23
|
+
/** Allowed log levels for --level filter (lowest to highest severity) */
|
|
24
|
+
const LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
|
|
25
|
+
|
|
26
|
+
/** Severity rank: higher = more severe. Used for "show this level and above". */
|
|
27
|
+
const LEVEL_RANK = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
28
|
+
|
|
29
|
+
/** Prefix pattern: INFO:, ERROR:, WARN:, WARNING:, DEBUG: at start of line */
|
|
30
|
+
const LEVEL_PREFIX_REGEX = /^(DEBUG|INFO|WARN|WARNING|ERROR)\s*[:-\s]/i;
|
|
31
|
+
|
|
32
|
+
/** Level after timestamp or space (e.g. "2026-02-11 08:51:01 error: msg" or " error: msg") */
|
|
33
|
+
const LEVEL_AFTER_PREFIX_REGEX = /(?:^|\s)(DEBUG|INFO|WARN|WARNING|ERROR)\s*:/i;
|
|
34
|
+
|
|
35
|
+
/** Level after word boundary (e.g. "[pino]error: msg" or BOM + "error:") so "error:" is detected anywhere */
|
|
36
|
+
const LEVEL_WORD_BOUNDARY_REGEX = /\b(DEBUG|INFO|WARN|WARNING|ERROR)\s*:/i;
|
|
37
|
+
|
|
38
|
+
/** JSON "level" field pattern (string: "error", "info", etc.) */
|
|
39
|
+
const LEVEL_JSON_REGEX = /"level"\s*:\s*"(\w+)"/i;
|
|
40
|
+
|
|
41
|
+
/** JSON "level" numeric (pino/bunyan: 50=error, 60=fatal, 40=warn, 30=info, 20=debug, 10=trace) */
|
|
42
|
+
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.) */
|
|
45
|
+
const ERROR_WORD_FALLBACK_REGEX = /\berror\b/i;
|
|
46
|
+
|
|
22
47
|
/** Env key patterns that indicate a secret (mask value) */
|
|
23
48
|
const SECRET_KEY_PATTERN = /password|secret|token|credential|api[_-]?key/i;
|
|
24
49
|
|
|
@@ -50,6 +75,55 @@ function maskEnvLine(line) {
|
|
|
50
75
|
return line;
|
|
51
76
|
}
|
|
52
77
|
|
|
78
|
+
/** Normalize level string to canonical 'debug'|'info'|'warn'|'error'. */
|
|
79
|
+
function normalizeLevel(raw) {
|
|
80
|
+
const s = (raw || '').toLowerCase();
|
|
81
|
+
return s === 'warning' ? 'warn' : s;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Map pino/bunyan numeric level (10–60) to level string. */
|
|
85
|
+
function numericLevelToName(num) {
|
|
86
|
+
if (num >= 50) return 'error';
|
|
87
|
+
if (num >= 40) return 'warn';
|
|
88
|
+
if (num >= 30) return 'info';
|
|
89
|
+
return 'debug';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Extract log level from a line (prefix like INFO:/error: or JSON "level":"info").
|
|
94
|
+
* Supports: INFO:, ERROR:, error:, info: (miso-controller/pino), WARN:, DEBUG:, and "level":"x" in JSON.
|
|
95
|
+
* @param {string} line - Single log line
|
|
96
|
+
* @returns {string|null} One of 'debug'|'info'|'warn'|'error', or null if not parseable
|
|
97
|
+
*/
|
|
98
|
+
function getLogLevel(line) {
|
|
99
|
+
if (!line || typeof line !== 'string') return null;
|
|
100
|
+
const prefixRe = [LEVEL_PREFIX_REGEX, LEVEL_AFTER_PREFIX_REGEX, LEVEL_WORD_BOUNDARY_REGEX, LEVEL_JSON_REGEX];
|
|
101
|
+
for (const re of prefixRe) {
|
|
102
|
+
const m = line.match(re);
|
|
103
|
+
if (m) return normalizeLevel(m[1]);
|
|
104
|
+
}
|
|
105
|
+
const jsonNum = line.match(LEVEL_JSON_NUMERIC_REGEX);
|
|
106
|
+
if (jsonNum) return numericLevelToName(parseInt(jsonNum[1], 10));
|
|
107
|
+
if (ERROR_WORD_FALLBACK_REGEX.test(line)) return 'error';
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Whether a line's level passes the minimum level filter (show this level and above).
|
|
113
|
+
* @param {string|null} lineLevel - Level from getLogLevel (null treated as 'info')
|
|
114
|
+
* @param {string|undefined|null} minLevel - Minimum level (e.g. 'error', 'info')
|
|
115
|
+
* @returns {boolean} True if line should be shown
|
|
116
|
+
*/
|
|
117
|
+
function passesLevelFilter(lineLevel, minLevel) {
|
|
118
|
+
if (minLevel === null || minLevel === undefined || minLevel === '') return true;
|
|
119
|
+
const normalized = (minLevel || '').toString().trim().toLowerCase();
|
|
120
|
+
if (!LOG_LEVELS.includes(normalized)) return true;
|
|
121
|
+
const lineRank =
|
|
122
|
+
lineLevel === null || lineLevel === undefined ? LEVEL_RANK.info : (LEVEL_RANK[lineLevel] ?? LEVEL_RANK.info);
|
|
123
|
+
const minRank = LEVEL_RANK[normalized];
|
|
124
|
+
return lineRank >= minRank;
|
|
125
|
+
}
|
|
126
|
+
|
|
53
127
|
/**
|
|
54
128
|
* Dump container env (masked) and print to logger
|
|
55
129
|
* @async
|
|
@@ -75,33 +149,102 @@ async function dumpMaskedEnv(containerName) {
|
|
|
75
149
|
}
|
|
76
150
|
|
|
77
151
|
/**
|
|
78
|
-
* Run docker logs (non-follow): tail N lines or full (tail 0)
|
|
152
|
+
* Run docker logs (non-follow): tail N lines or full (tail 0). If options.level is set, stdout is piped and filtered.
|
|
79
153
|
* @async
|
|
80
154
|
* @param {string} containerName - Docker container name
|
|
81
|
-
* @param {Object} options - { tail: number
|
|
155
|
+
* @param {Object} options - { tail: number, level?: string }
|
|
82
156
|
* @returns {Promise<void>}
|
|
83
157
|
*/
|
|
84
158
|
async function runDockerLogs(containerName, options) {
|
|
85
159
|
const args = options.tail === 0 ? ['logs', containerName] : ['logs', '--tail', String(options.tail), containerName];
|
|
160
|
+
const minLevel =
|
|
161
|
+
options.level !== undefined && options.level !== null && options.level !== ''
|
|
162
|
+
? String(options.level).trim().toLowerCase()
|
|
163
|
+
: null;
|
|
164
|
+
|
|
165
|
+
if (minLevel === null || minLevel === undefined || !LOG_LEVELS.includes(minLevel)) {
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
const proc = spawn('docker', args, { stdio: 'inherit' });
|
|
168
|
+
proc.on('error', reject);
|
|
169
|
+
proc.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`docker logs exited with ${code}`))));
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
86
173
|
return new Promise((resolve, reject) => {
|
|
87
|
-
const proc = spawn('docker', args, { stdio: 'inherit' });
|
|
174
|
+
const proc = spawn('docker', args, { stdio: ['inherit', 'pipe', 'pipe'] });
|
|
88
175
|
proc.on('error', reject);
|
|
89
|
-
|
|
176
|
+
|
|
177
|
+
function onLine(line) {
|
|
178
|
+
if (passesLevelFilter(getLogLevel(line), minLevel)) {
|
|
179
|
+
process.stdout.write(line + '\n');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const rlOut = readline.createInterface({ input: proc.stdout, crlfDelay: Infinity });
|
|
184
|
+
rlOut.on('line', onLine);
|
|
185
|
+
const rlErr = readline.createInterface({ input: proc.stderr, crlfDelay: Infinity });
|
|
186
|
+
rlErr.on('line', onLine);
|
|
187
|
+
|
|
188
|
+
let streamsClosed = 0;
|
|
189
|
+
let exitCode = null;
|
|
190
|
+
function tryFinish() {
|
|
191
|
+
if (streamsClosed < 2 || exitCode === null) return;
|
|
192
|
+
if (exitCode !== 0) reject(new Error(`docker logs exited with ${exitCode}`));
|
|
193
|
+
else resolve();
|
|
194
|
+
}
|
|
195
|
+
rlOut.on('close', () => {
|
|
196
|
+
streamsClosed += 1;
|
|
197
|
+
tryFinish();
|
|
198
|
+
});
|
|
199
|
+
rlErr.on('close', () => {
|
|
200
|
+
streamsClosed += 1;
|
|
201
|
+
tryFinish();
|
|
202
|
+
});
|
|
203
|
+
proc.on('close', (code) => {
|
|
204
|
+
exitCode = code;
|
|
205
|
+
tryFinish();
|
|
206
|
+
});
|
|
90
207
|
});
|
|
91
208
|
}
|
|
92
209
|
|
|
93
210
|
/**
|
|
94
|
-
* Run docker logs --follow (stream), optionally with tail
|
|
211
|
+
* Run docker logs --follow (stream), optionally with tail. If minLevel is set, stdout is piped and filtered.
|
|
95
212
|
* @param {string} containerName - Docker container name
|
|
96
213
|
* @param {number} [tail] - Lines to show (0 = full, omit --tail)
|
|
214
|
+
* @param {string|null} [minLevel] - Minimum log level to show (debug|info|warn|error)
|
|
97
215
|
*/
|
|
98
|
-
function runDockerLogsFollow(containerName, tail) {
|
|
216
|
+
function runDockerLogsFollow(containerName, tail, minLevel) {
|
|
99
217
|
const args = tail === 0 ? ['logs', '-f', containerName] : ['logs', '-f', '--tail', String(tail), containerName];
|
|
100
|
-
const
|
|
218
|
+
const level =
|
|
219
|
+
minLevel !== undefined && minLevel !== null && minLevel !== ''
|
|
220
|
+
? String(minLevel).trim().toLowerCase()
|
|
221
|
+
: null;
|
|
222
|
+
const useFilter = level !== null && level !== undefined && LOG_LEVELS.includes(level);
|
|
223
|
+
|
|
224
|
+
if (!useFilter) {
|
|
225
|
+
const proc = spawn('docker', args, { stdio: 'inherit' });
|
|
226
|
+
proc.on('error', (err) => {
|
|
227
|
+
logger.log(chalk.red(`Error: ${err.message}`));
|
|
228
|
+
process.exit(1);
|
|
229
|
+
});
|
|
230
|
+
proc.on('close', (code) => {
|
|
231
|
+
if (code !== 0 && code !== null) process.exit(code);
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const proc = spawn('docker', args, { stdio: ['inherit', 'pipe', 'pipe'] });
|
|
101
237
|
proc.on('error', (err) => {
|
|
102
238
|
logger.log(chalk.red(`Error: ${err.message}`));
|
|
103
239
|
process.exit(1);
|
|
104
240
|
});
|
|
241
|
+
function onLine(line) {
|
|
242
|
+
if (passesLevelFilter(getLogLevel(line), level)) process.stdout.write(line + '\n');
|
|
243
|
+
}
|
|
244
|
+
const rlOut = readline.createInterface({ input: proc.stdout, crlfDelay: Infinity });
|
|
245
|
+
rlOut.on('line', onLine);
|
|
246
|
+
const rlErr = readline.createInterface({ input: proc.stderr, crlfDelay: Infinity });
|
|
247
|
+
rlErr.on('line', onLine);
|
|
105
248
|
proc.on('close', (code) => {
|
|
106
249
|
if (code !== 0 && code !== null) process.exit(code);
|
|
107
250
|
});
|
|
@@ -114,10 +257,22 @@ function runDockerLogsFollow(containerName, tail) {
|
|
|
114
257
|
* @param {Object} options - CLI options
|
|
115
258
|
* @param {boolean} [options.follow] - Follow log stream (-f)
|
|
116
259
|
* @param {number} [options.tail] - Number of lines (default 100; 0 = full list)
|
|
260
|
+
* @param {string} [options.level] - Show only logs at this level or above (debug|info|warn|error)
|
|
117
261
|
* @returns {Promise<void>}
|
|
118
262
|
*/
|
|
119
263
|
async function runAppLogs(appKey, options = {}) {
|
|
120
264
|
validateAppName(appKey);
|
|
265
|
+
const rawLevel =
|
|
266
|
+
options.level !== undefined && options.level !== null && options.level !== ''
|
|
267
|
+
? String(options.level).trim()
|
|
268
|
+
: undefined;
|
|
269
|
+
const level = rawLevel ? rawLevel.toLowerCase() : undefined;
|
|
270
|
+
if (level !== undefined && level !== null && !LOG_LEVELS.includes(level)) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`Invalid log level '${rawLevel}'; use one of: ${LOG_LEVELS.join(', ')}`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
121
276
|
const developerId = await config.getDeveloperId();
|
|
122
277
|
const containerName = containerHelpers.getContainerName(appKey, developerId);
|
|
123
278
|
|
|
@@ -131,16 +286,16 @@ async function runAppLogs(appKey, options = {}) {
|
|
|
131
286
|
}
|
|
132
287
|
|
|
133
288
|
if (follow) {
|
|
134
|
-
runDockerLogsFollow(containerName, tail);
|
|
289
|
+
runDockerLogsFollow(containerName, tail, level);
|
|
135
290
|
return;
|
|
136
291
|
}
|
|
137
292
|
|
|
138
293
|
try {
|
|
139
|
-
await runDockerLogs(containerName, { tail });
|
|
294
|
+
await runDockerLogs(containerName, { tail, level });
|
|
140
295
|
} catch (err) {
|
|
141
296
|
logger.log(chalk.red(`Error: ${err.message}`));
|
|
142
297
|
throw new Error(`Failed to show logs: ${err.message}`);
|
|
143
298
|
}
|
|
144
299
|
}
|
|
145
300
|
|
|
146
|
-
module.exports = { runAppLogs, maskEnvLine };
|
|
301
|
+
module.exports = { runAppLogs, maskEnvLine, getLogLevel, passesLevelFilter };
|
package/lib/commands/app.js
CHANGED
|
@@ -17,21 +17,11 @@ const { rotateSecret } = require('../app/rotate-secret');
|
|
|
17
17
|
const { showApp } = require('../app/show');
|
|
18
18
|
const { runAppDeploymentList } = require('./deployment-list');
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* @param {Command} program - Commander program instance
|
|
23
|
-
*/
|
|
24
|
-
function setupAppCommands(program) {
|
|
25
|
-
const app = program
|
|
26
|
-
.command('app')
|
|
27
|
-
.description('Manage applications');
|
|
28
|
-
|
|
29
|
-
// Register command (controller and environment from config.yaml)
|
|
30
|
-
app
|
|
31
|
-
.command('register <appKey>')
|
|
20
|
+
function setupAppRegisterCommand(app) {
|
|
21
|
+
app.command('register <appKey>')
|
|
32
22
|
.description('Register application and get pipeline credentials')
|
|
33
|
-
.option('-p, --port <port>', 'Application port (default: from
|
|
34
|
-
.option('-u, --url <url>', 'Application URL. If omitted: app.url, deployment.dataplaneUrl or deployment.appUrl in
|
|
23
|
+
.option('-p, --port <port>', 'Application port (default: from application.yaml)')
|
|
24
|
+
.option('-u, --url <url>', 'Application URL. If omitted: app.url, deployment.dataplaneUrl or deployment.appUrl in application.yaml; else http://localhost:{build.localPort or port}')
|
|
35
25
|
.option('-n, --name <name>', 'Override display name')
|
|
36
26
|
.option('-d, --description <desc>', 'Override description')
|
|
37
27
|
.action(async(appKey, options) => {
|
|
@@ -42,10 +32,10 @@ function setupAppCommands(program) {
|
|
|
42
32
|
process.exit(1);
|
|
43
33
|
}
|
|
44
34
|
});
|
|
35
|
+
}
|
|
45
36
|
|
|
46
|
-
|
|
47
|
-
app
|
|
48
|
-
.command('list')
|
|
37
|
+
function setupAppListCommand(app) {
|
|
38
|
+
app.command('list')
|
|
49
39
|
.description('List applications')
|
|
50
40
|
.action(async(options) => {
|
|
51
41
|
try {
|
|
@@ -55,10 +45,10 @@ function setupAppCommands(program) {
|
|
|
55
45
|
process.exit(1);
|
|
56
46
|
}
|
|
57
47
|
});
|
|
48
|
+
}
|
|
58
49
|
|
|
59
|
-
|
|
60
|
-
app
|
|
61
|
-
.command('rotate-secret <appKey>')
|
|
50
|
+
function setupAppRotateSecretCommand(app) {
|
|
51
|
+
app.command('rotate-secret <appKey>')
|
|
62
52
|
.description('Rotate pipeline ClientSecret for an application')
|
|
63
53
|
.action(async(appKey, options) => {
|
|
64
54
|
try {
|
|
@@ -68,11 +58,12 @@ function setupAppCommands(program) {
|
|
|
68
58
|
process.exit(1);
|
|
69
59
|
}
|
|
70
60
|
});
|
|
61
|
+
}
|
|
71
62
|
|
|
72
|
-
|
|
73
|
-
app
|
|
74
|
-
.command('show <appKey>')
|
|
63
|
+
function setupAppShowCommand(app) {
|
|
64
|
+
app.command('show <appKey>')
|
|
75
65
|
.description('Show application from controller (online). Same as aifabrix show <appKey> --online')
|
|
66
|
+
.option('--online', 'Fetch from controller (default for this command)')
|
|
76
67
|
.option('--json', 'Output as JSON')
|
|
77
68
|
.option('--permissions', 'Show only list of permissions')
|
|
78
69
|
.action(async(appKey, options) => {
|
|
@@ -83,10 +74,10 @@ function setupAppCommands(program) {
|
|
|
83
74
|
process.exit(1);
|
|
84
75
|
}
|
|
85
76
|
});
|
|
77
|
+
}
|
|
86
78
|
|
|
87
|
-
|
|
88
|
-
app
|
|
89
|
-
.command('deployment <appKey>')
|
|
79
|
+
function setupAppDeploymentCommand(app) {
|
|
80
|
+
app.command('deployment <appKey>')
|
|
90
81
|
.description('List last N deployments for an application in current environment (default pageSize=50)')
|
|
91
82
|
.option('--controller <url>', 'Controller URL (default: from config)')
|
|
92
83
|
.option('--environment <env>', 'Environment key (default: from config)')
|
|
@@ -106,5 +97,18 @@ function setupAppCommands(program) {
|
|
|
106
97
|
});
|
|
107
98
|
}
|
|
108
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Setup application management commands
|
|
102
|
+
* @param {Command} program - Commander program instance
|
|
103
|
+
*/
|
|
104
|
+
function setupAppCommands(program) {
|
|
105
|
+
const app = program.command('app').description('Manage applications');
|
|
106
|
+
setupAppRegisterCommand(app);
|
|
107
|
+
setupAppListCommand(app);
|
|
108
|
+
setupAppRotateSecretCommand(app);
|
|
109
|
+
setupAppShowCommand(app);
|
|
110
|
+
setupAppDeploymentCommand(app);
|
|
111
|
+
}
|
|
112
|
+
|
|
109
113
|
module.exports = { setupAppCommands };
|
|
110
114
|
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert command: convert integration/external system and datasource config files between JSON and YAML.
|
|
3
|
+
*
|
|
4
|
+
* Process: validate first, then (unless --force) prompt for confirmation, then convert (write new files),
|
|
5
|
+
* update application config links, then delete old files.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Convert config format (JSON/YAML) for external integration files
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const readline = require('readline');
|
|
17
|
+
const { detectAppType } = require('../utils/paths');
|
|
18
|
+
const { logOfflinePathWhenType } = require('../utils/cli-utils');
|
|
19
|
+
const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
|
|
20
|
+
const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
|
|
21
|
+
|
|
22
|
+
const TARGET_EXT = { yaml: '.yaml', json: '.json' };
|
|
23
|
+
const APP_CONFIG_NAMES = { yaml: 'application.yaml', json: 'application.json' };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Prompts the user for confirmation (y/N).
|
|
27
|
+
*
|
|
28
|
+
* @param {string} message - Prompt message
|
|
29
|
+
* @returns {Promise<boolean>} True if user confirms (y/yes), false otherwise
|
|
30
|
+
*/
|
|
31
|
+
function promptConfirm(message) {
|
|
32
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
33
|
+
return new Promise(resolve => {
|
|
34
|
+
rl.question(message, answer => {
|
|
35
|
+
rl.close();
|
|
36
|
+
const normalized = (answer || '').trim().toLowerCase();
|
|
37
|
+
resolve(normalized === 'y' || normalized === 'yes');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns the target filename for a given file path and format (same base name, new extension).
|
|
44
|
+
*
|
|
45
|
+
* @param {string} filePath - Current file path
|
|
46
|
+
* @param {string} format - Target format: 'json' or 'yaml'
|
|
47
|
+
* @returns {string} New filename (basename only)
|
|
48
|
+
*/
|
|
49
|
+
function targetFileName(filePath, format) {
|
|
50
|
+
const base = path.basename(filePath, path.extname(filePath));
|
|
51
|
+
const ext = TARGET_EXT[format] || (format === 'json' ? '.json' : '.yaml');
|
|
52
|
+
return base + ext;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Converts a single config file to the target format: writes to new path, returns old and new paths.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} sourcePath - Absolute path to existing file
|
|
59
|
+
* @param {string} targetPath - Absolute path for new file
|
|
60
|
+
* @param {string} format - 'json' or 'yaml'
|
|
61
|
+
* @returns {{ oldPath: string, newPath: string }}
|
|
62
|
+
*/
|
|
63
|
+
function convertOneFile(sourcePath, targetPath, format) {
|
|
64
|
+
const obj = loadConfigFile(sourcePath);
|
|
65
|
+
writeConfigFile(targetPath, obj, format);
|
|
66
|
+
return { oldPath: sourcePath, newPath: targetPath };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Converts a list of config files (system or datasource) in a directory; skips missing files.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} schemaBasePath - Base directory for files
|
|
73
|
+
* @param {string[]} fileNames - List of filenames
|
|
74
|
+
* @param {string} format - 'json' or 'yaml'
|
|
75
|
+
* @returns {{ converted: string[], toDelete: string[], newNames: string[] }}
|
|
76
|
+
*/
|
|
77
|
+
function convertFileList(schemaBasePath, fileNames, format) {
|
|
78
|
+
const converted = [];
|
|
79
|
+
const toDelete = [];
|
|
80
|
+
const newNames = [];
|
|
81
|
+
for (const fileName of fileNames) {
|
|
82
|
+
const sourcePath = path.join(schemaBasePath, fileName);
|
|
83
|
+
if (!fs.existsSync(sourcePath)) {
|
|
84
|
+
newNames.push(fileName);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const newFileName = targetFileName(sourcePath, format);
|
|
88
|
+
const targetPath = path.join(schemaBasePath, newFileName);
|
|
89
|
+
convertOneFile(sourcePath, targetPath, format);
|
|
90
|
+
converted.push(targetPath);
|
|
91
|
+
newNames.push(newFileName);
|
|
92
|
+
if (path.normalize(sourcePath) !== path.normalize(targetPath)) {
|
|
93
|
+
toDelete.push(sourcePath);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { converted, toDelete, newNames };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validates app and prompts for confirmation unless opts.force.
|
|
101
|
+
*
|
|
102
|
+
* @param {Object} opts - Options
|
|
103
|
+
* @param {string} opts.appName - Application name
|
|
104
|
+
* @param {Object} opts.cmdOptions - Command options (force, type)
|
|
105
|
+
* @param {string} opts.schemaBasePath - Base path for external files
|
|
106
|
+
* @param {string[]} opts.systemFiles - System file names
|
|
107
|
+
* @param {string[]} opts.datasourceFiles - Datasource file names
|
|
108
|
+
* @param {string} opts.configPath - Current application config path
|
|
109
|
+
* @param {string} opts.format - Target format
|
|
110
|
+
* @throws {Error} If validation fails or user cancels
|
|
111
|
+
*/
|
|
112
|
+
async function validateAndPrompt(opts) {
|
|
113
|
+
const validate = require('../validation/validate');
|
|
114
|
+
const result = await validate.validateAppOrFile(opts.appName, opts.cmdOptions);
|
|
115
|
+
if (!result.valid) {
|
|
116
|
+
validate.displayValidationResults(result);
|
|
117
|
+
throw new Error('Validation failed. Fix errors before converting.');
|
|
118
|
+
}
|
|
119
|
+
const { configPath, format, schemaBasePath, systemFiles, datasourceFiles } = opts;
|
|
120
|
+
const appConfigName = APP_CONFIG_NAMES[format];
|
|
121
|
+
const targetConfigPath = path.join(path.dirname(configPath), appConfigName);
|
|
122
|
+
const willConvertAppConfig = path.normalize(configPath) !== path.normalize(targetConfigPath) ||
|
|
123
|
+
path.extname(configPath) !== (format === 'json' ? '.json' : '.yaml');
|
|
124
|
+
const summaryLines = [...systemFiles, ...datasourceFiles]
|
|
125
|
+
.filter(Boolean)
|
|
126
|
+
.map(f => ` • ${f} → ${targetFileName(path.join(schemaBasePath, f), format)}`);
|
|
127
|
+
if (willConvertAppConfig) summaryLines.push(` • application config → ${appConfigName}`);
|
|
128
|
+
summaryLines.push(' Old files will be removed after writing new ones.');
|
|
129
|
+
if (!opts.cmdOptions.force) {
|
|
130
|
+
const confirmed = await promptConfirm(`Convert the following to ${format}?\n${summaryLines.join('\n')}\nAre you sure? (y/N) `);
|
|
131
|
+
if (!confirmed) throw new Error('Convert cancelled.');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Converts files, updates application config, and deletes old files.
|
|
137
|
+
*
|
|
138
|
+
* @param {Object} variables - Current application config
|
|
139
|
+
* @param {string} configPath - Current application config path
|
|
140
|
+
* @param {string} schemaBasePath - Base path for external files
|
|
141
|
+
* @param {string[]} systemFiles - System file names
|
|
142
|
+
* @param {string[]} datasourceFiles - Datasource file names
|
|
143
|
+
* @param {string} format - Target format
|
|
144
|
+
* @returns {{ converted: string[], deleted: string[] }}
|
|
145
|
+
*/
|
|
146
|
+
function executeConversion(variables, configPath, schemaBasePath, systemFiles, datasourceFiles, format) {
|
|
147
|
+
const sys = convertFileList(schemaBasePath, systemFiles, format);
|
|
148
|
+
const ds = convertFileList(schemaBasePath, datasourceFiles, format);
|
|
149
|
+
const converted = [...sys.converted, ...ds.converted];
|
|
150
|
+
const toDelete = [...sys.toDelete, ...ds.toDelete];
|
|
151
|
+
const updatedVariables = { ...variables };
|
|
152
|
+
if (updatedVariables.externalIntegration) {
|
|
153
|
+
updatedVariables.externalIntegration = { ...updatedVariables.externalIntegration };
|
|
154
|
+
updatedVariables.externalIntegration.systems = sys.newNames;
|
|
155
|
+
updatedVariables.externalIntegration.dataSources = ds.newNames;
|
|
156
|
+
}
|
|
157
|
+
const appConfigName = APP_CONFIG_NAMES[format];
|
|
158
|
+
const targetConfigPath = path.join(path.dirname(configPath), appConfigName);
|
|
159
|
+
writeConfigFile(targetConfigPath, updatedVariables, format);
|
|
160
|
+
converted.push(targetConfigPath);
|
|
161
|
+
if (path.normalize(configPath) !== path.normalize(targetConfigPath)) toDelete.push(configPath);
|
|
162
|
+
toDelete.forEach(oldPath => {
|
|
163
|
+
if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
|
|
164
|
+
});
|
|
165
|
+
return { converted, deleted: toDelete };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Runs conversion: validate first, optional prompt, convert files, update app config links, delete old files.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} appName - Application name
|
|
172
|
+
* @param {Object} options - Command options
|
|
173
|
+
* @param {string} options.format - Target format: 'json' or 'yaml'
|
|
174
|
+
* @param {boolean} [options.force] - Skip confirmation prompt
|
|
175
|
+
*
|
|
176
|
+
* @returns {Promise<{ converted: string[], deleted: string[] }>} Lists of converted and deleted file paths
|
|
177
|
+
* @throws {Error} If validation fails, user aborts, or conversion fails
|
|
178
|
+
*/
|
|
179
|
+
async function runConvert(appName, options) {
|
|
180
|
+
const format = (options.format || '').toLowerCase();
|
|
181
|
+
if (format !== 'json' && format !== 'yaml') {
|
|
182
|
+
throw new Error('Option --format is required and must be \'json\' or \'yaml\'');
|
|
183
|
+
}
|
|
184
|
+
const { appPath } = await detectAppType(appName);
|
|
185
|
+
logOfflinePathWhenType(appPath);
|
|
186
|
+
const configPath = resolveApplicationConfigPath(appPath);
|
|
187
|
+
const variables = loadConfigFile(configPath);
|
|
188
|
+
const schemaBasePath = path.resolve(path.dirname(configPath), variables.externalIntegration?.schemaBasePath || './');
|
|
189
|
+
const systemFiles = variables.externalIntegration?.systems || [];
|
|
190
|
+
const datasourceFiles = variables.externalIntegration?.dataSources || [];
|
|
191
|
+
await validateAndPrompt({
|
|
192
|
+
appName, cmdOptions: options, schemaBasePath, systemFiles, datasourceFiles, configPath, format
|
|
193
|
+
});
|
|
194
|
+
return executeConversion(variables, configPath, schemaBasePath, systemFiles, datasourceFiles, format);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
runConvert,
|
|
199
|
+
promptConfirm,
|
|
200
|
+
targetFileName,
|
|
201
|
+
convertOneFile
|
|
202
|
+
};
|