From b2961409b71106b2c2fb257300fb8d43e3ddb7fa Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 18 May 2026 14:04:17 +0900 Subject: [PATCH 1/2] test(shell-env): add csh/tcsh detection and buildEnvPrefix coverage Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/shared/shell-env.test.ts | 55 ++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/shared/shell-env.test.ts b/src/shared/shell-env.test.ts index ef9aa52ea..bdc1146d2 100644 --- a/src/shared/shell-env.test.ts +++ b/src/shared/shell-env.test.ts @@ -120,6 +120,39 @@ describe("shell-env", () => { expect(result).toBe("unix") }) + + test("#given SHELL set to /bin/csh #when detectShellType is called #then returns csh", () => { + delete process.env.PSModulePath + delete process.env.MSYSTEM + process.env.SHELL = "/bin/csh" + Object.defineProperty(process, "platform", { value: "linux" }) + + const result = detectShellType() + + expect(result).toBe("csh") + }) + + test("#given SHELL set to /bin/tcsh #when detectShellType is called #then returns csh", () => { + delete process.env.PSModulePath + delete process.env.MSYSTEM + process.env.SHELL = "/bin/tcsh" + Object.defineProperty(process, "platform", { value: "linux" }) + + const result = detectShellType() + + expect(result).toBe("csh") + }) + + test("#given SHELL set to /usr/local/bin/tcsh #when detectShellType is called #then returns csh", () => { + delete process.env.PSModulePath + delete process.env.MSYSTEM + process.env.SHELL = "/usr/local/bin/tcsh" + Object.defineProperty(process, "platform", { value: "darwin" }) + + const result = detectShellType() + + expect(result).toBe("csh") + }) }) describe("shellEscape", () => { @@ -311,5 +344,27 @@ describe("shell-env", () => { expect(result).toBe("") }) }) + + describe("csh", () => { + test("#given single environment variable #when buildEnvPrefix is called with csh #then builds setenv command", () => { + const result = buildEnvPrefix({ VAR: "value" }, "csh") + expect(result).toBe("setenv VAR value;") + }) + + test("#given multiple environment variables #when buildEnvPrefix is called with csh #then builds separate setenv commands", () => { + const result = buildEnvPrefix({ VAR1: "val1", VAR2: "val2" }, "csh") + expect(result).toBe("setenv VAR1 val1; setenv VAR2 val2;") + }) + + test("#given env var with spaces #when buildEnvPrefix is called with csh #then escapes value with single quotes", () => { + const result = buildEnvPrefix({ MSG: "has spaces" }, "csh") + expect(result).toBe("setenv MSG 'has spaces';") + }) + + test("#given empty env object #when buildEnvPrefix is called with csh #then returns empty string", () => { + const result = buildEnvPrefix({}, "csh") + expect(result).toBe("") + }) + }) }) }) From 82b0672c05ece9fb0c8f48846cd8be331aaee145 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 18 May 2026 14:04:43 +0900 Subject: [PATCH 2/2] fix(git-master): emit csh-compatible setenv syntax for csh/tcsh shells buildShellAwareGitPrefix incorrectly returned raw bash VAR=value prefix for csh, which does not support inline env assignment. Now routes csh through buildEnvPrefix() to emit setenv syntax. Also sets code block lang to csh and skips bash block regex prefixing for csh. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../git-master-template-injection.test.ts | 69 +++++++++++++++++++ .../git-master-template-injection.ts | 10 +-- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/features/opencode-skill-loader/git-master-template-injection.test.ts b/src/features/opencode-skill-loader/git-master-template-injection.test.ts index 06be6051e..a521e13f4 100644 --- a/src/features/opencode-skill-loader/git-master-template-injection.test.ts +++ b/src/features/opencode-skill-loader/git-master-template-injection.test.ts @@ -231,6 +231,18 @@ describe("#given buildShellAwareGitPrefix", () => { }) }) + describe("#when shell type is csh", () => { + it("#then returns csh setenv syntax", () => { + const result = buildShellAwareGitPrefix("GIT_MASTER=1", "csh") + expect(result).toBe("setenv GIT_MASTER 1;") + }) + + it("#then handles multiple env vars", () => { + const result = buildShellAwareGitPrefix("CI=true GIT_MASTER=1", "csh") + expect(result).toBe("setenv CI true; setenv GIT_MASTER 1;") + }) + }) + describe("#when prefix is empty", () => { it("#then returns empty string", () => { const result = buildShellAwareGitPrefix("", "powershell") @@ -319,4 +331,61 @@ describe("#given PowerShell shell detection in injectGitMasterConfig", () => { expect(result).not.toContain("$env:") }) }) + + describe("#when shell is csh (SHELL set to /bin/csh)", () => { + it("#then emits setenv prefix syntax in csh code block", () => { + process.env.SHELL = "/bin/csh" + delete process.env.PSModulePath + delete process.env.MSYSTEM + Object.defineProperty(process, "platform", { value: "linux" }) + + const result = injectGitMasterConfig(SAMPLE_TEMPLATE, { + commit_footer: false, + include_co_authored_by: false, + git_env_prefix: "GIT_MASTER=1", + }) + + expect(result).toContain("setenv GIT_MASTER 1;") + expect(result).toContain("```csh") + expect(result).not.toContain("```bash\nsetenv") + }) + + it("#then does NOT prefix bash code blocks with setenv syntax", () => { + process.env.SHELL = "/bin/csh" + delete process.env.PSModulePath + delete process.env.MSYSTEM + Object.defineProperty(process, "platform", { value: "linux" }) + + const result = injectGitMasterConfig(SAMPLE_TEMPLATE, { + commit_footer: false, + include_co_authored_by: false, + git_env_prefix: "GIT_MASTER=1", + }) + + const bashBlockMatch = result.match(/```bash\r?\n([\s\S]*?)```/g) + if (bashBlockMatch) { + for (const block of bashBlockMatch) { + expect(block).not.toContain("setenv") + } + } + }) + }) + + describe("#when shell is tcsh (SHELL set to /usr/local/bin/tcsh)", () => { + it("#then emits setenv prefix syntax in csh code block", () => { + process.env.SHELL = "/usr/local/bin/tcsh" + delete process.env.PSModulePath + delete process.env.MSYSTEM + Object.defineProperty(process, "platform", { value: "darwin" }) + + const result = injectGitMasterConfig(SAMPLE_TEMPLATE, { + commit_footer: false, + include_co_authored_by: false, + git_env_prefix: "GIT_MASTER=1", + }) + + expect(result).toContain("setenv GIT_MASTER 1;") + expect(result).toContain("```csh") + }) + }) }) diff --git a/src/features/opencode-skill-loader/git-master-template-injection.ts b/src/features/opencode-skill-loader/git-master-template-injection.ts index 0483c2fc0..e1b1b05ba 100644 --- a/src/features/opencode-skill-loader/git-master-template-injection.ts +++ b/src/features/opencode-skill-loader/git-master-template-injection.ts @@ -26,15 +26,17 @@ export function parseBashEnvPrefix(prefix: string): Record { /** * Build the shell-aware command prefix for git commands. * Uses the shared shell detection and env prefix builder to emit correct syntax - * for PowerShell ($env:VAR='value';), cmd (set VAR="value" &&), or unix (VAR=value). + * for PowerShell ($env:VAR='value';), cmd (set VAR="value" &&), + * csh (setenv VAR value;), or unix (VAR=value). * * For unix shells, we use the inline VAR=value prefix style (not export) to match * the original behavior where the env var applies only to the immediately following command. + * For csh/tcsh, we use setenv syntax since csh does not support inline VAR=value. */ export function buildShellAwareGitPrefix(bashPrefix: string, shellType?: ShellType): string { if (!bashPrefix) return "" const resolvedShellType = shellType ?? detectShellType() - if (resolvedShellType === "unix" || resolvedShellType === "csh") { + if (resolvedShellType === "unix") { return bashPrefix } const envRecord = parseBashEnvPrefix(bashPrefix) @@ -48,8 +50,8 @@ export function injectGitMasterConfig(template: string, config?: GitMasterConfig const shellType = detectShellType() const shellPrefix = gitEnvPrefix ? buildShellAwareGitPrefix(gitEnvPrefix, shellType) : "" - const codeBlockLang = shellType === "powershell" ? "pwsh" : "bash" - const skipBashBlockPrefixing = shellType === "powershell" || shellType === "cmd" + const codeBlockLang = shellType === "powershell" ? "pwsh" : shellType === "csh" ? "csh" : "bash" + const skipBashBlockPrefixing = shellType === "powershell" || shellType === "cmd" || shellType === "csh" let result = gitEnvPrefix ? injectGitEnvPrefix(template, shellPrefix, codeBlockLang) : template