|
|
@@ -1,13 +1,13 @@
|
|
|
import type { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
|
|
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:
|
|
|
// 1. Non-interactive (default): sends a single prompt, streams events to
|
|
|
// 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).
|
|
|
-// 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.
|
|
|
//
|
|
|
// Also supports `--command` for slash-command execution, `--format json` for
|
|
|
@@ -217,21 +217,22 @@ export const RunCommand = effectCmd({
|
|
|
type: "boolean",
|
|
|
describe: "show thinking blocks",
|
|
|
})
|
|
|
+ .option("mini", {
|
|
|
+ type: "boolean",
|
|
|
+ hidden: true,
|
|
|
+ default: false,
|
|
|
+ })
|
|
|
.option("replay", {
|
|
|
type: "boolean",
|
|
|
default: true,
|
|
|
+ hidden: true,
|
|
|
describe: "replay interactive session history on resume and after resize (use --no-replay to disable)",
|
|
|
})
|
|
|
.option("replay-limit", {
|
|
|
type: "number",
|
|
|
+ hidden: true,
|
|
|
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", {
|
|
|
type: "boolean",
|
|
|
describe: "auto-approve permissions that are not explicitly denied (dangerous!)",
|
|
|
@@ -240,6 +241,7 @@ export const RunCommand = effectCmd({
|
|
|
.option("demo", {
|
|
|
type: "boolean",
|
|
|
default: false,
|
|
|
+ hidden: true,
|
|
|
describe: "enable direct interactive demo slash commands; pass one as the message to run it immediately",
|
|
|
}),
|
|
|
handler: Effect.fn("Cli.run")(function* (args) {
|
|
|
@@ -252,7 +254,8 @@ export const RunCommand = effectCmd({
|
|
|
const localInstance = yield* InstanceRef
|
|
|
yield* Effect.promise(async () => {
|
|
|
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 => {
|
|
|
UI.error(message)
|
|
|
process.exit(1)
|
|
|
@@ -269,20 +272,24 @@ export const RunCommand = effectCmd({
|
|
|
.map((arg) => (arg.includes(" ") ? `"${arg.replace(/"/g, '\\"')}"` : arg))
|
|
|
.join(" ")
|
|
|
|
|
|
- if (args.interactive && args.command) {
|
|
|
- die("--interactive cannot be used with --command")
|
|
|
+ if (interactive && args.command) {
|
|
|
+ die("--mini cannot be used with --command")
|
|
|
}
|
|
|
|
|
|
- if (args.demo && !args.interactive) {
|
|
|
- die("--demo requires --interactive")
|
|
|
+ if (interactive && args._?.[0] !== "mini") {
|
|
|
+ die("--mini must be used without the run subcommand")
|
|
|
}
|
|
|
|
|
|
- if (args.interactive && args.format === "json") {
|
|
|
- die("--interactive cannot be used with --format json")
|
|
|
+ if (args.demo && !interactive) {
|
|
|
+ die("--demo requires --mini")
|
|
|
}
|
|
|
|
|
|
- if (args["replay-limit"] !== undefined && !args.interactive) {
|
|
|
- die("--replay-limit requires --interactive")
|
|
|
+ if (interactive && args.format === "json") {
|
|
|
+ die("--mini cannot be used with --format json")
|
|
|
+ }
|
|
|
+
|
|
|
+ if (args["replay-limit"] !== undefined && !interactive) {
|
|
|
+ die("--replay-limit requires --mini")
|
|
|
}
|
|
|
|
|
|
if (
|
|
|
@@ -292,11 +299,11 @@ export const RunCommand = effectCmd({
|
|
|
die("--replay-limit must be a positive integer")
|
|
|
}
|
|
|
|
|
|
- if (args.interactive && !process.stdout.isTTY) {
|
|
|
- die("--interactive requires a TTY stdout")
|
|
|
+ if (interactive && !process.stdout.isTTY) {
|
|
|
+ die("--mini requires a TTY stdout")
|
|
|
}
|
|
|
|
|
|
- if (args.interactive) {
|
|
|
+ if (interactive) {
|
|
|
try {
|
|
|
resolveInteractiveStdin().cleanup?.()
|
|
|
} 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 directory = (() => {
|
|
|
@@ -393,7 +400,7 @@ export const RunCommand = effectCmd({
|
|
|
message = resolveRunInput(message, 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")
|
|
|
process.exit(1)
|
|
|
}
|
|
|
@@ -403,7 +410,7 @@ export const RunCommand = effectCmd({
|
|
|
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)
|
|
|
|
|
|
- if (!args.interactive) {
|
|
|
+ if (!interactive) {
|
|
|
const events = await client.event.subscribe()
|
|
|
const completed = loop(client, events).catch((e) => {
|
|
|
console.error(e)
|
|
|
@@ -875,7 +882,7 @@ export const RunCommand = effectCmd({
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- if (args.interactive && !args.attach && !args.session && !args.continue) {
|
|
|
+ if (interactive && !args.attach && !args.session && !args.continue) {
|
|
|
const model = pick(args.model)
|
|
|
const { runInteractiveLocalMode } = await import("./run/runtime")
|
|
|
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,
|
|
|
+ })
|
|
|
+}
|