Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ jobs:
matrix:
settings:
- name: linux
host: blacksmith-4vcpu-ubuntu-2404
host: ubuntu-24.04
unit_timeout: 20
- name: windows
host: blacksmith-4vcpu-windows-2025
host: windows-latest
unit_timeout: 45
runs-on: ${{ matrix.settings.host }}
defaults:
run:
Expand All @@ -54,6 +56,13 @@ jobs:
git config --global user.email "[email protected]"
git config --global user.name "opencode"

- name: Install ripgrep
if: runner.os == 'Windows'
shell: pwsh
run: |
choco install ripgrep -y --no-progress
"C:\ProgramData\chocolatey\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append

- name: Cache Turbo
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
Expand All @@ -64,7 +73,7 @@ jobs:
turbo-${{ runner.os }}-

- name: Run unit tests
timeout-minutes: 20
timeout-minutes: ${{ matrix.settings.unit_timeout }}
run: GITHUB_ACTIONS=false bun turbo test
env:
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
Expand All @@ -86,9 +95,9 @@ jobs:
matrix:
settings:
- name: linux
host: blacksmith-4vcpu-ubuntu-2404
host: ubuntu-24.04
- name: windows
host: blacksmith-4vcpu-windows-2025
host: windows-latest
runs-on: ${{ matrix.settings.host }}
env:
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.playwright-browsers
Expand Down
5 changes: 5 additions & 0 deletions packages/opencode/bin/opencode
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ const os = require("os")
const forwardedSignals = ["SIGINT", "SIGTERM", "SIGHUP"]

