/** * side-git.ts * * SideGitRunner, a hidden git repository ("side repo") whose object store * (GIT_DIR) lives under /.goodvibes/checkpoints/git while its * GIT_WORK_TREE is the live workspace itself. * * This is the dotfiles-bare-repo trick (`git --git-dir=X --work-tree=Y`): * git tracks arbitrary files in Y using an object store rooted at X, * completely independent of whatever real .git directory Y may or may not * already have. It gives us, for free: * - content-addressed, deduped storage (unchanged files cost ~nothing) * - `git diff` / `git diff --stat` between any two snapshots * - whole-tree restore via `read-tree` + `checkout-index` * - correct behavior in a workspace that is NOT itself a git repo * * DO NOT reuse GitService (../../git/service.ts) for this: it binds a single * baseDir with no GIT_DIR support, and it fires Pre/Post hook events on * every commit/add, automatic silent snapshots must never trigger a user's * PreCommit/PostCommit hooks. AgentWorktree (../../agents/worktree.ts) already * proves the `simpleGit(...).raw([...])` + explicit env pattern used here. * * Checkpoints are addressed entirely through our own ref namespace * (refs/goodvibes/checkpoints/) and through commit objects created via * `commit-tree`, never through the side repo's HEAD/branch. There is no * meaningful "current branch" in this design, parent/lineage is tracked in * the manifest (manager.ts), not via git HEAD, so there is nothing to leave * "detached" and no branch state to pollute. */ /** Git's well-known empty-tree object hash, valid in every repository. */ export declare const EMPTY_TREE_HASH = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; /** Ref namespace all workspace checkpoints live under. */ export declare const CHECKPOINT_REF_PREFIX = "refs/goodvibes/checkpoints/"; export interface SideGitRunnerOptions { /** Absolute path to the live workspace (GIT_WORK_TREE). */ readonly workspaceRoot: string; /** Absolute path to the side repo's object store (GIT_DIR). */ readonly gitDir: string; } /** * Resolve the top level of the git repository that ENCLOSES `dir`, or `null` * when `dir` is not inside a git working tree. * * This runs a plain `git rev-parse --show-toplevel` with a sanitized * environment that deliberately does NOT carry any GIT_DIR / GIT_WORK_TREE * override, so it discovers the user's real repository (walking up from * `dir`), never a side/checkpoint repo. Any failure, not a repo, git * missing, permission error, is swallowed and reported as `null`, since the * only caller uses this as a best-effort preference, not a hard requirement. */ export declare function detectGitToplevel(dir: string): Promise; /** * Thin runner around a `simple-git` instance permanently scoped (via `.env()`) * to an isolated GIT_DIR/GIT_WORK_TREE pair. Every method here is a small, * named wrapper around a raw git invocation, no hook emission, no shared * state with the user's real repository. */ export declare class SideGitRunner { readonly workspaceRoot: string; readonly gitDir: string; /** * The scoped `simple-git` instance, built on first use rather than in the * constructor. `simple-git` is an optionalDependency; a static import here * put its specifier on the module graph of every graph that reaches * checkpoints, the daemon's included, so an absent optional package removed * the process rather than the feature (see utils/optional-dependency.ts). * The promise is memoised, so the `.env()` scoping is still applied exactly * once per runner, and every method here was already async. */ private gitClient; /** * The environment the client is scoped to, snapshotted at construction, * the same moment `sanitizeGitEnv(process.env)` ran when the client itself * was built here, so deferring the construction does not defer the reading * of `process.env`. */ private readonly gitEnv; constructor(opts: SideGitRunnerOptions); /** * This runner's git client, permanently scoped to its isolated * GIT_DIR/GIT_WORK_TREE pair. When `simple-git` is absent the await throws * an error naming it, which surfaces on the same path as any other * checkpoint git failure. */ private git; /** Run an arbitrary raw git command against the side repo, returning stdout. */ raw(args: string[]): Promise; /** * Idempotently initialize the side repo: `git init` the GIT_DIR if it does * not already look initialized, set a local (side-repo-only) fallback * identity so commits never depend on the user's global git config, and * ensure `.goodvibes` is gitignored from the user's own repo's perspective. */ init(): Promise; /** * Ensure `/.goodvibes/.gitignore` contains a bare `*` line so * the side repo's own storage (and every other tool's state under * `.goodvibes`) is invisible to the user's OWN git repo (if any). Without * this, `git status`/`git add -A` in the user's real repo would see our * GIT_DIR as an untracked directory and could accidentally stage it. * * This intentionally only ever writes inside `.goodvibes/`, it never * touches the workspace's own top-level `.gitignore`. */ private ensureGoodvibesIgnored; /** * Stage changes into the side index. * * @param paths When provided (non-empty), stage only these pathspecs * (scoped snapshot). Otherwise sweep the whole work tree with a plain `.` * pathspec. `.goodvibes` (our own storage) is kept out of that sweep by * `.goodvibes/.gitignore`'s own `*` self-ignore line, written by `init()` * (via `ensureGoodvibesIgnored()`) before this method can ever run, NOT by * naming `.goodvibes` explicitly here. An earlier version of this method * did pass an explicit `:(exclude).goodvibes` / `:(exclude).goodvibes/**` * pathspec (mirroring the pattern AgentWorktree uses for the same reason), * but that breaks the moment the WORKSPACE's own top-level `.gitignore` * also happens to contain a `.goodvibes/` rule (exactly what this project's * own TUI writes at startup): git aborts the entire `add -A` with "The * following paths are ignored by one of your .gitignore files: .goodvibes * ... Use -f if you really want to add them," because naming an * already-ignored path in ANY pathspec, exclude magic or not, triggers * that check. A path git discovers and skips implicitly while walking a * wildcard `.` sweep never hits that check. Beyond `.goodvibes`, git's * normal `.gitignore` handling already applies here: `.gitignore` matching * is a work-tree-relative feature of git and works identically regardless * of where GIT_DIR points, so the workspace's own `.gitignore` * (node_modules, build output, etc.) is honored with no extra * configuration. */ stageAll(paths?: string[]): Promise; /** * Count how many files a full first-snapshot sweep (`git add -A -- .`) would * capture, WITHOUT writing a single blob into the object store. * * Uses `git ls-files --others --exclude-standard` (untracked files, honoring * every `.gitignore` including `.goodvibes/.gitignore`'s own `*` self-ignore). * This is exact for the FIRST snapshot, when the side index is empty and so * every file the sweep would stage is an "other" (untracked) file. Filenames * are NUL-delimited (`-z`) so newlines in names never inflate the count. */ countFirstSnapshotFiles(): Promise; /** Write the currently-staged index out as a tree object, without committing. Returns the tree hash. */ writeTree(): Promise; /** Resolve `^{tree}` for an existing commit hash. */ treeOf(commit: string): Promise; /** * Create a commit object from a tree, deliberately WITHOUT a git parent * (no `-p`) and WITHOUT moving any branch/HEAD. * * Checkpoint lineage is tracked exclusively via the manifest's `parentId` * field (manager.ts), never via git ancestry. This isn't just a style * choice: if checkpoint commits chained via `-p` the way a normal git * history does, deleting an OLD checkpoint's ref in `gc()` would free * nothing, because that commit stays reachable through every NEWER * checkpoint's parent pointer, the ref is gone but the commit (and any * tree/blob objects unique to it) is still walkable from every surviving * descendant. Parentless commits mean a checkpoint's own ref is the ONLY * thing keeping its commit reachable, so once that ref is deleted, * `git gc --prune=now` can genuinely reclaim it. * * The returned hash is only reachable once a ref is pointed at it via * `updateRef`. */ commitTree(treeHash: string, message: string): Promise; updateRef(refName: string, commit: string): Promise; deleteRef(refName: string): Promise; /** List every ref under CHECKPOINT_REF_PREFIX as `{ id, commit }`. */ listCheckpointRefs(): Promise<{ id: string; commit: string; }[]>; /** Files tracked in a commit's tree (recursive, name-only). */ listTrackedFiles(commitOrTree: string): Promise; /** Reset the side index to exactly match a commit's tree (does not touch the working tree). */ readTreeReset(commit: string): Promise; /** Write every file currently in the side index out to the working tree, overwriting existing files. */ checkoutIndexAll(): Promise; /** `git diff` between two commit-ish values. Omit `to` to diff against the live working tree. */ diff(from: string, to?: string): Promise; /** `git diff --stat` between two commit-ish values. Omit `to` to diff against the live working tree. */ diffStat(from: string, to?: string): Promise; /** `git diff --name-only` between two commit-ish values. Omit `to` to diff against the live working tree. */ diffNameOnly(from: string, to?: string): Promise; /** `git gc --prune=now` on the side repo. */ gc(): Promise; } //# sourceMappingURL=side-git.d.ts.map