/** * Repository Manager * * Manages GitNexus index storage: * - Per-repo metadata file (gitnexus.json) under .gitnexus/, dual-written to a * legacy meta.json mirror for backward compatibility (see MIGRATION.md) * - .gitnexus/ directory for local metadata and caches (parse-cache, parsedfile-store) * - Global registry at ~/.gitnexus/registry.json for MCP server discovery * * gitnexus.json is simply a filename distinct from the generic meta.json — it * has no bearing on git worktree behavior. .gitnexus/ remains fully git-ignored * in every case; each worktree already has its own independent .gitnexus/ by * construction (getStoragePath is per-checkout), regardless of which filename * the metadata inside it uses. */ import { branchSlug, resolveBranchPlacement, type BranchSummary } from './branch-index.js'; export { branchSlug, resolveBranchPlacement }; export type { BranchSummary }; /** * Normalise a repo path for registry comparison across platforms * (#664 review feedback from @evander-wang). * * Why this exists: `path.resolve` alone is NOT enough for * cross-platform registry stability. * - **macOS**: tmpdirs and `/var` are symlinks to `/private/var`. * A child process that stored `/private/var/folders/.../repo` in * the registry cannot later be matched by an outer caller that * supplies the symlink form `/var/folders/.../repo`. `path.resolve` * does not follow symlinks; `realpathSync.native` does. * - **Windows**: GitHub runners surface tmpdirs in 8.3 short-name * form (`RUNNERA~1\...`), but `process.cwd()` often returns the * long form (`runneradmin\...`). `realpathSync.native` normalises * both sides to the long-name canonical path. * * Fallback behaviour: if the path does not exist on disk (e.g. a user * passed `gitnexus remove some-alias` and the alias misses every * registry entry, or the caller is resolving a path that was deleted * after registration), we return `path.resolve(p)` rather than * throwing. This preserves the idempotent-on-missing semantics of * `resolveRegistryEntry` / `remove`. * * Backwards compatibility: this function is applied to BOTH the * caller-supplied input AND each stored `entry.path` at compare time * inside `resolveRegistryEntry`, so registries written by older * versions (where `registerRepo` only ran `path.resolve`) still match * correctly. Newly-written entries are canonicalised at write time too * so the registry stabilises over analyze/re-analyze cycles. */ export declare const canonicalizePath: (p: string) => string; /** * Compare two already-canonicalised registry paths. Case-insensitive on Windows * (its filesystem is), case-sensitive elsewhere. Both arguments must already be * run through {@link canonicalizePath}; this is the single comparison the registry * lookups/dedup/finalize checks all share so they answer identically. */ export declare const registryPathEquals: (a: string, b: string) => boolean; export interface RepoMeta { repoPath: string; lastCommit: string; indexedAt: string; /** * Canonical `origin` remote URL captured at index time. Used to * fingerprint the same logical repo across multiple on-disk clones * (worktrees, agent workspaces, "clean clone for indexing"). When * absent (no remote configured, git unavailable, etc.) the repo is * treated as path-only and sibling-clone detection is skipped. */ remoteUrl?: string; stats?: { files?: number; nodes?: number; edges?: number; communities?: number; processes?: number; embeddings?: number; }; /** * Bumped whenever incremental-indexing invariants change in an * incompatible way (delete-and-rewrite logic, subgraph extraction, * graph-wide node handling). On mismatch, runFullAnalysis forces a * full rebuild rather than risk an inconsistent incremental update. */ schemaVersion?: number; /** * The resolved GITNEXUS_FTS_CJK_SEGMENTATION mode ('none' | 'bigram') the * existing index's content/description columns were last written under * (#2331/#2339). On mismatch with the live process's resolved mode, * runFullAnalysis forces a full rebuild so indexed text and query-time * segmentation never diverge. Always stamped (never omitted), unlike * `pdg` below — the default 'none' is itself a meaningful value to * compare, not an absence. */ cjkSegmentation?: string; /** * SHA-256 of every file's content at the time of the last successful * indexing run. The next run computes current hashes and diffs against * this map to determine which files' DB rows must be replaced. * Map keys are repo-relative paths. */ fileHashes?: Record; /** * Crash-recovery dirty flag — a generic marker written to the metadata * file (gitnexus.json + its meta.json mirror) BEFORE any destructive DB * mutation by BOTH writeback branches (incremental since its introduction; * full rebuilds over an existing meta since #2099 F1); cleared on success * by overwriting the metadata file. If a run crashes between, the next * run sees the flag and forces a full rebuild — the cheapest path back * to a known-good index. */ incrementalInProgress?: { /** When the run started (epoch ms). */ startedAt: number; /** Number of files in the writable set, for diagnostic logs. * `0` on the full-rebuild path (no incremental write set exists). */ toWriteCount: number; }; /** * Name of the git branch this index represents (#2106). Absent for the * default/legacy single-branch case so the flat metadata file stays * byte-identical to pre-multi-branch output. When present in the FLAT * metadata file, it records which branch "owns" the flat slot (the first * branch indexed); per-branch indexes under `branches//` always carry * their own `branch`. */ branch?: string; /** * The parse-cache chunk keys this branch's index needs (#2106 R6). The * parse-cache and durable parsedfile store live ONCE at the repo root and are * shared across branches; recording each branch's live chunk keys lets the * prune step union them so re-analyzing one branch doesn't evict another * branch's still-live shards. Additive/optional; absent in legacy metas. */ cacheKeys?: string[]; /** * The effective `--pdg` configuration this index's DB rows were built * under (#2099 F1). Presence ≡ the BasicBlock/CFG layer exists in the DB; * ABSENT ≡ pdg-off — which covers every legacy meta, since `--pdg` * shipped opt-in. Caps are recorded RESOLVED (defaults applied) so an * explicit-default run compares equal to a default run. run-analyze * compares this against the requested options and forces a full * writeback on any mismatch — the incremental path only persists * changed-file nodes and would otherwise silently drop (or strand) the * CFG layer on a mode flip. Additive/optional, no * INCREMENTAL_SCHEMA_VERSION bump (a bump would force a one-time full * rebuild for every user). NOTE the removal mechanism is load-bearing: * the end-of-run meta is a fresh object literal, NOT a spread of the * prior meta, so omitting this field on a pdg-off run is what clears * the stamp after an on→off flip. */ pdg?: { /** Worker-side per-function source-line cap, resolved (0 = unlimited). */ maxFunctionLines: number; /** Emit-side per-function CFG edge cap, resolved (0 = unlimited). */ maxEdgesPerFunction: number; /** * Emit-side per-function REACHING_DEF edge cap, resolved (0 = unlimited; * #2082 M2). ABSENT on an M1-era stamp — which is exactly what makes * `pdgModeMismatch` trip on the first M2 run over an M1 index and force * the full writeback that populates REACHING_DEF rows. Optional in the * type for that reason; resolved (always present) on every M2+ write. */ maxReachingDefEdgesPerFunction?: number; /** * Emit-side per-function CDG (control-dependence) edge cap, resolved * (0 = unlimited; #2085 M5). ABSENT on any pre-M5 stamp — that absence is * what trips `pdgModeMismatch` on the first CDG-aware run and forces the * full writeback that materialises CDG edges. Optional for that upgrade * reason; resolved (always present) on every M5+ write. */ maxCdgEdgesPerFunction?: number; /** * Per-function taint findings cap, resolved (0 = unlimited; #2083 M3). * ABSENT on an M1/M2-era stamp — like `maxReachingDefEdgesPerFunction`, * that absence is what trips `pdgModeMismatch` on the first M3 run and * forces the full writeback that populates TAINTED/SANITIZES rows. */ maxTaintFindingsPerFunction?: number; /** Per-finding taint hop cap, resolved (0 = unlimited; #2083 M3 KTD6 — * bounds the persisted hop-encoded `reason`). Optional for the same * M2-era-stamp upgrade reason as the findings cap. */ maxTaintHops?: number; /** * Per-run cross-function caps, resolved (0 = unlimited; #2084 M4 review * P1-3). ABSENT on an M3-era stamp — that absence trips `pdgModeMismatch` * on the first run that adds them and forces the full writeback that * re-materialises TAINT_PATH within bounds. Optional for that upgrade * reason; resolved (always present) on every post-fix write. */ maxInterprocFindings?: number; maxInterprocHops?: number; maxInterprocEdges?: number; /** * Digest of the built-in taint model the persisted findings were * produced under (#2083 M3 KTD7/R7). Any model-content change ships a * new digest → mismatch → full writeback repopulates taint edges * without `--force`. Optional: absent on pre-M3 stamps. */ taintModelVersion?: string; /** * Identity of the reaching-definitions solver the persisted REACHING_DEF * rows were produced under (#2201 review R3). The SSA-sparse rewrite computes * FULL facts for deep-loop functions the old dense worklist truncated to * empty (the blocks×64 ceiling no longer fires) — but an existing `--pdg` * index built under the old solver carries those truncated rows. ABSENT on * any pre-#2201 stamp, so that absence trips `pdgModeMismatch` on the first * upgraded run and forces the full writeback that recomputes the now-fuller * REACHING_DEF coverage without `--force`. Bump the tag on any future change * that alters which facts the solver emits. Optional for that upgrade reason; * resolved (always present) on every post-#2201 write. */ reachingDefSolver?: string; /** * Whether this `--pdg` index recorded the FU-C `CALL_SUMMARY` return-value * ascent layer (per-callee param→return summary edges). `true` on every * FU-C+ (v4) write. ABSENT on any pre-FU-C (v3) `--pdg` stamp — that absence * is what tells `impact`'s PDG mode the index predates CALL_SUMMARY, so it * surfaces a "no return-value ascent (re-index for CALL_SUMMARY)" note while * STILL serving the intra slice. CALL_SUMMARY is deliberately NOT a required * sub-layer for `pdgLayerStatus` to report `'ready'`: a v3 index stays fully * usable for the intra-procedural statement slice; only the ascent upgrade is * unavailable. Optional for that back-compat reason. */ hasCallSummary?: boolean; }; } /** * Bumped whenever incremental-indexing invariants change incompatibly. * v2: `BasicBlock.callees` column added (statement-precise inter-procedural * reach substrate) — an index built before this lacks the column, so a full * re-analyze is required rather than an incremental top-up. * v3: `BasicBlock.calleeIds` column added (sound resolved-callee-id parallel * to `callees`, #2227) — same contract: an index built before this lacks the * column, so a full re-analyze is forced rather than an incremental top-up. * v4: `CALL_SUMMARY` relation type added (per-callee RETURN-VALUE ASCENT * summary edges, PDG FU-C). A pre-v4 `--pdg` index has NO CALL_SUMMARY edges, * so the engine would silently UNDER-REPORT return-value ascent on an * incremental top-up; force a full re-analyze instead (same contract as v2/v3). * This single bump covers the whole FU-C re-index window (and the later FU-B-2). * v5: `Route` node identity changed to `(method, url)` (#2289 — a same-URL * GET/POST pair is now two distinct Route nodes). Every declarative-route node * id moved from `Route:/x` to `Route:GET /x` (filesystem routes keep their * URL-only id). The incremental writeback preserves unchanged-file rows, so a * top-up against a pre-v5 index would strand old url-keyed Route nodes alongside * new composite-keyed ones — force a full re-analyze instead. */ export declare const INCREMENTAL_SCHEMA_VERSION = 5; export interface IndexedRepo { repoPath: string; storagePath: string; lbugPath: string; metaPath: string; meta: RepoMeta; } /** * Shape of an entry in the global registry (~/.gitnexus/registry.json) */ export interface RegistryEntry { name: string; path: string; storagePath: string; indexedAt: string; lastCommit: string; /** See {@link RepoMeta.remoteUrl}. Mirrored from meta at register time. */ remoteUrl?: string; stats?: RepoMeta['stats']; /** * Branch name owning the flat/primary index (#2106). Mirrors the flat * `meta.branch`. Absent for legacy single-branch entries and non-git repos — * additive and backward compatible. */ branch?: string; /** * Non-primary branch indexes for this same path (#2106). Absent when only the * primary branch is indexed, preserving the one-entry-per-path model and the * legacy registry shape. */ branches?: BranchSummary[]; } export declare const INDEX_METADATA_FILE = "gitnexus.json"; /** * Get the .gitnexus storage path for a repository. * Used for local metadata and caches that are not committed. */ export declare const getStoragePath: (repoPath: string) => string; /** * Get paths to key storage files. * * `storagePath` is ALWAYS the flat `/.gitnexus` — content-addressed * caches (`parse-cache/`, `parsedfile-store/`) live there and are shared * across branches (#2106 KTD7). When `branch` is provided, both `lbugPath` * and `metaPath` are scoped under `branches//`. For the flat call * (no `branch`), `storagePath` and `lbugPath` remain byte-identical to the * pre-multi-branch behavior (#2106); `metaPath`'s FILENAME changed from * `meta.json` to `gitnexus.json` (PR #2363) — `saveMeta` keeps a `meta.json` * mirror in sync for consumers that still read the legacy name. * * Each branch slot has its own metadata file: * - Primary/flat: /.gitnexus/gitnexus.json * - Feature branches: /.gitnexus/branches//gitnexus.json * * Callers should use `loadMeta(metaDir)` and `saveMeta(metaDir, meta)` where * metaDir is the directory containing the metadata file — both handle the * legacy mirror automatically. */ export declare const getStoragePaths: (repoPath: string, branch?: string) => { storagePath: string; lbugPath: string; metaPath: string; }; /** * Check whether a KuzuDB index exists in the given storage path. * Non-destructive — safe to call from status commands. */ export declare const hasKuzuIndex: (storagePath: string) => Promise; /** * Clean up stale KuzuDB files after migration to LadybugDB. * * Returns: * found — true if .gitnexus/kuzu existed and was deleted * needsReindex — true if kuzu existed but lbug does not (re-analyze required) * * Callers own the user-facing messaging; this function only deletes files. */ export declare const cleanupOldKuzuFiles: (storagePath: string) => Promise<{ found: boolean; needsReindex: boolean; }>; /** * Load metadata from a directory containing the metadata file (gitnexus.json). * For primary/flat: metaDir = /.gitnexus * For feature branches: metaDir = /.gitnexus/branches/ * * Falls back to the legacy `meta.json` mirror ONLY when `gitnexus.json` is * provably absent (ENOENT/ENOTDIR). Any other failure — a parse error, EACCES, * EIO — returns null instead of silently resurrecting possibly-stale legacy * content: a corrupt primary file must trigger the same safe full-rebuild path * a missing index would (the fail-safe `saveMeta`'s docstring relies on), not * an incremental run over a stale legacy baseline. */ export declare const loadMeta: (metaDir: string) => Promise; /** * Save metadata to the metadata file (gitnexus.json) in the given directory, * dual-writing the legacy `meta.json` mirror for backward compatibility. * * Atomic via tmp-file + rename (matches `saveParseCache`'s pattern). The * `incrementalInProgress` dirty flag travels through this file — a crash * mid-write would leave a corrupt `gitnexus.json` that the next run's * `loadMeta` would silently treat as "no prior index", losing the dirty * flag and skipping the recovery full-rebuild. Write-and-rename rules * that out: the rename is atomic on POSIX and on Windows (`fs.rename` * on `node:fs/promises` uses `MoveFileEx(REPLACE_EXISTING)`), so either * the old or the new file is observed at every moment. * * `gitnexus.json` is the primary write and must succeed. `meta.json` is a * best-effort mirror kept for consumers that only know the legacy filename * (see MIGRATION.md) — its write failure is logged, not thrown, so a * mirror-write hiccup never fails the caller's analyze run. */ export declare const saveMeta: (metaDir: string, meta: RepoMeta) => Promise; /** * Check if a path has a GitNexus index (metadata file or legacy location) */ export declare const hasIndex: (repoPath: string) => Promise; /** * Load an indexed repo from a path (checks metadata file first, then legacy) */ export declare const loadRepo: (repoPath: string) => Promise; /** * Reconcile the metadata files for a repo's flat slot and every * `branches//` slot. Runs once per `analyze` (see run-analyze.ts). * * This is a best-effort compatibility sync, NOT a one-way migration: the * legacy `meta.json` mirror is kept in sync indefinitely (removal happens at * a future major version — see MIGRATION.md), so older binaries, still-running * MCP servers, and the shipped editor hooks keep working, and a rollback to a * pre-rename version sees current metadata instead of "no prior index". * Returns true when any file was written. */ export declare const reconcileMetadataFiles: (repoPath: string) => Promise; /** * Find .gitnexus by walking up from a starting path */ export declare const findRepo: (startPath: string) => Promise; export declare function isReadOnlyFilesystemError(err: unknown): boolean; /** * True for errors that prove a path is absent (ENOENT/ENOTDIR) — as opposed * to transient/permission failures (EIO/EACCES/EBUSY…) where the file may * well still exist. Exported for consumers that need the same "provably * missing vs not provably absent" distinction (e.g. collectBranchCacheKeys). */ export declare function isMissingFilesystemError(err: unknown): boolean; /** * Keep .gitnexus/ ignored. It contains local index state and caches. */ export declare const ensureGitNexusIgnored: (repoPath: string) => Promise; /** * Get the path to the global GitNexus directory */ export declare const getGlobalDir: () => string; /** * Get the path to the global registry file */ export declare const getGlobalRegistryPath: () => string; /** * Read the global registry. Returns empty array if not found. */ export declare const readRegistry: () => Promise; /** * Options for {@link registerRepo}. All optional — callers without any * disambiguation requirement can keep calling `registerRepo(path, meta)` * unchanged. */ export interface RegisterRepoOptions { /** * User-provided alias from `analyze --name ` (#829). Overrides * the default basename-derived registry `name`. Persisted — subsequent * re-analyses of the same path without `--name` preserve the alias. */ name?: string; /** * Allow two DIFFERENT repo paths to register under the same alias * (#829). Mapped from the `--allow-duplicate-name` CLI flag. * * Scope: this flag governs cross-path alias sharing only — one repo * path always has exactly one registry entry (and therefore exactly * one alias). Re-analyzing the same path with `--name Y` overwrites * a previous `--name X`; it does NOT create a second entry or a * second alias for the same path (see the upsert-by-resolved-path * logic in {@link registerRepo} and the * `re-registerRepo with a different name overrides the previous * alias` test in `test/unit/repo-manager.test.ts`). * * Distinct from `--force` (which only triggers pipeline re-index); * a user accepting a duplicate alias should not be forced to also * re-run the full pipeline. */ allowDuplicateName?: boolean; /** * Non-primary branch this run indexed (#2106). When set, the branch's * summary is upserted into the entry's `branches[]` and the primary * top-level fields are left untouched. When `undefined`, this is a * primary/flat run that refreshes the top-level fields (and preserves any * existing branch summaries). */ branch?: string; } /** * Thrown by {@link registerRepo} when a requested name is already in * use by a DIFFERENT path. The CLI layer surfaces this as an actionable * error instead of relying on `.message` string-matching. * * The colliding alias is exposed as `err.registryName` (not `err.name`). * `err.name` keeps its inherited `Error.prototype.name` semantics (the * class name) so downstream code can do the usual `err.name === * 'RegistryNameCollisionError'` checks; use the `kind` discriminant or * `instanceof RegistryNameCollisionError` for type-safe narrowing. */ export declare class RegistryNameCollisionError extends Error { readonly registryName: string; readonly existingPath: string; readonly requestedPath: string; readonly kind: "RegistryNameCollisionError"; constructor(registryName: string, existingPath: string, requestedPath: string); } /** * Register (add or update) a repo in the global registry. * Called after `gitnexus analyze` completes. * * Name resolution precedence (#829, #979): * 1. explicit `opts.name` (from `analyze --name `) * 2. preserved alias on an existing entry for this path * 3. `git config --get remote.origin.url` repo name (#979 — recovers * a meaningful name for monorepo subprojects, git worktrees, and * Gas-Town-style `/refinery/rig/` layouts where the basename * is generic) * 4. `path.basename(repoPath)` (the original default) * * Duplicate-name guard: if another path already uses the resolved * `name`, throw {@link RegistryNameCollisionError} unless * `opts.allowDuplicateName` is set. The guard ONLY fires when the user explicitly passed a * `name`; un-aliased basename collisions continue to register silently * so existing users who don't know about `--name` see no behaviour * change. * * Returns the `name` that was actually written to the registry — the * caller can re-use it to keep AGENTS.md / skill files aligned with the * MCP-visible repo name (#979). */ export declare const registerRepo: (repoPath: string, meta: RepoMeta, opts?: RegisterRepoOptions) => Promise; /** * Remove a repo from the global registry. * Called after `gitnexus clean`. */ export declare const unregisterRepo: (repoPath: string) => Promise; /** * Remove a single non-primary branch's summary from a repo's registry entry * (#2106 R7). Called by `gitnexus clean --branch`. Returns `true` when a * matching `branches[]` summary was found and removed; `false` otherwise (so * the CLI can report "no such indexed branch" without crashing). The top-level * primary entry is left intact; an empty `branches[]` is dropped to keep the * registry shape legacy-clean. */ export declare const removeBranchIndex: (repoPath: string, branch: string) => Promise; /** * Record that the flat workspace slot now serves `branch` (#2354). * * The flat index follows the checked-out working tree, so when a plain * analyze lands on a branch that also has a pinned `branches//` * sub-index, that sub-index becomes permanently shadowed — explicit * `--branch` runs re-resolve to the flat slot and query-side branch scoping * serves the flat handle first. Delete the shadowed directory and drop its * registry summary in the same pass (leaving either half behind would strand * un-cleanable disk bloat), and refresh the entry's top-level `branch` label * so `list`/`list_repos`/branch-scoped queries stay coherent. * * Deliberately narrow for the analyze fast path: a missing registry entry is * a no-op — including the sub-index deletion, which only runs for registered * repos (never self-heals an unregistered repo, per #2264/#1169; the registry * check precedes the rm per #2364 review F2) — and no subprocess is spawned. */ export declare const adoptFlatBranchLabel: (repoPath: string, branch: string) => Promise; /** * Thrown by {@link resolveRegistryEntry} when no registered repo matches * the caller's target string (by alias, basename, remote-inferred name, * or resolved path). CLI callers that want idempotent "remove" semantics * should catch this and exit 0 with a warning; non-idempotent callers * (e.g. MCP tools) can surface the error directly. */ export declare class RegistryNotFoundError extends Error { readonly target: string; readonly availableNames: string[]; readonly kind: "RegistryNotFoundError"; constructor(target: string, availableNames: string[]); } /** * Thrown by {@link resolveRegistryEntry} when the target string matches * the `name` of two or more entries — only possible when the user * previously registered duplicates via `analyze --name X * --allow-duplicate-name` (#829). The error carries enough information * for the caller to render an actionable disambiguation hint without * string-matching on `.message`. * * `kind` is a string literal discriminant (same pattern as * {@link RegistryNameCollisionError}) so callers can narrow via * `err.kind === 'RegistryAmbiguousTargetError'` without importing the * class. */ export declare class RegistryAmbiguousTargetError extends Error { readonly target: string; readonly matches: RegistryEntry[]; readonly kind: "RegistryAmbiguousTargetError"; constructor(target: string, matches: RegistryEntry[]); } /** * Thrown by {@link assertAnalysisFinalized} when a successful `analyze` * run did not actually persist the index metadata file or did not register * the repo in `~/.gitnexus/registry.json` (#1169). * * Why this exists: on Windows, `gitnexus analyze` has been observed to * exit cleanly (code 0) with `lbug.wal` written but no metadata file, * leaving the repo invisible to `gitnexus list`/`status` and downstream * MCP discovery. The only signal to the user was an empty banner — * which is indistinguishable from a no-op early return. This invariant * fails loudly with an actionable diagnostic so the silent-finalize bug * surfaces with a non-zero exit code and a recoverable error message * regardless of the upstream root cause (re-exec churn, native module * side effects, antivirus, or future regressions). */ export declare class AnalysisNotFinalizedError extends Error { readonly repoPath: string; readonly storagePath: string; readonly missing: 'meta' | 'registry-entry'; readonly registryPath: string; readonly kind: "AnalysisNotFinalizedError"; constructor(repoPath: string, storagePath: string, missing: 'meta' | 'registry-entry', registryPath: string); } /** * True when the global registry already contains an entry whose canonical path * matches `repoPath`. Uses the same canonical, case-folded (Windows) comparison * as {@link assertAnalysisFinalized} so "is it registered?" answers identically * at the analyze fast-path gate and at the finalize assertion. Pure read. */ export declare const isRepoRegistered: (repoPath: string) => Promise; /** * Verify that a successful `analyze` call actually produced an indexed, * registered repo on disk. Two checks, both strictly required: * * 1. `gitnexus.json` must exist at `/.gitnexus/gitnexus.json` * (the primary metadata file; the legacy `meta.json` mirror is not * sufficient — a finalized analyze always writes the primary). * 2. The global registry (`getGlobalRegistryPath()`) must contain an * entry whose canonical path matches `repoPath`. * * Throws {@link AnalysisNotFinalizedError} on the first failure with the * specific missing artifact. Pure read — does not mutate disk state. * * Callers must skip this assertion on the `alreadyUpToDate` early-return * path, where the rebuild was deliberately not run. */ export declare const assertAnalysisFinalized: (repoPath: string) => Promise; /** * Thrown by {@link assertSafeStoragePath} when a registry entry's * `storagePath` does NOT point at the expected `/.gitnexus` * subfolder. CLI destructive commands (`remove`, `clean --all`) should * catch this and exit non-zero without deleting anything — the usual * cause is a corrupted or hand-edited `~/.gitnexus/registry.json`, and * proceeding would mean `fs.rm(recursive: true)` on whatever odd path * the entry is pointing at. */ export declare class UnsafeStoragePathError extends Error { readonly entry: RegistryEntry; readonly expectedStoragePath: string; readonly actualStoragePath: string; readonly kind: "UnsafeStoragePathError"; constructor(entry: RegistryEntry, expectedStoragePath: string, actualStoragePath: string); } /** * Guard rail for destructive CLI paths (`remove` #664, * `clean --all` #258, future MCP `remove` tool): verify that a * registry entry's `storagePath` is the canonical `/.gitnexus` * subfolder of its `path`. If not, throw {@link UnsafeStoragePathError} * so the caller exits without touching disk. * * Why this exists (#1003 review — @magyargergo): * - `~/.gitnexus/registry.json` is a plain-text user-writable file. * A corrupted, hand-edited, or downgrade/upgrade-racing entry * could plausibly end up with `storagePath === ""` (resolves to * cwd), `storagePath === path` (the repo root!), `storagePath` * equal to a parent/sibling of the repo, or simply any arbitrary * filesystem path. * - `fs.rm(recursive: true, force: true)` on ANY of those would be * a runtime disaster — at best delete the user's working tree, at * worst nuke an unrelated directory tree they happen to own. * - `clean` (default, cwd-scoped) is safe by construction — it * re-derives storagePath from `findRepo(cwd)` and never trusts * the registry field. But `clean --all` DOES iterate the registry * and trust each entry's stored storagePath (same shape as * `remove`), so this helper must be wired into that loop too. * - `server/api.ts` recomputes storagePath from `getStoragePath(entry.path)` * and so is likewise safe-by-construction. * * Pure string check — does NOT require the paths to exist on disk. * Windows: case-insensitive; POSIX: case-sensitive. Matches the * comparison shape used elsewhere in this module. */ export declare const assertSafeStoragePath: (entry: RegistryEntry) => void; /** * Resolve a user-supplied target string (from `gitnexus remove ` * or equivalent MCP tool argument) to a single registry entry. * * Match precedence (first hit wins, subsequent tiers are only tried if * the prior tier produces zero matches): * 1. Exact resolved-path match (Windows: case-insensitive). * Paths are unique by registry construction, so a path match can * never be ambiguous. * 2. Exact `name` match (case-insensitive). If ≥ 2 entries share the * name — only possible via `--allow-duplicate-name` (#829) — * throws {@link RegistryAmbiguousTargetError}. * * No fuzzy / partial matching — unambiguous, scriptable behaviour is * more important than convenience for destructive commands. * * Throws {@link RegistryNotFoundError} if no entry matches. * * `entries` is passed in (rather than re-read) so callers that already * hold the registry snapshot (e.g. to print a "before" state) can avoid * a second disk read, and so tests can inject fixtures without touching * `GITNEXUS_HOME`. */ export declare const resolveRegistryEntry: (entries: RegistryEntry[], target: string) => RegistryEntry; /** * List all registered repos from the global registry. * * With `validate: true`, prunes only entries whose metadata is *provably* gone * (fs.access on both gitnexus.json and legacy meta.json fails with ENOENT or * ENOTDIR) and persists the result. Entries that are merely "not provably * absent" — any other fs.access failure (EIO/EAGAIN/EBUSY/EACCES, etc.) — are * KEPT, so a transient I/O storm cannot wipe the registry. A kept entry is * therefore "not confirmed present," not "confirmed present"; downstream DB * opens are independently and lazily guarded. */ export declare const listRegisteredRepos: (opts?: { validate?: boolean; }) => Promise; export interface CLIConfig { apiKey?: string; model?: string; baseUrl?: string; provider?: 'openai' | 'openrouter' | 'azure' | 'custom' | 'cursor' | 'claude' | 'codex' | 'opencode'; cursorModel?: string; claudeModel?: string; codexModel?: string; opencodeModel?: string; /** Azure api-version query param (e.g. '2024-10-21'). Only used when provider is 'azure'. */ apiVersion?: string; /** Set true when the deployment is a reasoning model (o1, o3, o4-mini). Auto-detected for OpenAI; must be set for Azure deployments. */ isReasoningModel?: boolean; } /** * Get the path to the global CLI config file */ export declare const getGlobalConfigPath: () => string; /** * Load CLI config from ~/.gitnexus/config.json */ export declare const loadCLIConfig: () => Promise; /** * Save CLI config to ~/.gitnexus/config.json */ export declare const saveCLIConfig: (config: CLIConfig) => Promise; /** * Find other registered entries whose `remoteUrl` matches the given * one, excluding `selfPath` (case-insensitive on Windows). Entries * without a `remoteUrl` are ignored — we cannot prove sibling-ness * without a fingerprint. */ export declare const findSiblingClones: (remoteUrl: string | undefined, selfPath: string) => Promise; /** * Description of how a working directory relates to a registered index. * * `match` semantics: * - `path` — `cwd` is inside the registered entry's path. * - `sibling-by-remote` — `cwd` is in a different on-disk clone of the * same repo (same `remoteUrl`). * - `none` — no relationship found. */ export interface CwdMatch { match: 'path' | 'sibling-by-remote' | 'none'; entry?: RegistryEntry; /** The git toplevel of `cwd`, when `cwd` is inside a git work tree. */ cwdGitRoot?: string; /** HEAD of the cwd's clone, when resolvable. */ cwdHead?: string; /** * Number of commits the registered `lastCommit` is behind the * sibling-clone HEAD, when both refs are known to the cwd's clone. * `undefined` when the comparison cannot be performed (e.g. the * indexed commit isn't reachable from cwd). */ drift?: number; /** Human-readable hint, set whenever the situation warrants warning. */ hint?: string; }