function run(target) {
const displayName = path.basename(process.argv[1] || __filename).replace(/\.(cmd|exe)$/i, "")
const child = childProcess.spawn(target, process.argv.slice(2), {
stdio: "inherit",
env: {
...process.env,
OPENCODE_CLI_NAME: process.env.OPENCODE_CLI_NAME || displayName,
},
})

child.on("error", (error) => {
Expand Down
5 changes: 3 additions & 2 deletions packages/opencode/src/cli/cmd/attach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { UI } from "@/cli/ui"
import { errorMessage } from "@opencode-ai/tui/util/error"
import { validateSession } from "../tui/validate-session"
import { ServerAuth } from "@/server/auth"
import { CliDisplay } from "../display"

export const AttachCommand = cmd({
command: "attach <url>",
describe: "attach to a running opencode server",
describe: `attach to a running ${CliDisplay.cliProductName()} server`,
builder: (yargs) =>
yargs
.positional("url", {
Expand Down Expand Up @@ -40,7 +41,7 @@ export const AttachCommand = cmd({
.option("username", {
alias: ["u"],
type: "string",
describe: "basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')",
describe: CliDisplay.authUsernameDescription(),
})
.option("mini", {
type: "boolean",
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/cli/cmd/pr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { effectCmd, fail } from "../effect-cmd"
import { Git } from "@/git"
import { InstanceRef } from "@/effect/instance-ref"
import { Process } from "@/util/process"
import { CliDisplay } from "../display"

export const PrCommand = effectCmd({
command: "pr <number>",
describe: "fetch and checkout a GitHub PR branch, then run opencode",
describe: `fetch and checkout a GitHub PR branch, then run ${CliDisplay.cliProductName()}`,
builder: (yargs) =>
yargs.positional("number", {
type: "number",
Expand Down
5 changes: 3 additions & 2 deletions packages/opencode/src/cli/cmd/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Process } from "@/util/process"
import { errorMessage } from "@/util/error"
import { text } from "node:stream/consumers"
import { Effect, Option } from "effect"
import { CliDisplay } from "../display"

type PluginAuth = NonNullable<Hooks["auth"]>

Expand Down Expand Up @@ -239,7 +240,7 @@ export function resolvePluginProviders(input: {
export const ProvidersCommand = cmd({
command: "providers",
aliases: ["auth"],
describe: "manage AI providers and credentials",
describe: CliDisplay.providersDescription(),
builder: (yargs) =>
yargs.command(ProvidersListCommand).command(ProvidersLoginCommand).command(ProvidersLogoutCommand).demandCommand(),
async handler() {},
Expand Down Expand Up @@ -304,7 +305,7 @@ export const ProvidersLoginCommand = effectCmd({
builder: (yargs: Argv) =>
yargs
.positional("url", {
describe: "opencode auth provider",
describe: `${CliDisplay.cliProductName()} auth provider`,
type: "string",
})
.option("provider", {
Expand Down
9 changes: 5 additions & 4 deletions packages/opencode/src/cli/cmd/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
// 2. Interactive local (`opencode --mini`): boots the split-footer direct mode
// with an in-process server (no external HTTP).
// 3. Interactive attach (`opencode --mini --attach`): connects to a running
// opencode server and runs interactive mode against it.
// server and runs interactive mode against it.
//
// Also supports `--command` for slash-command execution, `--format json` for
// raw event streaming, `--continue` / `--session` for session resumption,
Expand All @@ -25,6 +25,7 @@ import { Filesystem } from "@/util/filesystem"
import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
import { FormatError, FormatUnknownError } from "../error"
import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./run/runtime.stdin"
import { CliDisplay } from "../display"

type ModelInput = Parameters<OpencodeClient["session"]["prompt"]>[0]["model"]

Expand Down Expand Up @@ -125,7 +126,7 @@ async function toolError(part: ToolPart) {

export const RunCommand = effectCmd({
command: "run [message..]",
describe: "run opencode with a message",
describe: `run ${CliDisplay.cliProductName()} with a message`,
// --attach connects to a remote server (no local instance needed); the
// default path runs an in-process server and needs the project instance.
instance: (args) => !args.attach,
Expand Down Expand Up @@ -189,7 +190,7 @@ export const RunCommand = effectCmd({
})
.option("attach", {
type: "string",
describe: "attach to a running opencode server (e.g., http://localhost:4096)",
describe: `attach to a running ${CliDisplay.cliProductName()} server (e.g., http://localhost:4096)`,
})
.option("password", {
alias: ["p"],
Expand All @@ -199,7 +200,7 @@ export const RunCommand = effectCmd({
.option("username", {
alias: ["u"],
type: "string",
describe: "basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')",
describe: CliDisplay.authUsernameDescription(),
})
.option("dir", {
type: "string",
Expand Down
5 changes: 3 additions & 2 deletions packages/opencode/src/cli/cmd/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { Effect } from "effect"
import { effectCmd } from "../effect-cmd"
import { withNetworkOptions, resolveNetworkOptions } from "../network"
import { Flag } from "@opencode-ai/core/flag/flag"
import { CliDisplay } from "../display"

export const ServeCommand = effectCmd({
command: "serve",
builder: (yargs) => withNetworkOptions(yargs),
describe: "starts a headless opencode server",
describe: `starts a headless ${CliDisplay.cliProductName()} server`,
// Server loads instances per-request via x-opencode-directory header — no
// need for an ambient project InstanceContext at startup.
instance: false,
Expand All @@ -17,7 +18,7 @@ export const ServeCommand = effectCmd({
}
const opts = yield* resolveNetworkOptions(args)
const server = yield* Effect.promise(() => Server.listen(opts))
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
console.log(`${CliDisplay.cliProductName()} server listening on http://${server.hostname}:${server.port}`)

yield* Effect.never
}),
Expand Down
5 changes: 3 additions & 2 deletions packages/opencode/src/cli/cmd/tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { writeHeapSnapshot } from "v8"
import { ServerAuth } from "@/server/auth"
import { validateSession } from "../tui/validate-session"
import { win32InstallCtrlCGuard } from "@opencode-ai/tui/terminal-win32"
import { CliDisplay } from "../display"

declare global {
const OPENCODE_WORKER_PATH: string
Expand Down Expand Up @@ -71,12 +72,12 @@ export function resolveThreadDirectory(project?: string, envPWD = process.env.PW

export const TuiThreadCommand = cmd({
command: "$0 [project]",
describe: "start opencode tui",
describe: `start ${CliDisplay.cliProductName()} tui`,
builder: (yargs) =>
withNetworkOptions(yargs)
.positional("project", {
type: "string",
describe: "path to start opencode in",
describe: `path to start ${CliDisplay.cliProductName()} in`,
})
.option("model", {
type: "string",
Expand Down
5 changes: 3 additions & 2 deletions packages/opencode/src/cli/cmd/uninstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import path from "path"
import os from "os"
import { Filesystem } from "@/util/filesystem"
import { Process } from "@/util/process"
import { CliDisplay } from "../display"

interface UninstallArgs {
keepConfig: boolean
Expand All @@ -24,7 +25,7 @@ interface RemovalTargets {

export const UninstallCommand = {
command: "uninstall",
describe: "uninstall opencode and remove all related files",
describe: `uninstall ${CliDisplay.cliProductName()} and remove all related files`,
builder: (yargs: Argv) =>
yargs
.option("keep-config", {
Expand Down Expand Up @@ -55,7 +56,7 @@ export const UninstallCommand = {
UI.empty()
UI.println(UI.logo(" "))
UI.empty()
prompts.intro("Uninstall VERDICT")
prompts.intro(`Uninstall ${CliDisplay.cliProductName()}`)

const method = await Installation.method()
prompts.log.info(`Installation method: ${method}`)
Expand Down
9 changes: 6 additions & 3 deletions packages/opencode/src/cli/cmd/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { UI } from "../ui"
import * as prompts from "@clack/prompts"
import { Installation } from "../../installation"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { CliDisplay } from "../display"

export const UpgradeCommand = {
command: "upgrade [target]",
describe: "upgrade opencode to the latest or a specific version",
describe: `upgrade ${CliDisplay.cliProductName()} to the latest or a specific version`,
builder: (yargs: Argv) => {
return yargs
.positional("target", {
Expand All @@ -28,7 +29,9 @@ export const UpgradeCommand = {
const detectedMethod = await Installation.method()
const method = (args.method as Installation.Method) ?? detectedMethod
if (method === "unknown") {
prompts.log.error(`opencode is installed to ${process.execPath} and may be managed by a package manager`)
prompts.log.error(
`${CliDisplay.cliProductName()} is installed to ${process.execPath} and may be managed by a package manager`,
)
const install = await prompts.select({
message: "Install anyways?",
options: [
Expand All @@ -46,7 +49,7 @@ export const UpgradeCommand = {
const target = args.target ? args.target.replace(/^v/, "") : await Installation.latest()

if (InstallationVersion === target) {
prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`)
prompts.log.warn(`${CliDisplay.cliProductName()} upgrade skipped: ${target} is already installed`)
prompts.outro("Done")
return
}
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/cli/cmd/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { withNetworkOptions, resolveNetworkOptions } from "../network"
import { Flag } from "@opencode-ai/core/flag/flag"
import open from "open"
import { networkInterfaces } from "os"
import { CliDisplay } from "../display"

function getNetworkIPs() {
const nets = networkInterfaces()
Expand All @@ -31,7 +32,7 @@ function getNetworkIPs() {
export const WebCommand = effectCmd({
command: "web",
builder: (yargs) => withNetworkOptions(yargs),
describe: "start opencode server and open web interface",
describe: `start ${CliDisplay.cliProductName()} server and open web interface`,
// Server loads instances per-request via x-opencode-directory header — no
// ambient project InstanceContext needed at startup.
instance: false,
Expand Down
34 changes: 34 additions & 0 deletions packages/opencode/src/cli/display.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import path from "path"

function normalizeName(value: string | undefined) {
const base = path.basename(value ?? "").replace(/\.exe$/i, "")
if (!base || base === "bun" || base === "node" || base === "index.ts" || base === "temporary.ts") return
return base
}

export function cliDisplayName(input?: { argv?: string[]; execPath?: string; env?: NodeJS.ProcessEnv }) {
const env = input?.env ?? process.env
const override = env.VERDICT_CLI_NAME || env.OPENCODE_CLI_NAME
if (override?.trim()) return override.trim()

const argv = input?.argv ?? process.argv
return normalizeName(input?.execPath ?? process.execPath) || normalizeName(argv[1]) || "opencode"
}

export function cliProductName(input?: { argv?: string[]; execPath?: string; env?: NodeJS.ProcessEnv }) {
const displayName = cliDisplayName(input)
return displayName.toLowerCase() === "verdict" ? "VERDICT" : displayName
}

export function authUsernameDescription(input?: { argv?: string[]; execPath?: string; env?: NodeJS.ProcessEnv }) {
const suffix = cliDisplayName(input).toLowerCase() === "opencode" ? " or 'opencode'" : " or built-in username"
return `basic auth username (defaults to OPENCODE_SERVER_USERNAME${suffix})`
}

export function providersDescription(input?: { argv?: string[]; execPath?: string; env?: NodeJS.ProcessEnv }) {
return cliDisplayName(input).toLowerCase() === "opencode"
? "manage AI providers and credentials"
: `manage ${cliProductName(input)} providers and credentials`
}

export * as CliDisplay from "./display"
7 changes: 3 additions & 4 deletions packages/opencode/src/cli/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import { Schema } from "effect"
import { logo as glyphs } from "./logo"

const wordmark = [
`⠀ ▄ `,
`█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█`,
`█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀`,
`▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`,
`█ █ █▀▀▀ █▀▀█ █▀▀▄ ▀██▀ █▀▀▀ ████`,
`████ █▀▀▀ █▀▀▄ ████ ██ ███ ██ `,
` ▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▄██▄ ▀▀▀▀ ▀▀ `,
]

export class CancelledError extends Schema.TaggedErrorClass<CancelledError>()("UICancelledError", {}) {}
Expand Down
6 changes: 4 additions & 2 deletions packages/opencode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ import { DbCommand } from "./cli/cmd/db"
import { errorMessage } from "./util/error"
import { PluginCommand } from "./cli/cmd/plug"
import { Heap } from "./cli/heap"
import { CliDisplay } from "./cli/display"

const args = hideBin(process.argv)
const displayName = CliDisplay.cliDisplayName()

function show(out: string) {
const text = out.trimStart()
if (!text.startsWith("opencode ")) {
if (!text.startsWith(displayName + " ")) {
process.stderr.write(UI.logo() + EOL + EOL)
process.stderr.write(text + EOL)
return
Expand All @@ -44,7 +46,7 @@ function show(out: string) {

const cli = yargs(args)
.parserConfiguration({ "populate--": true })
.scriptName("opencode")
.scriptName(displayName)
.wrap(100)
.help("help", "show help")
.alias("help", "h")
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/temporary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import yargs from "yargs"
import { TuiThreadCommand } from "./cli/cmd/tui"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { hideBin } from "yargs/helpers"
import { CliDisplay } from "./cli/display"
const cli = yargs(hideBin(process.argv))
.parserConfiguration({ "populate--": true })
.scriptName("opencode")
.scriptName(CliDisplay.cliDisplayName())
.wrap(100)
.help("help", "show help")
.alias("help", "h")
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/test/cli/acp/initialize-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe("opencode acp initialize/auth subprocess", () => {
expect(initialized.agentCapabilities?.sessionCapabilities?.fork).toEqual({})
expect(initialized.agentCapabilities?.sessionCapabilities?.list).toEqual({})
expect(initialized.agentCapabilities?.sessionCapabilities?.resume).toEqual({})
expect(initialized.agentInfo?.name).toBe("OpenCode")
expect(initialized.agentInfo?.name).toBe("VERDICT")
}),
60_000,
)
Expand Down
Loading
Loading