Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ The exact text is `src/mcp/server-instructions.ts` — the single source of trut
codegraph # Run interactive installer
codegraph install # Run installer (explicit)
codegraph uninstall # Remove CodeGraph from your agents AND the CLI (--keep-cli for configs only)
codegraph init [path] # Initialize a project + build its graph (one step)
codegraph init [path] # Initialize a project + build its graph (--all <dirs...> for many repos, --git-hooks for git sync hooks)
codegraph uninit [path] # Remove CodeGraph from a project (--force to skip prompt)
codegraph index [path] # Full index (--force to re-index, --quiet for less output)
codegraph sync [path] # Incremental update
Expand All @@ -524,6 +524,7 @@ codegraph callees <symbol> # Find what a function/method calls (--limit,
codegraph impact <symbol> # Analyze what code is affected by changing a symbol (--depth, --json)
codegraph affected [files...] # Find test files affected by changes (see below)
codegraph daemon # Manage background daemons — pick one to stop (alias: daemons)
codegraph config get|set <key> # Show or change a global setting (auto-init on|off)
codegraph telemetry [on|off] # Show or change anonymous usage telemetry
codegraph upgrade [version] # Update to the latest release (--check, --force)
codegraph version # Print the installed version (also -v, --version)
Expand Down
50 changes: 50 additions & 0 deletions __tests__/cli-config-command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* `codegraph config get|set auto-init` — mirrors the existing
* `codegraph telemetry` command's on/off/status shape (see cli-query-command
* .test.ts for the same execFileSync-against-dist convention).
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';

const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');

function run(args: string[], home: string): string {
return execFileSync(process.execPath, [BIN, ...args], {
encoding: 'utf-8',
env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1', CODEGRAPH_HOME: home },
stdio: ['ignore', 'pipe', 'ignore'],
});
}

describe('codegraph config auto-init', () => {
let home: string;

beforeEach(() => {
home = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-config-cmd-'));
});

afterEach(() => {
fs.rmSync(home, { recursive: true, force: true });
});

it('defaults to off', () => {
const out = run(['config', 'get', 'auto-init'], home);
expect(out).toMatch(/off|false/i);
});

it('turns on and reports on', () => {
run(['config', 'set', 'auto-init', 'on'], home);
const out = run(['config', 'get', 'auto-init'], home);
expect(out).toMatch(/on|true/i);
});

it('turns back off', () => {
run(['config', 'set', 'auto-init', 'on'], home);
run(['config', 'set', 'auto-init', 'off'], home);
const out = run(['config', 'get', 'auto-init'], home);
expect(out).toMatch(/off|false/i);
});
});
147 changes: 147 additions & 0 deletions __tests__/cli-init-batch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* `codegraph init --all <dirs...>` (batch indexing across many repos).
*
* Exercised end-to-end against the built binary, matching the convention in
* cli-query-command.test.ts.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';

const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');

function initAll(dirs: string[]): { stdout: string; status: number } {
try {
const stdout = execFileSync(process.execPath, [BIN, 'init', '--all', ...dirs], {
encoding: 'utf-8',
env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1' },
stdio: ['ignore', 'pipe', 'ignore'],
});
return { stdout, status: 0 };
} catch (err) {
const e = err as { stdout?: Buffer; status?: number };
return { stdout: e.stdout?.toString('utf-8') ?? '', status: e.status ?? 1 };
}
}

function initSingle(dir: string): { stdout: string; status: number } {
try {
const stdout = execFileSync(process.execPath, [BIN, 'init', dir], {
encoding: 'utf-8',
env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1' },
stdio: ['ignore', 'pipe', 'ignore'],
});
return { stdout, status: 0 };
} catch (err) {
const e = err as { stdout?: Buffer; status?: number };
return { stdout: e.stdout?.toString('utf-8') ?? '', status: e.status ?? 1 };
}
}

function makeRepo(root: string, name: string): string {
const dir = path.join(root, name);
fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
fs.writeFileSync(path.join(dir, 'src/main.ts'), 'export function main(){ return 1; }\n');
return dir;
}

