ソースを参照

refactor: unwrap Ripgrep namespace + self-reexport (#22965)

Kit Langton 4 ヶ月 前
コミット
30fc791480
1 ファイル変更486 行追加486 行削除
  1. 486 486
      packages/opencode/src/file/ripgrep.ts

+ 486 - 486
packages/opencode/src/file/ripgrep.ts

@@ -8,568 +8,568 @@ import { ripgrep } from "ripgrep"
 import { Filesystem } from "@/util"
 import { Log } from "@/util"
 
-export namespace Ripgrep {
-  const log = Log.create({ service: "ripgrep" })
-
-  const Stats = z.object({
-    elapsed: z.object({
-      secs: z.number(),
-      nanos: z.number(),
-      human: z.string(),
+const log = Log.create({ service: "ripgrep" })
+
+const Stats = z.object({
+  elapsed: z.object({
+    secs: z.number(),
+    nanos: z.number(),
+    human: z.string(),
+  }),
+  searches: z.number(),
+  searches_with_match: z.number(),
+  bytes_searched: z.number(),
+  bytes_printed: z.number(),
+  matched_lines: z.number(),
+  matches: z.number(),
+})
+
+const Begin = z.object({
+  type: z.literal("begin"),
+  data: z.object({
+    path: z.object({
+      text: z.string(),
     }),
-    searches: z.number(),
-    searches_with_match: z.number(),
-    bytes_searched: z.number(),
-    bytes_printed: z.number(),
-    matched_lines: z.number(),
-    matches: z.number(),
-  })
-
-  const Begin = z.object({
-    type: z.literal("begin"),
-    data: z.object({
-      path: z.object({
-        text: z.string(),
-      }),
+  }),
+})
+
+export const Match = z.object({
+  type: z.literal("match"),
+  data: z.object({
+    path: z.object({
+      text: z.string(),
     }),
-  })
-
-  export const Match = z.object({
-    type: z.literal("match"),
-    data: z.object({
-      path: z.object({
-        text: z.string(),
-      }),
-      lines: z.object({
-        text: z.string(),
-      }),
-      line_number: z.number(),
-      absolute_offset: z.number(),
-      submatches: z.array(
-        z.object({
-          match: z.object({
-            text: z.string(),
-          }),
-          start: z.number(),
-          end: z.number(),
-        }),
-      ),
+    lines: z.object({
+      text: z.string(),
     }),
-  })
-
-  const End = z.object({
-    type: z.literal("end"),
-    data: z.object({
-      path: z.object({
-        text: z.string(),
+    line_number: z.number(),
+    absolute_offset: z.number(),
+    submatches: z.array(
+      z.object({
+        match: z.object({
+          text: z.string(),
+        }),
+        start: z.number(),
+        end: z.number(),
       }),
-      binary_offset: z.number().nullable(),
-      stats: Stats,
+    ),
+  }),
+})
+
+const End = z.object({
+  type: z.literal("end"),
+  data: z.object({
+    path: z.object({
+      text: z.string(),
     }),
-  })
-
-  const Summary = z.object({
-    type: z.literal("summary"),
-    data: z.object({
-      elapsed_total: z.object({
-        human: z.string(),
-        nanos: z.number(),
-        secs: z.number(),
-      }),
-      stats: Stats,
+    binary_offset: z.number().nullable(),
+    stats: Stats,
+  }),
+})
+
+const Summary = z.object({
+  type: z.literal("summary"),
+  data: z.object({
+    elapsed_total: z.object({
+      human: z.string(),
+      nanos: z.number(),
+      secs: z.number(),
     }),
-  })
+    stats: Stats,
+  }),
+})
+
+const Result = z.union([Begin, Match, End, Summary])
+
+export type Result = z.infer<typeof Result>
+export type Match = z.infer<typeof Match>
+export type Item = Match["data"]
+export type Begin = z.infer<typeof Begin>
+export type End = z.infer<typeof End>
+export type Summary = z.infer<typeof Summary>
+export type Row = Match["data"]
+
+export interface SearchResult {
+  items: Item[]
+  partial: boolean
+}
 
-  const Result = z.union([Begin, Match, End, Summary])
+export interface FilesInput {
+  cwd: string
+  glob?: string[]
+  hidden?: boolean
+  follow?: boolean
+  maxDepth?: number
+  signal?: AbortSignal
+}
 
-  export type Result = z.infer<typeof Result>
-  export type Match = z.infer<typeof Match>
-  export type Item = Match["data"]
-  export type Begin = z.infer<typeof Begin>
-  export type End = z.infer<typeof End>
-  export type Summary = z.infer<typeof Summary>
-  export type Row = Match["data"]
+export interface SearchInput {
+  cwd: string
+  pattern: string
+  glob?: string[]
+  limit?: number
+  follow?: boolean
+  file?: string[]
+  signal?: AbortSignal
+}
 
-  export interface SearchResult {
-    items: Item[]
-    partial: boolean
-  }
+export interface TreeInput {
+  cwd: string
+  limit?: number
+  signal?: AbortSignal
+}
 
-  export interface FilesInput {
-    cwd: string
-    glob?: string[]
-    hidden?: boolean
-    follow?: boolean
-    maxDepth?: number
-    signal?: AbortSignal
-  }
+export interface Interface {
+  readonly files: (input: FilesInput) => Stream.Stream<string, Error>
+  readonly tree: (input: TreeInput) => Effect.Effect<string, Error>
+  readonly search: (input: SearchInput) => Effect.Effect<SearchResult, Error>
+}
 
-  export interface SearchInput {
-    cwd: string
-    pattern: string
-    glob?: string[]
-    limit?: number
-    follow?: boolean
-    file?: string[]
-    signal?: AbortSignal
-  }
+export class Service extends Context.Service<Service, Interface>()("@opencode/Ripgrep") {}
 
-  export interface TreeInput {
-    cwd: string
-    limit?: number
-    signal?: AbortSignal
-  }
+type Run = { kind: "files" | "search"; cwd: string; args: string[] }
 
-  export interface Interface {
-    readonly files: (input: FilesInput) => Stream.Stream<string, Error>
-    readonly tree: (input: TreeInput) => Effect.Effect<string, Error>
-    readonly search: (input: SearchInput) => Effect.Effect<SearchResult, Error>
-  }
+type WorkerResult = {
+  type: "result"
+  code: number
+  stdout: string
+  stderr: string
+}
 
-  export class Service extends Context.Service<Service, Interface>()("@opencode/Ripgrep") {}
+type WorkerLine = {
+  type: "line"
+  line: string
+}
 
-  type Run = { kind: "files" | "search"; cwd: string; args: string[] }
+type WorkerDone = {
+  type: "done"
+  code: number
+  stderr: string
+}
 
-  type WorkerResult = {
-    type: "result"
-    code: number
-    stdout: string
-    stderr: string
+type WorkerError = {
+  type: "error"
+  error: {
+    message: string
+    name?: string
+    stack?: string
   }
+}
 
-  type WorkerLine = {
-    type: "line"
-    line: string
-  }
+function env() {
+  const env = Object.fromEntries(
+    Object.entries(process.env).filter((item): item is [string, string] => item[1] !== undefined),
+  )
+  delete env.RIPGREP_CONFIG_PATH
+  return env
+}
 
-  type WorkerDone = {
-    type: "done"
-    code: number
-    stderr: string
-  }
+function text(input: unknown) {
+  if (typeof input === "string") return input
+  if (input instanceof ArrayBuffer) return Buffer.from(input).toString()
+  if (ArrayBuffer.isView(input)) return Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString()
+  return String(input)
+}
 
-  type WorkerError = {
-    type: "error"
-    error: {
-      message: string
-      name?: string
-      stack?: string
-    }
-  }
+function toError(input: unknown) {
+  if (input instanceof Error) return input
+  if (typeof input === "string") return new Error(input)
+  return new Error(String(input))
+}
 
-  function env() {
-    const env = Object.fromEntries(
-      Object.entries(process.env).filter((item): item is [string, string] => item[1] !== undefined),
-    )
-    delete env.RIPGREP_CONFIG_PATH
-    return env
-  }
+function abort(signal?: AbortSignal) {
+  const err = signal?.reason
+  if (err instanceof Error) return err
+  const out = new Error("Aborted")
+  out.name = "AbortError"
+  return out
+}
 
-  function text(input: unknown) {
-    if (typeof input === "string") return input
-    if (input instanceof ArrayBuffer) return Buffer.from(input).toString()
-    if (ArrayBuffer.isView(input)) return Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString()
-    return String(input)
-  }
+function error(stderr: string, code: number) {
+  const err = new Error(stderr.trim() || `ripgrep failed with code ${code}`)
+  err.name = "RipgrepError"
+  return err
+}
 
-  function toError(input: unknown) {
-    if (input instanceof Error) return input
-    if (typeof input === "string") return new Error(input)
-    return new Error(String(input))
-  }
+function clean(file: string) {
+  return path.normalize(file.replace(/^\.[\\/]/, ""))
+}
 
-  function abort(signal?: AbortSignal) {
-    const err = signal?.reason
-    if (err instanceof Error) return err
-    const out = new Error("Aborted")
-    out.name = "AbortError"
-    return out
+function row(data: Row): Row {
+  return {
+    ...data,
+    path: {
+      ...data.path,
+      text: clean(data.path.text),
+    },
   }
+}
 
-  function error(stderr: string, code: number) {
-    const err = new Error(stderr.trim() || `ripgrep failed with code ${code}`)
-    err.name = "RipgrepError"
-    return err
+function opts(cwd: string) {
+  return {
+    env: env(),
+    preopens: { ".": cwd },
   }
+}
 
-  function clean(file: string) {
-    return path.normalize(file.replace(/^\.[\\/]/, ""))
-  }
+function check(cwd: string) {
+  return Effect.tryPromise({
+    try: () => fs.stat(cwd).catch(() => undefined),
+    catch: toError,
+  }).pipe(
+    Effect.flatMap((stat) =>
+      stat?.isDirectory()
+        ? Effect.void
+        : Effect.fail(
+            Object.assign(new Error(`No such file or directory: '${cwd}'`), {
+              code: "ENOENT",
+              errno: -2,
+              path: cwd,
+            }),
+          ),
+    ),
+  )
+}
 
-  function row(data: Row): Row {
-    return {
-      ...data,
-      path: {
-        ...data.path,
-        text: clean(data.path.text),
-      },
+function filesArgs(input: FilesInput) {
+  const args = ["--files", "--glob=!.git/*"]
+  if (input.follow) args.push("--follow")
+  if (input.hidden !== false) args.push("--hidden")
+  if (input.maxDepth !== undefined) args.push(`--max-depth=${input.maxDepth}`)
+  if (input.glob) {
+    for (const glob of input.glob) {
+      args.push(`--glob=${glob}`)
     }
   }
+  args.push(".")
+  return args
+}
 
-  function opts(cwd: string) {
-    return {
-      env: env(),
-      preopens: { ".": cwd },
+function searchArgs(input: SearchInput) {
+  const args = ["--json", "--hidden", "--glob=!.git/*", "--no-messages"]
+  if (input.follow) args.push("--follow")
+  if (input.glob) {
+    for (const glob of input.glob) {
+      args.push(`--glob=${glob}`)
     }
   }
+  if (input.limit) args.push(`--max-count=${input.limit}`)
+  args.push("--", input.pattern, ...(input.file ?? ["."]))
+  return args
+}
 
-  function check(cwd: string) {
-    return Effect.tryPromise({
-      try: () => fs.stat(cwd).catch(() => undefined),
-      catch: toError,
-    }).pipe(
-      Effect.flatMap((stat) =>
-        stat?.isDirectory()
-          ? Effect.void
-          : Effect.fail(
-              Object.assign(new Error(`No such file or directory: '${cwd}'`), {
-                code: "ENOENT",
-                errno: -2,
-                path: cwd,
-              }),
-            ),
-      ),
-    )
-  }
-
-  function filesArgs(input: FilesInput) {
-    const args = ["--files", "--glob=!.git/*"]
-    if (input.follow) args.push("--follow")
-    if (input.hidden !== false) args.push("--hidden")
-    if (input.maxDepth !== undefined) args.push(`--max-depth=${input.maxDepth}`)
-    if (input.glob) {
-      for (const glob of input.glob) {
-        args.push(`--glob=${glob}`)
-      }
-    }
-    args.push(".")
-    return args
-  }
+function parse(stdout: string) {
+  return stdout
+    .trim()
+    .split(/\r?\n/)
+    .filter(Boolean)
+    .map((line) => Result.parse(JSON.parse(line)))
+    .flatMap((item) => (item.type === "match" ? [row(item.data)] : []))
+}
 
-  function searchArgs(input: SearchInput) {
-    const args = ["--json", "--hidden", "--glob=!.git/*", "--no-messages"]
-    if (input.follow) args.push("--follow")
-    if (input.glob) {
-      for (const glob of input.glob) {
-        args.push(`--glob=${glob}`)
-      }
-    }
-    if (input.limit) args.push(`--max-count=${input.limit}`)
-    args.push("--", input.pattern, ...(input.file ?? ["."]))
-    return args
-  }
+declare const OPENCODE_RIPGREP_WORKER_PATH: string
 
-  function parse(stdout: string) {
-    return stdout
-      .trim()
-      .split(/\r?\n/)
-      .filter(Boolean)
-      .map((line) => Result.parse(JSON.parse(line)))
-      .flatMap((item) => (item.type === "match" ? [row(item.data)] : []))
+function target(): Effect.Effect<string | URL, Error> {
+  if (typeof OPENCODE_RIPGREP_WORKER_PATH !== "undefined") {
+    return Effect.succeed(OPENCODE_RIPGREP_WORKER_PATH)
   }
+  const js = new URL("./ripgrep.worker.js", import.meta.url)
+  return Effect.tryPromise({
+    try: () => Filesystem.exists(fileURLToPath(js)),
+    catch: toError,
+  }).pipe(Effect.map((exists) => (exists ? js : new URL("./ripgrep.worker.ts", import.meta.url))))
+}
 
-  declare const OPENCODE_RIPGREP_WORKER_PATH: string
+function worker() {
+  return target().pipe(Effect.flatMap((file) => Effect.sync(() => new Worker(file, { env: env() }))))
+}
 
-  function target(): Effect.Effect<string | URL, Error> {
-    if (typeof OPENCODE_RIPGREP_WORKER_PATH !== "undefined") {
-      return Effect.succeed(OPENCODE_RIPGREP_WORKER_PATH)
-    }
-    const js = new URL("./ripgrep.worker.js", import.meta.url)
-    return Effect.tryPromise({
-      try: () => Filesystem.exists(fileURLToPath(js)),
-      catch: toError,
-    }).pipe(Effect.map((exists) => (exists ? js : new URL("./ripgrep.worker.ts", import.meta.url))))
+function drain(buf: string, chunk: unknown, push: (line: string) => void) {
+  const lines = (buf + text(chunk)).split(/\r?\n/)
+  buf = lines.pop() || ""
+  for (const line of lines) {
+    if (line) push(line)
   }
+  return buf
+}
 
-  function worker() {
-    return target().pipe(Effect.flatMap((file) => Effect.sync(() => new Worker(file, { env: env() }))))
-  }
+function fail(queue: Queue.Queue<string, Error | Cause.Done>, err: Error) {
+  Queue.failCauseUnsafe(queue, Cause.fail(err))
+}
 
-  function drain(buf: string, chunk: unknown, push: (line: string) => void) {
-    const lines = (buf + text(chunk)).split(/\r?\n/)
-    buf = lines.pop() || ""
-    for (const line of lines) {
-      if (line) push(line)
-    }
-    return buf
-  }
+function searchDirect(input: SearchInput) {
+  return Effect.tryPromise({
+    try: () =>
+      ripgrep(searchArgs(input), {
+        buffer: true,
+        ...opts(input.cwd),
+      }),
+    catch: toError,
+  }).pipe(
+    Effect.flatMap((ret) => {
+      const out = ret.stdout ?? ""
+      if (ret.code !== 0 && ret.code !== 1 && ret.code !== 2) {
+        return Effect.fail(error(ret.stderr ?? "", ret.code ?? 1))
+      }
+      return Effect.sync(() => ({
+        items: ret.code === 1 ? [] : parse(out),
+        partial: ret.code === 2,
+      }))
+    }),
+  )
+}
 
-  function fail(queue: Queue.Queue<string, Error | Cause.Done>, err: Error) {
-    Queue.failCauseUnsafe(queue, Cause.fail(err))
-  }
+function searchWorker(input: SearchInput) {
+  if (input.signal?.aborted) return Effect.fail(abort(input.signal))
 
-  function searchDirect(input: SearchInput) {
-    return Effect.tryPromise({
-      try: () =>
-        ripgrep(searchArgs(input), {
-          buffer: true,
-          ...opts(input.cwd),
-        }),
-      catch: toError,
-    }).pipe(
-      Effect.flatMap((ret) => {
-        const out = ret.stdout ?? ""
-        if (ret.code !== 0 && ret.code !== 1 && ret.code !== 2) {
-          return Effect.fail(error(ret.stderr ?? "", ret.code ?? 1))
+  return Effect.acquireUseRelease(
+    worker(),
+    (w) =>
+      Effect.callback<SearchResult, Error>((resume, signal) => {
+        let open = true
+        const done = (effect: Effect.Effect<SearchResult, Error>) => {
+          if (!open) return
+          open = false
+          resume(effect)
         }
-        return Effect.sync(() => ({
-          items: ret.code === 1 ? [] : parse(out),
-          partial: ret.code === 2,
-        }))
-      }),
-    )
-  }
+        const onabort = () => done(Effect.fail(abort(input.signal)))
 
-  function searchWorker(input: SearchInput) {
-    if (input.signal?.aborted) return Effect.fail(abort(input.signal))
-
-    return Effect.acquireUseRelease(
-      worker(),
-      (w) =>
-        Effect.callback<SearchResult, Error>((resume, signal) => {
-          let open = true
-          const done = (effect: Effect.Effect<SearchResult, Error>) => {
-            if (!open) return
-            open = false
-            resume(effect)
+        w.onerror = (evt) => {
+          done(Effect.fail(toError(evt.error ?? evt.message)))
+        }
+        w.onmessage = (evt: MessageEvent<WorkerResult | WorkerError>) => {
+          const msg = evt.data
+          if (msg.type === "error") {
+            done(Effect.fail(Object.assign(new Error(msg.error.message), msg.error)))
+            return
           }
-          const onabort = () => done(Effect.fail(abort(input.signal)))
-
-          w.onerror = (evt) => {
-            done(Effect.fail(toError(evt.error ?? evt.message)))
+          if (msg.code === 1) {
+            done(Effect.succeed({ items: [], partial: false }))
+            return
           }
-          w.onmessage = (evt: MessageEvent<WorkerResult | WorkerError>) => {
-            const msg = evt.data
-            if (msg.type === "error") {
-              done(Effect.fail(Object.assign(new Error(msg.error.message), msg.error)))
-              return
-            }
-            if (msg.code === 1) {
-              done(Effect.succeed({ items: [], partial: false }))
-              return
-            }
-            if (msg.code !== 0 && msg.code !== 1 && msg.code !== 2) {
-              done(Effect.fail(error(msg.stderr, msg.code)))
-              return
-            }
-            done(
-              Effect.sync(() => ({
-                items: parse(msg.stdout),
-                partial: msg.code === 2,
-              })),
-            )
+          if (msg.code !== 0 && msg.code !== 1 && msg.code !== 2) {
+            done(Effect.fail(error(msg.stderr, msg.code)))
+            return
           }
+          done(
+            Effect.sync(() => ({
+              items: parse(msg.stdout),
+              partial: msg.code === 2,
+            })),
+          )
+        }
 
-          input.signal?.addEventListener("abort", onabort, { once: true })
-          signal.addEventListener("abort", onabort, { once: true })
-          w.postMessage({
-            kind: "search",
-            cwd: input.cwd,
-            args: searchArgs(input),
-          } satisfies Run)
+        input.signal?.addEventListener("abort", onabort, { once: true })
+        signal.addEventListener("abort", onabort, { once: true })
+        w.postMessage({
+          kind: "search",
+          cwd: input.cwd,
+          args: searchArgs(input),
+        } satisfies Run)
+
+        return Effect.sync(() => {
+          input.signal?.removeEventListener("abort", onabort)
+          signal.removeEventListener("abort", onabort)
+          w.onerror = null
+          w.onmessage = null
+        })
+      }),
+    (w) => Effect.sync(() => w.terminate()),
+  )
+}
 
-          return Effect.sync(() => {
-            input.signal?.removeEventListener("abort", onabort)
-            signal.removeEventListener("abort", onabort)
-            w.onerror = null
-            w.onmessage = null
-          })
-        }),
-      (w) => Effect.sync(() => w.terminate()),
-    )
-  }
+function filesDirect(input: FilesInput) {
+  return Stream.callback<string, Error>(
+    Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) {
+      let buf = ""
+      let err = ""
 
-  function filesDirect(input: FilesInput) {
-    return Stream.callback<string, Error>(
-      Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) {
-        let buf = ""
-        let err = ""
-
-        const out = {
-          write(chunk: unknown) {
-            buf = drain(buf, chunk, (line) => {
-              Queue.offerUnsafe(queue, clean(line))
-            })
-          },
-        }
+      const out = {
+        write(chunk: unknown) {
+          buf = drain(buf, chunk, (line) => {
+            Queue.offerUnsafe(queue, clean(line))
+          })
+        },
+      }
 
-        const stderr = {
-          write(chunk: unknown) {
-            err += text(chunk)
-          },
-        }
+      const stderr = {
+        write(chunk: unknown) {
+          err += text(chunk)
+        },
+      }
 
-        yield* Effect.forkScoped(
-          Effect.gen(function* () {
-            yield* check(input.cwd)
-            const ret = yield* Effect.tryPromise({
-              try: () =>
-                ripgrep(filesArgs(input), {
-                  stdout: out,
-                  stderr,
-                  ...opts(input.cwd),
-                }),
-              catch: toError,
-            })
-            if (buf) Queue.offerUnsafe(queue, clean(buf))
-            if (ret.code === 0 || ret.code === 1) {
-              Queue.endUnsafe(queue)
-              return
-            }
-            fail(queue, error(err, ret.code ?? 1))
-          }).pipe(
-            Effect.catch((err) =>
-              Effect.sync(() => {
-                fail(queue, err)
+      yield* Effect.forkScoped(
+        Effect.gen(function* () {
+          yield* check(input.cwd)
+          const ret = yield* Effect.tryPromise({
+            try: () =>
+              ripgrep(filesArgs(input), {
+                stdout: out,
+                stderr,
+                ...opts(input.cwd),
               }),
-            ),
+            catch: toError,
+          })
+          if (buf) Queue.offerUnsafe(queue, clean(buf))
+          if (ret.code === 0 || ret.code === 1) {
+            Queue.endUnsafe(queue)
+            return
+          }
+          fail(queue, error(err, ret.code ?? 1))
+        }).pipe(
+          Effect.catch((err) =>
+            Effect.sync(() => {
+              fail(queue, err)
+            }),
           ),
-        )
-      }),
-    )
-  }
+        ),
+      )
+    }),
+  )
+}
 
-  function filesWorker(input: FilesInput) {
-    return Stream.callback<string, Error>(
-      Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) {
-        if (input.signal?.aborted) {
-          fail(queue, abort(input.signal))
-          return
-        }
+function filesWorker(input: FilesInput) {
+  return Stream.callback<string, Error>(
+    Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) {
+      if (input.signal?.aborted) {
+        fail(queue, abort(input.signal))
+        return
+      }
 
-        const w = yield* Effect.acquireRelease(worker(), (w) => Effect.sync(() => w.terminate()))
-        let open = true
-        const close = () => {
-          if (!open) return false
-          open = false
-          return true
-        }
-        const onabort = () => {
-          if (!close()) return
-          fail(queue, abort(input.signal))
-        }
+      const w = yield* Effect.acquireRelease(worker(), (w) => Effect.sync(() => w.terminate()))
+      let open = true
+      const close = () => {
+        if (!open) return false
+        open = false
+        return true
+      }
+      const onabort = () => {
+        if (!close()) return
+        fail(queue, abort(input.signal))
+      }
 
-        w.onerror = (evt) => {
-          if (!close()) return
-          fail(queue, toError(evt.error ?? evt.message))
+      w.onerror = (evt) => {
+        if (!close()) return
+        fail(queue, toError(evt.error ?? evt.message))
+      }
+      w.onmessage = (evt: MessageEvent<WorkerLine | WorkerDone | WorkerError>) => {
+        const msg = evt.data
+        if (msg.type === "line") {
+          if (open) Queue.offerUnsafe(queue, msg.line)
+          return
         }
-        w.onmessage = (evt: MessageEvent<WorkerLine | WorkerDone | WorkerError>) => {
-          const msg = evt.data
-          if (msg.type === "line") {
-            if (open) Queue.offerUnsafe(queue, msg.line)
-            return
-          }
-          if (!close()) return
-          if (msg.type === "error") {
-            fail(queue, Object.assign(new Error(msg.error.message), msg.error))
-            return
-          }
-          if (msg.code === 0 || msg.code === 1) {
-            Queue.endUnsafe(queue)
-            return
-          }
-          fail(queue, error(msg.stderr, msg.code))
+        if (!close()) return
+        if (msg.type === "error") {
+          fail(queue, Object.assign(new Error(msg.error.message), msg.error))
+          return
         }
+        if (msg.code === 0 || msg.code === 1) {
+          Queue.endUnsafe(queue)
+          return
+        }
+        fail(queue, error(msg.stderr, msg.code))
+      }
 
-        yield* Effect.acquireRelease(
+      yield* Effect.acquireRelease(
+        Effect.sync(() => {
+          input.signal?.addEventListener("abort", onabort, { once: true })
+          w.postMessage({
+            kind: "files",
+            cwd: input.cwd,
+            args: filesArgs(input),
+          } satisfies Run)
+        }),
+        () =>
           Effect.sync(() => {
-            input.signal?.addEventListener("abort", onabort, { once: true })
-            w.postMessage({
-              kind: "files",
-              cwd: input.cwd,
-              args: filesArgs(input),
-            } satisfies Run)
+            input.signal?.removeEventListener("abort", onabort)
+            w.onerror = null
+            w.onmessage = null
           }),
-          () =>
-            Effect.sync(() => {
-              input.signal?.removeEventListener("abort", onabort)
-              w.onerror = null
-              w.onmessage = null
-            }),
-        )
-      }),
-    )
-  }
+      )
+    }),
+  )
+}
 
-  export const layer = Layer.effect(
-    Service,
-    Effect.gen(function* () {
-      const source = (input: FilesInput) => {
-        const useWorker = !!input.signal && typeof Worker !== "undefined"
-        if (!useWorker && input.signal) {
-          log.warn("worker unavailable, ripgrep abort disabled")
-        }
-        return useWorker ? filesWorker(input) : filesDirect(input)
+export const layer = Layer.effect(
+  Service,
+  Effect.gen(function* () {
+    const source = (input: FilesInput) => {
+      const useWorker = !!input.signal && typeof Worker !== "undefined"
+      if (!useWorker && input.signal) {
+        log.warn("worker unavailable, ripgrep abort disabled")
       }
+      return useWorker ? filesWorker(input) : filesDirect(input)
+    }
 
-      const files: Interface["files"] = (input) => source(input)
+    const files: Interface["files"] = (input) => source(input)
 
-      const tree: Interface["tree"] = Effect.fn("Ripgrep.tree")(function* (input: TreeInput) {
-        log.info("tree", input)
-        const list = Array.from(yield* source({ cwd: input.cwd, signal: input.signal }).pipe(Stream.runCollect))
+    const tree: Interface["tree"] = Effect.fn("Ripgrep.tree")(function* (input: TreeInput) {
+      log.info("tree", input)
+      const list = Array.from(yield* source({ cwd: input.cwd, signal: input.signal }).pipe(Stream.runCollect))
 
-        interface Node {
-          name: string
-          children: Map<string, Node>
-        }
+      interface Node {
+        name: string
+        children: Map<string, Node>
+      }
 
-        function child(node: Node, name: string) {
-          const item = node.children.get(name)
-          if (item) return item
-          const next = { name, children: new Map() }
-          node.children.set(name, next)
-          return next
-        }
+      function child(node: Node, name: string) {
+        const item = node.children.get(name)
+        if (item) return item
+        const next = { name, children: new Map() }
+        node.children.set(name, next)
+        return next
+      }
 
-        function count(node: Node): number {
-          return Array.from(node.children.values()).reduce((sum, child) => sum + 1 + count(child), 0)
-        }
+      function count(node: Node): number {
+        return Array.from(node.children.values()).reduce((sum, child) => sum + 1 + count(child), 0)
+      }
 
-        const root: Node = { name: "", children: new Map() }
-        for (const file of list) {
-          if (file.includes(".opencode")) continue
-          const parts = file.split(path.sep)
-          if (parts.length < 2) continue
-          let node = root
-          for (const part of parts.slice(0, -1)) {
-            node = child(node, part)
-          }
+      const root: Node = { name: "", children: new Map() }
+      for (const file of list) {
+        if (file.includes(".opencode")) continue
+        const parts = file.split(path.sep)
+        if (parts.length < 2) continue
+        let node = root
+        for (const part of parts.slice(0, -1)) {
+          node = child(node, part)
         }
+      }
 
-        const total = count(root)
-        const limit = input.limit ?? total
-        const lines: string[] = []
-        const queue: Array<{ node: Node; path: string }> = Array.from(root.children.values())
-          .sort((a, b) => a.name.localeCompare(b.name))
-          .map((node) => ({ node, path: node.name }))
-
-        let used = 0
-        for (let i = 0; i < queue.length && used < limit; i++) {
-          const item = queue[i]
-          lines.push(item.path)
-          used++
-          queue.push(
-            ...Array.from(item.node.children.values())
-              .sort((a, b) => a.name.localeCompare(b.name))
-              .map((node) => ({ node, path: `${item.path}/${node.name}` })),
-          )
-        }
+      const total = count(root)
+      const limit = input.limit ?? total
+      const lines: string[] = []
+      const queue: Array<{ node: Node; path: string }> = Array.from(root.children.values())
+        .sort((a, b) => a.name.localeCompare(b.name))
+        .map((node) => ({ node, path: node.name }))
+
+      let used = 0
+      for (let i = 0; i < queue.length && used < limit; i++) {
+        const item = queue[i]
+        lines.push(item.path)
+        used++
+        queue.push(
+          ...Array.from(item.node.children.values())
+            .sort((a, b) => a.name.localeCompare(b.name))
+            .map((node) => ({ node, path: `${item.path}/${node.name}` })),
+        )
+      }
 
-        if (total > used) lines.push(`[${total - used} truncated]`)
-        return lines.join("\n")
-      })
+      if (total > used) lines.push(`[${total - used} truncated]`)
+      return lines.join("\n")
+    })
 
-      const search: Interface["search"] = Effect.fn("Ripgrep.search")(function* (input: SearchInput) {
-        const useWorker = !!input.signal && typeof Worker !== "undefined"
-        if (!useWorker && input.signal) {
-          log.warn("worker unavailable, ripgrep abort disabled")
-        }
-        return yield* useWorker ? searchWorker(input) : searchDirect(input)
-      })
+    const search: Interface["search"] = Effect.fn("Ripgrep.search")(function* (input: SearchInput) {
+      const useWorker = !!input.signal && typeof Worker !== "undefined"
+      if (!useWorker && input.signal) {
+        log.warn("worker unavailable, ripgrep abort disabled")
+      }
+      return yield* useWorker ? searchWorker(input) : searchDirect(input)
+    })
 
-      return Service.of({ files, tree, search })
-    }),
-  )
+    return Service.of({ files, tree, search })
+  }),
+)
 
-  export const defaultLayer = layer
-}
+export const defaultLayer = layer
+
+export * as Ripgrep from "./ripgrep"