@aifabrix/builder 2.0.2 → 2.0.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
@@ -1,9 +1,7 @@
1
- # 🧱 @aifabrix/builder
2
-
3
- [![npm version](https://img.shields.io/npm/v/%40aifabrix%2Fbuilder.svg)](https://www.npmjs.com/package/%40aifabrix%2Fbuilder)
1
+ # AI Fabrix - Builder SDK
4
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@aifabrix/builder.svg)](https://www.npmjs.com/package/@aifabrix/builder)
5
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
-
7
5
  Local development infrastructure + Azure deployment tool.
8
6
 
9
7
  ## Install
package/lib/app-run.js CHANGED
@@ -15,7 +15,6 @@ const path = require('path');
15
15
  const net = require('net');
16
16
  const chalk = require('chalk');
17
17
  const yaml = require('js-yaml');
18
- const handlebars = require('handlebars');
19
18
  const { exec } = require('child_process');
20
19
  const { promisify } = require('util');
21
20
  const validator = require('./validator');
@@ -23,6 +22,7 @@ const infra = require('./infra');
23
22
  const secrets = require('./secrets');
24
23
  const logger = require('./utils/logger');
25
24
  const { waitForHealthCheck } = require('./utils/health-check');
25
+ const composeGenerator = require('./utils/compose-generator');
26
26
 
27
27
  const execAsync = promisify(exec);
28
28
 
@@ -89,22 +89,6 @@ async function checkPortAvailable(port) {
89
89
  });
90
90
  }
91
91
 
92
- /**
93
- * Loads and compiles Docker Compose template
94
- * @param {string} language - Language type
95
- * @returns {Function} Compiled Handlebars template
96
- * @throws {Error} If template not found
97
- */
98
- function loadDockerComposeTemplate(language) {
99
- const templatePath = path.join(__dirname, '..', 'templates', language, 'docker-compose.hbs');
100
- if (!fsSync.existsSync(templatePath)) {
101
- throw new Error(`Docker Compose template not found for language: ${language}`);
102
- }
103
-
104
- const templateContent = fsSync.readFileSync(templatePath, 'utf8');
105
- return handlebars.compile(templateContent);
106
- }
107
-
108
92
  /**
109
93
  * Extracts image name from configuration (same logic as build.js)
110
94
  * @param {Object} config - Application configuration
@@ -112,146 +96,7 @@ function loadDockerComposeTemplate(language) {
112
96
  * @returns {string} Image name
113
97
  */