describe('codegraph init --all', () => {
let root: string;

beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-init-batch-'));
});

afterEach(() => {
fs.rmSync(root, { recursive: true, force: true });
});

it('indexes every directory listed and reports a summary line per repo', () => {
const repoA = makeRepo(root, 'repo-a');
const repoB = makeRepo(root, 'repo-b');

const { stdout, status } = initAll([repoA, repoB]);

expect(status).toBe(0);
expect(stdout).toContain(repoA);
expect(stdout).toContain(repoB);
expect(fs.existsSync(path.join(repoA, '.codegraph'))).toBe(true);
expect(fs.existsSync(path.join(repoB, '.codegraph'))).toBe(true);
});

it('continues past an already-initialized directory instead of stopping the batch', () => {
const repoA = makeRepo(root, 'repo-a');
const repoB = makeRepo(root, 'repo-b');
execFileSync(process.execPath, [BIN, 'init', repoA], {
env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1' },
stdio: 'ignore',
});

const { stdout, status } = initAll([repoA, repoB]);

expect(status).toBe(0);
expect(stdout).toContain('already');
expect(fs.existsSync(path.join(repoB, '.codegraph'))).toBe(true);
});

it('reports a refusal for an unsafe directory without aborting the rest of the batch', () => {
const repoB = makeRepo(root, 'repo-b');

const { stdout, status } = initAll([os.homedir(), repoB]);

expect(status).toBe(1); // batch exit code reflects the refusal
expect(stdout).toContain('refused');
expect(fs.existsSync(path.join(repoB, '.codegraph'))).toBe(true); // but repo-b still got indexed
});
});

describe('codegraph init (single-path regression)', () => {
it('refuses to index home directory and exits with code 1', () => {
const { stdout, status } = initSingle(os.homedir());

expect(status).toBe(1);
expect(stdout).toContain('Refusing');
});
});

describe('codegraph init --git-hooks (single path)', () => {
let root: string;

beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-init-githooks-'));
});

afterEach(() => {
fs.rmSync(root, { recursive: true, force: true });
});

it('installs the hooks without prompting when --git-hooks is passed to a single-path init', () => {
const repo = makeRepo(root, 'repo-a');
execFileSync('git', ['init', '-q'], { cwd: repo, stdio: 'ignore' });

// The flag's whole point is "yes, install them" — but single mode passed
// `yes: mode === 'batch'`, i.e. always false, so offerWatchFallback still
// ran clack.select() and the user could answer "manual" and get no hooks
// at all. In a non-interactive context (setup script, CI) that prompt has
// nobody to answer it.
//
// stdin is /dev/null here and nothing stubs `select`, so a surviving
// prompt cannot resolve to 'hook' by accident: the only way the hooks get
// installed is the non-interactive `yes` path.
const stdout = execFileSync(
process.execPath,
[BIN, 'init', repo, '--git-hooks'],
{
encoding: 'utf-8',
env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1' },
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 120_000,
},
);

expect(stdout).not.toContain('How should CodeGraph keep its index fresh?');
expect(fs.existsSync(path.join(repo, '.git', 'hooks', 'post-commit'))).toBe(true);
});
});
6 changes: 3 additions & 3 deletions __tests__/concurrent-locking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,13 @@ describe('issue #238 — ToolHandler reuses the default instance (#2)', () => {
fs.rmSync(dir, { recursive: true, force: true });
});

it('getCodeGraph(defaultRoot) returns the default instance, not a new connection', () => {
it('getCodeGraph(defaultRoot) returns the default instance, not a new connection', async () => {
const openSpy = vi.spyOn(CodeGraph, 'openSync');
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resolved = (handler as any).getCodeGraph(root);
const resolved = await (handler as any).getCodeGraph(root);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const nested = (handler as any).getCodeGraph(path.join(root, 'does', 'not', 'exist'));
const nested = await (handler as any).getCodeGraph(path.join(root, 'does', 'not', 'exist'));
expect(resolved).toBe(cg);
expect(nested).toBe(cg); // a sub-path resolves up to the same default project
expect(openSpy).not.toHaveBeenCalled(); // no second connection opened
Expand Down
Loading