mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-17 10:02:51 +00:00
effect(core): add stdin option to AppProcess.run; migrate snapshot+clipboard (#27224)
This commit is contained in:
@@ -3,9 +3,21 @@ import { lazy } from "../../../../util/lazy.js"
|
||||
import { tmpdir } from "os"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { Effect } from "effect"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { AppProcess } from "@opencode-ai/core/process"
|
||||
import * as Filesystem from "../../../../util/filesystem"
|
||||
import * as Process from "../../../../util/process"
|
||||
|
||||
const writeWithStdin = (cmd: string[], text: string): Promise<void> =>
|
||||
Effect.runPromise(
|
||||
AppProcess.Service.use((svc) => svc.run(ChildProcess.make(cmd[0]!, cmd.slice(1)), { stdin: text })).pipe(
|
||||
Effect.provide(AppProcess.defaultLayer),
|
||||
Effect.catch(() => Effect.void),
|
||||
Effect.asVoid,
|
||||
),
|
||||
).catch(() => undefined)
|
||||
|
||||
// Lazy load which and clipboardy to avoid expensive execa/which/isexe chain at startup
|
||||
const getWhich = lazy(async () => {
|
||||
const { which } = await import("../../../../util/which")
|
||||
@@ -125,49 +137,23 @@ const getCopyMethod = lazy(async () => {
|
||||
if (os === "linux") {
|
||||
if (process.env["WAYLAND_DISPLAY"] && which("wl-copy")) {
|
||||
console.log("clipboard: using wl-copy")
|
||||
return async (text: string) => {
|
||||
const proc = Process.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
|
||||
if (!proc.stdin) return
|
||||
proc.stdin.write(text)
|
||||
proc.stdin.end()
|
||||
await proc.exited.catch(() => {})
|
||||
}
|
||||
return (text: string) => writeWithStdin(["wl-copy"], text)
|
||||
}
|
||||
if (which("xclip")) {
|
||||
console.log("clipboard: using xclip")
|
||||
return async (text: string) => {
|
||||
const proc = Process.spawn(["xclip", "-selection", "clipboard"], {
|
||||
stdin: "pipe",
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
})
|
||||
if (!proc.stdin) return
|
||||
proc.stdin.write(text)
|
||||
proc.stdin.end()
|
||||
await proc.exited.catch(() => {})
|
||||
}
|
||||
return (text: string) => writeWithStdin(["xclip", "-selection", "clipboard"], text)
|
||||
}
|
||||
if (which("xsel")) {
|
||||
console.log("clipboard: using xsel")
|
||||
return async (text: string) => {
|
||||
const proc = Process.spawn(["xsel", "--clipboard", "--input"], {
|
||||
stdin: "pipe",
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
})
|
||||
if (!proc.stdin) return
|
||||
proc.stdin.write(text)
|
||||
proc.stdin.end()
|
||||
await proc.exited.catch(() => {})
|
||||
}
|
||||
return (text: string) => writeWithStdin(["xsel", "--clipboard", "--input"], text)
|
||||
}
|
||||
}
|
||||
|
||||
if (os === "win32") {
|
||||
console.log("clipboard: using powershell")
|
||||
return async (text: string) => {
|
||||
return (text: string) =>
|
||||
// Pipe via stdin to avoid PowerShell string interpolation ($env:FOO, $(), etc.)
|
||||
const proc = Process.spawn(
|
||||
writeWithStdin(
|
||||
[
|
||||
"powershell.exe",
|
||||
"-NonInteractive",
|
||||
@@ -175,18 +161,8 @@ const getCopyMethod = lazy(async () => {
|
||||
"-Command",
|
||||
"[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())",
|
||||
],
|
||||
{
|
||||
stdin: "pipe",
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
},
|
||||
text,
|
||||
)
|
||||
|
||||
if (!proc.stdin) return
|
||||
proc.stdin.write(text)
|
||||
proc.stdin.end()
|
||||
await proc.exited.catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
console.log("clipboard: no native support")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Cause, Duration, Effect, Layer, Schedule, Schema, Semaphore, Context, Stream } from "effect"
|
||||
import { Cause, Duration, Effect, Layer, Schedule, Schema, Semaphore, Context } from "effect"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { formatPatch, structuredPatch } from "diff"
|
||||
import path from "path"
|
||||
@@ -84,48 +84,13 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
|
||||
const args = (cmd: string[]) => ["--git-dir", state.gitdir, "--work-tree", state.worktree, ...cmd]
|
||||
|
||||
const enc = new TextEncoder()
|
||||
const feed = (list: string[]) => Stream.make(enc.encode(list.join("\0") + "\0"))
|
||||
|
||||
const gitWithStdin = Effect.fnUntraced(
|
||||
function* (
|
||||
cmd: string[],
|
||||
opts: { cwd?: string; env?: Record<string, string>; stdin: ChildProcess.CommandInput },
|
||||
) {
|
||||
// stdin-feed calls still need raw spawn — AppProcess.run does not yet
|
||||
// expose a stdin Stream API. Tracked as future AppProcess helper.
|
||||
const proc = ChildProcess.make("git", cmd, {
|
||||
cwd: opts.cwd,
|
||||
env: opts.env,
|
||||
extendEnv: true,
|
||||
stdin: opts.stdin,
|
||||
})
|
||||
const handle = yield* appProcess.spawn(proc)
|
||||
const [text, stderr] = yield* Effect.all(
|
||||
[Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))],
|
||||
{ concurrency: 2 },
|
||||
)
|
||||
const code = yield* handle.exitCode
|
||||
return { code, text, stderr } satisfies GitResult
|
||||
},
|
||||
Effect.scoped,
|
||||
Effect.catch((err) =>
|
||||
Effect.succeed({
|
||||
code: ChildProcessSpawner.ExitCode(1),
|
||||
text: "",
|
||||
stderr: err instanceof Error ? err.message : String(err),
|
||||
}),
|
||||
),
|
||||
)
|
||||
const feed = (list: string[]) => list.join("\0") + "\0"
|
||||
|
||||
const git = Effect.fnUntraced(
|
||||
function* (cmd: string[], opts?: { cwd?: string; env?: Record<string, string> }) {
|
||||
function* (cmd: string[], opts?: { cwd?: string; env?: Record<string, string>; stdin?: string }) {
|
||||
const result = yield* appProcess.run(
|
||||
ChildProcess.make("git", cmd, {
|
||||
cwd: opts?.cwd,
|
||||
env: opts?.env,
|
||||
extendEnv: true,
|
||||
}),
|
||||
ChildProcess.make("git", cmd, { cwd: opts?.cwd, env: opts?.env, extendEnv: true }),
|
||||
{ stdin: opts?.stdin },
|
||||
)
|
||||
return {
|
||||
code: ChildProcessSpawner.ExitCode(result.exitCode),
|
||||
@@ -144,7 +109,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
|
||||
const ignore = Effect.fnUntraced(function* (files: string[]) {
|
||||
if (!files.length) return new Set<string>()
|
||||
const check = yield* gitWithStdin(
|
||||
const check = yield* git(
|
||||
[
|
||||
...quote,
|
||||
"--git-dir",
|
||||
@@ -167,7 +132,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
|
||||
const drop = Effect.fnUntraced(function* (files: string[]) {
|
||||
if (!files.length) return
|
||||
yield* gitWithStdin(
|
||||
yield* git(
|
||||
[
|
||||
...cfg,
|
||||
...args(["rm", "--cached", "-f", "--ignore-unmatch", "--pathspec-from-file=-", "--pathspec-file-nul"]),
|
||||
@@ -181,7 +146,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
|
||||
const stage = Effect.fnUntraced(function* (files: string[]) {
|
||||
if (!files.length) return
|
||||
const result = yield* gitWithStdin(
|
||||
const result = yield* git(
|
||||
[...cfg, ...args(["add", "--all", "--sparse", "--pathspec-from-file=-", "--pathspec-file-nul"])],
|
||||
{
|
||||
cwd: state.directory,
|
||||
@@ -590,26 +555,21 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||
})
|
||||
if (!refs.length) return new Map<string, { before: string; after: string }>()
|
||||
|
||||
// cat-file --batch is a stdin-feed call — kept on raw spawn
|
||||
// until AppProcess.run exposes a stdin Stream API.
|
||||
const proc = ChildProcess.make("git", [...cfg, ...args(["cat-file", "--batch"])], {
|
||||
cwd: state.directory,
|
||||
extendEnv: true,
|
||||
stdin: Stream.make(new TextEncoder().encode(refs.map((item) => item.ref).join("\n") + "\n")),
|
||||
})
|
||||
const handle = yield* appProcess.spawn(proc)
|
||||
const [out, err] = yield* Effect.all(
|
||||
[Stream.mkUint8Array(handle.stdout), Stream.mkString(Stream.decodeText(handle.stderr))],
|
||||
{ concurrency: 2 },
|
||||
const batch = yield* appProcess.run(
|
||||
ChildProcess.make("git", [...cfg, ...args(["cat-file", "--batch"])], {
|
||||
cwd: state.directory,
|
||||
extendEnv: true,
|
||||
}),
|
||||
{ stdin: refs.map((item) => item.ref).join("\n") + "\n" },
|
||||
)
|
||||
const code = yield* handle.exitCode
|
||||
if (code !== 0) {
|
||||
if (batch.exitCode !== 0) {
|
||||
log.info("git cat-file --batch failed during snapshot diff, falling back to per-file git show", {
|
||||
stderr: err,
|
||||
stderr: batch.stderr.toString("utf8"),
|
||||
refs: refs.length,
|
||||
})
|
||||
return
|
||||
}
|
||||
const out = batch.stdout
|
||||
|
||||
const fail = (msg: string, extra?: Record<string, string>) => {
|
||||
log.info(msg, { ...extra, refs: refs.length })
|
||||
|
||||
Reference in New Issue
Block a user