container-superposition 0.1.1 → 0.1.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.
Files changed (136) hide show
  1. package/README.md +206 -1
  2. package/dist/scripts/init.js +235 -179
  3. package/dist/scripts/init.js.map +1 -1
  4. package/dist/tool/commands/doctor.d.ts +15 -0
  5. package/dist/tool/commands/doctor.d.ts.map +1 -0
  6. package/dist/tool/commands/doctor.js +862 -0
  7. package/dist/tool/commands/doctor.js.map +1 -0
  8. package/dist/tool/commands/explain.d.ts +13 -0
  9. package/dist/tool/commands/explain.d.ts.map +1 -0
  10. package/dist/tool/commands/explain.js +211 -0
  11. package/dist/tool/commands/explain.js.map +1 -0
  12. package/dist/tool/commands/list.d.ts +16 -0
  13. package/dist/tool/commands/list.d.ts.map +1 -0
  14. package/dist/tool/commands/list.js +121 -0
  15. package/dist/tool/commands/list.js.map +1 -0
  16. package/dist/tool/commands/plan.d.ts +16 -0
  17. package/dist/tool/commands/plan.d.ts.map +1 -0
  18. package/dist/tool/commands/plan.js +329 -0
  19. package/dist/tool/commands/plan.js.map +1 -0
  20. package/dist/tool/questionnaire/composer.d.ts +6 -1
  21. package/dist/tool/questionnaire/composer.d.ts.map +1 -1
  22. package/dist/tool/questionnaire/composer.js +300 -202
  23. package/dist/tool/questionnaire/composer.js.map +1 -1
  24. package/dist/tool/readme/markdown-parser.d.ts.map +1 -1
  25. package/dist/tool/readme/markdown-parser.js.map +1 -1
  26. package/dist/tool/readme/readme-generator.d.ts.map +1 -1
  27. package/dist/tool/readme/readme-generator.js +11 -6
  28. package/dist/tool/readme/readme-generator.js.map +1 -1
  29. package/dist/tool/schema/deployment-targets.d.ts +77 -0
  30. package/dist/tool/schema/deployment-targets.d.ts.map +1 -0
  31. package/dist/tool/schema/deployment-targets.js +91 -0
  32. package/dist/tool/schema/deployment-targets.js.map +1 -0
  33. package/dist/tool/schema/manifest-migrations.d.ts +51 -0
  34. package/dist/tool/schema/manifest-migrations.d.ts.map +1 -0
  35. package/dist/tool/schema/manifest-migrations.js +159 -0
  36. package/dist/tool/schema/manifest-migrations.js.map +1 -0
  37. package/dist/tool/schema/overlay-loader.d.ts +1 -1
  38. package/dist/tool/schema/overlay-loader.d.ts.map +1 -1
  39. package/dist/tool/schema/overlay-loader.js +42 -14
  40. package/dist/tool/schema/overlay-loader.js.map +1 -1
  41. package/dist/tool/schema/types.d.ts +44 -2
  42. package/dist/tool/schema/types.d.ts.map +1 -1
  43. package/dist/tool/utils/merge.d.ts +134 -0
  44. package/dist/tool/utils/merge.d.ts.map +1 -0
  45. package/dist/tool/utils/merge.js +277 -0
  46. package/dist/tool/utils/merge.js.map +1 -0
  47. package/dist/tool/utils/port-utils.d.ts +29 -0
  48. package/dist/tool/utils/port-utils.d.ts.map +1 -0
  49. package/dist/tool/utils/port-utils.js +128 -0
  50. package/dist/tool/utils/port-utils.js.map +1 -0
  51. package/dist/tool/utils/version.d.ts +9 -0
  52. package/dist/tool/utils/version.d.ts.map +1 -0
  53. package/dist/tool/utils/version.js +32 -0
  54. package/dist/tool/utils/version.js.map +1 -0
  55. package/docs/architecture.md +25 -21
  56. package/docs/deployment-targets.md +150 -0
  57. package/docs/discovery-commands.md +442 -0
  58. package/docs/merge-strategy.md +700 -0
  59. package/docs/minimal-and-editor.md +265 -0
  60. package/docs/overlay-imports.md +209 -0
  61. package/docs/overlay-manifest-refactoring.md +2 -2
  62. package/docs/overlay-metadata-archive.md +1 -1
  63. package/docs/overlays.md +91 -23
  64. package/docs/presets-architecture.md +3 -3
  65. package/docs/presets.md +1 -1
  66. package/docs/publishing.md +36 -35
  67. package/docs/team-workflow.md +540 -0
  68. package/overlays/.presets/data-engineering.yml +392 -0
  69. package/overlays/.presets/event-sourced-service.yml +262 -0
  70. package/overlays/.presets/frontend.yml +287 -0
  71. package/overlays/.presets/k8s-operator-dev.yml +462 -0
  72. package/overlays/.registry/README.md +1 -1
  73. package/overlays/.registry/deployment-targets.yml +54 -0
  74. package/overlays/.shared/README.md +43 -0
  75. package/overlays/.shared/compose/common-healthchecks.yml +38 -0
  76. package/overlays/.shared/otel/instrumentation.env +20 -0
  77. package/overlays/.shared/otel/otel-base-config.yaml +30 -0
  78. package/overlays/.shared/vscode/recommended-extensions.json +14 -0
  79. package/overlays/README.md +1 -1
  80. package/overlays/codex/overlay.yml +1 -0
  81. package/overlays/duckdb/README.md +274 -0
  82. package/overlays/duckdb/devcontainer.patch.json +10 -0
  83. package/overlays/duckdb/overlay.yml +17 -0
  84. package/overlays/duckdb/setup.sh +45 -0
  85. package/overlays/duckdb/verify.sh +32 -0
  86. package/overlays/git-helpers/overlay.yml +1 -0
  87. package/overlays/grafana/README.md +5 -5
  88. package/overlays/grafana/dashboard-provider.yml +1 -1
  89. package/overlays/grafana/docker-compose.yml +2 -2
  90. package/overlays/grafana/overlay.yml +6 -1
  91. package/overlays/jaeger/overlay.yml +16 -3
  92. package/overlays/jupyter/.env.example +6 -0
  93. package/overlays/jupyter/README.md +210 -0
  94. package/overlays/jupyter/devcontainer.patch.json +14 -0
  95. package/overlays/jupyter/docker-compose.yml +23 -0
  96. package/overlays/jupyter/overlay.yml +18 -0
  97. package/overlays/jupyter/verify.sh +35 -0
  98. package/overlays/kind/README.md +221 -0
  99. package/overlays/kind/devcontainer.patch.json +10 -0
  100. package/overlays/kind/overlay.yml +18 -0
  101. package/overlays/kind/setup.sh +43 -0
  102. package/overlays/kind/verify.sh +40 -0
  103. package/overlays/localstack/.env.example +6 -0
  104. package/overlays/localstack/README.md +188 -0
  105. package/overlays/localstack/devcontainer.patch.json +21 -0
  106. package/overlays/localstack/docker-compose.yml +25 -0
  107. package/overlays/localstack/overlay.yml +18 -0
  108. package/overlays/localstack/verify.sh +47 -0
  109. package/overlays/loki/overlay.yml +6 -1
  110. package/overlays/modern-cli-tools/overlay.yml +1 -0
  111. package/overlays/mongodb/overlay.yml +12 -2
  112. package/overlays/mysql/overlay.yml +12 -2
  113. package/overlays/nats/overlay.yml +12 -2
  114. package/overlays/openapi-tools/README.md +243 -0
  115. package/overlays/openapi-tools/devcontainer.patch.json +10 -0
  116. package/overlays/openapi-tools/overlay.yml +16 -0
  117. package/overlays/openapi-tools/setup.sh +45 -0
  118. package/overlays/openapi-tools/verify.sh +51 -0
  119. package/overlays/otel-collector/overlay.yml.example +26 -0
  120. package/overlays/postgres/overlay.yml +6 -1
  121. package/overlays/prometheus/overlay.yml +6 -1
  122. package/overlays/rabbitmq/overlay.yml +12 -2
  123. package/overlays/redis/overlay.yml +6 -1
  124. package/overlays/tilt/README.md +259 -0
  125. package/overlays/tilt/devcontainer.patch.json +17 -0
  126. package/overlays/tilt/overlay.yml +19 -0
  127. package/overlays/tilt/setup.sh +25 -0
  128. package/overlays/tilt/verify.sh +24 -0
  129. package/package.json +8 -6
  130. package/tool/README.md +12 -16
  131. package/tool/schema/overlay-manifest.schema.json +64 -4
  132. package/tool/schema/superposition-manifest.schema.json +104 -0
  133. /package/overlays/{presets → .presets}/docs-site.yml +0 -0
  134. /package/overlays/{presets → .presets}/fullstack.yml +0 -0
  135. /package/overlays/{presets → .presets}/microservice.yml +0 -0
  136. /package/overlays/{presets → .presets}/web-api.yml +0 -0
