import { afterEach, expect, test } from "bun:test"; import { createHash } from "node:crypto"; import { chmod, mkdir, mkdtemp, readFile, readdir, readlink, rm, symlink, 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.sh", import.meta.url)); const directories: string[] = []; const configContents = '{\n // Keep user settings and comments.\n "theme": "dark"\n}\n'; const jsonConfig = { permissions: { shell: "ask" }, plugins: [{ package: "existing-plugin" }] }; const previousPlugin = 'export default { id: "previous", setup() { return "preserved"; } };\n'; const previousTarget = "../stanp-opencode/install.previous"; 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 install(mode: "native" | "checksum" | "legacy", configured = false) { const directory = await mkdtemp(join(tmpdir(), "stanp-install-")); directories.push(directory); const config = mode === "native" ? join(directory, "xdg", "opencode") : join(directory, "custom config with spaces"); const bin = join(directory, "bin"); const fixture = join(directory, "package"); await Promise.all([ mkdir(bin), mkdir(join(fixture, "dist"), { recursive: true }), mkdir(join(config, "plugins"), { recursive: true }), mkdir(join(config, "stanp-opencode", "install.previous"), { recursive: true }), ]); await Promise.all([ writeFile(join(config, "opencode.jsonc"), configContents), writeFile( join(config, "opencode.json"), JSON.stringify( configured ? { ...jsonConfig, plugins: [ ...jsonConfig.plugins, { package: "./plugins/stanp-opencode", options: { mode: "gpt", provider: "existing-provider", lunaPriority: false, }, }, ], } : jsonConfig, ), ), symlink(previousTarget, join(config, "plugins", "stanp-opencode")), writeFile(join(config, "stanp-opencode", "install.previous", "index.js"), previousPlugin), writeFile( join(config, "stanp-opencode", "install.previous", "package.json"), '{"name":"stanp-opencode","type":"module","main":"./index.js"}\n', ), writeFile(join(fixture, "index.js"), 'export { default } from "./dist/index.js";\n'), writeFile( join(fixture, "package.json"), JSON.stringify({ name: "stanp-opencode", version: "1.0.0", type: "module", main: "./dist/index.js", exports: "./dist/index.js", scripts: { postinstall: "bun ./postinstall.js" }, }), ), writeFile( join(fixture, "postinstall.js"), 'await Bun.write(process.env.POSTINSTALL_MARKER, "executed");\n', ), writeFile( join(fixture, "dist", "index.js"), 'await Bun.write(process.env.IMPORT_MARKER, "imported");\n' + (mode === "legacy" ? 'export default { id: "stanp-opencode", async server() { return {}; } };\n' : 'export default { id: "stanp-opencode", setup() { return "native-ready"; } };\n'), ), ]); const env = { ...Bun.env, PATH: `${bin}:${dirname(process.execPath)}:${Bun.env.PATH ?? "/usr/bin:/bin"}`, HOME: directory, XDG_CONFIG_HOME: join(directory, "xdg"), OPENCODE_CONFIG_DIR: mode === "native" ? "" : config, FIXTURE_DIR: directory, POSTINSTALL_MARKER: join(directory, "postinstall-ran"), IMPORT_MARKER: join(directory, "import-ran"), }; const packed = await run(["tar", "-czf", "stanp-opencode.tgz", "package"], directory, env); expect(packed, packed.stderr).toMatchObject({ exitCode: 0 }); const digest = mode === "checksum" ? "0".repeat(64) : createHash("sha256") .update(await readFile(join(directory, "stanp-opencode.tgz"))) .digest("hex"); await Promise.all([ writeFile(join(directory, "stanp-opencode.tgz.sha256"), `${digest} stanp-opencode.tgz\n`), writeFile( join(directory, "release.json"), JSON.stringify({ tag_name: "v1.0.0", assets: ["stanp-opencode.tgz", "stanp-opencode.tgz.sha256"].map((name, id) => ({ id: id + 1, name, browser_download_url: `https://fixtures.invalid/${name}`, })), }), ), writeFile( join(bin, "curl"), `#!/usr/bin/env bash set -euo pipefail url="" output="" while (($#)); do case "$1" in -o|--output) output="$2"; shift 2 ;; https://*) url="$1"; shift ;; *) shift ;; esac done case "$url" in https://git.krtss.de/api/v1/repos/StanPonomarev/stanp-opencode/releases/latest) source=release.json ;; https://fixtures.invalid/stanp-opencode.tgz?asset_id=1) source=stanp-opencode.tgz ;; https://fixtures.invalid/stanp-opencode.tgz.sha256?asset_id=2) source=stanp-opencode.tgz.sha256 ;; *) printf 'Unexpected fixture URL: %s\\n' "$url" >&2; exit 1 ;; esac if [[ -n "$output" ]]; then cp "$FIXTURE_DIR/$source" "$output" else cat "$FIXTURE_DIR/$source" fi `, ), writeFile( join(bin, "sha256sum"), `#!/usr/bin/env bash if [[ "\${1:-}" == -* ]]; then printf 'usage: sha256sum [-bctwz] [files ...]\n' >&2 exit 1 fi PATH="\${PATH#*:}" export PATH exec sha256sum "$@" `, ), ]); await Promise.all([chmod(join(bin, "curl"), 0o755), chmod(join(bin, "sha256sum"), 0o755)]); const result = await run(["bash", installer], directory, env); return { directory, config, env, result }; } test("installs an importable native plugin and default config under XDG without running scripts", async () => { const { directory, config, env, result } = await install("native"); expect(result, result.stderr).toMatchObject({ exitCode: 0 }); // The generated package path is only known at runtime; exercise its module-loading boundary. const imported = await run( [ process.execPath, "-e", "const { default: plugin } = await import(process.argv[1]); console.log(JSON.stringify([plugin.id, plugin.setup()]));", join(config, "plugins", "stanp-opencode"), ], directory, env, ); expect(imported, imported.stderr).toMatchObject({ exitCode: 0, stdout: '["stanp-opencode","native-ready"]\n', }); expect(await readFile(join(config, "opencode.jsonc"), "utf8")).toBe(configContents); expect(JSON.parse(await readFile(join(config, "opencode.json"), "utf8"))).toEqual({ $schema: "https://opencode.ai/config.json", plugins: [ { package: "existing-plugin" }, { package: "./plugins/stanp-opencode", options: { mode: "gpt", provider: "openai", lunaPriority: true }, }, ], permissions: { shell: "ask" }, }); expect(await Bun.file(join(config, "plugins", "stanp-opencode", "package.json")).exists()).toBe( true, ); expect( await readFile(join(config, "stanp-opencode", "install.previous", "index.js"), "utf8"), ).toBe(previousPlugin); expect(await Bun.file(env.POSTINSTALL_MARKER).exists()).toBe(false); }, 60_000); test("preserves an existing stanp-opencode configuration", async () => { const { config, result } = await install("native", true); expect(result, result.stderr).toMatchObject({ exitCode: 0 }); const installed = JSON.parse(await readFile(join(config, "opencode.json"), "utf8")); expect(installed.plugins).toEqual([ { package: "existing-plugin" }, { package: "./plugins/stanp-opencode", options: { mode: "gpt", provider: "existing-provider", lunaPriority: false, }, }, ]); expect(installed.permissions).toEqual(jsonConfig.permissions); }, 60_000); for (const mode of ["checksum", "legacy"] as const) { test(`${mode} failure preserves the installed plugin in a custom path containing spaces`, async () => { const { directory, config, env, result } = await install(mode); expect(result.exitCode, result.stderr).not.toBe(0); expect(await readlink(join(config, "plugins", "stanp-opencode"))).toBe(previousTarget); expect(await readFile(join(config, "opencode.jsonc"), "utf8")).toBe(configContents); expect(JSON.parse(await readFile(join(config, "opencode.json"), "utf8"))).toEqual( jsonConfig, ); expect( await readFile(join(config, "stanp-opencode", "install.previous", "index.js"), "utf8"), ).toBe(previousPlugin); expect(await readdir(join(config, "stanp-opencode"))).toEqual(["install.previous"]); expect(await readdir(join(config, "plugins"))).toEqual(["stanp-opencode"]); // Load the preserved package from this test's runtime-selected configuration directory. const imported = await run( [ process.execPath, "-e", "const { default: plugin } = await import(process.argv[1]); console.log(plugin.setup());", join(config, "plugins", "stanp-opencode"), ], directory, env, ); expect(imported, imported.stderr).toMatchObject({ exitCode: 0, stdout: "preserved\n" }); expect(await Bun.file(env.POSTINSTALL_MARKER).exists()).toBe(false); expect(await Bun.file(env.IMPORT_MARKER).exists()).toBe(mode === "legacy"); }, 60_000); }