alchemylab-opencode 1.0.0

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 (379) hide show
  1. package/LICENSE +21 -0
  2. package/NODE_COMPAT.md +184 -0
  3. package/package.json +112 -0
  4. package/packages/opencode/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
  5. package/packages/opencode/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
  6. package/packages/opencode/migration/20260211171708_add_project_commands/migration.sql +1 -0
  7. package/packages/opencode/migration/20260211171708_add_project_commands/snapshot.json +806 -0
  8. package/packages/opencode/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
  9. package/packages/opencode/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
  10. package/packages/opencode/migration/20260225215848_workspace/migration.sql +7 -0
  11. package/packages/opencode/migration/20260225215848_workspace/snapshot.json +959 -0
  12. package/packages/opencode/migration/20260227213759_add_session_workspace_id/migration.sql +2 -0
  13. package/packages/opencode/migration/20260227213759_add_session_workspace_id/snapshot.json +983 -0
  14. package/packages/opencode/migration/20260228203230_blue_harpoon/migration.sql +17 -0
  15. package/packages/opencode/migration/20260228203230_blue_harpoon/snapshot.json +1102 -0
  16. package/packages/opencode/migration/20260303231226_add_workspace_fields/migration.sql +5 -0
  17. package/packages/opencode/migration/20260303231226_add_workspace_fields/snapshot.json +1013 -0
  18. package/packages/opencode/migration/20260309230000_move_org_to_state/migration.sql +3 -0
  19. package/packages/opencode/migration/20260309230000_move_org_to_state/snapshot.json +1156 -0
  20. package/packages/opencode/migration/20260312043431_session_message_cursor/migration.sql +4 -0
  21. package/packages/opencode/migration/20260312043431_session_message_cursor/snapshot.json +1168 -0
  22. package/packages/opencode/migration/20260323234822_events/migration.sql +13 -0
  23. package/packages/opencode/migration/20260323234822_events/snapshot.json +1271 -0
  24. package/packages/opencode/package.json +140 -0
  25. package/packages/opencode/src/account/account.sql.ts +39 -0
  26. package/packages/opencode/src/account/index.ts +463 -0
  27. package/packages/opencode/src/account/repo.ts +163 -0
  28. package/packages/opencode/src/account/schema.ts +91 -0
  29. package/packages/opencode/src/acp/README.md +174 -0
  30. package/packages/opencode/src/acp/agent.ts +1847 -0
  31. package/packages/opencode/src/acp/session.ts +116 -0
  32. package/packages/opencode/src/acp/types.ts +24 -0
  33. package/packages/opencode/src/agent/agent.ts +420 -0
  34. package/packages/opencode/src/agent/generate.txt +75 -0
  35. package/packages/opencode/src/agent/prompt/compaction.txt +15 -0
  36. package/packages/opencode/src/agent/prompt/explore.txt +18 -0
  37. package/packages/opencode/src/agent/prompt/summary.txt +11 -0
  38. package/packages/opencode/src/agent/prompt/title.txt +44 -0
  39. package/packages/opencode/src/auth/index.ts +110 -0
  40. package/packages/opencode/src/bus/bus-event.ts +40 -0
  41. package/packages/opencode/src/bus/global.ts +10 -0
  42. package/packages/opencode/src/bus/index.ts +185 -0
  43. package/packages/opencode/src/cli/bootstrap.ts +17 -0
  44. package/packages/opencode/src/cli/cmd/cmd.ts +7 -0
  45. package/packages/opencode/src/cli/cmd/run.ts +689 -0
  46. package/packages/opencode/src/cli/cmd/serve.ts +24 -0
  47. package/packages/opencode/src/cli/cmd/tui/event.ts +49 -0
  48. package/packages/opencode/src/cli/effect/prompt.ts +25 -0
  49. package/packages/opencode/src/cli/error.ts +46 -0
  50. package/packages/opencode/src/cli/heap.ts +59 -0
  51. package/packages/opencode/src/cli/logo.ts +6 -0
  52. package/packages/opencode/src/cli/network.ts +60 -0
  53. package/packages/opencode/src/cli/ui.ts +133 -0
  54. package/packages/opencode/src/cli/upgrade.ts +31 -0
  55. package/packages/opencode/src/command/index.ts +195 -0
  56. package/packages/opencode/src/command/template/initialize.txt +66 -0
  57. package/packages/opencode/src/command/template/review.txt +101 -0
  58. package/packages/opencode/src/config/config.ts +1599 -0
  59. package/packages/opencode/src/config/console-state.ts +15 -0
  60. package/packages/opencode/src/config/markdown.ts +99 -0
  61. package/packages/opencode/src/config/paths.ts +167 -0
  62. package/packages/opencode/src/config/tui-migrate.ts +155 -0
  63. package/packages/opencode/src/config/tui-schema.ts +37 -0
  64. package/packages/opencode/src/config/tui.ts +179 -0
  65. package/packages/opencode/src/control-plane/adaptors/index.ts +20 -0
  66. package/packages/opencode/src/control-plane/adaptors/worktree.ts +42 -0
  67. package/packages/opencode/src/control-plane/schema.ts +17 -0
  68. package/packages/opencode/src/control-plane/sse.ts +66 -0
  69. package/packages/opencode/src/control-plane/types.ts +32 -0
  70. package/packages/opencode/src/control-plane/workspace.sql.ts +17 -0
  71. package/packages/opencode/src/control-plane/workspace.ts +168 -0
  72. package/packages/opencode/src/effect/cross-spawn-spawner.ts +502 -0
  73. package/packages/opencode/src/effect/instance-ref.ts +6 -0
  74. package/packages/opencode/src/effect/instance-registry.ts +12 -0
  75. package/packages/opencode/src/effect/instance-state.ts +82 -0
  76. package/packages/opencode/src/effect/run-service.ts +33 -0
  77. package/packages/opencode/src/effect/runner.ts +216 -0
  78. package/packages/opencode/src/env/index.ts +28 -0
  79. package/packages/opencode/src/file/ignore.ts +82 -0
  80. package/packages/opencode/src/file/index.ts +686 -0
  81. package/packages/opencode/src/file/protected.ts +59 -0
  82. package/packages/opencode/src/file/ripgrep.ts +376 -0
  83. package/packages/opencode/src/file/time.ts +133 -0
  84. package/packages/opencode/src/file/watcher.ts +173 -0
  85. package/packages/opencode/src/filesystem/index.ts +236 -0
  86. package/packages/opencode/src/flag/flag.ts +156 -0
  87. package/packages/opencode/src/format/formatter.ts +413 -0
  88. package/packages/opencode/src/format/index.ts +203 -0
  89. package/packages/opencode/src/git/index.ts +303 -0
  90. package/packages/opencode/src/global/index.ts +56 -0
  91. package/packages/opencode/src/id/id.ts +85 -0
  92. package/packages/opencode/src/ide/index.ts +74 -0
  93. package/packages/opencode/src/installation/index.ts +355 -0
  94. package/packages/opencode/src/installation/meta.ts +7 -0
  95. package/packages/opencode/src/lsp/client.ts +252 -0
  96. package/packages/opencode/src/lsp/index.ts +558 -0
  97. package/packages/opencode/src/lsp/language.ts +120 -0
  98. package/packages/opencode/src/lsp/launch.ts +21 -0
  99. package/packages/opencode/src/lsp/server.ts +1968 -0
  100. package/packages/opencode/src/mcp/auth.ts +173 -0
  101. package/packages/opencode/src/mcp/index.ts +931 -0
  102. package/packages/opencode/src/mcp/oauth-callback.ts +216 -0
  103. package/packages/opencode/src/mcp/oauth-provider.ts +185 -0
  104. package/packages/opencode/src/node-loader.mjs +104 -0
  105. package/packages/opencode/src/node-preload.mjs +39 -0
  106. package/packages/opencode/src/node.ts +1 -0
  107. package/packages/opencode/src/npm/index.ts +188 -0
  108. package/packages/opencode/src/patch/index.ts +680 -0
  109. package/packages/opencode/src/permission/arity.ts +163 -0
  110. package/packages/opencode/src/permission/evaluate.ts +15 -0
  111. package/packages/opencode/src/permission/index.ts +325 -0
  112. package/packages/opencode/src/permission/schema.ts +17 -0
  113. package/packages/opencode/src/plugin/cloudflare.ts +67 -0
  114. package/packages/opencode/src/plugin/codex.ts +608 -0
  115. package/packages/opencode/src/plugin/github-copilot/copilot.ts +361 -0
  116. package/packages/opencode/src/plugin/github-copilot/models.ts +144 -0
  117. package/packages/opencode/src/plugin/index.ts +299 -0
  118. package/packages/opencode/src/plugin/install.ts +439 -0
  119. package/packages/opencode/src/plugin/loader.ts +174 -0
  120. package/packages/opencode/src/plugin/meta.ts +188 -0
  121. package/packages/opencode/src/plugin/shared.ts +323 -0
  122. package/packages/opencode/src/project/bootstrap.ts +31 -0
  123. package/packages/opencode/src/project/instance.ts +175 -0
  124. package/packages/opencode/src/project/project.sql.ts +16 -0
  125. package/packages/opencode/src/project/project.ts +519 -0
  126. package/packages/opencode/src/project/schema.ts +16 -0
  127. package/packages/opencode/src/project/state.ts +70 -0
  128. package/packages/opencode/src/project/vcs.ts +240 -0
  129. package/packages/opencode/src/provider/auth.ts +253 -0
  130. package/packages/opencode/src/provider/error.ts +197 -0
  131. package/packages/opencode/src/provider/models.ts +162 -0
  132. package/packages/opencode/src/provider/provider.ts +1709 -0
  133. package/packages/opencode/src/provider/schema.ts +38 -0
  134. package/packages/opencode/src/provider/sdk/copilot/README.md +5 -0
  135. package/packages/opencode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +170 -0
  136. package/packages/opencode/src/provider/sdk/copilot/chat/get-response-metadata.ts +15 -0
  137. package/packages/opencode/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +19 -0
  138. package/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-api-types.ts +64 -0
  139. package/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +815 -0
  140. package/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-options.ts +28 -0
  141. package/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
  142. package/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts +83 -0
  143. package/packages/opencode/src/provider/sdk/copilot/copilot-provider.ts +100 -0
  144. package/packages/opencode/src/provider/sdk/copilot/index.ts +2 -0
  145. package/packages/opencode/src/provider/sdk/copilot/openai-compatible-error.ts +27 -0
  146. package/packages/opencode/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts +335 -0
  147. package/packages/opencode/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts +22 -0
  148. package/packages/opencode/src/provider/sdk/copilot/responses/openai-config.ts +18 -0
  149. package/packages/opencode/src/provider/sdk/copilot/responses/openai-error.ts +22 -0
  150. package/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-api-types.ts +214 -0
  151. package/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +1769 -0
  152. package/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts +173 -0
  153. package/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-settings.ts +1 -0
  154. package/packages/opencode/src/provider/sdk/copilot/responses/tool/code-interpreter.ts +87 -0
  155. package/packages/opencode/src/provider/sdk/copilot/responses/tool/file-search.ts +127 -0
  156. package/packages/opencode/src/provider/sdk/copilot/responses/tool/image-generation.ts +114 -0
  157. package/packages/opencode/src/provider/sdk/copilot/responses/tool/local-shell.ts +64 -0
  158. package/packages/opencode/src/provider/sdk/copilot/responses/tool/web-search-preview.ts +103 -0
  159. package/packages/opencode/src/provider/sdk/copilot/responses/tool/web-search.ts +102 -0
  160. package/packages/opencode/src/provider/transform.ts +1050 -0
  161. package/packages/opencode/src/pty/index.ts +397 -0
  162. package/packages/opencode/src/pty/pty.node.ts +27 -0
  163. package/packages/opencode/src/pty/pty.ts +25 -0
  164. package/packages/opencode/src/pty/schema.ts +17 -0
  165. package/packages/opencode/src/question/index.ts +224 -0
  166. package/packages/opencode/src/question/schema.ts +17 -0
  167. package/packages/opencode/src/run.ts +28 -0
  168. package/packages/opencode/src/serve.ts +45 -0
  169. package/packages/opencode/src/server/error.ts +36 -0
  170. package/packages/opencode/src/server/event.ts +7 -0
  171. package/packages/opencode/src/server/instance.ts +324 -0
  172. package/packages/opencode/src/server/mdns.ts +60 -0
  173. package/packages/opencode/src/server/middleware.ts +33 -0
  174. package/packages/opencode/src/server/projectors.ts +28 -0
  175. package/packages/opencode/src/server/proxy.ts +131 -0
  176. package/packages/opencode/src/server/router.ts +105 -0
  177. package/packages/opencode/src/server/routes/config.ts +92 -0
  178. package/packages/opencode/src/server/routes/event.ts +83 -0
  179. package/packages/opencode/src/server/routes/experimental.ts +374 -0
  180. package/packages/opencode/src/server/routes/file.ts +197 -0
  181. package/packages/opencode/src/server/routes/global.ts +312 -0
  182. package/packages/opencode/src/server/routes/mcp.ts +225 -0
  183. package/packages/opencode/src/server/routes/permission.ts +69 -0
  184. package/packages/opencode/src/server/routes/project.ts +118 -0
  185. package/packages/opencode/src/server/routes/provider.ts +171 -0
  186. package/packages/opencode/src/server/routes/pty.ts +210 -0
  187. package/packages/opencode/src/server/routes/question.ts +99 -0
  188. package/packages/opencode/src/server/routes/session.ts +1030 -0
  189. package/packages/opencode/src/server/routes/tui.ts +379 -0
  190. package/packages/opencode/src/server/routes/workspace.ts +94 -0
  191. package/packages/opencode/src/server/server.ts +353 -0
  192. package/packages/opencode/src/session/compaction.ts +428 -0
  193. package/packages/opencode/src/session/index.ts +887 -0
  194. package/packages/opencode/src/session/instruction.ts +258 -0
  195. package/packages/opencode/src/session/llm.ts +354 -0
  196. package/packages/opencode/src/session/message-v2.ts +1031 -0
  197. package/packages/opencode/src/session/message.ts +191 -0
  198. package/packages/opencode/src/session/overflow.ts +22 -0
  199. package/packages/opencode/src/session/processor.ts +523 -0
  200. package/packages/opencode/src/session/projectors.ts +135 -0
  201. package/packages/opencode/src/session/prompt/anthropic.txt +105 -0
  202. package/packages/opencode/src/session/prompt/beast.txt +147 -0
  203. package/packages/opencode/src/session/prompt/build-switch.txt +5 -0
  204. package/packages/opencode/src/session/prompt/codex.txt +79 -0
  205. package/packages/opencode/src/session/prompt/copilot-gpt-5.txt +143 -0
  206. package/packages/opencode/src/session/prompt/default.txt +105 -0
  207. package/packages/opencode/src/session/prompt/gemini.txt +155 -0
  208. package/packages/opencode/src/session/prompt/gpt.txt +107 -0
  209. package/packages/opencode/src/session/prompt/kimi.txt +95 -0
  210. package/packages/opencode/src/session/prompt/max-steps.txt +16 -0
  211. package/packages/opencode/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  212. package/packages/opencode/src/session/prompt/plan.txt +26 -0
  213. package/packages/opencode/src/session/prompt/trinity.txt +97 -0
  214. package/packages/opencode/src/session/prompt.ts +1911 -0
  215. package/packages/opencode/src/session/retry.ts +106 -0
  216. package/packages/opencode/src/session/revert.ts +176 -0
  217. package/packages/opencode/src/session/schema.ts +38 -0
  218. package/packages/opencode/src/session/session.sql.ts +103 -0
  219. package/packages/opencode/src/session/status.ts +102 -0
  220. package/packages/opencode/src/session/summary.ts +177 -0
  221. package/packages/opencode/src/session/system.ts +76 -0
  222. package/packages/opencode/src/session/todo.ts +95 -0
  223. package/packages/opencode/src/share/share-next.ts +369 -0
  224. package/packages/opencode/src/share/share.sql.ts +13 -0
  225. package/packages/opencode/src/shell/shell.ts +110 -0
  226. package/packages/opencode/src/skill/discovery.ts +116 -0
  227. package/packages/opencode/src/skill/index.ts +277 -0
  228. package/packages/opencode/src/snapshot/index.ts +726 -0
  229. package/packages/opencode/src/sql.d.ts +4 -0
  230. package/packages/opencode/src/storage/db.node.ts +8 -0
  231. package/packages/opencode/src/storage/db.ts +177 -0
  232. package/packages/opencode/src/storage/json-migration.ts +425 -0
  233. package/packages/opencode/src/storage/schema.sql.ts +10 -0
  234. package/packages/opencode/src/storage/schema.ts +5 -0
  235. package/packages/opencode/src/storage/storage.ts +353 -0
  236. package/packages/opencode/src/sync/README.md +179 -0
  237. package/packages/opencode/src/sync/event.sql.ts +16 -0
  238. package/packages/opencode/src/sync/index.ts +263 -0
  239. package/packages/opencode/src/sync/schema.ts +14 -0
  240. package/packages/opencode/src/tool/apply_patch.ts +281 -0
  241. package/packages/opencode/src/tool/apply_patch.txt +33 -0
  242. package/packages/opencode/src/tool/bash.ts +496 -0
  243. package/packages/opencode/src/tool/bash.txt +117 -0
  244. package/packages/opencode/src/tool/batch.ts +183 -0
  245. package/packages/opencode/src/tool/batch.txt +24 -0
  246. package/packages/opencode/src/tool/codesearch.ts +132 -0
  247. package/packages/opencode/src/tool/codesearch.txt +12 -0
  248. package/packages/opencode/src/tool/edit.ts +667 -0
  249. package/packages/opencode/src/tool/edit.txt +10 -0
  250. package/packages/opencode/src/tool/external-directory.ts +46 -0
  251. package/packages/opencode/src/tool/glob.ts +78 -0
  252. package/packages/opencode/src/tool/glob.txt +6 -0
  253. package/packages/opencode/src/tool/grep.ts +156 -0
  254. package/packages/opencode/src/tool/grep.txt +8 -0
  255. package/packages/opencode/src/tool/invalid.ts +17 -0
  256. package/packages/opencode/src/tool/ls.ts +121 -0
  257. package/packages/opencode/src/tool/ls.txt +1 -0
  258. package/packages/opencode/src/tool/lsp.ts +97 -0
  259. package/packages/opencode/src/tool/lsp.txt +19 -0
  260. package/packages/opencode/src/tool/multiedit.ts +46 -0
  261. package/packages/opencode/src/tool/multiedit.txt +41 -0
  262. package/packages/opencode/src/tool/plan-enter.txt +14 -0
  263. package/packages/opencode/src/tool/plan-exit.txt +13 -0
  264. package/packages/opencode/src/tool/plan.ts +131 -0
  265. package/packages/opencode/src/tool/question.ts +46 -0
  266. package/packages/opencode/src/tool/question.txt +10 -0
  267. package/packages/opencode/src/tool/read.ts +332 -0
  268. package/packages/opencode/src/tool/read.txt +14 -0
  269. package/packages/opencode/src/tool/registry.ts +265 -0
  270. package/packages/opencode/src/tool/schema.ts +17 -0
  271. package/packages/opencode/src/tool/skill.ts +105 -0
  272. package/packages/opencode/src/tool/task.ts +166 -0
  273. package/packages/opencode/src/tool/task.txt +60 -0
  274. package/packages/opencode/src/tool/todo.ts +48 -0
  275. package/packages/opencode/src/tool/todowrite.txt +167 -0
  276. package/packages/opencode/src/tool/tool.ts +112 -0
  277. package/packages/opencode/src/tool/truncate.ts +144 -0
  278. package/packages/opencode/src/tool/truncation-dir.ts +4 -0
  279. package/packages/opencode/src/tool/webfetch.ts +206 -0
  280. package/packages/opencode/src/tool/webfetch.txt +13 -0
  281. package/packages/opencode/src/tool/websearch.ts +150 -0
  282. package/packages/opencode/src/tool/websearch.txt +14 -0
  283. package/packages/opencode/src/tool/write.ts +84 -0
  284. package/packages/opencode/src/tool/write.txt +8 -0
  285. package/packages/opencode/src/util/abort.ts +35 -0
  286. package/packages/opencode/src/util/archive.ts +17 -0
  287. package/packages/opencode/src/util/color.ts +19 -0
  288. package/packages/opencode/src/util/context.ts +25 -0
  289. package/packages/opencode/src/util/data-url.ts +9 -0
  290. package/packages/opencode/src/util/defer.ts +12 -0
  291. package/packages/opencode/src/util/effect-http-client.ts +11 -0
  292. package/packages/opencode/src/util/effect-zod.ts +98 -0
  293. package/packages/opencode/src/util/error.ts +77 -0
  294. package/packages/opencode/src/util/filesystem.ts +245 -0
  295. package/packages/opencode/src/util/flock.ts +333 -0
  296. package/packages/opencode/src/util/fn.ts +21 -0
  297. package/packages/opencode/src/util/format.ts +20 -0
  298. package/packages/opencode/src/util/glob.ts +34 -0
  299. package/packages/opencode/src/util/hash.ts +7 -0
  300. package/packages/opencode/src/util/iife.ts +3 -0
  301. package/packages/opencode/src/util/keybind.ts +103 -0
  302. package/packages/opencode/src/util/lazy.ts +23 -0
  303. package/packages/opencode/src/util/locale.ts +81 -0
  304. package/packages/opencode/src/util/lock.ts +98 -0
  305. package/packages/opencode/src/util/log.ts +182 -0
  306. package/packages/opencode/src/util/network.ts +9 -0
  307. package/packages/opencode/src/util/process.ts +176 -0
  308. package/packages/opencode/src/util/queue.ts +32 -0
  309. package/packages/opencode/src/util/record.ts +3 -0
  310. package/packages/opencode/src/util/rpc.ts +66 -0
  311. package/packages/opencode/src/util/schema.ts +53 -0
  312. package/packages/opencode/src/util/scrap.ts +10 -0
  313. package/packages/opencode/src/util/signal.ts +12 -0
  314. package/packages/opencode/src/util/timeout.ts +14 -0
  315. package/packages/opencode/src/util/token.ts +7 -0
  316. package/packages/opencode/src/util/update-schema.ts +13 -0
  317. package/packages/opencode/src/util/which.ts +14 -0
  318. package/packages/opencode/src/util/wildcard.ts +59 -0
  319. package/packages/opencode/src/worktree/index.ts +612 -0
  320. package/packages/plugin/package.json +38 -0
  321. package/packages/plugin/src/example.ts +18 -0
  322. package/packages/plugin/src/index.ts +282 -0
  323. package/packages/plugin/src/shell.ts +136 -0
  324. package/packages/plugin/src/tool.ts +38 -0
  325. package/packages/plugin/src/tui.ts +508 -0
  326. package/packages/script/package.json +16 -0
  327. package/packages/script/src/index.ts +77 -0
  328. package/packages/sdk/package.json +33 -0
  329. package/packages/sdk/src/client.ts +55 -0
  330. package/packages/sdk/src/gen/client/client.gen.ts +212 -0
  331. package/packages/sdk/src/gen/client/index.ts +25 -0
  332. package/packages/sdk/src/gen/client/types.gen.ts +222 -0
  333. package/packages/sdk/src/gen/client/utils.gen.ts +287 -0
  334. package/packages/sdk/src/gen/client.gen.ts +22 -0
  335. package/packages/sdk/src/gen/core/auth.gen.ts +41 -0
  336. package/packages/sdk/src/gen/core/bodySerializer.gen.ts +74 -0
  337. package/packages/sdk/src/gen/core/params.gen.ts +144 -0
  338. package/packages/sdk/src/gen/core/pathSerializer.gen.ts +167 -0
  339. package/packages/sdk/src/gen/core/queryKeySerializer.gen.ts +111 -0
  340. package/packages/sdk/src/gen/core/serverSentEvents.gen.ts +210 -0
  341. package/packages/sdk/src/gen/core/types.gen.ts +91 -0
  342. package/packages/sdk/src/gen/core/utils.gen.ts +109 -0
  343. package/packages/sdk/src/gen/sdk.gen.ts +1197 -0
  344. package/packages/sdk/src/gen/types.gen.ts +3904 -0
  345. package/packages/sdk/src/index.ts +21 -0
  346. package/packages/sdk/src/process.ts +31 -0
  347. package/packages/sdk/src/server.ts +134 -0
  348. package/packages/sdk/src/v2/client.ts +81 -0
  349. package/packages/sdk/src/v2/gen/client/client.gen.ts +285 -0
  350. package/packages/sdk/src/v2/gen/client/index.ts +25 -0
  351. package/packages/sdk/src/v2/gen/client/types.gen.ts +202 -0
  352. package/packages/sdk/src/v2/gen/client/utils.gen.ts +289 -0
  353. package/packages/sdk/src/v2/gen/client.gen.ts +18 -0
  354. package/packages/sdk/src/v2/gen/core/auth.gen.ts +41 -0
  355. package/packages/sdk/src/v2/gen/core/bodySerializer.gen.ts +82 -0
  356. package/packages/sdk/src/v2/gen/core/params.gen.ts +169 -0
  357. package/packages/sdk/src/v2/gen/core/pathSerializer.gen.ts +167 -0
  358. package/packages/sdk/src/v2/gen/core/queryKeySerializer.gen.ts +111 -0
  359. package/packages/sdk/src/v2/gen/core/serverSentEvents.gen.ts +239 -0
  360. package/packages/sdk/src/v2/gen/core/types.gen.ts +86 -0
  361. package/packages/sdk/src/v2/gen/core/utils.gen.ts +137 -0
  362. package/packages/sdk/src/v2/gen/sdk.gen.ts +4223 -0
  363. package/packages/sdk/src/v2/gen/types.gen.ts +5265 -0
  364. package/packages/sdk/src/v2/index.ts +21 -0
  365. package/packages/sdk/src/v2/server.ts +134 -0
  366. package/packages/util/package.json +20 -0
  367. package/packages/util/src/array.ts +10 -0
  368. package/packages/util/src/binary.ts +41 -0
  369. package/packages/util/src/encode.ts +51 -0
  370. package/packages/util/src/error.ts +54 -0
  371. package/packages/util/src/fn.ts +11 -0
  372. package/packages/util/src/identifier.ts +48 -0
  373. package/packages/util/src/iife.ts +3 -0
  374. package/packages/util/src/lazy.ts +11 -0
  375. package/packages/util/src/module.ts +10 -0
  376. package/packages/util/src/path.ts +37 -0
  377. package/packages/util/src/retry.ts +41 -0
  378. package/packages/util/src/slug.ts +74 -0
  379. package/postinstall.cjs +55 -0
