164 lines
5.2 KiB
JavaScript
164 lines
5.2 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { createRequire } from 'node:module';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const root = path.resolve(__dirname, '..');
|
|
const require = createRequire(import.meta.url);
|
|
const outputDir = path.join(root, 'build', 'opencode-ai');
|
|
const rootPackageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
|
|
const targetArgs = process.argv.slice(2);
|
|
for (const arg of targetArgs) {
|
|
if (!/^--(?:platform|arch)=\S+$/.test(arg)) {
|
|
throw new Error(`Unknown bundle target option: ${arg}`);
|
|
}
|
|
}
|
|
|
|
function targetOption(name, fallback) {
|
|
const prefix = `--${name}=`;
|
|
const option = targetArgs.find((arg) => arg.startsWith(prefix));
|
|
return option ? option.slice(prefix.length) : fallback;
|
|
}
|
|
|
|
const targetPlatform = targetOption('platform', process.platform);
|
|
const targetArch = targetOption('arch', process.arch);
|
|
|
|
function fail(message) {
|
|
console.error(`[bundle-opencode] ${message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (targetPlatform !== process.platform) {
|
|
fail(`Cross-OS OpenCode staging is not supported: host ${process.platform}, target ${targetPlatform}.`);
|
|
}
|
|
|
|
function resolvePackageRoot(packageName) {
|
|
try {
|
|
return path.dirname(require.resolve(`${packageName}/package.json`, {
|
|
paths: [root],
|
|
}));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const packageRoot = resolvePackageRoot('opencode-ai');
|
|
if (!packageRoot) {
|
|
fail('Missing dependency "opencode-ai". Run npm install or pnpm install before packaging.');
|
|
}
|
|
|
|
const packageJsonPath = path.join(packageRoot, 'package.json');
|
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
const declaredVersion = rootPackageJson.devDependencies?.['opencode-ai']
|
|
?? rootPackageJson.dependencies?.['opencode-ai'];
|
|
if (declaredVersion !== packageJson.version) {
|
|
fail(
|
|
`The opencode-ai dependency must be pinned exactly to the bundled version. Declared ${declaredVersion ?? 'missing'}, installed ${packageJson.version ?? 'unknown'}.`,
|
|
);
|
|
}
|
|
const packageRequire = createRequire(packageJsonPath);
|
|
const bin = packageJson.bin;
|
|
const hasOpencodeBin = typeof bin === 'string'
|
|
|| (bin && typeof bin === 'object' && typeof bin.opencode === 'string');
|
|
|
|
if (!hasOpencodeBin) {
|
|
fail('Installed "opencode-ai" package does not expose an "opencode" binary.');
|
|
}
|
|
|
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
fs.mkdirSync(path.dirname(outputDir), { recursive: true });
|
|
fs.cpSync(packageRoot, outputDir, {
|
|
recursive: true,
|
|
dereference: true,
|
|
filter: (src) => {
|
|
if (src.includes(`${path.sep}.git${path.sep}`)) return false;
|
|
if (path.relative(packageRoot, src).split(path.sep)[0] === 'node_modules') return false;
|
|
return true;
|
|
},
|
|
});
|
|
|
|
const platformMap = {
|
|
darwin: 'darwin',
|
|
linux: 'linux',
|
|
win32: 'windows',
|
|
};
|
|
const archMap = {
|
|
x64: 'x64',
|
|
arm64: 'arm64',
|
|
arm: 'arm',
|
|
};
|
|
|
|
function getNativePackageCandidates() {
|
|
const platform = platformMap[targetPlatform] ?? targetPlatform;
|
|
const arch = archMap[targetArch] ?? targetArch;
|
|
const base = `opencode-${platform}-${arch}`;
|
|
|
|
if (arch === 'x64') {
|
|
return [`${base}-baseline`, base];
|
|
}
|
|
|
|
return [base];
|
|
}
|
|
|
|
function bundleNativeLauncher() {
|
|
const binaryName = targetPlatform === 'win32' ? 'opencode.exe' : 'opencode';
|
|
const outputBinary = path.join(outputDir, 'bin', binaryName);
|
|
|
|
for (const packageName of getNativePackageCandidates()) {
|
|
let dependencyPackageJsonPath;
|
|
try {
|
|
dependencyPackageJsonPath = packageRequire.resolve(`${packageName}/package.json`);
|
|
} catch {
|
|
continue;
|
|
}
|
|
|
|
const dependencyRoot = path.dirname(dependencyPackageJsonPath);
|
|
const nativeBinary = path.join(dependencyRoot, 'bin', binaryName);
|
|
if (!fs.existsSync(nativeBinary)) {
|
|
continue;
|
|
}
|
|
|
|
fs.copyFileSync(nativeBinary, outputBinary);
|
|
fs.chmodSync(outputBinary, 0o755);
|
|
return { packageName, outputBinary };
|
|
}
|
|
|
|
fail(
|
|
`Missing native opencode binary for ${targetPlatform}/${targetArch}. Tried ${getNativePackageCandidates().join(', ')}.`,
|
|
);
|
|
}
|
|
|
|
const nativeLauncher = bundleNativeLauncher();
|
|
const verificationEnv = {
|
|
...process.env,
|
|
PATH: process.platform === 'win32'
|
|
? [process.env.SystemRoot && path.join(process.env.SystemRoot, 'System32'), process.env.SystemRoot]
|
|
.filter(Boolean)
|
|
.join(path.delimiter)
|
|
: '/usr/bin:/bin',
|
|
};
|
|
delete verificationEnv.NODE_PATH;
|
|
delete verificationEnv.BUN_INSTALL;
|
|
const versionProbe = spawnSync(nativeLauncher.outputBinary, ['--version'], {
|
|
cwd: outputDir,
|
|
encoding: 'utf8',
|
|
env: verificationEnv,
|
|
});
|
|
if (versionProbe.error || versionProbe.status !== 0) {
|
|
fail(
|
|
`Bundled native launcher failed its self-contained version probe: ${versionProbe.error?.message ?? versionProbe.stderr ?? versionProbe.stdout}`,
|
|
);
|
|
}
|
|
const reportedVersion = `${versionProbe.stdout ?? ''}${versionProbe.stderr ?? ''}`.trim();
|
|
if (reportedVersion !== packageJson.version) {
|
|
fail(`Bundled native launcher reported ${reportedVersion || 'no version'}, expected ${packageJson.version}.`);
|
|
}
|
|
console.log(
|
|
`[bundle-opencode] bundled and verified self-contained opencode-ai@${packageJson.version} launcher ${nativeLauncher.packageName} -> ${path.relative(root, outputDir)}`,
|
|
);
|