From 38138a048e479533bb6488e124167dfc41af0685 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 +- .../src/cli/cmd/tui/plugin/runtime.ts | 5 ++- packages/opencode/src/cli/cmd/tui/thread.ts | 5 ++- packages/opencode/src/config/variable.ts | 3 ++ 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 b80a2389ef24..1197812c377a 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -313,7 +313,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/plugin/runtime.ts b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts index 515e6175631f..5e8de80aeea8 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts @@ -235,12 +235,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/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 7230dae16ae4..9cfc5bccd00a 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -72,7 +72,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 44c985c991dd..5e9739a4cd2b 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/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index 696603adbb7e..483ce0729bea 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -143,6 +143,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 53b7488c2682..c723f7a94b3e 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/thread" +import { Filesystem } from "../../../src/util/filesystem" describe("tui thread", () => { async function check(project?: string) { @@ -26,3 +27,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)) + }) +})