refactor(core): move path resolve into fs service (#35201)

This commit is contained in:
James Long 2026-07-03 13:25:38 -04:00 committed by GitHub
commit aad8d90dd1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 37 additions and 21 deletions

View file

@ -38,6 +38,7 @@ export namespace FSUtil {
readonly ensureDir: (path: string) => Effect.Effect<void, Error>
readonly writeWithDirs: (path: string, content: string | Uint8Array, mode?: number) => Effect.Effect<void, Error>
readonly readDirectoryEntries: (path: string) => Effect.Effect<DirEntry[], Error>
readonly resolve: (path: string) => Effect.Effect<string>
readonly findUp: (target: string, start: string, stop?: string) => Effect.Effect<string[], Error>
readonly up: (options: { targets: string[]; start: string; stop?: string }) => Effect.Effect<string[], Error>
readonly globUp: (pattern: string, start: string, stop?: string) => Effect.Effect<string[], Error>
@ -89,6 +90,14 @@ export namespace FSUtil {
})
})
const resolve = Effect.fn("FileSystem.resolve")(function* (path: string) {
const resolved = pathResolve(windowsPath(path))
return yield* fs.realPath(resolved).pipe(
Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(resolved)),
Effect.orDie,
)
})
const readJson = Effect.fn("FileSystem.readJson")(function* (path: string) {
const text = yield* fs.readFileString(path)
return yield* Effect.try({
@ -187,6 +196,7 @@ export namespace FSUtil {
isDir,
isFile,
readDirectoryEntries,
resolve,
readJson,
writeJson,
ensureDir,

View file

@ -43,22 +43,24 @@ const layer = Layer.effect(
})
const observe = Effect.fn("InstructionContext.observe")(function* () {
const start = FSUtil.resolve(location.directory)
const stop = FSUtil.resolve(location.project.directory)
const start = yield* fs.resolve(location.directory)
const stop = yield* fs.resolve(location.project.directory)
const fromProject = relative(stop, start)
const insideProject =
fromProject === "" || (fromProject !== ".." && !fromProject.startsWith(`..${sep}`) && !isAbsolute(fromProject))
const discovered = new Set(
(Flag.OPENCODE_DISABLE_PROJECT_CONFIG || !insideProject
? []
: yield* fs.up({
targets: ["AGENTS.md"],
start,
stop,
})
).map(FSUtil.resolve),
yield* Effect.forEach(
Flag.OPENCODE_DISABLE_PROJECT_CONFIG || !insideProject
? []
: yield* fs.up({
targets: ["AGENTS.md"],
start,
stop,
}),
fs.resolve,
),
)
const paths = Array.dedupe([FSUtil.resolve(join(global.config, "AGENTS.md")), ...discovered])
const paths = Array.dedupe([yield* fs.resolve(join(global.config, "AGENTS.md")), ...discovered])
const files = yield* Effect.forEach(
paths,
(path) =>

View file

@ -139,7 +139,7 @@ const layer = Layer.effect(
})
const canonical = Effect.fnUntraced(function* (input: AbsolutePath) {
const resolved = AbsolutePath.make(FSUtil.resolve(input))
const resolved = AbsolutePath.make(yield* fs.resolve(input))
if (!(yield* fs.isDir(resolved))) return yield* new DirectoryUnavailableError({ directory: input })
return resolved
})

View file

@ -35,7 +35,7 @@ const layer = Layer.effect(
// Resolved once for the Location layer; the synthetic text and dedup ledger keep
// absolute paths, but the human-facing description shows paths relative to the project
// root so opening a subdirectory still describes paths from the project root.
const root = FSUtil.resolve(location.project.directory)
const root = yield* fs.resolve(location.project.directory)
// Same-turn parallel reads settle concurrently, so an in-memory claim guards each
// Session/path pair before any filesystem work. The durable history check below covers
// paths injected in earlier turns after this Location layer was reopened.

View file

@ -97,8 +97,8 @@ export const Plugin = {
// skipped, and discovery failures never fail the read.
yield* Effect.gen(function* () {
if (target.externalDirectory !== undefined) return
const resolved = FSUtil.resolve(target.canonical)
const root = FSUtil.resolve(location.directory)
const resolved = yield* fs.resolve(target.canonical)
const root = yield* fs.resolve(location.directory)
// up() searches its stop directory, so the Location-root AGENTS.md (already
// supplied by the core/instructions baseline) is dropped by the dirname filter.
const discovered = yield* fs.up({
@ -106,7 +106,7 @@ export const Plugin = {
start: type === "directory" ? resolved : dirname(resolved),
stop: root,
})
const candidates = discovered.map(FSUtil.resolve).filter((file) => dirname(file) !== root)
const candidates = (yield* Effect.forEach(discovered, fs.resolve)).filter((file) => dirname(file) !== root)
if (candidates.length === 0) return
yield* sessionInstructions.load({ sessionID: context.sessionID, paths: candidates })
}).pipe(

View file

@ -80,17 +80,21 @@ const modelOutput = (output: Output): string | undefined => {
const shellTokens = (command: string) => command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? []
const unquote = (value: string) => value.replace(/^(['"])(.*)\1$/, "$2")
const externalCommandDirectories = (command: string, cwd: string) => {
const externalCommandDirectories = Effect.fn("ShellTool.externalCommandDirectories")(function* (
fs: FSUtil.Interface,
command: string,
cwd: string,
) {
const directories = new Set<string>()
for (const token of shellTokens(command)) {
const value = unquote(token).replace(/[;,|&]+$/, "")
if (!path.isAbsolute(value)) continue
const resolved = FSUtil.resolve(value)
const resolved = yield* fs.resolve(value)
if (FSUtil.contains(cwd, resolved)) continue
directories.add(FSUtil.resolve(path.dirname(resolved)))
directories.add(yield* fs.resolve(path.dirname(resolved)))
}
return [...directories]
}
})
export const Plugin = {
id: "core-shell-tool",
@ -168,7 +172,7 @@ export const Plugin = {
agent: context.agent,
source,
})
const warnings = externalCommandDirectories(input.command, target.canonical).map(
const warnings = (yield* externalCommandDirectories(fsUtil, input.command, target.canonical)).map(
(directory) =>
`Command argument references external directory ${path.join(directory, "*").replaceAll("\\", "/")}. Shell runs with host-user filesystem, process, and network authority; this scan is advisory only.`,
)