@remixhq/core 0.1.2

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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +15 -0
  3. package/dist/api.d.ts +494 -0
  4. package/dist/api.js +7 -0
  5. package/dist/auth.d.ts +27 -0
  6. package/dist/auth.js +15 -0
  7. package/dist/binding.d.ts +16 -0
  8. package/dist/binding.js +11 -0
  9. package/dist/chunk-2WGZS7CD.js +0 -0
  10. package/dist/chunk-34WDQCPF.js +242 -0
  11. package/dist/chunk-4OCNZHHR.js +0 -0
  12. package/dist/chunk-54CBEP2W.js +570 -0
  13. package/dist/chunk-55K5GHAZ.js +252 -0
  14. package/dist/chunk-5H5CZKGN.js +691 -0
  15. package/dist/chunk-5NTOJXEZ.js +223 -0
  16. package/dist/chunk-7WUKH3ZD.js +221 -0
  17. package/dist/chunk-AE2HPMUZ.js +80 -0
  18. package/dist/chunk-AEAOYVIL.js +200 -0
  19. package/dist/chunk-BJFCN2C3.js +46 -0
  20. package/dist/chunk-DCU3646I.js +12 -0
  21. package/dist/chunk-DEWAIK5X.js +11 -0
  22. package/dist/chunk-DRD6EVTT.js +447 -0
  23. package/dist/chunk-E4KAGBU7.js +134 -0
  24. package/dist/chunk-EF3677RE.js +93 -0
  25. package/dist/chunk-EVWDYCBL.js +223 -0
  26. package/dist/chunk-FAZUMWBS.js +93 -0
  27. package/dist/chunk-GC2MOT3U.js +12 -0
  28. package/dist/chunk-GFOBGYW4.js +252 -0
  29. package/dist/chunk-INDDXWAH.js +92 -0
  30. package/dist/chunk-K57ZFDGC.js +15 -0
  31. package/dist/chunk-NDA7EJJA.js +286 -0
  32. package/dist/chunk-NK2DA4X6.js +357 -0
  33. package/dist/chunk-OJMTW22J.js +286 -0
  34. package/dist/chunk-OMUDRPUI.js +195 -0
  35. package/dist/chunk-ONKKRS2C.js +239 -0
  36. package/dist/chunk-OWFBBWU7.js +196 -0
  37. package/dist/chunk-P7EM3N73.js +46 -0
  38. package/dist/chunk-PR5QKMHM.js +46 -0
  39. package/dist/chunk-RIP2MIZL.js +710 -0
  40. package/dist/chunk-TQHLFQY4.js +448 -0
  41. package/dist/chunk-TY3SSQQK.js +688 -0
  42. package/dist/chunk-UGKPOCN5.js +710 -0
  43. package/dist/chunk-VM3CGCNX.js +46 -0
  44. package/dist/chunk-XOQIADCH.js +223 -0
  45. package/dist/chunk-YZ34ICNN.js +17 -0
  46. package/dist/chunk-ZBMOGUSJ.js +17 -0
  47. package/dist/collab.d.ts +680 -0
  48. package/dist/collab.js +1917 -0
  49. package/dist/config.d.ts +22 -0
  50. package/dist/config.js +9 -0
  51. package/dist/errors.d.ts +21 -0
  52. package/dist/errors.js +12 -0
  53. package/dist/index.cjs +1269 -0
  54. package/dist/index.d.cts +482 -0
  55. package/dist/index.d.ts +6 -0
  56. package/dist/index.js +34 -0
  57. package/dist/repo.d.ts +66 -0
  58. package/dist/repo.js +62 -0
  59. package/dist/tokenProvider-BWTusyj4.d.ts +63 -0
  60. package/package.json +72 -0
