Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### New Features

- `codegraph files --format package` and `codegraph_files` with `format: "package"` now list a project's files by the package that owns them, the nearest `package.json` or `composer.json` above each file that names one, so a monorepo splits into its workspace members with no setup and files nothing claims get a section of their own (#1871). Thanks @LuisAlbertoMK.
- `codegraph files --json` now marks each file as a test file or not, `--tests` and `--no-tests` filter the list in any format, and `codegraph node --file` says "test file" in its header, so your own tooling can ask CodeGraph instead of keeping a copy of its naming rules. Thanks @maxmilian. (#1877)
- `codegraph install` now sets up IBM Bob, for Bob IDE and Bob Shell: it adds the CodeGraph MCP server to `~/.bob/settings/mcp.json`, or to `.bob/mcp.json` in a project. Thanks @QuentinDstl.
- `codegraph install` now sets up Factory Droid: it adds the CodeGraph MCP server to `~/.factory/mcp.json` or a project's `.factory/mcp.json`, plus a short CodeGraph section in `AGENTS.md`, and Droid picks it up without a restart. Thanks @Rubyj and @KrystianMarek.
- `codegraph install` now sets up Windsurf: it adds the CodeGraph MCP server to `~/.codeium/windsurf/mcp_config.json`, which Devin, Windsurf's new name, imports by default. Thanks @SEPURI-SAI-KRISHNA.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ codegraph unlock [path] # Remove a stale lock file that's blocking ind
codegraph query <search> # Search symbols (--kind, --limit, --json)
codegraph explore <query> # Relevant symbols' source + call paths in one shot (same output as the codegraph_explore MCP tool)
codegraph node <symbol|file> # One symbol's source + callers, or read a file with line numbers (same output as codegraph_node)
codegraph files [path] # Show file structure (--format, --filter, --max-depth, --json)
codegraph files [path] # Show file structure (--format, --filter, --max-depth, --tests, --json)
codegraph callers <symbol> # Find what calls a function/method (--limit, --json)
codegraph callees <symbol> # Find what a function/method calls (--limit, --json)
codegraph impact <symbol> # Analyze what code is affected by changing a symbol (--depth, --json)
Expand Down
66 changes: 66 additions & 0 deletions __tests__/cli-files-test-classification.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';

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

describe('CLI test-file classification (#1877)', () => {
let dir: string;

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

beforeAll(async () => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-test-files-'));
fs.mkdirSync(path.join(dir, 'src'));
fs.writeFileSync(path.join(dir, 'src/app.ts'), 'export function app() { return 1; }\n');
fs.writeFileSync(path.join(dir, 'src/app.test.ts'), 'import { app } from "./app";\napp();\n');
const cg = CodeGraph.initSync(dir);
await cg.indexAll();
cg.close();
});

afterAll(() => fs.rmSync(dir, { recursive: true, force: true }));

it('reports the shared test-file predicate in files JSON and filters by it', () => {
const all = JSON.parse(run('files', ['--json']));
expect(all).toEqual(expect.arrayContaining([
expect.objectContaining({ path: 'src/app.ts', isTest: false }),
expect.objectContaining({ path: 'src/app.test.ts', isTest: true }),
]));
expect(JSON.parse(run('files', ['--json', '--tests'])).map((file: { path: string }) => file.path))
.toEqual(['src/app.test.ts']);
expect(JSON.parse(run('files', ['--json', '--no-tests'])).map((file: { path: string }) => file.path))
.toEqual(['src/app.ts']);
});

it('keeps the filter in every output format', () => {
for (const format of ['tree', 'flat', 'grouped', 'package']) {
const tests = run('files', ['--tests', '--format', format]);
expect(tests).toContain('app.test.ts');
expect(tests).not.toMatch(/app\.ts/);
const source = run('files', ['--no-tests', '--format', format]);
expect(source).toMatch(/app\.ts/);
expect(source).not.toContain('app.test.ts');
}
});

it('says so when a filter leaves no files', () => {
expect(run('files', ['--tests', '--filter', 'src/app.ts'])).toContain('No files found');
});

it('marks test files in the node file header', () => {
expect(run('node', ['-f', 'src/app.test.ts', '--symbols-only']))
.toContain('**src/app.test.ts** · test file');
expect(run('node', ['-f', 'src/app.ts', '--symbols-only']))
.not.toContain('test file');
});
});
2 changes: 1 addition & 1 deletion __tests__/owning-package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ describe('listing an indexed project by package', () => {
const plain = JSON.parse(codegraph('files', '--json')) as Array<Record<string, unknown>>;

expect(plain.map((file) => Object.keys(file).sort())).toEqual(
plain.map(() => ['errors', 'language', 'nodeCount', 'path', 'size']),
plain.map(() => ['errors', 'isTest', 'language', 'nodeCount', 'path', 'size']),
);
expect(JSON.parse(codegraph('files', '--json', '--format', 'package'))).toEqual(plain);
});
Expand Down
8 changes: 8 additions & 0 deletions src/bin/codegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1897,6 +1897,8 @@ program
.option('-p, --path <path>', 'Project path')
.option('--filter <dir>', 'Filter to files under this directory')
.option('--pattern <glob>', 'Filter files matching this glob pattern')
.option('--tests', 'Show only test files')
.option('--no-tests', 'Exclude test files')
.option('--format <format>', 'Output format (tree, flat, grouped, package)', 'tree')
.option('--max-depth <number>', 'Maximum directory depth for tree format')
.option('--no-metadata', 'Hide file metadata (language, symbol count)')
Expand All @@ -1905,6 +1907,7 @@ program
path?: string;
filter?: string;
pattern?: string;
tests?: boolean;
format?: string;
maxDepth?: string;
metadata?: boolean;
Expand Down Expand Up @@ -1939,6 +1942,10 @@ program
files = files.filter(f => regex.test(f.path));
}

