@aifabrix/builder 2.0.2 → 2.0.4

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);
@@ -391,11 +281,11 @@ async function startContainer(appName, composePath, port, config = null) {
391
281
  await execAsync(`docker-compose -f "${composePath}" up -d`, { env });
392
282
  logger.log(chalk.green(`✓ Container aifabrix-${appName} started`));
393
283
 
394
- // Wait for health check
284
+ // Wait for health check - detect actual mapped port from Docker
285
+ // Don't pass port so waitForHealthCheck will auto-detect it from container
395
286
  const healthCheckPath = config?.healthCheck?.path || '/health';
396
- const healthCheckUrl = `http://localhost:${port}${healthCheckPath}`;
397
- logger.log(chalk.blue(`Waiting for application to be healthy at ${healthCheckUrl}...`));
398
- await waitForHealthCheck(appName, 90, port, config);
287
+ logger.log(chalk.blue(`Waiting for application to be healthy at http://localhost:${port}${healthCheckPath}...`));
288
+ await waitForHealthCheck(appName, 90, null, config);
399
289
  }
400
290
 
401
291
  /**
@@ -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