@toolu/opencode 6.8.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 (331) hide show
  1. package/generated/GENERATED-NOTES.md +16 -0
  2. package/generated/agents/toolu--architect.md +52 -0
  3. package/generated/agents/toolu--deep-explore.md +65 -0
  4. package/generated/agents/toolu--implementer.md +50 -0
  5. package/generated/agents/toolu--quick-task.md +54 -0
  6. package/generated/agents/toolu--research-agent.md +101 -0
  7. package/generated/commands/toolu--commit.md +8 -0
  8. package/generated/commands/toolu--review-and-commit.md +8 -0
  9. package/generated/opencode.toolu.json +116 -0
  10. package/generated/skills/toolu--brainstorm/SKILL.md +57 -0
  11. package/generated/skills/toolu--brainstorm/references/design-questions.md +36 -0
  12. package/generated/skills/toolu--commit--skill/SKILL.md +11 -0
  13. package/generated/skills/toolu--debug/SKILL.md +54 -0
  14. package/generated/skills/toolu--deep-research/SKILL.md +51 -0
  15. package/generated/skills/toolu--execution/SKILL.md +77 -0
  16. package/generated/skills/toolu--orchestrator/SKILL.md +145 -0
  17. package/generated/skills/toolu--orchestrator/references/delegation-cost.md +104 -0
  18. package/generated/skills/toolu--orchestrator/references/model-routing.md +112 -0
  19. package/generated/skills/toolu--plan/SKILL.md +56 -0
  20. package/generated/skills/toolu--plan/references/ledger.md +18 -0
  21. package/generated/skills/toolu--plan-review/SKILL.md +48 -0
  22. package/generated/skills/toolu--review-and-commit--skill/SKILL.md +10 -0
  23. package/generated/skills/toolu--setup/SKILL.md +24 -0
  24. package/generated/skills/toolu--setup/scripts/setup.sh +206 -0
  25. package/generated/skills/toolu--spec/SKILL.md +67 -0
  26. package/generated/skills/toolu--spec-review/SKILL.md +50 -0
  27. package/generated/skills/toolu--test/SKILL.md +49 -0
  28. package/package.json +51 -0
  29. package/plugins/agent-browser/.claude-plugin/plugin.json +18 -0
  30. package/plugins/agent-browser/.codex-plugin/plugin.json +7 -0
  31. package/plugins/agent-browser/README.md +18 -0
  32. package/plugins/agent-browser/hooks/hooks.json +15 -0
  33. package/plugins/agent-browser/hooks/session-start.sh +42 -0
  34. package/plugins/agent-browser/skills/agent-browser/SKILL.md +77 -0
  35. package/plugins/agent-browser/skills/agent-browser/scripts/agent-browser.sh +80 -0
  36. package/plugins/ast-grep/.claude-plugin/plugin.json +18 -0
  37. package/plugins/ast-grep/.codex-plugin/plugin.json +7 -0
  38. package/plugins/ast-grep/README.md +21 -0
  39. package/plugins/ast-grep/hooks/hooks.json +15 -0
  40. package/plugins/ast-grep/hooks/post-tools.d/byte-savings.sh +67 -0
  41. package/plugins/ast-grep/hooks/pre-tools.d/search-nudge.sh +170 -0
  42. package/plugins/ast-grep/hooks/register.sh +68 -0
  43. package/plugins/ast-grep/scripts/byte-savings-report.sh +28 -0
  44. package/plugins/ast-grep/skills/ast-grep/SKILL.md +127 -0
  45. package/plugins/ast-grep/skills/ast-grep/references/ast-grep-advanced.md +44 -0
  46. package/plugins/ast-grep/skills/ast-grep/scripts/ast-grep.sh +103 -0
  47. package/plugins/context7/.claude-plugin/plugin.json +18 -0
  48. package/plugins/context7/.codex-plugin/plugin.json +7 -0
  49. package/plugins/context7/README.md +19 -0
  50. package/plugins/context7/hooks/hooks.json +15 -0
  51. package/plugins/context7/hooks/session-start.sh +42 -0
  52. package/plugins/context7/skills/context7/SKILL.md +64 -0
  53. package/plugins/context7/skills/context7/scripts/search.sh +151 -0
  54. package/plugins/exa-search/.claude-plugin/plugin.json +18 -0
  55. package/plugins/exa-search/.codex-plugin/plugin.json +7 -0
  56. package/plugins/exa-search/README.md +19 -0
  57. package/plugins/exa-search/hooks/hooks.json +15 -0
  58. package/plugins/exa-search/hooks/session-start.sh +34 -0
  59. package/plugins/exa-search/skills/exa-search/SKILL.md +91 -0
  60. package/plugins/exa-search/skills/exa-search/scripts/search.sh +221 -0
  61. package/plugins/jev/.claude-plugin/plugin.json +19 -0
  62. package/plugins/jev/.codex-plugin/plugin.json +7 -0
  63. package/plugins/jev/README.md +61 -0
  64. package/plugins/jev/hooks/hooks.json +25 -0
  65. package/plugins/jev/hooks/lib/common.sh +49 -0
  66. package/plugins/jev/hooks/session-start.sh +45 -0
  67. package/plugins/jev/hooks/user-prompt-submit.sh +41 -0
  68. package/plugins/jev/skills/jev/SKILL.md +81 -0
  69. package/plugins/jev/skills/jev/evals/README.md +98 -0
  70. package/plugins/jev/skills/jev/references/problem-solving.md +198 -0
  71. package/plugins/jev/skills/jev/scripts/jev.sh +405 -0
  72. package/plugins/jira/.claude-plugin/plugin.json +18 -0
  73. package/plugins/jira/.codex-plugin/plugin.json +7 -0
  74. package/plugins/jira/README.md +42 -0
  75. package/plugins/jira/hooks/hooks.json +15 -0
  76. package/plugins/jira/hooks/session-start.sh +34 -0
  77. package/plugins/jira/skills/jira/SKILL.md +123 -0
  78. package/plugins/jira/skills/jira/scripts/jira.sh +70 -0
  79. package/plugins/jira/skills/jira/scripts/lib/adf.sh +17 -0
  80. package/plugins/jira/skills/jira/scripts/lib/attachment.sh +94 -0
  81. package/plugins/jira/skills/jira/scripts/lib/board.sh +58 -0
  82. package/plugins/jira/skills/jira/scripts/lib/http.sh +184 -0
  83. package/plugins/jira/skills/jira/scripts/lib/issue.sh +159 -0
  84. package/plugins/jira/skills/jira/scripts/lib/paginate.sh +45 -0
  85. package/plugins/jira/skills/jira/scripts/lib/plan-parse.sh +70 -0
  86. package/plugins/jira/skills/jira/scripts/lib/plan-run.sh +115 -0
  87. package/plugins/jira/skills/jira/scripts/lib/plan-store.sh +168 -0
  88. package/plugins/jira/skills/jira/scripts/lib/plan.sh +108 -0
  89. package/plugins/jira/skills/jira/scripts/lib/project.sh +42 -0
  90. package/plugins/jira/skills/jira/scripts/lib/raw.sh +15 -0
  91. package/plugins/jira/skills/jira/scripts/lib/search.sh +41 -0
  92. package/plugins/jira/skills/jira/scripts/lib/sprint.sh +88 -0
  93. package/plugins/jira/skills/jira/scripts/lib/user.sh +45 -0
  94. package/plugins/jira/skills/jira/scripts/lib/worklog.sh +56 -0
  95. package/plugins/pr-babysit/.claude-plugin/plugin.json +25 -0
  96. package/plugins/pr-babysit/.codex-plugin/plugin.json +7 -0
  97. package/plugins/pr-babysit/README.md +38 -0
  98. package/plugins/pr-babysit/commands/babysit.md +13 -0
  99. package/plugins/pr-babysit/hooks/check-toolu.sh +20 -0
  100. package/plugins/pr-babysit/hooks/hooks.json +15 -0
  101. package/plugins/pr-babysit/scripts/babysit-tick.sh +133 -0
  102. package/plugins/pr-babysit/scripts/collect-pr.sh +224 -0
  103. package/plugins/pr-babysit/scripts/lib/common.sh +146 -0
  104. package/plugins/pr-babysit/scripts/lib/gh.sh +150 -0
  105. package/plugins/pr-babysit/scripts/lib/lock.sh +84 -0
  106. package/plugins/pr-babysit/scripts/lib/normalize.sh +98 -0
  107. package/plugins/pr-babysit/scripts/lib/state.sh +37 -0
  108. package/plugins/pr-babysit/scripts/parse-verdict.sh +171 -0
  109. package/plugins/pr-babysit/scripts/record.sh +69 -0
  110. package/plugins/pr-babysit/scripts/reduce-state.sh +245 -0
  111. package/plugins/pr-babysit/scripts/reply-thread.sh +90 -0
  112. package/plugins/pr-babysit/scripts/resolve-thread.sh +71 -0
  113. package/plugins/pr-babysit/skills/babysit/SKILL.md +31 -0
  114. package/plugins/pr-babysit/skills/babysit/references/helper.md +209 -0
  115. package/plugins/pr-babysit/workflows/babysit.md +722 -0
  116. package/plugins/python-quality/.claude-plugin/plugin.json +24 -0
  117. package/plugins/python-quality/.codex-plugin/plugin.json +6 -0
  118. package/plugins/python-quality/README.md +23 -0
  119. package/plugins/python-quality/hooks/check-toolu.sh +20 -0
  120. package/plugins/python-quality/hooks/concerns/00-preamble.sh +85 -0
  121. package/plugins/python-quality/hooks/concerns/10-size-file.sh +6 -0
  122. package/plugins/python-quality/hooks/concerns/20-tests.sh +37 -0
  123. package/plugins/python-quality/hooks/concerns/30-suppression.sh +33 -0
  124. package/plugins/python-quality/hooks/concerns/50-size-fn.sh +49 -0
  125. package/plugins/python-quality/hooks/concerns/70-no-mocks.sh +76 -0
  126. package/plugins/python-quality/hooks/concerns/90-docs.sh +52 -0
  127. package/plugins/python-quality/hooks/concerns/99-finalize.sh +36 -0
  128. package/plugins/python-quality/hooks/hooks.json +19 -0
  129. package/plugins/python-quality/hooks/register.sh +65 -0
  130. package/plugins/rust-quality/.claude-plugin/plugin.json +24 -0
  131. package/plugins/rust-quality/.codex-plugin/plugin.json +6 -0
  132. package/plugins/rust-quality/README.md +24 -0
  133. package/plugins/rust-quality/hooks/check-toolu.sh +20 -0
  134. package/plugins/rust-quality/hooks/concerns/00-preamble.sh +93 -0
  135. package/plugins/rust-quality/hooks/concerns/10-size-file.sh +10 -0
  136. package/plugins/rust-quality/hooks/concerns/20-tests.sh +84 -0
  137. package/plugins/rust-quality/hooks/concerns/30-suppression.sh +26 -0
  138. package/plugins/rust-quality/hooks/concerns/40-unsafe.sh +39 -0
  139. package/plugins/rust-quality/hooks/concerns/50-size-fn.sh +38 -0
  140. package/plugins/rust-quality/hooks/concerns/55-size-impl.sh +28 -0
  141. package/plugins/rust-quality/hooks/concerns/60-error-handling.sh +149 -0
  142. package/plugins/rust-quality/hooks/concerns/70-no-mocks.sh +139 -0
  143. package/plugins/rust-quality/hooks/concerns/90-docs.sh +32 -0
  144. package/plugins/rust-quality/hooks/concerns/99-finalize.sh +36 -0
  145. package/plugins/rust-quality/hooks/hooks.json +19 -0
  146. package/plugins/rust-quality/hooks/register.sh +65 -0
  147. package/plugins/statusline/.claude-plugin/plugin.json +19 -0
  148. package/plugins/statusline/.codex-plugin/plugin.json +7 -0
  149. package/plugins/statusline/README.md +108 -0
  150. package/plugins/statusline/commands/setup.md +30 -0
  151. package/plugins/statusline/hooks/hooks.json +15 -0
  152. package/plugins/statusline/hooks/session-start.sh +45 -0
  153. package/plugins/statusline/scripts/collect-status.sh +147 -0
  154. package/plugins/statusline/scripts/setup.sh +93 -0
  155. package/plugins/statusline/scripts/status.sh +55 -0
  156. package/plugins/statusline/skills/status/SKILL.md +13 -0
  157. package/plugins/statusline/statusline.sh +177 -0
  158. package/plugins/toolu/.claude-plugin/plugin.json +19 -0
  159. package/plugins/toolu/.codex-plugin/plugin.json +7 -0
  160. package/plugins/toolu/README.md +35 -0
  161. package/plugins/toolu/agents/.gitkeep +0 -0
  162. package/plugins/toolu/agents/architect.md +52 -0
  163. package/plugins/toolu/agents/deep-explore.md +65 -0
  164. package/plugins/toolu/agents/implementer.md +50 -0
  165. package/plugins/toolu/agents/quick-task.md +54 -0
  166. package/plugins/toolu/agents/research-agent.md +107 -0
  167. package/plugins/toolu/assets/agents/architect.toml +9 -0
  168. package/plugins/toolu/assets/agents/deep-explore.toml +9 -0
  169. package/plugins/toolu/assets/agents/implementer.toml +9 -0
  170. package/plugins/toolu/assets/agents/quick-task.toml +9 -0
  171. package/plugins/toolu/assets/agents/research-agent.toml +9 -0
  172. package/plugins/toolu/commands/.gitkeep +0 -0
  173. package/plugins/toolu/commands/commit.md +5 -0
  174. package/plugins/toolu/commands/review-and-commit.md +5 -0
  175. package/plugins/toolu/hooks/docs/gates.md +157 -0
  176. package/plugins/toolu/hooks/docs/model-routing.md +14 -0
  177. package/plugins/toolu/hooks/docs/post-compaction.md +3 -0
  178. package/plugins/toolu/hooks/docs/push-review.md +123 -0
  179. package/plugins/toolu/hooks/docs/session-start-python.md +3 -0
  180. package/plugins/toolu/hooks/docs/session-start-rust.md +4 -0
  181. package/plugins/toolu/hooks/docs/session-start-ts.md +3 -0
  182. package/plugins/toolu/hooks/docs/session-start.md +16 -0
  183. package/plugins/toolu/hooks/hooks.json +76 -0
  184. package/plugins/toolu/hooks/lib/config.sh +355 -0
  185. package/plugins/toolu/hooks/lib/detect.sh +929 -0
  186. package/plugins/toolu/hooks/lib/diff-sha.sh +32 -0
  187. package/plugins/toolu/hooks/lib/dispatch.sh +365 -0
  188. package/plugins/toolu/hooks/lib/docs-sync-config.sh +81 -0
  189. package/plugins/toolu/hooks/lib/edit-records.sh +157 -0
  190. package/plugins/toolu/hooks/lib/gate-file.sh +201 -0
  191. package/plugins/toolu/hooks/lib/gate-mode.sh +244 -0
  192. package/plugins/toolu/hooks/lib/host.sh +205 -0
  193. package/plugins/toolu/hooks/lib/permissions.sh +125 -0
  194. package/plugins/toolu/hooks/lib/plan-ledger-parse.sh +219 -0
  195. package/plugins/toolu/hooks/lib/plan-ledger-preflight.sh +137 -0
  196. package/plugins/toolu/hooks/lib/plan-ledger.sh +749 -0
  197. package/plugins/toolu/hooks/lib/push-waiver.sh +116 -0
  198. package/plugins/toolu/hooks/lib/quality-config.sh +184 -0
  199. package/plugins/toolu/hooks/lib/registry.sh +72 -0
  200. package/plugins/toolu/hooks/lib/state-sweeper.sh +194 -0
  201. package/plugins/toolu/hooks/lib/telemetry.sh +94 -0
  202. package/plugins/toolu/hooks/lib/verdict.sh +477 -0
  203. package/plugins/toolu/hooks/post-tools/mod.sh +42 -0
  204. package/plugins/toolu/hooks/post-tools/modules/gate-status.sh +140 -0
  205. package/plugins/toolu/hooks/post-tools/modules/push-waiver.sh +97 -0
  206. package/plugins/toolu/hooks/pre-compact.sh +18 -0
  207. package/plugins/toolu/hooks/pre-tools/agent-tier.sh +111 -0
  208. package/plugins/toolu/hooks/pre-tools/mod.sh +37 -0
  209. package/plugins/toolu/hooks/pre-tools/modules/bash-commands.sh +187 -0
  210. package/plugins/toolu/hooks/pre-tools/modules/code-edit-rules.sh +78 -0
  211. package/plugins/toolu/hooks/pre-tools/modules/commit-gate.sh +70 -0
  212. package/plugins/toolu/hooks/pre-tools/modules/docs-sync.sh +137 -0
  213. package/plugins/toolu/hooks/pre-tools/modules/mcp-blocker.sh +151 -0
  214. package/plugins/toolu/hooks/pre-tools/modules/plan-ledger.sh +201 -0
  215. package/plugins/toolu/hooks/pre-tools/modules/protected-files.sh +177 -0
  216. package/plugins/toolu/hooks/pre-tools/modules/push-review.sh +273 -0
  217. package/plugins/toolu/hooks/pre-tools/modules/quality-gate.sh +87 -0
  218. package/plugins/toolu/hooks/session-start.sh +379 -0
  219. package/plugins/toolu/hooks/user-prompt-submit.sh +197 -0
  220. package/plugins/toolu/scripts/context-budget.sh +111 -0
  221. package/plugins/toolu/scripts/debug-log.sh +98 -0
  222. package/plugins/toolu/scripts/debug-stack.sh +108 -0
  223. package/plugins/toolu/scripts/debug-testfail.sh +97 -0
  224. package/plugins/toolu/settings/.gitkeep +0 -0
  225. package/plugins/toolu/settings/README.md +127 -0
  226. package/plugins/toolu/settings/bash-allowlist.txt +12 -0
  227. package/plugins/toolu/settings/bash-denylist.txt +25 -0
  228. package/plugins/toolu/settings/code-edit-rules.json +67 -0
  229. package/plugins/toolu/settings/commit-prefixes.txt +12 -0
  230. package/plugins/toolu/settings/mcp-blocklist.txt +8 -0
  231. package/plugins/toolu/settings/permissions.fragment.json +36 -0
  232. package/plugins/toolu/settings/protected-files.txt +30 -0
  233. package/plugins/toolu/settings/rust-unsafe-exemptions.txt +5 -0
  234. package/plugins/toolu/settings/toolu.config.example.json +130 -0
  235. package/plugins/toolu/skills/.gitkeep +0 -0
  236. package/plugins/toolu/skills/brainstorm/SKILL.md +57 -0
  237. package/plugins/toolu/skills/brainstorm/references/design-questions.md +36 -0
  238. package/plugins/toolu/skills/commit/SKILL.md +11 -0
  239. package/plugins/toolu/skills/debug/SKILL.md +54 -0
  240. package/plugins/toolu/skills/debug/evals/README.md +17 -0
  241. package/plugins/toolu/skills/debug/evals/cases.json +24 -0
  242. package/plugins/toolu/skills/debug/evals/results.json +34 -0
  243. package/plugins/toolu/skills/deep-research/SKILL.md +51 -0
  244. package/plugins/toolu/skills/execution/SKILL.md +77 -0
  245. package/plugins/toolu/skills/orchestrator/SKILL.md +145 -0
  246. package/plugins/toolu/skills/orchestrator/references/delegation-cost.md +104 -0
  247. package/plugins/toolu/skills/orchestrator/references/model-routing.md +112 -0
  248. package/plugins/toolu/skills/plan/SKILL.md +56 -0
  249. package/plugins/toolu/skills/plan/references/ledger.md +18 -0
  250. package/plugins/toolu/skills/plan-review/SKILL.md +48 -0
  251. package/plugins/toolu/skills/review-and-commit/SKILL.md +10 -0
  252. package/plugins/toolu/skills/setup/SKILL.md +24 -0
  253. package/plugins/toolu/skills/setup/scripts/setup.sh +206 -0
  254. package/plugins/toolu/skills/spec/SKILL.md +67 -0
  255. package/plugins/toolu/skills/spec-review/SKILL.md +50 -0
  256. package/plugins/toolu/skills/test/SKILL.md +49 -0
  257. package/plugins/toolu/workflows/commit.md +21 -0
  258. package/plugins/toolu/workflows/host-mapping.md +22 -0
  259. package/plugins/toolu/workflows/review-and-commit.md +54 -0
  260. package/plugins/toolu/workflows/semantic-judgments.md +35 -0
  261. package/plugins/toolu-review/.claude-plugin/plugin.json +18 -0
  262. package/plugins/toolu-review/.codex-plugin/plugin.json +7 -0
  263. package/plugins/toolu-review/README.md +17 -0
  264. package/plugins/toolu-review/hooks/hooks.json +15 -0
  265. package/plugins/toolu-review/hooks/session-start.sh +34 -0
  266. package/plugins/toolu-review/skills/review/SKILL.md +90 -0
  267. package/plugins/toolu-review/skills/review/scripts/write-state.sh +189 -0
  268. package/plugins/ts-quality/.claude-plugin/plugin.json +24 -0
  269. package/plugins/ts-quality/.codex-plugin/plugin.json +6 -0
  270. package/plugins/ts-quality/README.md +24 -0
  271. package/plugins/ts-quality/hooks/check-toolu.sh +20 -0
  272. package/plugins/ts-quality/hooks/concerns/00-preamble.sh +123 -0
  273. package/plugins/ts-quality/hooks/concerns/10-imports.sh +33 -0
  274. package/plugins/ts-quality/hooks/concerns/15-type-as.sh +9 -0
  275. package/plugins/ts-quality/hooks/concerns/20-tests.sh +26 -0
  276. package/plugins/ts-quality/hooks/concerns/25-size-file.sh +40 -0
  277. package/plugins/ts-quality/hooks/concerns/30-size-fn.sh +53 -0
  278. package/plugins/ts-quality/hooks/concerns/35-react-hooks.sh +7 -0
  279. package/plugins/ts-quality/hooks/concerns/40-factory.sh +5 -0
  280. package/plugins/ts-quality/hooks/concerns/45-typeguard.sh +6 -0
  281. package/plugins/ts-quality/hooks/concerns/50-type-dup.sh +16 -0
  282. package/plugins/ts-quality/hooks/concerns/55-naming.sh +11 -0
  283. package/plugins/ts-quality/hooks/concerns/60-console.sh +6 -0
  284. package/plugins/ts-quality/hooks/concerns/65-suppression.sh +12 -0
  285. package/plugins/ts-quality/hooks/concerns/70-ui-confirm.sh +8 -0
  286. package/plugins/ts-quality/hooks/concerns/72-ui-radix.sh +12 -0
  287. package/plugins/ts-quality/hooks/concerns/74-react-props.sh +6 -0
  288. package/plugins/ts-quality/hooks/concerns/76-toast.sh +10 -0
  289. package/plugins/ts-quality/hooks/concerns/78-error-ast.sh +242 -0
  290. package/plugins/ts-quality/hooks/concerns/80-throw-literal.sh +23 -0
  291. package/plugins/ts-quality/hooks/concerns/85-no-mocks.sh +109 -0
  292. package/plugins/ts-quality/hooks/concerns/90-duplication.sh +30 -0
  293. package/plugins/ts-quality/hooks/concerns/92-docs.sh +36 -0
  294. package/plugins/ts-quality/hooks/concerns/94-handler.sh +15 -0
  295. package/plugins/ts-quality/hooks/concerns/99-finalize.sh +46 -0
  296. package/plugins/ts-quality/hooks/hooks.json +19 -0
  297. package/plugins/ts-quality/hooks/register.sh +65 -0
  298. package/src/adapter/__tests__/evaluate.test.ts +82 -0
  299. package/src/adapter/__tests__/permission-map.test.ts +91 -0
  300. package/src/adapter/evaluate.ts +54 -0
  301. package/src/adapter/permission-map.ts +177 -0
  302. package/src/bootstrap/__tests__/bootstrap.test.ts +148 -0
  303. package/src/bootstrap/entrypoint.ts +18 -0
  304. package/src/bootstrap/index.ts +5 -0
  305. package/src/bootstrap/readiness.ts +59 -0
  306. package/src/bootstrap/result.ts +21 -0
  307. package/src/bootstrap/runtime.ts +99 -0
  308. package/src/bootstrap/test-helpers.ts +25 -0
  309. package/src/host/__tests__/host.test.ts +37 -0
  310. package/src/host/detect.ts +46 -0
  311. package/src/host/index.ts +10 -0
  312. package/src/host/roots.ts +59 -0
  313. package/src/host/types.ts +14 -0
  314. package/src/inventory/__tests__/inventory.test.ts +69 -0
  315. package/src/inventory/index.ts +9 -0
  316. package/src/inventory/manifest.ts +46 -0
  317. package/src/inventory/scan.ts +66 -0
  318. package/src/inventory/selection.ts +92 -0
  319. package/src/inventory/types.ts +22 -0
  320. package/src/lifecycle/__tests__/lifecycle.test.ts +7 -0
  321. package/src/lifecycle/index.ts +2 -0
  322. package/src/lifecycle/table.ts +42 -0
  323. package/src/plugin/__tests__/plugin-stub.test.ts +6 -0
  324. package/src/plugin/plugin-stub.ts +7 -0
  325. package/src/plugin/toolu.ts +142 -0
  326. package/src/preflight/__tests__/preflight.test.ts +20 -0
  327. package/src/preflight/check.ts +75 -0
  328. package/src/preflight/index.ts +2 -0
  329. package/src/select/__tests__/select.test.ts +52 -0
  330. package/src/select/index.ts +2 -0
  331. package/src/select/resolve.ts +87 -0
