container-superposition 0.1.8 → 0.1.10
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 +3 -0
- package/dist/tool/cli/args.d.ts.map +1 -1
- package/dist/tool/cli/args.js +1 -1
- package/dist/tool/cli/args.js.map +1 -1
- package/dist/tool/commands/adopt.d.ts.map +1 -1
- package/dist/tool/commands/adopt.js +15 -21
- package/dist/tool/commands/adopt.js.map +1 -1
- package/dist/tool/commands/doctor.d.ts +1 -0
- package/dist/tool/commands/doctor.d.ts.map +1 -1
- package/dist/tool/commands/doctor.js +1370 -73
- package/dist/tool/commands/doctor.js.map +1 -1
- package/dist/tool/questionnaire/composer.d.ts +3 -1
- package/dist/tool/questionnaire/composer.d.ts.map +1 -1
- package/dist/tool/questionnaire/composer.js +273 -20
- package/dist/tool/questionnaire/composer.js.map +1 -1
- package/dist/tool/questionnaire/presets.d.ts.map +1 -1
- package/dist/tool/questionnaire/presets.js +1 -0
- package/dist/tool/questionnaire/presets.js.map +1 -1
- package/dist/tool/questionnaire/questionnaire.d.ts.map +1 -1
- package/dist/tool/questionnaire/questionnaire.js +3 -1
- package/dist/tool/questionnaire/questionnaire.js.map +1 -1
- package/dist/tool/schema/project-config.d.ts.map +1 -1
- package/dist/tool/schema/project-config.js +174 -1
- package/dist/tool/schema/project-config.js.map +1 -1
- package/dist/tool/schema/types.d.ts +53 -2
- package/dist/tool/schema/types.d.ts.map +1 -1
- package/docs/README.md +1 -0
- package/docs/overlays.md +188 -147
- package/docs/specs/001-verbose-plan-graph/spec.md +5 -12
- package/docs/specs/002-superposition-config-file/spec.md +5 -12
- package/docs/specs/003-mkdocs2-overlay/spec.md +2 -9
- package/docs/specs/004-doctor-fix/spec.md +1 -8
- package/docs/specs/005-cuda-overlay/spec.md +2 -9
- package/docs/specs/006-rocm-overlay/spec.md +3 -10
- package/docs/specs/007-target-aware-generation/spec.md +4 -11
- package/docs/specs/008-project-file-canonical/spec.md +7 -8
- package/docs/specs/009-project-env/spec.md +3 -10
- package/docs/specs/010-compose-env-materialization/spec.md +3 -10
- package/docs/specs/011-overlay-parameters/spec.md +2 -9
- package/docs/specs/012-ollama-cli-overlay/spec.md +47 -0
- package/docs/specs/013-doctor-dependency-check/spec.md +250 -0
- package/docs/specs/014-doctor-compose-port-cross-validation/spec.md +276 -0
- package/docs/specs/015-doctor-env-example-drift/spec.md +248 -0
- package/docs/specs/016-doctor-reproducibility-check/spec.md +276 -0
- package/docs/specs/017-doctor-dry-run/spec.md +276 -0
- package/docs/specs/{007-init-project-file → 018-init-project-file}/spec.md +2 -9
- package/docs/specs/019-project-mounts/spec.md +176 -0
- package/docs/specs/taxonomy.md +186 -0
- package/docs/superposition-yml.md +467 -0
- package/overlays/.presets/full-observability.yml +113 -0
- package/overlays/.presets/k8s-dev.yml +174 -0
- package/overlays/.presets/local-llm.yml +105 -0
- package/overlays/.presets/vector-ai.yml +150 -0
- package/overlays/.shared/vscode/js-ts-settings.json +19 -0
- package/overlays/.shared/vscode/markdown-extensions.json +8 -0
- package/overlays/alertmanager/devcontainer.patch.json +0 -1
- package/overlays/alertmanager/docker-compose.yml +8 -0
- package/overlays/alertmanager/overlay.yml +1 -0
- package/overlays/amp/devcontainer.patch.json +4 -1
- package/overlays/ansible/README.md +163 -0
- package/overlays/ansible/devcontainer.patch.json +14 -0
- package/overlays/ansible/overlay.yml +18 -0
- package/overlays/argocd/README.md +158 -0
- package/overlays/argocd/devcontainer.patch.json +9 -0
- package/overlays/argocd/overlay.yml +17 -0
- package/overlays/argocd/setup.sh +29 -0
- package/overlays/argocd/verify.sh +14 -0
- package/overlays/bun/devcontainer.patch.json +1 -10
- package/overlays/bun/overlay.yml +8 -1
- package/overlays/claude-code/devcontainer.patch.json +6 -1
- package/overlays/codex/devcontainer.patch.json +5 -0
- package/overlays/comfyui/docker-compose.yml +1 -0
- package/overlays/comfyui/overlay.yml +4 -0
- package/overlays/commitlint/devcontainer.patch.json +1 -6
- package/overlays/docker-sock/overlay.yml +1 -0
- package/overlays/dotnet/overlay.yml +4 -1
- package/overlays/fuseki/.env.example +5 -0
- package/overlays/fuseki/README.md +173 -0
- package/overlays/fuseki/devcontainer.patch.json +18 -0
- package/overlays/fuseki/docker-compose.yml +29 -0
- package/overlays/fuseki/overlay.yml +42 -0
- package/overlays/fuseki/verify.sh +58 -0
- package/overlays/gemini-cli/devcontainer.patch.json +4 -1
- package/overlays/go/overlay.yml +6 -1
- package/overlays/grafana/devcontainer.patch.json +0 -1
- package/overlays/grafana/docker-compose.yml +8 -2
- package/overlays/grafana/overlay.yml +6 -1
- package/overlays/jaeger/.env.example +11 -0
- package/overlays/jaeger/README.md +33 -4
- package/overlays/jaeger/devcontainer.patch.json +9 -1
- package/overlays/jaeger/docker-compose.yml +17 -0
- package/overlays/jaeger/overlay.yml +1 -12
- package/overlays/java/overlay.yml +6 -1
- package/overlays/jupyter/docker-compose.yml +1 -0
- package/overlays/jupyter/overlay.yml +1 -0
- package/overlays/keycloak/devcontainer.patch.json +0 -1
- package/overlays/keycloak/docker-compose.yml +1 -0
- package/overlays/keycloak/overlay.yml +15 -0
- package/overlays/localstack/docker-compose.yml +1 -0
- package/overlays/localstack/overlay.yml +19 -1
- package/overlays/loki/devcontainer.patch.json +0 -1
- package/overlays/loki/docker-compose.yml +8 -0
- package/overlays/loki/overlay.yml +1 -0
- package/overlays/mailpit/docker-compose.yml +1 -0
- package/overlays/mailpit/overlay.yml +1 -0
- package/overlays/minio/devcontainer.patch.json +1 -1
- package/overlays/minio/docker-compose.yml +1 -0
- package/overlays/minio/overlay.yml +23 -2
- package/overlays/mkdocs/devcontainer.patch.json +1 -5
- package/overlays/mkdocs/overlay.yml +3 -1
- package/overlays/mkdocs2/devcontainer.patch.json +1 -5
- package/overlays/mkdocs2/overlay.yml +2 -0
- package/overlays/mongodb/docker-compose.yml +2 -0
- package/overlays/mongodb/overlay.yml +26 -2
- package/overlays/mysql/docker-compose.yml +2 -0
- package/overlays/mysql/overlay.yml +36 -2
- package/overlays/nats/docker-compose.yml +1 -0
- package/overlays/nats/overlay.yml +18 -2
- package/overlays/nodejs/devcontainer.patch.json +1 -12
- package/overlays/nodejs/overlay.yml +8 -1
- package/overlays/ollama/README.md +4 -3
- package/overlays/ollama/docker-compose.yml +1 -0
- package/overlays/ollama/overlay.yml +6 -1
- package/overlays/ollama/verify.sh +5 -28
- package/overlays/ollama-cli/README.md +90 -0
- package/overlays/ollama-cli/devcontainer.patch.json +3 -0
- package/overlays/ollama-cli/overlay.yml +19 -0
- package/overlays/{ollama → ollama-cli}/setup.sh +7 -10
- package/overlays/ollama-cli/verify.sh +49 -0
- package/overlays/open-webui/docker-compose.yml +1 -0
- package/overlays/open-webui/overlay.yml +8 -1
- package/overlays/opencode/devcontainer.patch.json +4 -1
- package/overlays/otel-collector/README.md +4 -0
- package/overlays/otel-collector/devcontainer.patch.json +4 -1
- package/overlays/otel-collector/docker-compose.yml +8 -4
- package/overlays/otel-collector/overlay.yml +1 -0
- package/overlays/otel-demo-nodejs/devcontainer.patch.json +0 -1
- package/overlays/otel-demo-nodejs/docker-compose.yml +1 -0
- package/overlays/otel-demo-nodejs/overlay.yml +9 -1
- package/overlays/otel-demo-python/devcontainer.patch.json +0 -1
- package/overlays/otel-demo-python/docker-compose.yml +1 -0
- package/overlays/otel-demo-python/overlay.yml +6 -1
- package/overlays/pandoc/README.md +10 -0
- package/overlays/pandoc/devcontainer.patch.json +0 -5
- package/overlays/pandoc/overlay.yml +2 -0
- package/overlays/pandoc/setup.sh +10 -0
- package/overlays/pgvector/devcontainer.patch.json +11 -5
- package/overlays/pgvector/docker-compose.yml +1 -0
- package/overlays/pgvector/overlay.yml +3 -0
- package/overlays/playwright/devcontainer.patch.json +0 -5
- package/overlays/playwright/overlay.yml +2 -1
- package/overlays/postgres/docker-compose.yml +1 -0
- package/overlays/postgres/overlay.yml +4 -1
- package/overlays/pre-commit/devcontainer.patch.json +1 -7
- package/overlays/prometheus/devcontainer.patch.json +0 -1
- package/overlays/prometheus/docker-compose.yml +8 -0
- package/overlays/prometheus/overlay.yml +1 -0
- package/overlays/promtail/devcontainer.patch.json +1 -2
- package/overlays/promtail/docker-compose.yml +8 -0
- package/overlays/promtail/overlay.yml +1 -0
- package/overlays/qdrant/docker-compose.yml +1 -0
- package/overlays/qdrant/overlay.yml +5 -1
- package/overlays/rabbitmq/docker-compose.yml +1 -0
- package/overlays/rabbitmq/overlay.yml +25 -2
- package/overlays/redis/docker-compose.yml +7 -0
- package/overlays/redis/overlay.yml +15 -1
- package/overlays/redpanda/docker-compose.yml +1 -0
- package/overlays/redpanda/overlay.yml +15 -3
- package/overlays/rocm/overlay.yml +2 -1
- package/overlays/rust/overlay.yml +3 -1
- package/overlays/sqlserver/docker-compose.yml +1 -0
- package/overlays/sqlserver/overlay.yml +17 -0
- package/overlays/task/README.md +47 -0
- package/overlays/task/devcontainer.patch.json +9 -0
- package/overlays/task/overlay.yml +16 -0
- package/overlays/task/setup.sh +29 -0
- package/overlays/task/verify.sh +14 -0
- package/overlays/tempo/devcontainer.patch.json +0 -1
- package/overlays/tempo/docker-compose.yml +8 -0
- package/overlays/tempo/overlay.yml +1 -0
- package/overlays/windsurf-cli/devcontainer.patch.json +4 -1
- package/package.json +1 -1
- package/tool/schema/config.schema.json +74 -1
- package/overlays/.shared/otel/otel-base-config.yaml +0 -30
|
@@ -606,7 +606,7 @@ function validateImportPath(importPath, overlaysDir) {
|
|
|
606
606
|
/**
|
|
607
607
|
* Load and resolve imports from shared files for an overlay
|
|
608
608
|
*/
|
|
609
|
-
function loadImportsForOverlay(overlayName, overlaysDir) {
|
|
609
|
+
function loadImportsForOverlay(overlayName, overlaysDir, silent = false) {
|
|
610
610
|
let importedConfig = {};
|
|
611
611
|
// Load overlay manifest to get imports
|
|
612
612
|
const overlayDir = path.join(overlaysDir, overlayName);
|
|
@@ -638,14 +638,16 @@ function loadImportsForOverlay(overlayName, overlaysDir) {
|
|
|
638
638
|
const ext = path.extname(importPath).toLowerCase();
|
|
639
639
|
if (ext === '.json') {
|
|
640
640
|
// JSON files are merged as devcontainer patches
|
|
641
|
-
|
|
641
|
+
if (!silent)
|
|
642
|
+
console.log(chalk.dim(` 📎 Applying shared import: ${importPath}`));
|
|
642
643
|
const importedPatch = loadJson(fullImportPath);
|
|
643
644
|
importedConfig = deepMerge(importedConfig, importedPatch);
|
|
644
645
|
}
|
|
645
646
|
else if (ext === '.yaml' || ext === '.yml') {
|
|
646
647
|
// YAML files are loaded and merged as devcontainer patches
|
|
647
648
|
try {
|
|
648
|
-
|
|
649
|
+
if (!silent)
|
|
650
|
+
console.log(chalk.dim(` 📎 Applying shared import: ${importPath}`));
|
|
649
651
|
const yamlContent = fs.readFileSync(fullImportPath, 'utf8');
|
|
650
652
|
const importedPatch = yaml.load(yamlContent);
|
|
651
653
|
if (importedPatch && typeof importedPatch === 'object') {
|
|
@@ -658,7 +660,8 @@ function loadImportsForOverlay(overlayName, overlaysDir) {
|
|
|
658
660
|
}
|
|
659
661
|
else if (ext === '.env') {
|
|
660
662
|
// .env files are handled separately during env merging — skip here
|
|
661
|
-
|
|
663
|
+
if (!silent)
|
|
664
|
+
console.log(chalk.dim(` 📎 Shared .env import noted: ${importPath}`));
|
|
662
665
|
}
|
|
663
666
|
else {
|
|
664
667
|
// FR-007: Unsupported file types are errors
|
|
@@ -679,14 +682,16 @@ function loadImportsForOverlay(overlayName, overlaysDir) {
|
|
|
679
682
|
/**
|
|
680
683
|
* Apply an overlay to the base configuration
|
|
681
684
|
*/
|
|
682
|
-
export function applyOverlay(baseConfig, overlayName, overlaysDir) {
|
|
685
|
+
export function applyOverlay(baseConfig, overlayName, overlaysDir, options = {}) {
|
|
686
|
+
const { silent = false } = options;
|
|
683
687
|
const overlayPath = path.join(overlaysDir, overlayName, 'devcontainer.patch.json');
|
|
684
688
|
if (!fs.existsSync(overlayPath)) {
|
|
685
|
-
|
|
689
|
+
if (!silent)
|
|
690
|
+
console.warn(chalk.yellow(`⚠️ Overlay not found: ${overlayName}`));
|
|
686
691
|
return baseConfig;
|
|
687
692
|
}
|
|
688
693
|
// First, load and apply any imports
|
|
689
|
-
const importedConfig = loadImportsForOverlay(overlayName, overlaysDir);
|
|
694
|
+
const importedConfig = loadImportsForOverlay(overlayName, overlaysDir, silent);
|
|
690
695
|
if (Object.keys(importedConfig).length > 0) {
|
|
691
696
|
baseConfig = deepMerge(baseConfig, importedConfig);
|
|
692
697
|
}
|
|
@@ -728,8 +733,36 @@ class FileRegistry {
|
|
|
728
733
|
}
|
|
729
734
|
}
|
|
730
735
|
/**
|
|
731
|
-
*
|
|
732
|
-
*
|
|
736
|
+
* Recursively remove stale files within a registered subdirectory.
|
|
737
|
+
* Called for directories that ARE in the registry but may contain files from
|
|
738
|
+
* a previous run that are no longer part of the current generation (e.g.
|
|
739
|
+
* scripts/setup-rabbitmq.sh after rabbitmq was removed from the project).
|
|
740
|
+
* Returns the number of files removed.
|
|
741
|
+
*/
|
|
742
|
+
function cleanupStaleDirFiles(dirPath, prefix, expectedFiles) {
|
|
743
|
+
let removed = 0;
|
|
744
|
+
const entries = fs.readdirSync(dirPath);
|
|
745
|
+
for (const entry of entries) {
|
|
746
|
+
const entryPath = path.join(dirPath, entry);
|
|
747
|
+
const stat = fs.statSync(entryPath);
|
|
748
|
+
if (stat.isDirectory()) {
|
|
749
|
+
removed += cleanupStaleDirFiles(entryPath, `${prefix}${entry}/`, expectedFiles);
|
|
750
|
+
}
|
|
751
|
+
else {
|
|
752
|
+
const registryKey = `${prefix}${entry}`;
|
|
753
|
+
if (!expectedFiles.has(registryKey)) {
|
|
754
|
+
fs.unlinkSync(entryPath);
|
|
755
|
+
removed++;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return removed;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Clean up stale files from previous runs.
|
|
763
|
+
* Removes anything not in the registry (except preserved files like superposition.json).
|
|
764
|
+
* Also recurses into registered subdirectories to remove individual stale files within
|
|
765
|
+
* them — e.g. scripts/setup-rabbitmq.sh after rabbitmq is removed from the project.
|
|
733
766
|
*/
|
|
734
767
|
function cleanupStaleFiles(outputPath, registry) {
|
|
735
768
|
if (!fs.existsSync(outputPath)) {
|
|
@@ -753,11 +786,16 @@ function cleanupStaleFiles(outputPath, registry) {
|
|
|
753
786
|
if (preservedDirs.has(entry)) {
|
|
754
787
|
continue;
|
|
755
788
|
}
|
|
756
|
-
// Remove directory if not in registry
|
|
757
789
|
if (!expectedDirs.has(entry)) {
|
|
790
|
+
// Remove directory entirely — nothing inside belongs to this run
|
|
758
791
|
fs.rmSync(entryPath, { recursive: true, force: true });
|
|
759
792
|
removedCount++;
|
|
760
793
|
}
|
|
794
|
+
else {
|
|
795
|
+
// Directory is still expected, but individual files inside it may be stale
|
|
796
|
+
// (e.g. scripts/setup-rabbitmq.sh after rabbitmq was removed)
|
|
797
|
+
removedCount += cleanupStaleDirFiles(entryPath, `${entry}/`, expectedFiles);
|
|
798
|
+
}
|
|
761
799
|
}
|
|
762
800
|
else {
|
|
763
801
|
// Remove file if not in registry
|
|
@@ -790,6 +828,23 @@ function copyDir(src, dest) {
|
|
|
790
828
|
}
|
|
791
829
|
}
|
|
792
830
|
}
|
|
831
|
+
/**
|
|
832
|
+
* Recursively register every file inside a directory in the FileRegistry.
|
|
833
|
+
* Used after copyDir() to ensure cleanup logic doesn't delete the copied contents.
|
|
834
|
+
*/
|
|
835
|
+
function registerDirContents(registry, dirPath, prefix) {
|
|
836
|
+
if (!fs.existsSync(dirPath))
|
|
837
|
+
return;
|
|
838
|
+
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
|
839
|
+
const rel = `${prefix}${entry.name}`;
|
|
840
|
+
if (entry.isDirectory()) {
|
|
841
|
+
registerDirContents(registry, path.join(dirPath, entry.name), `${rel}/`);
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
registry.addFile(rel);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
793
848
|
/**
|
|
794
849
|
* Copy additional files from overlay to output directory
|
|
795
850
|
* Excludes devcontainer.patch.json and .env.example (handled separately)
|
|
@@ -832,6 +887,9 @@ function copyOverlayFiles(outputPath, overlayName, registry, overlaysDir) {
|
|
|
832
887
|
const destPath = path.join(outputPath, destDirName);
|
|
833
888
|
copyDir(srcPath, destPath);
|
|
834
889
|
registry.addDirectory(destDirName);
|
|
890
|
+
// Register every file inside the copied directory so that
|
|
891
|
+
// cleanupStaleDirFiles does not delete them during the same run.
|
|
892
|
+
registerDirContents(registry, destPath, `${destDirName}/`);
|
|
835
893
|
copiedFiles++;
|
|
836
894
|
}
|
|
837
895
|
}
|
|
@@ -946,6 +1004,178 @@ function applyProjectEnvToDevcontainer(config, projectEnv, stack, rootEnv) {
|
|
|
946
1004
|
console.log(chalk.dim(` 🌱 Applying project env to remoteEnv`));
|
|
947
1005
|
return deepMerge(config, { remoteEnv });
|
|
948
1006
|
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Resolve the abstract `ProjectMountTarget` to a concrete destination.
|
|
1009
|
+
*
|
|
1010
|
+
* - `devcontainerMount` — always routes to `devcontainer.json mounts[]`
|
|
1011
|
+
* - `composeVolume` — always routes to `docker-compose.yml services.devcontainer.volumes[]`;
|
|
1012
|
+
* throws if the stack is not `compose` (no docker-compose.yml is generated for plain stacks)
|
|
1013
|
+
* - `auto` (default) — always routes to `devcontainer.json mounts[]` regardless of stack,
|
|
1014
|
+
* so that the same `superposition.yml` works unchanged when swapping `stack: plain` ↔ `stack: compose`
|
|
1015
|
+
*
|
|
1016
|
+
* @throws {Error} When `composeVolume` is requested on a non-compose stack
|
|
1017
|
+
*/
|
|
1018
|
+
function resolveProjectMountTarget(mount, stack) {
|
|
1019
|
+
const target = mount.target ?? 'auto';
|
|
1020
|
+
if (target === 'composeVolume') {
|
|
1021
|
+
if (stack !== 'compose') {
|
|
1022
|
+
throw new Error('Project mount target "composeVolume" requires stack: compose because no docker-compose.yml is generated for plain stacks');
|
|
1023
|
+
}
|
|
1024
|
+
return 'composeVolume';
|
|
1025
|
+
}
|
|
1026
|
+
// Both 'auto' (default) and explicit 'devcontainerMount' are treated identically:
|
|
1027
|
+
// always route to devcontainer.json mounts[] regardless of stack
|
|
1028
|
+
return 'devcontainerMount';
|
|
1029
|
+
}
|
|
1030
|
+
function boolToReadonlyFlag(value) {
|
|
1031
|
+
return value ? ',readonly' : '';
|
|
1032
|
+
}
|
|
1033
|
+
function buildConsistencyValue(mount) {
|
|
1034
|
+
if (mount.cached) {
|
|
1035
|
+
return 'cached';
|
|
1036
|
+
}
|
|
1037
|
+
return mount.consistency;
|
|
1038
|
+
}
|
|
1039
|
+
function toDevcontainerMountSpec(mount) {
|
|
1040
|
+
if (mount.value) {
|
|
1041
|
+
return mount.value;
|
|
1042
|
+
}
|
|
1043
|
+
const source = mount.source?.trim();
|
|
1044
|
+
const destination = mount.destination?.trim();
|
|
1045
|
+
if (!source || !destination) {
|
|
1046
|
+
throw new Error('Structured project mounts require both "source" and "destination" when no raw "value" is provided');
|
|
1047
|
+
}
|
|
1048
|
+
const type = mount.type ?? 'bind';
|
|
1049
|
+
const consistency = buildConsistencyValue(mount);
|
|
1050
|
+
return `source=${source},target=${destination},type=${type}${consistency ? `,consistency=${consistency}` : ''}${boolToReadonlyFlag(mount.readOnly)}`;
|
|
1051
|
+
}
|
|
1052
|
+
function toComposeVolumeSpec(mount) {
|
|
1053
|
+
if (mount.value) {
|
|
1054
|
+
return mount.value;
|
|
1055
|
+
}
|
|
1056
|
+
const source = mount.source?.trim();
|
|
1057
|
+
const destination = mount.destination?.trim();
|
|
1058
|
+
if (!source || !destination) {
|
|
1059
|
+
throw new Error('Structured project mounts require both "source" and "destination" when no raw "value" is provided');
|
|
1060
|
+
}
|
|
1061
|
+
const options = [];
|
|
1062
|
+
if (mount.readOnly) {
|
|
1063
|
+
options.push('ro');
|
|
1064
|
+
}
|
|
1065
|
+
const consistency = buildConsistencyValue(mount);
|
|
1066
|
+
if (consistency) {
|
|
1067
|
+
options.push(consistency);
|
|
1068
|
+
}
|
|
1069
|
+
return options.length > 0
|
|
1070
|
+
? `${source}:${destination}:${options.join(',')}`
|
|
1071
|
+
: `${source}:${destination}`;
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Merge project mounts destined for `devcontainer.json` into the devcontainer config.
|
|
1075
|
+
*
|
|
1076
|
+
* Filters the `projectMounts` list to entries whose resolved target is `devcontainerMount`
|
|
1077
|
+
* (i.e. explicit `target: devcontainerMount`, or `target: auto`), then
|
|
1078
|
+
* deepMerge-concatenates their values into `config.mounts[]`. Returns early unchanged if
|
|
1079
|
+
* no applicable mounts exist.
|
|
1080
|
+
*/
|
|
1081
|
+
function applyProjectMountsToDevcontainer(config, projectMounts, stack) {
|
|
1082
|
+
if (!projectMounts?.length) {
|
|
1083
|
+
return config;
|
|
1084
|
+
}
|
|
1085
|
+
const devcontainerMounts = projectMounts
|
|
1086
|
+
.filter((m) => resolveProjectMountTarget(m, stack) === 'devcontainerMount')
|
|
1087
|
+
.map(toDevcontainerMountSpec);
|
|
1088
|
+
if (devcontainerMounts.length === 0) {
|
|
1089
|
+
return config;
|
|
1090
|
+
}
|
|
1091
|
+
console.log(chalk.dim(` 🗂️ Applying project mounts to devcontainer.json`));
|
|
1092
|
+
return deepMerge(config, { mounts: devcontainerMounts });
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Extract mount values destined for `docker-compose.yml services.devcontainer.volumes[]`.
|
|
1096
|
+
*
|
|
1097
|
+
* Returns the raw value strings for all mounts whose resolved target is `composeVolume`
|
|
1098
|
+
* (i.e. explicit `target: composeVolume` only). The caller is responsible for merging the
|
|
1099
|
+
* returned array into the compose service definition.
|
|
1100
|
+
*/
|
|
1101
|
+
function buildComposeProjectMountVolumes(projectMounts, stack) {
|
|
1102
|
+
if (!projectMounts?.length) {
|
|
1103
|
+
return [];
|
|
1104
|
+
}
|
|
1105
|
+
return projectMounts
|
|
1106
|
+
.filter((m) => resolveProjectMountTarget(m, stack) === 'composeVolume')
|
|
1107
|
+
.map(toComposeVolumeSpec);
|
|
1108
|
+
}
|
|
1109
|
+
function quoteShellSingle(value) {
|
|
1110
|
+
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
1111
|
+
}
|
|
1112
|
+
function applyProjectShellConfig(config, projectShell, outputPath, fileRegistry) {
|
|
1113
|
+
if (!projectShell) {
|
|
1114
|
+
return config;
|
|
1115
|
+
}
|
|
1116
|
+
const aliases = projectShell.aliases ?? {};
|
|
1117
|
+
const snippets = projectShell.snippets ?? [];
|
|
1118
|
+
if (Object.keys(aliases).length === 0 && snippets.length === 0) {
|
|
1119
|
+
return config;
|
|
1120
|
+
}
|
|
1121
|
+
const scriptsDir = path.join(outputPath, 'scripts');
|
|
1122
|
+
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
1123
|
+
fileRegistry.addDirectory('scripts');
|
|
1124
|
+
const shellInitPath = path.join(scriptsDir, 'shell-init.sh');
|
|
1125
|
+
const aliasLines = Object.entries(aliases)
|
|
1126
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
1127
|
+
.map(([name, cmd]) => `alias ${name}=${quoteShellSingle(cmd)}`);
|
|
1128
|
+
const shellInit = [
|
|
1129
|
+
'#!/usr/bin/env bash',
|
|
1130
|
+
'# Generated by container-superposition from superposition.yml:shell',
|
|
1131
|
+
...aliasLines,
|
|
1132
|
+
...(snippets.length > 0 ? ['', ...snippets] : []),
|
|
1133
|
+
'',
|
|
1134
|
+
].join('\n');
|
|
1135
|
+
fs.writeFileSync(shellInitPath, shellInit, 'utf8');
|
|
1136
|
+
fileRegistry.addFile('scripts/shell-init.sh');
|
|
1137
|
+
const hookScriptPath = path.join(scriptsDir, 'setup-project-shell.sh');
|
|
1138
|
+
const hookScript = `#!/usr/bin/env bash
|
|
1139
|
+
set -e
|
|
1140
|
+
BEGIN_MARKER="# >>> container-superposition shell >>>"
|
|
1141
|
+
END_MARKER="# <<< container-superposition shell <<<"
|
|
1142
|
+
DEVCONTAINER_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
|
|
1143
|
+
SHELL_INIT_FILE="\${DEVCONTAINER_DIR}/scripts/shell-init.sh"
|
|
1144
|
+
|
|
1145
|
+
install_hook() {
|
|
1146
|
+
local rc_file="$1"
|
|
1147
|
+
touch "$rc_file"
|
|
1148
|
+
local tmp_file
|
|
1149
|
+
tmp_file="$(mktemp)"
|
|
1150
|
+
awk -v b="$BEGIN_MARKER" -v e="$END_MARKER" '
|
|
1151
|
+
$0==b {skip=1; next}
|
|
1152
|
+
$0==e {skip=0; next}
|
|
1153
|
+
!skip {print}
|
|
1154
|
+
' "$rc_file" > "$tmp_file"
|
|
1155
|
+
cat >> "$tmp_file" <<EOF
|
|
1156
|
+
$BEGIN_MARKER
|
|
1157
|
+
[ -f "$SHELL_INIT_FILE" ] && source "$SHELL_INIT_FILE"
|
|
1158
|
+
$END_MARKER
|
|
1159
|
+
EOF
|
|
1160
|
+
mv "$tmp_file" "$rc_file"
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
install_hook "$HOME/.bashrc"
|
|
1164
|
+
install_hook "$HOME/.zshrc"
|
|
1165
|
+
`;
|
|
1166
|
+
fs.writeFileSync(hookScriptPath, hookScript, { mode: 0o755 });
|
|
1167
|
+
fileRegistry.addFile('scripts/setup-project-shell.sh');
|
|
1168
|
+
if (!config.postCreateCommand) {
|
|
1169
|
+
config.postCreateCommand = {};
|
|
1170
|
+
}
|
|
1171
|
+
if (typeof config.postCreateCommand === 'string') {
|
|
1172
|
+
config.postCreateCommand = { default: config.postCreateCommand };
|
|
1173
|
+
}
|
|
1174
|
+
config.postCreateCommand['setup-project-shell'] =
|
|
1175
|
+
'bash .devcontainer/scripts/setup-project-shell.sh';
|
|
1176
|
+
console.log(chalk.dim(` 🐚 Applying project shell aliases/snippets`));
|
|
1177
|
+
return config;
|
|
1178
|
+
}
|
|
949
1179
|
function mergeComposeEnvFile(outputPath, entries) {
|
|
950
1180
|
if (Object.keys(entries).length === 0) {
|
|
951
1181
|
return false;
|
|
@@ -1269,7 +1499,7 @@ function resolveDockerComposePortConflicts(services) {
|
|
|
1269
1499
|
/**
|
|
1270
1500
|
* Merge docker-compose.yml files from base and overlays into a single file
|
|
1271
1501
|
*/
|
|
1272
|
-
function mergeDockerComposeFiles(outputPath, baseStack, overlays, overlaysDir, portOffset, customImage, projectEnv) {
|
|
1502
|
+
function mergeDockerComposeFiles(outputPath, baseStack, overlays, overlaysDir, portOffset, customImage, projectEnv, projectMounts) {
|
|
1273
1503
|
const composeFiles = [];
|
|
1274
1504
|
// Add base docker-compose if exists
|
|
1275
1505
|
const baseComposePath = path.join(TEMPLATES_DIR, baseStack, '.devcontainer', 'docker-compose.yml');
|
|
@@ -1353,6 +1583,16 @@ function mergeDockerComposeFiles(outputPath, baseStack, overlays, overlaysDir, p
|
|
|
1353
1583
|
merged.services.devcontainer.environment = deepMerge(merged.services.devcontainer.environment ?? {}, composeEnv);
|
|
1354
1584
|
console.log(chalk.dim(` 🌱 Applying project env to docker-compose devcontainer service`));
|
|
1355
1585
|
}
|
|
1586
|
+
const composeMountVolumes = buildComposeProjectMountVolumes(projectMounts, baseStack);
|
|
1587
|
+
if (composeMountVolumes.length > 0) {
|
|
1588
|
+
const existing = Array.isArray(merged.services.devcontainer.volumes)
|
|
1589
|
+
? merged.services.devcontainer.volumes
|
|
1590
|
+
: [];
|
|
1591
|
+
merged.services.devcontainer.volumes = [
|
|
1592
|
+
...new Set([...existing, ...composeMountVolumes]),
|
|
1593
|
+
];
|
|
1594
|
+
console.log(chalk.dim(` 🗂️ Applying project mounts to docker-compose devcontainer service`));
|
|
1595
|
+
}
|
|
1356
1596
|
if (customImage) {
|
|
1357
1597
|
// Apply custom base image if specified
|
|
1358
1598
|
merged.services.devcontainer.image = customImage;
|
|
@@ -1776,6 +2016,7 @@ export async function composeDevContainer(answers, overlaysDir, options = {}) {
|
|
|
1776
2016
|
config = applyOverlay(config, overlay, actualOverlaysDir);
|
|
1777
2017
|
}
|
|
1778
2018
|
config = applyProjectEnvToDevcontainer(config, answers.projectEnv, answers.stack, rootEnv);
|
|
2019
|
+
config = applyProjectMountsToDevcontainer(config, answers.projectMounts, answers.stack);
|
|
1779
2020
|
// 7. Copy template files (docker-compose, scripts, etc.)
|
|
1780
2021
|
const entries = fs.readdirSync(templatePath);
|
|
1781
2022
|
for (const entry of entries) {
|
|
@@ -1799,11 +2040,19 @@ export async function composeDevContainer(answers, overlaysDir, options = {}) {
|
|
|
1799
2040
|
}
|
|
1800
2041
|
// 8.5. Copy cross-distro-packages feature if used
|
|
1801
2042
|
if (config.features?.['./features/cross-distro-packages']) {
|
|
1802
|
-
const
|
|
1803
|
-
const
|
|
2043
|
+
const featureName = 'cross-distro-packages';
|
|
2044
|
+
const featuresDir = path.join(outputPath, 'features', featureName);
|
|
2045
|
+
const sourceFeatureDir = path.join(REPO_ROOT, 'features', featureName);
|
|
1804
2046
|
if (fs.existsSync(sourceFeatureDir)) {
|
|
1805
2047
|
copyDir(sourceFeatureDir, featuresDir);
|
|
1806
2048
|
fileRegistry.addDirectory('features');
|
|
2049
|
+
// Register every file inside the feature so cleanupStaleDirFiles
|
|
2050
|
+
// does not remove them when it recurses into the 'features' directory.
|
|
2051
|
+
for (const f of fs.readdirSync(sourceFeatureDir)) {
|
|
2052
|
+
if (fs.statSync(path.join(sourceFeatureDir, f)).isFile()) {
|
|
2053
|
+
fileRegistry.addFile(`features/${featureName}/${f}`);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
1807
2056
|
console.log(chalk.dim(` 📦 Copied cross-distro-packages feature`));
|
|
1808
2057
|
}
|
|
1809
2058
|
}
|
|
@@ -1815,7 +2064,7 @@ export async function composeDevContainer(answers, overlaysDir, options = {}) {
|
|
|
1815
2064
|
let composePortRemappings = [];
|
|
1816
2065
|
if (answers.stack === 'compose') {
|
|
1817
2066
|
const customImage = config._customImage;
|
|
1818
|
-
composePortRemappings = mergeDockerComposeFiles(outputPath, answers.stack, overlays, actualOverlaysDir, answers.portOffset, customImage, answers.projectEnv);
|
|
2067
|
+
composePortRemappings = mergeDockerComposeFiles(outputPath, answers.stack, overlays, actualOverlaysDir, answers.portOffset, customImage, answers.projectEnv, answers.projectMounts);
|
|
1819
2068
|
// Update devcontainer.json to reference the combined file
|
|
1820
2069
|
if (config.dockerComposeFile) {
|
|
1821
2070
|
config.dockerComposeFile = 'docker-compose.yml';
|
|
@@ -1838,6 +2087,7 @@ export async function composeDevContainer(answers, overlaysDir, options = {}) {
|
|
|
1838
2087
|
}
|
|
1839
2088
|
// Merge setup scripts from overlays into postCreateCommand
|
|
1840
2089
|
mergeSetupScripts(config, overlays, outputPath, fileRegistry, actualOverlaysDir);
|
|
2090
|
+
config = applyProjectShellConfig(config, answers.projectShell, outputPath, fileRegistry);
|
|
1841
2091
|
// 10. Apply custom patches from .devcontainer/custom/ (if present)
|
|
1842
2092
|
const customPatches = loadCustomPatches(outputPath);
|
|
1843
2093
|
if (customPatches) {
|
|
@@ -2155,15 +2405,14 @@ function applyPortOffsetToDevcontainer(config, offset) {
|
|
|
2155
2405
|
function mergeSetupScripts(config, overlays, outputPath, fileRegistry, overlaysDir) {
|
|
2156
2406
|
const setupScripts = [];
|
|
2157
2407
|
const verifyScripts = [];
|
|
2158
|
-
// Create scripts subfolder
|
|
2159
2408
|
const scriptsDir = path.join(outputPath, 'scripts');
|
|
2160
|
-
|
|
2161
|
-
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
2162
|
-
}
|
|
2163
|
-
// Add scripts directory to registry if any scripts will be added
|
|
2409
|
+
// Only create the scripts directory (and register it) if at least one overlay needs it
|
|
2164
2410
|
const hasScripts = overlays.some((o) => fs.existsSync(path.join(overlaysDir, o, 'setup.sh')) ||
|
|
2165
2411
|
fs.existsSync(path.join(overlaysDir, o, 'verify.sh')));
|
|
2166
2412
|
if (hasScripts) {
|
|
2413
|
+
if (!fs.existsSync(scriptsDir)) {
|
|
2414
|
+
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
2415
|
+
}
|
|
2167
2416
|
fileRegistry.addDirectory('scripts');
|
|
2168
2417
|
// Emit shared setup utilities so overlay scripts can source them
|
|
2169
2418
|
const setupUtilsSrc = path.join(TEMPLATES_DIR, 'scripts', 'setup-utils.sh');
|
|
@@ -2291,7 +2540,11 @@ function mergeRunServices(config, overlays, overlaysDir) {
|
|
|
2291
2540
|
if (fs.existsSync(overlayPath)) {
|
|
2292
2541
|
const overlayConfig = loadJson(overlayPath);
|
|
2293
2542
|
if (overlayConfig.runServices) {
|
|
2294
|
-
const
|
|
2543
|
+
const manifestPath = path.join(overlaysDir, overlay, 'overlay.yml');
|
|
2544
|
+
const manifest = fs.existsSync(manifestPath)
|
|
2545
|
+
? yaml.load(fs.readFileSync(manifestPath, 'utf8'))
|
|
2546
|
+
: null;
|
|
2547
|
+
const order = manifest?.serviceOrder ?? 0;
|
|
2295
2548
|
for (const service of overlayConfig.runServices) {
|
|
2296
2549
|
services.push({ name: service, order });
|
|
2297
2550
|
}
|