cli: add --mini (#33353)
This commit is contained in:
@@ -41,14 +41,31 @@ export const AttachCommand = cmd({
|
|||||||
alias: ["u"],
|
alias: ["u"],
|
||||||
type: "string",
|
type: "string",
|
||||||
describe: "basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')",
|
describe: "basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')",
|
||||||
|
})
|
||||||
|
.option("mini", {
|
||||||
|
type: "boolean",
|
||||||
|
describe: "start the minimal interactive interface",
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
.option("replay", {
|
||||||
|
type: "boolean",
|
||||||
|
hidden: true,
|
||||||
|
})
|
||||||
|
.option("no-replay", {
|
||||||
|
type: "boolean",
|
||||||
|
describe: "disable mini session history replay on resume and after resize",
|
||||||
|
})
|
||||||
|
.option("replay-limit", {
|
||||||
|
type: "number",
|
||||||
|
describe: "cap visible mini replay to the newest N messages",
|
||||||
}),
|
}),
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
const { TuiConfig } = await import("@/config/tui")
|
if (args.replay === true) {
|
||||||
if (args.fork && !args.continue && !args.session) {
|
UI.error("--replay is not supported; replay is enabled by default")
|
||||||
UI.error("--fork requires --continue or --session")
|
|
||||||
process.exitCode = 1
|
process.exitCode = 1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const noReplay = args.replay === false || args.noReplay === true
|
||||||
|
|
||||||
const directory = (() => {
|
const directory = (() => {
|
||||||
if (!args.dir) return undefined
|
if (!args.dir) return undefined
|
||||||
@@ -60,6 +77,40 @@ export const AttachCommand = cmd({
|
|||||||
return args.dir
|
return args.dir
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
if (args.mini) {
|
||||||
|
const { runMini } = await import("./run")
|
||||||
|
await runMini({
|
||||||
|
attach: args.url,
|
||||||
|
directory,
|
||||||
|
password: args.password,
|
||||||
|
username: args.username,
|
||||||
|
continue: args.continue,
|
||||||
|
session: args.session,
|
||||||
|
fork: args.fork,
|
||||||
|
replay: noReplay ? false : undefined,
|
||||||
|
replayLimit: args.replayLimit,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsupported = [
|
||||||
|
["--no-replay", noReplay],
|
||||||
|
["--replay-limit", args.replayLimit !== undefined],
|
||||||
|
].find((entry) => entry[1])?.[0]
|
||||||
|
if (unsupported) {
|
||||||
|
UI.error(`${unsupported} requires --mini`)
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { TuiConfig } = await import("@/config/tui")
|
||||||
|
if (args.fork && !args.continue && !args.session) {
|
||||||
|
UI.error("--fork requires --continue or --session")
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const headers = ServerAuth.headers({ password: args.password, username: args.username })
|
const headers = ServerAuth.headers({ password: args.password, username: args.username })
|
||||||
const config = await TuiConfig.get()
|
const config = await TuiConfig.get()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { CommandModule } from "yargs"
|
import type { CommandModule } from "yargs"
|
||||||
|
|
||||||
export type WithDoubleDash<T> = T & { "--"?: string[] }
|
export type WithDoubleDash<T> = T & { "--"?: string[]; _?: Array<string | number> }
|
||||||
|
|
||||||
export function cmd<T, U>(input: CommandModule<T, WithDoubleDash<U>>) {
|
export function cmd<T, U>(input: CommandModule<T, WithDoubleDash<U>>) {
|
||||||
return input
|
return input
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
import type { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||||
// CLI entry point for `opencode run`.
|
// CLI entry point for `opencode run` and `opencode --mini`.
|
||||||
//
|
//
|
||||||
// Handles three modes:
|
// Handles three modes:
|
||||||
// 1. Non-interactive (default): sends a single prompt, streams events to
|
// 1. Non-interactive (default): sends a single prompt, streams events to
|
||||||
// stdout, and exits when the session goes idle.
|
// stdout, and exits when the session goes idle.
|
||||||
// 2. Interactive local (`--interactive`): boots the split-footer direct mode
|
// 2. Interactive local (`opencode --mini`): boots the split-footer direct mode
|
||||||
// with an in-process server (no external HTTP).
|
// with an in-process server (no external HTTP).
|
||||||
// 3. Interactive attach (`--interactive --attach`): connects to a running
|
// 3. Interactive attach (`opencode --mini --attach`): connects to a running
|
||||||
// opencode server and runs interactive mode against it.
|
// opencode server and runs interactive mode against it.
|
||||||
//
|
//
|
||||||
// Also supports `--command` for slash-command execution, `--format json` for
|
// Also supports `--command` for slash-command execution, `--format json` for
|
||||||
@@ -217,21 +217,22 @@ export const RunCommand = effectCmd({
|
|||||||
type: "boolean",
|
type: "boolean",
|
||||||
describe: "show thinking blocks",
|
describe: "show thinking blocks",
|
||||||
})
|
})
|
||||||
|
.option("mini", {
|
||||||
|
type: "boolean",
|
||||||
|
hidden: true,
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
.option("replay", {
|
.option("replay", {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
default: true,
|
default: true,
|
||||||
|
hidden: true,
|
||||||
describe: "replay interactive session history on resume and after resize (use --no-replay to disable)",
|
describe: "replay interactive session history on resume and after resize (use --no-replay to disable)",
|
||||||
})
|
})
|
||||||
.option("replay-limit", {
|
.option("replay-limit", {
|
||||||
type: "number",
|
type: "number",
|
||||||
|
hidden: true,
|
||||||
describe: "cap visible interactive replay to the newest N messages",
|
describe: "cap visible interactive replay to the newest N messages",
|
||||||
})
|
})
|
||||||
.option("interactive", {
|
|
||||||
alias: ["i"],
|
|
||||||
type: "boolean",
|
|
||||||
describe: "run in direct interactive split-footer mode",
|
|
||||||
default: false,
|
|
||||||
})
|
|
||||||
.option("dangerously-skip-permissions", {
|
.option("dangerously-skip-permissions", {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
describe: "auto-approve permissions that are not explicitly denied (dangerous!)",
|
describe: "auto-approve permissions that are not explicitly denied (dangerous!)",
|
||||||
@@ -240,6 +241,7 @@ export const RunCommand = effectCmd({
|
|||||||
.option("demo", {
|
.option("demo", {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
default: false,
|
default: false,
|
||||||
|
hidden: true,
|
||||||
describe: "enable direct interactive demo slash commands; pass one as the message to run it immediately",
|
describe: "enable direct interactive demo slash commands; pass one as the message to run it immediately",
|
||||||
}),
|
}),
|
||||||
handler: Effect.fn("Cli.run")(function* (args) {
|
handler: Effect.fn("Cli.run")(function* (args) {
|
||||||
@@ -252,7 +254,8 @@ export const RunCommand = effectCmd({
|
|||||||
const localInstance = yield* InstanceRef
|
const localInstance = yield* InstanceRef
|
||||||
yield* Effect.promise(async () => {
|
yield* Effect.promise(async () => {
|
||||||
const rawMessage = [...args.message, ...(args["--"] || [])].join(" ")
|
const rawMessage = [...args.message, ...(args["--"] || [])].join(" ")
|
||||||
const thinking = args.interactive ? (args.thinking ?? true) : (args.thinking ?? false)
|
const interactive = args.mini
|
||||||
|
const thinking = interactive ? (args.thinking ?? true) : (args.thinking ?? false)
|
||||||
const die = (message: string): never => {
|
const die = (message: string): never => {
|
||||||
UI.error(message)
|
UI.error(message)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
@@ -269,20 +272,24 @@ export const RunCommand = effectCmd({
|
|||||||
.map((arg) => (arg.includes(" ") ? `"${arg.replace(/"/g, '\\"')}"` : arg))
|
.map((arg) => (arg.includes(" ") ? `"${arg.replace(/"/g, '\\"')}"` : arg))
|
||||||
.join(" ")
|
.join(" ")
|
||||||
|
|
||||||
if (args.interactive && args.command) {
|
if (interactive && args.command) {
|
||||||
die("--interactive cannot be used with --command")
|
die("--mini cannot be used with --command")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.demo && !args.interactive) {
|
if (interactive && args._?.[0] !== "mini") {
|
||||||
die("--demo requires --interactive")
|
die("--mini must be used without the run subcommand")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.interactive && args.format === "json") {
|
if (args.demo && !interactive) {
|
||||||
die("--interactive cannot be used with --format json")
|
die("--demo requires --mini")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args["replay-limit"] !== undefined && !args.interactive) {
|
if (interactive && args.format === "json") {
|
||||||
die("--replay-limit requires --interactive")
|
die("--mini cannot be used with --format json")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args["replay-limit"] !== undefined && !interactive) {
|
||||||
|
die("--replay-limit requires --mini")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -292,11 +299,11 @@ export const RunCommand = effectCmd({
|
|||||||
die("--replay-limit must be a positive integer")
|
die("--replay-limit must be a positive integer")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.interactive && !process.stdout.isTTY) {
|
if (interactive && !process.stdout.isTTY) {
|
||||||
die("--interactive requires a TTY stdout")
|
die("--mini requires a TTY stdout")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.interactive) {
|
if (interactive) {
|
||||||
try {
|
try {
|
||||||
resolveInteractiveStdin().cleanup?.()
|
resolveInteractiveStdin().cleanup?.()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -304,7 +311,7 @@ export const RunCommand = effectCmd({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const replay = args.replay || args["replay-limit"] !== undefined
|
const replay = args.replay === false ? false : args.replay || args["replay-limit"] !== undefined
|
||||||
|
|
||||||
const root = Filesystem.resolve(process.env.PWD ?? process.cwd())
|
const root = Filesystem.resolve(process.env.PWD ?? process.cwd())
|
||||||
const directory = (() => {
|
const directory = (() => {
|
||||||
@@ -393,7 +400,7 @@ export const RunCommand = effectCmd({
|
|||||||
message = resolveRunInput(message, piped) ?? ""
|
message = resolveRunInput(message, piped) ?? ""
|
||||||
const initialInput = resolveRunInput(rawMessage, piped)
|
const initialInput = resolveRunInput(rawMessage, piped)
|
||||||
|
|
||||||
if (message.trim().length === 0 && !args.command && !args.interactive) {
|
if (message.trim().length === 0 && !args.command && !interactive) {
|
||||||
UI.error("You must provide a message or a command")
|
UI.error("You must provide a message or a command")
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
@@ -403,7 +410,7 @@ export const RunCommand = effectCmd({
|
|||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const rules: PermissionV1.Ruleset = args.interactive
|
const rules: PermissionV1.Ruleset = interactive
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
@@ -801,7 +808,7 @@ export const RunCommand = effectCmd({
|
|||||||
|
|
||||||
await share(client, sessionID)
|
await share(client, sessionID)
|
||||||
|
|
||||||
if (!args.interactive) {
|
if (!interactive) {
|
||||||
const events = await client.event.subscribe()
|
const events = await client.event.subscribe()
|
||||||
const completed = loop(client, events).catch((e) => {
|
const completed = loop(client, events).catch((e) => {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
@@ -875,7 +882,7 @@ export const RunCommand = effectCmd({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.interactive && !args.attach && !args.session && !args.continue) {
|
if (interactive && !args.attach && !args.session && !args.continue) {
|
||||||
const model = pick(args.model)
|
const model = pick(args.model)
|
||||||
const { runInteractiveLocalMode } = await import("./run/runtime")
|
const { runInteractiveLocalMode } = await import("./run/runtime")
|
||||||
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
@@ -933,3 +940,52 @@ export const RunCommand = effectCmd({
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
type MiniCommandInput = {
|
||||||
|
directory?: string
|
||||||
|
attach?: string
|
||||||
|
password?: string
|
||||||
|
username?: string
|
||||||
|
continue?: boolean
|
||||||
|
session?: string
|
||||||
|
fork?: boolean
|
||||||
|
model?: string
|
||||||
|
agent?: string
|
||||||
|
prompt?: string
|
||||||
|
replay?: boolean
|
||||||
|
replayLimit?: number
|
||||||
|
demo?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runMini(input: MiniCommandInput) {
|
||||||
|
if (!RunCommand.handler) throw new Error("Mini command handler is unavailable")
|
||||||
|
await RunCommand.handler({
|
||||||
|
$0: "opencode",
|
||||||
|
_: ["mini"],
|
||||||
|
message: input.prompt ? [input.prompt] : [],
|
||||||
|
command: undefined,
|
||||||
|
continue: input.continue,
|
||||||
|
session: input.session,
|
||||||
|
fork: input.fork,
|
||||||
|
share: undefined,
|
||||||
|
model: input.model,
|
||||||
|
agent: input.agent,
|
||||||
|
format: "default",
|
||||||
|
file: undefined,
|
||||||
|
title: undefined,
|
||||||
|
attach: input.attach,
|
||||||
|
password: input.password,
|
||||||
|
username: input.username,
|
||||||
|
dir: input.directory,
|
||||||
|
port: undefined,
|
||||||
|
variant: undefined,
|
||||||
|
thinking: undefined,
|
||||||
|
mini: true,
|
||||||
|
replay: input.replay ?? true,
|
||||||
|
"replay-limit": input.replayLimit,
|
||||||
|
replayLimit: input.replayLimit,
|
||||||
|
"dangerously-skip-permissions": false,
|
||||||
|
dangerouslySkipPermissions: false,
|
||||||
|
demo: input.demo ?? false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import * as tty from "node:tty"
|
import * as tty from "node:tty"
|
||||||
|
|
||||||
export const INTERACTIVE_INPUT_ERROR = "--interactive requires a controlling terminal for input"
|
export const INTERACTIVE_INPUT_ERROR = "--mini requires a controlling terminal for input"
|
||||||
|
|
||||||
type InteractiveStdin = {
|
type InteractiveStdin = {
|
||||||
stdin: NodeJS.ReadStream
|
stdin: NodeJS.ReadStream
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Top-level orchestrator for `run --interactive`.
|
// Top-level orchestrator for `opencode --mini`.
|
||||||
//
|
//
|
||||||
// Wires the boot sequence, lifecycle (renderer + footer), stream transport,
|
// Wires the boot sequence, lifecycle (renderer + footer), stream transport,
|
||||||
// and prompt queue together into a single session loop. Two entry points:
|
// and prompt queue together into a single session loop. Two entry points:
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: Scrollback
|
|||||||
lines,
|
lines,
|
||||||
body_left + label.length,
|
body_left + label.length,
|
||||||
top + 1,
|
top + 1,
|
||||||
`opencode run -i -s ${meta.session_id}`,
|
`opencode --mini -s ${meta.session_id}`,
|
||||||
right,
|
right,
|
||||||
undefined,
|
undefined,
|
||||||
TextAttributes.BOLD,
|
TextAttributes.BOLD,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Shared type vocabulary for the direct interactive mode (`run --interactive`).
|
// Shared type vocabulary for the direct interactive mode (`opencode --mini`).
|
||||||
//
|
//
|
||||||
// Direct mode uses a split-footer terminal layout: immutable scrollback for the
|
// Direct mode uses a split-footer terminal layout: immutable scrollback for the
|
||||||
// session transcript, and a mutable footer for prompt input, status, and
|
// session transcript, and a mutable footer for prompt input, status, and
|
||||||
|
|||||||
@@ -103,8 +103,73 @@ export const TuiThreadCommand = cmd({
|
|||||||
.option("agent", {
|
.option("agent", {
|
||||||
type: "string",
|
type: "string",
|
||||||
describe: "agent to use",
|
describe: "agent to use",
|
||||||
|
})
|
||||||
|
.option("mini", {
|
||||||
|
type: "boolean",
|
||||||
|
describe: "start the minimal interactive interface",
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
.option("replay", {
|
||||||
|
type: "boolean",
|
||||||
|
hidden: true,
|
||||||
|
})
|
||||||
|
.option("no-replay", {
|
||||||
|
type: "boolean",
|
||||||
|
describe: "disable mini session history replay on resume and after resize",
|
||||||
|
})
|
||||||
|
.option("replay-limit", {
|
||||||
|
type: "number",
|
||||||
|
describe: "cap visible mini replay to the newest N messages",
|
||||||
|
})
|
||||||
|
.option("demo", {
|
||||||
|
type: "boolean",
|
||||||
|
hidden: true,
|
||||||
}),
|
}),
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
|
if (args.replay === true) {
|
||||||
|
UI.error("--replay is not supported; replay is enabled by default")
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const noReplay = args.replay === false || args.noReplay === true
|
||||||
|
|
||||||
|
if (args.mini) {
|
||||||
|
const network = ["--port", "--hostname", "--mdns", "--no-mdns", "--mdns-domain", "--cors"].find((option) =>
|
||||||
|
process.argv.some((arg) => arg === option || arg.startsWith(option + "=")),
|
||||||
|
)
|
||||||
|
if (network) {
|
||||||
|
UI.error(`${network} cannot be used with --mini`)
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { runMini } = await import("./run")
|
||||||
|
await runMini({
|
||||||
|
directory: resolveThreadDirectory(args.project),
|
||||||
|
continue: args.continue,
|
||||||
|
session: args.session,
|
||||||
|
fork: args.fork,
|
||||||
|
model: args.model,
|
||||||
|
agent: args.agent,
|
||||||
|
prompt: args.prompt,
|
||||||
|
replay: noReplay ? false : undefined,
|
||||||
|
replayLimit: args.replayLimit,
|
||||||
|
demo: args.demo,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsupported = [
|
||||||
|
["--no-replay", noReplay],
|
||||||
|
["--replay-limit", args.replayLimit !== undefined],
|
||||||
|
["--demo", args.demo !== undefined],
|
||||||
|
].find((entry) => entry[1])?.[0]
|
||||||
|
if (unsupported) {
|
||||||
|
UI.error(`${unsupported} requires --mini`)
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const unguard = win32InstallCtrlCGuard()
|
const unguard = win32InstallCtrlCGuard()
|
||||||
try {
|
try {
|
||||||
const { TuiConfig } = await import("@/config/tui")
|
const { TuiConfig } = await import("@/config/tui")
|
||||||
|
|||||||
@@ -50,17 +50,21 @@ Positionals:
|
|||||||
url http://localhost:4096 [string] [required]
|
url http://localhost:4096 [string] [required]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h, --help show help [boolean]
|
-h, --help show help [boolean]
|
||||||
-v, --version show version number [boolean]
|
-v, --version show version number [boolean]
|
||||||
--print-logs print logs to stderr [boolean]
|
--print-logs print logs to stderr [boolean]
|
||||||
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
|
||||||
--pure run without external plugins [boolean]
|
--pure run without external plugins [boolean]
|
||||||
--dir directory to run in [string]
|
--dir directory to run in [string]
|
||||||
-c, --continue continue the last session [boolean]
|
-c, --continue continue the last session [boolean]
|
||||||
-s, --session session id to continue [string]
|
-s, --session session id to continue [string]
|
||||||
--fork fork the session when continuing (use with --continue or --session) [boolean]
|
--fork fork the session when continuing (use with --continue or --session) [boolean]
|
||||||
-p, --password basic auth password (defaults to OPENCODE_SERVER_PASSWORD) [string]
|
-p, --password basic auth password (defaults to OPENCODE_SERVER_PASSWORD) [string]
|
||||||
-u, --username basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')[string]"
|
-u, --username basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')
|
||||||
|
[string]
|
||||||
|
--mini start the minimal interactive interface [boolean] [default: false]
|
||||||
|
--no-replay disable mini session history replay on resume and after resize [boolean]
|
||||||
|
--replay-limit cap visible mini replay to the newest N messages [number]"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode run --help 1`] = `
|
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode run --help 1`] = `
|
||||||
@@ -103,16 +107,8 @@ Options:
|
|||||||
--variant model variant (provider-specific reasoning effort, e.g., high,
|
--variant model variant (provider-specific reasoning effort, e.g., high,
|
||||||
max, minimal) [string]
|
max, minimal) [string]
|
||||||
--thinking show thinking blocks [boolean]
|
--thinking show thinking blocks [boolean]
|
||||||
--replay replay interactive session history on resume and after resize
|
|
||||||
(use --no-replay to disable) [boolean] [default: true]
|
|
||||||
--replay-limit cap visible interactive replay to the newest N messages
|
|
||||||
[number]
|
|
||||||
-i, --interactive run in direct interactive split-footer mode
|
|
||||||
[boolean] [default: false]
|
|
||||||
--dangerously-skip-permissions auto-approve permissions that are not explicitly denied
|
--dangerously-skip-permissions auto-approve permissions that are not explicitly denied
|
||||||
(dangerous!) [boolean] [default: false]
|
(dangerous!) [boolean] [default: false]"
|
||||||
--demo enable direct interactive demo slash commands; pass one as the
|
|
||||||
message to run it immediately [boolean] [default: false]"
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode debug --help 1`] = `
|
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode debug --help 1`] = `
|
||||||
|
|||||||
@@ -102,6 +102,10 @@ describe("opencode CLI help-text snapshots", () => {
|
|||||||
const topLevel = yield* opencode.spawn(["--help"], { env: SNAPSHOT_ENV })
|
const topLevel = yield* opencode.spawn(["--help"], { env: SNAPSHOT_ENV })
|
||||||
expect(topLevel.exitCode).toBe(0)
|
expect(topLevel.exitCode).toBe(0)
|
||||||
expect(topLevel.stderr.endsWith(EOL)).toBe(true)
|
expect(topLevel.stderr.endsWith(EOL)).toBe(true)
|
||||||
|
expect(topLevel.stderr).toContain("--mini")
|
||||||
|
expect(topLevel.stderr).not.toContain("--thinking")
|
||||||
|
expect(topLevel.stderr).not.toContain("--variant")
|
||||||
|
expect(topLevel.stderr).not.toContain("--demo")
|
||||||
|
|
||||||
const argvs: Array<readonly string[]> = [...TOP_LEVEL.map((c) => [c] as const), ...SUBCOMMANDS]
|
const argvs: Array<readonly string[]> = [...TOP_LEVEL.map((c) => [c] as const), ...SUBCOMMANDS]
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { Effect } from "effect"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
import yargs from "yargs"
|
||||||
import { tmpdir } from "../../fixture/fixture"
|
import { tmpdir } from "../../fixture/fixture"
|
||||||
import { resolveThreadDirectory } from "../../../src/cli/cmd/tui"
|
import { TuiThreadCommand, resolveThreadDirectory } from "../../../src/cli/cmd/tui"
|
||||||
|
import { cliIt } from "../../lib/cli-process"
|
||||||
|
|
||||||
describe("tui thread", () => {
|
describe("tui thread", () => {
|
||||||
test("loads the TUI integration lazily", async () => {
|
test("loads the TUI integration lazily", async () => {
|
||||||
@@ -33,4 +36,60 @@ describe("tui thread", () => {
|
|||||||
test("uses the real cwd after resolving a relative project from PWD", async () => {
|
test("uses the real cwd after resolving a relative project from PWD", async () => {
|
||||||
await check(".")
|
await check(".")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("resolves a relative mini project from PWD when cwd differs", async () => {
|
||||||
|
await using pwd = await tmpdir({ git: true })
|
||||||
|
await using cwd = await tmpdir({ git: true })
|
||||||
|
|
||||||
|
expect(resolveThreadDirectory(".", pwd.path, cwd.path)).toBe(pwd.path)
|
||||||
|
expect(resolveThreadDirectory(undefined, pwd.path, cwd.path)).toBe(cwd.path)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("parses supported --no-replay forms", async () => {
|
||||||
|
for (const option of ["--no-replay", "--no-replay=true", "--noReplay"]) {
|
||||||
|
const args = await yargs([])
|
||||||
|
.command({ ...TuiThreadCommand, handler: () => {} })
|
||||||
|
.exitProcess(false)
|
||||||
|
.parse(["--mini", option, "--replay-limit", "10"])
|
||||||
|
|
||||||
|
expect(args.replay === false || args.noReplay === true).toBe(true)
|
||||||
|
expect(args.replayLimit).toBe(10)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("preserves boolean negation for existing options", async () => {
|
||||||
|
const args = await yargs([])
|
||||||
|
.command({ ...TuiThreadCommand, handler: () => {} })
|
||||||
|
.exitProcess(false)
|
||||||
|
.parse(["--mdns", "--no-mdns"])
|
||||||
|
|
||||||
|
expect(args.mdns).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
cliIt.live("rejects mini-only options without --mini", ({ opencode }) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const result = yield* opencode.spawn(["--replay-limit", "10"])
|
||||||
|
|
||||||
|
opencode.expectExit(result, 1)
|
||||||
|
expect(result.stderr).toContain("--replay-limit requires --mini")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
cliIt.live("routes attached sessions to mini mode", ({ opencode }) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const result = yield* opencode.spawn(["attach", "http://127.0.0.1:1", "--mini"])
|
||||||
|
|
||||||
|
opencode.expectExit(result, 1)
|
||||||
|
expect(result.stderr).toContain("--mini requires a TTY stdout")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
cliIt.live("rejects network options in mini mode", ({ opencode }) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const result = yield* opencode.spawn(["--mini", "--port", "4096"])
|
||||||
|
|
||||||
|
opencode.expectExit(result, 1)
|
||||||
|
expect(result.stderr).toContain("--port cannot be used with --mini")
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user