if (options.tests !== undefined) {
files = files.filter(f => isTestPath(f.path) === options.tests);
}

if (files.length === 0) {
info('No files found matching the criteria.');
cg.destroy();
Expand All @@ -1949,6 +1956,7 @@ program
if (options.json) {
const output = files.map(f => ({
path: f.path,
isTest: isTestPath(f.path),
language: f.language,
nodeCount: f.nodeCount,
size: f.size,
Expand Down
13 changes: 7 additions & 6 deletions src/mcp/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
} from '../sync/worktree';
import type { PendingFile } from '../sync';
import type { Node, Edge, SearchResult, Subgraph, NodeKind, Language } from '../types';
import { isTestFile, normalizeNameToken, splitAtCjkSwitches, isQueryNameToken } from '../search/query-utils';
import { isTestFile, isTestPath, normalizeNameToken, splitAtCjkSwitches, isQueryNameToken } from '../search/query-utils';
import { groupDefinitions, isQualifiedSymbol, lastQualifierPart, matchesSymbol, qualifiedSymbolMatches } from '../graph/symbol-lookup';
import { goPackageReaderFor } from '../graph/go-package';
import {
Expand Down Expand Up @@ -8277,6 +8277,7 @@ export class ToolHandler {
}

const filePath = resolved.path;
const testLabel = isTestPath(filePath) ? ' · test file' : '';
const nodes = cg.getNodesInFile(filePath)
.filter((n) => n.kind !== 'file' && n.kind !== 'import' && n.kind !== 'export')
.sort((a, b) => a.startLine - b.startLine);
Expand All @@ -8300,7 +8301,7 @@ export class ToolHandler {

// symbolsOnly → the cheap structural overview, no source.
if (opts.symbolsOnly) {
const out = [`**${filePath}** — ${nodes.length} symbol${nodes.length === 1 ? '' : 's'}, ${depSummary}`, ''];
const out = [`**${filePath}**${testLabel} — ${nodes.length} symbol${nodes.length === 1 ? '' : 's'}, ${depSummary}`, ''];
if (nodes.length) out.push(...symbolMap('**Symbols**'));
else out.push('_No indexed symbols in this file._');
out.push('', '> Drop `symbolsOnly` (or pass `offset`/`limit`) to read the source, like Read.');
Expand All @@ -8310,7 +8311,7 @@ export class ToolHandler {
// SECURITY (#383): never dump a raw config/data file — a yaml/properties
// line is `key: <secret>`. Summarize by key and point to a real Read.
if (CONFIG_LEAF_LANGUAGES.has(resolved.language)) {
const out = [`**${filePath}** — configuration/data file, ${depSummary}`, ''];
const out = [`**${filePath}**${testLabel} — configuration/data file, ${depSummary}`, ''];
if (nodes.length) out.push(...symbolMap('**Keys (values withheld for safety)**'));
out.push('', '> Values may be secrets, so codegraph indexes keys only. Read the file directly if you need a value.');
return this.textResult(this.truncateOutput(out.join('\n')));
Expand All @@ -8324,7 +8325,7 @@ export class ToolHandler {
try { content = readFileSync(abs, 'utf-8'); } catch { content = null; }
}
if (content === null) {
const out = [`**${filePath}** — could not read from disk (it may have moved since indexing). ${depSummary}`, ''];
const out = [`**${filePath}**${testLabel} — could not read from disk (it may have moved since indexing). ${depSummary}`, ''];
if (nodes.length) out.push(...symbolMap('**Symbols**'));
out.push('', `> Read \`${filePath}\` directly for its current content.`);
return this.textResult(this.truncateOutput(out.join('\n')));
Expand All @@ -8344,11 +8345,11 @@ export class ToolHandler {
const DEFAULT_LIMIT = 2000;
const offset = Math.max(1, opts.offset ?? 1);
if (offset > total) {
return this.textResult(`**${filePath}** has ${total} line${total === 1 ? '' : 's'} — offset ${offset} is past the end. ${depSummary}`);
return this.textResult(`**${filePath}**${testLabel} has ${total} line${total === 1 ? '' : 's'} — offset ${offset} is past the end. ${depSummary}`);
}
const maxLines = Math.max(1, opts.limit ?? DEFAULT_LIMIT);
const start = offset - 1; // 0-based
const header = `**${filePath}** — ${total} lines, ${nodes.length} symbol${nodes.length === 1 ? '' : 's'} · ${depSummary}`;
const header = `**${filePath}**${testLabel} — ${total} lines, ${nodes.length} symbol${nodes.length === 1 ? '' : 's'} · ${depSummary}`;

// Numbered lines, byte-for-byte Read's shape: `<n>\t<line>`, no left-pad.
const numbered: string[] = [];
Expand Down