Skip to content

Commit 7b62356

Browse files
colbymchenry12122Jeddieranclaude
authored
feat(cli): add version, indexPath, lastIndexed to status --json (colbymchenry#329)
Adds `version`, `indexPath`, and an ISO `lastIndexed` to `codegraph status --json`, plus a `CodeGraph.getLastIndexedAt()` library method. `agentCount` dropped (no clear consumer). Reworked from contributor PRs colbymchenry#333 and colbymchenry#480. Co-Authored-By: Javier Gómez <199902626+12122J@users.noreply.github.com> Co-Authored-By: Ran <8403607+eddieran@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ddb1a8f commit 7b62356

5 files changed

Lines changed: 124 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
99

1010
## [Unreleased]
1111

12+
### New Features
13+
14+
- `codegraph status --json` now also reports the running CLI `version`, the index directory (`indexPath`), and a `lastIndexed` timestamp (ISO-8601, or null when nothing's indexed yet), so CI and scripts can pin the CLI version and check index freshness from a single command. A matching `CodeGraph.getLastIndexedAt()` library method exposes the same freshness check without shelling out. Thanks @12122J and @eddieran. (#329)
15+
1216
### Fixes
1317

1418
- The background file watcher no longer exhausts your machine's file-descriptor budget. On macOS it previously kept **one open file handle per watched file**, so on a large project the running MCP server could pile up tens of thousands of handles and blow past the system-wide limit — at which point *unrelated* apps (your shell, editor, Docker, browser) started failing with "too many open files" until the codegraph process was killed. The watcher now uses a single recursive watch on macOS and Windows, and bounded per-directory watches on Linux, so its cost stays flat no matter how large the project is. (#644, #496, #555, #628, #579)

__tests__/status-json.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* Tests for the CI/scripting fields `codegraph status --json` exposes (issue
3+
* #329): the `version`, `indexPath`, and `lastIndexed` fields, plus the
4+
* matching `CodeGraph.getLastIndexedAt()` library method.
5+
*
6+
* The CLI itself is exercised end-to-end against the built binary so the JSON
7+
* field names survive future refactors of the underlying plumbing.
8+
*/
9+
10+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
11+
import { execFileSync } from 'child_process';
12+
import * as fs from 'fs';
13+
import * as path from 'path';
14+
import * as os from 'os';
15+
import { CodeGraph } from '../src';
16+
17+
const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
18+
const PKG_VERSION = JSON.parse(
19+
fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8'),
20+
).version as string;
21+
22+
function runStatusJson(cwd: string): Record<string, unknown> {
23+
const stdout = execFileSync(process.execPath, [BIN, 'status', '--json'], {
24+
cwd,
25+
encoding: 'utf-8',
26+
env: { ...process.env, CODEGRAPH_NO_DAEMON: '1' },
27+
stdio: ['ignore', 'pipe', 'pipe'],
28+
});
29+
// JSON mode prints exactly one line to stdout; be defensive about any stray
30+
// leading output by parsing the last non-empty line.
31+
const line = stdout.trim().split('\n').filter(Boolean).pop()!;
32+
return JSON.parse(line);
33+
}
34+
35+
describe('codegraph status --json — CI fields (#329)', () => {
36+
let tempDir: string;
37+
38+
beforeEach(() => {
39+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-status-json-'));
40+
});
41+
afterEach(() => {
42+
fs.rmSync(tempDir, { recursive: true, force: true });
43+
});
44+
45+
it('getLastIndexedAt() is null before indexing and a recent ms timestamp after', async () => {
46+
const cg = CodeGraph.initSync(tempDir);
47+
expect(cg.getLastIndexedAt()).toBeNull();
48+
49+
fs.writeFileSync(path.join(tempDir, 'a.ts'), 'export const x = 1;\n');
50+
const before = Date.now();
51+
await cg.indexAll();
52+
const after = Date.now();
53+
54+
const last = cg.getLastIndexedAt();
55+
expect(last).not.toBeNull();
56+
expect(typeof last).toBe('number');
57+
expect(last!).toBeGreaterThanOrEqual(before - 1000);
58+
expect(last!).toBeLessThanOrEqual(after + 1000);
59+
cg.close();
60+
});
61+
62+
it('status --json on an UNINITIALIZED project reports version + indexPath + lastIndexed:null', () => {
63+
const out = runStatusJson(tempDir);
64+
expect(out.initialized).toBe(false);
65+
expect(out.version).toBe(PKG_VERSION);
66+
expect(typeof out.indexPath).toBe('string');
67+
expect(out.indexPath as string).toContain('.codegraph');
68+
expect(out.lastIndexed).toBeNull();
69+
});
70+
71+
it('status --json on an INDEXED project reports version + indexPath + a round-trippable lastIndexed', async () => {
72+
fs.writeFileSync(path.join(tempDir, 'a.ts'), 'export const x = 1;\n');
73+
const before = Date.now();
74+
const cg = CodeGraph.initSync(tempDir);
75+
await cg.indexAll();
76+
const after = Date.now();
77+
cg.close();
78+
79+
const out = runStatusJson(tempDir);
80+
expect(out.initialized).toBe(true);
81+
expect(out.version).toBe(PKG_VERSION);
82+
expect(out.indexPath as string).toContain('.codegraph');
83+
expect(typeof out.lastIndexed).toBe('string');
84+
// ISO string that round-trips back into the index window.
85+
const ms = Date.parse(out.lastIndexed as string);
86+
expect(ms).toBeGreaterThanOrEqual(before - 1000);
87+
expect(ms).toBeLessThanOrEqual(after + 1000);
88+
});
89+
});

src/bin/codegraph.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,13 @@ program
676676
try {
677677
if (!isInitialized(projectPath)) {
678678
if (options.json) {
679-
console.log(JSON.stringify({ initialized: false, projectPath }));
679+
console.log(JSON.stringify({
680+
initialized: false,
681+
version: packageJson.version,
682+
projectPath,
683+
indexPath: getCodeGraphDir(projectPath),
684+
lastIndexed: null,
685+
}));
680686
return;
681687
}
682688
console.log(chalk.bold('\nCodeGraph Status\n'));
@@ -695,9 +701,13 @@ program
695701

696702
// JSON output mode
697703
if (options.json) {
704+
const lastIndexedMs = cg.getLastIndexedAt();
698705
console.log(JSON.stringify({
699706
initialized: true,
707+
version: packageJson.version,
700708
projectPath,
709+
indexPath: getCodeGraphDir(projectPath),
710+
lastIndexed: lastIndexedMs != null ? new Date(lastIndexedMs).toISOString() : null,
701711
fileCount: stats.fileCount,
702712
nodeCount: stats.nodeCount,
703713
edgeCount: stats.edgeCount,

src/db/queries.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1421,6 +1421,17 @@ export class QueryBuilder {
14211421
return rows.map(rowToFileRecord);
14221422
}
14231423

1424+
/**
1425+
* Most recent index timestamp (ms since epoch) across all tracked files, or
1426+
* null when nothing is indexed yet. One indexed aggregate, no per-row scan. (#329)
1427+
*/
1428+
getLastIndexedAt(): number | null {
1429+
const row = this.db
1430+
.prepare('SELECT MAX(indexed_at) AS last FROM files')
1431+
.get() as { last: number | null } | undefined;
1432+
return row?.last ?? null;
1433+
}
1434+
14241435
/**
14251436
* Get files that need re-indexing (hash changed)
14261437
*/

src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,15 @@ export class CodeGraph {
576576
return this.orchestrator.getChangedFiles();
577577
}
578578

579+
/**
580+
* Most recent index timestamp (ms since epoch) across all tracked files, or
581+
* null when nothing is indexed yet. Lets library consumers check index
582+
* freshness without shelling out to `codegraph status --json`. (#329)
583+
*/
584+
getLastIndexedAt(): number | null {
585+
return this.queries.getLastIndexedAt();
586+
}
587+
579588
/**
580589
* Extract nodes and edges from source code (without storing)
581590
*/

0 commit comments

Comments
 (0)