diff --git a/packages/app/src/custom-elements.d.ts b/packages/app/src/custom-elements.d.ts
index e4ea0d6cebd..bd6fdcad3b4 120000
--- a/packages/app/src/custom-elements.d.ts
+++ b/packages/app/src/custom-elements.d.ts
@@ -1 +1,2 @@
-../../ui/src/custom-elements.d.ts
\ No newline at end of file
+///
+export {}
diff --git a/packages/enterprise/src/custom-elements.d.ts b/packages/enterprise/src/custom-elements.d.ts
index e4ea0d6cebd..bd6fdcad3b4 120000
--- a/packages/enterprise/src/custom-elements.d.ts
+++ b/packages/enterprise/src/custom-elements.d.ts
@@ -1 +1,2 @@
-../../ui/src/custom-elements.d.ts
\ No newline at end of file
+///
+export {}
diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts
index 7b6f028fcd9..79b1b395e63 100644
--- a/packages/opencode/src/cli/cmd/tui/thread.ts
+++ b/packages/opencode/src/cli/cmd/tui/thread.ts
@@ -7,6 +7,151 @@ import { UI } from "@/cli/ui"
import { iife } from "@/util/iife"
import { Log } from "@/util/log"
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
+import { existsSync } from "fs"
+
+type PaneSource = "option" | "positional"
+
+const PANE_COUNTS = new Set([2, 4, 6])
+
+function parsePaneCount(value: string | number | undefined): number | null {
+ if (value === undefined || value === null) return null
+ const count = typeof value === "number" ? value : Number(value)
+ if (!Number.isInteger(count)) return null
+ return PANE_COUNTS.has(count) ? count : null
+}
+
+function buildChildArgs(
+ rawArgs: string[],
+ paneCount: number,
+ paneSource: PaneSource,
+): string[] {
+ const result: string[] = []
+ let removedPositional = false
+ let skipNext = false
+ let seenDoubleDash = false
+ const paneString = String(paneCount)
+
+ for (const arg of rawArgs) {
+ if (skipNext) {
+ skipNext = false
+ continue
+ }
+ if (arg === "--") {
+ seenDoubleDash = true
+ result.push(arg)
+ continue
+ }
+ if (!seenDoubleDash && paneSource === "option") {
+ if (arg === "--panes") {
+ skipNext = true
+ continue
+ }
+ if (arg.startsWith("--panes=")) {
+ continue
+ }
+ }
+ if (!seenDoubleDash && paneSource === "positional" && !removedPositional && !arg.startsWith("-") && arg === paneString) {
+ removedPositional = true
+ continue
+ }
+ result.push(arg)
+ }
+
+ return result
+}
+
+function buildSplitCommand(
+ direction: "V" | "H",
+ size: string,
+ cwd: string,
+ profileId: string | undefined,
+ childArgs: string[],
+): string[] {
+ const args = ["split-pane", `-${direction}`, "--size", size]
+ if (profileId) args.push("-p", profileId)
+ args.push("-d", cwd, "opencode", ...childArgs)
+ return args
+}
+
+function buildPaneLayout(
+ paneCount: number,
+ cwd: string,
+ profileId: string | undefined,
+ childArgs: string[],
+): string[] | null {
+ const splitV33 = buildSplitCommand("V", "0.33", cwd, profileId, childArgs)
+ const splitV50 = buildSplitCommand("V", "0.5", cwd, profileId, childArgs)
+ const splitH50 = buildSplitCommand("H", "0.5", cwd, profileId, childArgs)
+
+ if (paneCount === 2) {
+ return ["-w", "0", ...splitV50]
+ }
+ if (paneCount === 4) {
+ return [
+ "-w",
+ "0",
+ ...splitV50,
+ ";",
+ "move-focus",
+ "left",
+ ";",
+ ...splitH50,
+ ";",
+ "move-focus",
+ "right",
+ ";",
+ ...splitH50,
+ ]
+ }
+ if (paneCount === 6) {
+ return [
+ "-w",
+ "0",
+ ...splitV33,
+ ";",
+ ...splitV50,
+ ";",
+ ...splitH50,
+ ";",
+ "move-focus",
+ "left",
+ ";",
+ ...splitH50,
+ ";",
+ "move-focus",
+ "left",
+ ";",
+ ...splitH50,
+ ]
+ }
+ return null
+}
+
+function splitWindowsTerminalPanes(
+ paneCount: number,
+ cwd: string,
+ childArgs: string[],
+): boolean {
+ if (process.platform !== "win32") return false
+ if (!process.env.WT_SESSION) return false
+ const wt = Bun.which("wt") ?? Bun.which("wt.exe")
+ if (!wt) return false
+
+ const profileId = process.env.WT_PROFILE_ID
+ const layout = buildPaneLayout(paneCount, cwd, profileId, childArgs)
+ if (!layout) return false
+
+ try {
+ Bun.spawn({
+ cmd: [wt, ...layout],
+ stdout: "ignore",
+ stderr: "ignore",
+ })
+ return true
+ } catch {
+ return false
+ }
+}
declare global {
const OPENCODE_WORKER_PATH: string
@@ -21,6 +166,10 @@ export const TuiThreadCommand = cmd({
type: "string",
describe: "path to start opencode in",
})
+ .option("panes", {
+ type: "number",
+ describe: "open multiple Windows Terminal panes (2, 4, 6)",
+ })
.option("model", {
type: "string",
alias: ["m"],
@@ -47,7 +196,23 @@ export const TuiThreadCommand = cmd({
handler: async (args) => {
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
const baseCwd = process.env.PWD ?? process.cwd()
- const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
+ const rawArgs = process.argv.slice(2)
+ const paneFromOption = parsePaneCount(args.panes)
+ let paneCount = paneFromOption
+ let paneSource: PaneSource | null = paneFromOption ? "option" : null
+ let projectArg = args.project
+ if (!paneCount) {
+ const fromProject = parsePaneCount(args.project)
+ if (fromProject) {
+ const candidate = path.resolve(baseCwd, String(args.project))
+ if (!existsSync(candidate)) {
+ paneCount = fromProject
+ paneSource = "positional"
+ projectArg = undefined
+ }
+ }
+ }
+ const cwd = projectArg ? path.resolve(baseCwd, projectArg) : process.cwd()
const localWorker = new URL("./worker.ts", import.meta.url)
const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url)
const workerPath = await iife(async () => {
@@ -55,6 +220,11 @@ export const TuiThreadCommand = cmd({
if (await Bun.file(distWorker).exists()) return distWorker
return localWorker
})
+ if (paneCount && paneSource) {
+ const childArgs = buildChildArgs(rawArgs, paneCount, paneSource)
+ splitWindowsTerminalPanes(paneCount, cwd, childArgs)
+ }
+
try {
process.chdir(cwd)
} catch (e) {
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index ead3a0149b4..f6def884c0b 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -1,6 +1,7 @@
import { Log } from "../util/log"
import path from "path"
import { pathToFileURL } from "url"
+import { createRequire } from "module"
import os from "os"
import z from "zod"
import { Filesystem } from "../util/filesystem"
@@ -23,6 +24,24 @@ import { existsSync } from "fs"
export namespace Config {
const log = Log.create({ service: "config" })
+ function resolvePluginSpecifier(plugin: string, configFilepath: string) {
+ const baseUrl = pathToFileURL(configFilepath).href
+ try {
+ return import.meta.resolve(plugin, baseUrl)
+ } catch {}
+ try {
+ const req = createRequire(configFilepath)
+ return pathToFileURL(req.resolve(plugin)).href
+ } catch {}
+ try {
+ const resolved = Bun.resolveSync(plugin, configFilepath)
+ return resolved.startsWith("file://")
+ ? resolved
+ : pathToFileURL(resolved).href
+ } catch {}
+ return null
+ }
+
// Custom merge function that concatenates array fields instead of replacing them
function mergeConfigConcatArrays(target: Info, source: Info): Info {
const merged = mergeDeep(target, source)
@@ -1170,9 +1189,8 @@ export namespace Config {
if (data.plugin) {
for (let i = 0; i < data.plugin.length; i++) {
const plugin = data.plugin[i]
- try {
- data.plugin[i] = import.meta.resolve!(plugin, configFilepath)
- } catch (err) {}
+ const resolved = resolvePluginSpecifier(plugin, configFilepath)
+ if (resolved) data.plugin[i] = resolved
}
}
return data
diff --git a/packages/opencode/src/file/ignore.ts b/packages/opencode/src/file/ignore.ts
index 7230f67afeb..5beda1b8fbc 100644
--- a/packages/opencode/src/file/ignore.ts
+++ b/packages/opencode/src/file/ignore.ts
@@ -1,5 +1,3 @@
-import { sep } from "node:path"
-
export namespace FileIgnore {
const FOLDERS = new Set([
"node_modules",
@@ -64,18 +62,19 @@ export namespace FileIgnore {
whitelist?: Bun.Glob[]
},
) {
+ const normalized = filepath.replaceAll("\\", "/")
for (const glob of opts?.whitelist || []) {
- if (glob.match(filepath)) return false
+ if (glob.match(normalized)) return false
}
- const parts = filepath.split(sep)
+ const parts = normalized.split("/")
for (let i = 0; i < parts.length; i++) {
if (FOLDERS.has(parts[i])) return true
}
const extra = opts?.extra || []
for (const glob of [...FILE_GLOBS, ...extra]) {
- if (glob.match(filepath)) return true
+ if (glob.match(normalized)) return true
}
return false
diff --git a/packages/opencode/src/patch/index.ts b/packages/opencode/src/patch/index.ts
index 91d52065f6f..b2e107c3e69 100644
--- a/packages/opencode/src/patch/index.ts
+++ b/packages/opencode/src/patch/index.ts
@@ -76,25 +76,29 @@ export namespace Patch {
startIdx: number,
): { filePath: string; movePath?: string; nextIdx: number } | null {
const line = lines[startIdx]
+ const addPrefix = "*** Add File:"
+ const deletePrefix = "*** Delete File:"
+ const updatePrefix = "*** Update File:"
+ const movePrefix = "*** Move to:"
- if (line.startsWith("*** Add File:")) {
- const filePath = line.split(":", 2)[1]?.trim()
+ if (line.startsWith(addPrefix)) {
+ const filePath = line.slice(addPrefix.length).trim()
return filePath ? { filePath, nextIdx: startIdx + 1 } : null
}
- if (line.startsWith("*** Delete File:")) {
- const filePath = line.split(":", 2)[1]?.trim()
+ if (line.startsWith(deletePrefix)) {
+ const filePath = line.slice(deletePrefix.length).trim()
return filePath ? { filePath, nextIdx: startIdx + 1 } : null
}
- if (line.startsWith("*** Update File:")) {
- const filePath = line.split(":", 2)[1]?.trim()
+ if (line.startsWith(updatePrefix)) {
+ const filePath = line.slice(updatePrefix.length).trim()
let movePath: string | undefined
let nextIdx = startIdx + 1
// Check for move directive
- if (nextIdx < lines.length && lines[nextIdx].startsWith("*** Move to:")) {
- movePath = lines[nextIdx].split(":", 2)[1]?.trim()
+ if (nextIdx < lines.length && lines[nextIdx].startsWith(movePrefix)) {
+ movePath = lines[nextIdx].slice(movePrefix.length).trim()
nextIdx++
}
diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts
index 35fdd4717b2..3813b50f845 100644
--- a/packages/opencode/src/project/project.ts
+++ b/packages/opencode/src/project/project.ts
@@ -140,7 +140,7 @@ export namespace Project {
.then((x) => {
const dirname = path.dirname(x.trim())
if (dirname === ".") return sandbox
- return dirname
+ return path.resolve(sandbox, dirname)
})
.catch(() => undefined)
diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts
index e06a3f157cb..5236f29b1c7 100644
--- a/packages/opencode/src/tool/bash.ts
+++ b/packages/opencode/src/tool/bash.ts
@@ -117,6 +117,7 @@ export const BashTool = Tool.define("bash", async () => {
.nothrow()
.text()
.then((x) => x.trim())
+ .then((x) => (x ? x : path.resolve(cwd, arg)))
log.info("resolved path", { arg, resolved })
if (resolved) {
// Git Bash on Windows returns Unix-style paths like /c/Users/...
diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts
index 087eb0c628c..7dcbcc55cb6 100644
--- a/packages/opencode/test/config/config.test.ts
+++ b/packages/opencode/test/config/config.test.ts
@@ -6,6 +6,7 @@ import { tmpdir } from "../fixture/fixture"
import path from "path"
import fs from "fs/promises"
import { pathToFileURL } from "url"
+import { createRequire } from "module"
test("loads config with defaults when no files exist", async () => {
await using tmp = await tmpdir()
@@ -400,9 +401,24 @@ test("resolves scoped npm plugins in config", async () => {
const pluginEntries = config.plugin ?? []
const baseUrl = pathToFileURL(path.join(tmp.path, "opencode.json")).href
- const expected = import.meta.resolve("@scope/plugin", baseUrl)
-
- expect(pluginEntries.includes(expected)).toBe(true)
+ let expected: string | null = null
+ try {
+ expected = import.meta.resolve("@scope/plugin", baseUrl)
+ } catch {}
+ if (!expected) {
+ try {
+ expected = pathToFileURL(createRequire(baseUrl).resolve("@scope/plugin")).href
+ } catch {}
+ }
+ if (!expected) {
+ try {
+ expected = pathToFileURL(Bun.resolveSync("@scope/plugin", path.join(tmp.path, "opencode.json"))).href
+ } catch {}
+ }
+
+ expect(expected).toBeTruthy()
+
+ expect(pluginEntries.includes(expected!)).toBe(true)
const scopedEntry = pluginEntries.find((entry) => entry === expected)
expect(scopedEntry).toBeDefined()
diff --git a/packages/opencode/test/ide/ide.test.ts b/packages/opencode/test/ide/ide.test.ts
index 4d70140197f..74a456606b0 100644
--- a/packages/opencode/test/ide/ide.test.ts
+++ b/packages/opencode/test/ide/ide.test.ts
@@ -2,7 +2,7 @@ import { describe, expect, test, afterEach } from "bun:test"
import { Ide } from "../../src/ide"
describe("ide", () => {
- const original = structuredClone(process.env)
+ const original = Object.fromEntries(Object.entries(process.env))
afterEach(() => {
Object.keys(process.env).forEach((key) => {
diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts
index d44e606746e..8253cc84099 100644
--- a/packages/opencode/test/project/project.test.ts
+++ b/packages/opencode/test/project/project.test.ts
@@ -54,8 +54,8 @@ describe("Project.fromDirectory with worktrees", () => {
test("should set worktree to root when called from a worktree", async () => {
await using tmp = await tmpdir({ git: true })
-
- const worktreePath = path.join(tmp.path, "..", "worktree-test")
+ const baseName = path.basename(tmp.path)
+ const worktreePath = path.join(tmp.path, "..", `${baseName}-worktree-test`)
await $`git worktree add ${worktreePath} -b test-branch`.cwd(tmp.path).quiet()
const { project, sandbox } = await Project.fromDirectory(worktreePath)
@@ -70,9 +70,9 @@ describe("Project.fromDirectory with worktrees", () => {
test("should accumulate multiple worktrees in sandboxes", async () => {
await using tmp = await tmpdir({ git: true })
-
- const worktree1 = path.join(tmp.path, "..", "worktree-1")
- const worktree2 = path.join(tmp.path, "..", "worktree-2")
+ const baseName = path.basename(tmp.path)
+ const worktree1 = path.join(tmp.path, "..", `${baseName}-worktree-1`)
+ const worktree2 = path.join(tmp.path, "..", `${baseName}-worktree-2`)
await $`git worktree add ${worktree1} -b branch-1`.cwd(tmp.path).quiet()
await $`git worktree add ${worktree2} -b branch-2`.cwd(tmp.path).quiet()
diff --git a/packages/opencode/test/skill/skill.test.ts b/packages/opencode/test/skill/skill.test.ts
index 72415c1411e..6354b389d96 100644
--- a/packages/opencode/test/skill/skill.test.ts
+++ b/packages/opencode/test/skill/skill.test.ts
@@ -5,6 +5,8 @@ import { tmpdir } from "../fixture/fixture"
import path from "path"
import fs from "fs/promises"
+const normalizePath = (value: string) => value.replaceAll("\\", "/")
+
async function createGlobalSkill(homeDir: string) {
const skillDir = path.join(homeDir, ".claude", "skills", "global-test-skill")
await fs.mkdir(skillDir, { recursive: true })
@@ -49,8 +51,8 @@ Instructions here.
expect(skills.length).toBe(1)
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("skill/test-skill/SKILL.md")
+ expect(testSkill!.description).toBe("A test skill for verification.")
+ expect(normalizePath(testSkill!.location)).toContain("skill/test-skill/SKILL.md")
},
})
})
@@ -144,7 +146,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(".claude/skills/claude-skill/SKILL.md")
+ expect(normalizePath(claudeSkill!.location)).toContain(".claude/skills/claude-skill/SKILL.md")
},
})
})
@@ -164,7 +166,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(".claude/skills/global-test-skill/SKILL.md")
+ expect(normalizePath(skills[0].location)).toContain(".claude/skills/global-test-skill/SKILL.md")
},
})
} finally {
diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts
index cf933f81286..7c8aa08761f 100644
--- a/packages/opencode/test/snapshot/snapshot.test.ts
+++ b/packages/opencode/test/snapshot/snapshot.test.ts
@@ -3,6 +3,16 @@ import { $ } from "bun"
import { Snapshot } from "../../src/snapshot"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
+import path from "path"
+
+const normalizePath = (value: string) => value.replaceAll("\\", "/")
+const expectFilesToContain = (files: string[], expected: string) => {
+ expect(files.map(normalizePath)).toContain(normalizePath(expected))
+}
+const expectFilesNotToContain = (files: string[], expected: string) => {
+ expect(files.map(normalizePath)).not.toContain(normalizePath(expected))
+}
+const testPosix = process.platform === "win32" ? test.skip : test
async function bootstrap() {
return tmpdir({
@@ -33,7 +43,7 @@ test("tracks deleted files correctly", async () => {
await $`rm ${tmp.path}/a.txt`.quiet()
- expect((await Snapshot.patch(before!)).files).toContain(`${tmp.path}/a.txt`)
+ expectFilesToContain((await Snapshot.patch(before!)).files, `${tmp.path}/a.txt`)
},
})
})
@@ -126,7 +136,7 @@ test("binary file handling", async () => {
await Bun.write(`${tmp.path}/image.png`, new Uint8Array([0x89, 0x50, 0x4e, 0x47]))
const patch = await Snapshot.patch(before!)
- expect(patch.files).toContain(`${tmp.path}/image.png`)
+ expectFilesToContain(patch.files, `${tmp.path}/image.png`)
await Snapshot.revert([patch])
expect(await Bun.file(`${tmp.path}/image.png`).exists()).toBe(false)
@@ -134,7 +144,7 @@ test("binary file handling", async () => {
})
})
-test("symlink handling", async () => {
+testPosix("symlink handling", async () => {
await using tmp = await bootstrap()
await Instance.provide({
directory: tmp.path,
@@ -144,7 +154,7 @@ test("symlink handling", async () => {
await $`ln -s ${tmp.path}/a.txt ${tmp.path}/link.txt`.quiet()
- expect((await Snapshot.patch(before!)).files).toContain(`${tmp.path}/link.txt`)
+ expectFilesToContain((await Snapshot.patch(before!)).files, `${tmp.path}/link.txt`)
},
})
})
@@ -159,7 +169,7 @@ test("large file handling", async () => {
await Bun.write(`${tmp.path}/large.txt`, "x".repeat(1024 * 1024))
- expect((await Snapshot.patch(before!)).files).toContain(`${tmp.path}/large.txt`)
+ expectFilesToContain((await Snapshot.patch(before!)).files, `${tmp.path}/large.txt`)
},
})
})
@@ -195,9 +205,9 @@ test("special characters in filenames", async () => {
await Bun.write(`${tmp.path}/file_with_underscores.txt`, "UNDERSCORES")
const files = (await Snapshot.patch(before!)).files
- expect(files).toContain(`${tmp.path}/file with spaces.txt`)
- expect(files).toContain(`${tmp.path}/file-with-dashes.txt`)
- expect(files).toContain(`${tmp.path}/file_with_underscores.txt`)
+ expectFilesToContain(files, `${tmp.path}/file with spaces.txt`)
+ expectFilesToContain(files, `${tmp.path}/file-with-dashes.txt`)
+ expectFilesToContain(files, `${tmp.path}/file_with_underscores.txt`)
},
})
})
@@ -295,13 +305,15 @@ test("very long filenames", async () => {
const before = await Snapshot.track()
expect(before).toBeTruthy()
- const longName = "a".repeat(200) + ".txt"
+ const maxNameLength =
+ process.platform === "win32" ? Math.max(20, 240 - tmp.path.length) : 200
+ const longName = "a".repeat(maxNameLength) + ".txt"
const longFile = `${tmp.path}/${longName}`
await Bun.write(longFile, "long filename content")
const patch = await Snapshot.patch(before!)
- expect(patch.files).toContain(longFile)
+ expectFilesToContain(patch.files, longFile)
await Snapshot.revert([patch])
expect(await Bun.file(longFile).exists()).toBe(false)
@@ -322,14 +334,14 @@ test("hidden files", async () => {
await Bun.write(`${tmp.path}/.config`, "config content")
const patch = await Snapshot.patch(before!)
- expect(patch.files).toContain(`${tmp.path}/.hidden`)
- expect(patch.files).toContain(`${tmp.path}/.gitignore`)
- expect(patch.files).toContain(`${tmp.path}/.config`)
+ expectFilesToContain(patch.files, `${tmp.path}/.hidden`)
+ expectFilesToContain(patch.files, `${tmp.path}/.gitignore`)
+ expectFilesToContain(patch.files, `${tmp.path}/.config`)
},
})
})
-test("nested symlinks", async () => {
+testPosix("nested symlinks", async () => {
await using tmp = await bootstrap()
await Instance.provide({
directory: tmp.path,
@@ -343,13 +355,13 @@ test("nested symlinks", async () => {
await $`ln -s ${tmp.path}/sub ${tmp.path}/sub-link`.quiet()
const patch = await Snapshot.patch(before!)
- expect(patch.files).toContain(`${tmp.path}/sub/dir/link.txt`)
- expect(patch.files).toContain(`${tmp.path}/sub-link`)
+ expectFilesToContain(patch.files, `${tmp.path}/sub/dir/link.txt`)
+ expectFilesToContain(patch.files, `${tmp.path}/sub-link`)
},
})
})
-test("file permissions and ownership changes", async () => {
+testPosix("file permissions and ownership changes", async () => {
await using tmp = await bootstrap()
await Instance.provide({
directory: tmp.path,
@@ -402,11 +414,11 @@ test("gitignore changes", async () => {
const patch = await Snapshot.patch(before!)
// Should track gitignore itself
- expect(patch.files).toContain(`${tmp.path}/.gitignore`)
+ expectFilesToContain(patch.files, `${tmp.path}/.gitignore`)
// Should track normal files
- expect(patch.files).toContain(`${tmp.path}/normal.txt`)
+ expectFilesToContain(patch.files, `${tmp.path}/normal.txt`)
// Should not track ignored files (git won't see them)
- expect(patch.files).not.toContain(`${tmp.path}/test.ignored`)
+ expectFilesNotToContain(patch.files, `${tmp.path}/test.ignored`)
},
})
})
@@ -451,7 +463,7 @@ test("snapshot state isolation between projects", async () => {
const before1 = await Snapshot.track()
await Bun.write(`${tmp1.path}/project1.txt`, "project1 content")
const patch1 = await Snapshot.patch(before1!)
- expect(patch1.files).toContain(`${tmp1.path}/project1.txt`)
+ expectFilesToContain(patch1.files, `${tmp1.path}/project1.txt`)
},
})
@@ -461,10 +473,10 @@ test("snapshot state isolation between projects", async () => {
const before2 = await Snapshot.track()
await Bun.write(`${tmp2.path}/project2.txt`, "project2 content")
const patch2 = await Snapshot.patch(before2!)
- expect(patch2.files).toContain(`${tmp2.path}/project2.txt`)
+ expectFilesToContain(patch2.files, `${tmp2.path}/project2.txt`)
// Ensure project1 files don't appear in project2
- expect(patch2.files).not.toContain(`${tmp1?.path}/project1.txt`)
+ expectFilesNotToContain(patch2.files, `${tmp1?.path}/project1.txt`)
},
})
})
@@ -492,7 +504,7 @@ test("patch detects changes in secondary worktree", async () => {
await Bun.write(worktreeFile, "worktree content")
const patch = await Snapshot.patch(before!)
- expect(patch.files).toContain(worktreeFile)
+ expectFilesToContain(patch.files, worktreeFile)
},
})
} finally {
@@ -658,7 +670,7 @@ test("revert should not delete files that existed but were deleted in snapshot",
await Bun.write(`${tmp.path}/a.txt`, "recreated content")
const patch = await Snapshot.patch(snapshot2!)
- expect(patch.files).toContain(`${tmp.path}/a.txt`)
+ expectFilesToContain(patch.files, `${tmp.path}/a.txt`)
await Snapshot.revert([patch])
@@ -682,8 +694,8 @@ test("revert preserves file that existed in snapshot when deleted then recreated
await Bun.write(`${tmp.path}/newfile.txt`, "new")
const patch = await Snapshot.patch(snapshot!)
- expect(patch.files).toContain(`${tmp.path}/existing.txt`)
- expect(patch.files).toContain(`${tmp.path}/newfile.txt`)
+ expectFilesToContain(patch.files, `${tmp.path}/existing.txt`)
+ expectFilesToContain(patch.files, `${tmp.path}/newfile.txt`)
await Snapshot.revert([patch])