@@ -0,0 +1,175 @@
1
+ import { GlobalBus } from "@/bus/global"
2
+ import { disposeInstance } from "@/effect/instance-registry"
3
+ import { Filesystem } from "@/util/filesystem"
4
+ import { iife } from "@/util/iife"
5
+ import { Log } from "@/util/log"
6
+ import { Context } from "../util/context"
7
+ import { Project } from "./project"
8
+ import { State } from "./state"
9
+
10
+ export interface InstanceContext {
11
+ directory: string
12
+ worktree: string
13
+ project: Project.Info
14
+ }
15
+
16
+ const context = Context.create<InstanceContext>("instance")
17
+ const cache = new Map<string, Promise<InstanceContext>>()
18
+
19
+ const disposal = {
20
+ all: undefined as Promise<void> | undefined,
21
+ }
22
+
23
+ function emit(directory: string) {
24
+ GlobalBus.emit("event", {
25
+ directory,
26
+ payload: {
27
+ type: "server.instance.disposed",
28
+ properties: {
29
+ directory,
30
+ },
31
+ },
32
+ })
33
+ }
34
+
35
+ function boot(input: { directory: string; init?: () => Promise<any>; project?: Project.Info; worktree?: string }) {
36
+ return iife(async () => {
37
+ const ctx =
38
+ input.project && input.worktree
39
+ ? {
40
+ directory: input.directory,
41
+ worktree: input.worktree,
42
+ project: input.project,
43
+ }
44
+ : await Project.fromDirectory(input.directory).then(({ project, sandbox }) => ({
45
+ directory: input.directory,
46
+ worktree: sandbox,
47
+ project,
48
+ }))
49
+ await context.provide(ctx, async () => {
50
+ await input.init?.()
51
+ })
52
+ return ctx
53
+ })
54
+ }
55
+
56
+ function track(directory: string, next: Promise<InstanceContext>) {
57
+ const task = next.catch((error) => {
58
+ if (cache.get(directory) === task) cache.delete(directory)
59
+ throw error
60
+ })
61
+ cache.set(directory, task)
62
+ return task
63
+ }
64
+
65
+ export const Instance = {
66
+ async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> {
67
+ const directory = Filesystem.resolve(input.directory)
68
+ let existing = cache.get(directory)
69
+ if (!existing) {
70
+ Log.Default.info("creating instance", { directory })
71
+ existing = track(
72
+ directory,
73
+ boot({
74
+ directory,
75
+ init: input.init,
76
+ }),
77
+ )
78
+ }
79
+ const ctx = await existing
80
+ return context.provide(ctx, async () => {
81
+ return input.fn()
82
+ })
83
+ },
84
+ get current() {
85
+ return context.use()
86
+ },
87
+ get directory() {
88
+ return context.use().directory
89
+ },
90
+ get worktree() {
91
+ return context.use().worktree
92
+ },
93
+ get project() {
94
+ return context.use().project
95
+ },
96
+ /**
97
+ * Check if a path is within the project boundary.
98
+ * Returns true if path is inside Instance.directory OR Instance.worktree.
99
+ * Paths within the worktree but outside the working directory should not trigger external_directory permission.
100
+ */
101
+ containsPath(filepath: string) {
102
+ if (Filesystem.contains(Instance.directory, filepath)) return true
103
+ // Non-git projects set worktree to "/" which would match ANY absolute path.
104
+ // Skip worktree check in this case to preserve external_directory permissions.
105
+ if (Instance.worktree === "/") return false
106
+ return Filesystem.contains(Instance.worktree, filepath)
107
+ },
108
+ /**
109
+ * Captures the current instance ALS context and returns a wrapper that
110
+ * restores it when called. Use this for callbacks that fire outside the
111
+ * instance async context (native addons, event emitters, timers, etc.).
112
+ */
113
+ bind<F extends (...args: any[]) => any>(fn: F): F {
114
+ const ctx = context.use()
115
+ return ((...args: any[]) => context.provide(ctx, () => fn(...args))) as F
116
+ },
117
+ /**
118
+ * Run a synchronous function within the given instance context ALS.
119
+ * Use this to bridge from Effect (where InstanceRef carries context)
120
+ * back to sync code that reads Instance.directory from ALS.
121
+ */
122
+ restore<R>(ctx: InstanceContext, fn: () => R): R {
123
+ return context.provide(ctx, fn)
124
+ },
125
+ state<S>(init: () => S, dispose?: (state: Awaited<S>) => Promise<void>): () => S {
126
+ return State.create(() => Instance.directory, init, dispose)
127
+ },
128
+ async reload(input: { directory: string; init?: () => Promise<any>; project?: Project.Info; worktree?: string }) {
129
+ const directory = Filesystem.resolve(input.directory)
130
+ Log.Default.info("reloading instance", { directory })
131
+ await Promise.all([State.dispose(directory), disposeInstance(directory)])
132
+ cache.delete(directory)
133
+ const next = track(directory, boot({ ...input, directory }))
134
+ emit(directory)
135
+ return await next
136
+ },
137
+ async dispose() {
138
+ const directory = Instance.directory
139
+ Log.Default.info("disposing instance", { directory })
140
+ await Promise.all([State.dispose(directory), disposeInstance(directory)])
141
+ cache.delete(directory)
142
+ emit(directory)
143
+ },
144
+ async disposeAll() {
145
+ if (disposal.all) return disposal.all
146
+
147
+ disposal.all = iife(async () => {
148
+ Log.Default.info("disposing all instances")
149
+ const entries = [...cache.entries()]
150
+ for (const [key, value] of entries) {
151
+ if (cache.get(key) !== value) continue
152
+
153
+ const ctx = await value.catch((error) => {
154
+ Log.Default.warn("instance dispose failed", { key, error })
155
+ return undefined
156
+ })
157
+
158
+ if (!ctx) {
159
+ if (cache.get(key) === value) cache.delete(key)
160
+ continue
161
+ }
162
+
163
+ if (cache.get(key) !== value) continue
164
+
165
+ await context.provide(ctx, async () => {
166
+ await Instance.dispose()
167
+ })
168
+ }
169
+ }).finally(() => {
170
+ disposal.all = undefined
171
+ })
172
+
173
+ return disposal.all
174
+ },
175
+ }
@@ -0,0 +1,16 @@
1
+ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
2
+ import { Timestamps } from "../storage/schema.sql"
3
+ import type { ProjectID } from "./schema"
4
+
5
+ export const ProjectTable = sqliteTable("project", {
6
+ id: text().$type<ProjectID>().primaryKey(),
7
+ worktree: text().notNull(),
8
+ vcs: text(),
9
+ name: text(),
10
+ icon_url: text(),
11
+ icon_color: text(),
12
+ ...Timestamps,
13
+ time_initialized: integer(),
14
+ sandboxes: text({ mode: "json" }).notNull().$type<string[]>(),
15
+ commands: text({ mode: "json" }).$type<{ start?: string }>(),
16
+ })
@@ -0,0 +1,519 @@
1
+ import z from "zod"
2
+ import { and, Database, eq } from "../storage/db"
3
+ import { ProjectTable } from "./project.sql"
4
+ import { SessionTable } from "../session/session.sql"
5
+ import { Log } from "../util/log"
6
+ import { Flag } from "@/flag/flag"
7
+ import { BusEvent } from "@/bus/bus-event"
8
+ import { GlobalBus } from "@/bus/global"
9
+ import { which } from "../util/which"
10
+ import { ProjectID } from "./schema"
11
+ import { Effect, Layer, Path, Scope, ServiceMap, Stream } from "effect"
12
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
13
+ import { NodeFileSystem, NodePath } from "@effect/platform-node"
14
+ import { makeRuntime } from "@/effect/run-service"
15
+ import { AppFileSystem } from "@/filesystem"
16
+ import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
17
+
18
+ export namespace Project {
19
+ const log = Log.create({ service: "project" })
20
+
21
+ export const Info = z
22
+ .object({
23
+ id: ProjectID.zod,
24
+ worktree: z.string(),
25
+ vcs: z.literal("git").optional(),
26
+ name: z.string().optional(),
27
+ icon: z
28
+ .object({
29
+ url: z.string().optional(),
30
+ override: z.string().optional(),
31
+ color: z.string().optional(),
32
+ })
33
+ .optional(),
34
+ commands: z
35
+ .object({
36
+ start: z.string().optional().describe("Startup script to run when creating a new workspace (worktree)"),
37
+ })
38
+ .optional(),
39
+ time: z.object({
40
+ created: z.number(),
41
+ updated: z.number(),
42
+ initialized: z.number().optional(),
43
+ }),
44
+ sandboxes: z.array(z.string()),
45
+ })
46
+ .meta({
47
+ ref: "Project",
48
+ })
49
+ export type Info = z.infer<typeof Info>
50
+
51
+ export const Event = {
52
+ Updated: BusEvent.define("project.updated", Info),
53
+ }
54
+
55
+ type Row = typeof ProjectTable.$inferSelect
56
+
57
+ export function fromRow(row: Row): Info {
58
+ const icon =
59
+ row.icon_url || row.icon_color
60
+ ? { url: row.icon_url ?? undefined, color: row.icon_color ?? undefined }
61
+ : undefined
62
+ return {
63
+ id: row.id,
64
+ worktree: row.worktree,
65
+ vcs: row.vcs ? Info.shape.vcs.parse(row.vcs) : undefined,
66
+ name: row.name ?? undefined,
67
+ icon,
68
+ time: {
69
+ created: row.time_created,
70
+ updated: row.time_updated,
71
+ initialized: row.time_initialized ?? undefined,
72
+ },
73
+ sandboxes: row.sandboxes,
74
+ commands: row.commands ?? undefined,
75
+ }
76
+ }
77
+
78
+ export const UpdateInput = z.object({
79
+ projectID: ProjectID.zod,
80
+ name: z.string().optional(),
81
+ icon: Info.shape.icon.optional(),
82
+ commands: Info.shape.commands.optional(),
83
+ })
84
+ export type UpdateInput = z.infer<typeof UpdateInput>
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Effect service
88
+ // ---------------------------------------------------------------------------
89
+
90
+ export interface Interface {
91
+ readonly fromDirectory: (directory: string) => Effect.Effect<{ project: Info; sandbox: string }>
92
+ readonly discover: (input: Info) => Effect.Effect<void>
93
+ readonly list: () => Effect.Effect<Info[]>
94
+ readonly get: (id: ProjectID) => Effect.Effect<Info | undefined>
95
+ readonly update: (input: UpdateInput) => Effect.Effect<Info>
96
+ readonly initGit: (input: { directory: string; project: Info }) => Effect.Effect<Info>
97
+ readonly setInitialized: (id: ProjectID) => Effect.Effect<void>
98
+ readonly sandboxes: (id: ProjectID) => Effect.Effect<string[]>
99
+ readonly addSandbox: (id: ProjectID, directory: string) => Effect.Effect<void>
100
+ readonly removeSandbox: (id: ProjectID, directory: string) => Effect.Effect<void>
101
+ }
102
+
103
+ export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Project") {}
104
+
105
+ type GitResult = { code: number; text: string; stderr: string }
106
+
107
+ export const layer: Layer.Layer<
108
+ Service,
109
+ never,
110
+ AppFileSystem.Service | Path.Path | ChildProcessSpawner.ChildProcessSpawner
111
+ > = Layer.effect(
112
+ Service,
113
+ Effect.gen(function* () {
114
+ const fs = yield* AppFileSystem.Service
115
+ const pathSvc = yield* Path.Path
116
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
117
+
118
+ const git = Effect.fnUntraced(
119
+ function* (args: string[], opts?: { cwd?: string }) {
120
+ const handle = yield* spawner.spawn(
121
+ ChildProcess.make("git", args, { cwd: opts?.cwd, extendEnv: true, stdin: "ignore" }),
122
+ )
123
+ const [text, stderr] = yield* Effect.all(
124
+ [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))],
125
+ { concurrency: 2 },
126
+ )
127
+ const code = yield* handle.exitCode
128
+ return { code, text, stderr } satisfies GitResult
129
+ },
130
+ Effect.scoped,
131
+ Effect.catch(() => Effect.succeed({ code: 1, text: "", stderr: "" } satisfies GitResult)),
132
+ )
133
+
134
+ const db = <T>(fn: (d: Parameters<typeof Database.use>[0] extends (trx: infer D) => any ? D : never) => T) =>
135
+ Effect.sync(() => Database.use(fn))
136
+
137
+ const emitUpdated = (data: Info) =>
138
+ Effect.sync(() =>
139
+ GlobalBus.emit("event", {
140
+ payload: { type: Event.Updated.type, properties: data },
141
+ }),
142
+ )
143
+
144
+ const fakeVcs = Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS)
145
+
146
+ const resolveGitPath = (cwd: string, name: string) => {
147
+ if (!name) return cwd
148
+ name = name.replace(/[\r\n]+$/, "")
149
+ if (!name) return cwd
150
+ name = AppFileSystem.windowsPath(name)
151
+ if (pathSvc.isAbsolute(name)) return pathSvc.normalize(name)
152
+ return pathSvc.resolve(cwd, name)
153
+ }
154
+
155
+ const scope = yield* Scope.Scope
156
+
157
+ const readCachedProjectId = Effect.fnUntraced(function* (dir: string) {
158
+ return yield* fs.readFileString(pathSvc.join(dir, "opencode")).pipe(
159
+ Effect.map((x) => x.trim()),
160
+ Effect.map(ProjectID.make),
161
+ Effect.catch(() => Effect.succeed(undefined)),
162
+ )
163
+ })
164
+
165
+ const fromDirectory = Effect.fn("Project.fromDirectory")(function* (directory: string) {
166
+ log.info("fromDirectory", { directory })
167
+
168
+ // Phase 1: discover git info
169
+ type DiscoveryResult = { id: ProjectID; worktree: string; sandbox: string; vcs: Info["vcs"] }
170
+
171
+ const data: DiscoveryResult = yield* Effect.gen(function* () {
172
+ const dotgitMatches = yield* fs.up({ targets: [".git"], start: directory }).pipe(Effect.orDie)
173
+ const dotgit = dotgitMatches[0]
174
+
175
+ if (!dotgit) {
176
+ return {
177
+ id: ProjectID.global,
178
+ worktree: "/",
179
+ sandbox: "/",
180
+ vcs: fakeVcs,
181
+ }
182
+ }
183
+
184
+ let sandbox = pathSvc.dirname(dotgit)
185
+ const gitBinary = yield* Effect.sync(() => which("git"))
186
+ let id = yield* readCachedProjectId(dotgit)
187
+
188
+ if (!gitBinary) {
189
+ return {
190
+ id: id ?? ProjectID.global,
191
+ worktree: sandbox,
192
+ sandbox,
193
+ vcs: fakeVcs,
194
+ }
195
+ }
196
+
197
+ const commonDir = yield* git(["rev-parse", "--git-common-dir"], { cwd: sandbox })
198
+ if (commonDir.code !== 0) {
199
+ return {
200
+ id: id ?? ProjectID.global,
201
+ worktree: sandbox,
202
+ sandbox,
203
+ vcs: fakeVcs,
204
+ }
205
+ }
206
+ const worktree = (() => {
207
+ const common = resolveGitPath(sandbox, commonDir.text.trim())
208
+ return common === sandbox ? sandbox : pathSvc.dirname(common)
209
+ })()
210
+
211
+ if (id == null) {
212
+ id = yield* readCachedProjectId(pathSvc.join(worktree, ".git"))
213
+ }
214
+
215
+ if (!id) {
216
+ const revList = yield* git(["rev-list", "--max-parents=0", "HEAD"], { cwd: sandbox })
217
+ const roots = revList.text
218
+ .split("\n")
219
+ .filter(Boolean)
220
+ .map((x) => x.trim())
221
+ .toSorted()
222
+
223
+ id = roots[0] ? ProjectID.make(roots[0]) : undefined
224
+ if (id) {
225
+ yield* fs.writeFileString(pathSvc.join(worktree, ".git", "opencode"), id).pipe(Effect.ignore)
226
+ }
227
+ }
228
+
229
+ if (!id) {
230
+ return { id: ProjectID.global, worktree: sandbox, sandbox, vcs: "git" as const }
231
+ }
232
+
233
+ const topLevel = yield* git(["rev-parse", "--show-toplevel"], { cwd: sandbox })
234
+ if (topLevel.code !== 0) {
235
+ return {
236
+ id,
237
+ worktree: sandbox,
238
+ sandbox,
239
+ vcs: fakeVcs,
240
+ }
241
+ }
242
+ sandbox = resolveGitPath(sandbox, topLevel.text.trim())
243
+
244
+ return { id, sandbox, worktree, vcs: "git" as const }
245
+ })
246
+
247
+ // Phase 2: upsert
248
+ const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, data.id)).get())
249
+ const existing = row
250
+ ? fromRow(row)
251
+ : {
252
+ id: data.id,
253
+ worktree: data.worktree,
254
+ vcs: data.vcs,
255
+ sandboxes: [] as string[],
256
+ time: { created: Date.now(), updated: Date.now() },
257
+ }
258
+
259
+ if (Flag.OPENCODE_EXPERIMENTAL_ICON_DISCOVERY)
260
+ yield* discover(existing).pipe(Effect.ignore, Effect.forkIn(scope))
261
+
262
+ const result: Info = {
263
+ ...existing,
264
+ worktree: data.worktree,
265
+ vcs: data.vcs,
266
+ time: { ...existing.time, updated: Date.now() },
267
+ }
268
+ if (data.sandbox !== result.worktree && !result.sandboxes.includes(data.sandbox))
269
+ result.sandboxes.push(data.sandbox)
270
+ result.sandboxes = yield* Effect.forEach(
271
+ result.sandboxes,
272
+ (s) =>
273
+ fs.exists(s).pipe(
274
+ Effect.orDie,
275
+ Effect.map((exists) => (exists ? s : undefined)),
276
+ ),
277
+ { concurrency: "unbounded" },
278
+ ).pipe(Effect.map((arr) => arr.filter((x): x is string => x !== undefined)))
279
+
280
+ yield* db((d) =>
281
+ d
282
+ .insert(ProjectTable)
283
+ .values({
284
+ id: result.id,
285
+ worktree: result.worktree,
286
+ vcs: result.vcs ?? null,
287
+ name: result.name,
288
+ icon_url: result.icon?.url,
289
+ icon_color: result.icon?.color,
290
+ time_created: result.time.created,
291
+ time_updated: result.time.updated,
292
+ time_initialized: result.time.initialized,
293
+ sandboxes: result.sandboxes,
294
+ commands: result.commands,
295
+ })
296
+ .onConflictDoUpdate({
297
+ target: ProjectTable.id,
298
+ set: {
299
+ worktree: result.worktree,
300
+ vcs: result.vcs ?? null,
301
+ name: result.name,
302
+ icon_url: result.icon?.url,
303
+ icon_color: result.icon?.color,
304
+ time_updated: result.time.updated,
305
+ time_initialized: result.time.initialized,
306
+ sandboxes: result.sandboxes,
307
+ commands: result.commands,
308
+ },
309
+ })
310
+ .run(),
311
+ )
312
+
313
+ if (data.id !== ProjectID.global) {
314
+ yield* db((d) =>
315
+ d
316
+ .update(SessionTable)
317
+ .set({ project_id: data.id })
318
+ .where(and(eq(SessionTable.project_id, ProjectID.global), eq(SessionTable.directory, data.worktree)))
319
+ .run(),
320
+ )
321
+ }
322
+
323
+ yield* emitUpdated(result)
324
+ return { project: result, sandbox: data.sandbox }
325
+ })
326
+
327
+ const discover = Effect.fn("Project.discover")(function* (input: Info) {
328
+ if (input.vcs !== "git") return
329
+ if (input.icon?.override) return
330
+ if (input.icon?.url) return
331
+
332
+ const matches = yield* fs
333
+ .glob("**/favicon.{ico,png,svg,jpg,jpeg,webp}", {
334
+ cwd: input.worktree,
335
+ absolute: true,
336
+ include: "file",
337
+ })
338
+ .pipe(Effect.orDie)
339
+ const shortest = matches.sort((a, b) => a.length - b.length)[0]
340
+ if (!shortest) return
341
+
342
+ const buffer = yield* fs.readFile(shortest).pipe(Effect.orDie)
343
+ const base64 = Buffer.from(buffer).toString("base64")
344
+ const mime = AppFileSystem.mimeType(shortest)
345
+ const url = `data:${mime};base64,${base64}`
346
+ yield* update({ projectID: input.id, icon: { url } })
347
+ })
348
+
349
+ const list = Effect.fn("Project.list")(function* () {
350
+ return yield* db((d) => d.select().from(ProjectTable).all().map(fromRow))
351
+ })
352
+
353
+ const get = Effect.fn("Project.get")(function* (id: ProjectID) {
354
+ const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
355
+ return row ? fromRow(row) : undefined
356
+ })
357
+
358
+ const update = Effect.fn("Project.update")(function* (input: UpdateInput) {
359
+ const result = yield* db((d) =>
360
+ d
361
+ .update(ProjectTable)
362
+ .set({
363
+ name: input.name,
364
+ icon_url: input.icon?.url,
365
+ icon_color: input.icon?.color,
366
+ commands: input.commands,
367
+ time_updated: Date.now(),
368
+ })
369
+ .where(eq(ProjectTable.id, input.projectID))
370
+ .returning()
371
+ .get(),
372
+ )
373
+ if (!result) throw new Error(`Project not found: ${input.projectID}`)
374
+ const data = fromRow(result)
375
+ yield* emitUpdated(data)
376
+ return data
377
+ })
378
+
379
+ const initGit = Effect.fn("Project.initGit")(function* (input: { directory: string; project: Info }) {
380
+ if (input.project.vcs === "git") return input.project
381
+ if (!(yield* Effect.sync(() => which("git")))) throw new Error("Git is not installed")
382
+ const result = yield* git(["init", "--quiet"], { cwd: input.directory })
383
+ if (result.code !== 0) {
384
+ throw new Error(result.stderr.trim() || result.text.trim() || "Failed to initialize git repository")
385
+ }
386
+ const { project } = yield* fromDirectory(input.directory)
387
+ return project
388
+ })
389
+
390
+ const setInitialized = Effect.fn("Project.setInitialized")(function* (id: ProjectID) {
391
+ yield* db((d) =>
392
+ d.update(ProjectTable).set({ time_initialized: Date.now() }).where(eq(ProjectTable.id, id)).run(),
393
+ )
394
+ })
395
+
396
+ const sandboxes = Effect.fn("Project.sandboxes")(function* (id: ProjectID) {
397
+ const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
398
+ if (!row) return []
399
+ const data = fromRow(row)
400
+ return yield* Effect.forEach(
401
+ data.sandboxes,
402
+ (dir) =>
403
+ fs.isDir(dir).pipe(
404
+ Effect.orDie,
405
+ Effect.map((ok) => (ok ? dir : undefined)),
406
+ ),
407
+ { concurrency: "unbounded" },
408
+ ).pipe(Effect.map((arr) => arr.filter((x): x is string => x !== undefined)))
409
+ })
410
+
411
+ const addSandbox = Effect.fn("Project.addSandbox")(function* (id: ProjectID, directory: string) {
412
+ const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
413
+ if (!row) throw new Error(`Project not found: ${id}`)
414
+ const sboxes = [...row.sandboxes]
415
+ if (!sboxes.includes(directory)) sboxes.push(directory)
416
+ const result = yield* db((d) =>
417
+ d
418
+ .update(ProjectTable)
419
+ .set({ sandboxes: sboxes, time_updated: Date.now() })
420
+ .where(eq(ProjectTable.id, id))
421
+ .returning()
422
+ .get(),
423
+ )
424
+ if (!result) throw new Error(`Project not found: ${id}`)
425
+ yield* emitUpdated(fromRow(result))
426
+ })
427
+
428
+ const removeSandbox = Effect.fn("Project.removeSandbox")(function* (id: ProjectID, directory: string) {
429
+ const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
430
+ if (!row) throw new Error(`Project not found: ${id}`)
431
+ const sboxes = row.sandboxes.filter((s) => s !== directory)
432
+ const result = yield* db((d) =>
433
+ d
434
+ .update(ProjectTable)
435
+ .set({ sandboxes: sboxes, time_updated: Date.now() })
436
+ .where(eq(ProjectTable.id, id))
437
+ .returning()
438
+ .get(),
439
+ )
440
+ if (!result) throw new Error(`Project not found: ${id}`)
441
+ yield* emitUpdated(fromRow(result))
442
+ })
443
+
444
+ return Service.of({
445
+ fromDirectory,
446
+ discover,
447
+ list,
448
+ get,
449
+ update,
450
+ initGit,
451
+ setInitialized,
452
+ sandboxes,
453
+ addSandbox,
454
+ removeSandbox,
455
+ })
456
+ }),
457
+ )
458
+
459
+ export const defaultLayer = layer.pipe(
460
+ Layer.provide(CrossSpawnSpawner.defaultLayer),
461
+ Layer.provide(AppFileSystem.defaultLayer),
462
+ Layer.provide(NodePath.layer),
463
+ )
464
+ const { runPromise } = makeRuntime(Service, defaultLayer)
465
+
466
+ // ---------------------------------------------------------------------------
467
+ // Promise-based API (delegates to Effect service via runPromise)
468
+ // ---------------------------------------------------------------------------
469
+
470
+ export function fromDirectory(directory: string) {
471
+ return runPromise((svc) => svc.fromDirectory(directory))
472
+ }
473
+
474
+ export function discover(input: Info) {
475
+ return runPromise((svc) => svc.discover(input))
476
+ }
477
+
478
+ export function list() {
479
+ return Database.use((db) =>
480
+ db
481
+ .select()
482
+ .from(ProjectTable)
483
+ .all()
484
+ .map((row) => fromRow(row)),
485
+ )
486
+ }
487
+
488
+ export function get(id: ProjectID): Info | undefined {
489
+ const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
490
+ if (!row) return undefined
491
+ return fromRow(row)
492
+ }
493
+
494
+ export function setInitialized(id: ProjectID) {
495
+ Database.use((db) =>
496
+ db.update(ProjectTable).set({ time_initialized: Date.now() }).where(eq(ProjectTable.id, id)).run(),
497
+ )
498
+ }
499
+
500
+ export function initGit(input: { directory: string; project: Info }) {
501
+ return runPromise((svc) => svc.initGit(input))
502
+ }
503
+
504
+ export function update(input: UpdateInput) {
505
+ return runPromise((svc) => svc.update(input))
506
+ }
507
+
508
+ export function sandboxes(id: ProjectID) {
509
+ return runPromise((svc) => svc.sandboxes(id))
510
+ }
511
+
512
+ export function addSandbox(id: ProjectID, directory: string) {
513
+ return runPromise((svc) => svc.addSandbox(id, directory))
514
+ }
515
+
516
+ export function removeSandbox(id: ProjectID, directory: string) {
517
+ return runPromise((svc) => svc.removeSandbox(id, directory))
518
+ }
519
+ }