mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 20:21:39 +03:00
fix(cli): update npm installs through their own prefix (#63152)
This commit is contained in:
committed by
GitHub
Unverified
parent
63b42f6f02
commit
be6f5a367e
@@ -16,7 +16,8 @@ export default Runtime.handler(
|
||||
const method = Option.getOrUndefined(input.method) ?? (yield* updater.method())
|
||||
if (!method)
|
||||
return yield* Effect.fail(
|
||||
new Error("Could not detect the installation method. Pass --method to choose how to upgrade OpenCode."),
|
||||
(yield* updater.managed(Option.getOrUndefined(input.target))) ??
|
||||
new Error("Could not detect the installation method. Pass --method to choose how to upgrade OpenCode."),
|
||||
)
|
||||
|
||||
log.info(`Using method: ${method}`)
|
||||
|
||||
@@ -44,6 +44,9 @@ export class UpgradeError extends Error {
|
||||
const decodeVpPackages = Schema.decodeUnknownOption(
|
||||
Schema.fromJsonString(Schema.Array(Schema.Struct({ name: Schema.String }))),
|
||||
)
|
||||
const decodeManifestVersion = Schema.decodeUnknownOption(
|
||||
Schema.fromJsonString(Schema.Struct({ version: Schema.String })),
|
||||
)
|
||||
|
||||
const installNames: Record<Method, string> = {
|
||||
curl: "The OpenCode installer",
|
||||
@@ -110,6 +113,8 @@ export interface Interface {
|
||||
readonly check: () => Effect.Effect<CheckResult | undefined, Error>
|
||||
readonly apply: (version: string) => Effect.Effect<void, Error>
|
||||
readonly method: () => Effect.Effect<Method | undefined>
|
||||
/** Explains how to update a copy that Volta or mise manages, which `method` leaves undetected. */
|
||||
readonly managed: (version?: string) => Effect.Effect<UpgradeError | undefined, Error>
|
||||
readonly latest: (method?: Method) => Effect.Effect<string, Error>
|
||||
readonly upgrade: (method: Method, version: string) => Effect.Effect<void, Error>
|
||||
readonly removal: (
|
||||
@@ -160,6 +165,35 @@ const make = Effect.gen(function* () {
|
||||
)
|
||||
// WinGet extracts portable packages into a directory named after the package identifier.
|
||||
const installedWinget = /[\\/]winget[\\/]packages[\\/]anomaly\.opencode_/i.test(executable)
|
||||
// Volta packages (tools/image/packages) and mise's npm backend (installs/npm-<name>) keep their own record of the
|
||||
// installed version, so updating the copy in place would leave it stale. npm globals under a Volta or mise Node
|
||||
// (tools/image/node, installs/node) are ordinary npm installs.
|
||||
const installedOwner = !installedPackage
|
||||
? undefined
|
||||
: /[\\/]tools[\\/]image[\\/]packages[\\/]/.test(executable)
|
||||
? { name: "Volta", command: "volta install " }
|
||||
: /[\\/]installs[\\/]npm-[^\\/]+[\\/]/.test(executable)
|
||||
? { name: "mise", command: "mise use -g npm:" }
|
||||
: undefined
|
||||
// npm keeps global packages in <prefix>/lib/node_modules on POSIX. The npm on PATH may belong to another Node
|
||||
// install (fnm, nvm, mise) or prefix, so target the prefix that owns the running binary. Windows uses
|
||||
// <prefix>\node_modules, which Bun and pnpm global installs share, so it keeps the npm on PATH.
|
||||
const packageDirectory = path.dirname(path.dirname(executable))
|
||||
const npmPackageDirectory = installedPackage ? path.join("/lib/node_modules", installedPackage) : undefined
|
||||
const npmPrefix =
|
||||
npmPackageDirectory &&
|
||||
!installedOwner &&
|
||||
process.platform !== "win32" &&
|
||||
packageDirectory.endsWith(npmPackageDirectory)
|
||||
? packageDirectory.slice(0, -npmPackageDirectory.length) || undefined
|
||||
: undefined
|
||||
const npmPrefixArgs = npmPrefix ? ["--prefix", npmPrefix] : []
|
||||
const npmBin = npmPrefix ? path.join(npmPrefix, "bin") : undefined
|
||||
const prefixNpm =
|
||||
npmBin && (yield* fs.exists(path.join(npmBin, "npm")).pipe(Effect.orElseSucceed(() => false)))
|
||||
? path.join(npmBin, "npm")
|
||||
: undefined
|
||||
const npm = prefixNpm ?? "npm"
|
||||
|
||||
const readPolicy = Effect.fnUntraced(function* () {
|
||||
const values = yield* Effect.forEach(["config.json", "opencode.json", "opencode.jsonc"], (name) =>
|
||||
@@ -172,8 +206,14 @@ const make = Effect.gen(function* () {
|
||||
})
|
||||
|
||||
const exec = Effect.fnUntraced(function* (command: string[], timeout: Duration.Input = "30 seconds") {
|
||||
// The prefix's npm and the package's postinstall run `node` from PATH, which may be missing or belong to another
|
||||
// Node install, so put the prefix's own node first.
|
||||
const options =
|
||||
command[0] === prefixNpm
|
||||
? { env: { PATH: [npmBin, process.env.PATH].filter(Boolean).join(path.delimiter) }, extendEnv: true }
|
||||
: undefined
|
||||
return yield* appProcess
|
||||
.run(ChildProcess.make(command[0], command.slice(1)), {
|
||||
.run(ChildProcess.make(command[0], command.slice(1), options), {
|
||||
timeout,
|
||||
maxOutputBytes: 100_000,
|
||||
maxErrorBytes: 100_000,
|
||||
@@ -215,11 +255,11 @@ const make = Effect.gen(function* () {
|
||||
if (executable === curlBinary) return "curl"
|
||||
if (installedFormula) return "brew"
|
||||
if (installedWinget) return "winget"
|
||||
if (!installedPackage) return
|
||||
if (!installedPackage || installedOwner) return
|
||||
|
||||
const parts = executable.toLowerCase().split(path.sep).map((part) => part.replace(/^\./, ""))
|
||||
const checks: Array<{ method: Method; command: string[] }> = [
|
||||
{ method: "npm", command: ["npm", "list", "-g", "--depth=0", installedPackage] },
|
||||
{ method: "npm", command: [npm, "list", "-g", "--depth=0", ...npmPrefixArgs, installedPackage] },
|
||||
{ method: "pnpm", command: ["pnpm", "list", "-g", "--depth=0", installedPackage] },
|
||||
{ method: "bun", command: ["bun", "pm", "ls", "-g"] },
|
||||
{ method: "yarn", command: ["yarn", "global", "list"] },
|
||||
@@ -248,7 +288,7 @@ const make = Effect.gen(function* () {
|
||||
if (method === "winget") return ["winget", "uninstall", ...wingetArgs]
|
||||
if (method === "curl" || method === "brew" || !installedPackage) return undefined
|
||||
const commands = {
|
||||
npm: ["npm", "uninstall", "--global", installedPackage],
|
||||
npm: [npm, "uninstall", "--global", ...npmPrefixArgs, installedPackage],
|
||||
pnpm: ["pnpm", "remove", "--global", installedPackage],
|
||||
bun: ["bun", "remove", "--global", installedPackage],
|
||||
yarn: ["yarn", "global", "remove", installedPackage],
|
||||
@@ -365,6 +405,24 @@ const make = Effect.gen(function* () {
|
||||
),
|
||||
)
|
||||
|
||||
const managedError = (owner: { name: string; command: string }, target: string) =>
|
||||
new UpgradeError({
|
||||
title: `OpenCode is managed by ${owner.name}`,
|
||||
detail: `This copy was installed with ${owner.name}, which keeps its own record of the installed version, so OpenCode won't update it directly.`,
|
||||
command: `${owner.command}${target}`,
|
||||
retry: "Run the command above, then restart OpenCode.",
|
||||
})
|
||||
|
||||
const managedMessage = (error: UpgradeError) => `${error.title}. Update it with ${error.command}`
|
||||
|
||||
const managed = Effect.fnUntraced(function* (input?: string) {
|
||||
if (!installedOwner) return
|
||||
if (input !== undefined && !parseReleaseVersion(input))
|
||||
return yield* Effect.fail(new Error(`Invalid version: ${input}`))
|
||||
const data = yield* release("npm")
|
||||
return managedError(installedOwner, `${data.package}@${input?.trim().replace(/^v/, "") ?? data.version}`)
|
||||
})
|
||||
|
||||
const temporaryDirectory = (prefix: string) =>
|
||||
Effect.acquireRelease(fs.makeTempDirectory({ directory: global.cache, prefix }), (directory) =>
|
||||
fs.remove(directory, { recursive: true, force: true }).pipe(Effect.ignore),
|
||||
@@ -438,9 +496,10 @@ const make = Effect.gen(function* () {
|
||||
const commands: Record<Exclude<Method, "bun" | "curl" | "brew" | "winget">, string[]> = {
|
||||
// Keep the old package: uninstalling it can unlink the replacement command.
|
||||
npm: [
|
||||
"npm",
|
||||
npm,
|
||||
"install",
|
||||
"--global",
|
||||
...npmPrefixArgs,
|
||||
`--allow-scripts=${packageName}`,
|
||||
...((OPENCODE_ARTIFACT === "cli" && !installedPackage?.endsWith("/cli-node")) ||
|
||||
(installedPackage && packageName !== installedPackage)
|
||||
@@ -527,8 +586,22 @@ const make = Effect.gen(function* () {
|
||||
{ cause },
|
||||
),
|
||||
),
|
||||
Effect.asVoid,
|
||||
)
|
||||
// A package manager can succeed while updating a different copy, for example another prefix. pnpm keeps each
|
||||
// version in its own store directory, so the running copy never changes there.
|
||||
if (installedPackage !== packageName || !(method === "npm" || method === "bun" || method === "yarn")) return
|
||||
const installed = yield* fs.readFileString(path.join(packageDirectory, "package.json")).pipe(
|
||||
Effect.map((text) => Option.getOrUndefined(decodeManifestVersion(text))?.version),
|
||||
Effect.orElseSucceed(() => undefined),
|
||||
)
|
||||
if (installed !== version)
|
||||
return yield* Effect.fail(
|
||||
new UpgradeError({
|
||||
title: `${installNames[method]} did not update this copy of OpenCode`,
|
||||
detail: `${installNames[method]} reported success, but ${executable} is still version ${installed ?? "unknown"}.`,
|
||||
retry: `Update the copy at ${executable} with the package manager that installed it, then run opencode upgrade again.`,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const inspect = Effect.fnUntraced(function* () {
|
||||
@@ -575,12 +648,15 @@ const make = Effect.gen(function* () {
|
||||
})
|
||||
|
||||
const apply = Effect.fn("cli.updater.apply")(function* (version: string) {
|
||||
if (!(yield* install(version)))
|
||||
return yield* Effect.fail(
|
||||
new Error(
|
||||
"Could not detect the installation method. Update with your package manager or download the latest release.",
|
||||
),
|
||||
)
|
||||
if (yield* install(version)) return
|
||||
const error = yield* managed(version)
|
||||
return yield* Effect.fail(
|
||||
new Error(
|
||||
error
|
||||
? managedMessage(error)
|
||||
: "Could not detect the installation method. Update with your package manager or download the latest release.",
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
const check = Effect.fn("cli.updater.check")(function* () {
|
||||
@@ -590,6 +666,14 @@ const make = Effect.gen(function* () {
|
||||
message: "This build runs from a source checkout. Use an installed OpenCode release to check for updates.",
|
||||
}
|
||||
const detected = yield* method()
|
||||
if (!detected && installedOwner) {
|
||||
const data = yield* release("npm")
|
||||
if (action(OPENCODE_VERSION, data.version, "auto") === "none") return undefined
|
||||
return {
|
||||
type: "unavailable" as const,
|
||||
message: managedMessage(managedError(installedOwner, `${data.package}@${data.version}`)),
|
||||
}
|
||||
}
|
||||
if (!detected)
|
||||
return {
|
||||
type: "unavailable" as const,
|
||||
@@ -620,7 +704,7 @@ const make = Effect.gen(function* () {
|
||||
Effect.catch((error) => Effect.logWarning("update check failed", { error }).pipe(Effect.as(undefined))),
|
||||
)
|
||||
|
||||
return Service.of({ run, check, apply, method, latest, upgrade, removal })
|
||||
return Service.of({ run, check, apply, method, managed, latest, upgrade, removal })
|
||||
})
|
||||
|
||||
export const layer = Layer.effect(Service, make)
|
||||
|
||||
@@ -16,6 +16,7 @@ await Effect.runPromise(
|
||||
check: () => Effect.die("Manual upgrades must not check for TUI updates"),
|
||||
apply: () => Effect.die("Manual upgrades must not apply TUI updates"),
|
||||
removal: () => undefined,
|
||||
managed: () => Effect.succeed(undefined),
|
||||
method: () =>
|
||||
Effect.sync(() => {
|
||||
record("method")
|
||||
|
||||
@@ -6,7 +6,7 @@ import { AppProcess } from "@opencode/util/process"
|
||||
import { expect, spyOn, test } from "bun:test"
|
||||
import { Effect, FileSystem, Layer, PlatformError, Stream } from "effect"
|
||||
import { ChildProcessSpawner } from "effect/process"
|
||||
import { existsSync, readdirSync, readFileSync } from "node:fs"
|
||||
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"
|
||||
import path from "node:path"
|
||||
import { Updater } from "../src/services/updater"
|
||||
import { testEffect } from "../../core/test/lib/effect"
|
||||
@@ -26,24 +26,28 @@ function fixture(
|
||||
failCleanup = false,
|
||||
releasePackage = name,
|
||||
formula?: string,
|
||||
executableOverride?: (root: string, home: string) => Effect.Effect<string, PlatformError.PlatformError, FileSystem.FileSystem>,
|
||||
executableOverride?: (
|
||||
root: string,
|
||||
home: string,
|
||||
) => Effect.Effect<string, PlatformError.PlatformError, FileSystem.FileSystem>,
|
||||
) {
|
||||
return Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||
const root = yield* fs.makeTempDirectoryScoped({ prefix: "opencode-updater-" })
|
||||
const execPath = process.execPath
|
||||
const modules = path.join(root, "node_modules")
|
||||
// npm's global layout on POSIX.
|
||||
const modules = path.join(root, "lib", "node_modules")
|
||||
const executable = executableOverride
|
||||
? yield* executableOverride(root, path.join(root, "home"))
|
||||
: formula
|
||||
? path.join(root, "Cellar", formula, "2.0.20", "bin", "opencode")
|
||||
: path.join(modules, "@opencode", "cli", "bin", "opencode")
|
||||
: path.join(modules, ...name.split("/"), "bin", "opencode")
|
||||
yield* fs.makeDirectory(path.dirname(executable), { recursive: true })
|
||||
yield* fs.writeFileString(executable, "binary")
|
||||
if (!formula && !executableOverride)
|
||||
yield* fs.writeFileString(
|
||||
path.join(modules, "@opencode", "cli", "package.json"),
|
||||
path.join(modules, ...name.split("/"), "package.json"),
|
||||
JSON.stringify({ name, bin: { opencode: "bin/opencode" } }),
|
||||
)
|
||||
const requests: string[] = []
|
||||
@@ -83,6 +87,8 @@ function fixture(
|
||||
yield* fs.writeFileString(bash, "bash")
|
||||
}
|
||||
const commands: string[][] = []
|
||||
const paths: Array<string | undefined> = []
|
||||
const manifest = path.join(modules, ...name.split("/"), "package.json")
|
||||
const updater = yield* Updater.Service.pipe(
|
||||
Effect.provide(Updater.layer),
|
||||
Effect.provide(
|
||||
@@ -116,10 +122,15 @@ function fixture(
|
||||
const args = command.args.map(String)
|
||||
const lookup = command.command === "git" && args[0] === "--exec-path"
|
||||
if (!lookup) commands.push([command.command, ...args])
|
||||
if (!lookup) paths.push(command.options.env?.PATH?.toString())
|
||||
const result = lookup
|
||||
? { stdout: Buffer.from(`${path.join(git, "mingw64", "libexec", "git-core")}\n`) }
|
||||
: respond({ command: command.command, args })
|
||||
if (result.error) return Effect.fail(result.error)
|
||||
// Successful installs replace the running copy, which the updater verifies afterwards.
|
||||
const installed = args.find((arg) => arg.startsWith(`${name}@`))?.slice(name.length + 1)
|
||||
if (!formula && !executableOverride && installed && (result.exitCode ?? 0) === 0)
|
||||
writeFileSync(manifest, JSON.stringify({ name, version: installed, bin: { opencode: "bin/opencode" } }))
|
||||
return Effect.succeed({
|
||||
command: command.command,
|
||||
exitCode: 0,
|
||||
@@ -134,7 +145,10 @@ function fixture(
|
||||
}),
|
||||
),
|
||||
)
|
||||
return { updater, commands, requests, global, fs, executable, bash }
|
||||
const prefix = yield* fs.realPath(root)
|
||||
// Windows keeps the npm on PATH and its default prefix.
|
||||
const prefixArgs = process.platform === "win32" ? [] : ["--prefix", prefix]
|
||||
return { updater, commands, paths, requests, global, fs, executable, bash, prefix, prefixArgs }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -144,14 +158,22 @@ const unix = process.platform === "win32" ? it.live.skip : it.live
|
||||
const installs = [
|
||||
{
|
||||
method: "npm",
|
||||
command: ["npm", "install", "--global", "--allow-scripts=@opencode/cli", "--force", "@opencode/cli@2.3.4-beta.1"],
|
||||
command: (prefixArgs: string[]) => [
|
||||
"npm",
|
||||
"install",
|
||||
"--global",
|
||||
...prefixArgs,
|
||||
"--allow-scripts=@opencode/cli",
|
||||
"--force",
|
||||
"@opencode/cli@2.3.4-beta.1",
|
||||
],
|
||||
},
|
||||
{
|
||||
method: "pnpm",
|
||||
command: ["pnpm", "add", "--global", "--allow-build=@opencode/cli", "@opencode/cli@2.3.4-beta.1"],
|
||||
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"] },
|
||||
{ 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 }) => {
|
||||
@@ -159,7 +181,138 @@ installs.forEach(({ method, command }) => {
|
||||
Effect.gen(function* () {
|
||||
const test = yield* fixture()
|
||||
yield* test.updater.upgrade(method, "v2.3.4-beta.1")
|
||||
expect(test.commands).toEqual([[...command]])
|
||||
expect(test.commands).toEqual([command(test.prefixArgs)])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
// Installs a 2.2.7 package at <root>/<directory>, optionally with an npm in <root>/bin.
|
||||
const installed = (directory: string[], npm = false) =>
|
||||
fixture(
|
||||
() => ({}),
|
||||
"@opencode/cli",
|
||||
false,
|
||||
"@opencode/cli",
|
||||
undefined,
|
||||
(root) =>
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const pkg = path.join(root, ...directory)
|
||||
yield* fs.makeDirectory(pkg, { recursive: true })
|
||||
yield* fs.writeFileString(
|
||||
path.join(pkg, "package.json"),
|
||||
JSON.stringify({ name: "@opencode/cli", version: "2.2.7", bin: { opencode: "bin/opencode" } }),
|
||||
)
|
||||
if (npm) {
|
||||
yield* fs.makeDirectory(path.join(root, "bin"), { recursive: true })
|
||||
yield* fs.writeFileString(path.join(root, "bin", "npm"), "npm")
|
||||
}
|
||||
return path.join(pkg, "bin", "opencode")
|
||||
}),
|
||||
)
|
||||
const npmGlobal = ["lib", "node_modules", "@opencode", "cli"]
|
||||
|
||||
// Version managers such as fnm give each Node install its own prefix, while the npm on PATH may belong to another one.
|
||||
unix("npm runs from the prefix that owns the running binary with that prefix's node first on PATH", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* installed(npmGlobal, true)
|
||||
const npm = path.join(test.prefix, "bin", "npm")
|
||||
yield* test.updater.method()
|
||||
yield* test.updater.upgrade("npm", "2.2.7")
|
||||
const removal = test.updater.removal("npm")
|
||||
if (!removal) return yield* Effect.die("Expected npm removal command")
|
||||
yield* removal.run
|
||||
expect(test.commands[0]).toEqual([npm, "list", "-g", "--depth=0", "--prefix", test.prefix, "@opencode/cli"])
|
||||
expect(test.commands.slice(5).map((command) => command.slice(0, 5))).toEqual([
|
||||
[npm, "install", "--global", "--prefix", test.prefix],
|
||||
[npm, "uninstall", "--global", "--prefix", test.prefix],
|
||||
])
|
||||
const first = path.join(test.prefix, "bin") + path.delimiter
|
||||
expect(test.paths.map((value) => value?.startsWith(first) ?? false)).toEqual([
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
unix("npm only derives a prefix when the running package is that prefix's global package", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* installed(["lib", "node_modules", "wrapper", "node_modules", "@opencode", "cli"], true)
|
||||
yield* test.updater.method()
|
||||
expect(test.commands[0]).toEqual(["npm", "list", "-g", "--depth=0", "@opencode/cli"])
|
||||
expect(test.paths[0]).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("npm upgrades fail when the running copy keeps its old version", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* installed(npmGlobal)
|
||||
const error = yield* test.updater.upgrade("npm", "2.3.4").pipe(Effect.flip)
|
||||
expect(test.commands).toHaveLength(1)
|
||||
expect(error).toBeInstanceOf(Updater.UpgradeError)
|
||||
expect(error.message).toBe(
|
||||
`npm reported success, but ${yield* test.fs.realPath(test.executable)} is still version 2.2.7.`,
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
const managers = [
|
||||
{
|
||||
name: "Volta",
|
||||
label: "~/.volta",
|
||||
directory: [".volta", "tools", "image", "packages", "@opencode", "cli", ...npmGlobal],
|
||||
command: "volta install @opencode/cli",
|
||||
},
|
||||
{
|
||||
name: "Volta",
|
||||
label: "custom VOLTA_HOME",
|
||||
directory: ["volta-home", "tools", "image", "packages", "@opencode", "cli", ...npmGlobal],
|
||||
command: "volta install @opencode/cli",
|
||||
},
|
||||
{
|
||||
name: "mise",
|
||||
label: "mise npm backend",
|
||||
directory: ["mise", "installs", "npm-opencode-cli", "2.2.7", ...npmGlobal],
|
||||
command: "mise use -g npm:@opencode/cli",
|
||||
},
|
||||
] as const
|
||||
|
||||
managers.forEach((manager) => {
|
||||
it.live(`${manager.label} installs print the ${manager.name} command instead of updating in place`, () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* installed([...manager.directory], true)
|
||||
expect(yield* test.updater.method()).toBeUndefined()
|
||||
const latest = yield* test.updater.managed()
|
||||
const pinned = yield* test.updater.managed("v2.2.6")
|
||||
expect(latest?.title).toBe(`OpenCode is managed by ${manager.name}`)
|
||||
expect(latest?.command).toBe(`${manager.command}@2.3.4`)
|
||||
expect(pinned?.command).toBe(`${manager.command}@2.2.6`)
|
||||
const applied = yield* test.updater.apply("2.3.4").pipe(Effect.flip)
|
||||
expect(applied.message).toBe(`OpenCode is managed by ${manager.name}. Update it with ${manager.command}@2.3.4`)
|
||||
// An explicit --method keeps its existing command, and the version check reports the untouched copy.
|
||||
const explicit = yield* test.updater.upgrade("npm", "2.3.4").pipe(Effect.flip)
|
||||
expect(explicit.message).toContain("is still version 2.2.7")
|
||||
expect(test.commands).toEqual([
|
||||
["npm", "install", "--global", "--allow-scripts=@opencode/cli", "--force", "@opencode/cli@2.3.4"],
|
||||
])
|
||||
}),
|
||||
)
|
||||
})
|
||||
;[
|
||||
["mise", "installs", "node", "24.22.2"],
|
||||
[".volta", "tools", "image", "node", "22.23.4"],
|
||||
].forEach((node) => {
|
||||
it.live(`npm globals under ${node.slice(0, 3).join("/")} stay npm installs`, () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* installed([...node, ...npmGlobal])
|
||||
expect(yield* test.updater.managed()).toBeUndefined()
|
||||
yield* test.updater.method()
|
||||
expect(test.commands).toHaveLength(5)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -285,7 +438,7 @@ it.live("install failures expose stderr and process errors do not report success
|
||||
}))
|
||||
expect(yield* test.updater.method()).toBe(method)
|
||||
expect(test.commands).toEqual([
|
||||
["npm", "list", "-g", "--depth=0", "@opencode/cli"],
|
||||
["npm", "list", "-g", "--depth=0", ...test.prefixArgs, "@opencode/cli"],
|
||||
["pnpm", "list", "-g", "--depth=0", "@opencode/cli"],
|
||||
["bun", "pm", "ls", "-g"],
|
||||
["yarn", "global", "list"],
|
||||
@@ -592,12 +745,19 @@ if (typeof OPENCODE_CLI_NAME === "string" && OPENCODE_CLI_NAME === "opencode2-no
|
||||
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"],
|
||||
["npm", "list", "-g", "--depth=0", ...test.prefixArgs, "@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", "--allow-scripts=@opencode/cli-node", "@opencode/cli-node@2.3.4"],
|
||||
[
|
||||
"npm",
|
||||
"install",
|
||||
"--global",
|
||||
...test.prefixArgs,
|
||||
"--allow-scripts=@opencode/cli-node",
|
||||
"@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"],
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user