rovecode 0.4.0-beta.2 → 0.4.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 (428) hide show
  1. package/README.md +67 -69
  2. package/THIRD_PARTY_NOTICES.md +0 -44
  3. package/bin/rovecode.ts +21 -0
  4. package/package.json +16 -37
  5. package/src/account/keys.ts +97 -0
  6. package/src/account/login.ts +158 -0
  7. package/src/account/provision.ts +47 -0
  8. package/src/account/store.ts +63 -0
  9. package/src/acp/server.ts +373 -0
  10. package/src/cli/account-cmd.ts +116 -0
  11. package/src/cli/connect.ts +244 -0
  12. package/src/cli/context-cmd.ts +199 -0
  13. package/src/cli/dispatch.ts +109 -0
  14. package/src/cli/doctor.ts +324 -0
  15. package/src/cli/export.ts +278 -0
  16. package/src/cli/help.ts +240 -0
  17. package/src/cli/is-tui-invocation.ts +8 -0
  18. package/src/cli/main.ts +599 -0
  19. package/src/cli/market-cmd.ts +658 -0
  20. package/src/cli/mcp-market-cmd.ts +299 -0
  21. package/src/cli/output.ts +382 -0
  22. package/src/cli/repl.ts +172 -0
  23. package/src/cli/resume.ts +32 -0
  24. package/src/cli/run-limits.ts +78 -0
  25. package/src/cli/runtime.ts +792 -0
  26. package/src/cli/setup.ts +187 -0
  27. package/src/cli/update-cmd.ts +78 -0
  28. package/src/cli/workflow-cmd.ts +100 -0
  29. package/src/coding/checkpoints.ts +270 -0
  30. package/src/coding/diff.ts +136 -0
  31. package/src/coding/files.ts +339 -0
  32. package/src/coding/hashline.ts +319 -0
  33. package/src/coding/lsp.ts +406 -0
  34. package/src/coding/repomap-cache.ts +99 -0
  35. package/src/coding/repomap-files.ts +110 -0
  36. package/src/coding/repomap.ts +392 -0
  37. package/src/core/compaction.ts +399 -0
  38. package/src/core/config.ts +289 -0
  39. package/src/core/context-report.ts +228 -0
  40. package/src/core/context.ts +60 -0
  41. package/src/core/count-remote.ts +107 -0
  42. package/src/core/execpolicy-rules.ts +196 -0
  43. package/src/core/execpolicy.ts +385 -0
  44. package/src/core/executor.ts +397 -0
  45. package/src/core/guardrails.ts +400 -0
  46. package/src/core/hooks.ts +398 -0
  47. package/src/core/images.ts +230 -0
  48. package/src/core/intro.ts +236 -0
  49. package/src/core/loop.ts +621 -0
  50. package/src/core/modes.ts +372 -0
  51. package/src/core/orchestrator.ts +207 -0
  52. package/src/core/reflection.ts +165 -0
  53. package/src/core/sandbox-config.ts +167 -0
  54. package/src/core/session-images.ts +73 -0
  55. package/src/core/session.ts +398 -0
  56. package/src/core/settings.ts +98 -0
  57. package/src/core/stuck-detector.ts +273 -0
  58. package/src/core/tasks.ts +374 -0
  59. package/src/core/token-scale.ts +108 -0
  60. package/src/core/tool-output-budget.ts +166 -0
  61. package/src/core/tools.ts +288 -0
  62. package/src/core/types.ts +330 -0
  63. package/src/core/update-check.ts +171 -0
  64. package/src/core/update.ts +158 -0
  65. package/src/core/usage.ts +204 -0
  66. package/src/core/validate.ts +121 -0
  67. package/src/core/verify-gate.ts +159 -0
  68. package/src/core/verify.ts +237 -0
  69. package/src/core/voice.ts +158 -0
  70. package/src/core/win-job.ts +183 -0
  71. package/src/design/audit.ts +797 -0
  72. package/src/design/direction.ts +190 -0
  73. package/src/design/rules.ts +157 -0
  74. package/src/eval/bench.ts +150 -0
  75. package/src/eval/gauntlet-runner.ts +218 -0
  76. package/src/eval/gauntlet.ts +226 -0
  77. package/src/eval/grader.ts +186 -0
  78. package/src/eval/record.ts +202 -0
  79. package/src/eval/redact.ts +141 -0
  80. package/src/eval/replay.ts +147 -0
  81. package/src/eval/trajectory.ts +373 -0
  82. package/src/index.ts +17 -0
  83. package/src/market/catalogs/mcp-docs.json +111 -0
  84. package/src/market/catalogs/plugins.json +111 -0
  85. package/src/market/catalogs/skills.json +478 -0
  86. package/src/market/clone.ts +72 -0
  87. package/src/market/context-cost.ts +121 -0
  88. package/src/market/digest.ts +106 -0
  89. package/src/market/index.ts +22 -0
  90. package/src/market/install.ts +578 -0
  91. package/src/market/manifest.ts +187 -0
  92. package/src/market/prereq.ts +145 -0
  93. package/src/market/registry.ts +363 -0
  94. package/src/market/resolve.ts +111 -0
  95. package/src/market/types.ts +236 -0
  96. package/src/market/validate.ts +227 -0
  97. package/src/mcp/client.ts +431 -0
  98. package/src/mcp/config.ts +239 -0
  99. package/src/mcp/local-package.ts +211 -0
  100. package/src/mcp/market-catalog.ts +84 -0
  101. package/src/mcp/market-install.ts +289 -0
  102. package/src/mcp/market.ts +0 -0
  103. package/src/mcp/tools.ts +131 -0
  104. package/src/mcp/trust.ts +49 -0
  105. package/src/memory/blocks.ts +175 -0
  106. package/src/memory/recall.ts +355 -0
  107. package/src/memory/store.ts +105 -0
  108. package/src/memory/tools.ts +99 -0
  109. package/src/plugins/cli.ts +123 -0
  110. package/src/plugins/discover.ts +108 -0
  111. package/src/plugins/index.ts +50 -0
  112. package/src/plugins/init.ts +140 -0
  113. package/src/plugins/install.ts +184 -0
  114. package/src/plugins/load.ts +149 -0
  115. package/src/plugins/manifest.ts +106 -0
  116. package/src/plugins/state.ts +83 -0
  117. package/src/providers/auth.ts +293 -0
  118. package/src/providers/cache.ts +223 -0
  119. package/src/providers/catalog-local.ts +160 -0
  120. package/src/providers/catalog.ts +408 -0
  121. package/src/providers/middleware-context.ts +86 -0
  122. package/src/providers/middleware.ts +373 -0
  123. package/src/providers/profile-glm53.ts +111 -0
  124. package/src/providers/profile-sonnet5-persona.ts +65 -0
  125. package/src/providers/profile-sonnet5-voice.ts +23 -0
  126. package/src/providers/profiles.ts +156 -0
  127. package/src/providers/provider-config.ts +311 -0
  128. package/src/providers/registry.ts +302 -0
  129. package/src/providers/response-validation.ts +80 -0
  130. package/src/providers/retry.ts +234 -0
  131. package/src/providers/router.ts +294 -0
  132. package/src/providers/sse.ts +26 -0
  133. package/src/providers/stream-errors.ts +117 -0
  134. package/src/providers/stream.ts +569 -0
  135. package/src/providers/thinking.ts +189 -0
  136. package/src/providers/wire-messages.ts +129 -0
  137. package/src/sdk/client.ts +225 -0
  138. package/src/sdk/index.ts +3 -0
  139. package/src/server/dashboard.ts +144 -0
  140. package/src/server/http.ts +343 -0
  141. package/src/server/openapi.ts +246 -0
  142. package/src/sextant/card-hits.ts +102 -0
  143. package/src/sextant/card-keys.ts +55 -0
  144. package/src/sextant/context-source.ts +157 -0
  145. package/src/sextant/draw-agents.ts +273 -0
  146. package/src/sextant/draw-code.ts +388 -0
  147. package/src/sextant/draw-context.ts +222 -0
  148. package/src/sextant/draw-frame.ts +164 -0
  149. package/src/sextant/draw-market.ts +573 -0
  150. package/src/sextant/draw-messages.ts +386 -0
  151. package/src/sextant/draw-pet.ts +230 -0
  152. package/src/sextant/draw-plan.ts +159 -0
  153. package/src/sextant/draw-tabs.ts +85 -0
  154. package/src/sextant/draw-util.ts +65 -0
  155. package/src/sextant/engine.ts +230 -0
  156. package/src/sextant/frame-hits.ts +25 -0
  157. package/src/sextant/frame.ts +101 -0
  158. package/src/sextant/git-status.ts +197 -0
  159. package/src/sextant/grid.ts +59 -0
  160. package/src/sextant/input.ts +119 -0
  161. package/src/sextant/keys.ts +488 -0
  162. package/src/sextant/layout.ts +86 -0
  163. package/src/sextant/local-commands.ts +156 -0
  164. package/src/sextant/market-source.ts +287 -0
  165. package/src/sextant/mentions.ts +141 -0
  166. package/src/sextant/message-hits.ts +26 -0
  167. package/src/sextant/model.ts +387 -0
  168. package/src/sextant/overlays.ts +451 -0
  169. package/src/sextant/panel-hits.ts +38 -0
  170. package/src/sextant/pet.ts +399 -0
  171. package/src/sextant/screen.ts +324 -0
  172. package/src/sextant/scroll-hits.ts +66 -0
  173. package/src/sextant/scrollbar.ts +82 -0
  174. package/src/sextant/selection.ts +123 -0
  175. package/src/sextant/sextant-bridge.ts +174 -0
  176. package/src/sextant/sextant-cards.ts +142 -0
  177. package/src/sextant/sextant-diff-base.ts +63 -0
  178. package/src/sextant/sextant-files.ts +154 -0
  179. package/src/sextant/sextant-frame-loop.ts +314 -0
  180. package/src/sextant/sextant-renderer.ts +478 -0
  181. package/src/sextant/sextant-repo.ts +131 -0
  182. package/src/sextant/theme.ts +66 -0
  183. package/src/sextant/tool-rows.ts +189 -0
  184. package/src/sextant/types.ts +473 -0
  185. package/src/skills/index.ts +306 -0
  186. package/src/skills/tools.ts +69 -0
  187. package/src/skills/versioned.ts +227 -0
  188. package/src/telemetry/otel.ts +353 -0
  189. package/src/telemetry/otlp.ts +68 -0
  190. package/src/tools/ask-user.ts +156 -0
  191. package/src/tools/design.ts +151 -0
  192. package/src/tools/evalcell.ts +338 -0
  193. package/src/tools/html-text.ts +139 -0
  194. package/src/tools/provider.ts +149 -0
  195. package/src/tools/task.ts +216 -0
  196. package/src/tools/todo.ts +320 -0
  197. package/src/tools/webfetch.ts +331 -0
  198. package/src/tui/app.ts +608 -0
  199. package/src/tui/attach.ts +127 -0
  200. package/src/tui/checkpoints-cmd.ts +70 -0
  201. package/src/tui/clipboard-image.ts +81 -0
  202. package/src/tui/commands.ts +277 -0
  203. package/src/tui/cost.ts +108 -0
  204. package/src/tui/info-cmd.ts +144 -0
  205. package/src/tui/mcp-cmd.ts +128 -0
  206. package/src/tui/modes-cmd.ts +45 -0
  207. package/src/tui/overlays.ts +97 -0
  208. package/src/tui/pi-renderer.ts +424 -0
  209. package/src/tui/providers-cmd.ts +366 -0
  210. package/src/tui/renderer.ts +101 -0
  211. package/src/tui/replay-marker.ts +29 -0
  212. package/src/tui/session-cmd.ts +146 -0
  213. package/src/tui/sextant-attach.ts +68 -0
  214. package/src/tui/sextant-io.ts +184 -0
  215. package/src/tui/sextant-smoke.ts +110 -0
  216. package/src/tui/smoke.ts +72 -0
  217. package/src/tui/theme.ts +59 -0
  218. package/src/tui/todo-label.ts +7 -0
  219. package/src/workflow/engine.ts +266 -0
  220. package/tsconfig.json +30 -0
  221. package/vendor/pi-tui/LICENSE +21 -0
  222. package/vendor/pi-tui/PATCHES.md +12 -0
  223. package/vendor/pi-tui/PROVENANCE.md +12 -0
  224. package/vendor/pi-tui/README.upstream.md +854 -0
  225. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  226. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  227. package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
  228. package/vendor/pi-tui/src/autocomplete.ts +827 -0
  229. package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
  230. package/vendor/pi-tui/src/components/box.ts +138 -0
  231. package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
  232. package/vendor/pi-tui/src/components/editor.ts +2364 -0
  233. package/vendor/pi-tui/src/components/h-stack.ts +45 -0
  234. package/vendor/pi-tui/src/components/image.ts +128 -0
  235. package/vendor/pi-tui/src/components/input.ts +448 -0
  236. package/vendor/pi-tui/src/components/loader.ts +93 -0
  237. package/vendor/pi-tui/src/components/markdown.ts +1016 -0
  238. package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
  239. package/vendor/pi-tui/src/components/select-list.ts +230 -0
  240. package/vendor/pi-tui/src/components/settings-list.ts +277 -0
  241. package/vendor/pi-tui/src/components/spacer.ts +29 -0
  242. package/vendor/pi-tui/src/components/stack.ts +155 -0
  243. package/vendor/pi-tui/src/components/text.ts +108 -0
  244. package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
  245. package/vendor/pi-tui/src/components/v-stack.ts +34 -0
  246. package/vendor/pi-tui/src/editor-component.ts +75 -0
  247. package/vendor/pi-tui/src/fuzzy.ts +138 -0
  248. package/vendor/pi-tui/src/index.ts +149 -0
  249. package/vendor/pi-tui/src/keybindings.ts +321 -0
  250. package/vendor/pi-tui/src/keys.ts +1402 -0
  251. package/vendor/pi-tui/src/kill-ring.ts +47 -0
  252. package/vendor/pi-tui/src/latex.ts +1381 -0
  253. package/vendor/pi-tui/src/layout-node.ts +52 -0
  254. package/vendor/pi-tui/src/layout.ts +411 -0
  255. package/vendor/pi-tui/src/native-modifiers.ts +60 -0
  256. package/vendor/pi-tui/src/native-module-path.ts +32 -0
  257. package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
  258. package/vendor/pi-tui/src/terminal-colors.ts +74 -0
  259. package/vendor/pi-tui/src/terminal-image.ts +701 -0
  260. package/vendor/pi-tui/src/terminal.ts +554 -0
  261. package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
  262. package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
  263. package/vendor/pi-tui/src/tui.ts +1264 -0
  264. package/vendor/pi-tui/src/undo-stack.ts +29 -0
  265. package/vendor/pi-tui/src/utils.ts +1327 -0
  266. package/vendor/pi-tui/src/word-navigation.ts +118 -0
  267. package/vendor/pi-tui/test/test-themes.ts +39 -0
  268. package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
  269. package/CHANGELOG.md +0 -512
  270. package/bin/rovecode.js +0 -24
  271. package/dist/cli/app-dybnr56b.js +0 -2
  272. package/dist/cli/ask-user-p8hq4xgj.js +0 -2
  273. package/dist/cli/auth-login-ewpgw5sm.js +0 -2
  274. package/dist/cli/auth-m8p9grty.js +0 -2
  275. package/dist/cli/bench-xv3ypwev.js +0 -9
  276. package/dist/cli/catalog-737wb2s0.js +0 -2
  277. package/dist/cli/cli-arhg40m0.js +0 -2
  278. package/dist/cli/client-cf2pxx8q.js +0 -2
  279. package/dist/cli/commands-3p7e4xxs.js +0 -2
  280. package/dist/cli/connect-3q93d7cb.js +0 -2
  281. package/dist/cli/context-cmd-eqxmxhzq.js +0 -2
  282. package/dist/cli/context-report-hbw9zfes.js +0 -2
  283. package/dist/cli/count-remote-mby98cd0.js +0 -2
  284. package/dist/cli/design-122y0axd.js +0 -2
  285. package/dist/cli/dispatch-b4egzvvh.js +0 -2
  286. package/dist/cli/doctor-x4jkv72e.js +0 -3
  287. package/dist/cli/executor-ftvg6tsy.js +0 -2
  288. package/dist/cli/export-pdgdhkch.js +0 -2
  289. package/dist/cli/files-cez9a96p.js +0 -2
  290. package/dist/cli/gauntlet-r3xxaszc.js +0 -2
  291. package/dist/cli/gauntlet-runner-r515m7kk.js +0 -10
  292. package/dist/cli/gauntlet-wave3-bnkjk2v2.js +0 -5
  293. package/dist/cli/gauntlet-wave4-acs9s60q.js +0 -14
  294. package/dist/cli/hashline-ewg5hbe3.js +0 -2
  295. package/dist/cli/http-n0kehsk8.js +0 -5
  296. package/dist/cli/index-z5qt1s76.js +0 -2
  297. package/dist/cli/install-80mp63kx.js +0 -2
  298. package/dist/cli/loop-12twjcat.js +0 -2
  299. package/dist/cli/main-01pv9206.js +0 -4
  300. package/dist/cli/main-0jys2ccn.js +0 -3
  301. package/dist/cli/main-1ztz6fkj.js +0 -10
  302. package/dist/cli/main-23q7cmww.js +0 -9
  303. package/dist/cli/main-2rzbexn2.js +0 -3
  304. package/dist/cli/main-2wyax8k9.js +0 -9
  305. package/dist/cli/main-2yeveeve.js +0 -6
  306. package/dist/cli/main-2z3dek0b.js +0 -3
  307. package/dist/cli/main-2zgsknth.js +0 -3
  308. package/dist/cli/main-45ejth3a.js +0 -4
  309. package/dist/cli/main-45rn3trk.js +0 -22
  310. package/dist/cli/main-4p4e2w7x.js +0 -4
  311. package/dist/cli/main-4y0tnfpa.js +0 -16
  312. package/dist/cli/main-5py0rkmc.js +0 -4
  313. package/dist/cli/main-6dtqmbt6.js +0 -7
  314. package/dist/cli/main-6h9x282m.js +0 -4
  315. package/dist/cli/main-6vjeds42.js +0 -3
  316. package/dist/cli/main-78gq4bt9.js +0 -6
  317. package/dist/cli/main-7jd5vh3x.js +0 -4
  318. package/dist/cli/main-7kt6r53y.js +0 -4
  319. package/dist/cli/main-8c1tbazx.js +0 -58
  320. package/dist/cli/main-9a9rnh47.js +0 -19
  321. package/dist/cli/main-9ht36z12.js +0 -3
  322. package/dist/cli/main-a2yfvcy9.js +0 -7
  323. package/dist/cli/main-a3f51n0x.js +0 -5
  324. package/dist/cli/main-b8zq261k.js +0 -3
  325. package/dist/cli/main-bxtvnf6d.js +0 -13
  326. package/dist/cli/main-edxc3yzt.js +0 -4
  327. package/dist/cli/main-evgz4mp5.js +0 -21
  328. package/dist/cli/main-f33fc5je.js +0 -9
  329. package/dist/cli/main-fvnpq46y.js +0 -12
  330. package/dist/cli/main-gbbty4d4.js +0 -3
  331. package/dist/cli/main-gth53dnt.js +0 -25
  332. package/dist/cli/main-hqbz10aw.js +0 -9
  333. package/dist/cli/main-hrrvcfan.js +0 -38
  334. package/dist/cli/main-hzwtsb2m.js +0 -5
  335. package/dist/cli/main-j7ttv0sd.js +0 -34
  336. package/dist/cli/main-jak598k9.js +0 -5
  337. package/dist/cli/main-kba6zeyd.js +0 -6
  338. package/dist/cli/main-kwwsz6rq.js +0 -3
  339. package/dist/cli/main-m8vm17zq.js +0 -3
  340. package/dist/cli/main-mg4f96e1.js +0 -3
  341. package/dist/cli/main-mg9b20ac.js +0 -18
  342. package/dist/cli/main-mgb9ccnx.js +0 -3
  343. package/dist/cli/main-mjt2p7aj.js +0 -3
  344. package/dist/cli/main-n6qrdbmy.js +0 -3
  345. package/dist/cli/main-na7wse0x.js +0 -5
  346. package/dist/cli/main-nqveez48.js +0 -4
  347. package/dist/cli/main-ntqef02r.js +0 -10
  348. package/dist/cli/main-nvc3yjay.js +0 -136
  349. package/dist/cli/main-p0cfn6nr.js +0 -16
  350. package/dist/cli/main-qj2djy17.js +0 -19
  351. package/dist/cli/main-qsevpgsv.js +0 -3
  352. package/dist/cli/main-qvarybsp.js +0 -3
  353. package/dist/cli/main-rebtt91r.js +0 -5
  354. package/dist/cli/main-rpg7h8mb.js +0 -3
  355. package/dist/cli/main-rsy72qmw.js +0 -15
  356. package/dist/cli/main-rvetps99.js +0 -18
  357. package/dist/cli/main-s4bb0jav.js +0 -3
  358. package/dist/cli/main-s9v8k74e.js +0 -3
  359. package/dist/cli/main-tjvwmscs.js +0 -3
  360. package/dist/cli/main-tkgarpjj.js +0 -4
  361. package/dist/cli/main-v8y60bb2.js +0 -3
  362. package/dist/cli/main-vhrrq337.js +0 -3
  363. package/dist/cli/main-vp2dfb7s.js +0 -4
  364. package/dist/cli/main-vqbr22sz.js +0 -8
  365. package/dist/cli/main-vxnwe5xx.js +0 -18
  366. package/dist/cli/main-wgph00xf.js +0 -5
  367. package/dist/cli/main-wk2csfnj.js +0 -5
  368. package/dist/cli/main-wm997zjx.js +0 -3
  369. package/dist/cli/main-wpkyraxh.js +0 -3
  370. package/dist/cli/main-wqt32p5x.js +0 -4
  371. package/dist/cli/main-x9ct6y1a.js +0 -3
  372. package/dist/cli/main-xfekqh9m.js +0 -7
  373. package/dist/cli/main-xt9zc3n6.js +0 -7
  374. package/dist/cli/main-xx2z3zh5.js +0 -4
  375. package/dist/cli/main-y5c82rxr.js +0 -3
  376. package/dist/cli/main-yrjt2sqt.js +0 -14
  377. package/dist/cli/main-ys6zj3yr.js +0 -3
  378. package/dist/cli/main-ywbxshqc.js +0 -8
  379. package/dist/cli/main-z13755t8.js +0 -25
  380. package/dist/cli/main-zc7pyrbj.js +0 -4
  381. package/dist/cli/main.js +0 -279
  382. package/dist/cli/market-cmd-bm5xvn9f.js +0 -5
  383. package/dist/cli/mcp-login-bthtfpt7.js +0 -2
  384. package/dist/cli/mcp-market-cmd-mbeshfyd.js +0 -2
  385. package/dist/cli/notify-54v5z9dz.js +0 -2
  386. package/dist/cli/oauth-g5gme95c.js +0 -2
  387. package/dist/cli/output-satndjap.js +0 -16
  388. package/dist/cli/profiles-sfhpbq3m.js +0 -2
  389. package/dist/cli/provider-config-hv3xtdt4.js +0 -2
  390. package/dist/cli/provider-kwzq6g84.js +0 -2
  391. package/dist/cli/registry-fh0hdnyn.js +0 -2
  392. package/dist/cli/registry-y1y8e94r.js +0 -2
  393. package/dist/cli/repl-t4z03mqq.js +0 -11
  394. package/dist/cli/resume-fqt4chg8.js +0 -2
  395. package/dist/cli/run-flags-rysbag9t.js +0 -2
  396. package/dist/cli/runtime-j19fjbsa.js +0 -2
  397. package/dist/cli/sandbox-config-g4qxd7y5.js +0 -2
  398. package/dist/cli/server-r0b6bksk.js +0 -5
  399. package/dist/cli/session-arg-txmn5g4x.js +0 -2
  400. package/dist/cli/session-ed250d9j.js +0 -2
  401. package/dist/cli/sessions-cmd-adw7svfn.js +0 -7
  402. package/dist/cli/settings-y9rzcqx8.js +0 -2
  403. package/dist/cli/setup-jmbr11j0.js +0 -2
  404. package/dist/cli/sextant-smoke-tcth0vea.js +0 -5
  405. package/dist/cli/skills-cmd-zbdy99v6.js +0 -2
  406. package/dist/cli/smoke-1bg937kx.js +0 -8
  407. package/dist/cli/start-chat-p01cdks3.js +0 -12
  408. package/dist/cli/stream-4wmyaypz.js +0 -2
  409. package/dist/cli/task-eg4s093s.js +0 -2
  410. package/dist/cli/tasks-12v9rr9k.js +0 -2
  411. package/dist/cli/thinking-a5ngvqyh.js +0 -2
  412. package/dist/cli/todo-1wxpcecx.js +0 -2
  413. package/dist/cli/tools-2ftsya7w.js +0 -2
  414. package/dist/cli/tools-x1tj4fxm.js +0 -2
  415. package/dist/cli/trust-cmd-hccxehzb.js +0 -2
  416. package/dist/cli/update-check-ygt3vd7m.js +0 -2
  417. package/dist/cli/update-cmd-v23qhr8c.js +0 -2
  418. package/dist/cli/voice-g1gtck92.js +0 -2
  419. package/dist/cli/webfetch-0nnrjgb5.js +0 -2
  420. package/dist/cli/websearch-f0vr2p7d.js +0 -2
  421. package/dist/cli/workspace-9rq1w4ta.js +0 -2
  422. package/dist/lib/index.js +0 -62
  423. package/dist/lib/models-index.json +0 -1
  424. package/dist/lib/plugins.js +0 -6
  425. package/dist/lib/providers.js +0 -17
  426. package/dist/lib/public-api.js +0 -20
  427. package/dist/rovecode.exe +0 -4
  428. /package/{dist/cli → src/providers}/models-index.json +0 -0
