feat: remove legacy OpenCode runtime

Cut product flows over to Coding/Pi and retain only the migration-owned v1 boundary. Promote supported native optional packages because electron-builder omitted pnpm transitive optional closure from the packaged ASAR.
This commit is contained in:
2026-08-24 12:17:43 +08:00
parent 61fede2b4d
commit 5a275b93a7
199 changed files with 1330 additions and 64725 deletions

View File

@@ -55,6 +55,7 @@ const PERSISTED_USER_DATA_FILES = new Set([
'gateway-prelaunch-maintenance-cache.json',
'niancode-device-identity.json',
'niancode-providers.json',
// Legacy installed-user store filename; it must never enter an artifact.
'opencode-projects.json',
'settings.json',
]);

View File

@@ -1,163 +0,0 @@
#!/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)}`,
);

View File

@@ -1,202 +0,0 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { existsSync } from 'node:fs';
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createServer } from 'node:net';
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const enabled = process.env.NIANCODE_OPENCODE_REAL_SMOKE === '1'
|| process.env.OPENCODE_REAL_SMOKE === '1';
function skip(reason) {
console.log(`[opencode-real-smoke] skipped: ${reason}`);
process.exit(0);
}
if (!enabled) {
skip('set NIANCODE_OPENCODE_REAL_SMOKE=1 to launch a real opencode runtime');
}
function resolveOpencodeBin() {
const explicit = process.env.NIANCODE_OPENCODE_BIN || process.env.OPENCODE_BIN;
if (explicit) return explicit;
const binName = process.platform === 'win32' ? 'opencode.cmd' : 'opencode';
const localBin = join(repoRoot, 'node_modules', '.bin', binName);
if (existsSync(localBin)) return localBin;
return binName;
}
async function resolveConfigContent() {
if (process.env.OPENCODE_CONFIG_CONTENT) return process.env.OPENCODE_CONFIG_CONTENT;
if (process.env.OPENCODE_REAL_SMOKE_CONFIG) return process.env.OPENCODE_REAL_SMOKE_CONFIG;
if (process.env.OPENCODE_REAL_SMOKE_CONFIG_FILE) {
return await readFile(resolve(process.env.OPENCODE_REAL_SMOKE_CONFIG_FILE), 'utf8');
}
return null;
}
async function getFreePort() {
return await new Promise((resolvePort, reject) => {
const server = createServer();
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
const address = server.address();
const port = typeof address === 'object' && address ? address.port : null;
server.close(() => {
if (typeof port === 'number') {
resolvePort(port);
return;
}
reject(new Error('Unable to reserve a local smoke-test port'));
});
});
});
}
function waitForListening(proc, timeoutMs) {
return awaitableProcessOutput(proc, timeoutMs, (text) => {
const match = text.match(/opencode server listening\s+on\s+(https?:\/\/[^\s]+)/);
return match?.[1] ?? null;
});
}
function awaitableProcessOutput(proc, timeoutMs, resolveFromText) {
return new Promise((resolveUrl, reject) => {
let settled = false;
let stderr = '';
const finish = (callback) => {
if (settled) return;
settled = true;
clearTimeout(timer);
callback();
};
const timer = setTimeout(() => {
finish(() => reject(new Error(`Timed out waiting for opencode server after ${timeoutMs}ms${stderr ? `\n${stderr}` : ''}`)));
}, timeoutMs);
proc.stdout?.on('data', (chunk) => {
const text = chunk.toString();
process.stdout.write(text);
const result = resolveFromText(text);
if (result) finish(() => resolveUrl(result));
});
proc.stderr?.on('data', (chunk) => {
const text = chunk.toString();
stderr += text;
process.stderr.write(text);
});
proc.on('error', (error) => finish(() => reject(error)));
proc.on('exit', (code) => {
finish(() => reject(new Error(`opencode exited before it became ready: ${code ?? 'unknown'}${stderr ? `\n${stderr}` : ''}`)));
});
});
}
function sessionIdOf(session) {
if (!session || typeof session !== 'object') return null;
const value = session.id ?? session.sessionID ?? session.sessionId;
return typeof value === 'string' && value.trim() ? value : null;
}
async function requestJson(url, init = {}) {
const response = await fetch(url, {
...init,
headers: {
'Content-Type': 'application/json',
...(init.headers ?? {}),
},
});
if (!response.ok) {
const body = await response.text().catch(() => '');
throw new Error(`opencode request failed ${response.status} ${response.statusText}${body ? `\n${body}` : ''}`);
}
if (response.status === 204) return null;
return await response.json();
}
async function createProjectDir() {
if (process.env.OPENCODE_REAL_SMOKE_PROJECT) {
return {
path: resolve(process.env.OPENCODE_REAL_SMOKE_PROJECT),
cleanup: async () => {},
};
}
const path = await mkdtemp(join(tmpdir(), 'niancode-opencode-smoke-'));
await writeFile(join(path, 'README.md'), '# Makelore opencode real-runtime smoke\n');
return {
path,
cleanup: async () => {
await rm(path, { recursive: true, force: true });
},
};
}
const configContent = await resolveConfigContent();
if (!configContent && process.env.OPENCODE_REAL_SMOKE_ALLOW_LOCAL_CONFIG !== '1') {
skip('provide OPENCODE_CONFIG_CONTENT, OPENCODE_REAL_SMOKE_CONFIG, or OPENCODE_REAL_SMOKE_CONFIG_FILE');
}
JSON.parse(configContent ?? '{}');
const binPath = resolveOpencodeBin();
const port = Number(process.env.OPENCODE_REAL_SMOKE_PORT) || await getFreePort();
const startupTimeoutMs = Number(process.env.OPENCODE_REAL_SMOKE_STARTUP_TIMEOUT_MS) || 20_000;
const apiTimeoutMs = Number(process.env.OPENCODE_REAL_SMOKE_API_TIMEOUT_MS) || 60_000;
const project = await createProjectDir();
const proc = spawn(binPath, ['serve', '--hostname=127.0.0.1', `--port=${port}`], {
env: {
...process.env,
...(configContent ? { OPENCODE_CONFIG_CONTENT: configContent } : {}),
},
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
shell: process.platform === 'win32' && /\.cmd$/i.test(binPath),
});
try {
const baseUrl = await waitForListening(proc, startupTimeoutMs);
const directoryQuery = `directory=${encodeURIComponent(project.path)}`;
const scopedHeaders = { 'x-opencode-directory': encodeURIComponent(project.path) };
const sessions = await requestJson(`${baseUrl}/session?${directoryQuery}`, {
signal: AbortSignal.timeout(apiTimeoutMs),
headers: scopedHeaders,
});
if (!Array.isArray(sessions)) {
throw new Error('Expected /session to return an array');
}
const session = await requestJson(`${baseUrl}/session?${directoryQuery}`, {
method: 'POST',
body: JSON.stringify({}),
signal: AbortSignal.timeout(apiTimeoutMs),
headers: scopedHeaders,
});
const sessionId = sessionIdOf(session);
if (!sessionId) {
throw new Error('Created opencode session did not include an id');
}
if (process.env.OPENCODE_REAL_SMOKE_PROMPT) {
await requestJson(`${baseUrl}/session/${encodeURIComponent(sessionId)}/message?${directoryQuery}`, {
method: 'POST',
body: JSON.stringify({
parts: [
{ type: 'text', text: process.env.OPENCODE_REAL_SMOKE_PROMPT },
],
}),
signal: AbortSignal.timeout(apiTimeoutMs),
headers: scopedHeaders,
});
}
console.log(`[opencode-real-smoke] ok: started ${baseUrl}, listed ${sessions.length} session(s), created ${sessionId}`);
} finally {
proc.kill();
await project.cleanup();
}

