@neurocode-ai/core 1.18.8

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 (490) hide show
  1. package/bin/neurocode +199 -0
  2. package/bunfig.toml +2 -0
  3. package/drizzle.config.ts +10 -0
  4. package/package.json +131 -0
  5. package/schema.json +2071 -0
  6. package/script/fix-node-pty.ts +28 -0
  7. package/script/migration.ts +196 -0
  8. package/src/account/sql.ts +39 -0
  9. package/src/account.ts +101 -0
  10. package/src/agent.ts +111 -0
  11. package/src/aisdk.ts +235 -0
  12. package/src/background-job.ts +365 -0
  13. package/src/catalog.ts +301 -0
  14. package/src/command.ts +64 -0
  15. package/src/config/agent.ts +25 -0
  16. package/src/config/attachments.ts +15 -0
  17. package/src/config/command.ts +12 -0
  18. package/src/config/compaction.ts +15 -0
  19. package/src/config/experimental.ts +18 -0
  20. package/src/config/formatter.ts +12 -0
  21. package/src/config/lsp.ts +18 -0
  22. package/src/config/markdown.ts +36 -0
  23. package/src/config/mcp.ts +48 -0
  24. package/src/config/plugin/agent.ts +179 -0
  25. package/src/config/plugin/command.ts +83 -0
  26. package/src/config/plugin/external.ts +91 -0
  27. package/src/config/plugin/provider.ts +113 -0
  28. package/src/config/plugin/reference.ts +70 -0
  29. package/src/config/plugin/skill.ts +50 -0
  30. package/src/config/plugin.ts +13 -0
  31. package/src/config/provider.ts +71 -0
  32. package/src/config/reference.ts +22 -0
  33. package/src/config/tool-output.ts +9 -0
  34. package/src/config/watcher.ts +7 -0
  35. package/src/config.ts +227 -0
  36. package/src/control-plane/move-session.ts +148 -0
  37. package/src/control-plane/workspace.sql.ts +20 -0
  38. package/src/credential/sql.ts +14 -0
  39. package/src/credential.ts +138 -0
  40. package/src/cross-spawn-spawner.ts +507 -0
  41. package/src/data-migration.sql.ts +6 -0
  42. package/src/database/database.ts +57 -0
  43. package/src/database/migration/20260127222353_familiar_lady_ursula.ts +107 -0
  44. package/src/database/migration/20260211171708_add_project_commands.ts +11 -0
  45. package/src/database/migration/20260213144116_wakeful_the_professor.ts +23 -0
  46. package/src/database/migration/20260225215848_workspace.ts +19 -0
  47. package/src/database/migration/20260227213759_add_session_workspace_id.ts +12 -0
  48. package/src/database/migration/20260228203230_blue_harpoon.ts +30 -0
  49. package/src/database/migration/20260303231226_add_workspace_fields.ts +15 -0
  50. package/src/database/migration/20260309230000_move_org_to_state.ts +15 -0
  51. package/src/database/migration/20260312043431_session_message_cursor.ts +16 -0
  52. package/src/database/migration/20260323234822_events.ts +26 -0
  53. package/src/database/migration/20260410174513_workspace-name.ts +29 -0
  54. package/src/database/migration/20260413175956_chief_energizer.ts +24 -0
  55. package/src/database/migration/20260423070820_add_icon_url_override.ts +14 -0
  56. package/src/database/migration/20260427172553_slow_nightmare.ts +30 -0
  57. package/src/database/migration/20260428004200_add_session_path.ts +11 -0
  58. package/src/database/migration/20260501142318_next_venus.ts +12 -0
  59. package/src/database/migration/20260504145000_add_sync_owner.ts +11 -0
  60. package/src/database/migration/20260507164347_add_workspace_time.ts +11 -0
  61. package/src/database/migration/20260510033149_session_usage.ts +56 -0
  62. package/src/database/migration/20260511000411_data_migration_state.ts +16 -0
  63. package/src/database/migration/20260511173437_session-metadata.ts +16 -0
  64. package/src/database/migration/20260601010001_normalize_storage_paths.ts +22 -0
  65. package/src/database/migration/20260601202201_amazing_prowler.ts +11 -0
  66. package/src/database/migration/20260602002951_lowly_union_jack.ts +24 -0
  67. package/src/database/migration/20260602182828_add_project_directories.ts +20 -0
  68. package/src/database/migration/20260603001617_session_message_projection_indexes.ts +19 -0
  69. package/src/database/migration/20260603040000_session_message_projection_order.ts +19 -0
  70. package/src/database/migration/20260603141458_session_input_inbox.ts +25 -0
  71. package/src/database/migration/20260603160727_jittery_ezekiel_stane.ts +20 -0
  72. package/src/database/migration/20260604172448_event_sourced_session_input.ts +47 -0
  73. package/src/database/migration/20260605003541_add_session_context_snapshot.ts +21 -0
  74. package/src/database/migration/20260605042240_add_context_epoch_agent.ts +11 -0
  75. package/src/database/migration/20260611035744_credential.ts +25 -0
  76. package/src/database/migration/20260611192811_lush_chimera.ts +25 -0
  77. package/src/database/migration/20260612174303_project_dir_strategy.ts +29 -0
  78. package/src/database/migration/20260622142730_simplify_session_context_epoch.ts +13 -0
  79. package/src/database/migration/20260622170816_reset_v2_session_state.ts +17 -0
  80. package/src/database/migration/20260622202450_simplify_session_input.ts +17 -0
  81. package/src/database/migration.gen.ts +44 -0
  82. package/src/database/migration.ts +81 -0
  83. package/src/database/path.ts +91 -0
  84. package/src/database/schema.gen.ts +274 -0
  85. package/src/database/schema.sql.ts +10 -0
  86. package/src/database/sqlite.bun.ts +183 -0
  87. package/src/database/sqlite.node.ts +178 -0
  88. package/src/database/sqlite.ts +8 -0
  89. package/src/effect/app-node-builder.ts +23 -0
  90. package/src/effect/app-node-platform.ts +18 -0
  91. package/src/effect/app-node.ts +14 -0
  92. package/src/effect/dfdf +1 -0
  93. package/src/effect/keyed-mutex.ts +45 -0
  94. package/src/effect/layer-node.ts +333 -0
  95. package/src/effect/memo-map.ts +3 -0
  96. package/src/effect/runtime.ts +21 -0
  97. package/src/effect/service-use.ts +43 -0
  98. package/src/event/sql.ts +25 -0
  99. package/src/event.ts +638 -0
  100. package/src/file-mutation.ts +207 -0
  101. package/src/file.ts +6 -0
  102. package/src/filesystem/fff.bun.ts +140 -0
  103. package/src/filesystem/fff.node.ts +138 -0
  104. package/src/filesystem/ignore.ts +67 -0
  105. package/src/filesystem/protected.ts +53 -0
  106. package/src/filesystem/search.ts +239 -0
  107. package/src/filesystem/watcher.ts +140 -0
  108. package/src/filesystem.ts +118 -0
  109. package/src/flag/flag.ts +78 -0
  110. package/src/fs-util.ts +274 -0
  111. package/src/git.ts +987 -0
  112. package/src/github-copilot/README.md +5 -0
  113. package/src/github-copilot/chat/convert-to-openai-compatible-chat-messages.ts +170 -0
  114. package/src/github-copilot/chat/get-response-metadata.ts +15 -0
  115. package/src/github-copilot/chat/map-openai-compatible-finish-reason.ts +19 -0
  116. package/src/github-copilot/chat/openai-compatible-api-types.ts +64 -0
  117. package/src/github-copilot/chat/openai-compatible-chat-language-model.ts +815 -0
  118. package/src/github-copilot/chat/openai-compatible-chat-options.ts +28 -0
  119. package/src/github-copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
  120. package/src/github-copilot/chat/openai-compatible-prepare-tools.ts +83 -0
  121. package/src/github-copilot/copilot-provider.ts +100 -0
  122. package/src/github-copilot/openai-compatible-error.ts +27 -0
  123. package/src/github-copilot/responses/convert-to-openai-responses-input.ts +335 -0
  124. package/src/github-copilot/responses/map-openai-responses-finish-reason.ts +22 -0
  125. package/src/github-copilot/responses/openai-config.ts +18 -0
  126. package/src/github-copilot/responses/openai-error.ts +22 -0
  127. package/src/github-copilot/responses/openai-responses-api-types.ts +214 -0
  128. package/src/github-copilot/responses/openai-responses-language-model.ts +1770 -0
  129. package/src/github-copilot/responses/openai-responses-prepare-tools.ts +173 -0
  130. package/src/github-copilot/responses/openai-responses-settings.ts +1 -0
  131. package/src/github-copilot/responses/tool/code-interpreter.ts +87 -0
  132. package/src/github-copilot/responses/tool/file-search.ts +127 -0
  133. package/src/github-copilot/responses/tool/image-generation.ts +114 -0
  134. package/src/github-copilot/responses/tool/local-shell.ts +64 -0
  135. package/src/github-copilot/responses/tool/web-search-preview.ts +103 -0
  136. package/src/github-copilot/responses/tool/web-search.ts +102 -0
  137. package/src/global.ts +87 -0
  138. package/src/id/id.ts +47 -0
  139. package/src/image/photon.ts +94 -0
  140. package/src/image.ts +81 -0
  141. package/src/installation/version.ts +8 -0
  142. package/src/instruction-context.ts +101 -0
  143. package/src/integration/connection.ts +12 -0
  144. package/src/integration.ts +520 -0
  145. package/src/location-mutation.ts +162 -0
  146. package/src/location-service-map.ts +18 -0
  147. package/src/location-services.ts +115 -0
  148. package/src/location.ts +39 -0
  149. package/src/markdown.d.ts +4 -0
  150. package/src/model.ts +41 -0
  151. package/src/models-dev.ts +260 -0
  152. package/src/npm-config.ts +40 -0
  153. package/src/npm.ts +269 -0
  154. package/src/oauth/page.ts +276 -0
  155. package/src/observability/logging.ts +71 -0
  156. package/src/observability/otlp.ts +79 -0
  157. package/src/observability/shared.ts +1 -0
  158. package/src/observability.ts +24 -0
  159. package/src/patch.ts +197 -0
  160. package/src/permission/saved.ts +79 -0
  161. package/src/permission/sql.ts +20 -0
  162. package/src/permission.ts +310 -0
  163. package/src/plugin/agent.ts +206 -0
  164. package/src/plugin/command/initialize.txt +65 -0
  165. package/src/plugin/command/review.txt +100 -0
  166. package/src/plugin/command.ts +25 -0
  167. package/src/plugin/host.ts +219 -0
  168. package/src/plugin/internal.ts +153 -0
  169. package/src/plugin/layer-map.example.ts +94 -0
  170. package/src/plugin/models-dev.ts +183 -0
  171. package/src/plugin/promise.ts +93 -0
  172. package/src/plugin/provider/alibaba.ts +15 -0
  173. package/src/plugin/provider/amazon-bedrock.ts +126 -0
  174. package/src/plugin/provider/anthropic.ts +27 -0
  175. package/src/plugin/provider/azure.ts +82 -0
  176. package/src/plugin/provider/cerebras.ts +26 -0
  177. package/src/plugin/provider/cloudflare-ai-gateway.ts +81 -0
  178. package/src/plugin/provider/cloudflare-workers-ai.ts +81 -0
  179. package/src/plugin/provider/cohere.ts +15 -0
  180. package/src/plugin/provider/deepinfra.ts +15 -0
  181. package/src/plugin/provider/dynamic.ts +31 -0
  182. package/src/plugin/provider/gateway.ts +15 -0
  183. package/src/plugin/provider/github-copilot.ts +52 -0
  184. package/src/plugin/provider/gitlab.ts +65 -0
  185. package/src/plugin/provider/google-vertex.ts +173 -0
  186. package/src/plugin/provider/google.ts +15 -0
  187. package/src/plugin/provider/groq.ts +15 -0
  188. package/src/plugin/provider/kilo.ts +21 -0
  189. package/src/plugin/provider/llmgateway.ts +26 -0
  190. package/src/plugin/provider/mistral.ts +15 -0
  191. package/src/plugin/provider/nvidia.ts +22 -0
  192. package/src/plugin/provider/openai-compatible.ts +17 -0
  193. package/src/plugin/provider/openai.ts +292 -0
  194. package/src/plugin/provider/opencode.ts +311 -0
  195. package/src/plugin/provider/openrouter.ts +36 -0
  196. package/src/plugin/provider/perplexity.ts +15 -0
  197. package/src/plugin/provider/sap-ai-core.ts +46 -0
  198. package/src/plugin/provider/snowflake-cortex.ts +89 -0
  199. package/src/plugin/provider/togetherai.ts +15 -0
  200. package/src/plugin/provider/venice.ts +15 -0
  201. package/src/plugin/provider/vercel.ts +27 -0
  202. package/src/plugin/provider/xai.ts +22 -0
  203. package/src/plugin/provider/zenmux.ts +21 -0
  204. package/src/plugin/provider.ts +71 -0
  205. package/src/plugin/skill/customize-neurocode.md +452 -0
  206. package/src/plugin/skill.ts +31 -0
  207. package/src/plugin/variant.ts +39 -0
  208. package/src/plugin.ts +167 -0
  209. package/src/policy.ts +49 -0
  210. package/src/process.ts +261 -0
  211. package/src/project/copy-strategies.ts +35 -0
  212. package/src/project/copy.ts +292 -0
  213. package/src/project/directories.ts +158 -0
  214. package/src/project/schema.ts +16 -0
  215. package/src/project/sql.ts +35 -0
  216. package/src/project.ts +136 -0
  217. package/src/provider.ts +25 -0
  218. package/src/pty/protocol.ts +37 -0
  219. package/src/pty/pty.bun.ts +26 -0
  220. package/src/pty/pty.node.ts +29 -0
  221. package/src/pty/pty.ts +25 -0
  222. package/src/pty/schema.ts +1 -0
  223. package/src/pty/ticket.ts +56 -0
  224. package/src/pty.ts +318 -0
  225. package/src/public-event-manifest.ts +7 -0
  226. package/src/question.ts +153 -0
  227. package/src/reference/guidance.ts +69 -0
  228. package/src/reference.ts +125 -0
  229. package/src/repository-cache.ts +259 -0
  230. package/src/repository.ts +214 -0
  231. package/src/ripgrep/binary.ts +132 -0
  232. package/src/ripgrep.ts +281 -0
  233. package/src/schema.ts +79 -0
  234. package/src/session/compaction.ts +241 -0
  235. package/src/session/context-epoch.ts +174 -0
  236. package/src/session/error.ts +24 -0
  237. package/src/session/event.ts +2 -0
  238. package/src/session/execution/local.ts +46 -0
  239. package/src/session/execution.ts +34 -0
  240. package/src/session/history.ts +101 -0
  241. package/src/session/info.ts +50 -0
  242. package/src/session/input.ts +288 -0
  243. package/src/session/message-updater.ts +397 -0
  244. package/src/session/message.ts +2 -0
  245. package/src/session/projector.ts +458 -0
  246. package/src/session/prompt.ts +1 -0
  247. package/src/session/revert.ts +121 -0
  248. package/src/session/run-coordinator.ts +104 -0
  249. package/src/session/runner/index.ts +28 -0
  250. package/src/session/runner/llm.ts +432 -0
  251. package/src/session/runner/max-steps.ts +16 -0
  252. package/src/session/runner/model.ts +218 -0
  253. package/src/session/runner/publish-llm-event.ts +423 -0
  254. package/src/session/runner/to-llm-message.ts +171 -0
  255. package/src/session/schema.ts +9 -0
  256. package/src/session/sql.ts +176 -0
  257. package/src/session/store.ts +63 -0
  258. package/src/session/todo.ts +78 -0
  259. package/src/session.ts +486 -0
  260. package/src/share/sql.ts +13 -0
  261. package/src/shell.ts +226 -0
  262. package/src/skill/discovery.ts +213 -0
  263. package/src/skill/guidance.ts +76 -0
  264. package/src/skill.ts +132 -0
  265. package/src/snapshot.ts +266 -0
  266. package/src/state.ts +128 -0
  267. package/src/system-context/builtins.ts +50 -0
  268. package/src/system-context/index.ts +320 -0
  269. package/src/system-context/registry.ts +49 -0
  270. package/src/tool/AGENTS.md +59 -0
  271. package/src/tool/application-tools.ts +57 -0
  272. package/src/tool/apply-patch.ts +219 -0
  273. package/src/tool/bash.ts +207 -0
  274. package/src/tool/builtins.ts +48 -0
  275. package/src/tool/edit.ts +223 -0
  276. package/src/tool/glob.ts +105 -0
  277. package/src/tool/grep.ts +137 -0
  278. package/src/tool/http-body.ts +30 -0
  279. package/src/tool/question.ts +94 -0
  280. package/src/tool/read-filesystem.ts +366 -0
  281. package/src/tool/read.ts +117 -0
  282. package/src/tool/registry.ts +147 -0
  283. package/src/tool/skill.ts +109 -0
  284. package/src/tool/todowrite.ts +62 -0
  285. package/src/tool/tool.ts +162 -0
  286. package/src/tool/tools.ts +13 -0
  287. package/src/tool/webfetch.ts +218 -0
  288. package/src/tool/websearch.ts +260 -0
  289. package/src/tool/write.ts +101 -0
  290. package/src/tool-output-store.ts +211 -0
  291. package/src/util/array.ts +10 -0
  292. package/src/util/binary.ts +41 -0
  293. package/src/util/effect-flock.ts +284 -0
  294. package/src/util/encode.ts +51 -0
  295. package/src/util/error.ts +70 -0
  296. package/src/util/flock.ts +358 -0
  297. package/src/util/glob.ts +34 -0
  298. package/src/util/hash.ts +11 -0
  299. package/src/util/identifier.ts +1 -0
  300. package/src/util/iife.ts +3 -0
  301. package/src/util/lazy.ts +11 -0
  302. package/src/util/module.ts +10 -0
  303. package/src/util/path.ts +37 -0
  304. package/src/util/retry.ts +42 -0
  305. package/src/util/slug.ts +74 -0
  306. package/src/util/token.ts +5 -0
  307. package/src/util/which.ts +14 -0
  308. package/src/util/wildcard.ts +14 -0
  309. package/src/v1/config/agent.ts +89 -0
  310. package/src/v1/config/attachment.ts +25 -0
  311. package/src/v1/config/command.ts +13 -0
  312. package/src/v1/config/config.ts +192 -0
  313. package/src/v1/config/console-state.ts +16 -0
  314. package/src/v1/config/error.ts +39 -0
  315. package/src/v1/config/formatter.ts +13 -0
  316. package/src/v1/config/layout.ts +6 -0
  317. package/src/v1/config/lsp.ts +80 -0
  318. package/src/v1/config/mcp.ts +63 -0
  319. package/src/v1/config/migrate.ts +254 -0
  320. package/src/v1/config/permission.ts +50 -0
  321. package/src/v1/config/plugin.ts +9 -0
  322. package/src/v1/config/provider-options.ts +227 -0
  323. package/src/v1/config/provider.ts +121 -0
  324. package/src/v1/config/server.ts +19 -0
  325. package/src/v1/config/skills.ts +13 -0
  326. package/src/v1/permission.ts +33 -0
  327. package/src/v1/session.ts +68 -0
  328. package/src/v2-schema.ts +3 -0
  329. package/src/workspace.ts +6 -0
  330. package/sst-env.d.ts +10 -0
  331. package/test/agent.test.ts +131 -0
  332. package/test/application-tools.test.ts +287 -0
  333. package/test/background-job.test.ts +106 -0
  334. package/test/catalog.test.ts +353 -0
  335. package/test/command.test.ts +56 -0
  336. package/test/config/agent.test.ts +349 -0
  337. package/test/config/command.test.ts +83 -0
  338. package/test/config/config.test.ts +798 -0
  339. package/test/config/fixtures/plugin/directory-plugin.ts +13 -0
  340. package/test/config/plugin.test.ts +248 -0
  341. package/test/config/provider-options.test.ts +224 -0
  342. package/test/config/provider.test.ts +269 -0
  343. package/test/config/skill.test.ts +80 -0
  344. package/test/credential.test.ts +36 -0
  345. package/test/database-migration.test.ts +644 -0
  346. package/test/effect/cross-spawn-spawner.test.ts +426 -0
  347. package/test/effect/keyed-mutex.test.ts +73 -0
  348. package/test/effect/layer-node/layer-node-types.test.ts +143 -0
  349. package/test/effect/layer-node/layer-node.test.ts +261 -0
  350. package/test/effect/layer-node/node-build.test.ts +125 -0
  351. package/test/effect/observability.test.ts +109 -0
  352. package/test/event.test.ts +1124 -0
  353. package/test/file-mutation.test.ts +368 -0
  354. package/test/filesystem/filesystem.test.ts +387 -0
  355. package/test/filesystem/ignore.test.ts +10 -0
  356. package/test/filesystem/search.test.ts +44 -0
  357. package/test/filesystem/watcher.test.ts +266 -0
  358. package/test/fixture/effect-flock-worker.ts +60 -0
  359. package/test/fixture/flock-worker.ts +72 -0
  360. package/test/fixture/git.ts +49 -0
  361. package/test/fixture/location.ts +26 -0
  362. package/test/fixture/tmpdir.ts +25 -0
  363. package/test/fixtures/recordings/session-runner/openai-chat-streams-text.json +27 -0
  364. package/test/git.test.ts +164 -0
  365. package/test/github-copilot/convert-to-copilot-messages.test.ts +523 -0
  366. package/test/github-copilot/copilot-chat-model.test.ts +592 -0
  367. package/test/github-copilot/openai-responses-language-model.test.ts +206 -0
  368. package/test/global.test.ts +16 -0
  369. package/test/instruction-context.test.ts +323 -0
  370. package/test/integration.test.ts +349 -0
  371. package/test/legacy-event-schema.test.ts +16 -0
  372. package/test/lib/effect.ts +53 -0
  373. package/test/lib/tool.ts +20 -0
  374. package/test/location-filesystem.test.ts +69 -0
  375. package/test/location-layer.test.ts +226 -0
  376. package/test/location-mutation.test.ts +178 -0
  377. package/test/location.test.ts +42 -0
  378. package/test/model.test.ts +23 -0
  379. package/test/models.test.ts +290 -0
  380. package/test/move-session.test.ts +235 -0
  381. package/test/npm-config.test.ts +51 -0
  382. package/test/npm.test.ts +84 -0
  383. package/test/oauth-page.test.ts +15 -0
  384. package/test/patch.test.ts +68 -0
  385. package/test/permission.test.ts +315 -0
  386. package/test/plugin/command.test.ts +47 -0
  387. package/test/plugin/fixture.ts +52 -0
  388. package/test/plugin/fixtures/config-effect-plugin.ts +15 -0
  389. package/test/plugin/fixtures/config-promise-plugin.ts +13 -0
  390. package/test/plugin/fixtures/invalid-plugin.ts +1 -0
  391. package/test/plugin/fixtures/models-dev.json +14 -0
  392. package/test/plugin/fixtures/provider-factory.ts +9 -0
  393. package/test/plugin/host.ts +303 -0
  394. package/test/plugin/models-dev.test.ts +170 -0
  395. package/test/plugin/promise.test.ts +67 -0
  396. package/test/plugin/provider-alibaba.test.ts +92 -0
  397. package/test/plugin/provider-amazon-bedrock.test.ts +622 -0
  398. package/test/plugin/provider-anthropic.test.ts +94 -0
  399. package/test/plugin/provider-azure-cognitive-services.test.ts +199 -0
  400. package/test/plugin/provider-azure.test.ts +293 -0
  401. package/test/plugin/provider-cerebras.test.ts +136 -0
  402. package/test/plugin/provider-cloudflare-ai-gateway.test.ts +426 -0
  403. package/test/plugin/provider-cloudflare-workers-ai.test.ts +279 -0
  404. package/test/plugin/provider-cohere.test.ts +123 -0
  405. package/test/plugin/provider-deepinfra.test.ts +159 -0
  406. package/test/plugin/provider-dynamic.test.ts +184 -0
  407. package/test/plugin/provider-gateway.test.ts +115 -0
  408. package/test/plugin/provider-github-copilot.test.ts +278 -0
  409. package/test/plugin/provider-gitlab.test.ts +308 -0
  410. package/test/plugin/provider-google-vertex-anthropic.test.ts +264 -0
  411. package/test/plugin/provider-google-vertex.test.ts +387 -0
  412. package/test/plugin/provider-google.test.ts +80 -0
  413. package/test/plugin/provider-groq.test.ts +121 -0
  414. package/test/plugin/provider-kilo.test.ts +99 -0
  415. package/test/plugin/provider-llmgateway.test.ts +80 -0
  416. package/test/plugin/provider-mistral.test.ts +129 -0
  417. package/test/plugin/provider-nvidia.test.ts +97 -0
  418. package/test/plugin/provider-openai-compatible.test.ts +107 -0
  419. package/test/plugin/provider-openai.test.ts +176 -0
  420. package/test/plugin/provider-opencode.test.ts +423 -0
  421. package/test/plugin/provider-openrouter.test.ts +113 -0
  422. package/test/plugin/provider-perplexity.test.ts +122 -0
  423. package/test/plugin/provider-sap-ai-core.test.ts +159 -0
  424. package/test/plugin/provider-snowflake-cortex.test.ts +261 -0
  425. package/test/plugin/provider-togetherai.test.ts +129 -0
  426. package/test/plugin/provider-venice.test.ts +115 -0
  427. package/test/plugin/provider-vercel.test.ts +83 -0
  428. package/test/plugin/provider-xai.test.ts +125 -0
  429. package/test/plugin/provider-zenmux.test.ts +110 -0
  430. package/test/plugin/skill.test.ts +25 -0
  431. package/test/plugin/variant.test.ts +67 -0
  432. package/test/plugin.test.ts +71 -0
  433. package/test/policy.test.ts +85 -0
  434. package/test/preload.ts +5 -0
  435. package/test/process/process.test.ts +368 -0
  436. package/test/project-copy.test.ts +400 -0
  437. package/test/project-directories.test.ts +61 -0
  438. package/test/project.test.ts +221 -0
  439. package/test/provider-mistral.test.ts +282 -0
  440. package/test/provider-xai-responses.test.ts +32 -0
  441. package/test/pty/info-schema.test.ts +32 -0
  442. package/test/pty/protocol.test.ts +27 -0
  443. package/test/pty/pty-session.test.ts +240 -0
  444. package/test/pty/ticket.test.ts +62 -0
  445. package/test/question.test.ts +114 -0
  446. package/test/reference-guidance.test.ts +76 -0
  447. package/test/reference.test.ts +78 -0
  448. package/test/repository-cache.test.ts +153 -0
  449. package/test/repository.test.ts +71 -0
  450. package/test/ripgrep.test.ts +65 -0
  451. package/test/session-compaction.test.ts +27 -0
  452. package/test/session-create.test.ts +425 -0
  453. package/test/session-history.test.ts +165 -0
  454. package/test/session-projector.test.ts +533 -0
  455. package/test/session-prompt.test.ts +584 -0
  456. package/test/session-run-coordinator.test.ts +418 -0
  457. package/test/session-runner-message.test.ts +501 -0
  458. package/test/session-runner-model.test.ts +347 -0
  459. package/test/session-runner-recorded.test.ts +193 -0
  460. package/test/session-runner-tool-events.test.ts +136 -0
  461. package/test/session-runner-tool-registry.test.ts +452 -0
  462. package/test/session-runner.test.ts +3365 -0
  463. package/test/session-todo.test.ts +94 -0
  464. package/test/session-tool-progress.test.ts +158 -0
  465. package/test/shared-schema.test.ts +206 -0
  466. package/test/shell.test.ts +108 -0
  467. package/test/skill/guidance.test.ts +144 -0
  468. package/test/skill-discovery.test.ts +165 -0
  469. package/test/skill.test.ts +125 -0
  470. package/test/snapshot.test.ts +178 -0
  471. package/test/state.test.ts +115 -0
  472. package/test/system-context/builtins.test.ts +129 -0
  473. package/test/system-context/index.test.ts +307 -0
  474. package/test/system-context/registry.test.ts +114 -0
  475. package/test/tool-apply-patch.test.ts +436 -0
  476. package/test/tool-bash.test.ts +437 -0
  477. package/test/tool-edit.test.ts +434 -0
  478. package/test/tool-output-store.test.ts +247 -0
  479. package/test/tool-question.test.ts +162 -0
  480. package/test/tool-read-filesystem.test.ts +118 -0
  481. package/test/tool-read.test.ts +674 -0
  482. package/test/tool-skill.test.ts +149 -0
  483. package/test/tool-todowrite.test.ts +125 -0
  484. package/test/tool-webfetch.test.ts +281 -0
  485. package/test/tool-websearch.test.ts +316 -0
  486. package/test/tool-write.test.ts +305 -0
  487. package/test/util/effect-flock.test.ts +392 -0
  488. package/test/util/flock.test.ts +428 -0
  489. package/test/util/which.test.ts +100 -0
  490. package/tsconfig.json +7 -0
