fchek 1.0.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.
Files changed (66) hide show
  1. package/README.md +64 -0
  2. package/bin/fchek.js +107 -0
  3. package/lib/api.js +110 -0
  4. package/lib/audit.js +211 -0
  5. package/lib/bench.js +248 -0
  6. package/lib/config.js +191 -0
  7. package/lib/context.js +356 -0
  8. package/lib/convention.js +526 -0
  9. package/lib/coverage.js +604 -0
  10. package/lib/db.js +135 -0
  11. package/lib/deps-check.js +264 -0
  12. package/lib/deps.js +374 -0
  13. package/lib/docker.js +84 -0
  14. package/lib/doctor.js +149 -0
  15. package/lib/dom.js +226 -0
  16. package/lib/fuzz.js +470 -0
  17. package/lib/git.js +290 -0
  18. package/lib/goto.js +544 -0
  19. package/lib/launch.js +182 -0
  20. package/lib/lint.js +624 -0
  21. package/lib/new_features.test.js +181 -0
  22. package/lib/output.js +46 -0
  23. package/lib/port.js +173 -0
  24. package/lib/process.js +228 -0
  25. package/lib/profile.js +453 -0
  26. package/lib/python.js +41 -0
  27. package/lib/race.js +186 -0
  28. package/lib/registry.js +179 -0
  29. package/lib/repl.js +135 -0
  30. package/lib/run.js +403 -0
  31. package/lib/screenshot.js +152 -0
  32. package/lib/secrets.js +257 -0
  33. package/lib/state.js +219 -0
  34. package/lib/test.js +471 -0
  35. package/lib/vuln.js +253 -0
  36. package/lib/watch.js +240 -0
  37. package/lib/winlog.js +123 -0
  38. package/package.json +27 -0
  39. package/skills/ACTIVATE.md +274 -0
  40. package/skills/README.md +163 -0
  41. package/skills/agent.md +444 -0
  42. package/skills/api.md +47 -0
  43. package/skills/bench.md +117 -0
  44. package/skills/context.md +116 -0
  45. package/skills/convention.md +143 -0
  46. package/skills/coverage.md +99 -0
  47. package/skills/csharp.md +97 -0
  48. package/skills/db.md +66 -0
  49. package/skills/deps-check.md +135 -0
  50. package/skills/deps.md +143 -0
  51. package/skills/docker.md +61 -0
  52. package/skills/dom.md +56 -0
  53. package/skills/fuzz.md +167 -0
  54. package/skills/goto.md +111 -0
  55. package/skills/lint.md +123 -0
  56. package/skills/port.md +57 -0
  57. package/skills/profile.md +91 -0
  58. package/skills/race.md +117 -0
  59. package/skills/repl.md +81 -0
  60. package/skills/rules.md +318 -0
  61. package/skills/run.md +135 -0
  62. package/skills/secrets.md +170 -0
  63. package/skills/security.md +360 -0
  64. package/skills/state.md +261 -0
  65. package/skills/vuln.md +57 -0
  66. package/skills/windows.md +320 -0