@@ -0,0 +1,123 @@
1
+ /** `rovecode plugin …` — list · add · remove · enable · disable · trust · untrust · show. A pure
2
+ * command function over injected cwd/home/output so tests drive it without a process; cli/main.ts
3
+ * wires `case "plugin"` to it with process.cwd() and rovecodeHome(). Nothing here imports plugin
4
+ * code: `show` and `list` read manifests and folder digests only, so an untrusted project plugin can
5
+ * be inspected before it is ever run — that inspection is what `trust` asks the human to do. */
6
+
7
+ import { relative } from "node:path";
8
+ import { rovecodeHome } from "../providers/auth.ts";
9
+ import { discoverPlugins, type DiscoveredPlugin, type PluginScope } from "./discover.ts";
10
+ import { addPlugin, removePlugin, scopeRoot, setPluginEnabled, trustPlugin, untrustPlugin, type Spawn } from "./install.ts";
11
+ import { contributions } from "./manifest.ts";
12
+ import { pluginFiles, statePath } from "./state.ts";
13
+
14
+ export interface PluginCliDeps {
15
+ cwd?: string;
16
+ home?: string;
17
+ out?: (line: string) => void;
18
+ err?: (line: string) => void;
19
+ spawn?: Spawn;
20
+ }
21
+
22
+ export const PLUGIN_USAGE = [
23
+ "usage: rovecode plugin <command>",
24
+ " list every plugin in ~/.rovecode/plugins and .rovecode/plugins, with its status",
25
+ " init <name> [--project] scaffold a new plugin folder (manifest, entry, test, README) ready for `plugin add`",
26
+ " test [path] validate + activate a plugin in a dry harness, then run its plugin.test.ts",
27
+ " add <folder|git-url> [--project] [--force]",
28
+ " copy (or shallow-clone) a plugin into the user scope, or this repo's .rovecode/plugins",
29
+ " remove <name> [--project] delete the installed copy",
30
+ " enable <name> · disable <name>",
31
+ " switch a plugin off/on in every scope (~/.rovecode/plugins.json)",
32
+ " trust <name> approve a PROJECT plugin's current files on this machine — until they change",
33
+ " untrust <name> withdraw that approval",
34
+ " show <name> manifest, files and what it contributes, without running any of it",
35
+ "status: active · disabled · untrusted (project plugin not yet approved here) · broken (see problems)",
36
+ ].join("\n");
37
+
38
+ const flag = (args: string[], name: string): boolean => args.includes(name);
39
+ const words = (args: string[]): string[] => args.filter((a) => !a.startsWith("--"));
40
+
41
+ export async function cmdPlugin(args: string[], deps: PluginCliDeps = {}): Promise<number> {
42
+ const cwd = deps.cwd ?? process.cwd(), home = deps.home ?? rovecodeHome();
43
+ const out = deps.out ?? ((l) => console.log(l)), err = deps.err ?? ((l) => console.error(l));
44
+ const [sub = "list", ...rest] = args;
45
+ const scope: PluginScope = flag(rest, "--project") ? "project" : "user";
46
+ const name = words(rest)[0];
47
+ switch (sub) {
48
+ case "init": return await import("./init.ts").then((m) => m.cmdPluginInit(words(rest), { cwd, out, err }));
49
+ case "test": return await import("./init.ts").then((m) => m.cmdPluginTest(words(rest)[0], { cwd, home, out, err }));
50
+ case "list": case "ls": {
51
+ const { plugins, warnings } = discoverPlugins(cwd, { home });
52
+ if (plugins.length === 0) { out(`no plugins. add one: rovecode plugin add <folder|git-url> (user: ${scopeRoot("user", cwd, home)}, project: ${relative(cwd, scopeRoot("project", cwd, home)) || "."})`); }
53
+ for (const p of plugins) out(row(p));
54
+ for (const w of warnings) err(`warning: ${w}`);
55
+ return 0;
56
+ }
57
+ case "add": {
58
+ const src = name;
59
+ if (!src) { err(PLUGIN_USAGE); return 2; }
60
+ const r = await addPlugin(src, { cwd, home, scope, force: flag(rest, "--force"), ...(deps.spawn ? { spawn: deps.spawn } : {}) });
61
+ if (!r.ok) { err(`plugin add: ${r.error}`); return 1; }
62
+ out(`added ${r.name}@${r.manifest.version} (${r.scope}) → ${r.dir}`);
63
+ for (const c of contributions(r.manifest)) out(` ${c}`);
64
+ if (r.scope === "project") out(` trusted on this machine (recorded in ${statePath(home)}); a change to its files will ask again`);
65
+ out("restart rovecode to load it — plugins are read once per process, like hooks");
66
+ return 0;
67
+ }
68
+ case "remove": case "rm": {
69
+ if (!name) { err(PLUGIN_USAGE); return 2; }
70
+ const r = removePlugin(name, { cwd, home, scope });
71
+ if (!r.ok) { err(`plugin remove: ${r.error}`); return 1; }
72
+ out(`removed ${name} (${scope}) from ${r.dir}`);
73
+ return 0;
74
+ }
75
+ case "enable": case "disable": {
76
+ if (!name) { err(PLUGIN_USAGE); return 2; }
77
+ const known = discoverPlugins(cwd, { home }).plugins.some((p) => p.name === name);
78
+ if (!known) err(`warning: no installed plugin named "${name}" — the setting is recorded anyway`);
79
+ const path = setPluginEnabled(name, sub === "enable", home);
80
+ out(`${name} ${sub}d (${path})`);
81
+ return 0;
82
+ }
83
+ case "trust": {
84
+ if (!name) { err(PLUGIN_USAGE); return 2; }
85
+ const p = discoverPlugins(cwd, { home }).plugins.find((x) => x.name === name && x.scope === "project");
86
+ if (!p) { err(`plugin trust: no project plugin "${name}" under ${scopeRoot("project", cwd, home)}`); return 1; }
87
+ if (p.manifest === null) { err(`plugin trust: "${name}" is broken — fix it first:`); for (const w of p.problems) err(` ${w}`); return 1; }
88
+ const r = trustPlugin(name, { cwd, home });
89
+ if (!r.ok) { err(`plugin trust: ${r.error}`); return 1; }
90
+ out(`trusted ${name}@${p.manifest.version} — ${pluginFiles(p.dir).length} files, digest ${r.digest.slice(0, 12)}…`);
91
+ for (const c of contributions(p.manifest)) out(` ${c}`);
92
+ out("it loads on the next start; `rovecode plugin show " + name + "` lists every file it brings");
93
+ return 0;
94
+ }
95
+ case "untrust": {
96
+ if (!name) { err(PLUGIN_USAGE); return 2; }
97
+ out(untrustPlugin(name, { cwd, home }) ? `untrusted ${name}` : `${name} was not trusted`);
98
+ return 0;
99
+ }
100
+ case "show": case "info": {
101
+ if (!name) { err(PLUGIN_USAGE); return 2; }
102
+ const p = discoverPlugins(cwd, { home }).plugins.find((x) => x.name === name);
103
+ if (!p) { err(`plugin show: no plugin "${name}"`); return 1; }
104
+ out(row(p));
105
+ out(` folder: ${p.dir}`);
106
+ if (p.digest) out(` digest: ${p.digest}`);
107
+ if (p.manifest) for (const c of contributions(p.manifest)) out(` ${c}`);
108
+ for (const f of pluginFiles(p.dir)) out(` - ${f}`);
109
+ for (const w of p.problems) out(` ! ${w}`);
110
+ if (p.status === "untrusted") out(` run \`rovecode plugin trust ${p.name}\` after reading the files above`);
111
+ return 0;
112
+ }
113
+ case "help": case "--help": case "-h": out(PLUGIN_USAGE); return 0;
114
+ default: err(`unknown plugin command "${sub}"\n${PLUGIN_USAGE}`); return 2;
115
+ }
116
+ }
117
+
118
+ /** one list row: `name@version scope status — description` */
119
+ export function row(p: DiscoveredPlugin): string {
120
+ const v = p.manifest ? `@${p.manifest.version}` : "";
121
+ const desc = p.manifest?.description ? ` — ${p.manifest.description}` : "";
122
+ return `${(p.name + v).padEnd(28)} ${p.scope.padEnd(8)} ${p.status.padEnd(9)}${desc}`.trimEnd();
123
+ }
@@ -0,0 +1,108 @@
1
+ /** Plugins: discovery. Two roots — `~/.rovecode/plugins/<name>/` (user) and `<cwd>/.rovecode/plugins/
2
+ * <name>/` (project) — each subfolder with a `plugin.json` is a plugin. Sync and filesystem-only:
3
+ * nothing is imported here, so `rovecode plugin list` can show a hostile plugin without running it.
4
+ *
5
+ * Status decides what load.ts may do with a plugin:
6
+ * active — will be imported and wired
7
+ * disabled — the human switched it off (plugins.json)
8
+ * untrusted — a PROJECT plugin whose folder digest the human has not approved on this machine;
9
+ * listed, never imported (state.ts explains why the record lives in the user home)
10
+ * broken — no usable manifest; listed with its reasons so the author can fix it
11
+ * Same name in both roots: the project copy shadows the user copy (the commands loader's rule) and
12
+ * says so. ROVECODE_NO_PLUGINS=1 skips discovery the way ROVECODE_NO_HOOKS=1 skips hooks. */
13
+
14
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
15
+ import { join, resolve } from "node:path";
16
+ import { rovecodeHome } from "../providers/auth.ts";
17
+ import { normalizeEntry, type McpServerConfig } from "../mcp/config.ts";
18
+ import { MANIFEST_FILE, parseManifest, type PluginManifest } from "./manifest.ts";
19
+ import { isDir, isTrusted, loadState, pluginDigest, type PluginState } from "./state.ts";
20
+
21
+ export type PluginScope = "user" | "project";
22
+ export type PluginStatus = "active" | "disabled" | "untrusted" | "broken";
23
+
24
+ export interface DiscoveredPlugin {
25
+ name: string;
26
+ /** absolute plugin folder */
27
+ dir: string;
28
+ scope: PluginScope;
29
+ manifest: PluginManifest | null;
30
+ status: PluginStatus;
31
+ /** folder digest (state.ts); null when the folder could not be walked */
32
+ digest: string | null;
33
+ /** why it is broken / what the manifest dropped — the author's fix list */
34
+ problems: string[];
35
+ /** the declarative halves, resolved here because they need no code run: the runtime wires them
36
+ * synchronously at construction (the commands and skills loaders, the MCP config) and only the
37
+ * entry module waits for load.ts. Filled for every plugin with a manifest; the runtime reads
38
+ * them for ACTIVE ones only. */
39
+ commandsDir: string | null;
40
+ skillsDir: string | null;
41
+ mcp: McpServerConfig[];
42
+ }
43
+
44
+ export interface DiscoverOptions { home?: string; state?: PluginState }
45
+ export interface Discovered { plugins: DiscoveredPlugin[]; warnings: string[] }
46
+
47
+ /** the two roots, project last so it shadows; one root when cwd IS the home (commands.ts idiom) */
48
+ export function pluginRoots(cwd: string, home: string): [PluginScope, string][] {
49
+ const user = join(home, "plugins"), project = join(cwd, ".rovecode", "plugins");
50
+ return resolve(user) === resolve(project) ? [["project", project]] : [["user", user], ["project", project]];
51
+ }
52
+
53
+ export function discoverPlugins(cwd: string, opts: DiscoverOptions = {}): Discovered {
54
+ const out: Discovered = { plugins: [], warnings: [] };
55
+ if (process.env.ROVECODE_NO_PLUGINS === "1") return out;
56
+ const home = opts.home ?? rovecodeHome();
57
+ const state = opts.state ?? loadState(home);
58
+ const byName = new Map<string, DiscoveredPlugin>();
59
+ for (const [scope, root] of pluginRoots(cwd, home)) {
60
+ if (!isDir(root)) continue;
61
+ let names: string[];
62
+ try { names = readdirSync(root).sort(); } catch (e) { out.warnings.push(`${root}: cannot list — ${(e as Error).message}`); continue; }
63
+ for (const folder of names) {
64
+ const dir = join(root, folder);
65
+ if (!isDir(dir) || !existsSync(join(dir, MANIFEST_FILE))) continue; // a stray file or a folder without a manifest is not a plugin
66
+ const p = readPlugin(dir, folder, scope, state);
67
+ const prev = byName.get(p.name);
68
+ if (prev) out.warnings.push(`${dir}: plugin "${p.name}" shadows ${prev.dir} (project over user)`);
69
+ byName.set(p.name, p);
70
+ for (const w of p.problems) out.warnings.push(w);
71
+ }
72
+ }
73
+ out.plugins = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
74
+ return out;
75
+ }
76
+
77
+ /** one folder → its DiscoveredPlugin: manifest, status, digest, and the declarative halves resolved
78
+ * (a named commands/skills dir must exist; MCP entries go through the same normalizer as mcp.json) */
79
+ export function readPlugin(dir: string, folder: string, scope: PluginScope, state: PluginState): DiscoveredPlugin {
80
+ const file = join(dir, MANIFEST_FILE);
81
+ const problems: string[] = [];
82
+ let manifest: PluginManifest | null = null;
83
+ try { manifest = parseManifest(readFileSync(file, "utf8"), file, problems); }
84
+ catch (e) { problems.push(`${file}: cannot read — ${(e as Error).message}`); }
85
+ let digest: string | null = null;
86
+ try { digest = pluginDigest(dir); } catch { digest = null; }
87
+ const name = manifest?.name ?? folder;
88
+ if (manifest && manifest.name !== folder) problems.push(`${file}: name "${manifest.name}" differs from its folder "${folder}" — the manifest name is the plugin's identity`);
89
+ let status: PluginStatus;
90
+ if (manifest === null) status = "broken";
91
+ else if (state.disabled.includes(name)) status = "disabled";
92
+ else if (scope === "project" && (digest === null || !isTrusted(state, dir, digest))) status = "untrusted";
93
+ else status = "active";
94
+ const tag = `plugin ${name}`;
95
+ const commandsDir = manifest ? dirOrProblem(dir, manifest.commands, `${tag}: commands`, problems) : null;
96
+ const skillsDir = manifest ? dirOrProblem(dir, manifest.skills, `${tag}: skills`, problems) : null;
97
+ const mcp: McpServerConfig[] = [];
98
+ if (manifest?.mcp) for (const [n, raw] of Object.entries(manifest.mcp)) { const c = normalizeEntry(n, raw, file, problems); if (c) mcp.push(c); }
99
+ return { name, dir, scope, manifest, status, digest, problems, commandsDir, skillsDir, mcp };
100
+ }
101
+
102
+ function dirOrProblem(base: string, rel: string | undefined, what: string, problems: string[]): string | null {
103
+ if (!rel) return null;
104
+ const p = join(base, rel);
105
+ try { if (statSync(p).isDirectory()) return p; } catch { /* below */ }
106
+ problems.push(`${what} directory "${rel}" does not exist — ignored`);
107
+ return null;
108
+ }
@@ -0,0 +1,50 @@
1
+ /** Plugins (the package, not the surface). A plugin is a folder with a `plugin.json` that bundles what
2
+ * rovecode can already be extended with — an in-process module of tools and hooks, custom commands,
3
+ * skills, MCP servers — so one `rovecode plugin add` installs all of it, `plugin list` shows all of it,
4
+ * and a project can ship its own under `.rovecode/plugins/` behind a per-machine trust step.
5
+ *
6
+ * Module map: manifest.ts (plugin.json → PluginManifest) · state.ts (~/.rovecode/plugins.json:
7
+ * disabled + trusted digests) · discover.ts (the two roots → DiscoveredPlugin with a status, no code
8
+ * run) · load.ts (import + validate the active ones → typed contributions) · install.ts (the human's
9
+ * verbs) · cli.ts (`rovecode plugin …`). `loadPlugins` is the one call the runtime makes. */
10
+
11
+ import { rovecodeHome } from "../providers/auth.ts";
12
+ import { discoverPlugins, type DiscoveredPlugin } from "./discover.ts";
13
+ import { activatePlugins, type LoadedPlugin } from "./load.ts";
14
+ import { loadState } from "./state.ts";
15
+
16
+ export type { PluginManifest } from "./manifest.ts";
17
+ export { PLUGIN_API_VERSION, MANIFEST_FILE, parseManifest, contributions } from "./manifest.ts";
18
+ export type { PluginState } from "./state.ts";
19
+ export { loadState, saveState, statePath, pluginDigest, pluginFiles } from "./state.ts";
20
+ export type { DiscoveredPlugin, PluginScope, PluginStatus } from "./discover.ts";
21
+ export { discoverPlugins, pluginRoots } from "./discover.ts";
22
+ export type { LoadedPlugin, PluginModule, PluginCtx } from "./load.ts";
23
+ export { activatePlugins, DEFAULT_PLUGIN_TIMEOUT_MS } from "./load.ts";
24
+ export { addPlugin, removePlugin, setPluginEnabled, trustPlugin, untrustPlugin, scopeRoot, isGitSource } from "./install.ts";
25
+ export { cmdPlugin, PLUGIN_USAGE } from "./cli.ts";
26
+
27
+ export interface LoadPluginsOptions { home?: string; timeoutMs?: number }
28
+ export interface LoadedPlugins {
29
+ /** every discovered plugin, active ones carrying their contributions */
30
+ plugins: LoadedPlugin[];
31
+ /** discovery + activation notes, in order — for the boot transcript */
32
+ warnings: string[];
33
+ }
34
+
35
+ /** discover, then activate the active ones; the runtime's single entry point */
36
+ export async function loadPlugins(cwd: string, opts: LoadPluginsOptions = {}): Promise<LoadedPlugins> {
37
+ const home = opts.home ?? rovecodeHome();
38
+ const found = discoverPlugins(cwd, { home, state: loadState(home) });
39
+ const act = await activatePlugins(found.plugins, { cwd, home, ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}) });
40
+ return { plugins: act.plugins, warnings: [...found.warnings, ...act.warnings] };
41
+ }
42
+
43
+ /** one line per plugin the boot transcript can echo: `plugins: 2 active (safety-net, notes) · 1 untrusted (acme)` */
44
+ export function summarizePlugins(p: readonly Pick<DiscoveredPlugin, "name" | "status">[]): string | null {
45
+ if (p.length === 0) return null;
46
+ const by = (s: DiscoveredPlugin["status"]): string[] => p.filter((x) => x.status === s).map((x) => x.name);
47
+ const parts: string[] = [];
48
+ for (const s of ["active", "untrusted", "disabled", "broken"] as const) { const n = by(s); if (n.length) parts.push(`${n.length} ${s} (${n.join(", ")})`); }
49
+ return `plugins: ${parts.join(" · ")}`;
50
+ }
@@ -0,0 +1,140 @@
1
+ /** `rovecode plugin init` + `rovecode plugin test` (F4, sdk-blueprint.md §6).
2
+ *
3
+ * init: scaffolds a plugin folder (manifest with an empty permissions declaration, a
4
+ * tool+hook entry, a bun:test file, a README) into <cwd>/<name> — or straight into a
5
+ * scope with --project. Nothing is installed; `plugin add` is the install step.
6
+ *
7
+ * test: the development loop — parse the manifest, activate the plugin in a dry harness
8
+ * (activatePlugins against a synthetic discovery entry, cwd = a throwaway tmp dir so a
9
+ * misbehaving module cannot touch the repo), report every contribution and warning, then
10
+ * run the plugin's own plugin.test.ts under `bun test` when present. Exit codes: 2 usage,
11
+ * 1 validation/activation problems or failing tests, 0 clean. */
12
+
13
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join, resolve } from "node:path";
16
+ import { pathToFileURL } from "node:url";
17
+ import { parseManifest, contributions, PLUGIN_API_VERSION, MANIFEST_FILE, type PluginManifest } from "./manifest.ts";
18
+ import { activatePlugins } from "./load.ts";
19
+ import type { DiscoveredPlugin } from "./discover.ts";
20
+
21
+ interface Out { cwd: string; out: (l: string) => void; err: (l: string) => void }
22
+
23
+ const NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
24
+
25
+ const ENTRY_TS = `/** <name> — a rovecode plugin. The default export is the whole contract:
26
+ * api (PLUGIN_API_VERSION), tools (Tool[] or a factory), hooks (HookSet).
27
+ * Docs: docs/design/sdk-blueprint.md §6 */
28
+ import type { Tool, HookSet } from "rovecode/core"; // resolved by rovecode at load time
29
+
30
+ const hello: Tool = {
31
+ schema: {
32
+ name: "<tool>_hello",
33
+ description: "Says hello from the <name> plugin.",
34
+ args: { type: "object", properties: { who: { type: "string" } } },
35
+ },
36
+ kind: "read", // read = no workspace effect; kinds map to policy actions (see plugin.json permissions)
37
+ // sequential: false — concurrent with siblings in a batch; only stateful tools go sequential
38
+ async execute(args) {
39
+ const a = args as { who?: string };
40
+ return { ok: true, output: \`hello, \${typeof a.who === "string" ? a.who : "world"}!\` };
41
+ },
42
+ };
43
+
44
+ const hooks: HookSet = {
45
+ // pre_tool: async ({ tool }) => tool === "bash" ? { deny: "nope" } : undefined,
46
+ };
47
+
48
+ export default { api: ${PLUGIN_API_VERSION}, tools: [hello], hooks };
49
+ `;
50
+
51
+ const TEST_TS = `/** The plugin's own tests — \`rovecode plugin test\` runs this file under bun:test. */
52
+ import { test, expect } from "bun:test";
53
+ import plugin from "./index.ts";
54
+
55
+ test("module shape", () => {
56
+ expect(plugin.api).toBe(${PLUGIN_API_VERSION});
57
+ expect(Array.isArray(plugin.tools)).toBe(true);
58
+ });
59
+
60
+ test("hello tool", async () => {
61
+ const tool = (plugin.tools ?? [])[0]!;
62
+ const out = await tool.execute({ who: "tests" }, {} as never);
63
+ expect(out.ok).toBe(true);
64
+ expect(out.output).toContain("hello, tests!");
65
+ });
66
+ `;
67
+
68
+ const README_MD = (name: string): string => `# ${name}
69
+
70
+ A rovecode plugin.
71
+
72
+ - \\\"permissions\\\" in plugin.json declares the policy actions its tools may take
73
+ (file.read, file.write, shell.exec, spawn, memory.write, net.fetch). A tool whose
74
+ kind is not declared is refused at run time — the list is a promise, not a comment.
75
+ - Develop: \\\`rovecode plugin test .\\\` in this folder.
76
+ - Install: \\\`rovecode plugin add .\\\` (user scope) or \\\`rovecode plugin add . --project\\\`.
77
+ `;
78
+
79
+ export async function cmdPluginInit(words: string[], o: Out): Promise<number> {
80
+ const name = words[0];
81
+ if (!name || !NAME_RE.test(name)) { o.err(`plugin init: name must match ${NAME_RE}`); return 2; }
82
+ const dir = join(o.cwd, name);
83
+ if (existsSync(dir)) { o.err(`plugin init: ${dir} already exists`); return 1; }
84
+ mkdirSync(dir, { recursive: true });
85
+ const manifest = {
86
+ name,
87
+ version: "0.1.0",
88
+ description: `${name} plugin`,
89
+ api: PLUGIN_API_VERSION,
90
+ entry: "index.ts",
91
+ permissions: [] as string[],
92
+ };
93
+ writeFileSync(join(dir, MANIFEST_FILE), JSON.stringify(manifest, null, 2) + "\n");
94
+ writeFileSync(join(dir, "index.ts"), ENTRY_TS.replaceAll("<name>", name).replaceAll("<tool>", name.replaceAll("-", "_")));
95
+ writeFileSync(join(dir, "plugin.test.ts"), TEST_TS);
96
+ writeFileSync(join(dir, "README.md"), README_MD(name));
97
+ o.out(`scaffolded ${name} at ${dir}`);
98
+ o.out(`next: rovecode plugin test ${dir} · rovecode plugin add ${dir}`);
99
+ return 0;
100
+ }
101
+
102
+ export async function cmdPluginTest(path: string | undefined, o: { cwd: string; home: string; out: (l: string) => void; err: (l: string) => void }): Promise<number> {
103
+ const dir = resolve(o.cwd, path ?? ".");
104
+ const mf = join(dir, MANIFEST_FILE);
105
+ if (!existsSync(mf)) { o.err(`plugin test: no ${MANIFEST_FILE} in ${dir}`); return 2; }
106
+ const warnings: string[] = [];
107
+ const manifest = parseManifest(readFileSync(mf, "utf8"), mf, warnings);
108
+ if (!manifest) { for (const w of warnings) o.err(w); return 1; }
109
+ o.out(`manifest ok: ${manifest.name}@${manifest.version} (api ${manifest.api})`);
110
+ for (const c of contributions(manifest)) o.out(` ${c}`);
111
+
112
+ // dry activation: cwd is a throwaway dir, so a module that writes "to the project" writes nowhere real
113
+ const scratch = mkdtempSync(join(tmpdir(), "rovecode-plugintest-"));
114
+ try {
115
+ const discovered: DiscoveredPlugin = {
116
+ dir, name: manifest.name, scope: "project", status: "active", manifest,
117
+ digest: null, problems: [], commandsDir: null, skillsDir: null, mcp: [],
118
+ };
119
+ const { plugins, warnings: aw } = await activatePlugins([discovered], { cwd: scratch, home: scratch });
120
+ for (const w of aw) o.err(`warning: ${w}`);
121
+ const p = plugins[0];
122
+ const tools = p?.tools ?? [];
123
+ o.out(`activated: ${tools.length} tool(s)${tools.length ? ` [${tools.map((t) => `${t.schema.name}:${t.kind}`).join(", ")}]` : ""}, hooks ${p?.hooks ? "yes" : "none"}`);
124
+ if (aw.length > 0) return 1;
125
+ } finally {
126
+ rmSync(scratch, { recursive: true, force: true });
127
+ }
128
+
129
+ const testFile = join(dir, "plugin.test.ts");
130
+ if (existsSync(testFile)) {
131
+ o.out(`running ${testFile} …`);
132
+ const proc = Bun.spawn([process.execPath, "test", testFile], { cwd: dir, stdout: "inherit", stderr: "inherit" });
133
+ const code = await proc.exited;
134
+ if (code !== 0) { o.err(`plugin.test.ts failed (exit ${code})`); return 1; }
135
+ } else {
136
+ o.out("no plugin.test.ts — add one for `plugin test` to exercise behavior, not just shape");
137
+ }
138
+ o.out("plugin test: ok");
139
+ return 0;
140
+ }
@@ -0,0 +1,184 @@
1
+ /** Plugins: the human's verbs — add, remove, enable, disable, trust. Each is a small filesystem
2
+ * operation plus a plugins.json edit; none imports plugin code (that is load.ts, at runtime, after
3
+ * these decisions). `add` takes a folder or a git URL: a URL is cloned shallow into a temp folder and
4
+ * from there on treated exactly like a folder (validated BEFORE anything is copied into place — a
5
+ * folder without a usable manifest never lands in ~/.rovecode/plugins). Adding into the project scope
6
+ * records the folder's digest as trusted: the human ran the command, that is the yes. */
7
+
8
+ import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join, resolve, sep } from "node:path";
11
+ import { pluginRoots, type PluginScope } from "./discover.ts";
12
+ import { MANIFEST_FILE, parseManifest, type PluginManifest } from "./manifest.ts";
13
+ import { isDir, loadState, pluginDigest, saveState, trustKey } from "./state.ts";
14
+
15
+ export type Spawn = (cmd: string[], cwd: string) => Promise<{ code: number; stderr: string }>;
16
+ /** the copy step, injectable for one reason: a test cannot otherwise arrange a copy that fails PART WAY
17
+ * through, which is the only failure the staging directory exists to survive. A real half-copy needs a
18
+ * full disk or a locked file; a fake one needs three lines. */
19
+ export type CopyTree = (from: string, to: string, filter: (path: string) => boolean) => void;
20
+ export interface InstallOptions {
21
+ cwd: string;
22
+ home: string;
23
+ scope: PluginScope;
24
+ /** replace an existing plugin of the same name */
25
+ force?: boolean;
26
+ /** the plugin's folder INSIDE the source, when the source is a repository that publishes several.
27
+ *
28
+ * Every real plugin is one: rovecode's own three live in `plugins/safety-net`, `plugins/notes` and
29
+ * `plugins/conventional-commits`, and a catalog could not install any of them while `add` only looked
30
+ * at the clone root. Rejected rather than normalised: an absolute path, a drive letter, or any `..`
31
+ * segment — a subfolder that climbs out of the clone is an attempt, not a typo. */
32
+ subfolder?: string;
33
+ /** see {@link CopyTree} — real installs never pass this */
34
+ copy?: CopyTree;
35
+ /** Clones already made during THIS command, keyed by `cloneKey`. One monorepo publishes all three
36
+ * first-party plugins, so installing them today clones the same repository three times — measured at
37
+ * 15.5 s per plugin install, 28.3 s for a project-scope one. The map is opened per command and disposed
38
+ * with `disposeCloneCache`; nothing here caches across commands, because a clone that outlives the
39
+ * command it was made for is a stale tree waiting to be installed from. */
40
+ cloneCache?: Map<string, string>;
41
+ /** how `git clone` runs; default Bun.spawn — tests inject one that never touches the network */
42
+ spawn?: Spawn;
43
+ }
44
+
45
+ /** The key a clone is cached under.
46
+ *
47
+ * `ref` is not something `addPlugin` takes yet — pinning is not implemented — but it is part of the key
48
+ * from the start, and deliberately so: a cache keyed on the URL alone hands back the wrong tree the day
49
+ * two refs of one repository are in play, and that bug would appear in a feature far away from here. With
50
+ * the key in one function, whoever adds pinning changes this and every caller inherits it. */
51
+ export const cloneKey = (source: string, ref?: string): string => `${source}#${ref ?? ""}`;
52
+
53
+ /** Remove every clone a command made. Call it in a `finally`: a half-written clone that outlives its
54
+ * command is litter in %TEMP% that nobody will ever look at again. */
55
+ export function disposeCloneCache(cache: Map<string, string> | undefined): void {
56
+ if (cache === undefined) return;
57
+ for (const dir of cache.values()) rmSync(dir, { recursive: true, force: true });
58
+ cache.clear();
59
+ }
60
+ export type AddResult = { ok: true; name: string; dir: string; scope: PluginScope; manifest: PluginManifest } | { ok: false; error: string };
61
+
62
+ const GIT_URL_RE = /^(?:https?:\/\/|git@|ssh:\/\/|git:\/\/)|\.git$/i;
63
+ export const isGitSource = (s: string): boolean => GIT_URL_RE.test(s) && !existsSync(s);
64
+
65
+ const defaultSpawn: Spawn = async (cmd, cwd) => {
66
+ const proc = Bun.spawn(cmd, { cwd, stdout: "ignore", stderr: "pipe", stdin: "ignore" });
67
+ const stderr = await new Response(proc.stderr).text();
68
+ return { code: await proc.exited, stderr };
69
+ };
70
+
71
+ /** the folder a scope's plugins live in */
72
+ export function scopeRoot(scope: PluginScope, cwd: string, home: string): string {
73
+ return pluginRoots(cwd, home).find(([s]) => s === scope)?.[1] ?? join(home, "plugins");
74
+ }
75
+
76
+ /** true for a symbolic link; an entry we cannot stat is treated as one and skipped */
77
+ function isSymlink(p: string): boolean {
78
+ try { return lstatSync(p).isSymbolicLink(); } catch { return true; }
79
+ }
80
+
81
+ export async function addPlugin(source: string, opts: InstallOptions): Promise<AddResult> {
82
+ let src = source, tmp: string | null = null;
83
+ const copy: CopyTree = opts.copy ?? ((from, to, filter) => cpSync(from, to, { recursive: true, filter }));
84
+ try {
85
+ if (isGitSource(source)) {
86
+ const key = cloneKey(source);
87
+ const cached = opts.cloneCache?.get(key);
88
+ if (cached !== undefined && isDir(cached)) {
89
+ src = cached; // owned by the cache, so `tmp` stays null and nothing is removed
90
+ } else {
91
+ tmp = mkdtempSync(join(tmpdir(), "rovecode-plugin-"));
92
+ const r = await (opts.spawn ?? defaultSpawn)(["git", "clone", "--depth", "1", "--quiet", "--", source, "."], tmp);
93
+ if (r.code !== 0) return { ok: false, error: `git clone failed (exit ${r.code})${r.stderr.trim() ? `: ${r.stderr.trim().split("\n").at(-1)}` : ""}` };
94
+ src = tmp;
95
+ if (opts.cloneCache !== undefined) { opts.cloneCache.set(key, tmp); tmp = null; } // ownership moves
96
+ }
97
+ }
98
+ src = resolve(src);
99
+ if (opts.subfolder !== undefined && opts.subfolder !== "") {
100
+ const sub = opts.subfolder.split("\\").join("/");
101
+ if (sub.startsWith("/") || /^[A-Za-z]:/.test(sub) || sub.split("/").includes("..")) {
102
+ return { ok: false, error: `subfolder "${opts.subfolder}" escapes the source` };
103
+ }
104
+ const inner = resolve(join(src, sub));
105
+ // resolve() alone is not the check: it happily returns a path outside src for a crafted input
106
+ if (inner !== src && !inner.startsWith(src + sep)) return { ok: false, error: `subfolder "${opts.subfolder}" escapes the source` };
107
+ if (!isDir(inner)) return { ok: false, error: `${source}: no folder "${opts.subfolder}" inside it` };
108
+ src = inner;
109
+ }
110
+ if (!isDir(src)) return { ok: false, error: `${source}: not a directory` };
111
+ const file = join(src, MANIFEST_FILE);
112
+ if (!existsSync(file)) return { ok: false, error: `${source}: no ${MANIFEST_FILE} — a plugin is a folder with a manifest` };
113
+ const warnings: string[] = [];
114
+ const manifest = parseManifest(readFileSync(file, "utf8"), file, warnings);
115
+ if (manifest === null) return { ok: false, error: warnings.join("; ") || `${file}: invalid manifest` };
116
+ const root = scopeRoot(opts.scope, opts.cwd, opts.home);
117
+ const dest = join(root, manifest.name);
118
+ if (resolve(dest) === src) return { ok: false, error: `${source} is already the installed copy` };
119
+ if (existsSync(dest) && !opts.force) return { ok: false, error: `plugin "${manifest.name}" already exists at ${dest} (use --force to replace)` };
120
+ // Copied beside the target and swapped in. `rmSync(dest)` then `cpSync` reads naturally and is wrong:
121
+ // between those two lines the plugin does not exist, and anything that ends the copy early — a full
122
+ // disk, a file another process holds open on Windows, ^C during `market update --all` — has already
123
+ // deleted a working plugin and leaves a half-written folder wearing its name. Rename is the one step
124
+ // the filesystem will not perform half way.
125
+ mkdirSync(root, { recursive: true });
126
+ const staging = mkdtempSync(join(root, `.rovecode-add-${manifest.name}-`));
127
+ try {
128
+ copy(src, staging, (p) => !/(?:^|[\\/])(?:node_modules|\.git)(?:[\\/]|$)/.test(p) && !isSymlink(p));
129
+ if (existsSync(dest)) rmSync(dest, { recursive: true, force: true });
130
+ renameSync(staging, dest);
131
+ } catch (e) {
132
+ // a returned error, not a thrown one: every other way this function fails is a value, and `market
133
+ // install a b c` should report which one could not be copied and carry on with the rest
134
+ return { ok: false, error: `could not install "${manifest.name}" into ${dest}: ${e instanceof Error ? e.message : String(e)}` };
135
+ } finally {
136
+ rmSync(staging, { recursive: true, force: true }); // already gone once the rename succeeded
137
+ }
138
+ if (opts.scope === "project") { // the human installed it: trust this exact content on this machine
139
+ const state = loadState(opts.home);
140
+ state.trusted[trustKey(dest)] = pluginDigest(dest);
141
+ saveState(opts.home, state);
142
+ }
143
+ return { ok: true, name: manifest.name, dir: dest, scope: opts.scope, manifest };
144
+ } finally {
145
+ if (tmp) rmSync(tmp, { recursive: true, force: true });
146
+ }
147
+ }
148
+
149
+ export function removePlugin(name: string, opts: { cwd: string; home: string; scope: PluginScope }): { ok: true; dir: string } | { ok: false; error: string } {
150
+ const dir = join(scopeRoot(opts.scope, opts.cwd, opts.home), name);
151
+ if (!isDir(dir)) return { ok: false, error: `no ${opts.scope} plugin "${name}" at ${dir}` };
152
+ rmSync(dir, { recursive: true, force: true });
153
+ const state = loadState(opts.home);
154
+ delete state.trusted[trustKey(dir)];
155
+ saveState(opts.home, state);
156
+ return { ok: true, dir };
157
+ }
158
+
159
+ export function setPluginEnabled(name: string, enabled: boolean, home: string): string {
160
+ const state = loadState(home);
161
+ state.disabled = state.disabled.filter((n) => n !== name);
162
+ if (!enabled) state.disabled.push(name);
163
+ return saveState(home, state);
164
+ }
165
+
166
+ /** approve a project plugin's CURRENT content; a later change to any file asks again */
167
+ export function trustPlugin(name: string, opts: { cwd: string; home: string }): { ok: true; dir: string; digest: string } | { ok: false; error: string } {
168
+ const dir = join(scopeRoot("project", opts.cwd, opts.home), name);
169
+ if (!isDir(dir) || !existsSync(join(dir, MANIFEST_FILE))) return { ok: false, error: `no project plugin "${name}" under ${scopeRoot("project", opts.cwd, opts.home)}` };
170
+ const digest = pluginDigest(dir);
171
+ const state = loadState(opts.home);
172
+ state.trusted[trustKey(dir)] = digest;
173
+ saveState(opts.home, state);
174
+ return { ok: true, dir, digest };
175
+ }
176
+
177
+ export function untrustPlugin(name: string, opts: { cwd: string; home: string }): boolean {
178
+ const dir = join(scopeRoot("project", opts.cwd, opts.home), name);
179
+ const state = loadState(opts.home);
180
+ const had = trustKey(dir) in state.trusted;
181
+ delete state.trusted[trustKey(dir)];
182
+ saveState(opts.home, state);
183
+ return had;
184
+ }