chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,269 @@
#!/usr/bin/env bun
import { $ } from "bun"
import fs from "fs"
import path from "path"
import { fileURLToPath } from "url"
import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const dir = path.resolve(__dirname, "..")
process.chdir(dir)
await import("./generate.ts")
import { Script } from "@opencode-ai/script"
import pkg from "../package.json"
// Load migrations from migration directories
const migrationDirs = (
await fs.promises.readdir(path.join(dir, "migration"), {
withFileTypes: true,
})
)
.filter((entry) => entry.isDirectory() && /^\d{4}\d{2}\d{2}\d{2}\d{2}\d{2}/.test(entry.name))
.map((entry) => entry.name)
.sort()
const migrations = await Promise.all(
migrationDirs.map(async (name) => {
const file = path.join(dir, "migration", name, "migration.sql")
const sql = await Bun.file(file).text()
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 { sql, timestamp, name }
}),
)
console.log(`Loaded ${migrations.length} migrations`)
const singleFlag = process.argv.includes("--single")
const baselineFlag = process.argv.includes("--baseline")
const skipInstall = process.argv.includes("--skip-install")
const plugin = createSolidTransformPlugin()
const skipEmbedWebUi = process.argv.includes("--skip-embed-web-ui")
const createEmbeddedWebUIBundle = async () => {
console.log(`Building Web UI to embed in the binary`)
const appDir = path.join(import.meta.dirname, "../../app")
const dist = path.join(appDir, "dist")
await $`bun run --cwd ${appDir} build`
const files = (await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: dist })))
.map((file) => file.replaceAll("\\", "/"))
.sort()
const imports = files.map((file, i) => {
const spec = path.relative(dir, path.join(dist, file)).replaceAll("\\", "/")
return `import file_${i} from ${JSON.stringify(spec.startsWith(".") ? spec : `./${spec}`)} with { type: "file" };`
})
const entries = files.map((file, i) => ` ${JSON.stringify(file)}: file_${i},`)
return [
`// Import all files as file_$i with type: "file"`,
...imports,
`// Export with original mappings`,
`export default {`,
...entries,
`}`,
].join("\n")
}
const embeddedFileMap = skipEmbedWebUi ? null : await createEmbeddedWebUIBundle()
const allTargets: {
os: string
arch: "arm64" | "x64"
abi?: "musl"
avx2?: false
}[] = [
{
os: "linux",
arch: "arm64",
},
{
os: "linux",
arch: "x64",
},
{
os: "linux",
arch: "x64",
avx2: false,
},
{
os: "linux",
arch: "arm64",
abi: "musl",
},
{
os: "linux",
arch: "x64",
abi: "musl",
},
{
os: "linux",
arch: "x64",
abi: "musl",
avx2: false,
},
{
os: "darwin",
arch: "arm64",
},
{
os: "darwin",
arch: "x64",
},
{
os: "darwin",
arch: "x64",
avx2: false,
},
{
os: "win32",
arch: "arm64",
},
{
os: "win32",
arch: "x64",
},
{
os: "win32",
arch: "x64",
avx2: false,
},
]
const targets = singleFlag
? allTargets.filter((item) => {
if (item.os !== process.platform || item.arch !== process.arch) {
return false
}
// When building for the current platform, prefer a single native binary by default.
// Baseline binaries require additional Bun artifacts and can be flaky to download.
if (item.avx2 === false) {
return baselineFlag
}
// also skip abi-specific builds for the same reason
if (item.abi !== undefined) {
return false
}
return true
})
: allTargets
await $`rm -rf dist`
const binaries: Record<string, string> = {}
if (!skipInstall) {
await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}`
}
for (const item of targets) {
const name = [
pkg.name,
// changing to win32 flags npm for some reason
item.os === "win32" ? "windows" : item.os,
item.arch,
item.avx2 === false ? "baseline" : undefined,
item.abi === undefined ? undefined : item.abi,
]
.filter(Boolean)
.join("-")
console.log(`building ${name}`)
await $`mkdir -p dist/${name}/bin`
const localPath = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js")
const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath)
const workerPath = "./src/cli/cmd/tui/worker.ts"
// Use platform-specific bunfs root path based on target OS
const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/"
const workerRelativePath = path.relative(dir, parserWorker).replaceAll("\\", "/")
await Bun.build({
conditions: ["browser"],
tsconfig: "./tsconfig.json",
plugins: [plugin],
external: ["node-gyp"],
format: "esm",
minify: true,
splitting: true,
compile: {
autoloadBunfig: false,
autoloadDotenv: false,
autoloadTsconfig: true,
autoloadPackageJson: true,
target: name.replace(pkg.name, "bun") as any,
outfile: `dist/${name}/bin/opencode`,
execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"],
windows: {},
},
files: embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {},
entrypoints: ["./src/index.ts", parserWorker, workerPath, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])],
define: {
OPENCODE_VERSION: `'${pkg.version}'`,
OPENCODE_MIGRATIONS: JSON.stringify(migrations),
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
OPENCODE_WORKER_PATH: workerPath,
OPENCODE_CHANNEL: `'${Script.channel}'`,
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
},
})
// Smoke test: only run if binary is for current platform
if (item.os === process.platform && item.arch === process.arch && !item.abi) {
const binaryPath = `dist/${name}/bin/opencode`
console.log(`Running smoke test: ${binaryPath} --version`)
try {
const versionOutput = await $`${binaryPath} --version`.text()
console.log(`Smoke test passed: ${versionOutput.trim()}`)
} catch (e) {
console.error(`Smoke test failed for ${name}:`, e)
process.exit(1)
}
}
await $`rm -rf ./dist/${name}/bin/tui`
// Copy bundled models.json next to binary for runtime access
await $`mkdir -p dist/${name}/bin/assets`
await $`cp assets/models.json dist/${name}/bin/assets/models.json`
await Bun.file(`dist/${name}/package.json`).write(
JSON.stringify(
{
name,
version: pkg.version,
os: [item.os],
cpu: [item.arch],
},
null,
2,
),
)
binaries[name] = Script.version
}
if (Script.release) {
for (const key of Object.keys(binaries)) {
if (key.includes("linux")) {
await $`tar -czf ../../${key}.tar.gz *`.cwd(`dist/${key}/bin`)
} else {
await $`zip -r ../../${key}.zip *`.cwd(`dist/${key}/bin`)
}
}
await $`gh release upload v${Script.version} ./dist/*.zip ./dist/*.tar.gz --clobber --repo ${process.env.GH_REPO}`
}
export { binaries }

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bun
import { $ } from "bun"
// drizzle-kit check compares schema to migrations, exits non-zero if drift
const result = await $`bun drizzle-kit check`.quiet().nothrow()
if (result.exitCode !== 0) {
console.error("Schema has changes not captured in migrations!")
console.error("Run: bun drizzle-kit generate")
console.error("")
console.error(result.stderr.toString())
process.exit(1)
}
console.log("Migrations are up to date")

View File

@@ -0,0 +1,165 @@
#!/usr/bin/env bun
/**
* 版本一致性校验脚本(发布前 + 发布后)。
*
* 设计目标:
* 1) 统一“版本真相来源”为 packages/opencode/package.json 的 version
* 2) 发布前阻断所有“本地版本不一致”;
* 3) 发布后阻断所有“registry/安装态版本不一致”。
*
* 用法:
* bun run script/check-version-consistency.ts --phase pre --version 1.1.93
* bun run script/check-version-consistency.ts --phase post --version 1.1.93
*/
import { $ } from "bun"
import pkg from "../package.json"
type Phase = "pre" | "post"
function parseArg(flag: string): string | undefined {
const index = process.argv.indexOf(flag)
if (index === -1) return undefined
return process.argv[index + 1]
}
const phase = (parseArg("--phase") ?? "pre") as Phase
const targetVersion = parseArg("--version") ?? pkg.version
if (!["pre", "post"].includes(phase)) {
console.error(`不支持的 --phase: ${phase},可选值仅为 pre | post`)
process.exit(1)
}
async function ensureOptionalDependenciesConsistent(version: string) {
// package.json schema 推断类型里未必包含 optionalDependencies运行时在 release.sh 中会写入同步。
const optional =
(pkg as typeof pkg & { optionalDependencies?: Record<string, string> }).optionalDependencies ?? {}
const entries = Object.entries(optional).filter(([name]) => name.startsWith("qimingcode-"))
if (entries.length === 0) {
// 当前仓库里 optionalDependencies 由 publish.ts 在 dist 聚合包里动态生成。
// 因此源码 package.json 允许没有该字段,不应阻断发布。
console.warn("package.json 未定义 qimingcode-* optionalDependencies跳过源码 optional 一致性校验。")
return
}
for (const [name, depVersion] of entries) {
if (depVersion !== version) {
console.error(
`optionalDependencies 版本不一致:${name}=${depVersion},期望 ${version}` +
"请先同步 package.json 后再发布。",
)
process.exit(1)
}
}
}
async function ensureDistBinariesConsistent(version: string) {
const distPaths = [...new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" })]
if (distPaths.length === 0) {
console.error("dist 目录中未找到任何二进制包 package.json。请先执行构建。")
process.exit(1)
}
for (const rel of distPaths) {
const file = `./dist/${rel}`
const distPkg = (await Bun.file(file).json()) as { name: string; version: string }
if (!distPkg.name.startsWith("qimingcode-")) continue
if (distPkg.version !== version) {
console.error(
`dist 二进制包版本不一致:${distPkg.name}=${distPkg.version},期望 ${version}` +
"请重建二进制后再发布。",
)
process.exit(1)
}
}
}
async function ensureLocalSmokeVersion(version: string) {
// 选择当前机器最常见的本地产物做烟测mac arm64不存在则跳过不阻断非本机平台构建场景。
const smokeBin = "./dist/qimingcode-darwin-arm64/bin/opencode"
if (!(await Bun.file(smokeBin).exists())) {
console.warn(`未找到本地 smoke 二进制 ${smokeBin},跳过本地版本烟测。`)
return
}
const out = await $`${smokeBin} --version`.quiet().nothrow()
if (out.exitCode !== 0) {
console.error(`本地 smoke 二进制执行失败exit=${out.exitCode})。`)
process.exit(1)
}
const actual = out.stdout.toString().trim()
if (actual !== version) {
console.error(`本地二进制版本不一致smoke=${actual},期望 ${version}`)
process.exit(1)
}
}
async function ensureRegistryMainVersion(version: string) {
const check = await $`npm view qimingcode@${version} version --registry=https://registry.npmjs.org/`
.quiet()
.nothrow()
if (check.exitCode !== 0) {
console.error(`registry 中未查到 qimingcode@${version}`)
process.exit(1)
}
const actual = check.stdout.toString().trim()
if (actual !== version) {
console.error(`registry 主包版本不一致:${actual},期望 ${version}`)
process.exit(1)
}
}
async function ensureRegistryOptionalsComplete(version: string) {
const result =
await $`bun run script/verify-registry-complete.ts ${version}`.cwd(process.cwd()).quiet().nothrow()
if (result.exitCode !== 0) {
console.error("registry optionalDependencies 完整性校验失败。")
if (result.stderr.toString().trim()) console.error(result.stderr.toString())
process.exit(1)
}
}
async function ensureInstalledCliVersion(version: string) {
const cmd = [
"tmpdir=$(mktemp -d)",
"cd \"$tmpdir\"",
"npm init -y >/dev/null",
"npm i \"qimingcode@" + version + "\" --registry=https://registry.npmjs.org/ >/dev/null 2>&1",
"./node_modules/.bin/qimingcode -v",
].join(" && ")
const run = await $`bash -lc ${cmd}`.quiet().nothrow()
if (run.exitCode !== 0) {
console.error("安装态 CLI 版本校验执行失败。")
process.exit(1)
}
const actual = run.stdout.toString().trim()
if (actual !== version) {
console.error(`安装态 CLI 版本不一致:${actual},期望 ${version}`)
process.exit(1)
}
}
async function runPre(version: string) {
if (pkg.version !== version) {
console.error(`package.json 版本不一致:${pkg.version},期望 ${version}`)
process.exit(1)
}
await ensureOptionalDependenciesConsistent(version)
await ensureDistBinariesConsistent(version)
await ensureLocalSmokeVersion(version)
console.log(`pre 校验通过:本地主包/optional/dist/烟测版本均为 ${version}`)
}
async function runPost(version: string) {
await ensureRegistryMainVersion(version)
await ensureRegistryOptionalsComplete(version)
await ensureInstalledCliVersion(version)
console.log(`post 校验通过registry 与安装态 CLI 版本均为 ${version}`)
}
if (phase === "pre") {
await runPre(targetVersion)
} else {
await runPost(targetVersion)
}

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bun
import fs from "fs/promises"
import path from "path"
import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const dir = path.resolve(__dirname, "..")
if (process.platform !== "win32") {
const root = path.join(dir, "node_modules", "node-pty", "prebuilds")
const dirs = await fs.readdir(root, { withFileTypes: true }).catch(() => [])
const files = dirs.filter((x) => x.isDirectory()).map((x) => path.join(root, x.name, "spawn-helper"))
const result = await Promise.all(
files.map(async (file) => {
const stat = await fs.stat(file).catch(() => undefined)
if (!stat) return
if ((stat.mode & 0o111) === 0o111) return
await fs.chmod(file, stat.mode | 0o755)
return file
}),
)
const fixed = result.filter(Boolean)
if (fixed.length) {
console.log(`fixed node-pty permissions for ${fixed.length} helper${fixed.length === 1 ? "" : "s"}`)
}
}

View File

@@ -0,0 +1,23 @@
import path from "path"
import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const dir = path.resolve(__dirname, "..")
process.chdir(dir)
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev"
// Fetch and generate models.dev snapshot
const modelsData = process.env.MODELS_DEV_API_JSON
? await Bun.file(process.env.MODELS_DEV_API_JSON).text()
: await fetch(`${modelsUrl}/api.json`).then((x) => x.text())
await Bun.write(
path.join(dir, "src/provider/models-snapshot.js"),
`// @ts-nocheck\n// Auto-generated by build.ts - do not edit\nexport const snapshot = ${modelsData}\n`,
)
await Bun.write(
path.join(dir, "src/provider/models-snapshot.d.ts"),
`// Auto-generated by build.ts - do not edit\nexport declare const snapshot: Record<string, unknown>\n`,
)
console.log("Generated models-snapshot.js")

View File

@@ -0,0 +1,115 @@
#!/usr/bin/env node
import fs from "fs"
import path from "path"
import os from "os"
import { fileURLToPath } from "url"
import { createRequire } from "module"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const require = createRequire(import.meta.url)
function detectPlatformAndArch() {
// Map platform names
let platform
switch (os.platform()) {
case "darwin":
platform = "darwin"
break
case "linux":
platform = "linux"
break
case "win32":
platform = "windows"
break
default:
platform = os.platform()
break
}
// Map architecture names
let arch
switch (os.arch()) {
case "x64":
arch = "x64"
break
case "arm64":
arch = "arm64"
break
case "arm":
arch = "arm"
break
default:
arch = os.arch()
break
}
return { platform, arch }
}
function findBinary() {
const { platform, arch } = detectPlatformAndArch()
const binaryName = platform === "windows" ? "opencode.exe" : "opencode"
// 包名历史上经历过从 opencode-* 到 qimingcode-* 的迁移。
// 为了同时兼容:
// 1) 新版本主包optionalDependencies 指向 qimingcode-*
// 2) 旧版本或缓存环境(仍可能只安装了 opencode-*
// 这里按“新优先、旧兜底”的顺序尝试解析平台包。
const packageCandidates = [`qimingcode-${platform}-${arch}`, `opencode-${platform}-${arch}`]
let lastError = null
for (const packageName of packageCandidates) {
try {
// Use require.resolve to find the package
const packageJsonPath = require.resolve(`${packageName}/package.json`)
const packageDir = path.dirname(packageJsonPath)
const binaryPath = path.join(packageDir, "bin", binaryName)
if (!fs.existsSync(binaryPath)) {
throw new Error(`Binary not found at ${binaryPath}`)
}
return { binaryPath, binaryName }
} catch (error) {
lastError = error
}
}
throw new Error(
`Could not find platform package for ${platform}-${arch}. Tried: ${packageCandidates.join(", ")}. Last error: ${lastError?.message}`,
{ cause: lastError },
)
}
async function main() {
try {
if (os.platform() === "win32") {
// On Windows, the .exe is already included in the package and bin field points to it
// No postinstall setup needed
console.log("Windows detected: binary setup not needed (using packaged .exe)")
return
}
// On non-Windows platforms, just verify the binary package exists
// Don't replace the wrapper script - it handles binary execution
const { binaryPath } = findBinary()
const target = path.join(__dirname, "bin", ".opencode")
if (fs.existsSync(target)) fs.unlinkSync(target)
try {
fs.linkSync(binaryPath, target)
} catch {
fs.copyFileSync(binaryPath, target)
}
fs.chmodSync(target, 0o755)
} catch (error) {
console.error("Failed to setup opencode binary:", error.message)
process.exit(1)
}
}
try {
void main()
} catch (error) {
console.error("Postinstall script error:", error.message)
process.exit(0)
}

View File

@@ -0,0 +1,229 @@
#!/usr/bin/env bun
import { $ } from "bun"
import pkg from "../package.json"
import { Script } from "@opencode-ai/script"
import { fileURLToPath } from "url"
const dir = fileURLToPath(new URL("..", import.meta.url))
process.chdir(dir)
// 单一版本源规则:发布脚本上下文版本必须与 package.json 完全一致。
// 这样可以防止环境变量或分支上下文误导导致“主包/二进制版本错位发布”。
if (pkg.version !== Script.version) {
console.error(
`Version mismatch: package.json=${pkg.version}, publish-context=${Script.version}. ` +
"Please sync package.json version before publish.",
)
process.exit(1)
}
async function published(name: string, version: string) {
return (await $`npm view ${name}@${version} version`.nothrow()).exitCode === 0
}
async function publish(dir: string, name: string, version: string) {
// GitHub artifact downloads can drop the executable bit, and Docker uses the
// unpacked dist binaries directly rather than the published tarball.
if (process.platform !== "win32") await $`chmod -R 755 .`.cwd(dir)
if (await published(name, version)) {
console.log(`already published ${name}@${version}`)
return
}
// 避免目录中历史打包产物干扰本次发布npm publish *.tgz 期望单一目标包)。
// Bun shell 对未匹配的 glob 会报错,这里改为通过 bash 执行,确保“无 tgz 可删”也不失败。
await $`bash -lc "rm -f ./*.tgz"`.cwd(dir)
await $`bun pm pack`.cwd(dir)
await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir)
}
const binaries: Record<string, string> = {}
for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" })) {
const packageJsonPath = `./dist/${filepath}`
const distPkg = await Bun.file(packageJsonPath).json()
// 统一使用本次发布版本,确保所有平台二进制子包与主包版本号一致。
// 这允许我们在“主包修复补丁”场景下,显式推进整套 npm 包版本(例如 1.1.85 全量对齐)。
if (distPkg.version !== Script.version) {
distPkg.version = Script.version
await Bun.file(packageJsonPath).write(JSON.stringify(distPkg, null, 2) + "\n")
}
binaries[distPkg.name] = Script.version
}
console.log("binaries", binaries)
// 主包版本应由发布上下文Script.version决定而不是被 dist 二进制版本反向驱动。
// 这样当仅修复 JS/postinstall 逻辑时,可以发布新的主包版本并继续复用已发布的二进制包版本。
const version = Script.version
await $`mkdir -p ./dist/${pkg.name}`
await $`cp -r ./bin ./dist/${pkg.name}/bin`
// 始终提供实体入口 bin/qimingcode避免依赖软链或不存在的文件映射。
await $`cp ./dist/${pkg.name}/bin/opencode ./dist/${pkg.name}/bin/${pkg.name}`
await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs`
await Bun.file(`./dist/${pkg.name}/LICENSE`).write(await Bun.file("../../LICENSE").text())
await Bun.file(`./dist/${pkg.name}/package.json`).write(
JSON.stringify(
{
// 发布主聚合包qimingcode保持与历史发版行为一致。
// optionalDependencies 继续引用各平台二进制子包qimingcode-*)。
name: pkg.name,
bin: {
[pkg.name]: `./bin/${pkg.name}`,
},
scripts: {
postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs",
},
version: version,
license: pkg.license,
optionalDependencies: binaries,
},
null,
2,
),
)
const tasks = Object.entries(binaries).map(async ([name]) => {
await publish(`./dist/${name}`, name, binaries[name])
})
await Promise.all(tasks)
await publish(`./dist/${pkg.name}`, `${pkg.name}`, version)
const image = "ghcr.io/anomalyco/opencode"
const platforms = "linux/amd64,linux/arm64"
const tags = [`${image}:${version}`, `${image}:${Script.channel}`]
const tagFlags = tags.flatMap((t) => ["-t", t])
// 本地/紧急发版场景下,允许只执行 npm 发布,跳过 docker/aur/homebrew 这类
// 依赖外部守护进程或凭据的后续步骤,避免因为环境问题阻塞 npm 交付。
const skipDockerPublish = process.env.SKIP_DOCKER_PUBLISH === "1"
// registries
if (!Script.preview) {
if (skipDockerPublish) {
console.log("SKIP_DOCKER_PUBLISH=1跳过 docker 与后续镜像/仓库发布步骤。")
process.exit(0)
}
await $`docker buildx build --platform ${platforms} ${tagFlags} --push .`
// Calculate SHA values
const arm64Sha = await $`sha256sum ./dist/opencode-linux-arm64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim())
const x64Sha = await $`sha256sum ./dist/opencode-linux-x64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim())
const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const [pkgver, _subver = ""] = Script.version.split(/(-.*)/, 2)
// arch
const binaryPkgbuild = [
"# Maintainer: dax",
"# Maintainer: adam",
"",
"pkgname='opencode-bin'",
`pkgver=${pkgver}`,
`_subver=${_subver}`,
"options=('!debug' '!strip')",
"pkgrel=1",
"pkgdesc='The AI coding agent built for the terminal.'",
"url='https://github.com/anomalyco/opencode'",
"arch=('aarch64' 'x86_64')",
"license=('MIT')",
"provides=('opencode')",
"conflicts=('opencode')",
"depends=('ripgrep')",
"",
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.tar.gz::https://github.com/anomalyco/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-arm64.tar.gz")`,
`sha256sums_aarch64=('${arm64Sha}')`,
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.tar.gz::https://github.com/anomalyco/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-x64.tar.gz")`,
`sha256sums_x86_64=('${x64Sha}')`,
"",
"package() {",
' install -Dm755 ./opencode "${pkgdir}/usr/bin/opencode"',
"}",
"",
].join("\n")
for (const [pkg, pkgbuild] of [["opencode-bin", binaryPkgbuild]]) {
for (let i = 0; i < 30; i++) {
try {
await $`rm -rf ./dist/aur-${pkg}`
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
await $`cd ./dist/aur-${pkg} && git checkout master`
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild)
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
if ((await $`cd ./dist/aur-${pkg} && git diff --cached --quiet`.nothrow()).exitCode === 0) break
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${Script.version}"`
await $`cd ./dist/aur-${pkg} && git push`
break
} catch {
continue
}
}
}
// Homebrew formula
const homebrewFormula = [
"# typed: false",
"# frozen_string_literal: true",
"",
"# This file was generated by GoReleaser. DO NOT EDIT.",
"class Opencode < Formula",
` desc "The AI coding agent built for the terminal."`,
` homepage "https://github.com/anomalyco/opencode"`,
` version "${Script.version.split("-")[0]}"`,
"",
` depends_on "ripgrep"`,
"",
" on_macos do",
" if Hardware::CPU.intel?",
` url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-darwin-x64.zip"`,
` sha256 "${macX64Sha}"`,
"",
" def install",
' bin.install "opencode"',
" end",
" end",
" if Hardware::CPU.arm?",
` url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-darwin-arm64.zip"`,
` sha256 "${macArm64Sha}"`,
"",
" def install",
' bin.install "opencode"',
" end",
" end",
" end",
"",
" on_linux do",
" if Hardware::CPU.intel? and Hardware::CPU.is_64_bit?",
` url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-linux-x64.tar.gz"`,
` sha256 "${x64Sha}"`,
" def install",
' bin.install "opencode"',
" end",
" end",
" if Hardware::CPU.arm? and Hardware::CPU.is_64_bit?",
` url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-linux-arm64.tar.gz"`,
` sha256 "${arm64Sha}"`,
" def install",
' bin.install "opencode"',
" end",
" end",
" end",
"end",
"",
"",
].join("\n")
const token = process.env.GITHUB_TOKEN
if (!token) {
console.error("GITHUB_TOKEN is required to update homebrew tap")
process.exit(1)
}
const tap = `https://x-access-token:${token}@github.com/anomalyco/homebrew-tap.git`
await $`rm -rf ./dist/homebrew-tap`
await $`git clone ${tap} ./dist/homebrew-tap`
await Bun.file("./dist/homebrew-tap/opencode.rb").write(homebrewFormula)
await $`cd ./dist/homebrew-tap && git add opencode.rb`
if ((await $`cd ./dist/homebrew-tap && git diff --cached --quiet`.nothrow()).exitCode !== 0) {
await $`cd ./dist/homebrew-tap && git commit -m "Update to v${Script.version}"`
await $`cd ./dist/homebrew-tap && git push`
}
}