@@ -0,0 +1,862 @@
1
+ /**
2
+ * Doctor command - Environment validation and diagnostics
3
+ */
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+ import * as net from 'net';
7
+ import { execSync } from 'child_process';
8
+ import chalk from 'chalk';
9
+ import boxen from 'boxen';
10
+ import { loadOverlayManifest } from '../schema/overlay-loader.js';
11
+ import { detectManifestVersion, isVersionSupported, needsMigration, CURRENT_MANIFEST_VERSION, } from '../schema/manifest-migrations.js';
12
+ import { MERGE_STRATEGY } from '../utils/merge.js';
13
+ import { extractPorts } from '../utils/port-utils.js';
14
+ /**
15
+ * Semantic version comparison helper
16
+ */
17
+ function isVersionAtLeast(current, required) {
18
+ const parse = (v) => {
19
+ const parts = v.split('.');
20
+ const major = parseInt(parts[0] ?? '0', 10) || 0;
21
+ const minor = parseInt(parts[1] ?? '0', 10) || 0;
22
+ const patch = parseInt(parts[2] ?? '0', 10) || 0;
23
+ return [major, minor, patch];
24
+ };
25
+ const [cMajor, cMinor, cPatch] = parse(current);
26
+ const [rMajor, rMinor, rPatch] = parse(required);
27
+ if (cMajor !== rMajor) {
28
+ return cMajor > rMajor;
29
+ }
30
+ if (cMinor !== rMinor) {
31
+ return cMinor > rMinor;
32
+ }
33
+ return cPatch >= rPatch;
34
+ }
35
+ /**
36
+ * Check Node.js version compatibility
37
+ */
38
+ function checkNodeVersion() {
39
+ const nodeVersion = process.version;
40
+ const requiredVersion = '18.0.0';
41
+ const versionMatch = nodeVersion.match(/^v(\d+\.\d+\.\d+)/);
42
+ const currentVersion = versionMatch ? versionMatch[1] : '0.0.0';
43
+ const ok = isVersionAtLeast(currentVersion, requiredVersion);
44
+ return {
45
+ name: 'Node.js version',
46
+ status: ok ? 'pass' : 'fail',
47
+ message: ok
48
+ ? `${nodeVersion} (>= ${requiredVersion} required)`
49
+ : `${nodeVersion} - requires >= ${requiredVersion}`,
50
+ details: ok
51
+ ? undefined
52
+ : [
53
+ 'Update Node.js to version 18 or later',
54
+ 'Visit https://nodejs.org/ to download the latest version',
55
+ ],
56
+ };
57
+ }
58
+ /**
59
+ * Check if Docker daemon is accessible
60
+ */
61
+ function checkDocker() {
62
+ try {
63
+ // Use 'docker info' to verify daemon connectivity, not just CLI presence
64
+ execSync('docker info', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
65
+ // Get version for display
66
+ const version = execSync('docker --version', {
67
+ encoding: 'utf8',
68
+ stdio: ['pipe', 'pipe', 'ignore'],
69
+ });
70
+ return {
71
+ name: 'Docker daemon',
72
+ status: 'pass',
73
+ message: version.trim(),
74
+ };
75
+ }
76
+ catch {
77
+ return {
78
+ name: 'Docker daemon',
79
+ status: 'warn',
80
+ message: 'Not accessible',
81
+ details: [
82
+ 'Docker daemon is not running or not accessible',
83
+ 'Install Docker Desktop or Docker Engine',
84
+ 'Ensure Docker daemon is running',
85
+ ],
86
+ };
87
+ }
88
+ }
89
+ /**
90
+ * Check if Docker Compose v2 is available
91
+ */
92
+ function checkDockerCompose() {
93
+ try {
94
+ // Try docker compose (v2 syntax) first
95
+ const version = execSync('docker compose version', {
96
+ encoding: 'utf8',
97
+ stdio: ['pipe', 'pipe', 'ignore'],
98
+ });
99
+ const versionMatch = version.match(/v?(\d+\.\d+\.\d+)/);
100
+ const currentVersion = versionMatch ? versionMatch[1] : '0.0.0';
101
+ const [major] = currentVersion.split('.').map((n) => parseInt(n, 10));
102
+ if (major >= 2) {
103
+ return {
104
+ name: 'Docker Compose',
105
+ status: 'pass',
106
+ message: `v${currentVersion} (v2 required)`,
107
+ };
108
+ }
109
+ else {
110
+ return {
111
+ name: 'Docker Compose',
112
+ status: 'warn',
113
+ message: `v${currentVersion} - v2 recommended`,
114
+ details: [
115
+ 'Docker Compose v2 is recommended for compose-based templates',
116
+ 'Update Docker Desktop or install docker-compose-plugin',
117
+ ],
118
+ };
119
+ }
120
+ }
121
+ catch {
122
+ // Try docker-compose (v1 syntax)
123
+ try {
124
+ const version = execSync('docker-compose --version', {
125
+ encoding: 'utf8',
126
+ stdio: ['pipe', 'pipe', 'ignore'],
127
+ });
128
+ return {
129
+ name: 'Docker Compose',
130
+ status: 'warn',
131
+ message: `${version.trim()} - v2 recommended`,
132
+ details: [
133
+ 'Docker Compose v1 detected',
134
+ 'Consider upgrading to v2: docker compose (not docker-compose)',
135
+ ],
136
+ };
137
+ }
138
+ catch {
139
+ return {
140
+ name: 'Docker Compose',
141
+ status: 'warn',
142
+ message: 'Not found',
143
+ details: [
144
+ 'Docker Compose is required for compose-based templates',
145
+ 'Install Docker Desktop (includes Compose v2)',
146
+ 'Or install docker-compose-plugin',
147
+ ],
148
+ };
149
+ }
150
+ }
151
+ }
152
+ /**
153
+ * Run environment checks
154
+ */
155
+ function checkEnvironment(outputPath) {
156
+ const results = [checkNodeVersion(), checkDocker()];
157
+ // Only check Docker Compose if using compose stack
158
+ const baseTemplate = getBaseTemplateFromManifest(outputPath);
159
+ if (baseTemplate === 'compose') {
160
+ results.push(checkDockerCompose());
161
+ }
162
+ return results;
163
+ }
164
+ /**
165
+ * Get base template from manifest if it exists
166
+ */
167
+ function getBaseTemplateFromManifest(outputPath) {
168
+ const manifestPath = path.join(outputPath, 'superposition.json');
169
+ if (!fs.existsSync(manifestPath)) {
170
+ return undefined;
171
+ }
172
+ try {
173
+ const content = fs.readFileSync(manifestPath, 'utf8');
174
+ const manifest = JSON.parse(content);
175
+ return manifest.baseTemplate;
176
+ }
177
+ catch {
178
+ return undefined;
179
+ }
180
+ }
181
+ /**
182
+ * Validate overlay.yml against schema
183
+ */
184
+ function validateOverlayManifest(overlayDir, overlayId) {
185
+ const manifestPath = path.join(overlayDir, 'overlay.yml');
186
+ if (!fs.existsSync(manifestPath)) {
187
+ return {
188
+ name: `Overlay: ${overlayId}`,
189
+ status: 'fail',
190
+ message: 'Missing overlay.yml manifest',
191
+ details: [`Create overlay.yml in ${overlayDir}`],
192
+ };
193
+ }
194
+ const manifest = loadOverlayManifest(overlayDir);
195
+ if (!manifest) {
196
+ return {
197
+ name: `Overlay: ${overlayId}`,
198
+ status: 'fail',
199
+ message: 'Invalid overlay.yml manifest',
200
+ details: [
201
+ 'Manifest must include: id, name, description, category',
202
+ 'Check YAML syntax and required fields',
203
+ ],
204
+ };
205
+ }
206
+ // Validate required files
207
+ const requiredFiles = ['devcontainer.patch.json'];
208
+ const missingFiles = [];
209
+ for (const file of requiredFiles) {
210
+ const filePath = path.join(overlayDir, file);
211
+ if (!fs.existsSync(filePath)) {
212
+ missingFiles.push(file);
213
+ }
214
+ }
215
+ // Check for broken symlinks
216
+ const entries = fs.readdirSync(overlayDir, { withFileTypes: true });
217
+ const brokenSymlinks = [];
218
+ for (const entry of entries) {
219
+ if (entry.isSymbolicLink()) {
220
+ const linkPath = path.join(overlayDir, entry.name);
221
+ const targetExists = fs.existsSync(linkPath);
222
+ if (!targetExists) {
223
+ brokenSymlinks.push(entry.name);
224
+ }
225
+ }
226
+ }
227
+ if (missingFiles.length > 0 || brokenSymlinks.length > 0) {
228
+ const details = [];
229
+ if (missingFiles.length > 0) {
230
+ details.push(`Missing required files: ${missingFiles.join(', ')}`);
231
+ }
232
+ if (brokenSymlinks.length > 0) {
233
+ details.push(`Broken symlinks: ${brokenSymlinks.join(', ')}`);
234
+ }
235
+ return {
236
+ name: `Overlay: ${overlayId}`,
237
+ status: 'fail',
238
+ message: 'Missing files or broken symlinks',
239
+ details,
240
+ };
241
+ }
242
+ // Check devcontainer.patch.json is valid JSON
243
+ try {
244
+ const patchPath = path.join(overlayDir, 'devcontainer.patch.json');
245
+ const content = fs.readFileSync(patchPath, 'utf8');
246
+ JSON.parse(content);
247
+ }
248
+ catch (error) {
249
+ return {
250
+ name: `Overlay: ${overlayId}`,
251
+ status: 'fail',
252
+ message: 'Invalid devcontainer.patch.json',
253
+ details: [
254
+ `JSON syntax error: ${error instanceof Error ? error.message : String(error)}`,
255
+ ],
256
+ };
257
+ }
258
+ // Validate imports if present
259
+ if (manifest.imports && manifest.imports.length > 0) {
260
+ const overlaysDir = path.dirname(overlayDir);
261
+ const missingImports = [];
262
+ const invalidImports = [];
263
+ for (const importPath of manifest.imports) {
264
+ const fullImportPath = path.join(overlaysDir, importPath);
265
+ if (!fs.existsSync(fullImportPath)) {
266
+ missingImports.push(importPath);
267
+ continue;
268
+ }
269
+ // Validate import file type
270
+ const ext = path.extname(importPath).toLowerCase();
271
+ if (!['.json', '.yaml', '.yml', '.env'].includes(ext)) {
272
+ invalidImports.push(`${importPath} (unsupported type: ${ext})`);
273
+ }
274
+ }
275
+ if (missingImports.length > 0 || invalidImports.length > 0) {
276
+ const details = [];
277
+ if (missingImports.length > 0) {
278
+ details.push(`Missing imports: ${missingImports.join(', ')}`);
279
+ }
280
+ if (invalidImports.length > 0) {
281
+ details.push(`Invalid imports: ${invalidImports.join(', ')}`);
282
+ }
283
+ return {
284
+ name: `Overlay: ${overlayId}`,
285
+ status: 'warn',
286
+ message: 'Import validation issues',
287
+ details,
288
+ };
289
+ }
290
+ }
291
+ return {
292
+ name: `Overlay: ${overlayId}`,
293
+ status: 'pass',
294
+ message: 'Valid',
295
+ };
296
+ }
297
+ /**
298
+ * Validate all overlays
299
+ */
300
+ function checkOverlays(overlaysDir) {
301
+ const results = [];
302
+ if (!fs.existsSync(overlaysDir)) {
303
+ return [
304
+ {
305
+ name: 'Overlays directory',
306
+ status: 'fail',
307
+ message: `Directory not found: ${overlaysDir}`,
308
+ },
309
+ ];
310
+ }
311
+ const entries = fs.readdirSync(overlaysDir, { withFileTypes: true });
312
+ const overlayDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'));
313
+ if (overlayDirs.length === 0) {
314
+ return [
315
+ {
316
+ name: 'Overlays',
317
+ status: 'warn',
318
+ message: 'No overlays found',
319
+ },
320
+ ];
321
+ }
322
+ for (const dir of overlayDirs) {
323
+ const overlayDir = path.join(overlaysDir, dir.name);
324
+ const result = validateOverlayManifest(overlayDir, dir.name);
325
+ results.push(result);
326
+ }
327
+ return results;
328
+ }
329
+ /**
330
+ * Check if a port is in use (cross-platform using Node.js net module)
331
+ */
332
+ function isPortInUse(port) {
333
+ try {
334
+ const server = net.createServer();
335
+ let inUse = false;
336
+ // Try to listen on the port
337
+ server.once('error', (err) => {
338
+ if (err.code === 'EADDRINUSE') {
339
+ inUse = true;
340
+ }
341
+ });
342
+ server.listen(port, '127.0.0.1');
343
+ server.close();
344
+ return inUse;
345
+ }
346
+ catch {
347
+ // If we can't check, assume it's not in use
348
+ return false;
349
+ }
350
+ }
351
+ /**
352
+ * Check port availability for overlays
353
+ */
354
+ function checkPorts(overlaysConfig, manifestPath) {
355
+ const results = [];
356
+ const portsToCheck = new Map();
357
+ // If manifest exists, check ports from manifest
358
+ if (manifestPath && fs.existsSync(manifestPath)) {
359
+ try {
360
+ const manifestContent = fs.readFileSync(manifestPath, 'utf8');
361
+ const manifest = JSON.parse(manifestContent);
362
+ // Collect ports from selected overlays
363
+ const selectedOverlays = manifest.overlays || [];
364
+ for (const overlayId of selectedOverlays) {
365
+ const overlay = overlaysConfig.overlays.find((o) => o.id === overlayId);
366
+ if (overlay && overlay.ports && overlay.ports.length > 0) {
367
+ // Extract numeric ports from overlay
368
+ const ports = extractPorts([overlay]);
369
+ for (const port of ports) {
370
+ const actualPort = port + (manifest.portOffset || 0);
371
+ if (!portsToCheck.has(actualPort)) {
372
+ portsToCheck.set(actualPort, []);
373
+ }
374
+ portsToCheck.get(actualPort).push(overlay.id);
375
+ }
376
+ }
377
+ }
378
+ }
379
+ catch (error) {
380
+ // Ignore manifest parse errors - will be caught in manifest checks
381
+ }
382
+ }
383
+ if (portsToCheck.size === 0) {
384
+ return [];
385
+ }
386
+ // Check each port
387
+ for (const [port, overlayIds] of portsToCheck.entries()) {
388
+ const inUse = isPortInUse(port);
389
+ if (inUse) {
390
+ results.push({
391
+ name: `Port ${port}`,
392
+ status: 'warn',
393
+ message: `Port already in use (used by: ${overlayIds.join(', ')})`,
394
+ details: [
395
+ 'Use --port-offset flag to shift ports',
396
+ 'Or free the port by stopping the conflicting service',
397
+ ],
398
+ });
399
+ }
400
+ }
401
+ return results;
402
+ }
403
+ /**
404
+ * Check manifest compatibility
405
+ */
406
+ function checkManifest(outputPath) {
407
+ const results = [];
408
+ const manifestPath = path.join(outputPath, 'superposition.json');
409
+ // Check if output path exists
410
+ if (!fs.existsSync(outputPath)) {
411
+ return [
412
+ {
413
+ name: 'Devcontainer directory',
414
+ status: 'warn',
415
+ message: `Directory not found: ${outputPath}`,
416
+ details: ['Run "container-superposition init" to create a devcontainer'],
417
+ },
418
+ ];
419
+ }
420
+ // Check if manifest exists
421
+ if (!fs.existsSync(manifestPath)) {
422
+ return [
423
+ {
424
+ name: 'Manifest file',
425
+ status: 'warn',
426
+ message: 'superposition.json not found',
427
+ details: [
428
+ 'This may be a manually created devcontainer',
429
+ 'Or created with an older version of container-superposition',
430
+ ],
431
+ },
432
+ ];
433
+ }
434
+ // Validate manifest JSON
435
+ try {
436
+ const content = fs.readFileSync(manifestPath, 'utf8');
437
+ const manifest = JSON.parse(content);
438
+ // Check manifest version compatibility
439
+ const manifestVersion = detectManifestVersion(manifest);
440
+ const supported = isVersionSupported(manifestVersion);
441
+ const needsUpdate = needsMigration(manifest);
442
+ let versionStatus = 'pass';
443
+ let versionDetails;
444
+ if (!supported) {
445
+ versionStatus = 'fail';
446
+ versionDetails = [
447
+ `Manifest version ${manifestVersion} is not supported`,
448
+ 'Please upgrade your tool or regenerate the manifest',
449
+ ];
450
+ }
451
+ else if (needsUpdate) {
452
+ versionStatus = 'warn';
453
+ versionDetails = [
454
+ `Manifest is using ${manifest.manifestVersion ? 'version ' + manifest.manifestVersion : 'legacy format'}`,
455
+ `Current manifest version: ${CURRENT_MANIFEST_VERSION}`,
456
+ 'Manifest will be automatically migrated on next regeneration',
457
+ ];
458
+ }
459
+ results.push({
460
+ name: 'Manifest version',
461
+ status: versionStatus,
462
+ message: manifest.manifestVersion
463
+ ? `Schema version ${manifest.manifestVersion} (tool ${manifest.generatedBy || 'unknown'})`
464
+ : `Legacy format (tool ${manifest.version || 'unknown'})`,
465
+ details: versionDetails,
466
+ });
467
+ // Check for required fields
468
+ if (!manifest.baseTemplate) {
469
+ results.push({
470
+ name: 'Manifest base template',
471
+ status: 'fail',
472
+ message: 'Missing baseTemplate field',
473
+ });
474
+ }
475
+ // Check devcontainer.json exists
476
+ const devcontainerPath = path.join(outputPath, 'devcontainer.json');
477
+ if (!fs.existsSync(devcontainerPath)) {
478
+ results.push({
479
+ name: 'DevContainer config',
480
+ status: 'fail',
481
+ message: 'devcontainer.json not found',
482
+ details: ['Devcontainer configuration file is missing or corrupted'],
483
+ });
484
+ }
485
+ else {
486
+ // Validate devcontainer.json is valid JSON
487
+ try {
488
+ const devcontainerContent = fs.readFileSync(devcontainerPath, 'utf8');
489
+ JSON.parse(devcontainerContent);
490
+ results.push({
491
+ name: 'DevContainer config',
492
+ status: 'pass',
493
+ message: 'devcontainer.json valid',
494
+ });
495
+ }
496
+ catch {
497
+ results.push({
498
+ name: 'DevContainer config',
499
+ status: 'fail',
500
+ message: 'devcontainer.json has invalid JSON',
501
+ });
502
+ }
503
+ }
504
+ }
505
+ catch (error) {
506
+ results.push({
507
+ name: 'Manifest file',
508
+ status: 'fail',
509
+ message: 'Invalid JSON in superposition.json',
510
+ details: [`Parse error: ${error instanceof Error ? error.message : String(error)}`],
511
+ });
512
+ }
513
+ return results;
514
+ }
515
+ /**
516
+ * Check merge strategy configuration and validation
517
+ */
518
+ function checkMergeStrategy(outputPath) {
519
+ const results = [];
520
+ // Check 1: Merge strategy version info
521
+ results.push({
522
+ name: 'Merge strategy version',
523
+ status: 'pass',
524
+ message: `v${MERGE_STRATEGY.version} (${MERGE_STRATEGY.description})`,
525
+ });
526
+ // Check 2: Validate devcontainer.json structure
527
+ const devcontainerPath = path.join(outputPath, 'devcontainer.json');
528
+ if (fs.existsSync(devcontainerPath)) {
529
+ try {
530
+ const content = fs.readFileSync(devcontainerPath, 'utf8');
531
+ const devcontainer = JSON.parse(content);
532
+ // Check for potential merge conflicts in features
533
+ if (devcontainer.features) {
534
+ const featureKeys = Object.keys(devcontainer.features);
535
+ const duplicateFeatures = featureKeys.filter((key, index) => featureKeys.indexOf(key) !== index);
536
+ if (duplicateFeatures.length > 0) {
537
+ results.push({
538
+ name: 'Feature merge conflicts',
539
+ status: 'warn',
540
+ message: `Duplicate feature keys detected: ${duplicateFeatures.join(', ')}`,
541
+ details: [
542
+ 'Features should have unique keys',
543
+ 'Duplicates may indicate incorrect merge behavior',
544
+ ],
545
+ });
546
+ }
547
+ else {
548
+ results.push({
549
+ name: 'Feature merge',
550
+ status: 'pass',
551
+ message: `${featureKeys.length} features merged successfully`,
552
+ });
553
+ }
554
+ }
555
+ // Check for environment variable conflicts in remoteEnv
556
+ if (devcontainer.remoteEnv) {
557
+ const envKeys = Object.keys(devcontainer.remoteEnv);
558
+ const pathVar = devcontainer.remoteEnv.PATH;
559
+ if (pathVar && pathVar.includes('${containerEnv:PATH}')) {
560
+ results.push({
561
+ name: 'PATH variable merge',
562
+ status: 'pass',
563
+ message: 'PATH correctly includes ${containerEnv:PATH}',
564
+ });
565
+ }
566
+ else if (pathVar) {
567
+ results.push({
568
+ name: 'PATH variable merge',
569
+ status: 'warn',
570
+ message: 'PATH does not include ${containerEnv:PATH}',
571
+ details: [
572
+ 'PATH should end with ${containerEnv:PATH} to preserve system paths',
573
+ 'This may cause unexpected behavior',
574
+ ],
575
+ });
576
+ }
577
+ results.push({
578
+ name: 'Environment variables',
579
+ status: 'pass',
580
+ message: `${envKeys.length} environment variables configured`,
581
+ });
582
+ }
583
+ // Check for array field integrity
584
+ if (devcontainer.forwardPorts && Array.isArray(devcontainer.forwardPorts)) {
585
+ const uniquePorts = new Set(devcontainer.forwardPorts);
586
+ if (uniquePorts.size !== devcontainer.forwardPorts.length) {
587
+ results.push({
588
+ name: 'Port forwarding merge',
589
+ status: 'warn',
590
+ message: 'Duplicate ports in forwardPorts array',
591
+ details: [
592
+ 'Port deduplication may have failed',
593
+ 'This could indicate a merge strategy issue',
594
+ ],
595
+ });
596
+ }
597
+ else {
598
+ results.push({
599
+ name: 'Port forwarding merge',
600
+ status: 'pass',
601
+ message: `${uniquePorts.size} unique ports forwarded`,
602
+ });
603
+ }
604
+ }
605
+ }
606
+ catch (error) {
607
+ results.push({
608
+ name: 'DevContainer merge validation',
609
+ status: 'fail',
610
+ message: 'Unable to validate merge strategy',
611
+ details: [`Error: ${error instanceof Error ? error.message : String(error)}`],
612
+ });
613
+ }
614
+ }
615
+ // Check 3: Validate docker-compose.yml if it exists
616
+ const composePath = path.join(outputPath, 'docker-compose.yml');
617
+ if (fs.existsSync(composePath)) {
618
+ try {
619
+ const content = fs.readFileSync(composePath, 'utf8');
620
+ // Basic validation: check if it's parseable YAML
621
+ const yaml = require('js-yaml');
622
+ const compose = yaml.load(content);
623
+ if (compose.services) {
624
+ const serviceNames = Object.keys(compose.services);
625
+ results.push({
626
+ name: 'Compose service merge',
627
+ status: 'pass',
628
+ message: `${serviceNames.length} services merged successfully`,
629
+ });
630
+ // Check depends_on references
631
+ let hasInvalidDependencies = false;
632
+ const serviceNameSet = new Set(serviceNames);
633
+ for (const [serviceName, service] of Object.entries(compose.services)) {
634
+ if (service.depends_on) {
635
+ const deps = Array.isArray(service.depends_on)
636
+ ? service.depends_on
637
+ : Object.keys(service.depends_on);
638
+ for (const dep of deps) {
639
+ if (!serviceNameSet.has(dep)) {
640
+ hasInvalidDependencies = true;
641
+ break;
642
+ }
643
+ }
644
+ }
645
+ }
646
+ if (hasInvalidDependencies) {
647
+ results.push({
648
+ name: 'Service dependencies',
649
+ status: 'warn',
650
+ message: 'Invalid service dependencies detected',
651
+ details: [
652
+ 'Some depends_on references point to non-existent services',
653
+ 'Dependencies should be filtered during merge',
654
+ ],
655
+ });
656
+ }
657
+ else {
658
+ results.push({
659
+ name: 'Service dependencies',
660
+ status: 'pass',
661
+ message: 'All service dependencies are valid',
662
+ });
663
+ }
664
+ }
665
+ }
666
+ catch (error) {
667
+ results.push({
668
+ name: 'Compose merge validation',
669
+ status: 'warn',
670
+ message: 'Unable to validate docker-compose merge',
671
+ details: [`Error: ${error instanceof Error ? error.message : String(error)}`],
672
+ });
673
+ }
674
+ }
675
+ return results;
676
+ }
677
+ /**
678
+ * Generate doctor report
679
+ */
680
+ function generateReport(environmentChecks, overlayChecks, manifestChecks, mergeChecks, portChecks) {
681
+ const allChecks = [
682
+ ...environmentChecks,
683
+ ...overlayChecks,
684
+ ...manifestChecks,
685
+ ...mergeChecks,
686
+ ...portChecks,
687
+ ];
688
+ const passed = allChecks.filter((c) => c.status === 'pass').length;
689
+ const warnings = allChecks.filter((c) => c.status === 'warn').length;
690
+ const errors = allChecks.filter((c) => c.status === 'fail').length;
691
+ const fixable = allChecks.filter((c) => c.fixable).length;
692
+ return {
693
+ environment: environmentChecks,
694
+ overlays: overlayChecks,
695
+ manifest: manifestChecks,
696
+ merge: mergeChecks,
697
+ ports: portChecks,
698
+ summary: {
699
+ passed,
700
+ warnings,
701
+ errors,
702
+ fixable,
703
+ },
704
+ };
705
+ }
706
+ /**
707
+ * Format check result for display
708
+ */
709
+ function formatCheckResult(check) {
710
+ const icon = check.status === 'pass'
711
+ ? chalk.green('✓')
712
+ : check.status === 'warn'
713
+ ? chalk.yellow('⚠')
714
+ : chalk.red('✗');
715
+ const lines = [` ${icon} ${chalk.white(check.name)}: ${chalk.gray(check.message)}`];
716
+ if (check.details && check.details.length > 0) {
717
+ for (const detail of check.details) {
718
+ lines.push(` ${chalk.dim('→')} ${chalk.dim(detail)}`);
719
+ }
720
+ }
721
+ if (check.fixable) {
722
+ lines.push(` ${chalk.dim('→')} ${chalk.cyan('Fixable with --fix flag')}`);
723
+ }
724
+ return lines.join('\n');
725
+ }
726
+ /**
727
+ * Format doctor report as text
728
+ */
729
+ function formatAsText(report) {
730
+ const lines = [];
731
+ // Environment section
732
+ if (report.environment.length > 0) {
733
+ lines.push(chalk.bold('\nEnvironment:'));
734
+ for (const check of report.environment) {
735
+ lines.push(formatCheckResult(check));
736
+ }
737
+ }
738
+ // Overlays section
739
+ if (report.overlays.length > 0) {
740
+ const failedOverlays = report.overlays.filter((c) => c.status !== 'pass');
741
+ if (failedOverlays.length > 0) {
742
+ lines.push(chalk.bold('\nOverlays:'));
743
+ lines.push(` ${chalk.gray(`Checked ${report.overlays.length} overlays`)}`);
744
+ for (const check of failedOverlays) {
745
+ lines.push(formatCheckResult(check));
746
+ }
747
+ }
748
+ else {
749
+ lines.push(chalk.bold('\nOverlays:'));
750
+ lines.push(` ${chalk.green('✓')} ${chalk.white(`All ${report.overlays.length} overlays valid`)}`);
751
+ }
752
+ }
753
+ // Manifest section
754
+ if (report.manifest.length > 0) {
755
+ lines.push(chalk.bold('\nManifest:'));
756
+ for (const check of report.manifest) {
757
+ lines.push(formatCheckResult(check));
758
+ }
759
+ }
760
+ // Merge strategy section
761
+ if (report.merge.length > 0) {
762
+ lines.push(chalk.bold('\nMerge Strategy:'));
763
+ for (const check of report.merge) {
764
+ lines.push(formatCheckResult(check));
765
+ }
766
+ }
767
+ // Ports section
768
+ if (report.ports.length > 0) {
769
+ lines.push(chalk.bold('\nPort Availability:'));
770
+ for (const check of report.ports) {
771
+ lines.push(formatCheckResult(check));
772
+ }
773
+ }
774
+ // Summary
775
+ lines.push(chalk.bold('\nSummary:'));
776
+ lines.push(` ${chalk.green('✓')} ${report.summary.passed} passed`);
777
+ if (report.summary.warnings > 0) {
778
+ lines.push(` ${chalk.yellow('⚠')} ${report.summary.warnings} warnings`);
779
+ }
780
+ if (report.summary.errors > 0) {
781
+ lines.push(` ${chalk.red('✗')} ${report.summary.errors} errors`);
782
+ }
783
+ if (report.summary.fixable > 0) {
784
+ lines.push(` ${chalk.cyan('ℹ')} ${report.summary.fixable} fixable issues`);
785
+ lines.push(`\n ${chalk.dim('Run with --fix to apply automatic fixes where possible.')}`);
786
+ }
787
+ return lines.join('\n');
788
+ }
789
+ /**
790
+ * Apply automatic fixes
791
+ */
792
+ async function applyFixes(report, outputPath) {
793
+ console.log(chalk.bold('\nApplying fixes...\n'));
794
+ const fixableChecks = [
795
+ ...report.environment,
796
+ ...report.overlays,
797
+ ...report.manifest,
798
+ ...report.merge,
799
+ ...report.ports,
800
+ ].filter((c) => c.fixable);
801
+ if (fixableChecks.length === 0) {
802
+ console.log(chalk.yellow('No automatic fixes available.'));
803
+ return;
804
+ }
805
+ for (const check of fixableChecks) {
806
+ console.log(` ${chalk.cyan('→')} Fixing: ${check.name}...`);
807
+ // Currently, we don't have any auto-fixable issues
808
+ // This is a placeholder for future fix implementations
809
+ console.log(` ${chalk.dim('Manual intervention required')}`);
810
+ }
811
+ }
812
+ /**
813
+ * Doctor command implementation
814
+ */
815
+ export async function doctorCommand(overlaysConfig, overlaysDir, options) {
816
+ const outputPath = options.output || './.devcontainer';
817
+ if (!options.json) {
818
+ console.log('\n' +
819
+ boxen(chalk.bold('🔍 Running diagnostics...'), {
820
+ padding: 0.5,
821
+ borderColor: 'cyan',
822
+ borderStyle: 'round',
823
+ }));
824
+ }
825
+ // Run all checks
826
+ const environmentChecks = checkEnvironment(outputPath);
827
+ const overlayChecks = checkOverlays(overlaysDir);
828
+ const manifestChecks = checkManifest(outputPath);
829
+ const mergeChecks = checkMergeStrategy(outputPath);
830
+ const manifestPath = path.join(outputPath, 'superposition.json');
831
+ const portChecks = checkPorts(overlaysConfig, manifestPath);
832
+ // Generate report
833
+ const report = generateReport(environmentChecks, overlayChecks, manifestChecks, mergeChecks, portChecks);
834
+ // Output results
835
+ if (options.json) {
836
+ console.log(JSON.stringify(report, null, 2));
837
+ }
838
+ else {
839
+ console.log(formatAsText(report));
840
+ }
841
+ // Apply fixes if requested
842
+ if (options.fix && !options.json) {
843
+ await applyFixes(report, outputPath);
844
+ }
845
+ // Exit with appropriate code
846
+ const hasErrors = report.summary.errors > 0;
847
+ const hasWarnings = report.summary.warnings > 0;
848
+ if (!options.json) {
849
+ console.log(''); // Empty line at end
850
+ }
851
+ // Exit with error if there are critical failures
852
+ if (hasErrors) {
853
+ process.exit(1);
854
+ }
855
+ else if (hasWarnings && !options.json) {
856
+ process.exit(0); // Warnings don't fail the command
857
+ }
858
+ else {
859
+ process.exit(0);
860
+ }
861
+ }
862
+ //# sourceMappingURL=doctor.js.map