saeeol 1.2.9 → 1.3.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 (539) hide show
  1. package/.turbo/turbo-typecheck.log +1 -0
  2. package/AGENTS.md +72 -0
  3. package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
  4. package/Dockerfile +18 -0
  5. package/assets/saeeol.ico +0 -0
  6. package/bin/saeeol.cjs +0 -0
  7. package/database.db +0 -0
  8. package/drizzle.config.ts +10 -0
  9. package/git +0 -0
  10. package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
  11. package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
  12. package/migration/20260211171708_add_project_commands/migration.sql +1 -0
  13. package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
  14. package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
  15. package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
  16. package/migration/20260225215848_workspace/migration.sql +7 -0
  17. package/migration/20260225215848_workspace/snapshot.json +959 -0
  18. package/migration/20260227213759_add_session_workspace_id/migration.sql +2 -0
  19. package/migration/20260227213759_add_session_workspace_id/snapshot.json +983 -0
  20. package/migration/20260228203230_blue_harpoon/migration.sql +17 -0
  21. package/migration/20260228203230_blue_harpoon/snapshot.json +1102 -0
  22. package/migration/20260303231226_add_workspace_fields/migration.sql +5 -0
  23. package/migration/20260303231226_add_workspace_fields/snapshot.json +1013 -0
  24. package/migration/20260309230000_move_org_to_state/migration.sql +3 -0
  25. package/migration/20260309230000_move_org_to_state/snapshot.json +1156 -0
  26. package/migration/20260312043431_session_message_cursor/migration.sql +4 -0
  27. package/migration/20260312043431_session_message_cursor/snapshot.json +1168 -0
  28. package/migration/20260323234822_events/migration.sql +13 -0
  29. package/migration/20260323234822_events/snapshot.json +1271 -0
  30. package/migration/20260410174513_workspace-name/migration.sql +16 -0
  31. package/migration/20260410174513_workspace-name/snapshot.json +1271 -0
  32. package/migration/20260413175956_chief_energizer/migration.sql +13 -0
  33. package/migration/20260413175956_chief_energizer/snapshot.json +1399 -0
  34. package/migration/20260423070820_add_icon_url_override/migration.sql +2 -0
  35. package/migration/20260423070820_add_icon_url_override/snapshot.json +1409 -0
  36. package/migration/20260428004200_add_session_path/migration.sql +1 -0
  37. package/migration/20260428004200_add_session_path/snapshot.json +1419 -0
  38. package/npm/bin/saeeol +42 -0
  39. package/npm/package.json +39 -0
  40. package/npm/postinstall.js +162 -0
  41. package/package.json +201 -207
  42. package/parsers-config.ts +289 -0
  43. package/script/build.ts +393 -0
  44. package/script/check-migrations.ts +16 -0
  45. package/script/fix-node-pty.ts +34 -0
  46. package/script/generate.ts +23 -0
  47. package/script/postinstall.mjs +189 -0
  48. package/script/publish.ts +200 -0
  49. package/script/run-workspace-server +106 -0
  50. package/script/schema.ts +63 -0
  51. package/script/test-runner.ts +420 -0
  52. package/script/time.ts +6 -0
  53. package/script/trace-imports.ts +153 -0
  54. package/script/upgrade-opentui.ts +64 -0
  55. package/scripts/diff-sdk-types.sh +52 -0
  56. package/specs/effect/facades.md +221 -0
  57. package/specs/effect/http-api.md +401 -0
  58. package/specs/effect/instance-context.md +309 -0
  59. package/specs/effect/loose-ends.md +34 -0
  60. package/specs/effect/migration.md +299 -0
  61. package/specs/effect/routes.md +64 -0
  62. package/specs/effect/schema.md +399 -0
  63. package/specs/effect/server-package.md +668 -0
  64. package/specs/effect/tools.md +90 -0
  65. package/specs/tui-plugins.md +433 -0
  66. package/specs/v2/api.ts +67 -0
  67. package/specs/v2/keymappings.md +10 -0
  68. package/specs/v2/message-shape.md +136 -0
  69. package/src/acp/agent-message.ts +1 -1
  70. package/src/acp/agent-utils.ts +1 -1
  71. package/src/boxes/ansi.ts +17 -0
  72. package/src/boxes/atomic-write.ts +35 -0
  73. package/src/boxes/b64.ts +58 -0
  74. package/src/boxes/bash-security.ts +129 -0
  75. package/src/boxes/bom.ts +18 -0
  76. package/src/boxes/cancel.ts +16 -0
  77. package/src/boxes/chop.ts +12 -0
  78. package/src/boxes/clamp.ts +3 -0
  79. package/src/boxes/compact.ts +9 -0
  80. package/src/boxes/cost-tracker.ts +116 -0
  81. package/src/boxes/dataurl.ts +29 -0
  82. package/src/boxes/delay.ts +27 -0
  83. package/src/boxes/diff-apply.ts +53 -0
  84. package/src/boxes/disposable.ts +13 -0
  85. package/src/boxes/err.ts +34 -0
  86. package/src/boxes/human.ts +47 -0
  87. package/src/boxes/iife.ts +9 -0
  88. package/src/boxes/latch.ts +8 -0
  89. package/src/boxes/memory.ts +198 -0
  90. package/src/boxes/net.ts +16 -0
  91. package/src/boxes/plural.ts +4 -0
  92. package/src/boxes/puny.ts +21 -0
  93. package/src/boxes/retry.ts +49 -0
  94. package/src/boxes/rwlock.ts +41 -0
  95. package/src/boxes/schedule.ts +71 -0
  96. package/src/boxes/scope.ts +21 -0
  97. package/src/boxes/tokens.ts +9 -0
  98. package/src/boxes/ttl-cache.ts +63 -0
  99. package/src/boxes/typed-event.ts +51 -0
  100. package/src/boxes/uid.ts +50 -0
  101. package/src/boxes/wave6.test.ts +296 -0
  102. package/src/boxes/wildcard.ts +58 -0
  103. package/src/bus/global.ts +1 -1
  104. package/src/cli/cmd/github-run-api.ts +2 -2
  105. package/src/cli/cmd/run-events.ts +2 -2
  106. package/src/cli/cmd/tui/component/logo.tsx +1 -1
  107. package/src/cli/cmd/tui/component/prompt/use-prompt-memos.ts +2 -2
  108. package/src/cli/cmd/tui/context/app/editor-zed.ts +1 -1
  109. package/src/cli/cmd/tui/context/app/editor.ts +1 -1
  110. package/src/cli/cmd/tui/context/app/helper.tsx +1 -0
  111. package/src/cli/cmd/tui/context/app/theme.tsx +1 -0
  112. package/src/cli/cmd/tui/util/revert-diff.ts +1 -1
  113. package/src/overlay/cli/cmd/roll-call-call.ts +1 -1
  114. package/src/overlay/cost-tracker/format.ts +1 -1
  115. package/src/overlay/cost-tracker/index.ts +4 -4
  116. package/src/overlay/cost-tracker/state.ts +2 -2
  117. package/src/overlay/cost-tracker/types.ts +2 -2
  118. package/src/overlay/memory/age.ts +1 -1
  119. package/src/overlay/memory/index.ts +4 -4
  120. package/src/overlay/memory/paths.ts +2 -2
  121. package/src/overlay/memory/scan.ts +1 -1
  122. package/src/overlay/memory/types.ts +2 -2
  123. package/src/overlay/tool/bash-security.ts +3 -3
  124. package/src/overlay/util/url.ts +1 -1
  125. package/src/plugin/codex-auth.ts +1 -1
  126. package/src/provider/model-cache.ts +2 -2
  127. package/src/provider/provider-resolve.ts +3 -3
  128. package/src/provider/transform-message.ts +1 -1
  129. package/src/server/routes/game.ts +284 -0
  130. package/src/server/server.ts +2 -0
  131. package/src/session/core/compaction/compaction-helpers.ts +1 -1
  132. package/src/session/core/compaction/compaction.ts +1 -1
  133. package/src/session/core/session-events.ts +50 -8
  134. package/src/session/core/session.ts +2 -0
  135. package/src/sessions/ingest-queue.ts +2 -2
  136. package/src/sessions/remote-ws.ts +1 -1
  137. package/src/tool/workflow/question.ts +1 -1
  138. package/src/util/abort.ts +1 -1
  139. package/src/util/bom.ts +2 -2
  140. package/src/util/color.ts +1 -1
  141. package/src/util/data-url.ts +1 -1
  142. package/src/util/defer.ts +1 -1
  143. package/src/util/error.ts +2 -2
  144. package/src/util/filesystem.ts +2 -2
  145. package/src/util/format.ts +1 -1
  146. package/src/util/iife.ts +1 -1
  147. package/src/util/local-context.ts +1 -1
  148. package/src/util/locale.ts +2 -2
  149. package/src/util/lock.ts +1 -1
  150. package/src/util/network.ts +1 -1
  151. package/src/util/signal.ts +1 -1
  152. package/src/util/token.ts +1 -1
  153. package/src/util/wildcard.ts +1 -1
  154. package/sst-env.d.ts +10 -0
  155. package/test/AGENTS.md +133 -0
  156. package/test/account/repo.test.ts +352 -0
  157. package/test/account/service.test.ts +456 -0
  158. package/test/acp/agent-interface.test.ts +51 -0
  159. package/test/acp/event-subscription.test.ts +725 -0
  160. package/test/agent/agent.test.ts +890 -0
  161. package/test/auth/auth.test.ts +86 -0
  162. package/test/bun/registry.test.ts +75 -0
  163. package/test/bus/bus-effect.test.ts +161 -0
  164. package/test/bus/bus-integration.test.ts +87 -0
  165. package/test/bus/bus.test.ts +219 -0
  166. package/test/cli/account.test.ts +26 -0
  167. package/test/cli/auto-mode.test.ts +75 -0
  168. package/test/cli/bin-saeeol.test.ts +8 -0
  169. package/test/cli/cmd/tui/prompt-part.test.ts +47 -0
  170. package/test/cli/cmd/tui/prompt-traits.test.ts +38 -0
  171. package/test/cli/cmd/tui/sync.test.tsx +159 -0
  172. package/test/cli/error.test.ts +18 -0
  173. package/test/cli/github-action.test.ts +198 -0
  174. package/test/cli/github-remote.test.ts +85 -0
  175. package/test/cli/import.test.ts +97 -0
  176. package/test/cli/install-artifact.test.ts +72 -0
  177. package/test/cli/plugin-auth-picker.test.ts +120 -0
  178. package/test/cli/pr.test.ts +59 -0
  179. package/test/cli/tui/editor-context-zed.test.ts +356 -0
  180. package/test/cli/tui/editor-context.test.tsx +228 -0
  181. package/test/cli/tui/keybind-plugin.test.ts +90 -0
  182. package/test/cli/tui/markdown.test.ts +161 -0
  183. package/test/cli/tui/plugin-add.test.ts +111 -0
  184. package/test/cli/tui/plugin-install.test.ts +87 -0
  185. package/test/cli/tui/plugin-lifecycle.test.ts +224 -0
  186. package/test/cli/tui/plugin-loader-entrypoint.test.ts +484 -0
  187. package/test/cli/tui/plugin-loader-pure.test.ts +71 -0
  188. package/test/cli/tui/plugin-loader.test.ts +816 -0
  189. package/test/cli/tui/plugin-toggle.test.ts +157 -0
  190. package/test/cli/tui/revert-diff.test.ts +35 -0
  191. package/test/cli/tui/slot-replace.test.tsx +47 -0
  192. package/test/cli/tui/theme-store.test.ts +54 -0
  193. package/test/cli/tui/thread.test.ts +28 -0
  194. package/test/cli/tui/transcript.test.ts +426 -0
  195. package/test/cli/tui/usage.test.ts +60 -0
  196. package/test/cli/tui/use-event.test.tsx +175 -0
  197. package/test/config/agent-color.test.ts +67 -0
  198. package/test/config/config.test.ts +2544 -0
  199. package/test/config/fixtures/empty-frontmatter.md +4 -0
  200. package/test/config/fixtures/frontmatter.md +28 -0
  201. package/test/config/fixtures/markdown-header.md +11 -0
  202. package/test/config/fixtures/no-frontmatter.md +1 -0
  203. package/test/config/fixtures/weird-model-id.md +13 -0
  204. package/test/config/lsp.test.ts +87 -0
  205. package/test/config/markdown.test.ts +228 -0
  206. package/test/config/plugin.test.ts +0 -0
  207. package/test/config/tui.test.ts +624 -0
  208. package/test/control-plane/adapters.test.ts +71 -0
  209. package/test/control-plane/workspace.test.ts +1526 -0
  210. package/test/effect/app-runtime-logger.test.ts +98 -0
  211. package/test/effect/config-service.test.ts +65 -0
  212. package/test/effect/instance-state.test.ts +394 -0
  213. package/test/effect/run-service.test.ts +89 -0
  214. package/test/effect/runner.test.ts +523 -0
  215. package/test/fake/provider.ts +82 -0
  216. package/test/file/fsmonitor.test.ts +68 -0
  217. package/test/file/ignore.test.ts +10 -0
  218. package/test/file/index.test.ts +954 -0
  219. package/test/file/path-traversal.test.ts +205 -0
  220. package/test/file/ripgrep.test.ts +226 -0
  221. package/test/file/watcher.test.ts +249 -0
  222. package/test/filesystem/filesystem.test.ts +319 -0
  223. package/test/fixture/db.ts +11 -0
  224. package/test/fixture/fixture.test.ts +26 -0
  225. package/test/fixture/fixture.ts +175 -0
  226. package/test/fixture/flock-worker.ts +72 -0
  227. package/test/fixture/log-init-worker.ts +62 -0
  228. package/test/fixture/lsp/fake-lsp-server.js +249 -0
  229. package/test/fixture/plug-worker.ts +93 -0
  230. package/test/fixture/plugin-meta-worker.ts +19 -0
  231. package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
  232. package/test/fixture/skills/cloudflare/SKILL.md +211 -0
  233. package/test/fixture/skills/index.json +6 -0
  234. package/test/fixture/tui-plugin.ts +323 -0
  235. package/test/fixture/tui-runtime.ts +31 -0
  236. package/test/format/format.test.ts +272 -0
  237. package/test/git/git.test.ts +128 -0
  238. package/test/ide/ide.test.ts +82 -0
  239. package/test/installation/installation.test.ts +168 -0
  240. package/test/keybind.test.ts +421 -0
  241. package/test/lib/effect.ts +53 -0
  242. package/test/lib/filesystem.ts +10 -0
  243. package/test/lib/llm-server.ts +778 -0
  244. package/test/lib/websocket.ts +46 -0
  245. package/test/lsp/client.test.ts +482 -0
  246. package/test/lsp/index.test.ts +160 -0
  247. package/test/lsp/launch.test.ts +22 -0
  248. package/test/lsp/lifecycle.test.ts +184 -0
  249. package/test/ltm/ltm.test.ts +230 -0
  250. package/test/mcp/headers.test.ts +178 -0
  251. package/test/mcp/lifecycle.test.ts +787 -0
  252. package/test/mcp/oauth-auto-connect.test.ts +311 -0
  253. package/test/mcp/oauth-browser.test.ts +276 -0
  254. package/test/mcp/oauth-callback.test.ts +34 -0
  255. package/test/memory/abort-leak-webfetch.ts +49 -0
  256. package/test/memory/abort-leak.test.ts +128 -0
  257. package/test/patch/patch.test.ts +348 -0
  258. package/test/permission/arity.test.ts +33 -0
  259. package/test/permission/next.test.ts +1227 -0
  260. package/test/permission/next.toConfig.test.ts +110 -0
  261. package/test/permission-task.test.ts +326 -0
  262. package/test/plugin/auth-override.test.ts +79 -0
  263. package/test/plugin/cloudflare.test.ts +68 -0
  264. package/test/plugin/codex.test.ts +123 -0
  265. package/test/plugin/github-copilot-models.test.ts +261 -0
  266. package/test/plugin/install-concurrency.test.ts +140 -0
  267. package/test/plugin/install.test.ts +570 -0
  268. package/test/plugin/loader-shared.test.ts +1169 -0
  269. package/test/plugin/meta.test.ts +137 -0
  270. package/test/plugin/plugin-contract.test.ts +291 -0
  271. package/test/plugin/shared.test.ts +88 -0
  272. package/test/plugin/trigger.test.ts +102 -0
  273. package/test/plugin/workspace-adapter.test.ts +109 -0
  274. package/test/preload.ts +77 -0
  275. package/test/project/instance.test.ts +276 -0
  276. package/test/project/migrate-global.test.ts +152 -0
  277. package/test/project/project.test.ts +600 -0
  278. package/test/project/vcs.test.ts +286 -0
  279. package/test/project/worktree-remove.test.ts +126 -0
  280. package/test/project/worktree.test.ts +223 -0
  281. package/test/provider/amazon-bedrock.test.ts +462 -0
  282. package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
  283. package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
  284. package/test/provider/gitlab-duo.test.ts +413 -0
  285. package/test/provider/local.test.ts +208 -0
  286. package/test/provider/models.test.ts +261 -0
  287. package/test/provider/provider-category.test.ts +190 -0
  288. package/test/provider/provider.test.ts +2758 -0
  289. package/test/provider/transform.test.ts +3681 -0
  290. package/test/pty/pty-output-isolation.test.ts +147 -0
  291. package/test/pty/pty-session.test.ts +102 -0
  292. package/test/pty/pty-shell.test.ts +104 -0
  293. package/test/question/question.test.ts +490 -0
  294. package/test/saeeol/agent-global-config-dirs.test.ts +24 -0
  295. package/test/saeeol/agent-manager-tool.test.ts +71 -0
  296. package/test/saeeol/agent-permission-overrides.test.ts +75 -0
  297. package/test/saeeol/agent-skill-permissions.test.ts +37 -0
  298. package/test/saeeol/ask-agent-permissions.test.ts +303 -0
  299. package/test/saeeol/bash-hierarchy.test.ts +64 -0
  300. package/test/saeeol/bash-permission-metadata.test.ts +66 -0
  301. package/test/saeeol/bash-security-extended.test.ts +243 -0
  302. package/test/saeeol/bedrock-claude-empty-content.test.ts +138 -0
  303. package/test/saeeol/boxes-integration.test.ts +415 -0
  304. package/test/saeeol/builtin-skills.test.ts +75 -0
  305. package/test/saeeol/cleanup.ts +28 -0
  306. package/test/saeeol/cli/dev-setup.test.ts +74 -0
  307. package/test/saeeol/cli/roll-call.test.ts +161 -0
  308. package/test/saeeol/cli-run-auto-helper.test.ts +58 -0
  309. package/test/saeeol/codex-auth-refresh.test.ts +124 -0
  310. package/test/saeeol/commit-message/generate.test.ts +188 -0
  311. package/test/saeeol/commit-message/git-context.test.ts +303 -0
  312. package/test/saeeol/commit-message-windows.test.ts +38 -0
  313. package/test/saeeol/compaction-payload-recovery.test.ts +406 -0
  314. package/test/saeeol/compaction-preservation-audit.test.ts +122 -0
  315. package/test/saeeol/compaction-skip-guard.test.ts +224 -0
  316. package/test/saeeol/compaction-smart-select.test.ts +100 -0
  317. package/test/saeeol/config/config.test.ts +166 -0
  318. package/test/saeeol/config/indexing-default-plugin.test.ts +82 -0
  319. package/test/saeeol/config/opentelemetry-default.test.ts +29 -0
  320. package/test/saeeol/config-gitignore.test.ts +70 -0
  321. package/test/saeeol/config-injector.test.ts +305 -0
  322. package/test/saeeol/config-resilience.test.ts +234 -0
  323. package/test/saeeol/config-validation.test.ts +183 -0
  324. package/test/saeeol/cost-propagation.test.ts +94 -0
  325. package/test/saeeol/cost-tracker-extended.test.ts +141 -0
  326. package/test/saeeol/cost-tracker.test.ts +64 -0
  327. package/test/saeeol/custom-provider-delete.test.ts +149 -0
  328. package/test/saeeol/diff-full.test.ts +226 -0
  329. package/test/saeeol/edit-permission-filediff.test.ts +223 -0
  330. package/test/saeeol/encoding.test.ts +364 -0
  331. package/test/saeeol/enhance-prompt.test.ts +61 -0
  332. package/test/saeeol/ensure-plan-dir.test.ts +32 -0
  333. package/test/saeeol/errors.test.ts +144 -0
  334. package/test/saeeol/external-directory-boundary.test.ts +96 -0
  335. package/test/saeeol/gateway-headers.test.ts +88 -0
  336. package/test/saeeol/help.test.ts +191 -0
  337. package/test/saeeol/ignore-migrator.test.ts +308 -0
  338. package/test/saeeol/indexing-auth.test.ts +45 -0
  339. package/test/saeeol/indexing-feature.test.ts +44 -0
  340. package/test/saeeol/indexing-label.test.ts +70 -0
  341. package/test/saeeol/indexing-startup.test.ts +381 -0
  342. package/test/saeeol/indexing-worktree.test.ts +73 -0
  343. package/test/saeeol/instruction.test.ts +136 -0
  344. package/test/saeeol/lancedb-runtime.test.ts +116 -0
  345. package/test/saeeol/loader-auth.test.ts +168 -0
  346. package/test/saeeol/local-model.test.ts +621 -0
  347. package/test/saeeol/logo.test.ts +31 -0
  348. package/test/saeeol/lsp-typescript-lightweight.test.ts +89 -0
  349. package/test/saeeol/mcp-branding.test.ts +33 -0
  350. package/test/saeeol/mcp-docker-rm.test.ts +32 -0
  351. package/test/saeeol/mcp-migrator.test.ts +736 -0
  352. package/test/saeeol/mcp-oauth-callback.test.ts +33 -0
  353. package/test/saeeol/memory-io.test.ts +198 -0
  354. package/test/saeeol/memory-paths.test.ts +87 -0
  355. package/test/saeeol/memory-security.test.ts +166 -0
  356. package/test/saeeol/model-cache-org.test.ts +164 -0
  357. package/test/saeeol/model-info-panel-utils.test.ts +52 -0
  358. package/test/saeeol/model-info-panel.types.test.ts +7 -0
  359. package/test/saeeol/models-401-fallback.test.ts +52 -0
  360. package/test/saeeol/modes-migrator.test.ts +320 -0
  361. package/test/saeeol/nvidia-headers.test.ts +74 -0
  362. package/test/saeeol/patch-jsonc.test.ts +73 -0
  363. package/test/saeeol/patch.test.ts +172 -0
  364. package/test/saeeol/paths.test.ts +265 -0
  365. package/test/saeeol/permission/config-paths.test.ts +174 -0
  366. package/test/saeeol/permission/env-read.test.ts +149 -0
  367. package/test/saeeol/permission/external-directory-allow.test.ts +327 -0
  368. package/test/saeeol/permission/next.always-rules.test.ts +882 -0
  369. package/test/saeeol/permission/next.reply-http.test.ts +205 -0
  370. package/test/saeeol/permission/next.reply-routing.test.ts +184 -0
  371. package/test/saeeol/plan-exit-detection.test.ts +494 -0
  372. package/test/saeeol/plan-followup.test.ts +1376 -0
  373. package/test/saeeol/project-config-update.test.ts +120 -0
  374. package/test/saeeol/project-id.test.ts +455 -0
  375. package/test/saeeol/provider-cost.test.ts +171 -0
  376. package/test/saeeol/provider-list-failed-state.test.ts +100 -0
  377. package/test/saeeol/question-dismiss-all.test.ts +174 -0
  378. package/test/saeeol/read-directory.test.ts +116 -0
  379. package/test/saeeol/rules-migrator.test.ts +257 -0
  380. package/test/saeeol/run-auto.test.ts +176 -0
  381. package/test/saeeol/run-network.test.ts +224 -0
  382. package/test/saeeol/semantic-search.test.ts +186 -0
  383. package/test/saeeol/server/permission-allow-everything.test.ts +125 -0
  384. package/test/saeeol/session/instruction-substitution.test.ts +72 -0
  385. package/test/saeeol/session/platform-attribution.test.ts +118 -0
  386. package/test/saeeol/session/session.test.ts +105 -0
  387. package/test/saeeol/session-compaction-cap.test.ts +399 -0
  388. package/test/saeeol/session-compaction-chunks.test.ts +501 -0
  389. package/test/saeeol/session-compaction-safety.test.ts +481 -0
  390. package/test/saeeol/session-fork-remap.test.ts +251 -0
  391. package/test/saeeol/session-import-service.test.ts +114 -0
  392. package/test/saeeol/session-list.test.ts +47 -0
  393. package/test/saeeol/session-message-metadata.test.ts +128 -0
  394. package/test/saeeol/session-overflow.test.ts +78 -0
  395. package/test/saeeol/session-processor-empty-tool-calls.test.ts +571 -0
  396. package/test/saeeol/session-processor-network-offline.test.ts +204 -0
  397. package/test/saeeol/session-processor-retry-limit.test.ts +238 -0
  398. package/test/saeeol/session-processor-review-telemetry.test.ts +82 -0
  399. package/test/saeeol/session-prompt-compaction-safety.test.ts +517 -0
  400. package/test/saeeol/session-prompt-queue.test.ts +815 -0
  401. package/test/saeeol/sessions/inflight-cache.test.ts +157 -0
  402. package/test/saeeol/sessions/ingest-queue.test.ts +402 -0
  403. package/test/saeeol/sessions/remote-protocol.test.ts +258 -0
  404. package/test/saeeol/sessions/remote-sender.test.ts +1036 -0
  405. package/test/saeeol/sessions/remote-ws.test.ts +367 -0
  406. package/test/saeeol/sessions/sessions-enable-remote.test.disable +181 -0
  407. package/test/saeeol/slot-prop-reactivity.test.ts +142 -0
  408. package/test/saeeol/snapshot-cache.test.ts +84 -0
  409. package/test/saeeol/snapshot-freeze-repro.test.ts +100 -0
  410. package/test/saeeol/snapshot-track-timeout.test.ts +519 -0
  411. package/test/saeeol/stats-subagent-cost.test.ts +123 -0
  412. package/test/saeeol/suggestion/auto-dismiss.test.ts +65 -0
  413. package/test/saeeol/suggestion/suggestion.test.ts +145 -0
  414. package/test/saeeol/suggestion/tool.test.ts +298 -0
  415. package/test/saeeol/summary-file-diff.test.ts +28 -0
  416. package/test/saeeol/system-prompt.test.ts +142 -0
  417. package/test/saeeol/task-nesting.test.ts +193 -0
  418. package/test/saeeol/telemetry/feedback.test.ts +8 -0
  419. package/test/saeeol/todo-view.test.ts +57 -0
  420. package/test/saeeol/tool-encoding.test.ts +455 -0
  421. package/test/saeeol/tool-registry-indexing-import-failure.test.ts +49 -0
  422. package/test/saeeol/tool-registry-indexing.test.ts +236 -0
  423. package/test/saeeol/tool-registry-semantic-import-failure.test.ts +55 -0
  424. package/test/saeeol/tool-task-model.test.ts +352 -0
  425. package/test/saeeol/transform-opus-4.7.test.ts +89 -0
  426. package/test/saeeol/tui-diff.test.ts +91 -0
  427. package/test/saeeol/tui-sync.test.ts +80 -0
  428. package/test/saeeol/util/url.test.ts +141 -0
  429. package/test/saeeol/workflows-migrator.test.ts +261 -0
  430. package/test/saeeol/worktree-diff-summary.test.ts +64 -0
  431. package/test/saeeol/worktree-diff.test.ts +223 -0
  432. package/test/saeeol/worktree-remove-lock.test.ts +82 -0
  433. package/test/server/AGENTS.md +15 -0
  434. package/test/server/contract.test.ts +357 -0
  435. package/test/server/experimental-session-list.test.ts +157 -0
  436. package/test/server/global-session-list.test.ts +155 -0
  437. package/test/server/httpapi-authorization.test.ts +103 -0
  438. package/test/server/httpapi-bridge.test.ts +440 -0
  439. package/test/server/httpapi-config.test.ts +67 -0
  440. package/test/server/httpapi-cors.test.ts +89 -0
  441. package/test/server/httpapi-event.test.ts +57 -0
  442. package/test/server/httpapi-experimental.test.ts +219 -0
  443. package/test/server/httpapi-file.test.ts +79 -0
  444. package/test/server/httpapi-instance-context.test.ts +237 -0
  445. package/test/server/httpapi-instance.legacy.test.ts +140 -0
  446. package/test/server/httpapi-instance.test.ts +83 -0
  447. package/test/server/httpapi-json-parity.test.ts +263 -0
  448. package/test/server/httpapi-mcp-oauth.test.ts +76 -0
  449. package/test/server/httpapi-mcp.test.ts +189 -0
  450. package/test/server/httpapi-provider.test.ts +153 -0
  451. package/test/server/httpapi-pty-websocket.test.ts +16 -0
  452. package/test/server/httpapi-pty.test.ts +175 -0
  453. package/test/server/httpapi-raw-route-auth.test.ts +89 -0
  454. package/test/server/httpapi-sdk.test.ts +681 -0
  455. package/test/server/httpapi-session.test.ts +464 -0
  456. package/test/server/httpapi-sync.test.ts +130 -0
  457. package/test/server/httpapi-tui.test.ts +121 -0
  458. package/test/server/httpapi-workspace-routing.test.ts +471 -0
  459. package/test/server/httpapi-workspace.test.ts +427 -0
  460. package/test/server/lib/conformance.ts +88 -0
  461. package/test/server/lib/stateful.ts +112 -0
  462. package/test/server/project-init-git.test.ts +113 -0
  463. package/test/server/proxy-util.test.ts +113 -0
  464. package/test/server/session-actions.test.ts +49 -0
  465. package/test/server/session-list.test.ts +238 -0
  466. package/test/server/session-messages.test.ts +167 -0
  467. package/test/server/session-select.test.ts +100 -0
  468. package/test/server/trace-attributes.test.ts +76 -0
  469. package/test/server/workspace-proxy.test.ts +165 -0
  470. package/test/server/workspace-routing.test.ts +85 -0
  471. package/test/session/compaction.test.ts +2420 -0
  472. package/test/session/instruction.test.ts +247 -0
  473. package/test/session/llm.test.ts +1273 -0
  474. package/test/session/message-v2.test.ts +1291 -0
  475. package/test/session/messages-pagination.test.ts +1173 -0
  476. package/test/session/network.test.ts +249 -0
  477. package/test/session/processor-effect.test.ts +847 -0
  478. package/test/session/prompt.test.ts +2131 -0
  479. package/test/session/retry.test.ts +340 -0
  480. package/test/session/revert-compact.test.ts +639 -0
  481. package/test/session/schema-decoding.test.ts +311 -0
  482. package/test/session/session-entry-stepper.test.ts +917 -0
  483. package/test/session/session-schema.test.ts +76 -0
  484. package/test/session/snapshot-tool-race.test.ts +257 -0
  485. package/test/session/structured-output-integration.test.ts +265 -0
  486. package/test/session/structured-output.test.ts +381 -0
  487. package/test/session/system.test.ts +73 -0
  488. package/test/share/share-next.test.ts +333 -0
  489. package/test/shell/shell.test.ts +99 -0
  490. package/test/skill/discovery.test.ts +116 -0
  491. package/test/skill/skill.test.ts +393 -0
  492. package/test/snapshot/snapshot.test.ts +1531 -0
  493. package/test/storage/db.test.ts +23 -0
  494. package/test/storage/json-migration.test.ts +832 -0
  495. package/test/storage/storage.test.ts +293 -0
  496. package/test/suggestion/suggestion.test.ts +1 -0
  497. package/test/sync/index.test.ts +256 -0
  498. package/test/tool/__snapshots__/parameters.test.ts.snap +500 -0
  499. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  500. package/test/tool/apply_patch.test.ts +614 -0
  501. package/test/tool/bash.test.ts +1225 -0
  502. package/test/tool/diagnostics-filter.test.ts +55 -0
  503. package/test/tool/edit.test.ts +754 -0
  504. package/test/tool/external-directory.test.ts +169 -0
  505. package/test/tool/fixtures/large-image.png +0 -0
  506. package/test/tool/fixtures/models-api.json +65179 -0
  507. package/test/tool/glob.test.ts +107 -0
  508. package/test/tool/grep.test.ts +114 -0
  509. package/test/tool/lsp.test.ts +187 -0
  510. package/test/tool/parameters.test.ts +243 -0
  511. package/test/tool/question.test.ts +129 -0
  512. package/test/tool/read.test.ts +500 -0
  513. package/test/tool/recall.test.ts +151 -0
  514. package/test/tool/registry.test.ts +203 -0
  515. package/test/tool/skill.test.ts +135 -0
  516. package/test/tool/suggest.test.ts +1 -0
  517. package/test/tool/task.test.ts +612 -0
  518. package/test/tool/tool-define.test.ts +99 -0
  519. package/test/tool/truncation.test.ts +260 -0
  520. package/test/tool/webfetch.test.ts +103 -0
  521. package/test/tool/write.test.ts +291 -0
  522. package/test/util/data-url.test.ts +14 -0
  523. package/test/util/effect-zod.test.ts +754 -0
  524. package/test/util/error.test.ts +38 -0
  525. package/test/util/filesystem.test.ts +656 -0
  526. package/test/util/format.test.ts +59 -0
  527. package/test/util/glob.test.ts +164 -0
  528. package/test/util/iife.test.ts +36 -0
  529. package/test/util/lazy.test.ts +50 -0
  530. package/test/util/lock.test.ts +72 -0
  531. package/test/util/log.test.ts +86 -0
  532. package/test/util/module.test.ts +59 -0
  533. package/test/util/process.test.ts +128 -0
  534. package/test/util/timeout.test.ts +21 -0
  535. package/test/util/which.test.ts +100 -0
  536. package/test/util/wildcard.test.ts +90 -0
  537. package/test/workspace/workspace-restore.test.ts +296 -0
  538. package/src/provider/models-snapshot.d.ts +0 -2
  539. package/src/provider/models-snapshot.js +0 -3
