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/vuln.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const { output, ok, fail } = require('./output');
|
|
7
|
+
|
|
8
|
+
const HELP = `
|
|
9
|
+
fchek vuln [dir]
|
|
10
|
+
|
|
11
|
+
Scan project dependencies for vulnerabilities and typosquatting.
|
|
12
|
+
Automatically detects package.json, requirements.txt, and Cargo.toml.
|
|
13
|
+
Run npm audit / pip-audit / cargo audit, and runs a typosquatting check.
|
|
14
|
+
`.trim();
|
|
15
|
+
|
|
16
|
+
const POPULAR_PACKAGES = {
|
|
17
|
+
npm: [
|
|
18
|
+
'react', 'react-dom', 'lodash', 'express', 'chalk', 'commander', 'tslib',
|
|
19
|
+
'axios', 'moment', 'uuid', 'dotenv', 'fs-extra', 'async', 'prop-types',
|
|
20
|
+
'classnames', 'jquery', 'glob', 'webpack', 'typescript', 'request',
|
|
21
|
+
'cheerio', 'minimist', 'debug', 'bluebird', 'rxjs', 'vue', 'angular'
|
|
22
|
+
],
|
|
23
|
+
pip: [
|
|
24
|
+
'requests', 'numpy', 'pandas', 'flask', 'django', 'urllib3', 'six',
|
|
25
|
+
'cryptography', 'jinja2', 'scipy', 'matplotlib', 'pytest', 'boto3',
|
|
26
|
+
'click', 'pyyaml', 'black', 'ruff', 'pip', 'setuptools', 'wheel'
|
|
27
|
+
],
|
|
28
|
+
cargo: [
|
|
29
|
+
'serde', 'tokio', 'rand', 'reqwest', 'clap', 'syn', 'quote', 'log',
|
|
30
|
+
'lazy_static', 'anyhow', 'futures', 'libc', 'hyper', 'regex', 'cargo'
|
|
31
|
+
]
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function levenshtein(a, b) {
|
|
35
|
+
const tmp = [];
|
|
36
|
+
for (let i = 0; i <= a.length; i++) {
|
|
37
|
+
tmp[i] = [i];
|
|
38
|
+
}
|
|
39
|
+
for (let j = 0; j <= b.length; j++) {
|
|
40
|
+
tmp[0][j] = j;
|
|
41
|
+
}
|
|
42
|
+
for (let i = 1; i <= a.length; i++) {
|
|
43
|
+
for (let j = 1; j <= b.length; j++) {
|
|
44
|
+
if (a[i - 1] === b[j - 1]) {
|
|
45
|
+
tmp[i][j] = tmp[i - 1][j - 1];
|
|
46
|
+
} else {
|
|
47
|
+
tmp[i][j] = Math.min(
|
|
48
|
+
tmp[i - 1][j - 1] + 1, // substitution
|
|
49
|
+
tmp[i][j - 1] + 1, // insertion
|
|
50
|
+
tmp[i - 1][j] + 1 // deletion
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return tmp[a.length][b.length];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function checkTyposquatting(name, type) {
|
|
59
|
+
const popular = POPULAR_PACKAGES[type] || [];
|
|
60
|
+
const lowerName = name.toLowerCase();
|
|
61
|
+
|
|
62
|
+
// If exact match, it's fine
|
|
63
|
+
if (popular.includes(lowerName)) return null;
|
|
64
|
+
|
|
65
|
+
for (const pop of popular) {
|
|
66
|
+
const dist = levenshtein(lowerName, pop);
|
|
67
|
+
// If name is very close (distance 1 or 2) but not identical, warn
|
|
68
|
+
if (dist > 0 && dist <= 2 && Math.abs(lowerName.length - pop.length) <= 2) {
|
|
69
|
+
return {
|
|
70
|
+
package: name,
|
|
71
|
+
type,
|
|
72
|
+
suspectedTyposquatOf: pop,
|
|
73
|
+
distance: dist
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function run(args) {
|
|
81
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
82
|
+
console.log(HELP);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const targetDir = args.find(a => !a.startsWith('-')) || '.';
|
|
87
|
+
const resolvedDir = path.resolve(targetDir);
|
|
88
|
+
|
|
89
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
90
|
+
output(fail(`Directory does not exist: ${targetDir}`, 'vuln'));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const results = {
|
|
95
|
+
packages_checked: 0,
|
|
96
|
+
vulnerabilities: [],
|
|
97
|
+
typosquatting_warnings: [],
|
|
98
|
+
audit_ran: []
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Node (package.json)
|
|
102
|
+
const pkgPath = path.join(resolvedDir, 'package.json');
|
|
103
|
+
if (fs.existsSync(pkgPath)) {
|
|
104
|
+
try {
|
|
105
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
106
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
107
|
+
|
|
108
|
+
for (const name of Object.keys(deps)) {
|
|
109
|
+
results.packages_checked++;
|
|
110
|
+
const warn = checkTyposquatting(name, 'npm');
|
|
111
|
+
if (warn) results.typosquatting_warnings.push(warn);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Try running npm audit
|
|
115
|
+
try {
|
|
116
|
+
const auditOut = execSync('npm audit --json', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], cwd: resolvedDir });
|
|
117
|
+
const auditJson = JSON.parse(auditOut);
|
|
118
|
+
results.audit_ran.push('npm');
|
|
119
|
+
if (auditJson.vulnerabilities) {
|
|
120
|
+
for (const [name, vuln] of Object.entries(auditJson.vulnerabilities)) {
|
|
121
|
+
results.vulnerabilities.push({
|
|
122
|
+
package: name,
|
|
123
|
+
severity: vuln.severity,
|
|
124
|
+
via: vuln.via,
|
|
125
|
+
effects: vuln.effects
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch (auditErr) {
|
|
130
|
+
// npm audit exits with non-zero if vulnerabilities are found
|
|
131
|
+
results.audit_ran.push('npm');
|
|
132
|
+
try {
|
|
133
|
+
const auditJson = JSON.parse(auditErr.stdout);
|
|
134
|
+
if (auditJson.vulnerabilities) {
|
|
135
|
+
for (const [name, vuln] of Object.entries(auditJson.vulnerabilities)) {
|
|
136
|
+
results.vulnerabilities.push({
|
|
137
|
+
package: name,
|
|
138
|
+
severity: vuln.severity,
|
|
139
|
+
via: vuln.via,
|
|
140
|
+
effects: vuln.effects
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// If JSON parse failed, it was a real execution error or npm not installed
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch (err) {
|
|
149
|
+
// Ignore parse/read errors
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Python (requirements.txt)
|
|
154
|
+
const reqPath = path.join(resolvedDir, 'requirements.txt');
|
|
155
|
+
if (fs.existsSync(reqPath)) {
|
|
156
|
+
try {
|
|
157
|
+
const content = fs.readFileSync(reqPath, 'utf8');
|
|
158
|
+
const lines = content.split('\n');
|
|
159
|
+
for (const line of lines) {
|
|
160
|
+
const trimmed = line.trim();
|
|
161
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
162
|
+
// Match package name (before ==, >=, etc.)
|
|
163
|
+
const match = trimmed.match(/^([a-zA-Z0-9_\-]+)/);
|
|
164
|
+
if (match) {
|
|
165
|
+
const name = match[1];
|
|
166
|
+
results.packages_checked++;
|
|
167
|
+
const warn = checkTyposquatting(name, 'pip');
|
|
168
|
+
if (warn) results.typosquatting_warnings.push(warn);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Try running pip-audit if installed
|
|
173
|
+
try {
|
|
174
|
+
const auditOut = execSync('pip-audit --format json', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], cwd: resolvedDir });
|
|
175
|
+
const auditJson = JSON.parse(auditOut);
|
|
176
|
+
results.audit_ran.push('pip');
|
|
177
|
+
if (auditJson.dependencies) {
|
|
178
|
+
for (const dep of auditJson.dependencies) {
|
|
179
|
+
if (dep.vulns && dep.vulns.length > 0) {
|
|
180
|
+
for (const v of dep.vulns) {
|
|
181
|
+
results.vulnerabilities.push({
|
|
182
|
+
package: dep.name,
|
|
183
|
+
id: v.id,
|
|
184
|
+
severity: 'unknown',
|
|
185
|
+
description: v.description
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
// pip-audit not installed or failed
|
|
193
|
+
}
|
|
194
|
+
} catch (err) {
|
|
195
|
+
// Ignore
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Rust (Cargo.toml)
|
|
200
|
+
const cargoPath = path.join(resolvedDir, 'Cargo.toml');
|
|
201
|
+
if (fs.existsSync(cargoPath)) {
|
|
202
|
+
try {
|
|
203
|
+
const content = fs.readFileSync(cargoPath, 'utf8');
|
|
204
|
+
// Simple parser for dependencies block
|
|
205
|
+
const lines = content.split('\n');
|
|
206
|
+
let inDeps = false;
|
|
207
|
+
for (const line of lines) {
|
|
208
|
+
const trimmed = line.trim();
|
|
209
|
+
if (trimmed.startsWith('[dependencies]') || trimmed.startsWith('[dev-dependencies]')) {
|
|
210
|
+
inDeps = true;
|
|
211
|
+
continue;
|
|
212
|
+
} else if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
213
|
+
inDeps = false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (inDeps && trimmed && !trimmed.startsWith('#')) {
|
|
217
|
+
const eqIdx = trimmed.indexOf('=');
|
|
218
|
+
if (eqIdx !== -1) {
|
|
219
|
+
const name = trimmed.slice(0, eqIdx).trim();
|
|
220
|
+
results.packages_checked++;
|
|
221
|
+
const warn = checkTyposquatting(name, 'cargo');
|
|
222
|
+
if (warn) results.typosquatting_warnings.push(warn);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Try running cargo audit if available
|
|
228
|
+
try {
|
|
229
|
+
const auditOut = execSync('cargo audit --json', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], cwd: resolvedDir });
|
|
230
|
+
const auditJson = JSON.parse(auditOut);
|
|
231
|
+
results.audit_ran.push('cargo');
|
|
232
|
+
if (auditJson.vulnerabilities && auditJson.vulnerabilities.list) {
|
|
233
|
+
for (const v of auditJson.vulnerabilities.list) {
|
|
234
|
+
results.vulnerabilities.push({
|
|
235
|
+
package: v.package.name,
|
|
236
|
+
id: v.advisory.id,
|
|
237
|
+
severity: 'unknown',
|
|
238
|
+
description: v.advisory.description
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
// cargo-audit not available
|
|
244
|
+
}
|
|
245
|
+
} catch (err) {
|
|
246
|
+
// Ignore
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
output(ok(results, 'vuln'));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = { run };
|
package/lib/watch.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* watch.js — watch files and auto-rebuild/restart on changes
|
|
5
|
+
*
|
|
6
|
+
* Solves WPF/C# pain: change XAML → app auto-rebuilds → re-launches.
|
|
7
|
+
* No manual rebuild steps.
|
|
8
|
+
*
|
|
9
|
+
* Uses Node.js fs.watch (built-in, no external deps).
|
|
10
|
+
* On change: runs build command, then optionally restarts app.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { spawnSync, spawn } = require('child_process');
|
|
16
|
+
const { output, ok, fail } = require('./output');
|
|
17
|
+
|
|
18
|
+
const HELP = `
|
|
19
|
+
fchek watch <dir> [--cmd=<build_cmd>] [--ext=cs,xaml] [--launch=<exe>] [--debounce=1000]
|
|
20
|
+
|
|
21
|
+
Watch files for changes and auto-rebuild. For WPF: change XAML → rebuilds → relaunches.
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--cmd=<command> Build command to run on change (default: dotnet build)
|
|
25
|
+
--ext=cs,xaml File extensions to watch (default: cs,xaml,js,ts,py,rs)
|
|
26
|
+
--launch=<exe> Re-launch this exe after successful build
|
|
27
|
+
--debounce=1000 Wait Nms after last change before rebuilding (default: 1000)
|
|
28
|
+
--once Build once and exit (no watch loop)
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
fchek watch .
|
|
32
|
+
fchek watch . --ext=cs,xaml --cmd="dotnet build"
|
|
33
|
+
fchek watch . --cmd="dotnet build" --launch=bin/Debug/net8.0-windows/Vertex.exe
|
|
34
|
+
fchek watch src/ --ext=py --cmd="python -m pytest" --debounce=500
|
|
35
|
+
`.trim();
|
|
36
|
+
|
|
37
|
+
function runCommand(cmd, cwd) {
|
|
38
|
+
const parts = cmd.split(/\s+/);
|
|
39
|
+
const res = spawnSync(parts[0], parts.slice(1), {
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
cwd: cwd || process.cwd(),
|
|
42
|
+
timeout: 120000,
|
|
43
|
+
windowsHide: true,
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
success: res.status === 0,
|
|
47
|
+
exit_code: res.status,
|
|
48
|
+
stdout: (res.stdout || '').slice(-2000),
|
|
49
|
+
stderr: (res.stderr || '').slice(-1000),
|
|
50
|
+
error: res.error?.message || null,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function killProcess(name) {
|
|
55
|
+
if (process.platform !== 'win32') return;
|
|
56
|
+
spawnSync('powershell', [
|
|
57
|
+
'-NoProfile', '-NonInteractive', '-Command',
|
|
58
|
+
`Get-Process -Name "${name}" -ErrorAction SilentlyContinue | Stop-Process -Force`,
|
|
59
|
+
], { timeout: 5000, windowsHide: true });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function launchProcess(exe) {
|
|
63
|
+
const abs = path.resolve(exe);
|
|
64
|
+
if (!fs.existsSync(abs)) return { error: `Executable not found: ${abs}` };
|
|
65
|
+
const child = spawn(abs, [], {
|
|
66
|
+
detached: true,
|
|
67
|
+
stdio: 'ignore',
|
|
68
|
+
windowsHide: false,
|
|
69
|
+
});
|
|
70
|
+
child.unref();
|
|
71
|
+
return { pid: child.pid };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function run(args) {
|
|
75
|
+
if (args.length === 0 || args[0] === '--help') { console.log(HELP); return; }
|
|
76
|
+
|
|
77
|
+
const dir = args.find(a => !a.startsWith('--')) || '.';
|
|
78
|
+
const cmd = (args.find(a => a.startsWith('--cmd=')) || '').replace('--cmd=', '') || autoDetectCmd(dir);
|
|
79
|
+
const extArg = (args.find(a => a.startsWith('--ext=')) || '--ext=cs,xaml,js,ts,py,rs,go,cpp,c').replace('--ext=', '');
|
|
80
|
+
const launchExe = (args.find(a => a.startsWith('--launch=')) || '').replace('--launch=', '') || null;
|
|
81
|
+
const debounce = parseInt((args.find(a => a.startsWith('--debounce=')) || '--debounce=1000').replace('--debounce=', ''), 10);
|
|
82
|
+
const once = args.includes('--once');
|
|
83
|
+
|
|
84
|
+
if (!fs.existsSync(dir)) {
|
|
85
|
+
return output(fail(`Directory not found: ${dir}`));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const exts = extArg.split(',').map(e => `.${e.replace(/^\./, '')}`);
|
|
89
|
+
const absDir = path.resolve(dir);
|
|
90
|
+
|
|
91
|
+
if (!cmd) {
|
|
92
|
+
return output(fail(
|
|
93
|
+
'No build command detected. Use --cmd="dotnet build" or --cmd="npm run build"'
|
|
94
|
+
));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Run once and exit
|
|
98
|
+
if (once) {
|
|
99
|
+
console.error(`[fchek watch] Building: ${cmd}`);
|
|
100
|
+
const result = runCommand(cmd, absDir);
|
|
101
|
+
output(ok({
|
|
102
|
+
mode: 'once',
|
|
103
|
+
dir: absDir,
|
|
104
|
+
cmd,
|
|
105
|
+
...result,
|
|
106
|
+
built_at: new Date().toISOString(),
|
|
107
|
+
}));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Watch mode — output status line per rebuild, then stay running
|
|
112
|
+
console.error(`[fchek watch] Watching: ${absDir}`);
|
|
113
|
+
console.error(`[fchek watch] Extensions: ${exts.join(', ')}`);
|
|
114
|
+
console.error(`[fchek watch] Command: ${cmd}`);
|
|
115
|
+
if (launchExe) console.error(`[fchek watch] Launch: ${launchExe}`);
|
|
116
|
+
console.error(`[fchek watch] Debounce: ${debounce}ms\n`);
|
|
117
|
+
|
|
118
|
+
let debounceTimer = null;
|
|
119
|
+
let building = false;
|
|
120
|
+
let buildCount = 0;
|
|
121
|
+
|
|
122
|
+
const doBuild = () => {
|
|
123
|
+
if (building) return;
|
|
124
|
+
building = true;
|
|
125
|
+
buildCount++;
|
|
126
|
+
|
|
127
|
+
const ts = new Date().toLocaleTimeString();
|
|
128
|
+
console.error(`[${ts}] Change detected — building...`);
|
|
129
|
+
|
|
130
|
+
const result = runCommand(cmd, absDir);
|
|
131
|
+
|
|
132
|
+
if (result.success) {
|
|
133
|
+
console.error(`[${ts}] ✓ Build #${buildCount} succeeded`);
|
|
134
|
+
|
|
135
|
+
if (launchExe) {
|
|
136
|
+
const exeName = path.basename(launchExe, path.extname(launchExe));
|
|
137
|
+
killProcess(exeName);
|
|
138
|
+
const launched = launchProcess(launchExe);
|
|
139
|
+
if (launched.error) {
|
|
140
|
+
console.error(`[${ts}] ✗ Launch failed: ${launched.error}`);
|
|
141
|
+
} else {
|
|
142
|
+
console.error(`[${ts}] ✓ Launched PID ${launched.pid}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Output JSON for agent to read
|
|
147
|
+
console.log(JSON.stringify({
|
|
148
|
+
status: 'ok',
|
|
149
|
+
command: 'watch',
|
|
150
|
+
data: {
|
|
151
|
+
build: buildCount,
|
|
152
|
+
success: true,
|
|
153
|
+
cmd,
|
|
154
|
+
built_at: new Date().toISOString(),
|
|
155
|
+
stdout: result.stdout,
|
|
156
|
+
launched: launchExe ? true : null,
|
|
157
|
+
},
|
|
158
|
+
}));
|
|
159
|
+
} else {
|
|
160
|
+
console.error(`[${ts}] ✗ Build #${buildCount} failed (exit ${result.exit_code})`);
|
|
161
|
+
if (result.stderr) console.error(result.stderr.slice(0, 500));
|
|
162
|
+
|
|
163
|
+
console.log(JSON.stringify({
|
|
164
|
+
status: 'ok',
|
|
165
|
+
command: 'watch',
|
|
166
|
+
data: {
|
|
167
|
+
build: buildCount,
|
|
168
|
+
success: false,
|
|
169
|
+
cmd,
|
|
170
|
+
exit_code: result.exit_code,
|
|
171
|
+
stderr: result.stderr,
|
|
172
|
+
built_at: new Date().toISOString(),
|
|
173
|
+
},
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
building = false;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Initial build
|
|
181
|
+
doBuild();
|
|
182
|
+
|
|
183
|
+
// Watch for changes
|
|
184
|
+
const watchedDirs = new Set([absDir]);
|
|
185
|
+
|
|
186
|
+
// Also watch subdirs (fs.watch is not recursive on all platforms)
|
|
187
|
+
function addWatchers(d, depth = 0) {
|
|
188
|
+
if (depth > 5) return;
|
|
189
|
+
try {
|
|
190
|
+
for (const name of fs.readdirSync(d)) {
|
|
191
|
+
if (['node_modules', '.git', 'bin', 'obj', 'dist', 'target', '__pycache__'].includes(name)) continue;
|
|
192
|
+
const full = path.join(d, name);
|
|
193
|
+
try {
|
|
194
|
+
if (fs.statSync(full).isDirectory() && !watchedDirs.has(full)) {
|
|
195
|
+
watchedDirs.add(full);
|
|
196
|
+
addWatchers(full, depth + 1);
|
|
197
|
+
}
|
|
198
|
+
} catch {}
|
|
199
|
+
}
|
|
200
|
+
} catch {}
|
|
201
|
+
}
|
|
202
|
+
addWatchers(absDir);
|
|
203
|
+
|
|
204
|
+
for (const watchDir of watchedDirs) {
|
|
205
|
+
try {
|
|
206
|
+
fs.watch(watchDir, (eventType, filename) => {
|
|
207
|
+
if (!filename) return;
|
|
208
|
+
const ext = path.extname(filename).toLowerCase();
|
|
209
|
+
if (!exts.includes(ext)) return;
|
|
210
|
+
|
|
211
|
+
clearTimeout(debounceTimer);
|
|
212
|
+
debounceTimer = setTimeout(doBuild, debounce);
|
|
213
|
+
});
|
|
214
|
+
} catch {}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.error('[fchek watch] Press Ctrl+C to stop.\n');
|
|
218
|
+
|
|
219
|
+
// Keep process alive
|
|
220
|
+
process.on('SIGINT', () => {
|
|
221
|
+
console.error('\n[fchek watch] Stopped.');
|
|
222
|
+
process.exit(0);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
await new Promise(() => {}); // Never resolves — keeps running
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function autoDetectCmd(dir) {
|
|
229
|
+
const abs = path.resolve(dir);
|
|
230
|
+
const files = (() => { try { return fs.readdirSync(abs); } catch { return []; } })();
|
|
231
|
+
|
|
232
|
+
if (files.some(f => f.endsWith('.csproj') || f.endsWith('.sln'))) return 'dotnet build';
|
|
233
|
+
if (files.includes('Cargo.toml')) return 'cargo build';
|
|
234
|
+
if (files.includes('package.json')) return 'npm run build';
|
|
235
|
+
if (files.includes('go.mod')) return 'go build ./...';
|
|
236
|
+
if (files.some(f => f.endsWith('.py'))) return 'python -m py_compile *.py';
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = { run };
|
package/lib/winlog.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* winlog.js — read Windows Event Viewer via PowerShell
|
|
5
|
+
*
|
|
6
|
+
* Reads Application, System, or custom event logs.
|
|
7
|
+
* Filters by source name, level (Error/Warning/Info), time range.
|
|
8
|
+
* Agent uses this to see runtime crashes, silent failures, startup errors.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { spawnSync } = require('child_process');
|
|
12
|
+
const { output, ok, fail } = require('./output');
|
|
13
|
+
|
|
14
|
+
const HELP = `
|
|
15
|
+
fchek winlog [--source=Application|System|Security] [--filter=<name>] [--last=50] [--level=Error|Warning|Info] [--since=<minutes>]
|
|
16
|
+
|
|
17
|
+
Read Windows Event Viewer logs. Find crashes and silent failures your app generates.
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--source=Application Log source (default: Application)
|
|
21
|
+
--filter=<name> Filter by source/provider name (e.g. "Vertex", ".NET Runtime")
|
|
22
|
+
--last=50 Number of recent events (default: 50)
|
|
23
|
+
--level=Error Filter by level: Error, Warning, Information, Critical
|
|
24
|
+
--since=60 Events from last N minutes
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
fchek winlog
|
|
28
|
+
fchek winlog --source=Application --filter=Vertex
|
|
29
|
+
fchek winlog --level=Error --since=30
|
|
30
|
+
fchek winlog --source=System --level=Critical
|
|
31
|
+
`.trim();
|
|
32
|
+
|
|
33
|
+
function runPowerShell(script, timeoutMs = 30000) {
|
|
34
|
+
return spawnSync('powershell', [
|
|
35
|
+
'-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script,
|
|
36
|
+
], { encoding: 'utf8', timeout: timeoutMs, windowsHide: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function run(args) {
|
|
40
|
+
if (args[0] === '--help') { console.log(HELP); return; }
|
|
41
|
+
|
|
42
|
+
if (process.platform !== 'win32') {
|
|
43
|
+
return output(fail('fchek winlog is Windows-only.'));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const source = (args.find(a => a.startsWith('--source=')) || '--source=Application').replace('--source=', '');
|
|
47
|
+
const filter = (args.find(a => a.startsWith('--filter=')) || '').replace('--filter=', '') || null;
|
|
48
|
+
const last = parseInt((args.find(a => a.startsWith('--last=')) || '--last=50').replace('--last=', ''), 10);
|
|
49
|
+
const level = (args.find(a => a.startsWith('--level=')) || '').replace('--level=', '') || null;
|
|
50
|
+
const sinceMin = parseInt((args.find(a => a.startsWith('--since=')) || '--since=0').replace('--since=', ''), 10) || null;
|
|
51
|
+
|
|
52
|
+
// Build where clause
|
|
53
|
+
const whereParts = [];
|
|
54
|
+
if (filter) whereParts.push(`$_.ProviderName -match ${JSON.stringify(filter)}`);
|
|
55
|
+
if (level) whereParts.push(`$_.LevelDisplayName -eq ${JSON.stringify(level)}`);
|
|
56
|
+
if (sinceMin) whereParts.push(`$_.TimeCreated -ge (Get-Date).AddMinutes(-${sinceMin})`);
|
|
57
|
+
|
|
58
|
+
const whereClause = whereParts.length > 0
|
|
59
|
+
? `| Where-Object { ${whereParts.join(' -and ')} }`
|
|
60
|
+
: '';
|
|
61
|
+
|
|
62
|
+
const script = `
|
|
63
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
64
|
+
try {
|
|
65
|
+
$events = Get-WinEvent -LogName ${JSON.stringify(source)} -MaxEvents ${last * 3} ${whereClause} -ErrorAction SilentlyContinue |
|
|
66
|
+
Select-Object -First ${last} |
|
|
67
|
+
ForEach-Object {
|
|
68
|
+
@{
|
|
69
|
+
time = $_.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss")
|
|
70
|
+
level = $_.LevelDisplayName
|
|
71
|
+
provider = $_.ProviderName
|
|
72
|
+
id = $_.Id
|
|
73
|
+
message = ($_.Message -replace '"', "'") -replace "[\\r\\n]+", " " | ForEach-Object { $_.Substring(0, [Math]::Min($_.Length, 300)) }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
$json = $events | ConvertTo-Json -Depth 3 -Compress
|
|
78
|
+
if ($null -eq $json) { $json = "[]" }
|
|
79
|
+
Write-Output $json
|
|
80
|
+
} catch {
|
|
81
|
+
Write-Output ('{"error":"' + $_.Exception.Message.Replace('"',"'") + '"}')
|
|
82
|
+
}
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
const res = runPowerShell(script);
|
|
86
|
+
|
|
87
|
+
if (res.error) {
|
|
88
|
+
return output(fail(`PowerShell error: ${res.error.message}`));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const raw = (res.stdout || '').trim();
|
|
92
|
+
if (!raw) {
|
|
93
|
+
return output(ok({ source, filter, events: [], count: 0, note: 'No events found or log empty.' }));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let events;
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(raw);
|
|
99
|
+
// ConvertTo-Json wraps single item as object, multiple as array
|
|
100
|
+
events = Array.isArray(parsed) ? parsed : (parsed.error ? null : [parsed]);
|
|
101
|
+
if (!events) return output(fail(parsed.error));
|
|
102
|
+
} catch {
|
|
103
|
+
return output(fail(`Parse error: ${raw.slice(0, 300)}`));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Categorize
|
|
107
|
+
const errors = events.filter(e => e.level === 'Error' || e.level === 'Critical');
|
|
108
|
+
const warnings = events.filter(e => e.level === 'Warning');
|
|
109
|
+
|
|
110
|
+
output(ok({
|
|
111
|
+
source,
|
|
112
|
+
filter: filter || null,
|
|
113
|
+
level: level || null,
|
|
114
|
+
since_min: sinceMin || null,
|
|
115
|
+
count: events.length,
|
|
116
|
+
errors: errors.length,
|
|
117
|
+
warnings: warnings.length,
|
|
118
|
+
events,
|
|
119
|
+
verdict: errors.length > 0 ? 'errors_found' : warnings.length > 0 ? 'warnings_found' : 'clean',
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { run };
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fchek",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool for developers and AI agents: profiling, sanitizers, LSP, coverage, fuzzing and more",
|
|
5
|
+
"bin": {
|
|
6
|
+
"fchek": "./bin/fchek.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "./bin/fchek.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/fchek.js",
|
|
11
|
+
"build": "npx @yao-pkg/pkg . --target node22-win-x64 --output dist/fchek.exe --compress GZip",
|
|
12
|
+
"build:linux": "npx @yao-pkg/pkg . --target node22-linux-x64 --output dist/fchek --compress GZip",
|
|
13
|
+
"install-global": "npm install -g ."
|
|
14
|
+
},
|
|
15
|
+
"pkg": {
|
|
16
|
+
"targets": ["node22-win-x64"],
|
|
17
|
+
"scripts": [
|
|
18
|
+
"bin/**/*.js",
|
|
19
|
+
"lib/**/*.js"
|
|
20
|
+
],
|
|
21
|
+
"outputPath": "dist"
|
|
22
|
+
},
|
|
23
|
+
"keywords": ["cli", "profiler", "sanitizer", "lsp", "ai-tools"],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"files": ["bin/", "lib/", "skills/", "README.md"],
|
|
26
|
+
"engines": { "node": ">=18" }
|
|
27
|
+
}
|