@bevel-software/platform-core-backend 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +6 -6
- package/dist/core/catalog-cache-invalidation.d.ts +29 -0
- package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
- package/dist/core/catalog-cache-invalidation.js +71 -0
- package/dist/core/catalog-cache-invalidation.js.map +1 -0
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +99 -8
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +18 -1
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +96 -43
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +18 -6
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +26 -1
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +213 -71
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +59 -7
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access-model/access-grammar.d.ts +51 -1
- package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
- package/dist/modules/access-model/access-grammar.js +95 -1
- package/dist/modules/access-model/access-grammar.js.map +1 -1
- package/dist/modules/access-model/group-files.d.ts.map +1 -1
- package/dist/modules/access-model/group-files.js +4 -1
- package/dist/modules/access-model/group-files.js.map +1 -1
- package/dist/modules/access-model/plugin-principals.d.ts +49 -0
- package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
- package/dist/modules/access-model/plugin-principals.js +125 -0
- package/dist/modules/access-model/plugin-principals.js.map +1 -0
- package/dist/modules/database/core-schema.d.ts +326 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +52 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/kb-sync/index.d.ts +6 -0
- package/dist/modules/kb-sync/index.d.ts.map +1 -0
- package/dist/modules/kb-sync/index.js +5 -0
- package/dist/modules/kb-sync/index.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
- package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
- package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
- package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
- package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.service.js +183 -0
- package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
- package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
- package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
- package/dist/modules/kb-sync/sync-auth.js +62 -0
- package/dist/modules/kb-sync/sync-auth.js.map +1 -0
- package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
- package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
- package/dist/modules/kb-sync/sync-payload.js +105 -0
- package/dist/modules/kb-sync/sync-payload.js.map +1 -0
- package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
- package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/git-http.routes.js +190 -0
- package/dist/modules/marketplace/git-http.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
- package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
- package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/index.js +6 -0
- package/dist/modules/marketplace/github-facade/index.js.map +1 -0
- package/dist/modules/marketplace/index.d.ts +4 -0
- package/dist/modules/marketplace/index.d.ts.map +1 -0
- package/dist/modules/marketplace/index.js +4 -0
- package/dist/modules/marketplace/index.js.map +1 -0
- package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
- package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
- package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
- package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
- package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-state.js +3 -1
- package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
- package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
- package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
- package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
- package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
- package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
- package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/native.source.js +102 -0
- package/dist/modules/plugins/discovery/native.source.js.map +1 -0
- package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
- package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/plugin-source.js +2 -0
- package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
- package/dist/modules/plugins/index.d.ts +10 -1
- package/dist/modules/plugins/index.d.ts.map +1 -1
- package/dist/modules/plugins/index.js +8 -0
- package/dist/modules/plugins/index.js.map +1 -1
- package/dist/modules/plugins/plugin-links.d.ts +66 -0
- package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-links.js +160 -0
- package/dist/modules/plugins/plugin-links.js.map +1 -0
- package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
- package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-links.service.js +304 -0
- package/dist/modules/plugins/plugin-links.service.js.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.js +152 -52
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
- package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
- package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-rename.service.js +245 -0
- package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
- package/dist/modules/plugins/plugins.contract.d.ts +27 -1
- package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts +7 -1
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +120 -12
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/plugins/plugins.service.d.ts +50 -3
- package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.service.js +108 -47
- package/dist/modules/plugins/plugins.service.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +82 -2
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts +26 -0
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +9 -1
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/index.d.ts +1 -0
- 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.map +1 -1
- package/dist/modules/skills/pending-skills.service.js +9 -4
- package/dist/modules/skills/pending-skills.service.js.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
- package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
- package/dist/modules/skills/skill-access-requests.routes.js +168 -0
- package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
- package/dist/modules/skills/skills.contract.d.ts +31 -0
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.d.ts +28 -3
- package/dist/modules/skills/skills.routes.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.js +42 -3
- package/dist/modules/skills/skills.routes.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +4 -0
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +83 -22
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
- package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +26 -9
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -92
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +6 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +18 -25
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +40 -2
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +227 -54
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +8 -0
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +52 -1
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +242 -9
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.js +46 -2
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
- package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +9 -9
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +33 -0
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +110 -16
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/shared/domain-errors.d.ts +20 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +29 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/fs-errors.d.ts +8 -0
- package/dist/shared/fs-errors.d.ts.map +1 -0
- package/dist/shared/fs-errors.js +11 -0
- package/dist/shared/fs-errors.js.map +1 -0
- package/dist/shared/fs-walk.d.ts +20 -6
- package/dist/shared/fs-walk.d.ts.map +1 -1
- package/dist/shared/fs-walk.js +35 -27
- package/dist/shared/fs-walk.js.map +1 -1
- package/dist/shared/kb-walk.d.ts +52 -0
- package/dist/shared/kb-walk.d.ts.map +1 -0
- package/dist/shared/kb-walk.js +76 -0
- package/dist/shared/kb-walk.js.map +1 -0
- package/dist/shared/path-order.d.ts +18 -0
- package/dist/shared/path-order.d.ts.map +1 -0
- package/dist/shared/path-order.js +35 -0
- package/dist/shared/path-order.js.map +1 -0
- package/dist/shared/ttl-cache.d.ts +28 -1
- package/dist/shared/ttl-cache.d.ts.map +1 -1
- package/dist/shared/ttl-cache.js +33 -1
- package/dist/shared/ttl-cache.js.map +1 -1
- package/kb-template/.bevelignore +1 -1
- package/kb-template/AGENTS.md +95 -30
- package/kb-template/Skills/.gitkeep +0 -0
- package/migrations/0005_github_facade.sql +25 -0
- package/migrations/meta/0005_snapshot.json +1717 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +6 -6
- package/src/__tests__/kb-layout-config.test.ts +110 -0
- package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
- package/src/core/catalog-cache-invalidation.ts +92 -0
- package/src/core/create-core-server.ts +123 -7
- package/src/core/create-core-services.ts +162 -41
- package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
- package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
- package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
- package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
- package/src/modules/access/access-control.interface.ts +20 -9
- package/src/modules/access/access-control.service.ts +240 -78
- package/src/modules/access/access.routes.ts +96 -14
- package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
- package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
- package/src/modules/access-model/access-grammar.ts +114 -2
- package/src/modules/access-model/group-files.ts +4 -0
- package/src/modules/access-model/plugin-principals.ts +137 -0
- package/src/modules/database/core-schema.ts +54 -0
- package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
- package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
- package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
- package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
- package/src/modules/kb-sync/index.ts +19 -0
- package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
- package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
- package/src/modules/kb-sync/kb-sync.service.ts +201 -0
- package/src/modules/kb-sync/sync-auth.ts +118 -0
- package/src/modules/kb-sync/sync-payload.ts +143 -0
- package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
- package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
- package/src/modules/marketplace/git-http.routes.ts +193 -0
- package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
- package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
- package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
- package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
- package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
- package/src/modules/marketplace/github-facade/index.ts +25 -0
- package/src/modules/marketplace/index.ts +3 -0
- package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
- package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
- package/src/modules/mcp/oauth/oauth-state.ts +15 -3
- package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
- package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
- package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
- package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
- package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
- package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
- package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
- package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
- package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
- package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
- package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
- package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
- package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
- package/src/modules/plugins/discovery/native.source.ts +122 -0
- package/src/modules/plugins/discovery/plugin-source.ts +99 -0
- package/src/modules/plugins/index.ts +10 -0
- package/src/modules/plugins/plugin-links.service.ts +363 -0
- package/src/modules/plugins/plugin-links.ts +184 -0
- package/src/modules/plugins/plugin-provision.service.ts +160 -54
- package/src/modules/plugins/plugin-rename.service.ts +298 -0
- package/src/modules/plugins/plugins.contract.ts +28 -1
- package/src/modules/plugins/plugins.routes.ts +128 -11
- package/src/modules/plugins/plugins.service.ts +106 -53
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
- package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
- package/src/modules/settings/deployment-settings.service.ts +95 -2
- package/src/modules/settings/setup.routes.ts +24 -0
- package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
- package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
- package/src/modules/skills/index.ts +1 -0
- package/src/modules/skills/pending-skills.service.ts +7 -5
- package/src/modules/skills/skill-access-requests.routes.ts +199 -0
- package/src/modules/skills/skills.contract.ts +32 -0
- package/src/modules/skills/skills.routes.ts +59 -3
- package/src/modules/skills/skills.service.ts +84 -22
- package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
- package/src/modules/tool-auth/external-api-key.service.ts +31 -13
- package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +24 -1
- package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
- package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -88
- package/src/modules/tool-manuals/tool-manuals.service.ts +14 -23
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
- package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +2 -2
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
- package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
- package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
- package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
- package/src/modules/workflow/git/git.service.ts +239 -56
- package/src/modules/workflow/git/pull-request.service.ts +9 -0
- package/src/modules/workflow/workflow.service.ts +260 -9
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
- package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
- package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
- package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
- package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
- package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
- package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
- package/src/modules/workspace/workspace.routes.ts +9 -17
- package/src/modules/workspace/workspace.service.ts +107 -16
- package/src/shared/__tests__/filename.test.ts +0 -0
- package/src/shared/__tests__/fs-walk.test.ts +22 -1
- package/src/shared/__tests__/kb-walk.test.ts +107 -0
- package/src/shared/__tests__/path-order.test.ts +24 -0
- package/src/shared/__tests__/ttl-cache.test.ts +55 -0
- package/src/shared/domain-errors.ts +29 -0
- package/src/shared/fs-errors.ts +10 -0
- package/src/shared/fs-walk.ts +38 -23
- package/src/shared/kb-walk.ts +95 -0
- package/src/shared/path-order.ts +33 -0
- package/src/shared/ttl-cache.ts +33 -1
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
KNOWLEDGE_BASE_DIR,
|
|
5
|
+
PLUGINS_DIR,
|
|
6
|
+
SKILLS_DIR,
|
|
7
|
+
renderKbLayoutPlaceholders,
|
|
8
|
+
validateKbRootName,
|
|
9
|
+
} from '@bevel-software/platform-shared';
|
|
4
10
|
import { IGNORE_FILENAME } from '../../bevel-ignore.js';
|
|
5
11
|
import type { KbBranch, OnServerStart, ServerStartContext, StepResult } from '../on-server-start.js';
|
|
6
12
|
|
|
@@ -43,8 +49,8 @@ export const TEMPLATE_SOURCE_FALLBACKS: Readonly<Record<string, string>> = {
|
|
|
43
49
|
};
|
|
44
50
|
|
|
45
51
|
/**
|
|
46
|
-
* The
|
|
47
|
-
* that hold
|
|
52
|
+
* The three roots CORE gives a knowledge base: the ontologies, the shared
|
|
53
|
+
* skills, and the plugins that hold tools and link the skills.
|
|
48
54
|
*
|
|
49
55
|
* `Data/`, `Agents/` and `Pipelines/` are deliberately absent. They scaffold
|
|
50
56
|
* the agentic execution layer, which is not part of this platform — a core
|
|
@@ -54,8 +60,13 @@ export const TEMPLATE_SOURCE_FALLBACKS: Readonly<Record<string, string>> = {
|
|
|
54
60
|
* READMEs); the names stay reserved in `kb-layout.ts` either way, so a KB
|
|
55
61
|
* that has them still renders them as roots rather than folding them into
|
|
56
62
|
* Knowledge.
|
|
63
|
+
*
|
|
64
|
+
* A function: the three names are deployment-configurable live bindings, and
|
|
65
|
+
* a module-scope array would snapshot the defaults before configuration.
|
|
57
66
|
*/
|
|
58
|
-
|
|
67
|
+
function coreRequiredDirs(): readonly string[] {
|
|
68
|
+
return [KNOWLEDGE_BASE_DIR, SKILLS_DIR, PLUGINS_DIR];
|
|
69
|
+
}
|
|
59
70
|
|
|
60
71
|
/**
|
|
61
72
|
* A reserved root must be ONE path segment — `Data`, not `Data/x`, `../x` or
|
|
@@ -102,7 +113,7 @@ export function reservedRootDirs(extraRootDirs: readonly string[]): readonly str
|
|
|
102
113
|
);
|
|
103
114
|
}
|
|
104
115
|
}
|
|
105
|
-
return [...
|
|
116
|
+
return [...coreRequiredDirs(), ...extraRootDirs];
|
|
106
117
|
}
|
|
107
118
|
|
|
108
119
|
/**
|
|
@@ -200,14 +211,18 @@ export class TemplateFilesStep implements OnServerStart {
|
|
|
200
211
|
'Remove or rename it — the platform requires this name to be a readable file.',
|
|
201
212
|
);
|
|
202
213
|
}
|
|
203
|
-
let content
|
|
214
|
+
let content = await readTemplate(templateDir, rel);
|
|
204
215
|
// The on-disk merge below only runs against an EXISTING ignore file; a
|
|
205
216
|
// freshly-declared one was merely assumed to carry the AGENTS.md rule —
|
|
206
217
|
// true of the packaged template, not necessarily of a distribution's
|
|
207
218
|
// custom one. Make it true here, so the managed conventions doc is
|
|
208
219
|
// hidden from the file tree from the first boot either way.
|
|
220
|
+
// …and a template still shipping the skills rule an earlier release
|
|
221
|
+
// had (a distribution's copy, a stale packaged one) must not declare
|
|
222
|
+
// it: the on-disk reconciliation below never sees a file that was
|
|
223
|
+
// absent, so the declared content is reconciled here instead.
|
|
209
224
|
if (rel === IGNORE_FILENAME) {
|
|
210
|
-
content = withIgnorePattern(
|
|
225
|
+
content = withoutPlatformIgnorePattern(withIgnorePattern(content, 'AGENTS.md'), `${SKILLS_DIR}/`);
|
|
211
226
|
}
|
|
212
227
|
branch.write(rel, content);
|
|
213
228
|
added.push(rel);
|
|
@@ -224,7 +239,20 @@ export class TemplateFilesStep implements OnServerStart {
|
|
|
224
239
|
// LINE PRESENCE, not effective outcome: a later `!AGENTS.md` negation is
|
|
225
240
|
// the operator explicitly choosing to SHOW the file, and hiding it is a
|
|
226
241
|
// default this step provides, not a mandate it re-imposes every boot.
|
|
227
|
-
|
|
242
|
+
//
|
|
243
|
+
// The shared-skills root goes the OTHER way. An earlier release hid it
|
|
244
|
+
// like `Plugins/`; the Skills & Tools sidebar now renders it as a file
|
|
245
|
+
// tree read from the workspace tree, which the ignore file filters — so a
|
|
246
|
+
// KB still carrying that rule would show an empty Skills section. The
|
|
247
|
+
// line that release wrote comes out, recognised by the PLATFORM'S OWN
|
|
248
|
+
// COMMENT above it — an operator who wrote the same rule by hand keeps
|
|
249
|
+
// it, for the same reason the negation above is kept: the file is
|
|
250
|
+
// theirs. The Knowledge explorer never rendered the root and still does
|
|
251
|
+
// not. Spelled with the CONFIGURED root name, since a deployment may
|
|
252
|
+
// have renamed it. ONE read-modify-write for both rules: two passes
|
|
253
|
+
// would each read the on-disk file and the second declared write would
|
|
254
|
+
// lose the first's.
|
|
255
|
+
added.push(...(await reconcileIgnoreRules(repoDir, branch, { add: ['AGENTS.md'], drop: [`${SKILLS_DIR}/`] })));
|
|
228
256
|
|
|
229
257
|
// AGENTS.md is MANAGED, not merely seeded: the platform owns its content,
|
|
230
258
|
// and a stale copy is replaced with the packaged template's every startup
|
|
@@ -293,59 +321,135 @@ export class TemplateFilesStep implements OnServerStart {
|
|
|
293
321
|
}
|
|
294
322
|
}
|
|
295
323
|
|
|
296
|
-
/**
|
|
297
|
-
|
|
298
|
-
|
|
324
|
+
/**
|
|
325
|
+
* The template's content for `relPath`, RENDERED: the managed files name the
|
|
326
|
+
* three root folders, and a deployment may have renamed those, so the
|
|
327
|
+
* placeholders the template carries (`{{pluginsDir}}` …) are filled with the
|
|
328
|
+
* names in effect. Every required file is text; a template without
|
|
329
|
+
* placeholders passes through unchanged.
|
|
330
|
+
*/
|
|
331
|
+
async function readTemplate(templateDir: string, relPath: string): Promise<string> {
|
|
332
|
+
return renderKbLayoutPlaceholders(await fs.readFile(await templateSource(templateDir, relPath), 'utf8'));
|
|
299
333
|
}
|
|
300
334
|
|
|
301
335
|
/**
|
|
302
|
-
* Whether the repo's copy of `relPath` differs from the template's,
|
|
303
|
-
* line endings — a CRLF checkout of identical content must read as
|
|
304
|
-
* or the managed-file refresh would commit churn on every boot
|
|
336
|
+
* Whether the repo's copy of `relPath` differs from the RENDERED template's,
|
|
337
|
+
* modulo line endings — a CRLF checkout of identical content must read as
|
|
338
|
+
* "same", or the managed-file refresh would commit churn on every boot
|
|
339
|
+
* forever. Rendered, so a renamed root is compared against the guide that
|
|
340
|
+
* names it, not against the placeholders.
|
|
305
341
|
*/
|
|
306
342
|
async function templateDiffers(templateDir: string, repoDir: string, relPath: string): Promise<boolean> {
|
|
307
343
|
const norm = (text: string) => text.replace(/\r\n?/g, '\n');
|
|
308
344
|
const [current, template] = await Promise.all([
|
|
309
345
|
fs.readFile(path.join(repoDir, relPath), 'utf8'),
|
|
310
|
-
|
|
346
|
+
readTemplate(templateDir, relPath),
|
|
311
347
|
]);
|
|
312
348
|
return norm(current) !== norm(template);
|
|
313
349
|
}
|
|
314
350
|
|
|
315
351
|
/**
|
|
316
|
-
*
|
|
317
|
-
*
|
|
352
|
+
* Reconcile the platform's OWN rules in `.bevelignore`: every `add` pattern
|
|
353
|
+
* guaranteed present as a line, every `drop` pattern taken out. Returns the
|
|
354
|
+
* paths changed, for the note.
|
|
318
355
|
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
356
|
+
* Never rewrites the rest. The file is the operator's, and every rule already
|
|
357
|
+
* in it is theirs to keep: adding puts one line under a comment saying where
|
|
358
|
+
* it came from, and dropping removes exactly the line (and the comment) an
|
|
359
|
+
* earlier release put there — never a line the operator wrote. Absent file is
|
|
360
|
+
* a no-op — it means the template's copy (declared in the same step, and
|
|
361
|
+
* reconciled the same way at declaration) arrives with the right rules in it.
|
|
324
362
|
*
|
|
325
363
|
* Matched line-wise rather than by substring: a rule for `Plugins/AGENTS.md`
|
|
326
364
|
* is not a rule for the root `AGENTS.md`, and treating it as one would leave
|
|
327
365
|
* the mismatch this exists to close.
|
|
328
366
|
*/
|
|
329
|
-
async function
|
|
367
|
+
async function reconcileIgnoreRules(
|
|
368
|
+
repoDir: string,
|
|
369
|
+
branch: KbBranch,
|
|
370
|
+
rules: { add: string[]; drop: string[] },
|
|
371
|
+
): Promise<string[]> {
|
|
330
372
|
let current: string;
|
|
331
373
|
try {
|
|
332
374
|
current = await fs.readFile(path.join(repoDir, IGNORE_FILENAME), 'utf8');
|
|
333
375
|
} catch {
|
|
334
376
|
// No ignore file — the copy declared from the template arrives with the
|
|
335
|
-
//
|
|
336
|
-
// loop above).
|
|
377
|
+
// right rules in it (guaranteed at declaration time, see the
|
|
378
|
+
// required-files loop above).
|
|
337
379
|
return [];
|
|
338
380
|
}
|
|
339
|
-
const
|
|
381
|
+
const added = rules.add.reduce((text, pattern) => withIgnorePattern(text, pattern), current);
|
|
382
|
+
const merged = rules.drop.reduce((text, pattern) => withoutPlatformIgnorePattern(text, pattern), added);
|
|
340
383
|
if (merged === current) return [];
|
|
341
384
|
branch.write(IGNORE_FILENAME, merged);
|
|
342
385
|
return [IGNORE_FILENAME];
|
|
343
386
|
}
|
|
344
387
|
|
|
388
|
+
/** The comment `withIgnorePattern` writes above a line it appends. */
|
|
389
|
+
const PLATFORM_RULE_COMMENT = '# Added by the platform: the conventions doc is not node content.';
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* The template line an earlier release shipped above the shared-skills rule,
|
|
393
|
+
* split around its one variable: the plugins root's name, which a deployment
|
|
394
|
+
* may have renamed since. Everything else is fixed.
|
|
395
|
+
*/
|
|
396
|
+
const LEGACY_SKILLS_RULE_COMMENT_OPENING = '# The shared-skills root is rendered by the Skills & Tools app, like ';
|
|
397
|
+
const LEGACY_SKILLS_RULE_COMMENT_CLOSING = '/.';
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Whether a line is EXACTLY a comment the platform wrote above a rule it
|
|
401
|
+
* added. For the legacy template line that means the fixed opening, the
|
|
402
|
+
* fixed closing, and between them a name the platform could have rendered
|
|
403
|
+
* there — judged by the ONE rule that decides what a root may be called
|
|
404
|
+
* (`validateKbRootName`), not by a second grammar written here: a hand-made
|
|
405
|
+
* character class either admits names the validator refuses, or refuses
|
|
406
|
+
* names it admits (a space, say), and either way a line the platform did
|
|
407
|
+
* write would be left standing. A looser match (an opening, a substring)
|
|
408
|
+
* errs the other way and takes a line the operator wrote.
|
|
409
|
+
*/
|
|
410
|
+
function isPlatformRuleComment(line: string): boolean {
|
|
411
|
+
const trimmed = line.trim();
|
|
412
|
+
if (trimmed === PLATFORM_RULE_COMMENT) return true;
|
|
413
|
+
if (
|
|
414
|
+
!trimmed.startsWith(LEGACY_SKILLS_RULE_COMMENT_OPENING) ||
|
|
415
|
+
!trimmed.endsWith(LEGACY_SKILLS_RULE_COMMENT_CLOSING)
|
|
416
|
+
) {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
const name = trimmed.slice(
|
|
420
|
+
LEGACY_SKILLS_RULE_COMMENT_OPENING.length,
|
|
421
|
+
trimmed.length - LEGACY_SKILLS_RULE_COMMENT_CLOSING.length,
|
|
422
|
+
);
|
|
423
|
+
return name === name.trim() && validateKbRootName(name) === null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* `text` without the `pattern` lines THE PLATFORM WROTE — the ones sitting
|
|
428
|
+
* directly under its own comment (the one `withIgnorePattern` writes, or the
|
|
429
|
+
* template line an earlier release shipped) — and without that comment, plus
|
|
430
|
+
* the blank line that opened an appended block. Provenance is the comment:
|
|
431
|
+
* an identical line with no platform comment above it is the operator's and
|
|
432
|
+
* stays, as does a `!pattern` negation. Nothing else moves.
|
|
433
|
+
*/
|
|
434
|
+
function withoutPlatformIgnorePattern(text: string, pattern: string): string {
|
|
435
|
+
const lines = text.split('\n');
|
|
436
|
+
const kept: string[] = [];
|
|
437
|
+
for (const line of lines) {
|
|
438
|
+
const above = kept[kept.length - 1];
|
|
439
|
+
if (line.trim() !== pattern || above === undefined || !isPlatformRuleComment(above)) {
|
|
440
|
+
kept.push(line);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
kept.pop();
|
|
444
|
+
if (above.trim() === PLATFORM_RULE_COMMENT && kept.length > 1 && kept[kept.length - 1]?.trim() === '') kept.pop();
|
|
445
|
+
}
|
|
446
|
+
return kept.join('\n');
|
|
447
|
+
}
|
|
448
|
+
|
|
345
449
|
/**
|
|
346
450
|
* `text` with `pattern` guaranteed present as a LINE — appended under a
|
|
347
451
|
* comment naming its origin when absent, returned unchanged when present.
|
|
348
|
-
* Line-wise match, same rationale as {@link
|
|
452
|
+
* Line-wise match, same rationale as {@link mergeIgnorePatterns}.
|
|
349
453
|
*
|
|
350
454
|
* An explicit `!pattern` line also returns the text unchanged: that is the
|
|
351
455
|
* operator choosing to SHOW the file, and ordered matching means a positive
|
|
@@ -4,15 +4,7 @@ import { IGNORE_FILENAME } from './bevel-ignore.js';
|
|
|
4
4
|
import type { IAdminAccessService } from '../admin/admin.interface.js';
|
|
5
5
|
import express from 'express';
|
|
6
6
|
import type { AuthUser, IWorkflowService } from '@bevel-software/platform-shared';
|
|
7
|
-
import {
|
|
8
|
-
AGENTS_DIR,
|
|
9
|
-
DATA_DIR,
|
|
10
|
-
DEFAULT_BRANCH,
|
|
11
|
-
KNOWLEDGE_BASE_DIR,
|
|
12
|
-
KNOWLEDGE_DIR,
|
|
13
|
-
PIPELINES_DIR,
|
|
14
|
-
PLUGINS_DIR,
|
|
15
|
-
} from '@bevel-software/platform-shared';
|
|
7
|
+
import { DEFAULT_BRANCH, KNOWLEDGE_DIR, reservedRootDirNames } from '@bevel-software/platform-shared';
|
|
16
8
|
import { FolderTooLargeError, type ReadTreeFilter } from './workspace.service.js';
|
|
17
9
|
import { branchForWorkspaceId } from '../../shared/workspace-id.js';
|
|
18
10
|
import type { WorkspaceService } from './workspace.service.js';
|
|
@@ -290,6 +282,13 @@ export function createWorkspaceRoutes(
|
|
|
290
282
|
);
|
|
291
283
|
res.json({ workspace, fileTree });
|
|
292
284
|
} catch (error) {
|
|
285
|
+
// A typed domain answer — a branch origin no longer has (410), a name
|
|
286
|
+
// git refuses (400) — is the client's to act on, not a failure of ours;
|
|
287
|
+
// it keeps its status so the browser can say what happened.
|
|
288
|
+
if (error instanceof WorkflowDomainError) {
|
|
289
|
+
sendError(res, error);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
293
292
|
// Log the full stack so the next 500 isn't a guessing game — the
|
|
294
293
|
// bare `error.message` we returned before lost most diagnostic
|
|
295
294
|
// signal (cause chain, stack frames, error class).
|
|
@@ -476,14 +475,7 @@ export function createWorkspaceRoutes(
|
|
|
476
475
|
// same treatment so legacy clones don't collapse. Only the folders
|
|
477
476
|
// themselves are forced visible — their contents stay gated by the
|
|
478
477
|
// verdict above.
|
|
479
|
-
const structuralRoots = new Set([
|
|
480
|
-
KNOWLEDGE_BASE_DIR,
|
|
481
|
-
DATA_DIR,
|
|
482
|
-
AGENTS_DIR,
|
|
483
|
-
PIPELINES_DIR,
|
|
484
|
-
PLUGINS_DIR,
|
|
485
|
-
KNOWLEDGE_DIR,
|
|
486
|
-
]);
|
|
478
|
+
const structuralRoots = new Set([...reservedRootDirNames(), KNOWLEDGE_DIR]);
|
|
487
479
|
for (const wp of wsRelPaths) {
|
|
488
480
|
const rel = toKbRelative(wp, kbDirName);
|
|
489
481
|
if (rel !== null && structuralRoots.has(rel)) verdict.set(wp, true);
|
|
@@ -7,7 +7,11 @@ import type { AuthUser, IWorkspaceService, WorkspaceInfo, FileTreeEntry } from '
|
|
|
7
7
|
import { assertValidRelativePath, validateFilename, DEFAULT_BRANCH } from '@bevel-software/platform-shared';
|
|
8
8
|
import { BevelIgnoreStack } from './bevel-ignore.js';
|
|
9
9
|
import { workspaceIdForBranch, branchForWorkspaceId } from '../../shared/workspace-id.js';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
RemoteBranchGoneError,
|
|
12
|
+
WorkflowDomainError,
|
|
13
|
+
isMissingRemoteBranchFailure,
|
|
14
|
+
} from '../../shared/domain-errors.js';
|
|
11
15
|
import { assertValidBranchName } from '../kb-fs/branch-name.js';
|
|
12
16
|
import {
|
|
13
17
|
cloneCredentialArgs,
|
|
@@ -248,6 +252,73 @@ export class WorkspaceService implements IWorkspaceService {
|
|
|
248
252
|
return null;
|
|
249
253
|
}
|
|
250
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Every branch with a finished clone on disk, whether or not this process
|
|
257
|
+
* has touched it yet. The remote sync reads this: clones survive a restart,
|
|
258
|
+
* and a hook that fires before anyone opens a branch must still find its
|
|
259
|
+
* clone. Same disk scan as `findAnyWorkspaceId`.
|
|
260
|
+
*
|
|
261
|
+
* What is listed: directories holding a `<kbDirName>/.git`, whose name is
|
|
262
|
+
* the encoding of a branch git would accept, and whose bootstrap is not in
|
|
263
|
+
* flight right now. The in-flight exclusion matters because `git clone`
|
|
264
|
+
* creates `.git` long before the working tree is checked out — pulling a
|
|
265
|
+
* clone mid-bootstrap would run git against a half-written tree — and a
|
|
266
|
+
* clone that is being created this instant is current by definition. A
|
|
267
|
+
* clone left behind by a crash mid-checkout is NOT distinguished here: the
|
|
268
|
+
* rest of this service treats a directory with `.git` as a clone too, and
|
|
269
|
+
* the pull's own errors are how such a tree surfaces.
|
|
270
|
+
*
|
|
271
|
+
* A missing root means no clones (nothing has been bootstrapped yet); any
|
|
272
|
+
* other failure to read it is propagated, so a sync cannot report success
|
|
273
|
+
* over a root it could not see. One entry that cannot be probed (an ACL, a
|
|
274
|
+
* half-deleted directory) is returned with `unreadable` set rather than
|
|
275
|
+
* thrown, so it fails as one branch and not as the whole listing.
|
|
276
|
+
*/
|
|
277
|
+
async listClonedWorkspaces(): Promise<Array<{ id: string; branch: string; unreadable?: string }>> {
|
|
278
|
+
let entries: Array<{ name: string; isDirectory: () => boolean }>;
|
|
279
|
+
try {
|
|
280
|
+
entries = await fs.readdir(this.workspacesRoot, { withFileTypes: true });
|
|
281
|
+
} catch (err) {
|
|
282
|
+
const code = (err as NodeJS.ErrnoException | null)?.code;
|
|
283
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') return [];
|
|
284
|
+
throw err;
|
|
285
|
+
}
|
|
286
|
+
const cloned: Array<{ id: string; branch: string; unreadable?: string }> = [];
|
|
287
|
+
for (const entry of entries) {
|
|
288
|
+
if (!entry.isDirectory()) continue;
|
|
289
|
+
const branch = branchForWorkspaceId(entry.name);
|
|
290
|
+
// Not one of ours unless the name is the encoding of a branch git
|
|
291
|
+
// accepts: `branchForWorkspaceId` returns a malformed name unchanged, so
|
|
292
|
+
// the round-trip catches those, and the validator catches a stray
|
|
293
|
+
// directory whose name merely round-trips.
|
|
294
|
+
if (workspaceIdForBranch(branch) !== entry.name) continue;
|
|
295
|
+
try {
|
|
296
|
+
assertValidBranchName(branch);
|
|
297
|
+
} catch {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
await fs.access(path.join(this.workspacesRoot, entry.name, this.kbDirName, '.git'));
|
|
302
|
+
} catch (err) {
|
|
303
|
+
const code = (err as NodeJS.ErrnoException | null)?.code;
|
|
304
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') continue;
|
|
305
|
+
cloned.push({
|
|
306
|
+
id: entry.name,
|
|
307
|
+
branch,
|
|
308
|
+
unreadable: `Could not read this branch's clone: ${redactError(err)}`,
|
|
309
|
+
});
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
// AFTER the probe, with no await in between: a bootstrap registers
|
|
313
|
+
// itself before it creates `.git`, so any clone whose `.git` was found
|
|
314
|
+
// while its bootstrap is running is in this map by now. Checking before
|
|
315
|
+
// the probe left a window where the clone started in between.
|
|
316
|
+
if (this.isBootstrapInFlight(branch)) continue;
|
|
317
|
+
cloned.push({ id: entry.name, branch });
|
|
318
|
+
}
|
|
319
|
+
return cloned;
|
|
320
|
+
}
|
|
321
|
+
|
|
251
322
|
/**
|
|
252
323
|
* Run the clone. When a `referenceRepo` is supplied and the referenced
|
|
253
324
|
* clone fails (e.g. the sibling's object store is corrupt or mid-write),
|
|
@@ -408,6 +479,15 @@ export class WorkspaceService implements IWorkspaceService {
|
|
|
408
479
|
}
|
|
409
480
|
}
|
|
410
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Whether a clone of `branch` is being created at this moment. Read by the
|
|
484
|
+
* retirement of a clone the host deleted: a bootstrap in flight means the
|
|
485
|
+
* branch exists again, so there is nothing stale to remove.
|
|
486
|
+
*/
|
|
487
|
+
isBootstrapInFlight(branch: string): boolean {
|
|
488
|
+
return this.inFlightBootstraps.has(branch);
|
|
489
|
+
}
|
|
490
|
+
|
|
411
491
|
/**
|
|
412
492
|
* Bring an adopted clone's config back to what a fresh clone would carry.
|
|
413
493
|
*
|
|
@@ -620,9 +700,18 @@ export class WorkspaceService implements IWorkspaceService {
|
|
|
620
700
|
}
|
|
621
701
|
} catch (err) {
|
|
622
702
|
const redacted = redactError(err);
|
|
623
|
-
console.error(`[workspace] Failed to clone for branch "${branch}":`, redacted);
|
|
624
703
|
// Roll back partial state so the next bootstrap retries cleanly.
|
|
625
704
|
await fs.rm(targetDir, { recursive: true, force: true }).catch(() => {});
|
|
705
|
+
// A branch origin does not have is a fact about the branch, not a
|
|
706
|
+
// failure of ours: the host deleted it (and the sync retired the
|
|
707
|
+
// clone), or the link was to a branch that never existed. Typed, so the
|
|
708
|
+
// routes answer 410 with the branch named and the browser can say "this
|
|
709
|
+
// branch no longer exists" instead of "something went wrong".
|
|
710
|
+
if (isMissingRemoteBranchFailure(redacted)) {
|
|
711
|
+
console.log(`[workspace] branch "${branch}" does not exist on origin — nothing to clone`);
|
|
712
|
+
throw new RemoteBranchGoneError(branch);
|
|
713
|
+
}
|
|
714
|
+
console.error(`[workspace] Failed to clone for branch "${branch}":`, redacted);
|
|
626
715
|
throw new Error(`Failed to clone process map: ${redacted}`);
|
|
627
716
|
}
|
|
628
717
|
}
|
|
@@ -1433,23 +1522,25 @@ export class WorkspaceService implements IWorkspaceService {
|
|
|
1433
1522
|
});
|
|
1434
1523
|
}
|
|
1435
1524
|
|
|
1436
|
-
// Read-permission filter: ONE batched check per directory. Drops files
|
|
1437
|
-
//
|
|
1438
|
-
//
|
|
1439
|
-
//
|
|
1440
|
-
//
|
|
1441
|
-
//
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1525
|
+
// Read-permission filter: ONE batched check per directory. Drops files the
|
|
1526
|
+
// caller can't read. A directory is kept when the caller can read it — a
|
|
1527
|
+
// readable directory left empty after filtering stays visible, the folder
|
|
1528
|
+
// itself is readable — OR when something readable survives beneath it: a
|
|
1529
|
+
// grant below (a linked skill's folder opened to a plugin's readers, a
|
|
1530
|
+
// sub-folder shared on its own) makes the folders above it the way there,
|
|
1531
|
+
// shown as containers whose own contents stay filtered. So an unreadable
|
|
1532
|
+
// directory is walked, not skipped, and dropped only when the walk finds
|
|
1533
|
+
// nothing. Fail-closed: anything not explicitly readable is dropped. No
|
|
1534
|
+
// filter → identical to the pre-feature tree (regression-safe).
|
|
1535
|
+
const verdict = readFilter && candidates.length > 0 ? await readFilter(candidates.map((c) => c.rel)) : null;
|
|
1536
|
+
const readable = (rel: string) => verdict === null || verdict.get(rel) === true;
|
|
1447
1537
|
|
|
1448
1538
|
const children: FileTreeEntry[] = [];
|
|
1449
|
-
for (const { entry, entryPath, rel } of
|
|
1539
|
+
for (const { entry, entryPath, rel } of candidates) {
|
|
1450
1540
|
if (entry.isDirectory()) {
|
|
1451
|
-
|
|
1452
|
-
|
|
1541
|
+
const sub = await this.buildFileTree(entryPath, workspaceRoot, ignoreStack, readFilter);
|
|
1542
|
+
if (readable(rel) || (sub.children?.length ?? 0) > 0) children.push(sub);
|
|
1543
|
+
} else if (readable(rel)) {
|
|
1453
1544
|
children.push({ name: entry.name, relativePath: rel, type: 'file' });
|
|
1454
1545
|
}
|
|
1455
1546
|
}
|
|
Binary file
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
@@ -39,4 +39,25 @@ describe('walkFiles', () => {
|
|
|
39
39
|
|
|
40
40
|
expect(await walkFiles(root, (n) => n.endsWith('.tool'))).toEqual(['visible.tool']);
|
|
41
41
|
});
|
|
42
|
+
|
|
43
|
+
it('skips a directory it cannot list by default, and throws for a strict caller', async () => {
|
|
44
|
+
await fs.mkdir(path.join(root, 'locked'), { recursive: true });
|
|
45
|
+
await fs.writeFile(path.join(root, 'locked/a.tool'), '');
|
|
46
|
+
await fs.writeFile(path.join(root, 'b.tool'), '');
|
|
47
|
+
const real = fs.readdir;
|
|
48
|
+
const spy = vi.spyOn(fs, 'readdir').mockImplementation(((dir: string, opts: unknown) => {
|
|
49
|
+
if (String(dir).endsWith('locked')) {
|
|
50
|
+
return Promise.reject(Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' }));
|
|
51
|
+
}
|
|
52
|
+
return (real as (d: string, o: unknown) => Promise<unknown>).call(fs, dir, opts);
|
|
53
|
+
}) as never);
|
|
54
|
+
try {
|
|
55
|
+
// A catalog shows what it can…
|
|
56
|
+
expect(await walkFiles(root, (n) => n.endsWith('.tool'))).toEqual(['b.tool']);
|
|
57
|
+
// …a caller that must see everything gets the error, not a list with a hole.
|
|
58
|
+
await expect(walkFiles(root, (n) => n.endsWith('.tool'), { strict: true })).rejects.toThrow('EACCES');
|
|
59
|
+
} finally {
|
|
60
|
+
spy.mockRestore();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
42
63
|
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { walkKb, type KbWalkListener } from '../kb-walk.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The one walk: what it skips, in what order it visits, what it calls a hole
|
|
9
|
+
* — and that every listener on it is told exactly the same things.
|
|
10
|
+
*/
|
|
11
|
+
describe('walkKb', () => {
|
|
12
|
+
let root: string;
|
|
13
|
+
const write = async (rel: string, text = '') => {
|
|
14
|
+
const abs = path.join(root, rel);
|
|
15
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
16
|
+
await fs.writeFile(abs, text);
|
|
17
|
+
};
|
|
18
|
+
/** A listener that records every event it is told, in order. */
|
|
19
|
+
const recorder = () => {
|
|
20
|
+
const events: string[] = [];
|
|
21
|
+
const listener: KbWalkListener = {
|
|
22
|
+
onDir: (rel, entries) => void events.push(`dir ${rel || '.'} [${entries.map((e) => e.name).join(' ')}]`),
|
|
23
|
+
onFile: (dir, name) => void events.push(`file ${dir ? `${dir}/${name}` : name}`),
|
|
24
|
+
onHole: (rel) => void events.push(`hole ${rel || '.'}`),
|
|
25
|
+
};
|
|
26
|
+
return { events, listener };
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
beforeEach(async () => {
|
|
30
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'kb-walk-'));
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => fs.rm(root, { recursive: true, force: true }));
|
|
33
|
+
|
|
34
|
+
it('visits every folder once, in component order, skipping dot-entries and node_modules', async () => {
|
|
35
|
+
await write('Plugins/a-b/plugin.json');
|
|
36
|
+
await write('Plugins/a/b/plugin.json');
|
|
37
|
+
await write('KnowledgeBase/x.md');
|
|
38
|
+
await write('.git/HEAD');
|
|
39
|
+
await write('node_modules/dep/index.js');
|
|
40
|
+
await write('Plugins/.parked/plugin.json');
|
|
41
|
+
const { events, listener } = recorder();
|
|
42
|
+
const { holes } = await walkKb(root, [listener]);
|
|
43
|
+
expect(holes).toEqual([]);
|
|
44
|
+
expect(events).toEqual([
|
|
45
|
+
'dir . [KnowledgeBase Plugins]',
|
|
46
|
+
'dir KnowledgeBase [x.md]',
|
|
47
|
+
'file KnowledgeBase/x.md',
|
|
48
|
+
'dir Plugins [a a-b]', // "a" before "a-b": the walk order every consumer agrees on
|
|
49
|
+
'dir Plugins/a [b]',
|
|
50
|
+
'dir Plugins/a/b [plugin.json]',
|
|
51
|
+
'file Plugins/a/b/plugin.json',
|
|
52
|
+
'dir Plugins/a-b [plugin.json]',
|
|
53
|
+
'file Plugins/a-b/plugin.json',
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('tells every listener the same things', async () => {
|
|
58
|
+
await write('Plugins/GTM/plugin.json');
|
|
59
|
+
await write('Skills/Eng/deploy/SKILL.md');
|
|
60
|
+
const a = recorder();
|
|
61
|
+
const b = recorder();
|
|
62
|
+
await walkKb(root, [a.listener, b.listener]);
|
|
63
|
+
expect(a.events.length).toBeGreaterThan(0);
|
|
64
|
+
expect(b.events).toEqual(a.events);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('a folder that exists but cannot be listed is a hole — reported to every listener and in the result', async () => {
|
|
68
|
+
await write('Plugins/locked/plugin.json');
|
|
69
|
+
await write('Plugins/open/plugin.json');
|
|
70
|
+
const real = fs.readdir;
|
|
71
|
+
const spy = vi.spyOn(fs, 'readdir').mockImplementation(((dir: string, opts: unknown) =>
|
|
72
|
+
String(dir).endsWith('locked')
|
|
73
|
+
? Promise.reject(Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' }))
|
|
74
|
+
: (real as (d: string, o: unknown) => Promise<unknown>).call(fs, dir, opts)) as never);
|
|
75
|
+
try {
|
|
76
|
+
const { events, listener } = recorder();
|
|
77
|
+
const { holes } = await walkKb(root, [listener]);
|
|
78
|
+
expect(holes).toEqual(['Plugins/locked']);
|
|
79
|
+
expect(events).toContain('hole Plugins/locked');
|
|
80
|
+
// The rest of the tree is still walked.
|
|
81
|
+
expect(events).toContain('file Plugins/open/plugin.json');
|
|
82
|
+
} finally {
|
|
83
|
+
spy.mockRestore();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('a missing root is an empty walk; a folder that vanished between listing and visiting is not a hole', async () => {
|
|
88
|
+
const empty = recorder();
|
|
89
|
+
expect(await walkKb(path.join(root, 'nope'), [empty.listener])).toEqual({ holes: [] });
|
|
90
|
+
expect(empty.events).toEqual([]);
|
|
91
|
+
|
|
92
|
+
await write('Plugins/gone/plugin.json');
|
|
93
|
+
const real = fs.readdir;
|
|
94
|
+
const spy = vi.spyOn(fs, 'readdir').mockImplementation(((dir: string, opts: unknown) =>
|
|
95
|
+
String(dir).endsWith('gone')
|
|
96
|
+
? Promise.reject(Object.assign(new Error('ENOENT: no such file'), { code: 'ENOENT' }))
|
|
97
|
+
: (real as (d: string, o: unknown) => Promise<unknown>).call(fs, dir, opts)) as never);
|
|
98
|
+
try {
|
|
99
|
+
const { events, listener } = recorder();
|
|
100
|
+
const { holes } = await walkKb(root, [listener]);
|
|
101
|
+
expect(holes).toEqual([]);
|
|
102
|
+
expect(events.some((e) => e.startsWith('hole'))).toBe(false);
|
|
103
|
+
} finally {
|
|
104
|
+
spy.mockRestore();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { comparePathComponents } from '../path-order.js';
|
|
3
|
+
|
|
4
|
+
describe('comparePathComponents', () => {
|
|
5
|
+
it('orders component by component, the way a sorted directory walk visits paths', () => {
|
|
6
|
+
// A whole-string compare would weigh "-" against "/" and put a-b/x first.
|
|
7
|
+
expect(comparePathComponents('a/b/x', 'a-b/x')).toBeLessThan(0);
|
|
8
|
+
expect(comparePathComponents('Plugins/GTM', 'Plugins/GTM/skills')).toBeLessThan(0);
|
|
9
|
+
expect(comparePathComponents('Plugins/GTM', 'Plugins/GTM')).toBe(0);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('never calls two distinct paths equal: a collation tie falls through to code-unit order', () => {
|
|
13
|
+
// Precomposed é and its decomposed spelling collate as equal; two folders
|
|
14
|
+
// so named must still have ONE first, or discovery and principal synthesis
|
|
15
|
+
// could each claim a different one for the same slug.
|
|
16
|
+
const composed = 'Plugins/café';
|
|
17
|
+
const decomposed = 'Plugins/café';
|
|
18
|
+
// (Whether the host's collation equates them is the host's business —
|
|
19
|
+
// the property under test holds either way: distinct paths never tie.)
|
|
20
|
+
const order = comparePathComponents(composed, decomposed);
|
|
21
|
+
expect(order).not.toBe(0);
|
|
22
|
+
expect(comparePathComponents(decomposed, composed)).toBe(-order);
|
|
23
|
+
});
|
|
24
|
+
});
|