Skip to content

Commit c2b4c13

Browse files
committed
fix: restore watcher lifecycle for routing hints
1 parent cdc52a7 commit c2b4c13

File tree

3 files changed

+89
-7
lines changed

3 files changed

+89
-7
lines changed

src/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ import {
2525
import { loadCommandsFromDirectory } from "./commands/loader.js";
2626
import { RoutingHintController } from "./routing-hints.js";
2727
import { hasProjectMarker } from "./utils/files.js";
28+
import type { CombinedWatcher } from "./watcher/index.js";
29+
30+
let activeWatcher: CombinedWatcher | null = null;
31+
32+
function replaceActiveWatcher(nextWatcher: CombinedWatcher | null): void {
33+
activeWatcher?.stop();
34+
activeWatcher = nextWatcher;
35+
}
2836

2937
function getCommandsDir(): string {
3038
let currentDir = process.cwd();
@@ -65,7 +73,9 @@ const plugin: Plugin = async ({ directory }) => {
6573
}
6674

6775
if (config.indexing.watchFiles && isValidProject) {
68-
createWatcherWithIndexer(indexer, projectRoot, config);
76+
replaceActiveWatcher(createWatcherWithIndexer(getSharedIndexer, projectRoot, config));
77+
} else {
78+
replaceActiveWatcher(null);
6979
}
7080

7181
return {

src/watcher/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import chokidar, { FSWatcher } from "chokidar";
22
import * as path from "path";
33

4-
import { CodebaseIndexConfig } from "../config/schema.js";
4+
import type { CodebaseIndexConfig } from "../config/schema.js";
55
import { createIgnoreFilter, shouldIncludeFile } from "../utils/files.js";
6-
import { Indexer } from "../indexer/index.js";
6+
import type { Indexer } from "../indexer/index.js";
77
import { isGitRepo, getHeadPath, getCurrentBranch } from "../git/index.js";
88

99
export type FileChangeType = "add" | "change" | "unlink";
@@ -243,7 +243,7 @@ export interface CombinedWatcher {
243243
}
244244

245245
export function createWatcherWithIndexer(
246-
indexer: Indexer,
246+
getIndexer: () => Indexer,
247247
projectRoot: string,
248248
config: CodebaseIndexConfig
249249
): CombinedWatcher {
@@ -256,7 +256,7 @@ export function createWatcherWithIndexer(
256256
const hasDelete = changes.some((c) => c.type === "unlink");
257257

258258
if (hasAddOrChange || hasDelete) {
259-
await indexer.index();
259+
await getIndexer().index();
260260
}
261261
});
262262

@@ -266,7 +266,7 @@ export function createWatcherWithIndexer(
266266
gitWatcher = new GitHeadWatcher(projectRoot);
267267
gitWatcher.start(async (oldBranch, newBranch) => {
268268
console.log(`Branch changed: ${oldBranch ?? "(none)"} -> ${newBranch}`);
269-
await indexer.index();
269+
await getIndexer().index();
270270
});
271271
}
272272

tests/watcher.test.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
22
import * as fs from "fs";
33
import * as path from "path";
44
import * as os from "os";
5-
import { FileWatcher, GitHeadWatcher, FileChange } from "../src/watcher/index.js";
5+
import {
6+
FileWatcher,
7+
GitHeadWatcher,
8+
FileChange,
9+
createWatcherWithIndexer,
10+
} from "../src/watcher/index.js";
611
import { ParsedCodebaseIndexConfig } from "../src/config/schema.js";
712

813
const createTestConfig = (overrides: Partial<ParsedCodebaseIndexConfig> = {}): ParsedCodebaseIndexConfig => ({
@@ -119,6 +124,73 @@ describe("FileWatcher", () => {
119124
expect(tsChanges.length).toBeGreaterThanOrEqual(0);
120125
expect(mdChanges.length).toBe(0);
121126
});
127+
128+
it("should include matching root-level files", async () => {
129+
const changes: FileChange[] = [];
130+
watcher = new FileWatcher(tempDir, createTestConfig({ include: ["**/*.ts"] }));
131+
132+
watcher.start(async (c) => {
133+
changes.push(...c);
134+
});
135+
136+
await new Promise((r) => setTimeout(r, 100));
137+
138+
fs.writeFileSync(path.join(tempDir, "root.ts"), "export const root = 1;");
139+
140+
await new Promise((r) => setTimeout(r, 1500));
141+
142+
expect(changes.some((c) => c.path.endsWith("root.ts"))).toBe(true);
143+
});
144+
});
145+
146+
describe("createWatcherWithIndexer", () => {
147+
it("uses the latest indexer instance for file-triggered reindexing", async () => {
148+
const staleIndexer = {
149+
index: vi.fn().mockResolvedValue(undefined),
150+
};
151+
const refreshedIndexer = {
152+
index: vi.fn().mockResolvedValue(undefined),
153+
};
154+
155+
let currentIndexer = staleIndexer;
156+
const combinedWatcher = createWatcherWithIndexer(
157+
() => currentIndexer,
158+
tempDir,
159+
createTestConfig()
160+
);
161+
162+
await new Promise((r) => setTimeout(r, 100));
163+
currentIndexer = refreshedIndexer;
164+
165+
fs.writeFileSync(path.join(tempDir, "src", "reindex-me.ts"), "export const value = 1;");
166+
167+
await new Promise((r) => setTimeout(r, 1500));
168+
169+
expect(refreshedIndexer.index).toHaveBeenCalledTimes(1);
170+
expect(staleIndexer.index).not.toHaveBeenCalled();
171+
172+
combinedWatcher.stop();
173+
});
174+
175+
it("stops the watcher cleanly after start", () => {
176+
const indexer = {
177+
index: vi.fn().mockResolvedValue(undefined),
178+
};
179+
180+
const combinedWatcher = createWatcherWithIndexer(
181+
() => indexer,
182+
tempDir,
183+
createTestConfig()
184+
);
185+
186+
expect(combinedWatcher.fileWatcher.isRunning()).toBe(true);
187+
expect(combinedWatcher.gitWatcher?.isRunning() ?? false).toBe(false);
188+
189+
combinedWatcher.stop();
190+
191+
expect(combinedWatcher.fileWatcher.isRunning()).toBe(false);
192+
expect(combinedWatcher.gitWatcher?.isRunning() ?? false).toBe(false);
193+
});
122194
});
123195
});
124196

0 commit comments

Comments
 (0)