View File

@@ -0,0 +1,106 @@
#!/usr/bin/env bun
// This script runs a separate OpenCode server to be used as a remote
// workspace, simulating a remote environment but all local to make
// debugger easier
//
// *Important*: make sure you add the debug workspace plugin first.
// In `.opencode/opencode.jsonc` in the root of this project add:
//
// "plugin": ["../packages/opencode/src/control-plane/dev/debug-workspace-plugin.ts"]
//
// Afterwards, run `./packages/opencode/script/run-workspace-server`
import { stat } from "node:fs/promises"
import { setTimeout as sleep } from "node:timers/promises"
const DEV_DATA_FILE = "/tmp/opencode-workspace-dev-data.json"
const RESTART_POLL_INTERVAL = 250
async function readData() {
return await Bun.file(DEV_DATA_FILE).json()
}
async function readDataMtime() {
return await stat(DEV_DATA_FILE)
.then((info) => info.mtimeMs)
.catch((error) => {
if (typeof error === "object" && error && "code" in error && error.code === "ENOENT") {
return undefined
}
throw error
})
}
async function readSnapshot() {
while (true) {
try {
const before = await readDataMtime()
if (before === undefined) {
await sleep(RESTART_POLL_INTERVAL)
continue
}
const data = await readData()
const after = await readDataMtime()
if (before === after) {
return { data, mtime: after }
}
} catch (error) {
if (typeof error === "object" && error && "code" in error && error.code === "ENOENT") {
await sleep(RESTART_POLL_INTERVAL)
continue
}
throw error
}
}
}
function startDevServer(data: any) {
const env = Object.fromEntries(Object.entries(data.env ?? {}).filter(([, value]) => value !== undefined))
return Bun.spawn(["bun", "run", "dev", "serve", "--port", String(data.port), "--print-logs"], {
env: {
...process.env,
...env,
XDG_DATA_HOME: "/tmp/data",
},
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
})
}
async function waitForRestartSignal(mtime: number, signal: AbortSignal) {
while (!signal.aborted) {
await sleep(RESTART_POLL_INTERVAL)
if (signal.aborted) return false
if ((await readDataMtime()) !== mtime) return true
}
return false
}
while (true) {
const { data, mtime } = await readSnapshot()
const proc = startDevServer(data)
const restartAbort = new AbortController()
const result = await Promise.race([
proc.exited.then((code) => ({ type: "exit" as const, code })),
waitForRestartSignal(mtime, restartAbort.signal).then((restart) => ({ type: "restart" as const, restart })),
])
restartAbort.abort()
if (result.type === "restart" && result.restart) {
proc.kill()
await proc.exited
continue
}
process.exit(result.code)
}

