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
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
8
|
+
|
|
9
|
+
// Intercept output
|
|
10
|
+
const originalConsoleLog = console.log;
|
|
11
|
+
let capturedOutputs = [];
|
|
12
|
+
console.log = (msg) => {
|
|
13
|
+
try {
|
|
14
|
+
capturedOutputs.push(JSON.parse(msg));
|
|
15
|
+
} catch {
|
|
16
|
+
// Fallback if not JSON
|
|
17
|
+
originalConsoleLog(msg);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function getLatestOutput() {
|
|
22
|
+
return capturedOutputs.pop();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function runTests() {
|
|
26
|
+
console.log("=== Running fchek extensions unit tests ===");
|
|
27
|
+
|
|
28
|
+
// 1. Test Port Command
|
|
29
|
+
{
|
|
30
|
+
console.log("Testing port...");
|
|
31
|
+
const portCmd = require('./port');
|
|
32
|
+
|
|
33
|
+
// Test port: specified but not in use
|
|
34
|
+
await portCmd.run(['9999']);
|
|
35
|
+
const out1 = getLatestOutput();
|
|
36
|
+
assert.strictEqual(out1.status, 'ok');
|
|
37
|
+
assert.strictEqual(out1.data.in_use, false);
|
|
38
|
+
|
|
39
|
+
// Test port: missing arguments
|
|
40
|
+
await portCmd.run([]);
|
|
41
|
+
const out2 = getLatestOutput();
|
|
42
|
+
assert.strictEqual(out2.status, 'error');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. Test API Command
|
|
46
|
+
{
|
|
47
|
+
console.log("Testing api...");
|
|
48
|
+
const apiCmd = require('./api');
|
|
49
|
+
|
|
50
|
+
// Start a dummy HTTP server
|
|
51
|
+
const server = http.createServer((req, res) => {
|
|
52
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
53
|
+
res.end(JSON.stringify({ hello: 'world' }));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
57
|
+
const port = server.address().port;
|
|
58
|
+
|
|
59
|
+
await apiCmd.run([`http://127.0.0.1:${port}`]);
|
|
60
|
+
const out = getLatestOutput();
|
|
61
|
+
assert.strictEqual(out.status, 'ok');
|
|
62
|
+
assert.strictEqual(out.data.status_code, 200);
|
|
63
|
+
assert.ok(out.data.time_ms >= 0);
|
|
64
|
+
assert.strictEqual(JSON.parse(out.data.body).hello, 'world');
|
|
65
|
+
|
|
66
|
+
server.close();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 3. Test DB Command
|
|
70
|
+
{
|
|
71
|
+
console.log("Testing db...");
|
|
72
|
+
const dbCmd = require('./db');
|
|
73
|
+
const dbPath = path.join(__dirname, 'test_temp.db');
|
|
74
|
+
|
|
75
|
+
// Clean up old DB
|
|
76
|
+
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
|
|
77
|
+
|
|
78
|
+
// Create a dummy SQLite DB
|
|
79
|
+
const db = new DatabaseSync(dbPath);
|
|
80
|
+
db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
|
|
81
|
+
db.exec("INSERT INTO users (name) VALUES ('Alice')");
|
|
82
|
+
db.close();
|
|
83
|
+
|
|
84
|
+
// Test db schema
|
|
85
|
+
await dbCmd.run(['schema', dbPath]);
|
|
86
|
+
const schemaOut = getLatestOutput();
|
|
87
|
+
assert.strictEqual(schemaOut.status, 'ok');
|
|
88
|
+
assert.strictEqual(schemaOut.data.tables[0].name, 'users');
|
|
89
|
+
assert.strictEqual(schemaOut.data.tables[0].columns[0].name, 'id');
|
|
90
|
+
|
|
91
|
+
// Test db query
|
|
92
|
+
await dbCmd.run(['query', dbPath, 'SELECT * FROM users']);
|
|
93
|
+
const queryOut = getLatestOutput();
|
|
94
|
+
assert.strictEqual(queryOut.status, 'ok');
|
|
95
|
+
assert.strictEqual(queryOut.data.rows[0].name, 'Alice');
|
|
96
|
+
|
|
97
|
+
// Clean up
|
|
98
|
+
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 4. Test Docker Command
|
|
102
|
+
{
|
|
103
|
+
console.log("Testing docker...");
|
|
104
|
+
const dockerCmd = require('./docker');
|
|
105
|
+
|
|
106
|
+
// We should be able to run this gracefully (Docker might or might not be running)
|
|
107
|
+
await dockerCmd.run(['list']);
|
|
108
|
+
const listOut = getLatestOutput();
|
|
109
|
+
// Either ok or error is acceptable as long as it doesn't crash
|
|
110
|
+
assert.ok(['ok', 'error'].includes(listOut.status));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 5. Test Vuln Command
|
|
114
|
+
{
|
|
115
|
+
console.log("Testing vuln (typosquatting)...");
|
|
116
|
+
const vulnCmd = require('./vuln');
|
|
117
|
+
const tempDir = path.join(__dirname, 'temp_vuln_project');
|
|
118
|
+
if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir);
|
|
119
|
+
|
|
120
|
+
// Write a dummy package.json with a typosquatted package name
|
|
121
|
+
// e.g. "reqeusts" instead of "requests" or "react-doom" instead of "react-dom"
|
|
122
|
+
fs.writeFileSync(path.join(tempDir, 'package.json'), JSON.stringify({
|
|
123
|
+
dependencies: {
|
|
124
|
+
'react-doom': '1.0.0', // typosquatted react-dom
|
|
125
|
+
'lodash': '4.17.21' // correct popular package
|
|
126
|
+
}
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
await vulnCmd.run([tempDir]);
|
|
130
|
+
const vulnOut = getLatestOutput();
|
|
131
|
+
assert.strictEqual(vulnOut.status, 'ok');
|
|
132
|
+
assert.ok(vulnOut.data.packages_checked >= 2);
|
|
133
|
+
|
|
134
|
+
const warning = vulnOut.data.typosquatting_warnings.find(w => w.package === 'react-doom');
|
|
135
|
+
assert.ok(warning);
|
|
136
|
+
assert.strictEqual(warning.suspectedTyposquatOf, 'react-dom');
|
|
137
|
+
|
|
138
|
+
// Clean up
|
|
139
|
+
fs.unlinkSync(path.join(tempDir, 'package.json'));
|
|
140
|
+
fs.rmdirSync(tempDir);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 6. Test DOM Command
|
|
144
|
+
{
|
|
145
|
+
console.log("Testing dom...");
|
|
146
|
+
const domCmd = require('./dom');
|
|
147
|
+
const tempHtml = path.join(__dirname, 'temp.html');
|
|
148
|
+
fs.writeFileSync(tempHtml, `
|
|
149
|
+
<html>
|
|
150
|
+
<body>
|
|
151
|
+
<div id="header">Welcome</div>
|
|
152
|
+
<a class="link" href="https://google.com">Link</a>
|
|
153
|
+
<p>Text</p>
|
|
154
|
+
</body>
|
|
155
|
+
</html>
|
|
156
|
+
`);
|
|
157
|
+
|
|
158
|
+
// Test selector ID
|
|
159
|
+
await domCmd.run([tempHtml, '#header']);
|
|
160
|
+
const outId = getLatestOutput();
|
|
161
|
+
assert.strictEqual(outId.status, 'ok');
|
|
162
|
+
assert.strictEqual(outId.data.matches[0].text, 'Welcome');
|
|
163
|
+
|
|
164
|
+
// Test selector class
|
|
165
|
+
await domCmd.run([tempHtml, '.link', '--attr=href']);
|
|
166
|
+
const outClass = getLatestOutput();
|
|
167
|
+
assert.strictEqual(outClass.status, 'ok');
|
|
168
|
+
assert.strictEqual(outClass.data.matches[0].attribute_value, 'https://google.com');
|
|
169
|
+
|
|
170
|
+
// Clean up
|
|
171
|
+
if (fs.existsSync(tempHtml)) fs.unlinkSync(tempHtml);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log("All unit tests passed successfully!");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
runTests().catch(err => {
|
|
178
|
+
originalConsoleLog("\nTest failed with error:");
|
|
179
|
+
originalConsoleLog(err);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
});
|
package/lib/output.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* output.js — unified JSON output helper for all fchek commands.
|
|
5
|
+
*
|
|
6
|
+
* Every command outputs exactly one JSON object to stdout:
|
|
7
|
+
* {
|
|
8
|
+
* "status": "ok" | "error",
|
|
9
|
+
* "command": "<name>",
|
|
10
|
+
* "data": { ... } // present when status === "ok"
|
|
11
|
+
* "error": "..." // present when status === "error"
|
|
12
|
+
* }
|
|
13
|
+
*
|
|
14
|
+
* AI agents should parse stdout as JSON.
|
|
15
|
+
* Human-readable text mode is available with --text flag (checked globally).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const TEXT_MODE = process.argv.includes('--text');
|
|
19
|
+
|
|
20
|
+
function ok(data, command) {
|
|
21
|
+
return { status: 'ok', command: command || detectCommand(), data };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fail(errorMsg, command) {
|
|
25
|
+
return { status: 'error', command: command || detectCommand(), error: errorMsg };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function output(result) {
|
|
29
|
+
if (TEXT_MODE) {
|
|
30
|
+
// Human-readable fallback
|
|
31
|
+
if (result.status === 'error') {
|
|
32
|
+
console.error(`\n[fchek error] ${result.error}\n`);
|
|
33
|
+
} else {
|
|
34
|
+
console.log('\n' + JSON.stringify(result.data, null, 2) + '\n');
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
console.log(JSON.stringify(result));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function detectCommand() {
|
|
42
|
+
const args = process.argv.slice(2);
|
|
43
|
+
return args[0] || 'unknown';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { ok, fail, output, TEXT_MODE };
|
package/lib/port.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { output, ok, fail } = require('./output');
|
|
6
|
+
|
|
7
|
+
const HELP = `
|
|
8
|
+
fchek port <port> [--all]
|
|
9
|
+
|
|
10
|
+
Inspects network ports and active connections.
|
|
11
|
+
If <port> is specified, returns information about the process using that port.
|
|
12
|
+
If --all is specified, returns a list of all active listening ports.
|
|
13
|
+
`.trim();
|
|
14
|
+
|
|
15
|
+
function getProcessName(pid) {
|
|
16
|
+
if (!pid || pid === '-' || pid === '0') return 'System/Unknown';
|
|
17
|
+
try {
|
|
18
|
+
if (os.platform() === 'win32') {
|
|
19
|
+
const out = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
20
|
+
// "image_name","pid","session_name","session_num","mem_usage"
|
|
21
|
+
const parts = out.trim().split(',');
|
|
22
|
+
if (parts.length > 0) {
|
|
23
|
+
return parts[0].replace(/"/g, '');
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
const out = execSync(`ps -p ${pid} -o comm=`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
27
|
+
return out.trim();
|
|
28
|
+
}
|
|
29
|
+
} catch (err) {
|
|
30
|
+
// ignore
|
|
31
|
+
}
|
|
32
|
+
return 'Unknown';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getPortInfoWindows(targetPort) {
|
|
36
|
+
try {
|
|
37
|
+
const out = execSync('netstat -ano', { encoding: 'utf8' });
|
|
38
|
+
const lines = out.split('\n');
|
|
39
|
+
const results = [];
|
|
40
|
+
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
const trimmed = line.trim();
|
|
43
|
+
if (!trimmed.startsWith('TCP') && !trimmed.startsWith('UDP')) continue;
|
|
44
|
+
|
|
45
|
+
// Proto Local Address Foreign Address State PID
|
|
46
|
+
// TCP 0.0.0.0:80 0.0.0.0:0 LISTENING 4
|
|
47
|
+
const parts = trimmed.split(/\s+/);
|
|
48
|
+
if (parts.length < 4) continue;
|
|
49
|
+
|
|
50
|
+
const proto = parts[0];
|
|
51
|
+
const localAddress = parts[1];
|
|
52
|
+
const state = proto === 'TCP' ? parts[3] : 'UDP';
|
|
53
|
+
const pidStr = proto === 'TCP' ? parts[4] : parts[3];
|
|
54
|
+
const pid = parseInt(pidStr, 10);
|
|
55
|
+
|
|
56
|
+
// Extract port from local address (e.g. 0.0.0.0:80 or [::]:80)
|
|
57
|
+
const lastColon = localAddress.lastIndexOf(':');
|
|
58
|
+
if (lastColon === -1) continue;
|
|
59
|
+
const port = parseInt(localAddress.slice(lastColon + 1), 10);
|
|
60
|
+
|
|
61
|
+
if (targetPort && port !== targetPort) continue;
|
|
62
|
+
|
|
63
|
+
results.push({
|
|
64
|
+
proto,
|
|
65
|
+
localAddress,
|
|
66
|
+
state,
|
|
67
|
+
port,
|
|
68
|
+
pid,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// De-duplicate and add process names
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
const uniqueResults = [];
|
|
75
|
+
for (const r of results) {
|
|
76
|
+
const key = `${r.proto}-${r.port}-${r.pid}`;
|
|
77
|
+
if (seen.has(key)) continue;
|
|
78
|
+
seen.add(key);
|
|
79
|
+
r.processName = getProcessName(r.pid);
|
|
80
|
+
uniqueResults.push(r);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return uniqueResults;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
throw new Error(`Failed to run netstat: ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getPortInfoUnix(targetPort) {
|
|
90
|
+
try {
|
|
91
|
+
// Use lsof -i :port or ss -tulnp / netstat
|
|
92
|
+
let out = '';
|
|
93
|
+
if (targetPort) {
|
|
94
|
+
try {
|
|
95
|
+
out = execSync(`lsof -i :${targetPort} -sTCP:LISTEN -t`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
96
|
+
const pids = out.trim().split('\n').filter(Boolean);
|
|
97
|
+
return pids.map(pidStr => {
|
|
98
|
+
const pid = parseInt(pidStr, 10);
|
|
99
|
+
return {
|
|
100
|
+
proto: 'TCP',
|
|
101
|
+
localAddress: `*:${targetPort}`,
|
|
102
|
+
state: 'LISTEN',
|
|
103
|
+
port: targetPort,
|
|
104
|
+
pid,
|
|
105
|
+
processName: getProcessName(pid),
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
} catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// Return list using ss -tuln
|
|
113
|
+
try {
|
|
114
|
+
out = execSync('ss -tuln', { encoding: 'utf8' });
|
|
115
|
+
} catch {
|
|
116
|
+
out = execSync('netstat -tuln', { encoding: 'utf8' });
|
|
117
|
+
}
|
|
118
|
+
// Parse output...
|
|
119
|
+
const results = [];
|
|
120
|
+
const lines = out.split('\n');
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
const parts = line.trim().split(/\s+/);
|
|
123
|
+
// Simple heuristic to extract ports
|
|
124
|
+
for (const part of parts) {
|
|
125
|
+
const match = part.match(/:(\d+)$/);
|
|
126
|
+
if (match) {
|
|
127
|
+
const port = parseInt(match[1], 10);
|
|
128
|
+
results.push({ proto: 'TCP', port, state: 'LISTEN' });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return results;
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
throw new Error(`Failed to list ports: ${err.message}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function run(args) {
|
|
140
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
141
|
+
console.log(HELP);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const isAll = args.includes('--all');
|
|
146
|
+
const targetPortStr = args.find(a => !a.startsWith('-'));
|
|
147
|
+
const targetPort = targetPortStr ? parseInt(targetPortStr, 10) : null;
|
|
148
|
+
|
|
149
|
+
if (!targetPort && !isAll) {
|
|
150
|
+
output(fail('Please specify a port number (e.g. fchek port 8080) or use --all', 'port'));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const platform = os.platform();
|
|
155
|
+
let data;
|
|
156
|
+
if (platform === 'win32') {
|
|
157
|
+
data = getPortInfoWindows(targetPort);
|
|
158
|
+
} else {
|
|
159
|
+
data = getPortInfoUnix(targetPort);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (targetPort && data.length === 0) {
|
|
163
|
+
output(ok({ port: targetPort, in_use: false, connections: [] }, 'port'));
|
|
164
|
+
} else {
|
|
165
|
+
output(ok({
|
|
166
|
+
port: targetPort || 'all',
|
|
167
|
+
in_use: data.length > 0,
|
|
168
|
+
connections: data,
|
|
169
|
+
}, 'port'));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = { run };
|
package/lib/process.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* process.js — list, inspect, kill Windows processes
|
|
5
|
+
*
|
|
6
|
+
* Agent uses this to:
|
|
7
|
+
* - Check if app is running before launching
|
|
8
|
+
* - Monitor memory/CPU after launch
|
|
9
|
+
* - Kill stuck instances
|
|
10
|
+
* - Detect crashes (process disappeared)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { spawnSync } = require('child_process');
|
|
14
|
+
const { output, ok, fail } = require('./output');
|
|
15
|
+
|
|
16
|
+
const HELP = `
|
|
17
|
+
fchek process <action> [args...]
|
|
18
|
+
|
|
19
|
+
List, monitor, and manage Windows processes.
|
|
20
|
+
|
|
21
|
+
Actions:
|
|
22
|
+
list [--filter=<name>] List running processes (optional filter)
|
|
23
|
+
info <name_or_pid> Detailed info: memory, cpu, threads, start time
|
|
24
|
+
kill <name_or_pid> Kill a process by name or PID
|
|
25
|
+
exists <name> Check if process is running (returns bool)
|
|
26
|
+
wait-exit <name> [--timeout=ms] Wait until process exits
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
fchek process list
|
|
30
|
+
fchek process list --filter=Vertex
|
|
31
|
+
fchek process info Vertex
|
|
32
|
+
fchek process info 1234
|
|
33
|
+
fchek process kill Vertex
|
|
34
|
+
fchek process exists Vertex
|
|
35
|
+
fchek process wait-exit Vertex --timeout=10000
|
|
36
|
+
`.trim();
|
|
37
|
+
|
|
38
|
+
function runPowerShell(script, timeoutMs = 15000) {
|
|
39
|
+
return spawnSync('powershell', [
|
|
40
|
+
'-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script,
|
|
41
|
+
], { encoding: 'utf8', timeout: timeoutMs, windowsHide: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function run(args) {
|
|
45
|
+
if (args.length === 0 || args[0] === '--help') { console.log(HELP); return; }
|
|
46
|
+
|
|
47
|
+
if (process.platform !== 'win32') {
|
|
48
|
+
return output(fail('fchek process is Windows-only.'));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const action = args[0];
|
|
52
|
+
|
|
53
|
+
switch (action) {
|
|
54
|
+
case 'list': {
|
|
55
|
+
const filter = (args.find(a => a.startsWith('--filter=')) || '').replace('--filter=', '') || null;
|
|
56
|
+
|
|
57
|
+
const filterClause = filter
|
|
58
|
+
? `| Where-Object { $_.ProcessName -match ${JSON.stringify(filter)} -or $_.MainWindowTitle -match ${JSON.stringify(filter)} }`
|
|
59
|
+
: '';
|
|
60
|
+
|
|
61
|
+
const script = `
|
|
62
|
+
$procs = Get-Process ${filterClause} -ErrorAction SilentlyContinue |
|
|
63
|
+
Where-Object { $_.Id -ne $PID } |
|
|
64
|
+
Select-Object -First 100 |
|
|
65
|
+
ForEach-Object {
|
|
66
|
+
@{
|
|
67
|
+
pid = $_.Id
|
|
68
|
+
name = $_.ProcessName
|
|
69
|
+
title = $_.MainWindowTitle
|
|
70
|
+
has_window = ($_.MainWindowHandle -ne [IntPtr]::Zero)
|
|
71
|
+
mem_mb = [Math]::Round($_.WorkingSet64 / 1MB, 1)
|
|
72
|
+
cpu_s = [Math]::Round($_.TotalProcessorTime.TotalSeconds, 2)
|
|
73
|
+
threads = $_.Threads.Count
|
|
74
|
+
started = if ($_.StartTime) { $_.StartTime.ToString("HH:mm:ss") } else { "?" }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
$json = $procs | ConvertTo-Json -Depth 3 -Compress
|
|
78
|
+
if ($null -eq $json) { $json = "[]" }
|
|
79
|
+
Write-Output $json
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
const res = runPowerShell(script);
|
|
83
|
+
const raw = (res.stdout || '').trim();
|
|
84
|
+
let procs = [];
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(raw || '[]');
|
|
87
|
+
procs = Array.isArray(parsed) ? parsed : [parsed];
|
|
88
|
+
} catch { return output(fail(`Parse error: ${raw.slice(0, 300)}`)); }
|
|
89
|
+
|
|
90
|
+
output(ok({ filter, count: procs.length, processes: procs }));
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case 'info': {
|
|
95
|
+
const target = args[1];
|
|
96
|
+
if (!target) return output(fail('Usage: fchek process info <name_or_pid>'));
|
|
97
|
+
|
|
98
|
+
const isPid = /^\d+$/.test(target);
|
|
99
|
+
const selector = isPid ? `-Id ${target}` : `-Name "${target}"`;
|
|
100
|
+
|
|
101
|
+
const script = `
|
|
102
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
103
|
+
$proc = Get-Process ${selector} | Select-Object -First 1
|
|
104
|
+
if ($null -eq $proc) {
|
|
105
|
+
Write-Output '{"found":false}'
|
|
106
|
+
} else {
|
|
107
|
+
$modules = @()
|
|
108
|
+
try { $modules = $proc.Modules | ForEach-Object { $_.ModuleName } | Select-Object -First 20 } catch {}
|
|
109
|
+
@{
|
|
110
|
+
found = $true
|
|
111
|
+
pid = $proc.Id
|
|
112
|
+
name = $proc.ProcessName
|
|
113
|
+
title = $proc.MainWindowTitle
|
|
114
|
+
has_window = ($proc.MainWindowHandle -ne [IntPtr]::Zero)
|
|
115
|
+
mem_mb = [Math]::Round($proc.WorkingSet64 / 1MB, 1)
|
|
116
|
+
peak_mem_mb = [Math]::Round($proc.PeakWorkingSet64 / 1MB, 1)
|
|
117
|
+
cpu_s = [Math]::Round($proc.TotalProcessorTime.TotalSeconds, 2)
|
|
118
|
+
threads = $proc.Threads.Count
|
|
119
|
+
handles = $proc.HandleCount
|
|
120
|
+
started = if ($proc.StartTime) { $proc.StartTime.ToString("yyyy-MM-dd HH:mm:ss") } else { "?" }
|
|
121
|
+
exe_path = $proc.MainModule.FileName
|
|
122
|
+
modules_count = $proc.Modules.Count
|
|
123
|
+
} | ConvertTo-Json -Compress
|
|
124
|
+
}
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
const res = runPowerShell(script);
|
|
128
|
+
const raw = (res.stdout || '').trim();
|
|
129
|
+
let data;
|
|
130
|
+
try { data = JSON.parse(raw); } catch { return output(fail(`Parse error: ${raw.slice(0, 300)}`)); }
|
|
131
|
+
if (!data.found) return output(fail(`Process not found: ${target}`));
|
|
132
|
+
output(ok(data));
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case 'kill': {
|
|
137
|
+
const target = args[1];
|
|
138
|
+
if (!target) return output(fail('Usage: fchek process kill <name_or_pid>'));
|
|
139
|
+
|
|
140
|
+
const isPid = /^\d+$/.test(target);
|
|
141
|
+
const selector = isPid ? `-Id ${target}` : `-Name "${target}"`;
|
|
142
|
+
|
|
143
|
+
const script = `
|
|
144
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
145
|
+
$procs = Get-Process ${selector} -ErrorAction SilentlyContinue
|
|
146
|
+
if ($null -eq $procs -or $procs.Count -eq 0) {
|
|
147
|
+
Write-Output ('{"killed":false,"error":"Process not found: ${target}"}')
|
|
148
|
+
} else {
|
|
149
|
+
$killed = @()
|
|
150
|
+
foreach ($p in $procs) {
|
|
151
|
+
try {
|
|
152
|
+
$pid = $p.Id
|
|
153
|
+
$name = $p.ProcessName
|
|
154
|
+
$p.Kill()
|
|
155
|
+
$killed += @{ pid = $pid; name = $name }
|
|
156
|
+
} catch {}
|
|
157
|
+
}
|
|
158
|
+
$json = $killed | ConvertTo-Json -Compress
|
|
159
|
+
if ($null -eq $json) { $json = "[]" }
|
|
160
|
+
Write-Output ('{"killed":true,"processes":' + $json + '}')
|
|
161
|
+
}
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
const res = runPowerShell(script);
|
|
165
|
+
const raw = (res.stdout || '').trim();
|
|
166
|
+
let data;
|
|
167
|
+
try { data = JSON.parse(raw); } catch { return output(fail(`Parse error: ${raw.slice(0, 300)}`)); }
|
|
168
|
+
if (!data.killed) return output(fail(data.error));
|
|
169
|
+
output(ok(data));
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case 'exists': {
|
|
174
|
+
const name = args[1];
|
|
175
|
+
if (!name) return output(fail('Usage: fchek process exists <name>'));
|
|
176
|
+
|
|
177
|
+
const script = `
|
|
178
|
+
$proc = Get-Process -Name "${name}" -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
179
|
+
if ($null -eq $proc) {
|
|
180
|
+
Write-Output '{"exists":false}'
|
|
181
|
+
} else {
|
|
182
|
+
Write-Output ('{"exists":true,"pid":' + $proc.Id + ',"name":"' + $proc.ProcessName + '","mem_mb":' + [Math]::Round($proc.WorkingSet64/1MB,1) + '}')
|
|
183
|
+
}
|
|
184
|
+
`;
|
|
185
|
+
|
|
186
|
+
const res = runPowerShell(script);
|
|
187
|
+
const raw = (res.stdout || '').trim();
|
|
188
|
+
let data;
|
|
189
|
+
try { data = JSON.parse(raw); } catch { return output(fail(`Parse error: ${raw.slice(0, 300)}`)); }
|
|
190
|
+
output(ok(data));
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
case 'wait-exit': {
|
|
195
|
+
const name = args[1];
|
|
196
|
+
const timeout = parseInt((args.find(a => a.startsWith('--timeout=')) || '--timeout=30000').replace('--timeout=', ''), 10);
|
|
197
|
+
if (!name) return output(fail('Usage: fchek process wait-exit <name> [--timeout=30000]'));
|
|
198
|
+
|
|
199
|
+
const script = `
|
|
200
|
+
$step = 500
|
|
201
|
+
$elapsed = 0
|
|
202
|
+
$timeout = ${timeout}
|
|
203
|
+
while ($elapsed -lt $timeout) {
|
|
204
|
+
$proc = Get-Process -Name "${name}" -ErrorAction SilentlyContinue
|
|
205
|
+
if ($null -eq $proc) {
|
|
206
|
+
Write-Output ('{"exited":true,"elapsed_ms":' + $elapsed + '}')
|
|
207
|
+
exit 0
|
|
208
|
+
}
|
|
209
|
+
Start-Sleep -Milliseconds $step
|
|
210
|
+
$elapsed += $step
|
|
211
|
+
}
|
|
212
|
+
Write-Output ('{"exited":false,"elapsed_ms":' + $elapsed + ',"error":"Process still running after ' + $timeout + 'ms"}')
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
const res = runPowerShell(script, timeout + 5000);
|
|
216
|
+
const raw = (res.stdout || '').trim();
|
|
217
|
+
let data;
|
|
218
|
+
try { data = JSON.parse(raw); } catch { return output(fail(`Parse error: ${raw.slice(0, 300)}`)); }
|
|
219
|
+
output(ok({ name, ...data }));
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
default:
|
|
224
|
+
output(fail(`Unknown action: "${action}". Valid: list, info, kill, exists, wait-exit`));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = { run };
|