import { execFile } from 'node:child_process' import { appendFile, mkdir } from 'node:fs/promises' import { homedir } from 'node:os' import { dirname, join } from 'node:path' const textEncoder = new TextEncoder() let readBuffer = Buffer.alloc(0) let pendingRequests = 0 let stdinEnded = false function sendMessage(payload) { const body = Buffer.from(JSON.stringify(payload), 'utf8') const len = Buffer.alloc(4) len.writeUInt32LE(body.length, 0) process.stdout.write(len) process.stdout.write(body) } function parseHeaders(lines = []) { const map = new Map() for (const line of lines) { const idx = line.indexOf(':') if (idx <= 0) continue const key = line.slice(0, idx).trim().toLowerCase() const value = line.slice(idx + 1).trim() if (!key || !value) continue map.set(key, value) } return map } function parseCookieHeader(lines = []) { const headers = parseHeaders(lines) return headers.get('cookie') || '' } function queueFilePath() { return join(homedir(), '.gdown', 'external_add_queue.jsonl') } async function enqueueExternalAdd(payload) { const filePath = queueFilePath() await mkdir(dirname(filePath), { recursive: true }) await appendFile(filePath, `${JSON.stringify(payload)}\n`, 'utf8') } function focusGdownApp() { return new Promise((resolve) => { if (process.platform !== 'darwin') { resolve({ ok: true, note: 'focus noop on non-macos in step1 host' }) return } const attempts = [ ['osascript', ['-e', 'tell application id "com.tauri.dev" to activate']], ['osascript', ['-e', 'tell application "gdown" to activate']], ['osascript', ['-e', 'tell application "System Events" to set frontmost of first process whose name is "gdown" to true']], ['osascript', ['-e', 'tell application "System Events" to set frontmost of first process whose name is "Gdown" to true']], ['osascript', ['-e', 'tell application "System Events" to set frontmost of first process whose name is "app" to true']], ['open', ['-b', 'com.tauri.dev']], ['open', ['-a', 'gdown']], ] const run = (index) => { if (index >= attempts.length) { resolve({ ok: false, message: 'all focus strategies failed' }) return } const [bin, args] = attempts[index] execFile(bin, args, (error) => { if (!error) { resolve({ ok: true, strategy: `${bin} ${args.join(' ')}` }) return } run(index + 1) }) } run(0) }) } async function handleRequest(message) { const action = String(message?.action || '').trim() if (action === 'ping') { return { ok: true, version: '0.1.0', host: 'org.gdown.nativehost', capabilities: ['ping', 'addUri', 'focus'], } } if (action === 'addUri') { const url = String(message?.url || '').trim() if (!url) return { ok: false, error: 'url is required' } const referer = String(message?.referer || '').trim() const userAgent = String(message?.userAgent || '').trim() const out = String(message?.out || '').trim() const dir = String(message?.dir || '').trim() const cookie = String(message?.cookie || '').trim() const authorization = String(message?.authorization || '').trim() const proxy = String(message?.proxy || '').trim() const split = Number(message?.split || 0) const parsedCookie = parseCookieHeader(Array.isArray(message?.headers) ? message.headers : []) const cookieValue = cookie || parsedCookie await enqueueExternalAdd({ url, referer: referer || undefined, userAgent: userAgent || undefined, out: out || undefined, dir: dir || undefined, cookie: cookieValue || undefined, authorization: authorization || undefined, proxy: proxy || undefined, split: Number.isFinite(split) && split > 0 ? Math.round(split) : undefined, }) await focusGdownApp() return { ok: true, pending: true, mode: 'prompt', requestId: `pending-${Date.now()}`, } } if (action === 'focus') { return focusGdownApp() } return { ok: false, error: `unknown action: ${action || '(empty)'}` } } async function drainBuffer() { while (readBuffer.length >= 4) { const bodyLength = readBuffer.readUInt32LE(0) if (readBuffer.length < 4 + bodyLength) return const body = readBuffer.subarray(4, 4 + bodyLength) readBuffer = readBuffer.subarray(4 + bodyLength) let payload try { payload = JSON.parse(body.toString('utf8')) } catch (error) { sendMessage({ ok: false, error: `invalid JSON payload: ${String(error)}` }) continue } try { pendingRequests += 1 const result = await handleRequest(payload) sendMessage(result) } catch (error) { sendMessage({ ok: false, error: String(error) }) } finally { pendingRequests -= 1 if (stdinEnded && pendingRequests === 0) { process.exit(0) } } } } process.stdin.on('data', async (chunk) => { readBuffer = Buffer.concat([readBuffer, chunk]) await drainBuffer() }) process.stdin.on('end', () => { stdinEnded = true if (pendingRequests === 0) { process.exit(0) } }) // Emit one line for manual shell smoke visibility (not used by native messaging framing). process.stderr.write(`[native-host] started pid=${process.pid}\n`)