just-git 0.1.8 → 0.1.10

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/README.md CHANGED
@@ -2,8 +2,11 @@
2
2
 
3
3
  [![CI](https://github.com/blindmansion/just-git/actions/workflows/ci.yml/badge.svg)](https://github.com/blindmansion/just-git/actions/workflows/ci.yml)
4
4
  [![npm](https://img.shields.io/npm/v/just-git)](https://www.npmjs.com/package/just-git)
5
+ [![bundle size](https://img.shields.io/bundlejs/size/just-git)](https://bundlejs.com/?q=just-git)
5
6
 
6
- 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. ~100 kB gzipped.
7
+ Pure TypeScript git implementation. Zero dependencies. 34 commands. Works in Node, Bun, Deno, and the browser.
8
+
9
+ Designed for sandboxed environments where shelling out to real git isn't possible or desirable. Targets faithful reproduction of real git's behavior and output. Operates on an abstract `FileSystem` interface — plug in an in-memory VFS, a real filesystem, or anything else. Pairs with [just-bash](https://github.com/vercel-labs/just-bash), which provides an in-memory filesystem and shell that just-git registers into as a custom command.
7
10
 
8
11
  ## Install
9
12
 
@@ -37,27 +40,26 @@ await bash.exec("git log --oneline");
37
40
 
38
41
  `createGit(options?)` accepts:
39
42
 
40
- | Option | Description |
41
- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
42
- | `identity` | Author/committer override. With `locked: true`, always wins over env vars and git config. Without `locked`, acts as a fallback. |
43
- | `credentials` | `(url) => HttpAuth \| null` callback for Smart HTTP transport auth. |
44
- | `disabled` | `GitCommandName[]` of subcommands to block (e.g. `["push", "rebase"]`). |
45
- | `network` | `{ allowed?: string[], fetch?: FetchFunction }` to restrict HTTP access and/or provide a custom `fetch` implementation. Set to `false` to block all network access. |
43
+ | Option | Description |
44
+ | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
+ | `identity` | Author/committer override. With `locked: true`, always wins over env vars and git config. Without `locked`, acts as a fallback. |
46
+ | `credentials` | `(url) => HttpAuth \| null` callback for Smart HTTP transport auth. |
47
+ | `disabled` | `GitCommandName[]` of subcommands to block (e.g. `["push", "rebase"]`). |
48
+ | `network` | `{ allowed?: string[], fetch?: FetchFunction }` to restrict HTTP access and/or provide a custom `fetch`. `allowed` accepts hostnames (`"github.com"`) or URL prefixes (`"https://github.com/myorg/"`). Set to `false` to block all network access. |
49
+ | `resolveRemote` | `(url) => GitContext \| null` callback for cross-VFS remote resolution. See [Multi-agent collaboration](#multi-agent-collaboration). |
46
50
 
47
51
  ```ts
48
52
  const git = createGit({
49
53
  identity: { name: "Agent Bot", email: "bot@company.com", locked: true },
50
54
  credentials: async (url) => ({ type: "bearer", token: "ghp_..." }),
51
55
  disabled: ["rebase"],
52
- network: { allowed: ["github.com"], fetch: customFetch },
56
+ network: false, // no HTTP access
53
57
  });
54
58
  ```
55
59
 
56
60
  ## Middleware
57
61
 
58
- 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).
59
-
60
- The `CommandEvent` provides the execution context: `{ command, rawArgs, fs, cwd, env, stdin }`, plus optional `exec` and `signal` when available.
62
+ 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). `git.use()` returns an unsubscribe function.
61
63
 
62
64
  ```ts
63
65
  // Audit log — record every command the agent runs
@@ -90,15 +92,11 @@ git.use(async (event, next) => {
90
92
  });
91
93
  ```
92
94
 
93
- `git.use()` returns an unsubscribe function to remove the middleware dynamically.
94
-
95
95
  ## Hooks
96
96
 
97
97
  Hooks fire at specific points inside command execution (after middleware, inside operation logic). Register with `git.on(event, handler)`, which returns an unsubscribe function.
98
98
 
99
- ### Pre-hooks
100
-
101
- Pre-hooks can abort the operation by returning `{ abort: true, message? }`.
99
+ Pre-hooks can abort the operation by returning `{ abort: true, message? }`. Post-hooks are observational — return value is ignored.
102
100
 
103
101
  ```ts
104
102
  // Block secrets from being committed
@@ -115,71 +113,77 @@ git.on("commit-msg", (event) => {
115
113
  return { abort: true, message: "Commit message must follow conventional commits format" };
116
114
  }
117
115
  });
118
- ```
119
116
 
120
- | Hook | Payload |
121
- | ------------------ | --------------------------------------------------------------- |
122
- | `pre-commit` | `{ index, treeHash }` |
123
- | `commit-msg` | `{ message }` (mutable) |
124
- | `merge-msg` | `{ message, treeHash, headHash, theirsHash }` (mutable message) |
125
- | `pre-merge-commit` | `{ mergeMessage, treeHash, headHash, theirsHash }` |
126
- | `pre-checkout` | `{ target, mode }` |
127
- | `pre-push` | `{ remote, url, refs[] }` |
128
- | `pre-fetch` | `{ remote, url, refspecs, prune, tags }` |
129
- | `pre-clone` | `{ repository, targetPath, bare, branch }` |
130
- | `pre-pull` | `{ remote, branch }` |
131
- | `pre-rebase` | `{ upstream, branch }` |
132
- | `pre-reset` | `{ mode, target }` |
133
- | `pre-clean` | `{ dryRun, force, removeDirs, removeIgnored, onlyIgnored }` |
134
- | `pre-rm` | `{ paths, cached, recursive, force }` |
135
- | `pre-cherry-pick` | `{ mode, commit }` |
136
- | `pre-revert` | `{ mode, commit }` |
137
- | `pre-stash` | `{ action, ref }` |
138
-
139
- ### Post-hooks
140
-
141
- Post-hooks are observational -- return value is ignored. Handlers are awaited in registration order.
142
-
143
- ```ts
144
117
  // Feed agent activity to your UI or orchestration layer
145
118
  git.on("post-commit", (event) => {
146
119
  onAgentCommit({ hash: event.hash, branch: event.branch, message: event.message });
147
120
  });
121
+ ```
122
+
123
+ Available pre-hooks: `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`. Available post-hooks: `post-commit`, `post-merge`, `post-checkout`, `post-push`, `post-fetch`, `post-clone`, `post-pull`, `post-reset`, `post-clean`, `post-rm`, `post-cherry-pick`, `post-revert`, `post-stash`. Low-level events: `ref:update`, `ref:delete`, `object:write`.
124
+
125
+ See [HOOKS.md](HOOKS.md) for full payload types and the `CommandEvent` shape.
126
+
127
+ ## Multi-agent collaboration
128
+
129
+ Multiple agents can work on clones of the same repository in the same process, each with full VFS isolation. The `resolveRemote` option maps remote URLs to `GitContext` instances on other virtual filesystems, so clone/fetch/push/pull cross VFS boundaries without any network or shared filesystem.
130
+
131
+ ```ts
132
+ import { Bash, InMemoryFs } from "just-bash";
133
+ import { createGit, findGitDir } from "just-git";
134
+
135
+ // Origin repo on its own filesystem
136
+ const originFs = new InMemoryFs();
137
+ const setupBash = new Bash({
138
+ fs: originFs,
139
+ cwd: "/repo",
140
+ customCommands: [
141
+ createGit({ identity: { name: "Setup", email: "setup@example.com", locked: true } }),
142
+ ],
143
+ });
144
+ await setupBash.exec("git init");
145
+ await setupBash.exec("echo 'hello' > README.md");
146
+ await setupBash.exec("git add . && git commit -m 'initial'");
147
+
148
+ const originCtx = await findGitDir(originFs, "/repo");
149
+ const resolve = (url: string) => (url === "/origin" ? originCtx : null);
150
+
151
+ // Each agent gets its own filesystem + resolveRemote pointing to origin
152
+ const alice = new Bash({
153
+ fs: new InMemoryFs(),
154
+ cwd: "/repo",
155
+ customCommands: [
156
+ createGit({
157
+ identity: { name: "Alice", email: "alice@example.com", locked: true },
158
+ resolveRemote: resolve,
159
+ }),
160
+ ],
161
+ });
148
162
 
149
- git.on("post-push", (event) => {
150
- onAgentPush({ remote: event.remote, refs: event.refs });
163
+ const bob = new Bash({
164
+ fs: new InMemoryFs(),
165
+ cwd: "/repo",
166
+ customCommands: [
167
+ createGit({
168
+ identity: { name: "Bob", email: "bob@example.com", locked: true },
169
+ resolveRemote: resolve,
170
+ }),
171
+ ],
151
172
  });
173
+
174
+ await alice.exec("git clone /origin /repo");
175
+ await bob.exec("git clone /origin /repo");
176
+
177
+ // Alice and Bob work independently, push to origin, fetch each other's changes
152
178
  ```
153
179
 
154
- | Hook | Payload |
155
- | ------------------ | ------------------------------------------------ |
156
- | `post-commit` | `{ hash, message, branch, parents, author }` |
157
- | `post-merge` | `{ headHash, theirsHash, strategy, commitHash }` |
158
- | `post-checkout` | `{ prevHead, newHead, isBranchCheckout }` |
159
- | `post-push` | same payload as `pre-push` |
160
- | `post-fetch` | `{ remote, url, refsUpdated }` |
161
- | `post-clone` | `{ repository, targetPath, bare, branch }` |
162
- | `post-pull` | `{ remote, branch, strategy, commitHash }` |
163
- | `post-reset` | `{ mode, targetHash }` |
164
- | `post-clean` | `{ removed, dryRun }` |
165
- | `post-rm` | `{ removedPaths, cached }` |
166
- | `post-cherry-pick` | `{ mode, commitHash, hadConflicts }` |
167
- | `post-revert` | `{ mode, commitHash, hadConflicts }` |
168
- | `post-stash` | `{ action, ok }` |
169
-
170
- ### Low-level events
171
-
172
- Fire-and-forget events emitted on every object/ref write. Handler errors are caught and forwarded to `hooks.onError` (no-op by default).
173
-
174
- | Event | Payload |
175
- | -------------- | --------------------------- |
176
- | `ref:update` | `{ ref, oldHash, newHash }` |
177
- | `ref:delete` | `{ ref, oldHash }` |
178
- | `object:write` | `{ type, hash }` |
180
+ Concurrent pushes to the same remote are automatically serialized — if two agents push simultaneously, one succeeds and the other gets a proper non-fast-forward rejection, just like real git.
181
+
182
+ See [`examples/multi-agent.ts`](examples/multi-agent.ts) for a full working example with a coordinator agent that merges feature branches.
179
183
 
180
184
  ## Command coverage
181
185
 
182
- 34 commands implemented. See [CLI.md](CLI.md) for full usage details.
186
+ See [CLI.md](CLI.md) for full usage details.
183
187
 
184
188
  | Command | Flags / options |
185
189
  | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -221,6 +225,7 @@ Fire-and-forget events emitted on every object/ref write. Handler errors are cau
221
225
  ### Transport
222
226
 
223
227
  - **Local paths** -- direct filesystem transfer between repositories.
228
+ - **Cross-VFS** -- clone, fetch, and push between isolated in-memory filesystems via `resolveRemote`. See [Multi-agent collaboration](#multi-agent-collaboration).
224
229
  - **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.
225
230
 
226
231
  ### Internals
@@ -233,12 +238,30 @@ Fire-and-forget events emitted on every object/ref write. Handler errors are cau
233
238
  - Packfiles with zlib compression for storage and transport
234
239
  - Pathspec globs across `add`, `rm`, `diff`, `reset`, `checkout`, `restore`, `log`
235
240
 
236
- ## Goals and testing
241
+ ## Testing
242
+
243
+ Targets high fidelity to real git (2.53.0). Tested with an [oracle framework](test/oracle/README.md) that generates hundreds of randomized git workflows totaling hundreds of thousands of operations, runs them against real git, then replays each step against just-git and compares repository state and command output. State comparison covers HEAD, refs, index, worktree, active operation state, and stash. Output comparison covers exit codes, stdout, and stderr.
244
+
245
+ When backed by a real filesystem (e.g. just-bash `ReadWriteFs`), interoperable with real git on the same repo, though less extensively tested than behavioral correctness.
237
246
 
238
- High fidelity to real git (2.53.0) state and output. Tested using real git as an [oracle](test/oracle/README.md) — hundreds of randomized traces totaling hundreds of thousands of git operations, each verified step-by-step against real git's state and output.
247
+ ## Without just-bash
239
248
 
240
- When backed by a real filesystem (e.g. `just-bash` `ReadWriteFs`), interoperable with real git on the same repo, though less extensively tested than behavioral correctness.
249
+ `git.execute()` takes an args array and a `CommandContext`. Provide any `FileSystem` implementation:
250
+
251
+ ```ts
252
+ import { createGit } from "just-git";
253
+
254
+ const git = createGit({ identity: { name: "Bot", email: "bot@example.com" } });
255
+
256
+ const result = await git.execute(["init"], {
257
+ fs: myFileSystem, // any FileSystem implementation
258
+ cwd: "/repo",
259
+ env: new Map(),
260
+ stdin: "",
261
+ });
262
+
263
+ console.log(result.exitCode); // 0
264
+ ```
241
265
 
242
- ## Disclaimer
266
+ The `FileSystem` interface requires: `readFile`, `readFileBuffer`, `writeFile`, `exists`, `stat`, `mkdir`, `readdir`, `rm`. Optional: `lstat`, `readlink`, `symlink`.
243
267
 
244
- This project is not affiliated with [just-bash](https://github.com/vercel-labs/just-bash) or Vercel.
package/dist/index.d.ts CHANGED
@@ -28,10 +28,37 @@ interface FileSystem {
28
28
  symlink?(target: string, path: string): Promise<void>;
29
29
  }
30
30
 
31
+ /**
32
+ * Git object storage: compressed loose objects for new writes, with
33
+ * retained packfiles from fetch/clone. Reads check loose first,
34
+ * then fall back to pack indices.
35
+ */
36
+ declare class PackedObjectStore {
37
+ private fs;
38
+ private gitDir;
39
+ private hooks?;
40
+ private packs;
41
+ private loadedPackNames;
42
+ private discoverPromise;
43
+ constructor(fs: FileSystem, gitDir: string, hooks?: HookEmitter | undefined);
44
+ write(type: ObjectType, content: Uint8Array): Promise<ObjectId>;
45
+ read(hash: ObjectId): Promise<RawObject>;
46
+ exists(hash: ObjectId): Promise<boolean>;
47
+ ingestPack(packData: Uint8Array): Promise<number>;
48
+ /** Scan `.git/objects/pack/` for existing pack/idx pairs. */
49
+ private discover;
50
+ private doDiscover;
51
+ }
52
+
31
53
  /** 40-character lowercase hex SHA-1 hash. */
32
54
  type ObjectId = string;
33
55
  /** The four Git object types. */
34
56
  type ObjectType = "blob" | "tree" | "commit" | "tag";
57
+ /** An object as stored in .git/objects — type + raw content bytes. */
58
+ interface RawObject {
59
+ type: ObjectType;
60
+ content: Uint8Array;
61
+ }
35
62
  /** Author or committer identity with timestamp. */
36
63
  interface Identity {
37
64
  name: string;
@@ -68,6 +95,32 @@ interface Index {
68
95
  version: number;
69
96
  entries: IndexEntry[];
70
97
  }
98
+ /**
99
+ * Bundles the filesystem handle with resolved repository paths.
100
+ * Threaded through all library functions so they don't need to
101
+ * re-discover the .git directory on every call.
102
+ */
103
+ interface GitContext {
104
+ fs: FileSystem;
105
+ /** Absolute path to the .git directory. */
106
+ gitDir: string;
107
+ /** Absolute path to the working tree root, or null for bare repos. */
108
+ workTree: string | null;
109
+ /** Hook emitter for operation hooks and low-level events. */
110
+ hooks?: HookEmitter;
111
+ /** Operator-provided credential resolver (bypasses env vars). */
112
+ credentialProvider?: CredentialProvider;
113
+ /** Operator-provided identity override for author/committer. */
114
+ identityOverride?: IdentityOverride;
115
+ /** Custom fetch function for HTTP transport. Falls back to globalThis.fetch. */
116
+ fetchFn?: FetchFunction;
117
+ /** Network access policy. `false` blocks all HTTP access. */
118
+ networkPolicy?: NetworkPolicy | false;
119
+ /** Resolves remote URLs to GitContexts on potentially different VFS instances. */
120
+ resolveRemote?: RemoteResolver;
121
+ /** Cached object store instance. Lazily created by object-db. */
122
+ objectStore?: PackedObjectStore;
123
+ }
71
124
 
72
125
  type HttpAuth = {
73
126
  type: "basic";
@@ -90,6 +143,12 @@ interface IdentityOverride {
90
143
  email: string;
91
144
  locked?: boolean;
92
145
  }
146
+ /**
147
+ * Resolves a remote URL to a GitContext, enabling cross-VFS transport.
148
+ * Called before local filesystem lookup for non-HTTP URLs.
149
+ * Return null to fall back to local filesystem resolution.
150
+ */
151
+ type RemoteResolver = (url: string) => GitContext | null | Promise<GitContext | null>;
93
152
  type FetchFunction = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
94
153
  interface NetworkPolicy {
95
154
  /**
@@ -366,6 +425,12 @@ interface GitOptions {
366
425
  disabled?: GitCommandName[];
367
426
  /** Network policy. Set to `false` to block all HTTP access. */
368
427
  network?: NetworkPolicy | false;
428
+ /**
429
+ * Resolve a remote URL to a GitContext on a potentially different VFS.
430
+ * Called before local filesystem lookup for non-HTTP remote URLs.
431
+ * Return null to fall back to local filesystem resolution.
432
+ */
433
+ resolveRemote?: RemoteResolver;
369
434
  }
370
435
  /**
371
436
  * Bundle of operator-level extensions threaded into command handlers
@@ -377,6 +442,7 @@ interface GitExtensions {
377
442
  identityOverride?: IdentityOverride;
378
443
  fetchFn?: FetchFunction;
379
444
  networkPolicy?: NetworkPolicy | false;
445
+ resolveRemote?: RemoteResolver;
380
446
  }
381
447
  declare class Git {
382
448
  readonly name = "git";
@@ -392,4 +458,12 @@ declare class Git {
392
458
  }
393
459
  declare function createGit(options?: GitOptions): Git;
394
460
 
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 };
461
+ /**
462
+ * Walk up from `startPath` looking for a git repository.
463
+ * Checks for both normal repos (`.git/` subdirectory) and bare repos
464
+ * (`HEAD` + `objects/` + `refs/` directly in the directory).
465
+ * Returns a GitContext if found, null otherwise.
466
+ */
467
+ declare function findGitDir(fs: FileSystem, startPath: string): Promise<GitContext | null>;
468
+
469
+ 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 GitContext, 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, type RemoteResolver, createGit, findGitDir };