git-stint 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,268 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import * as session from "./session.js";
5
+ import { checkConflicts } from "./conflicts.js";
6
+ import { test, testCombine } from "./test-session.js";
7
+ const args = process.argv.slice(2);
8
+ const command = args[0];
9
+ // Known flags that take a value (used by arg parser to skip correctly)
10
+ const VALUE_FLAGS = new Set(["-m", "--session", "--title", "--combine"]);
11
+ function getFlag(flag) {
12
+ // Check for --flag=value syntax
13
+ const eqPrefix = flag + "=";
14
+ for (const arg of args) {
15
+ if (arg.startsWith(eqPrefix)) {
16
+ return arg.slice(eqPrefix.length);
17
+ }
18
+ }
19
+ // Check for --flag value syntax
20
+ const idx = args.indexOf(flag);
21
+ if (idx === -1)
22
+ return undefined;
23
+ const value = args[idx + 1];
24
+ if (value === undefined) {
25
+ throw new Error(`Flag '${flag}' requires a value.`);
26
+ }
27
+ return value;
28
+ }
29
+ /** Check if an arg is a value flag, handling both --flag and --flag=value forms. */
30
+ function isValueFlag(arg) {
31
+ if (VALUE_FLAGS.has(arg))
32
+ return true;
33
+ // --flag=value: the flag consumed its value inline, don't skip next arg
34
+ for (const f of VALUE_FLAGS) {
35
+ if (arg.startsWith(f + "="))
36
+ return false; // value is inline, no skip
37
+ }
38
+ return false;
39
+ }
40
+ function getPositional(index) {
41
+ let pos = 0;
42
+ for (let i = 1; i < args.length; i++) {
43
+ if (args[i].startsWith("-")) {
44
+ // Only skip next arg if this flag takes a separate value (not --flag=value)
45
+ if (isValueFlag(args[i]))
46
+ i++;
47
+ continue;
48
+ }
49
+ if (pos === index)
50
+ return args[i];
51
+ pos++;
52
+ }
53
+ return undefined;
54
+ }
55
+ function getAllPositional() {
56
+ const result = [];
57
+ for (let i = 1; i < args.length; i++) {
58
+ if (args[i].startsWith("-")) {
59
+ if (isValueFlag(args[i]))
60
+ i++;
61
+ continue;
62
+ }
63
+ result.push(args[i]);
64
+ }
65
+ return result;
66
+ }
67
+ try {
68
+ switch (command) {
69
+ case "start": {
70
+ const name = getPositional(0);
71
+ session.start(name);
72
+ break;
73
+ }
74
+ case "track": {
75
+ const files = getAllPositional();
76
+ if (files.length === 0) {
77
+ console.error("Usage: git stint track <file...>");
78
+ process.exit(1);
79
+ }
80
+ const sessionFlag = getFlag("--session");
81
+ session.track(files, sessionFlag);
82
+ break;
83
+ }
84
+ case "status": {
85
+ session.status(getFlag("--session"));
86
+ break;
87
+ }
88
+ case "diff": {
89
+ session.diff(getFlag("--session"));
90
+ break;
91
+ }
92
+ case "commit": {
93
+ const message = getFlag("-m");
94
+ if (!message) {
95
+ console.error("Usage: git stint commit -m \"message\"");
96
+ process.exit(1);
97
+ }
98
+ session.sessionCommit(message, getFlag("--session"));
99
+ break;
100
+ }
101
+ case "log": {
102
+ session.log(getFlag("--session"));
103
+ break;
104
+ }
105
+ case "squash": {
106
+ const message = getFlag("-m");
107
+ if (!message) {
108
+ console.error("Usage: git stint squash -m \"message\"");
109
+ process.exit(1);
110
+ }
111
+ session.squash(message, getFlag("--session"));
112
+ break;
113
+ }
114
+ case "merge": {
115
+ session.merge(getFlag("--session"));
116
+ break;
117
+ }
118
+ case "pr": {
119
+ const title = getFlag("--title");
120
+ session.pr(title, getFlag("--session"));
121
+ break;
122
+ }
123
+ case "end": {
124
+ session.end(getFlag("--session"));
125
+ break;
126
+ }
127
+ case "abort": {
128
+ session.abort(getFlag("--session"));
129
+ break;
130
+ }
131
+ case "undo": {
132
+ session.undo(getFlag("--session"));
133
+ break;
134
+ }
135
+ case "conflicts": {
136
+ checkConflicts(getFlag("--session"));
137
+ break;
138
+ }
139
+ case "test": {
140
+ const combineNames = getFlag("--combine");
141
+ if (combineNames) {
142
+ // Collect all names after --combine
143
+ const idx = args.indexOf("--combine");
144
+ const names = [];
145
+ for (let i = idx + 1; i < args.length; i++) {
146
+ if (args[i].startsWith("-"))
147
+ break;
148
+ names.push(args[i]);
149
+ }
150
+ if (names.length < 2) {
151
+ console.error("Usage: git stint test --combine <session1> <session2> [...]");
152
+ process.exit(1);
153
+ }
154
+ const dashIdx = args.indexOf("--");
155
+ const testCmd = dashIdx >= 0 ? args.slice(dashIdx + 1).join(" ") : undefined;
156
+ testCombine(names, testCmd);
157
+ }
158
+ else {
159
+ const dashIdx = args.indexOf("--");
160
+ const testCmd = dashIdx >= 0 ? args.slice(dashIdx + 1).join(" ") : undefined;
161
+ test(getFlag("--session"), testCmd);
162
+ }
163
+ break;
164
+ }
165
+ case "list": {
166
+ if (args.includes("--json")) {
167
+ session.listJson();
168
+ }
169
+ else {
170
+ session.list();
171
+ }
172
+ break;
173
+ }
174
+ case "prune": {
175
+ session.prune();
176
+ break;
177
+ }
178
+ case "install-hooks": {
179
+ const { install } = await import("./install-hooks.js");
180
+ const scope = args.includes("--user") ? "user" : "project";
181
+ install(scope);
182
+ break;
183
+ }
184
+ case "uninstall-hooks": {
185
+ const { uninstall } = await import("./install-hooks.js");
186
+ const scope = args.includes("--user") ? "user" : "project";
187
+ uninstall(scope);
188
+ break;
189
+ }
190
+ case "version":
191
+ case "--version":
192
+ case "-v": {
193
+ printVersion();
194
+ break;
195
+ }
196
+ case "help":
197
+ case "--help":
198
+ case "-h":
199
+ case undefined: {
200
+ printHelp();
201
+ break;
202
+ }
203
+ default: {
204
+ console.error(`Unknown command: ${command}`);
205
+ console.error("Run `git stint help` for usage.");
206
+ process.exit(1);
207
+ }
208
+ }
209
+ }
210
+ catch (err) {
211
+ const message = err instanceof Error ? err.message : String(err);
212
+ console.error(`Error: ${message}`);
213
+ process.exit(1);
214
+ }
215
+ function getVersion() {
216
+ try {
217
+ const thisDir = dirname(fileURLToPath(import.meta.url));
218
+ const pkgPath = join(thisDir, "..", "package.json");
219
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
220
+ return pkg.version || "unknown";
221
+ }
222
+ catch {
223
+ return "unknown";
224
+ }
225
+ }
226
+ function printVersion() {
227
+ console.log(`git-stint ${getVersion()}`);
228
+ }
229
+ function printHelp() {
230
+ console.log(`git-stint ${getVersion()} — Session-scoped change tracking for AI coding agents
231
+
232
+ Usage: git stint <command> [options]
233
+
234
+ Commands:
235
+ start [name] Create a new session (branch + worktree)
236
+ list List all active sessions
237
+ status Show current session state
238
+ track <file...> Add files to the pending list
239
+ diff Show uncommitted changes
240
+ commit -m "msg" Commit changes, advance baseline
241
+ log Show session commit history
242
+ squash -m "msg" Collapse all commits into one
243
+ merge Merge session into main (no PR)
244
+ pr [--title "..."] Push branch and create GitHub PR
245
+ end Finalize session, clean up everything
246
+ abort Discard session — delete all changes
247
+ undo Revert last commit, changes become pending
248
+ conflicts Check file overlap with other sessions
249
+ test [-- <cmd>] Run tests in the session worktree
250
+ test --combine A B Test multiple sessions merged together
251
+ prune Clean up orphaned worktrees/branches
252
+ install-hooks [--user] Install Claude Code hooks
253
+ uninstall-hooks [--user] Remove Claude Code hooks
254
+
255
+ Options:
256
+ --session <name> Specify session (auto-detected from CWD)
257
+ -m "message" Commit/squash message
258
+ --title "title" PR title
259
+ --version Show version number
260
+
261
+ Examples:
262
+ git stint start auth-fix
263
+ cd .stint/auth-fix/
264
+ # make changes...
265
+ git stint commit -m "Fix auth token refresh"
266
+ git stint pr --title "Fix auth bug"
267
+ git stint end`);
268
+ }
@@ -0,0 +1 @@
1
+ export declare function checkConflicts(sessionName?: string): void;
@@ -0,0 +1,49 @@
1
+ import * as git from "./git.js";
2
+ import { listManifests, resolveSession, getWorktreePath } from "./manifest.js";
3
+ export function checkConflicts(sessionName) {
4
+ const current = resolveSession(sessionName);
5
+ const others = listManifests().filter((m) => m.name !== current.name);
6
+ if (others.length === 0) {
7
+ console.log("No other active sessions.");
8
+ return;
9
+ }
10
+ // Get current session's changed files
11
+ const currentFiles = new Set();
12
+ for (const cs of current.changesets) {
13
+ cs.files.forEach((f) => currentFiles.add(f));
14
+ }
15
+ current.pending.forEach((f) => currentFiles.add(f));
16
+ // Also check uncommitted changes in worktree
17
+ const worktree = getWorktreePath(current);
18
+ try {
19
+ const uncommitted = git.gitInDir(worktree, "diff", "--name-only");
20
+ if (uncommitted) {
21
+ uncommitted.split("\n").forEach((f) => currentFiles.add(f));
22
+ }
23
+ const staged = git.gitInDir(worktree, "diff", "--cached", "--name-only");
24
+ if (staged) {
25
+ staged.split("\n").forEach((f) => currentFiles.add(f));
26
+ }
27
+ }
28
+ catch { /* worktree may not be accessible */ }
29
+ let hasConflicts = false;
30
+ for (const other of others) {
31
+ const otherFiles = new Set();
32
+ for (const cs of other.changesets) {
33
+ cs.files.forEach((f) => otherFiles.add(f));
34
+ }
35
+ other.pending.forEach((f) => otherFiles.add(f));
36
+ const overlap = [...currentFiles].filter((f) => otherFiles.has(f));
37
+ if (overlap.length > 0) {
38
+ hasConflicts = true;
39
+ console.log(`Overlap with session '${other.name}':`);
40
+ for (const f of overlap) {
41
+ console.log(` ${f}`);
42
+ }
43
+ console.log();
44
+ }
45
+ }
46
+ if (!hasConflicts) {
47
+ console.log("No file overlaps with other sessions.");
48
+ }
49
+ }
package/dist/git.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ export declare function git(...args: string[]): string;
2
+ export declare function gitInDir(dir: string, ...args: string[]): string;
3
+ export declare function getHead(dir?: string): string;
4
+ export declare function getGitDir(): string;
5
+ /** Returns the main .git dir (shared across worktrees). */
6
+ export declare function getGitCommonDir(): string;
7
+ export declare function getTopLevel(): string;
8
+ export declare function currentBranch(dir?: string): string;
9
+ /**
10
+ * Detect the default branch (main/master/etc) by checking the remote HEAD ref.
11
+ * Falls back to "main" if detection fails.
12
+ */
13
+ export declare function getDefaultBranch(): string;
14
+ export declare function branchExists(name: string): boolean;
15
+ export declare function createBranch(name: string, from: string): void;
16
+ export declare function deleteBranch(name: string): void;
17
+ /**
18
+ * Check if a remote-tracking ref exists locally (no network call).
19
+ * Uses `git branch -r --list` which reads the local ref cache.
20
+ */
21
+ export declare function remoteBranchExists(name: string): boolean;
22
+ export declare function deleteRemoteBranch(name: string): void;
23
+ export declare function addWorktree(path: string, branch: string): void;
24
+ /** Create a worktree in detached HEAD mode at a given ref. */
25
+ export declare function addWorktreeDetached(path: string, ref: string): void;
26
+ export declare function removeWorktree(path: string, force?: boolean): void;
27
+ export declare function diffNameOnly(base: string, head: string, dir?: string): string[];
28
+ export declare function diffStat(base: string, head: string): string;
29
+ export declare function logOneline(base: string, head: string): string;
30
+ export declare function statusShort(dir: string): string;
31
+ export declare function hasUncommittedChanges(dir: string): boolean;
32
+ export declare function addAll(dir: string): void;
33
+ export declare function commit(dir: string, message: string): string;
34
+ export declare function resetSoft(dir: string, to: string): void;
35
+ /** Reset HEAD to a specific target, keeping changes as unstaged. */
36
+ export declare function resetMixed(dir: string, to: string): void;
37
+ export declare function mergeInto(targetDir: string, ...branches: string[]): void;
38
+ export declare function push(branch: string): void;
39
+ export declare function isInsideGitRepo(): boolean;
40
+ export declare function hasCommits(): boolean;
package/dist/git.js ADDED
@@ -0,0 +1,149 @@
1
+ import { execFileSync } from "node:child_process";
2
+ export function git(...args) {
3
+ try {
4
+ return execFileSync("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
5
+ }
6
+ catch (err) {
7
+ const e = err;
8
+ const stderr = e.stderr?.trim() || e.message || "unknown error";
9
+ throw new Error(`git ${args[0]} failed: ${stderr}`);
10
+ }
11
+ }
12
+ export function gitInDir(dir, ...args) {
13
+ return git("-C", dir, ...args);
14
+ }
15
+ export function getHead(dir) {
16
+ return dir ? gitInDir(dir, "rev-parse", "HEAD") : git("rev-parse", "HEAD");
17
+ }
18
+ export function getGitDir() {
19
+ return git("rev-parse", "--git-dir");
20
+ }
21
+ /** Returns the main .git dir (shared across worktrees). */
22
+ export function getGitCommonDir() {
23
+ return git("rev-parse", "--git-common-dir");
24
+ }
25
+ export function getTopLevel() {
26
+ return git("rev-parse", "--show-toplevel");
27
+ }
28
+ export function currentBranch(dir) {
29
+ const args = ["rev-parse", "--abbrev-ref", "HEAD"];
30
+ return dir ? gitInDir(dir, ...args) : git(...args);
31
+ }
32
+ /**
33
+ * Detect the default branch (main/master/etc) by checking the remote HEAD ref.
34
+ * Falls back to "main" if detection fails.
35
+ */
36
+ export function getDefaultBranch() {
37
+ try {
38
+ const ref = git("symbolic-ref", "refs/remotes/origin/HEAD");
39
+ return ref.replace("refs/remotes/origin/", "");
40
+ }
41
+ catch {
42
+ // No remote HEAD — check if 'main' or 'master' exists locally
43
+ if (branchExists("main"))
44
+ return "main";
45
+ if (branchExists("master"))
46
+ return "master";
47
+ return "main";
48
+ }
49
+ }
50
+ export function branchExists(name) {
51
+ try {
52
+ git("show-ref", "--verify", "--quiet", `refs/heads/${name}`);
53
+ return true;
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ export function createBranch(name, from) {
60
+ git("branch", name, from);
61
+ }
62
+ export function deleteBranch(name) {
63
+ git("branch", "-D", name);
64
+ }
65
+ /**
66
+ * Check if a remote-tracking ref exists locally (no network call).
67
+ * Uses `git branch -r --list` which reads the local ref cache.
68
+ */
69
+ export function remoteBranchExists(name) {
70
+ try {
71
+ const output = git("branch", "-r", "--list", `origin/${name}`);
72
+ return output.trim().length > 0;
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ }
78
+ export function deleteRemoteBranch(name) {
79
+ git("push", "origin", "--delete", name);
80
+ }
81
+ export function addWorktree(path, branch) {
82
+ git("worktree", "add", path, branch);
83
+ }
84
+ /** Create a worktree in detached HEAD mode at a given ref. */
85
+ export function addWorktreeDetached(path, ref) {
86
+ git("worktree", "add", "--detach", path, ref);
87
+ }
88
+ export function removeWorktree(path, force = false) {
89
+ const args = force
90
+ ? ["worktree", "remove", "--force", path]
91
+ : ["worktree", "remove", path];
92
+ git(...args);
93
+ }
94
+ export function diffNameOnly(base, head, dir) {
95
+ const args = ["diff", "--name-only", `${base}..${head}`];
96
+ const output = dir ? gitInDir(dir, ...args) : git(...args);
97
+ return output ? output.split("\n") : [];
98
+ }
99
+ export function diffStat(base, head) {
100
+ return git("diff", "--stat", `${base}..${head}`);
101
+ }
102
+ export function logOneline(base, head) {
103
+ return git("log", "--oneline", `${base}..${head}`);
104
+ }
105
+ export function statusShort(dir) {
106
+ return gitInDir(dir, "status", "--short");
107
+ }
108
+ export function hasUncommittedChanges(dir) {
109
+ const status = statusShort(dir);
110
+ return status.length > 0;
111
+ }
112
+ export function addAll(dir) {
113
+ gitInDir(dir, "add", "-A");
114
+ }
115
+ export function commit(dir, message) {
116
+ gitInDir(dir, "commit", "-m", message);
117
+ return gitInDir(dir, "rev-parse", "HEAD");
118
+ }
119
+ export function resetSoft(dir, to) {
120
+ gitInDir(dir, "reset", "--soft", to);
121
+ }
122
+ /** Reset HEAD to a specific target, keeping changes as unstaged. */
123
+ export function resetMixed(dir, to) {
124
+ gitInDir(dir, "reset", to);
125
+ }
126
+ export function mergeInto(targetDir, ...branches) {
127
+ gitInDir(targetDir, "merge", ...branches);
128
+ }
129
+ export function push(branch) {
130
+ git("push", "-u", "origin", branch);
131
+ }
132
+ export function isInsideGitRepo() {
133
+ try {
134
+ git("rev-parse", "--is-inside-work-tree");
135
+ return true;
136
+ }
137
+ catch {
138
+ return false;
139
+ }
140
+ }
141
+ export function hasCommits() {
142
+ try {
143
+ git("rev-parse", "HEAD");
144
+ return true;
145
+ }
146
+ catch {
147
+ return false;
148
+ }
149
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Installs git-stint hooks into Claude Code's settings.
3
+ *
4
+ * Hooks installed:
5
+ * PreToolUse (Write/Edit): Track files written in session worktrees.
6
+ * Stop: Commit pending changes as WIP checkpoint.
7
+ */
8
+ export declare function install(scope: "project" | "user"): void;
9
+ export declare function uninstall(scope: "project" | "user"): void;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Installs git-stint hooks into Claude Code's settings.
3
+ *
4
+ * Hooks installed:
5
+ * PreToolUse (Write/Edit): Track files written in session worktrees.
6
+ * Stop: Commit pending changes as WIP checkpoint.
7
+ */
8
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
9
+ import { join, resolve } from "node:path";
10
+ const HOOKS = {
11
+ hooks: {
12
+ PreToolUse: [
13
+ {
14
+ matcher: "Write|Edit|NotebookEdit",
15
+ command: "git-stint-hook-pre-tool",
16
+ },
17
+ ],
18
+ Stop: [
19
+ {
20
+ command: "git-stint-hook-stop",
21
+ },
22
+ ],
23
+ },
24
+ };
25
+ function getSettingsPath(scope) {
26
+ if (scope === "user") {
27
+ const home = process.env.HOME || process.env.USERPROFILE || "~";
28
+ return join(home, ".claude", "settings.json");
29
+ }
30
+ // Project scope
31
+ return join(process.cwd(), ".claude", "settings.json");
32
+ }
33
+ export function install(scope) {
34
+ const settingsPath = getSettingsPath(scope);
35
+ const dir = resolve(settingsPath, "..");
36
+ // Read existing settings or start fresh
37
+ let settings = {};
38
+ if (existsSync(settingsPath)) {
39
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
40
+ }
41
+ // Merge hooks (don't overwrite existing hooks)
42
+ const hooks = (settings.hooks || {});
43
+ for (const [event, hookList] of Object.entries(HOOKS.hooks)) {
44
+ if (!hooks[event]) {
45
+ hooks[event] = [];
46
+ }
47
+ for (const hook of hookList) {
48
+ const exists = hooks[event].some((h) => h.command === hook.command);
49
+ if (!exists) {
50
+ hooks[event].push(hook);
51
+ }
52
+ }
53
+ }
54
+ settings.hooks = hooks;
55
+ if (!existsSync(dir)) {
56
+ mkdirSync(dir, { recursive: true });
57
+ }
58
+ // Atomic write: temp file + rename to prevent corruption if interrupted
59
+ const tmp = settingsPath + ".tmp";
60
+ writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
61
+ renameSync(tmp, settingsPath);
62
+ console.log(`Hooks installed to ${settingsPath}`);
63
+ console.log("\nHooks added:");
64
+ console.log(" PreToolUse (Write/Edit): track files in session worktrees");
65
+ console.log(" Stop: commit pending changes as WIP checkpoint");
66
+ }
67
+ /** Known hook commands installed by git-stint. */
68
+ const STINT_COMMANDS = new Set(Object.values(HOOKS.hooks).flat().map((h) => h.command));
69
+ export function uninstall(scope) {
70
+ const settingsPath = getSettingsPath(scope);
71
+ if (!existsSync(settingsPath)) {
72
+ console.log("No settings file found. Nothing to uninstall.");
73
+ return;
74
+ }
75
+ let settings;
76
+ try {
77
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
78
+ }
79
+ catch {
80
+ console.error(`Failed to parse ${settingsPath}. Fix it manually.`);
81
+ return;
82
+ }
83
+ const hooks = settings.hooks;
84
+ if (!hooks) {
85
+ console.log("No hooks configured. Nothing to uninstall.");
86
+ return;
87
+ }
88
+ let removed = 0;
89
+ for (const event of Object.keys(hooks)) {
90
+ const before = hooks[event].length;
91
+ hooks[event] = hooks[event].filter((h) => !STINT_COMMANDS.has(h.command));
92
+ removed += before - hooks[event].length;
93
+ // Remove empty arrays to keep settings clean
94
+ if (hooks[event].length === 0) {
95
+ delete hooks[event];
96
+ }
97
+ }
98
+ // Remove empty hooks object
99
+ if (Object.keys(hooks).length === 0) {
100
+ delete settings.hooks;
101
+ }
102
+ if (removed === 0) {
103
+ console.log("No git-stint hooks found. Nothing to uninstall.");
104
+ return;
105
+ }
106
+ // Atomic write
107
+ const tmp = settingsPath + ".tmp";
108
+ writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
109
+ renameSync(tmp, settingsPath);
110
+ console.log(`Removed ${removed} git-stint hook(s) from ${settingsPath}`);
111
+ }
@@ -0,0 +1,59 @@
1
+ export interface Changeset {
2
+ id: number;
3
+ sha: string;
4
+ message: string;
5
+ files: string[];
6
+ /** ISO 8601 timestamp */
7
+ timestamp: string;
8
+ }
9
+ export interface SessionManifest {
10
+ /** Schema version for forward compatibility. Current: 1. */
11
+ version: number;
12
+ name: string;
13
+ /** HEAD sha when session was created (never changes). */
14
+ startedAt: string;
15
+ /** Advances on each commit. Used to compute diffs for the next changeset. */
16
+ baseline: string;
17
+ /** Git branch name, e.g. "stint/my-feature" */
18
+ branch: string;
19
+ /** Worktree path relative to repo root, e.g. ".stint/my-feature" */
20
+ worktree: string;
21
+ changesets: Changeset[];
22
+ /** Files tracked since last commit. */
23
+ pending: string[];
24
+ }
25
+ declare const MANIFEST_VERSION = 1;
26
+ declare const BRANCH_PREFIX = "stint/";
27
+ declare const WORKTREE_DIR = ".stint";
28
+ export { BRANCH_PREFIX, WORKTREE_DIR, MANIFEST_VERSION };
29
+ export declare function getSessionsDir(): string;
30
+ export declare function loadManifest(name: string): SessionManifest | null;
31
+ /**
32
+ * Atomic write: write to temp file, then rename.
33
+ * Prevents corruption if the process is killed mid-write.
34
+ */
35
+ export declare function saveManifest(manifest: SessionManifest): void;
36
+ export declare function listManifests(): SessionManifest[];
37
+ export declare function deleteManifest(name: string): void;
38
+ /**
39
+ * Resolve which session is active.
40
+ * Priority:
41
+ * 1. Explicit name passed via --session flag
42
+ * 2. CWD is inside a .stint/<name>/ worktree
43
+ * 3. Only one session exists → use it
44
+ * 4. Error
45
+ */
46
+ export declare function resolveSession(explicit?: string): SessionManifest;
47
+ /**
48
+ * Get the repo root (main worktree root, not a stint worktree).
49
+ * Uses git's --show-toplevel from the main worktree context.
50
+ */
51
+ export declare function getRepoRoot(): string;
52
+ /**
53
+ * Get the absolute worktree path for a session.
54
+ */
55
+ export declare function getWorktreePath(manifest: SessionManifest): string;
56
+ /**
57
+ * Check if any sessions exist. Cheaper than loading all manifests.
58
+ */
59
+ export declare function hasAnySessions(): boolean;