@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,286 @@
1
+ import {
2
+ ComergeError
3
+ } from "./chunk-ZBMOGUSJ.js";
4
+
5
+ // src/api/client.ts
6
+ async function readJsonSafe(res) {
7
+ const ct = res.headers.get("content-type") ?? "";
8
+ if (!ct.toLowerCase().includes("application/json")) return null;
9
+ try {
10
+ return await res.json();
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+ function createApiClient(config, opts) {
16
+ const apiKey = (opts?.apiKey ?? "").trim();
17
+ const tokenProvider = opts?.tokenProvider;
18
+ const CLIENT_KEY_HEADER = "x-comerge-api-key";
19
+ async function request(path, init) {
20
+ if (!tokenProvider) {
21
+ throw new ComergeError("API client is missing a token provider.", {
22
+ exitCode: 1,
23
+ hint: "Configure auth before creating the Comerge API client."
24
+ });
25
+ }
26
+ const auth = await tokenProvider();
27
+ const url = new URL(path, config.apiUrl).toString();
28
+ const doFetch = async (bearer) => fetch(url, {
29
+ ...init,
30
+ headers: {
31
+ Accept: "application/json",
32
+ "Content-Type": "application/json",
33
+ ...init?.headers ?? {},
34
+ Authorization: `Bearer ${bearer}`,
35
+ ...apiKey ? { [CLIENT_KEY_HEADER]: apiKey } : {}
36
+ }
37
+ });
38
+ let res = await doFetch(auth.token);
39
+ if (res.status === 401 && !auth.fromEnv && auth.session?.refresh_token) {
40
+ const refreshed = await tokenProvider({ forceRefresh: true });
41
+ res = await doFetch(refreshed.token);
42
+ }
43
+ if (!res.ok) {
44
+ const body = await readJsonSafe(res);
45
+ const msg = (body && typeof body === "object" && body && "message" in body && typeof body.message === "string" ? body.message : null) ?? `Request failed (status ${res.status})`;
46
+ throw new ComergeError(msg, { exitCode: 1, hint: body ? JSON.stringify(body, null, 2) : null });
47
+ }
48
+ const json = await readJsonSafe(res);
49
+ return json ?? null;
50
+ }
51
+ async function requestBinary(path, init) {
52
+ if (!tokenProvider) {
53
+ throw new ComergeError("API client is missing a token provider.", {
54
+ exitCode: 1,
55
+ hint: "Configure auth before creating the Comerge API client."
56
+ });
57
+ }
58
+ const auth = await tokenProvider();
59
+ const url = new URL(path, config.apiUrl).toString();
60
+ const doFetch = async (bearer) => fetch(url, {
61
+ ...init,
62
+ headers: {
63
+ Accept: "*/*",
64
+ ...init?.headers ?? {},
65
+ Authorization: `Bearer ${bearer}`,
66
+ ...apiKey ? { [CLIENT_KEY_HEADER]: apiKey } : {}
67
+ }
68
+ });
69
+ let res = await doFetch(auth.token);
70
+ if (res.status === 401 && !auth.fromEnv && auth.session?.refresh_token) {
71
+ const refreshed = await tokenProvider({ forceRefresh: true });
72
+ res = await doFetch(refreshed.token);
73
+ }
74
+ if (!res.ok) {
75
+ const body = await readJsonSafe(res);
76
+ const msg = (body && typeof body === "object" && body && "message" in body && typeof body.message === "string" ? body.message : null) ?? `Request failed (status ${res.status})`;
77
+ throw new ComergeError(msg, { exitCode: 1, hint: body ? JSON.stringify(body, null, 2) : null });
78
+ }
79
+ const contentDisposition = res.headers.get("content-disposition") ?? "";
80
+ const fileNameMatch = contentDisposition.match(/filename=\"([^\"]+)\"/i);
81
+ return {
82
+ data: Buffer.from(await res.arrayBuffer()),
83
+ fileName: fileNameMatch?.[1] ?? null,
84
+ contentType: res.headers.get("content-type")
85
+ };
86
+ }
87
+ return {
88
+ getMe: () => request("/v1/me", { method: "GET" }),
89
+ listOrganizations: () => request("/v1/organizations", { method: "GET" }),
90
+ getOrganization: (orgId) => request(`/v1/organizations/${encodeURIComponent(orgId)}`, { method: "GET" }),
91
+ listProjects: (params) => {
92
+ const qs = new URLSearchParams();
93
+ if (params?.organizationId) qs.set("organizationId", params.organizationId);
94
+ if (params?.clientAppId) qs.set("clientAppId", params.clientAppId);
95
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
96
+ return request(`/v1/projects${suffix}`, { method: "GET" });
97
+ },
98
+ getProject: (projectId) => request(`/v1/projects/${encodeURIComponent(projectId)}`, { method: "GET" }),
99
+ resolveProjectBinding: (params) => {
100
+ const qs = new URLSearchParams();
101
+ if (params.repoFingerprint) qs.set("repoFingerprint", params.repoFingerprint);
102
+ if (params.remoteUrl) qs.set("remoteUrl", params.remoteUrl);
103
+ return request(`/v1/projects/bindings/resolve?${qs.toString()}`, { method: "GET" });
104
+ },
105
+ autoEnableDeveloper: () => request("/v1/developer/auto-enable", { method: "POST" }),
106
+ listClientApps: (params) => {
107
+ const qs = params?.orgId ? `?orgId=${encodeURIComponent(params.orgId)}` : "";
108
+ return request(`/v1/developer/client-apps${qs}`, { method: "GET" });
109
+ },
110
+ createClientApp: (payload) => request("/v1/developer/client-apps", { method: "POST", body: JSON.stringify(payload) }),
111
+ createClientAppKey: (clientAppId, payload) => request(`/v1/developer/client-apps/${encodeURIComponent(clientAppId)}/keys`, {
112
+ method: "POST",
113
+ body: JSON.stringify(payload ?? {})
114
+ }),
115
+ listApps: (params) => {
116
+ const qs = new URLSearchParams();
117
+ if (params?.projectId) qs.set("projectId", params.projectId);
118
+ if (params?.organizationId) qs.set("organizationId", params.organizationId);
119
+ if (params?.forked) qs.set("forked", params.forked);
120
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
121
+ return request(`/v1/apps${suffix}`, { method: "GET" });
122
+ },
123
+ getApp: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}`, { method: "GET" }),
124
+ getMergeRequest: (mrId) => request(`/v1/merge-requests/${encodeURIComponent(mrId)}`, { method: "GET" }),
125
+ presignImportUpload: (payload) => request("/v1/apps/import/upload/presign", { method: "POST", body: JSON.stringify(payload) }),
126
+ importFromUpload: (payload) => request("/v1/apps/import/upload", { method: "POST", body: JSON.stringify(payload) }),
127
+ presignImportUploadFirstParty: (payload) => request("/v1/apps/import/upload/presign/first-party", { method: "POST", body: JSON.stringify(payload) }),
128
+ importFromUploadFirstParty: (payload) => request("/v1/apps/import/upload/first-party", { method: "POST", body: JSON.stringify(payload) }),
129
+ importFromGithubFirstParty: (payload) => request("/v1/apps/import/github/first-party", { method: "POST", body: JSON.stringify(payload) }),
130
+ forkApp: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/fork`, { method: "POST", body: JSON.stringify(payload ?? {}) }),
131
+ downloadAppBundle: (appId) => requestBinary(`/v1/apps/${encodeURIComponent(appId)}/download.bundle`, { method: "GET" }),
132
+ createChangeStep: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/change-steps`, {
133
+ method: "POST",
134
+ body: JSON.stringify(payload)
135
+ }),
136
+ createCollabTurn: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/collab-turns`, {
137
+ method: "POST",
138
+ body: JSON.stringify(payload)
139
+ }),
140
+ listCollabTurns: (appId, params) => {
141
+ const qs = new URLSearchParams();
142
+ if (params?.limit !== void 0) qs.set("limit", String(params.limit));
143
+ if (params?.offset !== void 0) qs.set("offset", String(params.offset));
144
+ if (params?.changeStepId) qs.set("changeStepId", params.changeStepId);
145
+ if (params?.threadId) qs.set("threadId", params.threadId);
146
+ if (params?.createdAfter) qs.set("createdAfter", params.createdAfter);
147
+ if (params?.createdBefore) qs.set("createdBefore", params.createdBefore);
148
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
149
+ return request(`/v1/apps/${encodeURIComponent(appId)}/collab-turns${suffix}`, { method: "GET" });
150
+ },
151
+ getCollabTurn: (appId, collabTurnId) => request(`/v1/apps/${encodeURIComponent(appId)}/collab-turns/${encodeURIComponent(collabTurnId)}`, {
152
+ method: "GET"
153
+ }),
154
+ getAgentMemorySummary: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/agent-memory/summary`, { method: "GET" }),
155
+ listAgentMemoryTimeline: (appId, params) => {
156
+ const qs = new URLSearchParams();
157
+ if (params?.limit !== void 0) qs.set("limit", String(params.limit));
158
+ if (params?.offset !== void 0) qs.set("offset", String(params.offset));
159
+ if (params?.createdAfter) qs.set("createdAfter", params.createdAfter);
160
+ if (params?.createdBefore) qs.set("createdBefore", params.createdBefore);
161
+ if (params?.kinds?.length) {
162
+ for (const kind of params.kinds) qs.append("kinds", kind);
163
+ }
164
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
165
+ return request(`/v1/apps/${encodeURIComponent(appId)}/agent-memory/timeline${suffix}`, { method: "GET" });
166
+ },
167
+ searchAgentMemory: (appId, params) => {
168
+ const qs = new URLSearchParams();
169
+ qs.set("q", params.q);
170
+ if (params.limit !== void 0) qs.set("limit", String(params.limit));
171
+ if (params.offset !== void 0) qs.set("offset", String(params.offset));
172
+ if (params.createdAfter) qs.set("createdAfter", params.createdAfter);
173
+ if (params.createdBefore) qs.set("createdBefore", params.createdBefore);
174
+ if (params.kinds?.length) {
175
+ for (const kind of params.kinds) qs.append("kinds", kind);
176
+ }
177
+ return request(`/v1/apps/${encodeURIComponent(appId)}/agent-memory/search?${qs.toString()}`, { method: "GET" });
178
+ },
179
+ getChangeStep: (appId, changeStepId) => request(`/v1/apps/${encodeURIComponent(appId)}/change-steps/${encodeURIComponent(changeStepId)}`, { method: "GET" }),
180
+ getChangeStepDiff: (appId, changeStepId) => request(`/v1/apps/${encodeURIComponent(appId)}/change-steps/${encodeURIComponent(changeStepId)}/diff`, {
181
+ method: "GET"
182
+ }),
183
+ startChangeStepReplay: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/change-steps/replays`, {
184
+ method: "POST",
185
+ body: JSON.stringify(payload)
186
+ }),
187
+ getChangeStepReplay: (appId, replayId) => request(`/v1/apps/${encodeURIComponent(appId)}/change-steps/replays/${encodeURIComponent(replayId)}`, {
188
+ method: "GET"
189
+ }),
190
+ getChangeStepReplayDiff: (appId, replayId) => request(`/v1/apps/${encodeURIComponent(appId)}/change-steps/replays/${encodeURIComponent(replayId)}/diff`, {
191
+ method: "GET"
192
+ }),
193
+ listMergeRequests: (params) => {
194
+ const qs = new URLSearchParams();
195
+ if (params?.sourceAppId) qs.set("sourceAppId", params.sourceAppId);
196
+ if (params?.targetAppId) qs.set("targetAppId", params.targetAppId);
197
+ if (Array.isArray(params?.status)) {
198
+ for (const status of params.status) qs.append("status", status);
199
+ } else if (typeof params?.status === "string") {
200
+ qs.set("status", params.status);
201
+ }
202
+ if (params?.kind) qs.set("kind", params.kind);
203
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
204
+ return request(`/v1/merge-requests${suffix}`, { method: "GET" });
205
+ },
206
+ openMergeRequest: (sourceAppId) => request("/v1/merge-requests", { method: "POST", body: JSON.stringify({ sourceAppId }) }),
207
+ getMergeRequestReview: (mrId) => request(`/v1/merge-requests/${encodeURIComponent(mrId)}/review`, { method: "GET" }),
208
+ updateMergeRequest: (mrId, payload) => request(`/v1/merge-requests/${encodeURIComponent(mrId)}`, { method: "PATCH", body: JSON.stringify(payload) }),
209
+ createOrganizationInvite: (orgId, payload) => request(`/v1/organizations/${encodeURIComponent(orgId)}/invitations`, {
210
+ method: "POST",
211
+ body: JSON.stringify(payload)
212
+ }),
213
+ createProjectInvite: (projectId, payload) => request(`/v1/projects/${encodeURIComponent(projectId)}/invitations`, {
214
+ method: "POST",
215
+ body: JSON.stringify(payload)
216
+ }),
217
+ createAppInvite: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/invitations`, {
218
+ method: "POST",
219
+ body: JSON.stringify(payload)
220
+ }),
221
+ listOrganizationInvites: (orgId) => request(`/v1/organizations/${encodeURIComponent(orgId)}/invitations`, { method: "GET" }),
222
+ listProjectInvites: (projectId) => request(`/v1/projects/${encodeURIComponent(projectId)}/invitations`, { method: "GET" }),
223
+ listAppInvites: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/invitations`, { method: "GET" }),
224
+ resendOrganizationInvite: (orgId, inviteId, payload) => request(`/v1/organizations/${encodeURIComponent(orgId)}/invitations/${encodeURIComponent(inviteId)}/resend`, {
225
+ method: "POST",
226
+ body: JSON.stringify(payload ?? {})
227
+ }),
228
+ resendProjectInvite: (projectId, inviteId, payload) => request(`/v1/projects/${encodeURIComponent(projectId)}/invitations/${encodeURIComponent(inviteId)}/resend`, {
229
+ method: "POST",
230
+ body: JSON.stringify(payload ?? {})
231
+ }),
232
+ resendAppInvite: (appId, inviteId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/invitations/${encodeURIComponent(inviteId)}/resend`, {
233
+ method: "POST",
234
+ body: JSON.stringify(payload ?? {})
235
+ }),
236
+ revokeOrganizationInvite: (orgId, inviteId) => request(`/v1/organizations/${encodeURIComponent(orgId)}/invitations/${encodeURIComponent(inviteId)}`, {
237
+ method: "DELETE"
238
+ }),
239
+ revokeProjectInvite: (projectId, inviteId) => request(`/v1/projects/${encodeURIComponent(projectId)}/invitations/${encodeURIComponent(inviteId)}`, {
240
+ method: "DELETE"
241
+ }),
242
+ revokeAppInvite: (appId, inviteId) => request(`/v1/apps/${encodeURIComponent(appId)}/invitations/${encodeURIComponent(inviteId)}`, {
243
+ method: "DELETE"
244
+ }),
245
+ syncUpstreamApp: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/sync-upstream`, {
246
+ method: "POST",
247
+ body: JSON.stringify({})
248
+ }),
249
+ preflightAppReconcile: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/reconcile/preflight`, {
250
+ method: "POST",
251
+ body: JSON.stringify(payload)
252
+ }),
253
+ startAppReconcile: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/reconcile/start`, {
254
+ method: "POST",
255
+ body: JSON.stringify(payload)
256
+ }),
257
+ getAppReconcile: (appId, reconcileId) => request(`/v1/apps/${encodeURIComponent(appId)}/reconcile/${encodeURIComponent(reconcileId)}`, { method: "GET" }),
258
+ downloadAppReconcileBundle: (appId, reconcileId) => requestBinary(`/v1/apps/${encodeURIComponent(appId)}/reconcile/${encodeURIComponent(reconcileId)}/download.bundle`, {
259
+ method: "GET"
260
+ }),
261
+ syncLocalApp: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/sync-local`, {
262
+ method: "POST",
263
+ body: JSON.stringify(payload)
264
+ }),
265
+ initiateBundle: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/bundles`, { method: "POST", body: JSON.stringify(payload) }),
266
+ getBundle: (appId, bundleId) => request(`/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}`, { method: "GET" }),
267
+ getBundleDownloadUrl: (appId, bundleId, options) => request(
268
+ `/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}/download?redirect=${options?.redirect ?? false}`,
269
+ { method: "GET" }
270
+ ),
271
+ getBundleAssetsDownloadUrl: (appId, bundleId, options) => {
272
+ const qs = new URLSearchParams({
273
+ redirect: String(options?.redirect ?? false),
274
+ kind: options?.kind ?? "metro-assets"
275
+ });
276
+ return request(
277
+ `/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}/assets/download?${qs.toString()}`,
278
+ { method: "GET" }
279
+ );
280
+ }
281
+ };
282
+ }
283
+
284
+ export {
285
+ createApiClient
286
+ };
@@ -0,0 +1,357 @@
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 runGitRaw(args, cwd) {
19
+ const res = await execa("git", args, { cwd, stderr: "ignore", stripFinalNewline: false });
20
+ return String(res.stdout || "");
21
+ }
22
+ async function runGitDetailed(args, cwd) {
23
+ const res = await execa("git", args, { cwd, reject: false });
24
+ return {
25
+ exitCode: res.exitCode ?? 1,
26
+ stdout: String(res.stdout || ""),
27
+ stderr: String(res.stderr || "")
28
+ };
29
+ }
30
+ function cleanRepoPath(value) {
31
+ return value.trim().replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "");
32
+ }
33
+ function normalizeGitRemote(remote) {
34
+ const raw = String(remote ?? "").trim();
35
+ if (!raw) return null;
36
+ const canonicalMatch = raw.match(CANONICAL_GIT_REMOTE_RE);
37
+ if (canonicalMatch?.groups?.host && canonicalMatch.groups.path) {
38
+ const repoPath = cleanRepoPath(canonicalMatch.groups.path);
39
+ if (!repoPath) return null;
40
+ return `${canonicalMatch.groups.host}/${repoPath}`.toLowerCase();
41
+ }
42
+ if (GIT_REMOTE_PROTOCOL_RE.test(raw)) {
43
+ try {
44
+ const url = new URL(raw);
45
+ const repoPath = cleanRepoPath(url.pathname);
46
+ if (!url.hostname || !repoPath) return null;
47
+ return `${url.hostname}/${repoPath}`.toLowerCase();
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+ const scpMatch = raw.match(SCP_LIKE_GIT_REMOTE_RE);
53
+ if (scpMatch?.groups?.host && scpMatch.groups.path) {
54
+ const repoPath = cleanRepoPath(scpMatch.groups.path);
55
+ if (!repoPath) return null;
56
+ return `${scpMatch.groups.host}/${repoPath}`.toLowerCase();
57
+ }
58
+ return null;
59
+ }
60
+ function sanitizeRefFragment(value) {
61
+ return value.trim().replace(/[^A-Za-z0-9._/-]+/g, "-").replace(/\/{2,}/g, "/").replace(/^\/+|\/+$/g, "").replace(/^-+|-+$/g, "").slice(0, 120);
62
+ }
63
+ async function findGitRoot(startDir) {
64
+ try {
65
+ const root = await runGit(["rev-parse", "--show-toplevel"], startDir);
66
+ if (!root) throw new Error("empty");
67
+ return root;
68
+ } catch {
69
+ throw new ComergeError("Not inside a git repository.", {
70
+ exitCode: 2,
71
+ hint: "Run this command from the root of the repository or one of its subdirectories."
72
+ });
73
+ }
74
+ }
75
+ async function getCurrentBranch(cwd) {
76
+ try {
77
+ const branch = await runGit(["branch", "--show-current"], cwd);
78
+ return branch || null;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+ async function getRemoteOriginUrl(cwd) {
84
+ try {
85
+ const url = await runGit(["config", "--get", "remote.origin.url"], cwd);
86
+ return url || null;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+ async function getDefaultBranch(cwd) {
92
+ try {
93
+ const ref = await runGit(["symbolic-ref", "refs/remotes/origin/HEAD"], cwd);
94
+ if (!ref) return null;
95
+ const suffix = ref.replace(/^refs\/remotes\/origin\//, "").trim();
96
+ return suffix || null;
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+ async function listUntrackedFiles(cwd) {
102
+ try {
103
+ const out = await runGit(["ls-files", "--others", "--exclude-standard"], cwd);
104
+ return out.split("\n").map((line) => line.trim()).filter(Boolean);
105
+ } catch {
106
+ return [];
107
+ }
108
+ }
109
+ async function getWorkingTreeDiff(cwd) {
110
+ const untracked = await listUntrackedFiles(cwd);
111
+ if (untracked.length > 0) {
112
+ throw new ComergeError("Untracked files are not included in git diff mode.", {
113
+ exitCode: 2,
114
+ hint: "Provide `--diff-file`/`--diff-stdin`, or add the files to git before running `comerge collab add`."
115
+ });
116
+ }
117
+ try {
118
+ return await runGitRaw(["diff", "--binary", "--no-ext-diff", "HEAD"], cwd);
119
+ } catch {
120
+ throw new ComergeError("Failed to generate git diff.", { exitCode: 1 });
121
+ }
122
+ }
123
+ async function writeTempUnifiedDiffBackup(diff, prefix = "comerge-add-backup") {
124
+ const safePrefix = prefix.replace(/[^a-zA-Z0-9._-]+/g, "-") || "comerge-add-backup";
125
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), `${safePrefix}-`));
126
+ const backupPath = path.join(tmpDir, "submitted.diff");
127
+ await fs.writeFile(backupPath, diff, "utf8");
128
+ return { backupPath };
129
+ }
130
+ async function getHeadCommitHash(cwd) {
131
+ try {
132
+ const hash = await runGit(["rev-parse", "HEAD"], cwd);
133
+ return hash || null;
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+ async function createGitBundle(cwd, bundleName = "repository.bundle") {
139
+ const headCommitHash = await getHeadCommitHash(cwd);
140
+ if (!headCommitHash) {
141
+ throw new ComergeError("Failed to resolve local HEAD commit.", { exitCode: 1 });
142
+ }
143
+ const safeName = bundleName.replace(/[^a-zA-Z0-9._-]+/g, "_");
144
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "comerge-bundle-"));
145
+ const bundlePath = path.join(tmpDir, safeName);
146
+ const res = await runGitDetailed(["bundle", "create", bundlePath, "--all", "--tags"], cwd);
147
+ if (res.exitCode !== 0) {
148
+ const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
149
+ throw new ComergeError("Failed to create repository bundle.", {
150
+ exitCode: 1,
151
+ hint: detail || "Git could not create the bundle artifact."
152
+ });
153
+ }
154
+ return { bundlePath, headCommitHash };
155
+ }
156
+ async function getWorktreeStatus(cwd) {
157
+ try {
158
+ const out = await runGit(["status", "--porcelain"], cwd);
159
+ const entries = out.split("\n").map((line) => line.trimEnd()).filter(Boolean);
160
+ return { isClean: entries.length === 0, entries };
161
+ } catch {
162
+ return { isClean: false, entries: [] };
163
+ }
164
+ }
165
+ async function ensureCleanWorktree(cwd, operation = "`comerge collab sync`") {
166
+ const status = await getWorktreeStatus(cwd);
167
+ if (status.isClean) return;
168
+ const preview = status.entries.slice(0, 10).join("\n");
169
+ const suffix = status.entries.length > 10 ? `
170
+ ...and ${status.entries.length - 10} more` : "";
171
+ throw new ComergeError(`Working tree must be clean before running ${operation}.`, {
172
+ exitCode: 2,
173
+ hint: `Commit, stash, or discard local changes first.
174
+
175
+ ${preview}${suffix}`
176
+ });
177
+ }
178
+ async function discardTrackedChanges(cwd, operation = "`comerge collab add`") {
179
+ const res = await runGitDetailed(["reset", "--hard", "HEAD"], cwd);
180
+ if (res.exitCode !== 0) {
181
+ const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
182
+ throw new ComergeError(`Failed to discard local tracked changes while running ${operation}.`, {
183
+ exitCode: 1,
184
+ hint: detail || "Git could not reset tracked changes back to HEAD."
185
+ });
186
+ }
187
+ const hash = await getHeadCommitHash(cwd);
188
+ if (!hash) {
189
+ throw new ComergeError("Failed to resolve local HEAD after discarding tracked changes.", { exitCode: 1 });
190
+ }
191
+ return hash;
192
+ }
193
+ async function requireCurrentBranch(cwd) {
194
+ const branch = await getCurrentBranch(cwd);
195
+ if (!branch) {
196
+ throw new ComergeError("`comerge collab sync` requires a checked out local branch.", {
197
+ exitCode: 2,
198
+ hint: "Checkout a branch before syncing."
199
+ });
200
+ }
201
+ return branch;
202
+ }
203
+ async function importGitBundle(cwd, bundlePath, bundleRef) {
204
+ const verifyRes = await runGitDetailed(["bundle", "verify", bundlePath], cwd);
205
+ if (verifyRes.exitCode !== 0) {
206
+ const detail = [verifyRes.stderr.trim(), verifyRes.stdout.trim()].filter(Boolean).join("\n\n");
207
+ throw new ComergeError("Failed to verify sync bundle.", {
208
+ exitCode: 1,
209
+ hint: detail || "Git bundle verification failed."
210
+ });
211
+ }
212
+ const fetchRes = await runGitDetailed(["fetch", "--quiet", bundlePath, bundleRef], cwd);
213
+ if (fetchRes.exitCode !== 0) {
214
+ const detail = [fetchRes.stderr.trim(), fetchRes.stdout.trim()].filter(Boolean).join("\n\n");
215
+ throw new ComergeError("Failed to import sync bundle.", {
216
+ exitCode: 1,
217
+ hint: detail || "Git could not fetch objects from the sync bundle."
218
+ });
219
+ }
220
+ }
221
+ async function cloneGitBundleToDirectory(bundlePath, targetDir) {
222
+ const parentDir = path.dirname(targetDir);
223
+ const cloneRes = await runGitDetailed(["clone", bundlePath, targetDir], parentDir);
224
+ if (cloneRes.exitCode !== 0) {
225
+ const detail = [cloneRes.stderr.trim(), cloneRes.stdout.trim()].filter(Boolean).join("\n\n");
226
+ throw new ComergeError("Failed to create local remix checkout.", {
227
+ exitCode: 1,
228
+ hint: detail || "Git could not clone the remix repository bundle."
229
+ });
230
+ }
231
+ const remoteRemoveRes = await runGitDetailed(["remote", "remove", "origin"], targetDir);
232
+ if (remoteRemoveRes.exitCode !== 0) {
233
+ const detail = [remoteRemoveRes.stderr.trim(), remoteRemoveRes.stdout.trim()].filter(Boolean).join("\n\n");
234
+ throw new ComergeError("Failed to finalize local remix checkout.", {
235
+ exitCode: 1,
236
+ hint: detail || "Git could not remove the temporary bundle origin."
237
+ });
238
+ }
239
+ }
240
+ async function ensureGitInfoExcludeEntries(cwd, entries) {
241
+ const excludePath = path.join(cwd, ".git", "info", "exclude");
242
+ await fs.mkdir(path.dirname(excludePath), { recursive: true });
243
+ let current = "";
244
+ try {
245
+ current = await fs.readFile(excludePath, "utf8");
246
+ } catch {
247
+ }
248
+ const lines = new Set(current.split("\n").map((line) => line.trim()).filter(Boolean));
249
+ let changed = false;
250
+ for (const entry of entries) {
251
+ const normalized = entry.trim();
252
+ if (!normalized || lines.has(normalized)) continue;
253
+ lines.add(normalized);
254
+ changed = true;
255
+ }
256
+ if (!changed) return;
257
+ await fs.writeFile(excludePath, `${Array.from(lines).join("\n")}
258
+ `, "utf8");
259
+ }
260
+ async function ensureCommitExists(cwd, commitHash) {
261
+ const res = await runGitDetailed(["cat-file", "-e", `${commitHash}^{commit}`], cwd);
262
+ if (res.exitCode === 0) return;
263
+ throw new ComergeError("Expected target commit is missing after bundle import.", {
264
+ exitCode: 1,
265
+ hint: `Commit ${commitHash} is not available in the local repository.`
266
+ });
267
+ }
268
+ async function fastForwardToCommit(cwd, commitHash) {
269
+ const res = await runGitDetailed(["merge", "--ff-only", commitHash], cwd);
270
+ if (res.exitCode !== 0) {
271
+ const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
272
+ throw new ComergeError("Failed to fast-forward local branch.", {
273
+ exitCode: 1,
274
+ hint: detail || "Git could not fast-forward to the target commit."
275
+ });
276
+ }
277
+ const hash = await getHeadCommitHash(cwd);
278
+ if (!hash) throw new ComergeError("Failed to resolve local HEAD after fast-forward sync.", { exitCode: 1 });
279
+ return hash;
280
+ }
281
+ async function createBackupBranch(cwd, params) {
282
+ const sourceCommitHash = params?.sourceCommitHash?.trim() || await getHeadCommitHash(cwd);
283
+ if (!sourceCommitHash) {
284
+ throw new ComergeError("Failed to resolve local HEAD before creating reconcile backup.", { exitCode: 1 });
285
+ }
286
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
287
+ const branchFragment = sanitizeRefFragment(params?.branchName?.trim() || "current-branch");
288
+ const prefix = sanitizeRefFragment(params?.prefix?.trim() || "comerge/reconcile-backup");
289
+ const backupBranchName = `${prefix}/${branchFragment}-${timestamp}`;
290
+ const createRes = await runGitDetailed(["branch", backupBranchName, sourceCommitHash], cwd);
291
+ if (createRes.exitCode !== 0) {
292
+ const detail = [createRes.stderr.trim(), createRes.stdout.trim()].filter(Boolean).join("\n\n");
293
+ throw new ComergeError("Failed to create reconcile backup branch.", {
294
+ exitCode: 1,
295
+ hint: detail || "Git could not create the safety backup branch."
296
+ });
297
+ }
298
+ return { branchName: backupBranchName, commitHash: sourceCommitHash };
299
+ }
300
+ async function hardResetToCommit(cwd, commitHash, operation = "`comerge collab reconcile`") {
301
+ const res = await runGitDetailed(["reset", "--hard", commitHash], cwd);
302
+ if (res.exitCode !== 0) {
303
+ const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
304
+ throw new ComergeError(`Failed to move local branch while running ${operation}.`, {
305
+ exitCode: 1,
306
+ hint: detail || `Git could not reset the current branch to ${commitHash}.`
307
+ });
308
+ }
309
+ const hash = await getHeadCommitHash(cwd);
310
+ if (!hash) {
311
+ throw new ComergeError("Failed to resolve local HEAD after resetting branch.", { exitCode: 1 });
312
+ }
313
+ return hash;
314
+ }
315
+ async function buildRepoFingerprint(params) {
316
+ const remote = normalizeGitRemote(params.remoteUrl);
317
+ const defaultBranch = params.defaultBranch?.trim().toLowerCase() || "";
318
+ const payload = remote ? { remote, defaultBranch } : { local: path.resolve(params.gitRoot).toLowerCase(), defaultBranch };
319
+ return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
320
+ }
321
+ function summarizeUnifiedDiff(diff) {
322
+ const lines = diff.split("\n");
323
+ let changedFilesCount = 0;
324
+ let insertions = 0;
325
+ let deletions = 0;
326
+ for (const line of lines) {
327
+ if (line.startsWith("diff --git ")) changedFilesCount += 1;
328
+ if (line.startsWith("+") && !line.startsWith("+++")) insertions += 1;
329
+ if (line.startsWith("-") && !line.startsWith("---")) deletions += 1;
330
+ }
331
+ return { changedFilesCount, insertions, deletions };
332
+ }
333
+
334
+ export {
335
+ normalizeGitRemote,
336
+ findGitRoot,
337
+ getCurrentBranch,
338
+ getRemoteOriginUrl,
339
+ getDefaultBranch,
340
+ listUntrackedFiles,
341
+ getWorkingTreeDiff,
342
+ writeTempUnifiedDiffBackup,
343
+ getHeadCommitHash,
344
+ createGitBundle,
345
+ ensureCleanWorktree,
346
+ discardTrackedChanges,
347
+ requireCurrentBranch,
348
+ importGitBundle,
349
+ cloneGitBundleToDirectory,
350
+ ensureGitInfoExcludeEntries,
351
+ ensureCommitExists,
352
+ fastForwardToCommit,
353
+ createBackupBranch,
354
+ hardResetToCommit,
355
+ buildRepoFingerprint,
356
+ summarizeUnifiedDiff
357
+ };