Skip to content
Open
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
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion packages/opencode/src/cli/cmd/tui/plugin/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
5 changes: 4 additions & 1 deletion packages/opencode/src/cli/cmd/tui/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/src/config/variable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
5 changes: 5 additions & 0 deletions packages/opencode/src/util/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions packages/opencode/test/cli/tui/thread.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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))
})
})
Loading