@bevel-software/platform-core-backend 0.14.0 → 0.15.1

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 (385) hide show
  1. package/THIRD-PARTY-NOTICES.md +6 -6
  2. package/dist/core/catalog-cache-invalidation.d.ts +29 -0
  3. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
  4. package/dist/core/catalog-cache-invalidation.js +71 -0
  5. package/dist/core/catalog-cache-invalidation.js.map +1 -0
  6. package/dist/core/create-core-server.d.ts.map +1 -1
  7. package/dist/core/create-core-server.js +99 -8
  8. package/dist/core/create-core-server.js.map +1 -1
  9. package/dist/core/create-core-services.d.ts +18 -1
  10. package/dist/core/create-core-services.d.ts.map +1 -1
  11. package/dist/core/create-core-services.js +96 -43
  12. package/dist/core/create-core-services.js.map +1 -1
  13. package/dist/modules/access/access-control.interface.d.ts +18 -6
  14. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  15. package/dist/modules/access/access-control.service.d.ts +26 -1
  16. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  17. package/dist/modules/access/access-control.service.js +213 -71
  18. package/dist/modules/access/access-control.service.js.map +1 -1
  19. package/dist/modules/access/access.routes.d.ts.map +1 -1
  20. package/dist/modules/access/access.routes.js +59 -7
  21. package/dist/modules/access/access.routes.js.map +1 -1
  22. package/dist/modules/access-model/access-grammar.d.ts +51 -1
  23. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  24. package/dist/modules/access-model/access-grammar.js +95 -1
  25. package/dist/modules/access-model/access-grammar.js.map +1 -1
  26. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  27. package/dist/modules/access-model/group-files.js +4 -1
  28. package/dist/modules/access-model/group-files.js.map +1 -1
  29. package/dist/modules/access-model/plugin-principals.d.ts +49 -0
  30. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
  31. package/dist/modules/access-model/plugin-principals.js +125 -0
  32. package/dist/modules/access-model/plugin-principals.js.map +1 -0
  33. package/dist/modules/database/core-schema.d.ts +326 -0
  34. package/dist/modules/database/core-schema.d.ts.map +1 -1
  35. package/dist/modules/database/core-schema.js +52 -0
  36. package/dist/modules/database/core-schema.js.map +1 -1
  37. package/dist/modules/kb-sync/index.d.ts +6 -0
  38. package/dist/modules/kb-sync/index.d.ts.map +1 -0
  39. package/dist/modules/kb-sync/index.js +5 -0
  40. package/dist/modules/kb-sync/index.js.map +1 -0
  41. package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
  42. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
  43. package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
  44. package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
  45. package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
  46. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
  47. package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
  48. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
  49. package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
  50. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
  51. package/dist/modules/kb-sync/kb-sync.service.js +183 -0
  52. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
  53. package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
  54. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
  55. package/dist/modules/kb-sync/sync-auth.js +62 -0
  56. package/dist/modules/kb-sync/sync-auth.js.map +1 -0
  57. package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
  58. package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
  59. package/dist/modules/kb-sync/sync-payload.js +105 -0
  60. package/dist/modules/kb-sync/sync-payload.js.map +1 -0
  61. package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
  62. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
  63. package/dist/modules/marketplace/git-http.routes.js +190 -0
  64. package/dist/modules/marketplace/git-http.routes.js.map +1 -0
  65. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
  66. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
  67. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
  68. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
  69. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
  70. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
  71. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
  72. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
  73. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
  74. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
  75. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
  76. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
  77. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
  78. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
  79. package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
  80. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
  81. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
  82. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
  83. package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
  84. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
  85. package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
  86. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
  87. package/dist/modules/marketplace/github-facade/index.js +6 -0
  88. package/dist/modules/marketplace/github-facade/index.js.map +1 -0
  89. package/dist/modules/marketplace/index.d.ts +4 -0
  90. package/dist/modules/marketplace/index.d.ts.map +1 -0
  91. package/dist/modules/marketplace/index.js +4 -0
  92. package/dist/modules/marketplace/index.js.map +1 -0
  93. package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
  94. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
  95. package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
  96. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
  97. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  98. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
  99. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  100. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
  101. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  102. package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
  103. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  104. package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
  105. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
  106. package/dist/modules/mcp/oauth/oauth-state.js +3 -1
  107. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
  108. package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
  109. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
  110. package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
  111. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
  112. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
  113. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
  114. package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
  115. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
  116. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
  117. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
  118. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
  119. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
  120. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
  121. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
  122. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
  123. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
  124. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
  125. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
  126. package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
  127. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
  128. package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
  129. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
  130. package/dist/modules/plugins/discovery/native.source.js +102 -0
  131. package/dist/modules/plugins/discovery/native.source.js.map +1 -0
  132. package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
  133. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
  134. package/dist/modules/plugins/discovery/plugin-source.js +2 -0
  135. package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
  136. package/dist/modules/plugins/index.d.ts +10 -1
  137. package/dist/modules/plugins/index.d.ts.map +1 -1
  138. package/dist/modules/plugins/index.js +8 -0
  139. package/dist/modules/plugins/index.js.map +1 -1
  140. package/dist/modules/plugins/plugin-links.d.ts +66 -0
  141. package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
  142. package/dist/modules/plugins/plugin-links.js +160 -0
  143. package/dist/modules/plugins/plugin-links.js.map +1 -0
  144. package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
  145. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
  146. package/dist/modules/plugins/plugin-links.service.js +304 -0
  147. package/dist/modules/plugins/plugin-links.service.js.map +1 -0
  148. package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
  149. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  150. package/dist/modules/plugins/plugin-provision.service.js +152 -52
  151. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  152. package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
  153. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
  154. package/dist/modules/plugins/plugin-rename.service.js +245 -0
  155. package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
  156. package/dist/modules/plugins/plugins.contract.d.ts +27 -1
  157. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  158. package/dist/modules/plugins/plugins.routes.d.ts +7 -1
  159. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  160. package/dist/modules/plugins/plugins.routes.js +120 -12
  161. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  162. package/dist/modules/plugins/plugins.service.d.ts +50 -3
  163. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  164. package/dist/modules/plugins/plugins.service.js +108 -47
  165. package/dist/modules/plugins/plugins.service.js.map +1 -1
  166. package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
  167. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  168. package/dist/modules/settings/deployment-settings.service.js +82 -2
  169. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  170. package/dist/modules/settings/setup.routes.d.ts +26 -0
  171. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  172. package/dist/modules/settings/setup.routes.js +9 -1
  173. package/dist/modules/settings/setup.routes.js.map +1 -1
  174. package/dist/modules/skills/index.d.ts +1 -0
  175. package/dist/modules/skills/index.d.ts.map +1 -1
  176. package/dist/modules/skills/index.js +1 -0
  177. package/dist/modules/skills/index.js.map +1 -1
  178. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  179. package/dist/modules/skills/pending-skills.service.js +9 -4
  180. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  181. package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
  182. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
  183. package/dist/modules/skills/skill-access-requests.routes.js +168 -0
  184. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
  185. package/dist/modules/skills/skills.contract.d.ts +31 -0
  186. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  187. package/dist/modules/skills/skills.routes.d.ts +28 -3
  188. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  189. package/dist/modules/skills/skills.routes.js +42 -3
  190. package/dist/modules/skills/skills.routes.js.map +1 -1
  191. package/dist/modules/skills/skills.service.d.ts +4 -0
  192. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  193. package/dist/modules/skills/skills.service.js +83 -22
  194. package/dist/modules/skills/skills.service.js.map +1 -1
  195. package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
  196. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  197. package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
  198. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  199. package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
  200. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  201. package/dist/modules/tool-auth/external-api-key.service.js +26 -9
  202. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  203. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
  204. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  205. package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -92
  206. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  207. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +6 -1
  208. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  209. package/dist/modules/tool-manuals/tool-manuals.service.js +18 -25
  210. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  211. package/dist/modules/workflow/git/git.service.d.ts +40 -2
  212. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  213. package/dist/modules/workflow/git/git.service.js +227 -54
  214. package/dist/modules/workflow/git/git.service.js.map +1 -1
  215. package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
  216. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  217. package/dist/modules/workflow/git/pull-request.service.js +8 -0
  218. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  219. package/dist/modules/workflow/workflow.service.d.ts +52 -1
  220. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  221. package/dist/modules/workflow/workflow.service.js +242 -9
  222. package/dist/modules/workflow/workflow.service.js.map +1 -1
  223. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
  224. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
  225. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
  226. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
  227. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  228. package/dist/modules/workspace/startup/steps/seed-tree.js +46 -2
  229. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  230. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  231. package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
  232. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  233. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  234. package/dist/modules/workspace/workspace.routes.js +9 -9
  235. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  236. package/dist/modules/workspace/workspace.service.d.ts +33 -0
  237. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  238. package/dist/modules/workspace/workspace.service.js +110 -16
  239. package/dist/modules/workspace/workspace.service.js.map +1 -1
  240. package/dist/shared/domain-errors.d.ts +20 -0
  241. package/dist/shared/domain-errors.d.ts.map +1 -1
  242. package/dist/shared/domain-errors.js +29 -0
  243. package/dist/shared/domain-errors.js.map +1 -1
  244. package/dist/shared/fs-errors.d.ts +8 -0
  245. package/dist/shared/fs-errors.d.ts.map +1 -0
  246. package/dist/shared/fs-errors.js +11 -0
  247. package/dist/shared/fs-errors.js.map +1 -0
  248. package/dist/shared/fs-walk.d.ts +20 -6
  249. package/dist/shared/fs-walk.d.ts.map +1 -1
  250. package/dist/shared/fs-walk.js +35 -27
  251. package/dist/shared/fs-walk.js.map +1 -1
  252. package/dist/shared/kb-walk.d.ts +52 -0
  253. package/dist/shared/kb-walk.d.ts.map +1 -0
  254. package/dist/shared/kb-walk.js +76 -0
  255. package/dist/shared/kb-walk.js.map +1 -0
  256. package/dist/shared/path-order.d.ts +18 -0
  257. package/dist/shared/path-order.d.ts.map +1 -0
  258. package/dist/shared/path-order.js +35 -0
  259. package/dist/shared/path-order.js.map +1 -0
  260. package/dist/shared/ttl-cache.d.ts +28 -1
  261. package/dist/shared/ttl-cache.d.ts.map +1 -1
  262. package/dist/shared/ttl-cache.js +33 -1
  263. package/dist/shared/ttl-cache.js.map +1 -1
  264. package/kb-template/.bevelignore +1 -1
  265. package/kb-template/AGENTS.md +95 -30
  266. package/kb-template/Skills/.gitkeep +0 -0
  267. package/migrations/0005_github_facade.sql +25 -0
  268. package/migrations/meta/0005_snapshot.json +1717 -0
  269. package/migrations/meta/_journal.json +7 -0
  270. package/package.json +6 -6
  271. package/src/__tests__/kb-layout-config.test.ts +110 -0
  272. package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
  273. package/src/core/catalog-cache-invalidation.ts +92 -0
  274. package/src/core/create-core-server.ts +123 -7
  275. package/src/core/create-core-services.ts +162 -41
  276. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
  277. package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
  278. package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
  279. package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
  280. package/src/modules/access/access-control.interface.ts +20 -9
  281. package/src/modules/access/access-control.service.ts +240 -78
  282. package/src/modules/access/access.routes.ts +96 -14
  283. package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
  284. package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
  285. package/src/modules/access-model/access-grammar.ts +114 -2
  286. package/src/modules/access-model/group-files.ts +4 -0
  287. package/src/modules/access-model/plugin-principals.ts +137 -0
  288. package/src/modules/database/core-schema.ts +54 -0
  289. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
  290. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
  291. package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
  292. package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
  293. package/src/modules/kb-sync/index.ts +19 -0
  294. package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
  295. package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
  296. package/src/modules/kb-sync/kb-sync.service.ts +201 -0
  297. package/src/modules/kb-sync/sync-auth.ts +118 -0
  298. package/src/modules/kb-sync/sync-payload.ts +143 -0
  299. package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
  300. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
  301. package/src/modules/marketplace/git-http.routes.ts +193 -0
  302. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
  303. package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
  304. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
  305. package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
  306. package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
  307. package/src/modules/marketplace/github-facade/index.ts +25 -0
  308. package/src/modules/marketplace/index.ts +3 -0
  309. package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
  310. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
  311. package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
  312. package/src/modules/mcp/oauth/oauth-state.ts +15 -3
  313. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
  314. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
  315. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
  316. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
  317. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
  318. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
  319. package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
  320. package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
  321. package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
  322. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
  323. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
  324. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
  325. package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
  326. package/src/modules/plugins/discovery/native.source.ts +122 -0
  327. package/src/modules/plugins/discovery/plugin-source.ts +99 -0
  328. package/src/modules/plugins/index.ts +10 -0
  329. package/src/modules/plugins/plugin-links.service.ts +363 -0
  330. package/src/modules/plugins/plugin-links.ts +184 -0
  331. package/src/modules/plugins/plugin-provision.service.ts +160 -54
  332. package/src/modules/plugins/plugin-rename.service.ts +298 -0
  333. package/src/modules/plugins/plugins.contract.ts +28 -1
  334. package/src/modules/plugins/plugins.routes.ts +128 -11
  335. package/src/modules/plugins/plugins.service.ts +106 -53
  336. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
  337. package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
  338. package/src/modules/settings/deployment-settings.service.ts +95 -2
  339. package/src/modules/settings/setup.routes.ts +24 -0
  340. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
  341. package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
  342. package/src/modules/skills/index.ts +1 -0
  343. package/src/modules/skills/pending-skills.service.ts +7 -5
  344. package/src/modules/skills/skill-access-requests.routes.ts +199 -0
  345. package/src/modules/skills/skills.contract.ts +32 -0
  346. package/src/modules/skills/skills.routes.ts +59 -3
  347. package/src/modules/skills/skills.service.ts +84 -22
  348. package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
  349. package/src/modules/tool-auth/external-api-key.service.ts +31 -13
  350. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +24 -1
  351. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
  352. package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -88
  353. package/src/modules/tool-manuals/tool-manuals.service.ts +14 -23
  354. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
  355. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
  356. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +2 -2
  357. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
  358. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
  359. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
  360. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
  361. package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
  362. package/src/modules/workflow/git/git.service.ts +239 -56
  363. package/src/modules/workflow/git/pull-request.service.ts +9 -0
  364. package/src/modules/workflow/workflow.service.ts +260 -9
  365. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
  366. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
  367. package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
  368. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
  369. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
  370. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
  371. package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
  372. package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
  373. package/src/modules/workspace/workspace.routes.ts +9 -17
  374. package/src/modules/workspace/workspace.service.ts +107 -16
  375. package/src/shared/__tests__/filename.test.ts +0 -0
  376. package/src/shared/__tests__/fs-walk.test.ts +22 -1
  377. package/src/shared/__tests__/kb-walk.test.ts +107 -0
  378. package/src/shared/__tests__/path-order.test.ts +24 -0
  379. package/src/shared/__tests__/ttl-cache.test.ts +55 -0
  380. package/src/shared/domain-errors.ts +29 -0
  381. package/src/shared/fs-errors.ts +10 -0
  382. package/src/shared/fs-walk.ts +38 -23
  383. package/src/shared/kb-walk.ts +95 -0
  384. package/src/shared/path-order.ts +33 -0
  385. package/src/shared/ttl-cache.ts +33 -1
