@stonerzju/opencode 1.2.17 → 1.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/bin/opencode +29 -157
  2. package/package.json +29 -29
  3. package/src/acp/agent.ts +4 -4
  4. package/src/acp/session.ts +1 -1
  5. package/src/agent/agent.ts +3 -3
  6. package/src/bun/index.ts +2 -2
  7. package/src/cli/cmd/acp.ts +3 -3
  8. package/src/cli/cmd/debug/file.ts +1 -1
  9. package/src/cli/cmd/github.ts +2 -2
  10. package/src/cli/cmd/pr.ts +1 -1
  11. package/src/cli/cmd/tui/app.tsx +24 -24
  12. package/src/cli/cmd/tui/attach.ts +3 -3
  13. package/src/cli/cmd/tui/component/dialog-agent.tsx +3 -3
  14. package/src/cli/cmd/tui/component/dialog-command.tsx +3 -3
  15. package/src/cli/cmd/tui/component/dialog-mcp.tsx +5 -5
  16. package/src/cli/cmd/tui/component/dialog-model.tsx +4 -4
  17. package/src/cli/cmd/tui/component/dialog-provider.tsx +4 -4
  18. package/src/cli/cmd/tui/component/dialog-session-list.tsx +5 -5
  19. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +3 -3
  20. package/src/cli/cmd/tui/component/dialog-skill.tsx +3 -3
  21. package/src/cli/cmd/tui/component/dialog-stash.tsx +3 -3
  22. package/src/cli/cmd/tui/component/dialog-status.tsx +2 -2
  23. package/src/cli/cmd/tui/component/dialog-tag.tsx +3 -3
  24. package/src/cli/cmd/tui/component/logo.tsx +2 -2
  25. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +6 -6
  26. package/src/cli/cmd/tui/component/prompt/frecency.tsx +2 -2
  27. package/src/cli/cmd/tui/component/prompt/history.tsx +2 -2
  28. package/src/cli/cmd/tui/component/prompt/index.tsx +14 -14
  29. package/src/cli/cmd/tui/component/prompt/stash.tsx +2 -2
  30. package/src/cli/cmd/tui/component/textarea-keybindings.ts +1 -1
  31. package/src/cli/cmd/tui/component/tips.tsx +1 -1
  32. package/src/cli/cmd/tui/context/directory.ts +1 -1
  33. package/src/cli/cmd/tui/context/exit.tsx +1 -1
  34. package/src/cli/cmd/tui/context/keybind.tsx +2 -2
  35. package/src/cli/cmd/tui/context/kv.tsx +2 -2
  36. package/src/cli/cmd/tui/context/local.tsx +6 -6
  37. package/src/cli/cmd/tui/context/sync.tsx +4 -4
  38. package/src/cli/cmd/tui/context/theme/opencode.json +245 -0
  39. package/src/cli/cmd/tui/context/theme.tsx +2 -2
  40. package/src/cli/cmd/tui/context/tui-config.tsx +1 -1
  41. package/src/cli/cmd/tui/event.ts +2 -2
  42. package/src/cli/cmd/tui/routes/home.tsx +6 -6
  43. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +6 -6
  44. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +6 -6
  45. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +2 -2
  46. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +3 -3
  47. package/src/cli/cmd/tui/routes/session/header.tsx +5 -5
  48. package/src/cli/cmd/tui/routes/session/index.tsx +32 -32
  49. package/src/cli/cmd/tui/routes/session/permission.tsx +4 -4
  50. package/src/cli/cmd/tui/routes/session/sidebar.tsx +4 -4
  51. package/src/cli/cmd/tui/thread.ts +9 -9
  52. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +1 -1
  53. package/src/cli/cmd/tui/ui/dialog-help.tsx +2 -2
  54. package/src/cli/cmd/tui/ui/dialog-select.tsx +5 -5
  55. package/src/cli/cmd/tui/ui/dialog.tsx +3 -3
  56. package/src/cli/cmd/tui/ui/toast.tsx +1 -1
  57. package/src/cli/cmd/tui/util/editor.ts +3 -3
  58. package/src/cli/cmd/tui/util/transcript.ts +1 -1
  59. package/src/cli/cmd/tui/worker.ts +10 -10
  60. package/src/cli/error.ts +1 -1
  61. package/src/cli/ui.ts +1 -1
  62. package/src/cli/upgrade.ts +4 -4
  63. package/src/command/index.ts +1 -1
  64. package/src/config/config.ts +10 -10
  65. package/src/config/markdown.ts +1 -1
  66. package/src/config/migrate-tui-config.ts +5 -5
  67. package/src/config/paths.ts +4 -4
  68. package/src/config/tui.ts +4 -4
  69. package/src/control/control.sql.ts +1 -1
  70. package/src/control/index.ts +1 -1
  71. package/src/control-plane/adaptors/worktree.ts +1 -1
  72. package/src/control-plane/session-proxy-middleware.ts +1 -1
  73. package/src/control-plane/workspace.sql.ts +1 -1
  74. package/src/control-plane/workspace.ts +7 -7
  75. package/src/file/index.ts +1 -1
  76. package/src/file/ripgrep.ts +2 -2
  77. package/src/file/watcher.ts +5 -5
  78. package/src/format/formatter.ts +1 -1
  79. package/src/ide/index.ts +3 -3
  80. package/src/index.ts +1 -1
  81. package/src/installation/index.ts +3 -3
  82. package/src/lsp/client.ts +3 -3
  83. package/src/lsp/index.ts +3 -3
  84. package/src/mcp/index.ts +4 -4
  85. package/src/permission/index.ts +2 -2
  86. package/src/permission/next.ts +10 -10
  87. package/src/plugin/codex.ts +1 -1
  88. package/src/plugin/copilot.ts +2 -2
  89. package/src/plugin/index.ts +1 -1
  90. package/src/project/bootstrap.ts +2 -2
  91. package/src/project/instance.ts +4 -4
  92. package/src/project/project.sql.ts +1 -1
  93. package/src/project/project.ts +5 -5
  94. package/src/project/state.ts +1 -1
  95. package/src/project/vcs.ts +4 -4
  96. package/src/provider/auth.ts +4 -4
  97. package/src/provider/error.ts +1 -1
  98. package/src/provider/models-snapshot.ts +2 -0
  99. package/src/provider/models.ts +1 -1
  100. package/src/provider/provider.ts +2 -2
  101. package/src/provider/transform.ts +2 -2
  102. package/src/pty/index.ts +5 -5
  103. package/src/question/index.ts +5 -5
  104. package/src/server/event.ts +1 -1
  105. package/src/server/mdns.ts +1 -1
  106. package/src/server/routes/global.ts +3 -3
  107. package/src/server/routes/permission.ts +1 -1
  108. package/src/server/routes/pty.ts +1 -1
  109. package/src/server/routes/session.ts +4 -4
  110. package/src/server/routes/tui.ts +1 -1
  111. package/src/server/server.ts +3 -3
  112. package/src/session/compaction.ts +7 -7
  113. package/src/session/index.ts +10 -10
  114. package/src/session/instruction.ts +1 -1
  115. package/src/session/llm.ts +11 -11
  116. package/src/session/message-v2.ts +10 -10
  117. package/src/session/message.ts +1 -1
  118. package/src/session/processor.ts +10 -10
  119. package/src/session/prompt.ts +8 -8
  120. package/src/session/retry.ts +2 -2
  121. package/src/session/revert.ts +1 -1
  122. package/src/session/session.sql.ts +3 -3
  123. package/src/session/status.ts +3 -3
  124. package/src/session/summary.ts +5 -5
  125. package/src/session/system.ts +1 -1
  126. package/src/session/todo.ts +2 -2
  127. package/src/share/share-next.ts +7 -7
  128. package/src/share/share.sql.ts +1 -1
  129. package/src/shell/shell.ts +3 -3
  130. package/src/skill/skill.ts +6 -6
  131. package/src/storage/db.ts +1 -1
  132. package/src/storage/storage.ts +1 -1
  133. package/src/tool/bash.ts +6 -6
  134. package/src/tool/edit.ts +1 -1
  135. package/src/tool/registry.ts +2 -2
  136. package/src/tool/skill.ts +1 -1
  137. package/src/tool/task.ts +3 -3
  138. package/src/util/array.ts +10 -0
  139. package/src/util/binary.ts +41 -0
  140. package/src/util/encode.ts +51 -0
  141. package/src/util/error.ts +54 -0
  142. package/src/util/identifier.ts +48 -0
  143. package/src/util/lazy.ts +4 -16
  144. package/src/util/path.ts +37 -0
  145. package/src/util/retry.ts +41 -0
  146. package/src/util/slug.ts +74 -0
  147. package/src/worktree/index.ts +3 -3
  148. package/AGENTS.md +0 -10
  149. package/BUN_SHELL_MIGRATION_PLAN.md +0 -136
  150. package/Dockerfile +0 -18
  151. package/README.md +0 -15
  152. package/bunfig.toml +0 -7
  153. package/drizzle.config.ts +0 -10
  154. package/script/build.ts +0 -224
  155. package/script/check-migrations.ts +0 -16
  156. package/script/postinstall.mjs +0 -131
  157. package/script/publish.ts +0 -181
  158. package/script/schema.ts +0 -63
  159. package/script/seed-e2e.ts +0 -50
  160. package/sst-env.d.ts +0 -10
  161. package/test/AGENTS.md +0 -81
  162. package/test/acp/agent-interface.test.ts +0 -51
  163. package/test/acp/event-subscription.test.ts +0 -683
  164. package/test/agent/agent.test.ts +0 -689
  165. package/test/bun.test.ts +0 -53
  166. package/test/cli/github-action.test.ts +0 -197
  167. package/test/cli/github-remote.test.ts +0 -80
  168. package/test/cli/import.test.ts +0 -38
  169. package/test/cli/plugin-auth-picker.test.ts +0 -120
  170. package/test/cli/tui/transcript.test.ts +0 -322
  171. package/test/config/agent-color.test.ts +0 -71
  172. package/test/config/config.test.ts +0 -1886
  173. package/test/config/fixtures/empty-frontmatter.md +0 -4
  174. package/test/config/fixtures/frontmatter.md +0 -28
  175. package/test/config/fixtures/markdown-header.md +0 -11
  176. package/test/config/fixtures/no-frontmatter.md +0 -1
  177. package/test/config/fixtures/weird-model-id.md +0 -13
  178. package/test/config/markdown.test.ts +0 -228
  179. package/test/config/tui.test.ts +0 -510
  180. package/test/control-plane/session-proxy-middleware.test.ts +0 -147
  181. package/test/control-plane/sse.test.ts +0 -56
  182. package/test/control-plane/workspace-server-sse.test.ts +0 -65
  183. package/test/control-plane/workspace-sync.test.ts +0 -97
  184. package/test/file/ignore.test.ts +0 -10
  185. package/test/file/index.test.ts +0 -394
  186. package/test/file/path-traversal.test.ts +0 -198
  187. package/test/file/ripgrep.test.ts +0 -39
  188. package/test/file/time.test.ts +0 -361
  189. package/test/fixture/db.ts +0 -11
  190. package/test/fixture/fixture.ts +0 -45
  191. package/test/fixture/lsp/fake-lsp-server.js +0 -77
  192. package/test/fixture/skills/agents-sdk/SKILL.md +0 -152
  193. package/test/fixture/skills/agents-sdk/references/callable.md +0 -92
  194. package/test/fixture/skills/cloudflare/SKILL.md +0 -211
  195. package/test/fixture/skills/index.json +0 -6
  196. package/test/ide/ide.test.ts +0 -82
  197. package/test/keybind.test.ts +0 -421
  198. package/test/lsp/client.test.ts +0 -95
  199. package/test/mcp/headers.test.ts +0 -153
  200. package/test/mcp/oauth-browser.test.ts +0 -249
  201. package/test/memory/abort-leak.test.ts +0 -136
  202. package/test/patch/patch.test.ts +0 -348
  203. package/test/permission/arity.test.ts +0 -33
  204. package/test/permission/next.test.ts +0 -689
  205. package/test/permission-task.test.ts +0 -319
  206. package/test/plugin/auth-override.test.ts +0 -44
  207. package/test/plugin/codex.test.ts +0 -123
  208. package/test/preload.ts +0 -80
  209. package/test/project/project.test.ts +0 -348
  210. package/test/project/worktree-remove.test.ts +0 -65
  211. package/test/provider/amazon-bedrock.test.ts +0 -446
  212. package/test/provider/copilot/convert-to-copilot-messages.test.ts +0 -523
  213. package/test/provider/copilot/copilot-chat-model.test.ts +0 -592
  214. package/test/provider/gitlab-duo.test.ts +0 -262
  215. package/test/provider/provider.test.ts +0 -2220
  216. package/test/provider/transform.test.ts +0 -2353
  217. package/test/pty/pty-output-isolation.test.ts +0 -140
  218. package/test/question/question.test.ts +0 -300
  219. package/test/scheduler.test.ts +0 -73
  220. package/test/server/global-session-list.test.ts +0 -89
  221. package/test/server/session-list.test.ts +0 -90
  222. package/test/server/session-select.test.ts +0 -78
  223. package/test/session/compaction.test.ts +0 -423
  224. package/test/session/instruction.test.ts +0 -170
  225. package/test/session/llm.test.ts +0 -667
  226. package/test/session/message-v2.test.ts +0 -924
  227. package/test/session/prompt.test.ts +0 -211
  228. package/test/session/retry.test.ts +0 -188
  229. package/test/session/revert-compact.test.ts +0 -285
  230. package/test/session/session.test.ts +0 -71
  231. package/test/session/structured-output-integration.test.ts +0 -233
  232. package/test/session/structured-output.test.ts +0 -385
  233. package/test/skill/discovery.test.ts +0 -110
  234. package/test/skill/skill.test.ts +0 -388
  235. package/test/snapshot/snapshot.test.ts +0 -1180
  236. package/test/storage/json-migration.test.ts +0 -846
  237. package/test/tool/__snapshots__/tool.test.ts.snap +0 -9
  238. package/test/tool/apply_patch.test.ts +0 -566
  239. package/test/tool/bash.test.ts +0 -402
  240. package/test/tool/edit.test.ts +0 -496
  241. package/test/tool/external-directory.test.ts +0 -127
  242. package/test/tool/fixtures/large-image.png +0 -0
  243. package/test/tool/fixtures/models-api.json +0 -38413
  244. package/test/tool/grep.test.ts +0 -110
  245. package/test/tool/question.test.ts +0 -107
  246. package/test/tool/read.test.ts +0 -504
  247. package/test/tool/registry.test.ts +0 -122
  248. package/test/tool/skill.test.ts +0 -112
  249. package/test/tool/truncation.test.ts +0 -160
  250. package/test/tool/webfetch.test.ts +0 -100
  251. package/test/tool/write.test.ts +0 -348
  252. package/test/util/filesystem.test.ts +0 -443
  253. package/test/util/format.test.ts +0 -59
  254. package/test/util/glob.test.ts +0 -164
  255. package/test/util/iife.test.ts +0 -36
  256. package/test/util/lazy.test.ts +0 -50
  257. package/test/util/lock.test.ts +0 -72
  258. package/test/util/process.test.ts +0 -59
  259. package/test/util/timeout.test.ts +0 -21
  260. package/test/util/wildcard.test.ts +0 -90
  261. package/tsconfig.json +0 -16
