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
@@ -0,0 +1,526 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * convention.js — scan project for coding conventions
5
+ *
6
+ * Extracts "the rules of this project" automatically:
7
+ * - error handling patterns
8
+ * - naming conventions
9
+ * - test locations and naming
10
+ * - import style
11
+ * - async patterns
12
+ * - logging style
13
+ *
14
+ * Gives AI agents the project's actual patterns so new code fits in.
15
+ */
16
+
17
+ const path = require('path');
18
+ const fs = require('fs');
19
+ const { output, ok, fail } = require('./output');
20
+
21
+ const HELP = `
22
+ fchek convention <project_dir>
23
+
24
+ Scan a project and extract its coding conventions.
25
+ Call this at the start of a task so new code matches existing patterns.
26
+
27
+ Detects:
28
+ - Error handling style (try/except, Result<>, custom exceptions...)
29
+ - Naming conventions (snake_case, camelCase, PascalCase...)
30
+ - Test file locations and naming patterns
31
+ - Import/require style
32
+ - Async patterns (async/await, callbacks, promises...)
33
+ - Logging style (print, console.log, log crate, tracing...)
34
+ - Config/env loading patterns
35
+
36
+ Examples:
37
+ fchek convention .
38
+ fchek convention src/
39
+ `.trim();
40
+
41
+ const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'target', '__pycache__', '.venv', 'vendor']);
42
+
43
+ function readFile(p) {
44
+ try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
45
+ }
46
+
47
+ const EXT_GROUPS = {
48
+ python: ['.py'],
49
+ javascript: ['.js', '.mjs', '.cjs', '.jsx'],
50
+ typescript: ['.ts', '.tsx'],
51
+ rust: ['.rs'],
52
+ c: ['.c', '.cpp', '.cc', '.h', '.hpp'],
53
+ go: ['.go'],
54
+ csharp: ['.cs'],
55
+ };
56
+
57
+ function collectFiles(dir, exts) {
58
+ const results = [];
59
+ function walk(d) {
60
+ let entries;
61
+ try { entries = fs.readdirSync(d); } catch { return; }
62
+ for (const name of entries) {
63
+ if (IGNORED_DIRS.has(name) || name.startsWith('.')) continue;
64
+ const full = path.join(d, name);
65
+ const stat = fs.statSync(full);
66
+ if (stat.isDirectory()) walk(full);
67
+ else if (exts.includes(path.extname(name).toLowerCase())) results.push(full);
68
+ }
69
+ }
70
+ walk(dir);
71
+ return results;
72
+ }
73
+
74
+ function detectLangs(dir) {
75
+ const found = [];
76
+ for (const [lang, exts] of Object.entries(EXT_GROUPS)) {
77
+ if (collectFiles(dir, exts).length > 0) found.push(lang);
78
+ }
79
+ return found;
80
+ }
81
+
82
+ // ─── Counters / scorers ───────────────────────────────────────────────────────
83
+
84
+ function countMatches(lines, patterns) {
85
+ const counts = {};
86
+ for (const { name, regex } of patterns) {
87
+ counts[name] = lines.filter(l => regex.test(l)).length;
88
+ }
89
+ return counts;
90
+ }
91
+
92
+ function topN(obj, n) {
93
+ return Object.entries(obj)
94
+ .sort((a, b) => b[1] - a[1])
95
+ .slice(0, n)
96
+ .filter(([, v]) => v > 0)
97
+ .map(([k, v]) => ({ pattern: k, occurrences: v }));
98
+ }
99
+
100
+ // ─── Python conventions ───────────────────────────────────────────────────────
101
+
102
+ function analyzePython(files) {
103
+ let lines = [];
104
+ let testFiles = [];
105
+ let allNames = { snake: 0, camel: 0, pascal: 0 };
106
+
107
+ for (const f of files) {
108
+ const content = readFile(f);
109
+ lines.push(...content.split('\n'));
110
+ if (path.basename(f).startsWith('test_') || path.basename(f).endsWith('_test.py')) {
111
+ testFiles.push(f);
112
+ }
113
+ }
114
+
115
+ const errorHandling = countMatches(lines, [
116
+ { name: 'try/except', regex: /^\s*try:/ },
117
+ { name: 'raise Exception', regex: /raise\s+\w*Exception/ },
118
+ { name: 'raise custom', regex: /raise\s+[A-Z]\w+Error/ },
119
+ { name: 'except broad (bare)', regex: /except\s*:/ },
120
+ { name: 'logging.error', regex: /logging\.(error|exception|warning)/ },
121
+ ]);
122
+
123
+ const asyncPatterns = countMatches(lines, [
124
+ { name: 'async def', regex: /async\s+def/ },
125
+ { name: 'await', regex: /\bawait\b/ },
126
+ { name: 'asyncio.run', regex: /asyncio\.run/ },
127
+ { name: 'ThreadPoolExecutor', regex: /ThreadPoolExecutor/ },
128
+ ]);
129
+
130
+ const importStyle = countMatches(lines, [
131
+ { name: 'from X import Y', regex: /^from\s+\w/ },
132
+ { name: 'import X', regex: /^import\s+\w/ },
133
+ { name: 'relative import', regex: /^from\s+\./ },
134
+ ]);
135
+
136
+ const logging = countMatches(lines, [
137
+ { name: 'print()', regex: /\bprint\s*\(/ },
138
+ { name: 'logging module', regex: /\blogging\.\w+\(/ },
139
+ { name: 'loguru', regex: /\blogger\.\w+\(/ },
140
+ { name: 'structlog', regex: /\bstructlog\b/ },
141
+ ]);
142
+
143
+ // Naming: count function names
144
+ for (const line of lines) {
145
+ const m = line.match(/def\s+([a-zA-Z_]\w*)/);
146
+ if (!m) continue;
147
+ const name = m[1];
148
+ if (/^[a-z][a-z0-9_]*$/.test(name)) allNames.snake++;
149
+ else if (/^[a-z][a-zA-Z0-9]+$/.test(name)) allNames.camel++;
150
+ else if (/^[A-Z][a-zA-Z0-9]+$/.test(name)) allNames.pascal++;
151
+ }
152
+
153
+ const namingStyle = Object.entries(allNames).sort((a, b) => b[1] - a[1])[0]?.[0] || 'snake_case';
154
+
155
+ return {
156
+ lang: 'python',
157
+ test_files: testFiles.length,
158
+ test_locations: testFiles.slice(0, 5).map(f => path.relative(process.cwd(), f)),
159
+ naming_style: namingStyle,
160
+ error_handling: topN(errorHandling, 3),
161
+ async_patterns: topN(asyncPatterns, 3),
162
+ import_style: topN(importStyle, 2),
163
+ logging_style: topN(logging, 2),
164
+ };
165
+ }
166
+
167
+ // ─── JavaScript/TypeScript conventions ───────────────────────────────────────
168
+
169
+ function analyzeJs(files) {
170
+ let lines = [];
171
+ let testFiles = [];
172
+
173
+ for (const f of files) {
174
+ const content = readFile(f);
175
+ lines.push(...content.split('\n'));
176
+ const base = path.basename(f);
177
+ if (base.includes('.test.') || base.includes('.spec.') || f.includes('__tests__')) {
178
+ testFiles.push(f);
179
+ }
180
+ }
181
+
182
+ const errorHandling = countMatches(lines, [
183
+ { name: 'try/catch', regex: /\btry\s*\{/ },
184
+ { name: 'Promise.catch', regex: /\.catch\s*\(/ },
185
+ { name: 'custom Error class',regex: /class\s+\w+Error\s+extends/ },
186
+ { name: 'Result pattern', regex: /\{\s*ok\s*:|success\s*:/ },
187
+ { name: 'console.error', regex: /console\.error/ },
188
+ ]);
189
+
190
+ const asyncPatterns = countMatches(lines, [
191
+ { name: 'async/await', regex: /\basync\s+function|\basync\s+\(/ },
192
+ { name: 'Promise chain', regex: /\.then\s*\(/ },
193
+ { name: 'callback style', regex: /function\s*\(err,?\s*\w*\)/ },
194
+ ]);
195
+
196
+ const importStyle = countMatches(lines, [
197
+ { name: 'ES modules (import)', regex: /^import\s+/ },
198
+ { name: 'CommonJS (require)', regex: /require\s*\(/ },
199
+ ]);
200
+
201
+ const logging = countMatches(lines, [
202
+ { name: 'console.log', regex: /console\.log\s*\(/ },
203
+ { name: 'console.info', regex: /console\.info\s*\(/ },
204
+ { name: 'winston/pino', regex: /\b(?:winston|pino|logger)\.\w+\(/ },
205
+ { name: 'debug module', regex: /\bdebug\s*\(/ },
206
+ ]);
207
+
208
+ // Naming conventions
209
+ const funcNames = { camel: 0, pascal: 0, snake: 0 };
210
+ for (const line of lines) {
211
+ const m = line.match(/function\s+([a-zA-Z_]\w*)|const\s+([a-zA-Z_]\w*)\s*=/);
212
+ const name = m?.[1] || m?.[2];
213
+ if (!name) continue;
214
+ if (/^[a-z][a-zA-Z0-9]+$/.test(name)) funcNames.camel++;
215
+ else if (/^[A-Z][a-zA-Z0-9]+$/.test(name)) funcNames.pascal++;
216
+ else if (/^[a-z][a-z0-9_]+$/.test(name)) funcNames.snake++;
217
+ }
218
+ const namingStyle = Object.entries(funcNames).sort((a, b) => b[1] - a[1])[0]?.[0] || 'camelCase';
219
+
220
+ return {
221
+ lang: testFiles.some(f => f.endsWith('.ts') || f.endsWith('.tsx')) ? 'typescript' : 'javascript',
222
+ test_files: testFiles.length,
223
+ test_locations: testFiles.slice(0, 5).map(f => path.relative(process.cwd(), f)),
224
+ naming_style: namingStyle,
225
+ error_handling: topN(errorHandling, 3),
226
+ async_patterns: topN(asyncPatterns, 2),
227
+ import_style: topN(importStyle, 1),
228
+ logging_style: topN(logging, 2),
229
+ };
230
+ }
231
+
232
+ // ─── Rust conventions ─────────────────────────────────────────────────────────
233
+
234
+ function analyzeRust(files) {
235
+ let lines = [];
236
+ let testFiles = [];
237
+
238
+ for (const f of files) {
239
+ const content = readFile(f);
240
+ lines.push(...content.split('\n'));
241
+ if (f.includes('/tests/') || content.includes('#[cfg(test)]')) testFiles.push(f);
242
+ }
243
+
244
+ const errorHandling = countMatches(lines, [
245
+ { name: 'Result<T, E>', regex: /Result</ },
246
+ { name: '? operator', regex: /\?\s*;|\?\s*$/ },
247
+ { name: 'unwrap()', regex: /\.unwrap\s*\(\s*\)/ },
248
+ { name: 'expect()', regex: /\.expect\s*\(/ },
249
+ { name: 'custom Error enum', regex: /enum\s+\w+Error/ },
250
+ { name: 'anyhow/thiserror', regex: /anyhow|thiserror/ },
251
+ { name: 'panic!()', regex: /\bpanic!\s*\(/ },
252
+ ]);
253
+
254
+ const asyncPatterns = countMatches(lines, [
255
+ { name: 'async fn', regex: /\basync\s+fn/ },
256
+ { name: 'tokio', regex: /\btokio\b/ },
257
+ { name: 'async-std', regex: /async_std/ },
258
+ ]);
259
+
260
+ const logging = countMatches(lines, [
261
+ { name: 'println!()', regex: /\bprintln!\s*\(/ },
262
+ { name: 'log crate', regex: /\b(?:info|warn|error|debug)!\s*\(/ },
263
+ { name: 'tracing', regex: /\btracing::/ },
264
+ { name: 'eprintln!()', regex: /\beprintln!\s*\(/ },
265
+ ]);
266
+
267
+ const unwrapCount = errorHandling['unwrap()'] || 0;
268
+ const resultCount = errorHandling['Result<T, E>'] || 0;
269
+ const errorStyle = unwrapCount > resultCount ? 'unwrap-heavy (consider ? operator)'
270
+ : resultCount > 0 ? 'Result<T,E> with ? operator' : 'panic-based';
271
+
272
+ return {
273
+ lang: 'rust',
274
+ test_files: testFiles.length,
275
+ test_locations: testFiles.slice(0, 5).map(f => path.relative(process.cwd(), f)),
276
+ naming_style: 'snake_case (Rust standard)',
277
+ error_handling: topN(errorHandling, 4),
278
+ error_style_summary: errorStyle,
279
+ async_patterns: topN(asyncPatterns, 2),
280
+ logging_style: topN(logging, 2),
281
+ };
282
+ }
283
+
284
+ // ─── C/C++ conventions ────────────────────────────────────────────────────────
285
+
286
+ function analyzeC(files) {
287
+ let lines = [];
288
+ let testFiles = [];
289
+
290
+ for (const f of files) {
291
+ const content = readFile(f);
292
+ lines.push(...content.split('\n'));
293
+ const base = path.basename(f).toLowerCase();
294
+ if (base.startsWith('test_') || base.includes('_test.') || f.includes('/test') || f.includes('\\test')) {
295
+ testFiles.push(f);
296
+ }
297
+ }
298
+
299
+ const errorHandling = countMatches(lines, [
300
+ { name: 'return error code', regex: /\breturn\s+(-?\d+|NULL|nullptr|EXIT_FAILURE|false)\s*;/ },
301
+ { name: 'errno', regex: /\berrno\b/ },
302
+ { name: 'throw (C++)', regex: /\bthrow\s+/ },
303
+ { name: 'try/catch (C++)', regex: /\btry\s*\{/ },
304
+ { name: 'assert()', regex: /\bassert\s*\(/ },
305
+ { name: 'exit()', regex: /\bexit\s*\(/ },
306
+ { name: 'perror()', regex: /\bperror\s*\(/ },
307
+ ]);
308
+
309
+ const logging = countMatches(lines, [
310
+ { name: 'printf', regex: /\bprintf\s*\(/ },
311
+ { name: 'fprintf', regex: /\bfprintf\s*\(/ },
312
+ { name: 'std::cout', regex: /std::cout\s*<</ },
313
+ { name: 'spdlog', regex: /\bspdlog::/ },
314
+ ]);
315
+
316
+ const naming = { snake: 0, camel: 0, pascal: 0 };
317
+ for (const line of lines) {
318
+ const m = line.match(/^(?:static\s+|inline\s+)?(?:[\w:*&<>]+\s+)+([a-zA-Z_]\w*)\s*\([^;]*\)\s*\{?\s*$/);
319
+ const name = m?.[1];
320
+ if (!name || ['if', 'for', 'while', 'switch', 'return', 'main'].includes(name)) continue;
321
+ if (/^[a-z][a-z0-9_]+$/.test(name)) naming.snake++;
322
+ else if (/^[a-z][a-zA-Z0-9]+$/.test(name)) naming.camel++;
323
+ else if (/^[A-Z][a-zA-Z0-9]+$/.test(name)) naming.pascal++;
324
+ }
325
+ const namingStyle = Object.entries(naming).sort((a, b) => b[1] - a[1])[0]?.[0] || 'snake_case';
326
+
327
+ const isCpp = files.some(f => ['.cpp', '.cc', '.hpp'].includes(path.extname(f).toLowerCase()));
328
+
329
+ return {
330
+ lang: isCpp ? 'cpp' : 'c',
331
+ test_files: testFiles.length,
332
+ test_locations: testFiles.slice(0, 5).map(f => path.relative(process.cwd(), f)),
333
+ naming_style: namingStyle,
334
+ error_handling: topN(errorHandling, 4),
335
+ logging_style: topN(logging, 2),
336
+ note: isCpp ? 'C++ project (exceptions detected)' : 'C project (error codes style)',
337
+ };
338
+ }
339
+
340
+ // ─── Go conventions ───────────────────────────────────────────────────────────
341
+
342
+ function analyzeGo(files) {
343
+ let lines = [];
344
+ let testFiles = [];
345
+
346
+ for (const f of files) {
347
+ const content = readFile(f);
348
+ lines.push(...content.split('\n'));
349
+ if (path.basename(f).endsWith('_test.go')) testFiles.push(f);
350
+ }
351
+
352
+ const errorHandling = countMatches(lines, [
353
+ { name: 'if err != nil', regex: /if\s+err\s*!=\s*nil/ },
354
+ { name: 'panic()', regex: /\bpanic\s*\(/ },
355
+ { name: 'return err', regex: /\breturn\s+.*\berr\b/ },
356
+ { name: 'log.Fatal', regex: /log\.Fatal/ },
357
+ { name: 'errors.New', regex: /errors\.New/ },
358
+ { name: 'fmt.Errorf', regex: /fmt\.Errorf/ },
359
+ { name: 'errors.As/Is', regex: /errors\.(As|Is)\s*\(/ },
360
+ ]);
361
+
362
+ const asyncPatterns = countMatches(lines, [
363
+ { name: 'goroutine (go keyword)', regex: /\bgo\s+\w+\s*\(/ },
364
+ { name: 'channel', regex: /\bmake\s*\(\s*chan\b/ },
365
+ { name: 'select statement', regex: /\bselect\s*\{/ },
366
+ { name: 'sync.WaitGroup', regex: /sync\.WaitGroup/ },
367
+ { name: 'context.Context', regex: /context\.Context/ },
368
+ ]);
369
+
370
+ const logging = countMatches(lines, [
371
+ { name: 'fmt.Println', regex: /fmt\.Print/ },
372
+ { name: 'log package', regex: /\blog\.\w+\(/ },
373
+ { name: 'zap', regex: /\bzap\b/ },
374
+ { name: 'logrus', regex: /\blogrus\b/ },
375
+ { name: 'slog (Go1.21)',regex: /\bslog\.\w+\(/ },
376
+ ]);
377
+
378
+ return {
379
+ lang: 'go',
380
+ test_files: testFiles.length,
381
+ test_locations: testFiles.slice(0, 5).map(f => path.relative(process.cwd(), f)),
382
+ naming_style: 'camelCase for unexported, PascalCase for exported (Go standard)',
383
+ error_handling: topN(errorHandling, 4),
384
+ async_patterns: topN(asyncPatterns, 3),
385
+ logging_style: topN(logging, 2),
386
+ };
387
+ }
388
+
389
+ // ─── General project structure ────────────────────────────────────────────────
390
+
391
+ // ─── C# conventions ───────────────────────────────────────────────────────────
392
+
393
+ function analyzeCSharp(files) {
394
+ let lines = [];
395
+ let testFiles = [];
396
+
397
+ for (const f of files) {
398
+ const content = readFile(f);
399
+ lines.push(...content.split('\n'));
400
+ const base = path.basename(f).toLowerCase();
401
+ if (base.includes('test') || base.includes('spec') || f.includes('Tests')) {
402
+ testFiles.push(f);
403
+ }
404
+ }
405
+
406
+ const errorHandling = countMatches(lines, [
407
+ { name: 'try/catch', regex: /\btry\s*\{/ },
408
+ { name: 'throw exception', regex: /\bthrow\s+new\s+\w+Exception/ },
409
+ { name: 'custom exception', regex: /class\s+\w+Exception\s*:/ },
410
+ { name: 'Result pattern', regex: /\bResult[<(]/ },
411
+ { name: 'Task.FromException', regex: /Task\.FromException/ },
412
+ { name: 'ArgumentException', regex: /\bArgumentException\b/ },
413
+ ]);
414
+
415
+ const asyncPatterns = countMatches(lines, [
416
+ { name: 'async/await', regex: /\basync\s+Task|\basync\s+void/ },
417
+ { name: 'await', regex: /\bawait\b/ },
418
+ { name: 'Task<T>', regex: /Task<\w+>/ },
419
+ { name: 'CancellationToken', regex: /CancellationToken/ },
420
+ ]);
421
+
422
+ const logging = countMatches(lines, [
423
+ { name: 'ILogger', regex: /\bILogger\w*\b/ },
424
+ { name: 'Console.Write', regex: /Console\.(Write|WriteLine)\s*\(/ },
425
+ { name: 'Serilog', regex: /\bSerilog\b|Log\.(Information|Warning|Error|Debug)\s*\(/ },
426
+ { name: 'NLog', regex: /\bNLog\b|logger\.(Info|Warn|Error|Debug)\s*\(/ },
427
+ { name: 'Debug.WriteLine', regex: /Debug\.WriteLine\s*\(/ },
428
+ ]);
429
+
430
+ const naming = { pascal: 0, camel: 0 };
431
+ for (const line of lines) {
432
+ // Method names
433
+ const m = line.match(/\b(public|private|protected|internal)\s+(?:static\s+)?(?:async\s+)?(?:[\w<>?\[\]]+\s+)+([A-Z][a-zA-Z0-9]*)\s*\(/);
434
+ if (m) naming.pascal++;
435
+ const m2 = line.match(/\b(private|protected)\s+(?:readonly\s+)?(?:[\w<>?\[\]]+\s+)+(_?[a-z][a-zA-Z0-9]*)\s*[=;{]/);
436
+ if (m2) naming.camel++;
437
+ }
438
+
439
+ // Detect framework
440
+ const allLines = lines.join('\n');
441
+ const framework =
442
+ /\bMicrosoft\.AspNetCore\b/.test(allLines) ? 'ASP.NET Core' :
443
+ /\bSystem\.Windows\b|\bWPF\b|\.xaml\b/i.test(allLines) ? 'WPF' :
444
+ /\bXamarin\b|\bMAUI\b/.test(allLines) ? 'MAUI/Xamarin' :
445
+ /\bUnityEngine\b/.test(allLines) ? 'Unity' :
446
+ /\bBlazor\b/.test(allLines) ? 'Blazor' :
447
+ 'Unknown';
448
+
449
+ return {
450
+ lang: 'csharp',
451
+ framework,
452
+ test_files: testFiles.length,
453
+ test_locations: testFiles.slice(0, 5).map(f => path.relative(process.cwd(), f)),
454
+ naming_style: 'PascalCase for public members, camelCase/_camelCase for private fields (C# standard)',
455
+ error_handling: topN(errorHandling, 4),
456
+ async_patterns: topN(asyncPatterns, 3),
457
+ logging_style: topN(logging, 2),
458
+ note: `Framework detected: ${framework}. For deeper analysis use Roslyn analyzers / dotnet-format / SonarAnalyzer.`,
459
+ };
460
+ }
461
+
462
+ function analyzeStructure(dir) {
463
+ const hasDir = (d) => fs.existsSync(path.join(dir, d));
464
+ const hasFile = (f) => fs.existsSync(path.join(dir, f));
465
+
466
+ const structure = {
467
+ has_tests_dir: hasDir('tests') || hasDir('test') || hasDir('spec'),
468
+ has_src_dir: hasDir('src'),
469
+ has_docs_dir: hasDir('docs') || hasDir('doc'),
470
+ has_ci: hasDir('.github/workflows') || hasFile('.travis.yml') || hasFile('.gitlab-ci.yml'),
471
+ has_linter_config: hasFile('.eslintrc') || hasFile('.eslintrc.js') || hasFile('ruff.toml')
472
+ || hasFile('.flake8') || hasFile('pyproject.toml') || hasFile('.clang-format'),
473
+ has_env_example: hasFile('.env.example') || hasFile('.env.sample'),
474
+ has_docker: hasFile('Dockerfile') || hasFile('docker-compose.yml'),
475
+ config_files: [],
476
+ };
477
+
478
+ for (const f of ['.env.example', 'pyproject.toml', 'Cargo.toml', 'package.json', 'go.mod', 'CMakeLists.txt']) {
479
+ if (hasFile(f)) structure.config_files.push(f);
480
+ }
481
+
482
+ return structure;
483
+ }
484
+
485
+ // ─── Entry point ─────────────────────────────────────────────────────────────
486
+
487
+ async function run(args) {
488
+ if (args.length === 0 || args[0] === '--help') { console.log(HELP); return; }
489
+
490
+ const target = args[0];
491
+ if (!fs.existsSync(target)) return output(fail(`Not found: ${target}`));
492
+
493
+ const dir = path.resolve(fs.statSync(target).isDirectory() ? target : path.dirname(target));
494
+ const langs = detectLangs(dir);
495
+
496
+ if (langs.length === 0) {
497
+ return output(fail(`No supported source files found in: ${target}`));
498
+ }
499
+
500
+ const results = [];
501
+ const structure = analyzeStructure(dir);
502
+
503
+ for (const lang of langs) {
504
+ const exts = EXT_GROUPS[lang];
505
+ const files = collectFiles(dir, exts);
506
+ if (!files.length) continue;
507
+
508
+ if (lang === 'python') results.push(analyzePython(files));
509
+ else if (lang === 'javascript' || lang === 'typescript') results.push(analyzeJs(files));
510
+ else if (lang === 'rust') results.push(analyzeRust(files));
511
+ else if (lang === 'c') results.push(analyzeC(files));
512
+ else if (lang === 'go') results.push(analyzeGo(files));
513
+ else if (lang === 'csharp') results.push(analyzeCSharp(files));
514
+ }
515
+
516
+ output(ok({
517
+ project: dir,
518
+ languages: langs,
519
+ structure,
520
+ conventions: results,
521
+ summary: `Project uses: ${langs.join(', ')}. ` +
522
+ results.map(r => `${r.lang}: naming=${r.naming_style || 'n/a'}`).join('; '),
523
+ }));
524
+ }
525
+
526
+ module.exports = { run };