diff --git a/packages/opencode/src/kilocode/shell/shell.ts b/packages/opencode/src/kilocode/shell/shell.ts new file mode 100644 index 00000000000..34db598e530 --- /dev/null +++ b/packages/opencode/src/kilocode/shell/shell.ts @@ -0,0 +1,16 @@ +import { Buffer } from "node:buffer" + +export function args(command: string) { + return ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", encoded(command)] +} + +function encoded(command: string) { + const payload = Buffer.from(command, "utf8").toString("base64") + return Buffer.from( + `[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false); +[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); +$OutputEncoding = [Console]::OutputEncoding; +& ([scriptblock]::Create([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${payload}'))))`, + "utf16le", + ).toString("base64") +} diff --git a/packages/opencode/src/shell/shell.ts b/packages/opencode/src/shell/shell.ts index d86811cba0d..c51a0426efe 100644 --- a/packages/opencode/src/shell/shell.ts +++ b/packages/opencode/src/shell/shell.ts @@ -1,4 +1,5 @@ import { Flag } from "@opencode-ai/core/flag/flag" +import * as PowerShell from "@/kilocode/shell/shell" // kilocode_change - encoded PowerShell args import { lazy } from "@/util/lazy" import { Filesystem } from "@/util/filesystem" import { which } from "@/util/which" @@ -169,7 +170,7 @@ export function args(file: string, command: string, cwd: string) { cd -- "$1" eval ${JSON.stringify(command)} `, - "opencode", + "kilo", // kilocode_change cwd, ] } @@ -183,12 +184,12 @@ export function args(file: string, command: string, cwd: string) { cd -- "$1" eval ${JSON.stringify(command)} `, - "opencode", + "kilo", // kilocode_change cwd, ] } if (n === "cmd") return ["/c", command] - if (ps(file)) return ["-NoProfile", "-Command", command] + if (ps(file)) return PowerShell.args(command) // kilocode_change - encoded PowerShell args return ["-c", command] } diff --git a/packages/opencode/src/tool/shell.ts b/packages/opencode/src/tool/shell.ts index db3699af116..2261619cb65 100644 --- a/packages/opencode/src/tool/shell.ts +++ b/packages/opencode/src/tool/shell.ts @@ -312,7 +312,7 @@ const ask = Effect.fn("ShellTool.ask")(function* ( function cmd(shell: string, command: string, cwd: string, env: NodeJS.ProcessEnv) { if (process.platform === "win32" && Shell.ps(shell)) { - return ChildProcess.make(shell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", command], { + return ChildProcess.make(shell, Shell.args(shell, command, cwd), { // kilocode_change - encoded PowerShell args cwd, env, stdin: "ignore", diff --git a/packages/opencode/test/kilocode/shell/shell.test.ts b/packages/opencode/test/kilocode/shell/shell.test.ts new file mode 100644 index 00000000000..99eac3ecb02 --- /dev/null +++ b/packages/opencode/test/kilocode/shell/shell.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from "bun:test" +import { Buffer } from "node:buffer" +import * as PowerShell from "@/kilocode/shell/shell" +import { Shell } from "@/shell/shell" + +const command = `Write-Output "こんにちは 😀"; Write-Output '$value'; Write-Output \`tick\` +Write-Output "done"` + +function script(args: string[]) { + return Buffer.from(args[4], "base64").toString("utf16le") +} + +describe("PowerShell arguments", () => { + test("transports commands through UTF-8 inside EncodedCommand", () => { + const args = PowerShell.args(command) + const value = script(args) + const payload = value.match(/FromBase64String\('([^']+)'\)/)?.[1] + + expect(args.slice(0, 4)).toEqual(["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand"]) + expect(args).toHaveLength(5) + expect(value).toContain("[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)") + expect(value).toContain("[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)") + expect(value).toContain("$OutputEncoding = [Console]::OutputEncoding") + expect(payload).toBeDefined() + expect(Buffer.from(payload!, "base64").toString("utf8")).toBe(command) + }) + + test.each(["powershell", "pwsh"])("routes %s through the Kilo argument builder", (shell) => { + expect(Shell.args(shell, command, "/tmp")).toEqual(PowerShell.args(command)) + }) +})