pi-lens 3.8.52 → 3.8.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/dist/clients/dispatch/plan.js +6 -2
- package/dist/clients/dispatch/runners/detekt.js +1 -1
- package/dist/clients/dispatch/runners/elixir-check.js +79 -15
- package/dist/clients/dispatch/runners/markdownlint.js +11 -3
- package/dist/clients/dispatch/runners/shellcheck.js +5 -3
- package/dist/clients/dispatch/runners/shfmt.js +25 -1
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +8 -2
- package/dist/clients/dispatch/runners/zig-check.js +3 -1
- package/dist/clients/formatters.js +52 -3
- package/dist/clients/installer/index.js +116 -10
- package/dist/clients/language-policy.js +7 -7
- package/dist/clients/lsp/client.js +3 -1
- package/dist/clients/lsp/server.js +27 -15
- package/dist/clients/pipeline.js +127 -5
- package/dist/clients/safe-spawn.js +17 -1
- package/dist/clients/tool-policy.js +146 -9
- package/package.json +3 -3
|
@@ -472,8 +472,10 @@ export const TOOLS = [
|
|
|
472
472
|
installStrategy: "github",
|
|
473
473
|
binaryName: "ktlint",
|
|
474
474
|
github: {
|
|
475
|
-
// ktlint ships
|
|
476
|
-
//
|
|
475
|
+
// ktlint ships a self-executable `ktlint` (a JAR with a shell preamble)
|
|
476
|
+
// for Linux/macOS, plus a `ktlint.bat` wrapper for Windows that runs
|
|
477
|
+
// `java -jar %~dp0ktlint`. On Windows BOTH files are needed: the .bat AND
|
|
478
|
+
// the `ktlint` jar it wraps (#218). No arm64-specific asset.
|
|
477
479
|
repo: "pinterest/ktlint",
|
|
478
480
|
assetMatch: (platform, _arch) => {
|
|
479
481
|
if (platform === "linux")
|
|
@@ -484,6 +486,24 @@ export const TOOLS = [
|
|
|
484
486
|
return "ktlint.bat";
|
|
485
487
|
return undefined;
|
|
486
488
|
},
|
|
489
|
+
extraAssets: (platform) => (platform === "win32" ? ["ktlint"] : []),
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
// ktfmt (Meta's opinionated Kotlin formatter) ships only as a Maven-Central
|
|
494
|
+
// fat JAR — no native binary, no npm package — so it uses the maven strategy
|
|
495
|
+
// (#129). Run via a `java -jar` launcher; requires a JRE.
|
|
496
|
+
id: "ktfmt",
|
|
497
|
+
name: "ktfmt",
|
|
498
|
+
checkCommand: "ktfmt",
|
|
499
|
+
checkArgs: ["--version"],
|
|
500
|
+
installStrategy: "maven",
|
|
501
|
+
binaryName: "ktfmt",
|
|
502
|
+
maven: {
|
|
503
|
+
groupId: "com.facebook",
|
|
504
|
+
artifactId: "ktfmt",
|
|
505
|
+
version: "0.63",
|
|
506
|
+
classifier: "with-dependencies",
|
|
487
507
|
},
|
|
488
508
|
},
|
|
489
509
|
{
|
|
@@ -959,12 +979,13 @@ export async function getAllToolStatuses() {
|
|
|
959
979
|
continue;
|
|
960
980
|
}
|
|
961
981
|
}
|
|
962
|
-
// 4. Check
|
|
963
|
-
if (tool.installStrategy === "github") {
|
|
982
|
+
// 4. Check managed bin (~/.pi-lens/bin/) — github releases + maven launchers
|
|
983
|
+
if (tool.installStrategy === "github" || tool.installStrategy === "maven") {
|
|
964
984
|
const githubPath = await findGitHubToolPath(tool.binaryName || tool.id);
|
|
965
985
|
if (githubPath) {
|
|
966
986
|
status.installed = true;
|
|
967
|
-
status.source =
|
|
987
|
+
status.source =
|
|
988
|
+
tool.installStrategy === "maven" ? "maven-jar" : "github-release";
|
|
968
989
|
status.path = githubPath;
|
|
969
990
|
statuses.push(status);
|
|
970
991
|
continue;
|
|
@@ -1048,11 +1069,11 @@ export async function getToolPath(toolId) {
|
|
|
1048
1069
|
catch {
|
|
1049
1070
|
// fall through to global checks
|
|
1050
1071
|
}
|
|
1051
|
-
// For github
|
|
1052
|
-
// Managed installs are known-good binaries
|
|
1053
|
-
// when a PATH-resolved tool was broken or missing. Checking
|
|
1054
|
-
// force-reinstall flows find the newly downloaded binary.
|
|
1055
|
-
if (tool.installStrategy === "github") {
|
|
1072
|
+
// For github/maven tools, prefer the managed install (~/.pi-lens/bin/) over
|
|
1073
|
+
// PATH. Managed installs are known-good binaries/launchers pi-lens downloaded
|
|
1074
|
+
// as a fallback when a PATH-resolved tool was broken or missing. Checking
|
|
1075
|
+
// before PATH ensures force-reinstall flows find the newly downloaded binary.
|
|
1076
|
+
if (tool.installStrategy === "github" || tool.installStrategy === "maven") {
|
|
1056
1077
|
const githubPath = await findGitHubToolPath(tool.binaryName || tool.id);
|
|
1057
1078
|
if (githubPath)
|
|
1058
1079
|
return githubPath;
|
|
@@ -1447,6 +1468,27 @@ async function installGitHubTool(tool) {
|
|
|
1447
1468
|
logSessionStart(`github-install ${tool.id}: install failed: ${err.message}`);
|
|
1448
1469
|
return undefined;
|
|
1449
1470
|
}
|
|
1471
|
+
// Download any sibling assets the primary wrapper depends on (e.g. ktlint's
|
|
1472
|
+
// `ktlint` jar next to `ktlint.bat`, #218). Matched by EXACT name and written
|
|
1473
|
+
// as bare files into the same dir; a missing one fails the install.
|
|
1474
|
+
for (const extraName of spec.extraAssets?.(platform, arch) ?? []) {
|
|
1475
|
+
const extraAsset = releaseJson.assets.find((a) => a.name === extraName);
|
|
1476
|
+
if (!extraAsset) {
|
|
1477
|
+
logSessionStart(`github-install ${tool.id}: required extra asset "${extraName}" not found`);
|
|
1478
|
+
return undefined;
|
|
1479
|
+
}
|
|
1480
|
+
try {
|
|
1481
|
+
const extraBuffer = await httpsGet(extraAsset.browser_download_url);
|
|
1482
|
+
await fs.writeFile(path.join(GITHUB_BIN_DIR, extraName), extraBuffer, {
|
|
1483
|
+
mode: 0o755,
|
|
1484
|
+
});
|
|
1485
|
+
logSessionStart(`github-install ${tool.id}: installed extra asset ${extraName} (${extraBuffer.length} bytes)`);
|
|
1486
|
+
}
|
|
1487
|
+
catch (err) {
|
|
1488
|
+
logSessionStart(`github-install ${tool.id}: extra asset ${extraName} download failed: ${err.message}`);
|
|
1489
|
+
return undefined;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1450
1492
|
debugLog(`[github] installed ${tool.name} → ${destPath}`);
|
|
1451
1493
|
logSessionStart(`github-install ${tool.id}: installed → ${destPath}`);
|
|
1452
1494
|
return destPath;
|
|
@@ -1482,6 +1524,62 @@ const NEEDS_POSTINSTALL = new Set([
|
|
|
1482
1524
|
"esbuild",
|
|
1483
1525
|
"intelephense", // postinstall fetches platform binary; --ignore-scripts breaks install
|
|
1484
1526
|
]);
|
|
1527
|
+
const MAVEN_CENTRAL_BASE = "https://repo1.maven.org/maven2";
|
|
1528
|
+
/**
|
|
1529
|
+
* Install a Maven-distributed runnable fat JAR: download it into the managed bin
|
|
1530
|
+
* and write a `java -jar` launcher next to it (so it resolves like any managed
|
|
1531
|
+
* binary via findGitHubToolPath). Requires a JRE — gated on `java` availability.
|
|
1532
|
+
*/
|
|
1533
|
+
async function installMavenTool(tool) {
|
|
1534
|
+
const spec = tool.maven;
|
|
1535
|
+
if (!spec)
|
|
1536
|
+
return undefined;
|
|
1537
|
+
const binaryName = tool.binaryName ?? tool.id;
|
|
1538
|
+
const isWindows = process.platform === "win32";
|
|
1539
|
+
if (!(await isCommandAvailable("java", ["-version"]))) {
|
|
1540
|
+
logSessionStart(`maven-install ${tool.id}: java not found — a JAR tool can't run without a JRE`);
|
|
1541
|
+
return undefined;
|
|
1542
|
+
}
|
|
1543
|
+
// Strip trailing slashes without a regex (the `\/+$` form trips ReDoS
|
|
1544
|
+
// scanners — S5852 — even though the input is a trusted constant/registry
|
|
1545
|
+
// value). A plain loop is unambiguously linear.
|
|
1546
|
+
let base = spec.repoBaseUrl ?? MAVEN_CENTRAL_BASE;
|
|
1547
|
+
while (base.endsWith("/"))
|
|
1548
|
+
base = base.slice(0, -1);
|
|
1549
|
+
const groupPath = spec.groupId.replace(/\./g, "/");
|
|
1550
|
+
const jarFile = `${spec.artifactId}-${spec.version}${spec.classifier ? `-${spec.classifier}` : ""}.jar`;
|
|
1551
|
+
const url = `${base}/${groupPath}/${spec.artifactId}/${spec.version}/${jarFile}`;
|
|
1552
|
+
logSessionStart(`maven-install ${tool.id}: downloading ${url}`);
|
|
1553
|
+
let jarBuffer;
|
|
1554
|
+
try {
|
|
1555
|
+
jarBuffer = await httpsGet(url);
|
|
1556
|
+
}
|
|
1557
|
+
catch (err) {
|
|
1558
|
+
logSessionStart(`maven-install ${tool.id}: download failed: ${err.message}`);
|
|
1559
|
+
return undefined;
|
|
1560
|
+
}
|
|
1561
|
+
try {
|
|
1562
|
+
await fs.mkdir(GITHUB_BIN_DIR, { recursive: true });
|
|
1563
|
+
const jarPath = path.join(GITHUB_BIN_DIR, `${tool.id}.jar`);
|
|
1564
|
+
await fs.writeFile(jarPath, jarBuffer);
|
|
1565
|
+
// Launcher so the tool resolves as a normal command in the managed bin.
|
|
1566
|
+
const launcherName = isWindows ? `${binaryName}.bat` : binaryName;
|
|
1567
|
+
const launcherPath = path.join(GITHUB_BIN_DIR, launcherName);
|
|
1568
|
+
if (isWindows) {
|
|
1569
|
+
await fs.writeFile(launcherPath, `@echo off\r\njava -jar "%~dp0${tool.id}.jar" %*\r\n`);
|
|
1570
|
+
}
|
|
1571
|
+
else {
|
|
1572
|
+
await fs.writeFile(launcherPath, `#!/bin/sh\nexec java -jar "$(dirname "$0")/${tool.id}.jar" "$@"\n`, { mode: 0o755 });
|
|
1573
|
+
}
|
|
1574
|
+
logSessionStart(`maven-install ${tool.id}: installed → ${launcherPath} (${jarBuffer.length} bytes)`);
|
|
1575
|
+
debugLog(`[maven] installed ${tool.name} → ${launcherPath}`);
|
|
1576
|
+
return launcherPath;
|
|
1577
|
+
}
|
|
1578
|
+
catch (err) {
|
|
1579
|
+
logSessionStart(`maven-install ${tool.id}: install failed: ${err.message}`);
|
|
1580
|
+
return undefined;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1485
1583
|
async function installNpmTool(packageName, binaryName) {
|
|
1486
1584
|
try {
|
|
1487
1585
|
// Ensure tools directory exists
|
|
@@ -1798,6 +1896,14 @@ export async function installTool(toolId) {
|
|
|
1798
1896
|
logSessionStart(`auto-install ${tool.id}: ${ok ? "success" : "failed"} (${Date.now() - startedAt}ms)`);
|
|
1799
1897
|
return ok;
|
|
1800
1898
|
}
|
|
1899
|
+
case "maven": {
|
|
1900
|
+
if (!tool.maven)
|
|
1901
|
+
return false;
|
|
1902
|
+
const mavenPath = await installMavenTool(tool);
|
|
1903
|
+
const ok = mavenPath !== undefined;
|
|
1904
|
+
logSessionStart(`auto-install ${tool.id}: ${ok ? "success" : "failed"} (${Date.now() - startedAt}ms)`);
|
|
1905
|
+
return ok;
|
|
1906
|
+
}
|
|
1801
1907
|
default:
|
|
1802
1908
|
logSessionStart(`auto-install ${tool.id}: unsupported strategy`);
|
|
1803
1909
|
return false;
|
|
@@ -82,7 +82,7 @@ const PRIMARY_DISPATCH_GROUPS = {
|
|
|
82
82
|
filterKinds: ["rust"],
|
|
83
83
|
},
|
|
84
84
|
ruby: {
|
|
85
|
-
mode: "
|
|
85
|
+
mode: "all",
|
|
86
86
|
runnerIds: ["lsp", "rubocop"],
|
|
87
87
|
filterKinds: ["ruby"],
|
|
88
88
|
},
|
|
@@ -109,12 +109,12 @@ const PRIMARY_DISPATCH_GROUPS = {
|
|
|
109
109
|
filterKinds: ["markdown"],
|
|
110
110
|
},
|
|
111
111
|
css: {
|
|
112
|
-
mode: "
|
|
112
|
+
mode: "all",
|
|
113
113
|
runnerIds: ["lsp", "stylelint"],
|
|
114
114
|
filterKinds: ["css"],
|
|
115
115
|
},
|
|
116
116
|
yaml: {
|
|
117
|
-
mode: "
|
|
117
|
+
mode: "all",
|
|
118
118
|
runnerIds: ["lsp", "yamllint"],
|
|
119
119
|
filterKinds: ["yaml"],
|
|
120
120
|
},
|
|
@@ -124,12 +124,12 @@ const PRIMARY_DISPATCH_GROUPS = {
|
|
|
124
124
|
filterKinds: ["sql"],
|
|
125
125
|
},
|
|
126
126
|
html: {
|
|
127
|
-
mode: "
|
|
127
|
+
mode: "all",
|
|
128
128
|
runnerIds: ["lsp", "htmlhint"],
|
|
129
129
|
filterKinds: ["html"],
|
|
130
130
|
},
|
|
131
131
|
docker: {
|
|
132
|
-
mode: "
|
|
132
|
+
mode: "all",
|
|
133
133
|
runnerIds: ["lsp", "hadolint"],
|
|
134
134
|
filterKinds: ["docker"],
|
|
135
135
|
},
|
|
@@ -160,7 +160,7 @@ const PRIMARY_DISPATCH_GROUPS = {
|
|
|
160
160
|
filterKinds: ["java"],
|
|
161
161
|
},
|
|
162
162
|
kotlin: {
|
|
163
|
-
mode: "
|
|
163
|
+
mode: "all",
|
|
164
164
|
runnerIds: ["lsp", "ktlint"],
|
|
165
165
|
filterKinds: ["kotlin"],
|
|
166
166
|
},
|
|
@@ -196,7 +196,7 @@ const PRIMARY_DISPATCH_GROUPS = {
|
|
|
196
196
|
},
|
|
197
197
|
nix: { mode: "fallback", runnerIds: ["lsp"], filterKinds: ["nix"] },
|
|
198
198
|
toml: {
|
|
199
|
-
mode: "
|
|
199
|
+
mode: "all",
|
|
200
200
|
runnerIds: ["lsp", "taplo"],
|
|
201
201
|
filterKinds: ["toml"],
|
|
202
202
|
},
|
|
@@ -12,7 +12,9 @@ import { EventEmitter } from "node:events";
|
|
|
12
12
|
import { access } from "node:fs/promises";
|
|
13
13
|
import { pathToFileURL } from "node:url";
|
|
14
14
|
import { logLatency } from "../latency-logger.js";
|
|
15
|
-
|
|
15
|
+
// vscode-jsonrpc v9 ships an `exports` map exposing the Node entry as the
|
|
16
|
+
// `./node` subpath (no `.js`); the old `/node.js` file path no longer resolves.
|
|
17
|
+
import { createMessageConnection, StreamMessageReader, StreamMessageWriter, } from "vscode-jsonrpc/node";
|
|
16
18
|
import { normalizeMapKey, uriToPath } from "./path-utils.js";
|
|
17
19
|
import { getStrategy } from "./server-strategies.js";
|
|
18
20
|
// --- Constants ---
|
|
@@ -71,7 +71,7 @@ function logSessionStart(message) {
|
|
|
71
71
|
// best-effort logging
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
-
async function resolveAndLaunch(spec, allowInstall) {
|
|
74
|
+
export async function resolveAndLaunch(spec, allowInstall) {
|
|
75
75
|
const toolLabel = spec.managedToolId ??
|
|
76
76
|
spec.candidates[spec.candidates.length - 1] ??
|
|
77
77
|
"unknown";
|
|
@@ -82,6 +82,12 @@ async function resolveAndLaunch(spec, allowInstall) {
|
|
|
82
82
|
lastRuntimeFailure = err instanceof Error ? err : new Error(message);
|
|
83
83
|
}
|
|
84
84
|
};
|
|
85
|
+
// A candidate that fails while a LATER candidate (or managed install)
|
|
86
|
+
// succeeds is just fallback, not a failure — logging each immediately floods
|
|
87
|
+
// the logs with scary "candidate failed / npm shim failed / Run npm install"
|
|
88
|
+
// lines that read as smells even though the launch succeeded. Collect them and
|
|
89
|
+
// surface only if ALL direct candidates fail.
|
|
90
|
+
const candidateFailures = [];
|
|
85
91
|
// Step 1 & 2 — try all explicit candidates (includes bare command = PATH lookup)
|
|
86
92
|
for (const [index, command] of spec.candidates.entries()) {
|
|
87
93
|
logLatency({
|
|
@@ -120,23 +126,29 @@ async function resolveAndLaunch(spec, allowInstall) {
|
|
|
120
126
|
}
|
|
121
127
|
catch (err) {
|
|
122
128
|
const message = err instanceof Error ? err.message : String(err);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
phase: "lsp_launch_candidate_failed",
|
|
126
|
-
filePath: spec.cwd,
|
|
127
|
-
durationMs: 0,
|
|
128
|
-
metadata: {
|
|
129
|
-
tool: toolLabel,
|
|
130
|
-
command,
|
|
131
|
-
index,
|
|
132
|
-
error: message,
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
logSessionStart(`lsp launch candidate failed tool=${toolLabel} idx=${index} command=${command} error=${message}`);
|
|
136
|
-
trackRuntimeFailure(err);
|
|
129
|
+
// Defer logging: only a failure if no later candidate/install succeeds.
|
|
130
|
+
candidateFailures.push({ index, command, message, err });
|
|
137
131
|
// try next
|
|
138
132
|
}
|
|
139
133
|
}
|
|
134
|
+
// All direct candidates failed (a successful one returns above). Surface the
|
|
135
|
+
// deferred failures now so the all-failed case stays fully diagnosable.
|
|
136
|
+
for (const failure of candidateFailures) {
|
|
137
|
+
logLatency({
|
|
138
|
+
type: "phase",
|
|
139
|
+
phase: "lsp_launch_candidate_failed",
|
|
140
|
+
filePath: spec.cwd,
|
|
141
|
+
durationMs: 0,
|
|
142
|
+
metadata: {
|
|
143
|
+
tool: toolLabel,
|
|
144
|
+
command: failure.command,
|
|
145
|
+
index: failure.index,
|
|
146
|
+
error: failure.message,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
logSessionStart(`lsp launch candidate failed tool=${toolLabel} idx=${failure.index} command=${failure.command} error=${failure.message}`);
|
|
150
|
+
trackRuntimeFailure(failure.err);
|
|
151
|
+
}
|
|
140
152
|
if (!canInstall(allowInstall)) {
|
|
141
153
|
logSessionStart(`lsp launch install blocked tool=${toolLabel} cwd=${spec.cwd} allowInstall=${allowInstall !== false} globalDisabled=${isLspInstallDisabled()}`);
|
|
142
154
|
logLatency({
|
package/dist/clients/pipeline.js
CHANGED
|
@@ -21,7 +21,8 @@ import { getDiagnosticLogger } from "./diagnostic-logger.js";
|
|
|
21
21
|
import { getDiagnosticTracker } from "./diagnostic-tracker.js";
|
|
22
22
|
import { computeCascadeForFile, dispatchLintWithResult, } from "./dispatch/integration.js";
|
|
23
23
|
import { toRunnerDisplayPath } from "./dispatch/runner-context.js";
|
|
24
|
-
import { resolveCommandArgsWithInstallFallback, resolveToolCommand, resolveToolCommandWithInstallFallback, } from "./dispatch/runners/utils/runner-helpers.js";
|
|
24
|
+
import { createAvailabilityChecker, resolveAvailableOrInstall, resolveCommandArgsWithInstallFallback, resolveToolCommand, resolveToolCommandWithInstallFallback, } from "./dispatch/runners/utils/runner-helpers.js";
|
|
25
|
+
import { findDetektConfig } from "./dispatch/runners/detekt.js";
|
|
25
26
|
import { detectFileKind, getFileKindLabel } from "./file-kinds.js";
|
|
26
27
|
import { detectFileChangedAfterCommand, getProjectIgnoreMatcher, isExcludedDirName, } from "./file-utils.js";
|
|
27
28
|
import { logLatency } from "./latency-logger.js";
|
|
@@ -31,7 +32,7 @@ import { clearGraphCache } from "./review-graph/builder.js";
|
|
|
31
32
|
import { RUNTIME_CONFIG } from "./runtime-config.js";
|
|
32
33
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
33
34
|
import { formatSecrets, scanForSecrets } from "./secrets-scanner.js";
|
|
34
|
-
import { getAutofixPolicyForFile, getPreferredAutofixTools, getRubocopCommand, hasBiomeConfig, hasEslintConfig, hasRubocopConfig, hasSqlfluffConfig, hasStylelintConfig, } from "./tool-policy.js";
|
|
35
|
+
import { getAutofixPolicyForFile, getPreferredAutofixTools, getRubocopCommand, hasBiomeConfig, hasDetektConfig, hasEslintConfig, hasGolangciConfig, hasKtfmtConfig, hasOxlintConfig, hasRubocopConfig, hasSqlfluffConfig, hasStylelintConfig, } from "./tool-policy.js";
|
|
35
36
|
const LSP_MAX_FILE_BYTES = RUNTIME_CONFIG.pipeline.lspMaxFileBytes;
|
|
36
37
|
const LSP_MAX_FILE_LINES = RUNTIME_CONFIG.pipeline.lspMaxFileLines;
|
|
37
38
|
const LSP_SPAWN_BUDGET_MS = RUNTIME_CONFIG.pipeline.lspSpawnBudgetMs;
|
|
@@ -173,20 +174,26 @@ async function tryEslintFix(filePath, cwd) {
|
|
|
173
174
|
if (dry.status === 2)
|
|
174
175
|
return 0;
|
|
175
176
|
let fixableCount = 0;
|
|
177
|
+
let anyDryRunOutput = false;
|
|
176
178
|
try {
|
|
177
179
|
const results = JSON.parse(dry.stdout);
|
|
178
180
|
fixableCount = results.reduce((sum, r) => sum + (r.fixableErrorCount ?? 0) + (r.fixableWarningCount ?? 0), 0);
|
|
181
|
+
// `--fix-dry-run` reports the POST-fix state: when every problem is
|
|
182
|
+
// auto-fixable, `messages`/`fixableErrorCount` are 0 and the fixed source
|
|
183
|
+
// lands in the `output` field instead. Keying on `fixableErrorCount` alone
|
|
184
|
+
// therefore misses the common "all fixable" case and never applies fixes.
|
|
185
|
+
anyDryRunOutput = results.some((r) => typeof r.output === "string");
|
|
179
186
|
}
|
|
180
187
|
catch {
|
|
181
188
|
/* treat as zero fixable on error */
|
|
182
189
|
}
|
|
183
|
-
if (fixableCount === 0)
|
|
190
|
+
if (fixableCount === 0 && !anyDryRunOutput)
|
|
184
191
|
return 0;
|
|
185
192
|
// Apply the fixes
|
|
186
193
|
const fix = await safeSpawnAsync(cmd, ["--fix", "--no-error-on-unmatched-pattern", ...configArgs, filePath], { timeout: 30000, cwd });
|
|
187
194
|
if (fix.status === 2)
|
|
188
195
|
return 0;
|
|
189
|
-
return fixableCount;
|
|
196
|
+
return fixableCount > 0 ? fixableCount : anyDryRunOutput ? 1 : 0;
|
|
190
197
|
}
|
|
191
198
|
async function tryStylelintFix(filePath, cwd) {
|
|
192
199
|
const cmd = await resolveToolCommandWithInstallFallback(cwd, "stylelint");
|
|
@@ -216,6 +223,56 @@ async function tryKtlintFix(filePath, cwd) {
|
|
|
216
223
|
return 0;
|
|
217
224
|
return detectFileChangedAfterCommand(filePath, cmd, ["-F", filePath], cwd, [1]);
|
|
218
225
|
}
|
|
226
|
+
// golangci-lint/detekt/ktfmt have no TOOL_COMMAND_SPEC; resolve via availability
|
|
227
|
+
// checkers like their runners do.
|
|
228
|
+
const golangciAutofixChecker = createAvailabilityChecker("golangci-lint", ".exe");
|
|
229
|
+
const detektAutofixChecker = createAvailabilityChecker("detekt", ".bat");
|
|
230
|
+
const ktfmtAutofixChecker = createAvailabilityChecker("ktfmt", ".bat");
|
|
231
|
+
async function tryKtfmtFix(filePath, cwd) {
|
|
232
|
+
// Config-first: the autofix policy only reaches here when the project opted
|
|
233
|
+
// into ktfmt, so resolveAvailableOrInstall honors that gate. ktfmt writes the
|
|
234
|
+
// formatted file in place and exits 0; treat any byte change as the fix.
|
|
235
|
+
const cmd = await resolveAvailableOrInstall(ktfmtAutofixChecker, "ktfmt", cwd);
|
|
236
|
+
if (!cmd)
|
|
237
|
+
return 0;
|
|
238
|
+
const absPath = path.resolve(cwd, filePath);
|
|
239
|
+
return detectFileChangedAfterCommand(filePath, cmd, [absPath], cwd, [0]);
|
|
240
|
+
}
|
|
241
|
+
async function tryGolangciLintFix(filePath, cwd) {
|
|
242
|
+
// Config-first: the autofix policy only reaches here when a .golangci.* config
|
|
243
|
+
// exists. resolveAvailableOrInstall honors that gate (won't auto-install a
|
|
244
|
+
// config-first tool). golangci-lint exits non-zero when issues remain after
|
|
245
|
+
// --fix, so allow its issue-found codes.
|
|
246
|
+
const cmd = await resolveAvailableOrInstall(golangciAutofixChecker, "golangci-lint", cwd);
|
|
247
|
+
if (!cmd)
|
|
248
|
+
return 0;
|
|
249
|
+
return detectFileChangedAfterCommand(filePath, cmd, ["run", "--fix", filePath], cwd, [1, 7]);
|
|
250
|
+
}
|
|
251
|
+
async function tryDetektFix(filePath, cwd) {
|
|
252
|
+
const configPath = findDetektConfig(cwd);
|
|
253
|
+
if (!configPath)
|
|
254
|
+
return 0;
|
|
255
|
+
if (!(await detektAutofixChecker.isAvailableAsync(cwd)))
|
|
256
|
+
return 0;
|
|
257
|
+
const cmd = detektAutofixChecker.getCommand(cwd);
|
|
258
|
+
if (!cmd)
|
|
259
|
+
return 0;
|
|
260
|
+
const absPath = path.resolve(cwd, filePath);
|
|
261
|
+
return detectFileChangedAfterCommand(filePath, cmd, ["--auto-correct", "--input", absPath, "--config", configPath], cwd, [1, 2]);
|
|
262
|
+
}
|
|
263
|
+
async function tryMarkdownlintFix(filePath, cwd) {
|
|
264
|
+
const cmd = await resolveToolCommandWithInstallFallback(cwd, "markdownlint");
|
|
265
|
+
if (!cmd)
|
|
266
|
+
return 0;
|
|
267
|
+
// markdownlint-cli2 --fix exits non-zero when unfixable violations remain.
|
|
268
|
+
return detectFileChangedAfterCommand(filePath, cmd, ["--fix", filePath], cwd, [1]);
|
|
269
|
+
}
|
|
270
|
+
async function tryOxlintFix(filePath, cwd) {
|
|
271
|
+
const cmd = await resolveToolCommandWithInstallFallback(cwd, "oxlint");
|
|
272
|
+
if (!cmd)
|
|
273
|
+
return 0;
|
|
274
|
+
return detectFileChangedAfterCommand(filePath, cmd, ["--fix", filePath], cwd, [1]);
|
|
275
|
+
}
|
|
219
276
|
async function tryRustClippyFix(filePath) {
|
|
220
277
|
const check = await safeSpawnAsync("cargo", ["--version"], { timeout: 5000 });
|
|
221
278
|
if (check.error || check.status !== 0)
|
|
@@ -248,7 +305,7 @@ async function tryDartFix(filePath) {
|
|
|
248
305
|
return diffProjectSnapshot(pubspecDir, before);
|
|
249
306
|
}
|
|
250
307
|
// --- Pipeline phase helpers ---
|
|
251
|
-
async function runAutofix(filePath, cwd, getFlag, dbg, deps) {
|
|
308
|
+
export async function runAutofix(filePath, cwd, getFlag, dbg, deps) {
|
|
252
309
|
const { biomeClient, ruffClient, fixedThisTurn } = deps;
|
|
253
310
|
const noAutofix = getFlag("no-autofix");
|
|
254
311
|
let fixedCount = 0;
|
|
@@ -285,6 +342,10 @@ async function runAutofix(filePath, cwd, getFlag, dbg, deps) {
|
|
|
285
342
|
hasSqlfluffConfig: hasSqlfluffConfig(cwd),
|
|
286
343
|
hasRubocopConfig: hasRubocopConfig(cwd),
|
|
287
344
|
hasBiomeConfig: hasBiomeConfig(cwd),
|
|
345
|
+
hasGolangciConfig: hasGolangciConfig(cwd),
|
|
346
|
+
hasDetektConfig: hasDetektConfig(cwd),
|
|
347
|
+
hasKtfmtConfig: hasKtfmtConfig(cwd),
|
|
348
|
+
hasOxlintConfig: hasOxlintConfig(cwd),
|
|
288
349
|
};
|
|
289
350
|
const autofixPolicy = getAutofixPolicyForFile(filePath, autofixContext);
|
|
290
351
|
const preferredAutofixTools = autofixPolicy?.safe
|
|
@@ -438,6 +499,67 @@ async function runAutofix(filePath, cwd, getFlag, dbg, deps) {
|
|
|
438
499
|
dbg(`autofix: dart fix changed ${dartChangedFiles.length} file(s) from ${filePath}`);
|
|
439
500
|
needsContentRefresh = true;
|
|
440
501
|
}
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (toolName === "golangci-lint") {
|
|
505
|
+
const fixed = await tryGolangciLintFix(filePath, cwd);
|
|
506
|
+
if (fixed > 0) {
|
|
507
|
+
fixedCount += fixed;
|
|
508
|
+
autofixTools.push(`golangci-lint:${fixed}`);
|
|
509
|
+
fixedThisTurn.add(filePath);
|
|
510
|
+
markTargetChanged();
|
|
511
|
+
dbg(`autofix: golangci-lint fixed ${filePath}`);
|
|
512
|
+
needsContentRefresh = true;
|
|
513
|
+
}
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (toolName === "detekt") {
|
|
517
|
+
const fixed = await tryDetektFix(filePath, cwd);
|
|
518
|
+
if (fixed > 0) {
|
|
519
|
+
fixedCount += fixed;
|
|
520
|
+
autofixTools.push(`detekt:${fixed}`);
|
|
521
|
+
fixedThisTurn.add(filePath);
|
|
522
|
+
markTargetChanged();
|
|
523
|
+
dbg(`autofix: detekt --auto-correct fixed ${filePath}`);
|
|
524
|
+
needsContentRefresh = true;
|
|
525
|
+
}
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (toolName === "ktfmt") {
|
|
529
|
+
const fixed = await tryKtfmtFix(filePath, cwd);
|
|
530
|
+
if (fixed > 0) {
|
|
531
|
+
fixedCount += fixed;
|
|
532
|
+
autofixTools.push(`ktfmt:${fixed}`);
|
|
533
|
+
fixedThisTurn.add(filePath);
|
|
534
|
+
markTargetChanged();
|
|
535
|
+
dbg(`autofix: ktfmt formatted ${filePath}`);
|
|
536
|
+
needsContentRefresh = true;
|
|
537
|
+
}
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (toolName === "markdownlint") {
|
|
541
|
+
const fixed = await tryMarkdownlintFix(filePath, cwd);
|
|
542
|
+
if (fixed > 0) {
|
|
543
|
+
fixedCount += fixed;
|
|
544
|
+
autofixTools.push(`markdownlint:${fixed}`);
|
|
545
|
+
fixedThisTurn.add(filePath);
|
|
546
|
+
markTargetChanged();
|
|
547
|
+
dbg(`autofix: markdownlint --fix fixed ${filePath}`);
|
|
548
|
+
needsContentRefresh = true;
|
|
549
|
+
}
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (toolName === "oxlint") {
|
|
553
|
+
const fixed = await tryOxlintFix(filePath, cwd);
|
|
554
|
+
if (fixed > 0) {
|
|
555
|
+
fixedCount += fixed;
|
|
556
|
+
autofixTools.push(`oxlint:${fixed}`);
|
|
557
|
+
fixedThisTurn.add(filePath);
|
|
558
|
+
markTargetChanged();
|
|
559
|
+
dbg(`autofix: oxlint --fix fixed ${filePath}`);
|
|
560
|
+
needsContentRefresh = true;
|
|
561
|
+
}
|
|
562
|
+
continue;
|
|
441
563
|
}
|
|
442
564
|
}
|
|
443
565
|
if (attemptedTools.length > 0 && autofixTools.length === 0) {
|
|
@@ -47,6 +47,22 @@ function cmdEscapeArg(arg) {
|
|
|
47
47
|
return arg;
|
|
48
48
|
return `"${arg.replace(/"/g, '""')}"`;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Build the cmd.exe command string used for Windows `shell:true` spawning.
|
|
52
|
+
*
|
|
53
|
+
* The COMMAND must be escaped the same way as the args — escaping only the args
|
|
54
|
+
* (the bug behind #214) means a tool whose resolved path contains a space (e.g.
|
|
55
|
+
* `C:\Program Files\Go\bin\go.exe`) makes cmd.exe parse `C:\Program` as the
|
|
56
|
+
* command and fail with "'C:\Program' is not recognized". `cmdEscapeArg` is a
|
|
57
|
+
* no-op for space-free commands, so this is safe for the npm/.pi-lens tool paths
|
|
58
|
+
* that already worked. The `chcp 65001` prefix forces the UTF-8 code page (so
|
|
59
|
+
* tool output isn't mangled by the system code page) and, as a side benefit,
|
|
60
|
+
* keeps the (possibly quoted) command off the front of the line, avoiding
|
|
61
|
+
* cmd.exe's `/s` outer-quote-stripping quirk.
|
|
62
|
+
*/
|
|
63
|
+
export function buildWindowsShellCommand(command, args) {
|
|
64
|
+
return `chcp 65001 >nul 2>&1 && ${[command, ...args].map(cmdEscapeArg).join(" ")}`;
|
|
65
|
+
}
|
|
50
66
|
// ============================================================================
|
|
51
67
|
// ASYNC VERSION (Recommended - Non-blocking)
|
|
52
68
|
// ============================================================================
|
|
@@ -93,7 +109,7 @@ export async function safeSpawnAsync(command, args, options) {
|
|
|
93
109
|
// etc.) have their bytes decoded as the system code page (CP850/CP1252/
|
|
94
110
|
// CP936/CP932), producing garbled characters in stderr error messages.
|
|
95
111
|
const spawnCmd = isWindows
|
|
96
|
-
?
|
|
112
|
+
? buildWindowsShellCommand(command, args)
|
|
97
113
|
: command;
|
|
98
114
|
const spawnArgs = isWindows ? [] : args;
|
|
99
115
|
const child = spawn(spawnCmd, spawnArgs, {
|