genbox 1.0.90 → 1.0.92

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.
@@ -151,9 +151,25 @@ function convertScanToDetected(scan, root) {
151
151
  })),
152
152
  apps: {},
153
153
  };
154
- // Convert apps
154
+ // Collect docker app build contexts first (docker takes precedence over package.json)
155
+ const dockerBuildContexts = new Set();
156
+ if (scan.compose?.applications) {
157
+ for (const dockerApp of scan.compose.applications) {
158
+ if (dockerApp.build?.context) {
159
+ // Normalize the path for comparison
160
+ const ctx = dockerApp.build.context.replace(/^\.?\/?/, '').replace(/\/$/, '');
161
+ dockerBuildContexts.add(ctx);
162
+ }
163
+ }
164
+ }
165
+ // Convert apps from package.json (skip if docker app builds from same directory)
155
166
  const isMultiRepo = scan.structure.type === 'hybrid';
156
167
  for (const app of scan.apps) {
168
+ // Skip if there's a docker app building from this directory
169
+ const normalizedPath = app.path.replace(/^\.?\/?/, '').replace(/\/$/, '');
170
+ if (dockerBuildContexts.has(normalizedPath)) {
171
+ continue; // Docker app takes precedence
172
+ }
157
173
  const mappedType = mapAppType(app.type);
158
174
  let appGit = undefined;
159
175
  if (isMultiRepo) {
@@ -179,7 +195,7 @@ function convertScanToDetected(scan, root) {
179
195
  git: appGit,
180
196
  };
181
197
  }
182
- // Convert infrastructure
198
+ // Convert infrastructure (keep all, no deduplication - user selects by source file)
183
199
  if (scan.compose) {
184
200
  detected.infrastructure = [];
185
201
  for (const db of scan.compose.databases || []) {
@@ -188,7 +204,7 @@ function convertScanToDetected(scan, root) {
188
204
  type: 'database',
189
205
  image: db.image || 'unknown',
190
206
  port: db.ports?.[0]?.host || 0,
191
- source: 'docker-compose.yml',
207
+ source: db.sourceFile || 'docker-compose.yml',
192
208
  });
193
209
  }
194
210
  for (const cache of scan.compose.caches || []) {
@@ -197,7 +213,7 @@ function convertScanToDetected(scan, root) {
197
213
  type: 'cache',
198
214
  image: cache.image || 'unknown',
199
215
  port: cache.ports?.[0]?.host || 0,
200
- source: 'docker-compose.yml',
216
+ source: cache.sourceFile || 'docker-compose.yml',
201
217
  });
202
218
  }
203
219
  for (const queue of scan.compose.queues || []) {
@@ -206,21 +222,53 @@ function convertScanToDetected(scan, root) {
206
222
  type: 'queue',
207
223
  image: queue.image || 'unknown',
208
224
  port: queue.ports?.[0]?.host || 0,
209
- source: 'docker-compose.yml',
225
+ source: queue.sourceFile || 'docker-compose.yml',
226
+ });
227
+ }
228
+ // Add other infrastructure (vector DBs, search engines, etc.)
229
+ for (const infra of scan.compose.infrastructure || []) {
230
+ detected.infrastructure.push({
231
+ name: infra.name,
232
+ type: 'other',
233
+ image: infra.image || 'unknown',
234
+ port: infra.ports?.[0]?.host || 0,
235
+ source: infra.sourceFile || 'docker-compose.yml',
210
236
  });
211
237
  }
212
238
  // Add Docker application services as apps with runner: 'docker'
