feat: 品牌替换 + 启动优化 + AGENTS.md 模板定制
- 品牌替换:OpenCode/opencode → AirCoding/aircoding(16+ 文件) - Logo ASCII art:修复 left/right 行数不匹配导致的启动崩溃 - 启动诊断:添加 OPENCODE_PRINT_TIMING 计时探针 - dev 模式默认 --pure 跳过外部插件加载 - AGENTS.md 模板:追加 AirCoding 多 Agent 专项段落 - architect prompt + plugin:强化 AGENTS.md 产出验证
This commit is contained in:
94
packages/opencode/script/bench-search.ts
Normal file
94
packages/opencode/script/bench-search.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Effect } from "effect"
|
||||
import { Fff } from "@opencode-ai/core/filesystem/fff.bun"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
|
||||
const dir = AbsolutePath.make(process.cwd())
|
||||
|
||||
const FILE_QUERIES = ["fff", "package.json", "tools/ experiment"]
|
||||
const GREP_QUERIES = ["FileFinder", "import", "grep", "autocomplete"]
|
||||
const GLOB_QUERIES = ["**/*.test.ts"]
|
||||
|
||||
const FILE_LIMIT = 100
|
||||
const GREP_LIMIT = 50
|
||||
const GLOB_LIMIT = 50
|
||||
|
||||
const run = <A, R>(effect: Effect.Effect<A, unknown, R>) =>
|
||||
AppRuntime.runPromise(
|
||||
InstanceStore.Service.use((store) => store.provide({ directory: dir }, effect as never)),
|
||||
) as Promise<A>
|
||||
|
||||
// --- raw Fff picker ---
|
||||
const t0 = performance.now()
|
||||
const made = Fff.create({ basePath: dir, aiMode: true })
|
||||
if (!made.ok) {
|
||||
console.error("Fff.create failed:", made.error)
|
||||
process.exit(1)
|
||||
}
|
||||
const picker = made.value
|
||||
console.log(`picker create: ${(performance.now() - t0).toFixed(1)}ms`)
|
||||
|
||||
const tw = performance.now()
|
||||
await picker.waitForScan(2_500)
|
||||
console.log(`wait for scan: ${(performance.now() - tw).toFixed(1)}ms`)
|
||||
|
||||
// warmup grep to let the content index build
|
||||
const tWarmup = performance.now()
|
||||
picker.grep("_warmup_", { mode: "regex", maxMatchesPerFile: 1, timeBudgetMs: 1_500 })
|
||||
console.log(`grep warmup: ${(performance.now() - tWarmup).toFixed(1)}ms`)
|
||||
|
||||
console.log()
|
||||
console.log("--- raw picker (warm) ---")
|
||||
|
||||
for (const q of FILE_QUERIES) {
|
||||
const t = performance.now()
|
||||
const r = picker.fileSearch(q, { pageSize: Math.max(FILE_LIMIT, 100) })
|
||||
const count = r.ok ? r.value.items.length : "err"
|
||||
console.log(`[picker] fileSearch "${q}": ${(performance.now() - t).toFixed(1)}ms (${count} results)`)
|
||||
}
|
||||
|
||||
for (const q of GREP_QUERIES) {
|
||||
const t = performance.now()
|
||||
const r = picker.grep(q, { mode: "regex", pageSize: GREP_LIMIT, timeBudgetMs: 1_500 })
|
||||
const count = r.ok ? r.value.items.length : "err"
|
||||
console.log(`[picker] grep "${q}": ${(performance.now() - t).toFixed(1)}ms (${count} matches)`)
|
||||
}
|
||||
|
||||
picker.destroy()
|
||||
|
||||
// --- Search service: init breakdown ---
|
||||
console.log()
|
||||
|
||||
// 1) runtime + InstanceState + picker create + scan poll
|
||||
const tRuntime = performance.now()
|
||||
console.log(`[Search] init file (runtime + picker + scan): ${(performance.now() - tRuntime).toFixed(1)}ms`)
|
||||
|
||||
// 2) grep warmup (content index cold-start inside the Search service picker)
|
||||
const tGrepWarmup = performance.now()
|
||||
await run(FileSystem.Service.use((svc) => svc.grep({ pattern: "_warmup_grep_", limit: 1 })))
|
||||
console.log(`[Search] init grep (content index warmup): ${(performance.now() - tGrepWarmup).toFixed(1)}ms`)
|
||||
|
||||
console.log()
|
||||
console.log("--- Search service (warm) ---")
|
||||
|
||||
for (const q of FILE_QUERIES) {
|
||||
const t = performance.now()
|
||||
const r = await run(FileSystem.Service.use((svc) => svc.find({ query: q, limit: FILE_LIMIT })))
|
||||
console.log(`[Search.find] "${q}": ${(performance.now() - t).toFixed(1)}ms (${r.length} results)`)
|
||||
}
|
||||
|
||||
for (const q of GREP_QUERIES) {
|
||||
const t = performance.now()
|
||||
const r = await run(FileSystem.Service.use((svc) => svc.grep({ pattern: q, limit: GREP_LIMIT })))
|
||||
console.log(`[Search.grep] "${q}": ${(performance.now() - t).toFixed(1)}ms (${r.length} matches)`)
|
||||
}
|
||||
|
||||
for (const q of GLOB_QUERIES) {
|
||||
const t = performance.now()
|
||||
const r = await run(FileSystem.Service.use((svc) => svc.glob({ pattern: q, limit: GLOB_LIMIT })))
|
||||
console.log(`[Search.glob] "${q}": ${(performance.now() - t).toFixed(1)}ms (${r.length} files)`)
|
||||
}
|
||||
|
||||
process.exit(0)
|
||||
52
packages/opencode/script/bench-test-suite.ts
Normal file
52
packages/opencode/script/bench-test-suite.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// Full-suite timing harness for the test-speed research in ../../perf/test-suite.md.
|
||||
// Use this for periodic sanity checks; use profile-test-files.ts for discovery.
|
||||
// Env: BENCH_WARMUPS=0 BENCH_RUNS=1 bun run bench:test
|
||||
const warmups = Number(Bun.env.BENCH_WARMUPS ?? 0)
|
||||
const runs = Number(Bun.env.BENCH_RUNS ?? 1)
|
||||
const timings: number[] = []
|
||||
|
||||
if (!Number.isInteger(warmups) || warmups < 0) {
|
||||
console.error("BENCH_WARMUPS must be a non-negative integer")
|
||||
process.exit(1)
|
||||
}
|
||||
if (!Number.isInteger(runs) || runs < 1) {
|
||||
console.error("BENCH_RUNS must be a positive integer")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
for (const index of Array.from({ length: warmups + runs }, (_, index) => index)) {
|
||||
const measured = index >= warmups
|
||||
const label = measured ? `run ${index - warmups + 1}/${runs}` : `warmup ${index + 1}/${warmups}`
|
||||
const start = performance.now()
|
||||
console.log(`bench:test ${label}`)
|
||||
|
||||
const proc = Bun.spawn(["bun", "test", "--timeout", "30000"], {
|
||||
cwd: import.meta.dir + "/..",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
env: Bun.env,
|
||||
})
|
||||
|
||||
const exitCode = await proc.exited
|
||||
if (exitCode !== 0) {
|
||||
console.error(`bench:test failed during ${label} with exit code ${exitCode}`)
|
||||
process.exit(exitCode)
|
||||
}
|
||||
|
||||
const seconds = (performance.now() - start) / 1000
|
||||
console.log(`bench:test ${label} ${seconds.toFixed(3)}s`)
|
||||
if (measured) timings.push(seconds)
|
||||
}
|
||||
|
||||
const sorted = timings.toSorted((a, b) => a - b)
|
||||
const median = sorted[Math.floor(sorted.length / 2)]
|
||||
const mean = timings.reduce((sum, timing) => sum + timing, 0) / timings.length
|
||||
const best = sorted[0] ?? median
|
||||
const worst = sorted.at(-1) ?? median
|
||||
|
||||
console.log(
|
||||
`bench:test median=${median.toFixed(3)}s mean=${mean.toFixed(3)}s best=${best.toFixed(3)}s worst=${worst.toFixed(3)}s`,
|
||||
)
|
||||
console.log(`METRIC test_suite_seconds=${median.toFixed(3)}`)
|
||||
console.log(`METRIC test_suite_best_seconds=${best.toFixed(3)}`)
|
||||
console.log(`METRIC test_suite_worst_seconds=${worst.toFixed(3)}`)
|
||||
31
packages/opencode/script/build-node.ts
Executable file
31
packages/opencode/script/build-node.ts
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { Script } from "@opencode-ai/script"
|
||||
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 generated = await import("./generate.ts")
|
||||
|
||||
await Bun.build({
|
||||
target: "node",
|
||||
entrypoints: ["./src/node.ts"],
|
||||
outdir: "./dist/node",
|
||||
format: "esm",
|
||||
sourcemap: "linked",
|
||||
external: ["jsonc-parser", "@lydell/node-pty"],
|
||||
define: {
|
||||
OPENCODE_MODELS_DEV: generated.modelsData,
|
||||
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
||||
},
|
||||
files: {
|
||||
"opencode-web-ui.gen.ts": "",
|
||||
},
|
||||
})
|
||||
|
||||
console.log("Build complete")
|
||||
243
packages/opencode/script/build.ts
Executable file
243
packages/opencode/script/build.ts
Executable file
@@ -0,0 +1,243 @@
|
||||
#!/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)
|
||||
|
||||
const generated = await import("./generate.ts")
|
||||
|
||||
import { Script } from "@opencode-ai/script"
|
||||
import pkg from "../package.json"
|
||||
|
||||
const singleFlag = process.argv.includes("--single")
|
||||
const baselineFlag = process.argv.includes("--baseline")
|
||||
const skipInstall = process.argv.includes("--skip-install")
|
||||
const sourcemapsFlag = process.argv.includes("--sourcemaps")
|
||||
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 $`OPENCODE_CHANNEL=${Script.channel} bun run --cwd ${appDir} build`
|
||||
const files = (await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: dist })))
|
||||
.map((file) => file.replaceAll("\\", "/"))
|
||||
.filter((file) => !file.endsWith(".map"))
|
||||
.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"]}`
|
||||
await $`bun install --os="*" --cpu="*" @ff-labs/fff-bun@${pkg.dependencies["@ff-labs/fff-bun"]}`
|
||||
}
|
||||
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/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: ["bun", "node"],
|
||||
tsconfig: "./tsconfig.json",
|
||||
plugins: [plugin],
|
||||
external: ["node-gyp"],
|
||||
format: "esm",
|
||||
minify: true,
|
||||
sourcemap: sourcemapsFlag ? "linked" : "none",
|
||||
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: {
|
||||
FFF_LIBC: JSON.stringify(item.abi === "musl" ? "musl" : "gnu"),
|
||||
OPENCODE_VERSION: `'${Script.version}'`,
|
||||
OPENCODE_MODELS_DEV: generated.modelsData,
|
||||
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
|
||||
OPENCODE_WORKER_PATH: workerPath,
|
||||
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
||||
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
|
||||
...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(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`
|
||||
await Bun.file(`dist/${name}/package.json`).write(
|
||||
JSON.stringify(
|
||||
{
|
||||
name,
|
||||
version: Script.version,
|
||||
preferUnplugged: true,
|
||||
os: [item.os],
|
||||
cpu: [item.arch],
|
||||
...(item.abi ? { libc: [item.abi] } : {}),
|
||||
},
|
||||
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 }
|
||||
14
packages/opencode/script/generate.ts
Normal file
14
packages/opencode/script/generate.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
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"
|
||||
export 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())
|
||||
console.log("Loaded models.dev snapshot")
|
||||
1
packages/opencode/script/httpapi-exercise.ts
Normal file
1
packages/opencode/script/httpapi-exercise.ts
Normal file
@@ -0,0 +1 @@
|
||||
await import("../test/server/httpapi-exercise/index")
|
||||
189
packages/opencode/script/postinstall.mjs
Normal file
189
packages/opencode/script/postinstall.mjs
Normal file
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import childProcess from "child_process"
|
||||
import fs from "fs"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { createRequire } from "module"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const require = createRequire(import.meta.url)
|
||||
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8"))
|
||||
|
||||
const platformMap = {
|
||||
darwin: "darwin",
|
||||
linux: "linux",
|
||||
win32: "windows",
|
||||
}
|
||||
const archMap = {
|
||||
x64: "x64",
|
||||
arm64: "arm64",
|
||||
arm: "arm",
|
||||
}
|
||||
|
||||
const platform = platformMap[os.platform()] ?? os.platform()
|
||||
const arch = archMap[os.arch()] ?? os.arch()
|
||||
const base = `opencode-${platform}-${arch}`
|
||||
const sourceBinary = platform === "windows" ? "opencode.exe" : "opencode"
|
||||
const targetBinary = path.join(__dirname, "bin", "opencode.exe")
|
||||
|
||||
function supportsAvx2() {
|
||||
if (arch !== "x64") return false
|
||||
|
||||
if (platform === "linux") {
|
||||
try {
|
||||
return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === "darwin") {
|
||||
try {
|
||||
const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], {
|
||||
encoding: "utf8",
|
||||
timeout: 1500,
|
||||
})
|
||||
if (result.status !== 0) return false
|
||||
return (result.stdout || "").trim() === "1"
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === "windows") {
|
||||
const command =
|
||||
'(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)'
|
||||
|
||||
for (const executable of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) {
|
||||
try {
|
||||
const result = childProcess.spawnSync(executable, ["-NoProfile", "-NonInteractive", "-Command", command], {
|
||||
encoding: "utf8",
|
||||
timeout: 3000,
|
||||
windowsHide: true,
|
||||
})
|
||||
if (result.status !== 0) continue
|
||||
const output = (result.stdout || "").trim().toLowerCase()
|
||||
if (output === "true" || output === "1") return true
|
||||
if (output === "false" || output === "0") return false
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function isMusl() {
|
||||
if (platform !== "linux") return false
|
||||
|
||||
try {
|
||||
if (fs.existsSync("/etc/alpine-release")) return true
|
||||
} catch {
|
||||
// Ignore filesystem probes that are blocked by the host.
|
||||
}
|
||||
|
||||
try {
|
||||
const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
|
||||
return `${result.stdout || ""}${result.stderr || ""}`.toLowerCase().includes("musl")
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function packageNames() {
|
||||
const baseline = arch === "x64" && !supportsAvx2()
|
||||
|
||||
if (platform === "linux") {
|
||||
if (isMusl()) {
|
||||
if (arch === "x64")
|
||||
return baseline
|
||||
? [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
|
||||
: [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
|
||||
return [`${base}-musl`, base]
|
||||
}
|
||||
|
||||
if (arch === "x64")
|
||||
return baseline
|
||||
? [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
|
||||
: [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
|
||||
return [base, `${base}-musl`]
|
||||
}
|
||||
|
||||
if (arch === "x64") return baseline ? [`${base}-baseline`, base] : [base, `${base}-baseline`]
|
||||
return [base]
|
||||
}
|
||||
|
||||
function resolveBinary(name) {
|
||||
const packageJsonPath = require.resolve(`${name}/package.json`)
|
||||
const binaryPath = path.join(path.dirname(packageJsonPath), "bin", sourceBinary)
|
||||
if (!fs.existsSync(binaryPath)) throw new Error(`Binary not found at ${binaryPath}`)
|
||||
return binaryPath
|
||||
}
|
||||
|
||||
function installPackage(name) {
|
||||
const version = packageJson.optionalDependencies?.[name]
|
||||
if (!version) return
|
||||
|
||||
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-install-"))
|
||||
try {
|
||||
const result = childProcess.spawnSync(
|
||||
"npm",
|
||||
["install", "--ignore-scripts", "--no-save", "--loglevel=error", "--prefix", temp, `${name}@${version}`],
|
||||
{ stdio: "inherit", windowsHide: true },
|
||||
)
|
||||
if (result.status !== 0) return
|
||||
const packageDir = path.join(temp, "node_modules", name)
|
||||
copyBinary(path.join(packageDir, "bin", sourceBinary), targetBinary)
|
||||
return true
|
||||
} finally {
|
||||
fs.rmSync(temp, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
function copyBinary(source, target) {
|
||||
if (!fs.existsSync(source)) throw new Error(`Binary not found at ${source}`)
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true })
|
||||
if (fs.existsSync(target)) fs.unlinkSync(target)
|
||||
try {
|
||||
fs.linkSync(source, target)
|
||||
} catch {
|
||||
fs.copyFileSync(source, target)
|
||||
}
|
||||
fs.chmodSync(target, 0o755)
|
||||
}
|
||||
|
||||
function verifyBinary() {
|
||||
const result = childProcess.spawnSync(targetBinary, ["--version"], {
|
||||
encoding: "utf8",
|
||||
stdio: "ignore",
|
||||
windowsHide: true,
|
||||
})
|
||||
return result.status === 0
|
||||
}
|
||||
|
||||
function main() {
|
||||
for (const name of packageNames()) {
|
||||
try {
|
||||
copyBinary(resolveBinary(name), targetBinary)
|
||||
if (verifyBinary()) return
|
||||
} catch {
|
||||
if (installPackage(name) && verifyBinary()) return
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`It seems your package manager failed to install the right opencode CLI package. Try manually installing ${packageNames()
|
||||
.map((name) => JSON.stringify(name))
|
||||
.join(" or ")}.`,
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
main()
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
42
packages/opencode/script/profile-test-files.ts
Normal file
42
packages/opencode/script/profile-test-files.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// Per-file profiler for finding candidate test-speed work; see ../../perf/test-suite.md
|
||||
// for the benchmark notes, kept wins, and discarded experiments.
|
||||
// Example: TEST_PROFILE_GLOB='test/server/**/*.test.ts' TEST_PROFILE_TOP=15 bun run profile:test
|
||||
const pattern = Bun.env.TEST_PROFILE_GLOB ?? "test/**/*.test.{ts,tsx}"
|
||||
const limit = Number(Bun.env.TEST_PROFILE_LIMIT ?? 0)
|
||||
const timeout = Bun.env.TEST_PROFILE_TIMEOUT ?? "30000"
|
||||
const files = Array.fromAsync(new Bun.Glob(pattern).scan({ cwd: import.meta.dir + "/..", onlyFiles: true }))
|
||||
.then((files) => files.toSorted())
|
||||
.then((files) => (limit > 0 ? files.slice(0, limit) : files))
|
||||
|
||||
const results = []
|
||||
for (const file of await files) {
|
||||
const start = performance.now()
|
||||
const proc = Bun.spawn(["bun", "test", "--timeout", timeout, file], {
|
||||
cwd: import.meta.dir + "/..",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: Bun.env,
|
||||
})
|
||||
const [output, error, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
])
|
||||
const seconds = (performance.now() - start) / 1000
|
||||
results.push({ file, seconds, exitCode })
|
||||
console.log(`${exitCode === 0 ? "PASS" : "FAIL"} ${seconds.toFixed(3)}s ${file}`)
|
||||
if (exitCode !== 0) console.log((output + error).trim())
|
||||
}
|
||||
|
||||
const sorted = results.toSorted((a, b) => b.seconds - a.seconds)
|
||||
console.log("\nSlowest test files:")
|
||||
for (const result of sorted.slice(0, Number(Bun.env.TEST_PROFILE_TOP ?? 20))) {
|
||||
console.log(`${result.seconds.toFixed(3)}s ${result.exitCode === 0 ? "PASS" : "FAIL"} ${result.file}`)
|
||||
}
|
||||
|
||||
if (sorted[0]) {
|
||||
console.log(`METRIC slowest_test_file_seconds=${sorted[0].seconds.toFixed(3)}`)
|
||||
console.log(`METRIC profiled_test_files=${results.length}`)
|
||||
}
|
||||
|
||||
if (results.some((result) => result.exitCode !== 0)) process.exit(1)
|
||||
213
packages/opencode/script/publish.ts
Executable file
213
packages/opencode/script/publish.ts
Executable file
@@ -0,0 +1,213 @@
|
||||
#!/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)
|
||||
|
||||
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
|
||||
}
|
||||
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 pkg = await Bun.file(`./dist/${filepath}`).json()
|
||||
binaries[pkg.name] = pkg.version
|
||||
}
|
||||
console.log("binaries", binaries)
|
||||
const version = Object.values(binaries)[0]
|
||||
|
||||
await $`mkdir -p ./dist/${pkg.name}`
|
||||
await $`mkdir -p ./dist/${pkg.name}/bin`
|
||||
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}/bin/${pkg.name}.exe`).write(
|
||||
[
|
||||
`echo "Error: ${pkg.name}-ai's postinstall script was not run." >&2`,
|
||||
'echo "" >&2',
|
||||
'echo "This occurs when using --ignore-scripts during installation, or when using a" >&2',
|
||||
'echo "package manager like pnpm that does not run postinstall scripts by default." >&2',
|
||||
'echo "" >&2',
|
||||
'echo "To fix this, run the postinstall script manually:" >&2',
|
||||
`echo " cd node_modules/${pkg.name}-ai && node postinstall.mjs" >&2`,
|
||||
'echo "" >&2',
|
||||
`echo "Or reinstall ${pkg.name}-ai without the --ignore-scripts flag." >&2`,
|
||||
"exit 1",
|
||||
"",
|
||||
].join("\n"),
|
||||
)
|
||||
|
||||
await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
||||
JSON.stringify(
|
||||
{
|
||||
name: pkg.name + "-ai",
|
||||
bin: {
|
||||
[pkg.name]: `./bin/${pkg.name}.exe`,
|
||||
},
|
||||
scripts: {
|
||||
postinstall: "node ./postinstall.mjs",
|
||||
},
|
||||
version: version,
|
||||
license: pkg.license,
|
||||
os: ["darwin", "linux", "win32"],
|
||||
cpu: ["arm64", "x64"],
|
||||
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}-ai`, 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])
|
||||
|
||||
// registries
|
||||
if (!Script.preview) {
|
||||
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
packages/opencode/script/run-workspace-server
Executable file
106
packages/opencode/script/run-workspace-server
Executable 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)
|
||||
}
|
||||
77
packages/opencode/script/schema.ts
Executable file
77
packages/opencode/script/schema.ts
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { Config } from "@/config/config"
|
||||
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
|
||||
import { TuiConfig } from "@opencode-ai/tui/config"
|
||||
import { Schema } from "effect"
|
||||
|
||||
type JsonSchema = Record<string, unknown>
|
||||
const MODEL_REF = "https://models.dev/model-schema.json#/$defs/Model"
|
||||
|
||||
function generateEffect(schema: Schema.Top) {
|
||||
const document = Schema.toJsonSchemaDocument(schema)
|
||||
const normalized = normalize({
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema",
|
||||
...document.schema,
|
||||
$defs: document.definitions,
|
||||
})
|
||||
if (!isRecord(normalized)) throw new Error("schema generator produced a non-object schema")
|
||||
const restored = restoreModelRefs(normalized)
|
||||
if (!isRecord(restored)) throw new Error("schema generator produced a non-object schema")
|
||||
restored.allowComments = true
|
||||
restored.allowTrailingCommas = true
|
||||
return restored
|
||||
}
|
||||
|
||||
function normalize(value: unknown): unknown {
|
||||
if (Array.isArray(value)) return value.map(normalize)
|
||||
if (!isRecord(value)) return value
|
||||
|
||||
const schema = Object.fromEntries(Object.entries(value).map(([key, item]) => [key, normalize(item)]))
|
||||
|
||||
if (Array.isArray(schema.anyOf)) {
|
||||
const anyOf = schema.anyOf.filter((item) => !isRecord(item) || item.type !== "null")
|
||||
if (anyOf.length !== schema.anyOf.length) {
|
||||
const { anyOf: _, ...rest } = schema
|
||||
if (anyOf.length === 1 && isRecord(anyOf[0])) return normalize({ ...anyOf[0], ...rest })
|
||||
return { ...rest, anyOf }
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(schema.allOf) && schema.allOf.length === 1 && isRecord(schema.allOf[0])) {
|
||||
const { allOf: _, ...rest } = schema
|
||||
return normalize({ ...schema.allOf[0], ...rest })
|
||||
}
|
||||
|
||||
if (schema.type === "integer" && schema.maximum === undefined) {
|
||||
return { ...schema, maximum: Number.MAX_SAFE_INTEGER }
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
function restoreModelRefs(value: unknown, key?: string): unknown {
|
||||
if (Array.isArray(value)) return value.map((item) => restoreModelRefs(item))
|
||||
if (!isRecord(value)) return value
|
||||
|
||||
const schema = Object.fromEntries(Object.entries(value).map(([name, item]) => [name, restoreModelRefs(item, name)]))
|
||||
if ((key === "model" || key === "small_model") && schema.type === "string") {
|
||||
return { ...schema, $ref: MODEL_REF }
|
||||
}
|
||||
return schema
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is JsonSchema {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
const configFile = process.argv[2]
|
||||
const tuiFile = process.argv[3]
|
||||
|
||||
console.log(configFile)
|
||||
await Bun.write(configFile, JSON.stringify(generateEffect(ConfigV1.Info), null, 2))
|
||||
|
||||
if (tuiFile) {
|
||||
console.log(tuiFile)
|
||||
await Bun.write(tuiFile, JSON.stringify(generateEffect(TuiConfig.Info), null, 2))
|
||||
}
|
||||
6
packages/opencode/script/time.ts
Executable file
6
packages/opencode/script/time.ts
Executable 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
packages/opencode/script/trace-imports.ts
Executable file
153
packages/opencode/script/trace-imports.ts
Executable 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/plugin/tui/runtime.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)
|
||||
Reference in New Issue
Block a user