@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
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { GitService } from '../git.service.js';
|
|
6
|
+
import { RemoteBranchGoneError } from '../../../../shared/domain-errors.js';
|
|
7
|
+
import { runGit, gitOut, stubWorkflowHooks, stubWorkspaceService } from './git-test-helpers.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `syncFromRemote` against real repositories: what it reports about the clone
|
|
11
|
+
* before and after, the paths it names, an upstream that has no commits yet,
|
|
12
|
+
* and a branch the host has deleted.
|
|
13
|
+
*/
|
|
14
|
+
const BRANCH = 'target-company-state';
|
|
15
|
+
|
|
16
|
+
async function bareUpstream(root: string, withCommit: boolean): Promise<string> {
|
|
17
|
+
const upstream = path.join(root, 'upstream.git');
|
|
18
|
+
await runGit(root, ['init', '--bare', '-b', BRANCH, upstream]);
|
|
19
|
+
if (withCommit) {
|
|
20
|
+
const seed = path.join(root, '.seed');
|
|
21
|
+
await fs.mkdir(seed);
|
|
22
|
+
await runGit(seed, ['init', '-b', BRANCH]);
|
|
23
|
+
await runGit(seed, ['remote', 'add', 'origin', upstream]);
|
|
24
|
+
await fs.writeFile(path.join(seed, 'a.md'), 'a\n');
|
|
25
|
+
await fs.writeFile(path.join(seed, 'old name.md'), 'same content that survives a rename\n');
|
|
26
|
+
await runGit(seed, ['add', '.']);
|
|
27
|
+
await runGit(seed, ['commit', '-m', 'init']);
|
|
28
|
+
await runGit(seed, ['push', 'origin', BRANCH]);
|
|
29
|
+
}
|
|
30
|
+
await runGit(upstream, ['symbolic-ref', 'HEAD', `refs/heads/${BRANCH}`]);
|
|
31
|
+
return upstream;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function cloneWorkspace(root: string, upstream: string, workspaceId: string, branch = BRANCH) {
|
|
35
|
+
const workspaceDir = path.join(root, workspaceId);
|
|
36
|
+
const repo = path.join(workspaceDir, 'knowledge-base');
|
|
37
|
+
await fs.mkdir(workspaceDir, { recursive: true });
|
|
38
|
+
await runGit(root, ['-c', 'core.autocrlf=false', 'clone', '-b', branch, upstream, repo]);
|
|
39
|
+
await runGit(repo, ['config', 'user.email', 'workspace@bevel.test']);
|
|
40
|
+
await runGit(repo, ['config', 'user.name', 'bevel Workspace']);
|
|
41
|
+
await runGit(repo, ['config', 'core.autocrlf', 'false']);
|
|
42
|
+
return { workspaceDir, repo };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Land one commit on origin from a second clone; returns the paths it touched. */
|
|
46
|
+
async function pushFromElsewhere(root: string, upstream: string, edit: (dir: string) => Promise<void>, branch = BRANCH) {
|
|
47
|
+
const pusher = path.join(root, `.pusher-${Math.random().toString(36).slice(2)}`);
|
|
48
|
+
await runGit(root, ['-c', 'core.autocrlf=false', 'clone', '-b', branch, upstream, pusher]);
|
|
49
|
+
await runGit(pusher, ['config', 'user.email', 'other@bevel.test']);
|
|
50
|
+
await runGit(pusher, ['config', 'user.name', 'Other']);
|
|
51
|
+
await edit(pusher);
|
|
52
|
+
await runGit(pusher, ['add', '-A']);
|
|
53
|
+
await runGit(pusher, ['commit', '-m', 'from origin']);
|
|
54
|
+
await runGit(pusher, ['push', 'origin', branch]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('GitService.syncFromRemote', () => {
|
|
58
|
+
let root: string;
|
|
59
|
+
|
|
60
|
+
beforeEach(async () => {
|
|
61
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-git-sync-'));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterEach(async () => {
|
|
65
|
+
for (let i = 0; i < 5; i++) {
|
|
66
|
+
try {
|
|
67
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
68
|
+
return;
|
|
69
|
+
} catch {
|
|
70
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('reports where HEAD was and is, that the tree changed, and every path including both ends of a rename', async () => {
|
|
76
|
+
const upstream = await bareUpstream(root, true);
|
|
77
|
+
const { workspaceDir, repo } = await cloneWorkspace(root, upstream, 'ws');
|
|
78
|
+
const before = (await gitOut(repo, ['rev-parse', 'HEAD'])).trim();
|
|
79
|
+
await pushFromElsewhere(root, upstream, async (dir) => {
|
|
80
|
+
await fs.writeFile(path.join(dir, 'a.md'), 'a changed\n');
|
|
81
|
+
await fs.writeFile(path.join(dir, 'b.md'), 'b\n');
|
|
82
|
+
await fs.rename(path.join(dir, 'old name.md'), path.join(dir, 'new name.md'));
|
|
83
|
+
});
|
|
84
|
+
const git = new GitService(stubWorkspaceService({ ws: workspaceDir }), stubWorkflowHooks(), 'knowledge-base');
|
|
85
|
+
|
|
86
|
+
const r = await git.syncFromRemote('ws');
|
|
87
|
+
expect(r.before).toBe(before);
|
|
88
|
+
expect(r.after).toBe((await gitOut(repo, ['rev-parse', 'HEAD'])).trim());
|
|
89
|
+
expect(r.after).not.toBe(before);
|
|
90
|
+
expect(r.treeChanged).toBe(true);
|
|
91
|
+
expect([...r.changedPaths].sort()).toEqual(['a.md', 'b.md', 'new name.md', 'old name.md'].sort());
|
|
92
|
+
|
|
93
|
+
// Nothing new: same shas, no change, no paths.
|
|
94
|
+
const again = await git.syncFromRemote('ws');
|
|
95
|
+
expect(again).toEqual({ before: r.after, after: r.after, treeChanged: false, changedPaths: [] });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('an unborn clone of an empty upstream is tolerated, and receives its first content as a change from null', async () => {
|
|
99
|
+
const upstream = await bareUpstream(root, false);
|
|
100
|
+
const workspaceDir = path.join(root, 'ws');
|
|
101
|
+
const repo = path.join(workspaceDir, 'knowledge-base');
|
|
102
|
+
await fs.mkdir(workspaceDir, { recursive: true });
|
|
103
|
+
// Cloning an empty repository warns but succeeds; HEAD is unborn.
|
|
104
|
+
await runGit(root, ['-c', 'core.autocrlf=false', 'clone', upstream, repo]);
|
|
105
|
+
await runGit(repo, ['config', 'user.email', 'workspace@bevel.test']);
|
|
106
|
+
await runGit(repo, ['config', 'user.name', 'bevel Workspace']);
|
|
107
|
+
await runGit(repo, ['checkout', '-b', BRANCH]).catch(() => undefined);
|
|
108
|
+
const git = new GitService(stubWorkspaceService({ ws: workspaceDir }), stubWorkflowHooks(), 'knowledge-base');
|
|
109
|
+
|
|
110
|
+
// Still empty on both sides: nothing to sync, and NOT 'remote gone' — a
|
|
111
|
+
// fresh deployment nobody has pushed to must never have its clone retired.
|
|
112
|
+
expect(await git.syncFromRemote('ws')).toEqual({ before: null, after: null, treeChanged: false, changedPaths: [] });
|
|
113
|
+
|
|
114
|
+
// Origin gains its first commit.
|
|
115
|
+
const seed = path.join(root, '.first');
|
|
116
|
+
await fs.mkdir(seed);
|
|
117
|
+
await runGit(seed, ['init', '-b', BRANCH]);
|
|
118
|
+
await runGit(seed, ['config', 'user.email', 'other@bevel.test']);
|
|
119
|
+
await runGit(seed, ['config', 'user.name', 'Other']);
|
|
120
|
+
await fs.writeFile(path.join(seed, 'README.md'), 'hello\n');
|
|
121
|
+
await runGit(seed, ['add', '.']);
|
|
122
|
+
await runGit(seed, ['commit', '-m', 'first']);
|
|
123
|
+
await runGit(seed, ['push', upstream, BRANCH]);
|
|
124
|
+
|
|
125
|
+
const r = await git.syncFromRemote('ws');
|
|
126
|
+
expect(r.before).toBeNull();
|
|
127
|
+
expect(r.after).toMatch(/^[0-9a-f]{40}$/);
|
|
128
|
+
expect(r.treeChanged).toBe(true);
|
|
129
|
+
expect(r.changedPaths).toEqual(['README.md']);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('a branch deleted on the host is a typed "remote branch gone", not an unreachable remote', async () => {
|
|
133
|
+
const upstream = await bareUpstream(root, true);
|
|
134
|
+
await runGit(upstream, ['branch', 'ali/x', BRANCH]);
|
|
135
|
+
const { workspaceDir } = await cloneWorkspace(root, upstream, 'ali%2Fx', 'ali/x');
|
|
136
|
+
await runGit(upstream, ['update-ref', '-d', 'refs/heads/ali/x']);
|
|
137
|
+
const git = new GitService(stubWorkspaceService({ 'ali%2Fx': workspaceDir }), stubWorkflowHooks(), 'knowledge-base');
|
|
138
|
+
await expect(git.syncFromRemote('ali%2Fx')).rejects.toBeInstanceOf(RemoteBranchGoneError);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('GitService.syncFromRemote — the cases git exits 0 on but should not pass', () => {
|
|
143
|
+
let root: string;
|
|
144
|
+
beforeEach(async () => {
|
|
145
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-git-sync2-'));
|
|
146
|
+
});
|
|
147
|
+
afterEach(async () => {
|
|
148
|
+
for (let i = 0; i < 5; i++) {
|
|
149
|
+
try {
|
|
150
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
151
|
+
return;
|
|
152
|
+
} catch {
|
|
153
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('an autostashed edit that conflicts on reapply is a typed conflict, the tree is left clean, the edit is kept in stash', async () => {
|
|
159
|
+
const upstream = await bareUpstream(root, true);
|
|
160
|
+
const { workspaceDir, repo } = await cloneWorkspace(root, upstream, 'ws');
|
|
161
|
+
// A tracked edit nobody committed — what a lock-holder's in-progress save looks like.
|
|
162
|
+
await fs.writeFile(path.join(repo, 'a.md'), 'edited locally\n');
|
|
163
|
+
await pushFromElsewhere(root, upstream, async (dir) => {
|
|
164
|
+
await fs.writeFile(path.join(dir, 'a.md'), 'edited on the host\n');
|
|
165
|
+
});
|
|
166
|
+
const git = new GitService(stubWorkspaceService({ ws: workspaceDir }), stubWorkflowHooks(), 'knowledge-base');
|
|
167
|
+
|
|
168
|
+
await expect(git.syncFromRemote('ws')).rejects.toMatchObject({
|
|
169
|
+
name: 'PullRebaseConflictError',
|
|
170
|
+
conflictedPaths: ['a.md'],
|
|
171
|
+
});
|
|
172
|
+
// Clean tree at the remote commit; the local edit survives in the stash.
|
|
173
|
+
expect(await gitOut(repo, ['status', '--porcelain'])).toBe('');
|
|
174
|
+
expect(await gitOut(repo, ['rev-parse', 'HEAD'])).toBe(await gitOut(repo, ['rev-parse', 'refs/remotes/origin/' + BRANCH]));
|
|
175
|
+
expect((await gitOut(repo, ['stash', 'list'])).split('\n').filter(Boolean)).toHaveLength(1);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('an unborn clone whose branch was created and deleted on an origin that has other branches is remote gone, not fresh', async () => {
|
|
179
|
+
const upstream = await bareUpstream(root, true); // origin has BRANCH
|
|
180
|
+
const workspaceDir = path.join(root, 'ws');
|
|
181
|
+
const repo = path.join(workspaceDir, 'knowledge-base');
|
|
182
|
+
await fs.mkdir(workspaceDir, { recursive: true });
|
|
183
|
+
// An unborn clone checked out on a branch origin does not have.
|
|
184
|
+
await runGit(root, ['-c', 'core.autocrlf=false', 'clone', upstream, repo]);
|
|
185
|
+
await runGit(repo, ['checkout', '--orphan', 'ali/x']);
|
|
186
|
+
await runGit(repo, ['rm', '-rf', '--cached', '.']).catch(() => undefined);
|
|
187
|
+
await fs.rm(path.join(repo, 'a.md'), { force: true });
|
|
188
|
+
await fs.rm(path.join(repo, 'old name.md'), { force: true });
|
|
189
|
+
const git = new GitService(stubWorkspaceService({ ws: workspaceDir }), stubWorkflowHooks(), 'knowledge-base');
|
|
190
|
+
await expect(git.syncFromRemote('ws')).rejects.toBeInstanceOf(RemoteBranchGoneError);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('remoteBranchExists answers from origin, not from the clone', async () => {
|
|
194
|
+
const upstream = await bareUpstream(root, true);
|
|
195
|
+
await runGit(upstream, ['branch', 'ali/x', BRANCH]);
|
|
196
|
+
const { workspaceDir } = await cloneWorkspace(root, upstream, 'ws');
|
|
197
|
+
const git = new GitService(stubWorkspaceService({ ws: workspaceDir }), stubWorkflowHooks(), 'knowledge-base');
|
|
198
|
+
expect(await git.remoteBranchExists('ws', 'ali/x')).toBe(true);
|
|
199
|
+
await runGit(upstream, ['update-ref', '-d', 'refs/heads/ali/x']);
|
|
200
|
+
expect(await git.remoteBranchExists('ws', 'ali/x')).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('GitService.syncFromRemote — an unborn clone that already holds staged files', () => {
|
|
205
|
+
let root: string;
|
|
206
|
+
beforeEach(async () => {
|
|
207
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-git-sync3-'));
|
|
208
|
+
});
|
|
209
|
+
afterEach(async () => {
|
|
210
|
+
for (let i = 0; i < 5; i++) {
|
|
211
|
+
try {
|
|
212
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
213
|
+
return;
|
|
214
|
+
} catch {
|
|
215
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('is refused as a typed conflict naming the staged paths; nothing on disk is touched', async () => {
|
|
221
|
+
const upstream = await bareUpstream(root, false);
|
|
222
|
+
const workspaceDir = path.join(root, 'ws');
|
|
223
|
+
const repo = path.join(workspaceDir, 'knowledge-base');
|
|
224
|
+
await fs.mkdir(workspaceDir, { recursive: true });
|
|
225
|
+
await runGit(root, ['-c', 'core.autocrlf=false', 'clone', upstream, repo]);
|
|
226
|
+
await runGit(repo, ['config', 'user.email', 'workspace@bevel.test']);
|
|
227
|
+
await runGit(repo, ['config', 'user.name', 'bevel Workspace']);
|
|
228
|
+
await runGit(repo, ['checkout', '-b', BRANCH]).catch(() => undefined);
|
|
229
|
+
// Someone wrote and staged a file before the first commit ever existed —
|
|
230
|
+
// with a name git's line output would quote ("Entw\\303\\274rfe.md"), so
|
|
231
|
+
// the outcome has to carry the real spelling.
|
|
232
|
+
await fs.writeFile(path.join(repo, 'Entwürfe.md'), 'not yet committed\n');
|
|
233
|
+
await runGit(repo, ['add', 'Entwürfe.md']);
|
|
234
|
+
// Origin gains its first commit meanwhile.
|
|
235
|
+
const seed = path.join(root, '.first');
|
|
236
|
+
await fs.mkdir(seed);
|
|
237
|
+
await runGit(seed, ['init', '-b', BRANCH]);
|
|
238
|
+
await runGit(seed, ['config', 'user.email', 'other@bevel.test']);
|
|
239
|
+
await runGit(seed, ['config', 'user.name', 'Other']);
|
|
240
|
+
await fs.writeFile(path.join(seed, 'README.md'), 'hello\n');
|
|
241
|
+
await runGit(seed, ['add', '.']);
|
|
242
|
+
await runGit(seed, ['commit', '-m', 'first']);
|
|
243
|
+
await runGit(seed, ['push', upstream, BRANCH]);
|
|
244
|
+
const git = new GitService(stubWorkspaceService({ ws: workspaceDir }), stubWorkflowHooks(), 'knowledge-base');
|
|
245
|
+
|
|
246
|
+
await expect(git.syncFromRemote('ws')).rejects.toMatchObject({
|
|
247
|
+
name: 'PullRebaseConflictError',
|
|
248
|
+
conflictedPaths: ['Entwürfe.md'],
|
|
249
|
+
});
|
|
250
|
+
expect(await fs.readFile(path.join(repo, 'Entwürfe.md'), 'utf8')).toBe('not yet committed\n');
|
|
251
|
+
expect((await gitOut(repo, ['ls-files', '--cached', '-z'])).replace(/\0$/, '')).toBe('Entwürfe.md');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
BranchInfo,
|
|
8
8
|
CommitAttribution,
|
|
9
9
|
IGitService,
|
|
10
|
+
RemoteSyncPullResult,
|
|
10
11
|
PrFileStatus,
|
|
11
12
|
PullRequestFile,
|
|
12
13
|
ShareChangesRequest,
|
|
@@ -32,6 +33,8 @@ import {
|
|
|
32
33
|
WorkflowValidationError,
|
|
33
34
|
ProtectedBranchError,
|
|
34
35
|
PullRebaseConflictError,
|
|
36
|
+
RemoteBranchGoneError,
|
|
37
|
+
isMissingRemoteBranchFailure,
|
|
35
38
|
} from '../../../shared/domain-errors.js';
|
|
36
39
|
|
|
37
40
|
const execFileAsync = promisify(execFile);
|
|
@@ -1546,70 +1549,250 @@ export class GitService implements IGitService {
|
|
|
1546
1549
|
return `refs/remotes/origin/${branch}`;
|
|
1547
1550
|
}
|
|
1548
1551
|
|
|
1549
|
-
|
|
1552
|
+
/** A commit-free tree, for diffing against a HEAD that did not exist yet. */
|
|
1553
|
+
private static readonly EMPTY_TREE_SHA = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* The checked-out branch by its symbolic ref, which an UNBORN HEAD (a clone
|
|
1557
|
+
* of an empty upstream) still has — `rev-parse --abbrev-ref HEAD` does not
|
|
1558
|
+
* resolve there, so the tolerant tree reads below would never be reached.
|
|
1559
|
+
*/
|
|
1560
|
+
private async checkedOutBranch(cwd: string): Promise<string> {
|
|
1561
|
+
const { stdout } = await this.git(cwd, ['symbolic-ref', '--short', 'HEAD']);
|
|
1562
|
+
return stdout.trim();
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/**
|
|
1566
|
+
* Whether origin still has `branch`. `ls-remote --exit-code` answers with
|
|
1567
|
+
* exit status 2 for "no such ref" — the one failure that is a fact about the
|
|
1568
|
+
* branch rather than about reaching the host, which propagates.
|
|
1569
|
+
*/
|
|
1570
|
+
async remoteBranchExists(workspaceId: string, branch: string): Promise<boolean> {
|
|
1571
|
+
assertValidBranchName(branch);
|
|
1572
|
+
const cwd = await this.repoDir(workspaceId);
|
|
1573
|
+
try {
|
|
1574
|
+
await this.git(cwd, ['ls-remote', '--exit-code', '--heads', 'origin', `refs/heads/${branch}`]);
|
|
1575
|
+
return true;
|
|
1576
|
+
} catch (err) {
|
|
1577
|
+
if ((err as { exitCode?: number }).exitCode === 2) return false;
|
|
1578
|
+
throw err;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
/** Whether origin has ANY branch — an empty repository has none. */
|
|
1583
|
+
private async remoteHasAnyBranch(cwd: string): Promise<boolean> {
|
|
1584
|
+
const { stdout } = await this.git(cwd, ['ls-remote', '--heads', 'origin']);
|
|
1585
|
+
return stdout.trim().length > 0;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
/** `git rev-parse --verify` that answers null for a rev that does not resolve (an unborn HEAD). */
|
|
1589
|
+
private async revParseOrNull(cwd: string, rev: string): Promise<string | null> {
|
|
1590
|
+
try {
|
|
1591
|
+
const { stdout } = await this.git(cwd, ['rev-parse', '--verify', '--quiet', rev]);
|
|
1592
|
+
return stdout.trim() || null;
|
|
1593
|
+
} catch {
|
|
1594
|
+
return null;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/**
|
|
1599
|
+
* Refresh `refs/remotes/origin/<branch>` for a sync, naming the one failure
|
|
1600
|
+
* that is not "origin unreachable": the branch no longer exists there.
|
|
1601
|
+
*/
|
|
1602
|
+
private async refreshRemoteBranchRefForSync(cwd: string, branch: string): Promise<string> {
|
|
1603
|
+
try {
|
|
1604
|
+
return await this.refreshRemoteBranchRef(cwd, branch);
|
|
1605
|
+
} catch (err) {
|
|
1606
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1607
|
+
if (isMissingRemoteBranchFailure(msg)) throw new RemoteBranchGoneError(branch);
|
|
1608
|
+
throw err;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
/**
|
|
1613
|
+
* Rebase the checked-out branch onto `remoteRef`, turning a content
|
|
1614
|
+
* conflict into the typed error the workflow layer queues recovery for.
|
|
1615
|
+
* Shared by `pull` and `syncFromRemote` so the two cannot drift.
|
|
1616
|
+
*/
|
|
1617
|
+
private async rebaseOntoRemote(cwd: string, branch: string, remoteRef: string): Promise<void> {
|
|
1618
|
+
// An UNBORN HEAD (a clone of an upstream that was empty when cloned) has
|
|
1619
|
+
// no commit to replay and `git rebase` refuses to start from it, so the
|
|
1620
|
+
// branch is moved to the remote commit instead. That is only safe while
|
|
1621
|
+
// the index is EMPTY: files staged in an unborn clone are tracked, and a
|
|
1622
|
+
// hard reset would destroy them where the normal path autostashes — and
|
|
1623
|
+
// `git stash` cannot run without an initial commit. So a non-empty index
|
|
1624
|
+
// is refused as a conflict, naming the staged paths, and takes the same
|
|
1625
|
+
// recovery path a rebase conflict does; nothing on disk is touched.
|
|
1626
|
+
if ((await this.revParseOrNull(cwd, 'HEAD')) === null) {
|
|
1627
|
+
// `-z`: NUL-delimited and unquoted. Line output quotes a path with a
|
|
1628
|
+
// non-ASCII or control character ("Entw\303\274rfe.md"), and that
|
|
1629
|
+
// spelling would name a file that does not exist to whoever recovers.
|
|
1630
|
+
const { stdout: stagedOut } = await this.git(cwd, ['ls-files', '--cached', '-z']);
|
|
1631
|
+
const staged = stagedOut.split('\0').filter(Boolean);
|
|
1632
|
+
if (staged.length > 0) {
|
|
1633
|
+
throw new PullRebaseConflictError(
|
|
1634
|
+
branch,
|
|
1635
|
+
staged,
|
|
1636
|
+
'The clone has files staged before its first commit; they cannot be replayed onto the remote branch automatically.',
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
await this.git(cwd, ['reset', '--hard', remoteRef]);
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
try {
|
|
1643
|
+
// `--autostash` is load-bearing for the cooperative push recovery.
|
|
1644
|
+
// Under save=share the working tree is *supposed* to stay clean, but
|
|
1645
|
+
// in practice it can carry modified tracked artifacts (e.g. the
|
|
1646
|
+
// validator regenerates a dashboard HTML on every commit) that the
|
|
1647
|
+
// single-file commit didn't pick up. A rebase without it aborts
|
|
1648
|
+
// outright on those — "cannot rebase: You have unstaged
|
|
1649
|
+
// changes" — which is exactly the failure that strands a diverged
|
|
1650
|
+
// branch in an unrecoverable loop (the retry push stays non-fast-
|
|
1651
|
+
// forward forever). Autostash stashes those modifications, rebases
|
|
1652
|
+
// the local commits onto origin, then reapplies them, so the retry
|
|
1653
|
+
// push can fast-forward. Untracked files don't block rebase and are
|
|
1654
|
+
// left untouched.
|
|
1655
|
+
await this.git(cwd, ['rebase', '--autostash', remoteRef]);
|
|
1656
|
+
} catch (err) {
|
|
1657
|
+
// Capture the contested paths BEFORE the abort wipes the rebase
|
|
1658
|
+
// state — `--diff-filter=U` lists exactly the files whose replay
|
|
1659
|
+
// conflicted. Best-effort: an empty list just means this wasn't a
|
|
1660
|
+
// content conflict (or the probe itself failed) and the raw error
|
|
1661
|
+
// is surfaced unchanged.
|
|
1662
|
+
let conflictedPaths: string[] = [];
|
|
1663
|
+
try {
|
|
1664
|
+
const { stdout } = await this.git(cwd, ['diff', '--name-only', '--diff-filter=U']);
|
|
1665
|
+
conflictedPaths = stdout.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
1666
|
+
} catch {
|
|
1667
|
+
// fall through with an empty list
|
|
1668
|
+
}
|
|
1669
|
+
// A failed rebase leaves the repo in a "REBASE_HEAD" / detached-apply state,
|
|
1670
|
+
// which makes every subsequent git command behave unexpectedly. Return the
|
|
1671
|
+
// working tree to a clean state before surfacing the original error.
|
|
1672
|
+
let abortSucceeded = true;
|
|
1673
|
+
await this.git(cwd, ['rebase', '--abort']).catch(() => {
|
|
1674
|
+
abortSucceeded = false;
|
|
1675
|
+
});
|
|
1676
|
+
if (abortSucceeded && conflictedPaths.length > 0) {
|
|
1677
|
+
// Typed so the workflow layer can queue background recovery — this
|
|
1678
|
+
// divergence never resolves on its own (every retry pull hits the
|
|
1679
|
+
// same conflict) and the unpushed local commits are someone's saved
|
|
1680
|
+
// content stuck violating save=share.
|
|
1681
|
+
//
|
|
1682
|
+
// Gated on the abort succeeding: a failed abort means either no
|
|
1683
|
+
// rebase was ever active (the unmerged paths pre-date this pull —
|
|
1684
|
+
// some earlier operation left the index broken) or the clone is in
|
|
1685
|
+
// a state git itself can't unwind. Queueing recovery there would
|
|
1686
|
+
// point the worker's commitFile at a repo mid-conflict, risking a
|
|
1687
|
+
// commit full of conflict markers — surface the raw error instead
|
|
1688
|
+
// and leave diagnosis to a human.
|
|
1689
|
+
throw new PullRebaseConflictError(
|
|
1690
|
+
branch,
|
|
1691
|
+
conflictedPaths,
|
|
1692
|
+
err instanceof Error ? err.message : String(err),
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
throw err;
|
|
1696
|
+
}
|
|
1697
|
+
// `--autostash` reapplies the stashed edits AFTER the rebase, and when that
|
|
1698
|
+
// reapply conflicts git exits 0: the clone is left with an unmerged index,
|
|
1699
|
+
// the edits kept in `stash@{0}`, and nothing above has thrown. Read the
|
|
1700
|
+
// index rather than the exit status. The rebase itself landed, so the
|
|
1701
|
+
// branch is at the remote commit; resetting to it drops only the
|
|
1702
|
+
// half-applied stash (still in the stash list) and lets the conflict
|
|
1703
|
+
// travel the same typed path a rebase conflict does.
|
|
1704
|
+
const { stdout: unmergedOut } = await this.git(cwd, ['diff', '--name-only', '--diff-filter=U']);
|
|
1705
|
+
const unmerged = unmergedOut.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
1706
|
+
if (unmerged.length > 0) {
|
|
1707
|
+
await this.git(cwd, ['reset', '--hard', 'HEAD']);
|
|
1708
|
+
throw new PullRebaseConflictError(
|
|
1709
|
+
branch,
|
|
1710
|
+
unmerged,
|
|
1711
|
+
'Reapplying the autostashed working-tree edits conflicted; the edits are kept in git stash.',
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/**
|
|
1717
|
+
* Repo-relative paths whose content differs between two commits. Rename-
|
|
1718
|
+
* aware: a renamed file reports both its old and new path, so a viewer
|
|
1719
|
+
* holding the OLD path learns it is gone. Caller holds the mutex.
|
|
1720
|
+
*/
|
|
1721
|
+
private async changedPathsBetween(cwd: string, fromSha: string, toSha: string): Promise<string[]> {
|
|
1722
|
+
const { stdout } = await this.git(cwd, [
|
|
1723
|
+
'diff', '--name-status', '-M', '--no-color', '-z', fromSha, toSha,
|
|
1724
|
+
]);
|
|
1725
|
+
const fields = stdout.split('\0');
|
|
1726
|
+
const paths = new Set<string>();
|
|
1727
|
+
for (let i = 0; i < fields.length; ) {
|
|
1728
|
+
const status = fields[i];
|
|
1729
|
+
if (!status) break;
|
|
1730
|
+
if (status.startsWith('R') || status.startsWith('C')) {
|
|
1731
|
+
if (fields[i + 1]) paths.add(fields[i + 1]);
|
|
1732
|
+
if (fields[i + 2]) paths.add(fields[i + 2]);
|
|
1733
|
+
i += 3;
|
|
1734
|
+
} else {
|
|
1735
|
+
if (fields[i + 1]) paths.add(fields[i + 1]);
|
|
1736
|
+
i += 2;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
return [...paths];
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
async syncFromRemote(workspaceId: string): Promise<RemoteSyncPullResult> {
|
|
1743
|
+
// ONE mutex section for everything the sync reports about this clone.
|
|
1744
|
+
// Bracketing `pull` with separate HEAD reads and a diff took the hold
|
|
1745
|
+
// four times, and a save landing between any two of them was announced
|
|
1746
|
+
// back to its author as a "Git sync" change with the wrong sha.
|
|
1550
1747
|
return this.mutex.run(workspaceId, async () => {
|
|
1551
1748
|
const cwd = await this.repoDir(workspaceId);
|
|
1552
|
-
const branch = await this.
|
|
1749
|
+
const branch = await this.checkedOutBranch(cwd);
|
|
1750
|
+
const before = await this.revParseOrNull(cwd, 'HEAD');
|
|
1751
|
+
const treeBefore = (await this.revParseOrNull(cwd, 'HEAD^{tree}')) ?? '';
|
|
1752
|
+
let remoteRef: string;
|
|
1553
1753
|
try {
|
|
1554
|
-
|
|
1555
|
-
// `refreshRemoteBranchRef` for why both halves are load-bearing.
|
|
1556
|
-
//
|
|
1557
|
-
// `--autostash` is load-bearing for the cooperative push recovery.
|
|
1558
|
-
// Under save=share the working tree is *supposed* to stay clean, but
|
|
1559
|
-
// in practice it can carry modified tracked artifacts (e.g. the
|
|
1560
|
-
// validator regenerates a dashboard HTML on every commit) that the
|
|
1561
|
-
// single-file commit didn't pick up. A rebase without it aborts
|
|
1562
|
-
// outright on those — "cannot rebase: You have unstaged
|
|
1563
|
-
// changes" — which is exactly the failure that strands a diverged
|
|
1564
|
-
// branch in an unrecoverable loop (the retry push stays non-fast-
|
|
1565
|
-
// forward forever). Autostash stashes those modifications, rebases
|
|
1566
|
-
// the local commits onto origin, then reapplies them, so the retry
|
|
1567
|
-
// push can fast-forward. Untracked files don't block rebase and are
|
|
1568
|
-
// left untouched.
|
|
1569
|
-
const remoteRef = await this.refreshRemoteBranchRef(cwd, branch);
|
|
1570
|
-
await this.git(cwd, ['rebase', '--autostash', remoteRef]);
|
|
1754
|
+
remoteRef = await this.refreshRemoteBranchRefForSync(cwd, branch);
|
|
1571
1755
|
} catch (err) {
|
|
1572
|
-
//
|
|
1573
|
-
//
|
|
1574
|
-
//
|
|
1575
|
-
//
|
|
1576
|
-
// is
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
const { stdout } = await this.git(cwd, ['diff', '--name-only', '--diff-filter=U']);
|
|
1580
|
-
conflictedPaths = stdout.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
1581
|
-
} catch {
|
|
1582
|
-
// fall through with an empty list
|
|
1583
|
-
}
|
|
1584
|
-
// A failed rebase leaves the repo in a "REBASE_HEAD" / detached-apply state,
|
|
1585
|
-
// which makes every subsequent git command behave unexpectedly. Return the
|
|
1586
|
-
// working tree to a clean state before surfacing the original error.
|
|
1587
|
-
let abortSucceeded = true;
|
|
1588
|
-
await this.git(cwd, ['rebase', '--abort']).catch(() => {
|
|
1589
|
-
abortSucceeded = false;
|
|
1590
|
-
});
|
|
1591
|
-
if (abortSucceeded && conflictedPaths.length > 0) {
|
|
1592
|
-
// Typed so the workflow layer can queue background recovery — this
|
|
1593
|
-
// divergence never resolves on its own (every retry pull hits the
|
|
1594
|
-
// same conflict) and the unpushed local commits are someone's saved
|
|
1595
|
-
// content stuck violating save=share.
|
|
1596
|
-
//
|
|
1597
|
-
// Gated on the abort succeeding: a failed abort means either no
|
|
1598
|
-
// rebase was ever active (the unmerged paths pre-date this pull —
|
|
1599
|
-
// some earlier operation left the index broken) or the clone is in
|
|
1600
|
-
// a state git itself can't unwind. Queueing recovery there would
|
|
1601
|
-
// point the worker's commitFile at a repo mid-conflict, risking a
|
|
1602
|
-
// commit full of conflict markers — surface the raw error instead
|
|
1603
|
-
// and leave diagnosis to a human.
|
|
1604
|
-
throw new PullRebaseConflictError(
|
|
1605
|
-
branch,
|
|
1606
|
-
conflictedPaths,
|
|
1607
|
-
err instanceof Error ? err.message : String(err),
|
|
1608
|
-
);
|
|
1756
|
+
// An unborn clone whose branch origin does not have either: on a FRESH
|
|
1757
|
+
// deployment (origin has no branch at all) nobody has pushed yet —
|
|
1758
|
+
// nothing to sync, nothing to retire. But an origin that does have
|
|
1759
|
+
// branches and lacks this one has had this branch created and deleted
|
|
1760
|
+
// since the clone was made, and the clone is as stale as any other.
|
|
1761
|
+
if (err instanceof RemoteBranchGoneError && before === null && !(await this.remoteHasAnyBranch(cwd))) {
|
|
1762
|
+
return { before: null, after: null, treeChanged: false, changedPaths: [] };
|
|
1609
1763
|
}
|
|
1610
1764
|
throw err;
|
|
1611
1765
|
}
|
|
1766
|
+
await this.rebaseOntoRemote(cwd, branch, remoteRef);
|
|
1767
|
+
this.accessControl?.invalidate(workspaceId);
|
|
1768
|
+
const after = await this.revParseOrNull(cwd, 'HEAD');
|
|
1769
|
+
const treeAfter = (await this.revParseOrNull(cwd, 'HEAD^{tree}')) ?? '';
|
|
1770
|
+
const treeChanged = treeAfter !== treeBefore;
|
|
1771
|
+
const changedPaths =
|
|
1772
|
+
treeChanged && after
|
|
1773
|
+
? await this.changedPathsBetween(cwd, before ?? GitService.EMPTY_TREE_SHA, after)
|
|
1774
|
+
: [];
|
|
1775
|
+
return { before, after, treeChanged, changedPaths };
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
async pull(workspaceId: string): Promise<{ treeChanged: boolean }> {
|
|
1780
|
+
return this.mutex.run(workspaceId, async () => {
|
|
1781
|
+
const cwd = await this.repoDir(workspaceId);
|
|
1782
|
+
const branch = await this.checkedOutBranch(cwd);
|
|
1783
|
+
// For `treeChanged` (see IGitService.pull): TREE ids, not commit ids —
|
|
1784
|
+
// a pull that lands only content-identical commits (an empty commit)
|
|
1785
|
+
// must not read as a content change. Tolerant of an unborn HEAD (a
|
|
1786
|
+
// clone of an empty upstream): "" before + a tree after correctly
|
|
1787
|
+
// reads as changed, "" both sides as not.
|
|
1788
|
+
const treeBefore = (await this.revParseOrNull(cwd, 'HEAD^{tree}')) ?? '';
|
|
1789
|
+
// Refresh WITHOUT `git pull`, and without touching `FETCH_HEAD` — see
|
|
1790
|
+
// `refreshRemoteBranchRef` for why both halves are load-bearing.
|
|
1791
|
+
const remoteRef = await this.refreshRemoteBranchRef(cwd, branch);
|
|
1792
|
+
await this.rebaseOntoRemote(cwd, branch, remoteRef);
|
|
1612
1793
|
this.accessControl?.invalidate(workspaceId);
|
|
1794
|
+
const treeAfter = (await this.revParseOrNull(cwd, 'HEAD^{tree}')) ?? '';
|
|
1795
|
+
return { treeChanged: treeAfter !== treeBefore };
|
|
1613
1796
|
});
|
|
1614
1797
|
}
|
|
1615
1798
|
|
|
@@ -438,6 +438,15 @@ export class PullRequestService implements IPullRequestService {
|
|
|
438
438
|
this.cachedList.clear();
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Evict the CR-list caches alone. Each list entry carries `touchedNodePaths`
|
|
443
|
+
* resolved against a workspace's tree at the time; a remote sync that moved
|
|
444
|
+
* that tree makes them stale without touching any one request.
|
|
445
|
+
*/
|
|
446
|
+
invalidateListCache(): void {
|
|
447
|
+
this.cachedList.clear();
|
|
448
|
+
}
|
|
449
|
+
|
|
441
450
|
private async findRow(prNumber: number): Promise<ChangeRequestRow | null> {
|
|
442
451
|
const [row] = await this.db
|
|
443
452
|
.select()
|