Recursive fs.watch on a WSL2 /mnt NTFS/9p mount walks the directory tree with every readdir/stat crossing the Windows boundary, stalling the event loop long enough to blow past opencode's 30s MCP handshake timeout so the tools never appear. This is the file-watcher half of the #172 fix, which moved the DB/WASM open off the handshake but left the watcher on the critical path. - Add watchDisabledReason() policy: CODEGRAPH_NO_WATCH (off) > CODEGRAPH_FORCE_WATCH (force on) > WSL2 + /mnt auto-detect (off). FileWatcher.start() and the MCP server both honor it; the server now logs why watching is off and how to refresh. - Add `codegraph serve --mcp --no-watch`. - When watching is off, init/install offer git sync hooks (post-commit, post-merge, post-checkout) that run `codegraph sync` in the background, or fall back to manual sync; either way the user is told the index stays frozen until re-synced. uninit removes the hooks. - Tests: watch-policy + git-hooks (idempotency, user-content preservation, core.hooksPath). Root-cause analysis and workaround by @mengfanbo123. Closes #199 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
79b9601aae
commit
cf7db7cb98
+87
-4
@@ -22,6 +22,11 @@ import {
|
||||
} from './targets/registry';
|
||||
import type { AgentTarget, Location, WriteResult } from './targets/types';
|
||||
import { getGlyphs } from '../ui/glyphs';
|
||||
// Import the lightweight submodules directly (not the ../sync barrel, which
|
||||
// re-exports FileWatcher and would transitively pull in ../extraction — the
|
||||
// installer must stay importable even when native modules can't load).
|
||||
import { watchDisabledReason } from '../sync/watch-policy';
|
||||
import { isGitRepo, isSyncHookInstalled, installGitSyncHook } from '../sync/git-hooks';
|
||||
|
||||
// Backwards-compat: keep these named exports — downstream code may
|
||||
// import them. The shim in `config-writer.ts` continues to re-export
|
||||
@@ -198,7 +203,7 @@ export async function runInstallerWithOptions(opts: RunInstallerOptions): Promis
|
||||
|
||||
// Step 6: for local install, initialize the project.
|
||||
if (location === 'local') {
|
||||
await initializeLocalProject(clack);
|
||||
await initializeLocalProject(clack, useDefaults);
|
||||
}
|
||||
|
||||
if (location === 'global') {
|
||||
@@ -304,10 +309,14 @@ async function resolveTargets(
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize CodeGraph in the current project (for local installs).
|
||||
* Unchanged from the pre-refactor version — agent-agnostic by nature.
|
||||
* Initialize CodeGraph in the current project (for local installs), then
|
||||
* offer the watch fallback when the live watcher won't run here (see
|
||||
* offerWatchFallback). Agent-agnostic by nature.
|
||||
*/
|
||||
async function initializeLocalProject(clack: typeof import('@clack/prompts')): Promise<void> {
|
||||
async function initializeLocalProject(
|
||||
clack: typeof import('@clack/prompts'),
|
||||
useDefaults = false,
|
||||
): Promise<void> {
|
||||
const projectPath = process.cwd();
|
||||
|
||||
let CodeGraph: typeof import('../index').default;
|
||||
@@ -323,6 +332,7 @@ async function initializeLocalProject(clack: typeof import('@clack/prompts')): P
|
||||
// Check if already initialized
|
||||
if (CodeGraph.isInitialized(projectPath)) {
|
||||
clack.log.info('CodeGraph already initialized in this project');
|
||||
await offerWatchFallback(clack, projectPath, { yes: useDefaults });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -348,4 +358,77 @@ async function initializeLocalProject(clack: typeof import('@clack/prompts')): P
|
||||
}
|
||||
|
||||
cg.close();
|
||||
|
||||
await offerWatchFallback(clack, projectPath, { yes: useDefaults });
|
||||
}
|
||||
|
||||
/**
|
||||
* When the live file watcher will be disabled for this project (e.g. WSL2
|
||||
* /mnt drives, or CODEGRAPH_NO_WATCH), the index would silently go stale.
|
||||
* Explain that, and offer to keep it fresh automatically via git hooks
|
||||
* (commit / pull / checkout) instead of manual `codegraph sync`.
|
||||
*
|
||||
* No-op on environments where the watcher runs normally, so it's safe to
|
||||
* call unconditionally after init.
|
||||
*/
|
||||
export async function offerWatchFallback(
|
||||
clack: typeof import('@clack/prompts'),
|
||||
projectPath: string,
|
||||
opts: { yes?: boolean } = {},
|
||||
): Promise<void> {
|
||||
const reason = watchDisabledReason(projectPath);
|
||||
if (!reason) return; // Watcher runs normally — nothing to set up.
|
||||
|
||||
clack.log.warn(`Live file watching is disabled here — ${reason}.`);
|
||||
clack.log.info('Until you re-sync, the CodeGraph index stays frozen — it will not pick up edits on its own.');
|
||||
|
||||
// No git repo → the commit-hook path doesn't apply; point at manual sync.
|
||||
if (!isGitRepo(projectPath)) {
|
||||
clack.log.info('Run `codegraph sync` after changing files to refresh the index.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Already wired up on a previous run — confirm and move on without nagging.
|
||||
if (isSyncHookInstalled(projectPath)) {
|
||||
clack.log.info('Git sync hooks are already installed — the index refreshes after commit / pull / checkout.');
|
||||
return;
|
||||
}
|
||||
|
||||
let choice: 'hook' | 'manual';
|
||||
if (opts.yes) {
|
||||
choice = 'hook';
|
||||
} else {
|
||||
const sel = await clack.select({
|
||||
message: 'How should CodeGraph keep its index fresh?',
|
||||
options: [
|
||||
{ value: 'hook' as const, label: 'Sync on git commit / pull / checkout', hint: 'installs git hooks (recommended)' },
|
||||
{ value: 'manual' as const, label: 'I\'ll run `codegraph sync` myself', hint: 'fully manual' },
|
||||
],
|
||||
initialValue: 'hook' as const,
|
||||
});
|
||||
if (clack.isCancel(sel)) {
|
||||
clack.log.info('Skipped — run `codegraph sync` after changes to refresh the index.');
|
||||
return;
|
||||
}
|
||||
choice = sel;
|
||||
}
|
||||
|
||||
if (choice === 'manual') {
|
||||
clack.log.info('Run `codegraph sync` after changing files to refresh the index.');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = installGitSyncHook(projectPath);
|
||||
if (result.installed.length > 0) {
|
||||
clack.log.success(
|
||||
`Installed git ${result.installed.join(', ')} hook${result.installed.length > 1 ? 's' : ''} — ` +
|
||||
'the index refreshes in the background after each.',
|
||||
);
|
||||
clack.log.info('Run `codegraph sync` anytime to refresh immediately.');
|
||||
} else {
|
||||
clack.log.warn(
|
||||
`Could not install git hooks${result.skipped ? ` (${result.skipped})` : ''}. ` +
|
||||
'Run `codegraph sync` after changes instead.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user