diff --git a/packages/opencode/src/config/paths.ts b/packages/opencode/src/config/paths.ts index 396417e9a5..3b3876d2a2 100644 --- a/packages/opencode/src/config/paths.ts +++ b/packages/opencode/src/config/paths.ts @@ -1,5 +1,4 @@ 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 { NamedError } from "@opencode-ai/util/error" @@ -109,9 +108,7 @@ export namespace ConfigPaths { } let filePath = token.replace(/^\{file:/, "").replace(/\}$/, "") - if (filePath.startsWith("~/")) { - filePath = path.join(os.homedir(), filePath.slice(2)) - } + filePath = Filesystem.expandHome(filePath) const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath) const fileContent = ( diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index 86f73d0fd2..b482d761c9 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -95,9 +95,7 @@ export namespace InstructionPrompt { if (config.instructions) { for (let instruction of config.instructions) { if (instruction.startsWith("https://") || instruction.startsWith("http://")) continue - if (instruction.startsWith("~/")) { - instruction = path.join(os.homedir(), instruction.slice(2)) - } + instruction = Filesystem.expandHome(instruction) const matches = path.isAbsolute(instruction) ? await Glob.scan(path.basename(instruction), { cwd: path.dirname(instruction), diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 36162656aa..0f130c9d70 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -202,7 +202,7 @@ export namespace SessionPrompt { if (seen.has(name)) return seen.add(name) const filepath = name.startsWith("~/") - ? path.join(os.homedir(), name.slice(2)) + ? Filesystem.expandHome(name) : path.resolve(Instance.worktree, name) const stats = await fs.stat(filepath).catch(() => undefined) diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index 55bfad92a7..e8800f437b 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -1,4 +1,3 @@ -import os from "os" import path from "path" import { pathToFileURL } from "url" import z from "zod" @@ -12,6 +11,7 @@ import { runPromiseInstance } from "@/effect/runtime" import { Flag } from "@/flag/flag" import { Global } from "@/global" import { PermissionNext } from "@/permission" +import { Filesystem } from "@/util/filesystem" import { Config } from "../config/config" import { ConfigMarkdown } from "../config/markdown" import { Log } from "../util/log" @@ -127,14 +127,17 @@ export namespace Skill { symlink: true, dot: opts?.dot, }) - .pipe(Effect.orDie) + .pipe( + Effect.catch((error) => { + if (!opts?.scope) return Effect.fail(error) + return Effect.sync(() => { + log.error(`failed to scan ${opts.scope} skills`, { dir: root, error }) + return [] as string[] + }) + }), + ) - yield* Effect.forEach(matches, (match) => add(match), { concurrency: "unbounded" }).pipe( - Effect.catch((error) => { - if (!opts?.scope) return Effect.die(error) - return Effect.sync(() => log.error(`failed to scan ${opts.scope} skills`, { dir: root, error })) - }), - ) + yield* Effect.forEach(matches, (match) => add(match), { concurrency: "unbounded" }) }) const load = Effect.fn("Skill.load")(function* () { @@ -169,7 +172,7 @@ export namespace Skill { // Phase 4: Custom paths const cfg = yield* Effect.promise(() => Config.get()) for (const item of cfg.skills?.paths ?? []) { - const expanded = item.startsWith("~/") ? path.join(os.homedir(), item.slice(2)) : item + const expanded = Filesystem.expandHome(item) const dir = path.isAbsolute(expanded) ? expanded : path.join(instance.directory, expanded) if (!(yield* fs.isDir(dir).pipe(Effect.orDie))) { log.warn("skill path not found", { path: dir }) diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index 37f00c6b9c..833826c7e3 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -2,6 +2,7 @@ import { chmod, mkdir, readFile, writeFile } from "fs/promises" import { createWriteStream, existsSync, statSync } from "fs" import { lookup } from "mime-types" import { realpathSync } from "fs" +import os from "os" import { dirname, join, relative, resolve as pathResolve } from "path" import { Readable } from "stream" import { pipeline } from "stream/promises" @@ -95,6 +96,10 @@ export namespace Filesystem { } } + export function expandHome(p: string): string { + return p.startsWith("~/") ? join(os.homedir(), p.slice(2)) : p + } + export function mimeType(p: string): string { return lookup(p) || "application/octet-stream" }