pi-lens 3.8.39 → 3.8.41
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 +84 -5
- package/README.md +37 -1
- package/clients/biome-client.ts +5 -4
- package/clients/cache/rule-cache.ts +1 -1
- package/clients/complexity-client.ts +1 -1
- package/clients/dependency-checker.ts +1 -1
- package/clients/dispatch/diagnostic-taxonomy.ts +13 -1
- package/clients/dispatch/dispatcher.ts +9 -0
- package/clients/dispatch/fact-scheduler.ts +1 -1
- package/clients/dispatch/integration.ts +58 -3
- package/clients/dispatch/runners/index.ts +2 -0
- package/clients/dispatch/runners/semgrep.ts +269 -0
- package/clients/dispatch/runners/shellcheck.ts +2 -8
- package/clients/dispatch/runners/tree-sitter.ts +32 -11
- package/clients/dispatch/tool-profile.ts +1 -0
- package/clients/format-service.ts +10 -0
- package/clients/formatters.ts +22 -8
- package/clients/installer/index.ts +3 -3
- package/clients/knip-client.ts +360 -362
- package/clients/lsp/aggregation.ts +91 -0
- package/clients/lsp/client.ts +91 -38
- package/clients/lsp/index.ts +88 -72
- package/clients/lsp/launch.ts +107 -34
- package/clients/lsp/server-strategies.ts +71 -0
- package/clients/lsp/server.ts +76 -57
- package/clients/path-utils.ts +17 -0
- package/clients/pipeline.ts +23 -5
- package/clients/production-readiness.ts +2 -2
- package/clients/read-guard-logger.ts +41 -1
- package/clients/read-guard-tool-lines.ts +17 -4
- package/clients/read-guard.ts +95 -46
- package/clients/runtime-agent-end.ts +3 -0
- package/clients/runtime-session.ts +5 -0
- package/clients/runtime-tool-result.ts +48 -1
- package/clients/runtime-turn.ts +48 -4
- package/clients/sanitize.ts +1 -1
- package/clients/semgrep-config.ts +213 -0
- package/clients/tool-policy.ts +1982 -1936
- package/clients/tree-sitter-client.ts +1 -1
- package/clients/widget-state.ts +283 -0
- package/commands/booboo.ts +34 -2
- package/index.ts +231 -17
- package/package.json +3 -2
- package/rules/rule-catalog.json +25 -1
- package/rules/tree-sitter-queries/cobol/lock-table-cobol.yml +35 -0
- package/rules/tree-sitter-queries/cpp/unnecessary-bit-ops.yml +58 -0
- package/rules/tree-sitter-queries/java/infinite-loop.yml +58 -0
- package/rules/tree-sitter-queries/java/infinite-recursion.yml +58 -0
- package/rules/tree-sitter-queries/java/mockito-initialized.yml +66 -0
- package/rules/tree-sitter-queries/java/name-capitalization-conflict.yml +54 -0
- package/rules/tree-sitter-queries/java/no-octal-values.yml +48 -0
- package/rules/tree-sitter-queries/java/resources-closed.yml +57 -0
- package/rules/tree-sitter-queries/java/short-circuit-logic.yml +57 -0
- package/rules/tree-sitter-queries/java/tests-include-assertions.yml +60 -0
- package/rules/tree-sitter-queries/java/unnecessary-bit-ops-java.yml +57 -0
- package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +64 -0
- package/rules/tree-sitter-queries/plsql/lock-table.yml +42 -0
- package/rules/tree-sitter-queries/plsql/nchar-nvarchar2-bytes.yml +54 -0
- package/rules/tree-sitter-queries/python/no-super-torchscript.yml +52 -0
- package/rules/tree-sitter-queries/typescript/default-not-last.yml +54 -0
- package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +51 -0
- package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +54 -0
- package/rules/tree-sitter-queries/typescript/infinite-loop.yml +55 -0
- package/rules/tree-sitter-queries/typescript/self-assignment.yml +46 -0
- package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +64 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { classifyDefect } from "../diagnostic-taxonomy.js";
|
|
3
|
+
import { PRIORITY } from "../priorities.js";
|
|
4
|
+
import type {
|
|
5
|
+
DefectClass,
|
|
6
|
+
Diagnostic,
|
|
7
|
+
DispatchContext,
|
|
8
|
+
OutputSemantic,
|
|
9
|
+
RunnerDefinition,
|
|
10
|
+
RunnerResult,
|
|
11
|
+
} from "../types.js";
|
|
12
|
+
import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
13
|
+
import { resolveSemgrepConfig } from "../../semgrep-config.js";
|
|
14
|
+
import { createAvailabilityChecker } from "./utils/runner-helpers.js";
|
|
15
|
+
|
|
16
|
+
const semgrep = createAvailabilityChecker("semgrep", ".exe");
|
|
17
|
+
const MAX_DIAGNOSTICS = 50;
|
|
18
|
+
|
|
19
|
+
interface SemgrepJsonOutput {
|
|
20
|
+
results?: SemgrepResult[];
|
|
21
|
+
errors?: Array<{ message?: string; type?: string; level?: string }>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface SemgrepResult {
|
|
25
|
+
check_id?: string;
|
|
26
|
+
path?: string;
|
|
27
|
+
start?: { line?: number; col?: number };
|
|
28
|
+
extra?: {
|
|
29
|
+
message?: string;
|
|
30
|
+
severity?: string;
|
|
31
|
+
metadata?: Record<string, unknown>;
|
|
32
|
+
fix?: string;
|
|
33
|
+
fix_regex?: unknown;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getPiLensMetadata(
|
|
38
|
+
metadata: Record<string, unknown>,
|
|
39
|
+
): Record<string, unknown> {
|
|
40
|
+
const nested = metadata["pi-lens"] ?? metadata.pi_lens;
|
|
41
|
+
return nested && typeof nested === "object"
|
|
42
|
+
? (nested as Record<string, unknown>)
|
|
43
|
+
: {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function metadataString(
|
|
47
|
+
metadata: Record<string, unknown>,
|
|
48
|
+
piLens: Record<string, unknown>,
|
|
49
|
+
key: string,
|
|
50
|
+
): string | undefined {
|
|
51
|
+
const direct = piLens[key] ?? metadata[`pi_lens_${key}`];
|
|
52
|
+
return typeof direct === "string" && direct.trim()
|
|
53
|
+
? direct.trim()
|
|
54
|
+
: undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function metadataBoolean(
|
|
58
|
+
metadata: Record<string, unknown>,
|
|
59
|
+
piLens: Record<string, unknown>,
|
|
60
|
+
key: string,
|
|
61
|
+
): boolean {
|
|
62
|
+
return piLens[key] === true || metadata[`pi_lens_${key}`] === true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeDefectClass(
|
|
66
|
+
value: string | undefined,
|
|
67
|
+
): DefectClass | undefined {
|
|
68
|
+
if (!value) return undefined;
|
|
69
|
+
const normalized = value.toLowerCase().replace(/_/g, "-");
|
|
70
|
+
if (
|
|
71
|
+
normalized === "silent-error" ||
|
|
72
|
+
normalized === "injection" ||
|
|
73
|
+
normalized === "secrets" ||
|
|
74
|
+
normalized === "async-misuse" ||
|
|
75
|
+
normalized === "correctness" ||
|
|
76
|
+
normalized === "safety" ||
|
|
77
|
+
normalized === "style" ||
|
|
78
|
+
normalized === "unknown" ||
|
|
79
|
+
normalized === "unused-value"
|
|
80
|
+
) {
|
|
81
|
+
return normalized;
|
|
82
|
+
}
|
|
83
|
+
if (
|
|
84
|
+
normalized.includes("traversal") ||
|
|
85
|
+
normalized.includes("ssrf") ||
|
|
86
|
+
normalized.includes("xss") ||
|
|
87
|
+
normalized.includes("deserial") ||
|
|
88
|
+
normalized.includes("crypto") ||
|
|
89
|
+
normalized.includes("auth")
|
|
90
|
+
) {
|
|
91
|
+
return "safety";
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function semgrepSemantic(
|
|
97
|
+
result: SemgrepResult,
|
|
98
|
+
defectClass: DefectClass,
|
|
99
|
+
): OutputSemantic {
|
|
100
|
+
const metadata = result.extra?.metadata ?? {};
|
|
101
|
+
const piLens = getPiLensMetadata(metadata);
|
|
102
|
+
const explicitSemantic = metadataString(metadata, piLens, "semantic");
|
|
103
|
+
if (
|
|
104
|
+
explicitSemantic === "blocking" ||
|
|
105
|
+
metadataBoolean(metadata, piLens, "blocking")
|
|
106
|
+
) {
|
|
107
|
+
return "blocking";
|
|
108
|
+
}
|
|
109
|
+
if (explicitSemantic === "warning" || explicitSemantic === "silent") {
|
|
110
|
+
return explicitSemantic;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const severity = String(result.extra?.severity ?? "").toUpperCase();
|
|
114
|
+
const confidence = String(
|
|
115
|
+
metadata.confidence ??
|
|
116
|
+
metadata.semgrep_confidence ??
|
|
117
|
+
piLens.confidence ??
|
|
118
|
+
"",
|
|
119
|
+
).toLowerCase();
|
|
120
|
+
const highSignalSecurity =
|
|
121
|
+
defectClass === "injection" ||
|
|
122
|
+
defectClass === "secrets" ||
|
|
123
|
+
defectClass === "safety";
|
|
124
|
+
|
|
125
|
+
if (severity === "ERROR" && highSignalSecurity && confidence !== "low") {
|
|
126
|
+
return "blocking";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return "warning";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function mapSeverity(
|
|
133
|
+
semgrepSeverity: string | undefined,
|
|
134
|
+
semantic: OutputSemantic,
|
|
135
|
+
): Diagnostic["severity"] {
|
|
136
|
+
if (semantic === "blocking") return "error";
|
|
137
|
+
const severity = String(semgrepSeverity ?? "").toUpperCase();
|
|
138
|
+
if (severity === "ERROR") return "error";
|
|
139
|
+
if (severity === "INFO") return "info";
|
|
140
|
+
return "warning";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseSemgrepJson(raw: string, ctx: DispatchContext): Diagnostic[] {
|
|
144
|
+
if (!raw.trim()) return [];
|
|
145
|
+
let parsed: SemgrepJsonOutput;
|
|
146
|
+
try {
|
|
147
|
+
parsed = JSON.parse(raw) as SemgrepJsonOutput;
|
|
148
|
+
} catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const results = Array.isArray(parsed.results) ? parsed.results : [];
|
|
153
|
+
const diagnostics: Diagnostic[] = [];
|
|
154
|
+
|
|
155
|
+
for (const [index, result] of results.entries()) {
|
|
156
|
+
if (diagnostics.length >= MAX_DIAGNOSTICS) break;
|
|
157
|
+
const rule = result.check_id || "semgrep";
|
|
158
|
+
const message = result.extra?.message || rule;
|
|
159
|
+
const metadata = result.extra?.metadata ?? {};
|
|
160
|
+
const piLens = getPiLensMetadata(metadata);
|
|
161
|
+
const explicitDefect = normalizeDefectClass(
|
|
162
|
+
metadataString(metadata, piLens, "defect_class"),
|
|
163
|
+
);
|
|
164
|
+
const defectClass =
|
|
165
|
+
explicitDefect ?? classifyDefect(rule, "semgrep", message);
|
|
166
|
+
const semantic = semgrepSemantic(result, defectClass);
|
|
167
|
+
const filePath = result.path || ctx.filePath;
|
|
168
|
+
const line = result.start?.line ?? 1;
|
|
169
|
+
const column = result.start?.col ?? 1;
|
|
170
|
+
const fixSuggestion =
|
|
171
|
+
metadataString(metadata, piLens, "fix") ??
|
|
172
|
+
(typeof result.extra?.fix === "string" ? result.extra.fix : undefined);
|
|
173
|
+
|
|
174
|
+
diagnostics.push({
|
|
175
|
+
id: `semgrep:${rule}:${path.basename(filePath)}:${line}:${column}:${index}`,
|
|
176
|
+
message: `[${rule}] ${message}`,
|
|
177
|
+
filePath,
|
|
178
|
+
line,
|
|
179
|
+
column,
|
|
180
|
+
severity: mapSeverity(result.extra?.severity, semantic),
|
|
181
|
+
semantic,
|
|
182
|
+
tool: "semgrep",
|
|
183
|
+
rule,
|
|
184
|
+
defectClass,
|
|
185
|
+
fixable: Boolean(fixSuggestion || result.extra?.fix_regex),
|
|
186
|
+
autoFixAvailable: false,
|
|
187
|
+
fixKind:
|
|
188
|
+
fixSuggestion || result.extra?.fix_regex ? "suggestion" : undefined,
|
|
189
|
+
fixSuggestion,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return diagnostics;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const semgrepRunner: RunnerDefinition = {
|
|
197
|
+
id: "semgrep",
|
|
198
|
+
appliesTo: [
|
|
199
|
+
"csharp",
|
|
200
|
+
"css",
|
|
201
|
+
"cxx",
|
|
202
|
+
"dart",
|
|
203
|
+
"docker",
|
|
204
|
+
"go",
|
|
205
|
+
"html",
|
|
206
|
+
"java",
|
|
207
|
+
"json",
|
|
208
|
+
"jsts",
|
|
209
|
+
"kotlin",
|
|
210
|
+
"lua",
|
|
211
|
+
"php",
|
|
212
|
+
"python",
|
|
213
|
+
"ruby",
|
|
214
|
+
"rust",
|
|
215
|
+
"shell",
|
|
216
|
+
"swift",
|
|
217
|
+
"terraform",
|
|
218
|
+
"yaml",
|
|
219
|
+
],
|
|
220
|
+
priority: PRIORITY.DEEP_LANGUAGE_ANALYSIS,
|
|
221
|
+
enabledByDefault: false,
|
|
222
|
+
|
|
223
|
+
async when(ctx: DispatchContext): Promise<boolean> {
|
|
224
|
+
return resolveSemgrepConfig(ctx.cwd, {
|
|
225
|
+
enabled: Boolean(ctx.pi.getFlag("lens-semgrep")),
|
|
226
|
+
config: ctx.pi.getFlag("lens-semgrep-config"),
|
|
227
|
+
}).enabled;
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
231
|
+
const cwd = ctx.cwd || process.cwd();
|
|
232
|
+
const resolved = resolveSemgrepConfig(cwd, {
|
|
233
|
+
enabled: Boolean(ctx.pi.getFlag("lens-semgrep")),
|
|
234
|
+
config: ctx.pi.getFlag("lens-semgrep-config"),
|
|
235
|
+
});
|
|
236
|
+
if (!resolved.enabled) {
|
|
237
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!semgrep.isAvailable(cwd)) {
|
|
241
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
242
|
+
}
|
|
243
|
+
const cmd = semgrep.getCommand(cwd) ?? "semgrep";
|
|
244
|
+
const args = ["scan", "--json", "--metrics=off", "--timeout", "5"];
|
|
245
|
+
if (resolved.configArg) args.push("--config", resolved.configArg);
|
|
246
|
+
args.push(ctx.filePath);
|
|
247
|
+
|
|
248
|
+
const result = await safeSpawnAsync(cmd, args, { cwd, timeout: 20000 });
|
|
249
|
+
const raw = result.stdout || "";
|
|
250
|
+
const diagnostics = parseSemgrepJson(raw, ctx);
|
|
251
|
+
if (diagnostics.length === 0) {
|
|
252
|
+
return {
|
|
253
|
+
status: result.error ? "failed" : "succeeded",
|
|
254
|
+
diagnostics: [],
|
|
255
|
+
semantic: "none",
|
|
256
|
+
rawOutput: (result.stderr || "").slice(0, 500),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
|
|
261
|
+
return {
|
|
262
|
+
status: hasBlocking ? "failed" : "succeeded",
|
|
263
|
+
diagnostics,
|
|
264
|
+
semantic: hasBlocking ? "blocking" : "warning",
|
|
265
|
+
};
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export default semgrepRunner;
|
|
@@ -148,14 +148,8 @@ const shellcheckRunner: RunnerDefinition = {
|
|
|
148
148
|
}
|
|
149
149
|
if (!cmd) return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
150
150
|
|
|
151
|
-
// Determine shell dialect from file extension
|
|
152
|
-
const shellDialect =
|
|
153
|
-
? "bash"
|
|
154
|
-
: ctx.filePath.endsWith(".fish")
|
|
155
|
-
? "bash"
|
|
156
|
-
: ctx.filePath.endsWith(".sh")
|
|
157
|
-
? "bash"
|
|
158
|
-
: "bash"; // Default to bash for generic shell files
|
|
151
|
+
// Determine shell dialect from file extension (all map to bash for shellcheck)
|
|
152
|
+
const shellDialect = "bash";
|
|
159
153
|
|
|
160
154
|
// Build args
|
|
161
155
|
// --format json: JSON output
|
|
@@ -32,6 +32,11 @@ import type {
|
|
|
32
32
|
// Creating a new TreeSitterClient() on every write resets TRANSFER_BUFFER (a module-level
|
|
33
33
|
// WASM pointer) — concurrent writes race on _ts_init() and corrupt shared WASM state → crash.
|
|
34
34
|
let _sharedClient: TreeSitterClient | null = null;
|
|
35
|
+
// Once the wasm runtime aborts, the entire module-level wasm heap is corrupted — no
|
|
36
|
+
// recovery is possible within this process. Flag it and skip all further tree-sitter work
|
|
37
|
+
// rather than re-invoking the dead runtime (which prints "Aborted()" on every call and
|
|
38
|
+
// leaks memory on each retry).
|
|
39
|
+
let _wasmAborted = false;
|
|
35
40
|
const blastCooldownByFile = new Map<string, number>();
|
|
36
41
|
const BLAST_COOLDOWN_MS = 5_000;
|
|
37
42
|
|
|
@@ -244,7 +249,8 @@ function isLineInModifiedRanges(
|
|
|
244
249
|
return ranges.some((r) => line >= r.start && line <= r.end);
|
|
245
250
|
}
|
|
246
251
|
|
|
247
|
-
function getSharedClient(): TreeSitterClient {
|
|
252
|
+
function getSharedClient(): TreeSitterClient | null {
|
|
253
|
+
if (_wasmAborted) return null;
|
|
248
254
|
if (!_sharedClient) {
|
|
249
255
|
_sharedClient = new TreeSitterClient();
|
|
250
256
|
}
|
|
@@ -276,11 +282,11 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
276
282
|
// Use singleton client — WASM must never be re-initialized after first call
|
|
277
283
|
const client = getSharedClient();
|
|
278
284
|
logTreeSitter({ phase: "runner_start", filePath: ctx.filePath });
|
|
279
|
-
if (!client.isAvailable()) {
|
|
285
|
+
if (!client || !client.isAvailable()) {
|
|
280
286
|
logTreeSitter({
|
|
281
287
|
phase: "runner_skip",
|
|
282
288
|
filePath: ctx.filePath,
|
|
283
|
-
reason: "client_unavailable",
|
|
289
|
+
reason: _wasmAborted ? "wasm_aborted" : "client_unavailable",
|
|
284
290
|
status: "skipped",
|
|
285
291
|
});
|
|
286
292
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
@@ -506,14 +512,29 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
506
512
|
}
|
|
507
513
|
} catch (err) {
|
|
508
514
|
// pi-lens-ignore: missing-error-propagation — per-query resilience loop, intentional
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
515
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
516
|
+
// Emscripten abort() corrupts the entire module-level wasm heap.
|
|
517
|
+
// Poison the singleton so no further queries attempt to use the dead runtime.
|
|
518
|
+
if (msg.includes("Aborted") || msg.includes("abort()")) {
|
|
519
|
+
_wasmAborted = true;
|
|
520
|
+
_sharedClient = null;
|
|
521
|
+
logTreeSitter({
|
|
522
|
+
phase: "query_error",
|
|
523
|
+
filePath,
|
|
524
|
+
languageId,
|
|
525
|
+
queryId: query.id,
|
|
526
|
+
error: "wasm_aborted_fatal",
|
|
527
|
+
});
|
|
528
|
+
} else {
|
|
529
|
+
console.error(`[tree-sitter] Query ${query.id} failed:`, err);
|
|
530
|
+
logTreeSitter({
|
|
531
|
+
phase: "query_error",
|
|
532
|
+
filePath,
|
|
533
|
+
languageId,
|
|
534
|
+
queryId: query.id,
|
|
535
|
+
error: msg,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
517
538
|
}
|
|
518
539
|
return queryDiagnostics;
|
|
519
540
|
}),
|
|
@@ -26,6 +26,7 @@ const TOOL_PROFILE_MAP: Record<string, ToolProfile> = {
|
|
|
26
26
|
"rust-clippy": { dedupPriority: 95, lintLike: true },
|
|
27
27
|
shellcheck: { dedupPriority: 95, lintLike: true },
|
|
28
28
|
"type-safety": { dedupPriority: 95, lintLike: true },
|
|
29
|
+
semgrep: { dedupPriority: 105, lintLike: false },
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
export function getToolProfile(
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import * as path from "node:path";
|
|
15
|
+
import { recordFormatter } from "./widget-state.js";
|
|
15
16
|
import { FileTime } from "./file-time.js";
|
|
16
17
|
import {
|
|
17
18
|
clearFormatterRuntimeState,
|
|
@@ -114,6 +115,15 @@ export class FormatService {
|
|
|
114
115
|
// Record new file state after formatting
|
|
115
116
|
this.fileTime.read(absolutePath);
|
|
116
117
|
|
|
118
|
+
for (const [index, result] of results.entries()) {
|
|
119
|
+
recordFormatter(
|
|
120
|
+
absolutePath,
|
|
121
|
+
formatters[index]?.name ?? "unknown",
|
|
122
|
+
result.changed,
|
|
123
|
+
result.success,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
117
127
|
// Build summary
|
|
118
128
|
const anyChanged = results.some((r) => r.changed);
|
|
119
129
|
const allSucceeded = results.every((r) => r.success);
|
package/clients/formatters.ts
CHANGED
|
@@ -778,7 +778,11 @@ export const cmakeFormatFormatter: FormatterInfo = {
|
|
|
778
778
|
|
|
779
779
|
export const psscriptanalyzerFormatFormatter: FormatterInfo = {
|
|
780
780
|
name: "psscriptanalyzer-format",
|
|
781
|
-
command: [
|
|
781
|
+
command: [
|
|
782
|
+
"pwsh",
|
|
783
|
+
"-Command",
|
|
784
|
+
"Invoke-Formatter -ScriptDefinition (Get-Content -Raw '$FILE') | Set-Content '$FILE'",
|
|
785
|
+
],
|
|
782
786
|
extensions: [".ps1", ".psm1", ".psd1"],
|
|
783
787
|
async resolveCommand(filePath, _cwd) {
|
|
784
788
|
const pwsh = (await which("pwsh")) ?? (await which("powershell"));
|
|
@@ -794,11 +798,15 @@ export const psscriptanalyzerFormatFormatter: FormatterInfo = {
|
|
|
794
798
|
const pwsh = (await which("pwsh")) ?? (await which("powershell"));
|
|
795
799
|
if (!pwsh) return false;
|
|
796
800
|
// Check PSScriptAnalyzer module is available
|
|
797
|
-
const result = safeSpawn(
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
801
|
+
const result = safeSpawn(
|
|
802
|
+
pwsh,
|
|
803
|
+
[
|
|
804
|
+
"-NoProfile",
|
|
805
|
+
"-Command",
|
|
806
|
+
"Get-Module -ListAvailable PSScriptAnalyzer | Select-Object -First 1 -ExpandProperty Name",
|
|
807
|
+
],
|
|
808
|
+
{ timeout: 5_000 },
|
|
809
|
+
);
|
|
802
810
|
return (result.stdout ?? "").includes("PSScriptAnalyzer");
|
|
803
811
|
},
|
|
804
812
|
};
|
|
@@ -925,7 +933,9 @@ export async function getFormattersForFile(
|
|
|
925
933
|
} else if (!formatterPolicy) {
|
|
926
934
|
selectionReason = "detect";
|
|
927
935
|
} else {
|
|
928
|
-
selectionReason = candidateFormatters.some((f) =>
|
|
936
|
+
selectionReason = candidateFormatters.some((f) =>
|
|
937
|
+
hasExplicitFormatterConfig(f.name, cwd),
|
|
938
|
+
)
|
|
929
939
|
? "explicit-config"
|
|
930
940
|
: "smart-default";
|
|
931
941
|
}
|
|
@@ -934,7 +944,11 @@ export async function getFormattersForFile(
|
|
|
934
944
|
phase: "formatter_selected",
|
|
935
945
|
filePath: filePath,
|
|
936
946
|
durationMs: 0,
|
|
937
|
-
metadata: {
|
|
947
|
+
metadata: {
|
|
948
|
+
formatter: selected?.name ?? null,
|
|
949
|
+
reason: selectionReason,
|
|
950
|
+
cwd,
|
|
951
|
+
},
|
|
938
952
|
});
|
|
939
953
|
|
|
940
954
|
// Store the list of enabled formatter names in cache
|
|
@@ -1344,7 +1344,7 @@ async function installGitHubTool(
|
|
|
1344
1344
|
const srcBinary = path.join(tmpDir, spec.binaryInArchive ?? binaryName);
|
|
1345
1345
|
await fs.rename(srcBinary, destPath);
|
|
1346
1346
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
1347
|
-
if (!isWindows) await fs.chmod(destPath,
|
|
1347
|
+
if (!isWindows) await fs.chmod(destPath, 0o750);
|
|
1348
1348
|
} else if (assetName.endsWith(".zip")) {
|
|
1349
1349
|
// Write zip to temp, extract with unzip (Linux/macOS) or Expand-Archive (Windows)
|
|
1350
1350
|
const tmpArchive = path.join(GITHUB_BIN_DIR, `_tmp_${assetName}`);
|
|
@@ -1395,7 +1395,7 @@ async function installGitHubTool(
|
|
|
1395
1395
|
}
|
|
1396
1396
|
await fs.rename(srcBinary, destPath);
|
|
1397
1397
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
1398
|
-
if (!isWindows) await fs.chmod(destPath,
|
|
1398
|
+
if (!isWindows) await fs.chmod(destPath, 0o750);
|
|
1399
1399
|
} else {
|
|
1400
1400
|
// Bare binary (e.g. shfmt_*_linux_amd64)
|
|
1401
1401
|
await fs.writeFile(destPath, assetBuffer, { mode: 0o755 });
|
|
@@ -1538,7 +1538,7 @@ async function installNpmTool(
|
|
|
1538
1538
|
// Make executable on Unix
|
|
1539
1539
|
if (process.platform !== "win32") {
|
|
1540
1540
|
try {
|
|
1541
|
-
await fs.chmod(binPath,
|
|
1541
|
+
await fs.chmod(binPath, 0o750);
|
|
1542
1542
|
} catch {
|
|
1543
1543
|
/* ignore */
|
|
1544
1544
|
}
|