From a556b81d75af960fec546d3b839188d29e5776f0 Mon Sep 17 00:00:00 2001 From: msmps <7691252+msmps@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:34:16 +0100 Subject: [PATCH 1/2] feat(cli): support vite+ installations --- packages/cli/src/services/updater.ts | 22 ++++++-- packages/cli/test/updater-install.test.ts | 50 +++++++++++++++++-- packages/cli/test/upgrade.test.ts | 6 +++ .../www/src/docs/content/cli/commands.mdx | 1 + services/www/src/docs/content/index.mdx | 5 +- 5 files changed, 75 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/services/updater.ts b/packages/cli/src/services/updater.ts index 530929c7e93e..817a61b48fee 100644 --- a/packages/cli/src/services/updater.ts +++ b/packages/cli/src/services/updater.ts @@ -1,17 +1,21 @@ import { Global } from "@opencode/util/global" import { AppProcess } from "@opencode/util/process" import { OPENCODE_ARTIFACT, OPENCODE_CHANNEL, OPENCODE_LOCAL, OPENCODE_VERSION } from "../version" -import { Context, Duration, Effect, FileSystem, Layer, Ref } from "effect" +import { Context, Duration, Effect, FileSystem, Layer, Option, Ref, Schema } from "effect" import { ChildProcess } from "effect/unstable/process" import { parse, type ParseError } from "jsonc-parser" import path from "node:path" import { action, parseReleaseVersion, type Policy } from "./updater-action" -export const methods = ["curl", "npm", "pnpm", "bun", "yarn"] as const +export const methods = ["curl", "npm", "pnpm", "bun", "yarn", "vp"] as const export type Method = (typeof methods)[number] export type RunResult = { readonly type: "available" | "installed"; readonly version: string } export type CheckResult = RunResult | { readonly type: "unavailable"; readonly message: string } +const decodeVpPackages = Schema.decodeUnknownOption( + Schema.fromJsonString(Schema.Array(Schema.Struct({ name: Schema.String }))), +) + export interface Interface { readonly run: () => Effect.Effect readonly check: () => Effect.Effect @@ -103,13 +107,20 @@ const make = Effect.gen(function* () { { method: "pnpm", command: ["pnpm", "list", "-g", "--depth=0", installedPackage] }, { method: "bun", command: ["bun", "pm", "ls", "-g"] }, { method: "yarn", command: ["yarn", "global", "list"] }, + { method: "vp", command: ["vp", "list", "-g", "--json", installedPackage] }, ] const results = yield* Effect.forEach( checks, (check) => exec(check.command).pipe(Effect.map((result) => ({ check, result }))), { concurrency: "unbounded" }, ) - return results.find((result) => result.result.stdout.includes(installedPackage))?.check.method + return results.find((result) => { + if (result.check.method !== "vp") return result.result.stdout.includes(installedPackage) + // Vite+ repeats the filter in its successful no-match message, so substring detection would be a false positive. + return Option.exists(decodeVpPackages(result.result.stdout), (packages) => + packages.some((item) => item.name === installedPackage), + ) + })?.check.method }) const removal = (method: Method) => { @@ -119,6 +130,7 @@ const make = Effect.gen(function* () { pnpm: ["pnpm", "remove", "--global", installedPackage], bun: ["bun", "remove", "--global", installedPackage], yarn: ["yarn", "global", "remove", installedPackage], + vp: ["vp", "uninstall", "-g", installedPackage], } const command = commands[method] return { @@ -182,6 +194,10 @@ const make = Effect.gen(function* () { ], pnpm: ["pnpm", "add", "--global", `--allow-build=${packageName}`, target], yarn: ["yarn", "global", "add", target], + vp: + installedPackage && packageName !== installedPackage + ? ["vp", "install", "-g", "--force", target] + : ["vp", "update", "-g", target], } const result = yield* Effect.scoped( Effect.gen(function* () { diff --git a/packages/cli/test/updater-install.test.ts b/packages/cli/test/updater-install.test.ts index dc7085aa4603..0d55fbab91fb 100644 --- a/packages/cli/test/updater-install.test.ts +++ b/packages/cli/test/updater-install.test.ts @@ -19,6 +19,7 @@ function fixture( } = () => ({}), name = "@opencode/cli", failCleanup = false, + releasePackage = name, ) { return Effect.gen(function* () { const fs = yield* FileSystem.FileSystem @@ -34,7 +35,7 @@ function fixture( yield* Effect.acquireRelease( Effect.sync(() => spyOn(globalThis, "fetch").mockImplementation( - Object.assign(async () => Response.json({ version: "2.3.4", metadata: { package: name } }), { + Object.assign(async () => Response.json({ version: "2.3.4", metadata: { package: releasePackage } }), { preconnect: fetch.preconnect, }), ), @@ -106,6 +107,7 @@ const installs = [ command: ["pnpm", "add", "--global", "--allow-build=@opencode/cli", "@opencode/cli@2.3.4-beta.1"], }, { method: "yarn", command: ["yarn", "global", "add", "@opencode/cli@2.3.4-beta.1"] }, + { method: "vp", command: ["vp", "update", "-g", "@opencode/cli@2.3.4-beta.1"] }, ] as const installs.forEach(({ method, command }) => { @@ -117,6 +119,25 @@ installs.forEach(({ method, command }) => { }), ) }) + +it.live("vp force-installs a renamed V2 package that replaces the existing binary owner", () => + Effect.gen(function* () { + const test = yield* fixture(() => ({}), "@opencode/cli", false, "@opencode/cli-node") + yield* test.updater.upgrade("vp", "v2.3.4-beta.1") + expect(test.commands).toEqual([["vp", "install", "-g", "--force", "@opencode/cli-node@2.3.4-beta.1"]]) + }), +) + +it.live("vp removes the package from its managed global store", () => + Effect.gen(function* () { + const test = yield* fixture() + const removal = test.updater.removal("vp") + if (!removal) return yield* Effect.die("Expected vp removal command") + expect(removal.command).toEqual(["vp", "uninstall", "-g", "@opencode/cli"]) + yield* removal.run + expect(test.commands).toEqual([["vp", "uninstall", "-g", "@opencode/cli"]]) + }), +) ;[0, 1].forEach((exitCode) => { it.live(`bun isolates and removes its install cache after exit ${exitCode}`, () => Effect.gen(function* () { @@ -200,11 +221,19 @@ it.live("install failures expose stderr and process errors do not report success expect(missing.commands).toHaveLength(1) }), ) -;(["npm", "pnpm", "bun", "yarn", undefined] as const).forEach((method) => { +;(["npm", "pnpm", "bun", "yarn", "vp", undefined] as const).forEach((method) => { it.live(`method detection identifies ${method ?? "an unknown installation"} using the V2 package`, () => Effect.gen(function* () { const test = yield* fixture((command) => ({ - stdout: Buffer.from(command.command === method ? "@opencode/cli@2.3.4" : "opencode-ai@1.0.0"), + stdout: Buffer.from( + command.command === method + ? method === "vp" + ? JSON.stringify([{ name: "@opencode/cli", version: "2.3.4" }]) + : "@opencode/cli@2.3.4" + : command.command === "vp" + ? "[]" + : "opencode-ai@1.0.0", + ), })) expect(yield* test.updater.method()).toBe(method) expect(test.commands).toEqual([ @@ -212,6 +241,7 @@ it.live("install failures expose stderr and process errors do not report success ["pnpm", "list", "-g", "--depth=0", "@opencode/cli"], ["bun", "pm", "ls", "-g"], ["yarn", "global", "list"], + ["vp", "list", "-g", "--json", "@opencode/cli"], ]) }), ) @@ -225,7 +255,16 @@ it.live("method detection tolerates unavailable package managers", () => : { error: new AppProcess.AppProcessError({ command: command.command }) }, ) expect(yield* test.updater.method()).toBe("yarn") - expect(test.commands).toHaveLength(4) + expect(test.commands).toHaveLength(5) + }), +) + +it.live("vp detection ignores no-match output that repeats the package name", () => + Effect.gen(function* () { + const test = yield* fixture((command) => ({ + stdout: Buffer.from(command.command === "vp" ? "No global packages matching '@opencode/cli'." : ""), + })) + expect(yield* test.updater.method()).toBeUndefined() }), ) @@ -265,13 +304,16 @@ if (typeof OPENCODE_CLI_NAME === "string" && OPENCODE_CLI_NAME === "opencode2-no expect(yield* test.updater.method()).toBe("npm") yield* test.updater.upgrade("npm", "v2.3.4") yield* test.updater.upgrade("pnpm", "v2.3.4") + yield* test.updater.upgrade("vp", "v2.3.4") expect(test.commands).toEqual([ ["npm", "list", "-g", "--depth=0", "@opencode/cli-node"], ["pnpm", "list", "-g", "--depth=0", "@opencode/cli-node"], ["bun", "pm", "ls", "-g"], ["yarn", "global", "list"], + ["vp", "list", "-g", "--json", "@opencode/cli-node"], ["npm", "install", "--global", "@opencode/cli-node@2.3.4"], ["pnpm", "add", "--global", "--allow-build=@opencode/cli-node", "@opencode/cli-node@2.3.4"], + ["vp", "update", "-g", "@opencode/cli-node@2.3.4"], ]) }), ) diff --git a/packages/cli/test/upgrade.test.ts b/packages/cli/test/upgrade.test.ts index a9b773db5a0d..4eaf6f5a37d0 100644 --- a/packages/cli/test/upgrade.test.ts +++ b/packages/cli/test/upgrade.test.ts @@ -35,6 +35,12 @@ describe("upgrade command", () => { expect(result.events).toEqual([{ method: "bun", version: "2.0.0" }]) }) + test("accepts vp as an explicit installation method", async () => { + const result = await cli(["2.0.0", "--method", "vp"]) + expect(result.exitCode).toBe(0) + expect(result.events).toEqual([{ method: "vp", version: "2.0.0" }]) + }) + test("skips the already installed version", async () => { const result = await cli(["v0.0.0-beta-old"]) expect(result.exitCode).toBe(0) diff --git a/services/www/src/docs/content/cli/commands.mdx b/services/www/src/docs/content/cli/commands.mdx index 69d118c65297..c10cb372509b 100644 --- a/services/www/src/docs/content/cli/commands.mdx +++ b/services/www/src/docs/content/cli/commands.mdx @@ -540,6 +540,7 @@ Upgrade to a specific version with a specific package manager. ```bash $ opencode upgrade 1.18.15 --method bun +$ opencode upgrade 1.18.15 --method vp ``` View all subcommands and flags. diff --git a/services/www/src/docs/content/index.mdx b/services/www/src/docs/content/index.mdx index 4c7a518a72ce..c5c68c392648 100644 --- a/services/www/src/docs/content/index.mdx +++ b/services/www/src/docs/content/index.mdx @@ -17,12 +17,13 @@ OpenCode is an open source AI coding agent. It’s available as a terminal-based { label: "bun", code: "bun install -g --trust @opencode/cli" }, { label: "pnpm", code: "pnpm add -g --allow-build=@opencode/cli @opencode/cli" }, { label: "yarn", code: "yarn global add @opencode/cli" }, + { label: "vite+", code: "vp install -g @opencode/cli" }, { label: "aur", code: "paru -S opencode-beta" }, ]} /> -The npm package uses a trusted postinstall script to select the native `opencode` binary for your platform. The Bun and pnpm -commands above explicitly allow that script to run. +The npm package uses a postinstall script to select the native `opencode` binary for your platform. The Bun and pnpm commands +explicitly allow the required script; Vite+ requires no additional flag. Windows package managers are not supported. From 501b354384e2623ad77b49bc0de6a1d5884deb48 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 19 Sep 2026 12:08:16 -0500 Subject: [PATCH 2/2] remove redundant example Removed outdated command for upgrading to a specific version using 'vp' method. --- services/www/src/docs/content/cli/commands.mdx | 1 - 1 file changed, 1 deletion(-) diff --git a/services/www/src/docs/content/cli/commands.mdx b/services/www/src/docs/content/cli/commands.mdx index c10cb372509b..69d118c65297 100644 --- a/services/www/src/docs/content/cli/commands.mdx +++ b/services/www/src/docs/content/cli/commands.mdx @@ -540,7 +540,6 @@ Upgrade to a specific version with a specific package manager. ```bash $ opencode upgrade 1.18.15 --method bun -$ opencode upgrade 1.18.15 --method vp ``` View all subcommands and flags.