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,1232 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import chalk from 'chalk';
5
+ import * as yaml from 'js-yaml';
6
+ import { loadOverlaysConfig } from '../schema/overlay-loader.js';
7
+ import { loadCustomPatches, hasCustomDirectory, getCustomScriptPaths, } from '../schema/custom-loader.js';
8
+ import { generateReadme } from '../readme/readme-generator.js';
9
+ // Get __dirname equivalent in ESM
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ // Resolve REPO_ROOT that works in both source and compiled output
13
+ // When running from TypeScript sources (e.g. ts-node), __dirname is "<root>/tool/questionnaire"
14
+ // When running from compiled JS in "dist/tool/questionnaire", __dirname is "<root>/dist/tool/questionnaire"
15
+ const REPO_ROOT_CANDIDATES = [
16
+ path.join(__dirname, '..', '..'), // From source: tool/questionnaire -> root
17
+ path.join(__dirname, '..', '..', '..'), // From dist: dist/tool/questionnaire -> root
18
+ ];
19
+ const REPO_ROOT = REPO_ROOT_CANDIDATES.find((candidate) => fs.existsSync(path.join(candidate, 'templates')) &&
20
+ fs.existsSync(path.join(candidate, 'overlays'))) ?? REPO_ROOT_CANDIDATES[0];
21
+ const TEMPLATES_DIR = path.join(REPO_ROOT, 'templates');
22
+ const OVERLAYS_DIR = path.join(REPO_ROOT, 'overlays');
23
+ /**
24
+ * Deep merge two objects, with special handling for arrays
25
+ */
26
+ function deepMerge(target, source) {
27
+ const output = { ...target };
28
+ for (const key in source) {
29
+ if (source[key] instanceof Object && key in target) {
30
+ if (Array.isArray(source[key])) {
31
+ // For arrays, concatenate and deduplicate
32
+ output[key] = Array.isArray(target[key])
33
+ ? [...new Set([...target[key], ...source[key]])]
34
+ : source[key];
35
+ }
36
+ else if (key === 'remoteEnv') {
37
+ // Special handling for remoteEnv to merge PATH variables intelligently
38
+ output[key] = mergeRemoteEnv(target[key], source[key]);
39
+ }
40
+ else {
41
+ output[key] = deepMerge(target[key], source[key]);
42
+ }
43
+ }
44
+ else {
45
+ output[key] = source[key];
46
+ }
47
+ }
48
+ return output;
49
+ }
50
+ /**
51
+ * Split PATH string on colons, but preserve ${...} variable references
52
+ * e.g., "${containerEnv:HOME}/bin:${containerEnv:PATH}" -> ["${containerEnv:HOME}/bin", "${containerEnv:PATH}"]
53
+ */
54
+ function splitPath(pathString) {
55
+ const paths = [];
56
+ let current = '';
57
+ let braceDepth = 0;
58
+ for (let i = 0; i < pathString.length; i++) {
59
+ const char = pathString[i];
60
+ const nextChar = pathString[i + 1];
61
+ if (char === '$' && nextChar === '{') {
62
+ current += char;
63
+ braceDepth++;
64
+ }
65
+ else if (char === '}' && braceDepth > 0) {
66
+ current += char;
67
+ braceDepth--;
68
+ }
69
+ else if (char === ':' && braceDepth === 0) {
70
+ // Split here - we're not inside ${...}
71
+ if (current) {
72
+ paths.push(current);
73
+ }
74
+ current = '';
75
+ }
76
+ else {
77
+ current += char;
78
+ }
79
+ }
80
+ // Add the last component
81
+ if (current) {
82
+ paths.push(current);
83
+ }
84
+ return paths;
85
+ }
86
+ /**
87
+ * Merge remoteEnv objects, with special handling for PATH variables
88
+ */
89
+ function mergeRemoteEnv(target, source) {
90
+ const output = { ...target };
91
+ for (const key in source) {
92
+ if (key === 'PATH' && target[key]) {
93
+ // Collect PATH components from both target and source using smart split
94
+ const targetPaths = splitPath(target[key]).filter((p) => p && p !== '${containerEnv:PATH}');
95
+ const sourcePaths = splitPath(source[key]).filter((p) => p && p !== '${containerEnv:PATH}');
96
+ // Combine and deduplicate paths, preserving order
97
+ const allPaths = [...new Set([...targetPaths, ...sourcePaths])];
98
+ // Rebuild PATH with original ${containerEnv:PATH} at the end
99
+ output[key] = [...allPaths, '${containerEnv:PATH}'].join(':');
100
+ }
101
+ else {
102
+ // For non-PATH variables, source overwrites target
103
+ output[key] = source[key];
104
+ }
105
+ }
106
+ return output;
107
+ }
108
+ /**
109
+ * Merge packages from apt-get-packages feature
110
+ */
111
+ function mergeAptPackages(baseConfig, packages) {
112
+ const featureKey = 'ghcr.io/devcontainers-extra/features/apt-get-packages:1';
113
+ if (!baseConfig.features) {
114
+ baseConfig.features = {};
115
+ }
116
+ if (!baseConfig.features[featureKey]) {
117
+ baseConfig.features[featureKey] = { packages };
118
+ }
119
+ else {
120
+ const existing = baseConfig.features[featureKey].packages || '';
121
+ // Filter out empty tokens from split to avoid leading spaces
122
+ const existingPackages = existing.split(' ').filter((p) => p);
123
+ const newPackages = packages.split(' ').filter((p) => p);
124
+ const merged = [...new Set([...existingPackages, ...newPackages])].join(' ');
125
+ baseConfig.features[featureKey].packages = merged;
126
+ }
127
+ return baseConfig;
128
+ }
129
+ /**
130
+ * Merge packages from cross-distro-packages feature
131
+ */
132
+ function mergeCrossDistroPackages(baseConfig, apt, apk) {
133
+ const featureKey = './features/cross-distro-packages';
134
+ if (!baseConfig.features) {
135
+ baseConfig.features = {};
136
+ }
137
+ if (!baseConfig.features[featureKey]) {
138
+ baseConfig.features[featureKey] = {};
139
+ }
140
+ // Merge apt packages
141
+ if (apt) {
142
+ const existing = baseConfig.features[featureKey].apt || '';
143
+ const existingPackages = existing.split(' ').filter((p) => p);
144
+ const newPackages = apt.split(' ').filter((p) => p);
145
+ const merged = [...new Set([...existingPackages, ...newPackages])].join(' ');
146
+ baseConfig.features[featureKey].apt = merged;
147
+ }
148
+ // Merge apk packages
149
+ if (apk) {
150
+ const existing = baseConfig.features[featureKey].apk || '';
151
+ const existingPackages = existing.split(' ').filter((p) => p);
152
+ const newPackages = apk.split(' ').filter((p) => p);
153
+ const merged = [...new Set([...existingPackages, ...newPackages])].join(' ');
154
+ baseConfig.features[featureKey].apk = merged;
155
+ }
156
+ return baseConfig;
157
+ }
158
+ /**
159
+ * Load and parse a JSON file
160
+ */
161
+ function loadJson(filePath) {
162
+ const content = fs.readFileSync(filePath, 'utf-8');
163
+ return JSON.parse(content);
164
+ }
165
+ /**
166
+ * Get all overlay definitions as a flat array
167
+ */
168
+ function getAllOverlayDefs(config) {
169
+ return config.overlays;
170
+ }
171
+ /**
172
+ * Resolve dependencies for a set of overlays
173
+ * Returns the expanded list with dependencies and metadata about what was added
174
+ */
175
+ function resolveDependencies(requestedOverlays, allOverlayDefs) {
176
+ const overlayMap = new Map();
177
+ allOverlayDefs.forEach((def) => overlayMap.set(def.id, def));
178
+ const resolved = new Set(requestedOverlays);
179
+ const autoAdded = [];
180
+ const resolutionReasons = [];
181
+ // Resolve dependencies recursively
182
+ const toProcess = [...requestedOverlays];
183
+ const processed = new Set();
184
+ while (toProcess.length > 0) {
185
+ const current = toProcess.shift();
186
+ if (processed.has(current))
187
+ continue;
188
+ processed.add(current);
189
+ const overlayDef = overlayMap.get(current);
190
+ if (!overlayDef || !overlayDef.requires || overlayDef.requires.length === 0) {
191
+ continue;
192
+ }
193
+ // Add required dependencies
194
+ for (const required of overlayDef.requires) {
195
+ if (!resolved.has(required)) {
196
+ resolved.add(required);
197
+ autoAdded.push(required);
198
+ resolutionReasons.push(`${required} (required by ${current})`);
199
+ toProcess.push(required);
200
+ }
201
+ }
202
+ }
203
+ // Check for conflicts
204
+ const conflicts = [];
205
+ for (const overlayId of resolved) {
206
+ const overlayDef = overlayMap.get(overlayId);
207
+ if (!overlayDef || !overlayDef.conflicts)
208
+ continue;
209
+ for (const conflict of overlayDef.conflicts) {
210
+ if (resolved.has(conflict)) {
211
+ conflicts.push(`${overlayId} conflicts with ${conflict}`);
212
+ }
213
+ }
214
+ }
215
+ if (conflicts.length > 0) {
216
+ console.log(chalk.yellow(`\n⚠️ Warning: Conflicts detected:`));
217
+ conflicts.forEach((c) => console.log(chalk.yellow(` • ${c}`)));
218
+ console.log(chalk.yellow(`\nPlease resolve these conflicts manually.\n`));
219
+ }
220
+ const reason = autoAdded.length > 0 ? resolutionReasons.join(', ') : '';
221
+ return {
222
+ overlays: Array.from(resolved),
223
+ autoResolved: {
224
+ added: autoAdded,
225
+ reason,
226
+ },
227
+ };
228
+ }
229
+ /**
230
+ * Generate superposition.json manifest
231
+ */
232
+ function generateManifest(outputPath, answers, overlays, autoResolved, containerName) {
233
+ const manifest = {
234
+ version: '0.1.0',
235
+ generated: new Date().toISOString(),
236
+ baseTemplate: answers.stack,
237
+ baseImage: answers.baseImage === 'custom' && answers.customImage
238
+ ? answers.customImage
239
+ : answers.baseImage,
240
+ overlays,
241
+ portOffset: answers.portOffset,
242
+ preset: answers.preset,
243
+ presetChoices: answers.presetChoices,
244
+ containerName,
245
+ };
246
+ if (autoResolved.added.length > 0) {
247
+ manifest.autoResolved = autoResolved;
248
+ }
249
+ // Track customizations if custom directory exists
250
+ if (hasCustomDirectory(outputPath)) {
251
+ // Compute the custom directory location relative to workspace root
252
+ const outputDirName = path.basename(outputPath);
253
+ manifest.customizations = {
254
+ enabled: true,
255
+ location: `${outputDirName}/custom`,
256
+ };
257
+ }
258
+ const manifestPath = path.join(outputPath, 'superposition.json');
259
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
260
+ console.log(chalk.dim(` 📋 Generated superposition.json manifest`));
261
+ if (autoResolved.added.length > 0) {
262
+ console.log(chalk.cyan(` ℹ️ Auto-resolved dependencies: ${autoResolved.added.join(', ')}`));
263
+ }
264
+ if (answers.preset) {
265
+ console.log(chalk.cyan(` ℹ️ Used preset: ${answers.preset}`));
266
+ }
267
+ }
268
+ /**
269
+ * Apply an overlay to the base configuration
270
+ */
271
+ function applyOverlay(baseConfig, overlayName) {
272
+ const overlayPath = path.join(OVERLAYS_DIR, overlayName, 'devcontainer.patch.json');
273
+ if (!fs.existsSync(overlayPath)) {
274
+ console.warn(chalk.yellow(`⚠️ Overlay not found: ${overlayName}`));
275
+ return baseConfig;
276
+ }
277
+ const overlay = loadJson(overlayPath);
278
+ // Special handling for apt-get packages (legacy)
279
+ if (overlay.features?.['ghcr.io/devcontainers-extra/features/apt-get-packages:1']?.packages) {
280
+ const packages = overlay.features['ghcr.io/devcontainers-extra/features/apt-get-packages:1'].packages;
281
+ baseConfig = mergeAptPackages(baseConfig, packages);
282
+ // Remove it from overlay to avoid double-merge
283
+ delete overlay.features['ghcr.io/devcontainers-extra/features/apt-get-packages:1'];
284
+ }
285
+ // Special handling for cross-distro packages
286
+ if (overlay.features?.['./features/cross-distro-packages']) {
287
+ const aptPackages = overlay.features['./features/cross-distro-packages'].apt;
288
+ const apkPackages = overlay.features['./features/cross-distro-packages'].apk;
289
+ baseConfig = mergeCrossDistroPackages(baseConfig, aptPackages, apkPackages);
290
+ // Remove it from overlay to avoid double-merge
291
+ delete overlay.features['./features/cross-distro-packages'];
292
+ }
293
+ return deepMerge(baseConfig, overlay);
294
+ }
295
+ /**
296
+ * Registry to track all files that should exist in the output directory
297
+ */
298
+ class FileRegistry {
299
+ files = new Set();
300
+ directories = new Set();
301
+ addFile(relativePath) {
302
+ this.files.add(relativePath);
303
+ }
304
+ addDirectory(relativePath) {
305
+ this.directories.add(relativePath);
306
+ }
307
+ getFiles() {
308
+ return this.files;
309
+ }
310
+ getDirectories() {
311
+ return this.directories;
312
+ }
313
+ }
314
+ /**
315
+ * Clean up stale files from previous runs
316
+ * Removes anything not in the registry (except preserved files like superposition.json)
317
+ */
318
+ function cleanupStaleFiles(outputPath, registry) {
319
+ if (!fs.existsSync(outputPath)) {
320
+ return;
321
+ }
322
+ const preservedFiles = new Set(['superposition.json', '.env']); // User-managed files
323
+ const preservedDirs = new Set(['custom']); // User customizations directory
324
+ const expectedFiles = registry.getFiles();
325
+ const expectedDirs = registry.getDirectories();
326
+ const entries = fs.readdirSync(outputPath);
327
+ let removedCount = 0;
328
+ for (const entry of entries) {
329
+ // Skip preserved files
330
+ if (preservedFiles.has(entry)) {
331
+ continue;
332
+ }
333
+ const entryPath = path.join(outputPath, entry);
334
+ const stat = fs.statSync(entryPath);
335
+ if (stat.isDirectory()) {
336
+ // Skip preserved directories
337
+ if (preservedDirs.has(entry)) {
338
+ continue;
339
+ }
340
+ // Remove directory if not in registry
341
+ if (!expectedDirs.has(entry)) {
342
+ fs.rmSync(entryPath, { recursive: true, force: true });
343
+ removedCount++;
344
+ }
345
+ }
346
+ else {
347
+ // Remove file if not in registry
348
+ if (!expectedFiles.has(entry)) {
349
+ fs.unlinkSync(entryPath);
350
+ removedCount++;
351
+ }
352
+ }
353
+ }
354
+ if (removedCount > 0) {
355
+ console.log(chalk.dim(` 🧹 Removed ${removedCount} stale file(s) from previous runs`));
356
+ }
357
+ }
358
+ /**
359
+ * Copy a directory recursively
360
+ */
361
+ function copyDir(src, dest) {
362
+ if (!fs.existsSync(dest)) {
363
+ fs.mkdirSync(dest, { recursive: true });
364
+ }
365
+ const entries = fs.readdirSync(src, { withFileTypes: true });
366
+ for (const entry of entries) {
367
+ const srcPath = path.join(src, entry.name);
368
+ const destPath = path.join(dest, entry.name);
369
+ if (entry.isDirectory()) {
370
+ copyDir(srcPath, destPath);
371
+ }
372
+ else {
373
+ fs.copyFileSync(srcPath, destPath);
374
+ }
375
+ }
376
+ }
377
+ /**
378
+ * Copy additional files from overlay to output directory
379
+ * Excludes devcontainer.patch.json and .env.example (handled separately)
380
+ */
381
+ function copyOverlayFiles(outputPath, overlayName, registry) {
382
+ const overlayPath = path.join(OVERLAYS_DIR, overlayName);
383
+ if (!fs.existsSync(overlayPath)) {
384
+ return;
385
+ }
386
+ const entries = fs.readdirSync(overlayPath);
387
+ let copiedFiles = 0;
388
+ for (const entry of entries) {
389
+ // Skip devcontainer.patch.json, .env.example, docker-compose.yml, setup.sh, verify.sh, and metadata files (handled separately)
390
+ if (entry === 'devcontainer.patch.json' ||
391
+ entry === '.env.example' ||
392
+ entry === 'docker-compose.yml' ||
393
+ entry === 'setup.sh' ||
394
+ entry === 'verify.sh' ||
395
+ entry === 'README.md' ||
396
+ entry === 'overlay.yml') {
397
+ continue;
398
+ }
399
+ const srcPath = path.join(overlayPath, entry);
400
+ const stat = fs.statSync(srcPath);
401
+ if (stat.isFile()) {
402
+ // Copy config files with overlay prefix to avoid conflicts
403
+ // e.g., global-tools.txt -> global-tools-dotnet.txt
404
+ const basename = path.basename(entry, path.extname(entry));
405
+ const ext = path.extname(entry);
406
+ const destFilename = `${basename}-${overlayName}${ext}`;
407
+ const destPath = path.join(outputPath, destFilename);
408
+ fs.copyFileSync(srcPath, destPath);
409
+ registry.addFile(destFilename);
410
+ copiedFiles++;
411
+ }
412
+ else if (stat.isDirectory()) {
413
+ // Copy directories recursively with overlay prefix
414
+ const destDirName = `${entry}-${overlayName}`;
415
+ const destPath = path.join(outputPath, destDirName);
416
+ copyDir(srcPath, destPath);
417
+ registry.addDirectory(destDirName);
418
+ copiedFiles++;
419
+ }
420
+ }
421
+ if (copiedFiles > 0) {
422
+ console.log(chalk.dim(` 📋 Copied ${copiedFiles} file(s) from ${chalk.cyan(overlayName)}`));
423
+ }
424
+ }
425
+ /**
426
+ * Merge .env.example files from all selected overlays
427
+ */
428
+ /**
429
+ * Merge .env.example files from overlays and apply glue config
430
+ */
431
+ function mergeEnvExamples(outputPath, overlays, portOffset, glueConfig, presetName) {
432
+ const envSections = [];
433
+ for (const overlay of overlays) {
434
+ const envPath = path.join(OVERLAYS_DIR, overlay, '.env.example');
435
+ if (fs.existsSync(envPath)) {
436
+ const content = fs.readFileSync(envPath, 'utf-8').trim();
437
+ if (content) {
438
+ envSections.push(content);
439
+ }
440
+ }
441
+ }
442
+ // Add preset glue environment variables if present
443
+ if (glueConfig?.environment && Object.keys(glueConfig.environment).length > 0) {
444
+ let presetEnvSection = `# Preset: ${presetName || 'custom'}\n# Pre-configured environment variables from preset\n\n`;
445
+ for (const [key, value] of Object.entries(glueConfig.environment)) {
446
+ presetEnvSection += `${key}=${value}\n`;
447
+ }
448
+ envSections.push(presetEnvSection.trim());
449
+ }
450
+ if (envSections.length === 0) {
451
+ return false;
452
+ }
453
+ // Create combined .env.example
454
+ let header = `# Environment Variables
455
+ #
456
+ # Copy this file to .env in your project root to customize
457
+ # docker-compose and other service configurations.
458
+ #
459
+ # Generated by container-superposition init tool
460
+ `;
461
+ if (portOffset) {
462
+ header += `#
463
+ # NOTE: A port offset of ${portOffset} was applied to avoid conflicts.
464
+ # All service ports have been shifted by ${portOffset} (e.g., Grafana: ${3000 + portOffset} instead of 3000).
465
+ `;
466
+ }
467
+ header += '\n';
468
+ const combined = header + envSections.join('\n\n');
469
+ const envOutputPath = path.join(outputPath, '.env.example');
470
+ fs.writeFileSync(envOutputPath, combined + '\n');
471
+ console.log(chalk.dim(` 🔐 Created .env.example with ${overlays.length} overlay(s)`));
472
+ // If port offset is specified, create a .env file with offset values
473
+ if (portOffset) {
474
+ const envContent = applyPortOffsetToEnv(combined, portOffset);
475
+ const envFilePath = path.join(outputPath, '.env');
476
+ fs.writeFileSync(envFilePath, envContent);
477
+ console.log(chalk.dim(` 🔧 Created .env with port offset of ${portOffset}`));
478
+ }
479
+ return true;
480
+ }
481
+ /**
482
+ * Apply port offset to environment variables in .env content
483
+ */
484
+ function applyPortOffsetToEnv(envContent, offset) {
485
+ const lines = envContent.split('\n');
486
+ const portVarPattern = /^([A-Z_]*PORT[A-Z_]*)=(\d+)$/;
487
+ const modifiedLines = lines.map((line) => {
488
+ const match = line.match(portVarPattern);
489
+ if (match) {
490
+ const [, varName, portValue] = match;
491
+ const newPort = parseInt(portValue, 10) + offset;
492
+ return `${varName}=${newPort}`;
493
+ }
494
+ return line;
495
+ });
496
+ return modifiedLines.join('\n');
497
+ }
498
+ /**
499
+ * Apply preset glue configuration (README and port mappings)
500
+ * Note: Environment variables are handled in mergeEnvExamples to ensure proper port offset application
501
+ */
502
+ function applyGlueConfig(outputPath, glueConfig, presetName, fileRegistry) {
503
+ console.log(chalk.cyan(`\n📦 Applying preset glue configuration...\n`));
504
+ // 1. Create preset README if provided
505
+ if (glueConfig.readme) {
506
+ const readmePath = path.join(outputPath, 'PRESET-README.md');
507
+ fs.writeFileSync(readmePath, glueConfig.readme);
508
+ if (fileRegistry) {
509
+ fileRegistry.addFile('PRESET-README.md');
510
+ }
511
+ console.log(chalk.dim(` ✓ Created PRESET-README.md with usage instructions`));
512
+ }
513
+ // 2. Log port mappings (informational only - actual ports handled by overlay configs)
514
+ if (glueConfig.portMappings && Object.keys(glueConfig.portMappings).length > 0) {
515
+ console.log(chalk.dim(` ℹ️ Suggested port mappings:`));
516
+ for (const [service, port] of Object.entries(glueConfig.portMappings)) {
517
+ console.log(chalk.dim(` ${service}: ${port}`));
518
+ }
519
+ }
520
+ // 3. Log environment variables if present
521
+ if (glueConfig.environment && Object.keys(glueConfig.environment).length > 0) {
522
+ console.log(chalk.dim(` ✓ Added ${Object.keys(glueConfig.environment).length} environment variables to .env.example`));
523
+ }
524
+ console.log('');
525
+ }
526
+ /**
527
+ * Merge docker-compose.yml files from base and overlays into a single file
528
+ */
529
+ function mergeDockerComposeFiles(outputPath, baseStack, overlays, portOffset, customImage) {
530
+ const composeFiles = [];
531
+ // Add base docker-compose if exists
532
+ const baseComposePath = path.join(TEMPLATES_DIR, baseStack, '.devcontainer', 'docker-compose.yml');
533
+ if (fs.existsSync(baseComposePath)) {
534
+ composeFiles.push(baseComposePath);
535
+ }
536
+ // Add overlay docker-compose files
537
+ for (const overlay of overlays) {
538
+ const overlayComposePath = path.join(OVERLAYS_DIR, overlay, 'docker-compose.yml');
539
+ if (fs.existsSync(overlayComposePath)) {
540
+ composeFiles.push(overlayComposePath);
541
+ }
542
+ }
543
+ if (composeFiles.length === 0) {
544
+ return; // No docker-compose files to merge
545
+ }
546
+ // Merge all compose files
547
+ let merged = {
548
+ services: {},
549
+ volumes: {},
550
+ networks: {},
551
+ };
552
+ for (const composePath of composeFiles) {
553
+ const content = fs.readFileSync(composePath, 'utf-8');
554
+ const compose = yaml.load(content);
555
+ if (compose.services) {
556
+ // Deep merge services to preserve arrays like volumes, ports, etc.
557
+ for (const serviceName in compose.services) {
558
+ if (merged.services[serviceName]) {
559
+ merged.services[serviceName] = deepMerge(merged.services[serviceName], compose.services[serviceName]);
560
+ }
561
+ else {
562
+ merged.services[serviceName] = compose.services[serviceName];
563
+ }
564
+ }
565
+ }
566
+ if (compose.volumes) {
567
+ merged.volumes = { ...merged.volumes, ...compose.volumes };
568
+ }
569
+ if (compose.networks) {
570
+ merged.networks = { ...merged.networks, ...compose.networks };
571
+ }
572
+ }
573
+ // Ensure devcontainer service has an image
574
+ if (merged.services.devcontainer) {
575
+ if (customImage) {
576
+ // Apply custom base image if specified
577
+ merged.services.devcontainer.image = customImage;
578
+ }
579
+ else if (!merged.services.devcontainer.image) {
580
+ // Fallback to default if no image is set (shouldn't happen in normal flow)
581
+ console.warn(chalk.yellow('⚠️ No image specified, this should not happen'));
582
+ }
583
+ }
584
+ // Filter depends_on to only include services that exist
585
+ const serviceNames = Object.keys(merged.services);
586
+ for (const serviceName of serviceNames) {
587
+ const service = merged.services[serviceName];
588
+ if (service.depends_on && Array.isArray(service.depends_on)) {
589
+ service.depends_on = service.depends_on.filter((dep) => serviceNames.includes(dep));
590
+ if (service.depends_on.length === 0) {
591
+ delete service.depends_on;
592
+ }
593
+ }
594
+ }
595
+ // Remove empty sections
596
+ if (Object.keys(merged.volumes).length === 0)
597
+ delete merged.volumes;
598
+ if (Object.keys(merged.networks).length === 0)
599
+ delete merged.networks;
600
+ // Write combined docker-compose.yml
601
+ const outputComposePath = path.join(outputPath, 'docker-compose.yml');
602
+ const yamlContent = yaml.dump(merged, {
603
+ indent: 2,
604
+ lineWidth: -1, // No line wrapping
605
+ noRefs: true,
606
+ });
607
+ fs.writeFileSync(outputComposePath, yamlContent);
608
+ console.log(chalk.dim(` 🐳 Created combined docker-compose.yml with ${serviceNames.length} service(s)`));
609
+ }
610
+ /**
611
+ * Apply custom devcontainer patch from .devcontainer/custom/
612
+ */
613
+ function applyCustomDevcontainerPatch(config, customConfig) {
614
+ if (!customConfig.devcontainerPatch) {
615
+ return config;
616
+ }
617
+ console.log(chalk.dim(` 🎨 Applying custom devcontainer patches`));
618
+ return deepMerge(config, customConfig.devcontainerPatch);
619
+ }
620
+ /**
621
+ * Apply custom docker-compose patch to merged docker-compose
622
+ */
623
+ function applyCustomDockerComposePatch(outputPath, customConfig) {
624
+ if (!customConfig.dockerComposePatch) {
625
+ return;
626
+ }
627
+ const composePath = path.join(outputPath, 'docker-compose.yml');
628
+ if (!fs.existsSync(composePath)) {
629
+ console.warn(chalk.yellow('⚠️ docker-compose.yml not found, skipping custom docker-compose patch'));
630
+ return;
631
+ }
632
+ console.log(chalk.dim(` 🐳 Applying custom docker-compose patches`));
633
+ // Load existing compose file
634
+ const existingContent = fs.readFileSync(composePath, 'utf-8');
635
+ const existing = yaml.load(existingContent);
636
+ // Merge with custom patch
637
+ const merged = {
638
+ services: { ...existing.services },
639
+ volumes: { ...existing.volumes },
640
+ networks: { ...existing.networks },
641
+ };
642
+ const custom = customConfig.dockerComposePatch;
643
+ // Merge services
644
+ if (custom.services) {
645
+ for (const serviceName in custom.services) {
646
+ if (merged.services[serviceName]) {
647
+ merged.services[serviceName] = deepMerge(merged.services[serviceName], custom.services[serviceName]);
648
+ }
649
+ else {
650
+ merged.services[serviceName] = custom.services[serviceName];
651
+ }
652
+ }
653
+ }
654
+ // Merge volumes
655
+ if (custom.volumes) {
656
+ merged.volumes = { ...merged.volumes, ...custom.volumes };
657
+ }
658
+ // Merge networks
659
+ if (custom.networks) {
660
+ merged.networks = { ...merged.networks, ...custom.networks };
661
+ }
662
+ // Remove empty sections
663
+ if (Object.keys(merged.volumes).length === 0)
664
+ delete merged.volumes;
665
+ if (Object.keys(merged.networks).length === 0)
666
+ delete merged.networks;
667
+ // Write updated compose file
668
+ const yamlContent = yaml.dump(merged, {
669
+ indent: 2,
670
+ lineWidth: -1,
671
+ noRefs: true,
672
+ });
673
+ fs.writeFileSync(composePath, yamlContent);
674
+ }
675
+ /**
676
+ * Apply custom environment variables
677
+ * Returns true if .env.example was created or modified
678
+ */
679
+ function applyCustomEnvironment(outputPath, customConfig) {
680
+ if (!customConfig.environmentVars || Object.keys(customConfig.environmentVars).length === 0) {
681
+ return false;
682
+ }
683
+ console.log(chalk.dim(` 🔑 Applying custom environment variables`));
684
+ const envExamplePath = path.join(outputPath, '.env.example');
685
+ let content = '';
686
+ // Load existing .env.example if it exists
687
+ if (fs.existsSync(envExamplePath)) {
688
+ content = fs.readFileSync(envExamplePath, 'utf-8');
689
+ if (!content.endsWith('\n')) {
690
+ content += '\n';
691
+ }
692
+ content += '\n';
693
+ }
694
+ // Add custom environment section
695
+ content += '# Custom Environment Variables\n';
696
+ for (const [key, value] of Object.entries(customConfig.environmentVars)) {
697
+ content += `${key}=${value}\n`;
698
+ }
699
+ fs.writeFileSync(envExamplePath, content);
700
+ return true;
701
+ }
702
+ /**
703
+ * Apply custom lifecycle scripts
704
+ */
705
+ function applyCustomScripts(config, customConfig, outputPath) {
706
+ if (!customConfig.scripts) {
707
+ return config;
708
+ }
709
+ // Make custom scripts executable
710
+ const scriptPaths = getCustomScriptPaths(outputPath);
711
+ for (const scriptPath of scriptPaths) {
712
+ try {
713
+ fs.chmodSync(scriptPath, 0o755);
714
+ }
715
+ catch (error) {
716
+ console.warn(chalk.yellow(`⚠️ Failed to make ${scriptPath} executable:`, error));
717
+ }
718
+ }
719
+ // Add custom postCreateCommand scripts
720
+ if (customConfig.scripts.postCreate && customConfig.scripts.postCreate.length > 0) {
721
+ console.log(chalk.dim(` 🔧 Adding custom post-create script(s)`));
722
+ if (!config.postCreateCommand) {
723
+ config.postCreateCommand = {};
724
+ }
725
+ // Handle array form - convert to object
726
+ if (Array.isArray(config.postCreateCommand)) {
727
+ const arrayCommands = config.postCreateCommand;
728
+ config.postCreateCommand = {};
729
+ for (let i = 0; i < arrayCommands.length; i++) {
730
+ config.postCreateCommand[`command-${i}`] = arrayCommands[i];
731
+ }
732
+ }
733
+ // Handle string form - convert to object
734
+ if (typeof config.postCreateCommand === 'string') {
735
+ config.postCreateCommand = { default: config.postCreateCommand };
736
+ }
737
+ for (let i = 0; i < customConfig.scripts.postCreate.length; i++) {
738
+ const key = `custom-post-create-${i}`;
739
+ config.postCreateCommand[key] = customConfig.scripts.postCreate[i];
740
+ }
741
+ }
742
+ // Add custom postStartCommand scripts
743
+ if (customConfig.scripts.postStart && customConfig.scripts.postStart.length > 0) {
744
+ console.log(chalk.dim(` ✓ Adding custom post-start script(s)`));
745
+ if (!config.postStartCommand) {
746
+ config.postStartCommand = {};
747
+ }
748
+ // Handle array form - convert to object
749
+ if (Array.isArray(config.postStartCommand)) {
750
+ const arrayCommands = config.postStartCommand;
751
+ config.postStartCommand = {};
752
+ for (let i = 0; i < arrayCommands.length; i++) {
753
+ config.postStartCommand[`command-${i}`] = arrayCommands[i];
754
+ }
755
+ }
756
+ // Handle string form - convert to object
757
+ if (typeof config.postStartCommand === 'string') {
758
+ config.postStartCommand = { default: config.postStartCommand };
759
+ }
760
+ for (let i = 0; i < customConfig.scripts.postStart.length; i++) {
761
+ const key = `custom-post-start-${i}`;
762
+ config.postStartCommand[key] = customConfig.scripts.postStart[i];
763
+ }
764
+ }
765
+ return config;
766
+ }
767
+ /**
768
+ * Copy custom files from custom/files/ directory
769
+ */
770
+ function copyCustomFiles(customConfig, outputPath, fileRegistry) {
771
+ if (!customConfig.files || customConfig.files.length === 0) {
772
+ return;
773
+ }
774
+ console.log(chalk.dim(` 📄 Copying ${customConfig.files.length} custom file(s)`));
775
+ const directoriesAdded = new Set();
776
+ for (const file of customConfig.files) {
777
+ const destPath = path.join(outputPath, file.destination);
778
+ const destDir = path.dirname(destPath);
779
+ const relativeDest = path.relative(outputPath, destPath);
780
+ const relativeDestDir = path.relative(outputPath, destDir);
781
+ // Create destination directory if it doesn't exist
782
+ if (!fs.existsSync(destDir)) {
783
+ fs.mkdirSync(destDir, { recursive: true });
784
+ }
785
+ // Add directory to registry if not already added
786
+ if (relativeDestDir && relativeDestDir !== '.' && !directoriesAdded.has(relativeDestDir)) {
787
+ // Add all parent directories
788
+ const parts = relativeDestDir.split(path.sep);
789
+ for (let i = 1; i <= parts.length; i++) {
790
+ const dirPath = parts.slice(0, i).join(path.sep);
791
+ if (!directoriesAdded.has(dirPath)) {
792
+ fileRegistry.addDirectory(dirPath);
793
+ directoriesAdded.add(dirPath);
794
+ }
795
+ }
796
+ }
797
+ // Copy file
798
+ fs.copyFileSync(file.source, destPath);
799
+ // Add file to registry
800
+ fileRegistry.addFile(relativeDest);
801
+ }
802
+ }
803
+ /**
804
+ * Main composition logic
805
+ */
806
+ export async function composeDevContainer(answers) {
807
+ // 1. Load overlay configuration
808
+ const overlaysDir = path.join(REPO_ROOT, 'overlays');
809
+ const indexYmlPath = path.join(REPO_ROOT, 'overlays', 'index.yml');
810
+ const overlaysConfig = loadOverlaysConfig(overlaysDir, indexYmlPath);
811
+ // Collect all overlay definitions
812
+ const allOverlayDefs = getAllOverlayDefs(overlaysConfig);
813
+ // Build list of requested overlays
814
+ const requestedOverlays = [];
815
+ if (answers.language && answers.language.length > 0)
816
+ requestedOverlays.push(...answers.language);
817
+ if (answers.database && answers.database.length > 0)
818
+ requestedOverlays.push(...answers.database);
819
+ if (answers.observability)
820
+ requestedOverlays.push(...answers.observability);
821
+ if (answers.playwright)
822
+ requestedOverlays.push('playwright');
823
+ if (answers.cloudTools)
824
+ requestedOverlays.push(...answers.cloudTools);
825
+ if (answers.devTools)
826
+ requestedOverlays.push(...answers.devTools);
827
+ // Check compatibility
828
+ const incompatible = [];
829
+ for (const overlayId of requestedOverlays) {
830
+ const overlayDef = allOverlayDefs.find((o) => o.id === overlayId);
831
+ if (overlayDef?.supports && overlayDef.supports.length > 0) {
832
+ if (!overlayDef.supports.includes(answers.stack)) {
833
+ incompatible.push(`${overlayId} (requires: ${overlayDef.supports.join(', ')})`);
834
+ }
835
+ }
836
+ }
837
+ if (incompatible.length > 0) {
838
+ console.log(chalk.yellow(`\n⚠️ Warning: Some overlays are not compatible with '${answers.stack}' template:`));
839
+ incompatible.forEach((overlay) => {
840
+ console.log(chalk.yellow(` • ${overlay}`));
841
+ });
842
+ console.log(chalk.yellow(`\nThese overlays will be skipped.\n`));
843
+ // Filter out incompatible overlays
844
+ if (answers.database) {
845
+ answers.database = answers.database.filter((d) => !incompatible.some((i) => i.startsWith(d)));
846
+ }
847
+ if (answers.observability) {
848
+ answers.observability = answers.observability.filter((o) => !incompatible.some((i) => i.startsWith(o)));
849
+ }
850
+ // Update requestedOverlays after filtering
851
+ requestedOverlays.length = 0;
852
+ if (answers.language && answers.language.length > 0)
853
+ requestedOverlays.push(...answers.language);
854
+ if (answers.database && answers.database.length > 0)
855
+ requestedOverlays.push(...answers.database);
856
+ if (answers.observability)
857
+ requestedOverlays.push(...answers.observability);
858
+ if (answers.playwright)
859
+ requestedOverlays.push('playwright');
860
+ if (answers.cloudTools)
861
+ requestedOverlays.push(...answers.cloudTools);
862
+ if (answers.devTools)
863
+ requestedOverlays.push(...answers.devTools);
864
+ }
865
+ // 2. Resolve dependencies
866
+ const { overlays: resolvedOverlays, autoResolved } = resolveDependencies(requestedOverlays, allOverlayDefs);
867
+ // 3. Determine base template path
868
+ const templatePath = path.join(TEMPLATES_DIR, answers.stack, '.devcontainer');
869
+ if (!fs.existsSync(templatePath)) {
870
+ throw new Error(`Template not found: ${answers.stack}`);
871
+ }
872
+ // 4. Load base devcontainer.json
873
+ const baseConfigPath = path.join(templatePath, 'devcontainer.json');
874
+ let config = loadJson(baseConfigPath);
875
+ // 4a. Set container name if provided
876
+ if (answers.containerName) {
877
+ config.name = answers.containerName;
878
+ console.log(chalk.dim(` 📝 Container name: ${chalk.cyan(answers.containerName)}`));
879
+ }
880
+ // 4b. Apply base image selection
881
+ // Build image map from overlaysConfig instead of hardcoding
882
+ const imageMap = {};
883
+ for (const baseImage of overlaysConfig.base_images) {
884
+ if (baseImage.image) {
885
+ imageMap[baseImage.id] = baseImage.image;
886
+ }
887
+ }
888
+ // Get default base image (first in list)
889
+ const defaultBaseImage = overlaysConfig.base_images[0];
890
+ if (answers.baseImage === 'custom' && answers.customImage) {
891
+ // Use custom image provided by user
892
+ if (answers.stack === 'plain') {
893
+ config.image = answers.customImage;
894
+ }
895
+ else if (answers.stack === 'compose') {
896
+ // For compose, we'll need to update docker-compose.yml later
897
+ config._customImage = answers.customImage; // Temporary marker
898
+ }
899
+ console.log(chalk.yellow(` ⚠️ Using custom image: ${answers.customImage}`));
900
+ }
901
+ else if (answers.baseImage !== defaultBaseImage.id) {
902
+ // Apply non-default base image
903
+ const selectedImage = imageMap[answers.baseImage];
904
+ if (answers.stack === 'plain') {
905
+ config.image = selectedImage;
906
+ }
907
+ else if (answers.stack === 'compose') {
908
+ config._customImage = selectedImage; // Temporary marker
909
+ }
910
+ console.log(chalk.dim(` 🖼️ Using base image: ${chalk.cyan(answers.baseImage)}`));
911
+ }
912
+ // 5. Order overlays for proper dependency resolution
913
+ // Observability overlays (in dependency order)
914
+ const orderedOverlays = [];
915
+ const observabilityOrder = [
916
+ 'jaeger',
917
+ 'tempo',
918
+ 'prometheus',
919
+ 'alertmanager',
920
+ 'loki',
921
+ 'promtail',
922
+ 'otel-collector',
923
+ 'grafana',
924
+ 'otel-demo-nodejs',
925
+ 'otel-demo-python',
926
+ ];
927
+ // Add observability overlays in order
928
+ for (const obs of observabilityOrder) {
929
+ if (resolvedOverlays.includes(obs)) {
930
+ orderedOverlays.push(obs);
931
+ }
932
+ }
933
+ // Add remaining overlays
934
+ for (const overlay of resolvedOverlays) {
935
+ if (!orderedOverlays.includes(overlay)) {
936
+ orderedOverlays.push(overlay);
937
+ }
938
+ }
939
+ const overlays = orderedOverlays;
940
+ // 5. Create output directory and file registry for cleanup
941
+ const outputPath = path.resolve(answers.outputPath);
942
+ const fileRegistry = new FileRegistry();
943
+ if (!fs.existsSync(outputPath)) {
944
+ fs.mkdirSync(outputPath, { recursive: true });
945
+ }
946
+ // 6. Apply overlays
947
+ for (const overlay of overlays) {
948
+ console.log(chalk.dim(` 🔧 Applying overlay: ${chalk.cyan(overlay)}`));
949
+ config = applyOverlay(config, overlay);
950
+ }
951
+ // 7. Copy template files (docker-compose, scripts, etc.)
952
+ const entries = fs.readdirSync(templatePath);
953
+ for (const entry of entries) {
954
+ if (entry === 'devcontainer.json')
955
+ continue; // We'll write this separately
956
+ const srcPath = path.join(templatePath, entry);
957
+ const destPath = path.join(outputPath, entry);
958
+ const stat = fs.statSync(srcPath);
959
+ if (stat.isDirectory()) {
960
+ copyDir(srcPath, destPath);
961
+ fileRegistry.addDirectory(entry);
962
+ }
963
+ else {
964
+ fs.copyFileSync(srcPath, destPath);
965
+ fileRegistry.addFile(entry);
966
+ }
967
+ }
968
+ // 8. Copy overlay files (docker-compose, configs, etc.)
969
+ for (const overlay of overlays) {
970
+ copyOverlayFiles(outputPath, overlay, fileRegistry);
971
+ }
972
+ // 8.5. Copy cross-distro-packages feature if used
973
+ if (config.features?.['./features/cross-distro-packages']) {
974
+ const featuresDir = path.join(outputPath, 'features', 'cross-distro-packages');
975
+ const sourceFeatureDir = path.join(REPO_ROOT, 'features', 'cross-distro-packages');
976
+ if (fs.existsSync(sourceFeatureDir)) {
977
+ copyDir(sourceFeatureDir, featuresDir);
978
+ fileRegistry.addDirectory('features');
979
+ console.log(chalk.dim(` 📦 Copied cross-distro-packages feature`));
980
+ }
981
+ }
982
+ // 8. Filter docker-compose dependencies based on selected overlays
983
+ filterDockerComposeDependencies(outputPath, overlays);
984
+ // 9. Merge runServices array in correct order
985
+ mergeRunServices(config, overlays);
986
+ // 11. Merge docker-compose files into single combined file
987
+ if (answers.stack === 'compose') {
988
+ const customImage = config._customImage;
989
+ mergeDockerComposeFiles(outputPath, answers.stack, overlays, answers.portOffset, customImage);
990
+ // Update devcontainer.json to reference the combined file
991
+ if (config.dockerComposeFile) {
992
+ config.dockerComposeFile = 'docker-compose.yml';
993
+ }
994
+ }
995
+ // Apply port offset to devcontainer.json if specified
996
+ if (answers.portOffset) {
997
+ applyPortOffsetToDevcontainer(config, answers.portOffset);
998
+ }
999
+ // Merge setup scripts from overlays into postCreateCommand
1000
+ mergeSetupScripts(config, overlays, outputPath, fileRegistry);
1001
+ // 10. Apply custom patches from .devcontainer/custom/ (if present)
1002
+ const customPatches = loadCustomPatches(outputPath);
1003
+ if (customPatches) {
1004
+ console.log(chalk.cyan('\n🎨 Applying custom patches...'));
1005
+ // Apply custom devcontainer patch
1006
+ config = applyCustomDevcontainerPatch(config, customPatches);
1007
+ // Apply custom scripts
1008
+ config = applyCustomScripts(config, customPatches, outputPath);
1009
+ // Copy custom files
1010
+ copyCustomFiles(customPatches, outputPath, fileRegistry);
1011
+ }
1012
+ // Remove internal fields (those starting with _)
1013
+ Object.keys(config).forEach((key) => {
1014
+ if (key.startsWith('_')) {
1015
+ delete config[key];
1016
+ }
1017
+ });
1018
+ // 12. Write merged devcontainer.json
1019
+ const configPath = path.join(outputPath, 'devcontainer.json');
1020
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
1021
+ fileRegistry.addFile('devcontainer.json');
1022
+ console.log(chalk.dim(` 📝 Wrote devcontainer.json`));
1023
+ // Apply custom docker-compose patch (after writing base docker-compose.yml)
1024
+ if (customPatches && answers.stack === 'compose') {
1025
+ applyCustomDockerComposePatch(outputPath, customPatches);
1026
+ }
1027
+ // 13. Generate superposition.json manifest
1028
+ generateManifest(outputPath, answers, overlays, autoResolved, answers.containerName || config.name);
1029
+ fileRegistry.addFile('superposition.json');
1030
+ // 14. Merge .env.example files from overlays and apply glue config environment variables
1031
+ const envCreated = mergeEnvExamples(outputPath, overlays, answers.portOffset, answers.presetGlueConfig, answers.preset);
1032
+ if (envCreated) {
1033
+ fileRegistry.addFile('.env.example');
1034
+ }
1035
+ // Apply custom environment variables (after .env.example is created)
1036
+ if (customPatches) {
1037
+ const customEnvCreated = applyCustomEnvironment(outputPath, customPatches);
1038
+ // Add .env.example to registry if it was created by custom patches but not by overlays
1039
+ if (customEnvCreated && !envCreated) {
1040
+ fileRegistry.addFile('.env.example');
1041
+ }
1042
+ }
1043
+ // 15. Apply preset glue configuration (README and port mappings) if present
1044
+ if (answers.presetGlueConfig) {
1045
+ applyGlueConfig(outputPath, answers.presetGlueConfig, answers.preset, fileRegistry);
1046
+ }
1047
+ // 16. Generate consolidated README.md from selected overlays
1048
+ console.log(chalk.cyan('\n📖 Generating consolidated README...'));
1049
+ const overlayMetadataMap = new Map(allOverlayDefs.map((o) => [o.id, o]));
1050
+ generateReadme(answers, overlays, overlayMetadataMap, outputPath);
1051
+ fileRegistry.addFile('README.md');
1052
+ console.log(chalk.dim(` 📝 Created README.md with documentation from ${overlays.length} overlay(s)`));
1053
+ // 17. Clean up stale files from previous runs (preserves superposition.json and .env)
1054
+ cleanupStaleFiles(outputPath, fileRegistry);
1055
+ }
1056
+ /**
1057
+ * Apply port offset to devcontainer.json forwardPorts and portsAttributes
1058
+ */
1059
+ function applyPortOffsetToDevcontainer(config, offset) {
1060
+ // Offset forwardPorts
1061
+ if (config.forwardPorts && Array.isArray(config.forwardPorts)) {
1062
+ config.forwardPorts = config.forwardPorts.map((port) => {
1063
+ if (typeof port === 'number') {
1064
+ return port + offset;
1065
+ }
1066
+ return port;
1067
+ });
1068
+ }
1069
+ // Offset portsAttributes keys
1070
+ if (config.portsAttributes) {
1071
+ const newPortsAttributes = {};
1072
+ for (const [port, attrs] of Object.entries(config.portsAttributes)) {
1073
+ const portNum = parseInt(port, 10);
1074
+ if (!isNaN(portNum)) {
1075
+ newPortsAttributes[portNum + offset] = attrs;
1076
+ }
1077
+ else {
1078
+ newPortsAttributes[port] = attrs;
1079
+ }
1080
+ }
1081
+ config.portsAttributes = newPortsAttributes;
1082
+ }
1083
+ }
1084
+ /**
1085
+ * Merge setup scripts from overlays into postCreateCommand
1086
+ */
1087
+ function mergeSetupScripts(config, overlays, outputPath, fileRegistry) {
1088
+ const setupScripts = [];
1089
+ const verifyScripts = [];
1090
+ // Create scripts subfolder
1091
+ const scriptsDir = path.join(outputPath, 'scripts');
1092
+ if (!fs.existsSync(scriptsDir)) {
1093
+ fs.mkdirSync(scriptsDir, { recursive: true });
1094
+ }
1095
+ // Add scripts directory to registry if any scripts will be added
1096
+ const hasScripts = overlays.some((o) => fs.existsSync(path.join(OVERLAYS_DIR, o, 'setup.sh')) ||
1097
+ fs.existsSync(path.join(OVERLAYS_DIR, o, 'verify.sh')));
1098
+ if (hasScripts) {
1099
+ fileRegistry.addDirectory('scripts');
1100
+ }
1101
+ for (const overlay of overlays) {
1102
+ // Handle setup scripts
1103
+ const setupPath = path.join(OVERLAYS_DIR, overlay, 'setup.sh');
1104
+ if (fs.existsSync(setupPath)) {
1105
+ // Copy setup script to scripts subdirectory
1106
+ const destPath = path.join(scriptsDir, `setup-${overlay}.sh`);
1107
+ fs.copyFileSync(setupPath, destPath);
1108
+ // Make it executable
1109
+ fs.chmodSync(destPath, 0o755);
1110
+ fileRegistry.addFile(`scripts/setup-${overlay}.sh`);
1111
+ setupScripts.push(`bash .devcontainer/scripts/setup-${overlay}.sh`);
1112
+ }
1113
+ // Handle verify scripts
1114
+ const verifyPath = path.join(OVERLAYS_DIR, overlay, 'verify.sh');
1115
+ if (fs.existsSync(verifyPath)) {
1116
+ // Copy verify script to scripts subdirectory
1117
+ const destPath = path.join(scriptsDir, `verify-${overlay}.sh`);
1118
+ fs.copyFileSync(verifyPath, destPath);
1119
+ // Make it executable
1120
+ fs.chmodSync(destPath, 0o755);
1121
+ fileRegistry.addFile(`scripts/verify-${overlay}.sh`);
1122
+ verifyScripts.push(`bash .devcontainer/scripts/verify-${overlay}.sh`);
1123
+ }
1124
+ }
1125
+ if (setupScripts.length > 0) {
1126
+ // Initialize postCreateCommand if it doesn't exist
1127
+ if (!config.postCreateCommand) {
1128
+ config.postCreateCommand = {};
1129
+ }
1130
+ // If postCreateCommand is a string, convert to object
1131
+ if (typeof config.postCreateCommand === 'string') {
1132
+ config.postCreateCommand = { default: config.postCreateCommand };
1133
+ }
1134
+ // Add setup scripts
1135
+ for (let i = 0; i < setupScripts.length; i++) {
1136
+ const overlay = overlays.filter((o) => {
1137
+ const setupPath = path.join(OVERLAYS_DIR, o, 'setup.sh');
1138
+ return fs.existsSync(setupPath);
1139
+ })[i];
1140
+ config.postCreateCommand[`setup-${overlay}`] = setupScripts[i];
1141
+ }
1142
+ console.log(chalk.dim(` 🔧 Added ${setupScripts.length} setup script(s)`));
1143
+ }
1144
+ if (verifyScripts.length > 0) {
1145
+ // Initialize postStartCommand if it doesn't exist
1146
+ if (!config.postStartCommand) {
1147
+ config.postStartCommand = {};
1148
+ }
1149
+ // If postStartCommand is a string, convert to object
1150
+ if (typeof config.postStartCommand === 'string') {
1151
+ config.postStartCommand = { default: config.postStartCommand };
1152
+ }
1153
+ // Add verify scripts
1154
+ for (let i = 0; i < verifyScripts.length; i++) {
1155
+ const overlay = overlays.filter((o) => {
1156
+ const verifyPath = path.join(OVERLAYS_DIR, o, 'verify.sh');
1157
+ return fs.existsSync(verifyPath);
1158
+ })[i];
1159
+ config.postStartCommand[`verify-${overlay}`] = verifyScripts[i];
1160
+ }
1161
+ console.log(chalk.dim(` ✓ Added ${verifyScripts.length} verification script(s)`));
1162
+ }
1163
+ }
1164
+ /**
1165
+ * Filter depends_on in docker-compose files to only include selected services
1166
+ */
1167
+ function filterDockerComposeDependencies(outputPath, selectedOverlays) {
1168
+ const selectedServices = new Set(selectedOverlays);
1169
+ const composeFiles = fs
1170
+ .readdirSync(outputPath)
1171
+ .filter((f) => f.startsWith('docker-compose.') && f.endsWith('.yml'));
1172
+ for (const composeFile of composeFiles) {
1173
+ const composePath = path.join(outputPath, composeFile);
1174
+ let content = fs.readFileSync(composePath, 'utf-8');
1175
+ // Parse YAML manually for simple depends_on filtering
1176
+ // This is a simplified approach - for production, use a proper YAML parser
1177
+ const lines = content.split('\n');
1178
+ const filtered = [];
1179
+ let inDependsOn = false;
1180
+ let dependsOnIndent = 0;
1181
+ for (let i = 0; i < lines.length; i++) {
1182
+ const line = lines[i];
1183
+ const indent = line.search(/\S/);
1184
+ if (line.trim().startsWith('depends_on:')) {
1185
+ inDependsOn = true;
1186
+ dependsOnIndent = indent;
1187
+ filtered.push(line);
1188
+ continue;
1189
+ }
1190
+ if (inDependsOn) {
1191
+ if (indent <= dependsOnIndent && line.trim() !== '') {
1192
+ inDependsOn = false;
1193
+ }
1194
+ else if (line.trim().startsWith('-')) {
1195
+ // Extract service name
1196
+ const service = line.trim().substring(1).trim();
1197
+ if (selectedServices.has(service)) {
1198
+ filtered.push(line);
1199
+ }
1200
+ continue;
1201
+ }
1202
+ }
1203
+ filtered.push(line);
1204
+ }
1205
+ fs.writeFileSync(composePath, filtered.join('\n'));
1206
+ }
1207
+ }
1208
+ /**
1209
+ * Merge runServices from all overlays in correct order
1210
+ */
1211
+ function mergeRunServices(config, overlays) {
1212
+ const services = [];
1213
+ for (const overlay of overlays) {
1214
+ const overlayPath = path.join(OVERLAYS_DIR, overlay, 'devcontainer.patch.json');
1215
+ if (fs.existsSync(overlayPath)) {
1216
+ const overlayConfig = loadJson(overlayPath);
1217
+ if (overlayConfig.runServices) {
1218
+ const order = overlayConfig._serviceOrder || 0;
1219
+ for (const service of overlayConfig.runServices) {
1220
+ services.push({ name: service, order });
1221
+ }
1222
+ }
1223
+ }
1224
+ }
1225
+ // Sort by order, then merge
1226
+ services.sort((a, b) => a.order - b.order);
1227
+ const uniqueServices = [...new Set(services.map((s) => s.name))];
1228
+ if (uniqueServices.length > 0) {
1229
+ config.runServices = uniqueServices;
1230
+ }
1231
+ }
1232
+ //# sourceMappingURL=composer.js.map