@bevel-software/platform-core-backend 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-guide/access-control.md +17 -0
- package/agent-guide/conventions.md +8 -0
- package/agent-guide/html-views.md +83 -0
- package/agent-guide/new-knowledge-base.md +24 -0
- package/agent-guide/tool-manuals.md +10 -0
- package/dist/assets.d.ts +19 -0
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +26 -2
- package/dist/assets.js.map +1 -1
- package/dist/core/catalog-revision.d.ts +19 -1
- package/dist/core/catalog-revision.d.ts.map +1 -1
- package/dist/core/catalog-revision.js +14 -3
- package/dist/core/catalog-revision.js.map +1 -1
- package/dist/core/create-core-server.d.ts +5 -0
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +54 -3
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +26 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +100 -8
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +16 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +18 -1
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core-config.d.ts +21 -0
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +22 -1
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +8 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +1 -0
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +11 -0
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/directory-sync-bot.js +1 -1
- package/dist/modules/access/directory-sync-bot.js.map +1 -1
- package/dist/modules/agent-guide/agent-guide.d.ts +6 -0
- package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -1
- package/dist/modules/agent-guide/agent-guide.js +8 -0
- package/dist/modules/agent-guide/agent-guide.js.map +1 -1
- package/dist/modules/agent-guide/index.d.ts +1 -1
- package/dist/modules/agent-guide/index.d.ts.map +1 -1
- package/dist/modules/agent-guide/index.js +1 -1
- package/dist/modules/agent-guide/index.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts +5 -0
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +19 -2
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +19 -19
- package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
- package/dist/modules/audit/agent-audit.service.d.ts +21 -2
- package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
- package/dist/modules/audit/agent-audit.service.js +43 -2
- package/dist/modules/audit/agent-audit.service.js.map +1 -1
- package/dist/modules/audit/agent-connection.routes.d.ts +34 -0
- package/dist/modules/audit/agent-connection.routes.d.ts.map +1 -0
- package/dist/modules/audit/agent-connection.routes.js +38 -0
- package/dist/modules/audit/agent-connection.routes.js.map +1 -0
- package/dist/modules/audit/audit.contract.d.ts +24 -0
- package/dist/modules/audit/audit.contract.d.ts.map +1 -1
- package/dist/modules/audit/audit.contract.js.map +1 -1
- package/dist/modules/audit/index.d.ts +1 -0
- package/dist/modules/audit/index.d.ts.map +1 -1
- package/dist/modules/audit/index.js +1 -0
- package/dist/modules/audit/index.js.map +1 -1
- package/dist/modules/auth/account.routes.d.ts.map +1 -1
- package/dist/modules/auth/account.routes.js +10 -3
- package/dist/modules/auth/account.routes.js.map +1 -1
- package/dist/modules/auth/auth.middleware.d.ts +7 -0
- package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
- package/dist/modules/auth/auth.middleware.js +1 -1
- package/dist/modules/auth/auth.middleware.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +11 -3
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +96 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +32 -3
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/embed/account-link.service.d.ts +42 -0
- package/dist/modules/embed/account-link.service.d.ts.map +1 -0
- package/dist/modules/embed/account-link.service.js +50 -0
- package/dist/modules/embed/account-link.service.js.map +1 -0
- package/dist/modules/embed/embed-link.d.ts +58 -0
- package/dist/modules/embed/embed-link.d.ts.map +1 -0
- package/dist/modules/embed/embed-link.js +160 -0
- package/dist/modules/embed/embed-link.js.map +1 -0
- package/dist/modules/embed/embed.errors.d.ts +30 -0
- package/dist/modules/embed/embed.errors.d.ts.map +1 -0
- package/dist/modules/embed/embed.errors.js +42 -0
- package/dist/modules/embed/embed.errors.js.map +1 -0
- package/dist/modules/embed/embed.interface.d.ts +204 -0
- package/dist/modules/embed/embed.interface.d.ts.map +1 -0
- package/dist/modules/embed/embed.interface.js +2 -0
- package/dist/modules/embed/embed.interface.js.map +1 -0
- package/dist/modules/embed/embed.routes.d.ts +59 -0
- package/dist/modules/embed/embed.routes.d.ts.map +1 -0
- package/dist/modules/embed/embed.routes.js +343 -0
- package/dist/modules/embed/embed.routes.js.map +1 -0
- package/dist/modules/embed/embed.service.d.ts +169 -0
- package/dist/modules/embed/embed.service.d.ts.map +1 -0
- package/dist/modules/embed/embed.service.js +627 -0
- package/dist/modules/embed/embed.service.js.map +1 -0
- package/dist/modules/embed/embed.tools.d.ts +51 -0
- package/dist/modules/embed/embed.tools.d.ts.map +1 -0
- package/dist/modules/embed/embed.tools.js +170 -0
- package/dist/modules/embed/embed.tools.js.map +1 -0
- package/dist/modules/embed/mcp-app.d.ts +64 -0
- package/dist/modules/embed/mcp-app.d.ts.map +1 -0
- package/dist/modules/embed/mcp-app.js +147 -0
- package/dist/modules/embed/mcp-app.js.map +1 -0
- package/dist/modules/embed/mcp-app.routes.d.ts +21 -0
- package/dist/modules/embed/mcp-app.routes.d.ts.map +1 -0
- package/dist/modules/embed/mcp-app.routes.js +36 -0
- package/dist/modules/embed/mcp-app.routes.js.map +1 -0
- package/dist/modules/github-app/github-app.connection.d.ts +15 -0
- package/dist/modules/github-app/github-app.connection.d.ts.map +1 -1
- package/dist/modules/github-app/github-app.connection.js +34 -3
- package/dist/modules/github-app/github-app.connection.js.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.d.ts +65 -0
- package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.js +300 -0
- package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
- package/dist/modules/kb-sync/kb-sync.interface.d.ts +5 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.service.js +6 -0
- package/dist/modules/kb-sync/kb-sync.service.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +2 -1
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +28 -1
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +64 -8
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +32 -4
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/onboarding/onboarding.contract.d.ts +80 -0
- package/dist/modules/onboarding/onboarding.contract.d.ts.map +1 -0
- package/dist/modules/onboarding/onboarding.contract.js +7 -0
- package/dist/modules/onboarding/onboarding.contract.js.map +1 -0
- package/dist/modules/onboarding/onboarding.routes.d.ts +18 -0
- package/dist/modules/onboarding/onboarding.routes.d.ts.map +1 -0
- package/dist/modules/onboarding/onboarding.routes.js +81 -0
- package/dist/modules/onboarding/onboarding.routes.js.map +1 -0
- package/dist/modules/onboarding/starter-pack.service.d.ts +213 -0
- package/dist/modules/onboarding/starter-pack.service.d.ts.map +1 -0
- package/dist/modules/onboarding/starter-pack.service.js +492 -0
- package/dist/modules/onboarding/starter-pack.service.js.map +1 -0
- package/dist/modules/onboarding/starter-packs.d.ts +78 -0
- package/dist/modules/onboarding/starter-packs.d.ts.map +1 -0
- package/dist/modules/onboarding/starter-packs.js +156 -0
- package/dist/modules/onboarding/starter-packs.js.map +1 -0
- package/dist/modules/plugins/index.d.ts +1 -1
- package/dist/modules/plugins/index.d.ts.map +1 -1
- package/dist/modules/plugins/index.js +1 -1
- package/dist/modules/plugins/index.js.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.d.ts +19 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.js +18 -1
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts +0 -36
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +31 -0
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +34 -0
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +134 -1
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/repository-source.d.ts +1 -0
- package/dist/modules/settings/repository-source.d.ts.map +1 -1
- package/dist/modules/settings/repository-source.js +3 -3
- package/dist/modules/settings/repository-source.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +50 -32
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/connection-key-rejection.d.ts +10 -1
- package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -1
- package/dist/modules/tool-auth/connection-key-rejection.js +11 -3
- package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts +8 -0
- package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts +24 -2
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +59 -2
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +9 -4
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/index.d.ts +28 -1
- package/dist/modules/tool-helpers/index.d.ts.map +1 -1
- package/dist/modules/tool-helpers/index.js +28 -1
- package/dist/modules/tool-helpers/index.js.map +1 -1
- package/dist/modules/tool-helpers/route-argument-check.d.ts +10 -0
- package/dist/modules/tool-helpers/route-argument-check.d.ts.map +1 -0
- package/dist/modules/tool-helpers/route-argument-check.js +132 -0
- package/dist/modules/tool-helpers/route-argument-check.js.map +1 -0
- package/dist/modules/tool-helpers/route-tool-schemas.d.ts +89 -0
- package/dist/modules/tool-helpers/route-tool-schemas.d.ts.map +1 -0
- package/dist/modules/tool-helpers/route-tool-schemas.js +113 -0
- package/dist/modules/tool-helpers/route-tool-schemas.js.map +1 -0
- package/dist/modules/tool-helpers/tool-def.d.ts +71 -0
- package/dist/modules/tool-helpers/tool-def.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-def.js +70 -6
- package/dist/modules/tool-helpers/tool-def.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +46 -6
- package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.js +157 -8
- package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts +9 -0
- package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +14 -5
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-registry/description-length.d.ts +6 -2
- package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
- package/dist/modules/tool-registry/description-length.js +27 -5
- package/dist/modules/tool-registry/description-length.js.map +1 -1
- package/dist/modules/tool-registry/guide-first.d.ts +4 -0
- package/dist/modules/tool-registry/guide-first.d.ts.map +1 -1
- package/dist/modules/tool-registry/guide-first.js +9 -1
- package/dist/modules/tool-registry/guide-first.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +53 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js +97 -68
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +8 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +124 -37
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +29 -5
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +62 -5
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +72 -1
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +172 -36
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.d.ts +15 -0
- package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.js +78 -6
- package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +15 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +25 -1
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.d.ts +6 -1
- package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.js +23 -6
- package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
- package/dist/modules/workflow/recovery-bot.js +1 -1
- package/dist/modules/workflow/recovery-bot.js.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +2 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/sanitize-error.d.ts +16 -0
- package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
- package/dist/modules/workflow/sanitize-error.js +45 -0
- package/dist/modules/workflow/sanitize-error.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +183 -15
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +805 -106
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-download.builder.d.ts +105 -0
- package/dist/modules/workspace/agent-download.builder.d.ts.map +1 -0
- package/dist/modules/workspace/agent-download.builder.js +238 -0
- package/dist/modules/workspace/agent-download.builder.js.map +1 -0
- package/dist/modules/workspace/agent-download.routes.d.ts +77 -0
- package/dist/modules/workspace/agent-download.routes.d.ts.map +1 -0
- package/dist/modules/workspace/agent-download.routes.js +156 -0
- package/dist/modules/workspace/agent-download.routes.js.map +1 -0
- package/dist/modules/workspace/agent-download.store.d.ts +225 -0
- package/dist/modules/workspace/agent-download.store.d.ts.map +1 -0
- package/dist/modules/workspace/agent-download.store.js +423 -0
- package/dist/modules/workspace/agent-download.store.js.map +1 -0
- package/dist/modules/workspace/agent-upload.store.d.ts +10 -1
- package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -1
- package/dist/modules/workspace/agent-upload.store.js +16 -6
- package/dist/modules/workspace/agent-upload.store.js.map +1 -1
- package/dist/modules/workspace/file-readers/raw-mime.d.ts +6 -0
- package/dist/modules/workspace/file-readers/raw-mime.d.ts.map +1 -0
- package/dist/modules/workspace/file-readers/raw-mime.js +38 -0
- package/dist/modules/workspace/file-readers/raw-mime.js.map +1 -0
- package/dist/modules/workspace/file-readers/text-reader.d.ts +5 -5
- package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/text-reader.js +5 -8
- package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
- package/dist/modules/workspace/first-run.d.ts +111 -0
- package/dist/modules/workspace/first-run.d.ts.map +1 -0
- package/dist/modules/workspace/first-run.js +181 -0
- package/dist/modules/workspace/first-run.js.map +1 -0
- package/dist/modules/workspace/move-links.d.ts +92 -0
- package/dist/modules/workspace/move-links.d.ts.map +1 -0
- package/dist/modules/workspace/move-links.js +173 -0
- package/dist/modules/workspace/move-links.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 +3 -13
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +63 -22
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +75 -2
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +197 -24
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +59 -1
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +443 -65
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/write-access/write-access.d.ts.map +1 -1
- package/dist/modules/write-access/write-access.js +9 -0
- package/dist/modules/write-access/write-access.js.map +1 -1
- package/dist/shared/domain-errors.d.ts +73 -1
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +86 -12
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/git.contract.d.ts +10 -1
- package/dist/shared/git.contract.d.ts.map +1 -1
- package/dist/shared/git.contract.js.map +1 -1
- package/dist/shared/utf8-text.d.ts +15 -0
- package/dist/shared/utf8-text.d.ts.map +1 -0
- package/dist/shared/utf8-text.js +20 -0
- package/dist/shared/utf8-text.js.map +1 -0
- package/dist/tenancy/static-tenant-source.d.ts +2 -1
- package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
- package/dist/tenancy/static-tenant-source.js +9 -2
- package/dist/tenancy/static-tenant-source.js.map +1 -1
- package/mcp-app/page.html +349 -0
- package/migrations/0017_atlassian_account_links.sql +37 -0
- package/migrations/meta/0017_snapshot.json +2396 -0
- package/migrations/meta/_journal.json +8 -1
- package/package.json +5 -3
- package/src/__tests__/packaged-assets-ship.test.ts +16 -4
- package/src/assets.ts +28 -2
- package/src/core/__tests__/catalog-revision.test.ts +40 -0
- package/src/core/__tests__/gated-boot.test.ts +1 -0
- package/src/core/__tests__/lifecycle.test.ts +14 -0
- package/src/core/catalog-revision.ts +15 -2
- package/src/core/create-core-server.ts +67 -3
- package/src/core/create-core-services.ts +140 -6
- package/src/core/lifecycle.ts +28 -2
- package/src/core-config.ts +24 -1
- package/src/index.ts +2 -2
- package/src/modules/access/__tests__/access-control.service.test.ts +19 -0
- package/src/modules/access/access-control.interface.ts +13 -0
- package/src/modules/access/access-control.service.ts +16 -0
- package/src/modules/access/directory-sync-bot.ts +1 -1
- package/src/modules/agent-guide/__tests__/agent-guide.test.ts +58 -0
- package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +19 -1
- package/src/modules/agent-guide/agent-guide.ts +9 -0
- package/src/modules/agent-guide/index.ts +1 -0
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +14 -0
- package/src/modules/agent-instructions/compose.ts +17 -2
- package/src/modules/agent-instructions/shared-file-rules.ts +19 -19
- package/src/modules/audit/__tests__/agent-audit.service.test.ts +64 -4
- package/src/modules/audit/__tests__/agent-connection.routes.test.ts +102 -0
- package/src/modules/audit/agent-audit.service.ts +58 -3
- package/src/modules/audit/agent-connection.routes.ts +56 -0
- package/src/modules/audit/audit.contract.ts +26 -0
- package/src/modules/audit/index.ts +1 -0
- package/src/modules/auth/__tests__/account.routes.test.ts +5 -3
- package/src/modules/auth/account.routes.ts +10 -3
- package/src/modules/auth/auth.middleware.ts +1 -1
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +18 -2
- package/src/modules/code-mode/code-mode.tool.ts +13 -2
- package/src/modules/database/core-schema.ts +33 -3
- package/src/modules/embed/__tests__/account-links.migration.test.ts +98 -0
- package/src/modules/embed/__tests__/embed-link.test.ts +122 -0
- package/src/modules/embed/__tests__/embed.routes.test.ts +362 -0
- package/src/modules/embed/__tests__/embed.service.lock.test.ts +193 -0
- package/src/modules/embed/__tests__/embed.service.test.ts +805 -0
- package/src/modules/embed/__tests__/embed.tools.test.ts +304 -0
- package/src/modules/embed/__tests__/mcp-app.test.ts +207 -0
- package/src/modules/embed/account-link.service.ts +76 -0
- package/src/modules/embed/embed-link.ts +180 -0
- package/src/modules/embed/embed.errors.ts +43 -0
- package/src/modules/embed/embed.interface.ts +203 -0
- package/src/modules/embed/embed.routes.ts +363 -0
- package/src/modules/embed/embed.service.ts +705 -0
- package/src/modules/embed/embed.tools.ts +206 -0
- package/src/modules/embed/mcp-app.routes.ts +41 -0
- package/src/modules/embed/mcp-app.ts +173 -0
- package/src/modules/github-app/__tests__/github-app.test.ts +76 -0
- package/src/modules/github-app/github-app.connection.ts +38 -4
- package/src/modules/kb-fs/__tests__/move-with-edits.test.ts +304 -0
- package/src/modules/kb-fs/locking-filesystem.ts +287 -0
- package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +35 -0
- package/src/modules/kb-sync/kb-sync.interface.ts +2 -0
- package/src/modules/kb-sync/kb-sync.service.ts +6 -0
- package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +68 -11
- package/src/modules/mcp/__tests__/mcp-proxy-helpers.test.ts +37 -2
- package/src/modules/mcp/__tests__/mcp.app.e2e.test.ts +200 -0
- package/src/modules/mcp/__tests__/mcp.e2e.test.ts +3 -1
- package/src/modules/mcp/__tests__/mcp.service.test.ts +23 -13
- package/src/modules/mcp/__tests__/tool-call-interface.e2e.test.ts +499 -0
- package/src/modules/mcp/mcp-auth.middleware.ts +2 -1
- package/src/modules/mcp/mcp.service.ts +81 -12
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +42 -4
- package/src/modules/onboarding/__tests__/onboarding.routes.test.ts +163 -0
- package/src/modules/onboarding/__tests__/starter-pack.service.test.ts +635 -0
- package/src/modules/onboarding/__tests__/starter-packs.test.ts +169 -0
- package/src/modules/onboarding/onboarding.contract.ts +85 -0
- package/src/modules/onboarding/onboarding.routes.ts +85 -0
- package/src/modules/onboarding/starter-pack.service.ts +580 -0
- package/src/modules/onboarding/starter-packs.ts +182 -0
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +25 -2
- package/src/modules/plugins/index.ts +1 -0
- package/src/modules/plugins/plugin-provision.service.ts +22 -1
- package/src/modules/plugins/plugins.routes.ts +29 -0
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +150 -1
- package/src/modules/settings/__tests__/repository-source.test.ts +9 -0
- package/src/modules/settings/deployment-settings.service.ts +131 -1
- package/src/modules/settings/repository-source.ts +4 -4
- package/src/modules/skills/__tests__/skills.tools.description.test.ts +5 -1
- package/src/modules/skills/skills.tools.ts +55 -35
- package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +32 -0
- package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +65 -0
- package/src/modules/tool-auth/connection-key-rejection.ts +13 -3
- package/src/modules/tool-auth/external-api-key.interface.ts +9 -0
- package/src/modules/tool-auth/external-api-key.service.ts +61 -4
- package/src/modules/tool-auth/tool-auth.middleware.ts +13 -4
- package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +502 -0
- package/src/modules/tool-helpers/__tests__/route-argument-check.test.ts +337 -0
- package/src/modules/tool-helpers/index.ts +34 -1
- package/src/modules/tool-helpers/route-argument-check.ts +149 -0
- package/src/modules/tool-helpers/route-tool-schemas.ts +171 -0
- package/src/modules/tool-helpers/tool-def.ts +132 -7
- package/src/modules/tool-helpers/tool-handler.ts +196 -8
- package/src/modules/tool-helpers/tool.contract.ts +9 -0
- package/src/modules/tool-manuals/tool-manuals.tools.ts +15 -5
- package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +14 -1
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +23 -10
- package/src/modules/tool-registry/description-length.ts +34 -7
- package/src/modules/tool-registry/guide-first.ts +8 -1
- package/src/modules/workflow/__tests__/file-lock.service.deletion-gate.test.ts +104 -0
- package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +66 -0
- package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +31 -2
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +110 -6
- package/src/modules/workflow/__tests__/sanitize-error.test.ts +89 -1
- package/src/modules/workflow/__tests__/workflow.routes.push-refused.test.ts +202 -0
- package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +6 -6
- package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +1026 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +468 -10
- package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +4 -0
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +5 -4
- package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +54 -2
- package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +41 -5
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +127 -2
- package/src/modules/workflow/agent-tools/change-request-read.tools.ts +157 -122
- package/src/modules/workflow/agent-tools/workflow.tools.ts +129 -35
- package/src/modules/workflow/file-lock.service.ts +67 -5
- package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +164 -0
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +48 -1
- package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +231 -2
- package/src/modules/workflow/git/git.service.ts +176 -38
- package/src/modules/workflow/git/node-git-runner.ts +82 -7
- package/src/modules/workflow/pending-commits.service.ts +29 -1
- package/src/modules/workflow/pending-commits.worker.ts +24 -6
- package/src/modules/workflow/recovery-bot.ts +1 -1
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +2 -1
- package/src/modules/workflow/sanitize-error.ts +50 -0
- package/src/modules/workflow/workflow.service.ts +879 -112
- package/src/modules/workspace/__tests__/agent-downloads.test.ts +823 -0
- package/src/modules/workspace/__tests__/first-run.test.ts +234 -0
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +5 -2
- package/src/modules/workspace/__tests__/md-links.test.ts +353 -0
- package/src/modules/workspace/__tests__/move-links.test.ts +261 -0
- package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +219 -0
- package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +3 -0
- package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +102 -0
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +52 -5
- package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +190 -0
- package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +8 -4
- package/src/modules/workspace/__tests__/workspace.service.test.ts +50 -8
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +44 -1
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +409 -12
- package/src/modules/workspace/agent-download.builder.ts +308 -0
- package/src/modules/workspace/agent-download.routes.ts +183 -0
- package/src/modules/workspace/agent-download.store.ts +533 -0
- package/src/modules/workspace/agent-upload.store.ts +23 -7
- package/src/modules/workspace/file-readers/raw-mime.ts +40 -0
- package/src/modules/workspace/file-readers/text-reader.ts +5 -8
- package/src/modules/workspace/first-run.ts +222 -0
- package/src/modules/workspace/move-links.ts +237 -0
- package/src/modules/workspace/startup/steps/seed-tree.ts +3 -12
- package/src/modules/workspace/workspace.routes.ts +64 -22
- package/src/modules/workspace/workspace.service.ts +202 -25
- package/src/modules/workspace/workspace.tools.ts +492 -63
- package/src/modules/write-access/__tests__/write-access.test.ts +13 -0
- package/src/modules/write-access/write-access.ts +9 -0
- package/src/shared/domain-errors.ts +108 -11
- package/src/shared/git.contract.ts +8 -1
- package/src/shared/utf8-text.ts +21 -0
- package/src/tenancy/__tests__/static-tenant-source.test.ts +1 -0
- package/src/tenancy/static-tenant-source.ts +10 -2
- package/starter-packs/README.md +103 -0
- package/starter-packs/engineering/KnowledgeBase/About us.md +9 -0
- package/starter-packs/engineering/KnowledgeBase/Architecture.md +9 -0
- package/starter-packs/engineering/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/engineering/KnowledgeBase/How we ship.md +9 -0
- package/starter-packs/engineering/Plugins/engineering-starter/LICENSE +21 -0
- package/starter-packs/engineering/Plugins/engineering-starter/SOURCE.md +20 -0
- package/starter-packs/engineering/Plugins/engineering-starter/access.md +19 -0
- package/starter-packs/engineering/Plugins/engineering-starter/plugin.json +10 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/SKILL.md +171 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2a-startup-diagnostic.md +157 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2b-builder-brainstorm.md +39 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-ceo-review/SKILL.md +144 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-eng-review/SKILL.md +144 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/review/SKILL.md +80 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/review/checklist.md +184 -0
- package/starter-packs/engineering/pack.yaml +11 -0
- package/starter-packs/general/KnowledgeBase/About us.md +9 -0
- package/starter-packs/general/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/general/KnowledgeBase/How we work.md +9 -0
- package/starter-packs/general/Plugins/general-starter/LICENSE +202 -0
- package/starter-packs/general/Plugins/general-starter/SOURCE.md +10 -0
- package/starter-packs/general/Plugins/general-starter/access.md +19 -0
- package/starter-packs/general/Plugins/general-starter/plugin.json +10 -0
- package/starter-packs/general/Plugins/general-starter/skills/memory-management/SKILL.md +332 -0
- package/starter-packs/general/Plugins/general-starter/skills/productivity-start/SKILL.md +163 -0
- package/starter-packs/general/Plugins/general-starter/skills/productivity-update/SKILL.md +167 -0
- package/starter-packs/general/Plugins/general-starter/skills/task-management/SKILL.md +83 -0
- package/starter-packs/general/pack.yaml +10 -0
- package/starter-packs/operations/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/operations/KnowledgeBase/How we work.md +9 -0
- package/starter-packs/operations/KnowledgeBase/Processes.md +9 -0
- package/starter-packs/operations/KnowledgeBase/Tools we use.md +9 -0
- package/starter-packs/operations/KnowledgeBase/Vendors.md +9 -0
- package/starter-packs/operations/Plugins/operations-starter/LICENSE +202 -0
- package/starter-packs/operations/Plugins/operations-starter/SOURCE.md +10 -0
- package/starter-packs/operations/Plugins/operations-starter/access.md +19 -0
- package/starter-packs/operations/Plugins/operations-starter/plugin.json +10 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/capacity-plan/SKILL.md +111 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/change-request/SKILL.md +119 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/compliance-tracking/SKILL.md +44 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/process-doc/SKILL.md +80 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/process-optimization/SKILL.md +39 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/risk-assessment/SKILL.md +40 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/runbook/SKILL.md +85 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/status-report/SKILL.md +75 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/vendor-review/SKILL.md +102 -0
- package/starter-packs/operations/pack.yaml +12 -0
- package/starter-packs/sales/KnowledgeBase/About us.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Customers.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/sales/KnowledgeBase/How we sell.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Objections.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Pricing.md +9 -0
- package/starter-packs/sales/Plugins/sales-starter/LICENSE +202 -0
- package/starter-packs/sales/Plugins/sales-starter/SOURCE.md +10 -0
- package/starter-packs/sales/Plugins/sales-starter/access.md +19 -0
- package/starter-packs/sales/Plugins/sales-starter/plugin.json +10 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-context/SKILL.md +94 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-plan/SKILL.md +95 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-research/SKILL.md +99 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-tiering/SKILL.md +101 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/call-prep/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/call-summary/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/close-plan/SKILL.md +106 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/competitive-intelligence/SKILL.md +102 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/create-an-asset/SKILL.md +105 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/crm-hygiene-check/SKILL.md +103 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/customer-health/SKILL.md +108 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/customer-voice/SKILL.md +94 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/daily-briefing/SKILL.md +61 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-advance-gap/SKILL.md +102 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-review/SKILL.md +105 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-signals/SKILL.md +96 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-slip-scenario/SKILL.md +104 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/draft-outreach/SKILL.md +116 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/end-of-day/SKILL.md +114 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/expansion-whitespace/SKILL.md +97 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/forecast/SKILL.md +100 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/handle-objection/SKILL.md +93 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/inbox-sweep/SKILL.md +114 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/lead-triage/SKILL.md +106 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/log-activity/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/pipeline-review/SKILL.md +97 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/renewal-radar/SKILL.md +95 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/rep-context/SKILL.md +90 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/route-lead/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/sales-setup/SKILL.md +200 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/schedule-meeting/SKILL.md +99 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/stakeholder-map/SKILL.md +107 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/team-pipeline/SKILL.md +105 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/update-opportunity/SKILL.md +112 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/weekly-wrap/SKILL.md +90 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/win-loss-review/SKILL.md +88 -0
- package/starter-packs/sales/pack.yaml +13 -0
- package/starter-packs/support/KnowledgeBase/Escalations.md +9 -0
- package/starter-packs/support/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/support/KnowledgeBase/Known issues.md +9 -0
- package/starter-packs/support/KnowledgeBase/Products.md +9 -0
- package/starter-packs/support/KnowledgeBase/Tone of voice.md +9 -0
- package/starter-packs/support/Plugins/support-starter/LICENSE +202 -0
- package/starter-packs/support/Plugins/support-starter/SOURCE.md +10 -0
- package/starter-packs/support/Plugins/support-starter/access.md +19 -0
- package/starter-packs/support/Plugins/support-starter/plugin.json +10 -0
- package/starter-packs/support/Plugins/support-starter/skills/customer-escalation/SKILL.md +246 -0
- package/starter-packs/support/Plugins/support-starter/skills/customer-research/SKILL.md +250 -0
- package/starter-packs/support/Plugins/support-starter/skills/draft-response/SKILL.md +416 -0
- package/starter-packs/support/Plugins/support-starter/skills/kb-article/SKILL.md +350 -0
- package/starter-packs/support/Plugins/support-starter/skills/ticket-triage/SKILL.md +273 -0
- package/starter-packs/support/pack.yaml +12 -0
|
@@ -1,9 +1,18 @@
|
|
|
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
|
+
import http from 'node:http';
|
|
3
4
|
import os from 'node:os';
|
|
4
5
|
import path from 'node:path';
|
|
6
|
+
import type { AddressInfo } from 'node:net';
|
|
5
7
|
import { NodeGitRunner } from '../node-git-runner.js';
|
|
6
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
GitRunError,
|
|
10
|
+
gitCredentials,
|
|
11
|
+
isGitTimeout,
|
|
12
|
+
type GitCredentials,
|
|
13
|
+
} from '../../../../shared/git.contract.js';
|
|
14
|
+
import { setLogger } from '../../../../shared/logging.js';
|
|
15
|
+
import type { ILogger, LogFields } from '../../../../shared/logger.contract.js';
|
|
7
16
|
|
|
8
17
|
/**
|
|
9
18
|
* These run REAL git, like the rest of this module's suites: the behaviours
|
|
@@ -150,3 +159,223 @@ describe('NodeGitRunner', () => {
|
|
|
150
159
|
}
|
|
151
160
|
});
|
|
152
161
|
});
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* A git host that wants a credential, as a real `ls-remote` over HTTP meets
|
|
165
|
+
* it: a bare repository behind Basic auth, served the dumb way (`info/refs`
|
|
166
|
+
* as a file), which git falls back to when no smart service answers. Every
|
|
167
|
+
* refusal below is git's own — "could not read Username" when it had nothing
|
|
168
|
+
* to offer, "Authentication failed" when what it offered was thrown out.
|
|
169
|
+
*/
|
|
170
|
+
describe('NodeGitRunner: a call the host refuses a credential', () => {
|
|
171
|
+
let root: string;
|
|
172
|
+
let clone: string;
|
|
173
|
+
let server: http.Server;
|
|
174
|
+
let remote: string;
|
|
175
|
+
/** The one token the host accepts. */
|
|
176
|
+
let accepted: string;
|
|
177
|
+
/** How many times the host was asked, and refused, in order. */
|
|
178
|
+
let answers: number[];
|
|
179
|
+
/** No helper from the machine this runs on: the clone's config and the call's `-c` are the whole story. */
|
|
180
|
+
let env: NodeJS.ProcessEnv;
|
|
181
|
+
|
|
182
|
+
beforeEach(async () => {
|
|
183
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'git-runner-auth-'));
|
|
184
|
+
const empty = path.join(root, 'empty-gitconfig');
|
|
185
|
+
await fs.writeFile(empty, '');
|
|
186
|
+
env = { GIT_CONFIG_GLOBAL: empty, GIT_CONFIG_NOSYSTEM: '1' };
|
|
187
|
+
const plain = new NodeGitRunner();
|
|
188
|
+
const seed = path.join(root, 'seed');
|
|
189
|
+
await fs.mkdir(seed);
|
|
190
|
+
await plain.run(seed, ['init', '--initial-branch=main'], { env });
|
|
191
|
+
await plain.run(seed, ['-c', 'user.name=t', '-c', 'user.email=t@x', 'commit', '--allow-empty', '-m', 'init'], { env });
|
|
192
|
+
const upstream = path.join(root, 'upstream.git');
|
|
193
|
+
await plain.run(root, ['clone', '--bare', seed, upstream], { env });
|
|
194
|
+
await plain.run(upstream, ['update-server-info'], { env });
|
|
195
|
+
clone = path.join(root, 'clone');
|
|
196
|
+
await fs.mkdir(clone);
|
|
197
|
+
await plain.run(clone, ['init', '--initial-branch=main'], { env });
|
|
198
|
+
|
|
199
|
+
accepted = 'ghp_good';
|
|
200
|
+
answers = [];
|
|
201
|
+
server = http.createServer(async (req, res) => {
|
|
202
|
+
const expected = `Basic ${Buffer.from(`x-access-token:${accepted}`).toString('base64')}`;
|
|
203
|
+
if (req.headers.authorization !== expected) {
|
|
204
|
+
answers.push(401);
|
|
205
|
+
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="upstream"' });
|
|
206
|
+
res.end();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
answers.push(200);
|
|
210
|
+
const file = path.join(upstream, new URL(req.url ?? '/', 'http://x').pathname.replace(/^\/upstream\.git\/?/, ''));
|
|
211
|
+
try {
|
|
212
|
+
const bytes = await fs.readFile(file);
|
|
213
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
214
|
+
res.end(bytes);
|
|
215
|
+
} catch {
|
|
216
|
+
res.writeHead(404);
|
|
217
|
+
res.end();
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
await new Promise<void>((r) => server.listen(0, '127.0.0.1', r));
|
|
221
|
+
remote = `http://127.0.0.1:${(server.address() as AddressInfo).port}/upstream.git`;
|
|
222
|
+
await plain.run(clone, ['remote', 'add', 'origin', remote], { env });
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
afterEach(async () => {
|
|
226
|
+
await new Promise<void>((r) => server.close(() => r()));
|
|
227
|
+
await fs.rm(root, { recursive: true, force: true }).catch(() => undefined);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
/** Credentials whose token a test moves, with a `prepare` that records how it was asked. */
|
|
231
|
+
function moving(initial: string | null, onRefused: (set: (token: string | null) => void) => void = () => undefined) {
|
|
232
|
+
let token = initial;
|
|
233
|
+
const prepare = vi.fn(async (opts?: { refused?: boolean }) => {
|
|
234
|
+
if (opts?.refused) onRefused((next) => { token = next; });
|
|
235
|
+
});
|
|
236
|
+
const credentials: GitCredentials = {
|
|
237
|
+
username: () => 'x-access-token',
|
|
238
|
+
token: () => token,
|
|
239
|
+
prepare,
|
|
240
|
+
};
|
|
241
|
+
return { credentials, prepare };
|
|
242
|
+
}
|
|
243
|
+
const refusals = (prepare: ReturnType<typeof vi.fn>) => prepare.mock.calls.map((c) => (c[0] as { refused?: boolean } | undefined)?.refused ?? false);
|
|
244
|
+
|
|
245
|
+
/** Every line the runner logs during a test. */
|
|
246
|
+
let logged: string[] = [];
|
|
247
|
+
let previousLogger: ILogger | undefined;
|
|
248
|
+
const capturing = (bindings: LogFields = {}): ILogger => {
|
|
249
|
+
const line = (level: string) => (message: string, fields?: LogFields) =>
|
|
250
|
+
void logged.push(`${level} ${message} ${JSON.stringify({ ...bindings, ...fields })}`);
|
|
251
|
+
return { debug: line('debug'), info: line('info'), warn: line('warn'), error: line('error'), child: (m) => capturing({ ...bindings, ...m }) };
|
|
252
|
+
};
|
|
253
|
+
beforeEach(() => {
|
|
254
|
+
logged = [];
|
|
255
|
+
previousLogger = setLogger(capturing());
|
|
256
|
+
});
|
|
257
|
+
afterEach(() => {
|
|
258
|
+
if (previousLogger) setLogger(previousLogger);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('authenticates a clone whose config carries no helper: the helper rides the call', async () => {
|
|
262
|
+
const runner = new NodeGitRunner(undefined, gitCredentials('x-access-token', 'ghp_good'));
|
|
263
|
+
const { stdout } = await runner.run(clone, ['ls-remote', 'origin'], { env });
|
|
264
|
+
expect(stdout).toContain('refs/heads/main');
|
|
265
|
+
// Refused once, with nothing offered; then every request carried the token.
|
|
266
|
+
expect(answers.filter((a) => a === 401)).toHaveLength(1);
|
|
267
|
+
expect(answers).toContain(200);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('with nothing to offer, insists on a token and runs the command once more', async () => {
|
|
271
|
+
const { credentials, prepare } = moving(null, (set) => set('ghp_good'));
|
|
272
|
+
const runner = new NodeGitRunner(undefined, credentials);
|
|
273
|
+
const { stdout } = await runner.run(clone, ['ls-remote', 'origin'], { env });
|
|
274
|
+
expect(stdout).toContain('refs/heads/main');
|
|
275
|
+
// The ordinary renewal before the call, then the one that reports the refusal.
|
|
276
|
+
expect(refusals(prepare)).toEqual([false, true]);
|
|
277
|
+
expect(logged.some((l) => l.includes('the token was renewed and the command runs again'))).toBe(true);
|
|
278
|
+
// The first attempt never got to offer anything (one refusal, and git
|
|
279
|
+
// gave up); the second was refused once more before it offered the token.
|
|
280
|
+
expect(answers.filter((a) => a === 401)).toHaveLength(2);
|
|
281
|
+
expect(answers).toContain(200);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('with a token the host throws out, renews it and runs the command once more', async () => {
|
|
285
|
+
const { credentials, prepare } = moving('ghp_stale', (set) => set('ghp_good'));
|
|
286
|
+
const runner = new NodeGitRunner(undefined, credentials);
|
|
287
|
+
const { stdout } = await runner.run(clone, ['ls-remote', 'origin'], { env });
|
|
288
|
+
expect(stdout).toContain('refs/heads/main');
|
|
289
|
+
expect(refusals(prepare)).toEqual([false, true]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('runs the command again ONCE: a refusal with a token just issued is the answer', async () => {
|
|
293
|
+
const { credentials, prepare } = moving('ghp_stale', (set) => set('ghp_still_stale'));
|
|
294
|
+
const runner = new NodeGitRunner(undefined, credentials);
|
|
295
|
+
const err = await runner.run(clone, ['ls-remote', 'origin'], { env }).catch((e: unknown) => e);
|
|
296
|
+
expect(err).toBeInstanceOf(GitRunError);
|
|
297
|
+
expect(`${(err as GitRunError).stderr}`).toMatch(/Authentication failed/);
|
|
298
|
+
expect(refusals(prepare).filter(Boolean)).toHaveLength(1);
|
|
299
|
+
// One attempt with a credential offered is two refusals: git asks bare
|
|
300
|
+
// first, then with the token. Two attempts, four — and no third.
|
|
301
|
+
expect(answers).toEqual([401, 401, 401, 401]);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('does not run the command again when no token can be had, and keeps the refusal', async () => {
|
|
305
|
+
const { credentials, prepare } = moving(null, (set) => set(null));
|
|
306
|
+
const runner = new NodeGitRunner(undefined, credentials);
|
|
307
|
+
const err = await runner.run(clone, ['ls-remote', 'origin'], { env }).catch((e: unknown) => e);
|
|
308
|
+
expect(err).toBeInstanceOf(GitRunError);
|
|
309
|
+
expect(`${(err as GitRunError).stderr}`).toMatch(/could not read Username/);
|
|
310
|
+
expect(refusals(prepare).filter(Boolean)).toHaveLength(1);
|
|
311
|
+
// One attempt: with nothing to offer, running it again would only repeat the refusal.
|
|
312
|
+
expect(answers).toEqual([401]);
|
|
313
|
+
expect(logged.some((l) => l.includes('no other token could be had'))).toBe(true);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('does not run the command again with the SAME token the host just refused', async () => {
|
|
317
|
+
// A provider that cannot replace it: the fixed token of a token-mode
|
|
318
|
+
// deployment, whose `prepare` is absent. Running the command again would
|
|
319
|
+
// only be refused again.
|
|
320
|
+
const runner = new NodeGitRunner(undefined, gitCredentials('x-access-token', 'ghp_stale'));
|
|
321
|
+
const err = await runner.run(clone, ['ls-remote', 'origin'], { env }).catch((e: unknown) => e);
|
|
322
|
+
expect(err).toBeInstanceOf(GitRunError);
|
|
323
|
+
expect(`${(err as GitRunError).stderr}`).toMatch(/Authentication failed/);
|
|
324
|
+
// One attempt: the bare ask, then the token, refused. Nothing after.
|
|
325
|
+
expect(answers).toEqual([401, 401]);
|
|
326
|
+
// And one whose renewal hands back what it had.
|
|
327
|
+
answers = [];
|
|
328
|
+
const { credentials, prepare } = moving('ghp_stale', (set) => set('ghp_stale'));
|
|
329
|
+
await expect(new NodeGitRunner(undefined, credentials).run(clone, ['ls-remote', 'origin'], { env })).rejects.toBeInstanceOf(GitRunError);
|
|
330
|
+
expect(refusals(prepare).filter(Boolean)).toHaveLength(1);
|
|
331
|
+
expect(answers).toEqual([401, 401]);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('keeps the refusal, and logs why, when the renewal itself fails', async () => {
|
|
335
|
+
let token: string | null = 'ghp_stale';
|
|
336
|
+
const credentials: GitCredentials = {
|
|
337
|
+
username: () => 'x-access-token',
|
|
338
|
+
token: () => token,
|
|
339
|
+
prepare: async (opts) => {
|
|
340
|
+
if (!opts?.refused) return;
|
|
341
|
+
// What the host refused is dropped, and GitHub will not replace it.
|
|
342
|
+
token = null;
|
|
343
|
+
throw new Error('GitHub answered 401 to the app: bad private key');
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
const runner = new NodeGitRunner(undefined, credentials);
|
|
347
|
+
const err = await runner.run(clone, ['ls-remote', 'origin'], { env }).catch((e: unknown) => e);
|
|
348
|
+
expect(err).toBeInstanceOf(GitRunError);
|
|
349
|
+
expect(`${(err as GitRunError).stderr}`).toMatch(/Authentication failed/);
|
|
350
|
+
expect(answers).toEqual([401, 401]);
|
|
351
|
+
const why = logged.find((l) => l.includes('no other token could be had'));
|
|
352
|
+
expect(why).toBeDefined();
|
|
353
|
+
expect(why).toContain('bad private key');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("leaves a credential the caller laid over the call to the caller: the probe's own token is not renewed", async () => {
|
|
357
|
+
const { credentials, prepare } = moving('ghp_good', (set) => set('ghp_good'));
|
|
358
|
+
const runner = new NodeGitRunner(undefined, credentials);
|
|
359
|
+
const err = await runner
|
|
360
|
+
.run(clone, ['ls-remote', 'origin'], { env: { ...env, GITHUB_TOKEN: 'ghp_being_checked' } })
|
|
361
|
+
.catch((e: unknown) => e);
|
|
362
|
+
expect(err).toBeInstanceOf(GitRunError);
|
|
363
|
+
expect(refusals(prepare).filter(Boolean)).toHaveLength(0);
|
|
364
|
+
expect(answers).toEqual([401, 401]);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('names the command the caller asked for in a failure, never its own helper', async () => {
|
|
368
|
+
const runner = new NodeGitRunner(undefined, gitCredentials('x-access-token', 'ghp_stale'));
|
|
369
|
+
const err = await runner.run(clone, ['ls-remote', 'origin'], { env }).catch((e: unknown) => e);
|
|
370
|
+
expect((err as Error).message).toContain('git ls-remote failed');
|
|
371
|
+
expect((err as Error).message).not.toContain('credential.helper');
|
|
372
|
+
expect(`${(err as GitRunError).stderr}`).not.toContain('credential.helper');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('leaves every other failure alone', async () => {
|
|
376
|
+
const { credentials, prepare } = moving('ghp_good');
|
|
377
|
+
const runner = new NodeGitRunner(undefined, credentials);
|
|
378
|
+
await expect(runner.run(clone, ['checkout', 'no-such-branch'], { env })).rejects.toBeInstanceOf(GitRunError);
|
|
379
|
+
expect(refusals(prepare).filter(Boolean)).toHaveLength(0);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
@@ -35,6 +35,7 @@ import type { KbContext } from '../../../shared/kb-context.js';
|
|
|
35
35
|
import {
|
|
36
36
|
AppliedChangeMismatchError,
|
|
37
37
|
BranchAuthorshipError,
|
|
38
|
+
BranchDeleteRefusedError,
|
|
38
39
|
WorkflowDomainError,
|
|
39
40
|
WorkflowValidationError,
|
|
40
41
|
ProtectedBranchError,
|
|
@@ -743,12 +744,22 @@ export class GitService implements IGitService {
|
|
|
743
744
|
// fresh per-branch workspace gets its backup seeded by the workspace
|
|
744
745
|
// bootstrap path, not by a branch switch on an existing one.
|
|
745
746
|
|
|
747
|
+
/**
|
|
748
|
+
* Delete a branch, here and on origin. Answers the tip it had — origin's
|
|
749
|
+
* when origin had it, else the local head's; null when neither existed —
|
|
750
|
+
* so the caller can say which commit a restore would start from.
|
|
751
|
+
*
|
|
752
|
+
* `expectTip`, when given, is the tip the caller's checks were made
|
|
753
|
+
* against: the remote delete is then leased on it, and a branch that moved
|
|
754
|
+
* since (a colleague pushed) is refused by the host rather than deleted
|
|
755
|
+
* with work nobody looked at.
|
|
756
|
+
*/
|
|
746
757
|
async deleteBranch(
|
|
747
758
|
workspaceId: string,
|
|
748
759
|
name: string,
|
|
749
760
|
user: AuthUser,
|
|
750
|
-
opts: { onlyIfNoRemote?: boolean; systemCleanup?: boolean } = {},
|
|
751
|
-
): Promise<
|
|
761
|
+
opts: { onlyIfNoRemote?: boolean; systemCleanup?: boolean; expectTip?: string } = {},
|
|
762
|
+
): Promise<{ lastCommit: string | null }> {
|
|
752
763
|
assertValidBranchName(name);
|
|
753
764
|
if (this.kb.isProtectedBranch(name)) {
|
|
754
765
|
throw new ProtectedBranchError(name, 'deleting a protected branch');
|
|
@@ -777,28 +788,33 @@ export class GitService implements IGitService {
|
|
|
777
788
|
`Branch "${name}" still exists on origin — refusing to auto-prune.`,
|
|
778
789
|
);
|
|
779
790
|
}
|
|
780
|
-
} else {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
791
|
+
} else if (!(await this.mayDeleteBranch(workspaceId, name, user))) {
|
|
792
|
+
throw new BranchAuthorshipError(name);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Remote delete FIRST. Skip in `onlyIfNoRemote` mode (the contract is
|
|
796
|
+
// local-only cleanup) and skip when origin has no such branch. Origin
|
|
797
|
+
// is asked, not this clone's remote-tracking ref: a branch pushed from
|
|
798
|
+
// another clone since the last fetch has no tracking ref here, and
|
|
799
|
+
// trusting the cache would delete it locally while it lives on.
|
|
800
|
+
// A refusal by the host — or a host that cannot be asked — fails the
|
|
801
|
+
// whole delete: the local branch is untouched, so nothing is
|
|
802
|
+
// half-deleted and the person can try again. Deleting locally first
|
|
803
|
+
// would leave a branch that vanished here but lives on the remote —
|
|
804
|
+
// and, unlike a refused write, a deleted branch has no later push to
|
|
805
|
+
// reconcile it.
|
|
806
|
+
let remoteTip: string | null = null;
|
|
807
|
+
if (!opts.onlyIfNoRemote) {
|
|
808
|
+
try {
|
|
809
|
+
remoteTip = await this.originBranchTip(cwd, name);
|
|
810
|
+
if (remoteTip !== null) {
|
|
811
|
+
const lease = opts.expectTip ? [`--force-with-lease=refs/heads/${name}:${opts.expectTip}`] : [];
|
|
812
|
+
await this.git(cwd, ['push', ...lease, 'origin', '--delete', name]);
|
|
813
|
+
}
|
|
814
|
+
} catch (err) {
|
|
815
|
+
const detail = sanitizeError(err);
|
|
816
|
+
log.warn(`the repository host refused to delete branch "${name}"; leaving it in place:`, { detail });
|
|
817
|
+
throw new BranchDeleteRefusedError(name, detail);
|
|
802
818
|
}
|
|
803
819
|
}
|
|
804
820
|
|
|
@@ -810,19 +826,91 @@ export class GitService implements IGitService {
|
|
|
810
826
|
// Skip when no local ref exists — the picker can surface branches that
|
|
811
827
|
// exist only as `refs/remotes/origin/<name>` (the user hasn't worked on
|
|
812
828
|
// them locally), and "discard from origin" must still work for those.
|
|
829
|
+
let localTip: string | null = null;
|
|
813
830
|
if (await this.refExists(cwd, `refs/heads/${name}`)) {
|
|
831
|
+
localTip = await this.revParseOrNull(cwd, `refs/heads/${name}`);
|
|
814
832
|
await this.git(cwd, ['branch', '-D', name]);
|
|
815
833
|
}
|
|
834
|
+
return { lastCommit: remoteTip ?? localTip };
|
|
835
|
+
});
|
|
836
|
+
}
|
|
816
837
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
838
|
+
/**
|
|
839
|
+
* The author-or-Admin rule for deleting `name`. The branch's author (per
|
|
840
|
+
* the `<email-localpart>/...` naming convention — see `isBranchAuthoredBy`)
|
|
841
|
+
* can always delete their own draft. Admins (per the workspace's
|
|
842
|
+
* `roles.yaml`) can additionally clean up anyone's branch, including
|
|
843
|
+
* unprefixed CLI branches like `fix/...` that have no recognisable author.
|
|
844
|
+
* Admin is detected via `canWrite(_, _, 'roles.yaml')` because that
|
|
845
|
+
* predicate is the existing source of truth for `Admin`-role membership
|
|
846
|
+
* inside the access model — no separate `isAdmin()` plumbing needed.
|
|
847
|
+
*
|
|
848
|
+
* Two authorship conventions: `<localpart>/…` UI drafts, and the
|
|
849
|
+
* `suggestions/<who>-<id8>/…` namespace the propose flow files its branches
|
|
850
|
+
* under — a user resetting THEIR OWN suggestion bundle is deleting their
|
|
851
|
+
* own branch.
|
|
852
|
+
*/
|
|
853
|
+
async mayDeleteBranch(workspaceId: string, name: string, user: AuthUser): Promise<boolean> {
|
|
854
|
+
const isAuthor =
|
|
855
|
+
isBranchAuthoredBy(name, user.email) ||
|
|
856
|
+
isOwnSuggestionsBranch(name, { email: user.email, id: user.id });
|
|
857
|
+
if (isAuthor) return true;
|
|
858
|
+
return this.accessControl
|
|
859
|
+
? await this.accessControl.canWrite(workspaceId, user.email, 'roles.yaml')
|
|
860
|
+
: false;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Where `name` stands in this clone, read from its refs as the last fetch
|
|
865
|
+
* left them (the caller fetches first): whether it exists on origin or as a
|
|
866
|
+
* local head, its tip (origin's when it has one), and how many commits on
|
|
867
|
+
* it — on either ref — are not on `defaultBranch`.
|
|
868
|
+
*
|
|
869
|
+
* The default branch is read from origin's ref, falling back to the local
|
|
870
|
+
* head on a clone whose default was never pushed. A default that resolves
|
|
871
|
+
* on neither throws: a count against nothing would call every commit
|
|
872
|
+
* unmerged, or none.
|
|
873
|
+
*/
|
|
874
|
+
async branchState(
|
|
875
|
+
workspaceId: string,
|
|
876
|
+
name: string,
|
|
877
|
+
defaultBranch: string,
|
|
878
|
+
): Promise<{ exists: boolean; lastCommit: string | null; unmergedCommits: number }> {
|
|
879
|
+
assertValidBranchName(name);
|
|
880
|
+
assertValidBranchName(defaultBranch);
|
|
881
|
+
return this.mutex.run(workspaceId, async () => {
|
|
882
|
+
const cwd = await this.repoDir(workspaceId);
|
|
883
|
+
const remote = await this.revParseOrNull(cwd, `refs/remotes/origin/${name}`);
|
|
884
|
+
const local = await this.revParseOrNull(cwd, `refs/heads/${name}`);
|
|
885
|
+
if (remote === null && local === null) return { exists: false, lastCommit: null, unmergedCommits: 0 };
|
|
886
|
+
const base =
|
|
887
|
+
(await this.revParseOrNull(cwd, `refs/remotes/origin/${defaultBranch}`)) ??
|
|
888
|
+
(await this.revParseOrNull(cwd, `refs/heads/${defaultBranch}`));
|
|
889
|
+
if (base === null) {
|
|
890
|
+
throw new Error(`the default branch "${defaultBranch}" does not resolve in this clone`);
|
|
891
|
+
}
|
|
892
|
+
const tips = [remote, local].filter((sha): sha is string => sha !== null);
|
|
893
|
+
const { stdout } = await this.git(cwd, ['rev-list', '--count', ...tips, `^${base}`]);
|
|
894
|
+
return { exists: true, lastCommit: remote ?? local, unmergedCommits: Number(stdout.trim()) || 0 };
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Whether commit `ancestor` is contained in commit `descendant`. Exit 1 is
|
|
900
|
+
* git's "no"; anything else — a commit this clone does not have — throws,
|
|
901
|
+
* so a caller deciding to delete on a yes never gets one by accident.
|
|
902
|
+
*/
|
|
903
|
+
async isAncestor(workspaceId: string, ancestor: string, descendant: string): Promise<boolean> {
|
|
904
|
+
const sha = /^[0-9a-f]{4,64}$/i;
|
|
905
|
+
if (!sha.test(ancestor) || !sha.test(descendant)) throw new Error('isAncestor takes commit ids');
|
|
906
|
+
return this.mutex.run(workspaceId, async () => {
|
|
907
|
+
const cwd = await this.repoDir(workspaceId);
|
|
908
|
+
try {
|
|
909
|
+
await this.git(cwd, ['merge-base', '--is-ancestor', ancestor, descendant]);
|
|
910
|
+
return true;
|
|
911
|
+
} catch (err) {
|
|
912
|
+
if (err instanceof GitRunError && !err.timedOut && err.exitCode === 1) return false;
|
|
913
|
+
throw err;
|
|
826
914
|
}
|
|
827
915
|
});
|
|
828
916
|
}
|
|
@@ -1867,7 +1955,21 @@ export class GitService implements IGitService {
|
|
|
1867
1955
|
*/
|
|
1868
1956
|
async remoteBranchExists(workspaceId: string, branch: string): Promise<boolean> {
|
|
1869
1957
|
assertValidBranchName(branch);
|
|
1870
|
-
|
|
1958
|
+
return this.originHasBranch(await this.repoDir(workspaceId), branch);
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
/** Origin's tip of `branch`, asked of origin itself; null when it has no such branch. */
|
|
1962
|
+
private async originBranchTip(cwd: string, branch: string): Promise<string | null> {
|
|
1963
|
+
try {
|
|
1964
|
+
const { stdout } = await this.git(cwd, ['ls-remote', '--exit-code', '--heads', 'origin', `refs/heads/${branch}`]);
|
|
1965
|
+
return /^([0-9a-f]+)\s/.exec(stdout)?.[1] ?? null;
|
|
1966
|
+
} catch (err) {
|
|
1967
|
+
if ((err as { exitCode?: number }).exitCode === 2) return null;
|
|
1968
|
+
throw err;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
private async originHasBranch(cwd: string, branch: string): Promise<boolean> {
|
|
1871
1973
|
try {
|
|
1872
1974
|
await this.git(cwd, ['ls-remote', '--exit-code', '--heads', 'origin', `refs/heads/${branch}`]);
|
|
1873
1975
|
return true;
|
|
@@ -1962,9 +2064,10 @@ export class GitService implements IGitService {
|
|
|
1962
2064
|
// commit the clone holds but origin has not seen into cherry-picks of
|
|
1963
2065
|
// the commits it merged — the merged branch stops being a parent, so
|
|
1964
2066
|
// "does the published head contain the target's head" answers no
|
|
1965
|
-
// however many times the update runs.
|
|
1966
|
-
//
|
|
1967
|
-
//
|
|
2067
|
+
// however many times the update runs. The change-request update and
|
|
2068
|
+
// every push's cooperative recovery ask for it (a merge a refused push
|
|
2069
|
+
// stranded rides along with the branch's next push); with no merge
|
|
2070
|
+
// among the local commits the two spellings produce the same history.
|
|
1968
2071
|
await this.git(cwd, [
|
|
1969
2072
|
'rebase',
|
|
1970
2073
|
'--autostash',
|
|
@@ -2081,7 +2184,12 @@ export class GitService implements IGitService {
|
|
|
2081
2184
|
}
|
|
2082
2185
|
throw err;
|
|
2083
2186
|
}
|
|
2084
|
-
|
|
2187
|
+
// `preserveMerges` here too: this sync runs on a timer, so it is the
|
|
2188
|
+
// pull most likely to reach a branch while a merge a refused open or
|
|
2189
|
+
// update left local is still waiting for its push. A plain rebase would
|
|
2190
|
+
// flatten that merge before the push ever ran, and the branch would be
|
|
2191
|
+
// "behind" its target again however many times it was updated.
|
|
2192
|
+
await this.rebaseOntoRemote(cwd, branch, remoteRef, { preserveMerges: true });
|
|
2085
2193
|
this.accessControl?.invalidate(workspaceId);
|
|
2086
2194
|
const after = await this.revParseOrNull(cwd, 'HEAD');
|
|
2087
2195
|
const treeAfter = (await this.revParseOrNull(cwd, 'HEAD^{tree}')) ?? '';
|
|
@@ -2140,6 +2248,36 @@ export class GitService implements IGitService {
|
|
|
2140
2248
|
return this.mutex.run(workspaceId, async () => this.hasUnpushedCommitsAt(await this.repoDir(workspaceId)));
|
|
2141
2249
|
}
|
|
2142
2250
|
|
|
2251
|
+
/**
|
|
2252
|
+
* Whether this clone's HEAD already holds everything origin has on its
|
|
2253
|
+
* branch: a push would then fast-forward origin to HEAD, and no cooperative
|
|
2254
|
+
* pull-rebase would replay origin's commits INTO HEAD first. Asked after a
|
|
2255
|
+
* fetch, about the fetched ref.
|
|
2256
|
+
*
|
|
2257
|
+
* The roles.yaml preservation asks this before pushing a restore it finds
|
|
2258
|
+
* already committed. "HEAD's roles.yaml is the base version" is only worth
|
|
2259
|
+
* publishing when origin ends up AT HEAD — a clone behind origin would have
|
|
2260
|
+
* origin's divergent copy rebased into it on the way, and the push would
|
|
2261
|
+
* land exactly what the restore exists to keep off the base.
|
|
2262
|
+
*
|
|
2263
|
+
* Exit 1 is git's "not an ancestor". Every other failure propagates — a
|
|
2264
|
+
* tracking ref that does not exist, a broken repository — so a probe that
|
|
2265
|
+
* cannot answer fails the guard closed, never open.
|
|
2266
|
+
*/
|
|
2267
|
+
async headContainsOrigin(workspaceId: string): Promise<boolean> {
|
|
2268
|
+
return this.mutex.run(workspaceId, async () => {
|
|
2269
|
+
const cwd = await this.repoDir(workspaceId);
|
|
2270
|
+
const branch = await this.currentBranch(cwd);
|
|
2271
|
+
try {
|
|
2272
|
+
await this.git(cwd, ['merge-base', '--is-ancestor', `refs/remotes/origin/${branch}`, 'HEAD']);
|
|
2273
|
+
return true;
|
|
2274
|
+
} catch (err) {
|
|
2275
|
+
if (err instanceof GitRunError && !err.timedOut && err.exitCode === 1) return false;
|
|
2276
|
+
throw err;
|
|
2277
|
+
}
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2143
2281
|
/**
|
|
2144
2282
|
* `hasUnpushedCommits` without the workspace reservation — for a caller that
|
|
2145
2283
|
* already holds one (`mergeChangeRequest`, which has to ask inside the same
|
|
@@ -9,6 +9,24 @@ import {
|
|
|
9
9
|
type GitRunResult,
|
|
10
10
|
type IGitRunner,
|
|
11
11
|
} from '../../../shared/git.contract.js';
|
|
12
|
+
import { logger } from '../../../shared/logging.js';
|
|
13
|
+
import { credentialHelperValue } from '../../kb-fs/clone-config.js';
|
|
14
|
+
|
|
15
|
+
const log = logger('git-runner');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* What git says when it had no credential to offer, or offered one the host
|
|
19
|
+
* threw out. Both happen before a byte of the transfer moves, so a command
|
|
20
|
+
* that failed this way did nothing, and running it again is safe for every
|
|
21
|
+
* subcommand — fetch, push, ls-remote and clone alike.
|
|
22
|
+
*/
|
|
23
|
+
const REFUSED_CREDENTIAL_RE = /could not read (?:Username|Password) for|terminal prompts disabled|Authentication failed for/i;
|
|
24
|
+
|
|
25
|
+
/** Whether this failure is git being refused a credential (see {@link REFUSED_CREDENTIAL_RE}). */
|
|
26
|
+
export function isCredentialRefusal(err: unknown): boolean {
|
|
27
|
+
if (!(err instanceof GitRunError) || err.timedOut) return false;
|
|
28
|
+
return REFUSED_CREDENTIAL_RE.test(`${err.stderr ?? ''}\n${err.message}`);
|
|
29
|
+
}
|
|
12
30
|
|
|
13
31
|
/**
|
|
14
32
|
* The deadline every git call carries unless its caller says otherwise.
|
|
@@ -147,7 +165,8 @@ interface CommandFailure extends Error {
|
|
|
147
165
|
* missing git — a string `code`, which is how callers tell "git never ran"
|
|
148
166
|
* from "git said no"), and cuts off a child whose output passes `maxBuffer`.
|
|
149
167
|
*/
|
|
150
|
-
|
|
168
|
+
/** `shown` is what a failure message names — the caller's arguments, without the runner's own `-c` pairs. */
|
|
169
|
+
function spawnGit(args: string[], shown: string[], options: SpawnOptions & { maxBuffer: number }): {
|
|
151
170
|
promise: Promise<Captured>;
|
|
152
171
|
child: ChildProcess;
|
|
153
172
|
} {
|
|
@@ -195,7 +214,7 @@ function spawnGit(args: string[], options: SpawnOptions & { maxBuffer: number })
|
|
|
195
214
|
resolve({ stdout, stderr });
|
|
196
215
|
return;
|
|
197
216
|
}
|
|
198
|
-
const error: CommandFailure = new Error(`Command failed: git ${
|
|
217
|
+
const error: CommandFailure = new Error(`Command failed: git ${shown.join(' ')}\n${stderr.toString()}`);
|
|
199
218
|
error.code = code ?? undefined;
|
|
200
219
|
error.signal = signal;
|
|
201
220
|
error.stdout = stdout;
|
|
@@ -243,21 +262,77 @@ export class NodeGitRunner implements IGitRunner {
|
|
|
243
262
|
return { ...env, ...overrides, ...FIXED_ENV };
|
|
244
263
|
}
|
|
245
264
|
|
|
265
|
+
/**
|
|
266
|
+
* The credential helper, for THIS invocation, when a token is in hand: the
|
|
267
|
+
* same inline snippet the clones carry in their config, which reads the
|
|
268
|
+
* token from the child's environment. Given to every call so that a clone
|
|
269
|
+
* whose config lost its helper — unstamped while the deployment briefly
|
|
270
|
+
* had no token, created by a build that authenticated only its own clone,
|
|
271
|
+
* or touched by hand — still authenticates: the server's calls never
|
|
272
|
+
* depend on what a `.git/config` on disk happens to say. Nothing for a
|
|
273
|
+
* runner without a token, and nothing when the username cannot be made
|
|
274
|
+
* into a helper (the stamp refuses the same one, and says so).
|
|
275
|
+
*/
|
|
276
|
+
private helperArgs(): string[] {
|
|
277
|
+
if (!this.credentials.token()) return [];
|
|
278
|
+
try {
|
|
279
|
+
const helper = credentialHelperValue(this.credentials);
|
|
280
|
+
return helper ? ['-c', `credential.helper=${helper}`] : [];
|
|
281
|
+
} catch {
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
246
286
|
run(cwd: string, args: string[], opts: GitRunOptions & { encoding: 'buffer' }): Promise<GitRunResult<Buffer>>;
|
|
247
287
|
run(cwd: string, args: string[], opts?: GitRunOptions & { encoding?: 'utf8' }): Promise<GitRunResult>;
|
|
248
288
|
async run(cwd: string, args: string[], opts: GitRunOptions = {}): Promise<GitRunResult<string | Buffer>> {
|
|
249
|
-
const timeoutMs = opts.timeoutMs ?? this.defaultTimeoutMs;
|
|
250
|
-
const timers: NodeJS.Timeout[] = [];
|
|
251
|
-
let timedOut = false;
|
|
252
|
-
|
|
253
289
|
// A credential that expires is renewed before it is read. Renewing is
|
|
254
290
|
// the credential's own business and its failure is not this call's:
|
|
255
291
|
// plenty of git never leaves the disk, and a call that does is told by
|
|
256
292
|
// the host what the token in hand is worth.
|
|
257
293
|
await this.credentials.prepare?.().catch(() => undefined);
|
|
294
|
+
try {
|
|
295
|
+
return await this.attempt(cwd, args, opts);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
if (!isCredentialRefusal(err)) throw err;
|
|
298
|
+
// A credential the CALLER laid over the provider's (the setup probe
|
|
299
|
+
// checking a token it was given) is the caller's to judge: nothing
|
|
300
|
+
// here can replace it, so the refusal is the answer.
|
|
301
|
+
if (opts.env?.[GIT_TOKEN_ENV] !== undefined) throw err;
|
|
302
|
+
// Refused a credential: the token in hand was dead, or there was none
|
|
303
|
+
// to hand over. Tell the provider so — it drops what the host refused
|
|
304
|
+
// and asks for another, past whatever it remembers about a recent
|
|
305
|
+
// failure — and run the command once more ONLY with a credential that
|
|
306
|
+
// is not the one just refused. The same token again would only be
|
|
307
|
+
// refused again, and a renewal that failed leaves none.
|
|
308
|
+
const subcommand = subcommandOf(args);
|
|
309
|
+
const refused = this.credentials.token();
|
|
310
|
+
const renewal = await this.credentials.prepare?.({ refused: true }).then(
|
|
311
|
+
() => null,
|
|
312
|
+
(e: unknown) => (e instanceof Error ? e.message : String(e)),
|
|
313
|
+
);
|
|
314
|
+
const renewed = this.credentials.token();
|
|
315
|
+
if (!renewed || renewed === refused) {
|
|
316
|
+
log.warn(
|
|
317
|
+
`git ${subcommand} was refused a credential, and no other token could be had to run it again` +
|
|
318
|
+
(renewal ? `: ${redactGitToken(renewal, refused)}` : ''),
|
|
319
|
+
);
|
|
320
|
+
throw err;
|
|
321
|
+
}
|
|
322
|
+
log.warn(`git ${subcommand} was refused a credential; the token was renewed and the command runs again`);
|
|
323
|
+
return await this.attempt(cwd, args, opts);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private async attempt(cwd: string, args: string[], opts: GitRunOptions): Promise<GitRunResult<string | Buffer>> {
|
|
328
|
+
const timeoutMs = opts.timeoutMs ?? this.defaultTimeoutMs;
|
|
329
|
+
const timers: NodeJS.Timeout[] = [];
|
|
330
|
+
let timedOut = false;
|
|
258
331
|
|
|
259
332
|
try {
|
|
260
|
-
|
|
333
|
+
// The helper rides the invocation, not the error text: a failure
|
|
334
|
+
// message names the command the caller asked for.
|
|
335
|
+
const { promise: pending, child } = spawnGit([...this.helperArgs(), ...args], args, {
|
|
261
336
|
cwd,
|
|
262
337
|
env: this.childEnv(opts.env),
|
|
263
338
|
maxBuffer: MAX_OUTPUT_BYTES,
|