container-superposition 0.1.7 → 0.1.9

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 (242) hide show
  1. package/README.md +24 -15
  2. package/dist/scripts/init.js +1 -1537
  3. package/dist/scripts/init.js.map +1 -1
  4. package/dist/tool/cli/args.d.ts +20 -0
  5. package/dist/tool/cli/args.d.ts.map +1 -0
  6. package/dist/tool/cli/args.js +325 -0
  7. package/dist/tool/cli/args.js.map +1 -0
  8. package/dist/tool/cli/run.d.ts +2 -0
  9. package/dist/tool/cli/run.d.ts.map +1 -0
  10. package/dist/tool/cli/run.js +318 -0
  11. package/dist/tool/cli/run.js.map +1 -0
  12. package/dist/tool/commands/adopt.js +1 -1
  13. package/dist/tool/commands/adopt.js.map +1 -1
  14. package/dist/tool/commands/doctor.d.ts +1 -0
  15. package/dist/tool/commands/doctor.d.ts.map +1 -1
  16. package/dist/tool/commands/doctor.js +1510 -78
  17. package/dist/tool/commands/doctor.js.map +1 -1
  18. package/dist/tool/commands/explain.d.ts.map +1 -1
  19. package/dist/tool/commands/explain.js +9 -0
  20. package/dist/tool/commands/explain.js.map +1 -1
  21. package/dist/tool/commands/migrate.d.ts +7 -0
  22. package/dist/tool/commands/migrate.d.ts.map +1 -0
  23. package/dist/tool/commands/migrate.js +52 -0
  24. package/dist/tool/commands/migrate.js.map +1 -0
  25. package/dist/tool/questionnaire/answers.d.ts +16 -0
  26. package/dist/tool/questionnaire/answers.d.ts.map +1 -0
  27. package/dist/tool/questionnaire/answers.js +102 -0
  28. package/dist/tool/questionnaire/answers.js.map +1 -0
  29. package/dist/tool/questionnaire/composer.d.ts +6 -4
  30. package/dist/tool/questionnaire/composer.d.ts.map +1 -1
  31. package/dist/tool/questionnaire/composer.js +778 -45
  32. package/dist/tool/questionnaire/composer.js.map +1 -1
  33. package/dist/tool/questionnaire/presets.d.ts +60 -0
  34. package/dist/tool/questionnaire/presets.d.ts.map +1 -0
  35. package/dist/tool/questionnaire/presets.js +165 -0
  36. package/dist/tool/questionnaire/presets.js.map +1 -0
  37. package/dist/tool/questionnaire/questionnaire.d.ts +10 -0
  38. package/dist/tool/questionnaire/questionnaire.d.ts.map +1 -0
  39. package/dist/tool/questionnaire/questionnaire.js +582 -0
  40. package/dist/tool/questionnaire/questionnaire.js.map +1 -0
  41. package/dist/tool/schema/manifest-migrations.d.ts +5 -0
  42. package/dist/tool/schema/manifest-migrations.d.ts.map +1 -1
  43. package/dist/tool/schema/manifest-migrations.js +45 -0
  44. package/dist/tool/schema/manifest-migrations.js.map +1 -1
  45. package/dist/tool/schema/overlay-loader.d.ts.map +1 -1
  46. package/dist/tool/schema/overlay-loader.js +24 -0
  47. package/dist/tool/schema/overlay-loader.js.map +1 -1
  48. package/dist/tool/schema/project-config.d.ts +13 -1
  49. package/dist/tool/schema/project-config.d.ts.map +1 -1
  50. package/dist/tool/schema/project-config.js +188 -10
  51. package/dist/tool/schema/project-config.js.map +1 -1
  52. package/dist/tool/schema/target-rules.d.ts +78 -0
  53. package/dist/tool/schema/target-rules.d.ts.map +1 -0
  54. package/dist/tool/schema/target-rules.js +367 -0
  55. package/dist/tool/schema/target-rules.js.map +1 -0
  56. package/dist/tool/schema/types.d.ts +42 -3
  57. package/dist/tool/schema/types.d.ts.map +1 -1
  58. package/dist/tool/utils/parameters.d.ts +76 -0
  59. package/dist/tool/utils/parameters.d.ts.map +1 -0
  60. package/dist/tool/utils/parameters.js +125 -0
  61. package/dist/tool/utils/parameters.js.map +1 -0
  62. package/dist/tool/utils/paths.d.ts +2 -0
  63. package/dist/tool/utils/paths.d.ts.map +1 -0
  64. package/dist/tool/utils/paths.js +31 -0
  65. package/dist/tool/utils/paths.js.map +1 -0
  66. package/docs/deployment-targets.md +88 -56
  67. package/docs/examples.md +20 -17
  68. package/docs/filesystem-contract.md +5 -0
  69. package/docs/minimal-and-editor.md +65 -5
  70. package/docs/overlay-imports.md +92 -14
  71. package/docs/overlays.md +231 -135
  72. package/docs/specs/001-verbose-plan-graph/spec.md +5 -12
  73. package/docs/specs/002-superposition-config-file/spec.md +5 -12
  74. package/docs/specs/003-mkdocs2-overlay/spec.md +2 -9
  75. package/docs/specs/004-doctor-fix/spec.md +1 -8
  76. package/docs/specs/005-cuda-overlay/spec.md +2 -9
  77. package/docs/specs/006-rocm-overlay/spec.md +3 -10
  78. package/docs/specs/007-target-aware-generation/spec.md +119 -0
  79. package/docs/specs/008-project-file-canonical/spec.md +82 -0
  80. package/docs/specs/009-project-env/spec.md +140 -0
  81. package/docs/specs/010-compose-env-materialization/spec.md +123 -0
  82. package/docs/specs/011-overlay-parameters/spec.md +228 -0
  83. package/docs/specs/012-ollama-cli-overlay/spec.md +47 -0
  84. package/docs/specs/013-doctor-dependency-check/spec.md +250 -0
  85. package/docs/specs/014-doctor-compose-port-cross-validation/spec.md +276 -0
  86. package/docs/specs/015-doctor-env-example-drift/spec.md +248 -0
  87. package/docs/specs/016-doctor-reproducibility-check/spec.md +276 -0
  88. package/docs/specs/017-doctor-dry-run/spec.md +276 -0
  89. package/docs/specs/018-init-project-file/spec.md +59 -0
  90. package/docs/specs/taxonomy.md +186 -0
  91. package/overlays/.presets/full-observability.yml +113 -0
  92. package/overlays/.presets/k8s-dev.yml +174 -0
  93. package/overlays/.presets/local-llm.yml +105 -0
  94. package/overlays/.presets/vector-ai.yml +150 -0
  95. package/overlays/.shared/README.md +27 -2
  96. package/overlays/.shared/compose/nvidia-gpu-devcontainer.yml +22 -0
  97. package/overlays/.shared/vscode/js-ts-settings.json +19 -0
  98. package/overlays/.shared/vscode/markdown-extensions.json +8 -0
  99. package/overlays/alertmanager/devcontainer.patch.json +0 -1
  100. package/overlays/alertmanager/docker-compose.yml +8 -0
  101. package/overlays/alertmanager/overlay.yml +1 -0
  102. package/overlays/amp/devcontainer.patch.json +4 -1
  103. package/overlays/bun/devcontainer.patch.json +1 -10
  104. package/overlays/bun/overlay.yml +8 -1
  105. package/overlays/claude-code/devcontainer.patch.json +6 -1
  106. package/overlays/codex/devcontainer.patch.json +5 -0
  107. package/overlays/comfyui/.env.example +34 -0
  108. package/overlays/comfyui/README.md +342 -0
  109. package/overlays/comfyui/devcontainer.patch.json +15 -0
  110. package/overlays/comfyui/docker-compose.yml +40 -0
  111. package/overlays/comfyui/overlay.yml +24 -0
  112. package/overlays/comfyui/setup.sh +36 -0
  113. package/overlays/comfyui/verify.sh +103 -0
  114. package/overlays/commitlint/devcontainer.patch.json +1 -6
  115. package/overlays/docker-sock/overlay.yml +1 -0
  116. package/overlays/dotnet/overlay.yml +4 -1
  117. package/overlays/fuseki/.env.example +5 -0
  118. package/overlays/fuseki/README.md +173 -0
  119. package/overlays/fuseki/devcontainer.patch.json +18 -0
  120. package/overlays/fuseki/docker-compose.yml +29 -0
  121. package/overlays/fuseki/overlay.yml +42 -0
  122. package/overlays/fuseki/verify.sh +58 -0
  123. package/overlays/gemini-cli/devcontainer.patch.json +4 -1
  124. package/overlays/go/overlay.yml +6 -1
  125. package/overlays/grafana/devcontainer.patch.json +0 -1
  126. package/overlays/grafana/docker-compose.yml +8 -2
  127. package/overlays/grafana/overlay.yml +6 -1
  128. package/overlays/jaeger/.env.example +11 -0
  129. package/overlays/jaeger/README.md +33 -4
  130. package/overlays/jaeger/devcontainer.patch.json +9 -1
  131. package/overlays/jaeger/docker-compose.yml +17 -0
  132. package/overlays/jaeger/overlay.yml +1 -12
  133. package/overlays/java/overlay.yml +6 -1
  134. package/overlays/jupyter/docker-compose.yml +1 -0
  135. package/overlays/jupyter/overlay.yml +1 -0
  136. package/overlays/k3d/README.md +201 -0
  137. package/overlays/k3d/devcontainer.patch.json +9 -0
  138. package/overlays/k3d/overlay.yml +19 -0
  139. package/overlays/k3d/setup.sh +34 -0
  140. package/overlays/k3d/verify.sh +38 -0
  141. package/overlays/keycloak/devcontainer.patch.json +0 -1
  142. package/overlays/keycloak/docker-compose.yml +1 -0
  143. package/overlays/keycloak/overlay.yml +15 -0
  144. package/overlays/localstack/docker-compose.yml +1 -0
  145. package/overlays/localstack/overlay.yml +19 -1
  146. package/overlays/loki/devcontainer.patch.json +0 -1
  147. package/overlays/loki/docker-compose.yml +8 -0
  148. package/overlays/loki/overlay.yml +1 -0
  149. package/overlays/mailpit/docker-compose.yml +1 -0
  150. package/overlays/mailpit/overlay.yml +1 -0
  151. package/overlays/minio/devcontainer.patch.json +1 -1
  152. package/overlays/minio/docker-compose.yml +1 -0
  153. package/overlays/minio/overlay.yml +23 -2
  154. package/overlays/mkdocs/devcontainer.patch.json +1 -5
  155. package/overlays/mkdocs/overlay.yml +3 -1
  156. package/overlays/mkdocs2/devcontainer.patch.json +1 -5
  157. package/overlays/mkdocs2/overlay.yml +2 -0
  158. package/overlays/mongodb/docker-compose.yml +2 -0
  159. package/overlays/mongodb/overlay.yml +26 -2
  160. package/overlays/mysql/docker-compose.yml +2 -0
  161. package/overlays/mysql/overlay.yml +36 -2
  162. package/overlays/nats/docker-compose.yml +1 -0
  163. package/overlays/nats/overlay.yml +18 -2
  164. package/overlays/nodejs/devcontainer.patch.json +1 -12
  165. package/overlays/nodejs/overlay.yml +8 -1
  166. package/overlays/ollama/.env.example +14 -0
  167. package/overlays/ollama/README.md +326 -0
  168. package/overlays/ollama/devcontainer.patch.json +14 -0
  169. package/overlays/ollama/docker-compose.yml +25 -0
  170. package/overlays/ollama/overlay.yml +27 -0
  171. package/overlays/ollama/verify.sh +76 -0
  172. package/overlays/ollama-cli/README.md +90 -0
  173. package/overlays/ollama-cli/devcontainer.patch.json +3 -0
  174. package/overlays/ollama-cli/overlay.yml +19 -0
  175. package/overlays/ollama-cli/setup.sh +103 -0
  176. package/overlays/ollama-cli/verify.sh +49 -0
  177. package/overlays/open-webui/.env.example +5 -0
  178. package/overlays/open-webui/README.md +162 -0
  179. package/overlays/open-webui/devcontainer.patch.json +14 -0
  180. package/overlays/open-webui/docker-compose.yml +24 -0
  181. package/overlays/open-webui/overlay.yml +45 -0
  182. package/overlays/opencode/devcontainer.patch.json +4 -1
  183. package/overlays/otel-collector/README.md +4 -0
  184. package/overlays/otel-collector/devcontainer.patch.json +4 -1
  185. package/overlays/otel-collector/docker-compose.yml +8 -4
  186. package/overlays/otel-collector/overlay.yml +1 -0
  187. package/overlays/otel-demo-nodejs/devcontainer.patch.json +0 -1
  188. package/overlays/otel-demo-nodejs/docker-compose.yml +1 -0
  189. package/overlays/otel-demo-nodejs/overlay.yml +9 -1
  190. package/overlays/otel-demo-python/devcontainer.patch.json +0 -1
  191. package/overlays/otel-demo-python/docker-compose.yml +1 -0
  192. package/overlays/otel-demo-python/overlay.yml +6 -1
  193. package/overlays/pandoc/README.md +10 -0
  194. package/overlays/pandoc/devcontainer.patch.json +0 -5
  195. package/overlays/pandoc/overlay.yml +2 -0
  196. package/overlays/pandoc/setup.sh +10 -0
  197. package/overlays/pgvector/.env.example +6 -0
  198. package/overlays/pgvector/README.md +215 -0
  199. package/overlays/pgvector/devcontainer.patch.json +29 -0
  200. package/overlays/pgvector/docker-compose.yml +33 -0
  201. package/overlays/pgvector/overlay.yml +47 -0
  202. package/overlays/playwright/devcontainer.patch.json +0 -5
  203. package/overlays/playwright/overlay.yml +2 -1
  204. package/overlays/postgres/.env.example +5 -5
  205. package/overlays/postgres/devcontainer.patch.json +4 -4
  206. package/overlays/postgres/docker-compose.yml +11 -6
  207. package/overlays/postgres/overlay.yml +23 -2
  208. package/overlays/pre-commit/devcontainer.patch.json +1 -7
  209. package/overlays/prometheus/devcontainer.patch.json +0 -1
  210. package/overlays/prometheus/docker-compose.yml +8 -0
  211. package/overlays/prometheus/overlay.yml +1 -0
  212. package/overlays/promtail/devcontainer.patch.json +1 -2
  213. package/overlays/promtail/docker-compose.yml +8 -0
  214. package/overlays/promtail/overlay.yml +1 -0
  215. package/overlays/qdrant/.env.example +4 -0
  216. package/overlays/qdrant/README.md +216 -0
  217. package/overlays/qdrant/devcontainer.patch.json +20 -0
  218. package/overlays/qdrant/docker-compose.yml +26 -0
  219. package/overlays/qdrant/overlay.yml +44 -0
  220. package/overlays/rabbitmq/docker-compose.yml +1 -0
  221. package/overlays/rabbitmq/overlay.yml +25 -2
  222. package/overlays/redis/docker-compose.yml +7 -0
  223. package/overlays/redis/overlay.yml +15 -1
  224. package/overlays/redpanda/docker-compose.yml +1 -0
  225. package/overlays/redpanda/overlay.yml +15 -3
  226. package/overlays/rocm/overlay.yml +2 -1
  227. package/overlays/rust/overlay.yml +3 -1
  228. package/overlays/skaffold/README.md +256 -0
  229. package/overlays/skaffold/devcontainer.patch.json +9 -0
  230. package/overlays/skaffold/overlay.yml +20 -0
  231. package/overlays/skaffold/setup.sh +33 -0
  232. package/overlays/skaffold/verify.sh +24 -0
  233. package/overlays/sqlserver/docker-compose.yml +1 -0
  234. package/overlays/sqlserver/overlay.yml +17 -0
  235. package/overlays/tempo/devcontainer.patch.json +0 -1
  236. package/overlays/tempo/docker-compose.yml +8 -0
  237. package/overlays/tempo/overlay.yml +1 -0
  238. package/overlays/windsurf-cli/devcontainer.patch.json +4 -1
  239. package/package.json +3 -2
  240. package/tool/schema/config.schema.json +31 -1
  241. package/tool/schema/overlay-manifest.schema.json +33 -0
  242. package/overlays/.shared/otel/otel-base-config.yaml +0 -30
@@ -1,1540 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import * as fs from 'fs';
3
- import * as path from 'path';
4
- import { fileURLToPath } from 'url';
5
- import { Command } from 'commander';
6
- import chalk from 'chalk';
7
- import boxen from 'boxen';
8
- import ora from 'ora';
9
- import { select, checkbox, input } from '@inquirer/prompts';
10
- import yaml from 'js-yaml';
11
- import { composeDevContainer, generateManifestOnly } from '../tool/questionnaire/composer.js';
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 { adoptCommand } from '../tool/commands/adopt.js';
18
- import { hashCommand } from '../tool/commands/hash.js';
19
- import { getIncompatibleOverlays, DEPLOYMENT_TARGETS } from '../tool/schema/deployment-targets.js';
20
- import { migrateManifest, needsMigration, detectManifestVersion, isVersionSupported, CURRENT_MANIFEST_VERSION, SUPPORTED_MANIFEST_VERSIONS, } from '../tool/schema/manifest-migrations.js';
21
- import { getToolVersion } from '../tool/utils/version.js';
22
- import { printSummary } from '../tool/utils/summary.js';
23
- import { buildAnswersFromProjectConfig, loadProjectConfig, writeProjectConfigCustomizations, } from '../tool/schema/project-config.js';
24
- import { isInsideGitRepo, createBackup, ensureBackupPatternsInGitignore, } from '../tool/utils/backup.js';
25
- // Get __dirname equivalent in ESM
26
- const __filename = fileURLToPath(import.meta.url);
27
- const __dirname = path.dirname(__filename);
28
- const OVERLAYS_DIR_CANDIDATES = [
29
- // When running from TypeScript sources (e.g. ts-node), __dirname is "<root>/scripts"
30
- path.join(__dirname, '..', 'overlays'),
31
- // When running from compiled JS in "dist/scripts", __dirname is "<root>/dist/scripts"
32
- path.join(__dirname, '..', '..', 'overlays'),
33
- ];
34
- const OVERLAYS_DIR = OVERLAYS_DIR_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ??
35
- OVERLAYS_DIR_CANDIDATES[0];
36
- const OVERLAYS_CONFIG_CANDIDATES = [
37
- // When running from TypeScript sources (e.g. ts-node), __dirname is "<root>/scripts"
38
- // and "../overlays/index.yml" resolves to "<root>/overlays/index.yml".
39
- path.join(__dirname, '..', 'overlays', 'index.yml'),
40
- // When running from compiled JS in "dist/scripts", __dirname is "<root>/dist/scripts"
41
- // and "../../overlays/index.yml" resolves to "<root>/overlays/index.yml".
42
- path.join(__dirname, '..', '..', 'overlays', 'index.yml'),
43
- ];
44
- const OVERLAYS_CONFIG_PATH = OVERLAYS_CONFIG_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ??
45
- OVERLAYS_CONFIG_CANDIDATES[0];
46
- const PRESETS_DIR_CANDIDATES = [
47
- path.join(__dirname, '..', 'overlays', '.presets'),
48
- path.join(__dirname, '..', '..', 'overlays', '.presets'),
49
- ];
50
- const PRESETS_DIR = PRESETS_DIR_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ??
51
- PRESETS_DIR_CANDIDATES[0];
52
- /**
53
- * Load overlay metadata from individual manifests or fallback to YAML file
54
- */
55
- function loadOverlaysConfigWrapper() {
56
- return loadOverlaysConfig(OVERLAYS_DIR, OVERLAYS_CONFIG_PATH);
57
- }
58
- /**
59
- * Load preset definition from YAML file
60
- */
61
- function loadPresetDefinition(presetId) {
62
- const presetPath = path.join(PRESETS_DIR, `${presetId}.yml`);
63
- if (!fs.existsSync(presetPath)) {
64
- console.warn(chalk.yellow(`⚠️ Preset definition not found: ${presetPath}`));
65
- return null;
66
- }
67
- const content = fs.readFileSync(presetPath, 'utf8');
68
- return yaml.load(content);
69
- }
70
- function categorizeOverlayIds(overlayIds, config) {
71
- const language = [];
72
- const database = [];
73
- const observability = [];
74
- const cloudTools = [];
75
- const devTools = [];
76
- const overlayMap = new Map(config.overlays.map((o) => [o.id, o]));
77
- for (const id of overlayIds) {
78
- const overlay = overlayMap.get(id);
79
- if (!overlay)
80
- continue;
81
- switch (overlay.category) {
82
- case 'language':
83
- language.push(id);
84
- break;
85
- case 'database':
86
- database.push(id);
87
- break;
88
- case 'observability':
89
- observability.push(id);
90
- break;
91
- case 'cloud':
92
- cloudTools.push(id);
93
- break;
94
- case 'dev':
95
- devTools.push(id);
96
- break;
97
- }
98
- }
99
- return { language, database, observability, cloudTools, devTools };
100
- }
101
- function mergeUnique(left, right) {
102
- const merged = [...(left ?? []), ...(right ?? [])];
103
- return merged.length > 0 ? [...new Set(merged)] : undefined;
104
- }
105
- function expandPresetWithDefaults(presetId, stack, providedChoices = {}) {
106
- const preset = loadPresetDefinition(presetId);
107
- if (!preset) {
108
- throw new Error(`Preset definition not found for ${presetId}`);
109
- }
110
- const overlays = [...preset.selects.required];
111
- const choices = {};
112
- if (preset.selects.userChoice) {
113
- for (const [key, choice] of Object.entries(preset.selects.userChoice)) {
114
- const selectedOption = providedChoices[key] ?? choice.defaultOption;
115
- if (!selectedOption || !choice.options.includes(selectedOption)) {
116
- const valid = choice.options.join(', ');
117
- throw new Error(`Preset choice '${key}' must be one of: ${valid}`);
118
- }
119
- overlays.push(selectedOption);
120
- choices[key] = selectedOption;
121
- }
122
- }
123
- if (preset.parameters) {
124
- for (const [key, param] of Object.entries(preset.parameters)) {
125
- const selectedId = providedChoices[key] ?? param.default;
126
- const selectedOption = param.options.find((option) => option.id === selectedId);
127
- if (!selectedOption) {
128
- const valid = param.options.map((option) => option.id).join(', ');
129
- throw new Error(`Preset parameter '${key}' must be one of: ${valid}`);
130
- }
131
- overlays.push(...selectedOption.overlays);
132
- choices[key] = selectedId;
133
- }
134
- }
135
- const uniqueOverlays = [...new Set(overlays)];
136
- let resolvedGlueConfig = preset.glueConfig;
137
- if (resolvedGlueConfig?.environment) {
138
- const resolvedEnv = {};
139
- for (const [envKey, envValue] of Object.entries(resolvedGlueConfig.environment)) {
140
- resolvedEnv[envKey] = envValue.replace(/\{\{parameters\.(\w+)\.id\}\}/g, (_match, paramKey) => choices[paramKey] ?? _match);
141
- }
142
- resolvedGlueConfig = { ...resolvedGlueConfig, environment: resolvedEnv };
143
- }
144
- return { overlays: uniqueOverlays, choices, glueConfig: resolvedGlueConfig };
145
- }
146
- function applyPresetSelections(answers) {
147
- if (!answers.preset) {
148
- return answers;
149
- }
150
- const expansion = expandPresetWithDefaults(answers.preset, answers.stack ?? 'plain', answers.presetChoices ?? {});
151
- const categories = categorizeOverlayIds(expansion.overlays, loadOverlaysConfigWrapper());
152
- return {
153
- ...answers,
154
- language: mergeUnique(categories.language, answers.language),
155
- database: mergeUnique(categories.database, answers.database),
156
- observability: mergeUnique(categories.observability, answers.observability),
157
- cloudTools: mergeUnique(categories.cloudTools, answers.cloudTools),
158
- devTools: mergeUnique(categories.devTools, answers.devTools),
159
- playwright: answers.playwright ?? expansion.overlays.includes('playwright'),
160
- presetChoices: Object.keys(expansion.choices).length > 0 ? expansion.choices : undefined,
161
- presetGlueConfig: expansion.glueConfig,
162
- };
163
- }
164
- /**
165
- * Expand a preset into a list of overlay IDs with user choices resolved
166
- */
167
- async function expandPreset(presetId, stack, preProvidedChoices = {}) {
168
- const preset = loadPresetDefinition(presetId);
169
- if (!preset) {
170
- return { overlays: [], choices: {} };
171
- }
172
- console.log(chalk.cyan(`\n📦 Expanding preset: ${preset.name}\n`));
173
- const overlays = [...preset.selects.required];
174
- const choices = {};
175
- // Handle user choices (single overlay per option)
176
- if (preset.selects.userChoice) {
177
- for (const [key, choice] of Object.entries(preset.selects.userChoice)) {
178
- const preProvidedValue = preProvidedChoices[key];
179
- if (preProvidedValue !== undefined) {
180
- // Validate the pre-provided value
181
- if (!choice.options.includes(preProvidedValue)) {
182
- const valid = choice.options.join(', ');
183
- throw new Error(`Invalid value '${preProvidedValue}' for preset choice '${key}'. Valid options: ${valid}`);
184
- }
185
- console.log(chalk.dim(`✓ ${key}: ${preProvidedValue} (from CLI)`));
186
- overlays.push(preProvidedValue);
187
- choices[key] = preProvidedValue;
188
- }
189
- else {
190
- const selectedOption = (await select({
191
- message: choice.prompt,
192
- choices: choice.options.map((opt) => ({
193
- name: opt,
194
- value: opt,
195
- })),
196
- default: choice.defaultOption,
197
- }));
198
- overlays.push(selectedOption);
199
- choices[key] = selectedOption;
200
- }
201
- }
202
- }
203
- // Handle parameterized slots (multiple overlays per option)
204
- if (preset.parameters) {
205
- for (const [key, param] of Object.entries(preset.parameters)) {
206
- const preProvidedValue = preProvidedChoices[key];
207
- let selectedId;
208
- if (preProvidedValue !== undefined) {
209
- // Validate the pre-provided value
210
- const validOption = param.options.find((o) => o.id === preProvidedValue);
211
- if (!validOption) {
212
- const valid = param.options.map((o) => o.id).join(', ');
213
- throw new Error(`Invalid value '${preProvidedValue}' for preset parameter '${key}'. Valid options: ${valid}`);
214
- }
215
- console.log(chalk.dim(`✓ ${key}: ${preProvidedValue} (from CLI)`));
216
- selectedId = preProvidedValue;
217
- }
218
- else {
219
- const description = param.description || `Select ${key}`;
220
- selectedId = (await select({
221
- message: description,
222
- choices: param.options.map((opt) => ({
223
- name: opt.description ? `${opt.id} - ${opt.description}` : opt.id,
224
- value: opt.id,
225
- })),
226
- default: param.default,
227
- }));
228
- }
229
- // Add overlays for the selected option
230
- const selectedOption = param.options.find((o) => o.id === selectedId);
231
- if (selectedOption) {
232
- overlays.push(...selectedOption.overlays);
233
- }
234
- choices[key] = selectedId;
235
- }
236
- }
237
- // Deduplicate overlays
238
- const uniqueOverlays = [...new Set(overlays)];
239
- console.log(chalk.dim(`✓ Preset will include: ${uniqueOverlays.join(', ')}\n`));
240
- // Apply template substitution to glueConfig.environment values.
241
- // Replaces {{parameters.<key>.id}} with the selected choice for <key>.
242
- let resolvedGlueConfig = preset.glueConfig;
243
- if (resolvedGlueConfig?.environment) {
244
- const resolvedEnv = {};
245
- for (const [envKey, envValue] of Object.entries(resolvedGlueConfig.environment)) {
246
- resolvedEnv[envKey] = envValue.replace(/\{\{parameters\.(\w+)\.id\}\}/g, (_match, paramKey) => choices[paramKey] ?? _match);
247
- }
248
- resolvedGlueConfig = { ...resolvedGlueConfig, environment: resolvedEnv };
249
- }
250
- return { overlays: uniqueOverlays, choices, glueConfig: resolvedGlueConfig };
251
- }
252
- /**
253
- * Search for manifest file in multiple locations
254
- */
255
- function findManifestFile(manifestPath) {
256
- const searchPaths = [];
257
- if (manifestPath) {
258
- // If path specified, use it directly
259
- searchPaths.push(manifestPath);
260
- }
261
- else {
262
- // Search in common locations
263
- searchPaths.push('superposition.json', '.devcontainer/superposition.json', '../superposition.json', path.join(process.cwd(), 'superposition.json'), path.join(process.cwd(), '.devcontainer', 'superposition.json'));
264
- }
265
- for (const searchPath of searchPaths) {
266
- const resolvedPath = path.resolve(searchPath);
267
- if (fs.existsSync(resolvedPath)) {
268
- return resolvedPath;
269
- }
270
- }
271
- return null;
272
- }
273
- function findDefaultRegenManifest(outputPath = './.devcontainer') {
274
- const manifestSearchPaths = ['superposition.json', path.join(outputPath, 'superposition.json')];
275
- for (const searchPath of manifestSearchPaths) {
276
- const resolvedPath = path.resolve(searchPath);
277
- if (fs.existsSync(resolvedPath)) {
278
- return resolvedPath;
279
- }
280
- }
281
- return null;
282
- }
283
- /**
284
- * Load and validate manifest file
285
- */
286
- function loadManifest(manifestPath) {
287
- try {
288
- const content = fs.readFileSync(manifestPath, 'utf-8');
289
- const rawManifest = JSON.parse(content);
290
- // Detect manifest version
291
- const detectedVersion = detectManifestVersion(rawManifest);
292
- // Check if version is supported
293
- if (!isVersionSupported(detectedVersion)) {
294
- console.error(chalk.red(`✗ Manifest version ${detectedVersion} is not supported.\n` +
295
- ` This tool supports versions: ${SUPPORTED_MANIFEST_VERSIONS.join(', ')}\n` +
296
- ` Please upgrade your tool or regenerate the manifest.`));
297
- return null;
298
- }
299
- // Migrate if needed
300
- let manifest;
301
- if (needsMigration(rawManifest)) {
302
- const oldVersion = rawManifest.manifestVersion || rawManifest.version || 'legacy';
303
- console.log(chalk.cyan(`ℹ️ Migrating manifest from version ${oldVersion} to ${CURRENT_MANIFEST_VERSION}...`));
304
- manifest = migrateManifest(rawManifest);
305
- }
306
- else {
307
- manifest = rawManifest;
308
- }
309
- // Basic validation
310
- if (!manifest.baseTemplate) {
311
- console.error(chalk.red('✗ Invalid manifest format: missing required field "baseTemplate"'));
312
- return null;
313
- }
314
- if (!Array.isArray(manifest.overlays)) {
315
- console.error(chalk.red('✗ Invalid manifest format: "overlays" must be an array'));
316
- return null;
317
- }
318
- if (!manifest.overlays.every((overlay) => typeof overlay === 'string')) {
319
- console.error(chalk.red('✗ Invalid manifest format: all "overlays" entries must be strings'));
320
- return null;
321
- }
322
- return manifest;
323
- }
324
- catch (error) {
325
- console.error(chalk.red(`✗ Failed to load manifest: ${error instanceof Error ? error.message : String(error)}`));
326
- return null;
327
- }
328
- }
329
- /**
330
- * Build checkbox choices for overlay selection with optional pre-selection
331
- */
332
- function buildOverlayChoices(config, stack, categoryList, preselected) {
333
- const choices = [];
334
- categoryList.forEach((category) => {
335
- const filtered = category.overlays.filter((o) => !o.hidden && (!o.supports || o.supports.length === 0 || o.supports.includes(stack)));
336
- if (filtered.length > 0) {
337
- // Add category separator
338
- choices.push({
339
- type: 'separator',
340
- separator: chalk.cyan(`──── ${category.name} ────`),
341
- });
342
- // Add overlays in this category
343
- filtered.forEach((overlay) => {
344
- choices.push({
345
- name: overlay.name,
346
- value: overlay.id,
347
- description: overlay.description,
348
- checked: preselected.includes(overlay.id),
349
- });
350
- });
351
- }
352
- });
353
- return choices;
354
- }
355
- /**
356
- * Interactive questionnaire with modern checkbox selections
357
- */
358
- async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetChoices, defaultAnswers) {
359
- const config = loadOverlaysConfigWrapper();
360
- // Pretty banner
361
- console.log('\n' +
362
- boxen(chalk.bold.cyan('Container Superposition') +
363
- '\n' +
364
- chalk.gray(manifest ? 'DevContainer Regenerator' : 'DevContainer Initializer'), {
365
- padding: 1,
366
- margin: 1,
367
- borderStyle: 'round',
368
- borderColor: 'cyan',
369
- textAlignment: 'center',
370
- }));
371
- if (manifest) {
372
- console.log(chalk.cyan('📋 Loaded from manifest:'));
373
- console.log(chalk.dim(` Template: ${manifest.baseTemplate}`));
374
- console.log(chalk.dim(` Overlays: ${manifest.overlays.join(', ')}`));
375
- if (manifest.preset) {
376
- console.log(chalk.dim(` Preset: ${manifest.preset}`));
377
- }
378
- if (manifest.portOffset) {
379
- console.log(chalk.dim(` Port offset: ${manifest.portOffset}`));
380
- }
381
- console.log();
382
- }
383
- console.log(chalk.dim('Compose your ideal devcontainer from modular overlays.'));
384
- console.log(chalk.dim('Use ') +
385
- chalk.cyan('space') +
386
- chalk.dim(' to select, ') +
387
- chalk.cyan('enter') +
388
- chalk.dim(' to confirm.\n'));
389
- try {
390
- // Question 0: Optional preset selection
391
- let usePreset = false;
392
- // CLI preset takes precedence over manifest preset
393
- let selectedPresetId = cliPresetId || manifest?.preset;
394
- // CLI preset choices merged with manifest choices (CLI takes precedence)
395
- let presetChoices = {
396
- ...(manifest?.presetChoices || {}),
397
- ...(cliPresetChoices || {}),
398
- };
399
- let presetGlueConfig;
400
- const presetOverlaysFiltered = config.overlays.filter((o) => o.category === 'preset');
401
- let presetOverlays = [];
402
- if (presetOverlaysFiltered.length > 0) {
403
- // If a preset was pre-selected via CLI or manifest, skip the prompt
404
- if (selectedPresetId) {
405
- usePreset = true;
406
- console.log(chalk.cyan(`\n📦 Using preset: ${selectedPresetId}\n`));
407
- }
408
- else {
409
- const defaultPreset = 'custom';
410
- const presetChoice = (await select({
411
- message: 'Start from a preset or build custom?',
412
- choices: [
413
- {
414
- name: 'Custom (select overlays manually)',
415
- value: 'custom',
416
- description: 'Choose individual overlays yourself',
417
- },
418
- ...presetOverlaysFiltered.map((p) => ({
419
- name: p.name,
420
- value: p.id,
421
- description: p.description,
422
- })),
423
- ],
424
- default: defaultPreset,
425
- }));
426
- if (presetChoice !== 'custom') {
427
- usePreset = true;
428
- selectedPresetId = presetChoice;
429
- }
430
- else {
431
- // User chose custom - discard any pre-provided preset choices so the
432
- // manifest cannot end up with presetChoices but no preset.
433
- presetChoices = {};
434
- selectedPresetId = undefined;
435
- }
436
- }
437
- }
438
- // Question 1: Base template
439
- const stack = (await select({
440
- message: 'Select base template:',
441
- choices: config.base_templates.map((t) => ({
442
- name: t.name,
443
- value: t.id,
444
- description: t.description,
445
- })),
446
- default: manifest?.baseTemplate || defaultAnswers?.stack,
447
- }));
448
- // If using preset, expand it now (pass pre-provided choices to skip those prompts)
449
- if (usePreset && selectedPresetId) {
450
- const expansion = await expandPreset(selectedPresetId, stack, presetChoices);
451
- if (!expansion.overlays || expansion.overlays.length === 0) {
452
- // Preset failed to expand (e.g., missing or invalid preset definition).
453
- // Treat this as "no preset" so the manifest does not incorrectly record one.
454
- console.log(chalk.yellow(`\n⚠️ Preset "${selectedPresetId}" could not be applied. Falling back to custom overlay selection.\n`));
455
- usePreset = false;
456
- selectedPresetId = undefined;
457
- presetOverlays = [];
458
- presetChoices = {};
459
- presetGlueConfig = undefined;
460
- }
461
- else {
462
- presetOverlays = expansion.overlays;
463
- presetChoices = expansion.choices;
464
- presetGlueConfig = expansion.glueConfig;
465
- }
466
- }
467
- // Question 2: Base image selection
468
- // Check if manifest has a custom image or a known base image
469
- const knownBaseImageIds = config.base_images.map((img) => img.id);
470
- const manifestBaseImageIsKnown = manifest?.baseImage && knownBaseImageIds.includes(manifest.baseImage);
471
- const manifestDefaultBaseImage = manifestBaseImageIsKnown
472
- ? manifest.baseImage
473
- : manifest?.baseImage
474
- ? 'custom'
475
- : undefined;
476
- const defaultBaseImage = manifestDefaultBaseImage ||
477
- (defaultAnswers?.baseImage === 'custom' && defaultAnswers.customImage
478
- ? 'custom'
479
- : defaultAnswers?.baseImage);
480
- const baseImage = (await select({
481
- message: 'Select base image:',
482
- choices: config.base_images.map((img) => ({
483
- name: img.name,
484
- value: img.id,
485
- description: img.description,
486
- })),
487
- default: defaultBaseImage,
488
- }));
489
- // Question 2a: If custom, ask for image name
490
- let customImage;
491
- if (baseImage === 'custom') {
492
- // If manifest has a custom image, use it as default
493
- const manifestCustomImage = !manifestBaseImageIsKnown && manifest?.baseImage ? manifest.baseImage : undefined;
494
- const defaultCustomImage = manifestCustomImage || defaultAnswers?.customImage;
495
- customImage = await input({
496
- message: 'Enter custom Docker image (e.g., ubuntu:22.04):',
497
- default: defaultCustomImage,
498
- validate: (value) => {
499
- if (!value || value.trim() === '') {
500
- return 'Image name is required';
501
- }
502
- return true;
503
- },
504
- });
505
- console.log(chalk.yellow('\n⚠️ Warning: Custom images may conflict with overlays.'));
506
- console.log(chalk.dim(' Test thoroughly and adjust configurations as needed.\n'));
507
- }
508
- // Build categorized overlays with separators
509
- const categoryList = [
510
- {
511
- name: 'Language',
512
- overlays: config.overlays.filter((o) => o.category === 'language'),
513
- },
514
- {
515
- name: 'Database',
516
- overlays: config.overlays.filter((o) => o.category === 'database'),
517
- },
518
- {
519
- name: 'Observability',
520
- overlays: config.overlays.filter((o) => o.category === 'observability'),
521
- },
522
- { name: 'Cloud', overlays: config.overlays.filter((o) => o.category === 'cloud') },
523
- { name: 'DevTool', overlays: config.overlays.filter((o) => o.category === 'dev') },
524
- ];
525
- // Create a map of all overlays for dependency lookup
526
- const allOverlaysMap = new Map(config.overlays.map((o) => [o.id, o]));
527
- // Question 3: Categorized multi-select overlays with dependency tracking
528
- let userSelection;
529
- if (usePreset && presetOverlays.length > 0) {
530
- // Preset mode: Ask if user wants to customize
531
- console.log(chalk.cyan(`\n✓ Preset includes these overlays: ${presetOverlays.join(', ')}\n`));
532
- const customizePreset = (await select({
533
- message: 'Do you want to customize the overlay selection?',
534
- choices: [
535
- {
536
- name: 'Use preset as-is',
537
- value: 'no',
538
- description: 'Keep the preset overlay selection',
539
- },
540
- {
541
- name: 'Customize selection',
542
- value: 'yes',
543
- description: 'Add or remove overlays from the preset',
544
- },
545
- ],
546
- }));
547
- if (customizePreset === 'yes') {
548
- // Show overlay selection with preset overlays pre-selected
549
- console.log(chalk.dim('\n💡 Select overlays: Space to toggle, ↑/↓ to navigate, Enter to confirm'));
550
- console.log(chalk.dim(' Preset overlays are pre-selected\n'));
551
- const choices = buildOverlayChoices(config, stack, categoryList, presetOverlays);
552
- userSelection = await checkbox({
553
- message: 'Select overlays to include:',
554
- choices,
555
- pageSize: 15,
556
- loop: false,
557
- });
558
- }
559
- else {
560
- // Use preset selection as-is
561
- userSelection = presetOverlays;
562
- }
563
- }
564
- else if (manifest) {
565
- // Manifest mode: Pre-select overlays from manifest
566
- console.log(chalk.cyan(`\n✓ Manifest includes these overlays: ${manifest.overlays.join(', ')}\n`));
567
- const customizeManifest = (await select({
568
- message: 'Do you want to customize the overlay selection?',
569
- choices: [
570
- {
571
- name: 'Use manifest as-is',
572
- value: 'no',
573
- description: 'Keep the manifest overlay selection',
574
- },
575
- {
576
- name: 'Customize selection',
577
- value: 'yes',
578
- description: 'Add or remove overlays from the manifest',
579
- },
580
- ],
581
- }));
582
- if (customizeManifest === 'yes') {
583
- // Show overlay selection with manifest overlays pre-selected
584
- console.log(chalk.dim('\n💡 Select overlays: Space to toggle, ↑/↓ to navigate, Enter to confirm'));
585
- console.log(chalk.dim(' Manifest overlays are pre-selected\n'));
586
- // Filter out overlays that don't exist anymore
587
- const existingOverlays = manifest.overlays.filter((id) => allOverlaysMap.has(id));
588
- const missingOverlays = manifest.overlays.filter((id) => !allOverlaysMap.has(id));
589
- if (missingOverlays.length > 0) {
590
- console.log(chalk.yellow(`⚠️ Warning: Some overlays from manifest no longer exist: ${missingOverlays.join(', ')}\n`));
591
- }
592
- const choices = buildOverlayChoices(config, stack, categoryList, existingOverlays);
593
- userSelection = await checkbox({
594
- message: 'Select overlays to include:',
595
- choices,
596
- pageSize: 15,
597
- loop: false,
598
- });
599
- }
600
- else {
601
- // Use manifest selection as-is (filtering out missing overlays)
602
- const existingOverlays = manifest.overlays.filter((id) => allOverlaysMap.has(id));
603
- const missingOverlays = manifest.overlays.filter((id) => !allOverlaysMap.has(id));
604
- if (missingOverlays.length > 0) {
605
- console.log(chalk.yellow(`⚠️ Warning: Some overlays from manifest no longer exist and will be skipped: ${missingOverlays.join(', ')}\n`));
606
- }
607
- userSelection = existingOverlays;
608
- }
609
- }
610
- else {
611
- // Custom mode: Normal overlay selection
612
- console.log(chalk.dim('\n💡 Select overlays: Space to toggle, ↑/↓ to navigate, Enter to confirm\n'));
613
- const preselectedDefaults = [
614
- ...(defaultAnswers?.language ?? []),
615
- ...(defaultAnswers?.database ?? []),
616
- ...(defaultAnswers?.observability ?? []),
617
- ...(defaultAnswers?.cloudTools ?? []),
618
- ...(defaultAnswers?.devTools ?? []),
619
- ...(defaultAnswers?.playwright ? ['playwright'] : []),
620
- ];
621
- const choices = buildOverlayChoices(config, stack, categoryList, preselectedDefaults);
622
- userSelection = await checkbox({
623
- message: 'Select overlays to include:',
624
- choices,
625
- pageSize: 15,
626
- loop: false,
627
- });
628
- }
629
- // Add all required dependencies
630
- const withDependencies = new Set(userSelection);
631
- const toProcess = [...userSelection];
632
- while (toProcess.length > 0) {
633
- const current = toProcess.pop();
634
- const overlay = allOverlaysMap.get(current);
635
- if (overlay?.requires) {
636
- overlay.requires.forEach((req) => {
637
- if (!withDependencies.has(req)) {
638
- withDependencies.add(req);
639
- toProcess.push(req);
640
- }
641
- });
642
- }
643
- }
644
- let selectedOverlays = Array.from(withDependencies);
645
- // Check for conflicts and resolve
646
- let hasConflicts = true;
647
- while (hasConflicts) {
648
- const conflicts = new Map();
649
- // Find all conflicts
650
- selectedOverlays.forEach((selectedId) => {
651
- const overlay = allOverlaysMap.get(selectedId);
652
- if (overlay?.conflicts) {
653
- overlay.conflicts.forEach((conflictId) => {
654
- if (selectedOverlays.includes(conflictId)) {
655
- if (!conflicts.has(selectedId)) {
656
- conflicts.set(selectedId, []);
657
- }
658
- conflicts.get(selectedId).push(conflictId);
659
- }
660
- });
661
- }
662
- });
663
- if (conflicts.size === 0) {
664
- hasConflicts = false;
665
- }
666
- else {
667
- // Show conflict resolution UI
668
- console.log(chalk.yellow('\n⚠️ Conflicts detected in selection:\n'));
669
- const conflictChoices = [];
670
- conflicts.forEach((conflictingWith, overlayId) => {
671
- const overlay = allOverlaysMap.get(overlayId);
672
- const conflictNames = conflictingWith
673
- .map((id) => allOverlaysMap.get(id)?.name)
674
- .join(', ');
675
- conflictChoices.push({
676
- name: `Remove ${overlay.name}`,
677
- value: overlayId,
678
- description: `Conflicts with: ${conflictNames}`,
679
- });
680
- });
681
- const toRemove = await checkbox({
682
- message: 'Select overlays to remove to resolve conflicts:',
683
- choices: conflictChoices,
684
- pageSize: 15,
685
- loop: false,
686
- });
687
- if (toRemove.length === 0) {
688
- console.log(chalk.red('\n❌ You must remove at least one conflicting overlay'));
689
- continue;
690
- }
691
- // Remove selected overlays
692
- selectedOverlays = selectedOverlays.filter((id) => !toRemove.includes(id));
693
- }
694
- }
695
- // Question 4: Container name
696
- const containerName = await input({
697
- message: 'Container/project name (optional):',
698
- default: manifest?.containerName || defaultAnswers?.containerName || '',
699
- });
700
- // Question 5: Output path
701
- // If manifest provided, default to its location; otherwise use ./.devcontainer
702
- const defaultOutput = manifestDir || defaultAnswers?.outputPath || './.devcontainer';
703
- const outputPath = await input({
704
- message: 'Output path:',
705
- default: defaultOutput,
706
- });
707
- // Question 6: Port offset (optional, for running multiple instances)
708
- const portOffsetInput = await input({
709
- message: 'Port offset (leave empty for default ports, e.g., 100 to avoid conflicts):',
710
- default: manifest?.portOffset !== undefined
711
- ? String(manifest.portOffset)
712
- : defaultAnswers?.portOffset !== undefined
713
- ? String(defaultAnswers.portOffset)
714
- : '',
715
- });
716
- const portOffset = portOffsetInput ? parseInt(portOffsetInput, 10) : undefined;
717
- // Parse selected overlays into categories
718
- const overlayMap = new Map(config.overlays.map((o) => [o.id, o]));
719
- const language = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'language');
720
- const observability = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'observability');
721
- const cloudTools = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'cloud');
722
- const devTools = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'dev');
723
- const database = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'database');
724
- const playwright = selectedOverlays.includes('playwright');
725
- // Check for deployment target compatibility
726
- let target;
727
- // Check if any incompatible overlays selected
728
- const incompatibleOverlays = getIncompatibleOverlays(selectedOverlays, undefined);
729
- if (incompatibleOverlays.length > 0) {
730
- console.log(chalk.yellow('\n⚠️ Deployment Target Compatibility Check:\n'));
731
- console.log(chalk.gray('Some selected overlays may not work in all environments.'));
732
- console.log();
733
- // Show incompatibilities
734
- for (const { overlay, alternatives } of incompatibleOverlays) {
735
- const overlayMeta = allOverlaysMap.get(overlay);
736
- console.log(chalk.yellow(` • ${overlayMeta?.name || overlay}`));
737
- console.log(chalk.gray(` Not compatible with: ${DEPLOYMENT_TARGETS.codespaces.name}, ${DEPLOYMENT_TARGETS.gitpod.name}`));
738
- if (alternatives.length > 0) {
739
- const altNames = alternatives
740
- .map((id) => allOverlaysMap.get(id)?.name || id)
741
- .join(', ');
742
- console.log(chalk.cyan(` Alternatives: ${altNames}`));
743
- }
744
- console.log();
745
- }
746
- const targetChoice = (await select({
747
- message: 'Which environment are you targeting?',
748
- choices: [
749
- {
750
- name: '🖥️ Local Development (Docker Desktop)',
751
- value: 'local',
752
- description: 'Running on your local machine - supports all overlays',
753
- },
754
- {
755
- name: '☁️ GitHub Codespaces',
756
- value: 'codespaces',
757
- description: 'Cloud development - may require docker-in-docker',
758
- },
759
- {
760
- name: '🌐 Gitpod',
761
- value: 'gitpod',
762
- description: 'Cloud development - may require docker-in-docker',
763
- },
764
- {
765
- name: '📦 DevPod',
766
- value: 'devpod',
767
- description: 'Client-only dev environments',
768
- },
769
- ],
770
- default: 'local',
771
- }));
772
- target = targetChoice;
773
- // Show specific incompatibilities for selected target
774
- if (target !== 'local') {
775
- const targetIncompatible = getIncompatibleOverlays(selectedOverlays, target);
776
- if (targetIncompatible.length > 0) {
777
- console.log(chalk.yellow(`\n⚠️ Warning: Some overlays won't work in ${DEPLOYMENT_TARGETS[target].name}:\n`));
778
- for (const { overlay, alternatives } of targetIncompatible) {
779
- const overlayMeta = allOverlaysMap.get(overlay);
780
- console.log(chalk.red(` ✗ ${overlayMeta?.name || overlay}`));
781
- if (alternatives.length > 0) {
782
- const altNames = alternatives
783
- .map((id) => allOverlaysMap.get(id)?.name || id)
784
- .join(', ');
785
- console.log(chalk.cyan(` → Recommended: ${altNames}`));
786
- }
787
- }
788
- console.log();
789
- }
790
- }
791
- }
792
- return {
793
- stack,
794
- baseImage,
795
- customImage,
796
- containerName: containerName || undefined,
797
- preset: selectedPresetId,
798
- presetChoices: Object.keys(presetChoices).length > 0 ? presetChoices : undefined,
799
- presetGlueConfig,
800
- language,
801
- needsDocker: stack === 'compose', // Compose template includes docker-outside-of-docker
802
- database,
803
- playwright,
804
- cloudTools,
805
- devTools,
806
- observability,
807
- outputPath,
808
- portOffset,
809
- target: target ?? defaultAnswers?.target,
810
- minimal: defaultAnswers?.minimal,
811
- editor: defaultAnswers?.editor,
812
- customizations: defaultAnswers?.customizations,
813
- };
814
- }
815
- catch (error) {
816
- if (error.name === 'ExitPromptError') {
817
- console.log('\n' + chalk.yellow('Cancelled by user'));
818
- process.exit(0);
819
- }
820
- throw error;
821
- }
822
- }
823
- /**
824
- * Build partial answers from manifest
825
- * Note: Categories are only used for UI/questionnaire grouping.
826
- * The composer works with overlay IDs regardless of category.
827
- */
828
- function buildAnswersFromManifest(manifest, manifestDir) {
829
- const config = loadOverlaysConfigWrapper();
830
- // Helper to categorize overlays by type (for QuestionnaireAnswers structure)
831
- const categorizeOverlays = (overlayIds) => {
832
- const language = [];
833
- const database = [];
834
- const observability = [];
835
- const cloudTools = [];
836
- const devTools = [];
837
- // Build lookup map from unified overlays array
838
- const overlayMap = new Map(config.overlays.map((o) => [o.id, o]));
839
- // Categorize based on overlay metadata
840
- for (const id of overlayIds) {
841
- const overlay = overlayMap.get(id);
842
- if (!overlay)
843
- continue;
844
- switch (overlay.category) {
845
- case 'language':
846
- language.push(id);
847
- break;
848
- case 'database':
849
- database.push(id);
850
- break;
851
- case 'observability':
852
- observability.push(id);
853
- break;
854
- case 'cloud':
855
- cloudTools.push(id);
856
- break;
857
- case 'dev':
858
- devTools.push(id);
859
- break;
860
- }
861
- }
862
- return { language, database, observability, cloudTools, devTools };
863
- };
864
- const categories = categorizeOverlays(manifest.overlays);
865
- // Output path is always the directory containing the manifest
866
- const outputPath = manifestDir || './.devcontainer';
867
- // Handle baseImage - check if it's a known ID or a custom image string
868
- const knownBaseImageIds = ['bookworm', 'trixie', 'alpine', 'ubuntu', 'custom'];
869
- const isKnownBaseImage = knownBaseImageIds.includes(manifest.baseImage);
870
- return {
871
- stack: manifest.baseTemplate,
872
- baseImage: isKnownBaseImage ? manifest.baseImage : 'custom',
873
- customImage: isKnownBaseImage ? undefined : manifest.baseImage,
874
- containerName: manifest.containerName,
875
- preset: manifest.preset,
876
- presetChoices: manifest.presetChoices,
877
- ...categories,
878
- needsDocker: manifest.baseTemplate === 'compose',
879
- playwright: categories.devTools.includes('playwright'),
880
- outputPath,
881
- portOffset: manifest.portOffset,
882
- };
883
- }
884
- /**
885
- * Build partial answers from CLI arguments
886
- */
887
- function buildAnswersFromCliArgs(config) {
888
- const answers = {};
889
- if (config.stack) {
890
- answers.stack = config.stack;
891
- answers.needsDocker = config.stack === 'compose';
892
- }
893
- if (config.baseImage)
894
- answers.baseImage = config.baseImage;
895
- if (config.containerName)
896
- answers.containerName = config.containerName;
897
- if (config.language)
898
- answers.language = config.language;
899
- if (config.database)
900
- answers.database = config.database;
901
- if (config.playwright !== undefined)
902
- answers.playwright = config.playwright;
903
- if (config.observability)
904
- answers.observability = config.observability;
905
- if (config.cloudTools)
906
- answers.cloudTools = config.cloudTools;
907
- if (config.devTools)
908
- answers.devTools = config.devTools;
909
- if (config.portOffset !== undefined)
910
- answers.portOffset = config.portOffset;
911
- if (config.outputPath)
912
- answers.outputPath = config.outputPath;
913
- if (config.preset)
914
- answers.preset = config.preset;
915
- if (config.presetChoices)
916
- answers.presetChoices = config.presetChoices;
917
- if (config.target)
918
- answers.target = config.target;
919
- if (config.minimal !== undefined)
920
- answers.minimal = config.minimal;
921
- if (config.editor)
922
- answers.editor = config.editor;
923
- return answers;
924
- }
925
- /**
926
- * Merge multiple partial answers with precedence: cli > interactive > manifest > defaults
927
- */
928
- function mergeAnswers(...partials) {
929
- const merged = {
930
- language: [],
931
- database: [],
932
- cloudTools: [],
933
- devTools: [],
934
- observability: [],
935
- playwright: false,
936
- outputPath: './.devcontainer',
937
- };
938
- // Merge in order (later overrides earlier)
939
- for (const partial of partials) {
940
- if (!partial)
941
- continue;
942
- Object.keys(partial).forEach((key) => {
943
- const value = partial[key];
944
- if (value !== undefined && value !== null) {
945
- // For arrays, prefer non-empty values
946
- if (Array.isArray(value)) {
947
- if (value.length > 0) {
948
- merged[key] = value;
949
- }
950
- }
951
- else {
952
- merged[key] = value;
953
- }
954
- }
955
- });
956
- }
957
- // Ensure required fields have defaults
958
- if (!merged.stack)
959
- merged.stack = 'plain';
960
- if (!merged.baseImage)
961
- merged.baseImage = 'bookworm';
962
- if (!merged.needsDocker && merged.stack) {
963
- merged.needsDocker = merged.stack === 'compose';
964
- }
965
- return merged;
966
- }
967
- /**
968
- * Parse CLI arguments
969
- */
970
- async function parseCliArgs() {
971
- const program = new Command();
972
- // Store init options for access after parsing
973
- let initOptions = null;
974
- program
975
- .name('container-superposition')
976
- .description('Composable devcontainer scaffolds')
977
- .version(getToolVersion());
978
- // Init command (default)
979
- program
980
- .command('init', { isDefault: true })
981
- .description('Initialize a new devcontainer configuration')
982
- .option('--from-project', 'Load configuration from the repository project file')
983
- .option('--project-root <path>', 'Run project-file and manifest discovery relative to a different repository root')
984
- .option('--from-manifest <path>', 'Load configuration from existing superposition.json manifest')
985
- .option('--no-interactive', 'Use persisted input values directly without questionnaire')
986
- .option('--backup', 'Force or suppress backup; default is --no-backup inside a git repo, --backup outside')
987
- .option('--backup-dir <path>', 'Custom backup directory location')
988
- .option('--stack <type>', 'Base template: plain, compose')
989
- .option('--language <list>', 'Comma-separated language overlays: dotnet, nodejs, python, mkdocs, java, go, rust, bun, powershell')
990
- .option('--database <list>', 'Comma-separated database overlays: postgres, redis, mongodb, mysql, sqlserver, sqlite, minio, rabbitmq, redpanda, nats')
991
- .option('--observability <list>', 'Comma-separated: otel-collector, jaeger, prometheus, grafana, loki')
992
- .option('--playwright', 'Include Playwright browser automation')
993
- .option('--cloud-tools <list>', 'Comma-separated: aws-cli, azure-cli, gcloud, kubectl-helm, terraform, pulumi')
994
- .option('--dev-tools <list>', 'Comma-separated: docker-in-docker, docker-sock, playwright, codex, git-helpers, pre-commit, commitlint, just, direnv, modern-cli-tools, ngrok')
995
- .option('--port-offset <number>', 'Add offset to all exposed ports (e.g., 100 makes Grafana 3100 instead of 3000)')
996
- .option('--target <environment>', 'Deployment target: local, codespaces, gitpod, devpod (optimizes for environment)', 'local')
997
- .option('--minimal', 'Minimal mode - exclude optional/nice-to-have features and extensions')
998
- .option('--editor <profile>', 'Editor profile: vscode (default), jetbrains, none', 'vscode')
999
- .option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
1000
- .option('--write-manifest-only', 'Generate only superposition.json manifest without creating .devcontainer/ files')
1001
- .option('--preset <id>', 'Start from a preset (e.g., web-api, microservice)')
1002
- .option('--preset-param <value>', 'Set a preset parameter value (format: key=value, can be repeated)', (value, previous) => previous.concat([value]), [])
1003
- .action((options, command) => {
1004
- // Store options for main() to process
1005
- initOptions = {
1006
- ...options,
1007
- commandName: 'init',
1008
- _targetSource: command.getOptionValueSource('target'),
1009
- _editorSource: command.getOptionValueSource('editor'),
1010
- };
1011
- });
1012
- // Regen command
1013
- program
1014
- .command('regen')
1015
- .description('Regenerate devcontainer from a project file or existing superposition.json manifest')
1016
- .option('--from-project', 'Load configuration from the repository project file')
1017
- .option('--project-root <path>', 'Run project-file and manifest discovery relative to a different repository root')
1018
- .option('--from-manifest <path>', 'Load configuration from existing superposition.json manifest')
1019
- .option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
1020
- .option('--backup', 'Force or suppress backup; default is --no-backup inside a git repo, --backup outside')
1021
- .option('--backup-dir <path>', 'Custom backup directory location')
1022
- .option('--minimal', 'Minimal mode - exclude optional/nice-to-have features and extensions')
1023
- .option('--editor <profile>', 'Editor profile: vscode (default), jetbrains, none', 'vscode')
1024
- .action((options, command) => {
1025
- initOptions = {
1026
- ...options,
1027
- commandName: 'regen',
1028
- interactive: false,
1029
- _editorSource: command.getOptionValueSource('editor'),
1030
- };
1031
- });
1032
- // List command
1033
- program
1034
- .command('list')
1035
- .description('List available overlays and presets')
1036
- .option('--category <type>', 'Filter by category: language, database, observability, cloud, dev, preset')
1037
- .option('--tags <list>', 'Filter by tags (comma-separated)')
1038
- .option('--supports <stack>', 'Filter by stack support: plain, compose')
1039
- .option('--json', 'Output as JSON for scripting')
1040
- .action(async (options) => {
1041
- const overlaysConfig = loadOverlaysConfigWrapper();
1042
- await listCommand(overlaysConfig, options);
1043
- process.exit(0);
1044
- });
1045
- // Explain command
1046
- program
1047
- .command('explain <overlay>')
1048
- .description('Show detailed information about an overlay')
1049
- .option('--json', 'Output as JSON for scripting')
1050
- .action(async (overlayId, options) => {
1051
- const overlaysConfig = loadOverlaysConfigWrapper();
1052
- await explainCommand(overlaysConfig, OVERLAYS_DIR, overlayId, options);
1053
- process.exit(0);
1054
- });
1055
- // Plan command
1056
- program
1057
- .command('plan')
1058
- .description('Preview what will be generated before creating devcontainer')
1059
- .option('--stack <type>', 'Base template: plain, compose')
1060
- .option('--overlays <list>', 'Comma-separated list of overlay IDs')
1061
- .option('--from-manifest <path>', 'Load stack and overlays from an existing superposition.json manifest')
1062
- .option('--port-offset <number>', 'Add offset to all exposed ports', (val) => parseInt(val, 10), 0)
1063
- .option('-o, --output <path>', 'Compare against existing config at this path (default: ./.devcontainer)')
1064
- .option('--diff', 'Compare planned output vs existing configuration')
1065
- .option('--diff-format <format>', 'Diff output format: color (default), json', 'color')
1066
- .option('--diff-context <lines>', 'Context lines in diff output', (val) => parseInt(val, 10), 3)
1067
- .option('--verbose', 'Explain why each overlay was included in the resolved plan')
1068
- .option('--json', 'Output as JSON for scripting')
1069
- .action(async (options) => {
1070
- const overlaysConfig = loadOverlaysConfigWrapper();
1071
- await planCommand(overlaysConfig, OVERLAYS_DIR, options);
1072
- process.exit(0);
1073
- });
1074
- // Doctor command
1075
- program
1076
- .command('doctor')
1077
- .description('Check environment and validate configuration')
1078
- .option('-o, --output <path>', 'Devcontainer path to validate (default: ./.devcontainer)')
1079
- .option('--from-manifest <path>', 'Load configuration from an existing superposition.json manifest')
1080
- .option('--from-project', 'Load configuration from the repository project file')
1081
- .option('--project-root <path>', 'Run project-file and manifest discovery relative to a different repository root')
1082
- .option('--fix', 'Apply automatic fixes where possible')
1083
- .option('--json', 'Output as JSON for scripting')
1084
- .action(async (options) => {
1085
- const overlaysConfig = loadOverlaysConfigWrapper();
1086
- await doctorCommand(overlaysConfig, OVERLAYS_DIR, options);
1087
- });
1088
- // Adopt command
1089
- program
1090
- .command('adopt')
1091
- .description('Analyse an existing .devcontainer/ and suggest an equivalent overlay-based configuration')
1092
- .option('-d, --dir <path>', 'Path to the existing .devcontainer directory (default: ./.devcontainer)')
1093
- .option('--dry-run', 'Print analysis and suggested command only; no files written')
1094
- .option('--force', 'Overwrite existing superposition.json if present')
1095
- .option('--backup', 'Force backup creation even when inside a git repo (default: backup only outside git repos)')
1096
- .option('--no-backup', 'Disable backup creation even when it would normally be performed')
1097
- .option('--backup-dir <path>', 'Custom backup directory location')
1098
- .option('--project-file', 'Also write a repository-root project config (.superposition.yml or existing project file)')
1099
- .option('--json', 'Output as JSON for scripting')
1100
- .action(async (options) => {
1101
- const overlaysConfig = loadOverlaysConfigWrapper();
1102
- await adoptCommand(overlaysConfig, OVERLAYS_DIR, options);
1103
- process.exit(0);
1104
- });
1105
- // Hash command
1106
- program
1107
- .command('hash')
1108
- .description('Compute a deterministic fingerprint for a given configuration')
1109
- .option('--stack <type>', 'Base template: plain, compose')
1110
- .option('--overlays <list>', 'Comma-separated list of overlay IDs')
1111
- .option('--preset <id>', 'Preset ID (optional)')
1112
- .option('--base <image>', 'Base image / distro variant (e.g. bookworm, alpine)')
1113
- .option('--manifest <path>', 'Path to superposition.json manifest')
1114
- .option('-o, --output <path>', 'Directory to write hash file (used with --write)')
1115
- .option('--write', 'Write hash to .devcontainer/superposition.hash')
1116
- .option('--json', 'Output as JSON for scripting')
1117
- .action(async (options) => {
1118
- const overlaysConfig = loadOverlaysConfigWrapper();
1119
- await hashCommand(overlaysConfig, OVERLAYS_DIR, options);
1120
- process.exit(0);
1121
- });
1122
- await program.parseAsync(process.argv);
1123
- // If init or regen command was run, return the options
1124
- if (!initOptions) {
1125
- // No init/regen command run (list or doctor ran instead)
1126
- return null;
1127
- }
1128
- // If no options provided to init, return null to trigger interactive mode
1129
- if (Object.keys(initOptions).length === 0) {
1130
- return null;
1131
- }
1132
- const hasSourceFlags = Number(Boolean(initOptions.fromProject)) + Number(Boolean(initOptions.fromManifest));
1133
- if (hasSourceFlags > 1) {
1134
- console.error(chalk.red('✗ Error: --from-project and --from-manifest cannot be used together'));
1135
- process.exit(1);
1136
- }
1137
- const sourceSelectionConflicts = [
1138
- 'stack',
1139
- 'language',
1140
- 'database',
1141
- 'observability',
1142
- 'playwright',
1143
- 'cloudTools',
1144
- 'devTools',
1145
- 'portOffset',
1146
- 'preset',
1147
- ];
1148
- const hasPresetParams = Array.isArray(initOptions.presetParam) && initOptions.presetParam.length > 0;
1149
- const conflictingSelectionFlags = sourceSelectionConflicts.filter((key) => initOptions[key] !== undefined && initOptions[key] !== false);
1150
- if ((initOptions.fromProject || initOptions.fromManifest) &&
1151
- (conflictingSelectionFlags.length > 0 || hasPresetParams)) {
1152
- const conflicts = [...conflictingSelectionFlags.map((key) => `--${key}`)];
1153
- if (hasPresetParams) {
1154
- conflicts.push('--preset-param');
1155
- }
1156
- console.error(chalk.red(`✗ Error: Persisted input sources cannot be combined with clean-generation selection flags: ${conflicts.join(', ')}`));
1157
- console.error(chalk.dim(' Choose either a persisted input source (--from-project or --from-manifest) or direct selection flags for that run.'));
1158
- process.exit(1);
1159
- }
1160
- const config = {};
1161
- if (initOptions.stack)
1162
- config.stack = initOptions.stack;
1163
- if (initOptions.language) {
1164
- config.language = initOptions.language
1165
- .split(',')
1166
- .map((l) => l.trim());
1167
- }
1168
- if (initOptions.database) {
1169
- config.database = initOptions.database
1170
- .split(',')
1171
- .map((d) => d.trim());
1172
- }
1173
- if (initOptions.observability) {
1174
- config.observability = initOptions.observability
1175
- .split(',')
1176
- .map((t) => t.trim());
1177
- }
1178
- if (initOptions.playwright)
1179
- config.playwright = true;
1180
- if (initOptions.cloudTools) {
1181
- config.cloudTools = initOptions.cloudTools
1182
- .split(',')
1183
- .map((t) => t.trim());
1184
- }
1185
- if (initOptions.devTools) {
1186
- config.devTools = initOptions.devTools.split(',').map((t) => t.trim());
1187
- }
1188
- if (initOptions.portOffset) {
1189
- config.portOffset = parseInt(initOptions.portOffset, 10);
1190
- }
1191
- if (initOptions.target && initOptions._targetSource !== 'default') {
1192
- config.target = initOptions.target;
1193
- }
1194
- if (initOptions.minimal) {
1195
- config.minimal = true;
1196
- }
1197
- if (initOptions.editor && initOptions._editorSource !== 'default') {
1198
- const editorLower = initOptions.editor.toLowerCase();
1199
- if (['vscode', 'jetbrains', 'none'].includes(editorLower)) {
1200
- config.editor = editorLower;
1201
- }
1202
- else {
1203
- console.warn(chalk.yellow(`⚠️ Invalid editor profile: ${initOptions.editor}, using default (vscode)`));
1204
- config.editor = 'vscode';
1205
- }
1206
- }
1207
- if (initOptions.output)
1208
- config.outputPath = initOptions.output;
1209
- // Handle --preset flag
1210
- if (initOptions.preset) {
1211
- config.preset = initOptions.preset;
1212
- }
1213
- // Handle --preset-param flags (can be repeated)
1214
- if (initOptions.presetParam && initOptions.presetParam.length > 0) {
1215
- if (!initOptions.preset) {
1216
- console.warn(chalk.yellow('⚠️ Ignoring --preset-param because no --preset was provided. ' +
1217
- 'Preset parameters only apply when a preset is selected (e.g., --preset web-api --preset-param broker=nats).'));
1218
- }
1219
- else {
1220
- const presetChoices = {};
1221
- for (const param of initOptions.presetParam) {
1222
- const eqIdx = param.indexOf('=');
1223
- if (eqIdx > 0) {
1224
- const key = param.slice(0, eqIdx).trim();
1225
- const value = param.slice(eqIdx + 1).trim();
1226
- if (key) {
1227
- presetChoices[key] = value;
1228
- }
1229
- }
1230
- else {
1231
- console.warn(chalk.yellow(`⚠️ Invalid --preset-param format: "${param}". Expected "key=value" (e.g., --preset-param broker=nats).`));
1232
- }
1233
- }
1234
- if (Object.keys(presetChoices).length > 0) {
1235
- config.presetChoices = presetChoices;
1236
- }
1237
- }
1238
- }
1239
- return {
1240
- commandName: initOptions.commandName,
1241
- config,
1242
- manifestPath: initOptions.fromManifest,
1243
- fromProject: initOptions.fromProject === true,
1244
- projectRoot: initOptions.projectRoot,
1245
- backupOverride: initOptions.backup, // undefined = auto-detect; true = --backup; false = --no-backup
1246
- backupDir: initOptions.backupDir,
1247
- noInteractive: initOptions.interactive === false, // Commander creates options.interactive = false for --no-interactive
1248
- writeManifestOnly: initOptions.writeManifestOnly === true,
1249
- };
1250
- }
1251
- async function main() {
1252
- try {
1253
- const cliArgs = await parseCliArgs();
1254
- const initialCwd = process.cwd();
1255
- if (cliArgs?.projectRoot) {
1256
- const resolvedProjectRoot = path.resolve(initialCwd, cliArgs.projectRoot);
1257
- if (!fs.existsSync(resolvedProjectRoot)) {
1258
- console.error(chalk.red(`✗ Project root not found: ${resolvedProjectRoot}`));
1259
- process.exit(1);
1260
- }
1261
- if (!fs.statSync(resolvedProjectRoot).isDirectory()) {
1262
- console.error(chalk.red(`✗ Project root is not a directory: ${resolvedProjectRoot}`));
1263
- process.exit(1);
1264
- }
1265
- process.chdir(resolvedProjectRoot);
1266
- }
1267
- let projectConfig = undefined;
1268
- let projectConfigAnswers;
1269
- if (!cliArgs?.manifestPath) {
1270
- const overlaysConfigForProject = loadOverlaysConfigWrapper();
1271
- projectConfig = loadProjectConfig(overlaysConfigForProject, process.cwd()) ?? undefined;
1272
- if (projectConfig) {
1273
- projectConfigAnswers = applyPresetSelections(buildAnswersFromProjectConfig(projectConfig.selection, overlaysConfigForProject));
1274
- }
1275
- }
1276
- let manifest;
1277
- let manifestDir;
1278
- let backupDir;
1279
- let useManifestOnly = false;
1280
- let useProjectOnly = false;
1281
- if (cliArgs?.commandName === 'regen' && !cliArgs.manifestPath && !cliArgs.fromProject) {
1282
- if (projectConfigAnswers) {
1283
- useProjectOnly = true;
1284
- }
1285
- else {
1286
- const discoveredManifestPath = findDefaultRegenManifest(cliArgs?.config?.outputPath || './.devcontainer');
1287
- if (!discoveredManifestPath) {
1288
- console.error(chalk.red('✗ Error: No project file or manifest found'));
1289
- console.error(chalk.gray(' Looked for .superposition.yml or superposition.yml in the repository root, and superposition.json in common manifest locations.'));
1290
- process.exit(1);
1291
- }
1292
- cliArgs.manifestPath = discoveredManifestPath;
1293
- }
1294
- }
1295
- // Handle manifest loading
1296
- if (cliArgs?.manifestPath) {
1297
- const manifestPath = findManifestFile(cliArgs.manifestPath);
1298
- if (!manifestPath) {
1299
- console.error(chalk.red('✗ Could not find manifest file'));
1300
- console.error(chalk.red(` Searched for: ${cliArgs.manifestPath}`));
1301
- process.exit(1);
1302
- }
1303
- manifestDir = path.dirname(manifestPath);
1304
- const loadedManifest = loadManifest(manifestPath);
1305
- if (!loadedManifest) {
1306
- process.exit(1);
1307
- }
1308
- manifest = loadedManifest;
1309
- // Check for interaction options
1310
- if (cliArgs.backupDir) {
1311
- backupDir = cliArgs.backupDir;
1312
- }
1313
- if (cliArgs.noInteractive) {
1314
- useManifestOnly = true;
1315
- }
1316
- }
1317
- if (cliArgs?.fromProject) {
1318
- if (!projectConfigAnswers || !projectConfig) {
1319
- console.error(chalk.red('✗ Could not find project file'));
1320
- console.error(chalk.red(' Searched for: .superposition.yml, superposition.yml'));
1321
- process.exit(1);
1322
- }
1323
- useProjectOnly = cliArgs.noInteractive || cliArgs.commandName === 'regen';
1324
- }
1325
- // Validate --no-interactive requires a persisted input source
1326
- if (cliArgs?.noInteractive && !cliArgs?.manifestPath && !projectConfigAnswers) {
1327
- console.error(chalk.red('✗ Error: --no-interactive requires persisted input'));
1328
- console.error(chalk.dim(' Use --from-project, --from-manifest <path>, or run from a repository with .superposition.yml or superposition.yml'));
1329
- process.exit(1);
1330
- }
1331
- // Determine whether to create a backup:
1332
- // --backup → always backup
1333
- // --no-backup → never backup
1334
- // (neither) → backup only when NOT inside a git repository
1335
- // (git already tracks history, so backups are redundant)
1336
- const resolvedOutputPath = cliArgs?.config?.outputPath ||
1337
- projectConfigAnswers?.outputPath ||
1338
- manifestDir ||
1339
- './.devcontainer';
1340
- const backupCheckPath = path.resolve(resolvedOutputPath);
1341
- const inGitRepo = isInsideGitRepo(backupCheckPath);
1342
- let shouldBackup;
1343
- if (cliArgs?.backupOverride === true) {
1344
- shouldBackup = true;
1345
- }
1346
- else if (cliArgs?.backupOverride === false) {
1347
- shouldBackup = false;
1348
- }
1349
- else {
1350
- // Auto-detect based on git presence
1351
- shouldBackup = !inGitRepo;
1352
- if (!shouldBackup) {
1353
- console.log(chalk.dim('ℹ Skipping backup — git repo detected (use --backup to force one)\n'));
1354
- }
1355
- }
1356
- const isReplayMode = cliArgs?.commandName === 'regen' || useManifestOnly || useProjectOnly;
1357
- // Create backup if needed
1358
- let actualBackupPath;
1359
- if (shouldBackup && isReplayMode) {
1360
- const outputPath = resolvedOutputPath;
1361
- const backupPath = await createBackup(outputPath, backupDir);
1362
- if (backupPath) {
1363
- actualBackupPath = backupPath;
1364
- console.log(chalk.green(`✓ Backup created: ${backupPath}\n`));
1365
- ensureBackupPatternsInGitignore(outputPath);
1366
- }
1367
- }
1368
- // Build answers based on mode
1369
- let answers;
1370
- // Check if there are CLI overrides beyond just output path and preset flags
1371
- // Preset/presetChoices alone don't constitute "CLI overrides" that bypass interactive mode
1372
- const hasCliOverrides = cliArgs &&
1373
- Object.keys(cliArgs.config).some((key) => key !== 'outputPath' &&
1374
- key !== 'preset' &&
1375
- key !== 'presetChoices' &&
1376
- !(key === 'target' && cliArgs.config.target === 'local') &&
1377
- !(key === 'editor' && cliArgs.config.editor === 'vscode') &&
1378
- cliArgs.config[key] !== undefined);
1379
- const hasAnyCliConfig = cliArgs &&
1380
- Object.entries(cliArgs.config).some(([key, value]) => value !== undefined &&
1381
- !(key === 'target' && value === 'local') &&
1382
- !(key === 'editor' && value === 'vscode'));
1383
- if (useManifestOnly && manifest && !hasCliOverrides) {
1384
- // Mode 1: Manifest-only (--from-manifest --no-interactive, no CLI overrides)
1385
- const manifestAnswers = buildAnswersFromManifest(manifest, manifestDir);
1386
- answers = mergeAnswers(manifestAnswers);
1387
- console.log('\n' +
1388
- boxen(chalk.bold.cyan('Regenerating from Manifest (No Interactive)\n\n') +
1389
- chalk.white('Configuration:\n') +
1390
- chalk.gray(` Template: ${manifest.baseTemplate}\n`) +
1391
- chalk.gray(` Base Image: ${manifest.baseImage}\n`) +
1392
- (manifest.containerName
1393
- ? chalk.gray(` Container: ${manifest.containerName}\n`)
1394
- : '') +
1395
- chalk.gray(` Overlays: ${manifest.overlays.join(', ')}\n`) +
1396
- (manifest.preset ? chalk.gray(` Preset: ${manifest.preset}\n`) : '') +
1397
- (manifest.portOffset
1398
- ? chalk.gray(` Port offset: ${manifest.portOffset}\n`)
1399
- : '') +
1400
- chalk.gray(` Output: ${answers.outputPath}`), { padding: 1, borderColor: 'cyan', borderStyle: 'round', margin: 1 }));
1401
- }
1402
- else if (useProjectOnly && projectConfigAnswers && !hasCliOverrides) {
1403
- const projectFileName = projectConfig?.file.fileName ?? '.superposition.yml';
1404
- answers = mergeAnswers(projectConfigAnswers, {
1405
- outputPath: cliArgs?.config?.outputPath ||
1406
- projectConfigAnswers.outputPath ||
1407
- './.devcontainer',
1408
- minimal: cliArgs?.config?.minimal,
1409
- editor: cliArgs?.config?.editor,
1410
- });
1411
- console.log('\n' +
1412
- boxen(chalk.bold.cyan('Regenerating from Project File (No Interactive)\n\n') +
1413
- chalk.white('Configuration:\n') +
1414
- chalk.gray(` Project file: ${projectFileName}\n`) +
1415
- chalk.gray(` Output: ${answers.outputPath}`), {
1416
- padding: 1,
1417
- borderColor: 'cyan',
1418
- borderStyle: 'round',
1419
- margin: 1,
1420
- }));
1421
- }
1422
- else if ((cliArgs && (cliArgs.config.stack || hasCliOverrides)) ||
1423
- (projectConfigAnswers && (cliArgs?.noInteractive || hasAnyCliConfig))) {
1424
- // Mode 2: CLI-based (with optional manifest defaults)
1425
- // This includes regen with --minimal or --editor flags
1426
- const cliAnswers = buildAnswersFromCliArgs(cliArgs.config);
1427
- const manifestAnswers = manifest
1428
- ? buildAnswersFromManifest(manifest, manifestDir)
1429
- : undefined;
1430
- answers = mergeAnswers(projectConfigAnswers, manifestAnswers, cliAnswers, {
1431
- outputPath: cliAnswers.outputPath || projectConfigAnswers?.outputPath || './.devcontainer',
1432
- });
1433
- const modeLabel = useManifestOnly && hasCliOverrides
1434
- ? 'Regenerating from Manifest with Overrides'
1435
- : useProjectOnly && projectConfigAnswers
1436
- ? 'Regenerating from Project File with Overrides'
1437
- : projectConfigAnswers && !manifest
1438
- ? 'Running from Project Config'
1439
- : 'Running in CLI mode';
1440
- console.log('\n' +
1441
- boxen(chalk.bold(modeLabel), {
1442
- padding: 0.5,
1443
- borderColor: 'blue',
1444
- borderStyle: 'round',
1445
- }));
1446
- // Show what's being overridden
1447
- if ((useManifestOnly || useProjectOnly) && hasCliOverrides) {
1448
- const overrides = [];
1449
- if (cliAnswers.minimal)
1450
- overrides.push('minimal mode');
1451
- if (cliAnswers.editor)
1452
- overrides.push(`editor: ${cliAnswers.editor}`);
1453
- if (overrides.length > 0) {
1454
- console.log(chalk.dim(` Overrides: ${overrides.join(', ')}`));
1455
- }
1456
- }
1457
- }
1458
- else {
1459
- // Mode 3: Interactive (with optional manifest pre-population and CLI preset pre-selection)
1460
- const interactiveAnswers = await runQuestionnaire(manifest, manifestDir, cliArgs?.config.preset || projectConfigAnswers?.preset, cliArgs?.config.presetChoices || projectConfigAnswers?.presetChoices, projectConfigAnswers);
1461
- answers = mergeAnswers(projectConfigAnswers, interactiveAnswers);
1462
- }
1463
- if (!manifest && projectConfig?.selection.customizations) {
1464
- const materializedOutputPath = path.resolve(answers.outputPath);
1465
- if (!fs.existsSync(materializedOutputPath)) {
1466
- fs.mkdirSync(materializedOutputPath, { recursive: true });
1467
- }
1468
- writeProjectConfigCustomizations(materializedOutputPath, projectConfig.selection.customizations);
1469
- }
1470
- // Show configuration summary
1471
- const summaryLines = [
1472
- chalk.bold.white('Configuration Summary\n'),
1473
- chalk.cyan('Base: ') + chalk.white(answers.stack),
1474
- ];
1475
- if (answers.language && answers.language.length > 0) {
1476
- summaryLines.push(chalk.cyan('Languages: ') + chalk.white(answers.language.join(', ')));
1477
- }
1478
- if (answers.database && answers.database.length > 0) {
1479
- summaryLines.push(chalk.cyan('Database: ') + chalk.white(answers.database.join(', ')));
1480
- }
1481
- summaryLines.push(chalk.cyan('Playwright: ') + chalk.white(answers.playwright ? 'Yes' : 'No'));
1482
- if (answers.observability && answers.observability.length > 0) {
1483
- summaryLines.push(chalk.cyan('Observability: ') + chalk.white(answers.observability.join(', ')));
1484
- }
1485
- if (answers.cloudTools && answers.cloudTools.length > 0) {
1486
- summaryLines.push(chalk.cyan('Cloud tools: ') + chalk.white(answers.cloudTools.join(', ')));
1487
- }
1488
- if (projectConfig?.file && !manifest) {
1489
- summaryLines.push(chalk.cyan('Project config: ') + chalk.white(projectConfig.file.fileName));
1490
- }
1491
- summaryLines.push(chalk.cyan('Output: ') + chalk.white(answers.outputPath));
1492
- console.log('\n' +
1493
- boxen(summaryLines.join('\n'), {
1494
- padding: 1,
1495
- borderColor: 'green',
1496
- borderStyle: 'round',
1497
- margin: { top: 0, bottom: 1 },
1498
- }));
1499
- // Check if we're in manifest-only mode
1500
- const isManifestOnly = cliArgs?.writeManifestOnly === true;
1501
- // Generate with spinner
1502
- const spinner = ora({
1503
- text: isManifestOnly
1504
- ? chalk.cyan('Generating manifest file...')
1505
- : chalk.cyan('Generating devcontainer configuration...'),
1506
- color: 'cyan',
1507
- }).start();
1508
- try {
1509
- let summary;
1510
- if (isManifestOnly) {
1511
- summary = await generateManifestOnly(answers, undefined, {
1512
- isRegen: isReplayMode,
1513
- });
1514
- spinner.succeed(chalk.green('Manifest created successfully!'));
1515
- }
1516
- else {
1517
- summary = await composeDevContainer(answers, undefined, { isRegen: isReplayMode });
1518
- spinner.succeed(chalk.green('DevContainer created successfully!'));
1519
- }
1520
- // Update summary with backup path and regen status
1521
- if (actualBackupPath) {
1522
- summary.backupPath = actualBackupPath;
1523
- }
1524
- // Print comprehensive summary
1525
- printSummary(summary);
1526
- }
1527
- catch (error) {
1528
- spinner.fail(chalk.red(isManifestOnly ? 'Failed to create manifest' : 'Failed to create devcontainer'));
1529
- throw error;
1530
- }
1531
- }
1532
- catch (error) {
1533
- console.error('\n' +
1534
- boxen(chalk.bold.red('Error\n\n') +
1535
- chalk.white(error instanceof Error ? error.message : String(error)), { padding: 1, borderColor: 'red', borderStyle: 'round' }));
1536
- process.exit(1);
1537
- }
1538
- }
2
+ import { main } from '../tool/cli/run.js';
1539
3
  main();
1540
4
  //# sourceMappingURL=init.js.map