@stonerzju/opencode 1.2.16-offline.1

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 (496) hide show
  1. package/AGENTS.md +10 -0
  2. package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
  3. package/Dockerfile +18 -0
  4. package/README.md +15 -0
  5. package/bin/opencode +179 -0
  6. package/bunfig.toml +7 -0
  7. package/drizzle.config.ts +10 -0
  8. package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
  9. package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
  10. package/migration/20260211171708_add_project_commands/migration.sql +1 -0
  11. package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
  12. package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
  13. package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
  14. package/migration/20260225215848_workspace/migration.sql +7 -0
  15. package/migration/20260225215848_workspace/snapshot.json +959 -0
  16. package/package.json +140 -0
  17. package/package.json.bak +140 -0
  18. package/parsers-config.ts +254 -0
  19. package/script/build.ts +224 -0
  20. package/script/check-migrations.ts +16 -0
  21. package/script/postinstall.mjs +131 -0
  22. package/script/publish.ts +181 -0
  23. package/script/schema.ts +63 -0
  24. package/script/seed-e2e.ts +50 -0
  25. package/src/acp/README.md +174 -0
  26. package/src/acp/agent.ts +1741 -0
  27. package/src/acp/session.ts +116 -0
  28. package/src/acp/types.ts +23 -0
  29. package/src/agent/agent.ts +339 -0
  30. package/src/agent/generate.txt +75 -0
  31. package/src/agent/prompt/compaction.txt +14 -0
  32. package/src/agent/prompt/explore.txt +18 -0
  33. package/src/agent/prompt/summary.txt +11 -0
  34. package/src/agent/prompt/title.txt +44 -0
  35. package/src/auth/index.ts +68 -0
  36. package/src/bun/index.ts +131 -0
  37. package/src/bun/registry.ts +50 -0
  38. package/src/bus/bus-event.ts +43 -0
  39. package/src/bus/global.ts +10 -0
  40. package/src/bus/index.ts +105 -0
  41. package/src/cli/bootstrap.ts +17 -0
  42. package/src/cli/cmd/acp.ts +70 -0
  43. package/src/cli/cmd/agent.ts +257 -0
  44. package/src/cli/cmd/auth.ts +449 -0
  45. package/src/cli/cmd/cmd.ts +7 -0
  46. package/src/cli/cmd/db.ts +118 -0
  47. package/src/cli/cmd/debug/agent.ts +167 -0
  48. package/src/cli/cmd/debug/config.ts +16 -0
  49. package/src/cli/cmd/debug/file.ts +97 -0
  50. package/src/cli/cmd/debug/index.ts +48 -0
  51. package/src/cli/cmd/debug/lsp.ts +52 -0
  52. package/src/cli/cmd/debug/ripgrep.ts +87 -0
  53. package/src/cli/cmd/debug/scrap.ts +16 -0
  54. package/src/cli/cmd/debug/skill.ts +16 -0
  55. package/src/cli/cmd/debug/snapshot.ts +52 -0
  56. package/src/cli/cmd/export.ts +88 -0
  57. package/src/cli/cmd/generate.ts +38 -0
  58. package/src/cli/cmd/github.ts +1631 -0
  59. package/src/cli/cmd/import.ts +170 -0
  60. package/src/cli/cmd/mcp.ts +754 -0
  61. package/src/cli/cmd/models.ts +77 -0
  62. package/src/cli/cmd/pr.ts +112 -0
  63. package/src/cli/cmd/run.ts +625 -0
  64. package/src/cli/cmd/serve.ts +31 -0
  65. package/src/cli/cmd/session.ts +156 -0
  66. package/src/cli/cmd/stats.ts +410 -0
  67. package/src/cli/cmd/tui/app.tsx +845 -0
  68. package/src/cli/cmd/tui/attach.ts +88 -0
  69. package/src/cli/cmd/tui/component/border.tsx +21 -0
  70. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  71. package/src/cli/cmd/tui/component/dialog-command.tsx +147 -0
  72. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  73. package/src/cli/cmd/tui/component/dialog-model.tsx +165 -0
  74. package/src/cli/cmd/tui/component/dialog-provider.tsx +259 -0
  75. package/src/cli/cmd/tui/component/dialog-session-list.tsx +108 -0
  76. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  77. package/src/cli/cmd/tui/component/dialog-skill.tsx +36 -0
  78. package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
  79. package/src/cli/cmd/tui/component/dialog-status.tsx +167 -0
  80. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  81. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  82. package/src/cli/cmd/tui/component/logo.tsx +85 -0
  83. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +667 -0
  84. package/src/cli/cmd/tui/component/prompt/frecency.tsx +90 -0
  85. package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
  86. package/src/cli/cmd/tui/component/prompt/index.tsx +1155 -0
  87. package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
  88. package/src/cli/cmd/tui/component/spinner.tsx +24 -0
  89. package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
  90. package/src/cli/cmd/tui/component/tips.tsx +152 -0
  91. package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
  92. package/src/cli/cmd/tui/context/args.tsx +15 -0
  93. package/src/cli/cmd/tui/context/directory.ts +13 -0
  94. package/src/cli/cmd/tui/context/exit.tsx +53 -0
  95. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  96. package/src/cli/cmd/tui/context/keybind.tsx +102 -0
  97. package/src/cli/cmd/tui/context/kv.tsx +52 -0
  98. package/src/cli/cmd/tui/context/local.tsx +406 -0
  99. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  100. package/src/cli/cmd/tui/context/route.tsx +46 -0
  101. package/src/cli/cmd/tui/context/sdk.tsx +101 -0
  102. package/src/cli/cmd/tui/context/sync.tsx +488 -0
  103. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  104. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  105. package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
  106. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
  107. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  108. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  109. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  110. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  111. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  112. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  113. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  114. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  115. package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
  116. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  117. package/src/cli/cmd/tui/context/theme/lucent-orng.json +237 -0
  118. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  119. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  120. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  121. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  122. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  123. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  124. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  125. package/src/cli/cmd/tui/context/theme/orng.json +249 -0
  126. package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
  127. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  128. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  129. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  130. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  131. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  132. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  133. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  134. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  135. package/src/cli/cmd/tui/context/theme.tsx +1152 -0
  136. package/src/cli/cmd/tui/context/tui-config.tsx +9 -0
  137. package/src/cli/cmd/tui/event.ts +48 -0
  138. package/src/cli/cmd/tui/routes/home.tsx +145 -0
  139. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +64 -0
  140. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +109 -0
  141. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
  142. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  143. package/src/cli/cmd/tui/routes/session/footer.tsx +91 -0
  144. package/src/cli/cmd/tui/routes/session/header.tsx +135 -0
  145. package/src/cli/cmd/tui/routes/session/index.tsx +2219 -0
  146. package/src/cli/cmd/tui/routes/session/permission.tsx +685 -0
  147. package/src/cli/cmd/tui/routes/session/question.tsx +466 -0
  148. package/src/cli/cmd/tui/routes/session/sidebar.tsx +321 -0
  149. package/src/cli/cmd/tui/thread.ts +199 -0
  150. package/src/cli/cmd/tui/ui/dialog-alert.tsx +59 -0
  151. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +85 -0
  152. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +207 -0
  153. package/src/cli/cmd/tui/ui/dialog-help.tsx +40 -0
  154. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +80 -0
  155. package/src/cli/cmd/tui/ui/dialog-select.tsx +401 -0
  156. package/src/cli/cmd/tui/ui/dialog.tsx +182 -0
  157. package/src/cli/cmd/tui/ui/link.tsx +28 -0
  158. package/src/cli/cmd/tui/ui/spinner.ts +368 -0
  159. package/src/cli/cmd/tui/ui/toast.tsx +100 -0
  160. package/src/cli/cmd/tui/util/clipboard.ts +164 -0
  161. package/src/cli/cmd/tui/util/editor.ts +33 -0
  162. package/src/cli/cmd/tui/util/selection.ts +25 -0
  163. package/src/cli/cmd/tui/util/signal.ts +7 -0
  164. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  165. package/src/cli/cmd/tui/util/transcript.ts +98 -0
  166. package/src/cli/cmd/tui/win32.ts +129 -0
  167. package/src/cli/cmd/tui/worker.ts +157 -0
  168. package/src/cli/cmd/uninstall.ts +356 -0
  169. package/src/cli/cmd/upgrade.ts +73 -0
  170. package/src/cli/cmd/web.ts +81 -0
  171. package/src/cli/cmd/workspace-serve.ts +16 -0
  172. package/src/cli/error.ts +57 -0
  173. package/src/cli/logo.ts +6 -0
  174. package/src/cli/network.ts +60 -0
  175. package/src/cli/ui.ts +116 -0
  176. package/src/cli/upgrade.ts +25 -0
  177. package/src/command/index.ts +150 -0
  178. package/src/command/template/initialize.txt +10 -0
  179. package/src/command/template/review.txt +101 -0
  180. package/src/config/config.ts +1408 -0
  181. package/src/config/markdown.ts +99 -0
  182. package/src/config/migrate-tui-config.ts +155 -0
  183. package/src/config/paths.ts +174 -0
  184. package/src/config/tui-schema.ts +34 -0
  185. package/src/config/tui.ts +118 -0
  186. package/src/control/control.sql.ts +22 -0
  187. package/src/control/index.ts +67 -0
  188. package/src/control-plane/adaptors/index.ts +10 -0
  189. package/src/control-plane/adaptors/types.ts +7 -0
  190. package/src/control-plane/adaptors/worktree.ts +26 -0
  191. package/src/control-plane/config.ts +10 -0
  192. package/src/control-plane/session-proxy-middleware.ts +46 -0
  193. package/src/control-plane/sse.ts +66 -0
  194. package/src/control-plane/workspace-server/routes.ts +33 -0
  195. package/src/control-plane/workspace-server/server.ts +24 -0
  196. package/src/control-plane/workspace.sql.ts +12 -0
  197. package/src/control-plane/workspace.ts +160 -0
  198. package/src/env/index.ts +28 -0
  199. package/src/file/ignore.ts +82 -0
  200. package/src/file/index.ts +646 -0
  201. package/src/file/ripgrep.ts +372 -0
  202. package/src/file/time.ts +71 -0
  203. package/src/file/watcher.ts +128 -0
  204. package/src/flag/flag.ts +109 -0
  205. package/src/format/formatter.ts +395 -0
  206. package/src/format/index.ts +140 -0
  207. package/src/global/index.ts +54 -0
  208. package/src/id/id.ts +84 -0
  209. package/src/ide/index.ts +76 -0
  210. package/src/index.ts +210 -0
  211. package/src/installation/index.ts +266 -0
  212. package/src/lsp/client.ts +251 -0
  213. package/src/lsp/index.ts +485 -0
  214. package/src/lsp/language.ts +120 -0
  215. package/src/lsp/server.ts +2142 -0
  216. package/src/mcp/auth.ts +130 -0
  217. package/src/mcp/index.ts +937 -0
  218. package/src/mcp/oauth-callback.ts +200 -0
  219. package/src/mcp/oauth-provider.ts +176 -0
  220. package/src/patch/index.ts +680 -0
  221. package/src/permission/arity.ts +163 -0
  222. package/src/permission/index.ts +210 -0
  223. package/src/permission/next.ts +286 -0
  224. package/src/plugin/codex.ts +624 -0
  225. package/src/plugin/copilot.ts +327 -0
  226. package/src/plugin/index.ts +143 -0
  227. package/src/project/bootstrap.ts +33 -0
  228. package/src/project/instance.ts +114 -0
  229. package/src/project/project.sql.ts +15 -0
  230. package/src/project/project.ts +441 -0
  231. package/src/project/state.ts +70 -0
  232. package/src/project/vcs.ts +76 -0
  233. package/src/provider/auth.ts +147 -0
  234. package/src/provider/error.ts +189 -0
  235. package/src/provider/models.ts +146 -0
  236. package/src/provider/provider.ts +1338 -0
  237. package/src/provider/sdk/copilot/README.md +5 -0
  238. package/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +164 -0
  239. package/src/provider/sdk/copilot/chat/get-response-metadata.ts +15 -0
  240. package/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +17 -0
  241. package/src/provider/sdk/copilot/chat/openai-compatible-api-types.ts +64 -0
  242. package/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +780 -0
  243. package/src/provider/sdk/copilot/chat/openai-compatible-chat-options.ts +28 -0
  244. package/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
  245. package/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts +87 -0
  246. package/src/provider/sdk/copilot/copilot-provider.ts +100 -0
  247. package/src/provider/sdk/copilot/index.ts +2 -0
  248. package/src/provider/sdk/copilot/openai-compatible-error.ts +27 -0
  249. package/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts +303 -0
  250. package/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts +22 -0
  251. package/src/provider/sdk/copilot/responses/openai-config.ts +18 -0
  252. package/src/provider/sdk/copilot/responses/openai-error.ts +22 -0
  253. package/src/provider/sdk/copilot/responses/openai-responses-api-types.ts +207 -0
  254. package/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +1732 -0
  255. package/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts +177 -0
  256. package/src/provider/sdk/copilot/responses/openai-responses-settings.ts +1 -0
  257. package/src/provider/sdk/copilot/responses/tool/code-interpreter.ts +88 -0
  258. package/src/provider/sdk/copilot/responses/tool/file-search.ts +128 -0
  259. package/src/provider/sdk/copilot/responses/tool/image-generation.ts +115 -0
  260. package/src/provider/sdk/copilot/responses/tool/local-shell.ts +65 -0
  261. package/src/provider/sdk/copilot/responses/tool/web-search-preview.ts +104 -0
  262. package/src/provider/sdk/copilot/responses/tool/web-search.ts +103 -0
  263. package/src/provider/transform.ts +955 -0
  264. package/src/pty/index.ts +324 -0
  265. package/src/question/index.ts +171 -0
  266. package/src/scheduler/index.ts +61 -0
  267. package/src/server/error.ts +36 -0
  268. package/src/server/event.ts +7 -0
  269. package/src/server/mdns.ts +60 -0
  270. package/src/server/routes/config.ts +92 -0
  271. package/src/server/routes/experimental.ts +270 -0
  272. package/src/server/routes/file.ts +197 -0
  273. package/src/server/routes/global.ts +185 -0
  274. package/src/server/routes/mcp.ts +225 -0
  275. package/src/server/routes/permission.ts +68 -0
  276. package/src/server/routes/project.ts +82 -0
  277. package/src/server/routes/provider.ts +165 -0
  278. package/src/server/routes/pty.ts +200 -0
  279. package/src/server/routes/question.ts +98 -0
  280. package/src/server/routes/session.ts +974 -0
  281. package/src/server/routes/tui.ts +379 -0
  282. package/src/server/routes/workspace.ts +104 -0
  283. package/src/server/server.ts +623 -0
  284. package/src/session/compaction.ts +261 -0
  285. package/src/session/index.ts +877 -0
  286. package/src/session/instruction.ts +192 -0
  287. package/src/session/llm.ts +279 -0
  288. package/src/session/message-v2.ts +899 -0
  289. package/src/session/message.ts +189 -0
  290. package/src/session/processor.ts +421 -0
  291. package/src/session/prompt/anthropic-20250930.txt +166 -0
  292. package/src/session/prompt/anthropic.txt +105 -0
  293. package/src/session/prompt/beast.txt +147 -0
  294. package/src/session/prompt/build-switch.txt +5 -0
  295. package/src/session/prompt/codex_header.txt +79 -0
  296. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  297. package/src/session/prompt/gemini.txt +155 -0
  298. package/src/session/prompt/max-steps.txt +16 -0
  299. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  300. package/src/session/prompt/plan.txt +26 -0
  301. package/src/session/prompt/qwen.txt +109 -0
  302. package/src/session/prompt/trinity.txt +97 -0
  303. package/src/session/prompt.ts +1959 -0
  304. package/src/session/retry.ts +101 -0
  305. package/src/session/revert.ts +138 -0
  306. package/src/session/session.sql.ts +88 -0
  307. package/src/session/status.ts +76 -0
  308. package/src/session/summary.ts +161 -0
  309. package/src/session/system.ts +54 -0
  310. package/src/session/todo.ts +56 -0
  311. package/src/share/share-next.ts +210 -0
  312. package/src/share/share.sql.ts +13 -0
  313. package/src/shell/shell.ts +68 -0
  314. package/src/skill/discovery.ts +98 -0
  315. package/src/skill/index.ts +1 -0
  316. package/src/skill/skill.ts +189 -0
  317. package/src/snapshot/index.ts +297 -0
  318. package/src/sql.d.ts +4 -0
  319. package/src/storage/db.ts +155 -0
  320. package/src/storage/json-migration.ts +425 -0
  321. package/src/storage/schema.sql.ts +10 -0
  322. package/src/storage/schema.ts +5 -0
  323. package/src/storage/storage.ts +220 -0
  324. package/src/tool/apply_patch.ts +281 -0
  325. package/src/tool/apply_patch.txt +33 -0
  326. package/src/tool/bash.ts +274 -0
  327. package/src/tool/bash.txt +115 -0
  328. package/src/tool/batch.ts +181 -0
  329. package/src/tool/batch.txt +24 -0
  330. package/src/tool/codesearch.ts +132 -0
  331. package/src/tool/codesearch.txt +12 -0
  332. package/src/tool/edit.ts +654 -0
  333. package/src/tool/edit.txt +10 -0
  334. package/src/tool/external-directory.ts +32 -0
  335. package/src/tool/glob.ts +78 -0
  336. package/src/tool/glob.txt +6 -0
  337. package/src/tool/grep.ts +156 -0
  338. package/src/tool/grep.txt +8 -0
  339. package/src/tool/invalid.ts +17 -0
  340. package/src/tool/ls.ts +121 -0
  341. package/src/tool/ls.txt +1 -0
  342. package/src/tool/lsp.ts +97 -0
  343. package/src/tool/lsp.txt +19 -0
  344. package/src/tool/multiedit.ts +46 -0
  345. package/src/tool/multiedit.txt +41 -0
  346. package/src/tool/plan-enter.txt +14 -0
  347. package/src/tool/plan-exit.txt +13 -0
  348. package/src/tool/plan.ts +131 -0
  349. package/src/tool/question.ts +33 -0
  350. package/src/tool/question.txt +10 -0
  351. package/src/tool/read.ts +293 -0
  352. package/src/tool/read.txt +14 -0
  353. package/src/tool/registry.ts +173 -0
  354. package/src/tool/skill.ts +123 -0
  355. package/src/tool/task.ts +165 -0
  356. package/src/tool/task.txt +60 -0
  357. package/src/tool/todo.ts +53 -0
  358. package/src/tool/todoread.txt +14 -0
  359. package/src/tool/todowrite.txt +167 -0
  360. package/src/tool/tool.ts +89 -0
  361. package/src/tool/truncation.ts +107 -0
  362. package/src/tool/webfetch.ts +206 -0
  363. package/src/tool/webfetch.txt +13 -0
  364. package/src/tool/websearch.ts +150 -0
  365. package/src/tool/websearch.txt +14 -0
  366. package/src/tool/write.ts +84 -0
  367. package/src/tool/write.txt +8 -0
  368. package/src/util/abort.ts +35 -0
  369. package/src/util/archive.ts +16 -0
  370. package/src/util/color.ts +19 -0
  371. package/src/util/context.ts +25 -0
  372. package/src/util/defer.ts +12 -0
  373. package/src/util/eventloop.ts +20 -0
  374. package/src/util/filesystem.ts +189 -0
  375. package/src/util/fn.ts +11 -0
  376. package/src/util/format.ts +20 -0
  377. package/src/util/git.ts +35 -0
  378. package/src/util/glob.ts +34 -0
  379. package/src/util/iife.ts +3 -0
  380. package/src/util/keybind.ts +103 -0
  381. package/src/util/lazy.ts +23 -0
  382. package/src/util/locale.ts +81 -0
  383. package/src/util/lock.ts +98 -0
  384. package/src/util/log.ts +182 -0
  385. package/src/util/process.ts +126 -0
  386. package/src/util/proxied.ts +3 -0
  387. package/src/util/queue.ts +32 -0
  388. package/src/util/rpc.ts +66 -0
  389. package/src/util/scrap.ts +10 -0
  390. package/src/util/signal.ts +12 -0
  391. package/src/util/timeout.ts +14 -0
  392. package/src/util/token.ts +7 -0
  393. package/src/util/wildcard.ts +59 -0
  394. package/src/worktree/index.ts +643 -0
  395. package/sst-env.d.ts +10 -0
  396. package/test/AGENTS.md +81 -0
  397. package/test/acp/agent-interface.test.ts +51 -0
  398. package/test/acp/event-subscription.test.ts +683 -0
  399. package/test/agent/agent.test.ts +689 -0
  400. package/test/bun.test.ts +53 -0
  401. package/test/cli/github-action.test.ts +197 -0
  402. package/test/cli/github-remote.test.ts +80 -0
  403. package/test/cli/import.test.ts +38 -0
  404. package/test/cli/plugin-auth-picker.test.ts +120 -0
  405. package/test/cli/tui/transcript.test.ts +322 -0
  406. package/test/config/agent-color.test.ts +71 -0
  407. package/test/config/config.test.ts +1886 -0
  408. package/test/config/fixtures/empty-frontmatter.md +4 -0
  409. package/test/config/fixtures/frontmatter.md +28 -0
  410. package/test/config/fixtures/markdown-header.md +11 -0
  411. package/test/config/fixtures/no-frontmatter.md +1 -0
  412. package/test/config/fixtures/weird-model-id.md +13 -0
  413. package/test/config/markdown.test.ts +228 -0
  414. package/test/config/tui.test.ts +510 -0
  415. package/test/control-plane/session-proxy-middleware.test.ts +147 -0
  416. package/test/control-plane/sse.test.ts +56 -0
  417. package/test/control-plane/workspace-server-sse.test.ts +65 -0
  418. package/test/control-plane/workspace-sync.test.ts +97 -0
  419. package/test/file/ignore.test.ts +10 -0
  420. package/test/file/index.test.ts +394 -0
  421. package/test/file/path-traversal.test.ts +198 -0
  422. package/test/file/ripgrep.test.ts +39 -0
  423. package/test/file/time.test.ts +361 -0
  424. package/test/fixture/db.ts +11 -0
  425. package/test/fixture/fixture.ts +45 -0
  426. package/test/fixture/lsp/fake-lsp-server.js +77 -0
  427. package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
  428. package/test/fixture/skills/agents-sdk/references/callable.md +92 -0
  429. package/test/fixture/skills/cloudflare/SKILL.md +211 -0
  430. package/test/fixture/skills/index.json +6 -0
  431. package/test/ide/ide.test.ts +82 -0
  432. package/test/keybind.test.ts +421 -0
  433. package/test/lsp/client.test.ts +95 -0
  434. package/test/mcp/headers.test.ts +153 -0
  435. package/test/mcp/oauth-browser.test.ts +249 -0
  436. package/test/memory/abort-leak.test.ts +136 -0
  437. package/test/patch/patch.test.ts +348 -0
  438. package/test/permission/arity.test.ts +33 -0
  439. package/test/permission/next.test.ts +689 -0
  440. package/test/permission-task.test.ts +319 -0
  441. package/test/plugin/auth-override.test.ts +44 -0
  442. package/test/plugin/codex.test.ts +123 -0
  443. package/test/preload.ts +80 -0
  444. package/test/project/project.test.ts +348 -0
  445. package/test/project/worktree-remove.test.ts +65 -0
  446. package/test/provider/amazon-bedrock.test.ts +446 -0
  447. package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
  448. package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
  449. package/test/provider/gitlab-duo.test.ts +262 -0
  450. package/test/provider/provider.test.ts +2220 -0
  451. package/test/provider/transform.test.ts +2353 -0
  452. package/test/pty/pty-output-isolation.test.ts +140 -0
  453. package/test/question/question.test.ts +300 -0
  454. package/test/scheduler.test.ts +73 -0
  455. package/test/server/global-session-list.test.ts +89 -0
  456. package/test/server/session-list.test.ts +90 -0
  457. package/test/server/session-select.test.ts +78 -0
  458. package/test/session/compaction.test.ts +423 -0
  459. package/test/session/instruction.test.ts +170 -0
  460. package/test/session/llm.test.ts +667 -0
  461. package/test/session/message-v2.test.ts +924 -0
  462. package/test/session/prompt.test.ts +211 -0
  463. package/test/session/retry.test.ts +188 -0
  464. package/test/session/revert-compact.test.ts +285 -0
  465. package/test/session/session.test.ts +71 -0
  466. package/test/session/structured-output-integration.test.ts +233 -0
  467. package/test/session/structured-output.test.ts +385 -0
  468. package/test/skill/discovery.test.ts +110 -0
  469. package/test/skill/skill.test.ts +388 -0
  470. package/test/snapshot/snapshot.test.ts +1180 -0
  471. package/test/storage/json-migration.test.ts +846 -0
  472. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  473. package/test/tool/apply_patch.test.ts +566 -0
  474. package/test/tool/bash.test.ts +402 -0
  475. package/test/tool/edit.test.ts +496 -0
  476. package/test/tool/external-directory.test.ts +127 -0
  477. package/test/tool/fixtures/large-image.png +0 -0
  478. package/test/tool/fixtures/models-api.json +38413 -0
  479. package/test/tool/grep.test.ts +110 -0
  480. package/test/tool/question.test.ts +107 -0
  481. package/test/tool/read.test.ts +504 -0
  482. package/test/tool/registry.test.ts +122 -0
  483. package/test/tool/skill.test.ts +112 -0
  484. package/test/tool/truncation.test.ts +160 -0
  485. package/test/tool/webfetch.test.ts +100 -0
  486. package/test/tool/write.test.ts +348 -0
  487. package/test/util/filesystem.test.ts +443 -0
  488. package/test/util/format.test.ts +59 -0
  489. package/test/util/glob.test.ts +164 -0
  490. package/test/util/iife.test.ts +36 -0
  491. package/test/util/lazy.test.ts +50 -0
  492. package/test/util/lock.test.ts +72 -0
  493. package/test/util/process.test.ts +59 -0
  494. package/test/util/timeout.test.ts +21 -0
  495. package/test/util/wildcard.test.ts +90 -0
  496. package/tsconfig.json +16 -0
