just-git 1.0.2 → 1.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/README.md +135 -59
- package/dist/hooks-1dcjVqc3.d.ts +480 -0
- package/dist/index.d.ts +22 -407
- package/dist/index.js +398 -398
- package/dist/repo/index.d.ts +264 -0
- package/dist/repo/index.js +18 -0
- package/dist/server/index.d.ts +286 -0
- package/dist/server/index.js +42 -0
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -46,7 +46,8 @@ await bash.exec("git log --oneline");
|
|
|
46
46
|
| `credentials` | `(url) => HttpAuth \| null` callback for Smart HTTP transport auth. |
|
|
47
47
|
| `disabled` | `GitCommandName[]` of subcommands to block (e.g. `["push", "rebase"]`). |
|
|
48
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
|
-
| `
|
|
49
|
+
| `hooks` | `GitHooks` config object with named callback properties. See [Hooks](#hooks). |
|
|
50
|
+
| `resolveRemote` | `(url) => GitRepo \| null` callback for cross-VFS remote resolution. See [Multi-agent collaboration](#multi-agent-collaboration). |
|
|
50
51
|
|
|
51
52
|
```ts
|
|
52
53
|
const git = createGit({
|
|
@@ -57,80 +58,107 @@ const git = createGit({
|
|
|
57
58
|
});
|
|
58
59
|
```
|
|
59
60
|
|
|
60
|
-
##
|
|
61
|
+
## Hooks
|
|
62
|
+
|
|
63
|
+
Hooks fire at specific points inside command execution. Specified as a `GitHooks` config object at construction time. All hook event payloads include `repo: GitRepo`, providing access to the [repo module helpers](src/repo/) inside hooks.
|
|
61
64
|
|
|
62
|
-
|
|
65
|
+
Pre-hooks can reject the operation by returning `{ reject: true, message? }`. Post-hooks are observational — return value is ignored.
|
|
63
66
|
|
|
64
67
|
```ts
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const result = await next();
|
|
68
|
-
auditLog.push({ command: `git ${event.command}`, exitCode: result.exitCode });
|
|
69
|
-
return result;
|
|
70
|
-
});
|
|
68
|
+
import { createGit, type GitHooks } from "just-git";
|
|
69
|
+
import { getChangedFiles } from "just-git/repo";
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
71
|
+
const git = createGit({
|
|
72
|
+
hooks: {
|
|
73
|
+
// Block secrets from being committed
|
|
74
|
+
preCommit: ({ index }) => {
|
|
75
|
+
const forbidden = index.entries.filter((e) => /\.(env|pem|key)$/.test(e.path));
|
|
76
|
+
if (forbidden.length) {
|
|
77
|
+
return { reject: true, message: `Blocked: ${forbidden.map((e) => e.path).join(", ")}` };
|
|
78
|
+
}
|
|
79
|
+
},
|
|
79
80
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
// Enforce conventional commit messages
|
|
82
|
+
commitMsg: (event) => {
|
|
83
|
+
if (!/^(feat|fix|docs|refactor|test|chore)(\(.+\))?:/.test(event.message)) {
|
|
84
|
+
return { reject: true, message: "Commit message must follow conventional commits format" };
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// Feed agent activity to your UI — with changed file list
|
|
89
|
+
postCommit: async ({ repo, hash, branch, parents }) => {
|
|
90
|
+
const files = await getChangedFiles(repo, parents[0] ?? null, hash);
|
|
91
|
+
onAgentCommit({ hash, branch, changedFiles: files });
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// Audit log — record every command
|
|
95
|
+
afterCommand: ({ command, args, result }) => {
|
|
96
|
+
auditLog.push({ command: `git ${command}`, exitCode: result.exitCode });
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// Gate pushes on human approval
|
|
100
|
+
beforeCommand: async ({ command }) => {
|
|
101
|
+
if (command === "push" && !(await getHumanApproval())) {
|
|
102
|
+
return { reject: true, message: "Push blocked — awaiting approval." };
|
|
88
103
|
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return next();
|
|
104
|
+
},
|
|
105
|
+
},
|
|
92
106
|
});
|
|
93
107
|
```
|
|
94
108
|
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
Pre-hooks can abort the operation by returning `{ abort: true, message? }`. Post-hooks are observational — return value is ignored.
|
|
109
|
+
Use `composeGitHooks()` to combine multiple hook sets:
|
|
100
110
|
|
|
101
111
|
```ts
|
|
102
|
-
|
|
103
|
-
git.on("pre-commit", (event) => {
|
|
104
|
-
const forbidden = event.index.entries.filter((e) => /\.(env|pem|key)$/.test(e.path));
|
|
105
|
-
if (forbidden.length) {
|
|
106
|
-
return { abort: true, message: `Blocked: ${forbidden.map((e) => e.path).join(", ")}` };
|
|
107
|
-
}
|
|
108
|
-
});
|
|
112
|
+
import { createGit, composeGitHooks } from "just-git";
|
|
109
113
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (!/^(feat|fix|docs|refactor|test|chore)(\(.+\))?:/.test(event.message)) {
|
|
113
|
-
return { abort: true, message: "Commit message must follow conventional commits format" };
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// Feed agent activity to your UI or orchestration layer
|
|
118
|
-
git.on("post-commit", (event) => {
|
|
119
|
-
onAgentCommit({ hash: event.hash, branch: event.branch, message: event.message });
|
|
114
|
+
const git = createGit({
|
|
115
|
+
hooks: composeGitHooks(auditHooks, policyHooks, loggingHooks),
|
|
120
116
|
});
|
|
121
117
|
```
|
|
122
118
|
|
|
123
|
-
Available pre-hooks: `
|
|
119
|
+
Available pre-hooks: `preCommit`, `commitMsg`, `mergeMsg`, `preMergeCommit`, `preCheckout`, `prePush`, `preFetch`, `preClone`, `prePull`, `preRebase`, `preReset`, `preClean`, `preRm`, `preCherryPick`, `preRevert`, `preStash`. Available post-hooks: `postCommit`, `postMerge`, `postCheckout`, `postPush`, `postFetch`, `postClone`, `postPull`, `postReset`, `postClean`, `postRm`, `postCherryPick`, `postRevert`, `postStash`. Low-level events: `onRefUpdate`, `onRefDelete`, `onObjectWrite`. Command-level: `beforeCommand`, `afterCommand`.
|
|
124
120
|
|
|
125
|
-
|
|
121
|
+
## Repo module
|
|
122
|
+
|
|
123
|
+
`just-git/repo` provides a high-level API for working with repositories programmatically — reading commits, diffing trees, creating objects, and merging — without going through command execution. This is what you use inside hooks (all hook payloads include `repo: GitRepo`) and anywhere else you need direct repo access.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import {
|
|
127
|
+
readCommit,
|
|
128
|
+
readFileAtCommit,
|
|
129
|
+
getChangedFiles,
|
|
130
|
+
resolveRef,
|
|
131
|
+
listBranches,
|
|
132
|
+
diffTrees,
|
|
133
|
+
flattenTree,
|
|
134
|
+
createCommit,
|
|
135
|
+
writeBlob,
|
|
136
|
+
writeTree,
|
|
137
|
+
mergeTrees,
|
|
138
|
+
checkoutTo,
|
|
139
|
+
readonlyRepo,
|
|
140
|
+
} from "just-git/repo";
|
|
141
|
+
|
|
142
|
+
// Read a file at a specific commit
|
|
143
|
+
const content = await readFileAtCommit(repo, commitHash, "src/index.ts");
|
|
144
|
+
|
|
145
|
+
// Diff two commits
|
|
146
|
+
const changes = await getChangedFiles(repo, parentHash, commitHash);
|
|
147
|
+
|
|
148
|
+
// Merge two branches at the tree level (no worktree needed)
|
|
149
|
+
const result = await mergeTrees(repo, oursCommit, theirsCommit);
|
|
150
|
+
if (!result.clean) console.log("conflicts:", result.conflicts);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Also re-exports `PackedObjectStore` and `FileSystemRefStore` for custom storage backends, and `readonlyRepo()` to wrap a repo so all write operations throw.
|
|
126
154
|
|
|
127
155
|
## Multi-agent collaboration
|
|
128
156
|
|
|
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 `
|
|
157
|
+
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 `GitRepo` instances (any object/ref store — VFS-backed, SQLite, etc.), so clone/fetch/push/pull cross VFS boundaries without any network or shared filesystem.
|
|
130
158
|
|
|
131
159
|
```ts
|
|
132
160
|
import { Bash, InMemoryFs } from "just-bash";
|
|
133
|
-
import { createGit,
|
|
161
|
+
import { createGit, findRepo } from "just-git";
|
|
134
162
|
|
|
135
163
|
// Origin repo on its own filesystem
|
|
136
164
|
const originFs = new InMemoryFs();
|
|
@@ -145,17 +173,13 @@ await setupBash.exec("git init");
|
|
|
145
173
|
await setupBash.exec("echo 'hello' > README.md");
|
|
146
174
|
await setupBash.exec("git add . && git commit -m 'initial'");
|
|
147
175
|
|
|
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
176
|
const alice = new Bash({
|
|
153
177
|
fs: new InMemoryFs(),
|
|
154
178
|
cwd: "/repo",
|
|
155
179
|
customCommands: [
|
|
156
180
|
createGit({
|
|
157
181
|
identity: { name: "Alice", email: "alice@example.com", locked: true },
|
|
158
|
-
resolveRemote:
|
|
182
|
+
resolveRemote: () => findRepo(originFs, "/repo"),
|
|
159
183
|
}),
|
|
160
184
|
],
|
|
161
185
|
});
|
|
@@ -166,7 +190,7 @@ const bob = new Bash({
|
|
|
166
190
|
customCommands: [
|
|
167
191
|
createGit({
|
|
168
192
|
identity: { name: "Bob", email: "bob@example.com", locked: true },
|
|
169
|
-
resolveRemote:
|
|
193
|
+
resolveRemote: () => findRepo(originFs, "/repo"),
|
|
170
194
|
}),
|
|
171
195
|
],
|
|
172
196
|
});
|
|
@@ -181,6 +205,43 @@ Concurrent pushes to the same remote are automatically serialized — if two age
|
|
|
181
205
|
|
|
182
206
|
See [`examples/multi-agent.ts`](examples/multi-agent.ts) for a full working example with a coordinator agent that merges feature branches.
|
|
183
207
|
|
|
208
|
+
## Server
|
|
209
|
+
|
|
210
|
+
`just-git/server` is an embeddable Git Smart HTTP server. Any standard git client can clone, fetch, and push. Uses web-standard `Request`/`Response` — works with Bun, Hono, Cloudflare Workers, or any fetch-compatible runtime.
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
import { createGitServer, SqliteStorage } from "just-git/server";
|
|
214
|
+
import { getChangedFiles } from "just-git/repo";
|
|
215
|
+
import { Database } from "bun:sqlite";
|
|
216
|
+
|
|
217
|
+
const storage = new SqliteStorage(new Database("repos.sqlite"));
|
|
218
|
+
|
|
219
|
+
const server = createGitServer({
|
|
220
|
+
resolveRepo: async (repoPath) => storage.repo(repoPath),
|
|
221
|
+
hooks: {
|
|
222
|
+
preReceive: async ({ updates }) => {
|
|
223
|
+
for (const u of updates) {
|
|
224
|
+
if (u.ref === "refs/heads/main" && !u.isFF && !u.isCreate) {
|
|
225
|
+
return { reject: true, message: "no force-push to main" };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
postReceive: async ({ repo, updates }) => {
|
|
230
|
+
for (const u of updates) {
|
|
231
|
+
const files = await getChangedFiles(repo, u.oldHash, u.newHash);
|
|
232
|
+
console.log(`${u.ref}: ${files.length} files changed`);
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
Bun.serve({ fetch: server.fetch });
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`SqliteStorage` persists repos in SQLite without a filesystem — works with `bun:sqlite`, `better-sqlite3`, or any compatible driver. Repos backed by `SqliteStorage` work with both the server (external HTTP) and `resolveRemote` (in-process), with CAS-protected ref updates ensuring correctness regardless of write path.
|
|
242
|
+
|
|
243
|
+
See [SERVER.md](SERVER.md) for the full API: hooks, `createStandardHooks`, `SqliteStorage`, configuration, and deployment patterns. See [`examples/sqlite-server.ts`](examples/sqlite-server.ts) for a runnable SQLite server and [`examples/server.ts`](examples/server.ts) for a VFS-backed server with virtual client. See [`src/platform/`](src/platform/) for a reference implementation that builds GitHub-like functionality (repos, pull requests, merge strategies) on top of these primitives.
|
|
244
|
+
|
|
184
245
|
## Command coverage
|
|
185
246
|
|
|
186
247
|
See [CLI.md](CLI.md) for full usage details.
|
|
@@ -264,3 +325,18 @@ console.log(result.exitCode); // 0
|
|
|
264
325
|
```
|
|
265
326
|
|
|
266
327
|
The `FileSystem` interface requires: `readFile`, `readFileBuffer`, `writeFile`, `exists`, `stat`, `mkdir`, `readdir`, `rm`. Optional: `lstat`, `readlink`, `symlink`.
|
|
328
|
+
|
|
329
|
+
## Examples
|
|
330
|
+
|
|
331
|
+
Runnable examples in [`examples/`](examples/):
|
|
332
|
+
|
|
333
|
+
| File | What it demonstrates |
|
|
334
|
+
| --------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
335
|
+
| [`usage.ts`](examples/usage.ts) | Identity, disabled commands, hooks, compose, full sandbox setup |
|
|
336
|
+
| [`multi-agent.ts`](examples/multi-agent.ts) | Cross-VFS collaboration with clone/push/pull between isolated agents |
|
|
337
|
+
| [`server.ts`](examples/server.ts) | VFS-backed Smart HTTP server with virtual client clone and push |
|
|
338
|
+
| [`sqlite-server.ts`](examples/sqlite-server.ts) | SQLite-backed server with auto-creating repos, works with real `git` |
|
|
339
|
+
| [`platform-server.ts`](examples/platform-server.ts) | GitHub-like PR workflows: create, merge, close via REST API |
|
|
340
|
+
| [`agent-remote-workflow.ts`](examples/agent-remote-workflow.ts) | Clone from GitHub, work in sandbox, push back (requires token) |
|
|
341
|
+
|
|
342
|
+
Run any example with `bun examples/<file>`.
|