feat(directory): CODEGRAPH_DIR env var to override the index dir name (#636) (#741)

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:
Colby Mchenry
2026-06-08 19:31:50 -04:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 636d9fcb7d
commit a56d9e6941
10 changed files with 182 additions and 13 deletions
+91 -1
View File
@@ -10,7 +10,7 @@ import * as path from 'path';
import * as os from 'os';
import { CodeGraph } from '../src';
import { Node, Edge } from '../src/types';
import { isInitialized, getCodeGraphDir, validateDirectory } from '../src/directory';
import { isInitialized, getCodeGraphDir, validateDirectory, codeGraphDirName, isCodeGraphDataDir } from '../src/directory';
import { DatabaseConnection, getDatabasePath } from '../src/db';
// Create a temporary directory for each test
@@ -306,3 +306,93 @@ describe('Query Builder', () => {
expect(files).toEqual([]);
});
});
// Two environments that share one working tree (Windows-native + WSL) must not
// share one `.codegraph/`. CODEGRAPH_DIR overrides the data directory name so
// each side keeps its own index in the same tree (issue #636).
describe('CODEGRAPH_DIR override (#636)', () => {
const saved = process.env.CODEGRAPH_DIR;
let tempDir: string;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-dirname-'));
});
afterEach(() => {
if (saved === undefined) delete process.env.CODEGRAPH_DIR;
else process.env.CODEGRAPH_DIR = saved;
fs.rmSync(tempDir, { recursive: true, force: true });
});
describe('codeGraphDirName()', () => {
it('defaults to .codegraph when unset', () => {
delete process.env.CODEGRAPH_DIR;
expect(codeGraphDirName()).toBe('.codegraph');
});
it('honors a valid override', () => {
process.env.CODEGRAPH_DIR = '.codegraph-win';
expect(codeGraphDirName()).toBe('.codegraph-win');
});
// Anything that isn't a plain segment could escape the project root or
// clobber it, so it's ignored in favor of the default.
it.each(['foo/bar', 'a\\b', '..', '../x', '.', '/abs/path', ' ', ''])(
'falls back to .codegraph for invalid value %j',
(bad) => {
process.env.CODEGRAPH_DIR = bad;
expect(codeGraphDirName()).toBe('.codegraph');
}
);
});
describe('isCodeGraphDataDir()', () => {
it('matches the default, the active override, and .codegraph-* siblings', () => {
process.env.CODEGRAPH_DIR = '.codegraph-win';
expect(isCodeGraphDataDir('.codegraph')).toBe(true); // the other env's dir
expect(isCodeGraphDataDir('.codegraph-win')).toBe(true); // active override
expect(isCodeGraphDataDir('.codegraph-wsl')).toBe(true); // any sibling
});
it('does not match unrelated directories', () => {
delete process.env.CODEGRAPH_DIR;
for (const name of ['src', 'node_modules', '.git', 'codegraph', '.codegraphextra']) {
expect(isCodeGraphDataDir(name)).toBe(false);
}
});
});
it('init writes the index under the overridden directory, not .codegraph', () => {
process.env.CODEGRAPH_DIR = '.codegraph-win';
const cg = CodeGraph.initSync(tempDir);
try {
expect(fs.existsSync(path.join(tempDir, '.codegraph-win', 'codegraph.db'))).toBe(true);
expect(fs.existsSync(path.join(tempDir, '.codegraph'))).toBe(false);
expect(getCodeGraphDir(tempDir)).toBe(path.join(tempDir, '.codegraph-win'));
expect(CodeGraph.isInitialized(tempDir)).toBe(true);
} finally {
cg.close();
}
});
it('two index dirs coexist in one tree and the override side skips the sibling', async () => {
// WSL side: default `.codegraph`, with a source file.
delete process.env.CODEGRAPH_DIR;
fs.writeFileSync(path.join(tempDir, 'app.ts'), 'export function onlyReal() {}\n');
const wsl = await CodeGraph.init(tempDir, { index: true });
wsl.close();
// Windows side: override dir, same tree. Plant a decoy source file INSIDE
// the WSL data dir — the override-side index must not pick it up.
process.env.CODEGRAPH_DIR = '.codegraph-win';
fs.writeFileSync(path.join(tempDir, '.codegraph', 'decoy.ts'), 'export function decoyLeak() {}\n');
const win = await CodeGraph.init(tempDir, { index: true });
try {
expect(fs.existsSync(path.join(tempDir, '.codegraph', 'codegraph.db'))).toBe(true);
expect(fs.existsSync(path.join(tempDir, '.codegraph-win', 'codegraph.db'))).toBe(true);
expect(win.searchNodes('onlyReal').length).toBeGreaterThan(0);
expect(win.searchNodes('decoyLeak')).toEqual([]); // sibling data dir not indexed
} finally {
win.close();
}
});
});