diff --git a/__tests__/sync.test.ts b/__tests__/sync.test.ts index 708a92a4..b868500d 100644 --- a/__tests__/sync.test.ts +++ b/__tests__/sync.test.ts @@ -49,6 +49,14 @@ describe('Sync Module', () => { }); describe('getChangedFiles()', () => { + it('should report the most recent indexed timestamp', () => { + const lastIndexed = cg.getLastIndexedAt(); + + expect(lastIndexed).toEqual(expect.any(Number)); + expect(lastIndexed).toBeGreaterThan(0); + expect(new Date(lastIndexed!).toISOString()).toMatch(/^\d{4}-\d{2}-\d{2}T/); + }); + it('should detect added files', () => { // Add a new file fs.writeFileSync( diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index 6bc63b3f..863ab3d1 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -246,6 +246,39 @@ function createVerboseProgress(): (progress: { phase: string; current: number; t }; } +function toIsoTimestamp(timestamp: number | null | undefined): string | null { + if (timestamp == null) { + return null; + } + return new Date(timestamp).toISOString(); +} + +async function getConfiguredAgentCount(projectPath: string): Promise { + const previousCwd = process.cwd(); + try { + if (fs.existsSync(projectPath) && fs.statSync(projectPath).isDirectory()) { + process.chdir(projectPath); + } + + const { detectAll } = await import('../installer/targets/registry'); + const configuredTargets = new Set(); + + for (const location of ['global', 'local'] as const) { + for (const { target, detection } of detectAll(location)) { + if (detection.alreadyConfigured) { + configuredTargets.add(target.id); + } + } + } + + return configuredTargets.size; + } catch { + return 0; + } finally { + process.chdir(previousCwd); + } +} + /** * Print success message */ @@ -692,11 +725,20 @@ program .option('-j, --json', 'Output as JSON') .action(async (pathArg: string | undefined, options: { json?: boolean }) => { const projectPath = resolveProjectPath(pathArg); + const indexPath = getCodeGraphDir(projectPath); + const agentCount = options.json ? await getConfiguredAgentCount(projectPath) : 0; try { if (!isInitialized(projectPath)) { if (options.json) { - console.log(JSON.stringify({ initialized: false, projectPath })); + console.log(JSON.stringify({ + initialized: false, + projectPath, + indexPath, + lastIndexed: null, + agentCount, + version: packageJson.version, + })); return; } console.log(chalk.bold('\nCodeGraph Status\n')); @@ -712,12 +754,17 @@ program const changes = cg.getChangedFiles(); const backend = cg.getBackend(); const journalMode = cg.getJournalMode(); + const lastIndexed = toIsoTimestamp(cg.getLastIndexedAt()); // JSON output mode if (options.json) { console.log(JSON.stringify({ initialized: true, projectPath, + indexPath, + lastIndexed, + agentCount, + version: packageJson.version, fileCount: stats.fileCount, nodeCount: stats.nodeCount, edgeCount: stats.edgeCount, diff --git a/src/db/queries.ts b/src/db/queries.ts index 9419a313..a66df336 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -1192,6 +1192,16 @@ export class QueryBuilder { return rows.map(rowToFileRecord); } + /** + * Get the most recent index timestamp across all tracked files. + */ + getLastIndexedAt(): number | null { + const row = this.db.prepare('SELECT MAX(indexed_at) AS last_indexed_at FROM files').get() as { + last_indexed_at: number | null; + } | undefined; + return row?.last_indexed_at ?? null; + } + /** * Get files that need re-indexing (hash changed) */ diff --git a/src/index.ts b/src/index.ts index 784bdbfa..562458f3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -654,6 +654,13 @@ export class CodeGraph { return this.queries.getAllFiles(); } + /** + * Get the most recent index timestamp across all tracked files. + */ + getLastIndexedAt(): number | null { + return this.queries.getLastIndexedAt(); + } + // =========================================================================== // Graph Query Methods // ===========================================================================