@aifabrix/builder 2.1.7 → 2.2.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/lib/build.js CHANGED
@@ -19,10 +19,12 @@ const { promisify } = require('util');
19
19
  const chalk = require('chalk');
20
20
  const yaml = require('js-yaml');
21
21
  const secrets = require('./secrets');
22
+ const config = require('./config');
22
23
  const logger = require('./utils/logger');
23
24
  const validator = require('./validator');
24
25
  const dockerfileUtils = require('./utils/dockerfile-utils');
25
26
  const dockerBuild = require('./utils/docker-build');
27
+ const buildCopy = require('./utils/build-copy');
26
28
 
27
29
  const execAsync = promisify(exec);
28
30
 
@@ -127,7 +129,7 @@ function detectLanguage(appPath) {
127
129
  * const dockerfilePath = await generateDockerfile('myapp', 'typescript', config);
128
130
  * // Returns: '~/.aifabrix/myapp/Dockerfile.typescript'
129
131
  */
130
- async function generateDockerfile(appNameOrPath, language, config, buildConfig = {}) {
132
+ async function generateDockerfile(appNameOrPath, language, config, buildConfig = {}, devDir = null) {
131
133
  let appName;
132
134
  if (appNameOrPath.includes(path.sep) || appNameOrPath.includes('/') || appNameOrPath.includes('\\')) {
133
135
  appName = path.basename(appNameOrPath);
@@ -151,12 +153,19 @@ async function generateDockerfile(appNameOrPath, language, config, buildConfig =
151
153
 
152
154
  const dockerfileContent = dockerfileUtils.renderDockerfile(template, templateVars, language, isAppFlag, appSourcePath);
153
155
 
154
- const aifabrixDir = path.join(os.homedir(), '.aifabrix', appName);
155
- if (!fsSync.existsSync(aifabrixDir)) {
156
- await fs.mkdir(aifabrixDir, { recursive: true });
156
+ // Use dev directory if provided, otherwise use default aifabrix directory
157
+ let targetDir;
158
+ if (devDir) {
159
+ targetDir = devDir;
160
+ } else {
161
+ targetDir = path.join(os.homedir(), '.aifabrix', appName);
162
+ }
163
+
164
+ if (!fsSync.existsSync(targetDir)) {
165
+ await fs.mkdir(targetDir, { recursive: true });
157
166
  }
158
167
 
159
- const dockerfilePath = path.join(aifabrixDir, `Dockerfile.${language}`);
168
+ const dockerfilePath = path.join(targetDir, 'Dockerfile');
160
169
  await fs.writeFile(dockerfilePath, dockerfileContent, 'utf8');
161
170
 
162
171
  return dockerfilePath;
@@ -175,41 +184,29 @@ async function generateDockerfile(appNameOrPath, language, config, buildConfig =
175
184
  * @returns {Promise<string>} Path to Dockerfile
176
185
  */
177
186
  async function determineDockerfile(appName, options) {
178
- const builderPath = path.join(process.cwd(), 'builder', appName);
187
+ // Use dev directory if provided, otherwise fall back to builder directory
188
+ const searchPath = options.devDir || path.join(process.cwd(), 'builder', appName);
179
189
 
180
- const templateDockerfile = dockerfileUtils.checkTemplateDockerfile(builderPath, appName, options.forceTemplate);
190
+ const templateDockerfile = dockerfileUtils.checkTemplateDockerfile(searchPath, appName, options.forceTemplate);
181
191
  if (templateDockerfile) {
182
- logger.log(chalk.green(`✓ Using existing Dockerfile: builder/${appName}/Dockerfile`));
192
+ const relativePath = path.relative(process.cwd(), templateDockerfile);
193
+ logger.log(chalk.green(`✓ Using existing Dockerfile: ${relativePath}`));
183
194
  return templateDockerfile;
184
195
  }
185
196
 
186
- const customDockerfile = dockerfileUtils.checkProjectDockerfile(builderPath, appName, options.buildConfig, options.contextPath, options.forceTemplate);
197
+ const customDockerfile = dockerfileUtils.checkProjectDockerfile(searchPath, appName, options.buildConfig, options.contextPath, options.forceTemplate);
187
198
  if (customDockerfile) {
188
199
  logger.log(chalk.green(`✓ Using custom Dockerfile: ${options.buildConfig.dockerfile}`));
189
200
  return customDockerfile;
190
201
  }
191
202
 
192
- const dockerfilePath = await generateDockerfile(appName, options.language, options.config, options.buildConfig);
203
+ // Generate Dockerfile in dev directory if provided
204
+ const dockerfilePath = await generateDockerfile(appName, options.language, options.config, options.buildConfig, options.devDir);
193
205
  const relativePath = path.relative(process.cwd(), dockerfilePath);
194
206
  logger.log(chalk.green(`✓ Generated Dockerfile from template: ${relativePath}`));
195
207
  return dockerfilePath;
196
208
  }
197
209
 
198
- /**
199
- * Prepares build context path
200
- * @param {string} appName - Application name
201
- * @param {string} contextPath - Relative context path
202
- * @returns {string} Absolute context path
203
- */
204
- function prepareBuildContext(appName, contextPath) {
205
- // Ensure contextPath is a string
206
- const context = typeof contextPath === 'string' ? contextPath : (contextPath || '');
207
- return resolveContextPath(
208
- path.join(process.cwd(), 'builder', appName),
209
- context
210
- );
211
- }
212
-
213
210
  /**
214
211
  * Loads and validates configuration for build
215
212
  * @async
@@ -309,20 +306,41 @@ async function buildApp(appName, options = {}) {
309
306
  logger.log(chalk.blue(`\n🔨 Building application: ${appName}`));
310
307
 
311
308
  // 1. Load and validate configuration
312
- const { config, imageName, buildConfig } = await loadAndValidateConfig(appName);
313
-
314
- // 2. Prepare build context
315
- const contextPath = prepareBuildContext(appName, buildConfig.context);
309
+ const { config: appConfig, imageName, buildConfig } = await loadAndValidateConfig(appName);
310
+
311
+ // 2. Get developer ID and copy files to dev-specific directory
312
+ const developerId = await config.getDeveloperId();
313
+ const directoryName = developerId === 0 ? 'applications' : `dev-${developerId}`;
314
+ logger.log(chalk.blue(`Copying files to developer-specific directory (${directoryName})...`));
315
+ const devDir = await buildCopy.copyBuilderToDevDirectory(appName, developerId);
316
+ logger.log(chalk.green(`✓ Files copied to: ${devDir}`));
317
+
318
+ // 3. Prepare build context (use dev-specific directory)
319
+ // If buildConfig.context is relative, resolve it relative to devDir
320
+ // If buildConfig.context is '../..' (apps flag), keep it as is (it's relative to devDir)
321
+ let contextPath;
322
+ if (buildConfig.context && buildConfig.context !== '../..') {
323
+ // Resolve relative context path from dev directory
324
+ contextPath = path.resolve(devDir, buildConfig.context);
325
+ } else if (buildConfig.context === '../..') {
326
+ // For apps flag, context is relative to devDir (which is ~/.aifabrix/<app>-dev-<id>)
327
+ // So '../..' from devDir goes to ~/.aifabrix, then we need to go to project root
328
+ // Actually, we need to keep the relative path and let Docker resolve it
329
+ // But the build context should be the project root, not devDir
330
+ contextPath = process.cwd();
331
+ } else {
332
+ // No context specified, use dev directory
333
+ contextPath = devDir;
334
+ }
316
335
 
317
- // 3. Check if Dockerfile exists in builder/{appName}/ directory
318
- const builderPath = path.join(process.cwd(), 'builder', appName);
319
- const appDockerfilePath = path.join(builderPath, 'Dockerfile');
336
+ // 4. Check if Dockerfile exists in dev directory
337
+ const appDockerfilePath = path.join(devDir, 'Dockerfile');
320
338
  const hasExistingDockerfile = fsSync.existsSync(appDockerfilePath) && !options.forceTemplate;
321
339
 
322
- // 4. Determine language (skip if existing Dockerfile found)
340
+ // 5. Determine language (skip if existing Dockerfile found)
323
341
  let language = options.language || buildConfig.language;
324
342
  if (!language && !hasExistingDockerfile) {
325
- language = detectLanguage(builderPath);
343
+ language = detectLanguage(devDir);
326
344
  } else if (!language) {
327
345
  // Default language if existing Dockerfile is found (won't be used, but needed for API)
328
346
  language = 'typescript';
@@ -331,13 +349,15 @@ async function buildApp(appName, options = {}) {
331
349
  logger.log(chalk.green(`✓ Detected language: ${language}`));
332
350
  }
333
351
 
334
- // 5. Determine Dockerfile (needs context path to generate in correct location)
352
+ // 6. Determine Dockerfile (needs context path to generate in correct location)
353
+ // Use dev directory for Dockerfile generation
335
354
  const dockerfilePath = await determineDockerfile(appName, {
336
355
  language,
337
- config,
356
+ config: appConfig,
338
357
  buildConfig,
339
358
  contextPath,
340
- forceTemplate: options.forceTemplate
359
+ forceTemplate: options.forceTemplate,
360
+ devDir: devDir
341
361
  });
342
362
 
343
363
  // 6. Build Docker image
package/lib/cli.js CHANGED
@@ -15,6 +15,8 @@ const secrets = require('./secrets');
15
15
  const generator = require('./generator');
16
16
  const validator = require('./validator');
17
17
  const keyGenerator = require('./key-generator');
18
+ const config = require('./config');
19
+ const devConfig = require('./utils/dev-config');
18
20
  const chalk = require('chalk');
19
21
  const logger = require('./utils/logger');
20
22
  const { validateCommand, handleCommandError } = require('./utils/cli-utils');
@@ -28,11 +30,12 @@ function setupCommands(program) {
28
30
  // Authentication command
29
31
  program.command('login')
30
32
  .description('Authenticate with Miso Controller')
31
- .option('-u, --url <url>', 'Controller URL', 'http://localhost:3000')
33
+ .option('-c, --controller <url>', 'Controller URL', 'http://localhost:3000')
32
34
  .option('-m, --method <method>', 'Authentication method (device|credentials)')
33
- .option('--client-id <id>', 'Client ID (for credentials method)')
34
- .option('--client-secret <secret>', 'Client Secret (for credentials method)')
35
- .option('-e, --environment <env>', 'Environment key (for device method, e.g., dev, tst, pro)')
35
+ .option('-a, --app <app>', 'Application name (required for credentials method, reads from secrets.local.yaml)')
36
+ .option('--client-id <id>', 'Client ID (for credentials method, overrides secrets.local.yaml)')
37
+ .option('--client-secret <secret>', 'Client Secret (for credentials method, overrides secrets.local.yaml)')
38
+ .option('-e, --environment <env>', 'Environment key (updates root-level environment in config.yaml, e.g., miso, dev, tst, pro)')
36
39
  .action(async(options) => {
37
40
  try {
38
41
  await handleLogin(options);
@@ -45,9 +48,21 @@ function setupCommands(program) {
45
48
  // Infrastructure commands
46
49
  program.command('up')
47
50
  .description('Start local infrastructure services (Postgres, Redis, pgAdmin, Redis Commander)')
48
- .action(async() => {
51
+ .option('-d, --developer <id>', 'Set developer ID and start infrastructure')
52
+ .action(async(options) => {
49
53
  try {
50
- await infra.startInfra();
54
+ let developerId = null;
55
+ if (options.developer) {
56
+ const id = parseInt(options.developer, 10);
57
+ if (isNaN(id) || id < 0) {
58
+ throw new Error('Developer ID must be a non-negative number (0 = default infra, > 0 = developer-specific)');
59
+ }
60
+ await config.setDeveloperId(id);
61
+ process.env.AIFABRIX_DEVELOPERID = id.toString();
62
+ developerId = id;
63
+ logger.log(chalk.green(`✓ Developer ID set to ${id}`));
64
+ }
65
+ await infra.startInfra(developerId);
51
66
  } catch (error) {
52
67
  handleCommandError(error, 'up');
53
68
  process.exit(1);
@@ -138,7 +153,7 @@ function setupCommands(program) {
138
153
  program.command('deploy <app>')
139
154
  .description('Deploy to Azure via Miso Controller')
140
155
  .option('-c, --controller <url>', 'Controller URL')
141
- .option('-e, --environment <env>', 'Environment (dev, tst, pro)', 'dev')
156
+ .option('-e, --environment <env>', 'Environment (miso, dev, tst, pro)', 'dev')
142
157
  .option('--client-id <id>', 'Client ID (overrides config)')
143
158
  .option('--client-secret <secret>', 'Client Secret (overrides config)')
144
159
  .option('--poll', 'Poll for deployment status', true)
@@ -191,7 +206,7 @@ function setupCommands(program) {
191
206
  });
192
207
 
193
208
  program.command('status')
194
- .description('Show detailed infrastructure service status')
209
+ .description('Show detailed infrastructure service status and running applications')
195
210
  .action(async() => {
196
211
  try {
197
212
  const status = await infra.getInfraStatus();
@@ -207,6 +222,22 @@ function setupCommands(program) {
207
222
  logger.log(` URL: ${info.url}`);
208
223
  logger.log('');
209
224
  });
225
+
226
+ // Show running applications
227
+ const apps = await infra.getAppStatus();
228
+ if (apps.length > 0) {
229
+ logger.log('📱 Running Applications\n');
230
+ apps.forEach((app) => {
231
+ const normalizedStatus = String(app.status).trim().toLowerCase();
232
+ const icon = normalizedStatus.includes('running') || normalizedStatus.includes('up') ? '✅' : '❌';
233
+ logger.log(`${icon} ${app.name}:`);
234
+ logger.log(` Container: ${app.container}`);
235
+ logger.log(` Port: ${app.port}`);
236
+ logger.log(` Status: ${app.status}`);
237
+ logger.log(` URL: ${app.url}`);
238
+ logger.log('');
239
+ });
240
+ }
210
241
  } catch (error) {
211
242
  handleCommandError(error, 'status');
212
243
  process.exit(1);
@@ -291,6 +322,57 @@ function setupCommands(program) {
291
322
  process.exit(1);
292
323
  }
293
324
  });
325
+
326
+ // Developer configuration commands
327
+ program.command('dev config')
328
+ .description('Show or set developer configuration')
329
+ .option('--set-id <id>', 'Set developer ID')
330
+ .action(async(cmdName, opts) => {
331
+ try {
332
+ // For commands with spaces like 'dev config', Commander.js passes command name as first arg
333
+ // Options are passed as second arg, or if only one arg, it might be the command name
334
+ const options = typeof cmdName === 'object' && cmdName !== null ? cmdName : (opts || {});
335
+ // Commander.js converts --set-id to setId in options object
336
+ const setIdValue = options.setId || options['set-id'];
337
+ if (setIdValue) {
338
+ const id = parseInt(setIdValue, 10);
339
+ if (isNaN(id) || id < 0) {
340
+ throw new Error('Developer ID must be a non-negative number (0 = default infra, > 0 = developer-specific)');
341
+ }
342
+ await config.setDeveloperId(id);
343
+ process.env.AIFABRIX_DEVELOPERID = id.toString();
344
+ logger.log(chalk.green(`✓ Developer ID set to ${id}`));
345
+ // Use the ID we just set instead of reading from file to avoid race conditions
346
+ const devId = id;
347
+ const ports = devConfig.getDevPorts(devId);
348
+ logger.log('\n🔧 Developer Configuration\n');
349
+ logger.log(`Developer ID: ${devId}`);
350
+ logger.log('\nPorts:');
351
+ logger.log(` App: ${ports.app}`);
352
+ logger.log(` Postgres: ${ports.postgres}`);
353
+ logger.log(` Redis: ${ports.redis}`);
354
+ logger.log(` pgAdmin: ${ports.pgadmin}`);
355
+ logger.log(` Redis Commander: ${ports.redisCommander}`);
356
+ logger.log('');
357
+ return;
358
+ }
359
+
360
+ const devId = await config.getDeveloperId();
361
+ const ports = devConfig.getDevPorts(devId);
362
+ logger.log('\n🔧 Developer Configuration\n');
363
+ logger.log(`Developer ID: ${devId}`);
364
+ logger.log('\nPorts:');
365
+ logger.log(` App: ${ports.app}`);
366
+ logger.log(` Postgres: ${ports.postgres}`);
367
+ logger.log(` Redis: ${ports.redis}`);
368
+ logger.log(` pgAdmin: ${ports.pgadmin}`);
369
+ logger.log(` Redis Commander: ${ports.redisCommander}`);
370
+ logger.log('');
371
+ } catch (error) {
372
+ handleCommandError(error, 'dev config');
373
+ process.exit(1);
374
+ }
375
+ });
294
376
  }
295
377
 
296
378
  module.exports = {