fix(tool): strip leading colon before Windows path shapes

expandPath's leading-colon strip only fired before POSIX prefixes
(`/`, `~/`, `./`, `../`), so Windows-mangled inputs like `:C:\repo\file`
or `:.\src` slipped through and path.resolve treated them as relative
children of cwd. The omp grep subcommand is the Windows grep path, so it
hit the common Windows form of the same bug.

Broaden the lookahead to admit `\` separators, `.\`/`..\` relatives, and
drive-letter absolutes (`[A-Za-z]:`). Add expandPath regression tests
for the Windows forms and the bare-token non-strip cases.

Fixes #5624
This commit is contained in:
roboomp
2026-07-15 22:25:44 +00:00
parent e291d77cb0
commit f512d99614
3 changed files with 27 additions and 6 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
### Fixed
- Fixed the `omp grep` CLI subcommand failing on paths with a stray leading colon (e.g. `:/abs/path`); it now routes the path argument through `expandPath` like `read`/`edit`/in-agent `grep` ([#5624](https://github.com/can1357/oh-my-pi/issues/5624)).
- Fixed the `omp grep` CLI subcommand failing on paths with a stray leading colon (e.g. `:/abs/path`); it now routes the path argument through `expandPath` like `read`/`edit`/in-agent `grep`. Broadened `expandPath`'s leading-colon strip to also recover Windows-style shapes (`:C:\repo\file`, `:.\src`, `:..\rel`, `:\\server\share`) ([#5624](https://github.com/can1357/oh-my-pi/issues/5624)).
## [17.0.0] - 2026-07-15
@@ -148,11 +148,14 @@ export function expandTilde(filePath: string, home?: string): string {
export function expandPath(filePath: string): string {
// Some models intermittently prefix an otherwise-valid path with a stray
// `:` (e.g. `:/abs/path`, `:../rel`). No real path starts with `:` and it
// never begins a selector against an absolute/relative path, so strip it
// before resolution — mirroring the `@`-prefix normalization above and the
// implicit stripping `write` already tolerates (issue #5508).
const deColoned = /^:(?=[/~]|\.\.?\/)/.test(filePath) ? filePath.slice(1) : filePath;
// `:` (e.g. `:/abs/path`, `:../rel`, or the Windows forms `:C:\repo\file`
// and `:.\src`). No real path starts with `:` and it never begins a
// selector against an absolute/relative path, so strip it before
// resolution — mirroring the `@`-prefix normalization above and the
// implicit stripping `write` already tolerates (issues #5508, #5624). The
// lookahead admits POSIX (`/`, `~`, `./`, `../`) and Windows (`\`, `.\`,
// `..\`, drive-letter `C:`) path shapes.
const deColoned = /^:(?=[/\\~]|\.\.?[/\\]|[A-Za-z]:)/.test(filePath) ? filePath.slice(1) : filePath;
const normalized = stripWindowsExtendedLengthPathPrefix(
stripFileUrl(normalizeUnicodeSpaces(normalizeAtPrefix(deColoned))),
);
@@ -6,6 +6,7 @@ import { resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config
import { EditTool } from "@oh-my-pi/pi-coding-agent/edit";
import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
import {
expandPath,
probeLiteralPathExists,
resolveToCwd,
splitPathAndSel,
@@ -363,6 +364,23 @@ describe("leading-colon path recovery (issue #5508)", () => {
expect(resolveToCwd(":name.txt", tmpDir)).toBe(path.join(tmpDir, ":name.txt"));
});
it("strips a leading colon before Windows path shapes in expandPath (issue #5624)", () => {
// Windows native paths mangled with a stray leading colon: drive-letter
// absolutes and `\`/`.\`/`..\` relative forms. expandPath runs before any
// path.resolve, so the strip is platform-independent.
expect(expandPath(":C:\\repo\\file.ts")).toBe("C:\\repo\\file.ts");
expect(expandPath(":.\\src")).toBe(".\\src");
expect(expandPath(":..\\sibling")).toBe("..\\sibling");
expect(expandPath(":\\\\server\\share")).toBe("\\\\server\\share");
});
it("does not strip a colon before a bare drive letter without a path (expandPath)", () => {
// `:selector` shapes still round-trip; the drive-letter branch requires
// the `<letter>:` colon to follow, distinguishing `:C:\x` from `:cache`.
expect(expandPath(":raw")).toBe(":raw");
expect(expandPath(":cache")).toBe(":cache");
});
it("read opens a file addressed with a leading colon", async () => {
const abs = path.join(tmpDir, "colon-read.txt");
await Bun.write(abs, "test line A\ntest line B\n");