浏览代码

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

James Long 1 月之前
父节点
当前提交
aad8d90dd1

+ 10 - 0
packages/core/src/fs-util.ts

@@ -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,

+ 13 - 11
packages/core/src/instruction-context.ts

@@ -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) =>

+ 1 - 1
packages/core/src/project/copy.ts

@@ -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
     })

+ 1 - 1
packages/core/src/session/instructions.ts

@@ -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.

+ 3 - 3
packages/core/src/tool/read.ts

@@ -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(

+ 9 - 5
packages/core/src/tool/shell.ts

@@ -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.`,
               )