From fb3ad11f3ae6e7ea8acae006dd724ecc644f5b32 Mon Sep 17 00:00:00 2001 From: Stanislav Ponomarev Date: Thu, 10 Sep 2026 14:16:03 +0200 Subject: [PATCH] update config setup --- README.md | 8 +++--- install.sh | 38 +++++++++++++++++++++---- tests/install.test.ts | 64 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 97 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8a753b7..23bb41d 100644 --- a/README.md +++ b/README.md @@ -22,19 +22,19 @@ Installation defaults to `~/.config/opencode` and respects `XDG_CONFIG_HOME`. Ov OPENCODE_CONFIG_DIR=/path/to/opencode-config bash install.sh ``` -The installer atomically replaces `plugins/stanp-opencode.js` for [V2 automatic plugin discovery](https://opencode.ai/v2/docs/plugins). It does not create symlinks, install package dependencies, edit `opencode.json` or `opencode.jsonc`, install OpenCode itself, or configure provider credentials. +The installer atomically replaces the package directory at `plugins/stanp-opencode` and adds its default GPT configuration to `opencode.json`. Existing unrelated JSON settings and `opencode.jsonc` are preserved. It does not create symlinks, install package dependencies, install OpenCode itself, or configure provider credentials. Restart `opencode2` after installation (or `opencode2 service restart` for an existing service). Connect your provider, then select `coordinate` in the agent picker. The default configuration uses OpenAI GPT models; agents are only enabled when the required models are available. ## Configure -See [GPT configuration](examples/gpt.opencode.jsonc) and [custom model assignments](examples/custom.opencode.jsonc). When using the installer, point the example's plugin package entry at the installed package directory rather than the unpublished npm package name: +See [GPT configuration](examples/gpt.opencode.jsonc) and [custom model assignments](examples/custom.opencode.jsonc). When using the installer, point the example's plugin package entry at the installed package directory rather than the unpublished npm package name. Configured local plugin paths must be directories; pointing at `plugins/stanp-opencode.js` causes OpenCode to reject that configured entry and auto-load the file without its options: ```jsonc { "plugins": [ { - "package": "./plugins/stanp-opencode.js", + "package": "./plugins/stanp-opencode", "options": { "mode": "gpt", "provider": "openai", @@ -51,4 +51,4 @@ The plugin supplies session-local `todoread` and `todowrite` tools when no other ## Uninstall -Remove `plugins/stanp-opencode.js` from the selected config directory and remove any explicit configuration entry for it, then restart OpenCode. +Remove `plugins/stanp-opencode` from the selected config directory and remove any explicit configuration entry for it, then restart OpenCode. diff --git a/install.sh b/install.sh index fcc15c8..c8be9b9 100755 --- a/install.sh +++ b/install.sh @@ -45,11 +45,39 @@ bun -e ' ' "$install/package/dist/index.js" tag=$(jq -r '.tag_name' "$install/release.json") -if ! mv -f "$install/package/dist/index.js" "$config/plugins/stanp-opencode.js"; then +config_file="$config/opencode.json" +config_source="$config_file" +if [[ ! -e "$config_source" ]]; then + config_source="$install/empty-config.json" + printf '{}\n' > "$config_source" +fi +jq -e ' + if type != "object" then error("opencode.json must contain an object") else . end + | if (.plugins // []) | type != "array" then error("opencode.json plugins must be an array") else . end + | .["$schema"] = (.["$schema"] // "https://opencode.ai/config.json") + | .plugins = (.plugins // []) + | if any(.plugins[]?; .package == "./plugins/stanp-opencode") then . else + .plugins += [{ + package: "./plugins/stanp-opencode", + options: { mode: "gpt", provider: "openai", lunaPriority: true } + }] + end +' "$config_source" > "$install/opencode.json" +target="$config/plugins/stanp-opencode" +staged="$config/plugins/.stanp-opencode.new.$$" +backup="$config/plugins/.stanp-opencode.old.$$" +mkdir "$staged" +mv "$install/package/dist/index.js" "$staged/index.js" +cat > "$staged/package.json" <<'EOF' +{"name":"stanp-opencode","type":"module","main":"./index.js","exports":"./index.js"} +EOF +if [[ -e "$target" || -L "$target" ]]; then mv "$target" "$backup"; fi +if ! mv "$staged" "$target"; then + if [[ -e "$backup" || -L "$backup" ]]; then mv "$backup" "$target"; fi printf 'Could not activate plugin; installation unchanged\n' >&2 exit 1 fi -if [[ -L "$config/plugins/stanp-opencode" ]]; then - rm -f "$config/plugins/stanp-opencode" -fi -printf 'Installed stanp-opencode %s into %s/plugins/stanp-opencode.js\nRestart opencode2 to load it.\n' "$tag" "$config" +rm -rf "$backup" +rm -f "$config/plugins/stanp-opencode.js" +mv "$install/opencode.json" "$config_file" +printf 'Installed stanp-opencode %s into %s/plugins/stanp-opencode and updated %s\nRestart opencode2 to load it.\n' "$tag" "$config" "$config_file" diff --git a/tests/install.test.ts b/tests/install.test.ts index 7db16f9..a04d0fe 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -18,6 +18,7 @@ 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"; @@ -43,7 +44,7 @@ async function run(command: string[], cwd: string, env: NodeJS.ProcessEnv) { return { exitCode, stdout, stderr }; } -async function install(mode: "native" | "checksum" | "legacy") { +async function install(mode: "native" | "checksum" | "legacy", configured = false) { const directory = await mkdtemp(join(tmpdir(), "stanp-install-")); directories.push(directory); const config = @@ -60,6 +61,27 @@ async function install(mode: "native" | "checksum" | "legacy") { ]); 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( @@ -165,7 +187,7 @@ exec sha256sum "$@" return { directory, config, env, result }; } -test("installs an importable native plugin under XDG without changing user configuration or running scripts", async () => { +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. @@ -174,7 +196,7 @@ test("installs an importable native plugin under XDG without changing user confi process.execPath, "-e", "const { default: plugin } = await import(process.argv[1]); console.log(JSON.stringify([plugin.id, plugin.setup()]));", - join(config, "plugins", "stanp-opencode.js"), + join(config, "plugins", "stanp-opencode"), ], directory, env, @@ -184,19 +206,53 @@ test("installs an importable native plugin under XDG without changing user confi stdout: '["stanp-opencode","native-ready"]\n', }); expect(await readFile(join(config, "opencode.jsonc"), "utf8")).toBe(configContents); - expect(await Bun.file(join(config, "plugins", "stanp-opencode")).exists()).toBe(false); + 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);