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.
- package/README.md +64 -0
- package/bin/fchek.js +107 -0
- package/lib/api.js +110 -0
- package/lib/audit.js +211 -0
- package/lib/bench.js +248 -0
- package/lib/config.js +191 -0
- package/lib/context.js +356 -0
- package/lib/convention.js +526 -0
- package/lib/coverage.js +604 -0
- package/lib/db.js +135 -0
- package/lib/deps-check.js +264 -0
- package/lib/deps.js +374 -0
- package/lib/docker.js +84 -0
- package/lib/doctor.js +149 -0
- package/lib/dom.js +226 -0
- package/lib/fuzz.js +470 -0
- package/lib/git.js +290 -0
- package/lib/goto.js +544 -0
- package/lib/launch.js +182 -0
- package/lib/lint.js +624 -0
- package/lib/new_features.test.js +181 -0
- package/lib/output.js +46 -0
- package/lib/port.js +173 -0
- package/lib/process.js +228 -0
- package/lib/profile.js +453 -0
- package/lib/python.js +41 -0
- package/lib/race.js +186 -0
- package/lib/registry.js +179 -0
- package/lib/repl.js +135 -0
- package/lib/run.js +403 -0
- package/lib/screenshot.js +152 -0
- package/lib/secrets.js +257 -0
- package/lib/state.js +219 -0
- package/lib/test.js +471 -0
- package/lib/vuln.js +253 -0
- package/lib/watch.js +240 -0
- package/lib/winlog.js +123 -0
- package/package.json +27 -0
- package/skills/ACTIVATE.md +274 -0
- package/skills/README.md +163 -0
- package/skills/agent.md +444 -0
- package/skills/api.md +47 -0
- package/skills/bench.md +117 -0
- package/skills/context.md +116 -0
- package/skills/convention.md +143 -0
- package/skills/coverage.md +99 -0
- package/skills/csharp.md +97 -0
- package/skills/db.md +66 -0
- package/skills/deps-check.md +135 -0
- package/skills/deps.md +143 -0
- package/skills/docker.md +61 -0
- package/skills/dom.md +56 -0
- package/skills/fuzz.md +167 -0
- package/skills/goto.md +111 -0
- package/skills/lint.md +123 -0
- package/skills/port.md +57 -0
- package/skills/profile.md +91 -0
- package/skills/race.md +117 -0
- package/skills/repl.md +81 -0
- package/skills/rules.md +318 -0
- package/skills/run.md +135 -0
- package/skills/secrets.md +170 -0
- package/skills/security.md +360 -0
- package/skills/state.md +261 -0
- package/skills/vuln.md +57 -0
- 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 };
|