genbox 1.0.91 → 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.
- package/dist/commands/init.js +241 -50
- package/dist/scanner/compose-parser.js +27 -6
- package/dist/scanner/env-analyzer.js +2 -2
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -151,9 +151,25 @@ function convertScanToDetected(scan, root) {
|
|
|
151
151
|
})),
|
|
152
152
|
apps: {},
|
|
153
153
|
};
|
|
154
|
-
//
|
|
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
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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:
|
|
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,74 +342,216 @@ function saveDetectedConfig(rootDir, detected) {
|
|
|
293
342
|
// App Configuration
|
|
294
343
|
// =============================================================================
|
|
295
344
|
/**
|
|
296
|
-
*
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
|
330
|
-
|
|
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
|
}
|
|
334
468
|
/**
|
|
335
|
-
* Interactive infrastructure selection
|
|
469
|
+
* Interactive infrastructure selection (grouped by source file)
|
|
336
470
|
*/
|
|
337
471
|
async function selectInfrastructure(detected) {
|
|
338
472
|
if (!detected.infrastructure || detected.infrastructure.length === 0) {
|
|
339
473
|
return detected;
|
|
340
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
|
+
}
|
|
341
484
|
console.log('');
|
|
342
485
|
console.log(chalk_1.default.blue('=== Detected Infrastructure ==='));
|
|
343
486
|
console.log('');
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
+
}
|
|
352
499
|
}
|
|
353
500
|
console.log('');
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
+
};
|
|
362
523
|
});
|
|
363
|
-
const
|
|
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
|
+
}
|
|
364
555
|
return { ...detected, infrastructure: filteredInfra.length > 0 ? filteredInfra : undefined };
|
|
365
556
|
}
|
|
366
557
|
/**
|
|
@@ -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
|
}
|
|
@@ -177,11 +193,11 @@ class ComposeParser {
|
|
|
177
193
|
findRecursively(root);
|
|
178
194
|
return files;
|
|
179
195
|
}
|
|
180
|
-
normalizeService(name, config, root) {
|
|
196
|
+
normalizeService(name, config, root, composeDir, sourceFile) {
|
|
181
197
|
return {
|
|
182
198
|
name,
|
|
183
199
|
image: config.image,
|
|
184
|
-
build: this.normalizeBuild(config.build, root),
|
|
200
|
+
build: this.normalizeBuild(config.build, root, composeDir),
|
|
185
201
|
ports: this.normalizePorts(config.ports),
|
|
186
202
|
environment: this.normalizeEnvironment(config.environment),
|
|
187
203
|
envFile: this.normalizeEnvFile(config.env_file),
|
|
@@ -190,20 +206,25 @@ class ComposeParser {
|
|
|
190
206
|
healthcheck: this.normalizeHealthcheck(config.healthcheck),
|
|
191
207
|
command: this.normalizeCommand(config.command),
|
|
192
208
|
labels: this.normalizeLabels(config.labels),
|
|
209
|
+
sourceFile,
|
|
193
210
|
};
|
|
194
211
|
}
|
|
195
|
-
normalizeBuild(build, root) {
|
|
212
|
+
normalizeBuild(build, root, composeDir) {
|
|
196
213
|
if (!build)
|
|
197
214
|
return undefined;
|
|
198
215
|
if (typeof build === 'string') {
|
|
216
|
+
// Resolve relative to compose file directory, then make relative to root
|
|
217
|
+
const absoluteContext = path.resolve(composeDir, build);
|
|
199
218
|
return {
|
|
200
|
-
context: path.
|
|
219
|
+
context: path.relative(root, absoluteContext),
|
|
201
220
|
};
|
|
202
221
|
}
|
|
203
222
|
if (typeof build === 'object') {
|
|
204
223
|
const b = build;
|
|
224
|
+
// Resolve relative to compose file directory, then make relative to root
|
|
225
|
+
const absoluteContext = path.resolve(composeDir, b.context || '.');
|
|
205
226
|
return {
|
|
206
|
-
context: path.
|
|
227
|
+
context: path.relative(root, absoluteContext),
|
|
207
228
|
dockerfile: b.dockerfile,
|
|
208
229
|
target: b.target,
|
|
209
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))
|