diff --git a/src/cli/config-io.test.ts b/src/cli/config-io.test.ts index bf891e5..4082f50 100644 --- a/src/cli/config-io.test.ts +++ b/src/cli/config-io.test.ts @@ -415,6 +415,36 @@ describe("generateJsConfig", () => { // Should not have any route entries between the braces expect(output).toMatch(/routes: \{\n\s*\}/); }); + + it("omits worktreeConfig block when not provided", () => { + const output = generateJsConfig({ api: "http://localhost:4000" }); + expect(output).not.toContain("worktreeConfig"); + }); + + it("emits worktreeConfig block when provided", () => { + const output = generateJsConfig( + { api: "http://localhost:4000" }, + { portRange: [4001, 5000], directory: "../app-{branch}" }, + ); + expect(output).toContain("worktreeConfig:"); + expect(output).toContain('"portRange"'); + expect(output).toContain('"directory": "../app-{branch}"'); + }); + + it("emits worktreeConfig with services and hooks", () => { + const output = generateJsConfig( + {}, + { + portRange: [4001, 5000], + directory: "../{branch}", + services: { web: { env: "PORT" }, api: { env: "API_PORT" } }, + hooks: { "post-create": "pnpm install" }, + }, + ); + expect(output).toContain('"services"'); + expect(output).toContain('"env": "PORT"'); + expect(output).toContain('"post-create": "pnpm install"'); + }); }); // ── writeJsConfig ─────────────────────────────────────────── diff --git a/src/cli/config-io.ts b/src/cli/config-io.ts index 41d555a..a46f14f 100644 --- a/src/cli/config-io.ts +++ b/src/cli/config-io.ts @@ -179,23 +179,41 @@ export function writeProjectConfig(projectPath: string, cfg: RawProjectConfig): // ── JS config generation ──────────────────────────────────── -export function generateJsConfig(routes: Record): string { +function indentJson(value: unknown, indent: string): string { + return JSON.stringify(value, null, 2) + .split("\n") + .map((line, i) => (i === 0 ? line : indent + line)) + .join("\n"); +} + +export function generateJsConfig( + routes: Record, + worktreeConfig?: WorktreeConfig, +): string { const entries = Object.entries(routes) .map(([sub, target]) => ` ${JSON.stringify(sub)}: ${JSON.stringify(target)},`) .join("\n"); + const worktreeBlock = worktreeConfig + ? `\n worktreeConfig: ${indentJson(worktreeConfig, " ")},` + : ""; + return `/** @type {import('@reopt-ai/dev-proxy').Config} */ export default { routes: { ${entries} - }, + },${worktreeBlock} }; `; } -export function writeJsConfig(projectPath: string, routes: Record): void { +export function writeJsConfig( + projectPath: string, + routes: Record, + worktreeConfig?: WorktreeConfig, +): void { const configPath = resolve(projectPath, JS_CONFIG_NAMES[0] as string); - atomicWriteFileSync(configPath, generateJsConfig(routes)); + atomicWriteFileSync(configPath, generateJsConfig(routes, worktreeConfig)); } // ── Validation ─────────────────────────────────────────────── diff --git a/src/commands/migrate.test.ts b/src/commands/migrate.test.ts index 1e02065..88ebc77 100644 --- a/src/commands/migrate.test.ts +++ b/src/commands/migrate.test.ts @@ -37,6 +37,16 @@ vi.mock("../cli/config-io.js", () => ({ resolveProjectConfigFile: resolveProjectConfigFileMock, })); +const projectsList: { path: string; routes: Record }[] = []; + +vi.mock("../proxy/config.js", () => ({ + config: { + get projects() { + return projectsList; + }, + }, +})); + vi.mock("../cli/output.js", () => ({ Header: () => null, SuccessMessage: () => null, @@ -44,9 +54,10 @@ vi.mock("../cli/output.js", () => ({ ExitOnRender: () => null, })); -import { existsSync } from "node:fs"; +import { existsSync, unlinkSync } from "node:fs"; const mockExistsSync = vi.mocked(existsSync); +const mockUnlinkSync = vi.mocked(unlinkSync); const { __testing } = await import("./migrate.js"); const { migrateProject } = __testing; @@ -59,22 +70,25 @@ beforeEach(() => { writeProjectConfigMock.mockReset(); writeJsConfigMock.mockReset(); mockExistsSync.mockReset(); + mockUnlinkSync.mockReset(); + projectsList.length = 0; }); describe("migrateProject", () => { - it('returns { status: "skipped-js-exists" } when JS config already exists', () => { + it('returns "skipped-no-legacy" when mjs exists and .dev-proxy.json does not', () => { resolveProjectConfigFileMock.mockReturnValue({ type: "js", - path: "/p/dev-proxy.config.js", + path: "/p/dev-proxy.config.mjs", }); + mockExistsSync.mockReturnValue(false); const result = migrateProject("/p"); - expect(result).toEqual({ path: "/p", status: "skipped-js-exists" }); + expect(result).toEqual({ path: "/p", status: "skipped-no-legacy" }); expect(writeJsConfigMock).not.toHaveBeenCalled(); expect(writeProjectConfigMock).not.toHaveBeenCalled(); }); - it('returns { status: "skipped-no-json" } when .dev-proxy.json does not exist', () => { + it('returns "skipped-no-json" when neither mjs nor .dev-proxy.json exist', () => { resolveProjectConfigFileMock.mockReturnValue(null); mockExistsSync.mockReturnValue(false); @@ -83,7 +97,7 @@ describe("migrateProject", () => { expect(writeJsConfigMock).not.toHaveBeenCalled(); }); - it('returns { status: "skipped-no-routes" } when JSON has no routes and no worktreeConfig', () => { + it('returns "skipped-no-routes" when JSON has no routes and no worktreeConfig', () => { resolveProjectConfigFileMock.mockReturnValue(null); mockExistsSync.mockReturnValue(true); readProjectConfigMock.mockReturnValue({}); @@ -93,7 +107,7 @@ describe("migrateProject", () => { expect(writeJsConfigMock).not.toHaveBeenCalled(); }); - it('returns { status: "migrated" } and writes JS config when routes exist', () => { + it('returns "migrated" and writes mjs config when only routes exist', () => { resolveProjectConfigFileMock.mockReturnValue(null); mockExistsSync.mockReturnValue(true); readProjectConfigMock.mockReturnValue({ @@ -103,36 +117,18 @@ describe("migrateProject", () => { const result = migrateProject("/p"); expect(result).toEqual({ path: "/p", status: "migrated" }); - expect(writeJsConfigMock).toHaveBeenCalledWith("/p", { - api: "http://localhost:4000", - web: "http://localhost:3000", - }); + expect(writeJsConfigMock).toHaveBeenCalledWith( + "/p", + { api: "http://localhost:4000", web: "http://localhost:3000" }, + undefined, + ); expect(writeProjectConfigMock).toHaveBeenCalledWith("/p", { worktrees: { feat1: { port: 5001 } }, }); + expect(mockUnlinkSync).toHaveBeenCalled(); }); - it("moves worktrees through writeProjectConfig (split into new file)", () => { - resolveProjectConfigFileMock.mockReturnValue(null); - mockExistsSync.mockReturnValue(true); - readProjectConfigMock.mockReturnValue({ - routes: { app: "http://localhost:3000" }, - worktrees: { - feat1: { port: 5001 }, - feat2: { ports: { web: 5002, api: 5003 } }, - }, - }); - - migrateProject("/p"); - expect(writeProjectConfigMock).toHaveBeenCalledWith("/p", { - worktrees: { - feat1: { port: 5001 }, - feat2: { ports: { web: 5002, api: 5003 } }, - }, - }); - }); - - it("preserves worktreeConfig alongside worktrees during migration", () => { + it("moves worktreeConfig into mjs as the third argument", () => { resolveProjectConfigFileMock.mockReturnValue(null); mockExistsSync.mockReturnValue(true); const worktreeConfig = { @@ -145,14 +141,20 @@ describe("migrateProject", () => { worktreeConfig, }); - migrateProject("/p"); + const result = migrateProject("/p"); + expect(result).toEqual({ path: "/p", status: "migrated" }); + expect(writeJsConfigMock).toHaveBeenCalledWith( + "/p", + { app: "http://localhost:3000" }, + worktreeConfig, + ); expect(writeProjectConfigMock).toHaveBeenCalledWith("/p", { worktrees: { feat: { port: 5001 } }, - worktreeConfig, }); + expect(mockUnlinkSync).toHaveBeenCalled(); }); - it('returns "split-worktrees" when JS config exists and legacy file still has worktrees', () => { + it('returns "cleaned-legacy" when mjs exists and legacy file still has worktrees', () => { resolveProjectConfigFileMock.mockReturnValue({ type: "js", path: "/p/dev-proxy.config.mjs", @@ -163,26 +165,56 @@ describe("migrateProject", () => { }); const result = migrateProject("/p"); - expect(result).toEqual({ path: "/p", status: "split-worktrees" }); + expect(result).toEqual({ path: "/p", status: "cleaned-legacy" }); expect(writeProjectConfigMock).toHaveBeenCalledWith("/p", { worktrees: { stale: { port: 6000 } }, }); expect(writeJsConfigMock).not.toHaveBeenCalled(); + expect(mockUnlinkSync).toHaveBeenCalled(); }); - it('still returns "skipped-js-exists" when JS config has no legacy file at all', () => { + it("moves leftover worktreeConfig from legacy file into mjs preserving routes", () => { resolveProjectConfigFileMock.mockReturnValue({ type: "js", path: "/p/dev-proxy.config.mjs", }); - mockExistsSync.mockReturnValue(false); + mockExistsSync.mockReturnValue(true); + const worktreeConfig = { + portRange: [4000, 5000] as [number, number], + directory: "../{branch}", + }; + readProjectConfigMock.mockReturnValue({ worktreeConfig }); + projectsList.push({ + path: "/p", + routes: { api: "http://localhost:4000" }, + }); + + const result = migrateProject("/p"); + expect(result).toEqual({ path: "/p", status: "cleaned-legacy" }); + expect(writeJsConfigMock).toHaveBeenCalledWith( + "/p", + { api: "http://localhost:4000" }, + worktreeConfig, + ); + expect(mockUnlinkSync).toHaveBeenCalled(); + }); + + it('returns "cleaned-legacy" without writes when legacy file is empty', () => { + resolveProjectConfigFileMock.mockReturnValue({ + type: "js", + path: "/p/dev-proxy.config.mjs", + }); + mockExistsSync.mockReturnValue(true); + readProjectConfigMock.mockReturnValue({}); const result = migrateProject("/p"); - expect(result).toEqual({ path: "/p", status: "skipped-js-exists" }); + expect(result).toEqual({ path: "/p", status: "cleaned-legacy" }); expect(writeProjectConfigMock).not.toHaveBeenCalled(); + expect(writeJsConfigMock).not.toHaveBeenCalled(); + expect(mockUnlinkSync).toHaveBeenCalled(); }); - it("handles routes with wildcard correctly", () => { + it("handles wildcard routes correctly", () => { resolveProjectConfigFileMock.mockReturnValue(null); mockExistsSync.mockReturnValue(true); readProjectConfigMock.mockReturnValue({ @@ -191,21 +223,24 @@ describe("migrateProject", () => { const result = migrateProject("/p"); expect(result).toEqual({ path: "/p", status: "migrated" }); - expect(writeJsConfigMock).toHaveBeenCalledWith("/p", { - api: "http://localhost:4000", - "*": "http://localhost:3000", - }); - expect(writeProjectConfigMock).toHaveBeenCalledWith("/p", { worktrees: {} }); + expect(writeJsConfigMock).toHaveBeenCalledWith( + "/p", + { api: "http://localhost:4000", "*": "http://localhost:3000" }, + undefined, + ); }); - it('returns { status: "migrated" } when worktreeConfig exists even with no routes', () => { + it('returns "migrated" when worktreeConfig exists with no routes', () => { resolveProjectConfigFileMock.mockReturnValue(null); mockExistsSync.mockReturnValue(true); - readProjectConfigMock.mockReturnValue({ - worktreeConfig: { portRange: [4000, 5000], directory: "../{branch}" }, - }); + const worktreeConfig = { + portRange: [4000, 5000] as [number, number], + directory: "../{branch}", + }; + readProjectConfigMock.mockReturnValue({ worktreeConfig }); const result = migrateProject("/p"); expect(result).toEqual({ path: "/p", status: "migrated" }); + expect(writeJsConfigMock).toHaveBeenCalledWith("/p", {}, worktreeConfig); }); }); diff --git a/src/commands/migrate.tsx b/src/commands/migrate.tsx index b06f6ef..23cab74 100644 --- a/src/commands/migrate.tsx +++ b/src/commands/migrate.tsx @@ -10,13 +10,14 @@ import { resolveProjectConfigFile, } from "../cli/config-io.js"; import { Header, SuccessMessage, ErrorMessage, ExitOnRender } from "../cli/output.js"; +import { config } from "../proxy/config.js"; interface MigrateResult { path: string; status: | "migrated" - | "split-worktrees" - | "skipped-js-exists" + | "cleaned-legacy" + | "skipped-no-legacy" | "skipped-no-json" | "skipped-no-routes"; } @@ -26,23 +27,35 @@ function migrateProject(projectPath: string): MigrateResult { const legacyPath = resolve(projectPath, PROJECT_CONFIG_NAME); if (resolution?.type === "js") { - // Already on JS config — only thing left to do is split out worktrees - // from the legacy file (if any) into the dedicated worktrees file. + // mjs is already the active config. Move any leftovers from + // .dev-proxy.json (worktrees / worktreeConfig) into their proper homes, + // then delete the legacy file. if (!existsSync(legacyPath)) { - return { path: projectPath, status: "skipped-js-exists" }; + return { path: projectPath, status: "skipped-no-legacy" }; } + const cfg = readProjectConfig(projectPath); const hasWorktrees = cfg.worktrees && Object.keys(cfg.worktrees).length > 0; + const hasLegacyWtConfig = cfg.worktreeConfig !== undefined; + + if (!hasWorktrees && !hasLegacyWtConfig) { + cleanupLegacyFile(legacyPath); + return { path: projectPath, status: "cleaned-legacy" }; + } - if (!hasWorktrees) { - // Either nothing to migrate, or already split. cleanupLegacyFile is a - // no-op when worktreeConfig is present. - return { path: projectPath, status: "skipped-js-exists" }; + if (hasWorktrees) { + writeProjectConfig(projectPath, { worktrees: cfg.worktrees ?? {} }); } - writeProjectConfig(projectPath, { worktrees: cfg.worktrees ?? {} }); - cleanupLegacyFile(legacyPath, cfg.worktreeConfig); - return { path: projectPath, status: "split-worktrees" }; + if (hasLegacyWtConfig) { + // Preserve existing mjs routes (loaded via the proxy config singleton) + const project = config.projects.find((p) => p.path === projectPath); + const routes = project?.routes ?? {}; + writeJsConfig(projectPath, routes, cfg.worktreeConfig); + } + + cleanupLegacyFile(legacyPath); + return { path: projectPath, status: "cleaned-legacy" }; } if (!existsSync(legacyPath)) { @@ -56,27 +69,21 @@ function migrateProject(projectPath: string): MigrateResult { return { path: projectPath, status: "skipped-no-routes" }; } - writeJsConfig(projectPath, routes); + // Routes + worktreeConfig → dev-proxy.config.mjs + writeJsConfig(projectPath, routes, cfg.worktreeConfig); - // Persist worktrees to the new file and worktreeConfig (if any) back to - // the legacy file. writeProjectConfig handles the split automatically. - const splitCfg: { - worktrees: NonNullable; - worktreeConfig?: typeof cfg.worktreeConfig; - } = { - worktrees: cfg.worktrees ?? {}, - }; - if (cfg.worktreeConfig) splitCfg.worktreeConfig = cfg.worktreeConfig; - writeProjectConfig(projectPath, splitCfg); + // Worktrees → .dev-proxy.worktrees.json + if (cfg.worktrees && Object.keys(cfg.worktrees).length > 0) { + writeProjectConfig(projectPath, { worktrees: cfg.worktrees }); + } - // If the legacy file would now hold nothing (no worktreeConfig), delete it. - cleanupLegacyFile(legacyPath, cfg.worktreeConfig); + // .dev-proxy.json no longer holds anything — delete it. + cleanupLegacyFile(legacyPath); return { path: projectPath, status: "migrated" }; } -function cleanupLegacyFile(legacyPath: string, worktreeConfig: unknown): void { - if (worktreeConfig) return; +function cleanupLegacyFile(legacyPath: string): void { if (!existsSync(legacyPath)) return; try { unlinkSync(legacyPath); @@ -104,10 +111,10 @@ function Migrate() { const results = projects.map(migrateProject); const migrated = results.filter( - (r) => r.status === "migrated" || r.status === "split-worktrees", + (r) => r.status === "migrated" || r.status === "cleaned-legacy", ); const skipped = results.filter( - (r) => r.status !== "migrated" && r.status !== "split-worktrees", + (r) => r.status !== "migrated" && r.status !== "cleaned-legacy", ); return ( @@ -119,8 +126,8 @@ function Migrate() { @@ -129,7 +136,7 @@ function Migrate() { {skipped.map((r) => ( {" "} - {r.status === "skipped-js-exists" && `Skipped (already migrated): ${r.path}`} + {r.status === "skipped-no-legacy" && `Skipped (already on mjs): ${r.path}`} {r.status === "skipped-no-json" && `Skipped (no config files): ${r.path}`} {r.status === "skipped-no-routes" && `Skipped (no routes to migrate): ${r.path}`} @@ -139,7 +146,9 @@ function Migrate() { {migrated.length > 0 && ( - {" Routes → dev-proxy.config.mjs, worktrees → .dev-proxy.worktrees.json"} + { + " Routes + worktreeConfig → dev-proxy.config.mjs, worktrees → .dev-proxy.worktrees.json" + } )} diff --git a/src/commands/worktree.test.ts b/src/commands/worktree.test.ts index ffc714b..c4f48a9 100644 --- a/src/commands/worktree.test.ts +++ b/src/commands/worktree.test.ts @@ -44,14 +44,25 @@ vi.mock("ink", () => ({ Text: () => null, })); +const projectsList: { path: string; worktreeConfig?: unknown }[] = []; + +vi.mock("../proxy/config.js", () => ({ + config: { + get projects() { + return projectsList; + }, + }, +})); + const { __testing } = await import("./worktree.js"); -const { findOwningProject, formatPorts } = __testing; +const { findOwningProject, formatPorts, getWorktreeConfig } = __testing; // ── Lifecycle ────────────────────────────────────────────── beforeEach(() => { readGlobalConfigMock.mockReset(); readProjectConfigMock.mockReset(); + projectsList.length = 0; }); afterEach(() => { @@ -119,3 +130,28 @@ describe("formatPorts", () => { expect(formatPorts(entry)).toBe("web:5000"); }); }); + +// ── getWorktreeConfig ────────────────────────────────────── + +describe("getWorktreeConfig", () => { + it("returns worktreeConfig from the loaded proxy config singleton", () => { + projectsList.push({ + path: "/home/user/project", + worktreeConfig: { portRange: [4001, 5000], directory: "../{branch}" }, + }); + + expect(getWorktreeConfig("/home/user/project")).toEqual({ + portRange: [4001, 5000], + directory: "../{branch}", + }); + }); + + it("returns undefined when project is not registered", () => { + expect(getWorktreeConfig("/home/user/missing")).toBeUndefined(); + }); + + it("returns undefined when registered project has no worktreeConfig", () => { + projectsList.push({ path: "/home/user/project" }); + expect(getWorktreeConfig("/home/user/project")).toBeUndefined(); + }); +}); diff --git a/src/commands/worktree.tsx b/src/commands/worktree.tsx index 39fda4a..ee3ab58 100644 --- a/src/commands/worktree.tsx +++ b/src/commands/worktree.tsx @@ -12,6 +12,7 @@ import { getEntryPorts, generateEnvContent, type WorktreeEntry, + type WorktreeConfig, } from "../cli/config-io.js"; import { Header, @@ -20,6 +21,7 @@ import { ErrorMessage, ExitOnRender, } from "../cli/output.js"; +import { config } from "../proxy/config.js"; function findOwningProject(cwd: string): string | null { const cfg = readGlobalConfig(); @@ -32,6 +34,16 @@ function findOwningProject(cwd: string): string | null { return null; } +/** + * Look up worktreeConfig from the loaded proxy config singleton, which already + * merges dev-proxy.config.mjs (preferred) with .dev-proxy.json (legacy fallback). + * CLI invocations are short-lived, so the singleton is always fresh. + */ +function getWorktreeConfig(projectPath: string): WorktreeConfig | undefined { + const project = config.projects.find((p) => p.path === projectPath); + return project?.worktreeConfig as WorktreeConfig | undefined; +} + // ── Helpers ────────────────────────────────────────────────── function formatPorts(entry: WorktreeEntry): string { @@ -178,15 +190,15 @@ function WorktreeCreate({ branch }: { branch: string }) { } const cfg = readProjectConfig(projectPath); - const wtConfig = cfg.worktreeConfig; + const wtConfig = getWorktreeConfig(projectPath); if (!wtConfig) { return ( ); @@ -363,7 +375,7 @@ function WorktreeDestroy({ branch }: { branch: string }) { ); } - const wtConfig = cfg.worktreeConfig; + const wtConfig = getWorktreeConfig(projectPath); const messages: string[] = []; const warnings: string[] = []; @@ -494,4 +506,4 @@ if (subcommand === "create") { render(); } -export const __testing = { findOwningProject, formatPorts }; +export const __testing = { findOwningProject, formatPorts, getWorktreeConfig }; diff --git a/src/proxy/config.test.ts b/src/proxy/config.test.ts index c7e9809..a9d864b 100644 --- a/src/proxy/config.test.ts +++ b/src/proxy/config.test.ts @@ -365,6 +365,90 @@ describe("loadProjectConfig (JS config path)", () => { realFs.rmSync(tmpDir, { recursive: true }); } }); + + it("loads worktreeConfig from JS config when present", async () => { + const tmpDir = realFs.mkdtempSync(join(tmpdir(), "dev-proxy-test-")); + const jsPath = join(tmpDir, "dev-proxy.config.js"); + realFs.writeFileSync( + jsPath, + `export default { + routes: { app: "http://localhost:3000" }, + worktreeConfig: { portRange: [4001, 5000], directory: "../app-{branch}" }, + };\n`, + ); + + fsMock.existsSync.mockImplementation((p: string) => p === jsPath); + + try { + const result = await loadProjectConfig(tmpDir); + expect(result?.worktreeConfig).toEqual({ + portRange: [4001, 5000], + directory: "../app-{branch}", + }); + } finally { + realFs.rmSync(tmpDir, { recursive: true }); + } + }); + + it("falls back to legacy .dev-proxy.json worktreeConfig when JS config has none", async () => { + const tmpDir = realFs.mkdtempSync(join(tmpdir(), "dev-proxy-test-")); + const jsPath = join(tmpDir, "dev-proxy.config.js"); + const legacyPath = join(tmpDir, ".dev-proxy.json"); + realFs.writeFileSync(jsPath, "export default { routes: {} };\n"); + + fsMock.existsSync.mockImplementation((p: string) => p === jsPath || p === legacyPath); + fsMock.readFileSync.mockImplementation((p: string) => { + if (p === legacyPath) { + return JSON.stringify({ + worktreeConfig: { portRange: [4001, 5000], directory: "../legacy-{branch}" }, + }); + } + return ""; + }); + + try { + const result = await loadProjectConfig(tmpDir); + expect(result?.worktreeConfig).toEqual({ + portRange: [4001, 5000], + directory: "../legacy-{branch}", + }); + } finally { + realFs.rmSync(tmpDir, { recursive: true }); + } + }); + + it("prefers JS config worktreeConfig over legacy .dev-proxy.json", async () => { + const tmpDir = realFs.mkdtempSync(join(tmpdir(), "dev-proxy-test-")); + const jsPath = join(tmpDir, "dev-proxy.config.js"); + const legacyPath = join(tmpDir, ".dev-proxy.json"); + realFs.writeFileSync( + jsPath, + `export default { + routes: {}, + worktreeConfig: { portRange: [6000, 7000], directory: "../mjs-{branch}" }, + };\n`, + ); + + fsMock.existsSync.mockImplementation((p: string) => p === jsPath || p === legacyPath); + fsMock.readFileSync.mockImplementation((p: string) => { + if (p === legacyPath) { + return JSON.stringify({ + worktreeConfig: { portRange: [1000, 2000], directory: "../legacy-{branch}" }, + }); + } + return ""; + }); + + try { + const result = await loadProjectConfig(tmpDir); + expect(result?.worktreeConfig).toEqual({ + portRange: [6000, 7000], + directory: "../mjs-{branch}", + }); + } finally { + realFs.rmSync(tmpDir, { recursive: true }); + } + }); }); // ── loadConfig ───────────────────────────────────────────── diff --git a/src/proxy/config.ts b/src/proxy/config.ts index 8c2eaf4..add89ba 100644 --- a/src/proxy/config.ts +++ b/src/proxy/config.ts @@ -29,6 +29,7 @@ type WorktreeEntry = { ports: Record } | { port: number }; interface RawProjectConfig { routes?: Record; worktrees?: Record; + worktreeConfig?: unknown; } /** Raw shape of dev-proxy.config.mjs default export */ @@ -43,6 +44,7 @@ export interface ProjectConfig { configType: "js" | "json"; routes: Record; worktrees: Record; + worktreeConfig?: unknown; } export interface ResolvedConfig { @@ -126,6 +128,12 @@ function loadWorktrees(projectDir: string): Record { return legacyRaw?.worktrees ?? {}; } +function loadLegacyWorktreeConfig(projectDir: string): unknown { + const legacyPath = resolve(projectDir, PROJECT_CONFIG_NAME); + const legacyRaw = loadJson(legacyPath) as RawProjectConfig | null; + return legacyRaw?.worktreeConfig; +} + async function loadProjectConfig(projectDir: string): Promise { const resolution = resolveProjectConfigFile(projectDir); if (!resolution) return null; @@ -140,6 +148,7 @@ async function loadProjectConfig(projectDir: string): Promise