fchek 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +64 -0
  2. package/bin/fchek.js +107 -0
  3. package/lib/api.js +110 -0
  4. package/lib/audit.js +211 -0
  5. package/lib/bench.js +248 -0
  6. package/lib/config.js +191 -0
  7. package/lib/context.js +356 -0
  8. package/lib/convention.js +526 -0
  9. package/lib/coverage.js +604 -0
  10. package/lib/db.js +135 -0
  11. package/lib/deps-check.js +264 -0
  12. package/lib/deps.js +374 -0
  13. package/lib/docker.js +84 -0
  14. package/lib/doctor.js +149 -0
  15. package/lib/dom.js +226 -0
  16. package/lib/fuzz.js +470 -0
  17. package/lib/git.js +290 -0
  18. package/lib/goto.js +544 -0
  19. package/lib/launch.js +182 -0
  20. package/lib/lint.js +624 -0
  21. package/lib/new_features.test.js +181 -0
  22. package/lib/output.js +46 -0
  23. package/lib/port.js +173 -0
  24. package/lib/process.js +228 -0
  25. package/lib/profile.js +453 -0
  26. package/lib/python.js +41 -0
  27. package/lib/race.js +186 -0
  28. package/lib/registry.js +179 -0
  29. package/lib/repl.js +135 -0
  30. package/lib/run.js +403 -0
  31. package/lib/screenshot.js +152 -0
  32. package/lib/secrets.js +257 -0
  33. package/lib/state.js +219 -0
  34. package/lib/test.js +471 -0
  35. package/lib/vuln.js +253 -0
  36. package/lib/watch.js +240 -0
  37. package/lib/winlog.js +123 -0
  38. package/package.json +27 -0
  39. package/skills/ACTIVATE.md +274 -0
  40. package/skills/README.md +163 -0
  41. package/skills/agent.md +444 -0
  42. package/skills/api.md +47 -0
  43. package/skills/bench.md +117 -0
  44. package/skills/context.md +116 -0
  45. package/skills/convention.md +143 -0
  46. package/skills/coverage.md +99 -0
  47. package/skills/csharp.md +97 -0
  48. package/skills/db.md +66 -0
  49. package/skills/deps-check.md +135 -0
  50. package/skills/deps.md +143 -0
  51. package/skills/docker.md +61 -0
  52. package/skills/dom.md +56 -0
  53. package/skills/fuzz.md +167 -0
  54. package/skills/goto.md +111 -0
  55. package/skills/lint.md +123 -0
  56. package/skills/port.md +57 -0
  57. package/skills/profile.md +91 -0
  58. package/skills/race.md +117 -0
  59. package/skills/repl.md +81 -0
  60. package/skills/rules.md +318 -0
  61. package/skills/run.md +135 -0
  62. package/skills/secrets.md +170 -0
  63. package/skills/security.md +360 -0
  64. package/skills/state.md +261 -0
  65. package/skills/vuln.md +57 -0
  66. package/skills/windows.md +320 -0
@@ -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 };