@bevel-software/platform-core-backend 0.33.0 → 0.35.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 +146 -289
- package/agent-guide/conventions.md +23 -25
- package/agent-guide/directory-structure.md +72 -120
- package/agent-guide/drafts-and-change-requests.md +92 -0
- package/agent-guide/for-git-users.md +47 -0
- package/agent-guide/glossary.md +36 -0
- package/agent-guide/how-agents-find-things.md +49 -0
- package/agent-guide/html-views.md +239 -8
- package/agent-guide/new-knowledge-base.md +9 -8
- package/agent-guide/skills.md +67 -42
- package/agent-guide/start-here.md +65 -0
- package/agent-guide/tool-manuals.md +133 -155
- package/agent-guide/tools.md +67 -0
- package/agent-guide/where-a-new-file-goes.md +45 -28
- package/dist/core/catalog-revision.d.ts +12 -1
- package/dist/core/catalog-revision.d.ts.map +1 -1
- package/dist/core/catalog-revision.js +23 -3
- package/dist/core/catalog-revision.js.map +1 -1
- package/dist/core/core-ports.d.ts +41 -0
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +15 -8
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +22 -5
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +44 -16
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +16 -0
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +12 -2
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +39 -9
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +15 -3
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +239 -75
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-mutation.service.d.ts +22 -24
- package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
- package/dist/modules/access/access-mutation.service.js +48 -51
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-view.d.ts.map +1 -1
- package/dist/modules/access/access-view.js +12 -11
- package/dist/modules/access/access-view.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts +3 -0
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +58 -33
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/groups-admin.routes.d.ts.map +1 -1
- package/dist/modules/access/groups-admin.routes.js +4 -2
- package/dist/modules/access/groups-admin.routes.js.map +1 -1
- package/dist/modules/access/groups-admin.service.d.ts +44 -13
- package/dist/modules/access/groups-admin.service.d.ts.map +1 -1
- package/dist/modules/access/groups-admin.service.js +80 -38
- package/dist/modules/access/groups-admin.service.js.map +1 -1
- package/dist/modules/access/groups-edit.d.ts +1 -1
- package/dist/modules/access/groups-edit.d.ts.map +1 -1
- package/dist/modules/access/groups-edit.js +1 -1
- package/dist/modules/access/groups-edit.js.map +1 -1
- package/dist/modules/access/index.d.ts +1 -1
- package/dist/modules/access/index.d.ts.map +1 -1
- package/dist/modules/access/reference-scan.d.ts +116 -24
- package/dist/modules/access/reference-scan.d.ts.map +1 -1
- package/dist/modules/access/reference-scan.js +305 -44
- package/dist/modules/access/reference-scan.js.map +1 -1
- package/dist/modules/access/roles-admin.service.d.ts +17 -8
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +32 -23
- package/dist/modules/access/roles-admin.service.js.map +1 -1
- package/dist/modules/access-model/access-grammar.d.ts +35 -10
- package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
- package/dist/modules/access-model/access-grammar.js +60 -20
- package/dist/modules/access-model/access-grammar.js.map +1 -1
- package/dist/modules/access-model/access-splice.d.ts +22 -18
- package/dist/modules/access-model/access-splice.d.ts.map +1 -1
- package/dist/modules/access-model/access-splice.js +20 -14
- package/dist/modules/access-model/access-splice.js.map +1 -1
- package/dist/modules/access-model/group-files.d.ts +1 -0
- package/dist/modules/access-model/group-files.d.ts.map +1 -1
- package/dist/modules/access-model/group-files.js +5 -1
- package/dist/modules/access-model/group-files.js.map +1 -1
- package/dist/modules/agent-guide/agent-guide.d.ts +47 -7
- package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -1
- package/dist/modules/agent-guide/agent-guide.js +103 -13
- package/dist/modules/agent-guide/agent-guide.js.map +1 -1
- package/dist/modules/agent-guide/agent-guide.tools.d.ts +6 -2
- package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -1
- package/dist/modules/agent-guide/agent-guide.tools.js +46 -22
- package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -1
- package/dist/modules/agent-guide/guide-budget.d.ts +48 -0
- package/dist/modules/agent-guide/guide-budget.d.ts.map +1 -0
- package/dist/modules/agent-guide/guide-budget.js +67 -0
- package/dist/modules/agent-guide/guide-budget.js.map +1 -0
- package/dist/modules/agent-guide/index.d.ts +2 -1
- package/dist/modules/agent-guide/index.d.ts.map +1 -1
- package/dist/modules/agent-guide/index.js +2 -1
- package/dist/modules/agent-guide/index.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts +3 -1
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +7 -1
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +5 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +26 -9
- package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
- package/dist/modules/agent-scope/agent-scope.contract.d.ts +67 -0
- package/dist/modules/agent-scope/agent-scope.contract.d.ts.map +1 -0
- package/dist/modules/agent-scope/agent-scope.contract.js +27 -0
- package/dist/modules/agent-scope/agent-scope.contract.js.map +1 -0
- package/dist/modules/agent-scope/connection-scope.d.ts +111 -0
- package/dist/modules/agent-scope/connection-scope.d.ts.map +1 -0
- package/dist/modules/agent-scope/connection-scope.js +165 -0
- package/dist/modules/agent-scope/connection-scope.js.map +1 -0
- package/dist/modules/agent-scope/index.d.ts +3 -0
- package/dist/modules/agent-scope/index.d.ts.map +1 -0
- package/dist/modules/agent-scope/index.js +3 -0
- package/dist/modules/agent-scope/index.js.map +1 -0
- package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
- package/dist/modules/audit/agent-audit.service.js +2 -0
- package/dist/modules/audit/agent-audit.service.js.map +1 -1
- package/dist/modules/audit/audit.contract.d.ts +6 -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/auth/account-admission.d.ts +12 -0
- package/dist/modules/auth/account-admission.d.ts.map +1 -1
- package/dist/modules/auth/account-admission.js +15 -0
- package/dist/modules/auth/account-admission.js.map +1 -1
- package/dist/modules/auth/auth.middleware.d.ts +27 -1
- package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
- package/dist/modules/auth/auth.middleware.js +42 -4
- package/dist/modules/auth/auth.middleware.js.map +1 -1
- package/dist/modules/auth/auth.routes.d.ts +13 -9
- package/dist/modules/auth/auth.routes.d.ts.map +1 -1
- package/dist/modules/auth/auth.routes.js +35 -29
- package/dist/modules/auth/auth.routes.js.map +1 -1
- package/dist/modules/auth/auth.service.d.ts +55 -6
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +128 -34
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/auth/index.d.ts +1 -1
- package/dist/modules/auth/index.d.ts.map +1 -1
- package/dist/modules/auth/index.js +1 -1
- package/dist/modules/auth/index.js.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.js +2 -9
- package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +2 -2
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/connection-probe/connection-probe.service.d.ts +0 -12
- package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
- package/dist/modules/connection-probe/connection-probe.service.js +1 -38
- package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +68 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +33 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/declared-variables/declared-variables.routes.d.ts +8 -1
- package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
- package/dist/modules/declared-variables/declared-variables.routes.js +13 -2
- package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
- package/dist/modules/embed/embed.tools.d.ts.map +1 -1
- package/dist/modules/embed/embed.tools.js +5 -0
- package/dist/modules/embed/embed.tools.js.map +1 -1
- package/dist/modules/kb-fs/index.d.ts +2 -0
- package/dist/modules/kb-fs/index.d.ts.map +1 -1
- package/dist/modules/kb-fs/index.js +1 -0
- package/dist/modules/kb-fs/index.js.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.js +7 -1
- package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
- package/dist/modules/kb-fs/repo-path.d.ts +26 -6
- package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
- package/dist/modules/kb-fs/repo-path.js +22 -7
- package/dist/modules/kb-fs/repo-path.js.map +1 -1
- package/dist/modules/kb-fs/tmp-ref.d.ts +36 -0
- package/dist/modules/kb-fs/tmp-ref.d.ts.map +1 -0
- package/dist/modules/kb-fs/tmp-ref.js +76 -0
- package/dist/modules/kb-fs/tmp-ref.js.map +1 -0
- package/dist/modules/mcp/mcp-auth.middleware.d.ts +7 -0
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +14 -1
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/mcp.routes.d.ts +7 -1
- package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.routes.js +39 -4
- package/dist/modules/mcp/mcp.routes.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +11 -0
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +74 -48
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +45 -10
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +9 -0
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js +34 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
- 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.map +1 -1
- package/dist/modules/plugins/plugin-links.d.ts +18 -2
- package/dist/modules/plugins/plugin-links.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-links.js.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts +5 -1
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +32 -5
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +11 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.js +159 -24
- package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
- package/dist/modules/secrets-vault/index.d.ts +1 -1
- package/dist/modules/secrets-vault/index.d.ts.map +1 -1
- package/dist/modules/secrets-vault/index.js +1 -1
- package/dist/modules/secrets-vault/index.js.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +53 -11
- package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.contract.js +27 -0
- package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +9 -0
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.js +100 -42
- package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
- package/dist/modules/settings/oidc-check.d.ts.map +1 -1
- package/dist/modules/settings/oidc-check.js +5 -17
- package/dist/modules/settings/oidc-check.js.map +1 -1
- package/dist/modules/skills/index.d.ts +3 -3
- package/dist/modules/skills/index.d.ts.map +1 -1
- package/dist/modules/skills/index.js +0 -2
- package/dist/modules/skills/index.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +16 -0
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +6 -1
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +25 -13
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts +11 -0
- package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -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 +1 -0
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +4 -1
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/index.d.ts +1 -1
- package/dist/modules/tool-auth/index.d.ts.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +9 -0
- package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.js +1 -0
- package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts +27 -3
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +32 -6
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/tool-context.d.ts +7 -0
- package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-context.js +3 -0
- package/dist/modules/tool-helpers/tool-context.js.map +1 -1
- package/dist/modules/tool-helpers/tool-def.js +1 -1
- package/dist/modules/tool-helpers/tool-def.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +15 -0
- package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.js +19 -0
- package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts +10 -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-helpers/validate-token.d.ts +3 -1
- package/dist/modules/tool-helpers/validate-token.d.ts.map +1 -1
- package/dist/modules/tool-helpers/validate-token.js +1 -1
- package/dist/modules/tool-helpers/validate-token.js.map +1 -1
- package/dist/modules/tool-manuals/index.d.ts +3 -3
- package/dist/modules/tool-manuals/index.d.ts.map +1 -1
- package/dist/modules/tool-manuals/index.js +0 -2
- package/dist/modules/tool-manuals/index.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +7 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.js +69 -38
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +46 -6
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +7 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.js +41 -4
- package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +24 -13
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +138 -59
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +8 -2
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +56 -20
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-registry/manual.routes.d.ts +8 -1
- package/dist/modules/tool-registry/manual.routes.d.ts.map +1 -1
- package/dist/modules/tool-registry/manual.routes.js +27 -7
- package/dist/modules/tool-registry/manual.routes.js.map +1 -1
- package/dist/modules/tool-registry/tool.contract.d.ts +7 -0
- package/dist/modules/tool-registry/tool.contract.d.ts.map +1 -1
- package/dist/modules/tool-registry/tool.contract.js.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +36 -27
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +2 -2
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +20 -35
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +2 -1
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +3 -0
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-access.gate.d.ts +1 -1
- package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -1
- package/dist/modules/workspace/agent-access.gate.js +1 -1
- package/dist/modules/workspace/agent-access.gate.js.map +1 -1
- package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +28 -4
- package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/doc-extract.service.js +61 -40
- package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
- package/dist/modules/workspace/index.d.ts +8 -0
- package/dist/modules/workspace/index.d.ts.map +1 -1
- package/dist/modules/workspace/index.js +8 -0
- package/dist/modules/workspace/index.js.map +1 -1
- package/dist/modules/workspace/nul-scan.d.ts +19 -0
- package/dist/modules/workspace/nul-scan.d.ts.map +1 -0
- package/dist/modules/workspace/nul-scan.js +55 -0
- package/dist/modules/workspace/nul-scan.js.map +1 -0
- package/dist/modules/workspace/pattern-matcher.contract.d.ts +110 -27
- package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -1
- package/dist/modules/workspace/pattern-matcher.contract.js +30 -13
- package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -1
- package/dist/modules/workspace/ripgrep-search.d.ts +54 -0
- package/dist/modules/workspace/ripgrep-search.d.ts.map +1 -0
- package/dist/modules/workspace/ripgrep-search.js +657 -0
- package/dist/modules/workspace/ripgrep-search.js.map +1 -0
- package/dist/modules/workspace/ripgrep-version.d.ts +15 -0
- package/dist/modules/workspace/ripgrep-version.d.ts.map +1 -0
- package/dist/modules/workspace/ripgrep-version.js +31 -0
- package/dist/modules/workspace/ripgrep-version.js.map +1 -0
- package/dist/modules/workspace/startup/beside-checkout.d.ts +8 -5
- package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -1
- package/dist/modules/workspace/startup/beside-checkout.js +14 -5
- package/dist/modules/workspace/startup/beside-checkout.js.map +1 -1
- package/dist/modules/workspace/worker-pool.d.ts +3 -4
- package/dist/modules/workspace/worker-pool.d.ts.map +1 -1
- package/dist/modules/workspace/worker-pool.js.map +1 -1
- package/dist/modules/workspace/workspace-tmp.d.ts +146 -0
- package/dist/modules/workspace/workspace-tmp.d.ts.map +1 -0
- package/dist/modules/workspace/workspace-tmp.js +435 -0
- package/dist/modules/workspace/workspace-tmp.js.map +1 -0
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +5 -26
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +47 -6
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +86 -6
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +3 -2
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +531 -137
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/shared/oauth-token-error.d.ts +23 -0
- package/dist/shared/oauth-token-error.d.ts.map +1 -0
- package/dist/shared/oauth-token-error.js +47 -0
- package/dist/shared/oauth-token-error.js.map +1 -0
- package/dist/shared/path-order.d.ts +28 -8
- package/dist/shared/path-order.d.ts.map +1 -1
- package/dist/shared/path-order.js +37 -8
- package/dist/shared/path-order.js.map +1 -1
- package/dist/shared/read-capped.d.ts +19 -0
- package/dist/shared/read-capped.d.ts.map +1 -0
- package/dist/shared/read-capped.js +45 -0
- package/dist/shared/read-capped.js.map +1 -0
- package/migrations/0022_session_version.sql +6 -0
- package/migrations/0023_agent_path.sql +3 -0
- package/migrations/meta/0022_snapshot.json +2360 -0
- package/migrations/meta/0023_snapshot.json +2378 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +4 -3
- package/src/__tests__/hexis-catalog.ts +144 -0
- package/src/__tests__/pg-scratch.test.ts +69 -0
- package/src/__tests__/pg-scratch.ts +31 -0
- package/src/__tests__/real-disk.ts +21 -0
- package/src/__tests__/slow-ripgrep.ts +65 -0
- package/src/__tests__/tokens.ts +19 -0
- package/src/__tests__/tool-manuals.ts +32 -0
- package/src/__tests__/tool-wiring.ts +25 -0
- package/src/core/__tests__/boot-without-ripgrep.test.ts +34 -0
- package/src/core/__tests__/catalog-revision.test.ts +89 -0
- package/src/core/__tests__/damaged-document.tools.test.ts +2 -2
- package/src/core/__tests__/gated-boot.test.ts +8 -1
- package/src/core/__tests__/lifecycle.test.ts +46 -13
- package/src/core/__tests__/oauth-sign-in-real-file.test.ts +179 -0
- package/src/core/catalog-revision.ts +24 -2
- package/src/core/core-ports.ts +42 -0
- package/src/core/create-core-server.ts +14 -1
- package/src/core/create-core-services.ts +62 -15
- package/src/core/lifecycle.ts +26 -2
- package/src/index.ts +1 -1
- package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +12 -0
- package/src/modules/access/__tests__/access-control.colliding-name-cases.test.ts +113 -0
- package/src/modules/access/__tests__/access-control.frontmatter-head.test.ts +117 -0
- package/src/modules/access/__tests__/access-control.group-named-admin.test.ts +122 -0
- package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +50 -0
- package/src/modules/access/__tests__/access-declarations.test.ts +19 -0
- package/src/modules/access/__tests__/access-groups.test.ts +55 -22
- package/src/modules/access/__tests__/access-mutation.service.test.ts +85 -59
- package/src/modules/access/__tests__/access-restrict-here.test.ts +7 -5
- package/src/modules/access/__tests__/access.routes.admins.test.ts +1 -0
- package/src/modules/access/__tests__/access.routes.batch.test.ts +2 -0
- package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +2 -0
- package/src/modules/access/__tests__/access.routes.group-grant.test.ts +50 -14
- package/src/modules/access/__tests__/access.routes.overrides.test.ts +2 -1
- package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +1 -0
- package/src/modules/access/__tests__/access.routes.prospective.test.ts +52 -10
- package/src/modules/access/__tests__/access.routes.revoke.test.ts +18 -13
- package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
- package/src/modules/access/__tests__/grant-sources.test.ts +20 -15
- package/src/modules/access/__tests__/groups-admin.routes.test.ts +61 -0
- package/src/modules/access/__tests__/groups-admin.service.test.ts +211 -18
- package/src/modules/access/__tests__/holder-changes.test.ts +127 -0
- package/src/modules/access/__tests__/reference-scan.test.ts +366 -14
- package/src/modules/access/__tests__/roles-admin.security.test.ts +1 -1
- package/src/modules/access/__tests__/roles-admin.service.test.ts +43 -22
- package/src/modules/access/__tests__/roles-capabilities.test.ts +6 -2
- package/src/modules/access/__tests__/roles.routes.test.ts +1 -1
- package/src/modules/access/access-control.interface.ts +43 -10
- package/src/modules/access/access-control.service.ts +250 -72
- package/src/modules/access/access-mutation.service.ts +48 -53
- package/src/modules/access/access-view.ts +12 -10
- package/src/modules/access/access.routes.ts +66 -37
- package/src/modules/access/groups-admin.routes.ts +4 -2
- package/src/modules/access/groups-admin.service.ts +114 -44
- package/src/modules/access/groups-edit.ts +1 -1
- package/src/modules/access/index.ts +1 -1
- package/src/modules/access/reference-scan.ts +319 -46
- package/src/modules/access/roles-admin.service.ts +43 -26
- package/src/modules/access/synced-groups-writer.ts +1 -1
- package/src/modules/access-model/__tests__/access-grammar.test.ts +38 -1
- package/src/modules/access-model/__tests__/access-splice.test.ts +50 -8
- package/src/modules/access-model/access-grammar.ts +65 -20
- package/src/modules/access-model/access-splice.ts +31 -23
- package/src/modules/access-model/group-files.ts +5 -0
- package/src/modules/agent-guide/__tests__/agent-guide.test.ts +110 -25
- package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +38 -11
- package/src/modules/agent-guide/__tests__/guide-budget.test.ts +95 -0
- package/src/modules/agent-guide/__tests__/guide-eval.test.ts +182 -0
- package/src/modules/agent-guide/__tests__/guide-matches-code.test.ts +263 -0
- package/src/modules/agent-guide/__tests__/tool-surface.json +514 -0
- package/src/modules/agent-guide/__tests__/tool-surface.test.ts +75 -0
- package/src/modules/agent-guide/agent-guide.tools.ts +51 -22
- package/src/modules/agent-guide/agent-guide.ts +120 -13
- package/src/modules/agent-guide/guide-budget.ts +73 -0
- package/src/modules/agent-guide/index.ts +12 -0
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +5 -4
- package/src/modules/agent-instructions/compose.ts +7 -1
- package/src/modules/agent-instructions/shared-file-rules.ts +27 -9
- package/src/modules/agent-scope/__tests__/agent-surfaces.connect-as.test.ts +421 -0
- package/src/modules/agent-scope/__tests__/connection-scope.test.ts +46 -0
- package/src/modules/agent-scope/__tests__/in-memory-kb.ts +148 -0
- package/src/modules/agent-scope/agent-scope.contract.ts +83 -0
- package/src/modules/agent-scope/connection-scope.ts +199 -0
- package/src/modules/agent-scope/index.ts +15 -0
- package/src/modules/audit/agent-audit.service.ts +2 -0
- package/src/modules/audit/audit.contract.ts +6 -0
- package/src/modules/auth/__tests__/auth.middleware.test.ts +30 -2
- package/src/modules/auth/__tests__/auth.routes.test.ts +71 -5
- package/src/modules/auth/__tests__/auth.service.test.ts +90 -9
- package/src/modules/auth/__tests__/password-change-sessions.pg.test.ts +269 -0
- package/src/modules/auth/account-admission.ts +19 -0
- package/src/modules/auth/auth.middleware.ts +45 -4
- package/src/modules/auth/auth.routes.ts +46 -29
- package/src/modules/auth/auth.service.ts +135 -34
- package/src/modules/auth/index.ts +2 -0
- package/src/modules/auth/oidc-auth-provider.ts +2 -9
- package/src/modules/code-mode/code-mode.tool.ts +2 -2
- package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +5 -24
- package/src/modules/connection-probe/connection-probe.service.ts +1 -34
- package/src/modules/database/__tests__/session-version-migration.pg.test.ts +133 -0
- package/src/modules/database/core-schema.ts +33 -0
- package/src/modules/declared-variables/__tests__/declared-variables.route.test.ts +44 -1
- package/src/modules/declared-variables/declared-variables.routes.ts +12 -1
- package/src/modules/embed/__tests__/embed.tools.test.ts +12 -0
- package/src/modules/embed/embed.tools.ts +5 -0
- package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +25 -0
- package/src/modules/kb-fs/__tests__/repo-path.test.ts +53 -8
- package/src/modules/kb-fs/index.ts +2 -0
- package/src/modules/kb-fs/locking-filesystem.ts +7 -1
- package/src/modules/kb-fs/repo-path.ts +33 -7
- package/src/modules/kb-fs/tmp-ref.ts +81 -0
- package/src/modules/mcp/__tests__/connect-as.pg.test.ts +137 -0
- package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +29 -5
- package/src/modules/mcp/__tests__/mcp-routes-harness.ts +8 -0
- package/src/modules/mcp/__tests__/mcp.connect-as.test.ts +195 -0
- package/src/modules/mcp/__tests__/mcp.routes.connect-as.test.ts +192 -0
- package/src/modules/mcp/__tests__/oauth-consent.connect-as.test.ts +146 -0
- package/src/modules/mcp/mcp-auth.middleware.ts +19 -2
- package/src/modules/mcp/mcp.routes.ts +39 -3
- package/src/modules/mcp/mcp.service.ts +112 -52
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +46 -8
- package/src/modules/mcp/oauth/oauth-consent.routes.ts +41 -1
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +38 -2
- package/src/modules/plugins/index.ts +1 -1
- package/src/modules/plugins/plugin-links.ts +19 -2
- package/src/modules/plugins/plugins.routes.ts +35 -4
- package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +113 -0
- package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +430 -3
- package/src/modules/secrets-vault/__tests__/oauth-live-declaration.route.test.ts +218 -0
- package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +39 -6
- package/src/modules/secrets-vault/db-secrets-vault.service.ts +170 -27
- package/src/modules/secrets-vault/index.ts +1 -1
- package/src/modules/secrets-vault/secrets-vault.contract.ts +61 -11
- package/src/modules/secrets-vault/secrets-vault.routes.ts +114 -42
- package/src/modules/settings/oidc-check.ts +5 -18
- package/src/modules/skills/__tests__/skills.service.test.ts +34 -0
- package/src/modules/skills/index.ts +7 -2
- package/src/modules/skills/skills.contract.ts +17 -0
- package/src/modules/skills/skills.service.ts +5 -1
- package/src/modules/skills/skills.tools.ts +45 -14
- package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +4 -0
- package/src/modules/tool-auth/external-api-key.interface.ts +11 -1
- package/src/modules/tool-auth/external-api-key.service.ts +5 -2
- package/src/modules/tool-auth/index.ts +1 -1
- package/src/modules/tool-auth/internal-token.service.ts +10 -0
- package/src/modules/tool-auth/tool-auth.middleware.ts +45 -3
- package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +19 -11
- package/src/modules/tool-helpers/__tests__/can-write.tools.test.ts +3 -1
- package/src/modules/tool-helpers/__tests__/route-tool-schemas.catalog.test.ts +38 -0
- package/src/modules/tool-helpers/tool-context.ts +10 -0
- package/src/modules/tool-helpers/tool-def.ts +1 -1
- package/src/modules/tool-helpers/tool-handler.ts +34 -0
- package/src/modules/tool-helpers/tool.contract.ts +10 -0
- package/src/modules/tool-helpers/validate-token.ts +4 -2
- package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +80 -4
- package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +197 -11
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +42 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +99 -8
- package/src/modules/tool-manuals/index.ts +6 -2
- package/src/modules/tool-manuals/mcp-json-discovery.ts +73 -34
- package/src/modules/tool-manuals/tool-manuals.contract.ts +40 -6
- package/src/modules/tool-manuals/tool-manuals.routes.ts +40 -3
- package/src/modules/tool-manuals/tool-manuals.service.ts +169 -61
- package/src/modules/tool-manuals/tool-manuals.tools.ts +81 -25
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +6 -113
- package/src/modules/tool-registry/manual.routes.ts +26 -5
- package/src/modules/tool-registry/tool.contract.ts +7 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
- package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +1 -1
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +54 -3
- package/src/modules/workflow/agent-tools/workflow.tools.ts +36 -27
- package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +130 -3
- package/src/modules/workflow/git/git.service.ts +29 -48
- package/src/modules/workflow/workflow.service.ts +5 -0
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -2
- package/src/modules/workspace/__tests__/holds-nul.test.ts +66 -0
- package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +3 -3
- package/src/modules/workspace/__tests__/ripgrep-search.test.ts +570 -0
- package/src/modules/workspace/__tests__/ripgrep-version.test.ts +29 -0
- package/src/modules/workspace/__tests__/workspace-tmp.tools.test.ts +928 -0
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +17 -37
- package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +20 -8
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +10 -12
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +22 -10
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +549 -162
- package/src/modules/workspace/agent-access.gate.ts +1 -1
- package/src/modules/workspace/file-readers/__tests__/doc-extract.service.test.ts +10 -0
- package/src/modules/workspace/file-readers/doc-extract.service.ts +71 -38
- package/src/modules/workspace/index.ts +20 -0
- package/src/modules/workspace/nul-scan.ts +50 -0
- package/src/modules/workspace/pattern-matcher.contract.ts +123 -32
- package/src/modules/workspace/ripgrep-search.ts +715 -0
- package/src/modules/workspace/ripgrep-version.ts +35 -0
- package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +11 -0
- package/src/modules/workspace/startup/beside-checkout.ts +13 -5
- package/src/modules/workspace/worker-pool.ts +3 -4
- package/src/modules/workspace/workspace-tmp.ts +457 -0
- package/src/modules/workspace/workspace.routes.ts +6 -27
- package/src/modules/workspace/workspace.service.ts +92 -15
- package/src/modules/workspace/workspace.tools.ts +574 -176
- package/src/shared/__tests__/read-capped.test.ts +68 -0
- package/src/shared/oauth-token-error.ts +46 -0
- package/src/shared/path-order.ts +37 -8
- package/src/shared/read-capped.ts +40 -0
- package/starter-packs/engineering/Plugins/engineering-starter/access.md +1 -1
- package/starter-packs/general/Plugins/general-starter/access.md +1 -1
- package/starter-packs/operations/Plugins/operations-starter/access.md +1 -1
- package/starter-packs/sales/Plugins/sales-starter/access.md +1 -1
- package/starter-packs/support/Plugins/support-starter/access.md +1 -1
- package/agent-guide/finding-things.md +0 -7
- package/agent-guide/introduction.md +0 -27
- package/dist/modules/workspace/bounded-matcher.d.ts +0 -73
- package/dist/modules/workspace/bounded-matcher.d.ts.map +0 -1
- package/dist/modules/workspace/bounded-matcher.js +0 -200
- package/dist/modules/workspace/bounded-matcher.js.map +0 -1
- package/src/modules/workspace/__tests__/bounded-matcher.test.ts +0 -213
- package/src/modules/workspace/bounded-matcher.ts +0 -271
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
2
|
+
import { realpath } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { MAX_TIMER_MS, sizingOption } from './worker-pool.js';
|
|
5
|
+
import { ToolError } from '../tool-helpers/index.js';
|
|
6
|
+
import { logger } from '../../shared/logging.js';
|
|
7
|
+
import {
|
|
8
|
+
compareSearchPaths,
|
|
9
|
+
type FileMatches,
|
|
10
|
+
type IPatternMatcher,
|
|
11
|
+
type IPatternSearch,
|
|
12
|
+
type TreeScope,
|
|
13
|
+
type TreeSearchOptions,
|
|
14
|
+
PatternRefusedError,
|
|
15
|
+
PatternTooExpensiveError,
|
|
16
|
+
SearchAbandonedError,
|
|
17
|
+
SearchBusyError,
|
|
18
|
+
} from './pattern-matcher.contract.js';
|
|
19
|
+
|
|
20
|
+
const log = logger('grep');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The one place a CALLER'S regular expression is run (see
|
|
24
|
+
* {@link IPatternMatcher}): ripgrep, as a child process of the server.
|
|
25
|
+
*
|
|
26
|
+
* Every run is `rg` started WITHOUT a shell, with the caller's pattern after
|
|
27
|
+
* `-e` and the searched path after `--`, so nothing the caller sends can
|
|
28
|
+
* become an option; `--no-config` keeps `RIPGREP_CONFIG_PATH` out of it. A
|
|
29
|
+
* tree is walked sorted by path (`--sort path`), so an answer is stable and a
|
|
30
|
+
* caller that has enough stops the walk.
|
|
31
|
+
*
|
|
32
|
+
* Each run is owned by the call that started it, which kills it on every way
|
|
33
|
+
* out: the walk finished, the caller stopped reading, the budget ran out, the
|
|
34
|
+
* search's caller went away (its abort signal), or an error. `close()` kills
|
|
35
|
+
* whatever is still running.
|
|
36
|
+
*/
|
|
37
|
+
export interface RipgrepPatternMatcherOptions {
|
|
38
|
+
/** Wall time one search's runs may spend, summed over every run it makes. Default 5 s. */
|
|
39
|
+
budgetMs?: number;
|
|
40
|
+
/** ripgrep runs at once; a run past this waits for one to end. Default 4. */
|
|
41
|
+
maxRunning?: number;
|
|
42
|
+
/** Runs that may wait at once; past it a search is refused with a 503. Default 16. */
|
|
43
|
+
maxWaiting?: number;
|
|
44
|
+
/** Longest pattern accepted. Default 1000. */
|
|
45
|
+
maxPatternLength?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The flags every run starts with: ripgrep's own configuration file is never read, and bytes are searched as they are. */
|
|
49
|
+
const BASE_ARGS = ['--no-config', '--encoding', 'none'];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What a tree walk covers: hidden files, ignored files (no `.gitignore`,
|
|
53
|
+
* `.ignore` or global excludes apply), never a symbolic link, never a `.git`
|
|
54
|
+
* folder in any case, files visited sorted by path.
|
|
55
|
+
*/
|
|
56
|
+
const WALK_ARGS = ['--hidden', '--no-ignore', '--no-follow', '--no-messages', '--sort', 'path'];
|
|
57
|
+
|
|
58
|
+
/** Last among a walk's globs, so no extension a caller lets through can bring a `.git` folder back. */
|
|
59
|
+
const NO_GIT = ['--iglob', '!.git'];
|
|
60
|
+
|
|
61
|
+
/** How much of a refused run's stderr is kept. */
|
|
62
|
+
const STDERR_CAP = 4_096;
|
|
63
|
+
|
|
64
|
+
type Released = () => void;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* How long a run may wait for its slot, and for whom: the search's own
|
|
68
|
+
* limits, so a caller gone or a budget spent while queued leaves the queue
|
|
69
|
+
* with that answer rather than taking a slot later for a run it no longer
|
|
70
|
+
* wants or may not afford.
|
|
71
|
+
*/
|
|
72
|
+
type SlotWait = {
|
|
73
|
+
signal?: AbortSignal;
|
|
74
|
+
/** What is left of the search's budget; the wait ends with `expired()` when it runs out. */
|
|
75
|
+
timeoutMs: number;
|
|
76
|
+
expired(): Error;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** What a search borrows from its matcher: a slot to run in, and the one way to start `rg`. */
|
|
80
|
+
type Runner = {
|
|
81
|
+
slot(wait: SlotWait): Promise<Released>;
|
|
82
|
+
start(args: string[], cwd: string | undefined, withInput: boolean): ChildProcess;
|
|
83
|
+
/** Whether `close()` has run: a run it killed is answered busy. */
|
|
84
|
+
isClosed(): boolean;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export class RipgrepPatternMatcher implements IPatternMatcher {
|
|
88
|
+
private readonly budgetMs: number;
|
|
89
|
+
private readonly maxRunning: number;
|
|
90
|
+
private readonly maxWaiting: number;
|
|
91
|
+
private readonly maxPatternLength: number;
|
|
92
|
+
private readonly runner: Runner;
|
|
93
|
+
private readonly live = new Set<ChildProcess>();
|
|
94
|
+
private readonly waiting: { resolve: () => void; reject: (err: Error) => void }[] = [];
|
|
95
|
+
private running = 0;
|
|
96
|
+
private closed = false;
|
|
97
|
+
|
|
98
|
+
constructor(opts: RipgrepPatternMatcherOptions = {}) {
|
|
99
|
+
this.budgetMs = option('budgetMs', opts.budgetMs ?? 5_000, 1, MAX_TIMER_MS);
|
|
100
|
+
// None running would leave every search waiting for a slot that never comes.
|
|
101
|
+
this.maxRunning = option('maxRunning', opts.maxRunning ?? 4, 1);
|
|
102
|
+
this.maxWaiting = option('maxWaiting', opts.maxWaiting ?? 16, 0);
|
|
103
|
+
this.maxPatternLength = option('maxPatternLength', opts.maxPatternLength ?? 1_000, 1);
|
|
104
|
+
this.runner = {
|
|
105
|
+
slot: (wait) => this.slot(wait),
|
|
106
|
+
start: (args, cwd, withInput) => this.start(args, cwd, withInput),
|
|
107
|
+
isClosed: () => this.closed,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async search(pattern: string, opts: { ignoreCase: boolean; signal?: AbortSignal }): Promise<IPatternSearch> {
|
|
112
|
+
if (pattern.length > this.maxPatternLength) {
|
|
113
|
+
throw new ToolError(
|
|
114
|
+
`Pattern too long: ${pattern.length} characters, and a search pattern may have at most ${this.maxPatternLength}.`,
|
|
115
|
+
400,
|
|
116
|
+
{ code: 'pattern_too_long' },
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
// A process argument cannot carry a NUL; the escape `\x00` can.
|
|
120
|
+
if (pattern.includes('\0')) throw new PatternRefusedError('a pattern cannot contain a NUL character; write it as \\x00.');
|
|
121
|
+
const search = new RipgrepSearch(this.runner, pattern, opts.ignoreCase ? ['-i'] : [], this.budgetMs, opts.signal);
|
|
122
|
+
// The engine judges the pattern before anything is searched, so a refused
|
|
123
|
+
// one is answered as such whatever the search would have found.
|
|
124
|
+
await search.matchingLines(['']);
|
|
125
|
+
return search;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async close(): Promise<void> {
|
|
129
|
+
this.closed = true;
|
|
130
|
+
for (const w of this.waiting.splice(0)) w.reject(new SearchBusyError());
|
|
131
|
+
const exits = [...this.live].map((child) => exited(child));
|
|
132
|
+
for (const child of this.live) child.kill('SIGKILL');
|
|
133
|
+
await Promise.all(exits);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A run's slot: given at once while fewer than `maxRunning` run, else after
|
|
138
|
+
* a wait bounded by the search's own limits (see {@link SlotWait}). A waiter
|
|
139
|
+
* that leaves — its caller gone, its budget spent, the matcher closed —
|
|
140
|
+
* leaves the queue too, so the slot a run gives back passes to a waiter
|
|
141
|
+
* that still wants it.
|
|
142
|
+
*/
|
|
143
|
+
private async slot(wait: SlotWait): Promise<Released> {
|
|
144
|
+
if (this.closed) throw new SearchBusyError();
|
|
145
|
+
if (this.running < this.maxRunning) {
|
|
146
|
+
this.running++;
|
|
147
|
+
} else {
|
|
148
|
+
if (this.waiting.length >= this.maxWaiting) throw new SearchBusyError();
|
|
149
|
+
// Resolved by a run that ends: its slot passes straight to this one.
|
|
150
|
+
await new Promise<void>((resolve, reject) => {
|
|
151
|
+
// Every way out settles the same: the budget timer cleared, the
|
|
152
|
+
// abort listener removed. The closures below refer to one another
|
|
153
|
+
// and run only once all of them exist.
|
|
154
|
+
const settle = (): void => {
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
wait.signal?.removeEventListener('abort', onAbort);
|
|
157
|
+
};
|
|
158
|
+
const waiter = {
|
|
159
|
+
resolve: () => {
|
|
160
|
+
settle();
|
|
161
|
+
resolve();
|
|
162
|
+
},
|
|
163
|
+
reject: (err: Error) => {
|
|
164
|
+
settle();
|
|
165
|
+
reject(err);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
const leave = (err: Error): void => {
|
|
169
|
+
const at = this.waiting.indexOf(waiter);
|
|
170
|
+
if (at >= 0) this.waiting.splice(at, 1);
|
|
171
|
+
waiter.reject(err);
|
|
172
|
+
};
|
|
173
|
+
const onAbort = (): void => leave(new SearchAbandonedError());
|
|
174
|
+
const timer = setTimeout(() => leave(wait.expired()), wait.timeoutMs);
|
|
175
|
+
this.waiting.push(waiter);
|
|
176
|
+
if (wait.signal?.aborted) {
|
|
177
|
+
leave(new SearchAbandonedError());
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
wait.signal?.addEventListener('abort', onAbort, { once: true });
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
let released = false;
|
|
184
|
+
return () => {
|
|
185
|
+
if (released) return;
|
|
186
|
+
released = true;
|
|
187
|
+
const next = this.waiting.shift();
|
|
188
|
+
if (next) next.resolve();
|
|
189
|
+
else this.running--;
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Start `rg` from the server's PATH with `args`, registered so `close()` can end it. */
|
|
194
|
+
private start(args: string[], cwd: string | undefined, withInput: boolean): ChildProcess {
|
|
195
|
+
if (this.closed) throw new SearchBusyError();
|
|
196
|
+
const child = spawn('rg', args, {
|
|
197
|
+
cwd,
|
|
198
|
+
shell: false,
|
|
199
|
+
stdio: [withInput ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
200
|
+
windowsHide: true,
|
|
201
|
+
});
|
|
202
|
+
this.live.add(child);
|
|
203
|
+
child.once('close', () => this.live.delete(child));
|
|
204
|
+
// A spawn that fails emits 'error' and never 'close'.
|
|
205
|
+
child.once('error', () => this.live.delete(child));
|
|
206
|
+
return child;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** One search: the pattern, its flags, and what is left of its budget. */
|
|
211
|
+
class RipgrepSearch implements IPatternSearch {
|
|
212
|
+
private spentMs = 0;
|
|
213
|
+
|
|
214
|
+
constructor(
|
|
215
|
+
private readonly runner: Runner,
|
|
216
|
+
private readonly pattern: string,
|
|
217
|
+
private readonly flags: string[],
|
|
218
|
+
private readonly budgetMs: number,
|
|
219
|
+
private readonly signal: AbortSignal | undefined,
|
|
220
|
+
) {}
|
|
221
|
+
|
|
222
|
+
async *files(scope: TreeScope, opts: TreeSearchOptions): AsyncIterable<FileMatches> {
|
|
223
|
+
// Without a single whitelist glob ripgrep would search every file.
|
|
224
|
+
if (opts.only.length === 0) return;
|
|
225
|
+
// Each file is named by a whitelist glob anchored at `cwd`, where ripgrep
|
|
226
|
+
// runs: the walk stays the walk (no link followed, `.git` left out), and
|
|
227
|
+
// a file not named is never opened. Whitelist globs filter files only, so
|
|
228
|
+
// the walks start at the folders holding the named files rather than at
|
|
229
|
+
// the root, and a folder's own files are walked without its subfolders:
|
|
230
|
+
// a batch of a large tree walks about its own stretch of it.
|
|
231
|
+
const under = scope.root === '.' ? '' : `${scope.root}/`;
|
|
232
|
+
// Every name is checked before the first run, so none is half-searched.
|
|
233
|
+
for (const p of opts.only) exactGlob(`${under}${p}`);
|
|
234
|
+
let only = opts.only;
|
|
235
|
+
const max = opts.maxLinesPerFile;
|
|
236
|
+
while (only.length > 0) {
|
|
237
|
+
let capped = yield* this.walkFiles(scope, under, only, max, max);
|
|
238
|
+
if (capped === null) return;
|
|
239
|
+
const path = capped.path;
|
|
240
|
+
// A matching line that is not UTF-8 counts toward ripgrep's cap but is
|
|
241
|
+
// not answered, so a file can stop short of its lines: it is searched
|
|
242
|
+
// again on its own, with a cap raised at least twofold, until it has
|
|
243
|
+
// them or ripgrep reads it to its end (and answers it from the walk).
|
|
244
|
+
let maxCount = max;
|
|
245
|
+
while (capped !== null && capped.lines.length < max) {
|
|
246
|
+
maxCount += Math.max(max - capped.lines.length, maxCount);
|
|
247
|
+
capped = yield* this.walkFiles(scope, under, [path], max, maxCount);
|
|
248
|
+
}
|
|
249
|
+
// ripgrep stopped reading the file at its cap, so a NUL past it went
|
|
250
|
+
// unseen: the file is read on for one, by a run of its own once the
|
|
251
|
+
// walk's has ended, and the walk goes on with the files after it.
|
|
252
|
+
if (capped !== null && !(await this.holdsNul(scope, under, path))) yield capped;
|
|
253
|
+
only = only.filter((p) => compareSearchPaths(p, path) > 0);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The walks of `only`, each file answered at its `end` with at most
|
|
259
|
+
* `maxLinesPerFile` lines, until ripgrep stops one at `maxCount` matching
|
|
260
|
+
* lines (UTF-8 or not): the walk ends there, and that file is returned
|
|
261
|
+
* unanswered. `null` once every walk has finished.
|
|
262
|
+
*/
|
|
263
|
+
private async *walkFiles(
|
|
264
|
+
scope: TreeScope,
|
|
265
|
+
under: string,
|
|
266
|
+
only: readonly string[],
|
|
267
|
+
maxLinesPerFile: number,
|
|
268
|
+
maxCount: number,
|
|
269
|
+
): AsyncGenerator<FileMatches, FileMatches | null> {
|
|
270
|
+
// `--max-count`: a file is read no further than the lines it may answer.
|
|
271
|
+
const cap = ['--max-count', String(maxCount)];
|
|
272
|
+
for (const walk of await realWalks(scope, walkPlan(only))) {
|
|
273
|
+
const globs = walk.paths.flatMap((p) => ['--glob', exactGlob(`${under}${p}`)]);
|
|
274
|
+
const depth = walk.ownFilesOnly ? ['--max-depth', '1'] : [];
|
|
275
|
+
const args = [...BASE_ARGS, '--json', ...WALK_ARGS, ...depth, ...cap, ...globs, ...NO_GIT, ...this.flags, '-e', this.pattern, '--', ...walk.folders];
|
|
276
|
+
let current: FileMatches | null = null;
|
|
277
|
+
let matched = 0;
|
|
278
|
+
for await (const record of this.run(args, scope.cwd, undefined, '\n')) {
|
|
279
|
+
const event = parseEvent(record);
|
|
280
|
+
if (event === null) continue;
|
|
281
|
+
if (event.type === 'begin') {
|
|
282
|
+
const path = underRoot(scope.root, pathOf(event.data?.path));
|
|
283
|
+
current = path === null ? null : { path, lines: [] };
|
|
284
|
+
matched = 0;
|
|
285
|
+
} else if (event.type === 'match' && current !== null) {
|
|
286
|
+
matched++;
|
|
287
|
+
const text = textOf(event.data?.lines);
|
|
288
|
+
const line = event.data?.line_number;
|
|
289
|
+
if (text !== null && typeof line === 'number' && current.lines.length < maxLinesPerFile) {
|
|
290
|
+
current.lines.push({ line, text: withoutNewline(text) });
|
|
291
|
+
}
|
|
292
|
+
} else if (event.type === 'end' && current !== null) {
|
|
293
|
+
// A file ripgrep found binary part-way through is not text: whatever
|
|
294
|
+
// it matched before the NUL is dropped with the rest. That is known
|
|
295
|
+
// only here, so a file is answered at its end and not before.
|
|
296
|
+
const binary = event.data?.binary_offset !== null && event.data?.binary_offset !== undefined;
|
|
297
|
+
if (!binary && matched >= maxCount) return current;
|
|
298
|
+
if (!binary && current.lines.length > 0) yield current;
|
|
299
|
+
current = null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Whether `path` (relative to the root) holds a NUL anywhere, read by a walk
|
|
308
|
+
* of its own, which follows no link and stops at the first NUL.
|
|
309
|
+
*/
|
|
310
|
+
private async holdsNul(scope: TreeScope, under: string, path: string): Promise<boolean> {
|
|
311
|
+
for (const walk of await realWalks(scope, walkPlan([path]))) {
|
|
312
|
+
const depth = walk.ownFilesOnly ? ['--max-depth', '1'] : [];
|
|
313
|
+
const glob = ['--glob', exactGlob(`${under}${path}`)];
|
|
314
|
+
const args = [...BASE_ARGS, '--files-with-matches', ...WALK_ARGS, ...depth, ...glob, ...NO_GIT, '--text', '-e', '\\x00', '--', ...walk.folders];
|
|
315
|
+
for await (const record of this.run(args, scope.cwd, undefined, '\n')) if (record.length > 0) return true;
|
|
316
|
+
}
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async list(scope: TreeScope): Promise<string[]> {
|
|
321
|
+
if (await rootMoved(scope)) return [];
|
|
322
|
+
const args = [...BASE_ARGS, '--files', '--null', ...WALK_ARGS, ...NO_GIT, '--', scope.root];
|
|
323
|
+
const out: string[] = [];
|
|
324
|
+
for await (const record of this.run(args, scope.cwd, undefined, '\0')) {
|
|
325
|
+
const path = underRoot(scope.root, record);
|
|
326
|
+
if (path !== null) out.push(path);
|
|
327
|
+
}
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async matchingLines(texts: readonly string[]): Promise<number[][]> {
|
|
332
|
+
const hits: number[][] = texts.map(() => []);
|
|
333
|
+
if (texts.length === 0) return hits;
|
|
334
|
+
// One run for every text: they go in one after another, and each line
|
|
335
|
+
// number that comes back is placed in the text it fell in. The final `\n`
|
|
336
|
+
// makes ripgrep's lines exactly the texts' `split('\n')`, empty last line
|
|
337
|
+
// included.
|
|
338
|
+
const starts: number[] = [];
|
|
339
|
+
let lines = 0;
|
|
340
|
+
for (const text of texts) {
|
|
341
|
+
starts.push(lines);
|
|
342
|
+
lines += countLines(text);
|
|
343
|
+
}
|
|
344
|
+
const args = [...BASE_ARGS, '--json', '--text', ...this.flags, '-e', this.pattern, '-'];
|
|
345
|
+
for await (const record of this.run(args, undefined, `${texts.join('\n')}\n`, '\n')) {
|
|
346
|
+
const event = parseEvent(record);
|
|
347
|
+
const n = event?.type === 'match' ? event.data?.line_number : undefined;
|
|
348
|
+
if (typeof n !== 'number') continue;
|
|
349
|
+
const at = n - 1;
|
|
350
|
+
const which = lastStartAtOrBefore(starts, at);
|
|
351
|
+
hits[which]!.push(at - starts[which]!);
|
|
352
|
+
}
|
|
353
|
+
return hits;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async charged<T>(work: () => Promise<T>): Promise<T> {
|
|
357
|
+
if (this.signal?.aborted) throw new SearchAbandonedError();
|
|
358
|
+
const remaining = this.budgetMs - this.spentMs;
|
|
359
|
+
if (remaining <= 0) throw new PatternTooExpensiveError(this.pattern, this.budgetMs);
|
|
360
|
+
const started = Date.now();
|
|
361
|
+
let timer: NodeJS.Timeout | undefined;
|
|
362
|
+
let timedOut = false;
|
|
363
|
+
let onAbort: (() => void) | undefined;
|
|
364
|
+
// The work is not ours to stop; what it settles with after the budget is
|
|
365
|
+
// gone goes nowhere.
|
|
366
|
+
const pending = work();
|
|
367
|
+
pending.catch(() => undefined);
|
|
368
|
+
try {
|
|
369
|
+
return await Promise.race([
|
|
370
|
+
pending,
|
|
371
|
+
new Promise<never>((_, reject) => {
|
|
372
|
+
timer = setTimeout(() => {
|
|
373
|
+
timedOut = true;
|
|
374
|
+
reject(new PatternTooExpensiveError(this.pattern, this.budgetMs));
|
|
375
|
+
}, remaining);
|
|
376
|
+
onAbort = () => reject(new SearchAbandonedError());
|
|
377
|
+
this.signal?.addEventListener('abort', onAbort, { once: true });
|
|
378
|
+
}),
|
|
379
|
+
]);
|
|
380
|
+
} finally {
|
|
381
|
+
if (timer) clearTimeout(timer);
|
|
382
|
+
if (onAbort) this.signal?.removeEventListener('abort', onAbort);
|
|
383
|
+
this.spend(started, timedOut);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Spend the wall time since `started`, or the whole budget when its timer
|
|
389
|
+
* fired: `Date.now()` counts whole milliseconds, so the time measured can
|
|
390
|
+
* fall a millisecond short of the timer's, and a search already answered
|
|
391
|
+
* too expensive must have nothing left for more work or another run.
|
|
392
|
+
*/
|
|
393
|
+
private spend(started: number, timedOut: boolean): void {
|
|
394
|
+
this.spentMs = timedOut ? this.budgetMs : this.spentMs + (Date.now() - started);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Run ripgrep and yield its stdout split on `separator`. The run is this
|
|
399
|
+
* generator's: it is killed when the caller stops reading, when the
|
|
400
|
+
* search's budget runs out (answered with {@link PatternTooExpensiveError}),
|
|
401
|
+
* when the search's caller goes away (answered with
|
|
402
|
+
* {@link SearchAbandonedError}), and on any error, and its slot is given
|
|
403
|
+
* back on every one of those. The wait for the slot is the run's too: it
|
|
404
|
+
* counts against the budget, and a caller gone or a budget spent while
|
|
405
|
+
* queued is answered from the queue, without a run.
|
|
406
|
+
*/
|
|
407
|
+
private async *run(args: string[], cwd: string | undefined, input: string | undefined, separator: string): AsyncGenerator<string> {
|
|
408
|
+
let release: Released | undefined;
|
|
409
|
+
let child: ChildProcess | undefined;
|
|
410
|
+
let timer: NodeJS.Timeout | undefined;
|
|
411
|
+
let timedOut = false;
|
|
412
|
+
let abandoned = false;
|
|
413
|
+
const onAbort = (): void => {
|
|
414
|
+
abandoned = true;
|
|
415
|
+
child?.kill('SIGKILL');
|
|
416
|
+
};
|
|
417
|
+
const started = Date.now();
|
|
418
|
+
try {
|
|
419
|
+
if (this.signal?.aborted) throw new SearchAbandonedError();
|
|
420
|
+
const budgeted = this.budgetMs - this.spentMs;
|
|
421
|
+
if (budgeted <= 0) throw new PatternTooExpensiveError(this.pattern, this.budgetMs);
|
|
422
|
+
release = await this.runner.slot({
|
|
423
|
+
signal: this.signal,
|
|
424
|
+
timeoutMs: budgeted,
|
|
425
|
+
expired: () => {
|
|
426
|
+
timedOut = true;
|
|
427
|
+
return new PatternTooExpensiveError(this.pattern, this.budgetMs);
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
// What the wait left of the budget is this run's.
|
|
431
|
+
const remaining = budgeted - (Date.now() - started);
|
|
432
|
+
if (remaining <= 0) {
|
|
433
|
+
timedOut = true;
|
|
434
|
+
throw new PatternTooExpensiveError(this.pattern, this.budgetMs);
|
|
435
|
+
}
|
|
436
|
+
child = this.runner.start(args, cwd, input !== undefined);
|
|
437
|
+
this.signal?.addEventListener('abort', onAbort, { once: true });
|
|
438
|
+
// Listening for a failed spawn first, so its error wins over the exit it also causes.
|
|
439
|
+
const failed = new Promise<never>((_, reject) => child!.once('error', reject));
|
|
440
|
+
failed.catch(() => undefined);
|
|
441
|
+
const exit = exited(child);
|
|
442
|
+
timer = setTimeout(() => {
|
|
443
|
+
timedOut = true;
|
|
444
|
+
child!.kill('SIGKILL');
|
|
445
|
+
}, remaining);
|
|
446
|
+
let stderr = '';
|
|
447
|
+
child.stderr!.setEncoding('utf8');
|
|
448
|
+
child.stderr!.on('data', (chunk: string) => {
|
|
449
|
+
if (stderr.length < STDERR_CAP) stderr += chunk;
|
|
450
|
+
});
|
|
451
|
+
if (input !== undefined) {
|
|
452
|
+
// ripgrep may finish (or be killed) before it reads everything.
|
|
453
|
+
child.stdin!.on('error', () => undefined);
|
|
454
|
+
child.stdin!.end(input);
|
|
455
|
+
}
|
|
456
|
+
child.stdout!.setEncoding('utf8');
|
|
457
|
+
// Only each new chunk is scanned, and a record's parts are joined once,
|
|
458
|
+
// when it ends: a record of many chunks (a match on one long line) costs
|
|
459
|
+
// its length, not its length times its chunks.
|
|
460
|
+
const parts: string[] = [];
|
|
461
|
+
for await (const chunk of child.stdout! as AsyncIterable<string>) {
|
|
462
|
+
let from = 0;
|
|
463
|
+
let at: number;
|
|
464
|
+
while ((at = chunk.indexOf(separator, from)) >= 0) {
|
|
465
|
+
parts.push(chunk.slice(from, at));
|
|
466
|
+
const record = parts.join('');
|
|
467
|
+
parts.length = 0;
|
|
468
|
+
from = at + 1;
|
|
469
|
+
yield record;
|
|
470
|
+
}
|
|
471
|
+
if (from < chunk.length) parts.push(from === 0 ? chunk : chunk.slice(from));
|
|
472
|
+
}
|
|
473
|
+
const rest = parts.join('');
|
|
474
|
+
if (rest.length > 0 && !timedOut && !abandoned) yield rest;
|
|
475
|
+
const { code } = await Promise.race([exit, failed]);
|
|
476
|
+
if (abandoned) throw new SearchAbandonedError();
|
|
477
|
+
if (timedOut) throw new PatternTooExpensiveError(this.pattern, this.budgetMs);
|
|
478
|
+
if (code === null && this.runner.isClosed()) throw new SearchBusyError();
|
|
479
|
+
// 0: matches, 1: none. 2 with nothing on stderr is a file ripgrep could
|
|
480
|
+
// not read with `--no-messages` on, which is a file to skip; 2 with a
|
|
481
|
+
// message is the run itself refused.
|
|
482
|
+
if (code === 2 && stderr.trim().length > 0) throw refusal(stderr);
|
|
483
|
+
if (code !== 0 && code !== 1 && code !== 2) throw new Error(`ripgrep ended with exit code ${String(code)}`);
|
|
484
|
+
} catch (err) {
|
|
485
|
+
if (abandoned) throw new SearchAbandonedError();
|
|
486
|
+
if (timedOut) throw new PatternTooExpensiveError(this.pattern, this.budgetMs);
|
|
487
|
+
throw err;
|
|
488
|
+
} finally {
|
|
489
|
+
this.signal?.removeEventListener('abort', onAbort);
|
|
490
|
+
if (timer) clearTimeout(timer);
|
|
491
|
+
if (child && child.exitCode === null && child.signalCode === null && child.pid !== undefined) {
|
|
492
|
+
const exit = exited(child);
|
|
493
|
+
child.kill('SIGKILL');
|
|
494
|
+
await exit;
|
|
495
|
+
}
|
|
496
|
+
this.spend(started, timedOut);
|
|
497
|
+
release?.();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Resolves when `child` has exited and its streams are closed (or it never started). */
|
|
503
|
+
function exited(child: ChildProcess): Promise<{ code: number | null }> {
|
|
504
|
+
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve({ code: child.exitCode });
|
|
505
|
+
return new Promise((resolve) => {
|
|
506
|
+
child.once('close', (code: number | null) => resolve({ code }));
|
|
507
|
+
child.once('error', () => resolve({ code: null }));
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* The error for a run ripgrep refused. A pattern it cannot compile is the
|
|
513
|
+
* caller's to fix, so its own reason is quoted, without the advice to pass
|
|
514
|
+
* flags the caller has no way to pass. Anything else is the server's failure,
|
|
515
|
+
* logged rather than shown.
|
|
516
|
+
*/
|
|
517
|
+
function refusal(stderr: string): Error {
|
|
518
|
+
const reason = stderr
|
|
519
|
+
.replace(/^rg: /, '')
|
|
520
|
+
.split(/\n\s*\n/)
|
|
521
|
+
.filter((part) => !/^\s*Consider enabling/.test(part))
|
|
522
|
+
.join('\n\n')
|
|
523
|
+
.trim();
|
|
524
|
+
if (/regex|pattern/i.test(reason)) return new PatternRefusedError(reason);
|
|
525
|
+
log.error(`ripgrep failed: ${reason}`);
|
|
526
|
+
return new Error('The search engine failed; see the server log.');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
type RgEvent = {
|
|
530
|
+
type?: string;
|
|
531
|
+
data?: {
|
|
532
|
+
path?: unknown;
|
|
533
|
+
lines?: unknown;
|
|
534
|
+
line_number?: unknown;
|
|
535
|
+
binary_offset?: unknown;
|
|
536
|
+
};
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
/** One `--json` record, or null for a line that is not one. */
|
|
540
|
+
function parseEvent(record: string): RgEvent | null {
|
|
541
|
+
try {
|
|
542
|
+
const event = JSON.parse(record) as unknown;
|
|
543
|
+
return event !== null && typeof event === 'object' ? (event as RgEvent) : null;
|
|
544
|
+
} catch {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** ripgrep's `{ text }` or base64 `{ bytes }` (a name that is not UTF-8) as a path. */
|
|
550
|
+
function pathOf(value: unknown): string | null {
|
|
551
|
+
const v = value as { text?: unknown; bytes?: unknown } | undefined;
|
|
552
|
+
if (typeof v?.text === 'string') return v.text;
|
|
553
|
+
if (typeof v?.bytes === 'string') return Buffer.from(v.bytes, 'base64').toString('utf8');
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** A line's text, or null for one that is not UTF-8 (`{ bytes }`), which is not text. */
|
|
558
|
+
function textOf(value: unknown): string | null {
|
|
559
|
+
const v = value as { text?: unknown } | undefined;
|
|
560
|
+
return typeof v?.text === 'string' ? v.text : null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function withoutNewline(text: string): string {
|
|
564
|
+
return text.endsWith('\n') ? text.slice(0, -1) : text;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** `path` as ripgrep printed it (under `root`), relative to `root`; null when it is not under it. */
|
|
568
|
+
function underRoot(root: string, path: string | null): string | null {
|
|
569
|
+
if (path === null) return null;
|
|
570
|
+
const prefix = `${root}/`;
|
|
571
|
+
return path.startsWith(prefix) && path.length > prefix.length ? path.slice(prefix.length) : null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* The glob that names exactly `path` (relative to where ripgrep runs):
|
|
576
|
+
* anchored with `/`, every character a glob reads specially escaped. A line
|
|
577
|
+
* break has no exact spelling in a glob, and a near one could name a sibling
|
|
578
|
+
* too, so such a name is refused rather than approximated.
|
|
579
|
+
*/
|
|
580
|
+
function exactGlob(path: string): string {
|
|
581
|
+
if (/[\r\n]/.test(path)) throw new Error(`a search cannot name ${JSON.stringify(path)} exactly: it holds a line break`);
|
|
582
|
+
return `/${path.replace(/[\\*?[\]{}!# ]/g, (c) => `\\${c}`)}`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** One ripgrep run of a tree search: the folders it walks, in order, the files it answers, and whether it keeps to the folders' own files. */
|
|
586
|
+
interface Walk {
|
|
587
|
+
/** Relative to the scope's root; `''` is the root. */
|
|
588
|
+
folders: string[];
|
|
589
|
+
/** The files of the batch this run answers, relative to the root: no other is opened. */
|
|
590
|
+
paths: string[];
|
|
591
|
+
/** Walk each folder's own files, none of its subfolders (`--max-depth 1`). */
|
|
592
|
+
ownFilesOnly: boolean;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Runs one search makes at most for one batch of files: a folder whose own
|
|
597
|
+
* files and subfolders alternate costs a run per alternation, and past this
|
|
598
|
+
* many the batch is walked in the fewer, wider runs a folder walked whole
|
|
599
|
+
* gives.
|
|
600
|
+
*/
|
|
601
|
+
const MAX_WALKS = 8;
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* The runs a walk of `paths` (in {@link compareSearchPaths} order, relative
|
|
605
|
+
* to the root) makes, in order. The paths are one stretch of a sorted walk,
|
|
606
|
+
* so in each folder only the first and the last child can hold files outside
|
|
607
|
+
* it: those subfolders are narrowed further, every child between is walked
|
|
608
|
+
* whole, and a folder's own files are walked by a run that keeps to them —
|
|
609
|
+
* which a subfolder sorting between two of them splits, so a folder that
|
|
610
|
+
* alternates costs a run per stretch. Run one after another, the walks visit
|
|
611
|
+
* the files in the order one walk of the root would. Past {@link MAX_WALKS},
|
|
612
|
+
* a folder holding any of the paths directly is walked whole instead.
|
|
613
|
+
*/
|
|
614
|
+
function walkPlan(paths: readonly string[]): Walk[] {
|
|
615
|
+
if (paths.length === 0) return [];
|
|
616
|
+
const fine = plan(paths, true);
|
|
617
|
+
return fine.length <= MAX_WALKS ? fine : plan(paths, false);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function plan(paths: readonly string[], ownFiles: boolean): Walk[] {
|
|
621
|
+
const walks: Walk[] = [];
|
|
622
|
+
// Consecutive runs of one kind are one run given several folders; a
|
|
623
|
+
// folder's own files with nothing walked between them are one folder.
|
|
624
|
+
const push = (folder: string, ownFilesOnly: boolean, group: readonly string[][]): void => {
|
|
625
|
+
const paths = group.map((segments) => segments.join('/'));
|
|
626
|
+
const last = walks[walks.length - 1];
|
|
627
|
+
if (last === undefined || last.ownFilesOnly !== ownFilesOnly) {
|
|
628
|
+
walks.push({ folders: [folder], paths, ownFilesOnly });
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (!(ownFilesOnly && last.folders[last.folders.length - 1] === folder)) last.folders.push(folder);
|
|
632
|
+
last.paths.push(...paths);
|
|
633
|
+
};
|
|
634
|
+
const cover = (folder: string, depth: number, group: readonly string[][]): void => {
|
|
635
|
+
const holdsFiles = group.some((segments) => segments.length === depth + 1);
|
|
636
|
+
if (holdsFiles && !ownFiles) {
|
|
637
|
+
push(folder, false, group);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const children: string[][][] = [];
|
|
641
|
+
for (const segments of group) {
|
|
642
|
+
const last = children[children.length - 1];
|
|
643
|
+
if (last !== undefined && last[0]![depth] === segments[depth]) last.push(segments);
|
|
644
|
+
else children.push([segments]);
|
|
645
|
+
}
|
|
646
|
+
children.forEach((child, i) => {
|
|
647
|
+
if (child[0]!.length === depth + 1) {
|
|
648
|
+
push(folder, true, child);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const name = child[0]!.slice(0, depth + 1).join('/');
|
|
652
|
+
if (i === 0 || i === children.length - 1) cover(name, depth + 1, child);
|
|
653
|
+
else push(name, false, child);
|
|
654
|
+
});
|
|
655
|
+
};
|
|
656
|
+
cover('', 0, paths.map((p) => p.split('/')));
|
|
657
|
+
return walks;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* `walks` as ripgrep is given them — when each folder is still the real
|
|
662
|
+
* folder the walk listed. ripgrep follows a link it is GIVEN, so a folder
|
|
663
|
+
* that has become a link since (or is gone) sends the search back to one walk
|
|
664
|
+
* of the root, whose `--no-follow` never enters it. The root is checked
|
|
665
|
+
* again here, as where the scope was made: one that has become a link since
|
|
666
|
+
* is given to ripgrep not at all, and nothing is walked.
|
|
667
|
+
*/
|
|
668
|
+
async function realWalks(scope: TreeScope, walks: readonly Walk[]): Promise<Walk[]> {
|
|
669
|
+
if (walks.length === 0 || (await rootMoved(scope))) return [];
|
|
670
|
+
const folders = [...new Set(walks.flatMap((w) => w.folders).filter((folder) => folder !== ''))];
|
|
671
|
+
const real = await Promise.all(folders.map((folder) => isRealFolder(scope.cwd, `${scope.root}/${folder}`)));
|
|
672
|
+
if (!real.every(Boolean)) return [{ folders: [scope.root], paths: walks.flatMap((w) => w.paths), ownFilesOnly: false }];
|
|
673
|
+
return walks.map((w) => ({
|
|
674
|
+
...w,
|
|
675
|
+
folders: w.folders.map((folder) => (folder === '' ? scope.root : `${scope.root}/${folder}`)),
|
|
676
|
+
}));
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Whether the scope's root now resolves elsewhere: it, or a folder above it,
|
|
681
|
+
* has become a link since the scope was made. A root that is gone holds
|
|
682
|
+
* nothing for ripgrep to find, so it is walked as it is.
|
|
683
|
+
*/
|
|
684
|
+
async function rootMoved(scope: TreeScope): Promise<boolean> {
|
|
685
|
+
const absolute = join(scope.cwd, scope.root);
|
|
686
|
+
return (await realpath(absolute).catch(() => absolute)) !== absolute;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** Whether `folder` (relative to `cwd`) is still itself: no link on the way to it, and there. */
|
|
690
|
+
async function isRealFolder(cwd: string, folder: string): Promise<boolean> {
|
|
691
|
+
const absolute = join(cwd, folder);
|
|
692
|
+
return (await realpath(absolute).catch(() => null)) === absolute;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function countLines(text: string): number {
|
|
696
|
+
let n = 1;
|
|
697
|
+
for (let i = text.indexOf('\n'); i >= 0; i = text.indexOf('\n', i + 1)) n++;
|
|
698
|
+
return n;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/** The index of the last start at or before `at` (starts ascending, `starts[0]` is 0). */
|
|
702
|
+
function lastStartAtOrBefore(starts: number[], at: number): number {
|
|
703
|
+
let lo = 0;
|
|
704
|
+
let hi = starts.length - 1;
|
|
705
|
+
while (lo < hi) {
|
|
706
|
+
const mid = (lo + hi + 1) >> 1;
|
|
707
|
+
if (starts[mid]! <= at) lo = mid;
|
|
708
|
+
else hi = mid - 1;
|
|
709
|
+
}
|
|
710
|
+
return lo;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function option(name: string, value: number, min: number, max = Number.MAX_SAFE_INTEGER): number {
|
|
714
|
+
return sizingOption('RipgrepPatternMatcher', name, value, min, max);
|
|
715
|
+
}
|