3681 lines · 118.4 KB · JavaScript
RawDownload
#!/usr/bin/env node import { createHash } from "node:crypto";import {	copyFileSync,	existsSync,	mkdirSync,	readFileSync,	rmSync,	writeFileSync,} from "node:fs";import { mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";import { tmpdir } from "node:os";import { dirname, join, resolve } from "node:path";import { spawnSync } from "node:child_process";import { fileURLToPath } from "node:url"; const SOURCE_REPOSITORY = "anomalyco/opencode";const SOURCE_VERSION = "1.3.13";const SOURCE_TARBALL_URL = `https://github.com/${SOURCE_REPOSITORY}/archive/refs/tags/v${SOURCE_VERSION}.tar.gz`; // Upstream `packages/app/package.json` pins `ghostty-web: github:anomalyco/ghostty-web#main`,// but the bundled bun.lock snapshots the SHA below. Because `main` is a moving ref, a fresh// `bun install --frozen-lockfile` resolves to whatever `main` points at *now* and fails the// lockfile check. Rewriting the manifest to the lockfile's SHA before install keeps frozen-// lockfile guarantees intact (i.e. CI still breaks loudly if either side drifts) without// trusting arbitrary new HEADs on the ghostty-web branch.const GHOSTTY_WEB_PINNED_SHA = "4af877d"; const __dirname = dirname(fileURLToPath(import.meta.url));const packageDir = resolve(__dirname, "..");const distDir = resolve(packageDir, "dist");const cacheDir = resolve(packageDir, "node_modules", ".cache", "opencode-build");const patchPath = resolve(packageDir, "upstream", `opencode-v${SOURCE_VERSION}.patch`);const bundleDir = resolve(distDir, "opencode-acp");const manifestPath = resolve(distDir, "opencode-acp.manifest.json");const SQL_JS_VERSION = "1.14.1";const bunBin = resolve(	packageDir,	"node_modules",	".bin",	process.platform === "win32" ? "bun.cmd" : "bun",); function run(command, args, options = {}) {	const result = spawnSync(command, args, {		stdio: "inherit",		...options,	});	if (result.status !== 0) {		throw new Error(			`Command failed (${result.status ?? "unknown"}): ${command} ${args.join(" ")}`,		);	}	return result;} function pinGhosttyWebRef(sourceRoot) {	const manifestPath = resolve(sourceRoot, "packages", "app", "package.json");	const raw = readFileSync(manifestPath, "utf-8");	const movingRef = '"ghostty-web": "github:anomalyco/ghostty-web#main"';	const pinnedRef = `"ghostty-web": "github:anomalyco/ghostty-web#${GHOSTTY_WEB_PINNED_SHA}"`;	if (raw.includes(pinnedRef)) return;	if (!raw.includes(movingRef)) {		throw new Error(			`Expected ghostty-web ref ${movingRef} in ${manifestPath}; upstream layout changed — re-audit GHOSTTY_WEB_PINNED_SHA before updating.`,		);	}	writeFileSync(manifestPath, raw.replace(movingRef, pinnedRef));} const PATCHED_SOURCE_FILES = [	"packages/opencode/src/cli/cmd/acp.ts",	"packages/opencode/src/config/config.ts",	"packages/opencode/src/plugin/index.ts",	"packages/opencode/src/server/instance.ts",	"packages/opencode/src/server/server.ts",]; async function ensureNodeAcpPatch(sourceRoot, tarballPath) {	const serverFile = resolve(		sourceRoot,		"packages",		"opencode",		"src",		"server",		"server.ts",	);	const pluginFile = resolve(		sourceRoot,		"packages",		"opencode",		"src",		"plugin",		"index.ts",	);	const acpFile = resolve(		sourceRoot,		"packages",		"opencode",		"src",		"cli",		"cmd",		"acp.ts",	);	const serverSource = readFileSync(serverFile, "utf-8");	const pluginSource = readFileSync(pluginFile, "utf-8");	const acpSource = readFileSync(acpFile, "utf-8");	const alreadyPatched =		serverSource.includes('from "node:http"') &&		!serverSource.includes('from "hono/bun"') &&		pluginSource.includes("Bun shell is unavailable in the Node ACP build") &&		acpSource.includes("const server = await Server.listen(opts)"); 	if (alreadyPatched) {		return;	} 	const scratchRoot = await mkdtemp(join(tmpdir(), "agentos-opencode-patch-"));	try {		run("tar", ["-xzf", tarballPath, "--strip-components=1", "-C", scratchRoot]);		run("git", ["apply", "--whitespace=nowarn", patchPath], { cwd: scratchRoot });		for (const relativePath of PATCHED_SOURCE_FILES) {			copyFileSync(				resolve(scratchRoot, relativePath),				resolve(sourceRoot, relativePath),			);		}	} finally {		rmSync(scratchRoot, { recursive: true, force: true });	} 	const verifiedServerSource = readFileSync(serverFile, "utf-8");	const verifiedPluginSource = readFileSync(pluginFile, "utf-8");	const verifiedAcpSource = readFileSync(acpFile, "utf-8");	if (		!verifiedServerSource.includes('from "node:http"') ||		verifiedServerSource.includes('from "hono/bun"') ||		!verifiedPluginSource.includes("Bun shell is unavailable in the Node ACP build") ||		!verifiedAcpSource.includes("const server = await Server.listen(opts)")	) {		throw new Error("Failed to stage the Node ACP patches into the prepared OpenCode source tree");	}} async function downloadFile(url, destination) {	const response = await fetch(url);	if (!response.ok) {		throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);	} 	const buffer = Buffer.from(await response.arrayBuffer());	await writeFile(destination, buffer);} async function readMigrations(sourceRoot) {	const migrationRoot = join(sourceRoot, "packages", "opencode", "migration");	const entries = (await readdir(migrationRoot, { withFileTypes: true }))		.filter((entry) => entry.isDirectory() && /^\d{14}/.test(entry.name))		.map((entry) => entry.name)		.sort(); 	return Promise.all(		entries.map(async (name) => {			const sql = await readFile(				join(migrationRoot, name, "migration.sql"),				"utf8",			);			const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name);			const timestamp = match				? Date.UTC(						Number(match[1]),						Number(match[2]) - 1,						Number(match[3]),						Number(match[4]),						Number(match[5]),						Number(match[6]),					)				: 0;			return { name, sql, timestamp };		}),	);} async function rewriteSourceFile(sourceRoot, relativePath, transform) {	const filePath = resolve(sourceRoot, relativePath);	const original = await readFile(filePath, "utf8");	const next = transform(original);	if (next !== original) {		await writeFile(filePath, next);	}} async function ensureSqlJsDependency(sourceRoot) {	const packageJsonPath = resolve(		sourceRoot,		"packages",		"opencode",		"package.json",	);	const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));	const dependencies = packageJson.dependencies ?? {};	const bunStoreDir = resolve(sourceRoot, "node_modules", ".bun");	const hasInstalledSqlJs =		existsSync(bunStoreDir) &&		(await readdir(bunStoreDir)).some((entry) => entry.startsWith("sql.js@"));	if (dependencies["sql.js"] === SQL_JS_VERSION && hasInstalledSqlJs) {		return;	} 	packageJson.dependencies = {		...dependencies,		"sql.js": SQL_JS_VERSION,	};	await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);	run(bunBin, ["install"], { cwd: sourceRoot });} async function applyNodeAcpRuntimeTweaks(sourceRoot) {	await writeFile(		resolve(sourceRoot, "packages/opencode/src/cli/cmd/acp.ts"),		`import type { Argv, InferredOptionTypes } from "yargs"import { cmd } from "./cmd"import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"import { Log } from "../../util/log" const options = {  port: {    type: "number" as const,    describe: "port to listen on",    default: 0,  },  hostname: {    type: "string" as const,    describe: "hostname to listen on",    default: "127.0.0.1",  },  mdns: {    type: "boolean" as const,    describe: "enable mDNS service discovery (defaults hostname to 0.0.0.0)",    default: false,  },  "mdns-domain": {    type: "string" as const,    describe: "custom domain name for mDNS service (default: opencode.local)",    default: "opencode.local",  },  cors: {    type: "string" as const,    array: true,    describe: "additional domains to allow for CORS",    default: [] as string[],  },} type NetworkOptions = InferredOptionTypes<typeof options> function withNetworkOptions<T>(yargs: Argv<T>) {  return yargs.options(options)} async function resolveNetworkOptions(args: NetworkOptions) {  const { Config } = await import("../../config/config")  const config = await Config.getGlobal()  const portExplicitlySet = process.argv.includes("--port")  const hostnameExplicitlySet = process.argv.includes("--hostname")  const mdnsExplicitlySet = process.argv.includes("--mdns")  const mdnsDomainExplicitlySet = process.argv.includes("--mdns-domain")  const corsExplicitlySet = process.argv.includes("--cors")   const mdns = mdnsExplicitlySet ? args.mdns : (config?.server?.mdns ?? args.mdns)  const mdnsDomain = mdnsDomainExplicitlySet ? args["mdns-domain"] : (config?.server?.mdnsDomain ?? args["mdns-domain"])  const port = portExplicitlySet ? args.port : (config?.server?.port ?? args.port)  const hostname = hostnameExplicitlySet    ? args.hostname    : mdns && !config?.server?.hostname      ? "0.0.0.0"      : (config?.server?.hostname ?? args.hostname)  const configCors = config?.server?.cors ?? []  const argsCors = Array.isArray(args.cors) ? args.cors : args.cors ? [args.cors] : []  const cors = [...configCors, ...argsCors]   return { hostname, port, mdns, mdnsDomain, cors }} function wrapData<T>(data: T) {  return { data }} async function loadProviderCatalog(directory: string | undefined) {  const [{ Config }] = await Promise.all([import("../../config/config")])   return withDirectory(directory, async () => {    const config = await Config.get()    const providers: any[] = []     if (config?.provider?.anthropic || config?.model?.startsWith("anthropic/") || process.env.ANTHROPIC_API_KEY) {      providers.push({        id: "anthropic",        name: "Anthropic",        source: "config",        env: ["ANTHROPIC_API_KEY"],        options: {          ...(config?.provider?.anthropic?.options ?? {}),        },        models: {          "claude-sonnet-4-20250514": {            id: "claude-sonnet-4-20250514",            providerID: "anthropic",            api: { id: "claude-sonnet-4-20250514", url: "", npm: "@ai-sdk/anthropic" },            name: "Claude Sonnet 4",            family: "claude-sonnet-4",            capabilities: {              temperature: true,              reasoning: true,              attachment: true,              toolcall: true,              input: { text: true, audio: false, image: true, video: false, pdf: true },              output: { text: true, audio: false, image: false, video: false, pdf: false },              interleaved: false,            },            cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },            limit: { context: 200000, output: 32000 },            status: "active",            options: {},            headers: {},            release_date: "2025-05-14",            variants: {},          },          "claude-opus-4-1-20250805": {            id: "claude-opus-4-1-20250805",            providerID: "anthropic",            api: { id: "claude-opus-4-1-20250805", url: "", npm: "@ai-sdk/anthropic" },            name: "Claude Opus 4.1",            family: "claude-opus-4-1",            capabilities: {              temperature: true,              reasoning: true,              attachment: true,              toolcall: true,              input: { text: true, audio: false, image: true, video: false, pdf: true },              output: { text: true, audio: false, image: false, video: false, pdf: false },              interleaved: false,            },            cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },            limit: { context: 200000, output: 32000 },            status: "active",            options: {},            headers: {},            release_date: "2025-08-05",            variants: {},          },          "claude-haiku-4-5-20251001": {            id: "claude-haiku-4-5-20251001",            providerID: "anthropic",            api: { id: "claude-haiku-4-5-20251001", url: "", npm: "@ai-sdk/anthropic" },            name: "Claude Haiku 4.5",            family: "claude-haiku-4-5",            capabilities: {              temperature: true,              reasoning: false,              attachment: true,              toolcall: true,              input: { text: true, audio: false, image: true, video: false, pdf: true },              output: { text: true, audio: false, image: false, video: false, pdf: false },              interleaved: false,            },            cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },            limit: { context: 200000, output: 16000 },            status: "active",            options: {},            headers: {},            release_date: "2025-10-01",            variants: {},          },        },      })    }     if (config?.provider?.openai || config?.model?.startsWith("openai/") || process.env.OPENAI_API_KEY) {      providers.push({        id: "openai",        name: "OpenAI",        source: "config",        env: ["OPENAI_API_KEY"],        options: {          ...(config?.provider?.openai?.options ?? {}),        },        models: {          "gpt-5": {            id: "gpt-5",            providerID: "openai",            api: { id: "gpt-5", url: "", npm: "@ai-sdk/openai" },            name: "GPT-5",            family: "gpt-5",            capabilities: {              temperature: true,              reasoning: true,              attachment: true,              toolcall: true,              input: { text: true, audio: false, image: true, video: false, pdf: true },              output: { text: true, audio: false, image: false, video: false, pdf: false },              interleaved: false,            },            cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },            limit: { context: 200000, output: 32000 },            status: "active",            options: {},            headers: {},            release_date: "2025-01-01",            variants: {},          },          "gpt-5-mini": {            id: "gpt-5-mini",            providerID: "openai",            api: { id: "gpt-5-mini", url: "", npm: "@ai-sdk/openai" },            name: "GPT-5 Mini",            family: "gpt-5-mini",            capabilities: {              temperature: true,              reasoning: true,              attachment: true,              toolcall: true,              input: { text: true, audio: false, image: true, video: false, pdf: true },              output: { text: true, audio: false, image: false, video: false, pdf: false },              interleaved: false,            },            cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },            limit: { context: 200000, output: 16000 },            status: "active",            options: {},            headers: {},            release_date: "2025-01-01",            variants: {},          },        },      })    }     if (      config?.provider?.google ||      config?.model?.startsWith("google/") ||      process.env.GOOGLE_GENERATIVE_AI_API_KEY    ) {      providers.push({        id: "google",        name: "Google",        source: "config",        env: ["GOOGLE_GENERATIVE_AI_API_KEY"],        options: {          ...(config?.provider?.google?.options ?? {}),        },        models: {          "gemini-2.5-pro": {            id: "gemini-2.5-pro",            providerID: "google",            api: { id: "gemini-2.5-pro", url: "", npm: "@ai-sdk/google" },            name: "Gemini 2.5 Pro",            family: "gemini-2.5-pro",            capabilities: {              temperature: true,              reasoning: true,              attachment: true,              toolcall: true,              input: { text: true, audio: false, image: true, video: false, pdf: true },              output: { text: true, audio: false, image: false, video: false, pdf: false },              interleaved: false,            },            cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },            limit: { context: 200000, output: 32000 },            status: "active",            options: {},            headers: {},            release_date: "2025-01-01",            variants: {},          },          "gemini-2.5-flash": {            id: "gemini-2.5-flash",            providerID: "google",            api: { id: "gemini-2.5-flash", url: "", npm: "@ai-sdk/google" },            name: "Gemini 2.5 Flash",            family: "gemini-2.5-flash",            capabilities: {              temperature: true,              reasoning: true,              attachment: true,              toolcall: true,              input: { text: true, audio: false, image: true, video: false, pdf: true },              output: { text: true, audio: false, image: false, video: false, pdf: false },              interleaved: false,            },            cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },            limit: { context: 200000, output: 16000 },            status: "active",            options: {},            headers: {},            release_date: "2025-01-01",            variants: {},          },        },

Lines 1–500 of 3681.