diff --git a/packages/opencode/src/kilocode/cli/cmd/tui-worktree.ts b/packages/opencode/src/kilocode/cli/cmd/tui-worktree.ts index 46428f7985..be3318c5fe 100644 --- a/packages/opencode/src/kilocode/cli/cmd/tui-worktree.ts +++ b/packages/opencode/src/kilocode/cli/cmd/tui-worktree.ts @@ -5,10 +5,13 @@ // created in. import path from "path" import type { Effect } from "effect" +import * as Log from "@opencode-ai/core/util/log" import { UI } from "@/cli/ui" import { Filesystem } from "@/util/filesystem" import { errorMessage } from "@/util/error" +const log = Log.create({ service: "kilocode.tui-worktree" }) + // Matches packages/kilo-vscode/src/agent-manager/WorktreeManager.ts's placement // and its ensureGitExclude(), keeping `.kilo/worktrees/` out of `git status`. const KILO_WORKTREE_DIR = ".kilo/worktrees" @@ -19,7 +22,9 @@ export async function ensureGitExclude(root: string) { const current = (await Filesystem.readText(excludePath).catch(() => "")).replace(/\s+$/, "") if (current.includes(`${KILO_WORKTREE_DIR}/`)) return const prefix = current ? `${current}\n\n` : "" - await Filesystem.write(excludePath, `${prefix}# Kilo Code agent worktrees\n${KILO_WORKTREE_DIR}/\n`).catch(() => {}) + await Filesystem.write(excludePath, `${prefix}# Kilo Code agent worktrees\n${KILO_WORKTREE_DIR}/\n`).catch((err) => + log.error("failed to update .git/info/exclude", { excludePath, err }), + ) } function samePath(a: string, b: string) { @@ -140,13 +145,10 @@ async function resolveWorktree(name: string, root: string, timeoutMs = 10 * 60_0 // only with `-d` (not `-D`): it refuses unless the branch is fully merged, // so we never silently discard unmerged work from an interrupted worktree // or an unrelated branch that happens to share the name. - const branchRef = await run( - Git.Service.use((git) => - git.run(["show-ref", "--verify", "--quiet", `refs/heads/${slug}`], { cwd: ctx.worktree }), - ), - ) + const runGit = (args: string[]) => run(Git.Service.use((git) => git.run(args, { cwd: ctx.worktree }))) + const branchRef = await runGit(["show-ref", "--verify", "--quiet", `refs/heads/${slug}`]) if (branchRef.exitCode === 0) { - const deleted = await run(Git.Service.use((git) => git.run(["branch", "-d", slug], { cwd: ctx.worktree }))) + const deleted = await runGit(["branch", "-d", slug]) if (deleted.exitCode !== 0) { const message = deleted.stderr.toString("utf8").trim() || deleted.text().trim() throw new Error( diff --git a/packages/opencode/test/event-manifest.test.ts b/packages/opencode/test/event-manifest.test.ts index be22f5648a..99403925aa 100644 --- a/packages/opencode/test/event-manifest.test.ts +++ b/packages/opencode/test/event-manifest.test.ts @@ -9,7 +9,7 @@ describe("public event manifest", () => { expect(EventManifest.Definitions).toBe(SchemaEventManifest.Definitions) expect(EventManifest.Latest).toBe(SchemaEventManifest.Latest) expect(EventManifest.Durable).toBe(SchemaEventManifest.Durable) - expect(EventManifest.Latest.size).toBe(89) // kilocode_change - include global.config.updated + expect(EventManifest.Latest.size).toBe(90) // kilocode_change - include global.config.updated and worktree.setup.ready expect(EventManifest.Latest.get("session.next.step.ended")).toBe(SessionEvent.Step.Ended) expect(EventManifest.Latest.get("todo.updated")).toBe(Todo.Event.Updated) expect(EventManifest.Latest.has("ide.installed")).toBe(false) diff --git a/packages/opencode/test/kilocode/cli/cmd/tui-worktree.test.ts b/packages/opencode/test/kilocode/cli/cmd/tui-worktree.test.ts index b3d8f8f20d..627017ec0f 100644 --- a/packages/opencode/test/kilocode/cli/cmd/tui-worktree.test.ts +++ b/packages/opencode/test/kilocode/cli/cmd/tui-worktree.test.ts @@ -1,7 +1,6 @@ import { describe, expect, test } from "bun:test" import path from "path" -import { mkdtemp, rm } from "fs/promises" -import { tmpdir as osTmpdir } from "os" +import { tmpdir } from "../../../fixture/fixture" import { ensureGitExclude, slugify } from "@/kilocode/cli/cmd/tui-worktree" import { Filesystem } from "@/util/filesystem" @@ -22,46 +21,36 @@ describe("slugify", () => { }) describe("ensureGitExclude", () => { - async function withRepo(fn: (root: string) => Promise) { - const root = await mkdtemp(path.join(osTmpdir(), "tui-worktree-exclude-")) - try { - await fn(root) - } finally { - await rm(root, { recursive: true, force: true }) - } - } + test("appends the exclude entry when the file exists but is empty", async () => { + await using tmp = await tmpdir({ git: true }) + const excludePath = path.join(tmp.path, ".git", "info", "exclude") + await Filesystem.write(excludePath, "") + await ensureGitExclude(tmp.path) + expect(await Filesystem.readText(excludePath)).toContain(".kilo/worktrees/") + }) - test("appends the exclude entry when the file exists but is empty", () => - withRepo(async (root) => { - const excludePath = path.join(root, ".git", "info", "exclude") - await Filesystem.write(excludePath, "") - await ensureGitExclude(root) - const content = await Filesystem.readText(excludePath) - expect(content).toContain(".kilo/worktrees/") - })) + test("preserves existing content and adds a newline before the new entry", async () => { + await using tmp = await tmpdir({ git: true }) + const excludePath = path.join(tmp.path, ".git", "info", "exclude") + await Filesystem.write(excludePath, "*.log") + await ensureGitExclude(tmp.path) + const content = await Filesystem.readText(excludePath) + expect(content).toContain("*.log") + expect(content).toContain(".kilo/worktrees/") + }) - test("preserves existing content and adds a newline before the new entry", () => - withRepo(async (root) => { - const excludePath = path.join(root, ".git", "info", "exclude") - await Filesystem.write(excludePath, "*.log") - await ensureGitExclude(root) - const content = await Filesystem.readText(excludePath) - expect(content).toContain("*.log") - expect(content).toContain(".kilo/worktrees/") - })) + test("is idempotent when the entry already exists", async () => { + await using tmp = await tmpdir({ git: true }) + const excludePath = path.join(tmp.path, ".git", "info", "exclude") + await Filesystem.write(excludePath, "") + await ensureGitExclude(tmp.path) + await ensureGitExclude(tmp.path) + const content = await Filesystem.readText(excludePath) + expect(content.match(/\.kilo\/worktrees\//g)?.length).toBe(1) + }) - test("is idempotent when the entry already exists", () => - withRepo(async (root) => { - const excludePath = path.join(root, ".git", "info", "exclude") - await Filesystem.write(excludePath, "") - await ensureGitExclude(root) - await ensureGitExclude(root) - const content = await Filesystem.readText(excludePath) - expect(content.match(/\.kilo\/worktrees\//g)?.length).toBe(1) - })) - - test("does not throw when .git/info is missing", () => - withRepo(async (root) => { - await ensureGitExclude(root) - })) + test("does not throw when .git/info is missing", async () => { + await using tmp = await tmpdir() + await ensureGitExclude(tmp.path) + }) })