container-superposition 0.1.1 ā 0.1.4
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 +569 -8
- package/dist/scripts/init.js +436 -254
- package/dist/scripts/init.js.map +1 -1
- package/dist/tool/commands/doctor.d.ts +15 -0
- package/dist/tool/commands/doctor.d.ts.map +1 -0
- package/dist/tool/commands/doctor.js +862 -0
- package/dist/tool/commands/doctor.js.map +1 -0
- package/dist/tool/commands/explain.d.ts +13 -0
- package/dist/tool/commands/explain.d.ts.map +1 -0
- package/dist/tool/commands/explain.js +299 -0
- package/dist/tool/commands/explain.js.map +1 -0
- package/dist/tool/commands/list.d.ts +16 -0
- package/dist/tool/commands/list.d.ts.map +1 -0
- package/dist/tool/commands/list.js +121 -0
- package/dist/tool/commands/list.js.map +1 -0
- package/dist/tool/commands/plan.d.ts +67 -0
- package/dist/tool/commands/plan.d.ts.map +1 -0
- package/dist/tool/commands/plan.js +851 -0
- package/dist/tool/commands/plan.js.map +1 -0
- package/dist/tool/questionnaire/composer.d.ts +16 -2
- package/dist/tool/questionnaire/composer.d.ts.map +1 -1
- package/dist/tool/questionnaire/composer.js +411 -200
- package/dist/tool/questionnaire/composer.js.map +1 -1
- package/dist/tool/readme/markdown-parser.d.ts.map +1 -1
- package/dist/tool/readme/markdown-parser.js.map +1 -1
- package/dist/tool/readme/readme-generator.d.ts.map +1 -1
- package/dist/tool/readme/readme-generator.js +11 -6
- package/dist/tool/readme/readme-generator.js.map +1 -1
- package/dist/tool/schema/deployment-targets.d.ts +77 -0
- package/dist/tool/schema/deployment-targets.d.ts.map +1 -0
- package/dist/tool/schema/deployment-targets.js +91 -0
- package/dist/tool/schema/deployment-targets.js.map +1 -0
- package/dist/tool/schema/manifest-migrations.d.ts +51 -0
- package/dist/tool/schema/manifest-migrations.d.ts.map +1 -0
- package/dist/tool/schema/manifest-migrations.js +159 -0
- package/dist/tool/schema/manifest-migrations.js.map +1 -0
- package/dist/tool/schema/overlay-loader.d.ts +1 -1
- package/dist/tool/schema/overlay-loader.d.ts.map +1 -1
- package/dist/tool/schema/overlay-loader.js +42 -14
- package/dist/tool/schema/overlay-loader.js.map +1 -1
- package/dist/tool/schema/types.d.ts +62 -2
- package/dist/tool/schema/types.d.ts.map +1 -1
- package/dist/tool/utils/gitignore.d.ts +15 -0
- package/dist/tool/utils/gitignore.d.ts.map +1 -0
- package/dist/tool/utils/gitignore.js +41 -0
- package/dist/tool/utils/gitignore.js.map +1 -0
- package/dist/tool/utils/merge.d.ts +134 -0
- package/dist/tool/utils/merge.d.ts.map +1 -0
- package/dist/tool/utils/merge.js +277 -0
- package/dist/tool/utils/merge.js.map +1 -0
- package/dist/tool/utils/port-utils.d.ts +29 -0
- package/dist/tool/utils/port-utils.d.ts.map +1 -0
- package/dist/tool/utils/port-utils.js +128 -0
- package/dist/tool/utils/port-utils.js.map +1 -0
- package/dist/tool/utils/services-export.d.ts +14 -0
- package/dist/tool/utils/services-export.d.ts.map +1 -0
- package/dist/tool/utils/services-export.js +478 -0
- package/dist/tool/utils/services-export.js.map +1 -0
- package/dist/tool/utils/summary.d.ts +69 -0
- package/dist/tool/utils/summary.d.ts.map +1 -0
- package/dist/tool/utils/summary.js +260 -0
- package/dist/tool/utils/summary.js.map +1 -0
- package/dist/tool/utils/version.d.ts +9 -0
- package/dist/tool/utils/version.d.ts.map +1 -0
- package/dist/tool/utils/version.js +32 -0
- package/dist/tool/utils/version.js.map +1 -0
- package/docs/architecture.md +25 -21
- package/docs/deployment-targets.md +150 -0
- package/docs/discovery-commands.md +442 -0
- package/docs/merge-strategy.md +700 -0
- package/docs/minimal-and-editor.md +265 -0
- package/docs/overlay-imports.md +209 -0
- package/docs/overlay-manifest-refactoring.md +2 -2
- package/docs/overlay-metadata-archive.md +1 -1
- package/docs/overlays.md +139 -28
- package/docs/presets-architecture.md +3 -3
- package/docs/presets.md +1 -1
- package/docs/publishing.md +36 -35
- package/docs/team-workflow.md +540 -0
- package/overlays/.presets/data-engineering.yml +392 -0
- package/overlays/.presets/event-sourced-service.yml +262 -0
- package/overlays/.presets/frontend.yml +287 -0
- package/overlays/.presets/k8s-operator-dev.yml +462 -0
- package/overlays/{presets ā .presets}/microservice.yml +32 -6
- package/overlays/.presets/web-api.yml +129 -0
- package/overlays/.registry/README.md +1 -1
- package/overlays/.registry/deployment-targets.yml +54 -0
- package/overlays/.shared/README.md +43 -0
- package/overlays/.shared/compose/common-healthchecks.yml +38 -0
- package/overlays/.shared/otel/instrumentation.env +20 -0
- package/overlays/.shared/otel/otel-base-config.yaml +30 -0
- package/overlays/.shared/vscode/recommended-extensions.json +14 -0
- package/overlays/README.md +1 -1
- package/overlays/cloudflared/README.md +190 -0
- package/overlays/cloudflared/devcontainer.patch.json +3 -0
- package/overlays/cloudflared/overlay.yml +15 -0
- package/overlays/cloudflared/setup.sh +49 -0
- package/overlays/cloudflared/verify.sh +21 -0
- package/overlays/codex/overlay.yml +1 -0
- package/overlays/direnv/README.md +6 -4
- package/overlays/direnv/setup.sh +0 -12
- package/overlays/duckdb/README.md +274 -0
- package/overlays/duckdb/devcontainer.patch.json +10 -0
- package/overlays/duckdb/overlay.yml +17 -0
- package/overlays/duckdb/setup.sh +45 -0
- package/overlays/duckdb/verify.sh +32 -0
- package/overlays/git-helpers/overlay.yml +1 -0
- package/overlays/grafana/README.md +5 -5
- package/overlays/grafana/dashboard-provider.yml +1 -1
- package/overlays/grafana/docker-compose.yml +2 -2
- package/overlays/grafana/overlay.yml +6 -1
- package/overlays/grpc-tools/README.md +242 -0
- package/overlays/grpc-tools/devcontainer.patch.json +14 -0
- package/overlays/grpc-tools/overlay.yml +14 -0
- package/overlays/grpc-tools/setup.sh +57 -0
- package/overlays/grpc-tools/verify.sh +47 -0
- package/overlays/jaeger/overlay.yml +16 -3
- package/overlays/jupyter/.env.example +6 -0
- package/overlays/jupyter/README.md +210 -0
- package/overlays/jupyter/devcontainer.patch.json +14 -0
- package/overlays/jupyter/docker-compose.yml +23 -0
- package/overlays/jupyter/overlay.yml +18 -0
- package/overlays/jupyter/verify.sh +35 -0
- package/overlays/keycloak/.env.example +5 -0
- package/overlays/keycloak/README.md +238 -0
- package/overlays/keycloak/devcontainer.patch.json +17 -0
- package/overlays/keycloak/docker-compose.yml +32 -0
- package/overlays/keycloak/overlay.yml +23 -0
- package/overlays/keycloak/verify.sh +54 -0
- package/overlays/kind/README.md +221 -0
- package/overlays/kind/devcontainer.patch.json +10 -0
- package/overlays/kind/overlay.yml +18 -0
- package/overlays/kind/setup.sh +43 -0
- package/overlays/kind/verify.sh +40 -0
- package/overlays/localstack/.env.example +6 -0
- package/overlays/localstack/README.md +188 -0
- package/overlays/localstack/devcontainer.patch.json +21 -0
- package/overlays/localstack/docker-compose.yml +25 -0
- package/overlays/localstack/overlay.yml +18 -0
- package/overlays/localstack/verify.sh +47 -0
- package/overlays/loki/overlay.yml +6 -1
- package/overlays/mailpit/.env.example +4 -0
- package/overlays/mailpit/README.md +191 -0
- package/overlays/mailpit/devcontainer.patch.json +20 -0
- package/overlays/mailpit/docker-compose.yml +17 -0
- package/overlays/mailpit/overlay.yml +26 -0
- package/overlays/mailpit/verify.sh +52 -0
- package/overlays/modern-cli-tools/overlay.yml +1 -0
- package/overlays/mongodb/overlay.yml +12 -2
- package/overlays/mysql/overlay.yml +12 -2
- package/overlays/nats/overlay.yml +12 -2
- package/overlays/ngrok/overlay.yml +2 -1
- package/overlays/openapi-tools/README.md +243 -0
- package/overlays/openapi-tools/devcontainer.patch.json +10 -0
- package/overlays/openapi-tools/overlay.yml +16 -0
- package/overlays/openapi-tools/setup.sh +45 -0
- package/overlays/openapi-tools/verify.sh +51 -0
- package/overlays/otel-collector/overlay.yml.example +26 -0
- package/overlays/postgres/overlay.yml +6 -1
- package/overlays/prometheus/overlay.yml +6 -1
- package/overlays/python/README.md +51 -35
- package/overlays/python/devcontainer.patch.json +7 -4
- package/overlays/python/setup.sh +50 -23
- package/overlays/python/verify.sh +29 -1
- package/overlays/rabbitmq/overlay.yml +12 -2
- package/overlays/redis/overlay.yml +6 -1
- package/overlays/tilt/README.md +259 -0
- package/overlays/tilt/devcontainer.patch.json +17 -0
- package/overlays/tilt/overlay.yml +19 -0
- package/overlays/tilt/setup.sh +25 -0
- package/overlays/tilt/verify.sh +24 -0
- package/package.json +8 -6
- package/tool/README.md +12 -16
- package/tool/schema/overlay-manifest.schema.json +64 -4
- package/tool/schema/superposition-manifest.schema.json +104 -0
- package/overlays/presets/web-api.yml +0 -109
- /package/overlays/{presets ā .presets}/docs-site.yml +0 -0
- /package/overlays/{presets ā .presets}/fullstack.yml +0 -0
|
@@ -6,6 +6,13 @@ import * as yaml from 'js-yaml';
|
|
|
6
6
|
import { loadOverlaysConfig } from '../schema/overlay-loader.js';
|
|
7
7
|
import { loadCustomPatches, hasCustomDirectory, getCustomScriptPaths, } from '../schema/custom-loader.js';
|
|
8
8
|
import { generateReadme } from '../readme/readme-generator.js';
|
|
9
|
+
import { CURRENT_MANIFEST_VERSION } from '../schema/manifest-migrations.js';
|
|
10
|
+
import { getToolVersion } from '../utils/version.js';
|
|
11
|
+
import { deepMerge, mergePackages, filterDependsOn, applyPortOffsetToEnv, } from '../utils/merge.js';
|
|
12
|
+
import { generatePortsDocumentation } from '../utils/port-utils.js';
|
|
13
|
+
import { generateServicesMarkdown, generateEnvLocalExample } from '../utils/services-export.js';
|
|
14
|
+
import { appendGitignoreSection } from '../utils/gitignore.js';
|
|
15
|
+
import { detectWarnings, generateTips, generateNextSteps, overlaysToServices, portsToPortInfo, } from '../utils/summary.js';
|
|
9
16
|
// Get __dirname equivalent in ESM
|
|
10
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
18
|
const __dirname = path.dirname(__filename);
|
|
@@ -19,92 +26,6 @@ const REPO_ROOT_CANDIDATES = [
|
|
|
19
26
|
const REPO_ROOT = REPO_ROOT_CANDIDATES.find((candidate) => fs.existsSync(path.join(candidate, 'templates')) &&
|
|
20
27
|
fs.existsSync(path.join(candidate, 'overlays'))) ?? REPO_ROOT_CANDIDATES[0];
|
|
21
28
|
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
29
|
/**
|
|
109
30
|
* Merge packages from apt-get-packages feature
|
|
110
31
|
*/
|
|
@@ -118,10 +39,7 @@ function mergeAptPackages(baseConfig, packages) {
|
|
|
118
39
|
}
|
|
119
40
|
else {
|
|
120
41
|
const existing = baseConfig.features[featureKey].packages || '';
|
|
121
|
-
|
|
122
|
-
const existingPackages = existing.split(' ').filter((p) => p);
|
|
123
|
-
const newPackages = packages.split(' ').filter((p) => p);
|
|
124
|
-
const merged = [...new Set([...existingPackages, ...newPackages])].join(' ');
|
|
42
|
+
const merged = mergePackages(existing, packages);
|
|
125
43
|
baseConfig.features[featureKey].packages = merged;
|
|
126
44
|
}
|
|
127
45
|
return baseConfig;
|
|
@@ -140,17 +58,13 @@ function mergeCrossDistroPackages(baseConfig, apt, apk) {
|
|
|
140
58
|
// Merge apt packages
|
|
141
59
|
if (apt) {
|
|
142
60
|
const existing = baseConfig.features[featureKey].apt || '';
|
|
143
|
-
const
|
|
144
|
-
const newPackages = apt.split(' ').filter((p) => p);
|
|
145
|
-
const merged = [...new Set([...existingPackages, ...newPackages])].join(' ');
|
|
61
|
+
const merged = mergePackages(existing, apt);
|
|
146
62
|
baseConfig.features[featureKey].apt = merged;
|
|
147
63
|
}
|
|
148
64
|
// Merge apk packages
|
|
149
65
|
if (apk) {
|
|
150
66
|
const existing = baseConfig.features[featureKey].apk || '';
|
|
151
|
-
const
|
|
152
|
-
const newPackages = apk.split(' ').filter((p) => p);
|
|
153
|
-
const merged = [...new Set([...existingPackages, ...newPackages])].join(' ');
|
|
67
|
+
const merged = mergePackages(existing, apk);
|
|
154
68
|
baseConfig.features[featureKey].apk = merged;
|
|
155
69
|
}
|
|
156
70
|
return baseConfig;
|
|
@@ -226,12 +140,108 @@ function resolveDependencies(requestedOverlays, allOverlayDefs) {
|
|
|
226
140
|
},
|
|
227
141
|
};
|
|
228
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Prepare overlays for generation by loading configuration, building requested overlay list,
|
|
145
|
+
* filtering for minimal mode, checking compatibility, and resolving dependencies.
|
|
146
|
+
* This shared logic is used by both generateManifestOnly and composeDevContainer.
|
|
147
|
+
*/
|
|
148
|
+
function prepareOverlaysForGeneration(answers, overlaysDir) {
|
|
149
|
+
// 1. Load overlay configuration
|
|
150
|
+
const actualOverlaysDir = overlaysDir ?? path.join(REPO_ROOT, 'overlays');
|
|
151
|
+
const indexYmlPath = path.join(actualOverlaysDir, 'index.yml');
|
|
152
|
+
const overlaysConfig = loadOverlaysConfig(actualOverlaysDir, indexYmlPath);
|
|
153
|
+
// Collect all overlay definitions
|
|
154
|
+
const allOverlayDefs = getAllOverlayDefs(overlaysConfig);
|
|
155
|
+
// Build list of requested overlays
|
|
156
|
+
const requestedOverlays = [];
|
|
157
|
+
if (answers.language && answers.language.length > 0)
|
|
158
|
+
requestedOverlays.push(...answers.language);
|
|
159
|
+
if (answers.database && answers.database.length > 0)
|
|
160
|
+
requestedOverlays.push(...answers.database);
|
|
161
|
+
if (answers.observability)
|
|
162
|
+
requestedOverlays.push(...answers.observability);
|
|
163
|
+
if (answers.playwright)
|
|
164
|
+
requestedOverlays.push('playwright');
|
|
165
|
+
if (answers.cloudTools)
|
|
166
|
+
requestedOverlays.push(...answers.cloudTools);
|
|
167
|
+
if (answers.devTools)
|
|
168
|
+
requestedOverlays.push(...answers.devTools);
|
|
169
|
+
// Filter out "minimal" overlays if --minimal flag is set
|
|
170
|
+
let filteredRequestedOverlays = requestedOverlays;
|
|
171
|
+
if (answers.minimal) {
|
|
172
|
+
const minimalExcluded = [];
|
|
173
|
+
filteredRequestedOverlays = requestedOverlays.filter((overlayId) => {
|
|
174
|
+
const overlayDef = allOverlayDefs.find((o) => o.id === overlayId);
|
|
175
|
+
if (overlayDef?.minimal === true) {
|
|
176
|
+
minimalExcluded.push(overlayId);
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
});
|
|
181
|
+
if (minimalExcluded.length > 0) {
|
|
182
|
+
console.log(chalk.dim(` š¦ Minimal mode: Excluding ${minimalExcluded.length} optional overlay(s): ${minimalExcluded.join(', ')}`));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Check compatibility
|
|
186
|
+
const incompatible = [];
|
|
187
|
+
for (const overlayId of filteredRequestedOverlays) {
|
|
188
|
+
const overlayDef = allOverlayDefs.find((o) => o.id === overlayId);
|
|
189
|
+
if (overlayDef?.supports && overlayDef.supports.length > 0) {
|
|
190
|
+
if (!overlayDef.supports.includes(answers.stack)) {
|
|
191
|
+
incompatible.push(`${overlayId} (requires: ${overlayDef.supports.join(', ')})`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (incompatible.length > 0) {
|
|
196
|
+
console.log(chalk.yellow(`\nā ļø Warning: Some overlays are not compatible with '${answers.stack}' template:`));
|
|
197
|
+
incompatible.forEach((overlay) => {
|
|
198
|
+
console.log(chalk.yellow(` ⢠${overlay}`));
|
|
199
|
+
});
|
|
200
|
+
console.log(chalk.yellow(`\nThese overlays will be skipped.\n`));
|
|
201
|
+
// Filter out incompatible overlays
|
|
202
|
+
if (answers.database) {
|
|
203
|
+
answers.database = answers.database.filter((d) => !incompatible.some((i) => i.startsWith(d)));
|
|
204
|
+
}
|
|
205
|
+
if (answers.observability) {
|
|
206
|
+
answers.observability = answers.observability.filter((o) => !incompatible.some((i) => i.startsWith(o)));
|
|
207
|
+
}
|
|
208
|
+
// Update requestedOverlays after filtering
|
|
209
|
+
requestedOverlays.length = 0;
|
|
210
|
+
if (answers.language && answers.language.length > 0)
|
|
211
|
+
requestedOverlays.push(...answers.language);
|
|
212
|
+
if (answers.database && answers.database.length > 0)
|
|
213
|
+
requestedOverlays.push(...answers.database);
|
|
214
|
+
if (answers.observability)
|
|
215
|
+
requestedOverlays.push(...answers.observability);
|
|
216
|
+
if (answers.playwright)
|
|
217
|
+
requestedOverlays.push('playwright');
|
|
218
|
+
if (answers.cloudTools)
|
|
219
|
+
requestedOverlays.push(...answers.cloudTools);
|
|
220
|
+
if (answers.devTools)
|
|
221
|
+
requestedOverlays.push(...answers.devTools);
|
|
222
|
+
// Re-apply minimal filtering
|
|
223
|
+
filteredRequestedOverlays = requestedOverlays.filter((overlayId) => {
|
|
224
|
+
const overlayDef = allOverlayDefs.find((o) => o.id === overlayId);
|
|
225
|
+
return !answers.minimal || overlayDef?.minimal !== true;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
// Resolve dependencies
|
|
229
|
+
const { overlays: resolvedOverlays, autoResolved } = resolveDependencies(filteredRequestedOverlays, allOverlayDefs);
|
|
230
|
+
return {
|
|
231
|
+
overlays: resolvedOverlays,
|
|
232
|
+
autoResolved,
|
|
233
|
+
overlaysConfig,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
229
236
|
/**
|
|
230
237
|
* Generate superposition.json manifest
|
|
231
238
|
*/
|
|
232
239
|
function generateManifest(outputPath, answers, overlays, autoResolved, containerName) {
|
|
240
|
+
const toolVersion = getToolVersion();
|
|
233
241
|
const manifest = {
|
|
234
|
-
|
|
242
|
+
manifestVersion: CURRENT_MANIFEST_VERSION,
|
|
243
|
+
generatedBy: toolVersion,
|
|
244
|
+
version: '0.1.0', // Legacy field for backward compatibility
|
|
235
245
|
generated: new Date().toISOString(),
|
|
236
246
|
baseTemplate: answers.stack,
|
|
237
247
|
baseImage: answers.baseImage === 'custom' && answers.customImage
|
|
@@ -265,15 +275,74 @@ function generateManifest(outputPath, answers, overlays, autoResolved, container
|
|
|
265
275
|
console.log(chalk.cyan(` ā¹ļø Used preset: ${answers.preset}`));
|
|
266
276
|
}
|
|
267
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Load and resolve imports from shared files for an overlay
|
|
280
|
+
*/
|
|
281
|
+
function loadImportsForOverlay(overlayName, overlaysDir) {
|
|
282
|
+
let importedConfig = {};
|
|
283
|
+
// Load overlay manifest to get imports
|
|
284
|
+
const overlayDir = path.join(overlaysDir, overlayName);
|
|
285
|
+
const manifestPath = path.join(overlayDir, 'overlay.yml');
|
|
286
|
+
if (!fs.existsSync(manifestPath)) {
|
|
287
|
+
return importedConfig;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const manifestContent = fs.readFileSync(manifestPath, 'utf8');
|
|
291
|
+
const manifest = yaml.load(manifestContent);
|
|
292
|
+
if (!manifest.imports ||
|
|
293
|
+
!Array.isArray(manifest.imports) ||
|
|
294
|
+
manifest.imports.length === 0) {
|
|
295
|
+
return importedConfig;
|
|
296
|
+
}
|
|
297
|
+
// Process each import
|
|
298
|
+
for (const importPath of manifest.imports) {
|
|
299
|
+
const fullImportPath = path.join(overlaysDir, importPath);
|
|
300
|
+
if (!fs.existsSync(fullImportPath)) {
|
|
301
|
+
console.warn(chalk.yellow(`ā ļø Import not found: ${importPath} (for overlay: ${overlayName})`));
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
// Determine file type and merge appropriately
|
|
305
|
+
const ext = path.extname(importPath).toLowerCase();
|
|
306
|
+
if (ext === '.json') {
|
|
307
|
+
// JSON files are merged as devcontainer patches
|
|
308
|
+
const importedPatch = loadJson(fullImportPath);
|
|
309
|
+
importedConfig = deepMerge(importedConfig, importedPatch);
|
|
310
|
+
}
|
|
311
|
+
else if (ext === '.yaml' || ext === '.yml') {
|
|
312
|
+
// YAML files are loaded and merged as devcontainer patches
|
|
313
|
+
try {
|
|
314
|
+
const yamlContent = fs.readFileSync(fullImportPath, 'utf8');
|
|
315
|
+
const importedPatch = yaml.load(yamlContent);
|
|
316
|
+
if (importedPatch && typeof importedPatch === 'object') {
|
|
317
|
+
importedConfig = deepMerge(importedConfig, importedPatch);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
console.warn(chalk.yellow(`ā ļø Failed to parse YAML import: ${importPath}`));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// .env files are handled separately during env merging
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
console.warn(chalk.yellow(`ā ļø Failed to load imports for overlay: ${overlayName}`));
|
|
329
|
+
}
|
|
330
|
+
return importedConfig;
|
|
331
|
+
}
|
|
268
332
|
/**
|
|
269
333
|
* Apply an overlay to the base configuration
|
|
270
334
|
*/
|
|
271
|
-
function applyOverlay(baseConfig, overlayName) {
|
|
272
|
-
const overlayPath = path.join(
|
|
335
|
+
export function applyOverlay(baseConfig, overlayName, overlaysDir) {
|
|
336
|
+
const overlayPath = path.join(overlaysDir, overlayName, 'devcontainer.patch.json');
|
|
273
337
|
if (!fs.existsSync(overlayPath)) {
|
|
274
338
|
console.warn(chalk.yellow(`ā ļø Overlay not found: ${overlayName}`));
|
|
275
339
|
return baseConfig;
|
|
276
340
|
}
|
|
341
|
+
// First, load and apply any imports
|
|
342
|
+
const importedConfig = loadImportsForOverlay(overlayName, overlaysDir);
|
|
343
|
+
if (Object.keys(importedConfig).length > 0) {
|
|
344
|
+
baseConfig = deepMerge(baseConfig, importedConfig);
|
|
345
|
+
}
|
|
277
346
|
const overlay = loadJson(overlayPath);
|
|
278
347
|
// Special handling for apt-get packages (legacy)
|
|
279
348
|
if (overlay.features?.['ghcr.io/devcontainers-extra/features/apt-get-packages:1']?.packages) {
|
|
@@ -378,20 +447,21 @@ function copyDir(src, dest) {
|
|
|
378
447
|
* Copy additional files from overlay to output directory
|
|
379
448
|
* Excludes devcontainer.patch.json and .env.example (handled separately)
|
|
380
449
|
*/
|
|
381
|
-
function copyOverlayFiles(outputPath, overlayName, registry) {
|
|
382
|
-
const overlayPath = path.join(
|
|
450
|
+
function copyOverlayFiles(outputPath, overlayName, registry, overlaysDir) {
|
|
451
|
+
const overlayPath = path.join(overlaysDir, overlayName);
|
|
383
452
|
if (!fs.existsSync(overlayPath)) {
|
|
384
453
|
return;
|
|
385
454
|
}
|
|
386
455
|
const entries = fs.readdirSync(overlayPath);
|
|
387
456
|
let copiedFiles = 0;
|
|
388
457
|
for (const entry of entries) {
|
|
389
|
-
// Skip devcontainer.patch.json, .env.example, docker-compose.yml, setup.sh, verify.sh, and metadata files (handled separately)
|
|
458
|
+
// Skip devcontainer.patch.json, .env.example, docker-compose.yml, setup.sh, verify.sh, .gitignore, and metadata files (handled separately)
|
|
390
459
|
if (entry === 'devcontainer.patch.json' ||
|
|
391
460
|
entry === '.env.example' ||
|
|
392
461
|
entry === 'docker-compose.yml' ||
|
|
393
462
|
entry === 'setup.sh' ||
|
|
394
463
|
entry === 'verify.sh' ||
|
|
464
|
+
entry === '.gitignore' ||
|
|
395
465
|
entry === 'README.md' ||
|
|
396
466
|
entry === 'overlay.yml') {
|
|
397
467
|
continue;
|
|
@@ -428,10 +498,37 @@ function copyOverlayFiles(outputPath, overlayName, registry) {
|
|
|
428
498
|
/**
|
|
429
499
|
* Merge .env.example files from overlays and apply glue config
|
|
430
500
|
*/
|
|
431
|
-
function mergeEnvExamples(outputPath, overlays, portOffset, glueConfig, presetName) {
|
|
501
|
+
function mergeEnvExamples(outputPath, overlays, overlaysDir, portOffset, glueConfig, presetName) {
|
|
432
502
|
const envSections = [];
|
|
433
503
|
for (const overlay of overlays) {
|
|
434
|
-
|
|
504
|
+
// First, check for imports in the overlay and add any .env files from imports
|
|
505
|
+
const overlayDir = path.join(overlaysDir, overlay);
|
|
506
|
+
const manifestPath = path.join(overlayDir, 'overlay.yml');
|
|
507
|
+
if (fs.existsSync(manifestPath)) {
|
|
508
|
+
try {
|
|
509
|
+
const manifestContent = fs.readFileSync(manifestPath, 'utf8');
|
|
510
|
+
const manifest = yaml.load(manifestContent);
|
|
511
|
+
if (manifest.imports && Array.isArray(manifest.imports)) {
|
|
512
|
+
for (const importPath of manifest.imports) {
|
|
513
|
+
const ext = path.extname(importPath).toLowerCase();
|
|
514
|
+
if (ext === '.env') {
|
|
515
|
+
const fullImportPath = path.join(overlaysDir, importPath);
|
|
516
|
+
if (fs.existsSync(fullImportPath)) {
|
|
517
|
+
const content = fs.readFileSync(fullImportPath, 'utf-8').trim();
|
|
518
|
+
if (content) {
|
|
519
|
+
envSections.push(`# Imported from ${importPath}\n${content}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
// Ignore errors reading manifest
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// Then add the overlay's own .env.example
|
|
531
|
+
const envPath = path.join(overlaysDir, overlay, '.env.example');
|
|
435
532
|
if (fs.existsSync(envPath)) {
|
|
436
533
|
const content = fs.readFileSync(envPath, 'utf-8').trim();
|
|
437
534
|
if (content) {
|
|
@@ -479,21 +576,42 @@ function mergeEnvExamples(outputPath, overlays, portOffset, glueConfig, presetNa
|
|
|
479
576
|
return true;
|
|
480
577
|
}
|
|
481
578
|
/**
|
|
482
|
-
*
|
|
579
|
+
* Merge .gitignore files from overlays into the project root .gitignore.
|
|
580
|
+
* Writes to path.dirname(outputPath) ā the project root (parent of .devcontainer/).
|
|
581
|
+
* Only appends entries not already present; safe to run multiple times.
|
|
582
|
+
* Returns true if any entries were written.
|
|
483
583
|
*/
|
|
484
|
-
function
|
|
485
|
-
const
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
584
|
+
function mergeGitignoreFiles(outputPath, overlays, overlaysDir) {
|
|
585
|
+
const projectRoot = path.dirname(path.resolve(outputPath));
|
|
586
|
+
const destPath = path.join(projectRoot, '.gitignore');
|
|
587
|
+
let anyWritten = false;
|
|
588
|
+
let sectionsWritten = 0;
|
|
589
|
+
for (const overlay of overlays) {
|
|
590
|
+
const gitignorePath = path.join(overlaysDir, overlay, '.gitignore');
|
|
591
|
+
if (!fs.existsSync(gitignorePath))
|
|
592
|
+
continue;
|
|
593
|
+
const content = fs.readFileSync(gitignorePath, 'utf-8').trim();
|
|
594
|
+
if (!content)
|
|
595
|
+
continue;
|
|
596
|
+
const lines = content
|
|
597
|
+
.split('\n')
|
|
598
|
+
.map((l) => l.trim())
|
|
599
|
+
.filter((l) => l.length > 0 && !l.startsWith('#'));
|
|
600
|
+
if (lines.length === 0)
|
|
601
|
+
continue;
|
|
602
|
+
const written = appendGitignoreSection(destPath, `${overlay} (container-superposition)`, lines);
|
|
603
|
+
if (written) {
|
|
604
|
+
anyWritten = true;
|
|
605
|
+
sectionsWritten++;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (anyWritten) {
|
|
609
|
+
console.log(chalk.dim(` š Updated .gitignore with entries from ${sectionsWritten} overlay(s)`));
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
console.log(chalk.dim(` š .gitignore already up to date`));
|
|
613
|
+
}
|
|
614
|
+
return anyWritten;
|
|
497
615
|
}
|
|
498
616
|
/**
|
|
499
617
|
* Apply preset glue configuration (README and port mappings)
|
|
@@ -526,7 +644,7 @@ function applyGlueConfig(outputPath, glueConfig, presetName, fileRegistry) {
|
|
|
526
644
|
/**
|
|
527
645
|
* Merge docker-compose.yml files from base and overlays into a single file
|
|
528
646
|
*/
|
|
529
|
-
function mergeDockerComposeFiles(outputPath, baseStack, overlays, portOffset, customImage) {
|
|
647
|
+
function mergeDockerComposeFiles(outputPath, baseStack, overlays, overlaysDir, portOffset, customImage) {
|
|
530
648
|
const composeFiles = [];
|
|
531
649
|
// Add base docker-compose if exists
|
|
532
650
|
const baseComposePath = path.join(TEMPLATES_DIR, baseStack, '.devcontainer', 'docker-compose.yml');
|
|
@@ -535,7 +653,7 @@ function mergeDockerComposeFiles(outputPath, baseStack, overlays, portOffset, cu
|
|
|
535
653
|
}
|
|
536
654
|
// Add overlay docker-compose files
|
|
537
655
|
for (const overlay of overlays) {
|
|
538
|
-
const overlayComposePath = path.join(
|
|
656
|
+
const overlayComposePath = path.join(overlaysDir, overlay, 'docker-compose.yml');
|
|
539
657
|
if (fs.existsSync(overlayComposePath)) {
|
|
540
658
|
composeFiles.push(overlayComposePath);
|
|
541
659
|
}
|
|
@@ -583,13 +701,17 @@ function mergeDockerComposeFiles(outputPath, baseStack, overlays, portOffset, cu
|
|
|
583
701
|
}
|
|
584
702
|
// Filter depends_on to only include services that exist
|
|
585
703
|
const serviceNames = Object.keys(merged.services);
|
|
704
|
+
const serviceNameSet = new Set(serviceNames);
|
|
586
705
|
for (const serviceName of serviceNames) {
|
|
587
706
|
const service = merged.services[serviceName];
|
|
588
|
-
if (service.depends_on
|
|
589
|
-
|
|
590
|
-
if (
|
|
707
|
+
if (service.depends_on !== undefined) {
|
|
708
|
+
const filteredDependsOn = filterDependsOn(service.depends_on, serviceNameSet);
|
|
709
|
+
if (filteredDependsOn === undefined) {
|
|
591
710
|
delete service.depends_on;
|
|
592
711
|
}
|
|
712
|
+
else {
|
|
713
|
+
service.depends_on = filteredDependsOn;
|
|
714
|
+
}
|
|
593
715
|
}
|
|
594
716
|
}
|
|
595
717
|
// Remove empty sections
|
|
@@ -800,71 +922,61 @@ function copyCustomFiles(customConfig, outputPath, fileRegistry) {
|
|
|
800
922
|
fileRegistry.addFile(relativeDest);
|
|
801
923
|
}
|
|
802
924
|
}
|
|
925
|
+
/**
|
|
926
|
+
* Generate only the superposition.json manifest without creating .devcontainer files
|
|
927
|
+
* Used for team collaboration workflow where manifest is committed but .devcontainer is gitignored
|
|
928
|
+
*/
|
|
929
|
+
export async function generateManifestOnly(answers, overlaysDir, options = {}) {
|
|
930
|
+
// Prepare overlays using shared logic
|
|
931
|
+
const { overlays: resolvedOverlays, autoResolved } = prepareOverlaysForGeneration(answers, overlaysDir);
|
|
932
|
+
// Ensure output directory exists
|
|
933
|
+
const outputPath = answers.outputPath || '.';
|
|
934
|
+
if (!fs.existsSync(outputPath)) {
|
|
935
|
+
fs.mkdirSync(outputPath, { recursive: true });
|
|
936
|
+
}
|
|
937
|
+
// Generate manifest only
|
|
938
|
+
console.log(chalk.cyan('\nš Generating manifest only (team collaboration mode)...\n'));
|
|
939
|
+
generateManifest(outputPath, answers, resolvedOverlays, autoResolved, answers.containerName);
|
|
940
|
+
console.log(chalk.green(`\nā Manifest created: ${path.join(outputPath, 'superposition.json')}`));
|
|
941
|
+
console.log(chalk.dim(' Ready for team collaboration workflow.'));
|
|
942
|
+
console.log(chalk.dim(' Commit this manifest to your repository and let team members run "npx container-superposition regen"'));
|
|
943
|
+
// Load overlay configs to get metadata
|
|
944
|
+
const actualOverlaysDir = overlaysDir ?? path.join(REPO_ROOT, 'overlays');
|
|
945
|
+
const indexYmlPath = path.join(actualOverlaysDir, 'index.yml');
|
|
946
|
+
const overlaysConfig = loadOverlaysConfig(actualOverlaysDir, indexYmlPath);
|
|
947
|
+
const allOverlayDefs = getAllOverlayDefs(overlaysConfig);
|
|
948
|
+
const overlayMetadataMap = new Map(allOverlayDefs.map((o) => [o.id, o]));
|
|
949
|
+
const selectedOverlayMetadata = resolvedOverlays
|
|
950
|
+
.map((id) => overlayMetadataMap.get(id))
|
|
951
|
+
.filter((m) => m !== undefined);
|
|
952
|
+
// Return summary for manifest-only mode
|
|
953
|
+
const services = overlaysToServices(selectedOverlayMetadata);
|
|
954
|
+
const warnings = detectWarnings(selectedOverlayMetadata, answers);
|
|
955
|
+
const tips = generateTips(selectedOverlayMetadata, answers);
|
|
956
|
+
const nextSteps = generateNextSteps(true, options.isRegen === true);
|
|
957
|
+
return {
|
|
958
|
+
files: ['superposition.json'],
|
|
959
|
+
services,
|
|
960
|
+
ports: [],
|
|
961
|
+
warnings,
|
|
962
|
+
tips,
|
|
963
|
+
nextSteps,
|
|
964
|
+
portOffset: answers.portOffset ?? 0,
|
|
965
|
+
target: answers.target || 'local',
|
|
966
|
+
isManifestOnly: true,
|
|
967
|
+
manifestPath: path.join(outputPath, 'superposition.json'),
|
|
968
|
+
};
|
|
969
|
+
}
|
|
803
970
|
/**
|
|
804
971
|
* Main composition logic
|
|
805
972
|
*/
|
|
806
|
-
export async function composeDevContainer(answers) {
|
|
807
|
-
//
|
|
808
|
-
const
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
// Collect all overlay definitions
|
|
973
|
+
export async function composeDevContainer(answers, overlaysDir, options = {}) {
|
|
974
|
+
// Prepare overlays using shared logic
|
|
975
|
+
const actualOverlaysDir = overlaysDir ?? path.join(REPO_ROOT, 'overlays');
|
|
976
|
+
const { overlays: resolvedOverlays, autoResolved, overlaysConfig, } = prepareOverlaysForGeneration(answers, overlaysDir);
|
|
977
|
+
// Get all overlay definitions for later use
|
|
812
978
|
const allOverlayDefs = getAllOverlayDefs(overlaysConfig);
|
|
813
|
-
//
|
|
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
|
|
979
|
+
// Determine base template path
|
|
868
980
|
const templatePath = path.join(TEMPLATES_DIR, answers.stack, '.devcontainer');
|
|
869
981
|
if (!fs.existsSync(templatePath)) {
|
|
870
982
|
throw new Error(`Template not found: ${answers.stack}`);
|
|
@@ -946,7 +1058,7 @@ export async function composeDevContainer(answers) {
|
|
|
946
1058
|
// 6. Apply overlays
|
|
947
1059
|
for (const overlay of overlays) {
|
|
948
1060
|
console.log(chalk.dim(` š§ Applying overlay: ${chalk.cyan(overlay)}`));
|
|
949
|
-
config = applyOverlay(config, overlay);
|
|
1061
|
+
config = applyOverlay(config, overlay, actualOverlaysDir);
|
|
950
1062
|
}
|
|
951
1063
|
// 7. Copy template files (docker-compose, scripts, etc.)
|
|
952
1064
|
const entries = fs.readdirSync(templatePath);
|
|
@@ -967,7 +1079,7 @@ export async function composeDevContainer(answers) {
|
|
|
967
1079
|
}
|
|
968
1080
|
// 8. Copy overlay files (docker-compose, configs, etc.)
|
|
969
1081
|
for (const overlay of overlays) {
|
|
970
|
-
copyOverlayFiles(outputPath, overlay, fileRegistry);
|
|
1082
|
+
copyOverlayFiles(outputPath, overlay, fileRegistry, actualOverlaysDir);
|
|
971
1083
|
}
|
|
972
1084
|
// 8.5. Copy cross-distro-packages feature if used
|
|
973
1085
|
if (config.features?.['./features/cross-distro-packages']) {
|
|
@@ -982,11 +1094,11 @@ export async function composeDevContainer(answers) {
|
|
|
982
1094
|
// 8. Filter docker-compose dependencies based on selected overlays
|
|
983
1095
|
filterDockerComposeDependencies(outputPath, overlays);
|
|
984
1096
|
// 9. Merge runServices array in correct order
|
|
985
|
-
mergeRunServices(config, overlays);
|
|
1097
|
+
mergeRunServices(config, overlays, actualOverlaysDir);
|
|
986
1098
|
// 11. Merge docker-compose files into single combined file
|
|
987
1099
|
if (answers.stack === 'compose') {
|
|
988
1100
|
const customImage = config._customImage;
|
|
989
|
-
mergeDockerComposeFiles(outputPath, answers.stack, overlays, answers.portOffset, customImage);
|
|
1101
|
+
mergeDockerComposeFiles(outputPath, answers.stack, overlays, actualOverlaysDir, answers.portOffset, customImage);
|
|
990
1102
|
// Update devcontainer.json to reference the combined file
|
|
991
1103
|
if (config.dockerComposeFile) {
|
|
992
1104
|
config.dockerComposeFile = 'docker-compose.yml';
|
|
@@ -997,7 +1109,7 @@ export async function composeDevContainer(answers) {
|
|
|
997
1109
|
applyPortOffsetToDevcontainer(config, answers.portOffset);
|
|
998
1110
|
}
|
|
999
1111
|
// Merge setup scripts from overlays into postCreateCommand
|
|
1000
|
-
mergeSetupScripts(config, overlays, outputPath, fileRegistry);
|
|
1112
|
+
mergeSetupScripts(config, overlays, outputPath, fileRegistry, actualOverlaysDir);
|
|
1001
1113
|
// 10. Apply custom patches from .devcontainer/custom/ (if present)
|
|
1002
1114
|
const customPatches = loadCustomPatches(outputPath);
|
|
1003
1115
|
if (customPatches) {
|
|
@@ -1015,6 +1127,25 @@ export async function composeDevContainer(answers) {
|
|
|
1015
1127
|
delete config[key];
|
|
1016
1128
|
}
|
|
1017
1129
|
});
|
|
1130
|
+
// Handle editor profile filtering
|
|
1131
|
+
if (answers.editor === 'none' || answers.editor === 'jetbrains') {
|
|
1132
|
+
// Remove VS Code customizations
|
|
1133
|
+
if (config.customizations?.vscode) {
|
|
1134
|
+
if (answers.editor === 'none') {
|
|
1135
|
+
delete config.customizations.vscode;
|
|
1136
|
+
console.log(chalk.dim(` šØ Editor profile 'none': Removed VS Code customizations`));
|
|
1137
|
+
}
|
|
1138
|
+
else if (answers.editor === 'jetbrains') {
|
|
1139
|
+
// For JetBrains, remove VS Code customizations (future: could add JetBrains-specific settings)
|
|
1140
|
+
delete config.customizations.vscode;
|
|
1141
|
+
console.log(chalk.dim(` šØ Editor profile 'jetbrains': Removed VS Code customizations`));
|
|
1142
|
+
}
|
|
1143
|
+
// Clean up empty customizations object
|
|
1144
|
+
if (config.customizations && Object.keys(config.customizations).length === 0) {
|
|
1145
|
+
delete config.customizations;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1018
1149
|
// 12. Write merged devcontainer.json
|
|
1019
1150
|
const configPath = path.join(outputPath, 'devcontainer.json');
|
|
1020
1151
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
@@ -1028,7 +1159,7 @@ export async function composeDevContainer(answers) {
|
|
|
1028
1159
|
generateManifest(outputPath, answers, overlays, autoResolved, answers.containerName || config.name);
|
|
1029
1160
|
fileRegistry.addFile('superposition.json');
|
|
1030
1161
|
// 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);
|
|
1162
|
+
const envCreated = mergeEnvExamples(outputPath, overlays, actualOverlaysDir, answers.portOffset, answers.presetGlueConfig, answers.preset);
|
|
1032
1163
|
if (envCreated) {
|
|
1033
1164
|
fileRegistry.addFile('.env.example');
|
|
1034
1165
|
}
|
|
@@ -1040,6 +1171,10 @@ export async function composeDevContainer(answers) {
|
|
|
1040
1171
|
fileRegistry.addFile('.env.example');
|
|
1041
1172
|
}
|
|
1042
1173
|
}
|
|
1174
|
+
// 14b. Merge .gitignore files from overlays into project root .gitignore
|
|
1175
|
+
// Note: .gitignore lives at the project root (parent of outputPath), not inside outputPath,
|
|
1176
|
+
// so it is intentionally NOT added to fileRegistry (cleanupStaleFiles must not touch it).
|
|
1177
|
+
mergeGitignoreFiles(outputPath, overlays, actualOverlaysDir);
|
|
1043
1178
|
// 15. Apply preset glue configuration (README and port mappings) if present
|
|
1044
1179
|
if (answers.presetGlueConfig) {
|
|
1045
1180
|
applyGlueConfig(outputPath, answers.presetGlueConfig, answers.preset, fileRegistry);
|
|
@@ -1050,8 +1185,84 @@ export async function composeDevContainer(answers) {
|
|
|
1050
1185
|
generateReadme(answers, overlays, overlayMetadataMap, outputPath);
|
|
1051
1186
|
fileRegistry.addFile('README.md');
|
|
1052
1187
|
console.log(chalk.dim(` š Created README.md with documentation from ${overlays.length} overlay(s)`));
|
|
1053
|
-
// 17.
|
|
1188
|
+
// 17. Generate ports.json documentation
|
|
1189
|
+
const portOffset = answers.portOffset ?? 0;
|
|
1190
|
+
// Prepare overlay metadata for summary
|
|
1191
|
+
const selectedOverlayMetadata = overlays
|
|
1192
|
+
.map((id) => overlayMetadataMap.get(id))
|
|
1193
|
+
.filter((m) => m !== undefined);
|
|
1194
|
+
// Extract environment variables from .env.example for connection strings
|
|
1195
|
+
const envPath = path.join(outputPath, '.env.example');
|
|
1196
|
+
const envVars = {};
|
|
1197
|
+
if (fs.existsSync(envPath)) {
|
|
1198
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
1199
|
+
for (const line of envContent.split('\n')) {
|
|
1200
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
|
|
1201
|
+
if (match) {
|
|
1202
|
+
envVars[match[1].toLowerCase()] = match[2];
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
const hasOverlayPorts = overlays.some((o) => overlayMetadataMap.get(o)?.ports?.length);
|
|
1207
|
+
const shouldGeneratePortsDocumentation = portOffset > 0 || hasOverlayPorts;
|
|
1208
|
+
let portsDoc = null;
|
|
1209
|
+
if (shouldGeneratePortsDocumentation) {
|
|
1210
|
+
console.log(chalk.cyan('\nš” Generating ports documentation...'));
|
|
1211
|
+
portsDoc = generatePortsDocumentation(selectedOverlayMetadata, portOffset, envVars);
|
|
1212
|
+
const portsPath = path.join(outputPath, 'ports.json');
|
|
1213
|
+
fs.writeFileSync(portsPath, JSON.stringify(portsDoc, null, 2) + '\n');
|
|
1214
|
+
fileRegistry.addFile('ports.json');
|
|
1215
|
+
console.log(chalk.dim(` š” Created ports.json with ${portsDoc.ports.length} port(s)`));
|
|
1216
|
+
// Log summary of ports
|
|
1217
|
+
if (portsDoc.ports.length > 0) {
|
|
1218
|
+
console.log(chalk.dim('\n Available services:'));
|
|
1219
|
+
for (const port of portsDoc.ports) {
|
|
1220
|
+
const serviceLabel = port.service || 'unknown';
|
|
1221
|
+
const desc = port.description ? ` - ${port.description}` : '';
|
|
1222
|
+
const proto = port.protocol ? ` (${port.protocol})` : '';
|
|
1223
|
+
console.log(chalk.dim(` ⢠${serviceLabel}: ${port.actualPort}${proto}${desc}`));
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
// 17b. Generate services.md reference document
|
|
1228
|
+
const servicesMdContent = generateServicesMarkdown(selectedOverlayMetadata, portOffset, envVars);
|
|
1229
|
+
if (servicesMdContent) {
|
|
1230
|
+
const servicesMdPath = path.join(outputPath, 'services.md');
|
|
1231
|
+
fs.writeFileSync(servicesMdPath, servicesMdContent);
|
|
1232
|
+
fileRegistry.addFile('services.md');
|
|
1233
|
+
console.log(chalk.dim(` š Created services.md with service reference`));
|
|
1234
|
+
}
|
|
1235
|
+
// 17c. Generate env.local.example as an optional-overrides template
|
|
1236
|
+
const envLocalContent = generateEnvLocalExample(selectedOverlayMetadata, actualOverlaysDir, portOffset);
|
|
1237
|
+
if (envLocalContent) {
|
|
1238
|
+
const envLocalPath = path.join(outputPath, 'env.local.example');
|
|
1239
|
+
fs.writeFileSync(envLocalPath, envLocalContent);
|
|
1240
|
+
fileRegistry.addFile('env.local.example');
|
|
1241
|
+
console.log(chalk.dim(` š Created env.local.example with optional overrides`));
|
|
1242
|
+
}
|
|
1243
|
+
// 18. Clean up stale files from previous runs (preserves superposition.json and .env)
|
|
1054
1244
|
cleanupStaleFiles(outputPath, fileRegistry);
|
|
1245
|
+
// 19. Generate and return summary
|
|
1246
|
+
const files = Array.from(fileRegistry.getFiles());
|
|
1247
|
+
const services = overlaysToServices(selectedOverlayMetadata);
|
|
1248
|
+
const portInfos = portsDoc
|
|
1249
|
+
? portsToPortInfo(portsDoc.ports, portsDoc.connectionStrings || {})
|
|
1250
|
+
: [];
|
|
1251
|
+
const warnings = detectWarnings(selectedOverlayMetadata, answers);
|
|
1252
|
+
const tips = generateTips(selectedOverlayMetadata, answers);
|
|
1253
|
+
const nextSteps = generateNextSteps(false, options.isRegen === true);
|
|
1254
|
+
return {
|
|
1255
|
+
files,
|
|
1256
|
+
services,
|
|
1257
|
+
ports: portInfos,
|
|
1258
|
+
warnings,
|
|
1259
|
+
tips,
|
|
1260
|
+
nextSteps,
|
|
1261
|
+
portOffset: answers.portOffset ?? 0,
|
|
1262
|
+
target: answers.target || 'local',
|
|
1263
|
+
isManifestOnly: false,
|
|
1264
|
+
manifestPath: path.join(outputPath, 'superposition.json'),
|
|
1265
|
+
};
|
|
1055
1266
|
}
|
|
1056
1267
|
/**
|
|
1057
1268
|
* Apply port offset to devcontainer.json forwardPorts and portsAttributes
|
|
@@ -1084,7 +1295,7 @@ function applyPortOffsetToDevcontainer(config, offset) {
|
|
|
1084
1295
|
/**
|
|
1085
1296
|
* Merge setup scripts from overlays into postCreateCommand
|
|
1086
1297
|
*/
|
|
1087
|
-
function mergeSetupScripts(config, overlays, outputPath, fileRegistry) {
|
|
1298
|
+
function mergeSetupScripts(config, overlays, outputPath, fileRegistry, overlaysDir) {
|
|
1088
1299
|
const setupScripts = [];
|
|
1089
1300
|
const verifyScripts = [];
|
|
1090
1301
|
// Create scripts subfolder
|
|
@@ -1093,14 +1304,14 @@ function mergeSetupScripts(config, overlays, outputPath, fileRegistry) {
|
|
|
1093
1304
|
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
1094
1305
|
}
|
|
1095
1306
|
// Add scripts directory to registry if any scripts will be added
|
|
1096
|
-
const hasScripts = overlays.some((o) => fs.existsSync(path.join(
|
|
1097
|
-
fs.existsSync(path.join(
|
|
1307
|
+
const hasScripts = overlays.some((o) => fs.existsSync(path.join(overlaysDir, o, 'setup.sh')) ||
|
|
1308
|
+
fs.existsSync(path.join(overlaysDir, o, 'verify.sh')));
|
|
1098
1309
|
if (hasScripts) {
|
|
1099
1310
|
fileRegistry.addDirectory('scripts');
|
|
1100
1311
|
}
|
|
1101
1312
|
for (const overlay of overlays) {
|
|
1102
1313
|
// Handle setup scripts
|
|
1103
|
-
const setupPath = path.join(
|
|
1314
|
+
const setupPath = path.join(overlaysDir, overlay, 'setup.sh');
|
|
1104
1315
|
if (fs.existsSync(setupPath)) {
|
|
1105
1316
|
// Copy setup script to scripts subdirectory
|
|
1106
1317
|
const destPath = path.join(scriptsDir, `setup-${overlay}.sh`);
|
|
@@ -1111,7 +1322,7 @@ function mergeSetupScripts(config, overlays, outputPath, fileRegistry) {
|
|
|
1111
1322
|
setupScripts.push(`bash .devcontainer/scripts/setup-${overlay}.sh`);
|
|
1112
1323
|
}
|
|
1113
1324
|
// Handle verify scripts
|
|
1114
|
-
const verifyPath = path.join(
|
|
1325
|
+
const verifyPath = path.join(overlaysDir, overlay, 'verify.sh');
|
|
1115
1326
|
if (fs.existsSync(verifyPath)) {
|
|
1116
1327
|
// Copy verify script to scripts subdirectory
|
|
1117
1328
|
const destPath = path.join(scriptsDir, `verify-${overlay}.sh`);
|
|
@@ -1134,7 +1345,7 @@ function mergeSetupScripts(config, overlays, outputPath, fileRegistry) {
|
|
|
1134
1345
|
// Add setup scripts
|
|
1135
1346
|
for (let i = 0; i < setupScripts.length; i++) {
|
|
1136
1347
|
const overlay = overlays.filter((o) => {
|
|
1137
|
-
const setupPath = path.join(
|
|
1348
|
+
const setupPath = path.join(overlaysDir, o, 'setup.sh');
|
|
1138
1349
|
return fs.existsSync(setupPath);
|
|
1139
1350
|
})[i];
|
|
1140
1351
|
config.postCreateCommand[`setup-${overlay}`] = setupScripts[i];
|
|
@@ -1153,7 +1364,7 @@ function mergeSetupScripts(config, overlays, outputPath, fileRegistry) {
|
|
|
1153
1364
|
// Add verify scripts
|
|
1154
1365
|
for (let i = 0; i < verifyScripts.length; i++) {
|
|
1155
1366
|
const overlay = overlays.filter((o) => {
|
|
1156
|
-
const verifyPath = path.join(
|
|
1367
|
+
const verifyPath = path.join(overlaysDir, o, 'verify.sh');
|
|
1157
1368
|
return fs.existsSync(verifyPath);
|
|
1158
1369
|
})[i];
|
|
1159
1370
|
config.postStartCommand[`verify-${overlay}`] = verifyScripts[i];
|
|
@@ -1208,10 +1419,10 @@ function filterDockerComposeDependencies(outputPath, selectedOverlays) {
|
|
|
1208
1419
|
/**
|
|
1209
1420
|
* Merge runServices from all overlays in correct order
|
|
1210
1421
|
*/
|
|
1211
|
-
function mergeRunServices(config, overlays) {
|
|
1422
|
+
function mergeRunServices(config, overlays, overlaysDir) {
|
|
1212
1423
|
const services = [];
|
|
1213
1424
|
for (const overlay of overlays) {
|
|
1214
|
-
const overlayPath = path.join(
|
|
1425
|
+
const overlayPath = path.join(overlaysDir, overlay, 'devcontainer.patch.json');
|
|
1215
1426
|
if (fs.existsSync(overlayPath)) {
|
|
1216
1427
|
const overlayConfig = loadJson(overlayPath);
|
|
1217
1428
|
if (overlayConfig.runServices) {
|