View File

@@ -1,13 +1,8 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { getPythonTarget, PYTHON_VERSION } from './bundled-python-manifest.mjs';
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = path.resolve(SCRIPT_DIR, '..');
function readOption(name) {
const prefix = `--${name}=`;
const inline = process.argv.find((value) => value.startsWith(prefix));
@@ -35,10 +30,8 @@ if (!targetId || !root) {
try {
const target = getPythonTarget(targetId);
const executable = path.resolve(root, target.executable);
const meowaCli = path.join(PROJECT_ROOT, '.opencode', 'skills', 'game-assets', 'meowart_api.py');
runProbe(executable, 'version', ['--version']);
runProbe(executable, 'stdlib', ['-c', "import json, pathlib, urllib.request; print('stdlib-ok')"]);
runProbe(executable, 'meowa-help', [meowaCli, '--help']);
process.stdout.write(`[python] ${targetId} verified for Python ${PYTHON_VERSION}\n`);
} catch (error) {
process.stderr.write(`[python] ${targetId} verification failed: ${error instanceof Error ? error.message : String(error)}\n`);

View File

@@ -146,9 +146,6 @@ const npmPackagePath = join(npmRuntimeDir, 'package.json');
const npmCliPath = join(npmRuntimeDir, 'bin', 'npm-cli.js');
const appAsarPath = join(resourcesDir, 'app.asar');
const appAsarUnpackedPath = join(resourcesDir, 'app.asar.unpacked');
const opencodeRuntimeDir = join(resourcesDir, 'opencode-ai');
const opencodePackagePath = join(opencodeRuntimeDir, 'package.json');
const opencodeExecutable = join(opencodeRuntimeDir, 'bin', 'opencode.exe');
const pythonRuntimeDir = join(resourcesDir, 'python');
const pythonExecutable = join(pythonRuntimeDir, 'python.exe');
const toolsBinDir = join(resourcesDir, 'bin');
@@ -159,8 +156,6 @@ for (const filePath of [
appAsarPath,
npmPackagePath,
npmCliPath,
opencodePackagePath,
opencodeExecutable,
pythonExecutable,
uvExecutable,
]) {
@@ -170,31 +165,20 @@ for (const filePath of [
const PE_MACHINE_AMD64 = 0x8664;
for (const [label, executable] of [
['Makelore executable', appExecutable],
['OpenCode executable', opencodeExecutable],
['Python executable', pythonExecutable],
['uv executable', uvExecutable],
]) {
assertPeMachine(executable, PE_MACHINE_AMD64, label);
}
const declaredOpencodeVersion = packageJson.devDependencies?.['opencode-ai']
?? packageJson.dependencies?.['opencode-ai'];
const packagedOpencode = JSON.parse(readFileSync(opencodePackagePath, 'utf8'));
assertEqual(packagedOpencode.version, declaredOpencodeVersion, 'packaged OpenCode version');
const systemRoot = process.env.SystemRoot ?? process.env.WINDIR ?? 'C:\\Windows';
const installLocalProbeEnv = {
...process.env,
PATH: [join(systemRoot, 'System32'), systemRoot].join(';'),
};
for (const key of ['NODE_PATH', 'BUN_INSTALL', 'OPENCODE_BIN', 'OPENCODE_PATH', 'NIANCODE_PYTHON_PATH', 'NIANCODE_UV_PATH']) {
for (const key of ['NODE_PATH', 'BUN_INSTALL', 'NIANCODE_PYTHON_PATH', 'NIANCODE_UV_PATH']) {
delete installLocalProbeEnv[key];
}
const opencodeVersion = run(opencodeExecutable, ['--version'], {
cwd: opencodeRuntimeDir,
env: installLocalProbeEnv,
});
assertEqual(opencodeVersion, declaredOpencodeVersion, 'OpenCode executable version');
const uvVersion = run(uvExecutable, ['--version'], {
cwd: toolsBinDir,
env: installLocalProbeEnv,
@@ -232,8 +216,6 @@ const appRequire = createRequire(join(dirname(process.execPath), 'resources', 'a
const packagedPackage = appRequire('./package.json');
const msgpackResolved = appRequire.resolve('msgpackr');
const canvasResolved = appRequire.resolve('@napi-rs/canvas');
const playwrightPackageResolved = appRequire.resolve('@playwright/mcp/package.json');
const playwrightCliResolved = join(dirname(playwrightPackageResolved), 'cli.js');
const { pack, unpack } = appRequire('msgpackr');
const { createCanvas } = appRequire('@napi-rs/canvas');
const packedValue = unpack(pack({ ok: true, text: '\u5362\u6b22' }));
@@ -271,8 +253,6 @@ console.log('NIANCODE_ARTIFACT_PROBE=' + JSON.stringify({
},
msgpackResolved,
canvasResolved,
playwrightPackageResolved,
playwrightCliResolved,
nativeModules,
packedValue,
canvas: [canvas.width, canvas.height],
@@ -316,12 +296,6 @@ if (!isPathInside(appAsarPath, probe.msgpackResolved)) {
if (!isPathInside(appAsarPath, probe.canvasResolved)) {
throw new Error(`@napi-rs/canvas was not resolved from app.asar: ${probe.canvasResolved}`);
}
if (!isPathInside(appAsarPath, probe.playwrightPackageResolved)) {
throw new Error(`@playwright/mcp was not resolved from app.asar: ${probe.playwrightPackageResolved}`);
}
if (!isPathInside(appAsarPath, probe.playwrightCliResolved)) {
throw new Error(`@playwright/mcp CLI was not resolved from app.asar: ${probe.playwrightCliResolved}`);
}
const packagedNpm = JSON.parse(readFileSync(npmPackagePath, 'utf8'));
assertEqual(packagedNpm.version, packageJson.dependencies.npm, 'packaged npm version');
const npmVersion = run(appExecutable, [npmCliPath, '--version'], {
@@ -329,10 +303,6 @@ const npmVersion = run(appExecutable, [npmCliPath, '--version'], {
env: { ...installLocalProbeEnv, ELECTRON_RUN_AS_NODE: '1' },
});
assertEqual(npmVersion, packageJson.dependencies.npm, 'packaged npm CLI version');
run(appExecutable, [probe.playwrightCliResolved, '--help'], {
cwd: dirname(appExecutable),
env: { ...installLocalProbeEnv, ELECTRON_RUN_AS_NODE: '1' },
});
const msgpackNative = probe.nativeModules.find(({ modulePath, physicalPath, exists }) => (
exists
&& isPathInside(appAsarPath, modulePath)
@@ -372,15 +342,6 @@ const evidence = {
installerSha256,
runtime: { electron: probe.electron, node: probe.node },
installLocalRuntimes: {
opencode: {
executable: opencodeExecutable,
version: opencodeVersion,
sha256: await sha256(opencodeExecutable),
},
playwrightMcp: {
packageJson: probe.playwrightPackageResolved,
cli: probe.playwrightCliResolved,
},
python: pythonProbe,
uv: { executable: uvExecutable, version: uvVersion },
npm: { packageJson: npmPackagePath, cli: npmCliPath, version: npmVersion },