View File

@@ -0,0 +1,63 @@
#!/usr/bin/env bun
import { z } from "zod"
import { Config } from "../src/config"
import { TuiConfig } from "../src/cli/cmd/tui/config/tui"
function generate(schema: z.ZodType) {
const result = z.toJSONSchema(schema, {
io: "input", // Generate input shape (treats optional().default() as not required)
/**
* We'll use the `default` values of the field as the only value in `examples`.
* This will ensure no docs are needed to be read, as the configuration is
* self-documenting.
*
* See https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.9.5
*/
override(ctx) {
const schema = ctx.jsonSchema
// Preserve strictness: set additionalProperties: false for objects
if (
schema &&
typeof schema === "object" &&
schema.type === "object" &&
schema.additionalProperties === undefined
) {
schema.additionalProperties = false
}
// Add examples and default descriptions for string fields with defaults
if (schema && typeof schema === "object" && "type" in schema && schema.type === "string" && schema?.default) {
if (!schema.examples) {
schema.examples = [schema.default]
}
schema.description = [schema.description || "", `default: \`${String(schema.default)}\``]
.filter(Boolean)
.join("\n\n")
.trim()
}
},
}) as Record<string, unknown> & {
allowComments?: boolean
allowTrailingCommas?: boolean
}
// used for json lsps since config supports jsonc
result.allowComments = true
result.allowTrailingCommas = true
return result
}
const configFile = process.argv[2]
const tuiFile = process.argv[3]
console.log(configFile)
await Bun.write(configFile, JSON.stringify(generate(Config.Info.zod), null, 2))
if (tuiFile) {
console.log(tuiFile)
await Bun.write(tuiFile, JSON.stringify(generate(TuiConfig.Info), null, 2))
}

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bun
import path from "path"
const toDynamicallyImport = path.join(process.cwd(), process.argv[2])
await import(toDynamicallyImport)
console.log(performance.now())

