pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
4,534 lines • 167 kB
JavaScript
import { createRequire as __pilensCreateRequire } from "node:module"; const require = __pilensCreateRequire(import.meta.url);
import {
resolveToolCwd
} from "./chunk-RUOG5D2C.js";
import {
allAvailableGlobalBinDirs,
installArgs,
pmBinary,
resolveNodePackageManager
} from "./chunk-U3VGR4VU.js";
import {
assertInstallAllowed,
projectTrustDenialReason
} from "./chunk-NTUJEGDZ.js";
import {
probeToolAsync
} from "./chunk-EOKRQ3SA.js";
import {
TRANSIENT_MAX_COOLDOWN_MS,
classifyProbeFailure,
createAvailabilityLatch,
logAvailabilityDecision,
startHostStallSampler
} from "./chunk-WONERJTP.js";
import {
acquireBoundedPidFileLock,
acquireQuarantinePidFileLock,
createGenerationMap,
getGlobalPiLensDir,
heartbeatIntervalMs,
isLockContention,
ownsTopGeneration,
recordGenerationTakeover,
recordLegacyLockHeld,
releaseGeneration,
resetSafeSpawnWindowsCommandCache,
safeSpawnAsync,
startGenerationHeartbeat,
tryAcquireGeneration
} from "./chunk-VN2AXLEX.js";
import {
DEFAULT_MAX_OUTPUT_BYTES,
createBoundedOutputSink,
writeFileAtomic,
writeFileAtomicAsync
} from "./chunk-2EECBNC5.js";
import {
getDegradationLedgerGeneration,
incrementDegradationCount,
recordDegradationOnce
} from "./chunk-N3YQJI6O.js";
import {
logExtension
} from "./chunk-O6TQT6RI.js";
import {
createNdjsonLogger,
getGlobalPiLensLogDir,
getMaxLogSizeMB,
isTestMode
} from "./chunk-UK3CMEAL.js";
import {
BoundedLruCache
} from "./chunk-ABBOA7UD.js";
import {
findLocalToolConfig,
isFullyQualified
} from "./chunk-Q5U6FMDI.js";
import {
__require
} from "./chunk-NXL4FFQQ.js";
// dist/clients/installer/index.js
import { spawn } from "node:child_process";
import { existsSync, readFileSync as readFileSync2, statSync, unlinkSync, writeFileSync } from "node:fs";
import fs2 from "node:fs/promises";
import { createHash, randomUUID } from "node:crypto";
import https from "node:https";
import { createRequire } from "node:module";
import os from "node:os";
import path3 from "node:path";
import { createGunzip } from "node:zlib";
// dist/clients/durable-store.js
import * as fs from "node:fs";
import fsp from "node:fs/promises";
function readLocked(path4) {
try {
return fs.readFileSync(path4, "utf8");
} catch {
return void 0;
}
}
function commitDurableStore(options) {
const release = options.onContention === "skip-log" ? acquireBoundedPidFileLock(`${options.path}.lock`, {
waitMs: options.waitMs,
retryMs: options.retryMs,
timeoutMessage: options.timeoutMessage,
onContention: "skip-log",
logContention: options.logContention
}) : acquireBoundedPidFileLock(`${options.path}.lock`, {
waitMs: options.waitMs,
retryMs: options.retryMs,
timeoutMessage: options.timeoutMessage,
onContention: "throw"
});
if (!release)
return void 0;
let committed;
try {
const current = options.deserialize(readLocked(options.path));
committed = options.merge(current);
writeFileAtomic(options.path, options.serialize(committed), {
bestEffort: false
});
options.afterWriteLocked?.(committed);
} finally {
release();
}
return committed;
}
async function readLockedAsync(path4) {
try {
return await fsp.readFile(path4, "utf8");
} catch (error) {
if (error.code === "ENOENT")
return void 0;
throw error;
}
}
async function commitDurableStoreAsync(options) {
const release = options.onContention === "skip-log" ? await acquireQuarantinePidFileLock(`${options.path}.lock`, {
waitMs: options.waitMs,
retryMs: options.retryMs,
staleMs: options.staleMs,
timeoutMessage: options.timeoutMessage,
onContention: "skip-log",
logContention: options.logContention
}) : await acquireQuarantinePidFileLock(`${options.path}.lock`, {
waitMs: options.waitMs,
retryMs: options.retryMs,
staleMs: options.staleMs,
timeoutMessage: options.timeoutMessage,
onContention: "throw"
});
if (!release)
return void 0;
try {
const current = options.deserialize(await readLockedAsync(options.path));
const committed = options.merge(current);
await writeFileAtomicAsync(options.path, options.serialize(committed), {
bestEffort: false
});
await options.afterWriteLocked?.(committed);
return committed;
} finally {
await release();
}
}
// dist/clients/sessionstart-logger.js
import * as path from "node:path";
var SESSIONSTART_LOG_FILE = path.join(getGlobalPiLensLogDir(), "sessionstart.log");
var writer = createNdjsonLogger({
filePath: SESSIONSTART_LOG_FILE,
maxBytes: getMaxLogSizeMB() * 1024 * 1024,
backupPath: `${SESSIONSTART_LOG_FILE}.1`
});
function logSessionStart(message) {
if (isTestMode())
return;
writer.append(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}`);
}
// dist/clients/zizmor-config.js
import * as path2 from "node:path";
var LOCAL_ZIZMOR_CONFIG_NAMES = [
path2.join(".github", "zizmor.yml"),
path2.join(".github", "zizmor.yaml"),
"zizmor.yml",
"zizmor.yaml"
];
function findLocalZizmorConfig(startDir) {
return findLocalToolConfig(startDir, LOCAL_ZIZMOR_CONFIG_NAMES);
}
function isZizmorAuditTarget(filePath) {
const normalized = filePath.replace(/\\/g, "/");
const base = path2.basename(normalized).toLowerCase();
if (/(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/i.test(normalized))
return true;
if (base === "action.yml" || base === "action.yaml")
return true;
if (/(^|\/)\.github\/dependabot\.ya?ml$/i.test(normalized))
return true;
return false;
}
var ZIZMOR_TOKEN_MAX_COOLDOWN_MS = 12e4;
var ghTokenLatch = createAvailabilityLatch({
maxCooldownMs: ZIZMOR_TOKEN_MAX_COOLDOWN_MS
});
var cachedToken;
var GH_TOKEN_PROBE_TIMEOUT_MS = 5e3;
function resetZizmorTokenAvailability() {
ghTokenLatch.reset();
cachedToken = void 0;
}
function classifyGhTokenFailure(res, hostStallMs) {
const neverAnswered = res.status === null || (res.status ?? 0) < 0;
if (!res.error && !neverAnswered) {
return {
outcome: "non-installable",
cause: "probe-rejected",
classifiedBy: "caller"
};
}
if (res.spawnFailure?.kind === "tool-not-found") {
return { outcome: "missing", cause: "not-found", classifiedBy: "caller" };
}
const classified = classifyProbeFailure(res, { hostStallMs });
if (classified.outcome === "transient" || classified.outcome === "missing") {
return { ...classified, classifiedBy: "probe" };
}
return {
outcome: "transient",
cause: classified.cause,
classifiedBy: "caller"
};
}
async function deriveGhCliToken(consumer) {
const sampler = startHostStallSampler();
const startedAt = Date.now();
const res = await safeSpawnAsync("gh", ["auth", "token"], {
timeout: GH_TOKEN_PROBE_TIMEOUT_MS,
ignoreAmbientSignal: true
});
const hostStallMs = sampler.stop();
const elapsedMs = Date.now() - startedAt;
if (!res.error && res.status === 0) {
const token = res.stdout.trim();
if (token.length > 0) {
ghTokenLatch.noteAvailable();
logAvailabilityDecision({
tool: consumer === "zizmor" ? "zizmor-gh-token" : "github-token",
verdict: "available",
outcome: "success",
cause: "ok",
elapsedMs,
latched: true,
hostStallMs,
budgetMs: GH_TOKEN_PROBE_TIMEOUT_MS,
classifiedBy: "probe"
});
return token;
}
return recordGhTokenUnavailable(consumer, {
outcome: "non-installable",
cause: "empty-result",
classifiedBy: "caller"
}, elapsedMs, hostStallMs);
}
return recordGhTokenUnavailable(consumer, classifyGhTokenFailure(res, hostStallMs), elapsedMs, hostStallMs);
}
function recordGhTokenUnavailable(consumer, { outcome, cause, classifiedBy }, elapsedMs, hostStallMs) {
const retryAfterMs = ghTokenLatch.noteUnavailable(outcome, cause);
if (consumer === "zizmor" && outcome === "transient") {
recordZizmorOfflineDegradation(`gh auth token probe ${cause}; running offline until the next zizmor start (retry allowed in ${Math.round(retryAfterMs / 1e3)}s)`);
}
logAvailabilityDecision({
tool: consumer === "zizmor" ? "zizmor-gh-token" : "github-token",
verdict: "unavailable",
outcome,
cause,
elapsedMs,
latched: outcome !== "transient",
hostStallMs,
...retryAfterMs > 0 && { retryAfterMs },
budgetMs: GH_TOKEN_PROBE_TIMEOUT_MS,
// Shared tail for both call paths; the caller carries whether ITS
// own verdict was a `classifyProbeFailure` passthrough or one of
// this module's own assertions (#2226 review F2).
classifiedBy
});
return void 0;
}
function recordZizmorOfflineDegradation(reason) {
incrementDegradationCount({
kind: "mode-suppression",
subject: "zizmor",
reason
});
}
async function resolveZizmorGitHubToken() {
if (process.env.ZIZMOR_OFFLINE)
return void 0;
return resolveGitHubToken(["ZIZMOR_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"], { consumer: "zizmor" });
}
async function resolveGitHubToken(envKeys = ["GITHUB_TOKEN", "GH_TOKEN"], options = {}) {
const consumer = options.consumer ?? "github-api";
const fromEnv = envKeys.map((key) => process.env[key]).find(Boolean);
if (fromEnv)
return fromEnv;
const memo = ghTokenLatch.read();
if (memo !== null) {
if (consumer === "zizmor" && memo === false && ghTokenLatch.getOutcome() === "transient") {
const cause = ghTokenLatch.getCause();
if (cause === null) {
throw new Error("zizmor gh-token latch: transient outcome with no cause (invariant violated)");
}
const retryAfterMs = Math.max(0, ghTokenLatch.getRetryAtMs() - Date.now());
recordZizmorOfflineDegradation(`gh auth token still cooling down (${cause}); serving cached offline verdict, retry allowed in ${Math.round(retryAfterMs / 1e3)}s`);
logAvailabilityDecision({
tool: "zizmor-gh-token",
verdict: "unavailable",
outcome: "transient",
cause,
elapsedMs: 0,
latched: false,
hostStallMs: 0,
...retryAfterMs > 0 && { retryAfterMs },
budgetMs: GH_TOKEN_PROBE_TIMEOUT_MS,
// No probe ran here: the latch's own remembered cause is replayed
// as-is, so the call site is the one asserting it (#2209).
classifiedBy: "caller"
});
}
return memo ? cachedToken : void 0;
}
cachedToken = await deriveGhCliToken(consumer);
return cachedToken;
}
// dist/clients/installer/index.js
var _installerRequire = createRequire(import.meta.url);
var TOOLS_DIR = path3.join(getGlobalPiLensDir(), "tools");
var INSTALL_LOCK_PATH = path3.join(TOOLS_DIR, ".install.lock");
var INSTALL_LOCK_GENERATIONS = `${INSTALL_LOCK_PATH}s`;
var activeInstallLocks = /* @__PURE__ */ new Set();
var installLockExitCleanupRegistered = false;
function getManagedToolsDir() {
return TOOLS_DIR;
}
function isProcessAlive(pid) {
try {
process.kill(pid, 0);
return true;
} catch (error) {
return error.code === "EPERM";
}
}
function installLockMaxAgeMs() {
return (Number(process.env.PI_LENS_INSTALL_TIMEOUT_MS) || 12e4) + 6e4;
}
function legacyInstallLockIsStale(maxAgeMs) {
try {
const owner = JSON.parse(readFileSync2(INSTALL_LOCK_PATH, "utf8"));
const expired = Number.isFinite(owner.createdAt) && Date.now() - owner.createdAt > maxAgeMs;
return expired || Number.isInteger(owner.pid) && owner.pid > 0 && !isProcessAlive(owner.pid);
} catch {
try {
return Date.now() - statSync(INSTALL_LOCK_PATH).mtimeMs > maxAgeMs;
} catch {
return false;
}
}
}
function createLegacyInstallLock(token) {
try {
writeFileSync(INSTALL_LOCK_PATH, token, { flag: "wx" });
return true;
} catch (cause) {
if (isLockContention(cause))
return false;
throw cause;
}
}
function takeLegacyInstallLock(maxAgeMs, token) {
if (createLegacyInstallLock(token))
return true;
if (!legacyInstallLockIsStale(maxAgeMs))
return false;
try {
unlinkSync(INSTALL_LOCK_PATH);
} catch {
return false;
}
return createLegacyInstallLock(token);
}
function installLockOwner() {
try {
const owner = JSON.parse(readFileSync2(INSTALL_LOCK_PATH, "utf8"));
return `pid=${owner.pid} createdAt=${owner.createdAt}`;
} catch {
return "unknown owner";
}
}
function tryAcquireInstallLock(maxAgeMs) {
const hold = tryAcquireGeneration(INSTALL_LOCK_GENERATIONS, maxAgeMs);
if (!hold)
return "busy";
if (hold.tookOverStale)
recordGenerationTakeover(hold);
const heartbeat = startGenerationHeartbeat(hold, heartbeatIntervalMs(maxAgeMs));
const token = JSON.stringify({
pid: process.pid,
createdAt: Date.now(),
nonce: randomUUID()
});
let took;
try {
took = takeLegacyInstallLock(maxAgeMs, token);
} catch (cause) {
heartbeat.stop();
releaseGeneration(hold);
throw cause;
}
if (took)
return { generation: hold, token, heartbeat };
heartbeat.stop();
releaseGeneration(hold);
return "legacy-held";
}
function releaseInstallLock(hold) {
activeInstallLocks.delete(hold);
hold.heartbeat.stop();
try {
if (readFileSync2(INSTALL_LOCK_PATH, "utf8") === hold.token)
unlinkSync(INSTALL_LOCK_PATH);
} catch {
}
releaseGeneration(hold.generation);
}
async function acquireInstallLock() {
const timeoutMs = Number(process.env.PI_LENS_INSTALL_LOCK_TIMEOUT_MS) || 15e4;
const deadline = Date.now() + timeoutMs;
const maxAgeMs = installLockMaxAgeMs();
let legacyHeldRecorded = false;
while (Date.now() < deadline) {
const hold = tryAcquireInstallLock(maxAgeMs);
if (hold === "legacy-held" && !legacyHeldRecorded) {
legacyHeldRecorded = true;
recordLegacyLockHeld(INSTALL_LOCK_PATH);
}
if (typeof hold === "object") {
activeInstallLocks.add(hold);
if (!installLockExitCleanupRegistered) {
installLockExitCleanupRegistered = true;
process.once("exit", () => {
for (const held of activeInstallLocks)
releaseInstallLock(held);
});
}
let released = false;
return {
release: async () => {
if (released)
return;
released = true;
releaseInstallLock(hold);
},
ownsLock: () => ownsTopGeneration(hold.generation)
};
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
return {
reason: `timed out after ${timeoutMs}ms waiting for shared tools install lock (${installLockOwner()})`
};
}
var GITHUB_BIN_DIR = path3.join(getGlobalPiLensDir(), "bin");
var DEBUG = process.env.PI_LENS_DEBUG === "1" || process.argv.includes("--debug");
function installerPlatform() {
const override = process.env.PI_LENS_TEST_PLATFORM;
if (override === "win32" || override === "linux") {
return override;
}
return process.platform;
}
function debugLog(...args) {
if (DEBUG) {
logExtension({
subsystem: "auto-install",
level: "debug",
message: args.map((arg) => typeof arg === "string" ? arg : JSON.stringify(arg)).join(" ")
});
}
}
function archAssetMatch(table) {
return (platform, arch) => {
if (arch !== "x64" && arch !== "arm64")
return void 0;
return table[platform]?.[arch];
};
}
var OS_ARCH_ZIP_ASSETS = {
linux: { x64: "linux_amd64.zip", arm64: "linux_arm64.zip" },
darwin: { x64: "darwin_amd64.zip", arm64: "darwin_arm64.zip" },
win32: { x64: "windows_amd64.zip", arm64: "windows_arm64.zip" }
};
function managedPackageFormatterTool(spec) {
return {
id: spec.id,
name: spec.name,
checkCommand: spec.id,
checkArgs: ["--version"],
installStrategy: spec.installStrategy,
packageName: spec.packageName,
binaryName: spec.id
};
}
var MANAGED_PACKAGE_FORMATTERS = [
{ id: "black", name: "Black", installStrategy: "pip", packageName: "black" },
{
id: "cmake-format",
name: "cmake-format",
installStrategy: "pip",
// The `yaml` EXTRA, never the bare distribution (#3312). cmakelang reads a
// `.cmake-format.yaml` project config through `import yaml`, and upstream
// puts PyYAML behind an extra: cmakelang 0.6.13's PyPI metadata declares
// `pyyaml (>=5.3) ; extra == 'yaml'`. A bare install still answers
// `cmake-format --version` with status 0 and then dies with
// `ModuleNotFoundError: No module named 'yaml'` on the first YAML config —
// the nightly's red cmake row. Every rung of the pip ladder passes this
// string verbatim to pip/pipx, which both accept the `pkg[extra]` spec.
packageName: "cmakelang[yaml]"
},
{ id: "oxfmt", name: "oxfmt", installStrategy: "npm", packageName: "oxfmt" }
];
function managedGitHubFormatterTool(spec) {
return {
id: spec.id,
name: spec.name,
checkCommand: spec.id,
checkArgs: ["--version"],
installStrategy: "github",
binaryName: spec.id,
github: {
repo: `${spec.owner}/${spec.repo}`,
assetMatch: spec.assetPattern,
...spec.binaryInArchive && { binaryInArchive: spec.binaryInArchive },
...spec.kind === "phar" && { launcher: "php" },
...spec.kind === "jar" && { launcher: "java" }
}
};
}
var MANAGED_GITHUB_FORMATTERS = [
{
id: "typstyle",
name: "typstyle",
owner: "typstyle-rs",
repo: "typstyle",
assetPattern: archAssetMatch({
linux: {
x64: "typstyle-x86_64-unknown-linux-gnu",
arm64: "typstyle-aarch64-unknown-linux-gnu"
},
darwin: {
x64: "typstyle-x86_64-apple-darwin",
arm64: "typstyle-aarch64-apple-darwin"
},
win32: {
x64: "typstyle-x86_64-pc-windows-msvc.exe",
arm64: "typstyle-aarch64-pc-windows-msvc.exe"
}
}),
kind: "binary"
},
{
id: "stylua",
name: "StyLua",
owner: "JohnnyMorganz",
repo: "StyLua",
assetPattern: archAssetMatch({
linux: { x64: "linux-x86_64.zip", arm64: "linux-aarch64.zip" },
darwin: { x64: "macos-x86_64.zip", arm64: "macos-aarch64.zip" },
win32: { x64: "windows-x86_64.zip" }
}),
kind: "binary",
binaryInArchive: "stylua"
},
{
id: "php-cs-fixer",
name: "PHP CS Fixer",
owner: "PHP-CS-Fixer",
repo: "PHP-CS-Fixer",
assetPattern: (platform) => platform === "linux" || platform === "darwin" || platform === "win32" ? "php-cs-fixer.phar" : void 0,
kind: "phar"
},
{
id: "cljfmt",
name: "cljfmt",
owner: "weavejester",
repo: "cljfmt",
assetPattern: (platform, arch) => {
if (platform === "linux")
return arch === "arm64" ? "standalone.jar" : "linux-amd64-static.tar.gz";
if (platform === "darwin")
return "standalone.jar";
if (platform === "win32")
return "win-amd64.zip";
return void 0;
},
kind: "jar",
binaryInArchive: "cljfmt"
}
];
var MANAGED_MAVEN_FORMATTERS = [
{
id: "google-java-format",
name: "google-java-format",
groupId: "com.google.googlejavaformat",
artifactId: "google-java-format",
version: "1.27.0",
classifier: "all-deps"
}
];
function managedMavenFormatterTool(spec) {
return {
id: spec.id,
name: spec.name,
checkCommand: spec.id,
checkArgs: ["--version"],
installStrategy: "maven",
binaryName: spec.id,
maven: {
groupId: spec.groupId,
artifactId: spec.artifactId,
version: spec.version,
classifier: spec.classifier
}
};
}
var TOOLS = [
// Core LSP servers
{
id: "typescript-language-server",
name: "TypeScript Language Server",
checkCommand: "typescript-language-server",
checkArgs: ["--version"],
installStrategy: "npm",
// Pinned below the package's own floor: 6.0.0 declares
// engines.node >=22.22.2, above the pi host's own floor (pi-lens
// must never require more than pi does — #2633) — an unpinned
// install here resolves latest and prints EBADENGINE on any pi
// host's supported Node. 5.3.0's own floor is engines.node >=20.
packageName: "typescript-language-server@5.3.0",
binaryName: "typescript-language-server"
},
{
id: "typescript",
name: "TypeScript",
checkCommand: "tsc",
checkArgs: ["--version"],
installStrategy: "npm",
// The managed compiler serves the classic typescript-language-server
// fallback. TypeScript 7 removed lib/tsserver.js and is selected only from
// project-local installs through the native `tsc --lsp --stdio` path.
// Revisit when typescript-language-server supports TS 7 — refs #1436.
packageName: "typescript@5.9.3",
binaryName: "tsc"
},
{
id: "pyright",
name: "Pyright",
checkCommand: "pyright",
checkArgs: ["--version"],
installStrategy: "npm",
packageName: "pyright",
binaryName: "pyright"
},
// Linting/formatting tools
{
id: "prettier",
name: "Prettier",
checkCommand: "prettier",
checkArgs: ["--version"],
installStrategy: "npm",
packageName: "prettier",
binaryName: "prettier"
},
...MANAGED_PACKAGE_FORMATTERS.map(managedPackageFormatterTool),
{
id: "ruff",
name: "Ruff",
checkCommand: "ruff",
checkArgs: ["--version"],
installStrategy: "pip",
packageName: "ruff",
binaryName: "ruff"
},
{
// Alternate Python LSP (fallback when pyright/the `python` server is
// unavailable or disabled). Used as a managedToolId by PythonJediServer.
id: "jedi-language-server",
name: "Jedi Language Server",
checkCommand: "jedi-language-server",
checkArgs: ["--version"],
installStrategy: "pip",
packageName: "jedi-language-server",
binaryName: "jedi-language-server"
},
{
id: "biome",
name: "Biome",
checkCommand: "biome",
checkArgs: ["--version"],
installStrategy: "npm",
packageName: "@biomejs/biome",
binaryName: "biome",
platformPackage: {
base: "@biomejs/cli",
suffixes: {
"linux-x64": "linux-x64",
"linux-arm64": "linux-arm64",
"darwin-x64": "darwin-x64",
"darwin-arm64": "darwin-arm64",
"win32-x64": "win32-x64",
"win32-arm64": "win32-arm64"
},
binaries: ["biome"]
}
},
// Analysis tools (run at session start / turn end)
{
id: "madge",
name: "Madge",
checkCommand: "madge",
checkArgs: ["--version"],
installStrategy: "npm",
packageName: "madge",
binaryName: "madge"
},
{
id: "jscpd",
name: "jscpd",
checkCommand: "jscpd",
checkArgs: ["--version"],
installStrategy: "npm",
packageName: "jscpd@5.4.0",
// v5.4.0 is the repository's exact devDependency; the v4 packaging defect that required the older v5.0.12 pin is gone, and parseReport() reads the unchanged clone-report fields.
binaryName: "jscpd"
},
// Structural search and dead code detection
{
id: "ast-grep",
name: "ast-grep CLI",
checkCommand: "ast-grep",
checkArgs: ["--version"],
installStrategy: "npm",
packageName: "@ast-grep/cli",
binaryName: "ast-grep",
platformPackage: {
suffixes: {
"linux-x64": "linux-x64-gnu",
"linux-arm64": "linux-arm64-gnu",
"darwin-x64": "darwin-x64",
"darwin-arm64": "darwin-arm64",
"win32-x64": "win32-x64-msvc",
"win32-arm64": "win32-arm64-msvc",
"win32-ia32": "win32-ia32-msvc"
},
binaries: ["ast-grep", "sg"]
}
},
{
id: "knip",
name: "Knip",
checkCommand: "knip",
checkArgs: ["--version"],
installStrategy: "npm",
packageName: "knip",
binaryName: "knip"
},
{
id: "yamllint",
name: "yamllint",
checkCommand: "yamllint",
checkArgs: ["--version"],
installStrategy: "pip",
packageName: "yamllint",
binaryName: "yamllint"
},
{
id: "sqlfluff",
name: "sqlfluff",
checkCommand: "sqlfluff",
checkArgs: ["--version"],
installStrategy: "pip",
packageName: "sqlfluff",
binaryName: "sqlfluff"
},
{
id: "bash-language-server",
name: "Bash Language Server",
checkCommand: "bash-language-server",
checkArgs: ["--version"],
// The #2188 sweep measured a 9,667ms cold `--version` start with closed
// stdin — close enough to the 10s installer default that modest host
// contention pushes it over and emits a false verification degradation.
// 20s gives the same kind of headroom Vue's 30s bound gives its own
// slower cold start (#2176), without inventing a shared literal (#2194).
verificationTimeoutMs: 2e4,
installStrategy: "npm",
packageName: "bash-language-server",
binaryName: "bash-language-server"
},
{
id: "fish-lsp",
name: "Fish Language Server",
checkCommand: "fish-lsp",
checkArgs: ["--version"],
installStrategy: "npm",
packageName: "fish-lsp",
binaryName: "fish-lsp"
},
{
id: "cmake-language-server",
name: "CMake Language Server",
checkCommand: "cmake-language-server",
checkArgs: ["--version"],
installStrategy: "pip",
packageName: "cmake-language-server",
binaryName: "cmake-language-server",
// Upstream 0.1.11 (the latest release) imports `LanguageServer` from
// `pygls.server`, a symbol pygls 2 removed, while declaring only
// `pygls>=1.1.1` — so an unconstrained install resolves pygls 2.1.1 and
// produces a launcher that cannot start. Measured against PyPI for the
// nightly's interpreter (#3311): `pip download --python-version 3.12
// cmake-language-server` → `pygls-2.1.1`; with this constraint →
// `pygls-1.3.1` + `lsprotocol-2023.0.1`, and `cmake-language-server
// --version` then prints `cmake-language-server 0.1.11` and exits 0.
pipConstraints: ["pygls<2"]
},
{
id: "yaml-language-server",
name: "YAML Language Server",
checkCommand: "yaml-language-server",
checkArgs: ["--version"],
installStrategy: "npm",
packageName: "yaml-language-server",
binaryName: "yaml-language-server"
},
{
id: "vscode-json-language-server",
name: "VSCode JSON Language Server",
checkCommand: "vscode-json-language-server",
checkArgs: ["--version"],
// The #2188 sweep measured an 11,047ms cold `--version` start with closed
// stdin — over the 10s installer default, so a cold or contended host can
// see a false verification degradation before the binary ever answers.
// 20s mirrors the bash-language-server bound above (#2194).
verificationTimeoutMs: 2e4,
installStrategy: "npm",
packageName: "vscode-langservers-extracted",
binaryName: "vscode-json-language-server"
},
{
id: "vscode-html-languageserver-bin",
name: "VSCode HTML Language Server",
checkCommand: "vscode-html-language-server",
checkArgs: ["--version"],
installStrategy: "npm",
packageName: "vscode-langservers-extracted",
binaryName: "vscode-html-language-server"
},
{
id: "htmlhint",
name: "HTMLHint",
checkCommand: "htmlhint",
checkArgs: ["--version"],
installStrategy: "npm",
packageName: "htmlhint",
binaryName: "htmlhint"
},
{
id: "hadolint",
name: "Hadolint",
checkCommand: "hadolint",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "hadolint",
github: {
repo: "hadolint/hadolint",
assetMatch: (platform, arch) => {
if (platform === "linux")
return arch === "arm64" ? "linux.aarch64" : "linux.x86_64";
if (platform === "darwin")
return arch === "arm64" ? "macos-arm64" : "macos-x86_64";
if (platform === "win32")
return "windows-x86_64.exe";
return void 0;
}
}
},
{
id: "helm",
name: "Helm",
checkCommand: "helm",
// intended: version --short — unverified against real binary (shape 16), do not wire until probed
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "helm",
github: {
repo: "helm/helm",
assetMatch: (platform, arch) => {
const cpu = arch === "arm64" ? "arm64" : "amd64";
if (platform === "linux")
return `linux-${cpu}.tar.gz`;
if (platform === "darwin")
return `darwin-${cpu}.tar.gz`;
if (platform === "win32")
return `windows-${cpu}.zip`;
return void 0;
},
// Release archives nest the executable under an OS/arch directory;
// the installer searches recursively and adds the Windows suffix.
binaryInArchive: "helm"
}
},
{
// Opengrep: a single standalone binary per platform on GitHub releases —
// no login, no telemetry (the reason for switching off Semgrep, #111).
id: "opengrep",
name: "Opengrep",
checkCommand: "opengrep",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "opengrep",
github: {
repo: "opengrep/opengrep",
assetMatch: (platform, arch) => {
if (platform === "linux")
return arch === "arm64" ? "opengrep_manylinux_aarch64" : "opengrep_manylinux_x86";
if (platform === "darwin")
return arch === "arm64" ? "opengrep_osx_arm64" : "opengrep_osx_x86";
if (platform === "win32")
return "opengrep_windows_x86.exe";
return void 0;
}
}
},
{
id: "vscode-css-languageserver",
name: "VSCode CSS Language Server",
checkCommand: "vscode-css-language-server",
checkArgs: ["--version"],
installStrategy: "npm",
packageName: "vscode-langservers-extracted",
binaryName: "vscode-css-language-server"
},
{
id: "dockerfile-language-server-nodejs",
name: "Dockerfile Language Server",
checkCommand: "docker-langserver",
checkArgs: ["--version"],
installStrategy: "npm",
packageName: "dockerfile-language-server-nodejs",
binaryName: "docker-langserver"
},
{
// #2722: intelephense has no CLI — its entry calls createConnection()
// unconditionally — and Node prints the offending source line before the
// error. The bundle is one ~4 MB minified line, so the transport-required
// marker #208 rescues on lands at byte 4,154,741 of a 4,423,356-byte
// stderr while Node truncates piped stderr at 1 MiB on exit. Measured on
// intelephense@1.18.5, linux, Node v22.22.1:
// $ intelephense --version 2>&1 | wc -c -> 1048576
// $ intelephense --version 2>&1 | grep -c "Connection input stream is not set" -> 0
// $ intelephense --version 2>err.txt; wc -c < err.txt -> 4423356
// Same run over the alternatives, each with stdin closed the way
// verifyToolBinary closes it (`input: ""`):
// --help, -v, --socket=0 -> identical dump (exit 1, 4423356 bytes,
// marker at 4154741, 1048576 through a pipe, marker absent)
// --stdio -> exit 1 with ZERO bytes on either stream,
// so it carries no marker to rescue on either.
// No checkArgs value produces a verdict. Verified spawn-free instead.
id: "intelephense",
name: "Intelephense",
checkCommand: "intelephense",
checkArgs: ["--version"],
verification: "package-entry",
installStrategy: "npm",
packageName: "intelephense",
binaryName: "intelephense"
},
{
id: "@prisma/language-server",
name: "Prisma Language Server",
checkCommand: "prisma-language-server",
checkArgs: ["--version"],
// #2169: a real closed-stdin cold run measured 27,265ms, and a warm-cache
// rerun still took 9,860ms — well past the 10s installer default. 40s
// keeps the same margin-over-worst-observed ratio Vue's 30s bound uses
// (#2176) rather than trimming it for a slower binary.
verificationTimeoutMs: 4e4,
installStrategy: "npm",
packageName: "@prisma/language-server",
binaryName: "prisma-language-server"
},
{
id: "@vue/language-server",
name: "Vue Language Server",
checkCommand: "vue-language-server",
checkArgs: ["--version"],
// Vue's launcher loads the full language-service bundle before answering
// --version. Its cold start exceeds the dispatch probe budget on some hosts
// even though warm starts complete quickly (#2176).
verificationTimeoutMs: 3e4,
installStrategy: "npm",
packageName: "@vue/language-server",
binaryName: "vue-language-server"
},
{
id: "svelte-language-server",
name: "Svelte Language Server",
checkCommand: "svelteserver",
checkArgs: ["--version"],
// #2169: a real closed-stdin cold run measured 12,410ms — over the 10s
// installer default, matching the bash/JSON class of false verification
// degradation from a cold-cache host (#2194). 20s mirrors that bound.
verificationTimeoutMs: 2e4,
installStrategy: "npm",
packageName: "svelte-language-server",
binaryName: "svelteserver"
},
{
id: "markdownlint",
name: "markdownlint-cli2",
checkCommand: "markdownlint-cli2",
// `--version` is interpreted as a file glob by markdownlint-cli2. Use
// stdin with globs disabled so verification cannot scan the workspace.
checkArgs: ["--no-globs", "-"],
installStrategy: "npm",
packageName: "markdownlint-cli2",
binaryName: "markdownlint-cli2"
},
{
id: "mypy",
name: "mypy",
checkCommand: "mypy",
checkArgs: ["--version"],
installStrategy: "pip",
packageName: "mypy",
binaryName: "mypy"
},
{
id: "rubocop",
name: "RuboCop",
checkCommand: "rubocop",
checkArgs: ["--version"],
installStrategy: "gem",
packageName: "rubocop",
binaryName: "rubocop"
},
{
id: "stylelint",
name: "Stylelint",
checkCommand: "stylelint",
checkArgs: ["--version"],
installStrategy: "npm",
packageName: "stylelint",
binaryName: "stylelint"
},
{
id: "oxlint",
name: "Oxlint",
checkCommand: "oxlint",
checkArgs: ["--version"],
installStrategy: "npm",
packageName: "oxlint",
binaryName: "oxlint"
},
// GitHub release binaries
{
id: "shellcheck",
name: "ShellCheck",
checkCommand: "shellcheck",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "shellcheck",
github: {
repo: "koalaman/shellcheck",
assetMatch: (platform, arch) => {
if (platform === "linux")
return arch === "arm64" ? "linux.aarch64.tar.xz" : "linux.x86_64.tar.xz";
if (platform === "darwin")
return arch === "arm64" ? "darwin.aarch64.tar.xz" : "darwin.x86_64.tar.xz";
if (platform === "win32")
return "zip";
return void 0;
},
binaryInArchive: "shellcheck"
}
},
{
id: "shfmt",
name: "shfmt",
checkCommand: "shfmt",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "shfmt",
github: {
repo: "mvdan/sh",
assetMatch: (platform, arch) => {
if (platform === "linux")
return arch === "arm64" ? "linux_arm64" : "linux_amd64";
if (platform === "darwin")
return arch === "arm64" ? "darwin_arm64" : "darwin_amd64";
if (platform === "win32")
return arch === "arm64" ? "windows_arm64.exe" : "windows_amd64.exe";
return void 0;
}
// bare binary, no archive
}
},
...MANAGED_GITHUB_FORMATTERS.map(managedGitHubFormatterTool),
...MANAGED_MAVEN_FORMATTERS.map(managedMavenFormatterTool),
{
id: "rust-analyzer",
name: "rust-analyzer",
checkCommand: "rust-analyzer",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "rust-analyzer",
github: {
repo: "rust-lang/rust-analyzer",
assetMatch: (platform, arch) => {
if (platform === "linux")
return arch === "arm64" ? "aarch64-unknown-linux-gnu.gz" : "x86_64-unknown-linux-gnu.gz";
if (platform === "darwin")
return arch === "arm64" ? "aarch64-apple-darwin.gz" : "x86_64-apple-darwin.gz";
if (platform === "win32")
return "x86_64-pc-windows-msvc.zip";
return void 0;
}
// Linux/macOS: bare .gz; Windows: .zip archive containing rust-analyzer.exe
}
},
{
// Alternate JS/TS LSP (fallback when the `typescript` server is unavailable
// or disabled — e.g. Deno projects). Used as a managedToolId by DenoServer.
// Every platform ships a .zip containing the `deno` binary (the github
// strategy extracts it, as it does for rust-analyzer's Windows .zip).
id: "deno",
name: "Deno",
checkCommand: "deno",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "deno",
github: {
repo: "denoland/deno",
assetMatch: (platform, arch) => {
if (platform === "linux")
return arch === "arm64" ? "deno-aarch64-unknown-linux-gnu.zip" : "deno-x86_64-unknown-linux-gnu.zip";
if (platform === "darwin")
return arch === "arm64" ? "deno-aarch64-apple-darwin.zip" : "deno-x86_64-apple-darwin.zip";
if (platform === "win32")
return "deno-x86_64-pc-windows-msvc.zip";
return void 0;
}
}
},
{
id: "golangci-lint",
name: "golangci-lint",
checkCommand: "golangci-lint",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "golangci-lint",
github: {
repo: "golangci/golangci-lint",
assetMatch: (platform, arch) => {
if (platform === "linux")
return arch === "arm64" ? "linux-arm64.tar.gz" : "linux-amd64.tar.gz";
if (platform === "darwin")
return arch === "arm64" ? "darwin-arm64.tar.gz" : "darwin-amd64.tar.gz";
if (platform === "win32")
return arch === "arm64" ? "windows-arm64.zip" : "windows-amd64.zip";
return void 0;
},
binaryInArchive: "golangci-lint"
}
},
{
id: "ktlint",
name: "ktlint",
checkCommand: "ktlint",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "ktlint",
github: {
// ktlint ships a self-executable `ktlint` (a JAR with a shell preamble)
// for Linux/macOS, plus a `ktlint.bat` wrapper for Windows that runs
// `java -jar %~dp0ktlint`. On Windows BOTH files are needed: the .bat AND
// the `ktlint` jar it wraps (#218). No arm64-specific asset.
repo: "pinterest/ktlint",
assetMatch: (platform, _arch) => {
if (platform === "linux")
return "ktlint";
if (platform === "darwin")
return "ktlint";
if (platform === "win32")
return "ktlint.bat";
return void 0;
},
extraAssets: (platform) => platform === "win32" ? ["ktlint"] : []
}
},
{
// ktfmt (Meta's opinionated Kotlin formatter) ships only as a Maven-Central
// fat JAR — no native binary, no npm package — so it uses the maven strategy
// (#129). Run via a `java -jar` launcher; requires a JRE.
id: "ktfmt",
name: "ktfmt",
checkCommand: "ktfmt",
checkArgs: ["--version"],
installStrategy: "maven",
binaryName: "ktfmt",
maven: {
groupId: "com.facebook",
artifactId: "ktfmt",
version: "0.63",
classifier: "with-dependencies"
}
},
{
// SpotBugs (bytecode bug-pattern analyzer for Java/Kotlin/Scala/Groovy)
// ships as a distribution archive — a lib/ of many JARs + bin/ launchers,
// NOT a runnable fat JAR — so it uses the archive strategy, not maven
// (refs #133). Requires a JRE (gated by the runner, not the install).
id: "spotbugs",
name: "SpotBugs",
checkCommand: "spotbugs",
// intended: -version — unverified against real binary (shape 16), do not wire until probed
checkArgs: ["--version"],
installStrategy: "archive",
binaryName: "spotbugs",
archive: {
url: "https://github.com/spotbugs/spotbugs/releases/download/4.10.2/spotbugs-4.10.2.tgz",
kind: "tgz",
launcher: "bin/spotbugs",
sha256: {
"https://github.com/spotbugs/spotbugs/releases/download/4.10.2/spotbugs-4.10.2.tgz": "63d7687c35fba12cbc8e55ec2a889a2bbf1b9be299dea91f2b0d351dc285308a"
}
}
},
{
// kotlin-language-server (fwcd, #3400) — a platform-agnostic launcher ZIP
// (`server/{bin,lib}`, 87 MB) whose `bin/kotlin-language-server` script
// starts a JVM on ANY argument and has no stable `--version`, so it is
// verified at install by manifest, not by spawn: the pinned sha256, the
// launcher on disk with an exec bit, and `java` on PATH or JAVA_HOME before
// the download (`verification: "tree-manifest"`). The 2025 release publishes
// no digest, so its pin is trust-on-first-use (the hash of the download). The initialize handshake is the nightly
// tool-smoke `kotlin` row. fwcd 1.3.13 (2025-01-18) rather than JetBrains'
// kotlin-lsp (v263.4702.0): its CDN ships the macOS standalone archives as
// `.sit`, which this installer's tar/unzip/Expand-Archive paths cannot
// extract, and its six per-platform archives would need six pins.
id: "kotlin-language-server",
name: "Kotlin Language Server",
checkCommand: "kotlin-language-server",
checkArgs: ["--version"],
installStrategy: "archive",
binaryName: "kotlin-language-server",
verification: "tree-manifest",
archive: {
url: "https://github.com/fwcd/kotlin-language-server/releases/download/1.3.13/server.zip",
kind: "zip",
launcher: "bin/kotlin-language-server",
runtime: "java",
sha256: {
"https://github.com/fwcd/kotlin-language-server/releases/download/1.3.13/server.zip": "4fe7d71d087b307c7869036171bd9d8c6a4284cd7c25b89098b0a24eb2d9b6d2"
}
}
},
{
// PowerShell Editor Services (#278). NOT a single binary — a multi-folder
// PowerShell MODULE BUNDLE launched via `pwsh Start-EditorServices.ps1
// -Stdio` (see PowerShellServer.spawn). archive TREE BUNDLE: the release zip
// extracts sibling module dirs (PowerShellEditorServices/, PSReadLine/,
// PSScriptAnalyzer/) at the root with no wrapping dir, so stripComponents:0
// + no launcher — the whole tree is kept and resolved to its extract dir.
// checkCommand "pwsh" documents the runtime but is unused for resolution
// (tree bundles resolve only via the extract dir + treeMarker).
id: "powershell-editor-services",
name: "PowerShell Editor Services",
checkCommand: "pwsh",
checkArgs: [
"-NoProfile",
"-Command",
"$PSVersionTable.PSVersion.ToString()"
],
installStrategy: "archive",
binaryName: "powershell-editor-services",
archive: {
url: "https://github.com/PowerShell/PowerShellEditorServices/releases/download/v4.6.0/PowerShellEditorServices.zip",
kind: "zip",
stripComponents: 0,
treeMarker: "PowerShellEditorServices/Start-EditorServices.ps1",
sha256: {
"https://github.com/PowerShell/PowerShellEditorServices/releases/download/v4.6.0/PowerShellEditorServices.zip": "0d91898f73d4faeb64291336f6386f0c890a933df012827571adf7008480a04a"
}
}
},
{
// clangd (C/C++/Obj-C LSP, #241) — a self-contained native TREE BUNDLE: the
// release zip wraps `clangd_<ver>/{bin,lib}` (bin/clangd[.exe] + the bundled
// libclang headers under lib/), so stripComponents:1 drops the version dir and
// the whole tree is kept (no launcher). Unlike PSES there is no external
// runtime — CppServer launches `<bundle>/bin/clangd` directly. checkCommand
// documents the binary but is unused for resolution (tree bundles resolve only
// via the extract dir + treeMarker). Platform-matched url: clangd ships x64
// prebuilts; arm runs the x64 build under Rosetta/emulation (darwin/win32),
// while linux/arm64 has no official build → undefined (graceful unavailable).
id: "clangd",
name: "clangd",
checkCommand: "clangd",
checkArgs: ["--version"],
installStrategy: "archive",
binaryName: "clangd",
archive: {
url: (platform, arch) => {
const version = "22.1.0";
const base = `https://github.com/clangd/clangd/releases/download/${version}`;
if (platform === "linux")
return arch === "x64" ? `${base}/clangd-linux-${version}.zip` : void 0;
if (platform === "darwin")
return `${base}/clangd-mac-${version}.zip`;
if (platform === "win32")
return `${base}/clangd-windows-${version}.zip`;
return void 0;
},
kind: "zip",
stripComponents: 1,
treeMarker: "bin",
sha256: {
"https://github.com/clangd/clangd/releases/download/22.1.0/clangd-linux-22.1.0.zip": "c54e57dbff3ccc9e8352367ddb7030ad3f624073ec58c7477424e7919f578572",
"https://github.com/clangd/clangd/releases/download/22.1.0/clangd-mac-22.1.0.zip": "71eddc5303da9a5bc5e8b509488b5b2c5acf45f20e33b8394e71a12a56d67198",
"https://github.com/clangd/clangd/releases/download/22.1.0/clangd-windows-22.1.0.zip": "e31e271fe11f6dcd7cf87ca74be4a12788ff8ce5a0b07762583e335c058e939a"
}
}
},
{
// lua-language-server (#564, split from #241) — same self-contained native
// TREE BUNDLE shape as clangd: bin/lua-language-server[.exe] + bundled
// locale/meta files, no external runtime. UNLIKE clangd, the release
// archive has NO wrapping version dir (verified by inspecting the actual
// 3.18.2 linux-x64 .tar.gz and win32-x64 .zip contents: `bin/`, `LICENSE`,
// `locale/`, … sit at archive root) — so stripComponents:0, not 1.
// LuaServer launches `<bundle>/bin/lua-language-server` directly.
// checkCommand documents the binary but is unused for resolution (tree
// bundles resolve only via the extract dir + treeMarker). Platform-matched
// url: LuaLS publishes darwin/linux x64+arm64 and win32 x64 (no win32/arm64
// build as of 3.18.2 → undefined, graceful unavailable); asset naming
// verified against the live GitHub release listing, not guessed.
id: "lua-language-server",
name: "lua-language-server",
checkCommand: "lua-language-server",
checkArgs: ["--version"],
installStrategy: "archive",
binaryName: "lua-language-server",
archive: {
url: (platform, arch) => {
const version = "3.18.2";
const base = `https://github.com/LuaLS/lua-language-server/releases/download/${version}`;
if (platform === "linux")
return arch === "arm64" ? `${base}/lua-language-server-${version}-linux-arm64.tar.gz` : `${base}/lua-language-server-${version}-linux-x64.tar.gz`;
if (platform === "darwin")
return arch === "arm64" ? `${base}/lua-language-server-${version}-darwin-arm64.tar.gz` : `${base}/lua-language-server-${version}-darwin-x64.tar.gz`;
if (platform === "win32")
return arch === "arm64" ? void 0 : `${base}/lua-language-server-${version}-win32-x64.zip`;
return void 0;
},
kind: (platform) => platform === "win32" ? "zip" : "tgz",
stripComponents: 0,
treeMarker: "bin",
sha256: {
"https://github.com/LuaLS/lua-language-server/releases/download/3.18.2/lua-language-server-3.18.2-linux-arm64.tar.gz": "273af33f26f4a1143f27c96d9f9e1188aba619c71e0807042134f66b4bd27f24",
"https://github.com/LuaLS/lua-language-server/releases/download/3.18.2/lua-language-server-3.18.2-linux-x64.tar.gz": "ca71415dd19f19e30aaa35a4915aefca9fdb5fec31b98331cc3d77f778d539c5",
"https://github.com/LuaLS/lua-language-server/releases/download/3.18.2/lua-language-server-3.18.2-darwin-arm64.tar.gz": "cec99d70b1f612acec4a10a79a03664e3aa0c229d4d8a586cb3f928ec37d509e",
"https://github.com/LuaLS/lua-language-server/releases/download/3.18.2/lua-language-server-3.18.2-darwin-x64.tar.gz": "e26cfefe423dd7326fc7c649539e4d4aaa4f35f34d2fefd8af2ed7090b72c556",
"https://github.com/LuaLS/lua-language-server/releases/download/3.18.2/lua-language-server-3.18.2-win32-x64.zip": "a4439a8f5e8e9e6505c11f045a7bf45db602124a1e246371c1dbe34924f3cf71"
}
}
},
{
id: "actionlint",
name: "actionlint",
checkCommand: "actionlint",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "actionlint",
github: {
repo: "rhysd/actionlint",
assetMatch: (platform, arch) => {
if (platform === "linux")
return arch === "arm64" ? "linux_arm64.tar.gz" : "linux_amd64.tar.gz";
if (platform === "darwin")
return arch === "arm64" ? "darwin_arm64.tar.gz" : "darwin_amd64.tar.gz";
if (platform === "win32")
return arch === "arm64" ? "windows_arm64.zip" : "windows_amd64.zip";
return void 0;
},
binaryInArchive: "actionlint"
}
},
{
// zizmor: GitHub Actions workflow security scanner that speaks LSP (#272).
// cargo-dist release archives, one per target triple, each holding a single
// `zizmor` binary (extracted via the recursive binary find). Online audits
// (known-vulnerable-actions, unpinned-uses, …) need a GitHub token — the LSP
// spawn forwards one via resolveZizmorGitHubToken (clients/zizmor-config.ts).
id: "zizmor",
name: "zizmor",
checkCommand: "zizmor",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "zizmor",
github: {
repo: "zizmorcore/zizmor",
assetMatch: (platform, arch) => {
if (platform === "linux")
return arch === "arm64" ? "aarch64-unknown-linux-gnu.tar.gz" : "x86_64-unknown-linux-gnu.tar.gz";
if (platform === "darwin")
return arch === "arm64" ? "aarch64-apple-darwin.tar.gz" : "x86_64-apple-darwin.tar.gz";
if (platform === "win32")
return "x86_64-pc-windows-msvc.zip";
return void 0;
},
binaryInArchive: "zizmor"
}
},
{
// typos-lsp: source-code spell checker that speaks LSP (#283). cargo-dist
// release archives, one per target triple, each holding a single `typos-lsp`
// binary (extracted via the recursive binary find). NO token / network — the
// dictionary is compiled in. The binary takes no `--version` (it ignores args
// and serves the LSP on stdin/stdout); the PATH probe ignores checkArgs and
// verifyToolBinary runs with stdin:ignore so the server gets EOF and exits.
id: "typos-lsp",
name: "typos-lsp",
checkCommand: "typos-lsp",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "typos-lsp",
github: {
repo: "tekumara/typos-lsp",
assetMatch: (platform, arch) => {
if (platform === "linux")
return arch === "arm64" ? "aarch64-unknown-linux-gnu.tar.gz" : "x86_64-unknown-linux-gnu.tar.gz";
if (platform === "darwin")
return arch === "arm64" ? "aarch64-apple-darwin.tar.gz" : "x86_64-apple-darwin.tar.gz";
if (platform === "win32")
return arch === "arm64" ? "aarch64-pc-windows-msvc.zip" : "x86_64-pc-windows-msvc.zip";
return void 0;
},
binaryInArchive: "typos-lsp"
}
},
{
id: "tflint",
name: "tflint",
checkCommand: "tflint",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "tflint",
github: {
repo: "terraform-linters/tflint",
assetMatch: archAssetMatch(OS_ARCH_ZIP_ASSETS),
binaryInArchive: "tflint"
}
},
{
// Terragrunt ships a bare native binary per platform on GitHub releases.
// Windows arm64 uses the x64 binary through Windows' built-in emulation —
// there is no terragrunt_windows_arm64.exe upstream.
id: "terragrunt",
name: "terragrunt",
checkCommand: "terragrunt",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "terragrunt",
github: {
repo: "gruntwork-io/terragrunt",
assetMatch: archAssetMatch({
linux: {
x64: "terragrunt_linux_amd64",
arm64: "terragrunt_linux_arm64"
},
darwin: {
x64: "terragrunt_darwin_amd64",
arm64: "terragrunt_darwin_arm64"
},
win32: {
x64: "terragrunt_windows_amd64.exe",
arm64: "terragrunt_windows_amd64.exe"
}
})
// bare binary — no binaryInArchive
}
},
{
id: "gitleaks",
name: "gitleaks",
checkCommand: "gitleaks",
// intended: version — unverified against real binary (shape 16), do not wire until probed
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "gitleaks",
github: {
repo: "gitleaks/gitleaks",
// gitleaks asset naming uses `x64` not `amd64` (unlike most Go-built
// tools). Substring match is exact-enough — release assets are
// named e.g. `gitleaks_8.18.4_linux_x64.tar.gz`.
assetMatch: archAssetMatch({
linux: { x64: "linux_x64.tar.gz", arm64: "linux_arm64.tar.gz" },
darwin: { x64: "darwin_x64.tar.gz", arm64: "darwin_arm64.tar.gz" },
win32: { x64: "windows_x64.zip", arm64: "windows_arm64.zip" }
}),
binaryInArchive: "gitleaks"
}
},
{
id: "trivy",
name: "Trivy",
checkCommand: "trivy",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "trivy",
github: {
repo: "aquasecurity/trivy",
// Trivy asset naming is `trivy_<ver>_<OS>-<bits>.{tar.gz,zip}` with a
// capitalized OS and `64bit`/`ARM64` arch tokens — e.g.
// `trivy_0.71.2_Linux-64bit.tar.gz`, `trivy_0.71.2_macOS-ARM64.tar.gz`.
// No windows-arm64 asset exists (win32.arm64 omitted), so (like
// swiftlint) trivy is absent from GITHUB_TOOLS and covered by the
// weaker "at least one platform" guard.
assetMatch: archAssetMatch({
linux: { x64: "Linux-64bit.tar.gz", arm64: "Linux-ARM64.tar.gz" },
darwin: { x64: "macOS-64bit.tar.gz", arm64: "macOS-ARM64.tar.gz" },
win32: { x64: "windows-64bit.zip" }
}),
binaryInArchive: "trivy"
}
},
{
id: "swiftlint",
name: "SwiftLint",
checkCommand: "swiftlint",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "swiftlint",
github: {
repo: "realm/SwiftLint",
assetMatch: (platform, arch) => {
if (platform === "darwin")
return "portable_swiftlint.zip";
if (platform === "linux")
return arch === "arm64" ? "swiftlint_linux_arm64.zip" : "swiftlint_linux_amd64.zip";
return void 0;
},
binaryInArchive: "swiftlint"
}
},
{
id: "taplo",
name: "taplo",
checkCommand: "taplo",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "taplo",
github: {
repo: "tamasfe/taplo",
assetMatch: (platform, arch) => {
if (platform === "linux")
return arch === "arm64" ? "taplo-linux-aarch64.gz" : "taplo-linux-x86_64.gz";
if (platform === "darwin")
return arch === "arm64" ? "taplo-darwin-aarch64.gz" : "taplo-darwin-x86_64.gz";
if (platform === "win32")
return "taplo-windows-x86_64.gz";
return void 0;
}
}
},
{
id: "vale",
name: "Vale",
checkCommand: "vale",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "vale",
github: {
repo: "vale-cli/vale",
assetMatch: (platform, arch) => {
const version = "3.14.2";
if (platform === "linux")
return arch === "arm64" ? `vale_${version}_Linux_arm64.tar.gz` : `vale_${version}_Linux_64-bit.tar.gz`;
if (platform === "darwin")
return arch === "arm64" ? `vale_${version}_macOS_arm64.tar.gz` : `vale_${version}_macOS_64-bit.tar.gz`;
if (platform === "win32")
return `vale_${version}_Windows_64-bit.zip`;
return void 0;
},
binaryInArchive: "vale"
}
},
{
id: "terraform-ls",
name: "terraform-ls",
checkCommand: "terraform-ls",
// intended: version — unverified against real binary (shape 16), do not wire until probed
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "terraform-ls",
github: {
repo: "hashicorp/terraform-ls",
hashiCorpReleaseProduct: "terraform-ls",
assetMatch: archAssetMatch(OS_ARCH_ZIP_ASSETS),
binaryInArchive: "terraform-ls"
}
},
{
id: "zls",
name: "zls",
checkCommand: "zls",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "zls",
github: {
repo: "zigtools/zls",
assetMatch: (platform, arch) => {
if (platform === "linux")
return arch === "arm64" ? "aarch64-linux.tar.xz" : "x86_64-linux.tar.xz";
if (platform === "darwin")
return arch === "arm64" ? "aarch64-macos.tar.xz" : "x86_64-macos.tar.xz";
if (platform === "win32")
return arch === "arm64" ? "aarch64-windows.zip" : "x86_64-windows.zip";
return void 0;
},
binaryInArchive: "zls"
}
},
{
// clojure-lsp ships a self-contained native (GraalVM) binary per platform
// on GitHub releases — no JVM needed. Used as managedToolId by ClojureServer.
// The .zip carries the bare binary (located recursively on extract).
id: "clojure-lsp",
name: "clojure-lsp",
checkCommand: "clojure-lsp",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "clojure-lsp",
github: {
repo: "clojure-lsp/clojure-lsp",
assetMatch: (platform, arch) => {
if (platform === "linux")
return arch === "arm64" ? "native-linux-aarch64.zip" : "native-linux-amd64.zip";
if (platform === "darwin")
return arch === "arm64" ? "native-macos-aarch64.zip" : "native-macos-amd64.zip";
if (platform === "win32")
return "native-windows-amd64.zip";
return void 0;
},
binaryInArchive: "clojure-lsp"
}
},
{
// CUE ships a single native binary per platform on GitHub releases;
// the LSP runs via `cue lsp serve`. Used as managedToolId by CueServer.
id: "cue",
name: "CUE",
checkCommand: "cue",
// Probed locally with CUE v0.17.1: `cue version` exits 0.
checkArgs: ["version"],
installStrategy: "github",
binaryName: "cue",
github: {
repo: "cue-lang/cue",
assetMatch: (platform, arch) => {
if (platform === "linux")
return arch === "arm64" ? "linux_arm64.tar.gz" : "linux_amd64.tar.gz";
if (platform === "darwin")
return arch === "arm64" ? "darwin_arm64.tar.gz" : "darwin_amd64.tar.gz";
if (platform === "win32")
return arch === "arm64" ? "windows_arm64.zip" : "windows_amd64.zip";
return void 0;
},
binaryInArchive: "cue"
}
},
{
// gleam ships a single static binary per platform on GitHub releases; the
// LSP runs via `gleam lsp`. Used as managedToolId by GleamServer. The linux
// build is a FLAT musl tarball (a bare `gleam`), handled by the recursive
// tar-binary lookup in installGitHubTool.
id: "gleam",
name: "Gleam",
checkCommand: "gleam",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "gleam",
github: {
repo: "gleam-lang/gleam",
assetMatch: (platform, arch) => {
if (platform === "linux")
return arch === "arm64" ? "aarch64-unknown-linux-musl.tar.gz" : "x86_64-unknown-linux-musl.tar.gz";
if (platform === "darwin")
return arch === "arm64" ? "aarch64-apple-darwin.tar.gz" : "x86_64-apple-darwin.tar.gz";
if (platform === "win32")
return arch === "arm64" ? "aarch64-pc-windows-msvc.zip" : "x86_64-pc-windows-msvc.zip";
return void 0;
},
binaryInArchive: "gleam"
}
},
{
// Tinymist publishes cargo-dist archives containing the `tinymist` binary
// for the supported desktop targets. The LSP server uses `tinymist lsp`.
id: "tinymist",
name: "Tinymist",
checkCommand: "tinymist",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "tinymist",
github: {
repo: "Myriad-Dreamin/tinymist",
assetMatch: archAssetMatch({
linux: {
x64: "tinymist-x86_64-unknown-linux-gnu.tar.gz",
arm64: "tinymist-aarch64-unknown-linux-gnu.tar.gz"
},
darwin: {
x64: "tinymist-x86_64-apple-darwin.tar.gz",
arm64: "tinymist-aarch64-apple-darwin.tar.gz"
},
win32: {
x64: "tinymist-x86_64-pc-windows-msvc.zip",
arm64: "tinymist-aarch64-pc-windows-msvc.zip"
}
}),
binaryInArchive: "tinymist"
}
},
{
// marksman ships a single BARE (uncompressed) binary per platform on GitHub
// releases — no archive, so it lands via the bare-binary branch of
// installGitHubTool (the `else` that writes the asset directly, like shfmt).
// Used as managedToolId by MarksmanServer; LSP entrypoint is `marksman
// server` (stdio). macOS ships a universal binary; Windows has only x64
// (runs on arm64 via emulation) — so all six platform/arch combos resolve.
id: "marksman",
name: "Marksman",
checkCommand: "marksman",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "marksman",
github: {
repo: "artempyanykh/marksman",
assetMatch: (platform, arch) => {
if (platform === "linux")
return arch === "arm64" ? "marksman-linux-arm64" : "marksman-linux-x64";
if (platform === "darwin")
return "marksman-macos";
if (platform === "win32")
return "marksman.exe";
return void 0;
}
// bare binary — no binaryInArchive
}
},
{
// Expert ships a bare native binary per platform on GitHub releases. Its
// `--stdio` flag is required to start the LSP transport. Windows arm64 uses
// the x64 binary through Windows' built-in x64 emulation.
id: "expert",
name: "Expert",
checkCommand: "expert",
checkArgs: ["--version"],
installStrategy: "github",
binaryName: "expert",
github: {
repo: "expert-lsp/expert",
assetMatch: (platform, arch) => {
if (arch !== "x64" && arch !== "arm64")
return void 0;
if (platform === "linux")
return arch === "arm64" ? "expert_linux_arm64" : "expert_linux_amd64";
if (platform === "darwin")
return arch === "arm64" ? "expert_darwin_arm64" : "expert_darwin_amd64";
if (platform === "win32")
return "expert_windows_amd64.exe";
return void 0;
}
// bare binary — no binaryInArchive
}
}
];
function getToolVerificationTimeout(tool) {
return tool.verificationTimeoutMs ?? 1e4;
}
var ensureInFlight = /* @__PURE__ */ new Map();
var installFailureReasons = /* @__PURE__ */ new Map();
function getInstallFailureReason(toolId) {
return installFailureReasons.get(toolId);
}
var installAttempts = /* @__PURE__ */ new Map();
function noteInstallAttempt(toolId, outcome, reason) {
installAttempts.set(toolId, { outcome, reason, at: Date.now() });
}
function noteInstallAttemptIfUnrecorded(toolId, outcome, reason) {
if (installAttempts.has(toolId))
return;
noteInstallAttempt(toolId, outcome, reason);
}
function getInstallAttempt(toolId) {
return installAttempts.get(toolId);
}
var lastEnsureResolutionSource = /* @__PURE__ */ new Map();
function getLastEnsureResolutionSource(toolId) {
return lastEnsureResolutionSource.get(toolId);
}
var resolvedPathCache = new BoundedLruCache(256);
function resetResolvedPathCache() {
resolvedPathCache.clear();
}
var PROBE_CACHE_PATH = path3.join(getGlobalPiLensDir(), "probe-cache.json");
var PROBE_CACHE_LOCK_STALE_MS = 18e4;
var PROBE_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
var PROBE_CACHE_TRANSIENT_COOLDOWN_MS = TRANSIENT_MAX_COOLDOWN_MS;
var PROBE_CACHE_FLUSH_LOCK_WAIT_MS = 250;
var PROBE_CACHE_FLUSH_RETRY_DELAY_MS = 300;
var PROBE_CACHE_FLUSH_RETRY_MAX_DELAY_MS = 3e4;
var _probeCache = null;
var _probeCacheDirty = false;
var _probeCacheFlushTimer = null;
var _probeCacheWriteInFlight = null;
var _probeCacheRetryAttempt = 0;
var _probeCacheChangeGeneration = 0;
var _probeCacheChanges = /* @__PURE__ */ new Map();
var _probeCacheChangeVersions = /* @__PURE__ */ new Map();
async function readProbeCache() {
if (_probeCache !== null)
return _probeCache;
try {
const raw = await fs2.readFile(PROBE_CACHE_PATH, "utf-8");
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("probe-cache root is not an object");
}
_probeCache = parsed;
} catch (err) {
const code = err?.code;
if (code !== "ENOENT") {
logSessionStart(`auto-install probe-cache: read failed (${code ?? "invalid"}); treating cache as unavailable`);
}
_probeCache = {};
}
return _probeCache;
}
function markProbeCacheChange(toolId, entry) {
_probeCacheChanges.set(toolId, entry);
_probeCacheChangeVersions.set(toolId, ++_probeCacheChangeGeneration);
_probeCacheDirty = true;
scheduleProbeFlush();
}
function scheduleProbeFlush(delayMs = PROBE_CACHE_FLUSH_RETRY_DELAY_MS) {
if (_probeCacheFlushTimer !== null)
return;
_probeCacheFlushTimer = setTimeout(() => {
void flushProbeCache();
}, delayMs);
_probeCacheFlushTimer.unref?.();
}
function scheduleProbeFlushRetry() {
const delay = Math.min(PROBE_CACHE_FLUSH_RETRY_MAX_DELAY_MS, PROBE_CACHE_FLUSH_RETRY_DELAY_MS * 2 ** Math.min(_probeCacheRetryAttempt, 6));
_probeCacheRetryAttempt += 1;
scheduleProbeFlush(delay);
}
function snapshotProbeCacheChanges() {
return {
changes: new Map(_probeCacheChanges),
versions: new Map(_probeCacheChangeVersions)
};
}
function deserializeProbeCache(contents) {
if (contents === void 0)
return {};
try {
const parsed = JSON.parse(contents);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("probe-cache root is not an object");
}
return parsed;
} catch (err) {
logSessionStart(`auto-install probe-cache: write-side read was corrupt (${err.message}); recovering as empty`);
return {};
}
}
function applyProbeCacheChanges(disk, changes) {
for (const [toolId, entry] of changes) {
if (entry === null)
delete disk[toolId];
else
disk[toolId] = entry;
}
}
function ageProbeCache(disk) {
const now = Date.now();
for (const [toolId, entry] of Object.entries(disk)) {
const ttl = entry.transient ? PROBE_CACHE_TRANSIENT_COOLDOWN_MS : PROBE_CACHE_TTL_MS;
if (!Number.isFinite(entry.cachedAt) || entry.cachedAt < now - ttl) {
delete disk[toolId];
}
}
}
function publishProbeCacheWrite(disk, snapshotVersions) {
const pendingAfterWrite = new Map(_probeCacheChanges);
_probeCache = disk;
for (const [toolId, entry] of pendingAfterWrite) {
if (entry === null)
delete _probeCache[toolId];
else
_probeCache[toolId] = entry;
}
for (const [toolId, version] of snapshotVersions) {
if (_probeCacheChangeVersions.get(toolId) === version) {
_probeCacheChanges.delete(toolId);
_probeCacheChangeVersions.delete(toolId);
}
}
_probeCacheDirty = _probeCacheChanges.size > 0;
_probeCacheRetryAttempt = 0;
return _probeCacheDirty ? "written-with-pending" : "written";
}
async function writeProbeCache() {
try {
const { changes, versions } = snapshotProbeCacheChanges();
let result = "written";
const committed = await commitDurableStoreAsync({
path: PROBE_CACHE_PATH,
deserialize: deserializeProbeCache,
merge: (disk) => {
ageProbeCache(disk);
applyProbeCacheChanges(disk, changes);
return disk;
},
serialize: (disk) => JSON.stringify(disk, null, 2),
waitMs: PROBE_CACHE_FLUSH_LOCK_WAIT_MS,
retryMs: 25,
staleMs: PROBE_CACHE_LOCK_STALE_MS,
timeoutMessage: "Timed out waiting for probe-cache lock",
onContention: "skip-log",
logContention: () => {
logSessionStart("auto-install probe-cache: flush deferred because another process owns the lock");
},
afterWriteLocked: (disk) => {
result = publishProbeCacheWrite(disk, versions);
}
});
if (committed === void 0) {
scheduleProbeFlushRetry();
return "deferred";
}
return result;
} catch (err) {
logSessionStart(`auto-install probe-cache: flush failed (${err?.code ?? "write error"}); pending update retained`);
scheduleProbeFlushRetry();
return "failed";
}
}
async function flushProbeCache() {
if (_probeCacheFlushTimer !== null) {
clearTimeout(_probeCacheFlushTimer);
_probeCacheFlushTimer = null;
}
if (_probeCacheWriteInFlight) {
await _probeCacheWriteInFlight;
if (!_probeCacheDirty)
return "written";
}
if (!_probeCacheDirty || _probeCache === null)
return "idle";
const write = writeProbeCache();
_probeCacheWriteInFlight = write;
try {
return await write;
} finally {
if (_probeCacheWriteInFlight === write)
_probeCacheWriteInFlight = null;
}
}
function isAstGrepVersionOutput(output) {
return /\bast[- ]grep\b/i.test(output);
}
async function verifyAstGrepProbePath(binPath) {
const result = await probeToolAsync(binPath, ["--version"], {
timeout: 5e3
});
return !result.error && result.status === 0 && isAstGrepVersionOutput(`${result.stdout}${result.stderr}`);
}
async function checkProbeCache(toolId) {
const cache = await readProbeCache();
const entry = cache[toolId];
if (!entry)
return void 0;
const ttl = entry.transient ? PROBE_CACHE_TRANSIENT_COOLDOWN_MS : PROBE_CACHE_TTL_MS;
if (Date.now() - entry.cachedAt > ttl) {
logSessionStart(`auto-install probe-cache ${toolId}: miss (${entry.transient ? "transient cooldown" : "ttl"} expired)`);
delete cache[toolId];
markProbeCacheChange(toolId, null);
return void 0;
}
try {
await fs2.access(entry.path);
const stat = await fs2.stat(entry.path);
if (stat.mtimeMs !== entry.mtimeMs || entry.sizeBytes !== void 0 && stat.size !== entry.sizeBytes) {
logSessionStart(`auto-install probe-cache ${toolId}: miss (mtime/size changed)`);
delete cache[toolId];
markProbeCacheChange(toolId, null);
return void 0;
}
if (toolId === "ast-grep" && !await verifyAstGrepProbePath(entry.path)) {
logSessionStart(`auto-install probe-cache ${toolId}: miss (not ast-grep: ${entry.path})`);
delete cache[toolId];
markProbeCacheChange(toolId, null);
return void 0;
}
return entry.path;
} catch {
logSessionStart(`auto-install probe-cache ${toolId}: miss (gone: ${entry.path})`);
delete cache[toolId];
markProbeCacheChange(toolId, null);
return void 0;
}
}
async function updateProbeCache(toolId, resolvedPath, transient = false) {
try {
const stat = await fs2.stat(resolvedPath);
const cache = await readProbeCache();
const entry = {
path: resolvedPath,
mtimeMs: stat.mtimeMs,
sizeBytes: stat.size,
cachedAt: Date.now(),
...transient && { transient: true }
};
cache[toolId] = entry;
markProbeCacheChange(toolId, entry);
} catch {
}
}
function resetProbeCacheStateForTesting() {
_probeCache = null;
_probeCacheDirty = false;
_probeCacheChanges.clear();
_probeCacheChangeVersions.clear();
_probeCacheChangeGeneration = 0;
_probeCacheRetryAttempt = 0;
resetResolvedPathCache();
ensureInFlight.clear();
installFailureReasons.clear();
installAttempts.clear();
lastEnsureResolutionSource.clear();
lastManagedInstallVersion.clear();
lastResolveTransient.clear();
resetPathWalkMemo();
if (_probeCacheFlushTimer !== null) {
clearTimeout(_probeCacheFlushTimer);
_probeCacheFlushTimer = null;
}
}
function _peekEnsureInFlightForTesting() {
return ensureInFlight;
}
var pathWalkMemo = /* @__PURE__ */ new Map();
function hashSync(value) {
let hash = 2166136261;
for (let i = 0; i < value.length; i += 1) {
hash ^= value.charCodeAt(i);
hash = Math.imul(hash, 16777619);
}
return (hash >>> 0).toString(16);
}
function resetPathWalkMemo() {
pathWalkMemo.clear();
}
async function isCommandAvailable(command, _args) {
const isWindows = installerPlatform() === "win32";
const pathEnv = process.env.PATH || process.env.Path || process.env.path || "";
const dirs = pathEnv.split(path3.delimiter);
const names = isWindows ? [command, `${command}.exe`, `${command}.cmd`, `${command}.bat`] : [command];
for (const dir of dirs) {
if (!dir)
continue;
for (const name of names) {
const candidate = path3.join(dir, name);
try {
const stat = statSync(candidate);
if (stat.isFile() && stat.size > 0) {
return true;
}
} catch {
}
}
}
return false;
}
async function isSpawnableCommand(command) {
if (/[\\/]/.test(command)) {
try {
const stat = statSync(command);
return stat.isFile() && stat.size > 0;
} catch {
return false;
}
}
const memoKey = `${command}:${hashSync(process.env.PATH || "")}`;
if (pathWalkMemo.has(memoKey))
return pathWalkMemo.get(memoKey) ?? false;
const isSpawnable = await isCommandAvailable(command);
pathWalkMemo.set(memoKey, isSpawnable);
return isSpawnable;
}
function isLspTransportRequiredError(output) {
return /Connection (?:input|output) stream is not set|Use arguments of createConnection/i.test(output);
}
var lspTransportRequiredMatcher = /Connection (?:input|output) stream is not set|Use arguments of createConnection/i;
function parsePinnedVersion(packageName) {
const at = packageName.lastIndexOf("@");
if (at <= 0)
return void 0;
return packageName.slice(at + 1) || void 0;
}
function extractVersionToken(output) {
return output.match(/\d+\.\d+\.\d+(?:[-+][\w.]+)?/)?.[0];
}
var lastManagedInstallVersion = /* @__PURE__ */ new Map();
function packageEntryVerification(tool) {
return tool.verification === "package-entry" ? tool.packageName : void 0;
}
async function verifyNpmPackageEntry(binPath, packageName) {
const nodeModulesDir = path3.dirname(path3.dirname(binPath));
const pinned = parsePinnedVersion(packageName);
const bareName = pinned ? packageName.slice(0, -(pinned.length + 1)) : packageName;
const packageDir = path3.join(nodeModulesDir, ...bareName.split("/"));
const shimName = path3.basename(binPath).replace(/\.(cmd|exe|ps1|bat)$/i, "").toLowerCase();
const fail = (reason) => {
logSessionStart(`auto-install verify: failed for ${binPath} (check=package-entry, kind=${reason})`);
return false;
};
try {
const shim = statSync(binPath);
if (!shim.isFile() || shim.size === 0)
return fail("shim-not-a-file");
} catch {
return fail("shim-missing");
}
let manifest;
try {
manifest = JSON.parse(await fs2.readFile(path3.join(packageDir, "package.json"), "utf8"));
} catch {
return fail("package-json-unreadable");
}
if (typeof manifest.version !== "string" || !manifest.version) {
return fail("package-json-no-version");
}
const bin = manifest.bin;
const entry = typeof bin === "string" ? bin : bin && typeof bin === "object" ? Object.entries(bin).find(
// Case-folded on purpose: on a case-insensitive filesystem the
// shim on disk can differ in case from the manifest key npm
// wrote it from, and `path.basename` reads the disk name.
([name]) => name.toLowerCase() === shimName
)?.[1] : void 0;
if (typeof entry !== "string" || !entry) {
return fail("package-json-no-entry");
}
try {
const stat = statSync(path3.join(packageDir, entry));
if (!stat.isFile() || stat.size === 0)
return fail("entry-not-a-file");
} catch {
return fail("entry-missing");
}
logSessionStart(`auto-install verify: succeeded for ${binPath} (check=package-entry, version=${manifest.version}, entry=${entry})`);
return true;
}
async function verifyToolBinary(binPath, onVersionOutput, onTransient, timeoutMs = 1e4, verificationArgs = ["--version"], packageEntryOf, onInconclusive) {
if (packageEntryOf !== void 0) {
return verifyNpmPackageEntry(binPath, packageEntryOf);
}
const isWindows = installerPlatform() === "win32";
const hasKnownWindowsExt = /\.(cmd|exe|ps1)$/i.test(binPath);
let execPath = isWindows && !hasKnownWindowsExt ? `${binPath}.cmd` : binPath;
if (isWindows && /\.ps1$/i.test(execPath)) {
const cmdSibling = `${execPath.slice(0, -4)}.cmd`;
if (__require("node:fs").existsSync(cmdSibling)) {
execPath = cmdSibling;
}
}
const useShell = isWindows && /\.(cmd|bat)$/i.test(execPath);
void useShell;
try {
const result = await safeSpawnAsync(execPath, verificationArgs, {
timeout: timeoutMs,
input: "",
// A broken or version-blind language server can emit a bundled
// megabytes-long diagnostic on stderr. Keep verification bounded even
// when the child reaches its normal timeout first.
maxOutputBytes: 64 * 1024,
matchWhileStreaming: lspTransportRequiredMatcher
});
const output = `${result.stdout}
${result.stderr}`;
if (result.outputTruncated) {
recordDegradationOnce({
kind: "installer-verification-output-truncated",
subject: binPath,
reason: `verification output exceeded 65536 bytes (${verificationArgs.join(" ")})`
});
}
if (result.status === 0 && !result.error) {
debugLog(`Verified: ${binPath} (${verificationArgs.join(" ")}: ${result.stdout.trim()})`);
onVersionOutput?.(result.stdout);
return true;
}
if (result.streamingMatch || isLspTransportRequiredError(output)) {
debugLog(`Verified (stdio LSP, transport-required): ${binPath}`);
return true;
}
if (result.signal !== void 0 || result.spawnFailure)
onTransient?.();
if (result.outputTruncated && result.signal === void 0 && !result.spawnFailure) {
onInconclusive?.();
recordDegradationOnce({
kind: "installer-verification-inconclusive",
subject: binPath,
reason: `transport-required marker unresolved in truncated output (${verificationArgs.join(" ")})`
});
}
const kind = result.spawnFailure?.kind ?? (result.signal ? `killed-signal=${result.signal}` : result.error ? "spawn-error" : "exit-nonzero");
logSessionStart(`auto-install verify: failed for ${binPath} (check=${verificationArgs.join(" ")}, kind=${kind}${result.status !== null ? `, exit=${result.status}` : ""})`);
return false;
} catch (err) {
logSessionStart(`auto-install verify: spawn threw for ${binPath} (${err instanceof Error ? err.message : String(err)})`);
onTransient?.();
return false;
}
}
async function getAllToolStatuses() {
const statuses = [];
for (const tool of TOOLS) {
const status = {
id: tool.id,
name: tool.name,
installed: false,
source: "not-installed",
strategy: tool.installStrategy
};
if (tool.installStrategy === "archive" && !tool.archive?.launcher) {
const bundleDir = await getArchiveTreeBundlePath(tool);
if (bundleDir) {
status.installed = true;
status.source = "archive-dist";
status.path = bundleDir;
}
statuses.push(status);
continue;
}
if (await isCommandAvailable(tool.checkCommand, tool.checkArgs)) {
status.installed = true;
status.source = "global-path";
status.path = tool.checkCommand;
if (tool.verification !== "tree-manifest") {
const probe = await probeToolAsync(tool.checkCommand, ["--version"], {
timeout: 5e3
});
status.version = `${probe.stdout}${probe.stderr}`.trim().split("\n")[0]?.slice(0, 30) || void 0;
}
statuses.push(status);
continue;
}
if (tool.installStrategy === "npm") {
const npmPath = await findNpmGlobalToolPath(tool.binaryName || tool.id, void 0, tool.checkArgs, getToolVerificationTimeout(tool));
if (npmPath) {
status.installed = true;
status.source = "npm-global";
status.path = npmPath;
statuses.push(status);
continue;
}
}
if (tool.installStrategy === "pip") {
const pipPath = await findPipUserToolPath(tool.binaryName || tool.id, void 0, tool.checkArgs, getToolVerificationTimeout(tool));
if (pipPath) {
status.installed = true;
status.source = "pip-user";
status.path = pipPath;
statuses.push(status);
continue;
}
}
if (tool.installStrategy === "github" || tool.installStrategy === "maven" || tool.installStrategy === "archive") {
const githubPath = await findGitHubToolPath(tool.binaryName || tool.id);
if (githubPath) {
status.installed = true;
status.source = tool.installStrategy === "maven" ? "maven-jar" : tool.installStrategy === "archive" ? "archive-dist" : "github-release";
status.path = githubPath;
statuses.push(status);
continue;
}
}
const localBase = path3.join(TOOLS_DIR, "node_modules", ".bin", tool.binaryName || tool.id);
const localPath = installerPlatform() === "win32" ? `${localBase}.cmd` : localBase;
try {
await fs2.access(localPath);
if (await verifyToolBinary(localPath, void 0, void 0, getToolVerificationTimeout(tool), tool.checkArgs, packageEntryVerification(tool))) {
status.installed = true;
status.source = "pi-lens-auto";
status.path = localPath;
statuses.push(status);
continue;
}
} catch {
}
if (tool.installStrategy === "npm") {
status.source = "npx-fallback";
}
statuses.push(status);
}
return statuses;
}
async function isToolInstalled(toolId) {
return await getToolPath(toolId) !== void 0;
}
async function getArchiveTreeBundlePath(tool) {
if (tool.installStrategy !== "archive" || tool.archive?.launcher) {
return void 0;
}
const extractDir = path3.join(TOOLS_DIR, tool.id);
const marker = tool.archive?.treeMarker ? path3.join(extractDir, ...tool.archive.treeMarker.split("/")) : extractDir;
try {
await fs2.access(marker);
return extractDir;
} catch {
return void 0;
}
}
function resolvePlatformPackageBinary(tool) {
const spec = tool.platformPackage;
if (!spec || !tool.packageName)
return void 0;
const suffix = spec.suffixes[`${installerPlatform()}-${process.arch}`];
if (!suffix)
return void 0;
const platformPkg = `${spec.base ?? tool.packageName}-${suffix}`;
try {
const mainPkgJson = _installerRequire.resolve(`${tool.packageName}/package.json`);
const fromMain = createRequire(mainPkgJson);
let pkgDir;
try {
pkgDir = path3.dirname(fromMain.resolve(`${platformPkg}/package.json`));
} catch {
pkgDir = path3.dirname(_installerRequire.resolve(`${platformPkg}/package.json`));
}
const isWin = installerPlatform() === "win32";
for (const bin of spec.binaries) {
for (const name of isWin ? [`${bin}.exe`, bin] : [bin]) {
const candidate = path3.join(pkgDir, name);
if (existsSync(candidate))
return candidate;
}
}
} catch {
}
return void 0;
}
var lastResolveTransient = /* @__PURE__ */ new Map();
var lastInstallResolutionId = /* @__PURE__ */ new Map();
function wasLastResolveTransient(toolId) {
return lastResolveTransient.get(toolId) ?? false;
}
async function getToolPath(toolId) {
let sawTransient = false;
const markTransient = () => {
sawTransient = true;
};
const result = await getToolPathResolved(toolId, markTransient);
lastResolveTransient.set(toolId, sawTransient);
return result;
}
async function getToolPathResolved(toolId, onTransient) {
const tool = TOOLS.find((t) => t.id === toolId);
if (!tool)
return void 0;
if (tool.installStrategy === "archive" && !tool.archive?.launcher) {
return getArchiveTreeBundlePath(tool);
}
const pinnedVersion = tool.installStrategy === "npm" && tool.packageName ? parsePinnedVersion(tool.packageName) : void 0;
if (pinnedVersion)
lastManagedInstallVersion.delete(toolId);
const recordVersion = pinnedVersion ? (output) => {
const seen = extractVersionToken(output);
if (seen)
lastManagedInstallVersion.set(toolId, seen);
} : void 0;
const localBase = path3.join(TOOLS_DIR, "node_modules", ".bin", tool.binaryName || tool.id);
if (installerPlatform() === "win32") {
const cmdPath = `${localBase}.cmd`;
try {
await fs2.access(cmdPath);
if (await verifyToolBinary(cmdPath, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs, packageEntryVerification(tool))) {
return cmdPath;
}
logSessionStart(`auto-install verify: ${cmdPath} exists but is broken, will reinstall`);
} catch {
}
const exePath = `${localBase}.exe`;
try {
await fs2.access(exePath);
if (await verifyToolBinary(exePath, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs, packageEntryVerification(tool))) {
return exePath;
}
logSessionStart(`auto-install verify: ${exePath} exists but is broken, will reinstall`);
} catch {
}
}
if (installerPlatform() !== "win32") {
try {
await fs2.access(localBase);
if (await verifyToolBinary(localBase, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs, packageEntryVerification(tool))) {
return localBase;
}
logSessionStart(`auto-install verify: ${localBase} exists but is broken, will reinstall`);
} catch {
}
}
if (tool.platformPackage) {
const platformBin = resolvePlatformPackageBinary(tool);
if (platformBin && await verifyToolBinary(platformBin, void 0, onTransient, getToolVerificationTimeout(tool), tool.checkArgs)) {
logSessionStart(`auto-install ${toolId}: resolved platform-package binary at ${platformBin}`);
return platformBin;
}
logSessionStart(`auto-install ${toolId}: platform-package binary not resolved (${process.platform}-${process.arch}, base=${tool.platformPackage.base ?? tool.packageName}) \u2014 falling back to PATH/managed install`);
}
if (tool.installStrategy === "github" || tool.installStrategy === "maven" || tool.installStrategy === "archive") {
const githubPath = await findGitHubToolPath(tool.binaryName || tool.id);
if (githubPath)
return githubPath;
}
if (await isCommandAvailable(tool.checkCommand)) {
if (packageEntryVerification(tool) !== void 0)
return tool.checkCommand;
if (tool.verification === "tree-manifest")
return tool.checkCommand;
let probeStalled = false;
const verified = await verifyToolBinary(tool.checkCommand, void 0, () => {
probeStalled = true;
onTransient();
}, getToolVerificationTimeout(tool), tool.checkArgs, void 0, () => {
probeStalled = true;
});
if (verified || probeStalled)
return tool.checkCommand;
recordDegradationOnce({
kind: "installer-path-candidate-unrunnable",
subject: toolId,
reason: `${tool.checkCommand} on PATH failed its own check (${tool.checkArgs.join(" ")}); ignoring PATH for this tool`
});
logSessionStart(`auto-install ${toolId}: PATH candidate ${tool.checkCommand} failed ${tool.checkArgs.join(" ")} \u2014 ignoring PATH, trying managed install`);
}
if (tool.installStrategy === "npm") {
const npmPath = await findNpmGlobalToolPath(tool.binaryName || tool.id, onTransient, tool.checkArgs, getToolVerificationTimeout(tool));
if (npmPath) {
return npmPath;
}
}
if (tool.installStrategy === "pip") {
const pipPath = await findPipUserToolPath(tool.binaryName || tool.id, onTransient, tool.checkArgs, getToolVerificationTimeout(tool));
if (pipPath) {
return pipPath;
}
}
return void 0;
}
async function findGitHubToolPath(binaryName) {
const isWindows = process.platform === "win32";
const candidates = isWindows ? [
path3.join(GITHUB_BIN_DIR, `${binaryName}.exe`),
path3.join(GITHUB_BIN_DIR, `${binaryName}.bat`),
path3.join(GITHUB_BIN_DIR, `${binaryName}.cmd`),
path3.join(GITHUB_BIN_DIR, binaryName)
] : [path3.join(GITHUB_BIN_DIR, binaryName)];
for (const candidate of candidates) {
try {
await fs2.access(candidate);
return candidate;
} catch {
}
}
return void 0;
}
async function findManagedToolBinary(toolId) {
const tool = TOOLS.find((candidate) => candidate.id === toolId);
if (!tool)
return void 0;
if (tool.installStrategy !== "github" && tool.installStrategy !== "maven" && tool.installStrategy !== "archive") {
return void 0;
}
return findGitHubToolPath(tool.binaryName || tool.id);
}
function hasExecutableExtension(name) {
return /\.(exe|bat|cmd|ps1)$/i.test(name);
}
function getGitHubInstalledBinaryName(binaryName, platform, assetName) {
if (platform !== "win32")
return binaryName;
if (hasExecutableExtension(binaryName))
return binaryName;
if (assetName.endsWith(".bat"))
return `${binaryName}.bat`;
if (assetName.endsWith(".cmd"))
return `${binaryName}.cmd`;
return `${binaryName}.exe`;
}
function launcherForGitHubAsset(spec, assetName) {
if (spec.launcher === "php" && assetName.endsWith(".phar"))
return "php";
if (spec.launcher === "java" && assetName.endsWith(".jar"))
return "java";
return void 0;
}
function getArchiveBinaryCandidates(binaryName, platform, assetName) {
if (platform !== "win32")
return [binaryName];
if (hasExecutableExtension(binaryName))
return [binaryName];
const candidates = /* @__PURE__ */ new Set();
if (assetName.endsWith(".bat"))
candidates.add(`${binaryName}.bat`);
if (assetName.endsWith(".cmd"))
candidates.add(`${binaryName}.cmd`);
candidates.add(`${binaryName}.exe`);
candidates.add(binaryName);
candidates.add(`${binaryName}.bat`);
candidates.add(`${binaryName}.cmd`);
return [...candidates];
}
async function findNpmGlobalToolPath(binaryName, onTransient, verificationArgs = ["--version"], verificationTimeoutMs = 1e4) {
const isWindows = process.platform === "win32";
const binDirs = await getNpmGlobalBinCandidates(onTransient);
for (const dir of binDirs) {
const candidates = isWindows ? [
path3.join(dir, `${binaryName}.cmd`),
path3.join(dir, `${binaryName}.exe`)
] : [path3.join(dir, binaryName)];
for (const candidate of candidates) {
try {
await fs2.access(candidate);
if (await verifyToolBinary(candidate, void 0, onTransient, verificationTimeoutMs, verificationArgs)) {
return candidate;
}
} catch {
}
}
}
return void 0;
}
async function getNpmGlobalBinCandidates(onTransient) {
const dirs = [];
const seen = /* @__PURE__ */ new Set();
const add = (value) => {
if (!value)
return;
const normalized = path3.resolve(value.trim());
if (!normalized)
return;
if (seen.has(normalized))
return;
seen.add(normalized);
dirs.push(normalized);
};
if (process.platform === "win32") {
add(path3.join(process.env.APPDATA || "", "npm"));
} else {
add(path3.join(os.homedir(), ".npm-global", "bin"));
}
for (const dir of await allAvailableGlobalBinDirs(onTransient)) {
add(dir);
}
return dirs;
}
async function findPipUserToolPath(binaryName, onTransient, verificationArgs = ["--version"], verificationTimeoutMs = 1e4) {
const isWindows = installerPlatform() === "win32";
const userBaseCandidates = [
path3.join(getGlobalPiLensDir(), "pip-tools"),
path3.join(getGlobalPiLensDir(), "pip-user"),
...await getPythonUserBaseCandidates()
];
for (const userBase of userBaseCandidates) {
const scriptDirs = [pipScriptsDir(userBase, installerPlatform())];
if (isWindows) {
try {
const children = await fs2.readdir(userBase, { withFileTypes: true });
for (const entry of children) {
if (!entry.isDirectory())
continue;
if (!/^python\d+$/i.test(entry.name))
continue;
scriptDirs.push(path3.join(userBase, entry.name, "Scripts"));
}
} catch {
}
}
for (const dir of scriptDirs) {
const candidates = isWindows ? [
path3.join(dir, `${binaryName}.exe`),
path3.join(dir, `${binaryName}.cmd`),
path3.join(dir, binaryName)
] : [path3.join(dir, binaryName)];
for (const candidate of candidates) {
try {
await fs2.access(candidate);
if (await verifyToolBinary(candidate, void 0, onTransient, verificationTimeoutMs, verificationArgs)) {
return candidate;
}
} catch {
}
}
}
}
return void 0;
}
function userBaseProbeResult(command, code, stdout) {
if (stdout.truncated) {
recordDegradationOnce({
kind: "spawn-output-cap-truncated",
subject: `user-base-probe:${command}`,
reason: `\`${command} -m site --user-base\` reached the ${DEFAULT_MAX_OUTPUT_BYTES}-byte default cap after ${stdout.observedBytes} bytes; the probed user base is unusable`,
metadata: {
capBytes: DEFAULT_MAX_OUTPUT_BYTES,
capSource: "default",
observedBytes: stdout.observedBytes,
killed: false
}
});
return "";
}
return code === 0 ? stdout.text.trim() : "";
}
async function getPythonUserBaseCandidates() {
const candidates = [];
const seen = /* @__PURE__ */ new Set();
const add = (value) => {
if (!value)
return;
const normalized = value.trim();
if (!normalized)
return;
if (seen.has(normalized))
return;
seen.add(normalized);
candidates.push(normalized);
};
if (process.platform === "win32") {
add(path3.join(process.env.APPDATA || "", "Python"));
}
const probes = process.platform === "win32" ? [
{ command: "py", args: ["-m", "site", "--user-base"] },
{ command: "python", args: ["-m", "site", "--user-base"] }
] : [
{ command: "python3", args: ["-m", "site", "--user-base"] },
{ command: "python", args: ["-m", "site", "--user-base"] }
];
for (const probe of probes) {
const userBase = await new Promise((resolve) => {
const isWin = process.platform === "win32";
const spawnCmd = isWin ? [probe.command, ...probe.args].join(" ") : probe.command;
let proc;
try {
proc = spawn(spawnCmd, isWin ? [] : probe.args, {
stdio: ["ignore", "pipe", "pipe"],
shell: isWin
});
} catch {
resolve("");
return;
}
const stdout = createBoundedOutputSink();
proc.stdout?.on("data", (data) => stdout.append(data));
proc.on("exit", (code) => resolve(userBaseProbeResult(probe.command, code, stdout)));
proc.on("error", () => resolve(""));
});
add(userBase);
}
return candidates;
}
async function githubApiAuthHeaders() {
const token = await resolveGitHubToken();
return token ? { Authorization: `Bearer ${token}` } : {};
}
var HttpStatusError = class extends Error {
statusCode;
constructor(statusCode, url) {
super(url ? `HTTP ${statusCode} for ${url}` : `HTTP ${statusCode}`);
this.name = "HttpStatusError";
this.statusCode = statusCode;
}
};
var GitHubHttpError = class extends HttpStatusError {
anonymousRateLimitExhausted;
constructor(statusCode, requestHeaders, responseHeaders) {
super(statusCode);
this.name = "GitHubHttpError";
this.message = `GitHub API HTTP ${statusCode}`;
this.anonymousRateLimitExhausted = statusCode === 403 && !Object.keys(requestHeaders).some((key) => key.toLowerCase() === "authorization") && readHeader(responseHeaders, "x-ratelimit-remaining") === "0";
}
};
function readHeader(headers, name) {
const entry = Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1];
return Array.isArray(entry) ? entry[0]?.trim() : entry?.trim();
}
function isGitHubApiUrl(url) {
try {
return new URL(url).host.toLowerCase() === "api.github.com";
} catch {
return false;
}
}
function sameHost(a, b) {
try {
return new URL(a).host === new URL(b).host;
} catch {
return false;
}
}
function httpsGetWithMeta(url, maxRedirects = 5, headers = {}) {
return new Promise((resolve, reject) => {
https.get(url, { headers: { "User-Agent": "pi-lens/1.0", ...headers } }, (res) => {
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
if (maxRedirects === 0)
return reject(new Error("Too many redirects"));
const location = res.headers.location;
const nextHeaders = sameHost(url, location) ? headers : (() => {
const { Authorization: _drop, ...rest } = headers;
return rest;
})();
return resolve(httpsGetWithMeta(location, maxRedirects - 1, nextHeaders));
}
const etag = typeof res.headers.etag === "string" ? res.headers.etag : void 0;
if (res.statusCode === 304) {
res.resume();
return resolve({ statusCode: 304, body: Buffer.alloc(0), etag });
}
if (res.statusCode !== 200) {
res.resume();
return reject(isGitHubApiUrl(url) ? new GitHubHttpError(res.statusCode ?? 0, headers, res.headers) : new HttpStatusError(res.statusCode ?? 0, url));
}
const chunks = [];
res.on("data", (chunk) => chunks.push(chunk));
res.on("end", () => resolve({ statusCode: 200, body: Buffer.concat(chunks), etag }));
res.on("error", reject);
}).on("error", reject);
});
}
async function httpsGet(url, maxRedirects = 5, headers = {}) {
const response = await httpsGetWithMeta(url, maxRedirects, headers);
if (response.statusCode !== 200) {
throw new Error(`HTTP ${response.statusCode} for ${url}`);
}
return response.body;
}
function runCommand(command, args, cwd) {
return new Promise((resolve) => {
let proc;
try {
proc = spawn(command, args, {
cwd,
stdio: "ignore",
shell: process.platform === "win32"
});
} catch {
resolve(false);
return;
}
proc.on("exit", (code) => resolve(code === 0));
proc.on("error", () => resolve(false));
});
}
async function installGitHubTool(tool, prefetchedRelease) {
const spec = tool.github;
if (!spec)
return void 0;
const platform = process.platform;
const arch = process.arch;
const assetSubstring = spec.assetMatch(platform, arch);
if (!assetSubstring) {
logSessionStart(`github-install ${tool.id}: unsupported platform=${platform} arch=${arch}`);
return void 0;
}
let releaseJson;
if (prefetchedRelease) {
releaseJson = prefetchedRelease;
logSessionStart(`github-install ${tool.id}: using caller-supplied release metadata for ${spec.repo} (${prefetchedRelease.tag_name ?? "untagged"})`);
} else {
logSessionStart(`github-install ${tool.id}: fetching release metadata from ${spec.repo}`);
try {
const body = await httpsGet(`https://api.github.com/repos/${spec.repo}/releases/latest`, 5, await githubApiAuthHeaders());
releaseJson = JSON.parse(body.toString("utf8"));
} catch (err) {
const reason = err instanceof GitHubHttpError && err.anonymousRateLimitExhausted ? "GitHub API rate limit exhausted; authenticate with `gh auth login` or set GITHUB_TOKEN/GH_TOKEN and retry" : `release fetch failed: ${err.message}`;
if (err instanceof GitHubHttpError && err.anonymousRateLimitExhausted) {
recordDegradationOnce({
kind: "github-api-rate-limit",
subject: tool.id,
reason
});
}
logSessionStart(`github-install ${tool.id}: ${reason}`);
return void 0;
}
}
if (releaseJson.tag_name) {
lastInstallResolutionId.set(tool.id, releaseJson.tag_name);
}
if (!Array.isArray(releaseJson.assets)) {
logSessionStart(`github-install ${tool.id}: release metadata has no assets array`);
return void 0;
}
const asset = pickReleaseAsset(releaseJson.assets, assetSubstring) ?? deriveHashiCorpReleaseAsset(tool, releaseJson.tag_name, assetSubstring);
if (!asset) {
logSessionStart(`github-install ${tool.id}: no asset matched "${assetSubstring}"`);
return void 0;
}
logSessionStart(`github-install ${tool.id}: downloading ${asset.name}`);
debugLog(`[github] downloading ${asset.name} from ${asset.browser_download_url}`);
const downloadStart = Date.now();
let assetBuffer;
try {
assetBuffer = await httpsGet(asset.browser_download_url);
logSessionStart(`github-install ${tool.id}: downloaded ${asset.name} (${assetBuffer.length} bytes, ${Date.now() - downloadStart}ms)`);
} catch (err) {
logSessionStart(`github-install ${tool.id}: download failed: ${err.message}`);
return void 0;
}
await fs2.mkdir(GITHUB_BIN_DIR, { recursive: true });
const binaryName = tool.binaryName ?? tool.id;
const isWindows = platform === "win32";
const finalBinaryName = getGitHubInstalledBinaryName(binaryName, platform, asset.name);
let destPath = path3.join(GITHUB_BIN_DIR, finalBinaryName);
const assetName = asset.name;
const launcherRuntime = launcherForGitHubAsset(spec, assetName);
if (launcherRuntime && !await isCommandAvailable(launcherRuntime, ["--version"])) {
logSessionStart(`github-install ${tool.id}: ${launcherRuntime} not found \u2014 asset requires a runtime launcher`);
return void 0;
}
try {
if (launcherRuntime) {
const assetPath = path3.join(GITHUB_BIN_DIR, `${tool.id}${assetName.endsWith(".phar") ? ".phar" : ".jar"}`);
await writeFileAtomicAsync(assetPath, assetBuffer, {
bestEffort: false,
mode: 488
});
const launcherName = isWindows ? `${binaryName}.bat` : binaryName;
destPath = path3.join(GITHUB_BIN_DIR, launcherName);
const runtimeTarget = assetName.endsWith(".phar") ? `${tool.id}.phar` : `${tool.id}.jar`;
const command = isWindows ? `@echo off\r
${launcherRuntime} "%~dp0${runtimeTarget}" %*\r
` : `#!/bin/sh
exec ${launcherRuntime} "$(dirname "$0")/${runtimeTarget}" "$@"
`;
await writeFileAtomicAsync(destPath, command, {
bestEffort: false,
mode: isWindows ? void 0 : 488
});
} else if (assetName.endsWith(".gz") && !assetName.endsWith(".tar.gz")) {
const decompressed = await new Promise((resolve, reject) => {
const gunzip = createGunzip();
const chunks = [];
gunzip.on("data", (chunk) => chunks.push(chunk));
gunzip.on("end", () => resolve(Buffer.concat(chunks)));
gunzip.on("error", reject);
gunzip.end(assetBuffer);
});
await writeFileAtomicAsync(destPath, decompressed, {
bestEffort: false,
mode: 488
});
} else if (assetName.endsWith(".tar.gz") || assetName.endsWith(".tar.xz")) {
const tmpArchive = path3.join(GITHUB_BIN_DIR, `_tmp_${assetName}`);
await fs2.writeFile(tmpArchive, assetBuffer);
const tmpDir = path3.join(GITHUB_BIN_DIR, `_tmp_extract_${tool.id}`);
await fs2.mkdir(tmpDir, { recursive: true });
const extracted = await runCommand("tar", ["xf", tmpArchive, "-C", tmpDir], GITHUB_BIN_DIR);
await fs2.rm(tmpArchive, { force: true });
if (!extracted) {
await fs2.rm(tmpDir, { recursive: true, force: true });
logSessionStart(`github-install ${tool.id}: tar extraction failed for ${assetName}`);
return void 0;
}
const tarBinaryName = spec.binaryInArchive ?? binaryName;
const tarSrcBinary = await findFirstFileRecursive(tmpDir, getArchiveBinaryCandidates(tarBinaryName, platform, assetName));
if (!tarSrcBinary) {
await fs2.rm(tmpDir, { recursive: true, force: true });
logSessionStart(`github-install ${tool.id}: binary candidates ${JSON.stringify(getArchiveBinaryCandidates(tarBinaryName, platform, assetName))} not found in tar ${assetName}`);
return void 0;
}
await fs2.rename(tarSrcBinary, destPath);
await fs2.rm(tmpDir, { recursive: true, force: true });
if (!isWindows)
await fs2.chmod(destPath, 488);
} else if (assetName.endsWith(".zip")) {
const tmpArchive = path3.join(GITHUB_BIN_DIR, `_tmp_${assetName}`);
await fs2.writeFile(tmpArchive, assetBuffer);
const tmpDir = path3.join(GITHUB_BIN_DIR, `_tmp_extract_${tool.id}`);
await fs2.mkdir(tmpDir, { recursive: true });
const extracted = isWindows ? await runCommand("powershell", [
"-NoProfile",
"-Command",
`Expand-Archive -LiteralPath '${tmpArchive}' -DestinationPath '${tmpDir}' -Force`
], GITHUB_BIN_DIR) : await runCommand("unzip", ["-q", "-o", tmpArchive, "-d", tmpDir], GITHUB_BIN_DIR);
await fs2.rm(tmpArchive, { force: true });
if (!extracted) {
await fs2.rm(tmpDir, { recursive: true, force: true });
logSessionStart(`github-install ${tool.id}: zip extraction failed for ${assetName}`);
return void 0;
}
const archiveBinaryName = spec.binaryInArchive ?? binaryName;
const srcBinary = await findFirstFileRecursive(tmpDir, getArchiveBinaryCandidates(archiveBinaryName, platform, assetName));
if (!srcBinary) {
await fs2.rm(tmpDir, { recursive: true, force: true });
logSessionStart(`github-install ${tool.id}: binary candidates ${JSON.stringify(getArchiveBinaryCandidates(archiveBinaryName, platform, assetName))} not found in zip ${assetName}`);
return void 0;
}
await fs2.rename(srcBinary, destPath);
await fs2.rm(tmpDir, { recursive: true, force: true });
if (!isWindows)
await fs2.chmod(destPath, 488);
} else {
await writeFileAtomicAsync(destPath, assetBuffer, {
bestEffort: false,
mode: 488
});
}
} catch (err) {
logSessionStart(`github-install ${tool.id}: install failed: ${err.message}`);
return void 0;
}
for (const extraName of spec.extraAssets?.(platform, arch) ?? []) {
const extraAsset = releaseJson.assets.find((a) => a.name === extraName);
if (!extraAsset) {
logSessionStart(`github-install ${tool.id}: required extra asset "${extraName}" not found`);
return void 0;
}
try {
const extraBuffer = await httpsGet(extraAsset.browser_download_url);
await writeFileAtomicAsync(path3.join(GITHUB_BIN_DIR, extraName), extraBuffer, { bestEffort: false, mode: 488 });
logSessionStart(`github-install ${tool.id}: installed extra asset ${extraName} (${extraBuffer.length} bytes)`);
} catch (err) {
logSessionStart(`github-install ${tool.id}: extra asset ${extraName} download failed: ${err.message}`);
return void 0;
}
}
debugLog(`[github] installed ${tool.name} \u2192 ${destPath}`);
logSessionStart(`github-install ${tool.id}: installed \u2192 ${destPath}`);
return destPath;
}
async function findFirstFileRecursive(dir, names) {
const wanted = new Set(names.map((name) => name.toLowerCase()));
const entries = await fs2.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const full = path3.join(dir, entry.name);
if (entry.isDirectory()) {
const found = await findFirstFileRecursive(full, names);
if (found)
return found;
} else if (wanted.has(entry.name.toLowerCase())) {
return full;
}
}
return void 0;
}
var NEEDS_POSTINSTALL = /* @__PURE__ */ new Set([
"@biomejs/biome",
"@ast-grep/cli",
// postinstall copies platform binary (ast-grep.exe/sg.exe) into place
"@ast-grep/napi",
"esbuild",
"intelephense"
// postinstall fetches platform binary; --ignore-scripts breaks install
]);
function npmToolNeedsPostinstall(packageName) {
return NEEDS_POSTINSTALL.has(packageName);
}
function getRefreshableManagedNpmTools() {
const seenPackages = /* @__PURE__ */ new Set();
return getRefreshableManagedTools().filter((candidate) => candidate.strategy === "npm").map((candidate) => ({
toolId: candidate.toolId,
// The npm branch of getRefreshableManagedTools only admits entries that
// have both, so these are total.
packageName: candidate.packageName,
binaryName: candidate.binaryName
})).filter((candidate) => {
if (seenPackages.has(candidate.packageName))
return false;
seenPackages.add(candidate.packageName);
return true;
});
}
function mavenCoordinate(spec) {
return [
spec.repoBaseUrl ?? MAVEN_CENTRAL_BASE,
spec.groupId,
spec.artifactId,
spec.version,
spec.classifier ?? ""
].join(":");
}
function getRefreshableManagedTools() {
const refreshable = [];
for (const tool of TOOLS) {
switch (tool.installStrategy) {
case "npm": {
if (!tool.packageName)
continue;
if (parsePinnedVersion(tool.packageName) !== void 0)
continue;
if (!tool.binaryName)
continue;
refreshable.push({
toolId: tool.id,
strategy: "npm",
checkArgs: tool.checkArgs,
packageName: tool.packageName,
binaryName: tool.binaryName,
verificationTimeoutMs: tool.verificationTimeoutMs,
packageEntryOf: packageEntryVerification(tool)
});
break;
}
case "pip":
case "gem": {
if (!tool.packageName)
continue;
refreshable.push({
toolId: tool.id,
strategy: tool.installStrategy,
checkArgs: tool.checkArgs,
packageName: tool.packageName
});
break;
}
case "github": {
if (!tool.github)
continue;
refreshable.push({
toolId: tool.id,
strategy: "github",
checkArgs: tool.checkArgs
});
break;
}
case "maven": {
if (!tool.maven)
continue;
refreshable.push({
toolId: tool.id,
strategy: "maven",
checkArgs: tool.checkArgs,
pinnedCoordinate: mavenCoordinate(tool.maven)
});
break;
}
case "archive": {
if (!tool.archive)
continue;
const url = resolveArchiveUrl(tool.archive);
if (!url)
continue;
refreshable.push({
toolId: tool.id,
strategy: "archive",
checkArgs: tool.checkArgs,
pinnedCoordinate: url
});
break;
}
}
}
return refreshable;
}
async function isManagedToolPresent(toolId) {
const tool = TOOLS.find((t) => t.id === toolId);
if (!tool)
return false;
switch (tool.installStrategy) {
case "npm": {
if (!tool.packageName)
return false;
try {
await fs2.access(path3.join(TOOLS_DIR, "node_modules", tool.packageName, "package.json"));
return true;
} catch {
return false;
}
}
case "github":
case "maven":
return await findGitHubToolPath(tool.binaryName ?? tool.id) !== void 0;
case "archive":
return await findGitHubToolPath(tool.binaryName ?? tool.id) !== void 0 || await getArchiveTreeBundlePath(tool) !== void 0;
case "pip":
case "gem": {
const cached = (await readProbeCache())[toolId];
return cached?.path !== void 0 && existsSync(cached.path);
}
default:
return false;
}
}
async function probeManagedToolVersion(tool) {
const cached = (await readProbeCache())[tool.id];
if (!cached?.path || !existsSync(cached.path))
return void 0;
try {
const result = await probeToolAsync(cached.path, tool.checkArgs, {
timeout: getToolVerificationTimeout(tool),
input: "",
ignoreAmbientSignal: true,
resourceLabel: `tool-refresh-version:${tool.id}`
});
if (result.status !== 0)
return void 0;
return extractVersionToken(`${result.stdout}
${result.stderr}`);
} catch {
return void 0;
}
}
async function acquireManagedInstallGate(context) {
if (process.env.PI_LENS_DISABLE_TOOL_INSTALL === "1") {
return {
ok: false,
reason: "installation disabled by PI_LENS_DISABLE_TOOL_INSTALL=1"
};
}
if (!assertInstallAllowed(context)) {
return {
ok: false,
reason: `project trust: ${projectTrustDenialReason()}`
};
}
const lock = await acquireInstallLock();
if (!lock.release) {
return { ok: false, reason: lock.reason ?? "install lock held" };
}
return { ok: true, release: lock.release };
}
async function refreshManagedTool(toolId, known = {}) {
const tool = TOOLS.find((t) => t.id === toolId);
if (!tool) {
return { ok: false, unchanged: true, reason: "unknown tool id" };
}
if (tool.installStrategy !== "github" && tool.installStrategy !== "pip" && tool.installStrategy !== "gem" && tool.installStrategy !== "maven" && tool.installStrategy !== "archive") {
return {
ok: false,
unchanged: true,
reason: `strategy ${tool.installStrategy} is not refreshable here`
};
}
const strategy = tool.installStrategy;
const gate = await acquireManagedInstallGate(`managed tool refresh: ${toolId}`);
if (!gate.ok) {
return { ok: false, unchanged: true, declined: true, reason: gate.reason };
}
try {
switch (strategy) {
case "github":
return await refreshGitHubManagedTool(tool, known);
case "pip":
case "gem":
return await refreshPackageManagerManagedTool(tool);
case "maven":
case "archive":
return await refreshPinnedManagedTool(tool, known);
default:
return {
ok: false,
unchanged: true,
reason: `strategy ${strategy} is not refreshable here`
};
}
} finally {
await gate.release?.();
}
}
async function refreshGitHubManagedTool(tool, known) {
const spec = tool.github;
if (!spec)
return { ok: false, unchanged: true, reason: "no github spec" };
let response;
try {
response = await httpsGetWithMeta(`https://api.github.com/repos/${spec.repo}/releases/latest`, 5, {
...await githubApiAuthHeaders(),
...known.etag ? { "If-None-Match": known.etag } : {}
});
} catch (err) {
const reason = err instanceof GitHubHttpError && err.anonymousRateLimitExhausted ? "GitHub API rate limit exhausted; authenticate with `gh auth login` or set GITHUB_TOKEN/GH_TOKEN and retry" : `release query failed: ${err.message}`;
if (err instanceof GitHubHttpError && err.anonymousRateLimitExhausted) {
recordDegradationOnce({
kind: "github-api-rate-limit",
subject: tool.id,
reason
});
}
return {
ok: false,
unchanged: true,
reason
};
}
if (response.statusCode === 304) {
return {
ok: true,
unchanged: true,
resolutionId: known.resolutionId,
etag: known.etag,
version: known.version
};
}
let release;
try {
const parsed = JSON.parse(response.body.toString("utf8"));
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("release metadata is not an object");
}
release = parsed;
} catch (err) {
return {
ok: false,
unchanged: true,
reason: `release metadata unparseable: ${err.message}`
};
}
const tag = release.tag_name;
if (!tag) {
return { ok: false, unchanged: true, reason: "release has no tag_name" };
}
if (known.resolutionId === tag) {
return {
ok: true,
unchanged: true,
resolutionId: tag,
etag: response.etag ?? known.etag,
version: known.version ?? tag
};
}
const installed = await installGitHubTool(tool, release);
if (!installed) {
return {
ok: false,
unchanged: true,
// Keep the ETag off the failure stamp: replaying it would make the next
// attempt a 304 and skip the download this one never completed.
resolutionId: known.resolutionId,
version: known.version,
reason: `install from release ${tag} failed`
};
}
const verified = await verifyRefreshedArtifact(tool, installed);
if (!verified) {
return {
ok: false,
unchanged: false,
resolutionId: known.resolutionId,
version: known.version,
reason: `release ${tag} installed but its binary does not run`
};
}
return {
ok: true,
unchanged: false,
resolutionId: tag,
etag: response.etag,
version: tag
};
}
async function verifyRefreshedArtifact(tool, installedPath) {
if (tool.installStrategy === "archive" && (!tool.archive?.launcher || tool.verification === "tree-manifest")) {
await updateProbeCache(tool.id, installedPath);
return true;
}
if (!await verifyToolBinary(installedPath, void 0, void 0, getToolVerificationTimeout(tool), tool.checkArgs)) {
logSessionStart(`managed-tool-refresh ${tool.id}: refreshed artifact at ${installedPath} failed its verification check`);
return false;
}
await updateProbeCache(tool.id, installedPath);
return true;
}
async function refreshPackageManagerManagedTool(tool) {
if (!tool.packageName) {
return { ok: false, unchanged: true, reason: "no package name" };
}
const previous = await probeManagedToolVersion(tool);
const installed = tool.installStrategy === "pip" ? (
// `-U` is the whole fix: without it pip treats the installed copy as
// satisfying the requirement and the day-one version never moves.
await installPipTool(tool.id, tool.packageName, tool.binaryName ?? tool.id, {
upgrade: true
})
) : (
// `gem install` always fetches the newest version that satisfies the
// requirement, so the install command IS the upgrade command.
await installGemTool(tool.id, tool.packageName)
);
if (!installed) {
return {
ok: false,
unchanged: true,
version: previous,
reason: `${tool.installStrategy} upgrade failed`
};
}
const current = await probeManagedToolVersion(tool);
if (previous !== void 0 && current === void 0) {
return {
ok: false,
unchanged: false,
version: previous,
reason: `${tool.installStrategy} upgrade left the binary unable to report a version`
};
}
return {
ok: true,
// An unreadable version on both sides cannot prove a move, so it reads as
// unchanged rather than inventing one.
unchanged: previous === void 0 || current === void 0 || previous === current,
version: current ?? previous
};
}
async function refreshPinnedManagedTool(tool, known) {
const coordinate = tool.installStrategy === "maven" ? tool.maven && mavenCoordinate(tool.maven) : tool.archive && resolveArchiveUrl(tool.archive);
if (!coordinate) {
return {
ok: false,
unchanged: true,
reason: "no coordinate for this platform"
};
}
if (known.resolutionId === coordinate) {
return {
ok: true,
unchanged: true,
resolutionId: coordinate,
version: known.version
};
}
const installed = tool.installStrategy === "maven" ? await installMavenTool(tool) : await installArchiveTool(tool);
if (!installed) {
return {
ok: false,
unchanged: true,
resolutionId: known.resolutionId,
version: known.version,
reason: `reinstall from ${coordinate} failed`
};
}
if (!await verifyRefreshedArtifact(tool, installed)) {
return {
ok: false,
unchanged: false,
resolutionId: known.resolutionId,
version: known.version,
reason: `reinstall from ${coordinate} produced an artifact that does not run`
};
}
return {
ok: true,
// A first-ever stamp is an adoption, not a version move: the coordinate did
// not change, pi-lens simply had no record of it. Reporting it as a move
// would put a false "x → y" row in the log.
unchanged: known.resolutionId === void 0,
resolutionId: coordinate,
version: coordinate
};
}
function resolveManagedNpmBinPath(binaryName) {
const binBase = path3.join(TOOLS_DIR, "node_modules", ".bin", binaryName);
return installerPlatform() === "win32" ? `${binBase}.cmd` : binBase;
}
function invalidateManagedToolResolution(toolId) {
resolvedPathCache.delete(toolId);
if (_probeCache !== null)
delete _probeCache[toolId];
markProbeCacheChange(toolId, null);
}
var MAVEN_CENTRAL_BASE = "https://repo1.maven.org/maven2";
async function installMavenTool(tool) {
const spec = tool.maven;
if (!spec)
return void 0;
const binaryName = tool.binaryName ?? tool.id;
const isWindows = process.platform === "win32";
if (!await isCommandAvailable("java", ["-version"])) {
logSessionStart(`maven-install ${tool.id}: java not found \u2014 a JAR tool can't run without a JRE`);
return void 0;
}
let base = spec.repoBaseUrl ?? MAVEN_CENTRAL_BASE;
while (base.endsWith("/"))
base = base.slice(0, -1);
const groupPath = spec.groupId.replace(/\./g, "/");
const jarFile = `${spec.artifactId}-${spec.version}${spec.classifier ? `-${spec.classifier}` : ""}.jar`;
const url = `${base}/${groupPath}/${spec.artifactId}/${spec.version}/${jarFile}`;
lastInstallResolutionId.set(tool.id, mavenCoordinate(spec));
logSessionStart(`maven-install ${tool.id}: downloading ${url}`);
let jarBuffer;
try {
jarBuffer = await httpsGet(url);
} catch (err) {
logSessionStart(`maven-install ${tool.id}: download failed: ${err.message}`);
return void 0;
}
try {
await fs2.mkdir(GITHUB_BIN_DIR, { recursive: true });
const jarPath = path3.join(GITHUB_BIN_DIR, `${tool.id}.jar`);
await writeFileAtomicAsync(jarPath, jarBuffer, { bestEffort: false });
const launcherName = isWindows ? `${binaryName}.bat` : binaryName;
const launcherPath = path3.join(GITHUB_BIN_DIR, launcherName);
if (isWindows) {
await writeFileAtomicAsync(launcherPath, `@echo off\r
java -jar "%~dp0${tool.id}.jar" %*\r
`, { bestEffort: false });
} else {
await writeFileAtomicAsync(launcherPath, `#!/bin/sh
exec java -jar "$(dirname "$0")/${tool.id}.jar" "$@"
`, { bestEffort: false, mode: 488 });
}
logSessionStart(`maven-install ${tool.id}: installed \u2192 ${launcherPath} (${jarBuffer.length} bytes)`);
debugLog(`[maven] installed ${tool.name} \u2192 ${launcherPath}`);
return launcherPath;
} catch (err) {
logSessionStart(`maven-install ${tool.id}: install failed: ${err.message}`);
return void 0;
}
}
function resolveArchiveUrl(spec, platform = installerPlatform(), arch = process.arch) {
return typeof spec.url === "function" ? spec.url(platform, arch) : spec.url;
}
function resolveArchiveKind(spec, platform = installerPlatform(), arch = process.arch) {
return typeof spec.kind === "function" ? spec.kind(platform, arch) : spec.kind;
}
function recordArchiveExtractionDegradation(toolId, format, reason) {
recordDegradationOnce({
kind: "managed-tool-install",
subject: `${toolId}:${format}`,
reason: `archive extraction ${reason}`
});
}
async function stripExtractedArchiveRoot(dir, components) {
for (let i = 0; i < components; i++) {
const entries = await fs2.readdir(dir, { withFileTypes: true });
const [entry] = entries;
if (entries.length !== 1 || !entry?.isDirectory())
return false;
const root = path3.join(dir, entry.name);
for (const child of await fs2.readdir(root))
await fs2.rename(path3.join(root, child), path3.join(dir, child));
await fs2.rm(root, { recursive: true, force: true });
}
return true;
}
async function swapExtractedDir(toolId, tmpDir, finalDir) {
const backupDir = `${finalDir}.rollback`;
await fs2.rm(backupDir, { recursive: true, force: true });
let hadPrevious = false;
try {
await fs2.rename(finalDir, backupDir);
hadPrevious = true;
} catch (err) {
if (err.code !== "ENOENT")
throw err;
}
try {
await fs2.rename(tmpDir, finalDir);
} catch (err) {
if (hadPrevious) {
try {
await fs2.rename(backupDir, finalDir);
} catch (rollbackErr) {
recordDegradationOnce({
kind: "managed-tool-refresh",
subject: toolId,
reason: `swap rollback failed after a failed install: working copy orphaned at ${backupDir} (${rollbackErr.message})`
});
logSessionStart(`archive-install ${toolId}: swap rollback failed \u2014 working copy orphaned at ${backupDir} (${rollbackErr.message})`);
}
}
throw err;
}
if (hadPrevious) {
await fs2.rm(backupDir, { recursive: true, force: true }).catch(() => {
});
}
}
var ARCHIVE_RUNTIME_HOMES = {
java: { env: "JAVA_HOME", dir: "bin" }
};
function runtimeHomeVerdict(runtime) {
const home = ARCHIVE_RUNTIME_HOMES[runtime];
const root = home ? process.env[home.env] : void 0;
if (!home || !root)
return void 0;
const windows = installerPlatform() === "win32";
const names = windows ? [`${runtime}.exe`, runtime] : [runtime];
return names.some((name) => {
try {
const stat = statSync(path3.join(root, home.dir, name));
return stat.isFile() && stat.size > 0 && (windows || (stat.mode & 73) !== 0);
} catch {
return false;
}
});
}
async function installArchiveTool(tool) {
const spec = tool.archive;
if (!spec)
return void 0;
const binaryName = tool.binaryName ?? tool.id;
const platform = installerPlatform();
const isWindows = platform === "win32";
const archiveKind = resolveArchiveKind(spec, platform, process.arch);
const url = resolveArchiveUrl(spec, platform, process.arch);
if (!url) {
logSessionStart(`archive-install ${tool.id}: no archive for ${process.platform}/${process.arch} \u2014 unsupported, skipping`);
return void 0;
}
lastInstallResolutionId.set(tool.id, url);
logSessionStart(`archive-install ${tool.id}: downloading ${url}`);
let archiveBuffer;
try {
archiveBuffer = await httpsGet(url);
} catch (err) {
logSessionStart(`archive-install ${tool.id}: download failed: ${err.message}`);
return void 0;
}
const pinned = spec.sha256?.[url];
const digest = createHash("sha256").update(archiveBuffer).digest("hex");
if (digest !== pinned) {
const reason = pinned === void 0 ? "sha256-unpinned" : "sha256-mismatch";
recordDegradationOnce({
kind: "managed-tool-install",
subject: `${tool.id}:${archiveKind}:integrity`,
reason: `archive integrity ${reason}`
});
installFailureReasons.set(tool.id, `archive integrity ${reason} (${archiveBuffer.length} bytes from ${url})`);
logSessionStart(`archive-install ${tool.id}: refused ${reason} (got ${digest}, pinned ${pinned ?? "none"}) \u2014 keeping installed version`);
return void 0;
}
const extractName = tool.id;
const tmpExtractName = `${extractName}.refresh-tmp`;
const archiveName = `${tool.id}.download.${archiveKind === "zip" ? "zip" : "tgz"}`;
const extractDir = path3.join(TOOLS_DIR, extractName);
const tmpExtractDir = path3.join(TOOLS_DIR, tmpExtractName);
const tmpArchive = path3.join(TOOLS_DIR, archiveName);
try {
await fs2.mkdir(TOOLS_DIR, { recursive: true });
await fs2.rm(tmpExtractDir, { recursive: true, force: true });
await fs2.mkdir(tmpExtractDir, { recursive: true });
await fs2.writeFile(tmpArchive, archiveBuffer);
const stripComponents = spec.stripComponents ?? 1;
const extractionArgs = archiveKind === "zip" && isWindows ? [
"-NoProfile",
"-Command",
`Expand-Archive -LiteralPath '${archiveName}' -DestinationPath '${tmpExtractName}' -Force`
] : archiveKind === "zip" ? ["-q", "-o", archiveName, "-d", tmpExtractName] : [
"-xzf",
archiveName,
"-C",
tmpExtractName,
...stripComponents > 0 ? [`--strip-components=${stripComponents}`] : []
];
const extractor = archiveKind === "zip" && isWindows ? `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe` : archiveKind === "zip" ? "unzip" : isWindows ? `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\tar.exe` : "tar";
const extractionResult = await safeSpawnAsync(extractor, extractionArgs, {
cwd: TOOLS_DIR,
timeout: 12e4,
ignoreAmbientSignal: true,
lifetimeCoupled: true
});
const extracted = {
ok: extractionResult.status === 0,
reason: extractionResult.spawnFailure?.kind === "tool-not-found" ? "extractor-unavailable" : extractionResult.spawnFailure ? "extractor-error" : "extractor-exit"
};
await fs2.rm(tmpArchive, { force: true });
if (!extracted.ok) {
recordArchiveExtractionDegradation(tool.id, archiveKind, extracted.reason);
logSessionStart(`archive-install ${tool.id}: ${archiveKind} extraction failed (${extracted.reason}) \u2014 keeping installed version`);
await fs2.rm(tmpExtractDir, { recursive: true, force: true }).catch(() => {
});
return void 0;
}
if (archiveKind === "zip" && !await stripExtractedArchiveRoot(tmpExtractDir, stripComponents)) {
recordArchiveExtractionDegradation(tool.id, archiveKind, "layout-invalid");
logSessionStart(`archive-install ${tool.id}: ${archiveKind} layout invalid \u2014 keeping installed version`);
await fs2.rm(tmpExtractDir, { recursive: true, force: true }).catch(() => {
});
return void 0;
}
if (!spec.launcher) {
const tmpMarker = spec.treeMarker ? path3.join(tmpExtractDir, ...spec.treeMarker.split("/")) : tmpExtractDir;
try {
await fs2.access(tmpMarker);
} catch {
recordArchiveExtractionDegradation(tool.id, archiveKind, "marker-missing");
logSessionStart(`archive-install ${tool.id}: ${archiveKind} marker missing after extraction \u2014 keeping installed version`);
await fs2.rm(tmpExtractDir, { recursive: true, force: true }).catch(() => {
});
return void 0;
}
await swapExtractedDir(tool.id, tmpExtractDir, extractDir);
logSessionStart(`archive-install ${tool.id}: installed tree bundle \u2192 ${extractDir} (extracted ${archiveBuffer.length} bytes, sha256 verified ${digest})`);
debugLog(`[archive] installed ${tool.name} bundle \u2192 ${extractDir}`);
return extractDir;
}
const launcherParts = spec.launcher.split("/").map((p) => p);
const tmpInnerLauncher = path3.join(tmpExtractDir, ...launcherParts);
const tmpResolvedInner = isWindows ? `${tmpInnerLauncher}.bat` : tmpInnerLauncher;
try {
await fs2.access(tmpResolvedInner);
} catch {
recordArchiveExtractionDegradation(tool.id, archiveKind, "launcher-missing");
logSessionStart(`archive-install ${tool.id}: ${archiveKind} launcher missing after extraction \u2014 keeping installed version`);
await fs2.rm(tmpExtractDir, { recursive: true, force: true }).catch(() => {
});
return void 0;
}
if (!isWindows)
await fs2.chmod(tmpResolvedInner, 488).catch(() => {
});
const launcherStat = await fs2.stat(tmpResolvedInner).catch(() => void 0);
const launcherRunnable = launcherStat?.isFile() === true && launcherStat.size > 0 && (isWindows || (launcherStat.mode & 73) !== 0);
if (!launcherRunnable) {
recordArchiveExtractionDegradation(tool.id, archiveKind, "launcher-invalid");
logSessionStart(`archive-install ${tool.id}: ${archiveKind} launcher is empty or not executable \u2014 keeping installed version`);
await fs2.rm(tmpExtractDir, { recursive: true, force: true }).catch(() => {
});
return void 0;
}
await swapExtractedDir(tool.id, tmpExtractDir, extractDir);
const innerLauncher = path3.join(extractDir, ...launcherParts);
const resolvedInner = isWindows ? `${innerLauncher}.bat` : innerLauncher;
await fs2.mkdir(GITHUB_BIN_DIR, { recursive: true });
const launcherName = isWindows ? `${binaryName}.bat` : binaryName;
const shimPath = path3.join(GITHUB_BIN_DIR, launcherName);
if (isWindows) {
await writeFileAtomicAsync(shimPath, `@echo off\r
call "${resolvedInner}" %*\r
`, { bestEffort: false });
} else {
await writeFileAtomicAsync(shimPath, `#!/bin/sh
exec "${resolvedInner}" "$@"
`, { bestEffort: false, mode: 488 });
}
logSessionStart(`archive-install ${tool.id}: installed \u2192 ${shimPath} (extracted ${archiveBuffer.length} bytes, sha256 verified ${digest})`);
debugLog(`[archive] installed ${tool.name} \u2192 ${shimPath}`);
return shimPath;
} catch (err) {
await fs2.rm(tmpArchive, { force: true }).catch(() => {
});
await fs2.rm(tmpExtractDir, { recursive: true, force: true }).catch(() => {
});
logSessionStart(`archive-install ${tool.id}: install failed: ${err.message} \u2014 keeping installed version`);
return void 0;
}
}
function recordPackageManagerInstallException(toolId, strategyLabel, packageName, err) {
const message = boundInstallError(err.message);
logSessionStart(`auto-install ${strategyLabel} ${packageName}: exception: ${message}`);
installFailureReasons.set(toolId, message);
return void 0;
}
var INSTALL_ERROR_LINE_LIMIT = 1e3;
var INSTALL_CANDIDATE_ERROR_LIMIT = 200;
function boundInstallError(value, limit = INSTALL_ERROR_LINE_LIMIT) {
const line = value.replace(/[\r\n]+/g, " ").trim();
return line.length > limit ? `${line.slice(0, limit - 3)}...` : line;
}
var InstallLockLostError = class extends Error {
};
async function installNpmTool(toolId, packageName, binaryName, verificationArgs = ["--version"], verificationTimeoutMs = 1e4, packageEntryOf, ownsLock) {
const assertOwnsLock = (context) => {
if (ownsLock && !ownsLock()) {
recordDegradationOnce({
kind: "install-lock-lost-mid-install",
subject: toolId,
reason: `install lock generation lost before ${context}; aborting rather than risk a second writer in TOOLS_DIR`
});
throw new InstallLockLostError(`install lock lost before ${context} for ${packageName} (#3515)`);
}
};
try {
await fs2.mkdir(TOOLS_DIR, { recursive: true });
const packageJsonPath = path3.join(TOOLS_DIR, "package.json");
try {
await fs2.access(packageJsonPath);
} catch {
await writeFileAtomicAsync(packageJsonPath, JSON.stringify({ name: "pi-lens-tools", version: "1.0.0" }, null, 2), { bestEffort: false });
}
const isWindows = installerPlatform() === "win32";
const pm = await resolveNodePackageManager(TOOLS_DIR);
const testNpmScript = process.env.PI_LENS_TEST_MODE === "1" ? process.env.PI_LENS_TEST_NPM_SCRIPT : void 0;
const pmCommand = testNpmScript ? process.execPath : pmBinary(pm);
const needsScripts = NEEDS_POSTINSTALL.has(packageName);
const baseInstallArgs = installArgs(pm, packageName, {
ignoreScripts: !needsScripts
});
const INSTALL_TIMEOUT_MS = Number(process.env.PI_LENS_INSTALL_TIMEOUT_MS) || 12e4;
const runInstallAttempt = async (args) => {
const result = await safeSpawnAsync(pmCommand, args, {
cwd: TOOLS_DIR,
timeout: INSTALL_TIMEOUT_MS,
ignoreAmbientSignal: true,
lifetimeCoupled: true
});
return {
ok: result.status === 0,
stderr: result.error?.message ?? result.stderr
};
};
assertOwnsLock(`spawning ${pmCommand} install`);
let outcome = await runInstallAttempt([
...testNpmScript ? [testNpmScript] : [],
...baseInstallArgs
]);
const erResolve = outcome.ok === false && /npm\s+error\s+ERESOLVE|\bERESOLVE\b|could not resolve/i.test(outcome.stderr);
if (pm === "npm" && erResolve) {
assertOwnsLock(`retrying ${pmCommand} install with --legacy-peer-deps`);
const retryArgs = installArgs(pm, packageName, {
ignoreScripts: !needsScripts,
legacyPeerDeps: true
});
logSessionStart(`auto-install npm ${packageName}: retry with --legacy-peer-deps after ERESOLVE`);
outcome = await runInstallAttempt([
...testNpmScript ? [testNpmScript] : [],
...retryArgs
]);
}
if (!outcome.ok) {
throw new Error(`Failed to install ${packageName}: ${outcome.stderr}`);
}
const binBase = path3.join(TOOLS_DIR, "node_modules", ".bin", binaryName);
const binPath = installerPlatform() === "win32" ? `${binBase}.cmd` : binBase;
if (installerPlatform() !== "win32") {
try {
await fs2.chmod(binPath, 488);
} catch {
}
}
await new Promise((r) => setTimeout(r, 500));
debugLog(`Verifying ${binaryName}...`);
let isValid = false;
let lastAttemptTransient = false;
let lastAttemptInconclusive = false;
for (let attempt = 1; attempt <= 3; attempt++) {
lastAttemptTransient = false;
lastAttemptInconclusive = false;
isValid = await verifyToolBinary(binPath, void 0, () => {
lastAttemptTransient = true;
}, verificationTimeoutMs, verificationArgs, packageEntryOf, () => {
lastAttemptInconclusive = true;
});
if (isValid)
break;
if (attempt < 3) {
logSessionStart(`auto-install verify ${binaryName}: attempt ${attempt} failed, retrying in ${attempt}s`);
await new Promise((r) => setTimeout(r, 1e3 * attempt));
}
}
if (!isValid && lastAttemptInconclusive) {
if (await verifyNpmPackageEntry(binPath, packageName)) {
logSessionStart(`auto-install ${packageName}: verification inconclusive (output truncated before the transport-required marker) but the installed tree is intact; keeping installation for re-probe`);
return void 0;
}
logSessionStart(`auto-install ${packageName}: verification inconclusive AND the installed tree is incomplete; cleaning up so the next install can repair it`);
}
if (!isValid && lastAttemptTransient) {
logSessionStart(`auto-install ${packageName}: verification inconclusive (transient); keeping installation for re-probe`);
return void 0;
}
if (!isValid) {
logSessionStart(`auto-install ${packageName}: installed but verification failed, cleaning up`);
try {
const packagePath = path3.join(TOOLS_DIR, "node_modules", packageName);
await fs2.rm(packagePath, { recursive: true, force: true });
await fs2.rm(binBase, { force: true });
if (isWindows) {
await fs2.rm(`${binBase}.cmd`, { force: true });
await fs2.rm(`${binBase}.ps1`, { force: true });
}
} catch {
}
return void 0;
}
return binPath;
} catch (err) {
return recordPackageManagerInstallException(toolId, "npm", packageName, err);
}
}
function pipCommandCandidates() {
return process.platform === "win32" ? ["pip", "py", "python"] : ["pip3", "pip", "python3", "python"];
}
function pipScriptsDir(base, platform = installerPlatform()) {
return path3.join(base, platform === "win32" ? "Scripts" : "bin");
}
var pipPep668LoggedRefusals = createGenerationMap("installer-pep668-log");
async function pipConstraintEnvFor(toolId) {
const constraints = TOOLS.find((t) => t.id === toolId)?.pipConstraints;
if (!constraints || constraints.length === 0)
return {};
const directories = [
path3.join(getGlobalPiLensDir(), "pip-constraints"),
path3.join(os.tmpdir(), "pi-lens-pip-constraints")
];
const directory = directories.find((candidate) => !/\s/.test(candidate));
if (!directory) {
recordDegradationOnce({
kind: "pip-constraint-path-unusable",
subject: toolId,
reason: `no whitespace-free directory for the constraints file (tried ${directories.join(", ")}); installing unconstrained`
});
return {};
}
const file = path3.join(directory, `${toolId}.txt`);
try {
await fs2.mkdir(path3.dirname(file), { recursive: true });
await writeFileAtomicAsync(file, `${constraints.join("\n")}
`, {
bestEffort: false
});
} catch (err) {
recordDegradationOnce({
kind: "pip-constraint-file-unwritable",
subject: toolId,
reason: `${file}: ${err instanceof Error ? err.message : String(err)}`
});
return {};
}
logSessionStart(`auto-install pip ${toolId}: constraining resolution with ${constraints.join(", ")} (${file})`);
return { PIP_CONSTRAINT: file, UV_CONSTRAINT: file };
}
async function installPipTool(toolId, packageName, binaryName, options = {}) {
try {
const isWindows = installerPlatform() === "win32";
const verb = options.upgrade ? ["install", "-U"] : ["install"];
const pipConstraintEnv = await pipConstraintEnvFor(toolId);
const pipCandidates = pipCommandCandidates().map((command) => ({
command,
args: command === "pip" || command === "pip3" ? [...verb, packageName] : ["-m", "pip", ...verb, packageName]
}));
const pep668 = /externally-managed-environment/i;
const refuse = (strategy, reason) => {
if (!pep668.test(reason))
return;
const subject = `${toolId}:${strategy}`;
recordDegradationOnce({
kind: "pip-pep668-strategy-refused",
subject,
reason
});
const logKey = `${getDegradationLedgerGeneration()}:${subject}`;
if (pipPep668LoggedRefusals.current(logKey) === 0) {
pipPep668LoggedRefusals.bump(logKey);
logSessionStart(`auto-install pip ${packageName}: ${strategy} refused by PEP 668 (${boundInstallError(reason)})`);
}
};
const succeeded = (strategy, binaryPath) => {
recordDegradationOnce({
kind: "pip-install-strategy-succeeded",
subject: `${toolId}:${strategy}`,
reason: binaryPath
});
return binaryPath;
};
const run = (command, args, env) => {
const spawnEnv = Object.keys(pipConstraintEnv).length > 0 ? { ...env ?? process.env, ...pipConstraintEnv } : env;
return safeSpawnAsync(command, args, {
timeout: 12e4,
ignoreAmbientSignal: true,
lifetimeCoupled: true,
cwd: resolveToolCwd("runner", toolId, getGlobalPiLensDir(), {
cwd: getGlobalPiLensDir(),
suppressTelemetry: true
}).cwd,
...spawnEnv ? { env: spawnEnv } : {}
});
};
const addBinToPath = async (binDir) => {
try {
await fs2.access(binDir);
} catch {
return void 0;
}
const currentPath = process.env.PATH || process.env.Path || "";
const separator = isWindows ? ";" : path3.delimiter;
if (!currentPath.toLowerCase().split(separator).includes(binDir.toLowerCase())) {
const updatedPath = `${binDir}${separator}${currentPath}`;
process.env.PATH = updatedPath;
if (isWindows)
process.env.Path = updatedPath;
}
const names = isWindows ? [`${binaryName}.exe`, `${binaryName}.cmd`, binaryName] : [binaryName];
for (const name of names) {
const candidate = path3.join(binDir, name);
try {
await fs2.access(candidate);
return candidate;
} catch {
}
}
return void 0;
};
if (await isCommandAvailable("pipx")) {
const result = await run("pipx", options.upgrade ? ["upgrade", packageName] : ["install", "--force", packageName]);
const error = (result.error?.message ?? result.stderr).trim();
if (result.status === 0) {
const location = await run("pipx", [
"environment",
"--value",
"PIPX_BIN_DIR"
]);
const binDir = location.status === 0 && location.stdout.trim() ? location.stdout.trim() : path3.join(os.homedir(), ".local", "bin");
const binaryPath = await addBinToPath(binDir);
if (binaryPath)
return succeeded("pipx", binaryPath);
throw new Error(`pipx installed ${packageName} but ${binaryName} is not resolvable`);
}
refuse("pipx", error);
}
const pythonCandidates = pipCandidates.filter(({ command }) => command === "python3" || command === "python" || command === "py");
const pythonAvailability = await Promise.all(pythonCandidates.map(({ command }) => isCommandAvailable(command)));
const availablePythonCandidates = pythonCandidates.filter((_, index) => pythonAvailability[index]);
const venvRoot = path3.join(getGlobalPiLensDir(), "pip-tools");
for (const candidate of availablePythonCandidates) {
const venvBin = pipScriptsDir(venvRoot, installerPlatform());
let venvPip = path3.join(venvBin, isWindows ? "pip.exe" : "pip");
try {
await fs2.access(venvPip);
} catch {
const created = await run(candidate.command, ["-m", "venv", venvRoot]);
const error2 = (created.error?.message ?? created.stderr).trim();
if (created.status !== 0) {
refuse("venv", error2 || "python venv module unavailable");
continue;
}
}
try {
await fs2.access(venvPip);
} catch {
venvPip = path3.join(venvBin, isWindows ? "pip.cmd" : "pip3");
try {
await fs2.access(venvPip);
} catch {
continue;
}
}
const result = await run(venvPip, [...verb, packageName]);
const error = (result.error?.message ?? result.stderr).trim();
if (result.status === 0) {
const binaryPath = await addBinToPath(venvBin);
if (binaryPath)
return succeeded("venv", binaryPath);
throw new Error(`venv installed ${packageName} but ${binaryName} is not resolvable`);
}
refuse("venv", error);
}
const candidateErrors = [];
let userRefused = false;
for (const candidate of pipCandidates) {
const args = candidate.command === "pip" || candidate.command === "pip3" ? [...verb, "--user", packageName] : ["-m", "pip", ...verb, "--user", packageName];
const result = await run(candidate.command, args);
const error = (result.error?.message ?? result.stderr).trim();
if (result.status === 0) {
const base = await new Promise((resolve) => {
let probe;
try {
probe = spawn(candidate.command, ["-m", "site", "--user-base"], {
stdio: ["ignore", "pipe", "pipe"],
shell: isWindows
});
} catch {
resolve("");
return;
}
const stdout = createBoundedOutputSink();
probe.stdout?.on("data", (data) => stdout.append(data));
probe.on("exit", (code) => resolve(userBaseProbeResult(candidate.command, code, stdout)));
probe.on("error", () => resolve(""));
});
const binaryPath = base ? await addBinToPath(pipScriptsDir(base, installerPlatform())) : void 0;
return succeeded("user", binaryPath ?? packageName);
}
const candidateError = `${candidate.command} ${args.join(" ")}: ${boundInstallError(error, INSTALL_CANDIDATE_ERROR_LIMIT)}`;
candidateErrors.push(candidateError);
if (pep668.test(error)) {
userRefused = true;
refuse("user", error);
}
}
if (!userRefused)
throw new Error(`pip install failed: ${candidateErrors.join(" | ") || "unknown error"}`);
const privateBase = path3.join(getGlobalPiLensDir(), "pip-user");
const privateEnv = { ...process.env, PYTHONUSERBASE: privateBase };
for (const candidate of pipCandidates) {
const args = candidate.command === "pip" || candidate.command === "pip3" ? [...verb, "--user", "--break-system-packages", packageName] : [
"-m",
"pip",
...verb,
"--user",
"--break-system-packages",
packageName
];
const result = await run(candidate.command, args, privateEnv);
const error = (result.error?.message ?? result.stderr).trim();
if (result.status !== 0) {
const candidateError = `${candidate.command} ${args.join(" ")}: ${boundInstallError(error, INSTALL_CANDIDATE_ERROR_LIMIT)}`;
candidateErrors.push(candidateError);
continue;
}
const binaryPath = await addBinToPath(pipScriptsDir(privateBase, installerPlatform()));
if (binaryPath)
return succeeded("private-prefix", binaryPath);
throw new Error(`private-prefix pip installed ${packageName} but ${binaryName} is not resolvable`);
}
throw new Error(`pip install failed: ${candidateErrors.join(" | ") || "unknown error"}`);
} catch (err) {
return recordPackageManagerInstallException(toolId, "pip", packageName, err);
}
}
async function installGemTool(toolId, packageName) {
try {
const gemResult = await safeSpawnAsync("gem", ["install", packageName, "--no-document"], {
timeout: 12e4,
ignoreAmbientSignal: true,
lifetimeCoupled: true
});
const outcome = {
ok: gemResult.status === 0,
error: (gemResult.error?.message ?? gemResult.stderr).trim()
};
if (!outcome.ok) {
throw new Error(`Failed to install ${packageName} via gem: ${outcome.error}`);
}
return packageName;
} catch (err) {
return recordPackageManagerInstallException(toolId, "gem", packageName, err);
}
}
async function stampInstallResolution(toolId) {
const tool = TOOLS.find((t) => t.id === toolId);
if (tool?.installStrategy === "npm")
return;
try {
const resolutionId = lastInstallResolutionId.get(toolId);
const refresh = await import("./chunk-T4ERT73Y.js");
await refresh.stampManagedToolInstall(toolId, resolutionId);
} catch {
}
}
async function finishInstallAttempt(toolId, ok, startedAt) {
logSessionStart(`auto-install ${toolId}: ${ok ? "success" : "failed"} (${Date.now() - startedAt}ms)`);
if (ok)
await stampInstallResolution(toolId);
noteInstallAttempt(toolId, ok ? "succeeded" : "failed", ok ? void 0 : installFailureReasons.get(toolId) ?? "install failed");
if (ok) {
resetSafeSpawnWindowsCommandCache();
try {
const { resetMadgeManagedPathMemo } = await import("./chunk-T6D4Q4AQ.js");
resetMadgeManagedPathMemo();
} catch (err) {
logSessionStart(`auto-install ${toolId}: madge memo reset failed: ${err.message}`);
}
}
return ok;
}
async function installTool(toolId, ownsLock) {
if (process.env.PI_LENS_DISABLE_TOOL_INSTALL === "1") {
installFailureReasons.set(toolId, "installation disabled by PI_LENS_DISABLE_TOOL_INSTALL=1");
noteInstallAttempt(toolId, "declined", "installation disabled by PI_LENS_DISABLE_TOOL_INSTALL=1");
logSessionStart(`auto-install ${toolId}: refused \u2014 PI_LENS_DISABLE_TOOL_INSTALL=1`);
return false;
}
const tool = TOOLS.find((t) => t.id === toolId);
if (!tool) {
noteInstallAttempt(toolId, "declined", "unknown tool id");
logSessionStart(`auto-install ${toolId}: unknown tool id`);
return false;
}
const startedAt = Date.now();
logSessionStart(`auto-install ${tool.id}: start strategy=${tool.installStrategy} package=${tool.packageName ?? "n/a"}`);
try {
switch (tool.installStrategy) {
case "npm": {
if (!tool.packageName || !tool.binaryName)
return false;
const npmPath = await installNpmTool(tool.id, tool.packageName, tool.binaryName, tool.checkArgs, getToolVerificationTimeout(tool), packageEntryVerification(tool), ownsLock);
if (npmPath !== void 0) {
await import("./chunk-T4ERT73Y.js").then((m) => m.stampManagedToolInstalled(tool.id, tool.packageName)).catch(() => {
});
}
return finishInstallAttempt(tool.id, npmPath !== void 0, startedAt);
}
case "pip": {
if (!tool.packageName)
return false;
const pipPath = await installPipTool(tool.id, tool.packageName, tool.binaryName ?? tool.id);
return finishInstallAttempt(tool.id, pipPath !== void 0, startedAt);
}
case "gem": {
if (!tool.packageName)
return false;
const gemPath = await installGemTool(tool.id, tool.packageName);
return finishInstallAttempt(tool.id, gemPath !== void 0, startedAt);
}
case "github": {
if (!tool.github)
return false;
if (!tool.github.assetMatch(process.platform, process.arch)) {
const reason = `unsupported platform=${process.platform} arch=${process.arch}`;
noteInstallAttempt(tool.id, "unavailable", reason);
logSessionStart(`auto-install ${tool.id}: ${reason}`);
return false;
}
const ghPath = await installGitHubTool(tool);
return finishInstallAttempt(tool.id, ghPath !== void 0, startedAt);
}
case "maven": {
if (!tool.maven)
return false;
const mavenPath = await installMavenTool(tool);
return finishInstallAttempt(tool.id, mavenPath !== void 0, startedAt);
}
case "archive": {
if (!tool.archive)
return false;
if (!resolveArchiveUrl(tool.archive)) {
const reason = `unsupported platform=${process.platform} arch=${process.arch}`;
noteInstallAttempt(tool.id, "unavailable", reason);
logSessionStart(`auto-install ${tool.id}: ${reason}`);
return false;
}
const runtime = tool.archive.runtime;
const viaHome = runtime ? runtimeHomeVerdict(runtime) : void 0;
if (runtime && !(viaHome ?? await isCommandAvailable(runtime))) {
const reason = viaHome === void 0 ? `runtime ${runtime} not found on PATH` : `runtime ${runtime} is not an executable file under ${ARCHIVE_RUNTIME_HOMES[runtime]?.env}`;
noteInstallAttempt(tool.id, "unavailable", reason);
logSessionStart(`auto-install ${tool.id}: ${reason}`);
return false;
}
const archivePath = await installArchiveTool(tool);
return finishInstallAttempt(tool.id, archivePath !== void 0, startedAt);
}
default:
logSessionStart(`auto-install ${tool.id}: unsupported strategy`);
return false;
}
} catch (err) {
logSessionStart(`auto-install ${tool.id}: exception ${err.message} (${Date.now() - startedAt}ms)`);
return false;
}
}
async function ensureTool(toolId, opts) {
if (opts?.allowInstall !== false && !assertInstallAllowed(`managed tool ensure: ${toolId}`)) {
logSessionStart(`auto-install ensure ${toolId}: install gated \u2014 ${projectTrustDenialReason()}; discovery only`);
const denialReason = projectTrustDenialReason();
const discovered = await ensureToolResolved(toolId, {
...opts,
allowInstall: false
});
noteInstallAttempt(toolId, "declined", `project trust: ${denialReason}`);
return discovered;
}
return ensureToolResolved(toolId, opts);
}
async function ensureToolResolved(toolId, opts) {
installFailureReasons.delete(toolId);
installAttempts.delete(toolId);
lastEnsureResolutionSource.delete(toolId);
const cacheResolvedPath = (result) => {
if (result) {
resolvedPathCache.set(toolId, result);
void updateProbeCache(toolId, result, wasLastResolveTransient(toolId));
}
return result;
};
if (opts?.forceReinstall) {
const ensureStartMs2 = Date.now();
logSessionStart(`auto-install ensure ${toolId}: force reinstall \u2014 clearing caches`);
resolvedPathCache.delete(toolId);
try {
const probeCache = await readProbeCache();
delete probeCache[toolId];
markProbeCacheChange(toolId, null);
} catch {
}
if (opts.allowInstall === false) {
noteInstallAttempt(toolId, "declined", "install disabled by caller");
logSessionStart(`auto-install ensure ${toolId}: force reinstall blocked \u2014 install disabled, discovery only (${Date.now() - ensureStartMs2}ms)`);
return cacheResolvedPath(await getToolPath(toolId));
}
if (process.env.PI_LENS_DISABLE_TOOL_INSTALL === "1") {
installFailureReasons.set(toolId, "installation disabled by PI_LENS_DISABLE_TOOL_INSTALL=1");
noteInstallAttempt(toolId, "declined", "installation disabled by PI_LENS_DISABLE_TOOL_INSTALL=1");
logSessionStart(`auto-install ensure ${toolId}: refused \u2014 PI_LENS_DISABLE_TOOL_INSTALL=1`);
return void 0;
}
const lock = await acquireInstallLock();
if (!lock.release) {
noteInstallAttempt(toolId, "skipped", lock.reason ?? "install lock held");
logSessionStart(`auto-install ensure ${toolId}: ${lock.reason}`);
return void 0;
}
let installed;
try {
installed = await installTool(toolId, lock.ownsLock);
} finally {
await lock.release();
}
if (!installed) {
noteInstallAttemptIfUnrecorded(toolId, "failed", "install failed");
logSessionStart(`auto-install ensure ${toolId}: force reinstall failed (${Date.now() - ensureStartMs2}ms)`);
return void 0;
}
const result = cacheResolvedPath(await getToolPath(toolId));
if (result) {
logSessionStart(`auto-install ensure ${toolId}: force reinstall success at ${result} (${Date.now() - ensureStartMs2}ms)`);
}
return result;
}
const cached = resolvedPathCache.get(toolId);
if (cached) {
if (!isFullyQualified(cached)) {
lastEnsureResolutionSource.set(toolId, "session-cache");
return cached;
}
try {
await fs2.access(cached);
lastEnsureResolutionSource.set(toolId, "session-cache");
return cached;
} catch {
}
resolvedPathCache.delete(toolId);
const probeCache = await readProbeCache();
delete probeCache[toolId];
markProbeCacheChange(toolId, null);
logSessionStart(`auto-install ensure ${toolId}: cached path disappeared; re-probing`);
}
const diskCached = await checkProbeCache(toolId);
if (diskCached) {
resolvedPathCache.set(toolId, diskCached);
lastEnsureResolutionSource.set(toolId, "probe-cache");
logSessionStart(`auto-install ensure ${toolId}: probe cache hit \u2192 ${diskCached}`);
return diskCached;
}
const inFlightKey = opts?.allowInstall === false ? `${toolId}:discovery-only` : toolId;
const inFlight = ensureInFlight.get(inFlightKey);
if (inFlight) {
logSessionStart(`auto-install ensure ${toolId}: waiting for in-flight ensure (${inFlightKey})`);
return inFlight;
}
const ensureStartMs = Date.now();
const ensurePromise = (async () => {
logSessionStart(`auto-install ensure ${toolId}: start`);
const existingPath = await getToolPath(toolId);
if (existingPath) {
const tool = TOOLS.find((t) => t.id === toolId);
const pinnedVersion = tool?.installStrategy === "npm" && tool.packageName ? parsePinnedVersion(tool.packageName) : void 0;
if (pinnedVersion) {
const seenVersion = lastManagedInstallVersion.get(toolId);
if (seenVersion && seenVersion !== pinnedVersion) {
lastManagedInstallVersion.delete(toolId);
logSessionStart(`auto-install ensure ${toolId}: version drift (installed ${seenVersion} != pinned ${pinnedVersion}) \u2014 forcing reinstall (${Date.now() - ensureStartMs}ms)`);
return ensureTool(toolId, {
forceReinstall: true,
allowInstall: opts?.allowInstall
});
}
}
resolvedPathCache.set(toolId, existingPath);
void updateProbeCache(toolId, existingPath, wasLastResolveTransient(toolId));
lastEnsureResolutionSource.set(toolId, "path");
logSessionStart(`auto-install ensure ${toolId}: already available at ${existingPath} (${Date.now() - ensureStartMs}ms)`);
return existingPath;
}
if (opts?.allowInstall === false) {
noteInstallAttempt(toolId, "declined", "install disabled by caller");
logSessionStart(`auto-install ensure ${toolId}: install disabled \u2014 discovery only, not found (${Date.now() - ensureStartMs}ms)`);
return void 0;
}
if (process.env.PI_LENS_DISABLE_TOOL_INSTALL === "1") {
installFailureReasons.set(toolId, "installation disabled by PI_LENS_DISABLE_TOOL_INSTALL=1");
noteInstallAttempt(toolId, "declined", "installation disabled by PI_LENS_DISABLE_TOOL_INSTALL=1");
logSessionStart(`auto-install ensure ${toolId}: refused \u2014 PI_LENS_DISABLE_TOOL_INSTALL=1 (${Date.now() - ensureStartMs}ms)`);
return void 0;
}
const lock = await acquireInstallLock();
if (!lock.release) {
installFailureReasons.set(toolId, lock.reason ?? "install lock failed");
noteInstallAttempt(toolId, "skipped", lock.reason ?? "install lock held");
logSessionStart(`auto-install ensure ${toolId}: ${lock.reason}`);
return void 0;
}
let installed;
try {
const installedByPeer = await getToolPath(toolId);
if (installedByPeer) {
noteInstallAttempt(toolId, "succeeded", "installed by a concurrent process");
resolvedPathCache.set(toolId, installedByPeer);
void updateProbeCache(toolId, installedByPeer, wasLastResolveTransient(toolId));
return installedByPeer;
}
installed = await installTool(toolId, lock.ownsLock);
} finally {
await lock.release();
}
if (!installed) {
noteInstallAttemptIfUnrecorded(toolId, "failed", "install failed");
logSessionStart(`auto-install ensure ${toolId}: unavailable (${Date.now() - ensureStartMs}ms)`);
return void 0;
}
const result = await getToolPath(toolId);
if (result) {
resolvedPathCache.set(toolId, result);
void updateProbeCache(toolId, result, wasLastResolveTransient(toolId));
logSessionStart(`auto-install ensure ${toolId}: success at ${result} (${Date.now() - ensureStartMs}ms)`);
} else {
logSessionStart(`auto-install ensure ${toolId}: unavailable (${Date.now() - ensureStartMs}ms)`);
}
return result;
})();
ensureInFlight.set(inFlightKey, ensurePromise);
try {
return await ensurePromise;
} finally {
if (ensureInFlight.get(inFlightKey) === ensurePromise) {
ensureInFlight.delete(inFlightKey);
}
}
}
async function getToolEnvironment() {
const localBin = path3.join(TOOLS_DIR, "node_modules", ".bin");
const currentPath = process.env.PATH || process.env.Path || process.env.path || "";
const separator = process.platform === "win32" ? ";" : ":";
const nodeDir = path3.dirname(process.execPath);
const withNode = nodeDir ? `${nodeDir}${separator}${currentPath}` : currentPath;
const augmentedPath = `${GITHUB_BIN_DIR}${separator}${localBin}${separator}${withNode}`;
const env = {
...process.env,
PATH: augmentedPath
};
if (process.platform === "win32") {
env.Path = augmentedPath;
}
return env;
}
async function checkAllTools() {
const results = [];
for (const tool of TOOLS) {
const path4 = await getToolPath(tool.id);
results.push({
id: tool.id,
name: tool.name,
installed: path4 !== void 0,
path: path4
});
}
return results;
}
function isKnownToolId(toolId) {
return TOOLS.some((tool) => tool.id === toolId);
}
function getToolInstallStrategy(toolId) {
return TOOLS.find((tool) => tool.id === toolId)?.installStrategy;
}
var GITHUB_TOOLS = [
"cljfmt",
"php-cs-fixer",
"shellcheck",
"shfmt",
"rust-analyzer",
"golangci-lint",
"ktlint",
"actionlint",
"zizmor",
"typos-lsp",
"tflint",
"terragrunt",
"terraform-ls",
"zls",
"hadolint",
"helm",
"gitleaks",
"taplo",
"vale",
"opengrep",
"deno",
"clojure-lsp",
"cue",
"gleam",
"typstyle",
"tinymist",
"marksman",
"expert"
];
function resolveGitHubAsset(toolId, platform, arch) {
const tool = TOOLS.find((t) => t.id === toolId);
return tool?.github?.assetMatch(platform, arch);
}
function resolveGitHubAssetLauncher(toolId, _platform, assetName) {
const tool = TOOLS.find((t) => t.id === toolId);
return tool?.github ? launcherForGitHubAsset(tool.github, assetName) : void 0;
}
function resolveGitHubInstalledBinaryName(toolId, platform, assetName) {
const tool = TOOLS.find((t) => t.id === toolId);
if (!tool)
return void 0;
return getGitHubInstalledBinaryName(tool.binaryName ?? tool.id, platform, assetName);
}
function resolveGitHubArchiveBinaryCandidates(toolId, platform, assetName) {
const tool = TOOLS.find((t) => t.id === toolId);
if (!tool)
return void 0;
const binaryName = tool.github?.binaryInArchive ?? tool.binaryName ?? tool.id;
return getArchiveBinaryCandidates(binaryName, platform, assetName);
}
var ASSET_SIDECAR_SUFFIXES = [
".asc",
".sig",
".minisig",
".pem",
".cert",
".sbom",
".sha256",
".sha256sum",
".md5"
];
function isAssetSidecar(name) {
const lower = name.toLowerCase();
return ASSET_SIDECAR_SUFFIXES.some((suffix) => lower.endsWith(suffix));
}
function pickReleaseAsset(assets, assetSubstring) {
return assets.find((a) => a.name === assetSubstring) ?? assets.find((a) => a.name.includes(assetSubstring) && !isAssetSidecar(a.name));
}
function deriveHashiCorpReleaseAsset(tool, tagName, assetSubstring) {
const product = tool.github?.hashiCorpReleaseProduct;
if (!product || !tagName)
return void 0;
const version = tagName.replace(/^v/, "").trim();
if (!version)
return void 0;
const assetName = `${product}_${version}_${assetSubstring}`;
return {
name: assetName,
browser_download_url: `https://releases.hashicorp.com/${product}/${version}/${assetName}`
};
}
function resolveDerivedHashiCorpReleaseAsset(toolId, tagName, platform, arch) {
const tool = TOOLS.find((t) => t.id === toolId);
if (!tool)
return void 0;
const assetSubstring = tool.github?.assetMatch(platform, arch);
if (!assetSubstring)
return void 0;
return deriveHashiCorpReleaseAsset(tool, tagName, assetSubstring);
}
export {
commitDurableStore,
commitDurableStoreAsync,
findLocalZizmorConfig,
isZizmorAuditTarget,
resetZizmorTokenAvailability,
resolveZizmorGitHubToken,
logSessionStart,
getManagedToolsDir,
TOOLS,
getToolVerificationTimeout,
getInstallFailureReason,
getInstallAttempt,
getLastEnsureResolutionSource,
resetResolvedPathCache,
flushProbeCache,
checkProbeCache,
updateProbeCache,
resetProbeCacheStateForTesting,
_peekEnsureInFlightForTesting,
resetPathWalkMemo,
isCommandAvailable,
isSpawnableCommand,
isLspTransportRequiredError,
parsePinnedVersion,
packageEntryVerification,
verifyNpmPackageEntry,
verifyToolBinary,
getAllToolStatuses,
isToolInstalled,
resolvePlatformPackageBinary,
wasLastResolveTransient,
getToolPath,
findManagedToolBinary,
userBaseProbeResult,
npmToolNeedsPostinstall,
getRefreshableManagedNpmTools,
getRefreshableManagedTools,
isManagedToolPresent,
acquireManagedInstallGate,
refreshManagedTool,
resolveManagedNpmBinPath,
invalidateManagedToolResolution,
resolveArchiveUrl,
resolveArchiveKind,
swapExtractedDir,
pipCommandCandidates,
pipScriptsDir,
installTool,
ensureTool,
getToolEnvironment,
checkAllTools,
isKnownToolId,
getToolInstallStrategy,
GITHUB_TOOLS,
resolveGitHubAsset,
resolveGitHubAssetLauncher,
resolveGitHubInstalledBinaryName,
resolveGitHubArchiveBinaryCandidates,
pickReleaseAsset,
resolveDerivedHashiCorpReleaseAsset
};