diff --git a/packages/loop-js/src/cli/cron/wrapper.percent.test.ts b/packages/loop-js/src/cli/cron/wrapper.percent.test.ts new file mode 100644 index 0000000..9db610e --- /dev/null +++ b/packages/loop-js/src/cli/cron/wrapper.percent.test.ts @@ -0,0 +1,17 @@ +import { expect, test } from "bun:test" +import { cmdWrapper } from "./wrapper.ts" + +const AT = 1_700_000_000 + +test("cmd wrapper escapes literal percent signs in generated path and argv literals", () => { + const text = cmdWrapper( + { id: "abc123", dir: "C:\\work\\%TEMP%\\proj", until: { settled: false } }, + ["C:\\tools\\100%\\bun.exe", "C:\\cli\\loop%20cli.ts"], + AT, + ) + + expect(text).toContain('cd /d "C:\\work\\%%TEMP%%\\proj" || exit /b 1') + expect(text).toContain('"C:\\tools\\100%%\\bun.exe" "C:\\cli\\loop%%20cli.ts" "run"') + expect(text).toContain('>> "C:\\work\\%%TEMP%%\\proj\\.loop\\cron\\abc123.log" 2>&1') + expect(text).not.toContain('"C:\\work\\%TEMP%\\proj"') +}) diff --git a/packages/loop-js/src/cli/cron/wrapper.ts b/packages/loop-js/src/cli/cron/wrapper.ts index 54a687d..d0bf306 100644 --- a/packages/loop-js/src/cli/cron/wrapper.ts +++ b/packages/loop-js/src/cli/cron/wrapper.ts @@ -131,7 +131,9 @@ exit $rc export function cmdWrapper(entry: Pick, cli: string[], installedAt: number): string { const { id, dir, until } = entry const log = file(dir, id, "\\", ".log") - const q = (s: string) => `"${s}"` + // `%...%` and `%0`-`%9` are substitutions even inside quotes in batch files. `%%` emits a + // literal percent sign, so filesystem/argv strings survive paths such as `C:\work\%TEMP%`. + const q = (s: string) => `"${s.replaceAll("%", "%%")}"` const argv = (sub: string[]) => [...cli, ...sub].map(q).join(" ") const run = `${argv(["run"])} >> ${q(log)} 2>&1` const remove = `${argv(["cron", "remove", id])} >> ${q(log)} 2>&1`