aislop 0.8.3 → 0.9.1
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/README.md +1 -0
- package/dist/cli.js +1193 -304
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1007 -237
- package/dist/{json-D8h2EZW6.js → json-B_2_Zt7I.js} +1 -1
- package/dist/{json-BbMwrgyd.js → json-OIzja7OM.js} +1 -1
- package/dist/mcp.js +669 -155
- package/dist/{typecheck-B1MXNAy-.js → typecheck-wVSohmOX.js} +1 -1
- package/dist/{version-BynHxO1X.js → version-CBcgcofs.js} +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -4,14 +4,14 @@ import { Command } from "commander";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import crypto, { randomUUID } from "node:crypto";
|
|
8
|
+
import { performance } from "node:perf_hooks";
|
|
7
9
|
import YAML from "yaml";
|
|
8
10
|
import { z } from "zod/v4";
|
|
9
|
-
import { performance } from "node:perf_hooks";
|
|
10
11
|
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
11
12
|
import micromatch from "micromatch";
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
13
14
|
import ts from "typescript";
|
|
14
|
-
import crypto from "node:crypto";
|
|
15
15
|
import { isCancel, multiselect, select, text } from "@clack/prompts";
|
|
16
16
|
import pc from "picocolors";
|
|
17
17
|
import wcwidth from "wcwidth";
|
|
@@ -32,6 +32,366 @@ var __exportAll = (all, no_symbols) => {
|
|
|
32
32
|
return target;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/version.ts
|
|
37
|
+
const APP_VERSION = "0.9.1";
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/telemetry/env.ts
|
|
41
|
+
const detectPackageManager$1 = (env = process.env) => {
|
|
42
|
+
const execPath = env.npm_execpath ?? "";
|
|
43
|
+
if (execPath.includes("npx")) return "npx";
|
|
44
|
+
const userAgent = env.npm_config_user_agent ?? "";
|
|
45
|
+
if (userAgent.startsWith("pnpm/")) return "pnpm";
|
|
46
|
+
if (userAgent.startsWith("yarn/")) return "yarn";
|
|
47
|
+
if (userAgent.startsWith("bun/")) return "bun";
|
|
48
|
+
if (userAgent.startsWith("npm/")) return "npm";
|
|
49
|
+
if (execPath.includes("pnpm")) return "pnpm";
|
|
50
|
+
if (execPath.includes("yarn")) return "yarn";
|
|
51
|
+
if (execPath.includes("bun")) return "bun";
|
|
52
|
+
if (execPath.includes("npm")) return "npm";
|
|
53
|
+
return "unknown";
|
|
54
|
+
};
|
|
55
|
+
const CI_ENV_KEYS = [
|
|
56
|
+
"CI",
|
|
57
|
+
"GITHUB_ACTIONS",
|
|
58
|
+
"GITLAB_CI",
|
|
59
|
+
"CIRCLECI",
|
|
60
|
+
"TRAVIS",
|
|
61
|
+
"BUILDKITE",
|
|
62
|
+
"DRONE",
|
|
63
|
+
"TEAMCITY_VERSION",
|
|
64
|
+
"TF_BUILD"
|
|
65
|
+
];
|
|
66
|
+
const isCiEnv = (env = process.env) => CI_ENV_KEYS.some((k) => {
|
|
67
|
+
const v = env[k];
|
|
68
|
+
return v === "true" || v === "1" || v != null && v.length > 0 && k !== "CI";
|
|
69
|
+
}) || env.CI === "true" || env.CI === "1";
|
|
70
|
+
const fileCountBucket = (count) => {
|
|
71
|
+
if (count < 10) return "0-10";
|
|
72
|
+
if (count < 50) return "10-50";
|
|
73
|
+
if (count < 100) return "50-100";
|
|
74
|
+
if (count < 500) return "100-500";
|
|
75
|
+
if (count < 1e3) return "500-1000";
|
|
76
|
+
return "1000+";
|
|
77
|
+
};
|
|
78
|
+
const scoreBucket = (score) => {
|
|
79
|
+
if (score >= 75) return "75-100";
|
|
80
|
+
if (score >= 50) return "50-75";
|
|
81
|
+
if (score >= 25) return "25-50";
|
|
82
|
+
return "0-25";
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/telemetry/identity.ts
|
|
87
|
+
const FILE_BASENAME = "install_id";
|
|
88
|
+
const resolveInstallIdPath = (homedir = os.homedir(), env = process.env) => {
|
|
89
|
+
if (process.platform === "linux" && env.XDG_STATE_HOME) return path.join(env.XDG_STATE_HOME, "aislop", FILE_BASENAME);
|
|
90
|
+
return path.join(homedir, ".aislop", FILE_BASENAME);
|
|
91
|
+
};
|
|
92
|
+
const ensureInstallId = (idPath = resolveInstallIdPath()) => {
|
|
93
|
+
if (fs.existsSync(idPath)) {
|
|
94
|
+
const existing = fs.readFileSync(idPath, "utf-8").trim();
|
|
95
|
+
if (existing.length > 0) return {
|
|
96
|
+
installId: existing,
|
|
97
|
+
created: false
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const dir = path.dirname(idPath);
|
|
101
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
102
|
+
const installId = randomUUID();
|
|
103
|
+
const tmpPath = `${idPath}.${process.pid}.tmp`;
|
|
104
|
+
fs.writeFileSync(tmpPath, `${installId}\n`, { mode: 384 });
|
|
105
|
+
try {
|
|
106
|
+
fs.renameSync(tmpPath, idPath);
|
|
107
|
+
return {
|
|
108
|
+
installId,
|
|
109
|
+
created: true
|
|
110
|
+
};
|
|
111
|
+
} catch {
|
|
112
|
+
fs.rmSync(tmpPath, { force: true });
|
|
113
|
+
return {
|
|
114
|
+
installId: fs.readFileSync(idPath, "utf-8").trim(),
|
|
115
|
+
created: false
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/telemetry/redaction.ts
|
|
122
|
+
const SAFE_PROPERTY_NAMES = new Set([
|
|
123
|
+
"aislop_version",
|
|
124
|
+
"node_version",
|
|
125
|
+
"os",
|
|
126
|
+
"arch",
|
|
127
|
+
"schema_version",
|
|
128
|
+
"anonymous_install_id",
|
|
129
|
+
"package_manager",
|
|
130
|
+
"is_ci",
|
|
131
|
+
"command",
|
|
132
|
+
"language_summary",
|
|
133
|
+
"lang_typescript",
|
|
134
|
+
"lang_javascript",
|
|
135
|
+
"lang_python",
|
|
136
|
+
"lang_java",
|
|
137
|
+
"file_count_bucket",
|
|
138
|
+
"exit_code",
|
|
139
|
+
"duration_ms",
|
|
140
|
+
"error_kind",
|
|
141
|
+
"score",
|
|
142
|
+
"score_bucket",
|
|
143
|
+
"finding_count",
|
|
144
|
+
"error_count",
|
|
145
|
+
"warning_count",
|
|
146
|
+
"fixable_count",
|
|
147
|
+
"fix_steps",
|
|
148
|
+
"fix_resolved",
|
|
149
|
+
"fix_score_delta",
|
|
150
|
+
"engine_format_issues",
|
|
151
|
+
"engine_format_ms",
|
|
152
|
+
"engine_lint_issues",
|
|
153
|
+
"engine_lint_ms",
|
|
154
|
+
"engine_code_quality_issues",
|
|
155
|
+
"engine_code_quality_ms",
|
|
156
|
+
"engine_ai_slop_issues",
|
|
157
|
+
"engine_ai_slop_ms",
|
|
158
|
+
"engine_architecture_issues",
|
|
159
|
+
"engine_architecture_ms",
|
|
160
|
+
"engine_security_issues",
|
|
161
|
+
"engine_security_ms",
|
|
162
|
+
"tool",
|
|
163
|
+
"ok",
|
|
164
|
+
"agent",
|
|
165
|
+
"score_delta"
|
|
166
|
+
]);
|
|
167
|
+
const redactProperties = (props) => {
|
|
168
|
+
const clean = {};
|
|
169
|
+
const dropped = [];
|
|
170
|
+
for (const [key, value] of Object.entries(props)) {
|
|
171
|
+
if (value === void 0) continue;
|
|
172
|
+
if (SAFE_PROPERTY_NAMES.has(key)) clean[key] = value;
|
|
173
|
+
else dropped.push(key);
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
clean,
|
|
177
|
+
dropped
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/telemetry/client.ts
|
|
183
|
+
const POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
184
|
+
const POSTHOG_KEY = "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
|
|
185
|
+
const SCHEMA_VERSION = "v2";
|
|
186
|
+
const REQUEST_TIMEOUT_MS = 3e3;
|
|
187
|
+
const isTelemetryDisabled = (config) => {
|
|
188
|
+
const env = process.env;
|
|
189
|
+
if (env.AISLOP_NO_TELEMETRY === "1" || env.DO_NOT_TRACK === "1") return true;
|
|
190
|
+
if (config?.enabled === false) return true;
|
|
191
|
+
if (config?.enabled === true) return false;
|
|
192
|
+
if (env.CI === "true" || env.CI === "1") return true;
|
|
193
|
+
return false;
|
|
194
|
+
};
|
|
195
|
+
const isDebug = () => process.env.AISLOP_TELEMETRY_DEBUG === "1";
|
|
196
|
+
const pendingRequests = /* @__PURE__ */ new Set();
|
|
197
|
+
let cachedInstallId = null;
|
|
198
|
+
let installCreated = false;
|
|
199
|
+
const baseProperties = (installId) => ({
|
|
200
|
+
aislop_version: APP_VERSION,
|
|
201
|
+
node_version: process.version,
|
|
202
|
+
os: os.platform(),
|
|
203
|
+
arch: os.arch(),
|
|
204
|
+
schema_version: SCHEMA_VERSION,
|
|
205
|
+
anonymous_install_id: installId,
|
|
206
|
+
package_manager: detectPackageManager$1(),
|
|
207
|
+
is_ci: isCiEnv()
|
|
208
|
+
});
|
|
209
|
+
const track = (input) => {
|
|
210
|
+
if (isTelemetryDisabled(input.config)) return { installCreated: false };
|
|
211
|
+
if (cachedInstallId == null) {
|
|
212
|
+
const ensured = ensureInstallId(resolveInstallIdPath());
|
|
213
|
+
cachedInstallId = ensured.installId;
|
|
214
|
+
installCreated = ensured.created;
|
|
215
|
+
}
|
|
216
|
+
const { clean, dropped } = redactProperties({
|
|
217
|
+
...baseProperties(cachedInstallId),
|
|
218
|
+
...input.properties
|
|
219
|
+
});
|
|
220
|
+
if (isDebug()) {
|
|
221
|
+
const compact = JSON.stringify({
|
|
222
|
+
event: input.event,
|
|
223
|
+
properties: clean
|
|
224
|
+
});
|
|
225
|
+
process.stderr.write(`[telemetry] ${compact}\n`);
|
|
226
|
+
if (dropped.length > 0) for (const key of dropped) process.stderr.write(`[telemetry] dropped non-allowlisted property: ${key}\n`);
|
|
227
|
+
}
|
|
228
|
+
if (process.env.AISLOP_TELEMETRY_DRY_RUN === "1") return { installCreated };
|
|
229
|
+
const payload = {
|
|
230
|
+
api_key: POSTHOG_KEY,
|
|
231
|
+
event: input.event,
|
|
232
|
+
distinct_id: cachedInstallId,
|
|
233
|
+
properties: clean,
|
|
234
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
235
|
+
};
|
|
236
|
+
const request = fetch(`${POSTHOG_HOST}/capture/`, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: { "Content-Type": "application/json" },
|
|
239
|
+
body: JSON.stringify(payload),
|
|
240
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
241
|
+
}).then(() => {}).catch(() => {}).finally(() => {
|
|
242
|
+
pendingRequests.delete(request);
|
|
243
|
+
});
|
|
244
|
+
pendingRequests.add(request);
|
|
245
|
+
return { installCreated };
|
|
246
|
+
};
|
|
247
|
+
const flushTelemetry = async () => {
|
|
248
|
+
if (pendingRequests.size === 0) return;
|
|
249
|
+
await Promise.all(pendingRequests);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/telemetry/language.ts
|
|
254
|
+
const ALL_LANGUAGES = [
|
|
255
|
+
"typescript",
|
|
256
|
+
"javascript",
|
|
257
|
+
"python",
|
|
258
|
+
"java"
|
|
259
|
+
];
|
|
260
|
+
const buildLanguageProperties = (detected) => {
|
|
261
|
+
const present = new Set(detected);
|
|
262
|
+
const summary = [...present].filter((l) => ALL_LANGUAGES.includes(l));
|
|
263
|
+
summary.sort();
|
|
264
|
+
return {
|
|
265
|
+
language_summary: summary.join(","),
|
|
266
|
+
lang_typescript: present.has("typescript"),
|
|
267
|
+
lang_javascript: present.has("javascript"),
|
|
268
|
+
lang_python: present.has("python"),
|
|
269
|
+
lang_java: present.has("java")
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/telemetry/events.ts
|
|
275
|
+
const buildCommandStartedProps = (input) => {
|
|
276
|
+
const props = { command: input.command };
|
|
277
|
+
if (input.languages) Object.assign(props, buildLanguageProperties(input.languages));
|
|
278
|
+
if (typeof input.fileCount === "number") props.file_count_bucket = fileCountBucket(input.fileCount);
|
|
279
|
+
return props;
|
|
280
|
+
};
|
|
281
|
+
const ENGINE_KEY_MAP = {
|
|
282
|
+
format: "engine_format",
|
|
283
|
+
lint: "engine_lint",
|
|
284
|
+
"code-quality": "engine_code_quality",
|
|
285
|
+
"ai-slop": "engine_ai_slop",
|
|
286
|
+
architecture: "engine_architecture",
|
|
287
|
+
security: "engine_security"
|
|
288
|
+
};
|
|
289
|
+
const flattenEngineStats = (issues, timings) => {
|
|
290
|
+
const out = {};
|
|
291
|
+
for (const [engine, count] of Object.entries(issues)) {
|
|
292
|
+
const key = ENGINE_KEY_MAP[engine];
|
|
293
|
+
if (key != null && typeof count === "number") out[`${key}_issues`] = count;
|
|
294
|
+
}
|
|
295
|
+
for (const [engine, ms] of Object.entries(timings)) {
|
|
296
|
+
const key = ENGINE_KEY_MAP[engine];
|
|
297
|
+
if (key != null && typeof ms === "number") out[`${key}_ms`] = Math.round(ms);
|
|
298
|
+
}
|
|
299
|
+
return out;
|
|
300
|
+
};
|
|
301
|
+
const buildCommandCompletedProps = (input) => {
|
|
302
|
+
const props = {
|
|
303
|
+
...input.startProps,
|
|
304
|
+
exit_code: input.exitCode,
|
|
305
|
+
duration_ms: Math.round(input.durationMs)
|
|
306
|
+
};
|
|
307
|
+
if (input.errorKind) props.error_kind = input.errorKind;
|
|
308
|
+
if (typeof input.score === "number") {
|
|
309
|
+
props.score = input.score;
|
|
310
|
+
props.score_bucket = scoreBucket(input.score);
|
|
311
|
+
}
|
|
312
|
+
if (typeof input.findingCount === "number") props.finding_count = input.findingCount;
|
|
313
|
+
if (typeof input.errorCount === "number") props.error_count = input.errorCount;
|
|
314
|
+
if (typeof input.warningCount === "number") props.warning_count = input.warningCount;
|
|
315
|
+
if (typeof input.fixableCount === "number") props.fixable_count = input.fixableCount;
|
|
316
|
+
if (input.engineIssues && input.engineTimings) Object.assign(props, flattenEngineStats(input.engineIssues, input.engineTimings));
|
|
317
|
+
if (typeof input.fixSteps === "number") props.fix_steps = input.fixSteps;
|
|
318
|
+
if (typeof input.fixResolved === "number") props.fix_resolved = input.fixResolved;
|
|
319
|
+
if (typeof input.fixScoreDelta === "number") props.fix_score_delta = input.fixScoreDelta;
|
|
320
|
+
return props;
|
|
321
|
+
};
|
|
322
|
+
const buildHookScanCompletedProps = (input) => {
|
|
323
|
+
const props = {
|
|
324
|
+
agent: input.agent,
|
|
325
|
+
score: input.score,
|
|
326
|
+
score_bucket: scoreBucket(input.score),
|
|
327
|
+
finding_count: input.findingCount,
|
|
328
|
+
file_count_bucket: fileCountBucket(input.fileCount)
|
|
329
|
+
};
|
|
330
|
+
if (typeof input.scoreDelta === "number") props.score_delta = input.scoreDelta;
|
|
331
|
+
return props;
|
|
332
|
+
};
|
|
333
|
+
const errorKindFromException = (error) => {
|
|
334
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
335
|
+
if (message.includes("timeout") || message.includes("timed out")) return "timeout";
|
|
336
|
+
if (message.includes("invalid config") || message.includes("config_invalid")) return "config_invalid";
|
|
337
|
+
if (message.includes("engine") && message.includes("crash")) return "engine_crash";
|
|
338
|
+
return "unknown";
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region src/telemetry/lifecycle.ts
|
|
343
|
+
const withCommandLifecycle = async (start, run) => {
|
|
344
|
+
const startProps = buildCommandStartedProps({
|
|
345
|
+
command: start.command,
|
|
346
|
+
languages: start.languages,
|
|
347
|
+
fileCount: start.fileCount
|
|
348
|
+
});
|
|
349
|
+
track({
|
|
350
|
+
event: "cli_command_started",
|
|
351
|
+
properties: startProps,
|
|
352
|
+
config: start.config
|
|
353
|
+
});
|
|
354
|
+
const startedAt = performance.now();
|
|
355
|
+
try {
|
|
356
|
+
const result = await run();
|
|
357
|
+
const durationMs = performance.now() - startedAt;
|
|
358
|
+
track({
|
|
359
|
+
event: "cli_command_completed",
|
|
360
|
+
properties: buildCommandCompletedProps({
|
|
361
|
+
startProps,
|
|
362
|
+
exitCode: result.exitCode,
|
|
363
|
+
durationMs,
|
|
364
|
+
score: result.score,
|
|
365
|
+
findingCount: result.findingCount,
|
|
366
|
+
errorCount: result.errorCount,
|
|
367
|
+
warningCount: result.warningCount,
|
|
368
|
+
fixableCount: result.fixableCount,
|
|
369
|
+
engineIssues: result.engineIssues,
|
|
370
|
+
engineTimings: result.engineTimings,
|
|
371
|
+
fixSteps: result.fixSteps,
|
|
372
|
+
fixResolved: result.fixResolved,
|
|
373
|
+
fixScoreDelta: result.fixScoreDelta
|
|
374
|
+
}),
|
|
375
|
+
config: start.config
|
|
376
|
+
});
|
|
377
|
+
await flushTelemetry();
|
|
378
|
+
return result;
|
|
379
|
+
} catch (error) {
|
|
380
|
+
track({
|
|
381
|
+
event: "cli_command_completed",
|
|
382
|
+
properties: buildCommandCompletedProps({
|
|
383
|
+
startProps,
|
|
384
|
+
exitCode: 1,
|
|
385
|
+
durationMs: performance.now() - startedAt,
|
|
386
|
+
errorKind: errorKindFromException(error)
|
|
387
|
+
}),
|
|
388
|
+
config: start.config
|
|
389
|
+
});
|
|
390
|
+
await flushTelemetry();
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
35
395
|
//#endregion
|
|
36
396
|
//#region src/hooks/feedback.ts
|
|
37
397
|
const fingerprintFinding = (f) => `${f.file}:${f.line}:${f.ruleId}`;
|
|
@@ -104,7 +464,21 @@ const buildSuggestedActions = (diagnostics, findings, regressed, delta) => {
|
|
|
104
464
|
});
|
|
105
465
|
return actions;
|
|
106
466
|
};
|
|
107
|
-
const
|
|
467
|
+
const buildAccountability = (meta, findings, regressed, newSinceBaseline) => {
|
|
468
|
+
if (!meta?.agent && (!meta?.touchedFiles || meta.touchedFiles.length === 0)) return void 0;
|
|
469
|
+
const touchedFiles = Array.from(new Set(meta.touchedFiles ?? []));
|
|
470
|
+
const newFindingCount = newSinceBaseline?.length ?? findings.length;
|
|
471
|
+
const mustFixBeforeDone = regressed || findings.some((f) => f.severity === "error");
|
|
472
|
+
const reason = mustFixBeforeDone ? regressed ? "Score regressed against the captured baseline. The agent should fix or justify the new findings before finishing." : "Error-severity findings remain in files touched by this agent turn." : "No blocking regression detected for this agent turn.";
|
|
473
|
+
return {
|
|
474
|
+
agent: meta.agent,
|
|
475
|
+
touchedFiles,
|
|
476
|
+
newFindingCount,
|
|
477
|
+
mustFixBeforeDone,
|
|
478
|
+
reason
|
|
479
|
+
};
|
|
480
|
+
};
|
|
481
|
+
const buildFeedback = (diagnostics, score, rootDirectory, baseline, meta) => {
|
|
108
482
|
const all = diagnostics.map((d) => toFinding(d, rootDirectory)).filter((x) => x !== null);
|
|
109
483
|
const capped = all.slice(0, MAX_FINDINGS);
|
|
110
484
|
const elided = all.length > MAX_FINDINGS ? all.length - MAX_FINDINGS : void 0;
|
|
@@ -132,6 +506,7 @@ const buildFeedback = (diagnostics, score, rootDirectory, baseline) => {
|
|
|
132
506
|
baseline: baselineScore,
|
|
133
507
|
delta,
|
|
134
508
|
regressed,
|
|
509
|
+
accountability: buildAccountability(meta, capped, regressed, newSinceBaseline),
|
|
135
510
|
counts,
|
|
136
511
|
findings: capped,
|
|
137
512
|
elided,
|
|
@@ -188,6 +563,7 @@ const DEFAULT_CONFIG = {
|
|
|
188
563
|
"build",
|
|
189
564
|
"coverage"
|
|
190
565
|
],
|
|
566
|
+
include: [],
|
|
191
567
|
engines: {
|
|
192
568
|
format: true,
|
|
193
569
|
lint: true,
|
|
@@ -223,7 +599,7 @@ const DEFAULT_CONFIG = {
|
|
|
223
599
|
smoothing: 20
|
|
224
600
|
},
|
|
225
601
|
ci: {
|
|
226
|
-
failBelow:
|
|
602
|
+
failBelow: 70,
|
|
227
603
|
format: "json"
|
|
228
604
|
},
|
|
229
605
|
telemetry: { enabled: true }
|
|
@@ -349,7 +725,7 @@ const ScoringSchema = z.object({
|
|
|
349
725
|
smoothing: z.number().nonnegative().default(20)
|
|
350
726
|
});
|
|
351
727
|
const CiSchema = z.object({
|
|
352
|
-
failBelow: z.number().default(
|
|
728
|
+
failBelow: z.number().default(70),
|
|
353
729
|
format: z.enum(["json"]).default("json")
|
|
354
730
|
});
|
|
355
731
|
const TelemetrySchema = z.object({ enabled: z.boolean().default(true) });
|
|
@@ -383,7 +759,7 @@ const AislopConfigSchema = z.object({
|
|
|
383
759
|
smoothing: 20
|
|
384
760
|
})),
|
|
385
761
|
ci: CiSchema.default(() => ({
|
|
386
|
-
failBelow:
|
|
762
|
+
failBelow: 70,
|
|
387
763
|
format: "json"
|
|
388
764
|
})),
|
|
389
765
|
telemetry: TelemetrySchema.default(() => ({ enabled: true })),
|
|
@@ -393,7 +769,8 @@ const AislopConfigSchema = z.object({
|
|
|
393
769
|
"dist",
|
|
394
770
|
"build",
|
|
395
771
|
"coverage"
|
|
396
|
-
])
|
|
772
|
+
]),
|
|
773
|
+
include: z.array(z.string()).default(() => [])
|
|
397
774
|
});
|
|
398
775
|
const defaults = AislopConfigSchema.parse({});
|
|
399
776
|
/**
|
|
@@ -473,31 +850,68 @@ const EXCLUDED_DIRS = [
|
|
|
473
850
|
"dist",
|
|
474
851
|
"build",
|
|
475
852
|
".git",
|
|
853
|
+
".agents",
|
|
476
854
|
"vendor",
|
|
855
|
+
"examples",
|
|
856
|
+
"example",
|
|
857
|
+
"demos",
|
|
858
|
+
"demo",
|
|
859
|
+
"bench",
|
|
860
|
+
"benches",
|
|
861
|
+
"benchmarks",
|
|
862
|
+
"fixtures",
|
|
863
|
+
"fixture",
|
|
864
|
+
"samples",
|
|
865
|
+
"sample",
|
|
866
|
+
"tutorials",
|
|
867
|
+
"tutorial",
|
|
868
|
+
"code_samples",
|
|
869
|
+
"code-samples",
|
|
870
|
+
"notebooks",
|
|
477
871
|
"tests",
|
|
478
872
|
"test",
|
|
479
873
|
"__tests__",
|
|
480
874
|
"__test__",
|
|
481
875
|
"spec",
|
|
482
876
|
"__mocks__",
|
|
483
|
-
"fixtures",
|
|
484
877
|
"test_data",
|
|
485
878
|
".next",
|
|
486
879
|
".nuxt",
|
|
487
880
|
"coverage",
|
|
488
|
-
".turbo"
|
|
881
|
+
".turbo",
|
|
882
|
+
"public"
|
|
489
883
|
];
|
|
490
884
|
const FIND_PRUNE_DIRS = [
|
|
491
885
|
"node_modules",
|
|
492
886
|
"dist",
|
|
493
887
|
"build",
|
|
494
888
|
".git",
|
|
889
|
+
".agents",
|
|
495
890
|
"vendor",
|
|
891
|
+
"examples",
|
|
892
|
+
"example",
|
|
893
|
+
"demos",
|
|
894
|
+
"demo",
|
|
895
|
+
"bench",
|
|
896
|
+
"benches",
|
|
897
|
+
"benchmarks",
|
|
898
|
+
"fixtures",
|
|
899
|
+
"fixture",
|
|
900
|
+
"samples",
|
|
901
|
+
"sample",
|
|
902
|
+
"tutorials",
|
|
903
|
+
"tutorial",
|
|
904
|
+
"code_samples",
|
|
905
|
+
"code-samples",
|
|
906
|
+
"notebooks",
|
|
496
907
|
".next",
|
|
497
908
|
".nuxt",
|
|
498
909
|
"coverage",
|
|
499
|
-
".turbo"
|
|
910
|
+
".turbo",
|
|
911
|
+
"public"
|
|
500
912
|
];
|
|
913
|
+
const BUILD_CACHE_FILE_PATTERNS = [/\.timestamp-\d+-[a-z0-9]+\.[mc]?js$/i];
|
|
914
|
+
const isBuildCacheFile = (filePath) => BUILD_CACHE_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
501
915
|
const TEST_FILE_PATTERNS = [
|
|
502
916
|
/(?:^|\/).*\.test\.[^/]+$/i,
|
|
503
917
|
/(?:^|\/).*\.spec\.[^/]+$/i,
|
|
@@ -522,6 +936,7 @@ const hasAllowedExtension = (filePath, extraExtensions) => {
|
|
|
522
936
|
return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
|
|
523
937
|
};
|
|
524
938
|
const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
|
|
939
|
+
const isExcludedFromScan = (relativePath) => isExcludedPath(relativePath) || isBuildCacheFile(relativePath);
|
|
525
940
|
const isTestFile$2 = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
526
941
|
const getIgnoredPaths = (rootDirectory, files) => {
|
|
527
942
|
if (files.length === 0) return /* @__PURE__ */ new Set();
|
|
@@ -580,7 +995,7 @@ const normalizeExcludePatterns = (patterns) => {
|
|
|
580
995
|
return [p];
|
|
581
996
|
});
|
|
582
997
|
};
|
|
583
|
-
const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude = []) => {
|
|
998
|
+
const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude = [], include = []) => {
|
|
584
999
|
const extraSet = new Set(extraExtensions);
|
|
585
1000
|
const normalizedFiles = files.map((file) => {
|
|
586
1001
|
const absolutePath = path.isAbsolute(file) ? file : path.resolve(rootDirectory, file);
|
|
@@ -595,8 +1010,16 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
|
|
|
595
1010
|
if (!normalizedExcludePatterns.length) return false;
|
|
596
1011
|
return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
|
|
597
1012
|
};
|
|
1013
|
+
const hasIncludePatterns = include.length > 0;
|
|
1014
|
+
const isUserIncluded = (relativePath) => {
|
|
1015
|
+
if (!hasIncludePatterns) return true;
|
|
1016
|
+
return micromatch.isMatch(relativePath, include, { dot: true });
|
|
1017
|
+
};
|
|
598
1018
|
return normalizedFiles.filter(({ absolutePath, relativePath }) => {
|
|
599
|
-
|
|
1019
|
+
if (!fs.existsSync(absolutePath) || !isWithinProject(relativePath) || isExcludedPath(relativePath) || isTestFile$2(relativePath) || ignoredPaths.has(relativePath)) return false;
|
|
1020
|
+
if (!isUserIncluded(relativePath)) return false;
|
|
1021
|
+
if (isUserExcluded(relativePath)) return false;
|
|
1022
|
+
return hasAllowedExtension(relativePath, extraSet);
|
|
600
1023
|
}).map(({ absolutePath }) => absolutePath);
|
|
601
1024
|
};
|
|
602
1025
|
const filterExplicitFiles = (rootDirectory, files, extraExtensions = []) => {
|
|
@@ -1326,6 +1749,86 @@ const PYTHON_IMPORT_TO_PIP = {
|
|
|
1326
1749
|
redis: "redis"
|
|
1327
1750
|
};
|
|
1328
1751
|
|
|
1752
|
+
//#endregion
|
|
1753
|
+
//#region src/engines/ai-slop/python-manifest.ts
|
|
1754
|
+
const addPyDep = (pyDeps, name) => {
|
|
1755
|
+
const normalized = name.toLowerCase().replace(/_/g, "-");
|
|
1756
|
+
pyDeps.add(normalized);
|
|
1757
|
+
};
|
|
1758
|
+
const collectFromRequirementsTxt = (rootDir, pyDeps) => {
|
|
1759
|
+
const reqPath = path.join(rootDir, "requirements.txt");
|
|
1760
|
+
if (!fs.existsSync(reqPath)) return false;
|
|
1761
|
+
try {
|
|
1762
|
+
const content = fs.readFileSync(reqPath, "utf-8");
|
|
1763
|
+
for (const line of content.split("\n")) {
|
|
1764
|
+
const trimmed = line.trim();
|
|
1765
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
|
|
1766
|
+
const match = trimmed.match(/^([a-zA-Z0-9_\-.]+)/);
|
|
1767
|
+
if (match) addPyDep(pyDeps, match[1]);
|
|
1768
|
+
}
|
|
1769
|
+
return true;
|
|
1770
|
+
} catch {
|
|
1771
|
+
return false;
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
const collectFromPyproject = (rootDir, pyDeps) => {
|
|
1775
|
+
const pyprojPath = path.join(rootDir, "pyproject.toml");
|
|
1776
|
+
if (!fs.existsSync(pyprojPath)) return false;
|
|
1777
|
+
try {
|
|
1778
|
+
const content = fs.readFileSync(pyprojPath, "utf-8");
|
|
1779
|
+
const projectNameMatch = content.match(/\[project\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
|
|
1780
|
+
if (projectNameMatch) addPyDep(pyDeps, projectNameMatch[1]);
|
|
1781
|
+
const poetryNameMatch = content.match(/\[tool\.poetry\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
|
|
1782
|
+
if (poetryNameMatch) addPyDep(pyDeps, poetryNameMatch[1]);
|
|
1783
|
+
const pep621 = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
|
|
1784
|
+
if (pep621) for (const line of pep621[1].split("\n")) {
|
|
1785
|
+
const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
|
|
1786
|
+
if (m) addPyDep(pyDeps, m[1]);
|
|
1787
|
+
}
|
|
1788
|
+
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1789
|
+
let match = poetryRe.exec(content);
|
|
1790
|
+
while (match !== null) {
|
|
1791
|
+
for (const line of match[1].split("\n")) {
|
|
1792
|
+
const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
|
|
1793
|
+
if (m && m[1] !== "python") addPyDep(pyDeps, m[1]);
|
|
1794
|
+
}
|
|
1795
|
+
match = poetryRe.exec(content);
|
|
1796
|
+
}
|
|
1797
|
+
return true;
|
|
1798
|
+
} catch {
|
|
1799
|
+
return false;
|
|
1800
|
+
}
|
|
1801
|
+
};
|
|
1802
|
+
const collectFromPipfile = (rootDir, pyDeps) => {
|
|
1803
|
+
const pipfilePath = path.join(rootDir, "Pipfile");
|
|
1804
|
+
if (!fs.existsSync(pipfilePath)) return false;
|
|
1805
|
+
try {
|
|
1806
|
+
const content = fs.readFileSync(pipfilePath, "utf-8");
|
|
1807
|
+
const sectionRe = /\[(packages|dev-packages)\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1808
|
+
let match = sectionRe.exec(content);
|
|
1809
|
+
while (match !== null) {
|
|
1810
|
+
for (const line of match[2].split("\n")) {
|
|
1811
|
+
const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
|
|
1812
|
+
if (m) addPyDep(pyDeps, m[1]);
|
|
1813
|
+
}
|
|
1814
|
+
match = sectionRe.exec(content);
|
|
1815
|
+
}
|
|
1816
|
+
return true;
|
|
1817
|
+
} catch {
|
|
1818
|
+
return false;
|
|
1819
|
+
}
|
|
1820
|
+
};
|
|
1821
|
+
const collectPythonDeps = (rootDir) => {
|
|
1822
|
+
const pyDeps = /* @__PURE__ */ new Set();
|
|
1823
|
+
const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
|
|
1824
|
+
const hasPyproject = collectFromPyproject(rootDir, pyDeps);
|
|
1825
|
+
const hasPipfile = collectFromPipfile(rootDir, pyDeps);
|
|
1826
|
+
return {
|
|
1827
|
+
pyDeps,
|
|
1828
|
+
hasPyManifest: hasReq || hasPyproject || hasPipfile
|
|
1829
|
+
};
|
|
1830
|
+
};
|
|
1831
|
+
|
|
1329
1832
|
//#endregion
|
|
1330
1833
|
//#region src/engines/ai-slop/hallucinated-imports.ts
|
|
1331
1834
|
const JS_EXTENSIONS$2 = new Set([
|
|
@@ -1462,10 +1965,26 @@ const buildAliasMatcher = (key) => {
|
|
|
1462
1965
|
};
|
|
1463
1966
|
const collectAliasMatchersFromConfig = (configPath, matchers) => {
|
|
1464
1967
|
const opts = readJson(configPath)?.compilerOptions;
|
|
1465
|
-
if (!opts
|
|
1968
|
+
if (!opts) return;
|
|
1466
1969
|
const paths = opts.paths;
|
|
1467
|
-
if (
|
|
1468
|
-
|
|
1970
|
+
if (paths && typeof paths === "object") for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
|
|
1971
|
+
const baseUrl = opts.baseUrl;
|
|
1972
|
+
if (typeof baseUrl === "string") {
|
|
1973
|
+
const baseUrlDir = path.resolve(path.dirname(configPath), baseUrl);
|
|
1974
|
+
let entries;
|
|
1975
|
+
try {
|
|
1976
|
+
entries = fs.readdirSync(baseUrlDir);
|
|
1977
|
+
} catch {
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
const baseSpecifiers = /* @__PURE__ */ new Set();
|
|
1981
|
+
for (const entry of entries) {
|
|
1982
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
1983
|
+
const base = entry.replace(/\.(?:[jt]sx?|mjs|cjs|d\.ts)$/i, "");
|
|
1984
|
+
if (base.length > 0) baseSpecifiers.add(base);
|
|
1985
|
+
}
|
|
1986
|
+
for (const name of baseSpecifiers) matchers.push((spec) => spec === name || spec.startsWith(`${name}/`));
|
|
1987
|
+
}
|
|
1469
1988
|
};
|
|
1470
1989
|
const collectTsPathAliases = (rootDir) => {
|
|
1471
1990
|
const matchers = [];
|
|
@@ -1473,97 +1992,35 @@ const collectTsPathAliases = (rootDir) => {
|
|
|
1473
1992
|
for (const dir of dirs) for (const fname of TS_CONFIG_FILES) collectAliasMatchersFromConfig(path.join(dir, fname), matchers);
|
|
1474
1993
|
return matchers;
|
|
1475
1994
|
};
|
|
1476
|
-
const addPyDep = (pyDeps, name) => {
|
|
1477
|
-
const normalized = name.toLowerCase().replace(/_/g, "-");
|
|
1478
|
-
pyDeps.add(normalized);
|
|
1479
|
-
};
|
|
1480
|
-
const collectFromRequirementsTxt = (rootDir, pyDeps) => {
|
|
1481
|
-
const reqPath = path.join(rootDir, "requirements.txt");
|
|
1482
|
-
if (!fs.existsSync(reqPath)) return false;
|
|
1483
|
-
try {
|
|
1484
|
-
const content = fs.readFileSync(reqPath, "utf-8");
|
|
1485
|
-
for (const line of content.split("\n")) {
|
|
1486
|
-
const trimmed = line.trim();
|
|
1487
|
-
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
|
|
1488
|
-
const match = trimmed.match(/^([a-zA-Z0-9_\-.]+)/);
|
|
1489
|
-
if (match) addPyDep(pyDeps, match[1]);
|
|
1490
|
-
}
|
|
1491
|
-
return true;
|
|
1492
|
-
} catch {
|
|
1493
|
-
return false;
|
|
1494
|
-
}
|
|
1495
|
-
};
|
|
1496
|
-
const collectFromPyproject = (rootDir, pyDeps) => {
|
|
1497
|
-
const pyprojPath = path.join(rootDir, "pyproject.toml");
|
|
1498
|
-
if (!fs.existsSync(pyprojPath)) return false;
|
|
1499
|
-
try {
|
|
1500
|
-
const content = fs.readFileSync(pyprojPath, "utf-8");
|
|
1501
|
-
const projectNameMatch = content.match(/\[project\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
|
|
1502
|
-
if (projectNameMatch) addPyDep(pyDeps, projectNameMatch[1]);
|
|
1503
|
-
const poetryNameMatch = content.match(/\[tool\.poetry\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
|
|
1504
|
-
if (poetryNameMatch) addPyDep(pyDeps, poetryNameMatch[1]);
|
|
1505
|
-
const pep621 = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
|
|
1506
|
-
if (pep621) for (const line of pep621[1].split("\n")) {
|
|
1507
|
-
const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
|
|
1508
|
-
if (m) addPyDep(pyDeps, m[1]);
|
|
1509
|
-
}
|
|
1510
|
-
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1511
|
-
let match = poetryRe.exec(content);
|
|
1512
|
-
while (match !== null) {
|
|
1513
|
-
for (const line of match[1].split("\n")) {
|
|
1514
|
-
const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
|
|
1515
|
-
if (m && m[1] !== "python") addPyDep(pyDeps, m[1]);
|
|
1516
|
-
}
|
|
1517
|
-
match = poetryRe.exec(content);
|
|
1518
|
-
}
|
|
1519
|
-
return true;
|
|
1520
|
-
} catch {
|
|
1521
|
-
return false;
|
|
1522
|
-
}
|
|
1523
|
-
};
|
|
1524
|
-
const collectFromPipfile = (rootDir, pyDeps) => {
|
|
1525
|
-
const pipfilePath = path.join(rootDir, "Pipfile");
|
|
1526
|
-
if (!fs.existsSync(pipfilePath)) return false;
|
|
1527
|
-
try {
|
|
1528
|
-
const content = fs.readFileSync(pipfilePath, "utf-8");
|
|
1529
|
-
const sectionRe = /\[(packages|dev-packages)\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1530
|
-
let match = sectionRe.exec(content);
|
|
1531
|
-
while (match !== null) {
|
|
1532
|
-
for (const line of match[2].split("\n")) {
|
|
1533
|
-
const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
|
|
1534
|
-
if (m) addPyDep(pyDeps, m[1]);
|
|
1535
|
-
}
|
|
1536
|
-
match = sectionRe.exec(content);
|
|
1537
|
-
}
|
|
1538
|
-
return true;
|
|
1539
|
-
} catch {
|
|
1540
|
-
return false;
|
|
1541
|
-
}
|
|
1542
|
-
};
|
|
1543
1995
|
const loadManifest = (rootDir) => {
|
|
1544
1996
|
const jsDeps = /* @__PURE__ */ new Set();
|
|
1545
|
-
const pyDeps = /* @__PURE__ */ new Set();
|
|
1546
1997
|
const hasJsManifest = collectJsDeps(rootDir, jsDeps);
|
|
1547
|
-
const
|
|
1548
|
-
const hasPyproject = collectFromPyproject(rootDir, pyDeps);
|
|
1549
|
-
const hasPipfile = collectFromPipfile(rootDir, pyDeps);
|
|
1998
|
+
const { pyDeps, hasPyManifest } = collectPythonDeps(rootDir);
|
|
1550
1999
|
return {
|
|
1551
2000
|
jsDeps,
|
|
1552
2001
|
pyDeps,
|
|
1553
2002
|
hasJsManifest,
|
|
1554
|
-
hasPyManifest
|
|
2003
|
+
hasPyManifest
|
|
1555
2004
|
};
|
|
1556
2005
|
};
|
|
1557
2006
|
const isJsRelativeOrAbsolute = (spec) => spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~/");
|
|
2007
|
+
const RUNTIME_BUILTINS = new Set(["bun"]);
|
|
1558
2008
|
const isJsBuiltin = (spec) => {
|
|
2009
|
+
if (RUNTIME_BUILTINS.has(spec)) return true;
|
|
1559
2010
|
return isBuiltin(spec.startsWith("node:") ? spec.slice(5) : spec) || isBuiltin(spec);
|
|
1560
2011
|
};
|
|
1561
2012
|
const VIRTUAL_MODULE_PREFIXES = [
|
|
1562
2013
|
"astro:",
|
|
1563
2014
|
"virtual:",
|
|
1564
|
-
"bun:"
|
|
2015
|
+
"bun:",
|
|
2016
|
+
"~icons/"
|
|
1565
2017
|
];
|
|
1566
2018
|
const isJsVirtualModule = (spec) => VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p));
|
|
2019
|
+
const stripImportQuery = (spec) => {
|
|
2020
|
+
const idx = spec.indexOf("?");
|
|
2021
|
+
return idx === -1 ? spec : spec.slice(0, idx);
|
|
2022
|
+
};
|
|
2023
|
+
const VIRTUAL_ASSET_FILES = { "unfonts.css": "unplugin-fonts" };
|
|
1567
2024
|
const TEMPLATE_PLACEHOLDER_RE = /\$\{/;
|
|
1568
2025
|
const isLikelyRealImportSpec = (spec) => {
|
|
1569
2026
|
if (spec.length === 0) return false;
|
|
@@ -1630,10 +2087,14 @@ const extractPyImports = (content) => {
|
|
|
1630
2087
|
}
|
|
1631
2088
|
return results;
|
|
1632
2089
|
};
|
|
1633
|
-
const checkJsImport = (
|
|
2090
|
+
const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
|
|
2091
|
+
const spec = stripImportQuery(rawSpec);
|
|
2092
|
+
if (spec.length === 0) return null;
|
|
1634
2093
|
if (isJsRelativeOrAbsolute(spec)) return null;
|
|
1635
2094
|
if (isJsBuiltin(spec)) return null;
|
|
1636
2095
|
if (isJsVirtualModule(spec)) return null;
|
|
2096
|
+
const virtualOwner = VIRTUAL_ASSET_FILES[spec];
|
|
2097
|
+
if (virtualOwner && manifest.jsDeps.has(virtualOwner)) return null;
|
|
1637
2098
|
if (tsAliasMatchers.some((m) => m(spec))) return null;
|
|
1638
2099
|
const pkg = packageNameFromImport(spec);
|
|
1639
2100
|
if (manifest.jsDeps.has(pkg)) return null;
|
|
@@ -2989,64 +3450,88 @@ const analyzeFunctions = (content, ext) => {
|
|
|
2989
3450
|
}
|
|
2990
3451
|
return functions;
|
|
2991
3452
|
};
|
|
2992
|
-
const
|
|
3453
|
+
const FILE_LOC_MULTIPLIERS = {
|
|
3454
|
+
".tsx": 1.5,
|
|
3455
|
+
".jsx": 1.5,
|
|
3456
|
+
".rs": 2.5,
|
|
3457
|
+
".go": 1.5
|
|
3458
|
+
};
|
|
3459
|
+
const DECLARATION_FILE_RE = /\.d\.ts$/i;
|
|
3460
|
+
const fileLocBudget = (ext, relativePath, base) => {
|
|
3461
|
+
if (DECLARATION_FILE_RE.test(relativePath)) return Number.POSITIVE_INFINITY;
|
|
3462
|
+
const multiplier = FILE_LOC_MULTIPLIERS[ext] ?? 1;
|
|
3463
|
+
return Math.ceil(base * multiplier);
|
|
3464
|
+
};
|
|
2993
3465
|
const checkFileDiagnostics = (relativePath, content, limits) => {
|
|
2994
3466
|
const results = [];
|
|
2995
3467
|
const lineCount = content.split("\n").length;
|
|
2996
3468
|
const ext = path.extname(relativePath).toLowerCase();
|
|
2997
3469
|
if (isDataFile(content)) return results;
|
|
2998
|
-
const configuredMax = ext
|
|
3470
|
+
const configuredMax = fileLocBudget(ext, relativePath, limits.maxFileLoc);
|
|
3471
|
+
if (!Number.isFinite(configuredMax)) return results;
|
|
2999
3472
|
if (lineCount > Math.ceil(configuredMax * 1.1)) results.push({
|
|
3000
3473
|
filePath: relativePath,
|
|
3001
3474
|
engine: "code-quality",
|
|
3002
3475
|
rule: "complexity/file-too-large",
|
|
3003
3476
|
severity: "warning",
|
|
3004
|
-
message: `File
|
|
3477
|
+
message: `File too large (max: ${configuredMax})`,
|
|
3005
3478
|
help: "Consider splitting this file into smaller modules",
|
|
3006
3479
|
line: 0,
|
|
3007
3480
|
column: 0,
|
|
3008
3481
|
category: "Complexity",
|
|
3009
|
-
fixable: false
|
|
3482
|
+
fixable: false,
|
|
3483
|
+
detail: `${lineCount} lines`
|
|
3010
3484
|
});
|
|
3011
3485
|
return results;
|
|
3012
3486
|
};
|
|
3013
|
-
const
|
|
3487
|
+
const JSX_EXTENSIONS = new Set([".tsx", ".jsx"]);
|
|
3488
|
+
const isComponentFunction = (name, ext) => JSX_EXTENSIONS.has(ext) && /^[A-Z]/.test(name);
|
|
3489
|
+
const functionLocBudget = (fn, ext, base) => {
|
|
3490
|
+
if (isComponentFunction(fn.name, ext)) return Math.ceil(base * 2);
|
|
3491
|
+
if (ext === ".rs") return Math.ceil(base * 1.5);
|
|
3492
|
+
return base;
|
|
3493
|
+
};
|
|
3494
|
+
const checkFunctionDiagnostics = (relativePath, fn, limits, ext) => {
|
|
3014
3495
|
const results = [];
|
|
3015
|
-
|
|
3496
|
+
const fnMax = functionLocBudget(fn, ext, limits.maxFunctionLoc);
|
|
3497
|
+
if (fn.lineCount - fn.templateLines > Math.ceil(fnMax * 1.1)) results.push({
|
|
3016
3498
|
filePath: relativePath,
|
|
3017
3499
|
engine: "code-quality",
|
|
3018
3500
|
rule: "complexity/function-too-long",
|
|
3019
3501
|
severity: "warning",
|
|
3020
|
-
message: `Function
|
|
3502
|
+
message: `Function too long (max: ${fnMax})`,
|
|
3021
3503
|
help: "Consider breaking this function into smaller pieces",
|
|
3022
3504
|
line: fn.startLine,
|
|
3023
3505
|
column: 0,
|
|
3024
3506
|
category: "Complexity",
|
|
3025
|
-
fixable: false
|
|
3507
|
+
fixable: false,
|
|
3508
|
+
detail: `${fn.name} · ${fn.lineCount} lines`
|
|
3026
3509
|
});
|
|
3027
3510
|
if (fn.maxNesting > limits.maxNesting) results.push({
|
|
3028
3511
|
filePath: relativePath,
|
|
3029
3512
|
engine: "code-quality",
|
|
3030
3513
|
rule: "complexity/deep-nesting",
|
|
3031
3514
|
severity: "warning",
|
|
3032
|
-
message: `Function
|
|
3515
|
+
message: `Function nested too deeply (max: ${limits.maxNesting})`,
|
|
3033
3516
|
help: "Consider using early returns or extracting nested logic",
|
|
3034
3517
|
line: fn.startLine,
|
|
3035
3518
|
column: 0,
|
|
3036
3519
|
category: "Complexity",
|
|
3037
|
-
fixable: false
|
|
3520
|
+
fixable: false,
|
|
3521
|
+
detail: `${fn.name} · depth ${fn.maxNesting}`
|
|
3038
3522
|
});
|
|
3039
3523
|
if (fn.paramCount > limits.maxParams) results.push({
|
|
3040
3524
|
filePath: relativePath,
|
|
3041
3525
|
engine: "code-quality",
|
|
3042
3526
|
rule: "complexity/too-many-params",
|
|
3043
3527
|
severity: "warning",
|
|
3044
|
-
message: `Function
|
|
3528
|
+
message: `Function has too many parameters (max: ${limits.maxParams})`,
|
|
3045
3529
|
help: "Consider using an options object parameter",
|
|
3046
3530
|
line: fn.startLine,
|
|
3047
3531
|
column: 0,
|
|
3048
3532
|
category: "Complexity",
|
|
3049
|
-
fixable: false
|
|
3533
|
+
fixable: false,
|
|
3534
|
+
detail: `${fn.name} · ${fn.paramCount} params`
|
|
3050
3535
|
});
|
|
3051
3536
|
return results;
|
|
3052
3537
|
};
|
|
@@ -3061,7 +3546,7 @@ const checkFileComplexity = (filePath, rootDirectory, limits) => {
|
|
|
3061
3546
|
}
|
|
3062
3547
|
const ext = path.extname(filePath).toLowerCase();
|
|
3063
3548
|
const diagnostics = checkFileDiagnostics(relativePath, content, limits);
|
|
3064
|
-
for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits));
|
|
3549
|
+
for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits, ext));
|
|
3065
3550
|
return diagnostics;
|
|
3066
3551
|
};
|
|
3067
3552
|
const checkComplexity = async (context) => {
|
|
@@ -3166,17 +3651,19 @@ const findDuplicateBlocks = (content, relativePath) => {
|
|
|
3166
3651
|
});
|
|
3167
3652
|
}
|
|
3168
3653
|
return reports.map((r) => {
|
|
3654
|
+
const span = r.currentEnd - r.currentStart + 1;
|
|
3169
3655
|
return {
|
|
3170
3656
|
filePath: relativePath,
|
|
3171
3657
|
engine: "code-quality",
|
|
3172
3658
|
rule: "code-quality/duplicate-block",
|
|
3173
3659
|
severity: "warning",
|
|
3174
|
-
message:
|
|
3660
|
+
message: "Duplicate code block — extract a shared helper",
|
|
3175
3661
|
help: `Pull the shared logic into a function both sites can call. Keeps one version of the truth and makes future changes one-shot instead of N-shot.`,
|
|
3176
3662
|
line: r.currentStart,
|
|
3177
3663
|
column: 0,
|
|
3178
3664
|
category: "Complexity",
|
|
3179
|
-
fixable: false
|
|
3665
|
+
fixable: false,
|
|
3666
|
+
detail: `${span} lines duplicate block at L${r.priorStart}`
|
|
3180
3667
|
};
|
|
3181
3668
|
});
|
|
3182
3669
|
};
|
|
@@ -3842,16 +4329,34 @@ const isToolAvailable = async (toolName) => {
|
|
|
3842
4329
|
return isToolInstalled(toolName);
|
|
3843
4330
|
};
|
|
3844
4331
|
|
|
4332
|
+
//#endregion
|
|
4333
|
+
//#region src/engines/python-targets.ts
|
|
4334
|
+
const PYTHON_EXTENSIONS = new Set([".py", ".pyi"]);
|
|
4335
|
+
const normalizeProjectPath = (filePath) => filePath.split(path.sep).join("/");
|
|
4336
|
+
const getPythonTargets = (context) => {
|
|
4337
|
+
const targets = (context.files ?? getSourceFiles(context)).filter((filePath) => PYTHON_EXTENSIONS.has(path.extname(filePath).toLowerCase())).map((filePath) => {
|
|
4338
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(context.rootDirectory, filePath);
|
|
4339
|
+
return normalizeProjectPath(path.relative(context.rootDirectory, absolutePath));
|
|
4340
|
+
}).filter((filePath) => filePath.length > 0 && !filePath.startsWith(".."));
|
|
4341
|
+
return [...new Set(targets)];
|
|
4342
|
+
};
|
|
4343
|
+
const getRuffDiagnosticPath = (rootDirectory, filePath) => {
|
|
4344
|
+
const normalizedPath = filePath.replace(/^a\//, "");
|
|
4345
|
+
return normalizeProjectPath(path.isAbsolute(normalizedPath) ? path.relative(rootDirectory, normalizedPath) : normalizedPath);
|
|
4346
|
+
};
|
|
4347
|
+
|
|
3845
4348
|
//#endregion
|
|
3846
4349
|
//#region src/engines/format/ruff-format.ts
|
|
3847
4350
|
const runRuffFormat = async (context) => {
|
|
3848
4351
|
const ruffBinary = resolveToolBinary("ruff");
|
|
4352
|
+
const targets = getPythonTargets(context);
|
|
4353
|
+
if (targets.length === 0) return [];
|
|
3849
4354
|
try {
|
|
3850
4355
|
const result = await runSubprocess(ruffBinary, [
|
|
3851
4356
|
"format",
|
|
3852
4357
|
"--check",
|
|
3853
4358
|
"--diff",
|
|
3854
|
-
|
|
4359
|
+
...targets
|
|
3855
4360
|
], {
|
|
3856
4361
|
cwd: context.rootDirectory,
|
|
3857
4362
|
timeout: 6e4
|
|
@@ -3867,9 +4372,9 @@ const parseRuffFormatOutput = (output, rootDir) => {
|
|
|
3867
4372
|
const filePattern = /^--- (.+)$/gm;
|
|
3868
4373
|
let match;
|
|
3869
4374
|
while ((match = filePattern.exec(output)) !== null) {
|
|
3870
|
-
const filePath = match[1]
|
|
4375
|
+
const filePath = getRuffDiagnosticPath(rootDir, match[1]);
|
|
3871
4376
|
diagnostics.push({
|
|
3872
|
-
filePath
|
|
4377
|
+
filePath,
|
|
3873
4378
|
engine: "format",
|
|
3874
4379
|
rule: "python-formatting",
|
|
3875
4380
|
severity: "warning",
|
|
@@ -4378,6 +4883,95 @@ const resolveOxlintBinary = () => {
|
|
|
4378
4883
|
return "oxlint";
|
|
4379
4884
|
}
|
|
4380
4885
|
};
|
|
4886
|
+
const VITE_QUERY_RE = /["'][^"']*\?(worker|sharedworker|worker-url|url|raw|inline|init)\b/;
|
|
4887
|
+
const isViteVirtualImportFalsePositive = (rule, message) => rule.startsWith("import/") && VITE_QUERY_RE.test(message);
|
|
4888
|
+
const AMBIENT_GLOBAL_DEPS = [
|
|
4889
|
+
"unplugin-icons",
|
|
4890
|
+
"@types/bun",
|
|
4891
|
+
"bun-types"
|
|
4892
|
+
];
|
|
4893
|
+
const SST_PLATFORM_REF_RE = /\/\/\/\s*<reference\s+path=["'][^"']*sst[\\/]+platform[\\/]+config\.d\.ts["']/;
|
|
4894
|
+
const ICON_AUTOIMPORT_RE = /^Icon[A-Z]/;
|
|
4895
|
+
const NO_UNDEF_IDENT_RE = /^['‘"`]([^'’"`]+)['’"`]/;
|
|
4896
|
+
const detectAmbientSources = (rootDir) => {
|
|
4897
|
+
const found = /* @__PURE__ */ new Set();
|
|
4898
|
+
const skipDirs = new Set([
|
|
4899
|
+
"node_modules",
|
|
4900
|
+
".git",
|
|
4901
|
+
"dist",
|
|
4902
|
+
"build",
|
|
4903
|
+
"out",
|
|
4904
|
+
"target",
|
|
4905
|
+
"coverage",
|
|
4906
|
+
".next",
|
|
4907
|
+
".turbo"
|
|
4908
|
+
]);
|
|
4909
|
+
const walk = (dir, depth) => {
|
|
4910
|
+
if (depth > 4 || found.size === AMBIENT_GLOBAL_DEPS.length) return;
|
|
4911
|
+
let entries;
|
|
4912
|
+
try {
|
|
4913
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
4914
|
+
} catch {
|
|
4915
|
+
return;
|
|
4916
|
+
}
|
|
4917
|
+
for (const entry of entries) {
|
|
4918
|
+
if (found.size === AMBIENT_GLOBAL_DEPS.length) return;
|
|
4919
|
+
if (entry.name.startsWith(".") && entry.name !== ".github") continue;
|
|
4920
|
+
if (skipDirs.has(entry.name)) continue;
|
|
4921
|
+
const full = path.join(dir, entry.name);
|
|
4922
|
+
if (entry.isDirectory()) walk(full, depth + 1);
|
|
4923
|
+
else if (entry.name === "package.json") try {
|
|
4924
|
+
const pkg = JSON.parse(fs.readFileSync(full, "utf-8"));
|
|
4925
|
+
const allDeps = {
|
|
4926
|
+
...pkg.dependencies ?? {},
|
|
4927
|
+
...pkg.devDependencies ?? {},
|
|
4928
|
+
...pkg.peerDependencies ?? {}
|
|
4929
|
+
};
|
|
4930
|
+
for (const dep of AMBIENT_GLOBAL_DEPS) if (dep in allDeps) found.add(dep);
|
|
4931
|
+
} catch {}
|
|
4932
|
+
}
|
|
4933
|
+
};
|
|
4934
|
+
walk(rootDir, 0);
|
|
4935
|
+
return found;
|
|
4936
|
+
};
|
|
4937
|
+
const extractNoUndefIdentifier = (message) => {
|
|
4938
|
+
return NO_UNDEF_IDENT_RE.exec(message)?.[1] ?? null;
|
|
4939
|
+
};
|
|
4940
|
+
const isAmbientFalsePositive = (rule, message, sources) => {
|
|
4941
|
+
if (rule !== "eslint/no-undef") return false;
|
|
4942
|
+
const ident = extractNoUndefIdentifier(message);
|
|
4943
|
+
if (!ident) return false;
|
|
4944
|
+
if (sources.has("unplugin-icons") && ICON_AUTOIMPORT_RE.test(ident)) return true;
|
|
4945
|
+
if ((sources.has("@types/bun") || sources.has("bun-types")) && ident === "Bun") return true;
|
|
4946
|
+
return false;
|
|
4947
|
+
};
|
|
4948
|
+
const sstReferencedFiles = /* @__PURE__ */ new Map();
|
|
4949
|
+
const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
|
|
4950
|
+
const cached = sstReferencedFiles.get(relativeFilePath);
|
|
4951
|
+
if (cached !== void 0) return cached;
|
|
4952
|
+
const absolute = path.isAbsolute(relativeFilePath) ? relativeFilePath : path.join(rootDir, relativeFilePath);
|
|
4953
|
+
let referenced = false;
|
|
4954
|
+
try {
|
|
4955
|
+
const fd = fs.openSync(absolute, "r");
|
|
4956
|
+
try {
|
|
4957
|
+
const buf = Buffer.alloc(512);
|
|
4958
|
+
const bytesRead = fs.readSync(fd, buf, 0, 512, 0);
|
|
4959
|
+
referenced = SST_PLATFORM_REF_RE.test(buf.toString("utf-8", 0, bytesRead));
|
|
4960
|
+
} finally {
|
|
4961
|
+
fs.closeSync(fd);
|
|
4962
|
+
}
|
|
4963
|
+
} catch {
|
|
4964
|
+
referenced = false;
|
|
4965
|
+
}
|
|
4966
|
+
sstReferencedFiles.set(relativeFilePath, referenced);
|
|
4967
|
+
return referenced;
|
|
4968
|
+
};
|
|
4969
|
+
const UNUSED_VAR_IDENT_RE = /(?:Variable|Parameter|Catch parameter) '([^']+)' (?:is declared but never used|is caught but never used)/;
|
|
4970
|
+
const isUnderscoreUnusedVar = (rule, message) => {
|
|
4971
|
+
if (rule !== "eslint/no-unused-vars") return false;
|
|
4972
|
+
const match = UNUSED_VAR_IDENT_RE.exec(message);
|
|
4973
|
+
return match ? match[1].startsWith("_") : false;
|
|
4974
|
+
};
|
|
4381
4975
|
const parseRuleCode = (code) => {
|
|
4382
4976
|
if (!code) return {
|
|
4383
4977
|
plugin: "eslint",
|
|
@@ -4474,6 +5068,8 @@ const runOxlint = async (context) => {
|
|
|
4474
5068
|
framework: context.frameworks.find((f) => f !== "none"),
|
|
4475
5069
|
testFramework: detectTestFramework(context.rootDirectory)
|
|
4476
5070
|
});
|
|
5071
|
+
const ambientSources = detectAmbientSources(context.rootDirectory);
|
|
5072
|
+
sstReferencedFiles.clear();
|
|
4477
5073
|
try {
|
|
4478
5074
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
4479
5075
|
const args = [
|
|
@@ -4513,6 +5109,11 @@ const runOxlint = async (context) => {
|
|
|
4513
5109
|
fixable: false
|
|
4514
5110
|
};
|
|
4515
5111
|
}).filter((d) => {
|
|
5112
|
+
if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
|
|
5113
|
+
if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
|
|
5114
|
+
if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
|
|
5115
|
+
if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
|
|
5116
|
+
if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
|
|
4516
5117
|
const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
|
|
4517
5118
|
if (seen.has(key)) return false;
|
|
4518
5119
|
seen.add(key);
|
|
@@ -4576,18 +5177,20 @@ const fixOxlint = async (context, options = {}) => {
|
|
|
4576
5177
|
//#region src/engines/lint/ruff.ts
|
|
4577
5178
|
const runRuffLint = async (context) => {
|
|
4578
5179
|
const ruffBinary = resolveToolBinary("ruff");
|
|
5180
|
+
const targets = getPythonTargets(context);
|
|
5181
|
+
if (targets.length === 0) return [];
|
|
4579
5182
|
try {
|
|
4580
5183
|
const output = (await runSubprocess(ruffBinary, [
|
|
4581
5184
|
"check",
|
|
4582
5185
|
"--output-format=json",
|
|
4583
|
-
|
|
5186
|
+
...targets
|
|
4584
5187
|
], {
|
|
4585
5188
|
cwd: context.rootDirectory,
|
|
4586
5189
|
timeout: 6e4
|
|
4587
5190
|
})).stdout;
|
|
4588
5191
|
if (!output) return [];
|
|
4589
5192
|
return JSON.parse(output).map((d) => ({
|
|
4590
|
-
filePath:
|
|
5193
|
+
filePath: getRuffDiagnosticPath(context.rootDirectory, d.filename),
|
|
4591
5194
|
engine: "lint",
|
|
4592
5195
|
rule: `ruff/${d.code}`,
|
|
4593
5196
|
severity: d.code.startsWith("E") || d.code.startsWith("F") ? "error" : "warning",
|
|
@@ -4636,7 +5239,7 @@ const lintEngine = {
|
|
|
4636
5239
|
const promises = [];
|
|
4637
5240
|
if (languages.includes("typescript") || languages.includes("javascript")) {
|
|
4638
5241
|
promises.push(runOxlint(context));
|
|
4639
|
-
if (context.config.lint.typecheck) promises.push(import("./typecheck-
|
|
5242
|
+
if (context.config.lint.typecheck) promises.push(import("./typecheck-wVSohmOX.js").then((mod) => mod.runTypecheck(context)));
|
|
4640
5243
|
}
|
|
4641
5244
|
if (context.frameworks.includes("expo")) promises.push(Promise.resolve().then(() => expo_doctor_exports).then((mod) => mod.runExpoDoctor(context)));
|
|
4642
5245
|
if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
|
|
@@ -4705,56 +5308,94 @@ const runPnpmAuditWithFallback = async (rootDir, timeout) => {
|
|
|
4705
5308
|
return [];
|
|
4706
5309
|
}
|
|
4707
5310
|
};
|
|
5311
|
+
const SEVERITY_RANK = {
|
|
5312
|
+
critical: 4,
|
|
5313
|
+
high: 3,
|
|
5314
|
+
moderate: 2,
|
|
5315
|
+
low: 1
|
|
5316
|
+
};
|
|
4708
5317
|
const toSeverity = (value) => value === "critical" || value === "high" ? "error" : "warning";
|
|
4709
|
-
const
|
|
5318
|
+
const upsertVuln = (bucket, packageName, severity, recommendation) => {
|
|
5319
|
+
const existing = bucket.get(packageName);
|
|
5320
|
+
if (existing) {
|
|
5321
|
+
existing.advisories++;
|
|
5322
|
+
if ((SEVERITY_RANK[severity] ?? 0) > (SEVERITY_RANK[existing.worstSeverity] ?? 0)) existing.worstSeverity = severity;
|
|
5323
|
+
if (recommendation) existing.recommendations.add(recommendation);
|
|
5324
|
+
} else bucket.set(packageName, {
|
|
5325
|
+
packageName,
|
|
5326
|
+
worstSeverity: severity,
|
|
5327
|
+
advisories: 1,
|
|
5328
|
+
recommendations: recommendation ? new Set([recommendation]) : /* @__PURE__ */ new Set()
|
|
5329
|
+
});
|
|
5330
|
+
};
|
|
5331
|
+
const SEMVER_RE = /(\d+)\.(\d+)\.(\d+)/;
|
|
5332
|
+
const cmpSemver = (a, b) => {
|
|
5333
|
+
const [, a1, a2, a3] = SEMVER_RE.exec(a) ?? [
|
|
5334
|
+
"",
|
|
5335
|
+
"0",
|
|
5336
|
+
"0",
|
|
5337
|
+
"0"
|
|
5338
|
+
];
|
|
5339
|
+
const [, b1, b2, b3] = SEMVER_RE.exec(b) ?? [
|
|
5340
|
+
"",
|
|
5341
|
+
"0",
|
|
5342
|
+
"0",
|
|
5343
|
+
"0"
|
|
5344
|
+
];
|
|
5345
|
+
if (Number(a1) !== Number(b1)) return Number(a1) - Number(b1);
|
|
5346
|
+
if (Number(a2) !== Number(b2)) return Number(a2) - Number(b2);
|
|
5347
|
+
return Number(a3) - Number(b3);
|
|
5348
|
+
};
|
|
5349
|
+
const pickBestRecommendation = (recs) => {
|
|
5350
|
+
if (recs.length <= 1) return recs[0] ?? "";
|
|
5351
|
+
const versioned = recs.filter((r) => SEMVER_RE.test(r));
|
|
5352
|
+
if (versioned.length === 0) return recs[0];
|
|
5353
|
+
return versioned.reduce((best, r) => cmpSemver(r, best) > 0 ? r : best);
|
|
5354
|
+
};
|
|
5355
|
+
const cleanRecommendation = (raw) => {
|
|
5356
|
+
const t = raw.trim();
|
|
5357
|
+
if (!t || t.toLowerCase() === "none") return "no fix available";
|
|
5358
|
+
return t;
|
|
5359
|
+
};
|
|
5360
|
+
const aggregateToDiagnostic = (agg, source) => {
|
|
5361
|
+
const best = cleanRecommendation(pickBestRecommendation([...agg.recommendations]));
|
|
5362
|
+
const countLabel = agg.advisories > 1 ? ` (${agg.advisories} advisories)` : "";
|
|
5363
|
+
const recLabel = best ? ` — ${best}` : "";
|
|
5364
|
+
return {
|
|
5365
|
+
filePath: "package.json",
|
|
5366
|
+
engine: "security",
|
|
5367
|
+
rule: "security/vulnerable-dependency",
|
|
5368
|
+
severity: toSeverity(agg.worstSeverity),
|
|
5369
|
+
message: `${agg.packageName} (${agg.worstSeverity})${recLabel}${countLabel}`,
|
|
5370
|
+
help: "",
|
|
5371
|
+
line: 0,
|
|
5372
|
+
column: 0,
|
|
5373
|
+
category: "Security",
|
|
5374
|
+
fixable: false,
|
|
5375
|
+
detail: source === "npm audit" ? "npm" : "pnpm"
|
|
5376
|
+
};
|
|
5377
|
+
};
|
|
4710
5378
|
const parseLegacyAdvisories = (advisories, source) => {
|
|
4711
|
-
const
|
|
4712
|
-
for (const [key, advisory] of Object.entries(advisories))
|
|
4713
|
-
|
|
4714
|
-
const severity = (advisory.severity ?? "moderate").toLowerCase();
|
|
4715
|
-
const recommendation = advisory.recommendation ?? advisory.title ?? `Run \`${defaultAuditFixCommand(source)}\` to resolve`;
|
|
4716
|
-
diagnostics.push({
|
|
4717
|
-
filePath: "package.json",
|
|
4718
|
-
engine: "security",
|
|
4719
|
-
rule: "security/vulnerable-dependency",
|
|
4720
|
-
severity: toSeverity(severity),
|
|
4721
|
-
message: `Vulnerable dependency (${source}): ${packageName} (${severity})`,
|
|
4722
|
-
help: withFixHint(recommendation),
|
|
4723
|
-
line: 0,
|
|
4724
|
-
column: 0,
|
|
4725
|
-
category: "Security",
|
|
4726
|
-
fixable: false
|
|
4727
|
-
});
|
|
4728
|
-
}
|
|
4729
|
-
return diagnostics;
|
|
5379
|
+
const bucket = /* @__PURE__ */ new Map();
|
|
5380
|
+
for (const [key, advisory] of Object.entries(advisories)) upsertVuln(bucket, advisory.module_name ?? advisory.name ?? advisory.package ?? key, (advisory.severity ?? "moderate").toLowerCase(), advisory.recommendation ?? advisory.title ?? "");
|
|
5381
|
+
return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
|
|
4730
5382
|
};
|
|
4731
5383
|
const parseModernVulnerabilities = (vulnerabilities, source) => {
|
|
4732
|
-
const
|
|
5384
|
+
const bucket = /* @__PURE__ */ new Map();
|
|
4733
5385
|
for (const [packageName, vulnerability] of Object.entries(vulnerabilities)) {
|
|
4734
5386
|
const severity = (vulnerability.severity ?? "moderate").toLowerCase();
|
|
4735
5387
|
const fixAvailable = vulnerability.fixAvailable;
|
|
4736
5388
|
const isDirect = vulnerability.isDirect === true;
|
|
4737
|
-
let recommendation =
|
|
4738
|
-
if (fixAvailable === false) recommendation = isDirect ? "
|
|
4739
|
-
else if (!isDirect && fixAvailable === true) recommendation = "
|
|
5389
|
+
let recommendation = "";
|
|
5390
|
+
if (fixAvailable === false) recommendation = isDirect ? "no automatic fix" : "transitive — needs override or parent upgrade";
|
|
5391
|
+
else if (!isDirect && fixAvailable === true) recommendation = "transitive — may need override or parent upgrade";
|
|
4740
5392
|
else if (fixAvailable && typeof fixAvailable === "object" && "name" in fixAvailable && "version" in fixAvailable) {
|
|
4741
5393
|
const target = fixAvailable;
|
|
4742
|
-
if (target.name && target.version) recommendation = `
|
|
5394
|
+
if (target.name && target.version) recommendation = `upgrade to ${target.name}@${target.version}`;
|
|
4743
5395
|
}
|
|
4744
|
-
|
|
4745
|
-
filePath: "package.json",
|
|
4746
|
-
engine: "security",
|
|
4747
|
-
rule: "security/vulnerable-dependency",
|
|
4748
|
-
severity: toSeverity(severity),
|
|
4749
|
-
message: `Vulnerable dependency (${source}): ${packageName} (${severity})`,
|
|
4750
|
-
help: withFixHint(recommendation),
|
|
4751
|
-
line: 0,
|
|
4752
|
-
column: 0,
|
|
4753
|
-
category: "Security",
|
|
4754
|
-
fixable: false
|
|
4755
|
-
});
|
|
5396
|
+
upsertVuln(bucket, packageName, severity, recommendation);
|
|
4756
5397
|
}
|
|
4757
|
-
return
|
|
5398
|
+
return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
|
|
4758
5399
|
};
|
|
4759
5400
|
const parseJsAudit = (output, source) => {
|
|
4760
5401
|
if (!output) return [];
|
|
@@ -5854,7 +6495,20 @@ const runClaudeHook = async (deps = {}) => {
|
|
|
5854
6495
|
const feedback = buildFeedback(diagnostics, score, rootDirectory, baseline ? {
|
|
5855
6496
|
score: baseline.score,
|
|
5856
6497
|
findingFingerprints: baseline.findingFingerprints
|
|
5857
|
-
} : void 0
|
|
6498
|
+
} : void 0, {
|
|
6499
|
+
agent: "claude",
|
|
6500
|
+
touchedFiles: files
|
|
6501
|
+
});
|
|
6502
|
+
track({
|
|
6503
|
+
event: "hook_scan_completed",
|
|
6504
|
+
properties: buildHookScanCompletedProps({
|
|
6505
|
+
agent: "claude",
|
|
6506
|
+
score,
|
|
6507
|
+
scoreDelta: baseline ? score - baseline.score : null,
|
|
6508
|
+
findingCount: diagnostics.length,
|
|
6509
|
+
fileCount: files.length
|
|
6510
|
+
})
|
|
6511
|
+
});
|
|
5858
6512
|
const envelope = renderClaudeOutput(JSON.stringify(feedback));
|
|
5859
6513
|
write(JSON.stringify(envelope));
|
|
5860
6514
|
return 0;
|
|
@@ -5925,6 +6579,9 @@ const runClaudeStopHook = async (deps = {}) => {
|
|
|
5925
6579
|
const feedback = buildFeedback(diagnostics, score, rootDirectory, {
|
|
5926
6580
|
score: baseline.score,
|
|
5927
6581
|
findingFingerprints: baseline.findingFingerprints
|
|
6582
|
+
}, {
|
|
6583
|
+
agent: "claude",
|
|
6584
|
+
touchedFiles: sessionFiles
|
|
5928
6585
|
});
|
|
5929
6586
|
if (!feedback.regressed) {
|
|
5930
6587
|
clearSessionFiles(cwd);
|
|
@@ -5985,7 +6642,19 @@ const runCursorHook = async (deps = {}) => {
|
|
|
5985
6642
|
if (!release) return 0;
|
|
5986
6643
|
try {
|
|
5987
6644
|
const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
|
|
5988
|
-
const feedback = buildFeedback(diagnostics, score, rootDirectory
|
|
6645
|
+
const feedback = buildFeedback(diagnostics, score, rootDirectory, void 0, {
|
|
6646
|
+
agent: "cursor",
|
|
6647
|
+
touchedFiles: files
|
|
6648
|
+
});
|
|
6649
|
+
track({
|
|
6650
|
+
event: "hook_scan_completed",
|
|
6651
|
+
properties: buildHookScanCompletedProps({
|
|
6652
|
+
agent: "cursor",
|
|
6653
|
+
score,
|
|
6654
|
+
findingCount: diagnostics.length,
|
|
6655
|
+
fileCount: files.length
|
|
6656
|
+
})
|
|
6657
|
+
});
|
|
5989
6658
|
const serialized = JSON.stringify(feedback);
|
|
5990
6659
|
write(JSON.stringify(renderCursorOutput(serialized)));
|
|
5991
6660
|
writeErr(`${serialized}\n`);
|
|
@@ -6035,7 +6704,19 @@ const runGeminiHook = async (deps = {}) => {
|
|
|
6035
6704
|
if (!release) return 0;
|
|
6036
6705
|
try {
|
|
6037
6706
|
const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
|
|
6038
|
-
const feedback = buildFeedback(diagnostics, score, rootDirectory
|
|
6707
|
+
const feedback = buildFeedback(diagnostics, score, rootDirectory, void 0, {
|
|
6708
|
+
agent: "gemini",
|
|
6709
|
+
touchedFiles: files
|
|
6710
|
+
});
|
|
6711
|
+
track({
|
|
6712
|
+
event: "hook_scan_completed",
|
|
6713
|
+
properties: buildHookScanCompletedProps({
|
|
6714
|
+
agent: "gemini",
|
|
6715
|
+
score,
|
|
6716
|
+
findingCount: diagnostics.length,
|
|
6717
|
+
fileCount: files.length
|
|
6718
|
+
})
|
|
6719
|
+
});
|
|
6039
6720
|
write(JSON.stringify(renderGeminiOutput(JSON.stringify(feedback))));
|
|
6040
6721
|
return 0;
|
|
6041
6722
|
} catch {
|
|
@@ -7050,12 +7731,18 @@ const registerInstall = (hook) => {
|
|
|
7050
7731
|
install.action(async (positional, opts) => {
|
|
7051
7732
|
const agents = await pickAgents("install", opts, positional);
|
|
7052
7733
|
if (agents === null || agents.length === 0) return;
|
|
7053
|
-
await
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7734
|
+
await withCommandLifecycle({
|
|
7735
|
+
command: "hook_install",
|
|
7736
|
+
config: loadConfig(process.cwd()).telemetry
|
|
7737
|
+
}, async () => {
|
|
7738
|
+
await hookInstall({
|
|
7739
|
+
agents,
|
|
7740
|
+
scope: resolveScope(opts),
|
|
7741
|
+
dryRun: Boolean(opts.dryRun),
|
|
7742
|
+
yes: Boolean(opts.yes),
|
|
7743
|
+
qualityGate: Boolean(opts.qualityGate)
|
|
7744
|
+
});
|
|
7745
|
+
return { exitCode: 0 };
|
|
7059
7746
|
});
|
|
7060
7747
|
});
|
|
7061
7748
|
};
|
|
@@ -7065,21 +7752,39 @@ const registerUninstall = (hook) => {
|
|
|
7065
7752
|
uninstall.action(async (positional, opts) => {
|
|
7066
7753
|
const agents = await pickAgents("uninstall", opts, positional);
|
|
7067
7754
|
if (agents === null || agents.length === 0) return;
|
|
7068
|
-
await
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7755
|
+
await withCommandLifecycle({
|
|
7756
|
+
command: "hook_uninstall",
|
|
7757
|
+
config: loadConfig(process.cwd()).telemetry
|
|
7758
|
+
}, async () => {
|
|
7759
|
+
await hookUninstall({
|
|
7760
|
+
agents,
|
|
7761
|
+
scope: resolveScope(opts),
|
|
7762
|
+
dryRun: Boolean(opts.dryRun),
|
|
7763
|
+
yes: true,
|
|
7764
|
+
qualityGate: false
|
|
7765
|
+
});
|
|
7766
|
+
return { exitCode: 0 };
|
|
7074
7767
|
});
|
|
7075
7768
|
});
|
|
7076
7769
|
};
|
|
7077
7770
|
const registerCallbacks = (hook) => {
|
|
7078
7771
|
hook.command("status").description("Show which agent hooks are installed").action(async () => {
|
|
7079
|
-
await
|
|
7772
|
+
await withCommandLifecycle({
|
|
7773
|
+
command: "hook_status",
|
|
7774
|
+
config: loadConfig(process.cwd()).telemetry
|
|
7775
|
+
}, async () => {
|
|
7776
|
+
await hookStatus();
|
|
7777
|
+
return { exitCode: 0 };
|
|
7778
|
+
});
|
|
7080
7779
|
});
|
|
7081
7780
|
hook.command("baseline").description("Capture the current project score as the quality-gate baseline").action(async () => {
|
|
7082
|
-
await
|
|
7781
|
+
await withCommandLifecycle({
|
|
7782
|
+
command: "hook_baseline",
|
|
7783
|
+
config: loadConfig(process.cwd()).telemetry
|
|
7784
|
+
}, async () => {
|
|
7785
|
+
await hookBaseline();
|
|
7786
|
+
return { exitCode: 0 };
|
|
7787
|
+
});
|
|
7083
7788
|
});
|
|
7084
7789
|
hook.command("claude").description("Internal: Claude Code PostToolUse / Stop / FileChanged callback (reads stdin)").option("--stop", "run in Stop-hook mode for the quality gate").option("--on-file-changed", "run in FileChanged mode (refresh baseline on watched file change)").action(async (opts) => {
|
|
7085
7790
|
await hookRun("claude", {
|
|
@@ -7333,8 +8038,53 @@ const wrapHelpText = (text, maxWidth, indent) => {
|
|
|
7333
8038
|
};
|
|
7334
8039
|
const terminalWidth = () => {
|
|
7335
8040
|
const raw = process.stdout.columns;
|
|
7336
|
-
if (typeof raw !== "number" || raw <= 0) return
|
|
7337
|
-
return Math.min(raw,
|
|
8041
|
+
if (typeof raw !== "number" || raw <= 0) return 120;
|
|
8042
|
+
return Math.min(raw, 120);
|
|
8043
|
+
};
|
|
8044
|
+
const renderRuleHeader = (first, count, lines) => {
|
|
8045
|
+
const level = toSeverityLabel(first.severity);
|
|
8046
|
+
const countLabel = count > 1 ? ` (${count})` : "";
|
|
8047
|
+
const status = colorBySeverity(level, first.severity);
|
|
8048
|
+
const fixableTag = first.fixable ? ` ${style(theme, "muted", "[auto]")}` : "";
|
|
8049
|
+
const fixableWidth = first.fixable ? 7 : 0;
|
|
8050
|
+
const badgePrefix = ` [${status}]${fixableTag} `;
|
|
8051
|
+
const badgePrefixWidth = 5 + level.length + 1 + fixableWidth + 1;
|
|
8052
|
+
const wrapped = wrapText(`${first.message}${countLabel}`, terminalWidth(), badgePrefixWidth, " ");
|
|
8053
|
+
lines.push(`${badgePrefix}${wrapped[0]}`);
|
|
8054
|
+
for (let i = 1; i < wrapped.length; i++) lines.push(wrapped[i]);
|
|
8055
|
+
};
|
|
8056
|
+
const renderLocations = (ruleDiags, verbose, lines) => {
|
|
8057
|
+
const unique = [];
|
|
8058
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8059
|
+
for (const d of ruleDiags) {
|
|
8060
|
+
const label = toLocationLabel(d);
|
|
8061
|
+
const detail = d.detail ?? "";
|
|
8062
|
+
const key = `${label}|${detail}`;
|
|
8063
|
+
if (seen.has(key)) continue;
|
|
8064
|
+
seen.add(key);
|
|
8065
|
+
unique.push({
|
|
8066
|
+
label,
|
|
8067
|
+
detail
|
|
8068
|
+
});
|
|
8069
|
+
}
|
|
8070
|
+
const shown = verbose ? unique : unique.slice(0, 3);
|
|
8071
|
+
const maxLabel = shown.reduce((w, l) => Math.max(w, l.label.length), 0);
|
|
8072
|
+
for (const { label, detail } of shown) {
|
|
8073
|
+
const padded = detail ? `${label.padEnd(maxLabel)} ${detail}` : label;
|
|
8074
|
+
lines.push(style(theme, "muted", ` ${padded}`));
|
|
8075
|
+
}
|
|
8076
|
+
if (!verbose && unique.length > shown.length) lines.push(style(theme, "muted", ` +${unique.length - shown.length} more location(s), use -d for full list`));
|
|
8077
|
+
};
|
|
8078
|
+
const renderHiddenFooter = (sorted, maxRules, lines) => {
|
|
8079
|
+
const hidden = sorted.slice(maxRules);
|
|
8080
|
+
const hiddenErrors = hidden.reduce((acc, [, diags]) => acc + (diags[0].severity === "error" ? diags.length : 0), 0);
|
|
8081
|
+
const hiddenWarnings = hidden.reduce((acc, [, diags]) => acc + (diags[0].severity === "warning" ? diags.length : 0), 0);
|
|
8082
|
+
const parts = [];
|
|
8083
|
+
if (hiddenErrors > 0) parts.push(`${hiddenErrors} error${hiddenErrors === 1 ? "" : "s"}`);
|
|
8084
|
+
if (hiddenWarnings > 0) parts.push(`${hiddenWarnings} warning${hiddenWarnings === 1 ? "" : "s"}`);
|
|
8085
|
+
const detail = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
8086
|
+
lines.push(style(theme, "muted", ` ... and ${hidden.length} more rules hidden${detail}. Run with -v or --verbose to see full output.`));
|
|
8087
|
+
lines.push("");
|
|
7338
8088
|
};
|
|
7339
8089
|
const renderDiagnostics = (diagnostics, verbose) => {
|
|
7340
8090
|
const lines = [];
|
|
@@ -7343,29 +8093,23 @@ const renderDiagnostics = (diagnostics, verbose) => {
|
|
|
7343
8093
|
const label = getEngineLabel(engine);
|
|
7344
8094
|
lines.push(` ${style(theme, "bold", `${symbols.engineActive} ${label}`)}`);
|
|
7345
8095
|
const sorted = [...groupBy(engineDiags, (d) => `${d.rule}:${d.message}`).entries()].sort(([, a], [, b]) => {
|
|
7346
|
-
|
|
8096
|
+
const sa = a[0].severity === "error" ? 0 : a[0].severity === "warning" ? 1 : 2;
|
|
8097
|
+
const sb = b[0].severity === "error" ? 0 : b[0].severity === "warning" ? 1 : 2;
|
|
8098
|
+
if (sa !== sb) return sa - sb;
|
|
8099
|
+
return b.length - a.length;
|
|
7347
8100
|
});
|
|
7348
|
-
|
|
8101
|
+
const maxRules = verbose ? Infinity : 40;
|
|
8102
|
+
for (const [, ruleDiags] of sorted.slice(0, maxRules)) {
|
|
7349
8103
|
const first = ruleDiags[0];
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
const status = colorBySeverity(level, first.severity);
|
|
7353
|
-
const fixableTag = first.fixable ? ` ${style(theme, "muted", "[auto]")}` : "";
|
|
7354
|
-
const fixableWidth = first.fixable ? 7 : 0;
|
|
7355
|
-
const badgePrefix = ` [${status}]${fixableTag} `;
|
|
7356
|
-
const badgePrefixWidth = 5 + level.length + 1 + fixableWidth + 1;
|
|
7357
|
-
const wrappedMsg = wrapText(`${first.message}${count}`, terminalWidth(), badgePrefixWidth, " ");
|
|
7358
|
-
lines.push(`${badgePrefix}${wrappedMsg[0]}`);
|
|
7359
|
-
for (let i = 1; i < wrappedMsg.length; i++) lines.push(wrappedMsg[i]);
|
|
7360
|
-
const locations = verbose ? ruleDiags : ruleDiags.slice(0, 3);
|
|
7361
|
-
for (const diagnostic of locations) lines.push(style(theme, "muted", ` ${toLocationLabel(diagnostic)}`));
|
|
7362
|
-
if (!verbose && ruleDiags.length > locations.length) lines.push(style(theme, "muted", ` +${ruleDiags.length - locations.length} more location(s), use -d for full list`));
|
|
8104
|
+
renderRuleHeader(first, ruleDiags.length, lines);
|
|
8105
|
+
renderLocations(ruleDiags, verbose, lines);
|
|
7363
8106
|
if (first.help) {
|
|
7364
8107
|
const wrapped = wrapHelpText(first.help, terminalWidth(), " ");
|
|
7365
8108
|
for (const line of wrapped) lines.push(style(theme, "muted", line));
|
|
7366
8109
|
}
|
|
7367
8110
|
lines.push("");
|
|
7368
8111
|
}
|
|
8112
|
+
if (sorted.length > maxRules) renderHiddenFooter(sorted, maxRules, lines);
|
|
7369
8113
|
}
|
|
7370
8114
|
return `${lines.join("\n")}\n`;
|
|
7371
8115
|
};
|
|
@@ -7544,6 +8288,81 @@ var LiveGrid = class {
|
|
|
7544
8288
|
}
|
|
7545
8289
|
};
|
|
7546
8290
|
|
|
8291
|
+
//#endregion
|
|
8292
|
+
//#region src/output/rule-labels.ts
|
|
8293
|
+
const RULE_LABELS = {
|
|
8294
|
+
formatting: "Code not formatted",
|
|
8295
|
+
"code-quality/duplicate-block": "Duplicate code block",
|
|
8296
|
+
"complexity/file-too-large": "File too large",
|
|
8297
|
+
"complexity/function-too-long": "Function too long",
|
|
8298
|
+
"complexity/deep-nesting": "Deeply nested code",
|
|
8299
|
+
"complexity/too-many-params": "Too many parameters",
|
|
8300
|
+
"knip/files": "Unused file",
|
|
8301
|
+
"knip/dependencies": "Unused dependency",
|
|
8302
|
+
"knip/devDependencies": "Unused dev dependency",
|
|
8303
|
+
"knip/unlisted": "Used but not in package.json",
|
|
8304
|
+
"knip/unresolved": "Unresolved import",
|
|
8305
|
+
"knip/binaries": "Unused binary",
|
|
8306
|
+
"knip/exports": "Unused export",
|
|
8307
|
+
"knip/types": "Unused type",
|
|
8308
|
+
"ai-slop/trivial-comment": "Trivial restating comment",
|
|
8309
|
+
"ai-slop/swallowed-exception": "Empty catch (swallowed error)",
|
|
8310
|
+
"ai-slop/thin-wrapper": "Thin function wrapper",
|
|
8311
|
+
"ai-slop/generic-naming": "Generic/vague identifier name",
|
|
8312
|
+
"ai-slop/unused-import": "Unused import",
|
|
8313
|
+
"ai-slop/console-leftover": "console.log left in code",
|
|
8314
|
+
"ai-slop/todo-stub": "Unresolved TODO/FIXME",
|
|
8315
|
+
"ai-slop/unreachable-code": "Unreachable code",
|
|
8316
|
+
"ai-slop/constant-condition": "Constant condition",
|
|
8317
|
+
"ai-slop/empty-function": "Empty function body",
|
|
8318
|
+
"ai-slop/unsafe-type-assertion": "Unsafe type cast",
|
|
8319
|
+
"ai-slop/double-type-assertion": "Double type cast",
|
|
8320
|
+
"ai-slop/ts-directive": "@ts-ignore / @ts-expect-error",
|
|
8321
|
+
"ai-slop/narrative-comment": "Narrative comment block",
|
|
8322
|
+
"ai-slop/duplicate-import": "Duplicate import statement",
|
|
8323
|
+
"ai-slop/python-bare-except": "Bare except",
|
|
8324
|
+
"ai-slop/python-broad-except": "Broad except",
|
|
8325
|
+
"ai-slop/python-mutable-default": "Mutable default argument",
|
|
8326
|
+
"ai-slop/python-print-debug": "print() left in code",
|
|
8327
|
+
"ai-slop/go-library-panic": "panic() in Go library code",
|
|
8328
|
+
"ai-slop/rust-non-test-unwrap": "Rust .unwrap() in production code",
|
|
8329
|
+
"ai-slop/rust-todo-stub": "Rust todo!() stub",
|
|
8330
|
+
"ai-slop/hallucinated-import": "Import not in package.json",
|
|
8331
|
+
"security/hardcoded-secret": "Possible hardcoded secret",
|
|
8332
|
+
"security/vulnerable-dependency": "Vulnerable dependency",
|
|
8333
|
+
"security/eval": "eval() usage",
|
|
8334
|
+
"security/innerhtml": "innerHTML assignment",
|
|
8335
|
+
"security/dangerously-set-innerhtml": "dangerouslySetInnerHTML (XSS risk)",
|
|
8336
|
+
"security/sql-injection": "Possible SQL injection",
|
|
8337
|
+
"security/shell-injection": "Possible shell injection",
|
|
8338
|
+
"eslint/no-undef": "Undefined identifier",
|
|
8339
|
+
"eslint/no-unused-vars": "Unused variable",
|
|
8340
|
+
"eslint/no-unassigned-vars": "Variable never assigned",
|
|
8341
|
+
"eslint/no-empty": "Empty block statement",
|
|
8342
|
+
"eslint/no-unused-expressions": "Unused expression",
|
|
8343
|
+
"eslint/no-shadow-restricted-names": "Shadowing restricted name",
|
|
8344
|
+
"eslint/no-constant-binary-expression": "Constant binary expression",
|
|
8345
|
+
"eslint/no-unsafe-optional-chaining": "Unsafe optional chaining",
|
|
8346
|
+
"eslint/require-yield": "Generator with no yield",
|
|
8347
|
+
"import/no-duplicates": "Duplicate import path",
|
|
8348
|
+
"import/default": "Missing default export",
|
|
8349
|
+
"import/named": "Missing named export",
|
|
8350
|
+
"import/namespace": "Invalid namespace import",
|
|
8351
|
+
"typescript-eslint/triple-slash-reference": "Triple-slash reference",
|
|
8352
|
+
"unicorn/no-useless-fallback-in-spread": "Useless spread fallback",
|
|
8353
|
+
"unicorn/no-invalid-remove-event-listener": "Invalid removeEventListener",
|
|
8354
|
+
"unicorn/no-empty-file": "Empty file",
|
|
8355
|
+
"unicorn/no-useless-length-check": "Useless array length check",
|
|
8356
|
+
"unicorn/no-new-array": "Avoid new Array(n)",
|
|
8357
|
+
"unicorn/no-useless-spread": "Useless spread",
|
|
8358
|
+
"unicorn/no-single-promise-in-promise-methods": "Single-element Promise.all"
|
|
8359
|
+
};
|
|
8360
|
+
const prettifyFallback = (ruleId) => {
|
|
8361
|
+
const spaced = (ruleId.includes("/") ? ruleId.slice(ruleId.indexOf("/") + 1) : ruleId).replace(/[-_]/g, " ").replace(/\//g, " · ");
|
|
8362
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
8363
|
+
};
|
|
8364
|
+
const labelForRule = (ruleId) => RULE_LABELS[ruleId] ?? prettifyFallback(ruleId);
|
|
8365
|
+
|
|
7547
8366
|
//#endregion
|
|
7548
8367
|
//#region src/ui/summary.ts
|
|
7549
8368
|
const elapsed = (ms) => ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(1)}s`;
|
|
@@ -7570,6 +8389,34 @@ const renderSummary = (input, deps = {}) => {
|
|
|
7570
8389
|
` ${style(t, "muted", `${input.files} files`)} ${sep} ${style(t, "muted", `${input.engines} engines`)} ${sep} ${style(t, "muted", elapsed(input.elapsedMs))}`,
|
|
7571
8390
|
""
|
|
7572
8391
|
];
|
|
8392
|
+
if (input.breakdown && input.breakdown.rows.length > 0) {
|
|
8393
|
+
lines.push(` ${style(t, "bold", "Top findings")}`);
|
|
8394
|
+
const maxCountWidth = input.breakdown.rows.reduce((w, r) => Math.max(w, String(r.errors + r.warnings + r.info).length), 0);
|
|
8395
|
+
const labels = input.breakdown.rows.map((r) => labelForRule(r.rule));
|
|
8396
|
+
const maxLabelWidth = labels.reduce((w, l) => Math.max(w, l.length), 0);
|
|
8397
|
+
for (let i = 0; i < input.breakdown.rows.length; i++) {
|
|
8398
|
+
const row = input.breakdown.rows[i];
|
|
8399
|
+
const total = row.errors + row.warnings + row.info;
|
|
8400
|
+
const count = String(total).padStart(maxCountWidth);
|
|
8401
|
+
const label = padEnd(labels[i], maxLabelWidth);
|
|
8402
|
+
const tags = [];
|
|
8403
|
+
if (row.errors > 0) tags.push(style(t, "danger", `${row.errors} err`));
|
|
8404
|
+
if (row.warnings > 0) tags.push(style(t, "warn", `${row.warnings} warn`));
|
|
8405
|
+
if (row.info > 0) tags.push(style(t, "muted", `${row.info} info`));
|
|
8406
|
+
if (row.fixable > 0) tags.push(style(t, "success", `${row.fixable} fix`));
|
|
8407
|
+
const tagBlock = tags.length > 0 ? ` ${style(t, "muted", "·")} ${tags.join(" ")}` : "";
|
|
8408
|
+
const ruleHint = style(t, "muted", `(${row.rule})`);
|
|
8409
|
+
lines.push(` ${style(t, "muted", count)} ${label} ${ruleHint}${tagBlock}`);
|
|
8410
|
+
}
|
|
8411
|
+
if (input.breakdown.hiddenRules > 0) {
|
|
8412
|
+
const hiddenParts = [];
|
|
8413
|
+
if (input.breakdown.hiddenErrors > 0) hiddenParts.push(`${input.breakdown.hiddenErrors} error${input.breakdown.hiddenErrors === 1 ? "" : "s"}`);
|
|
8414
|
+
if (input.breakdown.hiddenWarnings > 0) hiddenParts.push(`${input.breakdown.hiddenWarnings} warning${input.breakdown.hiddenWarnings === 1 ? "" : "s"}`);
|
|
8415
|
+
const detail = hiddenParts.length > 0 ? ` (${hiddenParts.join(", ")})` : "";
|
|
8416
|
+
lines.push(style(t, "muted", ` +${input.breakdown.hiddenRules} more rule${input.breakdown.hiddenRules === 1 ? "" : "s"}${detail}. Run with -v for the full list.`));
|
|
8417
|
+
}
|
|
8418
|
+
lines.push("");
|
|
8419
|
+
}
|
|
7573
8420
|
if (input.nextSteps.length > 0) {
|
|
7574
8421
|
for (const step of input.nextSteps) {
|
|
7575
8422
|
const glyph = step.emphasis === "primary" ? s.hint : s.bullet;
|
|
@@ -7592,77 +8439,43 @@ const renderCleanRun = (input, deps = {}) => {
|
|
|
7592
8439
|
return `\n ${parts.join(` ${sep} `)}\n`;
|
|
7593
8440
|
};
|
|
7594
8441
|
|
|
7595
|
-
//#endregion
|
|
7596
|
-
//#region src/version.ts
|
|
7597
|
-
const APP_VERSION = "0.8.3";
|
|
7598
|
-
|
|
7599
|
-
//#endregion
|
|
7600
|
-
//#region src/utils/telemetry.ts
|
|
7601
|
-
const POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
7602
|
-
const POSTHOG_KEY = "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
|
|
7603
|
-
/**
|
|
7604
|
-
* Returns true if telemetry should be disabled.
|
|
7605
|
-
* Telemetry is opt-out: it runs unless explicitly disabled.
|
|
7606
|
-
*/
|
|
7607
|
-
const isTelemetryDisabled = (configEnabled) => {
|
|
7608
|
-
if (process.env.AISLOP_NO_TELEMETRY === "1" || process.env.DO_NOT_TRACK === "1") return true;
|
|
7609
|
-
if (process.env.CI === "true" || process.env.CI === "1") return true;
|
|
7610
|
-
if (configEnabled === false) return true;
|
|
7611
|
-
return false;
|
|
7612
|
-
};
|
|
7613
|
-
const getScoreBucket = (score) => {
|
|
7614
|
-
if (score >= 75) return "75-100";
|
|
7615
|
-
if (score >= 50) return "50-75";
|
|
7616
|
-
if (score >= 25) return "25-50";
|
|
7617
|
-
return "0-25";
|
|
7618
|
-
};
|
|
7619
|
-
const getAnonymousId = () => {
|
|
7620
|
-
const raw = `${os.hostname()}-${os.platform()}-${os.arch()}`;
|
|
7621
|
-
let hash = 5381;
|
|
7622
|
-
for (let i = 0; i < raw.length; i++) hash = hash * 33 ^ raw.charCodeAt(i);
|
|
7623
|
-
return `aislop_${(hash >>> 0).toString(36)}`;
|
|
7624
|
-
};
|
|
7625
|
-
/** Pending telemetry request — kept alive so Node doesn't exit before it completes. */
|
|
7626
|
-
let pendingRequest = null;
|
|
7627
|
-
const trackEvent = (event) => {
|
|
7628
|
-
const payload = {
|
|
7629
|
-
api_key: POSTHOG_KEY,
|
|
7630
|
-
event: `cli_${event.command}`,
|
|
7631
|
-
distinct_id: getAnonymousId(),
|
|
7632
|
-
properties: {
|
|
7633
|
-
version: APP_VERSION,
|
|
7634
|
-
node_version: process.version,
|
|
7635
|
-
os: os.platform(),
|
|
7636
|
-
arch: os.arch(),
|
|
7637
|
-
languages: event.languages,
|
|
7638
|
-
score_bucket: event.scoreBucket,
|
|
7639
|
-
engine_issues: event.engineIssues,
|
|
7640
|
-
engine_timings: event.engineTimings,
|
|
7641
|
-
elapsed_ms: event.elapsedMs,
|
|
7642
|
-
file_count: event.fileCount,
|
|
7643
|
-
fix_steps: event.fixSteps,
|
|
7644
|
-
fix_resolved: event.fixResolved
|
|
7645
|
-
},
|
|
7646
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7647
|
-
};
|
|
7648
|
-
pendingRequest = fetch(`${POSTHOG_HOST}/capture/`, {
|
|
7649
|
-
method: "POST",
|
|
7650
|
-
headers: { "Content-Type": "application/json" },
|
|
7651
|
-
body: JSON.stringify(payload),
|
|
7652
|
-
signal: AbortSignal.timeout(3e3)
|
|
7653
|
-
}).then(() => {}).catch(() => {});
|
|
7654
|
-
};
|
|
7655
|
-
const flushTelemetry = async () => {
|
|
7656
|
-
if (pendingRequest) {
|
|
7657
|
-
await pendingRequest;
|
|
7658
|
-
pendingRequest = null;
|
|
7659
|
-
}
|
|
7660
|
-
};
|
|
7661
|
-
|
|
7662
8442
|
//#endregion
|
|
7663
8443
|
//#region src/commands/scan.ts
|
|
7664
8444
|
const shouldUseSpinner = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
|
|
7665
8445
|
const ALL_ENGINE_NAMES = Object.keys(ENGINE_INFO);
|
|
8446
|
+
const BREAKDOWN_TOP_N = 10;
|
|
8447
|
+
const computeBreakdown = (diagnostics) => {
|
|
8448
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
8449
|
+
for (const d of diagnostics) {
|
|
8450
|
+
const row = byRule.get(d.rule) ?? {
|
|
8451
|
+
rule: d.rule,
|
|
8452
|
+
errors: 0,
|
|
8453
|
+
warnings: 0,
|
|
8454
|
+
info: 0,
|
|
8455
|
+
fixable: 0
|
|
8456
|
+
};
|
|
8457
|
+
if (d.severity === "error") row.errors++;
|
|
8458
|
+
else if (d.severity === "warning") row.warnings++;
|
|
8459
|
+
else row.info++;
|
|
8460
|
+
if (d.fixable) row.fixable++;
|
|
8461
|
+
byRule.set(d.rule, row);
|
|
8462
|
+
}
|
|
8463
|
+
const sorted = [...byRule.values()].sort((a, b) => {
|
|
8464
|
+
const aTotal = a.errors + a.warnings + a.info;
|
|
8465
|
+
const bTotal = b.errors + b.warnings + b.info;
|
|
8466
|
+
if (aTotal !== bTotal) return bTotal - aTotal;
|
|
8467
|
+
if (a.errors !== b.errors) return b.errors - a.errors;
|
|
8468
|
+
return a.rule.localeCompare(b.rule);
|
|
8469
|
+
});
|
|
8470
|
+
const rows = sorted.slice(0, BREAKDOWN_TOP_N);
|
|
8471
|
+
const hidden = sorted.slice(BREAKDOWN_TOP_N);
|
|
8472
|
+
return {
|
|
8473
|
+
rows,
|
|
8474
|
+
hiddenRules: hidden.length,
|
|
8475
|
+
hiddenErrors: hidden.reduce((acc, r) => acc + r.errors, 0),
|
|
8476
|
+
hiddenWarnings: hidden.reduce((acc, r) => acc + r.warnings, 0)
|
|
8477
|
+
};
|
|
8478
|
+
};
|
|
7666
8479
|
const buildScanRender = (input) => {
|
|
7667
8480
|
const deps = {
|
|
7668
8481
|
theme: createTheme(),
|
|
@@ -7712,11 +8525,11 @@ const buildScanRender = (input) => {
|
|
|
7712
8525
|
engines: input.results.length,
|
|
7713
8526
|
elapsedMs: input.elapsedMs,
|
|
7714
8527
|
nextSteps,
|
|
8528
|
+
breakdown: computeBreakdown(input.diagnostics),
|
|
7715
8529
|
thresholds: input.thresholds
|
|
7716
8530
|
}, deps)}`;
|
|
7717
8531
|
};
|
|
7718
8532
|
const scanCommand = async (directory, config, options) => {
|
|
7719
|
-
const startTime = performance.now();
|
|
7720
8533
|
const resolvedDir = path.resolve(directory);
|
|
7721
8534
|
if (!fs.existsSync(resolvedDir)) {
|
|
7722
8535
|
const msg = `Path does not exist: ${resolvedDir}`;
|
|
@@ -7730,9 +8543,18 @@ const scanCommand = async (directory, config, options) => {
|
|
|
7730
8543
|
else log.error(msg);
|
|
7731
8544
|
return { exitCode: 1 };
|
|
7732
8545
|
}
|
|
8546
|
+
const projectInfo = await discoverProject(resolvedDir);
|
|
8547
|
+
return withCommandLifecycle({
|
|
8548
|
+
command: options.command ?? "scan",
|
|
8549
|
+
config: config.telemetry,
|
|
8550
|
+
languages: projectInfo.languages,
|
|
8551
|
+
fileCount: projectInfo.sourceFileCount
|
|
8552
|
+
}, () => runScanBody(resolvedDir, config, options, projectInfo));
|
|
8553
|
+
};
|
|
8554
|
+
const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
8555
|
+
const startTime = performance.now();
|
|
7733
8556
|
const showHeader = options.showHeader !== false;
|
|
7734
8557
|
const useLiveProgress = !options.json && shouldUseSpinner();
|
|
7735
|
-
const projectInfo = await discoverProject(resolvedDir);
|
|
7736
8558
|
let files;
|
|
7737
8559
|
if (options.staged) {
|
|
7738
8560
|
files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [], config.exclude);
|
|
@@ -7799,28 +8621,27 @@ const scanCommand = async (directory, config, options) => {
|
|
|
7799
8621
|
const elapsedMs = performance.now() - startTime;
|
|
7800
8622
|
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
|
|
7801
8623
|
const exitCode = allDiagnostics.some((d) => d.severity === "error") || scoreResult.score < config.ci.failBelow ? 1 : 0;
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
}
|
|
8624
|
+
const engineIssues = {};
|
|
8625
|
+
const engineTimings = {};
|
|
8626
|
+
for (const r of results) {
|
|
8627
|
+
engineIssues[r.engine] = r.diagnostics.length;
|
|
8628
|
+
engineTimings[r.engine] = Math.round(r.elapsed);
|
|
8629
|
+
}
|
|
8630
|
+
const completion = {
|
|
8631
|
+
exitCode,
|
|
8632
|
+
score: scoreResult.score,
|
|
8633
|
+
findingCount: allDiagnostics.length,
|
|
8634
|
+
errorCount: allDiagnostics.filter((d) => d.severity === "error").length,
|
|
8635
|
+
warningCount: allDiagnostics.filter((d) => d.severity === "warning").length,
|
|
8636
|
+
fixableCount: allDiagnostics.filter((d) => d.fixable).length,
|
|
8637
|
+
engineIssues,
|
|
8638
|
+
engineTimings
|
|
8639
|
+
};
|
|
7819
8640
|
if (options.json) {
|
|
7820
|
-
const { buildJsonOutput } = await import("./json-
|
|
8641
|
+
const { buildJsonOutput } = await import("./json-OIzja7OM.js");
|
|
7821
8642
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
7822
8643
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
7823
|
-
return
|
|
8644
|
+
return completion;
|
|
7824
8645
|
}
|
|
7825
8646
|
const projectName = projectInfo.projectName ?? "project";
|
|
7826
8647
|
const language = projectInfo.languages[0] ?? "unknown";
|
|
@@ -7837,7 +8658,7 @@ const scanCommand = async (directory, config, options) => {
|
|
|
7837
8658
|
includeHeader: showHeader,
|
|
7838
8659
|
printBrand: options.printBrand
|
|
7839
8660
|
}));
|
|
7840
|
-
return
|
|
8661
|
+
return completion;
|
|
7841
8662
|
};
|
|
7842
8663
|
|
|
7843
8664
|
//#endregion
|
|
@@ -9785,15 +10606,23 @@ const fixCommand = async (directory, config, options = {
|
|
|
9785
10606
|
verbose: false,
|
|
9786
10607
|
showHeader: true
|
|
9787
10608
|
}) => {
|
|
9788
|
-
const startTime = performance.now();
|
|
9789
10609
|
const resolvedDir = path.resolve(directory);
|
|
9790
10610
|
if (!fs.existsSync(resolvedDir) || !fs.statSync(resolvedDir).isDirectory()) {
|
|
9791
10611
|
const msg = !fs.existsSync(resolvedDir) ? `Path does not exist: ${resolvedDir}` : `Not a directory: ${resolvedDir}`;
|
|
9792
10612
|
log.error(msg);
|
|
9793
10613
|
return;
|
|
9794
10614
|
}
|
|
9795
|
-
const showHeader = options.showHeader !== false;
|
|
9796
10615
|
const projectInfo = await discoverProject(resolvedDir);
|
|
10616
|
+
await withCommandLifecycle({
|
|
10617
|
+
command: "fix",
|
|
10618
|
+
config: config.telemetry,
|
|
10619
|
+
languages: projectInfo.languages,
|
|
10620
|
+
fileCount: projectInfo.sourceFileCount
|
|
10621
|
+
}, () => runFixBody(resolvedDir, config, options, projectInfo));
|
|
10622
|
+
};
|
|
10623
|
+
const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
10624
|
+
const startTime = performance.now();
|
|
10625
|
+
const showHeader = options.showHeader !== false;
|
|
9797
10626
|
const projectName = projectInfo.projectName ?? "project";
|
|
9798
10627
|
if (showHeader) process.stdout.write(renderHeader({
|
|
9799
10628
|
version: APP_VERSION,
|
|
@@ -9830,12 +10659,6 @@ const fixCommand = async (directory, config, options = {
|
|
|
9830
10659
|
await runFormattingStep(pipelineDeps);
|
|
9831
10660
|
await runForceSteps(pipelineDeps);
|
|
9832
10661
|
const totalResolved = steps.reduce((sum, s) => sum + s.resolvedIssues, 0);
|
|
9833
|
-
if (!isTelemetryDisabled(config.telemetry?.enabled)) trackEvent({
|
|
9834
|
-
command: "fix",
|
|
9835
|
-
languages: projectInfo.languages,
|
|
9836
|
-
fixSteps: steps.length,
|
|
9837
|
-
fixResolved: totalResolved
|
|
9838
|
-
});
|
|
9839
10662
|
const configDir = findConfigDir(resolvedDir);
|
|
9840
10663
|
const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
|
|
9841
10664
|
const engineConfig = {
|
|
@@ -9858,7 +10681,9 @@ const fixCommand = async (directory, config, options = {
|
|
|
9858
10681
|
});
|
|
9859
10682
|
const allDiagnostics = scanResults.flatMap((r) => r.diagnostics);
|
|
9860
10683
|
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
|
|
9861
|
-
const
|
|
10684
|
+
const errors = allDiagnostics.filter((d) => d.severity === "error").length;
|
|
10685
|
+
const warnings = allDiagnostics.filter((d) => d.severity === "warning").length;
|
|
10686
|
+
const remaining = errors + warnings;
|
|
9862
10687
|
if (steps.length === 0) rail.complete({
|
|
9863
10688
|
status: "skipped",
|
|
9864
10689
|
label: "No applicable auto-fixers found"
|
|
@@ -9887,12 +10712,31 @@ const fixCommand = async (directory, config, options = {
|
|
|
9887
10712
|
}
|
|
9888
10713
|
if (options.agent) {
|
|
9889
10714
|
launchAgent(options.agent, resolvedDir, allDiagnostics, scoreResult.score);
|
|
9890
|
-
return
|
|
10715
|
+
return {
|
|
10716
|
+
exitCode: 0,
|
|
10717
|
+
score: scoreResult.score,
|
|
10718
|
+
fixSteps: steps.length,
|
|
10719
|
+
fixResolved: totalResolved
|
|
10720
|
+
};
|
|
9891
10721
|
}
|
|
9892
10722
|
if (options.prompt) {
|
|
9893
10723
|
printPrompt(resolvedDir, allDiagnostics, scoreResult.score);
|
|
9894
|
-
return
|
|
10724
|
+
return {
|
|
10725
|
+
exitCode: 0,
|
|
10726
|
+
score: scoreResult.score,
|
|
10727
|
+
fixSteps: steps.length,
|
|
10728
|
+
fixResolved: totalResolved
|
|
10729
|
+
};
|
|
9895
10730
|
}
|
|
10731
|
+
return {
|
|
10732
|
+
exitCode: 0,
|
|
10733
|
+
score: scoreResult.score,
|
|
10734
|
+
findingCount: allDiagnostics.length,
|
|
10735
|
+
errorCount: errors,
|
|
10736
|
+
warningCount: warnings,
|
|
10737
|
+
fixSteps: steps.length,
|
|
10738
|
+
fixResolved: totalResolved
|
|
10739
|
+
};
|
|
9896
10740
|
};
|
|
9897
10741
|
|
|
9898
10742
|
//#endregion
|
|
@@ -10008,10 +10852,18 @@ const promptForConfigChoices = async () => {
|
|
|
10008
10852
|
return {
|
|
10009
10853
|
engines: enginesSelection,
|
|
10010
10854
|
failBelow: Number(failBelowRaw),
|
|
10855
|
+
typecheck: DEFAULT_CONFIG.lint.typecheck,
|
|
10011
10856
|
telemetryEnabled: telemetryChoice === "enabled",
|
|
10012
10857
|
writeGithubWorkflow: workflowChoice === "yes"
|
|
10013
10858
|
};
|
|
10014
10859
|
};
|
|
10860
|
+
const strictChoices = () => ({
|
|
10861
|
+
engines: Object.keys(DEFAULT_CONFIG.engines),
|
|
10862
|
+
failBelow: 85,
|
|
10863
|
+
typecheck: true,
|
|
10864
|
+
telemetryEnabled: DEFAULT_CONFIG.telemetry.enabled,
|
|
10865
|
+
writeGithubWorkflow: true
|
|
10866
|
+
});
|
|
10015
10867
|
const writeAislopConfig = (configDir, configPath, choices) => {
|
|
10016
10868
|
const selected = new Set(choices.engines);
|
|
10017
10869
|
const engines = {
|
|
@@ -10026,6 +10878,7 @@ const writeAislopConfig = (configDir, configPath, choices) => {
|
|
|
10026
10878
|
version: DEFAULT_CONFIG.version,
|
|
10027
10879
|
engines,
|
|
10028
10880
|
quality: { ...DEFAULT_CONFIG.quality },
|
|
10881
|
+
lint: { typecheck: choices.typecheck },
|
|
10029
10882
|
security: { ...DEFAULT_CONFIG.security },
|
|
10030
10883
|
scoring: {
|
|
10031
10884
|
weights: { ...DEFAULT_CONFIG.scoring.weights },
|
|
@@ -10078,7 +10931,7 @@ const initCommand = async (directory, options = {}) => {
|
|
|
10078
10931
|
return;
|
|
10079
10932
|
}
|
|
10080
10933
|
}
|
|
10081
|
-
const choices = await promptForConfigChoices();
|
|
10934
|
+
const choices = options.strict ? strictChoices() : await promptForConfigChoices();
|
|
10082
10935
|
if (!choices) return;
|
|
10083
10936
|
writeAislopConfig(configDir, configPath, choices);
|
|
10084
10937
|
const steps = [{
|
|
@@ -10375,29 +11228,38 @@ const interactiveCommand = async (directory, config) => {
|
|
|
10375
11228
|
//#region src/cli.ts
|
|
10376
11229
|
process.on("SIGINT", () => process.exit(0));
|
|
10377
11230
|
process.on("SIGTERM", () => process.exit(0));
|
|
10378
|
-
const
|
|
11231
|
+
const fireInstalledOnce = () => {
|
|
11232
|
+
if (isTelemetryDisabled(loadConfig(process.cwd()).telemetry)) return;
|
|
11233
|
+
if (ensureInstallId(resolveInstallIdPath()).created) track({
|
|
11234
|
+
event: "cli_installed",
|
|
11235
|
+
config: loadConfig(process.cwd()).telemetry
|
|
11236
|
+
});
|
|
11237
|
+
};
|
|
11238
|
+
const commaSeparatedParser = (value, previous = []) => {
|
|
10379
11239
|
const parts = value.split(",").map((v) => v.trim()).filter(Boolean);
|
|
10380
11240
|
return [...previous, ...parts];
|
|
10381
11241
|
};
|
|
10382
11242
|
const runScan = async (directory, flags) => {
|
|
10383
11243
|
const config = loadConfig(directory);
|
|
10384
|
-
const { exitCode } = await scanCommand(directory,
|
|
11244
|
+
const { exitCode } = await scanCommand(directory, {
|
|
10385
11245
|
...config,
|
|
10386
|
-
exclude: [...config.exclude ?? [], ...flags.exclude]
|
|
10387
|
-
|
|
11246
|
+
exclude: [...config.exclude ?? [], ...flags.exclude ?? []],
|
|
11247
|
+
include: [...config.include ?? [], ...flags.include ?? []]
|
|
11248
|
+
}, {
|
|
10388
11249
|
changes: Boolean(flags.changes),
|
|
10389
11250
|
staged: Boolean(flags.staged),
|
|
10390
11251
|
verbose: Boolean(flags.verbose),
|
|
10391
11252
|
json: Boolean(flags.json),
|
|
10392
|
-
exclude: flags.exclude
|
|
11253
|
+
exclude: flags.exclude,
|
|
11254
|
+
include: flags.include
|
|
10393
11255
|
});
|
|
10394
11256
|
if (exitCode !== 0) {
|
|
10395
11257
|
await flushTelemetry();
|
|
10396
|
-
process.
|
|
11258
|
+
process.exitCode = exitCode;
|
|
10397
11259
|
}
|
|
10398
11260
|
};
|
|
10399
|
-
const noFlagsPassed = (flags) => !flags.changes && !flags.staged && !flags.verbose && !flags.json && !(flags.exclude && flags.exclude.length > 0);
|
|
10400
|
-
const program = new Command().name("aislop").description("The unified code quality CLI").version(APP_VERSION, "-v, --version").argument("[directory]", "project directory to scan", ".").option("--changes", "only scan changed files (git diff)").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON instead of terminal UI").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude",
|
|
11261
|
+
const noFlagsPassed = (flags) => !flags.changes && !flags.staged && !flags.verbose && !flags.json && !(flags.exclude && flags.exclude.length > 0) && !(flags.include && flags.include.length > 0);
|
|
11262
|
+
const program = new Command().name("aislop").description("The unified code quality CLI").version(APP_VERSION, "-v, --version").argument("[directory]", "project directory to scan", ".").option("--changes", "only scan changed files (git diff)").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON instead of terminal UI").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude", commaSeparatedParser, []).option("--include <patterns>", "comma-separated or repeatable list of paths and files to include", commaSeparatedParser, []).action(async (directory, flags) => {
|
|
10401
11263
|
if (noFlagsPassed(flags) && process.stdin.isTTY) try {
|
|
10402
11264
|
await interactiveCommand(directory, loadConfig(directory));
|
|
10403
11265
|
return;
|
|
@@ -10433,7 +11295,7 @@ ${style(theme, "dim", "Examples:")}
|
|
|
10433
11295
|
npx aislop scan --exclude node_modules --exclude dist --exclude **/*.ts
|
|
10434
11296
|
${renderHintLine("Run npx aislop scan to scan your project").trimEnd()}
|
|
10435
11297
|
`);
|
|
10436
|
-
program.command("scan [directory]").description("Run full code quality scan").option("--changes", "only scan changed files").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude",
|
|
11298
|
+
program.command("scan [directory]").description("Run full code quality scan").option("--changes", "only scan changed files").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude", commaSeparatedParser, []).option("--include <patterns>", "comma-separated or repeatable list of paths and files to include", commaSeparatedParser, []).action(async (directory = ".", _flags, command) => {
|
|
10437
11299
|
await runScan(directory, command.optsWithGlobals());
|
|
10438
11300
|
});
|
|
10439
11301
|
const FIX_AGENT_FLAGS = [
|
|
@@ -10522,43 +11384,70 @@ fixProgram.action(async (directory = ".", _flags, command) => {
|
|
|
10522
11384
|
agent: matchFixAgent(flags)
|
|
10523
11385
|
});
|
|
10524
11386
|
});
|
|
10525
|
-
program.command("init [directory]").description("Initialize aislop config in project").action(async (directory = ".") => {
|
|
10526
|
-
|
|
11387
|
+
program.command("init [directory]").description("Initialize aislop config in project").option("--strict", "write an enterprise-grade default config: all engines, typecheck on, CI failBelow 85, workflow included").action(async (directory = ".", _flags, command) => {
|
|
11388
|
+
const flags = command.optsWithGlobals();
|
|
11389
|
+
await withCommandLifecycle({
|
|
11390
|
+
command: "init",
|
|
11391
|
+
config: loadConfig(directory).telemetry
|
|
11392
|
+
}, async () => {
|
|
11393
|
+
await initCommand(directory, { strict: Boolean(flags.strict) });
|
|
11394
|
+
return { exitCode: 0 };
|
|
11395
|
+
});
|
|
10527
11396
|
});
|
|
10528
11397
|
program.command("doctor [directory]").description("Check installed tools and environment").action(async (directory = ".") => {
|
|
10529
|
-
await
|
|
11398
|
+
await withCommandLifecycle({
|
|
11399
|
+
command: "doctor",
|
|
11400
|
+
config: loadConfig(directory).telemetry
|
|
11401
|
+
}, async () => {
|
|
11402
|
+
await doctorCommand(directory);
|
|
11403
|
+
return { exitCode: 0 };
|
|
11404
|
+
});
|
|
10530
11405
|
});
|
|
10531
11406
|
program.command("ci [directory]").description("CI-friendly JSON output with exit codes").option("--human", "render the human-friendly scan design instead of JSON").action(async (directory = ".", _flags, command) => {
|
|
10532
11407
|
const flags = command.optsWithGlobals();
|
|
10533
11408
|
const { exitCode } = await ciCommand(directory, loadConfig(directory), { human: Boolean(flags.human) });
|
|
10534
11409
|
if (exitCode !== 0) {
|
|
10535
11410
|
await flushTelemetry();
|
|
10536
|
-
process.
|
|
11411
|
+
process.exitCode = exitCode;
|
|
10537
11412
|
}
|
|
10538
11413
|
});
|
|
10539
11414
|
program.command("rules [directory]").description("List all available rules").action(async (directory = ".") => {
|
|
10540
|
-
await
|
|
11415
|
+
await withCommandLifecycle({
|
|
11416
|
+
command: "rules",
|
|
11417
|
+
config: loadConfig(directory).telemetry
|
|
11418
|
+
}, async () => {
|
|
11419
|
+
await rulesCommand(directory);
|
|
11420
|
+
return { exitCode: 0 };
|
|
11421
|
+
});
|
|
10541
11422
|
});
|
|
10542
11423
|
program.command("badge [directory]").description("Print the public score badge URL + README markdown for this repo").option("--owner <owner>", "GitHub owner (auto-detected from git remote if omitted)").option("--repo <repo>", "GitHub repo name (auto-detected from git remote if omitted)").option("--json", "emit machine-readable JSON instead of the rendered output").action(async (directory = ".", _flags, command) => {
|
|
10543
11424
|
const flags = command.optsWithGlobals();
|
|
10544
11425
|
try {
|
|
10545
|
-
await
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
|
|
10549
|
-
|
|
11426
|
+
await withCommandLifecycle({
|
|
11427
|
+
command: "badge",
|
|
11428
|
+
config: loadConfig(directory).telemetry
|
|
11429
|
+
}, async () => {
|
|
11430
|
+
await badgeCommand({
|
|
11431
|
+
directory,
|
|
11432
|
+
owner: flags.owner,
|
|
11433
|
+
repo: flags.repo,
|
|
11434
|
+
json: Boolean(flags.json)
|
|
11435
|
+
});
|
|
11436
|
+
return { exitCode: 0 };
|
|
10550
11437
|
});
|
|
10551
11438
|
} catch (err) {
|
|
10552
|
-
|
|
11439
|
+
const message = err instanceof Error ? err.message : "Failed to print badge";
|
|
11440
|
+
process.stderr.write(`${message}\n`);
|
|
10553
11441
|
process.exit(1);
|
|
10554
11442
|
}
|
|
10555
11443
|
});
|
|
10556
11444
|
registerHookCommand(program);
|
|
10557
11445
|
const main = async () => {
|
|
11446
|
+
fireInstalledOnce();
|
|
10558
11447
|
await program.parseAsync();
|
|
10559
11448
|
await flushTelemetry();
|
|
10560
11449
|
};
|
|
10561
11450
|
main();
|
|
10562
11451
|
|
|
10563
11452
|
//#endregion
|
|
10564
|
-
export {
|
|
11453
|
+
export { runSubprocess as n, APP_VERSION as r, ENGINE_INFO as t };
|