114
98
  function getImageName(config, appName) {
115
- if (typeof config.image === 'string') {
116
- return config.image.split(':')[0];
117
- } else if (config.image?.name) {
118
- return config.image.name;
119
- } else if (config.app?.key) {
120
- return config.app.key;
121
- }
122
- return appName;
123
- }
124
-
125
- /**
126
- * Builds app configuration section
127
- * @param {string} appName - Application name
128
- * @param {Object} config - Application configuration
129
- * @returns {Object} App configuration
130
- */
131
- function buildAppConfig(appName, config) {
132
- return {
133
- key: appName,
134
- name: config.displayName || appName
135
- };
136
- }
137
-
138
- /**
139
- * Builds image configuration section
140
- * @param {Object} config - Application configuration
141
- * @param {string} appName - Application name
142
- * @returns {Object} Image configuration
143
- */
144
- function buildImageConfig(config, appName) {
145
- const imageName = getImageName(config, appName);
146
- const imageTag = config.image?.tag || 'latest';
147
- return {
148
- name: imageName,
149
- tag: imageTag
150
- };
151
- }
152
-
153
- /**
154
- * Builds health check configuration section
155
- * @param {Object} config - Application configuration
156
- * @returns {Object} Health check configuration
157
- */
158
- function buildHealthCheckConfig(config) {
159
- return {
160
- path: config.healthCheck?.path || '/health',
161
- interval: config.healthCheck?.interval || 30
162
- };
163
- }
164
-
165
- /**
166
- * Builds requires configuration section
167
- * @param {Object} config - Application configuration
168
- * @returns {Object} Requires configuration
169
- */
170
- function buildRequiresConfig(config) {
171
- return {
172
- requiresDatabase: config.requires?.database || config.services?.database || false,
173
- requiresStorage: config.requires?.storage || config.services?.storage || false,
174
- requiresRedis: config.requires?.redis || config.services?.redis || false
175
- };
176
- }
177
-
178
- /**
179
- * Builds service configuration for template data
180
- * @param {string} appName - Application name
181
- * @param {Object} config - Application configuration
182
- * @param {number} port - Application port
183
- * @returns {Object} Service configuration
184
- */
185
- function buildServiceConfig(appName, config, port) {
186
- const containerPort = config.build?.containerPort || config.port || 3000;
187
-
188
- return {
189
- app: buildAppConfig(appName, config),
190
- image: buildImageConfig(config, appName),
191
- port: containerPort,
192
- build: {
193
- localPort: port
194
- },
195
- healthCheck: buildHealthCheckConfig(config),
196
- ...buildRequiresConfig(config)
197
- };
198
- }
199
-
200
- /**
201
- * Builds volumes configuration for template data
202
- * @param {string} appName - Application name
203
- * @returns {Object} Volumes configuration
204
- */
205
- function buildVolumesConfig(appName) {
206
- // Use forward slashes for Docker paths (works on both Windows and Unix)
207
- const volumePath = path.join(process.cwd(), 'data', appName);
208
- return {
209
- mountVolume: volumePath.replace(/\\/g, '/')
210
- };
211
- }
212
-
213
- /**
214
- * Builds networks configuration for template data
215
- * @param {Object} config - Application configuration
216
- * @returns {Object} Networks configuration
217
- */
218
- function buildNetworksConfig(config) {
219
- // Get databases from requires.databases or top-level databases
220
- const databases = config.requires?.databases || config.databases || [];
221
- return {
222
- databases: databases
223
- };
224
- }
225
-
226
- /**
227
- * Generates Docker Compose configuration from template
228
- * @param {string} appName - Application name
229
- * @param {Object} config - Application configuration
230
- * @param {Object} options - Run options
231
- * @returns {Promise<string>} Generated compose content
232
- */
233
- async function generateDockerCompose(appName, config, options) {
234
- const language = config.build?.language || config.language || 'typescript';
235
- const template = loadDockerComposeTemplate(language);
236
-
237
- const port = options.port || config.build?.localPort || config.port || 3000;
238
-
239
- const serviceConfig = buildServiceConfig(appName, config, port);
240
- const volumesConfig = buildVolumesConfig(appName);
241
- const networksConfig = buildNetworksConfig(config);
242
-
243
- // Get absolute path to .env file for docker-compose
244
- const envFilePath = path.join(process.cwd(), 'builder', appName, '.env');
245
- const envFileAbsolutePath = envFilePath.replace(/\\/g, '/'); // Use forward slashes for Docker
246
-
247
- const templateData = {
248
- ...serviceConfig,
249
- ...volumesConfig,
250
- ...networksConfig,
251
- envFile: envFileAbsolutePath
252
- };
253
-
254
- return template(templateData);
99
+ return composeGenerator.getImageName(config, appName);
255
100
  }
256
101
 
