@bevel-software/platform-core-backend 0.4.1 → 0.5.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.
- package/dist/core/core-ports.d.ts +15 -0
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +40 -6
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +17 -1
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +153 -29
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core-config.d.ts +70 -21
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +134 -58
- package/dist/core-config.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +6 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +74 -5
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +116 -15
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-declarations.d.ts +93 -0
- package/dist/modules/access/access-declarations.d.ts.map +1 -0
- package/dist/modules/access/access-declarations.js +168 -0
- package/dist/modules/access/access-declarations.js.map +1 -0
- package/dist/modules/access/access-mutation.service.js +4 -4
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-splice.d.ts +16 -1
- package/dist/modules/access/access-splice.d.ts.map +1 -1
- package/dist/modules/access/access-splice.js +40 -21
- package/dist/modules/access/access-splice.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +51 -0
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/creator-access.d.ts +8 -0
- package/dist/modules/access/creator-access.d.ts.map +1 -1
- package/dist/modules/access/creator-access.js +21 -2
- package/dist/modules/access/creator-access.js.map +1 -1
- package/dist/modules/admin/admin-access.service.d.ts +1 -1
- package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
- package/dist/modules/admin/admin-access.service.js +3 -2
- package/dist/modules/admin/admin-access.service.js.map +1 -1
- package/dist/modules/auth/auth.routes.d.ts.map +1 -1
- package/dist/modules/auth/auth.routes.js +50 -1
- package/dist/modules/auth/auth.routes.js.map +1 -1
- package/dist/modules/auth/auth.service.d.ts +39 -10
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +38 -16
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +124 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +45 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/groups/group-provision.service.d.ts +130 -0
- package/dist/modules/groups/group-provision.service.d.ts.map +1 -0
- package/dist/modules/groups/group-provision.service.js +288 -0
- package/dist/modules/groups/group-provision.service.js.map +1 -0
- package/dist/modules/groups/groups.contract.d.ts +106 -0
- package/dist/modules/groups/groups.contract.d.ts.map +1 -0
- package/dist/modules/groups/groups.contract.js +36 -0
- package/dist/modules/groups/groups.contract.js.map +1 -0
- package/dist/modules/groups/groups.routes.d.ts +42 -0
- package/dist/modules/groups/groups.routes.d.ts.map +1 -0
- package/dist/modules/groups/groups.routes.js +379 -0
- package/dist/modules/groups/groups.routes.js.map +1 -0
- package/dist/modules/groups/groups.service.d.ts +60 -0
- package/dist/modules/groups/groups.service.d.ts.map +1 -0
- package/dist/modules/groups/groups.service.js +172 -0
- package/dist/modules/groups/groups.service.js.map +1 -0
- package/dist/modules/groups/index.d.ts +7 -0
- package/dist/modules/groups/index.d.ts.map +1 -0
- package/dist/modules/groups/index.js +6 -0
- package/dist/modules/groups/index.js.map +1 -0
- package/dist/modules/groups/join-proposals.d.ts +53 -0
- package/dist/modules/groups/join-proposals.d.ts.map +1 -0
- package/dist/modules/groups/join-proposals.js +67 -0
- package/dist/modules/groups/join-proposals.js.map +1 -0
- package/dist/modules/groups/join-requests.service.d.ts +81 -0
- package/dist/modules/groups/join-requests.service.d.ts.map +1 -0
- package/dist/modules/groups/join-requests.service.js +135 -0
- package/dist/modules/groups/join-requests.service.js.map +1 -0
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +20 -0
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.js +59 -11
- package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +129 -0
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -0
- package/dist/modules/settings/deployment-settings.service.js +368 -0
- package/dist/modules/settings/deployment-settings.service.js.map +1 -0
- package/dist/modules/settings/setup.routes.d.ts +49 -0
- package/dist/modules/settings/setup.routes.d.ts.map +1 -0
- package/dist/modules/settings/setup.routes.js +292 -0
- package/dist/modules/settings/setup.routes.js.map +1 -0
- package/dist/modules/skills/index.d.ts +2 -1
- package/dist/modules/skills/index.d.ts.map +1 -1
- package/dist/modules/skills/index.js +1 -0
- package/dist/modules/skills/index.js.map +1 -1
- package/dist/modules/skills/pending-skills.service.d.ts +40 -0
- package/dist/modules/skills/pending-skills.service.d.ts.map +1 -0
- package/dist/modules/skills/pending-skills.service.js +147 -0
- package/dist/modules/skills/pending-skills.service.js.map +1 -0
- package/dist/modules/skills/skills.contract.d.ts +32 -3
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.contract.js +1 -1
- package/dist/modules/skills/skills.routes.d.ts +3 -2
- package/dist/modules/skills/skills.routes.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.js +18 -1
- package/dist/modules/skills/skills.routes.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +22 -0
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +24 -12
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/tool-manuals/index.d.ts +1 -1
- package/dist/modules/tool-manuals/index.d.ts.map +1 -1
- package/dist/modules/tool-manuals/index.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +47 -2
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +4 -0
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.js +23 -0
- package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +3 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +84 -17
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -2
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +4 -2
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/event-bus.d.ts +11 -0
- package/dist/modules/workflow/event-bus.d.ts.map +1 -1
- package/dist/modules/workflow/event-bus.js +22 -0
- package/dist/modules/workflow/event-bus.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +9 -0
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +16 -0
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/branch-name.d.ts +1 -1
- package/dist/modules/workflow/git/branch-name.d.ts.map +1 -1
- package/dist/modules/workflow/git/branch-name.js +1 -1
- package/dist/modules/workflow/git/branch-name.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +31 -2
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +144 -52
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts +12 -6
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +19 -9
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +95 -14
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +92 -3
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +381 -108
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/bevel-ignore.d.ts +5 -0
- package/dist/modules/workspace/bevel-ignore.d.ts.map +1 -1
- package/dist/modules/workspace/bevel-ignore.js +5 -1
- package/dist/modules/workspace/bevel-ignore.js.map +1 -1
- package/dist/modules/workspace/kb-seed.interface.d.ts +1 -1
- package/dist/modules/workspace/kb-seed.service.d.ts +60 -5
- package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
- package/dist/modules/workspace/kb-seed.service.js +228 -51
- package/dist/modules/workspace/kb-seed.service.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts +2 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +51 -18
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +16 -4
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +36 -9
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +8 -3
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/shared/kb-layout.test.js +2 -2
- package/dist/test-setup.d.ts +2 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +18 -0
- package/dist/test-setup.js.map +1 -0
- package/kb-template/.bevelignore +6 -2
- package/kb-template/AGENTS.md +272 -0
- package/kb-template/KnowledgeBase/How to get started.md +65 -0
- package/kb-template/access.md +36 -36
- package/migrations/0002_user_onboarding_done.sql +1 -0
- package/migrations/0003_deployment_settings.sql +9 -0
- package/migrations/meta/0001_snapshot.json +1486 -0
- package/migrations/meta/0002_snapshot.json +1493 -0
- package/migrations/meta/0003_snapshot.json +1552 -0
- package/migrations/meta/_journal.json +15 -1
- package/package.json +3 -3
- package/src/__tests__/branch-model.test.ts +79 -0
- package/src/__tests__/core-config.admin.test.ts +114 -0
- package/src/__tests__/database-url.test.ts +70 -0
- package/src/core/core-ports.ts +15 -0
- package/src/core/create-core-server.ts +55 -6
- package/src/core/create-core-services.ts +201 -36
- package/src/core-config.ts +139 -59
- package/src/modules/access/__tests__/access-declarations.test.ts +195 -0
- package/src/modules/access/__tests__/access-md-format.test.ts +191 -0
- package/src/modules/access/__tests__/access-splice.test.ts +21 -0
- package/src/modules/access/__tests__/access.routes.overrides.test.ts +214 -0
- package/src/modules/access/access-control.interface.ts +11 -0
- package/src/modules/access/access-control.service.ts +113 -15
- package/src/modules/access/access-declarations.ts +251 -0
- package/src/modules/access/access-mutation.service.ts +4 -4
- package/src/modules/access/access-splice.ts +64 -21
- package/src/modules/access/access.routes.ts +61 -0
- package/src/modules/access/creator-access.ts +22 -2
- package/src/modules/admin/admin-access.service.ts +4 -3
- package/src/modules/auth/__tests__/auth.service.test.ts +57 -0
- package/src/modules/auth/__tests__/onboarding-done.route.test.ts +147 -0
- package/src/modules/auth/auth.routes.ts +50 -1
- package/src/modules/auth/auth.service.ts +55 -17
- package/src/modules/database/core-schema.ts +46 -0
- package/src/modules/groups/__tests__/group-index.service.test.ts +225 -0
- package/src/modules/groups/__tests__/group-provision.service.test.ts +247 -0
- package/src/modules/groups/__tests__/groups.routes.test.ts +493 -0
- package/src/modules/groups/__tests__/join-proposals.test.ts +101 -0
- package/src/modules/groups/__tests__/join-requests.service.test.ts +175 -0
- package/src/modules/groups/group-provision.service.ts +328 -0
- package/src/modules/groups/groups.contract.ts +107 -0
- package/src/modules/groups/groups.routes.ts +433 -0
- package/src/modules/groups/groups.service.ts +193 -0
- package/src/modules/groups/index.ts +17 -0
- package/src/modules/groups/join-proposals.ts +114 -0
- package/src/modules/groups/join-requests.service.ts +185 -0
- package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +283 -0
- package/src/modules/secrets-vault/secrets-vault.routes.ts +68 -12
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +256 -0
- package/src/modules/settings/__tests__/setup.routes.test.ts +201 -0
- package/src/modules/settings/deployment-settings.service.ts +419 -0
- package/src/modules/settings/setup.routes.ts +317 -0
- package/src/modules/skills/__tests__/pending-skills.service.test.ts +240 -0
- package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
- package/src/modules/skills/index.ts +3 -0
- package/src/modules/skills/pending-skills.service.ts +168 -0
- package/src/modules/skills/skills.contract.ts +34 -3
- package/src/modules/skills/skills.routes.ts +23 -2
- package/src/modules/skills/skills.service.ts +24 -16
- package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +262 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +61 -23
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +156 -156
- package/src/modules/tool-manuals/index.ts +2 -0
- package/src/modules/tool-manuals/tool-manuals.contract.ts +50 -2
- package/src/modules/tool-manuals/tool-manuals.routes.ts +21 -0
- package/src/modules/tool-manuals/tool-manuals.service.ts +86 -17
- package/src/modules/tool-manuals/tool-manuals.tools.ts +206 -202
- package/src/modules/workflow/__tests__/event-bus.test.ts +35 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +251 -5
- package/src/modules/workflow/agent-tools/workflow.tools.ts +4 -2
- package/src/modules/workflow/event-bus.ts +25 -0
- package/src/modules/workflow/file-lock.service.ts +17 -0
- package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +9 -49
- package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +101 -0
- package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +219 -211
- package/src/modules/workflow/git/__tests__/git.service.fileContentsAtCommit.test.ts +132 -0
- package/src/modules/workflow/git/__tests__/git.service.logForFile.test.ts +4 -2
- package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +275 -272
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +5 -0
- package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +44 -0
- package/src/modules/workflow/git/branch-name.ts +5 -1
- package/src/modules/workflow/git/git.service.ts +159 -62
- package/src/modules/workflow/git/pull-request.service.ts +22 -11
- package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +4 -1
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
- package/src/modules/workflow/workflow.routes.ts +91 -13
- package/src/modules/workflow/workflow.service.ts +353 -13
- package/src/modules/workspace/__tests__/kb-seed.service.test.ts +255 -24
- package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -233
- package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -176
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -303
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -276
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +14 -6
- package/src/modules/workspace/bevel-ignore.ts +66 -62
- package/src/modules/workspace/kb-seed.interface.ts +1 -1
- package/src/modules/workspace/kb-seed.service.ts +247 -53
- package/src/modules/workspace/workspace.routes.ts +54 -18
- package/src/modules/workspace/workspace.service.ts +45 -7
- package/src/modules/workspace/workspace.tools.ts +922 -917
- package/src/shared/__tests__/frontmatter-id.test.ts +53 -0
- package/src/shared/__tests__/join-request.test.ts +96 -0
- package/src/shared/__tests__/kb-layout.group.test.ts +45 -0
- package/src/shared/kb-layout.test.ts +2 -2
- package/src/test-setup.ts +18 -0
- package/kb-template/Agents/README.md +0 -38
- package/kb-template/CLAUDE.md +0 -419
- package/kb-template/Data/.gitkeep +0 -0
- package/kb-template/Data/README.md +0 -39
- package/kb-template/KnowledgeBase/README.md +0 -37
- package/kb-template/Pipelines/.gitkeep +0 -0
- package/kb-template/Pipelines/README.md +0 -38
- package/kb-template/Skills/.gitkeep +0 -0
- package/kb-template/Skills/README.md +0 -32
- package/kb-template/Tools/.gitkeep +0 -0
- package/kb-template/Tools/README.md +0 -42
- /package/kb-template/{Agents → Groups}/.gitkeep +0 -0
|
@@ -338,25 +338,33 @@ describe('workspace file primitives', () => {
|
|
|
338
338
|
expect(res.stdout.trim()).toBe('kb-seed');
|
|
339
339
|
});
|
|
340
340
|
|
|
341
|
-
|
|
341
|
+
// POSIX-only probes: `$GIT_DIR` expansion and `>/dev/null` are `sh`
|
|
342
|
+
// semantics, and `shell: true` on Windows runs cmd.exe, where the probe
|
|
343
|
+
// itself (not the scoping under test) is meaningless. CI runs Linux.
|
|
344
|
+
it.skipIf(process.platform === 'win32')('execute_command does not leak GIT_DIR into non-git commands', async () => {
|
|
342
345
|
const base = await start();
|
|
343
346
|
// The KB-clone GIT_DIR/GIT_WORK_TREE override is scoped to bare `git …`
|
|
344
347
|
// only; a non-git command (e.g. npm/pip that shells git internally) must
|
|
345
348
|
// NOT inherit it, or the nested git child would target the KB clone.
|
|
346
|
-
|
|
349
|
+
// Probe via `node -e` rather than shell expansion — the override lives in
|
|
350
|
+
// the spawn env, and `$GIT_DIR` syntax doesn't expand under cmd.exe.
|
|
351
|
+
const res = (await (await post(`${base}/api/agent/tools/execute_command`, { branch: 'main', command: `node -e "console.log('GIT_DIR=['+(process.env.GIT_DIR||'')+']')"` })).json()) as { stdout: string; stderr: string; exitCode: number };
|
|
347
352
|
expect(res.exitCode).toBe(0);
|
|
348
353
|
expect(res.stdout.trim()).toBe('GIT_DIR=[]');
|
|
349
354
|
});
|
|
350
355
|
|
|
351
|
-
it('execute_command does not leak GIT_DIR into a chained step after git', async () => {
|
|
356
|
+
it.skipIf(process.platform === 'win32')('execute_command does not leak GIT_DIR into a chained step after git', async () => {
|
|
352
357
|
const base = await start();
|
|
353
358
|
// Runs under `shell: true`, so a command that merely STARTS with git but
|
|
354
359
|
// chains another step must not export the KB git env to the whole shell —
|
|
355
360
|
// otherwise `git … && npm ci` would leak the KB repo context into the npm/pip
|
|
356
|
-
// git subprocess. `git --version` needs no repo, so exit stays 0.
|
|
357
|
-
|
|
361
|
+
// git subprocess. `git --version` needs no repo, so exit stays 0. The
|
|
362
|
+
// version line lands on stdout (`>/dev/null` isn't portable to cmd.exe),
|
|
363
|
+
// so the leak probe is asserted on the LAST line.
|
|
364
|
+
const res = (await (await post(`${base}/api/agent/tools/execute_command`, { branch: 'main', command: `git --version && node -e "console.log('leak=['+(process.env.GIT_DIR||'')+']')"` })).json()) as { stdout: string; stderr: string; exitCode: number };
|
|
358
365
|
expect(res.exitCode).toBe(0);
|
|
359
|
-
|
|
366
|
+
const lines = res.stdout.trim().split(/\r?\n/);
|
|
367
|
+
expect(lines[lines.length - 1].trim()).toBe('leak=[]');
|
|
360
368
|
});
|
|
361
369
|
|
|
362
370
|
it('read scope refuses write tools (403) but allows reads', async () => {
|
|
@@ -1,62 +1,66 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import ignore, { type Ignore } from 'ignore';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import ignore, { type Ignore } from 'ignore';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The ignore file's own name. Exported because the file-tree filter hides it
|
|
7
|
+
* from non-admins, and both places must agree on the spelling.
|
|
8
|
+
*/
|
|
9
|
+
export const IGNORE_FILENAME = '.bevelignore';
|
|
10
|
+
|
|
11
|
+
/** One .bevelignore file's rules, scoped to the directory it lives in. */
|
|
12
|
+
interface IgnoreLayer {
|
|
13
|
+
/** Absolute path of the directory where the .bevelignore lives. */
|
|
14
|
+
readonly root: string;
|
|
15
|
+
readonly matcher: Ignore;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A hierarchical stack of `.bevelignore` rule sets.
|
|
20
|
+
*
|
|
21
|
+
* Each `.bevelignore` file applies to paths beneath the directory it lives in,
|
|
22
|
+
* using standard gitignore syntax (negations, `**`, directory-only patterns, anchoring).
|
|
23
|
+
* Deeper files combine with — and can override — rules from shallower files,
|
|
24
|
+
* mirroring how git layers `.gitignore` files.
|
|
25
|
+
*/
|
|
26
|
+
export class BevelIgnoreStack {
|
|
27
|
+
private constructor(private readonly layers: readonly IgnoreLayer[]) {}
|
|
28
|
+
|
|
29
|
+
static empty(): BevelIgnoreStack {
|
|
30
|
+
return new BevelIgnoreStack([]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Return a new stack that additionally includes a `.bevelignore` from `dir`
|
|
35
|
+
* (if present). If no `.bevelignore` exists in `dir`, returns this stack unchanged.
|
|
36
|
+
*/
|
|
37
|
+
async extendedWith(dir: string): Promise<BevelIgnoreStack> {
|
|
38
|
+
const file = path.join(dir, IGNORE_FILENAME);
|
|
39
|
+
let contents: string;
|
|
40
|
+
try {
|
|
41
|
+
contents = await fs.readFile(file, 'utf-8');
|
|
42
|
+
} catch {
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
const matcher = ignore().add(contents);
|
|
46
|
+
return new BevelIgnoreStack([...this.layers, { root: dir, matcher }]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether the given entry should be hidden from the file tree.
|
|
51
|
+
* @param absolutePath absolute path of the entry
|
|
52
|
+
* @param isDirectory needed because gitignore patterns ending in `/` match directories only
|
|
53
|
+
*/
|
|
54
|
+
isIgnored(absolutePath: string, isDirectory: boolean): boolean {
|
|
55
|
+
for (const layer of this.layers) {
|
|
56
|
+
const rel = path.relative(layer.root, absolutePath).replace(/\\/g, '/');
|
|
57
|
+
// Skip entries that aren't under this layer's root (shouldn't happen in normal
|
|
58
|
+
// recursion, but guards against absolute-path quirks on Windows).
|
|
59
|
+
if (!rel || rel.startsWith('..')) continue;
|
|
60
|
+
// Append trailing slash for directories so `foo/`-style rules match correctly.
|
|
61
|
+
const probe = isDirectory ? `${rel}/` : rel;
|
|
62
|
+
if (layer.matcher.ignores(probe)) return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -15,7 +15,7 @@ export interface IKbSeedService {
|
|
|
15
15
|
* work runs at most once per process.
|
|
16
16
|
*
|
|
17
17
|
* - **Empty remote** → full seed: all template files (incl. the sample
|
|
18
|
-
* ontology) committed to every protected branch.
|
|
18
|
+
* ontology) committed to every protected branch. Uses `ADMIN_EMAIL`
|
|
19
19
|
* (a repo with no Admin is unusable), otherwise throws.
|
|
20
20
|
* - **Existing remote** → create only the protected branches that don't exist
|
|
21
21
|
* yet, off an existing one. No file changes here — per-branch base files are
|
|
@@ -3,14 +3,8 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
DATA_DIR,
|
|
9
|
-
KNOWLEDGE_BASE_DIR,
|
|
10
|
-
PIPELINES_DIR,
|
|
11
|
-
SKILLS_DIR,
|
|
12
|
-
TOOLS_DIR,
|
|
13
|
-
} from '@bevel-software/platform-shared';
|
|
6
|
+
import { KNOWLEDGE_BASE_DIR, GROUPS_DIR } from '@bevel-software/platform-shared';
|
|
7
|
+
import { IGNORE_FILENAME } from './bevel-ignore.js';
|
|
14
8
|
import type { IKbSeedService } from './kb-seed.interface.js';
|
|
15
9
|
|
|
16
10
|
const execFileAsync = promisify(execFile);
|
|
@@ -29,24 +23,57 @@ const BOT_EMAIL = 'bevel-workflow@bevel.software';
|
|
|
29
23
|
*
|
|
30
24
|
* Two kinds:
|
|
31
25
|
* - {@link REQUIRED_FILES}: repo-root files added when the file is missing.
|
|
32
|
-
* - {@link
|
|
33
|
-
* dir is entirely absent it's created by adding
|
|
34
|
-
* the *directory's* existence, not the
|
|
35
|
-
*
|
|
26
|
+
* - {@link CORE_REQUIRED_DIRS} (plus any `extraDirs`): well-known root dirs the
|
|
27
|
+
* app expects to exist; when a dir is entirely absent it's created by adding
|
|
28
|
+
* its `<dir>/.gitkeep`. Keyed on the *directory's* existence, not the
|
|
29
|
+
* `.gitkeep` file — so a branch that already has content under
|
|
30
|
+
* `KnowledgeBase/` never gets a pointless placeholder.
|
|
36
31
|
*
|
|
37
32
|
* `roles.yaml` is in neither, and is not part of the template at all: it is
|
|
38
|
-
* generated from `
|
|
33
|
+
* generated from `ADMIN_EMAIL` (see {@link renderRolesYaml}) and written
|
|
39
34
|
* directly, so a repo can't be seeded with a stale hard-coded Admin list.
|
|
40
35
|
*/
|
|
41
|
-
const REQUIRED_FILES: readonly string[] = ['access.md', '
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
36
|
+
const REQUIRED_FILES: readonly string[] = ['access.md', 'AGENTS.md', '.bevelignore', '.gitignore'];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The two roots CORE gives a knowledge base: the ontologies, and the groups
|
|
40
|
+
* that hold skills and tools.
|
|
41
|
+
*
|
|
42
|
+
* `Data/`, `Agents/` and `Pipelines/` are deliberately absent. They scaffold
|
|
43
|
+
* the agentic execution layer, which is not part of this platform — a core
|
|
44
|
+
* deployment that created them would be handing every operator three empty
|
|
45
|
+
* folders it has no feature to fill. A distribution that DOES own that layer
|
|
46
|
+
* passes them as `extraDirs` (and ships a template carrying their READMEs);
|
|
47
|
+
* the names stay reserved in `kb-layout.ts` either way, so a KB that has them
|
|
48
|
+
* still renders them as roots rather than folding them into Knowledge.
|
|
49
|
+
*/
|
|
50
|
+
export const CORE_REQUIRED_DIRS: readonly string[] = [KNOWLEDGE_BASE_DIR, GROUPS_DIR];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A reserved root must be ONE path segment — `Data`, not `Data/x`, `../x` or
|
|
54
|
+
* `/x`. The name is joined onto the repo root, so anything else writes outside
|
|
55
|
+
* the repo being seeded.
|
|
56
|
+
*
|
|
57
|
+
* Deliberately NOT a check against the reserved-root set in `kb-layout.ts`:
|
|
58
|
+
* `Data`, `Agents` and `Pipelines` are all in that set, and they are precisely
|
|
59
|
+
* what a distribution passes here. Being reserved is what makes a name worth
|
|
60
|
+
* claiming — the file tree renders it as its own root instead of folding it
|
|
61
|
+
* into Knowledge — so rejecting reserved names would reject the only real use.
|
|
62
|
+
*/
|
|
63
|
+
function assertRootSegment(dir: string): void {
|
|
64
|
+
if (
|
|
65
|
+
!dir ||
|
|
66
|
+
dir === '.' ||
|
|
67
|
+
dir === '..' ||
|
|
68
|
+
dir.includes('/') ||
|
|
69
|
+
dir.includes('\\') ||
|
|
70
|
+
path.isAbsolute(dir)
|
|
71
|
+
) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Reserved KB root must be a single path segment (no separators, no ".."); got "${dir}"`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
50
77
|
|
|
51
78
|
/** Redact the token from any string that might surface in a log or error. */
|
|
52
79
|
function redact(s: string): string {
|
|
@@ -62,7 +89,7 @@ function renderRolesYaml(adminEmails: readonly string[]): string {
|
|
|
62
89
|
'# Role names are case- and whitespace-insensitive. The `Admin` role is special:\n' +
|
|
63
90
|
'# only Admins may edit this file, and at least one Admin must always exist.\n' +
|
|
64
91
|
'#\n' +
|
|
65
|
-
'# Generated by the Bevel platform at KB-seed time from
|
|
92
|
+
'# Generated by the Bevel platform at KB-seed time from ADMIN_EMAIL.\n' +
|
|
66
93
|
'roles:\n' +
|
|
67
94
|
' Admin:\n' +
|
|
68
95
|
`${lines}\n`
|
|
@@ -96,15 +123,56 @@ export class KbSeedService implements IKbSeedService {
|
|
|
96
123
|
* base when creating a missing protected branch.
|
|
97
124
|
* @param seedAdminEmails Admins written into the generated `roles.yaml`.
|
|
98
125
|
* @param gitUsername Basic-auth username for git-over-HTTPS (provider-specific).
|
|
126
|
+
* @param extraDirs Additional root folders this distribution reserves,
|
|
127
|
+
* on top of {@link CORE_REQUIRED_DIRS}. Their
|
|
128
|
+
* `.gitkeep` is written directly rather than copied,
|
|
129
|
+
* so a distribution can claim a root without also
|
|
130
|
+
* shipping a template entry for it.
|
|
99
131
|
*/
|
|
100
132
|
constructor(
|
|
101
|
-
|
|
133
|
+
kbRepoUrl: string | (() => string),
|
|
102
134
|
private readonly kbTemplateDir: string,
|
|
103
|
-
|
|
104
|
-
|
|
135
|
+
protectedBranches: readonly string[] | (() => readonly string[]),
|
|
136
|
+
defaultBranch: string | (() => string),
|
|
105
137
|
private readonly seedAdminEmails: readonly string[],
|
|
106
|
-
|
|
107
|
-
|
|
138
|
+
gitUsername: string | (() => string) = 'x-access-token',
|
|
139
|
+
extraDirs: readonly string[] = [],
|
|
140
|
+
) {
|
|
141
|
+
// A getter is read per-operation, so a remote supplied through the setup
|
|
142
|
+
// screen is seeded against without restarting; a string is still accepted.
|
|
143
|
+
this.kbRepoUrl = typeof kbRepoUrl === 'function' ? kbRepoUrl : () => kbRepoUrl;
|
|
144
|
+
// Read when seeding, not at construction: the branch model can be supplied
|
|
145
|
+
// through the setup screen, which happens after this object exists.
|
|
146
|
+
this.protectedBranches =
|
|
147
|
+
typeof protectedBranches === 'function' ? protectedBranches : () => protectedBranches;
|
|
148
|
+
this.defaultBranch = typeof defaultBranch === 'function' ? defaultBranch : () => defaultBranch;
|
|
149
|
+
this.gitUsername = typeof gitUsername === 'function' ? gitUsername : () => gitUsername;
|
|
150
|
+
// NOT a getter, unlike its neighbours above: the reserved roots are named
|
|
151
|
+
// by the composition root in code, not collected on the setup screen, so
|
|
152
|
+
// there is nothing to re-read. Validated once, here — every entry is joined
|
|
153
|
+
// onto the repo root and onto `<dir>/.gitkeep`, so a separator or a `..`
|
|
154
|
+
// would write outside the repo being seeded, and a bad value should fail at
|
|
155
|
+
// boot beside the rest of the wiring rather than part-way through seeding
|
|
156
|
+
// somebody's knowledge base. Same contract as `KB_DIR_NAME` in `CoreConfig`.
|
|
157
|
+
for (const dir of extraDirs) {
|
|
158
|
+
assertRootSegment(dir);
|
|
159
|
+
// A root named after a required FILE is a typo with a silent outcome:
|
|
160
|
+
// the file is laid down first in both seed paths, so `ensureRequiredDirs`
|
|
161
|
+
// finds the path taken and skips it, and the directory the caller asked
|
|
162
|
+
// for never appears with nothing said about why.
|
|
163
|
+
if (REQUIRED_FILES.includes(dir)) {
|
|
164
|
+
throw new Error(`Reserved KB root "${dir}" collides with a required file of the same name`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
this.requiredDirs = [...CORE_REQUIRED_DIRS, ...extraDirs];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private readonly kbRepoUrl: () => string;
|
|
171
|
+
private readonly protectedBranches: () => readonly string[];
|
|
172
|
+
private readonly defaultBranch: () => string;
|
|
173
|
+
private readonly gitUsername: () => string;
|
|
174
|
+
/** Root folders this deployment guarantees — core's two plus any extras. */
|
|
175
|
+
private readonly requiredDirs: readonly string[];
|
|
108
176
|
|
|
109
177
|
ensureRemoteSeeded(): Promise<void> {
|
|
110
178
|
// Cache the promise, not just a boolean, so concurrent first-callers share
|
|
@@ -125,9 +193,11 @@ export class KbSeedService implements IKbSeedService {
|
|
|
125
193
|
if (heads.size === 0) {
|
|
126
194
|
if (this.seedAdminEmails.length === 0) {
|
|
127
195
|
throw new Error(
|
|
128
|
-
`KB remote ${redact(this.kbRepoUrl)} is empty and cannot be seeded: ` +
|
|
129
|
-
'
|
|
130
|
-
'
|
|
196
|
+
`KB remote ${redact(this.kbRepoUrl())} is empty and cannot be seeded: ` +
|
|
197
|
+
'no initial Admin was supplied. A seeded KB with no Admin is unusable ' +
|
|
198
|
+
'(access resolution requires at least one Admin). Unreachable in ' +
|
|
199
|
+
'normal operation — ADMIN_EMAIL is required at boot — so this guards ' +
|
|
200
|
+
'a caller that constructed the seeder with an empty list.',
|
|
131
201
|
);
|
|
132
202
|
}
|
|
133
203
|
await this.seedEmptyRemote();
|
|
@@ -138,7 +208,7 @@ export class KbSeedService implements IKbSeedService {
|
|
|
138
208
|
// later `clone -b <branch>` succeeds. This creates the branch pointer only;
|
|
139
209
|
// per-branch file top-up happens in `topUpWorkspace` when the branch loads.
|
|
140
210
|
const base = this.pickBase(heads);
|
|
141
|
-
for (const branch of this.protectedBranches) {
|
|
211
|
+
for (const branch of this.protectedBranches()) {
|
|
142
212
|
if (!heads.has(branch)) {
|
|
143
213
|
await this.createBranchOnRemote(branch, base);
|
|
144
214
|
}
|
|
@@ -155,21 +225,42 @@ export class KbSeedService implements IKbSeedService {
|
|
|
155
225
|
* stays consistent with origin, and the top-up is retried on a future clone.
|
|
156
226
|
*/
|
|
157
227
|
async topUpWorkspace(repoDir: string, branch: string): Promise<void> {
|
|
228
|
+
// Scaffolding lands on PROTECTED branches only. A draft or suggestions
|
|
229
|
+
// branch is somebody's change-in-waiting, and a seeder commit there
|
|
230
|
+
// surfaces as noise in their change request's diff against the default
|
|
231
|
+
// branch (a stray scaffolding file riding along in a skill proposal was
|
|
232
|
+
// exactly this). Whatever the protected branches are missing, they get
|
|
233
|
+
// when THEY load — and drafts fork from them.
|
|
234
|
+
if (!this.protectedBranches().includes(branch)) return;
|
|
158
235
|
try {
|
|
159
236
|
const added: string[] = [];
|
|
160
237
|
for (const rel of REQUIRED_FILES) {
|
|
161
238
|
if (!(await this.exists(path.join(repoDir, rel)))) {
|
|
162
239
|
await this.copyTemplateFile(rel, repoDir);
|
|
163
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') added.push(...(await this.mergeIgnorePattern(repoDir, rel)));
|
|
164
247
|
}
|
|
165
248
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
249
|
+
// AGENTS.md is MANAGED, not merely seeded: the platform owns its
|
|
250
|
+
// content, and a stale copy is replaced with the packaged template's on
|
|
251
|
+
// every top-up (each fresh clone of a protected branch — in practice,
|
|
252
|
+
// every server restart). The file's own header says so, which is what
|
|
253
|
+
// makes overwriting edits a stated contract instead of a surprise.
|
|
254
|
+
let agentsRefreshed = false;
|
|
255
|
+
if (
|
|
256
|
+
!added.includes('AGENTS.md') &&
|
|
257
|
+
(await this.templateDiffers(repoDir, 'AGENTS.md'))
|
|
258
|
+
) {
|
|
259
|
+
await this.copyTemplateFile('AGENTS.md', repoDir);
|
|
260
|
+
added.push('AGENTS.md');
|
|
261
|
+
agentsRefreshed = true;
|
|
172
262
|
}
|
|
263
|
+
added.push(...(await this.ensureRequiredDirs(repoDir)));
|
|
173
264
|
if (!(await this.exists(path.join(repoDir, 'roles.yaml')))) {
|
|
174
265
|
if (this.seedAdminEmails.length > 0) {
|
|
175
266
|
await fs.writeFile(
|
|
@@ -180,9 +271,9 @@ export class KbSeedService implements IKbSeedService {
|
|
|
180
271
|
added.push('roles.yaml');
|
|
181
272
|
} else {
|
|
182
273
|
console.warn(
|
|
183
|
-
`[kb-seed] Branch "${branch}" is missing roles.yaml
|
|
184
|
-
'
|
|
185
|
-
'Admin roles.yaml exists; set
|
|
274
|
+
`[kb-seed] Branch "${branch}" is missing roles.yaml and ADMIN_EMAIL is ` +
|
|
275
|
+
'unset — leaving it absent. Access resolution will fail until an ' +
|
|
276
|
+
'Admin roles.yaml exists; set ADMIN_EMAIL or add roles.yaml manually.',
|
|
186
277
|
);
|
|
187
278
|
}
|
|
188
279
|
}
|
|
@@ -194,7 +285,11 @@ export class KbSeedService implements IKbSeedService {
|
|
|
194
285
|
// values, so this is a harmless no-op there).
|
|
195
286
|
await this.stampIdentity(repoDir);
|
|
196
287
|
await this.git(repoDir, ['add', '--', ...added]);
|
|
197
|
-
|
|
288
|
+
const message =
|
|
289
|
+
agentsRefreshed && added.length === 1
|
|
290
|
+
? 'Update AGENTS.md to the current platform template'
|
|
291
|
+
: `Add missing KB scaffolding: ${added.join(', ')}`;
|
|
292
|
+
await this.git(repoDir, ['commit', '-m', message]);
|
|
198
293
|
try {
|
|
199
294
|
await this.git(repoDir, ['push', 'origin', `HEAD:refs/heads/${branch}`]);
|
|
200
295
|
console.log(`[kb-seed] Topped up "${branch}" with: ${added.join(', ')}`);
|
|
@@ -220,8 +315,8 @@ export class KbSeedService implements IKbSeedService {
|
|
|
220
315
|
|
|
221
316
|
/** Prefer the default branch as a base, else the first protected branch present, else any head. */
|
|
222
317
|
private pickBase(heads: Set<string>): string {
|
|
223
|
-
if (heads.has(this.defaultBranch)) return this.defaultBranch;
|
|
224
|
-
const protectedPresent = this.protectedBranches.find((b) => heads.has(b));
|
|
318
|
+
if (heads.has(this.defaultBranch())) return this.defaultBranch();
|
|
319
|
+
const protectedPresent = this.protectedBranches().find((b) => heads.has(b));
|
|
225
320
|
if (protectedPresent) return protectedPresent;
|
|
226
321
|
// Deterministic pick so repeated runs behave identically.
|
|
227
322
|
return [...heads].sort()[0];
|
|
@@ -236,7 +331,7 @@ export class KbSeedService implements IKbSeedService {
|
|
|
236
331
|
// Username is provider-specific; the token is always the Basic-auth password.
|
|
237
332
|
args.push(
|
|
238
333
|
'-c',
|
|
239
|
-
`credential.helper=!f() { echo "username=${this.gitUsername}"; echo "password=$GITHUB_TOKEN"; }; f`,
|
|
334
|
+
`credential.helper=!f() { echo "username=${this.gitUsername()}"; echo "password=$GITHUB_TOKEN"; }; f`,
|
|
240
335
|
);
|
|
241
336
|
}
|
|
242
337
|
return args;
|
|
@@ -260,7 +355,7 @@ export class KbSeedService implements IKbSeedService {
|
|
|
260
355
|
|
|
261
356
|
/** Set of branch names on the remote (empty ⇒ uninitialised repo). */
|
|
262
357
|
private async lsRemoteHeads(): Promise<Set<string>> {
|
|
263
|
-
const out = await this.git(null, [...this.credArgs(), 'ls-remote', '--heads', this.kbRepoUrl]);
|
|
358
|
+
const out = await this.git(null, [...this.credArgs(), 'ls-remote', '--heads', this.kbRepoUrl()]);
|
|
264
359
|
const heads = new Set<string>();
|
|
265
360
|
for (const line of out.split('\n')) {
|
|
266
361
|
const m = line.match(/\srefs\/heads\/(.+)$/);
|
|
@@ -302,6 +397,20 @@ export class KbSeedService implements IKbSeedService {
|
|
|
302
397
|
}
|
|
303
398
|
|
|
304
399
|
/** Copy one template file (by repo-relative path) into `dest`, creating parents. */
|
|
400
|
+
/**
|
|
401
|
+
* Whether the repo's copy of `relPath` differs from the template's, modulo
|
|
402
|
+
* line endings — a CRLF checkout of identical content must read as "same",
|
|
403
|
+
* or the managed-file refresh would commit churn on every boot forever.
|
|
404
|
+
*/
|
|
405
|
+
private async templateDiffers(repoDir: string, relPath: string): Promise<boolean> {
|
|
406
|
+
const norm = (text: string) => text.replace(/\r\n?/g, '\n');
|
|
407
|
+
const [current, template] = await Promise.all([
|
|
408
|
+
fs.readFile(path.join(repoDir, relPath), 'utf8'),
|
|
409
|
+
fs.readFile(path.join(this.kbTemplateDir, relPath), 'utf8'),
|
|
410
|
+
]);
|
|
411
|
+
return norm(current) !== norm(template);
|
|
412
|
+
}
|
|
413
|
+
|
|
305
414
|
private async copyTemplateFile(relPath: string, dest: string): Promise<void> {
|
|
306
415
|
const from = path.join(this.kbTemplateDir, relPath);
|
|
307
416
|
const to = path.join(dest, relPath);
|
|
@@ -309,11 +418,96 @@ export class KbSeedService implements IKbSeedService {
|
|
|
309
418
|
await fs.copyFile(from, to);
|
|
310
419
|
}
|
|
311
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Ensure `.bevelignore` carries `pattern`, appending it when absent. Returns
|
|
423
|
+
* the paths changed, for the commit.
|
|
424
|
+
*
|
|
425
|
+
* APPENDS — never rewrites. The file is the operator's, and every rule
|
|
426
|
+
* already in it is theirs to keep; this adds one line under a comment saying
|
|
427
|
+
* where it came from. Absent file, or a file that already lists the pattern,
|
|
428
|
+
* is a no-op, so running it on every clone changes nothing after the first.
|
|
429
|
+
*
|
|
430
|
+
* Matched line-wise rather than by substring: a rule for `Groups/AGENTS.md`
|
|
431
|
+
* is not a rule for the root `AGENTS.md`, and treating it as one would leave
|
|
432
|
+
* the mismatch this exists to close.
|
|
433
|
+
*/
|
|
434
|
+
private async mergeIgnorePattern(repoDir: string, pattern: string): Promise<string[]> {
|
|
435
|
+
const ignorePath = path.join(repoDir, IGNORE_FILENAME);
|
|
436
|
+
let current: string;
|
|
437
|
+
try {
|
|
438
|
+
current = await fs.readFile(ignorePath, 'utf8');
|
|
439
|
+
} catch {
|
|
440
|
+
return []; // No ignore file — the template's copy arrives with the pattern in it.
|
|
441
|
+
}
|
|
442
|
+
const lines = current.split('\n').map((l) => l.trim());
|
|
443
|
+
if (lines.includes(pattern)) return [];
|
|
444
|
+
const separator = current.endsWith('\n') ? '' : '\n';
|
|
445
|
+
await fs.appendFile(
|
|
446
|
+
ignorePath,
|
|
447
|
+
`${separator}\n# Added by the platform: the conventions doc is not node content.\n${pattern}\n`,
|
|
448
|
+
'utf8',
|
|
449
|
+
);
|
|
450
|
+
return [IGNORE_FILENAME];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Create any reserved root folder this repo is missing, as an empty
|
|
455
|
+
* `<dir>/.gitkeep`. Returns the paths added, for the commit message.
|
|
456
|
+
*
|
|
457
|
+
* WRITTEN, not copied from the template. A `.gitkeep` is empty by definition,
|
|
458
|
+
* and requiring a template entry per root would mean a distribution could not
|
|
459
|
+
* reserve one without forking the packaged template.
|
|
460
|
+
*
|
|
461
|
+
* Keyed on the DIRECTORY's existence, not the `.gitkeep` file — a branch that
|
|
462
|
+
* already has content under `KnowledgeBase/` never gets a pointless
|
|
463
|
+
* placeholder alongside it.
|
|
464
|
+
*/
|
|
465
|
+
private async ensureRequiredDirs(repoDir: string): Promise<string[]> {
|
|
466
|
+
const added: string[] = [];
|
|
467
|
+
for (const rootDir of this.requiredDirs) {
|
|
468
|
+
const abs = path.join(repoDir, rootDir);
|
|
469
|
+
// `lstat`, not `exists`: `fs.access` answers "is there something here?",
|
|
470
|
+
// which is true of a FILE named `Groups` — and the old skip-if-present
|
|
471
|
+
// check then did nothing and reported success, leaving a knowledge base
|
|
472
|
+
// permanently missing a root it claims to guarantee. `lstat` rather than
|
|
473
|
+
// `stat` so a SYMLINK is rejected too: a link named `Groups` is not a KB
|
|
474
|
+
// layout, and one pointing outside the repo would make every later write
|
|
475
|
+
// into it land somewhere nobody asked for.
|
|
476
|
+
const found = await this.lstatOrNull(abs);
|
|
477
|
+
if (found) {
|
|
478
|
+
if (found.isDirectory()) continue;
|
|
479
|
+
throw new Error(
|
|
480
|
+
`KB root "${rootDir}" exists but is not a directory ` +
|
|
481
|
+
`(${found.isSymbolicLink() ? 'symlink' : 'file'}). Remove or rename it — ` +
|
|
482
|
+
'the platform requires this name to be a folder.',
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
await fs.mkdir(abs, { recursive: true });
|
|
486
|
+
await fs.writeFile(path.join(abs, '.gitkeep'), '', 'utf8');
|
|
487
|
+
added.push(`${rootDir}/.gitkeep`);
|
|
488
|
+
}
|
|
489
|
+
return added;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** `lstat` without the throw — null when nothing is at `p`. */
|
|
493
|
+
private async lstatOrNull(p: string): Promise<import('node:fs').Stats | null> {
|
|
494
|
+
try {
|
|
495
|
+
return await fs.lstat(p);
|
|
496
|
+
} catch {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
312
501
|
/** `git init` a temp repo, lay down the full template + generated roles.yaml, commit. */
|
|
313
502
|
private async buildSeedCommit(dir: string): Promise<void> {
|
|
314
|
-
await this.git(dir, ['init', '-b', this.defaultBranch]);
|
|
503
|
+
await this.git(dir, ['init', '-b', this.defaultBranch()]);
|
|
315
504
|
await this.stampIdentity(dir);
|
|
316
505
|
await this.copyTemplateTree(dir);
|
|
506
|
+
// Reserved roots the template does not carry. Without this the seed commit
|
|
507
|
+
// would hold only what the template has, and a distribution's own roots
|
|
508
|
+
// would appear a step later, when the first clone gets topped up — the same
|
|
509
|
+
// folders, arriving in a second commit for no reason.
|
|
510
|
+
await this.ensureRequiredDirs(dir);
|
|
317
511
|
await fs.writeFile(path.join(dir, 'roles.yaml'), renderRolesYaml(this.seedAdminEmails), 'utf8');
|
|
318
512
|
await this.git(dir, ['add', '-A']);
|
|
319
513
|
await this.git(dir, ['commit', '-m', 'Seed knowledge base from Bevel template']);
|
|
@@ -325,17 +519,17 @@ export class KbSeedService implements IKbSeedService {
|
|
|
325
519
|
await this.buildSeedCommit(dir);
|
|
326
520
|
// Point every protected branch at the seed commit. The init branch is
|
|
327
521
|
// already `defaultBranch`; create the rest as refs to HEAD.
|
|
328
|
-
for (const branch of this.protectedBranches) {
|
|
329
|
-
if (branch !== this.defaultBranch) {
|
|
522
|
+
for (const branch of this.protectedBranches()) {
|
|
523
|
+
if (branch !== this.defaultBranch()) {
|
|
330
524
|
await this.git(dir, ['branch', branch]);
|
|
331
525
|
}
|
|
332
526
|
}
|
|
333
|
-
await this.git(dir, ['remote', 'add', 'origin', this.kbRepoUrl]);
|
|
527
|
+
await this.git(dir, ['remote', 'add', 'origin', this.kbRepoUrl()]);
|
|
334
528
|
// Push only the protected branches — never the stray init branch if it
|
|
335
529
|
// isn't itself protected.
|
|
336
|
-
await this.git(dir, [...this.credArgs(), 'push', '-u', 'origin', ...this.protectedBranches]);
|
|
530
|
+
await this.git(dir, [...this.credArgs(), 'push', '-u', 'origin', ...this.protectedBranches()]);
|
|
337
531
|
console.log(
|
|
338
|
-
`[kb-seed] Seeded empty KB remote with branches: ${this.protectedBranches.join(', ')}`,
|
|
532
|
+
`[kb-seed] Seeded empty KB remote with branches: ${this.protectedBranches().join(', ')}`,
|
|
339
533
|
);
|
|
340
534
|
});
|
|
341
535
|
}
|
|
@@ -343,7 +537,7 @@ export class KbSeedService implements IKbSeedService {
|
|
|
343
537
|
/** Create a missing protected branch on the remote, pointed at `base`'s tip. */
|
|
344
538
|
private async createBranchOnRemote(branch: string, base: string): Promise<void> {
|
|
345
539
|
await this.withTempDir(async (dir) => {
|
|
346
|
-
await this.git(dir, [...this.credArgs(), 'clone', '--depth', '1', '-b', base, this.kbRepoUrl, dir]);
|
|
540
|
+
await this.git(dir, [...this.credArgs(), 'clone', '--depth', '1', '-b', base, this.kbRepoUrl(), dir]);
|
|
347
541
|
// Push base's fetched tip up under the new branch name.
|
|
348
542
|
await this.git(dir, [...this.credArgs(), 'push', 'origin', `HEAD:refs/heads/${branch}`]);
|
|
349
543
|
console.log(`[kb-seed] Created missing protected branch "${branch}" from "${base}"`);
|