feat(cli): add codegraph upgrade self-update + stale-index re-index hint (#710)

`codegraph upgrade [version]` detects how the CLI was installed — the standalone
install.sh/install.ps1 bundle, npm-global, npx, or a source checkout — and
updates in place: re-running the canonical install.sh on macOS/Linux, an
in-place rename-and-extract swap on Windows (a running node.exe can't be
deleted, only renamed, so the detached-helper approach is avoided), and
npm/npx/source-specific guidance otherwise. Flags: `--check` (report only),
`--force`, and a positional version to pin.

Each full index is now stamped with the engine's EXTRACTION_VERSION in
project_metadata; `codegraph status` (and `--json`) flags an index built by an
older engine and recommends re-indexing, and `upgrade` prints the same reminder.
Gated on EXTRACTION_VERSION so it never nags on extraction-neutral releases.

Validated end-to-end on macOS (real bundle upgrade), Linux (Docker, real
curl|sh) and Windows (Parallels VM, real in-place swap). 32 new unit tests.

Closes #679

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-06 13:38:38 -04:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 07af3db6c7
commit 4e5cf2de56
9 changed files with 1053 additions and 3 deletions
+40
View File
@@ -47,6 +47,8 @@ import { GraphTraverser, GraphQueryManager } from './graph';
import { ContextBuilder, createContextBuilder } from './context';
import { Mutex, FileLock } from './utils';
import { FileWatcher, WatchOptions, PendingFile, LockUnavailableError } from './sync';
import { EXTRACTION_VERSION } from './extraction/extraction-version';
import { CodeGraphPackageVersion } from './mcp/version';
// Re-export types for consumers
export * from './types';
@@ -382,6 +384,18 @@ export class CodeGraph {
result.edgesCreated = after.edges - before.edges;
}
// Stamp the index with the engine that built it, so `codegraph status`
// and `codegraph upgrade` can recommend a re-index when the running
// engine produces richer extraction than the one on disk. Only on a
// real full index — a sync touches a subset, so it must NOT advance the
// extraction stamp (the bulk would still be stale). See extraction-version.ts.
if (result.success && result.filesIndexed > 0) {
try {
this.queries.setMetadata('indexed_with_version', CodeGraphPackageVersion);
this.queries.setMetadata('indexed_with_extraction_version', String(EXTRACTION_VERSION));
} catch { /* metadata is advisory — never fail an index over it */ }
}
return result;
} finally {
this.fileLock.release();
@@ -585,6 +599,32 @@ export class CodeGraph {
return this.queries.getLastIndexedAt();
}
/**
* Which engine built the current index: the package version + extraction
* version stamped at the last full `indexAll`. Either field is null for an
* index built before stamping existed (treated as stale). See
* `extraction-version.ts` and `isIndexStale()`.
*/
getIndexBuildInfo(): { version: string | null; extractionVersion: number | null } {
const version = this.queries.getMetadata('indexed_with_version');
const ev = this.queries.getMetadata('indexed_with_extraction_version');
const parsed = ev != null ? parseInt(ev, 10) : NaN;
return { version, extractionVersion: Number.isFinite(parsed) ? parsed : null };
}
/**
* True when the on-disk index was built by an engine whose extraction is
* older than the one now running — i.e. a re-index would add data a migration
* can't backfill. False when there's no index yet (nothing to refresh) or the
* stamp is current. This is the signal behind `codegraph status`'s re-index
* hint and `codegraph upgrade`'s reminder.
*/
isIndexStale(): boolean {
if (this.queries.getLastIndexedAt() == null) return false;
const { extractionVersion } = this.getIndexBuildInfo();
return extractionVersion == null || extractionVersion < EXTRACTION_VERSION;
}
/**
* Extract nodes and edges from source code (without storing)
*/