pi-lens 3.8.72 → 3.8.74
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 +1162 -0
- package/README.md +45 -28
- package/dist/clients/atomic-write.js +3 -3
- package/dist/clients/bus-events-logger.js +5 -1
- package/dist/clients/cache/rule-cache.js +43 -2
- package/dist/clients/cache-observability.js +211 -0
- package/dist/clients/call-graph.js +20 -0
- package/dist/clients/cascade-logger.js +5 -1
- package/dist/clients/complexity-client.js +4 -7
- package/dist/clients/dependency-checker.js +151 -20
- package/dist/clients/deps/js-yaml.js +1 -1
- package/dist/clients/diagnostic-dispositions.js +22 -2
- package/dist/clients/dispatch/auxiliary-lsp.js +34 -0
- package/dist/clients/dispatch/dispatcher.js +16 -1
- package/dist/clients/dispatch/fact-store.js +11 -0
- package/dist/clients/dispatch/facts/function-facts.js +14 -0
- package/dist/clients/dispatch/facts/import-facts.js +100 -101
- package/dist/clients/dispatch/facts/tree-sitter-facts.js +5 -8
- package/dist/clients/dispatch/facts/try-catch-facts.js +13 -0
- package/dist/clients/dispatch/integration.js +94 -15
- 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 +84 -8
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -1
- package/dist/clients/dispatch/runners/tree-sitter.js +153 -166
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +17 -18
- package/dist/clients/feature-hints.js +53 -7
- package/dist/clients/file-kinds.js +86 -31
- package/dist/clients/fix-worklog.js +14 -2
- package/dist/clients/generated-artifacts.js +21 -0
- package/dist/clients/gzip-stage-write.js +84 -0
- package/dist/clients/installer/index.js +358 -129
- package/dist/clients/instance-registry.js +12 -1
- package/dist/clients/knip-client.js +64 -1
- package/dist/clients/language-profile.js +61 -72
- package/dist/clients/latency-logger.js +6 -16
- package/dist/clients/lens-config.js +128 -165
- package/dist/clients/lens-engine.js +39 -5
- package/dist/clients/lens-flag-registry.js +285 -0
- package/dist/clients/lens-map.js +2 -7
- package/dist/clients/log-cleanup.js +6 -17
- package/dist/clients/lsp/cascade-tier.js +22 -2
- package/dist/clients/lsp/client.js +74 -10
- package/dist/clients/lsp/config.js +62 -9
- package/dist/clients/lsp/index.js +113 -31
- package/dist/clients/lsp/language.js +4 -1
- package/dist/clients/lsp/launch.js +8 -6
- package/dist/clients/lsp/server.js +84 -29
- package/dist/clients/lsp-document-symbols.js +112 -0
- package/dist/clients/mcp/analyze.js +5 -4
- package/dist/clients/mcp/review.js +36 -3
- package/dist/clients/module-report.js +100 -80
- package/dist/clients/ndjson-logger.js +51 -21
- package/dist/clients/path-keyed-map.js +76 -0
- package/dist/clients/path-utils.js +35 -0
- package/dist/clients/performance-report.js +327 -0
- package/dist/clients/pipeline.js +7 -0
- package/dist/clients/project-changes.js +77 -4
- 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 +109 -0
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +37 -5
- package/dist/clients/project-diagnostics/scanner.js +247 -127
- package/dist/clients/project-lens-config.js +53 -26
- package/dist/clients/project-report.js +39 -14
- package/dist/clients/project-scale.js +8 -10
- package/dist/clients/project-snapshot-persist-worker.js +6 -0
- package/dist/clients/project-snapshot.js +611 -7
- package/dist/clients/quiet-window.js +9 -1
- package/dist/clients/read-expansion.js +22 -12
- package/dist/clients/read-guard.js +78 -0
- package/dist/clients/redact/secrets.js +449 -0
- package/dist/clients/resource-sampler.js +133 -14
- package/dist/clients/reverse-deps.js +48 -0
- package/dist/clients/review-graph/builder.js +1522 -179
- 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-logger.js +24 -0
- package/dist/clients/runtime-agent-end.js +4 -2
- package/dist/clients/runtime-coordinator.js +2 -2
- package/dist/clients/runtime-session.js +433 -59
- package/dist/clients/runtime-tool-call.js +46 -14
- package/dist/clients/runtime-turn.js +97 -26
- package/dist/clients/safe-spawn.js +102 -15
- package/dist/clients/session-start-observability.js +15 -0
- package/dist/clients/session-state-store.js +2 -1
- package/dist/clients/sessionstart-logger.js +18 -0
- package/dist/clients/sg-runner.js +48 -33
- package/dist/clients/source-filter.js +49 -25
- package/dist/clients/startup-marker.js +6 -0
- package/dist/clients/startup-timing.js +9 -0
- package/dist/clients/todo-scanner.js +30 -3
- package/dist/clients/tree-sitter-cache.js +147 -139
- package/dist/clients/tree-sitter-client.js +1340 -67
- package/dist/clients/tree-sitter-logger.js +43 -1
- package/dist/clients/tree-sitter-query-loader.js +110 -11
- package/dist/clients/tree-sitter-shared.js +43 -19
- package/dist/clients/tree-sitter-symbol-extractor.js +71 -4
- package/dist/clients/widget-state.js +50 -14
- package/dist/clients/word-index-logger.js +44 -0
- package/dist/clients/word-index.js +252 -35
- package/dist/clients/workspace-topology.js +11 -1
- package/dist/index.js +45976 -39387
- package/dist/mcp/analyze-cli.js +3 -0
- package/dist/mcp/cli.js +110 -0
- package/dist/mcp/server.js +63 -8
- package/dist/tools/lens-diagnostics.js +47 -5
- package/dist/tools/lsp-diagnostics.js +126 -9
- package/dist/tools/lsp-navigation.js +5 -28
- package/dist/tools/symbol-search.js +21 -2
- package/docs/agent-guide.md +247 -0
- package/docs/astplayground.md +3 -4
- package/docs/audit1.md +5 -5
- package/docs/environment-variables.md +153 -11
- package/docs/features.md +4 -3
- package/docs/globalconfig.md +56 -4
- package/docs/language-coverage.md +2 -2
- package/docs/mcp.md +17 -16
- package/docs/servercapabilities.md +5 -2
- package/docs/settings.md +181 -0
- package/docs/tools.md +14 -0
- package/docs/tree-sitter_rules_catalog.md +11 -31
- package/docs/usage.md +13 -0
- package/package.json +139 -139
- 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-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/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-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/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/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/no-equality-in-for-condition.yml +1 -0
- package/rules/tree-sitter-queries/typescript/no-jump-in-finally.yml +1 -0
- 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/skills/pi-lens-write-tree-sitter-rule/SKILL.md +5 -0
- package/dist/clients/config-enabled-shape.js +0 -39
- 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
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
* - GitHub releases (platform-specific binaries → ~/.pi-lens/bin/)
|
|
47
47
|
*/
|
|
48
48
|
import { spawn } from "node:child_process";
|
|
49
|
-
import { existsSync, statSync } from "node:fs";
|
|
49
|
+
import { existsSync, statSync, unlinkSync } from "node:fs";
|
|
50
50
|
import fs from "node:fs/promises";
|
|
51
51
|
import https from "node:https";
|
|
52
52
|
import { createRequire } from "node:module";
|
|
@@ -54,17 +54,116 @@ import os from "node:os";
|
|
|
54
54
|
import path from "node:path";
|
|
55
55
|
const _installerRequire = createRequire(import.meta.url);
|
|
56
56
|
import { createGunzip } from "node:zlib";
|
|
57
|
-
import {
|
|
57
|
+
import { logSessionStart } from "../sessionstart-logger.js";
|
|
58
58
|
import { getGlobalPiLensDir } from "../file-utils.js";
|
|
59
59
|
import { allAvailableGlobalBinDirs, installArgs, pmBinary, resolveNodePackageManager, } from "../package-manager.js";
|
|
60
|
+
import { safeSpawnAsync } from "../safe-spawn.js";
|
|
60
61
|
// Global installation directory for pi-lens tools
|
|
61
62
|
const TOOLS_DIR = path.join(getGlobalPiLensDir(), "tools");
|
|
63
|
+
const INSTALL_LOCK_PATH = path.join(TOOLS_DIR, ".install.lock");
|
|
64
|
+
const activeInstallLocks = new Set();
|
|
65
|
+
let installLockExitCleanupRegistered = false;
|
|
66
|
+
function isProcessAlive(pid) {
|
|
67
|
+
try {
|
|
68
|
+
process.kill(pid, 0);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return error.code === "EPERM";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function acquireInstallLock() {
|
|
76
|
+
await fs.mkdir(TOOLS_DIR, { recursive: true });
|
|
77
|
+
// #946 review F2: the waiter's bound must exceed the owner's install bound
|
|
78
|
+
// (PI_LENS_INSTALL_TIMEOUT_MS, default 120s) — a 30s waiter gave up on a
|
|
79
|
+
// legitimate slow install and reported the tool unavailable for the whole
|
|
80
|
+
// session even though it arrived seconds later.
|
|
81
|
+
const timeoutMs = Number(process.env.PI_LENS_INSTALL_LOCK_TIMEOUT_MS) || 150_000;
|
|
82
|
+
const deadline = Date.now() + timeoutMs;
|
|
83
|
+
let lastOwner = "unknown owner";
|
|
84
|
+
while (Date.now() < deadline) {
|
|
85
|
+
try {
|
|
86
|
+
const handle = await fs.open(INSTALL_LOCK_PATH, "wx");
|
|
87
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, createdAt: Date.now() }));
|
|
88
|
+
await handle.close();
|
|
89
|
+
activeInstallLocks.add(INSTALL_LOCK_PATH);
|
|
90
|
+
if (!installLockExitCleanupRegistered) {
|
|
91
|
+
installLockExitCleanupRegistered = true;
|
|
92
|
+
process.once("exit", () => {
|
|
93
|
+
for (const lockPath of activeInstallLocks) {
|
|
94
|
+
try {
|
|
95
|
+
unlinkSync(lockPath);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Best effort; the next owner verifies this PID is dead.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
let released = false;
|
|
104
|
+
return {
|
|
105
|
+
release: async () => {
|
|
106
|
+
if (released)
|
|
107
|
+
return;
|
|
108
|
+
released = true;
|
|
109
|
+
activeInstallLocks.delete(INSTALL_LOCK_PATH);
|
|
110
|
+
await fs.rm(INSTALL_LOCK_PATH, { force: true });
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error.code !== "EEXIST")
|
|
116
|
+
throw error;
|
|
117
|
+
try {
|
|
118
|
+
const owner = JSON.parse(await fs.readFile(INSTALL_LOCK_PATH, "utf8"));
|
|
119
|
+
lastOwner = `pid=${owner.pid} createdAt=${owner.createdAt}`;
|
|
120
|
+
// #946 review F1: PID liveness alone cannot detect a hard-killed
|
|
121
|
+
// owner whose PID Windows has recycled — that lock would poison
|
|
122
|
+
// every future install with a full-timeout wait. A lock older
|
|
123
|
+
// than any legitimate install (owner install bound + slack) is
|
|
124
|
+
// stale regardless of what the PID now points at.
|
|
125
|
+
const maxAgeMs = (Number(process.env.PI_LENS_INSTALL_TIMEOUT_MS) || 120_000) +
|
|
126
|
+
60_000;
|
|
127
|
+
const expired = Number.isFinite(owner.createdAt) &&
|
|
128
|
+
Date.now() - owner.createdAt > maxAgeMs;
|
|
129
|
+
if (expired ||
|
|
130
|
+
(Number.isInteger(owner.pid) &&
|
|
131
|
+
owner.pid > 0 &&
|
|
132
|
+
!isProcessAlive(owner.pid))) {
|
|
133
|
+
await fs.rm(INSTALL_LOCK_PATH, { force: true });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (readError) {
|
|
138
|
+
lastOwner = "unreadable owner";
|
|
139
|
+
// An unreadable/empty lock has no createdAt to age out — fall
|
|
140
|
+
// back to the file's own mtime for the max-age check so it is
|
|
141
|
+
// eventually recoverable (#946 review F1/F6).
|
|
142
|
+
try {
|
|
143
|
+
const stat = await fs.stat(INSTALL_LOCK_PATH);
|
|
144
|
+
const maxAgeMs = (Number(process.env.PI_LENS_INSTALL_TIMEOUT_MS) || 120_000) +
|
|
145
|
+
60_000;
|
|
146
|
+
if (Date.now() - stat.mtimeMs > maxAgeMs) {
|
|
147
|
+
await fs.rm(INSTALL_LOCK_PATH, { force: true });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// stat raced a release — loop and retry acquisition.
|
|
153
|
+
}
|
|
154
|
+
void readError;
|
|
155
|
+
}
|
|
156
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
reason: `timed out after ${timeoutMs}ms waiting for shared tools install lock (${lastOwner})`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
62
163
|
// Directory for GitHub-downloaded binaries
|
|
63
164
|
const GITHUB_BIN_DIR = path.join(getGlobalPiLensDir(), "bin");
|
|
64
165
|
// Debug flag - set via PI_LENS_DEBUG=1 or --debug
|
|
65
166
|
const DEBUG = process.env.PI_LENS_DEBUG === "1" || process.argv.includes("--debug");
|
|
66
|
-
const SESSIONSTART_LOG_DIR = getGlobalPiLensDir();
|
|
67
|
-
const SESSIONSTART_LOG = path.join(SESSIONSTART_LOG_DIR, "sessionstart.log");
|
|
68
167
|
/**
|
|
69
168
|
* Log debug messages only when DEBUG is enabled
|
|
70
169
|
*/
|
|
@@ -73,18 +172,6 @@ function debugLog(...args) {
|
|
|
73
172
|
console.error("[auto-install:debug]", ...args);
|
|
74
173
|
}
|
|
75
174
|
}
|
|
76
|
-
function logSessionStart(msg) {
|
|
77
|
-
if (isTestMode()) {
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
|
81
|
-
void fs
|
|
82
|
-
.mkdir(SESSIONSTART_LOG_DIR, { recursive: true })
|
|
83
|
-
.then(() => fs.appendFile(SESSIONSTART_LOG, line))
|
|
84
|
-
.catch(() => {
|
|
85
|
-
// best-effort logging
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
175
|
/**
|
|
89
176
|
* Build a GitHub-release `assetMatch` from a small per-platform table, replacing
|
|
90
177
|
* the copy-pasted `if (platform === "linux") return arch === "arm64" ? … : …`
|
|
@@ -257,6 +344,24 @@ export const TOOLS = [
|
|
|
257
344
|
packageName: "bash-language-server",
|
|
258
345
|
binaryName: "bash-language-server",
|
|
259
346
|
},
|
|
347
|
+
{
|
|
348
|
+
id: "fish-lsp",
|
|
349
|
+
name: "Fish Language Server",
|
|
350
|
+
checkCommand: "fish-lsp",
|
|
351
|
+
checkArgs: ["--version"],
|
|
352
|
+
installStrategy: "npm",
|
|
353
|
+
packageName: "fish-lsp",
|
|
354
|
+
binaryName: "fish-lsp",
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
id: "cmake-language-server",
|
|
358
|
+
name: "CMake Language Server",
|
|
359
|
+
checkCommand: "cmake-language-server",
|
|
360
|
+
checkArgs: ["--version"],
|
|
361
|
+
installStrategy: "pip",
|
|
362
|
+
packageName: "cmake-language-server",
|
|
363
|
+
binaryName: "cmake-language-server",
|
|
364
|
+
},
|
|
260
365
|
{
|
|
261
366
|
id: "yaml-language-server",
|
|
262
367
|
name: "YAML Language Server",
|
|
@@ -1110,6 +1215,11 @@ export const TOOLS = [
|
|
|
1110
1215
|
},
|
|
1111
1216
|
];
|
|
1112
1217
|
const ensureInFlight = new Map();
|
|
1218
|
+
const installFailureReasons = new Map();
|
|
1219
|
+
/** Last honest install refusal/failure reason for callers that need diagnostics. */
|
|
1220
|
+
export function getInstallFailureReason(toolId) {
|
|
1221
|
+
return installFailureReasons.get(toolId);
|
|
1222
|
+
}
|
|
1113
1223
|
// Session-lifetime cache: once a tool path is resolved, skip the process-spawn check on subsequent calls.
|
|
1114
1224
|
const resolvedPathCache = new Map();
|
|
1115
1225
|
const PROBE_CACHE_PATH = path.join(getGlobalPiLensDir(), "probe-cache.json");
|
|
@@ -1133,26 +1243,61 @@ function scheduleProbeFlush() {
|
|
|
1133
1243
|
if (_probeCacheFlushTimer !== null)
|
|
1134
1244
|
return;
|
|
1135
1245
|
_probeCacheFlushTimer = setTimeout(() => {
|
|
1136
|
-
|
|
1137
|
-
if (!_probeCacheDirty || _probeCache === null)
|
|
1138
|
-
return;
|
|
1139
|
-
_probeCacheDirty = false;
|
|
1140
|
-
void fs
|
|
1141
|
-
.writeFile(PROBE_CACHE_PATH, JSON.stringify(_probeCache, null, 2))
|
|
1142
|
-
.catch(() => { });
|
|
1246
|
+
void flushProbeCache();
|
|
1143
1247
|
}, 300);
|
|
1144
1248
|
_probeCacheFlushTimer.unref?.();
|
|
1145
1249
|
}
|
|
1250
|
+
let _probeCacheWriteInFlight = null;
|
|
1251
|
+
/** Await pending probe-cache persistence before a one-shot process exits. */
|
|
1252
|
+
export async function flushProbeCache() {
|
|
1253
|
+
if (_probeCacheFlushTimer !== null) {
|
|
1254
|
+
clearTimeout(_probeCacheFlushTimer);
|
|
1255
|
+
_probeCacheFlushTimer = null;
|
|
1256
|
+
}
|
|
1257
|
+
// #946 review F4: if the 300ms timer's write already started, the dirty
|
|
1258
|
+
// flag is false but the write may still be in flight — an immediate
|
|
1259
|
+
// process.exit would truncate it. Always await the in-flight write.
|
|
1260
|
+
if (!_probeCacheDirty || _probeCache === null) {
|
|
1261
|
+
await _probeCacheWriteInFlight;
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
_probeCacheDirty = false;
|
|
1265
|
+
const write = (async () => {
|
|
1266
|
+
try {
|
|
1267
|
+
await fs.writeFile(PROBE_CACHE_PATH, JSON.stringify(_probeCache, null, 2));
|
|
1268
|
+
}
|
|
1269
|
+
catch {
|
|
1270
|
+
// Cache persistence is best effort; discovery remains authoritative.
|
|
1271
|
+
}
|
|
1272
|
+
})();
|
|
1273
|
+
_probeCacheWriteInFlight = write;
|
|
1274
|
+
try {
|
|
1275
|
+
await write;
|
|
1276
|
+
}
|
|
1277
|
+
finally {
|
|
1278
|
+
if (_probeCacheWriteInFlight === write)
|
|
1279
|
+
_probeCacheWriteInFlight = null;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1146
1282
|
function isAstGrepVersionOutput(output) {
|
|
1147
1283
|
return /\bast[- ]grep\b/i.test(output);
|
|
1148
1284
|
}
|
|
1149
1285
|
async function verifyAstGrepProbePath(binPath) {
|
|
1150
1286
|
return new Promise((resolve) => {
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1287
|
+
let proc;
|
|
1288
|
+
try {
|
|
1289
|
+
proc = spawn(binPath, ["--version"], {
|
|
1290
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1291
|
+
shell: process.platform === "win32" && /\.(cmd|bat)$/i.test(binPath),
|
|
1292
|
+
timeout: 5000,
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
catch {
|
|
1296
|
+
// SYNCHRONOUS spawn throw (Windows `spawn UNKNOWN`/EINVAL, the pidusage
|
|
1297
|
+
// bug class, #533) — best-effort probe, resolve rather than reject.
|
|
1298
|
+
resolve(false);
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1156
1301
|
let output = "";
|
|
1157
1302
|
proc.stdout?.on("data", (data) => (output += data));
|
|
1158
1303
|
proc.stderr?.on("data", (data) => (output += data));
|
|
@@ -1225,6 +1370,7 @@ export function resetProbeCacheStateForTesting() {
|
|
|
1225
1370
|
_probeCacheDirty = false;
|
|
1226
1371
|
resolvedPathCache.clear();
|
|
1227
1372
|
ensureInFlight.clear();
|
|
1373
|
+
installFailureReasons.clear();
|
|
1228
1374
|
lastManagedInstallVersion.clear();
|
|
1229
1375
|
if (_probeCacheFlushTimer !== null) {
|
|
1230
1376
|
clearTimeout(_probeCacheFlushTimer);
|
|
@@ -1238,7 +1384,7 @@ export function resetProbeCacheStateForTesting() {
|
|
|
1238
1384
|
* Catches broken symlinks (stat throws ENOENT or returns size 0) without
|
|
1239
1385
|
* spawning a process — ~μs per candidate vs ~50ms for which/where.
|
|
1240
1386
|
*/
|
|
1241
|
-
async function isCommandAvailable(command, _args) {
|
|
1387
|
+
export async function isCommandAvailable(command, _args) {
|
|
1242
1388
|
const isWindows = process.platform === "win32";
|
|
1243
1389
|
const pathEnv = process.env.PATH || process.env.Path || process.env.path || "";
|
|
1244
1390
|
const dirs = pathEnv.split(path.delimiter);
|
|
@@ -1266,6 +1412,23 @@ async function isCommandAvailable(command, _args) {
|
|
|
1266
1412
|
}
|
|
1267
1413
|
return false;
|
|
1268
1414
|
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Can `command` even be spawned? Path-bearing commands stat the file directly;
|
|
1417
|
+
* bare names walk PATH. ~μs either way — callers use this to skip a `--version`
|
|
1418
|
+
* probe that is guaranteed to fail with a full spawn round-trip.
|
|
1419
|
+
*/
|
|
1420
|
+
export async function isSpawnableCommand(command) {
|
|
1421
|
+
if (/[\\/]/.test(command)) {
|
|
1422
|
+
try {
|
|
1423
|
+
const stat = statSync(command);
|
|
1424
|
+
return stat.isFile() && stat.size > 0;
|
|
1425
|
+
}
|
|
1426
|
+
catch {
|
|
1427
|
+
return false;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
return isCommandAvailable(command);
|
|
1431
|
+
}
|
|
1269
1432
|
// --- Verification Functions
|
|
1270
1433
|
/**
|
|
1271
1434
|
* Stdio LSP servers built on `vscode-languageserver-node` (the entire
|
|
@@ -1341,11 +1504,21 @@ async function verifyToolBinary(binPath, onVersionOutput) {
|
|
|
1341
1504
|
}
|
|
1342
1505
|
// When shell:true (Windows .cmd), bake args into the command string to avoid DEP0190.
|
|
1343
1506
|
const spawnCmd = useShell ? `"${execPath}" --version` : execPath;
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1507
|
+
let proc;
|
|
1508
|
+
try {
|
|
1509
|
+
proc = spawn(spawnCmd, useShell ? [] : ["--version"], {
|
|
1510
|
+
timeout: 10000,
|
|
1511
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1512
|
+
shell: useShell,
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
catch (err) {
|
|
1516
|
+
// SYNCHRONOUS spawn throw (Windows `spawn UNKNOWN`/EINVAL, the pidusage
|
|
1517
|
+
// bug class, #533) — best-effort verify, resolve rather than reject.
|
|
1518
|
+
logSessionStart(`auto-install verify: spawn threw for ${binPath} (${err instanceof Error ? err.message : String(err)})`);
|
|
1519
|
+
resolve(false);
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1349
1522
|
let stdout = "";
|
|
1350
1523
|
let stderr = "";
|
|
1351
1524
|
proc.stdout?.on("data", (data) => (stdout += data));
|
|
@@ -1405,11 +1578,20 @@ export async function getAllToolStatuses() {
|
|
|
1405
1578
|
status.path = tool.checkCommand;
|
|
1406
1579
|
// Try to get version
|
|
1407
1580
|
const versionResult = await new Promise((resolve) => {
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1581
|
+
let proc;
|
|
1582
|
+
try {
|
|
1583
|
+
proc = spawn(tool.checkCommand, ["--version"], {
|
|
1584
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1585
|
+
shell: process.platform === "win32",
|
|
1586
|
+
timeout: 5000,
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
catch {
|
|
1590
|
+
// SYNCHRONOUS spawn throw (Windows `spawn UNKNOWN`/EINVAL, the
|
|
1591
|
+
// pidusage bug class, #533) — best-effort, resolve empty version.
|
|
1592
|
+
resolve("");
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1413
1595
|
let out = "";
|
|
1414
1596
|
proc.stdout?.on("data", (d) => (out += d));
|
|
1415
1597
|
proc.stderr?.on("data", (d) => (out += d));
|
|
@@ -1849,10 +2031,19 @@ async function getPythonUserBaseCandidates() {
|
|
|
1849
2031
|
const spawnCmd = isWin
|
|
1850
2032
|
? [probe.command, ...probe.args].join(" ")
|
|
1851
2033
|
: probe.command;
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
2034
|
+
let proc;
|
|
2035
|
+
try {
|
|
2036
|
+
proc = spawn(spawnCmd, isWin ? [] : probe.args, {
|
|
2037
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2038
|
+
shell: isWin,
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
catch {
|
|
2042
|
+
// SYNCHRONOUS spawn throw (Windows `spawn UNKNOWN`/EINVAL, the
|
|
2043
|
+
// pidusage bug class, #533) — best-effort probe, resolve empty.
|
|
2044
|
+
resolve("");
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
1856
2047
|
let stdout = "";
|
|
1857
2048
|
proc.stdout?.on("data", (data) => (stdout += data));
|
|
1858
2049
|
proc.on("exit", (code) => resolve(code === 0 ? stdout.trim() : ""));
|
|
@@ -1925,11 +2116,20 @@ function httpsGet(url, maxRedirects = 5, headers = {}) {
|
|
|
1925
2116
|
*/
|
|
1926
2117
|
function runCommand(command, args, cwd) {
|
|
1927
2118
|
return new Promise((resolve) => {
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2119
|
+
let proc;
|
|
2120
|
+
try {
|
|
2121
|
+
proc = spawn(command, args, {
|
|
2122
|
+
cwd,
|
|
2123
|
+
stdio: "ignore",
|
|
2124
|
+
shell: process.platform === "win32",
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
catch {
|
|
2128
|
+
// SYNCHRONOUS spawn throw (Windows `spawn UNKNOWN`/EINVAL, the pidusage
|
|
2129
|
+
// bug class, #533) — resolve false rather than reject/crash.
|
|
2130
|
+
resolve(false);
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
1933
2133
|
proc.on("exit", (code) => resolve(code === 0));
|
|
1934
2134
|
proc.on("error", () => resolve(false));
|
|
1935
2135
|
});
|
|
@@ -2257,23 +2457,16 @@ async function installArchiveTool(tool) {
|
|
|
2257
2457
|
const tarBin = isWindows
|
|
2258
2458
|
? `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\tar.exe`
|
|
2259
2459
|
: "tar";
|
|
2260
|
-
const
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
let stderr = "";
|
|
2266
|
-
proc.stderr?.on("data", (d) => (stderr += d));
|
|
2267
|
-
const timer = setTimeout(() => {
|
|
2268
|
-
proc.kill();
|
|
2269
|
-
resolve({ ok: false, stderr: "extraction timed out" });
|
|
2270
|
-
}, 120_000);
|
|
2271
|
-
proc.on("exit", (code) => {
|
|
2272
|
-
clearTimeout(timer);
|
|
2273
|
-
resolve({ ok: code === 0, stderr });
|
|
2274
|
-
});
|
|
2275
|
-
proc.on("error", (err) => resolve({ ok: false, stderr: err.message }));
|
|
2460
|
+
const extractResult = await safeSpawnAsync(tarBin, tarArgs, {
|
|
2461
|
+
cwd: TOOLS_DIR,
|
|
2462
|
+
timeout: 120_000,
|
|
2463
|
+
ignoreAmbientSignal: true,
|
|
2464
|
+
lifetimeCoupled: true,
|
|
2276
2465
|
});
|
|
2466
|
+
const extracted = {
|
|
2467
|
+
ok: extractResult.status === 0,
|
|
2468
|
+
stderr: extractResult.error?.message ?? extractResult.stderr,
|
|
2469
|
+
};
|
|
2277
2470
|
await fs.rm(tmpArchive, { force: true });
|
|
2278
2471
|
if (!extracted.ok) {
|
|
2279
2472
|
logSessionStart(`archive-install ${tool.id}: extraction failed: ${extracted.stderr}`);
|
|
@@ -2346,39 +2539,33 @@ async function installNpmTool(packageName, binaryName) {
|
|
|
2346
2539
|
// Resolve the package manager for the tools dir and build install args.
|
|
2347
2540
|
const isWindows = process.platform === "win32";
|
|
2348
2541
|
const pm = await resolveNodePackageManager(TOOLS_DIR);
|
|
2349
|
-
const
|
|
2542
|
+
const testNpmScript = process.env.PI_LENS_TEST_MODE === "1"
|
|
2543
|
+
? process.env.PI_LENS_TEST_NPM_SCRIPT
|
|
2544
|
+
: undefined;
|
|
2545
|
+
const pmCommand = testNpmScript ? process.execPath : pmBinary(pm);
|
|
2350
2546
|
// Use --ignore-scripts unless the package explicitly needs postinstall
|
|
2351
2547
|
// (e.g. biome downloads a platform-specific native binary via postinstall).
|
|
2352
2548
|
const needsScripts = NEEDS_POSTINSTALL.has(packageName);
|
|
2353
2549
|
const baseInstallArgs = installArgs(pm, packageName, {
|
|
2354
2550
|
ignoreScripts: !needsScripts,
|
|
2355
2551
|
});
|
|
2356
|
-
const INSTALL_TIMEOUT_MS = 120_000;
|
|
2357
|
-
const runInstallAttempt = async (args) =>
|
|
2358
|
-
const
|
|
2552
|
+
const INSTALL_TIMEOUT_MS = Number(process.env.PI_LENS_INSTALL_TIMEOUT_MS) || 120_000;
|
|
2553
|
+
const runInstallAttempt = async (args) => {
|
|
2554
|
+
const result = await safeSpawnAsync(pmCommand, args, {
|
|
2359
2555
|
cwd: TOOLS_DIR,
|
|
2360
|
-
|
|
2361
|
-
|
|
2556
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
2557
|
+
ignoreAmbientSignal: true,
|
|
2558
|
+
lifetimeCoupled: true,
|
|
2362
2559
|
});
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
proc.on("exit", (code) => {
|
|
2373
|
-
clearTimeout(timer);
|
|
2374
|
-
resolve({ ok: code === 0, stderr });
|
|
2375
|
-
});
|
|
2376
|
-
proc.on("error", (err) => {
|
|
2377
|
-
clearTimeout(timer);
|
|
2378
|
-
resolve({ ok: false, stderr: err.message });
|
|
2379
|
-
});
|
|
2380
|
-
});
|
|
2381
|
-
let outcome = await runInstallAttempt(baseInstallArgs);
|
|
2560
|
+
return {
|
|
2561
|
+
ok: result.status === 0,
|
|
2562
|
+
stderr: result.error?.message ?? result.stderr,
|
|
2563
|
+
};
|
|
2564
|
+
};
|
|
2565
|
+
let outcome = await runInstallAttempt([
|
|
2566
|
+
...(testNpmScript ? [testNpmScript] : []),
|
|
2567
|
+
...baseInstallArgs,
|
|
2568
|
+
]);
|
|
2382
2569
|
// --legacy-peer-deps is npm-only; retry just npm's ERESOLVE failures.
|
|
2383
2570
|
const erResolve = outcome.ok === false &&
|
|
2384
2571
|
/npm\s+error\s+ERESOLVE|\bERESOLVE\b|could not resolve/i.test(outcome.stderr);
|
|
@@ -2388,7 +2575,10 @@ async function installNpmTool(packageName, binaryName) {
|
|
|
2388
2575
|
legacyPeerDeps: true,
|
|
2389
2576
|
});
|
|
2390
2577
|
logSessionStart(`auto-install npm ${packageName}: retry with --legacy-peer-deps after ERESOLVE`);
|
|
2391
|
-
outcome = await runInstallAttempt(
|
|
2578
|
+
outcome = await runInstallAttempt([
|
|
2579
|
+
...(testNpmScript ? [testNpmScript] : []),
|
|
2580
|
+
...retryArgs,
|
|
2581
|
+
]);
|
|
2392
2582
|
}
|
|
2393
2583
|
if (!outcome.ok) {
|
|
2394
2584
|
throw new Error(`Failed to install ${packageName}: ${outcome.stderr}`);
|
|
@@ -2475,33 +2665,32 @@ async function installPipTool(packageName) {
|
|
|
2475
2665
|
];
|
|
2476
2666
|
let lastError = "";
|
|
2477
2667
|
for (const candidate of pipCandidates) {
|
|
2478
|
-
const
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
});
|
|
2483
|
-
let stderr = "";
|
|
2484
|
-
proc.stderr?.on("data", (data) => (stderr += data));
|
|
2485
|
-
proc.on("exit", (code) => {
|
|
2486
|
-
if (code === 0) {
|
|
2487
|
-
resolve({ ok: true, error: "" });
|
|
2488
|
-
}
|
|
2489
|
-
else {
|
|
2490
|
-
resolve({ ok: false, error: stderr.trim() });
|
|
2491
|
-
}
|
|
2492
|
-
});
|
|
2493
|
-
proc.on("error", (err) => {
|
|
2494
|
-
resolve({ ok: false, error: err.message });
|
|
2495
|
-
});
|
|
2668
|
+
const pipResult = await safeSpawnAsync(candidate.command, candidate.args, {
|
|
2669
|
+
timeout: 120_000,
|
|
2670
|
+
ignoreAmbientSignal: true,
|
|
2671
|
+
lifetimeCoupled: true,
|
|
2496
2672
|
});
|
|
2673
|
+
const outcome = {
|
|
2674
|
+
ok: pipResult.status === 0,
|
|
2675
|
+
error: (pipResult.error?.message ?? pipResult.stderr).trim(),
|
|
2676
|
+
};
|
|
2497
2677
|
if (outcome.ok) {
|
|
2498
2678
|
// Ensure user-level scripts directory is available in current process PATH.
|
|
2499
2679
|
// This helps tools installed via `pip install --user` become immediately callable.
|
|
2500
2680
|
const userBaseResult = await new Promise((resolve) => {
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2681
|
+
let probe;
|
|
2682
|
+
try {
|
|
2683
|
+
probe = spawn(candidate.command, ["-m", "site", "--user-base"], {
|
|
2684
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2685
|
+
shell: isWindows,
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2688
|
+
catch {
|
|
2689
|
+
// SYNCHRONOUS spawn throw (Windows `spawn UNKNOWN`/EINVAL, the
|
|
2690
|
+
// pidusage bug class, #533) — best-effort probe, resolve empty.
|
|
2691
|
+
resolve("");
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2505
2694
|
let stdout = "";
|
|
2506
2695
|
probe.stdout?.on("data", (data) => (stdout += data));
|
|
2507
2696
|
probe.on("exit", (code) => {
|
|
@@ -2576,21 +2765,15 @@ async function installPipTool(packageName) {
|
|
|
2576
2765
|
}
|
|
2577
2766
|
async function installGemTool(packageName) {
|
|
2578
2767
|
try {
|
|
2579
|
-
const
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
shell: isWindows,
|
|
2584
|
-
});
|
|
2585
|
-
let stderr = "";
|
|
2586
|
-
proc.stderr?.on("data", (data) => (stderr += data));
|
|
2587
|
-
proc.on("exit", (code) => {
|
|
2588
|
-
resolve({ ok: code === 0, error: stderr.trim() });
|
|
2589
|
-
});
|
|
2590
|
-
proc.on("error", (err) => {
|
|
2591
|
-
resolve({ ok: false, error: err.message });
|
|
2592
|
-
});
|
|
2768
|
+
const gemResult = await safeSpawnAsync("gem", ["install", packageName, "--no-document"], {
|
|
2769
|
+
timeout: 120_000,
|
|
2770
|
+
ignoreAmbientSignal: true,
|
|
2771
|
+
lifetimeCoupled: true,
|
|
2593
2772
|
});
|
|
2773
|
+
const outcome = {
|
|
2774
|
+
ok: gemResult.status === 0,
|
|
2775
|
+
error: (gemResult.error?.message ?? gemResult.stderr).trim(),
|
|
2776
|
+
};
|
|
2594
2777
|
if (!outcome.ok) {
|
|
2595
2778
|
throw new Error(`Failed to install ${packageName} via gem: ${outcome.error}`);
|
|
2596
2779
|
}
|
|
@@ -2605,6 +2788,11 @@ async function installGemTool(packageName) {
|
|
|
2605
2788
|
* Install a tool by ID
|
|
2606
2789
|
*/
|
|
2607
2790
|
export async function installTool(toolId) {
|
|
2791
|
+
if (process.env.PI_LENS_DISABLE_TOOL_INSTALL === "1") {
|
|
2792
|
+
installFailureReasons.set(toolId, "installation disabled by PI_LENS_DISABLE_TOOL_INSTALL=1");
|
|
2793
|
+
logSessionStart(`auto-install ${toolId}: refused — PI_LENS_DISABLE_TOOL_INSTALL=1`);
|
|
2794
|
+
return false;
|
|
2795
|
+
}
|
|
2608
2796
|
const tool = TOOLS.find((t) => t.id === toolId);
|
|
2609
2797
|
if (!tool) {
|
|
2610
2798
|
logSessionStart(`auto-install ${toolId}: unknown tool id`);
|
|
@@ -2676,6 +2864,7 @@ export async function installTool(toolId) {
|
|
|
2676
2864
|
* Ensure a tool is installed (check first, install if missing)
|
|
2677
2865
|
*/
|
|
2678
2866
|
export async function ensureTool(toolId, opts) {
|
|
2867
|
+
installFailureReasons.delete(toolId);
|
|
2679
2868
|
const cacheResolvedPath = (result) => {
|
|
2680
2869
|
if (result) {
|
|
2681
2870
|
resolvedPathCache.set(toolId, result);
|
|
@@ -2706,8 +2895,23 @@ export async function ensureTool(toolId, opts) {
|
|
|
2706
2895
|
logSessionStart(`auto-install ensure ${toolId}: force reinstall blocked — install disabled, discovery only (${Date.now() - ensureStartMs}ms)`);
|
|
2707
2896
|
return cacheResolvedPath(await getToolPath(toolId));
|
|
2708
2897
|
}
|
|
2709
|
-
|
|
2710
|
-
|
|
2898
|
+
if (process.env.PI_LENS_DISABLE_TOOL_INSTALL === "1") {
|
|
2899
|
+
installFailureReasons.set(toolId, "installation disabled by PI_LENS_DISABLE_TOOL_INSTALL=1");
|
|
2900
|
+
logSessionStart(`auto-install ensure ${toolId}: refused — PI_LENS_DISABLE_TOOL_INSTALL=1`);
|
|
2901
|
+
return undefined;
|
|
2902
|
+
}
|
|
2903
|
+
const lock = await acquireInstallLock();
|
|
2904
|
+
if (!lock.release) {
|
|
2905
|
+
logSessionStart(`auto-install ensure ${toolId}: ${lock.reason}`);
|
|
2906
|
+
return undefined;
|
|
2907
|
+
}
|
|
2908
|
+
let installed;
|
|
2909
|
+
try {
|
|
2910
|
+
installed = await installTool(toolId);
|
|
2911
|
+
}
|
|
2912
|
+
finally {
|
|
2913
|
+
await lock.release();
|
|
2914
|
+
}
|
|
2711
2915
|
if (!installed) {
|
|
2712
2916
|
logSessionStart(`auto-install ensure ${toolId}: force reinstall failed (${Date.now() - ensureStartMs}ms)`);
|
|
2713
2917
|
return undefined;
|
|
@@ -2783,7 +2987,32 @@ export async function ensureTool(toolId, opts) {
|
|
|
2783
2987
|
logSessionStart(`auto-install ensure ${toolId}: install disabled — discovery only, not found (${Date.now() - ensureStartMs}ms)`);
|
|
2784
2988
|
return undefined;
|
|
2785
2989
|
}
|
|
2786
|
-
|
|
2990
|
+
if (process.env.PI_LENS_DISABLE_TOOL_INSTALL === "1") {
|
|
2991
|
+
installFailureReasons.set(toolId, "installation disabled by PI_LENS_DISABLE_TOOL_INSTALL=1");
|
|
2992
|
+
logSessionStart(`auto-install ensure ${toolId}: refused — PI_LENS_DISABLE_TOOL_INSTALL=1 (${Date.now() - ensureStartMs}ms)`);
|
|
2993
|
+
return undefined;
|
|
2994
|
+
}
|
|
2995
|
+
const lock = await acquireInstallLock();
|
|
2996
|
+
if (!lock.release) {
|
|
2997
|
+
installFailureReasons.set(toolId, lock.reason ?? "install lock failed");
|
|
2998
|
+
logSessionStart(`auto-install ensure ${toolId}: ${lock.reason}`);
|
|
2999
|
+
return undefined;
|
|
3000
|
+
}
|
|
3001
|
+
let installed;
|
|
3002
|
+
try {
|
|
3003
|
+
// Cross-process double-check: the lock waiter may now observe the tool
|
|
3004
|
+
// installed by its predecessor and must not run a second package manager.
|
|
3005
|
+
const installedByPeer = await getToolPath(toolId);
|
|
3006
|
+
if (installedByPeer) {
|
|
3007
|
+
resolvedPathCache.set(toolId, installedByPeer);
|
|
3008
|
+
void updateProbeCache(toolId, installedByPeer);
|
|
3009
|
+
return installedByPeer;
|
|
3010
|
+
}
|
|
3011
|
+
installed = await installTool(toolId);
|
|
3012
|
+
}
|
|
3013
|
+
finally {
|
|
3014
|
+
await lock.release();
|
|
3015
|
+
}
|
|
2787
3016
|
if (!installed) {
|
|
2788
3017
|
logSessionStart(`auto-install ensure ${toolId}: unavailable (${Date.now() - ensureStartMs}ms)`);
|
|
2789
3018
|
return undefined;
|