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
@@ -8,8 +8,15 @@ import boxen from 'boxen';
8
8
  import ora from 'ora';
9
9
  import { select, checkbox, input } from '@inquirer/prompts';
10
10
  import yaml from 'js-yaml';
11
- import { composeDevContainer } from '../tool/questionnaire/composer.js';
11
+ import { composeDevContainer, generateManifestOnly } from '../tool/questionnaire/composer.js';
12
12
  import { loadOverlaysConfig } from '../tool/schema/overlay-loader.js';
13
+ import { listCommand } from '../tool/commands/list.js';
14
+ import { explainCommand } from '../tool/commands/explain.js';
15
+ import { planCommand } from '../tool/commands/plan.js';
16
+ import { doctorCommand } from '../tool/commands/doctor.js';
17
+ import { getIncompatibleOverlays, DEPLOYMENT_TARGETS } from '../tool/schema/deployment-targets.js';
18
+ import { migrateManifest, needsMigration, detectManifestVersion, isVersionSupported, CURRENT_MANIFEST_VERSION, SUPPORTED_MANIFEST_VERSIONS, } from '../tool/schema/manifest-migrations.js';
19
+ import { getToolVersion } from '../tool/utils/version.js';
13
20
  // Get __dirname equivalent in ESM
14
21
  const __filename = fileURLToPath(import.meta.url);
15
22
  const __dirname = path.dirname(__filename);