@@ -0,0 +1,294 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { DEFAULT_BRANCH } from '@bevel-software/platform-shared';
6
+
7
+ import type { WorkspaceService } from '../../workspace/workspace.service.js';
8
+ import { AccessControlService } from '../../access/access-control.service.js';
9
+ import { SkillService } from '../../skills/skills.service.js';
10
+ import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
11
+ import { PluginLinkIndex } from '../plugin-links.js';
12
+ import { MarketplaceCompilerService } from '../compile/marketplace-compiler.service.js';
13
+
14
+ /**
15
+ * Source in, distribution out — for one caller. The real resolver decides
16
+ * what is readable, the real catalog and link index say what is where; the
17
+ * assertions are on the produced tree.
18
+ */
19
+
20
+ const KB_DIR = 'knowledge-base';
21
+ const wsId = workspaceIdForBranch(DEFAULT_BRANCH);
22
+
23
+ describe('compileMarketplace', () => {
24
+ let root: string;
25
+ let repo: string;
26
+ let compiler: MarketplaceCompilerService;
27
+
28
+ const write = async (rel: string, text: string) => {
29
+ const abs = path.join(repo, rel);
30
+ await fs.mkdir(path.dirname(abs), { recursive: true });
31
+ await fs.writeFile(abs, text);
32
+ };
33
+
34
+ beforeEach(async () => {
35
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-compile-'));
36
+ repo = path.join(root, wsId, KB_DIR);
37
+ const workspaceService = {
38
+ getOrCreateForBranch: async () => ({ id: wsId }),
39
+ getWorkspacePath: async (id: string) => path.join(root, id),
40
+ readFile: async (id: string, rel: string) => fs.readFile(path.join(root, id, rel), 'utf-8'),
41
+ ensureRemotesFetched: async () => undefined,
42
+ } as unknown as WorkspaceService;
43
+
44
+ await write('roles.yaml', 'roles:\n Admin:\n - admin@x.io\n');
45
+ await write('access.md', '---\nwrite:\n - Admin\n---\n');
46
+ // GTM: Sam is a member. One inline skill, one MCP server with a vault header.
47
+ await write(
48
+ 'Plugins/GTM/access.md',
49
+ '---\nread:\n - everyone\n---\nread:\n - Sam <sam@x.io>\nwrite:\n - Mia <mia@x.io>\n',
50
+ );
51
+ await write(
52
+ 'Plugins/GTM/plugin.json',
53
+ JSON.stringify({
54
+ name: 'gtm',
55
+ version: '2.1.0',
56
+ description: 'Go to market',
57
+ extensions: { 'software.bevel.hexis': { skills: ['Skills/Eng/deploy'] } },
58
+ }),
59
+ );
60
+ await write('Plugins/GTM/skills/outreach/SKILL.md', '---\ndescription: Reach out.\n---\nBody\n');
61
+ await write('Plugins/GTM/skills/outreach/scripts/run.py', 'print(1)\n');
62
+ await write('Plugins/GTM/software.bevel.hexis/tools/web.tool', 'not shipped');
63
+ await write(
64
+ 'Plugins/GTM/mcp.json',
65
+ JSON.stringify({
66
+ mcpServers: {
67
+ notion: { type: 'streamable-http', url: 'https://mcp.notion.com', headers: { Authorization: 'Bearer ${NOTION_TOKEN}', 'X-Client': 'hexis' } },
68
+ local: { type: 'stdio', command: 'npx', args: ['-y', 'x'] },
69
+ },
70
+ }),
71
+ );
72
+ // Shared skills: Eng/deploy linked into GTM and readable by its members;
73
+ // Eng/secret readable by nobody but admins; Sales/pitch public.
74
+ await write('Skills/Eng/deploy/access.md', '---\n---\nread:\n - plugin/GTM/read\n');
75
+ await write('Skills/Eng/deploy/SKILL.md', '---\ndescription: Ship it.\n---\n');
76
+ await write('Skills/Eng/deploy/references/notes.md', 'notes');
77
+ await write('Skills/Eng/secret/SKILL.md', '---\ndescription: Hush.\n---\n');
78
+ await write('Skills/Sales/access.md', '---\n---\nread:\n - everyone\n');
79
+ await write('Skills/Sales/pitch/SKILL.md', '---\ndescription: Pitch.\n---\n');
80
+
81
+ const access = new AccessControlService(workspaceService, KB_DIR);
82
+ const skills = new SkillService(workspaceService, access, KB_DIR);
83
+ const links = new PluginLinkIndex(workspaceService, skills, access, KB_DIR);
84
+ compiler = new MarketplaceCompilerService(workspaceService, access, skills, links, KB_DIR, {
85
+ name: 'acme-hexis',
86
+ owner: 'Acme',
87
+ knowledgeBaseMcp: { name: 'hexis', url: 'https://kb.acme.com/api/mcp' },
88
+ });
89
+ });
90
+ afterEach(() => fs.rm(root, { recursive: true, force: true }));
91
+
92
+ const text = (tree: { files: Map<string, Buffer> }, rel: string) => tree.files.get(rel)?.toString('utf-8');
93
+ const json = (tree: { files: Map<string, Buffer> }, rel: string) => JSON.parse(text(tree, rel) ?? 'null');
94
+
95
+ it('compiles exactly what the caller may read, in the layouts the clients want', async () => {
96
+ const tree = await compiler.compileFor({ userEmail: 'sam@x.io' });
97
+ const paths = [...tree.files.keys()].sort();
98
+
99
+ // GTM: inline + linked skills copied in, tools and access files left out.
100
+ expect(paths).toContain('plugins/gtm/skills/outreach/SKILL.md');
101
+ expect(paths).toContain('plugins/gtm/skills/outreach/scripts/run.py');
102
+ expect(paths).toContain('plugins/gtm/skills/deploy/SKILL.md');
103
+ expect(paths).toContain('plugins/gtm/skills/deploy/references/notes.md');
104
+ expect(paths.some((p) => p.includes('.tool') || p.endsWith('access.md'))).toBe(false);
105
+ // The unreadable skill is nowhere — not in a plugin, not in the flat copy.
106
+ expect(paths.some((p) => p.includes('secret'))).toBe(false);
107
+
108
+ // Three manifests per plugin, with the source manifest's identity.
109
+ expect(json(tree, 'plugins/gtm/plugin.json')).toMatchObject({ name: 'gtm', version: '2.1.0', description: 'Go to market' });
110
+ expect(json(tree, 'plugins/gtm/.claude-plugin/plugin.json')).toMatchObject({ name: 'gtm', version: '2.1.0' });
111
+ expect(json(tree, 'plugins/gtm/.codex-plugin/plugin.json')).toMatchObject({ name: 'gtm' });
112
+
113
+ // The portable half of mcp.json: the vault header is gone, the literal stays.
114
+ const mcp = json(tree, 'plugins/gtm/.mcp.json');
115
+ expect(mcp.mcpServers.notion).toEqual({ type: 'streamable-http', url: 'https://mcp.notion.com', headers: { 'X-Client': 'hexis' } });
116
+ expect(mcp.mcpServers.local).toEqual({ type: 'stdio', command: 'npx', args: ['-y', 'x'] });
117
+
118
+ // The leftovers plugin holds what no real plugin ships (pitch), never what
119
+ // one does (deploy is GTM's); the flat copy carries everything once.
120
+ expect(paths).toContain('plugins/skills-and-knowledge/skills/pitch/SKILL.md');
121
+ expect(paths).not.toContain('plugins/skills-and-knowledge/skills/deploy/SKILL.md');
122
+ // …and the knowledge base itself as an MCP server, URL only.
123
+ expect(json(tree, 'plugins/skills-and-knowledge/.mcp.json')).toEqual({
124
+ mcpServers: { hexis: { type: 'streamable-http', url: 'https://kb.acme.com/api/mcp' } },
125
+ });
126
+ expect(paths).toContain('skills/deploy/SKILL.md');
127
+ expect(paths).toContain('skills/pitch/SKILL.md');
128
+ expect(paths).toContain('skills/outreach/SKILL.md');
129
+
130
+ // The bundle depends on every other plugin; the catalogues list them all.
131
+ expect(json(tree, 'plugins/hexis-all/.claude-plugin/plugin.json').dependencies.sort()).toEqual(['gtm', 'skills-and-knowledge']);
132
+ const claude = json(tree, '.claude-plugin/marketplace.json');
133
+ expect(claude.name).toBe('acme-hexis');
134
+ expect(claude.plugins.map((p: { name: string; source: string }) => [p.name, p.source])).toEqual([
135
+ ['gtm', './plugins/gtm'],
136
+ ['skills-and-knowledge', './plugins/skills-and-knowledge'],
137
+ ['hexis-all', './plugins/hexis-all'],
138
+ ]);
139
+ const codex = json(tree, '.agents/plugins/marketplace.json');
140
+ expect(codex.plugins.every((p: { policy: { installation: string } }) => p.policy.installation === 'INSTALLED_BY_DEFAULT')).toBe(true);
141
+ expect(codex.plugins[0].source).toEqual({ source: 'local', path: './plugins/gtm' });
142
+ expect(text(tree, 'README.md')).toContain('Compiled from Acme');
143
+ expect(tree.warnings).toEqual([]);
144
+ });
145
+
146
+ it('ships only mcp servers a client could run, and names each one it leaves out', async () => {
147
+ // Written as text, not from an object literal: a literal `__proto__` key
148
+ // sets the prototype instead of an own property, and the point is a
149
+ // repository file that CARRIES that key.
150
+ await write(
151
+ 'Plugins/GTM/mcp.json',
152
+ `{"mcpServers": {
153
+ "notion": { "type": "streamable-http", "url": "https://mcp.notion.com" },
154
+ "events": { "type": "sse", "url": "https://events.example" },
155
+ "Bad Name": { "type": "stdio", "command": "x" },
156
+ "empty": { "type": "stdio" },
157
+ "creds": { "type": "streamable-http", "url": "https://user:pw@mcp.example" },
158
+ "badenv": { "type": "stdio", "command": "x", "env": { "A": 1 } },
159
+ "badargs": { "type": "stdio", "command": "x", "args": [1, "b"] },
160
+ "badheaders": { "type": "streamable-http", "url": "https://x.example", "headers": { "A": 1 } },
161
+ "__proto__": { "type": "stdio", "command": "x" }
162
+ }}`,
163
+ );
164
+ const tree = await compiler.compileFor({ userEmail: 'sam@x.io' });
165
+ const mcp = json(tree, 'plugins/gtm/mcp.json');
166
+ expect(Object.keys(mcp.mcpServers)).toEqual(['notion']);
167
+ const left = (name: string) => tree.warnings.find((w) => w.includes(`"${name}" not shipped`)) ?? '';
168
+ expect(left('events')).toContain('sse');
169
+ expect(left('Bad Name')).toContain('name');
170
+ expect(left('empty')).toContain('no command');
171
+ expect(left('creds')).toContain('credentials');
172
+ expect(left('badenv')).toContain('env');
173
+ expect(left('badargs')).toContain('args');
174
+ expect(left('badheaders')).toContain('headers');
175
+ expect(left('__proto__')).toContain('name');
176
+ });
177
+
178
+ it('the manifest name is the identity: a folder called something else publishes, and is granted, under the name', async () => {
179
+ await write(
180
+ 'Plugins/GTM/plugin.json',
181
+ JSON.stringify({ name: 'go-to-market', version: '2.1.0', extensions: { 'software.bevel.hexis': { skills: ['Skills/Eng/deploy'] } } }),
182
+ );
183
+ // The linked skill's grant spelled by the name reaches Sam…
184
+ await write('Skills/Eng/deploy/access.md', '---\n---\nread:\n - plugin/go-to-market/read\n');
185
+ const tree = await compiler.compileFor({ userEmail: 'sam@x.io' });
186
+ expect(tree.plugins).toContain('go-to-market');
187
+ expect(tree.plugins).not.toContain('gtm');
188
+ expect([...tree.files.keys()]).toContain('plugins/go-to-market/skills/deploy/SKILL.md');
189
+ // …and a name that differs from the folder is nothing to warn about.
190
+ expect(tree.warnings).toEqual([]);
191
+ });
192
+
193
+ it('a grant spelled by the folder names no plugin once the manifest says otherwise', async () => {
194
+ await write('Plugins/GTM/plugin.json', JSON.stringify({ name: 'go-to-market', extensions: { 'software.bevel.hexis': { skills: ['Skills/Eng/deploy'] } } }));
195
+ // The fixture's grant on Skills/Eng/deploy still says plugin/GTM/read.
196
+ const tree = await compiler.compileFor({ userEmail: 'sam@x.io' });
197
+ expect([...tree.files.keys()].some((p) => p.includes('deploy'))).toBe(false);
198
+ });
199
+
200
+ it('for a stranger the plugin keeps only its MCP servers, and the public scope is all that ships', async () => {
201
+ const tree = await compiler.compileFor({ userEmail: 'nobody@elsewhere.io' });
202
+ const paths = [...tree.files.keys()].sort();
203
+ expect(paths.some((p) => p.startsWith('plugins/gtm/skills/'))).toBe(false);
204
+ expect(paths).toContain('plugins/gtm/.mcp.json'); // portable servers are not access-gated content
205
+ expect(paths).toContain('plugins/skills-and-knowledge/skills/pitch/SKILL.md');
206
+ expect(paths.some((p) => p.includes('deploy'))).toBe(false);
207
+ expect(tree.plugins).toEqual(['gtm', 'skills-and-knowledge', 'hexis-all']);
208
+ });
209
+
210
+ it('the everyone audience carries the public subset only', async () => {
211
+ const tree = await compiler.compileFor({ everyone: true });
212
+ const paths = [...tree.files.keys()];
213
+ expect(paths).toContain('skills/pitch/SKILL.md');
214
+ expect(paths.some((p) => p.includes('deploy') || p.includes('outreach') || p.includes('secret'))).toBe(false);
215
+ });
216
+
217
+ it('with the endpoint configured, the skills plugin exists even for a caller with no leftover skill', async () => {
218
+ // Make the one public skill part of GTM, so nothing is left over.
219
+ await write(
220
+ 'Plugins/GTM/plugin.json',
221
+ JSON.stringify({ name: 'gtm', extensions: { 'software.bevel.hexis': { skills: ['Skills/Eng/deploy', 'Skills/Sales'] } } }),
222
+ );
223
+ const tree = await compiler.compileFor({ userEmail: 'sam@x.io' });
224
+ const paths = [...tree.files.keys()];
225
+ expect(paths.some((p) => p.startsWith('plugins/skills-and-knowledge/skills/'))).toBe(false);
226
+ expect(paths).toContain('plugins/skills-and-knowledge/.mcp.json');
227
+ expect(tree.plugins).toEqual(['gtm', 'skills-and-knowledge', 'hexis-all']);
228
+ });
229
+
230
+ it('a manifest name that is no identifier is ignored, with a warning — the folder names the plugin', async () => {
231
+ // A hostile or mistaken manifest name must never become a path segment
232
+ // that escapes the compiled tree, nor an identity grants could spell.
233
+ await write('Plugins/GTM/plugin.json', JSON.stringify({ name: '../../etc/passwd' }));
234
+ const tree = await compiler.compileFor({ userEmail: 'sam@x.io' });
235
+ const paths = [...tree.files.keys()];
236
+ expect(paths.every((p) => !p.includes('..') && !p.startsWith('/'))).toBe(true);
237
+ expect(paths).toContain('plugins/gtm/skills/outreach/SKILL.md');
238
+ expect(tree.warnings.some((w) => w.includes('Plugins/GTM') && w.includes('not a plugin identifier'))).toBe(true);
239
+ });
240
+
241
+ it('two folders claiming one identity: the first by path keeps it, the second is skipped and named', async () => {
242
+ await write('Plugins/Twin/plugin.json', JSON.stringify({ name: 'gtm' }));
243
+ await write('Plugins/Twin/access.md', '---\nread:\n - everyone\n---\nread:\n - Sam <sam@x.io>\n');
244
+ await write('Plugins/Twin/skills/twin-skill/SKILL.md', '---\ndescription: t\n---\n');
245
+
246
+ const tree = await compiler.compileFor({ userEmail: 'sam@x.io' });
247
+ const paths = [...tree.files.keys()];
248
+ expect(paths).toContain('plugins/gtm/skills/outreach/SKILL.md');
249
+ expect(paths).not.toContain('plugins/gtm/skills/twin-skill/SKILL.md');
250
+ expect(tree.warnings.some((w) => w.includes('Plugins/Twin') && w.includes('already used'))).toBe(true);
251
+ });
252
+
253
+ it('a compile reads the checkout as it is now, not the catalogs a moment ago', async () => {
254
+ // Warm every cache: the skill catalog, the link index, the access model.
255
+ const before = await compiler.compileFor({ userEmail: 'sam@x.io' });
256
+ expect([...before.files.keys()]).not.toContain('skills/late/SKILL.md');
257
+ expect([...before.files.keys()].some((p) => p.includes('secret'))).toBe(false);
258
+
259
+ // Then the knowledge base moves under them, the way an MCP `write_files`
260
+ // moves it: a new public skill, linked into GTM by its manifest, and a
261
+ // grant that opens the secret skill — all within every cache's TTL.
262
+ await write('Skills/Sales/late/SKILL.md', '---\ndescription: Just landed.\n---\n');
263
+ await write(
264
+ 'Plugins/GTM/plugin.json',
265
+ JSON.stringify({
266
+ name: 'gtm',
267
+ version: '2.1.0',
268
+ description: 'Go to market',
269
+ extensions: { 'software.bevel.hexis': { skills: ['Skills/Eng/deploy', 'Skills/Sales/late'] } },
270
+ }),
271
+ );
272
+ await write('Skills/Eng/secret/access.md', '---\n---\nread:\n - everyone\n');
273
+
274
+ const after = await compiler.compileFor({ userEmail: 'sam@x.io' });
275
+ const paths = [...after.files.keys()];
276
+ expect(paths).toContain('skills/late/SKILL.md');
277
+ expect(paths).toContain('plugins/gtm/skills/late/SKILL.md');
278
+ expect(paths).toContain('skills/secret/SKILL.md');
279
+ });
280
+
281
+ it('a name clash inside one plugin keeps the first by path and says so', async () => {
282
+ // A second `deploy` skill, linked from the same plugin via a folder root.
283
+ await write('Skills/Ops/deploy/SKILL.md', '---\ndescription: Ops deploy.\n---\n');
284
+ await write('Skills/Ops/access.md', '---\n---\nread:\n - plugin/GTM/read\n');
285
+ await write(
286
+ 'Plugins/GTM/plugin.json',
287
+ JSON.stringify({ name: 'gtm', extensions: { 'software.bevel.hexis': { skills: ['Skills/Eng/deploy', 'Skills/Ops'] } } }),
288
+ );
289
+ // Catalog-level dedup already keeps ONE `deploy` (Skills/Eng wins by path),
290
+ // so the compiled plugin has one and the tree stays coherent.
291
+ const tree = await compiler.compileFor({ userEmail: 'sam@x.io' });
292
+ expect(text(tree, 'plugins/gtm/skills/deploy/SKILL.md')).toContain('Ship it.');
293
+ });
294
+ });
@@ -4,6 +4,7 @@ import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { DEFAULT_BRANCH } from '@bevel-software/platform-shared';
6
6
  import { PluginIndexService } from '../plugins.service.js';
