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
package/lib/run.js ADDED
@@ -0,0 +1,403 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * run.js — execute code and return stdout/stderr/exit in JSON
5
+ *
6
+ * MANDATORY before claiming code "works".
7
+ * Works on Windows, Linux, macOS — detects available compilers/runtimes.
8
+ */
9
+
10
+ const { spawnSync, execSync } = require('child_process');
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+ const os = require('os');
14
+ const { output, ok, fail } = require('./output');
15
+
16
+ const HELP = `
17
+ fchek run <file> [args...] [--timeout=30000] [--stdin=<text>] [--no-color]
18
+
19
+ Execute code and return stdout, stderr, exit code in JSON.
20
+ MANDATORY before claiming code "works". No guessing — only real output.
21
+
22
+ Auto-detects runtime by extension:
23
+ .py → python3 / python
24
+ .js / .mjs → node
25
+ .ts → ts-node (falls back to tsc + node)
26
+ .rs → cargo run (Cargo project) or rustc + run
27
+ .c / .cpp → gcc/g++ (Linux/macOS) or MinGW/cl (Windows)
28
+ .go → go run
29
+ .sh → bash (Linux/macOS) or WSL bash (Windows)
30
+ .rb → ruby
31
+ .php → php
32
+
33
+ Options:
34
+ --timeout=30000 Max run time in ms (default: 30000)
35
+ --stdin=<text> Pass text as stdin
36
+ --no-color Strip ANSI codes from output
37
+
38
+ Examples:
39
+ fchek run main.py
40
+ fchek run main.cpp
41
+ fchek run app.ts --timeout=10000
42
+ fchek run script.sh --stdin="hello"
43
+ `.trim();
44
+
45
+ const DEFAULT_TIMEOUT = 30_000;
46
+
47
+ // ─── Platform helpers ─────────────────────────────────────────────────────────
48
+
49
+ const IS_WIN = process.platform === 'win32';
50
+
51
+ function commandExists(cmd) {
52
+ try {
53
+ execSync(IS_WIN ? `where ${cmd}` : `which ${cmd}`, { stdio: 'ignore', timeout: 3000 });
54
+ return true;
55
+ } catch { return false; }
56
+ }
57
+
58
+ /** Find a C/C++ compiler that actually exists on this machine */
59
+ function findCCompiler(isCpp) {
60
+ if (isCpp) {
61
+ const candidates = IS_WIN
62
+ ? ['g++', 'clang++', 'c++']
63
+ : ['g++', 'clang++', 'c++'];
64
+ for (const c of candidates) {
65
+ if (commandExists(c)) return c;
66
+ }
67
+ return null;
68
+ } else {
69
+ const candidates = IS_WIN
70
+ ? ['gcc', 'clang', 'cc']
71
+ : ['gcc', 'clang', 'cc'];
72
+ for (const c of candidates) {
73
+ if (commandExists(c)) return c;
74
+ }
75
+ return null;
76
+ }
77
+ }
78
+
79
+ function findPython() {
80
+ // Delegate to shared python helper which handles Windows Store alias correctly
81
+ return require('./python').findPython();
82
+ }
83
+
84
+ function stripAnsi(str) {
85
+ // eslint-disable-next-line no-control-regex
86
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
87
+ }
88
+
89
+ // ─── Result builder ───────────────────────────────────────────────────────────
90
+
91
+ function buildResult(file, lang, spawnResult, timeoutMs, extra = {}) {
92
+ const timedOut = spawnResult.error?.code === 'ETIMEDOUT';
93
+ const notFound = spawnResult.error?.code === 'ENOENT';
94
+ const stdout = (spawnResult.stdout || '').slice(0, 50000);
95
+ const stderr = (spawnResult.stderr || '').slice(0, 10000);
96
+ const exitCode = timedOut || notFound ? null : spawnResult.status;
97
+
98
+ let errorMsg = null;
99
+ if (timedOut) {
100
+ errorMsg = `Timed out after ${timeoutMs}ms — possible infinite loop or blocking I/O`;
101
+ } else if (notFound) {
102
+ errorMsg = `Runtime not found: ${extra.runner || '?'}. Run: fchek doctor`;
103
+ } else if (spawnResult.error) {
104
+ errorMsg = spawnResult.error.message;
105
+ }
106
+
107
+ return {
108
+ file: path.resolve(file),
109
+ lang,
110
+ exit_code: exitCode,
111
+ success: !timedOut && !notFound && spawnResult.status === 0,
112
+ timed_out: timedOut,
113
+ stdout,
114
+ stderr,
115
+ stdout_lines: stdout.split('\n').filter(Boolean).length,
116
+ error: errorMsg,
117
+ ...extra,
118
+ };
119
+ }
120
+
121
+ // ─── C / C++ ──────────────────────────────────────────────────────────────────
122
+
123
+ function compileAndRunC(file, extraArgs, timeoutMs, stdinText) {
124
+ const ext = path.extname(file).toLowerCase();
125
+ const isCpp = ext === '.cpp' || ext === '.cc';
126
+ const compiler = findCCompiler(isCpp);
127
+
128
+ if (!compiler) {
129
+ const hint = IS_WIN
130
+ ? 'Install MinGW: https://winlibs.com OR use WSL'
131
+ : `Install: ${isCpp ? 'apt install g++' : 'apt install gcc'}`;
132
+ return output(fail(
133
+ `No C${isCpp ? '++' : ''} compiler found on this system.\n${hint}`
134
+ ));
135
+ }
136
+
137
+ const outBin = path.join(
138
+ os.tmpdir(),
139
+ `fchek_run_${path.basename(file, ext)}_${Date.now()}${IS_WIN ? '.exe' : ''}`
140
+ );
141
+
142
+ const compileArgs = isCpp
143
+ ? ['-std=c++17', '-o', outBin, file]
144
+ : ['-o', outBin, file];
145
+
146
+ const compile = spawnSync(compiler, compileArgs, { encoding: 'utf8', timeout: 30000 });
147
+
148
+ if (compile.status !== 0 || compile.error) {
149
+ return output(ok({
150
+ file: path.resolve(file),
151
+ lang: isCpp ? 'cpp' : 'c',
152
+ compiler,
153
+ compiled: false,
154
+ exit_code: compile.status,
155
+ success: false,
156
+ stdout: '',
157
+ stderr: (compile.stderr || compile.error?.message || 'Compilation failed').slice(0, 5000),
158
+ error: 'Compilation failed — see stderr',
159
+ }));
160
+ }
161
+
162
+ const run = spawnSync(outBin, extraArgs, {
163
+ encoding: 'utf8',
164
+ timeout: timeoutMs,
165
+ input: stdinText || undefined,
166
+ });
167
+
168
+ try { fs.unlinkSync(outBin); } catch {}
169
+
170
+ output(ok({
171
+ ...buildResult(file, isCpp ? 'cpp' : 'c', run, timeoutMs, { compiler, runner: outBin }),
172
+ compiled: true,
173
+ }));
174
+ }
175
+
176
+ // ─── Rust ─────────────────────────────────────────────────────────────────────
177
+
178
+ function runRust(file, extraArgs, timeoutMs, stdinText) {
179
+ if (!commandExists('cargo') && !commandExists('rustc')) {
180
+ return output(fail(
181
+ 'Rust not found. Install: https://rustup.rs\n' +
182
+ 'Then: rustup default stable'
183
+ ));
184
+ }
185
+
186
+ // Walk up to find Cargo.toml
187
+ let dir = path.dirname(path.resolve(file));
188
+ for (let i = 0; i < 6; i++) {
189
+ if (fs.existsSync(path.join(dir, 'Cargo.toml')) && commandExists('cargo')) {
190
+ const res = spawnSync('cargo', ['run', '--', ...extraArgs], {
191
+ encoding: 'utf8',
192
+ cwd: dir,
193
+ timeout: timeoutMs,
194
+ input: stdinText || undefined,
195
+ });
196
+ return output(ok(buildResult(file, 'rust', res, timeoutMs, { tool: 'cargo run', cwd: dir })));
197
+ }
198
+ const parent = path.dirname(dir);
199
+ if (parent === dir) break;
200
+ dir = parent;
201
+ }
202
+
203
+ // Single file with rustc
204
+ if (!commandExists('rustc')) {
205
+ return output(fail('rustc not found. Install: https://rustup.rs'));
206
+ }
207
+
208
+ const outBin = path.join(
209
+ os.tmpdir(),
210
+ `fchek_run_${path.basename(file, '.rs')}_${Date.now()}${IS_WIN ? '.exe' : ''}`
211
+ );
212
+ const compile = spawnSync('rustc', [file, '-o', outBin], { encoding: 'utf8', timeout: 30000 });
213
+ if (compile.status !== 0) {
214
+ return output(ok({
215
+ file: path.resolve(file), lang: 'rust', compiled: false,
216
+ exit_code: compile.status, success: false, stdout: '',
217
+ stderr: (compile.stderr || '').slice(0, 5000), error: 'Compilation failed',
218
+ }));
219
+ }
220
+
221
+ const run = spawnSync(outBin, extraArgs, {
222
+ encoding: 'utf8', timeout: timeoutMs, input: stdinText || undefined,
223
+ });
224
+ try { fs.unlinkSync(outBin); } catch {}
225
+ output(ok({ ...buildResult(file, 'rust', run, timeoutMs, { tool: 'rustc', runner: outBin }), compiled: true }));
226
+ }
227
+
228
+ // ─── TypeScript ───────────────────────────────────────────────────────────────
229
+
230
+ function runTypeScript(file, extraArgs, timeoutMs, stdinText) {
231
+ // Try ts-node first
232
+ if (commandExists('ts-node')) {
233
+ const res = spawnSync('ts-node', [file, ...extraArgs], {
234
+ encoding: 'utf8', timeout: timeoutMs, input: stdinText || undefined,
235
+ });
236
+ return output(ok(buildResult(file, 'typescript', res, timeoutMs, { runner: 'ts-node' })));
237
+ }
238
+
239
+ // Try npx ts-node
240
+ if (commandExists('npx')) {
241
+ const res = spawnSync('npx', ['ts-node', file, ...extraArgs], {
242
+ encoding: 'utf8', timeout: timeoutMs, input: stdinText || undefined,
243
+ });
244
+ if (!res.error) {
245
+ return output(ok(buildResult(file, 'typescript', res, timeoutMs, { runner: 'npx ts-node' })));
246
+ }
247
+ }
248
+
249
+ return output(fail(
250
+ 'ts-node not found. Install: npm install -g ts-node typescript\n' +
251
+ 'Or run: npx ts-node ' + file
252
+ ));
253
+ }
254
+
255
+ // ─── Shell ────────────────────────────────────────────────────────────────────
256
+
257
+ function runShell(file, extraArgs, timeoutMs, stdinText) {
258
+ if (IS_WIN) {
259
+ // Try WSL bash, then Git bash
260
+ for (const bash of ['wsl', 'bash']) {
261
+ if (commandExists(bash)) {
262
+ const wslArgs = bash === 'wsl' ? ['bash', file, ...extraArgs] : [file, ...extraArgs];
263
+ const res = spawnSync(bash, wslArgs, {
264
+ encoding: 'utf8', timeout: timeoutMs, input: stdinText || undefined,
265
+ });
266
+ return output(ok(buildResult(file, 'shell', res, timeoutMs, { runner: bash })));
267
+ }
268
+ }
269
+ return output(fail(
270
+ 'No bash found on Windows.\n' +
271
+ 'Install WSL: wsl --install\n' +
272
+ 'Or install Git for Windows (includes bash): https://git-scm.com'
273
+ ));
274
+ }
275
+
276
+ const bash = commandExists('bash') ? 'bash' : 'sh';
277
+ const res = spawnSync(bash, [file, ...extraArgs], {
278
+ encoding: 'utf8', timeout: timeoutMs, input: stdinText || undefined,
279
+ });
280
+ return output(ok(buildResult(file, 'shell', res, timeoutMs, { runner: bash })));
281
+ }
282
+
283
+ // ─── Generic runner ───────────────────────────────────────────────────────────
284
+
285
+ function runGeneric(file, runner, lang, extraArgs, timeoutMs, stdinText) {
286
+ if (!commandExists(runner)) {
287
+ return output(fail(
288
+ `${runner} not found. Run: fchek doctor to see what's missing.`
289
+ ));
290
+ }
291
+ const res = spawnSync(runner, [file, ...extraArgs], {
292
+ encoding: 'utf8', timeout: timeoutMs, input: stdinText || undefined,
293
+ });
294
+ return output(ok(buildResult(file, lang, res, timeoutMs, { runner })));
295
+ }
296
+
297
+ // ─── Entry point ─────────────────────────────────────────────────────────────
298
+
299
+ async function run(args) {
300
+ if (args.length === 0 || args[0] === '--help') { console.log(HELP); return; }
301
+
302
+ const file = args[0];
303
+ const flagArgs = args.slice(1);
304
+ const extraArgs = flagArgs.filter(a =>
305
+ !a.startsWith('--timeout=') && !a.startsWith('--stdin=') && a !== '--no-color'
306
+ );
307
+ const timeoutMs = parseInt(
308
+ (flagArgs.find(a => a.startsWith('--timeout=')) || `--timeout=${DEFAULT_TIMEOUT}`)
309
+ .replace('--timeout=', ''), 10
310
+ );
311
+ const stdinText = (flagArgs.find(a => a.startsWith('--stdin=')) || '').replace('--stdin=', '') || null;
312
+ const noColor = flagArgs.includes('--no-color');
313
+
314
+ if (!fs.existsSync(file)) {
315
+ return output(fail(`File not found: ${file}`));
316
+ }
317
+
318
+ const ext = path.extname(file).toLowerCase();
319
+
320
+ let result;
321
+
322
+ if (['.c', '.cpp', '.cc'].includes(ext)) {
323
+ return compileAndRunC(file, extraArgs, timeoutMs, stdinText);
324
+ }
325
+ if (ext === '.rs') {
326
+ return runRust(file, extraArgs, timeoutMs, stdinText);
327
+ }
328
+ if (ext === '.ts' || ext === '.tsx') {
329
+ return runTypeScript(file, extraArgs, timeoutMs, stdinText);
330
+ }
331
+ if (ext === '.sh' || ext === '.bash') {
332
+ return runShell(file, extraArgs, timeoutMs, stdinText);
333
+ }
334
+ if (ext === '.py') {
335
+ const py = findPython();
336
+ if (!py) return output(fail('Python not found. Install: https://python.org'));
337
+ const res = spawnSync(py, [file, ...extraArgs], {
338
+ encoding: 'utf8', timeout: timeoutMs, input: stdinText || undefined,
339
+ windowsHide: true,
340
+ });
341
+ result = buildResult(file, 'python', res, timeoutMs, { runner: py });
342
+ } else if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
343
+ if (!commandExists('node')) {
344
+ return output(fail('node not found. Install: https://nodejs.org'));
345
+ }
346
+ const res = spawnSync('node', [file, ...extraArgs], {
347
+ encoding: 'utf8', timeout: timeoutMs, input: stdinText || undefined,
348
+ });
349
+ result = buildResult(file, 'javascript', res, timeoutMs, { runner: 'node' });
350
+ } else if (ext === '.go') {
351
+ if (!commandExists('go')) {
352
+ return output(fail('go not found. Install: https://go.dev'));
353
+ }
354
+ const res = spawnSync('go', ['run', file, ...extraArgs], {
355
+ encoding: 'utf8', timeout: timeoutMs, input: stdinText || undefined,
356
+ });
357
+ result = buildResult(file, 'go', res, timeoutMs, { runner: 'go run' });
358
+ } else if (ext === '.cs') {
359
+ // C# — needs a .csproj. Walk up to find it, then use dotnet run
360
+ let projDir = path.dirname(path.resolve(file));
361
+ let csproj = null;
362
+ for (let i = 0; i < 6; i++) {
363
+ const found = fs.readdirSync(projDir).find(f => f.endsWith('.csproj') || f.endsWith('.sln'));
364
+ if (found) { csproj = projDir; break; }
365
+ const parent = path.dirname(projDir);
366
+ if (parent === projDir) break;
367
+ projDir = parent;
368
+ }
369
+ if (!commandExists('dotnet')) {
370
+ return output(fail(
371
+ 'dotnet not found. Install .NET SDK: https://dotnet.microsoft.com/download'
372
+ ));
373
+ }
374
+ if (!csproj) {
375
+ return output(fail(
376
+ 'No .csproj or .sln found in parent directories.\n' +
377
+ 'Create a project first: dotnet new console -o MyApp'
378
+ ));
379
+ }
380
+ const res = spawnSync('dotnet', ['run', '--project', csproj, '--', ...extraArgs], {
381
+ encoding: 'utf8', timeout: timeoutMs, input: stdinText || undefined,
382
+ });
383
+ result = buildResult(file, 'csharp', res, timeoutMs, { runner: 'dotnet run', cwd: csproj });
384
+ } else if (ext === '.rb') {
385
+ return runGeneric(file, 'ruby', 'ruby', extraArgs, timeoutMs, stdinText);
386
+ } else if (ext === '.php') {
387
+ return runGeneric(file, 'php', 'php', extraArgs, timeoutMs, stdinText);
388
+ } else {
389
+ return output(fail(
390
+ `No runner for extension: ${ext}\n` +
391
+ `Supported: .py .js .ts .rs .c .cpp .go .cs .sh .rb .php`
392
+ ));
393
+ }
394
+
395
+ if (noColor) {
396
+ result.stdout = stripAnsi(result.stdout);
397
+ result.stderr = stripAnsi(result.stderr);
398
+ }
399
+
400
+ output(ok(result));
401
+ }
402
+
403
+ module.exports = { run };
@@ -0,0 +1,152 @@
1
+ 'use strict';
2
+
3
+ const { spawnSync } = require('child_process');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const { output, ok, fail } = require('./output');
8
+
9
+ const HELP = `
10
+ fchek screenshot [--window=<title>] [--screen] [--out=<path>] [--region=x,y,w,h]
11
+
12
+ Capture a screenshot of a window or the full screen. Returns PNG file path in JSON.
13
+
14
+ Examples:
15
+ fchek screenshot --screen
16
+ fchek screenshot --window=Vertex
17
+ fchek screenshot --region=0,0,800,600
18
+ `.trim();
19
+
20
+ function runPs(lines, timeoutMs) {
21
+ const script = lines.join('\r\n');
22
+ const tmp = path.join(os.tmpdir(), 'fchek_shot_' + process.pid + '.ps1');
23
+ fs.writeFileSync(tmp, script, 'utf8');
24
+ const res = spawnSync('powershell', [
25
+ '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', tmp,
26
+ ], { encoding: 'utf8', timeout: timeoutMs || 20000, windowsHide: true });
27
+ try { fs.unlinkSync(tmp); } catch {}
28
+ return res;
29
+ }
30
+
31
+ function parseResult(res) {
32
+ const raw = (res.stdout || '').trim();
33
+ const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
34
+ for (let i = lines.length - 1; i >= 0; i--) {
35
+ if (lines[i].startsWith('{')) {
36
+ try { return JSON.parse(lines[i]); } catch {}
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+
42
+ async function run(args) {
43
+ if (args[0] === '--help') { console.log(HELP); return; }
44
+ if (process.platform !== 'win32') return output(fail('fchek screenshot is Windows-only.'));
45
+
46
+ const windowTitle = (args.find(a => a.startsWith('--window=')) || '').replace('--window=', '') || null;
47
+ const outArg = (args.find(a => a.startsWith('--out=')) || '').replace('--out=', '') || null;
48
+ const regionArg = (args.find(a => a.startsWith('--region=')) || '').replace('--region=', '') || null;
49
+
50
+ const outFile = outArg || path.join(os.tmpdir(), 'fchek_screenshot_' + Date.now() + '.png');
51
+ const safeOut = outFile.replace(/"/g, '`"');
52
+
53
+ let lines;
54
+
55
+ const winApiLines = [
56
+ 'Add-Type -AssemblyName System.Windows.Forms',
57
+ 'Add-Type -AssemblyName System.Drawing',
58
+ 'Add-Type @"',
59
+ 'using System;',
60
+ 'using System.Runtime.InteropServices;',
61
+ 'using System.Drawing;',
62
+ 'public class WC {',
63
+ ' [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);',
64
+ ' [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);',
65
+ ' [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int c);',
66
+ ' public struct RECT { public int Left, Top, Right, Bottom; }',
67
+ '}',
68
+ '"@',
69
+ ];
70
+
71
+ if (windowTitle) {
72
+ const safeTitle = windowTitle.replace(/"/g, '`"');
73
+ lines = [
74
+ ...winApiLines,
75
+ '$outFile = "' + safeOut + '"',
76
+ '$title = "' + safeTitle + '"',
77
+ '$procs = Get-Process | Where-Object { $_.MainWindowTitle -match [regex]::Escape($title) -and $_.MainWindowHandle -ne [IntPtr]::Zero }',
78
+ 'if ($procs.Count -eq 0) {',
79
+ ' Write-Output (@{found=$false; error="Window not found: $title"} | ConvertTo-Json -Compress)',
80
+ ' exit',
81
+ '}',
82
+ '$proc = $procs[0]',
83
+ '[WC]::ShowWindow($proc.MainWindowHandle, 9) | Out-Null',
84
+ '[WC]::SetForegroundWindow($proc.MainWindowHandle) | Out-Null',
85
+ 'Start-Sleep -Milliseconds 400',
86
+ '$r = New-Object WC+RECT',
87
+ '[WC]::GetWindowRect($proc.MainWindowHandle, [ref]$r) | Out-Null',
88
+ '$w = $r.Right - $r.Left',
89
+ '$h = $r.Bottom - $r.Top',
90
+ 'if ($w -le 0 -or $h -le 0) {',
91
+ ' Write-Output (@{found=$true; error="Window has zero size (minimized?)"} | ConvertTo-Json -Compress)',
92
+ ' exit',
93
+ '}',
94
+ '$bmp = New-Object System.Drawing.Bitmap($w, $h)',
95
+ '$g = [System.Drawing.Graphics]::FromImage($bmp)',
96
+ '$g.CopyFromScreen($r.Left, $r.Top, 0, 0, [System.Drawing.Size]::new($w, $h))',
97
+ '$bmp.Save($outFile)',
98
+ '$g.Dispose(); $bmp.Dispose()',
99
+ '$wt = $proc.MainWindowTitle',
100
+ 'Write-Output (@{found=$true; window=$wt; pid=$proc.Id; x=$r.Left; y=$r.Top; width=$w; height=$h; file=$outFile} | ConvertTo-Json -Compress)',
101
+ ];
102
+
103
+ } else if (regionArg) {
104
+ const [rx, ry, rw, rh] = regionArg.split(',').map(Number);
105
+ lines = [
106
+ 'Add-Type -AssemblyName System.Drawing',
107
+ '$outFile = "' + safeOut + '"',
108
+ '$bmp = New-Object System.Drawing.Bitmap(' + rw + ', ' + rh + ')',
109
+ '$g = [System.Drawing.Graphics]::FromImage($bmp)',
110
+ '$g.CopyFromScreen(' + rx + ', ' + ry + ', 0, 0, [System.Drawing.Size]::new(' + rw + ', ' + rh + '))',
111
+ '$bmp.Save($outFile)',
112
+ '$g.Dispose(); $bmp.Dispose()',
113
+ 'Write-Output (@{file=$outFile; width=' + rw + '; height=' + rh + '; type="region"} | ConvertTo-Json -Compress)',
114
+ ];
115
+
116
+ } else {
117
+ lines = [
118
+ 'Add-Type -AssemblyName System.Windows.Forms',
119
+ 'Add-Type -AssemblyName System.Drawing',
120
+ '$outFile = "' + safeOut + '"',
121
+ '$s = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds',
122
+ '$bmp = New-Object System.Drawing.Bitmap($s.Width, $s.Height)',
123
+ '$g = [System.Drawing.Graphics]::FromImage($bmp)',
124
+ '$g.CopyFromScreen($s.Location, [System.Drawing.Point]::Empty, $s.Size)',
125
+ '$bmp.Save($outFile)',
126
+ '$g.Dispose(); $bmp.Dispose()',
127
+ 'Write-Output (@{file=$outFile; width=$s.Width; height=$s.Height; type="fullscreen"} | ConvertTo-Json -Compress)',
128
+ ];
129
+ }
130
+
131
+ const res = runPs(lines);
132
+ const data = parseResult(res);
133
+
134
+ if (!data) {
135
+ return output(fail('Screenshot failed: ' + (res.stderr || res.stdout || 'no output').slice(0, 400)));
136
+ }
137
+ if (data.error) return output(fail(data.error));
138
+ if (!fs.existsSync(outFile)) return output(fail('Screenshot file not created.'));
139
+
140
+ const stat = fs.statSync(outFile);
141
+ output(ok({
142
+ file: outFile,
143
+ size_kb: Math.round(stat.size / 1024),
144
+ width: data.width,
145
+ height: data.height,
146
+ window: data.window || null,
147
+ pid: data.pid || null,
148
+ type: windowTitle ? 'window' : regionArg ? 'region' : 'fullscreen',
149
+ }));
150
+ }
151
+
152
+ module.exports = { run };