#!/usr/bin/env node /** * Make qvac/worker.entry.mjs portable for packaged Electron / CI clients. * * `@qvac/sdk` bundleSdk writes absolute `file:///…/node_modules/@qvac/…` imports * so bare-pack can resolve addons on the *build* machine. Those paths break as * soon as the app is shipped elsewhere → Bare worker dies → * "RPC initialization timed out after 30000ms — the worker process may have failed to start" * * This rewrites those URLs to relative paths from qvac/worker.entry.mjs, e.g. * ../node_modules/@qvac/sdk/dist/server/worker-core.js * * Usage: * node scripts/rewrite-qvac-worker-entry.cjs * node scripts/rewrite-qvac-worker-entry.cjs --entry /path/to/qvac/worker.entry.mjs * node scripts/rewrite-qvac-worker-entry.cjs --root /path/to/app */ 'use strict' const fs = require('fs') const path = require('path') const { pathToFileURL, fileURLToPath } = require('url') const rootDefault = path.resolve(__dirname, '..') /** * @param {string} entryPath absolute path to worker.entry.mjs * @returns {{ rewritten: number, path: string, ok: boolean, reason?: string }} */ function rewriteWorkerEntry(entryPath) { if (!fs.existsSync(entryPath)) { return { rewritten: 0, path: entryPath, ok: false, reason: 'missing' } } const original = fs.readFileSync(entryPath, 'utf8') const entryDir = path.dirname(entryPath) let count = 0 const next = original.replace(/file:\/\/\/[^\s"'`]+|file:\/\/[^\s"'`]+/g, (href) => { try { let filePath try { filePath = fileURLToPath(href) } catch { return href } if (!fs.existsSync(filePath)) { // Still rewrite if it points under a known node_modules layout // (CI may rewrite before files are fully staged) } let rel = path.relative(entryDir, filePath) if (!rel || rel === path.basename(filePath)) { // same dir rel = `./${path.basename(filePath)}` } else if (!rel.startsWith('.') && !path.isAbsolute(rel)) { rel = `./${rel}` } // ESM wants POSIX separators rel = rel.split(path.sep).join('/') // Prefer bare relative specifier (no file://) — Bare resolves these count++ return rel } catch { return href } }) if (count === 0 && !/file:\/\//.test(original)) { // Already portable — ensure relative @qvac paths exist or write canonical entry if ( original.includes('../node_modules/@qvac/sdk/') || original.includes('@qvac/sdk/') ) { return { rewritten: 0, path: entryPath, ok: true, reason: 'already-portable' } } } if (next !== original) { fs.writeFileSync(entryPath, next, 'utf8') } // Guard: no absolute build-machine paths left if (/file:\/\/\/Users\/|file:\/\/\/home\/|file:\/\/\/[A-Za-z]:\//.test(next)) { return { rewritten: count, path: entryPath, ok: false, reason: 'absolute file:// paths remain after rewrite', } } return { rewritten: count, path: entryPath, ok: true } } /** * Write a known-good portable LLM-only worker entry (matches qvac.config.json). * Used when rewrite fails or entry is missing. * @param {string} entryPath */ function writePortableLlmWorkerEntry(entryPath) { const content = `/** * QVAC SDK Worker Entry (portable) * Relative imports only — safe for packaged Electron / CI clients. * Plugins: 1 — @qvac/sdk/llamacpp-completion/plugin * * Regenerated by scripts/rewrite-qvac-worker-entry.cjs * (bundleSdk emits absolute file:// URLs that break off the build machine) */ import { initializeWorkerCore, ensureRPCSetup } from '../node_modules/@qvac/sdk/dist/server/worker-core.js' import { registerPlugin } from '../node_modules/@qvac/sdk/dist/server/plugins/index.js' import { getServerLogger } from '../node_modules/@qvac/sdk/dist/logging/index.js' import { llmPlugin } from '../node_modules/@qvac/sdk/dist/server/bare/plugins/llamacpp-completion/plugin.js' const { hasRPCConfig } = initializeWorkerCore() const logger = getServerLogger() logger.info('🐻 QVAC Worker (portable bundle)') logger.info('📦 Plugins: 1 (llamacpp-completion)') registerPlugin(llmPlugin) if (hasRPCConfig) { ensureRPCSetup() } ` fs.mkdirSync(path.dirname(entryPath), { recursive: true }) fs.writeFileSync(entryPath, content, 'utf8') return entryPath } /** * @param {{ root?: string, entry?: string, forcePortable?: boolean }} [opts] */ function ensurePortableWorkerEntry(opts = {}) { const root = opts.root || rootDefault const entryPath = opts.entry || path.join(root, 'qvac', 'worker.entry.mjs') if (opts.forcePortable || !fs.existsSync(entryPath)) { writePortableLlmWorkerEntry(entryPath) console.log(`[rewrite-qvac-worker] wrote portable entry: ${entryPath}`) return { ok: true, path: entryPath, rewritten: -1, reason: 'wrote-portable' } } const result = rewriteWorkerEntry(entryPath) if (!result.ok && result.reason !== 'missing') { console.warn( `[rewrite-qvac-worker] rewrite incomplete (${result.reason}) — writing portable LLM entry` ) writePortableLlmWorkerEntry(entryPath) return { ok: true, path: entryPath, rewritten: result.rewritten, reason: 'fallback-portable' } } if (result.reason === 'missing') { writePortableLlmWorkerEntry(entryPath) return { ok: true, path: entryPath, rewritten: -1, reason: 'wrote-portable' } } console.log( `[rewrite-qvac-worker] ${entryPath}: rewritten=${result.rewritten} (${result.reason || 'ok'})` ) return result } function parseArgs(argv) { /** @type {{ root?: string, entry?: string, forcePortable?: boolean }} */ const opts = {} for (let i = 0; i < argv.length; i++) { const a = argv[i] if (a === '--root' && argv[i + 1]) opts.root = path.resolve(argv[++i]) else if (a.startsWith('--root=')) opts.root = path.resolve(a.slice(7)) else if (a === '--entry' && argv[i + 1]) opts.entry = path.resolve(argv[++i]) else if (a.startsWith('--entry=')) opts.entry = path.resolve(a.slice(8)) else if (a === '--force-portable') opts.forcePortable = true else if (a === '--help' || a === '-h') { console.log( 'Usage: node scripts/rewrite-qvac-worker-entry.cjs [--root DIR] [--entry FILE] [--force-portable]' ) process.exit(0) } } return opts } if (require.main === module) { const result = ensurePortableWorkerEntry(parseArgs(process.argv.slice(2))) if (!result.ok) { console.error('[rewrite-qvac-worker] FAILED', result) process.exit(1) } } module.exports = { rewriteWorkerEntry, writePortableLlmWorkerEntry, ensurePortableWorkerEntry, pathToFileURL, }