257
102
  /**
@@ -267,10 +112,34 @@ async function validateAppConfiguration(appName) {
267
112
  throw new Error('Application name is required');
268
113
  }
269
114
 
115
+ // Check if we're running from inside the builder directory
116
+ const currentDir = process.cwd();
117
+ const normalizedPath = currentDir.replace(/\\/g, '/');
118
+ const expectedBuilderPath = `builder/${appName}`;
119
+
120
+ // If inside builder/{appName}, suggest moving to project root
121
+ if (normalizedPath.endsWith(expectedBuilderPath)) {
122
+ const projectRoot = path.resolve(currentDir, '../..');
123
+ throw new Error(
124
+ 'You\'re running from inside the builder directory.\n' +
125
+ `Current directory: ${currentDir}\n` +
126
+ 'Please change to the project root and try again:\n' +
127
+ ` cd ${projectRoot}\n` +
128
+ ` aifabrix run ${appName}`
129
+ );
130
+ }
131
+
270
132
  // Load and validate app configuration
271
133
  const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
272
134
  if (!fsSync.existsSync(configPath)) {
273
- throw new Error(`Application configuration not found: ${configPath}\nRun 'aifabrix create ${appName}' first`);
135
+ const expectedDir = path.join(currentDir, 'builder', appName);
136
+ throw new Error(
137
+ `Application configuration not found: ${configPath}\n` +
138
+ `Current directory: ${currentDir}\n` +
139
+ `Expected location: ${expectedDir}\n` +
140
+ 'Make sure you\'re running from the project root (where \'builder\' directory exists)\n' +
141
+ `Run 'aifabrix create ${appName}' first if configuration doesn't exist`
142
+ );
274
143
  }
275
144
 
276
145
  const configContent = fsSync.readFileSync(configPath, 'utf8');
@@ -279,7 +148,28 @@ async function validateAppConfiguration(appName) {
279
148
  // Validate configuration
280
149
  const validation = await validator.validateApplication(appName);
281
150
  if (!validation.valid) {
282
- throw new Error(`Configuration validation failed:\n${validation.variables.errors.join('\n')}`);
151
+ const allErrors = [];
152
+
153
+ if (validation.variables && validation.variables.errors && validation.variables.errors.length > 0) {
154
+ allErrors.push('variables.yaml:');
155
+ allErrors.push(...validation.variables.errors.map(err => ` ${err}`));
156
+ }
157
+
158
+ if (validation.rbac && validation.rbac.errors && validation.rbac.errors.length > 0) {
159
+ allErrors.push('rbac.yaml:');
160
+ allErrors.push(...validation.rbac.errors.map(err => ` ${err}`));
161
+ }
162
+
163
+ if (validation.env && validation.env.errors && validation.env.errors.length > 0) {
164
+ allErrors.push('env.template:');
165
+ allErrors.push(...validation.env.errors.map(err => ` ${err}`));
166
+ }
167
+
168
+ if (allErrors.length === 0) {
169
+ throw new Error('Configuration validation failed');
170
+ }
171
+
172
+ throw new Error(`Configuration validation failed:\n${allErrors.join('\n')}`);
283
173
  }
284
174
 
285
175
  return config;
@@ -354,7 +244,7 @@ async function prepareEnvironment(appName, config, options) {
354
244
 
355
245
  // Generate Docker Compose configuration
356
246
  logger.log(chalk.blue('Generating Docker Compose configuration...'));
357
- const composeContent = await generateDockerCompose(appName, config, options);
247
+ const composeContent = await composeGenerator.generateDockerCompose(appName, config, options);
358
248
  // Write compose file to temporary location
359
249
  const tempComposePath = path.join(process.cwd(), 'builder', appName, 'docker-compose.yaml');
360
250
  await fs.writeFile(tempComposePath, composeContent);
@@ -487,6 +377,6 @@ module.exports = {
487
377
  checkContainerRunning,
488
378
  stopAndRemoveContainer,
489
379
  checkPortAvailable,
490
- generateDockerCompose,
380
+ generateDockerCompose: composeGenerator.generateDockerCompose,
491
381
  waitForHealthCheck
492
382
  };
package/lib/build.js CHANGED
@@ -22,6 +22,7 @@ const secrets = require('./secrets');
22
22
  const logger = require('./utils/logger');
23
23
  const validator = require('./validator');
24
24
  const dockerfileUtils = require('./utils/dockerfile-utils');
25
+ const dockerBuild = require('./utils/docker-build');
25
26
 
26
27
  const execAsync = promisify(exec);
27
28
 
@@ -71,66 +72,6 @@ function resolveContextPath(builderPath, contextPath) {
71
72
  return resolvedPath;
72
73
  }
73
74
 
74
- /**
75
- * Checks if error indicates Docker is not running or not installed
76
- * @param {string} errorMessage - Error message to check
77
- * @returns {boolean} True if Docker is not available
78
- */
79
- function isDockerNotAvailableError(errorMessage) {
80
- return errorMessage.includes('docker: command not found') ||
81
- errorMessage.includes('Cannot connect to the Docker daemon') ||
82
- errorMessage.includes('Is the docker daemon running') ||
83
- errorMessage.includes('Cannot connect to Docker');
84
- }
85
-
86
- /**
87
- * Handles Docker build errors and provides user-friendly messages
88
- * @param {Error} error - Build error
89
- * @throws {Error} Formatted error message
90
- */
91
- function handleBuildError(error) {
92
- const errorMessage = error.message || error.stderr || String(error);
93
-
94
- if (isDockerNotAvailableError(errorMessage)) {
95
- throw new Error('Docker is not running or not installed. Please start Docker Desktop and try again.');
96
- }
97
-
98
- const detailedError = error.stderr || error.stdout || errorMessage;
99
- throw new Error(`Docker build failed: ${detailedError}`);
100
- }
101
-
102
- /**
103
- * Executes Docker build command with proper error handling
104
- * @param {string} imageName - Image name to build
105
- * @param {string} dockerfilePath - Path to Dockerfile
106
- * @param {string} contextPath - Build context path
107
- * @param {string} tag - Image tag
108
- * @returns {Promise<void>} Resolves when build completes
109
- * @throws {Error} If build fails
110
- */
111
- async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag) {
112
- const dockerCommand = `docker build -t ${imageName}:${tag} -f "${dockerfilePath}" "${contextPath}"`;
113
-
114
- try {
115
- logger.log(chalk.blue('Building image...'));
116
- logger.log(chalk.gray(`Command: ${dockerCommand}`));
117
-
118
- const { stdout, stderr } = await execAsync(dockerCommand);
119
-
120
- if (stderr && !stderr.includes('warning')) {
121
- logger.log(chalk.yellow(stderr));
122
- }
123
-
124
- if (stdout) {
125
- logger.log(stdout);
126
- }
127
-
128
- logger.log(chalk.green(`✓ Image built: ${imageName}:${tag}`));
129
- } catch (error) {
130
- handleBuildError(error);
131
- }
132
- }
133
-
134
75
  /**
135
76
  * Detects the runtime language of an application
136
77
  * Analyzes project files to determine TypeScript, Python, etc.
@@ -317,7 +258,7 @@ async function loadAndValidateConfig(appName) {
317
258
  * @param {Object} options - Build options
318
259
  */
