Add first-class superposition.yml project ports with generation-time expansion#162
Add first-class superposition.yml project ports with generation-time expansion#162Copilot wants to merge 12 commits into
superposition.yml project ports with generation-time expansion#162Conversation
There was a problem hiding this comment.
Pull request overview
This pull request adds first-class project port bindings to superposition.yml, including generation-time ${VAR} / ${VAR:-default} expansion from the repo-root .env, and applies these ports to both plain and compose outputs.
Changes:
- Extends the project config model + schema to support
portsentries (string shorthand and object form withlabel/onAutoForward) - Resolves project ports at generation time and applies them to
devcontainer.json(and todocker-compose.ymlfor compose stacks), explicitly bypassingportOffset - Adds Vitest coverage and updates docs/README/CHANGELOG to describe the new
portsfeature and ordering
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tool/schema/types.ts | Adds ProjectPort and ProjectPortAutoForwardAction types and wires projectPorts / ports into selection/answers models |
| tool/schema/superposition.schema.json | Regenerated schema including the new ports field and its allowed onAutoForward values |
| tool/schema/project-config.ts | Parses/serializes ports from superposition.yml and maps selection ↔ answers (ports ↔ projectPorts) |
| tool/questionnaire/composer.ts | Implements generation-time port resolution from root .env and applies results to devcontainer + compose outputs |
| tool/tests/project-ports.test.ts | Adds tests for parsing/serialization and for generation-time application in plain/compose stacks |
| scripts/generate-schema.ts | Updates schema generator to emit the ports schema block and enum values |
| README.md | Adds a minimal ports: example to the project config snippet |
| docs/superposition-yml.md | Documents the new ports field, supported metadata, and application order |
| CHANGELOG.md | Adds an [Unreleased] entry documenting the new ports capability and its portOffset bypass semantics |
| config.forwardPorts = existingForwardPorts as number[]; | ||
|
|
There was a problem hiding this comment.
Fixed in the latest commit. Plain-stack HOST:CONTAINER bindings are now supported: both sides are resolved and validated, the container port goes into forwardPorts, and when hostPort !== containerPort a -p hostPort:containerPort entry is appended to runArgs. When hostPort === containerPort no runArgs entry is added (no-op case).
| ) as { | ||
| forwardPorts?: number[]; | ||
| portsAttributes?: Record<string, { label?: string; onAutoForward?: string }>; | ||
| }; |
There was a problem hiding this comment.
Updated in the same commit. The "throws for HOST:CONTAINER on plain stack" test is replaced with two tests that verify the concrete binding: one asserts forwardPorts contains the container port (8080) and runArgs contains "-p" / "9001:8080" when host ≠ container, and another asserts no -p entry is added when host == container.
📦 Prerelease published to npmA prerelease version has been published for this PR: Or run (regen) directly from the prerelease version: |
- Updated documentation for `ports` section to clarify usage for `stack: plain` and `stack: compose`. - Enhanced test coverage for project port serialization and resolution, ensuring correct handling of both string and object entries. - Implemented new logic in `prepareProjectPorts` to differentiate between plain and compose stacks, enforcing correct port binding formats. - Improved error handling for unresolved environment variables and invalid port configurations. - Adjusted type definitions for project ports to accommodate new structure and validation rules.
- Updated documentation to clarify the use of `${VAR}` and `{{cs.KEY}}` in `env:` values, including their resolution times and safety for secrets.
- Implemented checks for sensitive parameters in project files and `.devcontainer/.env`, warning users about hardcoded sensitive values.
- Added unit tests for parameter token substitution and validation, ensuring proper handling of `{{cs.KEY}}` and `${VAR}` expressions.
- Introduced a utility for parsing simple env files, improving code reuse across commands.
- Enhanced schema definitions to reflect new features and provide better descriptions for env variable values.
- Added support for project-only parameters in `superposition.yml`, allowing users to define parameters not declared by any overlay. - Updated documentation to reflect the new project-only parameters feature, including examples of usage and console output. - Enhanced the `doctor` command to warn about project-only parameters, providing guidance on their usage. - Modified parameter resolution logic to include project-only parameters in the output and ensure they are available for substitution in environment values. - Updated tests to cover the new functionality, ensuring correct handling and output of project-only parameters.
| Write a container port number or `${VAR:-default}` expression. Do **not** use `HOST:CONTAINER` | ||
| format — the tool rejects it with an error. The tool **resolves** `${VAR}` at generation time | ||
| using `superposition.yml env` first, then the root `.env`, then the inline default. |
| * For stack: plain — Container port expression, e.g. "${API_PORT:-8080}" or "8080". | ||
| * Must NOT contain ":". Resolved from superposition.yml env, | ||
| * then root .env, then inline default. | ||
| * For stack: compose — Full docker-compose short syntax, e.g. "${API_PORT:-8080}:8080". | ||
| * Must contain ":". Written VERBATIM to docker-compose ports; |
| ports: { | ||
| type: 'array', | ||
| description: | ||
| 'Project port bindings expanded at generation time (supports ${VAR} and ${VAR:-default}) and applied to both devcontainer.json and compose output. These ports are not affected by portOffset.', | ||
| items: { |
| label: { | ||
| type: 'string', | ||
| description: | ||
| 'Optional devcontainer.json portsAttributes label for the resolved host port', | ||
| }, |
| merged.services.devcontainer.ports = [ | ||
| ...new Set([ | ||
| ...existing, | ||
| ...resolvedProjectPorts.map((port) => port.rawBinding ?? port.value), | ||
| ]), |
| 'Move port bindings to the top-level ports: field.', | ||
| 'ports: supports validation, auto-forward, and port-offset. See spec 024.', | ||
| ], |
| - **Console output for unlisted parameters** — the `⚠️ Unknown overlay parameters …` warning | ||
| (yellow, comma-separated) is replaced by a neutral `⚙️ Project-only parameters (not declared | ||
| by any selected overlay):` informational block using the same `KEY=VALUE` per-line format as | ||
| overlay parameters. |
| - **`ports` semantics corrected** — **BREAKING** for any `superposition.yml` that uses `ports` on `stack: plain` | ||
| - Plain stack: `value` must now be a bare container port expression (no colon). Any `HOST:CONTAINER` format on plain stack now throws an error at generation time with a clear message. | ||
| - Compose stack: `value` is now written **verbatim** to `docker-compose.yml`; `${VAR}` references are no longer expanded by the tool. `portsAttributes` key is now the extracted container port, not the host port. | ||
| - **Migration**: change `"8080:8080"` → `"8080"` and `"${API_PORT:-8080}:8080"` → `"${API_PORT:-8080}"` for plain-stack ports. |
| { | ||
| "enableSkillCommands": true, | ||
| "enableInstallTelemetry": false, | ||
| "extensions": ["extensions/subagent"] | ||
| "enableInstallTelemetry": false | ||
| } |
| - Write: a container port number or `${VAR:-default}` expression. | ||
| - Do **not** write `HOST:CONTAINER` — the tool rejects it with an error. | ||
| - Use `env` in `superposition.yml` to drive the port value. That `env` also sets the container |
This change adds a project-level
portsfield tosuperposition.ymlso teams can declare explicit host/container bindings (with${VAR}/${VAR:-default}expansion) that work across both plain and compose stacks. These project-defined ports are materialized at generation time and intentionally bypassportOffset.Project config model (
superposition.yml)portssupport in parser/types/serialization (stringshorthand and object form).labelandonAutoForward.onAutoForwardallowed values.Generation behavior
.envfor${VAR}/${VAR:-default}.plain: injects resolved host ports intodevcontainer.json.forwardPortsand metadata intoportsAttributes.compose: also injects resolved bindings intodocker-compose.ymlservices.devcontainer.ports.portsare applied after offset handling so they are not shifted byportOffset.Schema and docs
tool/schema/superposition.schema.jsonto includeports.docs/superposition-yml.mdand README authoring examples withportsusage and metadata.