const { createHash } = require('node:crypto') const { chmod, mkdir, readFile, rename, rm, stat, writeFile } = require('node:fs/promises') const { existsSync } = require('node:fs') const { join } = require('node:path') const { spawnSync } = require('node:child_process') const tar = require('tar') const { sha256File } = require('./file-hash.cjs') const opencodeVersion = '1.18.9' const architectureNames = { 1: 'x64', 3: 'arm64' } const platformNames = { darwin: 'darwin', linux: 'linux', win32: 'windows' } function sha512Integrity(contents) { return `sha512-${createHash('sha512').update(contents).digest('base64')}` } async function lockedIntegrity(projectDir, packageName) { const lock = JSON.parse( await readFile(join(projectDir, 'package-lock.json'), 'utf8') ) const entry = lock.packages?.[`node_modules/${packageName}`] if ( entry?.version !== opencodeVersion || typeof entry.integrity !== 'string' ) { throw new Error( `Missing locked ${packageName}@${opencodeVersion} integrity` ) } return entry.integrity } function npmInvocation() { const npmCli = process.env.npm_execpath if (npmCli) { return { command: process.execPath, prefixArgs: [npmCli] } } if (process.platform === 'win32') { throw new Error('npm_execpath is required to prepare bundled runtimes') } return { command: 'npm', prefixArgs: [] } } async function downloadPackage(projectDir, packageName, integrity) { const cacheDirectory = join( projectDir, '.runtime-resources', 'cache' ) await mkdir(cacheDirectory, { recursive: true }) const archivePath = join( cacheDirectory, `${packageName}-${opencodeVersion}.tgz` ) if (existsSync(archivePath)) { const cached = await readFile(archivePath) if (sha512Integrity(cached) === integrity) { return archivePath } await rm(archivePath, { force: true }) } const npm = npmInvocation() const result = spawnSync( npm.command, [ ...npm.prefixArgs, 'pack', `${packageName}@${opencodeVersion}`, '--ignore-scripts', '--json', '--pack-destination', cacheDirectory ], { cwd: projectDir, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, shell: false, windowsHide: true } ) if (result.status !== 0) { throw new Error( `Unable to fetch ${packageName}: ${ result.error?.message || result.stderr || result.stdout || `npm exited with ${result.status}` }` ) } const output = JSON.parse(result.stdout) const downloadedPath = join(cacheDirectory, output[0].filename) const contents = await readFile(downloadedPath) if (sha512Integrity(contents) !== integrity) { await rm(downloadedPath, { force: true }) throw new Error(`Integrity verification failed for ${packageName}`) } if (downloadedPath !== archivePath) { await rm(archivePath, { force: true }) await rename(downloadedPath, archivePath) } return archivePath } module.exports = async function prepareBundledRuntimes(context) { const platform = context.electronPlatformName const architecture = architectureNames[context.arch] const packagePlatform = platformNames[platform] if (!architecture || !packagePlatform) { throw new Error( `Bundled OpenCode does not support ${platform}/${context.arch}` ) } const suffix = architecture === 'x64' ? `${architecture}-baseline` : architecture const packageName = `opencode-${packagePlatform}-${suffix}` const projectDir = context.packager.projectDir const integrity = await lockedIntegrity(projectDir, packageName) const targetDirectory = join( projectDir, '.runtime-resources', architecture ) const readyPath = join(targetDirectory, '.ready.json') const executable = platform === 'win32' ? 'opencode.exe' : 'opencode' const preparedPath = join(targetDirectory, executable) const identity = { packageName, version: opencodeVersion, integrity } try { const ready = JSON.parse(await readFile(readyPath, 'utf8')) if ( ready.packageName === identity.packageName && ready.version === identity.version && ready.integrity === identity.integrity && typeof ready.executableSha256 === 'string' && (platform === 'win32' || ((await stat(preparedPath)).mode & 0o111) !== 0) && (await sha256File(preparedPath)) === ready.executableSha256 ) { return } } catch { // Rebuild an incomplete or stale runtime cache. } const archivePath = await downloadPackage( projectDir, packageName, integrity ) const stagingDirectory = `${targetDirectory}.staging-${process.pid}` await rm(stagingDirectory, { recursive: true, force: true }) await mkdir(stagingDirectory, { recursive: true }) try { await tar.x({ file: archivePath, cwd: stagingDirectory, strip: 1 }) const sourcePath = join(stagingDirectory, 'bin', executable) const stagingExecutable = join(stagingDirectory, executable) await rename(sourcePath, stagingExecutable) if (platform !== 'win32') { await chmod(stagingExecutable, 0o755) } const executableSha256 = await sha256File(stagingExecutable) await writeFile( join(stagingDirectory, '.ready.json'), JSON.stringify({ ...identity, executableSha256 }), 'utf8' ) await rm(targetDirectory, { recursive: true, force: true }) await rename(stagingDirectory, targetDirectory) } finally { await rm(stagingDirectory, { recursive: true, force: true }) } }