Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions packages/opencode/src/cli/cmd/tui/context/theme.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CliRenderEvents, SyntaxStyle, RGBA, type TerminalColors } from "@opentui/core"
import path from "path"
import { existsSync } from "fs"
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
import { createSimpleContext } from "./helper"
import { Glob } from "@opencode-ai/shared/util/glob"
Expand Down Expand Up @@ -40,7 +41,6 @@ import { useKV } from "./kv"
import { useRenderer } from "@opentui/solid"
import { createStore, produce } from "solid-js/store"
import { Global } from "@/global"
import { Filesystem } from "@/util/filesystem"
import { useTuiConfig } from "./tui-config"
import { isRecord } from "@/util/record"
import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui"
Expand Down Expand Up @@ -477,15 +477,19 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
})

async function getCustomThemes() {
const directories = [
Global.Path.config,
...(await Array.fromAsync(
Filesystem.up({
targets: [".opencode"],
start: process.cwd(),
}),
)),
]
const ups = (start: string) => {
const out: string[] = []
let dir = start
while (true) {
const next = path.join(dir, ".opencode")
if (existsSync(next)) out.push(next)
const parent = path.dirname(dir)
if (parent === dir) return out
dir = parent
}
}

const directories = [Global.Path.config, ...ups(process.cwd())]

const result: Record<string, ThemeJson> = {}
for (const dir of directories) {
Expand All @@ -496,7 +500,7 @@ async function getCustomThemes() {
symlink: true,
})) {
const name = path.basename(item, ".json")
result[name] = await Filesystem.readJson(item)
result[name] = (await Bun.file(item).json()) as ThemeJson
}
}
return result
Expand Down
64 changes: 54 additions & 10 deletions packages/opencode/src/config/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,74 @@ import path from "path"
import os from "os"
import z from "zod"
import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
import { Effect } from "effect"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { NamedError } from "@opencode-ai/shared/util/error"
import { Filesystem } from "@/util/filesystem"
import { Flag } from "@/flag/flag"
import { Global } from "@/global"
import { AppRuntime } from "@/effect/app-runtime"

async function withFs<A>(fn: (fs: AppFileSystem.Interface) => Effect.Effect<A, AppFileSystem.Error>) {
return AppRuntime.runPromise(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
return yield* fn(fs)
}),
)
}

function missing(err: unknown) {
if (typeof err !== "object" || err === null) return false
if ("code" in err && err.code === "ENOENT") return true
return (
"reason" in err &&
typeof err.reason === "object" &&
err.reason !== null &&
"_tag" in err.reason &&
err.reason._tag === "NotFound"
)
}

