diff --git a/bun.lock b/bun.lock index ca57250eb..60a5b303c 100644 --- a/bun.lock +++ b/bun.lock @@ -207,10 +207,8 @@ "partial-json": "0.1.7", "remeda": "catalog:", "solid-js": "catalog:", - "tree-sitter": "0.22.4", - "tree-sitter-bash": "0.23.3", - "tree-sitter-highlight": "1.0.1", - "tree-sitter-typescript": "0.23.2", + "tree-sitter": "0.25.0", + "tree-sitter-bash": "0.25.0", "turndown": "7.2.0", "ulid": "catalog:", "vscode-jsonrpc": "8.2.1", @@ -3308,15 +3306,9 @@ "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - "tree-sitter": ["tree-sitter@0.22.4", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg=="], + "tree-sitter": ["tree-sitter@0.25.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-PGZZzFW63eElZJDe/b/R/LbsjDDYJa5UEjLZJB59RQsMX+fo0j54fqBPn1MGKav/QNa0JR0zBiVaikYDWCj5KQ=="], - "tree-sitter-bash": ["tree-sitter-bash@0.23.3", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.21.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-36cg/GQ2YmIbeiBeqeuh4fBJ6i4kgVouDaqTxqih5ysPag+zHufyIaxMOFeM8CeplwAK/Luj1o5XHqgdAfoCZg=="], - - "tree-sitter-highlight": ["tree-sitter-highlight@1.0.1", "", {}, "sha512-PKoUNRI++rVXLsiYP5s3Ro5TnamZ0+7GMIzTGiga0i4YsyWlleei9cdBjX1GWCSAreOw+fnhqCQF5bHChnY8KQ=="], - - "tree-sitter-javascript": ["tree-sitter-javascript@0.23.1", "", { "dependencies": { "node-addon-api": "^8.2.2", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.21.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-/bnhbrTD9frUYHQTiYnPcxyHORIw157ERBa6dqzaKxvR/x3PC4Yzd+D1pZIMS6zNg2v3a8BZ0oK7jHqsQo9fWA=="], - - "tree-sitter-typescript": ["tree-sitter-typescript@0.23.2", "", { "dependencies": { "node-addon-api": "^8.2.2", "node-gyp-build": "^4.8.2", "tree-sitter-javascript": "^0.23.1" }, "peerDependencies": { "tree-sitter": "^0.21.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-e04JUUKxTT53/x3Uq1zIL45DoYKVfHH4CZqwgZhPg5qYROl5nQjV+85ruFzFGZxu+QeFVbRTPDRnqL9UbU4VeA=="], + "tree-sitter-bash": ["tree-sitter-bash@0.25.0", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-gZtlj9+qFS81qKxpLfD6H0UssQ3QBc/F0nKkPsiFDyfQF2YBqYvglFJUzchrPpVhZe9kLZTrJ9n2J6lmka69Vg=="], "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], @@ -3722,6 +3714,8 @@ "@opencode-ai/web/shiki": ["shiki@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/engine-javascript": "3.4.2", "@shikijs/engine-oniguruma": "3.4.2", "@shikijs/langs": "3.4.2", "@shikijs/themes": "3.4.2", "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-wuxzZzQG8kvZndD7nustrNFIKYJ1jJoWIPaBpVe2+KHSvtzMi4SBjOxrigs8qeqce/l3U0cwiC+VAkLKSunHQQ=="], + "@opentui/core/web-tree-sitter": ["web-tree-sitter@0.26.0", "", {}, "sha512-wGGAMnJEMF8wy33iEGxSvnyEOfVLzSaa3x6g66aEHsL/hsgFb6IVPrpacIordAMz198pE9qReCEqFUuM0pnfwg=="], + "@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], "@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.9", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.1" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.8" }, "optionalPeers": ["solid-js"] }, "sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw=="], @@ -4072,10 +4066,6 @@ "tree-sitter-bash/node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], - "tree-sitter-javascript/node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], - - "tree-sitter-typescript/node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], - "tw-to-css/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "tw-to-css/tailwindcss": ["tailwindcss@3.3.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.18.2", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 3d60d6f15..30f9e4053 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -77,10 +77,8 @@ "partial-json": "0.1.7", "remeda": "catalog:", "solid-js": "catalog:", - "tree-sitter": "0.22.4", - "tree-sitter-bash": "0.23.3", - "tree-sitter-highlight": "1.0.1", - "tree-sitter-typescript": "0.23.2", + "tree-sitter": "0.25.0", + "tree-sitter-bash": "0.25.0", "turndown": "7.2.0", "ulid": "catalog:", "vscode-jsonrpc": "8.2.1", diff --git a/packages/opencode/src/cli/cmd/tui/event.ts b/packages/opencode/src/cli/cmd/tui/event.ts index d5b2d7860..ad48ebc6a 100644 --- a/packages/opencode/src/cli/cmd/tui/event.ts +++ b/packages/opencode/src/cli/cmd/tui/event.ts @@ -28,8 +28,5 @@ export const TuiEvent = { ]), }), ), - ToastShow: Bus.event( - "tui.toast.show", - ToastSchema, - ), + ToastShow: Bus.event("tui.toast.show", ToastSchema), } diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 5c06162e8..e0552aa1e 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -1,6 +1,12 @@ import { Log } from "../util/log" import { Bus } from "../bus" -import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi" +import { + describeRoute, + generateSpecs, + validator, + resolver, + openAPIRouteHandler, +} from "hono-openapi" import { Hono } from "hono" import { cors } from "hono/cors" import { stream, streamSSE } from "hono/streaming" @@ -35,7 +41,7 @@ import { InstanceBootstrap } from "../project/bootstrap" import { MCP } from "../mcp" import { Storage } from "../storage/storage" import type { ContentfulStatusCode } from "hono/utils/http-status" -import { TuiEvent } from "@/cli/cmd/tui/event" +import { type TuiEvent } from "@/cli/cmd/tui/event" import { Snapshot } from "@/snapshot" import { SessionSummary } from "@/session/summary" @@ -249,7 +255,9 @@ export namespace Server { id: t.id, description: t.description, // Handle both Zod schemas and plain JSON schemas - parameters: (t.parameters as any)?._def ? zodToJsonSchema(t.parameters as any) : t.parameters, + parameters: (t.parameters as any)?._def + ? zodToJsonSchema(t.parameters as any) + : t.parameters, })), ) }, @@ -1038,7 +1046,10 @@ export namespace Server { const providers = await Provider.list().then((x) => mapValues(x, (item) => item.info)) return c.json({ providers: Object.values(providers), - default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id), + default: mapValues( + providers, + (item) => Provider.sort(Object.values(item.models))[0].id, + ), }) }, ) @@ -1579,7 +1590,10 @@ export namespace Server { ), async (c) => { const evt = c.req.valid("json") - await Bus.publish(Object.values(TuiEvent).find((def) => def.type === evt.type)!, evt.properties) + await Bus.publish( + Object.values(TuiEvent).find((def) => def.type === evt.type)!, + evt.properties, + ) return c.json(true) }, ) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 212615194..98b316804 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -56,7 +56,8 @@ export namespace Snapshot { .trim() .split("\n") .map((x) => x.trim()) - .filter(Boolean), + .filter(Boolean) + .map((x) => path.join(Instance.worktree, x)), } } @@ -101,8 +102,7 @@ export namespace Snapshot { }) } else { log.info("file did not exist in snapshot, deleting", { file }) - const absolutePath = path.join(Instance.worktree, file) - await fs.unlink(absolutePath).catch(() => {}) + await fs.unlink(file).catch(() => {}) } } files.add(file) diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index a7778a747..3b27c27fc 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -4,6 +4,13 @@ import { Tool } from "./tool" import DESCRIPTION from "./bash.txt" import { Log } from "../util/log" import { Instance } from "../project/instance" +import { lazy } from "@/util/lazy" +import { Language } from "web-tree-sitter" +import { Agent } from "@/agent/agent" +import { $ } from "bun" +import { Filesystem } from "@/util/filesystem" +import { Wildcard } from "@/util/wildcard" +import { Permission } from "@/permission" const MAX_OUTPUT_LENGTH = 30_000 const DEFAULT_TIMEOUT = 1 * 60 * 1000 @@ -12,34 +19,24 @@ const SIGKILL_TIMEOUT_MS = 200 export const log = Log.create({ service: "bash-tool" }) -/* const parser = lazy(async () => { - try { - const { default: Parser } = await import("tree-sitter") - const Bash = await import("tree-sitter-bash") - const p = new Parser() - p.setLanguage(Bash.language as any) - return p - } catch (e) { - const { default: Parser } = await import("web-tree-sitter") - const { default: treeWasm } = await import("web-tree-sitter/tree-sitter.wasm" as string, { - with: { type: "wasm" }, - }) - await Parser.init({ - locateFile() { - return treeWasm - }, - }) - const { default: bashWasm } = await import("tree-sitter-bash/tree-sitter-bash.wasm" as string, { - with: { type: "wasm" }, - }) - const bashLanguage = await Language.load(bashWasm) - const p = new Parser() - p.setLanguage(bashLanguage) - return p - } + const { Parser } = await import("web-tree-sitter") + const { default: treeWasm } = await import("web-tree-sitter/tree-sitter.wasm" as string, { + with: { type: "wasm" }, + }) + await Parser.init({ + locateFile() { + return treeWasm + }, + }) + const { default: bashWasm } = await import("tree-sitter-bash/tree-sitter-bash.wasm" as string, { + with: { type: "wasm" }, + }) + const bashLanguage = await Language.load(bashWasm) + const p = new Parser() + p.setLanguage(bashLanguage) + return p }) -*/ export const BashTool = Tool.define("bash", { description: DESCRIPTION, @@ -59,7 +56,6 @@ export const BashTool = Tool.define("bash", { ) } const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT) - /* const tree = await parser().then((p) => p.parse(params.command)) if (!tree) { throw new Error("Failed to parse command") @@ -143,7 +139,6 @@ export const BashTool = Tool.define("bash", { }, }) } - */ const proc = spawn(params.command, { shell: true, diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index 9506c7df1..b72717cd1 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -126,7 +126,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(`image.png`) + expect(patch.files).toContain(`${tmp.path}/image.png`) await Snapshot.revert([patch]) expect(await Bun.file(`${tmp.path}/image.png`).exists()).toBe(false) @@ -195,9 +195,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(`file with spaces.txt`) - expect(files).toContain(`file-with-dashes.txt`) - expect(files).toContain(`file_with_underscores.txt`) + 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`) }, }) }) @@ -249,7 +249,7 @@ test("revert non-existent file", async () => { Snapshot.revert([ { hash: before!, - files: [`nonexistent.txt`], + files: [`${tmp.path}/nonexistent.txt`], }, ]), ).resolves.toBeUndefined() @@ -301,7 +301,7 @@ test("very long filenames", async () => { await Bun.write(longFile, "long filename content") const patch = await Snapshot.patch(before!) - expect(patch.files).toContain(longName) + expect(patch.files).toContain(longFile) await Snapshot.revert([patch]) expect(await Bun.file(longFile).exists()).toBe(false) @@ -322,9 +322,9 @@ test("hidden files", async () => { await Bun.write(`${tmp.path}/.config`, "config content") const patch = await Snapshot.patch(before!) - expect(patch.files).toContain(`.hidden`) - expect(patch.files).toContain(`.gitignore`) - expect(patch.files).toContain(`.config`) + expect(patch.files).toContain(`${tmp.path}/.hidden`) + expect(patch.files).toContain(`${tmp.path}/.gitignore`) + expect(patch.files).toContain(`${tmp.path}/.config`) }, }) }) @@ -343,8 +343,8 @@ 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(`sub/dir/link.txt`) - expect(patch.files).toContain(`sub-link`) + expect(patch.files).toContain(`${tmp.path}/sub/dir/link.txt`) + expect(patch.files).toContain(`${tmp.path}/sub-link`) }, }) }) @@ -402,11 +402,11 @@ test("gitignore changes", async () => { const patch = await Snapshot.patch(before!) // Should track gitignore itself - expect(patch.files).toContain(`.gitignore`) + expect(patch.files).toContain(`${tmp.path}/.gitignore`) // Should track normal files - expect(patch.files).toContain(`normal.txt`) + expect(patch.files).toContain(`${tmp.path}/normal.txt`) // Should not track ignored files (git won't see them) - expect(patch.files).not.toContain(`test.ignored`) + expect(patch.files).not.toContain(`${tmp.path}/test.ignored`) }, }) }) @@ -451,7 +451,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(`project1.txt`) + expect(patch1.files).toContain(`${tmp1.path}/project1.txt`) }, }) @@ -461,10 +461,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(`project2.txt`) + expect(patch2.files).toContain(`${tmp2.path}/project2.txt`) // Ensure project1 files don't appear in project2 - expect(patch2.files).not.toContain(`project1.txt`) + expect(patch2.files).not.toContain(`${tmp1?.path}/project1.txt`) }, }) }) @@ -549,7 +549,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(`a.txt`) + expect(patch.files).toContain(`${tmp.path}/a.txt`) await Snapshot.revert([patch]) @@ -573,8 +573,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(`existing.txt`) - expect(patch.files).toContain(`newfile.txt`) + expect(patch.files).toContain(`${tmp.path}/existing.txt`) + expect(patch.files).toContain(`${tmp.path}/newfile.txt`) await Snapshot.revert([patch]) diff --git a/packages/opencode/test/tool/patch.test.ts b/packages/opencode/test/tool/patch.test.ts index 649119dce..a34d7718d 100644 --- a/packages/opencode/test/tool/patch.test.ts +++ b/packages/opencode/test/tool/patch.test.ts @@ -21,7 +21,9 @@ describe("tool.patch", () => { await Instance.provide({ directory: "/tmp", fn: async () => { - await expect(patchTool.execute({ patchText: "" }, ctx)).rejects.toThrow("patchText is required") + await expect(patchTool.execute({ patchText: "" }, ctx)).rejects.toThrow( + "patchText is required", + ) }, }) }) @@ -30,7 +32,9 @@ describe("tool.patch", () => { await Instance.provide({ directory: "/tmp", fn: async () => { - await expect(patchTool.execute({ patchText: "invalid patch" }, ctx)).rejects.toThrow("Failed to parse patch") + await expect(patchTool.execute({ patchText: "invalid patch" }, ctx)).rejects.toThrow( + "Failed to parse patch", + ) }, }) }) @@ -113,7 +117,9 @@ describe("tool.patch", () => { // Verify file was created with correct content const filePath = path.join(fixture.path, "config.js") const content = await fs.readFile(filePath, "utf-8") - expect(content).toBe('const API_KEY = "test-key"\nconst DEBUG = false\nconst VERSION = "1.0"') + expect(content).toBe( + 'const API_KEY = "test-key"\nconst DEBUG = false\nconst VERSION = "1.0"', + ) }, }) })