package/lib/lint.js ADDED
@@ -0,0 +1,624 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * lint.js — linter + auto-fix + show diff of what changed
5
+ *
6
+ * Python (.py) → ruff check --fix (fast, modern)
7
+ * Rust → cargo clippy --fix
8
+ * JS/TS → eslint --fix
9
+ * C/C++ → clang-tidy --fix-errors (or clang-format)
10
+ * Go → gofmt -w + go vet
11
+ *
12
+ * Always:
13
+ * 1. Read file before fix
14
+ * 2. Apply fixes
15
+ * 3. Read file after fix
16
+ * 4. Build unified diff
17
+ * 5. Return issues + diff in JSON
18
+ */
19
+
20
+ const { spawnSync, execSync } = require('child_process');
21
+ const path = require('path');
22
+ const fs = require('fs');
23
+ const { output, ok, fail } = require('./output');
24
+
25
+ const HELP = `
26
+ fchek lint <file_or_dir> [--no-fix] [--strict]
27
+
28
+ Run linter, apply safe auto-fixes, and show a diff of what changed.
29
+
30
+ Supported:
31
+ .py → ruff (pip install ruff)
32
+ .rs / Cargo → cargo clippy --fix
33
+ .js / .ts → eslint --fix (npm install -g eslint)
34
+ .c / .cpp → clang-tidy + clang-format
35
+ .go → gofmt -w + go vet
36
+
37
+ Options:
38
+ --no-fix Report issues only, do not modify files
39
+ --strict Include warnings (not just errors)
40
+
41
+ Examples:
42
+ fchek lint main.py
43
+ fchek lint src/
44
+ fchek lint app.ts --no-fix
45
+ fchek lint . --strict (Rust: cargo clippy in cwd)
46
+ `.trim();
47
+
48
+ const DEFAULT_TIMEOUT = 60_000;
49
+
50
+ function commandExists(cmd) {
51
+ try {
52
+ execSync(process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`, { stdio: 'ignore' });
53
+ return true;
54
+ } catch { return false; }
55
+ }
56
+
57
+ function readFile(p) {
58
+ try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
59
+ }
60
+
61
+ /** Build a simple unified-diff-like structure between two strings */
62
+ function buildLineDiff(before, after, filePath) {
63
+ if (before === after) return [];
64
+ const bLines = before.split('\n');
65
+ const aLines = after.split('\n');
66
+ const hunks = [];
67
+ const maxLen = Math.max(bLines.length, aLines.length);
68
+
69
+ let i = 0;
70
+ while (i < maxLen) {
71
+ if (bLines[i] !== aLines[i]) {
72
+ const hunkStart = i;
73
+ const hunk = { file: filePath, from_line: i + 1, changes: [] };
74
+ while (i < maxLen && (bLines[i] !== aLines[i] || (i - hunkStart < 3))) {
75
+ if (i >= maxLen) break;
76
+ if (bLines[i] !== aLines[i]) {
77
+ if (bLines[i] !== undefined) hunk.changes.push({ op: '-', line: i + 1, text: bLines[i] });
78
+ if (aLines[i] !== undefined) hunk.changes.push({ op: '+', line: i + 1, text: aLines[i] });
79
+ } else {
80
+ hunk.changes.push({ op: ' ', line: i + 1, text: bLines[i] });
81
+ }
82
+ i++;
83
+ if (hunk.changes.length > 20) break;
84
+ }
85
+ hunks.push(hunk);
86
+ } else {
87
+ i++;
88
+ }
89
+ }
90
+ return hunks.slice(0, 15); // cap hunks
91
+ }
92
+
93
+ // ─── Python: ruff ─────────────────────────────────────────────────────────────
94
+
95
+ function lintPython(target, fix, strict, timeoutMs) {
96
+ if (!commandExists('ruff')) {
97
+ return output(fail('ruff not found. Install: pip install ruff'));
98
+ }
99
+
100
+ // 1. Capture issues BEFORE fixing
101
+ const checkArgs = ['check', '--output-format=json', target];
102
+ const checkRes = spawnSync('ruff', checkArgs, { encoding: 'utf8', timeout: timeoutMs });
103
+
104
+ let issuesBefore = [];
105
+ try { issuesBefore = JSON.parse(checkRes.stdout || '[]'); } catch {}
106
+
107
+ let diff = [];
108
+ let issuesAfter = issuesBefore;
109
+
110
+ if (fix) {
111
+ // Snapshot files before fix
112
+ const snapshotBefore = snapshotFiles(target, ['.py']);
113
+
114
+ const fixArgs = ['check', '--fix', target];
115
+ spawnSync('ruff', fixArgs, { encoding: 'utf8', timeout: timeoutMs });
116
+ spawnSync('ruff', ['format', target], { encoding: 'utf8', timeout: timeoutMs });
117
+
118
+ // Snapshot after fix
119
+ const snapshotAfter = snapshotFiles(target, ['.py']);
120
+
121
+ // Build diff for changed files
122
+ for (const [file, before] of Object.entries(snapshotBefore)) {
123
+ const after = snapshotAfter[file];
124
+ if (after && before !== after) {
125
+ diff.push(...buildLineDiff(before, after, file));
126
+ }
127
+ }
128
+
129
+ // Re-check to see remaining issues
130
+ const recheckRes = spawnSync('ruff', ['check', '--output-format=json', target], { encoding: 'utf8', timeout: timeoutMs });
131
+ try { issuesAfter = JSON.parse(recheckRes.stdout || '[]'); } catch {}
132
+ }
133
+
134
+ const fixedCount = issuesBefore.length - issuesAfter.length;
135
+
136
+ output(ok({
137
+ target,
138
+ lang: 'python',
139
+ tool: 'ruff',
140
+ fix_applied: fix,
141
+ issues_before: issuesBefore.length,
142
+ issues_after: issuesAfter.length,
143
+ fixed_count: fixedCount,
144
+ remaining_issues: issuesAfter.slice(0, 30).map(i => ({
145
+ file: i.filename,
146
+ line: i.location?.row,
147
+ col: i.location?.column,
148
+ code: i.code,
149
+ message: i.message,
150
+ })),
151
+ diff: diff.slice(0, 20),
152
+ }));
153
+ }
154
+
155
+ // ─── Rust: cargo clippy ───────────────────────────────────────────────────────
156
+
157
+ function lintRust(target, fix, strict, timeoutMs) {
158
+ if (!commandExists('cargo')) {
159
+ return output(fail('cargo not found. Install Rust: https://rustup.rs'));
160
+ }
161
+
162
+ const cwd = path.resolve(target === '.' ? process.cwd() : target);
163
+ const strictFlag = strict ? ['--', '-W', 'clippy::all'] : [];
164
+
165
+ // Run clippy check first to get issues
166
+ const checkArgs = ['clippy', '--message-format=json', ...strictFlag];
167
+ const checkRes = spawnSync('cargo', checkArgs, { encoding: 'utf8', cwd, timeout: timeoutMs });
168
+
169
+ const issuesBefore = parseCargoMessages(checkRes.stdout || '');
170
+
171
+ let diff = [];
172
+ let issuesAfter = issuesBefore;
173
+ let fixNote = null;
174
+
175
+ if (fix) {
176
+ // cargo clippy --fix requires either a clean git repo OR --allow-dirty/--allow-staged.
177
+ // If not a git repo at all, those flags still cause an error on some versions.
178
+ // Detect whether we are inside a git repo.
179
+ const isGitRepo = (() => {
180
+ try {
181
+ const r = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {
182
+ encoding: 'utf8', cwd, timeout: 3000,
183
+ });
184
+ return r.status === 0;
185
+ } catch { return false; }
186
+ })();
187
+
188
+ const fixArgs = isGitRepo
189
+ ? ['clippy', '--fix', '--allow-dirty', '--allow-staged', ...strictFlag]
190
+ : ['clippy', '--fix', ...strictFlag]; // no git flags if no repo
191
+
192
+ const snapshotBefore = snapshotDir(cwd, ['.rs']);
193
+ const fixRes = spawnSync('cargo', fixArgs, { encoding: 'utf8', cwd, timeout: timeoutMs });
194
+
195
+ // If clippy --fix failed because of git check even without the flags, report clearly
196
+ if (fixRes.status !== 0) {
197
+ const errOut = (fixRes.stderr || '');
198
+ if (errOut.includes('no VCS found') || errOut.includes('uncommitted changes')) {
199
+ fixNote = 'cargo clippy --fix requires a git repository or clean working tree. ' +
200
+ 'Run: git init && git add . && git commit -m init — then re-run fchek lint.';
201
+ // Still report issues found, just without fixes applied
202
+ }
203
+ }
204
+
205
+ const snapshotAfter = snapshotDir(cwd, ['.rs']);
206
+ for (const [file, before] of Object.entries(snapshotBefore)) {
207
+ const after = snapshotAfter[file];
208
+ if (after && before !== after) {
209
+ diff.push(...buildLineDiff(before, after, file));
210
+ }
211
+ }
212
+
213
+ const recheckRes = spawnSync('cargo', checkArgs, { encoding: 'utf8', cwd, timeout: timeoutMs });
214
+ issuesAfter = parseCargoMessages(recheckRes.stdout || '');
215
+ }
216
+
217
+ output(ok({
218
+ target: cwd,
219
+ lang: 'rust',
220
+ tool: 'cargo-clippy',
221
+ fix_applied: fix && !fixNote,
222
+ fix_note: fixNote,
223
+ issues_before: issuesBefore.length,
224
+ issues_after: issuesAfter.length,
225
+ fixed_count: issuesBefore.length - issuesAfter.length,
226
+ remaining_issues: issuesAfter.slice(0, 20),
227
+ diff: diff.slice(0, 20),
228
+ }));
229
+ }
230
+
231
+ function parseCargoMessages(raw) {
232
+ const issues = [];
233
+ for (const line of raw.split('\n')) {
234
+ try {
235
+ const msg = JSON.parse(line);
236
+ if (msg.reason === 'compiler-message' && msg.message?.level === 'warning') {
237
+ issues.push({
238
+ message: msg.message.message,
239
+ code: msg.message.code?.code ?? null,
240
+ spans: (msg.message.spans || []).slice(0, 2).map(s => ({
241
+ file: s.file_name, line: s.line_start,
242
+ })),
243
+ });
244
+ }
245
+ } catch {}
246
+ }
247
+ return issues;
248
+ }
249
+
250
+ // ─── JS/TS: eslint ────────────────────────────────────────────────────────────
251
+
252
+ function lintJs(target, fix, strict, timeoutMs) {
253
+ const eslint = commandExists('eslint') ? 'eslint' : null;
254
+ if (!eslint) {
255
+ return output(fail('eslint not found. Install: npm install -g eslint'));
256
+ }
257
+
258
+ const snapshotBefore = snapshotFiles(target, ['.js', '.ts', '.jsx', '.tsx', '.mjs']);
259
+ const checkArgs = [target, '--format=json', fix ? '--fix' : ''];
260
+ const res = spawnSync(eslint, checkArgs.filter(Boolean), { encoding: 'utf8', timeout: timeoutMs });
261
+
262
+ let issues = [];
263
+ try {
264
+ const parsed = JSON.parse(res.stdout || '[]');
265
+ for (const file of parsed) {
266
+ for (const msg of file.messages || []) {
267
+ if (!strict && msg.severity < 2) continue;
268
+ issues.push({
269
+ file: file.filePath,
270
+ line: msg.line,
271
+ col: msg.column,
272
+ rule: msg.ruleId,
273
+ message: msg.message,
274
+ severity: msg.severity === 2 ? 'error' : 'warning',
275
+ });
276
+ }
277
+ }
278
+ } catch {}
279
+
280
+ let diff = [];
281
+ if (fix) {
282
+ const snapshotAfter = snapshotFiles(target, ['.js', '.ts', '.jsx', '.tsx', '.mjs']);
283
+ for (const [file, before] of Object.entries(snapshotBefore)) {
284
+ const after = snapshotAfter[file];
285
+ if (after && before !== after) diff.push(...buildLineDiff(before, after, file));
286
+ }
287
+ }
288
+
289
+ output(ok({
290
+ target,
291
+ lang: 'javascript',
292
+ tool: 'eslint',
293
+ fix_applied: fix,
294
+ remaining_issues: issues.slice(0, 30),
295
+ issue_count: issues.length,
296
+ diff: diff.slice(0, 20),
297
+ }));
298
+ }
299
+
300
+ // ─── C#: dotnet format + dotnet build ────────────────────────────────────────
301
+
302
+ function lintCSharp(target, fix, timeoutMs) {
303
+ if (!commandExists('dotnet')) {
304
+ return output(fail('dotnet not found. Install .NET SDK: https://dotnet.microsoft.com/download'));
305
+ }
306
+
307
+ // Find project root (where .csproj or .sln lives)
308
+ let projDir = path.resolve(fs.statSync(target).isDirectory() ? target : path.dirname(target));
309
+ for (let i = 0; i < 6; i++) {
310
+ const found = fs.readdirSync(projDir).find(f => f.endsWith('.csproj') || f.endsWith('.sln'));
311
+ if (found) break;
312
+ const parent = path.dirname(projDir);
313
+ if (parent === projDir) { projDir = null; break; }
314
+ projDir = parent;
315
+ }
316
+
317
+ if (!projDir) {
318
+ return output(fail('No .csproj or .sln found. Run: dotnet new console -o MyApp'));
319
+ }
320
+
321
+ // Snapshot .cs files before fix
322
+ const snapshotBefore = fix ? snapshotFiles(projDir, ['.cs']) : {};
323
+
324
+ // Run dotnet format
325
+ const formatArgs = fix
326
+ ? ['format', projDir]
327
+ : ['format', projDir, '--verify-no-changes'];
328
+
329
+ const formatRes = spawnSync('dotnet', formatArgs, {
330
+ encoding: 'utf8', cwd: projDir, timeout: timeoutMs,
331
+ });
332
+
333
+ // Build diff if fix was applied
334
+ const diff = [];
335
+ if (fix) {
336
+ const snapshotAfter = snapshotFiles(projDir, ['.cs']);
337
+ for (const [file, before] of Object.entries(snapshotBefore)) {
338
+ const after = snapshotAfter[file];
339
+ if (after && before !== after) diff.push(...buildLineDiff(before, after, file));
340
+ }
341
+ }
342
+
343
+ // Run dotnet build to catch compiler errors/warnings
344
+ const buildRes = spawnSync('dotnet', ['build', projDir, '--no-restore', '-v', 'quiet'],
345
+ { encoding: 'utf8', cwd: projDir, timeout: timeoutMs });
346
+
347
+ const buildOutput = (buildRes.stdout || '') + (buildRes.stderr || '');
348
+ const issues = [];
349
+ for (const line of buildOutput.split('\n')) {
350
+ // Format: file.cs(line,col): warning/error CS1234: message
351
+ const m = line.match(/^(.+\.cs)\((\d+),(\d+)\):\s+(warning|error)\s+(CS\d+):\s+(.+)$/);
352
+ if (m) {
353
+ issues.push({
354
+ file: m[1], line: parseInt(m[2]), col: parseInt(m[3]),
355
+ severity: m[4], code: m[5], message: m[6].trim(),
356
+ });
357
+ }
358
+ }
359
+
360
+ const formatFailed = !fix && formatRes.status !== 0;
361
+
362
+ output(ok({
363
+ target: projDir,
364
+ lang: 'csharp',
365
+ tools: ['dotnet format', 'dotnet build'],
366
+ fix_applied: fix,
367
+ format_needs_changes: formatFailed,
368
+ issues_count: issues.length,
369
+ remaining_issues: issues.slice(0, 30),
370
+ diff: diff.slice(0, 20),
371
+ build_clean: buildRes.status === 0,
372
+ }));
373
+ }
374
+
375
+ // ─── C/C++: clang-format + clang-tidy ────────────────────────────────────────
376
+
377
+ function lintC(file, fix, timeoutMs) {
378
+ const results = { issues: [], diff: [] };
379
+
380
+ // clang-format
381
+ if (commandExists('clang-format')) {
382
+ const before = readFile(file);
383
+ const fmtRes = spawnSync('clang-format', [file], { encoding: 'utf8', timeout: timeoutMs });
384
+ const after = fmtRes.stdout || '';
385
+ if (fix && before && before !== after) {
386
+ fs.writeFileSync(file, after, 'utf8');
387
+ results.diff.push(...buildLineDiff(before, after, file));
388
+ }
389
+ }
390
+
391
+ // clang-tidy issues (no auto-fix to avoid breaking changes)
392
+ if (commandExists('clang-tidy')) {
393
+ const tidyRes = spawnSync('clang-tidy', [file, '--'], { encoding: 'utf8', timeout: timeoutMs });
394
+ const lines = (tidyRes.stdout || tidyRes.stderr || '').split('\n');
395
+ for (const line of lines) {
396
+ const m = line.match(/^(.+):(\d+):(\d+):\s+(warning|error):\s+(.+)\[(.+)\]$/);
397
+ if (m) {
398
+ results.issues.push({
399
+ file: m[1], line: parseInt(m[2]), col: parseInt(m[3]),
400
+ severity: m[4], message: m[5], code: m[6],
401
+ });
402
+ }
403
+ }
404
+ }
405
+
406
+ output(ok({
407
+ target: path.resolve(file),
408
+ lang: 'c',
409
+ tools: ['clang-format', 'clang-tidy'],
410
+ fix_applied: fix,
411
+ remaining_issues: results.issues.slice(0, 30),
412
+ issue_count: results.issues.length,
413
+ diff: results.diff.slice(0, 20),
414
+ }));
415
+ }
416
+
417
+ // ─── Go: gofmt + go vet ──────────────────────────────────────────────────────
418
+
419
+ function lintGo(target, fix, timeoutMs) {
420
+ if (!commandExists('gofmt')) {
421
+ return output(fail('gofmt not found. Install Go: https://go.dev'));
422
+ }
423
+
424
+ const files = fs.statSync(target).isDirectory()
425
+ ? findFiles(target, ['.go'])
426
+ : [target];
427
+
428
+ const allDiff = [];
429
+ const vetIssues = [];
430
+
431
+ for (const file of files) {
432
+ const before = readFile(file);
433
+ const fmtRes = spawnSync('gofmt', ['-l', file], { encoding: 'utf8', timeout: 10_000 });
434
+ const needsFmt = (fmtRes.stdout || '').trim() !== '';
435
+
436
+ if (fix && needsFmt) {
437
+ spawnSync('gofmt', ['-w', file], { encoding: 'utf8' });
438
+ const after = readFile(file);
439
+ if (before && after && before !== after) {
440
+ allDiff.push(...buildLineDiff(before, after, file));
441
+ }
442
+ }
443
+ }
444
+
445
+ // go vet
446
+ const vetRes = spawnSync('go', ['vet', './...'], { encoding: 'utf8', cwd: path.resolve(target), timeout: timeoutMs });
447
+ for (const line of (vetRes.stderr || '').split('\n')) {
448
+ if (line.trim()) vetIssues.push(line.trim());
449
+ }
450
+
451
+ output(ok({
452
+ target,
453
+ lang: 'go',
454
+ tools: ['gofmt', 'go vet'],
455
+ fix_applied: fix,
456
+ vet_issues: vetIssues.slice(0, 20),
457
+ issue_count: vetIssues.length,
458
+ diff: allDiff.slice(0, 20),
459
+ }));
460
+ }
461
+
462
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
463
+
464
+ function findFiles(dir, exts) {
465
+ const results = [];
466
+ const ignored = new Set(['node_modules', '.git', 'dist', 'target', 'build']);
467
+ function walk(d) {
468
+ let entries;
469
+ try { entries = fs.readdirSync(d); } catch { return; }
470
+ for (const name of entries) {
471
+ if (ignored.has(name)) continue;
472
+ const full = path.join(d, name);
473
+ const stat = fs.statSync(full);
474
+ if (stat.isDirectory()) walk(full);
475
+ else if (exts.includes(path.extname(name).toLowerCase())) results.push(full);
476
+ }
477
+ }
478
+ walk(dir);
479
+ return results;
480
+ }
481
+
482
+ function snapshotFiles(target, exts) {
483
+ const snap = {};
484
+ const stat = fs.existsSync(target) && fs.statSync(target);
485
+ if (!stat) return snap;
486
+ const files = stat.isDirectory() ? findFiles(target, exts) : [target];
487
+ for (const f of files) {
488
+ const content = readFile(f);
489
+ if (content !== null) snap[f] = content;
490
+ }
491
+ return snap;
492
+ }
493
+
494
+ function snapshotDir(dir, exts) {
495
+ return snapshotFiles(dir, exts);
496
+ }
497
+
498
+ // ─── Entry point ─────────────────────────────────────────────────────────────
499
+
500
+ async function run(args) {
501
+ if (args.length === 0 || args[0] === '--help') {
502
+ console.log(HELP);
503
+ return;
504
+ }
505
+
506
+ const target = args[0];
507
+ const noFix = args.includes('--no-fix');
508
+ const strict = args.includes('--strict');
509
+ const fix = !noFix;
510
+
511
+ if (!fs.existsSync(target)) {
512
+ return output(fail(`Not found: ${target}`));
513
+ }
514
+
515
+ const ext = path.extname(target).toLowerCase();
516
+ const isDir = fs.statSync(target).isDirectory();
517
+
518
+ // Single file — route directly by extension
519
+ if (!isDir) {
520
+ if (ext === '.cs') return lintCSharp(target, fix, DEFAULT_TIMEOUT);
521
+ if (ext === '.py') return lintPython(target, fix, strict, DEFAULT_TIMEOUT);
522
+ if (ext === '.rs') return lintRust(target, fix, strict, DEFAULT_TIMEOUT);
523
+ if (['.js','.ts','.jsx','.tsx','.mjs'].includes(ext)) return lintJs(target, fix, strict, DEFAULT_TIMEOUT);
524
+ if (['.c','.cpp','.cc'].includes(ext)) return lintC(target, fix, DEFAULT_TIMEOUT);
525
+ if (ext === '.go') return lintGo(target, fix, DEFAULT_TIMEOUT);
526
+ return output(fail(`Cannot detect linter for: ${target}. Supported: .py .rs .js .ts .c .cpp .go .cs`));
527
+ }
528
+
529
+ // Directory — detect ALL languages present and run each linter
530
+ const hasCsproj = fs.readdirSync(target).some(f => f.endsWith('.csproj') || f.endsWith('.sln'));
531
+ const hasCargo = fs.existsSync(path.join(target, 'Cargo.toml'));
532
+ const hasPkgJson = fs.existsSync(path.join(target, 'package.json'));
533
+ const hasGoMod = fs.existsSync(path.join(target, 'go.mod'));
534
+
535
+ const hasPy = findFiles(target, ['.py']).length > 0;
536
+ const hasJs = findFiles(target, ['.js', '.ts', '.jsx', '.tsx', '.mjs']).length > 0;
537
+ const hasC = findFiles(target, ['.c', '.cpp', '.cc']).length > 0;
538
+ const hasCs = findFiles(target, ['.cs']).length > 0;
539
+ const hasGo = findFiles(target, ['.go']).length > 0;
540
+
541
+ const langs = [];
542
+ if (hasCsproj || hasCs) langs.push('csharp');
543
+ if (hasCargo) langs.push('rust');
544
+ if (hasPkgJson || hasJs) langs.push('javascript');
545
+ if (hasGoMod || hasGo) langs.push('go');
546
+ if (hasPy) langs.push('python');
547
+ if (hasC && !hasCargo) langs.push('c');
548
+
549
+ if (langs.length === 0) {
550
+ return output(fail(`No supported source files found in: ${target}. Supported: .py .rs .js .ts .c .cpp .go .cs`));
551
+ }
552
+
553
+ // Single language — run directly (returns output itself)
554
+ if (langs.length === 1) {
555
+ const lang = langs[0];
556
+ if (lang === 'csharp') return lintCSharp(target, fix, DEFAULT_TIMEOUT);
557
+ if (lang === 'rust') return lintRust(target, fix, strict, DEFAULT_TIMEOUT);
558
+ if (lang === 'javascript') return lintJs(target, fix, strict, DEFAULT_TIMEOUT);
559
+ if (lang === 'go') return lintGo(target, fix, DEFAULT_TIMEOUT);
560
+ if (lang === 'python') return lintPython(target, fix, strict, DEFAULT_TIMEOUT);
561
+ if (lang === 'c') return lintC(target, fix, DEFAULT_TIMEOUT);
562
+ }
563
+
564
+ // Multiple languages — capture output from each linter and combine
565
+ const results = [];
566
+ let totalIssues = 0;
567
+
568
+ for (const lang of langs) {
569
+ // Temporarily capture console.log to get the JSON result
570
+ const lines = [];
571
+ const origLog = console.log;
572
+ console.log = (s) => lines.push(s);
573
+
574
+ try {
575
+ if (lang === 'csharp') await lintCSharp(target, fix, DEFAULT_TIMEOUT);
576
+ else if (lang === 'rust') await lintRust(target, fix, strict, DEFAULT_TIMEOUT);
577
+ else if (lang === 'javascript') await lintJs(target, fix, strict, DEFAULT_TIMEOUT);
578
+ else if (lang === 'go') await lintGo(target, fix, DEFAULT_TIMEOUT);
579
+ else if (lang === 'python') await lintPython(target, fix, strict, DEFAULT_TIMEOUT);
580
+ else if (lang === 'c') {
581
+ // C linter works per-file — collect all .c/.cpp files
582
+ const cFiles = findFiles(target, ['.c', '.cpp', '.cc']);
583
+ for (const cFile of cFiles.slice(0, 20)) {
584
+ await lintC(cFile, fix, DEFAULT_TIMEOUT);
585
+ }
586
+ }
587
+ } catch {}
588
+
589
+ console.log = origLog;
590
+
591
+ // Parse last JSON line from captured output
592
+ let result = null;
593
+ for (let i = lines.length - 1; i >= 0; i--) {
594
+ try { result = JSON.parse(lines[i]); break; } catch {}
595
+ }
596
+
597
+ if (result?.data) {
598
+ const d = result.data;
599
+ const issues = d.issues_count ?? d.issue_count ?? d.remaining_issues?.length ?? d.vet_issues?.length ?? 0;
600
+ totalIssues += issues;
601
+ results.push({
602
+ lang,
603
+ tool: d.tool ?? d.tools ?? null,
604
+ issues_count: issues,
605
+ fixed_count: d.fixed_count ?? null,
606
+ build_clean: d.build_clean ?? null,
607
+ diff_count: d.diff?.length ?? 0,
608
+ });
609
+ } else {
610
+ results.push({ lang, error: result?.error ?? 'no output' });
611
+ }
612
+ }
613
+
614
+ output(ok({
615
+ target,
616
+ langs_detected: langs,
617
+ total_issues: totalIssues,
618
+ fix_applied: fix,
619
+ results,
620
+ verdict: totalIssues === 0 ? 'clean' : 'issues_found',
621
+ }));
622
+ }
623
+
624
+ module.exports = { run };