mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-09 10:59:49 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74240c7447 | |||
| 850a08e84c |
@@ -1,5 +1,6 @@
|
|||||||
import { CliRenderEvents, SyntaxStyle, RGBA, type TerminalColors } from "@opentui/core"
|
import { CliRenderEvents, SyntaxStyle, RGBA, type TerminalColors } from "@opentui/core"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
import { existsSync } from "fs"
|
||||||
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
|
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
|
||||||
import { createSimpleContext } from "./helper"
|
import { createSimpleContext } from "./helper"
|
||||||
import { Glob } from "@opencode-ai/shared/util/glob"
|
import { Glob } from "@opencode-ai/shared/util/glob"
|
||||||
@@ -40,7 +41,6 @@ import { useKV } from "./kv"
|
|||||||
import { useRenderer } from "@opentui/solid"
|
import { useRenderer } from "@opentui/solid"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { Global } from "@/global"
|
import { Global } from "@/global"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
|
||||||
import { useTuiConfig } from "./tui-config"
|
import { useTuiConfig } from "./tui-config"
|
||||||
import { isRecord } from "@/util/record"
|
import { isRecord } from "@/util/record"
|
||||||
import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui"
|
import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui"
|
||||||
@@ -477,15 +477,19 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function getCustomThemes() {
|
async function getCustomThemes() {
|
||||||
const directories = [
|
const ups = (start: string) => {
|
||||||
Global.Path.config,
|
const out: string[] = []
|
||||||
...(await Array.fromAsync(
|
let dir = start
|
||||||
Filesystem.up({
|
while (true) {
|
||||||
targets: [".opencode"],
|
const next = path.join(dir, ".opencode")
|
||||||
start: process.cwd(),
|
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> = {}
|
const result: Record<string, ThemeJson> = {}
|
||||||
for (const dir of directories) {
|
for (const dir of directories) {
|
||||||
@@ -496,7 +500,7 @@ async function getCustomThemes() {
|
|||||||
symlink: true,
|
symlink: true,
|
||||||
})) {
|
})) {
|
||||||
const name = path.basename(item, ".json")
|
const name = path.basename(item, ".json")
|
||||||
result[name] = await Filesystem.readJson(item)
|
result[name] = (await Bun.file(item).json()) as ThemeJson
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -2,30 +2,74 @@ import path from "path"
|
|||||||
import os from "os"
|
import os from "os"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
|
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 { NamedError } from "@opencode-ai/shared/util/error"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
import { Global } from "@/global"
|
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 namespace ConfigPaths {
|
||||||
export async function projectFiles(name: string, directory: string, worktree: string) {
|
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) {
|
export async function directories(directory: string, worktree: string) {
|
||||||
return [
|
return [
|
||||||
Global.Path.config,
|
Global.Path.config,
|
||||||
...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG
|
...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG
|
||||||
? await Array.fromAsync(
|
? await withFs((fs) =>
|
||||||
Filesystem.up({
|
fs.up({
|
||||||
targets: [".opencode"],
|
targets: [".opencode"],
|
||||||
start: directory,
|
start: directory,
|
||||||
stop: worktree,
|
stop: worktree,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
: []),
|
: []),
|
||||||
...(await Array.fromAsync(
|
...(await withFs((fs) =>
|
||||||
Filesystem.up({
|
fs.up({
|
||||||
targets: [".opencode"],
|
targets: [".opencode"],
|
||||||
start: Global.Path.home,
|
start: Global.Path.home,
|
||||||
stop: Global.Path.home,
|
stop: Global.Path.home,
|
||||||
@@ -58,8 +102,8 @@ export namespace ConfigPaths {
|
|||||||
|
|
||||||
/** Read a config file, returning undefined for missing files and throwing JsonError for other failures. */
|
/** Read a config file, returning undefined for missing files and throwing JsonError for other failures. */
|
||||||
export async function readFile(filepath: string) {
|
export async function readFile(filepath: string) {
|
||||||
return Filesystem.readText(filepath).catch((err: NodeJS.ErrnoException) => {
|
return withFs((fs) => fs.readFileString(filepath)).catch((err: unknown) => {
|
||||||
if (err.code === "ENOENT") return
|
if (missing(err)) return
|
||||||
throw new JsonError({ path: filepath }, { cause: err })
|
throw new JsonError({ path: filepath }, { cause: err })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -108,11 +152,11 @@ export namespace ConfigPaths {
|
|||||||
|
|
||||||
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
|
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
|
||||||
const fileContent = (
|
const fileContent = (
|
||||||
await Filesystem.readText(resolvedPath).catch((error: NodeJS.ErrnoException) => {
|
await withFs((fs) => fs.readFileString(resolvedPath)).catch((error: unknown) => {
|
||||||
if (missing === "empty") return ""
|
if (missing === "empty") return ""
|
||||||
|
|
||||||
const errMsg = `bad file reference: "${token}"`
|
const errMsg = `bad file reference: "${token}"`
|
||||||
if (error.code === "ENOENT") {
|
if (missing(error)) {
|
||||||
throw new InvalidError(
|
throw new InvalidError(
|
||||||
{
|
{
|
||||||
path: configSource,
|
path: configSource,
|
||||||
|
|||||||
@@ -1,10 +1,33 @@
|
|||||||
|
import { Effect } from "effect"
|
||||||
|
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||||
import { Npm } from "../npm"
|
import { Npm } from "../npm"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { Filesystem } from "../util/filesystem"
|
|
||||||
import { Process } from "../util/process"
|
import { Process } from "../util/process"
|
||||||
import { which } from "../util/which"
|
import { which } from "../util/which"
|
||||||
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
import { Flag } from "@/flag/flag"
|
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 {
|
export interface Info {
|
||||||
name: string
|
name: string
|
||||||
environment?: Record<string, string>
|
environment?: Record<string, string>
|
||||||
@@ -66,9 +89,9 @@ export const prettier: Info = {
|
|||||||
".gql",
|
".gql",
|
||||||
],
|
],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
const items = await Filesystem.findUp("package.json", Instance.directory, Instance.worktree)
|
const items = await find("package.json")
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const json = await Filesystem.readJson<{
|
const json = await readJson<{
|
||||||
dependencies?: Record<string, string>
|
dependencies?: Record<string, string>
|
||||||
devDependencies?: Record<string, string>
|
devDependencies?: Record<string, string>
|
||||||
}>(item)
|
}>(item)
|
||||||
@@ -89,9 +112,9 @@ export const oxfmt: Info = {
|
|||||||
extensions: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"],
|
extensions: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
if (!Flag.OPENCODE_EXPERIMENTAL_OXFMT) return false
|
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) {
|
for (const item of items) {
|
||||||
const json = await Filesystem.readJson<{
|
const json = await readJson<{
|
||||||
dependencies?: Record<string, string>
|
dependencies?: Record<string, string>
|
||||||
devDependencies?: Record<string, string>
|
devDependencies?: Record<string, string>
|
||||||
}>(item)
|
}>(item)
|
||||||
@@ -140,7 +163,7 @@ export const biome: Info = {
|
|||||||
async enabled() {
|
async enabled() {
|
||||||
const configs = ["biome.json", "biome.jsonc"]
|
const configs = ["biome.json", "biome.jsonc"]
|
||||||
for (const config of configs) {
|
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 (found.length > 0) {
|
||||||
const bin = await Npm.which("@biomejs/biome")
|
const bin = await Npm.which("@biomejs/biome")
|
||||||
if (bin) return [bin, "format", "--write", "$FILE"]
|
if (bin) return [bin, "format", "--write", "$FILE"]
|
||||||
@@ -164,7 +187,7 @@ export const clang: Info = {
|
|||||||
name: "clang-format",
|
name: "clang-format",
|
||||||
extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"],
|
extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
const items = await Filesystem.findUp(".clang-format", Instance.directory, Instance.worktree)
|
const items = await find(".clang-format")
|
||||||
if (items.length > 0) {
|
if (items.length > 0) {
|
||||||
const match = which("clang-format")
|
const match = which("clang-format")
|
||||||
if (match) return [match, "-i", "$FILE"]
|
if (match) return [match, "-i", "$FILE"]
|
||||||
@@ -190,10 +213,10 @@ export const ruff: Info = {
|
|||||||
if (!which("ruff")) return false
|
if (!which("ruff")) return false
|
||||||
const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"]
|
const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"]
|
||||||
for (const config of configs) {
|
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 (found.length > 0) {
|
||||||
if (config === "pyproject.toml") {
|
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"]
|
if (content.includes("[tool.ruff]")) return ["ruff", "format", "$FILE"]
|
||||||
} else {
|
} else {
|
||||||
return ["ruff", "format", "$FILE"]
|
return ["ruff", "format", "$FILE"]
|
||||||
@@ -202,9 +225,9 @@ export const ruff: Info = {
|
|||||||
}
|
}
|
||||||
const deps = ["requirements.txt", "pyproject.toml", "Pipfile"]
|
const deps = ["requirements.txt", "pyproject.toml", "Pipfile"]
|
||||||
for (const dep of deps) {
|
for (const dep of deps) {
|
||||||
const found = await Filesystem.findUp(dep, Instance.directory, Instance.worktree)
|
const found = await find(dep)
|
||||||
if (found.length > 0) {
|
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"]
|
if (content.includes("ruff")) return ["ruff", "format", "$FILE"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,7 +311,7 @@ export const ocamlformat: Info = {
|
|||||||
extensions: [".ml", ".mli"],
|
extensions: [".ml", ".mli"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
if (!which("ocamlformat")) return false
|
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"]
|
if (items.length > 0) return ["ocamlformat", "-i", "$FILE"]
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
@@ -358,9 +381,9 @@ export const pint: Info = {
|
|||||||
name: "pint",
|
name: "pint",
|
||||||
extensions: [".php"],
|
extensions: [".php"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
const items = await Filesystem.findUp("composer.json", Instance.directory, Instance.worktree)
|
const items = await find("composer.json")
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const json = await Filesystem.readJson<{
|
const json = await readJson<{
|
||||||
require?: Record<string, string>
|
require?: Record<string, string>
|
||||||
"require-dev"?: Record<string, string>
|
"require-dev"?: Record<string, string>
|
||||||
}>(item)
|
}>(item)
|
||||||
|
|||||||
Reference in New Issue
Block a user