cli: inline text imports as content in the node bundle

Vite's built-in asset plugin claims known asset types (.txt) before
normal-priority plugins load, rewriting the import to an asset URL
string — the models.dev snapshot shipped as "/assets/snapshot-*.txt"
and cold-cache catalog access died decoding it. Pre-existing instances
of the same class: the node bundle's prompt and tool-description .txt
imports. enforce: "pre" intercepts first; a build assertion now fails
the build if text imports surface as asset URLs, since the bundle still
builds and runs --help convincingly without their content.

Also: drop the redundant text.d.ts (bun-types already declares *.txt),
give the snapshot refresh script a provider-count floor and
Windows-safe path printing.
This commit is contained in:
Kit Langton
2026-08-11 16:56:15 -04:00
parent cf570e9a22
commit 7c5182ecaf
4 changed files with 32 additions and 8 deletions
+21 -1
View File
@@ -2,7 +2,7 @@
import { spawnSync } from "node:child_process"
import { createHash } from "node:crypto"
import { chmod, copyFile, mkdir, mkdtemp, realpath, rename, rm, stat, writeFile } from "node:fs/promises"
import { chmod, copyFile, mkdir, mkdtemp, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import { build } from "vite"
@@ -55,6 +55,25 @@ const builder =
? await resolveHostNode()
: undefined
// Vite silently rewrites text imports of known asset types (.txt) to asset
// URL strings when the raw-text plugin doesn't intercept them first — the
// bundle still builds and `--help` still runs, so only content assertions
// catch it. Guards the models.dev snapshot and the prompt/tool description
// text that ships inside the bundle.
async function assertTextImportsInlined(bundlePath: string) {
const bundle = await readFile(bundlePath, "utf8")
const markers = [
{ marker: '"zhipuai"', source: "models-dev snapshot" },
{ marker: "/assets/snapshot", source: "models-dev snapshot inlined as asset URL", forbidden: true },
{ marker: '="/assets/', source: "text import inlined as asset URL", forbidden: true },
]
for (const { marker, source, forbidden } of markers) {
const present = bundle.includes(marker)
if (forbidden ? present : !present)
throw new Error(`${bundlePath}: ${source} — text imports are not inlined as content (marker ${marker})`)
}
}
for (const target of targets) {
console.log(`building cli-node-${targetName(target)}`)
const assets = await collectNodeAssets(target)
@@ -63,6 +82,7 @@ for (const target of targets) {
const input = { version: Script.version, channel: Script.channel, assetHash, target }
await copyNodeAssets(assets)
await build(mainConfig(input))
await assertTextImportsInlined("dist-node/opencode.mjs")
const host = target.platform === process.platform && target.arch === process.arch
if (host) {
+5
View File
@@ -10,6 +10,11 @@ const dir = import.meta.dirname
function rawTextPlugin(): Plugin {
return {
name: "opencode:raw-text",
// "pre" is load-bearing for .txt: Vite's built-in asset plugin claims
// known asset types (.txt among them) ahead of normal-priority plugins,
// replacing the import with an asset URL string instead of the content.
// .md only ever worked without it because .md is not a known asset type.
enforce: "pre",
async load(id) {
if (!id.endsWith(".md") && !id.endsWith(".txt")) return
return `export default ${JSON.stringify(await readFile(id, "utf8"))}`
@@ -12,10 +12,13 @@ if (!response.ok) {
}
const text = await response.text()
const parsed: unknown = JSON.parse(text)
if (typeof parsed !== "object" || parsed === null || Object.keys(parsed).length === 0) {
console.error("Fetched catalog is empty; refusing to write snapshot")
// A floor, not equality: guards against committing an error page or a
// truncated body that still parses as a small object.
const MINIMUM_PROVIDERS = 100
if (typeof parsed !== "object" || parsed === null || Object.keys(parsed).length < MINIMUM_PROVIDERS) {
console.error(`Fetched catalog has fewer than ${MINIMUM_PROVIDERS} providers; refusing to write snapshot`)
process.exit(1)
}
const target = new URL("../src/models-dev/snapshot.txt", import.meta.url)
await Bun.write(target, text)
console.log(`Wrote ${Object.keys(parsed).length} providers (${text.length} bytes) to ${target.pathname}`)
console.log(`Wrote ${Object.keys(parsed).length} providers (${text.length} bytes) to ${Bun.fileURLToPath(target)}`)
-4
View File
@@ -1,4 +0,0 @@
declare module "*.txt" {
const content: string
export default content
}