#!/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 --tmpdir // stdout: single JSON line {ok:true, sha256, size, finalUrl, source, extRoot} // on success, or {ok:false, error:"", 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 `` // 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 `/lib/*.jar`. We walk immediate // children of ; the first child with a `lib/` subdir containing a // `.jar` file is the plugin root. Fallback: return . 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); });