From 80fee8691dc43db3114ec2225e6fedf1ff8155ff Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 19:42:43 -0400 Subject: [PATCH] refactor: unwrap FileWatcher namespace + self-reexport --- packages/opencode/src/file/watcher.ts | 278 +++++++++++++------------- 1 file changed, 139 insertions(+), 139 deletions(-) diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index 3e3da444a5..dc20333758 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -19,145 +19,145 @@ import { Log } from "../util" declare const OPENCODE_LIBC: string | undefined -export namespace FileWatcher { - const log = Log.create({ service: "file.watcher" }) - const SUBSCRIBE_TIMEOUT_MS = 10_000 +const log = Log.create({ service: "file.watcher" }) +const SUBSCRIBE_TIMEOUT_MS = 10_000 - export const Event = { - Updated: BusEvent.define( - "file.watcher.updated", - z.object({ - file: z.string(), - event: z.union([z.literal("add"), z.literal("change"), z.literal("unlink")]), - }), - ), - } - - const watcher = lazy((): typeof import("@parcel/watcher") | undefined => { - try { - const binding = require( - `@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${OPENCODE_LIBC || "glibc"}` : ""}`, - ) - return createWrapper(binding) as typeof import("@parcel/watcher") - } catch (error) { - log.error("failed to load watcher binding", { error }) - return - } - }) - - function getBackend() { - if (process.platform === "win32") return "windows" - if (process.platform === "darwin") return "fs-events" - if (process.platform === "linux") return "inotify" - } - - function protecteds(dir: string) { - return Protected.paths().filter((item) => { - const rel = path.relative(dir, item) - return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel) - }) - } - - export const hasNativeBinding = () => !!watcher() - - export interface Interface { - readonly init: () => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/FileWatcher") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const config = yield* Config.Service - const git = yield* Git.Service - - const state = yield* InstanceState.make( - Effect.fn("FileWatcher.state")( - function* () { - if (yield* Flag.OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER) return - - log.info("init", { directory: Instance.directory }) - - const backend = getBackend() - if (!backend) { - log.error("watcher backend not supported", { directory: Instance.directory, platform: process.platform }) - return - } - - const w = watcher() - if (!w) return - - log.info("watcher backend", { directory: Instance.directory, platform: process.platform, backend }) - - const subs: ParcelWatcher.AsyncSubscription[] = [] - yield* Effect.addFinalizer(() => - Effect.promise(() => Promise.allSettled(subs.map((sub) => sub.unsubscribe()))), - ) - - const cb: ParcelWatcher.SubscribeCallback = Instance.bind((err, evts) => { - if (err) return - for (const evt of evts) { - if (evt.type === "create") void Bus.publish(Event.Updated, { file: evt.path, event: "add" }) - if (evt.type === "update") void Bus.publish(Event.Updated, { file: evt.path, event: "change" }) - if (evt.type === "delete") void Bus.publish(Event.Updated, { file: evt.path, event: "unlink" }) - } - }) - - const subscribe = (dir: string, ignore: string[]) => { - const pending = w.subscribe(dir, cb, { ignore, backend }) - return Effect.gen(function* () { - const sub = yield* Effect.promise(() => pending) - subs.push(sub) - }).pipe( - Effect.timeout(SUBSCRIBE_TIMEOUT_MS), - Effect.catchCause((cause) => { - log.error("failed to subscribe", { dir, cause: Cause.pretty(cause) }) - pending.then((s) => s.unsubscribe()).catch(() => {}) - return Effect.void - }), - ) - } - - const cfg = yield* config.get() - const cfgIgnores = cfg.watcher?.ignore ?? [] - - if (yield* Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) { - yield* subscribe(Instance.directory, [ - ...FileIgnore.PATTERNS, - ...cfgIgnores, - ...protecteds(Instance.directory), - ]) - } - - if (Instance.project.vcs === "git") { - const result = yield* git.run(["rev-parse", "--git-dir"], { - cwd: Instance.project.worktree, - }) - const vcsDir = - result.exitCode === 0 ? path.resolve(Instance.project.worktree, result.text().trim()) : undefined - if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) { - const ignore = (yield* Effect.promise(() => readdir(vcsDir).catch(() => []))).filter( - (entry) => entry !== "HEAD", - ) - yield* subscribe(vcsDir, ignore) - } - } - }, - Effect.catchCause((cause) => { - log.error("failed to init watcher service", { cause: Cause.pretty(cause) }) - return Effect.void - }), - ), - ) - - return Service.of({ - init: Effect.fn("FileWatcher.init")(function* () { - yield* InstanceState.get(state) - }), - }) +export const Event = { + Updated: BusEvent.define( + "file.watcher.updated", + z.object({ + file: z.string(), + event: z.union([z.literal("add"), z.literal("change"), z.literal("unlink")]), }), - ) - - export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Git.defaultLayer)) + ), } + +const watcher = lazy((): typeof import("@parcel/watcher") | undefined => { + try { + const binding = require( + `@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${OPENCODE_LIBC || "glibc"}` : ""}`, + ) + return createWrapper(binding) as typeof import("@parcel/watcher") + } catch (error) { + log.error("failed to load watcher binding", { error }) + return + } +}) + +function getBackend() { + if (process.platform === "win32") return "windows" + if (process.platform === "darwin") return "fs-events" + if (process.platform === "linux") return "inotify" +} + +function protecteds(dir: string) { + return Protected.paths().filter((item) => { + const rel = path.relative(dir, item) + return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel) + }) +} + +export const hasNativeBinding = () => !!watcher() + +export interface Interface { + readonly init: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/FileWatcher") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const config = yield* Config.Service + const git = yield* Git.Service + + const state = yield* InstanceState.make( + Effect.fn("FileWatcher.state")( + function* () { + if (yield* Flag.OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER) return + + log.info("init", { directory: Instance.directory }) + + const backend = getBackend() + if (!backend) { + log.error("watcher backend not supported", { directory: Instance.directory, platform: process.platform }) + return + } + + const w = watcher() + if (!w) return + + log.info("watcher backend", { directory: Instance.directory, platform: process.platform, backend }) + + const subs: ParcelWatcher.AsyncSubscription[] = [] + yield* Effect.addFinalizer(() => + Effect.promise(() => Promise.allSettled(subs.map((sub) => sub.unsubscribe()))), + ) + + const cb: ParcelWatcher.SubscribeCallback = Instance.bind((err, evts) => { + if (err) return + for (const evt of evts) { + if (evt.type === "create") void Bus.publish(Event.Updated, { file: evt.path, event: "add" }) + if (evt.type === "update") void Bus.publish(Event.Updated, { file: evt.path, event: "change" }) + if (evt.type === "delete") void Bus.publish(Event.Updated, { file: evt.path, event: "unlink" }) + } + }) + + const subscribe = (dir: string, ignore: string[]) => { + const pending = w.subscribe(dir, cb, { ignore, backend }) + return Effect.gen(function* () { + const sub = yield* Effect.promise(() => pending) + subs.push(sub) + }).pipe( + Effect.timeout(SUBSCRIBE_TIMEOUT_MS), + Effect.catchCause((cause) => { + log.error("failed to subscribe", { dir, cause: Cause.pretty(cause) }) + pending.then((s) => s.unsubscribe()).catch(() => {}) + return Effect.void + }), + ) + } + + const cfg = yield* config.get() + const cfgIgnores = cfg.watcher?.ignore ?? [] + + if (yield* Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) { + yield* subscribe(Instance.directory, [ + ...FileIgnore.PATTERNS, + ...cfgIgnores, + ...protecteds(Instance.directory), + ]) + } + + if (Instance.project.vcs === "git") { + const result = yield* git.run(["rev-parse", "--git-dir"], { + cwd: Instance.project.worktree, + }) + const vcsDir = + result.exitCode === 0 ? path.resolve(Instance.project.worktree, result.text().trim()) : undefined + if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) { + const ignore = (yield* Effect.promise(() => readdir(vcsDir).catch(() => []))).filter( + (entry) => entry !== "HEAD", + ) + yield* subscribe(vcsDir, ignore) + } + } + }, + Effect.catchCause((cause) => { + log.error("failed to init watcher service", { cause: Cause.pretty(cause) }) + return Effect.void + }), + ), + ) + + return Service.of({ + init: Effect.fn("FileWatcher.init")(function* () { + yield* InstanceState.get(state) + }), + }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Git.defaultLayer)) + +export * as FileWatcher from "./watcher"