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.
- package/README.md +843 -0
- package/dist/scripts/init.d.ts +3 -0
- package/dist/scripts/init.d.ts.map +1 -0
- package/dist/scripts/init.js +1190 -0
- package/dist/scripts/init.js.map +1 -0
- package/dist/scripts/migrate-to-manifests.d.ts +12 -0
- package/dist/scripts/migrate-to-manifests.d.ts.map +1 -0
- package/dist/scripts/migrate-to-manifests.js +230 -0
- package/dist/scripts/migrate-to-manifests.js.map +1 -0
- package/dist/tool/questionnaire/composer.d.ts +6 -0
- package/dist/tool/questionnaire/composer.d.ts.map +1 -0
- package/dist/tool/questionnaire/composer.js +1232 -0
- package/dist/tool/questionnaire/composer.js.map +1 -0
- package/dist/tool/readme/markdown-parser.d.ts +30 -0
- package/dist/tool/readme/markdown-parser.d.ts.map +1 -0
- package/dist/tool/readme/markdown-parser.js +139 -0
- package/dist/tool/readme/markdown-parser.js.map +1 -0
- package/dist/tool/readme/readme-generator.d.ts +9 -0
- package/dist/tool/readme/readme-generator.d.ts.map +1 -0
- package/dist/tool/readme/readme-generator.js +422 -0
- package/dist/tool/readme/readme-generator.js.map +1 -0
- package/dist/tool/schema/custom-loader.d.ts +17 -0
- package/dist/tool/schema/custom-loader.d.ts.map +1 -0
- package/dist/tool/schema/custom-loader.js +149 -0
- package/dist/tool/schema/custom-loader.js.map +1 -0
- package/dist/tool/schema/overlay-loader.d.ts +47 -0
- package/dist/tool/schema/overlay-loader.d.ts.map +1 -0
- package/dist/tool/schema/overlay-loader.js +252 -0
- package/dist/tool/schema/overlay-loader.js.map +1 -0
- package/dist/tool/schema/types.d.ts +212 -0
- package/dist/tool/schema/types.d.ts.map +1 -0
- package/dist/tool/schema/types.js +5 -0
- package/dist/tool/schema/types.js.map +1 -0
- package/docs/README.md +308 -0
- package/docs/architecture.md +233 -0
- package/docs/creating-overlays.md +549 -0
- package/docs/custom-patches.md +540 -0
- package/docs/dependencies.md +279 -0
- package/docs/examples/custom-patches-example.md +85 -0
- package/docs/examples.md +576 -0
- package/docs/messaging-comparison.md +265 -0
- package/docs/messaging-quick-start.md +385 -0
- package/docs/observability-workflow.md +537 -0
- package/docs/overlay-manifest-refactoring.md +214 -0
- package/docs/overlay-metadata-archive.md +54 -0
- package/docs/overlays.md +523 -0
- package/docs/presets-architecture.md +498 -0
- package/docs/presets.md +366 -0
- package/docs/publishing.md +476 -0
- package/docs/quick-reference.md +326 -0
- package/docs/ux.md +170 -0
- package/features/README.md +85 -0
- package/features/cross-distro-packages/README.md +146 -0
- package/features/cross-distro-packages/devcontainer-feature.json +20 -0
- package/features/cross-distro-packages/install.sh +58 -0
- package/features/local-secrets-manager/devcontainer-feature.json +18 -0
- package/features/local-secrets-manager/install.sh +127 -0
- package/features/project-scaffolder/devcontainer-feature.json +24 -0
- package/features/project-scaffolder/install.sh +100 -0
- package/features/team-conventions/devcontainer-feature.json +24 -0
- package/features/team-conventions/install.sh +93 -0
- package/overlays/.registry/README.md +14 -0
- package/overlays/.registry/base-images.yml +26 -0
- package/overlays/.registry/base-templates.yml +7 -0
- package/overlays/README.md +155 -0
- package/overlays/alertmanager/.env.example +5 -0
- package/overlays/alertmanager/README.md +465 -0
- package/overlays/alertmanager/alert-rules.yml +56 -0
- package/overlays/alertmanager/alertmanager.yml +42 -0
- package/overlays/alertmanager/devcontainer.patch.json +12 -0
- package/overlays/alertmanager/docker-compose.yml +20 -0
- package/overlays/alertmanager/overlay.yml +17 -0
- package/overlays/alertmanager/setup.sh +53 -0
- package/overlays/alertmanager/verify.sh +31 -0
- package/overlays/aws-cli/README.md +473 -0
- package/overlays/aws-cli/devcontainer.patch.json +13 -0
- package/overlays/aws-cli/overlay.yml +13 -0
- package/overlays/azure-cli/README.md +551 -0
- package/overlays/azure-cli/devcontainer.patch.json +8 -0
- package/overlays/azure-cli/overlay.yml +13 -0
- package/overlays/bun/README.md +312 -0
- package/overlays/bun/devcontainer.patch.json +41 -0
- package/overlays/bun/overlay.yml +16 -0
- package/overlays/bun/setup.sh +79 -0
- package/overlays/bun/verify.sh +30 -0
- package/overlays/codex/README.md +128 -0
- package/overlays/codex/devcontainer.patch.json +3 -0
- package/overlays/codex/overlay.yml +14 -0
- package/overlays/codex/setup.sh +24 -0
- package/overlays/codex/verify.sh +30 -0
- package/overlays/commitlint/README.md +333 -0
- package/overlays/commitlint/devcontainer.patch.json +8 -0
- package/overlays/commitlint/overlay.yml +16 -0
- package/overlays/commitlint/setup.sh +234 -0
- package/overlays/direnv/README.md +504 -0
- package/overlays/direnv/devcontainer.patch.json +6 -0
- package/overlays/direnv/overlay.yml +13 -0
- package/overlays/direnv/setup.sh +139 -0
- package/overlays/docker-in-docker/README.md +534 -0
- package/overlays/docker-in-docker/devcontainer.patch.json +10 -0
- package/overlays/docker-in-docker/overlay.yml +13 -0
- package/overlays/docker-sock/README.md +256 -0
- package/overlays/docker-sock/devcontainer.patch.json +9 -0
- package/overlays/docker-sock/docker-compose.yml +8 -0
- package/overlays/docker-sock/overlay.yml +13 -0
- package/overlays/dotnet/README.md +147 -0
- package/overlays/dotnet/devcontainer.patch.json +51 -0
- package/overlays/dotnet/global-tools.txt +24 -0
- package/overlays/dotnet/overlay.yml +13 -0
- package/overlays/dotnet/setup.sh +51 -0
- package/overlays/dotnet/verify.sh +26 -0
- package/overlays/gcloud/README.md +269 -0
- package/overlays/gcloud/devcontainer.patch.json +14 -0
- package/overlays/gcloud/overlay.yml +14 -0
- package/overlays/gcloud/verify.sh +52 -0
- package/overlays/git-helpers/README.md +168 -0
- package/overlays/git-helpers/devcontainer.patch.json +33 -0
- package/overlays/git-helpers/overlay.yml +15 -0
- package/overlays/git-helpers/setup.sh +91 -0
- package/overlays/go/README.md +293 -0
- package/overlays/go/devcontainer.patch.json +43 -0
- package/overlays/go/overlay.yml +15 -0
- package/overlays/go/setup.sh +33 -0
- package/overlays/go/verify.sh +40 -0
- package/overlays/grafana/.env.example +9 -0
- package/overlays/grafana/README.md +462 -0
- package/overlays/grafana/dashboard-provider.yml +11 -0
- package/overlays/grafana/dashboards/observability-overview.json +263 -0
- package/overlays/grafana/devcontainer.patch.json +12 -0
- package/overlays/grafana/docker-compose.yml +27 -0
- package/overlays/grafana/grafana-datasources.yml +57 -0
- package/overlays/grafana/overlay.yml +21 -0
- package/overlays/grafana/verify.sh +34 -0
- package/overlays/jaeger/.env.example +7 -0
- package/overlays/jaeger/README.md +867 -0
- package/overlays/jaeger/devcontainer.patch.json +12 -0
- package/overlays/jaeger/docker-compose.yml +17 -0
- package/overlays/jaeger/overlay.yml +19 -0
- package/overlays/java/README.md +267 -0
- package/overlays/java/devcontainer.patch.json +44 -0
- package/overlays/java/overlay.yml +16 -0
- package/overlays/java/setup.sh +41 -0
- package/overlays/java/verify.sh +42 -0
- package/overlays/just/README.md +443 -0
- package/overlays/just/devcontainer.patch.json +3 -0
- package/overlays/just/overlay.yml +13 -0
- package/overlays/just/setup.sh +182 -0
- package/overlays/kubectl-helm/README.md +660 -0
- package/overlays/kubectl-helm/devcontainer.patch.json +10 -0
- package/overlays/kubectl-helm/overlay.yml +13 -0
- package/overlays/loki/.env.example +5 -0
- package/overlays/loki/README.md +1156 -0
- package/overlays/loki/devcontainer.patch.json +12 -0
- package/overlays/loki/docker-compose.yml +18 -0
- package/overlays/loki/loki-config.yaml +45 -0
- package/overlays/loki/overlay.yml +17 -0
- package/overlays/minio/.env.example +9 -0
- package/overlays/minio/README.md +639 -0
- package/overlays/minio/devcontainer.patch.json +30 -0
- package/overlays/minio/docker-compose.yml +28 -0
- package/overlays/minio/overlay.yml +18 -0
- package/overlays/minio/setup.sh +61 -0
- package/overlays/minio/verify.sh +64 -0
- package/overlays/mkdocs/README.md +309 -0
- package/overlays/mkdocs/devcontainer.patch.json +24 -0
- package/overlays/mkdocs/overlay.yml +15 -0
- package/overlays/modern-cli-tools/README.md +556 -0
- package/overlays/modern-cli-tools/devcontainer.patch.json +3 -0
- package/overlays/modern-cli-tools/overlay.yml +13 -0
- package/overlays/modern-cli-tools/setup.sh +153 -0
- package/overlays/mongodb/.env.example +9 -0
- package/overlays/mongodb/README.md +481 -0
- package/overlays/mongodb/devcontainer.patch.json +32 -0
- package/overlays/mongodb/docker-compose.yml +44 -0
- package/overlays/mongodb/overlay.yml +17 -0
- package/overlays/mongodb/verify.sh +48 -0
- package/overlays/mysql/.env.example +11 -0
- package/overlays/mysql/README.md +542 -0
- package/overlays/mysql/devcontainer.patch.json +34 -0
- package/overlays/mysql/docker-compose.yml +55 -0
- package/overlays/mysql/overlay.yml +16 -0
- package/overlays/mysql/verify.sh +48 -0
- package/overlays/nats/.env.example +5 -0
- package/overlays/nats/README.md +762 -0
- package/overlays/nats/devcontainer.patch.json +24 -0
- package/overlays/nats/docker-compose.yml +31 -0
- package/overlays/nats/overlay.yml +18 -0
- package/overlays/nats/verify.sh +50 -0
- package/overlays/ngrok/README.md +503 -0
- package/overlays/ngrok/devcontainer.patch.json +3 -0
- package/overlays/ngrok/overlay.yml +14 -0
- package/overlays/ngrok/setup.sh +125 -0
- package/overlays/nodejs/README.md +192 -0
- package/overlays/nodejs/devcontainer.patch.json +49 -0
- package/overlays/nodejs/global-packages.txt +16 -0
- package/overlays/nodejs/overlay.yml +14 -0
- package/overlays/nodejs/setup.sh +46 -0
- package/overlays/nodejs/verify.sh +32 -0
- package/overlays/otel-collector/.env.example +9 -0
- package/overlays/otel-collector/README.md +1257 -0
- package/overlays/otel-collector/devcontainer.patch.json +28 -0
- package/overlays/otel-collector/docker-compose.yml +22 -0
- package/overlays/otel-collector/otel-collector-config.yaml +68 -0
- package/overlays/otel-collector/overlay.yml +21 -0
- package/overlays/otel-collector/setup.sh +49 -0
- package/overlays/otel-demo-nodejs/.env.example +2 -0
- package/overlays/otel-demo-nodejs/Dockerfile-otel-demo-nodejs +17 -0
- package/overlays/otel-demo-nodejs/README.md +409 -0
- package/overlays/otel-demo-nodejs/devcontainer.patch.json +12 -0
- package/overlays/otel-demo-nodejs/docker-compose.yml +19 -0
- package/overlays/otel-demo-nodejs/overlay.yml +23 -0
- package/overlays/otel-demo-nodejs/package-otel-demo-nodejs.json +20 -0
- package/overlays/otel-demo-nodejs/server-otel-demo-nodejs.js +259 -0
- package/overlays/otel-demo-nodejs/tracing-otel-demo-nodejs.js +57 -0
- package/overlays/otel-demo-nodejs/verify.sh +31 -0
- package/overlays/otel-demo-python/.env.example +2 -0
- package/overlays/otel-demo-python/Dockerfile-otel-demo-python +16 -0
- package/overlays/otel-demo-python/README.md +82 -0
- package/overlays/otel-demo-python/app-otel-demo-python.py +208 -0
- package/overlays/otel-demo-python/devcontainer.patch.json +12 -0
- package/overlays/otel-demo-python/docker-compose.yml +19 -0
- package/overlays/otel-demo-python/overlay.yml +23 -0
- package/overlays/otel-demo-python/requirements-otel-demo-python.txt +4 -0
- package/overlays/otel-demo-python/verify.sh +31 -0
- package/overlays/playwright/README.md +629 -0
- package/overlays/playwright/devcontainer.patch.json +9 -0
- package/overlays/playwright/overlay.yml +13 -0
- package/overlays/postgres/.env.example +6 -0
- package/overlays/postgres/README.md +602 -0
- package/overlays/postgres/devcontainer.patch.json +21 -0
- package/overlays/postgres/docker-compose.yml +22 -0
- package/overlays/postgres/overlay.yml +15 -0
- package/overlays/postgres/verify.sh +45 -0
- package/overlays/powershell/README.md +314 -0
- package/overlays/powershell/devcontainer.patch.json +22 -0
- package/overlays/powershell/overlay.yml +13 -0
- package/overlays/powershell/setup.sh +29 -0
- package/overlays/powershell/verify.sh +38 -0
- package/overlays/pre-commit/README.md +263 -0
- package/overlays/pre-commit/devcontainer.patch.json +9 -0
- package/overlays/pre-commit/overlay.yml +16 -0
- package/overlays/pre-commit/setup.sh +129 -0
- package/overlays/presets/docs-site.yml +118 -0
- package/overlays/presets/fullstack.yml +181 -0
- package/overlays/presets/microservice.yml +118 -0
- package/overlays/presets/web-api.yml +109 -0
- package/overlays/prometheus/.env.example +5 -0
- package/overlays/prometheus/README.md +1246 -0
- package/overlays/prometheus/devcontainer.patch.json +12 -0
- package/overlays/prometheus/docker-compose.yml +22 -0
- package/overlays/prometheus/overlay.yml +17 -0
- package/overlays/prometheus/prometheus.yml +12 -0
- package/overlays/prometheus/verify.sh +34 -0
- package/overlays/promtail/.env.example +2 -0
- package/overlays/promtail/README.md +357 -0
- package/overlays/promtail/devcontainer.patch.json +5 -0
- package/overlays/promtail/docker-compose.yml +16 -0
- package/overlays/promtail/overlay.yml +17 -0
- package/overlays/promtail/promtail-config.yaml +60 -0
- package/overlays/promtail/verify.sh +31 -0
- package/overlays/pulumi/README.md +472 -0
- package/overlays/pulumi/devcontainer.patch.json +13 -0
- package/overlays/pulumi/overlay.yml +14 -0
- package/overlays/pulumi/verify.sh +31 -0
- package/overlays/python/README.md +919 -0
- package/overlays/python/devcontainer.patch.json +41 -0
- package/overlays/python/overlay.yml +12 -0
- package/overlays/python/requirements-overlay.txt +13 -0
- package/overlays/python/setup.sh +47 -0
- package/overlays/python/verify.sh +32 -0
- package/overlays/rabbitmq/.env.example +7 -0
- package/overlays/rabbitmq/README.md +680 -0
- package/overlays/rabbitmq/devcontainer.patch.json +28 -0
- package/overlays/rabbitmq/docker-compose.yml +30 -0
- package/overlays/rabbitmq/overlay.yml +18 -0
- package/overlays/rabbitmq/verify.sh +41 -0
- package/overlays/redis/.env.example +4 -0
- package/overlays/redis/README.md +776 -0
- package/overlays/redis/devcontainer.patch.json +21 -0
- package/overlays/redis/docker-compose.yml +21 -0
- package/overlays/redis/overlay.yml +15 -0
- package/overlays/redis/verify.sh +41 -0
- package/overlays/redpanda/.env.example +10 -0
- package/overlays/redpanda/README.md +703 -0
- package/overlays/redpanda/devcontainer.patch.json +37 -0
- package/overlays/redpanda/docker-compose.yml +67 -0
- package/overlays/redpanda/overlay.yml +21 -0
- package/overlays/redpanda/verify.sh +48 -0
- package/overlays/rust/README.md +299 -0
- package/overlays/rust/devcontainer.patch.json +39 -0
- package/overlays/rust/overlay.yml +15 -0
- package/overlays/rust/setup.sh +36 -0
- package/overlays/rust/verify.sh +51 -0
- package/overlays/sqlite/README.md +584 -0
- package/overlays/sqlite/devcontainer.patch.json +14 -0
- package/overlays/sqlite/overlay.yml +15 -0
- package/overlays/sqlite/setup.sh +27 -0
- package/overlays/sqlite/verify.sh +43 -0
- package/overlays/sqlserver/.env.example +6 -0
- package/overlays/sqlserver/README.md +592 -0
- package/overlays/sqlserver/devcontainer.patch.json +22 -0
- package/overlays/sqlserver/docker-compose.yml +32 -0
- package/overlays/sqlserver/overlay.yml +17 -0
- package/overlays/sqlserver/verify.sh +30 -0
- package/overlays/tempo/.env.example +5 -0
- package/overlays/tempo/README.md +273 -0
- package/overlays/tempo/devcontainer.patch.json +12 -0
- package/overlays/tempo/docker-compose.yml +20 -0
- package/overlays/tempo/overlay.yml +20 -0
- package/overlays/tempo/tempo-config.yaml +32 -0
- package/overlays/tempo/verify.sh +31 -0
- package/overlays/terraform/README.md +389 -0
- package/overlays/terraform/devcontainer.patch.json +15 -0
- package/overlays/terraform/overlay.yml +14 -0
- package/overlays/terraform/verify.sh +63 -0
- package/package.json +74 -0
- package/templates/README.md +285 -0
- package/templates/compose/.devcontainer/devcontainer.json +46 -0
- package/templates/compose/.devcontainer/docker-compose.yml +12 -0
- package/templates/compose/README.md +20 -0
- package/templates/plain/.devcontainer/devcontainer.json +35 -0
- package/templates/plain/README.md +21 -0
- package/tool/README.md +281 -0
- package/tool/schema/base-images.schema.json +43 -0
- package/tool/schema/base-templates.schema.json +34 -0
- package/tool/schema/config.schema.json +71 -0
- 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
|