From 2ce25f2dc2c91e2a1f6c2d2500412eb9652b2ce3 Mon Sep 17 00:00:00 2001 From: oldschoola Date: Tue, 23 Jun 2026 13:55:51 -0700 Subject: [PATCH] fix: normalize backslashes in all POSIX path components for profile-alias Address codex review feedback on PR #3346: - path.posix.join only adds / separators but preserves existing backslashes in input segments (homeDir, ZDOTDIR, XDG_CONFIG_HOME), producing mixed paths like C:\Users\me/.bashrc on Windows. - Add toPosix() helper to normalize backslashes to forward slashes before path.posix.join, applied to all POSIX-shell path components. - PowerShell paths remain platform-native (path.join) as before. - Add 3 regression tests: bash homeDir, zsh ZDOTDIR, fish XDG_CONFIG_HOME all with Windows backslash paths. --- .../coding-agent/src/cli/profile-alias.ts | 21 +++++-- .../coding-agent/test/profile-alias.test.ts | 61 +++++++++++++++++++ 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/packages/coding-agent/src/cli/profile-alias.ts b/packages/coding-agent/src/cli/profile-alias.ts index 3c5802189..8229be9bc 100644 --- a/packages/coding-agent/src/cli/profile-alias.ts +++ b/packages/coding-agent/src/cli/profile-alias.ts @@ -211,6 +211,13 @@ export function resolveProfileAliasCommandFromProcess( }; } +/** Normalize backslashes to forward slashes for POSIX-shell paths. + * path.posix.join only adds / separators — it preserves existing backslashes + * in input segments like homeDir ("C:\Users\me"), producing mixed paths. */ +function toPosix(p: string): string { + return p.replace(/\\/g, "/"); +} + function resolveShellConfigPath( shell: ProfileAliasShell, homeDir: string, @@ -218,25 +225,29 @@ function resolveShellConfigPath( env: NodeJS.ProcessEnv, ): string { // POSIX shells (bash/zsh/fish) always need forward-slash config paths, - // even on Windows — path.posix.join guarantees that regardless of platform. + // even on Windows. path.posix.join adds / separators but preserves existing + // backslashes in input segments, so we normalize each component with toPosix. // PowerShell profiles use the platform-native path.join (backslashes on // Windows, forward slashes elsewhere). + const posixHome = toPosix(homeDir); switch (shell) { case "zsh": - return path.posix.join(env.ZDOTDIR || homeDir, ".zshrc"); + return path.posix.join(env.ZDOTDIR ? toPosix(env.ZDOTDIR) : posixHome, ".zshrc"); case "bash": - return platform === "darwin" ? path.posix.join(homeDir, ".bash_profile") : path.posix.join(homeDir, ".bashrc"); + return platform === "darwin" + ? path.posix.join(posixHome, ".bash_profile") + : path.posix.join(posixHome, ".bashrc"); case "fish": { // fish sources conf.d from $XDG_CONFIG_HOME/fish (default ~/.config/fish); // a hard-coded ~/.config would be silently ignored when the user relocates // their XDG config root, leaving the alias unsourced after a restart. - const configHome = env.XDG_CONFIG_HOME || path.posix.join(homeDir, ".config"); + const configHome = env.XDG_CONFIG_HOME ? toPosix(env.XDG_CONFIG_HOME) : path.posix.join(posixHome, ".config"); return path.posix.join(configHome, "fish", "conf.d", "omp-profiles.fish"); } case "pwsh": return platform === "win32" ? path.join(homeDir, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1") - : path.posix.join(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"); + : path.posix.join(posixHome, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"); case "powershell": return path.join(homeDir, "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1"); } diff --git a/packages/coding-agent/test/profile-alias.test.ts b/packages/coding-agent/test/profile-alias.test.ts index 74861e2d7..839c6eb12 100644 --- a/packages/coding-agent/test/profile-alias.test.ts +++ b/packages/coding-agent/test/profile-alias.test.ts @@ -360,4 +360,65 @@ describe("profile alias installer", () => { ).rejects.toThrow("Invalid OMP profile"); expect(files.size).toBe(0); }); + + it("normalizes backslashes in Windows homeDir for POSIX shell config paths", async () => { + const files = new Map(); + + const result = await installProfileAlias({ + profile: "work", + aliasName: "omp-work", + shellPath: "/bin/bash", + platform: "win32", + homeDir: "C:\\Users\\me", + readFile: async filePath => files.get(filePath) ?? "", + writeFile: async (filePath, content) => { + files.set(filePath, content); + }, + }); + + // path.posix.join preserves backslashes in input segments, so we must + // normalize them — bash/zsh/fish can't resolve C:\Users\me/.bashrc + expect(result.configPath).toBe("C:/Users/me/.bashrc"); + expect(result.reloadedWith).toBe(". 'C:/Users/me/.bashrc'"); + }); + + it("normalizes backslashes in ZDOTDIR for zsh config paths on Windows", async () => { + const files = new Map(); + + const result = await installProfileAlias({ + profile: "work", + aliasName: "omp-work", + shellPath: "/bin/zsh", + platform: "win32", + homeDir: "C:\\Users\\me", + env: { ZDOTDIR: "D:\\zdotdir" }, + readFile: async filePath => files.get(filePath) ?? "", + writeFile: async (filePath, content) => { + files.set(filePath, content); + }, + }); + + expect(result.configPath).toBe("D:/zdotdir/.zshrc"); + expect(result.reloadedWith).toBe(". 'D:/zdotdir/.zshrc'"); + }); + + it("normalizes backslashes in XDG_CONFIG_HOME for fish config paths on Windows", async () => { + const files = new Map(); + + const result = await installProfileAlias({ + profile: "work", + aliasName: "omp-work", + shellPath: "/bin/fish", + platform: "win32", + homeDir: "C:\\Users\\me", + env: { XDG_CONFIG_HOME: "D:\\xdg" }, + readFile: async filePath => files.get(filePath) ?? "", + writeFile: async (filePath, content) => { + files.set(filePath, content); + }, + }); + + expect(result.configPath).toBe("D:/xdg/fish/conf.d/omp-profiles.fish"); + expect(result.reloadedWith).toBe("source 'D:/xdg/fish/conf.d/omp-profiles.fish'"); + }); });