@webpresso/agent-kit 3.1.30 → 3.3.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 (300) hide show
  1. package/README.md +53 -15
  2. package/bin/_run.js +183 -20
  3. package/catalog/AGENTS.md.tpl +3 -3
  4. package/catalog/agent/agents/implementer.md +45 -0
  5. package/catalog/agent/rules/parallel-orchestration-quota.md +100 -0
  6. package/catalog/agent/rules/pre-implementation.md +28 -1
  7. package/catalog/agent/rules/supported-agent-clis.md +9 -0
  8. package/catalog/agent/rules/typescript-version.md +50 -0
  9. package/catalog/agent/skills/codex/SKILL.md +25 -1
  10. package/catalog/agent/skills/verify/SKILL.md +4 -2
  11. package/catalog/docs/templates/blueprint.md +1 -1
  12. package/dist/esm/audit/agents.js +1 -0
  13. package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +12 -0
  14. package/dist/esm/audit/blueprint-lifecycle-sql.js +23 -2
  15. package/dist/esm/audit/blueprint-pr-coverage.js +4 -0
  16. package/dist/esm/audit/blueprint-trust.js +14 -10
  17. package/dist/esm/audit/changeset-evidence.d.ts +23 -0
  18. package/dist/esm/audit/changeset-evidence.js +30 -0
  19. package/dist/esm/audit/changeset-required.d.ts +10 -0
  20. package/dist/esm/audit/changeset-required.js +73 -0
  21. package/dist/esm/audit/ci-guardrails-detection.d.ts +27 -0
  22. package/dist/esm/audit/ci-guardrails-detection.js +68 -0
  23. package/dist/esm/audit/ci-guardrails-wiring.d.ts +22 -0
  24. package/dist/esm/audit/ci-guardrails-wiring.js +80 -0
  25. package/dist/esm/audit/ci-test-perf.js +30 -0
  26. package/dist/esm/audit/command-surface.js +4 -2
  27. package/dist/esm/audit/host-substitution-risk.d.ts +2 -0
  28. package/dist/esm/audit/host-substitution-risk.js +256 -0
  29. package/dist/esm/audit/registry.d.ts +4 -4
  30. package/dist/esm/audit/registry.js +13 -1
  31. package/dist/esm/audit/toolchain-isolation.js +1 -2
  32. package/dist/esm/audit/typescript-version.d.ts +2 -0
  33. package/dist/esm/audit/typescript-version.js +123 -0
  34. package/dist/esm/blueprint/core/parser.js +101 -2
  35. package/dist/esm/blueprint/core/schema.d.ts +1 -0
  36. package/dist/esm/blueprint/core/schema.js +26 -0
  37. package/dist/esm/blueprint/core/validation/criteria.d.ts +14 -0
  38. package/dist/esm/blueprint/core/validation/criteria.js +41 -2
  39. package/dist/esm/blueprint/core/validation/state.js +25 -14
  40. package/dist/esm/blueprint/db/migrations/run.js +4 -2
  41. package/dist/esm/blueprint/lifecycle/audit.d.ts +20 -0
  42. package/dist/esm/blueprint/lifecycle/audit.js +87 -20
  43. package/dist/esm/blueprint/lifecycle/canonicalize.d.ts +6 -0
  44. package/dist/esm/blueprint/lifecycle/canonicalize.js +135 -0
  45. package/dist/esm/blueprint/lifecycle/local.d.ts +2 -1
  46. package/dist/esm/blueprint/lifecycle/local.js +19 -3
  47. package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
  48. package/dist/esm/blueprint/lifecycle/review-provenance.js +68 -12
  49. package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +97 -0
  50. package/dist/esm/blueprint/lifecycle/rollout-anchor.js +161 -0
  51. package/dist/esm/blueprint/service/BlueprintCreationService.d.ts +1 -0
  52. package/dist/esm/blueprint/service/BlueprintCreationService.js +53 -4
  53. package/dist/esm/blueprint/service/blueprint-tech-debt-links.js +16 -2
  54. package/dist/esm/blueprint/sync/client.d.ts +2 -1
  55. package/dist/esm/blueprint/sync/client.js +1 -1
  56. package/dist/esm/blueprint/trust/command-runner.d.ts +26 -0
  57. package/dist/esm/blueprint/trust/command-runner.js +148 -0
  58. package/dist/esm/blueprint/trust/dossier.d.ts +1 -0
  59. package/dist/esm/blueprint/trust/dossier.js +40 -6
  60. package/dist/esm/blueprint/trust/gates.d.ts +7 -0
  61. package/dist/esm/blueprint/trust/gates.js +20 -0
  62. package/dist/esm/blueprint/trust/promotion.d.ts +8 -0
  63. package/dist/esm/blueprint/trust/promotion.js +74 -112
  64. package/dist/esm/blueprint/trust/scaffold.d.ts +19 -0
  65. package/dist/esm/blueprint/trust/scaffold.js +64 -0
  66. package/dist/esm/blueprint/trust/validator.d.ts +1 -0
  67. package/dist/esm/blueprint/trust/validator.js +7 -1
  68. package/dist/esm/blueprint/utils/archive.js +27 -5
  69. package/dist/esm/blueprint/utils/document-paths.d.ts +18 -0
  70. package/dist/esm/blueprint/utils/document-paths.js +45 -12
  71. package/dist/esm/build/cli-mcp-parity.js +15 -0
  72. package/dist/esm/cli/bundle/commands/blueprint.d.ts +1 -0
  73. package/dist/esm/cli/bundle/commands/blueprint.js +1 -0
  74. package/dist/esm/cli/cli.d.ts +1 -1
  75. package/dist/esm/cli/cli.js +43 -0
  76. package/dist/esm/cli/commands/agent-launch.d.ts +8 -1
  77. package/dist/esm/cli/commands/agent-launch.js +93 -4
  78. package/dist/esm/cli/commands/audit-core.d.ts +1 -1
  79. package/dist/esm/cli/commands/audit-core.js +8 -1
  80. package/dist/esm/cli/commands/audit.js +21 -5
  81. package/dist/esm/cli/commands/blueprint/execution.js +18 -6
  82. package/dist/esm/cli/commands/blueprint/mutations.js +59 -40
  83. package/dist/esm/cli/commands/blueprint/router-dispatch.d.ts +2 -1
  84. package/dist/esm/cli/commands/blueprint/router-dispatch.js +20 -1
  85. package/dist/esm/cli/commands/blueprint/router-output.d.ts +21 -0
  86. package/dist/esm/cli/commands/blueprint/router-output.js +48 -17
  87. package/dist/esm/cli/commands/blueprint/router.d.ts +7 -0
  88. package/dist/esm/cli/commands/blueprint/router.js +13 -1
  89. package/dist/esm/cli/commands/ci-preflight.d.ts +13 -2
  90. package/dist/esm/cli/commands/ci-preflight.js +30 -6
  91. package/dist/esm/cli/commands/compile.js +5 -31
  92. package/dist/esm/cli/commands/dash/delivery-status.d.ts +1 -0
  93. package/dist/esm/cli/commands/dash/delivery-status.js +11 -4
  94. package/dist/esm/cli/commands/doctor.js +5 -0
  95. package/dist/esm/cli/commands/hook.d.ts +5 -0
  96. package/dist/esm/cli/commands/hook.js +15 -9
  97. package/dist/esm/cli/commands/init/config.js +2 -2
  98. package/dist/esm/cli/commands/init/convergence-apply.d.ts +88 -0
  99. package/dist/esm/cli/commands/init/convergence-apply.js +325 -0
  100. package/dist/esm/cli/commands/init/convergence-state.d.ts +13 -0
  101. package/dist/esm/cli/commands/init/convergence-state.js +54 -0
  102. package/dist/esm/cli/commands/init/convergence.d.ts +23 -0
  103. package/dist/esm/cli/commands/init/convergence.js +57 -0
  104. package/dist/esm/cli/commands/init/git-convergence.d.ts +19 -0
  105. package/dist/esm/cli/commands/init/git-convergence.js +168 -0
  106. package/dist/esm/cli/commands/init/gitignore-patcher.js +2 -0
  107. package/dist/esm/cli/commands/init/grok-privacy-smoke.d.ts +52 -0
  108. package/dist/esm/cli/commands/init/grok-privacy-smoke.js +91 -0
  109. package/dist/esm/cli/commands/init/host-native-proof.d.ts +18 -0
  110. package/dist/esm/cli/commands/init/host-native-proof.js +88 -0
  111. package/dist/esm/cli/commands/init/host-visibility.d.ts +2 -2
  112. package/dist/esm/cli/commands/init/host-visibility.js +94 -5
  113. package/dist/esm/cli/commands/init/index.d.ts +3 -0
  114. package/dist/esm/cli/commands/init/index.js +326 -173
  115. package/dist/esm/cli/commands/init/mcp-spec.d.ts +57 -0
  116. package/dist/esm/cli/commands/init/mcp-spec.js +273 -0
  117. package/dist/esm/cli/commands/init/merge.d.ts +1 -0
  118. package/dist/esm/cli/commands/init/merge.js +30 -1
  119. package/dist/esm/cli/commands/init/package-root.d.ts +7 -0
  120. package/dist/esm/cli/commands/init/package-root.js +10 -2
  121. package/dist/esm/cli/commands/init/plugin-cache-prune.d.ts +0 -3
  122. package/dist/esm/cli/commands/init/plugin-cache-prune.js +0 -1
  123. package/dist/esm/cli/commands/init/scaffold-agents-md.d.ts +1 -1
  124. package/dist/esm/cli/commands/init/scaffold-agents-md.js +9 -3
  125. package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +20 -0
  126. package/dist/esm/cli/commands/init/scaffold-base-kit.js +61 -5
  127. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.d.ts +14 -8
  128. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.js +41 -24
  129. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.d.ts +2 -0
  130. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +54 -24
  131. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +12 -31
  132. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +288 -202
  133. package/dist/esm/cli/commands/init/scaffolders/file-transaction.d.ts +22 -0
  134. package/dist/esm/cli/commands/init/scaffolders/file-transaction.js +230 -0
  135. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.d.ts +38 -0
  136. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.js +73 -0
  137. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.d.ts +21 -0
  138. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.js +559 -63
  139. package/dist/esm/cli/commands/init/untracked-collision.d.ts +14 -0
  140. package/dist/esm/cli/commands/init/untracked-collision.js +42 -0
  141. package/dist/esm/cli/commands/opencode-probe.d.ts +107 -0
  142. package/dist/esm/cli/commands/opencode-probe.js +323 -0
  143. package/dist/esm/cli/commands/opencode-rotate.d.ts +72 -0
  144. package/dist/esm/cli/commands/opencode-rotate.js +147 -0
  145. package/dist/esm/cli/commands/public-readiness.d.ts +11 -0
  146. package/dist/esm/cli/commands/public-readiness.js +42 -0
  147. package/dist/esm/cli/commands/quality-runner.js +3 -3
  148. package/dist/esm/cli/commands/review.d.ts +35 -3
  149. package/dist/esm/cli/commands/review.js +751 -209
  150. package/dist/esm/cli/commands/run-wait.d.ts +2 -0
  151. package/dist/esm/cli/commands/run-wait.js +122 -0
  152. package/dist/esm/cli/commands/rust-check.d.ts +24 -0
  153. package/dist/esm/cli/commands/rust-check.js +110 -0
  154. package/dist/esm/cli/commands/setup-help.js +1 -1
  155. package/dist/esm/cli/commands/setup-repair-help.js +1 -1
  156. package/dist/esm/cli/commands/typecheck.d.ts +1 -0
  157. package/dist/esm/cli/commands/typecheck.js +30 -1
  158. package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +6 -0
  159. package/dist/esm/cli/commands/worktree/router-dispatch.js +126 -9
  160. package/dist/esm/cli/commands/worktree/router.js +10 -0
  161. package/dist/esm/cli/direct-provider-launch.js +9 -5
  162. package/dist/esm/cli/optional-tool-freshness.d.ts +27 -0
  163. package/dist/esm/cli/optional-tool-freshness.js +219 -9
  164. package/dist/esm/daemon/domains/git-actions.d.ts +29 -0
  165. package/dist/esm/daemon/domains/git-actions.js +136 -0
  166. package/dist/esm/daemon/domains/run-registry-interop.d.ts +16 -0
  167. package/dist/esm/daemon/domains/run-registry-interop.js +45 -0
  168. package/dist/esm/daemon/domains/sessions-search.d.ts +11 -0
  169. package/dist/esm/daemon/domains/sessions-search.js +29 -0
  170. package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +5 -0
  171. package/dist/esm/daemon/handlers/blueprints-handlers.js +36 -0
  172. package/dist/esm/daemon/handlers/deploy-handlers.d.ts +6 -0
  173. package/dist/esm/daemon/handlers/deploy-handlers.js +5 -0
  174. package/dist/esm/daemon/handlers/fleet-handlers.d.ts +15 -0
  175. package/dist/esm/daemon/handlers/fleet-handlers.js +91 -0
  176. package/dist/esm/daemon/handlers/git-handlers.d.ts +6 -0
  177. package/dist/esm/daemon/handlers/git-handlers.js +27 -0
  178. package/dist/esm/daemon/handlers/product.d.ts +7 -0
  179. package/dist/esm/daemon/handlers/product.js +10 -119
  180. package/dist/esm/daemon/handlers/sessions-handlers.d.ts +12 -0
  181. package/dist/esm/daemon/handlers/sessions-handlers.js +31 -0
  182. package/dist/esm/daemon/handlers/shared.d.ts +5 -0
  183. package/dist/esm/daemon/handlers/shared.js +16 -0
  184. package/dist/esm/daemon/methods.d.ts +8 -1
  185. package/dist/esm/daemon/methods.js +7 -0
  186. package/dist/esm/daemon/protocol/contract.d.ts +43 -0
  187. package/dist/esm/daemon/protocol/contract.js +26 -0
  188. package/dist/esm/daemon/protocol/index.d.ts +1 -1
  189. package/dist/esm/daemon/protocol/index.js +1 -1
  190. package/dist/esm/hooks/__conformance__/matrix.d.ts +6 -0
  191. package/dist/esm/hooks/__conformance__/matrix.js +137 -0
  192. package/dist/esm/hooks/conformance/batch.js +5 -0
  193. package/dist/esm/hooks/doctor.d.ts +5 -3
  194. package/dist/esm/hooks/doctor.js +67 -16
  195. package/dist/esm/hooks/permission-request/index.js +9 -3
  196. package/dist/esm/hooks/permission-request/policy.d.ts +9 -0
  197. package/dist/esm/hooks/permission-request/policy.js +13 -0
  198. package/dist/esm/hooks/pretool-guard/dev-routing.d.ts +9 -0
  199. package/dist/esm/hooks/pretool-guard/dev-routing.js +41 -5
  200. package/dist/esm/hooks/pretool-guard/logger.d.ts +7 -0
  201. package/dist/esm/hooks/pretool-guard/logger.js +16 -0
  202. package/dist/esm/hooks/pretool-guard/runner.d.ts +6 -0
  203. package/dist/esm/hooks/pretool-guard/runner.js +68 -16
  204. package/dist/esm/hooks/pretool-guard/validators/blueprint.js +30 -10
  205. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +54 -0
  206. package/dist/esm/hooks/pretool-guard/validators/index.d.ts +7 -0
  207. package/dist/esm/hooks/pretool-guard/validators/index.js +21 -0
  208. package/dist/esm/hooks/pretool-guard/validators/path-contract.d.ts +19 -0
  209. package/dist/esm/hooks/pretool-guard/validators/path-contract.js +25 -8
  210. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +6 -1
  211. package/dist/esm/hooks/shared/guard-outcome.d.ts +149 -0
  212. package/dist/esm/hooks/shared/guard-outcome.js +215 -0
  213. package/dist/esm/hooks/shared/hook-bootstrap.d.ts +26 -0
  214. package/dist/esm/hooks/shared/hook-bootstrap.js +64 -1
  215. package/dist/esm/hooks/shared/types.d.ts +8 -5
  216. package/dist/esm/hooks/shared/types.js +50 -9
  217. package/dist/esm/hooks/stop/qa-changed-files.d.ts +11 -0
  218. package/dist/esm/hooks/stop/qa-changed-files.js +92 -0
  219. package/dist/esm/mcp/blueprint/_shared/lifecycle.js +14 -1
  220. package/dist/esm/mcp/blueprint/handlers/document-mutations.d.ts +5 -0
  221. package/dist/esm/mcp/blueprint/handlers/document-mutations.js +109 -11
  222. package/dist/esm/mcp/blueprint/handlers/review-log.js +8 -1
  223. package/dist/esm/mcp/blueprint/handlers/task-advance.js +113 -54
  224. package/dist/esm/mcp/blueprint/handlers/task-verify.js +39 -19
  225. package/dist/esm/mcp/blueprint/registration.js +3 -2
  226. package/dist/esm/mcp/server.d.ts +1 -0
  227. package/dist/esm/mcp/server.js +27 -0
  228. package/dist/esm/mcp/tools/_names.d.ts +1 -1
  229. package/dist/esm/mcp/tools/_names.js +5 -0
  230. package/dist/esm/mcp/tools/_registry.js +10 -0
  231. package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
  232. package/dist/esm/mcp/tools/_shared/result.js +44 -1
  233. package/dist/esm/mcp/tools/audit.d.ts +5 -1
  234. package/dist/esm/mcp/tools/audit.js +42 -0
  235. package/dist/esm/mcp/tools/audits.d.ts +4 -0
  236. package/dist/esm/mcp/tools/ci-preflight.d.ts +50 -0
  237. package/dist/esm/mcp/tools/ci-preflight.js +183 -0
  238. package/dist/esm/mcp/tools/pr-upsert.js +39 -1
  239. package/dist/esm/mcp/tools/review-run.d.ts +28 -0
  240. package/dist/esm/mcp/tools/review-run.js +216 -0
  241. package/dist/esm/mcp/tools/run-wait.d.ts +86 -0
  242. package/dist/esm/mcp/tools/run-wait.js +593 -0
  243. package/dist/esm/mcp/tools/session-fetch-and-index.d.ts +1 -1
  244. package/dist/esm/mcp/tools/session-id.d.ts +3 -0
  245. package/dist/esm/mcp/tools/session-id.js +45 -0
  246. package/dist/esm/mcp/tools/session-info.d.ts +3 -0
  247. package/dist/esm/mcp/tools/session-info.js +49 -0
  248. package/dist/esm/output-transforms/audit.d.ts +23 -0
  249. package/dist/esm/output-transforms/audit.js +47 -0
  250. package/dist/esm/output-transforms/index.js +2 -0
  251. package/dist/esm/package.json +2 -0
  252. package/dist/esm/platform/client.d.ts +7 -1
  253. package/dist/esm/platform/github-contract-source.d.ts +2 -1
  254. package/dist/esm/pr-description/contract.js +24 -4
  255. package/dist/esm/review/authority.js +27 -14
  256. package/dist/esm/review/availability.d.ts +33 -0
  257. package/dist/esm/review/availability.js +122 -20
  258. package/dist/esm/review/delivery-verifier.d.ts +27 -0
  259. package/dist/esm/review/delivery-verifier.js +115 -0
  260. package/dist/esm/review/events.d.ts +3 -2
  261. package/dist/esm/review/events.js +23 -3
  262. package/dist/esm/review/execution/adapters.d.ts +11 -1
  263. package/dist/esm/review/execution/adapters.js +36 -10
  264. package/dist/esm/review/execution/artifacts.d.ts +1 -1
  265. package/dist/esm/review/execution/output-policy.d.ts +2 -0
  266. package/dist/esm/review/execution/output-policy.js +12 -0
  267. package/dist/esm/review/execution/review-checkout.js +121 -21
  268. package/dist/esm/review/execution/supervisor.js +36 -4
  269. package/dist/esm/review/execution/types.d.ts +9 -0
  270. package/dist/esm/review/opencode-account.d.ts +24 -0
  271. package/dist/esm/review/opencode-account.js +68 -0
  272. package/dist/esm/review/opencode-auth.d.ts +33 -0
  273. package/dist/esm/review/opencode-auth.js +79 -0
  274. package/dist/esm/review/subject.d.ts +25 -0
  275. package/dist/esm/review/subject.js +116 -26
  276. package/dist/esm/runtime/opencode-account-materializer.d.ts +34 -0
  277. package/dist/esm/runtime/opencode-account-materializer.js +33 -0
  278. package/dist/esm/rust/affected-manifests.d.ts +38 -0
  279. package/dist/esm/rust/affected-manifests.js +69 -0
  280. package/dist/esm/session-memory/fetch-index.d.ts +1 -1
  281. package/dist/esm/status/snapshot.js +32 -25
  282. package/dist/esm/test/duration-sequencer.d.ts +20 -0
  283. package/dist/esm/test/duration-sequencer.js +54 -0
  284. package/dist/esm/test/shard-durations.json +922 -0
  285. package/dist/esm/test/worker-budget.d.ts +6 -0
  286. package/dist/esm/test/worker-budget.js +59 -1
  287. package/dist/esm/test-helpers/global-setup.d.ts +1 -2
  288. package/dist/esm/test-helpers/global-setup.js +79 -32
  289. package/dist/esm/typecheck/planner.d.ts +9 -1
  290. package/dist/esm/typecheck/planner.js +31 -0
  291. package/dist/esm/ultragoal/service.d.ts +7 -0
  292. package/dist/esm/ultragoal/service.js +42 -2
  293. package/dist/esm/utils/write-json-file.js +3 -1
  294. package/dist/esm/worktrees/codex-project-trust.d.ts +15 -0
  295. package/dist/esm/worktrees/codex-project-trust.js +56 -0
  296. package/dist/esm/worktrees/orphan-scan.d.ts +50 -0
  297. package/dist/esm/worktrees/orphan-scan.js +211 -0
  298. package/dist/esm/worktrees/registry.d.ts +8 -0
  299. package/dist/esm/worktrees/registry.js +18 -3
  300. package/package.json +18 -13
