@bevel-software/platform-core-backend 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/dist/core/create-core-server.d.ts.map +1 -1
  2. package/dist/core/create-core-server.js +22 -0
  3. package/dist/core/create-core-server.js.map +1 -1
  4. package/dist/core/create-core-services.d.ts +3 -0
  5. package/dist/core/create-core-services.d.ts.map +1 -1
  6. package/dist/core/create-core-services.js +10 -1
  7. package/dist/core/create-core-services.js.map +1 -1
  8. package/dist/modules/audit/agent-audit.service.d.ts +54 -0
  9. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -0
  10. package/dist/modules/audit/agent-audit.service.js +290 -0
  11. package/dist/modules/audit/agent-audit.service.js.map +1 -0
  12. package/dist/modules/audit/agent-rest-audit.middleware.d.ts +40 -0
  13. package/dist/modules/audit/agent-rest-audit.middleware.d.ts.map +1 -0
  14. package/dist/modules/audit/agent-rest-audit.middleware.js +75 -0
  15. package/dist/modules/audit/agent-rest-audit.middleware.js.map +1 -0
  16. package/dist/modules/audit/audit.contract.d.ts +145 -0
  17. package/dist/modules/audit/audit.contract.d.ts.map +1 -0
  18. package/dist/modules/audit/audit.contract.js +35 -0
  19. package/dist/modules/audit/audit.contract.js.map +1 -0
  20. package/dist/modules/audit/audit.routes.d.ts +21 -0
  21. package/dist/modules/audit/audit.routes.d.ts.map +1 -0
  22. package/dist/modules/audit/audit.routes.js +157 -0
  23. package/dist/modules/audit/audit.routes.js.map +1 -0
  24. package/dist/modules/audit/event-classifier.d.ts +52 -0
  25. package/dist/modules/audit/event-classifier.d.ts.map +1 -0
  26. package/dist/modules/audit/event-classifier.js +123 -0
  27. package/dist/modules/audit/event-classifier.js.map +1 -0
  28. package/dist/modules/audit/index.d.ts +7 -0
  29. package/dist/modules/audit/index.d.ts.map +1 -0
  30. package/dist/modules/audit/index.js +7 -0
  31. package/dist/modules/audit/index.js.map +1 -0
  32. package/dist/modules/audit/request-audit.d.ts +57 -0
  33. package/dist/modules/audit/request-audit.d.ts.map +1 -0
  34. package/dist/modules/audit/request-audit.js +117 -0
  35. package/dist/modules/audit/request-audit.js.map +1 -0
  36. package/dist/modules/database/core-schema.d.ts +414 -0
  37. package/dist/modules/database/core-schema.d.ts.map +1 -1
  38. package/dist/modules/database/core-schema.js +99 -0
  39. package/dist/modules/database/core-schema.js.map +1 -1
  40. package/dist/modules/diff/diff.service.d.ts.map +1 -1
  41. package/dist/modules/diff/diff.service.js +6 -1
  42. package/dist/modules/diff/diff.service.js.map +1 -1
  43. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts +9 -1
  44. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts.map +1 -1
  45. package/dist/modules/kb-fs/git-guarded-filesystem.js +16 -3
  46. package/dist/modules/kb-fs/git-guarded-filesystem.js.map +1 -1
  47. package/dist/modules/mcp/mcp-auth.middleware.d.ts +1 -0
  48. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  49. package/dist/modules/mcp/mcp-auth.middleware.js +32 -0
  50. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  51. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  52. package/dist/modules/mcp/mcp.routes.js +8 -1
  53. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  54. package/dist/modules/mcp/mcp.service.d.ts +9 -1
  55. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  56. package/dist/modules/mcp/mcp.service.js +48 -5
  57. package/dist/modules/mcp/mcp.service.js.map +1 -1
  58. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +59 -0
  59. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  60. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +180 -8
  61. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  62. package/dist/modules/settings/deployment-settings.service.d.ts +10 -2
  63. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  64. package/dist/modules/settings/deployment-settings.service.js +32 -6
  65. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  66. package/dist/modules/tool-auth/external-api-key.interface.d.ts +21 -5
  67. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  68. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  69. package/dist/modules/tool-auth/external-api-key.service.d.ts +1 -0
  70. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  71. package/dist/modules/tool-auth/external-api-key.service.js +22 -9
  72. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  73. package/dist/modules/tool-auth/internal-token.service.d.ts +10 -0
  74. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
  75. package/dist/modules/tool-auth/internal-token.service.js +1 -0
  76. package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
  77. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +8 -0
  78. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  79. package/dist/modules/tool-auth/tool-auth.middleware.js +9 -1
  80. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  81. package/dist/modules/workspace/workspace.service.d.ts +8 -0
  82. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  83. package/dist/modules/workspace/workspace.service.js +9 -0
  84. package/dist/modules/workspace/workspace.service.js.map +1 -1
  85. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  86. package/dist/modules/workspace/workspace.tools.js +49 -13
  87. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  88. package/dist/shared/git-internals.d.ts +7 -1
  89. package/dist/shared/git-internals.d.ts.map +1 -1
  90. package/dist/shared/git-internals.js +67 -9
  91. package/dist/shared/git-internals.js.map +1 -1
  92. package/migrations/0012_agent_audit.sql +41 -0
  93. package/migrations/0013_agent_identity_and_key_soft_delete.sql +29 -0
  94. package/migrations/meta/0012_snapshot.json +2247 -0
  95. package/migrations/meta/0013_snapshot.json +2259 -0
  96. package/migrations/meta/_journal.json +14 -0
  97. package/package.json +4 -4
  98. package/src/core/create-core-server.ts +28 -0
  99. package/src/core/create-core-services.ts +13 -0
  100. package/src/modules/audit/__tests__/agent-audit.service.test.ts +370 -0
  101. package/src/modules/audit/__tests__/agent-rest-audit.middleware.test.ts +113 -0
  102. package/src/modules/audit/__tests__/audit.routes.test.ts +201 -0
  103. package/src/modules/audit/__tests__/event-classifier.test.ts +121 -0
  104. package/src/modules/audit/agent-audit.service.ts +316 -0
  105. package/src/modules/audit/agent-rest-audit.middleware.ts +83 -0
  106. package/src/modules/audit/audit.contract.ts +164 -0
  107. package/src/modules/audit/audit.routes.ts +168 -0
  108. package/src/modules/audit/event-classifier.ts +155 -0
  109. package/src/modules/audit/index.ts +14 -0
  110. package/src/modules/audit/request-audit.ts +141 -0
  111. package/src/modules/database/core-schema.ts +104 -0
  112. package/src/modules/diff/diff.service.ts +6 -1
  113. package/src/modules/kb-fs/git-guarded-filesystem.ts +16 -3
  114. package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +183 -13
  115. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -2
  116. package/src/modules/mcp/__tests__/mcp.routes.local-token.test.ts +24 -4
  117. package/src/modules/mcp/__tests__/mcp.routes.stateless.test.ts +1 -1
  118. package/src/modules/mcp/__tests__/mcp.service.test.ts +120 -3
  119. package/src/modules/mcp/mcp-auth.middleware.ts +31 -0
  120. package/src/modules/mcp/mcp.routes.ts +11 -1
  121. package/src/modules/mcp/mcp.service.ts +62 -4
  122. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +184 -6
  123. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +35 -0
  124. package/src/modules/settings/deployment-settings.service.ts +47 -8
  125. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +29 -8
  126. package/src/modules/tool-auth/__tests__/internal-token.service.test.ts +6 -0
  127. package/src/modules/tool-auth/external-api-key.interface.ts +22 -5
  128. package/src/modules/tool-auth/external-api-key.service.ts +25 -8
  129. package/src/modules/tool-auth/internal-token.service.ts +11 -0
  130. package/src/modules/tool-auth/tool-auth.middleware.ts +17 -1
  131. package/src/modules/workspace/__tests__/git-internals.security.test.ts +135 -44
  132. package/src/modules/workspace/workspace.service.ts +9 -0
  133. package/src/modules/workspace/workspace.tools.ts +47 -11
  134. package/src/shared/__tests__/git-internals.test.ts +41 -0
  135. package/src/shared/git-internals.ts +61 -6
  136. package/dist/modules/access/access-errors.d.ts +0 -34
  137. package/dist/modules/access/access-errors.d.ts.map +0 -1
  138. package/dist/modules/access/access-errors.js +0 -40
  139. package/dist/modules/access/access-errors.js.map +0 -1
  140. package/dist/modules/access/access-splice.d.ts +0 -140
  141. package/dist/modules/access/access-splice.d.ts.map +0 -1
  142. package/dist/modules/access/access-splice.js +0 -389
  143. package/dist/modules/access/access-splice.js.map +0 -1
  144. package/dist/modules/access/group-files.d.ts +0 -83
  145. package/dist/modules/access/group-files.d.ts.map +0 -1
  146. package/dist/modules/access/group-files.js +0 -167
  147. package/dist/modules/access/group-files.js.map +0 -1
  148. package/dist/modules/access/kb-read-filter.d.ts +0 -41
  149. package/dist/modules/access/kb-read-filter.d.ts.map +0 -1
  150. package/dist/modules/access/kb-read-filter.js +0 -60
  151. package/dist/modules/access/kb-read-filter.js.map +0 -1
  152. package/dist/modules/access/render-roles-yaml.d.ts +0 -22
  153. package/dist/modules/access/render-roles-yaml.d.ts.map +0 -1
  154. package/dist/modules/access/render-roles-yaml.js +0 -56
  155. package/dist/modules/access/render-roles-yaml.js.map +0 -1
  156. package/dist/modules/access/roles-yaml-guard.d.ts +0 -56
  157. package/dist/modules/access/roles-yaml-guard.d.ts.map +0 -1
  158. package/dist/modules/access/roles-yaml-guard.js +0 -79
  159. package/dist/modules/access/roles-yaml-guard.js.map +0 -1
  160. package/dist/modules/diff/diff-paths.d.ts +0 -2
  161. package/dist/modules/diff/diff-paths.d.ts.map +0 -1
  162. package/dist/modules/diff/diff-paths.js +0 -9
  163. package/dist/modules/diff/diff-paths.js.map +0 -1
  164. package/dist/modules/groups/group-provision.service.d.ts +0 -130
  165. package/dist/modules/groups/group-provision.service.d.ts.map +0 -1
  166. package/dist/modules/groups/group-provision.service.js +0 -288
  167. package/dist/modules/groups/group-provision.service.js.map +0 -1
  168. package/dist/modules/groups/groups.contract.d.ts +0 -106
  169. package/dist/modules/groups/groups.contract.d.ts.map +0 -1
  170. package/dist/modules/groups/groups.contract.js +0 -36
  171. package/dist/modules/groups/groups.contract.js.map +0 -1
  172. package/dist/modules/groups/groups.routes.d.ts +0 -42
  173. package/dist/modules/groups/groups.routes.d.ts.map +0 -1
  174. package/dist/modules/groups/groups.routes.js +0 -379
  175. package/dist/modules/groups/groups.routes.js.map +0 -1
  176. package/dist/modules/groups/groups.service.d.ts +0 -60
  177. package/dist/modules/groups/groups.service.d.ts.map +0 -1
  178. package/dist/modules/groups/groups.service.js +0 -172
  179. package/dist/modules/groups/groups.service.js.map +0 -1
  180. package/dist/modules/groups/index.d.ts +0 -7
  181. package/dist/modules/groups/index.d.ts.map +0 -1
  182. package/dist/modules/groups/index.js +0 -6
  183. package/dist/modules/groups/index.js.map +0 -1
  184. package/dist/modules/groups/join-proposals.d.ts +0 -53
  185. package/dist/modules/groups/join-proposals.d.ts.map +0 -1
  186. package/dist/modules/groups/join-proposals.js +0 -67
  187. package/dist/modules/groups/join-proposals.js.map +0 -1
  188. package/dist/modules/groups/join-requests.service.d.ts +0 -81
  189. package/dist/modules/groups/join-requests.service.d.ts.map +0 -1
  190. package/dist/modules/groups/join-requests.service.js +0 -135
  191. package/dist/modules/groups/join-requests.service.js.map +0 -1
  192. package/dist/modules/mcp/mcp-session-store.d.ts +0 -74
  193. package/dist/modules/mcp/mcp-session-store.d.ts.map +0 -1
  194. package/dist/modules/mcp/mcp-session-store.js +0 -131
  195. package/dist/modules/mcp/mcp-session-store.js.map +0 -1
  196. package/dist/modules/workflow/file-change-notifier.d.ts +0 -38
  197. package/dist/modules/workflow/file-change-notifier.d.ts.map +0 -1
  198. package/dist/modules/workflow/file-change-notifier.js +0 -22
  199. package/dist/modules/workflow/file-change-notifier.js.map +0 -1
  200. package/dist/modules/workflow/git/branch-name.d.ts +0 -10
  201. package/dist/modules/workflow/git/branch-name.d.ts.map +0 -1
  202. package/dist/modules/workflow/git/branch-name.js +0 -76
  203. package/dist/modules/workflow/git/branch-name.js.map +0 -1
  204. package/dist/modules/workflow/git/clone-config.d.ts +0 -61
  205. package/dist/modules/workflow/git/clone-config.d.ts.map +0 -1
  206. package/dist/modules/workflow/git/clone-config.js +0 -69
  207. package/dist/modules/workflow/git/clone-config.js.map +0 -1
  208. package/dist/modules/workflow/git/mutex.d.ts +0 -11
  209. package/dist/modules/workflow/git/mutex.d.ts.map +0 -1
  210. package/dist/modules/workflow/git/mutex.js +0 -23
  211. package/dist/modules/workflow/git/mutex.js.map +0 -1
  212. package/dist/modules/workflow/locking-filesystem.d.ts +0 -137
  213. package/dist/modules/workflow/locking-filesystem.d.ts.map +0 -1
  214. package/dist/modules/workflow/locking-filesystem.js +0 -553
  215. package/dist/modules/workflow/locking-filesystem.js.map +0 -1
  216. package/dist/modules/workflow/read-only-filesystem.d.ts +0 -24
  217. package/dist/modules/workflow/read-only-filesystem.d.ts.map +0 -1
  218. package/dist/modules/workflow/read-only-filesystem.js +0 -39
  219. package/dist/modules/workflow/read-only-filesystem.js.map +0 -1
  220. package/dist/modules/workflow/workflow.errors.d.ts +0 -197
  221. package/dist/modules/workflow/workflow.errors.d.ts.map +0 -1
  222. package/dist/modules/workflow/workflow.errors.js +0 -298
  223. package/dist/modules/workflow/workflow.errors.js.map +0 -1
  224. package/dist/modules/workspace/bevel-ignore.d.ts +0 -30
  225. package/dist/modules/workspace/bevel-ignore.d.ts.map +0 -1
  226. package/dist/modules/workspace/bevel-ignore.js +0 -61
  227. package/dist/modules/workspace/bevel-ignore.js.map +0 -1
  228. package/dist/modules/workspace/kb-seed.interface.d.ts +0 -36
  229. package/dist/modules/workspace/kb-seed.interface.d.ts.map +0 -1
  230. package/dist/modules/workspace/kb-seed.interface.js +0 -2
  231. package/dist/modules/workspace/kb-seed.interface.js.map +0 -1
  232. package/dist/modules/workspace/kb-seed.service.d.ts +0 -125
  233. package/dist/modules/workspace/kb-seed.service.d.ts.map +0 -1
  234. package/dist/modules/workspace/kb-seed.service.js +0 -535
  235. package/dist/modules/workspace/kb-seed.service.js.map +0 -1
  236. package/dist/modules/workspace/plugins-migration.d.ts +0 -50
  237. package/dist/modules/workspace/plugins-migration.d.ts.map +0 -1
  238. package/dist/modules/workspace/plugins-migration.js +0 -379
  239. package/dist/modules/workspace/plugins-migration.js.map +0 -1
  240. package/dist/shared/fs-errors.d.ts +0 -8
  241. package/dist/shared/fs-errors.d.ts.map +0 -1
  242. package/dist/shared/fs-errors.js +0 -11
  243. package/dist/shared/fs-errors.js.map +0 -1
  244. package/dist/shared/fs-walk.d.ts +0 -23
  245. package/dist/shared/fs-walk.d.ts.map +0 -1
  246. package/dist/shared/fs-walk.js +0 -42
  247. package/dist/shared/fs-walk.js.map +0 -1
  248. package/dist/shared/hash-email.d.ts +0 -11
  249. package/dist/shared/hash-email.d.ts.map +0 -1
  250. package/dist/shared/hash-email.js +0 -14
  251. package/dist/shared/hash-email.js.map +0 -1
  252. package/dist/shared/kb-walk.d.ts +0 -52
  253. package/dist/shared/kb-walk.d.ts.map +0 -1
  254. package/dist/shared/kb-walk.js +0 -76
  255. package/dist/shared/kb-walk.js.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"kb-seed.interface.js","sourceRoot":"","sources":["../../../src/modules/workspace/kb-seed.interface.ts"],"names":[],"mappings":""}
