From d15e5cc2c40deff7f9ae1c95b0cf1c1e01fed93d Mon Sep 17 00:00:00 2001 From: GitNimay Date: Sat, 23 May 2026 05:29:49 +0000 Subject: [PATCH] fix(opencode): avoid spinner noise in non-tty plugin install --- packages/opencode/src/cli/cmd/plug.ts | 21 ++++++++++-- packages/opencode/test/plugin/install.test.ts | 32 ++++++++++++++++--- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/cli/cmd/plug.ts b/packages/opencode/src/cli/cmd/plug.ts index 1529e9b71df3..ac060b1fb31a 100644 --- a/packages/opencode/src/cli/cmd/plug.ts +++ b/packages/opencode/src/cli/cmd/plug.ts @@ -19,6 +19,7 @@ type Spin = { export type PlugDeps = { spinner: () => Spin + tty: boolean log: { error: (msg: string) => void info: (msg: string) => void @@ -46,6 +47,7 @@ export type PlugCtx = { const defaultPlugDeps: PlugDeps = { spinner: () => spinner(), + tty: process.stdout.isTTY, log: { error: (msg) => log.error(msg), info: (msg) => log.info(msg), @@ -67,13 +69,26 @@ function cause(err: unknown) { return (err as { cause?: unknown }).cause } +function progress(dep: PlugDeps) { + if (dep.tty) return dep.spinner() + return { + start(msg: string) { + dep.log.info(msg) + }, + stop(msg: string, code?: number) { + if (!code) return + dep.log.error(msg) + }, + } +} + export function createPlugTask(input: PlugInput, dep: PlugDeps = defaultPlugDeps) { const mod = input.mod const force = Boolean(input.force) const global = Boolean(input.global) return async (ctx: PlugCtx) => { - const install = dep.spinner() + const install = progress(dep) install.start("Installing plugin package...") const target = await installPlugin(mod, dep) if (!target.ok) { @@ -101,7 +116,7 @@ export function createPlugTask(input: PlugInput, dep: PlugDeps = defaultPlugDeps } install.stop("Plugin package ready") - const inspect = dep.spinner() + const inspect = progress(dep) inspect.start("Reading plugin manifest...") const manifest = await readPluginManifest(target.target) if (!manifest.ok) { @@ -129,7 +144,7 @@ export function createPlugTask(input: PlugInput, dep: PlugDeps = defaultPlugDeps `Detected ${manifest.targets.map((item) => item.kind).join(" + ")} target${manifest.targets.length === 1 ? "" : "s"}`, ) - const patch = dep.spinner() + const patch = progress(dep) patch.start("Updating plugin config...") const out = await patchPluginConfig( { diff --git a/packages/opencode/test/plugin/install.test.ts b/packages/opencode/test/plugin/install.test.ts index 6dc9175be42a..44c3ebc96bf8 100644 --- a/packages/opencode/test/plugin/install.test.ts +++ b/packages/opencode/test/plugin/install.test.ts @@ -6,16 +6,17 @@ import { Filesystem } from "@/util/filesystem" import { createPlugTask, type PlugCtx, type PlugDeps } from "../../src/cli/cmd/plug" import { tmpdir } from "../fixture/fixture" -function deps(global: string, target: string | Error): PlugDeps { +function deps(global: string, target: string | Error, options?: { tty?: boolean; logs?: string[] }): PlugDeps { return { spinner: () => ({ start() {}, stop() {}, }), + tty: options?.tty ?? true, log: { - error() {}, - info() {}, - success() {}, + error: (msg) => options?.logs?.push(`error:${msg}`), + info: (msg) => options?.logs?.push(`info:${msg}`), + success: (msg) => options?.logs?.push(`success:${msg}`), }, resolve: async () => { if (target instanceof Error) throw target @@ -109,6 +110,29 @@ async function read(file: string) { } describe("plugin.install.task", () => { + test("logs plain progress messages when stdout is not a tty", async () => { + await using tmp = await tmpdir() + const target = await plugin(tmp.path, ["server"]) + const logs: string[] = [] + const run = createPlugTask( + { + mod: "acme@1.2.3", + }, + deps(path.join(tmp.path, "global"), target, { tty: false, logs }), + ) + + const ok = await run(ctx(tmp.path)) + expect(ok).toBe(true) + expect(logs).toEqual([ + "info:Installing plugin package...", + "info:Reading plugin manifest...", + "info:Updating plugin config...", + `info:Added to ${path.join(tmp.path, ".opencode", "opencode.jsonc")}`, + "success:Installed acme@1.2.3", + `info:Scope: local (${path.join(tmp.path, ".opencode")})`, + ]) + }) + test("writes both server and tui config entries", async () => { await using tmp = await tmpdir() const target = await plugin(tmp.path, ["server", "tui"])