@stonerzju/opencode 1.2.16-offline.1 → 1.2.18

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 (262) 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/package.json.bak +0 -140
  155. package/script/build.ts +0 -224
  156. package/script/check-migrations.ts +0 -16
  157. package/script/postinstall.mjs +0 -131
  158. package/script/publish.ts +0 -181
  159. package/script/schema.ts +0 -63
  160. package/script/seed-e2e.ts +0 -50
  161. package/sst-env.d.ts +0 -10
  162. package/test/AGENTS.md +0 -81
  163. package/test/acp/agent-interface.test.ts +0 -51
  164. package/test/acp/event-subscription.test.ts +0 -683
  165. package/test/agent/agent.test.ts +0 -689
  166. package/test/bun.test.ts +0 -53
  167. package/test/cli/github-action.test.ts +0 -197
  168. package/test/cli/github-remote.test.ts +0 -80
  169. package/test/cli/import.test.ts +0 -38
  170. package/test/cli/plugin-auth-picker.test.ts +0 -120
  171. package/test/cli/tui/transcript.test.ts +0 -322
  172. package/test/config/agent-color.test.ts +0 -71
  173. package/test/config/config.test.ts +0 -1886
  174. package/test/config/fixtures/empty-frontmatter.md +0 -4
  175. package/test/config/fixtures/frontmatter.md +0 -28
  176. package/test/config/fixtures/markdown-header.md +0 -11
  177. package/test/config/fixtures/no-frontmatter.md +0 -1
  178. package/test/config/fixtures/weird-model-id.md +0 -13
  179. package/test/config/markdown.test.ts +0 -228
  180. package/test/config/tui.test.ts +0 -510
  181. package/test/control-plane/session-proxy-middleware.test.ts +0 -147
  182. package/test/control-plane/sse.test.ts +0 -56
  183. package/test/control-plane/workspace-server-sse.test.ts +0 -65
  184. package/test/control-plane/workspace-sync.test.ts +0 -97
  185. package/test/file/ignore.test.ts +0 -10
  186. package/test/file/index.test.ts +0 -394
  187. package/test/file/path-traversal.test.ts +0 -198
  188. package/test/file/ripgrep.test.ts +0 -39
  189. package/test/file/time.test.ts +0 -361
  190. package/test/fixture/db.ts +0 -11
  191. package/test/fixture/fixture.ts +0 -45
  192. package/test/fixture/lsp/fake-lsp-server.js +0 -77
  193. package/test/fixture/skills/agents-sdk/SKILL.md +0 -152
  194. package/test/fixture/skills/agents-sdk/references/callable.md +0 -92
  195. package/test/fixture/skills/cloudflare/SKILL.md +0 -211
  196. package/test/fixture/skills/index.json +0 -6
  197. package/test/ide/ide.test.ts +0 -82
  198. package/test/keybind.test.ts +0 -421
  199. package/test/lsp/client.test.ts +0 -95
  200. package/test/mcp/headers.test.ts +0 -153
  201. package/test/mcp/oauth-browser.test.ts +0 -249
  202. package/test/memory/abort-leak.test.ts +0 -136
  203. package/test/patch/patch.test.ts +0 -348
  204. package/test/permission/arity.test.ts +0 -33
  205. package/test/permission/next.test.ts +0 -689
  206. package/test/permission-task.test.ts +0 -319
  207. package/test/plugin/auth-override.test.ts +0 -44
  208. package/test/plugin/codex.test.ts +0 -123
  209. package/test/preload.ts +0 -80
  210. package/test/project/project.test.ts +0 -348
  211. package/test/project/worktree-remove.test.ts +0 -65
  212. package/test/provider/amazon-bedrock.test.ts +0 -446
  213. package/test/provider/copilot/convert-to-copilot-messages.test.ts +0 -523
  214. package/test/provider/copilot/copilot-chat-model.test.ts +0 -592
  215. package/test/provider/gitlab-duo.test.ts +0 -262
  216. package/test/provider/provider.test.ts +0 -2220
  217. package/test/provider/transform.test.ts +0 -2353
  218. package/test/pty/pty-output-isolation.test.ts +0 -140
  219. package/test/question/question.test.ts +0 -300
  220. package/test/scheduler.test.ts +0 -73
  221. package/test/server/global-session-list.test.ts +0 -89
  222. package/test/server/session-list.test.ts +0 -90
  223. package/test/server/session-select.test.ts +0 -78
  224. package/test/session/compaction.test.ts +0 -423
  225. package/test/session/instruction.test.ts +0 -170
  226. package/test/session/llm.test.ts +0 -667
  227. package/test/session/message-v2.test.ts +0 -924
  228. package/test/session/prompt.test.ts +0 -211
  229. package/test/session/retry.test.ts +0 -188
  230. package/test/session/revert-compact.test.ts +0 -285
  231. package/test/session/session.test.ts +0 -71
  232. package/test/session/structured-output-integration.test.ts +0 -233
  233. package/test/session/structured-output.test.ts +0 -385
  234. package/test/skill/discovery.test.ts +0 -110
  235. package/test/skill/skill.test.ts +0 -388
  236. package/test/snapshot/snapshot.test.ts +0 -1180
  237. package/test/storage/json-migration.test.ts +0 -846
  238. package/test/tool/__snapshots__/tool.test.ts.snap +0 -9
  239. package/test/tool/apply_patch.test.ts +0 -566
  240. package/test/tool/bash.test.ts +0 -402
  241. package/test/tool/edit.test.ts +0 -496
  242. package/test/tool/external-directory.test.ts +0 -127
  243. package/test/tool/fixtures/large-image.png +0 -0
  244. package/test/tool/fixtures/models-api.json +0 -38413
  245. package/test/tool/grep.test.ts +0 -110
  246. package/test/tool/question.test.ts +0 -107
  247. package/test/tool/read.test.ts +0 -504
  248. package/test/tool/registry.test.ts +0 -122
  249. package/test/tool/skill.test.ts +0 -112
  250. package/test/tool/truncation.test.ts +0 -160
  251. package/test/tool/webfetch.test.ts +0 -100
  252. package/test/tool/write.test.ts +0 -348
  253. package/test/util/filesystem.test.ts +0 -443
  254. package/test/util/format.test.ts +0 -59
  255. package/test/util/glob.test.ts +0 -164
  256. package/test/util/iife.test.ts +0 -36
  257. package/test/util/lazy.test.ts +0 -50
  258. package/test/util/lock.test.ts +0 -72
  259. package/test/util/process.test.ts +0 -59
  260. package/test/util/timeout.test.ts +0 -21
  261. package/test/util/wildcard.test.ts +0 -90
  262. package/tsconfig.json +0 -16
