ktg-plugin-marketplace/plugins/llm-security/scanners/lib/jetbrains-fetch-worker.mjs

114 lines
3.8 KiB
JavaScript

#!/usr/bin/env node
// jetbrains-fetch-worker.mjs — Sub-process worker that fetches a JetBrains
// plugin URL and extracts the downloaded ZIP into a writable directory.
// Mirrors `vsix-fetch-worker.mjs` IPC exactly.
//
// Contract:
// stdin: none
// argv: --url <url> --tmpdir <writable-dir>
// stdout: single JSON line {ok:true, sha256, size, finalUrl, source, extRoot}
// on success, or {ok:false, error:"<msg>", code?:"<ZIP_CODE>"} on failure
// stderr: never (silent — all errors via JSON on stdout)
// exit: 0 on success, 1 on any failure (caller still parses stdout)
//
// Key JetBrains-specific difference from the VSIX worker: plugin archives are
// NOT nested under `extension/`. The top-level entry is usually the plugin
// directory itself — identified by the presence of a `lib/` subdir containing
// at least one `*.jar`. If no such directory is found, fall back to `<tmpdir>`
// itself and let the parser surface a warning.
import { existsSync, readdirSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { fetchJetBrainsPlugin, detectUrlType } from './vsix-fetch.mjs';
import { extractToDir, ZipError } from './zip-extract.mjs';
function emit(obj) {
process.stdout.write(JSON.stringify(obj) + '\n');
}
function parseArgs(argv) {
const out = { url: null, tmpdir: null };
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--url' && i + 1 < argv.length) out.url = argv[++i];
else if (argv[i] === '--tmpdir' && i + 1 < argv.length) out.tmpdir = argv[++i];
}
return out;
}
// Find the top-level plugin dir inside an extracted JetBrains archive.
// JetBrains plugin zips contain `<plugin-name>/lib/*.jar`. We walk immediate
// children of <tmpdir>; the first child with a `lib/` subdir containing a
// `.jar` file is the plugin root. Fallback: return <tmpdir>.
function findPluginRoot(tmpdir) {
let entries;
try { entries = readdirSync(tmpdir); } catch { return tmpdir; }
for (const name of entries) {
const candidate = join(tmpdir, name);
let s;
try { s = statSync(candidate); } catch { continue; }
if (!s.isDirectory()) continue;
const libDir = join(candidate, 'lib');
let libStat;
try { libStat = statSync(libDir); } catch { continue; }
if (!libStat.isDirectory()) continue;
let libEntries;
try { libEntries = readdirSync(libDir); } catch { continue; }
if (libEntries.some((n) => n.toLowerCase().endsWith('.jar'))) {
return candidate;
}
}
return tmpdir;
}
async function main() {
const { url, tmpdir: dir } = parseArgs(process.argv.slice(2));
if (!url || !dir) {
emit({ ok: false, error: 'missing --url or --tmpdir' });
process.exit(1);
}
const detected = detectUrlType(url);
if (detected.type !== 'jetbrains') {
emit({ ok: false, error: `worker expected JetBrains URL, got type=${detected.type}` });
process.exit(1);
}
let fetched;
try {
fetched = await fetchJetBrainsPlugin({
numericId: detected.numericId,
xmlId: detected.xmlId,
version: detected.version,
});
} catch (err) {
emit({ ok: false, error: `fetch failed: ${err.message}` });
process.exit(1);
}
try {
await extractToDir(fetched.buffer, dir);
} catch (err) {
if (err instanceof ZipError) {
emit({ ok: false, error: `malformed JetBrains plugin (${err.code}): ${err.message}`, code: err.code });
} else {
emit({ ok: false, error: `extract failed: ${err.message}` });
}
process.exit(1);
}
const extRoot = existsSync(dir) ? findPluginRoot(dir) : dir;
emit({
ok: true,
sha256: fetched.sha256,
size: fetched.size,
finalUrl: fetched.finalUrl,
source: fetched.source,
extRoot,
});
process.exit(0);
}
main().catch((err) => {
emit({ ok: false, error: `worker crashed: ${err.message || String(err)}` });
process.exit(1);
});