Skip to content

Commit 0042a07

Browse files
edemaineHona
andauthored
fix: Windows path support and canonicalization (anomalyco#13671)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
1 parent ab75ef8 commit 0042a07

6 files changed

Lines changed: 28 additions & 17 deletions

File tree

packages/opencode/src/patch/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,23 +79,23 @@ export namespace Patch {
7979
const line = lines[startIdx]
8080

8181
if (line.startsWith("*** Add File:")) {
82-
const filePath = line.split(":", 2)[1]?.trim()
82+
const filePath = line.slice("*** Add File:".length).trim()
8383
return filePath ? { filePath, nextIdx: startIdx + 1 } : null
8484
}
8585

8686
if (line.startsWith("*** Delete File:")) {
87-
const filePath = line.split(":", 2)[1]?.trim()
87+
const filePath = line.slice("*** Delete File:".length).trim()
8888
return filePath ? { filePath, nextIdx: startIdx + 1 } : null
8989
}
9090

9191
if (line.startsWith("*** Update File:")) {
92-
const filePath = line.split(":", 2)[1]?.trim()
92+
const filePath = line.slice("*** Update File:".length).trim()
9393
let movePath: string | undefined
9494
let nextIdx = startIdx + 1
9595

9696
// Check for move directive
9797
if (nextIdx < lines.length && lines[nextIdx].startsWith("*** Move to:")) {
98-
movePath = lines[nextIdx].split(":", 2)[1]?.trim()
98+
movePath = lines[nextIdx].slice("*** Move to:".length).trim()
9999
nextIdx++
100100
}
101101

packages/opencode/src/snapshot/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export namespace Snapshot {
105105
.split("\n")
106106
.map((x) => x.trim())
107107
.filter(Boolean)
108-
.map((x) => path.join(Instance.worktree, x)),
108+
.map((x) => path.join(Instance.worktree, x).replaceAll("\\", "/")),
109109
}
110110
}
111111

packages/opencode/src/tool/apply_patch.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
161161
// Build per-file metadata for UI rendering (used for both permission and result)
162162
const files = fileChanges.map((change) => ({
163163
filePath: change.filePath,
164-
relativePath: path.relative(Instance.worktree, change.movePath ?? change.filePath),
164+
relativePath: path.relative(Instance.worktree, change.movePath ?? change.filePath).replaceAll("\\", "/"),
165165
type: change.type,
166166
diff: change.diff,
167167
before: change.oldContent,
@@ -172,7 +172,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
172172
}))
173173

174174
// Check permissions if needed
175-
const relativePaths = fileChanges.map((c) => path.relative(Instance.worktree, c.filePath))
175+
const relativePaths = fileChanges.map((c) => path.relative(Instance.worktree, c.filePath).replaceAll("\\", "/"))
176176
await ctx.ask({
177177
permission: "edit",
178178
patterns: relativePaths,
@@ -242,13 +242,13 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
242242
// Generate output summary
243243
const summaryLines = fileChanges.map((change) => {
244244
if (change.type === "add") {
245-
return `A ${path.relative(Instance.worktree, change.filePath)}`
245+
return `A ${path.relative(Instance.worktree, change.filePath).replaceAll("\\", "/")}`
246246
}
247247
if (change.type === "delete") {
248-
return `D ${path.relative(Instance.worktree, change.filePath)}`
248+
return `D ${path.relative(Instance.worktree, change.filePath).replaceAll("\\", "/")}`
249249
}
250250
const target = change.movePath ?? change.filePath
251-
return `M ${path.relative(Instance.worktree, target)}`
251+
return `M ${path.relative(Instance.worktree, target).replaceAll("\\", "/")}`
252252
})
253253
let output = `Success. Updated the following files:\n${summaryLines.join("\n")}`
254254

@@ -264,7 +264,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
264264
const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE)
265265
const suffix =
266266
errors.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more` : ""
267-
output += `\n\nLSP errors detected in ${path.relative(Instance.worktree, target)}, please fix:\n<diagnostics file="${target}">\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</diagnostics>`
267+
output += `\n\nLSP errors detected in ${path.relative(Instance.worktree, target).replaceAll("\\", "/")}, please fix:\n<diagnostics file="${target}">\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</diagnostics>`
268268
}
269269
}
270270

packages/opencode/src/tool/bash.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,11 @@ export const BashTool = Tool.define("bash", async () => {
142142
}
143143

144144
if (directories.size > 0) {
145-
const globs = Array.from(directories).map((dir) => path.join(dir, "*"))
145+
const globs = Array.from(directories).map((dir) => {
146+
// Preserve POSIX-looking paths with /s, even on Windows
147+
if (dir.startsWith("/")) return `${dir.replace(/[\\/]+$/, "")}/*`
148+
return path.join(dir, "*")
149+
})
146150
await ctx.ask({
147151
permission: "external_directory",
148152
patterns: globs,

packages/opencode/test/skill/skill.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Instructions here.
5050
const testSkill = skills.find((s) => s.name === "test-skill")
5151
expect(testSkill).toBeDefined()
5252
expect(testSkill!.description).toBe("A test skill for verification.")
53-
expect(testSkill!.location).toContain("skill/test-skill/SKILL.md")
53+
expect(testSkill!.location).toContain(path.join("skill", "test-skill", "SKILL.md"))
5454
},
5555
})
5656
})
@@ -180,7 +180,7 @@ description: A skill in the .claude/skills directory.
180180
expect(skills.length).toBe(1)
181181
const claudeSkill = skills.find((s) => s.name === "claude-skill")
182182
expect(claudeSkill).toBeDefined()
183-
expect(claudeSkill!.location).toContain(".claude/skills/claude-skill/SKILL.md")
183+
expect(claudeSkill!.location).toContain(path.join(".claude", "skills", "claude-skill", "SKILL.md"))
184184
},
185185
})
186186
})
@@ -200,7 +200,7 @@ test("discovers global skills from ~/.claude/skills/ directory", async () => {
200200
expect(skills.length).toBe(1)
201201
expect(skills[0].name).toBe("global-test-skill")
202202
expect(skills[0].description).toBe("A global skill from ~/.claude/skills for testing.")
203-
expect(skills[0].location).toContain(".claude/skills/global-test-skill/SKILL.md")
203+
expect(skills[0].location).toContain(path.join(".claude", "skills", "global-test-skill", "SKILL.md"))
204204
},
205205
})
206206
} finally {
@@ -245,7 +245,7 @@ description: A skill in the .agents/skills directory.
245245
expect(skills.length).toBe(1)
246246
const agentSkill = skills.find((s) => s.name === "agent-skill")
247247
expect(agentSkill).toBeDefined()
248-
expect(agentSkill!.location).toContain(".agents/skills/agent-skill/SKILL.md")
248+
expect(agentSkill!.location).toContain(path.join(".agents", "skills", "agent-skill", "SKILL.md"))
249249
},
250250
})
251251
})
@@ -279,7 +279,7 @@ This skill is loaded from the global home directory.
279279
expect(skills.length).toBe(1)
280280
expect(skills[0].name).toBe("global-agent-skill")
281281
expect(skills[0].description).toBe("A global skill from ~/.agents/skills for testing.")
282-
expect(skills[0].location).toContain(".agents/skills/global-agent-skill/SKILL.md")
282+
expect(skills[0].location).toContain(path.join(".agents", "skills", "global-agent-skill", "SKILL.md"))
283283
},
284284
})
285285
} finally {

packages/opencode/test/tool/apply_patch.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ describe("tool.apply_patch freeform", () => {
9393

9494
expect(result.title).toContain("Success. Updated the following files")
9595
expect(result.output).toContain("Success. Updated the following files")
96+
// Strict formatting assertions for slashes
97+
expect(result.output).toMatch(/A nested\/new\.txt/)
98+
expect(result.output).toMatch(/D delete\.txt/)
99+
expect(result.output).toMatch(/M modify\.txt/)
100+
if (process.platform === "win32") {
101+
expect(result.output).not.toContain("\\")
102+
}
96103
expect(result.metadata.diff).toContain("Index:")
97104
expect(calls.length).toBe(1)
98105

0 commit comments

Comments
 (0)