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
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
# Merge Strategy Specification
|
|
2
|
+
|
|
3
|
+
This document defines the **exact, deterministic merge behavior** for container-superposition composition. All merge operations must follow these rules to ensure predictable and reproducible results.
|
|
4
|
+
|
|
5
|
+
## Design Principles
|
|
6
|
+
|
|
7
|
+
1. **Deterministic**: Given the same inputs, merging always produces the same output
|
|
8
|
+
2. **Explicit**: No magic or undocumented special cases
|
|
9
|
+
3. **Last Writer Wins**: For conflicting primitive values, later overlays override earlier ones
|
|
10
|
+
4. **Union by Default**: Arrays and collections are merged (unioned), not replaced
|
|
11
|
+
5. **Deep Merge**: Objects are recursively merged, not replaced wholesale
|
|
12
|
+
|
|
13
|
+
## File Types and Merge Strategies
|
|
14
|
+
|
|
15
|
+
### devcontainer.json (JSON Merging)
|
|
16
|
+
|
|
17
|
+
DevContainer configurations use **deep object merging** with field-specific strategies for arrays.
|
|
18
|
+
|
|
19
|
+
#### Object Merging
|
|
20
|
+
|
|
21
|
+
**Rule**: Objects are recursively merged. When both target and source contain the same key:
|
|
22
|
+
|
|
23
|
+
- If both values are objects (non-array): recursively merge
|
|
24
|
+
- If both values are arrays: apply array-specific strategy (see below)
|
|
25
|
+
- If values are primitives: source overwrites target (last writer wins)
|
|
26
|
+
|
|
27
|
+
**Example**:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
// Base
|
|
31
|
+
{
|
|
32
|
+
"customizations": {
|
|
33
|
+
"vscode": {
|
|
34
|
+
"settings": {
|
|
35
|
+
"editor.fontSize": 14
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Overlay
|
|
42
|
+
{
|
|
43
|
+
"customizations": {
|
|
44
|
+
"vscode": {
|
|
45
|
+
"settings": {
|
|
46
|
+
"editor.tabSize": 2
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Result (deep merged)
|
|
53
|
+
{
|
|
54
|
+
"customizations": {
|
|
55
|
+
"vscode": {
|
|
56
|
+
"settings": {
|
|
57
|
+
"editor.fontSize": 14,
|
|
58
|
+
"editor.tabSize": 2
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
#### Array Merge Strategies
|
|
66
|
+
|
|
67
|
+
Arrays have **field-specific merge strategies** to ensure correctness:
|
|
68
|
+
|
|
69
|
+
**Union Strategy** (default for most arrays):
|
|
70
|
+
|
|
71
|
+
- Concatenate arrays
|
|
72
|
+
- Deduplicate elements
|
|
73
|
+
- Preserve order (target items first, then source items)
|
|
74
|
+
- Fields: `features`, `extensions`, `mounts`, `forwardPorts`, `runArgs`, `capAdd`
|
|
75
|
+
|
|
76
|
+
**Example**:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
// Base
|
|
80
|
+
{
|
|
81
|
+
"forwardPorts": [3000, 8080]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Overlay
|
|
85
|
+
{
|
|
86
|
+
"forwardPorts": [8080, 9090]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Result (union, deduplicated)
|
|
90
|
+
{
|
|
91
|
+
"forwardPorts": [3000, 8080, 9090]
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Features Object Merge**:
|
|
96
|
+
|
|
97
|
+
- Features are objects with keys being feature identifiers
|
|
98
|
+
- Feature configurations are deep merged
|
|
99
|
+
- If same feature appears in multiple overlays, configurations are merged
|
|
100
|
+
|
|
101
|
+
**Example**:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
// Base
|
|
105
|
+
{
|
|
106
|
+
"features": {
|
|
107
|
+
"ghcr.io/devcontainers/features/node:1": {
|
|
108
|
+
"version": "lts"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Overlay
|
|
114
|
+
{
|
|
115
|
+
"features": {
|
|
116
|
+
"ghcr.io/devcontainers/features/node:1": {
|
|
117
|
+
"nodeGypDependencies": true
|
|
118
|
+
},
|
|
119
|
+
"ghcr.io/devcontainers/features/git:1": {}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Result (features merged)
|
|
124
|
+
{
|
|
125
|
+
"features": {
|
|
126
|
+
"ghcr.io/devcontainers/features/node:1": {
|
|
127
|
+
"version": "lts",
|
|
128
|
+
"nodeGypDependencies": true
|
|
129
|
+
},
|
|
130
|
+
"ghcr.io/devcontainers/features/git:1": {}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### Special Field: remoteEnv
|
|
136
|
+
|
|
137
|
+
**Rule**: Environment variables are merged with intelligent PATH handling.
|
|
138
|
+
|
|
139
|
+
**For PATH variables**:
|
|
140
|
+
|
|
141
|
+
1. Split both target and source PATH on `:` (preserving `${...}` references)
|
|
142
|
+
2. Filter out `${containerEnv:PATH}` placeholder from both
|
|
143
|
+
3. Concatenate and deduplicate path components
|
|
144
|
+
4. Append `${containerEnv:PATH}` at the end
|
|
145
|
+
|
|
146
|
+
**For other environment variables**:
|
|
147
|
+
|
|
148
|
+
- Source overwrites target (last writer wins)
|
|
149
|
+
|
|
150
|
+
**Example**:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
// Base
|
|
154
|
+
{
|
|
155
|
+
"remoteEnv": {
|
|
156
|
+
"PATH": "/usr/local/bin:${containerEnv:PATH}",
|
|
157
|
+
"NODE_ENV": "development"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Overlay
|
|
162
|
+
{
|
|
163
|
+
"remoteEnv": {
|
|
164
|
+
"PATH": "${containerEnv:HOME}/.local/bin:${containerEnv:PATH}",
|
|
165
|
+
"NODE_ENV": "production"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Result
|
|
170
|
+
{
|
|
171
|
+
"remoteEnv": {
|
|
172
|
+
"PATH": "/usr/local/bin:${containerEnv:HOME}/.local/bin:${containerEnv:PATH}",
|
|
173
|
+
"NODE_ENV": "production"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### Special Field: portsAttributes
|
|
179
|
+
|
|
180
|
+
**Rule**: Port attributes are merged by port number key.
|
|
181
|
+
|
|
182
|
+
**Example**:
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
// Base
|
|
186
|
+
{
|
|
187
|
+
"portsAttributes": {
|
|
188
|
+
"3000": {
|
|
189
|
+
"label": "Dev Server"
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Overlay
|
|
195
|
+
{
|
|
196
|
+
"portsAttributes": {
|
|
197
|
+
"3000": {
|
|
198
|
+
"onAutoForward": "openBrowser"
|
|
199
|
+
},
|
|
200
|
+
"8080": {
|
|
201
|
+
"label": "API"
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Result (merged by port key)
|
|
207
|
+
{
|
|
208
|
+
"portsAttributes": {
|
|
209
|
+
"3000": {
|
|
210
|
+
"label": "Dev Server",
|
|
211
|
+
"onAutoForward": "openBrowser"
|
|
212
|
+
},
|
|
213
|
+
"8080": {
|
|
214
|
+
"label": "API"
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### docker-compose.yml (YAML Merging)
|
|
221
|
+
|
|
222
|
+
Docker Compose files use **service-based deep merging**.
|
|
223
|
+
|
|
224
|
+
#### Service Merging
|
|
225
|
+
|
|
226
|
+
**Rule**: Services are merged by service name using deep object merge.
|
|
227
|
+
|
|
228
|
+
**Example**:
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
# Base
|
|
232
|
+
services:
|
|
233
|
+
devcontainer:
|
|
234
|
+
image: mcr.microsoft.com/devcontainers/base:ubuntu
|
|
235
|
+
volumes:
|
|
236
|
+
- ../:/workspace:cached
|
|
237
|
+
|
|
238
|
+
# Overlay
|
|
239
|
+
services:
|
|
240
|
+
devcontainer:
|
|
241
|
+
environment:
|
|
242
|
+
NODE_ENV: development
|
|
243
|
+
ports:
|
|
244
|
+
- "3000:3000"
|
|
245
|
+
|
|
246
|
+
# Result (deep merged)
|
|
247
|
+
services:
|
|
248
|
+
devcontainer:
|
|
249
|
+
image: mcr.microsoft.com/devcontainers/base:ubuntu
|
|
250
|
+
volumes:
|
|
251
|
+
- ../:/workspace:cached
|
|
252
|
+
environment:
|
|
253
|
+
NODE_ENV: development
|
|
254
|
+
ports:
|
|
255
|
+
- "3000:3000"
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
#### Array Fields in Services
|
|
259
|
+
|
|
260
|
+
**Rule**: Service array fields (volumes, ports, environment, etc.) are **concatenated and deduplicated**.
|
|
261
|
+
|
|
262
|
+
**Example**:
|
|
263
|
+
|
|
264
|
+
```yaml
|
|
265
|
+
# Base service
|
|
266
|
+
volumes:
|
|
267
|
+
- postgres-data:/var/lib/postgresql/data
|
|
268
|
+
|
|
269
|
+
# Overlay adds to same service
|
|
270
|
+
volumes:
|
|
271
|
+
- postgres-data:/var/lib/postgresql/data
|
|
272
|
+
- ./backups:/backups
|
|
273
|
+
|
|
274
|
+
# Result (union)
|
|
275
|
+
volumes:
|
|
276
|
+
- postgres-data:/var/lib/postgresql/data
|
|
277
|
+
- ./backups:/backups
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### Special Field: depends_on
|
|
281
|
+
|
|
282
|
+
**Rule**: Filter dependencies to only include services that exist in the final composition.
|
|
283
|
+
|
|
284
|
+
**Supported syntaxes**:
|
|
285
|
+
|
|
286
|
+
- Array form: `depends_on: [serviceA, serviceB]`
|
|
287
|
+
- Object form: `depends_on: { serviceA: { condition: ... } }`
|
|
288
|
+
|
|
289
|
+
**Example**:
|
|
290
|
+
|
|
291
|
+
```yaml
|
|
292
|
+
# Overlay defines dependency
|
|
293
|
+
services:
|
|
294
|
+
app:
|
|
295
|
+
depends_on:
|
|
296
|
+
- postgres
|
|
297
|
+
- redis
|
|
298
|
+
- rabbitmq # This service doesn't exist
|
|
299
|
+
|
|
300
|
+
# After filtering (rabbitmq removed)
|
|
301
|
+
services:
|
|
302
|
+
app:
|
|
303
|
+
depends_on:
|
|
304
|
+
- postgres
|
|
305
|
+
- redis
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
#### Volumes and Networks
|
|
309
|
+
|
|
310
|
+
**Rule**: Volumes and networks are merged by name.
|
|
311
|
+
|
|
312
|
+
**Example**:
|
|
313
|
+
|
|
314
|
+
```yaml
|
|
315
|
+
# Base
|
|
316
|
+
volumes:
|
|
317
|
+
postgres-data:
|
|
318
|
+
|
|
319
|
+
networks:
|
|
320
|
+
devnet:
|
|
321
|
+
|
|
322
|
+
# Overlay
|
|
323
|
+
volumes:
|
|
324
|
+
redis-data:
|
|
325
|
+
|
|
326
|
+
# Result (merged by key)
|
|
327
|
+
volumes:
|
|
328
|
+
postgres-data:
|
|
329
|
+
redis-data:
|
|
330
|
+
|
|
331
|
+
networks:
|
|
332
|
+
devnet:
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
#### Port Offset
|
|
336
|
+
|
|
337
|
+
**Rule**: When port offset is specified, it's applied to **host ports only** (not container ports).
|
|
338
|
+
|
|
339
|
+
**Format**: `"host:container"` → `"(host+offset):container"`
|
|
340
|
+
|
|
341
|
+
**Example**:
|
|
342
|
+
|
|
343
|
+
```yaml
|
|
344
|
+
# Before offset (offset = 100)
|
|
345
|
+
ports:
|
|
346
|
+
- "5432:5432"
|
|
347
|
+
- "6379:6379"
|
|
348
|
+
|
|
349
|
+
# After offset
|
|
350
|
+
ports:
|
|
351
|
+
- "5532:5432" # host port shifted
|
|
352
|
+
- "6479:6379" # host port shifted
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### .env Files
|
|
356
|
+
|
|
357
|
+
Environment files use **simple key-value merging** with precedence rules.
|
|
358
|
+
|
|
359
|
+
#### Merge Precedence
|
|
360
|
+
|
|
361
|
+
From lowest to highest priority:
|
|
362
|
+
|
|
363
|
+
1. Base template `.env.example`
|
|
364
|
+
2. Overlay `.env.example` files (in application order)
|
|
365
|
+
3. Overlay imports (`.env` files from `.shared/`)
|
|
366
|
+
4. Custom `.env.example` (if present)
|
|
367
|
+
5. Manifest overrides (from preset `glueConfig.environment`)
|
|
368
|
+
|
|
369
|
+
**Rule**: Later sources override earlier sources for the same key.
|
|
370
|
+
|
|
371
|
+
#### Merge Algorithm
|
|
372
|
+
|
|
373
|
+
1. Start with empty result
|
|
374
|
+
2. For each source (in precedence order):
|
|
375
|
+
- Parse key=value pairs
|
|
376
|
+
- Add to result (overwriting existing keys)
|
|
377
|
+
3. Generate combined `.env.example` with all sections
|
|
378
|
+
4. If port offset specified, also generate `.env` with offset values
|
|
379
|
+
|
|
380
|
+
**Example**:
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
# Base overlay .env.example
|
|
384
|
+
POSTGRES_VERSION=15
|
|
385
|
+
POSTGRES_PORT=5432
|
|
386
|
+
|
|
387
|
+
# Another overlay .env.example
|
|
388
|
+
POSTGRES_PORT=5433 # Conflict!
|
|
389
|
+
REDIS_PORT=6379
|
|
390
|
+
|
|
391
|
+
# Preset glueConfig.environment
|
|
392
|
+
POSTGRES_USER=myapp
|
|
393
|
+
|
|
394
|
+
# Result (later wins for conflicts)
|
|
395
|
+
POSTGRES_VERSION=15
|
|
396
|
+
POSTGRES_PORT=5433 # Overlay 2 wins
|
|
397
|
+
REDIS_PORT=6379
|
|
398
|
+
POSTGRES_USER=myapp # From preset
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
#### Port Offset in .env Files
|
|
402
|
+
|
|
403
|
+
**Rule**: Variables matching pattern `*PORT*=\d+` are automatically offset.
|
|
404
|
+
|
|
405
|
+
**Example**:
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
# .env.example (before offset)
|
|
409
|
+
POSTGRES_PORT=5432
|
|
410
|
+
GRAFANA_HTTP_PORT=3000
|
|
411
|
+
APP_NAME=myapp # Not affected
|
|
412
|
+
|
|
413
|
+
# .env (with offset=100)
|
|
414
|
+
POSTGRES_PORT=5532 # Offset applied
|
|
415
|
+
GRAFANA_HTTP_PORT=3100 # Offset applied
|
|
416
|
+
APP_NAME=myapp # Not affected
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Package Merging
|
|
420
|
+
|
|
421
|
+
### apt-get-packages Feature
|
|
422
|
+
|
|
423
|
+
**Rule**: Space-separated package lists are split, merged, and deduplicated.
|
|
424
|
+
|
|
425
|
+
**Example**:
|
|
426
|
+
|
|
427
|
+
```json
|
|
428
|
+
// Base
|
|
429
|
+
{
|
|
430
|
+
"features": {
|
|
431
|
+
"ghcr.io/devcontainers-extra/features/apt-get-packages:1": {
|
|
432
|
+
"packages": "curl wget"
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Overlay
|
|
438
|
+
{
|
|
439
|
+
"features": {
|
|
440
|
+
"ghcr.io/devcontainers-extra/features/apt-get-packages:1": {
|
|
441
|
+
"packages": "wget jq"
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Result (deduplicated)
|
|
447
|
+
{
|
|
448
|
+
"features": {
|
|
449
|
+
"ghcr.io/devcontainers-extra/features/apt-get-packages:1": {
|
|
450
|
+
"packages": "curl wget jq"
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### cross-distro-packages Feature
|
|
457
|
+
|
|
458
|
+
**Rule**: Both `apt` and `apk` package lists are merged independently.
|
|
459
|
+
|
|
460
|
+
**Example**:
|
|
461
|
+
|
|
462
|
+
```json
|
|
463
|
+
// Base
|
|
464
|
+
{
|
|
465
|
+
"features": {
|
|
466
|
+
"./features/cross-distro-packages": {
|
|
467
|
+
"apt": "build-essential wget",
|
|
468
|
+
"apk": "build-base wget"
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Overlay
|
|
474
|
+
{
|
|
475
|
+
"features": {
|
|
476
|
+
"./features/cross-distro-packages": {
|
|
477
|
+
"apt": "wget curl",
|
|
478
|
+
"apk": "wget curl"
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Result (each distro merged independently)
|
|
484
|
+
{
|
|
485
|
+
"features": {
|
|
486
|
+
"./features/cross-distro-packages": {
|
|
487
|
+
"apt": "build-essential wget curl",
|
|
488
|
+
"apk": "build-base wget curl"
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
## Lifecycle Commands
|
|
495
|
+
|
|
496
|
+
### postCreateCommand and postStartCommand
|
|
497
|
+
|
|
498
|
+
**Rule**: Commands are **concatenated** with `&&` operator, not merged.
|
|
499
|
+
|
|
500
|
+
**Example**:
|
|
501
|
+
|
|
502
|
+
```json
|
|
503
|
+
// Base
|
|
504
|
+
{
|
|
505
|
+
"postCreateCommand": "npm install"
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Overlay
|
|
509
|
+
{
|
|
510
|
+
"postCreateCommand": "bash setup-nodejs.sh"
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Result (concatenated)
|
|
514
|
+
{
|
|
515
|
+
"postCreateCommand": "npm install && bash setup-nodejs.sh"
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
**Note**: Scripts are copied to `.devcontainer/scripts/` and referenced, not inlined.
|
|
520
|
+
|
|
521
|
+
## Application Order
|
|
522
|
+
|
|
523
|
+
Overlays are applied in **category order** to ensure consistent precedence:
|
|
524
|
+
|
|
525
|
+
1. Base template (plain or compose)
|
|
526
|
+
2. Language overlays
|
|
527
|
+
3. Database overlays
|
|
528
|
+
4. Observability overlays
|
|
529
|
+
5. Cloud tool overlays
|
|
530
|
+
6. Dev tool overlays
|
|
531
|
+
7. Custom patches (if present)
|
|
532
|
+
|
|
533
|
+
**Within each category**: Overlays are applied in the order specified in the manifest/answers.
|
|
534
|
+
|
|
535
|
+
## Edge Cases and Special Handling
|
|
536
|
+
|
|
537
|
+
### Empty Arrays
|
|
538
|
+
|
|
539
|
+
**Rule**: Empty arrays in source do **not** clear target arrays.
|
|
540
|
+
|
|
541
|
+
**Example**:
|
|
542
|
+
|
|
543
|
+
```json
|
|
544
|
+
// Base
|
|
545
|
+
{
|
|
546
|
+
"forwardPorts": [3000, 8080]
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Overlay with empty array
|
|
550
|
+
{
|
|
551
|
+
"forwardPorts": []
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Result (base preserved, not cleared)
|
|
555
|
+
{
|
|
556
|
+
"forwardPorts": [3000, 8080]
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
**Rationale**: Empty arrays in overlays are typically omitted fields, not intentional clearing.
|
|
561
|
+
|
|
562
|
+
### Null Values
|
|
563
|
+
|
|
564
|
+
**Rule**: `null` values in source **overwrite** target values.
|
|
565
|
+
|
|
566
|
+
**Example**:
|
|
567
|
+
|
|
568
|
+
```json
|
|
569
|
+
// Base
|
|
570
|
+
{
|
|
571
|
+
"workspaceFolder": "/workspace"
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Overlay
|
|
575
|
+
{
|
|
576
|
+
"workspaceFolder": null
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Result (null overwrites)
|
|
580
|
+
{
|
|
581
|
+
"workspaceFolder": null
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### Undefined vs Missing Keys
|
|
586
|
+
|
|
587
|
+
**Rule**: Missing keys in source have **no effect** on target. Only explicitly defined keys are merged.
|
|
588
|
+
|
|
589
|
+
**Example**:
|
|
590
|
+
|
|
591
|
+
```json
|
|
592
|
+
// Base
|
|
593
|
+
{
|
|
594
|
+
"name": "My Container",
|
|
595
|
+
"workspaceFolder": "/workspace"
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Overlay (missing 'name')
|
|
599
|
+
{
|
|
600
|
+
"workspaceFolder": "/app"
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Result ('name' preserved)
|
|
604
|
+
{
|
|
605
|
+
"name": "My Container",
|
|
606
|
+
"workspaceFolder": "/app"
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
## Conflict Detection
|
|
611
|
+
|
|
612
|
+
### When Conflicts Occur
|
|
613
|
+
|
|
614
|
+
Conflicts are **warnings**, not errors. The merge still succeeds with the last writer winning.
|
|
615
|
+
|
|
616
|
+
**Scenarios that generate warnings**:
|
|
617
|
+
|
|
618
|
+
1. Same environment variable with different values (except PORT variables with offset)
|
|
619
|
+
2. Same port forwarded with conflicting attributes
|
|
620
|
+
3. Incompatible feature configurations
|
|
621
|
+
|
|
622
|
+
**Scenarios that do NOT conflict**:
|
|
623
|
+
|
|
624
|
+
- Same package in multiple overlays (union behavior)
|
|
625
|
+
- Same port in multiple overlays (deduplicated)
|
|
626
|
+
- Same PATH component in multiple overlays (deduplicated)
|
|
627
|
+
|
|
628
|
+
### Conflict Resolution
|
|
629
|
+
|
|
630
|
+
**Doctor command validation** checks for potential conflicts:
|
|
631
|
+
|
|
632
|
+
```bash
|
|
633
|
+
$ container-superposition doctor
|
|
634
|
+
|
|
635
|
+
⚠️ Warning: Environment variable conflict
|
|
636
|
+
POSTGRES_PORT defined in multiple overlays:
|
|
637
|
+
- postgres: 5432
|
|
638
|
+
- custom: 5433
|
|
639
|
+
Resolution: custom value (5433) used
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
## Standards and References
|
|
643
|
+
|
|
644
|
+
This merge strategy is informed by:
|
|
645
|
+
|
|
646
|
+
- **RFC 7386**: JSON Merge Patch specification
|
|
647
|
+
- We use deep merge (more powerful than shallow merge patch)
|
|
648
|
+
- Objects are merged recursively
|
|
649
|
+
- Arrays use union strategy (not replacement)
|
|
650
|
+
|
|
651
|
+
- **Docker Compose Specification**:
|
|
652
|
+
- Service-based merging follows Docker Compose extend semantics
|
|
653
|
+
- Array fields are concatenated per Compose specification
|
|
654
|
+
|
|
655
|
+
- **DevContainer Specification**:
|
|
656
|
+
- Features object merging follows devcontainer.json schema
|
|
657
|
+
- Lifecycle commands follow devcontainer command chaining
|
|
658
|
+
|
|
659
|
+
## Testing Strategy
|
|
660
|
+
|
|
661
|
+
All merge behaviors are validated with **golden tests**:
|
|
662
|
+
|
|
663
|
+
1. **Unit tests**: Test individual merge functions (deepMerge, mergeRemoteEnv, etc.)
|
|
664
|
+
2. **Integration tests**: Test full overlay composition scenarios
|
|
665
|
+
3. **Golden tests**: Snapshot expected outputs for complex merges
|
|
666
|
+
4. **Doctor validation**: Runtime checks for merge correctness
|
|
667
|
+
|
|
668
|
+
See `tool/__tests__/merge-strategy.test.ts` for comprehensive test coverage.
|
|
669
|
+
|
|
670
|
+
## Migration and Compatibility
|
|
671
|
+
|
|
672
|
+
### Version Compatibility
|
|
673
|
+
|
|
674
|
+
- Merge behavior is **stable** across minor versions
|
|
675
|
+
- Breaking changes to merge strategy require **major version bump**
|
|
676
|
+
- Generated configurations include `manifestVersion` for future migration
|
|
677
|
+
|
|
678
|
+
### Legacy Behavior
|
|
679
|
+
|
|
680
|
+
**Pre-0.1.0**: No formal merge specification
|
|
681
|
+
|
|
682
|
+
- Behavior was implicit and partially documented
|
|
683
|
+
- Some edge cases had undefined behavior
|
|
684
|
+
|
|
685
|
+
**0.1.0+**: Formal specification (this document)
|
|
686
|
+
|
|
687
|
+
- All merge behavior is deterministic and tested
|
|
688
|
+
- Doctor command validates merge correctness
|
|
689
|
+
|
|
690
|
+
## Summary
|
|
691
|
+
|
|
692
|
+
The merge strategy is:
|
|
693
|
+
|
|
694
|
+
- **Deterministic**: Same inputs always produce same output
|
|
695
|
+
- **Explicit**: All rules are documented
|
|
696
|
+
- **Union-oriented**: Collections are merged, not replaced
|
|
697
|
+
- **Last-writer-wins**: For primitive conflicts
|
|
698
|
+
- **Field-aware**: Arrays have specific strategies per field
|
|
699
|
+
|
|
700
|
+
For implementation details, see `tool/utils/merge.ts` and `tool/questionnaire/composer.ts`.
|