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/bench.js ADDED
@@ -0,0 +1,248 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * bench.js — benchmark diff before/after refactoring
5
+ *
6
+ * Runs the same target twice (before and after a change) and compares:
7
+ * - wall time
8
+ * - memory (RSS)
9
+ * - custom metric if the program outputs a number
10
+ *
11
+ * Modes:
12
+ * --before=<file|cmd> Run this as "before" version
13
+ * --after=<file|cmd> Run this as "after" version
14
+ * --runs=N How many times to run each (default: 5) for stable average
15
+ * --timeout=ms Per-run timeout (default: 30000)
16
+ *
17
+ * Language auto-detection for plain file args:
18
+ * .py → python <file>
19
+ * .rs → cargo run (in project root)
20
+ * .js → node <file>
21
+ * .cpp → compile then run
22
+ */
23
+
24
+ const { spawnSync } = require('child_process');
25
+ const path = require('path');
26
+ const fs = require('fs');
27
+ const os = require('os');
28
+ const { output, ok, fail } = require('./output');
29
+
30
+ const HELP = `
31
+ fchek bench <file> --before=<ver_a> --after=<ver_b> [--runs=5] [--timeout=30000]
32
+
33
+ Benchmark two versions of the same code and show the diff.
34
+ Eliminates "I think it got faster" guesswork.
35
+
36
+ Arguments:
37
+ <file> The entry point to benchmark
38
+ --before=<path> "Before" version (file path or shell command)
39
+ --after=<path> "After" version (file path or shell command)
40
+ --runs=N Number of runs per version for stable average (default: 5)
41
+ --timeout=ms Per-run timeout in ms (default: 30000)
42
+
43
+ Output includes:
44
+ - avg/min/max wall time per version
45
+ - memory RSS
46
+ - speedup ratio (after vs before)
47
+ - raw diff of stdout between versions
48
+
49
+ Examples:
50
+ fchek bench main.py --before=main_old.py --after=main.py
51
+ fchek bench . --before="cargo run --release -- old" --after="cargo run --release"
52
+ fchek bench app.js --before=app_v1.js --after=app_v2.js --runs=10
53
+ `.trim();
54
+
55
+ const DEFAULT_RUNS = 5;
56
+ const DEFAULT_TIMEOUT = 30_000;
57
+
58
+ function detectRunner(file) {
59
+ const ext = path.extname(file).toLowerCase();
60
+ if (ext === '.py') {
61
+ const py = require('./python').findPython();
62
+ if (!py) return null;
63
+ return [py, [file]];
64
+ }
65
+ if (ext === '.js') return ['node', [file]];
66
+ if (ext === '.ts') return ['ts-node', [file]];
67
+ if (['.cpp', '.cc', '.c'].includes(ext)) return null; // needs compile step
68
+ return null;
69
+ }
70
+
71
+ function compileC(file) {
72
+ const ext = path.extname(file).toLowerCase();
73
+ const compiler = ext === '.cpp' || ext === '.cc' ? 'g++' : 'gcc';
74
+ const outBin = path.join(os.tmpdir(), `fchek_bench_${path.basename(file, ext)}_${Date.now()}`);
75
+ const res = spawnSync(compiler, ['-O2', '-o', outBin, file], { encoding: 'utf8' });
76
+ if (res.status !== 0) return { error: res.stderr };
77
+ return { bin: outBin };
78
+ }
79
+
80
+ function runOnce(cmd, args, timeoutMs) {
81
+ const t0 = process.hrtime.bigint();
82
+ const res = spawnSync(cmd, args, {
83
+ encoding: 'utf8',
84
+ timeout: timeoutMs,
85
+ maxBuffer: 1024 * 1024,
86
+ });
87
+ const t1 = process.hrtime.bigint();
88
+ const wallMs = Number(t1 - t0) / 1e6;
89
+
90
+ if (res.error?.code === 'ETIMEDOUT') {
91
+ return { error: `timed_out after ${timeoutMs}ms` };
92
+ }
93
+ if (res.error) {
94
+ return { error: res.error.message };
95
+ }
96
+
97
+ return {
98
+ wall_ms: Math.round(wallMs),
99
+ exit_code: res.status,
100
+ stdout: (res.stdout || '').slice(0, 2000),
101
+ stderr: (res.stderr || '').slice(0, 500),
102
+ // RSS from /proc/self/status is not available cross-platform via spawnSync
103
+ // We record approximate Node host memory instead as a proxy
104
+ };
105
+ }
106
+
107
+ function runN(cmd, args, n, timeoutMs) {
108
+ const results = [];
109
+ for (let i = 0; i < n; i++) {
110
+ const r = runOnce(cmd, args, timeoutMs);
111
+ if (r.error) return { error: r.error };
112
+ results.push(r);
113
+ }
114
+
115
+ const times = results.map(r => r.wall_ms);
116
+ const avg = Math.round(times.reduce((a, b) => a + b, 0) / times.length);
117
+ const min = Math.min(...times);
118
+ const max = Math.max(...times);
119
+
120
+ // Measurement note:
121
+ // wall_ms includes Node.js process fork overhead (~5-50ms per spawn).
122
+ // min_ms is the PRIMARY metric — least affected by OS scheduling noise.
123
+ // For programs < 100ms, overhead may dominate. Use --runs=20+ for stability.
124
+ const overhead_warning = min < 100
125
+ ? `min_ms=${min}ms is under 100ms. Fork overhead (~5-50ms) may be significant. Use --runs=${Math.max(n, 20)} for better accuracy.`
126
+ : null;
127
+
128
+ return {
129
+ runs: n,
130
+ min_ms: min, // PRIMARY — use this for comparison
131
+ avg_ms: avg,
132
+ max_ms: max,
133
+ jitter_ms: max - min,
134
+ overhead_warning,
135
+ stdout_sample: results[0].stdout,
136
+ exit_codes: results.map(r => r.exit_code),
137
+ };
138
+ }
139
+
140
+ function buildDiff(before, after) {
141
+ if (!before || !after) return null;
142
+ const bLines = before.split('\n');
143
+ const aLines = after.split('\n');
144
+ const diff = [];
145
+
146
+ const maxLen = Math.max(bLines.length, aLines.length);
147
+ for (let i = 0; i < maxLen; i++) {
148
+ const b = bLines[i] ?? null;
149
+ const a = aLines[i] ?? null;
150
+ if (b !== a) {
151
+ diff.push({ line: i + 1, before: b, after: a });
152
+ }
153
+ }
154
+ return diff.slice(0, 30); // cap at 30 diff lines
155
+ }
156
+
157
+ function resolveTarget(spec, defaultExt) {
158
+ // If it looks like a shell command (spaces), split it
159
+ if (spec.includes(' ')) {
160
+ const parts = spec.split(/\s+/);
161
+ return { cmd: parts[0], args: parts.slice(1), tmpBin: null };
162
+ }
163
+
164
+ if (!fs.existsSync(spec)) {
165
+ return { error: `File not found: ${spec}` };
166
+ }
167
+
168
+ const ext = path.extname(spec).toLowerCase();
169
+ if (['.c', '.cpp', '.cc'].includes(ext)) {
170
+ const compiled = compileC(spec);
171
+ if (compiled.error) return { error: `Compile failed: ${compiled.error}` };
172
+ return { cmd: compiled.bin, args: [], tmpBin: compiled.bin };
173
+ }
174
+
175
+ const runner = detectRunner(spec);
176
+ if (!runner) {
177
+ return { error: `Cannot auto-detect runner for: ${spec}. Use --before="<cmd> ${spec}"` };
178
+ }
179
+
180
+ return { cmd: runner[0], args: runner[1], tmpBin: null };
181
+ }
182
+
183
+ async function run(args) {
184
+ if (args.length === 0 || args[0] === '--help') {
185
+ console.log(HELP);
186
+ return;
187
+ }
188
+
189
+ const beforeArg = args.find(a => a.startsWith('--before='))?.replace('--before=', '');
190
+ const afterArg = args.find(a => a.startsWith('--after='))?.replace('--after=', '');
191
+ const runsArg = parseInt((args.find(a => a.startsWith('--runs=')) ?? `--runs=${DEFAULT_RUNS}`).replace('--runs=', ''), 10);
192
+ const timeoutArg = parseInt((args.find(a => a.startsWith('--timeout=')) ?? `--timeout=${DEFAULT_TIMEOUT}`).replace('--timeout=', ''), 10);
193
+
194
+ if (!beforeArg || !afterArg) {
195
+ return output(fail('Both --before=<path> and --after=<path> are required'));
196
+ }
197
+
198
+ const beforeTarget = resolveTarget(beforeArg);
199
+ const afterTarget = resolveTarget(afterArg);
200
+
201
+ if (beforeTarget.error) return output(fail(beforeTarget.error));
202
+ if (afterTarget.error) return output(fail(afterTarget.error));
203
+
204
+ const beforeResult = runN(beforeTarget.cmd, beforeTarget.args, runsArg, timeoutArg);
205
+ const afterResult = runN(afterTarget.cmd, afterTarget.args, runsArg, timeoutArg);
206
+
207
+ // Cleanup temp binaries
208
+ if (beforeTarget.tmpBin) try { fs.unlinkSync(beforeTarget.tmpBin); } catch {}
209
+ if (afterTarget.tmpBin) try { fs.unlinkSync(afterTarget.tmpBin); } catch {}
210
+
211
+ if (beforeResult.error) return output(fail(`Before run failed: ${beforeResult.error}`));
212
+ if (afterResult.error) return output(fail(`After run failed: ${afterResult.error}`));
213
+
214
+ // Use min_ms as primary comparison metric — most stable, least OS noise
215
+ const speedup = beforeResult.min_ms / afterResult.min_ms;
216
+ const timeDeltaMs = afterResult.min_ms - beforeResult.min_ms;
217
+ const verdict = speedup >= 1.05
218
+ ? 'faster'
219
+ : speedup <= 0.95
220
+ ? 'slower'
221
+ : 'no_significant_change';
222
+
223
+ const stdoutDiff = buildDiff(beforeResult.stdout_sample, afterResult.stdout_sample);
224
+
225
+ output(ok({
226
+ before: {
227
+ spec: beforeArg,
228
+ ...beforeResult,
229
+ },
230
+ after: {
231
+ spec: afterArg,
232
+ ...afterResult,
233
+ },
234
+ comparison: {
235
+ time_delta_ms: timeDeltaMs,
236
+ speedup_ratio: parseFloat(speedup.toFixed(3)),
237
+ verdict, // "faster" | "slower" | "no_significant_change"
238
+ pct_change: parseFloat(((speedup - 1) * 100).toFixed(1)),
239
+ },
240
+ stdout_diff: stdoutDiff,
241
+ config: {
242
+ runs: runsArg,
243
+ timeout_ms: timeoutArg,
244
+ },
245
+ }));
246
+ }
247
+
248
+ module.exports = { run };
package/lib/config.js ADDED
@@ -0,0 +1,191 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * config.js — read and validate .fchekrc project config
5
+ *
6
+ * .fchekrc is a JSON file in project root that configures fchek behavior.
7
+ * Agent reads this first to understand project-specific rules and thresholds.
8
+ */
9
+
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+ const { output, ok, fail } = require('./output');
13
+
14
+ const HELP = `
15
+ fchek config [action] [key] [value]
16
+
17
+ Read and manage .fchekrc project configuration.
18
+
19
+ Actions:
20
+ show Show current config (merged: defaults + .fchekrc)
21
+ init Create a .fchekrc with defaults for this project
22
+ get <key> Get a specific config value
23
+ set <key> <val> Set a config value in .fchekrc
24
+
25
+ Example .fchekrc:
26
+ {
27
+ "lang": "python",
28
+ "test_command": "pytest -x",
29
+ "lint_threshold": 0,
30
+ "coverage_threshold": 80,
31
+ "secrets_scan_on_run": true,
32
+ "ignore_paths": ["migrations/", "*.generated.py"],
33
+ "auto_fix_lint": true
34
+ }
35
+ `.trim();
36
+
37
+ const DEFAULTS = {
38
+ lang: null, // auto-detect if null
39
+ test_command: null, // auto-detect if null
40
+ lint_threshold: 0, // 0 = any issue is a failure
41
+ coverage_threshold: 70, // minimum coverage %
42
+ secrets_scan_on_run: true, // run secrets check with every fchek run
43
+ auto_fix_lint: false, // auto-apply lint fixes
44
+ ignore_paths: [], // paths to skip in all scans
45
+ timeout_ms: 60000, // default timeout for commands
46
+ git_auto_stage: false, // auto-stage on commit
47
+ };
48
+
49
+ function findProjectRoot(startDir) {
50
+ const markers = ['package.json', 'Cargo.toml', '.git', 'pyproject.toml', 'go.mod', '.csproj'];
51
+ let dir = path.resolve(startDir);
52
+ for (let i = 0; i < 8; i++) {
53
+ if (markers.some(m => {
54
+ if (m === '.csproj') {
55
+ try { return fs.readdirSync(dir).some(f => f.endsWith('.csproj')); } catch { return false; }
56
+ }
57
+ return fs.existsSync(path.join(dir, m));
58
+ })) return dir;
59
+ const parent = path.dirname(dir);
60
+ if (parent === dir) break;
61
+ dir = parent;
62
+ }
63
+ return path.resolve(startDir);
64
+ }
65
+
66
+ function loadRc(dir) {
67
+ const rcPath = path.join(dir, '.fchekrc');
68
+ if (!fs.existsSync(rcPath)) return {};
69
+ try {
70
+ return JSON.parse(fs.readFileSync(rcPath, 'utf8'));
71
+ } catch (e) {
72
+ throw new Error(`.fchekrc parse error: ${e.message}`);
73
+ }
74
+ }
75
+
76
+ function mergedConfig(dir) {
77
+ const userRc = loadRc(dir);
78
+ return { ...DEFAULTS, ...userRc, _source: path.join(dir, '.fchekrc') };
79
+ }
80
+
81
+ function detectLangForInit(dir) {
82
+ // Check current dir first, then walk up
83
+ const check = (d) => {
84
+ if (fs.existsSync(path.join(d, 'Cargo.toml'))) return 'rust';
85
+ if (fs.existsSync(path.join(d, 'go.mod'))) return 'go';
86
+ if (fs.existsSync(path.join(d, 'pyproject.toml'))
87
+ || fs.existsSync(path.join(d, 'requirements.txt'))
88
+ || fs.existsSync(path.join(d, 'setup.py'))
89
+ || fs.existsSync(path.join(d, 'conftest.py'))) return 'python';
90
+ if (fs.existsSync(path.join(d, 'package.json'))) return 'javascript';
91
+ try {
92
+ if (fs.readdirSync(d).some(f => f.endsWith('.csproj') || f.endsWith('.sln'))) return 'csharp';
93
+ } catch {}
94
+ // Check if majority of source files are .py
95
+ try {
96
+ const files = fs.readdirSync(d);
97
+ const pyFiles = files.filter(f => f.endsWith('.py')).length;
98
+ const jsFiles = files.filter(f => f.endsWith('.js') || f.endsWith('.ts')).length;
99
+ if (pyFiles > 0 && pyFiles >= jsFiles) return 'python';
100
+ } catch {}
101
+ return null;
102
+ };
103
+ return check(dir);
104
+ }
105
+
106
+ async function run(args) {
107
+ if (args[0] === '--help') { console.log(HELP); return; }
108
+
109
+ const root = findProjectRoot(process.cwd());
110
+ const action = args[0] || 'show';
111
+
112
+ switch (action) {
113
+ case 'show': {
114
+ let config;
115
+ try { config = mergedConfig(root); }
116
+ catch (e) { return output(fail(e.message)); }
117
+ output(ok({ action: 'show', root, config }));
118
+ break;
119
+ }
120
+
121
+ case 'init': {
122
+ const rcPath = path.join(root, '.fchekrc');
123
+ if (fs.existsSync(rcPath)) {
124
+ return output(fail(`.fchekrc already exists at ${rcPath}. Use "fchek config show" to view it.`));
125
+ }
126
+ const lang = detectLangForInit(root);
127
+ const initConfig = {
128
+ lang,
129
+ test_command: null,
130
+ lint_threshold: 0,
131
+ coverage_threshold: 70,
132
+ secrets_scan_on_run: true,
133
+ auto_fix_lint: false,
134
+ ignore_paths: [],
135
+ timeout_ms: 60000,
136
+ };
137
+ fs.writeFileSync(rcPath, JSON.stringify(initConfig, null, 2), 'utf8');
138
+
139
+ // Add to .gitignore? No — .fchekrc should be committed (it's project config, not secrets)
140
+ output(ok({ action: 'init', file: rcPath, config: initConfig }));
141
+ break;
142
+ }
143
+
144
+ case 'get': {
145
+ const key = args[1];
146
+ if (!key) return output(fail('Usage: fchek config get <key>'));
147
+ let config;
148
+ try { config = mergedConfig(root); } catch (e) { return output(fail(e.message)); }
149
+ if (!(key in config)) return output(fail(`Key not found: "${key}"`));
150
+ output(ok({ action: 'get', key, value: config[key] }));
151
+ break;
152
+ }
153
+
154
+ case 'set': {
155
+ const key = args[1];
156
+ const value = args.slice(2).join(' ');
157
+ if (!key) return output(fail('Usage: fchek config set <key> <value>'));
158
+
159
+ const rcPath = path.join(root, '.fchekrc');
160
+ let rc = {};
161
+ if (fs.existsSync(rcPath)) {
162
+ try { rc = JSON.parse(fs.readFileSync(rcPath, 'utf8')); }
163
+ catch (e) { return output(fail(`.fchekrc parse error: ${e.message}`)); }
164
+ }
165
+
166
+ // Auto-convert types
167
+ let parsed = value;
168
+ if (value === 'true') parsed = true;
169
+ if (value === 'false') parsed = false;
170
+ if (/^\d+$/.test(value)) parsed = parseInt(value, 10);
171
+ if (value.startsWith('[')) try { parsed = JSON.parse(value); } catch {}
172
+
173
+ rc[key] = parsed;
174
+ fs.writeFileSync(rcPath, JSON.stringify(rc, null, 2), 'utf8');
175
+ output(ok({ action: 'set', key, value: parsed, file: rcPath }));
176
+ break;
177
+ }
178
+
179
+ default:
180
+ output(fail(`Unknown action: "${action}". Valid: show, init, get, set`));
181
+ }
182
+ }
183
+
184
+ // Export for use by other modules
185
+ function getConfig(dir) {
186
+ const root = findProjectRoot(dir || process.cwd());
187
+ try { return mergedConfig(root); }
188
+ catch { return { ...DEFAULTS }; }
189
+ }
190
+
191
+ module.exports = { run, getConfig, DEFAULTS };