container-superposition 0.1.1 ā 0.1.3
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 +206 -1
- package/dist/scripts/init.js +235 -179
- 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 +211 -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 +16 -0
- package/dist/tool/commands/plan.d.ts.map +1 -0
- package/dist/tool/commands/plan.js +329 -0
- package/dist/tool/commands/plan.js.map +1 -0
- package/dist/tool/questionnaire/composer.d.ts +6 -1
- package/dist/tool/questionnaire/composer.d.ts.map +1 -1
- package/dist/tool/questionnaire/composer.js +300 -202
- 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 +44 -2
- package/dist/tool/schema/types.d.ts.map +1 -1
- 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/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 +91 -23
- 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/.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/codex/overlay.yml +1 -0
- 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/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/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/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/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/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 ā .presets}/docs-site.yml +0 -0
- /package/overlays/{presets ā .presets}/fullstack.yml +0 -0
- /package/overlays/{presets ā .presets}/microservice.yml +0 -0
- /package/overlays/{presets ā .presets}/web-api.yml +0 -0
|
@@ -6,6 +6,10 @@ 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';
|
|
9
13
|
// Get __dirname equivalent in ESM
|
|
10
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
15
|
const __dirname = path.dirname(__filename);
|
|
@@ -19,92 +23,6 @@ const REPO_ROOT_CANDIDATES = [
|
|
|
19
23
|
const REPO_ROOT = REPO_ROOT_CANDIDATES.find((candidate) => fs.existsSync(path.join(candidate, 'templates')) &&
|
|
20
24
|
fs.existsSync(path.join(candidate, 'overlays'))) ?? REPO_ROOT_CANDIDATES[0];
|
|
21
25
|
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
26
|
/**
|
|
109
27
|
* Merge packages from apt-get-packages feature
|
|
110
28
|
*/
|
|
@@ -118,10 +36,7 @@ function mergeAptPackages(baseConfig, packages) {
|
|
|
118
36
|
}
|
|
119
37
|
else {
|
|
120
38
|
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(' ');
|
|
39
|
+
const merged = mergePackages(existing, packages);
|
|
125
40
|
baseConfig.features[featureKey].packages = merged;
|
|
126
41
|
}
|
|
127
42
|
return baseConfig;
|
|
@@ -140,17 +55,13 @@ function mergeCrossDistroPackages(baseConfig, apt, apk) {
|
|
|
140
55
|
// Merge apt packages
|
|
141
56
|
if (apt) {
|
|
142
57
|
const existing = baseConfig.features[featureKey].apt || '';
|
|
143
|
-
const
|
|
144
|
-
const newPackages = apt.split(' ').filter((p) => p);
|
|
145
|
-
const merged = [...new Set([...existingPackages, ...newPackages])].join(' ');
|
|
58
|
+
const merged = mergePackages(existing, apt);
|
|
146
59
|
baseConfig.features[featureKey].apt = merged;
|
|
147
60
|
}
|
|
148
61
|
// Merge apk packages
|
|
149
62
|
if (apk) {
|
|
150
63
|
const existing = baseConfig.features[featureKey].apk || '';
|
|
151
|
-
const
|
|
152
|
-
const newPackages = apk.split(' ').filter((p) => p);
|
|
153
|
-
const merged = [...new Set([...existingPackages, ...newPackages])].join(' ');
|
|
64
|
+
const merged = mergePackages(existing, apk);
|
|
154
65
|
baseConfig.features[featureKey].apk = merged;
|
|
155
66
|
}
|
|
156
67
|
return baseConfig;
|
|
@@ -226,12 +137,108 @@ function resolveDependencies(requestedOverlays, allOverlayDefs) {
|
|
|
226
137
|
},
|
|
227
138
|
};
|
|
228
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Prepare overlays for generation by loading configuration, building requested overlay list,
|
|
142
|
+
* filtering for minimal mode, checking compatibility, and resolving dependencies.
|
|
143
|
+
* This shared logic is used by both generateManifestOnly and composeDevContainer.
|
|
144
|
+
*/
|
|
145
|
+
function prepareOverlaysForGeneration(answers, overlaysDir) {
|
|
146
|
+
// 1. Load overlay configuration
|
|
147
|
+
const actualOverlaysDir = overlaysDir ?? path.join(REPO_ROOT, 'overlays');
|
|
148
|
+
const indexYmlPath = path.join(actualOverlaysDir, 'index.yml');
|
|
149
|
+
const overlaysConfig = loadOverlaysConfig(actualOverlaysDir, indexYmlPath);
|
|
150
|
+
// Collect all overlay definitions
|
|
151
|
+
const allOverlayDefs = getAllOverlayDefs(overlaysConfig);
|
|
152
|
+
// Build list of requested overlays
|
|
153
|
+
const requestedOverlays = [];
|
|
154
|
+
if (answers.language && answers.language.length > 0)
|
|
155
|
+
requestedOverlays.push(...answers.language);
|
|
156
|
+
if (answers.database && answers.database.length > 0)
|
|
157
|
+
requestedOverlays.push(...answers.database);
|
|
158
|
+
if (answers.observability)
|
|
159
|
+
requestedOverlays.push(...answers.observability);
|
|
160
|
+
if (answers.playwright)
|
|
161
|
+
requestedOverlays.push('playwright');
|
|
162
|
+
if (answers.cloudTools)
|
|
163
|
+
requestedOverlays.push(...answers.cloudTools);
|
|
164
|
+
if (answers.devTools)
|
|
165
|
+
requestedOverlays.push(...answers.devTools);
|
|
166
|
+
// Filter out "minimal" overlays if --minimal flag is set
|
|
167
|
+
let filteredRequestedOverlays = requestedOverlays;
|
|
168
|
+
if (answers.minimal) {
|
|
169
|
+
const minimalExcluded = [];
|
|
170
|
+
filteredRequestedOverlays = requestedOverlays.filter((overlayId) => {
|
|
171
|
+
const overlayDef = allOverlayDefs.find((o) => o.id === overlayId);
|
|
172
|
+
if (overlayDef?.minimal === true) {
|
|
173
|
+
minimalExcluded.push(overlayId);
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
return true;
|
|
177
|
+
});
|
|
178
|
+
if (minimalExcluded.length > 0) {
|
|
179
|
+
console.log(chalk.dim(` š¦ Minimal mode: Excluding ${minimalExcluded.length} optional overlay(s): ${minimalExcluded.join(', ')}`));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Check compatibility
|
|
183
|
+
const incompatible = [];
|
|
184
|
+
for (const overlayId of filteredRequestedOverlays) {
|
|
185
|
+
const overlayDef = allOverlayDefs.find((o) => o.id === overlayId);
|
|
186
|
+
if (overlayDef?.supports && overlayDef.supports.length > 0) {
|
|
187
|
+
if (!overlayDef.supports.includes(answers.stack)) {
|
|
188
|
+
incompatible.push(`${overlayId} (requires: ${overlayDef.supports.join(', ')})`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (incompatible.length > 0) {
|
|
193
|
+
console.log(chalk.yellow(`\nā ļø Warning: Some overlays are not compatible with '${answers.stack}' template:`));
|
|
194
|
+
incompatible.forEach((overlay) => {
|
|
195
|
+
console.log(chalk.yellow(` ⢠${overlay}`));
|
|
196
|
+
});
|
|
197
|
+
console.log(chalk.yellow(`\nThese overlays will be skipped.\n`));
|
|
198
|
+
// Filter out incompatible overlays
|
|
199
|
+
if (answers.database) {
|
|
200
|
+
answers.database = answers.database.filter((d) => !incompatible.some((i) => i.startsWith(d)));
|
|
201
|
+
}
|
|
202
|
+
if (answers.observability) {
|
|
203
|
+
answers.observability = answers.observability.filter((o) => !incompatible.some((i) => i.startsWith(o)));
|
|
204
|
+
}
|
|
205
|
+
// Update requestedOverlays after filtering
|
|
206
|
+
requestedOverlays.length = 0;
|
|
207
|
+
if (answers.language && answers.language.length > 0)
|
|
208
|
+
requestedOverlays.push(...answers.language);
|
|
209
|
+
if (answers.database && answers.database.length > 0)
|
|
210
|
+
requestedOverlays.push(...answers.database);
|
|
211
|
+
if (answers.observability)
|
|
212
|
+
requestedOverlays.push(...answers.observability);
|
|
213
|
+
if (answers.playwright)
|
|
214
|
+
requestedOverlays.push('playwright');
|
|
215
|
+
if (answers.cloudTools)
|
|
216
|
+
requestedOverlays.push(...answers.cloudTools);
|
|
217
|
+
if (answers.devTools)
|
|
218
|
+
requestedOverlays.push(...answers.devTools);
|
|
219
|
+
// Re-apply minimal filtering
|
|
220
|
+
filteredRequestedOverlays = requestedOverlays.filter((overlayId) => {
|
|
221
|
+
const overlayDef = allOverlayDefs.find((o) => o.id === overlayId);
|
|
222
|
+
return !answers.minimal || overlayDef?.minimal !== true;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
// Resolve dependencies
|
|
226
|
+
const { overlays: resolvedOverlays, autoResolved } = resolveDependencies(filteredRequestedOverlays, allOverlayDefs);
|
|
227
|
+
return {
|
|
228
|
+
overlays: resolvedOverlays,
|
|
229
|
+
autoResolved,
|
|
230
|
+
overlaysConfig,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
229
233
|
/**
|
|
230
234
|
* Generate superposition.json manifest
|
|
231
235
|
*/
|
|
232
236
|
function generateManifest(outputPath, answers, overlays, autoResolved, containerName) {
|
|
237
|
+
const toolVersion = getToolVersion();
|
|
233
238
|
const manifest = {
|
|
234
|
-
|
|
239
|
+
manifestVersion: CURRENT_MANIFEST_VERSION,
|
|
240
|
+
generatedBy: toolVersion,
|
|
241
|
+
version: '0.1.0', // Legacy field for backward compatibility
|
|
235
242
|
generated: new Date().toISOString(),
|
|
236
243
|
baseTemplate: answers.stack,
|
|
237
244
|
baseImage: answers.baseImage === 'custom' && answers.customImage
|
|
@@ -265,15 +272,74 @@ function generateManifest(outputPath, answers, overlays, autoResolved, container
|
|
|
265
272
|
console.log(chalk.cyan(` ā¹ļø Used preset: ${answers.preset}`));
|
|
266
273
|
}
|
|
267
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Load and resolve imports from shared files for an overlay
|
|
277
|
+
*/
|
|
278
|
+
function loadImportsForOverlay(overlayName, overlaysDir) {
|
|
279
|
+
let importedConfig = {};
|
|
280
|
+
// Load overlay manifest to get imports
|
|
281
|
+
const overlayDir = path.join(overlaysDir, overlayName);
|
|
282
|
+
const manifestPath = path.join(overlayDir, 'overlay.yml');
|
|
283
|
+
if (!fs.existsSync(manifestPath)) {
|
|
284
|
+
return importedConfig;
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
const manifestContent = fs.readFileSync(manifestPath, 'utf8');
|
|
288
|
+
const manifest = yaml.load(manifestContent);
|
|
289
|
+
if (!manifest.imports ||
|
|
290
|
+
!Array.isArray(manifest.imports) ||
|
|
291
|
+
manifest.imports.length === 0) {
|
|
292
|
+
return importedConfig;
|
|
293
|
+
}
|
|
294
|
+
// Process each import
|
|
295
|
+
for (const importPath of manifest.imports) {
|
|
296
|
+
const fullImportPath = path.join(overlaysDir, importPath);
|
|
297
|
+
if (!fs.existsSync(fullImportPath)) {
|
|
298
|
+
console.warn(chalk.yellow(`ā ļø Import not found: ${importPath} (for overlay: ${overlayName})`));
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
// Determine file type and merge appropriately
|
|
302
|
+
const ext = path.extname(importPath).toLowerCase();
|
|
303
|
+
if (ext === '.json') {
|
|
304
|
+
// JSON files are merged as devcontainer patches
|
|
305
|
+
const importedPatch = loadJson(fullImportPath);
|
|
306
|
+
importedConfig = deepMerge(importedConfig, importedPatch);
|
|
307
|
+
}
|
|
308
|
+
else if (ext === '.yaml' || ext === '.yml') {
|
|
309
|
+
// YAML files are loaded and merged as devcontainer patches
|
|
310
|
+
try {
|
|
311
|
+
const yamlContent = fs.readFileSync(fullImportPath, 'utf8');
|
|
312
|
+
const importedPatch = yaml.load(yamlContent);
|
|
313
|
+
if (importedPatch && typeof importedPatch === 'object') {
|
|
314
|
+
importedConfig = deepMerge(importedConfig, importedPatch);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
console.warn(chalk.yellow(`ā ļø Failed to parse YAML import: ${importPath}`));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// .env files are handled separately during env merging
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
console.warn(chalk.yellow(`ā ļø Failed to load imports for overlay: ${overlayName}`));
|
|
326
|
+
}
|
|
327
|
+
return importedConfig;
|
|
328
|
+
}
|
|
268
329
|
/**
|
|
269
330
|
* Apply an overlay to the base configuration
|
|
270
331
|
*/
|
|
271
|
-
function applyOverlay(baseConfig, overlayName) {
|
|
272
|
-
const overlayPath = path.join(
|
|
332
|
+
function applyOverlay(baseConfig, overlayName, overlaysDir) {
|
|
333
|
+
const overlayPath = path.join(overlaysDir, overlayName, 'devcontainer.patch.json');
|
|
273
334
|
if (!fs.existsSync(overlayPath)) {
|
|
274
335
|
console.warn(chalk.yellow(`ā ļø Overlay not found: ${overlayName}`));
|
|
275
336
|
return baseConfig;
|
|
276
337
|
}
|
|
338
|
+
// First, load and apply any imports
|
|
339
|
+
const importedConfig = loadImportsForOverlay(overlayName, overlaysDir);
|
|
340
|
+
if (Object.keys(importedConfig).length > 0) {
|
|
341
|
+
baseConfig = deepMerge(baseConfig, importedConfig);
|
|
342
|
+
}
|
|
277
343
|
const overlay = loadJson(overlayPath);
|
|
278
344
|
// Special handling for apt-get packages (legacy)
|
|
279
345
|
if (overlay.features?.['ghcr.io/devcontainers-extra/features/apt-get-packages:1']?.packages) {
|
|
@@ -378,8 +444,8 @@ function copyDir(src, dest) {
|
|
|
378
444
|
* Copy additional files from overlay to output directory
|
|
379
445
|
* Excludes devcontainer.patch.json and .env.example (handled separately)
|
|
380
446
|
*/
|
|
381
|
-
function copyOverlayFiles(outputPath, overlayName, registry) {
|
|
382
|
-
const overlayPath = path.join(
|
|
447
|
+
function copyOverlayFiles(outputPath, overlayName, registry, overlaysDir) {
|
|
448
|
+
const overlayPath = path.join(overlaysDir, overlayName);
|
|
383
449
|
if (!fs.existsSync(overlayPath)) {
|
|
384
450
|
return;
|
|
385
451
|
}
|
|
@@ -428,10 +494,37 @@ function copyOverlayFiles(outputPath, overlayName, registry) {
|
|
|
428
494
|
/**
|
|
429
495
|
* Merge .env.example files from overlays and apply glue config
|
|
430
496
|
*/
|
|
431
|
-
function mergeEnvExamples(outputPath, overlays, portOffset, glueConfig, presetName) {
|
|
497
|
+
function mergeEnvExamples(outputPath, overlays, overlaysDir, portOffset, glueConfig, presetName) {
|
|
432
498
|
const envSections = [];
|
|
433
499
|
for (const overlay of overlays) {
|
|
434
|
-
|
|
500
|
+
// First, check for imports in the overlay and add any .env files from imports
|
|
501
|
+
const overlayDir = path.join(overlaysDir, overlay);
|
|
502
|
+
const manifestPath = path.join(overlayDir, 'overlay.yml');
|
|
503
|
+
if (fs.existsSync(manifestPath)) {
|
|
504
|
+
try {
|
|
505
|
+
const manifestContent = fs.readFileSync(manifestPath, 'utf8');
|
|
506
|
+
const manifest = yaml.load(manifestContent);
|
|
507
|
+
if (manifest.imports && Array.isArray(manifest.imports)) {
|
|
508
|
+
for (const importPath of manifest.imports) {
|
|
509
|
+
const ext = path.extname(importPath).toLowerCase();
|
|
510
|
+
if (ext === '.env') {
|
|
511
|
+
const fullImportPath = path.join(overlaysDir, importPath);
|
|
512
|
+
if (fs.existsSync(fullImportPath)) {
|
|
513
|
+
const content = fs.readFileSync(fullImportPath, 'utf-8').trim();
|
|
514
|
+
if (content) {
|
|
515
|
+
envSections.push(`# Imported from ${importPath}\n${content}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
// Ignore errors reading manifest
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// Then add the overlay's own .env.example
|
|
527
|
+
const envPath = path.join(overlaysDir, overlay, '.env.example');
|
|
435
528
|
if (fs.existsSync(envPath)) {
|
|
436
529
|
const content = fs.readFileSync(envPath, 'utf-8').trim();
|
|
437
530
|
if (content) {
|
|
@@ -478,23 +571,6 @@ function mergeEnvExamples(outputPath, overlays, portOffset, glueConfig, presetNa
|
|
|
478
571
|
}
|
|
479
572
|
return true;
|
|
480
573
|
}
|
|
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
574
|
/**
|
|
499
575
|
* Apply preset glue configuration (README and port mappings)
|
|
500
576
|
* Note: Environment variables are handled in mergeEnvExamples to ensure proper port offset application
|
|
@@ -526,7 +602,7 @@ function applyGlueConfig(outputPath, glueConfig, presetName, fileRegistry) {
|
|
|
526
602
|
/**
|
|
527
603
|
* Merge docker-compose.yml files from base and overlays into a single file
|
|
528
604
|
*/
|
|
529
|
-
function mergeDockerComposeFiles(outputPath, baseStack, overlays, portOffset, customImage) {
|
|
605
|
+
function mergeDockerComposeFiles(outputPath, baseStack, overlays, overlaysDir, portOffset, customImage) {
|
|
530
606
|
const composeFiles = [];
|
|
531
607
|
// Add base docker-compose if exists
|
|
532
608
|
const baseComposePath = path.join(TEMPLATES_DIR, baseStack, '.devcontainer', 'docker-compose.yml');
|
|
@@ -535,7 +611,7 @@ function mergeDockerComposeFiles(outputPath, baseStack, overlays, portOffset, cu
|
|
|
535
611
|
}
|
|
536
612
|
// Add overlay docker-compose files
|
|
537
613
|
for (const overlay of overlays) {
|
|
538
|
-
const overlayComposePath = path.join(
|
|
614
|
+
const overlayComposePath = path.join(overlaysDir, overlay, 'docker-compose.yml');
|
|
539
615
|
if (fs.existsSync(overlayComposePath)) {
|
|
540
616
|
composeFiles.push(overlayComposePath);
|
|
541
617
|
}
|
|
@@ -583,13 +659,17 @@ function mergeDockerComposeFiles(outputPath, baseStack, overlays, portOffset, cu
|
|
|
583
659
|
}
|
|
584
660
|
// Filter depends_on to only include services that exist
|
|
585
661
|
const serviceNames = Object.keys(merged.services);
|
|
662
|
+
const serviceNameSet = new Set(serviceNames);
|
|
586
663
|
for (const serviceName of serviceNames) {
|
|
587
664
|
const service = merged.services[serviceName];
|
|
588
|
-
if (service.depends_on
|
|
589
|
-
|
|
590
|
-
if (
|
|
665
|
+
if (service.depends_on !== undefined) {
|
|
666
|
+
const filteredDependsOn = filterDependsOn(service.depends_on, serviceNameSet);
|
|
667
|
+
if (filteredDependsOn === undefined) {
|
|
591
668
|
delete service.depends_on;
|
|
592
669
|
}
|
|
670
|
+
else {
|
|
671
|
+
service.depends_on = filteredDependsOn;
|
|
672
|
+
}
|
|
593
673
|
}
|
|
594
674
|
}
|
|
595
675
|
// Remove empty sections
|
|
@@ -800,71 +880,35 @@ function copyCustomFiles(customConfig, outputPath, fileRegistry) {
|
|
|
800
880
|
fileRegistry.addFile(relativeDest);
|
|
801
881
|
}
|
|
802
882
|
}
|
|
883
|
+
/**
|
|
884
|
+
* Generate only the superposition.json manifest without creating .devcontainer files
|
|
885
|
+
* Used for team collaboration workflow where manifest is committed but .devcontainer is gitignored
|
|
886
|
+
*/
|
|
887
|
+
export async function generateManifestOnly(answers, overlaysDir) {
|
|
888
|
+
// Prepare overlays using shared logic
|
|
889
|
+
const { overlays: resolvedOverlays, autoResolved } = prepareOverlaysForGeneration(answers, overlaysDir);
|
|
890
|
+
// Ensure output directory exists
|
|
891
|
+
const outputPath = answers.outputPath || '.';
|
|
892
|
+
if (!fs.existsSync(outputPath)) {
|
|
893
|
+
fs.mkdirSync(outputPath, { recursive: true });
|
|
894
|
+
}
|
|
895
|
+
// Generate manifest only
|
|
896
|
+
console.log(chalk.cyan('\nš Generating manifest only (team collaboration mode)...\n'));
|
|
897
|
+
generateManifest(outputPath, answers, resolvedOverlays, autoResolved, answers.containerName);
|
|
898
|
+
console.log(chalk.green(`\nā Manifest created: ${path.join(outputPath, 'superposition.json')}`));
|
|
899
|
+
console.log(chalk.dim(' Ready for team collaboration workflow.'));
|
|
900
|
+
console.log(chalk.dim(' Commit this manifest to your repository and let team members run "npx container-superposition regen"'));
|
|
901
|
+
}
|
|
803
902
|
/**
|
|
804
903
|
* Main composition logic
|
|
805
904
|
*/
|
|
806
|
-
export async function composeDevContainer(answers) {
|
|
807
|
-
//
|
|
808
|
-
const
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
// Collect all overlay definitions
|
|
905
|
+
export async function composeDevContainer(answers, overlaysDir) {
|
|
906
|
+
// Prepare overlays using shared logic
|
|
907
|
+
const actualOverlaysDir = overlaysDir ?? path.join(REPO_ROOT, 'overlays');
|
|
908
|
+
const { overlays: resolvedOverlays, autoResolved, overlaysConfig, } = prepareOverlaysForGeneration(answers, overlaysDir);
|
|
909
|
+
// Get all overlay definitions for later use
|
|
812
910
|
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
|
|
911
|
+
// Determine base template path
|
|
868
912
|
const templatePath = path.join(TEMPLATES_DIR, answers.stack, '.devcontainer');
|
|
869
913
|
if (!fs.existsSync(templatePath)) {
|
|
870
914
|
throw new Error(`Template not found: ${answers.stack}`);
|
|
@@ -946,7 +990,7 @@ export async function composeDevContainer(answers) {
|
|
|
946
990
|
// 6. Apply overlays
|
|
947
991
|
for (const overlay of overlays) {
|
|
948
992
|
console.log(chalk.dim(` š§ Applying overlay: ${chalk.cyan(overlay)}`));
|
|
949
|
-
config = applyOverlay(config, overlay);
|
|
993
|
+
config = applyOverlay(config, overlay, actualOverlaysDir);
|
|
950
994
|
}
|
|
951
995
|
// 7. Copy template files (docker-compose, scripts, etc.)
|
|
952
996
|
const entries = fs.readdirSync(templatePath);
|
|
@@ -967,7 +1011,7 @@ export async function composeDevContainer(answers) {
|
|
|
967
1011
|
}
|
|
968
1012
|
// 8. Copy overlay files (docker-compose, configs, etc.)
|
|
969
1013
|
for (const overlay of overlays) {
|
|
970
|
-
copyOverlayFiles(outputPath, overlay, fileRegistry);
|
|
1014
|
+
copyOverlayFiles(outputPath, overlay, fileRegistry, actualOverlaysDir);
|
|
971
1015
|
}
|
|
972
1016
|
// 8.5. Copy cross-distro-packages feature if used
|
|
973
1017
|
if (config.features?.['./features/cross-distro-packages']) {
|
|
@@ -982,11 +1026,11 @@ export async function composeDevContainer(answers) {
|
|
|
982
1026
|
// 8. Filter docker-compose dependencies based on selected overlays
|
|
983
1027
|
filterDockerComposeDependencies(outputPath, overlays);
|
|
984
1028
|
// 9. Merge runServices array in correct order
|
|
985
|
-
mergeRunServices(config, overlays);
|
|
1029
|
+
mergeRunServices(config, overlays, actualOverlaysDir);
|
|
986
1030
|
// 11. Merge docker-compose files into single combined file
|
|
987
1031
|
if (answers.stack === 'compose') {
|
|
988
1032
|
const customImage = config._customImage;
|
|
989
|
-
mergeDockerComposeFiles(outputPath, answers.stack, overlays, answers.portOffset, customImage);
|
|
1033
|
+
mergeDockerComposeFiles(outputPath, answers.stack, overlays, actualOverlaysDir, answers.portOffset, customImage);
|
|
990
1034
|
// Update devcontainer.json to reference the combined file
|
|
991
1035
|
if (config.dockerComposeFile) {
|
|
992
1036
|
config.dockerComposeFile = 'docker-compose.yml';
|
|
@@ -997,7 +1041,7 @@ export async function composeDevContainer(answers) {
|
|
|
997
1041
|
applyPortOffsetToDevcontainer(config, answers.portOffset);
|
|
998
1042
|
}
|
|
999
1043
|
// Merge setup scripts from overlays into postCreateCommand
|
|
1000
|
-
mergeSetupScripts(config, overlays, outputPath, fileRegistry);
|
|
1044
|
+
mergeSetupScripts(config, overlays, outputPath, fileRegistry, actualOverlaysDir);
|
|
1001
1045
|
// 10. Apply custom patches from .devcontainer/custom/ (if present)
|
|
1002
1046
|
const customPatches = loadCustomPatches(outputPath);
|
|
1003
1047
|
if (customPatches) {
|
|
@@ -1015,6 +1059,25 @@ export async function composeDevContainer(answers) {
|
|
|
1015
1059
|
delete config[key];
|
|
1016
1060
|
}
|
|
1017
1061
|
});
|
|
1062
|
+
// Handle editor profile filtering
|
|
1063
|
+
if (answers.editor === 'none' || answers.editor === 'jetbrains') {
|
|
1064
|
+
// Remove VS Code customizations
|
|
1065
|
+
if (config.customizations?.vscode) {
|
|
1066
|
+
if (answers.editor === 'none') {
|
|
1067
|
+
delete config.customizations.vscode;
|
|
1068
|
+
console.log(chalk.dim(` šØ Editor profile 'none': Removed VS Code customizations`));
|
|
1069
|
+
}
|
|
1070
|
+
else if (answers.editor === 'jetbrains') {
|
|
1071
|
+
// For JetBrains, remove VS Code customizations (future: could add JetBrains-specific settings)
|
|
1072
|
+
delete config.customizations.vscode;
|
|
1073
|
+
console.log(chalk.dim(` šØ Editor profile 'jetbrains': Removed VS Code customizations`));
|
|
1074
|
+
}
|
|
1075
|
+
// Clean up empty customizations object
|
|
1076
|
+
if (config.customizations && Object.keys(config.customizations).length === 0) {
|
|
1077
|
+
delete config.customizations;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1018
1081
|
// 12. Write merged devcontainer.json
|
|
1019
1082
|
const configPath = path.join(outputPath, 'devcontainer.json');
|
|
1020
1083
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
@@ -1028,7 +1091,7 @@ export async function composeDevContainer(answers) {
|
|
|
1028
1091
|
generateManifest(outputPath, answers, overlays, autoResolved, answers.containerName || config.name);
|
|
1029
1092
|
fileRegistry.addFile('superposition.json');
|
|
1030
1093
|
// 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);
|
|
1094
|
+
const envCreated = mergeEnvExamples(outputPath, overlays, actualOverlaysDir, answers.portOffset, answers.presetGlueConfig, answers.preset);
|
|
1032
1095
|
if (envCreated) {
|
|
1033
1096
|
fileRegistry.addFile('.env.example');
|
|
1034
1097
|
}
|
|
@@ -1050,7 +1113,42 @@ export async function composeDevContainer(answers) {
|
|
|
1050
1113
|
generateReadme(answers, overlays, overlayMetadataMap, outputPath);
|
|
1051
1114
|
fileRegistry.addFile('README.md');
|
|
1052
1115
|
console.log(chalk.dim(` š Created README.md with documentation from ${overlays.length} overlay(s)`));
|
|
1053
|
-
// 17.
|
|
1116
|
+
// 17. Generate ports.json documentation
|
|
1117
|
+
const portOffset = answers.portOffset ?? 0;
|
|
1118
|
+
if (portOffset > 0 || overlays.some((o) => overlayMetadataMap.get(o)?.ports?.length)) {
|
|
1119
|
+
console.log(chalk.cyan('\nš” Generating ports documentation...'));
|
|
1120
|
+
const selectedOverlayMetadata = overlays
|
|
1121
|
+
.map((id) => overlayMetadataMap.get(id))
|
|
1122
|
+
.filter((m) => m !== undefined);
|
|
1123
|
+
// Extract environment variables from .env.example for connection strings
|
|
1124
|
+
const envPath = path.join(outputPath, '.env.example');
|
|
1125
|
+
const envVars = {};
|
|
1126
|
+
if (fs.existsSync(envPath)) {
|
|
1127
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
1128
|
+
for (const line of envContent.split('\n')) {
|
|
1129
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
|
|
1130
|
+
if (match) {
|
|
1131
|
+
envVars[match[1].toLowerCase()] = match[2];
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
const portsDoc = generatePortsDocumentation(selectedOverlayMetadata, portOffset, envVars);
|
|
1136
|
+
const portsPath = path.join(outputPath, 'ports.json');
|
|
1137
|
+
fs.writeFileSync(portsPath, JSON.stringify(portsDoc, null, 2) + '\n');
|
|
1138
|
+
fileRegistry.addFile('ports.json');
|
|
1139
|
+
console.log(chalk.dim(` š” Created ports.json with ${portsDoc.ports.length} port(s)`));
|
|
1140
|
+
// Log summary of ports
|
|
1141
|
+
if (portsDoc.ports.length > 0) {
|
|
1142
|
+
console.log(chalk.dim('\n Available services:'));
|
|
1143
|
+
for (const port of portsDoc.ports) {
|
|
1144
|
+
const serviceLabel = port.service || 'unknown';
|
|
1145
|
+
const desc = port.description ? ` - ${port.description}` : '';
|
|
1146
|
+
const proto = port.protocol ? ` (${port.protocol})` : '';
|
|
1147
|
+
console.log(chalk.dim(` ⢠${serviceLabel}: ${port.actualPort}${proto}${desc}`));
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
// 18. Clean up stale files from previous runs (preserves superposition.json and .env)
|
|
1054
1152
|
cleanupStaleFiles(outputPath, fileRegistry);
|
|
1055
1153
|
}
|
|
1056
1154
|
/**
|
|
@@ -1084,7 +1182,7 @@ function applyPortOffsetToDevcontainer(config, offset) {
|
|
|
1084
1182
|
/**
|
|
1085
1183
|
* Merge setup scripts from overlays into postCreateCommand
|
|
1086
1184
|
*/
|
|
1087
|
-
function mergeSetupScripts(config, overlays, outputPath, fileRegistry) {
|
|
1185
|
+
function mergeSetupScripts(config, overlays, outputPath, fileRegistry, overlaysDir) {
|
|
1088
1186
|
const setupScripts = [];
|
|
1089
1187
|
const verifyScripts = [];
|
|
1090
1188
|
// Create scripts subfolder
|
|
@@ -1093,14 +1191,14 @@ function mergeSetupScripts(config, overlays, outputPath, fileRegistry) {
|
|
|
1093
1191
|
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
1094
1192
|
}
|
|
1095
1193
|
// 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(
|
|
1194
|
+
const hasScripts = overlays.some((o) => fs.existsSync(path.join(overlaysDir, o, 'setup.sh')) ||
|
|
1195
|
+
fs.existsSync(path.join(overlaysDir, o, 'verify.sh')));
|
|
1098
1196
|
if (hasScripts) {
|
|
1099
1197
|
fileRegistry.addDirectory('scripts');
|
|
1100
1198
|
}
|
|
1101
1199
|
for (const overlay of overlays) {
|
|
1102
1200
|
// Handle setup scripts
|
|
1103
|
-
const setupPath = path.join(
|
|
1201
|
+
const setupPath = path.join(overlaysDir, overlay, 'setup.sh');
|
|
1104
1202
|
if (fs.existsSync(setupPath)) {
|
|
1105
1203
|
// Copy setup script to scripts subdirectory
|
|
1106
1204
|
const destPath = path.join(scriptsDir, `setup-${overlay}.sh`);
|
|
@@ -1111,7 +1209,7 @@ function mergeSetupScripts(config, overlays, outputPath, fileRegistry) {
|
|
|
1111
1209
|
setupScripts.push(`bash .devcontainer/scripts/setup-${overlay}.sh`);
|
|
1112
1210
|
}
|
|
1113
1211
|
// Handle verify scripts
|
|
1114
|
-
const verifyPath = path.join(
|
|
1212
|
+
const verifyPath = path.join(overlaysDir, overlay, 'verify.sh');
|
|
1115
1213
|
if (fs.existsSync(verifyPath)) {
|
|
1116
1214
|
// Copy verify script to scripts subdirectory
|
|
1117
1215
|
const destPath = path.join(scriptsDir, `verify-${overlay}.sh`);
|
|
@@ -1134,7 +1232,7 @@ function mergeSetupScripts(config, overlays, outputPath, fileRegistry) {
|
|
|
1134
1232
|
// Add setup scripts
|
|
1135
1233
|
for (let i = 0; i < setupScripts.length; i++) {
|
|
1136
1234
|
const overlay = overlays.filter((o) => {
|
|
1137
|
-
const setupPath = path.join(
|
|
1235
|
+
const setupPath = path.join(overlaysDir, o, 'setup.sh');
|
|
1138
1236
|
return fs.existsSync(setupPath);
|
|
1139
1237
|
})[i];
|
|
1140
1238
|
config.postCreateCommand[`setup-${overlay}`] = setupScripts[i];
|
|
@@ -1153,7 +1251,7 @@ function mergeSetupScripts(config, overlays, outputPath, fileRegistry) {
|
|
|
1153
1251
|
// Add verify scripts
|
|
1154
1252
|
for (let i = 0; i < verifyScripts.length; i++) {
|
|
1155
1253
|
const overlay = overlays.filter((o) => {
|
|
1156
|
-
const verifyPath = path.join(
|
|
1254
|
+
const verifyPath = path.join(overlaysDir, o, 'verify.sh');
|
|
1157
1255
|
return fs.existsSync(verifyPath);
|
|
1158
1256
|
})[i];
|
|
1159
1257
|
config.postStartCommand[`verify-${overlay}`] = verifyScripts[i];
|
|
@@ -1208,10 +1306,10 @@ function filterDockerComposeDependencies(outputPath, selectedOverlays) {
|
|
|
1208
1306
|
/**
|
|
1209
1307
|
* Merge runServices from all overlays in correct order
|
|
1210
1308
|
*/
|
|
1211
|
-
function mergeRunServices(config, overlays) {
|
|
1309
|
+
function mergeRunServices(config, overlays, overlaysDir) {
|
|
1212
1310
|
const services = [];
|
|
1213
1311
|
for (const overlay of overlays) {
|
|
1214
|
-
const overlayPath = path.join(
|
|
1312
|
+
const overlayPath = path.join(overlaysDir, overlay, 'devcontainer.patch.json');
|
|
1215
1313
|
if (fs.existsSync(overlayPath)) {
|
|
1216
1314
|
const overlayConfig = loadJson(overlayPath);
|
|
1217
1315
|
if (overlayConfig.runServices) {
|