@@ -1,443 +0,0 @@
1
- import { describe, test, expect } from "bun:test"
2
- import path from "path"
3
- import fs from "fs/promises"
4
- import { Filesystem } from "../../src/util/filesystem"
5
- import { tmpdir } from "../fixture/fixture"
6
-
7
- describe("filesystem", () => {
8
- describe("exists()", () => {
9
- test("returns true for existing file", async () => {
10
- await using tmp = await tmpdir()
11
- const filepath = path.join(tmp.path, "test.txt")
12
- await fs.writeFile(filepath, "content", "utf-8")
13
-
14
- expect(await Filesystem.exists(filepath)).toBe(true)
15
- })
16
-
17
- test("returns false for non-existent file", async () => {
18
- await using tmp = await tmpdir()
19
- const filepath = path.join(tmp.path, "does-not-exist.txt")
20
-
21
- expect(await Filesystem.exists(filepath)).toBe(false)
22
- })
23
-
24
- test("returns true for existing directory", async () => {
25
- await using tmp = await tmpdir()
26
- const dirpath = path.join(tmp.path, "subdir")
27
- await fs.mkdir(dirpath)
28
-
29
- expect(await Filesystem.exists(dirpath)).toBe(true)
30
- })
31
- })
32
-
33
- describe("isDir()", () => {
34
- test("returns true for directory", async () => {
35
- await using tmp = await tmpdir()
36
- const dirpath = path.join(tmp.path, "testdir")
37
- await fs.mkdir(dirpath)
38
-
39
- expect(await Filesystem.isDir(dirpath)).toBe(true)
40
- })
41
-
42
- test("returns false for file", async () => {
43
- await using tmp = await tmpdir()
44
- const filepath = path.join(tmp.path, "test.txt")
45
- await fs.writeFile(filepath, "content", "utf-8")
46
-
47
- expect(await Filesystem.isDir(filepath)).toBe(false)
48
- })
49
-
50
- test("returns false for non-existent path", async () => {
51
- await using tmp = await tmpdir()
52
- const filepath = path.join(tmp.path, "does-not-exist")
53
-
54
- expect(await Filesystem.isDir(filepath)).toBe(false)
55
- })
56
- })
57
-
58
- describe("size()", () => {
59
- test("returns file size", async () => {
60
- await using tmp = await tmpdir()
61
- const filepath = path.join(tmp.path, "test.txt")
62
- const content = "Hello, World!"
63
- await fs.writeFile(filepath, content, "utf-8")
64
-
65
- expect(await Filesystem.size(filepath)).toBe(content.length)
66
- })
67
-
68
- test("returns 0 for non-existent file", async () => {
69
- await using tmp = await tmpdir()
70
- const filepath = path.join(tmp.path, "does-not-exist.txt")
71
-
72
- expect(await Filesystem.size(filepath)).toBe(0)
73
- })
74
-
75
- test("returns directory size", async () => {
76
- await using tmp = await tmpdir()
77
- const dirpath = path.join(tmp.path, "testdir")
78
- await fs.mkdir(dirpath)
79
-
80
- // Directories have size on some systems
81
- const size = await Filesystem.size(dirpath)
82
- expect(typeof size).toBe("number")
83
- })
84
- })
85
-
86
- describe("readText()", () => {
87
- test("reads file content", async () => {
88
- await using tmp = await tmpdir()
89
- const filepath = path.join(tmp.path, "test.txt")
90
- const content = "Hello, World!"
91
- await fs.writeFile(filepath, content, "utf-8")
92
-
93
- expect(await Filesystem.readText(filepath)).toBe(content)
94
- })
95
-
96
- test("throws for non-existent file", async () => {
97
- await using tmp = await tmpdir()
98
- const filepath = path.join(tmp.path, "does-not-exist.txt")
99
-
100
- await expect(Filesystem.readText(filepath)).rejects.toThrow()
101
- })
102
-
103
- test("reads UTF-8 content correctly", async () => {
104
- await using tmp = await tmpdir()
105
- const filepath = path.join(tmp.path, "unicode.txt")
106
- const content = "Hello 世界 🌍"
107
- await fs.writeFile(filepath, content, "utf-8")
108
-
109
- expect(await Filesystem.readText(filepath)).toBe(content)
110
- })
111
- })
112
-
113
- describe("readJson()", () => {
114
- test("reads and parses JSON", async () => {
115
- await using tmp = await tmpdir()
116
- const filepath = path.join(tmp.path, "test.json")
117
- const data = { key: "value", nested: { array: [1, 2, 3] } }
118
- await fs.writeFile(filepath, JSON.stringify(data), "utf-8")
119
-
120
- const result: typeof data = await Filesystem.readJson(filepath)
121
- expect(result).toEqual(data)
122
- })
123
-
124
- test("throws for invalid JSON", async () => {
125
- await using tmp = await tmpdir()
126
- const filepath = path.join(tmp.path, "invalid.json")
127
- await fs.writeFile(filepath, "{ invalid json", "utf-8")
128
-
129
- await expect(Filesystem.readJson(filepath)).rejects.toThrow()
130
- })
131
-
132
- test("throws for non-existent file", async () => {
133
- await using tmp = await tmpdir()
134
- const filepath = path.join(tmp.path, "does-not-exist.json")
135
-
136
- await expect(Filesystem.readJson(filepath)).rejects.toThrow()
137
- })
138
-
139
- test("returns typed data", async () => {
140
- await using tmp = await tmpdir()
141
- const filepath = path.join(tmp.path, "typed.json")
142
- interface Config {
143
- name: string
144
- version: number
145
- }
146
- const data: Config = { name: "test", version: 1 }
147
- await fs.writeFile(filepath, JSON.stringify(data), "utf-8")
148
-
149
- const result = await Filesystem.readJson<Config>(filepath)
150
- expect(result.name).toBe("test")
151
- expect(result.version).toBe(1)
152
- })
153
- })
154
-
155
- describe("readBytes()", () => {
156
- test("reads file as buffer", async () => {
157
- await using tmp = await tmpdir()
158
- const filepath = path.join(tmp.path, "test.txt")
159
- const content = "Hello, World!"
160
- await fs.writeFile(filepath, content, "utf-8")
161
-
162
- const buffer = await Filesystem.readBytes(filepath)
163
- expect(buffer).toBeInstanceOf(Buffer)
164
- expect(buffer.toString("utf-8")).toBe(content)
165
- })
166
-
167
- test("throws for non-existent file", async () => {
168
- await using tmp = await tmpdir()
169
- const filepath = path.join(tmp.path, "does-not-exist.bin")
170
-
171
- await expect(Filesystem.readBytes(filepath)).rejects.toThrow()
172
- })
173
- })
174
-
175
- describe("write()", () => {
176
- test("writes text content", async () => {
177
- await using tmp = await tmpdir()
178
- const filepath = path.join(tmp.path, "test.txt")
179
- const content = "Hello, World!"
180
-
181
- await Filesystem.write(filepath, content)
182
-
183
- expect(await fs.readFile(filepath, "utf-8")).toBe(content)
184
- })
185
-
186
- test("writes buffer content", async () => {
187
- await using tmp = await tmpdir()
188
- const filepath = path.join(tmp.path, "test.bin")
189
- const content = Buffer.from([0x00, 0x01, 0x02, 0x03])
190
-
191
- await Filesystem.write(filepath, content)
192
-
193
- const read = await fs.readFile(filepath)
194
- expect(read).toEqual(content)
195
- })
196
-
197
- test("writes with permissions", async () => {
198
- await using tmp = await tmpdir()
199
- const filepath = path.join(tmp.path, "protected.txt")
200
- const content = "secret"
201
-
202
- await Filesystem.write(filepath, content, 0o600)
203
-
204
- const stats = await fs.stat(filepath)
205
- // Check permissions on Unix
206
- if (process.platform !== "win32") {
207
- expect(stats.mode & 0o777).toBe(0o600)
208
- }
209
- })
210
-
211
- test("creates parent directories", async () => {
212
- await using tmp = await tmpdir()
213
- const filepath = path.join(tmp.path, "nested", "deep", "file.txt")
214
- const content = "nested content"
215
-
216
- await Filesystem.write(filepath, content)
217
-
218
- expect(await fs.readFile(filepath, "utf-8")).toBe(content)
219
- })
220
- })
221
-
222
- describe("writeJson()", () => {
223
- test("writes JSON data", async () => {
224
- await using tmp = await tmpdir()
225
- const filepath = path.join(tmp.path, "data.json")
226
- const data = { key: "value", number: 42 }
227
-
228
- await Filesystem.writeJson(filepath, data)
229
-
230
- const content = await fs.readFile(filepath, "utf-8")
231
- expect(JSON.parse(content)).toEqual(data)
232
- })
233
-
234
- test("writes formatted JSON", async () => {
235
- await using tmp = await tmpdir()
236
- const filepath = path.join(tmp.path, "pretty.json")
237
- const data = { key: "value" }
238
-
239
- await Filesystem.writeJson(filepath, data)
240
-
241
- const content = await fs.readFile(filepath, "utf-8")
242
- expect(content).toContain("\n")
243
- expect(content).toContain(" ")
244
- })
245
-
246
- test("writes with permissions", async () => {
247
- await using tmp = await tmpdir()
248
- const filepath = path.join(tmp.path, "config.json")
249
- const data = { secret: "data" }
250
-
251
- await Filesystem.writeJson(filepath, data, 0o600)
252
-
253
- const stats = await fs.stat(filepath)
254
- if (process.platform !== "win32") {
255
- expect(stats.mode & 0o777).toBe(0o600)
256
- }
257
- })
258
- })
259
-
260
- describe("mimeType()", () => {
261
- test("returns correct MIME type for JSON", () => {
262
- expect(Filesystem.mimeType("test.json")).toContain("application/json")
263
- })
264
-
265
- test("returns correct MIME type for JavaScript", () => {
266
- expect(Filesystem.mimeType("test.js")).toContain("javascript")
267
- })
268
-
269
- test("returns MIME type for TypeScript (or video/mp2t due to extension conflict)", () => {
270
- const mime = Filesystem.mimeType("test.ts")
271
- // .ts is ambiguous: TypeScript vs MPEG-2 TS video
272
- expect(mime === "video/mp2t" || mime === "application/typescript" || mime === "text/typescript").toBe(true)
273
- })
274
-
275
- test("returns correct MIME type for images", () => {
276
- expect(Filesystem.mimeType("test.png")).toContain("image/png")
277
- expect(Filesystem.mimeType("test.jpg")).toContain("image/jpeg")
278
- })
279
-
280
- test("returns default for unknown extension", () => {
281
- expect(Filesystem.mimeType("test.unknown")).toBe("application/octet-stream")
282
- })
283
-
284
- test("handles files without extension", () => {
285
- expect(Filesystem.mimeType("Makefile")).toBe("application/octet-stream")
286
- })
287
- })
288
-
289
- describe("windowsPath()", () => {
290
- test("converts Git Bash paths", () => {
291
- if (process.platform === "win32") {
292
- expect(Filesystem.windowsPath("/c/Users/test")).toBe("C:/Users/test")
293
- expect(Filesystem.windowsPath("/d/dev/project")).toBe("D:/dev/project")
294
- } else {
295
- expect(Filesystem.windowsPath("/c/Users/test")).toBe("/c/Users/test")
296
- }
297
- })
298
-
299
- test("converts Cygwin paths", () => {
300
- if (process.platform === "win32") {
301
- expect(Filesystem.windowsPath("/cygdrive/c/Users/test")).toBe("C:/Users/test")
302
- expect(Filesystem.windowsPath("/cygdrive/x/dev/project")).toBe("X:/dev/project")
303
- } else {
304
- expect(Filesystem.windowsPath("/cygdrive/c/Users/test")).toBe("/cygdrive/c/Users/test")
305
- }
306
- })
307
-
308
- test("converts WSL paths", () => {
309
- if (process.platform === "win32") {
310
- expect(Filesystem.windowsPath("/mnt/c/Users/test")).toBe("C:/Users/test")
311
- expect(Filesystem.windowsPath("/mnt/z/dev/project")).toBe("Z:/dev/project")
312
- } else {
313
- expect(Filesystem.windowsPath("/mnt/c/Users/test")).toBe("/mnt/c/Users/test")
314
- }
315
- })
316
-
317
- test("ignores normal Windows paths", () => {
318
- expect(Filesystem.windowsPath("C:/Users/test")).toBe("C:/Users/test")
319
- expect(Filesystem.windowsPath("D:\\dev\\project")).toBe("D:\\dev\\project")
320
- })
321
- })
322
-
323
- describe("writeStream()", () => {
324
- test("writes from Web ReadableStream", async () => {
325
- await using tmp = await tmpdir()
326
- const filepath = path.join(tmp.path, "streamed.txt")
327
- const content = "Hello from stream!"
328
- const encoder = new TextEncoder()
329
- const stream = new ReadableStream({
330
- start(controller) {
331
- controller.enqueue(encoder.encode(content))
332
- controller.close()
333
- },
334
- })
335
-
336
- await Filesystem.writeStream(filepath, stream)
337
-
338
- expect(await fs.readFile(filepath, "utf-8")).toBe(content)
339
- })
340
-
341
- test("writes from Node.js Readable stream", async () => {
342
- await using tmp = await tmpdir()
343
- const filepath = path.join(tmp.path, "node-streamed.txt")
344
- const content = "Hello from Node stream!"
345
- const { Readable } = await import("stream")
346
- const stream = Readable.from([content])
347
-
348
- await Filesystem.writeStream(filepath, stream)
349
-
350
- expect(await fs.readFile(filepath, "utf-8")).toBe(content)
351
- })
352
-
353
- test("writes binary data from Web ReadableStream", async () => {
354
- await using tmp = await tmpdir()
355
- const filepath = path.join(tmp.path, "binary.dat")
356
- const binaryData = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff])
357
- const stream = new ReadableStream({
358
- start(controller) {
359
- controller.enqueue(binaryData)
360
- controller.close()
361
- },
362
- })
363
-
364
- await Filesystem.writeStream(filepath, stream)
365
-
366
- const read = await fs.readFile(filepath)
367
- expect(Buffer.from(read)).toEqual(Buffer.from(binaryData))
368
- })
369
-
370
- test("writes large content in chunks", async () => {
371
- await using tmp = await tmpdir()
372
- const filepath = path.join(tmp.path, "large.txt")
373
- const chunks = ["chunk1", "chunk2", "chunk3", "chunk4", "chunk5"]
374
- const stream = new ReadableStream({
375
- start(controller) {
376
- for (const chunk of chunks) {
377
- controller.enqueue(new TextEncoder().encode(chunk))
378
- }
379
- controller.close()
380
- },
381
- })
382
-
383
- await Filesystem.writeStream(filepath, stream)
384
-
385
- expect(await fs.readFile(filepath, "utf-8")).toBe(chunks.join(""))
386
- })
387
-
388
- test("creates parent directories", async () => {
389
- await using tmp = await tmpdir()
390
- const filepath = path.join(tmp.path, "nested", "deep", "streamed.txt")
391
- const content = "nested stream content"
392
- const stream = new ReadableStream({
393
- start(controller) {
394
- controller.enqueue(new TextEncoder().encode(content))
395
- controller.close()
396
- },
397
- })
398
-
399
- await Filesystem.writeStream(filepath, stream)
400
-
401
- expect(await fs.readFile(filepath, "utf-8")).toBe(content)
402
- })
403
-
404
- test("writes with permissions", async () => {
405
- await using tmp = await tmpdir()
406
- const filepath = path.join(tmp.path, "protected-stream.txt")
407
- const content = "secret stream content"
408
- const stream = new ReadableStream({
409
- start(controller) {
410
- controller.enqueue(new TextEncoder().encode(content))
411
- controller.close()
412
- },
413
- })
414
-
415
- await Filesystem.writeStream(filepath, stream, 0o600)
416
-
417
- const stats = await fs.stat(filepath)
418
- if (process.platform !== "win32") {
419
- expect(stats.mode & 0o777).toBe(0o600)
420
- }
421
- })
422
-
423
- test("writes executable with permissions", async () => {
424
- await using tmp = await tmpdir()
425
- const filepath = path.join(tmp.path, "script.sh")
426
- const content = "#!/bin/bash\necho hello"
427
- const stream = new ReadableStream({
428
- start(controller) {
429
- controller.enqueue(new TextEncoder().encode(content))
430
- controller.close()
431
- },
432
- })
433
-
434
- await Filesystem.writeStream(filepath, stream, 0o755)
435
-
436
- const stats = await fs.stat(filepath)
437
- if (process.platform !== "win32") {
438
- expect(stats.mode & 0o777).toBe(0o755)
439
- }
440
- expect(await fs.readFile(filepath, "utf-8")).toBe(content)
441
- })
442
- })
443
- })
@@ -1,59 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { formatDuration } from "../../src/util/format"
3
-
4
- describe("util.format", () => {
5
- describe("formatDuration", () => {
6
- test("returns empty string for zero or negative values", () => {
7
- expect(formatDuration(0)).toBe("")
8
- expect(formatDuration(-1)).toBe("")
9
- expect(formatDuration(-100)).toBe("")
10
- })
11
-
12
- test("formats seconds under a minute", () => {
13
- expect(formatDuration(1)).toBe("1s")
14
- expect(formatDuration(30)).toBe("30s")
15
- expect(formatDuration(59)).toBe("59s")
16
- })
17
-
18
- test("formats minutes under an hour", () => {
19
- expect(formatDuration(60)).toBe("1m")
20
- expect(formatDuration(61)).toBe("1m 1s")
21
- expect(formatDuration(90)).toBe("1m 30s")
22
- expect(formatDuration(120)).toBe("2m")
23
- expect(formatDuration(330)).toBe("5m 30s")
24
- expect(formatDuration(3599)).toBe("59m 59s")
25
- })
26
-
27
- test("formats hours under a day", () => {
28
- expect(formatDuration(3600)).toBe("1h")
29
- expect(formatDuration(3660)).toBe("1h 1m")
30
- expect(formatDuration(7200)).toBe("2h")
31
- expect(formatDuration(8100)).toBe("2h 15m")
32
- expect(formatDuration(86399)).toBe("23h 59m")
33
- })
34
-
35
- test("formats days under a week", () => {
36
- expect(formatDuration(86400)).toBe("~1 day")
37
- expect(formatDuration(172800)).toBe("~2 days")
38
- expect(formatDuration(259200)).toBe("~3 days")
39
- expect(formatDuration(604799)).toBe("~6 days")
40
- })
41
-
42
- test("formats weeks", () => {
43
- expect(formatDuration(604800)).toBe("~1 week")
44
- expect(formatDuration(1209600)).toBe("~2 weeks")
45
- expect(formatDuration(1609200)).toBe("~2 weeks")
46
- })
47
-
48
- test("handles boundary values correctly", () => {
49
- expect(formatDuration(59)).toBe("59s")
50
- expect(formatDuration(60)).toBe("1m")
51
- expect(formatDuration(3599)).toBe("59m 59s")
52
- expect(formatDuration(3600)).toBe("1h")
53
- expect(formatDuration(86399)).toBe("23h 59m")
54
- expect(formatDuration(86400)).toBe("~1 day")
55
- expect(formatDuration(604799)).toBe("~6 days")
56
- expect(formatDuration(604800)).toBe("~1 week")
57
- })
58
- })
59
- })
@@ -1,164 +0,0 @@
1
- import { describe, test, expect } from "bun:test"
2
- import path from "path"
3
- import fs from "fs/promises"
4
- import { Glob } from "../../src/util/glob"
5
- import { tmpdir } from "../fixture/fixture"
6
-
7
- describe("Glob", () => {
8
- describe("scan()", () => {
9
- test("finds files matching pattern", async () => {
10
- await using tmp = await tmpdir()
11
- await fs.writeFile(path.join(tmp.path, "a.txt"), "", "utf-8")
12
- await fs.writeFile(path.join(tmp.path, "b.txt"), "", "utf-8")
13
- await fs.writeFile(path.join(tmp.path, "c.md"), "", "utf-8")
14
-
15
- const results = await Glob.scan("*.txt", { cwd: tmp.path })
16
-
17
- expect(results.sort()).toEqual(["a.txt", "b.txt"])
18
- })
19
-
20
- test("returns absolute paths when absolute option is true", async () => {
21
- await using tmp = await tmpdir()
22
- await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8")
23
-
24
- const results = await Glob.scan("*.txt", { cwd: tmp.path, absolute: true })
25
-
26
- expect(results[0]).toBe(path.join(tmp.path, "file.txt"))
27
- })
28
-
29
- test("excludes directories by default", async () => {
30
- await using tmp = await tmpdir()
31
- await fs.mkdir(path.join(tmp.path, "subdir"))
32
- await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8")
33
-
34
- const results = await Glob.scan("*", { cwd: tmp.path })
35
-
36
- expect(results).toEqual(["file.txt"])
37
- })
38
-
39
- test("excludes directories when include is 'file'", async () => {
40
- await using tmp = await tmpdir()
41
- await fs.mkdir(path.join(tmp.path, "subdir"))
42
- await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8")
43
-
44
- const results = await Glob.scan("*", { cwd: tmp.path, include: "file" })
45
-
46
- expect(results).toEqual(["file.txt"])
47
- })
48
-
49
- test("includes directories when include is 'all'", async () => {
50
- await using tmp = await tmpdir()
51
- await fs.mkdir(path.join(tmp.path, "subdir"))
52
- await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8")
53
-
54
- const results = await Glob.scan("*", { cwd: tmp.path, include: "all" })
55
-
56
- expect(results.sort()).toEqual(["file.txt", "subdir"])
57
- })
58
-
59
- test("handles nested patterns", async () => {
60
- await using tmp = await tmpdir()
61
- await fs.mkdir(path.join(tmp.path, "nested"), { recursive: true })
62
- await fs.writeFile(path.join(tmp.path, "nested", "deep.txt"), "", "utf-8")
63
-
64
- const results = await Glob.scan("**/*.txt", { cwd: tmp.path })
65
-
66
- expect(results).toEqual([path.join("nested", "deep.txt")])
67
- })
68
-
69
- test("returns empty array for no matches", async () => {
70
- await using tmp = await tmpdir()
71
-
72
- const results = await Glob.scan("*.nonexistent", { cwd: tmp.path })
73
-
74
- expect(results).toEqual([])
75
- })
76
-
77
- test("does not follow symlinks by default", async () => {
78
- await using tmp = await tmpdir()
79
- await fs.mkdir(path.join(tmp.path, "realdir"))
80
- await fs.writeFile(path.join(tmp.path, "realdir", "file.txt"), "", "utf-8")
81
- await fs.symlink(path.join(tmp.path, "realdir"), path.join(tmp.path, "linkdir"))
82
-
83
- const results = await Glob.scan("**/*.txt", { cwd: tmp.path })
84
-
85
- expect(results).toEqual([path.join("realdir", "file.txt")])
86
- })
87
-
88
- test("follows symlinks when symlink option is true", async () => {
89
- await using tmp = await tmpdir()
90
- await fs.mkdir(path.join(tmp.path, "realdir"))
91
- await fs.writeFile(path.join(tmp.path, "realdir", "file.txt"), "", "utf-8")
92
- await fs.symlink(path.join(tmp.path, "realdir"), path.join(tmp.path, "linkdir"))
93
-
94
- const results = await Glob.scan("**/*.txt", { cwd: tmp.path, symlink: true })
95
-
96
- expect(results.sort()).toEqual([path.join("linkdir", "file.txt"), path.join("realdir", "file.txt")])
97
- })
98
-
99
- test("includes dotfiles when dot option is true", async () => {
100
- await using tmp = await tmpdir()
101
- await fs.writeFile(path.join(tmp.path, ".hidden"), "", "utf-8")
102
- await fs.writeFile(path.join(tmp.path, "visible"), "", "utf-8")
103
-
104
- const results = await Glob.scan("*", { cwd: tmp.path, dot: true })
105
-
106
- expect(results.sort()).toEqual([".hidden", "visible"])
107
- })
108
-
109
- test("excludes dotfiles when dot option is false", async () => {
110
- await using tmp = await tmpdir()
111
- await fs.writeFile(path.join(tmp.path, ".hidden"), "", "utf-8")
112
- await fs.writeFile(path.join(tmp.path, "visible"), "", "utf-8")
113
-
114
- const results = await Glob.scan("*", { cwd: tmp.path, dot: false })
115
-
116
- expect(results).toEqual(["visible"])
117
- })
118
- })
119
-
120
- describe("scanSync()", () => {
121
- test("finds files matching pattern synchronously", async () => {
122
- await using tmp = await tmpdir()
123
- await fs.writeFile(path.join(tmp.path, "a.txt"), "", "utf-8")
124
- await fs.writeFile(path.join(tmp.path, "b.txt"), "", "utf-8")
125
-
126
- const results = Glob.scanSync("*.txt", { cwd: tmp.path })
127
-
128
- expect(results.sort()).toEqual(["a.txt", "b.txt"])
129
- })
130
-
131
- test("respects options", async () => {
132
- await using tmp = await tmpdir()
133
- await fs.mkdir(path.join(tmp.path, "subdir"))
134
- await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8")
135
-
136
- const results = Glob.scanSync("*", { cwd: tmp.path, include: "all" })
137
-
138
- expect(results.sort()).toEqual(["file.txt", "subdir"])
139
- })
140
- })
141
-
142
- describe("match()", () => {
143
- test("matches simple patterns", () => {
144
- expect(Glob.match("*.txt", "file.txt")).toBe(true)
145
- expect(Glob.match("*.txt", "file.js")).toBe(false)
146
- })
147
-
148
- test("matches directory patterns", () => {
149
- expect(Glob.match("**/*.js", "src/index.js")).toBe(true)
150
- expect(Glob.match("**/*.js", "src/index.ts")).toBe(false)
151
- })
152
-
153
- test("matches dot files", () => {
154
- expect(Glob.match(".*", ".gitignore")).toBe(true)
155
- expect(Glob.match("**/*.md", ".github/README.md")).toBe(true)
156
- })
157
-
158
- test("matches brace expansion", () => {
159
- expect(Glob.match("*.{js,ts}", "file.js")).toBe(true)
160
- expect(Glob.match("*.{js,ts}", "file.ts")).toBe(true)
161
- expect(Glob.match("*.{js,ts}", "file.py")).toBe(false)
162
- })
163
- })
164
- })