@@ -0,0 +1,205 @@
1
+ import { test, expect, describe } from "bun:test"
2
+ import { Effect } from "effect"
3
+ import path from "path"
4
+ import fs from "fs/promises"
5
+ import { Filesystem } from "@/util/filesystem"
6
+ import { File } from "../../src/file"
7
+ import { Instance } from "../../src/project/instance"
8
+ import { containsPath } from "../../src/project/instance-context"
9
+ import { provideInstance, tmpdir } from "../fixture/fixture"
10
+
11
+ const run = <A, E>(eff: Effect.Effect<A, E, File.Service>) =>
12
+ Effect.runPromise(provideInstance(Instance.directory)(eff.pipe(Effect.provide(File.defaultLayer))))
13
+ const read = (file: string) => run(File.Service.use((svc) => svc.read(file)))
14
+ const list = (dir?: string) => run(File.Service.use((svc) => svc.list(dir)))
15
+
16
+ describe("Filesystem.contains", () => {
17
+ test("allows paths within project", () => {
18
+ expect(Filesystem.contains("/project", "/project/src")).toBe(true)
19
+ expect(Filesystem.contains("/project", "/project/src/file.ts")).toBe(true)
20
+ expect(Filesystem.contains("/project", "/project")).toBe(true)
21
+ })
22
+
23
+ test("blocks ../ traversal", () => {
24
+ expect(Filesystem.contains("/project", "/project/../etc")).toBe(false)
25
+ expect(Filesystem.contains("/project", "/project/src/../../etc")).toBe(false)
26
+ expect(Filesystem.contains("/project", "/etc/passwd")).toBe(false)
27
+ })
28
+
29
+ test("blocks absolute paths outside project", () => {
30
+ expect(Filesystem.contains("/project", "/etc/passwd")).toBe(false)
31
+ expect(Filesystem.contains("/project", "/tmp/file")).toBe(false)
32
+ expect(Filesystem.contains("/home/user/project", "/home/user/other")).toBe(false)
33
+ })
34
+
35
+ test("handles prefix collision edge cases", () => {
36
+ expect(Filesystem.contains("/project", "/project-other/file")).toBe(false)
37
+ expect(Filesystem.contains("/project", "/projectfile")).toBe(false)
38
+ })
39
+ })
40
+
41
+ /*
42
+ * Integration tests for read() and list() path traversal protection.
43
+ *
44
+ * These tests verify the HTTP API code path is protected. The HTTP endpoints
45
+ * in server.ts (GET /file/content, GET /file) call read()/list()
46
+ * directly - they do NOT go through ReadTool or the agent permission layer.
47
+ *
48
+ * This is a SEPARATE code path from ReadTool, which has its own checks.
49
+ */
50
+ describe("File.read path traversal protection", () => {
51
+ test("rejects ../ traversal attempting to read /etc/passwd", async () => {
52
+ await using tmp = await tmpdir({
53
+ init: async (dir) => {
54
+ await Bun.write(path.join(dir, "allowed.txt"), "allowed content")
55
+ },
56
+ })
57
+
58
+ await Instance.provide({
59
+ directory: tmp.path,
60
+ fn: async () => {
61
+ await expect(read("../../../etc/passwd")).rejects.toThrow("Access denied: path escapes project directory")
62
+ },
63
+ })
64
+ })
65
+
66
+ test("rejects deeply nested traversal", async () => {
67
+ await using tmp = await tmpdir()
68
+
69
+ await Instance.provide({
70
+ directory: tmp.path,
71
+ fn: async () => {
72
+ await expect(read("src/nested/../../../../../../../etc/passwd")).rejects.toThrow(
73
+ "Access denied: path escapes project directory",
74
+ )
75
+ },
76
+ })
77
+ })
78
+
79
+ test("allows valid paths within project", async () => {
80
+ await using tmp = await tmpdir({
81
+ init: async (dir) => {
82
+ await Bun.write(path.join(dir, "valid.txt"), "valid content")
83
+ },
84
+ })
85
+
86
+ await Instance.provide({
87
+ directory: tmp.path,
88
+ fn: async () => {
89
+ const result = await read("valid.txt")
90
+ expect(result.content).toBe("valid content")
91
+ },
92
+ })
93
+ })
94
+ })
95
+
96
+ describe("File.list path traversal protection", () => {
97
+ test("rejects ../ traversal attempting to list /etc", async () => {
98
+ await using tmp = await tmpdir()
99
+
100
+ await Instance.provide({
101
+ directory: tmp.path,
102
+ fn: async () => {
103
+ await expect(list("../../../etc")).rejects.toThrow("Access denied: path escapes project directory")
104
+ },
105
+ })
106
+ })
107
+
108
+ test("allows valid subdirectory listing", async () => {
109
+ await using tmp = await tmpdir({
110
+ init: async (dir) => {
111
+ await Bun.write(path.join(dir, "subdir", "file.txt"), "content")
112
+ },
113
+ })
114
+
115
+ await Instance.provide({
116
+ directory: tmp.path,
117
+ fn: async () => {
118
+ const result = await list("subdir")
119
+ expect(Array.isArray(result)).toBe(true)
120
+ },
121
+ })
122
+ })
123
+ })
124
+
125
+ describe("containsPath", () => {
126
+ test("returns true for path inside directory", async () => {
127
+ await using tmp = await tmpdir({ git: true })
128
+
129
+ await Instance.provide({
130
+ directory: tmp.path,
131
+ fn: () => {
132
+ expect(containsPath(path.join(tmp.path, "foo.txt"), Instance.current)).toBe(true)
133
+ expect(containsPath(path.join(tmp.path, "src", "file.ts"), Instance.current)).toBe(true)
134
+ },
135
+ })
136
+ })
137
+
138
+ test("returns true for path inside worktree but outside directory (monorepo subdirectory scenario)", async () => {
139
+ await using tmp = await tmpdir({ git: true })
140
+ const subdir = path.join(tmp.path, "packages", "lib")
141
+ await fs.mkdir(subdir, { recursive: true })
142
+
143
+ await Instance.provide({
144
+ directory: subdir,
145
+ fn: () => {
146
+ // .saeeol at worktree root, but we're running from packages/lib
147
+ expect(containsPath(path.join(tmp.path, ".saeeol", "state"), Instance.current)).toBe(true)
148
+ // sibling package should also be accessible
149
+ expect(containsPath(path.join(tmp.path, "packages", "other", "file.ts"), Instance.current)).toBe(true)
150
+ // worktree root itself
151
+ expect(containsPath(tmp.path, Instance.current)).toBe(true)
152
+ },
153
+ })
154
+ })
155
+
156
+ test("returns false for path outside both directory and worktree", async () => {
157
+ await using tmp = await tmpdir({ git: true })
158
+
159
+ await Instance.provide({
160
+ directory: tmp.path,
161
+ fn: () => {
162
+ expect(containsPath("/etc/passwd", Instance.current)).toBe(false)
163
+ expect(containsPath("/tmp/other-project", Instance.current)).toBe(false)
164
+ },
165
+ })
166
+ })
167
+
168
+ test("returns false for path with .. escaping worktree", async () => {
169
+ await using tmp = await tmpdir({ git: true })
170
+
171
+ await Instance.provide({
172
+ directory: tmp.path,
173
+ fn: () => {
174
+ expect(containsPath(path.join(tmp.path, "..", "escape.txt"), Instance.current)).toBe(false)
175
+ },
176
+ })
177
+ })
178
+
179
+ test("handles directory === worktree (running from repo root)", async () => {
180
+ await using tmp = await tmpdir({ git: true })
181
+
182
+ await Instance.provide({
183
+ directory: tmp.path,
184
+ fn: () => {
185
+ expect(Instance.directory).toBe(Instance.worktree)
186
+ expect(containsPath(path.join(tmp.path, "file.txt"), Instance.current)).toBe(true)
187
+ expect(containsPath("/etc/passwd", Instance.current)).toBe(false)
188
+ },
189
+ })
190
+ })
191
+
192
+ test("non-git project does not allow arbitrary paths via worktree='/'", async () => {
193
+ await using tmp = await tmpdir() // no git: true
194
+
195
+ await Instance.provide({
196
+ directory: tmp.path,
197
+ fn: () => {
198
+ // worktree is "/" for non-git projects, but containsPath should NOT allow all paths
199
+ expect(containsPath(path.join(tmp.path, "file.txt"), Instance.current)).toBe(true)
200
+ expect(containsPath("/etc/passwd", Instance.current)).toBe(false)
201
+ expect(containsPath("/tmp/other", Instance.current)).toBe(false)
202
+ },
203
+ })
204
+ })
205
+ })
@@ -0,0 +1,226 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { Effect } from "effect"
3
+ import * as Stream from "effect/Stream"
4
+ import fs from "fs/promises"
5
+ import path from "path"
6
+ import { tmpdir } from "../fixture/fixture"
7
+ import { Ripgrep } from "../../src/file/ripgrep"
8
+
9
+ const run = <A>(effect: Effect.Effect<A, unknown, Ripgrep.Service>) =>
10
+ effect.pipe(Effect.provide(Ripgrep.defaultLayer), Effect.runPromise)
11
+ describe.skipIf(process.platform === "win32")("file.ripgrep", () => {
12
+ test("defaults to include hidden", async () => {
13
+ await using tmp = await tmpdir({
14
+ init: async (dir) => {
15
+ await Bun.write(path.join(dir, "visible.txt"), "hello")
16
+ await fs.mkdir(path.join(dir, ".saeeol"), { recursive: true })
17
+ await Bun.write(path.join(dir, ".saeeol", "thing.json"), "{}")
18
+ },
19
+ })
20
+
21
+ const files = await run(
22
+ Ripgrep.Service.use((rg) =>
23
+ rg.files({ cwd: tmp.path }).pipe(
24
+ Stream.runCollect,
25
+ Effect.map((c) => [...c]),
26
+ ),
27
+ ),
28
+ )
29
+ expect(files.includes("visible.txt")).toBe(true)
30
+ expect(files.includes(path.join(".saeeol", "thing.json"))).toBe(true)
31
+ })
32
+
33
+ test("hidden false excludes hidden", async () => {
34
+ await using tmp = await tmpdir({
35
+ init: async (dir) => {
36
+ await Bun.write(path.join(dir, "visible.txt"), "hello")
37
+ await fs.mkdir(path.join(dir, ".saeeol"), { recursive: true })
38
+ await Bun.write(path.join(dir, ".saeeol", "thing.json"), "{}")
39
+ },
40
+ })
41
+
42
+ const files = await run(
43
+ Ripgrep.Service.use((rg) =>
44
+ rg.files({ cwd: tmp.path, hidden: false }).pipe(
45
+ Stream.runCollect,
46
+ Effect.map((c) => [...c]),
47
+ ),
48
+ ),
49
+ )
50
+ expect(files.includes("visible.txt")).toBe(true)
51
+ expect(files.includes(path.join(".saeeol", "thing.json"))).toBe(false)
52
+ })
53
+ test("tree skips .saeeol directory files", async () => {
54
+ await using tmp = await tmpdir({
55
+ init: async (dir) => {
56
+ await Bun.write(path.join(dir, "src", "main.ts"), "export {}")
57
+ await fs.mkdir(path.join(dir, ".saeeol"), { recursive: true })
58
+ await Bun.write(path.join(dir, ".saeeol", "config.json"), "{}")
59
+ },
60
+ })
61
+
62
+ const result = await run(Ripgrep.Service.use((rg) => rg.tree({ cwd: tmp.path })))
63
+ expect(result).not.toContain(".saeeol")
64
+ expect(result).toContain("src")
65
+ })
66
+
67
+ test("search returns empty when nothing matches", async () => {
68
+ await using tmp = await tmpdir({
69
+ init: async (dir) => {
70
+ await Bun.write(path.join(dir, "match.ts"), "const value = 'other'\n")
71
+ },
72
+ })
73
+
74
+ const result = await run(Ripgrep.Service.use((rg) => rg.search({ cwd: tmp.path, pattern: "needle" })))
75
+ expect(result.partial).toBe(false)
76
+ expect(result.items).toEqual([])
77
+ })
78
+
79
+ test("search returns match metadata with normalized path", async () => {
80
+ await using tmp = await tmpdir({
81
+ init: async (dir) => {
82
+ await fs.mkdir(path.join(dir, "src"), { recursive: true })
83
+ await Bun.write(path.join(dir, "src", "match.ts"), "const needle = 1\n")
84
+ },
85
+ })
86
+
87
+ const result = await run(Ripgrep.Service.use((rg) => rg.search({ cwd: tmp.path, pattern: "needle" })))
88
+ expect(result.partial).toBe(false)
89
+ expect(result.items).toHaveLength(1)
90
+ expect(result.items[0]?.path.text).toBe(path.join("src", "match.ts"))
91
+ expect(result.items[0]?.line_number).toBe(1)
92
+ expect(result.items[0]?.lines.text).toContain("needle")
93
+ })
94
+
95
+ test("search returns matched rows with glob filter", async () => {
96
+ await using tmp = await tmpdir({
97
+ init: async (dir) => {
98
+ await Bun.write(path.join(dir, "match.ts"), "const value = 'needle'\n")
99
+ await Bun.write(path.join(dir, "skip.txt"), "const value = 'other'\n")
100
+ },
101
+ })
102
+
103
+ const result = await run(
104
+ Ripgrep.Service.use((rg) => rg.search({ cwd: tmp.path, pattern: "needle", glob: ["*.ts"] })),
105
+ )
106
+ expect(result.partial).toBe(false)
107
+ expect(result.items).toHaveLength(1)
108
+ expect(result.items[0]?.path.text).toContain("match.ts")
109
+ expect(result.items[0]?.lines.text).toContain("needle")
110
+ })
111
+
112
+ test("search supports explicit file targets", async () => {
113
+ await using tmp = await tmpdir({
114
+ init: async (dir) => {
115
+ await Bun.write(path.join(dir, "match.ts"), "const value = 'needle'\n")
116
+ await Bun.write(path.join(dir, "skip.ts"), "const value = 'needle'\n")
117
+ },
118
+ })
119
+
120
+ const file = path.join(tmp.path, "match.ts")
121
+ const result = await run(Ripgrep.Service.use((rg) => rg.search({ cwd: tmp.path, pattern: "needle", file: [file] })))
122
+ expect(result.partial).toBe(false)
123
+ expect(result.items).toHaveLength(1)
124
+ expect(result.items[0]?.path.text).toBe(file)
125
+ })
126
+
127
+ test("files returns empty when glob matches no files", async () => {
128
+ await using tmp = await tmpdir({
129
+ init: async (dir) => {
130
+ await fs.mkdir(path.join(dir, "packages", "console"), { recursive: true })
131
+ await Bun.write(path.join(dir, "packages", "console", "package.json"), "{}")
132
+ },
133
+ })
134
+
135
+ const files = await run(
136
+ Ripgrep.Service.use((rg) =>
137
+ rg.files({ cwd: tmp.path, glob: ["packages/*"] }).pipe(
138
+ Stream.runCollect,
139
+ Effect.map((c) => [...c]),
140
+ ),
141
+ ),
142
+ )
143
+ expect(files).toEqual([])
144
+ })
145
+
146
+ test("files returns stream of filenames", async () => {
147
+ await using tmp = await tmpdir({
148
+ init: async (dir) => {
149
+ await Bun.write(path.join(dir, "a.txt"), "hello")
150
+ await Bun.write(path.join(dir, "b.txt"), "world")
151
+ },
152
+ })
153
+
154
+ const files = await run(
155
+ Ripgrep.Service.use((rg) =>
156
+ rg.files({ cwd: tmp.path }).pipe(
157
+ Stream.runCollect,
158
+ Effect.map((c) => [...c].sort()),
159
+ ),
160
+ ),
161
+ )
162
+ expect(files).toEqual(["a.txt", "b.txt"])
163
+ })
164
+
165
+ test("files respects glob filter", async () => {
166
+ await using tmp = await tmpdir({
167
+ init: async (dir) => {
168
+ await Bun.write(path.join(dir, "keep.ts"), "yes")
169
+ await Bun.write(path.join(dir, "skip.txt"), "no")
170
+ },
171
+ })
172
+
173
+ const files = await run(
174
+ Ripgrep.Service.use((rg) =>
175
+ rg.files({ cwd: tmp.path, glob: ["*.ts"] }).pipe(
176
+ Stream.runCollect,
177
+ Effect.map((c) => [...c]),
178
+ ),
179
+ ),
180
+ )
181
+ expect(files).toEqual(["keep.ts"])
182
+ })
183
+
184
+ test("files dies on nonexistent directory", async () => {
185
+ const exit = await Ripgrep.Service.use((rg) =>
186
+ rg.files({ cwd: "/tmp/nonexistent-dir-12345" }).pipe(Stream.runCollect),
187
+ ).pipe(Effect.provide(Ripgrep.defaultLayer), Effect.runPromiseExit)
188
+ expect(exit._tag).toBe("Failure")
189
+ })
190
+
191
+ test("ignores RIPGREP_CONFIG_PATH in direct mode", async () => {
192
+ await using tmp = await tmpdir({
193
+ init: async (dir) => {
194
+ await Bun.write(path.join(dir, "match.ts"), "const needle = 1\n")
195
+ },
196
+ })
197
+
198
+ const prev = process.env["RIPGREP_CONFIG_PATH"]
199
+ process.env["RIPGREP_CONFIG_PATH"] = path.join(tmp.path, "missing-ripgreprc")
200
+ try {
201
+ const result = await run(Ripgrep.Service.use((rg) => rg.search({ cwd: tmp.path, pattern: "needle" })))
202
+ expect(result.items).toHaveLength(1)
203
+ } finally {
204
+ if (prev === undefined) delete process.env["RIPGREP_CONFIG_PATH"]
205
+ else process.env["RIPGREP_CONFIG_PATH"] = prev
206
+ }
207
+ })
208
+
209
+ test("ignores RIPGREP_CONFIG_PATH in worker mode", async () => {
210
+ await using tmp = await tmpdir({
211
+ init: async (dir) => {
212
+ await Bun.write(path.join(dir, "match.ts"), "const needle = 1\n")
213
+ },
214
+ })
215
+
216
+ const prev = process.env["RIPGREP_CONFIG_PATH"]
217
+ process.env["RIPGREP_CONFIG_PATH"] = path.join(tmp.path, "missing-ripgreprc")
218
+ try {
219
+ const result = await run(Ripgrep.Service.use((rg) => rg.search({ cwd: tmp.path, pattern: "needle" })))
220
+ expect(result.items).toHaveLength(1)
221
+ } finally {
222
+ if (prev === undefined) delete process.env["RIPGREP_CONFIG_PATH"]
223
+ else process.env["RIPGREP_CONFIG_PATH"] = prev
224
+ }
225
+ })
226
+ })
@@ -0,0 +1,249 @@
1
+ import { $ } from "bun"
2
+ import { afterEach, describe, expect, test } from "bun:test"
3
+ import fs from "fs/promises"
4
+ import path from "path"
5
+ import { ConfigProvider, Deferred, Effect, Layer, ManagedRuntime, Option } from "effect"
6
+ import { disposeAllInstances, tmpdir } from "../fixture/fixture"
7
+ import { Bus } from "../../src/bus"
8
+ import { Config } from "@/config/config"
9
+ import { FileWatcher } from "../../src/file/watcher"
10
+ import { Git } from "../../src/git"
11
+ import { Instance } from "../../src/project/instance"
12
+
13
+ // Native @parcel/watcher bindings aren't reliably available in CI (missing on Linux, flaky on Windows)
14
+ const describeWatcher = FileWatcher.hasNativeBinding() && !process.env.CI ? describe : describe.skip
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Helpers
18
+ // ---------------------------------------------------------------------------
19
+
20
+ const watcherConfigLayer = ConfigProvider.layer(
21
+ ConfigProvider.fromUnknown({
22
+ SAEEOL_EXPERIMENTAL_FILEWATCHER: "true",
23
+ SAEEOL_EXPERIMENTAL_DISABLE_FILEWATCHER: "false",
24
+ }),
25
+ )
26
+
27
+ type WatcherEvent = { file: string; event: "add" | "change" | "unlink" }
28
+
29
+ /** Run `body` with a live FileWatcher service. */
30
+ function withWatcher<E>(directory: string, body: Effect.Effect<void, E>) {
31
+ return Instance.provide({
32
+ directory,
33
+ fn: async () => {
34
+ const layer: Layer.Layer<FileWatcher.Service, never, never> = FileWatcher.layer.pipe(
35
+ Layer.provide(Config.defaultLayer),
36
+ Layer.provide(Git.defaultLayer),
37
+ Layer.provide(watcherConfigLayer),
38
+ )
39
+ const rt = ManagedRuntime.make(layer)
40
+ try {
41
+ await rt.runPromise(FileWatcher.Service.use((s) => s.init()))
42
+ await Effect.runPromise(ready(directory))
43
+ await Effect.runPromise(body)
44
+ } finally {
45
+ await rt.dispose()
46
+ }
47
+ },
48
+ })
49
+ }
50
+
51
+ function listen(directory: string, check: (evt: WatcherEvent) => boolean, hit: (evt: WatcherEvent) => void) {
52
+ let done = false
53
+
54
+ const unsub = Bus.subscribe(FileWatcher.Event.Updated, (evt) => {
55
+ if (done) return
56
+ if (!check(evt.properties)) return
57
+ hit(evt.properties)
58
+ })
59
+
60
+ return () => {
61
+ if (done) return
62
+ done = true
63
+ unsub()
64
+ }
65
+ }
66
+
67
+ function wait(directory: string, check: (evt: WatcherEvent) => boolean) {
68
+ return Effect.gen(function* () {
69
+ const deferred = yield* Deferred.make<WatcherEvent>()
70
+ const cleanup = yield* Effect.sync(() => {
71
+ let off = () => {}
72
+ off = listen(directory, check, (evt) => {
73
+ off()
74
+ Deferred.doneUnsafe(deferred, Effect.succeed(evt))
75
+ })
76
+ return off
77
+ })
78
+ return { cleanup, deferred }
79
+ })
80
+ }
81
+
82
+ function nextUpdate<E>(directory: string, check: (evt: WatcherEvent) => boolean, trigger: Effect.Effect<void, E>) {
83
+ return Effect.acquireUseRelease(
84
+ wait(directory, check),
85
+ ({ deferred }) =>
86
+ Effect.gen(function* () {
87
+ yield* trigger
88
+ return yield* Deferred.await(deferred).pipe(Effect.timeout("5 seconds"))
89
+ }),
90
+ ({ cleanup }) => Effect.sync(cleanup),
91
+ )
92
+ }
93
+
94
+ /** Effect that asserts no matching event arrives within `ms`. */
95
+ function noUpdate<E>(
96
+ directory: string,
97
+ check: (evt: WatcherEvent) => boolean,
98
+ trigger: Effect.Effect<void, E>,
99
+ ms = 500,
100
+ ) {
101
+ return Effect.acquireUseRelease(
102
+ wait(directory, check),
103
+ ({ deferred }) =>
104
+ Effect.gen(function* () {
105
+ yield* trigger
106
+ expect(yield* Deferred.await(deferred).pipe(Effect.timeoutOption(`${ms} millis`))).toEqual(Option.none())
107
+ }),
108
+ ({ cleanup }) => Effect.sync(cleanup),
109
+ )
110
+ }
111
+
112
+ function ready(directory: string) {
113
+ const file = path.join(directory, `.watcher-${Math.random().toString(36).slice(2)}`)
114
+ const head = path.join(directory, ".git", "HEAD")
115
+
116
+ return Effect.gen(function* () {
117
+ yield* nextUpdate(
118
+ directory,
119
+ (evt) => evt.file === file && evt.event === "add",
120
+ Effect.promise(() => fs.writeFile(file, "ready")),
121
+ ).pipe(Effect.ensuring(Effect.promise(() => fs.rm(file, { force: true }).catch(() => undefined))), Effect.asVoid)
122
+
123
+ const git = yield* Effect.promise(() =>
124
+ fs
125
+ .stat(head)
126
+ .then(() => true)
127
+ .catch(() => false),
128
+ )
129
+ if (!git) return
130
+
131
+ const branch = `watch-${Math.random().toString(36).slice(2)}`
132
+ const hash = yield* Effect.promise(() => $`git rev-parse HEAD`.cwd(directory).quiet().text())
133
+ yield* nextUpdate(
134
+ directory,
135
+ (evt) => evt.file === head && evt.event !== "unlink",
136
+ Effect.promise(async () => {
137
+ await fs.writeFile(path.join(directory, ".git", "refs", "heads", branch), hash.trim() + "\n")
138
+ await fs.writeFile(head, `ref: refs/heads/${branch}\n`)
139
+ }),
140
+ ).pipe(Effect.asVoid)
141
+ })
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Tests
146
+ // ---------------------------------------------------------------------------
147
+
148
+ describeWatcher("FileWatcher", () => {
149
+ afterEach(async () => {
150
+ await disposeAllInstances()
151
+ })
152
+
153
+ test("publishes root create, update, and delete events", async () => {
154
+ await using tmp = await tmpdir({ git: true })
155
+ const file = path.join(tmp.path, "watch.txt")
156
+ const dir = tmp.path
157
+ const cases = [
158
+ { event: "add" as const, trigger: Effect.promise(() => fs.writeFile(file, "a")) },
159
+ { event: "change" as const, trigger: Effect.promise(() => fs.writeFile(file, "b")) },
160
+ { event: "unlink" as const, trigger: Effect.promise(() => fs.unlink(file)) },
161
+ ]
162
+
163
+ await withWatcher(
164
+ dir,
165
+ Effect.forEach(cases, ({ event, trigger }) =>
166
+ nextUpdate(dir, (evt) => evt.file === file && evt.event === event, trigger).pipe(
167
+ Effect.tap((evt) => Effect.sync(() => expect(evt).toEqual({ file, event }))),
168
+ ),
169
+ ),
170
+ )
171
+ })
172
+
173
+ test("watches non-git roots", async () => {
174
+ await using tmp = await tmpdir()
175
+ const file = path.join(tmp.path, "plain.txt")
176
+ const dir = tmp.path
177
+
178
+ await withWatcher(
179
+ dir,
180
+ nextUpdate(
181
+ dir,
182
+ (e) => e.file === file && e.event === "add",
183
+ Effect.promise(() => fs.writeFile(file, "plain")),
184
+ ).pipe(Effect.tap((evt) => Effect.sync(() => expect(evt).toEqual({ file, event: "add" })))),
185
+ )
186
+ })
187
+
188
+ test("cleanup stops publishing events", async () => {
189
+ await using tmp = await tmpdir({ git: true })
190
+ const file = path.join(tmp.path, "after-dispose.txt")
191
+
192
+ // Start and immediately stop the watcher (withWatcher disposes on exit)
193
+ await withWatcher(tmp.path, Effect.void)
194
+
195
+ // Now write a file — no watcher should be listening
196
+ await Instance.provide({
197
+ directory: tmp.path,
198
+ fn: () =>
199
+ Effect.runPromise(
200
+ noUpdate(
201
+ tmp.path,
202
+ (e) => e.file === file,
203
+ Effect.promise(() => fs.writeFile(file, "gone")),
204
+ ),
205
+ ),
206
+ })
207
+ })
208
+
209
+ test("ignores .git/index changes", async () => {
210
+ await using tmp = await tmpdir({ git: true })
211
+ const gitIndex = path.join(tmp.path, ".git", "index")
212
+ const edit = path.join(tmp.path, "tracked.txt")
213
+
214
+ await withWatcher(
215
+ tmp.path,
216
+ noUpdate(
217
+ tmp.path,
218
+ (e) => e.file === gitIndex,
219
+ Effect.promise(async () => {
220
+ await fs.writeFile(edit, "a")
221
+ await $`git add .`.cwd(tmp.path).quiet().nothrow()
222
+ }),
223
+ ),
224
+ )
225
+ })
226
+
227
+ test("publishes .git/HEAD events", async () => {
228
+ await using tmp = await tmpdir({ git: true })
229
+ const head = path.join(tmp.path, ".git", "HEAD")
230
+ const branch = `watch-${Math.random().toString(36).slice(2)}`
231
+ await $`git branch ${branch}`.cwd(tmp.path).quiet()
232
+
233
+ await withWatcher(
234
+ tmp.path,
235
+ nextUpdate(
236
+ tmp.path,
237
+ (evt) => evt.file === head && evt.event !== "unlink",
238
+ Effect.promise(() => fs.writeFile(head, `ref: refs/heads/${branch}\n`)),
239
+ ).pipe(
240
+ Effect.tap((evt) =>
241
+ Effect.sync(() => {
242
+ expect(evt.file).toBe(head)
243
+ expect(["add", "change"]).toContain(evt.event)
244
+ }),
245
+ ),
246
+ ),
247
+ )
248
+ })
249
+ })