mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-25 01:51:43 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 042e63fdc3 |
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}"`)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { intro, log, outro, spinner } from "@clack/prompts"
|
||||
import type { Argv } from "yargs"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
import { Effect } from "effect"
|
||||
|
||||
import { ConfigPaths } from "../../config/paths"
|
||||
import { Global } from "../../global"
|
||||
@@ -9,6 +7,7 @@ import { installPlugin, patchPluginConfig, readPluginManifest } from "../../plug
|
||||
import { resolvePluginTarget } from "../../plugin/shared"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { errorMessage } from "../../util/error"
|
||||
import { Filesystem } from "../../util/filesystem"
|
||||
import { Process } from "../../util/process"
|
||||
import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
@@ -45,10 +44,6 @@ export type PlugCtx = {
|
||||
directory: string
|
||||
}
|
||||
|
||||
function file<T>(fn: (fs: AppFileSystem.Interface) => Effect.Effect<T, AppFileSystem.Error>) {
|
||||
return Effect.runPromise(AppFileSystem.Service.use(fn).pipe(Effect.provide(AppFileSystem.defaultLayer)))
|
||||
}
|
||||
|
||||
const defaultPlugDeps: PlugDeps = {
|
||||
spinner: () => spinner(),
|
||||
log: {
|
||||
@@ -57,9 +52,11 @@ const defaultPlugDeps: PlugDeps = {
|
||||
success: (msg) => log.success(msg),
|
||||
},
|
||||
resolve: (spec) => resolvePluginTarget(spec),
|
||||
readText: (path) => file((fs) => fs.readFileString(path)),
|
||||
write: (path, text) => file((fs) => fs.writeWithDirs(path, text)),
|
||||
exists: (path) => file((fs) => fs.existsSafe(path)),
|
||||
readText: (file) => Filesystem.readText(file),
|
||||
write: async (file, text) => {
|
||||
await Filesystem.write(file, text)
|
||||
},
|
||||
exists: (file) => Filesystem.exists(file),
|
||||
files: (dir, name) => ConfigPaths.fileInDirectory(dir, name),
|
||||
global: Global.Path.config,
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
type TuiSlotPlugin,
|
||||
type TuiTheme,
|
||||
} from "@opencode-ai/plugin/tui"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
import { Effect, Option } from "effect"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
@@ -34,6 +32,7 @@ import { PluginMeta } from "@/plugin/meta"
|
||||
import { installPlugin as installModulePlugin, patchPluginConfig, readPluginManifest } from "@/plugin/install"
|
||||
import { hasTheme, upsertTheme } from "../context/theme"
|
||||
import { Global } from "@/global"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { Process } from "@/util/process"
|
||||
import { Flock } from "@/util/flock"
|
||||
import { Flag } from "@/flag/flag"
|
||||
@@ -88,10 +87,6 @@ const EMPTY_TUI: TuiPluginModule = {
|
||||
tui: async () => {},
|
||||
}
|
||||
|
||||
function io<T>(fn: (fs: AppFileSystem.Interface) => Effect.Effect<T, AppFileSystem.Error>) {
|
||||
return Effect.runPromise(AppFileSystem.Service.use(fn).pipe(Effect.provide(AppFileSystem.defaultLayer)))
|
||||
}
|
||||
|
||||
function fail(message: string, data: Record<string, unknown>) {
|
||||
if (!("error" in data)) {
|
||||
log.error(message, data)
|
||||
@@ -168,13 +163,13 @@ function createThemeInstaller(
|
||||
: path.join(source_dir, ".opencode", "themes")
|
||||
const dest_dir = meta.scope === "local" ? local_dir : path.join(Global.Path.config, "themes")
|
||||
const dest = path.join(dest_dir, `${name}.json`)
|
||||
const stat = await io((fs) => fs.stat(src)).catch(() => undefined)
|
||||
const mtime = stat ? Option.getOrUndefined(stat.mtime)?.getTime() : undefined
|
||||
const size = stat ? Number(stat.size) : undefined
|
||||
const stat = await Filesystem.statAsync(src)
|
||||
const mtime = stat ? Math.floor(typeof stat.mtimeMs === "bigint" ? Number(stat.mtimeMs) : stat.mtimeMs) : undefined
|
||||
const size = stat ? (typeof stat.size === "bigint" ? Number(stat.size) : stat.size) : undefined
|
||||
const info = {
|
||||
src,
|
||||
dest,
|
||||
mtime: mtime === undefined ? undefined : Math.floor(mtime),
|
||||
mtime,
|
||||
size,
|
||||
}
|
||||
|
||||
@@ -196,7 +191,7 @@ function createThemeInstaller(
|
||||
const prev = plugin.themes[name]
|
||||
if (exists) {
|
||||
if (plugin.meta.state !== "updated") {
|
||||
if (!prev && (await io((fs) => fs.existsSafe(dest)))) {
|
||||
if (!prev && (await Filesystem.exists(dest))) {
|
||||
await save()
|
||||
}
|
||||
return
|
||||
@@ -204,7 +199,7 @@ function createThemeInstaller(
|
||||
if (prev?.dest === dest && prev.mtime === mtime && prev.size === size) return
|
||||
}
|
||||
|
||||
const text = await io((fs) => fs.readFileString(src)).catch((error) => {
|
||||
const text = await Filesystem.readText(src).catch((error) => {
|
||||
log.warn("failed to read tui plugin theme", { path: spec, theme: src, error })
|
||||
return
|
||||
})
|
||||
@@ -224,8 +219,8 @@ function createThemeInstaller(
|
||||
return
|
||||
}
|
||||
|
||||
if (exists || !(await io((fs) => fs.existsSafe(dest)))) {
|
||||
await io((fs) => fs.writeWithDirs(dest, text)).catch((error) => {
|
||||
if (exists || !(await Filesystem.exists(dest))) {
|
||||
await Filesystem.write(dest, text).catch((error) => {
|
||||
log.warn("failed to persist tui plugin theme", { path: spec, theme: src, dest, error })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,32 +1,17 @@
|
||||
import semver from "semver"
|
||||
import z from "zod"
|
||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
import { Effect } from "effect"
|
||||
import { Global } from "../global"
|
||||
import { Log } from "../util/log"
|
||||
import path from "path"
|
||||
import { readdir, rm } from "fs/promises"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { Flock } from "@/util/flock"
|
||||
import { Arborist } from "@npmcli/arborist"
|
||||
|
||||
export namespace Npm {
|
||||
const log = Log.create({ service: "npm" })
|
||||
const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined
|
||||
type Deps = Record<string, string>
|
||||
type Manifest = {
|
||||
dependencies?: Deps
|
||||
devDependencies?: Deps
|
||||
peerDependencies?: Deps
|
||||
optionalDependencies?: Deps
|
||||
}
|
||||
type Lock = {
|
||||
packages?: Record<string, Manifest>
|
||||
}
|
||||
|
||||
function file<T>(fn: (fs: AppFileSystem.Interface) => Effect.Effect<T, AppFileSystem.Error>) {
|
||||
return Effect.runPromise(AppFileSystem.Service.use(fn).pipe(Effect.provide(AppFileSystem.defaultLayer)))
|
||||
}
|
||||
|
||||
export const InstallFailedError = NamedError.create(
|
||||
"NpmInstallFailedError",
|
||||
@@ -78,7 +63,7 @@ export namespace Npm {
|
||||
|
||||
export async function add(pkg: string) {
|
||||
const dir = directory(pkg)
|
||||
await using _ = await Flock.acquire(`npm-install:${AppFileSystem.resolve(dir)}`)
|
||||
await using _ = await Flock.acquire(`npm-install:${Filesystem.resolve(dir)}`)
|
||||
log.info("installing package", {
|
||||
pkg,
|
||||
})
|
||||
@@ -133,18 +118,14 @@ export namespace Npm {
|
||||
await arb.reify().catch(() => {})
|
||||
}
|
||||
|
||||
if (!(await file((fs) => fs.existsSafe(path.join(dir, "node_modules"))))) {
|
||||
if (!(await Filesystem.exists(path.join(dir, "node_modules")))) {
|
||||
log.info("node_modules missing, reifying")
|
||||
await reify()
|
||||
return
|
||||
}
|
||||
|
||||
const pkg = await file((fs) => fs.readJson(path.join(dir, "package.json")))
|
||||
.then((item) => item as Manifest)
|
||||
.catch(() => ({}))
|
||||
const lock = await file((fs) => fs.readJson(path.join(dir, "package-lock.json")))
|
||||
.then((item) => item as Lock)
|
||||
.catch(() => ({}))
|
||||
const pkg = await Filesystem.readJson(path.join(dir, "package.json")).catch(() => ({}))
|
||||
const lock = await Filesystem.readJson(path.join(dir, "package-lock.json")).catch(() => ({}))
|
||||
|
||||
const declared = new Set([
|
||||
...Object.keys(pkg.dependencies || {}),
|
||||
@@ -181,9 +162,9 @@ export namespace Npm {
|
||||
if (files.length === 0) return undefined
|
||||
if (files.length === 1) return files[0]
|
||||
// Multiple binaries — resolve from package.json bin field like npx does
|
||||
const pkgJson = await file((fs) => fs.readJson(path.join(dir, "node_modules", pkg, "package.json")))
|
||||
.then((item) => item as { bin?: string | Record<string, string> })
|
||||
.catch(() => undefined)
|
||||
const pkgJson = await Filesystem.readJson<{ bin?: string | Record<string, string> }>(
|
||||
path.join(dir, "node_modules", pkg, "package.json"),
|
||||
).catch(() => undefined)
|
||||
if (pkgJson?.bin) {
|
||||
const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg
|
||||
const bin = pkgJson.bin
|
||||
|
||||
@@ -6,11 +6,10 @@ import {
|
||||
parse as parseJsonc,
|
||||
printParseErrorCode,
|
||||
} from "jsonc-parser"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
import { Effect } from "effect"
|
||||
|
||||
import { ConfigPaths } from "@/config/paths"
|
||||
import { Global } from "@/global"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { Flock } from "@/util/flock"
|
||||
import { isRecord } from "@/util/record"
|
||||
|
||||
@@ -80,14 +79,12 @@ const defaultInstallDeps: InstallDeps = {
|
||||
resolve: (spec) => resolvePluginTarget(spec),
|
||||
}
|
||||
|
||||
function file<T>(fn: (fs: AppFileSystem.Interface) => Effect.Effect<T, AppFileSystem.Error>) {
|
||||
return Effect.runPromise(AppFileSystem.Service.use(fn).pipe(Effect.provide(AppFileSystem.defaultLayer)))
|
||||
}
|
||||
|
||||
const defaultPatchDeps: PatchDeps = {
|
||||
readText: (path) => file((fs) => fs.readFileString(path)),
|
||||
write: (path, text) => file((fs) => fs.writeWithDirs(path, text)),
|
||||
exists: (path) => file((fs) => fs.existsSafe(path)),
|
||||
readText: (file) => Filesystem.readText(file),
|
||||
write: async (file, text) => {
|
||||
await Filesystem.write(file, text)
|
||||
},
|
||||
exists: (file) => Filesystem.exists(file),
|
||||
files: (dir, name) => ConfigPaths.fileInDirectory(dir, name),
|
||||
}
|
||||
|
||||
@@ -347,7 +344,7 @@ function patchName(kind: Kind): "opencode" | "tui" {
|
||||
|
||||
async function patchOne(dir: string, target: Target, spec: string, force: boolean, dep: PatchDeps): Promise<PatchOne> {
|
||||
const name = patchName(target.kind)
|
||||
await using _ = await Flock.acquire(`plug-config:${AppFileSystem.resolve(path.join(dir, name))}`)
|
||||
await using _ = await Flock.acquire(`plug-config:${Filesystem.resolve(path.join(dir, name))}`)
|
||||
|
||||
const files = dep.files(dir, name)
|
||||
let cfg = files[0]
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
import { Effect, Option } from "effect"
|
||||
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Global } from "@/global"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { Flock } from "@/util/flock"
|
||||
|
||||
import { parsePluginSpecifier, pluginSource } from "./shared"
|
||||
|
||||
export namespace PluginMeta {
|
||||
function io<T>(fn: (fs: AppFileSystem.Interface) => Effect.Effect<T, AppFileSystem.Error>) {
|
||||
return Effect.runPromise(AppFileSystem.Service.use(fn).pipe(Effect.provide(AppFileSystem.defaultLayer)))
|
||||
}
|
||||
|
||||
type Source = "file" | "npm"
|
||||
|
||||
export type Theme = {
|
||||
@@ -66,11 +61,10 @@ export namespace PluginMeta {
|
||||
}
|
||||
|
||||
async function modifiedAt(file: string) {
|
||||
const stat = await io((fs) => fs.stat(file)).catch(() => undefined)
|
||||
const stat = await Filesystem.statAsync(file)
|
||||
if (!stat) return
|
||||
const mtime = Option.getOrUndefined(stat.mtime)?.getTime()
|
||||
if (mtime === undefined) return
|
||||
return Math.floor(mtime)
|
||||
const mtime = stat.mtimeMs
|
||||
return Math.floor(typeof mtime === "bigint" ? Number(mtime) : mtime)
|
||||
}
|
||||
|
||||
function resolvedTarget(target: string) {
|
||||
@@ -80,10 +74,9 @@ export namespace PluginMeta {
|
||||
|
||||
async function npmVersion(target: string) {
|
||||
const resolved = resolvedTarget(target)
|
||||
const stat = await io((fs) => fs.stat(resolved)).catch(() => undefined)
|
||||
const dir = stat?.type === "Directory" ? resolved : path.dirname(resolved)
|
||||
return io((fs) => fs.readJson(path.join(dir, "package.json")))
|
||||
.then((item) => item as { version?: string })
|
||||
const stat = await Filesystem.statAsync(resolved)
|
||||
const dir = stat?.isDirectory() ? resolved : path.dirname(resolved)
|
||||
return Filesystem.readJson<{ version?: string }>(path.join(dir, "package.json"))
|
||||
.then((item) => item.version)
|
||||
.catch(() => undefined)
|
||||
}
|
||||
@@ -119,9 +112,7 @@ export namespace PluginMeta {
|
||||
}
|
||||
|
||||
async function read(file: string): Promise<Store> {
|
||||
return io((fs) => fs.readJson(file))
|
||||
.then((item) => item as Store)
|
||||
.catch(() => ({}) as Store)
|
||||
return Filesystem.readJson<Store>(file).catch(() => ({}) as Store)
|
||||
}
|
||||
|
||||
async function row(item: Touch): Promise<Row> {
|
||||
@@ -163,7 +154,7 @@ export namespace PluginMeta {
|
||||
store[item.id] = hit.entry
|
||||
out.push(hit)
|
||||
}
|
||||
await io((fs) => fs.writeWithDirs(file, JSON.stringify(store, null, 2)))
|
||||
await Filesystem.writeJson(file, store)
|
||||
return out
|
||||
})
|
||||
}
|
||||
@@ -186,7 +177,7 @@ export namespace PluginMeta {
|
||||
...(entry.themes ?? {}),
|
||||
[name]: theme,
|
||||
}
|
||||
await io((fs) => fs.writeWithDirs(file, JSON.stringify(store, null, 2)))
|
||||
await Filesystem.writeJson(file, store)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@ import path from "path"
|
||||
import { fileURLToPath, pathToFileURL } from "url"
|
||||
import npa from "npm-package-arg"
|
||||
import semver from "semver"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
import { Effect } from "effect"
|
||||
import { Npm } from "../npm"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { isRecord } from "@/util/record"
|
||||
|
||||
// Old npm package names for plugins that are now built-in
|
||||
@@ -54,10 +53,6 @@ export type PluginEntry = {
|
||||
|
||||
const INDEX_FILES = ["index.ts", "index.tsx", "index.js", "index.mjs", "index.cjs"]
|
||||
|
||||
function io<T>(fn: (fs: AppFileSystem.Interface) => Effect.Effect<T, AppFileSystem.Error>) {
|
||||
return Effect.runPromise(AppFileSystem.Service.use(fn).pipe(Effect.provide(AppFileSystem.defaultLayer)))
|
||||
}
|
||||
|
||||
export function pluginSource(spec: string): PluginSource {
|
||||
if (isPathPluginSpec(spec)) return "file"
|
||||
return "npm"
|
||||
@@ -93,9 +88,9 @@ function packageMain(pkg: PluginPackage) {
|
||||
|
||||
function resolvePackageFile(spec: string, raw: string, kind: string, pkg: PluginPackage) {
|
||||
const resolved = resolveExportPath(raw, pkg.dir)
|
||||
const root = AppFileSystem.resolve(pkg.dir)
|
||||
const next = AppFileSystem.resolve(resolved)
|
||||
if (!AppFileSystem.contains(root, next)) {
|
||||
const root = Filesystem.resolve(pkg.dir)
|
||||
const next = Filesystem.resolve(resolved)
|
||||
if (!Filesystem.contains(root, next)) {
|
||||
throw new Error(`Plugin ${spec} resolved ${kind} entry outside plugin directory`)
|
||||
}
|
||||
return next
|
||||
@@ -126,15 +121,15 @@ function targetPath(target: string) {
|
||||
async function resolveDirectoryIndex(dir: string) {
|
||||
for (const name of INDEX_FILES) {
|
||||
const file = path.join(dir, name)
|
||||
if (await io((fs) => fs.existsSafe(file))) return file
|
||||
if (await Filesystem.exists(file)) return file
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveTargetDirectory(target: string) {
|
||||
const file = targetPath(target)
|
||||
if (!file) return
|
||||
const stat = await io((fs) => fs.stat(file)).catch(() => undefined)
|
||||
if (stat?.type !== "Directory") return
|
||||
const stat = await Filesystem.statAsync(file)
|
||||
if (!stat?.isDirectory()) return
|
||||
return file
|
||||
}
|
||||
|
||||
@@ -180,13 +175,13 @@ export function isPathPluginSpec(spec: string) {
|
||||
export async function resolvePathPluginTarget(spec: string) {
|
||||
const raw = spec.startsWith("file://") ? fileURLToPath(spec) : spec
|
||||
const file = path.isAbsolute(raw) || /^[A-Za-z]:[\\/]/.test(raw) ? raw : path.resolve(raw)
|
||||
const stat = await io((fs) => fs.stat(file)).catch(() => undefined)
|
||||
if (stat?.type !== "Directory") {
|
||||
const stat = await Filesystem.statAsync(file)
|
||||
if (!stat?.isDirectory()) {
|
||||
if (spec.startsWith("file://")) return spec
|
||||
return pathToFileURL(file).href
|
||||
}
|
||||
|
||||
if (await io((fs) => fs.existsSafe(path.join(file, "package.json")))) {
|
||||
if (await Filesystem.exists(path.join(file, "package.json"))) {
|
||||
return pathToFileURL(file).href
|
||||
}
|
||||
|
||||
@@ -219,10 +214,10 @@ export async function resolvePluginTarget(spec: string) {
|
||||
|
||||
export async function readPluginPackage(target: string): Promise<PluginPackage> {
|
||||
const file = target.startsWith("file://") ? fileURLToPath(target) : target
|
||||
const stat = await io((fs) => fs.stat(file)).catch(() => undefined)
|
||||
const dir = stat?.type === "Directory" ? file : path.dirname(file)
|
||||
const stat = await Filesystem.statAsync(file)
|
||||
const dir = stat?.isDirectory() ? file : path.dirname(file)
|
||||
const pkg = path.join(dir, "package.json")
|
||||
const json = await io((fs) => fs.readJson(pkg)).then((item) => item as Record<string, unknown>)
|
||||
const json = await Filesystem.readJson<Record<string, unknown>>(pkg)
|
||||
return { dir, pkg, json }
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@ import { Global } from "../global"
|
||||
import { Log } from "../util/log"
|
||||
import path from "path"
|
||||
import z from "zod"
|
||||
import { statSync } from "fs"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
import { Effect } from "effect"
|
||||
import { Installation } from "../installation"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { lazy } from "@/util/lazy"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Flock } from "@/util/flock"
|
||||
import { Hash } from "@/util/hash"
|
||||
|
||||
@@ -26,10 +24,6 @@ export namespace ModelsDev {
|
||||
|
||||
type JsonValue = string | number | boolean | null | { [key: string]: JsonValue } | JsonValue[]
|
||||
|
||||
function file<T>(fn: (fs: AppFileSystem.Interface) => Effect.Effect<T, AppFileSystem.Error>) {
|
||||
return Effect.runPromise(AppFileSystem.Service.use(fn).pipe(Effect.provide(AppFileSystem.defaultLayer)))
|
||||
}
|
||||
|
||||
const JsonValue: z.ZodType<JsonValue> = z.lazy(() =>
|
||||
z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(JsonValue), z.record(z.string(), JsonValue)]),
|
||||
)
|
||||
@@ -119,7 +113,7 @@ export namespace ModelsDev {
|
||||
}
|
||||
|
||||
function fresh() {
|
||||
return Date.now() - Number(statSync(filepath, { throwIfNoEntry: false })?.mtimeMs ?? 0) < ttl
|
||||
return Date.now() - Number(Filesystem.stat(filepath)?.mtimeMs ?? 0) < ttl
|
||||
}
|
||||
|
||||
function skip(force: boolean) {
|
||||
@@ -135,7 +129,7 @@ export namespace ModelsDev {
|
||||
}
|
||||
|
||||
export const Data = lazy(async () => {
|
||||
const result = await file((fs) => fs.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath)).catch(() => {})
|
||||
const result = await Filesystem.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).catch(() => {})
|
||||
if (result) return result
|
||||
// @ts-ignore
|
||||
const snapshot = await import("./models-snapshot.js")
|
||||
@@ -144,11 +138,11 @@ export namespace ModelsDev {
|
||||
if (snapshot) return snapshot
|
||||
if (Flag.OPENCODE_DISABLE_MODELS_FETCH) return {}
|
||||
return Flock.withLock(`models-dev:${filepath}`, async () => {
|
||||
const result = await file((fs) => fs.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath)).catch(() => {})
|
||||
const result = await Filesystem.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).catch(() => {})
|
||||
if (result) return result
|
||||
const result2 = await fetchApi()
|
||||
if (result2.ok) {
|
||||
await file((fs) => fs.writeWithDirs(filepath, result2.text)).catch((e) => {
|
||||
await Filesystem.write(filepath, result2.text).catch((e) => {
|
||||
log.error("Failed to write models cache", { error: e })
|
||||
})
|
||||
}
|
||||
@@ -167,7 +161,7 @@ export namespace ModelsDev {
|
||||
if (skip(force)) return ModelsDev.Data.reset()
|
||||
const result = await fetchApi()
|
||||
if (!result.ok) return
|
||||
await file((fs) => fs.writeWithDirs(filepath, result.text))
|
||||
await Filesystem.write(filepath, result.text)
|
||||
ModelsDev.Data.reset()
|
||||
}).catch((e) => {
|
||||
log.error("Failed to fetch models.dev", {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { SQLiteBunDatabase } from "drizzle-orm/bun-sqlite"
|
||||
import type { NodeSQLiteDatabase } from "drizzle-orm/node-sqlite"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
import { Effect } from "effect"
|
||||
import { Global } from "../global"
|
||||
import { Log } from "../util/log"
|
||||
import { ProjectTable } from "../project/project.sql"
|
||||
@@ -9,15 +7,12 @@ import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } fro
|
||||
import { SessionShareTable } from "../share/share.sql"
|
||||
import path from "path"
|
||||
import { existsSync } from "fs"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Glob } from "@opencode-ai/shared/util/glob"
|
||||
|
||||
export namespace JsonMigration {
|
||||
const log = Log.create({ service: "json-migration" })
|
||||
|
||||
function file<T>(fn: (fs: AppFileSystem.Interface) => Effect.Effect<T, AppFileSystem.Error>) {
|
||||
return Effect.runPromise(AppFileSystem.Service.use(fn).pipe(Effect.provide(AppFileSystem.defaultLayer)))
|
||||
}
|
||||
|
||||
export type Progress = {
|
||||
current: number
|
||||
total: number
|
||||
@@ -84,7 +79,7 @@ export namespace JsonMigration {
|
||||
const count = end - start
|
||||
const tasks = new Array(count)
|
||||
for (let i = 0; i < count; i++) {
|
||||
tasks[i] = file((fs) => fs.readJson(files[start + i]))
|
||||
tasks[i] = Filesystem.readJson(files[start + i])
|
||||
}
|
||||
const results = await Promise.allSettled(tasks)
|
||||
const items = new Array(count)
|
||||
|
||||
Reference in New Issue
Block a user