From 63734eb5af0f7472d2b13872d7ce252dcd1a42cc Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Sat, 4 Apr 2026 16:40:48 -0500 Subject: [PATCH] Handle missing workspace paths in file tree watcher - Guard `fs.watch` against missing workspace directories so startup does not fail - Bump workspace package versions in the lockfile --- apps/server/src/wsServer.ts | 41 ++++++++++++++++++++++--------------- bun.lock | 10 ++++----- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index ab38dd195..18eeb5749 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -855,26 +855,35 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< let fileTreeDebounceTimer: ReturnType | null = null; - const fileTreeWatcher = fs.watch(cwd, { recursive: true }, (_eventType, filename) => { - if (!filename) return; - - // Ignore changes inside noisy directories - const normalized = String(filename).replaceAll("\\", "/"); - const firstSegment = normalized.split("/")[0]; - if (firstSegment && IGNORED_WATCHER_DIRS.has(firstSegment)) return; - - // Debounce rapid consecutive changes into a single push - if (fileTreeDebounceTimer) clearTimeout(fileTreeDebounceTimer); - fileTreeDebounceTimer = setTimeout(() => { - fileTreeDebounceTimer = null; - clearWorkspaceIndexCache(cwd); - void Effect.runPromise(pushBus.publishAll(WS_CHANNELS.projectFileTreeChanged, { cwd })); - }, FILE_TREE_DEBOUNCE_MS); + // fs.watch throws ENOENT when the directory does not exist (e.g. in tests + // or when a workspace path has been removed). Guard against this so the + // server can still start up. + const fileTreeWatcher = yield* Effect.sync(() => { + try { + return fs.watch(cwd, { recursive: true }, (_eventType, filename) => { + if (!filename) return; + + // Ignore changes inside noisy directories + const normalized = String(filename).replaceAll("\\", "/"); + const firstSegment = normalized.split("/")[0]; + if (firstSegment && IGNORED_WATCHER_DIRS.has(firstSegment)) return; + + // Debounce rapid consecutive changes into a single push + if (fileTreeDebounceTimer) clearTimeout(fileTreeDebounceTimer); + fileTreeDebounceTimer = setTimeout(() => { + fileTreeDebounceTimer = null; + clearWorkspaceIndexCache(cwd); + void Effect.runPromise(pushBus.publishAll(WS_CHANNELS.projectFileTreeChanged, { cwd })); + }, FILE_TREE_DEBOUNCE_MS); + }); + } catch { + return undefined; + } }); yield* Effect.addFinalizer(() => Effect.sync(() => { - fileTreeWatcher.close(); + fileTreeWatcher?.close(); if (fileTreeDebounceTimer) clearTimeout(fileTreeDebounceTimer); }), ); diff --git a/bun.lock b/bun.lock index 845efff8b..f8376d645 100644 --- a/bun.lock +++ b/bun.lock @@ -19,7 +19,7 @@ }, "apps/desktop": { "name": "@okcode/desktop", - "version": "0.13.0", + "version": "0.14.0", "dependencies": { "effect": "catalog:", "electron": "40.6.0", @@ -103,7 +103,7 @@ }, "apps/mobile": { "name": "@okcode/mobile", - "version": "0.13.0", + "version": "0.14.0", "dependencies": { "@capacitor/android": "^8.3.0", "@capacitor/app": "^8.1.0", @@ -123,7 +123,7 @@ }, "apps/server": { "name": "okcodes", - "version": "0.13.0", + "version": "0.14.0", "bin": { "okcode": "./dist/index.mjs", }, @@ -154,7 +154,7 @@ }, "apps/web": { "name": "@okcode/web", - "version": "0.13.0", + "version": "0.14.0", "dependencies": { "@base-ui/react": "^1.2.0", "@codemirror/language": "^6.12.3", @@ -214,7 +214,7 @@ }, "packages/contracts": { "name": "@okcode/contracts", - "version": "0.13.0", + "version": "0.14.0", "dependencies": { "effect": "catalog:", },