chore: initialize qiming workspace repository
This commit is contained in:
269
qimingcode/packages/opencode/script/build.ts
Normal file
269
qimingcode/packages/opencode/script/build.ts
Normal 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 }
|
||||
16
qimingcode/packages/opencode/script/check-migrations.ts
Normal file
16
qimingcode/packages/opencode/script/check-migrations.ts
Normal 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")
|
||||
165
qimingcode/packages/opencode/script/check-version-consistency.ts
Normal file
165
qimingcode/packages/opencode/script/check-version-consistency.ts
Normal 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)
|
||||
}
|
||||
28
qimingcode/packages/opencode/script/fix-node-pty.ts
Normal file
28
qimingcode/packages/opencode/script/fix-node-pty.ts
Normal 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"}`)
|
||||
}
|
||||
}
|
||||
23
qimingcode/packages/opencode/script/generate.ts
Normal file
23
qimingcode/packages/opencode/script/generate.ts
Normal 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")
|
||||
115
qimingcode/packages/opencode/script/postinstall.mjs
Normal file
115
qimingcode/packages/opencode/script/postinstall.mjs
Normal 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)
|
||||
}
|
||||
229
qimingcode/packages/opencode/script/publish.ts
Normal file
229
qimingcode/packages/opencode/script/publish.ts
Normal 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`
|
||||
}
|
||||
}
|
||||
106
qimingcode/packages/opencode/script/run-workspace-server
Normal file
106
qimingcode/packages/opencode/script/run-workspace-server
Normal 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)
|
||||
}
|
||||
63
qimingcode/packages/opencode/script/schema.ts
Normal file
63
qimingcode/packages/opencode/script/schema.ts
Normal 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))
|
||||
}
|
||||
6
qimingcode/packages/opencode/script/time.ts
Normal file
6
qimingcode/packages/opencode/script/time.ts
Normal 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())
|
||||
153
qimingcode/packages/opencode/script/trace-imports.ts
Normal file
153
qimingcode/packages/opencode/script/trace-imports.ts
Normal 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)
|
||||
64
qimingcode/packages/opencode/script/upgrade-opentui.ts
Normal file
64
qimingcode/packages/opencode/script/upgrade-opentui.ts
Normal 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}`)
|
||||
}
|
||||
@@ -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 上存在。`,
|
||||
)
|
||||
76
qimingcode/packages/opencode/script/verify-sandbox.ts
Normal file
76
qimingcode/packages/opencode/script/verify-sandbox.ts
Normal 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.")
|
||||
Reference in New Issue
Block a user