import { resolve } from 'node:path'; import { readFileSync } from 'node:fs'; import { createHash } from 'node:crypto'; // Adapt host services at build time; never edit installed or vendored Pi code. // Exact package pin + component checksums make this boundary reviewable. export function piBrowser() { const sourceRoot = resolve('vendor/pi-coding-agent'); const provenance = JSON.parse(readFileSync(resolve(sourceRoot, 'provenance.json'), 'utf8')); for (const [file, expected] of Object.entries(provenance.files)) { const actual = createHash('sha256').update(readFileSync(resolve(sourceRoot, file))).digest('hex'); if (actual !== expected) throw Error(`Vendored Pi component differs from its upstream checksum: ${file}`); } const host = resolve('src/pi/browser-host.mjs'); return { name: 'pi-browser-host', enforce: 'pre', resolveId(source, importer = '') { if (source === '@earendil-works/pi-tui') return resolve('src/pi/entry.mjs'); if (importer.includes('/@earendil-works/pi-tui/')) { if (source.endsWith('/terminal-image.js')) return resolve('src/pi/capabilities.mjs'); if (source === 'node:perf_hooks') return host; } if (importer.includes('/vendor/pi-coding-agent/') && source === '../theme/theme.js') return resolve('src/pi/theme.mjs'); }, transform(code, id) { if (!id.includes('/@earendil-works/pi-tui/') || !id.split('?')[0].endsWith('.js')) return; // Browser timer IDs don't have Node's event-loop keepalive methods. // Scrolling, selection autoscroll and flash timers still run normally. let adapted = code.replace(/\.unref\(\)/g, '.unref?.()'); if (/\b(?:process|Buffer)\./.test(code)) adapted = `import { browserProcess as process, Buffer } from ${JSON.stringify(host)};\n${adapted}`; if (adapted !== code) return { code: adapted, map: null }; }, }; }