Compare commits

...

1 Commits

Author SHA1 Message Date
Kit Langton
042e63fdc3 refactor(opencode): switch cli file io to AppFileSystem 2026-04-15 11:27:54 -04:00
12 changed files with 66 additions and 37 deletions

View File

@@ -33,7 +33,6 @@ import {
import { Log } from "../util/log"
import { pathToFileURL } from "url"
import { Filesystem } from "../util/filesystem"
import { Hash } from "../util/hash"
import { ACPSessionManager } from "./session"
import type { ACPConfig } from "./types"
@@ -48,7 +47,9 @@ import { Todo } from "@/session/todo"
import { z } from "zod"
import { LoadAPIKeyError } from "ai"
import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { applyPatch } from "diff"
import { Effect } from "effect"
type ModeOption = { id: string; name: string; description?: string }
type ModelOption = { modelId: string; name: string }
@@ -238,7 +239,13 @@ export namespace ACP {
const metadata = permission.metadata || {}
const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : ""
const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : ""
const content = (await Filesystem.exists(filepath)) ? await Filesystem.readText(filepath) : ""
const content = await AppRuntime.runPromise(
AppFileSystem.Service.use((fs) =>
fs
.existsSafe(filepath)
.pipe(Effect.flatMap((exists) => (exists ? fs.readFileString(filepath) : Effect.succeed("")))),
),
)
const newContent = getNewContent(content, diff)
if (newContent) {

View File

@@ -7,11 +7,11 @@ import { Agent } from "../../agent/agent"
import { Provider } from "../../provider/provider"
import path from "path"
import fs from "fs/promises"
import { Filesystem } from "../../util/filesystem"
import matter from "gray-matter"
import { Instance } from "../../project/instance"
import { EOL } from "os"
import type { Argv } from "yargs"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
type AgentMode = "all" | "primary" | "subagent"
@@ -194,7 +194,7 @@ const AgentCreateCommand = cmd({
await fs.mkdir(targetPath, { recursive: true })
if (await Filesystem.exists(filePath)) {
if (await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.existsSafe(filePath)))) {
if (isFullyNonInteractive) {
console.error(`Error: Agent file already exists: ${filePath}`)
process.exit(1)
@@ -203,7 +203,7 @@ const AgentCreateCommand = cmd({
throw new UI.CancelledError()
}
await Filesystem.write(filePath, content)
await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.writeWithDirs(filePath, content)))
if (isFullyNonInteractive) {
console.log(filePath)

View File

@@ -1,6 +1,5 @@
import path from "path"
import { exec } from "child_process"
import { Filesystem } from "../../util/filesystem"
import * as prompts from "@clack/prompts"
import { map, pipe, sortBy, values } from "remeda"
import { Octokit } from "@octokit/rest"
@@ -34,6 +33,7 @@ import { Git } from "@/git"
import { setTimeout as sleep } from "node:timers/promises"
import { Process } from "@/util/process"
import { Effect } from "effect"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
type GitHubAuthor = {
login: string
@@ -381,9 +381,11 @@ export const GithubInstallCommand = cmd({
? ""
: `\n env:${providers[provider].env.map((e) => `\n ${e}: \${{ secrets.${e} }}`).join("")}`
await Filesystem.write(
path.join(app.root, WORKFLOW_FILE),
`name: opencode
await AppRuntime.runPromise(
AppFileSystem.Service.use((fs) =>
fs.writeWithDirs(
path.join(app.root, WORKFLOW_FILE),
`name: opencode
on:
issue_comment:
@@ -414,6 +416,8 @@ jobs:
uses: anomalyco/opencode/github@latest${envStr}
with:
model: ${provider}/${model}`,
),
),
)
prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`)

View File

@@ -9,8 +9,8 @@ import { SessionTable, MessageTable, PartTable } from "../../session/session.sql
import { Instance } from "../../project/instance"
import { ShareNext } from "../../share/share-next"
import { EOL } from "os"
import { Filesystem } from "../../util/filesystem"
import { AppRuntime } from "@/effect/app-runtime"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
/** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
export type ShareData =
@@ -140,7 +140,9 @@ export const ImportCommand = cmd({
exportData = transformed
} else {
exportData = await Filesystem.readJson<NonNullable<typeof exportData>>(args.file).catch(() => undefined)
exportData = (await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.readJson(args.file))).catch(
() => undefined,
)) as NonNullable<typeof exportData> | undefined
if (!exportData) {
process.stdout.write(`File not found: ${args.file}`)
process.stdout.write(EOL)

View File

@@ -13,10 +13,10 @@ import { Installation } from "../../installation"
import path from "path"
import { Global } from "../../global"
import { modify, applyEdits } from "jsonc-parser"
import { Filesystem } from "../../util/filesystem"
import { Bus } from "../../bus"
import { AppRuntime } from "../../effect/app-runtime"
import { Effect } from "effect"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
function getAuthStatusIcon(status: MCP.AuthStatus): string {
switch (status) {
@@ -416,7 +416,7 @@ async function resolveConfigPath(baseDir: string, global = false) {
}
for (const candidate of candidates) {
if (await Filesystem.exists(candidate)) {
if (await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.existsSafe(candidate)))) {
return candidate
}
}
@@ -427,8 +427,8 @@ async function resolveConfigPath(baseDir: string, global = false) {
async function addMcpToConfig(name: string, mcpConfig: Config.Mcp, configPath: string) {
let text = "{}"
if (await Filesystem.exists(configPath)) {
text = await Filesystem.readText(configPath)
if (await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.existsSafe(configPath)))) {
text = await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.readFileString(configPath)))
}
// Use jsonc-parser to modify while preserving comments
@@ -437,7 +437,7 @@ async function addMcpToConfig(name: string, mcpConfig: Config.Mcp, configPath: s
})
const result = applyEdits(text, edits)
await Filesystem.write(configPath, result)
await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.writeWithDirs(configPath, result)))
return configPath
}

View File

@@ -1,12 +1,12 @@
import type { Argv } from "yargs"
import path from "path"
import { pathToFileURL } from "url"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { UI } from "../ui"
import { cmd } from "./cmd"
import { Flag } from "../../flag/flag"
import { bootstrap } from "../bootstrap"
import { EOL } from "os"
import { Filesystem } from "../../util/filesystem"
import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
import { Server } from "../../server/server"
import { Provider } from "../../provider/provider"
@@ -332,12 +332,14 @@ export const RunCommand = cmd({
for (const filePath of list) {
const resolvedPath = path.resolve(process.cwd(), filePath)
if (!(await Filesystem.exists(resolvedPath))) {
if (!(await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.existsSafe(resolvedPath))))) {
UI.error(`File not found: ${filePath}`)
process.exit(1)
}
const mime = (await Filesystem.isDir(resolvedPath)) ? "application/x-directory" : "text/plain"
const mime = (await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.isDir(resolvedPath))))
? "application/x-directory"
: "text/plain"
files.push({
type: "file",

View File

@@ -7,8 +7,8 @@ import { Global } from "../../global"
import fs from "fs/promises"
import path from "path"
import os from "os"
import { Filesystem } from "../../util/filesystem"
import { Process } from "../../util/process"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
interface UninstallArgs {
keepConfig: boolean
@@ -266,7 +266,9 @@ async function getShellConfigFile(): Promise<string | null> {
.catch(() => false)
if (!exists) continue
const content = await Filesystem.readText(file).catch(() => "")
const content = await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.readFileString(file))).catch(
() => "",
)
if (content.includes("# opencode") || content.includes(".opencode/bin")) {
return file
}
@@ -276,7 +278,7 @@ async function getShellConfigFile(): Promise<string | null> {
}
async function cleanShellConfig(file: string) {
const content = await Filesystem.readText(file)
const content = await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.readFileString(file)))
const lines = content.split("\n")
const filtered: string[] = []
@@ -312,7 +314,7 @@ async function cleanShellConfig(file: string) {
}
const output = filtered.join("\n") + "\n"
await Filesystem.write(file, output)
await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.writeWithDirs(file, output)))
}
async function getDirectorySize(dir: string): Promise<number> {

View File

@@ -1,7 +1,8 @@
import { NamedError } from "@opencode-ai/shared/util/error"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import matter from "gray-matter"
import { z } from "zod"
import { Filesystem } from "../util/filesystem"
import { AppRuntime } from "@/effect/app-runtime"
export namespace ConfigMarkdown {
export const FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g
@@ -69,7 +70,7 @@ export namespace ConfigMarkdown {
}
export async function parse(filePath: string) {
const template = await Filesystem.readText(filePath)
const template = await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.readFileString(filePath)))
try {
const md = matter(template)

View File

@@ -9,9 +9,9 @@ import { SyncEvent } from "@/sync"
import { EventTable } from "@/sync/event.sql"
import { Flag } from "@/flag/flag"
import { Log } from "@/util/log"
import { Filesystem } from "@/util/filesystem"
import { ProjectID } from "@/project/schema"
import { Slug } from "@opencode-ai/shared/util/slug"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { WorkspaceTable } from "./workspace.sql"
import { getAdaptor } from "./adaptors"
import { WorkspaceInfo } from "./types"
@@ -418,7 +418,7 @@ export namespace Workspace {
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return
if (space.type === "worktree") {
void Filesystem.exists(space.directory!).then((exists) => {
void AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.existsSafe(space.directory!))).then((exists) => {
setStatus(space.id, exists ? "connected" : "error", exists ? undefined : "directory does not exist")
})
return

View File

@@ -1,11 +1,11 @@
import fs from "fs/promises"
import path from "path"
import { fileURLToPath } from "url"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import z from "zod"
import { Cause, Context, Effect, Layer, Queue, Stream } from "effect"
import { ripgrep } from "ripgrep"
import { makeRuntime } from "@/effect/run-service"
import { Filesystem } from "@/util/filesystem"
import { Log } from "@/util/log"
export namespace Ripgrep {
@@ -275,10 +275,11 @@ export namespace Ripgrep {
return Effect.succeed(OPENCODE_RIPGREP_WORKER_PATH)
}
const js = new URL("./ripgrep.worker.js", import.meta.url)
return Effect.tryPromise({
try: () => Filesystem.exists(fileURLToPath(js)),
catch: toError,
}).pipe(Effect.map((exists) => (exists ? js : new URL("./ripgrep.worker.ts", import.meta.url))))
return Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const exists = yield* fs.exists(fileURLToPath(js)).pipe(Effect.orElseSucceed(() => false))
return exists ? js : new URL("./ripgrep.worker.ts", import.meta.url)
})
}
function worker() {

View File

@@ -1,8 +1,9 @@
import fs from "fs/promises"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir"
import path from "path"
import os from "os"
import { Filesystem } from "../util/filesystem"
import { Effect } from "effect"
const app = "opencode"
@@ -36,7 +37,11 @@ await Promise.all([
const CACHE_VERSION = "21"
const version = await Filesystem.readText(path.join(Global.Path.cache, "version")).catch(() => "0")
const version = await Effect.runPromise(
AppFileSystem.Service.use((fs) => fs.readFileString(path.join(Global.Path.cache, "version"))).pipe(
Effect.provide(AppFileSystem.defaultLayer),
),
).catch(() => "0")
if (version !== CACHE_VERSION) {
try {
@@ -50,5 +55,9 @@ if (version !== CACHE_VERSION) {
),
)
} catch (e) {}
await Filesystem.write(path.join(Global.Path.cache, "version"), CACHE_VERSION)
await Effect.runPromise(
AppFileSystem.Service.use((fs) => fs.writeWithDirs(path.join(Global.Path.cache, "version"), CACHE_VERSION)).pipe(
Effect.provide(AppFileSystem.defaultLayer),
),
)
}

View File

@@ -11,10 +11,10 @@ import { UninstallCommand } from "./cli/cmd/uninstall"
import { ModelsCommand } from "./cli/cmd/models"
import { UI } from "./cli/ui"
import { Installation } from "./installation"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { NamedError } from "@opencode-ai/shared/util/error"
import { FormatError } from "./cli/error"
import { ServeCommand } from "./cli/cmd/serve"
import { Filesystem } from "./util/filesystem"
import { DebugCommand } from "./cli/cmd/debug"
import { StatsCommand } from "./cli/cmd/stats"
import { McpCommand } from "./cli/cmd/mcp"
@@ -37,6 +37,7 @@ import { errorMessage } from "./util/error"
import { PluginCommand } from "./cli/cmd/plug"
import { Heap } from "./cli/heap"
import { drizzle } from "drizzle-orm/bun-sqlite"
import { AppRuntime } from "@/effect/app-runtime"
process.on("unhandledRejection", (e) => {
Log.Default.error("rejection", {
@@ -110,7 +111,7 @@ const cli = yargs(args)
})
const marker = path.join(Global.Path.data, "opencode.db")
if (!(await Filesystem.exists(marker))) {
if (!(await AppRuntime.runPromise(AppFileSystem.Service.use((fs) => fs.existsSafe(marker))))) {
const tty = process.stderr.isTTY
process.stderr.write("Performing one time database migration, may take a few minutes..." + EOL)
const width = 36