@herbertgao/pi-extensions 2026.9.1 → 2026.9.2
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/node_modules/@narumitw/pi-btw/README.md +57 -141
- package/node_modules/@narumitw/pi-btw/dist/index.ts +466 -41
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
- package/node_modules/@narumitw/pi-btw/docs/workflows.md +74 -0
- package/node_modules/@narumitw/pi-btw/package.json +2 -1
- package/node_modules/@narumitw/pi-btw/src/btw.ts +15 -1
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +32 -2
- package/node_modules/@narumitw/pi-btw/src/keybindings.ts +330 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +136 -19
- package/node_modules/@narumitw/pi-btw/src/settings.ts +32 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +57 -5
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +52 -30
- package/node_modules/pi-lens/CHANGELOG.md +84 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -7
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +1 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +1 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -7
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +2 -0
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +133 -1
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -6
- package/node_modules/pi-lens/dist/clients/bootstrap.js +4 -4
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +1 -1
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -5
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +1 -9
- package/node_modules/pi-lens/dist/clients/cache-observability.js +1 -6
- package/node_modules/pi-lens/dist/clients/cargo-manifest.js +2 -2
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -7
- package/node_modules/pi-lens/dist/clients/child-unref.js +1 -19
- package/node_modules/pi-lens/dist/clients/config-core/index.js +11 -8
- package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +1 -1
- package/node_modules/pi-lens/dist/clients/config-core/provenance.js +2 -9
- package/node_modules/pi-lens/dist/clients/config-core/records.js +1 -1
- package/node_modules/pi-lens/dist/clients/config-core/schema.js +3 -5
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +1 -1
- package/node_modules/pi-lens/dist/clients/config-resolve.js +4 -4
- package/node_modules/pi-lens/dist/clients/config-schema.js +39 -2
- package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -4
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +1 -1
- package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -4
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -26
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +18 -1
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -6
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -3
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +2 -2
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +3 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +1 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +6 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +4 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +1 -64
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -21
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +2 -20
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +1 -0
- package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +2 -2
- package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -7
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +2 -2
- package/node_modules/pi-lens/dist/clients/file-kinds.js +1 -38
- package/node_modules/pi-lens/dist/clients/file-time.js +1 -1
- package/node_modules/pi-lens/dist/clients/file-utils.js +5 -1
- package/node_modules/pi-lens/dist/clients/format-service.js +0 -1
- package/node_modules/pi-lens/dist/clients/formatters.js +110 -3
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +3 -4
- package/node_modules/pi-lens/dist/clients/git-guard.js +5 -0
- package/node_modules/pi-lens/dist/clients/grammar-source.js +3 -3
- package/node_modules/pi-lens/dist/clients/installer/index.js +220 -9
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +84 -18
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +6 -6
- package/node_modules/pi-lens/dist/clients/language-profile.js +0 -9
- package/node_modules/pi-lens/dist/clients/language-registry.js +5 -1
- package/node_modules/pi-lens/dist/clients/latency-logger.js +5 -7
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -1
- package/node_modules/pi-lens/dist/clients/lens-config.js +19 -0
- package/node_modules/pi-lens/dist/clients/lens-engine.js +3 -4
- package/node_modules/pi-lens/dist/clients/lens-events.js +1 -1
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +18 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +1 -1
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -36
- package/node_modules/pi-lens/dist/clients/lsp/client.js +4 -3
- package/node_modules/pi-lens/dist/clients/lsp/config.js +1 -0
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +3 -3
- package/node_modules/pi-lens/dist/clients/lsp/index.js +216 -25
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -8
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -2
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -1
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +5 -5
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +6 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +1 -1
- package/node_modules/pi-lens/dist/clients/metrics-history.js +28 -107
- package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +1 -1
- package/node_modules/pi-lens/dist/clients/module-report.js +1 -1
- package/node_modules/pi-lens/dist/clients/mutating-tool.js +3 -3
- package/node_modules/pi-lens/dist/clients/mutation-attribution.js +3 -3
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -4
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +9 -9
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +2 -6
- package/node_modules/pi-lens/dist/clients/opengrep-config.js +1 -1
- package/node_modules/pi-lens/dist/clients/path-utils.js +23 -2
- package/node_modules/pi-lens/dist/clients/performance-report.js +2 -2
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +1 -1
- package/node_modules/pi-lens/dist/clients/pipeline.js +2 -2
- package/node_modules/pi-lens/dist/clients/project-changes.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +46 -1
- package/node_modules/pi-lens/dist/clients/project-scan-policy.js +3 -51
- package/node_modules/pi-lens/dist/clients/python-provenance.js +1 -1
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +1 -1
- package/node_modules/pi-lens/dist/clients/read-guard.js +5 -2
- package/node_modules/pi-lens/dist/clients/recent-touches.js +2 -2
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +3 -5
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +1 -1
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +1 -1
- package/node_modules/pi-lens/dist/clients/runtime-session.js +45 -10
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +11 -1
- package/node_modules/pi-lens/dist/clients/sanitize.js +0 -11
- package/node_modules/pi-lens/dist/clients/scan-utils.js +1 -56
- package/node_modules/pi-lens/dist/clients/session-summary.js +0 -24
- package/node_modules/pi-lens/dist/clients/sgconfig.js +1 -5
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +1 -1
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +2 -2
- package/node_modules/pi-lens/dist/clients/startup-scan.js +2 -10
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +77 -72
- package/node_modules/pi-lens/dist/clients/tool-policy.js +1 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +1 -52
- package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -4
- package/node_modules/pi-lens/dist/clients/user-notify.js +0 -4
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -7
- package/node_modules/pi-lens/dist/clients/word-index-store.js +3 -7
- package/node_modules/pi-lens/dist/clients/word-index.js +9 -16
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +1 -1
- package/node_modules/pi-lens/dist/index.js +1062 -538
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +1 -1
- package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +1 -1
- package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +1 -1
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +1 -1
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +37 -6
- package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +3 -3
- package/node_modules/pi-lens/dist/tools/render-compact.js +1 -1
- package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -2
- package/node_modules/pi-lens/docs/globalconfig.md +20 -0
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +9 -1
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +6 -0
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +12 -1
- package/node_modules/pi-lens/docs/release-qa-baseline.md +12 -4
- package/node_modules/pi-lens/docs/servercapabilities.md +7 -3
- package/node_modules/pi-lens/docs/word-index.md +1 -1
- package/node_modules/pi-lens/package.json +6 -15
- package/node_modules/pi-lens/rules/typos/_typos.toml +4 -2
- package/node_modules/pi-lens/scripts/lib/skills-predicate.mjs +1 -1
- package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +1 -1
- package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +4 -2
- package/package.json +3 -3
|
@@ -302,7 +302,7 @@ function matchesGitignorePattern(pattern, relativePath, isDirectory, matchExpand
|
|
|
302
302
|
return candidates.some((value) => matchExpanded(value, expanded));
|
|
303
303
|
});
|
|
304
304
|
}
|
|
305
|
-
|
|
305
|
+
function readGitignorePatterns(rootDir, layer = "gitignore") {
|
|
306
306
|
const gitignorePath = path.join(rootDir, ".gitignore");
|
|
307
307
|
try {
|
|
308
308
|
return parseGitignoreContent(fs.readFileSync(gitignorePath, "utf-8"), layer);
|
|
@@ -762,7 +762,11 @@ export function readGitignoreDirs(rootDir) {
|
|
|
762
762
|
.map((entry) => entry.pattern);
|
|
763
763
|
}
|
|
764
764
|
function globToRegExp(glob) {
|
|
765
|
+
// Directory names use the same `*`-only dialect as read-guard exemptions.
|
|
766
|
+
// Collapse adjacent stars before compiling to avoid nullable-group
|
|
767
|
+
// backtracking on a non-matching name (#2622).
|
|
765
768
|
const escaped = glob
|
|
769
|
+
.replace(/\*+/g, "*")
|
|
766
770
|
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
767
771
|
.replace(/\*/g, ".*")
|
|
768
772
|
.replace(/\?/g, ".");
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
import { logExtension } from "./extension-log.js";
|
|
12
12
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
13
|
import * as fs from "node:fs/promises";
|
|
14
|
+
import * as os from "node:os";
|
|
14
15
|
import * as path from "node:path";
|
|
15
16
|
import { BoundedLruCache } from "./bounded-cache.js";
|
|
16
17
|
import { createGenerationSource } from "./generation-guard.js";
|
|
17
|
-
import { normalizeMapKey } from "./path-utils.js";
|
|
18
|
+
import { findNearestMarkerRoot, isRealGitMarker, normalizeMapKey, } from "./path-utils.js";
|
|
18
19
|
import { resolveCargoPackageEdition } from "./cargo-manifest.js";
|
|
19
20
|
import { resolveKtfmtGradleStyle } from "./gradle-ktfmt-style.js";
|
|
20
21
|
import { resolvePhpCsFixerConfig } from "./php-cs-fixer-config.js";
|
|
@@ -602,6 +603,110 @@ async function indentationArgs(filePath, tool, cwd) {
|
|
|
602
603
|
String(indentation.width),
|
|
603
604
|
];
|
|
604
605
|
}
|
|
606
|
+
const FORMATTER_MARKERS_BY_NAME = new Map([
|
|
607
|
+
["biome", ["biome.json", "biome.jsonc", "package.json"]],
|
|
608
|
+
[
|
|
609
|
+
"prettier",
|
|
610
|
+
[
|
|
611
|
+
".prettierrc",
|
|
612
|
+
".prettierrc.json",
|
|
613
|
+
".prettierrc.yaml",
|
|
614
|
+
".prettierrc.yml",
|
|
615
|
+
".prettierrc.js",
|
|
616
|
+
".prettierrc.cjs",
|
|
617
|
+
".prettierrc.mjs",
|
|
618
|
+
"prettier.config.js",
|
|
619
|
+
"prettier.config.cjs",
|
|
620
|
+
"prettier.config.mjs",
|
|
621
|
+
".prettierignore",
|
|
622
|
+
"package.json",
|
|
623
|
+
],
|
|
624
|
+
],
|
|
625
|
+
[
|
|
626
|
+
"oxfmt",
|
|
627
|
+
["oxfmt.toml", ".oxfmtrc.json", "vite-plus.json", "package.json"],
|
|
628
|
+
],
|
|
629
|
+
["ruff", ["pyproject.toml", "ruff.toml", ".ruff.toml"]],
|
|
630
|
+
["black", ["pyproject.toml", "black.toml", ".black"]],
|
|
631
|
+
["sqlfluff", [".sqlfluff", "pyproject.toml", "setup.cfg"]],
|
|
632
|
+
["rustfmt", ["rustfmt.toml", ".rustfmt.toml", "Cargo.toml"]],
|
|
633
|
+
["rubocop", [".rubocop.yml", ".rubocop.yaml"]],
|
|
634
|
+
["standardrb", [".standard.yml", ".standard.yaml"]],
|
|
635
|
+
["clang-format", [".clang-format", "_clang-format"]],
|
|
636
|
+
["php-cs-fixer", [".php-cs-fixer.php", ".php-cs-fixer.dist.php"]],
|
|
637
|
+
["stylua", ["stylua.toml", ".stylua.toml"]],
|
|
638
|
+
["ocamlformat", [".ocamlformat"]],
|
|
639
|
+
["google-java-format", [".google-java-format", ".editorconfig"]],
|
|
640
|
+
["cljfmt", [".cljfmt.edn", "cljfmt.edn", ".cljfmt"]],
|
|
641
|
+
[
|
|
642
|
+
"cmake-format",
|
|
643
|
+
[
|
|
644
|
+
".cmake-format",
|
|
645
|
+
".cmake-format.yaml",
|
|
646
|
+
".cmake-format.yml",
|
|
647
|
+
".cmake-format.json",
|
|
648
|
+
".cmake-format.py",
|
|
649
|
+
"cmake-format.yaml",
|
|
650
|
+
"cmake-format.yml",
|
|
651
|
+
".editorconfig",
|
|
652
|
+
],
|
|
653
|
+
],
|
|
654
|
+
[
|
|
655
|
+
"psscriptanalyzer-format",
|
|
656
|
+
["PSScriptAnalyzerSettings.psd1", "ScriptAnalyzerSettings.psd1"],
|
|
657
|
+
],
|
|
658
|
+
[
|
|
659
|
+
"csharpier",
|
|
660
|
+
[
|
|
661
|
+
".csharpierrc",
|
|
662
|
+
".csharpierrc.json",
|
|
663
|
+
".csharpierrc.yaml",
|
|
664
|
+
".csharpierrc.yml",
|
|
665
|
+
],
|
|
666
|
+
],
|
|
667
|
+
["ormolu", [".ormolu"]],
|
|
668
|
+
["taplo", ["taplo.toml", ".taplo.toml"]],
|
|
669
|
+
["terraform", [".terraform.lock.hcl"]],
|
|
670
|
+
["swiftformat", [".swiftformat"]],
|
|
671
|
+
["fantomas", [".fantomasignore", ".editorconfig"]],
|
|
672
|
+
["mix", [".formatter.exs"]],
|
|
673
|
+
["shfmt", [".editorconfig"]],
|
|
674
|
+
["ktlint", [".editorconfig"]],
|
|
675
|
+
[
|
|
676
|
+
"ktfmt",
|
|
677
|
+
[
|
|
678
|
+
".editorconfig",
|
|
679
|
+
".ktfmt",
|
|
680
|
+
".ktfmt.kts",
|
|
681
|
+
"build.gradle",
|
|
682
|
+
"build.gradle.kts",
|
|
683
|
+
"settings.gradle",
|
|
684
|
+
"settings.gradle.kts",
|
|
685
|
+
],
|
|
686
|
+
],
|
|
687
|
+
]);
|
|
688
|
+
/**
|
|
689
|
+
* Resolve the cwd for the formatter child, capped at the user's home
|
|
690
|
+
* directory. Files outside a project retain the historical file-directory
|
|
691
|
+
* cwd. The optional homeDir is only for the ceiling regression test.
|
|
692
|
+
*/
|
|
693
|
+
export function resolveFormatterCwd(absolutePath, formatterName, homeDir = os.homedir()) {
|
|
694
|
+
const fileDir = path.dirname(path.resolve(absolutePath));
|
|
695
|
+
const effectiveHome = homeDir || undefined;
|
|
696
|
+
const markers = formatterName
|
|
697
|
+
? [...(FORMATTER_MARKERS_BY_NAME.get(formatterName) ?? []), ".gitignore"]
|
|
698
|
+
: [".gitignore"];
|
|
699
|
+
const root = findNearestMarkerRoot(fileDir, markers, {
|
|
700
|
+
homeDir: effectiveHome,
|
|
701
|
+
});
|
|
702
|
+
if (root)
|
|
703
|
+
return root;
|
|
704
|
+
const gitRoot = findNearestMarkerRoot(fileDir, [".git"], {
|
|
705
|
+
homeDir: effectiveHome,
|
|
706
|
+
markerPredicate: isRealGitMarker,
|
|
707
|
+
});
|
|
708
|
+
return gitRoot ?? fileDir;
|
|
709
|
+
}
|
|
605
710
|
/**
|
|
606
711
|
* Every biome invocation must carry this. Biome exits 1 with "No files were
|
|
607
712
|
* processed in the specified paths" when the path is ignored by the repo's own
|
|
@@ -1438,7 +1543,7 @@ const detectionCache = new BoundedLruCache(32);
|
|
|
1438
1543
|
// The signature is immutable for a cache generation. This memo is separate
|
|
1439
1544
|
// from detectionCache because a cwd can have several extension entries, and a
|
|
1440
1545
|
// warm lookup must not repeat the ancestor walk or stat matched configs.
|
|
1441
|
-
const formatterSignatureFlights = new
|
|
1546
|
+
const formatterSignatureFlights = new BoundedLruCache(32);
|
|
1442
1547
|
const formatterCacheGeneration = createGenerationSource("formatter-cache");
|
|
1443
1548
|
// These are the formatter configuration files consulted by the policy helpers
|
|
1444
1549
|
// above. Their metadata is captured by the cold detection signature. The
|
|
@@ -1810,6 +1915,7 @@ export function _getFormatterResetStateForTests() {
|
|
|
1810
1915
|
whichTransientCommands,
|
|
1811
1916
|
cooldownRecordedForRetryAtMs,
|
|
1812
1917
|
detectionCache,
|
|
1918
|
+
formatterSignatureFlights,
|
|
1813
1919
|
};
|
|
1814
1920
|
}
|
|
1815
1921
|
export function clearFormatterRuntimeState() {
|
|
@@ -1887,6 +1993,7 @@ export async function formatFile(filePath, formatter) {
|
|
|
1887
1993
|
try {
|
|
1888
1994
|
const absolutePath = path.resolve(filePath);
|
|
1889
1995
|
const cwd = path.dirname(absolutePath);
|
|
1996
|
+
const formatterCwd = resolveFormatterCwd(absolutePath, formatter.name);
|
|
1890
1997
|
const contentBefore = await fs.readFile(absolutePath, "utf-8");
|
|
1891
1998
|
// Resolve command: prefer local (venv/vendor/node_modules) over global.
|
|
1892
1999
|
// The shared seam must honor SKIP_FORMATTING before selecting the static
|
|
@@ -1912,7 +2019,7 @@ export async function formatFile(filePath, formatter) {
|
|
|
1912
2019
|
// Run formatter without blocking the event loop.
|
|
1913
2020
|
const result = await safeSpawnAsync(cmd[0], cmd.slice(1), {
|
|
1914
2021
|
timeout: 15000,
|
|
1915
|
-
cwd,
|
|
2022
|
+
cwd: formatterCwd,
|
|
1916
2023
|
});
|
|
1917
2024
|
// A resolver that could NOT prove absence (it never probed PATH — e.g.
|
|
1918
2025
|
// black/sqlfluff, which only look in a venv) legitimately falls back to the
|
|
@@ -205,7 +205,7 @@ function hasGeneratedArtifactContent(content) {
|
|
|
205
205
|
* file is not machine-emitted evidence), and an empty or whitespace-only
|
|
206
206
|
* file yields a zero mean and stays source.
|
|
207
207
|
*/
|
|
208
|
-
|
|
208
|
+
const MACHINE_EMITTED_LINE_SHAPE_MIN_CONTENT = 2048;
|
|
209
209
|
export const MACHINE_EMITTED_LINE_SHAPE_MEAN_THRESHOLD = 2500;
|
|
210
210
|
export function classifyMachineEmittedLineShape(content) {
|
|
211
211
|
const contentLength = content.length;
|
|
@@ -325,8 +325,7 @@ function analyzeFileHeader(filePath, maxBytes) {
|
|
|
325
325
|
* function and short-circuit on a hit, so by the time this function runs for
|
|
326
326
|
* those callers evidence (a) has already come back negative. Non-walk
|
|
327
327
|
* callers with no upstream sibling probe (`file-role.ts`'s content-based
|
|
328
|
-
* check,
|
|
329
|
-
* evaluate evidence (b) only — a documented, narrower guarantee than the
|
|
328
|
+
* check) evaluate evidence (b) only — a documented, narrower guarantee than the
|
|
330
329
|
* walk path's.
|
|
331
330
|
*
|
|
332
331
|
* Tradeoff (documented per the issue): when NEITHER piece of evidence
|
|
@@ -387,7 +386,7 @@ export function classifyGeneratedOrArtifactDetailed(filePath, options = {}) {
|
|
|
387
386
|
: { verdict: "clean" };
|
|
388
387
|
}
|
|
389
388
|
/** Verdict-only convenience wrapper over {@link classifyGeneratedOrArtifactDetailed}. */
|
|
390
|
-
|
|
389
|
+
function classifyGeneratedOrArtifact(filePath, options = {}) {
|
|
391
390
|
return classifyGeneratedOrArtifactDetailed(filePath, options).verdict;
|
|
392
391
|
}
|
|
393
392
|
export function isGeneratedOrArtifact(filePath, options = {}) {
|
|
@@ -141,6 +141,11 @@ function canonicalizeGuardCommand(command) {
|
|
|
141
141
|
let pendingSpace = false;
|
|
142
142
|
quote = undefined;
|
|
143
143
|
for (const ch of result) {
|
|
144
|
+
if (!quote && ch === "\n") {
|
|
145
|
+
collapsed += ch;
|
|
146
|
+
pendingSpace = false;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
144
149
|
if (!quote && /\s/.test(ch)) {
|
|
145
150
|
pendingSpace = collapsed.length > 0;
|
|
146
151
|
continue;
|
|
@@ -18,7 +18,7 @@ import { getPackageRoot } from "./package-root.js";
|
|
|
18
18
|
/** tree-sitter-wasms release the grammars are pulled from. */
|
|
19
19
|
export const TREE_SITTER_WASMS_VERSION = "0.1.13";
|
|
20
20
|
/** unpkg mirror of the tree-sitter-wasms artifacts. */
|
|
21
|
-
|
|
21
|
+
const GRAMMAR_CDN_BASE = `https://unpkg.com/tree-sitter-wasms@${TREE_SITTER_WASMS_VERSION}/out`;
|
|
22
22
|
export const GRAMMAR_SOURCE_OVERRIDES = {
|
|
23
23
|
"tree-sitter-lua.wasm": {
|
|
24
24
|
package: "@tree-sitter-grammars/tree-sitter-lua",
|
|
@@ -145,7 +145,7 @@ export const BLOCKED_GRAMMARS = {
|
|
|
145
145
|
},
|
|
146
146
|
};
|
|
147
147
|
/** Runtime signals for the currently-running process. */
|
|
148
|
-
|
|
148
|
+
function currentGrammarRuntime() {
|
|
149
149
|
const m = /^v?(\d+)/.exec(process.versions?.node ?? "");
|
|
150
150
|
return {
|
|
151
151
|
nodeMajor: m ? Number(m[1]) : 0,
|
|
@@ -173,7 +173,7 @@ export function grammarBlockReason(filename, rt = currentGrammarRuntime()) {
|
|
|
173
173
|
* both the download path (validate before writing) and the on-disk path
|
|
174
174
|
* (`fileHasWasmMagic`, for a file poisoned before this shipped) key off it.
|
|
175
175
|
*/
|
|
176
|
-
|
|
176
|
+
const WASM_MAGIC = [0x00, 0x61, 0x73, 0x6d];
|
|
177
177
|
/** Do `bytes` start with the wasm magic number? */
|
|
178
178
|
export function hasWasmMagic(bytes) {
|
|
179
179
|
if (bytes.length < WASM_MAGIC.length)
|
|
@@ -548,10 +548,27 @@ export const TOOLS = [
|
|
|
548
548
|
binaryName: "docker-langserver",
|
|
549
549
|
},
|
|
550
550
|
{
|
|
551
|
+
// #2722: intelephense has no CLI — its entry calls createConnection()
|
|
552
|
+
// unconditionally — and Node prints the offending source line before the
|
|
553
|
+
// error. The bundle is one ~4 MB minified line, so the transport-required
|
|
554
|
+
// marker #208 rescues on lands at byte 4,154,741 of a 4,423,356-byte
|
|
555
|
+
// stderr while Node truncates piped stderr at 1 MiB on exit. Measured on
|
|
556
|
+
// intelephense@1.18.5, linux, Node v22.22.1:
|
|
557
|
+
// $ intelephense --version 2>&1 | wc -c -> 1048576
|
|
558
|
+
// $ intelephense --version 2>&1 | grep -c "Connection input stream is not set" -> 0
|
|
559
|
+
// $ intelephense --version 2>err.txt; wc -c < err.txt -> 4423356
|
|
560
|
+
// Same run over the alternatives, each with stdin closed the way
|
|
561
|
+
// verifyToolBinary closes it (`input: ""`):
|
|
562
|
+
// --help, -v, --socket=0 -> identical dump (exit 1, 4423356 bytes,
|
|
563
|
+
// marker at 4154741, 1048576 through a pipe, marker absent)
|
|
564
|
+
// --stdio -> exit 1 with ZERO bytes on either stream,
|
|
565
|
+
// so it carries no marker to rescue on either.
|
|
566
|
+
// No checkArgs value produces a verdict. Verified spawn-free instead.
|
|
551
567
|
id: "intelephense",
|
|
552
568
|
name: "Intelephense",
|
|
553
569
|
checkCommand: "intelephense",
|
|
554
570
|
checkArgs: ["--version"],
|
|
571
|
+
verification: "package-entry",
|
|
555
572
|
installStrategy: "npm",
|
|
556
573
|
packageName: "intelephense",
|
|
557
574
|
binaryName: "intelephense",
|
|
@@ -1851,6 +1868,111 @@ function extractVersionToken(output) {
|
|
|
1851
1868
|
* maven/archive) never populate or read this map.
|
|
1852
1869
|
*/
|
|
1853
1870
|
const lastManagedInstallVersion = new Map();
|
|
1871
|
+
/**
|
|
1872
|
+
* The package whose entry module verifies `tool`'s managed install spawn-free,
|
|
1873
|
+
* or undefined when the tool is verified by spawning `checkArgs` (#2722).
|
|
1874
|
+
* Registry-declared per tool — never inferred from the strategy: a spawn probe
|
|
1875
|
+
* is the stronger check everywhere it can actually return a verdict.
|
|
1876
|
+
*/
|
|
1877
|
+
export function packageEntryVerification(tool) {
|
|
1878
|
+
return tool.verification === "package-entry" ? tool.packageName : undefined;
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Spawn-free verification of a managed npm install (#2722).
|
|
1882
|
+
*
|
|
1883
|
+
* For an stdio LSP server with no CLI surface, every `checkArgs` value is a
|
|
1884
|
+
* throw, and #208's transport-required rescue is output-ORDER dependent: Node
|
|
1885
|
+
* prints the offending source line ahead of the error, so a server whose bundle
|
|
1886
|
+
* is megabytes of minified JS pushes the marker past both our retained window
|
|
1887
|
+
* and Node's own 1 MiB pipe truncation. The bytes never leave the child, so no
|
|
1888
|
+
* spawn can answer "is this install intact".
|
|
1889
|
+
*
|
|
1890
|
+
* The evidence used instead is the same class the `archive` strategy already
|
|
1891
|
+
* accepts through `treeMarker`: the installed tree is inspected on disk. The
|
|
1892
|
+
* install verifies when the package directory beside the `.bin` shim carries a
|
|
1893
|
+
* readable `package.json` with a `version`, and the entry module that
|
|
1894
|
+
* package.json itself names — `bin[<shim name>]` or a bare `bin` string —
|
|
1895
|
+
* exists as a non-empty file. `main` is deliberately NOT a fallback: npm
|
|
1896
|
+
* creates the `node_modules/.bin/<shim>` this function is given only from a
|
|
1897
|
+
* `bin` field, so a manifest without one cannot be the manifest behind this
|
|
1898
|
+
* shim. That is exactly what `verifyToolBinary` was documented to catch here —
|
|
1899
|
+
* broken symlinks and partial installs — and it is strictly more than the
|
|
1900
|
+
* `--version` spawn can report for this class.
|
|
1901
|
+
*/
|
|
1902
|
+
export async function verifyNpmPackageEntry(binPath, packageName) {
|
|
1903
|
+
// `<...>/node_modules/.bin/<shim>` → `<...>/node_modules`.
|
|
1904
|
+
const nodeModulesDir = path.dirname(path.dirname(binPath));
|
|
1905
|
+
const pinned = parsePinnedVersion(packageName);
|
|
1906
|
+
const bareName = pinned
|
|
1907
|
+
? packageName.slice(0, -(pinned.length + 1))
|
|
1908
|
+
: packageName;
|
|
1909
|
+
const packageDir = path.join(nodeModulesDir, ...bareName.split("/"));
|
|
1910
|
+
const shimName = path
|
|
1911
|
+
.basename(binPath)
|
|
1912
|
+
.replace(/\.(cmd|exe|ps1|bat)$/i, "")
|
|
1913
|
+
.toLowerCase();
|
|
1914
|
+
const fail = (reason) => {
|
|
1915
|
+
logSessionStart(`auto-install verify: failed for ${binPath} (check=package-entry, kind=${reason})`);
|
|
1916
|
+
return false;
|
|
1917
|
+
};
|
|
1918
|
+
// R2-F1: the shim ITSELF, before anything else. Every path below is DERIVED
|
|
1919
|
+
// from `binPath`, so without this a partial install — npm never wrote the
|
|
1920
|
+
// `.bin` entry, or wrote one that is empty, a directory, or a symlink
|
|
1921
|
+
// dangling at a deleted target — verified `true` on the strength of a
|
|
1922
|
+
// package tree that nothing can execute, and `installNpmTool` then recorded
|
|
1923
|
+
// the install as SUCCEEDED. `classifyInstallOutcome` grades any non-"failed"
|
|
1924
|
+
// outcome as `⚠ unavailable (succeeded)`, so that answer re-hid exactly the
|
|
1925
|
+
// nightly row #2722 exists to expose. `statSync` FOLLOWS the link, which is
|
|
1926
|
+
// what makes missing and dangling one branch.
|
|
1927
|
+
try {
|
|
1928
|
+
const shim = statSync(binPath);
|
|
1929
|
+
if (!shim.isFile() || shim.size === 0)
|
|
1930
|
+
return fail("shim-not-a-file");
|
|
1931
|
+
}
|
|
1932
|
+
catch {
|
|
1933
|
+
return fail("shim-missing");
|
|
1934
|
+
}
|
|
1935
|
+
let manifest;
|
|
1936
|
+
try {
|
|
1937
|
+
manifest = JSON.parse(await fs.readFile(path.join(packageDir, "package.json"), "utf8"));
|
|
1938
|
+
}
|
|
1939
|
+
catch {
|
|
1940
|
+
return fail("package-json-unreadable");
|
|
1941
|
+
}
|
|
1942
|
+
if (typeof manifest.version !== "string" || !manifest.version) {
|
|
1943
|
+
return fail("package-json-no-version");
|
|
1944
|
+
}
|
|
1945
|
+
const bin = manifest.bin;
|
|
1946
|
+
const entry = typeof bin === "string"
|
|
1947
|
+
? bin
|
|
1948
|
+
: bin && typeof bin === "object"
|
|
1949
|
+
? Object.entries(bin).find(
|
|
1950
|
+
// Case-folded on purpose: on a case-insensitive filesystem the
|
|
1951
|
+
// shim on disk can differ in case from the manifest key npm
|
|
1952
|
+
// wrote it from, and `path.basename` reads the disk name.
|
|
1953
|
+
([name]) => name.toLowerCase() === shimName)?.[1]
|
|
1954
|
+
: undefined;
|
|
1955
|
+
if (typeof entry !== "string" || !entry) {
|
|
1956
|
+
return fail("package-json-no-entry");
|
|
1957
|
+
}
|
|
1958
|
+
try {
|
|
1959
|
+
const stat = statSync(path.join(packageDir, entry));
|
|
1960
|
+
if (!stat.isFile() || stat.size === 0)
|
|
1961
|
+
return fail("entry-not-a-file");
|
|
1962
|
+
}
|
|
1963
|
+
catch {
|
|
1964
|
+
return fail("entry-missing");
|
|
1965
|
+
}
|
|
1966
|
+
// R2-F3 (catalog shape 31): "did the new verifier run at all, and on what"
|
|
1967
|
+
// has to be answerable from sessionstart.log, not only from a debug build —
|
|
1968
|
+
// the failure branches above already log there. Measured volume on a real
|
|
1969
|
+
// intelephense install, scratch PI_LENS_HOME: TWO rows on the session that
|
|
1970
|
+
// installs (installNpmTool's verify, then the post-install resolution), and
|
|
1971
|
+
// ZERO on every later session — the probe cache answers before any verify
|
|
1972
|
+
// runs (`auto-install ensure intelephense: probe cache hit`).
|
|
1973
|
+
logSessionStart(`auto-install verify: succeeded for ${binPath} (check=package-entry, version=${manifest.version}, entry=${entry})`);
|
|
1974
|
+
return true;
|
|
1975
|
+
}
|
|
1854
1976
|
/**
|
|
1855
1977
|
* Verify a tool binary actually works by running its configured checkArgs.
|
|
1856
1978
|
* This catches broken symlinks, partial installs, and corrupted binaries.
|
|
@@ -1878,7 +2000,26 @@ onTransient,
|
|
|
1878
2000
|
* sensitive caller on the dispatch hot path passes a shorter one and treats
|
|
1879
2001
|
* the expiry as transient rather than as a verdict (#1657).
|
|
1880
2002
|
*/
|
|
1881
|
-
timeoutMs = 10000, verificationArgs = ["--version"]
|
|
2003
|
+
timeoutMs = 10000, verificationArgs = ["--version"],
|
|
2004
|
+
/**
|
|
2005
|
+
* Package name from {@link packageEntryVerification} — present only for a
|
|
2006
|
+
* registry entry declaring `verification: "package-entry"`. When present the
|
|
2007
|
+
* spawn is skipped entirely and {@link verifyNpmPackageEntry} answers (#2722).
|
|
2008
|
+
*/
|
|
2009
|
+
packageEntryOf,
|
|
2010
|
+
/**
|
|
2011
|
+
* Called when a `false` verdict is INCONCLUSIVE rather than a verdict: the
|
|
2012
|
+
* transport-required matcher was armed, never matched, and the child's
|
|
2013
|
+
* output was cut off before the tail arrived, so the #208 rescue could not
|
|
2014
|
+
* be evaluated at all (#2722). Distinct from `onTransient` on purpose — the
|
|
2015
|
+
* prober DID run to completion and re-probing reproduces it exactly, so this
|
|
2016
|
+
* is a durable "cannot be verified", not a stall (catalog shape 13). A
|
|
2017
|
+
* caller must not treat it as "broken" and delete the installation.
|
|
2018
|
+
*/
|
|
2019
|
+
onInconclusive) {
|
|
2020
|
+
if (packageEntryOf !== undefined) {
|
|
2021
|
+
return verifyNpmPackageEntry(binPath, packageEntryOf);
|
|
2022
|
+
}
|
|
1882
2023
|
// #2015: safeSpawnAsync instead of raw spawn. Raw spawn's timeout
|
|
1883
2024
|
// SIGTERMed only cmd.exe on Windows (.cmd shims run shell:true), orphaning
|
|
1884
2025
|
// the grandchild node process - which kept scanning, held handles against
|
|
@@ -1937,6 +2078,31 @@ timeoutMs = 10000, verificationArgs = ["--version"]) {
|
|
|
1937
2078
|
// verdict from the binary (#1569 transient semantics).
|
|
1938
2079
|
if (result.signal !== undefined || result.spawnFailure)
|
|
1939
2080
|
onTransient?.();
|
|
2081
|
+
if (result.outputTruncated &&
|
|
2082
|
+
result.signal === undefined &&
|
|
2083
|
+
!result.spawnFailure) {
|
|
2084
|
+
// #2722: the transport-required matcher above is armed on EVERY probe,
|
|
2085
|
+
// and it never matched — but the output we kept is a prefix, so the
|
|
2086
|
+
// marker may simply sit past it (intelephense emits ~4 MB of bundle
|
|
2087
|
+
// ahead of its own diagnostic). "Unmatched within a truncated prefix"
|
|
2088
|
+
// is not "this binary is broken"; callers must not delete the install
|
|
2089
|
+
// on it. Recorded as its own kind so a monitor can tell an unreadable
|
|
2090
|
+
// probe apart from a rejected binary.
|
|
2091
|
+
//
|
|
2092
|
+
// R2-F4: gated on the probe having actually FINISHED. A verbose child
|
|
2093
|
+
// that is SIGTERMed at the timeout also arrives here truncated, and
|
|
2094
|
+
// that is the #1569 transient class one line above, not this one —
|
|
2095
|
+
// ungated, the two overlapped and `installNpmTool` (which tests
|
|
2096
|
+
// inconclusive first) replaced the #2015 transient message with this
|
|
2097
|
+
// one. The doc comment's "the prober DID run to completion" is now
|
|
2098
|
+
// true rather than aspirational.
|
|
2099
|
+
onInconclusive?.();
|
|
2100
|
+
recordDegradationOnce({
|
|
2101
|
+
kind: "installer-verification-inconclusive",
|
|
2102
|
+
subject: binPath,
|
|
2103
|
+
reason: `transport-required marker unresolved in truncated output (${verificationArgs.join(" ")})`,
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
1940
2106
|
const kind = result.spawnFailure?.kind ??
|
|
1941
2107
|
(result.signal
|
|
1942
2108
|
? `killed-signal=${result.signal}`
|
|
@@ -2056,7 +2222,7 @@ export async function getAllToolStatuses() {
|
|
|
2056
2222
|
const localPath = installerPlatform() === "win32" ? `${localBase}.cmd` : localBase;
|
|
2057
2223
|
try {
|
|
2058
2224
|
await fs.access(localPath);
|
|
2059
|
-
if (await verifyToolBinary(localPath, undefined, undefined, getToolVerificationTimeout(tool), tool.checkArgs)) {
|
|
2225
|
+
if (await verifyToolBinary(localPath, undefined, undefined, getToolVerificationTimeout(tool), tool.checkArgs, packageEntryVerification(tool))) {
|
|
2060
2226
|
status.installed = true;
|
|
2061
2227
|
status.source = "pi-lens-auto";
|
|
2062
2228
|
status.path = localPath;
|
|
@@ -2225,7 +2391,7 @@ async function getToolPathResolved(toolId, onTransient) {
|
|
|
2225
2391
|
const cmdPath = `${localBase}.cmd`;
|
|
2226
2392
|
try {
|
|
2227
2393
|
await fs.access(cmdPath);
|
|
2228
|
-
if (await verifyToolBinary(cmdPath, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs)) {
|
|
2394
|
+
if (await verifyToolBinary(cmdPath, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs, packageEntryVerification(tool))) {
|
|
2229
2395
|
return cmdPath;
|
|
2230
2396
|
}
|
|
2231
2397
|
logSessionStart(`auto-install verify: ${cmdPath} exists but is broken, will reinstall`);
|
|
@@ -2238,7 +2404,7 @@ async function getToolPathResolved(toolId, onTransient) {
|
|
|
2238
2404
|
const exePath = `${localBase}.exe`;
|
|
2239
2405
|
try {
|
|
2240
2406
|
await fs.access(exePath);
|
|
2241
|
-
if (await verifyToolBinary(exePath, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs)) {
|
|
2407
|
+
if (await verifyToolBinary(exePath, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs, packageEntryVerification(tool))) {
|
|
2242
2408
|
return exePath;
|
|
2243
2409
|
}
|
|
2244
2410
|
logSessionStart(`auto-install verify: ${exePath} exists but is broken, will reinstall`);
|
|
@@ -2250,7 +2416,7 @@ async function getToolPathResolved(toolId, onTransient) {
|
|
|
2250
2416
|
if (installerPlatform() !== "win32") {
|
|
2251
2417
|
try {
|
|
2252
2418
|
await fs.access(localBase);
|
|
2253
|
-
if (await verifyToolBinary(localBase, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs)) {
|
|
2419
|
+
if (await verifyToolBinary(localBase, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs, packageEntryVerification(tool))) {
|
|
2254
2420
|
return localBase;
|
|
2255
2421
|
}
|
|
2256
2422
|
logSessionStart(`auto-install verify: ${localBase} exists but is broken, will reinstall`);
|
|
@@ -2873,6 +3039,7 @@ export function npmToolNeedsPostinstall(packageName) {
|
|
|
2873
3039
|
* that wanders off it.
|
|
2874
3040
|
*/
|
|
2875
3041
|
export function getRefreshableManagedNpmTools() {
|
|
3042
|
+
const seenPackages = new Set();
|
|
2876
3043
|
return getRefreshableManagedTools()
|
|
2877
3044
|
.filter((candidate) => candidate.strategy === "npm")
|
|
2878
3045
|
.map((candidate) => ({
|
|
@@ -2881,7 +3048,13 @@ export function getRefreshableManagedNpmTools() {
|
|
|
2881
3048
|
// have both, so these are total.
|
|
2882
3049
|
packageName: candidate.packageName,
|
|
2883
3050
|
binaryName: candidate.binaryName,
|
|
2884
|
-
}))
|
|
3051
|
+
}))
|
|
3052
|
+
.filter((candidate) => {
|
|
3053
|
+
if (seenPackages.has(candidate.packageName))
|
|
3054
|
+
return false;
|
|
3055
|
+
seenPackages.add(candidate.packageName);
|
|
3056
|
+
return true;
|
|
3057
|
+
});
|
|
2885
3058
|
}
|
|
2886
3059
|
/** The Maven GAV a `maven` entry pins, as a stable identity string. */
|
|
2887
3060
|
function mavenCoordinate(spec) {
|
|
@@ -2928,6 +3101,7 @@ export function getRefreshableManagedTools() {
|
|
|
2928
3101
|
packageName: tool.packageName,
|
|
2929
3102
|
binaryName: tool.binaryName,
|
|
2930
3103
|
verificationTimeoutMs: tool.verificationTimeoutMs,
|
|
3104
|
+
packageEntryOf: packageEntryVerification(tool),
|
|
2931
3105
|
});
|
|
2932
3106
|
break;
|
|
2933
3107
|
}
|
|
@@ -3757,7 +3931,9 @@ function recordPackageManagerInstallException(toolId, strategyLabel, packageName
|
|
|
3757
3931
|
installFailureReasons.set(toolId, message);
|
|
3758
3932
|
return undefined;
|
|
3759
3933
|
}
|
|
3760
|
-
async function installNpmTool(toolId, packageName, binaryName, verificationArgs = ["--version"], verificationTimeoutMs = 10_000
|
|
3934
|
+
async function installNpmTool(toolId, packageName, binaryName, verificationArgs = ["--version"], verificationTimeoutMs = 10_000,
|
|
3935
|
+
/** See {@link packageEntryVerification} — spawn-free verification (#2722). */
|
|
3936
|
+
packageEntryOf) {
|
|
3761
3937
|
try {
|
|
3762
3938
|
// Ensure tools directory exists
|
|
3763
3939
|
await fs.mkdir(TOOLS_DIR, { recursive: true });
|
|
@@ -3837,11 +4013,15 @@ async function installNpmTool(toolId, packageName, binaryName, verificationArgs
|
|
|
3837
4013
|
debugLog(`Verifying ${binaryName}...`);
|
|
3838
4014
|
let isValid = false;
|
|
3839
4015
|
let lastAttemptTransient = false;
|
|
4016
|
+
let lastAttemptInconclusive = false;
|
|
3840
4017
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
3841
4018
|
lastAttemptTransient = false;
|
|
4019
|
+
lastAttemptInconclusive = false;
|
|
3842
4020
|
isValid = await verifyToolBinary(binPath, undefined, () => {
|
|
3843
4021
|
lastAttemptTransient = true;
|
|
3844
|
-
}, verificationTimeoutMs, verificationArgs)
|
|
4022
|
+
}, verificationTimeoutMs, verificationArgs, packageEntryOf, () => {
|
|
4023
|
+
lastAttemptInconclusive = true;
|
|
4024
|
+
});
|
|
3845
4025
|
if (isValid)
|
|
3846
4026
|
break;
|
|
3847
4027
|
if (attempt < 3) {
|
|
@@ -3849,6 +4029,37 @@ async function installNpmTool(toolId, packageName, binaryName, verificationArgs
|
|
|
3849
4029
|
await new Promise((r) => setTimeout(r, 1000 * attempt));
|
|
3850
4030
|
}
|
|
3851
4031
|
}
|
|
4032
|
+
if (!isValid && lastAttemptInconclusive) {
|
|
4033
|
+
// #2722: the probe ran to completion but could not decide — the #208
|
|
4034
|
+
// transport-required matcher was armed, never matched, and the kept
|
|
4035
|
+
// output is a truncated prefix, so the marker may sit past it (the
|
|
4036
|
+
// class intelephense fell into: ~4 MB of bundle ahead of the marker,
|
|
4037
|
+
// unreachable through any pipe). Same rule as the transient branch
|
|
4038
|
+
// below (#2015): a non-verdict must not delete a freshly installed
|
|
4039
|
+
// package. Deliberately NOT folded into `lastAttemptTransient` —
|
|
4040
|
+
// re-probing reproduces this exactly, so it is durable, and a
|
|
4041
|
+
// transient verdict would keep re-arming the reinstall path
|
|
4042
|
+
// (catalog shape 13).
|
|
4043
|
+
// R2-F2: a non-verdict is a reason to keep ONLY while the tree on disk
|
|
4044
|
+
// still looks like a complete install. With intelephense verified
|
|
4045
|
+
// spawn-free, no registry entry reaches this branch healthy (measured:
|
|
4046
|
+
// no other npm-strategy LSP server emits more than 1,527 bytes), so
|
|
4047
|
+
// its live population is BROKEN servers that spew past the retained
|
|
4048
|
+
// window and die — and for those the delete was the only repair that
|
|
4049
|
+
// existed. Measured on npm 9.2.0 against a real managed prefix: a
|
|
4050
|
+
// package file corrupted IN PLACE is not repaired by re-installing
|
|
4051
|
+
// (`up to date`, the zeroed file stays zeroed), while a package
|
|
4052
|
+
// directory, a `.bin` shim or a nested dependency that is GONE is
|
|
4053
|
+
// reinstalled. So the same on-disk evidence `verification:
|
|
4054
|
+
// "package-entry"` uses decides keep-vs-delete here — strictly as a
|
|
4055
|
+
// gate, never as a verdict: the install is still recorded as failed,
|
|
4056
|
+
// so the tool still reads `✗` and nothing is re-hidden.
|
|
4057
|
+
if (await verifyNpmPackageEntry(binPath, packageName)) {
|
|
4058
|
+
logSessionStart(`auto-install ${packageName}: verification inconclusive (output truncated before the transport-required marker) but the installed tree is intact; keeping installation for re-probe`);
|
|
4059
|
+
return undefined;
|
|
4060
|
+
}
|
|
4061
|
+
logSessionStart(`auto-install ${packageName}: verification inconclusive AND the installed tree is incomplete; cleaning up so the next install can repair it`);
|
|
4062
|
+
}
|
|
3852
4063
|
if (!isValid && lastAttemptTransient) {
|
|
3853
4064
|
// #2015: a killed/spawn-failed prober is NOT a verdict about the
|
|
3854
4065
|
// binary (#1569 semantics). Cold-start verifies on fresh installs
|
|
@@ -4132,7 +4343,7 @@ export async function installTool(toolId) {
|
|
|
4132
4343
|
case "npm": {
|
|
4133
4344
|
if (!tool.packageName || !tool.binaryName)
|
|
4134
4345
|
return false;
|
|
4135
|
-
const npmPath = await installNpmTool(tool.id, tool.packageName, tool.binaryName, tool.checkArgs, getToolVerificationTimeout(tool));
|
|
4346
|
+
const npmPath = await installNpmTool(tool.id, tool.packageName, tool.binaryName, tool.checkArgs, getToolVerificationTimeout(tool), packageEntryVerification(tool));
|
|
4136
4347
|
if (npmPath !== undefined) {
|
|
4137
4348
|
// #1746 review F4: an install just resolved this package's range
|
|
4138
4349
|
// against the registry, so record it as freshly checked. Otherwise a
|