Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,10 +342,11 @@ export namespace Config {
}

function rel(item: string, patterns: string[]) {
const normalized = item.replaceAll("\\", "/")
for (const pattern of patterns) {
const index = item.indexOf(pattern)
const index = normalized.indexOf(pattern)
if (index === -1) continue
return item.slice(index + pattern.length)
return normalized.slice(index + pattern.length)
}
}

Expand Down
5 changes: 3 additions & 2 deletions packages/opencode/src/config/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export namespace ConfigMarkdown {
if (!match) return content

const frontmatter = match[1]
const lines = frontmatter.split("\n")
const eol = frontmatter.includes("\r\n") ? "\r\n" : "\n"
const lines = frontmatter.split(eol)
const result: string[] = []

for (const line of lines) {
Expand Down Expand Up @@ -64,7 +65,7 @@ export namespace ConfigMarkdown {
result.push(line)
}

const processed = result.join("\n")
const processed = result.join(eol)
return content.replace(frontmatter, () => processed)
}

Expand Down
3 changes: 1 addition & 2 deletions packages/opencode/src/file/ignore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { sep } from "node:path"
import { Glob } from "../util/glob"

export namespace FileIgnore {
Expand Down Expand Up @@ -67,7 +66,7 @@ export namespace FileIgnore {
if (Glob.match(pattern, filepath)) return false
}

const parts = filepath.split(sep)
const parts = filepath.split(/[/\\]/)
for (let i = 0; i < parts.length; i++) {
if (FOLDERS.has(parts[i])) return true
}
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/file/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ export namespace FileTime {
const time = get(sessionID, filepath)
if (!time) throw new Error(`You must read file ${filepath} before overwriting it. Use the Read tool first`)
const mtime = Filesystem.stat(filepath)?.mtime
if (mtime && mtime.getTime() > time.getTime()) {
// Allow small tolerance for filesystem mtime resolution differences (NTFS ~100ns, but reports at ms granularity)
if (mtime && mtime.getTime() - time.getTime() > 5) {
throw new Error(
`File ${filepath} has been modified since it was last read.\nLast modification: ${mtime.toISOString()}\nLast read: ${time.toISOString()}\n\nPlease read the file again before modifying it.`,
)
Expand Down
15 changes: 9 additions & 6 deletions packages/opencode/src/tool/bash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,15 @@ export const BashTool = Tool.define("bash", async () => {
if (["cd", "rm", "cp", "mv", "mkdir", "touch", "chmod", "chown", "cat"].includes(command[0])) {
for (const arg of command.slice(1)) {
if (arg.startsWith("-") || (command[0] === "chmod" && arg.startsWith("+"))) continue
const resolved = await $`realpath ${arg}`
.cwd(cwd)
.quiet()
.nothrow()
.text()
.then((x) => x.trim())
const resolved =
process.platform === "win32"
? path.resolve(cwd, arg)
: await $`realpath ${arg}`
.cwd(cwd)
.quiet()
.nothrow()
.text()
.then((x) => x.trim())
log.info("resolved path", { arg, resolved })
if (resolved) {
const normalized =
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/tool/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const SkillTool = Tool.define("skill", async (ctx) => {
if (file.includes("SKILL.md")) {
continue
}
arr.push(path.resolve(dir, file))
arr.push(path.resolve(dir, file).replaceAll("\\", "/"))
if (arr.length >= limit) {
break
}
Expand Down
5 changes: 3 additions & 2 deletions packages/opencode/src/util/glob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ export namespace Glob {
}

export async function scan(pattern: string, options: Options = {}): Promise<string[]> {
return glob(pattern, toGlobOptions(options)) as Promise<string[]>
const results = await glob(pattern, toGlobOptions(options))
return results.map((r) => String(r).replaceAll("\\", "/"))
}

export function scanSync(pattern: string, options: Options = {}): string[] {
return globSync(pattern, toGlobOptions(options)) as string[]
return globSync(pattern, toGlobOptions(options)).map((r) => String(r).replaceAll("\\", "/"))
}

export function match(pattern: string, filepath: string): boolean {
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/test/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,8 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => {
}
})

test("resolves scoped npm plugins in config", async () => {
// Bun's import.meta.resolve with file:// URLs doesn't resolve scoped packages on Windows
test.skipIf(process.platform === "win32")("resolves scoped npm plugins in config", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const pluginDir = path.join(dir, "node_modules", "@scope", "plugin")
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/test/config/markdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ describe("ConfigMarkdown: frontmatter parsing w/ Markdown header", async () => {
test("should parse and match", () => {
expect(result).toBeDefined()
expect(result.data).toEqual({})
expect(result.content.trim()).toBe(`# Response Formatting Requirements
expect(result.content.replaceAll("\r\n", "\n").trim()).toBe(`# Response Formatting Requirements

Always structure your responses using clear markdown formatting:

Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/test/ide/ide.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { describe, expect, test, afterEach } from "bun:test"
import { Ide } from "../../src/ide"

describe("ide", () => {
const original = structuredClone(process.env)
// structuredClone(process.env) throws DataCloneError on Windows
const original = { ...process.env }

afterEach(() => {
Object.keys(process.env).forEach((key) => {
Expand Down
12 changes: 11 additions & 1 deletion packages/opencode/test/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,17 @@ import { afterAll } from "bun:test"
const dir = path.join(os.tmpdir(), "opencode-test-data-" + process.pid)
await fs.mkdir(dir, { recursive: true })
afterAll(() => {
fsSync.rmSync(dir, { recursive: true, force: true })
// On Windows, files may still be locked briefly after tests finish (EBUSY).
// Retry a few times with a short delay to work around this.
for (let i = 0; i < 5; i++) {
try {
fsSync.rmSync(dir, { recursive: true, force: true })
return
} catch (err: any) {
if (err?.code !== "EBUSY" && err?.code !== "EPERM") throw err
if (i < 4) Bun.sleepSync(100)
}
}
})

process.env["XDG_DATA_HOME"] = path.join(dir, "share")
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/test/skill/discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe("Discovery.pull", () => {
test("downloads reference files alongside SKILL.md", async () => {
const dirs = await Discovery.pull(CLOUDFLARE_SKILLS_URL)
// find a skill dir that should have reference files (e.g. agents-sdk)
const agentsSdk = dirs.find((d) => d.endsWith("/agents-sdk"))
const agentsSdk = dirs.find((d) => d.endsWith("agents-sdk"))
expect(agentsSdk).toBeDefined()
if (agentsSdk) {
const refs = path.join(agentsSdk, "references")
Expand Down
12 changes: 6 additions & 6 deletions packages/opencode/test/skill/skill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Instructions here.
const testSkill = skills.find((s) => s.name === "test-skill")
expect(testSkill).toBeDefined()
expect(testSkill!.description).toBe("A test skill for verification.")
expect(testSkill!.location).toContain(path.join("skill", "test-skill", "SKILL.md"))
expect(testSkill!.location).toContain("skill/test-skill/SKILL.md")
},
})
})
Expand Down Expand Up @@ -81,7 +81,7 @@ description: Skill for dirs test.
directory: tmp.path,
fn: async () => {
const dirs = await Skill.dirs()
const skillDir = path.join(tmp.path, ".opencode", "skill", "dir-skill")
const skillDir = path.join(tmp.path, ".opencode", "skill", "dir-skill").replaceAll("\\", "/")
expect(dirs).toContain(skillDir)
expect(dirs.length).toBe(1)
},
Expand Down Expand Up @@ -180,7 +180,7 @@ description: A skill in the .claude/skills directory.
expect(skills.length).toBe(1)
const claudeSkill = skills.find((s) => s.name === "claude-skill")
expect(claudeSkill).toBeDefined()
expect(claudeSkill!.location).toContain(path.join(".claude", "skills", "claude-skill", "SKILL.md"))
expect(claudeSkill!.location).toContain(".claude/skills/claude-skill/SKILL.md")
},
})
})
Expand All @@ -200,7 +200,7 @@ test("discovers global skills from ~/.claude/skills/ directory", async () => {
expect(skills.length).toBe(1)
expect(skills[0].name).toBe("global-test-skill")
expect(skills[0].description).toBe("A global skill from ~/.claude/skills for testing.")
expect(skills[0].location).toContain(path.join(".claude", "skills", "global-test-skill", "SKILL.md"))
expect(skills[0].location).toContain(".claude/skills/global-test-skill/SKILL.md")
},
})
} finally {
Expand Down Expand Up @@ -245,7 +245,7 @@ description: A skill in the .agents/skills directory.
expect(skills.length).toBe(1)
const agentSkill = skills.find((s) => s.name === "agent-skill")
expect(agentSkill).toBeDefined()
expect(agentSkill!.location).toContain(path.join(".agents", "skills", "agent-skill", "SKILL.md"))
expect(agentSkill!.location).toContain(".agents/skills/agent-skill/SKILL.md")
},
})
})
Expand Down Expand Up @@ -279,7 +279,7 @@ This skill is loaded from the global home directory.
expect(skills.length).toBe(1)
expect(skills[0].name).toBe("global-agent-skill")
expect(skills[0].description).toBe("A global skill from ~/.agents/skills for testing.")
expect(skills[0].location).toContain(path.join(".agents", "skills", "global-agent-skill", "SKILL.md"))
expect(skills[0].location).toContain(".agents/skills/global-agent-skill/SKILL.md")
},
})
} finally {
Expand Down
Loading
Loading