@@ -1,125 +0,0 @@
1
- import type { IKbSeedService } from './kb-seed.interface.js';
2
- /**
3
- * The two roots CORE gives a knowledge base: the ontologies, and the plugins
4
- * that hold skills and tools.
5
- *
6
- * `Data/`, `Agents/` and `Pipelines/` are deliberately absent. They scaffold
7
- * the agentic execution layer, which is not part of this platform — a core
8
- * deployment that created them would be handing every operator three empty
9
- * folders it has no feature to fill. A distribution that DOES own that layer
10
- * passes them as `extraDirs` (and ships a template carrying their READMEs);
11
- * the names stay reserved in `kb-layout.ts` either way, so a KB that has them
12
- * still renders them as roots rather than folding them into Knowledge.
13
- */
14
- export declare const CORE_REQUIRED_DIRS: readonly string[];
15
- /**
16
- * Seeds/tops-up the KB *remote* so the rest of the app's assumption — that the
17
- * repo it clones already carries the protected branches and base scaffolding —
18
- * holds even for a brand-new or partially-populated GitHub repo. The template
19
- * ships inside the platform (`kb-template/`); this reads from it.
20
- *
21
- * Split into two moments so the file-level top-up rides along with the clone the
22
- * app already performs, rather than doing its own extra clones:
23
- * - {@link ensureRemoteSeeded} runs once before the first clone (an empty remote
24
- * can't be `clone -b`'d): it seeds an empty repo and makes sure every
25
- * protected branch *exists* as a ref.
26
- * - {@link topUpWorkspace} runs on the freshly-cloned workspace of whatever
27
- * branch a user loads: it fills in any missing base files on that branch.
28
- */
29
- export declare class KbSeedService implements IKbSeedService {
30
- private readonly kbTemplateDir;
31
- private readonly seedAdminEmails;
32
- /** Single-flight: the first caller runs the remote pass; the rest await its result. */
33
- private inFlight;
34
- /**
35
- * @param kbRepoUrl Clone/push URL of the KB remote.
36
- * @param kbTemplateDir Filesystem path to the `kb-template/` seed source.
37
- * @param protectedBranches Branch names that must exist (e.g. current/target-company-state).
38
- * @param defaultBranch The branch new checkouts land on — used as the init
39
- * branch of the empty-seed commit and the preferred
40
- * base when creating a missing protected branch.
41
- * @param seedAdminEmails Admins written into the generated `roles.yaml`.
42
- * @param gitUsername Basic-auth username for git-over-HTTPS (provider-specific).
43
- * @param extraDirs Additional root folders this distribution reserves,
44
- * on top of {@link CORE_REQUIRED_DIRS}. Their
45
- * `.gitkeep` is written directly rather than copied,
46
- * so a distribution can claim a root without also
47
- * shipping a template entry for it.
48
- */
49
- constructor(kbRepoUrl: string | (() => string), kbTemplateDir: string, protectedBranches: readonly string[] | (() => readonly string[]), defaultBranch: string | (() => string), seedAdminEmails: readonly string[], gitUsername?: string | (() => string), extraDirs?: readonly string[]);
50
- private readonly kbRepoUrl;
51
- private readonly protectedBranches;
52
- private readonly defaultBranch;
53
- private readonly gitUsername;
54
- /** Root folders this deployment guarantees — core's two plus any extras. */
55
- private readonly requiredDirs;
56
- ensureRemoteSeeded(): Promise<void>;
57
- private run;
58
- /**
59
- * Add any missing base scaffolding to an already-cloned workspace of `branch`,
60
- * then commit + push it. Called once per fresh clone (any branch a user loads),
61
- * so the check reuses the clone the app already made instead of a dedicated one.
62
- *
63
- * Best-effort: never throws. If the push is rejected (e.g. a concurrent update
64
- * to the branch), the local scaffolding commit is rolled back so the workspace
65
- * stays consistent with origin, and the top-up is retried on a future clone.
66
- */
67
- topUpWorkspace(repoDir: string, branch: string): Promise<void>;
68
- /** Prefer the default branch as a base, else the first protected branch present, else any head. */
69
- private pickBase;
70
- /** `-c` args that inject credentials + longpaths, mirroring the workspace clone. */
71
- private credArgs;
72
- private git;
73
- /** Set of branch names on the remote (empty ⇒ uninitialised repo). */
74
- private lsRemoteHeads;
75
- /** Run a block against a throwaway temp dir, always cleaned up. */
76
- private withTempDir;
77
- private stampIdentity;
78
- /** Copy the entire template tree into `dest` (roles.yaml isn't in it — it's generated). */
79
- private copyTemplateTree;
80
- /** Copy one template file (by repo-relative path) into `dest`, creating parents. */
81
- /**
82
- * Whether the repo's copy of `relPath` differs from the template's, modulo
83
- * line endings — a CRLF checkout of identical content must read as "same",
84
- * or the managed-file refresh would commit churn on every boot forever.
85
- */
86
- private templateDiffers;
87
- private copyTemplateFile;
88
- /**
89
- * Ensure `.bevelignore` carries `pattern`, appending it when absent. Returns
90
- * the paths changed, for the commit.
91
- *
92
- * APPENDS — never rewrites. The file is the operator's, and every rule
93
- * already in it is theirs to keep; this adds one line under a comment saying
94
- * where it came from. Absent file, or a file that already lists the pattern,
95
- * is a no-op, so running it on every clone changes nothing after the first.
96
- *
97
- * Matched line-wise rather than by substring: a rule for `Plugins/AGENTS.md`
98
- * is not a rule for the root `AGENTS.md`, and treating it as one would leave
99
- * the mismatch this exists to close.
100
- */
101
- private mergeIgnorePattern;
102
- /**
103
- * Create any reserved root folder this repo is missing, as an empty
104
- * `<dir>/.gitkeep`. Returns the paths added, for the commit message.
105
- *
106
- * WRITTEN, not copied from the template. A `.gitkeep` is empty by definition,
107
- * and requiring a template entry per root would mean a distribution could not
108
- * reserve one without forking the packaged template.
109
- *
110
- * Keyed on the DIRECTORY's existence, not the `.gitkeep` file — a branch that
111
- * already has content under `KnowledgeBase/` never gets a pointless
112
- * placeholder alongside it.
113
- */
114
- private ensureRequiredDirs;
115
- /** `lstat` without the throw — null when nothing is at `p`. */
116
- private lstatOrNull;
117
- /** `git init` a temp repo, lay down the full template + generated roles.yaml, commit. */
118
- private buildSeedCommit;
119
- /** Empty remote → one seed commit pushed to every protected branch. */
120
- private seedEmptyRemote;
121
- /** Create a missing protected branch on the remote, pointed at `base`'s tip. */
122
- private createBranchOnRemote;
123
- private exists;
124
- }
125
- //# sourceMappingURL=kb-seed.service.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"kb-seed.service.d.ts","sourceRoot":"","sources":["../../../src/modules/workspace/kb-seed.service.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AA8B7D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,MAAM,EAAsC,CAAC;AAiDvF;;;;;;;;;;;;;GAaG;AACH,qBAAa,aAAc,YAAW,cAAc;IAqBhD,OAAO,CAAC,QAAQ,CAAC,aAAa;IAG9B,OAAO,CAAC,QAAQ,CAAC,eAAe;IAvBlC,uFAAuF;IACvF,OAAO,CAAC,QAAQ,CAA8B;IAE9C;;;;;;;;;;;;;;OAcG;gBAED,SAAS,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,EACjB,aAAa,EAAE,MAAM,EACtC,iBAAiB,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,MAAM,SAAS,MAAM,EAAE,CAAC,EAChE,aAAa,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,EACrB,eAAe,EAAE,SAAS,MAAM,EAAE,EACnD,WAAW,GAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAoB,EACvD,SAAS,GAAE,SAAS,MAAM,EAAO;IA+BnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA0B;IAC5D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAe;IAC3C,4EAA4E;IAC5E,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAoB;IAEjD,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;YAarB,GAAG;IA4BjB;;;;;;;;OAQG;IACG,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAqHpE,mGAAmG;IACnG,OAAO,CAAC,QAAQ;IAQhB,oFAAoF;IACpF,OAAO,CAAC,QAAQ;YAcF,GAAG;IAgBjB,sEAAsE;YACxD,aAAa;IAU3B,mEAAmE;YACrD,WAAW;YASX,aAAa;IAK3B,2FAA2F;YAC7E,gBAAgB;IAgB9B,oFAAoF;IACpF;;;;OAIG;YACW,eAAe;YASf,gBAAgB;IAO9B;;;;;;;;;;;;OAYG;YACW,kBAAkB;IAmBhC;;;;;;;;;;;OAWG;YACW,kBAAkB;IA2BhC,+DAA+D;YACjD,WAAW;IAQzB,yFAAyF;YAC3E,eAAe;IAc7B,uEAAuE;YACzD,eAAe;IAoB7B,gFAAgF;YAClE,oBAAoB;YASpB,MAAM;CAQrB"}
@@ -1,535 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { execFile } from 'node:child_process';
5
- import { promisify } from 'node:util';
6
- import { KNOWLEDGE_BASE_DIR, LEGACY_GROUPS_DIR, PLUGINS_DIR } from '@bevel-software/platform-shared';
7
- import { IGNORE_FILENAME } from './bevel-ignore.js';
8
- import { migrateGroupsToPlugins } from './plugins-migration.js';
9
- const execFileAsync = promisify(execFile);
10
- /**
11
- * Fallback committer identity for seed commits. Mirrors the workspace clone's
12
- * bot identity so seed commits attribute to the same bot in `git log`.
13
- */
14
- const BOT_NAME = 'Bevel Workflow';
15
- const BOT_EMAIL = 'bevel-workflow@bevel.software';
16
- /**
17
- * The **required scaffolding** — the minimum an operational KB needs. When a
18
- * branch is loaded, any of these that are missing are added to it; the sample
19
- * ontology is NOT (it only seeds a fully-empty repo).
20
- *
21
- * Two kinds:
22
- * - {@link REQUIRED_FILES}: repo-root files added when the file is missing.
23
- * - {@link CORE_REQUIRED_DIRS} (plus any `extraDirs`): well-known root dirs the
24
- * app expects to exist; when a dir is entirely absent it's created by adding
25
- * its `<dir>/.gitkeep`. Keyed on the *directory's* existence, not the
26
- * `.gitkeep` file — so a branch that already has content under
27
- * `KnowledgeBase/` never gets a pointless placeholder.
28
- *
29
- * `roles.yaml` is in neither, and is not part of the template at all: it is
30
- * generated from `ADMIN_EMAIL` (see {@link renderRolesYaml}) and written
31
- * directly, so a repo can't be seeded with a stale hard-coded Admin list.
32
- */
33
- const REQUIRED_FILES = ['access.md', 'AGENTS.md', '.bevelignore', '.gitignore'];
34
- /**
35
- * The two roots CORE gives a knowledge base: the ontologies, and the plugins
36
- * that hold skills and tools.
37
- *
38
- * `Data/`, `Agents/` and `Pipelines/` are deliberately absent. They scaffold
39
- * the agentic execution layer, which is not part of this platform — a core
40
- * deployment that created them would be handing every operator three empty
41
- * folders it has no feature to fill. A distribution that DOES own that layer
42
- * passes them as `extraDirs` (and ships a template carrying their READMEs);
43
- * the names stay reserved in `kb-layout.ts` either way, so a KB that has them
44
- * still renders them as roots rather than folding them into Knowledge.
45
- */
46
- export const CORE_REQUIRED_DIRS = [KNOWLEDGE_BASE_DIR, PLUGINS_DIR];
47
- /**
48
- * A reserved root must be ONE path segment — `Data`, not `Data/x`, `../x` or
49
- * `/x`. The name is joined onto the repo root, so anything else writes outside
50
- * the repo being seeded.
51
- *
52
- * Deliberately NOT a check against the reserved-root set in `kb-layout.ts`:
53
- * `Data`, `Agents` and `Pipelines` are all in that set, and they are precisely
54
- * what a distribution passes here. Being reserved is what makes a name worth
55
- * claiming — the file tree renders it as its own root instead of folding it
56
- * into Knowledge — so rejecting reserved names would reject the only real use.
57
- */
58
- function assertRootSegment(dir) {
59
- if (!dir ||
60
- dir === '.' ||
61
- dir === '..' ||
62
- dir.includes('/') ||
63
- dir.includes('\\') ||
64
- path.isAbsolute(dir)) {
65
- throw new Error(`Reserved KB root must be a single path segment (no separators, no ".."); got "${dir}"`);
66
- }
67
- }
68
- /** Redact the token from any string that might surface in a log or error. */
69
- function redact(s) {
70
- const token = process.env.GITHUB_TOKEN;
71
- return token ? s.replaceAll(token, '***') : s;
72
- }
73
- /** Generate `roles.yaml` granting Admin to each seed email. */
74
- function renderRolesYaml(adminEmails) {
75
- const lines = adminEmails.map((e) => ` - ${e}`).join('\n');
76
- return ('# Identity → role mapping for access control.\n' +
77
- '# Role names are case- and whitespace-insensitive. The `Admin` role is special:\n' +
78
- '# only Admins may edit this file, and at least one Admin must always exist.\n' +
79
- '#\n' +
80
- '# Generated by the Bevel platform at KB-seed time from ADMIN_EMAIL.\n' +
81
- 'roles:\n' +
82
- ' Admin:\n' +
83
- `${lines}\n`);
84
- }
85
- /**
86
- * Seeds/tops-up the KB *remote* so the rest of the app's assumption — that the
87
- * repo it clones already carries the protected branches and base scaffolding —
88
- * holds even for a brand-new or partially-populated GitHub repo. The template
89
- * ships inside the platform (`kb-template/`); this reads from it.
90
- *
91
- * Split into two moments so the file-level top-up rides along with the clone the
92
- * app already performs, rather than doing its own extra clones:
93
- * - {@link ensureRemoteSeeded} runs once before the first clone (an empty remote
94
- * can't be `clone -b`'d): it seeds an empty repo and makes sure every
95
- * protected branch *exists* as a ref.
96
- * - {@link topUpWorkspace} runs on the freshly-cloned workspace of whatever
97
- * branch a user loads: it fills in any missing base files on that branch.
98
- */
99
- export class KbSeedService {
100
- kbTemplateDir;
101
- seedAdminEmails;
102
- /** Single-flight: the first caller runs the remote pass; the rest await its result. */
103
- inFlight = null;
104
- /**
105
- * @param kbRepoUrl Clone/push URL of the KB remote.
106
- * @param kbTemplateDir Filesystem path to the `kb-template/` seed source.
107
- * @param protectedBranches Branch names that must exist (e.g. current/target-company-state).
108
- * @param defaultBranch The branch new checkouts land on — used as the init
109
- * branch of the empty-seed commit and the preferred
110
- * base when creating a missing protected branch.
111
- * @param seedAdminEmails Admins written into the generated `roles.yaml`.
112
- * @param gitUsername Basic-auth username for git-over-HTTPS (provider-specific).
113
- * @param extraDirs Additional root folders this distribution reserves,
114
- * on top of {@link CORE_REQUIRED_DIRS}. Their
115
- * `.gitkeep` is written directly rather than copied,
116
- * so a distribution can claim a root without also
117
- * shipping a template entry for it.
118
- */
119
- constructor(kbRepoUrl, kbTemplateDir, protectedBranches, defaultBranch, seedAdminEmails, gitUsername = 'x-access-token', extraDirs = []) {
120
- this.kbTemplateDir = kbTemplateDir;
121
- this.seedAdminEmails = seedAdminEmails;
122
- // A getter is read per-operation, so a remote supplied through the setup
123
- // screen is seeded against without restarting; a string is still accepted.
124
- this.kbRepoUrl = typeof kbRepoUrl === 'function' ? kbRepoUrl : () => kbRepoUrl;
125
- // Read when seeding, not at construction: the branch model can be supplied
126
- // through the setup screen, which happens after this object exists.
127
- this.protectedBranches =
128
- typeof protectedBranches === 'function' ? protectedBranches : () => protectedBranches;
129
- this.defaultBranch = typeof defaultBranch === 'function' ? defaultBranch : () => defaultBranch;
130
- this.gitUsername = typeof gitUsername === 'function' ? gitUsername : () => gitUsername;
131
- // NOT a getter, unlike its neighbours above: the reserved roots are named
132
- // by the composition root in code, not collected on the setup screen, so
133
- // there is nothing to re-read. Validated once, here — every entry is joined
134
- // onto the repo root and onto `<dir>/.gitkeep`, so a separator or a `..`
135
- // would write outside the repo being seeded, and a bad value should fail at
136
- // boot beside the rest of the wiring rather than part-way through seeding
137
- // somebody's knowledge base. Same contract as `KB_DIR_NAME` in `CoreConfig`.
138
- for (const dir of extraDirs) {
139
- assertRootSegment(dir);
140
- // A root named after a required FILE is a typo with a silent outcome:
141
- // the file is laid down first in both seed paths, so `ensureRequiredDirs`
142
- // finds the path taken and skips it, and the directory the caller asked
143
- // for never appears with nothing said about why.
144
- if (REQUIRED_FILES.includes(dir)) {
145
- throw new Error(`Reserved KB root "${dir}" collides with a required file of the same name`);
146
- }
147
- }
148
- this.requiredDirs = [...CORE_REQUIRED_DIRS, ...extraDirs];
149
- }
150
- kbRepoUrl;
151
- protectedBranches;
152
- defaultBranch;
153
- gitUsername;
154
- /** Root folders this deployment guarantees — core's two plus any extras. */
155
- requiredDirs;
156
- ensureRemoteSeeded() {
157
- // Cache the promise, not just a boolean, so concurrent first-callers share
158
- // one run. On failure we clear it so a later call can retry (a transient
159
- // network blip on ls-remote shouldn't wedge seeding for the whole process).
160
- if (!this.inFlight) {
161
- this.inFlight = this.run().catch((err) => {
162
- this.inFlight = null;
163
- throw err;
164
- });
165
- }
166
- return this.inFlight;
167
- }
168
- async run() {
169
- const heads = await this.lsRemoteHeads();
170
- if (heads.size === 0) {
171
- if (this.seedAdminEmails.length === 0) {
172
- throw new Error(`KB remote ${redact(this.kbRepoUrl())} is empty and cannot be seeded: ` +
173
- 'no initial Admin was supplied. A seeded KB with no Admin is unusable ' +
174
- '(access resolution requires at least one Admin). Unreachable in ' +
175
- 'normal operation — ADMIN_EMAIL is required at boot — so this guards ' +
176
- 'a caller that constructed the seeder with an empty list.');
177
- }
178
- await this.seedEmptyRemote();
179
- return;
180
- }
181
- // Non-empty remote: make sure every protected branch exists as a ref so a
182
- // later `clone -b <branch>` succeeds. This creates the branch pointer only;
183
- // per-branch file top-up happens in `topUpWorkspace` when the branch loads.
184
- const base = this.pickBase(heads);
185
- for (const branch of this.protectedBranches()) {
186
- if (!heads.has(branch)) {
187
- await this.createBranchOnRemote(branch, base);
188
- }
189
- }
190
- }
191
- /**
192
- * Add any missing base scaffolding to an already-cloned workspace of `branch`,
193
- * then commit + push it. Called once per fresh clone (any branch a user loads),
194
- * so the check reuses the clone the app already made instead of a dedicated one.
195
- *
196
- * Best-effort: never throws. If the push is rejected (e.g. a concurrent update
197
- * to the branch), the local scaffolding commit is rolled back so the workspace
198
- * stays consistent with origin, and the top-up is retried on a future clone.
199
- */
200
- async topUpWorkspace(repoDir, branch) {
201
- // Scaffolding lands on PROTECTED branches only. A draft or suggestions
202
- // branch is somebody's change-in-waiting, and a seeder commit there
203
- // surfaces as noise in their change request's diff against the default
204
- // branch (a stray scaffolding file riding along in a skill proposal was
205
- // exactly this). Whatever the protected branches are missing, they get
206
- // when THEY load — and drafts fork from them.
207
- if (!this.protectedBranches().includes(branch))
208
- return;
209
- try {
210
- const added = [];
211
- // BEFORE the dir top-up: `ensureRequiredDirs` would otherwise create an
212
- // empty `Plugins/` next to the `Plugins/` that is about to become it, and
213
- // the migration refuses a destination that already exists — so the
214
- // scaffolding would quietly block the very migration it precedes.
215
- const migration = await migrateGroupsToPlugins(repoDir);
216
- // Notes are logged unconditionally: an advisory note (a manual that
217
- // refuses to convert) is exactly the run where the operator needs to
218
- // hear about it, and such a run changes no files.
219
- for (const note of migration.notes)
220
- console.log(`[plugins-migration] ${note}`);
221
- // `migrated` means FILES CHANGED — a note-only run stages nothing, or
222
- // the commit below would fail empty on every boot with a warning about
223
- // a migration that did nothing.
224
- if (migration.migrated) {
225
- // Stage the legacy root ONLY when the rename happened this run: `git
226
- // add -A -- Groups Plugins` fails outright on a pathspec that matches
227
- // nothing, and a reorganisation inside an existing Plugins/ tree has
228
- // no Groups/ to stage.
229
- if (migration.renamed)
230
- added.push(LEGACY_GROUPS_DIR);
231
- added.push(PLUGINS_DIR);
232
- // The rename's companion edit to the repo-root ignore file — outside
233
- // the two roots, so it needs its own pathspec to land in the commit.
234
- if (migration.ignoreRewritten)
235
- added.push(IGNORE_FILENAME);
236
- }
237
- for (const rel of REQUIRED_FILES) {
238
- if (!(await this.exists(path.join(repoDir, rel)))) {
239
- await this.copyTemplateFile(rel, repoDir);
240
- added.push(rel);
241
- // Adding AGENTS.md to a knowledge base seeded before the rename
242
- // leaves it VISIBLE: that repo's `.bevelignore` lists CLAUDE.md and
243
- // knows nothing of the new name, so the conventions doc starts
244
- // showing up in the file tree and the agent view. We created the
245
- // mismatch by adding the file, so we close it here.
246
- if (rel === 'AGENTS.md')
247
- added.push(...(await this.mergeIgnorePattern(repoDir, rel)));
248
- }
249
- }
250
- // AGENTS.md is MANAGED, not merely seeded: the platform owns its
251
- // content, and a stale copy is replaced with the packaged template's on
252
- // every top-up (each fresh clone of a protected branch — in practice,
253
- // every server restart). The file's own header says so, which is what
254
- // makes overwriting edits a stated contract instead of a surprise.
255
- let agentsRefreshed = false;
256
- if (!added.includes('AGENTS.md') &&
257
- (await this.templateDiffers(repoDir, 'AGENTS.md'))) {
258
- await this.copyTemplateFile('AGENTS.md', repoDir);
259
- added.push('AGENTS.md');
260
- agentsRefreshed = true;
261
- }
262
- added.push(...(await this.ensureRequiredDirs(repoDir)));
263
- if (!(await this.exists(path.join(repoDir, 'roles.yaml')))) {
264
- if (this.seedAdminEmails.length > 0) {
265
- await fs.writeFile(path.join(repoDir, 'roles.yaml'), renderRolesYaml(this.seedAdminEmails), 'utf8');
266
- added.push('roles.yaml');
267
- }
268
- else {
269
- console.warn(`[kb-seed] Branch "${branch}" is missing roles.yaml and ADMIN_EMAIL is ` +
270
- 'unset — leaving it absent. Access resolution will fail until an ' +
271
- 'Admin roles.yaml exists; set ADMIN_EMAIL or add roles.yaml manually.');
272
- }
273
- }
274
- if (added.length === 0)
275
- return; // already fully scaffolded → no-op
276
- // Stamp the bot identity so the commit has an author even if the clone
277
- // wasn't configured with one (the workspace clone already sets the same
278
- // values, so this is a harmless no-op there).
279
- await this.stampIdentity(repoDir);
280
- // `-A` so the migration's renames stage their DELETIONS as well; a plain
281
- // `add` would commit the new tree while leaving `Plugins/` in the index.
282
- await this.git(repoDir, ['add', '-A', '--', ...added]);
283
- const message = migration.migrated
284
- ? migration.renamed
285
- ? `Move ${LEGACY_GROUPS_DIR}/ to ${PLUGINS_DIR}/ (Agent Plugins layout)`
286
- : `Reorganise ${PLUGINS_DIR}/ to the Agent Plugins layout`
287
- : agentsRefreshed && added.length === 1
288
- ? 'Update AGENTS.md to the current platform template'
289
- : `Add missing KB scaffolding: ${added.join(', ')}`;
290
- await this.git(repoDir, ['commit', '-m', message]);
291
- try {
292
- await this.git(repoDir, ['push', 'origin', `HEAD:refs/heads/${branch}`]);
293
- console.log(`[kb-seed] Topped up "${branch}" with: ${added.join(', ')}`);
294
- }
295
- catch (err) {
296
- // Roll the local branch back to origin so the workspace never sits ahead
297
- // of the remote (which would confuse the workflow git layer). The files
298
- // simply aren't present this session; a later fresh clone retries.
299
- console.warn(`[kb-seed] Could not push scaffolding to "${branch}", rolling back local commit:`, err instanceof Error ? redact(err.message) : String(err));
300
- await this.git(repoDir, ['reset', '--hard', `origin/${branch}`]).catch(() => { });
301
- }
302
- }
303
- catch (err) {
304
- // Top-up is best-effort infrastructure — a failure here must not block the
305
- // user from loading the branch.
306
- console.warn(`[kb-seed] Scaffolding top-up for "${branch}" failed (non-fatal):`, err instanceof Error ? redact(err.message) : String(err));
307
- }
308
- }
309
- /** Prefer the default branch as a base, else the first protected branch present, else any head. */
310
- pickBase(heads) {
311
- if (heads.has(this.defaultBranch()))
312
- return this.defaultBranch();
313
- const protectedPresent = this.protectedBranches().find((b) => heads.has(b));
314
- if (protectedPresent)
315
- return protectedPresent;
316
- // Deterministic pick so repeated runs behave identically.
317
- return [...heads].sort()[0];
318
- }
319
- /** `-c` args that inject credentials + longpaths, mirroring the workspace clone. */
320
- credArgs() {
321
- const args = ['-c', 'core.longpaths=true'];
322
- const token = process.env.GITHUB_TOKEN;
323
- if (token) {
324
- // The helper reads GITHUB_TOKEN at runtime, so the value never appears in argv.
325
- // Username is provider-specific; the token is always the Basic-auth password.
326
- args.push('-c', `credential.helper=!f() { echo "username=${this.gitUsername()}"; echo "password=$GITHUB_TOKEN"; }; f`);
327
- }
328
- return args;
329
- }
330
- async git(cwd, args) {
331
- try {
332
- const { stdout } = await execFileAsync('git', args, {
333
- cwd: cwd ?? undefined,
334
- env: { ...process.env, LC_ALL: 'C', LANG: 'C' },
335
- maxBuffer: 32 * 1024 * 1024,
336
- });
337
- return stdout.toString();
338
- }
339
- catch (err) {
340
- const msg = err instanceof Error ? err.message : String(err);
341
- let i = 0;
342
- while (i < args.length && args[i] === '-c')
343
- i += 2;
344
- throw new Error(`git ${args[i] ?? args[0]} failed: ${redact(msg)}`);
345
- }
346
- }
347
- /** Set of branch names on the remote (empty ⇒ uninitialised repo). */
348
- async lsRemoteHeads() {
349
- const out = await this.git(null, [...this.credArgs(), 'ls-remote', '--heads', this.kbRepoUrl()]);
350
- const heads = new Set();
351
- for (const line of out.split('\n')) {
352
- const m = line.match(/\srefs\/heads\/(.+)$/);
353
- if (m)
354
- heads.add(m[1].trim());
355
- }
356
- return heads;
357
- }
358
- /** Run a block against a throwaway temp dir, always cleaned up. */
359
- async withTempDir(fn) {
360
- const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'kb-seed-'));
361
- try {
362
- return await fn(dir);
363
- }
364
- finally {
365
- await fs.rm(dir, { recursive: true, force: true }).catch(() => { });
366
- }
367
- }
368
- async stampIdentity(repo) {
369
- await this.git(repo, ['config', 'user.name', BOT_NAME]);
370
- await this.git(repo, ['config', 'user.email', BOT_EMAIL]);
371
- }
372
- /** Copy the entire template tree into `dest` (roles.yaml isn't in it — it's generated). */
373
- async copyTemplateTree(dest) {
374
- const walk = async (relDir) => {
375
- const abs = path.join(this.kbTemplateDir, relDir);
376
- const entries = await fs.readdir(abs, { withFileTypes: true });
377
- for (const entry of entries) {
378
- const rel = relDir ? path.join(relDir, entry.name) : entry.name;
379
- if (entry.isDirectory()) {
380
- await walk(rel);
381
- }
382
- else {
383
- await this.copyTemplateFile(rel, dest);
384
- }
385
- }
386
- };
387
- await walk('');
388
- }
389
- /** Copy one template file (by repo-relative path) into `dest`, creating parents. */
390
- /**
391
- * Whether the repo's copy of `relPath` differs from the template's, modulo
392
- * line endings — a CRLF checkout of identical content must read as "same",
393
- * or the managed-file refresh would commit churn on every boot forever.
394
- */
395
- async templateDiffers(repoDir, relPath) {
396
- const norm = (text) => text.replace(/\r\n?/g, '\n');
397
- const [current, template] = await Promise.all([
398
- fs.readFile(path.join(repoDir, relPath), 'utf8'),
399
- fs.readFile(path.join(this.kbTemplateDir, relPath), 'utf8'),
400
- ]);
401
- return norm(current) !== norm(template);
402
- }
403
- async copyTemplateFile(relPath, dest) {
404
- const from = path.join(this.kbTemplateDir, relPath);
405
- const to = path.join(dest, relPath);
406
- await fs.mkdir(path.dirname(to), { recursive: true });
407
- await fs.copyFile(from, to);
408
- }
409
- /**
410
- * Ensure `.bevelignore` carries `pattern`, appending it when absent. Returns
411
- * the paths changed, for the commit.
412
- *
413
- * APPENDS — never rewrites. The file is the operator's, and every rule
414
- * already in it is theirs to keep; this adds one line under a comment saying
415
- * where it came from. Absent file, or a file that already lists the pattern,
416
- * is a no-op, so running it on every clone changes nothing after the first.
417
- *
418
- * Matched line-wise rather than by substring: a rule for `Plugins/AGENTS.md`
419
- * is not a rule for the root `AGENTS.md`, and treating it as one would leave
420
- * the mismatch this exists to close.
421
- */
422
- async mergeIgnorePattern(repoDir, pattern) {
423
- const ignorePath = path.join(repoDir, IGNORE_FILENAME);
424
- let current;
425
- try {
426
- current = await fs.readFile(ignorePath, 'utf8');
427
- }
428
- catch {
429
- return []; // No ignore file — the template's copy arrives with the pattern in it.
430
- }
431
- const lines = current.split('\n').map((l) => l.trim());
432
- if (lines.includes(pattern))
433
- return [];
434
- const separator = current.endsWith('\n') ? '' : '\n';
435
- await fs.appendFile(ignorePath, `${separator}\n# Added by the platform: the conventions doc is not node content.\n${pattern}\n`, 'utf8');
436
- return [IGNORE_FILENAME];
437
- }
438
- /**
439
- * Create any reserved root folder this repo is missing, as an empty
440
- * `<dir>/.gitkeep`. Returns the paths added, for the commit message.
441
- *
442
- * WRITTEN, not copied from the template. A `.gitkeep` is empty by definition,
443
- * and requiring a template entry per root would mean a distribution could not
444
- * reserve one without forking the packaged template.
445
- *
446
- * Keyed on the DIRECTORY's existence, not the `.gitkeep` file — a branch that
447
- * already has content under `KnowledgeBase/` never gets a pointless
448
- * placeholder alongside it.
449
- */
450
- async ensureRequiredDirs(repoDir) {
451
- const added = [];
452
- for (const rootDir of this.requiredDirs) {
453
- const abs = path.join(repoDir, rootDir);
454
- // `lstat`, not `exists`: `fs.access` answers "is there something here?",
455
- // which is true of a FILE named `Plugins` — and the old skip-if-present
456
- // check then did nothing and reported success, leaving a knowledge base
457
- // permanently missing a root it claims to guarantee. `lstat` rather than
458
- // `stat` so a SYMLINK is rejected too: a link named `Plugins` is not a KB
459
- // layout, and one pointing outside the repo would make every later write
460
- // into it land somewhere nobody asked for.
461
- const found = await this.lstatOrNull(abs);
462
- if (found) {
463
- if (found.isDirectory())
464
- continue;
465
- throw new Error(`KB root "${rootDir}" exists but is not a directory ` +
466
- `(${found.isSymbolicLink() ? 'symlink' : 'file'}). Remove or rename it — ` +
467
- 'the platform requires this name to be a folder.');
468
- }
469
- await fs.mkdir(abs, { recursive: true });
470
- await fs.writeFile(path.join(abs, '.gitkeep'), '', 'utf8');
471
- added.push(`${rootDir}/.gitkeep`);
472
- }
473
- return added;
474
- }
475
- /** `lstat` without the throw — null when nothing is at `p`. */
476
- async lstatOrNull(p) {
477
- try {
478
- return await fs.lstat(p);
479
- }
480
- catch {
481
- return null;
482
- }
483
- }
484
- /** `git init` a temp repo, lay down the full template + generated roles.yaml, commit. */
485
- async buildSeedCommit(dir) {
486
- await this.git(dir, ['init', '-b', this.defaultBranch()]);
487
- await this.stampIdentity(dir);
488
- await this.copyTemplateTree(dir);
489
- // Reserved roots the template does not carry. Without this the seed commit
490
- // would hold only what the template has, and a distribution's own roots
491
- // would appear a step later, when the first clone gets topped up — the same
492
- // folders, arriving in a second commit for no reason.
493
- await this.ensureRequiredDirs(dir);
494
- await fs.writeFile(path.join(dir, 'roles.yaml'), renderRolesYaml(this.seedAdminEmails), 'utf8');
495
- await this.git(dir, ['add', '-A']);
496
- await this.git(dir, ['commit', '-m', 'Seed knowledge base from Bevel template']);
497
- }
498
- /** Empty remote → one seed commit pushed to every protected branch. */
499
- async seedEmptyRemote() {
500
- await this.withTempDir(async (dir) => {
501
- await this.buildSeedCommit(dir);
502
- // Point every protected branch at the seed commit. The init branch is
503
- // already `defaultBranch`; create the rest as refs to HEAD.
504
- for (const branch of this.protectedBranches()) {
505
- if (branch !== this.defaultBranch()) {
506
- await this.git(dir, ['branch', branch]);
507
- }
508
- }
509
- await this.git(dir, ['remote', 'add', 'origin', this.kbRepoUrl()]);
510
- // Push only the protected branches — never the stray init branch if it
511
- // isn't itself protected.
512
- await this.git(dir, [...this.credArgs(), 'push', '-u', 'origin', ...this.protectedBranches()]);
513
- console.log(`[kb-seed] Seeded empty KB remote with branches: ${this.protectedBranches().join(', ')}`);
514
- });
515
- }
516
- /** Create a missing protected branch on the remote, pointed at `base`'s tip. */
517
- async createBranchOnRemote(branch, base) {
518
- await this.withTempDir(async (dir) => {
519
- await this.git(dir, [...this.credArgs(), 'clone', '--depth', '1', '-b', base, this.kbRepoUrl(), dir]);
520
- // Push base's fetched tip up under the new branch name.
521
- await this.git(dir, [...this.credArgs(), 'push', 'origin', `HEAD:refs/heads/${branch}`]);
522
- console.log(`[kb-seed] Created missing protected branch "${branch}" from "${base}"`);
523
- });
524
- }
525
- async exists(p) {
526
- try {
527
- await fs.access(p);
528
- return true;
529
- }
530
- catch {
531
- return false;
532
- }
533
- }
534
- }
535
- //# sourceMappingURL=kb-seed.service.js.map