just-git 0.1.0

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 blindmansion
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,240 @@
1
+ # just-git
2
+
3
+ Git implementation for virtual bash environments (particularly [just-bash](https://github.com/vercel-labs/just-bash)). Pure TypeScript, zero dependencies. Works in Node, Bun, Deno, and the browser. ~97 kB gzipped.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install just-git
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ import { Bash } from "just-bash";
15
+ import { createGit } from "just-git";
16
+
17
+ const git = createGit({
18
+ identity: { name: "Alice", email: "alice@example.com" },
19
+ });
20
+
21
+ const bash = new Bash({
22
+ cwd: "/repo",
23
+ customCommands: [git],
24
+ });
25
+
26
+ await bash.exec("git init");
27
+ await bash.exec("echo 'hello' > README.md");
28
+ await bash.exec("git add .");
29
+ await bash.exec('git commit -m "initial commit"');
30
+ await bash.exec("git log --oneline");
31
+ ```
32
+
33
+ ## Options
34
+
35
+ `createGit(options?)` accepts:
36
+
37
+ | Option | Description |
38
+ | ------------- | ------------------------------------------------------------------------------------------------------------------------------- |
39
+ | `identity` | Author/committer override. With `locked: true`, always wins over env vars and git config. Without `locked`, acts as a fallback. |
40
+ | `credentials` | `(url) => HttpAuth \| null` callback for Smart HTTP transport auth. |
41
+ | `disabled` | `GitCommandName[]` of subcommands to block (e.g. `["push", "rebase"]`). |
42
+ | `network` | `{ allowed?: string[] }` to restrict HTTP access by hostname or URL prefix. Set to `false` to block all network access. |
43
+
44
+ ```ts
45
+ const git = createGit({
46
+ identity: { name: "Agent Bot", email: "bot@company.com", locked: true },
47
+ credentials: async (url) => ({ type: "bearer", token: "ghp_..." }),
48
+ disabled: ["rebase"],
49
+ network: { allowed: ["github.com"] },
50
+ });
51
+ ```
52
+
53
+ ## Middleware
54
+
55
+ Middleware wraps every `git <subcommand>` invocation. Each middleware receives a `CommandEvent` and a `next()` function. Call `next()` to proceed, or return an `ExecResult` to short-circuit. Middlewares compose in registration order (first registered = outermost).
56
+
57
+ The `CommandEvent` provides the execution context: `{ command, rawArgs, fs, cwd, env, stdin }`, plus optional `exec` and `signal` when available.
58
+
59
+ ```ts
60
+ // Audit log — record every command the agent runs
61
+ git.use(async (event, next) => {
62
+ const result = await next();
63
+ auditLog.push({ command: `git ${event.command}`, exitCode: result.exitCode });
64
+ return result;
65
+ });
66
+
67
+ // Gate pushes on human approval
68
+ git.use(async (event, next) => {
69
+ if (event.command === "push" && !(await getHumanApproval(event.rawArgs))) {
70
+ return { stdout: "", stderr: "Push blocked — awaiting approval.\n", exitCode: 1 };
71
+ }
72
+ return next();
73
+ });
74
+
75
+ // Block commits that add large files (uses event.fs to read the worktree)
76
+ git.use(async (event, next) => {
77
+ if (event.command === "add") {
78
+ for (const path of event.rawArgs.filter((a) => !a.startsWith("-"))) {
79
+ const resolved = path.startsWith("/") ? path : `${event.cwd}/${path}`;
80
+ const stat = await event.fs.stat(resolved).catch(() => null);
81
+ if (stat && stat.size > 5_000_000) {
82
+ return { stdout: "", stderr: `Blocked: ${path} exceeds 5 MB\n`, exitCode: 1 };
83
+ }
84
+ }
85
+ }
86
+ return next();
87
+ });
88
+ ```
89
+
90
+ `git.use()` returns an unsubscribe function to remove the middleware dynamically.
91
+
92
+ ## Hooks
93
+
94
+ Hooks fire at specific points inside command execution (after middleware, inside operation logic). Register with `git.on(event, handler)`, which returns an unsubscribe function.
95
+
96
+ ### Pre-hooks
97
+
98
+ Pre-hooks can abort the operation by returning `{ abort: true, message? }`.
99
+
100
+ ```ts
101
+ // Block secrets from being committed
102
+ git.on("pre-commit", (event) => {
103
+ const forbidden = event.index.entries.filter((e) => /\.(env|pem|key)$/.test(e.path));
104
+ if (forbidden.length) {
105
+ return { abort: true, message: `Blocked: ${forbidden.map((e) => e.path).join(", ")}` };
106
+ }
107
+ });
108
+
109
+ // Enforce conventional commit messages
110
+ git.on("commit-msg", (event) => {
111
+ if (!/^(feat|fix|docs|refactor|test|chore)(\(.+\))?:/.test(event.message)) {
112
+ return { abort: true, message: "Commit message must follow conventional commits format" };
113
+ }
114
+ });
115
+ ```
116
+
117
+ | Hook | Payload |
118
+ | ------------------ | --------------------------------------------------------------- |
119
+ | `pre-commit` | `{ index, treeHash }` |
120
+ | `commit-msg` | `{ message }` (mutable) |
121
+ | `merge-msg` | `{ message, treeHash, headHash, theirsHash }` (mutable message) |
122
+ | `pre-merge-commit` | `{ mergeMessage, treeHash, headHash, theirsHash }` |
123
+ | `pre-checkout` | `{ target, mode }` |
124
+ | `pre-push` | `{ remote, url, refs[] }` |
125
+ | `pre-fetch` | `{ remote, url, refspecs, prune, tags }` |
126
+ | `pre-clone` | `{ repository, targetPath, bare, branch }` |
127
+ | `pre-pull` | `{ remote, branch }` |
128
+ | `pre-rebase` | `{ upstream, branch }` |
129
+ | `pre-reset` | `{ mode, target }` |
130
+ | `pre-clean` | `{ dryRun, force, removeDirs, removeIgnored, onlyIgnored }` |
131
+ | `pre-rm` | `{ paths, cached, recursive, force }` |
132
+ | `pre-cherry-pick` | `{ mode, commit }` |
133
+ | `pre-revert` | `{ mode, commit }` |
134
+ | `pre-stash` | `{ action, ref }` |
135
+
136
+ ### Post-hooks
137
+
138
+ Post-hooks are observational -- return value is ignored. Handlers are awaited in registration order.
139
+
140
+ ```ts
141
+ // Feed agent activity to your UI or orchestration layer
142
+ git.on("post-commit", (event) => {
143
+ onAgentCommit({ hash: event.hash, branch: event.branch, message: event.message });
144
+ });
145
+
146
+ git.on("post-push", (event) => {
147
+ onAgentPush({ remote: event.remote, refs: event.refs });
148
+ });
149
+ ```
150
+
151
+ | Hook | Payload |
152
+ | ------------------ | ------------------------------------------------ |
153
+ | `post-commit` | `{ hash, message, branch, parents, author }` |
154
+ | `post-merge` | `{ headHash, theirsHash, strategy, commitHash }` |
155
+ | `post-checkout` | `{ prevHead, newHead, isBranchCheckout }` |
156
+ | `post-push` | same payload as `pre-push` |
157
+ | `post-fetch` | `{ remote, url, refsUpdated }` |
158
+ | `post-clone` | `{ repository, targetPath, bare, branch }` |
159
+ | `post-pull` | `{ remote, branch, strategy, commitHash }` |
160
+ | `post-reset` | `{ mode, targetHash }` |
161
+ | `post-clean` | `{ removed, dryRun }` |
162
+ | `post-rm` | `{ removedPaths, cached }` |
163
+ | `post-cherry-pick` | `{ mode, commitHash, hadConflicts }` |
164
+ | `post-revert` | `{ mode, commitHash, hadConflicts }` |
165
+ | `post-stash` | `{ action, ok }` |
166
+
167
+ ### Low-level events
168
+
169
+ Fire-and-forget events emitted on every object/ref write. Handler errors are caught and forwarded to `hooks.onError` (no-op by default).
170
+
171
+ | Event | Payload |
172
+ | -------------- | --------------------------- |
173
+ | `ref:update` | `{ ref, oldHash, newHash }` |
174
+ | `ref:delete` | `{ ref, oldHash }` |
175
+ | `object:write` | `{ type, hash }` |
176
+
177
+ ## Command coverage
178
+
179
+ 33 commands implemented. See [CLI.md](CLI.md) for full usage details.
180
+
181
+ | Command | Flags / options |
182
+ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
183
+ | `init [<dir>]` | `--bare`, `--initial-branch` |
184
+ | `clone <repo> [<dir>]` | `--bare`, `-b <branch>` |
185
+ | `blame <file>` | `-L <start>,<end>`, `-l`/`--long`, `-e`/`--show-email`, `-s`/`--suppress`, `-p`/`--porcelain`, `--line-porcelain` |
186
+ | `add <paths>` | `.`, `--all`/`-A`, `--update`/`-u`, `--force`/`-f`, `-n`/`--dry-run`, glob pathspecs |
187
+ | `rm <paths>` | `--cached`, `-r`, `-f`, `-n`/`--dry-run`, glob pathspecs |
188
+ | `mv <src> <dst>` | `-f`, `-n`/`--dry-run`, `-k` |
189
+ | `commit` | `-m`, `-F <file>` / `-F -`, `--allow-empty`, `--amend`, `--no-edit`, `-a` |
190
+ | `status` | `-s`/`--short`, `--porcelain`, `-b`/`--branch` |
191
+ | `log` | `--oneline`, `-n`, `--all`, `--reverse`, `--decorate`, `--format`/`--pretty`, `A..B`, `A...B`, `-- <path>`, `--author=`, `--grep=`, `--since`/`--after`, `--until`/`--before` |
192
+ | `show [<object>]` | Commits (with diff), annotated tags, trees, blobs |
193
+ | `diff` | `--cached`/`--staged`, `<commit>`, `<commit> <commit>`, `A..B`, `A...B`, `-- <path>`, `--stat`, `--shortstat`, `--numstat`, `--name-only`, `--name-status` |
194
+ | `branch` | `-d`, `-D`, `-m`, `-M`, `-r`, `-a`/`--all`, `-v`/`-vv`, `-u`/`--set-upstream-to` |
195
+ | `tag [<name>] [<commit>]` | `-a -m` (annotated), `-d`, `-l <pattern>`, `-f` |
196
+ | `switch` | `-c`/`-C` (create/force-create), `--detach`/`-d`, `--orphan`, `-` (previous branch), `--guess`/`--no-guess` |
197
+ | `restore` | `-s`/`--source`, `-S`/`--staged`, `-W`/`--worktree`, `-S -W` (both), `--ours`/`--theirs`, pathspec globs |
198
+ | `checkout` | `-b`, `-B`, `--orphan`, detached HEAD, `-- <paths>`, `--ours`/`--theirs`, pathspec globs |
199
+ | `reset [<commit>]` | `-- <paths>`, `--soft`, `--mixed`, `--hard`, pathspec globs |
200
+ | `merge <branch>` | `--no-ff`, `--ff-only`, `--squash`, `-m`, `--abort`, `--continue`, conflict markers |
201
+ | `revert <commit>` | `--abort`, `--continue`, `-n`/`--no-commit`, `--no-edit`, `-m`/`--mainline` |
202
+ | `cherry-pick <commit>` | `--abort`, `--continue`, `--skip`, `-x`, `-m`/`--mainline`, `-n`/`--no-commit`, preserves original author |
203
+ | `rebase <upstream>` | `--onto <newbase>`, `--abort`, `--continue`, `--skip` |
204
+ | `stash` | `push`, `pop`, `apply`, `list`, `drop`, `show`, `clear`, `-m`, `-u`/`--include-untracked`, `stash@{N}` |
205
+ | `remote` | `add`, `remove`/`rm`, `rename`, `set-url`, `get-url`, `-v` |
206
+ | `config` | `get`, `set`, `unset`, `list`, `--list`/`-l`, `--unset` |
207
+ | `fetch [<remote>] [<refspec>...]` | `--all`, `--tags`, `--prune`/`-p` |
208
+ | `push [<remote>] [<refspec>...]` | `--force`/`-f`, `-u`/`--set-upstream`, `--all`, `--tags`, `--delete`/`-d` |
209
+ | `pull [<remote>] [<branch>]` | `--ff-only`, `--no-ff`, `--rebase`/`-r`, `--no-rebase` |
210
+ | `clean` | `-f`, `-n`/`--dry-run`, `-d`, `-x`, `-X`, `-e`/`--exclude` |
211
+ | `reflog` | `show [<ref>]`, `exists`, `-n`/`--max-count` |
212
+ | `gc` | `--aggressive` |
213
+ | `repack` | `-a`/`--all`, `-d`/`--delete` |
214
+ | `rev-parse` | `--verify`, `--short`, `--abbrev-ref`, `--symbolic-full-name`, `--show-toplevel`, `--git-dir`, `--is-inside-work-tree`, `--is-bare-repository`, `--show-prefix`, `--show-cdup` |
215
+ | `ls-files` | `-c`/`--cached`, `-m`/`--modified`, `-d`/`--deleted`, `-o`/`--others`, `-u`/`--unmerged`, `-s`/`--stage`, `--exclude-standard`, `-z`, `-t` |
216
+
217
+ ### Transport
218
+
219
+ - **Local paths** -- direct filesystem transfer between repositories.
220
+ - **Smart HTTP** -- clone, fetch, and push against real Git servers (e.g. GitHub) via Git Smart HTTP protocol. Auth via `credentials` option or `GIT_HTTP_BEARER_TOKEN` / `GIT_HTTP_USER` + `GIT_HTTP_PASSWORD` env vars.
221
+
222
+ ### Internals
223
+
224
+ - `.gitignore` support (hierarchical, negation, `info/exclude`, `core.excludesFile`)
225
+ - Merge-ort strategy with rename detection and recursive merge bases
226
+ - Reflog for HEAD, branches, and tracking refs
227
+ - Index in Git binary v2 format
228
+ - Object storage in real Git format (SHA-1 addressed)
229
+ - Packfiles with zlib compression for storage and transport
230
+ - Pathspec globs across `add`, `rm`, `diff`, `reset`, `checkout`, `restore`, `log`
231
+
232
+ ## Goals and testing
233
+
234
+ High fidelity to real git (2.53.0) state and output. Tested using real git as an [oracle](test/oracle/README.md) across hundreds of randomized command traces.
235
+
236
+ If you're running just-bash over a real filesystem, mixing commands between this implementation and real git in the same repo has not been extensively tested yet.
237
+
238
+ ## Disclaimer
239
+
240
+ This project is not affiliated with [just-bash](https://github.com/vercel-labs/just-bash) or Vercel.
@@ -0,0 +1,395 @@
1
+ interface FileStat {
2
+ isFile: boolean;
3
+ isDirectory: boolean;
4
+ isSymbolicLink: boolean;
5
+ mode: number;
6
+ size: number;
7
+ mtime: Date;
8
+ }
9
+ interface FileSystem {
10
+ readFile(path: string): Promise<string>;
11
+ readFileBuffer(path: string): Promise<Uint8Array>;
12
+ writeFile(path: string, content: string | Uint8Array): Promise<void>;
13
+ exists(path: string): Promise<boolean>;
14
+ stat(path: string): Promise<FileStat>;
15
+ mkdir(path: string, options?: {
16
+ recursive?: boolean;
17
+ }): Promise<void>;
18
+ readdir(path: string): Promise<string[]>;
19
+ rm(path: string, options?: {
20
+ recursive?: boolean;
21
+ force?: boolean;
22
+ }): Promise<void>;
23
+ /** Stat without following symlinks. Falls back to stat() semantics when not implemented. */
24
+ lstat?(path: string): Promise<FileStat>;
25
+ /** Read the target of a symbolic link. */
26
+ readlink?(path: string): Promise<string>;
27
+ /** Create a symbolic link pointing to target at the given path. */
28
+ symlink?(target: string, path: string): Promise<void>;
29
+ }
30
+
31
+ /** 40-character lowercase hex SHA-1 hash. */
32
+ type ObjectId = string;
33
+ /** The four Git object types. */
34
+ type ObjectType = "blob" | "tree" | "commit" | "tag";
35
+ /** Author or committer identity with timestamp. */
36
+ interface Identity {
37
+ name: string;
38
+ email: string;
39
+ /** Unix epoch seconds. */
40
+ timestamp: number;
41
+ /** Timezone offset string, e.g. "+0000", "-0500". */
42
+ timezone: string;
43
+ }
44
+ /** Stat-like metadata stored per index entry. */
45
+ interface IndexStat {
46
+ ctimeSeconds: number;
47
+ ctimeNanoseconds: number;
48
+ mtimeSeconds: number;
49
+ mtimeNanoseconds: number;
50
+ dev: number;
51
+ ino: number;
52
+ uid: number;
53
+ gid: number;
54
+ size: number;
55
+ }
56
+ interface IndexEntry {
57
+ /** File path relative to the work tree root. */
58
+ path: string;
59
+ /** File mode as a numeric value (e.g. 0o100644). */
60
+ mode: number;
61
+ /** SHA-1 of the blob content. */
62
+ hash: ObjectId;
63
+ /** Merge stage: 0 = normal, 1 = base, 2 = ours, 3 = theirs. */
64
+ stage: number;
65
+ stat: IndexStat;
66
+ }
67
+ interface Index {
68
+ version: number;
69
+ entries: IndexEntry[];
70
+ }
71
+
72
+ type HttpAuth = {
73
+ type: "basic";
74
+ username: string;
75
+ password: string;
76
+ } | {
77
+ type: "bearer";
78
+ token: string;
79
+ };
80
+
81
+ interface ExecResult {
82
+ stdout: string;
83
+ stderr: string;
84
+ exitCode: number;
85
+ }
86
+
87
+ type CredentialProvider = (url: string) => HttpAuth | null | Promise<HttpAuth | null>;
88
+ interface IdentityOverride {
89
+ name: string;
90
+ email: string;
91
+ locked?: boolean;
92
+ }
93
+ type FetchFunction = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
94
+ interface NetworkPolicy {
95
+ /**
96
+ * Allowed URL patterns. Can be:
97
+ * - A hostname: "github.com" (matches any URL whose host equals this)
98
+ * - A URL prefix: "https://github.com/myorg/" (matches URLs starting with this)
99
+ */
100
+ allowed?: string[];
101
+ /** Custom fetch function for HTTP transport. Falls back to globalThis.fetch. */
102
+ fetch?: FetchFunction;
103
+ }
104
+ interface PreCommitEvent {
105
+ readonly index: Index;
106
+ readonly treeHash: ObjectId;
107
+ }
108
+ interface CommitMsgEvent {
109
+ message: string;
110
+ }
111
+ interface MergeMsgEvent {
112
+ message: string;
113
+ readonly treeHash: ObjectId;
114
+ readonly headHash: ObjectId;
115
+ readonly theirsHash: ObjectId;
116
+ }
117
+ interface PostCommitEvent {
118
+ readonly hash: ObjectId;
119
+ readonly message: string;
120
+ readonly branch: string | null;
121
+ readonly parents: readonly ObjectId[];
122
+ readonly author: Identity;
123
+ }
124
+ interface PreMergeCommitEvent {
125
+ readonly mergeMessage: string;
126
+ readonly treeHash: ObjectId;
127
+ readonly headHash: ObjectId;
128
+ readonly theirsHash: ObjectId;
129
+ }
130
+ interface PostMergeEvent {
131
+ readonly headHash: ObjectId;
132
+ readonly theirsHash: ObjectId;
133
+ readonly strategy: "fast-forward" | "three-way";
134
+ readonly commitHash: ObjectId | null;
135
+ }
136
+ interface PostCheckoutEvent {
137
+ readonly prevHead: ObjectId | null;
138
+ readonly newHead: ObjectId;
139
+ readonly isBranchCheckout: boolean;
140
+ }
141
+ interface PrePushEvent {
142
+ readonly remote: string;
143
+ readonly url: string;
144
+ readonly refs: ReadonlyArray<{
145
+ srcRef: string | null;
146
+ srcHash: ObjectId | null;
147
+ dstRef: string;
148
+ dstHash: ObjectId | null;
149
+ force: boolean;
150
+ delete: boolean;
151
+ }>;
152
+ }
153
+ type PostPushEvent = PrePushEvent;
154
+ interface PreRebaseEvent {
155
+ readonly upstream: string;
156
+ readonly branch: string | null;
157
+ }
158
+ interface PreCheckoutEvent {
159
+ readonly target: string;
160
+ readonly mode: "switch" | "detach" | "create-branch" | "paths";
161
+ }
162
+ interface PreFetchEvent {
163
+ readonly remote: string;
164
+ readonly url: string;
165
+ readonly refspecs: readonly string[];
166
+ readonly prune: boolean;
167
+ readonly tags: boolean;
168
+ }
169
+ interface PostFetchEvent {
170
+ readonly remote: string;
171
+ readonly url: string;
172
+ readonly refsUpdated: number;
173
+ }
174
+ interface PreCloneEvent {
175
+ readonly repository: string;
176
+ readonly targetPath: string;
177
+ readonly bare: boolean;
178
+ readonly branch: string | null;
179
+ }
180
+ interface PostCloneEvent {
181
+ readonly repository: string;
182
+ readonly targetPath: string;
183
+ readonly bare: boolean;
184
+ readonly branch: string | null;
185
+ }
186
+ interface PrePullEvent {
187
+ readonly remote: string;
188
+ readonly branch: string | null;
189
+ }
190
+ interface PostPullEvent {
191
+ readonly remote: string;
192
+ readonly branch: string | null;
193
+ readonly strategy: "up-to-date" | "fast-forward" | "three-way";
194
+ readonly commitHash: ObjectId | null;
195
+ }
196
+ interface PreResetEvent {
197
+ readonly mode: "soft" | "mixed" | "hard" | "paths";
198
+ readonly target: string | null;
199
+ }
200
+ interface PostResetEvent {
201
+ readonly mode: "soft" | "mixed" | "hard" | "paths";
202
+ readonly targetHash: ObjectId | null;
203
+ }
204
+ interface PreCleanEvent {
205
+ readonly dryRun: boolean;
206
+ readonly force: boolean;
207
+ readonly removeDirs: boolean;
208
+ readonly removeIgnored: boolean;
209
+ readonly onlyIgnored: boolean;
210
+ }
211
+ interface PostCleanEvent {
212
+ readonly removed: readonly string[];
213
+ readonly dryRun: boolean;
214
+ }
215
+ interface PreRmEvent {
216
+ readonly paths: readonly string[];
217
+ readonly cached: boolean;
218
+ readonly recursive: boolean;
219
+ readonly force: boolean;
220
+ }
221
+ interface PostRmEvent {
222
+ readonly removedPaths: readonly string[];
223
+ readonly cached: boolean;
224
+ }
225
+ interface PreCherryPickEvent {
226
+ readonly mode: "pick" | "continue" | "abort";
227
+ readonly commit: string | null;
228
+ }
229
+ interface PostCherryPickEvent {
230
+ readonly mode: "pick" | "continue" | "abort";
231
+ readonly commitHash: ObjectId | null;
232
+ readonly hadConflicts: boolean;
233
+ }
234
+ interface PreRevertEvent {
235
+ readonly mode: "revert" | "continue" | "abort";
236
+ readonly commit: string | null;
237
+ }
238
+ interface PostRevertEvent {
239
+ readonly mode: "revert" | "continue" | "abort";
240
+ readonly commitHash: ObjectId | null;
241
+ readonly hadConflicts: boolean;
242
+ }
243
+ interface PreStashEvent {
244
+ readonly action: "push" | "pop" | "apply" | "list" | "drop" | "show" | "clear";
245
+ readonly ref: string | null;
246
+ }
247
+ interface PostStashEvent {
248
+ readonly action: "push" | "pop" | "apply" | "list" | "drop" | "show" | "clear";
249
+ readonly ok: boolean;
250
+ }
251
+ interface RefUpdateEvent {
252
+ readonly ref: string;
253
+ readonly oldHash: ObjectId | null;
254
+ readonly newHash: ObjectId;
255
+ }
256
+ interface RefDeleteEvent {
257
+ readonly ref: string;
258
+ readonly oldHash: ObjectId | null;
259
+ }
260
+ interface ObjectWriteEvent {
261
+ readonly type: ObjectType;
262
+ readonly hash: ObjectId;
263
+ }
264
+ interface HookEventMap {
265
+ "pre-commit": PreCommitEvent;
266
+ "commit-msg": CommitMsgEvent;
267
+ "merge-msg": MergeMsgEvent;
268
+ "post-commit": PostCommitEvent;
269
+ "pre-merge-commit": PreMergeCommitEvent;
270
+ "post-merge": PostMergeEvent;
271
+ "pre-checkout": PreCheckoutEvent;
272
+ "post-checkout": PostCheckoutEvent;
273
+ "pre-push": PrePushEvent;
274
+ "post-push": PostPushEvent;
275
+ "pre-fetch": PreFetchEvent;
276
+ "post-fetch": PostFetchEvent;
277
+ "pre-clone": PreCloneEvent;
278
+ "post-clone": PostCloneEvent;
279
+ "pre-pull": PrePullEvent;
280
+ "post-pull": PostPullEvent;
281
+ "pre-rebase": PreRebaseEvent;
282
+ "pre-reset": PreResetEvent;
283
+ "post-reset": PostResetEvent;
284
+ "pre-clean": PreCleanEvent;
285
+ "post-clean": PostCleanEvent;
286
+ "pre-rm": PreRmEvent;
287
+ "post-rm": PostRmEvent;
288
+ "pre-cherry-pick": PreCherryPickEvent;
289
+ "post-cherry-pick": PostCherryPickEvent;
290
+ "pre-revert": PreRevertEvent;
291
+ "post-revert": PostRevertEvent;
292
+ "pre-stash": PreStashEvent;
293
+ "post-stash": PostStashEvent;
294
+ "ref:update": RefUpdateEvent;
295
+ "ref:delete": RefDeleteEvent;
296
+ "object:write": ObjectWriteEvent;
297
+ }
298
+ type PreHookName = "pre-commit" | "commit-msg" | "merge-msg" | "pre-merge-commit" | "pre-checkout" | "pre-push" | "pre-fetch" | "pre-clone" | "pre-pull" | "pre-rebase" | "pre-reset" | "pre-clean" | "pre-rm" | "pre-cherry-pick" | "pre-revert" | "pre-stash";
299
+ interface AbortResult {
300
+ abort: true;
301
+ message?: string;
302
+ }
303
+ type PreHookHandler<E extends PreHookName> = (event: HookEventMap[E]) => void | AbortResult | Promise<void | AbortResult>;
304
+ type PostHookHandler<E extends keyof HookEventMap> = (event: HookEventMap[E]) => void | Promise<void>;
305
+ type HookHandler<E extends keyof HookEventMap> = E extends PreHookName ? PreHookHandler<E> : PostHookHandler<E>;
306
+
307
+ interface CommandEvent {
308
+ /** The git subcommand being invoked (e.g. "commit", "push"). */
309
+ command: string | undefined;
310
+ /** Arguments after the subcommand. */
311
+ rawArgs: string[];
312
+ /** Virtual filesystem — same instance custom commands receive from just-bash. */
313
+ fs: FileSystem;
314
+ /** Current working directory. */
315
+ cwd: string;
316
+ /** Environment variables. */
317
+ env: Map<string, string>;
318
+ /** Standard input content. */
319
+ stdin: string;
320
+ /** Execute a subcommand in the shell. Available when running via just-bash. */
321
+ exec?: (command: string, options: CommandExecOptions) => Promise<ExecResult>;
322
+ /** Abort signal for cooperative cancellation. */
323
+ signal?: AbortSignal;
324
+ }
325
+ type Middleware = (event: CommandEvent, next: () => Promise<ExecResult>) => ExecResult | Promise<ExecResult>;
326
+ declare class HookEmitter {
327
+ private listeners;
328
+ onError: (error: unknown) => void;
329
+ on<E extends keyof HookEventMap>(event: E, handler: HookHandler<E>): () => void;
330
+ /**
331
+ * Emit a pre-hook event. Returns an AbortResult if any handler aborts,
332
+ * or null if all handlers allow the operation to proceed.
333
+ */
334
+ emitPre<E extends PreHookName>(event: E, data: HookEventMap[E]): Promise<AbortResult | null>;
335
+ /** Emit a post-hook event and await all handlers in order. */
336
+ emitPost<E extends keyof HookEventMap>(event: E, data: HookEventMap[E]): Promise<void>;
337
+ /** Emit low-level events (synchronous, fire-and-forget). */
338
+ emit<E extends keyof HookEventMap>(event: E, data: HookEventMap[E]): void;
339
+ }
340
+
341
+ /** Options for subcommand execution (mirrors just-bash's CommandExecOptions). */
342
+ interface CommandExecOptions {
343
+ env?: Record<string, string>;
344
+ replaceEnv?: boolean;
345
+ cwd: string;
346
+ stdin?: string;
347
+ signal?: AbortSignal;
348
+ }
349
+ /**
350
+ * Context provided to commands during execution.
351
+ * Shadows just-bash's CommandContext — structurally compatible
352
+ * so this library can run with or without just-bash.
353
+ */
354
+ interface CommandContext {
355
+ fs: FileSystem;
356
+ cwd: string;
357
+ env: Map<string, string>;
358
+ stdin: string;
359
+ exec?: (command: string, options: CommandExecOptions) => Promise<ExecResult>;
360
+ signal?: AbortSignal;
361
+ }
362
+ type GitCommandName = "init" | "clone" | "fetch" | "pull" | "push" | "add" | "blame" | "commit" | "status" | "log" | "branch" | "tag" | "checkout" | "diff" | "reset" | "merge" | "cherry-pick" | "revert" | "rebase" | "mv" | "rm" | "remote" | "config" | "show" | "stash" | "rev-parse" | "ls-files" | "clean" | "switch" | "restore" | "reflog" | "repack" | "gc";
363
+ interface GitOptions {
364
+ credentials?: CredentialProvider;
365
+ identity?: IdentityOverride;
366
+ disabled?: GitCommandName[];
367
+ /** Network policy. Set to `false` to block all HTTP access. */
368
+ network?: NetworkPolicy | false;
369
+ }
370
+ /**
371
+ * Bundle of operator-level extensions threaded into command handlers
372
+ * via closures and merged onto GitContext after discovery.
373
+ */
374
+ interface GitExtensions {
375
+ hooks?: HookEmitter;
376
+ credentialProvider?: CredentialProvider;
377
+ identityOverride?: IdentityOverride;
378
+ fetchFn?: FetchFunction;
379
+ networkPolicy?: NetworkPolicy | false;
380
+ }
381
+ declare class Git {
382
+ readonly name = "git";
383
+ readonly hooks: HookEmitter;
384
+ private middlewares;
385
+ private extensions;
386
+ private inner;
387
+ constructor(options?: GitOptions);
388
+ on<E extends keyof HookEventMap>(event: E, handler: HookHandler<E>): () => void;
389
+ use(middleware: Middleware): () => void;
390
+ execute: (args: string[], ctx: CommandContext) => Promise<ExecResult>;
391
+ private runMiddleware;
392
+ }
393
+ declare function createGit(options?: GitOptions): Git;
394
+
395
+ export { type AbortResult, type CommandContext, type CommandEvent, type CommandExecOptions, type CommitMsgEvent, type CredentialProvider, type ExecResult, type FetchFunction, type FileStat, type FileSystem, Git, type GitCommandName, type GitExtensions, type GitOptions, type HookEventMap, type HookHandler, type IdentityOverride, type MergeMsgEvent, type Middleware, type NetworkPolicy, type ObjectWriteEvent, type PostCheckoutEvent, type PostCherryPickEvent, type PostCleanEvent, type PostCloneEvent, type PostCommitEvent, type PostFetchEvent, type PostMergeEvent, type PostPullEvent, type PostPushEvent, type PostResetEvent, type PostRevertEvent, type PostRmEvent, type PostStashEvent, type PreCheckoutEvent, type PreCherryPickEvent, type PreCleanEvent, type PreCloneEvent, type PreCommitEvent, type PreFetchEvent, type PreMergeCommitEvent, type PrePullEvent, type PrePushEvent, type PreRebaseEvent, type PreResetEvent, type PreRevertEvent, type PreRmEvent, type PreStashEvent, type RefDeleteEvent, type RefUpdateEvent, createGit };