@@ -1,65 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test"
2
- import { Log } from "../../src/util/log"
3
- import { WorkspaceServer } from "../../src/control-plane/workspace-server/server"
4
- import { parseSSE } from "../../src/control-plane/sse"
5
- import { GlobalBus } from "../../src/bus/global"
6
- import { resetDatabase } from "../fixture/db"
7
-
8
- afterEach(async () => {
9
- await resetDatabase()
10
- })
11
-
12
- Log.init({ print: false })
13
-
14
- describe("control-plane/workspace-server SSE", () => {
15
- test("streams GlobalBus events and parseSSE reads them", async () => {
16
- const app = WorkspaceServer.App()
17
- const stop = new AbortController()
18
- const seen: unknown[] = []
19
-
20
- try {
21
- const response = await app.request("/event", {
22
- signal: stop.signal,
23
- })
24
-
25
- expect(response.status).toBe(200)
26
- expect(response.body).toBeDefined()
27
-
28
- const done = new Promise<void>((resolve, reject) => {
29
- const timeout = setTimeout(() => {
30
- reject(new Error("timed out waiting for workspace.test event"))
31
- }, 3000)
32
-
33
- void parseSSE(response.body!, stop.signal, (event) => {
34
- seen.push(event)
35
- const next = event as { type?: string }
36
- if (next.type === "server.connected") {
37
- GlobalBus.emit("event", {
38
- payload: {
39
- type: "workspace.test",
40
- properties: { ok: true },
41
- },
42
- })
43
- return
44
- }
45
- if (next.type !== "workspace.test") return
46
- clearTimeout(timeout)
47
- resolve()
48
- }).catch((error) => {
49
- clearTimeout(timeout)
50
- reject(error)
51
- })
52
- })
53
-
54
- await done
55
-
56
- expect(seen.some((event) => (event as { type?: string }).type === "server.connected")).toBe(true)
57
- expect(seen).toContainEqual({
58
- type: "workspace.test",
59
- properties: { ok: true },
60
- })
61
- } finally {
62
- stop.abort()
63
- }
64
- })
65
- })
@@ -1,97 +0,0 @@
1
- import { afterEach, describe, expect, mock, test } from "bun:test"
2
- import { Identifier } from "../../src/id/id"
3
- import { Log } from "../../src/util/log"
4
- import { tmpdir } from "../fixture/fixture"
5
- import { Project } from "../../src/project/project"
6
- import { Database } from "../../src/storage/db"
7
- import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
8
- import { GlobalBus } from "../../src/bus/global"
9
- import { resetDatabase } from "../fixture/db"
10
-
11
- afterEach(async () => {
12
- mock.restore()
13
- await resetDatabase()
14
- })
15
-
16
- Log.init({ print: false })
17
-
18
- const seen: string[] = []
19
- const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert.config
20
-
21
- mock.module("../../src/control-plane/adaptors", () => ({
22
- getAdaptor: (config: { type: string }) => {
23
- seen.push(config.type)
24
- return {
25
- async create() {
26
- throw new Error("not used")
27
- },
28
- async remove() {},
29
- async request() {
30
- const body = new ReadableStream<Uint8Array>({
31
- start(controller) {
32
- const encoder = new TextEncoder()
33
- controller.enqueue(encoder.encode('data: {"type":"remote.ready","properties":{}}\n\n'))
34
- controller.close()
35
- },
36
- })
37
- return new Response(body, {
38
- status: 200,
39
- headers: {
40
- "content-type": "text/event-stream",
41
- },
42
- })
43
- },
44
- }
45
- },
46
- }))
47
-
48
- describe("control-plane/workspace.startSyncing", () => {
49
- test("syncs only remote workspaces and emits remote SSE events", async () => {
50
- const { Workspace } = await import("../../src/control-plane/workspace")
51
- await using tmp = await tmpdir({ git: true })
52
- const { project } = await Project.fromDirectory(tmp.path)
53
-
54
- const id1 = Identifier.descending("workspace")
55
- const id2 = Identifier.descending("workspace")
56
-
57
- Database.use((db) =>
58
- db
59
- .insert(WorkspaceTable)
60
- .values([
61
- {
62
- id: id1,
63
- branch: "main",
64
- project_id: project.id,
65
- config: remote,
66
- },
67
- {
68
- id: id2,
69
- branch: "main",
70
- project_id: project.id,
71
- config: { type: "worktree", directory: tmp.path },
72
- },
73
- ])
74
- .run(),
75
- )
76
-
77
- const done = new Promise<void>((resolve) => {
78
- const listener = (event: { directory?: string; payload: { type: string } }) => {
79
- if (event.directory !== id1) return
80
- if (event.payload.type !== "remote.ready") return
81
- GlobalBus.off("event", listener)
82
- resolve()
83
- }
84
- GlobalBus.on("event", listener)
85
- })
86
-
87
- const sync = Workspace.startSyncing(project)
88
- await Promise.race([
89
- done,
90
- new Promise((_, reject) => setTimeout(() => reject(new Error("timed out waiting for sync event")), 2000)),
91
- ])
92
-
93
- await sync.stop()
94
- expect(seen).toContain("testing")
95
- expect(seen).not.toContain("worktree")
96
- })
97
- })
@@ -1,10 +0,0 @@
1
- import { test, expect } from "bun:test"
2
- import { FileIgnore } from "../../src/file/ignore"
3
-
4
- test("match nested and non-nested", () => {
5
- expect(FileIgnore.match("node_modules/index.js")).toBe(true)
6
- expect(FileIgnore.match("node_modules")).toBe(true)
7
- expect(FileIgnore.match("node_modules/")).toBe(true)
8
- expect(FileIgnore.match("node_modules/bar")).toBe(true)
9
- expect(FileIgnore.match("node_modules/bar/")).toBe(true)
10
- })
@@ -1,394 +0,0 @@
1
- import { describe, test, expect } from "bun:test"
2
- import path from "path"
3
- import fs from "fs/promises"
4
- import { File } from "../../src/file"
5
- import { Instance } from "../../src/project/instance"
6
- import { Filesystem } from "../../src/util/filesystem"
7
- import { tmpdir } from "../fixture/fixture"
8
-
9
- describe("file/index Filesystem patterns", () => {
10
- describe("File.read() - text content", () => {
11
- test("reads text file via Filesystem.readText()", async () => {
12
- await using tmp = await tmpdir()
13
- const filepath = path.join(tmp.path, "test.txt")
14
- await fs.writeFile(filepath, "Hello World", "utf-8")
15
-
16
- await Instance.provide({
17
- directory: tmp.path,
18
- fn: async () => {
19
- const result = await File.read("test.txt")
20
- expect(result.type).toBe("text")
21
- expect(result.content).toBe("Hello World")
22
- },
23
- })
24
- })
25
-
26
- test("reads with Filesystem.exists() check", async () => {
27
- await using tmp = await tmpdir()
28
-
29
- await Instance.provide({
30
- directory: tmp.path,
31
- fn: async () => {
32
- // Non-existent file should return empty content
33
- const result = await File.read("nonexistent.txt")
34
- expect(result.type).toBe("text")
35
- expect(result.content).toBe("")
36
- },
37
- })
38
- })
39
-
40
- test("trims whitespace from text content", async () => {
41
- await using tmp = await tmpdir()
42
- const filepath = path.join(tmp.path, "test.txt")
43
- await fs.writeFile(filepath, " content with spaces \n\n", "utf-8")
44
-
45
- await Instance.provide({
46
- directory: tmp.path,
47
- fn: async () => {
48
- const result = await File.read("test.txt")
49
- expect(result.content).toBe("content with spaces")
50
- },
51
- })
52
- })
53
-
54
- test("handles empty text file", async () => {
55
- await using tmp = await tmpdir()
56
- const filepath = path.join(tmp.path, "empty.txt")
57
- await fs.writeFile(filepath, "", "utf-8")
58
-
59
- await Instance.provide({
60
- directory: tmp.path,
61
- fn: async () => {
62
- const result = await File.read("empty.txt")
63
- expect(result.type).toBe("text")
64
- expect(result.content).toBe("")
65
- },
66
- })
67
- })
68
-
69
- test("handles multi-line text files", async () => {
70
- await using tmp = await tmpdir()
71
- const filepath = path.join(tmp.path, "multiline.txt")
72
- await fs.writeFile(filepath, "line1\nline2\nline3", "utf-8")
73
-
74
- await Instance.provide({
75
- directory: tmp.path,
76
- fn: async () => {
77
- const result = await File.read("multiline.txt")
78
- expect(result.content).toBe("line1\nline2\nline3")
79
- },
80
- })
81
- })
82
- })
83
-
84
- describe("File.read() - binary content", () => {
85
- test("reads binary file via Filesystem.readArrayBuffer()", async () => {
86
- await using tmp = await tmpdir()
87
- const filepath = path.join(tmp.path, "image.png")
88
- const binaryContent = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
89
- await fs.writeFile(filepath, binaryContent)
90
-
91
- await Instance.provide({
92
- directory: tmp.path,
93
- fn: async () => {
94
- const result = await File.read("image.png")
95
- expect(result.type).toBe("text") // Images return as text with base64 encoding
96
- expect(result.encoding).toBe("base64")
97
- expect(result.mimeType).toBe("image/png")
98
- expect(result.content).toBe(binaryContent.toString("base64"))
99
- },
100
- })
101
- })
102
-
103
- test("returns empty for binary non-image files", async () => {
104
- await using tmp = await tmpdir()
105
- const filepath = path.join(tmp.path, "binary.so")
106
- await fs.writeFile(filepath, Buffer.from([0x7f, 0x45, 0x4c, 0x46]), "binary")
107
-
108
- await Instance.provide({
109
- directory: tmp.path,
110
- fn: async () => {
111
- const result = await File.read("binary.so")
112
- expect(result.type).toBe("binary")
113
- expect(result.content).toBe("")
114
- },
115
- })
116
- })
117
- })
118
-
119
- describe("File.read() - Filesystem.mimeType()", () => {
120
- test("detects MIME type via Filesystem.mimeType()", async () => {
121
- await using tmp = await tmpdir()
122
- const filepath = path.join(tmp.path, "test.json")
123
- await fs.writeFile(filepath, '{"key": "value"}', "utf-8")
124
-
125
- await Instance.provide({
126
- directory: tmp.path,
127
- fn: async () => {
128
- expect(Filesystem.mimeType(filepath)).toContain("application/json")
129
-
130
- const result = await File.read("test.json")
131
- expect(result.type).toBe("text")
132
- },
133
- })
134
- })
135
-
136
- test("handles various image MIME types", async () => {
137
- await using tmp = await tmpdir()
138
- const testCases = [
139
- { ext: "jpg", mime: "image/jpeg" },
140
- { ext: "png", mime: "image/png" },
141
- { ext: "gif", mime: "image/gif" },
142
- { ext: "webp", mime: "image/webp" },
143
- ]
144
-
145
- for (const { ext, mime } of testCases) {
146
- const filepath = path.join(tmp.path, `test.${ext}`)
147
- await fs.writeFile(filepath, Buffer.from([0x00, 0x00, 0x00, 0x00]), "binary")
148
-
149
- await Instance.provide({
150
- directory: tmp.path,
151
- fn: async () => {
152
- expect(Filesystem.mimeType(filepath)).toContain(mime)
153
- },
154
- })
155
- }
156
- })
157
- })
158
-
159
- describe("File.list() - Filesystem.exists() and readText()", () => {
160
- test("reads .gitignore via Filesystem.exists() and readText()", async () => {
161
- await using tmp = await tmpdir({ git: true })
162
-
163
- await Instance.provide({
164
- directory: tmp.path,
165
- fn: async () => {
166
- const gitignorePath = path.join(tmp.path, ".gitignore")
167
- await fs.writeFile(gitignorePath, "node_modules\ndist\n", "utf-8")
168
-
169
- // This is used internally in File.list()
170
- expect(await Filesystem.exists(gitignorePath)).toBe(true)
171
-
172
- const content = await Filesystem.readText(gitignorePath)
173
- expect(content).toContain("node_modules")
174
- },
175
- })
176
- })
177
-
178
- test("reads .ignore file similarly", async () => {
179
- await using tmp = await tmpdir({ git: true })
180
-
181
- await Instance.provide({
182
- directory: tmp.path,
183
- fn: async () => {
184
- const ignorePath = path.join(tmp.path, ".ignore")
185
- await fs.writeFile(ignorePath, "*.log\n.env\n", "utf-8")
186
-
187
- expect(await Filesystem.exists(ignorePath)).toBe(true)
188
- expect(await Filesystem.readText(ignorePath)).toContain("*.log")
189
- },
190
- })
191
- })
192
-
193
- test("handles missing .gitignore gracefully", async () => {
194
- await using tmp = await tmpdir({ git: true })
195
-
196
- await Instance.provide({
197
- directory: tmp.path,
198
- fn: async () => {
199
- const gitignorePath = path.join(tmp.path, ".gitignore")
200
- expect(await Filesystem.exists(gitignorePath)).toBe(false)
201
-
202
- // File.list() should still work
203
- const nodes = await File.list()
204
- expect(Array.isArray(nodes)).toBe(true)
205
- },
206
- })
207
- })
208
- })
209
-
210
- describe("File.changed() - Filesystem.readText() for untracked files", () => {
211
- test("reads untracked files via Filesystem.readText()", async () => {
212
- await using tmp = await tmpdir({ git: true })
213
-
214
- await Instance.provide({
215
- directory: tmp.path,
216
- fn: async () => {
217
- const untrackedPath = path.join(tmp.path, "untracked.txt")
218
- await fs.writeFile(untrackedPath, "new content\nwith multiple lines", "utf-8")
219
-
220
- // This is how File.changed() reads untracked files
221
- const content = await Filesystem.readText(untrackedPath)
222
- const lines = content.split("\n").length
223
- expect(lines).toBe(2)
224
- },
225
- })
226
- })
227
- })
228
-
229
- describe("Error handling", () => {
230
- test("handles errors gracefully in Filesystem.readText()", async () => {
231
- await using tmp = await tmpdir()
232
- const filepath = path.join(tmp.path, "readonly.txt")
233
- await fs.writeFile(filepath, "content", "utf-8")
234
-
235
- await Instance.provide({
236
- directory: tmp.path,
237
- fn: async () => {
238
- const nonExistentPath = path.join(tmp.path, "does-not-exist.txt")
239
- // Filesystem.readText() on non-existent file throws
240
- await expect(Filesystem.readText(nonExistentPath)).rejects.toThrow()
241
-
242
- // But File.read() handles this gracefully
243
- const result = await File.read("does-not-exist.txt")
244
- expect(result.content).toBe("")
245
- },
246
- })
247
- })
248
-
249
- test("handles errors in Filesystem.readArrayBuffer()", async () => {
250
- await using tmp = await tmpdir()
251
-
252
- await Instance.provide({
253
- directory: tmp.path,
254
- fn: async () => {
255
- const nonExistentPath = path.join(tmp.path, "does-not-exist.bin")
256
- const buffer = await Filesystem.readArrayBuffer(nonExistentPath).catch(() => new ArrayBuffer(0))
257
- expect(buffer.byteLength).toBe(0)
258
- },
259
- })
260
- })
261
-
262
- test("returns empty array buffer on error for images", async () => {
263
- await using tmp = await tmpdir()
264
- const filepath = path.join(tmp.path, "broken.png")
265
- // Don't create the file
266
-
267
- await Instance.provide({
268
- directory: tmp.path,
269
- fn: async () => {
270
- // File.read() handles missing images gracefully
271
- const result = await File.read("broken.png")
272
- expect(result.type).toBe("text")
273
- expect(result.content).toBe("")
274
- },
275
- })
276
- })
277
- })
278
-
279
- describe("shouldEncode() logic", () => {
280
- test("treats .ts files as text", async () => {
281
- await using tmp = await tmpdir()
282
- const filepath = path.join(tmp.path, "test.ts")
283
- await fs.writeFile(filepath, "export const value = 1", "utf-8")
284
-
285
- await Instance.provide({
286
- directory: tmp.path,
287
- fn: async () => {
288
- const result = await File.read("test.ts")
289
- expect(result.type).toBe("text")
290
- expect(result.content).toBe("export const value = 1")
291
- },
292
- })
293
- })
294
-
295
- test("treats .mts files as text", async () => {
296
- await using tmp = await tmpdir()
297
- const filepath = path.join(tmp.path, "test.mts")
298
- await fs.writeFile(filepath, "export const value = 1", "utf-8")
299
-
300
- await Instance.provide({
301
- directory: tmp.path,
302
- fn: async () => {
303
- const result = await File.read("test.mts")
304
- expect(result.type).toBe("text")
305
- expect(result.content).toBe("export const value = 1")
306
- },
307
- })
308
- })
309
-
310
- test("treats .sh files as text", async () => {
311
- await using tmp = await tmpdir()
312
- const filepath = path.join(tmp.path, "test.sh")
313
- await fs.writeFile(filepath, "#!/usr/bin/env bash\necho hello", "utf-8")
314
-
315
- await Instance.provide({
316
- directory: tmp.path,
317
- fn: async () => {
318
- const result = await File.read("test.sh")
319
- expect(result.type).toBe("text")
320
- expect(result.content).toBe("#!/usr/bin/env bash\necho hello")
321
- },
322
- })
323
- })
324
-
325
- test("treats Dockerfile as text", async () => {
326
- await using tmp = await tmpdir()
327
- const filepath = path.join(tmp.path, "Dockerfile")
328
- await fs.writeFile(filepath, "FROM alpine:3.20", "utf-8")
329
-
330
- await Instance.provide({
331
- directory: tmp.path,
332
- fn: async () => {
333
- const result = await File.read("Dockerfile")
334
- expect(result.type).toBe("text")
335
- expect(result.content).toBe("FROM alpine:3.20")
336
- },
337
- })
338
- })
339
-
340
- test("returns encoding info for text files", async () => {
341
- await using tmp = await tmpdir()
342
- const filepath = path.join(tmp.path, "test.txt")
343
- await fs.writeFile(filepath, "simple text", "utf-8")
344
-
345
- await Instance.provide({
346
- directory: tmp.path,
347
- fn: async () => {
348
- const result = await File.read("test.txt")
349
- expect(result.encoding).toBeUndefined()
350
- expect(result.type).toBe("text")
351
- },
352
- })
353
- })
354
-
355
- test("returns base64 encoding for images", async () => {
356
- await using tmp = await tmpdir()
357
- const filepath = path.join(tmp.path, "test.jpg")
358
- await fs.writeFile(filepath, Buffer.from([0xff, 0xd8, 0xff, 0xe0]), "binary")
359
-
360
- await Instance.provide({
361
- directory: tmp.path,
362
- fn: async () => {
363
- const result = await File.read("test.jpg")
364
- expect(result.encoding).toBe("base64")
365
- expect(result.mimeType).toBe("image/jpeg")
366
- },
367
- })
368
- })
369
- })
370
-
371
- describe("Path security", () => {
372
- test("throws for paths outside project directory", async () => {
373
- await using tmp = await tmpdir()
374
-
375
- await Instance.provide({
376
- directory: tmp.path,
377
- fn: async () => {
378
- await expect(File.read("../outside.txt")).rejects.toThrow("Access denied")
379
- },
380
- })
381
- })
382
-
383
- test("throws for paths outside project directory", async () => {
384
- await using tmp = await tmpdir()
385
-
386
- await Instance.provide({
387
- directory: tmp.path,
388
- fn: async () => {
389
- await expect(File.read("../outside.txt")).rejects.toThrow("Access denied")
390
- },
391
- })
392
- })
393
- })
394
- })