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/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 };
|