package/src/git.ts ADDED
@@ -0,0 +1,987 @@
1
+ export * as Git from "./git"
2
+
3
+ import path from "path"
4
+ import { randomUUID } from "crypto"
5
+ import { Context, Effect, Layer, Schema, Stream } from "effect"
6
+ import { ChildProcess } from "effect/unstable/process"
7
+ import { AbsolutePath, RelativePath } from "./schema"
8
+ import { FSUtil } from "./fs-util"
9
+ import { AppProcess } from "./process"
10
+ import { makeGlobalNode } from "./effect/app-node"
11
+ import { File } from "./file"
12
+ import { KeyedMutex } from "./effect/keyed-mutex"
13
+
14
+ export class Repository extends Schema.Class<Repository>("Git.Repository")({
15
+ worktree: AbsolutePath,
16
+ gitDirectory: AbsolutePath,
17
+ commonDirectory: AbsolutePath,
18
+ }) {}
19
+
20
+ export const ChangeSet = Schema.String.pipe(Schema.brand("Git.ChangeSet"))
21
+ export type ChangeSet = typeof ChangeSet.Type
22
+
23
+ export const TreeID = Schema.String.pipe(Schema.brand("Git.TreeID"))
24
+ export type TreeID = typeof TreeID.Type
25
+
26
+ export class OperationError extends Schema.TaggedErrorClass<OperationError>()("Git.OperationError", {
27
+ operation: Schema.Literals([
28
+ "clone",
29
+ "fetch",
30
+ "checkout",
31
+ "reset",
32
+ "create",
33
+ "refresh",
34
+ "write_tree",
35
+ "list_files",
36
+ "diff",
37
+ "restore",
38
+ ]),
39
+ message: Schema.String,
40
+ directory: Schema.optional(AbsolutePath),
41
+ cause: Schema.optional(Schema.Defect()),
42
+ }) {}
43
+
44
+ export class Worktree extends Schema.Class<Worktree>("Git.Worktree")({
45
+ directory: AbsolutePath,
46
+ kind: Schema.Literals(["main", "linked"]),
47
+ }) {}
48
+
49
+ export class WorktreeError extends Schema.TaggedErrorClass<WorktreeError>()("Git.WorktreeError", {
50
+ operation: Schema.Literals(["create", "remove", "list"]),
51
+ message: Schema.String,
52
+ directory: Schema.optional(AbsolutePath),
53
+ forceRequired: Schema.optional(Schema.Boolean),
54
+ cause: Schema.optional(Schema.Defect()),
55
+ }) {}
56
+
57
+ export class PatchError extends Schema.TaggedErrorClass<PatchError>()("Git.PatchError", {
58
+ operation: Schema.Literals(["capture", "apply", "reset"]),
59
+ directory: AbsolutePath,
60
+ message: Schema.String,
61
+ cause: Schema.optional(Schema.Defect()),
62
+ }) {}
63
+
64
+ export interface Interface {
65
+ readonly repo: {
66
+ readonly discover: (input: AbsolutePath) => Effect.Effect<Repository | undefined>
67
+ readonly clone: (input: {
68
+ remote: string
69
+ directory: AbsolutePath
70
+ branch?: string
71
+ depth?: number
72
+ }) => Effect.Effect<Repository, OperationError>
73
+ readonly create: (input: {
74
+ worktree: AbsolutePath
75
+ gitDirectory: AbsolutePath
76
+ seed?: Repository
77
+ }) => Effect.Effect<Repository, OperationError>
78
+ }
79
+ readonly remote: {
80
+ readonly get: (repository: Repository, name?: string) => Effect.Effect<string | undefined>
81
+ }
82
+ readonly history: {
83
+ readonly head: (repository: Repository) => Effect.Effect<string | undefined>
84
+ readonly branch: (repository: Repository) => Effect.Effect<string | undefined>
85
+ readonly defaultRemoteBranch: (repository: Repository, remote?: string) => Effect.Effect<string | undefined>
86
+ readonly rootCommits: (repository: Repository) => Effect.Effect<readonly string[]>
87
+ }
88
+ readonly sync: {
89
+ readonly fetchRemotes: (repository: Repository, input?: { prune?: boolean }) => Effect.Effect<void, OperationError>
90
+ readonly fetchBranch: (
91
+ repository: Repository,
92
+ input: { remote?: string; branch: string; force?: boolean },
93
+ ) => Effect.Effect<void, OperationError>
94
+ readonly checkoutRemoteBranch: (
95
+ repository: Repository,
96
+ input: { remote?: string; branch: string; reset?: boolean },
97
+ ) => Effect.Effect<void, OperationError>
98
+ readonly resetHard: (repository: Repository, revision: string) => Effect.Effect<void, OperationError>
99
+ }
100
+ readonly change: {
101
+ readonly capture: (input: { repository: Repository; path: AbsolutePath }) => Effect.Effect<ChangeSet, PatchError>
102
+ readonly apply: (input: {
103
+ repository: Repository
104
+ path: AbsolutePath
105
+ changes: ChangeSet
106
+ }) => Effect.Effect<void, PatchError>
107
+ readonly discard: (input: {
108
+ repository: Repository
109
+ path: AbsolutePath
110
+ index: "preserve" | "reset"
111
+ untracked: "preserve" | "remove"
112
+ }) => Effect.Effect<void, PatchError>
113
+ }
114
+ readonly worktree: {
115
+ readonly create: (input: {
116
+ repository: Repository
117
+ directory: AbsolutePath
118
+ }) => Effect.Effect<Repository, WorktreeError>
119
+ readonly remove: (input: {
120
+ repository: Repository
121
+ directory: AbsolutePath
122
+ force: boolean
123
+ }) => Effect.Effect<void, WorktreeError>
124
+ readonly list: (repository: Repository) => Effect.Effect<readonly Worktree[], WorktreeError>
125
+ }
126
+ readonly index: {
127
+ /** Refresh only the requested project-relative scope, preserving all other entries. */
128
+ readonly refresh: (input: {
129
+ repository: Repository
130
+ scope: RelativePath
131
+ ignores?: Repository
132
+ maximumUntrackedFileBytes?: number
133
+ }) => Effect.Effect<{ readonly skipped: readonly RelativePath[] }, OperationError>
134
+ readonly ignored: (input: {
135
+ repository: Repository
136
+ paths: readonly RelativePath[]
137
+ }) => Effect.Effect<ReadonlySet<RelativePath>, OperationError>
138
+ }
139
+ readonly tree: {
140
+ readonly capture: (input: {
141
+ repository: Repository
142
+ scopes: readonly RelativePath[]
143
+ ignores?: Repository
144
+ maximumUntrackedFileBytes?: number
145
+ }) => Effect.Effect<TreeID, OperationError>
146
+ readonly write: (repository: Repository) => Effect.Effect<TreeID, OperationError>
147
+ readonly files: (input: {
148
+ repository: Repository
149
+ from: TreeID
150
+ to: TreeID
151
+ }) => Effect.Effect<readonly RelativePath[], OperationError>
152
+ readonly diff: (input: {
153
+ repository: Repository
154
+ from: TreeID
155
+ to: TreeID
156
+ context?: number
157
+ paths?: readonly RelativePath[]
158
+ }) => Effect.Effect<readonly File.Diff[], OperationError>
159
+ readonly preview: (input: {
160
+ repository: Repository
161
+ current: TreeID
162
+ files: ReadonlyMap<RelativePath, TreeID>
163
+ context?: number
164
+ }) => Effect.Effect<readonly File.Diff[], OperationError>
165
+ readonly restore: (input: {
166
+ repository: Repository
167
+ files: ReadonlyMap<RelativePath, TreeID>
168
+ }) => Effect.Effect<void, OperationError>
169
+ readonly checkout: (input: { repository: Repository; tree: TreeID }) => Effect.Effect<void, OperationError>
170
+ }
171
+ }
172
+
173
+ export class Service extends Context.Service<Service, Interface>()("@neurocode/GitV2") {}
174
+
175
+ const layer = Layer.effect(
176
+ Service,
177
+ Effect.gen(function* () {
178
+ const fs = yield* FSUtil.Service
179
+ const proc = yield* AppProcess.Service
180
+ const locks = KeyedMutex.makeUnsafe<string>()
181
+ const locked = <A, E, R>(repository: Repository, effect: Effect.Effect<A, E, R>) =>
182
+ locks.withLock(repository.gitDirectory)(effect)
183
+
184
+ const discover = Effect.fn("Git.repo.discover")(function* (input: AbsolutePath) {
185
+ const dotgit = yield* fs.up({ targets: [".git"], start: input }).pipe(
186
+ Effect.map((matches) => matches[0]),
187
+ Effect.catch(() => Effect.succeed(undefined)),
188
+ )
189
+ if (!dotgit) return undefined
190
+
191
+ const cwd = path.dirname(dotgit)
192
+ const git = run(cwd, proc)
193
+ const topLevel = yield* git(["rev-parse", "--show-toplevel"])
194
+ const gitDir = yield* git(["rev-parse", "--git-dir"])
195
+ const commonDir = yield* git(["rev-parse", "--git-common-dir"])
196
+ if (gitDir.exitCode !== 0 || commonDir.exitCode !== 0) return undefined
197
+
198
+ return new Repository({
199
+ worktree: AbsolutePath.make(topLevel.exitCode === 0 ? resolvePath(cwd, topLevel.text) : cwd),
200
+ gitDirectory: AbsolutePath.make(resolvePath(cwd, gitDir.text)),
201
+ commonDirectory: AbsolutePath.make(resolvePath(cwd, commonDir.text)),
202
+ })
203
+ })
204
+
205
+ const remote = Effect.fn("Git.remote.get")(function* (repository: Repository, name = "origin") {
206
+ const result = yield* run(repository.worktree, proc)(["remote", "get-url", name])
207
+ if (result.exitCode !== 0) return undefined
208
+ return result.text.trim() || undefined
209
+ })
210
+
211
+ const roots = Effect.fn("Git.history.rootCommits")(function* (repository: Repository) {
212
+ const result = yield* run(repository.worktree, proc)(["rev-list", "--max-parents=0", "HEAD"])
213
+ if (result.exitCode !== 0) return []
214
+ return result.text
215
+ .split("\n")
216
+ .map((item) => item.trim())
217
+ .filter(Boolean)
218
+ .toSorted()
219
+ })
220
+
221
+ const head = Effect.fn("Git.history.head")(function* (repository: Repository) {
222
+ const result = yield* run(repository.worktree, proc)(["rev-parse", "HEAD"])
223
+ if (result.exitCode !== 0) return undefined
224
+ return result.text.trim() || undefined
225
+ })
226
+
227
+ const branch = Effect.fn("Git.history.branch")(function* (repository: Repository) {
228
+ const result = yield* run(repository.worktree, proc)(["symbolic-ref", "--quiet", "--short", "HEAD"])
229
+ if (result.exitCode !== 0) return undefined
230
+ return result.text.trim() || undefined
231
+ })
232
+
233
+ const remoteHead = Effect.fn("Git.history.defaultRemoteBranch")(function* (
234
+ repository: Repository,
235
+ remoteName = "origin",
236
+ ) {
237
+ const result = yield* run(repository.worktree, proc)(["symbolic-ref", `refs/remotes/${remoteName}/HEAD`])
238
+ if (result.exitCode !== 0) return undefined
239
+ return result.text.trim().replace(new RegExp(`^refs/remotes/${remoteName}/`), "") || undefined
240
+ })
241
+
242
+ const operation = Effect.fnUntraced(function* (
243
+ operation: OperationError["operation"],
244
+ directory: AbsolutePath,
245
+ args: string[],
246
+ ) {
247
+ const result = yield* execute(
248
+ directory,
249
+ proc,
250
+ )(args).pipe(
251
+ Effect.mapError((cause) => new OperationError({ operation, directory, message: cause.message, cause })),
252
+ )
253
+ if (result.exitCode === 0) return
254
+ return yield* new OperationError({
255
+ operation,
256
+ directory,
257
+ message: result.stderr.trim() || result.text.trim() || `Git ${operation} failed`,
258
+ })
259
+ })
260
+
261
+ const clone = Effect.fn("Git.repo.clone")(function* (input: {
262
+ remote: string
263
+ directory: AbsolutePath
264
+ branch?: string
265
+ depth?: number
266
+ }) {
267
+ yield* operation("clone", AbsolutePath.make(path.dirname(input.directory)), [
268
+ "clone",
269
+ "--depth",
270
+ String(input.depth ?? 100),
271
+ ...(input.branch ? ["--branch", input.branch] : []),
272
+ "--",
273
+ input.remote,
274
+ input.directory,
275
+ ])
276
+ const repository = yield* discover(input.directory)
277
+ if (repository) return repository
278
+ return yield* new OperationError({
279
+ operation: "clone",
280
+ directory: input.directory,
281
+ message: "Cloned repository could not be opened",
282
+ })
283
+ })
284
+
285
+ const fetch = Effect.fn("Git.sync.fetchRemotes")(function* (
286
+ repository: Repository,
287
+ input: { prune?: boolean } = {},
288
+ ) {
289
+ yield* operation("fetch", repository.worktree, ["fetch", "--all", ...(input.prune === false ? [] : ["--prune"])])
290
+ })
291
+
292
+ const fetchBranch = Effect.fn("Git.sync.fetchBranch")(function* (
293
+ repository: Repository,
294
+ input: { remote?: string; branch: string; force?: boolean },
295
+ ) {
296
+ const remoteName = input.remote ?? "origin"
297
+ const spec = `refs/heads/${input.branch}:refs/remotes/${remoteName}/${input.branch}`
298
+ yield* operation("fetch", repository.worktree, ["fetch", remoteName, input.force === false ? spec : `+${spec}`])
299
+ })
300
+
301
+ const checkout = Effect.fn("Git.sync.checkoutRemoteBranch")(function* (
302
+ repository: Repository,
303
+ input: { remote?: string; branch: string; reset?: boolean },
304
+ ) {
305
+ const remoteName = input.remote ?? "origin"
306
+ yield* operation("checkout", repository.worktree, [
307
+ "checkout",
308
+ ...(input.reset === false ? [input.branch] : ["-B", input.branch, `${remoteName}/${input.branch}`]),
309
+ ])
310
+ })
311
+
312
+ const reset = Effect.fn("Git.sync.resetHard")(function* (repository: Repository, revision: string) {
313
+ yield* operation("reset", repository.worktree, ["reset", "--hard", revision])
314
+ })
315
+
316
+ const repositoryArgs = (repository: Repository, args: string[]) => [
317
+ "--git-dir",
318
+ repository.gitDirectory,
319
+ "--work-tree",
320
+ repository.worktree,
321
+ ...args,
322
+ ]
323
+
324
+ const repositoryOperation = Effect.fnUntraced(function* (
325
+ operationName: OperationError["operation"],
326
+ repository: Repository,
327
+ args: string[],
328
+ options?: { stdin?: string; env?: Record<string, string> },
329
+ ) {
330
+ const result = yield* proc
331
+ .run(
332
+ ChildProcess.make("git", repositoryArgs(repository, args), {
333
+ cwd: repository.worktree,
334
+ env: options?.env,
335
+ extendEnv: true,
336
+ }),
337
+ { stdin: options?.stdin },
338
+ )
339
+ .pipe(
340
+ Effect.mapError(
341
+ (cause) =>
342
+ new OperationError({
343
+ operation: operationName,
344
+ directory: repository.worktree,
345
+ message: cause.message,
346
+ cause,
347
+ }),
348
+ ),
349
+ )
350
+ const text = result.stdout.toString("utf8")
351
+ if (result.exitCode === 0) return { text, stderr: result.stderr.toString("utf8") }
352
+ return yield* new OperationError({
353
+ operation: operationName,
354
+ directory: repository.worktree,
355
+ message: result.stderr.toString("utf8").trim() || text.trim() || `Git ${operationName} failed`,
356
+ })
357
+ })
358
+
359
+ const create = Effect.fn("Git.repo.create")(function* (input: {
360
+ worktree: AbsolutePath
361
+ gitDirectory: AbsolutePath
362
+ seed?: Repository
363
+ }) {
364
+ yield* fs.ensureDir(input.gitDirectory).pipe(
365
+ Effect.mapError(
366
+ (cause) =>
367
+ new OperationError({
368
+ operation: "create",
369
+ directory: input.gitDirectory,
370
+ message: "Failed to create Git storage",
371
+ cause,
372
+ }),
373
+ ),
374
+ )
375
+ const repository = new Repository({
376
+ worktree: input.worktree,
377
+ gitDirectory: input.gitDirectory,
378
+ commonDirectory: input.gitDirectory,
379
+ })
380
+ yield* repositoryOperation("create", repository, ["init"])
381
+ yield* Effect.forEach(
382
+ [
383
+ ["core.autocrlf", "false"],
384
+ ["core.longpaths", "true"],
385
+ ["core.symlinks", "true"],
386
+ ["core.fsmonitor", "false"],
387
+ ["feature.manyFiles", "true"],
388
+ ["index.version", "4"],
389
+ ["index.threads", "true"],
390
+ ["core.untrackedCache", "true"],
391
+ ],
392
+ ([key, value]) => repositoryOperation("create", repository, ["config", key, value]),
393
+ { discard: true },
394
+ )
395
+ if (!input.seed) return repository
396
+ yield* fs.ensureDir(path.join(input.gitDirectory, "objects", "info")).pipe(
397
+ Effect.mapError(
398
+ (cause) =>
399
+ new OperationError({
400
+ operation: "create",
401
+ directory: input.gitDirectory,
402
+ message: "Failed to configure shared Git objects",
403
+ cause,
404
+ }),
405
+ ),
406
+ )
407
+ yield* fs
408
+ .writeFileString(
409
+ path.join(input.gitDirectory, "objects", "info", "alternates"),
410
+ path.join(input.seed.commonDirectory, "objects") + "\n",
411
+ )
412
+ .pipe(
413
+ Effect.mapError(
414
+ (cause) =>
415
+ new OperationError({
416
+ operation: "create",
417
+ directory: input.gitDirectory,
418
+ message: "Failed to configure shared Git objects",
419
+ cause,
420
+ }),
421
+ ),
422
+ )
423
+ yield* fs
424
+ .copyFile(path.join(input.seed.gitDirectory, "index"), path.join(input.gitDirectory, "index"))
425
+ .pipe(Effect.catch(() => Effect.void))
426
+ return repository
427
+ })
428
+
429
+ const refresh = Effect.fn("Git.index.refresh")(function* (input: {
430
+ repository: Repository
431
+ scope: RelativePath
432
+ ignores?: Repository
433
+ maximumUntrackedFileBytes?: number
434
+ }) {
435
+ const list = (args: string[]) =>
436
+ repositoryOperation("refresh", input.repository, args).pipe(
437
+ Effect.map((result) => result.text.split("\0").filter(Boolean)),
438
+ )
439
+ const [tracked, untracked] = yield* Effect.all(
440
+ [
441
+ list(["diff-files", "--name-only", "-z", "--", input.scope]),
442
+ list(["ls-files", "--others", "--exclude-standard", "-z", "--", input.scope]),
443
+ ],
444
+ { concurrency: 2 },
445
+ )
446
+ const candidates = Array.from(new Set([...tracked, ...untracked]))
447
+ if (!candidates.length) return { skipped: [] }
448
+ const ignored = input.ignores
449
+ ? new Set(
450
+ (yield* repositoryOperation("refresh", input.ignores, ["check-ignore", "--no-index", "--stdin", "-z"], {
451
+ stdin: candidates.join("\0") + "\0",
452
+ }).pipe(Effect.catch(() => Effect.succeed({ text: "", stderr: "" })))).text
453
+ .split("\0")
454
+ .filter(Boolean),
455
+ )
456
+ : new Set<string>()
457
+ const allowed = candidates.filter((item) => !ignored.has(item))
458
+ const maximum = input.maximumUntrackedFileBytes
459
+ const skipped = maximum
460
+ ? (yield* Effect.forEach(
461
+ untracked.filter((item) => allowed.includes(item)),
462
+ (item) =>
463
+ fs.stat(path.join(input.repository.worktree, item)).pipe(
464
+ Effect.map((info) =>
465
+ info.type === "File" && Number(info.size) > maximum ? RelativePath.make(item) : undefined,
466
+ ),
467
+ Effect.catch(() => Effect.succeed(undefined)),
468
+ ),
469
+ { concurrency: 8 },
470
+ )).filter((item): item is RelativePath => item !== undefined)
471
+ : []
472
+ const stage = allowed.filter((item) => !skipped.includes(RelativePath.make(item)))
473
+ const remove = [...ignored, ...skipped]
474
+ if (remove.length)
475
+ yield* repositoryOperation(
476
+ "refresh",
477
+ input.repository,
478
+ ["rm", "--cached", "-f", "--ignore-unmatch", "--pathspec-from-file=-", "--pathspec-file-nul"],
479
+ { stdin: remove.join("\0") + "\0" },
480
+ )
481
+ if (stage.length)
482
+ yield* repositoryOperation(
483
+ "refresh",
484
+ input.repository,
485
+ ["add", "--all", "--sparse", "--pathspec-from-file=-", "--pathspec-file-nul"],
486
+ { stdin: stage.join("\0") + "\0" },
487
+ )
488
+ return { skipped }
489
+ })
490
+
491
+ const ignored = Effect.fn("Git.index.ignored")(function* (input: {
492
+ repository: Repository
493
+ paths: readonly RelativePath[]
494
+ }) {
495
+ if (!input.paths.length) return new Set<RelativePath>()
496
+ const result = yield* proc
497
+ .run(
498
+ ChildProcess.make("git", repositoryArgs(input.repository, ["check-ignore", "--no-index", "--stdin", "-z"]), {
499
+ cwd: input.repository.worktree,
500
+ extendEnv: true,
501
+ }),
502
+ { stdin: input.paths.join("\0") + "\0" },
503
+ )
504
+ .pipe(
505
+ Effect.mapError(
506
+ (cause) =>
507
+ new OperationError({
508
+ operation: "list_files",
509
+ directory: input.repository.worktree,
510
+ message: cause.message,
511
+ cause,
512
+ }),
513
+ ),
514
+ )
515
+ if (result.exitCode !== 0 && result.exitCode !== 1)
516
+ return yield* new OperationError({
517
+ operation: "list_files",
518
+ directory: input.repository.worktree,
519
+ message: result.stderr.toString("utf8").trim() || "Failed to check ignored paths",
520
+ })
521
+ return new Set(
522
+ result.stdout
523
+ .toString("utf8")
524
+ .split("\0")
525
+ .filter(Boolean)
526
+ .map((file) => RelativePath.make(file)),
527
+ )
528
+ })
529
+
530
+ const writeTree = Effect.fn("Git.tree.write")(function* (repository: Repository) {
531
+ return TreeID.make((yield* repositoryOperation("write_tree", repository, ["write-tree"])).text.trim())
532
+ })
533
+
534
+ const captureTree = Effect.fn("Git.tree.capture")(
535
+ (input: {
536
+ repository: Repository
537
+ scopes: readonly RelativePath[]
538
+ ignores?: Repository
539
+ maximumUntrackedFileBytes?: number
540
+ }) =>
541
+ locked(
542
+ input.repository,
543
+ Effect.gen(function* () {
544
+ yield* Effect.forEach(input.scopes, (scope) => refresh({ ...input, scope }), { discard: true })
545
+ return yield* writeTree(input.repository)
546
+ }),
547
+ ),
548
+ )
549
+
550
+ const treeFiles = Effect.fn("Git.tree.files")(function* (input: {
551
+ repository: Repository
552
+ from: TreeID
553
+ to: TreeID
554
+ }) {
555
+ return (yield* repositoryOperation("list_files", input.repository, [
556
+ "diff",
557
+ "--name-only",
558
+ "-z",
559
+ input.from,
560
+ input.to,
561
+ ])).text
562
+ .split("\0")
563
+ .filter(Boolean)
564
+ .map((file) => RelativePath.make(file))
565
+ })
566
+
567
+ const treeDiff = Effect.fn("Git.tree.diff")(function* (input: {
568
+ repository: Repository
569
+ from: TreeID
570
+ to: TreeID
571
+ context?: number
572
+ paths?: readonly RelativePath[]
573
+ }) {
574
+ const paths = input.paths ?? (yield* treeFiles(input))
575
+ return yield* Effect.forEach(paths, (file) =>
576
+ Effect.gen(function* () {
577
+ const statusText = (yield* repositoryOperation("diff", input.repository, [
578
+ "diff",
579
+ "--name-status",
580
+ "--no-renames",
581
+ input.from,
582
+ input.to,
583
+ "--",
584
+ file,
585
+ ])).text.trim()
586
+ const status = statusText.startsWith("A") ? "added" : statusText.startsWith("D") ? "deleted" : "modified"
587
+ const stats = (yield* repositoryOperation("diff", input.repository, [
588
+ "diff",
589
+ "--numstat",
590
+ "--no-renames",
591
+ input.from,
592
+ input.to,
593
+ "--",
594
+ file,
595
+ ])).text.split("\t")
596
+ const binary = stats[0] === "-" || stats[1] === "-"
597
+ const patch = binary
598
+ ? ""
599
+ : (yield* repositoryOperation("diff", input.repository, [
600
+ "diff",
601
+ `--unified=${input.context ?? 3}`,
602
+ "--no-renames",
603
+ input.from,
604
+ input.to,
605
+ "--",
606
+ file,
607
+ ])).text
608
+ return {
609
+ path: file,
610
+ status,
611
+ additions: binary ? 0 : Number(stats[0] ?? 0),
612
+ deletions: binary ? 0 : Number(stats[1] ?? 0),
613
+ patch,
614
+ } satisfies File.Diff
615
+ }),
616
+ )
617
+ })
618
+
619
+ const entry = Effect.fnUntraced(function* (repository: Repository, tree: TreeID, file: RelativePath) {
620
+ const text = (yield* repositoryOperation("restore", repository, [
621
+ "ls-tree",
622
+ "-z",
623
+ tree,
624
+ "--",
625
+ file,
626
+ ])).text.replace(/\0$/, "")
627
+ if (!text) return
628
+ const match = text.match(/^(\d+)\s+\w+\s+([0-9a-f]+)\t/)
629
+ if (!match)
630
+ return yield* new OperationError({
631
+ operation: "restore",
632
+ directory: repository.worktree,
633
+ message: `Invalid tree entry for ${file}`,
634
+ })
635
+ return { mode: match[1], object: match[2] }
636
+ })
637
+
638
+ const preview = Effect.fn("Git.tree.preview")(
639
+ (input: {
640
+ repository: Repository
641
+ current: TreeID
642
+ files: ReadonlyMap<RelativePath, TreeID>
643
+ context?: number
644
+ }) =>
645
+ locked(
646
+ input.repository,
647
+ Effect.gen(function* () {
648
+ const index = path.join(input.repository.gitDirectory, `preview-${randomUUID()}.index`)
649
+ const env = { GIT_INDEX_FILE: index }
650
+ return yield* Effect.gen(function* () {
651
+ yield* repositoryOperation("diff", input.repository, ["read-tree", input.current], { env })
652
+ yield* Effect.forEach(
653
+ input.files,
654
+ ([file, tree]) =>
655
+ Effect.gen(function* () {
656
+ const source = yield* entry(input.repository, tree, file)
657
+ if (!source) {
658
+ yield* repositoryOperation(
659
+ "diff",
660
+ input.repository,
661
+ ["update-index", "--force-remove", "--", file],
662
+ { env },
663
+ )
664
+ return
665
+ }
666
+ yield* repositoryOperation(
667
+ "diff",
668
+ input.repository,
669
+ ["update-index", "--add", "--cacheinfo", source.mode, source.object, file],
670
+ { env },
671
+ )
672
+ }),
673
+ { discard: true },
674
+ )
675
+ const target = TreeID.make(
676
+ (yield* repositoryOperation("diff", input.repository, ["write-tree"], { env })).text.trim(),
677
+ )
678
+ return yield* treeDiff({
679
+ repository: input.repository,
680
+ from: input.current,
681
+ to: target,
682
+ context: input.context,
683
+ paths: Array.from(input.files.keys()),
684
+ })
685
+ }).pipe(Effect.ensuring(fs.remove(index).pipe(Effect.catch(() => Effect.void))))
686
+ }),
687
+ ),
688
+ )
689
+
690
+ const restore = Effect.fn("Git.tree.restore")(
691
+ (input: { repository: Repository; files: ReadonlyMap<RelativePath, TreeID> }) =>
692
+ locked(
693
+ input.repository,
694
+ Effect.forEach(
695
+ input.files,
696
+ ([file, tree]) =>
697
+ Effect.gen(function* () {
698
+ if (yield* entry(input.repository, tree, file)) {
699
+ yield* repositoryOperation("restore", input.repository, ["checkout", tree, "--", file])
700
+ return
701
+ }
702
+ yield* fs.remove(path.join(input.repository.worktree, file), { recursive: true, force: true }).pipe(
703
+ Effect.mapError(
704
+ (cause) =>
705
+ new OperationError({
706
+ operation: "restore",
707
+ directory: input.repository.worktree,
708
+ message: `Failed to remove ${file}`,
709
+ cause,
710
+ }),
711
+ ),
712
+ )
713
+ }),
714
+ { discard: true },
715
+ ),
716
+ ),
717
+ )
718
+
719
+ const checkoutTree = Effect.fn("Git.tree.checkout")((input: { repository: Repository; tree: TreeID }) =>
720
+ locked(
721
+ input.repository,
722
+ Effect.gen(function* () {
723
+ yield* repositoryOperation("restore", input.repository, ["read-tree", input.tree])
724
+ yield* repositoryOperation("restore", input.repository, ["checkout-index", "--all", "--force"])
725
+ }),
726
+ ),
727
+ )
728
+
729
+ const capture = Effect.fn("Git.change.capture")(function* (input: { repository: Repository; path: AbsolutePath }) {
730
+ const scope = path.relative(input.repository.worktree, input.path).replaceAll("\\", "/") || "."
731
+ const tracked = yield* execute(
732
+ input.repository.worktree,
733
+ proc,
734
+ )(["diff", "--binary", "HEAD", "--", scope]).pipe(
735
+ Effect.mapError(
736
+ (cause) => new PatchError({ operation: "capture", directory: input.path, message: cause.message, cause }),
737
+ ),
738
+ )
739
+ if (tracked.exitCode !== 0) {
740
+ return yield* new PatchError({
741
+ operation: "capture",
742
+ directory: input.path,
743
+ message: tracked.stderr.trim() || tracked.text.trim() || "Failed to capture tracked changes",
744
+ })
745
+ }
746
+
747
+ const untracked = yield* execute(
748
+ input.repository.worktree,
749
+ proc,
750
+ )(["ls-files", "--others", "--exclude-standard", "-z", "--", scope]).pipe(
751
+ Effect.mapError(
752
+ (cause) => new PatchError({ operation: "capture", directory: input.path, message: cause.message, cause }),
753
+ ),
754
+ )
755
+ if (untracked.exitCode !== 0) {
756
+ return yield* new PatchError({
757
+ operation: "capture",
758
+ directory: input.path,
759
+ message: untracked.stderr.trim() || untracked.text.trim() || "Failed to list untracked changes",
760
+ })
761
+ }
762
+
763
+ const created = yield* Effect.forEach(untracked.text.split("\0").filter(Boolean), (file) =>
764
+ execute(
765
+ input.repository.worktree,
766
+ proc,
767
+ )(["diff", "--binary", "--no-index", "--", "/dev/null", file]).pipe(
768
+ Effect.mapError(
769
+ (cause) => new PatchError({ operation: "capture", directory: input.path, message: cause.message, cause }),
770
+ ),
771
+ Effect.flatMap((result) =>
772
+ // git diff --no-index returns 1 when differences were found.
773
+ result.exitCode === 0 || result.exitCode === 1
774
+ ? Effect.succeed(result.text)
775
+ : Effect.fail(
776
+ new PatchError({
777
+ operation: "capture",
778
+ directory: input.path,
779
+ message:
780
+ result.stderr.trim() || result.text.trim() || `Failed to capture untracked change: ${file}`,
781
+ }),
782
+ ),
783
+ ),
784
+ ),
785
+ )
786
+ return ChangeSet.make([tracked.text, ...created].filter(Boolean).join("\n"))
787
+ })
788
+
789
+ const apply = Effect.fn("Git.change.apply")(function* (input: {
790
+ repository: Repository
791
+ path: AbsolutePath
792
+ changes: ChangeSet
793
+ }) {
794
+ const result = yield* proc
795
+ .run(
796
+ ChildProcess.make("git", ["apply", "-"], {
797
+ cwd: input.path,
798
+ extendEnv: true,
799
+ stdin: Stream.make(new TextEncoder().encode(input.changes)),
800
+ }),
801
+ )
802
+ .pipe(
803
+ Effect.mapError(
804
+ (cause) => new PatchError({ operation: "apply", directory: input.path, message: cause.message, cause }),
805
+ ),
806
+ )
807
+ if (result.exitCode === 0) return
808
+ return yield* new PatchError({
809
+ operation: "apply",
810
+ directory: input.path,
811
+ message:
812
+ result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim() || "Failed to apply changes",
813
+ })
814
+ })
815
+
816
+ const discard = Effect.fn("Git.change.discard")(function* (input: {
817
+ repository: Repository
818
+ path: AbsolutePath
819
+ index: "preserve" | "reset"
820
+ untracked: "preserve" | "remove"
821
+ }) {
822
+ const scope = path.relative(input.repository.worktree, input.path).replaceAll("\\", "/") || "."
823
+ const restore = yield* execute(
824
+ input.repository.worktree,
825
+ proc,
826
+ )(input.index === "reset" ? ["checkout", "HEAD", "--", scope] : ["checkout", "--", scope]).pipe(
827
+ Effect.mapError(
828
+ (cause) => new PatchError({ operation: "reset", directory: input.path, message: cause.message, cause }),
829
+ ),
830
+ )
831
+ if (restore.exitCode !== 0) {
832
+ return yield* new PatchError({
833
+ operation: "reset",
834
+ directory: input.path,
835
+ message: restore.stderr.trim() || restore.text.trim() || "Failed to restore tracked changes",
836
+ })
837
+ }
838
+ if (input.untracked === "preserve") return
839
+ const clean = yield* execute(
840
+ input.repository.worktree,
841
+ proc,
842
+ )(["clean", "-fd", "--", scope]).pipe(
843
+ Effect.mapError(
844
+ (cause) => new PatchError({ operation: "reset", directory: input.path, message: cause.message, cause }),
845
+ ),
846
+ )
847
+ if (clean.exitCode === 0) return
848
+ return yield* new PatchError({
849
+ operation: "reset",
850
+ directory: input.path,
851
+ message: clean.stderr.trim() || clean.text.trim() || "Failed to clean untracked changes",
852
+ })
853
+ })
854
+
855
+ const worktreeRun = Effect.fnUntraced(function* (
856
+ operation: "create" | "remove" | "list",
857
+ repository: Repository,
858
+ args: string[],
859
+ worktreeDirectory?: AbsolutePath,
860
+ cwd = repository.worktree,
861
+ ) {
862
+ const result = yield* proc
863
+ .run(ChildProcess.make("git", args, { cwd, extendEnv: true, stdin: "ignore" }))
864
+ .pipe(
865
+ Effect.mapError(
866
+ (cause) => new WorktreeError({ operation, directory: worktreeDirectory, message: cause.message, cause }),
867
+ ),
868
+ )
869
+ if (result.exitCode === 0) return result.stdout.toString("utf8")
870
+ const message = result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim() || "Git failed"
871
+ return yield* new WorktreeError({
872
+ operation,
873
+ directory: worktreeDirectory,
874
+ message,
875
+ forceRequired: operation === "remove" && /contains modified or untracked files|is dirty/i.test(message),
876
+ })
877
+ })
878
+
879
+ const worktreeCreate = Effect.fn("Git.worktree.create")(function* (input: {
880
+ repository: Repository
881
+ directory: AbsolutePath
882
+ }) {
883
+ yield* worktreeRun(
884
+ "create",
885
+ input.repository,
886
+ ["worktree", "add", "--detach", input.directory, "HEAD"],
887
+ input.directory,
888
+ )
889
+ const repository = yield* discover(input.directory)
890
+ if (repository) return repository
891
+ return yield* new WorktreeError({
892
+ operation: "create",
893
+ directory: input.directory,
894
+ message: "Created worktree could not be opened",
895
+ })
896
+ })
897
+
898
+ const worktreeRemove = Effect.fn("Git.worktree.remove")(function* (input: {
899
+ repository: Repository
900
+ directory: AbsolutePath
901
+ force: boolean
902
+ }) {
903
+ yield* worktreeRun(
904
+ "remove",
905
+ input.repository,
906
+ ["worktree", "remove", ...(input.force ? ["--force"] : []), input.directory],
907
+ input.directory,
908
+ input.repository.commonDirectory,
909
+ )
910
+ })
911
+
912
+ const worktreeList = Effect.fn("Git.worktree.list")(function* (repository: Repository) {
913
+ return (yield* worktreeRun("list", repository, ["worktree", "list", "--porcelain"]))
914
+ .split("\n")
915
+ .filter((line) => line.startsWith("worktree "))
916
+ .map(
917
+ (line, index) =>
918
+ new Worktree({
919
+ directory: AbsolutePath.make(resolvePath(repository.worktree, line.slice("worktree ".length).trim())),
920
+ kind: index === 0 ? "main" : "linked",
921
+ }),
922
+ )
923
+ })
924
+
925
+ return Service.of({
926
+ repo: { discover, clone, create },
927
+ remote: { get: remote },
928
+ history: { head, branch, defaultRemoteBranch: remoteHead, rootCommits: roots },
929
+ sync: { fetchRemotes: fetch, fetchBranch, checkoutRemoteBranch: checkout, resetHard: reset },
930
+ change: { capture, apply, discard },
931
+ worktree: { create: worktreeCreate, remove: worktreeRemove, list: worktreeList },
932
+ index: { refresh, ignored },
933
+ tree: {
934
+ capture: captureTree,
935
+ write: writeTree,
936
+ files: treeFiles,
937
+ diff: treeDiff,
938
+ preview,
939
+ restore,
940
+ checkout: checkoutTree,
941
+ },
942
+ })
943
+ }),
944
+ )
945
+
946
+ export const node = makeGlobalNode({ service: Service, layer: layer, deps: [FSUtil.node, AppProcess.node] })
947
+
948
+ interface Result {
949
+ readonly exitCode: number
950
+ readonly text: string
951
+ readonly stderr: string
952
+ }
953
+
954
+ function run(cwd: string, proc: AppProcess.Interface) {
955
+ return (args: string[]) =>
956
+ execute(cwd, proc)(args).pipe(Effect.catch(() => Effect.succeed({ exitCode: 1, text: "", stderr: "" })))
957
+ }
958
+
959
+ function execute(cwd: string, proc: AppProcess.Interface) {
960
+ return (args: string[]) =>
961
+ proc
962
+ .run(
963
+ ChildProcess.make("git", args, {
964
+ cwd,
965
+ extendEnv: true,
966
+ stdin: "ignore",
967
+ }),
968
+ )
969
+ .pipe(
970
+ Effect.map(
971
+ (result) =>
972
+ ({
973
+ exitCode: result.exitCode,
974
+ text: result.stdout.toString("utf8"),
975
+ stderr: result.stderr.toString("utf8"),
976
+ }) satisfies Result,
977
+ ),
978
+ )
979
+ }
980
+
981
+ function resolvePath(cwd: string, value: string) {
982
+ const trimmed = value.replace(/[\r\n]+$/, "")
983
+ if (!trimmed) return cwd
984
+ const normalized = FSUtil.windowsPath(trimmed)
985
+ if (path.isAbsolute(normalized)) return path.normalize(normalized)
986
+ return path.resolve(cwd, normalized)
987
+ }