import { afterEach, expect, test } from "bun:test"; import { chmod, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const installer = fileURLToPath(new URL("../install-local.sh", import.meta.url)); const directories: string[] = []; const jsonc = '{\n // This file must remain untouched.\n "theme": "dark"\n}\n'; const oldPlugin = 'export default { id: "old", setup() { return "old"; } };\n'; type FixtureConfig = | { permissions: { shell: string }; plugins: { package: string }[] } | { theme: string; plugins: { package: string; options: { mode: string; coordinate: string; lunaPriority: boolean }; }[]; } | { plugins: string }; afterEach(async () => { await Promise.all( directories.splice(0).map((path) => rm(path, { recursive: true, force: true })), ); }); async function run(command: string[], cwd: string, env: NodeJS.ProcessEnv) { const child = Bun.spawn(command, { cwd, env, stdout: "pipe", stderr: "pipe", timeout: 20_000, }); const [exitCode, stdout, stderr] = await Promise.all([ child.exited, new Response(child.stdout).text(), new Response(child.stderr).text(), ]); return { exitCode, stdout, stderr }; } async function fixture(configValue?: FixtureConfig) { const directory = await mkdtemp(join(tmpdir(), "stanp-local-install-")); directories.push(directory); const checkout = join(directory, "checkout with spaces"); const cwd = join(directory, "different cwd"); const config = join(directory, "config with spaces"); const bin = join(directory, "bin"); const installed = join(config, "plugins", "stanp-opencode"); await Promise.all([ mkdir(checkout), mkdir(cwd), mkdir(bin), mkdir(installed, { recursive: true }), ]); await Promise.all([ Bun.write(join(checkout, "install-local.sh"), Bun.file(installer)), writeFile( join(checkout, "package.json"), JSON.stringify({ name: "stanp-opencode", version: "1.0.0", type: "module", exports: { ".": "./dist/index.js", "./tui": { import: "./dist/tui.js" } }, }), ), writeFile(join(checkout, "bun.lock"), ""), writeFile(join(config, "opencode.jsonc"), jsonc), writeFile( join(config, "opencode.json"), JSON.stringify( configValue ?? { permissions: { shell: "ask" }, plugins: [{ package: "existing-plugin" }], }, ), ), writeFile(join(installed, "index.js"), oldPlugin), writeFile( join(installed, "package.json"), '{"name":"stanp-opencode","type":"module","main":"./index.js"}\n', ), writeFile(join(config, "plugins", "stanp-opencode.js"), "legacy\n"), writeFile( join(bin, "bun"), `#!/usr/bin/env bash set -euo pipefail if [[ "\${1:-}" == "install" ]]; then [[ "\${2:-}" == "--frozen-lockfile" ]] printf '%s\n' "$PWD" >> "$CALLS" [[ "$MODE" != "dependency" ]] exit fi if [[ "\${1:-}" == "run" && "\${2:-}" == "build" ]]; then printf '%s\n' "$PWD" >> "$CALLS" [[ "$MODE" != "build" ]] mkdir -p dist if [[ "$MODE" == "validation" ]]; then printf '%s\n' 'export default { id: "stanp-opencode", server() {} };' > dist/index.js else printf 'export default { id: "stanp-opencode", setup() { return "%s"; } };\n' "$GENERATION" > dist/index.js fi printf '%s\n' 'export default { id: "stanp-opencode.quota-tui", setup() {} };' > dist/tui.js exit fi exec "$REAL_BUN" "$@" `, ), writeFile( join(bin, "curl"), `#!/usr/bin/env bash touch "$FETCH_MARKER" printf 'release fetching is forbidden\n' >&2 exit 99 `, ), ]); await Promise.all([chmod(join(bin, "bun"), 0o755), chmod(join(bin, "curl"), 0o755)]); const env = { ...Bun.env, PATH: `${bin}:${dirname(process.execPath)}:${Bun.env.PATH ?? "/usr/bin:/bin"}`, HOME: join(directory, "home"), XDG_CONFIG_HOME: join(directory, "ignored-xdg"), OPENCODE_CONFIG_DIR: config, REAL_BUN: process.execPath, MODE: "success", GENERATION: "first", CALLS: join(directory, "calls"), FETCH_MARKER: join(directory, "release-fetch-attempted"), }; return { directory, checkout, cwd, config, installed, env }; } test("builds from its checkout and installs defaults when invoked elsewhere", async () => { const setup = await fixture(); const result = await run( ["bash", join(setup.checkout, "install-local.sh")], setup.cwd, setup.env, ); expect(result, result.stderr).toMatchObject({ exitCode: 0 }); expect(await readFile(setup.env.CALLS, "utf8")).toBe(`${setup.checkout}\n${setup.checkout}\n`); expect(await Bun.file(setup.env.FETCH_MARKER).exists()).toBe(false); expect(await readFile(join(setup.config, "opencode.jsonc"), "utf8")).toBe(jsonc); expect(JSON.parse(await readFile(join(setup.config, "opencode.json"), "utf8"))).toEqual({ $schema: "https://opencode.ai/config.json", permissions: { shell: "ask" }, plugins: [ { package: "existing-plugin" }, { package: "./plugins/stanp-opencode", options: { mode: "gpt", provider: "openai", lunaPriority: true }, }, ], }); const imported = await run( [ process.execPath, "-e", "const { default: plugin } = await import(process.argv[1]); console.log(plugin.setup());", setup.installed, ], setup.cwd, setup.env, ); expect(imported, imported.stderr).toMatchObject({ exitCode: 0, stdout: "first\n" }); expect((await readdir(setup.installed)).sort()).toEqual(["index.js", "package.json", "tui.js"]); expect(await Bun.file(join(setup.config, "plugins", "stanp-opencode.js")).exists()).toBe(false); }); test("reinstall replaces bundles while preserving existing settings and plugin options", async () => { const configured = { theme: "dark", plugins: [ { package: "./plugins/stanp-opencode", options: { mode: "custom", coordinate: "provider/model", lunaPriority: false }, }, ], }; const setup = await fixture(configured); const first = await run( ["bash", join(setup.checkout, "install-local.sh")], setup.cwd, setup.env, ); expect(first, first.stderr).toMatchObject({ exitCode: 0 }); const second = await run(["bash", join(setup.checkout, "install-local.sh")], setup.cwd, { ...setup.env, GENERATION: "second", }); expect(second, second.stderr).toMatchObject({ exitCode: 0 }); expect(JSON.parse(await readFile(join(setup.config, "opencode.json"), "utf8"))).toEqual({ $schema: "https://opencode.ai/config.json", ...configured, }); expect(await readFile(join(setup.installed, "index.js"), "utf8")).toContain('return "second"'); }); for (const mode of ["dependency", "build", "validation", "config"] as const) { test(`${mode} failure leaves the previous installation and configuration untouched`, async () => { const invalidConfig = mode === "config" ? { plugins: "not-an-array" } : undefined; const setup = await fixture(invalidConfig); const beforeConfig = await readFile(join(setup.config, "opencode.json"), "utf8"); const result = await run(["bash", join(setup.checkout, "install-local.sh")], setup.cwd, { ...setup.env, MODE: mode === "config" ? "success" : mode, }); expect(result.exitCode, result.stderr).not.toBe(0); expect(await readFile(join(setup.installed, "index.js"), "utf8")).toBe(oldPlugin); expect(await readFile(join(setup.config, "opencode.json"), "utf8")).toBe(beforeConfig); expect(await readFile(join(setup.config, "opencode.jsonc"), "utf8")).toBe(jsonc); expect(await readFile(join(setup.config, "plugins", "stanp-opencode.js"), "utf8")).toBe( "legacy\n", ); expect((await readdir(join(setup.config, "plugins"))).sort()).toEqual([ "stanp-opencode", "stanp-opencode.js", ]); expect(await Bun.file(setup.env.FETCH_MARKER).exists()).toBe(false); }); }