@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
@@ -7,11 +7,13 @@ import path from 'node:path';
7
7
  import { KbStartupRunner } from '../../kb-startup-runner.js';
8
8
  import type { OnServerStart, ServerStartContext, StepResult } from '../../on-server-start.js';
9
9
  import { GroupsToPluginsStep } from '../groups-to-plugins.step.js';
10
+ import { PluginManifestsStep } from '../plugin-manifests.step.js';
10
11
  import { RolesYamlStep } from '../roles-yaml.step.js';
11
12
  import { renderRolesYaml } from '../../../../access-model/render-roles-yaml.js';
12
13
  import { TemplateFilesStep } from '../template-files.step.js';
13
14
  import { buildSeedTree } from '../seed-tree.js';
14
15
  import { defaultKbTemplateDir } from '../../../../../assets.js';
16
+ import { DEFAULT_KB_LAYOUT, configureKbLayout, renderKbLayoutPlaceholders } from '@bevel-software/platform-shared';
15
17
 
16
18
  const execFileAsync = promisify(execFile);
17
19
 
@@ -54,6 +56,7 @@ beforeEach(async () => {
54
56
 
55
57
  afterEach(async () => {
56
58
  vi.restoreAllMocks();
59
+ configureKbLayout({ ...DEFAULT_KB_LAYOUT });
57
60
  await fs.rm(root, { recursive: true, force: true });
58
61
  });
59
62
 
@@ -109,8 +112,9 @@ async function exists(dir: string, rel: string): Promise<boolean> {
109
112
 
110
113
  const norm = (text: string) => text.replace(/\r\n?/g, '\n');
111
114
 
115
+ /** The template as the step writes it under the default layout — placeholders rendered. */
112
116
  async function template(name: string): Promise<string> {
113
- return fs.readFile(path.join(TEMPLATE_DIR, name), 'utf8');
117
+ return renderKbLayoutPlaceholders(await fs.readFile(path.join(TEMPLATE_DIR, name), 'utf8'), DEFAULT_KB_LAYOUT);
114
118
  }
115
119
 
116
120
  /** Every required file + reserved root already present, from the real template. */
@@ -122,6 +126,7 @@ async function fullScaffold(): Promise<Record<string, string>> {
122
126
  '.gitignore': await template('gitignore.template'),
123
127
  'KnowledgeBase/.gitkeep': '',
124
128
  'Plugins/.gitkeep': '',
129
+ 'Skills/.gitkeep': '',
125
130
  };
126
131
  }
127
132
 
@@ -144,12 +149,39 @@ describe('TemplateFilesStep', () => {
144
149
  // Reserved roots materialize as <dir>/.gitkeep.
145
150
  expect(await exists(dir, 'KnowledgeBase/.gitkeep')).toBe(true);
146
151
  expect(await exists(dir, 'Plugins/.gitkeep')).toBe(true);
152
+ expect(await exists(dir, 'Skills/.gitkeep')).toBe(true);
147
153
  const subject = (await git(dir, ['log', '--format=%s', '-1'])).trim();
148
154
  expect(subject).toMatch(/^Add missing KB scaffolding: /);
149
155
  expect(subject).toContain('.gitignore');
150
156
  }
151
157
  });
152
158
 
159
+ it('renders the managed files with the deployment\'s own root names', async () => {
160
+ // A deployment that renamed its roots must hand the agent a guide naming
161
+ // the folders it will find, and an ignore file hiding the real ones.
162
+ configureKbLayout({ knowledgeBaseDir: 'docs', skillsDir: 'skills', pluginsDir: 'plugins' });
163
+ await seedUpstream({ 'marker.txt': 'seeded' });
164
+ await makeRunner([new TemplateFilesStep()]).runAll();
165
+
166
+ const dir = await checkout(DEFAULT_BRANCH);
167
+ const agents = norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'));
168
+ expect(agents).toContain('plugins/<Plugin>/plugin.json');
169
+ expect(agents).toContain('`docs/`');
170
+ expect(agents).not.toContain('{{');
171
+ expect(agents).not.toContain('KnowledgeBase/');
172
+ const ignore = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8')).split('\n').map((l) => l.trim());
173
+ expect(ignore).toContain('plugins/');
174
+ // The skills root stays VISIBLE: the Skills & Tools sidebar reads it from the workspace tree.
175
+ expect(ignore).not.toContain('skills/');
176
+ expect(ignore).not.toContain('Plugins/');
177
+ expect(await exists(dir, 'docs/.gitkeep')).toBe(true);
178
+
179
+ // And a second boot sees the rendered guide as current: no churn commit.
180
+ await makeRunner([new TemplateFilesStep()]).runAll();
181
+ const again = await checkout(DEFAULT_BRANCH);
182
+ expect((await git(again, ['rev-list', '--count', 'HEAD'])).trim()).toBe('2'); // init + scaffolding
183
+ });
184
+
153
185
  it('replaces a drifted AGENTS.md, and says so when that is the only change', async () => {
154
186
  await seedUpstream({ ...(await fullScaffold()), 'AGENTS.md': 'stale conventions\n' });
155
187
  await makeRunner([new TemplateFilesStep()]).runAll();
@@ -184,20 +216,160 @@ describe('TemplateFilesStep', () => {
184
216
  const lines = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8')).split('\n').map((l) => l.trim());
185
217
  expect(lines).toContain('MyStuff/'); // the operator's rules survive
186
218
  expect(lines).toContain('AGENTS.md'); // the platform's rule was appended
219
+ expect(lines).not.toContain('Skills/'); // never the skills root — the Library's tree needs it
220
+ });
221
+
222
+ it('drops the Skills/ rule an earlier release appended, and its comment, keeping every other rule', async () => {
223
+ // A knowledge base whose ignore file was topped up by the release that
224
+ // hid the skills root: the platform's comment + line sit at the end. The
225
+ // Skills & Tools sidebar reads that root from the workspace tree now, so
226
+ // the rule comes out exactly as it went in — nothing of the operator's moves.
227
+ const scaffold = await fullScaffold();
228
+ scaffold['.bevelignore'] =
229
+ '# mine\n.git/\nAGENTS.md\nPlugins/\n\n# Added by the platform: the conventions doc is not node content.\nSkills/\n';
230
+ await seedUpstream(scaffold);
231
+
232
+ await makeRunner([new TemplateFilesStep()]).runAll();
233
+
234
+ const dir = await checkout(DEFAULT_BRANCH);
235
+ const text = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'));
236
+ expect(text).toBe('# mine\n.git/\nAGENTS.md\nPlugins/\n');
237
+ // Idempotent: a second boot has nothing to change.
238
+ await makeRunner([new TemplateFilesStep()]).runAll();
239
+ const again = await checkout(DEFAULT_BRANCH);
240
+ expect(norm(await fs.readFile(path.join(again, '.bevelignore'), 'utf8'))).toBe(text);
241
+ });
242
+
243
+ it("drops the template's own Skills/ rule from a KB seeded by that release, comment included — whatever root the comment named", async () => {
244
+ // The previous template listed the rule under its own explanatory line,
245
+ // ending with the plugins root's name of the day; a deployment may have
246
+ // renamed that root since, so the line is known by its opening.
247
+ const scaffold = await fullScaffold();
248
+ scaffold['.bevelignore'] =
249
+ 'AGENTS.md\nPlugins/\n# The shared-skills root is rendered by the Skills & Tools app, like Groups/.\nSkills/\n';
250
+ await seedUpstream(scaffold);
251
+
252
+ await makeRunner([new TemplateFilesStep()]).runAll();
253
+
254
+ const dir = await checkout(DEFAULT_BRANCH);
255
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe('AGENTS.md\nPlugins/\n');
256
+ });
257
+
258
+ it('recognises the legacy comment for a renamed plugins root with a space in its name', async () => {
259
+ // The name between the fixed opening and closing is judged by the one
260
+ // root-name rule the platform has, so every name it could have rendered
261
+ // there is recognised — and nothing a root cannot be called is.
262
+ const scaffold = await fullScaffold();
263
+ scaffold['.bevelignore'] =
264
+ 'AGENTS.md\nPlugins/\n# The shared-skills root is rendered by the Skills & Tools app, like My Plugins/.\nSkills/\n';
265
+ await seedUpstream(scaffold);
266
+
267
+ await makeRunner([new TemplateFilesStep()]).runAll();
268
+
269
+ const dir = await checkout(DEFAULT_BRANCH);
270
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe('AGENTS.md\nPlugins/\n');
271
+ });
272
+
273
+ it("keeps an operator's Skills/ rule whose own comment merely opens like the platform's", async () => {
274
+ // Provenance is the platform's EXACT comment. A comment that begins the
275
+ // same way and goes on differently was never written by the platform.
276
+ const scaffold = await fullScaffold();
277
+ const text =
278
+ 'AGENTS.md\nPlugins/\n# The shared-skills root is rendered by the Skills & Tools app, and I hide it anyway\nSkills/\n';
279
+ scaffold['.bevelignore'] = text;
280
+ await seedUpstream(scaffold);
281
+
282
+ await makeRunner([new TemplateFilesStep()]).runAll();
283
+
284
+ const dir = await checkout(DEFAULT_BRANCH);
285
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe(text);
286
+ });
287
+
288
+ it("keeps a Skills/ rule the operator wrote themselves — provenance is the platform's comment", async () => {
289
+ const scaffold = await fullScaffold();
290
+ scaffold['.bevelignore'] = 'AGENTS.md\nPlugins/\n# I hide skills on purpose\nSkills/\n';
291
+ await seedUpstream(scaffold);
292
+
293
+ await makeRunner([new TemplateFilesStep()]).runAll();
294
+
295
+ const dir = await checkout(DEFAULT_BRANCH);
296
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe(
297
+ 'AGENTS.md\nPlugins/\n# I hide skills on purpose\nSkills/\n',
298
+ );
299
+ });
300
+
301
+ it('seeds a binary template file byte for byte and keeps a script executable — text is what decodes', async () => {
302
+ const customTemplate = path.join(root, 'custom-template-bytes');
303
+ // The packaged template as a base, plus two files it does not ship.
304
+ await fs.cp(TEMPLATE_DIR, customTemplate, { recursive: true });
305
+ await fs.mkdir(path.join(customTemplate, 'assets'), { recursive: true });
306
+ await fs.mkdir(path.join(customTemplate, 'scripts'), { recursive: true });
307
+ // Not UTF-8, and holding a NUL: text by no reading of the bytes. A
308
+ // name-based rule once sent this through the decoder and changed it.
309
+ const binary = Buffer.from([0x89, 0x50, 0x00, 0xff, 0xfe, 0x7b, 0x7b, 0x7d, 0x7d]);
310
+ await fs.writeFile(path.join(customTemplate, 'assets', '.logo.bin'), binary);
311
+ await fs.writeFile(path.join(customTemplate, 'scripts', 'run.sh'), '#!/bin/sh\necho {{skillsDir}}\n', { mode: 0o755 });
312
+ // The empty-remote seed is the one path that copies a whole template.
313
+ const dir = path.join(root, 'seeded-bytes');
314
+ await fs.mkdir(dir, { recursive: true });
315
+ await buildSeedTree(customTemplate, [], ['admin@example.com'])(dir);
316
+
317
+ expect(await fs.readFile(path.join(dir, 'assets', '.logo.bin'))).toEqual(binary);
318
+ expect(norm(await fs.readFile(path.join(dir, 'scripts', 'run.sh'), 'utf8'))).toBe('#!/bin/sh\necho Skills\n');
319
+ if (process.platform !== 'win32') {
320
+ expect((await fs.stat(path.join(dir, 'scripts', 'run.sh'))).mode & 0o111).not.toBe(0);
321
+ }
322
+ });
323
+
324
+ it("declares a custom template's ignore file without the stale Skills/ rule it still ships", async () => {
325
+ // A KB with NO ignore file gets the template's copy — and a distribution's
326
+ // template may still carry the rule the previous release had. The on-disk
327
+ // reconciliation never runs on an absent file, so the declared content
328
+ // must arrive already reconciled.
329
+ const customTemplate = path.join(root, 'custom-template-stale');
330
+ await fs.mkdir(customTemplate, { recursive: true });
331
+ await fs.writeFile(path.join(customTemplate, 'AGENTS.md'), await template('AGENTS.md'), 'utf8');
332
+ await fs.writeFile(
333
+ path.join(customTemplate, '.bevelignore'),
334
+ '# custom\nMyStuff/\n# The shared-skills root is rendered by the Skills & Tools app, like {{pluginsDir}}/.\n{{skillsDir}}/\n',
335
+ 'utf8',
336
+ );
337
+ const scaffold = await fullScaffold();
338
+ delete scaffold['.bevelignore'];
339
+ await seedUpstream(scaffold);
340
+
341
+ await makeRunner([new TemplateFilesStep()], customTemplate).runAll();
342
+
343
+ const dir = await checkout(DEFAULT_BRANCH);
344
+ const text = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'));
345
+ expect(text).toContain('MyStuff/');
346
+ expect(text.split('\n').map((l) => l.trim())).not.toContain('Skills/');
347
+ expect(text).not.toContain('shared-skills root');
348
+ });
349
+
350
+ it("leaves an operator's !Skills/ negation alone — there is nothing of the platform's to remove", async () => {
351
+ const scaffold = await fullScaffold();
352
+ scaffold['.bevelignore'] = 'AGENTS.md\nPlugins/\n!Skills/\n';
353
+ await seedUpstream(scaffold);
354
+
355
+ await makeRunner([new TemplateFilesStep()]).runAll();
356
+
357
+ const dir = await checkout(DEFAULT_BRANCH);
358
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe('AGENTS.md\nPlugins/\n!Skills/\n');
187
359
  });
188
360
 
189
361
  it('respects an explicit !AGENTS.md negation — hiding the doc is a default, not a mandate', async () => {
190
362
  // Appending the positive rule after the negation would WIN under ordered
191
363
  // matching and silently defeat the operator's stated choice to show it.
192
364
  const scaffold = await fullScaffold();
193
- scaffold['.bevelignore'] = '# operator wants the doc visible\n!AGENTS.md\n';
365
+ scaffold['.bevelignore'] = '# operator wants the doc visible\n!AGENTS.md\nPlugins/\n';
194
366
  await seedUpstream(scaffold);
195
367
 
196
368
  await makeRunner([new TemplateFilesStep()]).runAll();
197
369
 
198
370
  const dir = await checkout(DEFAULT_BRANCH);
199
371
  expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe(
200
- '# operator wants the doc visible\n!AGENTS.md\n',
372
+ '# operator wants the doc visible\n!AGENTS.md\nPlugins/\n',
201
373
  );
202
374
  });
203
375
 
@@ -294,11 +466,54 @@ describe('buildSeedTree', () => {
294
466
  expect(await fs.readFile(path.join(dest, 'docs/guide.md'), 'utf8')).toBe('guide');
295
467
  expect(await fs.readFile(path.join(dest, 'access.md'), 'utf8')).toBe('policy');
296
468
  // The generated paths — what the runner force-adds past a template .gitignore.
297
- expect(generated.sort()).toEqual(['KnowledgeBase/.gitkeep', 'Plugins/.gitkeep', 'roles.yaml']);
469
+ expect(generated.sort()).toEqual(['KnowledgeBase/.gitkeep', 'Plugins/.gitkeep', 'Skills/.gitkeep', 'roles.yaml']);
298
470
  expect(await exists(dest, 'roles.yaml')).toBe(true);
299
471
  });
300
472
  });
301
473
 
474
+ describe('PluginManifestsStep', () => {
475
+ it('writes plugin.json into legacy plugin folders on every branch, and leaves scopes and real plugins alone', async () => {
476
+ const scaffold = await fullScaffold();
477
+ await seedUpstream({
478
+ ...scaffold,
479
+ // Legacy shapes: access.md only; mcp.json only; a bare skill tree.
480
+ 'Plugins/GTM/access.md': '---\n---\nread:\n - everyone\n',
481
+ 'Plugins/GTM/outreach/SKILL.md': '---\ndescription: x\n---\n',
482
+ 'Plugins/Servers/mcp.json': '{"mcpServers":{}}',
483
+ 'Plugins/Bare/deploy/SKILL.md': '---\ndescription: y\n---\n',
484
+ // Already a plugin, both shapes.
485
+ 'Plugins/Modern/plugin.json': '{"name":"modern"}',
486
+ 'Plugins/functional/cluster/example/plugin.bundle.json': '{"name":"example"}',
487
+ // A scope with rules of its own above a bundle: not a plugin.
488
+ 'Plugins/functional/access.md': '---\n---\nread:\n - everyone\n',
489
+ // Nothing plugin-shaped at all.
490
+ 'Plugins/notes/README.md': 'just a folder',
491
+ });
492
+
493
+ await makeRunner([new PluginManifestsStep()]).runAll();
494
+
495
+ for (const branch of PROTECTED) {
496
+ const dir = await checkout(branch);
497
+ for (const legacy of ['GTM', 'Servers', 'Bare']) {
498
+ const manifest = JSON.parse(await fs.readFile(path.join(dir, `Plugins/${legacy}/plugin.json`), 'utf8'));
499
+ expect(manifest.name).toBe(legacy.toLowerCase());
500
+ }
501
+ expect(await exists(dir, 'Plugins/functional/plugin.json')).toBe(false);
502
+ expect(await exists(dir, 'Plugins/notes/plugin.json')).toBe(false);
503
+ expect(await fs.readFile(path.join(dir, 'Plugins/Modern/plugin.json'), 'utf8')).toBe('{"name":"modern"}');
504
+ }
505
+ const dir = await checkout(DEFAULT_BRANCH);
506
+ const log = (await git(dir, ['log', '-1', '--format=%B'])).trim();
507
+ expect(log).toContain('Add plugin manifests to 3 legacy plugin folders');
508
+ expect(log).toContain('Plugins/GTM: plugin.json written');
509
+
510
+ // Idempotent: nothing left to write on the next boot.
511
+ await makeRunner([new PluginManifestsStep()]).runAll();
512
+ const again = await checkout(DEFAULT_BRANCH);
513
+ expect((await git(again, ['rev-list', '--count', 'HEAD'])).trim()).toBe('2'); // init + one migration commit
514
+ });
515
+ });
516
+
302
517
  describe('GroupsToPluginsStep', () => {
303
518
  it('migrates a Groups/ tree to the Plugins layout — on the protected branches AND a draft', async () => {
304
519
  const seed = await seedUpstream({
@@ -0,0 +1,129 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {
4
+ HEXIS_EXTENSION_NS,
5
+ PLUGINS_DIR,
6
+ PLUGIN_MANIFEST_FILE,
7
+ PLUGIN_MCP_FILE,
8
+ PLUGIN_SKILLS_DIR,
9
+ renderPluginManifest,
10
+ } from '@bevel-software/platform-shared';
11
+ import { BUNDLE_FILE } from '../../../plugins/discovery/bundle-dialect/bundle.source.js';
12
+ import { isAbsence } from '../../../../shared/fs-errors.js';
13
+ import type { KbBranch, OnServerStart, ServerStartContext, StepResult } from '../on-server-start.js';
14
+
15
+ /**
16
+ * Give every legacy plugin folder its manifest, as an {@link OnServerStart}
17
+ * step.
18
+ *
19
+ * A plugin IS a folder carrying `plugin.json` (or a `plugin.bundle.json`, the
20
+ * read-only customer dialect) — discovery reads nothing else, at any depth.
21
+ * Folders from before the manifest existed have `access.md`, an `mcp.json`,
22
+ * a `skills/` tree or `.tool` manuals and no `plugin.json` beside them; the
23
+ * old scanners read those by position (directly under the plugins root),
24
+ * and this step is what retires that rule: it writes the minimal manifest
25
+ * into each such folder, once, so the position never has to mean anything
26
+ * again.
27
+ *
28
+ * WHICH folders: walk the plugins root; a folder holding either file is a
29
+ * plugin and is not entered. Any other folder is entered first. A folder
30
+ * DIRECTLY under the root — the only place the legacy layout ever put a
31
+ * plugin — becomes one when nothing plugin-shaped lives beneath it and its
32
+ * own content is legacy content: a scope folder (the dialect's
33
+ * `plugins/functional/…`) can carry an `access.md` of its own and must stay
34
+ * a scope, and a skill folder inside a legacy plugin must not become a
35
+ * plugin of its own. "Legacy content" is the set of things provisioning and
36
+ * the old migration ever put in a plugin folder: `access.md`, `mcp.json`,
37
+ * `skills/`, the hexis extension directory, a `.tool` file, or a `SKILL.md`
38
+ * anywhere beneath (the pre-`skills/` shape).
39
+ *
40
+ * Every branch, drafts included, like the Groups→Plugins migration and for
41
+ * the same reason: a draft migrated alongside its target diffs by the user's
42
+ * own changes only. Idempotent: a folder that has its manifest is skipped.
43
+ */
44
+ export class PluginManifestsStep implements OnServerStart {
45
+ readonly name = 'plugin-manifests';
46
+
47
+ async run(ctx: ServerStartContext): Promise<StepResult> {
48
+ for (const branch of await ctx.allBranches()) {
49
+ await addManifests(branch);
50
+ }
51
+ return { outcome: 'ok' };
52
+ }
53
+ }
54
+
55
+ async function addManifests(branch: KbBranch): Promise<void> {
56
+ const repoDir = await branch.repoDir();
57
+ const root = path.join(repoDir, PLUGINS_DIR);
58
+ const added: string[] = [];
59
+
60
+ /** Resolves to how many plugins sit at or beneath `dir`. */
61
+ const visit = async (dir: string, rel: string): Promise<number> => {
62
+ if (rel && ((await isFile(path.join(dir, PLUGIN_MANIFEST_FILE))) || (await isFile(path.join(dir, BUNDLE_FILE))))) {
63
+ return 1;
64
+ }
65
+ let entries: import('node:fs').Dirent[];
66
+ try {
67
+ entries = await fs.readdir(dir, { withFileTypes: true });
68
+ } catch (err) {
69
+ // Absence is the plugins root not existing yet; anything else must stop
70
+ // the boot rather than quietly leave legacy plugins without manifests.
71
+ if (isAbsence(err)) return 0;
72
+ throw err;
73
+ }
74
+ let beneath = 0;
75
+ for (const entry of entries.filter((e) => e.isDirectory() && !e.name.startsWith('.'))) {
76
+ beneath += await visit(path.join(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
77
+ }
78
+ if (beneath > 0 || !rel || rel.includes('/')) return beneath;
79
+ if (!(await looksLikeLegacyPlugin(dir, entries))) return 0;
80
+ const manifestRel = `${PLUGINS_DIR}/${rel}/${PLUGIN_MANIFEST_FILE}`;
81
+ branch.write(manifestRel, renderPluginManifest(path.posix.basename(rel)));
82
+ added.push(rel);
83
+ return 1;
84
+ };
85
+
86
+ await visit(root, '');
87
+ if (added.length === 0) return;
88
+ branch.note(`Add plugin manifests to ${added.length === 1 ? 'a legacy plugin folder' : `${added.length} legacy plugin folders`}`);
89
+ for (const rel of added) branch.note(`${PLUGINS_DIR}/${rel}: ${PLUGIN_MANIFEST_FILE} written`);
90
+ }
91
+
92
+ async function looksLikeLegacyPlugin(dir: string, entries: import('node:fs').Dirent[]): Promise<boolean> {
93
+ for (const entry of entries) {
94
+ if (entry.isFile() && (entry.name === 'access.md' || entry.name === PLUGIN_MCP_FILE || entry.name.toLowerCase().endsWith('.tool'))) {
95
+ return true;
96
+ }
97
+ if (entry.isDirectory() && (entry.name === PLUGIN_SKILLS_DIR || entry.name === HEXIS_EXTENSION_NS)) return true;
98
+ }
99
+ return hasSkillBeneath(dir);
100
+ }
101
+
102
+ /** The pre-`skills/` shape: `Plugins/<Plugin>/<skill>/SKILL.md`, at any depth. */
103
+ async function hasSkillBeneath(dir: string): Promise<boolean> {
104
+ let entries: import('node:fs').Dirent[];
105
+ try {
106
+ entries = await fs.readdir(dir, { withFileTypes: true });
107
+ } catch (err) {
108
+ if (isAbsence(err)) return false;
109
+ throw err;
110
+ }
111
+ if (entries.some((e) => e.isFile() && e.name === 'SKILL.md')) return true;
112
+ for (const entry of entries) {
113
+ if (entry.isDirectory() && !entry.name.startsWith('.') && (await hasSkillBeneath(path.join(dir, entry.name)))) {
114
+ return true;
115
+ }
116
+ }
117
+ return false;
118
+ }
119
+
120
+ async function isFile(abs: string): Promise<boolean> {
121
+ return fs.stat(abs).then(
122
+ (s) => s.isFile(),
123
+ (err: unknown) => {
124
+ if (isAbsence(err)) return false;
125
+ throw err; // a probe that fails for another reason is not "no manifest"
126
+ },
127
+ );
128
+ }
129
+
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { renderKbLayoutPlaceholders } from '@bevel-software/platform-shared';
3
4
  import { renderRolesYaml } from '../../../access-model/render-roles-yaml.js';
4
5
  import { TEMPLATE_SOURCE_FALLBACKS, reservedRootDirs, templateSource } from './template-files.step.js';
5
6
 
@@ -88,12 +89,53 @@ async function copyTemplateTree(templateDir: string, dest: string): Promise<void
88
89
  await walk('');
89
90
  }
90
91
 
91
- /** Copy one template file (by repo-relative path) into `dest`, creating parents. */
92
+ /**
93
+ * Copy one template file (by repo-relative path) into `dest`, creating
94
+ * parents. Text files are RENDERED — the managed guide and the ignore file
95
+ * name the three root folders, which a deployment may have renamed — and a
96
+ * file without placeholders comes out byte-identical to its source.
97
+ *
98
+ * "Text" is decided by the BYTES, not the name: strict UTF-8 with no NUL.
99
+ * A name-based rule mistook a hidden binary for text and re-encoded it;
100
+ * anything that does not decode is copied byte for byte. Either way the
101
+ * source's mode survives — a template script keeps its executable bit.
102
+ */
92
103
  async function copyTemplateFile(templateDir: string, relPath: string, dest: string): Promise<void> {
93
104
  const from = await templateSource(templateDir, relPath);
94
105
  const to = path.join(dest, relPath);
95
106
  await fs.mkdir(path.dirname(to), { recursive: true });
96
- await fs.copyFile(from, to);
107
+ // A binary is spotted from its first bytes (a NUL turns up early in any
108
+ // real one) and streamed across without ever being read whole; only what
109
+ // may be text is read in full, and the full decode is still the judge.
110
+ const text = (await headHasNul(from)) ? null : asText(await fs.readFile(from));
111
+ if (text === null) {
112
+ await fs.copyFile(from, to);
113
+ } else {
114
+ await fs.writeFile(to, renderKbLayoutPlaceholders(text), 'utf8');
115
+ }
116
+ await fs.chmod(to, (await fs.stat(from)).mode & 0o777);
117
+ }
118
+
119
+ /** Whether the first 8 KiB carry a NUL byte — the cheap half of "is this text". */
120
+ async function headHasNul(file: string): Promise<boolean> {
121
+ const handle = await fs.open(file, 'r');
122
+ try {
123
+ const buf = Buffer.alloc(8192);
124
+ const { bytesRead } = await handle.read(buf, 0, buf.length, 0);
125
+ return buf.subarray(0, bytesRead).includes(0);
126
+ } finally {
127
+ await handle.close();
128
+ }
129
+ }
130
+
131
+ /** The bytes as text when they ARE text — strict UTF-8, BOM kept, no NUL — else null. */
132
+ function asText(bytes: Buffer): string | null {
133
+ if (bytes.includes(0)) return null;
134
+ try {
135
+ return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
136
+ } catch {
137
+ return null;
138
+ }
97
139
  }
98
140
 
99
141
  async function exists(p: string): Promise<boolean> {