@paa1997/metho 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/src/logger.js ADDED
@@ -0,0 +1,95 @@
1
+ import chalk from 'chalk';
2
+ import { appendFileSync, mkdirSync } from 'fs';
3
+ import { dirname } from 'path';
4
+
5
+ export function createLogger(debugMode = false) {
6
+ let logFilePath = null;
7
+ let eventCallback = null;
8
+
9
+ function timestamp() {
10
+ return new Date().toISOString().replace('T', ' ').replace('Z', '');
11
+ }
12
+
13
+ function writeToFile(level, msg) {
14
+ if (!logFilePath) return;
15
+ try {
16
+ appendFileSync(logFilePath, `[${timestamp()}] [${level}] ${msg}\n`);
17
+ } catch { /* ignore file write errors */ }
18
+ }
19
+
20
+ function emit(event) {
21
+ if (eventCallback) {
22
+ event.timestamp = timestamp();
23
+ eventCallback(event);
24
+ }
25
+ }
26
+
27
+ return {
28
+ setLogFile(filePath) {
29
+ logFilePath = filePath;
30
+ mkdirSync(dirname(filePath), { recursive: true });
31
+ },
32
+
33
+ onEvent(callback) {
34
+ eventCallback = callback;
35
+ },
36
+
37
+ info(msg) {
38
+ console.log(chalk.blue('ℹ'), msg);
39
+ writeToFile('INFO', msg);
40
+ emit({ type: 'log', level: 'info', message: msg });
41
+ },
42
+
43
+ debug(msg) {
44
+ writeToFile('DEBUG', msg);
45
+ if (debugMode) {
46
+ console.log(chalk.gray(` [debug] ${msg}`));
47
+ }
48
+ emit({ type: 'log', level: 'debug', message: msg });
49
+ },
50
+
51
+ success(msg) {
52
+ console.log(chalk.green('✔'), msg);
53
+ writeToFile('SUCCESS', msg);
54
+ emit({ type: 'log', level: 'success', message: msg });
55
+ },
56
+
57
+ error(msg) {
58
+ console.error(chalk.red('✖'), msg);
59
+ writeToFile('ERROR', msg);
60
+ emit({ type: 'log', level: 'error', message: msg });
61
+ },
62
+
63
+ warn(msg) {
64
+ console.log(chalk.yellow('⚠'), msg);
65
+ writeToFile('WARN', msg);
66
+ emit({ type: 'log', level: 'warn', message: msg });
67
+ },
68
+
69
+ step(stepNum, name, status) {
70
+ const prefix = chalk.cyan(`[Step ${stepNum}]`);
71
+ if (status === 'start') {
72
+ console.log(`\n${prefix} ${chalk.bold(name)} ${chalk.gray('starting...')}`);
73
+ } else if (status === 'done') {
74
+ console.log(`${prefix} ${chalk.bold(name)} ${chalk.green('done')}`);
75
+ } else if (status === 'skip') {
76
+ console.log(`${prefix} ${chalk.bold(name)} ${chalk.yellow('skipped')}`);
77
+ }
78
+ writeToFile('STEP', `[Step ${stepNum}] ${name} - ${status}`);
79
+ emit({ type: 'step', stepNum, name, status });
80
+ },
81
+
82
+ result(stepName, file, lines) {
83
+ writeToFile('RESULT', `${stepName}: ${lines} lines → ${file}`);
84
+ emit({ type: 'result', step: stepName, file, lines });
85
+ },
86
+
87
+ banner(msg) {
88
+ console.log(chalk.bold.magenta(`\n${'═'.repeat(60)}`));
89
+ console.log(chalk.bold.magenta(` ${msg}`));
90
+ console.log(chalk.bold.magenta(`${'═'.repeat(60)}\n`));
91
+ writeToFile('BANNER', msg);
92
+ emit({ type: 'banner', message: msg });
93
+ },
94
+ };
95
+ }
package/src/server.js ADDED
@@ -0,0 +1,261 @@
1
+ import { createServer } from 'http';
2
+ import { readFileSync } from 'fs';
3
+ import { resolve, dirname, join } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { exec } from 'child_process';
6
+ import { createLogger } from './logger.js';
7
+ import { runPipeline } from './engine.js';
8
+ import { createRunContext } from './signals.js';
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+
12
+ export function startServer(port = 3000) {
13
+ let sseClients = [];
14
+ let runCounter = 0;
15
+ // Track active runs: runId → { ctx, logger, label, config }
16
+ const activeRuns = new Map();
17
+
18
+ // Read HTML at startup
19
+ let guiHtml;
20
+ try {
21
+ guiHtml = readFileSync(join(__dirname, 'gui.html'), 'utf-8');
22
+ } catch (err) {
23
+ console.error('Failed to read gui.html:', err.message);
24
+ process.exit(1);
25
+ }
26
+
27
+ function broadcast(event) {
28
+ const data = `data: ${JSON.stringify(event)}\n\n`;
29
+ const dead = [];
30
+ for (let i = 0; i < sseClients.length; i++) {
31
+ try {
32
+ sseClients[i].write(data);
33
+ } catch {
34
+ dead.push(i);
35
+ }
36
+ }
37
+ for (let i = dead.length - 1; i >= 0; i--) {
38
+ sseClients.splice(dead[i], 1);
39
+ }
40
+ }
41
+
42
+ function readBody(req) {
43
+ return new Promise((resolve, reject) => {
44
+ let body = '';
45
+ req.on('data', (chunk) => { body += chunk; });
46
+ req.on('end', () => resolve(body));
47
+ req.on('error', reject);
48
+ });
49
+ }
50
+
51
+ const server = createServer(async (req, res) => {
52
+ const url = req.url.split('?')[0];
53
+ const method = req.method;
54
+
55
+ console.log(`[server] ${method} ${url}`);
56
+
57
+ try {
58
+ res.setHeader('Access-Control-Allow-Origin', '*');
59
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
60
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
61
+
62
+ if (method === 'OPTIONS') {
63
+ res.writeHead(204);
64
+ return res.end();
65
+ }
66
+
67
+ // GET / → serve GUI
68
+ if (method === 'GET' && url === '/') {
69
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
70
+ return res.end(guiHtml);
71
+ }
72
+
73
+ // GET /status → active runs info
74
+ if (method === 'GET' && url === '/status') {
75
+ const runs = [];
76
+ for (const [id, run] of activeRuns) {
77
+ runs.push({ runId: id, label: run.label });
78
+ }
79
+ res.writeHead(200, { 'Content-Type': 'application/json' });
80
+ return res.end(JSON.stringify({ running: activeRuns.size, runs }));
81
+ }
82
+
83
+ // GET /events → SSE stream
84
+ if (method === 'GET' && url === '/events') {
85
+ res.writeHead(200, {
86
+ 'Content-Type': 'text/event-stream',
87
+ 'Cache-Control': 'no-cache',
88
+ 'Connection': 'keep-alive',
89
+ 'X-Accel-Buffering': 'no',
90
+ });
91
+ res.write('data: {"type":"connected"}\n\n');
92
+ sseClients.push(res);
93
+ req.on('close', () => {
94
+ sseClients = sseClients.filter(c => c !== res);
95
+ });
96
+ return;
97
+ }
98
+
99
+ // POST /run → start a new pipeline run
100
+ if (method === 'POST' && url === '/run') {
101
+ const body = await readBody(req);
102
+ let params;
103
+ try {
104
+ params = JSON.parse(body);
105
+ } catch {
106
+ res.writeHead(400, { 'Content-Type': 'application/json' });
107
+ return res.end(JSON.stringify({ error: 'Invalid JSON body' }));
108
+ }
109
+
110
+ if (!params.domain && !params.domainList) {
111
+ res.writeHead(400, { 'Content-Type': 'application/json' });
112
+ return res.end(JSON.stringify({ error: 'Must provide domain or domainList' }));
113
+ }
114
+
115
+ runCounter++;
116
+ const runId = 'run-' + runCounter;
117
+ const label = params.domain || params.domainList;
118
+
119
+ const config = {
120
+ domain: params.domain || null,
121
+ domainList: params.domainList ? resolve(params.domainList) : null,
122
+ outputDir: resolve(params.outputDir || './metho-results'),
123
+ skipSubfinder: !!params.skipSubfinder,
124
+ skipSubdomainProbe: !!params.skipSubdomainProbe,
125
+ skipGau: !!params.skipGau,
126
+ skipFilter: !!params.skipFilter,
127
+ skipKatana: !!params.skipKatana,
128
+ skipFindsomething: !!params.skipFindsomething,
129
+ katanaChunkSize: parseInt(params.katanaChunkSize, 10) || 1000,
130
+ katanaDepth: parseInt(params.katanaDepth, 10) || 2,
131
+ findsomethingPath: params.findsomethingPath || '',
132
+ debug: true,
133
+ };
134
+
135
+ res.writeHead(200, { 'Content-Type': 'application/json' });
136
+ res.end(JSON.stringify({ ok: true, runId, label }));
137
+ console.log(`[server] Starting ${runId} (${label})`);
138
+
139
+ setImmediate(() => startPipelineRun(runId, label, config));
140
+ return;
141
+ }
142
+
143
+ // POST /stop → stop a specific run or all runs
144
+ if (method === 'POST' && url === '/stop') {
145
+ const body = await readBody(req);
146
+ let params = {};
147
+ try { params = JSON.parse(body); } catch { /* empty body = stop all */ }
148
+
149
+ if (params.runId) {
150
+ // Stop a specific run
151
+ const run = activeRuns.get(params.runId);
152
+ if (run) {
153
+ run.ctx.triggerInterrupt(run.logger);
154
+ console.log(`[server] Stopping ${params.runId}`);
155
+ }
156
+ } else {
157
+ // Stop all runs
158
+ for (const [id, run] of activeRuns) {
159
+ run.ctx.triggerInterrupt(run.logger);
160
+ console.log(`[server] Stopping ${id}`);
161
+ }
162
+ }
163
+
164
+ res.writeHead(200, { 'Content-Type': 'application/json' });
165
+ return res.end(JSON.stringify({ ok: true }));
166
+ }
167
+
168
+ // GET /file?path=... → read result file
169
+ if (method === 'GET' && url === '/file') {
170
+ const fullUrl = new URL(req.url, `http://localhost:${port}`);
171
+ const filePath = fullUrl.searchParams.get('path');
172
+ if (!filePath) {
173
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
174
+ return res.end('Missing path parameter');
175
+ }
176
+ try {
177
+ const content = readFileSync(filePath, 'utf-8');
178
+ res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
179
+ return res.end(content);
180
+ } catch (err) {
181
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
182
+ return res.end('File not found: ' + err.message);
183
+ }
184
+ }
185
+
186
+ // 404
187
+ res.writeHead(404, { 'Content-Type': 'application/json' });
188
+ res.end(JSON.stringify({ error: 'Not found' }));
189
+ } catch (err) {
190
+ console.error('[server] Request handler error:', err);
191
+ if (!res.headersSent) {
192
+ res.writeHead(500, { 'Content-Type': 'application/json' });
193
+ }
194
+ res.end(JSON.stringify({ error: 'Internal server error' }));
195
+ }
196
+ });
197
+
198
+ async function startPipelineRun(runId, label, config) {
199
+ const ctx = createRunContext();
200
+ const logger = createLogger(true);
201
+
202
+ activeRuns.set(runId, { ctx, logger, label, config });
203
+
204
+ // Prefix all SSE events with runId
205
+ logger.onEvent((event) => {
206
+ broadcast({ ...event, runId });
207
+ });
208
+
209
+ // Notify GUI of new run
210
+ broadcast({ type: 'run-started', runId, label });
211
+
212
+ try {
213
+ const result = await runPipeline(config, logger, ctx);
214
+ const results = result?.results || [];
215
+ const runDir = result?.runDir || '';
216
+ broadcast({ type: 'complete', runId, results, runDir });
217
+ } catch (err) {
218
+ console.error(`[server] Pipeline ${runId} error:`, err);
219
+ broadcast({ type: 'error', runId, message: err.message });
220
+ } finally {
221
+ activeRuns.delete(runId);
222
+ broadcast({ type: 'run-ended', runId, activeRuns: activeRuns.size });
223
+
224
+ // Close SSE clients only when ALL runs are done
225
+ if (activeRuns.size === 0) {
226
+ setTimeout(() => {
227
+ if (activeRuns.size === 0) {
228
+ for (const client of sseClients) {
229
+ try { client.end(); } catch { /* ignore */ }
230
+ }
231
+ sseClients = [];
232
+ }
233
+ }, 3000);
234
+ }
235
+ }
236
+ }
237
+
238
+ server.on('error', (err) => {
239
+ if (err.code === 'EADDRINUSE') {
240
+ console.error(`Port ${port} is already in use. Try: metho --gui --port ${port + 1}`);
241
+ } else {
242
+ console.error('Server error:', err);
243
+ }
244
+ process.exit(1);
245
+ });
246
+
247
+ server.listen(port, () => {
248
+ const url = `http://localhost:${port}`;
249
+ console.log(`\n Metho GUI running at ${url}\n`);
250
+ openBrowser(url);
251
+ });
252
+
253
+ return server;
254
+ }
255
+
256
+ function openBrowser(url) {
257
+ const cmd = process.platform === 'win32' ? `start "" "${url}"`
258
+ : process.platform === 'darwin' ? `open "${url}"`
259
+ : `xdg-open "${url}"`;
260
+ exec(cmd, () => {});
261
+ }
package/src/signals.js ADDED
@@ -0,0 +1,101 @@
1
+ import { exec } from 'child_process';
2
+
3
+ const isWin = process.platform === 'win32';
4
+
5
+ function forceKillChildren(children, logger) {
6
+ for (const child of children) {
7
+ const pid = child.pid;
8
+ if (!pid) continue;
9
+ if (logger) logger.debug(`Force-killing PID ${pid}`);
10
+ try {
11
+ if (isWin) {
12
+ exec(`taskkill /F /T /PID ${pid}`, () => {});
13
+ } else {
14
+ try { process.kill(-pid, 'SIGKILL'); } catch {}
15
+ try { child.kill('SIGKILL'); } catch {}
16
+ }
17
+ } catch {
18
+ try { child.kill('SIGKILL'); } catch {}
19
+ }
20
+ }
21
+ children.clear();
22
+ }
23
+
24
+ /**
25
+ * Per-run context — isolates children, interrupt flag, and tracked files for one pipeline run.
26
+ */
27
+ export class RunContext {
28
+ constructor() {
29
+ this.children = new Set();
30
+ this.interrupted = false;
31
+ this.files = [];
32
+ }
33
+
34
+ registerChild(child) {
35
+ this.children.add(child);
36
+ child.on('close', () => this.children.delete(child));
37
+ }
38
+
39
+ clearChild(child) {
40
+ if (child) this.children.delete(child);
41
+ }
42
+
43
+ trackFile(filePath, description) {
44
+ this.files.push({ path: filePath, description });
45
+ }
46
+
47
+ isInterrupted() {
48
+ return this.interrupted;
49
+ }
50
+
51
+ triggerInterrupt(logger) {
52
+ if (this.interrupted) return;
53
+ this.interrupted = true;
54
+ if (logger) logger.warn('Stop requested. Killing active processes...');
55
+ forceKillChildren(this.children, logger);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Create a new isolated run context.
61
+ */
62
+ export function createRunContext() {
63
+ return new RunContext();
64
+ }
65
+
66
+ // ---- Global context (used by CLI mode + SIGINT/SIGTERM handler) ----
67
+
68
+ let globalCtx = new RunContext();
69
+
70
+ /** Set up process signal handlers that interrupt the global context. */
71
+ export function setupSignalHandlers(logger) {
72
+ const handler = (signal) => {
73
+ if (globalCtx.interrupted) {
74
+ logger.warn('Force exit.');
75
+ process.exit(1);
76
+ }
77
+ globalCtx.interrupted = true;
78
+ logger.warn(`\n${signal} received. Stopping...`);
79
+ forceKillChildren(globalCtx.children, logger);
80
+
81
+ if (globalCtx.files.length > 0) {
82
+ logger.info('Partial results saved:');
83
+ for (const f of globalCtx.files) {
84
+ logger.info(` ${f.description}: ${f.path}`);
85
+ }
86
+ }
87
+ };
88
+
89
+ process.on('SIGINT', () => handler('SIGINT'));
90
+ process.on('SIGTERM', () => handler('SIGTERM'));
91
+ }
92
+
93
+ /** Get the global context (for CLI single-run mode). */
94
+ export function getGlobalContext() {
95
+ return globalCtx;
96
+ }
97
+
98
+ /** Reset the global context (for CLI mode restarts). */
99
+ export function resetGlobalContext() {
100
+ globalCtx = new RunContext();
101
+ }
@@ -0,0 +1,111 @@
1
+ import { spawn } from 'child_process';
2
+ import { createReadStream } from 'fs';
3
+ import { stat } from 'fs/promises';
4
+
5
+ const isWin = process.platform === 'win32';
6
+
7
+ export class BaseStep {
8
+ constructor(name, logger, ctx) {
9
+ this.name = name;
10
+ this.logger = logger;
11
+ this.ctx = ctx;
12
+ }
13
+
14
+ /**
15
+ * Spawn a child process with streaming output.
16
+ * Returns a promise that resolves with { code, lineCount }.
17
+ */
18
+ runCommand(cmd, args, options = {}) {
19
+ return new Promise((resolve, reject) => {
20
+ if (this.ctx.isInterrupted()) {
21
+ return reject(new Error('Pipeline interrupted'));
22
+ }
23
+
24
+ this.logger.debug(`${this.name}: ${cmd} ${args.join(' ')}`);
25
+
26
+ const spawnOpts = {
27
+ stdio: ['pipe', 'pipe', 'pipe'],
28
+ shell: isWin,
29
+ };
30
+ if (options.cwd) spawnOpts.cwd = options.cwd;
31
+
32
+ const child = spawn(cmd, args, spawnOpts);
33
+ this.ctx.registerChild(child);
34
+
35
+ let stderrBuf = '';
36
+
37
+ child.stdout.on('data', (data) => {
38
+ const lines = data.toString().trim();
39
+ if (lines) this.logger.debug(`${this.name} stdout: ${lines}`);
40
+ });
41
+
42
+ child.stderr.on('data', (data) => {
43
+ const text = data.toString().trim();
44
+ if (text) {
45
+ stderrBuf += text + '\n';
46
+ this.logger.debug(`${this.name} stderr: ${text}`);
47
+ }
48
+ });
49
+
50
+ // Pipe stdin from file if requested
51
+ if (options.stdinFile) {
52
+ const stdinStream = createReadStream(options.stdinFile);
53
+ stdinStream.pipe(child.stdin);
54
+ stdinStream.on('error', (err) => {
55
+ this.logger.debug(`${this.name} stdin stream error: ${err.message}`);
56
+ });
57
+ } else {
58
+ child.stdin.end();
59
+ }
60
+
61
+ child.on('error', (err) => {
62
+ this.ctx.clearChild(child);
63
+ reject(new Error(`${this.name}: Failed to spawn ${cmd}: ${err.message}`));
64
+ });
65
+
66
+ child.on('close', async (code) => {
67
+ this.ctx.clearChild(child);
68
+
69
+ if (this.ctx.isInterrupted()) {
70
+ return reject(new Error('Pipeline interrupted'));
71
+ }
72
+
73
+ let lineCount = 0;
74
+ if (options.outputFile) {
75
+ lineCount = await this.countLines(options.outputFile);
76
+ }
77
+
78
+ if (code !== 0) {
79
+ if (lineCount > 0) {
80
+ this.logger.warn(`${this.name} exited with code ${code} but produced ${lineCount} results`);
81
+ return resolve({ code, lineCount });
82
+ }
83
+ return reject(new Error(`${this.name}: ${cmd} exited with code ${code}\n${stderrBuf}`));
84
+ }
85
+
86
+ resolve({ code, lineCount });
87
+ });
88
+ });
89
+ }
90
+
91
+ async countLines(filePath) {
92
+ try {
93
+ const stats = await stat(filePath);
94
+ if (stats.size === 0) return 0;
95
+
96
+ return new Promise((resolve) => {
97
+ let count = 0;
98
+ const stream = createReadStream(filePath, { encoding: 'utf-8' });
99
+ stream.on('data', (chunk) => {
100
+ for (let i = 0; i < chunk.length; i++) {
101
+ if (chunk[i] === '\n') count++;
102
+ }
103
+ });
104
+ stream.on('end', () => resolve(count));
105
+ stream.on('error', () => resolve(0));
106
+ });
107
+ } catch {
108
+ return 0;
109
+ }
110
+ }
111
+ }
@@ -0,0 +1,43 @@
1
+ import { BaseStep } from './base-step.js';
2
+ import { getToolPath } from '../tools/manager.js';
3
+ import { filterExtensions } from '../utils/filter-extensions.js';
4
+ import { join, dirname } from 'path';
5
+ import { HTTPX_STATUS_CODES } from '../constants.js';
6
+
7
+ export class FilterStep extends BaseStep {
8
+ constructor(logger, ctx) {
9
+ super('filter', logger, ctx);
10
+ }
11
+
12
+ async execute(config, inputFile, outputFile) {
13
+ const filteredTmpFile = join(dirname(outputFile), 'tmp-extension-filtered.txt');
14
+ this.logger.debug('Filter Phase A: Removing blocked extensions...');
15
+
16
+ const { kept, removed } = await filterExtensions(inputFile, filteredTmpFile);
17
+ this.logger.info(`Extension filter: kept ${kept}, removed ${removed} URLs`);
18
+
19
+ if (kept === 0) {
20
+ this.logger.warn('No URLs remaining after extension filtering');
21
+ const { writeFileSync } = await import('fs');
22
+ writeFileSync(outputFile, '');
23
+ return { code: 0, lineCount: 0 };
24
+ }
25
+
26
+ this.logger.debug('Filter Phase B: httpx liveness check...');
27
+ const args = [
28
+ '-l', filteredTmpFile,
29
+ '-o', outputFile,
30
+ '-mc', HTTPX_STATUS_CODES,
31
+ '-silent',
32
+ ];
33
+
34
+ const result = await this.runCommand(getToolPath('httpx'), args, { outputFile });
35
+
36
+ try {
37
+ const { unlinkSync } = await import('fs');
38
+ unlinkSync(filteredTmpFile);
39
+ } catch { /* ignore */ }
40
+
41
+ return result;
42
+ }
43
+ }
@@ -0,0 +1,42 @@
1
+ import { BaseStep } from './base-step.js';
2
+ import { existsSync } from 'fs';
3
+ import { dirname, join } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const isWin = process.platform === 'win32';
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+
9
+ // Bundled script path: metho/scripts/findsomething_secrets.py
10
+ const BUNDLED_SCRIPT = join(__dirname, '..', '..', 'scripts', 'findsomething_secrets.py');
11
+
12
+ export class FindSomethingStep extends BaseStep {
13
+ constructor(logger, ctx) {
14
+ super('findsomething', logger, ctx);
15
+ }
16
+
17
+ async execute(config, inputFile, outputFile) {
18
+ // Priority: explicit config > env var > bundled script
19
+ let scriptPath = config.findsomethingPath;
20
+ if (!scriptPath || !existsSync(scriptPath)) {
21
+ scriptPath = process.env.FINDSOMETHING_PATH;
22
+ }
23
+ if (!scriptPath || !existsSync(scriptPath)) {
24
+ scriptPath = BUNDLED_SCRIPT;
25
+ }
26
+
27
+ if (!existsSync(scriptPath)) {
28
+ throw new Error(
29
+ `FindSomething script not found.\n` +
30
+ `Checked: ${BUNDLED_SCRIPT}\n` +
31
+ `Set the path with --findsomething-path <path> or FINDSOMETHING_PATH env var`
32
+ );
33
+ }
34
+
35
+ this.logger.debug(`Using findsomething script: ${scriptPath}`);
36
+ const pythonBin = isWin ? 'python' : 'python3';
37
+ const args = [scriptPath, '-l', inputFile, '-o', outputFile];
38
+
39
+ const result = await this.runCommand(pythonBin, args, { outputFile });
40
+ return result;
41
+ }
42
+ }