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/deps.js ADDED
@@ -0,0 +1,374 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * deps.js — dependency graph + dead code / unused imports
5
+ *
6
+ * Python → pydeps (graph) + vulture (dead code) + ruff (unused imports)
7
+ * Rust → cargo-udeps (unused deps) + cargo-machete
8
+ * JS/TS → madge (graph + circular) + depcheck (unused)
9
+ * Go → go mod tidy --dry-run + deadcode (golang.org/x/tools/cmd/deadcode)
10
+ */
11
+
12
+ const { spawnSync, execSync } = require('child_process');
13
+ const path = require('path');
14
+ const fs = require('fs');
15
+ const { output, ok, fail } = require('./output');
16
+
17
+ const HELP = `
18
+ fchek deps <file_or_dir> [--graph] [--unused-only]
19
+
20
+ Show dependency graph and detect dead code / unused imports.
21
+ Hard to keep in your head across a whole project — let the tool do it.
22
+
23
+ Supported:
24
+ .py / dir → pydeps (graph) + vulture (dead code) + ruff (unused imports)
25
+ .rs / Cargo → cargo-udeps + cargo-machete (unused Cargo.toml deps)
26
+ .js / .ts → madge (circular deps) + depcheck (unused npm packages)
27
+ go.mod / .go → go mod tidy + deadcode
28
+
29
+ Options:
30
+ --graph Also output the dependency graph (can be large)
31
+ --unused-only Show only unused/dead items, skip full graph
32
+
33
+ Examples:
34
+ fchek deps main.py
35
+ fchek deps src/
36
+ fchek deps . (Rust or JS project in cwd)
37
+ fchek deps . --unused-only
38
+ `.trim();
39
+
40
+ const DEFAULT_TIMEOUT = 120_000;
41
+
42
+ function commandExists(cmd) {
43
+ try {
44
+ execSync(process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`, { stdio: 'ignore' });
45
+ return true;
46
+ } catch { return false; }
47
+ }
48
+
49
+ // ─── Python ──────────────────────────────────────────────────────────────────
50
+
51
+ function depsPython(target, showGraph, unusedOnly, timeoutMs) {
52
+ const py = commandExists('python3') ? 'python3' : 'python';
53
+ const results = { lang: 'python', target, unused_imports: [], dead_code: [], graph: null };
54
+ const tools_used = [];
55
+
56
+ // 1. ruff — unused imports (fast, always available if ruff installed)
57
+ if (commandExists('ruff')) {
58
+ tools_used.push('ruff');
59
+ const ruffRes = spawnSync(
60
+ 'ruff', ['check', '--select=F401', '--output-format=json', target],
61
+ { encoding: 'utf8', timeout: 30_000 }
62
+ );
63
+ try {
64
+ const issues = JSON.parse(ruffRes.stdout || '[]');
65
+ results.unused_imports = issues.map(i => ({
66
+ file: i.filename,
67
+ line: i.location?.row,
68
+ symbol: i.message,
69
+ code: i.code,
70
+ }));
71
+ } catch {}
72
+ }
73
+
74
+ // 2. vulture — dead code detection
75
+ if (commandExists('vulture')) {
76
+ tools_used.push('vulture');
77
+ const vRes = spawnSync('vulture', [target, '--min-confidence=60'], {
78
+ encoding: 'utf8', timeout: timeoutMs
79
+ });
80
+ const lines = (vRes.stdout || '').split('\n').filter(Boolean);
81
+ results.dead_code = lines.slice(0, 40).map(line => {
82
+ // Format: path/file.py:42: unused function 'foo' (60% confidence)
83
+ const m = line.match(/^(.+):(\d+):\s+(.+)$/);
84
+ return m ? { file: m[1], line: parseInt(m[2]), issue: m[3] } : { raw: line };
85
+ });
86
+ } else {
87
+ results.dead_code_note = 'vulture not found. Install: pip install vulture';
88
+ }
89
+
90
+ // 3. pydeps — full import graph (optional, slow)
91
+ if (showGraph && commandExists('pydeps')) {
92
+ tools_used.push('pydeps');
93
+ const outJson = '/tmp/fchek_pydeps.json';
94
+ const pRes = spawnSync(
95
+ 'pydeps', [target, '--show-deps', '--no-output', '--json', outJson],
96
+ { encoding: 'utf8', timeout: timeoutMs }
97
+ );
98
+ try {
99
+ if (fs.existsSync(outJson)) {
100
+ results.graph = JSON.parse(fs.readFileSync(outJson, 'utf8'));
101
+ try { fs.unlinkSync(outJson); } catch {}
102
+ }
103
+ } catch {}
104
+ } else if (showGraph) {
105
+ results.graph_note = 'pydeps not found. Install: pip install pydeps';
106
+ }
107
+
108
+ output(ok({ ...results, tools_used }));
109
+ }
110
+
111
+ // ─── Rust ─────────────────────────────────────────────────────────────────────
112
+
113
+ function depsRust(target, showGraph, timeoutMs) {
114
+ if (!commandExists('cargo')) {
115
+ return output(fail('cargo not found. Install Rust: https://rustup.rs'));
116
+ }
117
+
118
+ const cwd = path.resolve(target === '.' ? process.cwd() : target);
119
+ const tools_used = [];
120
+ const results = { lang: 'rust', target: cwd, unused_deps: [], dead_code: [] };
121
+
122
+ // cargo-machete — much faster than cargo-udeps, no nightly needed
123
+ if (commandExists('cargo-machete') || (() => {
124
+ const r = spawnSync('cargo', ['machete', '--version'], { encoding: 'utf8', timeout: 5000 });
125
+ return r.status === 0;
126
+ })()) {
127
+ tools_used.push('cargo-machete');
128
+ const res = spawnSync('cargo', ['machete'], { encoding: 'utf8', cwd, timeout: timeoutMs });
129
+ const raw = (res.stdout || '') + (res.stderr || '');
130
+ // Parse: "The following packages are not used: serde, regex"
131
+ const m = raw.match(/not used[:\s]+([^\n]+)/i);
132
+ if (m) {
133
+ results.unused_deps = m[1].split(/,\s*/).map(s => s.trim()).filter(Boolean);
134
+ }
135
+ results.machete_raw = raw.slice(0, 2000);
136
+ } else {
137
+ results.unused_deps_note = 'cargo-machete not found. Install: cargo install cargo-machete';
138
+ }
139
+
140
+ // cargo-udeps — deeper analysis (requires nightly)
141
+ if (commandExists('cargo') && !results.unused_deps.length) {
142
+ const udepsRes = spawnSync('cargo', ['+nightly', 'udeps'], { encoding: 'utf8', cwd, timeout: timeoutMs });
143
+ if (udepsRes.status === 0) {
144
+ tools_used.push('cargo-udeps');
145
+ results.udeps_raw = (udepsRes.stdout || '').slice(0, 2000);
146
+ }
147
+ }
148
+
149
+ // Dependency graph via cargo tree
150
+ if (showGraph) {
151
+ tools_used.push('cargo-tree');
152
+ const treeRes = spawnSync('cargo', ['tree', '--prefix=depth'], { encoding: 'utf8', cwd, timeout: 30_000 });
153
+ results.graph_raw = (treeRes.stdout || '').slice(0, 5000);
154
+ }
155
+
156
+ output(ok({ ...results, tools_used }));
157
+ }
158
+
159
+ // ─── JS/TS ────────────────────────────────────────────────────────────────────
160
+
161
+ function depsJs(target, showGraph, timeoutMs) {
162
+ const cwd = path.resolve(target === '.' ? process.cwd() : target);
163
+ const tools_used = [];
164
+ const results = { lang: 'javascript', target: cwd };
165
+
166
+ // madge — import graph + circular dependency detection
167
+ if (commandExists('madge')) {
168
+ tools_used.push('madge');
169
+
170
+ // Circular deps
171
+ const circRes = spawnSync('madge', ['--circular', '--json', cwd], {
172
+ encoding: 'utf8', timeout: timeoutMs
173
+ });
174
+ try {
175
+ const circular = JSON.parse(circRes.stdout || '[]');
176
+ results.circular_deps = circular.slice(0, 20);
177
+ results.circular_count = circular.length;
178
+ } catch {
179
+ results.circular_deps = [];
180
+ }
181
+
182
+ // Full graph (optional)
183
+ if (showGraph) {
184
+ const graphRes = spawnSync('madge', ['--json', cwd], { encoding: 'utf8', timeout: timeoutMs });
185
+ try {
186
+ results.graph = JSON.parse(graphRes.stdout || '{}');
187
+ } catch {}
188
+ }
189
+ } else {
190
+ results.circular_deps_note = 'madge not found. Install: npm install -g madge';
191
+ }
192
+
193
+ // depcheck — unused npm packages
194
+ if (commandExists('depcheck')) {
195
+ tools_used.push('depcheck');
196
+ const dcRes = spawnSync('depcheck', ['--json'], { encoding: 'utf8', cwd, timeout: timeoutMs });
197
+ try {
198
+ const dc = JSON.parse(dcRes.stdout || '{}');
199
+ results.unused_packages = dc.dependencies || [];
200
+ results.unused_dev_packages = dc.devDependencies || [];
201
+ results.missing_packages = Object.keys(dc.missing || {});
202
+ } catch {}
203
+ } else {
204
+ results.unused_packages_note = 'depcheck not found. Install: npm install -g depcheck';
205
+ }
206
+
207
+ output(ok({ ...results, tools_used }));
208
+ }
209
+
210
+ // ─── C# / .NET ────────────────────────────────────────────────────────────────
211
+
212
+ function depsCSharp(target, timeoutMs) {
213
+ if (!commandExists('dotnet')) {
214
+ return output(fail('dotnet not found. Install .NET SDK: https://dotnet.microsoft.com/download'));
215
+ }
216
+
217
+ const cwd = path.resolve(fs.statSync(target).isDirectory() ? target : path.dirname(target));
218
+
219
+ // Find .csproj or .sln
220
+ let projDir = cwd;
221
+ let foundProj = false;
222
+ for (let i = 0; i < 6; i++) {
223
+ try {
224
+ if (fs.readdirSync(projDir).some(f => f.endsWith('.csproj') || f.endsWith('.sln'))) {
225
+ foundProj = true;
226
+ break;
227
+ }
228
+ } catch {}
229
+ const parent = path.dirname(projDir);
230
+ if (parent === projDir) break;
231
+ projDir = parent;
232
+ }
233
+
234
+ if (!foundProj) {
235
+ return output(fail(
236
+ 'No .csproj or .sln found.\n' +
237
+ 'Create a project first: dotnet new console -o MyApp\n' +
238
+ 'Then run: fchek deps <project_folder>'
239
+ ));
240
+ }
241
+
242
+ const tools_used = [];
243
+ const results = { lang: 'csharp', target: projDir };
244
+
245
+ // dotnet list package -- lists all NuGet packages
246
+ const listRes = spawnSync('dotnet', ['list', projDir, 'package'], {
247
+ encoding: 'utf8', cwd: projDir, timeout: 30000,
248
+ });
249
+ tools_used.push('dotnet list package');
250
+
251
+ const packages = [];
252
+ const lines = (listRes.stdout || '').split('\n');
253
+ for (const line of lines) {
254
+ // Format: " > PackageName requestedVersion resolvedVersion"
255
+ const m = line.match(/>\s+(\S+)\s+(\S+)\s+(\S+)/);
256
+ if (m) packages.push({ name: m[1], requested: m[2], resolved: m[3] });
257
+ }
258
+ results.packages = packages;
259
+ results.package_count = packages.length;
260
+
261
+ // dotnet list package --outdated
262
+ const outdatedRes = spawnSync('dotnet', ['list', projDir, 'package', '--outdated'], {
263
+ encoding: 'utf8', cwd: projDir, timeout: 60000,
264
+ });
265
+ const outdated = [];
266
+ for (const line of (outdatedRes.stdout || '').split('\n')) {
267
+ const m = line.match(/>\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/);
268
+ if (m) outdated.push({ name: m[1], current: m[3], latest: m[4] });
269
+ }
270
+ results.outdated_packages = outdated;
271
+ results.outdated_count = outdated.length;
272
+
273
+ // dotnet list package --vulnerable (requires .NET 7+)
274
+ const vulnRes = spawnSync('dotnet', ['list', projDir, 'package', '--vulnerable'], {
275
+ encoding: 'utf8', cwd: projDir, timeout: 60000,
276
+ });
277
+ const vulnerable = [];
278
+ for (const line of (vulnRes.stdout || '').split('\n')) {
279
+ const m = line.match(/>\s+(\S+)\s+(\S+)\s+(\S+)/);
280
+ if ((m && line.toLowerCase().includes('critical')) || line.toLowerCase().includes('high')) {
281
+ vulnerable.push({ raw: line.trim() });
282
+ }
283
+ }
284
+ results.vulnerable_packages = vulnerable;
285
+
286
+ output(ok({ ...results, tools_used }));
287
+ }
288
+
289
+ // ─── Go ───────────────────────────────────────────────────────────────────────
290
+
291
+ function depsGo(target, timeoutMs) {
292
+ if (!commandExists('go')) {
293
+ return output(fail('go not found. Install: https://go.dev'));
294
+ }
295
+
296
+ const cwd = path.resolve(target === '.' ? process.cwd() : path.dirname(target));
297
+ const tools_used = ['go mod'];
298
+ const results = { lang: 'go', target: cwd };
299
+
300
+ // go mod tidy --dry-run (Go 1.21+)
301
+ const tidyRes = spawnSync('go', ['mod', 'tidy', '-v'], { encoding: 'utf8', cwd, timeout: timeoutMs });
302
+ results.mod_tidy_output = ((tidyRes.stdout || '') + (tidyRes.stderr || '')).slice(0, 2000);
303
+
304
+ // go list -m all — list all modules
305
+ const listRes = spawnSync('go', ['list', '-m', 'all'], { encoding: 'utf8', cwd, timeout: 30_000 });
306
+ results.all_modules = (listRes.stdout || '').trim().split('\n').filter(Boolean).slice(0, 50);
307
+
308
+ // deadcode tool (golang.org/x/tools/cmd/deadcode)
309
+ if (commandExists('deadcode')) {
310
+ tools_used.push('deadcode');
311
+ const dcRes = spawnSync('deadcode', ['-test', './...'], { encoding: 'utf8', cwd, timeout: timeoutMs });
312
+ results.dead_functions = ((dcRes.stdout || '') + (dcRes.stderr || '')).trim().split('\n').filter(Boolean).slice(0, 30);
313
+ } else {
314
+ results.dead_code_note = 'deadcode not found. Install: go install golang.org/x/tools/cmd/deadcode@latest';
315
+ }
316
+
317
+ output(ok({ ...results, tools_used }));
318
+ }
319
+
320
+ // ─── Entry point ─────────────────────────────────────────────────────────────
321
+
322
+ async function run(args) {
323
+ if (args.length === 0 || args[0] === '--help') {
324
+ console.log(HELP);
325
+ return;
326
+ }
327
+
328
+ const target = args[0];
329
+ const showGraph = args.includes('--graph');
330
+
331
+ if (!fs.existsSync(target)) {
332
+ return output(fail(`Not found: ${target}`));
333
+ }
334
+
335
+ const ext = path.extname(target).toLowerCase();
336
+ const stat = fs.statSync(target);
337
+ const isDir = stat.isDirectory();
338
+
339
+ // Detect language
340
+ if (ext === '.py' || (isDir && fs.existsSync(path.join(target, '__init__.py')))) {
341
+ return depsPython(target, showGraph, false, DEFAULT_TIMEOUT);
342
+ }
343
+ if (ext === '.rs' || isDir && fs.existsSync(path.join(target, 'Cargo.toml')) || target === '.') {
344
+ // Check for Cargo.toml first
345
+ if (fs.existsSync(path.join(path.resolve(target), 'Cargo.toml')) || ext === '.rs') {
346
+ return depsRust(target, showGraph, DEFAULT_TIMEOUT);
347
+ }
348
+ }
349
+ if (['.js', '.ts', '.mjs'].includes(ext) || (isDir && fs.existsSync(path.join(target, 'package.json')))) {
350
+ return depsJs(target, showGraph, DEFAULT_TIMEOUT);
351
+ }
352
+ if (ext === '.go' || (isDir && fs.existsSync(path.join(target, 'go.mod')))) {
353
+ return depsGo(target, DEFAULT_TIMEOUT);
354
+ }
355
+ if (ext === '.cs' || (isDir && fs.readdirSync(path.resolve(target)).some(f => f.endsWith('.csproj') || f.endsWith('.sln')))) {
356
+ return depsCSharp(target, DEFAULT_TIMEOUT);
357
+ }
358
+ // Fallback for "." — try all
359
+ if (target === '.') {
360
+ const cwd = process.cwd();
361
+ if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) return depsRust('.', showGraph, DEFAULT_TIMEOUT);
362
+ if (fs.existsSync(path.join(cwd, 'package.json'))) return depsJs('.', showGraph, DEFAULT_TIMEOUT);
363
+ if (fs.existsSync(path.join(cwd, 'go.mod'))) return depsGo('.', DEFAULT_TIMEOUT);
364
+ if (fs.existsSync(path.join(cwd, 'global.json')) || fs.readdirSync(cwd).some(f => f.endsWith('.csproj') || f.endsWith('.sln'))) {
365
+ return depsCSharp('.', DEFAULT_TIMEOUT);
366
+ }
367
+ // Python: look for py files
368
+ return depsPython('.', showGraph, false, DEFAULT_TIMEOUT);
369
+ }
370
+
371
+ output(fail(`Cannot detect language for: ${target}. Supported: .py .rs .js .ts .go .cs`));
372
+ }
373
+
374
+ module.exports = { run };
package/lib/docker.js ADDED
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ const { execSync } = require('child_process');
4
+ const { output, ok, fail } = require('./output');
5
+
6
+ const HELP = `
7
+ fchek docker list [--all]
8
+ fchek docker logs <container> [--tail=<num>]
9
+
10
+ Docker container assistant.
11
+ list: Show running containers. Use --all to show all containers.
12
+ logs: Fetch logs of a container. Customize tail length using --tail (default 50).
13
+ `.trim();
14
+
15
+ function isDockerInstalled() {
16
+ try {
17
+ execSync('docker --version', { stdio: 'ignore' });
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ async function run(args) {
25
+ if (args.includes('--help') || args.includes('-h')) {
26
+ console.log(HELP);
27
+ return;
28
+ }
29
+
30
+ if (!isDockerInstalled()) {
31
+ output(fail('Docker is not installed or not in PATH', 'docker'));
32
+ return;
33
+ }
34
+
35
+ const action = args[0];
36
+ if (!action || !['list', 'logs'].includes(action)) {
37
+ output(fail('Invalid docker command. Use "list" or "logs"', 'docker'));
38
+ return;
39
+ }
40
+
41
+ if (action === 'list') {
42
+ const isAll = args.includes('--all') || args.includes('-a');
43
+ const cmd = `docker ps ${isAll ? '-a' : ''} --format "{{json .}}"`;
44
+ try {
45
+ const out = execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
46
+ const containers = out.split('\n')
47
+ .map(line => line.trim())
48
+ .filter(Boolean)
49
+ .map(line => {
50
+ try {
51
+ return JSON.parse(line);
52
+ } catch {
53
+ return { raw: line };
54
+ }
55
+ });
56
+ output(ok({ containers }, 'docker'));
57
+ } catch (err) {
58
+ output(fail(`Failed to list containers (is Docker daemon running?): ${err.message}`, 'docker'));
59
+ }
60
+ } else if (action === 'logs') {
61
+ const container = args[1];
62
+ if (!container || container.startsWith('-')) {
63
+ output(fail('Please specify a container name or ID', 'docker'));
64
+ return;
65
+ }
66
+
67
+ let tail = '50';
68
+ for (const arg of args) {
69
+ if (arg.startsWith('--tail=')) {
70
+ tail = arg.split('=')[1];
71
+ }
72
+ }
73
+
74
+ const cmd = `docker logs --tail ${tail} ${container}`;
75
+ try {
76
+ const out = execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
77
+ output(ok({ container, logs: out }, 'docker'));
78
+ } catch (err) {
79
+ output(fail(`Failed to get logs for container ${container}: ${err.message}`, 'docker'));
80
+ }
81
+ }
82
+ }
83
+
84
+ module.exports = { run };
package/lib/doctor.js ADDED
@@ -0,0 +1,149 @@
1
+ 'use strict';
2
+
3
+ const { execSync } = require('child_process');
4
+ const os = require('os');
5
+ const { output, ok } = require('./output');
6
+
7
+ const HELP = `
8
+ fchek doctor
9
+
10
+ Check the environment: Node.js version, OS, and all external tools needed by fchek.
11
+ Run this first to see what is installed and what needs to be set up.
12
+ `.trim();
13
+
14
+ const TOOLS = [
15
+ // ── profile ──────────────────────────────────────────
16
+ { name: 'py-spy', cmd: 'py-spy --version', for: 'profile (Python)', install: 'pip install py-spy', platform: ['linux','darwin','win32'] },
17
+ { name: 'python3', cmd: 'python3 --version', for: 'profile / coverage', install: 'https://python.org', platform: ['linux','darwin','win32'], alt: 'python --version' },
18
+ { name: 'valgrind', cmd: 'valgrind --version', for: 'profile (C/C++)', install: 'apt install valgrind', platform: ['linux'] },
19
+
20
+ // ── race ─────────────────────────────────────────────
21
+ { name: 'gcc', cmd: 'gcc --version', for: 'race / coverage (C)', install: 'apt install build-essential', platform: ['linux','darwin'] },
22
+ { name: 'g++', cmd: 'g++ --version', for: 'race (C++)', install: 'apt install build-essential', platform: ['linux','darwin'] },
23
+ { name: 'clang++', cmd: 'clang++ --version', for: 'race (macOS/clang)', install: 'xcode-select --install', platform: ['darwin'] },
24
+
25
+ // ── coverage ─────────────────────────────────────────
26
+ { name: 'coverage', cmd: 'coverage --version', for: 'coverage (Python)', install: 'pip install coverage', platform: ['linux','darwin','win32'] },
27
+ { name: 'gcov', cmd: 'gcov --version', for: 'coverage (C/C++)', install: 'apt install gcc', platform: ['linux'] },
28
+ { name: 'c8', cmd: 'c8 --version', for: 'coverage (JS/TS)', install: 'npm install -g c8', platform: ['linux','darwin','win32'] },
29
+
30
+ // ── bench ─────────────────────────────────────────────
31
+ { name: 'node', cmd: 'node --version', for: 'bench (JS)', install: 'https://nodejs.org', platform: ['linux','darwin','win32'] },
32
+ { name: 'ts-node', cmd: 'ts-node --version', for: 'bench (TypeScript)', install: 'npm install -g ts-node', platform: ['linux','darwin','win32'] },
33
+
34
+ // ── lint ─────────────────────────────────────────────
35
+ { name: 'ruff', cmd: 'ruff --version', for: 'lint (Python)', install: 'pip install ruff', platform: ['linux','darwin','win32'] },
36
+ { name: 'eslint', cmd: 'eslint --version', for: 'lint (JS/TS)', install: 'npm install -g eslint', platform: ['linux','darwin','win32'] },
37
+ { name: 'clang-format',cmd: 'clang-format --version', for: 'lint (C/C++)', install: 'apt install clang-format', platform: ['linux','darwin'] },
38
+ { name: 'clang-tidy', cmd: 'clang-tidy --version', for: 'lint (C/C++)', install: 'apt install clang-tidy', platform: ['linux','darwin'] },
39
+ { name: 'gofmt', cmd: 'gofmt --help', for: 'lint (Go)', install: 'https://go.dev', platform: ['linux','darwin','win32'] },
40
+
41
+ // ── deps ─────────────────────────────────────────────
42
+ { name: 'vulture', cmd: 'vulture --version', for: 'deps (Python dead code)', install: 'pip install vulture', platform: ['linux','darwin','win32'] },
43
+ { name: 'pydeps', cmd: 'pydeps --help', for: 'deps graph (Python)', install: 'pip install pydeps', platform: ['linux','darwin','win32'] },
44
+ { name: 'madge', cmd: 'madge --version', for: 'deps (JS/TS circular)', install: 'npm install -g madge', platform: ['linux','darwin','win32'] },
45
+ { name: 'depcheck', cmd: 'depcheck --version', for: 'deps unused (JS/TS)', install: 'npm install -g depcheck', platform: ['linux','darwin','win32'] },
46
+ { name: 'deadcode', cmd: 'deadcode --help', for: 'deps dead code (Go)', install: 'go install golang.org/x/tools/cmd/deadcode@latest', platform: ['linux','darwin','win32'] },
47
+
48
+ // ── fuzz ─────────────────────────────────────────────
49
+ { name: 'afl-fuzz', cmd: 'afl-fuzz --help', for: 'fuzz (C/C++)', install: 'apt install afl++', platform: ['linux','darwin'] },
50
+ { name: 'cargo', cmd: 'cargo --version', for: 'bench/coverage/fuzz (Rust)', install: 'https://rustup.rs', platform: ['linux','darwin','win32'] },
51
+
52
+ // ── goto / LSP ────────────────────────────────────────
53
+ { name: 'clangd', cmd: 'clangd --version', for: 'goto (C/C++ LSP)', install: 'apt install clangd', platform: ['linux','darwin','win32'] },
54
+ { name: 'rust-analyzer', cmd: 'rust-analyzer --version', for: 'goto (Rust LSP)', install: 'rustup component add rust-analyzer', platform: ['linux','darwin','win32'] },
55
+ { name: 'pyright', cmd: 'pyright --version', for: 'goto (Python LSP)', install: 'pip install pyright', platform: ['linux','darwin','win32'] },
56
+ { name: 'typescript-language-server', cmd: 'typescript-language-server --version', for: 'goto (TS/JS LSP)', install: 'npm install -g typescript-language-server typescript', platform: ['linux','darwin','win32'] },
57
+
58
+ // ── C# / .NET ─────────────────────────────────────────────────────────────────
59
+ { name: 'dotnet', cmd: 'dotnet --version', for: 'run / convention / context (C#)', install: 'https://dotnet.microsoft.com/download', platform: ['linux','darwin','win32'] },
60
+ { name: 'omnisharp', cmd: 'omnisharp --version', for: 'goto (C# LSP)', install: 'dotnet tool install -g omnisharp', platform: ['linux','darwin','win32'], alt: 'OmniSharp --version' },
61
+ { name: 'reportgenerator', cmd: 'reportgenerator --version', for: 'coverage HTML report (C#)', install: 'dotnet tool install -g dotnet-reportgenerator-globaltool', platform: ['linux','darwin','win32'] },
62
+
63
+ // ── secrets ───────────────────────────────────────────────────────────────────
64
+ { name: 'gitleaks', cmd: 'gitleaks version', for: 'secrets (best coverage)', install: 'https://github.com/gitleaks/gitleaks#installing', platform: ['linux','darwin','win32'] },
65
+ { name: 'trufflehog', cmd: 'trufflehog --version',for: 'secrets (alternative)', install: 'pip install trufflehog', platform: ['linux','darwin','win32'] },
66
+
67
+ // ── run ───────────────────────────────────────────────────────────────────────
68
+ { name: 'ts-node', cmd: 'ts-node --version', for: 'run (TypeScript files)', install: 'npm install -g ts-node', platform: ['linux','darwin','win32'] },
69
+ { name: 'ruby', cmd: 'ruby --version', for: 'run (.rb files)', install: 'https://www.ruby-lang.org', platform: ['linux','darwin','win32'] },
70
+
71
+ // ── Windows tools ─────────────────────────────────────────────────────────────
72
+ { name: 'powershell', cmd: 'powershell -Command "$PSVersionTable.PSVersion.ToString()"', for: 'screenshot/launch/winlog/registry/process', install: 'Built-in on Windows', platform: ['win32'] },
73
+ { name: 'dotnet', cmd: 'dotnet --version', for: 'run/lint/coverage (C#/.NET)', install: 'https://dotnet.microsoft.com/download', platform: ['linux','darwin','win32'] },
74
+ { name: 'reportgenerator', cmd: 'reportgenerator --version', for: 'coverage HTML (C#)', install: 'dotnet tool install -g dotnet-reportgenerator-globaltool', platform: ['linux','darwin','win32'] },
75
+ { name: 'git', cmd: 'git --version', for: 'git status/diff/commit', install: 'https://git-scm.com', platform: ['linux','darwin','win32'] },
76
+
77
+ // ── deps-check ────────────────────────────────────────────────────────────────
78
+ // deps-check uses https module directly (no external tool needed)
79
+ ];
80
+
81
+ function checkTool(tool, platform) {
82
+ if (!tool.platform.includes(platform)) {
83
+ return { name: tool.name, status: 'skipped', reason: `Not applicable on ${platform}`, for: tool.for };
84
+ }
85
+
86
+ let version = null;
87
+ let found = false;
88
+
89
+ for (const cmd of [tool.cmd, tool.alt].filter(Boolean)) {
90
+ try {
91
+ const out = execSync(cmd, { encoding: 'utf8', stdio: ['ignore','pipe','ignore'], timeout: 5000 });
92
+ version = out.trim().split('\n')[0].slice(0, 80);
93
+ found = true;
94
+ break;
95
+ } catch {}
96
+ }
97
+
98
+ return {
99
+ name: tool.name,
100
+ status: found ? 'ok' : 'missing',
101
+ version: version || null,
102
+ for: tool.for,
103
+ install: found ? undefined : tool.install,
104
+ };
105
+ }
106
+
107
+ async function run(args) {
108
+ if (args[0] === '--help') { console.log(HELP); return; }
109
+
110
+ const platform = os.platform();
111
+ const nodeVer = process.version;
112
+ const nodeMajor = parseInt(nodeVer.slice(1), 10);
113
+
114
+ const results = TOOLS.map(t => checkTool(t, platform));
115
+ const missing = results.filter(r => r.status === 'missing');
116
+ const present = results.filter(r => r.status === 'ok');
117
+ const skipped = results.filter(r => r.status === 'skipped');
118
+
119
+ const platformWarnings = [];
120
+ if (platform === 'win32') {
121
+ platformWarnings.push('race and fuzz (C/C++) require WSL on Windows. Install: wsl --install');
122
+ platformWarnings.push('valgrind and afl++ are not available on native Windows.');
123
+ platformWarnings.push('AFL++ fuzzing: use WSL or Docker.');
124
+ }
125
+ if (platform === 'darwin') {
126
+ platformWarnings.push('valgrind does not support macOS >= 10.15. Use AddressSanitizer via clang for race detection.');
127
+ platformWarnings.push('AFL++ on macOS may require: brew install afl++ and SIP adjustments.');
128
+ }
129
+
130
+ output(ok({
131
+ environment: {
132
+ platform,
133
+ node_version: nodeVer,
134
+ node_ok: nodeMajor >= 18,
135
+ node_warning: nodeMajor < 18 ? 'Node.js >= 18 required' : null,
136
+ },
137
+ tools: results,
138
+ summary: {
139
+ total: results.length,
140
+ ok: present.length,
141
+ missing: missing.length,
142
+ skipped: skipped.length,
143
+ },
144
+ platform_warnings: platformWarnings,
145
+ missing_installs: missing.map(m => ({ tool: m.name, for: m.for, install: m.install })),
146
+ }));
147
+ }
148
+
149
+ module.exports = { run };