@remixhq/core 0.1.14 → 0.1.16

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 (72) hide show
  1. package/dist/api.d.ts +1 -1
  2. package/dist/collab.d.ts +2 -2
  3. package/dist/collab.js +1 -1
  4. package/dist/{contracts-NbV3P_Rl.d.ts → contracts-CHmD-fMj.d.ts} +1 -1
  5. package/dist/history.d.ts +64 -0
  6. package/dist/history.js +529 -0
  7. package/dist/index.d.ts +1 -1
  8. package/package.json +6 -2
  9. package/dist/chunk-2WGZS7CD.js +0 -0
  10. package/dist/chunk-34WDQCPF.js +0 -242
  11. package/dist/chunk-4276ARDF.js +0 -303
  12. package/dist/chunk-4L3ZBZUQ.js +0 -281
  13. package/dist/chunk-54CBEP2W.js +0 -570
  14. package/dist/chunk-55K5GHAZ.js +0 -252
  15. package/dist/chunk-5H5CZKGN.js +0 -691
  16. package/dist/chunk-5NTOJXEZ.js +0 -223
  17. package/dist/chunk-7WUKH3ZD.js +0 -221
  18. package/dist/chunk-AE2HPMUZ.js +0 -80
  19. package/dist/chunk-AEAOYVIL.js +0 -200
  20. package/dist/chunk-B5S3PUIR.js +0 -388
  21. package/dist/chunk-BJFCN2C3.js +0 -46
  22. package/dist/chunk-BNKPTE2U.js +0 -401
  23. package/dist/chunk-C5NBNU32.js +0 -240
  24. package/dist/chunk-CJFGQE7D.js +0 -46
  25. package/dist/chunk-CUUXZSKW.js +0 -611
  26. package/dist/chunk-DCU3646I.js +0 -12
  27. package/dist/chunk-DEWAIK5X.js +0 -11
  28. package/dist/chunk-DRD6EVTT.js +0 -447
  29. package/dist/chunk-DXCL6I4Q.js +0 -399
  30. package/dist/chunk-E4KAGBU7.js +0 -134
  31. package/dist/chunk-E6AYE22H.js +0 -343
  32. package/dist/chunk-EF3677RE.js +0 -93
  33. package/dist/chunk-EVWDYCBL.js +0 -223
  34. package/dist/chunk-EW4PWFHB.js +0 -46
  35. package/dist/chunk-FAZUMWBS.js +0 -93
  36. package/dist/chunk-GEHSFPCD.js +0 -93
  37. package/dist/chunk-GFOBGYW4.js +0 -252
  38. package/dist/chunk-INDDXWAH.js +0 -92
  39. package/dist/chunk-IXWQWFYT.js +0 -342
  40. package/dist/chunk-J3J4PBQ7.js +0 -710
  41. package/dist/chunk-K54U353Z.js +0 -691
  42. package/dist/chunk-K57ZFDGC.js +0 -15
  43. package/dist/chunk-NDA7EJJA.js +0 -286
  44. package/dist/chunk-NK2DA4X6.js +0 -357
  45. package/dist/chunk-OBYR4JHZ.js +0 -374
  46. package/dist/chunk-OJMTW22J.js +0 -286
  47. package/dist/chunk-OMUDRPUI.js +0 -195
  48. package/dist/chunk-ONKKRS2C.js +0 -239
  49. package/dist/chunk-OWFBBWU7.js +0 -196
  50. package/dist/chunk-OZRXDDEL.js +0 -46
  51. package/dist/chunk-P7EM3N73.js +0 -46
  52. package/dist/chunk-POYB6MCQ.js +0 -373
  53. package/dist/chunk-PR5QKMHM.js +0 -46
  54. package/dist/chunk-R44EOUS4.js +0 -288
  55. package/dist/chunk-R7FVSCQW.js +0 -415
  56. package/dist/chunk-RIP2MIZL.js +0 -710
  57. package/dist/chunk-RKMMEML5.js +0 -46
  58. package/dist/chunk-RM2BGDBB.js +0 -400
  59. package/dist/chunk-RREREIGW.js +0 -710
  60. package/dist/chunk-TQHLFQY4.js +0 -448
  61. package/dist/chunk-TY3SSQQK.js +0 -688
  62. package/dist/chunk-UGKPOCN5.js +0 -710
  63. package/dist/chunk-UIGKSCTD.js +0 -406
  64. package/dist/chunk-UWIVJRTI.js +0 -343
  65. package/dist/chunk-VA6WXRWB.js +0 -636
  66. package/dist/chunk-XC2FV57P.js +0 -385
  67. package/dist/chunk-XOQIADCH.js +0 -223
  68. package/dist/chunk-ZAQZKEH4.js +0 -46
  69. package/dist/chunk-ZBMOGUSJ.js +0 -17
  70. package/dist/chunk-ZXP6ENQY.js +0 -244
  71. package/dist/index.cjs +0 -1269
  72. package/dist/index.d.cts +0 -482
