mini: move frontend into tui package (#37754)
This commit is contained in:
parent
3f5ad8441f
commit
c50554d907
80 changed files with 2365 additions and 1488 deletions
209
packages/cli/src/mini-host.ts
Normal file
209
packages/cli/src/mini-host.ts
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
import type { MiniFrontendInput } from "@opencode-ai/tui/mini"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import fs from "node:fs"
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { ReadStream } from "node:tty"
|
||||
|
||||
export const INTERACTIVE_INPUT_ERROR = "opencode mini requires a controlling terminal for input"
|
||||
|
||||
export type InteractiveStdin = {
|
||||
stdin: NodeJS.ReadStream
|
||||
cleanup(): void
|
||||
}
|
||||
|
||||
type MiniHost = MiniFrontendInput["host"]
|
||||
type ModelState = Record<string, unknown> & {
|
||||
variant?: Record<string, string | undefined>
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function state(value: unknown): ModelState {
|
||||
if (!isRecord(value)) return {}
|
||||
const variant = isRecord(value.variant)
|
||||
? Object.fromEntries(
|
||||
Object.entries(value.variant).flatMap(([key, item]) =>
|
||||
typeof item === "string" ? ([[key, item]] as const) : [],
|
||||
),
|
||||
)
|
||||
: undefined
|
||||
return { ...value, variant }
|
||||
}
|
||||
|
||||
function variantKey(model: NonNullable<MiniFrontendInput["model"]>) {
|
||||
return `${model.providerID}/${model.modelID}`
|
||||
}
|
||||
|
||||
function preferences(statePath: string): MiniHost["preferences"] {
|
||||
const file = path.join(statePath, "model.json")
|
||||
const read = () =>
|
||||
readFile(file, "utf8")
|
||||
.then((value) => state(JSON.parse(value)))
|
||||
.catch(() => state(undefined))
|
||||
return {
|
||||
async resolveVariant(model) {
|
||||
if (!model) return
|
||||
return (await read()).variant?.[variantKey(model)]
|
||||
},
|
||||
async saveVariant(model, variant) {
|
||||
if (!model) return
|
||||
const current = await read()
|
||||
const next = { ...current.variant }
|
||||
if (variant) next[variantKey(model)] = variant
|
||||
if (!variant) delete next[variantKey(model)]
|
||||
await mkdir(path.dirname(file), { recursive: true })
|
||||
.then(() => writeFile(file, JSON.stringify({ ...current, variant: next }, null, 2)))
|
||||
.catch(() => {})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function signal(name: "SIGINT" | "SIGUSR2"): MiniHost["signals"]["sigint"] {
|
||||
return {
|
||||
subscribe(listener) {
|
||||
let subscribed = true
|
||||
process.on(name, listener)
|
||||
return () => {
|
||||
if (!subscribed) return
|
||||
subscribed = false
|
||||
process.off(name, listener)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createTrace(
|
||||
logPath: string,
|
||||
diagnostics: Pick<MiniHost["diagnostics"], "pid" | "cwd" | "argv">,
|
||||
): MiniHost["diagnostics"]["trace"] {
|
||||
if (!process.env.OPENCODE_DIRECT_TRACE) return
|
||||
const stamp = new Date()
|
||||
.toISOString()
|
||||
.replace(/[-:]/g, "")
|
||||
.replace(/\.\d+Z$/, "Z")
|
||||
const target = path.join(logPath, "direct", `${stamp}-${diagnostics.pid}.jsonl`)
|
||||
const text = (data: unknown) =>
|
||||
JSON.stringify(data, (_key, value) => (typeof value === "bigint" ? String(value) : value), 0)
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(logPath, "direct", "latest.json"),
|
||||
text({
|
||||
time: new Date().toISOString(),
|
||||
...diagnostics,
|
||||
path: target,
|
||||
}) + "\n",
|
||||
)
|
||||
const trace = {
|
||||
write(type: string, data?: unknown) {
|
||||
fs.appendFileSync(
|
||||
target,
|
||||
text({
|
||||
time: new Date().toISOString(),
|
||||
pid: diagnostics.pid,
|
||||
type,
|
||||
data,
|
||||
}) + "\n",
|
||||
)
|
||||
},
|
||||
}
|
||||
trace.write("trace.start", {
|
||||
argv: diagnostics.argv,
|
||||
cwd: diagnostics.cwd,
|
||||
path: target,
|
||||
})
|
||||
return trace
|
||||
}
|
||||
|
||||
function openTerminalStdin(target: string): NodeJS.ReadStream {
|
||||
return new ReadStream(fs.openSync(target, "r"))
|
||||
}
|
||||
|
||||
export function resolveInteractiveStdin(
|
||||
stdin: NodeJS.ReadStream = process.stdin,
|
||||
open: (target: string) => NodeJS.ReadStream = openTerminalStdin,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): InteractiveStdin {
|
||||
if (stdin.isTTY) return { stdin, cleanup() {} }
|
||||
const target = platform === "win32" ? "CONIN$" : "/dev/tty"
|
||||
try {
|
||||
const source = open(target)
|
||||
let cleaned = false
|
||||
return {
|
||||
stdin: source,
|
||||
cleanup() {
|
||||
if (cleaned) return
|
||||
cleaned = true
|
||||
source.destroy()
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(INTERACTIVE_INPUT_ERROR, { cause: error })
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal Exported for owner-local resource cleanup tests. */
|
||||
export async function usingInteractiveStdin<T>(
|
||||
run: (terminal: InteractiveStdin) => Promise<T>,
|
||||
resolve: () => InteractiveStdin = resolveInteractiveStdin,
|
||||
) {
|
||||
const terminal = resolve()
|
||||
try {
|
||||
return await run(terminal)
|
||||
} finally {
|
||||
terminal.cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal Exported for owner-local host capability tests. */
|
||||
export function createMiniHost(input: {
|
||||
terminal: InteractiveStdin
|
||||
directory: string
|
||||
paths?: MiniHost["paths"]
|
||||
}): MiniHost {
|
||||
const paths = input.paths ?? {
|
||||
home: Global.Path.home,
|
||||
state: Global.Path.state,
|
||||
log: Global.Path.log,
|
||||
}
|
||||
const diagnostics = {
|
||||
pid: process.pid,
|
||||
cwd: input.directory,
|
||||
argv: process.argv.slice(2),
|
||||
}
|
||||
return {
|
||||
terminal: input.terminal,
|
||||
platform: process.platform,
|
||||
stdout: {
|
||||
write(value) {
|
||||
process.stdout.write(value)
|
||||
},
|
||||
},
|
||||
files: {
|
||||
readText: (url) => readFile(new URL(url), "utf8"),
|
||||
},
|
||||
editor: {
|
||||
async open(options) {
|
||||
const { openEditor } = await import("@opencode-ai/tui/editor")
|
||||
return openEditor(options)
|
||||
},
|
||||
},
|
||||
paths,
|
||||
signals: {
|
||||
sigint: signal("SIGINT"),
|
||||
sigusr2: signal("SIGUSR2"),
|
||||
},
|
||||
startup: {
|
||||
showTiming: Flag.OPENCODE_SHOW_TTFD,
|
||||
now: () => performance.now(),
|
||||
},
|
||||
diagnostics: {
|
||||
...diagnostics,
|
||||
trace: createTrace(paths.log, diagnostics),
|
||||
},
|
||||
preferences: preferences(paths.state),
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue