aislop 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,3880 @@
1
+ import { n as getEngineLabel, r as APP_VERSION, t as ENGINE_INFO } from "./engine-info-DBG3uXLc.js";
2
+ import { n as runSubprocess, t as isToolInstalled } from "./subprocess-99puEEGl.js";
3
+ import { createRequire } from "node:module";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import pc from "picocolors";
7
+ import { spawn, spawnSync } from "node:child_process";
8
+ import { fileURLToPath } from "node:url";
9
+ import { performance } from "node:perf_hooks";
10
+ import os from "node:os";
11
+ import YAML from "yaml";
12
+ import { z } from "zod/v4";
13
+ import ora from "ora";
14
+
15
+ //#region src/utils/highlighter.ts
16
+ const highlighter = {
17
+ error: pc.red,
18
+ warn: pc.yellow,
19
+ info: pc.cyan,
20
+ success: pc.green,
21
+ dim: pc.dim,
22
+ bold: pc.bold
23
+ };
24
+
25
+ //#endregion
26
+ //#region src/utils/logger.ts
27
+ const logger = {
28
+ error(...args) {
29
+ console.log(highlighter.error(args.join(" ")));
30
+ },
31
+ warn(...args) {
32
+ console.log(highlighter.warn(args.join(" ")));
33
+ },
34
+ info(...args) {
35
+ console.log(highlighter.info(args.join(" ")));
36
+ },
37
+ success(...args) {
38
+ console.log(highlighter.success(args.join(" ")));
39
+ },
40
+ dim(...args) {
41
+ console.log(highlighter.dim(args.join(" ")));
42
+ },
43
+ log(...args) {
44
+ console.log(args.join(" "));
45
+ },
46
+ break() {
47
+ console.log("");
48
+ }
49
+ };
50
+
51
+ //#endregion
52
+ //#region src/output/layout.ts
53
+ const formatElapsed$1 = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
54
+ const printCommandHeader = (commandName) => {
55
+ logger.log(highlighter.bold(`aislop ${commandName}`));
56
+ logger.log(highlighter.dim(`v${APP_VERSION}`));
57
+ logger.break();
58
+ };
59
+ const formatProjectSummary = (project) => `Project ${highlighter.info(project.projectName)} (${highlighter.info(project.languages.join(", "))})`;
60
+ const printProjectMetadata = (project) => {
61
+ logger.log(` Source files: ${highlighter.info(String(project.sourceFileCount))}`);
62
+ const frameworks = project.frameworks.filter((framework) => framework !== "none");
63
+ if (frameworks.length > 0) logger.log(` Frameworks: ${highlighter.info(frameworks.join(", "))}`);
64
+ logger.break();
65
+ };
66
+
67
+ //#endregion
68
+ //#region src/utils/source-files.ts
69
+ const MAX_BUFFER$1 = 50 * 1024 * 1024;
70
+ const SOURCE_EXTENSIONS = new Set([
71
+ ".ts",
72
+ ".tsx",
73
+ ".js",
74
+ ".jsx",
75
+ ".mjs",
76
+ ".cjs",
77
+ ".py",
78
+ ".go",
79
+ ".rs",
80
+ ".rb",
81
+ ".java",
82
+ ".php"
83
+ ]);
84
+ const EXCLUDED_DIRS = [
85
+ "node_modules",
86
+ "dist",
87
+ "build",
88
+ ".git",
89
+ "vendor",
90
+ "tests",
91
+ "test",
92
+ "__tests__",
93
+ "__test__",
94
+ "spec",
95
+ "__mocks__",
96
+ "fixtures",
97
+ "test_data",
98
+ ".next",
99
+ ".nuxt",
100
+ "coverage",
101
+ ".turbo"
102
+ ];
103
+ const FIND_PRUNE_DIRS = [
104
+ "node_modules",
105
+ "dist",
106
+ "build",
107
+ ".git",
108
+ "vendor",
109
+ ".next",
110
+ ".nuxt",
111
+ "coverage",
112
+ ".turbo"
113
+ ];
114
+ const TEST_FILE_PATTERNS = [
115
+ /(?:^|\/).*\.test\.[^/]+$/i,
116
+ /(?:^|\/).*\.spec\.[^/]+$/i,
117
+ /(?:^|\/)test_[^/]+\.(?:py|rb|php|js|jsx|ts|tsx|java)$/i,
118
+ /(?:^|\/)[^/]+_test\.(?:py|go|rb|php|js|jsx|ts|tsx|java)$/i
119
+ ];
120
+ const AUTO_GENERATED_PATTERNS = [
121
+ /auto-generated/i,
122
+ /@generated/i,
123
+ /DO NOT (?:EDIT|MODIFY)/i,
124
+ /this file (?:is|was) (?:auto-?)?generated/i,
125
+ /automatically generated/i,
126
+ /generated by/i
127
+ ];
128
+ const toProjectPath = (rootDirectory, filePath) => {
129
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(rootDirectory, filePath);
130
+ return path.relative(rootDirectory, absolutePath).split(path.sep).join("/");
131
+ };
132
+ const isWithinProject = (relativePath) => relativePath.length > 0 && !relativePath.startsWith("..");
133
+ const hasAllowedExtension = (filePath, extraExtensions) => {
134
+ const extension = path.extname(filePath);
135
+ return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
136
+ };
137
+ const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
138
+ const isTestFile = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
139
+ const getIgnoredPaths = (rootDirectory, files) => {
140
+ if (files.length === 0) return /* @__PURE__ */ new Set();
141
+ const result = spawnSync("git", [
142
+ "check-ignore",
143
+ "--no-index",
144
+ "--stdin"
145
+ ], {
146
+ cwd: rootDirectory,
147
+ encoding: "utf-8",
148
+ input: files.join("\n"),
149
+ maxBuffer: MAX_BUFFER$1
150
+ });
151
+ if (result.error || result.status !== 0 && result.status !== 1) return /* @__PURE__ */ new Set();
152
+ return new Set(result.stdout.split("\n").map((file) => file.trim()).filter((file) => file.length > 0));
153
+ };
154
+ const listProjectFiles = (rootDirectory) => {
155
+ const result = spawnSync("git", [
156
+ "ls-files",
157
+ "--cached",
158
+ "--others",
159
+ "--exclude-standard"
160
+ ], {
161
+ cwd: rootDirectory,
162
+ encoding: "utf-8",
163
+ maxBuffer: MAX_BUFFER$1
164
+ });
165
+ if (!result.error && result.status === 0) return result.stdout.split("\n").filter((file) => file.length > 0);
166
+ const findResult = spawnSync("find", [
167
+ ".",
168
+ "(",
169
+ ...FIND_PRUNE_DIRS.flatMap((dir, index) => index === 0 ? ["-name", dir] : [
170
+ "-o",
171
+ "-name",
172
+ dir
173
+ ]),
174
+ ")",
175
+ "-prune",
176
+ "-o",
177
+ "-type",
178
+ "f",
179
+ "-print"
180
+ ], {
181
+ cwd: rootDirectory,
182
+ encoding: "utf-8",
183
+ maxBuffer: MAX_BUFFER$1
184
+ });
185
+ if (findResult.error || findResult.status !== 0) return [];
186
+ return findResult.stdout.split("\n").filter((file) => file.length > 0).map((file) => file.replace(/^\.\//, ""));
187
+ };
188
+ const filterProjectFiles = (rootDirectory, files, extraExtensions = []) => {
189
+ const extraSet = new Set(extraExtensions);
190
+ const normalizedFiles = files.map((file) => {
191
+ const absolutePath = path.isAbsolute(file) ? file : path.resolve(rootDirectory, file);
192
+ return {
193
+ absolutePath,
194
+ relativePath: toProjectPath(rootDirectory, absolutePath)
195
+ };
196
+ }).filter(({ relativePath }) => isWithinProject(relativePath));
197
+ const ignoredPaths = getIgnoredPaths(rootDirectory, normalizedFiles.map(({ relativePath }) => relativePath));
198
+ return normalizedFiles.filter(({ relativePath }) => hasAllowedExtension(relativePath, extraSet) && !isExcludedPath(relativePath) && !isTestFile(relativePath) && !ignoredPaths.has(relativePath)).map(({ absolutePath }) => absolutePath);
199
+ };
200
+ const filterExplicitFiles = (rootDirectory, files, extraExtensions = []) => {
201
+ const extraSet = new Set(extraExtensions);
202
+ return files.map((file) => {
203
+ const absolutePath = path.isAbsolute(file) ? file : path.resolve(rootDirectory, file);
204
+ return {
205
+ absolutePath,
206
+ relativePath: toProjectPath(rootDirectory, absolutePath)
207
+ };
208
+ }).filter(({ relativePath }) => isWithinProject(relativePath) && hasAllowedExtension(relativePath, extraSet)).map(({ absolutePath }) => absolutePath);
209
+ };
210
+ const isAutoGenerated = (filePath) => {
211
+ try {
212
+ const fd = fs.openSync(filePath, "r");
213
+ const buf = Buffer.alloc(512);
214
+ const bytesRead = fs.readSync(fd, buf, 0, 512, 0);
215
+ fs.closeSync(fd);
216
+ const header = buf.toString("utf-8", 0, bytesRead);
217
+ return AUTO_GENERATED_PATTERNS.some((pattern) => pattern.test(header));
218
+ } catch {
219
+ return false;
220
+ }
221
+ };
222
+ const getSourceFilesForRoot = (rootDirectory) => filterProjectFiles(rootDirectory, listProjectFiles(rootDirectory));
223
+ const getSourceFiles = (context) => {
224
+ if (context.files) return filterExplicitFiles(context.rootDirectory, context.files);
225
+ return getSourceFilesForRoot(context.rootDirectory);
226
+ };
227
+ const getSourceFilesWithExtras = (context, extraExtensions) => {
228
+ if (context.files) return filterExplicitFiles(context.rootDirectory, context.files, extraExtensions);
229
+ return filterProjectFiles(context.rootDirectory, listProjectFiles(context.rootDirectory), extraExtensions);
230
+ };
231
+
232
+ //#endregion
233
+ //#region src/utils/tooling.ts
234
+ const THIS_FILE = fileURLToPath(import.meta.url);
235
+ const esmRequire$2 = createRequire(import.meta.url);
236
+ const resolvePackageRoot = (startFile) => {
237
+ let current = path.dirname(startFile);
238
+ while (true) {
239
+ const packageJsonPath = path.join(current, "package.json");
240
+ if (fs.existsSync(packageJsonPath)) try {
241
+ if (JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")).name === "aislop") return current;
242
+ } catch {}
243
+ const parent = path.dirname(current);
244
+ if (parent === current) break;
245
+ current = parent;
246
+ }
247
+ return path.resolve(path.dirname(startFile), "..", "..");
248
+ };
249
+ const PACKAGE_ROOT = resolvePackageRoot(THIS_FILE);
250
+ const TOOLS_BIN_DIR = path.join(PACKAGE_ROOT, "tools", "bin");
251
+ const BUNDLED_TOOL_NAMES = new Set(["ruff", "golangci-lint"]);
252
+ const withExecutableExtension = (toolName) => process.platform === "win32" ? `${toolName}.exe` : toolName;
253
+ const getBundledToolPath = (toolName) => {
254
+ if (!BUNDLED_TOOL_NAMES.has(toolName)) return null;
255
+ const candidate = path.join(TOOLS_BIN_DIR, withExecutableExtension(toolName));
256
+ return fs.existsSync(candidate) ? candidate : null;
257
+ };
258
+ const resolveToolBinary = (toolName) => getBundledToolPath(toolName) ?? toolName;
259
+ const isBundledTool = (toolName) => getBundledToolPath(toolName) !== null;
260
+ const isToolAvailable = async (toolName) => {
261
+ if (isBundledTool(toolName)) return true;
262
+ return isToolInstalled(toolName);
263
+ };
264
+ const isNodePackageAvailable = (packageName) => {
265
+ try {
266
+ esmRequire$2.resolve(`${packageName}/package.json`);
267
+ return true;
268
+ } catch {
269
+ return false;
270
+ }
271
+ };
272
+
273
+ //#endregion
274
+ //#region src/utils/discover.ts
275
+ const LANGUAGE_SIGNALS = {
276
+ "tsconfig.json": "typescript",
277
+ "go.mod": "go",
278
+ "Cargo.toml": "rust",
279
+ Gemfile: "ruby",
280
+ "composer.json": "php"
281
+ };
282
+ const PYTHON_SIGNALS = [
283
+ "requirements.txt",
284
+ "pyproject.toml",
285
+ "setup.py",
286
+ "setup.cfg",
287
+ "Pipfile",
288
+ "poetry.lock"
289
+ ];
290
+ const JAVA_SIGNALS = [
291
+ "pom.xml",
292
+ "build.gradle",
293
+ "build.gradle.kts"
294
+ ];
295
+ const FRAMEWORK_PACKAGES = {
296
+ next: "nextjs",
297
+ react: "react",
298
+ vite: "vite",
299
+ "@remix-run/react": "remix",
300
+ expo: "expo"
301
+ };
302
+ const PYTHON_FRAMEWORKS = {
303
+ django: "django",
304
+ flask: "flask",
305
+ fastapi: "fastapi"
306
+ };
307
+ const NEXT_CONFIG_FILENAMES = [
308
+ "next.config.js",
309
+ "next.config.mjs",
310
+ "next.config.ts",
311
+ "next.config.cjs"
312
+ ];
313
+ const readPackageJson = (filePath) => {
314
+ try {
315
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
316
+ } catch {
317
+ return null;
318
+ }
319
+ };
320
+ const countSourceFiles = (rootDirectory) => getSourceFilesForRoot(rootDirectory).length;
321
+ const detectLanguages = (directory) => {
322
+ const languages = /* @__PURE__ */ new Set();
323
+ for (const [file, lang] of Object.entries(LANGUAGE_SIGNALS)) if (fs.existsSync(path.join(directory, file))) languages.add(lang);
324
+ if (readPackageJson(path.join(directory, "package.json"))) if (fs.existsSync(path.join(directory, "tsconfig.json"))) languages.add("typescript");
325
+ else languages.add("javascript");
326
+ for (const signal of PYTHON_SIGNALS) if (fs.existsSync(path.join(directory, signal))) {
327
+ languages.add("python");
328
+ break;
329
+ }
330
+ for (const signal of JAVA_SIGNALS) if (fs.existsSync(path.join(directory, signal))) {
331
+ languages.add("java");
332
+ break;
333
+ }
334
+ return [...languages];
335
+ };
336
+ const detectFrameworks = (directory) => {
337
+ const frameworks = /* @__PURE__ */ new Set();
338
+ const packageJson = readPackageJson(path.join(directory, "package.json"));
339
+ if (packageJson) {
340
+ const allDeps = {
341
+ ...packageJson.dependencies,
342
+ ...packageJson.devDependencies
343
+ };
344
+ for (const [pkg, fw] of Object.entries(FRAMEWORK_PACKAGES)) if (allDeps[pkg]) frameworks.add(fw);
345
+ }
346
+ for (const configFile of NEXT_CONFIG_FILENAMES) if (fs.existsSync(path.join(directory, configFile))) {
347
+ frameworks.add("nextjs");
348
+ break;
349
+ }
350
+ const requirementsPath = path.join(directory, "requirements.txt");
351
+ if (fs.existsSync(requirementsPath)) try {
352
+ const content = fs.readFileSync(requirementsPath, "utf-8").toLowerCase();
353
+ for (const [pkg, fw] of Object.entries(PYTHON_FRAMEWORKS)) if (content.includes(pkg)) frameworks.add(fw);
354
+ } catch {}
355
+ if (frameworks.size === 0) frameworks.add("none");
356
+ return [...frameworks];
357
+ };
358
+ const TOOLS_TO_CHECK = [
359
+ "oxlint",
360
+ "biome",
361
+ "ruff",
362
+ "golangci-lint",
363
+ "npm",
364
+ "pnpm",
365
+ "govulncheck",
366
+ "gofmt",
367
+ "pip-audit",
368
+ "cargo",
369
+ "clippy-driver",
370
+ "rustfmt",
371
+ "rubocop",
372
+ "phpcs",
373
+ "php-cs-fixer"
374
+ ];
375
+ const checkInstalledTools = async () => {
376
+ const results = {};
377
+ await Promise.all(TOOLS_TO_CHECK.map(async (tool) => {
378
+ results[tool] = await isToolAvailable(tool);
379
+ }));
380
+ return results;
381
+ };
382
+ const discoverProject = async (directory) => {
383
+ const resolvedDir = path.resolve(directory);
384
+ const languages = detectLanguages(resolvedDir);
385
+ const frameworks = detectFrameworks(resolvedDir);
386
+ const sourceFileCount = countSourceFiles(resolvedDir);
387
+ const installedTools = await checkInstalledTools();
388
+ return {
389
+ rootDirectory: resolvedDir,
390
+ projectName: readPackageJson(path.join(resolvedDir, "package.json"))?.name ?? path.basename(resolvedDir),
391
+ languages,
392
+ frameworks,
393
+ sourceFileCount,
394
+ installedTools
395
+ };
396
+ };
397
+
398
+ //#endregion
399
+ //#region src/commands/doctor.ts
400
+ const LANGUAGE_TOOLS = {
401
+ typescript: [{
402
+ name: "oxlint",
403
+ purpose: "Lint (JS/TS)"
404
+ }, {
405
+ name: "biome",
406
+ purpose: "Format (JS/TS)"
407
+ }],
408
+ javascript: [{
409
+ name: "oxlint",
410
+ purpose: "Lint (JS)"
411
+ }, {
412
+ name: "biome",
413
+ purpose: "Format (JS)"
414
+ }],
415
+ python: [{
416
+ name: "ruff",
417
+ purpose: "Lint + Format (Python)"
418
+ }, {
419
+ name: "pip-audit",
420
+ purpose: "Dependency vulnerability scan (Python)"
421
+ }],
422
+ go: [
423
+ {
424
+ name: "golangci-lint",
425
+ purpose: "Lint (Go)"
426
+ },
427
+ {
428
+ name: "gofmt",
429
+ purpose: "Format (Go)"
430
+ },
431
+ {
432
+ name: "govulncheck",
433
+ purpose: "Dependency vulnerability scan (Go)"
434
+ }
435
+ ],
436
+ rust: [{
437
+ name: "cargo",
438
+ purpose: "Lint + Format (Rust)"
439
+ }],
440
+ java: [],
441
+ ruby: [{
442
+ name: "rubocop",
443
+ purpose: "Lint + Format (Ruby)"
444
+ }],
445
+ php: [{
446
+ name: "phpcs",
447
+ purpose: "Lint (PHP)"
448
+ }, {
449
+ name: "php-cs-fixer",
450
+ purpose: "Format (PHP)"
451
+ }]
452
+ };
453
+ const printProjectDetails = (projectInfo) => {
454
+ logger.success(` ✓ ${formatProjectSummary(projectInfo)}`);
455
+ printProjectMetadata(projectInfo);
456
+ logger.log(" Checks");
457
+ logger.break();
458
+ };
459
+ const createToolReporter = () => {
460
+ let allGood = true;
461
+ const seenTools = /* @__PURE__ */ new Set();
462
+ const reportTool = (name, purpose, options = { installed: false }) => {
463
+ if (seenTools.has(name)) return;
464
+ seenTools.add(name);
465
+ if (options.installed) {
466
+ const sourceLabel = options.bundled ? " (bundled)" : "";
467
+ logger.success(` ✓ ${name}${sourceLabel} — ${purpose}`);
468
+ return;
469
+ }
470
+ logger.warn(` ✗ ${name} — ${purpose} (not installed)`);
471
+ allGood = false;
472
+ };
473
+ return {
474
+ reportTool,
475
+ isAllGood: () => allGood
476
+ };
477
+ };
478
+ const reportBundledTools = () => {
479
+ logger.success(" ✓ oxlint (bundled)");
480
+ logger.success(" ✓ biome (bundled)");
481
+ logger.success(" ✓ knip (bundled)");
482
+ };
483
+ const reportLanguageTools = (projectInfo, reportTool) => {
484
+ for (const lang of projectInfo.languages) for (const tool of LANGUAGE_TOOLS[lang] ?? []) {
485
+ if (tool.name === "oxlint" || tool.name === "biome") continue;
486
+ reportTool(tool.name, tool.purpose, {
487
+ installed: projectInfo.installedTools[tool.name] === true,
488
+ bundled: isBundledTool(tool.name)
489
+ });
490
+ }
491
+ };
492
+ const reportJsAuditTool = (resolvedDir, projectInfo, reportTool) => {
493
+ if (!(projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript"))) return;
494
+ const hasPnpmLock = fs.existsSync(path.join(resolvedDir, "pnpm-lock.yaml"));
495
+ const hasNpmLock = fs.existsSync(path.join(resolvedDir, "package-lock.json"));
496
+ if (hasPnpmLock) {
497
+ reportTool("pnpm", "Dependency vulnerability scan (JS/TS via pnpm audit)", { installed: projectInfo.installedTools["pnpm"] === true });
498
+ return;
499
+ }
500
+ if (hasNpmLock || fs.existsSync(path.join(resolvedDir, "package.json"))) reportTool("npm", "Dependency vulnerability scan (JS/TS via npm audit)", { installed: projectInfo.installedTools["npm"] === true });
501
+ };
502
+ const reportFrameworkTools = (projectInfo, reportTool) => {
503
+ if (!projectInfo.frameworks.includes("expo")) return;
504
+ const hasExpoDoctor = isNodePackageAvailable("expo-doctor");
505
+ reportTool("expo-doctor", "Expo project health checks", {
506
+ installed: hasExpoDoctor,
507
+ bundled: hasExpoDoctor
508
+ });
509
+ };
510
+ const printDoctorConclusion = (allGood) => {
511
+ logger.break();
512
+ if (allGood) logger.success(" All tools are available. You're good to go!");
513
+ else {
514
+ logger.warn(" Some tools are missing. Install them for full coverage.");
515
+ logger.dim(" Missing tools will be skipped during scans.");
516
+ }
517
+ logger.break();
518
+ };
519
+ const doctorCommand = async (directory) => {
520
+ const resolvedDir = path.resolve(directory);
521
+ printCommandHeader("Doctor");
522
+ const projectInfo = await discoverProject(resolvedDir);
523
+ printProjectDetails(projectInfo);
524
+ const { reportTool, isAllGood } = createToolReporter();
525
+ reportBundledTools();
526
+ reportLanguageTools(projectInfo, reportTool);
527
+ reportJsAuditTool(resolvedDir, projectInfo, reportTool);
528
+ reportFrameworkTools(projectInfo, reportTool);
529
+ printDoctorConclusion(isAllGood());
530
+ };
531
+
532
+ //#endregion
533
+ //#region src/engines/format/biome.ts
534
+ const esmRequire$1 = createRequire(import.meta.url);
535
+ const resolveLocalBiomeScript = () => {
536
+ try {
537
+ const packageJsonPath = esmRequire$1.resolve("@biomejs/biome/package.json");
538
+ return path.join(path.dirname(packageJsonPath), "bin", "biome");
539
+ } catch {
540
+ return null;
541
+ }
542
+ };
543
+ const runBiome = async (args, rootDirectory, timeout) => {
544
+ const localScript = resolveLocalBiomeScript();
545
+ if (localScript) return runSubprocess(process.execPath, [localScript, ...args], {
546
+ cwd: rootDirectory,
547
+ timeout
548
+ });
549
+ return runSubprocess("biome", args, {
550
+ cwd: rootDirectory,
551
+ timeout
552
+ });
553
+ };
554
+ const BIOME_EXTENSIONS = new Set([
555
+ ".js",
556
+ ".jsx",
557
+ ".ts",
558
+ ".tsx",
559
+ ".mjs",
560
+ ".cjs"
561
+ ]);
562
+ const getBiomeTargets = (context) => getSourceFiles(context).filter((filePath) => BIOME_EXTENSIONS.has(path.extname(filePath))).map((filePath) => path.relative(context.rootDirectory, filePath));
563
+ const projectUsesDecorators = (rootDir) => {
564
+ try {
565
+ const tsconfigPath = path.join(rootDir, "tsconfig.json");
566
+ if (!fs.existsSync(tsconfigPath)) return false;
567
+ const content = fs.readFileSync(tsconfigPath, "utf-8");
568
+ return /experimentalDecorators.*true/i.test(content);
569
+ } catch {
570
+ return false;
571
+ }
572
+ };
573
+ const runBiomeFormat = async (context) => {
574
+ const targets = getBiomeTargets(context);
575
+ if (targets.length === 0) return [];
576
+ const args = [
577
+ "format",
578
+ "--reporter=json",
579
+ ...targets
580
+ ];
581
+ try {
582
+ const result = await runBiome(args, context.rootDirectory, 6e4);
583
+ const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
584
+ if (!output) return [];
585
+ let diagnostics = parseBiomeJsonOutput(output, context.rootDirectory);
586
+ if (projectUsesDecorators(context.rootDirectory)) diagnostics = diagnostics.filter((d) => {
587
+ const msg = d.message.toLowerCase();
588
+ return !msg.includes("decorator") && !msg.includes("parsing error");
589
+ });
590
+ return diagnostics;
591
+ } catch {
592
+ return [];
593
+ }
594
+ };
595
+ const parseBiomeJsonOutput = (output, rootDir) => {
596
+ const diagnostics = [];
597
+ for (const line of output.split("\n")) {
598
+ const trimmed = line.trim();
599
+ if (!trimmed.startsWith("{")) continue;
600
+ let parsed = null;
601
+ try {
602
+ parsed = JSON.parse(trimmed);
603
+ } catch {
604
+ parsed = null;
605
+ }
606
+ if (!parsed || !Array.isArray(parsed.diagnostics)) continue;
607
+ for (const entry of parsed.diagnostics) {
608
+ const rawPath = entry.location?.path;
609
+ if (!rawPath) continue;
610
+ const severity = entry.severity === "error" ? "error" : "warning";
611
+ diagnostics.push({
612
+ filePath: path.isAbsolute(rawPath) ? path.relative(rootDir, rawPath) : rawPath,
613
+ engine: "format",
614
+ rule: "formatting",
615
+ severity,
616
+ message: entry.message ?? "File is not formatted correctly",
617
+ help: "Run `aislop fix` to auto-format",
618
+ line: entry.location?.start?.line ?? 0,
619
+ column: entry.location?.start?.column ?? 0,
620
+ category: "Format",
621
+ fixable: true
622
+ });
623
+ }
624
+ }
625
+ return diagnostics;
626
+ };
627
+ const fixBiomeFormat = async (context) => {
628
+ const targets = getBiomeTargets(context);
629
+ if (targets.length === 0) return;
630
+ const result = await runBiome([
631
+ "check",
632
+ "--write",
633
+ "--formatter-enabled=true",
634
+ "--linter-enabled=false",
635
+ ...targets
636
+ ], context.rootDirectory, 6e4);
637
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `Biome exited with code ${result.exitCode}`);
638
+ };
639
+
640
+ //#endregion
641
+ //#region src/engines/format/gofmt.ts
642
+ const runGofmt = async (context) => {
643
+ try {
644
+ const result = await runSubprocess("gofmt", ["-l", context.rootDirectory], {
645
+ cwd: context.rootDirectory,
646
+ timeout: 6e4
647
+ });
648
+ if (!result.stdout) return [];
649
+ return result.stdout.split("\n").filter((f) => f.length > 0).map((file) => ({
650
+ filePath: path.relative(context.rootDirectory, file),
651
+ engine: "format",
652
+ rule: "go-formatting",
653
+ severity: "warning",
654
+ message: "Go file is not formatted correctly",
655
+ help: "Run `aislop fix` to auto-format with gofmt",
656
+ line: 0,
657
+ column: 0,
658
+ category: "Format",
659
+ fixable: true
660
+ }));
661
+ } catch {
662
+ return [];
663
+ }
664
+ };
665
+ const fixGofmt = async (rootDirectory) => {
666
+ const result = await runSubprocess("gofmt", ["-w", rootDirectory], {
667
+ cwd: rootDirectory,
668
+ timeout: 6e4
669
+ });
670
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `gofmt exited with code ${result.exitCode}`);
671
+ };
672
+
673
+ //#endregion
674
+ //#region src/engines/format/ruff-format.ts
675
+ const runRuffFormat = async (context) => {
676
+ const ruffBinary = resolveToolBinary("ruff");
677
+ try {
678
+ const result = await runSubprocess(ruffBinary, [
679
+ "format",
680
+ "--check",
681
+ "--diff",
682
+ context.rootDirectory
683
+ ], {
684
+ cwd: context.rootDirectory,
685
+ timeout: 6e4
686
+ });
687
+ if (result.exitCode === 0) return [];
688
+ return parseRuffFormatOutput(result.stdout || result.stderr, context.rootDirectory);
689
+ } catch {
690
+ return [];
691
+ }
692
+ };
693
+ const parseRuffFormatOutput = (output, rootDir) => {
694
+ const diagnostics = [];
695
+ const filePattern = /^--- (.+)$/gm;
696
+ let match;
697
+ while ((match = filePattern.exec(output)) !== null) {
698
+ const filePath = match[1].replace(/^a\//, "");
699
+ diagnostics.push({
700
+ filePath: path.relative(rootDir, filePath),
701
+ engine: "format",
702
+ rule: "python-formatting",
703
+ severity: "warning",
704
+ message: "Python file is not formatted correctly",
705
+ help: "Run `aislop fix` to auto-format with ruff",
706
+ line: 0,
707
+ column: 0,
708
+ category: "Format",
709
+ fixable: true
710
+ });
711
+ }
712
+ return diagnostics;
713
+ };
714
+ const fixRuffFormat = async (rootDirectory) => {
715
+ const result = await runSubprocess(resolveToolBinary("ruff"), ["format", rootDirectory], {
716
+ cwd: rootDirectory,
717
+ timeout: 6e4
718
+ });
719
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `ruff format exited with code ${result.exitCode}`);
720
+ };
721
+
722
+ //#endregion
723
+ //#region src/engines/lint/oxlint-config.ts
724
+ const createOxlintConfig = (options) => {
725
+ const plugins = [
726
+ "import",
727
+ "unicorn",
728
+ "typescript"
729
+ ];
730
+ const rules = {
731
+ "no-unused-vars": "warn",
732
+ "no-undef": "error",
733
+ "no-constant-condition": "warn",
734
+ "no-debugger": "warn",
735
+ "no-empty": "warn",
736
+ "no-extra-boolean-cast": "warn",
737
+ "no-irregular-whitespace": "warn",
738
+ "no-loss-of-precision": "error",
739
+ "import/no-duplicates": "warn",
740
+ "unicorn/no-unnecessary-await": "warn"
741
+ };
742
+ if (options.framework === "react" || options.framework === "nextjs" || options.framework === "vite" || options.framework === "remix") {
743
+ plugins.push("react", "react-hooks", "jsx-a11y");
744
+ Object.assign(rules, {
745
+ "react/no-direct-mutation-state": "error",
746
+ "react-hooks/rules-of-hooks": "error",
747
+ "react-hooks/exhaustive-deps": "warn"
748
+ });
749
+ }
750
+ if (options.framework === "nextjs") plugins.push("nextjs");
751
+ const env = {
752
+ browser: true,
753
+ node: true,
754
+ es2022: true
755
+ };
756
+ const globals = {};
757
+ if (options.testFramework === "jest") {
758
+ globals.jest = "readonly";
759
+ globals.describe = "readonly";
760
+ globals.it = "readonly";
761
+ globals.expect = "readonly";
762
+ globals.test = "readonly";
763
+ globals.beforeAll = "readonly";
764
+ globals.afterAll = "readonly";
765
+ globals.beforeEach = "readonly";
766
+ globals.afterEach = "readonly";
767
+ } else if (options.testFramework === "vitest") {
768
+ globals.describe = "readonly";
769
+ globals.it = "readonly";
770
+ globals.expect = "readonly";
771
+ globals.test = "readonly";
772
+ globals.beforeAll = "readonly";
773
+ globals.afterAll = "readonly";
774
+ globals.beforeEach = "readonly";
775
+ globals.afterEach = "readonly";
776
+ globals.vi = "readonly";
777
+ } else if (options.testFramework === "mocha") {
778
+ globals.describe = "readonly";
779
+ globals.it = "readonly";
780
+ globals.before = "readonly";
781
+ globals.after = "readonly";
782
+ globals.beforeEach = "readonly";
783
+ globals.afterEach = "readonly";
784
+ }
785
+ if (options.framework === "expo" || options.framework === "react") globals.__DEV__ = "readonly";
786
+ return {
787
+ plugins,
788
+ rules,
789
+ env,
790
+ globals,
791
+ settings: {}
792
+ };
793
+ };
794
+
795
+ //#endregion
796
+ //#region src/engines/lint/oxlint.ts
797
+ const esmRequire = createRequire(import.meta.url);
798
+ const resolveOxlintBinary = () => {
799
+ try {
800
+ const oxlintMainPath = esmRequire.resolve("oxlint");
801
+ const oxlintDir = path.resolve(path.dirname(oxlintMainPath), "..");
802
+ return path.join(oxlintDir, "bin", "oxlint");
803
+ } catch {
804
+ return "oxlint";
805
+ }
806
+ };
807
+ const parseRuleCode = (code) => {
808
+ const match = code.match(/^(.+)\((.+)\)$/);
809
+ if (!match) return {
810
+ plugin: "unknown",
811
+ rule: code
812
+ };
813
+ return {
814
+ plugin: match[1].replace(/^eslint-plugin-/, ""),
815
+ rule: match[2]
816
+ };
817
+ };
818
+ const detectTestFramework = (rootDir) => {
819
+ try {
820
+ const raw = fs.readFileSync(path.join(rootDir, "package.json"), "utf-8");
821
+ const pkg = JSON.parse(raw);
822
+ const allDeps = {
823
+ ...pkg.dependencies,
824
+ ...pkg.devDependencies
825
+ };
826
+ if (allDeps.vitest) return "vitest";
827
+ if (allDeps.jest || allDeps["ts-jest"] || allDeps["@jest/core"]) return "jest";
828
+ if (allDeps.mocha) return "mocha";
829
+ if (fs.existsSync(path.join(rootDir, "jest.config.js")) || fs.existsSync(path.join(rootDir, "jest.config.ts")) || fs.existsSync(path.join(rootDir, "jest.config.mjs"))) return "jest";
830
+ if (fs.existsSync(path.join(rootDir, "vitest.config.ts")) || fs.existsSync(path.join(rootDir, "vitest.config.js"))) return "vitest";
831
+ if (fs.existsSync(path.join(rootDir, ".mocharc.yml"))) return "mocha";
832
+ } catch {}
833
+ return null;
834
+ };
835
+ const runOxlint = async (context) => {
836
+ const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-${process.pid}.json`);
837
+ const config = createOxlintConfig({
838
+ framework: context.frameworks.find((f) => f !== "none"),
839
+ testFramework: detectTestFramework(context.rootDirectory)
840
+ });
841
+ try {
842
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
843
+ const args = [
844
+ resolveOxlintBinary(),
845
+ "-c",
846
+ configPath,
847
+ "--format",
848
+ "json"
849
+ ];
850
+ if (context.languages.includes("typescript") && fs.existsSync(path.join(context.rootDirectory, "tsconfig.json"))) args.push("--tsconfig", "./tsconfig.json");
851
+ args.push(".");
852
+ const result = await runSubprocess(process.execPath, args, {
853
+ cwd: context.rootDirectory,
854
+ timeout: 12e4
855
+ });
856
+ if (!result.stdout) return [];
857
+ let output;
858
+ try {
859
+ output = JSON.parse(result.stdout);
860
+ } catch {
861
+ return [];
862
+ }
863
+ return output.diagnostics.map((d) => {
864
+ const { plugin, rule } = parseRuleCode(d.code);
865
+ const label = d.labels[0];
866
+ return {
867
+ filePath: d.filename,
868
+ engine: "lint",
869
+ rule: `${plugin}/${rule}`,
870
+ severity: d.severity,
871
+ message: d.message.replace(/\S+\.\w+:\d+:\d+[\s\S]*$/, "").trim() || d.message,
872
+ help: d.help || "",
873
+ line: label?.span.line ?? 0,
874
+ column: label?.span.column ?? 0,
875
+ category: plugin === "react" ? "React" : plugin === "import" ? "Imports" : "Lint",
876
+ fixable: false
877
+ };
878
+ });
879
+ } finally {
880
+ if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
881
+ }
882
+ };
883
+ const fixOxlint = async (context) => {
884
+ const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-fix-${process.pid}.json`);
885
+ const config = createOxlintConfig({
886
+ framework: context.frameworks.find((f) => f !== "none"),
887
+ testFramework: detectTestFramework(context.rootDirectory)
888
+ });
889
+ try {
890
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
891
+ const args = [
892
+ resolveOxlintBinary(),
893
+ "-c",
894
+ configPath,
895
+ "--fix",
896
+ "."
897
+ ];
898
+ const result = await runSubprocess(process.execPath, args, {
899
+ cwd: context.rootDirectory,
900
+ timeout: 12e4
901
+ });
902
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `Oxlint exited with code ${result.exitCode}`);
903
+ } finally {
904
+ if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
905
+ }
906
+ };
907
+
908
+ //#endregion
909
+ //#region src/engines/lint/ruff.ts
910
+ const runRuffLint = async (context) => {
911
+ const ruffBinary = resolveToolBinary("ruff");
912
+ try {
913
+ const output = (await runSubprocess(ruffBinary, [
914
+ "check",
915
+ "--output-format=json",
916
+ context.rootDirectory
917
+ ], {
918
+ cwd: context.rootDirectory,
919
+ timeout: 6e4
920
+ })).stdout;
921
+ if (!output) return [];
922
+ return JSON.parse(output).map((d) => ({
923
+ filePath: path.relative(context.rootDirectory, d.filename),
924
+ engine: "lint",
925
+ rule: `ruff/${d.code}`,
926
+ severity: d.code.startsWith("E") || d.code.startsWith("F") ? "error" : "warning",
927
+ message: d.message,
928
+ help: "",
929
+ line: d.location.row,
930
+ column: d.location.column,
931
+ category: "Python Lint",
932
+ fixable: d.fix?.applicability === "safe"
933
+ }));
934
+ } catch {
935
+ return [];
936
+ }
937
+ };
938
+ const fixRuffLint = async (rootDirectory) => {
939
+ const result = await runSubprocess(resolveToolBinary("ruff"), [
940
+ "check",
941
+ "--fix",
942
+ rootDirectory
943
+ ], {
944
+ cwd: rootDirectory,
945
+ timeout: 6e4
946
+ });
947
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `ruff check --fix exited with code ${result.exitCode}`);
948
+ };
949
+
950
+ //#endregion
951
+ //#region src/output/pager.ts
952
+ const DEFAULT_COLUMNS = 80;
953
+ const DEFAULT_ROWS = 24;
954
+ const ANSI_PATTERN = new RegExp(String.raw`\u001B\[[0-?]*[ -/]*[@-~]`, "g");
955
+ const stripAnsi = (text) => text.replace(ANSI_PATTERN, "");
956
+ const resolvePagerCommand = () => {
957
+ const pager = process.env.PAGER?.trim();
958
+ if (pager) {
959
+ const [command, ...args] = pager.split(/\s+/);
960
+ if (command) return {
961
+ command,
962
+ args
963
+ };
964
+ }
965
+ return {
966
+ command: "less",
967
+ args: [
968
+ "-R",
969
+ "-F",
970
+ "-X"
971
+ ]
972
+ };
973
+ };
974
+ const writeToStdout = (text) => {
975
+ process.stdout.write(text);
976
+ };
977
+ const pipeToPager = async (command, args, text) => new Promise((resolve) => {
978
+ let settled = false;
979
+ const finish = (success) => {
980
+ if (settled) return;
981
+ settled = true;
982
+ resolve(success);
983
+ };
984
+ try {
985
+ const child = spawn(command, args, {
986
+ stdio: [
987
+ "pipe",
988
+ "inherit",
989
+ "inherit"
990
+ ],
991
+ windowsHide: true
992
+ });
993
+ child.once("error", () => finish(false));
994
+ child.once("close", (code) => finish(code === 0));
995
+ child.stdin?.on("error", () => void 0);
996
+ child.stdin?.end(text);
997
+ } catch {
998
+ finish(false);
999
+ }
1000
+ });
1001
+ const countRenderedLines = (text, columns = DEFAULT_COLUMNS) => {
1002
+ const width = Math.max(1, columns);
1003
+ return text.split("\n").reduce((count, line) => {
1004
+ const visibleLine = stripAnsi(line).replaceAll(" ", " ");
1005
+ return count + Math.max(1, Math.ceil(visibleLine.length / width));
1006
+ }, 0);
1007
+ };
1008
+ const shouldPageOutput = (text, options = {}) => {
1009
+ if (text.trim().length === 0) return false;
1010
+ const stdinIsTTY = options.stdinIsTTY ?? Boolean(process.stdin.isTTY);
1011
+ const stdoutIsTTY = options.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
1012
+ if (!stdinIsTTY || !stdoutIsTTY) return false;
1013
+ const rows = Math.max(1, options.rows ?? process.stdout.rows ?? DEFAULT_ROWS);
1014
+ return countRenderedLines(text, Math.max(1, options.columns ?? process.stdout.columns ?? DEFAULT_COLUMNS)) > rows - 1;
1015
+ };
1016
+ const printMaybePaged = async (text) => {
1017
+ if (!shouldPageOutput(text)) {
1018
+ writeToStdout(text);
1019
+ return;
1020
+ }
1021
+ const pager = resolvePagerCommand();
1022
+ if (!await pipeToPager(pager.command, pager.args, text)) writeToStdout(text);
1023
+ };
1024
+
1025
+ //#endregion
1026
+ //#region src/commands/fix.ts
1027
+ const uniqueFiles = (diagnostics) => [...new Set(diagnostics.map((d) => d.filePath))];
1028
+ const uniqueFileCount = (diagnostics) => uniqueFiles(diagnostics).length;
1029
+ const getFilePreviewLines = (title, files, verbose) => {
1030
+ if (files.length === 0) return [];
1031
+ const lines = [highlighter.dim(` ${title}: ${files.length} file(s)`)];
1032
+ const preview = verbose ? files : files.slice(0, 5);
1033
+ for (const file of preview) lines.push(highlighter.dim(` ${file}`));
1034
+ if (!verbose && files.length > preview.length) lines.push(highlighter.dim(` +${files.length - preview.length} more file(s), use -d for full list`));
1035
+ return lines;
1036
+ };
1037
+ const getReasonLines = (reason) => {
1038
+ return {
1039
+ firstLine: reason.split("\n").find((line) => line.trim().length > 0) ?? reason,
1040
+ printable: reason
1041
+ };
1042
+ };
1043
+ const getStepStatusLine = (result, name, elapsedLabel) => {
1044
+ if (result.failed) return highlighter.error(` ✗ ${name}: failed (${result.afterIssues} issue${result.afterIssues === 1 ? "" : "s"} remain, ${elapsedLabel})`);
1045
+ if (result.beforeIssues === 0) return highlighter.success(` ✓ ${name}: done (0 issues, ${elapsedLabel})`);
1046
+ if (result.afterIssues === 0) return highlighter.success(` ✓ ${name}: done (${result.resolvedIssues} resolved across ${result.beforeFiles} file(s), ${elapsedLabel})`);
1047
+ if (result.resolvedIssues > 0) return highlighter.warn(` ! ${name}: done (${result.resolvedIssues} resolved, ${result.afterIssues} remaining, ${elapsedLabel})`);
1048
+ return highlighter.warn(` ! ${name}: done (no auto-fix changes, ${result.afterIssues} issue${result.afterIssues === 1 ? "" : "s"}, ${elapsedLabel})`);
1049
+ };
1050
+ const runFixStep = async (name, detect, applyFix, options) => {
1051
+ const stepStart = performance.now();
1052
+ const before = await detect();
1053
+ let applyError = null;
1054
+ try {
1055
+ await applyFix();
1056
+ } catch (error) {
1057
+ applyError = error;
1058
+ }
1059
+ const after = await detect();
1060
+ const elapsedMs = performance.now() - stepStart;
1061
+ const result = {
1062
+ name,
1063
+ beforeIssues: before.length,
1064
+ afterIssues: after.length,
1065
+ resolvedIssues: Math.max(0, before.length - after.length),
1066
+ beforeFiles: uniqueFileCount(before),
1067
+ failed: applyError !== null && before.length === after.length,
1068
+ elapsedMs
1069
+ };
1070
+ const lines = [getStepStatusLine(result, name, formatElapsed$1(result.elapsedMs))];
1071
+ if (applyError) {
1072
+ const reasonLines = getReasonLines(applyError instanceof Error ? applyError.message : String(applyError));
1073
+ const reasonToPrint = options.verbose ? reasonLines.printable : reasonLines.firstLine;
1074
+ for (const line of reasonToPrint.split("\n")) lines.push(highlighter.dim(` ${line}`));
1075
+ if (!options.verbose && reasonLines.printable !== reasonToPrint) lines.push(highlighter.dim(" Re-run with -d for full tool output."));
1076
+ }
1077
+ lines.push(...getFilePreviewLines("Affected", uniqueFiles(before), options.verbose));
1078
+ if (after.length > 0) lines.push(...getFilePreviewLines("Remaining", uniqueFiles(after), options.verbose));
1079
+ await printMaybePaged(`${lines.join("\n")}\n\n`);
1080
+ return result;
1081
+ };
1082
+ const createEngineContext = (rootDirectory, projectInfo, config) => ({
1083
+ rootDirectory,
1084
+ languages: projectInfo.languages,
1085
+ frameworks: projectInfo.frameworks,
1086
+ installedTools: projectInfo.installedTools,
1087
+ config: {
1088
+ quality: config.quality,
1089
+ security: config.security
1090
+ }
1091
+ });
1092
+ const summarizeFixRun = (steps) => {
1093
+ const totals = steps.reduce((acc, step) => {
1094
+ acc.beforeIssues += step.beforeIssues;
1095
+ acc.afterIssues += step.afterIssues;
1096
+ acc.resolvedIssues += step.resolvedIssues;
1097
+ if (step.failed) acc.failedSteps += 1;
1098
+ return acc;
1099
+ }, {
1100
+ beforeIssues: 0,
1101
+ afterIssues: 0,
1102
+ resolvedIssues: 0,
1103
+ failedSteps: 0
1104
+ });
1105
+ if (totals.failedSteps > 0) {
1106
+ logger.log(` Fix summary: checked ${steps.length} step(s), resolved ${totals.resolvedIssues} issue(s).`);
1107
+ logger.warn(` ${totals.failedSteps} step(s) reported tool errors; unresolved issue count is unknown for failed steps.`);
1108
+ } else logger.log(` Fix summary: checked ${steps.length} step(s), resolved ${totals.resolvedIssues} issue(s), remaining ${totals.afterIssues}.`);
1109
+ if (totals.failedSteps === 0 && totals.beforeIssues > 0 && totals.resolvedIssues === 0) logger.dim(" No auto-fixable changes were applied. Current findings are likely manual-fix categories.");
1110
+ };
1111
+ const fixCommand = async (directory, config, options = {
1112
+ verbose: false,
1113
+ showHeader: true
1114
+ }) => {
1115
+ const resolvedDir = path.resolve(directory);
1116
+ if (options.showHeader !== false) printCommandHeader("Fix");
1117
+ const projectInfo = await discoverProject(resolvedDir);
1118
+ logger.success(` ✓ ${formatProjectSummary(projectInfo)}`);
1119
+ printProjectMetadata(projectInfo);
1120
+ const context = createEngineContext(resolvedDir, projectInfo, config);
1121
+ const steps = [];
1122
+ if (config.engines.format) {
1123
+ if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS formatting", () => runBiomeFormat(context), () => fixBiomeFormat(context), options));
1124
+ if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python formatting", () => runRuffFormat(context), () => fixRuffFormat(resolvedDir), options));
1125
+ else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python formatting fixes.");
1126
+ if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) steps.push(await runFixStep("Go formatting", () => runGofmt(context), () => fixGofmt(resolvedDir), options));
1127
+ else if (projectInfo.languages.includes("go")) logger.warn(" Go detected but gofmt is not installed; skipping Go formatting fixes.");
1128
+ }
1129
+ if (config.engines.lint) {
1130
+ if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS lint fixes", () => runOxlint(context), () => fixOxlint(context), options));
1131
+ if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => fixRuffLint(resolvedDir), options));
1132
+ else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python lint fixes.");
1133
+ }
1134
+ if (steps.length === 0) logger.dim(" No applicable auto-fixers found for this project.");
1135
+ else {
1136
+ logger.break();
1137
+ summarizeFixRun(steps);
1138
+ }
1139
+ logger.break();
1140
+ logger.success(" ✓ Done. Run `aislop scan` to verify.");
1141
+ logger.break();
1142
+ };
1143
+
1144
+ //#endregion
1145
+ //#region src/config/defaults.ts
1146
+ const DEFAULT_CONFIG = {
1147
+ version: 1,
1148
+ engines: {
1149
+ format: true,
1150
+ lint: true,
1151
+ "code-quality": true,
1152
+ "ai-slop": true,
1153
+ architecture: false,
1154
+ security: true
1155
+ },
1156
+ quality: {
1157
+ maxFunctionLoc: 80,
1158
+ maxFileLoc: 400,
1159
+ maxNesting: 5,
1160
+ maxParams: 6
1161
+ },
1162
+ security: {
1163
+ audit: true,
1164
+ auditTimeout: 25e3
1165
+ },
1166
+ scoring: {
1167
+ weights: {
1168
+ format: .5,
1169
+ lint: 1,
1170
+ "code-quality": 1.5,
1171
+ "ai-slop": 1,
1172
+ architecture: 1,
1173
+ security: 2
1174
+ },
1175
+ thresholds: {
1176
+ good: 75,
1177
+ ok: 50
1178
+ }
1179
+ },
1180
+ ci: {
1181
+ failBelow: 0,
1182
+ format: "json"
1183
+ }
1184
+ };
1185
+ const DEFAULT_CONFIG_YAML = `version: 1
1186
+
1187
+ engines:
1188
+ format: true
1189
+ lint: true
1190
+ code-quality: true
1191
+ ai-slop: true
1192
+ architecture: false
1193
+ security: true
1194
+
1195
+ quality:
1196
+ maxFunctionLoc: 80
1197
+ maxFileLoc: 400
1198
+ maxNesting: 5
1199
+ maxParams: 6
1200
+
1201
+ security:
1202
+ audit: true
1203
+ auditTimeout: 25000
1204
+
1205
+ scoring:
1206
+ weights:
1207
+ format: 0.5
1208
+ lint: 1.0
1209
+ code-quality: 1.5
1210
+ ai-slop: 1.0
1211
+ architecture: 1.0
1212
+ security: 2.0
1213
+ thresholds:
1214
+ good: 75
1215
+ ok: 50
1216
+
1217
+ ci:
1218
+ failBelow: 0
1219
+ format: json
1220
+ `;
1221
+ const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
1222
+ # Uncomment and customize to enforce your project's conventions.
1223
+ #
1224
+ # rules:
1225
+ # - name: no-axios
1226
+ # type: forbid_import
1227
+ # match: "axios"
1228
+ # severity: error
1229
+ #
1230
+ # - name: controller-no-db
1231
+ # type: forbid_import_from_path
1232
+ # from: "src/controllers/**"
1233
+ # forbid: "src/db/**"
1234
+ # severity: error
1235
+ `;
1236
+
1237
+ //#endregion
1238
+ //#region src/config/schema.ts
1239
+ const DEFAULT_WEIGHTS = {
1240
+ format: .5,
1241
+ lint: 1,
1242
+ "code-quality": 1.5,
1243
+ "ai-slop": 1,
1244
+ architecture: 1,
1245
+ security: 2
1246
+ };
1247
+ const EnginesSchema = z.object({
1248
+ format: z.boolean().default(true),
1249
+ lint: z.boolean().default(true),
1250
+ "code-quality": z.boolean().default(true),
1251
+ "ai-slop": z.boolean().default(true),
1252
+ architecture: z.boolean().default(false),
1253
+ security: z.boolean().default(true)
1254
+ });
1255
+ const QualitySchema = z.object({
1256
+ maxFunctionLoc: z.number().positive().default(80),
1257
+ maxFileLoc: z.number().positive().default(400),
1258
+ maxNesting: z.number().positive().default(5),
1259
+ maxParams: z.number().positive().default(6)
1260
+ });
1261
+ const SecurityConfigSchema = z.object({
1262
+ audit: z.boolean().default(true),
1263
+ auditTimeout: z.number().positive().default(25e3)
1264
+ });
1265
+ const ThresholdsSchema = z.object({
1266
+ good: z.number().default(75),
1267
+ ok: z.number().default(50)
1268
+ });
1269
+ const ScoringSchema = z.object({
1270
+ weights: z.record(z.string(), z.number()).default(DEFAULT_WEIGHTS),
1271
+ thresholds: ThresholdsSchema.default(() => ({
1272
+ good: 75,
1273
+ ok: 50
1274
+ }))
1275
+ });
1276
+ const CiSchema = z.object({
1277
+ failBelow: z.number().default(0),
1278
+ format: z.enum(["json"]).default("json")
1279
+ });
1280
+ const AislopConfigSchema = z.object({
1281
+ version: z.number().default(1),
1282
+ engines: EnginesSchema.default(() => ({
1283
+ format: true,
1284
+ lint: true,
1285
+ "code-quality": true,
1286
+ "ai-slop": true,
1287
+ architecture: false,
1288
+ security: true
1289
+ })),
1290
+ quality: QualitySchema.default(() => ({
1291
+ maxFunctionLoc: 80,
1292
+ maxFileLoc: 400,
1293
+ maxNesting: 5,
1294
+ maxParams: 6
1295
+ })),
1296
+ security: SecurityConfigSchema.default(() => ({
1297
+ audit: true,
1298
+ auditTimeout: 25e3
1299
+ })),
1300
+ scoring: ScoringSchema.default(() => ({
1301
+ weights: { ...DEFAULT_WEIGHTS },
1302
+ thresholds: {
1303
+ good: 75,
1304
+ ok: 50
1305
+ }
1306
+ })),
1307
+ ci: CiSchema.default(() => ({
1308
+ failBelow: 0,
1309
+ format: "json"
1310
+ }))
1311
+ });
1312
+ const defaults = AislopConfigSchema.parse({});
1313
+ /**
1314
+ * Pre-merge scoring weights so partial overrides extend the defaults
1315
+ * rather than replacing them entirely (z.record replaces by default).
1316
+ */
1317
+ const preMergeWeights = (raw) => {
1318
+ const scoring = raw.scoring;
1319
+ if (!scoring) return;
1320
+ const userWeights = scoring.weights;
1321
+ if (!userWeights || typeof userWeights !== "object") return;
1322
+ scoring.weights = {
1323
+ ...DEFAULT_WEIGHTS,
1324
+ ...userWeights
1325
+ };
1326
+ };
1327
+ const parseConfig = (raw) => {
1328
+ if (!raw || typeof raw !== "object") return defaults;
1329
+ try {
1330
+ const input = raw;
1331
+ preMergeWeights(input);
1332
+ return AislopConfigSchema.parse(input);
1333
+ } catch {
1334
+ return defaults;
1335
+ }
1336
+ };
1337
+
1338
+ //#endregion
1339
+ //#region src/config/index.ts
1340
+ const CONFIG_DIR = ".aislop";
1341
+ const CONFIG_FILE = "config.yml";
1342
+ const RULES_FILE = "rules.yml";
1343
+ const findConfigDir = (startDir) => {
1344
+ let current = path.resolve(startDir);
1345
+ while (true) {
1346
+ const candidate = path.join(current, CONFIG_DIR);
1347
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) return candidate;
1348
+ const parent = path.dirname(current);
1349
+ if (parent === current) break;
1350
+ current = parent;
1351
+ }
1352
+ return null;
1353
+ };
1354
+ const loadConfig = (directory) => {
1355
+ const configDir = findConfigDir(directory);
1356
+ if (!configDir) return DEFAULT_CONFIG;
1357
+ const configPath = path.join(configDir, CONFIG_FILE);
1358
+ if (!fs.existsSync(configPath)) return DEFAULT_CONFIG;
1359
+ try {
1360
+ const raw = fs.readFileSync(configPath, "utf-8");
1361
+ return parseConfig(YAML.parse(raw));
1362
+ } catch {
1363
+ return DEFAULT_CONFIG;
1364
+ }
1365
+ };
1366
+
1367
+ //#endregion
1368
+ //#region src/utils/spinner.ts
1369
+ const createNoopHandle = () => ({
1370
+ succeed: () => void 0,
1371
+ fail: () => void 0,
1372
+ stop: () => void 0
1373
+ });
1374
+ const shouldRenderSpinner = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
1375
+ const spinner = (text) => ({ start() {
1376
+ if (!shouldRenderSpinner()) return createNoopHandle();
1377
+ const instance = ora({ text }).start();
1378
+ return {
1379
+ succeed: (displayText) => instance.succeed(displayText),
1380
+ fail: (displayText) => instance.fail(displayText),
1381
+ stop: () => instance.stop()
1382
+ };
1383
+ } });
1384
+
1385
+ //#endregion
1386
+ //#region src/commands/init.ts
1387
+ const initCommand = async (directory) => {
1388
+ const resolvedDir = path.resolve(directory);
1389
+ printCommandHeader("Init");
1390
+ const s1 = spinner("Detecting project...").start();
1391
+ const projectInfo = await discoverProject(resolvedDir);
1392
+ s1.stop();
1393
+ logger.success(` ✓ ${formatProjectSummary(projectInfo)}`);
1394
+ printProjectMetadata(projectInfo);
1395
+ const configDir = path.join(resolvedDir, CONFIG_DIR);
1396
+ if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
1397
+ const configPath = path.join(configDir, CONFIG_FILE);
1398
+ if (fs.existsSync(configPath)) logger.dim(` ${CONFIG_DIR}/${CONFIG_FILE} already exists, skipping`);
1399
+ else {
1400
+ fs.writeFileSync(configPath, DEFAULT_CONFIG_YAML);
1401
+ spinner(`Creating ${CONFIG_DIR}/${CONFIG_FILE}...`).start().succeed(`Created ${highlighter.info(`${CONFIG_DIR}/${CONFIG_FILE}`)}`);
1402
+ }
1403
+ const rulesPath = path.join(configDir, RULES_FILE);
1404
+ if (fs.existsSync(rulesPath)) logger.dim(` ${CONFIG_DIR}/${RULES_FILE} already exists, skipping`);
1405
+ else {
1406
+ fs.writeFileSync(rulesPath, DEFAULT_RULES_YAML);
1407
+ spinner(`Creating ${CONFIG_DIR}/${RULES_FILE}...`).start().succeed(`Created ${highlighter.info(`${CONFIG_DIR}/${RULES_FILE}`)}`);
1408
+ }
1409
+ logger.break();
1410
+ logger.log(" Next steps:");
1411
+ logger.dim(" 1. Edit .aislop/config.yml to customize engines and thresholds");
1412
+ logger.dim(" 2. Edit .aislop/rules.yml to add architecture rules");
1413
+ logger.dim(" 3. Run `aislop scan` to see your score");
1414
+ logger.dim(" 4. Add `aislop scan --staged` to your pre-commit hook");
1415
+ logger.break();
1416
+ };
1417
+
1418
+ //#endregion
1419
+ //#region src/engines/ai-slop/abstractions.ts
1420
+ const THIN_WRAPPER_PATTERNS = [
1421
+ /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
1422
+ /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
1423
+ /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm
1424
+ ];
1425
+ const AI_NAMING_PATTERNS = [/(?:helper|util|handler|process|do|handle|execute|perform)_?\d+/i, /(?:data|temp|result|value|item|obj|arr|str|num|val)\d+/];
1426
+ const FRAMEWORK_METHOD_NAMES = /^(?:setUp|tearDown|setUpClass|tearDownClass|setUpModule|tearDownModule)$/;
1427
+ const DUNDER_PATTERN = /^__\w+__$/;
1428
+ const hasHardcodedArgs = (matchText) => {
1429
+ const innerCallMatch = matchText.match(/=>\s*\w+\(([^)]*)\)\s*;?\s*$/);
1430
+ if (!innerCallMatch) {
1431
+ const returnCallMatch = matchText.match(/return\s+\w+\(([^)]*)\)\s*;?\s*\}/);
1432
+ if (!returnCallMatch) return false;
1433
+ return /['"`]\w+['"`]|(?<!\w)\d+(?!\w)/.test(returnCallMatch[1]);
1434
+ }
1435
+ return /['"`]\w+['"`]|(?<!\w)\d+(?!\w)/.test(innerCallMatch[1]);
1436
+ };
1437
+ const isUseContextWrapper = (matchText) => /\buse\w+/.test(matchText) && /useContext\s*\(/.test(matchText);
1438
+ const detectThinWrappers = (content, relativePath) => {
1439
+ const diagnostics = [];
1440
+ const lines = content.split("\n");
1441
+ for (const pattern of THIN_WRAPPER_PATTERNS) {
1442
+ const regex = new RegExp(pattern.source, pattern.flags);
1443
+ let match;
1444
+ while ((match = regex.exec(content)) !== null) {
1445
+ const funcName = match[1];
1446
+ const matchText = match[0];
1447
+ const lineNumber = content.slice(0, match.index).split("\n").length;
1448
+ if (DUNDER_PATTERN.test(funcName)) continue;
1449
+ if (FRAMEWORK_METHOD_NAMES.test(funcName)) continue;
1450
+ if (lineNumber >= 2) {
1451
+ const prevLine = lines[lineNumber - 2]?.trim();
1452
+ if (prevLine && prevLine.startsWith("@")) continue;
1453
+ }
1454
+ if (hasHardcodedArgs(matchText)) continue;
1455
+ if (isUseContextWrapper(matchText)) continue;
1456
+ diagnostics.push({
1457
+ filePath: relativePath,
1458
+ engine: "ai-slop",
1459
+ rule: "ai-slop/thin-wrapper",
1460
+ severity: "warning",
1461
+ message: `Function '${funcName}' is a thin wrapper that only calls another function`,
1462
+ help: "Consider calling the inner function directly instead of wrapping it",
1463
+ line: lineNumber,
1464
+ column: 0,
1465
+ category: "AI Slop",
1466
+ fixable: false
1467
+ });
1468
+ }
1469
+ }
1470
+ return diagnostics;
1471
+ };
1472
+ const detectAiNaming = (content, relativePath) => {
1473
+ const diagnostics = [];
1474
+ const lines = content.split("\n");
1475
+ for (let i = 0; i < lines.length; i++) {
1476
+ const declMatch = lines[i].match(/(?:const|let|var|function|def|func|fn)\s+(\w+)/);
1477
+ if (!declMatch) continue;
1478
+ const name = declMatch[1];
1479
+ if (!AI_NAMING_PATTERNS.some((pattern) => pattern.test(name))) continue;
1480
+ diagnostics.push({
1481
+ filePath: relativePath,
1482
+ engine: "ai-slop",
1483
+ rule: "ai-slop/generic-naming",
1484
+ severity: "info",
1485
+ message: `'${name}' uses generic AI-style naming`,
1486
+ help: "Use descriptive names that explain what the code does",
1487
+ line: i + 1,
1488
+ column: 0,
1489
+ category: "AI Slop",
1490
+ fixable: false
1491
+ });
1492
+ }
1493
+ return diagnostics;
1494
+ };
1495
+ const detectOverAbstraction = async (context) => {
1496
+ const files = getSourceFiles(context);
1497
+ const diagnostics = [];
1498
+ for (const filePath of files) {
1499
+ if (isAutoGenerated(filePath)) continue;
1500
+ let content;
1501
+ try {
1502
+ content = fs.readFileSync(filePath, "utf-8");
1503
+ } catch {
1504
+ continue;
1505
+ }
1506
+ const relativePath = path.relative(context.rootDirectory, filePath);
1507
+ diagnostics.push(...detectThinWrappers(content, relativePath));
1508
+ diagnostics.push(...detectAiNaming(content, relativePath));
1509
+ }
1510
+ return diagnostics;
1511
+ };
1512
+
1513
+ //#endregion
1514
+ //#region src/engines/ai-slop/comments.ts
1515
+ const TRIVIAL_JS_COMMENT_PATTERNS = [
1516
+ /\/\/\s*This (?:function|method|class|variable|constant) (?:will |is used to |is responsible for )?/i,
1517
+ /\/\/\s*Import(?:ing|s)?\s+/i,
1518
+ /\/\/\s*Defin(?:e|ing)\s+(?:the\s+)?/i,
1519
+ /\/\/\s*Initializ(?:e|ing)\s+(?:the\s+)?/i,
1520
+ /\/\/\s*Set(?:ting)?\s+\w+\s+to\s+/i,
1521
+ /\/\/\s*Return(?:ing|s)?\s+(?:the\s+)?/i,
1522
+ /\/\/\s*Check(?:ing)?\s+(?:if|whether)\s+/i,
1523
+ /\/\/\s*(?:Loop(?:ing)?\s+through|Iterat(?:e|ing)\s+over)\s+/i,
1524
+ /\/\/\s*Creat(?:e|ing)\s+(?:a\s+(?:new\s+)?)?/i,
1525
+ /\/\/\s*Updat(?:e|ing)\s+(?:the\s+)?/i,
1526
+ /\/\/\s*(?:Delet|Remov)(?:e|ing)\s+(?:the\s+)?/i,
1527
+ /\/\/\s*Handl(?:e|ing)\s+(?:the\s+)?/i,
1528
+ /\/\/\s*(?:Get(?:ting)?|Fetch(?:ing)?)\s+(?:the\s+)?/i,
1529
+ /\/\/\s*(?:Increment|Decrement)(?:ing)?\s+/i
1530
+ ];
1531
+ const TRIVIAL_PYTHON_COMMENT_PATTERNS = [/^#\s*This (?:function|method|class) (?:will |is used to )?/i, /^#\s*(?:Import|Define|Initialize|Return|Check|Create|Update|Delete|Handle|Get|Fetch)/i];
1532
+ const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?)\b/i;
1533
+ const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
1534
+ const MAX_TRIVIAL_COMMENT_LENGTH = 60;
1535
+ const isJsComment = (trimmed) => trimmed.startsWith("//");
1536
+ const isPythonComment = (trimmed) => trimmed.startsWith("#") && !trimmed.startsWith("#!");
1537
+ /**
1538
+ * Extract just the comment text after the comment marker.
1539
+ */
1540
+ const getCommentBody = (trimmed) => {
1541
+ if (trimmed.startsWith("//")) return trimmed.slice(2).trim();
1542
+ if (trimmed.startsWith("#")) return trimmed.slice(1).trim();
1543
+ return trimmed;
1544
+ };
1545
+ const isTrivialComment = (trimmed, nextLine) => {
1546
+ const isJs = isJsComment(trimmed);
1547
+ const isPy = isPythonComment(trimmed);
1548
+ if (!isJs && !isPy) return false;
1549
+ const commentBody = getCommentBody(trimmed);
1550
+ if (commentBody.length > MAX_TRIVIAL_COMMENT_LENGTH) return false;
1551
+ if (EXPLANATORY_KEYWORDS.test(commentBody)) return false;
1552
+ if (commentBody.includes("(") && commentBody.includes(")")) return false;
1553
+ if (COMMENTED_CODE_CHARS.test(commentBody)) return false;
1554
+ if (nextLine !== void 0 && nextLine.trim() === "") return false;
1555
+ if (/[─━═╌╍┄┅│┃]/.test(commentBody)) return false;
1556
+ if (/^-{3,}|─{3,}/.test(commentBody)) return false;
1557
+ return (isJs ? TRIVIAL_JS_COMMENT_PATTERNS : TRIVIAL_PYTHON_COMMENT_PATTERNS).some((pattern) => pattern.test(trimmed));
1558
+ };
1559
+ const scanFileForTrivialComments = (content, relativePath) => {
1560
+ const diagnostics = [];
1561
+ const lines = content.split("\n");
1562
+ for (let i = 0; i < lines.length; i++) {
1563
+ if (!isTrivialComment(lines[i].trim(), i + 1 < lines.length ? lines[i + 1] : void 0)) continue;
1564
+ diagnostics.push({
1565
+ filePath: relativePath,
1566
+ engine: "ai-slop",
1567
+ rule: "ai-slop/trivial-comment",
1568
+ severity: "warning",
1569
+ message: "Trivial comment that restates the code",
1570
+ help: "Remove comments that don't add information beyond what the code already expresses",
1571
+ line: i + 1,
1572
+ column: 0,
1573
+ category: "AI Slop",
1574
+ fixable: true
1575
+ });
1576
+ }
1577
+ return diagnostics;
1578
+ };
1579
+ const detectTrivialComments = async (context) => {
1580
+ const files = getSourceFiles(context);
1581
+ const diagnostics = [];
1582
+ for (const filePath of files) {
1583
+ if (isAutoGenerated(filePath)) continue;
1584
+ let content;
1585
+ try {
1586
+ content = fs.readFileSync(filePath, "utf-8");
1587
+ } catch {
1588
+ continue;
1589
+ }
1590
+ const relativePath = path.relative(context.rootDirectory, filePath);
1591
+ diagnostics.push(...scanFileForTrivialComments(content, relativePath));
1592
+ }
1593
+ return diagnostics;
1594
+ };
1595
+
1596
+ //#endregion
1597
+ //#region src/engines/ai-slop/dead-patterns.ts
1598
+ const JS_EXTENSIONS$1 = new Set([
1599
+ ".ts",
1600
+ ".tsx",
1601
+ ".js",
1602
+ ".jsx",
1603
+ ".mjs",
1604
+ ".cjs"
1605
+ ]);
1606
+ const CONSOLE_LOG_PATTERN = /\bconsole\.(?:log|debug|info|trace|dir|table)\s*\(/;
1607
+ const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
1608
+ const SCRIPT_DIR_PATTERN = /(?:^|\/)(scripts|bin)\//;
1609
+ const detectConsoleLeftovers = (content, relativePath, ext) => {
1610
+ if (!JS_EXTENSIONS$1.has(ext)) return [];
1611
+ if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
1612
+ if (SCRIPT_DIR_PATTERN.test(relativePath)) return [];
1613
+ const diagnostics = [];
1614
+ const lines = content.split("\n");
1615
+ for (let i = 0; i < lines.length; i++) {
1616
+ const trimmed = lines[i].trim();
1617
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1618
+ if (CONSOLE_LOG_PATTERN.test(trimmed)) {
1619
+ if (/console\.(?:error|warn)\s*\(/.test(trimmed)) continue;
1620
+ if (/console\.log\(\s*JSON\.stringify\b/.test(trimmed)) continue;
1621
+ diagnostics.push({
1622
+ filePath: relativePath,
1623
+ engine: "ai-slop",
1624
+ rule: "ai-slop/console-leftover",
1625
+ severity: "warning",
1626
+ message: "console.log/debug/info statement left in production code",
1627
+ help: "Remove debugging console statements or replace with a proper logger",
1628
+ line: i + 1,
1629
+ column: 0,
1630
+ category: "AI Slop",
1631
+ fixable: true
1632
+ });
1633
+ }
1634
+ }
1635
+ return diagnostics;
1636
+ };
1637
+ const TODO_PATTERN = new RegExp(`\\b(?:${[
1638
+ "TODO",
1639
+ "FIXME",
1640
+ "HACK",
1641
+ "XXX"
1642
+ ].join("|")}|TEMP|PLACEHOLDER|STUB)\\b[:\\s]`, "i");
1643
+ const detectTodoStubs = (content, relativePath) => {
1644
+ const diagnostics = [];
1645
+ const lines = content.split("\n");
1646
+ for (let i = 0; i < lines.length; i++) {
1647
+ const trimmed = lines[i].trim();
1648
+ if (!trimmed.startsWith("//") && !trimmed.startsWith("#") && !trimmed.startsWith("*") && !trimmed.startsWith("/*")) continue;
1649
+ if (TODO_PATTERN.test(trimmed)) diagnostics.push({
1650
+ filePath: relativePath,
1651
+ engine: "ai-slop",
1652
+ rule: "ai-slop/todo-stub",
1653
+ severity: "info",
1654
+ message: "Unresolved TODO/FIXME/HACK comment indicates incomplete code",
1655
+ help: "Resolve the TODO or create a tracked issue for it",
1656
+ line: i + 1,
1657
+ column: 0,
1658
+ category: "AI Slop",
1659
+ fixable: false
1660
+ });
1661
+ }
1662
+ return diagnostics;
1663
+ };
1664
+ const detectDeadCodePatterns = (content, relativePath, ext) => {
1665
+ const diagnostics = [];
1666
+ const lines = content.split("\n");
1667
+ for (let i = 0; i < lines.length; i++) {
1668
+ const trimmed = lines[i].trim();
1669
+ const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
1670
+ if (JS_EXTENSIONS$1.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !nextLine.startsWith("}") && !nextLine.startsWith("//") && !nextLine.startsWith("/*") && !nextLine.startsWith("case ") && !nextLine.startsWith("default:") && !nextLine.startsWith("if ") && !nextLine.startsWith("if(") && !nextLine.startsWith("else")) diagnostics.push({
1671
+ filePath: relativePath,
1672
+ engine: "ai-slop",
1673
+ rule: "ai-slop/unreachable-code",
1674
+ severity: "warning",
1675
+ message: "Code after return/throw statement is unreachable",
1676
+ help: "Remove the unreachable code or restructure the control flow",
1677
+ line: i + 2,
1678
+ column: 0,
1679
+ category: "AI Slop",
1680
+ fixable: false
1681
+ });
1682
+ if (/\bif\s*\(\s*(?:false|true|0|1)\s*\)/.test(trimmed) && !trimmed.startsWith("//") && !trimmed.startsWith("*") && !/["'`].*\bif\s*\(/.test(trimmed) && !/\/.*\bif\s*\(/.test(trimmed.replace(/\/\/.*$/, ""))) diagnostics.push({
1683
+ filePath: relativePath,
1684
+ engine: "ai-slop",
1685
+ rule: "ai-slop/constant-condition",
1686
+ severity: "warning",
1687
+ message: "Conditional with a constant value — likely debugging leftover",
1688
+ help: "Remove the constant condition or replace with proper logic",
1689
+ line: i + 1,
1690
+ column: 0,
1691
+ category: "AI Slop",
1692
+ fixable: false
1693
+ });
1694
+ if (JS_EXTENSIONS$1.has(ext) && /(?:function\s+\w+|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ")) diagnostics.push({
1695
+ filePath: relativePath,
1696
+ engine: "ai-slop",
1697
+ rule: "ai-slop/empty-function",
1698
+ severity: "info",
1699
+ message: "Empty function body — possible stub or unfinished implementation",
1700
+ help: "Implement the function body or add a comment explaining why it's empty",
1701
+ line: i + 1,
1702
+ column: 0,
1703
+ category: "AI Slop",
1704
+ fixable: false
1705
+ });
1706
+ }
1707
+ return diagnostics;
1708
+ };
1709
+ const asAnyPattern = new RegExp(`\\bas\\s+any\\b`);
1710
+ const doubleAssertPattern = new RegExp(`\\bas\\s+unknown\\s+as\\s+`);
1711
+ const detectUnsafeTypePatterns = (content, relativePath, ext) => {
1712
+ if (ext !== ".ts" && ext !== ".tsx") return [];
1713
+ const diagnostics = [];
1714
+ const lines = content.split("\n");
1715
+ for (let i = 0; i < lines.length; i++) {
1716
+ const trimmed = lines[i].trim();
1717
+ if (/\/\/\s*@ts-(?:ignore|expect-error)/.test(trimmed) || /\/\*\s*@ts-(?:ignore|expect-error)/.test(trimmed)) diagnostics.push({
1718
+ filePath: relativePath,
1719
+ engine: "ai-slop",
1720
+ rule: "ai-slop/ts-directive",
1721
+ severity: "info",
1722
+ message: "@ts-ignore/@ts-expect-error suppresses type checking — review if still needed",
1723
+ help: "Fix the underlying type issue instead of suppressing the error",
1724
+ line: i + 1,
1725
+ column: 0,
1726
+ category: "AI Slop",
1727
+ fixable: false
1728
+ });
1729
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1730
+ if (/\bRegExp\b|new\s+RegExp|\/.*\\b/.test(trimmed)) continue;
1731
+ if (/["'`].*\\b.*["'`]/.test(trimmed)) continue;
1732
+ if (asAnyPattern.test(trimmed)) diagnostics.push({
1733
+ filePath: relativePath,
1734
+ engine: "ai-slop",
1735
+ rule: "ai-slop/unsafe-type-assertion",
1736
+ severity: "warning",
1737
+ message: `'as any' bypasses type safety`,
1738
+ help: "Use a proper type or a more specific assertion",
1739
+ line: i + 1,
1740
+ column: 0,
1741
+ category: "AI Slop",
1742
+ fixable: false
1743
+ });
1744
+ if (doubleAssertPattern.test(trimmed)) diagnostics.push({
1745
+ filePath: relativePath,
1746
+ engine: "ai-slop",
1747
+ rule: "ai-slop/double-type-assertion",
1748
+ severity: "warning",
1749
+ message: `Double type assertion (as unknown as X) bypasses type checking`,
1750
+ help: "Refactor to avoid needing a double assertion — it usually indicates a design issue",
1751
+ line: i + 1,
1752
+ column: 0,
1753
+ category: "AI Slop",
1754
+ fixable: false
1755
+ });
1756
+ }
1757
+ return diagnostics;
1758
+ };
1759
+ const detectDeadPatterns = async (context) => {
1760
+ const files = getSourceFiles(context);
1761
+ const diagnostics = [];
1762
+ for (const filePath of files) {
1763
+ if (isAutoGenerated(filePath)) continue;
1764
+ let content;
1765
+ try {
1766
+ content = fs.readFileSync(filePath, "utf-8");
1767
+ } catch {
1768
+ continue;
1769
+ }
1770
+ const ext = path.extname(filePath);
1771
+ const relativePath = path.relative(context.rootDirectory, filePath);
1772
+ diagnostics.push(...detectConsoleLeftovers(content, relativePath, ext));
1773
+ diagnostics.push(...detectTodoStubs(content, relativePath));
1774
+ diagnostics.push(...detectDeadCodePatterns(content, relativePath, ext));
1775
+ diagnostics.push(...detectUnsafeTypePatterns(content, relativePath, ext));
1776
+ }
1777
+ return diagnostics;
1778
+ };
1779
+
1780
+ //#endregion
1781
+ //#region src/engines/ai-slop/exceptions.ts
1782
+ const SWALLOWED_EXCEPTION_PATTERNS = [
1783
+ {
1784
+ pattern: /catch\s*\([^)]*\)\s*\{\s*(?:\/\/[^\n]*)?\s*\}/,
1785
+ languages: [
1786
+ ".ts",
1787
+ ".tsx",
1788
+ ".js",
1789
+ ".jsx",
1790
+ ".mjs",
1791
+ ".cjs"
1792
+ ],
1793
+ message: "Empty catch block swallows errors silently"
1794
+ },
1795
+ {
1796
+ pattern: /catch\s*\([^)]*\)\s*\{\s*console\.log\([^)]*\);\s*\}/,
1797
+ languages: [
1798
+ ".ts",
1799
+ ".tsx",
1800
+ ".js",
1801
+ ".jsx",
1802
+ ".mjs",
1803
+ ".cjs"
1804
+ ],
1805
+ message: "Catch block only logs error without proper handling"
1806
+ },
1807
+ {
1808
+ pattern: /except(?:\s+\w+)?:\s*\n\s*pass/,
1809
+ languages: [".py"],
1810
+ message: "Bare except with pass swallows errors silently"
1811
+ },
1812
+ {
1813
+ pattern: /except\s+\w+(?:\s+as\s+\w+)?:\s*\n\s*print\(/,
1814
+ languages: [".py"],
1815
+ message: "Catch block only prints error without proper handling"
1816
+ },
1817
+ {
1818
+ pattern: /\w+,\s*_\s*:?=\s*\w+\(/,
1819
+ languages: [".go"],
1820
+ message: "Error return value is being ignored"
1821
+ },
1822
+ {
1823
+ pattern: /rescue(?:\s+\w+)?\s*(?:=>?\s*\w+)?\s*\n\s*(?:nil|#)/,
1824
+ languages: [".rb"],
1825
+ message: "Rescue block swallows errors silently"
1826
+ },
1827
+ {
1828
+ pattern: /catch\s*\(\w+\s+\w+\)\s*\{\s*(?:\/\/[^\n]*)?\s*\}/,
1829
+ languages: [".java"],
1830
+ message: "Empty catch block swallows errors silently"
1831
+ }
1832
+ ];
1833
+ const detectSwallowedExceptions = async (context) => {
1834
+ const files = getSourceFiles(context);
1835
+ const diagnostics = [];
1836
+ for (const filePath of files) {
1837
+ if (isAutoGenerated(filePath)) continue;
1838
+ let content;
1839
+ try {
1840
+ content = fs.readFileSync(filePath, "utf-8");
1841
+ } catch {
1842
+ continue;
1843
+ }
1844
+ const ext = path.extname(filePath);
1845
+ const relativePath = path.relative(context.rootDirectory, filePath);
1846
+ for (const { pattern, languages, message } of SWALLOWED_EXCEPTION_PATTERNS) {
1847
+ if (!languages.includes(ext)) continue;
1848
+ let match;
1849
+ const regex = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes("g") ? "" : "g"));
1850
+ while ((match = regex.exec(content)) !== null) {
1851
+ const line = content.slice(0, match.index).split("\n").length;
1852
+ diagnostics.push({
1853
+ filePath: relativePath,
1854
+ engine: "ai-slop",
1855
+ rule: "ai-slop/swallowed-exception",
1856
+ severity: "error",
1857
+ message,
1858
+ help: "Handle errors explicitly: log with context, rethrow, or return an error value",
1859
+ line,
1860
+ column: 0,
1861
+ category: "AI Slop",
1862
+ fixable: false
1863
+ });
1864
+ }
1865
+ }
1866
+ }
1867
+ return diagnostics;
1868
+ };
1869
+
1870
+ //#endregion
1871
+ //#region src/engines/ai-slop/unused-imports.ts
1872
+ const JS_EXTENSIONS = new Set([
1873
+ ".ts",
1874
+ ".tsx",
1875
+ ".js",
1876
+ ".jsx",
1877
+ ".mjs",
1878
+ ".cjs"
1879
+ ]);
1880
+ const PY_EXTENSIONS = new Set([".py"]);
1881
+ const extractJsImportedSymbols = (lines) => {
1882
+ const symbols = [];
1883
+ const importLines = /* @__PURE__ */ new Set();
1884
+ for (let i = 0; i < lines.length; i++) {
1885
+ const trimmed = lines[i].trim();
1886
+ if (!trimmed.startsWith("import ")) continue;
1887
+ importLines.add(i);
1888
+ if (/^import\s+["']/.test(trimmed)) continue;
1889
+ if (/^import\s+type\s/.test(trimmed)) continue;
1890
+ let fullImport = trimmed;
1891
+ let endLine = i;
1892
+ while (!fullImport.includes("from") && endLine < lines.length - 1) {
1893
+ endLine++;
1894
+ fullImport += ` ${lines[endLine].trim()}`;
1895
+ importLines.add(endLine);
1896
+ }
1897
+ const namespaceMatch = fullImport.match(/import\s+\*\s+as\s+(\w+)\s+from/);
1898
+ if (namespaceMatch) {
1899
+ symbols.push({
1900
+ name: namespaceMatch[1],
1901
+ line: i + 1,
1902
+ isDefault: false,
1903
+ isNamespace: true
1904
+ });
1905
+ continue;
1906
+ }
1907
+ const defaultMatch = fullImport.match(/import\s+(\w+)\s*(?:,\s*\{[^}]*\})?\s+from/);
1908
+ if (defaultMatch && defaultMatch[1] !== "type") symbols.push({
1909
+ name: defaultMatch[1],
1910
+ line: i + 1,
1911
+ isDefault: true,
1912
+ isNamespace: false
1913
+ });
1914
+ const namedMatch = fullImport.match(/\{([^}]+)\}/);
1915
+ if (namedMatch) {
1916
+ const namedImports = namedMatch[1].split(",");
1917
+ for (const ni of namedImports) {
1918
+ const parts = ni.trim().split(/\s+as\s+/);
1919
+ if (parts.length === 0 || !parts[0]) continue;
1920
+ const cleanParts = parts.map((p) => p.trim().replace(/^type\s+/, ""));
1921
+ const localName = cleanParts.length > 1 ? cleanParts[1] : cleanParts[0];
1922
+ if (localName && /^\w+$/.test(localName)) symbols.push({
1923
+ name: localName,
1924
+ line: i + 1,
1925
+ isDefault: false,
1926
+ isNamespace: false
1927
+ });
1928
+ }
1929
+ }
1930
+ }
1931
+ return {
1932
+ symbols,
1933
+ importLines
1934
+ };
1935
+ };
1936
+ const extractPyImportedSymbols = (lines) => {
1937
+ const symbols = [];
1938
+ const importLines = /* @__PURE__ */ new Set();
1939
+ for (let i = 0; i < lines.length; i++) {
1940
+ const trimmed = lines[i].trim();
1941
+ const fromMatch = trimmed.match(/^from\s+[\w.]+\s+import\s+(.+)/);
1942
+ if (fromMatch) {
1943
+ importLines.add(i);
1944
+ const importPart = fromMatch[1].replace(/#.*$/, "").trim();
1945
+ if (importPart === "*") continue;
1946
+ const cleaned = importPart.replace(/[()]/g, "");
1947
+ for (const item of cleaned.split(",")) {
1948
+ const parts = item.trim().split(/\s+as\s+/);
1949
+ const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim();
1950
+ if (localName && /^\w+$/.test(localName)) symbols.push({
1951
+ name: localName,
1952
+ line: i + 1,
1953
+ isDefault: false,
1954
+ isNamespace: false
1955
+ });
1956
+ }
1957
+ continue;
1958
+ }
1959
+ const importMatch = trimmed.match(/^import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
1960
+ if (importMatch) {
1961
+ importLines.add(i);
1962
+ const simpleName = (importMatch[2] ?? importMatch[1]).split(".")[0];
1963
+ if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
1964
+ name: simpleName,
1965
+ line: i + 1,
1966
+ isDefault: false,
1967
+ isNamespace: true
1968
+ });
1969
+ }
1970
+ }
1971
+ return {
1972
+ symbols,
1973
+ importLines
1974
+ };
1975
+ };
1976
+ const isSymbolUsed = (name, content, importLines, lines) => {
1977
+ const pattern = new RegExp(`\\b${name}\\b`, "g");
1978
+ let match;
1979
+ while ((match = pattern.exec(content)) !== null) {
1980
+ const lineIndex = content.slice(0, match.index).split("\n").length - 1;
1981
+ if (!importLines.has(lineIndex)) return true;
1982
+ }
1983
+ for (let i = 0; i < lines.length; i++) {
1984
+ if (importLines.has(i)) continue;
1985
+ if (lines[i].includes(name)) return true;
1986
+ }
1987
+ return false;
1988
+ };
1989
+ const detectUnusedImports = async (context) => {
1990
+ const files = getSourceFiles(context);
1991
+ const diagnostics = [];
1992
+ for (const filePath of files) {
1993
+ if (isAutoGenerated(filePath)) continue;
1994
+ let content;
1995
+ try {
1996
+ content = fs.readFileSync(filePath, "utf-8");
1997
+ } catch {
1998
+ continue;
1999
+ }
2000
+ const ext = path.extname(filePath);
2001
+ const relativePath = path.relative(context.rootDirectory, filePath);
2002
+ const lines = content.split("\n");
2003
+ let symbols;
2004
+ let importLinesSet;
2005
+ if (JS_EXTENSIONS.has(ext)) {
2006
+ const result = extractJsImportedSymbols(lines);
2007
+ symbols = result.symbols;
2008
+ importLinesSet = result.importLines;
2009
+ } else if (PY_EXTENSIONS.has(ext)) {
2010
+ const result = extractPyImportedSymbols(lines);
2011
+ symbols = result.symbols;
2012
+ importLinesSet = result.importLines;
2013
+ } else continue;
2014
+ for (const symbol of symbols) if (!isSymbolUsed(symbol.name, content, importLinesSet, lines)) diagnostics.push({
2015
+ filePath: relativePath,
2016
+ engine: "ai-slop",
2017
+ rule: "ai-slop/unused-import",
2018
+ severity: "warning",
2019
+ message: `Imported symbol '${symbol.name}' is never used`,
2020
+ help: "Remove unused imports to keep the code clean",
2021
+ line: symbol.line,
2022
+ column: 0,
2023
+ category: "AI Slop",
2024
+ fixable: true
2025
+ });
2026
+ }
2027
+ return diagnostics;
2028
+ };
2029
+
2030
+ //#endregion
2031
+ //#region src/engines/ai-slop/index.ts
2032
+ const aiSlopEngine = {
2033
+ name: "ai-slop",
2034
+ async run(context) {
2035
+ const diagnostics = [];
2036
+ const results = await Promise.allSettled([
2037
+ detectTrivialComments(context),
2038
+ detectSwallowedExceptions(context),
2039
+ detectOverAbstraction(context),
2040
+ detectDeadPatterns(context),
2041
+ detectUnusedImports(context)
2042
+ ]);
2043
+ for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
2044
+ return {
2045
+ engine: "ai-slop",
2046
+ diagnostics,
2047
+ elapsed: 0,
2048
+ skipped: false
2049
+ };
2050
+ }
2051
+ };
2052
+
2053
+ //#endregion
2054
+ //#region src/engines/architecture/matchers.ts
2055
+ const minimatch = (filePath, pattern) => {
2056
+ let regex = "";
2057
+ let i = 0;
2058
+ while (i < pattern.length) {
2059
+ const ch = pattern[i];
2060
+ if (ch === "*" && pattern[i + 1] === "*") {
2061
+ regex += ".*";
2062
+ i += 2;
2063
+ if (pattern[i] === "/") i++;
2064
+ } else if (ch === "*") {
2065
+ regex += "[^/]*";
2066
+ i++;
2067
+ } else if (ch === "?") {
2068
+ regex += "[^/]";
2069
+ i++;
2070
+ } else if (ch === "[") {
2071
+ const closeIndex = pattern.indexOf("]", i + 1);
2072
+ if (closeIndex === -1) {
2073
+ regex += "\\[";
2074
+ i++;
2075
+ } else {
2076
+ regex += pattern.slice(i, closeIndex + 1);
2077
+ i = closeIndex + 1;
2078
+ }
2079
+ } else if (".+^${}()|\\".includes(ch)) {
2080
+ regex += `\\${ch}`;
2081
+ i++;
2082
+ } else {
2083
+ regex += ch;
2084
+ i++;
2085
+ }
2086
+ }
2087
+ return new RegExp(`^${regex}$`).test(filePath);
2088
+ };
2089
+ const extractImports = (content, ext) => {
2090
+ const imports = [];
2091
+ if ([
2092
+ ".ts",
2093
+ ".tsx",
2094
+ ".js",
2095
+ ".jsx",
2096
+ ".mjs",
2097
+ ".cjs"
2098
+ ].includes(ext)) {
2099
+ const esPattern = /(?:import|from)\s+["']([^"']+)["']/g;
2100
+ let match;
2101
+ while ((match = esPattern.exec(content)) !== null) imports.push(match[1]);
2102
+ const reqPattern = /require\s*\(\s*["']([^"']+)["']\s*\)/g;
2103
+ while ((match = reqPattern.exec(content)) !== null) imports.push(match[1]);
2104
+ }
2105
+ if (ext === ".py") {
2106
+ const pyPattern = /(?:from|import)\s+([\w.]+)/g;
2107
+ let match;
2108
+ while ((match = pyPattern.exec(content)) !== null) imports.push(match[1]);
2109
+ }
2110
+ if (ext === ".go") {
2111
+ const goSingleImport = /^\s*import\s+"([^"]+)"/gm;
2112
+ let match;
2113
+ while ((match = goSingleImport.exec(content)) !== null) imports.push(match[1]);
2114
+ const goMultiImport = /import\s*\(([^)]*)\)/gs;
2115
+ while ((match = goMultiImport.exec(content)) !== null) {
2116
+ const block = match[1];
2117
+ const pkgPattern = /"([^"]+)"/g;
2118
+ let pkgMatch;
2119
+ while ((pkgMatch = pkgPattern.exec(block)) !== null) imports.push(pkgMatch[1]);
2120
+ }
2121
+ }
2122
+ return imports;
2123
+ };
2124
+ const applyForbidImport = (rule, imports, content, relativePath) => {
2125
+ if (!rule.match) return [];
2126
+ return imports.filter((imp) => imp.includes(rule.match)).map((imp) => ({
2127
+ filePath: relativePath,
2128
+ engine: "architecture",
2129
+ rule: `arch/${rule.name}`,
2130
+ severity: rule.severity,
2131
+ message: `Forbidden import '${imp}' (rule: ${rule.name})`,
2132
+ help: `This import is not allowed by your architecture rules`,
2133
+ line: findImportLine(content, imp),
2134
+ column: 0,
2135
+ category: "Architecture",
2136
+ fixable: false
2137
+ }));
2138
+ };
2139
+ const applyForbidImportFromPath = (rule, imports, content, relativePath) => {
2140
+ if (!rule.from || !rule.forbid) return [];
2141
+ if (!minimatch(relativePath, rule.from)) return [];
2142
+ return imports.filter((imp) => minimatch(imp, rule.forbid) || imp.includes(rule.forbid.replace(/\*\*/g, ""))).map((imp) => ({
2143
+ filePath: relativePath,
2144
+ engine: "architecture",
2145
+ rule: `arch/${rule.name}`,
2146
+ severity: rule.severity,
2147
+ message: `Import '${imp}' is forbidden from '${rule.from}' (rule: ${rule.name})`,
2148
+ help: `Files in '${rule.from}' cannot import from '${rule.forbid}'`,
2149
+ line: findImportLine(content, imp),
2150
+ column: 0,
2151
+ category: "Architecture",
2152
+ fixable: false
2153
+ }));
2154
+ };
2155
+ const applyRequirePattern = (rule, content, relativePath) => {
2156
+ if (!rule.where || !rule.pattern) return [];
2157
+ if (!minimatch(relativePath, rule.where)) return [];
2158
+ if (content.includes(rule.pattern)) return [];
2159
+ return [{
2160
+ filePath: relativePath,
2161
+ engine: "architecture",
2162
+ rule: `arch/${rule.name}`,
2163
+ severity: rule.severity,
2164
+ message: `Required pattern '${rule.pattern}' not found (rule: ${rule.name})`,
2165
+ help: `Files matching '${rule.where}' must contain '${rule.pattern}'`,
2166
+ line: 0,
2167
+ column: 0,
2168
+ category: "Architecture",
2169
+ fixable: false
2170
+ }];
2171
+ };
2172
+ const applyRule = (rule, imports, content, relativePath) => {
2173
+ switch (rule.type) {
2174
+ case "forbid_import": return applyForbidImport(rule, imports, content, relativePath);
2175
+ case "forbid_import_from_path": return applyForbidImportFromPath(rule, imports, content, relativePath);
2176
+ case "require_pattern": return applyRequirePattern(rule, content, relativePath);
2177
+ default: return [];
2178
+ }
2179
+ };
2180
+ const checkRules = async (context, rules) => {
2181
+ const files = getSourceFiles(context);
2182
+ const diagnostics = [];
2183
+ for (const filePath of files) {
2184
+ let content;
2185
+ try {
2186
+ content = fs.readFileSync(filePath, "utf-8");
2187
+ } catch {
2188
+ continue;
2189
+ }
2190
+ const relativePath = path.relative(context.rootDirectory, filePath);
2191
+ const imports = extractImports(content, path.extname(filePath));
2192
+ for (const rule of rules) diagnostics.push(...applyRule(rule, imports, content, relativePath));
2193
+ }
2194
+ return diagnostics;
2195
+ };
2196
+ const findImportLine = (content, importPath) => {
2197
+ const lines = content.split("\n");
2198
+ for (let i = 0; i < lines.length; i++) if (lines[i].includes(importPath)) return i + 1;
2199
+ return 0;
2200
+ };
2201
+
2202
+ //#endregion
2203
+ //#region src/engines/architecture/rule-loader.ts
2204
+ const loadArchitectureRules = (rulesPath) => {
2205
+ if (!fs.existsSync(rulesPath)) return [];
2206
+ try {
2207
+ const content = fs.readFileSync(rulesPath, "utf-8");
2208
+ return YAML.parse(content)?.rules ?? [];
2209
+ } catch {
2210
+ return [];
2211
+ }
2212
+ };
2213
+
2214
+ //#endregion
2215
+ //#region src/engines/architecture/index.ts
2216
+ const architectureEngine = {
2217
+ name: "architecture",
2218
+ async run(context) {
2219
+ if (!context.config.architectureRulesPath) return {
2220
+ engine: "architecture",
2221
+ diagnostics: [],
2222
+ elapsed: 0,
2223
+ skipped: true,
2224
+ skipReason: "No architecture rules configured"
2225
+ };
2226
+ const rules = loadArchitectureRules(context.config.architectureRulesPath);
2227
+ if (rules.length === 0) return {
2228
+ engine: "architecture",
2229
+ diagnostics: [],
2230
+ elapsed: 0,
2231
+ skipped: true,
2232
+ skipReason: "No rules found in rules file"
2233
+ };
2234
+ return {
2235
+ engine: "architecture",
2236
+ diagnostics: await checkRules(context, rules),
2237
+ elapsed: 0,
2238
+ skipped: false
2239
+ };
2240
+ }
2241
+ };
2242
+
2243
+ //#endregion
2244
+ //#region src/engines/code-quality/complexity.ts
2245
+ const FUNCTION_PATTERNS = [
2246
+ {
2247
+ regex: /^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
2248
+ langFilter: [
2249
+ ".js",
2250
+ ".ts",
2251
+ ".jsx",
2252
+ ".tsx",
2253
+ ".mjs",
2254
+ ".cjs"
2255
+ ]
2256
+ },
2257
+ {
2258
+ regex: /^\s*(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?:=>|:\s*\w)/,
2259
+ langFilter: [
2260
+ ".js",
2261
+ ".ts",
2262
+ ".jsx",
2263
+ ".tsx",
2264
+ ".mjs",
2265
+ ".cjs"
2266
+ ]
2267
+ },
2268
+ {
2269
+ regex: /^\s*def\s+(\w+)\s*\(([^)]*)\)/,
2270
+ langFilter: [".py"]
2271
+ },
2272
+ {
2273
+ regex: /^\s*func\s+(?:\([^)]*\)\s+)?(\w+)\s*\(([^)]*)\)/,
2274
+ langFilter: [".go"]
2275
+ },
2276
+ {
2277
+ regex: /^\s*fn\s+(\w+)\s*\(([^)]*)\)/,
2278
+ langFilter: [".rs"]
2279
+ },
2280
+ {
2281
+ regex: /^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:\w+\s+)(\w+)\s*\(([^)]*)\)/,
2282
+ langFilter: [
2283
+ ".java",
2284
+ ".cs",
2285
+ ".cpp",
2286
+ ".c",
2287
+ ".php"
2288
+ ]
2289
+ }
2290
+ ];
2291
+ const countParams = (paramStr) => {
2292
+ if (!paramStr.trim()) return 0;
2293
+ return paramStr.split(",").length;
2294
+ };
2295
+ const matchFunctionOnLine = (line, ext) => {
2296
+ for (let i = 0; i < FUNCTION_PATTERNS.length; i++) {
2297
+ const pattern = FUNCTION_PATTERNS[i];
2298
+ if (!pattern.langFilter.includes(ext)) continue;
2299
+ const match = line.match(pattern.regex);
2300
+ if (match) return {
2301
+ name: match[1],
2302
+ params: match[2] ?? "",
2303
+ patternIndex: i
2304
+ };
2305
+ }
2306
+ return null;
2307
+ };
2308
+ const PYTHON_CONTROL_FLOW_RE = /^\s*(?:if|for|while|with|try|except|else|elif|finally|def|class)\b/;
2309
+ const findFunctionEnd = (lines, startIndex, isPython) => {
2310
+ if (isPython) return findPythonFunctionEnd(lines, startIndex);
2311
+ return findBraceFunctionEnd(lines, startIndex);
2312
+ };
2313
+ const isControlFlowBrace = (lineText, braceIndex) => {
2314
+ const before = lineText.substring(0, braceIndex).trimEnd();
2315
+ if (before.endsWith(")")) return true;
2316
+ if (before.endsWith("=>")) return true;
2317
+ if (/\b(?:else|try|finally|do)$/.test(before)) return true;
2318
+ return false;
2319
+ };
2320
+ const findBraceFunctionEnd = (lines, startIndex) => {
2321
+ let depth = 0;
2322
+ let started = false;
2323
+ let endLine = startIndex;
2324
+ let maxNesting = 0;
2325
+ let functionStartDepth = 0;
2326
+ const braceStack = [];
2327
+ for (let j = startIndex; j < lines.length; j++) {
2328
+ const l = lines[j];
2329
+ for (let ci = 0; ci < l.length; ci++) {
2330
+ const ch = l[ci];
2331
+ if (ch === "{") {
2332
+ depth++;
2333
+ if (!started) {
2334
+ started = true;
2335
+ functionStartDepth = depth;
2336
+ braceStack.push(false);
2337
+ } else {
2338
+ const isCF = isControlFlowBrace(l, ci);
2339
+ braceStack.push(isCF);
2340
+ if (isCF) {
2341
+ let cfCount = 0;
2342
+ for (const b of braceStack) if (b) cfCount++;
2343
+ if (cfCount > maxNesting) maxNesting = cfCount;
2344
+ }
2345
+ }
2346
+ } else if (ch === "}") {
2347
+ depth--;
2348
+ braceStack.pop();
2349
+ }
2350
+ }
2351
+ if (started && depth < functionStartDepth && j > startIndex) {
2352
+ endLine = j;
2353
+ break;
2354
+ }
2355
+ if (j === lines.length - 1) endLine = j;
2356
+ }
2357
+ if (!started) return {
2358
+ endLine: startIndex,
2359
+ maxNesting: 0
2360
+ };
2361
+ return {
2362
+ endLine,
2363
+ maxNesting
2364
+ };
2365
+ };
2366
+ const findPythonFunctionEnd = (lines, startIndex) => {
2367
+ const baseIndent = lines[startIndex].match(/^(\s*)/)?.[1].length ?? 0;
2368
+ let endLine = startIndex;
2369
+ let maxNesting = 0;
2370
+ const controlIndentStack = [];
2371
+ for (let j = startIndex + 1; j < lines.length; j++) {
2372
+ const l = lines[j];
2373
+ if (l.trim() === "") {
2374
+ endLine = j;
2375
+ continue;
2376
+ }
2377
+ const currentIndent = l.match(/^(\s*)/)?.[1].length ?? 0;
2378
+ if (currentIndent <= baseIndent) break;
2379
+ endLine = j;
2380
+ while (controlIndentStack.length > 0 && currentIndent <= controlIndentStack[controlIndentStack.length - 1]) controlIndentStack.pop();
2381
+ if (PYTHON_CONTROL_FLOW_RE.test(l)) {
2382
+ controlIndentStack.push(currentIndent);
2383
+ const nesting = controlIndentStack.length;
2384
+ if (nesting > maxNesting) maxNesting = nesting;
2385
+ }
2386
+ }
2387
+ return {
2388
+ endLine,
2389
+ maxNesting
2390
+ };
2391
+ };
2392
+ const isDataFile = (content) => {
2393
+ const nonEmpty = content.split("\n").filter((l) => l.trim().length > 0);
2394
+ if (nonEmpty.length === 0) return false;
2395
+ const dataLinePattern = /^\s*[{}[\]"']/;
2396
+ return nonEmpty.filter((l) => dataLinePattern.test(l)).length / nonEmpty.length > .8;
2397
+ };
2398
+ const isBlockArrow = (lines, startIndex) => {
2399
+ if (/=>\s*\{/.test(lines[startIndex])) return true;
2400
+ if (/=>\s*$/.test(lines[startIndex])) {
2401
+ const next = lines[startIndex + 1];
2402
+ if (next && /^\s*\{/.test(next)) return true;
2403
+ }
2404
+ for (let j = startIndex + 1; j < Math.min(startIndex + 3, lines.length); j++) {
2405
+ const l = lines[j];
2406
+ if (l.trim() === "" || /^(?:export\s+)?(?:const|let|var|function|class)\s/.test(l.trim())) break;
2407
+ if (/=>\s*\{/.test(l)) return true;
2408
+ if (/^\s*\{/.test(l)) return true;
2409
+ }
2410
+ return false;
2411
+ };
2412
+ const analyzeFunctions = (content, ext) => {
2413
+ const lines = content.split("\n");
2414
+ const functions = [];
2415
+ for (let i = 0; i < lines.length; i++) {
2416
+ const fnMatch = matchFunctionOnLine(lines[i], ext);
2417
+ if (!fnMatch) continue;
2418
+ const isPython = fnMatch.patternIndex === 2;
2419
+ if (fnMatch.patternIndex === 1 && !isBlockArrow(lines, i)) continue;
2420
+ const { endLine, maxNesting } = findFunctionEnd(lines, i, isPython);
2421
+ functions.push({
2422
+ name: fnMatch.name,
2423
+ startLine: i + 1,
2424
+ lineCount: endLine - i + 1,
2425
+ maxNesting,
2426
+ paramCount: countParams(fnMatch.params)
2427
+ });
2428
+ }
2429
+ return functions;
2430
+ };
2431
+ const checkFileDiagnostics = (relativePath, content, limits) => {
2432
+ const results = [];
2433
+ const lineCount = content.split("\n").length;
2434
+ const ext = path.extname(relativePath).toLowerCase();
2435
+ if (isDataFile(content)) return results;
2436
+ const effectiveMax = ext === ".jsx" || ext === ".tsx" ? limits.maxFileLoc * 2 : limits.maxFileLoc;
2437
+ if (lineCount > effectiveMax) results.push({
2438
+ filePath: relativePath,
2439
+ engine: "code-quality",
2440
+ rule: "complexity/file-too-large",
2441
+ severity: "warning",
2442
+ message: `File has ${lineCount} lines (max: ${effectiveMax})`,
2443
+ help: "Consider splitting this file into smaller modules",
2444
+ line: 0,
2445
+ column: 0,
2446
+ category: "Complexity",
2447
+ fixable: false
2448
+ });
2449
+ return results;
2450
+ };
2451
+ const checkFunctionDiagnostics = (relativePath, fn, limits) => {
2452
+ const results = [];
2453
+ if (fn.lineCount > limits.maxFunctionLoc) results.push({
2454
+ filePath: relativePath,
2455
+ engine: "code-quality",
2456
+ rule: "complexity/function-too-long",
2457
+ severity: "warning",
2458
+ message: `Function '${fn.name}' has ${fn.lineCount} lines (max: ${limits.maxFunctionLoc})`,
2459
+ help: "Consider breaking this function into smaller pieces",
2460
+ line: fn.startLine,
2461
+ column: 0,
2462
+ category: "Complexity",
2463
+ fixable: false
2464
+ });
2465
+ if (fn.maxNesting > limits.maxNesting) results.push({
2466
+ filePath: relativePath,
2467
+ engine: "code-quality",
2468
+ rule: "complexity/deep-nesting",
2469
+ severity: "warning",
2470
+ message: `Function '${fn.name}' has nesting depth ${fn.maxNesting} (max: ${limits.maxNesting})`,
2471
+ help: "Consider using early returns or extracting nested logic",
2472
+ line: fn.startLine,
2473
+ column: 0,
2474
+ category: "Complexity",
2475
+ fixable: false
2476
+ });
2477
+ if (fn.paramCount > limits.maxParams) results.push({
2478
+ filePath: relativePath,
2479
+ engine: "code-quality",
2480
+ rule: "complexity/too-many-params",
2481
+ severity: "warning",
2482
+ message: `Function '${fn.name}' has ${fn.paramCount} parameters (max: ${limits.maxParams})`,
2483
+ help: "Consider using an options object parameter",
2484
+ line: fn.startLine,
2485
+ column: 0,
2486
+ category: "Complexity",
2487
+ fixable: false
2488
+ });
2489
+ return results;
2490
+ };
2491
+ const checkFileComplexity = (filePath, rootDirectory, limits) => {
2492
+ let content;
2493
+ try {
2494
+ content = fs.readFileSync(filePath, "utf-8");
2495
+ } catch {
2496
+ return [];
2497
+ }
2498
+ const relativePath = path.relative(rootDirectory, filePath);
2499
+ const ext = path.extname(filePath).toLowerCase();
2500
+ const diagnostics = checkFileDiagnostics(relativePath, content, limits);
2501
+ for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits));
2502
+ return diagnostics;
2503
+ };
2504
+ const checkComplexity = async (context) => {
2505
+ const files = getSourceFiles(context);
2506
+ const limits = context.config.quality;
2507
+ const diagnostics = [];
2508
+ for (const filePath of files) {
2509
+ if (isAutoGenerated(filePath)) continue;
2510
+ diagnostics.push(...checkFileComplexity(filePath, context.rootDirectory, limits));
2511
+ }
2512
+ return diagnostics;
2513
+ };
2514
+
2515
+ //#endregion
2516
+ //#region src/engines/code-quality/duplication.ts
2517
+ const MIN_DUPLICATE_LINES = 12;
2518
+ const MIN_DUPLICATE_CHARS = 240;
2519
+ const MAX_DUPLICATE_REPORTS = 50;
2520
+ const isIgnorableLine = (line) => {
2521
+ const trimmed = line.trim();
2522
+ return trimmed.length === 0 || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("#");
2523
+ };
2524
+ const normalizeLine = (line) => line.trim().replace(/\s+/g, " ");
2525
+ const extractDuplicateBlocks = (content) => {
2526
+ const blocks = [];
2527
+ const lines = content.split("\n");
2528
+ for (let i = 0; i <= lines.length - MIN_DUPLICATE_LINES; i++) {
2529
+ const segment = lines.slice(i, i + MIN_DUPLICATE_LINES);
2530
+ if (segment.some(isIgnorableLine)) continue;
2531
+ const key = segment.map(normalizeLine).join("\n");
2532
+ if (key.length < MIN_DUPLICATE_CHARS) continue;
2533
+ blocks.push({
2534
+ key,
2535
+ startLine: i + 1
2536
+ });
2537
+ }
2538
+ return blocks;
2539
+ };
2540
+ const checkDuplication = async (context) => {
2541
+ const files = getSourceFiles(context);
2542
+ const duplicates = /* @__PURE__ */ new Map();
2543
+ for (const absoluteFilePath of files) {
2544
+ if (isAutoGenerated(absoluteFilePath)) continue;
2545
+ let content = "";
2546
+ try {
2547
+ content = fs.readFileSync(absoluteFilePath, "utf-8");
2548
+ } catch {
2549
+ continue;
2550
+ }
2551
+ const relativeFilePath = path.relative(context.rootDirectory, absoluteFilePath);
2552
+ for (const block of extractDuplicateBlocks(content)) {
2553
+ const occurrence = {
2554
+ filePath: relativeFilePath,
2555
+ startLine: block.startLine
2556
+ };
2557
+ const list = duplicates.get(block.key) ?? [];
2558
+ list.push(occurrence);
2559
+ duplicates.set(block.key, list);
2560
+ }
2561
+ }
2562
+ const diagnostics = [];
2563
+ const reportedPairs = /* @__PURE__ */ new Set();
2564
+ for (const occurrences of duplicates.values()) {
2565
+ if (occurrences.length < 2) continue;
2566
+ const source = occurrences[0];
2567
+ for (const occurrence of occurrences.slice(1)) {
2568
+ if (diagnostics.length >= MAX_DUPLICATE_REPORTS) return diagnostics;
2569
+ if (occurrence.filePath === source.filePath && occurrence.startLine === source.startLine) continue;
2570
+ if (occurrence.filePath === source.filePath) continue;
2571
+ const pairKey = `${source.filePath}->${occurrence.filePath}`;
2572
+ if (reportedPairs.has(pairKey)) continue;
2573
+ reportedPairs.add(pairKey);
2574
+ diagnostics.push({
2575
+ filePath: occurrence.filePath,
2576
+ engine: "code-quality",
2577
+ rule: "duplication/block",
2578
+ severity: "warning",
2579
+ message: `Possible duplicated code block (${MIN_DUPLICATE_LINES}+ lines) also found at ${source.filePath}:${source.startLine}`,
2580
+ help: "Extract shared logic into a reusable function or module",
2581
+ line: occurrence.startLine,
2582
+ column: 0,
2583
+ category: "Duplication",
2584
+ fixable: false
2585
+ });
2586
+ }
2587
+ }
2588
+ return diagnostics;
2589
+ };
2590
+
2591
+ //#endregion
2592
+ //#region src/engines/code-quality/knip.ts
2593
+ const KNIP_MESSAGE_MAP = {
2594
+ files: "Unused file",
2595
+ exports: "Unused export",
2596
+ types: "Unused type",
2597
+ duplicates: "Duplicate export"
2598
+ };
2599
+ const collectIssues = (fileIssue, issueType, rootDir, knipCwd) => {
2600
+ const diagnostics = [];
2601
+ const issues = issueType === "exports" ? fileIssue.exports ?? [] : issueType === "types" ? fileIssue.types ?? [] : fileIssue.duplicates ?? [];
2602
+ for (const issue of issues) {
2603
+ const symbol = issue.name ?? issue.symbol ?? "unknown";
2604
+ const absolutePath = path.resolve(knipCwd, fileIssue.file);
2605
+ diagnostics.push({
2606
+ filePath: path.relative(rootDir, absolutePath),
2607
+ engine: "code-quality",
2608
+ rule: `knip/${issueType}`,
2609
+ severity: "warning",
2610
+ message: `${KNIP_MESSAGE_MAP[issueType]}: ${symbol}`,
2611
+ help: "",
2612
+ line: issue.line ?? 0,
2613
+ column: issue.col ?? 0,
2614
+ category: "Dead Code",
2615
+ fixable: false
2616
+ });
2617
+ }
2618
+ return diagnostics;
2619
+ };
2620
+ const findMonorepoRoot = (directory) => {
2621
+ let current = path.dirname(directory);
2622
+ while (current !== path.dirname(current)) {
2623
+ if (fs.existsSync(path.join(current, "pnpm-workspace.yaml")) || (() => {
2624
+ const pkgPath = path.join(current, "package.json");
2625
+ if (!fs.existsSync(pkgPath)) return false;
2626
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
2627
+ return Array.isArray(pkg.workspaces) || pkg.workspaces?.packages;
2628
+ })()) return current;
2629
+ current = path.dirname(current);
2630
+ }
2631
+ return null;
2632
+ };
2633
+ const KNIP_RELATIVE_BIN = path.join("node_modules", "knip", "bin", "knip.js");
2634
+ const findKnipBin = (rootDirectory, monorepoRoot) => {
2635
+ const localPath = path.join(rootDirectory, KNIP_RELATIVE_BIN);
2636
+ if (fs.existsSync(localPath)) return {
2637
+ binPath: localPath,
2638
+ cwd: rootDirectory
2639
+ };
2640
+ if (monorepoRoot) {
2641
+ const monorepoPath = path.join(monorepoRoot, KNIP_RELATIVE_BIN);
2642
+ if (fs.existsSync(monorepoPath)) return {
2643
+ binPath: monorepoPath,
2644
+ cwd: monorepoRoot
2645
+ };
2646
+ }
2647
+ return null;
2648
+ };
2649
+ const runKnip = async (rootDirectory) => {
2650
+ const knipRuntime = findKnipBin(rootDirectory, findMonorepoRoot(rootDirectory));
2651
+ if (!knipRuntime) return [];
2652
+ try {
2653
+ const args = [
2654
+ knipRuntime.binPath,
2655
+ "--no-progress",
2656
+ "--reporter",
2657
+ "json",
2658
+ "--no-exit-code"
2659
+ ];
2660
+ const result = await runSubprocess(process.execPath, args, {
2661
+ cwd: knipRuntime.cwd,
2662
+ timeout: 2e4,
2663
+ env: { FORCE_COLOR: "0" }
2664
+ });
2665
+ if (!result.stdout) return [];
2666
+ const parsed = JSON.parse(result.stdout);
2667
+ const diagnostics = [];
2668
+ const files = parsed.files ?? [];
2669
+ for (const unusedFile of files) diagnostics.push({
2670
+ filePath: path.relative(rootDirectory, path.resolve(knipRuntime.cwd, unusedFile)),
2671
+ engine: "code-quality",
2672
+ rule: "knip/files",
2673
+ severity: "warning",
2674
+ message: KNIP_MESSAGE_MAP.files,
2675
+ help: "This file is not imported by any other file in the project.",
2676
+ line: 0,
2677
+ column: 0,
2678
+ category: "Dead Code",
2679
+ fixable: false
2680
+ });
2681
+ const issues = parsed.issues ?? [];
2682
+ for (const fileIssue of issues) for (const type of [
2683
+ "exports",
2684
+ "types",
2685
+ "duplicates"
2686
+ ]) diagnostics.push(...collectIssues(fileIssue, type, rootDirectory, knipRuntime.cwd));
2687
+ return diagnostics;
2688
+ } catch {
2689
+ return [];
2690
+ }
2691
+ };
2692
+
2693
+ //#endregion
2694
+ //#region src/engines/code-quality/index.ts
2695
+ const codeQualityEngine = {
2696
+ name: "code-quality",
2697
+ async run(context) {
2698
+ const diagnostics = [];
2699
+ const promises = [];
2700
+ if (context.languages.includes("typescript") || context.languages.includes("javascript")) promises.push(runKnip(context.rootDirectory));
2701
+ promises.push(checkComplexity(context));
2702
+ promises.push(checkDuplication(context));
2703
+ const results = await Promise.allSettled(promises);
2704
+ for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
2705
+ return {
2706
+ engine: "code-quality",
2707
+ diagnostics,
2708
+ elapsed: 0,
2709
+ skipped: false
2710
+ };
2711
+ }
2712
+ };
2713
+
2714
+ //#endregion
2715
+ //#region src/engines/format/generic.ts
2716
+ const FORMATTERS = {
2717
+ rust: {
2718
+ command: "cargo",
2719
+ checkArgs: ["fmt", "--check"],
2720
+ fixArgs: ["fmt"],
2721
+ parseOutput: (output, _rootDir) => {
2722
+ const diagnostics = [];
2723
+ const lines = output.split("\n").filter((l) => l.startsWith("Diff in"));
2724
+ for (const line of lines) {
2725
+ const match = line.match(/Diff in (.+) at line (\d+)/);
2726
+ if (match) diagnostics.push({
2727
+ filePath: match[1],
2728
+ engine: "format",
2729
+ rule: "rust-formatting",
2730
+ severity: "warning",
2731
+ message: "Rust file is not formatted correctly",
2732
+ help: "Run `aislop fix` to auto-format with rustfmt",
2733
+ line: parseInt(match[2], 10),
2734
+ column: 0,
2735
+ category: "Format",
2736
+ fixable: true
2737
+ });
2738
+ }
2739
+ return diagnostics;
2740
+ }
2741
+ },
2742
+ ruby: {
2743
+ command: "rubocop",
2744
+ checkArgs: [
2745
+ "--format",
2746
+ "json",
2747
+ "--only",
2748
+ "Layout"
2749
+ ],
2750
+ fixArgs: [
2751
+ "--auto-correct",
2752
+ "--only",
2753
+ "Layout"
2754
+ ],
2755
+ parseOutput: (output) => {
2756
+ try {
2757
+ const parsed = JSON.parse(output);
2758
+ const diagnostics = [];
2759
+ for (const file of parsed.files ?? []) for (const offense of file.offenses ?? []) diagnostics.push({
2760
+ filePath: file.path,
2761
+ engine: "format",
2762
+ rule: offense.cop_name ?? "ruby-formatting",
2763
+ severity: "warning",
2764
+ message: offense.message ?? "Ruby formatting issue",
2765
+ help: "Run `aislop fix` to auto-format",
2766
+ line: offense.location?.start_line ?? 0,
2767
+ column: offense.location?.start_column ?? 0,
2768
+ category: "Format",
2769
+ fixable: offense.correctable ?? false
2770
+ });
2771
+ return diagnostics;
2772
+ } catch {
2773
+ return [];
2774
+ }
2775
+ }
2776
+ },
2777
+ php: {
2778
+ command: "php-cs-fixer",
2779
+ checkArgs: [
2780
+ "fix",
2781
+ "--dry-run",
2782
+ "--format=json",
2783
+ "."
2784
+ ],
2785
+ fixArgs: ["fix", "."],
2786
+ parseOutput: (output) => {
2787
+ try {
2788
+ const parsed = JSON.parse(output);
2789
+ const diagnostics = [];
2790
+ for (const file of parsed.files ?? []) diagnostics.push({
2791
+ filePath: file.name,
2792
+ engine: "format",
2793
+ rule: "php-formatting",
2794
+ severity: "warning",
2795
+ message: "PHP file is not formatted correctly",
2796
+ help: "Run `aislop fix` to auto-format",
2797
+ line: 0,
2798
+ column: 0,
2799
+ category: "Format",
2800
+ fixable: true
2801
+ });
2802
+ return diagnostics;
2803
+ } catch {
2804
+ return [];
2805
+ }
2806
+ }
2807
+ }
2808
+ };
2809
+ const runGenericFormatter = async (context, language) => {
2810
+ const config = FORMATTERS[language];
2811
+ if (!config) return [];
2812
+ try {
2813
+ const result = await runSubprocess(config.command, config.checkArgs, {
2814
+ cwd: context.rootDirectory,
2815
+ timeout: 6e4
2816
+ });
2817
+ const output = result.stdout || result.stderr;
2818
+ if (!output) return [];
2819
+ return config.parseOutput(output, context.rootDirectory);
2820
+ } catch {
2821
+ return [];
2822
+ }
2823
+ };
2824
+
2825
+ //#endregion
2826
+ //#region src/engines/format/index.ts
2827
+ const formatEngine = {
2828
+ name: "format",
2829
+ async run(context) {
2830
+ const diagnostics = [];
2831
+ const { languages, installedTools } = context;
2832
+ const promises = [];
2833
+ if (languages.includes("typescript") || languages.includes("javascript")) promises.push(runBiomeFormat(context));
2834
+ if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffFormat(context));
2835
+ if (languages.includes("go") && installedTools["gofmt"]) promises.push(runGofmt(context));
2836
+ if (languages.includes("rust") && installedTools["rustfmt"]) promises.push(runGenericFormatter(context, "rust"));
2837
+ if (languages.includes("ruby") && installedTools["rubocop"]) promises.push(runGenericFormatter(context, "ruby"));
2838
+ if (languages.includes("php") && installedTools["php-cs-fixer"]) promises.push(runGenericFormatter(context, "php"));
2839
+ const results = await Promise.allSettled(promises);
2840
+ for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
2841
+ return {
2842
+ engine: "format",
2843
+ diagnostics,
2844
+ elapsed: 0,
2845
+ skipped: false
2846
+ };
2847
+ }
2848
+ };
2849
+
2850
+ //#endregion
2851
+ //#region src/engines/lint/generic.ts
2852
+ const runGenericLinter = async (context, language) => {
2853
+ switch (language) {
2854
+ case "rust": return runClippy(context);
2855
+ case "ruby": return runRubocop(context);
2856
+ default: return [];
2857
+ }
2858
+ };
2859
+ const runClippy = async (context) => {
2860
+ try {
2861
+ return parseClippyDiagnostics((await runSubprocess("cargo", [
2862
+ "clippy",
2863
+ "--message-format=json",
2864
+ "--quiet"
2865
+ ], {
2866
+ cwd: context.rootDirectory,
2867
+ timeout: 12e4
2868
+ })).stdout);
2869
+ } catch {
2870
+ return [];
2871
+ }
2872
+ };
2873
+ const parseClippyEntry = (line) => {
2874
+ if (!line.startsWith("{")) return null;
2875
+ try {
2876
+ return JSON.parse(line);
2877
+ } catch {
2878
+ return null;
2879
+ }
2880
+ };
2881
+ const toClippyDiagnostic = (entry) => {
2882
+ if (entry.reason !== "compiler-message" || !entry.message) return null;
2883
+ const message = entry.message;
2884
+ const span = message.spans?.[0];
2885
+ return {
2886
+ filePath: span?.file_name ?? "",
2887
+ engine: "lint",
2888
+ rule: `clippy/${message.code?.code ?? "unknown"}`,
2889
+ severity: message.level === "error" ? "error" : "warning",
2890
+ message: message.message ?? "",
2891
+ help: message.children?.[0]?.message ?? "",
2892
+ line: span?.line_start ?? 0,
2893
+ column: span?.column_start ?? 0,
2894
+ category: "Rust Lint",
2895
+ fixable: false
2896
+ };
2897
+ };
2898
+ const parseClippyDiagnostics = (output) => {
2899
+ const diagnostics = [];
2900
+ for (const line of output.split("\n")) {
2901
+ const entry = parseClippyEntry(line);
2902
+ if (!entry) continue;
2903
+ const diagnostic = toClippyDiagnostic(entry);
2904
+ if (diagnostic) diagnostics.push(diagnostic);
2905
+ }
2906
+ return diagnostics;
2907
+ };
2908
+ const runRubocop = async (context) => {
2909
+ try {
2910
+ const output = (await runSubprocess("rubocop", [
2911
+ "--format",
2912
+ "json",
2913
+ "--except",
2914
+ "Layout"
2915
+ ], {
2916
+ cwd: context.rootDirectory,
2917
+ timeout: 6e4
2918
+ })).stdout;
2919
+ if (!output) return [];
2920
+ const parsed = JSON.parse(output);
2921
+ const diagnostics = [];
2922
+ for (const file of parsed.files ?? []) for (const offense of file.offenses ?? []) diagnostics.push({
2923
+ filePath: file.path,
2924
+ engine: "lint",
2925
+ rule: `rubocop/${offense.cop_name}`,
2926
+ severity: offense.severity === "error" || offense.severity === "fatal" ? "error" : "warning",
2927
+ message: offense.message,
2928
+ help: "",
2929
+ line: offense.location?.start_line ?? 0,
2930
+ column: offense.location?.start_column ?? 0,
2931
+ category: "Ruby Lint",
2932
+ fixable: offense.correctable ?? false
2933
+ });
2934
+ return diagnostics;
2935
+ } catch {
2936
+ return [];
2937
+ }
2938
+ };
2939
+
2940
+ //#endregion
2941
+ //#region src/engines/lint/golangci.ts
2942
+ const runGolangciLint = async (context) => {
2943
+ const golangciBinary = resolveToolBinary("golangci-lint");
2944
+ try {
2945
+ const output = (await runSubprocess(golangciBinary, [
2946
+ "run",
2947
+ "--out-format=json",
2948
+ "./..."
2949
+ ], {
2950
+ cwd: context.rootDirectory,
2951
+ timeout: 12e4
2952
+ })).stdout;
2953
+ if (!output) return [];
2954
+ let parsed;
2955
+ try {
2956
+ parsed = JSON.parse(output);
2957
+ } catch {
2958
+ return [];
2959
+ }
2960
+ return (parsed.Issues ?? []).map((issue) => ({
2961
+ filePath: path.relative(context.rootDirectory, issue.Pos.Filename),
2962
+ engine: "lint",
2963
+ rule: `go/${issue.FromLinter}`,
2964
+ severity: "warning",
2965
+ message: issue.Text,
2966
+ help: "",
2967
+ line: issue.Pos.Line,
2968
+ column: issue.Pos.Column,
2969
+ category: "Go Lint",
2970
+ fixable: false
2971
+ }));
2972
+ } catch {
2973
+ return [];
2974
+ }
2975
+ };
2976
+
2977
+ //#endregion
2978
+ //#region src/engines/lint/index.ts
2979
+ const lintEngine = {
2980
+ name: "lint",
2981
+ async run(context) {
2982
+ const diagnostics = [];
2983
+ const { languages, installedTools } = context;
2984
+ const promises = [];
2985
+ if (languages.includes("typescript") || languages.includes("javascript")) promises.push(runOxlint(context));
2986
+ if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-CGXGLgMJ.js").then((mod) => mod.runExpoDoctor(context)));
2987
+ if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
2988
+ if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
2989
+ if (languages.includes("rust") && installedTools["cargo"]) promises.push(runGenericLinter(context, "rust"));
2990
+ if (languages.includes("ruby") && installedTools["rubocop"]) promises.push(runGenericLinter(context, "ruby"));
2991
+ const results = await Promise.allSettled(promises);
2992
+ for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
2993
+ return {
2994
+ engine: "lint",
2995
+ diagnostics,
2996
+ elapsed: 0,
2997
+ skipped: false
2998
+ };
2999
+ }
3000
+ };
3001
+
3002
+ //#endregion
3003
+ //#region src/engines/security/audit.ts
3004
+ const runDependencyAudit = async (context) => {
3005
+ const diagnostics = [];
3006
+ const timeout = context.config.security.auditTimeout;
3007
+ const promises = [];
3008
+ if (context.languages.includes("typescript") || context.languages.includes("javascript")) {
3009
+ if (fs.existsSync(path.join(context.rootDirectory, "pnpm-lock.yaml"))) promises.push(runPnpmAudit(context.rootDirectory, timeout));
3010
+ else if (fs.existsSync(path.join(context.rootDirectory, "package-lock.json")) || fs.existsSync(path.join(context.rootDirectory, "package.json"))) promises.push(runNpmAudit(context.rootDirectory, timeout));
3011
+ }
3012
+ if (context.languages.includes("python") && context.installedTools["pip-audit"]) promises.push(runPipAudit(context.rootDirectory, timeout));
3013
+ if (context.languages.includes("go") && context.installedTools["govulncheck"]) promises.push(runGovulncheck(context.rootDirectory, timeout));
3014
+ if (context.languages.includes("rust")) promises.push(runCargoAudit(context.rootDirectory, timeout));
3015
+ const results = await Promise.allSettled(promises);
3016
+ for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
3017
+ return diagnostics;
3018
+ };
3019
+ const runNpmAudit = async (rootDir, timeout) => {
3020
+ try {
3021
+ return parseJsAudit((await runSubprocess("npm", ["audit", "--json"], {
3022
+ cwd: rootDir,
3023
+ timeout
3024
+ })).stdout, "npm audit");
3025
+ } catch {
3026
+ return [];
3027
+ }
3028
+ };
3029
+ const runPnpmAudit = async (rootDir, timeout) => {
3030
+ try {
3031
+ return parseJsAudit((await runSubprocess("pnpm", ["audit", "--json"], {
3032
+ cwd: rootDir,
3033
+ timeout
3034
+ })).stdout, "pnpm audit");
3035
+ } catch {
3036
+ return [];
3037
+ }
3038
+ };
3039
+ const toSeverity = (value) => value === "critical" || value === "high" ? "error" : "warning";
3040
+ const defaultAuditFixCommand = (source) => source === "pnpm audit" ? "pnpm audit --fix" : "npm audit fix";
3041
+ const parseLegacyAdvisories = (advisories, source) => {
3042
+ const diagnostics = [];
3043
+ for (const [key, advisory] of Object.entries(advisories)) {
3044
+ const packageName = advisory.module_name ?? advisory.name ?? advisory.package ?? key;
3045
+ const severity = (advisory.severity ?? "moderate").toLowerCase();
3046
+ const recommendation = advisory.recommendation ?? advisory.title ?? `Run \`${defaultAuditFixCommand(source)}\` to resolve`;
3047
+ diagnostics.push({
3048
+ filePath: "package.json",
3049
+ engine: "security",
3050
+ rule: "security/vulnerable-dependency",
3051
+ severity: toSeverity(severity),
3052
+ message: `Vulnerable dependency (${source}): ${packageName} (${severity})`,
3053
+ help: recommendation,
3054
+ line: 0,
3055
+ column: 0,
3056
+ category: "Security",
3057
+ fixable: false
3058
+ });
3059
+ }
3060
+ return diagnostics;
3061
+ };
3062
+ const parseModernVulnerabilities = (vulnerabilities, source) => {
3063
+ const diagnostics = [];
3064
+ for (const [packageName, vulnerability] of Object.entries(vulnerabilities)) {
3065
+ const severity = (vulnerability.severity ?? "moderate").toLowerCase();
3066
+ const fixAvailable = vulnerability.fixAvailable;
3067
+ let recommendation = `Run \`${defaultAuditFixCommand(source)}\` to resolve`;
3068
+ if (fixAvailable === false) recommendation = "No automatic fix available.";
3069
+ else if (fixAvailable && typeof fixAvailable === "object" && "name" in fixAvailable && "version" in fixAvailable) {
3070
+ const target = fixAvailable;
3071
+ if (target.name && target.version) recommendation = `Upgrade to ${target.name}@${target.version}.`;
3072
+ }
3073
+ diagnostics.push({
3074
+ filePath: "package.json",
3075
+ engine: "security",
3076
+ rule: "security/vulnerable-dependency",
3077
+ severity: toSeverity(severity),
3078
+ message: `Vulnerable dependency (${source}): ${packageName} (${severity})`,
3079
+ help: recommendation,
3080
+ line: 0,
3081
+ column: 0,
3082
+ category: "Security",
3083
+ fixable: false
3084
+ });
3085
+ }
3086
+ return diagnostics;
3087
+ };
3088
+ const parseJsAudit = (output, source) => {
3089
+ if (!output) return [];
3090
+ try {
3091
+ const parsed = JSON.parse(output);
3092
+ const error = parsed.error;
3093
+ if (error?.code === "ENOLOCK") return [{
3094
+ filePath: "package.json",
3095
+ engine: "security",
3096
+ rule: "security/dependency-audit-skipped",
3097
+ severity: "info",
3098
+ message: `Dependency audit skipped (${source}): lockfile is missing`,
3099
+ help: error.detail ?? "Generate a lockfile, then re-run `aislop scan` for dependency vulnerability checks.",
3100
+ line: 0,
3101
+ column: 0,
3102
+ category: "Security",
3103
+ fixable: false
3104
+ }];
3105
+ if (error?.summary || error?.code) return [{
3106
+ filePath: "package.json",
3107
+ engine: "security",
3108
+ rule: "security/dependency-audit-skipped",
3109
+ severity: "info",
3110
+ message: `Dependency audit did not complete (${source})`,
3111
+ help: error.detail ?? error.summary ?? "Re-run dependency audit directly to inspect the underlying error.",
3112
+ line: 0,
3113
+ column: 0,
3114
+ category: "Security",
3115
+ fixable: false
3116
+ }];
3117
+ const advisories = parsed.advisories;
3118
+ if (advisories && typeof advisories === "object") return parseLegacyAdvisories(advisories, source);
3119
+ const vulnerabilities = parsed.vulnerabilities;
3120
+ if (vulnerabilities && typeof vulnerabilities === "object") return parseModernVulnerabilities(vulnerabilities, source);
3121
+ return [];
3122
+ } catch {
3123
+ return [];
3124
+ }
3125
+ };
3126
+ const runPipAudit = async (rootDir, timeout) => {
3127
+ try {
3128
+ const result = await runSubprocess("pip-audit", ["--format=json"], {
3129
+ cwd: rootDir,
3130
+ timeout
3131
+ });
3132
+ if (!result.stdout) return [];
3133
+ return (JSON.parse(result.stdout).dependencies ?? []).filter((d) => Array.isArray(d.vulns) && d.vulns.length > 0).map((d) => ({
3134
+ filePath: "requirements.txt",
3135
+ engine: "security",
3136
+ rule: "security/vulnerable-dependency",
3137
+ severity: "error",
3138
+ message: `Vulnerable Python dependency: ${d.name}`,
3139
+ help: `Upgrade ${d.name} to fix known vulnerabilities`,
3140
+ line: 0,
3141
+ column: 0,
3142
+ category: "Security",
3143
+ fixable: false
3144
+ }));
3145
+ } catch {
3146
+ return [];
3147
+ }
3148
+ };
3149
+ const runGovulncheck = async (rootDir, timeout) => {
3150
+ try {
3151
+ const result = await runSubprocess("govulncheck", ["-json", "./..."], {
3152
+ cwd: rootDir,
3153
+ timeout
3154
+ });
3155
+ if (!result.stdout) return [];
3156
+ return parseGovulncheckOutput(result.stdout);
3157
+ } catch {
3158
+ return [];
3159
+ }
3160
+ };
3161
+ const toGovulnDiagnostic = (entry) => {
3162
+ if (!entry.vulnerability) return null;
3163
+ return {
3164
+ filePath: "go.mod",
3165
+ engine: "security",
3166
+ rule: "security/vulnerable-dependency",
3167
+ severity: "error",
3168
+ message: `Go vulnerability: ${entry.vulnerability.id ?? "unknown"}`,
3169
+ help: entry.vulnerability.details ?? "",
3170
+ line: 0,
3171
+ column: 0,
3172
+ category: "Security",
3173
+ fixable: false
3174
+ };
3175
+ };
3176
+ const parseGovulncheckOutput = (output) => {
3177
+ const diagnostics = [];
3178
+ for (const line of output.split("\n")) {
3179
+ if (!line.startsWith("{")) continue;
3180
+ let parsed = null;
3181
+ try {
3182
+ parsed = JSON.parse(line);
3183
+ } catch {
3184
+ parsed = null;
3185
+ }
3186
+ if (!parsed) continue;
3187
+ const diagnostic = toGovulnDiagnostic(parsed);
3188
+ if (diagnostic) diagnostics.push(diagnostic);
3189
+ }
3190
+ return diagnostics;
3191
+ };
3192
+ const runCargoAudit = async (rootDir, timeout) => {
3193
+ try {
3194
+ const result = await runSubprocess("cargo", ["audit", "--json"], {
3195
+ cwd: rootDir,
3196
+ timeout
3197
+ });
3198
+ if (!result.stdout) return [];
3199
+ return (JSON.parse(result.stdout).vulnerabilities?.list ?? []).map((v) => ({
3200
+ filePath: "Cargo.toml",
3201
+ engine: "security",
3202
+ rule: "security/vulnerable-dependency",
3203
+ severity: "error",
3204
+ message: `Rust vulnerability: ${v.advisory?.id ?? "unknown"}`,
3205
+ help: v.advisory?.title ?? "",
3206
+ line: 0,
3207
+ column: 0,
3208
+ category: "Security",
3209
+ fixable: false
3210
+ }));
3211
+ } catch {
3212
+ return [];
3213
+ }
3214
+ };
3215
+
3216
+ //#endregion
3217
+ //#region src/engines/security/risky.ts
3218
+ const ev = "eval";
3219
+ const Fn = "Function";
3220
+ const RISKY_PATTERNS = [
3221
+ {
3222
+ pattern: new RegExp(`\\b${ev}\\s*\\(`, "g"),
3223
+ extensions: [
3224
+ ".ts",
3225
+ ".tsx",
3226
+ ".js",
3227
+ ".jsx",
3228
+ ".mjs",
3229
+ ".cjs",
3230
+ ".py",
3231
+ ".rb",
3232
+ ".php"
3233
+ ],
3234
+ name: "eval",
3235
+ message: `Use of ${ev}() is a security risk`,
3236
+ help: `Avoid ${ev} — use safer alternatives like JSON.parse, Function constructor, or AST-based approaches`
3237
+ },
3238
+ {
3239
+ pattern: new RegExp(`new\\s+${Fn}\\s*\\(`, "g"),
3240
+ extensions: [
3241
+ ".ts",
3242
+ ".tsx",
3243
+ ".js",
3244
+ ".jsx",
3245
+ ".mjs",
3246
+ ".cjs"
3247
+ ],
3248
+ name: "new-function",
3249
+ message: `Use of new ${Fn}() is similar to ${ev} and can be a security risk`,
3250
+ help: "Avoid dynamic code execution — refactor to use static code paths"
3251
+ },
3252
+ {
3253
+ pattern: /\.innerHTML\s*=/g,
3254
+ extensions: [
3255
+ ".ts",
3256
+ ".tsx",
3257
+ ".js",
3258
+ ".jsx"
3259
+ ],
3260
+ name: "innerhtml",
3261
+ message: "Direct innerHTML assignment can lead to XSS",
3262
+ help: "Use textContent, DOM APIs, or a sanitization library instead"
3263
+ },
3264
+ {
3265
+ pattern: /dangerouslySetInnerHTML/g,
3266
+ extensions: [".tsx", ".jsx"],
3267
+ name: "dangerously-set-innerhtml",
3268
+ message: "dangerouslySetInnerHTML can lead to XSS if not sanitized",
3269
+ help: "Ensure the HTML is sanitized with DOMPurify or similar before rendering"
3270
+ },
3271
+ {
3272
+ pattern: /pickle\.loads?\s*\(/g,
3273
+ extensions: [".py"],
3274
+ name: "pickle-load",
3275
+ message: "pickle.load can execute arbitrary code — unsafe deserialization",
3276
+ help: "Use JSON, MessagePack, or other safe serialization formats for untrusted data"
3277
+ },
3278
+ {
3279
+ pattern: new RegExp(`\\bexec\\s*\\(`, "g"),
3280
+ extensions: [".py"],
3281
+ name: "python-exec",
3282
+ message: "Use of exec() can execute arbitrary code",
3283
+ help: "Avoid exec — use safer alternatives"
3284
+ },
3285
+ {
3286
+ pattern: /(?:child_process|subprocess|os\.system|exec|spawn)\s*\([^)]*\$\{/g,
3287
+ extensions: [
3288
+ ".ts",
3289
+ ".tsx",
3290
+ ".js",
3291
+ ".jsx",
3292
+ ".mjs",
3293
+ ".cjs",
3294
+ ".py"
3295
+ ],
3296
+ name: "shell-injection",
3297
+ message: "Possible shell injection — user input in command execution",
3298
+ help: "Use parameterized commands or a safe shell execution library"
3299
+ },
3300
+ {
3301
+ pattern: /(?:query|execute|raw)\s*\(\s*`[^`]*\$\{/g,
3302
+ extensions: [
3303
+ ".ts",
3304
+ ".tsx",
3305
+ ".js",
3306
+ ".jsx",
3307
+ ".mjs",
3308
+ ".cjs"
3309
+ ],
3310
+ name: "sql-injection",
3311
+ message: "Possible SQL injection — template literal in query",
3312
+ help: "Use parameterized queries or an ORM instead of string interpolation"
3313
+ },
3314
+ {
3315
+ pattern: /(?:query|execute|raw)\s*\(\s*["'][^"']*["']\s*\+/g,
3316
+ extensions: [
3317
+ ".ts",
3318
+ ".tsx",
3319
+ ".js",
3320
+ ".jsx",
3321
+ ".mjs",
3322
+ ".cjs"
3323
+ ],
3324
+ name: "sql-injection",
3325
+ message: "Possible SQL injection — string concatenation in query",
3326
+ help: "Use parameterized queries or an ORM instead of string concatenation"
3327
+ }
3328
+ ];
3329
+ const detectRiskyConstructs = async (context) => {
3330
+ const files = getSourceFiles(context);
3331
+ const diagnostics = [];
3332
+ for (const filePath of files) {
3333
+ const ext = path.extname(filePath);
3334
+ let content;
3335
+ try {
3336
+ content = fs.readFileSync(filePath, "utf-8");
3337
+ } catch {
3338
+ continue;
3339
+ }
3340
+ const relativePath = path.relative(context.rootDirectory, filePath);
3341
+ const normalizedPath = relativePath.split(path.sep).join("/");
3342
+ const isMigrationOrSeeder = /(?:^|\/)(migrations|seeders|seeds|migrate)\//.test(normalizedPath);
3343
+ for (const { pattern, extensions, name, message, help } of RISKY_PATTERNS) {
3344
+ if (!extensions.includes(ext)) continue;
3345
+ if (isMigrationOrSeeder && name === "sql-injection") continue;
3346
+ const regex = new RegExp(pattern.source, pattern.flags);
3347
+ let match;
3348
+ while ((match = regex.exec(content)) !== null) {
3349
+ const line = content.slice(0, match.index).split("\n").length;
3350
+ if (name === "sql-injection") {
3351
+ const afterMatch = content.slice(match.index + match[0].length, match.index + match[0].length + 100);
3352
+ if (/^(?:\w+\.join\s*\(|[A-Z_]+\}|tableName\}|table\})/.test(afterMatch)) continue;
3353
+ }
3354
+ diagnostics.push({
3355
+ filePath: relativePath,
3356
+ engine: "security",
3357
+ rule: `security/${name}`,
3358
+ severity: "error",
3359
+ message,
3360
+ help,
3361
+ line,
3362
+ column: 0,
3363
+ category: "Security",
3364
+ fixable: false
3365
+ });
3366
+ }
3367
+ }
3368
+ }
3369
+ return diagnostics;
3370
+ };
3371
+
3372
+ //#endregion
3373
+ //#region src/engines/security/secrets.ts
3374
+ const SECRET_PATTERNS = [
3375
+ {
3376
+ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
3377
+ name: "API key"
3378
+ },
3379
+ {
3380
+ pattern: /AKIA[0-9A-Z]{16}/g,
3381
+ name: "AWS Access Key"
3382
+ },
3383
+ {
3384
+ pattern: /(?:aws[_-]?secret|secret[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
3385
+ name: "AWS Secret Key"
3386
+ },
3387
+ {
3388
+ pattern: /(?:password|passwd|pwd|secret)\s*[:=]\s*["']([^"']{8,})["']/gi,
3389
+ name: "Hardcoded password/secret"
3390
+ },
3391
+ {
3392
+ pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
3393
+ name: "Private key"
3394
+ },
3395
+ {
3396
+ pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
3397
+ name: "JWT token"
3398
+ },
3399
+ {
3400
+ pattern: /(?:token|bearer)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
3401
+ name: "Authentication token"
3402
+ },
3403
+ {
3404
+ pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/g,
3405
+ name: "GitHub token"
3406
+ },
3407
+ {
3408
+ pattern: /xox[baprs]-[A-Za-z0-9-]+/g,
3409
+ name: "Slack token"
3410
+ },
3411
+ {
3412
+ pattern: /(?:mongodb|postgres|mysql|redis):\/\/[^"'\s]+:[^"'\s]+@/gi,
3413
+ name: "Database connection string with credentials"
3414
+ }
3415
+ ];
3416
+ const PLACEHOLDER_EXACT = new Set([
3417
+ "changeme",
3418
+ "password",
3419
+ "secret",
3420
+ "xxx",
3421
+ "todo",
3422
+ "replace_me"
3423
+ ]);
3424
+ const isPlaceholderValue = (matchedText) => {
3425
+ if (/env\(/i.test(matchedText)) return true;
3426
+ if (matchedText.includes("process.env")) return true;
3427
+ if (matchedText.includes("os.environ")) return true;
3428
+ if (matchedText.includes("${")) return true;
3429
+ if (matchedText.includes("<") && matchedText.includes(">")) return true;
3430
+ if (/^your_/i.test(matchedText)) return true;
3431
+ if (PLACEHOLDER_EXACT.has(matchedText.toLowerCase())) return true;
3432
+ return false;
3433
+ };
3434
+ const scanSecrets = async (context) => {
3435
+ const files = getSourceFilesWithExtras(context, [
3436
+ ".env",
3437
+ ".yaml",
3438
+ ".yml",
3439
+ ".json",
3440
+ ".toml"
3441
+ ]);
3442
+ const diagnostics = [];
3443
+ for (const filePath of files) {
3444
+ let content;
3445
+ try {
3446
+ content = fs.readFileSync(filePath, "utf-8");
3447
+ } catch {
3448
+ continue;
3449
+ }
3450
+ const relativePath = path.relative(context.rootDirectory, filePath);
3451
+ for (const { pattern, name } of SECRET_PATTERNS) {
3452
+ const regex = new RegExp(pattern.source, pattern.flags);
3453
+ let match;
3454
+ while ((match = regex.exec(content)) !== null) {
3455
+ if (isPlaceholderValue(match[1] ?? match[0])) continue;
3456
+ const line = content.slice(0, match.index).split("\n").length;
3457
+ diagnostics.push({
3458
+ filePath: relativePath,
3459
+ engine: "security",
3460
+ rule: "security/hardcoded-secret",
3461
+ severity: "error",
3462
+ message: `Possible ${name} detected in source code`,
3463
+ help: "Move secrets to environment variables or a secrets manager",
3464
+ line,
3465
+ column: 0,
3466
+ category: "Security",
3467
+ fixable: false
3468
+ });
3469
+ }
3470
+ }
3471
+ }
3472
+ return diagnostics;
3473
+ };
3474
+
3475
+ //#endregion
3476
+ //#region src/engines/security/index.ts
3477
+ const securityEngine = {
3478
+ name: "security",
3479
+ async run(context) {
3480
+ const diagnostics = [];
3481
+ const promises = [scanSecrets(context), detectRiskyConstructs(context)];
3482
+ if (context.config.security.audit) promises.push(runDependencyAudit(context));
3483
+ const results = await Promise.allSettled(promises);
3484
+ for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
3485
+ return {
3486
+ engine: "security",
3487
+ diagnostics,
3488
+ elapsed: 0,
3489
+ skipped: false
3490
+ };
3491
+ }
3492
+ };
3493
+
3494
+ //#endregion
3495
+ //#region src/engines/orchestrator.ts
3496
+ const ALL_ENGINES = [
3497
+ formatEngine,
3498
+ lintEngine,
3499
+ codeQualityEngine,
3500
+ aiSlopEngine,
3501
+ architectureEngine,
3502
+ securityEngine
3503
+ ];
3504
+ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
3505
+ const engines = ALL_ENGINES.filter((e) => enabledEngines[e.name] !== false);
3506
+ return (await Promise.allSettled(engines.map(async (engine) => {
3507
+ onStart?.(engine.name);
3508
+ const start = performance.now();
3509
+ try {
3510
+ const result = await engine.run(context);
3511
+ result.elapsed = performance.now() - start;
3512
+ onComplete?.(result);
3513
+ return result;
3514
+ } catch (error) {
3515
+ const result = {
3516
+ engine: engine.name,
3517
+ diagnostics: [],
3518
+ elapsed: performance.now() - start,
3519
+ skipped: true,
3520
+ skipReason: error instanceof Error ? error.message : String(error)
3521
+ };
3522
+ onComplete?.(result);
3523
+ return result;
3524
+ }
3525
+ }))).map((r, i) => r.status === "fulfilled" ? r.value : {
3526
+ engine: engines[i].name,
3527
+ diagnostics: [],
3528
+ elapsed: 0,
3529
+ skipped: true,
3530
+ skipReason: r.reason instanceof Error ? r.reason.message : String(r.reason)
3531
+ });
3532
+ };
3533
+
3534
+ //#endregion
3535
+ //#region src/output/scan-progress.ts
3536
+ const SPINNER_FRAMES = [
3537
+ "⠋",
3538
+ "⠙",
3539
+ "⠹",
3540
+ "⠸",
3541
+ "⠼",
3542
+ "⠴",
3543
+ "⠦",
3544
+ "⠧",
3545
+ "⠇",
3546
+ "⠏"
3547
+ ];
3548
+ const shouldRenderLiveScanProgress = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
3549
+ const formatElapsed = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
3550
+ const truncateText = (text, maxLength = 52) => text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}…`;
3551
+ const getIssueSummary = (result) => {
3552
+ const errors = result.diagnostics.filter((d) => d.severity === "error").length;
3553
+ const warnings = result.diagnostics.filter((d) => d.severity === "warning").length;
3554
+ if (errors === 0 && warnings === 0) return `Done (0 issues, ${formatElapsed(result.elapsed)})`;
3555
+ const parts = [];
3556
+ if (errors > 0) parts.push(`${errors} error${errors === 1 ? "" : "s"}`);
3557
+ if (warnings > 0) parts.push(`${warnings} warning${warnings === 1 ? "" : "s"}`);
3558
+ return `Done (${parts.join(", ")}, ${formatElapsed(result.elapsed)})`;
3559
+ };
3560
+ const getStatusParts = (state, frameIndex) => {
3561
+ if (state.status === "running") return {
3562
+ icon: highlighter.info(SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length]),
3563
+ detail: highlighter.info("Running")
3564
+ };
3565
+ if (state.status === "done") {
3566
+ const result = state.result;
3567
+ if (!result) return {
3568
+ icon: highlighter.success("✓"),
3569
+ detail: highlighter.dim("Done")
3570
+ };
3571
+ const hasErrors = result.diagnostics.some((d) => d.severity === "error");
3572
+ const hasWarnings = result.diagnostics.some((d) => d.severity === "warning");
3573
+ let icon = highlighter.success("✓");
3574
+ if (hasErrors) icon = highlighter.error("✗");
3575
+ else if (hasWarnings) icon = highlighter.warn("!");
3576
+ return {
3577
+ icon,
3578
+ detail: highlighter.dim(getIssueSummary(result))
3579
+ };
3580
+ }
3581
+ if (state.status === "skipped") {
3582
+ const reason = state.result?.skipReason?.split("\n").find((line) => line.trim().length > 0) ?? "Skipped";
3583
+ return {
3584
+ icon: highlighter.warn("!"),
3585
+ detail: highlighter.dim(`Skipped (${truncateText(reason)})`)
3586
+ };
3587
+ }
3588
+ return {
3589
+ icon: highlighter.dim("○"),
3590
+ detail: highlighter.dim("Waiting")
3591
+ };
3592
+ };
3593
+ const renderScanProgressBlock = (states, frameIndex) => {
3594
+ if (states.length === 0) return ` ${highlighter.bold("Engines 0/0")} ${highlighter.dim("nothing to run")}\n`;
3595
+ const completedCount = states.filter((state) => state.status === "done" || state.status === "skipped").length;
3596
+ const runningCount = states.filter((state) => state.status === "running").length;
3597
+ const labelWidth = Math.max(...states.map((state) => getEngineLabel(state.engine).length));
3598
+ const headingStatus = completedCount === states.length ? highlighter.dim("complete") : runningCount > 0 ? highlighter.dim(`${runningCount} running`) : highlighter.dim("starting");
3599
+ return `${[` ${highlighter.bold(`Engines ${completedCount}/${states.length}`)} ${headingStatus}`, ...states.map((state) => {
3600
+ const label = getEngineLabel(state.engine).padEnd(labelWidth, " ");
3601
+ const { icon, detail } = getStatusParts(state, frameIndex);
3602
+ return ` ${icon} ${label} ${detail}`;
3603
+ })].join("\n")}\n`;
3604
+ };
3605
+ const clearRenderedLines = (lineCount) => {
3606
+ if (lineCount === 0) return;
3607
+ process.stderr.write(`\u001B[${lineCount}F`);
3608
+ for (let index = 0; index < lineCount; index += 1) {
3609
+ process.stderr.write("\x1B[2K");
3610
+ if (index < lineCount - 1) process.stderr.write("\x1B[1E");
3611
+ }
3612
+ if (lineCount > 1) process.stderr.write(`\u001B[${lineCount - 1}F`);
3613
+ };
3614
+ var ScanProgressRenderer = class {
3615
+ states;
3616
+ previousLineCount = 0;
3617
+ frameIndex = 0;
3618
+ timer;
3619
+ constructor(engines) {
3620
+ this.states = engines.map((engine) => ({
3621
+ engine,
3622
+ status: "pending"
3623
+ }));
3624
+ }
3625
+ start() {
3626
+ if (!shouldRenderLiveScanProgress()) return;
3627
+ this.render();
3628
+ this.timer = setInterval(() => {
3629
+ this.frameIndex += 1;
3630
+ this.render();
3631
+ }, 100);
3632
+ this.timer.unref();
3633
+ }
3634
+ markStarted(engine) {
3635
+ const state = this.states.find((entry) => entry.engine === engine);
3636
+ if (!state) return;
3637
+ state.status = "running";
3638
+ this.render();
3639
+ }
3640
+ markComplete(result) {
3641
+ const state = this.states.find((entry) => entry.engine === result.engine);
3642
+ if (!state) return;
3643
+ state.status = result.skipped ? "skipped" : "done";
3644
+ state.result = result;
3645
+ this.render();
3646
+ }
3647
+ stop() {
3648
+ if (this.timer) {
3649
+ clearInterval(this.timer);
3650
+ this.timer = void 0;
3651
+ }
3652
+ if (!shouldRenderLiveScanProgress()) return;
3653
+ this.render();
3654
+ }
3655
+ render() {
3656
+ if (!shouldRenderLiveScanProgress()) return;
3657
+ if (this.previousLineCount > 0) clearRenderedLines(this.previousLineCount);
3658
+ const output = renderScanProgressBlock(this.states, this.frameIndex);
3659
+ process.stderr.write(output);
3660
+ this.previousLineCount = output.split("\n").length - 1;
3661
+ }
3662
+ };
3663
+
3664
+ //#endregion
3665
+ //#region src/scoring/index.ts
3666
+ const PERFECT_SCORE$1 = 100;
3667
+ const calculateScore = (diagnostics, weights, thresholds) => {
3668
+ if (diagnostics.length === 0) return {
3669
+ score: PERFECT_SCORE$1,
3670
+ label: "Healthy"
3671
+ };
3672
+ let deductions = 0;
3673
+ for (const d of diagnostics) {
3674
+ const engineWeight = weights[d.engine] ?? 1;
3675
+ const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
3676
+ deductions += severityPenalty * engineWeight;
3677
+ }
3678
+ const score = Math.max(0, Math.round(PERFECT_SCORE$1 - PERFECT_SCORE$1 * Math.log1p(deductions) / Math.log1p(PERFECT_SCORE$1 + deductions)));
3679
+ return {
3680
+ score,
3681
+ label: score >= thresholds.good ? "Healthy" : score >= thresholds.ok ? "Needs Work" : "Critical"
3682
+ };
3683
+ };
3684
+ const getScoreColor = (score, thresholds) => {
3685
+ if (score >= thresholds.good) return "success";
3686
+ if (score >= thresholds.ok) return "warn";
3687
+ return "error";
3688
+ };
3689
+
3690
+ //#endregion
3691
+ //#region src/output/terminal.ts
3692
+ const PERFECT_SCORE = 100;
3693
+ const groupBy = (items, key) => {
3694
+ const map = /* @__PURE__ */ new Map();
3695
+ for (const item of items) {
3696
+ const k = key(item);
3697
+ const group = map.get(k) ?? [];
3698
+ group.push(item);
3699
+ map.set(k, group);
3700
+ }
3701
+ return map;
3702
+ };
3703
+ const colorBySeverity = (text, severity) => severity === "error" ? highlighter.error(text) : highlighter.warn(text);
3704
+ const colorByScore = (text, score, thresholds) => {
3705
+ return highlighter[getScoreColor(score, thresholds)](text);
3706
+ };
3707
+ const toElapsedLabel = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
3708
+ const toSeverityLabel = (severity) => {
3709
+ if (severity === "error") return "ERROR";
3710
+ if (severity === "warning") return "WARN";
3711
+ return "INFO";
3712
+ };
3713
+ const toLocationLabel = (diagnostic) => {
3714
+ const line = diagnostic.line > 0 ? `:${diagnostic.line}` : "";
3715
+ const column = diagnostic.column > 0 ? `:${diagnostic.column}` : "";
3716
+ return `${diagnostic.filePath}${line}${column}`;
3717
+ };
3718
+ const renderDiagnostics = (diagnostics, verbose) => {
3719
+ const lines = [];
3720
+ const byEngine = groupBy(diagnostics, (d) => d.engine);
3721
+ for (const [engine, engineDiags] of byEngine) {
3722
+ const label = getEngineLabel(engine);
3723
+ lines.push(` ${highlighter.bold(`➤ ${label}`)}`);
3724
+ const sorted = [...groupBy(engineDiags, (d) => `${d.rule}:${d.message}`).entries()].sort(([, a], [, b]) => {
3725
+ return (a[0].severity === "error" ? 0 : a[0].severity === "warning" ? 1 : 2) - (b[0].severity === "error" ? 0 : b[0].severity === "warning" ? 1 : 2);
3726
+ });
3727
+ for (const [, ruleDiags] of sorted) {
3728
+ const first = ruleDiags[0];
3729
+ const level = toSeverityLabel(first.severity);
3730
+ const count = ruleDiags.length > 1 ? ` (${ruleDiags.length})` : "";
3731
+ const status = colorBySeverity(level, first.severity);
3732
+ lines.push(` [${status}] ${first.message}${count}`);
3733
+ const locations = verbose ? ruleDiags : ruleDiags.slice(0, 3);
3734
+ for (const diagnostic of locations) lines.push(highlighter.dim(` ${toLocationLabel(diagnostic)}`));
3735
+ if (!verbose && ruleDiags.length > locations.length) lines.push(highlighter.dim(` +${ruleDiags.length - locations.length} more location(s), use -d for full list`));
3736
+ if (first.help) lines.push(highlighter.dim(` ${first.help}`));
3737
+ lines.push("");
3738
+ }
3739
+ }
3740
+ return `${lines.join("\n")}\n`;
3741
+ };
3742
+ const renderSummary = (diagnostics, scoreResult, elapsedMs, fileCount, thresholds) => {
3743
+ const errorCount = diagnostics.filter((d) => d.severity === "error").length;
3744
+ const warningCount = diagnostics.filter((d) => d.severity === "warning").length;
3745
+ const fixableCount = diagnostics.filter((d) => d.fixable).length;
3746
+ const elapsed = toElapsedLabel(elapsedMs);
3747
+ return `${[
3748
+ highlighter.dim("------------------------------------------------------------"),
3749
+ highlighter.bold("Summary"),
3750
+ ` Score: ${colorByScore(`${scoreResult.score}/${PERFECT_SCORE}`, scoreResult.score, thresholds)} ${colorByScore(`(${scoreResult.label})`, scoreResult.score, thresholds)}`,
3751
+ ` Issues: ${highlighter.error(`${errorCount} error${errorCount === 1 ? "" : "s"}`)}, ${highlighter.warn(`${warningCount} warning${warningCount === 1 ? "" : "s"}`)}`,
3752
+ ` Auto-fixable: ${highlighter.info(String(fixableCount))}`,
3753
+ ` Files: ${highlighter.info(String(fileCount))}`,
3754
+ ` Time: ${highlighter.info(elapsed)}`,
3755
+ highlighter.dim("------------------------------------------------------------")
3756
+ ].join("\n")}\n`;
3757
+ };
3758
+ const printEngineStatus = (result) => {
3759
+ const label = getEngineLabel(result.engine);
3760
+ const elapsed = toElapsedLabel(result.elapsed);
3761
+ if (result.skipped) logger.warn(` ! ${label}: skipped${result.skipReason ? ` (${result.skipReason})` : ""}`);
3762
+ else if (result.diagnostics.length === 0) logger.success(` ✓ ${label}: done (0 issues, ${elapsed})`);
3763
+ else {
3764
+ const errors = result.diagnostics.filter((d) => d.severity === "error").length;
3765
+ const warnings = result.diagnostics.filter((d) => d.severity === "warning").length;
3766
+ const parts = [];
3767
+ if (errors > 0) parts.push(`${errors} error${errors === 1 ? "" : "s"}`);
3768
+ if (warnings > 0) parts.push(`${warnings} warning${warnings === 1 ? "" : "s"}`);
3769
+ const statusText = `${parts.join(", ")}, ${elapsed}`;
3770
+ if (errors > 0) logger.error(` ✗ ${label}: done (${statusText})`);
3771
+ else logger.warn(` ! ${label}: done (${statusText})`);
3772
+ }
3773
+ };
3774
+
3775
+ //#endregion
3776
+ //#region src/utils/git.ts
3777
+ const MAX_BUFFER = 50 * 1024 * 1024;
3778
+ const getChangedFiles = (cwd, base) => {
3779
+ const result = spawnSync("git", [
3780
+ "diff",
3781
+ "--name-only",
3782
+ "--diff-filter=ACMR",
3783
+ base ?? "HEAD"
3784
+ ], {
3785
+ cwd,
3786
+ encoding: "utf-8",
3787
+ maxBuffer: MAX_BUFFER
3788
+ });
3789
+ if (result.error || result.status !== 0) return [];
3790
+ return result.stdout.split("\n").filter((f) => f.length > 0).map((f) => path.resolve(cwd, f));
3791
+ };
3792
+ const getStagedFiles = (cwd) => {
3793
+ const result = spawnSync("git", [
3794
+ "diff",
3795
+ "--cached",
3796
+ "--name-only",
3797
+ "--diff-filter=ACMR"
3798
+ ], {
3799
+ cwd,
3800
+ encoding: "utf-8",
3801
+ maxBuffer: MAX_BUFFER
3802
+ });
3803
+ if (result.error || result.status !== 0) return [];
3804
+ return result.stdout.split("\n").filter((f) => f.length > 0).map((f) => path.resolve(cwd, f));
3805
+ };
3806
+
3807
+ //#endregion
3808
+ //#region src/commands/scan.ts
3809
+ const shouldUseSpinner = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
3810
+ const ALL_ENGINE_NAMES = Object.keys(ENGINE_INFO);
3811
+ const scanCommand = async (directory, config, options) => {
3812
+ const startTime = performance.now();
3813
+ const resolvedDir = path.resolve(directory);
3814
+ const showHeader = options.showHeader !== false;
3815
+ const useLiveProgress = !options.json && shouldUseSpinner();
3816
+ if (!options.json && showHeader) printCommandHeader("Scan");
3817
+ const discoverSpinner = options.json || !shouldUseSpinner() || !showHeader ? null : spinner("Discovering project...").start();
3818
+ const projectInfo = await discoverProject(resolvedDir);
3819
+ const projectSummary = formatProjectSummary(projectInfo);
3820
+ if (discoverSpinner) discoverSpinner.succeed(projectSummary);
3821
+ else if (!options.json) logger.success(` ✓ ${projectSummary}`);
3822
+ if (!options.json) printProjectMetadata(projectInfo);
3823
+ let files;
3824
+ if (options.staged) {
3825
+ files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir));
3826
+ if (!options.json) {
3827
+ logger.dim(` Scope: ${files.length} staged file(s)`);
3828
+ logger.break();
3829
+ }
3830
+ } else if (options.changes) {
3831
+ files = filterProjectFiles(resolvedDir, getChangedFiles(resolvedDir));
3832
+ if (!options.json) {
3833
+ logger.dim(` Scope: ${files.length} changed file(s)`);
3834
+ logger.break();
3835
+ }
3836
+ }
3837
+ const configDir = findConfigDir(resolvedDir);
3838
+ const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
3839
+ const engineConfig = {
3840
+ quality: config.quality,
3841
+ security: config.security,
3842
+ architectureRulesPath: config.engines.architecture ? rulesPath : void 0
3843
+ };
3844
+ const progressRenderer = useLiveProgress ? new ScanProgressRenderer(ALL_ENGINE_NAMES.filter((engine) => config.engines[engine] !== false)) : null;
3845
+ progressRenderer?.start();
3846
+ const results = await runEngines({
3847
+ rootDirectory: resolvedDir,
3848
+ languages: projectInfo.languages,
3849
+ frameworks: projectInfo.frameworks,
3850
+ files,
3851
+ installedTools: projectInfo.installedTools,
3852
+ config: engineConfig
3853
+ }, config.engines, (engine) => {
3854
+ progressRenderer?.markStarted(engine);
3855
+ }, (result) => {
3856
+ progressRenderer?.markComplete(result);
3857
+ if (!options.json && !progressRenderer) printEngineStatus(result);
3858
+ });
3859
+ progressRenderer?.stop();
3860
+ const allDiagnostics = results.flatMap((r) => r.diagnostics);
3861
+ const elapsedMs = performance.now() - startTime;
3862
+ const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds);
3863
+ const exitCode = scoreResult.score < config.ci.failBelow ? 1 : 0;
3864
+ if (options.json) {
3865
+ const { buildJsonOutput } = await import("./json-DkpW9UQj.js");
3866
+ const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
3867
+ console.log(JSON.stringify(jsonOut, null, 2));
3868
+ return { exitCode };
3869
+ }
3870
+ await printMaybePaged([
3871
+ "",
3872
+ allDiagnostics.length === 0 ? `${highlighter.success(" ✓ No issues found.")}\n` : renderDiagnostics(allDiagnostics, options.verbose),
3873
+ renderSummary(allDiagnostics, scoreResult, elapsedMs, projectInfo.sourceFileCount, config.scoring.thresholds),
3874
+ ""
3875
+ ].join("\n"));
3876
+ return { exitCode };
3877
+ };
3878
+
3879
+ //#endregion
3880
+ export { calculateScore, discoverProject, doctorCommand, fixCommand, initCommand, loadConfig, scanCommand };