@aifabrix/builder 2.39.1 → 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
 
package/lib/app/readme.js CHANGED
@@ -12,6 +12,7 @@ const fs = require('fs').promises;
12
12
  const fsSync = require('fs');
13
13
  const path = require('path');
14
14
  const handlebars = require('handlebars');
15
+ const yaml = require('js-yaml');
15
16
  const { generateExternalReadmeContent } = require('../utils/external-readme');
16
17
 
17
18
  /**
@@ -158,28 +159,69 @@ function generateReadmeMd(appName, config) {
158
159
  }
159
160
 
160
161
  /**
161
- * Generates README.md file if it doesn't exist
162
+ * Generates README.md file (optionally only if missing)
162
163
  * @async
163
164
  * @function generateReadmeMdFile
164
165
  * @param {string} appPath - Path to application directory
165
166
  * @param {string} appName - Application name
166
- * @param {Object} config - Application configuration
167
+ * @param {Object} config - Application configuration (e.g. from variables.yaml)
168
+ * @param {Object} [options] - Options
169
+ * @param {boolean} [options.force] - If true, overwrite existing README.md (dynamic generation)
167
170
  * @returns {Promise<void>} Resolves when README.md is generated or skipped
168
171
  * @throws {Error} If file generation fails
169
172
  */
