@aifabrix/builder 2.39.2 → 2.39.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -103,7 +103,7 @@ All guides and references are listed in **[docs/README.md](docs/README.md)** (ta
103
103
 
104
104
  - [CLI Reference](docs/cli-reference.md) – All commands
105
105
  - [Infrastructure](docs/infrastructure.md) – What runs and why
106
- - [Configuration](docs/configuration.md) – Config files
106
+ - [Configuration reference](docs/configuration/README.md) – Config files (deployment key, variables.yaml, env.template, secrets)
107
107
 
108
108
  ---
109
109
 
@@ -177,9 +177,9 @@ async function registerApplication(appKey, options = {}) {
177
177
  registrationData
178
178
  );
179
179
 
180
- // Save credentials and display results
180
+ // Save credentials and display results (pass display name we sent so output shows it when API returns key as displayName)
181
181
  await saveLocalCredentials(responseData, authConfig.apiUrl);
182
- displayRegistrationResults(responseData, authConfig.apiUrl, environment);
182
+ displayRegistrationResults(responseData, authConfig.apiUrl, environment, registrationData.displayName);
183
183
  }
184
184
 
185
185
  module.exports = { registerApplication, getEnvironmentPrefix };
@@ -187,13 +187,16 @@ See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`)
187
187
  .description('Show application container logs (and optional env summary with secrets masked)')
188
188
  .option('-f', 'Follow log stream')
189
189
  .option('-t, --tail <lines>', 'Number of lines (default: 100); 0 = full list', '100')
190
+ .option('-l, --level <level>', 'Show only logs at this level or above (debug|info|warn|error)')
190
191
  .action(async(appName, options) => {
191
192
  try {
192
193
  const { runAppLogs } = require('../commands/app-logs');
193
194
  const tailNum = parseInt(options.tail, 10);
195
+ const level = options.level !== undefined && options.level !== null && options.level !== '' ? String(options.level).trim() : undefined;
194
196
  await runAppLogs(appName, {
195
197
  follow: options.f,
196
- tail: Number.isNaN(tailNum) ? 100 : tailNum
198
+ tail: Number.isNaN(tailNum) ? 100 : tailNum,
199
+ level
197
200
  });
198
201
  } catch (error) {
199
202
  handleCommandError(error, 'logs');
@@ -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,24 @@ 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 */
39
+ const LEVEL_JSON_REGEX = /"level"\s*:\s*"(\w+)"/i;
40
+
22
41
  /** Env key patterns that indicate a secret (mask value) */
23
42
  const SECRET_KEY_PATTERN = /password|secret|token|credential|api[_-]?key/i;
24
43
 
@@ -50,6 +69,53 @@ function maskEnvLine(line) {
50
69
  return line;
51
70
  }
52
71
 
72
+ /**
73
+ * Extract log level from a line (prefix like INFO:/error: or JSON "level":"info").
74
+ * Supports: INFO:, ERROR:, error:, info: (miso-controller/pino), WARN:, DEBUG:, and "level":"x" in JSON.
75
+ * @param {string} line - Single log line
76
+ * @returns {string|null} One of 'debug'|'info'|'warn'|'error', or null if not parseable
77
+ */
78
+ function getLogLevel(line) {
79
+ if (!line || typeof line !== 'string') return null;
80
+ const prefixMatch = line.match(LEVEL_PREFIX_REGEX);
81
+ if (prefixMatch) {
82
+ const raw = prefixMatch[1].toLowerCase();
83
+ return raw === 'warning' ? 'warn' : raw;
84
+ }
85
+ const afterPrefixMatch = line.match(LEVEL_AFTER_PREFIX_REGEX);
86
+ if (afterPrefixMatch) {
87
+ const raw = afterPrefixMatch[1].toLowerCase();
88
+ return raw === 'warning' ? 'warn' : raw;
89
+ }
90
+ const wordBoundaryMatch = line.match(LEVEL_WORD_BOUNDARY_REGEX);
91
+ if (wordBoundaryMatch) {
92
+ const raw = wordBoundaryMatch[1].toLowerCase();
93
+ return raw === 'warning' ? 'warn' : raw;
94
+ }
95
+ const jsonMatch = line.match(LEVEL_JSON_REGEX);
96
+ if (jsonMatch) {
97
+ const raw = jsonMatch[1].toLowerCase();
98
+ return raw === 'warning' ? 'warn' : raw;
99
+ }
100
+ return null;
101
+ }
102
+
103
+ /**
104
+ * Whether a line's level passes the minimum level filter (show this level and above).
105
+ * @param {string|null} lineLevel - Level from getLogLevel (null treated as 'info')
106
+ * @param {string|undefined|null} minLevel - Minimum level (e.g. 'error', 'info')
107
+ * @returns {boolean} True if line should be shown
108
+ */
109
+ function passesLevelFilter(lineLevel, minLevel) {
110
+ if (minLevel === null || minLevel === undefined || minLevel === '') return true;
111
+ const normalized = (minLevel || '').toString().trim().toLowerCase();
112
+ if (!LOG_LEVELS.includes(normalized)) return true;
113
+ const lineRank =
114
+ lineLevel === null || lineLevel === undefined ? LEVEL_RANK.info : (LEVEL_RANK[lineLevel] ?? LEVEL_RANK.info);
115
+ const minRank = LEVEL_RANK[normalized];
116
+ return lineRank >= minRank;
117
+ }
118
+
53
119
  /**
54
120
  * Dump container env (masked) and print to logger
55
121
  * @async
@@ -75,33 +141,102 @@ async function dumpMaskedEnv(containerName) {
75
141
  }
76
142
 
77
143
  /**
78
- * Run docker logs (non-follow): tail N lines or full (tail 0)
144
+ * Run docker logs (non-follow): tail N lines or full (tail 0). If options.level is set, stdout is piped and filtered.
79
145
  * @async
80
146
  * @param {string} containerName - Docker container name
81
- * @param {Object} options - { tail: number } (0 = full, no limit)
147
+ * @param {Object} options - { tail: number, level?: string }
82
148
  * @returns {Promise<void>}
83
149
  */
84
150
  async function runDockerLogs(containerName, options) {
85
151
  const args = options.tail === 0 ? ['logs', containerName] : ['logs', '--tail', String(options.tail), containerName];
152
+ const minLevel =
153
+ options.level !== undefined && options.level !== null && options.level !== ''
154
+ ? String(options.level).trim().toLowerCase()
155
+ : null;
156
+
157
+ if (minLevel === null || minLevel === undefined || !LOG_LEVELS.includes(minLevel)) {
158
+ return new Promise((resolve, reject) => {
159
+ const proc = spawn('docker', args, { stdio: 'inherit' });
160
+ proc.on('error', reject);
161
+ proc.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`docker logs exited with ${code}`))));
162
+ });
163
+ }
164
+
86
165
  return new Promise((resolve, reject) => {
87
- const proc = spawn('docker', args, { stdio: 'inherit' });
166
+ const proc = spawn('docker', args, { stdio: ['inherit', 'pipe', 'pipe'] });
88
167
  proc.on('error', reject);
89
- proc.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`docker logs exited with ${code}`))));
168
+
169
+ function onLine(line) {
170
+ if (passesLevelFilter(getLogLevel(line), minLevel)) {
171
+ process.stdout.write(line + '\n');
172
+ }
173
+ }
174
+
175
+ const rlOut = readline.createInterface({ input: proc.stdout, crlfDelay: Infinity });
176
+ rlOut.on('line', onLine);
177
+ const rlErr = readline.createInterface({ input: proc.stderr, crlfDelay: Infinity });
178
+ rlErr.on('line', onLine);
179
+
180
+ let streamsClosed = 0;
181
+ let exitCode = null;
182
+ function tryFinish() {
183
+ if (streamsClosed < 2 || exitCode === null) return;
184
+ if (exitCode !== 0) reject(new Error(`docker logs exited with ${exitCode}`));
185
+ else resolve();
186
+ }
187
+ rlOut.on('close', () => {
188
+ streamsClosed += 1;
189
+ tryFinish();
190
+ });
191
+ rlErr.on('close', () => {
192
+ streamsClosed += 1;
193
+ tryFinish();
194
+ });
195
+ proc.on('close', (code) => {
196
+ exitCode = code;
197
+ tryFinish();
198
+ });
90
199
  });
91
200
  }
92
201
 
93
202
  /**
94
- * Run docker logs --follow (stream), optionally with tail
203
+ * Run docker logs --follow (stream), optionally with tail. If minLevel is set, stdout is piped and filtered.
95
204
  * @param {string} containerName - Docker container name
96
205
  * @param {number} [tail] - Lines to show (0 = full, omit --tail)
206
+ * @param {string|null} [minLevel] - Minimum log level to show (debug|info|warn|error)
97
207
  */
98
- function runDockerLogsFollow(containerName, tail) {
208
+ function runDockerLogsFollow(containerName, tail, minLevel) {
99
209
  const args = tail === 0 ? ['logs', '-f', containerName] : ['logs', '-f', '--tail', String(tail), containerName];
100
- const proc = spawn('docker', args, { stdio: 'inherit' });
210
+ const level =
211
+ minLevel !== undefined && minLevel !== null && minLevel !== ''
212
+ ? String(minLevel).trim().toLowerCase()
213
+ : null;
214
+ const useFilter = level !== null && level !== undefined && LOG_LEVELS.includes(level);
215
+
216
+ if (!useFilter) {
217
+ const proc = spawn('docker', args, { stdio: 'inherit' });
218
+ proc.on('error', (err) => {
219
+ logger.log(chalk.red(`Error: ${err.message}`));
220
+ process.exit(1);
221
+ });
222
+ proc.on('close', (code) => {
223
+ if (code !== 0 && code !== null) process.exit(code);
224
+ });
225
+ return;
226
+ }
227
+
228
+ const proc = spawn('docker', args, { stdio: ['inherit', 'pipe', 'pipe'] });
101
229
  proc.on('error', (err) => {
102
230
  logger.log(chalk.red(`Error: ${err.message}`));
103
231
  process.exit(1);
104
232
  });
233
+ function onLine(line) {
234
+ if (passesLevelFilter(getLogLevel(line), level)) process.stdout.write(line + '\n');
235
+ }
236
+ const rlOut = readline.createInterface({ input: proc.stdout, crlfDelay: Infinity });
237
+ rlOut.on('line', onLine);
238
+ const rlErr = readline.createInterface({ input: proc.stderr, crlfDelay: Infinity });
239
+ rlErr.on('line', onLine);
105
240
  proc.on('close', (code) => {
106
241
  if (code !== 0 && code !== null) process.exit(code);
107
242
  });
@@ -114,10 +249,22 @@ function runDockerLogsFollow(containerName, tail) {
114
249
  * @param {Object} options - CLI options
115
250
  * @param {boolean} [options.follow] - Follow log stream (-f)
116
251
  * @param {number} [options.tail] - Number of lines (default 100; 0 = full list)
252
+ * @param {string} [options.level] - Show only logs at this level or above (debug|info|warn|error)
117
253
  * @returns {Promise<void>}
118
254
  */
119
255
  async function runAppLogs(appKey, options = {}) {
120
256
  validateAppName(appKey);
257
+ const rawLevel =
258
+ options.level !== undefined && options.level !== null && options.level !== ''
259
+ ? String(options.level).trim()
260
+ : undefined;
261
+ const level = rawLevel ? rawLevel.toLowerCase() : undefined;
262
+ if (level !== undefined && level !== null && !LOG_LEVELS.includes(level)) {
263
+ throw new Error(
264
+ `Invalid log level '${rawLevel}'; use one of: ${LOG_LEVELS.join(', ')}`
265
+ );
266
+ }
267
+
121
268
  const developerId = await config.getDeveloperId();
122
269
  const containerName = containerHelpers.getContainerName(appKey, developerId);
123
270
 
@@ -131,16 +278,16 @@ async function runAppLogs(appKey, options = {}) {
131
278
  }
132
279
 
133
280
  if (follow) {
134
- runDockerLogsFollow(containerName, tail);
281
+ runDockerLogsFollow(containerName, tail, level);
135
282
  return;
136
283
  }
137
284
 
138
285
  try {
139
- await runDockerLogs(containerName, { tail });
286
+ await runDockerLogs(containerName, { tail, level });
140
287
  } catch (err) {
141
288
  logger.log(chalk.red(`Error: ${err.message}`));
142
289
  throw new Error(`Failed to show logs: ${err.message}`);
143
290
  }
144
291
  }
145
292
 
146
- module.exports = { runAppLogs, maskEnvLine };
293
+ module.exports = { runAppLogs, maskEnvLine, getLogLevel, passesLevelFilter };
@@ -49,7 +49,6 @@ application:
49
49
  overridablePaths:
50
50
  - configuration.items.value
51
51
  - authentication.endpoints
52
- - deployment.controllerUrl
53
52
  - healthCheck.interval
54
53
  - healthCheck.probeIntervalInSeconds
55
54
 
@@ -60,7 +59,6 @@ externalSystem:
60
59
  - description
61
60
  - type
62
61
  - enabled
63
- - environment
64
62
  - authentication
65
63
  - openapi
66
64
  - mcp
@@ -75,8 +73,6 @@ externalSystem:
75
73
  - generateMcpContract
76
74
  - generateOpenApiContract
77
75
  overridablePaths:
78
- - environment.baseUrl
79
- - environment.region
80
76
  - authentication.oauth2
81
77
  - authentication.apikey
82
78
  - authentication.basic
@@ -37,18 +37,37 @@ function getEnvironmentPrefix(environment) {
37
37
  return upper.length <= 4 ? upper : upper.substring(0, 4);
38
38
  }
39
39
 
40
+ /**
41
+ * Resolve display name for output: use API value only when it is a real display name (not the key).
42
+ * @param {Object} application - Application from API response
43
+ * @param {string} [requestedDisplayName] - Display name sent in registration payload
44
+ * @returns {string} Display name to show
45
+ */
46
+ function resolveDisplayName(application, requestedDisplayName) {
47
+ const apiDisplayName = application.displayName;
48
+ const key = application.key;
49
+ // If API returned a display name and it differs from key, use it
50
+ if (apiDisplayName && apiDisplayName !== key) {
51
+ return apiDisplayName;
52
+ }
53
+ // Otherwise use the display name we sent, or fall back to key
54
+ return requestedDisplayName || key;
55
+ }
56
+
40
57
  /**
41
58
  * Display registration success and credentials
42
59
  * @param {Object} data - Registration response data
43
60
  * @param {string} apiUrl - API URL
44
61
  * @param {string} environment - Environment key
62
+ * @param {string} [requestedDisplayName] - Display name sent in registration (used when API echoes key as displayName)
45
63
  */
46
- function displayRegistrationResults(data, apiUrl, environment) {
64
+ function displayRegistrationResults(data, apiUrl, environment, requestedDisplayName) {
65
+ const displayName = resolveDisplayName(data.application, requestedDisplayName);
47
66
  logger.log(chalk.green('✅ Application registered successfully!\n'));
48
67
  logger.log(chalk.bold('📋 Application Details:'));
49
68
  logger.log(` ID: ${data.application.id}`);
50
69
  logger.log(` Key: ${data.application.key}`);
51
- logger.log(` Display Name: ${data.application.displayName}`);
70
+ logger.log(` Display Name: ${displayName}`);
52
71
  logger.log(` Controller: ${apiUrl}\n`);
53
72
 
54
73
  logger.log(chalk.bold.yellow('🔑 CREDENTIALS (save these immediately):'));
@@ -66,5 +85,5 @@ function displayRegistrationResults(data, apiUrl, environment) {
66
85
  logger.log(chalk.cyan(` ${envPrefix}_MISO_CLIENTSECRET = ${data.credentials.clientSecret}\n`));
67
86
  }
68
87
 
69
- module.exports = { displayRegistrationResults, getEnvironmentPrefix };
88
+ module.exports = { displayRegistrationResults, getEnvironmentPrefix, resolveDisplayName };
70
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.39.2",
3
+ "version": "2.39.3",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {