container-superposition 0.1.1

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 (327) hide show
  1. package/README.md +843 -0
  2. package/dist/scripts/init.d.ts +3 -0
  3. package/dist/scripts/init.d.ts.map +1 -0
  4. package/dist/scripts/init.js +1190 -0
  5. package/dist/scripts/init.js.map +1 -0
  6. package/dist/scripts/migrate-to-manifests.d.ts +12 -0
  7. package/dist/scripts/migrate-to-manifests.d.ts.map +1 -0
  8. package/dist/scripts/migrate-to-manifests.js +230 -0
  9. package/dist/scripts/migrate-to-manifests.js.map +1 -0
  10. package/dist/tool/questionnaire/composer.d.ts +6 -0
  11. package/dist/tool/questionnaire/composer.d.ts.map +1 -0
  12. package/dist/tool/questionnaire/composer.js +1232 -0
  13. package/dist/tool/questionnaire/composer.js.map +1 -0
  14. package/dist/tool/readme/markdown-parser.d.ts +30 -0
  15. package/dist/tool/readme/markdown-parser.d.ts.map +1 -0
  16. package/dist/tool/readme/markdown-parser.js +139 -0
  17. package/dist/tool/readme/markdown-parser.js.map +1 -0
  18. package/dist/tool/readme/readme-generator.d.ts +9 -0
  19. package/dist/tool/readme/readme-generator.d.ts.map +1 -0
  20. package/dist/tool/readme/readme-generator.js +422 -0
  21. package/dist/tool/readme/readme-generator.js.map +1 -0
  22. package/dist/tool/schema/custom-loader.d.ts +17 -0
  23. package/dist/tool/schema/custom-loader.d.ts.map +1 -0
  24. package/dist/tool/schema/custom-loader.js +149 -0
  25. package/dist/tool/schema/custom-loader.js.map +1 -0
  26. package/dist/tool/schema/overlay-loader.d.ts +47 -0
  27. package/dist/tool/schema/overlay-loader.d.ts.map +1 -0
  28. package/dist/tool/schema/overlay-loader.js +252 -0
  29. package/dist/tool/schema/overlay-loader.js.map +1 -0
  30. package/dist/tool/schema/types.d.ts +212 -0
  31. package/dist/tool/schema/types.d.ts.map +1 -0
  32. package/dist/tool/schema/types.js +5 -0
  33. package/dist/tool/schema/types.js.map +1 -0
  34. package/docs/README.md +308 -0
  35. package/docs/architecture.md +233 -0
  36. package/docs/creating-overlays.md +549 -0
  37. package/docs/custom-patches.md +540 -0
  38. package/docs/dependencies.md +279 -0
  39. package/docs/examples/custom-patches-example.md +85 -0
  40. package/docs/examples.md +576 -0
  41. package/docs/messaging-comparison.md +265 -0
  42. package/docs/messaging-quick-start.md +385 -0
  43. package/docs/observability-workflow.md +537 -0
  44. package/docs/overlay-manifest-refactoring.md +214 -0
  45. package/docs/overlay-metadata-archive.md +54 -0
  46. package/docs/overlays.md +523 -0
  47. package/docs/presets-architecture.md +498 -0
  48. package/docs/presets.md +366 -0
  49. package/docs/publishing.md +476 -0
  50. package/docs/quick-reference.md +326 -0
  51. package/docs/ux.md +170 -0
  52. package/features/README.md +85 -0
  53. package/features/cross-distro-packages/README.md +146 -0
  54. package/features/cross-distro-packages/devcontainer-feature.json +20 -0
  55. package/features/cross-distro-packages/install.sh +58 -0
  56. package/features/local-secrets-manager/devcontainer-feature.json +18 -0
  57. package/features/local-secrets-manager/install.sh +127 -0
  58. package/features/project-scaffolder/devcontainer-feature.json +24 -0
  59. package/features/project-scaffolder/install.sh +100 -0
  60. package/features/team-conventions/devcontainer-feature.json +24 -0
  61. package/features/team-conventions/install.sh +93 -0
  62. package/overlays/.registry/README.md +14 -0
  63. package/overlays/.registry/base-images.yml +26 -0
  64. package/overlays/.registry/base-templates.yml +7 -0
  65. package/overlays/README.md +155 -0
  66. package/overlays/alertmanager/.env.example +5 -0
  67. package/overlays/alertmanager/README.md +465 -0
  68. package/overlays/alertmanager/alert-rules.yml +56 -0
  69. package/overlays/alertmanager/alertmanager.yml +42 -0
  70. package/overlays/alertmanager/devcontainer.patch.json +12 -0
  71. package/overlays/alertmanager/docker-compose.yml +20 -0
  72. package/overlays/alertmanager/overlay.yml +17 -0
  73. package/overlays/alertmanager/setup.sh +53 -0
  74. package/overlays/alertmanager/verify.sh +31 -0
  75. package/overlays/aws-cli/README.md +473 -0
  76. package/overlays/aws-cli/devcontainer.patch.json +13 -0
  77. package/overlays/aws-cli/overlay.yml +13 -0
  78. package/overlays/azure-cli/README.md +551 -0
  79. package/overlays/azure-cli/devcontainer.patch.json +8 -0
  80. package/overlays/azure-cli/overlay.yml +13 -0
  81. package/overlays/bun/README.md +312 -0
  82. package/overlays/bun/devcontainer.patch.json +41 -0
  83. package/overlays/bun/overlay.yml +16 -0
  84. package/overlays/bun/setup.sh +79 -0
  85. package/overlays/bun/verify.sh +30 -0
  86. package/overlays/codex/README.md +128 -0
  87. package/overlays/codex/devcontainer.patch.json +3 -0
  88. package/overlays/codex/overlay.yml +14 -0
  89. package/overlays/codex/setup.sh +24 -0
  90. package/overlays/codex/verify.sh +30 -0
  91. package/overlays/commitlint/README.md +333 -0
  92. package/overlays/commitlint/devcontainer.patch.json +8 -0
  93. package/overlays/commitlint/overlay.yml +16 -0
  94. package/overlays/commitlint/setup.sh +234 -0
  95. package/overlays/direnv/README.md +504 -0
  96. package/overlays/direnv/devcontainer.patch.json +6 -0
  97. package/overlays/direnv/overlay.yml +13 -0
  98. package/overlays/direnv/setup.sh +139 -0
  99. package/overlays/docker-in-docker/README.md +534 -0
  100. package/overlays/docker-in-docker/devcontainer.patch.json +10 -0
  101. package/overlays/docker-in-docker/overlay.yml +13 -0
  102. package/overlays/docker-sock/README.md +256 -0
  103. package/overlays/docker-sock/devcontainer.patch.json +9 -0
  104. package/overlays/docker-sock/docker-compose.yml +8 -0
  105. package/overlays/docker-sock/overlay.yml +13 -0
  106. package/overlays/dotnet/README.md +147 -0
  107. package/overlays/dotnet/devcontainer.patch.json +51 -0
  108. package/overlays/dotnet/global-tools.txt +24 -0
  109. package/overlays/dotnet/overlay.yml +13 -0
  110. package/overlays/dotnet/setup.sh +51 -0
  111. package/overlays/dotnet/verify.sh +26 -0
  112. package/overlays/gcloud/README.md +269 -0
  113. package/overlays/gcloud/devcontainer.patch.json +14 -0
  114. package/overlays/gcloud/overlay.yml +14 -0
  115. package/overlays/gcloud/verify.sh +52 -0
  116. package/overlays/git-helpers/README.md +168 -0
  117. package/overlays/git-helpers/devcontainer.patch.json +33 -0
  118. package/overlays/git-helpers/overlay.yml +15 -0
  119. package/overlays/git-helpers/setup.sh +91 -0
  120. package/overlays/go/README.md +293 -0
  121. package/overlays/go/devcontainer.patch.json +43 -0
  122. package/overlays/go/overlay.yml +15 -0
  123. package/overlays/go/setup.sh +33 -0
  124. package/overlays/go/verify.sh +40 -0
  125. package/overlays/grafana/.env.example +9 -0
  126. package/overlays/grafana/README.md +462 -0
  127. package/overlays/grafana/dashboard-provider.yml +11 -0
  128. package/overlays/grafana/dashboards/observability-overview.json +263 -0
  129. package/overlays/grafana/devcontainer.patch.json +12 -0
  130. package/overlays/grafana/docker-compose.yml +27 -0
  131. package/overlays/grafana/grafana-datasources.yml +57 -0
  132. package/overlays/grafana/overlay.yml +21 -0
  133. package/overlays/grafana/verify.sh +34 -0
  134. package/overlays/jaeger/.env.example +7 -0
  135. package/overlays/jaeger/README.md +867 -0
  136. package/overlays/jaeger/devcontainer.patch.json +12 -0
  137. package/overlays/jaeger/docker-compose.yml +17 -0
  138. package/overlays/jaeger/overlay.yml +19 -0
  139. package/overlays/java/README.md +267 -0
  140. package/overlays/java/devcontainer.patch.json +44 -0
  141. package/overlays/java/overlay.yml +16 -0
  142. package/overlays/java/setup.sh +41 -0
  143. package/overlays/java/verify.sh +42 -0
  144. package/overlays/just/README.md +443 -0
  145. package/overlays/just/devcontainer.patch.json +3 -0
  146. package/overlays/just/overlay.yml +13 -0
  147. package/overlays/just/setup.sh +182 -0
  148. package/overlays/kubectl-helm/README.md +660 -0
  149. package/overlays/kubectl-helm/devcontainer.patch.json +10 -0
  150. package/overlays/kubectl-helm/overlay.yml +13 -0
  151. package/overlays/loki/.env.example +5 -0
  152. package/overlays/loki/README.md +1156 -0
  153. package/overlays/loki/devcontainer.patch.json +12 -0
  154. package/overlays/loki/docker-compose.yml +18 -0
  155. package/overlays/loki/loki-config.yaml +45 -0
  156. package/overlays/loki/overlay.yml +17 -0
  157. package/overlays/minio/.env.example +9 -0
  158. package/overlays/minio/README.md +639 -0
  159. package/overlays/minio/devcontainer.patch.json +30 -0
  160. package/overlays/minio/docker-compose.yml +28 -0
  161. package/overlays/minio/overlay.yml +18 -0
  162. package/overlays/minio/setup.sh +61 -0
  163. package/overlays/minio/verify.sh +64 -0
  164. package/overlays/mkdocs/README.md +309 -0
  165. package/overlays/mkdocs/devcontainer.patch.json +24 -0
  166. package/overlays/mkdocs/overlay.yml +15 -0
  167. package/overlays/modern-cli-tools/README.md +556 -0
  168. package/overlays/modern-cli-tools/devcontainer.patch.json +3 -0
  169. package/overlays/modern-cli-tools/overlay.yml +13 -0
  170. package/overlays/modern-cli-tools/setup.sh +153 -0
  171. package/overlays/mongodb/.env.example +9 -0
  172. package/overlays/mongodb/README.md +481 -0
  173. package/overlays/mongodb/devcontainer.patch.json +32 -0
  174. package/overlays/mongodb/docker-compose.yml +44 -0
  175. package/overlays/mongodb/overlay.yml +17 -0
  176. package/overlays/mongodb/verify.sh +48 -0
  177. package/overlays/mysql/.env.example +11 -0
  178. package/overlays/mysql/README.md +542 -0
  179. package/overlays/mysql/devcontainer.patch.json +34 -0
  180. package/overlays/mysql/docker-compose.yml +55 -0
  181. package/overlays/mysql/overlay.yml +16 -0
  182. package/overlays/mysql/verify.sh +48 -0
  183. package/overlays/nats/.env.example +5 -0
  184. package/overlays/nats/README.md +762 -0
  185. package/overlays/nats/devcontainer.patch.json +24 -0
  186. package/overlays/nats/docker-compose.yml +31 -0
  187. package/overlays/nats/overlay.yml +18 -0
  188. package/overlays/nats/verify.sh +50 -0
  189. package/overlays/ngrok/README.md +503 -0
  190. package/overlays/ngrok/devcontainer.patch.json +3 -0
  191. package/overlays/ngrok/overlay.yml +14 -0
  192. package/overlays/ngrok/setup.sh +125 -0
  193. package/overlays/nodejs/README.md +192 -0
  194. package/overlays/nodejs/devcontainer.patch.json +49 -0
  195. package/overlays/nodejs/global-packages.txt +16 -0
  196. package/overlays/nodejs/overlay.yml +14 -0
  197. package/overlays/nodejs/setup.sh +46 -0
  198. package/overlays/nodejs/verify.sh +32 -0
  199. package/overlays/otel-collector/.env.example +9 -0
  200. package/overlays/otel-collector/README.md +1257 -0
  201. package/overlays/otel-collector/devcontainer.patch.json +28 -0
  202. package/overlays/otel-collector/docker-compose.yml +22 -0
  203. package/overlays/otel-collector/otel-collector-config.yaml +68 -0
  204. package/overlays/otel-collector/overlay.yml +21 -0
  205. package/overlays/otel-collector/setup.sh +49 -0
  206. package/overlays/otel-demo-nodejs/.env.example +2 -0
  207. package/overlays/otel-demo-nodejs/Dockerfile-otel-demo-nodejs +17 -0
  208. package/overlays/otel-demo-nodejs/README.md +409 -0
  209. package/overlays/otel-demo-nodejs/devcontainer.patch.json +12 -0
  210. package/overlays/otel-demo-nodejs/docker-compose.yml +19 -0
  211. package/overlays/otel-demo-nodejs/overlay.yml +23 -0
  212. package/overlays/otel-demo-nodejs/package-otel-demo-nodejs.json +20 -0
  213. package/overlays/otel-demo-nodejs/server-otel-demo-nodejs.js +259 -0
  214. package/overlays/otel-demo-nodejs/tracing-otel-demo-nodejs.js +57 -0
  215. package/overlays/otel-demo-nodejs/verify.sh +31 -0
  216. package/overlays/otel-demo-python/.env.example +2 -0
  217. package/overlays/otel-demo-python/Dockerfile-otel-demo-python +16 -0
  218. package/overlays/otel-demo-python/README.md +82 -0
  219. package/overlays/otel-demo-python/app-otel-demo-python.py +208 -0
  220. package/overlays/otel-demo-python/devcontainer.patch.json +12 -0
  221. package/overlays/otel-demo-python/docker-compose.yml +19 -0
  222. package/overlays/otel-demo-python/overlay.yml +23 -0
  223. package/overlays/otel-demo-python/requirements-otel-demo-python.txt +4 -0
  224. package/overlays/otel-demo-python/verify.sh +31 -0
  225. package/overlays/playwright/README.md +629 -0
  226. package/overlays/playwright/devcontainer.patch.json +9 -0
  227. package/overlays/playwright/overlay.yml +13 -0
  228. package/overlays/postgres/.env.example +6 -0
  229. package/overlays/postgres/README.md +602 -0
  230. package/overlays/postgres/devcontainer.patch.json +21 -0
  231. package/overlays/postgres/docker-compose.yml +22 -0
  232. package/overlays/postgres/overlay.yml +15 -0
  233. package/overlays/postgres/verify.sh +45 -0
  234. package/overlays/powershell/README.md +314 -0
  235. package/overlays/powershell/devcontainer.patch.json +22 -0
  236. package/overlays/powershell/overlay.yml +13 -0
  237. package/overlays/powershell/setup.sh +29 -0
  238. package/overlays/powershell/verify.sh +38 -0
  239. package/overlays/pre-commit/README.md +263 -0
  240. package/overlays/pre-commit/devcontainer.patch.json +9 -0
  241. package/overlays/pre-commit/overlay.yml +16 -0
  242. package/overlays/pre-commit/setup.sh +129 -0
  243. package/overlays/presets/docs-site.yml +118 -0
  244. package/overlays/presets/fullstack.yml +181 -0
  245. package/overlays/presets/microservice.yml +118 -0
  246. package/overlays/presets/web-api.yml +109 -0
  247. package/overlays/prometheus/.env.example +5 -0
  248. package/overlays/prometheus/README.md +1246 -0
  249. package/overlays/prometheus/devcontainer.patch.json +12 -0
  250. package/overlays/prometheus/docker-compose.yml +22 -0
  251. package/overlays/prometheus/overlay.yml +17 -0
  252. package/overlays/prometheus/prometheus.yml +12 -0
  253. package/overlays/prometheus/verify.sh +34 -0
  254. package/overlays/promtail/.env.example +2 -0
  255. package/overlays/promtail/README.md +357 -0
  256. package/overlays/promtail/devcontainer.patch.json +5 -0
  257. package/overlays/promtail/docker-compose.yml +16 -0
  258. package/overlays/promtail/overlay.yml +17 -0
  259. package/overlays/promtail/promtail-config.yaml +60 -0
  260. package/overlays/promtail/verify.sh +31 -0
  261. package/overlays/pulumi/README.md +472 -0
  262. package/overlays/pulumi/devcontainer.patch.json +13 -0
  263. package/overlays/pulumi/overlay.yml +14 -0
  264. package/overlays/pulumi/verify.sh +31 -0
  265. package/overlays/python/README.md +919 -0
  266. package/overlays/python/devcontainer.patch.json +41 -0
  267. package/overlays/python/overlay.yml +12 -0
  268. package/overlays/python/requirements-overlay.txt +13 -0
  269. package/overlays/python/setup.sh +47 -0
  270. package/overlays/python/verify.sh +32 -0
  271. package/overlays/rabbitmq/.env.example +7 -0
  272. package/overlays/rabbitmq/README.md +680 -0
  273. package/overlays/rabbitmq/devcontainer.patch.json +28 -0
  274. package/overlays/rabbitmq/docker-compose.yml +30 -0
  275. package/overlays/rabbitmq/overlay.yml +18 -0
  276. package/overlays/rabbitmq/verify.sh +41 -0
  277. package/overlays/redis/.env.example +4 -0
  278. package/overlays/redis/README.md +776 -0
  279. package/overlays/redis/devcontainer.patch.json +21 -0
  280. package/overlays/redis/docker-compose.yml +21 -0
  281. package/overlays/redis/overlay.yml +15 -0
  282. package/overlays/redis/verify.sh +41 -0
  283. package/overlays/redpanda/.env.example +10 -0
  284. package/overlays/redpanda/README.md +703 -0
  285. package/overlays/redpanda/devcontainer.patch.json +37 -0
  286. package/overlays/redpanda/docker-compose.yml +67 -0
  287. package/overlays/redpanda/overlay.yml +21 -0
  288. package/overlays/redpanda/verify.sh +48 -0
  289. package/overlays/rust/README.md +299 -0
  290. package/overlays/rust/devcontainer.patch.json +39 -0
  291. package/overlays/rust/overlay.yml +15 -0
  292. package/overlays/rust/setup.sh +36 -0
  293. package/overlays/rust/verify.sh +51 -0
  294. package/overlays/sqlite/README.md +584 -0
  295. package/overlays/sqlite/devcontainer.patch.json +14 -0
  296. package/overlays/sqlite/overlay.yml +15 -0
  297. package/overlays/sqlite/setup.sh +27 -0
  298. package/overlays/sqlite/verify.sh +43 -0
  299. package/overlays/sqlserver/.env.example +6 -0
  300. package/overlays/sqlserver/README.md +592 -0
  301. package/overlays/sqlserver/devcontainer.patch.json +22 -0
  302. package/overlays/sqlserver/docker-compose.yml +32 -0
  303. package/overlays/sqlserver/overlay.yml +17 -0
  304. package/overlays/sqlserver/verify.sh +30 -0
  305. package/overlays/tempo/.env.example +5 -0
  306. package/overlays/tempo/README.md +273 -0
  307. package/overlays/tempo/devcontainer.patch.json +12 -0
  308. package/overlays/tempo/docker-compose.yml +20 -0
  309. package/overlays/tempo/overlay.yml +20 -0
  310. package/overlays/tempo/tempo-config.yaml +32 -0
  311. package/overlays/tempo/verify.sh +31 -0
  312. package/overlays/terraform/README.md +389 -0
  313. package/overlays/terraform/devcontainer.patch.json +15 -0
  314. package/overlays/terraform/overlay.yml +14 -0
  315. package/overlays/terraform/verify.sh +63 -0
  316. package/package.json +74 -0
  317. package/templates/README.md +285 -0
  318. package/templates/compose/.devcontainer/devcontainer.json +46 -0
  319. package/templates/compose/.devcontainer/docker-compose.yml +12 -0
  320. package/templates/compose/README.md +20 -0
  321. package/templates/plain/.devcontainer/devcontainer.json +35 -0
  322. package/templates/plain/README.md +21 -0
  323. package/tool/README.md +281 -0
  324. package/tool/schema/base-images.schema.json +43 -0
  325. package/tool/schema/base-templates.schema.json +34 -0
  326. package/tool/schema/config.schema.json +71 -0
  327. package/tool/schema/overlay-manifest.schema.json +86 -0
