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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rahul Chandrasekaran
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,238 @@
1
+ # git-stint
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
+ [![Node.js: 20+](https://img.shields.io/badge/Node.js-20%2B-green.svg)](https://nodejs.org)
5
+
6
+ Session-scoped change tracking for AI coding agents. Each session gets a real git branch and worktree — isolated by default, mergeable at the end.
7
+
8
+ Built to replace GitButler for AI-agent workflows. No virtual branches, no custom merge engine, no state corruption. Just git.
9
+
10
+ ## Why
11
+
12
+ AI coding agents (Claude Code, Cursor, Copilot) edit files but have no clean way to:
13
+
14
+ 1. **Track what they changed** — separate agent changes from human changes
15
+ 2. **Isolate sessions** — two parallel agents editing the same repo shouldn't conflict
16
+ 3. **Produce clean commits** — agent work should result in reviewable, mergeable PRs
17
+ 4. **Test in isolation** — verify one session's changes without interference
18
+
19
+ git-stint solves this with ~600 lines of TypeScript on top of standard git primitives.
20
+
21
+ ## Prerequisites
22
+
23
+ - [Node.js](https://nodejs.org) 20+
24
+ - [git](https://git-scm.com) 2.20+ (worktree support)
25
+ - [`gh` CLI](https://cli.github.com) (optional, for PR creation)
26
+
27
+ ## Quick Start
28
+
29
+ ```bash
30
+ npm install -g git-stint
31
+
32
+ # Start a session (creates branch + worktree)
33
+ git stint start auth-fix
34
+ cd .stint/auth-fix/
35
+
36
+ # Work normally — make changes, edit files...
37
+
38
+ # Commit progress (advances baseline)
39
+ git stint commit -m "Fix token refresh logic"
40
+
41
+ # More changes...
42
+ git stint commit -m "Add refresh token tests"
43
+
44
+ # Squash into a single clean commit
45
+ git stint squash -m "Fix auth token refresh"
46
+
47
+ # Create PR and clean up
48
+ git stint pr --title "Fix auth bug"
49
+ git stint end
50
+ ```
51
+
52
+ ## Commands
53
+
54
+ | Command | Description |
55
+ |---------|-------------|
56
+ | `git stint start [name]` | Create a new session (branch + worktree) |
57
+ | `git stint list` | List all active sessions |
58
+ | `git stint status` | Show current session state |
59
+ | `git stint track <file...>` | Add files to the pending list |
60
+ | `git stint diff` | Show uncommitted changes in worktree |
61
+ | `git stint commit -m "msg"` | Commit changes, advance baseline |
62
+ | `git stint log` | Show session commit history |
63
+ | `git stint squash -m "msg"` | Collapse all commits into one |
64
+ | `git stint merge` | Merge session into current branch (no PR) |
65
+ | `git stint pr [--title "..."]` | Push branch and create GitHub PR |
66
+ | `git stint end` | Finalize session, clean up everything |
67
+ | `git stint abort` | Discard session — delete all changes |
68
+ | `git stint undo` | Revert last commit, changes become pending |
69
+ | `git stint conflicts` | Check file overlap with other sessions |
70
+ | `git stint test [-- cmd]` | Run tests in the session worktree |
71
+ | `git stint test --combine A B` | Test multiple sessions merged together |
72
+ | `git stint prune` | Clean up orphaned worktrees/branches |
73
+ | `git stint install-hooks` | Install Claude Code hooks |
74
+ | `git stint uninstall-hooks` | Remove Claude Code hooks |
75
+
76
+ ### Options
77
+
78
+ - `--session <name>` — Specify which session (auto-detected from CWD)
79
+ - `-m "message"` — Commit or squash message
80
+ - `--title "title"` — PR title
81
+ - `--version` — Show version number
82
+
83
+ ## Claude Code Integration
84
+
85
+ git-stint includes hooks that make it work seamlessly with Claude Code:
86
+
87
+ - **PreToolUse hook**: When Claude writes/edits a file inside a session worktree, the file is automatically tracked. If Claude tries to write to the main repo while a session is active, the hook blocks the write and redirects to the worktree.
88
+ - **Stop hook**: When a Claude Code conversation ends, pending changes are auto-committed as a WIP checkpoint.
89
+
90
+ ### Install Hooks
91
+
92
+ ```bash
93
+ # Install to project settings (.claude/settings.json)
94
+ git stint install-hooks
95
+
96
+ # Or install to user settings (~/.claude/settings.json)
97
+ git stint install-hooks --user
98
+
99
+ # To remove hooks later
100
+ git stint uninstall-hooks
101
+ ```
102
+
103
+ ### Workflow with Claude Code
104
+
105
+ ```bash
106
+ # Start a session before asking Claude to work
107
+ git stint start my-feature
108
+ cd .stint/my-feature/
109
+
110
+ # Tell Claude to work on things — hooks handle tracking automatically
111
+
112
+ # When done, squash and PR
113
+ git stint squash -m "Implement feature X"
114
+ git stint pr
115
+ git stint end
116
+ ```
117
+
118
+ ## How It Works
119
+
120
+ ### Session Model
121
+
122
+ Each session creates:
123
+ - A **git branch** (`stint/<name>`) forked from HEAD
124
+ - A **worktree** (`.stint/<name>/`) for isolated file access
125
+ - A **manifest** (`.git/sessions/<name>.json`) tracking state
126
+
127
+ ```
128
+ Session starts at HEAD = abc123
129
+
130
+ Edit config.ts, server.ts
131
+
132
+ "commit" → changeset 1 (baseline advances to new SHA)
133
+
134
+ Edit server.ts (again), test.ts
135
+
136
+ "commit" → changeset 2 (only NEW changes since last commit)
137
+ ```
138
+
139
+ The **baseline cursor** advances on each commit. `git diff baseline..HEAD` always gives exactly the uncommitted work. No virtual branches, no custom merge engine.
140
+
141
+ ### Parallel Sessions
142
+
143
+ Multiple sessions run simultaneously with full isolation:
144
+
145
+ ```
146
+ Session A: edits config.ts, server.ts → .stint/session-a/
147
+ Session B: edits server.ts, constants.ts → .stint/session-b/
148
+ ↑ overlap detected by `git stint conflicts`
149
+ ```
150
+
151
+ Each session has its own worktree — no interference. Conflicts resolve at PR merge time, using git's standard merge machinery.
152
+
153
+ ### Testing
154
+
155
+ ```bash
156
+ # Test a single session in its worktree
157
+ git stint test -- npm test
158
+
159
+ # Test multiple sessions merged together
160
+ git stint test --combine auth-fix perf-update -- npm test
161
+ ```
162
+
163
+ Combined testing creates a temporary octopus merge of the specified sessions, runs the test command, then cleans up. No permanent state changes.
164
+
165
+ ## Architecture
166
+
167
+ ```
168
+ ┌─────────────┐ ┌──────────────┐ ┌────────────────┐
169
+ │ CLI │ │ Session │ │ Manifest │
170
+ │ (cli.ts) │────▶│ (session.ts) │────▶│ (manifest.ts) │
171
+ │ arg parsing │ │ commands │ │ JSON state │
172
+ └─────────────┘ └──────┬───────┘ └────────────────┘
173
+
174
+ ┌──────▼───────┐
175
+ │ Git │
176
+ │ (git.ts) │
177
+ │ plumbing │
178
+ └──────────────┘
179
+ ```
180
+
181
+ | File | Purpose | Lines |
182
+ |------|---------|-------|
183
+ | `src/git.ts` | Git command wrapper (`execFileSync`) | ~170 |
184
+ | `src/manifest.ts` | Session state CRUD in `.git/sessions/` | ~160 |
185
+ | `src/session.ts` | Core commands (start, commit, squash, pr, end...) | ~600 |
186
+ | `src/conflicts.ts` | Cross-session file overlap detection | ~55 |
187
+ | `src/test-session.ts` | Worktree-based testing + combined testing | ~140 |
188
+ | `src/cli.ts` | Entry point, argument parsing | ~230 |
189
+
190
+ ### Design Decisions
191
+
192
+ - **Real git branches** — not virtual branches. Every git tool works: `git log`, `git diff`, lazygit, tig, VS Code.
193
+ - **Worktrees for isolation** — the default state is isolated. No unapply/apply dance.
194
+ - **JSON manifests** — stored in `.git/sessions/`. Disposable. Worst case: delete and start over.
195
+ - **No custom merge engine** — git's built-in merge handles everything. Source of most GitButler complexity eliminated.
196
+ - **`execFileSync` everywhere** — array arguments prevent shell injection. No `execSync` with string interpolation.
197
+ - **Atomic manifest writes** — write to `.tmp`, then `rename()`. Crash-safe.
198
+
199
+ ## git-stint vs GitButler
200
+
201
+ | Aspect | git-stint | GitButler |
202
+ |--------|-----------|-----------|
203
+ | Isolation | Default (each branch IS isolated) | Opt-in (unapply other branches) |
204
+ | Branch storage | Real git branches | Virtual branches (TOML + SQLite) |
205
+ | Working dir | One branch per worktree | Permanent octopus merge overlay |
206
+ | Merge engine | Git's built-in | Custom hunk-level engine |
207
+ | Git compatibility | Full — all git tools work | Partial — writes break state |
208
+ | State | JSON manifests (disposable) | SQLite + TOML (can corrupt) |
209
+ | Code size | ~600 lines TypeScript | ~100k+ lines Rust |
210
+ | Dependencies | git, gh (optional) | Tauri desktop app |
211
+
212
+ git-stint is designed for AI agent workflows where sessions are independent and short-lived. GitButler is a full-featured branch management GUI for teams.
213
+
214
+ ## Development
215
+
216
+ ```bash
217
+ git clone https://github.com/rchaz/git-stint.git
218
+ cd git-stint
219
+ npm install
220
+ npm run build
221
+ npm link # Install globally for testing
222
+
223
+ # Run tests
224
+ npm test # Unit tests
225
+ npm run test:security # Security tests
226
+ npm run test:integration # Integration tests
227
+ npm run test:all # Everything
228
+ ```
229
+
230
+ ## Contributing
231
+
232
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and PR guidelines.
233
+
234
+ Please note that this project is released with a [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you agree to abide by its terms.
235
+
236
+ ## License
237
+
238
+ MIT — see [LICENSE](LICENSE)
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Claude Code PreToolUse hook for git-stint
4
+ # Called before Write/Edit/NotebookEdit tool use.
5
+ #
6
+ # Reads tool input from stdin (JSON), extracts file_path, and tracks it.
7
+ #
8
+ # Behavior:
9
+ # - File is inside a .stint/ worktree: tracks the file, allows the write.
10
+ # - File is in main repo with active session: blocks the write and tells
11
+ # the agent to work in the worktree directory instead.
12
+ # - No session active: allows the write silently.
13
+ #
14
+
15
+ set -euo pipefail
16
+
17
+ # Check that git-stint is available
18
+ if ! command -v git-stint &>/dev/null; then
19
+ echo "Warning: git-stint is not in PATH. Hooks will not work." >&2
20
+ echo "Install with: npm install -g git-stint (or npm link from the repo)" >&2
21
+ exit 0
22
+ fi
23
+
24
+ # Read the tool input from stdin
25
+ INPUT=$(cat)
26
+
27
+ # Extract file_path using jq if available, fallback to grep/sed
28
+ if command -v jq &>/dev/null; then
29
+ FILE_PATH=$(echo "$INPUT" | jq -r '.file_path // .notebook_path // empty' 2>/dev/null || true)
30
+ else
31
+ FILE_PATH=$(echo "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
32
+ if [ -z "$FILE_PATH" ]; then
33
+ FILE_PATH=$(echo "$INPUT" | grep -o '"notebook_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"notebook_path"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
34
+ fi
35
+ fi
36
+
37
+ # If no file path found, allow silently
38
+ if [ -z "$FILE_PATH" ]; then
39
+ exit 0
40
+ fi
41
+
42
+ # Check if the file being edited is inside a stint worktree (check the file path, not pwd)
43
+ # Match /.stint/<name>/ precisely — avoid false positives like /my.stint/ or /.stinted/
44
+ if echo "$FILE_PATH" | grep -qE '/\.stint/[^/]+/'; then
45
+ # File is in a stint worktree — track and allow
46
+ git-stint track "$FILE_PATH" 2>/dev/null || true
47
+ exit 0
48
+ fi
49
+
50
+ # The file is NOT in a stint worktree. Check if any stint session exists.
51
+ SESSIONS_DIR="$(git rev-parse --git-common-dir 2>/dev/null)/sessions"
52
+ if [ -d "$SESSIONS_DIR" ]; then
53
+ # Iterate session manifests to find the best one to suggest.
54
+ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
55
+ BEST_WORKTREE=""
56
+ BEST_SESSION=""
57
+
58
+ for manifest in "$SESSIONS_DIR"/*.json; do
59
+ [ -f "$manifest" ] || continue
60
+ # Skip .tmp files (glob won't match, but be safe)
61
+ case "$manifest" in *.tmp) continue ;; esac
62
+
63
+ if command -v jq &>/dev/null; then
64
+ wt=$(jq -r '.worktree // empty' "$manifest" 2>/dev/null || true)
65
+ sn=$(jq -r '.name // empty' "$manifest" 2>/dev/null || true)
66
+ else
67
+ wt=$(grep -o '"worktree"[[:space:]]*:[[:space:]]*"[^"]*"' "$manifest" | head -1 | sed 's/.*"worktree"[[:space:]]*:[[:space:]]*"//;s/"$//')
68
+ sn=$(grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' "$manifest" | head -1 | sed 's/.*"name"[[:space:]]*:[[:space:]]*"//;s/"$//')
69
+ fi
70
+
71
+ if [ -z "$BEST_WORKTREE" ] && [ -n "$wt" ]; then
72
+ BEST_WORKTREE="$wt"
73
+ BEST_SESSION="$sn"
74
+ fi
75
+ done
76
+
77
+ if [ -n "$BEST_WORKTREE" ]; then
78
+ # Active session exists but agent is writing to the main repo — block.
79
+ if [ -n "$REPO_ROOT" ]; then
80
+ ABS_WORKTREE="$REPO_ROOT/$BEST_WORKTREE"
81
+ echo "BLOCKED: You have an active stint session '${BEST_SESSION}'." >&2
82
+ echo "You must work in the session worktree, not the main repo." >&2
83
+ echo "Run: cd \"$ABS_WORKTREE\"" >&2
84
+ echo "Then retry your edit. The file you need is: $ABS_WORKTREE/${FILE_PATH#$REPO_ROOT/}" >&2
85
+ else
86
+ echo "BLOCKED: An active stint session exists. Work in the session worktree instead." >&2
87
+ echo "Run: git stint list" >&2
88
+ fi
89
+
90
+ # Exit non-zero to block the tool call
91
+ exit 1
92
+ fi
93
+ fi
94
+
95
+ # No session active — allow the write
96
+ exit 0
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Claude Code Stop hook for git-stint
4
+ # Called when a Claude Code session ends.
5
+ #
6
+ # Commits any pending changes as a WIP checkpoint.
7
+ # The session persists — user can resume or finalize later.
8
+ #
9
+
10
+ set -euo pipefail
11
+
12
+ # Check that git-stint is available
13
+ if ! command -v git-stint &>/dev/null; then
14
+ echo "Warning: git-stint is not in PATH. Stop hook cannot commit pending changes." >&2
15
+ echo "Install with: npm install -g git-stint (or npm link from the repo)" >&2
16
+ exit 0
17
+ fi
18
+
19
+ # Commit pending work for all active sessions.
20
+ # Uses --json to get session names, avoiding "multiple sessions" ambiguity error.
21
+ SESSIONS_JSON=$(git-stint list --json 2>/dev/null || echo "[]")
22
+
23
+ if command -v jq &>/dev/null; then
24
+ NAMES=$(echo "$SESSIONS_JSON" | jq -r '.[].name' 2>/dev/null || true)
25
+ else
26
+ NAMES=$(echo "$SESSIONS_JSON" | grep -o '"name":"[^"]*"' | sed 's/"name":"//;s/"$//' || true)
27
+ fi
28
+
29
+ if [ -n "$NAMES" ]; then
30
+ echo "$NAMES" | while read -r name; do
31
+ [ -z "$name" ] && continue
32
+ git-stint commit -m "WIP: session checkpoint" --session "$name" 2>/dev/null || true
33
+ done
34
+ fi
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Installs git-stint hooks into Claude Code's settings.
3
+ *
4
+ * Usage: node install.js [--project | --user]
5
+ *
6
+ * Hooks installed:
7
+ * PreToolUse (Write/Edit): Track files written in session worktrees
8
+ * Stop: Commit pending changes as WIP checkpoint
9
+ */
10
+ declare function install(scope: "project" | "user"): void;
11
+ export { install };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Standalone installer script for git-stint hooks.
3
+ * Re-exports from src/install-hooks.ts for backward compatibility.
4
+ *
5
+ * Usage: node install.js [--project | --user]
6
+ */
7
+
8
+ export { install } from "../../src/install-hooks.js";
9
+
10
+ // Run directly when executed as a script
11
+ import { install } from "../../src/install-hooks.js";
12
+ const isDirectRun = process.argv[1]?.endsWith("install.js") || process.argv[1]?.endsWith("install.ts");
13
+ if (isDirectRun) {
14
+ const scope = process.argv.includes("--user") ? "user" : "project";
15
+ install(scope);
16
+ }
package/bin/git-stint ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/cli.js');
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};