@@ -1,636 +0,0 @@
1
- import {
2
- REMIX_ERROR_CODES
3
- } from "./chunk-GC2MOT3U.js";
4
- import {
5
- RemixError
6
- } from "./chunk-YZ34ICNN.js";
7
-
8
- // src/infrastructure/repo/gitRepo.ts
9
- import fs from "fs/promises";
10
- import { createHash } from "crypto";
11
- import os from "os";
12
- import path from "path";
13
- import { execa } from "execa";
14
- var GIT_REMOTE_PROTOCOL_RE = /^(https?|ssh):\/\//i;
15
- var SCP_LIKE_GIT_REMOTE_RE = /^(?<user>[^@\s]+)@(?<host>[^:\s]+):(?<path>[^\\\s]+)$/;
16
- var CANONICAL_GIT_REMOTE_RE = /^(?<host>(?:localhost|[a-z0-9.-]+))\/(?<path>[^\\\s]+)$/i;
17
- async function runGit(args, cwd) {
18
- const res = await execa("git", args, { cwd, stderr: "ignore" });
19
- return String(res.stdout || "").trim();
20
- }
21
- async function runGitWithEnv(args, cwd, env) {
22
- const res = await execa("git", args, { cwd, env, stderr: "ignore" });
23
- return String(res.stdout || "").trim();
24
- }
25
- async function runGitRaw(args, cwd) {
26
- const res = await execa("git", args, { cwd, stderr: "ignore", stripFinalNewline: false });
27
- return String(res.stdout || "");
28
- }
29
- async function runGitRawWithEnv(args, cwd, env) {
30
- const res = await execa("git", args, { cwd, env, stderr: "ignore", stripFinalNewline: false });
31
- return String(res.stdout || "");
32
- }
33
- async function runGitDetailed(args, cwd) {
34
- const res = await execa("git", args, { cwd, reject: false });
35
- return {
36
- exitCode: res.exitCode ?? 1,
37
- stdout: String(res.stdout || ""),
38
- stderr: String(res.stderr || "")
39
- };
40
- }
41
- function cleanRepoPath(value) {
42
- return value.trim().replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "");
43
- }
44
- function normalizeGitRemote(remote) {
45
- const raw = String(remote ?? "").trim();
46
- if (!raw) return null;
47
- const canonicalMatch = raw.match(CANONICAL_GIT_REMOTE_RE);
48
- if (canonicalMatch?.groups?.host && canonicalMatch.groups.path) {
49
- const repoPath = cleanRepoPath(canonicalMatch.groups.path);
50
- if (!repoPath) return null;
51
- return `${canonicalMatch.groups.host}/${repoPath}`.toLowerCase();
52
- }
53
- if (GIT_REMOTE_PROTOCOL_RE.test(raw)) {
54
- try {
55
- const url = new URL(raw);
56
- const repoPath = cleanRepoPath(url.pathname);
57
- if (!url.hostname || !repoPath) return null;
58
- return `${url.hostname}/${repoPath}`.toLowerCase();
59
- } catch {
60
- return null;
61
- }
62
- }
63
- const scpMatch = raw.match(SCP_LIKE_GIT_REMOTE_RE);
64
- if (scpMatch?.groups?.host && scpMatch.groups.path) {
65
- const repoPath = cleanRepoPath(scpMatch.groups.path);
66
- if (!repoPath) return null;
67
- return `${scpMatch.groups.host}/${repoPath}`.toLowerCase();
68
- }
69
- return null;
70
- }
71
- function sanitizeRefFragment(value) {
72
- return value.trim().replace(/[^A-Za-z0-9._/-]+/g, "-").replace(/\/{2,}/g, "/").replace(/^\/+|\/+$/g, "").replace(/^-+|-+$/g, "").slice(0, 120);
73
- }
74
- function normalizeRepoRelativePath(value) {
75
- const normalized = path.posix.normalize(value.replace(/\\/g, "/").trim());
76
- if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../") || path.posix.isAbsolute(normalized)) {
77
- throw new RemixError("Git returned an invalid repository-relative path.", {
78
- exitCode: 1,
79
- hint: `Path: ${value}`
80
- });
81
- }
82
- return normalized;
83
- }
84
- function sha256Hex(value) {
85
- return createHash("sha256").update(value).digest("hex");
86
- }
87
- function resolveGitReportedPath(cwd, reportedPath) {
88
- const value = reportedPath.trim();
89
- if (!value) {
90
- throw new RemixError("Git returned an empty internal path.", { exitCode: 1 });
91
- }
92
- return path.isAbsolute(value) ? value : path.resolve(cwd, value);
93
- }
94
- function parseGitNameStatusZ(stdout) {
95
- const tokens = stdout.split("\0");
96
- const entries = [];
97
- for (let i = 0; i < tokens.length; i += 1) {
98
- const rawToken = tokens[i];
99
- if (!rawToken) continue;
100
- let status = rawToken;
101
- let inlinePath = null;
102
- const tabIdx = rawToken.indexOf(" ");
103
- if (tabIdx >= 0) {
104
- status = rawToken.slice(0, tabIdx);
105
- inlinePath = rawToken.slice(tabIdx + 1) || null;
106
- }
107
- const kind = status[0]?.toUpperCase() ?? "";
108
- if (kind === "R" || kind === "C") {
109
- const oldPath = inlinePath ?? tokens[++i] ?? "";
110
- const newPath = tokens[++i] ?? "";
111
- if (!newPath) continue;
112
- entries.push({
113
- status,
114
- path: normalizeRepoRelativePath(newPath),
115
- oldPath: oldPath ? normalizeRepoRelativePath(oldPath) : null
116
- });
117
- continue;
118
- }
119
- const nextPath = inlinePath ?? tokens[++i] ?? "";
120
- if (!nextPath) continue;
121
- entries.push({
122
- status,
123
- path: normalizeRepoRelativePath(nextPath),
124
- oldPath: null
125
- });
126
- }
127
- return entries;
128
- }
129
- function buildStagePlan(entries) {
130
- const updatePaths = /* @__PURE__ */ new Set();
131
- const addPaths = /* @__PURE__ */ new Set();
132
- for (const entry of entries) {
133
- const kind = entry.status[0]?.toUpperCase() ?? "";
134
- if (kind === "A" || kind === "C") {
135
- addPaths.add(entry.path);
136
- continue;
137
- }
138
- if (kind === "R") {
139
- if (entry.oldPath) updatePaths.add(entry.oldPath);
140
- addPaths.add(entry.path);
141
- continue;
142
- }
143
- updatePaths.add(entry.path);
144
- }
145
- const expectedPaths = /* @__PURE__ */ new Set([...updatePaths, ...addPaths]);
146
- return {
147
- updatePaths: Array.from(updatePaths).sort(),
148
- addPaths: Array.from(addPaths).sort(),
149
- expectedPaths: Array.from(expectedPaths).sort()
150
- };
151
- }
152
- async function findGitRoot(startDir) {
153
- try {
154
- const root = await runGit(["rev-parse", "--show-toplevel"], startDir);
155
- if (!root) throw new Error("empty");
156
- return root;
157
- } catch {
158
- throw new RemixError("Not inside a git repository.", {
159
- exitCode: 2,
160
- hint: "Run this command from the root of the repository or one of its subdirectories."
161
- });
162
- }
163
- }
164
- async function getAbsoluteGitDir(cwd) {
165
- try {
166
- const gitDir = await runGit(["rev-parse", "--absolute-git-dir"], cwd);
167
- return resolveGitReportedPath(cwd, gitDir);
168
- } catch {
169
- throw new RemixError("Failed to resolve the git directory.", {
170
- exitCode: 1,
171
- hint: "Ensure the current working directory is inside a valid git repository."
172
- });
173
- }
174
- }
175
- async function getGitCommonDir(cwd) {
176
- try {
177
- const commonDir = await runGit(["rev-parse", "--git-common-dir"], cwd);
178
- return resolveGitReportedPath(cwd, commonDir);
179
- } catch {
180
- throw new RemixError("Failed to resolve the git common directory.", {
181
- exitCode: 1,
182
- hint: "Ensure the current working directory is inside a valid git repository."
183
- });
184
- }
185
- }
186
- async function getGitPath(cwd, relativePath) {
187
- try {
188
- const gitPath = await runGit(["rev-parse", "--git-path", relativePath], cwd);
189
- return resolveGitReportedPath(cwd, gitPath);
190
- } catch {
191
- throw new RemixError("Failed to resolve a git-managed path.", {
192
- exitCode: 1,
193
- hint: `Path: ${relativePath}`
194
- });
195
- }
196
- }
197
- async function getCurrentBranch(cwd) {
198
- try {
199
- const branch = await runGit(["branch", "--show-current"], cwd);
200
- return branch || null;
201
- } catch {
202
- return null;
203
- }
204
- }
205
- async function checkoutLocalBranch(cwd, branchName) {
206
- const normalized = sanitizeRefFragment(branchName);
207
- if (!normalized) {
208
- throw new RemixError("Preferred local branch name is invalid.", {
209
- exitCode: 2,
210
- hint: `Branch: ${branchName}`
211
- });
212
- }
213
- const res = await runGitDetailed(["checkout", "-B", normalized], cwd);
214
- if (res.exitCode !== 0) {
215
- const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
216
- throw new RemixError("Failed to prepare the preferred local branch.", {
217
- exitCode: 1,
218
- hint: detail || `Git could not switch the checkout to ${normalized}.`
219
- });
220
- }
221
- return normalized;
222
- }
223
- async function getRemoteOriginUrl(cwd) {
224
- try {
225
- const url = await runGit(["config", "--get", "remote.origin.url"], cwd);
226
- return url || null;
227
- } catch {
228
- return null;
229
- }
230
- }
231
- async function setRemoteOriginUrl(cwd, remoteUrl) {
232
- const normalized = String(remoteUrl ?? "").trim();
233
- if (!normalized) return;
234
- const existing = await runGitDetailed(["remote", "get-url", "origin"], cwd);
235
- if (existing.exitCode === 0) {
236
- const setUrl = await runGitDetailed(["remote", "set-url", "origin", normalized], cwd);
237
- if (setUrl.exitCode !== 0) {
238
- const detail = [setUrl.stderr.trim(), setUrl.stdout.trim()].filter(Boolean).join("\n\n");
239
- throw new RemixError("Failed to update the local git origin remote.", {
240
- exitCode: 1,
241
- hint: detail || normalized
242
- });
243
- }
244
- return;
245
- }
246
- const addRemote = await runGitDetailed(["remote", "add", "origin", normalized], cwd);
247
- if (addRemote.exitCode !== 0) {
248
- const detail = [addRemote.stderr.trim(), addRemote.stdout.trim()].filter(Boolean).join("\n\n");
249
- throw new RemixError("Failed to write the local git origin remote.", {
250
- exitCode: 1,
251
- hint: detail || normalized
252
- });
253
- }
254
- }
255
- async function getDefaultBranch(cwd) {
256
- try {
257
- const ref = await runGit(["symbolic-ref", "refs/remotes/origin/HEAD"], cwd);
258
- if (!ref) return null;
259
- const suffix = ref.replace(/^refs\/remotes\/origin\//, "").trim();
260
- return suffix || null;
261
- } catch {
262
- return null;
263
- }
264
- }
265
- async function listUntrackedFiles(cwd) {
266
- try {
267
- const out = await runGit(["ls-files", "--others", "--exclude-standard"], cwd);
268
- return out.split("\n").map((line) => line.trim()).filter(Boolean);
269
- } catch {
270
- return [];
271
- }
272
- }
273
- async function getWorkingTreeDiff(cwd) {
274
- const untracked = await listUntrackedFiles(cwd);
275
- if (untracked.length > 0) {
276
- throw new RemixError("Untracked files are not included in git diff mode.", {
277
- exitCode: 2,
278
- hint: "Provide `--diff-file`/`--diff-stdin`, or add the files to git before running `remix collab add`."
279
- });
280
- }
281
- try {
282
- return await runGitRaw(["diff", "--binary", "--no-ext-diff", "HEAD"], cwd);
283
- } catch {
284
- throw new RemixError("Failed to generate git diff.", { exitCode: 1 });
285
- }
286
- }
287
- async function getWorkspaceDiff(cwd) {
288
- const snapshot = await getWorkspaceSnapshot(cwd);
289
- return {
290
- diff: snapshot.diff,
291
- includedUntrackedPaths: snapshot.includedUntrackedPaths
292
- };
293
- }
294
- async function getWorkspaceSnapshot(cwd) {
295
- const headCommitHash = await getHeadCommitHash(cwd);
296
- if (!headCommitHash) {
297
- throw new RemixError("Failed to resolve local HEAD commit.", { exitCode: 1 });
298
- }
299
- const includedUntrackedPaths = Array.from(new Set((await listUntrackedFiles(cwd)).map((entry) => normalizeRepoRelativePath(entry))));
300
- const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "remix-index-"));
301
- const tempIndexPath = path.join(tempDir, "index");
302
- const env = { ...process.env, GIT_INDEX_FILE: tempIndexPath };
303
- try {
304
- try {
305
- await runGitWithEnv(["read-tree", "HEAD"], cwd, env);
306
- await runGitWithEnv(["add", "-A", "--", "."], cwd, env);
307
- const diff = await runGitRawWithEnv(["diff", "--binary", "--no-ext-diff", "--cached", "HEAD"], cwd, env);
308
- const nameStatus = await runGitRawWithEnv(["diff", "--name-status", "--find-renames", "-z", "--cached", "HEAD"], cwd, env);
309
- const pathEntries = parseGitNameStatusZ(nameStatus);
310
- return {
311
- diff,
312
- diffSha256: sha256Hex(diff),
313
- includedUntrackedPaths,
314
- pathEntries,
315
- stagePlan: buildStagePlan(pathEntries)
316
- };
317
- } catch {
318
- throw new RemixError("Failed to generate workspace diff.", {
319
- exitCode: 1,
320
- hint: "Git could not snapshot the current workspace into an isolated index."
321
- });
322
- }
323
- } finally {
324
- await fs.rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
325
- }
326
- }
327
- async function writeTempUnifiedDiffBackup(diff, prefix = "remix-add-backup") {
328
- const safePrefix = prefix.replace(/[^a-zA-Z0-9._-]+/g, "-") || "remix-add-backup";
329
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), `${safePrefix}-`));
330
- const backupPath = path.join(tmpDir, "submitted.diff");
331
- await fs.writeFile(backupPath, diff, "utf8");
332
- return { backupPath, diffSha256: sha256Hex(diff) };
333
- }
334
- async function applyUnifiedDiffToWorktree(cwd, diff, operation = "`remix collab pull`") {
335
- const normalizedDiff = String(diff ?? "");
336
- if (!normalizedDiff.trim()) {
337
- throw new RemixError("Diff is empty.", { exitCode: 2, hint: `${operation} requires a non-empty diff.` });
338
- }
339
- const { backupPath, diffSha256 } = await writeTempUnifiedDiffBackup(normalizedDiff, "remix-apply-diff");
340
- try {
341
- const applyRes = await runGitDetailed(["apply", "--binary", "--whitespace=nowarn", backupPath], cwd);
342
- if (applyRes.exitCode !== 0) {
343
- const detail = [applyRes.stderr.trim(), applyRes.stdout.trim()].filter(Boolean).join("\n\n");
344
- throw new RemixError("Failed to apply unified diff to the local working tree.", {
345
- exitCode: 1,
346
- hint: detail || `${operation} could not apply the server diff to the current working tree.`
347
- });
348
- }
349
- return { diffSha256 };
350
- } finally {
351
- await fs.rm(path.dirname(backupPath), { recursive: true, force: true }).catch(() => void 0);
352
- }
353
- }
354
- async function getHeadCommitHash(cwd) {
355
- try {
356
- const hash = await runGit(["rev-parse", "HEAD"], cwd);
357
- return hash || null;
358
- } catch {
359
- return null;
360
- }
361
- }
362
- async function createGitBundle(cwd, bundleName = "repository.bundle") {
363
- const headCommitHash = await getHeadCommitHash(cwd);
364
- if (!headCommitHash) {
365
- throw new RemixError("Failed to resolve local HEAD commit.", { exitCode: 1 });
366
- }
367
- const safeName = bundleName.replace(/[^a-zA-Z0-9._-]+/g, "_");
368
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "remix-bundle-"));
369
- const bundlePath = path.join(tmpDir, safeName);
370
- const res = await runGitDetailed(["bundle", "create", bundlePath, "--all"], cwd);
371
- if (res.exitCode !== 0) {
372
- const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
373
- throw new RemixError("Failed to create repository bundle.", {
374
- exitCode: 1,
375
- hint: detail || "Git could not create the bundle artifact."
376
- });
377
- }
378
- return { bundlePath, headCommitHash };
379
- }
380
- async function getWorktreeStatus(cwd) {
381
- try {
382
- const out = await runGit(["status", "--porcelain"], cwd);
383
- const entries = out.split("\n").map((line) => line.trimEnd()).filter(Boolean);
384
- return { isClean: entries.length === 0, entries };
385
- } catch {
386
- return { isClean: false, entries: [] };
387
- }
388
- }
389
- async function captureRepoSnapshot(cwd, options) {
390
- const [branch, headCommitHash, worktreeStatus] = await Promise.all([
391
- getCurrentBranch(cwd),
392
- getHeadCommitHash(cwd),
393
- getWorktreeStatus(cwd)
394
- ]);
395
- const workspaceDiffSha256 = options?.includeWorkspaceDiffHash ? (await getWorkspaceSnapshot(cwd)).diffSha256 : null;
396
- return {
397
- branch,
398
- headCommitHash,
399
- statusEntries: worktreeStatus.entries,
400
- statusSha256: sha256Hex(worktreeStatus.entries.join("\n")),
401
- workspaceDiffSha256
402
- };
403
- }
404
- async function assertRepoSnapshotUnchanged(cwd, snapshot, params) {
405
- const current = await captureRepoSnapshot(cwd, {
406
- includeWorkspaceDiffHash: snapshot.workspaceDiffSha256 !== null
407
- });
408
- const changes = [];
409
- if (current.branch !== snapshot.branch) {
410
- changes.push(`Branch changed: ${snapshot.branch ?? "(detached)"} -> ${current.branch ?? "(detached)"}`);
411
- }
412
- if (current.headCommitHash !== snapshot.headCommitHash) {
413
- changes.push(`HEAD changed: ${snapshot.headCommitHash ?? "(missing)"} -> ${current.headCommitHash ?? "(missing)"}`);
414
- }
415
- if (current.statusSha256 !== snapshot.statusSha256) {
416
- changes.push("Working tree status changed.");
417
- }
418
- if (snapshot.workspaceDiffSha256 !== null && current.workspaceDiffSha256 !== snapshot.workspaceDiffSha256) {
419
- changes.push("Workspace diff changed.");
420
- }
421
- if (changes.length === 0) return;
422
- const operation = params?.operation?.trim() || "this Remix operation";
423
- const hint = [
424
- `Operation: ${operation}`,
425
- ...changes,
426
- params?.recoveryHint?.trim() || "Review the local changes, then rerun the operation."
427
- ].filter(Boolean).join("\n");
428
- throw new RemixError("Repository state changed during the operation.", {
429
- code: REMIX_ERROR_CODES.REPO_STATE_CHANGED_DURING_OPERATION,
430
- exitCode: 2,
431
- hint
432
- });
433
- }
434
- async function ensureCleanWorktree(cwd, operation = "`remix collab sync`") {
435
- const status = await getWorktreeStatus(cwd);
436
- if (status.isClean) return;
437
- const preview = status.entries.slice(0, 10).join("\n");
438
- const suffix = status.entries.length > 10 ? `
439
- ...and ${status.entries.length - 10} more` : "";
440
- throw new RemixError(`Working tree must be clean before running ${operation}.`, {
441
- exitCode: 2,
442
- hint: `Commit, stash, or discard local changes first.
443
-
444
- ${preview}${suffix}`
445
- });
446
- }
447
- async function discardTrackedChanges(cwd, operation = "`remix collab add`") {
448
- const res = await runGitDetailed(["reset", "--hard", "HEAD"], cwd);
449
- if (res.exitCode !== 0) {
450
- const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
451
- throw new RemixError(`Failed to discard local tracked changes while running ${operation}.`, {
452
- exitCode: 1,
453
- hint: detail || "Git could not reset tracked changes back to HEAD."
454
- });
455
- }
456
- const hash = await getHeadCommitHash(cwd);
457
- if (!hash) {
458
- throw new RemixError("Failed to resolve local HEAD after discarding tracked changes.", { exitCode: 1 });
459
- }
460
- return hash;
461
- }
462
- async function requireCurrentBranch(cwd) {
463
- const branch = await getCurrentBranch(cwd);
464
- if (!branch) {
465
- throw new RemixError("`remix collab sync` requires a checked out local branch.", {
466
- exitCode: 2,
467
- hint: "Checkout a branch before syncing."
468
- });
469
- }
470
- return branch;
471
- }
472
- async function importGitBundle(cwd, bundlePath, bundleRef) {
473
- const verifyRes = await runGitDetailed(["bundle", "verify", bundlePath], cwd);
474
- if (verifyRes.exitCode !== 0) {
475
- const detail = [verifyRes.stderr.trim(), verifyRes.stdout.trim()].filter(Boolean).join("\n\n");
476
- throw new RemixError("Failed to verify sync bundle.", {
477
- exitCode: 1,
478
- hint: detail || "Git bundle verification failed."
479
- });
480
- }
481
- const fetchRes = await runGitDetailed(["fetch", "--quiet", bundlePath, bundleRef], cwd);
482
- if (fetchRes.exitCode !== 0) {
483
- const detail = [fetchRes.stderr.trim(), fetchRes.stdout.trim()].filter(Boolean).join("\n\n");
484
- throw new RemixError("Failed to import sync bundle.", {
485
- exitCode: 1,
486
- hint: detail || "Git could not fetch objects from the sync bundle."
487
- });
488
- }
489
- }
490
- async function cloneGitBundleToDirectory(bundlePath, targetDir) {
491
- const parentDir = path.dirname(targetDir);
492
- const cloneRes = await runGitDetailed(["clone", bundlePath, targetDir], parentDir);
493
- if (cloneRes.exitCode !== 0) {
494
- const detail = [cloneRes.stderr.trim(), cloneRes.stdout.trim()].filter(Boolean).join("\n\n");
495
- throw new RemixError("Failed to create local remix checkout.", {
496
- exitCode: 1,
497
- hint: detail || "Git could not clone the remix repository bundle."
498
- });
499
- }
500
- const remoteRemoveRes = await runGitDetailed(["remote", "remove", "origin"], targetDir);
501
- if (remoteRemoveRes.exitCode !== 0) {
502
- const detail = [remoteRemoveRes.stderr.trim(), remoteRemoveRes.stdout.trim()].filter(Boolean).join("\n\n");
503
- throw new RemixError("Failed to finalize local remix checkout.", {
504
- exitCode: 1,
505
- hint: detail || "Git could not remove the temporary bundle origin."
506
- });
507
- }
508
- }
509
- async function ensureGitInfoExcludeEntries(cwd, entries) {
510
- const excludePath = await getGitPath(cwd, "info/exclude");
511
- await fs.mkdir(path.dirname(excludePath), { recursive: true });
512
- let current = "";
513
- try {
514
- current = await fs.readFile(excludePath, "utf8");
515
- } catch {
516
- }
517
- const lines = new Set(current.split("\n").map((line) => line.trim()).filter(Boolean));
518
- let changed = false;
519
- for (const entry of entries) {
520
- const normalized = entry.trim();
521
- if (!normalized || lines.has(normalized)) continue;
522
- lines.add(normalized);
523
- changed = true;
524
- }
525
- if (!changed) return;
526
- await fs.writeFile(excludePath, `${Array.from(lines).join("\n")}
527
- `, "utf8");
528
- }
529
- async function ensureCommitExists(cwd, commitHash) {
530
- const res = await runGitDetailed(["cat-file", "-e", `${commitHash}^{commit}`], cwd);
531
- if (res.exitCode === 0) return;
532
- throw new RemixError("Expected target commit is missing after bundle import.", {
533
- exitCode: 1,
534
- hint: `Commit ${commitHash} is not available in the local repository.`
535
- });
536
- }
537
- async function fastForwardToCommit(cwd, commitHash) {
538
- const res = await runGitDetailed(["merge", "--ff-only", commitHash], cwd);
539
- if (res.exitCode !== 0) {
540
- const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
541
- throw new RemixError("Failed to fast-forward local branch.", {
542
- exitCode: 1,
543
- hint: detail || "Git could not fast-forward to the target commit."
544
- });
545
- }
546
- const hash = await getHeadCommitHash(cwd);
547
- if (!hash) throw new RemixError("Failed to resolve local HEAD after fast-forward sync.", { exitCode: 1 });
548
- return hash;
549
- }
550
- async function createBackupBranch(cwd, params) {
551
- const sourceCommitHash = params?.sourceCommitHash?.trim() || await getHeadCommitHash(cwd);
552
- if (!sourceCommitHash) {
553
- throw new RemixError("Failed to resolve local HEAD before creating reconcile backup.", { exitCode: 1 });
554
- }
555
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
556
- const branchFragment = sanitizeRefFragment(params?.branchName?.trim() || "current-branch");
557
- const prefix = sanitizeRefFragment(params?.prefix?.trim() || "remix/reconcile-backup");
558
- const backupBranchName = `${prefix}/${branchFragment}-${timestamp}`;
559
- const createRes = await runGitDetailed(["branch", backupBranchName, sourceCommitHash], cwd);
560
- if (createRes.exitCode !== 0) {
561
- const detail = [createRes.stderr.trim(), createRes.stdout.trim()].filter(Boolean).join("\n\n");
562
- throw new RemixError("Failed to create reconcile backup branch.", {
563
- exitCode: 1,
564
- hint: detail || "Git could not create the safety backup branch."
565
- });
566
- }
567
- return { branchName: backupBranchName, commitHash: sourceCommitHash };
568
- }
569
- async function hardResetToCommit(cwd, commitHash, operation = "`remix collab reconcile`") {
570
- const res = await runGitDetailed(["reset", "--hard", commitHash], cwd);
571
- if (res.exitCode !== 0) {
572
- const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
573
- throw new RemixError(`Failed to move local branch while running ${operation}.`, {
574
- exitCode: 1,
575
- hint: detail || `Git could not reset the current branch to ${commitHash}.`
576
- });
577
- }
578
- const hash = await getHeadCommitHash(cwd);
579
- if (!hash) {
580
- throw new RemixError("Failed to resolve local HEAD after resetting branch.", { exitCode: 1 });
581
- }
582
- return hash;
583
- }
584
- async function buildRepoFingerprint(params) {
585
- const remote = normalizeGitRemote(params.remoteUrl);
586
- const defaultBranch = params.defaultBranch?.trim().toLowerCase() || "";
587
- const payload = remote ? { remote, defaultBranch } : { local: path.resolve(params.gitRoot).toLowerCase(), defaultBranch };
588
- return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
589
- }
590
- function summarizeUnifiedDiff(diff) {
591
- const lines = diff.split("\n");
592
- let changedFilesCount = 0;
593
- let insertions = 0;
594
- let deletions = 0;
595
- for (const line of lines) {
596
- if (line.startsWith("diff --git ")) changedFilesCount += 1;
597
- if (line.startsWith("+") && !line.startsWith("+++")) insertions += 1;
598
- if (line.startsWith("-") && !line.startsWith("---")) deletions += 1;
599
- }
600
- return { changedFilesCount, insertions, deletions };
601
- }
602
-
603
- export {
604
- normalizeGitRemote,
605
- findGitRoot,
606
- getAbsoluteGitDir,
607
- getGitCommonDir,
608
- getGitPath,
609
- getCurrentBranch,
610
- checkoutLocalBranch,
611
- getRemoteOriginUrl,
612
- setRemoteOriginUrl,
613
- getDefaultBranch,
614
- listUntrackedFiles,
615
- getWorkingTreeDiff,
616
- getWorkspaceDiff,
617
- writeTempUnifiedDiffBackup,
618
- applyUnifiedDiffToWorktree,
619
- getHeadCommitHash,
620
- createGitBundle,
621
- getWorktreeStatus,
622
- captureRepoSnapshot,
623
- assertRepoSnapshotUnchanged,
624
- ensureCleanWorktree,
625
- discardTrackedChanges,
626
- requireCurrentBranch,
627
- importGitBundle,
628
- cloneGitBundleToDirectory,
629
- ensureGitInfoExcludeEntries,
630
- ensureCommitExists,
631
- fastForwardToCommit,
632
- createBackupBranch,
633
- hardResetToCommit,
634
- buildRepoFingerprint,
635
- summarizeUnifiedDiff
636
- };