pi-lens 3.8.73 → 4.0.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/CHANGELOG.md +2614 -12
- package/README.md +50 -21
- package/dist/clients/actionable-warnings.js +73 -18
- package/dist/clients/agent-behavior-client.js +21 -4
- package/dist/clients/ast-grep-client.js +68 -16
- package/dist/clients/ast-grep-types.js +0 -1
- package/dist/clients/ast-grep-yaml-synth.js +37 -10
- package/dist/clients/atomic-write-staging.js +63 -0
- package/dist/clients/atomic-write.js +116 -16
- package/dist/clients/bash-file-access.js +193 -23
- package/dist/clients/biome-client.js +29 -25
- package/dist/clients/bootstrap.js +11 -2
- package/dist/clients/bounded-cache.js +34 -0
- package/dist/clients/bounded-pid-file-lock.js +192 -0
- package/dist/clients/bus-events-logger.js +5 -1
- package/dist/clients/bus-publish.js +10 -3
- package/dist/clients/cache/rule-cache.js +89 -6
- package/dist/clients/cache-manager.js +97 -9
- package/dist/clients/cache-observability.js +498 -0
- package/dist/clients/call-graph.js +460 -108
- package/dist/clients/cascade-logger.js +5 -1
- package/dist/clients/child-unref.js +85 -0
- package/dist/clients/codebase-model.js +52 -23
- package/dist/clients/collateral-test-role.js +40 -0
- package/dist/clients/complexity-client.js +6 -8
- package/dist/clients/console-guard-install.js +14 -0
- package/dist/clients/cooperative-budget.js +42 -0
- package/dist/clients/dead-code-client.js +2 -1
- package/dist/clients/debug-handles.js +206 -0
- package/dist/clients/debug-heap.js +167 -0
- package/dist/clients/degradation-ledger.js +153 -0
- package/dist/clients/dependency-checker.js +466 -63
- package/dist/clients/diagnostic-dispositions.js +83 -13
- package/dist/clients/diagnostics-publish.js +10 -3
- package/dist/clients/dispatch/auxiliary-lsp.js +34 -0
- package/dist/clients/dispatch/dispatcher.js +71 -13
- package/dist/clients/dispatch/fact-store.js +11 -0
- package/dist/clients/dispatch/facts/function-facts.js +37 -7
- package/dist/clients/dispatch/facts/import-facts.js +108 -119
- package/dist/clients/dispatch/facts/tree-sitter-facts.js +12 -12
- package/dist/clients/dispatch/facts/try-catch-facts.js +13 -0
- package/dist/clients/dispatch/indent-detect.js +35 -0
- package/dist/clients/dispatch/inline-suppressions.js +17 -8
- package/dist/clients/dispatch/integration.js +1094 -611
- package/dist/clients/dispatch/lazy.js +14 -0
- package/dist/clients/dispatch/plan.js +11 -1
- package/dist/clients/dispatch/rule-id-normalize.js +50 -0
- package/dist/clients/dispatch/rule-policy.js +135 -0
- package/dist/clients/dispatch/rules/async-noise.js +1 -0
- package/dist/clients/dispatch/rules/unsafe-boundary.js +7 -0
- package/dist/clients/dispatch/runners/ast-grep-napi.js +47 -6
- package/dist/clients/dispatch/runners/biome-check.js +5 -9
- package/dist/clients/dispatch/runners/detekt.js +3 -2
- package/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/dist/clients/dispatch/runners/helm-lint.js +154 -0
- package/dist/clients/dispatch/runners/htmlhint.js +3 -2
- package/dist/clients/dispatch/runners/index.js +5 -1
- package/dist/clients/dispatch/runners/lsp.js +14 -6
- package/dist/clients/dispatch/runners/markdownlint.js +4 -9
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -1
- package/dist/clients/dispatch/runners/pyright.js +9 -5
- package/dist/clients/dispatch/runners/ruff.js +4 -8
- package/dist/clients/dispatch/runners/shellcheck.js +2 -3
- package/dist/clients/dispatch/runners/shfmt.js +2 -3
- package/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/dist/clients/dispatch/runners/taplo.js +2 -1
- package/dist/clients/dispatch/runners/terragrunt.js +163 -0
- package/dist/clients/dispatch/runners/tflint.js +37 -5
- package/dist/clients/dispatch/runners/tree-sitter.js +155 -166
- package/dist/clients/dispatch/runners/trivy-config.js +13 -7
- package/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +284 -34
- package/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
- package/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
- package/dist/clients/disposition-publish.js +10 -3
- package/dist/clients/durable-store.js +97 -0
- package/dist/clients/event-loop-monitor.js +94 -3
- package/dist/clients/extension-log.js +139 -0
- package/dist/clients/extension-mode.js +73 -0
- package/dist/clients/feature-hints.js +53 -7
- package/dist/clients/file-kinds.js +143 -31
- package/dist/clients/file-role.js +18 -3
- package/dist/clients/file-utils.js +56 -34
- package/dist/clients/fix-worklog.js +14 -2
- package/dist/clients/format-events-publish.js +16 -3
- package/dist/clients/format-service.js +24 -8
- package/dist/clients/formatters-lazy.js +8 -0
- package/dist/clients/formatters.js +358 -61
- package/dist/clients/generated-artifacts.js +169 -15
- package/dist/clients/git-guard.js +681 -11
- package/dist/clients/git-tracked-ignore.js +4 -0
- package/dist/clients/go-client.js +2 -1
- package/dist/clients/govulncheck-client.js +8 -0
- package/dist/clients/grammar-source.js +12 -4
- package/dist/clients/gzip-stage-write.js +102 -0
- package/dist/clients/host-ports.js +31 -0
- package/dist/clients/installer/index.js +733 -213
- package/dist/clients/instance-reaper.js +142 -172
- package/dist/clients/jscpd-client.js +31 -54
- package/dist/clients/knip-client.js +78 -36
- package/dist/clients/language-policy.js +14 -2
- package/dist/clients/language-profile.js +72 -77
- package/dist/clients/latency-logger.js +28 -16
- package/dist/clients/lens-config.js +140 -166
- package/dist/clients/lens-engine.js +124 -21
- package/dist/clients/lens-events.js +27 -4
- package/dist/clients/lens-flag-registry.js +293 -0
- package/dist/clients/lens-map.js +96 -26
- package/dist/clients/live-bus-emitter.js +30 -0
- package/dist/clients/log-cleanup.js +8 -18
- package/dist/clients/lsp/cascade-tier.js +23 -3
- package/dist/clients/lsp/client.js +566 -57
- package/dist/clients/lsp/config.js +74 -10
- package/dist/clients/lsp/diagnostic-binding.js +86 -0
- package/dist/clients/lsp/edits.js +1012 -121
- package/dist/clients/lsp/index.js +1840 -803
- package/dist/clients/lsp/language.js +4 -1
- package/dist/clients/lsp/launch.js +40 -32
- package/dist/clients/lsp/path-utils.js +1 -1
- package/dist/clients/lsp/ruby-drive-dirs.js +83 -0
- package/dist/clients/lsp/server.js +279 -60
- package/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
- package/dist/clients/lsp-document-symbols.js +112 -0
- package/dist/clients/lsp-lazy.js +8 -0
- package/dist/clients/lsp-mutation.js +248 -0
- package/dist/clients/mcp/analyze.js +128 -36
- package/dist/clients/mcp/ipc.js +157 -24
- package/dist/clients/mcp/review.js +36 -3
- package/dist/clients/mcp/session.js +216 -8
- package/dist/clients/memory-sampler.js +115 -0
- package/dist/clients/metrics-client.js +2 -1
- package/dist/clients/metrics-history.js +2 -1
- package/dist/clients/module-report-lsp.js +5 -0
- package/dist/clients/module-report.js +267 -82
- package/dist/clients/ndjson-logger.js +334 -103
- package/dist/clients/opengrep-client.js +1 -1
- package/dist/clients/partial-edit-apply.js +120 -5
- package/dist/clients/path-keyed-map.js +76 -0
- package/dist/clients/path-utils.js +141 -5
- package/dist/clients/performance-report.js +327 -0
- package/dist/clients/pipeline.js +54 -13
- package/dist/clients/print-mode.js +21 -0
- package/dist/clients/project-changes.js +117 -4
- package/dist/clients/project-diagnostics/cache.js +3 -2
- package/dist/clients/project-diagnostics/extractors.js +26 -115
- package/dist/clients/project-diagnostics/fresh-fetch.js +149 -19
- package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +117 -0
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +37 -5
- package/dist/clients/project-diagnostics/scanner.js +268 -128
- package/dist/clients/project-lens-config.js +138 -34
- package/dist/clients/project-report.js +64 -25
- package/dist/clients/project-scale.js +8 -10
- package/dist/clients/project-snapshot-persist-worker.js +6 -0
- package/dist/clients/project-snapshot.js +767 -8
- package/dist/clients/project-trust.js +181 -0
- package/dist/clients/quiet-window.js +27 -5
- package/dist/clients/read-bridge.js +147 -0
- package/dist/clients/read-expansion.js +22 -12
- package/dist/clients/read-guard-logger.js +159 -1
- package/dist/clients/read-guard-tool-lines.js +150 -29
- package/dist/clients/read-guard.js +262 -3
- package/dist/clients/redact/secrets.js +449 -0
- package/dist/clients/resource-sampler.js +144 -39
- package/dist/clients/reverse-deps.js +48 -0
- package/dist/clients/review-graph/builder.js +2570 -326
- package/dist/clients/review-graph/git-identity.js +2 -1
- package/dist/clients/review-graph/import-resolvers.js +21 -10
- package/dist/clients/review-graph/persist-worker.js +10 -0
- package/dist/clients/review-graph/query.js +5 -0
- package/dist/clients/review-graph/shared-extraction-ir.js +90 -0
- package/dist/clients/review-graph/symbol-id.js +46 -0
- package/dist/clients/review-graph/tsconfig-paths.js +57 -6
- package/dist/clients/review-graph/workspace-modules.js +51 -3
- package/dist/clients/review-graph-logger.js +53 -0
- package/dist/clients/ruff-client.js +27 -44
- package/dist/clients/runtime-agent-end.js +111 -75
- package/dist/clients/runtime-context.js +36 -12
- package/dist/clients/runtime-coordinator.js +76 -8
- package/dist/clients/runtime-session.js +799 -137
- package/dist/clients/runtime-tool-call.js +117 -62
- package/dist/clients/runtime-tool-result.js +218 -23
- package/dist/clients/runtime-turn.js +284 -40
- package/dist/clients/rust-client.js +2 -1
- package/dist/clients/safe-spawn.js +873 -71
- package/dist/clients/security-scan-client.js +2 -1
- package/dist/clients/session-start-observability.js +15 -0
- package/dist/clients/session-state-store.js +5 -3
- package/dist/clients/sessionstart-logger.js +18 -0
- package/dist/clients/sg-runner.js +284 -159
- package/dist/clients/slow-fs.js +2 -1
- package/dist/clients/smells-rollup.js +224 -0
- package/dist/clients/source-filter.js +231 -43
- package/dist/clients/source-walker.js +66 -10
- package/dist/clients/startup-scan.js +15 -16
- package/dist/clients/startup-timing.js +9 -0
- package/dist/clients/test-runner-client.js +38 -8
- package/dist/clients/todo-scanner.js +30 -3
- package/dist/clients/tool-event.js +35 -0
- package/dist/clients/tool-policy.js +388 -39
- package/dist/clients/tool-render.js +155 -0
- package/dist/clients/tree-sitter-cache.js +153 -140
- package/dist/clients/tree-sitter-client.js +1722 -80
- package/dist/clients/tree-sitter-logger.js +64 -1
- package/dist/clients/tree-sitter-query-loader.js +117 -12
- package/dist/clients/tree-sitter-shared.js +48 -19
- package/dist/clients/tree-sitter-symbol-extractor.js +115 -11
- package/dist/clients/user-notify.js +51 -0
- package/dist/clients/vanished-instance-marker.js +57 -0
- package/dist/clients/warm-attach.js +33 -13
- package/dist/clients/widget-state.js +342 -49
- package/dist/clients/word-index-logger.js +44 -0
- package/dist/clients/word-index.js +612 -68
- package/dist/clients/workspace-topology.js +100 -7
- package/dist/index.js +65923 -47161
- package/dist/mcp/analyze-cli.js +185 -29
- package/dist/mcp/cli.js +110 -0
- package/dist/mcp/server.js +272 -50
- package/dist/tools/ast-grep-search.js +93 -27
- package/dist/tools/lens-diagnostics.js +247 -41
- package/dist/tools/lsp-diagnostics.js +153 -40
- package/dist/tools/lsp-navigation.js +51 -31
- package/dist/tools/module-report.js +9 -0
- package/dist/tools/symbol-search.js +21 -2
- package/docs/agent-guide.md +247 -0
- package/docs/agent-tools.md +21 -10
- package/docs/analysisall.md +192 -0
- package/docs/api-ports-inventory.md +165 -0
- package/docs/ast-grep_rules_catalog.md +5 -2
- package/docs/astplayground.md +3 -4
- package/docs/audit1.md +5 -5
- package/docs/durable-store-audit-1202.md +32 -0
- package/docs/environment-variables.md +197 -11
- package/docs/fable.md +27 -0
- package/docs/features.md +35 -5
- package/docs/globalconfig.md +101 -5
- package/docs/language-coverage.md +6 -5
- package/docs/lsp-capability-matrix.md +2 -2
- package/docs/mcp.md +54 -20
- package/docs/servercapabilities.md +7 -4
- package/docs/settings.md +192 -0
- package/docs/tools.md +18 -1
- package/docs/tree-sitter_rules_catalog.md +11 -31
- package/docs/usage.md +31 -0
- package/package.json +139 -138
- package/rules/ast-grep-rules/rule-tests/hardcoded-url-js-test.yml +1 -0
- package/rules/ast-grep-rules/rule-tests/hardcoded-url-test.yml +2 -0
- package/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
- package/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
- package/rules/ast-grep-rules/rule-tests/no-case-declarations-js-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-case-declarations-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-js-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
- package/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
- package/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
- package/rules/ast-grep-rules/rule-tests/strict-equality-js-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-equality-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-inequality-js-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-inequality-test.yml +4 -0
- package/rules/ast-grep-rules/rules/hardcoded-url-js.yml +22 -4
- package/rules/ast-grep-rules/rules/hardcoded-url.yml +22 -4
- package/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
- package/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
- package/rules/ast-grep-rules/rules/no-case-declarations-js.yml +4 -3
- package/rules/ast-grep-rules/rules/no-case-declarations.yml +4 -3
- package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +10 -0
- package/rules/ast-grep-rules/rules/no-console-except-error.yml +10 -0
- package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +42 -6
- package/rules/ast-grep-rules/rules/no-javascript-url.yml +42 -6
- package/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
- package/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
- package/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
- package/rules/ast-grep-rules/rules/strict-equality-js.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-equality.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-inequality-js.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-inequality.yml +14 -1
- package/rules/tree-sitter-queries/cpp/no-memset-sensitive-data.yml +9 -3
- package/rules/tree-sitter-queries/cpp/no-scoped-lock-without-args.yml +6 -6
- package/rules/tree-sitter-queries/css/calc-spacing.yml +8 -6
- package/rules/tree-sitter-queries/go/go-mutex-copy.yml +21 -18
- package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +3 -3
- package/rules/tree-sitter-queries/java/infinite-loop-java.yml +3 -3
- package/rules/tree-sitter-queries/java/junit-call-super.yml +6 -4
- package/rules/tree-sitter-queries/java/no-double-checked-locking.yml +10 -8
- package/rules/tree-sitter-queries/java/no-field-shadowing.yml +1 -1
- package/rules/tree-sitter-queries/java/prepared-statement-valid-indices.yml +2 -3
- package/rules/tree-sitter-queries/java/resources-closed.yml +2 -2
- package/rules/tree-sitter-queries/java/springboot-default-package.yml +6 -3
- package/rules/tree-sitter-queries/java/switch-fall-through.yml +6 -4
- package/rules/tree-sitter-queries/java/switch-non-case-labels.yml +4 -3
- package/rules/tree-sitter-queries/java/tests-include-assertions.yml +6 -5
- package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +5 -8
- package/rules/tree-sitter-queries/javascript/switch-non-case-labels.yml +3 -2
- package/rules/tree-sitter-queries/kotlin/prepared-statement-indices.yml +9 -5
- package/rules/tree-sitter-queries/php/this-in-static-context.yml +5 -5
- package/rules/tree-sitter-queries/python/exit-signature-check.yml +2 -9
- package/rules/tree-sitter-queries/python/in-operator-unsupported.yml +3 -4
- package/rules/tree-sitter-queries/python/no-super-torchscript.yml +4 -8
- package/rules/tree-sitter-queries/python/notimplemented-boolean-context.yml +1 -5
- package/rules/tree-sitter-queries/python/python-empty-except.yml +1 -1
- package/rules/tree-sitter-queries/python/return-in-init.yml +0 -2
- package/rules/tree-sitter-queries/python/yield-return-outside-function.yml +0 -3
- package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +1 -12
- package/rules/tree-sitter-queries/ruby/ruby-eval.yml +1 -1
- package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +4 -4
- package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +4 -11
- package/rules/tree-sitter-queries/ruby/ruby-open-struct.yml +1 -1
- package/rules/tree-sitter-queries/ruby/ruby-string-eval.yml +2 -3
- package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +4 -4
- package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +4 -3
- package/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
- package/rules/tree-sitter-queries/typescript/console-statement.yml +11 -0
- package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +4 -9
- package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +13 -9
- package/rules/tree-sitter-queries/typescript/infinite-loop.yml +2 -2
- package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +10 -1
- package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +5 -8
- package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +7 -10
- package/rules/tree-sitter-queries/typescript/unsafe-regex.yml +4 -2
- package/rules/typos/_typos.toml +17 -0
- package/scripts/analyze-pi-lens-logs.mjs +55 -2
- package/skills/pi-lens-write-tree-sitter-rule/SKILL.md +5 -0
- package/dist/clients/ast-grep-parser.js +0 -86
- package/dist/clients/config-enabled-shape.js +0 -39
- package/dist/clients/lsp/interactive-install.js +0 -367
- package/dist/clients/lsp/lsp-index.js +0 -10
- package/dist/clients/lsp/server-strategies.js +0 -5
- package/dist/clients/project-metadata.js +0 -690
- package/dist/clients/source-groups.js +0 -96
- package/dist/clients/types.js +0 -11
- package/docs/env_variables.md +0 -10
- package/rules/tree-sitter-queries/abap-disabled/delete-where.yml +0 -46
- package/rules/tree-sitter-queries/cpp/no-confused-move-forward.yml +0 -59
- package/rules/tree-sitter-queries/cpp/noexcept-functions.yml +0 -58
- package/rules/tree-sitter-queries/java/mockito-initialized.yml +0 -66
- package/rules/tree-sitter-queries/java/no-wait-notify-on-thread.yml +0 -58
- package/rules/tree-sitter-queries/java/short-circuit-logic.yml +0 -57
- package/rules/tree-sitter-queries/java/spring-session-attributes-setcomplete.yml +0 -75
- package/rules/tree-sitter-queries/plsql-disabled/delete-update-where.yml +0 -56
- package/rules/tree-sitter-queries/plsql-disabled/end-loop-semicolon.yml +0 -51
- package/rules/tree-sitter-queries/plsql-disabled/fetch-bulk-collect-limit.yml +0 -47
- package/rules/tree-sitter-queries/plsql-disabled/forallsave-exceptions.yml +0 -51
- package/rules/tree-sitter-queries/plsql-disabled/nchar-nvarchar2-bytes.yml +0 -54
- package/rules/tree-sitter-queries/plsql-disabled/not-null-initialization.yml +0 -59
- package/rules/tree-sitter-queries/plsql-disabled/raise-application-error-codes.yml +0 -50
- package/rules/tree-sitter-queries/typescript-disabled/ts-path-traversal.yml +0 -71
|
@@ -15,10 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import * as nodeFs from "node:fs";
|
|
17
17
|
import * as path from "node:path";
|
|
18
|
+
import { isReadableSourceFile } from "./file-kinds.js";
|
|
18
19
|
import { countFileLines } from "./read-guard-tool-lines.js";
|
|
19
|
-
// Source-ish extensions worth registering. Anchored end-check → linear (no
|
|
20
|
-
// catastrophic backtracking).
|
|
21
|
-
const READABLE_EXT_RE = /\.(?:ts|tsx|js|jsx|mjs|cjs|py|sh|rs|go|cs|java|kt|rb|php|c|cpp|cc|h|hpp|json|jsonc|yaml|yml|toml|md|txt|env|cfg|conf|ini|html|css|scss|less|xml|sql|vue|svelte)$/i;
|
|
22
20
|
function stripQuotes(token) {
|
|
23
21
|
if (token.length >= 2) {
|
|
24
22
|
const first = token[0];
|
|
@@ -32,14 +30,123 @@ function stripQuotes(token) {
|
|
|
32
30
|
function stripAnsi(value) {
|
|
33
31
|
return value.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
|
|
34
32
|
}
|
|
35
|
-
function
|
|
36
|
-
const
|
|
37
|
-
|
|
33
|
+
export function tokenizeShellCommand(command) {
|
|
34
|
+
const segments = [];
|
|
35
|
+
let tokens = [];
|
|
36
|
+
let word = "";
|
|
37
|
+
let quote;
|
|
38
|
+
let escaped = false;
|
|
39
|
+
let unsupported = false;
|
|
40
|
+
let atTokenStart = true;
|
|
41
|
+
const flushWord = () => {
|
|
42
|
+
if (word)
|
|
43
|
+
tokens.push(word);
|
|
44
|
+
word = "";
|
|
45
|
+
atTokenStart = true;
|
|
46
|
+
};
|
|
47
|
+
const flushSegment = () => {
|
|
48
|
+
flushWord();
|
|
49
|
+
if (tokens.length > 0 || unsupported)
|
|
50
|
+
segments.push({ tokens, unsupported });
|
|
51
|
+
tokens = [];
|
|
52
|
+
unsupported = false;
|
|
53
|
+
atTokenStart = true;
|
|
54
|
+
};
|
|
55
|
+
for (let i = 0; i < command.length; i++) {
|
|
56
|
+
const ch = command[i];
|
|
57
|
+
const next = command[i + 1];
|
|
58
|
+
if (quote === "single") {
|
|
59
|
+
if (ch === "'")
|
|
60
|
+
quote = undefined;
|
|
61
|
+
else
|
|
62
|
+
word += ch;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (quote === "double") {
|
|
66
|
+
if (escaped) {
|
|
67
|
+
word += ch;
|
|
68
|
+
escaped = false;
|
|
69
|
+
}
|
|
70
|
+
else if (ch === "\\")
|
|
71
|
+
escaped = true;
|
|
72
|
+
else if (ch === '"')
|
|
73
|
+
quote = undefined;
|
|
74
|
+
else
|
|
75
|
+
word += ch;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (escaped) {
|
|
79
|
+
// A backslash-newline is a shell line continuation, not part of the
|
|
80
|
+
// command argument. Keeping the newline here makes a continued git command
|
|
81
|
+
// visible to command guards.
|
|
82
|
+
if (ch !== "\n")
|
|
83
|
+
word += ch;
|
|
84
|
+
escaped = false;
|
|
85
|
+
atTokenStart = false;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (ch === "\\") {
|
|
89
|
+
// Bash uses backslash as a general escape, but command inputs on
|
|
90
|
+
// Windows routinely contain native paths (C:\\src\\file.ts). Preserve
|
|
91
|
+
// backslashes before ordinary path characters while still honoring
|
|
92
|
+
// shell escapes and line continuations.
|
|
93
|
+
if (next === "\n" || /[\s\\'\";$|&<>]/.test(next ?? "")) {
|
|
94
|
+
escaped = true;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
word += ch;
|
|
98
|
+
atTokenStart = false;
|
|
99
|
+
}
|
|
100
|
+
atTokenStart = false;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (ch === "'") {
|
|
104
|
+
quote = "single";
|
|
105
|
+
atTokenStart = false;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (ch === '"') {
|
|
109
|
+
quote = "double";
|
|
110
|
+
atTokenStart = false;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (ch === "#" && atTokenStart) {
|
|
114
|
+
while (i + 1 < command.length && command[i + 1] !== "\n")
|
|
115
|
+
i++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (/\s/.test(ch)) {
|
|
119
|
+
flushWord();
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (ch === ";" || ch === "\n" || ch === "|" || ch === "&") {
|
|
123
|
+
flushWord();
|
|
124
|
+
if (ch === "|" && next === "|")
|
|
125
|
+
i++;
|
|
126
|
+
else if (ch === "&" && next === "&")
|
|
127
|
+
i++;
|
|
128
|
+
else if (ch === "|" || ch === "&")
|
|
129
|
+
unsupported = true;
|
|
130
|
+
flushSegment();
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (ch === "<" || ch === ">") {
|
|
134
|
+
flushWord();
|
|
135
|
+
unsupported = true;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
word += ch;
|
|
139
|
+
atTokenStart = false;
|
|
140
|
+
}
|
|
141
|
+
if (quote || escaped)
|
|
142
|
+
unsupported = true;
|
|
143
|
+
flushSegment();
|
|
144
|
+
return segments;
|
|
38
145
|
}
|
|
39
146
|
/** Resolve a token to an absolute path if it looks like a source file. */
|
|
40
147
|
function resolveCandidate(token, cwd) {
|
|
41
148
|
const cleaned = stripQuotes(token);
|
|
42
|
-
if (!cleaned || cleaned.startsWith("-") || !
|
|
149
|
+
if (!cleaned || cleaned.startsWith("-") || !isReadableSourceFile(cleaned)) {
|
|
43
150
|
return null;
|
|
44
151
|
}
|
|
45
152
|
return path.isAbsolute(cleaned) ? cleaned : path.resolve(cwd, cleaned);
|
|
@@ -52,8 +159,67 @@ function parseCountFlag(token) {
|
|
|
52
159
|
const n = Number.parseInt(digits, 10);
|
|
53
160
|
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
54
161
|
}
|
|
55
|
-
function
|
|
56
|
-
return command.
|
|
162
|
+
function commandSegments(command) {
|
|
163
|
+
return tokenizeShellCommand(command).map((segment) => segment.tokens);
|
|
164
|
+
}
|
|
165
|
+
/** Find redirect targets without treating quoted `>` characters as syntax. */
|
|
166
|
+
function extractRedirectTargets(command) {
|
|
167
|
+
const targets = [];
|
|
168
|
+
let quote;
|
|
169
|
+
let escaped = false;
|
|
170
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
171
|
+
const ch = command[i];
|
|
172
|
+
if (quote === "single") {
|
|
173
|
+
if (ch === "'")
|
|
174
|
+
quote = undefined;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (quote === "double") {
|
|
178
|
+
if (escaped)
|
|
179
|
+
escaped = false;
|
|
180
|
+
else if (ch === "\\")
|
|
181
|
+
escaped = true;
|
|
182
|
+
else if (ch === '"')
|
|
183
|
+
quote = undefined;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (escaped) {
|
|
187
|
+
escaped = false;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (ch === "\\") {
|
|
191
|
+
escaped = true;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (ch === "'") {
|
|
195
|
+
quote = "single";
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (ch === '"') {
|
|
199
|
+
quote = "double";
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (ch !== ">")
|
|
203
|
+
continue;
|
|
204
|
+
while (i + 1 < command.length && /\s/.test(command[i + 1]))
|
|
205
|
+
i += 1;
|
|
206
|
+
if (command[i + 1] === ">")
|
|
207
|
+
i += 1;
|
|
208
|
+
while (i + 1 < command.length && /\s/.test(command[i + 1]))
|
|
209
|
+
i += 1;
|
|
210
|
+
let target = "";
|
|
211
|
+
for (i += 1; i < command.length; i += 1) {
|
|
212
|
+
const next = command[i];
|
|
213
|
+
if (/\s/.test(next) || next === ";" || next === "|" || next === "&") {
|
|
214
|
+
i -= 1;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
target += next;
|
|
218
|
+
}
|
|
219
|
+
if (target)
|
|
220
|
+
targets.push(target);
|
|
221
|
+
}
|
|
222
|
+
return targets;
|
|
57
223
|
}
|
|
58
224
|
/**
|
|
59
225
|
* Extract the line ranges a bash command explicitly showed the agent.
|
|
@@ -93,11 +259,9 @@ export function extractReadPathsFromCommand(command, cwd) {
|
|
|
93
259
|
seen.add(key);
|
|
94
260
|
spans.push({ filePath: file.abs, offset, limit });
|
|
95
261
|
};
|
|
96
|
-
for (const
|
|
97
|
-
|
|
98
|
-
if (!segment)
|
|
262
|
+
for (const tokens of commandSegments(command)) {
|
|
263
|
+
if (tokens.length === 0)
|
|
99
264
|
continue;
|
|
100
|
-
const tokens = segment.split(/\s+/);
|
|
101
265
|
const verb = path.basename(tokens[0] ?? "");
|
|
102
266
|
const args = tokens.slice(1);
|
|
103
267
|
if (["cat", "bat", "less", "more", "nl"].includes(verb)) {
|
|
@@ -247,8 +411,7 @@ function parseGrepLineWithoutFile(line, file) {
|
|
|
247
411
|
function collectGrepCommandFiles(command, cwd) {
|
|
248
412
|
const files = new Set();
|
|
249
413
|
let hasLineNumberGrep = false;
|
|
250
|
-
for (const
|
|
251
|
-
const tokens = tokenizeSegment(rawSegment.trim());
|
|
414
|
+
for (const tokens of commandSegments(command)) {
|
|
252
415
|
const verb = path.basename(stripQuotes(tokens[0] ?? ""));
|
|
253
416
|
if (verb !== "grep" && verb !== "egrep" && verb !== "fgrep")
|
|
254
417
|
continue;
|
|
@@ -311,15 +474,22 @@ export function extractWrittenPathsFromCommand(command, cwd) {
|
|
|
311
474
|
if (abs)
|
|
312
475
|
out.add(abs);
|
|
313
476
|
};
|
|
314
|
-
for (const
|
|
315
|
-
|
|
316
|
-
|
|
477
|
+
for (const tokens of commandSegments(command)) {
|
|
478
|
+
if (tokens.length === 0)
|
|
479
|
+
continue;
|
|
480
|
+
// Redirect targets are collected by a quote-aware scanner. The shared
|
|
481
|
+
// tokenizer supplies the normalized command arguments; it deliberately
|
|
482
|
+
// does not expose shell redirection operators as arguments.
|
|
483
|
+
for (const target of extractRedirectTargets(command))
|
|
484
|
+
add(target);
|
|
485
|
+
// A command may contain multiple segments; only the first pass should
|
|
486
|
+
// attach redirects, otherwise targets would be duplicated harmlessly but
|
|
487
|
+
// needlessly rescanned.
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
for (const tokens of commandSegments(command)) {
|
|
491
|
+
if (tokens.length === 0)
|
|
317
492
|
continue;
|
|
318
|
-
// Redirect targets: the token after `>` / `>>` (optionally prefixed by a
|
|
319
|
-
// file descriptor like `2>` or `&>`, with or without a space).
|
|
320
|
-
for (const m of segment.matchAll(/>>?\s*([^\s>|&]+)/g))
|
|
321
|
-
add(m[1]);
|
|
322
|
-
const tokens = segment.split(/\s+/);
|
|
323
493
|
const verb = path.basename(tokens[0] ?? "");
|
|
324
494
|
const args = tokens.slice(1);
|
|
325
495
|
if (verb === "tee" || verb === "touch") {
|
|
@@ -7,12 +7,15 @@
|
|
|
7
7
|
* Requires: npm install @biomejs/biome (or npx @biomejs/biome)
|
|
8
8
|
* Docs: https://biomejs.dev/
|
|
9
9
|
*/
|
|
10
|
+
import { createSubsystemLogger } from "./extension-log.js";
|
|
10
11
|
import * as fs from "node:fs";
|
|
11
12
|
import * as path from "node:path";
|
|
12
13
|
import { isFileKind } from "./file-kinds.js";
|
|
13
14
|
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
14
15
|
import { findGlobalBinary } from "./package-manager.js";
|
|
15
16
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
17
|
+
import { biomeConfigArgs } from "./tool-policy.js";
|
|
18
|
+
import { resolveManagedToolClient } from "./dispatch/runners/utils/runner-helpers.js";
|
|
16
19
|
// --- Client ---
|
|
17
20
|
export class BiomeClient {
|
|
18
21
|
biomeAvailable = null;
|
|
@@ -29,7 +32,7 @@ export class BiomeClient {
|
|
|
29
32
|
log;
|
|
30
33
|
constructor(verbose = false) {
|
|
31
34
|
this.log = verbose
|
|
32
|
-
? (
|
|
35
|
+
? createSubsystemLogger("biome")
|
|
33
36
|
: () => { };
|
|
34
37
|
}
|
|
35
38
|
/**
|
|
@@ -87,7 +90,7 @@ export class BiomeClient {
|
|
|
87
90
|
}
|
|
88
91
|
async spawnBiomeAsync(args, timeout = 15000, cwd) {
|
|
89
92
|
const { cmd, args: prefix } = await this.getBiomeBinary(cwd);
|
|
90
|
-
return safeSpawnAsync(cmd, [...prefix, ...args], { timeout });
|
|
93
|
+
return safeSpawnAsync(cmd, [...prefix, ...args], { timeout, cwd });
|
|
91
94
|
}
|
|
92
95
|
/**
|
|
93
96
|
* Ensure Biome is available, auto-installing if necessary.
|
|
@@ -112,27 +115,22 @@ export class BiomeClient {
|
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
117
|
async doEnsureAvailable() {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return true;
|
|
132
|
-
}
|
|
133
|
-
this.log("Biome auto-install failed");
|
|
134
|
-
this.biomeAvailable = false;
|
|
135
|
-
return false;
|
|
118
|
+
const outcome = await resolveManagedToolClient({
|
|
119
|
+
toolId: "biome",
|
|
120
|
+
cwd: process.cwd(),
|
|
121
|
+
probe: async () => {
|
|
122
|
+
const result = await this.spawnBiomeAsync(["--version"], 10000);
|
|
123
|
+
return !result.error && result.status === 0
|
|
124
|
+
? { outcome: "success", value: true }
|
|
125
|
+
: { outcome: "missing" };
|
|
126
|
+
},
|
|
127
|
+
acceptInstalled: (installedPath) => {
|
|
128
|
+
this.autoInstalledBinaryPath = installedPath;
|
|
129
|
+
return true;
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
this.biomeAvailable = outcome.outcome === "success";
|
|
133
|
+
return this.biomeAvailable;
|
|
136
134
|
}
|
|
137
135
|
/**
|
|
138
136
|
* Check if a file is supported by Biome
|
|
@@ -142,8 +140,10 @@ export class BiomeClient {
|
|
|
142
140
|
}
|
|
143
141
|
/**
|
|
144
142
|
* Async auto-fix variant for pipeline use (non-blocking spawn).
|
|
143
|
+
* `cwd` is the dispatch language root (used for config discovery); when
|
|
144
|
+
* omitted it defaults to the file's directory.
|
|
145
145
|
*/
|
|
146
|
-
async fixFileAsync(filePath) {
|
|
146
|
+
async fixFileAsync(filePath, cwd) {
|
|
147
147
|
if (!(await this.ensureAvailable())) {
|
|
148
148
|
return {
|
|
149
149
|
success: false,
|
|
@@ -163,7 +163,11 @@ export class BiomeClient {
|
|
|
163
163
|
}
|
|
164
164
|
try {
|
|
165
165
|
const before = await fs.promises.readFile(absolutePath, "utf-8");
|
|
166
|
-
const
|
|
166
|
+
const configCwd = cwd ?? path.dirname(absolutePath);
|
|
167
|
+
// Shared config-args seam (#1247): the lint runner consumes the same
|
|
168
|
+
// builder, so `lint --write` can never drift to biome's default
|
|
169
|
+
// config when a user config or the package fallback exists.
|
|
170
|
+
const result = await this.spawnBiomeAsync(["lint", "--write", ...biomeConfigArgs(configCwd), absolutePath], 15000, configCwd);
|
|
167
171
|
if (result.error) {
|
|
168
172
|
return {
|
|
169
173
|
success: false,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logExtension } from "./extension-log.js";
|
|
1
2
|
let bootstrapPromise = null;
|
|
2
3
|
/**
|
|
3
4
|
* A stand-in for an analysis client whose module failed to load (an unresolved
|
|
@@ -28,11 +29,19 @@ export function degradedClient() {
|
|
|
28
29
|
*/
|
|
29
30
|
async function logBootstrapFailures(failures) {
|
|
30
31
|
for (const { name, err } of failures) {
|
|
31
|
-
|
|
32
|
+
logExtension({
|
|
33
|
+
subsystem: "bootstrap",
|
|
34
|
+
message: `analyzer "${name}" disabled (degraded mode): ${err?.message ?? String(err)}`,
|
|
35
|
+
metadata: { analyzer: name },
|
|
36
|
+
});
|
|
32
37
|
}
|
|
33
38
|
try {
|
|
34
39
|
const { collectInstallDiagnostics, formatInstallDiagnostics } = await import("./install-diagnostics.js");
|
|
35
|
-
|
|
40
|
+
logExtension({
|
|
41
|
+
subsystem: "bootstrap",
|
|
42
|
+
message: formatInstallDiagnostics(collectInstallDiagnostics(), failures[0]?.err),
|
|
43
|
+
metadata: { kind: "install_diagnostics" },
|
|
44
|
+
});
|
|
36
45
|
}
|
|
37
46
|
catch {
|
|
38
47
|
// the per-analyzer lines above already named the failures
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Small insertion-ordered LRU used for process-lifetime memo tables. */
|
|
2
|
+
export class BoundedLruCache {
|
|
3
|
+
maxEntries;
|
|
4
|
+
entries = new Map();
|
|
5
|
+
constructor(maxEntries) {
|
|
6
|
+
this.maxEntries = maxEntries;
|
|
7
|
+
}
|
|
8
|
+
get(key) {
|
|
9
|
+
const value = this.entries.get(key);
|
|
10
|
+
if (value !== undefined) {
|
|
11
|
+
this.entries.delete(key);
|
|
12
|
+
this.entries.set(key, value);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
has(key) { return this.entries.has(key); }
|
|
17
|
+
set(key, value) {
|
|
18
|
+
this.entries.delete(key);
|
|
19
|
+
this.entries.set(key, value);
|
|
20
|
+
while (this.entries.size > this.maxEntries) {
|
|
21
|
+
const oldest = this.entries.keys().next().value;
|
|
22
|
+
if (oldest === undefined)
|
|
23
|
+
break;
|
|
24
|
+
this.entries.delete(oldest);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
delete(key) { return this.entries.delete(key); }
|
|
28
|
+
clear() { this.entries.clear(); }
|
|
29
|
+
get size() { return this.entries.size; }
|
|
30
|
+
keys() { return this.entries.keys(); }
|
|
31
|
+
values() { return this.entries.values(); }
|
|
32
|
+
entriesArray() { return [...this.entries.entries()]; }
|
|
33
|
+
[Symbol.iterator]() { return this.entries[Symbol.iterator](); }
|
|
34
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
const waitArray = new Int32Array(new SharedArrayBuffer(4));
|
|
6
|
+
function ownerPidIsLive(pid) {
|
|
7
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
8
|
+
return false;
|
|
9
|
+
try {
|
|
10
|
+
process.kill(pid, 0);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
return error.code === "EPERM";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function quarantinePath(lockPath, token) {
|
|
18
|
+
return `${lockPath}.quarantine-${process.pid}-${token}`;
|
|
19
|
+
}
|
|
20
|
+
async function restoreQuarantinedLock(lockPath, quarantined) {
|
|
21
|
+
try {
|
|
22
|
+
await fsp.rename(quarantined, lockPath);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// A replacement owner may already hold the canonical name. Never overwrite it.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function releaseQuarantineLock(lockPath, token) {
|
|
29
|
+
const quarantined = quarantinePath(lockPath, `release-${token}`);
|
|
30
|
+
let renamed = false;
|
|
31
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
32
|
+
try {
|
|
33
|
+
await fsp.rename(lockPath, quarantined);
|
|
34
|
+
renamed = true;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error.code !== "ENOENT" ||
|
|
39
|
+
attempt === 2)
|
|
40
|
+
return;
|
|
41
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!renamed)
|
|
45
|
+
return;
|
|
46
|
+
try {
|
|
47
|
+
const owner = JSON.parse(await fsp.readFile(path.join(quarantined, "owner.json"), "utf8"));
|
|
48
|
+
if (owner.token === token) {
|
|
49
|
+
await fsp.rm(quarantined, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
await restoreQuarantinedLock(lockPath, quarantined);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
await restoreQuarantinedLock(lockPath, quarantined);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function quarantineOwnerIsStale(owner, staleMs) {
|
|
60
|
+
return Number.isInteger(owner.pid) && owner.pid > 0 &&
|
|
61
|
+
Number.isFinite(owner.createdAt) &&
|
|
62
|
+
(!ownerPidIsLive(owner.pid) || Date.now() - owner.createdAt > staleMs);
|
|
63
|
+
}
|
|
64
|
+
async function reclaimQuarantineLock(lockPath, staleMs) {
|
|
65
|
+
const quarantined = quarantinePath(lockPath, `reclaim-${Date.now()}-${randomUUID()}`);
|
|
66
|
+
try {
|
|
67
|
+
await fsp.rename(lockPath, quarantined);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
let stale = false;
|
|
73
|
+
try {
|
|
74
|
+
const owner = JSON.parse(await fsp.readFile(path.join(quarantined, "owner.json"), "utf8"));
|
|
75
|
+
stale = quarantineOwnerIsStale(owner, staleMs);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
try {
|
|
79
|
+
stale = Date.now() - (await fsp.stat(quarantined)).mtimeMs > staleMs;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
stale = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (stale) {
|
|
86
|
+
await fsp.rm(quarantined, { recursive: true, force: true });
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
await restoreQuarantinedLock(lockPath, quarantined);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
async function tryAcquireQuarantineLock(lockPath, staleMs) {
|
|
93
|
+
const owner = {
|
|
94
|
+
pid: process.pid,
|
|
95
|
+
createdAt: Date.now(),
|
|
96
|
+
token: `${process.pid}-${Date.now()}-${randomUUID()}`,
|
|
97
|
+
};
|
|
98
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
99
|
+
try {
|
|
100
|
+
await fsp.mkdir(lockPath);
|
|
101
|
+
try {
|
|
102
|
+
await fsp.writeFile(path.join(lockPath, "owner.json"), JSON.stringify(owner), "utf8");
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
await fsp.rm(lockPath, { recursive: true, force: true }).catch(() => { });
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
return () => releaseQuarantineLock(lockPath, owner.token);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (error.code !== "EEXIST")
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
let stale;
|
|
115
|
+
try {
|
|
116
|
+
const existing = JSON.parse(await fsp.readFile(path.join(lockPath, "owner.json"), "utf8"));
|
|
117
|
+
stale = quarantineOwnerIsStale(existing, staleMs);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
try {
|
|
121
|
+
stale = Date.now() - (await fsp.stat(lockPath)).mtimeMs > staleMs;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
stale = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!stale || !(await reclaimQuarantineLock(lockPath, staleMs)))
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
export async function acquireQuarantinePidFileLock(lockPath, options) {
|
|
133
|
+
const deadline = Date.now() + options.waitMs;
|
|
134
|
+
for (;;) {
|
|
135
|
+
const release = await tryAcquireQuarantineLock(lockPath, options.staleMs);
|
|
136
|
+
if (release)
|
|
137
|
+
return release;
|
|
138
|
+
const remaining = deadline - Date.now();
|
|
139
|
+
if (remaining <= 0) {
|
|
140
|
+
if (options.onContention === "skip-log") {
|
|
141
|
+
options.logContention();
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
throw new Error(options.timeoutMessage);
|
|
145
|
+
}
|
|
146
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(options.retryMs, remaining)));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export function acquireBoundedPidFileLock(lockPath, options) {
|
|
150
|
+
const token = `${process.pid}:${Date.now()}:${randomUUID()}`;
|
|
151
|
+
const deadline = Date.now() + options.waitMs;
|
|
152
|
+
for (;;) {
|
|
153
|
+
try {
|
|
154
|
+
const fd = fs.openSync(lockPath, "wx");
|
|
155
|
+
fs.writeFileSync(fd, token, "utf8");
|
|
156
|
+
fs.closeSync(fd);
|
|
157
|
+
return () => {
|
|
158
|
+
try {
|
|
159
|
+
if (fs.readFileSync(lockPath, "utf8") === token) {
|
|
160
|
+
fs.unlinkSync(lockPath);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Protected write completed; cleanup is best-effort.
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (error.code !== "EEXIST")
|
|
170
|
+
throw error;
|
|
171
|
+
try {
|
|
172
|
+
const [pidText] = fs.readFileSync(lockPath, "utf8").split(":", 1);
|
|
173
|
+
if (!ownerPidIsLive(Number.parseInt(pidText ?? "", 10))) {
|
|
174
|
+
fs.unlinkSync(lockPath);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (lockError) {
|
|
179
|
+
if (lockError.code === "ENOENT")
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (Date.now() >= deadline) {
|
|
183
|
+
if (options.onContention === "skip-log") {
|
|
184
|
+
options.logContention();
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
throw new Error(options.timeoutMessage);
|
|
188
|
+
}
|
|
189
|
+
Atomics.wait(waitArray, 0, 0, options.retryMs);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -36,8 +36,12 @@ import * as path from "node:path";
|
|
|
36
36
|
import { isTestMode } from "./env-utils.js";
|
|
37
37
|
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
38
38
|
import { createNdjsonLogger } from "./ndjson-logger.js";
|
|
39
|
+
import { getMaxLogSizeMB } from "./log-cleanup.js";
|
|
39
40
|
const BUS_EVENTS_LOG_FILE = path.join(getGlobalPiLensDir(), "bus-events.log");
|
|
40
|
-
const writer = createNdjsonLogger({
|
|
41
|
+
const writer = createNdjsonLogger({
|
|
42
|
+
filePath: BUS_EVENTS_LOG_FILE,
|
|
43
|
+
maxBytes: getMaxLogSizeMB() * 1024 * 1024,
|
|
44
|
+
});
|
|
41
45
|
export function logBusEvent(entry) {
|
|
42
46
|
if (isTestMode()) {
|
|
43
47
|
return;
|
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
import { logBusEvent } from "./bus-events-logger.js";
|
|
21
21
|
import { normalizeFilePath } from "./path-utils.js";
|
|
22
22
|
import { appendRecentTouches } from "./recent-touches.js";
|
|
23
|
+
import { createLiveBusEmitter, recordStaleBusFailure, } from "./live-bus-emitter.js";
|
|
23
24
|
export const BUS_FILES_TOUCHED_EVENT = "pilens:files:touched";
|
|
24
25
|
export const BUS_FILES_TOUCHED_VERSION = 1;
|
|
25
|
-
|
|
26
|
+
const liveEmitter = createLiveBusEmitter();
|
|
26
27
|
let hasLoggedFailure = false;
|
|
27
28
|
// Log-once-per-process guards for the two structural-no-op outcomes below
|
|
28
29
|
// (see bus-events-logger.ts's module doc for why these aren't logged on
|
|
@@ -38,11 +39,14 @@ let hasLoggedDisabled = false;
|
|
|
38
39
|
* no `pi.events`).
|
|
39
40
|
*/
|
|
40
41
|
export function wireBusEmitter(emitFn) {
|
|
41
|
-
|
|
42
|
+
liveEmitter.wire(emitFn);
|
|
43
|
+
}
|
|
44
|
+
export function wireBusEmitterGetter(getter) {
|
|
45
|
+
liveEmitter.wireGetter(getter);
|
|
42
46
|
}
|
|
43
47
|
/** Test-only: reset module state between test files. */
|
|
44
48
|
export function _resetForTests() {
|
|
45
|
-
|
|
49
|
+
liveEmitter.reset();
|
|
46
50
|
hasLoggedFailure = false;
|
|
47
51
|
hasLoggedUnwired = false;
|
|
48
52
|
hasLoggedDisabled = false;
|
|
@@ -102,6 +106,7 @@ export function publishFilesTouched(args) {
|
|
|
102
106
|
}).catch((err) => {
|
|
103
107
|
args.dbg?.(`bus-publish: recent-touches append failed: ${err}`);
|
|
104
108
|
});
|
|
109
|
+
const busEmit = liveEmitter.get();
|
|
105
110
|
if (!busEmit) {
|
|
106
111
|
if (!hasLoggedUnwired) {
|
|
107
112
|
hasLoggedUnwired = true;
|
|
@@ -129,6 +134,7 @@ export function publishFilesTouched(args) {
|
|
|
129
134
|
}));
|
|
130
135
|
}
|
|
131
136
|
busEmit(BUS_FILES_TOUCHED_EVENT, payload);
|
|
137
|
+
hasLoggedFailure = false;
|
|
132
138
|
logBusEvent({
|
|
133
139
|
event: BUS_FILES_TOUCHED_EVENT,
|
|
134
140
|
outcome: "emitted",
|
|
@@ -147,6 +153,7 @@ export function publishFilesTouched(args) {
|
|
|
147
153
|
});
|
|
148
154
|
if (!hasLoggedFailure) {
|
|
149
155
|
hasLoggedFailure = true;
|
|
156
|
+
recordStaleBusFailure(BUS_FILES_TOUCHED_EVENT, err);
|
|
150
157
|
args.dbg?.(`bus-publish: pilens:files:touched emit failed (further failures suppressed): ${err}`);
|
|
151
158
|
}
|
|
152
159
|
}
|