mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-31 14:06:21 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e84a25369 |
@@ -1,6 +1,7 @@
|
||||
import { Argument, Flag, GlobalFlag } from "effect/unstable/cli"
|
||||
import { Schema } from "effect"
|
||||
import { Spec } from "../framework/spec"
|
||||
import { Updater } from "../services/updater"
|
||||
|
||||
export const PrintLogs = GlobalFlag.setting("print-logs")({
|
||||
flag: Flag.boolean("print-logs").pipe(
|
||||
@@ -56,6 +57,20 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
prompt: Flag.string("prompt").pipe(Flag.withDescription("Prompt to use"), Flag.optional),
|
||||
},
|
||||
commands: [
|
||||
Spec.make("upgrade", {
|
||||
description: "Upgrade OpenCode to the latest or a specific version",
|
||||
params: {
|
||||
target: Argument.string("target").pipe(
|
||||
Argument.withDescription("Version to upgrade to (with or without a leading v)"),
|
||||
Argument.optional,
|
||||
),
|
||||
method: Flag.choice("method", Updater.methods).pipe(
|
||||
Flag.withAlias("m"),
|
||||
Flag.withDescription("Installation method to use"),
|
||||
Flag.optional,
|
||||
),
|
||||
},
|
||||
}),
|
||||
Spec.make("acp", { description: "Start an Agent Client Protocol server" }),
|
||||
Spec.make("api", {
|
||||
description: "Make a request to the running server",
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { intro, log, outro, spinner } from "@clack/prompts"
|
||||
import { Effect, Option } from "effect"
|
||||
import { Commands } from "../commands"
|
||||
import { Runtime } from "../../framework/runtime"
|
||||
import { Updater } from "../../services/updater"
|
||||
import { handlePromptErrors } from "../../ui/prompt"
|
||||
import { OPENCODE_VERSION } from "../../version"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.upgrade,
|
||||
Effect.fn("cli.upgrade")(function* (input) {
|
||||
intro("Upgrade")
|
||||
const updater = yield* Updater.Service
|
||||
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."),
|
||||
)
|
||||
|
||||
log.info(`Using method: ${method}`)
|
||||
const target = Option.getOrUndefined(input.target) ?? (yield* updater.latest())
|
||||
const version = target.trim().replace(/^v/, "")
|
||||
if (version === OPENCODE_VERSION) {
|
||||
log.warn(`OpenCode upgrade skipped: ${version} is already installed`)
|
||||
outro("Done")
|
||||
return
|
||||
}
|
||||
|
||||
log.info(`From ${OPENCODE_VERSION} → ${version}`)
|
||||
const progress = spinner()
|
||||
progress.start("Upgrading...")
|
||||
yield* updater.upgrade(method, target).pipe(
|
||||
Effect.tap(() => Effect.sync(() => progress.stop("Upgrade complete"))),
|
||||
Effect.tapCause(() => Effect.sync(() => progress.stop("Upgrade failed", 1))),
|
||||
)
|
||||
outro("Done")
|
||||
}, handlePromptErrors),
|
||||
)
|
||||
@@ -17,6 +17,7 @@ import { CpuProfile } from "./cpu-profile"
|
||||
|
||||
const Handlers = Runtime.handlers(Commands, {
|
||||
$: () => import("./commands/handlers/default"),
|
||||
upgrade: () => import("./commands/handlers/upgrade"),
|
||||
acp: () => import("./commands/handlers/acp"),
|
||||
api: () => import("./commands/handlers/api"),
|
||||
auth: {
|
||||
|
||||
@@ -15,7 +15,7 @@ export function action(current: string, latest: string, policy: Policy): Action
|
||||
return policy === "notify" ? "notify" : "upgrade"
|
||||
}
|
||||
|
||||
function parseReleaseVersion(input: string) {
|
||||
export function parseReleaseVersion(input: string) {
|
||||
if (input.length > 256) return
|
||||
const match = input.trim().match(versionPattern)
|
||||
if (!match) return
|
||||
|
||||
@@ -5,19 +5,21 @@ import { Context, Duration, Effect, FileSystem, Layer } from "effect"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { parse, type ParseError } from "jsonc-parser"
|
||||
import path from "node:path"
|
||||
import { action, type Policy } from "./updater-action"
|
||||
import { action, parseReleaseVersion, type Policy } from "./updater-action"
|
||||
|
||||
declare const OPENCODE_CLI_NAME: string | undefined
|
||||
|
||||
type Method = "npm" | "pnpm" | "bun" | "yarn" | "curl"
|
||||
export const methods = ["curl", "npm", "pnpm", "bun", "yarn"] as const
|
||||
export type Method = (typeof methods)[number]
|
||||
|
||||
const packageName =
|
||||
typeof OPENCODE_CLI_NAME === "string" && OPENCODE_CLI_NAME === "opencode2-node"
|
||||
? OPENCODE_CLI_NAME
|
||||
: "@opencode-ai/cli"
|
||||
typeof OPENCODE_CLI_NAME === "string" && OPENCODE_CLI_NAME === "opencode2-node" ? "opencode-node" : "@opencode-ai/cli"
|
||||
|
||||
export interface Interface {
|
||||
readonly check: () => Effect.Effect<void>
|
||||
readonly method: () => Effect.Effect<Method | undefined>
|
||||
readonly latest: () => Effect.Effect<string, Error>
|
||||
readonly upgrade: (method: Method, version: string) => Effect.Effect<void, Error>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/cli/Updater") {}
|
||||
@@ -110,7 +112,9 @@ export const layer = Layer.effect(
|
||||
return data.version
|
||||
})
|
||||
|
||||
const upgrade = Effect.fnUntraced(function* (method: Method, version: string) {
|
||||
const upgrade = Effect.fnUntraced(function* (method: Method, input: string) {
|
||||
if (!parseReleaseVersion(input)) return yield* Effect.fail(new Error(`Invalid version: ${input}`))
|
||||
const version = input.trim().replace(/^v/, "")
|
||||
const target = `${packageName}@${version}`
|
||||
const commands: Record<Exclude<Method, "bun" | "curl">, string[]> = {
|
||||
npm: ["npm", "install", "--global", target],
|
||||
@@ -138,7 +142,7 @@ export const layer = Layer.effect(
|
||||
}
|
||||
return yield* run(commands[method], "5 minutes")
|
||||
}),
|
||||
)
|
||||
).pipe(Effect.mapError((cause) => new Error(`Failed to update with ${method}`, { cause })))
|
||||
if (result.code === 0) return
|
||||
return yield* Effect.fail(new Error(result.stderr.trim() || `Failed to update with ${method}`))
|
||||
})
|
||||
@@ -173,7 +177,7 @@ export const layer = Layer.effect(
|
||||
Effect.catchCause((cause) => Effect.logWarning("automatic update failed", { cause })),
|
||||
)
|
||||
|
||||
return Service.of({ check })
|
||||
return Service.of({ check, method, latest, upgrade })
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NodeServices } from "@effect/platform-node"
|
||||
import { Effect } from "effect"
|
||||
import { Command } from "effect/unstable/cli"
|
||||
import { Commands } from "../../src/commands/commands"
|
||||
import upgrade from "../../src/commands/handlers/upgrade"
|
||||
import { Updater } from "../../src/services/updater"
|
||||
|
||||
const record = (event: unknown) => console.log(`EVENT ${JSON.stringify(event)}`)
|
||||
|
||||
await Effect.runPromise(
|
||||
Command.runWith(Commands.commands.upgrade.spec.pipe(Command.withHandler(upgrade)), { version: "test" })(
|
||||
process.argv.slice(2),
|
||||
).pipe(
|
||||
Effect.provideService(Updater.Service, {
|
||||
check: () => Effect.die("Manual upgrades must not run the automatic update check"),
|
||||
method: () =>
|
||||
Effect.sync(() => {
|
||||
record("method")
|
||||
return Updater.methods.find((method) => method === (process.env.UPGRADE_TEST_METHOD ?? "npm"))
|
||||
}),
|
||||
latest: () =>
|
||||
Effect.suspend(() => {
|
||||
record("latest")
|
||||
return process.env.UPGRADE_TEST_LATEST_ERROR
|
||||
? Effect.fail(new Error("Update check failed"))
|
||||
: Effect.succeed("0.0.0-beta-new")
|
||||
}),
|
||||
upgrade: (method, version) =>
|
||||
Effect.suspend(() => {
|
||||
record({ method, version })
|
||||
return process.env.UPGRADE_TEST_INSTALL_ERROR ? Effect.fail(new Error("Permission denied")) : Effect.void
|
||||
}),
|
||||
}),
|
||||
Effect.provide(NodeServices.layer),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,227 @@
|
||||
import { NodeServices } from "@effect/platform-node"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { AppProcess } from "@opencode-ai/util/process"
|
||||
import { expect, test } from "bun:test"
|
||||
import { Effect, FileSystem, Stream } from "effect"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { existsSync } from "node:fs"
|
||||
import path from "node:path"
|
||||
import { Updater } from "../src/services/updater"
|
||||
import { testEffect } from "../../core/test/lib/effect"
|
||||
|
||||
const it = testEffect(NodeServices.layer)
|
||||
|
||||
declare const OPENCODE_CLI_NAME: string | undefined
|
||||
|
||||
function fixture(
|
||||
respond: (command: ChildProcess.StandardCommand) => Partial<AppProcess.RunResult> & {
|
||||
error?: AppProcess.AppProcessError
|
||||
} = () => ({}),
|
||||
) {
|
||||
return Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||
const root = yield* fs.makeTempDirectoryScoped({ prefix: "opencode-updater-" })
|
||||
const global = Global.make({
|
||||
home: path.join(root, "home"),
|
||||
data: path.join(root, "data"),
|
||||
cache: path.join(root, "cache"),
|
||||
config: path.join(root, "config"),
|
||||
state: path.join(root, "state"),
|
||||
tmp: path.join(root, "tmp"),
|
||||
bin: path.join(root, "bin"),
|
||||
log: path.join(root, "log"),
|
||||
repos: path.join(root, "repos"),
|
||||
})
|
||||
const commands: string[][] = []
|
||||
const updater = yield* Updater.Service.pipe(
|
||||
Effect.provide(Updater.layer),
|
||||
Effect.provideService(Global.Service, global),
|
||||
Effect.provideService(
|
||||
AppProcess.Service,
|
||||
AppProcess.Service.of({
|
||||
...spawner,
|
||||
run: (command) =>
|
||||
Effect.suspend(() => {
|
||||
if (command._tag !== "StandardCommand") return Effect.die("Unexpected piped install command")
|
||||
commands.push([command.command, ...command.args])
|
||||
const result = respond(command)
|
||||
if (result.error) return Effect.fail(result.error)
|
||||
return Effect.succeed({
|
||||
command: command.command,
|
||||
exitCode: 0,
|
||||
stdout: Buffer.alloc(0),
|
||||
stderr: Buffer.alloc(0),
|
||||
stdoutTruncated: false,
|
||||
stderrTruncated: false,
|
||||
...result,
|
||||
})
|
||||
}),
|
||||
runStream: () => Stream.die("Unexpected streaming install command"),
|
||||
}),
|
||||
),
|
||||
)
|
||||
return { updater, commands, global, fs }
|
||||
})
|
||||
}
|
||||
|
||||
const installs = [
|
||||
{ method: "npm", command: ["npm", "install", "--global", "@opencode-ai/cli@2.3.4-beta.1"] },
|
||||
{
|
||||
method: "pnpm",
|
||||
command: ["pnpm", "add", "--global", "--allow-build=@opencode-ai/cli", "@opencode-ai/cli@2.3.4-beta.1"],
|
||||
},
|
||||
{ method: "yarn", command: ["yarn", "global", "add", "@opencode-ai/cli@2.3.4-beta.1"] },
|
||||
] as const
|
||||
|
||||
installs.forEach(({ method, command }) => {
|
||||
it.live(`${method} installs the explicit V2 package version without a leading v`, () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* fixture()
|
||||
yield* test.updater.upgrade(method, "v2.3.4-beta.1")
|
||||
expect(test.commands).toEqual([[...command]])
|
||||
}),
|
||||
)
|
||||
})
|
||||
;[0, 1].forEach((exitCode) => {
|
||||
it.live(`bun isolates and removes its install cache after exit ${exitCode}`, () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* fixture((command) => {
|
||||
expect(command.command).toBe("bun")
|
||||
expect(existsSync(command.args[4])).toBe(true)
|
||||
return { exitCode, stderr: Buffer.from("bun install failed") }
|
||||
})
|
||||
const result = yield* test.updater.upgrade("bun", "v2.3.4-beta.1").pipe(Effect.flip, Effect.option)
|
||||
const cache = test.commands[0]?.[5]
|
||||
expect(cache).toStartWith(path.join(test.global.cache, "update-"))
|
||||
expect(test.commands).toEqual([
|
||||
["bun", "install", "--global", "--trust", "--cache-dir", cache, "@opencode-ai/cli@2.3.4-beta.1"],
|
||||
])
|
||||
expect(yield* test.fs.readDirectory(test.global.cache)).toEqual([])
|
||||
expect(result._tag).toBe(exitCode === 0 ? "None" : "Some")
|
||||
if (result._tag === "Some") expect(result.value.message).toBe("bun install failed")
|
||||
}),
|
||||
)
|
||||
})
|
||||
;["success", "download", "install"].forEach((failure) => {
|
||||
it.live(`curl uses the V2 installer and cleans its directory: ${failure}`, () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* fixture((command) => {
|
||||
const installer = command.command === "curl" ? command.args[2] : command.args[0]
|
||||
expect(existsSync(path.dirname(installer))).toBe(true)
|
||||
return {
|
||||
exitCode: command.command === (failure === "download" ? "curl" : failure === "install" ? "bash" : "") ? 1 : 0,
|
||||
stderr: Buffer.from(`${failure} failed`),
|
||||
}
|
||||
})
|
||||
const result = yield* test.updater.upgrade("curl", "v2.3.4-beta.1").pipe(Effect.flip, Effect.option)
|
||||
const installer = test.commands[0]?.[3]
|
||||
expect(installer).toStartWith(path.join(test.global.cache, "update-"))
|
||||
expect(test.commands).toEqual([
|
||||
["curl", "-fsSL", "-o", installer, "https://opencode.ai/v2/install"],
|
||||
...(failure === "download" ? [] : [["bash", installer, "--version", "2.3.4-beta.1", "--no-modify-path"]]),
|
||||
])
|
||||
expect(yield* test.fs.readDirectory(test.global.cache)).toEqual([])
|
||||
expect(result._tag).toBe(failure === "success" ? "None" : "Some")
|
||||
if (result._tag === "Some") expect(result.value.message).toBe(`${failure} failed`)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it.live("invalid version targets never execute a command or create a cache", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* fixture()
|
||||
yield* Effect.forEach(Updater.methods, (method) =>
|
||||
Effect.forEach(
|
||||
["", "latest", "2.3", "01.2.3", "vv2.3.4", "2.3.4; echo unsafe", "--global", "v2.3.4\n--force"],
|
||||
(version) =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* test.updater.upgrade(method, version).pipe(Effect.flip)
|
||||
expect(error.message).toBe(`Invalid version: ${version}`)
|
||||
}),
|
||||
),
|
||||
)
|
||||
expect(test.commands).toEqual([])
|
||||
expect(yield* test.fs.exists(test.global.cache)).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("install failures expose stderr and process errors do not report success", () =>
|
||||
Effect.gen(function* () {
|
||||
const failed = yield* fixture(() => ({ exitCode: 1, stderr: Buffer.from(" registry denied access\n") }))
|
||||
const error = yield* failed.updater.upgrade("npm", "2.3.4").pipe(Effect.flip)
|
||||
expect(error.message).toBe("registry denied access")
|
||||
const missing = yield* fixture(() => ({ error: new AppProcess.AppProcessError({ command: "npm" }) }))
|
||||
const unavailable = yield* missing.updater.upgrade("npm", "2.3.4").pipe(Effect.flip)
|
||||
expect(unavailable.message).toBe("Failed to update with npm")
|
||||
expect(failed.commands).toHaveLength(1)
|
||||
expect(missing.commands).toHaveLength(1)
|
||||
}),
|
||||
)
|
||||
;(["npm", "pnpm", "bun", "yarn", 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-ai/cli@2.3.4" : "opencode-ai@1.0.0"),
|
||||
}))
|
||||
expect(yield* test.updater.method()).toBe(method)
|
||||
expect(test.commands).toEqual([
|
||||
["npm", "list", "-g", "--depth=0", "@opencode-ai/cli"],
|
||||
["pnpm", "list", "-g", "--depth=0", "@opencode-ai/cli"],
|
||||
["bun", "pm", "ls", "-g"],
|
||||
["yarn", "global", "list"],
|
||||
])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it.live("method detection tolerates unavailable package managers", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* fixture((command) =>
|
||||
command.command === "yarn"
|
||||
? { stdout: Buffer.from("@opencode-ai/cli@2.3.4") }
|
||||
: { error: new AppProcess.AppProcessError({ command: command.command }) },
|
||||
)
|
||||
expect(yield* test.updater.method()).toBe("yarn")
|
||||
expect(test.commands).toHaveLength(4)
|
||||
}),
|
||||
)
|
||||
|
||||
test("Node distribution honors the compile-time CLI name", async () => {
|
||||
const child = Bun.spawn(
|
||||
[
|
||||
process.execPath,
|
||||
"test",
|
||||
import.meta.path,
|
||||
"--define",
|
||||
'OPENCODE_CLI_NAME="opencode2-node"',
|
||||
"--test-name-pattern",
|
||||
"^Node distribution resolves the published npm package$",
|
||||
],
|
||||
{ cwd: path.join(import.meta.dir, ".."), stdout: "ignore", stderr: "pipe" },
|
||||
)
|
||||
const [code, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()])
|
||||
expect(code, stderr).toBe(0)
|
||||
expect(stderr).toContain("1 pass")
|
||||
})
|
||||
|
||||
if (typeof OPENCODE_CLI_NAME === "string" && OPENCODE_CLI_NAME === "opencode2-node") {
|
||||
it.live("Node distribution resolves the published npm package", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* fixture((command) => ({
|
||||
stdout: Buffer.from(command.command === "npm" ? "opencode-node@2.3.4" : ""),
|
||||
}))
|
||||
expect(yield* test.updater.method()).toBe("npm")
|
||||
yield* test.updater.upgrade("npm", "v2.3.4")
|
||||
yield* test.updater.upgrade("pnpm", "v2.3.4")
|
||||
expect(test.commands).toEqual([
|
||||
["npm", "list", "-g", "--depth=0", "opencode-node"],
|
||||
["pnpm", "list", "-g", "--depth=0", "opencode-node"],
|
||||
["bun", "pm", "ls", "-g"],
|
||||
["yarn", "global", "list"],
|
||||
["npm", "install", "--global", "opencode-node@2.3.4"],
|
||||
["pnpm", "add", "--global", "--allow-build=opencode-node", "opencode-node@2.3.4"],
|
||||
])
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { mkdtemp, rm } from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
|
||||
describe("upgrade command", () => {
|
||||
test("is registered in root help and documents its options", async () => {
|
||||
const root = await cli(["--help"], {}, "../src/index.ts")
|
||||
const help = await cli(["upgrade", "--help"], {}, "../src/index.ts")
|
||||
expect(root.exitCode).toBe(0)
|
||||
expect(root.stdout).toContain("upgrade")
|
||||
expect(help.exitCode).toBe(0)
|
||||
expect(help.stdout).toContain("[<target>]")
|
||||
expect(help.stdout).toContain("--method")
|
||||
expect(help.stdout).toContain("-m")
|
||||
})
|
||||
|
||||
test("detects the installation method and resolves the latest version", async () => {
|
||||
const result = await cli([])
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(result.events).toEqual(["method", "latest", { method: "npm", version: "0.0.0-beta-new" }])
|
||||
expect(result.stdout).toContain("Upgrade complete")
|
||||
})
|
||||
|
||||
test("accepts an explicit version and method without detection or a version lookup", async () => {
|
||||
const result = await cli(["v0.0.0-beta-target", "--method", "pnpm"])
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(result.events).toEqual([{ method: "pnpm", version: "v0.0.0-beta-target" }])
|
||||
expect(result.stdout).toContain("0.0.0-beta-old → 0.0.0-beta-target")
|
||||
})
|
||||
|
||||
test("accepts the short method flag and an explicit major upgrade", async () => {
|
||||
const result = await cli(["2.0.0", "-m", "bun"])
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(result.events).toEqual([{ method: "bun", 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)
|
||||
expect(result.events).toEqual(["method"])
|
||||
expect(result.stdout).toContain("already installed")
|
||||
})
|
||||
|
||||
test("requires an explicit method when detection fails", async () => {
|
||||
const result = await cli([], { UPGRADE_TEST_METHOD: "unknown" })
|
||||
expect(result.exitCode).toBe(1)
|
||||
expect(result.events).toEqual(["method"])
|
||||
expect(result.stdout).toContain("Pass --method")
|
||||
})
|
||||
|
||||
test("rejects unsupported methods before attempting an upgrade", async () => {
|
||||
const result = await cli(["--method", "brew"])
|
||||
expect(result.exitCode).not.toBe(0)
|
||||
expect(result.events).toEqual([])
|
||||
})
|
||||
|
||||
test("reports version lookup failures without installing", async () => {
|
||||
const result = await cli([], { UPGRADE_TEST_LATEST_ERROR: "1" })
|
||||
expect(result.exitCode).toBe(1)
|
||||
expect(result.events).toEqual(["method", "latest"])
|
||||
expect(result.stdout).toContain("Update check failed")
|
||||
})
|
||||
|
||||
test("reports installation failures with a nonzero exit code", async () => {
|
||||
const result = await cli([], { UPGRADE_TEST_INSTALL_ERROR: "1" })
|
||||
expect(result.exitCode).toBe(1)
|
||||
expect(result.stdout).toContain("Upgrade failed")
|
||||
expect(result.stdout).toContain("Permission denied")
|
||||
expect(result.stdout).not.toContain("Upgrade complete")
|
||||
})
|
||||
})
|
||||
|
||||
async function cli(args: string[], env: Record<string, string> = {}, entry = "fixture/upgrade.ts") {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "opencode-upgrade-"))
|
||||
try {
|
||||
const child = Bun.spawn(
|
||||
[process.execPath, "--define", 'OPENCODE_VERSION="0.0.0-beta-old"', path.join(import.meta.dir, entry), ...args],
|
||||
{
|
||||
cwd: path.join(import.meta.dir, ".."),
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCODE_TEST_HOME: root,
|
||||
XDG_DATA_HOME: path.join(root, "data"),
|
||||
XDG_CONFIG_HOME: path.join(root, "config"),
|
||||
XDG_CACHE_HOME: path.join(root, "cache"),
|
||||
XDG_STATE_HOME: path.join(root, "state"),
|
||||
OPENCODE_DISABLE_AUTOUPDATE: "1",
|
||||
...env,
|
||||
},
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
},
|
||||
)
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(child.stdout).text(),
|
||||
new Response(child.stderr).text(),
|
||||
child.exited,
|
||||
])
|
||||
const events = stdout
|
||||
.split("\n")
|
||||
.filter((line) => line.startsWith("EVENT "))
|
||||
.map((line) => JSON.parse(line.slice(6)))
|
||||
expect(await Bun.file(path.join(root, "state", "opencode", "service-local.json")).exists()).toBe(false)
|
||||
return { stdout, stderr, exitCode, events }
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user