@@ -32,8 +39,8 @@ const OVERLAYS_CONFIG_CANDIDATES = [
32
39
  const OVERLAYS_CONFIG_PATH = OVERLAYS_CONFIG_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ??
33
40
  OVERLAYS_CONFIG_CANDIDATES[0];
34
41
  const PRESETS_DIR_CANDIDATES = [
35
- path.join(__dirname, '..', 'overlays', 'presets'),
36
- path.join(__dirname, '..', '..', 'overlays', 'presets'),
42
+ path.join(__dirname, '..', 'overlays', '.presets'),
43
+ path.join(__dirname, '..', '..', 'overlays', '.presets'),
37
44
  ];
38
45
  const PRESETS_DIR = PRESETS_DIR_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ??
39
46
  PRESETS_DIR_CANDIDATES[0];
@@ -111,10 +118,29 @@ function findManifestFile(manifestPath) {
111
118
  function loadManifest(manifestPath) {
112
119
  try {
113
120
  const content = fs.readFileSync(manifestPath, 'utf-8');
114
- const manifest = JSON.parse(content);
121
+ const rawManifest = JSON.parse(content);
122
+ // Detect manifest version
123
+ const detectedVersion = detectManifestVersion(rawManifest);
124
+ // Check if version is supported
125
+ if (!isVersionSupported(detectedVersion)) {
126
+ console.error(chalk.red(`✗ Manifest version ${detectedVersion} is not supported.\n` +
127
+ ` This tool supports versions: ${SUPPORTED_MANIFEST_VERSIONS.join(', ')}\n` +
128
+ ` Please upgrade your tool or regenerate the manifest.`));
129
+ return null;
130
+ }
131
+ // Migrate if needed
132
+ let manifest;
133
+ if (needsMigration(rawManifest)) {
134
+ const oldVersion = rawManifest.manifestVersion || rawManifest.version || 'legacy';
135
+ console.log(chalk.cyan(`ℹ️ Migrating manifest from version ${oldVersion} to ${CURRENT_MANIFEST_VERSION}...`));
136
+ manifest = migrateManifest(rawManifest);
137
+ }
138
+ else {
139
+ manifest = rawManifest;
140
+ }
115
141
  // Basic validation
116
- if (!manifest.version || !manifest.baseTemplate) {
117
- console.error(chalk.red('✗ Invalid manifest format: missing required fields (version, baseTemplate)'));
142
+ if (!manifest.baseTemplate) {
143
+ console.error(chalk.red('✗ Invalid manifest format: missing required field "baseTemplate"'));
118
144
  return null;
119
145
  }
120
146
  if (!Array.isArray(manifest.overlays)) {
@@ -125,10 +151,6 @@ function loadManifest(manifestPath) {
125
151
  console.error(chalk.red('✗ Invalid manifest format: all "overlays" entries must be strings'));
126
152
  return null;
127
153
  }
128
- // Version check (warn if different, but continue)
129
- if (manifest.version !== '0.1.0') {
130
- console.warn(chalk.yellow(`⚠️ Manifest version ${manifest.version} may not be fully compatible with this tool`));
131
- }
132
154
  return manifest;
133
155
  }
134
156
  catch (error) {
@@ -603,6 +625,73 @@ async function runQuestionnaire(manifest, manifestDir) {
603
625
  const devTools = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'dev');
604
626
  const database = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'database');
605
627
  const playwright = selectedOverlays.includes('playwright');
628
+ // Check for deployment target compatibility
629
+ let target;
630
+ // Check if any incompatible overlays selected
631
+ const incompatibleOverlays = getIncompatibleOverlays(selectedOverlays, undefined);
632
+ if (incompatibleOverlays.length > 0) {
633
+ console.log(chalk.yellow('\n⚠️ Deployment Target Compatibility Check:\n'));
634
+ console.log(chalk.gray('Some selected overlays may not work in all environments.'));
635
+ console.log();
636
+ // Show incompatibilities
637
+ for (const { overlay, alternatives } of incompatibleOverlays) {
638
+ const overlayMeta = allOverlaysMap.get(overlay);
639
+ console.log(chalk.yellow(` • ${overlayMeta?.name || overlay}`));
640
+ console.log(chalk.gray(` Not compatible with: ${DEPLOYMENT_TARGETS.codespaces.name}, ${DEPLOYMENT_TARGETS.gitpod.name}`));
641
+ if (alternatives.length > 0) {
642
+ const altNames = alternatives
643
+ .map((id) => allOverlaysMap.get(id)?.name || id)
644
+ .join(', ');
645
+ console.log(chalk.cyan(` Alternatives: ${altNames}`));
646
+ }
647
+ console.log();
648
+ }
649
+ const targetChoice = (await select({
650
+ message: 'Which environment are you targeting?',
651
+ choices: [
652
+ {
653
+ name: '🖥️ Local Development (Docker Desktop)',
654
+ value: 'local',
655
+ description: 'Running on your local machine - supports all overlays',
656
+ },
657
+ {
658
+ name: '☁️ GitHub Codespaces',
659
+ value: 'codespaces',
660
+ description: 'Cloud development - may require docker-in-docker',
661
+ },
662
+ {
663
+ name: '🌐 Gitpod',
664
+ value: 'gitpod',
665
+ description: 'Cloud development - may require docker-in-docker',
666
+ },
667
+ {
668
+ name: '📦 DevPod',
669
+ value: 'devpod',
670
+ description: 'Client-only dev environments',
671
+ },
672
+ ],
673
+ default: 'local',
674
+ }));
675
+ target = targetChoice;
676
+ // Show specific incompatibilities for selected target
677
+ if (target !== 'local') {
678
+ const targetIncompatible = getIncompatibleOverlays(selectedOverlays, target);
679
+ if (targetIncompatible.length > 0) {
680
+ console.log(chalk.yellow(`\n⚠️ Warning: Some overlays won't work in ${DEPLOYMENT_TARGETS[target].name}:\n`));
681
+ for (const { overlay, alternatives } of targetIncompatible) {
682
+ const overlayMeta = allOverlaysMap.get(overlay);
683
+ console.log(chalk.red(` ✗ ${overlayMeta?.name || overlay}`));
684
+ if (alternatives.length > 0) {
685
+ const altNames = alternatives
686
+ .map((id) => allOverlaysMap.get(id)?.name || id)
687
+ .join(', ');
688
+ console.log(chalk.cyan(` → Recommended: ${altNames}`));
689
+ }
690
+ }
691
+ console.log();
692
+ }
693
+ }
694
+ }
606
695
  return {
607
696
  stack,
608
697
  baseImage,
@@ -620,6 +709,7 @@ async function runQuestionnaire(manifest, manifestDir) {
620
709
  observability,
621
710
  outputPath,
622
711
  portOffset,
712
+ target,
623
713
  };
624
714
  }
625
715
  catch (error) {
@@ -724,6 +814,12 @@ function buildAnswersFromCliArgs(config) {
724
814
  answers.preset = config.preset;
725
815
  if (config.presetChoices)
726
816
  answers.presetChoices = config.presetChoices;
817
+ if (config.target)
818
+ answers.target = config.target;
819
+ if (config.minimal !== undefined)
820
+ answers.minimal = config.minimal;
821
+ if (config.editor)
822
+ answers.editor = config.editor;
727
823
  return answers;
728
824
  }
729
825
  /**
@@ -768,157 +864,6 @@ function mergeAnswers(...partials) {
768
864
  }
769
865
  return merged;
770
866
  }
771
- /**
772
- * List available overlays command
773
- */
774
- async function listOverlays(options) {
775
- try {
776
- const overlaysConfig = loadOverlaysConfigWrapper();
777
- const category = options.category?.toLowerCase();
778
- console.log('\n' +
779
- boxen(chalk.bold('Available Overlays'), {
780
- padding: 0.5,
781
- borderColor: 'cyan',
782
- borderStyle: 'round',
783
- }));
784
- const categories = [
785
- { name: 'language', title: '📚 Language & Framework' },
786
- { name: 'database', title: '🗄️ Database & Messaging' },
787
- { name: 'observability', title: '📊 Observability' },
788
- { name: 'cloud', title: '☁️ Cloud Tools' },
789
- { name: 'dev', title: '🔧 Dev Tools' },
790
- { name: 'preset', title: '🎯 Presets' },
791
- ];
792
- for (const cat of categories) {
793
- if (category && cat.name !== category)
794
- continue;
795
- const overlays = overlaysConfig.overlays.filter((o) => o.category === cat.name);
796
- if (overlays.length === 0)
797
- continue;
798
- console.log(`\n${chalk.bold(cat.title)}`);
799
- for (const overlay of overlays) {
800
- console.log(` ${chalk.cyan(overlay.id.padEnd(20))} ${chalk.gray(overlay.description)}`);
801
- }
802
- }
803
- console.log(chalk.dim(`\n💡 Use "container-superposition init --language nodejs,python --database postgres" to compose overlays\n`));
804
- process.exit(0);
805
- }
806
- catch (error) {
807
- console.error(chalk.red('✗ Error listing overlays:'), error);
808
- process.exit(1);
809
- }
810
- }
811
- /**
812
- * Doctor command - check environment and validate configuration
813
- */
814
- async function runDoctor(options) {
815
- try {
816
- const outputPath = options.output || './.devcontainer';
817
- console.log('\n' +
818
- boxen(chalk.bold('Environment Check'), {
819
- padding: 0.5,
820
- borderColor: 'cyan',
821
- borderStyle: 'round',
822
- }));
823
- const checks = [];
824
- // Helper function for semantic version comparison
825
- const isVersionAtLeast = (current, required) => {
826
- const parse = (v) => {
827
- const parts = v.split('.');
828
- const major = parseInt(parts[0] ?? '0', 10) || 0;
829
- const minor = parseInt(parts[1] ?? '0', 10) || 0;
830
- const patch = parseInt(parts[2] ?? '0', 10) || 0;
831
- return [major, minor, patch];
832
- };
833
- const [cMajor, cMinor, cPatch] = parse(current);
834
- const [rMajor, rMinor, rPatch] = parse(required);
835
- if (cMajor !== rMajor) {
836
- return cMajor > rMajor;
837
- }
838
- if (cMinor !== rMinor) {
839
- return cMinor > rMinor;
840
- }
841
- return cPatch >= rPatch;
842
- };
843
- // Check Node.js version
844
- const nodeVersion = process.version;
845
- const requiredVersion = '20.0.0';
846
- const versionMatch = nodeVersion.match(/^v(\d+\.\d+\.\d+)/);
847
- const currentVersion = versionMatch ? versionMatch[1] : '0.0.0';
848
- const nodeOk = isVersionAtLeast(currentVersion, requiredVersion);
849
- checks.push({
850
- name: 'Node.js version',
851
- status: nodeOk,
852
- message: nodeOk
853
- ? `${nodeVersion} ✓`
854
- : `${nodeVersion} (requires >= ${requiredVersion})`,
855
- });
856
- // Check if Docker is available
857
- let dockerOk = false;
858
- try {
859
- const { execSync } = await import('child_process');
860
- execSync('docker --version', { stdio: 'ignore' });
861
- dockerOk = true;
862
- }
863
- catch {
864
- dockerOk = false;
865
- }
866
- checks.push({
867
- name: 'Docker',
868
- status: dockerOk,
869
- message: dockerOk ? 'Available ✓' : 'Not found (required for devcontainers)',
870
- });
871
- // Check if devcontainer exists
872
- const devcontainerExists = fs.existsSync(outputPath);
873
- checks.push({
874
- name: 'Devcontainer path',
875
- status: devcontainerExists,
876
- message: devcontainerExists
877
- ? `${outputPath} exists ✓`
878
- : `${outputPath} not found (run init first)`,
879
- });
880
- // Check if manifest exists
881
- if (devcontainerExists) {
882
- const manifestPath = path.join(outputPath, 'superposition.json');
883
- const manifestExists = fs.existsSync(manifestPath);
884
- checks.push({
885
- name: 'Manifest',
886
- status: manifestExists,
887
- message: manifestExists
888
- ? 'superposition.json found ✓'
889
- : 'superposition.json missing (manual edit or old version)',
890
- });
891
- // Check devcontainer.json
892
- const devcontainerJsonPath = path.join(outputPath, 'devcontainer.json');
893
- const devcontainerJsonExists = fs.existsSync(devcontainerJsonPath);
894
- checks.push({
895
- name: 'DevContainer config',
896
- status: devcontainerJsonExists,
897
- message: devcontainerJsonExists
898
- ? 'devcontainer.json found ✓'
899
- : 'devcontainer.json missing',
900
- });
901
- }
902
- console.log('');
903
- for (const check of checks) {
904
- const icon = check.status ? chalk.green('✓') : chalk.red('✗');
905
- console.log(` ${icon} ${chalk.white(check.name)}: ${chalk.gray(check.message)}`);
906
- }
907
- const allPassed = checks.every((c) => c.status);
908
- if (allPassed) {
909
- console.log(chalk.green('\n✓ All checks passed!\n'));
910
- process.exit(0);
911
- }
912
- else {
913
- console.log(chalk.yellow('\n⚠ Some checks failed. See above for details.\n'));
914
- process.exit(1);
915
- }
916
- }
917
- catch (error) {
918
- console.error(chalk.red('✗ Error running doctor:'), error);
919
- process.exit(1);
920
- }
921
- }
922
867
  /**
923
868
  * Parse CLI arguments
924
869
  */
@@ -929,7 +874,7 @@ async function parseCliArgs() {
929
874
  program
930
875
  .name('container-superposition')
931
876
  .description('Composable devcontainer scaffolds')
932
- .version('0.1.0');
877
+ .version(getToolVersion());
933
878
  // Init command (default)
934
879
  program
935
880
  .command('init', { isDefault: true })
@@ -946,7 +891,11 @@ async function parseCliArgs() {
946
891
  .option('--cloud-tools <list>', 'Comma-separated: aws-cli, azure-cli, gcloud, kubectl-helm, terraform, pulumi')
947
892
  .option('--dev-tools <list>', 'Comma-separated: docker-in-docker, docker-sock, playwright, codex, git-helpers, pre-commit, commitlint, just, direnv, modern-cli-tools, ngrok')
948
893
  .option('--port-offset <number>', 'Add offset to all exposed ports (e.g., 100 makes Grafana 3100 instead of 3000)')
894
+ .option('--target <environment>', 'Deployment target: local, codespaces, gitpod, devpod (optimizes for environment)', 'local')
895
+ .option('--minimal', 'Minimal mode - exclude optional/nice-to-have features and extensions')
896
+ .option('--editor <profile>', 'Editor profile: vscode (default), jetbrains, none', 'vscode')
949
897
  .option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
898
+ .option('--write-manifest-only', 'Generate only superposition.json manifest without creating .devcontainer/ files')
950
899
  .action((options) => {
951
900
  // Store options for main() to process
952
901
  initOptions = options;
@@ -958,12 +907,29 @@ async function parseCliArgs() {
958
907
  .option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
959
908
  .option('--no-backup', 'Skip creating backup before regeneration')
960
909
  .option('--backup-dir <path>', 'Custom backup directory location')
910
+ .option('--minimal', 'Minimal mode - exclude optional/nice-to-have features and extensions')
911
+ .option('--editor <profile>', 'Editor profile: vscode (default), jetbrains, none', 'vscode')
961
912
  .action((options) => {
962
913
  const outputPath = options.output || './.devcontainer';
963
- const manifestPath = path.join(outputPath, 'superposition.json');
964
- if (!fs.existsSync(manifestPath)) {
965
- console.error(chalk.red(`✗ Error: No manifest found at ${manifestPath}`));
966
- console.error(chalk.gray(' Run "container-superposition init" first to create a configuration'));
914
+ // Look for manifest in multiple locations for team workflow:
915
+ // 1. Current directory (./superposition.json) - team workflow
916
+ // 2. Output directory (e.g., ./.devcontainer/superposition.json) - legacy
917
+ const manifestSearchPaths = [
918
+ 'superposition.json',
919
+ path.join(outputPath, 'superposition.json'),
920
+ ];
921
+ let manifestPath = null;
922
+ for (const searchPath of manifestSearchPaths) {
923
+ if (fs.existsSync(searchPath)) {
924
+ manifestPath = searchPath;
925
+ break;
926
+ }
927
+ }
928
+ if (!manifestPath) {
929
+ console.error(chalk.red(`✗ Error: No manifest found`));
930
+ console.error(chalk.gray(' Searched for: ./superposition.json, ' +
931
+ path.join(outputPath, 'superposition.json')));
932
+ console.error(chalk.gray(' Run "container-superposition init --write-manifest-only" to create a manifest'));
967
933
  process.exit(1);
968
934
  }
969
935
  // Store options for main() to process
@@ -978,16 +944,47 @@ async function parseCliArgs() {
978
944
  .command('list')
979
945
  .description('List available overlays and presets')
980
946
  .option('--category <type>', 'Filter by category: language, database, observability, cloud, dev, preset')
947
+ .option('--tags <list>', 'Filter by tags (comma-separated)')
948
+ .option('--supports <stack>', 'Filter by stack support: plain, compose')
949
+ .option('--json', 'Output as JSON for scripting')
981
950
  .action(async (options) => {
982
- await listOverlays(options);
951
+ const overlaysConfig = loadOverlaysConfigWrapper();
952
+ await listCommand(overlaysConfig, options);
953
+ process.exit(0);
954
+ });
955
+ // Explain command
956
+ program
957
+ .command('explain <overlay>')
958
+ .description('Show detailed information about an overlay')
959
+ .option('--json', 'Output as JSON for scripting')
960
+ .action(async (overlayId, options) => {
961
+ const overlaysConfig = loadOverlaysConfigWrapper();
962
+ await explainCommand(overlaysConfig, OVERLAYS_DIR, overlayId, options);
963
+ process.exit(0);
964
+ });
965
+ // Plan command
966
+ program
967
+ .command('plan')
968
+ .description('Preview what will be generated before creating devcontainer')
969
+ .option('--stack <type>', 'Base template: plain, compose', 'compose')
970
+ .option('--overlays <list>', 'Comma-separated list of overlay IDs')
971
+ .option('--port-offset <number>', 'Add offset to all exposed ports', (val) => parseInt(val, 10), 0)
972
+ .option('--json', 'Output as JSON for scripting')
973
+ .action(async (options) => {
974
+ const overlaysConfig = loadOverlaysConfigWrapper();
975
+ await planCommand(overlaysConfig, OVERLAYS_DIR, options);
976
+ process.exit(0);
983
977
  });
984
978
  // Doctor command
985
979
  program
986
980
  .command('doctor')
987
981
  .description('Check environment and validate configuration')
988
982
  .option('-o, --output <path>', 'Devcontainer path to validate (default: ./.devcontainer)')
983
+ .option('--fix', 'Apply automatic fixes where possible')
984
+ .option('--json', 'Output as JSON for scripting')
989
985
  .action(async (options) => {
990
- await runDoctor(options);
986
+ const overlaysConfig = loadOverlaysConfigWrapper();
987
+ await doctorCommand(overlaysConfig, OVERLAYS_DIR, options);
991
988
  });
992
989
  await program.parseAsync(process.argv);
993
990
  // If init or regen command was run, return the options
@@ -1030,6 +1027,22 @@ async function parseCliArgs() {
1030
1027
  if (initOptions.portOffset) {
1031
1028
  config.portOffset = parseInt(initOptions.portOffset, 10);
1032
1029
  }
1030
+ if (initOptions.target) {
1031
+ config.target = initOptions.target;
1032
+ }
1033
+ if (initOptions.minimal) {
1034
+ config.minimal = true;
1035
+ }
1036
+ if (initOptions.editor) {
1037
+ const editorLower = initOptions.editor.toLowerCase();
1038
+ if (['vscode', 'jetbrains', 'none'].includes(editorLower)) {
1039
+ config.editor = editorLower;
1040
+ }
1041
+ else {
1042
+ console.warn(chalk.yellow(`⚠️ Invalid editor profile: ${initOptions.editor}, using default (vscode)`));
1043
+ config.editor = 'vscode';
1044
+ }
1045
+ }
1033
1046
  if (initOptions.output)
1034
1047
  config.outputPath = initOptions.output;
1035
1048
  return {
@@ -1038,6 +1051,7 @@ async function parseCliArgs() {
1038
1051
  noBackup: initOptions.backup === false, // Commander creates options.backup = false for --no-backup
1039
1052
  backupDir: initOptions.backupDir,
1040
1053
  noInteractive: initOptions.interactive === false, // Commander creates options.interactive = false for --no-interactive
1054
+ writeManifestOnly: initOptions.writeManifestOnly === true,
1041
1055
  };
1042
1056
  }
1043
1057
  async function main() {
@@ -1091,8 +1105,12 @@ async function main() {
1091
1105
  }
1092
1106
  // Build answers based on mode
1093
1107
  let answers;
1094
- if (useManifestOnly && manifest) {
1095
- // Mode 1: Manifest-only (--from-manifest --no-interactive)
1108
+ // Check if there are CLI overrides beyond just output path
1109
+ const hasCliOverrides = cliArgs &&
1110
+ Object.keys(cliArgs.config).some((key) => key !== 'outputPath' &&
1111
+ cliArgs.config[key] !== undefined);
1112
+ if (useManifestOnly && manifest && !hasCliOverrides) {
1113
+ // Mode 1: Manifest-only (--from-manifest --no-interactive, no CLI overrides)
1096
1114
  const manifestAnswers = buildAnswersFromManifest(manifest, manifestDir);
1097
1115
  answers = mergeAnswers(manifestAnswers);
1098
1116
  console.log('\n' +
@@ -1110,8 +1128,9 @@ async function main() {
1110
1128
  : '') +
1111
1129
  chalk.gray(` Output: ${answers.outputPath}`), { padding: 1, borderColor: 'cyan', borderStyle: 'round', margin: 1 }));
1112
1130
  }
1113
- else if (cliArgs && cliArgs.config.stack) {
1131
+ else if (cliArgs && (cliArgs.config.stack || hasCliOverrides)) {
1114
1132
  // Mode 2: CLI-based (with optional manifest defaults)
1133
+ // This includes regen with --minimal or --editor flags
1115
1134
  const cliAnswers = buildAnswersFromCliArgs(cliArgs.config);
1116
1135
  const manifestAnswers = manifest
1117
1136
  ? buildAnswersFromManifest(manifest, manifestDir)
@@ -1119,12 +1138,26 @@ async function main() {
1119
1138
  answers = mergeAnswers(manifestAnswers, cliAnswers, {
1120
1139
  outputPath: cliAnswers.outputPath || './.devcontainer',
1121
1140
  });
1141
+ const modeLabel = useManifestOnly && hasCliOverrides
1142
+ ? 'Regenerating from Manifest with Overrides'
1143
+ : 'Running in CLI mode';
1122
1144
  console.log('\n' +
1123
- boxen(chalk.bold('Running in CLI mode'), {
1145
+ boxen(chalk.bold(modeLabel), {
1124
1146
  padding: 0.5,
1125
1147
  borderColor: 'blue',
1126
1148
  borderStyle: 'round',
1127
1149
  }));
1150
+ // Show what's being overridden
1151
+ if (useManifestOnly && hasCliOverrides) {
1152
+ const overrides = [];
1153
+ if (cliAnswers.minimal)
1154
+ overrides.push('minimal mode');
1155
+ if (cliAnswers.editor)
1156
+ overrides.push(`editor: ${cliAnswers.editor}`);
1157
+ if (overrides.length > 0) {
1158
+ console.log(chalk.dim(` Overrides: ${overrides.join(', ')}`));
1159
+ }
1160
+ }
1128
1161
  }
1129
1162
  else {
1130
1163
  // Mode 3: Interactive (with optional manifest pre-population)
@@ -1157,27 +1190,50 @@ async function main() {
1157
1190
  borderStyle: 'round',
1158
1191
  margin: { top: 0, bottom: 1 },
1159
1192
  }));
1193
+ // Check if we're in manifest-only mode
1194
+ const isManifestOnly = cliArgs?.writeManifestOnly === true;
1160
1195
  // Generate with spinner
1161
1196
  const spinner = ora({
1162
- text: chalk.cyan('Generating devcontainer configuration...'),
1197
+ text: isManifestOnly
1198
+ ? chalk.cyan('Generating manifest file...')
1199
+ : chalk.cyan('Generating devcontainer configuration...'),
1163
1200
  color: 'cyan',
1164
1201
  }).start();
1165
1202
  try {
1166
- await composeDevContainer(answers);
1167
- spinner.succeed(chalk.green('DevContainer created successfully!'));
1203
+ if (isManifestOnly) {
1204
+ await generateManifestOnly(answers);
1205
+ spinner.succeed(chalk.green('Manifest created successfully!'));
1206
+ }
1207
+ else {
1208
+ await composeDevContainer(answers);
1209
+ spinner.succeed(chalk.green('DevContainer created successfully!'));
1210
+ }
1168
1211
  }
1169
1212
  catch (error) {
1170
- spinner.fail(chalk.red('Failed to create devcontainer'));
1213
+ spinner.fail(chalk.red(isManifestOnly ? 'Failed to create manifest' : 'Failed to create devcontainer'));
1171
1214
  throw error;
1172
1215
  }
1173
1216
  // Success message
1174
- console.log('\n' +
1175
- boxen(chalk.bold.green('✓ Setup Complete!\n\n') +
1217
+ const successMessage = isManifestOnly
1218
+ ? chalk.bold.green('✓ Manifest Created!\n\n') +
1219
+ chalk.white('Next steps:\n') +
1220
+ chalk.gray(' 1. Review the generated superposition.json file\n') +
1221
+ chalk.gray(' 2. Commit it to your repository\n') +
1222
+ chalk.gray(' 3. Team members can run "npx container-superposition regen"\n\n') +
1223
+ chalk.dim('Team workflow: commit manifest, .gitignore .devcontainer/, customize with .devcontainer/custom/')
1224
+ : chalk.bold.green('✓ Setup Complete!\n\n') +
1176
1225
  chalk.white('Next steps:\n') +
1177
1226
  chalk.gray(' 1. Review the generated .devcontainer/ folder\n') +
1178
1227
  chalk.gray(" 2. Customize as needed (it's just normal JSON!)\n") +
1179
1228
  chalk.gray(' 3. Open in VS Code and rebuild container\n\n') +
1180
- chalk.dim('The generated configuration is fully editable and independent of this tool.'), { padding: 1, borderColor: 'green', borderStyle: 'double', margin: 1 }));
1229
+ chalk.dim('The generated configuration is fully editable and independent of this tool.');
1230
+ console.log('\n' +
1231
+ boxen(successMessage, {
1232
+ padding: 1,
1233
+ borderColor: 'green',
1234
+ borderStyle: 'double',
1235
+ margin: 1,
1236
+ }));
1181
1237
  }
1182
1238
  catch (error) {
1183
1239
  console.error('\n' +