@trebired/git-host 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.
Files changed (212) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +451 -0
  4. package/dist/api/handler/action.d.ts +5 -0
  5. package/dist/api/handler/action.d.ts.map +1 -0
  6. package/dist/api/handler/action.js +47 -0
  7. package/dist/api/handler/action.js.map +1 -0
  8. package/dist/api/handler/response.d.ts +34 -0
  9. package/dist/api/handler/response.d.ts.map +1 -0
  10. package/dist/api/handler/response.js +87 -0
  11. package/dist/api/handler/response.js.map +1 -0
  12. package/dist/api/handler/route.d.ts +15 -0
  13. package/dist/api/handler/route.d.ts.map +1 -0
  14. package/dist/api/handler/route.js +51 -0
  15. package/dist/api/handler/route.js.map +1 -0
  16. package/dist/api/handler.d.ts +6 -0
  17. package/dist/api/handler.d.ts.map +1 -0
  18. package/dist/api/handler.js +117 -0
  19. package/dist/api/handler.js.map +1 -0
  20. package/dist/constants.d.ts +8 -0
  21. package/dist/constants.d.ts.map +1 -0
  22. package/dist/constants.js +16 -0
  23. package/dist/constants.js.map +1 -0
  24. package/dist/core/create_git_host/branch_methods.d.ts +5 -0
  25. package/dist/core/create_git_host/branch_methods.d.ts.map +1 -0
  26. package/dist/core/create_git_host/branch_methods.js +137 -0
  27. package/dist/core/create_git_host/branch_methods.js.map +1 -0
  28. package/dist/core/create_git_host/content_methods.d.ts +5 -0
  29. package/dist/core/create_git_host/content_methods.d.ts.map +1 -0
  30. package/dist/core/create_git_host/content_methods.js +24 -0
  31. package/dist/core/create_git_host/content_methods.js.map +1 -0
  32. package/dist/core/create_git_host/remote_methods.d.ts +5 -0
  33. package/dist/core/create_git_host/remote_methods.d.ts.map +1 -0
  34. package/dist/core/create_git_host/remote_methods.js +48 -0
  35. package/dist/core/create_git_host/remote_methods.js.map +1 -0
  36. package/dist/core/create_git_host/shared.d.ts +21 -0
  37. package/dist/core/create_git_host/shared.d.ts.map +1 -0
  38. package/dist/core/create_git_host/shared.js +17 -0
  39. package/dist/core/create_git_host/shared.js.map +1 -0
  40. package/dist/core/create_git_host/working_tree_methods.d.ts +5 -0
  41. package/dist/core/create_git_host/working_tree_methods.d.ts.map +1 -0
  42. package/dist/core/create_git_host/working_tree_methods.js +63 -0
  43. package/dist/core/create_git_host/working_tree_methods.js.map +1 -0
  44. package/dist/core/create_git_host.d.ts +4 -0
  45. package/dist/core/create_git_host.d.ts.map +1 -0
  46. package/dist/core/create_git_host.js +167 -0
  47. package/dist/core/create_git_host.js.map +1 -0
  48. package/dist/core/inspect/helpers.d.ts +27 -0
  49. package/dist/core/inspect/helpers.d.ts.map +1 -0
  50. package/dist/core/inspect/helpers.js +86 -0
  51. package/dist/core/inspect/helpers.js.map +1 -0
  52. package/dist/core/inspect.d.ts +17 -0
  53. package/dist/core/inspect.d.ts.map +1 -0
  54. package/dist/core/inspect.js +174 -0
  55. package/dist/core/inspect.js.map +1 -0
  56. package/dist/core/locks.d.ts +5 -0
  57. package/dist/core/locks.d.ts.map +1 -0
  58. package/dist/core/locks.js +27 -0
  59. package/dist/core/locks.js.map +1 -0
  60. package/dist/core/operation_state.d.ts +4 -0
  61. package/dist/core/operation_state.d.ts.map +1 -0
  62. package/dist/core/operation_state.js +51 -0
  63. package/dist/core/operation_state.js.map +1 -0
  64. package/dist/core/remote.d.ts +13 -0
  65. package/dist/core/remote.d.ts.map +1 -0
  66. package/dist/core/remote.js +187 -0
  67. package/dist/core/remote.js.map +1 -0
  68. package/dist/core/repository/parsers.d.ts +12 -0
  69. package/dist/core/repository/parsers.d.ts.map +1 -0
  70. package/dist/core/repository/parsers.js +181 -0
  71. package/dist/core/repository/parsers.js.map +1 -0
  72. package/dist/core/repository.d.ts +11 -0
  73. package/dist/core/repository.d.ts.map +1 -0
  74. package/dist/core/repository.js +81 -0
  75. package/dist/core/repository.js.map +1 -0
  76. package/dist/core/run_git/env.d.ts +7 -0
  77. package/dist/core/run_git/env.d.ts.map +1 -0
  78. package/dist/core/run_git/env.js +18 -0
  79. package/dist/core/run_git/env.js.map +1 -0
  80. package/dist/core/run_git/process.d.ts +13 -0
  81. package/dist/core/run_git/process.d.ts.map +1 -0
  82. package/dist/core/run_git/process.js +96 -0
  83. package/dist/core/run_git/process.js.map +1 -0
  84. package/dist/core/run_git/repository_setup.d.ts +20 -0
  85. package/dist/core/run_git/repository_setup.d.ts.map +1 -0
  86. package/dist/core/run_git/repository_setup.js +116 -0
  87. package/dist/core/run_git/repository_setup.js.map +1 -0
  88. package/dist/core/run_git.d.ts +6 -0
  89. package/dist/core/run_git.d.ts.map +1 -0
  90. package/dist/core/run_git.js +6 -0
  91. package/dist/core/run_git.js.map +1 -0
  92. package/dist/core/working_tree/mutate.d.ts +9 -0
  93. package/dist/core/working_tree/mutate.d.ts.map +1 -0
  94. package/dist/core/working_tree/mutate.js +159 -0
  95. package/dist/core/working_tree/mutate.js.map +1 -0
  96. package/dist/core/working_tree/read.d.ts +6 -0
  97. package/dist/core/working_tree/read.d.ts.map +1 -0
  98. package/dist/core/working_tree/read.js +108 -0
  99. package/dist/core/working_tree/read.js.map +1 -0
  100. package/dist/core/working_tree/shared.d.ts +20 -0
  101. package/dist/core/working_tree/shared.d.ts.map +1 -0
  102. package/dist/core/working_tree/shared.js +87 -0
  103. package/dist/core/working_tree/shared.js.map +1 -0
  104. package/dist/core/working_tree.d.ts +3 -0
  105. package/dist/core/working_tree.d.ts.map +1 -0
  106. package/dist/core/working_tree.js +3 -0
  107. package/dist/core/working_tree.js.map +1 -0
  108. package/dist/errors.d.ts +10 -0
  109. package/dist/errors.d.ts.map +1 -0
  110. package/dist/errors.js +13 -0
  111. package/dist/errors.js.map +1 -0
  112. package/dist/http/handler/helpers.d.ts +35 -0
  113. package/dist/http/handler/helpers.d.ts.map +1 -0
  114. package/dist/http/handler/helpers.js +146 -0
  115. package/dist/http/handler/helpers.js.map +1 -0
  116. package/dist/http/handler.d.ts +6 -0
  117. package/dist/http/handler.d.ts.map +1 -0
  118. package/dist/http/handler.js +208 -0
  119. package/dist/http/handler.js.map +1 -0
  120. package/dist/index.d.ts +18 -0
  121. package/dist/index.d.ts.map +1 -0
  122. package/dist/index.js +15 -0
  123. package/dist/index.js.map +1 -0
  124. package/dist/logging.d.ts +4 -0
  125. package/dist/logging.d.ts.map +1 -0
  126. package/dist/logging.js +134 -0
  127. package/dist/logging.js.map +1 -0
  128. package/dist/react/client/error.d.ts +14 -0
  129. package/dist/react/client/error.d.ts.map +1 -0
  130. package/dist/react/client/error.js +32 -0
  131. package/dist/react/client/error.js.map +1 -0
  132. package/dist/react/client/helpers.d.ts +12 -0
  133. package/dist/react/client/helpers.d.ts.map +1 -0
  134. package/dist/react/client/helpers.js +45 -0
  135. package/dist/react/client/helpers.js.map +1 -0
  136. package/dist/react/client/types.d.ts +61 -0
  137. package/dist/react/client/types.d.ts.map +1 -0
  138. package/dist/react/client/types.js +2 -0
  139. package/dist/react/client/types.js.map +1 -0
  140. package/dist/react/client.d.ts +6 -0
  141. package/dist/react/client.d.ts.map +1 -0
  142. package/dist/react/client.js +115 -0
  143. package/dist/react/client.js.map +1 -0
  144. package/dist/react/hooks/query.d.ts +10 -0
  145. package/dist/react/hooks/query.d.ts.map +1 -0
  146. package/dist/react/hooks/query.js +74 -0
  147. package/dist/react/hooks/query.js.map +1 -0
  148. package/dist/react/hooks/resources.d.ts +11 -0
  149. package/dist/react/hooks/resources.d.ts.map +1 -0
  150. package/dist/react/hooks/resources.js +113 -0
  151. package/dist/react/hooks/resources.js.map +1 -0
  152. package/dist/react/hooks/types.d.ts +40 -0
  153. package/dist/react/hooks/types.d.ts.map +1 -0
  154. package/dist/react/hooks/types.js +2 -0
  155. package/dist/react/hooks/types.js.map +1 -0
  156. package/dist/react/hooks.d.ts +4 -0
  157. package/dist/react/hooks.d.ts.map +1 -0
  158. package/dist/react/hooks.js +3 -0
  159. package/dist/react/hooks.js.map +1 -0
  160. package/dist/react/index.d.ts +5 -0
  161. package/dist/react/index.d.ts.map +1 -0
  162. package/dist/react/index.js +3 -0
  163. package/dist/react/index.js.map +1 -0
  164. package/dist/ssh/keys.d.ts +18 -0
  165. package/dist/ssh/keys.d.ts.map +1 -0
  166. package/dist/ssh/keys.js +41 -0
  167. package/dist/ssh/keys.js.map +1 -0
  168. package/dist/ssh/server/audit.d.ts +5 -0
  169. package/dist/ssh/server/audit.d.ts.map +1 -0
  170. package/dist/ssh/server/audit.js +21 -0
  171. package/dist/ssh/server/audit.js.map +1 -0
  172. package/dist/ssh/server/shared.d.ts +30 -0
  173. package/dist/ssh/server/shared.d.ts.map +1 -0
  174. package/dist/ssh/server/shared.js +93 -0
  175. package/dist/ssh/server/shared.js.map +1 -0
  176. package/dist/ssh/server.d.ts +5 -0
  177. package/dist/ssh/server.d.ts.map +1 -0
  178. package/dist/ssh/server.js +265 -0
  179. package/dist/ssh/server.js.map +1 -0
  180. package/dist/types/common.d.ts +32 -0
  181. package/dist/types/common.d.ts.map +1 -0
  182. package/dist/types/common.js +2 -0
  183. package/dist/types/common.js.map +1 -0
  184. package/dist/types/host.d.ts +142 -0
  185. package/dist/types/host.d.ts.map +1 -0
  186. package/dist/types/host.js +2 -0
  187. package/dist/types/host.js.map +1 -0
  188. package/dist/types/index.d.ts +5 -0
  189. package/dist/types/index.d.ts.map +1 -0
  190. package/dist/types/index.js +2 -0
  191. package/dist/types/index.js.map +1 -0
  192. package/dist/types/repository.d.ts +163 -0
  193. package/dist/types/repository.d.ts.map +1 -0
  194. package/dist/types/repository.js +2 -0
  195. package/dist/types/repository.js.map +1 -0
  196. package/dist/types/transports.d.ts +156 -0
  197. package/dist/types/transports.d.ts.map +1 -0
  198. package/dist/types/transports.js +2 -0
  199. package/dist/types/transports.js.map +1 -0
  200. package/dist/types.d.ts +2 -0
  201. package/dist/types.d.ts.map +1 -0
  202. package/dist/types.js +2 -0
  203. package/dist/types.js.map +1 -0
  204. package/dist/utils/paths.d.ts +10 -0
  205. package/dist/utils/paths.d.ts.map +1 -0
  206. package/dist/utils/paths.js +51 -0
  207. package/dist/utils/paths.js.map +1 -0
  208. package/dist/utils/text.d.ts +4 -0
  209. package/dist/utils/text.d.ts.map +1 -0
  210. package/dist/utils/text.js +10 -0
  211. package/dist/utils/text.js.map +1 -0
  212. package/package.json +81 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@trebired/git-host` will be documented here.
4
+
5
+ This project follows semantic versioning once published.
6
+
7
+ ## 0.1.0
8
+
9
+ - Added the initial `@trebired/git-host` package scaffold with publishable metadata, README, MIT license, contribution guide, and TypeScript build setup.
10
+ - Added a reusable core `createGitHost()` API for worktree-backed repositories with real Git CLI execution, repository summary reads, tree and blob inspection, commit and ref comparison reads, working-tree mutation helpers, branch operations, checkout, remote fetch/pull/push helpers, and per-repository mutation locking.
11
+ - Added `createGitHttpHandler()` for plain Node smart HTTP hosting with host-owned repository resolution and optional authorization hooks.
12
+ - Added `createGitSshServer()` for plain Node SSH Git hosting with host-owned public key authentication, repository resolution, authorization hooks, and Git-only command execution.
13
+ - Added `createGitApiHandler()` for plain Node JSON API routing over repository summaries, branches, commits, trees, blobs, and diffs.
14
+ - Added `@trebired/git-host/react` as an optional React companion with a typed JSON API client, provider, and headless data hooks.
15
+ - Added hosted transport identity and audit hook support for smart HTTP and SSH adapters.
16
+ - Added `checkoutRef()`, `readStagedFile()`, and `readUnstagedFile()` to the core host API for detached ref checkout and pre-commit file inspection.
17
+ - Added host-owned remote transport auth ergonomics for clone, fetch, pull, and push through `remoteCredentials`, `httpHeaders`, and `sshCommand` options.
18
+ - Added SSH key utilities for generation, normalization, comparison, and fingerprinting.
19
+ - Added `@trebired/logger`-style logger support across the main git-host entrypoints with optional verbose diagnostics.
20
+ - Added hosted repository config defaults so worktree-backed repositories can accept smart HTTP push updates through the checked-out branch.
21
+ - Added tests covering repository init, clone, summary reads, tree and blob reads, staged and unstaged file reads, commit detail reads, working-tree staging and commit flows, branch operations, checkout, detached ref checkout, ref comparison, fetch/pull/push helpers, authenticated HTTP remote sync, operation continue and abort, JSON API reads, smart HTTP clone/push, smart HTTP auth hooks, SSH clone/push, SSH audit hooks, locking, and path rejection.
22
+ - Initial public release.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Miroslav M. and Trebired contributors
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,451 @@
1
+ # @trebired/git-host
2
+
3
+ Embeddable Git host for Node.js and Bun apps.
4
+
5
+ `@trebired/git-host` gives your app real Git repository operations and real Git transports without making you adopt a full forge product. It runs the real Git CLI, helps you resolve repository paths safely, serializes mutations per repository, and exposes a reusable API for repository initialization, summary reads, content inspection, branch operations, working-tree changes, remote sync helpers, JSON API handlers, and smart HTTP and SSH hosting.
6
+
7
+ It is aimed at platforms and products that already own users, permissions, tokens, repository records, and UI, but want to stop hand-rolling the Git layer underneath all of that.
8
+
9
+ The package keeps auth, permission, and persistence decisions host-owned while giving you reusable Git behavior and transport adapters.
10
+
11
+ It also exposes an optional React companion at `@trebired/git-host/react` for typed API clients, providers, and headless data hooks on top of the JSON API.
12
+
13
+ In plain terms:
14
+
15
+ - it is a Git hosting layer you embed into your app
16
+ - it is not a full Git forge like GitLab, Gitea, or Forgejo
17
+ - it is not a reimplementation of Git
18
+ - it uses the real `git` binary for the hard parts
19
+
20
+ ## Install
21
+
22
+ Runtime support: Bun 1+ and Node.js 18+.
23
+
24
+ ```sh
25
+ npm install @trebired/git-host
26
+ ```
27
+
28
+ ```sh
29
+ npm install @trebired/logger
30
+ ```
31
+
32
+ Optional React companion:
33
+
34
+ ```sh
35
+ npm install react
36
+ ```
37
+
38
+ ```ts
39
+ import { createGitHost, resolveRepositoryPath } from "@trebired/git-host";
40
+ import { createLog } from "@trebired/logger";
41
+
42
+ const log = createLog({
43
+ console: true,
44
+ quiet: true,
45
+ save: false,
46
+ });
47
+
48
+ const repositoriesRoot = "/srv/git-workspaces";
49
+
50
+ const gitHost = createGitHost({
51
+ logger: log,
52
+ resolveRepository(repositoryId) {
53
+ return {
54
+ id: repositoryId,
55
+ path: resolveRepositoryPath({
56
+ rootDir: repositoriesRoot,
57
+ repositoryPath: `${repositoryId}/workspace`,
58
+ }),
59
+ };
60
+ },
61
+ });
62
+
63
+ await gitHost.ensureRepository("demo", {
64
+ actor: {
65
+ name: "Alice",
66
+ email: "alice@example.com",
67
+ },
68
+ });
69
+
70
+ const summary = await gitHost.readSummary("demo");
71
+ console.log(summary.repository.current_branch);
72
+
73
+ const workingTree = await gitHost.readWorkingTree("demo");
74
+ console.log(workingTree.unstaged_entries);
75
+
76
+ await gitHost.fetch("demo", {
77
+ remoteCredentials: {
78
+ username: "git-user",
79
+ password: process.env.GIT_TOKEN || "",
80
+ },
81
+ });
82
+ ```
83
+
84
+ Smart HTTP hosting:
85
+
86
+ ```ts
87
+ import { createServer } from "node:http";
88
+ import { createGitHost, createGitHttpHandler } from "@trebired/git-host";
89
+ import { createLog } from "@trebired/logger";
90
+
91
+ const log = createLog({
92
+ console: true,
93
+ quiet: true,
94
+ save: false,
95
+ });
96
+
97
+ const gitHost = createGitHost({
98
+ resolveRepository(repositoryId) {
99
+ return {
100
+ id: repositoryId,
101
+ path: `/srv/git-workspaces/${repositoryId}/workspace`,
102
+ };
103
+ },
104
+ });
105
+
106
+ const server = createServer(createGitHttpHandler({
107
+ basePath: "/git",
108
+ logger: log,
109
+ resolveRepository(repositoryKey) {
110
+ return {
111
+ id: repositoryKey,
112
+ path: `/srv/git-workspaces/${repositoryKey}/workspace`,
113
+ };
114
+ },
115
+ }));
116
+
117
+ server.listen(3000);
118
+ ```
119
+
120
+ Then clients can use:
121
+
122
+ ```sh
123
+ git clone http://127.0.0.1:3000/git/demo.git
124
+ git push
125
+ ```
126
+
127
+ SSH hosting:
128
+
129
+ ```ts
130
+ import { createGitSshServer } from "@trebired/git-host";
131
+ import { createLog } from "@trebired/logger";
132
+
133
+ const log = createLog({
134
+ console: true,
135
+ quiet: true,
136
+ save: false,
137
+ });
138
+
139
+ const sshServer = createGitSshServer({
140
+ hostKeys: [hostPrivateKeyPem],
141
+ logger: log,
142
+ authenticate({ publicKey, username }) {
143
+ if (username !== "git") return null;
144
+ const account = findAccountBySshPublicKey(publicKey);
145
+ if (!account) return null;
146
+ return {
147
+ publicKey: account.publicKey,
148
+ remoteUser: account.username,
149
+ identity: account,
150
+ };
151
+ },
152
+ resolveRepository(repositoryKey) {
153
+ return {
154
+ id: repositoryKey,
155
+ path: `/srv/git-workspaces/${repositoryKey}/workspace`,
156
+ };
157
+ },
158
+ });
159
+
160
+ sshServer.listen(2222, "0.0.0.0");
161
+ ```
162
+
163
+ Then clients can use:
164
+
165
+ ```sh
166
+ git clone ssh://git@127.0.0.1:2222/demo.git
167
+ git push
168
+ ```
169
+
170
+ JSON API hosting:
171
+
172
+ ```ts
173
+ import { createServer } from "node:http";
174
+ import { createGitApiHandler, createGitHost } from "@trebired/git-host";
175
+ import { createLog } from "@trebired/logger";
176
+
177
+ const log = createLog({
178
+ console: true,
179
+ quiet: true,
180
+ save: false,
181
+ });
182
+
183
+ const gitHost = createGitHost({
184
+ resolveRepository(repositoryId) {
185
+ return {
186
+ id: repositoryId,
187
+ path: `/srv/git-workspaces/${repositoryId}/workspace`,
188
+ };
189
+ },
190
+ });
191
+
192
+ const apiServer = createServer(createGitApiHandler({
193
+ basePath: "/api/git",
194
+ gitHost,
195
+ logger: log,
196
+ authorize({ action, repositoryId }) {
197
+ return canReadRepository(repositoryId, action);
198
+ },
199
+ }));
200
+
201
+ apiServer.listen(3100);
202
+ ```
203
+
204
+ Then apps can use routes like:
205
+
206
+ ```txt
207
+ GET /api/git/repositories/demo/summary
208
+ GET /api/git/repositories/demo/branches
209
+ GET /api/git/repositories/demo/commits?limit=20
210
+ GET /api/git/repositories/demo/commits/<commit-ref>
211
+ GET /api/git/repositories/demo/tree?ref=HEAD&path=src
212
+ GET /api/git/repositories/demo/blob?ref=HEAD&path=README.md
213
+ GET /api/git/repositories/demo/diff?baseRef=main&headRef=feature%2Fx
214
+ ```
215
+
216
+ React companion:
217
+
218
+ ```ts
219
+ import { createGitApiClient, GitApiClientProvider, useGitRepositorySummary } from "@trebired/git-host/react";
220
+
221
+ const gitClient = createGitApiClient({
222
+ baseUrl: "/api/git",
223
+ });
224
+
225
+ function RepositorySummaryCard() {
226
+ const summary = useGitRepositorySummary("demo");
227
+
228
+ if (summary.loading) return "Loading...";
229
+ if (summary.error) return summary.error.message;
230
+ if (!summary.data) return "Missing repository";
231
+
232
+ return `${summary.data.repository.current_branch} @ ${summary.data.repository.head_short}`;
233
+ }
234
+
235
+ function App() {
236
+ return (
237
+ <GitApiClientProvider client={gitClient}>
238
+ <RepositorySummaryCard />
239
+ </GitApiClientProvider>
240
+ );
241
+ }
242
+ ```
243
+
244
+ The React entry is intentionally headless. It helps apps fetch and mutate Git data consistently, but it does not ship a bundled styled UI.
245
+
246
+ ## Current API
247
+
248
+ The first public slice is intentionally small:
249
+
250
+ - `createGitHost()`
251
+ - `resolveRepositoryPath()`
252
+ - `runGit()`
253
+ - `buildGitEnv()`
254
+ - `RepositoryLockManager`
255
+ - `createGitApiHandler()`
256
+ - `createGitHttpHandler()`
257
+ - `generateSshKeyPair()`
258
+ - `normalizeSshPublicKey()`
259
+ - `compareSshPublicKeys()`
260
+ - `fingerprintSshPublicKey()`
261
+ - `createGitSshServer()`
262
+ - `@trebired/git-host/react`
263
+
264
+ And the main host instance methods:
265
+
266
+ - `ensureRepository()`
267
+ - `readSummary()`
268
+ - `listBranches()`
269
+ - `listCommits()`
270
+ - `listTree()`
271
+ - `readBlob()`
272
+ - `readCommit()`
273
+ - `diff()`
274
+ - `readWorkingTree()`
275
+ - `readStagedFile()`
276
+ - `readUnstagedFile()`
277
+ - `createBranch()`
278
+ - `checkoutBranch()`
279
+ - `checkoutRef()`
280
+ - `deleteBranch()`
281
+ - `stagePaths()`
282
+ - `unstagePaths()`
283
+ - `discardPaths()`
284
+ - `commit()`
285
+ - `continueOperation()`
286
+ - `abortOperation()`
287
+ - `fetch()`
288
+ - `pull()`
289
+ - `push()`
290
+ - `withRepositoryLock()`
291
+
292
+ The React entry currently exports:
293
+
294
+ - `createGitApiClient()`
295
+ - `GitApiClientProvider`
296
+ - `useGitRepositorySummary()`
297
+ - `useGitBranches()`
298
+ - `useGitCommits()`
299
+ - `useGitCommit()`
300
+ - `useGitTree()`
301
+ - `useGitBlob()`
302
+ - `useGitDiff()`
303
+ - `useGitApiQuery()`
304
+
305
+ ## Repository Model
306
+
307
+ This package does not own your app database.
308
+
309
+ Your app resolves a repository id to an absolute repository path. The package then runs Git operations against that path. This keeps repository metadata, permissions, tokens, SSH keys, and UI decisions inside the host app where they belong.
310
+
311
+ The current public API is worktree-first because that keeps the reusable boundary compact and predictable.
312
+
313
+ Private remotes are still host-owned. The package now helps with the transport plumbing by supporting:
314
+
315
+ - `remoteCredentials` for clone, fetch, pull, and push
316
+ - `httpHeaders` for per-command HTTP headers such as bearer auth
317
+ - `sshCommand` for per-command SSH transport overrides
318
+
319
+ ## Why This Package
320
+
321
+ Most alternatives fall into one of three buckets:
322
+
323
+ - full forge products such as GitLab, Gitea, or Forgejo
324
+ - Git implementation libraries that reimplement Git behavior in another runtime
325
+ - one-off app code that shells out to `git` without a reusable boundary
326
+
327
+ `@trebired/git-host` is aiming at the gap between those options.
328
+
329
+ Use it when you want:
330
+
331
+ - your app to keep owning users, permissions, tokens, SSH keys, repository records, and UI
332
+ - real Git behavior from the system `git` binary
333
+ - clone, fetch, pull, and push over smart HTTP and SSH
334
+ - a reusable Git runtime instead of spreading Git shell calls all over your platform code
335
+ - optional headless React helpers over the JSON API without coupling the core package to a UI framework
336
+
337
+ Do not use it when you want:
338
+
339
+ - a ready-made Git product with issues, pull requests, teams, admin screens, and built-in account management
340
+ - a pure JavaScript Git implementation with no `git` binary dependency
341
+
342
+ That makes it useful for internal developer platforms, product-specific source management, deployment systems, controlled automation environments, and apps that need Git as a capability rather than Git hosting as a separate product.
343
+
344
+ ## Path Safety
345
+
346
+ Repository paths should never come straight from request input.
347
+
348
+ The intended flow is:
349
+
350
+ ```txt
351
+ request repo id -> host app record lookup -> absolute repository path -> git-host
352
+ ```
353
+
354
+ `resolveRepositoryPath()` is provided as a safe join helper when your host app stores repository-relative paths under one known root.
355
+
356
+ ## Hosted Transport Hooks
357
+
358
+ Hosted transports keep identity and permission policy in your app.
359
+
360
+ - `createGitHttpHandler()` supports host-owned repository resolution, optional identity resolution, permission checks, and request audit events.
361
+ - `createGitSshServer()` supports host-owned public key authentication, permission checks, and command audit events.
362
+ - `createGitApiHandler()` supports host-owned repository id mapping and per-route authorization.
363
+ - `generateSshKeyPair()`, `normalizeSshPublicKey()`, `compareSshPublicKeys()`, and `fingerprintSshPublicKey()` help host apps manage SSH transport setup without owning the parsing details themselves.
364
+
365
+ ## Platform Fit
366
+
367
+ `@trebired/git-host` is a good fit when a larger platform already owns users, permissions, repository records, tokens, SSH keys, and UI, but wants to stop hand-rolling the reusable Git layer.
368
+
369
+ The package is meant to replace or simplify:
370
+
371
+ - Git CLI execution and environment shaping
372
+ - repository locking and mutation coordination
373
+ - repository summary, tree, blob, commit, diff, and working-tree reads
374
+ - branch, checkout, commit, fetch, pull, and push operations
375
+ - smart HTTP and SSH Git transport handling
376
+ - thin JSON API route internals around those Git operations
377
+
378
+ The host platform should still own:
379
+
380
+ - repository and source metadata persistence
381
+ - permission checks and route authorization policy
382
+ - access token issuance, revocation, and storage
383
+ - SSH key ownership, private key storage, and known-host persistence
384
+ - merge requests, reviews, UI flows, and other product-specific features
385
+
386
+ That boundary is where the package simplifies a platform the most without turning into a forge product of its own.
387
+
388
+ ## Logger Support
389
+
390
+ `@trebired/git-host` works best with `@trebired/logger`, and that is the recommended logger.
391
+
392
+ Why we recommend it:
393
+
394
+ - it is simple
395
+ - it already matches git-host's expected method shape
396
+ - it keeps application logs and git-host diagnostics in one consistent format
397
+
398
+ The logger style:
399
+
400
+ ```ts
401
+ log.info("git-host", "initializing repository", { repositoryId: "demo" });
402
+ ```
403
+
404
+ comes from `@trebired/logger`.
405
+
406
+ You can pass that same `log` object into `createGitHost()`, `createGitHttpHandler()`, `createGitSshServer()`, and `createGitApiHandler()` through their `logger` option.
407
+
408
+ If you do not pass a logger and `@trebired/logger` is installed in the host app, git-host will create a quiet console-only logger automatically before falling back to raw `console`.
409
+
410
+ If you also set `verbose: true`, git-host will emit successful lifecycle and transport diagnostics through that logger. Without `verbose`, it stays much quieter and mainly reports rejected or failed operations.
411
+
412
+ Custom loggers can also use one of these shapes:
413
+
414
+ ```ts
415
+ type Logger = {
416
+ info(group: string, message: string, metadata?: unknown): void;
417
+ warn(group: string, message: string, metadata?: unknown): void;
418
+ error(group: string, message: string, metadata?: unknown): void;
419
+ fail(group: string, message: string, metadata?: unknown): void;
420
+ };
421
+
422
+ type Event = {
423
+ level: "info" | "warn" | "error" | "fail";
424
+ group: string;
425
+ message: string;
426
+ metadata?: unknown;
427
+ };
428
+
429
+ type EventLogger = (event: Event) => void;
430
+
431
+ type SinkLogger = {
432
+ log?(event: Event): void;
433
+ write?(event: Event): void;
434
+ fatal?(message: string, metadata?: unknown): void;
435
+ };
436
+ ```
437
+
438
+ Common logger objects such as `console`, pino-style level methods, or Winston-style sinks are also adapted as sensibly as possible.
439
+
440
+ If no logger is provided and `@trebired/logger` is not installed, git-host falls back to plain `console` output for its own diagnostics.
441
+
442
+ ## Roadmap
443
+
444
+ The remaining package work is mostly convenience and hardening:
445
+
446
+ - thin Express wrappers when they stay truly thin
447
+ - broader examples for host-app integration patterns
448
+
449
+ ## Contributing
450
+
451
+ See `CONTRIBUTING.md` for development commands and package guidelines.
@@ -0,0 +1,5 @@
1
+ import type { CreateGitApiHandlerOptions } from "../../types.js";
2
+ import { parseGitApiRoute } from "./route.js";
3
+ declare function runGitApiAction(options: CreateGitApiHandlerOptions, route: ReturnType<typeof parseGitApiRoute>, repositoryId: string, searchParams: URLSearchParams): Promise<import("../../types.js").GitCommitDetail | import("../../types.js").GitCommitSummary[] | import("../../types.js").GitCompareSummary | import("../../types.js").GitBlob | import("../../types.js").GitBranchSummary[] | import("../../types.js").GitRepositorySummary | import("../../types.js").GitTreeEntry[]>;
4
+ export { runGitApiAction };
5
+ //# sourceMappingURL=action.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action.d.ts","sourceRoot":"","sources":["../../../src/api/handler/action.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAGjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C,iBAAe,eAAe,CAC5B,OAAO,EAAE,0BAA0B,EACnC,KAAK,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,EAC1C,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,eAAe,2TAwC9B;AAED,OAAO,EAAE,eAAe,EAAE,CAAC"}
@@ -0,0 +1,47 @@
1
+ import { GitHostError } from "../../errors.js";
2
+ import { isTruthy, text } from "../../utils/text.js";
3
+ import { parsePositiveInt } from "./response.js";
4
+ async function runGitApiAction(options, route, repositoryId, searchParams) {
5
+ if (!route)
6
+ throw new GitHostError("git_command_failed", "API route is required.");
7
+ switch (route.action) {
8
+ case "summary":
9
+ return await options.gitHost.readSummary(repositoryId, {
10
+ commitLimit: parsePositiveInt(searchParams.get("commitLimit"), "commitLimit"),
11
+ });
12
+ case "branches":
13
+ return await options.gitHost.listBranches(repositoryId);
14
+ case "commits":
15
+ return await options.gitHost.listCommits(repositoryId, {
16
+ limit: parsePositiveInt(searchParams.get("limit"), "limit"),
17
+ });
18
+ case "commit":
19
+ return await options.gitHost.readCommit(repositoryId, route.commitRef);
20
+ case "tree":
21
+ return await options.gitHost.listTree(repositoryId, {
22
+ path: text(searchParams.get("path")),
23
+ recursive: isTruthy(searchParams.get("recursive")),
24
+ ref: text(searchParams.get("ref")),
25
+ });
26
+ case "blob": {
27
+ const blobPath = text(searchParams.get("path"));
28
+ if (!blobPath)
29
+ throw new GitHostError("invalid_repository_path", "blob path is required.");
30
+ return await options.gitHost.readBlob(repositoryId, {
31
+ path: blobPath,
32
+ ref: text(searchParams.get("ref")),
33
+ });
34
+ }
35
+ case "diff": {
36
+ const baseRef = text(searchParams.get("baseRef"));
37
+ const headRef = text(searchParams.get("headRef"));
38
+ if (!baseRef || !headRef)
39
+ throw new GitHostError("git_command_failed", "baseRef and headRef are required.");
40
+ return await options.gitHost.diff(repositoryId, { baseRef, headRef });
41
+ }
42
+ default:
43
+ throw new GitHostError("git_command_failed", "Unsupported Git API action.");
44
+ }
45
+ }
46
+ export { runGitApiAction };
47
+ //# sourceMappingURL=action.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action.js","sourceRoot":"","sources":["../../../src/api/handler/action.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/C,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAGjD,KAAK,UAAU,eAAe,CAC5B,OAAmC,EACnC,KAA0C,EAC1C,YAAoB,EACpB,YAA6B;IAE7B,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,YAAY,CAAC,oBAAoB,EAAE,wBAAwB,CAAC,CAAC;IAEnF,QAAQ,KAAK,CAAC,MAAM,EAAE,CAAC;QACrB,KAAK,SAAS;YACZ,OAAO,MAAM,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,YAAY,EAAE;gBACrD,WAAW,EAAE,gBAAgB,CAAC,YAAY,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;aAC9E,CAAC,CAAC;QACL,KAAK,UAAU;YACb,OAAO,MAAM,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;QAC1D,KAAK,SAAS;YACZ,OAAO,MAAM,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,YAAY,EAAE;gBACrD,KAAK,EAAE,gBAAgB,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;aAC5D,CAAC,CAAC;QACL,KAAK,QAAQ;YACX,OAAO,MAAM,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;QACzE,KAAK,MAAM;YACT,OAAO,MAAM,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,YAAY,EAAE;gBAClD,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBACpC,SAAS,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;gBAClD,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;aACnC,CAAC,CAAC;QACL,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;YAChD,IAAI,CAAC,QAAQ;gBAAE,MAAM,IAAI,YAAY,CAAC,yBAAyB,EAAE,wBAAwB,CAAC,CAAC;YAC3F,OAAO,MAAM,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,YAAY,EAAE;gBAClD,IAAI,EAAE,QAAQ;gBACd,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;aACnC,CAAC,CAAC;QACL,CAAC;QACD,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC;YAClD,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC;YAClD,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO;gBAAE,MAAM,IAAI,YAAY,CAAC,oBAAoB,EAAE,mCAAmC,CAAC,CAAC;YAC5G,OAAO,MAAM,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;QACxE,CAAC;QACD;YACE,MAAM,IAAI,YAAY,CAAC,oBAAoB,EAAE,6BAA6B,CAAC,CAAC;IAChF,CAAC;AACH,CAAC;AAED,OAAO,EAAE,eAAe,EAAE,CAAC"}
@@ -0,0 +1,34 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { GitApiAuthorizationResult } from "../../types.js";
3
+ declare function applyAuthorizationHeaders(res: ServerResponse, headers: Record<string, string> | undefined): void;
4
+ declare function authorizationAllowed(value: GitApiAuthorizationResult | undefined): {
5
+ allowed: boolean;
6
+ status: number;
7
+ message: string;
8
+ headers?: undefined;
9
+ } | {
10
+ allowed: boolean;
11
+ headers: Record<string, string>;
12
+ message: string;
13
+ status: number;
14
+ };
15
+ declare function parsePositiveInt(value: string | null, name: string): number | undefined;
16
+ declare function statusForError(error: unknown): number;
17
+ declare function serializeError(error: unknown): {
18
+ ok: boolean;
19
+ error: {
20
+ code: string;
21
+ details: Record<string, unknown>;
22
+ message: string;
23
+ };
24
+ } | {
25
+ ok: boolean;
26
+ error: {
27
+ code: string;
28
+ message: string;
29
+ details?: undefined;
30
+ };
31
+ };
32
+ declare function writeJson(req: IncomingMessage, res: ServerResponse, status: number, payload: unknown): void;
33
+ export { applyAuthorizationHeaders, authorizationAllowed, parsePositiveInt, serializeError, statusForError, writeJson };
34
+ //# sourceMappingURL=response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../../../src/api/handler/response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGjE,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAGhE,iBAAS,yBAAyB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,QAMlG;AAED,iBAAS,oBAAoB,CAAC,KAAK,EAAE,yBAAyB,GAAG,SAAS;;;;;;;;;;EAUzE;AAED,iBAAS,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOhF;AAED,iBAAS,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAiB9C;AAED,iBAAS,cAAc,CAAC,KAAK,EAAE,OAAO;;;;;;;;;;;;;;EA6BrC;AAED,iBAAS,SAAS,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,QAQ7F;AAED,OAAO,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,cAAc,EAAE,cAAc,EAAE,SAAS,EAAE,CAAC"}
@@ -0,0 +1,87 @@
1
+ import { GitHostError, isGitHostError } from "../../errors.js";
2
+ import { text } from "../../utils/text.js";
3
+ function applyAuthorizationHeaders(res, headers) {
4
+ const nextHeaders = headers && typeof headers === "object" ? headers : {};
5
+ for (const [name, value] of Object.entries(nextHeaders)) {
6
+ if (!name || typeof value !== "string")
7
+ continue;
8
+ res.setHeader(name, value);
9
+ }
10
+ }
11
+ function authorizationAllowed(value) {
12
+ if (value == null)
13
+ return { allowed: true, status: 200, message: "" };
14
+ if (typeof value === "boolean")
15
+ return { allowed: value, status: value ? 200 : 403, message: "" };
16
+ return {
17
+ allowed: value.allowed === true,
18
+ headers: value.headers,
19
+ message: text(value.message),
20
+ status: Number(value.status) || (value.allowed === true ? 200 : 403),
21
+ };
22
+ }
23
+ function parsePositiveInt(value, name) {
24
+ if (value == null || text(value) === "")
25
+ return undefined;
26
+ const next = Number(value);
27
+ if (!Number.isInteger(next) || next <= 0) {
28
+ throw new GitHostError("git_command_failed", `${name} must be a positive integer.`, { value });
29
+ }
30
+ return next;
31
+ }
32
+ function statusForError(error) {
33
+ if (isGitHostError(error)) {
34
+ switch (error.code) {
35
+ case "invalid_branch_name":
36
+ case "invalid_repository_path":
37
+ return 400;
38
+ case "repository_not_found":
39
+ return 404;
40
+ case "repository_not_initialized":
41
+ case "repository_clone_target_not_empty":
42
+ return 409;
43
+ default:
44
+ return 400;
45
+ }
46
+ }
47
+ return 500;
48
+ }
49
+ function serializeError(error) {
50
+ if (isGitHostError(error)) {
51
+ return {
52
+ ok: false,
53
+ error: {
54
+ code: error.code,
55
+ details: error.details,
56
+ message: error.message,
57
+ },
58
+ };
59
+ }
60
+ if (error instanceof Error) {
61
+ return {
62
+ ok: false,
63
+ error: {
64
+ code: "internal_error",
65
+ message: error.message,
66
+ },
67
+ };
68
+ }
69
+ return {
70
+ ok: false,
71
+ error: {
72
+ code: "internal_error",
73
+ message: "Git API request failed.",
74
+ },
75
+ };
76
+ }
77
+ function writeJson(req, res, status, payload) {
78
+ res.statusCode = status;
79
+ res.setHeader("content-type", "application/json; charset=utf-8");
80
+ if (text(req.method).toUpperCase() === "HEAD") {
81
+ res.end();
82
+ return;
83
+ }
84
+ res.end(JSON.stringify(payload, null, 2));
85
+ }
86
+ export { applyAuthorizationHeaders, authorizationAllowed, parsePositiveInt, serializeError, statusForError, writeJson };
87
+ //# sourceMappingURL=response.js.map