View File

@@ -0,0 +1,153 @@
#!/usr/bin/env bun
import * as path from "path"
import * as ts from "typescript"
const BASE_DIR = "/home/thdxr/dev/projects/anomalyco/opencode/packages/opencode"
// Get entry file from command line arg or use default
const ENTRY_FILE = process.argv[2] || "src/cli/cmd/tui/plugin/index.ts"
const visited = new Set<string>()
function resolveImport(importPath: string, fromFile: string): string | null {
if (importPath.startsWith("@/")) {
return path.join(BASE_DIR, "src", importPath.slice(2))
}
if (importPath.startsWith("./") || importPath.startsWith("../")) {
const dir = path.dirname(fromFile)
return path.resolve(dir, importPath)
}
return null
}
function isInternalImport(importPath: string): boolean {
return importPath.startsWith("@/") || importPath.startsWith("./") || importPath.startsWith("../")
}
async function tryExtensions(filePath: string): Promise<string | null> {
const extensions = [".ts", ".tsx", ".js", ".jsx"]
try {
const file = Bun.file(filePath)
const stat = await file.stat()
if (stat?.isDirectory()) {
for (const ext of extensions) {
const indexPath = path.join(filePath, "index" + ext)
const indexFile = Bun.file(indexPath)
if (await indexFile.exists()) return indexPath
}
return null
}
// It's a file
return filePath
} catch {
// Path doesn't exist, try adding extensions
for (const ext of extensions) {
const withExt = filePath + ext
const extFile = Bun.file(withExt)
if (await extFile.exists()) return withExt
}
return null
}
}
function extractImports(sourceFile: ts.SourceFile): string[] {
const imports: string[] = []
function visit(node: ts.Node) {
// import x from "path" or import { x } from "path"
if (ts.isImportDeclaration(node)) {
// Skip type-only imports
if (node.importClause?.isTypeOnly) return
const moduleSpec = node.moduleSpecifier
if (ts.isStringLiteral(moduleSpec)) {
imports.push(moduleSpec.text)
}
}
// export { x } from "path"
if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
if (ts.isStringLiteral(node.moduleSpecifier)) {
imports.push(node.moduleSpecifier.text)
}
}
// Dynamic import: import("path")
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
const arg = node.arguments[0]
if (arg && ts.isStringLiteral(arg)) {
imports.push(arg.text)
}
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
return imports
}
async function traceFile(filePath: string, depth = 0): Promise<void> {
const normalizedPath = path.relative(BASE_DIR, filePath)
if (visited.has(filePath)) {
return
}
// Only trace TypeScript/JavaScript files
if (!filePath.match(/\.(ts|tsx|js|jsx)$/)) {
return
}
visited.add(filePath)
console.log("\t".repeat(depth) + normalizedPath)
let content: string
try {
content = await Bun.file(filePath).text()
} catch {
return
}
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true)
const imports = extractImports(sourceFile)
const internalImports = imports.filter(isInternalImport)
const externalImports = imports.filter((imp) => !isInternalImport(imp))
// Print external imports
for (const imp of externalImports) {
console.log("\t".repeat(depth + 1) + `[ext] ${imp}`)
}
for (const imp of internalImports) {
const resolved = resolveImport(imp, filePath)
if (!resolved) continue
const actualPath = await tryExtensions(resolved)
if (!actualPath) continue
await traceFile(actualPath, depth + 1)
}
}
async function main() {
const entryPath = path.join(BASE_DIR, ENTRY_FILE)
// Check if file exists
const file = Bun.file(entryPath)
if (!(await file.exists())) {
console.error(`File not found: ${ENTRY_FILE}`)
console.error(`Resolved to: ${entryPath}`)
process.exit(1)
}
await traceFile(entryPath)
}
main().catch(console.error)

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env bun
import path from "node:path"
const raw = process.argv[2]
if (!raw) {
console.error("Usage: bun run script/upgrade-opentui.ts <version>")
process.exit(1)
}
const ver = raw.replace(/^v/, "")
const root = path.resolve(import.meta.dir, "../../..")
const skip = new Set([".git", ".opencode", ".turbo", "dist", "node_modules"])
const keys = ["@opentui/core", "@opentui/solid"] as const
const files = (await Array.fromAsync(new Bun.Glob("**/package.json").scan({ cwd: root }))).filter(
(file) => !file.split("/").some((part) => skip.has(part)),
)
const set = (cur: string) => {
if (cur.startsWith(">=")) return `>=${ver}`
if (cur.startsWith("^")) return `^${ver}`
if (cur.startsWith("~")) return `~${ver}`
return ver
}
const edit = (obj: unknown) => {
if (!obj || typeof obj !== "object") return false
const map = obj as Record<string, unknown>
return keys
.map((key) => {
const cur = map[key]
if (typeof cur !== "string") return false
const next = set(cur)
if (next === cur) return false
map[key] = next
return true
})
.some(Boolean)
}
const out = (
await Promise.all(
files.map(async (rel) => {
const file = path.join(root, rel)
const txt = await Bun.file(file).text()
const json = JSON.parse(txt)
const hit = [json.dependencies, json.devDependencies, json.peerDependencies].map(edit).some(Boolean)
if (!hit) return null
await Bun.write(file, `${JSON.stringify(json, null, 2)}\n`)
return rel
}),
)
).filter((item): item is string => item !== null)
if (out.length === 0) {
console.log("No opentui deps found")
process.exit(0)
}
console.log(`Updated opentui to ${ver} in:`)
for (const file of out) {
console.log(`- ${file}`)
}

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env bun
/**
* 校验 npm 上已发布的 `qimingcode@<version>`:其 optionalDependencies 里的每个平台子包
* 在 registry 上是否都能解析到同版本。
*
* 用于:
* - 发版流程末尾确认本次发布完整(避免再次出现主包有、子包 404
* - 排查用户 `npm i -g qimingcode` 报找不到 `qimingcode-linux-x64/package.json`。
*
* 用法(在 packages/opencode 目录下,或通过 bun 指定路径):
* bun run script/verify-registry-complete.ts
* bun run script/verify-registry-complete.ts 1.1.75
*/
import { $ } from "bun"
import pkg from "../package.json"
const version = process.argv[2] ?? pkg.version
const mainSpec = `qimingcode@${version}`
const metaResult =
await $`npm view ${mainSpec} optionalDependencies --json`.quiet().nothrow()
if (metaResult.exitCode !== 0) {
console.error(
`无法从 registry 读取 qimingcode@${version}exit ${metaResult.exitCode})。` +
`若尚未发布该版本的主包,请先发布或传入已存在的版本号。`,
)
if (metaResult.stderr.toString().trim()) {
console.error(metaResult.stderr.toString())
}
process.exit(1)
}
const raw = metaResult.stdout.toString().trim()
let optional: Record<string, string>
try {
optional = JSON.parse(raw) as Record<string, string>
} catch {
console.error("npm 返回的 optionalDependencies 不是合法 JSON:", raw.slice(0, 200))
process.exit(1)
}
if (!optional || typeof optional !== "object" || Object.keys(optional).length === 0) {
console.error(`qimingcode@${version} 没有 optionalDependencies 或字段为空`)
process.exit(1)
}
const missing: string[] = []
for (const [name, ver] of Object.entries(optional)) {
const v = String(ver)
const subSpec = `${name}@${v}`
const check = await $`npm view ${subSpec} version`.quiet().nothrow()
if (check.exitCode !== 0) {
missing.push(`${name}@${v}`)
continue
}
const published = check.stdout.toString().trim()
if (!published) {
missing.push(`${name}@${v}`)
}
}
if (missing.length > 0) {
console.error(`qimingcode@${version} 以下 optional 子包在 registry 上缺失或不可安装:`)
for (const m of missing) {
console.error(` - ${m}`)
}
process.exit(1)
}
console.log(
`校验通过qimingcode@${version}${Object.keys(optional).length} 个 optional 子包均在 registry 上存在。`,
)

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env bun
/**
* Local smoke test for qimingcode 1.2.0 native sandbox (no Electron).
* Usage: bun run script/verify-sandbox.ts
*/
import path from "path"
import fs from "fs"
import { Config, ConfigParse } from "../src/config"
import { resolveWritableRoots, isPathWritable } from "../src/sandbox/path"
const fail = (msg: string) => {
console.error(`FAIL: ${msg}`)
process.exit(1)
}
const ok = (msg: string) => console.log(`OK: ${msg}`)
// 1) Strict config schema
const parsed = ConfigParse.schema(
Config.Info.zod,
{
sandbox: {
sandbox_mode: "strict",
helper_path: "C:\\helper\\qiming-sandbox-helper.exe",
mode: "workspace-write",
network_enabled: true,
},
},
"verify.json",
)
if (parsed.sandbox?.sandbox_mode !== "strict") fail("sandbox_mode not strict")
ok("Config.Info accepts OPENCODE sandbox object")
try {
ConfigParse.schema(
Config.Info.zod,
{ sandbox: { sandbox_mode: "strict" }, extra_key: true } as Record<string, unknown>,
"bad.json",
)
fail("expected unknown top-level key to throw")
} catch {
ok("unknown top-level keys rejected")
}
// 2) Writable roots (strict = session only)
const session = path.resolve(process.cwd(), "_verify_session")
const sibling = path.resolve(process.cwd(), "_verify_sibling", "out.txt")
fs.mkdirSync(session, { recursive: true })
fs.mkdirSync(path.dirname(sibling), { recursive: true })
const roots = resolveWritableRoots({ sandbox_mode: "strict" }, session)
if (!isPathWritable(path.join(session, "inside.txt"), roots)) {
fail("write inside session should be allowed")
}
if (isPathWritable(sibling, roots)) {
fail("write outside session should be blocked")
}
ok("strict writable_roots: session allowed, sibling denied")
// 3) Built binary version (if dist exists)
const distExe = path.resolve(
import.meta.dir,
"../dist/qimingcode-windows-x64/bin/opencode.exe",
)
if (fs.existsSync(distExe)) {
const proc = Bun.spawn([distExe, "--version"], { stdout: "pipe", stderr: "pipe" })
const version = (await new Response(proc.stdout).text()).trim()
if (!version.startsWith("1.2.")) {
fail(`dist binary version expected 1.2.x, got ${version}`)
}
ok(`dist binary --version = ${version}`)
} else {
console.log("SKIP: dist binary not found (run: bun run build --single --skip-embed-web-ui)")
}
console.log("\nAll local qimingcode sandbox checks passed.")