319
260
  async function executeBuild(imageName, dockerfilePath, contextPath, tag, options) {
320
- await executeDockerBuild(imageName, dockerfilePath, contextPath, tag);
261
+ await dockerBuild.executeDockerBuild(imageName, dockerfilePath, contextPath, tag);
321
262
 
322
263
  // Tag image if additional tag provided
323
264
  if (options.tag && options.tag !== 'latest') {
@@ -417,7 +358,7 @@ async function buildApp(appName, options = {}) {
417
358
  module.exports = {
418
359
  loadVariablesYaml,
419
360
  resolveContextPath,
420
- executeDockerBuild,
361
+ executeDockerBuild: dockerBuild.executeDockerBuild,
421
362
  detectLanguage,
422
363
  generateDockerfile,
423
364
  buildApp
@@ -7,9 +7,17 @@ environments:
7
7
  REDIS_HOST: redis
8
8
  MISO_HOST: miso-controller
9
9
  KEYCLOAK_HOST: keycloak
10
-
10
+ MORI_HOST: mori-controller
11
+ OPENWEBUI_HOST: openwebui
12
+ FLOWISE_HOST: flowise
13
+ DATAPLANE_HOST: dataplane
14
+
11
15
  local:
12
16
  DB_HOST: localhost
13
17
  REDIS_HOST: localhost
14
18
  MISO_HOST: localhost
15
19
  KEYCLOAK_HOST: localhost
20
+ MORI_HOST: localhost
21
+ OPENWEBUI_HOST: localhost
22
+ FLOWISE_HOST: localhost
23
+ DATAPLANE_HOST: localhost
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Docker Compose Generation Utilities
3
+ *
4
+ * This module handles Docker Compose configuration generation for application running.
5
+ * Separated from app-run.js to maintain file size limits.
6
+ *
7
+ * @fileoverview Docker Compose generation utilities
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fsSync = require('fs');
13
+ const path = require('path');
14
+ const handlebars = require('handlebars');
15
+
16
+ /**
17
+ * Loads and compiles Docker Compose template
18
+ * @param {string} language - Language type
19
+ * @returns {Function} Compiled Handlebars template
20
+ * @throws {Error} If template not found
21
+ */
22
+ function loadDockerComposeTemplate(language) {
23
+ const templatePath = path.join(__dirname, '..', '..', 'templates', language, 'docker-compose.hbs');
24
+ if (!fsSync.existsSync(templatePath)) {
25
+ throw new Error(`Docker Compose template not found for language: ${language}`);
26
+ }
27
+
28
+ const templateContent = fsSync.readFileSync(templatePath, 'utf8');
29
+ return handlebars.compile(templateContent);
30
+ }
31
+
32
+ /**
33
+ * Extracts image name from configuration (same logic as build.js)
34
+ * @param {Object} config - Application configuration
35
+ * @param {string} appName - Application name (fallback)
36
+ * @returns {string} Image name
37
+ */
38
+ function getImageName(config, appName) {
39
+ if (typeof config.image === 'string') {
40
+ return config.image.split(':')[0];
41
+ } else if (config.image?.name) {
42
+ return config.image.name;
43
+ } else if (config.app?.key) {
44
+ return config.app.key;
45
+ }
46
+ return appName;
47
+ }
48
+
49
+ /**
50
+ * Builds app configuration section
51
+ * @param {string} appName - Application name
52
+ * @param {Object} config - Application configuration
53
+ * @returns {Object} App configuration
54
+ */
55
+ function buildAppConfig(appName, config) {
56
+ return {
57
+ key: appName,
58
+ name: config.displayName || appName
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Builds image configuration section
64
+ * @param {Object} config - Application configuration
65
+ * @param {string} appName - Application name
66
+ * @returns {Object} Image configuration
67
+ */
68
+ function buildImageConfig(config, appName) {
69
+ const imageName = getImageName(config, appName);
70
+ const imageTag = config.image?.tag || 'latest';
71
+ return {
72
+ name: imageName,
73
+ tag: imageTag
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Builds health check configuration section
79
+ * @param {Object} config - Application configuration
80
+ * @returns {Object} Health check configuration
81
+ */
82
+ function buildHealthCheckConfig(config) {
83
+ return {
84
+ path: config.healthCheck?.path || '/health',
85
+ interval: config.healthCheck?.interval || 30
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Builds requires configuration section
91
+ * @param {Object} config - Application configuration
92
+ * @returns {Object} Requires configuration
93
+ */
94
+ function buildRequiresConfig(config) {
95
+ return {
96
+ requiresDatabase: config.requires?.database || config.services?.database || false,
97
+ requiresStorage: config.requires?.storage || config.services?.storage || false,
98
+ requiresRedis: config.requires?.redis || config.services?.redis || false
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Builds service configuration for template data
104
+ * @param {string} appName - Application name
105
+ * @param {Object} config - Application configuration
106
+ * @param {number} port - Application port
107
+ * @returns {Object} Service configuration
108
+ */
109
+ function buildServiceConfig(appName, config, port) {
110
+ const containerPort = config.build?.containerPort || config.port || 3000;
111
+
112
+ return {
113
+ app: buildAppConfig(appName, config),
114
+ image: buildImageConfig(config, appName),
115
+ port: containerPort,
116
+ build: {
117
+ localPort: port
118
+ },
119
+ healthCheck: buildHealthCheckConfig(config),
120
+ ...buildRequiresConfig(config)
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Builds volumes configuration for template data
126
+ * @param {string} appName - Application name
127
+ * @returns {Object} Volumes configuration
128
+ */
129
+ function buildVolumesConfig(appName) {
130
+ // Use forward slashes for Docker paths (works on both Windows and Unix)
131
+ const volumePath = path.join(process.cwd(), 'data', appName);
132
+ return {
133
+ mountVolume: volumePath.replace(/\\/g, '/')
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Builds networks configuration for template data
139
+ * @param {Object} config - Application configuration
140
+ * @returns {Object} Networks configuration
141
+ */
142
+ function buildNetworksConfig(config) {
143
+ // Get databases from requires.databases or top-level databases
144
+ const databases = config.requires?.databases || config.databases || [];
145
+ return {
146
+ databases: databases
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Generates Docker Compose configuration from template
152
+ * @param {string} appName - Application name
153
+ * @param {Object} config - Application configuration
154
+ * @param {Object} options - Run options
155
+ * @returns {Promise<string>} Generated compose content
156
+ */
157
+ async function generateDockerCompose(appName, config, options) {
158
+ const language = config.build?.language || config.language || 'typescript';
159
+ const template = loadDockerComposeTemplate(language);
160
+
161
+ const port = options.port || config.build?.localPort || config.port || 3000;
162
+
163
+ const serviceConfig = buildServiceConfig(appName, config, port);
164
+ const volumesConfig = buildVolumesConfig(appName);
165
+ const networksConfig = buildNetworksConfig(config);
166
+
167
+ // Get absolute path to .env file for docker-compose
168
+ const envFilePath = path.join(process.cwd(), 'builder', appName, '.env');
169
+ const envFileAbsolutePath = envFilePath.replace(/\\/g, '/'); // Use forward slashes for Docker
170
+
171
+ const templateData = {
172
+ ...serviceConfig,
173
+ ...volumesConfig,
174
+ ...networksConfig,
175
+ envFile: envFileAbsolutePath
176
+ };
177
+
178
+ return template(templateData);
179
+ }
180
+
181
+ module.exports = {
182
+ generateDockerCompose,
183
+ getImageName
184
+ };
185
+
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Docker Build Utilities
3
+ *
4
+ * This module handles Docker image building with progress indicators.
5
+ * Separated from build.js to maintain file size limits.
6
+ *
7
+ * @fileoverview Docker build execution utilities
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const { spawn } = require('child_process');
13
+ const ora = require('ora');
14
+
15
+ /**
16
+ * Checks if error indicates Docker is not running or not installed
17
+ * @param {string} errorMessage - Error message to check
18
+ * @returns {boolean} True if Docker is not available
19
+ */
20
+ function isDockerNotAvailableError(errorMessage) {
21
+ return errorMessage.includes('docker: command not found') ||
22
+ errorMessage.includes('Cannot connect to the Docker daemon') ||
23
+ errorMessage.includes('Is the docker daemon running') ||
24
+ errorMessage.includes('Cannot connect to Docker');
25
+ }
26
+
27
+ /**
28
+ * Parses Docker build output to extract progress information
29
+ * @param {string} line - Single line of Docker build output
30
+ * @returns {string|null} Progress message or null if no progress info
31
+ */
32
+ function parseDockerBuildProgress(line) {
33
+ // Match step progress: "Step 1/10 : FROM node:20-alpine"
34
+ const stepMatch = line.match(/^Step\s+(\d+)\/(\d+)\s*:/i);
35
+ if (stepMatch) {
36
+ return `Step ${stepMatch[1]}/${stepMatch[2]}`;
37
+ }
38
+
39
+ // Match layer pulling: "Pulling from library/node"
40
+ const pullingMatch = line.match(/^Pulling\s+(.+)$/i);
41
+ if (pullingMatch) {
42
+ return `Pulling ${pullingMatch[1]}`;
43
+ }
44
+
45
+ // Match layer extracting: "Extracting [====> ]"
46
+ const extractingMatch = line.match(/^Extracting/i);
47
+ if (extractingMatch) {
48
+ return 'Extracting layers';
49
+ }
50
+
51
+ // Match build progress: " => [internal] load build context"
52
+ const buildMatch = line.match(/^=>\s*\[(.*?)\]\s*(.+)$/i);
53
+ if (buildMatch) {
54
+ return buildMatch[2].substring(0, 50);
55
+ }
56
+
57
+ // Match progress bars: "[====> ] 10.5MB/50MB"
58
+ const progressMatch = line.match(/\[.*?\]\s+([\d.]+(MB|KB|GB))\/([\d.]+(MB|KB|GB))/i);
59
+ if (progressMatch) {
60
+ return `${progressMatch[1]}/${progressMatch[3]}`;
61
+ }
62
+
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * Executes Docker build command with progress indicator
68
+ * @param {string} imageName - Image name to build
69
+ * @param {string} dockerfilePath - Path to Dockerfile
70
+ * @param {string} contextPath - Build context path
71
+ * @param {string} tag - Image tag
72
+ * @returns {Promise<void>} Resolves when build completes
73
+ * @throws {Error} If build fails
74
+ */
75
+ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag) {
76
+ const spinner = ora({
77
+ text: 'Starting Docker build...',
78
+ spinner: 'dots'
79
+ }).start();
80
+
81
+ return new Promise((resolve, reject) => {
82
+ // Use spawn for streaming output
83
+ const dockerProcess = spawn('docker', [
84
+ 'build',
85
+ '-t', `${imageName}:${tag}`,
86
+ '-f', dockerfilePath,
87
+ contextPath
88
+ ], {
89
+ shell: process.platform === 'win32'
90
+ });
91
+
92
+ let stdoutBuffer = '';
93
+ let stderrBuffer = '';
94
+ let lastProgressUpdate = Date.now();
95
+
96
+ dockerProcess.stdout.on('data', (data) => {
97
+ const output = data.toString();
98
+ stdoutBuffer += output;
99
+
100
+ // Parse progress from output lines
101
+ const lines = output.split('\n');
102
+ for (const line of lines) {
103
+ const progress = parseDockerBuildProgress(line.trim());
104
+ if (progress) {
105
+ // Update spinner text with progress (throttle updates)
106
+ const now = Date.now();
107
+ if (now - lastProgressUpdate > 200) {
108
+ spinner.text = `Building image... ${progress}`;
109
+ lastProgressUpdate = now;
110
+ }
111
+ }
112
+ }
113
+ });
114
+
115
+ dockerProcess.stderr.on('data', (data) => {
116
+ const output = data.toString();
117
+ stderrBuffer += output;
118
+
119
+ // Check for warnings vs errors
120
+ if (!output.toLowerCase().includes('warning')) {
121
+ // Parse progress from stderr too (Docker outputs progress to stderr)
122
+ const lines = output.split('\n');
123
+ for (const line of lines) {
124
+ const progress = parseDockerBuildProgress(line.trim());
125
+ if (progress) {
126
+ const now = Date.now();
127
+ if (now - lastProgressUpdate > 200) {
128
+ spinner.text = `Building image... ${progress}`;
129
+ lastProgressUpdate = now;
130
+ }
131
+ }
132
+ }
133
+ }
134
+ });
135
+
136
+ dockerProcess.on('close', (code) => {
137
+ if (code === 0) {
138
+ spinner.succeed(`Image built: ${imageName}:${tag}`);
139
+ resolve();
140
+ } else {
141
+ spinner.fail('Build failed');
142
+
143
+ const errorMessage = stderrBuffer || stdoutBuffer || 'Docker build failed';
144
+
145
+ if (isDockerNotAvailableError(errorMessage)) {
146
+ reject(new Error('Docker is not running or not installed. Please start Docker Desktop and try again.'));
147
+ } else {
148
+ // Show last few lines of error output
149
+ const errorLines = errorMessage.split('\n').filter(line => line.trim());
150
+ const lastError = errorLines.slice(-5).join('\n');
151
+ reject(new Error(`Docker build failed: ${lastError}`));
152
+ }
153
+ }
154
+ });
155
+
156
+ dockerProcess.on('error', (error) => {
157
+ spinner.fail('Build failed');
158
+ const errorMessage = error.message || String(error);
159
+
160
+ if (isDockerNotAvailableError(errorMessage)) {
161
+ reject(new Error('Docker is not running or not installed. Please start Docker Desktop and try again.'));
162
+ } else {
163
+ reject(new Error(`Docker build failed: ${errorMessage}`));
164
+ }
165
+ });
166
+ });
167
+ }
168
+
169
+ module.exports = {
170
+ executeDockerBuild,
171
+ isDockerNotAvailableError
172
+ };
173
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "lint:ci": "eslint . --ext .js --format json --output-file eslint-report.json",
19
19
  "dev": "node bin/aifabrix.js",
20
20
  "build": "npm run lint && npm run test:ci",
21
+ "pack": "npm run build && npm pack",
21
22
  "validate": "npm run build",
22
23
  "prepublishOnly": "npm run validate",
23
24
  "precommit": "npm run lint:fix && npm run test"
@@ -52,6 +52,53 @@ permissions:
52
52
  roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
53
53
  description: "Deactivate service users"
54
54
 
55
+ # User Management
56
+ - name: "users:create"
57
+ roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
58
+ description: "Create new users"
59
+
60
+ - name: "users:read"
61
+ roles: ["aifabrix-platform-admin", "aifabrix-security-admin", "aifabrix-observer"]
62
+ description: "View user information and profiles"
63
+
64
+ - name: "users:update"
65
+ roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
66
+ description: "Update user information and manage group memberships"
67
+
68
+ - name: "users:delete"
69
+ roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
70
+ description: "Delete users"
71
+
72
+ # Group Management
73
+ - name: "groups:create"
74
+ roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
75
+ description: "Create new groups"
76
+
77
+ - name: "groups:read"
78
+ roles: ["aifabrix-platform-admin", "aifabrix-security-admin", "aifabrix-observer"]
79
+ description: "View group information and members"
80
+
81
+ - name: "groups:update"
82
+ roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
83
+ description: "Update group information"
84
+
85
+ - name: "groups:delete"
86
+ roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
87
+ description: "Delete groups"
88
+
89
+ # Administrative Permissions
90
+ - name: "admin:read"
91
+ roles: ["aifabrix-platform-admin"]
92
+ description: "Administrative read access to all resources"
93
+
94
+ - name: "admin:write"
95
+ roles: ["aifabrix-platform-admin"]
96
+ description: "Administrative write access to all resources"
97
+
98
+ - name: "admin:delete"
99
+ roles: ["aifabrix-platform-admin"]
100
+ description: "Administrative delete access to all resources"
101
+
55
102
  # Template Applications (environment = null)
56
103
  - name: "applications:create"
57
104
  roles: ["aifabrix-platform-admin", "aifabrix-infrastructure-admin", "aifabrix-deployment-admin"]
@@ -165,4 +212,3 @@ permissions:
165
212
  - name: "dashboard:read"
166
213
  roles: ["aifabrix-platform-admin", "aifabrix-deployment-admin", "aifabrix-developer", "aifabrix-observer"]
167
214
  description: "View dashboard summaries and aggregates"
168
-
@@ -1,6 +1,6 @@
1
1
  # Application Metadata
2
2
  app:
3
- key: miso
3
+ key: miso-controller
4
4
  displayName: "Miso Controller"
5
5
  description: "AI Fabrix Miso Controller - Backend API and orchestration service"
6
6
  type: webapp
@@ -36,21 +36,21 @@ healthCheck:
36
36
  authentication:
37
37
  type: keycloak
38
38
  enableSSO: true
39
- requiredRoles: ["aifabrix-user"]
39
+ requiredRoles:
40
+ - aifabrix-user
40
41
  endpoints:
41
- local: "http://localhost:3000/auth/callback"
42
+ local: http://localhost:3000/auth/callback
42
43
 
43
44
  # Build Configuration
44
45
  build:
45
- context: .. # Docker build context (relative to builder/)
46
- dockerfile: builder/Dockerfile # Dockerfile name (empty = use template)
47
- envOutputPath: ../packages/miso-controller/.env # Copy .env to repo root for local dev
48
- localPort: 3010 # Port for local development (different from Docker port)
49
- language: typescript # Runtime language for template selection
50
- secrets: # Path to secrets file (optional)
46
+ context: .. # Docker build context (relative to builder/)
47
+ dockerfile: builder/miso-controller/Dockerfile # Dockerfile name (empty = use template)
48
+ envOutputPath: # Copy .env to repo root for local dev (relative to builder/) (if null, no .env file is copied) (if empty, .env file is copied to repo root)
49
+ localPort: 3010 # Port for local development (different from Docker port)
50
+ language: typescript # Runtime language for template selection (typescript or python)
51
+ secrets: # Path to secrets file
51
52
 
52
53
  # Docker Compose
53
54
  compose:
54
55
  file: docker-compose.yaml
55
56
  service: miso-controller
56
-