213
239
  if (scan.compose.applications && scan.compose.applications.length > 0) {
240
+ // Collect infrastructure service keys (name@source) to exclude from apps
241
+ // Only exclude if same name AND same source file
242
+ const infraKeys = new Set([
243
+ ...(scan.compose.databases || []).map(d => `${d.name}@${d.sourceFile || ''}`),
244
+ ...(scan.compose.caches || []).map(c => `${c.name}@${c.sourceFile || ''}`),
245
+ ...(scan.compose.queues || []).map(q => `${q.name}@${q.sourceFile || ''}`),
246
+ ...(scan.compose.infrastructure || []).map(i => `${i.name}@${i.sourceFile || ''}`),
247
+ ]);
214
248
  for (const dockerApp of scan.compose.applications) {
215
- if (detected.apps[dockerApp.name])
216
- continue;
249
+ const infraCheckKey = `${dockerApp.name}@${dockerApp.sourceFile || ''}`;
250
+ if (infraKeys.has(infraCheckKey))
251
+ continue; // Skip infrastructure services from same file
217
252
  const { type: mappedType, reason: typeReason } = inferDockerAppType(dockerApp.name, dockerApp.build?.context, root);
218
- detected.apps[dockerApp.name] = {
253
+ // Use unique key if app name already exists (from different source)
254
+ let detectedAppKey = dockerApp.name;
255
+ if (detected.apps[detectedAppKey]) {
256
+ // Check if it's from a different source - if so, use source in key
257
+ const existingSource = detected.apps[detectedAppKey].source;
258
+ const newSource = dockerApp.sourceFile || 'docker-compose';
259
+ if (existingSource !== newSource) {
260
+ detectedAppKey = `${dockerApp.name}@${newSource}`;
261
+ }
262
+ else {
263
+ continue; // Same source, skip duplicate
264
+ }
265
+ }
266
+ detected.apps[detectedAppKey] = {
219
267
  path: dockerApp.build?.context || '.',
220
268
  type: mappedType,
221
269
  type_reason: typeReason,
222
270
  runner: 'docker',
223
- runner_reason: 'defined in docker-compose.yml',
271
+ runner_reason: `defined in ${dockerApp.sourceFile || 'docker-compose.yml'}`,
224
272
  docker: {
225
273
  service: dockerApp.name,
226
274
  build_context: dockerApp.build?.context,
@@ -228,7 +276,8 @@ function convertScanToDetected(scan, root) {
228
276
  image: dockerApp.image,
229
277
  },
230
278
  port: dockerApp.ports?.[0]?.host,
231
- port_source: 'docker-compose.yml ports',
279
+ port_source: `${dockerApp.sourceFile || 'docker-compose.yml'} ports`,
280
+ source: dockerApp.sourceFile || 'docker-compose',
232
281
  };
233
282
  }
234
283
  }
@@ -293,44 +342,218 @@ function saveDetectedConfig(rootDir, detected) {
293
342
  // App Configuration
294
343
  // =============================================================================
295
344
  /**
296
- * Interactive app selection
345
+ * Get app source group for grouping purposes
346
+ */
347
+ function getAppSourceGroup(name, app) {
348
+ // Docker apps - group by docker-compose source file
349
+ if (app.runner === 'docker' && app.source) {
350
+ return app.source;
351
+ }
352
+ // Use app path for grouping
353
+ if (app.path && app.path !== '.') {
354
+ return app.path;
355
+ }
356
+ return 'root';
357
+ }
358
+ /**
359
+ * Interactive app selection (grouped by source)
297
360
  */
298
361
  async function selectApps(detected) {
299
362
  const appEntries = Object.entries(detected.apps);
300
363
  if (appEntries.length === 0)
301
364
  return detected;
365
+ // Group apps by source
366
+ const appsBySource = new Map();
367
+ for (const [name, app] of appEntries) {
368
+ const source = getAppSourceGroup(name, app);
369
+ if (!appsBySource.has(source)) {
370
+ appsBySource.set(source, []);
371
+ }
372
+ appsBySource.get(source).push([name, app]);
373
+ }
302
374
  console.log('');
303
375
  console.log(chalk_1.default.blue('=== Detected Apps ==='));
304
376
  console.log('');
305
- for (const [name, app] of appEntries) {
306
- const parts = [
307
- chalk_1.default.cyan(name),
308
- app.type ? `(${app.type})` : '',
309
- app.framework ? `[${app.framework}]` : '',
310
- app.port ? `port:${app.port}` : '',
311
- app.runner ? chalk_1.default.dim(`runner:${app.runner}`) : '',
312
- ].filter(Boolean);
313
- console.log(` ${parts.join(' ')}`);
314
- if (app.git) {
315
- console.log(chalk_1.default.dim(` └─ ${app.git.remote}`));
377
+ // Display grouped by source
378
+ for (const [source, apps] of appsBySource) {
379
+ console.log(chalk_1.default.dim(` ${source}:`));
380
+ for (const [name, app] of apps) {
381
+ const parts = [
382
+ chalk_1.default.cyan(name),
383
+ app.type ? `(${app.type})` : '',
384
+ app.framework ? `[${app.framework}]` : '',
385
+ app.port ? `port:${app.port}` : '',
386
+ app.runner ? chalk_1.default.dim(`runner:${app.runner}`) : '',
387
+ ].filter(Boolean);
388
+ console.log(` ${parts.join(' ')}`);
316
389
  }
317
390
  }
318
391
  console.log('');
319
- const appChoices = appEntries.map(([name, app]) => ({
320
- name: `${name} (${app.type || 'unknown'}${app.framework ? `, ${app.framework}` : ''})`,
321
- value: name,
322
- checked: app.type !== 'library',
323
- }));
324
- const selectedApps = await prompts.checkbox({
325
- message: 'Select apps to include:',
326
- choices: appChoices,
392
+ // If only one source group, select individual apps
393
+ if (appsBySource.size === 1) {
394
+ const appChoices = appEntries.map(([name, app]) => ({
395
+ name: `${name} (${app.type || 'unknown'}${app.framework ? `, ${app.framework}` : ''})`,
396
+ value: name,
397
+ checked: app.type !== 'library',
398
+ }));
399
+ const selectedApps = await prompts.checkbox({
400
+ message: 'Select apps to include:',
401
+ choices: appChoices,
402
+ });
403
+ const filteredApps = {};
404
+ for (const appName of selectedApps) {
405
+ filteredApps[appName] = detected.apps[appName];
406
+ }
407
+ return { ...detected, apps: filteredApps };
408
+ }
409
+ // Multiple source groups - let user select by group first
410
+ const sourceChoices = Array.from(appsBySource.entries()).map(([source, apps]) => {
411
+ const hasLibraryOnly = apps.every(([, a]) => a.type === 'library');
412
+ const appNames = apps.map(([n]) => n).join(', ');
413
+ return {
414
+ name: `${source} (${appNames})`,
415
+ value: source,
416
+ checked: !hasLibraryOnly, // Uncheck if all are libraries
417
+ };
327
418
  });
419
+ const selectedSources = await prompts.checkbox({
420
+ message: 'Select app groups to include:',
421
+ choices: sourceChoices,
422
+ });
423
+ // Collect all apps from selected sources (excluding libraries by default)
328
424
  const filteredApps = {};
329
- for (const appName of selectedApps) {
330
- filteredApps[appName] = detected.apps[appName];
425
+ for (const source of selectedSources) {
426
+ const apps = appsBySource.get(source);
427
+ if (apps) {
428
+ for (const [name, app] of apps) {
429
+ // Include non-libraries by default from selected groups
430
+ if (app.type !== 'library') {
431
+ filteredApps[name] = app;
432
+ }
433
+ }
434
+ }
435
+ }
436
+ // If user selected groups, ask if they want to fine-tune individual apps
437
+ if (selectedSources.length > 0 && Object.keys(filteredApps).length > 0) {
438
+ const fineTune = await prompts.confirm({
439
+ message: `${Object.keys(filteredApps).length} apps selected. Fine-tune individual apps?`,
440
+ default: false,
441
+ });
442
+ if (fineTune) {
443
+ // Show all apps from selected groups for fine-tuning
444
+ const allAppsInGroups = [];
445
+ for (const source of selectedSources) {
446
+ const apps = appsBySource.get(source);
447
+ if (apps)
448
+ allAppsInGroups.push(...apps);
449
+ }
450
+ const appChoices = allAppsInGroups.map(([name, app]) => ({
451
+ name: `${name} (${app.type || 'unknown'}${app.framework ? `, ${app.framework}` : ''})`,
452
+ value: name,
453
+ checked: app.type !== 'library',
454
+ }));
455
+ const selectedApps = await prompts.checkbox({
456
+ message: 'Select individual apps:',
457
+ choices: appChoices,
458
+ });
459
+ const finalApps = {};
460
+ for (const appName of selectedApps) {
461
+ finalApps[appName] = detected.apps[appName];
462
+ }
463
+ return { ...detected, apps: finalApps };
464
+ }
331
465
  }
332
466
  return { ...detected, apps: filteredApps };
333
467
  }
468
+ /**
469
+ * Interactive infrastructure selection (grouped by source file)
470
+ */
471
+ async function selectInfrastructure(detected) {
472
+ if (!detected.infrastructure || detected.infrastructure.length === 0) {
473
+ return detected;
474
+ }
475
+ // Group infrastructure by source file
476
+ const infraBySource = new Map();
477
+ for (const infra of detected.infrastructure) {
478
+ const source = infra.source || 'unknown';
479
+ if (!infraBySource.has(source)) {
480
+ infraBySource.set(source, []);
481
+ }
482
+ infraBySource.get(source).push(infra);
483
+ }
484
+ console.log('');
485
+ console.log(chalk_1.default.blue('=== Detected Infrastructure ==='));
486
+ console.log('');
487
+ // Display grouped by source
488
+ for (const [source, infraList] of infraBySource) {
489
+ console.log(chalk_1.default.dim(` ${source}:`));
490
+ for (const infra of infraList) {
491
+ const parts = [
492
+ chalk_1.default.cyan(infra.name),
493
+ `(${infra.type})`,
494
+ infra.image && infra.image !== 'unknown' ? `[${infra.image}]` : '',
495
+ infra.port ? `port:${infra.port}` : '',
496
+ ].filter(Boolean);
497
+ console.log(` ${parts.join(' ')}`);
498
+ }
499
+ }
500
+ console.log('');
501
+ // If only one source file, select individual services
502
+ if (infraBySource.size === 1) {
503
+ const infraChoices = detected.infrastructure.map(infra => ({
504
+ name: `${infra.name} (${infra.type}${infra.image && infra.image !== 'unknown' ? `, ${infra.image}` : ''})`,
505
+ value: infra.name,
506
+ checked: true,
507
+ }));
508
+ const selectedInfra = await prompts.checkbox({
509
+ message: 'Select infrastructure to include:',
510
+ choices: infraChoices,
511
+ });
512
+ const filteredInfra = detected.infrastructure.filter(infra => selectedInfra.includes(infra.name));
513
+ return { ...detected, infrastructure: filteredInfra.length > 0 ? filteredInfra : undefined };
514
+ }
515
+ // Multiple source files - let user select by file first
516
+ const sourceChoices = Array.from(infraBySource.entries()).map(([source, infraList]) => {
517
+ const serviceNames = infraList.map(i => i.name).join(', ');
518
+ return {
519
+ name: `${source} (${serviceNames})`,
520
+ value: source,
521
+ checked: true,
522
+ };
523
+ });
524
+ const selectedSources = await prompts.checkbox({
525
+ message: 'Select infrastructure sources to include:',
526
+ choices: sourceChoices,
527
+ });
528
+ // Collect all infrastructure from selected sources
529
+ let filteredInfra = [];
530
+ for (const source of selectedSources) {
531
+ const infraList = infraBySource.get(source);
532
+ if (infraList) {
533
+ filteredInfra.push(...infraList);
534
+ }
535
+ }
536
+ // If user selected sources, ask if they want to fine-tune individual services
537
+ if (selectedSources.length > 0 && filteredInfra.length > 0) {
538
+ const fineTune = await prompts.confirm({
539
+ message: `${filteredInfra.length} infrastructure services selected. Fine-tune individual services?`,
540
+ default: false,
541
+ });
542
+ if (fineTune) {
543
+ const infraChoices = filteredInfra.map(infra => ({
544
+ name: `${infra.name} (${infra.type}${infra.image && infra.image !== 'unknown' ? `, ${infra.image}` : ''}) - ${infra.source}`,
545
+ value: `${infra.name}@${infra.source}`, // Unique key
546
+ checked: true,
547
+ }));
548
+ const selectedInfraKeys = await prompts.checkbox({
549
+ message: 'Select individual infrastructure services:',
550
+ choices: infraChoices,
551
+ });
552
+ filteredInfra = filteredInfra.filter(infra => selectedInfraKeys.includes(`${infra.name}@${infra.source}`));
553
+ }
554
+ }
555
+ return { ...detected, infrastructure: filteredInfra.length > 0 ? filteredInfra : undefined };
556
+ }
334
557
  /**
335
558
  * Interactive app editing
336
559
  */
@@ -1832,9 +2055,10 @@ exports.initCommand = new commander_1.Command('init')
1832
2055
  return;
1833
2056
  }
1834
2057
  // =========================================
1835
- // PHASE 2: App Configuration
2058
+ // PHASE 2: App & Infrastructure Configuration
1836
2059
  // =========================================
1837
2060
  detected = await selectApps(detected);
2061
+ detected = await selectInfrastructure(detected);
1838
2062
  if (!options.skipEdit) {
1839
2063
  detected = await editApps(detected);
1840
2064
  }
@@ -93,6 +93,18 @@ const INFRA_PATTERNS = [
93
93
  /^zipkin/i,
94
94
  /^vault/i,
95
95
  /^consul/i,
96
+ // Vector databases
97
+ /^qdrant/i,
98
+ /^weaviate/i,
99
+ /^semitechnologies\/weaviate/i,
100
+ /^pinecone/i,
101
+ /^milvus/i,
102
+ /^chroma/i,
103
+ // Logging
104
+ /^loki/i,
105
+ /^promtail/i,
106
+ /^fluentd/i,
107
+ /^logstash/i,
96
108
  ];
97
109
  class ComposeParser {
98
110
  /**
@@ -107,9 +119,13 @@ class ComposeParser {
107
119
  for (const file of composeFiles) {
108
120
  try {
109
121
  const content = yaml.load(fs.readFileSync(file, 'utf8'));
122
+ // Get relative path for cleaner display
123
+ const relativeFile = path.relative(root, file);
124
+ // Get compose file's directory for resolving relative build contexts
125
+ const composeDir = path.dirname(file);
110
126
  if (content && content.services) {
111
127
  for (const [name, config] of Object.entries(content.services)) {
112
- const service = this.normalizeService(name, config, root);
128
+ const service = this.normalizeService(name, config, root, composeDir, relativeFile);
113
129
  allServices.push(service);
114
130
  }
115
131
  }
@@ -141,33 +157,47 @@ class ComposeParser {
141
157
  }
142
158
  findComposeFiles(root) {
143
159
  const files = [];
144
- const patterns = [
145
- 'docker-compose.yml',
146
- 'docker-compose.yaml',
147
- 'docker-compose.dev.yml',
148
- 'docker-compose.dev.yaml',
149
- 'docker-compose.local.yml',
150
- 'docker-compose.local.yaml',
151
- 'docker-compose.override.yml',
152
- 'docker-compose.override.yaml',
153
- 'docker-compose.secure.yml',
154
- 'docker-compose.secure.yaml',
155
- 'compose.yml',
156
- 'compose.yaml',
157
- ];
158
- for (const pattern of patterns) {
159
- const filePath = path.join(root, pattern);
160
- if (fs.existsSync(filePath)) {
161
- files.push(filePath);
160
+ const seenPaths = new Set();
161
+ const findRecursively = (dir, depth = 0) => {
162
+ // Limit depth to avoid going too deep
163
+ if (depth > 5)
164
+ return;
165
+ try {
166
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
167
+ for (const entry of entries) {
168
+ const fullPath = path.join(dir, entry.name);
169
+ if (entry.isDirectory()) {
170
+ // Skip hidden dirs and node_modules
171
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
172
+ continue;
173
+ findRecursively(fullPath, depth + 1);
174
+ }
175
+ else if (entry.isFile()) {
176
+ // Check if it's a docker-compose file
177
+ const name = entry.name.toLowerCase();
178
+ if ((name.startsWith('docker-compose') || name.startsWith('compose')) &&
179
+ (name.endsWith('.yml') || name.endsWith('.yaml'))) {
180
+ const realPath = fs.realpathSync(fullPath);
181
+ if (!seenPaths.has(realPath)) {
182
+ seenPaths.add(realPath);
183
+ files.push(fullPath);
184
+ }
185
+ }
186
+ }
187
+ }
162
188
  }
163
- }
189
+ catch {
190
+ // Ignore errors reading directories
191
+ }
192
+ };
193
+ findRecursively(root);
164
194
  return files;
165
195
  }
166
- normalizeService(name, config, root) {
196
+ normalizeService(name, config, root, composeDir, sourceFile) {
167
197
  return {
168
198
  name,
169
199
  image: config.image,
170
- build: this.normalizeBuild(config.build, root),
200
+ build: this.normalizeBuild(config.build, root, composeDir),
171
201
  ports: this.normalizePorts(config.ports),
172
202
  environment: this.normalizeEnvironment(config.environment),
173
203
  envFile: this.normalizeEnvFile(config.env_file),
@@ -176,20 +206,25 @@ class ComposeParser {
176
206
  healthcheck: this.normalizeHealthcheck(config.healthcheck),
177
207
  command: this.normalizeCommand(config.command),
178
208
  labels: this.normalizeLabels(config.labels),
209
+ sourceFile,
179
210
  };
180
211
  }
181
- normalizeBuild(build, root) {
212
+ normalizeBuild(build, root, composeDir) {
182
213
  if (!build)
183
214
  return undefined;
184
215
  if (typeof build === 'string') {
216
+ // Resolve relative to compose file directory, then make relative to root
217
+ const absoluteContext = path.resolve(composeDir, build);
185
218
  return {
186
- context: path.resolve(root, build),
219
+ context: path.relative(root, absoluteContext),
187
220
  };
188
221
  }
189
222
  if (typeof build === 'object') {
190
223
  const b = build;
224
+ // Resolve relative to compose file directory, then make relative to root
225
+ const absoluteContext = path.resolve(composeDir, b.context || '.');
191
226
  return {
192
- context: path.resolve(root, b.context || '.'),
227
+ context: path.relative(root, absoluteContext),
193
228
  dockerfile: b.dockerfile,
194
229
  target: b.target,
195
230
  };
@@ -323,8 +323,8 @@ class EnvAnalyzer {
323
323
  return type;
324
324
  }
325
325
  }
326
- // Check value format
327
- if (value) {
326
+ // Check value format (ensure value is a string)
327
+ if (value && typeof value === 'string') {
328
328
  if (/^(true|false)$/i.test(value))
329
329
  return 'boolean';
330
330
  if (/^\d+$/.test(value))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.90",
3
+ "version": "1.0.92",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {