diff --git a/README.md b/README.md index 0783d43..ff8841d 100644 --- a/README.md +++ b/README.md @@ -132,12 +132,13 @@ Before starting, `wtc` copies infrastructure files from main into the worktree: ### Env Injection -After copying `.env`, `wtc` appends an idempotent block with allocated port overrides: +After copying `.env`, `wtc` appends an idempotent block with the worktree's `COMPOSE_PROJECT_NAME` and allocated port overrides: ```bash # existing .env content stays untouched... # --- wtc port overrides --- +COMPOSE_PROJECT_NAME=myapp-wt-1-feature-auth POSTGRES_PORT=25435 REDIS_PORT=26381 BACKEND_PORT=28001 @@ -145,6 +146,8 @@ FRONTEND_PORT=25174 # --- end wtc --- ``` +Because `COMPOSE_PROJECT_NAME` lives in `.env`, external tooling (Makefiles, raw `docker compose ...` invocations, editor tasks, etc.) running inside a worktree automatically targets the correct project. + ## Commands ### `wtc start [indices...]` @@ -219,7 +222,24 @@ Extra files/directories to copy from main into each worktree on start. Use for g ### `envOverrides` -Additional env vars injected into `.env`. Supports `${VAR}` interpolation with allocated port values. Use when env vars depend on allocated ports (e.g. `VITE_API_URL`). +Additional env vars to inject into each worktree's `.env`. Values may reference the per-worktree allocated ports and project name as `${...}` placeholders — `wtc` substitutes them at inject time. You don't supply the values of these placeholders; they're computed by `wtc`. + +Available placeholders: + +- `${COMPOSE_PROJECT_NAME}` — the per-worktree Compose project (e.g. `myapp-wt-1-feature-auth`). Always injected automatically as a top-level line in the managed block; this placeholder is for referencing that value inside your own overrides. +- `${_PORT}` — each allocated host port (e.g. `${BACKEND_PORT}`, `${POSTGRES_PORT}`). + +Example: + +```json +{ + "envOverrides": { + "VITE_API_URL": "http://localhost:${BACKEND_PORT}", + "SENTRY_ENVIRONMENT": "dev-${COMPOSE_PROJECT_NAME}", + "LOG_PREFIX": "[${COMPOSE_PROJECT_NAME}] " + } +} +``` ## MCP Server diff --git a/src/commands/start.ts b/src/commands/start.ts index 98b9525..798d3d7 100644 --- a/src/commands/start.ts +++ b/src/commands/start.ts @@ -43,6 +43,7 @@ export function startCommand(indices: number[]): void { injectPortOverrides( `${wt.path}/.env`, allocations, + project, ctx.config.envOverrides, ); log.success("Injected port overrides into .env"); diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 60b6c93..42c7925 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -33,6 +33,7 @@ function startWorktrees(indices: number[]): string { injectPortOverrides( `${wt.path}/.env`, allocations, + project, ctx.config.envOverrides, ); diff --git a/src/sync/env.ts b/src/sync/env.ts index 3fcc076..2cc4bc4 100644 --- a/src/sync/env.ts +++ b/src/sync/env.ts @@ -20,21 +20,25 @@ export function stripOverrideBlock(content: string): string { export function buildOverrideBlock( allocations: PortAllocation[], + projectName: string, envOverrides?: Record, ): string { - const lines: string[] = [BLOCK_START]; + const lines: string[] = [BLOCK_START, `COMPOSE_PROJECT_NAME=${projectName}`]; + + const interpolations = new Map([ + ["COMPOSE_PROJECT_NAME", projectName], + ]); - const portValues = new Map(); for (const a of allocations) { lines.push(`${a.envVar}=${a.port}`); - portValues.set(a.envVar, a.port); + interpolations.set(a.envVar, String(a.port)); } if (envOverrides) { for (const [key, template] of Object.entries(envOverrides)) { let value = template; - for (const [envVar, port] of portValues) { - value = value.replace(`\${${envVar}}`, String(port)); + for (const [varName, varValue] of interpolations) { + value = value.replaceAll(`\${${varName}}`, varValue); } lines.push(`${key}=${value}`); } @@ -47,6 +51,7 @@ export function buildOverrideBlock( export function injectPortOverrides( envPath: string, allocations: PortAllocation[], + projectName: string, envOverrides?: Record, ): void { let content = ""; @@ -56,7 +61,7 @@ export function injectPortOverrides( content = stripOverrideBlock(content); - const block = buildOverrideBlock(allocations, envOverrides); + const block = buildOverrideBlock(allocations, projectName, envOverrides); const result = content.trimEnd() + "\n\n" + block + "\n"; fs.writeFileSync(envPath, result, "utf-8"); diff --git a/tests/sync/env.test.ts b/tests/sync/env.test.ts index 83a5cf8..e712584 100644 --- a/tests/sync/env.test.ts +++ b/tests/sync/env.test.ts @@ -19,30 +19,54 @@ const allocations: PortAllocation[] = [ }, ]; +const project = "myapp-wt-1-feature-auth"; + describe("buildOverrideBlock", () => { - it("creates a delimited block with port assignments", () => { - const block = buildOverrideBlock(allocations); + it("creates a delimited block with the project name and port assignments", () => { + const block = buildOverrideBlock(allocations, project); expect(block).toContain("# --- wtc port overrides ---"); + expect(block).toContain(`COMPOSE_PROJECT_NAME=${project}`); expect(block).toContain("BACKEND_PORT=28001"); expect(block).toContain("FRONTEND_PORT=25174"); expect(block).toContain("# --- end wtc ---"); }); + it("always writes COMPOSE_PROJECT_NAME (required argument)", () => { + const block = buildOverrideBlock(allocations, project); + expect(block).toContain(`COMPOSE_PROJECT_NAME=${project}`); + }); + it("interpolates envOverrides with port values", () => { - const block = buildOverrideBlock(allocations, { + const block = buildOverrideBlock(allocations, project, { VITE_API_URL: "http://localhost:${BACKEND_PORT}", }); expect(block).toContain("VITE_API_URL=http://localhost:28001"); }); it("handles multiple envOverrides", () => { - const block = buildOverrideBlock(allocations, { + const block = buildOverrideBlock(allocations, project, { VITE_API_URL: "http://localhost:${BACKEND_PORT}", VITE_APP_URL: "http://localhost:${FRONTEND_PORT}", }); expect(block).toContain("VITE_API_URL=http://localhost:28001"); expect(block).toContain("VITE_APP_URL=http://localhost:25174"); }); + + it("interpolates ${COMPOSE_PROJECT_NAME} inside envOverrides", () => { + const block = buildOverrideBlock(allocations, project, { + STACK_LABEL: "stack:${COMPOSE_PROJECT_NAME}", + MIXED: "${COMPOSE_PROJECT_NAME}-${BACKEND_PORT}", + }); + expect(block).toContain(`STACK_LABEL=stack:${project}`); + expect(block).toContain(`MIXED=${project}-28001`); + }); + + it("replaces every occurrence of an interpolation token", () => { + const block = buildOverrideBlock(allocations, project, { + DUP: "${BACKEND_PORT}/${BACKEND_PORT}", + }); + expect(block).toContain("DUP=28001/28001"); + }); }); describe("stripOverrideBlock", () => {