@@ -0,0 +1,448 @@
1
+ import {
2
+ ComergeError
3
+ } from "./chunk-K57ZFDGC.js";
4
+
5
+ // src/infrastructure/repo/gitRepo.ts
6
+ import fs from "fs/promises";
7
+ import { createHash } from "crypto";
8
+ import os from "os";
9
+ import path from "path";
10
+ import { execa } from "execa";
11
+ var GIT_REMOTE_PROTOCOL_RE = /^(https?|ssh):\/\//i;
12
+ var SCP_LIKE_GIT_REMOTE_RE = /^(?<user>[^@\s]+)@(?<host>[^:\s]+):(?<path>[^\\\s]+)$/;
13
+ var CANONICAL_GIT_REMOTE_RE = /^(?<host>(?:localhost|[a-z0-9.-]+))\/(?<path>[^\\\s]+)$/i;
14
+ async function runGit(args, cwd) {
15
+ const res = await execa("git", args, { cwd, stderr: "ignore" });
16
+ return String(res.stdout || "").trim();
17
+ }
18
+ async function runGitWithEnv(args, cwd, env) {
19
+ const res = await execa("git", args, { cwd, env, stderr: "ignore" });
20
+ return String(res.stdout || "").trim();
21
+ }
22
+ async function runGitRaw(args, cwd) {
23
+ const res = await execa("git", args, { cwd, stderr: "ignore", stripFinalNewline: false });
24
+ return String(res.stdout || "");
25
+ }
26
+ async function runGitRawWithEnv(args, cwd, env) {
27
+ const res = await execa("git", args, { cwd, env, stderr: "ignore", stripFinalNewline: false });
28
+ return String(res.stdout || "");
29
+ }
30
+ async function runGitDetailed(args, cwd) {
31
+ const res = await execa("git", args, { cwd, reject: false });
32
+ return {
33
+ exitCode: res.exitCode ?? 1,
34
+ stdout: String(res.stdout || ""),
35
+ stderr: String(res.stderr || "")
36
+ };
37
+ }
38
+ function cleanRepoPath(value) {
39
+ return value.trim().replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "");
40
+ }
41
+ function normalizeGitRemote(remote) {
42
+ const raw = String(remote ?? "").trim();
43
+ if (!raw) return null;
44
+ const canonicalMatch = raw.match(CANONICAL_GIT_REMOTE_RE);
45
+ if (canonicalMatch?.groups?.host && canonicalMatch.groups.path) {
46
+ const repoPath = cleanRepoPath(canonicalMatch.groups.path);
47
+ if (!repoPath) return null;
48
+ return `${canonicalMatch.groups.host}/${repoPath}`.toLowerCase();
49
+ }
50
+ if (GIT_REMOTE_PROTOCOL_RE.test(raw)) {
51
+ try {
52
+ const url = new URL(raw);
53
+ const repoPath = cleanRepoPath(url.pathname);
54
+ if (!url.hostname || !repoPath) return null;
55
+ return `${url.hostname}/${repoPath}`.toLowerCase();
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+ const scpMatch = raw.match(SCP_LIKE_GIT_REMOTE_RE);
61
+ if (scpMatch?.groups?.host && scpMatch.groups.path) {
62
+ const repoPath = cleanRepoPath(scpMatch.groups.path);
63
+ if (!repoPath) return null;
64
+ return `${scpMatch.groups.host}/${repoPath}`.toLowerCase();
65
+ }
66
+ return null;
67
+ }
68
+ function sanitizeRefFragment(value) {
69
+ return value.trim().replace(/[^A-Za-z0-9._/-]+/g, "-").replace(/\/{2,}/g, "/").replace(/^\/+|\/+$/g, "").replace(/^-+|-+$/g, "").slice(0, 120);
70
+ }
71
+ function normalizeRepoRelativePath(value) {
72
+ const normalized = path.posix.normalize(value.replace(/\\/g, "/").trim());
73
+ if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../") || path.posix.isAbsolute(normalized)) {
74
+ throw new ComergeError("Git returned an invalid repository-relative path.", {
75
+ exitCode: 1,
76
+ hint: `Path: ${value}`
77
+ });
78
+ }
79
+ return normalized;
80
+ }
81
+ function resolveRepoRelativePath(repoRoot, relativePath) {
82
+ return path.resolve(repoRoot, ...relativePath.split("/"));
83
+ }
84
+ function isWithinRepoRoot(repoRoot, targetPath) {
85
+ const relative = path.relative(repoRoot, targetPath);
86
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
87
+ }
88
+ async function pruneEmptyParentDirectories(repoRoot, filePath) {
89
+ let current = path.dirname(filePath);
90
+ while (current !== repoRoot && isWithinRepoRoot(repoRoot, current)) {
91
+ const entries = await fs.readdir(current).catch(() => null);
92
+ if (!entries || entries.length > 0) return;
93
+ await fs.rmdir(current).catch(() => void 0);
94
+ current = path.dirname(current);
95
+ }
96
+ }
97
+ async function findGitRoot(startDir) {
98
+ try {
99
+ const root = await runGit(["rev-parse", "--show-toplevel"], startDir);
100
+ if (!root) throw new Error("empty");
101
+ return root;
102
+ } catch {
103
+ throw new ComergeError("Not inside a git repository.", {
104
+ exitCode: 2,
105
+ hint: "Run this command from the root of the repository or one of its subdirectories."
106
+ });
107
+ }
108
+ }
109
+ async function getCurrentBranch(cwd) {
110
+ try {
111
+ const branch = await runGit(["branch", "--show-current"], cwd);
112
+ return branch || null;
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+ async function getRemoteOriginUrl(cwd) {
118
+ try {
119
+ const url = await runGit(["config", "--get", "remote.origin.url"], cwd);
120
+ return url || null;
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+ async function getDefaultBranch(cwd) {
126
+ try {
127
+ const ref = await runGit(["symbolic-ref", "refs/remotes/origin/HEAD"], cwd);
128
+ if (!ref) return null;
129
+ const suffix = ref.replace(/^refs\/remotes\/origin\//, "").trim();
130
+ return suffix || null;
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+ async function listUntrackedFiles(cwd) {
136
+ try {
137
+ const out = await runGit(["ls-files", "--others", "--exclude-standard"], cwd);
138
+ return out.split("\n").map((line) => line.trim()).filter(Boolean);
139
+ } catch {
140
+ return [];
141
+ }
142
+ }
143
+ async function getWorkingTreeDiff(cwd) {
144
+ const untracked = await listUntrackedFiles(cwd);
145
+ if (untracked.length > 0) {
146
+ throw new ComergeError("Untracked files are not included in git diff mode.", {
147
+ exitCode: 2,
148
+ hint: "Provide `--diff-file`/`--diff-stdin`, or add the files to git before running `comerge collab add`."
149
+ });
150
+ }
151
+ try {
152
+ return await runGitRaw(["diff", "--binary", "--no-ext-diff", "HEAD"], cwd);
153
+ } catch {
154
+ throw new ComergeError("Failed to generate git diff.", { exitCode: 1 });
155
+ }
156
+ }
157
+ async function getWorkspaceDiff(cwd) {
158
+ const headCommitHash = await getHeadCommitHash(cwd);
159
+ if (!headCommitHash) {
160
+ throw new ComergeError("Failed to resolve local HEAD commit.", { exitCode: 1 });
161
+ }
162
+ const includedUntrackedPaths = Array.from(new Set((await listUntrackedFiles(cwd)).map((entry) => normalizeRepoRelativePath(entry))));
163
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "comerge-index-"));
164
+ const tempIndexPath = path.join(tempDir, "index");
165
+ const env = { ...process.env, GIT_INDEX_FILE: tempIndexPath };
166
+ try {
167
+ try {
168
+ await runGitWithEnv(["read-tree", "HEAD"], cwd, env);
169
+ await runGitWithEnv(["add", "-A", "--", "."], cwd, env);
170
+ const diff = await runGitRawWithEnv(["diff", "--binary", "--no-ext-diff", "--cached", "HEAD"], cwd, env);
171
+ return { diff, includedUntrackedPaths };
172
+ } catch {
173
+ throw new ComergeError("Failed to generate workspace diff.", {
174
+ exitCode: 1,
175
+ hint: "Git could not snapshot the current workspace into an isolated index."
176
+ });
177
+ }
178
+ } finally {
179
+ await fs.rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
180
+ }
181
+ }
182
+ async function writeTempUnifiedDiffBackup(diff, prefix = "comerge-add-backup") {
183
+ const safePrefix = prefix.replace(/[^a-zA-Z0-9._-]+/g, "-") || "comerge-add-backup";
184
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), `${safePrefix}-`));
185
+ const backupPath = path.join(tmpDir, "submitted.diff");
186
+ await fs.writeFile(backupPath, diff, "utf8");
187
+ return { backupPath };
188
+ }
189
+ async function getHeadCommitHash(cwd) {
190
+ try {
191
+ const hash = await runGit(["rev-parse", "HEAD"], cwd);
192
+ return hash || null;
193
+ } catch {
194
+ return null;
195
+ }
196
+ }
197
+ async function createGitBundle(cwd, bundleName = "repository.bundle") {
198
+ const headCommitHash = await getHeadCommitHash(cwd);
199
+ if (!headCommitHash) {
200
+ throw new ComergeError("Failed to resolve local HEAD commit.", { exitCode: 1 });
201
+ }
202
+ const safeName = bundleName.replace(/[^a-zA-Z0-9._-]+/g, "_");
203
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "comerge-bundle-"));
204
+ const bundlePath = path.join(tmpDir, safeName);
205
+ const res = await runGitDetailed(["bundle", "create", bundlePath, "--all", "--tags"], cwd);
206
+ if (res.exitCode !== 0) {
207
+ const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
208
+ throw new ComergeError("Failed to create repository bundle.", {
209
+ exitCode: 1,
210
+ hint: detail || "Git could not create the bundle artifact."
211
+ });
212
+ }
213
+ return { bundlePath, headCommitHash };
214
+ }
215
+ async function getWorktreeStatus(cwd) {
216
+ try {
217
+ const out = await runGit(["status", "--porcelain"], cwd);
218
+ const entries = out.split("\n").map((line) => line.trimEnd()).filter(Boolean);
219
+ return { isClean: entries.length === 0, entries };
220
+ } catch {
221
+ return { isClean: false, entries: [] };
222
+ }
223
+ }
224
+ async function ensureCleanWorktree(cwd, operation = "`comerge collab sync`") {
225
+ const status = await getWorktreeStatus(cwd);
226
+ if (status.isClean) return;
227
+ const preview = status.entries.slice(0, 10).join("\n");
228
+ const suffix = status.entries.length > 10 ? `
229
+ ...and ${status.entries.length - 10} more` : "";
230
+ throw new ComergeError(`Working tree must be clean before running ${operation}.`, {
231
+ exitCode: 2,
232
+ hint: `Commit, stash, or discard local changes first.
233
+
234
+ ${preview}${suffix}`
235
+ });
236
+ }
237
+ async function discardTrackedChanges(cwd, operation = "`comerge collab add`") {
238
+ const res = await runGitDetailed(["reset", "--hard", "HEAD"], cwd);
239
+ if (res.exitCode !== 0) {
240
+ const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
241
+ throw new ComergeError(`Failed to discard local tracked changes while running ${operation}.`, {
242
+ exitCode: 1,
243
+ hint: detail || "Git could not reset tracked changes back to HEAD."
244
+ });
245
+ }
246
+ const hash = await getHeadCommitHash(cwd);
247
+ if (!hash) {
248
+ throw new ComergeError("Failed to resolve local HEAD after discarding tracked changes.", { exitCode: 1 });
249
+ }
250
+ return hash;
251
+ }
252
+ async function discardCapturedUntrackedChanges(repoRoot, capturedPaths) {
253
+ const normalizedCapturedPaths = Array.from(new Set(capturedPaths.map((entry) => normalizeRepoRelativePath(entry))));
254
+ if (normalizedCapturedPaths.length === 0) {
255
+ return { removedPaths: [] };
256
+ }
257
+ const currentUntracked = new Set((await listUntrackedFiles(repoRoot)).map((entry) => normalizeRepoRelativePath(entry)));
258
+ const removedPaths = [];
259
+ for (const relativePath of normalizedCapturedPaths) {
260
+ if (!currentUntracked.has(relativePath)) continue;
261
+ const absolutePath = resolveRepoRelativePath(repoRoot, relativePath);
262
+ if (!isWithinRepoRoot(repoRoot, absolutePath)) {
263
+ throw new ComergeError("Refusing to delete a path outside the repository root.", {
264
+ exitCode: 1,
265
+ hint: `Path: ${relativePath}`
266
+ });
267
+ }
268
+ await fs.rm(absolutePath, { recursive: true, force: true }).catch((err) => {
269
+ throw new ComergeError("Failed to remove a captured untracked path before syncing.", {
270
+ exitCode: 1,
271
+ hint: err instanceof Error ? `${relativePath}
272
+
273
+ ${err.message}` : relativePath
274
+ });
275
+ });
276
+ removedPaths.push(relativePath);
277
+ await pruneEmptyParentDirectories(repoRoot, absolutePath);
278
+ }
279
+ return { removedPaths };
280
+ }
281
+ async function requireCurrentBranch(cwd) {
282
+ const branch = await getCurrentBranch(cwd);
283
+ if (!branch) {
284
+ throw new ComergeError("`comerge collab sync` requires a checked out local branch.", {
285
+ exitCode: 2,
286
+ hint: "Checkout a branch before syncing."
287
+ });
288
+ }
289
+ return branch;
290
+ }
291
+ async function importGitBundle(cwd, bundlePath, bundleRef) {
292
+ const verifyRes = await runGitDetailed(["bundle", "verify", bundlePath], cwd);
293
+ if (verifyRes.exitCode !== 0) {
294
+ const detail = [verifyRes.stderr.trim(), verifyRes.stdout.trim()].filter(Boolean).join("\n\n");
295
+ throw new ComergeError("Failed to verify sync bundle.", {
296
+ exitCode: 1,
297
+ hint: detail || "Git bundle verification failed."
298
+ });
299
+ }
300
+ const fetchRes = await runGitDetailed(["fetch", "--quiet", bundlePath, bundleRef], cwd);
301
+ if (fetchRes.exitCode !== 0) {
302
+ const detail = [fetchRes.stderr.trim(), fetchRes.stdout.trim()].filter(Boolean).join("\n\n");
303
+ throw new ComergeError("Failed to import sync bundle.", {
304
+ exitCode: 1,
305
+ hint: detail || "Git could not fetch objects from the sync bundle."
306
+ });
307
+ }
308
+ }
309
+ async function cloneGitBundleToDirectory(bundlePath, targetDir) {
310
+ const parentDir = path.dirname(targetDir);
311
+ const cloneRes = await runGitDetailed(["clone", bundlePath, targetDir], parentDir);
312
+ if (cloneRes.exitCode !== 0) {
313
+ const detail = [cloneRes.stderr.trim(), cloneRes.stdout.trim()].filter(Boolean).join("\n\n");
314
+ throw new ComergeError("Failed to create local remix checkout.", {
315
+ exitCode: 1,
316
+ hint: detail || "Git could not clone the remix repository bundle."
317
+ });
318
+ }
319
+ const remoteRemoveRes = await runGitDetailed(["remote", "remove", "origin"], targetDir);
320
+ if (remoteRemoveRes.exitCode !== 0) {
321
+ const detail = [remoteRemoveRes.stderr.trim(), remoteRemoveRes.stdout.trim()].filter(Boolean).join("\n\n");
322
+ throw new ComergeError("Failed to finalize local remix checkout.", {
323
+ exitCode: 1,
324
+ hint: detail || "Git could not remove the temporary bundle origin."
325
+ });
326
+ }
327
+ }
328
+ async function ensureGitInfoExcludeEntries(cwd, entries) {
329
+ const excludePath = path.join(cwd, ".git", "info", "exclude");
330
+ await fs.mkdir(path.dirname(excludePath), { recursive: true });
331
+ let current = "";
332
+ try {
333
+ current = await fs.readFile(excludePath, "utf8");
334
+ } catch {
335
+ }
336
+ const lines = new Set(current.split("\n").map((line) => line.trim()).filter(Boolean));
337
+ let changed = false;
338
+ for (const entry of entries) {
339
+ const normalized = entry.trim();
340
+ if (!normalized || lines.has(normalized)) continue;
341
+ lines.add(normalized);
342
+ changed = true;
343
+ }
344
+ if (!changed) return;
345
+ await fs.writeFile(excludePath, `${Array.from(lines).join("\n")}
346
+ `, "utf8");
347
+ }
348
+ async function ensureCommitExists(cwd, commitHash) {
349
+ const res = await runGitDetailed(["cat-file", "-e", `${commitHash}^{commit}`], cwd);
350
+ if (res.exitCode === 0) return;
351
+ throw new ComergeError("Expected target commit is missing after bundle import.", {
352
+ exitCode: 1,
353
+ hint: `Commit ${commitHash} is not available in the local repository.`
354
+ });
355
+ }
356
+ async function fastForwardToCommit(cwd, commitHash) {
357
+ const res = await runGitDetailed(["merge", "--ff-only", commitHash], cwd);
358
+ if (res.exitCode !== 0) {
359
+ const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
360
+ throw new ComergeError("Failed to fast-forward local branch.", {
361
+ exitCode: 1,
362
+ hint: detail || "Git could not fast-forward to the target commit."
363
+ });
364
+ }
365
+ const hash = await getHeadCommitHash(cwd);
366
+ if (!hash) throw new ComergeError("Failed to resolve local HEAD after fast-forward sync.", { exitCode: 1 });
367
+ return hash;
368
+ }
369
+ async function createBackupBranch(cwd, params) {
370
+ const sourceCommitHash = params?.sourceCommitHash?.trim() || await getHeadCommitHash(cwd);
371
+ if (!sourceCommitHash) {
372
+ throw new ComergeError("Failed to resolve local HEAD before creating reconcile backup.", { exitCode: 1 });
373
+ }
374
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
375
+ const branchFragment = sanitizeRefFragment(params?.branchName?.trim() || "current-branch");
376
+ const prefix = sanitizeRefFragment(params?.prefix?.trim() || "comerge/reconcile-backup");
377
+ const backupBranchName = `${prefix}/${branchFragment}-${timestamp}`;
378
+ const createRes = await runGitDetailed(["branch", backupBranchName, sourceCommitHash], cwd);
379
+ if (createRes.exitCode !== 0) {
380
+ const detail = [createRes.stderr.trim(), createRes.stdout.trim()].filter(Boolean).join("\n\n");
381
+ throw new ComergeError("Failed to create reconcile backup branch.", {
382
+ exitCode: 1,
383
+ hint: detail || "Git could not create the safety backup branch."
384
+ });
385
+ }
386
+ return { branchName: backupBranchName, commitHash: sourceCommitHash };
387
+ }
388
+ async function hardResetToCommit(cwd, commitHash, operation = "`comerge collab reconcile`") {
389
+ const res = await runGitDetailed(["reset", "--hard", commitHash], cwd);
390
+ if (res.exitCode !== 0) {
391
+ const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
392
+ throw new ComergeError(`Failed to move local branch while running ${operation}.`, {
393
+ exitCode: 1,
394
+ hint: detail || `Git could not reset the current branch to ${commitHash}.`
395
+ });
396
+ }
397
+ const hash = await getHeadCommitHash(cwd);
398
+ if (!hash) {
399
+ throw new ComergeError("Failed to resolve local HEAD after resetting branch.", { exitCode: 1 });
400
+ }
401
+ return hash;
402
+ }
403
+ async function buildRepoFingerprint(params) {
404
+ const remote = normalizeGitRemote(params.remoteUrl);
405
+ const defaultBranch = params.defaultBranch?.trim().toLowerCase() || "";
406
+ const payload = remote ? { remote, defaultBranch } : { local: path.resolve(params.gitRoot).toLowerCase(), defaultBranch };
407
+ return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
408
+ }
409
+ function summarizeUnifiedDiff(diff) {
410
+ const lines = diff.split("\n");
411
+ let changedFilesCount = 0;
412
+ let insertions = 0;
413
+ let deletions = 0;
414
+ for (const line of lines) {
415
+ if (line.startsWith("diff --git ")) changedFilesCount += 1;
416
+ if (line.startsWith("+") && !line.startsWith("+++")) insertions += 1;
417
+ if (line.startsWith("-") && !line.startsWith("---")) deletions += 1;
418
+ }
419
+ return { changedFilesCount, insertions, deletions };
420
+ }
421
+
422
+ export {
423
+ normalizeGitRemote,
424
+ findGitRoot,
425
+ getCurrentBranch,
426
+ getRemoteOriginUrl,
427
+ getDefaultBranch,
428
+ listUntrackedFiles,
429
+ getWorkingTreeDiff,
430
+ getWorkspaceDiff,
431
+ writeTempUnifiedDiffBackup,
432
+ getHeadCommitHash,
433
+ createGitBundle,
434
+ getWorktreeStatus,
435
+ ensureCleanWorktree,
436
+ discardTrackedChanges,
437
+ discardCapturedUntrackedChanges,
438
+ requireCurrentBranch,
439
+ importGitBundle,
440
+ cloneGitBundleToDirectory,
441
+ ensureGitInfoExcludeEntries,
442
+ ensureCommitExists,
443
+ fastForwardToCommit,
444
+ createBackupBranch,
445
+ hardResetToCommit,
446
+ buildRepoFingerprint,
447
+ summarizeUnifiedDiff
448
+ };