From 73ca7e15deffb30457faa0f1fa39cf60220c7177 Mon Sep 17 00:00:00 2001 From: LifeJiggy Date: Tue, 26 May 2026 08:21:22 +0100 Subject: [PATCH] fix(tui): handle non-string path.isAbsolute inputs gracefully - Add Filesystem.isAbsolutePath() type-safe wrapper - Guard resolveThreadDirectory against non-string project arg - Guard plugin resolveRoot against non-string root input - Add runtime type check in config file path resolution - Add tests for safe path handling with numeric/null/object inputs --- packages/opencode/src/cli/cmd/run.ts | 2 +- packages/opencode/src/cli/cmd/tui.ts | 5 ++- packages/opencode/src/config/variable.ts | 3 ++ packages/opencode/src/plugin/tui/runtime.ts | 5 ++- packages/opencode/src/util/filesystem.ts | 5 +++ packages/opencode/test/cli/tui/thread.test.ts | 42 +++++++++++++++++++ 6 files changed, 59 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 18d033dadb3c..9d124170750a 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -308,7 +308,7 @@ export const RunCommand = effectCmd({ if (args.attach) return args.dir try { - process.chdir(path.isAbsolute(args.dir) ? args.dir : path.join(root, args.dir)) + process.chdir(Filesystem.isAbsolutePath(args.dir) ? args.dir : path.join(root, args.dir)) return process.cwd() } catch { UI.error("Failed to change directory to " + args.dir) diff --git a/packages/opencode/src/cli/cmd/tui.ts b/packages/opencode/src/cli/cmd/tui.ts index 68941e976ac6..598da50da629 100644 --- a/packages/opencode/src/cli/cmd/tui.ts +++ b/packages/opencode/src/cli/cmd/tui.ts @@ -64,7 +64,10 @@ async function input(value?: string) { export function resolveThreadDirectory(project?: string, envPWD = process.env.PWD, cwd = process.cwd()) { const root = Filesystem.resolve(envPWD ?? cwd) - if (project) return Filesystem.resolve(path.isAbsolute(project) ? project : path.join(root, project)) + if (project !== undefined && project !== null) { + const resolved = Filesystem.isAbsolutePath(project) ? project : path.join(root, String(project)) + return Filesystem.resolve(resolved) + } return Filesystem.resolve(cwd) } diff --git a/packages/opencode/src/config/variable.ts b/packages/opencode/src/config/variable.ts index 52c449538fa1..50f3c7a230d3 100644 --- a/packages/opencode/src/config/variable.ts +++ b/packages/opencode/src/config/variable.ts @@ -63,6 +63,9 @@ export async function substitute(input: SubstituteInput) { filePath = path.join(os.homedir(), filePath.slice(2)) } + if (typeof filePath !== "string") { + throw new InvalidError({ path: configSource, message: `bad file reference: file path must be a string, got ${typeof filePath}` }) + } const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath) const fileContent = ( await Filesystem.readText(resolvedPath).catch((error: NodeJS.ErrnoException) => { diff --git a/packages/opencode/src/plugin/tui/runtime.ts b/packages/opencode/src/plugin/tui/runtime.ts index 4673805cf3bc..c874921f3521 100644 --- a/packages/opencode/src/plugin/tui/runtime.ts +++ b/packages/opencode/src/plugin/tui/runtime.ts @@ -232,12 +232,15 @@ function isTheme(value: unknown) { } function resolveRoot(root: string) { + if (typeof root !== "string") { + return path.resolve(process.cwd(), String(root)) + } if (root.startsWith("file://")) { const file = fileURLToPath(root) if (root.endsWith("/")) return file return path.dirname(file) } - if (path.isAbsolute(root)) return root + if (Filesystem.isAbsolutePath(root)) return root return path.resolve(process.cwd(), root) } diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index 78a89174145d..0b5f905a7f6f 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -144,6 +144,11 @@ export function resolve(p: string): string { } } +export function isAbsolutePath(p: unknown): p is string { + if (typeof p !== "string") return false + return isAbsolute(p) +} + export function resolveFilePath(root: string, file: string): string { const raw = file.startsWith("file://") ? fileURLToPath(file) : file if (isAbsolute(raw)) return raw diff --git a/packages/opencode/test/cli/tui/thread.test.ts b/packages/opencode/test/cli/tui/thread.test.ts index f79fd40da740..836fd86bc59c 100644 --- a/packages/opencode/test/cli/tui/thread.test.ts +++ b/packages/opencode/test/cli/tui/thread.test.ts @@ -3,6 +3,7 @@ import fs from "fs/promises" import path from "path" import { tmpdir } from "../../fixture/fixture" import { resolveThreadDirectory } from "../../../src/cli/cmd/tui" +import { Filesystem } from "../../../src/util/filesystem" describe("tui thread", () => { test("loads the TUI integration lazily", async () => { @@ -34,3 +35,44 @@ describe("tui thread", () => { await check(".") }) }) + +describe("safe path handling", () => { + test("isAbsolutePath returns false for numeric input", () => { + expect(Filesystem.isAbsolutePath(123 as any)).toBe(false) + }) + + test("isAbsolutePath returns false for null", () => { + expect(Filesystem.isAbsolutePath(null as any)).toBe(false) + }) + + test("isAbsolutePath returns false for undefined", () => { + expect(Filesystem.isAbsolutePath(undefined as any)).toBe(false) + }) + + test("isAbsolutePath returns false for object", () => { + expect(Filesystem.isAbsolutePath({} as any)).toBe(false) + }) + + test("isAbsolutePath returns true for absolute path strings", () => { + const abs = process.platform === "win32" ? "C:\\Users" : "/usr" + expect(Filesystem.isAbsolutePath(abs)).toBe(true) + }) + + test("isAbsolutePath returns false for relative path strings", () => { + expect(Filesystem.isAbsolutePath("relative/path")).toBe(false) + }) + + test("resolveThreadDirectory handles numeric project gracefully", () => { + const cwd = process.cwd() + const result = resolveThreadDirectory(42 as any, cwd, cwd) + expect(typeof result).toBe("string") + expect(result.length).toBeGreaterThan(0) + }) + + test("resolveThreadDirectory handles null project gracefully", () => { + const cwd = process.cwd() + const result = resolveThreadDirectory(null as any, cwd, cwd) + expect(typeof result).toBe("string") + expect(result).toBe(Filesystem.resolve(cwd)) + }) +})