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/context.js
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* context.js — dump all functions/classes/symbols in a project
|
|
5
|
+
*
|
|
6
|
+
* Prevents AI from creating duplicate names or missing existing abstractions.
|
|
7
|
+
* Uses AST parsing per language, falls back to regex grep.
|
|
8
|
+
*
|
|
9
|
+
* Call this BEFORE writing new code in an existing project.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { spawnSync } = require('child_process');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const { output, ok, fail } = require('./output');
|
|
16
|
+
const { findPython } = require('./python');
|
|
17
|
+
|
|
18
|
+
const HELP = `
|
|
19
|
+
fchek context <file_or_dir> [--lang=auto] [--names-only] [--json]
|
|
20
|
+
|
|
21
|
+
Dump all functions, classes, methods and exports in a project.
|
|
22
|
+
Call this BEFORE writing new code to avoid naming conflicts and duplicates.
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--names-only Output just symbol names (compact list)
|
|
26
|
+
--depth=N Max directory depth (default: 5)
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
fchek context src/
|
|
30
|
+
fchek context main.py
|
|
31
|
+
fchek context . --names-only
|
|
32
|
+
fchek context src/auth.rs
|
|
33
|
+
`.trim();
|
|
34
|
+
|
|
35
|
+
const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'target', '__pycache__', '.venv', 'vendor']);
|
|
36
|
+
|
|
37
|
+
// ─── AST extraction per language ─────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/** Python: use ast module via subprocess for accurate extraction.
|
|
40
|
+
* Script is written to a tmpfile to avoid sys.argv[1] confusion with -c mode.
|
|
41
|
+
*/
|
|
42
|
+
function extractPython(filePath) {
|
|
43
|
+
const os = require('os');
|
|
44
|
+
const absPath = path.resolve(filePath);
|
|
45
|
+
|
|
46
|
+
// Escape backslashes for embedding in Python string literal (Windows paths)
|
|
47
|
+
const escapedPath = absPath.replace(/\\/g, '\\\\');
|
|
48
|
+
|
|
49
|
+
const script = `
|
|
50
|
+
import ast, json, sys
|
|
51
|
+
|
|
52
|
+
TARGET = ${JSON.stringify(absPath)}
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
with open(TARGET, encoding='utf-8', errors='replace') as f:
|
|
56
|
+
src = f.read()
|
|
57
|
+
tree = ast.parse(src, filename=TARGET)
|
|
58
|
+
except SyntaxError as e:
|
|
59
|
+
print(json.dumps({"error": str(e)}))
|
|
60
|
+
sys.exit(0)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
print(json.dumps({"error": str(e)}))
|
|
63
|
+
sys.exit(0)
|
|
64
|
+
|
|
65
|
+
symbols = []
|
|
66
|
+
for node in ast.walk(tree):
|
|
67
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
68
|
+
symbols.append({
|
|
69
|
+
"kind": "function" if isinstance(node, ast.FunctionDef) else "async_function",
|
|
70
|
+
"name": node.name,
|
|
71
|
+
"line": node.lineno,
|
|
72
|
+
"args": [a.arg for a in node.args.args]
|
|
73
|
+
})
|
|
74
|
+
elif isinstance(node, ast.ClassDef):
|
|
75
|
+
methods = [
|
|
76
|
+
n.name for n in ast.walk(node)
|
|
77
|
+
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
78
|
+
and n.col_offset > 0
|
|
79
|
+
]
|
|
80
|
+
symbols.append({
|
|
81
|
+
"kind": "class",
|
|
82
|
+
"name": node.name,
|
|
83
|
+
"line": node.lineno,
|
|
84
|
+
"methods": methods
|
|
85
|
+
})
|
|
86
|
+
print(json.dumps(symbols))
|
|
87
|
+
`.trim();
|
|
88
|
+
|
|
89
|
+
// Write script to tmpfile — avoids any sys.argv / -c interaction issues
|
|
90
|
+
const tmpScript = path.join(os.tmpdir(), `fchek_ast_${process.pid}.py`);
|
|
91
|
+
try {
|
|
92
|
+
fs.writeFileSync(tmpScript, script, 'utf8');
|
|
93
|
+
} catch (e) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const py = findPython();
|
|
98
|
+
|
|
99
|
+
if (!py) {
|
|
100
|
+
try { fs.unlinkSync(tmpScript); } catch {}
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const res = spawnSync(py, [tmpScript], { encoding: 'utf8', timeout: 15000, windowsHide: true });
|
|
105
|
+
try { fs.unlinkSync(tmpScript); } catch {}
|
|
106
|
+
|
|
107
|
+
if (res.error || !res.stdout) return [];
|
|
108
|
+
try { return JSON.parse(res.stdout.trim()); } catch { return []; }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** JavaScript/TypeScript: regex-based extraction (reliable, no deps) */
|
|
112
|
+
function extractJs(filePath) {
|
|
113
|
+
const src = readFile(filePath);
|
|
114
|
+
if (!src) return [];
|
|
115
|
+
const symbols = [];
|
|
116
|
+
const lines = src.split('\n');
|
|
117
|
+
|
|
118
|
+
lines.forEach((line, i) => {
|
|
119
|
+
const ln = i + 1;
|
|
120
|
+
// function declarations
|
|
121
|
+
let m = line.match(/^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/);
|
|
122
|
+
if (m) { symbols.push({ kind: 'function', name: m[1], line: ln }); return; }
|
|
123
|
+
// arrow functions assigned to const/let
|
|
124
|
+
m = line.match(/^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(/);
|
|
125
|
+
if (m) { symbols.push({ kind: 'arrow_function', name: m[1], line: ln }); return; }
|
|
126
|
+
// class declarations
|
|
127
|
+
m = line.match(/^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/);
|
|
128
|
+
if (m) { symbols.push({ kind: 'class', name: m[1], line: ln }); return; }
|
|
129
|
+
// class methods
|
|
130
|
+
m = line.match(/^\s{2,}(?:async\s+)?(?:static\s+)?(?:get\s+|set\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*\w+\s*)?\{/);
|
|
131
|
+
if (m && !['if', 'for', 'while', 'switch', 'catch'].includes(m[1])) {
|
|
132
|
+
symbols.push({ kind: 'method', name: m[1], line: ln }); return;
|
|
133
|
+
}
|
|
134
|
+
// module.exports
|
|
135
|
+
m = line.match(/module\.exports\s*=\s*\{([^}]+)\}/);
|
|
136
|
+
if (m) {
|
|
137
|
+
const names = m[1].split(',').map(s => s.trim().split(':')[0].trim()).filter(Boolean);
|
|
138
|
+
names.forEach(name => symbols.push({ kind: 'export', name, line: ln }));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return symbols;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Rust: regex-based extraction */
|
|
146
|
+
function extractRust(filePath) {
|
|
147
|
+
const src = readFile(filePath);
|
|
148
|
+
if (!src) return [];
|
|
149
|
+
const symbols = [];
|
|
150
|
+
const lines = src.split('\n');
|
|
151
|
+
|
|
152
|
+
lines.forEach((line, i) => {
|
|
153
|
+
const ln = i + 1;
|
|
154
|
+
let m = line.match(/^(?:pub(?:\([\w:]+\))?\s+)?(?:async\s+)?fn\s+(\w+)/);
|
|
155
|
+
if (m) { symbols.push({ kind: 'function', name: m[1], line: ln }); return; }
|
|
156
|
+
m = line.match(/^(?:pub(?:\([\w:]+\))?\s+)?struct\s+(\w+)/);
|
|
157
|
+
if (m) { symbols.push({ kind: 'struct', name: m[1], line: ln }); return; }
|
|
158
|
+
m = line.match(/^(?:pub(?:\([\w:]+\))?\s+)?enum\s+(\w+)/);
|
|
159
|
+
if (m) { symbols.push({ kind: 'enum', name: m[1], line: ln }); return; }
|
|
160
|
+
m = line.match(/^(?:pub(?:\([\w:]+\))?\s+)?trait\s+(\w+)/);
|
|
161
|
+
if (m) { symbols.push({ kind: 'trait', name: m[1], line: ln }); return; }
|
|
162
|
+
m = line.match(/^(?:pub(?:\([\w:]+\))?\s+)?impl(?:<[^>]+>)?\s+(?:\w+\s+for\s+)?(\w+)/);
|
|
163
|
+
if (m) { symbols.push({ kind: 'impl', name: m[1], line: ln }); return; }
|
|
164
|
+
m = line.match(/^(?:pub(?:\([\w:]+\))?\s+)?(?:type|const)\s+(\w+)/);
|
|
165
|
+
if (m) { symbols.push({ kind: 'type_or_const', name: m[1], line: ln }); }
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return symbols;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** C/C++: regex-based */
|
|
172
|
+
function extractC(filePath) {
|
|
173
|
+
const src = readFile(filePath);
|
|
174
|
+
if (!src) return [];
|
|
175
|
+
const symbols = [];
|
|
176
|
+
const lines = src.split('\n');
|
|
177
|
+
|
|
178
|
+
lines.forEach((line, i) => {
|
|
179
|
+
const ln = i + 1;
|
|
180
|
+
// function definitions: type name(...)
|
|
181
|
+
let m = line.match(/^(?:static\s+|inline\s+|extern\s+)?(?:[\w:*&<>]+\s+)+(\w+)\s*\([^;]*\)\s*(?:const\s*)?(?:override\s*)?(?:noexcept\s*)?\{?\s*$/);
|
|
182
|
+
if (m && !['if', 'for', 'while', 'switch', 'else', 'return'].includes(m[1])) {
|
|
183
|
+
symbols.push({ kind: 'function', name: m[1], line: ln }); return;
|
|
184
|
+
}
|
|
185
|
+
// class/struct
|
|
186
|
+
m = line.match(/^(?:class|struct)\s+(\w+)/);
|
|
187
|
+
if (m) { symbols.push({ kind: line.startsWith('class') ? 'class' : 'struct', name: m[1], line: ln }); return; }
|
|
188
|
+
// #define macros
|
|
189
|
+
m = line.match(/^#define\s+(\w+)/);
|
|
190
|
+
if (m) { symbols.push({ kind: 'macro', name: m[1], line: ln }); }
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return symbols;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Go: regex-based */
|
|
197
|
+
function extractGo(filePath) {
|
|
198
|
+
const src = readFile(filePath);
|
|
199
|
+
if (!src) return [];
|
|
200
|
+
const symbols = [];
|
|
201
|
+
const lines = src.split('\n');
|
|
202
|
+
|
|
203
|
+
lines.forEach((line, i) => {
|
|
204
|
+
const ln = i + 1;
|
|
205
|
+
let m = line.match(/^func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)\s*\(/);
|
|
206
|
+
if (m) { symbols.push({ kind: 'function', name: m[1], line: ln }); return; }
|
|
207
|
+
m = line.match(/^type\s+(\w+)\s+(?:struct|interface)/);
|
|
208
|
+
if (m) { symbols.push({ kind: line.includes('struct') ? 'struct' : 'interface', name: m[1], line: ln }); }
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return symbols;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** C#: regex-based */
|
|
215
|
+
function extractCSharp(filePath) {
|
|
216
|
+
const src = readFile(filePath);
|
|
217
|
+
if (!src) return [];
|
|
218
|
+
const symbols = [];
|
|
219
|
+
const lines = src.split('\n');
|
|
220
|
+
|
|
221
|
+
lines.forEach((line, i) => {
|
|
222
|
+
const ln = i + 1;
|
|
223
|
+
// class / interface / record / struct / enum
|
|
224
|
+
let m = line.match(/^\s*(?:public|private|protected|internal|static|abstract|sealed|partial|\s)*\s+(class|interface|record|struct|enum)\s+(\w+)/);
|
|
225
|
+
if (m) { symbols.push({ kind: m[1], name: m[2], line: ln }); return; }
|
|
226
|
+
// methods: visibility returnType name(
|
|
227
|
+
m = line.match(/^\s*(?:public|private|protected|internal|static|async|override|virtual|abstract|\s)+(?:[\w<>\[\]?]+\s+)+(\w+)\s*\(/);
|
|
228
|
+
if (m && !['if', 'for', 'while', 'switch', 'catch', 'using', 'return'].includes(m[1])) {
|
|
229
|
+
symbols.push({ kind: 'method', name: m[1], line: ln }); return;
|
|
230
|
+
}
|
|
231
|
+
// properties
|
|
232
|
+
m = line.match(/^\s*(?:public|private|protected|internal|static|\s)+(?:[\w<>\[\]?]+\s+)+(\w+)\s*\{[^}]*get/);
|
|
233
|
+
if (m) { symbols.push({ kind: 'property', name: m[1], line: ln }); }
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
return symbols;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── File walker ─────────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
function readFile(p) {
|
|
242
|
+
try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const EXT_MAP = {
|
|
246
|
+
'.py': 'python', '.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
|
|
247
|
+
'.ts': 'typescript', '.tsx': 'typescript', '.jsx': 'javascript',
|
|
248
|
+
'.rs': 'rust', '.c': 'c', '.cpp': 'c', '.cc': 'c', '.h': 'c', '.hpp': 'c',
|
|
249
|
+
'.go': 'go',
|
|
250
|
+
'.cs': 'csharp', '.vb': 'csharp',
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
function extractFile(filePath) {
|
|
254
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
255
|
+
const lang = EXT_MAP[ext];
|
|
256
|
+
if (!lang) return null;
|
|
257
|
+
|
|
258
|
+
let symbols;
|
|
259
|
+
if (lang === 'python') symbols = extractPython(filePath);
|
|
260
|
+
else if (lang === 'javascript' || lang === 'typescript') symbols = extractJs(filePath);
|
|
261
|
+
else if (lang === 'rust') symbols = extractRust(filePath);
|
|
262
|
+
else if (lang === 'c') symbols = extractC(filePath);
|
|
263
|
+
else if (lang === 'go') symbols = extractGo(filePath);
|
|
264
|
+
else if (lang === 'csharp') symbols = extractCSharp(filePath);
|
|
265
|
+
else return null;
|
|
266
|
+
|
|
267
|
+
if (!Array.isArray(symbols) || symbols.length === 0) return null;
|
|
268
|
+
|
|
269
|
+
return { file: filePath, lang, symbols };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function walkDir(dir, depth, maxDepth) {
|
|
273
|
+
if (depth > maxDepth) return [];
|
|
274
|
+
const results = [];
|
|
275
|
+
let entries;
|
|
276
|
+
try { entries = fs.readdirSync(dir); } catch { return []; }
|
|
277
|
+
|
|
278
|
+
for (const name of entries) {
|
|
279
|
+
if (IGNORED_DIRS.has(name) || name.startsWith('.')) continue;
|
|
280
|
+
const full = path.join(dir, name);
|
|
281
|
+
const stat = fs.statSync(full);
|
|
282
|
+
if (stat.isDirectory()) {
|
|
283
|
+
results.push(...walkDir(full, depth + 1, maxDepth));
|
|
284
|
+
} else if (stat.isFile()) {
|
|
285
|
+
const r = extractFile(full);
|
|
286
|
+
if (r) results.push(r);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return results;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ─── Entry point ─────────────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
async function run(args) {
|
|
295
|
+
if (args.length === 0 || args[0] === '--help') { console.log(HELP); return; }
|
|
296
|
+
|
|
297
|
+
const target = args[0];
|
|
298
|
+
const namesOnly = args.includes('--names-only');
|
|
299
|
+
const depthArg = parseInt((args.find(a => a.startsWith('--depth=')) || '--depth=5').replace('--depth=', ''), 10);
|
|
300
|
+
|
|
301
|
+
if (!fs.existsSync(target)) {
|
|
302
|
+
return output(fail(`Not found: ${target}`));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const stat = fs.statSync(target);
|
|
306
|
+
const files = [];
|
|
307
|
+
|
|
308
|
+
if (stat.isDirectory()) {
|
|
309
|
+
files.push(...walkDir(path.resolve(target), 0, depthArg));
|
|
310
|
+
} else {
|
|
311
|
+
const r = extractFile(path.resolve(target));
|
|
312
|
+
if (r) files.push(r);
|
|
313
|
+
else return output(fail(`Unsupported file type: ${path.extname(target)}`));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (files.length === 0) {
|
|
317
|
+
return output(fail(`No supported source files found in: ${target}`));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const totalSymbols = files.reduce((n, f) => n + f.symbols.length, 0);
|
|
321
|
+
|
|
322
|
+
// Build flat name index for quick duplicate detection
|
|
323
|
+
const nameIndex = {};
|
|
324
|
+
for (const file of files) {
|
|
325
|
+
for (const sym of file.symbols) {
|
|
326
|
+
if (!nameIndex[sym.name]) nameIndex[sym.name] = [];
|
|
327
|
+
nameIndex[sym.name].push({ file: file.file, kind: sym.kind, line: sym.line });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Detect duplicate names across files
|
|
332
|
+
const duplicates = Object.entries(nameIndex)
|
|
333
|
+
.filter(([, locs]) => locs.length > 1)
|
|
334
|
+
.map(([name, locs]) => ({ name, locations: locs }));
|
|
335
|
+
|
|
336
|
+
if (namesOnly) {
|
|
337
|
+
output(ok({
|
|
338
|
+
target,
|
|
339
|
+
total_files: files.length,
|
|
340
|
+
total_symbols: totalSymbols,
|
|
341
|
+
names: Object.keys(nameIndex).sort(),
|
|
342
|
+
duplicates: duplicates.slice(0, 20),
|
|
343
|
+
}));
|
|
344
|
+
} else {
|
|
345
|
+
output(ok({
|
|
346
|
+
target,
|
|
347
|
+
total_files: files.length,
|
|
348
|
+
total_symbols: totalSymbols,
|
|
349
|
+
files,
|
|
350
|
+
name_index: nameIndex,
|
|
351
|
+
duplicates: duplicates.slice(0, 20),
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
module.exports = { run };
|