Two environments that share one working tree — most concretely Windows and WSL — can't safely share a single `.codegraph/`: the daemon lockfile records a platform-specific pid + socket (named pipe vs Unix socket), and SQLite locking across the WSL2/Windows filesystem boundary is unreliable, so two daemons over one index risks corruption. Add a `CODEGRAPH_DIR` env var (default `.codegraph`) that overrides the per-project data directory name, so each environment keeps its own index in the same tree (e.g. `CODEGRAPH_DIR=.codegraph-win` on Windows). The name is resolved live and validated (rejects separators / `..` / absolute, falling back to the default with a one-time stderr warning). Indexing and file-watching now skip ANY `.codegraph-*` sibling so neither side trips over the other's data. Routes the previously-hardcoded `.codegraph` literals (db path, lockfile, error log, watcher ignore, file-scan skip, installer) through the resolver. No extraction-version bump — index content is unchanged. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
636d9fcb7d
commit
a56d9e6941
@@ -356,7 +356,7 @@ function printIndexResult(clack: typeof import('@clack/prompts'), result: IndexR
|
||||
clack.log.info(`The index is fully usable ${getGlyphs().dash} only the failed files are missing.`);
|
||||
}
|
||||
} else if (projectPath) {
|
||||
const logPath = path.join(projectPath, '.codegraph', 'errors.log');
|
||||
const logPath = path.join(getCodeGraphDir(projectPath), 'errors.log');
|
||||
if (fs.existsSync(logPath)) {
|
||||
fs.unlinkSync(logPath);
|
||||
}
|
||||
@@ -367,7 +367,7 @@ function printIndexResult(clack: typeof import('@clack/prompts'), result: IndexR
|
||||
* Write detailed error log to .codegraph/errors.log
|
||||
*/
|
||||
function writeErrorLog(projectPath: string, errors: Array<{ message: string; filePath?: string; severity: string; code?: string }>): void {
|
||||
const cgDir = path.join(projectPath, '.codegraph');
|
||||
const cgDir = getCodeGraphDir(projectPath);
|
||||
if (!fs.existsSync(cgDir)) return;
|
||||
|
||||
const logPath = path.join(cgDir, 'errors.log');
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { SchemaVersion } from '../types';
|
||||
import { runMigrations, getCurrentVersion, CURRENT_SCHEMA_VERSION } from './migrations';
|
||||
import { getCodeGraphDir } from '../directory';
|
||||
|
||||
export { SqliteDatabase, SqliteBackend } from './sqlite-adapter';
|
||||
|
||||
@@ -240,5 +241,5 @@ export const DATABASE_FILENAME = 'codegraph.db';
|
||||
* Get the default database path for a project
|
||||
*/
|
||||
export function getDatabasePath(projectRoot: string): string {
|
||||
return path.join(projectRoot, '.codegraph', DATABASE_FILENAME);
|
||||
return path.join(getCodeGraphDir(projectRoot), DATABASE_FILENAME);
|
||||
}
|
||||
|
||||
+69
-3
@@ -7,16 +7,82 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/** The default per-project data directory name. */
|
||||
const DEFAULT_CODEGRAPH_DIR = '.codegraph';
|
||||
|
||||
let warnedBadDirName = false;
|
||||
|
||||
/**
|
||||
* CodeGraph directory name
|
||||
* Resolve the per-project data directory name, honoring the `CODEGRAPH_DIR`
|
||||
* environment override (default `.codegraph`). The override is a single path
|
||||
* segment that lives in the project root.
|
||||
*
|
||||
* Why this exists: two environments that share one working tree must NOT share
|
||||
* one `.codegraph/` — most concretely Windows-native and WSL (issue #636). The
|
||||
* daemon lockfile (`.codegraph/daemon.pid`) records a platform-specific pid and
|
||||
* socket path (a Windows named pipe vs a WSL Unix socket), and SQLite file
|
||||
* locking across the WSL2 ↔ Windows filesystem boundary is unreliable, so two
|
||||
* daemons sharing one index risks corruption. Setting `CODEGRAPH_DIR=.codegraph-win`
|
||||
* on one side gives each environment its own index in the same tree.
|
||||
*
|
||||
* Read live (not captured at load) so it is both process-accurate and testable.
|
||||
* An override that isn't a plain directory name — empty, containing a path
|
||||
* separator, `.`, `..`/traversal, or absolute — is ignored (we keep the
|
||||
* default) rather than risk writing the index outside the project or into the
|
||||
* project root itself; we warn once to stderr so the misconfiguration is seen.
|
||||
*/
|
||||
export const CODEGRAPH_DIR = '.codegraph';
|
||||
export function codeGraphDirName(): string {
|
||||
const raw = process.env.CODEGRAPH_DIR?.trim();
|
||||
if (!raw) return DEFAULT_CODEGRAPH_DIR;
|
||||
const invalid =
|
||||
raw === '.' ||
|
||||
raw.includes('..') ||
|
||||
raw.includes('/') ||
|
||||
raw.includes('\\') ||
|
||||
path.isAbsolute(raw);
|
||||
if (invalid) {
|
||||
if (!warnedBadDirName) {
|
||||
warnedBadDirName = true;
|
||||
// stderr only — stdout is the MCP protocol channel.
|
||||
console.warn(
|
||||
`[codegraph] Ignoring invalid CODEGRAPH_DIR="${raw}" — it must be a plain ` +
|
||||
`directory name (no path separators, no "..", not absolute). Using "${DEFAULT_CODEGRAPH_DIR}".`
|
||||
);
|
||||
}
|
||||
return DEFAULT_CODEGRAPH_DIR;
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* CodeGraph directory name — a load-time snapshot of {@link codeGraphDirName}.
|
||||
* A running process's environment is fixed, so this equals the live value;
|
||||
* it's kept as a stable string export for backward compatibility. Internal code
|
||||
* resolves the name through {@link codeGraphDirName} / {@link getCodeGraphDir}
|
||||
* so the `CODEGRAPH_DIR` override always applies.
|
||||
*/
|
||||
export const CODEGRAPH_DIR = codeGraphDirName();
|
||||
|
||||
/**
|
||||
* Is `name` (a single path segment) a CodeGraph data directory? Matches the
|
||||
* default `.codegraph`, the active `CODEGRAPH_DIR` override, and any
|
||||
* `.codegraph-*` sibling. File-watching and the indexer skip ALL of these, so
|
||||
* when two environments share one working tree (Windows + WSL, issue #636)
|
||||
* neither indexes or watches the other's index directory.
|
||||
*/
|
||||
export function isCodeGraphDataDir(name: string): boolean {
|
||||
return (
|
||||
name === DEFAULT_CODEGRAPH_DIR ||
|
||||
name === codeGraphDirName() ||
|
||||
name.startsWith(DEFAULT_CODEGRAPH_DIR + '-')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the .codegraph directory path for a project
|
||||
*/
|
||||
export function getCodeGraphDir(projectRoot: string): string {
|
||||
return path.join(projectRoot, CODEGRAPH_DIR);
|
||||
return path.join(projectRoot, codeGraphDirName());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { QueryBuilder } from '../db/queries';
|
||||
import { extractFromSource } from './tree-sitter';
|
||||
import { detectLanguage, isSourceFile, isLanguageSupported, isFileLevelOnlyLanguage, initGrammars, loadGrammarsForLanguages } from './grammars';
|
||||
import { isCodeGraphDataDir } from '../directory';
|
||||
import { logDebug, logWarn } from '../errors';
|
||||
import { validatePathWithinRoot, normalizePath } from '../utils';
|
||||
import ignore, { Ignore } from 'ignore';
|
||||
@@ -454,8 +455,9 @@ function scanDirectoryWalk(
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
// Never descend into git internals or our own data directory.
|
||||
if (entry.name === '.git' || entry.name === '.codegraph') continue;
|
||||
// Never descend into git internals or any CodeGraph data directory
|
||||
// (the active one or a sibling another environment created — #636).
|
||||
if (entry.name === '.git' || isCodeGraphDataDir(entry.name)) continue;
|
||||
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
const relativePath = normalizePath(path.relative(rootDir, fullPath));
|
||||
|
||||
+2
-1
@@ -48,6 +48,7 @@ import { ContextBuilder, createContextBuilder } from './context';
|
||||
import { Mutex, FileLock } from './utils';
|
||||
import { FileWatcher, WatchOptions, PendingFile, LockUnavailableError } from './sync';
|
||||
import { EXTRACTION_VERSION } from './extraction/extraction-version';
|
||||
import { getCodeGraphDir } from './directory';
|
||||
import { CodeGraphPackageVersion } from './mcp/version';
|
||||
|
||||
// Re-export types for consumers
|
||||
@@ -154,7 +155,7 @@ export class CodeGraph {
|
||||
this.queries = queries;
|
||||
this.projectRoot = projectRoot;
|
||||
this.fileLock = new FileLock(
|
||||
path.join(projectRoot, '.codegraph', 'codegraph.lock')
|
||||
path.join(getCodeGraphDir(projectRoot), 'codegraph.lock')
|
||||
);
|
||||
this.orchestrator = new ExtractionOrchestrator(projectRoot, queries);
|
||||
this.resolver = createResolver(projectRoot, queries);
|
||||
|
||||
@@ -28,6 +28,7 @@ import { getGlyphs } from '../ui/glyphs';
|
||||
// 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';
|
||||
import { getCodeGraphDir, codeGraphDirName } from '../directory';
|
||||
|
||||
// Backwards-compat: keep these named exports — downstream code may
|
||||
// import them. The shim in `config-writer.ts` continues to re-export
|
||||
@@ -362,8 +363,8 @@ export async function runUninstaller(opts: RunUninstallerOptions): Promise<void>
|
||||
|
||||
// Step 4: for local uninstall, the index dir is separate — point at
|
||||
// `uninit` so the user knows it's still there (and how to remove it).
|
||||
if (location === 'local' && fs.existsSync(path.join(process.cwd(), '.codegraph'))) {
|
||||
clack.log.info('The .codegraph/ index for this project is still here. Run `codegraph uninit` to delete it.');
|
||||
if (location === 'local' && fs.existsSync(getCodeGraphDir(process.cwd()))) {
|
||||
clack.log.info(`The ${codeGraphDirName()}/ index for this project is still here. Run \`codegraph uninit\` to delete it.`);
|
||||
}
|
||||
|
||||
// Step 5: summary.
|
||||
|
||||
+6
-1
@@ -37,6 +37,7 @@ import type { Ignore } from 'ignore';
|
||||
import { isSourceFile, buildDefaultIgnore } from '../extraction';
|
||||
import { logDebug, logWarn } from '../errors';
|
||||
import { normalizePath } from '../utils';
|
||||
import { isCodeGraphDataDir } from '../directory';
|
||||
import { watchDisabledReason } from './watch-policy';
|
||||
|
||||
/**
|
||||
@@ -425,8 +426,12 @@ export class FileWatcher {
|
||||
|
||||
/** Our own dirs are always ignored, regardless of .gitignore. */
|
||||
private isAlwaysIgnored(rel: string): boolean {
|
||||
// First path segment. Ignore any CodeGraph data dir — the active one AND a
|
||||
// sibling like `.codegraph-win` a second environment (Windows/WSL) created
|
||||
// in the same tree, so neither side watches the other's index (#636).
|
||||
const top = rel.split('/')[0] ?? rel;
|
||||
return (
|
||||
rel === '.codegraph' || rel.startsWith('.codegraph/') ||
|
||||
isCodeGraphDataDir(top) ||
|
||||
rel === '.git' || rel.startsWith('.git/')
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user