@@ -0,0 +1,1190 @@
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 } from '../tool/questionnaire/composer.js';
12
+ import { loadOverlaysConfig } from '../tool/schema/overlay-loader.js';
13
+ // Get __dirname equivalent in ESM
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+ const OVERLAYS_DIR_CANDIDATES = [
17
+ // When running from TypeScript sources (e.g. ts-node), __dirname is "<root>/scripts"
18
+ path.join(__dirname, '..', 'overlays'),
19
+ // When running from compiled JS in "dist/scripts", __dirname is "<root>/dist/scripts"
20
+ path.join(__dirname, '..', '..', 'overlays'),
21
+ ];
22
+ const OVERLAYS_DIR = OVERLAYS_DIR_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ??
23
+ OVERLAYS_DIR_CANDIDATES[0];
24
+ const OVERLAYS_CONFIG_CANDIDATES = [
25
+ // When running from TypeScript sources (e.g. ts-node), __dirname is "<root>/scripts"
26
+ // and "../overlays/index.yml" resolves to "<root>/overlays/index.yml".
27
+ path.join(__dirname, '..', 'overlays', 'index.yml'),
28
+ // When running from compiled JS in "dist/scripts", __dirname is "<root>/dist/scripts"
29
+ // and "../../overlays/index.yml" resolves to "<root>/overlays/index.yml".
30
+ path.join(__dirname, '..', '..', 'overlays', 'index.yml'),
31
+ ];
32
+ const OVERLAYS_CONFIG_PATH = OVERLAYS_CONFIG_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ??
33
+ OVERLAYS_CONFIG_CANDIDATES[0];
34
+ const PRESETS_DIR_CANDIDATES = [
35
+ path.join(__dirname, '..', 'overlays', 'presets'),
36
+ path.join(__dirname, '..', '..', 'overlays', 'presets'),
37
+ ];
38
+ const PRESETS_DIR = PRESETS_DIR_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ??
39
+ PRESETS_DIR_CANDIDATES[0];
40
+ /**
41
+ * Load overlay metadata from individual manifests or fallback to YAML file
42
+ */
43
+ function loadOverlaysConfigWrapper() {
44
+ return loadOverlaysConfig(OVERLAYS_DIR, OVERLAYS_CONFIG_PATH);
45
+ }
46
+ /**
47
+ * Load preset definition from YAML file
48
+ */
49
+ function loadPresetDefinition(presetId) {
50
+ const presetPath = path.join(PRESETS_DIR, `${presetId}.yml`);
51
+ if (!fs.existsSync(presetPath)) {
52
+ console.warn(chalk.yellow(`⚠️ Preset definition not found: ${presetPath}`));
53
+ return null;
54
+ }
55
+ const content = fs.readFileSync(presetPath, 'utf8');
56
+ return yaml.load(content);
57
+ }
58
+ /**
59
+ * Expand a preset into a list of overlay IDs with user choices resolved
60
+ */
61
+ async function expandPreset(presetId, stack) {
62
+ const preset = loadPresetDefinition(presetId);
63
+ if (!preset) {
64
+ return { overlays: [], choices: {} };
65
+ }
66
+ console.log(chalk.cyan(`\n📦 Expanding preset: ${preset.name}\n`));
67
+ const overlays = [...preset.selects.required];
68
+ const choices = {};
69
+ // Handle user choices
70
+ if (preset.selects.userChoice) {
71
+ for (const [key, choice] of Object.entries(preset.selects.userChoice)) {
72
+ const selectedOption = (await select({
73
+ message: choice.prompt,
74
+ choices: choice.options.map((opt) => ({
75
+ name: opt,
76
+ value: opt,
77
+ })),
78
+ default: choice.defaultOption,
79
+ }));
80
+ overlays.push(selectedOption);
81
+ choices[key] = selectedOption;
82
+ }
83
+ }
84
+ console.log(chalk.dim(`✓ Preset will include: ${overlays.join(', ')}\n`));
85
+ return { overlays, choices, glueConfig: preset.glueConfig };
86
+ }
87
+ /**
88
+ * Search for manifest file in multiple locations
89
+ */
90
+ function findManifestFile(manifestPath) {
91
+ const searchPaths = [];
92
+ if (manifestPath) {
93
+ // If path specified, use it directly
94
+ searchPaths.push(manifestPath);
95
+ }
96
+ else {
97
+ // Search in common locations
98
+ searchPaths.push('superposition.json', '.devcontainer/superposition.json', '../superposition.json', path.join(process.cwd(), 'superposition.json'), path.join(process.cwd(), '.devcontainer', 'superposition.json'));
99
+ }
100
+ for (const searchPath of searchPaths) {
101
+ const resolvedPath = path.resolve(searchPath);
102
+ if (fs.existsSync(resolvedPath)) {
103
+ return resolvedPath;
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+ /**
109
+ * Load and validate manifest file
110
+ */
111
+ function loadManifest(manifestPath) {
112
+ try {
113
+ const content = fs.readFileSync(manifestPath, 'utf-8');
114
+ const manifest = JSON.parse(content);
115
+ // Basic validation
116
+ if (!manifest.version || !manifest.baseTemplate) {
117
+ console.error(chalk.red('✗ Invalid manifest format: missing required fields (version, baseTemplate)'));
118
+ return null;
119
+ }
120
+ if (!Array.isArray(manifest.overlays)) {
121
+ console.error(chalk.red('✗ Invalid manifest format: "overlays" must be an array'));
122
+ return null;
123
+ }
124
+ if (!manifest.overlays.every((overlay) => typeof overlay === 'string')) {
125
+ console.error(chalk.red('✗ Invalid manifest format: all "overlays" entries must be strings'));
126
+ return null;
127
+ }
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
+ return manifest;
133
+ }
134
+ catch (error) {
135
+ console.error(chalk.red(`✗ Failed to load manifest: ${error instanceof Error ? error.message : String(error)}`));
136
+ return null;
137
+ }
138
+ }
139
+ /**
140
+ * Create timestamped backup of existing devcontainer and manifest
141
+ */
142
+ async function createBackup(outputPath, backupDir) {
143
+ // Check for devcontainer files to backup
144
+ const devcontainerJsonPath = path.join(outputPath, 'devcontainer.json');
145
+ const dockerComposePath = path.join(outputPath, 'docker-compose.yml');
146
+ const devcontainerSubdir = path.join(outputPath, '.devcontainer');
147
+ const manifestPath = path.join(outputPath, 'superposition.json');
148
+ // Determine what exists
149
+ const hasDevcontainerJson = fs.existsSync(devcontainerJsonPath);
150
+ const hasDockerCompose = fs.existsSync(dockerComposePath);
151
+ const hasDevcontainerSubdir = fs.existsSync(devcontainerSubdir) && fs.statSync(devcontainerSubdir).isDirectory();
152
+ const hasManifest = fs.existsSync(manifestPath);
153
+ if (!hasDevcontainerJson && !hasDockerCompose && !hasDevcontainerSubdir && !hasManifest) {
154
+ return null; // Nothing to backup
155
+ }
156
+ // Create timestamp
157
+ const timestamp = new Date()
158
+ .toISOString()
159
+ .replace(/:/g, '-')
160
+ .replace(/\..+/, '')
161
+ .replace('T', '-');
162
+ // Determine backup location - create next to outputPath, not inside it
163
+ const resolvedOutputPath = path.resolve(outputPath);
164
+ const outputParentDir = path.dirname(resolvedOutputPath);
165
+ const outputBaseName = path.basename(resolvedOutputPath);
166
+ const backupBaseName = outputBaseName === '.devcontainer' ? '.devcontainer' : outputBaseName;
167
+ const backupPath = backupDir
168
+ ? path.resolve(backupDir)
169
+ : path.join(outputParentDir, `${backupBaseName}.backup-${timestamp}`);
170
+ // Create backup directory
171
+ fs.mkdirSync(backupPath, { recursive: true });
172
+ // Backup files and directories
173
+ if (hasDevcontainerJson) {
174
+ fs.copyFileSync(devcontainerJsonPath, path.join(backupPath, 'devcontainer.json'));
175
+ }
176
+ if (hasDockerCompose) {
177
+ fs.copyFileSync(dockerComposePath, path.join(backupPath, 'docker-compose.yml'));
178
+ }
179
+ if (hasDevcontainerSubdir) {
180
+ const destDir = path.join(backupPath, '.devcontainer');
181
+ await copyDirectory(devcontainerSubdir, destDir);
182
+ }
183
+ if (hasManifest) {
184
+ fs.copyFileSync(manifestPath, path.join(backupPath, 'superposition.json'));
185
+ }
186
+ // Also backup other common devcontainer files
187
+ const otherFiles = ['.env', '.env.example', '.gitignore', 'features', 'scripts'];
188
+ for (const file of otherFiles) {
189
+ const srcPath = path.join(outputPath, file);
190
+ if (fs.existsSync(srcPath)) {
191
+ const destPath = path.join(backupPath, file);
192
+ if (fs.statSync(srcPath).isDirectory()) {
193
+ await copyDirectory(srcPath, destPath);
194
+ }
195
+ else {
196
+ fs.copyFileSync(srcPath, destPath);
197
+ }
198
+ }
199
+ }
200
+ return backupPath;
201
+ }
202
+ /**
203
+ * Recursively copy directory
204
+ */
205
+ async function copyDirectory(src, dest) {
206
+ fs.mkdirSync(dest, { recursive: true });
207
+ const entries = fs.readdirSync(src, { withFileTypes: true });
208
+ for (const entry of entries) {
209
+ const srcPath = path.join(src, entry.name);
210
+ const destPath = path.join(dest, entry.name);
211
+ if (entry.isDirectory()) {
212
+ await copyDirectory(srcPath, destPath);
213
+ }
214
+ else {
215
+ fs.copyFileSync(srcPath, destPath);
216
+ }
217
+ }
218
+ }
219
+ /**
220
+ * Ensure backup patterns are in .gitignore
221
+ */
222
+ async function ensureBackupPatternsInGitignore(outputPath) {
223
+ // Write to the parent directory's .gitignore (project root), not inside outputPath
224
+ const resolvedOutputPath = path.resolve(outputPath);
225
+ const projectRoot = path.dirname(resolvedOutputPath);
226
+ const gitignorePath = path.join(projectRoot, '.gitignore');
227
+ const backupPatterns = [
228
+ '',
229
+ '# Container Superposition backups',
230
+ '.devcontainer.backup-*/',
231
+ '*.backup-*',
232
+ 'superposition.json.backup-*',
233
+ ].join('\n');
234
+ if (!fs.existsSync(gitignorePath)) {
235
+ // Create new .gitignore with backup patterns
236
+ await fs.promises.writeFile(gitignorePath, backupPatterns + '\n');
237
+ console.log(chalk.dim(' 📝 Created .gitignore with backup patterns'));
238
+ }
239
+ else {
240
+ // Check if patterns already exist
241
+ const content = await fs.promises.readFile(gitignorePath, 'utf-8');
242
+ if (!content.includes('Container Superposition backups')) {
243
+ // Append patterns
244
+ await fs.promises.appendFile(gitignorePath, '\n' + backupPatterns + '\n');
245
+ console.log(chalk.dim(' 📝 Updated .gitignore with backup patterns'));
246
+ }
247
+ }
248
+ }
249
+ /**
250
+ * Build checkbox choices for overlay selection with optional pre-selection
251
+ */
252
+ function buildOverlayChoices(config, stack, categoryList, preselected) {
253
+ const choices = [];
254
+ categoryList.forEach((category) => {
255
+ const filtered = category.overlays.filter((o) => !o.supports || o.supports.length === 0 || o.supports.includes(stack));
256
+ if (filtered.length > 0) {
257
+ // Add category separator
258
+ choices.push({
259
+ type: 'separator',
260
+ separator: chalk.cyan(`──── ${category.name} ────`),
261
+ });
262
+ // Add overlays in this category
263
+ filtered.forEach((overlay) => {
264
+ choices.push({
265
+ name: overlay.name,
266
+ value: overlay.id,
267
+ description: overlay.description,
268
+ checked: preselected.includes(overlay.id),
269
+ });
270
+ });
271
+ }
272
+ });
273
+ return choices;
274
+ }
275
+ /**
276
+ * Interactive questionnaire with modern checkbox selections
277
+ */
278
+ async function runQuestionnaire(manifest, manifestDir) {
279
+ const config = loadOverlaysConfigWrapper();
280
+ // Pretty banner
281
+ console.log('\n' +
282
+ boxen(chalk.bold.cyan('Container Superposition') +
283
+ '\n' +
284
+ chalk.gray(manifest ? 'DevContainer Regenerator' : 'DevContainer Initializer'), {
285
+ padding: 1,
286
+ margin: 1,
287
+ borderStyle: 'round',
288
+ borderColor: 'cyan',
289
+ textAlignment: 'center',
290
+ }));
291
+ if (manifest) {
292
+ console.log(chalk.cyan('📋 Loaded from manifest:'));
293
+ console.log(chalk.dim(` Template: ${manifest.baseTemplate}`));
294
+ console.log(chalk.dim(` Overlays: ${manifest.overlays.join(', ')}`));
295
+ if (manifest.preset) {
296
+ console.log(chalk.dim(` Preset: ${manifest.preset}`));
297
+ }
298
+ if (manifest.portOffset) {
299
+ console.log(chalk.dim(` Port offset: ${manifest.portOffset}`));
300
+ }
301
+ console.log();
302
+ }
303
+ console.log(chalk.dim('Compose your ideal devcontainer from modular overlays.'));
304
+ console.log(chalk.dim('Use ') +
305
+ chalk.cyan('space') +
306
+ chalk.dim(' to select, ') +
307
+ chalk.cyan('enter') +
308
+ chalk.dim(' to confirm.\n'));
309
+ try {
310
+ // Question 0: Optional preset selection
311
+ let usePreset = false;
312
+ let selectedPresetId = manifest?.preset;
313
+ let presetChoices = manifest?.presetChoices || {};
314
+ let presetGlueConfig;
315
+ const presetOverlaysFiltered = config.overlays.filter((o) => o.category === 'preset');
316
+ let presetOverlays = [];
317
+ if (presetOverlaysFiltered.length > 0) {
318
+ const defaultPreset = manifest?.preset || 'custom';
319
+ const presetChoice = (await select({
320
+ message: 'Start from a preset or build custom?',
321
+ choices: [
322
+ {
323
+ name: 'Custom (select overlays manually)',
324
+ value: 'custom',
325
+ description: 'Choose individual overlays yourself',
326
+ },
327
+ ...presetOverlaysFiltered.map((p) => ({
328
+ name: p.name,
329
+ value: p.id,
330
+ description: p.description,
331
+ })),
332
+ ],
333
+ default: defaultPreset,
334
+ }));
335
+ if (presetChoice !== 'custom') {
336
+ usePreset = true;
337
+ selectedPresetId = presetChoice;
338
+ }
339
+ }
340
+ // Question 1: Base template
341
+ const stack = (await select({
342
+ message: 'Select base template:',
343
+ choices: config.base_templates.map((t) => ({
344
+ name: t.name,
345
+ value: t.id,
346
+ description: t.description,
347
+ })),
348
+ default: manifest?.baseTemplate,
349
+ }));
350
+ // If using preset, expand it now
351
+ if (usePreset && selectedPresetId) {
352
+ const expansion = await expandPreset(selectedPresetId, stack);
353
+ if (!expansion.overlays || expansion.overlays.length === 0) {
354
+ // Preset failed to expand (e.g., missing or invalid preset definition).
355
+ // Treat this as "no preset" so the manifest does not incorrectly record one.
356
+ console.log(chalk.yellow(`\n⚠️ Preset "${selectedPresetId}" could not be applied. Falling back to custom overlay selection.\n`));
357
+ usePreset = false;
358
+ selectedPresetId = undefined;
359
+ presetOverlays = [];
360
+ presetChoices = {};
361
+ presetGlueConfig = undefined;
362
+ }
363
+ else {
364
+ presetOverlays = expansion.overlays;
365
+ presetChoices = expansion.choices;
366
+ presetGlueConfig = expansion.glueConfig;
367
+ }
368
+ }
369
+ // Question 2: Base image selection
370
+ // Check if manifest has a custom image or a known base image
371
+ const knownBaseImageIds = config.base_images.map((img) => img.id);
372
+ const manifestBaseImageIsKnown = manifest?.baseImage && knownBaseImageIds.includes(manifest.baseImage);
373
+ const manifestDefaultBaseImage = manifestBaseImageIsKnown ? manifest.baseImage : 'custom';
374
+ const baseImage = (await select({
375
+ message: 'Select base image:',
376
+ choices: config.base_images.map((img) => ({
377
+ name: img.name,
378
+ value: img.id,
379
+ description: img.description,
380
+ })),
381
+ default: manifestDefaultBaseImage,
382
+ }));
383
+ // Question 2a: If custom, ask for image name
384
+ let customImage;
385
+ if (baseImage === 'custom') {
386
+ // If manifest has a custom image, use it as default
387
+ const manifestCustomImage = !manifestBaseImageIsKnown && manifest?.baseImage ? manifest.baseImage : undefined;
388
+ customImage = await input({
389
+ message: 'Enter custom Docker image (e.g., ubuntu:22.04):',
390
+ default: manifestCustomImage,
391
+ validate: (value) => {
392
+ if (!value || value.trim() === '') {
393
+ return 'Image name is required';
394
+ }
395
+ return true;
396
+ },
397
+ });
398
+ console.log(chalk.yellow('\n⚠️ Warning: Custom images may conflict with overlays.'));
399
+ console.log(chalk.dim(' Test thoroughly and adjust configurations as needed.\n'));
400
+ }
401
+ // Build categorized overlays with separators
402
+ const categoryList = [
403
+ {
404
+ name: 'Language',
405
+ overlays: config.overlays.filter((o) => o.category === 'language'),
406
+ },
407
+ {
408
+ name: 'Database',
409
+ overlays: config.overlays.filter((o) => o.category === 'database'),
410
+ },
411
+ {
412
+ name: 'Observability',
413
+ overlays: config.overlays.filter((o) => o.category === 'observability'),
414
+ },
415
+ { name: 'Cloud', overlays: config.overlays.filter((o) => o.category === 'cloud') },
416
+ { name: 'DevTool', overlays: config.overlays.filter((o) => o.category === 'dev') },
417
+ ];
418
+ // Create a map of all overlays for dependency lookup
419
+ const allOverlaysMap = new Map(config.overlays.map((o) => [o.id, o]));
420
+ // Question 3: Categorized multi-select overlays with dependency tracking
421
+ let userSelection;
422
+ if (usePreset && presetOverlays.length > 0) {
423
+ // Preset mode: Ask if user wants to customize
424
+ console.log(chalk.cyan(`\n✓ Preset includes these overlays: ${presetOverlays.join(', ')}\n`));
425
+ const customizePreset = (await select({
426
+ message: 'Do you want to customize the overlay selection?',
427
+ choices: [
428
+ {
429
+ name: 'Use preset as-is',
430
+ value: 'no',
431
+ description: 'Keep the preset overlay selection',
432
+ },
433
+ {
434
+ name: 'Customize selection',
435
+ value: 'yes',
436
+ description: 'Add or remove overlays from the preset',
437
+ },
438
+ ],
439
+ }));
440
+ if (customizePreset === 'yes') {
441
+ // Show overlay selection with preset overlays pre-selected
442
+ console.log(chalk.dim('\n💡 Select overlays: Space to toggle, ↑/↓ to navigate, Enter to confirm'));
443
+ console.log(chalk.dim(' Preset overlays are pre-selected\n'));
444
+ const choices = buildOverlayChoices(config, stack, categoryList, presetOverlays);
445
+ userSelection = await checkbox({
446
+ message: 'Select overlays to include:',
447
+ choices,
448
+ pageSize: 15,
449
+ loop: false,
450
+ });
451
+ }
452
+ else {
453
+ // Use preset selection as-is
454
+ userSelection = presetOverlays;
455
+ }
456
+ }
457
+ else if (manifest) {
458
+ // Manifest mode: Pre-select overlays from manifest
459
+ console.log(chalk.cyan(`\n✓ Manifest includes these overlays: ${manifest.overlays.join(', ')}\n`));
460
+ const customizeManifest = (await select({
461
+ message: 'Do you want to customize the overlay selection?',
462
+ choices: [
463
+ {
464
+ name: 'Use manifest as-is',
465
+ value: 'no',
466
+ description: 'Keep the manifest overlay selection',
467
+ },
468
+ {
469
+ name: 'Customize selection',
470
+ value: 'yes',
471
+ description: 'Add or remove overlays from the manifest',
472
+ },
473
+ ],
474
+ }));
475
+ if (customizeManifest === 'yes') {
476
+ // Show overlay selection with manifest overlays pre-selected
477
+ console.log(chalk.dim('\n💡 Select overlays: Space to toggle, ↑/↓ to navigate, Enter to confirm'));
478
+ console.log(chalk.dim(' Manifest overlays are pre-selected\n'));
479
+ // Filter out overlays that don't exist anymore
480
+ const existingOverlays = manifest.overlays.filter((id) => allOverlaysMap.has(id));
481
+ const missingOverlays = manifest.overlays.filter((id) => !allOverlaysMap.has(id));
482
+ if (missingOverlays.length > 0) {
483
+ console.log(chalk.yellow(`⚠️ Warning: Some overlays from manifest no longer exist: ${missingOverlays.join(', ')}\n`));
484
+ }
485
+ const choices = buildOverlayChoices(config, stack, categoryList, existingOverlays);
486
+ userSelection = await checkbox({
487
+ message: 'Select overlays to include:',
488
+ choices,
489
+ pageSize: 15,
490
+ loop: false,
491
+ });
492
+ }
493
+ else {
494
+ // Use manifest selection as-is (filtering out missing overlays)
495
+ const existingOverlays = manifest.overlays.filter((id) => allOverlaysMap.has(id));
496
+ const missingOverlays = manifest.overlays.filter((id) => !allOverlaysMap.has(id));
497
+ if (missingOverlays.length > 0) {
498
+ console.log(chalk.yellow(`⚠️ Warning: Some overlays from manifest no longer exist and will be skipped: ${missingOverlays.join(', ')}\n`));
499
+ }
500
+ userSelection = existingOverlays;
501
+ }
502
+ }
503
+ else {
504
+ // Custom mode: Normal overlay selection
505
+ console.log(chalk.dim('\n💡 Select overlays: Space to toggle, ↑/↓ to navigate, Enter to confirm\n'));
506
+ const choices = buildOverlayChoices(config, stack, categoryList, []);
507
+ userSelection = await checkbox({
508
+ message: 'Select overlays to include:',
509
+ choices,
510
+ pageSize: 15,
511
+ loop: false,
512
+ });
513
+ }
514
+ // Add all required dependencies
515
+ const withDependencies = new Set(userSelection);
516
+ const toProcess = [...userSelection];
517
+ while (toProcess.length > 0) {
518
+ const current = toProcess.pop();
519
+ const overlay = allOverlaysMap.get(current);
520
+ if (overlay?.requires) {
521
+ overlay.requires.forEach((req) => {
522
+ if (!withDependencies.has(req)) {
523
+ withDependencies.add(req);
524
+ toProcess.push(req);
525
+ }
526
+ });
527
+ }
528
+ }
529
+ let selectedOverlays = Array.from(withDependencies);
530
+ // Check for conflicts and resolve
531
+ let hasConflicts = true;
532
+ while (hasConflicts) {
533
+ const conflicts = new Map();
534
+ // Find all conflicts
535
+ selectedOverlays.forEach((selectedId) => {
536
+ const overlay = allOverlaysMap.get(selectedId);
537
+ if (overlay?.conflicts) {
538
+ overlay.conflicts.forEach((conflictId) => {
539
+ if (selectedOverlays.includes(conflictId)) {
540
+ if (!conflicts.has(selectedId)) {
541
+ conflicts.set(selectedId, []);
542
+ }
543
+ conflicts.get(selectedId).push(conflictId);
544
+ }
545
+ });
546
+ }
547
+ });
548
+ if (conflicts.size === 0) {
549
+ hasConflicts = false;
550
+ }
551
+ else {
552
+ // Show conflict resolution UI
553
+ console.log(chalk.yellow('\n⚠️ Conflicts detected in selection:\n'));
554
+ const conflictChoices = [];
555
+ conflicts.forEach((conflictingWith, overlayId) => {
556
+ const overlay = allOverlaysMap.get(overlayId);
557
+ const conflictNames = conflictingWith
558
+ .map((id) => allOverlaysMap.get(id)?.name)
559
+ .join(', ');
560
+ conflictChoices.push({
561
+ name: `Remove ${overlay.name}`,
562
+ value: overlayId,
563
+ description: `Conflicts with: ${conflictNames}`,
564
+ });
565
+ });
566
+ const toRemove = await checkbox({
567
+ message: 'Select overlays to remove to resolve conflicts:',
568
+ choices: conflictChoices,
569
+ pageSize: 15,
570
+ loop: false,
571
+ });
572
+ if (toRemove.length === 0) {
573
+ console.log(chalk.red('\n❌ You must remove at least one conflicting overlay'));
574
+ continue;
575
+ }
576
+ // Remove selected overlays
577
+ selectedOverlays = selectedOverlays.filter((id) => !toRemove.includes(id));
578
+ }
579
+ }
580
+ // Question 4: Container name
581
+ const containerName = await input({
582
+ message: 'Container/project name (optional):',
583
+ default: manifest?.containerName || '',
584
+ });
585
+ // Question 5: Output path
586
+ // If manifest provided, default to its location; otherwise use ./.devcontainer
587
+ const defaultOutput = manifestDir || './.devcontainer';
588
+ const outputPath = await input({
589
+ message: 'Output path:',
590
+ default: defaultOutput,
591
+ });
592
+ // Question 6: Port offset (optional, for running multiple instances)
593
+ const portOffsetInput = await input({
594
+ message: 'Port offset (leave empty for default ports, e.g., 100 to avoid conflicts):',
595
+ default: manifest?.portOffset ? String(manifest.portOffset) : '',
596
+ });
597
+ const portOffset = portOffsetInput ? parseInt(portOffsetInput, 10) : undefined;
598
+ // Parse selected overlays into categories
599
+ const overlayMap = new Map(config.overlays.map((o) => [o.id, o]));
600
+ const language = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'language');
601
+ const observability = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'observability');
602
+ const cloudTools = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'cloud');
603
+ const devTools = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'dev');
604
+ const database = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'database');
605
+ const playwright = selectedOverlays.includes('playwright');
606
+ return {
607
+ stack,
608
+ baseImage,
609
+ customImage,
610
+ containerName: containerName || undefined,
611
+ preset: selectedPresetId,
612
+ presetChoices: Object.keys(presetChoices).length > 0 ? presetChoices : undefined,
613
+ presetGlueConfig,
614
+ language,
615
+ needsDocker: stack === 'compose', // Compose template includes docker-outside-of-docker
616
+ database,
617
+ playwright,
618
+ cloudTools,
619
+ devTools,
620
+ observability,
621
+ outputPath,
622
+ portOffset,
623
+ };
624
+ }
625
+ catch (error) {
626
+ if (error.name === 'ExitPromptError') {
627
+ console.log('\n' + chalk.yellow('Cancelled by user'));
628
+ process.exit(0);
629
+ }
630
+ throw error;
631
+ }
632
+ }
633
+ /**
634
+ * Build partial answers from manifest
635
+ * Note: Categories are only used for UI/questionnaire grouping.
636
+ * The composer works with overlay IDs regardless of category.
637
+ */
638
+ function buildAnswersFromManifest(manifest, manifestDir) {
639
+ const config = loadOverlaysConfigWrapper();
640
+ // Helper to categorize overlays by type (for QuestionnaireAnswers structure)
641
+ const categorizeOverlays = (overlayIds) => {
642
+ const language = [];
643
+ const database = [];
644
+ const observability = [];
645
+ const cloudTools = [];
646
+ const devTools = [];
647
+ // Build lookup map from unified overlays array
648
+ const overlayMap = new Map(config.overlays.map((o) => [o.id, o]));
649
+ // Categorize based on overlay metadata
650
+ for (const id of overlayIds) {
651
+ const overlay = overlayMap.get(id);
652
+ if (!overlay)
653
+ continue;
654
+ switch (overlay.category) {
655
+ case 'language':
656
+ language.push(id);
657
+ break;
658
+ case 'database':
659
+ database.push(id);
660
+ break;
661
+ case 'observability':
662
+ observability.push(id);
663
+ break;
664
+ case 'cloud':
665
+ cloudTools.push(id);
666
+ break;
667
+ case 'dev':
668
+ devTools.push(id);
669
+ break;
670
+ }
671
+ }
672
+ return { language, database, observability, cloudTools, devTools };
673
+ };
674
+ const categories = categorizeOverlays(manifest.overlays);
675
+ // Output path is always the directory containing the manifest
676
+ const outputPath = manifestDir || './.devcontainer';
677
+ // Handle baseImage - check if it's a known ID or a custom image string
678
+ const knownBaseImageIds = ['bookworm', 'trixie', 'alpine', 'ubuntu', 'custom'];
679
+ const isKnownBaseImage = knownBaseImageIds.includes(manifest.baseImage);
680
+ return {
681
+ stack: manifest.baseTemplate,
682
+ baseImage: isKnownBaseImage ? manifest.baseImage : 'custom',
683
+ customImage: isKnownBaseImage ? undefined : manifest.baseImage,
684
+ containerName: manifest.containerName,
685
+ preset: manifest.preset,
686
+ presetChoices: manifest.presetChoices,
687
+ ...categories,
688
+ needsDocker: manifest.baseTemplate === 'compose',
689
+ playwright: categories.devTools.includes('playwright'),
690
+ outputPath,
691
+ portOffset: manifest.portOffset,
692
+ };
693
+ }
694
+ /**
695
+ * Build partial answers from CLI arguments
696
+ */
697
+ function buildAnswersFromCliArgs(config) {
698
+ const answers = {};
699
+ if (config.stack) {
700
+ answers.stack = config.stack;
701
+ answers.needsDocker = config.stack === 'compose';
702
+ }
703
+ if (config.baseImage)
704
+ answers.baseImage = config.baseImage;
705
+ if (config.containerName)
706
+ answers.containerName = config.containerName;
707
+ if (config.language)
708
+ answers.language = config.language;
709
+ if (config.database)
710
+ answers.database = config.database;
711
+ if (config.playwright !== undefined)
712
+ answers.playwright = config.playwright;
713
+ if (config.observability)
714
+ answers.observability = config.observability;
715
+ if (config.cloudTools)
716
+ answers.cloudTools = config.cloudTools;
717
+ if (config.devTools)
718
+ answers.devTools = config.devTools;
719
+ if (config.portOffset !== undefined)
720
+ answers.portOffset = config.portOffset;
721
+ if (config.outputPath)
722
+ answers.outputPath = config.outputPath;
723
+ if (config.preset)
724
+ answers.preset = config.preset;
725
+ if (config.presetChoices)
726
+ answers.presetChoices = config.presetChoices;
727
+ return answers;
728
+ }
729
+ /**
730
+ * Merge multiple partial answers with precedence: cli > interactive > manifest > defaults
731
+ */
732
+ function mergeAnswers(...partials) {
733
+ const merged = {
734
+ language: [],
735
+ database: [],
736
+ cloudTools: [],
737
+ devTools: [],
738
+ observability: [],
739
+ playwright: false,
740
+ outputPath: './.devcontainer',
741
+ };
742
+ // Merge in order (later overrides earlier)
743
+ for (const partial of partials) {
744
+ if (!partial)
745
+ continue;
746
+ Object.keys(partial).forEach((key) => {
747
+ const value = partial[key];
748
+ if (value !== undefined && value !== null) {
749
+ // For arrays, prefer non-empty values
750
+ if (Array.isArray(value)) {
751
+ if (value.length > 0) {
752
+ merged[key] = value;
753
+ }
754
+ }
755
+ else {
756
+ merged[key] = value;
757
+ }
758
+ }
759
+ });
760
+ }
761
+ // Ensure required fields have defaults
762
+ if (!merged.stack)
763
+ merged.stack = 'plain';
764
+ if (!merged.baseImage)
765
+ merged.baseImage = 'bookworm';
766
+ if (!merged.needsDocker && merged.stack) {
767
+ merged.needsDocker = merged.stack === 'compose';
768
+ }
769
+ return merged;
770
+ }
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
+ /**
923
+ * Parse CLI arguments
924
+ */
925
+ async function parseCliArgs() {
926
+ const program = new Command();
927
+ // Store init options for access after parsing
928
+ let initOptions = null;
929
+ program
930
+ .name('container-superposition')
931
+ .description('Composable devcontainer scaffolds')
932
+ .version('0.1.0');
933
+ // Init command (default)
934
+ program
935
+ .command('init', { isDefault: true })
936
+ .description('Initialize a new devcontainer configuration')
937
+ .option('--from-manifest <path>', 'Load configuration from existing superposition.json manifest')
938
+ .option('--no-interactive', 'Use manifest values directly without questionnaire (requires --from-manifest)')
939
+ .option('--no-backup', 'Skip creating backup before regeneration')
940
+ .option('--backup-dir <path>', 'Custom backup directory location')
941
+ .option('--stack <type>', 'Base template: plain, compose')
942
+ .option('--language <list>', 'Comma-separated language overlays: dotnet, nodejs, python, mkdocs, java, go, rust, bun, powershell')
943
+ .option('--database <list>', 'Comma-separated database overlays: postgres, redis, mongodb, mysql, sqlserver, sqlite, minio, rabbitmq, redpanda, nats')
944
+ .option('--observability <list>', 'Comma-separated: otel-collector, jaeger, prometheus, grafana, loki')
945
+ .option('--playwright', 'Include Playwright browser automation')
946
+ .option('--cloud-tools <list>', 'Comma-separated: aws-cli, azure-cli, gcloud, kubectl-helm, terraform, pulumi')
947
+ .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
+ .option('--port-offset <number>', 'Add offset to all exposed ports (e.g., 100 makes Grafana 3100 instead of 3000)')
949
+ .option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
950
+ .action((options) => {
951
+ // Store options for main() to process
952
+ initOptions = options;
953
+ });
954
+ // Regen command
955
+ program
956
+ .command('regen')
957
+ .description('Regenerate devcontainer from existing superposition.json manifest')
958
+ .option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
959
+ .option('--no-backup', 'Skip creating backup before regeneration')
960
+ .option('--backup-dir <path>', 'Custom backup directory location')
961
+ .action((options) => {
962
+ 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'));
967
+ process.exit(1);
968
+ }
969
+ // Store options for main() to process
970
+ initOptions = {
971
+ ...options,
972
+ fromManifest: manifestPath,
973
+ interactive: false,
974
+ };
975
+ });
976
+ // List command
977
+ program
978
+ .command('list')
979
+ .description('List available overlays and presets')
980
+ .option('--category <type>', 'Filter by category: language, database, observability, cloud, dev, preset')
981
+ .action(async (options) => {
982
+ await listOverlays(options);
983
+ });
984
+ // Doctor command
985
+ program
986
+ .command('doctor')
987
+ .description('Check environment and validate configuration')
988
+ .option('-o, --output <path>', 'Devcontainer path to validate (default: ./.devcontainer)')
989
+ .action(async (options) => {
990
+ await runDoctor(options);
991
+ });
992
+ await program.parseAsync(process.argv);
993
+ // If init or regen command was run, return the options
994
+ if (!initOptions) {
995
+ // No init/regen command run (list or doctor ran instead)
996
+ return null;
997
+ }
998
+ // If no options provided to init, return null to trigger interactive mode
999
+ if (Object.keys(initOptions).length === 0) {
1000
+ return null;
1001
+ }
1002
+ const config = {};
1003
+ if (initOptions.stack)
1004
+ config.stack = initOptions.stack;
1005
+ if (initOptions.language) {
1006
+ config.language = initOptions.language
1007
+ .split(',')
1008
+ .map((l) => l.trim());
1009
+ }
1010
+ if (initOptions.database) {
1011
+ config.database = initOptions.database
1012
+ .split(',')
1013
+ .map((d) => d.trim());
1014
+ }
1015
+ if (initOptions.observability) {
1016
+ config.observability = initOptions.observability
1017
+ .split(',')
1018
+ .map((t) => t.trim());
1019
+ }
1020
+ if (initOptions.playwright)
1021
+ config.playwright = true;
1022
+ if (initOptions.cloudTools) {
1023
+ config.cloudTools = initOptions.cloudTools
1024
+ .split(',')
1025
+ .map((t) => t.trim());
1026
+ }
1027
+ if (initOptions.devTools) {
1028
+ config.devTools = initOptions.devTools.split(',').map((t) => t.trim());
1029
+ }
1030
+ if (initOptions.portOffset) {
1031
+ config.portOffset = parseInt(initOptions.portOffset, 10);
1032
+ }
1033
+ if (initOptions.output)
1034
+ config.outputPath = initOptions.output;
1035
+ return {
1036
+ config,
1037
+ manifestPath: initOptions.fromManifest,
1038
+ noBackup: initOptions.backup === false, // Commander creates options.backup = false for --no-backup
1039
+ backupDir: initOptions.backupDir,
1040
+ noInteractive: initOptions.interactive === false, // Commander creates options.interactive = false for --no-interactive
1041
+ };
1042
+ }
1043
+ async function main() {
1044
+ try {
1045
+ const cliArgs = await parseCliArgs();
1046
+ // Validate --no-interactive requires --from-manifest
1047
+ if (cliArgs?.noInteractive && !cliArgs?.manifestPath) {
1048
+ console.error(chalk.red('✗ Error: --no-interactive requires --from-manifest'));
1049
+ console.error(chalk.dim(' Use both flags together: --from-manifest <path> --no-interactive'));
1050
+ process.exit(1);
1051
+ }
1052
+ let manifest;
1053
+ let manifestDir;
1054
+ let shouldBackup = true;
1055
+ let backupDir;
1056
+ let useManifestOnly = false;
1057
+ // Handle manifest loading
1058
+ if (cliArgs?.manifestPath) {
1059
+ const manifestPath = findManifestFile(cliArgs.manifestPath);
1060
+ if (!manifestPath) {
1061
+ console.error(chalk.red('✗ Could not find manifest file'));
1062
+ console.error(chalk.red(` Searched for: ${cliArgs.manifestPath}`));
1063
+ process.exit(1);
1064
+ }
1065
+ manifestDir = path.dirname(manifestPath);
1066
+ const loadedManifest = loadManifest(manifestPath);
1067
+ if (!loadedManifest) {
1068
+ process.exit(1);
1069
+ }
1070
+ manifest = loadedManifest;
1071
+ // Check for backup and interaction options
1072
+ if (cliArgs.noBackup) {
1073
+ shouldBackup = false;
1074
+ }
1075
+ if (cliArgs.backupDir) {
1076
+ backupDir = cliArgs.backupDir;
1077
+ }
1078
+ if (cliArgs.noInteractive) {
1079
+ useManifestOnly = true;
1080
+ }
1081
+ }
1082
+ // Create backup if needed
1083
+ if (shouldBackup && manifest) {
1084
+ // Output path is the directory containing the manifest
1085
+ const outputPath = manifestDir || './.devcontainer';
1086
+ const backupPath = await createBackup(outputPath, backupDir);
1087
+ if (backupPath) {
1088
+ console.log(chalk.green(`✓ Backup created: ${backupPath}\n`));
1089
+ await ensureBackupPatternsInGitignore(outputPath);
1090
+ }
1091
+ }
1092
+ // Build answers based on mode
1093
+ let answers;
1094
+ if (useManifestOnly && manifest) {
1095
+ // Mode 1: Manifest-only (--from-manifest --no-interactive)
1096
+ const manifestAnswers = buildAnswersFromManifest(manifest, manifestDir);
1097
+ answers = mergeAnswers(manifestAnswers);
1098
+ console.log('\n' +
1099
+ boxen(chalk.bold.cyan('Regenerating from Manifest (No Interactive)\n\n') +
1100
+ chalk.white('Configuration:\n') +
1101
+ chalk.gray(` Template: ${manifest.baseTemplate}\n`) +
1102
+ chalk.gray(` Base Image: ${manifest.baseImage}\n`) +
1103
+ (manifest.containerName
1104
+ ? chalk.gray(` Container: ${manifest.containerName}\n`)
1105
+ : '') +
1106
+ chalk.gray(` Overlays: ${manifest.overlays.join(', ')}\n`) +
1107
+ (manifest.preset ? chalk.gray(` Preset: ${manifest.preset}\n`) : '') +
1108
+ (manifest.portOffset
1109
+ ? chalk.gray(` Port offset: ${manifest.portOffset}\n`)
1110
+ : '') +
1111
+ chalk.gray(` Output: ${answers.outputPath}`), { padding: 1, borderColor: 'cyan', borderStyle: 'round', margin: 1 }));
1112
+ }
1113
+ else if (cliArgs && cliArgs.config.stack) {
1114
+ // Mode 2: CLI-based (with optional manifest defaults)
1115
+ const cliAnswers = buildAnswersFromCliArgs(cliArgs.config);
1116
+ const manifestAnswers = manifest
1117
+ ? buildAnswersFromManifest(manifest, manifestDir)
1118
+ : undefined;
1119
+ answers = mergeAnswers(manifestAnswers, cliAnswers, {
1120
+ outputPath: cliAnswers.outputPath || './.devcontainer',
1121
+ });
1122
+ console.log('\n' +
1123
+ boxen(chalk.bold('Running in CLI mode'), {
1124
+ padding: 0.5,
1125
+ borderColor: 'blue',
1126
+ borderStyle: 'round',
1127
+ }));
1128
+ }
1129
+ else {
1130
+ // Mode 3: Interactive (with optional manifest pre-population)
1131
+ const interactiveAnswers = await runQuestionnaire(manifest, manifestDir);
1132
+ answers = mergeAnswers(interactiveAnswers);
1133
+ }
1134
+ // Show configuration summary
1135
+ const summaryLines = [
1136
+ chalk.bold.white('Configuration Summary\n'),
1137
+ chalk.cyan('Base: ') + chalk.white(answers.stack),
1138
+ ];
1139
+ if (answers.language && answers.language.length > 0) {
1140
+ summaryLines.push(chalk.cyan('Languages: ') + chalk.white(answers.language.join(', ')));
1141
+ }
1142
+ if (answers.database && answers.database.length > 0) {
1143
+ summaryLines.push(chalk.cyan('Database: ') + chalk.white(answers.database.join(', ')));
1144
+ }
1145
+ summaryLines.push(chalk.cyan('Playwright: ') + chalk.white(answers.playwright ? 'Yes' : 'No'));
1146
+ if (answers.observability && answers.observability.length > 0) {
1147
+ summaryLines.push(chalk.cyan('Observability: ') + chalk.white(answers.observability.join(', ')));
1148
+ }
1149
+ if (answers.cloudTools && answers.cloudTools.length > 0) {
1150
+ summaryLines.push(chalk.cyan('Cloud tools: ') + chalk.white(answers.cloudTools.join(', ')));
1151
+ }
1152
+ summaryLines.push(chalk.cyan('Output: ') + chalk.white(answers.outputPath));
1153
+ console.log('\n' +
1154
+ boxen(summaryLines.join('\n'), {
1155
+ padding: 1,
1156
+ borderColor: 'green',
1157
+ borderStyle: 'round',
1158
+ margin: { top: 0, bottom: 1 },
1159
+ }));
1160
+ // Generate with spinner
1161
+ const spinner = ora({
1162
+ text: chalk.cyan('Generating devcontainer configuration...'),
1163
+ color: 'cyan',
1164
+ }).start();
1165
+ try {
1166
+ await composeDevContainer(answers);
1167
+ spinner.succeed(chalk.green('DevContainer created successfully!'));
1168
+ }
1169
+ catch (error) {
1170
+ spinner.fail(chalk.red('Failed to create devcontainer'));
1171
+ throw error;
1172
+ }
1173
+ // Success message
1174
+ console.log('\n' +
1175
+ boxen(chalk.bold.green('✓ Setup Complete!\n\n') +
1176
+ chalk.white('Next steps:\n') +
1177
+ chalk.gray(' 1. Review the generated .devcontainer/ folder\n') +
1178
+ chalk.gray(" 2. Customize as needed (it's just normal JSON!)\n") +
1179
+ 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 }));
1181
+ }
1182
+ catch (error) {
1183
+ console.error('\n' +
1184
+ boxen(chalk.bold.red('Error\n\n') +
1185
+ chalk.white(error instanceof Error ? error.message : String(error)), { padding: 1, borderColor: 'red', borderStyle: 'round' }));
1186
+ process.exit(1);
1187
+ }
1188
+ }
1189
+ main();
1190
+ //# sourceMappingURL=init.js.map