7
+ import type { PluginLinkIndex } from '../plugin-links.js';
7
8
  import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
8
9
  import type { WorkspaceService } from '../../workspace/workspace.service.js';
9
10
  import type { IAccessControl } from '../../access/access-control.interface.js';
@@ -62,9 +63,14 @@ describe('PluginIndexService', () => {
62
63
 
63
64
  const kb = () => join(root, wsId, KB_DIR);
64
65
 
65
- /** A REAL plugin: a folder carrying the access.md that makes it exist. */
66
+ /**
67
+ * A REAL plugin: a folder carrying the manifest discovery reads AND the
68
+ * access.md that makes it exist to the index (a legacy folder without a
69
+ * manifest gets one from the boot step before the index ever runs).
70
+ */
66
71
  const pluginDir = async (name: string) => {
67
72
  await mkdir(join(kb(), 'Plugins', name), { recursive: true });
73
+ await writeFile(join(kb(), 'Plugins', name, 'plugin.json'), `{"name":"${name.toLowerCase()}"}`);
68
74
  await writeFile(join(kb(), 'Plugins', name, 'access.md'), '---\nread:\n - everyone\n---\n');
69
75
  };
70
76
 
@@ -79,8 +85,9 @@ describe('PluginIndexService', () => {
79
85
  await pluginDir('Engineering');
80
86
 
81
87
  const catalog = await svc().catalog();
82
- expect(catalog.map((g) => g.name)).toEqual(['Engineering', 'GTM']);
83
- expect(catalog[1].folders).toEqual(['Plugins/GTM']);
88
+ // The manifest name is the identity; the folder is what people see.
89
+ expect(catalog.map((g) => g.name)).toEqual(['engineering', 'gtm']);
90
+ expect(catalog[1]).toMatchObject({ displayName: 'GTM', folders: ['Plugins/GTM'] });
84
91
  });
85
92
 
86
93
  test('a folder without an access.md is not a plugin', async () => {
@@ -90,10 +97,14 @@ describe('PluginIndexService', () => {
90
97
  await mkdir(join(kb(), 'Plugins', 'Ghost'), { recursive: true });
91
98
  await mkdir(join(kb(), 'Plugins', 'Residue'), { recursive: true });
92
99
  await writeFile(join(kb(), 'Plugins', 'Residue', 'notes.md'), 'leftover');
100
+ // A manifest alone is a plugin to discovery but not to the index: the
101
+ // access.md is what a deleted plugin's residue lacks.
102
+ await mkdir(join(kb(), 'Plugins', 'Manifest-only'), { recursive: true });
103
+ await writeFile(join(kb(), 'Plugins', 'Manifest-only', 'plugin.json'), '{"name":"manifest-only"}');
93
104
  await pluginDir('Real');
94
105
 
95
106
  const catalog = await svc().catalog();
96
- expect(catalog.map((g) => g.name)).toEqual(['Real']);
107
+ expect(catalog.map((g) => g.name)).toEqual(['real']);
97
108
  });
98
109
 
99
110
  test('the retired Skills/ and Tools/ roots are NOT plugin roots', async () => {
@@ -102,7 +113,7 @@ describe('PluginIndexService', () => {
102
113
  await pluginDir('Engineering');
103
114
 
104
115
  const catalog = await svc().catalog();
105
- expect(catalog.map((g) => g.name)).toEqual(['Engineering']);
116
+ expect(catalog.map((g) => g.name)).toEqual(['engineering']);
106
117
  });
107
118
 
108
119
  test('ignores loose files and dot-dirs under the plugin root', async () => {
@@ -113,7 +124,7 @@ describe('PluginIndexService', () => {
113
124
  await writeFile(join(kb(), 'Plugins', 'slack.tool'), '{}');
114
125
 
115
126
  const catalog = await svc().catalog();
116
- expect(catalog.map((g) => g.name)).toEqual(['GTM']);
127
+ expect(catalog.map((g) => g.name)).toEqual(['gtm']);
117
128
  });
118
129
 
119
130
  test('counts skills and tools from the global catalogs by pluginOfPath', async () => {
@@ -126,14 +137,47 @@ describe('PluginIndexService', () => {
126
137
  tools: tools('Plugins/GTM/heyreach.tool', 'Plugins/slack.tool'),
127
138
  }).catalog();
128
139
 
129
- const gtm = catalog.find((g) => g.name === 'GTM')!;
140
+ const gtm = catalog.find((g) => g.name === 'gtm')!;
130
141
  expect(gtm.skillCount).toBe(2);
131
142
  expect(gtm.toolCount).toBe(1);
132
- const product = catalog.find((g) => g.name === 'Product')!;
143
+ const product = catalog.find((g) => g.name === 'product')!;
133
144
  expect(product.skillCount).toBe(1);
134
145
  expect(product.toolCount).toBe(0);
135
146
  });
136
147
 
148
+ test("counts the linked skills a plugin's members cannot read, from the unfiltered link index", async () => {
149
+ await pluginDir('GTM');
150
+ await pluginDir('Product');
151
+ // The link index's view: GTM links two shared skills, one of which lost
152
+ // its grant; Product's inline skill is granted by definition.
153
+ const membership = vi.fn(async () => ({
154
+ bySkill: new Map([
155
+ ['Skills/Eng/deploy', [{ name: 'gtm', linked: true, granted: false }]],
156
+ ['Skills/Eng/rollback', [{ name: 'gtm', linked: true, granted: true }]],
157
+ ['Plugins/Product/roadmap', [{ name: 'product', linked: false, granted: true }]],
158
+ ]),
159
+ byPlugin: new Map(),
160
+ }));
161
+ const links = { membership } as unknown as PluginLinkIndex;
162
+ const catalog = await new PluginIndexService(
163
+ workspaceService,
164
+ principals,
165
+ skillService(skills('Skills/Eng/deploy', 'Skills/Eng/rollback', 'Plugins/Product/roadmap')),
166
+ toolService(),
167
+ KB_DIR,
168
+ Date.now,
169
+ links,
170
+ ).catalog();
171
+
172
+ expect(catalog.find((g) => g.name === 'gtm')).toMatchObject({ skillCount: 2, brokenLinks: 1 });
173
+ expect(catalog.find((g) => g.name === 'product')).toMatchObject({ skillCount: 1, brokenLinks: 0 });
174
+ // Both counts from ONE read of the index: its cache has no single-flight,
175
+ // so two concurrent reads of a cold catalog would build the tree twice.
176
+ expect(membership).toHaveBeenCalledTimes(1);
177
+ // Without a link index there are no links to be broken.
178
+ expect((await svc().catalog()).every((g) => g.brokenLinks === 0)).toBe(true);
179
+ });
180
+
137
181
  test('resolves principals on the plugin folder', async () => {
138
182
  await pluginDir('GTM');
139
183
 
@@ -190,7 +234,7 @@ describe('PluginIndexService', () => {
190
234
  await expect(service.catalog()).resolves.toEqual([]);
191
235
 
192
236
  fail = false;
193
- expect((await service.catalog()).map((g) => g.name)).toEqual(['GTM']);
237
+ expect((await service.catalog()).map((g) => g.name)).toEqual(['gtm']);
194
238
  warn.mockRestore();
195
239
  });
196
240
 
@@ -214,12 +258,12 @@ describe('PluginIndexService', () => {
214
258
  await pluginDir('GTM');
215
259
  const service = svc();
216
260
 
217
- expect((await service.catalog()).map((g) => g.name)).toEqual(['GTM']);
261
+ expect((await service.catalog()).map((g) => g.name)).toEqual(['gtm']);
218
262
  // A plugin added out of band is NOT seen while the cache holds…
219
263
  await pluginDir('Finance');
220
- expect((await service.catalog()).map((g) => g.name)).toEqual(['GTM']);
264
+ expect((await service.catalog()).map((g) => g.name)).toEqual(['gtm']);
221
265
  // …and IS seen once the file-change subscriber drops it.
222
266
  service.invalidate();
223
- expect((await service.catalog()).map((g) => g.name)).toEqual(['Finance', 'GTM']);
267
+ expect((await service.catalog()).map((g) => g.name)).toEqual(['finance', 'gtm']);
224
268
  });
225
269
  });