Просмотр исходного кода

feat(cli): add version, indexPath, lastIndexed to status --json (#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 #333 and #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>
Colby Mchenry 2 недель назад
Родитель
Сommit
7b62356f53
5 измененных файлов с 124 добавлено и 1 удалено
  1. 4 0
      CHANGELOG.md
  2. 89 0
      __tests__/status-json.test.ts
  3. 11 1
      src/bin/codegraph.ts
  4. 11 0
      src/db/queries.ts
  5. 9 0
      src/index.ts

+ 4 - 0
CHANGELOG.md

@@ -9,6 +9,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 
 ## [Unreleased]
 ## [Unreleased]
 
 
+### New Features
+
+- `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)
+
 ### Fixes
 ### Fixes
 
 
 - 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)
 - 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)

+ 89 - 0
__tests__/status-json.test.ts

@@ -0,0 +1,89 @@
+/**
+ * Tests for the CI/scripting fields `codegraph status --json` exposes (issue
+ * #329): the `version`, `indexPath`, and `lastIndexed` fields, plus the
+ * matching `CodeGraph.getLastIndexedAt()` library method.
+ *
+ * The CLI itself is exercised end-to-end against the built binary so the JSON
+ * field names survive future refactors of the underlying plumbing.
+ */
+
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { execFileSync } from 'child_process';
+import * as fs from 'fs';
+import * as path from 'path';
+import * as os from 'os';
+import { CodeGraph } from '../src';
+
+const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
+const PKG_VERSION = JSON.parse(
+  fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8'),
+).version as string;
+
+function runStatusJson(cwd: string): Record<string, unknown> {
+  const stdout = execFileSync(process.execPath, [BIN, 'status', '--json'], {
+    cwd,
+    encoding: 'utf-8',
+    env: { ...process.env, CODEGRAPH_NO_DAEMON: '1' },
+    stdio: ['ignore', 'pipe', 'pipe'],
+  });
+  // JSON mode prints exactly one line to stdout; be defensive about any stray
+  // leading output by parsing the last non-empty line.
+  const line = stdout.trim().split('\n').filter(Boolean).pop()!;
+  return JSON.parse(line);
+}
+
+describe('codegraph status --json — CI fields (#329)', () => {
+  let tempDir: string;
+
+  beforeEach(() => {
+    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-status-json-'));
+  });
+  afterEach(() => {
+    fs.rmSync(tempDir, { recursive: true, force: true });
+  });
+
+  it('getLastIndexedAt() is null before indexing and a recent ms timestamp after', async () => {
+    const cg = CodeGraph.initSync(tempDir);
+    expect(cg.getLastIndexedAt()).toBeNull();
+
+    fs.writeFileSync(path.join(tempDir, 'a.ts'), 'export const x = 1;\n');
+    const before = Date.now();
+    await cg.indexAll();
+    const after = Date.now();
+
+    const last = cg.getLastIndexedAt();
+    expect(last).not.toBeNull();
+    expect(typeof last).toBe('number');
+    expect(last!).toBeGreaterThanOrEqual(before - 1000);
+    expect(last!).toBeLessThanOrEqual(after + 1000);
+    cg.close();
+  });
+
+  it('status --json on an UNINITIALIZED project reports version + indexPath + lastIndexed:null', () => {
+    const out = runStatusJson(tempDir);
+    expect(out.initialized).toBe(false);
+    expect(out.version).toBe(PKG_VERSION);
+    expect(typeof out.indexPath).toBe('string');
+    expect(out.indexPath as string).toContain('.codegraph');
+    expect(out.lastIndexed).toBeNull();
+  });
+
+  it('status --json on an INDEXED project reports version + indexPath + a round-trippable lastIndexed', async () => {
+    fs.writeFileSync(path.join(tempDir, 'a.ts'), 'export const x = 1;\n');
+    const before = Date.now();
+    const cg = CodeGraph.initSync(tempDir);
+    await cg.indexAll();
+    const after = Date.now();
+    cg.close();
+
+    const out = runStatusJson(tempDir);
+    expect(out.initialized).toBe(true);
+    expect(out.version).toBe(PKG_VERSION);
+    expect(out.indexPath as string).toContain('.codegraph');
+    expect(typeof out.lastIndexed).toBe('string');
+    // ISO string that round-trips back into the index window.
+    const ms = Date.parse(out.lastIndexed as string);
+    expect(ms).toBeGreaterThanOrEqual(before - 1000);
+    expect(ms).toBeLessThanOrEqual(after + 1000);
+  });
+});

+ 11 - 1
src/bin/codegraph.ts

@@ -676,7 +676,13 @@ program
     try {
     try {
       if (!isInitialized(projectPath)) {
       if (!isInitialized(projectPath)) {
         if (options.json) {
         if (options.json) {
-          console.log(JSON.stringify({ initialized: false, projectPath }));
+          console.log(JSON.stringify({
+            initialized: false,
+            version: packageJson.version,
+            projectPath,
+            indexPath: getCodeGraphDir(projectPath),
+            lastIndexed: null,
+          }));
           return;
           return;
         }
         }
         console.log(chalk.bold('\nCodeGraph Status\n'));
         console.log(chalk.bold('\nCodeGraph Status\n'));
@@ -695,9 +701,13 @@ program
 
 
       // JSON output mode
       // JSON output mode
       if (options.json) {
       if (options.json) {
+        const lastIndexedMs = cg.getLastIndexedAt();
         console.log(JSON.stringify({
         console.log(JSON.stringify({
           initialized: true,
           initialized: true,
+          version: packageJson.version,
           projectPath,
           projectPath,
+          indexPath: getCodeGraphDir(projectPath),
+          lastIndexed: lastIndexedMs != null ? new Date(lastIndexedMs).toISOString() : null,
           fileCount: stats.fileCount,
           fileCount: stats.fileCount,
           nodeCount: stats.nodeCount,
           nodeCount: stats.nodeCount,
           edgeCount: stats.edgeCount,
           edgeCount: stats.edgeCount,

+ 11 - 0
src/db/queries.ts

@@ -1421,6 +1421,17 @@ export class QueryBuilder {
     return rows.map(rowToFileRecord);
     return rows.map(rowToFileRecord);
   }
   }
 
 
+  /**
+   * Most recent index timestamp (ms since epoch) across all tracked files, or
+   * null when nothing is indexed yet. One indexed aggregate, no per-row scan. (#329)
+   */
+  getLastIndexedAt(): number | null {
+    const row = this.db
+      .prepare('SELECT MAX(indexed_at) AS last FROM files')
+      .get() as { last: number | null } | undefined;
+    return row?.last ?? null;
+  }
+
   /**
   /**
    * Get files that need re-indexing (hash changed)
    * Get files that need re-indexing (hash changed)
    */
    */

+ 9 - 0
src/index.ts

@@ -576,6 +576,15 @@ export class CodeGraph {
     return this.orchestrator.getChangedFiles();
     return this.orchestrator.getChangedFiles();
   }
   }
 
 
+  /**
+   * Most recent index timestamp (ms since epoch) across all tracked files, or
+   * null when nothing is indexed yet. Lets library consumers check index
+   * freshness without shelling out to `codegraph status --json`. (#329)
+   */
+  getLastIndexedAt(): number | null {
+    return this.queries.getLastIndexedAt();
+  }
+
   /**
   /**
    * Extract nodes and edges from source code (without storing)
    * Extract nodes and edges from source code (without storing)
    */
    */