export namespace ConfigPaths {
export async function projectFiles(name: string, directory: string, worktree: string) {
return Filesystem.findUp([`${name}.json`, `${name}.jsonc`], directory, worktree, { rootFirst: true })
return withFs(
Effect.fn("ConfigPaths.projectFiles")(function* (fs) {
const dirs = [directory]
let dir = directory
while (true) {
if (worktree === dir) break
const parent = path.dirname(dir)
if (parent === dir) break
dirs.push(parent)
dir = parent
}

const out: string[] = []
for (const dir of dirs.toReversed()) {
for (const target of [`${name}.json`, `${name}.jsonc`]) {
const file = path.join(dir, target)
if (yield* fs.existsSafe(file)) out.push(file)
}
}
return out
}),
)
}

export async function directories(directory: string, worktree: string) {
return [
Global.Path.config,
...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG
? await Array.fromAsync(
Filesystem.up({
? await withFs((fs) =>
fs.up({
targets: [".opencode"],
start: directory,
stop: worktree,
}),
)
: []),
...(await Array.fromAsync(
Filesystem.up({
...(await withFs((fs) =>
fs.up({
targets: [".opencode"],
start: Global.Path.home,
stop: Global.Path.home,
Expand Down Expand Up @@ -58,8 +102,8 @@ export namespace ConfigPaths {

/** Read a config file, returning undefined for missing files and throwing JsonError for other failures. */
export async function readFile(filepath: string) {
return Filesystem.readText(filepath).catch((err: NodeJS.ErrnoException) => {
if (err.code === "ENOENT") return
return withFs((fs) => fs.readFileString(filepath)).catch((err: unknown) => {
if (missing(err)) return
throw new JsonError({ path: filepath }, { cause: err })
})
}
Expand Down Expand Up @@ -108,11 +152,11 @@ export namespace ConfigPaths {

const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
const fileContent = (
await Filesystem.readText(resolvedPath).catch((error: NodeJS.ErrnoException) => {
await withFs((fs) => fs.readFileString(resolvedPath)).catch((error: unknown) => {
if (missing === "empty") return ""

const errMsg = `bad file reference: "${token}"`
if (error.code === "ENOENT") {
if (missing(error)) {
throw new InvalidError(
{
path: configSource,
Expand Down
51 changes: 37 additions & 14 deletions packages/opencode/src/format/formatter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
import { Effect } from "effect"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Npm } from "../npm"
import { Instance } from "../project/instance"
import { Filesystem } from "../util/filesystem"
import { Process } from "../util/process"
import { which } from "../util/which"
import { AppRuntime } from "@/effect/app-runtime"
import { Flag } from "@/flag/flag"

async function withFs<A>(fn: (fs: AppFileSystem.Interface) => Effect.Effect<A, AppFileSystem.Error>) {
return AppRuntime.runPromise(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
return yield* fn(fs)
}),
)
}

async function find(target: string) {
return withFs((fs) => fs.findUp(target, Instance.directory, Instance.worktree))
}

async function readJson<T>(file: string) {
return withFs((fs) => fs.readJson(file).pipe(Effect.map((json) => json as T)))
}

async function readText(file: string) {
return withFs((fs) => fs.readFileString(file))
}

export interface Info {
name: string
environment?: Record<string, string>
Expand Down Expand Up @@ -66,9 +89,9 @@ export const prettier: Info = {
".gql",
],
async enabled() {
const items = await Filesystem.findUp("package.json", Instance.directory, Instance.worktree)
const items = await find("package.json")
for (const item of items) {
const json = await Filesystem.readJson<{
const json = await readJson<{
dependencies?: Record<string, string>
devDependencies?: Record<string, string>
}>(item)
Expand All @@ -89,9 +112,9 @@ export const oxfmt: Info = {
extensions: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"],
async enabled() {
if (!Flag.OPENCODE_EXPERIMENTAL_OXFMT) return false
const items = await Filesystem.findUp("package.json", Instance.directory, Instance.worktree)
const items = await find("package.json")
for (const item of items) {
const json = await Filesystem.readJson<{
const json = await readJson<{
dependencies?: Record<string, string>
devDependencies?: Record<string, string>
}>(item)
Expand Down Expand Up @@ -140,7 +163,7 @@ export const biome: Info = {
async enabled() {
const configs = ["biome.json", "biome.jsonc"]
for (const config of configs) {
const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree)
const found = await find(config)
if (found.length > 0) {
const bin = await Npm.which("@biomejs/biome")
if (bin) return [bin, "format", "--write", "$FILE"]
Expand All @@ -164,7 +187,7 @@ export const clang: Info = {
name: "clang-format",
extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"],
async enabled() {
const items = await Filesystem.findUp(".clang-format", Instance.directory, Instance.worktree)
const items = await find(".clang-format")
if (items.length > 0) {
const match = which("clang-format")
if (match) return [match, "-i", "$FILE"]
Expand All @@ -190,10 +213,10 @@ export const ruff: Info = {
if (!which("ruff")) return false
const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"]
for (const config of configs) {
const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree)
const found = await find(config)
if (found.length > 0) {
if (config === "pyproject.toml") {
const content = await Filesystem.readText(found[0])
const content = await readText(found[0])
if (content.includes("[tool.ruff]")) return ["ruff", "format", "$FILE"]
} else {
return ["ruff", "format", "$FILE"]
Expand All @@ -202,9 +225,9 @@ export const ruff: Info = {
}
const deps = ["requirements.txt", "pyproject.toml", "Pipfile"]
for (const dep of deps) {
const found = await Filesystem.findUp(dep, Instance.directory, Instance.worktree)
const found = await find(dep)
if (found.length > 0) {
const content = await Filesystem.readText(found[0])
const content = await readText(found[0])
if (content.includes("ruff")) return ["ruff", "format", "$FILE"]
}
}
Expand Down Expand Up @@ -288,7 +311,7 @@ export const ocamlformat: Info = {
extensions: [".ml", ".mli"],
async enabled() {
if (!which("ocamlformat")) return false
const items = await Filesystem.findUp(".ocamlformat", Instance.directory, Instance.worktree)
const items = await find(".ocamlformat")
if (items.length > 0) return ["ocamlformat", "-i", "$FILE"]
return false
},
Expand Down Expand Up @@ -358,9 +381,9 @@ export const pint: Info = {
name: "pint",
extensions: [".php"],
async enabled() {
const items = await Filesystem.findUp("composer.json", Instance.directory, Instance.worktree)
const items = await find("composer.json")
for (const item of items) {
const json = await Filesystem.readJson<{
const json = await readJson<{
require?: Record<string, string>
"require-dev"?: Record<string, string>
}>(item)
Expand Down
Loading