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