@@ -1,5 +1,8 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
1
+ import { existsSync, readFileSync, rmSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { withScaffolderFileLock } from "#cli/commands/init/scaffolders/file-transaction.js";
4
+ import { writeFileAtomicStrict } from "#shared-utils/write-json-file.js";
5
+ import { agentKitMcpLaunchCommand, resolveWebpressoMcpEntry, WEBPRESSO_MCP_SPEC, } from "#cli/commands/init/mcp-spec.js";
3
6
  const STALE_OPENCODE_HOOK_PLUGIN_PATH_PARTS = [
4
7
  ".opencode",
5
8
  "plugins",
@@ -9,6 +12,22 @@ export const STALE_OPENCODE_HOOK_PLUGIN_RELATIVE_PATH = STALE_OPENCODE_HOOK_PLUG
9
12
  export const OPENCODE_PLUGIN_SUPPORT_LEVEL = "native-npm";
10
13
  export const OPENCODE_PLUGIN_PACKAGE = "@webpresso/opencode-plugin";
11
14
  const OPENCODE_PLUGIN_CONFIG_KEY = "plugin";
15
+ export const OPENCODE_MCP_SERVER_NAME = WEBPRESSO_MCP_SPEC.name;
16
+ /**
17
+ * The canonical OpenCode `mcp.webpresso` entry. OpenCode's local-server shape
18
+ * folds the launcher and its args into a single `command` array, keys env
19
+ * under `environment`, and gates activation with `enabled`. Projected from the
20
+ * one canonical {@link WEBPRESSO_MCP_SPEC}.
21
+ */
22
+ export function opencodeWebpressoMcpEntry(entryPath) {
23
+ const launch = agentKitMcpLaunchCommand(entryPath);
24
+ return {
25
+ type: "local",
26
+ command: [launch.command, ...launch.args],
27
+ enabled: true,
28
+ environment: { ...WEBPRESSO_MCP_SPEC.env },
29
+ };
30
+ }
12
31
  const LEGACY_WEBPRESSO_HOOK_PLUGIN_MARKERS = [
13
32
  "Generated by webpresso (`wp setup`).",
14
33
  "export const WebpressoHooksPlugin",
@@ -20,53 +39,36 @@ function isJsonRecord(value) {
20
39
  function isStringArray(value) {
21
40
  return Array.isArray(value) && value.every((entry) => typeof entry === "string");
22
41
  }
23
- function commandParts(value) {
24
- if (!isJsonRecord(value))
25
- return [];
26
- const command = value.command;
27
- if (typeof command === "string")
28
- return [command];
29
- if (Array.isArray(command)) {
30
- return command.filter((part) => typeof part === "string");
31
- }
32
- return [];
33
- }
34
42
  function isPotentialWebpressoOpenCodeMcpAlias(name, value) {
35
- return (name === "agent-kit" || name === "webpresso" || isWebpressoOwnedOpenCodeMcpAlias(name, value));
43
+ return isJsonRecord(value) && (name === "agent-kit" || name === "webpresso");
36
44
  }
37
- function isWebpressoOwnedOpenCodeMcpAlias(name, value) {
38
- const parts = commandParts(value);
39
- if (parts.length === 0)
40
- return false;
41
- const commandText = parts.join(" ");
42
- const isNamedLegacyAlias = name === "agent-kit" || name === "webpresso";
43
- return (parts.some((part) => /[/\\]agent-kit[/\\]/u.test(part) ||
44
- part.includes("@webpresso/agent-kit") ||
45
- part.includes("src/mcp/cli.ts")) ||
46
- (isNamedLegacyAlias && /(?:^|\s)(?:vp\s+exec\s+)?wp\s+mcp(?:\s|$)/u.test(commandText)));
47
- }
48
- function collectPreservedLegacyMcpAliases(value, removeOwned) {
45
+ function collectPreservedLegacyMcpAliases(value) {
49
46
  if (!isJsonRecord(value))
50
47
  return [];
51
48
  return Object.entries(value)
52
49
  .filter(([name, server]) => isPotentialWebpressoOpenCodeMcpAlias(name, server) &&
53
- !(removeOwned && isWebpressoOwnedOpenCodeMcpAlias(name, server)))
50
+ !isKnownWebpressoOwnedLegacyMcpAlias(name, server))
54
51
  .map(([name]) => `mcp:${name}`)
55
52
  .toSorted();
56
53
  }
57
- function removeOwnedLegacyMcpAliases(value) {
54
+ function commandWords(value) {
58
55
  if (!isJsonRecord(value))
59
- return { nextMcp: undefined, removed: [] };
60
- const nextMcp = {};
61
- const removed = [];
62
- for (const [name, server] of Object.entries(value)) {
63
- if (isWebpressoOwnedOpenCodeMcpAlias(name, server)) {
64
- removed.push(`mcp:${name}`);
65
- continue;
66
- }
67
- nextMcp[name] = server;
56
+ return [];
57
+ const command = value.command;
58
+ return isStringArray(command) ? command : [];
59
+ }
60
+ function isKnownWebpressoOwnedLegacyMcpAlias(name, value) {
61
+ const command = commandWords(value);
62
+ const commandText = command.join(" ");
63
+ if (name === "webpresso") {
64
+ return command.join("\0") === ["vp", "exec", "wp", "mcp"].join("\0");
68
65
  }
69
- return { nextMcp, removed: removed.toSorted() };
66
+ if (name !== "agent-kit")
67
+ return false;
68
+ return (commandText.includes("@webpresso/agent-kit") ||
69
+ commandText.includes(".claude/plugins/cache") ||
70
+ commandText.includes("/agent-kit/src/mcp/") ||
71
+ commandText.includes("\\agent-kit\\src\\mcp\\"));
70
72
  }
71
73
  function isWebpressoOwnedStaleHookPlugin(staleHookPluginPath) {
72
74
  if (!existsSync(staleHookPluginPath))
@@ -74,15 +76,488 @@ function isWebpressoOwnedStaleHookPlugin(staleHookPluginPath) {
74
76
  const staleHookPluginContent = readFileSync(staleHookPluginPath, "utf8");
75
77
  return LEGACY_WEBPRESSO_HOOK_PLUGIN_MARKERS.every((marker) => staleHookPluginContent.includes(marker));
76
78
  }
79
+ function stripJsoncForParse(raw) {
80
+ let out = "";
81
+ let inString = false;
82
+ let quote = null;
83
+ let escaped = false;
84
+ for (let i = 0; i < raw.length; i += 1) {
85
+ const ch = raw[i];
86
+ const next = raw[i + 1];
87
+ if (inString) {
88
+ out += ch;
89
+ if (escaped) {
90
+ escaped = false;
91
+ }
92
+ else if (ch === "\\") {
93
+ escaped = true;
94
+ }
95
+ else if (ch === quote) {
96
+ inString = false;
97
+ quote = null;
98
+ }
99
+ continue;
100
+ }
101
+ if (ch === '"' || ch === "'") {
102
+ inString = true;
103
+ quote = ch;
104
+ out += ch;
105
+ continue;
106
+ }
107
+ if (ch === "/" && next === "/") {
108
+ while (i < raw.length && raw[i] !== "\n")
109
+ i += 1;
110
+ out += "\n";
111
+ continue;
112
+ }
113
+ if (ch === "/" && next === "*") {
114
+ i += 2;
115
+ while (i < raw.length && !(raw[i] === "*" && raw[i + 1] === "/"))
116
+ i += 1;
117
+ i += 1;
118
+ continue;
119
+ }
120
+ out += ch;
121
+ }
122
+ return stripJsoncTrailingCommas(out);
123
+ }
124
+ function stripJsoncTrailingCommas(raw) {
125
+ let out = "";
126
+ let inString = false;
127
+ let quote = null;
128
+ let escaped = false;
129
+ for (let i = 0; i < raw.length; i += 1) {
130
+ const ch = raw[i];
131
+ if (inString) {
132
+ out += ch;
133
+ if (escaped) {
134
+ escaped = false;
135
+ }
136
+ else if (ch === "\\") {
137
+ escaped = true;
138
+ }
139
+ else if (ch === quote) {
140
+ inString = false;
141
+ quote = null;
142
+ }
143
+ continue;
144
+ }
145
+ if (ch === '"' || ch === "'") {
146
+ inString = true;
147
+ quote = ch;
148
+ out += ch;
149
+ continue;
150
+ }
151
+ if (ch === ",") {
152
+ let j = i + 1;
153
+ while (/\s/u.test(raw[j] ?? ""))
154
+ j += 1;
155
+ if (raw[j] === "}" || raw[j] === "]")
156
+ continue;
157
+ }
158
+ out += ch;
159
+ }
160
+ return out;
161
+ }
162
+ function parseOpenCodeConfig(existing, configPath) {
163
+ if (existing.trim().length === 0)
164
+ return {};
165
+ const parseInput = configPath.endsWith(".jsonc") ? stripJsoncForParse(existing) : existing;
166
+ return JSON.parse(parseInput);
167
+ }
168
+ function isJsoncConfigPath(configPath) {
169
+ return configPath.endsWith(".jsonc");
170
+ }
171
+ function findMatchingJsoncBracket(raw, openIndex) {
172
+ const open = raw[openIndex];
173
+ const close = open === "{" ? "}" : open === "[" ? "]" : null;
174
+ if (!close)
175
+ return -1;
176
+ let depth = 0;
177
+ let inString = false;
178
+ let quote = null;
179
+ let escaped = false;
180
+ for (let i = openIndex; i < raw.length; i += 1) {
181
+ const ch = raw[i];
182
+ const next = raw[i + 1];
183
+ if (inString) {
184
+ if (escaped)
185
+ escaped = false;
186
+ else if (ch === "\\")
187
+ escaped = true;
188
+ else if (ch === quote) {
189
+ inString = false;
190
+ quote = null;
191
+ }
192
+ continue;
193
+ }
194
+ if (ch === '"' || ch === "'") {
195
+ inString = true;
196
+ quote = ch;
197
+ continue;
198
+ }
199
+ if (ch === "/" && next === "/") {
200
+ while (i < raw.length && raw[i] !== "\n")
201
+ i += 1;
202
+ continue;
203
+ }
204
+ if (ch === "/" && next === "*") {
205
+ i += 2;
206
+ while (i < raw.length && !(raw[i] === "*" && raw[i + 1] === "/"))
207
+ i += 1;
208
+ i += 1;
209
+ continue;
210
+ }
211
+ if (ch === open)
212
+ depth += 1;
213
+ if (ch === close) {
214
+ depth -= 1;
215
+ if (depth === 0)
216
+ return i;
217
+ }
218
+ }
219
+ return -1;
220
+ }
221
+ function skipJsoncTrivia(raw, start) {
222
+ let i = start;
223
+ while (i < raw.length) {
224
+ const ch = raw[i];
225
+ const next = raw[i + 1];
226
+ if (/\s/u.test(ch)) {
227
+ i += 1;
228
+ continue;
229
+ }
230
+ if (ch === "/" && next === "/") {
231
+ while (i < raw.length && raw[i] !== "\n")
232
+ i += 1;
233
+ continue;
234
+ }
235
+ if (ch === "/" && next === "*") {
236
+ i += 2;
237
+ while (i < raw.length && !(raw[i] === "*" && raw[i + 1] === "/"))
238
+ i += 1;
239
+ i += 2;
240
+ continue;
241
+ }
242
+ break;
243
+ }
244
+ return i;
245
+ }
246
+ function parseJsoncStringToken(raw, start) {
247
+ if (raw[start] !== '"')
248
+ return null;
249
+ let json = '"';
250
+ for (let i = start + 1; i < raw.length; i += 1) {
251
+ const ch = raw[i];
252
+ json += ch;
253
+ if (ch === "\\") {
254
+ i += 1;
255
+ if (i < raw.length)
256
+ json += raw[i];
257
+ continue;
258
+ }
259
+ if (ch === '"') {
260
+ try {
261
+ return { value: JSON.parse(json), end: i + 1 };
262
+ }
263
+ catch {
264
+ return null;
265
+ }
266
+ }
267
+ }
268
+ return null;
269
+ }
270
+ function findJsoncValueEnd(raw, start, objectEnd) {
271
+ const ch = raw[start];
272
+ if (ch === "{" || ch === "[") {
273
+ const end = findMatchingJsoncBracket(raw, start);
274
+ return end === -1 ? objectEnd : end + 1;
275
+ }
276
+ if (ch === '"')
277
+ return parseJsoncStringToken(raw, start)?.end ?? objectEnd;
278
+ let i = start;
279
+ while (i < objectEnd) {
280
+ const current = raw[i];
281
+ const next = raw[i + 1];
282
+ if (current === "/" && next === "/") {
283
+ i += 2;
284
+ while (i < objectEnd && raw[i] !== "\n")
285
+ i += 1;
286
+ continue;
287
+ }
288
+ if (current === "/" && next === "*") {
289
+ i += 2;
290
+ while (i < objectEnd && !(raw[i] === "*" && raw[i + 1] === "/"))
291
+ i += 1;
292
+ i = Math.min(i + 2, objectEnd);
293
+ continue;
294
+ }
295
+ if (current === "," || current === "}")
296
+ break;
297
+ i += 1;
298
+ }
299
+ return i;
300
+ }
301
+ function findJsoncObjectProperty(raw, objectStart, objectEnd, key) {
302
+ let i = objectStart + 1;
303
+ while (i < objectEnd) {
304
+ i = skipJsoncTrivia(raw, i);
305
+ if (i >= objectEnd || raw[i] === "}")
306
+ return null;
307
+ const keyToken = parseJsoncStringToken(raw, i);
308
+ if (!keyToken)
309
+ return null;
310
+ const colon = skipJsoncTrivia(raw, keyToken.end);
311
+ if (raw[colon] !== ":")
312
+ return null;
313
+ const valueStart = skipJsoncTrivia(raw, colon + 1);
314
+ const valueEnd = findJsoncValueEnd(raw, valueStart, objectEnd);
315
+ if (keyToken.value === key)
316
+ return { keyStart: i, valueStart, valueEnd };
317
+ i = skipJsoncTrivia(raw, valueEnd);
318
+ if (raw[i] === ",")
319
+ i += 1;
320
+ }
321
+ return null;
322
+ }
323
+ function countJsoncObjectProperties(raw, objectStart, objectEnd, key) {
324
+ let count = 0;
325
+ let i = objectStart + 1;
326
+ while (i < objectEnd) {
327
+ i = skipJsoncTrivia(raw, i);
328
+ if (i >= objectEnd || raw[i] === "}")
329
+ return count;
330
+ const keyToken = parseJsoncStringToken(raw, i);
331
+ if (!keyToken)
332
+ return count;
333
+ const colon = skipJsoncTrivia(raw, keyToken.end);
334
+ if (raw[colon] !== ":")
335
+ return count;
336
+ const valueStart = skipJsoncTrivia(raw, colon + 1);
337
+ const valueEnd = findJsoncValueEnd(raw, valueStart, objectEnd);
338
+ if (keyToken.value === key)
339
+ count += 1;
340
+ i = skipJsoncTrivia(raw, valueEnd);
341
+ if (raw[i] === ",")
342
+ i += 1;
343
+ }
344
+ return count;
345
+ }
346
+ function hasDuplicateManagedJsoncKeys(raw, configPath) {
347
+ if (!isJsoncConfigPath(configPath) || raw.trim().length === 0)
348
+ return false;
349
+ const rootStart = skipJsoncTrivia(raw, 0);
350
+ if (rootStart >= raw.length || raw[rootStart] !== "{")
351
+ return false;
352
+ const rootEnd = findMatchingJsoncBracket(raw, rootStart);
353
+ if (rootEnd === -1)
354
+ return false;
355
+ if (countJsoncObjectProperties(raw, rootStart, rootEnd, OPENCODE_PLUGIN_CONFIG_KEY) > 1)
356
+ return true;
357
+ if (countJsoncObjectProperties(raw, rootStart, rootEnd, "mcp") > 1)
358
+ return true;
359
+ const mcpProp = findJsoncObjectProperty(raw, rootStart, rootEnd, "mcp");
360
+ if (mcpProp === null)
361
+ return false;
362
+ const mcpEnd = findMatchingJsoncBracket(raw, mcpProp.valueStart);
363
+ if (mcpEnd === -1)
364
+ return false;
365
+ return countJsoncObjectProperties(raw, mcpProp.valueStart, mcpEnd, OPENCODE_MCP_SERVER_NAME) > 1;
366
+ }
367
+ function previousJsoncSignificantChar(raw, before) {
368
+ let last = "";
369
+ let inString = false;
370
+ let quote = null;
371
+ let escaped = false;
372
+ for (let i = 0; i < before; i += 1) {
373
+ const ch = raw[i];
374
+ const next = raw[i + 1];
375
+ if (inString) {
376
+ if (escaped)
377
+ escaped = false;
378
+ else if (ch === "\\")
379
+ escaped = true;
380
+ else if (ch === quote) {
381
+ inString = false;
382
+ quote = null;
383
+ }
384
+ continue;
385
+ }
386
+ if (ch === '"' || ch === "'") {
387
+ inString = true;
388
+ quote = ch;
389
+ last = ch;
390
+ continue;
391
+ }
392
+ if (ch === "/" && next === "/") {
393
+ while (i < before && raw[i] !== "\n")
394
+ i += 1;
395
+ continue;
396
+ }
397
+ if (ch === "/" && next === "*") {
398
+ i += 2;
399
+ while (i < before && !(raw[i] === "*" && raw[i + 1] === "/"))
400
+ i += 1;
401
+ i += 1;
402
+ continue;
403
+ }
404
+ if (!/\s/u.test(ch))
405
+ last = ch;
406
+ }
407
+ return last;
408
+ }
409
+ function insertJsoncObjectProperty(raw, objectEnd, key, value, indent) {
410
+ const previous = previousJsoncSignificantChar(raw, objectEnd);
411
+ const needsComma = previous !== "{" && previous !== ",";
412
+ const prefix = needsComma ? "," : "";
413
+ const rendered = JSON.stringify(value, null, 2)
414
+ .split("\n")
415
+ .map((line, index) => (index === 0 ? line : `${indent}${line}`))
416
+ .join("\n");
417
+ return `${raw.slice(0, objectEnd)}${prefix}\n${indent}${JSON.stringify(key)}: ${rendered}\n${raw.slice(objectEnd)}`;
418
+ }
419
+ function removeJsoncObjectProperty(raw, property, objectStart) {
420
+ const afterValue = skipJsoncTrivia(raw, property.valueEnd);
421
+ if (raw[afterValue] === ",") {
422
+ return `${raw.slice(0, property.keyStart)}${raw.slice(afterValue + 1)}`;
423
+ }
424
+ let previousComma = -1;
425
+ let inString = false;
426
+ let quote = null;
427
+ let escaped = false;
428
+ for (let i = objectStart + 1; i < property.keyStart; i += 1) {
429
+ const ch = raw[i];
430
+ const next = raw[i + 1];
431
+ if (inString) {
432
+ if (escaped)
433
+ escaped = false;
434
+ else if (ch === "\\")
435
+ escaped = true;
436
+ else if (ch === quote) {
437
+ inString = false;
438
+ quote = null;
439
+ }
440
+ continue;
441
+ }
442
+ if (ch === '"' || ch === "'") {
443
+ inString = true;
444
+ quote = ch;
445
+ continue;
446
+ }
447
+ if (ch === "/" && next === "/") {
448
+ while (i < property.keyStart && raw[i] !== "\n")
449
+ i += 1;
450
+ continue;
451
+ }
452
+ if (ch === "/" && next === "*") {
453
+ i += 2;
454
+ while (i < property.keyStart && !(raw[i] === "*" && raw[i + 1] === "/"))
455
+ i += 1;
456
+ i += 1;
457
+ continue;
458
+ }
459
+ if (ch === ",")
460
+ previousComma = i;
461
+ }
462
+ if (previousComma !== -1) {
463
+ return `${raw.slice(0, previousComma)}${raw.slice(property.valueEnd)}`;
464
+ }
465
+ return `${raw.slice(0, property.keyStart)}${raw.slice(property.valueEnd)}`;
466
+ }
467
+ function appendJsoncArrayString(raw, arrayEnd, value, indent) {
468
+ const previous = previousJsoncSignificantChar(raw, arrayEnd);
469
+ const needsComma = previous !== "[" && previous !== ",";
470
+ const prefix = needsComma ? "," : "";
471
+ return `${raw.slice(0, arrayEnd)}${prefix}\n${indent}${JSON.stringify(value)}\n${raw.slice(arrayEnd)}`;
472
+ }
473
+ function renderOpenCodeConfig(existing, configPath, nextRoot, nextPlugins, hasPluginRegistration, mcpEntryPath, removedMcpServerNames) {
474
+ if (!isJsoncConfigPath(configPath))
475
+ return `${JSON.stringify(nextRoot, null, 2)}\n`;
476
+ if (existing.trim().length === 0)
477
+ return `${JSON.stringify(nextRoot, null, 2)}\n`;
478
+ let next = existing;
479
+ const rootStart = skipJsoncTrivia(next, 0);
480
+ if (rootStart >= next.length || next[rootStart] !== "{")
481
+ return `${JSON.stringify(nextRoot, null, 2)}\n`;
482
+ let rootEnd = findMatchingJsoncBracket(next, rootStart);
483
+ if (rootEnd === -1)
484
+ return `${JSON.stringify(nextRoot, null, 2)}\n`;
485
+ const pluginProp = findJsoncObjectProperty(next, rootStart, rootEnd, OPENCODE_PLUGIN_CONFIG_KEY);
486
+ if (pluginProp === null) {
487
+ next = insertJsoncObjectProperty(next, rootEnd, OPENCODE_PLUGIN_CONFIG_KEY, nextPlugins, " ");
488
+ }
489
+ else {
490
+ const shouldAppendPlugin = !hasPluginRegistration && nextPlugins.includes(OPENCODE_PLUGIN_PACKAGE);
491
+ if (shouldAppendPlugin) {
492
+ const arrayEnd = findMatchingJsoncBracket(next, pluginProp.valueStart);
493
+ if (arrayEnd !== -1) {
494
+ next = appendJsoncArrayString(next, arrayEnd, OPENCODE_PLUGIN_PACKAGE, " ");
495
+ }
496
+ }
497
+ }
498
+ rootEnd = findMatchingJsoncBracket(next, rootStart);
499
+ if (rootEnd === -1)
500
+ return next.endsWith("\n") ? next : `${next}\n`;
501
+ const mcpProp = findJsoncObjectProperty(next, rootStart, rootEnd, "mcp");
502
+ if (mcpProp === null) {
503
+ if (mcpEntryPath === null)
504
+ return next.endsWith("\n") ? next : `${next}\n`;
505
+ const mcpEntry = opencodeWebpressoMcpEntry(mcpEntryPath);
506
+ next = insertJsoncObjectProperty(next, rootEnd, "mcp", { [OPENCODE_MCP_SERVER_NAME]: mcpEntry }, " ");
507
+ return next.endsWith("\n") ? next : `${next}\n`;
508
+ }
509
+ const mcpEnd = findMatchingJsoncBracket(next, mcpProp.valueStart);
510
+ if (mcpEnd === -1)
511
+ return next.endsWith("\n") ? next : `${next}\n`;
512
+ for (const name of removedMcpServerNames) {
513
+ const legacyProp = findJsoncObjectProperty(next, mcpProp.valueStart, mcpEnd, name);
514
+ if (legacyProp !== null) {
515
+ next = removeJsoncObjectProperty(next, legacyProp, mcpProp.valueStart);
516
+ }
517
+ }
518
+ const nextMcpEnd = findMatchingJsoncBracket(next, mcpProp.valueStart);
519
+ if (nextMcpEnd === -1)
520
+ return next.endsWith("\n") ? next : `${next}\n`;
521
+ if (mcpEntryPath === null)
522
+ return next.endsWith("\n") ? next : `${next}\n`;
523
+ const mcpEntry = opencodeWebpressoMcpEntry(mcpEntryPath);
524
+ const webpressoProp = findJsoncObjectProperty(next, mcpProp.valueStart, nextMcpEnd, OPENCODE_MCP_SERVER_NAME);
525
+ if (webpressoProp === null) {
526
+ next = insertJsoncObjectProperty(next, nextMcpEnd, OPENCODE_MCP_SERVER_NAME, mcpEntry, " ");
527
+ }
528
+ else {
529
+ const rendered = JSON.stringify(mcpEntry, null, 2)
530
+ .split("\n")
531
+ .map((line, index) => (index === 0 ? line : ` ${line}`))
532
+ .join("\n");
533
+ next = `${next.slice(0, webpressoProp.valueStart)}${rendered}${next.slice(webpressoProp.valueEnd)}`;
534
+ }
535
+ return next.endsWith("\n") ? next : `${next}\n`;
536
+ }
537
+ function defaultOpenCodeConfigPath(repoRoot) {
538
+ const jsonPath = join(repoRoot, "opencode.json");
539
+ if (existsSync(jsonPath))
540
+ return jsonPath;
541
+ const jsoncPath = join(repoRoot, "opencode.jsonc");
542
+ if (existsSync(jsoncPath))
543
+ return jsoncPath;
544
+ return jsonPath;
545
+ }
77
546
  export function ensureOpenCodeWebpressoPlugin(input) {
78
- const configPath = input.configPath ?? join(input.repoRoot, "opencode.json");
547
+ const configPath = input.configPath ?? defaultOpenCodeConfigPath(input.repoRoot);
79
548
  if (input.options.dryRun) {
80
549
  return { kind: "opencode-webpresso-plugin-skipped-dry-run", path: configPath };
81
550
  }
551
+ return withScaffolderFileLock(configPath, () => ensureOpenCodeWebpressoPluginLocked(input, configPath));
552
+ }
553
+ function ensureOpenCodeWebpressoPluginLocked(input, configPath) {
82
554
  const existing = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
555
+ if (hasDuplicateManagedJsoncKeys(existing, configPath)) {
556
+ return { kind: "opencode-webpresso-plugin-invalid-config", path: configPath };
557
+ }
83
558
  let parsed;
84
559
  try {
85
- parsed = existing.trim().length > 0 ? JSON.parse(existing) : {};
560
+ parsed = parseOpenCodeConfig(existing, configPath);
86
561
  }
87
562
  catch {
88
563
  return { kind: "opencode-webpresso-plugin-invalid-json", path: configPath };
@@ -93,15 +568,22 @@ export function ensureOpenCodeWebpressoPlugin(input) {
93
568
  if (parsed.mcp !== undefined && !isJsonRecord(parsed.mcp)) {
94
569
  return { kind: "opencode-webpresso-plugin-invalid-config", path: configPath };
95
570
  }
96
- const staleHookPluginPath = join(input.repoRoot, STALE_OPENCODE_HOOK_PLUGIN_RELATIVE_PATH);
97
- const ownsStaleHookPlugin = isWebpressoOwnedStaleHookPlugin(staleHookPluginPath);
98
- const mcpCleanup = removeOwnedLegacyMcpAliases(parsed.mcp);
99
- const preserved = [
100
- ...(!ownsStaleHookPlugin && existsSync(staleHookPluginPath)
101
- ? [`stale-hook-plugin:${STALE_OPENCODE_HOOK_PLUGIN_RELATIVE_PATH}`]
102
- : []),
103
- ...collectPreservedLegacyMcpAliases(parsed.mcp, true),
104
- ].toSorted();
571
+ const resolution = input.entryPath === undefined ? resolveWebpressoMcpEntry(input.probe) : undefined;
572
+ const mcpEntryPath = input.entryPath ?? resolution?.entryPath ?? null;
573
+ // When we can author the canonical `mcp.webpresso` entry we OWN the alias:
574
+ // drop any prior copy silently (not a "removal") and re-author it below.
575
+ // When no launcher resolves, fall back to GC-only semantics on the prior mcp
576
+ // so a consumer's own entries (including a same-named one) stay untouched.
577
+ const priorMcp = isJsonRecord(parsed.mcp) ? parsed.mcp : {};
578
+ const mcpForCleanup = mcpEntryPath !== null
579
+ ? Object.fromEntries(Object.entries(priorMcp).filter(([name, value]) => name !== OPENCODE_MCP_SERVER_NAME &&
580
+ !isKnownWebpressoOwnedLegacyMcpAlias(name, value)))
581
+ : Object.fromEntries(Object.entries(priorMcp).filter(([name, value]) => !isKnownWebpressoOwnedLegacyMcpAlias(name, value)));
582
+ const removedMcpAliases = Object.entries(priorMcp)
583
+ .filter(([name, value]) => isKnownWebpressoOwnedLegacyMcpAlias(name, value))
584
+ .map(([name]) => `mcp:${name}`)
585
+ .toSorted();
586
+ const preserved = [...collectPreservedLegacyMcpAliases(mcpForCleanup)].toSorted();
105
587
  const pluginValue = parsed[OPENCODE_PLUGIN_CONFIG_KEY];
106
588
  if (pluginValue !== undefined && !isStringArray(pluginValue)) {
107
589
  return { kind: "opencode-webpresso-plugin-invalid-config", path: configPath };
@@ -115,30 +597,43 @@ export function ensureOpenCodeWebpressoPlugin(input) {
115
597
  ...parsed,
116
598
  [OPENCODE_PLUGIN_CONFIG_KEY]: nextPlugins,
117
599
  };
118
- const removed = [
119
- ...mcpCleanup.removed,
120
- ...(ownsStaleHookPlugin
121
- ? [`stale-hook-plugin:${STALE_OPENCODE_HOOK_PLUGIN_RELATIVE_PATH}`]
122
- : []),
123
- ];
124
- const nextMcp = mcpCleanup.nextMcp ?? {};
600
+ const nextMcp = { ...mcpForCleanup };
601
+ if (mcpEntryPath !== null) {
602
+ nextMcp[OPENCODE_MCP_SERVER_NAME] = opencodeWebpressoMcpEntry(mcpEntryPath);
603
+ }
125
604
  nextRoot.mcp = nextMcp;
126
- const next = `${JSON.stringify(nextRoot, null, 2)}\n`;
605
+ const next = renderOpenCodeConfig(existing, configPath, nextRoot, nextPlugins, dedupedExistingPlugins.includes(OPENCODE_PLUGIN_PACKAGE), mcpEntryPath, removedMcpAliases.map((alias) => alias.slice("mcp:".length)));
127
606
  const configChanged = next !== existing;
128
607
  if (configChanged) {
129
- mkdirSync(dirname(configPath), { recursive: true });
130
- writeFileSync(configPath, next, "utf8");
608
+ const mode = existsSync(configPath) ? statSync(configPath).mode & 0o777 : 0o600;
609
+ writeFileAtomicStrict(configPath, next, { encoding: "utf8", mode });
131
610
  }
132
- if (ownsStaleHookPlugin) {
611
+ const staleHookPluginPath = join(input.repoRoot, STALE_OPENCODE_HOOK_PLUGIN_RELATIVE_PATH);
612
+ const staleCleanup = withScaffolderFileLock(staleHookPluginPath, () => {
613
+ if (!existsSync(staleHookPluginPath))
614
+ return { removed: [], preserved: [] };
615
+ if (!isWebpressoOwnedStaleHookPlugin(staleHookPluginPath)) {
616
+ return {
617
+ removed: [],
618
+ preserved: [`stale-hook-plugin:${STALE_OPENCODE_HOOK_PLUGIN_RELATIVE_PATH}`],
619
+ };
620
+ }
133
621
  rmSync(staleHookPluginPath, { force: true });
134
- }
622
+ return {
623
+ removed: [`stale-hook-plugin:${STALE_OPENCODE_HOOK_PLUGIN_RELATIVE_PATH}`],
624
+ preserved: [],
625
+ };
626
+ });
627
+ const removed = [...removedMcpAliases, ...staleCleanup.removed].toSorted();
628
+ const finalPreserved = [...preserved, ...staleCleanup.preserved].toSorted();
135
629
  if (configChanged || removed.length > 0) {
136
630
  return {
137
631
  kind: "opencode-webpresso-plugin-written",
138
632
  path: configPath,
139
633
  pluginPackage: OPENCODE_PLUGIN_PACKAGE,
140
634
  removed: removed.toSorted(),
141
- preserved,
635
+ preserved: finalPreserved,
636
+ mcpEntryPath,
142
637
  };
143
638
  }
144
639
  return {
@@ -146,6 +641,7 @@ export function ensureOpenCodeWebpressoPlugin(input) {
146
641
  path: configPath,
147
642
  pluginPackage: OPENCODE_PLUGIN_PACKAGE,
148
643
  removed: [],
149
- preserved,
644
+ preserved: finalPreserved,
645
+ mcpEntryPath,
150
646
  };
151
647
  }