@@ -0,0 +1,59 @@
1
+ /** Bootstrap output proof — exit 0 alone is not ready (#211). */
2
+ import { existsSync, readdirSync, statSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { opencodeRegistryRoot } from "../host/roots.ts";
5
+
6
+ const REGISTRY_EVENT_DIRS = ["pre-tools.d", "post-tools.d"] as const;
7
+
8
+ const SESSION_ARTIFACT_NAMES = [".gate-preset-notice-v6"] as const;
9
+
10
+ function listShModules(dir: string): string[] {
11
+ if (!existsSync(dir)) {
12
+ return [];
13
+ }
14
+ const out: string[] = [];
15
+ for (const entry of readdirSync(dir)) {
16
+ const path = join(dir, entry);
17
+ try {
18
+ if (statSync(path).isFile() && entry.endsWith(".sh")) {
19
+ out.push(path);
20
+ }
21
+ } catch {
22
+ continue;
23
+ }
24
+ }
25
+ return out;
26
+ }
27
+
28
+ /** Concrete artifacts produced by register/session-start under dataRoot. */
29
+ export function collectBootstrapArtifacts(dataRoot: string): string[] {
30
+ const regRoot = opencodeRegistryRoot(dataRoot);
31
+ const artifacts: string[] = [];
32
+ for (const sub of REGISTRY_EVENT_DIRS) {
33
+ artifacts.push(...listShModules(join(regRoot, sub)));
34
+ }
35
+ for (const name of SESSION_ARTIFACT_NAMES) {
36
+ const path = join(regRoot, name);
37
+ if (existsSync(path)) {
38
+ artifacts.push(path);
39
+ }
40
+ }
41
+ return artifacts;
42
+ }
43
+
44
+ export function evaluateBootstrapReadiness(dataRoot: string): {
45
+ ready: boolean;
46
+ artifacts: string[];
47
+ reason?: string;
48
+ } {
49
+ const artifacts = collectBootstrapArtifacts(dataRoot);
50
+ if (artifacts.length === 0) {
51
+ return {
52
+ ready: false,
53
+ artifacts,
54
+ reason:
55
+ "bootstrap entrypoints exited cleanly but required registry/session artifacts are missing under the OpenCode data root",
56
+ };
57
+ }
58
+ return { ready: true, artifacts };
59
+ }
@@ -0,0 +1,21 @@
1
+ /** Bootstrap Ready / NotReady union (#211). */
2
+
3
+ export type ReadyResult = {
4
+ status: "ready";
5
+ artifacts: string[];
6
+ };
7
+
8
+ export type NotReadyResult = {
9
+ status: "not-ready";
10
+ reason: string;
11
+ };
12
+
13
+ export type BootstrapResult = ReadyResult | NotReadyResult;
14
+
15
+ export function ready(artifacts: string[]): ReadyResult {
16
+ return { status: "ready", artifacts };
17
+ }
18
+
19
+ export function notReady(reason: string): NotReadyResult {
20
+ return { status: "not-ready", reason };
21
+ }
@@ -0,0 +1,99 @@
1
+ /** Assemble registry via argv-only bash runner (#211). */
2
+ import { mkdirSync } from "node:fs";
3
+ import { createBunBashRunner, type BashRunner } from "@toolu/core/runner";
4
+ import { opencodeDataRoot } from "../host/roots.ts";
5
+ import type { PluginManifest } from "../inventory/types.ts";
6
+ import { notReady, ready, type BootstrapResult } from "./result.ts";
7
+ import { pluginBootstrapScript } from "./entrypoint.ts";
8
+ import { evaluateBootstrapReadiness } from "./readiness.ts";
9
+
10
+ export type BootstrapRuntimeOptions = {
11
+ repoRoot: string;
12
+ dataRoot?: string;
13
+ projectRoot: string;
14
+ plugins: PluginManifest[];
15
+ env?: Record<string, string>;
16
+ runner?: BashRunner;
17
+ isolatedHome?: string;
18
+ deadlineMs?: number;
19
+ };
20
+
21
+ function bootstrapEnv(options: BootstrapRuntimeOptions, dataRoot: string): Record<string, string> {
22
+ const base: Record<string, string> = {
23
+ TOOLU_CONFIG_DIR: dataRoot,
24
+ TOOLU_PROJECT_DIR: options.projectRoot,
25
+ TOOLU_PROJECT_CONFIG_DIRNAME: ".opencode",
26
+ TOOLU_HOST_OVERRIDE: "opencode",
27
+ HOME: options.isolatedHome ?? dataRoot,
28
+ };
29
+ if (options.env) {
30
+ for (const [key, value] of Object.entries(options.env)) {
31
+ base[key] = value;
32
+ }
33
+ }
34
+ return base;
35
+ }
36
+
37
+ async function runEntrypoint(
38
+ runner: BashRunner,
39
+ scriptPath: string,
40
+ cwd: string,
41
+ env: Record<string, string>,
42
+ deadlineMs: number,
43
+ ): Promise<BootstrapResult | null> {
44
+ const result = await runner.run({
45
+ argv: ["bash", scriptPath],
46
+ cwd,
47
+ env,
48
+ stdin: "{}",
49
+ deadlineMs,
50
+ maxStdoutBytes: 512_000,
51
+ });
52
+ if (!result.ok) {
53
+ return notReady(`bootstrap script failed (${scriptPath}): ${result.message}`);
54
+ }
55
+ if (result.exitCode !== 0) {
56
+ return notReady(
57
+ `bootstrap script ${scriptPath} exited ${result.exitCode}: ${result.stderr || result.stdout}`,
58
+ );
59
+ }
60
+ return null;
61
+ }
62
+
63
+ /** Run plugin register/session-start scripts and verify output artifacts. */
64
+ export async function bootstrapRuntime(options: BootstrapRuntimeOptions): Promise<BootstrapResult> {
65
+ const dataRoot = options.dataRoot ?? opencodeDataRoot({ projectRoot: options.projectRoot });
66
+ mkdirSync(dataRoot, { recursive: true });
67
+
68
+ const runner = options.runner ?? createBunBashRunner();
69
+ const env = bootstrapEnv(options, dataRoot);
70
+ const deadlineMs = options.deadlineMs ?? 120_000;
71
+
72
+ const runChain = async (index: number): Promise<BootstrapResult | null> => {
73
+ const plugin = options.plugins[index];
74
+ if (!plugin) {
75
+ return null;
76
+ }
77
+ const script = pluginBootstrapScript(plugin.pluginDir);
78
+ // Not every plugin has register.sh / session-start.sh — skip, don't abort.
79
+ if (!script) {
80
+ return runChain(index + 1);
81
+ }
82
+ const failure = await runEntrypoint(runner, script, options.projectRoot, env, deadlineMs);
83
+ if (failure) {
84
+ return failure;
85
+ }
86
+ return runChain(index + 1);
87
+ };
88
+
89
+ const chainFailure = await runChain(0);
90
+ if (chainFailure) {
91
+ return chainFailure;
92
+ }
93
+
94
+ const proof = evaluateBootstrapReadiness(dataRoot);
95
+ if (!proof.ready) {
96
+ return notReady(proof.reason ?? "bootstrap artifacts missing");
97
+ }
98
+ return ready(proof.artifacts);
99
+ }
@@ -0,0 +1,25 @@
1
+ /** Test helper: exit 0 without producing registry output → NotReady (#211 AC-4). */
2
+ import type { BashRunner, RawProcessResult } from "@toolu/core/runner";
3
+ import { bootstrapRuntime, type BootstrapRuntimeOptions } from "./runtime.ts";
4
+ import type { BootstrapResult } from "./result.ts";
5
+
6
+ export function createNoOpRunner(): BashRunner {
7
+ return {
8
+ run(): Promise<RawProcessResult> {
9
+ return Promise.resolve({
10
+ ok: true,
11
+ exitCode: 0,
12
+ stdout: "",
13
+ stderr: "",
14
+ truncated: false,
15
+ });
16
+ },
17
+ };
18
+ }
19
+
20
+ /** Run bootstrap with a runner that always succeeds without touching disk scripts. */
21
+ export async function bootstrapWithNoOpRunner(
22
+ options: BootstrapRuntimeOptions,
23
+ ): Promise<BootstrapResult> {
24
+ return bootstrapRuntime({ ...options, runner: createNoOpRunner() });
25
+ }
@@ -0,0 +1,37 @@
1
+ import { expect, test } from "bun:test";
2
+ import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { detectHost } from "../detect.ts";
5
+ import { opencodeConfigRoot, opencodeDataRoot, opencodePluginSelectionPath } from "../roots.ts";
6
+
7
+ const tmpBase = process.env.TMPDIR ?? "/tmp";
8
+
9
+ test("detectHost honors TOOLU_HOST_OVERRIDE=opencode", () => {
10
+ expect(detectHost({ env: { TOOLU_HOST_OVERRIDE: "opencode" } })).toBe("opencode");
11
+ });
12
+
13
+ test("detectHost uses OPENCODE_* env and .opencode project marker", () => {
14
+ const project = mkdtempSync(join(tmpBase, "toolu-oc-host-"));
15
+ mkdirSync(join(project, ".opencode"), { recursive: true });
16
+ expect(detectHost({ env: { OPENCODE_HOME: "/tmp/oc" } })).toBe("opencode");
17
+ expect(detectHost({ env: {}, projectRoot: project })).toBe("opencode");
18
+ });
19
+
20
+ test("opencode roots honor TOOLU_CONFIG_DIR and TOOLU_OPENCODE_HOME", () => {
21
+ const project = mkdtempSync(join(tmpBase, "toolu-oc-roots-"));
22
+ const explicit = join(project, "data");
23
+ expect(opencodeDataRoot({ dataRoot: explicit })).toBe(explicit);
24
+ expect(opencodeConfigRoot({ env: { TOOLU_CONFIG_DIR: "/cfg" } })).toBe("/cfg");
25
+ expect(opencodeConfigRoot({ env: { TOOLU_OPENCODE_HOME: "/oc-home" } })).toBe("/oc-home");
26
+ const derived = opencodeDataRoot({ projectRoot: project });
27
+ expect(derived).toBe(join(project, ".opencode", "toolu", "state"));
28
+ });
29
+
30
+ test("selection path lives under project .opencode", () => {
31
+ const project = mkdtempSync(join(tmpBase, "toolu-oc-sel-"));
32
+ mkdirSync(join(project, ".opencode", "toolu"), { recursive: true });
33
+ writeFileSync(opencodePluginSelectionPath(project), '{"version":1,"enabled":["toolu"]}\n');
34
+ expect(opencodePluginSelectionPath(project)).toBe(
35
+ join(project, ".opencode", "toolu", "plugins.json"),
36
+ );
37
+ });
@@ -0,0 +1,46 @@
1
+ /** Detect runtime host; OpenCode via override, env, or project markers (#211). */
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import type { HostDetectOptions, TooluHost } from "./types.ts";
5
+
6
+ const OPENCODE_ENV_KEYS = [
7
+ "OPENCODE_HOME",
8
+ "OPENCODE_CONFIG",
9
+ "OPENCODE_PROJECT_DIR",
10
+ "OPENCODE_BIN",
11
+ ] as const;
12
+
13
+ function hasOpencodeEnv(env: NodeJS.ProcessEnv): boolean {
14
+ return OPENCODE_ENV_KEYS.some((key) => {
15
+ const value = env[key];
16
+ return typeof value === "string" && value.length > 0;
17
+ });
18
+ }
19
+
20
+ function hasOpencodeProjectMarker(projectRoot: string): boolean {
21
+ return existsSync(join(projectRoot, ".opencode"));
22
+ }
23
+
24
+ /** Resolve host from env overrides and optional project markers. */
25
+ export function detectHost(options: HostDetectOptions = {}): TooluHost {
26
+ const env = options.env ?? process.env;
27
+ const override = env.TOOLU_HOST_OVERRIDE;
28
+ if (override === "claude" || override === "codex" || override === "opencode") {
29
+ return override;
30
+ }
31
+
32
+ if (hasOpencodeEnv(env)) {
33
+ return "opencode";
34
+ }
35
+
36
+ const projectRoot = options.projectRoot ?? env.TOOLU_PROJECT_DIR;
37
+ if (projectRoot && hasOpencodeProjectMarker(projectRoot)) {
38
+ return "opencode";
39
+ }
40
+
41
+ if (env.PLUGIN_ROOT) {
42
+ return "codex";
43
+ }
44
+
45
+ return "claude";
46
+ }
@@ -0,0 +1,10 @@
1
+ export { detectHost } from "./detect.ts";
2
+ export {
3
+ opencodeConfigRoot,
4
+ opencodeDataRoot,
5
+ opencodePluginSelectionPath,
6
+ opencodeProjectConfigPath,
7
+ opencodeProjectDir,
8
+ opencodeRegistryRoot,
9
+ } from "./roots.ts";
10
+ export type { HostDetectOptions, OpencodeRootsOptions, TooluHost } from "./types.ts";
@@ -0,0 +1,59 @@
1
+ /** OpenCode config/state roots; honors TOOLU_* without Claude/Codex homes (#211). */
2
+ import { join } from "node:path";
3
+ import type { OpencodeRootsOptions } from "./types.ts";
4
+
5
+ const OPENCODE_PROJECT_DIRNAME = ".opencode";
6
+ const OPENCODE_STATE_SEGMENT = join("toolu", "state");
7
+
8
+ /** Writable data root for assembled registry and host state. */
9
+ export function opencodeDataRoot(options: OpencodeRootsOptions = {}): string {
10
+ const env = options.env ?? process.env;
11
+ if (options.dataRoot) {
12
+ return options.dataRoot;
13
+ }
14
+ if (env.TOOLU_CONFIG_DIR) {
15
+ return env.TOOLU_CONFIG_DIR;
16
+ }
17
+ if (env.TOOLU_OPENCODE_HOME) {
18
+ return env.TOOLU_OPENCODE_HOME;
19
+ }
20
+ const projectRoot = options.projectRoot ?? env.TOOLU_PROJECT_DIR;
21
+ if (!projectRoot) {
22
+ throw new Error(
23
+ "opencodeDataRoot: set dataRoot, TOOLU_CONFIG_DIR, TOOLU_OPENCODE_HOME, or projectRoot",
24
+ );
25
+ }
26
+ return join(projectRoot, OPENCODE_PROJECT_DIRNAME, OPENCODE_STATE_SEGMENT);
27
+ }
28
+
29
+ /** Alias for portable-core export map. */
30
+ export function opencodeConfigRoot(options: OpencodeRootsOptions = {}): string {
31
+ return opencodeDataRoot(options);
32
+ }
33
+
34
+ /** Project git/worktree root for config resolution. */
35
+ export function opencodeProjectDir(options: OpencodeRootsOptions = {}): string {
36
+ const env = options.env ?? process.env;
37
+ if (options.projectRoot) {
38
+ return options.projectRoot;
39
+ }
40
+ if (env.TOOLU_PROJECT_DIR) {
41
+ return env.TOOLU_PROJECT_DIR;
42
+ }
43
+ throw new Error("opencodeProjectDir: set projectRoot or TOOLU_PROJECT_DIR");
44
+ }
45
+
46
+ /** Project-scoped toolu.config.json path under .opencode/. */
47
+ export function opencodeProjectConfigPath(projectRoot: string): string {
48
+ return join(projectRoot, OPENCODE_PROJECT_DIRNAME, "toolu.config.json");
49
+ }
50
+
51
+ /** Selection file for enabled plugins (never Claude installed_plugins.json). */
52
+ export function opencodePluginSelectionPath(projectRoot: string): string {
53
+ return join(projectRoot, OPENCODE_PROJECT_DIRNAME, "toolu", "plugins.json");
54
+ }
55
+
56
+ /** Registry root under the OpenCode data root. */
57
+ export function opencodeRegistryRoot(dataRoot: string): string {
58
+ return join(dataRoot, "toolu");
59
+ }
@@ -0,0 +1,14 @@
1
+ /** Host detection types for OpenCode bootstrap (#211). */
2
+
3
+ export type TooluHost = "claude" | "codex" | "opencode";
4
+
5
+ export type HostDetectOptions = {
6
+ env?: NodeJS.ProcessEnv;
7
+ projectRoot?: string;
8
+ };
9
+
10
+ export type OpencodeRootsOptions = {
11
+ env?: NodeJS.ProcessEnv;
12
+ dataRoot?: string;
13
+ projectRoot?: string;
14
+ };
@@ -0,0 +1,69 @@
1
+ import { expect, test } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { buildInventory, lookupPluginInstallState } from "../scan.ts";
6
+ import { resolveEnabledPluginNames } from "../selection.ts";
7
+ import { opencodePluginSelectionPath, opencodeProjectConfigPath } from "../../host/roots.ts";
8
+
9
+ const tmpBase = process.env.TMPDIR ?? "/tmp";
10
+
11
+ function repoRoot(): string {
12
+ return join(dirname(fileURLToPath(import.meta.url)), "../../../../..");
13
+ }
14
+
15
+ function writeManifest(pluginsRoot: string, name: string): void {
16
+ const dir = join(pluginsRoot, name, ".claude-plugin");
17
+ mkdirSync(dir, { recursive: true });
18
+ writeFileSync(
19
+ join(dir, "plugin.json"),
20
+ JSON.stringify({ name, version: "1.0.0", dependencies: [] }),
21
+ );
22
+ }
23
+
24
+ test("inventory install states installed absent unknown", () => {
25
+ const pluginsRoot = join(repoRoot(), "plugins");
26
+ expect(lookupPluginInstallState(pluginsRoot, "toolu")).toBe("installed");
27
+ expect(lookupPluginInstallState(pluginsRoot, "no-such-plugin-ever")).toBe("absent");
28
+ expect(lookupPluginInstallState("/nonexistent/plugins-root", "toolu")).toBe("unknown");
29
+ });
30
+
31
+ test("enabled selection from .opencode/toolu/plugins.json", () => {
32
+ const project = mkdtempSync(join(tmpBase, "toolu-inv-en-"));
33
+ const pluginsRoot = mkdtempSync(join(tmpBase, "toolu-inv-pl-"));
34
+ writeManifest(pluginsRoot, "toolu");
35
+ writeManifest(pluginsRoot, "ts-quality");
36
+ mkdirSync(join(project, ".opencode", "toolu"), { recursive: true });
37
+ writeFileSync(
38
+ opencodePluginSelectionPath(project),
39
+ JSON.stringify({ version: 1, enabled: ["toolu"] }),
40
+ );
41
+ const result = resolveEnabledPluginNames(pluginsRoot, project);
42
+ expect(result.ok).toBe(true);
43
+ if (!result.ok) {
44
+ return;
45
+ }
46
+ expect(result.enabled.has("toolu")).toBe(true);
47
+ expect(result.enabled.has("ts-quality")).toBe(false);
48
+ const inventory = buildInventory(pluginsRoot, result.enabled);
49
+ const tsEntry = inventory?.find((e) => e.name === "ts-quality");
50
+ expect(tsEntry).toBeDefined();
51
+ expect(tsEntry?.enabled).toBe("disabled");
52
+ });
53
+
54
+ test("skills false in toolu.config disables plugin", () => {
55
+ const project = mkdtempSync(join(tmpBase, "toolu-inv-sk-"));
56
+ const pluginsRoot = mkdtempSync(join(tmpBase, "toolu-inv-sk-pl-"));
57
+ writeManifest(pluginsRoot, "toolu");
58
+ mkdirSync(join(project, ".opencode"), { recursive: true });
59
+ writeFileSync(
60
+ opencodeProjectConfigPath(project),
61
+ JSON.stringify({ version: 1, skills: { toolu: false } }),
62
+ );
63
+ const result = resolveEnabledPluginNames(pluginsRoot, project);
64
+ expect(result.ok).toBe(true);
65
+ if (!result.ok) {
66
+ return;
67
+ }
68
+ expect(result.enabled.has("toolu")).toBe(false);
69
+ });
@@ -0,0 +1,9 @@
1
+ export { buildInventory, listPluginManifests, lookupPluginInstallState } from "./scan.ts";
2
+ export { readPluginManifest, pluginSpec } from "./manifest.ts";
3
+ export { resolveEnabledPluginNames } from "./selection.ts";
4
+ export type {
5
+ PluginEnabledState,
6
+ PluginInstallState,
7
+ PluginInventoryEntry,
8
+ PluginManifest,
9
+ } from "./types.ts";
@@ -0,0 +1,46 @@
1
+ /** Read plugin.json manifests from a plugins root (#211). */
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { z } from "zod";
5
+ import type { PluginManifest } from "./types.ts";
6
+
7
+ const DependencySchema = z
8
+ .object({
9
+ name: z.string().min(1),
10
+ marketplace: z.string().min(1),
11
+ })
12
+ .strict();
13
+
14
+ const PluginJsonSchema = z.object({
15
+ name: z.string().min(1),
16
+ version: z.string().min(1),
17
+ dependencies: z.array(DependencySchema).optional(),
18
+ });
19
+
20
+ export function pluginSpec(name: string, marketplace: string): string {
21
+ return `${name}@${marketplace}`;
22
+ }
23
+
24
+ export function readPluginManifest(pluginDir: string): PluginManifest | null {
25
+ const manifestPath = join(pluginDir, ".claude-plugin", "plugin.json");
26
+ let raw: unknown;
27
+ try {
28
+ raw = JSON.parse(readFileSync(manifestPath, "utf8"));
29
+ } catch {
30
+ return null;
31
+ }
32
+ const parsed = PluginJsonSchema.safeParse(raw);
33
+ if (!parsed.success) {
34
+ return null;
35
+ }
36
+ const marketplace = "toolu";
37
+ const { name, version, dependencies } = parsed.data;
38
+ return {
39
+ name,
40
+ spec: pluginSpec(name, marketplace),
41
+ marketplace,
42
+ version,
43
+ pluginDir,
44
+ dependencies: dependencies ?? [],
45
+ };
46
+ }
@@ -0,0 +1,66 @@
1
+ /** Scan plugins root for manifests; never reads Claude install sets (#211). */
2
+ import { readdirSync, statSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { readPluginManifest } from "./manifest.ts";
5
+ import type { PluginInventoryEntry, PluginManifest } from "./types.ts";
6
+
7
+ export function listPluginManifests(pluginsRoot: string): PluginManifest[] | null {
8
+ let entries: string[];
9
+ try {
10
+ entries = readdirSync(pluginsRoot);
11
+ } catch {
12
+ return null;
13
+ }
14
+
15
+ const manifests: PluginManifest[] = [];
16
+ for (const entry of entries) {
17
+ const pluginDir = join(pluginsRoot, entry);
18
+ try {
19
+ if (!statSync(pluginDir).isDirectory()) {
20
+ continue;
21
+ }
22
+ } catch {
23
+ continue;
24
+ }
25
+ const manifest = readPluginManifest(pluginDir);
26
+ if (manifest) {
27
+ manifests.push(manifest);
28
+ }
29
+ }
30
+ return manifests;
31
+ }
32
+
33
+ export function lookupPluginInstallState(
34
+ pluginsRoot: string,
35
+ pluginName: string,
36
+ ): PluginInventoryEntry["install"] {
37
+ const manifests = listPluginManifests(pluginsRoot);
38
+ if (manifests === null) {
39
+ return "unknown";
40
+ }
41
+ const hit = manifests.some((m) => m.name === pluginName);
42
+ return hit ? "installed" : "absent";
43
+ }
44
+
45
+ export function buildInventory(
46
+ pluginsRoot: string,
47
+ enabledNames: Set<string> | null,
48
+ ): PluginInventoryEntry[] | null {
49
+ const manifests = listPluginManifests(pluginsRoot);
50
+ if (manifests === null) {
51
+ return null;
52
+ }
53
+ return manifests.map((manifest) => {
54
+ let enabled: PluginInventoryEntry["enabled"] = "unknown";
55
+ if (enabledNames !== null) {
56
+ enabled = enabledNames.has(manifest.name) ? "enabled" : "disabled";
57
+ }
58
+ return {
59
+ name: manifest.name,
60
+ spec: manifest.spec,
61
+ pluginDir: manifest.pluginDir,
62
+ install: "installed",
63
+ enabled,
64
+ };
65
+ });
66
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Enabled plugin selection (#211).
3
+ *
4
+ * Sources (in order):
5
+ * 1. `<project>/.opencode/toolu/plugins.json` — `{ "version": 1, "enabled": ["toolu", ...] }`
6
+ * 2. `<project>/.opencode/toolu.config.json` or repo `.opencode/toolu.config.json` —
7
+ * `skills.<pluginName> === false` disables an otherwise-installed plugin.
8
+ * 3. Default: every installed plugin is enabled.
9
+ *
10
+ * Never reads Claude `installed_plugins.json` or Codex install snapshots.
11
+ */
12
+ import { existsSync, readFileSync } from "node:fs";
13
+ import { opencodePluginSelectionPath, opencodeProjectConfigPath } from "../host/roots.ts";
14
+ import { listPluginManifests } from "./scan.ts";
15
+ import { z } from "zod";
16
+ import { TooluConfigSchema } from "@toolu/core/config";
17
+
18
+ const SelectionFileSchema = z
19
+ .object({
20
+ version: z.literal(1),
21
+ enabled: z.array(z.string().min(1)),
22
+ })
23
+ .strict();
24
+
25
+ function readSelectionFile(path: string): string[] | null {
26
+ if (!existsSync(path)) {
27
+ return null;
28
+ }
29
+ try {
30
+ const raw: unknown = JSON.parse(readFileSync(path, "utf8"));
31
+ const parsed = SelectionFileSchema.safeParse(raw);
32
+ if (!parsed.success) {
33
+ return null;
34
+ }
35
+ return parsed.data.enabled;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function readSkillsDisabled(projectRoot: string): Set<string> {
42
+ const disabled = new Set<string>();
43
+ const configPath = opencodeProjectConfigPath(projectRoot);
44
+ if (!existsSync(configPath)) {
45
+ return disabled;
46
+ }
47
+ try {
48
+ const raw: unknown = JSON.parse(readFileSync(configPath, "utf8"));
49
+ const parsed = TooluConfigSchema.safeParse(raw);
50
+ if (!parsed.success) {
51
+ return disabled;
52
+ }
53
+ const skills = parsed.data.skills;
54
+ if (!skills) {
55
+ return disabled;
56
+ }
57
+ for (const [name, on] of Object.entries(skills)) {
58
+ if (!on) {
59
+ disabled.add(name);
60
+ }
61
+ }
62
+ } catch {
63
+ return disabled;
64
+ }
65
+ return disabled;
66
+ }
67
+
68
+ /** Resolve enabled plugin names for a project + plugins root. */
69
+ export function resolveEnabledPluginNames(
70
+ pluginsRoot: string,
71
+ projectRoot: string,
72
+ ): { ok: true; enabled: Set<string> } | { ok: false; reason: string } {
73
+ const manifests = listPluginManifests(pluginsRoot);
74
+ if (manifests === null) {
75
+ return { ok: false, reason: `cannot read plugins root: ${pluginsRoot}` };
76
+ }
77
+ const installed = new Set(manifests.map((m) => m.name));
78
+
79
+ const fromFile = readSelectionFile(opencodePluginSelectionPath(projectRoot));
80
+ let enabled: Set<string>;
81
+ if (fromFile !== null) {
82
+ enabled = new Set(fromFile.filter((name) => installed.has(name)));
83
+ } else {
84
+ enabled = new Set(installed);
85
+ }
86
+
87
+ for (const name of readSkillsDisabled(projectRoot)) {
88
+ enabled.delete(name);
89
+ }
90
+
91
+ return { ok: true, enabled };
92
+ }
@@ -0,0 +1,22 @@
1
+ /** Plugin inventory tri-state (#211). */
2
+
3
+ export type PluginInstallState = "installed" | "absent" | "unknown";
4
+
5
+ export type PluginEnabledState = "enabled" | "disabled" | "unknown";
6
+
7
+ export type PluginInventoryEntry = {
8
+ name: string;
9
+ spec: string;
10
+ pluginDir: string;
11
+ install: PluginInstallState;
12
+ enabled: PluginEnabledState;
13
+ };
14
+
15
+ export type PluginManifest = {
16
+ name: string;
17
+ spec: string;
18
+ marketplace: string;
19
+ version: string;
20
+ pluginDir: string;
21
+ dependencies: Array<{ name: string; marketplace: string }>;
22
+ };