170
- async function generateReadmeMdFile(appPath, appName, config) {
171
- // Ensure directory exists
173
+ async function generateReadmeMdFile(appPath, appName, config, options = {}) {
172
174
  await fs.mkdir(appPath, { recursive: true });
173
175
  const readmePath = path.join(appPath, 'README.md');
174
- if (!(await fileExists(readmePath))) {
175
- const readmeContent = generateReadmeMd(appName, config);
176
- await fs.writeFile(readmePath, readmeContent, 'utf8');
176
+ if (!options.force && (await fileExists(readmePath))) {
177
+ return;
178
+ }
179
+ const readmeContent = generateReadmeMd(appName, config);
180
+ await fs.writeFile(readmePath, readmeContent, 'utf8');
181
+ }
182
+
183
+ /**
184
+ * Loads variables.yaml from app path and generates README.md (overwrites if present).
185
+ * Used when copying template apps or running up-miso / up-platform / up-dataplane.
186
+ * @async
187
+ * @function ensureReadmeForAppPath
188
+ * @param {string} appPath - Path to application directory (must contain variables.yaml)
189
+ * @param {string} appName - Application name
190
+ * @returns {Promise<void>} Resolves when README.md is written or skipped (no variables.yaml)
191
+ */
192
+ async function ensureReadmeForAppPath(appPath, appName) {
193
+ const variablesPath = path.join(appPath, 'variables.yaml');
194
+ if (!(await fileExists(variablesPath))) {
195
+ return;
196
+ }
197
+ const content = await fs.readFile(variablesPath, 'utf8');
198
+ const config = yaml.load(content) || {};
199
+ await generateReadmeMdFile(appPath, appName, config, { force: true });
200
+ }
201
+
202
+ /**
203
+ * Generates README.md for an app at builder path(s): primary and cwd/builder if different.
204
+ * Use after ensureAppFromTemplate in up-miso / up-dataplane so README reflects current config.
205
+ * @async
206
+ * @function ensureReadmeForApp
207
+ * @param {string} appName - Application name (e.g. keycloak, miso-controller, dataplane)
208
+ * @returns {Promise<void>}
209
+ */
210
+ async function ensureReadmeForApp(appName) {
211
+ const pathsUtil = require('../utils/paths');
212
+ const primaryPath = pathsUtil.getBuilderPath(appName);
213
+ await ensureReadmeForAppPath(primaryPath, appName);
214
+ const cwdBuilderPath = path.join(process.cwd(), 'builder', appName);
215
+ if (path.resolve(cwdBuilderPath) !== path.resolve(primaryPath)) {
216
+ await ensureReadmeForAppPath(cwdBuilderPath, appName);
177
217
  }
178
218
  }
179
219
 
180
220
  module.exports = {
181
221
  generateReadmeMdFile,
182
222
  generateReadmeMd,
183
- formatAppDisplayName
223
+ formatAppDisplayName,
224
+ ensureReadmeForAppPath,
225
+ ensureReadmeForApp
184
226
  };
185
227
 
@@ -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 };
@@ -15,9 +15,11 @@ const chalk = require('chalk');
15
15
  const logger = require('../utils/logger');
16
16
  const pathsUtil = require('../utils/paths');
17
17
  const { copyTemplateFiles } = require('../validation/template');
18
+ const { ensureReadmeForAppPath, ensureReadmeForApp } = require('../app/readme');
18
19
 
19
20
  /**
20
21
  * Copy template to a target path if variables.yaml is missing there.
22
+ * After copy, generates README.md from templates/applications/README.md.hbs.
21
23
  * @param {string} appName - Application name
22
24
  * @param {string} targetAppPath - Target directory (e.g. builder/keycloak)
23
25
  * @returns {Promise<boolean>} True if template was copied, false if already present
@@ -28,6 +30,7 @@ async function ensureTemplateAtPath(appName, targetAppPath) {
28
30
  return false;
29
31
  }
30
32
  await copyTemplateFiles(appName, targetAppPath);
33
+ await ensureReadmeForAppPath(targetAppPath, appName);
31
34
  return true;
32
35
  }
33
36
 
@@ -160,6 +163,7 @@ async function ensureAppFromTemplate(appName) {
160
163
  }
161
164
  }
162
165
 
166
+ await ensureReadmeForApp(appName);
163
167
  return primaryCopied;
164
168
  }
165
169
 
@@ -35,13 +35,19 @@ async function generateExternalSystemTemplate(appPath, systemKey, config) {
35
35
  const templateContent = await fs.readFile(templatePath, 'utf8');
36
36
  const template = handlebars.compile(templateContent);
37
37
 
38
+ const roles = (config.roles || null) && config.roles.map((role) => ({
39
+ ...role,
40
+ groups: role.groups || role.Groups || undefined
41
+ }));
42
+
38
43
  const context = {
39
44
  systemKey: systemKey,
40
45
  systemDisplayName: config.systemDisplayName || systemKey.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
41
46
  systemDescription: config.systemDescription || `External system integration for ${systemKey}`,
42
47
  systemType: config.systemType || 'openapi',
43
48
  authType: config.authType || 'apikey',
44
- roles: config.roles || null,
49
+ baseUrl: config.baseUrl || null,
50
+ roles: roles || null,
45
51
  permissions: config.permissions || null
46
52
  };
47
53
 
@@ -74,6 +80,8 @@ async function generateExternalDataSourceTemplate(appPath, datasourceKey, config
74
80
  const templateContent = await fs.readFile(templatePath, 'utf8');
75
81
  const template = handlebars.compile(templateContent);
76
82
 
83
+ const dimensions = config.dimensions || {};
84
+ const attributes = config.attributes || {};
77
85
  const context = {
78
86
  datasourceKey: datasourceKey,
79
87
  datasourceDisplayName: config.datasourceDisplayName || datasourceKey.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
@@ -82,8 +90,11 @@ async function generateExternalDataSourceTemplate(appPath, datasourceKey, config
82
90
  entityType: config.entityType || datasourceKey.split('-').pop(),
83
91
  resourceType: config.resourceType || 'document',
84
92
  systemType: config.systemType || 'openapi',
85
- dimensions: config.dimensions || {},
86
- attributes: config.attributes || {}
93
+ // Pass non-empty objects so template uses custom block; empty/null so template uses schema-valid defaults
94
+ dimensions: Object.keys(dimensions).length > 0 ? dimensions : null,
95
+ attributes: Object.keys(attributes).length > 0 ? attributes : null,
96
+ // Literal expression strings for default attribute block (schema: pipe-based DSL {{raw.path}})
97
+ raw: { id: '{{raw.id}}', name: '{{raw.name}}' }
87
98
  };
88
99
 
89
100
  const rendered = template(context);
@@ -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