diff --git a/packages/opencode/src/bun/index.ts b/packages/opencode/src/bun/index.ts index fe2f0dec371..c979c3514c4 100644 --- a/packages/opencode/src/bun/index.ts +++ b/packages/opencode/src/bun/index.ts @@ -60,10 +60,16 @@ export namespace BunProc { }), ) - export async function install(pkg: string, version = "latest") { + export async function install(pkg: string, version?: string) { // Use lock to ensure only one install at a time using _ = await Lock.write("bun-install") + const isGithub = pkg.startsWith("github:") + // GitHub: default to "main" when no version; npm: default to "latest" + const pkgVersion = version ?? (isGithub ? "main" : "latest") + // Use # for GitHub, @ for npm + const pkgArg = isGithub ? `${pkg}#${pkgVersion}` : `${pkg}@${pkgVersion}` + const mod = path.join(Global.Path.cache, "node_modules", pkg) const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json")) const parsed = await pkgjson.json().catch(async () => { @@ -71,7 +77,7 @@ export namespace BunProc { await Bun.write(pkgjson.name!, JSON.stringify(result, null, 2)) return result }) - if (parsed.dependencies[pkg] === version) return mod + if (parsed.dependencies[pkg] === pkgVersion) return mod const proxied = !!( process.env.HTTP_PROXY || @@ -89,7 +95,7 @@ export namespace BunProc { ...(proxied ? ["--no-cache"] : []), "--cwd", Global.Path.cache, - pkg + "@" + version, + pkgArg, ] // Let Bun handle registry resolution: @@ -98,32 +104,32 @@ export namespace BunProc { // - No need to pass --registry flag log.info("installing package using Bun's default registry resolution", { pkg, - version, + version: pkgVersion, }) await BunProc.run(args, { cwd: Global.Path.cache, }).catch((e) => { throw new InstallFailedError( - { pkg, version }, + { pkg, version: pkgVersion }, { cause: e, }, ) }) - // Resolve actual version from installed package when using "latest" + // For npm packages with "latest", resolve to actual version from installed package // This ensures subsequent starts use the cached version until explicitly updated - let resolvedVersion = version - if (version === "latest") { + let finalVersion = pkgVersion + if (version === undefined && !isGithub) { const installedPkgJson = Bun.file(path.join(mod, "package.json")) const installedPkg = await installedPkgJson.json().catch(() => null) if (installedPkg?.version) { - resolvedVersion = installedPkg.version + finalVersion = installedPkg.version } } - parsed.dependencies[pkg] = resolvedVersion + parsed.dependencies[pkg] = finalVersion await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2)) return mod } diff --git a/packages/opencode/test/bun.test.ts b/packages/opencode/test/bun.test.ts index d607ae47820..9824101ae60 100644 --- a/packages/opencode/test/bun.test.ts +++ b/packages/opencode/test/bun.test.ts @@ -43,11 +43,52 @@ describe("BunProc registry configuration", () => { expect(installFunction).toContain('"--exact"') expect(installFunction).toContain('"--cwd"') expect(installFunction).toContain("Global.Path.cache") - expect(installFunction).toContain('pkg + "@" + version') + + // Verify package argument structure - supports both regular and GitHub URLs + // Regular packages: pkg@version + // GitHub URLs: github:user/repo#version + expect(installFunction).toContain("isGithub") + expect(installFunction).toContain("#") + expect(installFunction).toContain("github:") // Verify no registry argument is added expect(installFunction).not.toContain('"--registry"') expect(installFunction).not.toContain('args.push("--registry') } }) + + test("should support GitHub URLs with # version separator", async () => { + // Read the bun/index.ts file + const bunIndexPath = path.join(__dirname, "../src/bun/index.ts") + const content = await fs.readFile(bunIndexPath, "utf-8") + + // Extract the install function + const installFunctionMatch = content.match(/export async function install[\s\S]*?^ }/m) + expect(installFunctionMatch).toBeTruthy() + + if (installFunctionMatch) { + const installFunction = installFunctionMatch[0] + + // Verify GitHub URLs use # for version separator + expect(installFunction).toContain("github:") + expect(installFunction).toContain("#${pkgVersion}") + } + }) + + test("should default to main branch for GitHub, latest for npm when no version specified", async () => { + // Read the bun/index.ts file + const bunIndexPath = path.join(__dirname, "../src/bun/index.ts") + const content = await fs.readFile(bunIndexPath, "utf-8") + + // Extract the install function + const installFunctionMatch = content.match(/export async function install[\s\S]*?^ }/m) + expect(installFunctionMatch).toBeTruthy() + + if (installFunctionMatch) { + const installFunction = installFunctionMatch[0] + + // Verify version resolution + expect(installFunction).toContain('version ?? (isGithub ? "main" : "latest")') + } + }) }) diff --git a/packages/web/src/content/docs/plugins.mdx b/packages/web/src/content/docs/plugins.mdx index bf26744f6c4..8ef6fbfc8dc 100644 --- a/packages/web/src/content/docs/plugins.mdx +++ b/packages/web/src/content/docs/plugins.mdx @@ -11,7 +11,7 @@ For examples, check out the [plugins](/docs/ecosystem#plugins) created by the co ## Use a plugin -There are two ways to load plugins. +There are three ways to load plugins. --- @@ -39,6 +39,29 @@ Specify npm packages in your config file. Both regular and scoped npm packages are supported. +--- + +### From GitHub + +Install plugins directly from GitHub repositories using the `github:` prefix. + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "plugin": ["github:eznix86/myfancyplugin#main"] +} +``` + +The format is `github:owner/repo#ref` where `ref` can be: + +- A branch (e.g., `#main`, `#develop`) +- A tag (e.g., `#v1.0.0`) +- A commit hash (e.g., `#abc123def`) + +If no ref is specified, it defaults to `#main`. + +GitHub plugins are installed using Bun's [add-git](https://bun.com/docs/guides/install/add-git) command. + Browse available plugins in the [ecosystem](/docs/ecosystem#plugins). ---