Compare commits

...

1 Commits

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