@@ -0,0 +1,198 @@
1
+ import { test, expect, describe } from "bun:test"
2
+ import path from "path"
3
+ import fs from "fs/promises"
4
+ import { Filesystem } from "../../src/util/filesystem"
5
+ import { File } from "../../src/file"
6
+ import { Instance } from "../../src/project/instance"
7
+ import { tmpdir } from "../fixture/fixture"
8
+
9
+ describe("Filesystem.contains", () => {
10
+ test("allows paths within project", () => {
11
+ expect(Filesystem.contains("/project", "/project/src")).toBe(true)
12
+ expect(Filesystem.contains("/project", "/project/src/file.ts")).toBe(true)
13
+ expect(Filesystem.contains("/project", "/project")).toBe(true)
14
+ })
15
+
16
+ test("blocks ../ traversal", () => {
17
+ expect(Filesystem.contains("/project", "/project/../etc")).toBe(false)
18
+ expect(Filesystem.contains("/project", "/project/src/../../etc")).toBe(false)
19
+ expect(Filesystem.contains("/project", "/etc/passwd")).toBe(false)
20
+ })
21
+
22
+ test("blocks absolute paths outside project", () => {
23
+ expect(Filesystem.contains("/project", "/etc/passwd")).toBe(false)
24
+ expect(Filesystem.contains("/project", "/tmp/file")).toBe(false)
25
+ expect(Filesystem.contains("/home/user/project", "/home/user/other")).toBe(false)
26
+ })
27
+
28
+ test("handles prefix collision edge cases", () => {
29
+ expect(Filesystem.contains("/project", "/project-other/file")).toBe(false)
30
+ expect(Filesystem.contains("/project", "/projectfile")).toBe(false)
31
+ })
32
+ })
33
+
34
+ /*
35
+ * Integration tests for File.read() and File.list() path traversal protection.
36
+ *
37
+ * These tests verify the HTTP API code path is protected. The HTTP endpoints
38
+ * in server.ts (GET /file/content, GET /file) call File.read()/File.list()
39
+ * directly - they do NOT go through ReadTool or the agent permission layer.
40
+ *
41
+ * This is a SEPARATE code path from ReadTool, which has its own checks.
42
+ */
43
+ describe("File.read path traversal protection", () => {
44
+ test("rejects ../ traversal attempting to read /etc/passwd", async () => {
45
+ await using tmp = await tmpdir({
46
+ init: async (dir) => {
47
+ await Bun.write(path.join(dir, "allowed.txt"), "allowed content")
48
+ },
49
+ })
50
+
51
+ await Instance.provide({
52
+ directory: tmp.path,
53
+ fn: async () => {
54
+ await expect(File.read("../../../etc/passwd")).rejects.toThrow("Access denied: path escapes project directory")
55
+ },
56
+ })
57
+ })
58
+
59
+ test("rejects deeply nested traversal", async () => {
60
+ await using tmp = await tmpdir()
61
+
62
+ await Instance.provide({
63
+ directory: tmp.path,
64
+ fn: async () => {
65
+ await expect(File.read("src/nested/../../../../../../../etc/passwd")).rejects.toThrow(
66
+ "Access denied: path escapes project directory",
67
+ )
68
+ },
69
+ })
70
+ })
71
+
72
+ test("allows valid paths within project", async () => {
73
+ await using tmp = await tmpdir({
74
+ init: async (dir) => {
75
+ await Bun.write(path.join(dir, "valid.txt"), "valid content")
76
+ },
77
+ })
78
+
79
+ await Instance.provide({
80
+ directory: tmp.path,
81
+ fn: async () => {
82
+ const result = await File.read("valid.txt")
83
+ expect(result.content).toBe("valid content")
84
+ },
85
+ })
86
+ })
87
+ })
88
+
89
+ describe("File.list path traversal protection", () => {
90
+ test("rejects ../ traversal attempting to list /etc", async () => {
91
+ await using tmp = await tmpdir()
92
+
93
+ await Instance.provide({
94
+ directory: tmp.path,
95
+ fn: async () => {
96
+ await expect(File.list("../../../etc")).rejects.toThrow("Access denied: path escapes project directory")
97
+ },
98
+ })
99
+ })
100
+
101
+ test("allows valid subdirectory listing", async () => {
102
+ await using tmp = await tmpdir({
103
+ init: async (dir) => {
104
+ await Bun.write(path.join(dir, "subdir", "file.txt"), "content")
105
+ },
106
+ })
107
+
108
+ await Instance.provide({
109
+ directory: tmp.path,
110
+ fn: async () => {
111
+ const result = await File.list("subdir")
112
+ expect(Array.isArray(result)).toBe(true)
113
+ },
114
+ })
115
+ })
116
+ })
117
+
118
+ describe("Instance.containsPath", () => {
119
+ test("returns true for path inside directory", async () => {
120
+ await using tmp = await tmpdir({ git: true })
121
+
122
+ await Instance.provide({
123
+ directory: tmp.path,
124
+ fn: () => {
125
+ expect(Instance.containsPath(path.join(tmp.path, "foo.txt"))).toBe(true)
126
+ expect(Instance.containsPath(path.join(tmp.path, "src", "file.ts"))).toBe(true)
127
+ },
128
+ })
129
+ })
130
+
131
+ test("returns true for path inside worktree but outside directory (monorepo subdirectory scenario)", async () => {
132
+ await using tmp = await tmpdir({ git: true })
133
+ const subdir = path.join(tmp.path, "packages", "lib")
134
+ await fs.mkdir(subdir, { recursive: true })
135
+
136
+ await Instance.provide({
137
+ directory: subdir,
138
+ fn: () => {
139
+ // .opencode at worktree root, but we're running from packages/lib
140
+ expect(Instance.containsPath(path.join(tmp.path, ".opencode", "state"))).toBe(true)
141
+ // sibling package should also be accessible
142
+ expect(Instance.containsPath(path.join(tmp.path, "packages", "other", "file.ts"))).toBe(true)
143
+ // worktree root itself
144
+ expect(Instance.containsPath(tmp.path)).toBe(true)
145
+ },
146
+ })
147
+ })
148
+
149
+ test("returns false for path outside both directory and worktree", async () => {
150
+ await using tmp = await tmpdir({ git: true })
151
+
152
+ await Instance.provide({
153
+ directory: tmp.path,
154
+ fn: () => {
155
+ expect(Instance.containsPath("/etc/passwd")).toBe(false)
156
+ expect(Instance.containsPath("/tmp/other-project")).toBe(false)
157
+ },
158
+ })
159
+ })
160
+
161
+ test("returns false for path with .. escaping worktree", async () => {
162
+ await using tmp = await tmpdir({ git: true })
163
+
164
+ await Instance.provide({
165
+ directory: tmp.path,
166
+ fn: () => {
167
+ expect(Instance.containsPath(path.join(tmp.path, "..", "escape.txt"))).toBe(false)
168
+ },
169
+ })
170
+ })
171
+
172
+ test("handles directory === worktree (running from repo root)", async () => {
173
+ await using tmp = await tmpdir({ git: true })
174
+
175
+ await Instance.provide({
176
+ directory: tmp.path,
177
+ fn: () => {
178
+ expect(Instance.directory).toBe(Instance.worktree)
179
+ expect(Instance.containsPath(path.join(tmp.path, "file.txt"))).toBe(true)
180
+ expect(Instance.containsPath("/etc/passwd")).toBe(false)
181
+ },
182
+ })
183
+ })
184
+
185
+ test("non-git project does not allow arbitrary paths via worktree='/'", async () => {
186
+ await using tmp = await tmpdir() // no git: true
187
+
188
+ await Instance.provide({
189
+ directory: tmp.path,
190
+ fn: () => {
191
+ // worktree is "/" for non-git projects, but containsPath should NOT allow all paths
192
+ expect(Instance.containsPath(path.join(tmp.path, "file.txt"))).toBe(true)
193
+ expect(Instance.containsPath("/etc/passwd")).toBe(false)
194
+ expect(Instance.containsPath("/tmp/other")).toBe(false)
195
+ },
196
+ })
197
+ })
198
+ })
@@ -0,0 +1,39 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import fs from "fs/promises"
3
+ import path from "path"
4
+ import { tmpdir } from "../fixture/fixture"
5
+ import { Ripgrep } from "../../src/file/ripgrep"
6
+
7
+ describe("file.ripgrep", () => {
8
+ test("defaults to include hidden", async () => {
9
+ await using tmp = await tmpdir({
10
+ init: async (dir) => {
11
+ await Bun.write(path.join(dir, "visible.txt"), "hello")
12
+ await fs.mkdir(path.join(dir, ".opencode"), { recursive: true })
13
+ await Bun.write(path.join(dir, ".opencode", "thing.json"), "{}")
14
+ },
15
+ })
16
+
17
+ const files = await Array.fromAsync(Ripgrep.files({ cwd: tmp.path }))
18
+ const hasVisible = files.includes("visible.txt")
19
+ const hasHidden = files.includes(path.join(".opencode", "thing.json"))
20
+ expect(hasVisible).toBe(true)
21
+ expect(hasHidden).toBe(true)
22
+ })
23
+
24
+ test("hidden false excludes hidden", async () => {
25
+ await using tmp = await tmpdir({
26
+ init: async (dir) => {
27
+ await Bun.write(path.join(dir, "visible.txt"), "hello")
28
+ await fs.mkdir(path.join(dir, ".opencode"), { recursive: true })
29
+ await Bun.write(path.join(dir, ".opencode", "thing.json"), "{}")
30
+ },
31
+ })
32
+
33
+ const files = await Array.fromAsync(Ripgrep.files({ cwd: tmp.path, hidden: false }))
34
+ const hasVisible = files.includes("visible.txt")
35
+ const hasHidden = files.includes(path.join(".opencode", "thing.json"))
36
+ expect(hasVisible).toBe(true)
37
+ expect(hasHidden).toBe(false)
38
+ })
39
+ })
@@ -0,0 +1,361 @@
1
+ import { describe, test, expect, beforeEach } from "bun:test"
2
+ import path from "path"
3
+ import fs from "fs/promises"
4
+ import { FileTime } from "../../src/file/time"
5
+ import { Instance } from "../../src/project/instance"
6
+ import { Filesystem } from "../../src/util/filesystem"
7
+ import { tmpdir } from "../fixture/fixture"
8
+
9
+ describe("file/time", () => {
10
+ const sessionID = "test-session-123"
11
+
12
+ describe("read() and get()", () => {
13
+ test("stores read timestamp", async () => {
14
+ await using tmp = await tmpdir()
15
+ const filepath = path.join(tmp.path, "file.txt")
16
+ await fs.writeFile(filepath, "content", "utf-8")
17
+
18
+ await Instance.provide({
19
+ directory: tmp.path,
20
+ fn: async () => {
21
+ const before = FileTime.get(sessionID, filepath)
22
+ expect(before).toBeUndefined()
23
+
24
+ FileTime.read(sessionID, filepath)
25
+
26
+ const after = FileTime.get(sessionID, filepath)
27
+ expect(after).toBeInstanceOf(Date)
28
+ expect(after!.getTime()).toBeGreaterThan(0)
29
+ },
30
+ })
31
+ })
32
+
33
+ test("tracks separate timestamps per session", async () => {
34
+ await using tmp = await tmpdir()
35
+ const filepath = path.join(tmp.path, "file.txt")
36
+ await fs.writeFile(filepath, "content", "utf-8")
37
+
38
+ await Instance.provide({
39
+ directory: tmp.path,
40
+ fn: async () => {
41
+ FileTime.read("session1", filepath)
42
+ FileTime.read("session2", filepath)
43
+
44
+ const time1 = FileTime.get("session1", filepath)
45
+ const time2 = FileTime.get("session2", filepath)
46
+
47
+ expect(time1).toBeDefined()
48
+ expect(time2).toBeDefined()
49
+ },
50
+ })
51
+ })
52
+
53
+ test("updates timestamp on subsequent reads", async () => {
54
+ await using tmp = await tmpdir()
55
+ const filepath = path.join(tmp.path, "file.txt")
56
+ await fs.writeFile(filepath, "content", "utf-8")
57
+
58
+ await Instance.provide({
59
+ directory: tmp.path,
60
+ fn: async () => {
61
+ FileTime.read(sessionID, filepath)
62
+ const first = FileTime.get(sessionID, filepath)!
63
+
64
+ await new Promise((resolve) => setTimeout(resolve, 10))
65
+
66
+ FileTime.read(sessionID, filepath)
67
+ const second = FileTime.get(sessionID, filepath)!
68
+
69
+ expect(second.getTime()).toBeGreaterThanOrEqual(first.getTime())
70
+ },
71
+ })
72
+ })
73
+ })
74
+
75
+ describe("assert()", () => {
76
+ test("passes when file has not been modified", async () => {
77
+ await using tmp = await tmpdir()
78
+ const filepath = path.join(tmp.path, "file.txt")
79
+ await fs.writeFile(filepath, "content", "utf-8")
80
+
81
+ await Instance.provide({
82
+ directory: tmp.path,
83
+ fn: async () => {
84
+ FileTime.read(sessionID, filepath)
85
+
86
+ // Should not throw
87
+ await FileTime.assert(sessionID, filepath)
88
+ },
89
+ })
90
+ })
91
+
92
+ test("throws when file was not read first", async () => {
93
+ await using tmp = await tmpdir()
94
+ const filepath = path.join(tmp.path, "file.txt")
95
+ await fs.writeFile(filepath, "content", "utf-8")
96
+
97
+ await Instance.provide({
98
+ directory: tmp.path,
99
+ fn: async () => {
100
+ await expect(FileTime.assert(sessionID, filepath)).rejects.toThrow("You must read file")
101
+ },
102
+ })
103
+ })
104
+
105
+ test("throws when file was modified after read", async () => {
106
+ await using tmp = await tmpdir()
107
+ const filepath = path.join(tmp.path, "file.txt")
108
+ await fs.writeFile(filepath, "content", "utf-8")
109
+
110
+ await Instance.provide({
111
+ directory: tmp.path,
112
+ fn: async () => {
113
+ FileTime.read(sessionID, filepath)
114
+
115
+ // Wait to ensure different timestamps
116
+ await new Promise((resolve) => setTimeout(resolve, 100))
117
+
118
+ // Modify file after reading
119
+ await fs.writeFile(filepath, "modified content", "utf-8")
120
+
121
+ await expect(FileTime.assert(sessionID, filepath)).rejects.toThrow("modified since it was last read")
122
+ },
123
+ })
124
+ })
125
+
126
+ test("includes timestamps in error message", async () => {
127
+ await using tmp = await tmpdir()
128
+ const filepath = path.join(tmp.path, "file.txt")
129
+ await fs.writeFile(filepath, "content", "utf-8")
130
+
131
+ await Instance.provide({
132
+ directory: tmp.path,
133
+ fn: async () => {
134
+ FileTime.read(sessionID, filepath)
135
+ await new Promise((resolve) => setTimeout(resolve, 100))
136
+ await fs.writeFile(filepath, "modified", "utf-8")
137
+
138
+ let error: Error | undefined
139
+ try {
140
+ await FileTime.assert(sessionID, filepath)
141
+ } catch (e) {
142
+ error = e as Error
143
+ }
144
+ expect(error).toBeDefined()
145
+ expect(error!.message).toContain("Last modification:")
146
+ expect(error!.message).toContain("Last read:")
147
+ },
148
+ })
149
+ })
150
+
151
+ test("skips check when OPENCODE_DISABLE_FILETIME_CHECK is true", async () => {
152
+ await using tmp = await tmpdir()
153
+ const filepath = path.join(tmp.path, "file.txt")
154
+ await fs.writeFile(filepath, "content", "utf-8")
155
+
156
+ await Instance.provide({
157
+ directory: tmp.path,
158
+ fn: async () => {
159
+ const { Flag } = await import("../../src/flag/flag")
160
+ const original = Flag.OPENCODE_DISABLE_FILETIME_CHECK
161
+ ;(Flag as { OPENCODE_DISABLE_FILETIME_CHECK: boolean }).OPENCODE_DISABLE_FILETIME_CHECK = true
162
+
163
+ try {
164
+ // Should not throw even though file wasn't read
165
+ await FileTime.assert(sessionID, filepath)
166
+ } finally {
167
+ ;(Flag as { OPENCODE_DISABLE_FILETIME_CHECK: boolean }).OPENCODE_DISABLE_FILETIME_CHECK = original
168
+ }
169
+ },
170
+ })
171
+ })
172
+ })
173
+
174
+ describe("withLock()", () => {
175
+ test("executes function within lock", async () => {
176
+ await using tmp = await tmpdir()
177
+ const filepath = path.join(tmp.path, "file.txt")
178
+
179
+ await Instance.provide({
180
+ directory: tmp.path,
181
+ fn: async () => {
182
+ let executed = false
183
+ await FileTime.withLock(filepath, async () => {
184
+ executed = true
185
+ return "result"
186
+ })
187
+ expect(executed).toBe(true)
188
+ },
189
+ })
190
+ })
191
+
192
+ test("returns function result", async () => {
193
+ await using tmp = await tmpdir()
194
+ const filepath = path.join(tmp.path, "file.txt")
195
+
196
+ await Instance.provide({
197
+ directory: tmp.path,
198
+ fn: async () => {
199
+ const result = await FileTime.withLock(filepath, async () => {
200
+ return "success"
201
+ })
202
+ expect(result).toBe("success")
203
+ },
204
+ })
205
+ })
206
+
207
+ test("serializes concurrent operations on same file", async () => {
208
+ await using tmp = await tmpdir()
209
+ const filepath = path.join(tmp.path, "file.txt")
210
+
211
+ await Instance.provide({
212
+ directory: tmp.path,
213
+ fn: async () => {
214
+ const order: number[] = []
215
+
216
+ const op1 = FileTime.withLock(filepath, async () => {
217
+ order.push(1)
218
+ await new Promise((resolve) => setTimeout(resolve, 10))
219
+ order.push(2)
220
+ })
221
+
222
+ const op2 = FileTime.withLock(filepath, async () => {
223
+ order.push(3)
224
+ order.push(4)
225
+ })
226
+
227
+ await Promise.all([op1, op2])
228
+
229
+ // Operations should be serialized
230
+ expect(order).toContain(1)
231
+ expect(order).toContain(2)
232
+ expect(order).toContain(3)
233
+ expect(order).toContain(4)
234
+ },
235
+ })
236
+ })
237
+
238
+ test("allows concurrent operations on different files", async () => {
239
+ await using tmp = await tmpdir()
240
+ const filepath1 = path.join(tmp.path, "file1.txt")
241
+ const filepath2 = path.join(tmp.path, "file2.txt")
242
+
243
+ await Instance.provide({
244
+ directory: tmp.path,
245
+ fn: async () => {
246
+ let started1 = false
247
+ let started2 = false
248
+
249
+ const op1 = FileTime.withLock(filepath1, async () => {
250
+ started1 = true
251
+ await new Promise((resolve) => setTimeout(resolve, 50))
252
+ expect(started2).toBe(true) // op2 should have started while op1 is running
253
+ })
254
+
255
+ const op2 = FileTime.withLock(filepath2, async () => {
256
+ started2 = true
257
+ })
258
+
259
+ await Promise.all([op1, op2])
260
+
261
+ expect(started1).toBe(true)
262
+ expect(started2).toBe(true)
263
+ },
264
+ })
265
+ })
266
+
267
+ test("releases lock even if function throws", async () => {
268
+ await using tmp = await tmpdir()
269
+ const filepath = path.join(tmp.path, "file.txt")
270
+
271
+ await Instance.provide({
272
+ directory: tmp.path,
273
+ fn: async () => {
274
+ await expect(
275
+ FileTime.withLock(filepath, async () => {
276
+ throw new Error("Test error")
277
+ }),
278
+ ).rejects.toThrow("Test error")
279
+
280
+ // Lock should be released, subsequent operations should work
281
+ let executed = false
282
+ await FileTime.withLock(filepath, async () => {
283
+ executed = true
284
+ })
285
+ expect(executed).toBe(true)
286
+ },
287
+ })
288
+ })
289
+
290
+ test("deadlocks on nested locks (expected behavior)", async () => {
291
+ await using tmp = await tmpdir()
292
+ const filepath = path.join(tmp.path, "file.txt")
293
+
294
+ await Instance.provide({
295
+ directory: tmp.path,
296
+ fn: async () => {
297
+ // Nested locks on same file cause deadlock - this is expected
298
+ // The outer lock waits for inner to complete, but inner waits for outer to release
299
+ const timeout = new Promise<never>((_, reject) =>
300
+ setTimeout(() => reject(new Error("Deadlock detected")), 100),
301
+ )
302
+
303
+ const nestedLock = FileTime.withLock(filepath, async () => {
304
+ return FileTime.withLock(filepath, async () => {
305
+ return "inner"
306
+ })
307
+ })
308
+
309
+ // Should timeout due to deadlock
310
+ await expect(Promise.race([nestedLock, timeout])).rejects.toThrow("Deadlock detected")
311
+ },
312
+ })
313
+ })
314
+ })
315
+
316
+ describe("stat() Filesystem.stat pattern", () => {
317
+ test("reads file modification time via Filesystem.stat()", async () => {
318
+ await using tmp = await tmpdir()
319
+ const filepath = path.join(tmp.path, "file.txt")
320
+ await fs.writeFile(filepath, "content", "utf-8")
321
+
322
+ await Instance.provide({
323
+ directory: tmp.path,
324
+ fn: async () => {
325
+ FileTime.read(sessionID, filepath)
326
+
327
+ const stats = Filesystem.stat(filepath)
328
+ expect(stats?.mtime).toBeInstanceOf(Date)
329
+ expect(stats!.mtime.getTime()).toBeGreaterThan(0)
330
+
331
+ // FileTime.assert uses this stat internally
332
+ await FileTime.assert(sessionID, filepath)
333
+ },
334
+ })
335
+ })
336
+
337
+ test("detects modification via stat mtime", async () => {
338
+ await using tmp = await tmpdir()
339
+ const filepath = path.join(tmp.path, "file.txt")
340
+ await fs.writeFile(filepath, "original", "utf-8")
341
+
342
+ await Instance.provide({
343
+ directory: tmp.path,
344
+ fn: async () => {
345
+ FileTime.read(sessionID, filepath)
346
+
347
+ const originalStat = Filesystem.stat(filepath)
348
+
349
+ // Wait and modify
350
+ await new Promise((resolve) => setTimeout(resolve, 100))
351
+ await fs.writeFile(filepath, "modified", "utf-8")
352
+
353
+ const newStat = Filesystem.stat(filepath)
354
+ expect(newStat!.mtime.getTime()).toBeGreaterThan(originalStat!.mtime.getTime())
355
+
356
+ await expect(FileTime.assert(sessionID, filepath)).rejects.toThrow()
357
+ },
358
+ })
359
+ })
360
+ })
361
+ })
@@ -0,0 +1,11 @@
1
+ import { rm } from "fs/promises"
2
+ import { Instance } from "../../src/project/instance"
3
+ import { Database } from "../../src/storage/db"
4
+
5
+ export async function resetDatabase() {
6
+ await Instance.disposeAll().catch(() => undefined)
7
+ Database.close()
8
+ await rm(Database.Path, { force: true }).catch(() => undefined)
9
+ await rm(`${Database.Path}-wal`, { force: true }).catch(() => undefined)
10
+ await rm(`${Database.Path}-shm`, { force: true }).catch(() => undefined)
11
+ }
@@ -0,0 +1,45 @@
1
+ import { $ } from "bun"
2
+ import * as fs from "fs/promises"
3
+ import os from "os"
4
+ import path from "path"
5
+ import type { Config } from "../../src/config/config"
6
+
7
+ // Strip null bytes from paths (defensive fix for CI environment issues)
8
+ function sanitizePath(p: string): string {
9
+ return p.replace(/\0/g, "")
10
+ }
11
+
12
+ type TmpDirOptions<T> = {
13
+ git?: boolean
14
+ config?: Partial<Config.Info>
15
+ init?: (dir: string) => Promise<T>
16
+ dispose?: (dir: string) => Promise<T>
17
+ }
18
+ export async function tmpdir<T>(options?: TmpDirOptions<T>) {
19
+ const dirpath = sanitizePath(path.join(os.tmpdir(), "opencode-test-" + Math.random().toString(36).slice(2)))
20
+ await fs.mkdir(dirpath, { recursive: true })
21
+ if (options?.git) {
22
+ await $`git init`.cwd(dirpath).quiet()
23
+ await $`git commit --allow-empty -m "root commit ${dirpath}"`.cwd(dirpath).quiet()
24
+ }
25
+ if (options?.config) {
26
+ await Bun.write(
27
+ path.join(dirpath, "opencode.json"),
28
+ JSON.stringify({
29
+ $schema: "https://opencode.ai/config.json",
30
+ ...options.config,
31
+ }),
32
+ )
33
+ }
34
+ const extra = await options?.init?.(dirpath)
35
+ const realpath = sanitizePath(await fs.realpath(dirpath))
36
+ const result = {
37
+ [Symbol.asyncDispose]: async () => {
38
+ await options?.dispose?.(dirpath)
39
+ // await fs.rm(dirpath, { recursive: true, force: true })
40
+ },
41
+ path: realpath,
42
+ extra: extra as T,
43
+ }
44
+ return result
45
+ }