@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.
- package/LICENSE +21 -0
- package/README.md +15 -0
- package/dist/api.d.ts +494 -0
- package/dist/api.js +7 -0
- package/dist/auth.d.ts +27 -0
- package/dist/auth.js +15 -0
- package/dist/binding.d.ts +16 -0
- package/dist/binding.js +11 -0
- package/dist/chunk-2WGZS7CD.js +0 -0
- package/dist/chunk-34WDQCPF.js +242 -0
- package/dist/chunk-4OCNZHHR.js +0 -0
- package/dist/chunk-54CBEP2W.js +570 -0
- package/dist/chunk-55K5GHAZ.js +252 -0
- package/dist/chunk-5H5CZKGN.js +691 -0
- package/dist/chunk-5NTOJXEZ.js +223 -0
- package/dist/chunk-7WUKH3ZD.js +221 -0
- package/dist/chunk-AE2HPMUZ.js +80 -0
- package/dist/chunk-AEAOYVIL.js +200 -0
- package/dist/chunk-BJFCN2C3.js +46 -0
- package/dist/chunk-DCU3646I.js +12 -0
- package/dist/chunk-DEWAIK5X.js +11 -0
- package/dist/chunk-DRD6EVTT.js +447 -0
- package/dist/chunk-E4KAGBU7.js +134 -0
- package/dist/chunk-EF3677RE.js +93 -0
- package/dist/chunk-EVWDYCBL.js +223 -0
- package/dist/chunk-FAZUMWBS.js +93 -0
- package/dist/chunk-GC2MOT3U.js +12 -0
- package/dist/chunk-GFOBGYW4.js +252 -0
- package/dist/chunk-INDDXWAH.js +92 -0
- package/dist/chunk-K57ZFDGC.js +15 -0
- package/dist/chunk-NDA7EJJA.js +286 -0
- package/dist/chunk-NK2DA4X6.js +357 -0
- package/dist/chunk-OJMTW22J.js +286 -0
- package/dist/chunk-OMUDRPUI.js +195 -0
- package/dist/chunk-ONKKRS2C.js +239 -0
- package/dist/chunk-OWFBBWU7.js +196 -0
- package/dist/chunk-P7EM3N73.js +46 -0
- package/dist/chunk-PR5QKMHM.js +46 -0
- package/dist/chunk-RIP2MIZL.js +710 -0
- package/dist/chunk-TQHLFQY4.js +448 -0
- package/dist/chunk-TY3SSQQK.js +688 -0
- package/dist/chunk-UGKPOCN5.js +710 -0
- package/dist/chunk-VM3CGCNX.js +46 -0
- package/dist/chunk-XOQIADCH.js +223 -0
- package/dist/chunk-YZ34ICNN.js +17 -0
- package/dist/chunk-ZBMOGUSJ.js +17 -0
- package/dist/collab.d.ts +680 -0
- package/dist/collab.js +1917 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +9 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.js +12 -0
- package/dist/index.cjs +1269 -0
- package/dist/index.d.cts +482 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +34 -0
- package/dist/repo.d.ts +66 -0
- package/dist/repo.js +62 -0
- package/dist/tokenProvider-BWTusyj4.d.ts +63 -0
- package/package.json +72 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1269 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
CliError: () => CliError,
|
|
34
|
+
buildRepoFingerprint: () => buildRepoFingerprint,
|
|
35
|
+
cloneGitBundleToDirectory: () => cloneGitBundleToDirectory,
|
|
36
|
+
collabAdd: () => collabAdd,
|
|
37
|
+
collabApprove: () => collabApprove,
|
|
38
|
+
collabInbox: () => collabInbox,
|
|
39
|
+
collabInit: () => collabInit,
|
|
40
|
+
collabInvite: () => collabInvite,
|
|
41
|
+
collabList: () => collabList,
|
|
42
|
+
collabReconcile: () => collabReconcile,
|
|
43
|
+
collabReject: () => collabReject,
|
|
44
|
+
collabRemix: () => collabRemix,
|
|
45
|
+
collabRequestMerge: () => collabRequestMerge,
|
|
46
|
+
collabSync: () => collabSync,
|
|
47
|
+
collabSyncUpstream: () => collabSyncUpstream,
|
|
48
|
+
collabView: () => collabView,
|
|
49
|
+
createBackupBranch: () => createBackupBranch,
|
|
50
|
+
createGitBundle: () => createGitBundle,
|
|
51
|
+
discardTrackedChanges: () => discardTrackedChanges,
|
|
52
|
+
ensureCleanWorktree: () => ensureCleanWorktree,
|
|
53
|
+
ensureCommitExists: () => ensureCommitExists,
|
|
54
|
+
ensureGitInfoExcludeEntries: () => ensureGitInfoExcludeEntries,
|
|
55
|
+
fastForwardToCommit: () => fastForwardToCommit,
|
|
56
|
+
findGitRoot: () => findGitRoot,
|
|
57
|
+
getCollabBindingPath: () => getCollabBindingPath,
|
|
58
|
+
getCurrentBranch: () => getCurrentBranch,
|
|
59
|
+
getDefaultBranch: () => getDefaultBranch,
|
|
60
|
+
getHeadCommitHash: () => getHeadCommitHash,
|
|
61
|
+
getRemoteOriginUrl: () => getRemoteOriginUrl,
|
|
62
|
+
getWorkingTreeDiff: () => getWorkingTreeDiff,
|
|
63
|
+
hardResetToCommit: () => hardResetToCommit,
|
|
64
|
+
importGitBundle: () => importGitBundle,
|
|
65
|
+
listUntrackedFiles: () => listUntrackedFiles,
|
|
66
|
+
normalizeGitRemote: () => normalizeGitRemote,
|
|
67
|
+
readCollabBinding: () => readCollabBinding,
|
|
68
|
+
requireCurrentBranch: () => requireCurrentBranch,
|
|
69
|
+
summarizeUnifiedDiff: () => summarizeUnifiedDiff,
|
|
70
|
+
writeCollabBinding: () => writeCollabBinding,
|
|
71
|
+
writeTempUnifiedDiffBackup: () => writeTempUnifiedDiffBackup
|
|
72
|
+
});
|
|
73
|
+
module.exports = __toCommonJS(index_exports);
|
|
74
|
+
|
|
75
|
+
// src/errors/cliError.ts
|
|
76
|
+
var CliError = class extends Error {
|
|
77
|
+
exitCode;
|
|
78
|
+
hint;
|
|
79
|
+
constructor(message, opts) {
|
|
80
|
+
super(message);
|
|
81
|
+
this.name = "CliError";
|
|
82
|
+
this.exitCode = opts?.exitCode ?? 1;
|
|
83
|
+
this.hint = opts?.hint ?? null;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// src/infrastructure/binding/collabBindingStore.ts
|
|
88
|
+
var import_promises2 = __toESM(require("fs/promises"), 1);
|
|
89
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
90
|
+
|
|
91
|
+
// src/shared/fs.ts
|
|
92
|
+
var import_promises = __toESM(require("fs/promises"), 1);
|
|
93
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
94
|
+
async function pathExists(targetPath) {
|
|
95
|
+
try {
|
|
96
|
+
await import_promises.default.access(targetPath);
|
|
97
|
+
return true;
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function findAvailableDirPath(preferredDir) {
|
|
103
|
+
if (!await pathExists(preferredDir)) return preferredDir;
|
|
104
|
+
const parent = import_node_path.default.dirname(preferredDir);
|
|
105
|
+
const base = import_node_path.default.basename(preferredDir);
|
|
106
|
+
for (let i = 2; i <= 1e3; i++) {
|
|
107
|
+
const candidate = import_node_path.default.join(parent, `${base}-${i}`);
|
|
108
|
+
if (!await pathExists(candidate)) return candidate;
|
|
109
|
+
}
|
|
110
|
+
throw new CliError("No available output directory name.", {
|
|
111
|
+
exitCode: 2,
|
|
112
|
+
hint: `Tried ${base}-2 through ${base}-1000 under ${parent}.`
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async function writeJsonAtomic(filePath, value) {
|
|
116
|
+
const dir = import_node_path.default.dirname(filePath);
|
|
117
|
+
await import_promises.default.mkdir(dir, { recursive: true });
|
|
118
|
+
const tmp = `${filePath}.tmp-${Date.now()}`;
|
|
119
|
+
await import_promises.default.writeFile(tmp, `${JSON.stringify(value, null, 2)}
|
|
120
|
+
`, "utf8");
|
|
121
|
+
await import_promises.default.rename(tmp, filePath);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/infrastructure/binding/collabBindingStore.ts
|
|
125
|
+
function getCollabBindingPath(repoRoot) {
|
|
126
|
+
return import_node_path2.default.join(repoRoot, ".comerge", "config.json");
|
|
127
|
+
}
|
|
128
|
+
async function readCollabBinding(repoRoot) {
|
|
129
|
+
try {
|
|
130
|
+
const raw = await import_promises2.default.readFile(getCollabBindingPath(repoRoot), "utf8");
|
|
131
|
+
const parsed = JSON.parse(raw);
|
|
132
|
+
if (parsed?.schemaVersion !== 1) return null;
|
|
133
|
+
if (!parsed.projectId || !parsed.currentAppId || !parsed.upstreamAppId) return null;
|
|
134
|
+
return {
|
|
135
|
+
schemaVersion: 1,
|
|
136
|
+
projectId: parsed.projectId,
|
|
137
|
+
currentAppId: parsed.currentAppId,
|
|
138
|
+
upstreamAppId: parsed.upstreamAppId,
|
|
139
|
+
threadId: parsed.threadId ?? null,
|
|
140
|
+
repoFingerprint: parsed.repoFingerprint ?? null,
|
|
141
|
+
remoteUrl: parsed.remoteUrl ?? null,
|
|
142
|
+
defaultBranch: parsed.defaultBranch ?? null
|
|
143
|
+
};
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function writeCollabBinding(repoRoot, binding) {
|
|
149
|
+
const filePath = getCollabBindingPath(repoRoot);
|
|
150
|
+
await writeJsonAtomic(filePath, {
|
|
151
|
+
schemaVersion: 1,
|
|
152
|
+
...binding
|
|
153
|
+
});
|
|
154
|
+
return filePath;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/infrastructure/repo/gitRepo.ts
|
|
158
|
+
var import_promises3 = __toESM(require("fs/promises"), 1);
|
|
159
|
+
var import_node_crypto = require("crypto");
|
|
160
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
161
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
162
|
+
var import_execa = require("execa");
|
|
163
|
+
var GIT_REMOTE_PROTOCOL_RE = /^(https?|ssh):\/\//i;
|
|
164
|
+
var SCP_LIKE_GIT_REMOTE_RE = /^(?<user>[^@\s]+)@(?<host>[^:\s]+):(?<path>[^\\\s]+)$/;
|
|
165
|
+
var CANONICAL_GIT_REMOTE_RE = /^(?<host>(?:localhost|[a-z0-9.-]+))\/(?<path>[^\\\s]+)$/i;
|
|
166
|
+
async function runGit(args, cwd) {
|
|
167
|
+
const res = await (0, import_execa.execa)("git", args, { cwd, stderr: "ignore" });
|
|
168
|
+
return String(res.stdout || "").trim();
|
|
169
|
+
}
|
|
170
|
+
async function runGitRaw(args, cwd) {
|
|
171
|
+
const res = await (0, import_execa.execa)("git", args, { cwd, stderr: "ignore", stripFinalNewline: false });
|
|
172
|
+
return String(res.stdout || "");
|
|
173
|
+
}
|
|
174
|
+
async function runGitDetailed(args, cwd) {
|
|
175
|
+
const res = await (0, import_execa.execa)("git", args, { cwd, reject: false });
|
|
176
|
+
return {
|
|
177
|
+
exitCode: res.exitCode ?? 1,
|
|
178
|
+
stdout: String(res.stdout || ""),
|
|
179
|
+
stderr: String(res.stderr || "")
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function cleanRepoPath(value) {
|
|
183
|
+
return value.trim().replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
184
|
+
}
|
|
185
|
+
function normalizeGitRemote(remote) {
|
|
186
|
+
const raw = String(remote ?? "").trim();
|
|
187
|
+
if (!raw) return null;
|
|
188
|
+
const canonicalMatch = raw.match(CANONICAL_GIT_REMOTE_RE);
|
|
189
|
+
if (canonicalMatch?.groups?.host && canonicalMatch.groups.path) {
|
|
190
|
+
const repoPath = cleanRepoPath(canonicalMatch.groups.path);
|
|
191
|
+
if (!repoPath) return null;
|
|
192
|
+
return `${canonicalMatch.groups.host}/${repoPath}`.toLowerCase();
|
|
193
|
+
}
|
|
194
|
+
if (GIT_REMOTE_PROTOCOL_RE.test(raw)) {
|
|
195
|
+
try {
|
|
196
|
+
const url = new URL(raw);
|
|
197
|
+
const repoPath = cleanRepoPath(url.pathname);
|
|
198
|
+
if (!url.hostname || !repoPath) return null;
|
|
199
|
+
return `${url.hostname}/${repoPath}`.toLowerCase();
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const scpMatch = raw.match(SCP_LIKE_GIT_REMOTE_RE);
|
|
205
|
+
if (scpMatch?.groups?.host && scpMatch.groups.path) {
|
|
206
|
+
const repoPath = cleanRepoPath(scpMatch.groups.path);
|
|
207
|
+
if (!repoPath) return null;
|
|
208
|
+
return `${scpMatch.groups.host}/${repoPath}`.toLowerCase();
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
function sanitizeRefFragment(value) {
|
|
213
|
+
return value.trim().replace(/[^A-Za-z0-9._/-]+/g, "-").replace(/\/{2,}/g, "/").replace(/^\/+|\/+$/g, "").replace(/^-+|-+$/g, "").slice(0, 120);
|
|
214
|
+
}
|
|
215
|
+
async function findGitRoot(startDir) {
|
|
216
|
+
try {
|
|
217
|
+
const root = await runGit(["rev-parse", "--show-toplevel"], startDir);
|
|
218
|
+
if (!root) throw new Error("empty");
|
|
219
|
+
return root;
|
|
220
|
+
} catch {
|
|
221
|
+
throw new CliError("Not inside a git repository.", {
|
|
222
|
+
exitCode: 2,
|
|
223
|
+
hint: "Run this command from the root of the repository or one of its subdirectories."
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function getCurrentBranch(cwd) {
|
|
228
|
+
try {
|
|
229
|
+
const branch = await runGit(["branch", "--show-current"], cwd);
|
|
230
|
+
return branch || null;
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function getRemoteOriginUrl(cwd) {
|
|
236
|
+
try {
|
|
237
|
+
const url = await runGit(["config", "--get", "remote.origin.url"], cwd);
|
|
238
|
+
return url || null;
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function getDefaultBranch(cwd) {
|
|
244
|
+
try {
|
|
245
|
+
const ref = await runGit(["symbolic-ref", "refs/remotes/origin/HEAD"], cwd);
|
|
246
|
+
if (!ref) return null;
|
|
247
|
+
const suffix = ref.replace(/^refs\/remotes\/origin\//, "").trim();
|
|
248
|
+
return suffix || null;
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function listUntrackedFiles(cwd) {
|
|
254
|
+
try {
|
|
255
|
+
const out = await runGit(["ls-files", "--others", "--exclude-standard"], cwd);
|
|
256
|
+
return out.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
257
|
+
} catch {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function getWorkingTreeDiff(cwd) {
|
|
262
|
+
const untracked = await listUntrackedFiles(cwd);
|
|
263
|
+
if (untracked.length > 0) {
|
|
264
|
+
throw new CliError("Untracked files are not included in git diff mode.", {
|
|
265
|
+
exitCode: 2,
|
|
266
|
+
hint: "Provide `--diff-file`/`--diff-stdin`, or add the files to git before running `comerge collab add`."
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
return await runGitRaw(["diff", "--binary", "--no-ext-diff", "HEAD"], cwd);
|
|
271
|
+
} catch {
|
|
272
|
+
throw new CliError("Failed to generate git diff.", { exitCode: 1 });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async function writeTempUnifiedDiffBackup(diff, prefix = "comerge-add-backup") {
|
|
276
|
+
const safePrefix = prefix.replace(/[^a-zA-Z0-9._-]+/g, "-") || "comerge-add-backup";
|
|
277
|
+
const tmpDir = await import_promises3.default.mkdtemp(import_node_path3.default.join(import_node_os.default.tmpdir(), `${safePrefix}-`));
|
|
278
|
+
const backupPath = import_node_path3.default.join(tmpDir, "submitted.diff");
|
|
279
|
+
await import_promises3.default.writeFile(backupPath, diff, "utf8");
|
|
280
|
+
return { backupPath };
|
|
281
|
+
}
|
|
282
|
+
async function getHeadCommitHash(cwd) {
|
|
283
|
+
try {
|
|
284
|
+
const hash = await runGit(["rev-parse", "HEAD"], cwd);
|
|
285
|
+
return hash || null;
|
|
286
|
+
} catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function createGitBundle(cwd, bundleName = "repository.bundle") {
|
|
291
|
+
const headCommitHash = await getHeadCommitHash(cwd);
|
|
292
|
+
if (!headCommitHash) {
|
|
293
|
+
throw new CliError("Failed to resolve local HEAD commit.", { exitCode: 1 });
|
|
294
|
+
}
|
|
295
|
+
const safeName = bundleName.replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
296
|
+
const tmpDir = await import_promises3.default.mkdtemp(import_node_path3.default.join(import_node_os.default.tmpdir(), "comerge-bundle-"));
|
|
297
|
+
const bundlePath = import_node_path3.default.join(tmpDir, safeName);
|
|
298
|
+
const res = await runGitDetailed(["bundle", "create", bundlePath, "--all", "--tags"], cwd);
|
|
299
|
+
if (res.exitCode !== 0) {
|
|
300
|
+
const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
|
|
301
|
+
throw new CliError("Failed to create repository bundle.", {
|
|
302
|
+
exitCode: 1,
|
|
303
|
+
hint: detail || "Git could not create the bundle artifact."
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return { bundlePath, headCommitHash };
|
|
307
|
+
}
|
|
308
|
+
async function getWorktreeStatus(cwd) {
|
|
309
|
+
try {
|
|
310
|
+
const out = await runGit(["status", "--porcelain"], cwd);
|
|
311
|
+
const entries = out.split("\n").map((line) => line.trimEnd()).filter(Boolean);
|
|
312
|
+
return { isClean: entries.length === 0, entries };
|
|
313
|
+
} catch {
|
|
314
|
+
return { isClean: false, entries: [] };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function ensureCleanWorktree(cwd, operation = "`comerge collab sync`") {
|
|
318
|
+
const status = await getWorktreeStatus(cwd);
|
|
319
|
+
if (status.isClean) return;
|
|
320
|
+
const preview = status.entries.slice(0, 10).join("\n");
|
|
321
|
+
const suffix = status.entries.length > 10 ? `
|
|
322
|
+
...and ${status.entries.length - 10} more` : "";
|
|
323
|
+
throw new CliError(`Working tree must be clean before running ${operation}.`, {
|
|
324
|
+
exitCode: 2,
|
|
325
|
+
hint: `Commit, stash, or discard local changes first.
|
|
326
|
+
|
|
327
|
+
${preview}${suffix}`
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
async function discardTrackedChanges(cwd, operation = "`comerge collab add`") {
|
|
331
|
+
const res = await runGitDetailed(["reset", "--hard", "HEAD"], cwd);
|
|
332
|
+
if (res.exitCode !== 0) {
|
|
333
|
+
const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
|
|
334
|
+
throw new CliError(`Failed to discard local tracked changes while running ${operation}.`, {
|
|
335
|
+
exitCode: 1,
|
|
336
|
+
hint: detail || "Git could not reset tracked changes back to HEAD."
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
const hash = await getHeadCommitHash(cwd);
|
|
340
|
+
if (!hash) {
|
|
341
|
+
throw new CliError("Failed to resolve local HEAD after discarding tracked changes.", { exitCode: 1 });
|
|
342
|
+
}
|
|
343
|
+
return hash;
|
|
344
|
+
}
|
|
345
|
+
async function requireCurrentBranch(cwd) {
|
|
346
|
+
const branch = await getCurrentBranch(cwd);
|
|
347
|
+
if (!branch) {
|
|
348
|
+
throw new CliError("`comerge collab sync` requires a checked out local branch.", {
|
|
349
|
+
exitCode: 2,
|
|
350
|
+
hint: "Checkout a branch before syncing."
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return branch;
|
|
354
|
+
}
|
|
355
|
+
async function importGitBundle(cwd, bundlePath, bundleRef) {
|
|
356
|
+
const verifyRes = await runGitDetailed(["bundle", "verify", bundlePath], cwd);
|
|
357
|
+
if (verifyRes.exitCode !== 0) {
|
|
358
|
+
const detail = [verifyRes.stderr.trim(), verifyRes.stdout.trim()].filter(Boolean).join("\n\n");
|
|
359
|
+
throw new CliError("Failed to verify sync bundle.", {
|
|
360
|
+
exitCode: 1,
|
|
361
|
+
hint: detail || "Git bundle verification failed."
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
const fetchRes = await runGitDetailed(["fetch", "--quiet", bundlePath, bundleRef], cwd);
|
|
365
|
+
if (fetchRes.exitCode !== 0) {
|
|
366
|
+
const detail = [fetchRes.stderr.trim(), fetchRes.stdout.trim()].filter(Boolean).join("\n\n");
|
|
367
|
+
throw new CliError("Failed to import sync bundle.", {
|
|
368
|
+
exitCode: 1,
|
|
369
|
+
hint: detail || "Git could not fetch objects from the sync bundle."
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async function cloneGitBundleToDirectory(bundlePath, targetDir) {
|
|
374
|
+
const parentDir = import_node_path3.default.dirname(targetDir);
|
|
375
|
+
const cloneRes = await runGitDetailed(["clone", bundlePath, targetDir], parentDir);
|
|
376
|
+
if (cloneRes.exitCode !== 0) {
|
|
377
|
+
const detail = [cloneRes.stderr.trim(), cloneRes.stdout.trim()].filter(Boolean).join("\n\n");
|
|
378
|
+
throw new CliError("Failed to create local remix checkout.", {
|
|
379
|
+
exitCode: 1,
|
|
380
|
+
hint: detail || "Git could not clone the remix repository bundle."
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
const remoteRemoveRes = await runGitDetailed(["remote", "remove", "origin"], targetDir);
|
|
384
|
+
if (remoteRemoveRes.exitCode !== 0) {
|
|
385
|
+
const detail = [remoteRemoveRes.stderr.trim(), remoteRemoveRes.stdout.trim()].filter(Boolean).join("\n\n");
|
|
386
|
+
throw new CliError("Failed to finalize local remix checkout.", {
|
|
387
|
+
exitCode: 1,
|
|
388
|
+
hint: detail || "Git could not remove the temporary bundle origin."
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async function ensureGitInfoExcludeEntries(cwd, entries) {
|
|
393
|
+
const excludePath = import_node_path3.default.join(cwd, ".git", "info", "exclude");
|
|
394
|
+
await import_promises3.default.mkdir(import_node_path3.default.dirname(excludePath), { recursive: true });
|
|
395
|
+
let current = "";
|
|
396
|
+
try {
|
|
397
|
+
current = await import_promises3.default.readFile(excludePath, "utf8");
|
|
398
|
+
} catch {
|
|
399
|
+
}
|
|
400
|
+
const lines = new Set(current.split("\n").map((line) => line.trim()).filter(Boolean));
|
|
401
|
+
let changed = false;
|
|
402
|
+
for (const entry of entries) {
|
|
403
|
+
const normalized = entry.trim();
|
|
404
|
+
if (!normalized || lines.has(normalized)) continue;
|
|
405
|
+
lines.add(normalized);
|
|
406
|
+
changed = true;
|
|
407
|
+
}
|
|
408
|
+
if (!changed) return;
|
|
409
|
+
await import_promises3.default.writeFile(excludePath, `${Array.from(lines).join("\n")}
|
|
410
|
+
`, "utf8");
|
|
411
|
+
}
|
|
412
|
+
async function ensureCommitExists(cwd, commitHash) {
|
|
413
|
+
const res = await runGitDetailed(["cat-file", "-e", `${commitHash}^{commit}`], cwd);
|
|
414
|
+
if (res.exitCode === 0) return;
|
|
415
|
+
throw new CliError("Expected target commit is missing after bundle import.", {
|
|
416
|
+
exitCode: 1,
|
|
417
|
+
hint: `Commit ${commitHash} is not available in the local repository.`
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
async function fastForwardToCommit(cwd, commitHash) {
|
|
421
|
+
const res = await runGitDetailed(["merge", "--ff-only", commitHash], cwd);
|
|
422
|
+
if (res.exitCode !== 0) {
|
|
423
|
+
const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
|
|
424
|
+
throw new CliError("Failed to fast-forward local branch.", {
|
|
425
|
+
exitCode: 1,
|
|
426
|
+
hint: detail || "Git could not fast-forward to the target commit."
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
const hash = await getHeadCommitHash(cwd);
|
|
430
|
+
if (!hash) throw new CliError("Failed to resolve local HEAD after fast-forward sync.", { exitCode: 1 });
|
|
431
|
+
return hash;
|
|
432
|
+
}
|
|
433
|
+
async function createBackupBranch(cwd, params) {
|
|
434
|
+
const sourceCommitHash = params?.sourceCommitHash?.trim() || await getHeadCommitHash(cwd);
|
|
435
|
+
if (!sourceCommitHash) {
|
|
436
|
+
throw new CliError("Failed to resolve local HEAD before creating reconcile backup.", { exitCode: 1 });
|
|
437
|
+
}
|
|
438
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
439
|
+
const branchFragment = sanitizeRefFragment(params?.branchName?.trim() || "current-branch");
|
|
440
|
+
const prefix = sanitizeRefFragment(params?.prefix?.trim() || "comerge/reconcile-backup");
|
|
441
|
+
const backupBranchName = `${prefix}/${branchFragment}-${timestamp}`;
|
|
442
|
+
const createRes = await runGitDetailed(["branch", backupBranchName, sourceCommitHash], cwd);
|
|
443
|
+
if (createRes.exitCode !== 0) {
|
|
444
|
+
const detail = [createRes.stderr.trim(), createRes.stdout.trim()].filter(Boolean).join("\n\n");
|
|
445
|
+
throw new CliError("Failed to create reconcile backup branch.", {
|
|
446
|
+
exitCode: 1,
|
|
447
|
+
hint: detail || "Git could not create the safety backup branch."
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
return { branchName: backupBranchName, commitHash: sourceCommitHash };
|
|
451
|
+
}
|
|
452
|
+
async function hardResetToCommit(cwd, commitHash, operation = "`comerge collab reconcile`") {
|
|
453
|
+
const res = await runGitDetailed(["reset", "--hard", commitHash], cwd);
|
|
454
|
+
if (res.exitCode !== 0) {
|
|
455
|
+
const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
|
|
456
|
+
throw new CliError(`Failed to move local branch while running ${operation}.`, {
|
|
457
|
+
exitCode: 1,
|
|
458
|
+
hint: detail || `Git could not reset the current branch to ${commitHash}.`
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
const hash = await getHeadCommitHash(cwd);
|
|
462
|
+
if (!hash) {
|
|
463
|
+
throw new CliError("Failed to resolve local HEAD after resetting branch.", { exitCode: 1 });
|
|
464
|
+
}
|
|
465
|
+
return hash;
|
|
466
|
+
}
|
|
467
|
+
async function buildRepoFingerprint(params) {
|
|
468
|
+
const remote = normalizeGitRemote(params.remoteUrl);
|
|
469
|
+
const defaultBranch = params.defaultBranch?.trim().toLowerCase() || "";
|
|
470
|
+
const payload = remote ? { remote, defaultBranch } : { local: import_node_path3.default.resolve(params.gitRoot).toLowerCase(), defaultBranch };
|
|
471
|
+
return (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
472
|
+
}
|
|
473
|
+
function summarizeUnifiedDiff(diff) {
|
|
474
|
+
const lines = diff.split("\n");
|
|
475
|
+
let changedFilesCount = 0;
|
|
476
|
+
let insertions = 0;
|
|
477
|
+
let deletions = 0;
|
|
478
|
+
for (const line of lines) {
|
|
479
|
+
if (line.startsWith("diff --git ")) changedFilesCount += 1;
|
|
480
|
+
if (line.startsWith("+") && !line.startsWith("+++")) insertions += 1;
|
|
481
|
+
if (line.startsWith("-") && !line.startsWith("---")) deletions += 1;
|
|
482
|
+
}
|
|
483
|
+
return { changedFilesCount, insertions, deletions };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/application/collab/collabInit.ts
|
|
487
|
+
var import_promises4 = __toESM(require("fs/promises"), 1);
|
|
488
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
489
|
+
|
|
490
|
+
// src/application/collab/shared.ts
|
|
491
|
+
var import_node_crypto2 = require("crypto");
|
|
492
|
+
function unwrapResponseObject(resp, label) {
|
|
493
|
+
const obj = resp?.responseObject;
|
|
494
|
+
if (obj === void 0 || obj === null) {
|
|
495
|
+
const message = typeof resp?.message === "string" && resp.message.trim().length > 0 ? resp.message : `Missing ${label} response`;
|
|
496
|
+
throw new CliError(message, { exitCode: 1, hint: resp ? JSON.stringify(resp, null, 2) : null });
|
|
497
|
+
}
|
|
498
|
+
return obj;
|
|
499
|
+
}
|
|
500
|
+
function sleep(ms) {
|
|
501
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
502
|
+
}
|
|
503
|
+
function buildDeterministicIdempotencyKey(parts) {
|
|
504
|
+
return (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(parts)).digest("hex");
|
|
505
|
+
}
|
|
506
|
+
function formatCliErrorDetail(err) {
|
|
507
|
+
if (err instanceof CliError) {
|
|
508
|
+
return [err.message, err.hint].filter(Boolean).join("\n\n") || null;
|
|
509
|
+
}
|
|
510
|
+
if (err instanceof Error) {
|
|
511
|
+
return err.message || null;
|
|
512
|
+
}
|
|
513
|
+
return typeof err === "string" && err.trim() ? err.trim() : null;
|
|
514
|
+
}
|
|
515
|
+
function sanitizeCheckoutDirName(value) {
|
|
516
|
+
const sanitized = value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
517
|
+
return sanitized || "comerge-remix";
|
|
518
|
+
}
|
|
519
|
+
async function pollAppReady(api, appId) {
|
|
520
|
+
const started = Date.now();
|
|
521
|
+
let delay = 2e3;
|
|
522
|
+
while (Date.now() - started < 20 * 60 * 1e3) {
|
|
523
|
+
const appResp = await api.getApp(appId);
|
|
524
|
+
const app = unwrapResponseObject(appResp, "app");
|
|
525
|
+
const status = typeof app.status === "string" ? app.status : "";
|
|
526
|
+
if (status === "ready") return app;
|
|
527
|
+
if (status === "error") {
|
|
528
|
+
throw new CliError("App is in error state.", {
|
|
529
|
+
exitCode: 1,
|
|
530
|
+
hint: typeof app.statusError === "string" ? app.statusError : null
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
await sleep(delay);
|
|
534
|
+
delay = Math.min(1e4, Math.floor(delay * 1.4));
|
|
535
|
+
}
|
|
536
|
+
throw new CliError("Timed out waiting for app to become ready.", { exitCode: 1 });
|
|
537
|
+
}
|
|
538
|
+
async function pollChangeStep(api, appId, changeStepId) {
|
|
539
|
+
const started = Date.now();
|
|
540
|
+
let delay = 1500;
|
|
541
|
+
while (Date.now() - started < 20 * 60 * 1e3) {
|
|
542
|
+
const resp = await api.getChangeStep(appId, changeStepId);
|
|
543
|
+
const step = unwrapResponseObject(resp, "change step");
|
|
544
|
+
const status = typeof step.status === "string" ? step.status : "";
|
|
545
|
+
if (status === "succeeded") return step;
|
|
546
|
+
if (status === "failed") {
|
|
547
|
+
throw new CliError("Change step failed.", {
|
|
548
|
+
exitCode: 1,
|
|
549
|
+
hint: typeof step.statusError === "string" ? step.statusError : null
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
await sleep(delay);
|
|
553
|
+
delay = Math.min(1e4, Math.floor(delay * 1.4));
|
|
554
|
+
}
|
|
555
|
+
throw new CliError("Timed out waiting for change step.", { exitCode: 1 });
|
|
556
|
+
}
|
|
557
|
+
async function pollReconcile(api, appId, reconcileId) {
|
|
558
|
+
const started = Date.now();
|
|
559
|
+
let delay = 1500;
|
|
560
|
+
while (Date.now() - started < 30 * 60 * 1e3) {
|
|
561
|
+
const resp = await api.getAppReconcile(appId, reconcileId);
|
|
562
|
+
const reconcile = unwrapResponseObject(resp, "reconcile");
|
|
563
|
+
if (reconcile.status === "succeeded") return reconcile;
|
|
564
|
+
if (reconcile.status === "manual_reconcile_required") {
|
|
565
|
+
throw new CliError("Reconciliation requires manual intervention.", {
|
|
566
|
+
exitCode: 2,
|
|
567
|
+
hint: reconcile.statusError || "The server could not safely replay the local-only commits."
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
if (reconcile.status === "failed" || reconcile.status === "cancelled") {
|
|
571
|
+
throw new CliError("Reconciliation failed.", {
|
|
572
|
+
exitCode: 1,
|
|
573
|
+
hint: reconcile.statusError || null
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
await sleep(delay);
|
|
577
|
+
delay = Math.min(1e4, Math.floor(delay * 1.4));
|
|
578
|
+
}
|
|
579
|
+
throw new CliError("Timed out waiting for reconcile job.", { exitCode: 1 });
|
|
580
|
+
}
|
|
581
|
+
async function pollUpstreamSyncCompletion(api, appId, params) {
|
|
582
|
+
const started = Date.now();
|
|
583
|
+
let delay = 1500;
|
|
584
|
+
let sawNonReady = params.initialStatus !== "ready";
|
|
585
|
+
while (Date.now() - started < 30 * 60 * 1e3) {
|
|
586
|
+
const appResp = await api.getApp(appId);
|
|
587
|
+
const app = unwrapResponseObject(appResp, "app");
|
|
588
|
+
const status = typeof app.status === "string" ? app.status : "";
|
|
589
|
+
const headCommitId = typeof app.headCommitId === "string" ? app.headCommitId : null;
|
|
590
|
+
if (status === "error") {
|
|
591
|
+
throw new CliError("App upstream sync failed.", {
|
|
592
|
+
exitCode: 1,
|
|
593
|
+
hint: typeof app.statusError === "string" ? app.statusError : null
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
if (status !== "ready") sawNonReady = true;
|
|
597
|
+
if (status === "ready" && headCommitId && headCommitId !== params.initialHeadCommitId) {
|
|
598
|
+
return app;
|
|
599
|
+
}
|
|
600
|
+
if (status === "ready" && sawNonReady) {
|
|
601
|
+
return app;
|
|
602
|
+
}
|
|
603
|
+
await sleep(delay);
|
|
604
|
+
delay = Math.min(1e4, Math.floor(delay * 1.4));
|
|
605
|
+
}
|
|
606
|
+
throw new CliError("Timed out waiting for upstream sync.", { exitCode: 1 });
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/shared/hash.ts
|
|
610
|
+
var import_node_crypto3 = __toESM(require("crypto"), 1);
|
|
611
|
+
var import_node_fs = __toESM(require("fs"), 1);
|
|
612
|
+
async function sha256FileHex(filePath) {
|
|
613
|
+
const hash = import_node_crypto3.default.createHash("sha256");
|
|
614
|
+
await new Promise((resolve, reject) => {
|
|
615
|
+
const stream = import_node_fs.default.createReadStream(filePath);
|
|
616
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
617
|
+
stream.on("error", reject);
|
|
618
|
+
stream.on("end", () => resolve());
|
|
619
|
+
});
|
|
620
|
+
return hash.digest("hex");
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/shared/upload.ts
|
|
624
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
625
|
+
var import_node_stream = require("stream");
|
|
626
|
+
async function uploadPresigned(params) {
|
|
627
|
+
const stats = await import_node_fs2.default.promises.stat(params.filePath).catch(() => null);
|
|
628
|
+
if (!stats || !stats.isFile()) {
|
|
629
|
+
throw new CliError("Upload file not found.", { exitCode: 2 });
|
|
630
|
+
}
|
|
631
|
+
const totalBytes = stats.size;
|
|
632
|
+
const fileStream = import_node_fs2.default.createReadStream(params.filePath);
|
|
633
|
+
const pass = new import_node_stream.PassThrough();
|
|
634
|
+
let sentBytes = 0;
|
|
635
|
+
fileStream.on("data", (chunk) => {
|
|
636
|
+
sentBytes += chunk.length;
|
|
637
|
+
params.onProgress?.({ sentBytes, totalBytes });
|
|
638
|
+
});
|
|
639
|
+
fileStream.on("error", (err) => pass.destroy(err));
|
|
640
|
+
fileStream.pipe(pass);
|
|
641
|
+
const response = await fetch(params.uploadUrl, {
|
|
642
|
+
method: "PUT",
|
|
643
|
+
headers: params.headers,
|
|
644
|
+
body: pass,
|
|
645
|
+
duplex: "half"
|
|
646
|
+
});
|
|
647
|
+
if (!response.ok) {
|
|
648
|
+
const text = await response.text().catch(() => "");
|
|
649
|
+
throw new CliError("Upload failed.", {
|
|
650
|
+
exitCode: 1,
|
|
651
|
+
hint: `Status: ${response.status}
|
|
652
|
+
${text}`.trim() || null
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/application/collab/collabInit.ts
|
|
658
|
+
async function collabInit(params) {
|
|
659
|
+
const repoRoot = await findGitRoot(params.cwd);
|
|
660
|
+
await ensureCleanWorktree(repoRoot, "`comerge collab init`");
|
|
661
|
+
if (params.path?.trim()) {
|
|
662
|
+
throw new CliError("`comerge collab init --path` is not supported.", {
|
|
663
|
+
exitCode: 2,
|
|
664
|
+
hint: "History-preserving init imports the full git repository. Run the command from the repository root without --path."
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
const remoteUrl = normalizeGitRemote(await getRemoteOriginUrl(repoRoot));
|
|
668
|
+
const currentBranch = await getCurrentBranch(repoRoot);
|
|
669
|
+
const defaultBranch = await getDefaultBranch(repoRoot) ?? currentBranch;
|
|
670
|
+
const repoFingerprint = await buildRepoFingerprint({ gitRoot: repoRoot, remoteUrl, defaultBranch });
|
|
671
|
+
if (!params.forceNew) {
|
|
672
|
+
const bindingResp = await params.api.resolveProjectBinding({
|
|
673
|
+
repoFingerprint,
|
|
674
|
+
remoteUrl: remoteUrl ?? void 0
|
|
675
|
+
});
|
|
676
|
+
const existing = bindingResp?.responseObject;
|
|
677
|
+
if (existing?.projectId && existing?.appId) {
|
|
678
|
+
const bindingPath2 = await writeCollabBinding(repoRoot, {
|
|
679
|
+
projectId: String(existing.projectId),
|
|
680
|
+
currentAppId: String(existing.appId),
|
|
681
|
+
upstreamAppId: String(existing.upstreamAppId ?? existing.appId),
|
|
682
|
+
threadId: existing.threadId ? String(existing.threadId) : null,
|
|
683
|
+
repoFingerprint,
|
|
684
|
+
remoteUrl,
|
|
685
|
+
defaultBranch: defaultBranch ?? null
|
|
686
|
+
});
|
|
687
|
+
return {
|
|
688
|
+
reused: true,
|
|
689
|
+
projectId: String(existing.projectId),
|
|
690
|
+
appId: String(existing.appId),
|
|
691
|
+
upstreamAppId: String(existing.upstreamAppId ?? existing.appId),
|
|
692
|
+
bindingPath: bindingPath2,
|
|
693
|
+
repoRoot
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const { bundlePath, headCommitHash } = await createGitBundle(repoRoot, "repository.bundle");
|
|
698
|
+
const bundleSha = await sha256FileHex(bundlePath);
|
|
699
|
+
const bundleSize = (await import_promises4.default.stat(bundlePath)).size;
|
|
700
|
+
const presignResp = await params.api.presignImportUploadFirstParty({
|
|
701
|
+
file: {
|
|
702
|
+
name: "repository.bundle",
|
|
703
|
+
mimeType: "application/x-git-bundle",
|
|
704
|
+
size: bundleSize,
|
|
705
|
+
checksumSha256: bundleSha
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
const presign = unwrapResponseObject(presignResp, "upload");
|
|
709
|
+
await uploadPresigned({
|
|
710
|
+
uploadUrl: String(presign.uploadUrl),
|
|
711
|
+
headers: presign.headers ?? {},
|
|
712
|
+
filePath: bundlePath
|
|
713
|
+
});
|
|
714
|
+
const importResp = await params.api.importFromUploadFirstParty({
|
|
715
|
+
uploadId: String(presign.uploadId),
|
|
716
|
+
appName: params.appName?.trim() || import_node_path4.default.basename(repoRoot),
|
|
717
|
+
path: params.path?.trim() || void 0,
|
|
718
|
+
platform: "generic",
|
|
719
|
+
isPublic: false,
|
|
720
|
+
remoteUrl: remoteUrl ?? void 0,
|
|
721
|
+
defaultBranch: defaultBranch ?? void 0,
|
|
722
|
+
repoFingerprint,
|
|
723
|
+
headCommitHash
|
|
724
|
+
});
|
|
725
|
+
const imported = unwrapResponseObject(importResp, "import");
|
|
726
|
+
const app = await pollAppReady(params.api, String(imported.appId));
|
|
727
|
+
const bindingPath = await writeCollabBinding(repoRoot, {
|
|
728
|
+
projectId: String(app.projectId),
|
|
729
|
+
currentAppId: String(app.id),
|
|
730
|
+
upstreamAppId: String(app.id),
|
|
731
|
+
threadId: app.threadId ? String(app.threadId) : null,
|
|
732
|
+
repoFingerprint,
|
|
733
|
+
remoteUrl,
|
|
734
|
+
defaultBranch: defaultBranch ?? null
|
|
735
|
+
});
|
|
736
|
+
return {
|
|
737
|
+
reused: false,
|
|
738
|
+
projectId: String(app.projectId),
|
|
739
|
+
appId: String(app.id),
|
|
740
|
+
upstreamAppId: String(app.id),
|
|
741
|
+
bindingPath,
|
|
742
|
+
repoRoot,
|
|
743
|
+
remoteUrl,
|
|
744
|
+
defaultBranch
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// src/application/collab/collabList.ts
|
|
749
|
+
async function collabList(params) {
|
|
750
|
+
const resp = await params.api.listApps({ forked: "all" });
|
|
751
|
+
const apps = unwrapResponseObject(resp, "apps");
|
|
752
|
+
return { apps };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/application/collab/collabRemix.ts
|
|
756
|
+
var import_promises5 = __toESM(require("fs/promises"), 1);
|
|
757
|
+
var import_node_os2 = __toESM(require("os"), 1);
|
|
758
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
759
|
+
async function collabRemix(params) {
|
|
760
|
+
const sourceAppId = params.appId?.trim() || null;
|
|
761
|
+
if (!sourceAppId) {
|
|
762
|
+
throw new CliError("No source app selected.", {
|
|
763
|
+
exitCode: 2,
|
|
764
|
+
hint: "Pass the source app id to remix."
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
const parentDir = import_node_path5.default.resolve(params.cwd);
|
|
768
|
+
const parentStats = await import_promises5.default.stat(parentDir).catch(() => null);
|
|
769
|
+
if (!parentStats?.isDirectory()) {
|
|
770
|
+
throw new CliError("Remix output parent directory does not exist.", {
|
|
771
|
+
exitCode: 2,
|
|
772
|
+
hint: `Create the directory first: ${parentDir}`
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
const forkResp = await params.api.forkApp(sourceAppId, { name: params.name?.trim() || void 0, platform: "generic" });
|
|
776
|
+
const forked = unwrapResponseObject(forkResp, "fork");
|
|
777
|
+
const app = await pollAppReady(params.api, String(forked.id));
|
|
778
|
+
const baseDirName = sanitizeCheckoutDirName(String(params.name?.trim() || app.name || app.id));
|
|
779
|
+
const repoRoot = await findAvailableDirPath(import_node_path5.default.join(parentDir, baseDirName));
|
|
780
|
+
const bundleTempDir = await import_promises5.default.mkdtemp(import_node_path5.default.join(import_node_os2.default.tmpdir(), "comerge-remix-"));
|
|
781
|
+
const bundlePath = import_node_path5.default.join(bundleTempDir, "repository.bundle");
|
|
782
|
+
try {
|
|
783
|
+
const bundle = await params.api.downloadAppBundle(String(app.id));
|
|
784
|
+
await import_promises5.default.writeFile(bundlePath, bundle.data);
|
|
785
|
+
await cloneGitBundleToDirectory(bundlePath, repoRoot);
|
|
786
|
+
await ensureGitInfoExcludeEntries(repoRoot, [".comerge/"]);
|
|
787
|
+
} catch (err) {
|
|
788
|
+
await import_promises5.default.rm(repoRoot, { recursive: true, force: true }).catch(() => {
|
|
789
|
+
});
|
|
790
|
+
throw err;
|
|
791
|
+
} finally {
|
|
792
|
+
await import_promises5.default.rm(bundleTempDir, { recursive: true, force: true });
|
|
793
|
+
}
|
|
794
|
+
const remoteUrl = normalizeGitRemote(await getRemoteOriginUrl(repoRoot));
|
|
795
|
+
const defaultBranch = await getDefaultBranch(repoRoot) ?? await getCurrentBranch(repoRoot) ?? null;
|
|
796
|
+
const repoFingerprint = remoteUrl ? await buildRepoFingerprint({ gitRoot: repoRoot, remoteUrl, defaultBranch }) : null;
|
|
797
|
+
const bindingPath = await writeCollabBinding(repoRoot, {
|
|
798
|
+
projectId: String(app.projectId),
|
|
799
|
+
currentAppId: String(app.id),
|
|
800
|
+
upstreamAppId: String(app.forkedFromAppId ?? sourceAppId),
|
|
801
|
+
threadId: app.threadId ? String(app.threadId) : null,
|
|
802
|
+
repoFingerprint,
|
|
803
|
+
remoteUrl,
|
|
804
|
+
defaultBranch
|
|
805
|
+
});
|
|
806
|
+
return {
|
|
807
|
+
appId: String(app.id),
|
|
808
|
+
projectId: String(app.projectId),
|
|
809
|
+
upstreamAppId: String(app.forkedFromAppId ?? sourceAppId),
|
|
810
|
+
bindingPath,
|
|
811
|
+
repoRoot
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// src/application/collab/collabAdd.ts
|
|
816
|
+
var import_promises7 = __toESM(require("fs/promises"), 1);
|
|
817
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
818
|
+
|
|
819
|
+
// src/application/collab/collabSync.ts
|
|
820
|
+
var import_promises6 = __toESM(require("fs/promises"), 1);
|
|
821
|
+
var import_node_os3 = __toESM(require("os"), 1);
|
|
822
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
823
|
+
async function collabSync(params) {
|
|
824
|
+
const repoRoot = await findGitRoot(params.cwd);
|
|
825
|
+
const binding = await readCollabBinding(repoRoot);
|
|
826
|
+
if (!binding) {
|
|
827
|
+
throw new CliError("Repository is not bound to Comerge.", {
|
|
828
|
+
exitCode: 2,
|
|
829
|
+
hint: "Run `comerge collab init` first."
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
await ensureCleanWorktree(repoRoot);
|
|
833
|
+
const branch = await requireCurrentBranch(repoRoot);
|
|
834
|
+
const headCommitHash = await getHeadCommitHash(repoRoot);
|
|
835
|
+
if (!headCommitHash) {
|
|
836
|
+
throw new CliError("Failed to resolve local HEAD commit.", { exitCode: 1 });
|
|
837
|
+
}
|
|
838
|
+
const resp = await params.api.syncLocalApp(binding.currentAppId, {
|
|
839
|
+
baseCommitHash: headCommitHash,
|
|
840
|
+
repoFingerprint: binding.repoFingerprint ?? void 0,
|
|
841
|
+
remoteUrl: binding.remoteUrl ?? void 0,
|
|
842
|
+
defaultBranch: binding.defaultBranch ?? void 0,
|
|
843
|
+
dryRun: params.dryRun
|
|
844
|
+
});
|
|
845
|
+
const sync = unwrapResponseObject(resp, "sync result");
|
|
846
|
+
if (sync.status === "conflict_risk") {
|
|
847
|
+
throw new CliError("Local repository metadata conflicts with the bound Comerge app.", {
|
|
848
|
+
exitCode: 2,
|
|
849
|
+
hint: sync.warnings.join("\n") || "Run the command from the correct bound repository."
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
if (sync.status === "base_unknown") {
|
|
853
|
+
throw new CliError("Local repository cannot be fast-forward synced.", {
|
|
854
|
+
exitCode: 2,
|
|
855
|
+
hint: "Your local HEAD is not on the app sandbox history. Reconcile the repository manually before syncing."
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
if (sync.status === "up_to_date") {
|
|
859
|
+
return {
|
|
860
|
+
status: sync.status,
|
|
861
|
+
branch,
|
|
862
|
+
repoRoot,
|
|
863
|
+
baseCommitHash: sync.baseCommitHash,
|
|
864
|
+
targetCommitHash: sync.targetCommitHash,
|
|
865
|
+
targetCommitId: sync.targetCommitId,
|
|
866
|
+
stats: sync.stats,
|
|
867
|
+
localCommitHash: headCommitHash,
|
|
868
|
+
applied: false,
|
|
869
|
+
dryRun: params.dryRun
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
const previewResult = {
|
|
873
|
+
status: sync.status,
|
|
874
|
+
branch,
|
|
875
|
+
repoRoot,
|
|
876
|
+
baseCommitHash: sync.baseCommitHash,
|
|
877
|
+
targetCommitHash: sync.targetCommitHash,
|
|
878
|
+
targetCommitId: sync.targetCommitId,
|
|
879
|
+
stats: sync.stats,
|
|
880
|
+
bundleRef: sync.bundleRef,
|
|
881
|
+
bundleSizeBytes: sync.bundleSizeBytes,
|
|
882
|
+
localCommitHash: headCommitHash,
|
|
883
|
+
applied: false,
|
|
884
|
+
dryRun: params.dryRun
|
|
885
|
+
};
|
|
886
|
+
if (params.dryRun) {
|
|
887
|
+
return previewResult;
|
|
888
|
+
}
|
|
889
|
+
if (!sync.bundleBase64 || !sync.bundleRef) {
|
|
890
|
+
throw new CliError("Sync bundle payload is missing.", { exitCode: 1 });
|
|
891
|
+
}
|
|
892
|
+
const tempDir = await import_promises6.default.mkdtemp(import_node_path6.default.join(import_node_os3.default.tmpdir(), "comerge-sync-"));
|
|
893
|
+
const bundlePath = import_node_path6.default.join(tempDir, "sync-local.bundle");
|
|
894
|
+
try {
|
|
895
|
+
await import_promises6.default.writeFile(bundlePath, Buffer.from(sync.bundleBase64, "base64"));
|
|
896
|
+
await importGitBundle(repoRoot, bundlePath, sync.bundleRef);
|
|
897
|
+
await ensureCommitExists(repoRoot, sync.targetCommitHash);
|
|
898
|
+
const localCommitHash = await fastForwardToCommit(repoRoot, sync.targetCommitHash);
|
|
899
|
+
return {
|
|
900
|
+
...previewResult,
|
|
901
|
+
localCommitHash,
|
|
902
|
+
applied: true,
|
|
903
|
+
dryRun: false
|
|
904
|
+
};
|
|
905
|
+
} finally {
|
|
906
|
+
await import_promises6.default.rm(tempDir, { recursive: true, force: true });
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// src/application/collab/collabAdd.ts
|
|
911
|
+
async function collabAdd(params) {
|
|
912
|
+
const repoRoot = await findGitRoot(params.cwd);
|
|
913
|
+
const binding = await readCollabBinding(repoRoot);
|
|
914
|
+
if (!binding) {
|
|
915
|
+
throw new CliError("Repository is not bound to Comerge.", {
|
|
916
|
+
exitCode: 2,
|
|
917
|
+
hint: "Run `comerge collab init` first."
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
const prompt = params.prompt.trim();
|
|
921
|
+
if (!prompt) throw new CliError("Prompt is required.", { exitCode: 2 });
|
|
922
|
+
const diffSource = params.diffSource ?? (params.diff ? "external" : "worktree");
|
|
923
|
+
const diff = params.diff ?? await getWorkingTreeDiff(repoRoot);
|
|
924
|
+
if (!diff.trim()) {
|
|
925
|
+
throw new CliError("Diff is empty.", {
|
|
926
|
+
exitCode: 2,
|
|
927
|
+
hint: "Make changes first, or pass `--diff-file`/`--diff-stdin`."
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
const branch = await getCurrentBranch(repoRoot);
|
|
931
|
+
const headCommitHash = await getHeadCommitHash(repoRoot);
|
|
932
|
+
const stats = summarizeUnifiedDiff(diff);
|
|
933
|
+
const idempotencyKey = params.idempotencyKey?.trim() || buildDeterministicIdempotencyKey({
|
|
934
|
+
appId: binding.currentAppId,
|
|
935
|
+
upstreamAppId: binding.upstreamAppId,
|
|
936
|
+
headCommitHash,
|
|
937
|
+
prompt,
|
|
938
|
+
diff
|
|
939
|
+
});
|
|
940
|
+
const resp = await params.api.createChangeStep(binding.currentAppId, {
|
|
941
|
+
threadId: binding.threadId ?? void 0,
|
|
942
|
+
prompt,
|
|
943
|
+
diff,
|
|
944
|
+
baseCommitHash: headCommitHash,
|
|
945
|
+
changedFilesCount: stats.changedFilesCount,
|
|
946
|
+
insertions: stats.insertions,
|
|
947
|
+
deletions: stats.deletions,
|
|
948
|
+
actor: params.actor,
|
|
949
|
+
workspaceMetadata: {
|
|
950
|
+
branch,
|
|
951
|
+
repoRoot,
|
|
952
|
+
remoteUrl: binding.remoteUrl,
|
|
953
|
+
defaultBranch: binding.defaultBranch
|
|
954
|
+
},
|
|
955
|
+
idempotencyKey
|
|
956
|
+
});
|
|
957
|
+
const created = unwrapResponseObject(resp, "change step");
|
|
958
|
+
const step = await pollChangeStep(params.api, binding.currentAppId, String(created.id));
|
|
959
|
+
const autoSyncEnabled = params.sync !== false;
|
|
960
|
+
const canAutoSyncLocally = autoSyncEnabled && diffSource === "worktree";
|
|
961
|
+
if (!autoSyncEnabled || !canAutoSyncLocally) {
|
|
962
|
+
return step;
|
|
963
|
+
}
|
|
964
|
+
const { backupPath } = await writeTempUnifiedDiffBackup(diff, "comerge-add");
|
|
965
|
+
try {
|
|
966
|
+
await discardTrackedChanges(repoRoot, "`comerge collab add`");
|
|
967
|
+
await collabSync({
|
|
968
|
+
api: params.api,
|
|
969
|
+
cwd: repoRoot,
|
|
970
|
+
dryRun: false
|
|
971
|
+
});
|
|
972
|
+
await import_promises7.default.rm(import_node_path7.default.dirname(backupPath), { recursive: true, force: true }).catch(() => void 0);
|
|
973
|
+
} catch (err) {
|
|
974
|
+
const detail = formatCliErrorDetail(err);
|
|
975
|
+
const hint = [
|
|
976
|
+
detail,
|
|
977
|
+
`The submitted diff backup was preserved at: ${backupPath}`,
|
|
978
|
+
"The change step already succeeded remotely. Inspect or reapply that diff manually if needed, then run `comerge collab sync`."
|
|
979
|
+
].filter(Boolean).join("\n\n");
|
|
980
|
+
throw new CliError("Change step succeeded remotely, but automatic local sync failed.", {
|
|
981
|
+
exitCode: err instanceof CliError ? err.exitCode : 1,
|
|
982
|
+
hint
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
return step;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// src/application/collab/collabSyncUpstream.ts
|
|
989
|
+
async function collabSyncUpstream(params) {
|
|
990
|
+
const repoRoot = await findGitRoot(params.cwd);
|
|
991
|
+
const binding = await readCollabBinding(repoRoot);
|
|
992
|
+
if (!binding) {
|
|
993
|
+
throw new CliError("Repository is not bound to Comerge.", {
|
|
994
|
+
exitCode: 2,
|
|
995
|
+
hint: "Run `comerge collab init` first."
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
await ensureCleanWorktree(repoRoot, "`comerge collab sync-upstream`");
|
|
999
|
+
await requireCurrentBranch(repoRoot);
|
|
1000
|
+
if (binding.currentAppId === binding.upstreamAppId) {
|
|
1001
|
+
throw new CliError("Current repository is not bound to a remix/fork app.", {
|
|
1002
|
+
exitCode: 2,
|
|
1003
|
+
hint: "`comerge collab sync-upstream` only applies to remixes/forks that have an upstream app."
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
const currentAppResp = await params.api.getApp(binding.currentAppId);
|
|
1007
|
+
const currentApp = unwrapResponseObject(currentAppResp, "app");
|
|
1008
|
+
const initialHeadCommitId = typeof currentApp.headCommitId === "string" ? currentApp.headCommitId : null;
|
|
1009
|
+
const initialStatus = typeof currentApp.status === "string" ? currentApp.status : null;
|
|
1010
|
+
const resp = await params.api.syncUpstreamApp(binding.currentAppId);
|
|
1011
|
+
const syncUpstream = unwrapResponseObject(resp, "upstream sync");
|
|
1012
|
+
if (syncUpstream.status === "up-to-date") {
|
|
1013
|
+
return {
|
|
1014
|
+
status: syncUpstream.status,
|
|
1015
|
+
repoRoot,
|
|
1016
|
+
appId: binding.currentAppId,
|
|
1017
|
+
upstreamAppId: binding.upstreamAppId,
|
|
1018
|
+
localUpdated: false
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
const completedApp = await pollUpstreamSyncCompletion(params.api, binding.currentAppId, {
|
|
1022
|
+
initialHeadCommitId,
|
|
1023
|
+
initialStatus
|
|
1024
|
+
});
|
|
1025
|
+
const localSyncResult = await collabSync({
|
|
1026
|
+
api: params.api,
|
|
1027
|
+
cwd: repoRoot,
|
|
1028
|
+
dryRun: false
|
|
1029
|
+
});
|
|
1030
|
+
return {
|
|
1031
|
+
status: syncUpstream.status,
|
|
1032
|
+
mergeRequestId: syncUpstream.mergeRequestId ?? null,
|
|
1033
|
+
repoRoot,
|
|
1034
|
+
appId: binding.currentAppId,
|
|
1035
|
+
upstreamAppId: binding.upstreamAppId,
|
|
1036
|
+
remoteHeadCommitId: typeof completedApp.headCommitId === "string" ? completedApp.headCommitId : null,
|
|
1037
|
+
localSync: localSyncResult,
|
|
1038
|
+
localUpdated: true
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// src/application/collab/collabReconcile.ts
|
|
1043
|
+
var import_promises8 = __toESM(require("fs/promises"), 1);
|
|
1044
|
+
var import_node_os4 = __toESM(require("os"), 1);
|
|
1045
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
1046
|
+
async function collabReconcile(params) {
|
|
1047
|
+
const repoRoot = await findGitRoot(params.cwd);
|
|
1048
|
+
const binding = await readCollabBinding(repoRoot);
|
|
1049
|
+
if (!binding) {
|
|
1050
|
+
throw new CliError("Repository is not bound to Comerge.", {
|
|
1051
|
+
exitCode: 2,
|
|
1052
|
+
hint: "Run `comerge collab init` first."
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
await ensureCleanWorktree(repoRoot, "`comerge collab reconcile`");
|
|
1056
|
+
const branch = await requireCurrentBranch(repoRoot);
|
|
1057
|
+
const headCommitHash = await getHeadCommitHash(repoRoot);
|
|
1058
|
+
if (!headCommitHash) {
|
|
1059
|
+
throw new CliError("Failed to resolve local HEAD commit.", { exitCode: 1 });
|
|
1060
|
+
}
|
|
1061
|
+
const syncResp = await params.api.syncLocalApp(binding.currentAppId, {
|
|
1062
|
+
baseCommitHash: headCommitHash,
|
|
1063
|
+
repoFingerprint: binding.repoFingerprint ?? void 0,
|
|
1064
|
+
remoteUrl: binding.remoteUrl ?? void 0,
|
|
1065
|
+
defaultBranch: binding.defaultBranch ?? void 0,
|
|
1066
|
+
dryRun: true
|
|
1067
|
+
});
|
|
1068
|
+
const sync = unwrapResponseObject(syncResp, "sync result");
|
|
1069
|
+
if (sync.status === "conflict_risk") {
|
|
1070
|
+
throw new CliError("Local repository metadata conflicts with the bound Comerge app.", {
|
|
1071
|
+
exitCode: 2,
|
|
1072
|
+
hint: sync.warnings.join("\n") || "Run the command from the correct bound repository."
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
if (sync.status !== "base_unknown") {
|
|
1076
|
+
return collabSync(params);
|
|
1077
|
+
}
|
|
1078
|
+
const preflightResp = await params.api.preflightAppReconcile(binding.currentAppId, {
|
|
1079
|
+
localHeadCommitHash: headCommitHash,
|
|
1080
|
+
repoFingerprint: binding.repoFingerprint ?? void 0,
|
|
1081
|
+
remoteUrl: binding.remoteUrl ?? void 0,
|
|
1082
|
+
defaultBranch: binding.defaultBranch ?? void 0
|
|
1083
|
+
});
|
|
1084
|
+
const preflight = unwrapResponseObject(preflightResp, "reconcile preflight");
|
|
1085
|
+
if (preflight.status === "metadata_conflict") {
|
|
1086
|
+
throw new CliError("Local repository metadata conflicts with the bound Comerge app.", {
|
|
1087
|
+
exitCode: 2,
|
|
1088
|
+
hint: preflight.warnings.join("\n") || "Run the command from the correct bound repository."
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
if (preflight.status === "up_to_date") {
|
|
1092
|
+
return collabSync(params);
|
|
1093
|
+
}
|
|
1094
|
+
const previewResult = {
|
|
1095
|
+
status: preflight.status,
|
|
1096
|
+
branch,
|
|
1097
|
+
repoRoot,
|
|
1098
|
+
localHeadCommitHash: headCommitHash,
|
|
1099
|
+
targetHeadCommitId: preflight.targetHeadCommitId,
|
|
1100
|
+
targetHeadCommitHash: preflight.targetHeadCommitHash,
|
|
1101
|
+
warnings: preflight.warnings,
|
|
1102
|
+
applied: false,
|
|
1103
|
+
dryRun: params.dryRun
|
|
1104
|
+
};
|
|
1105
|
+
if (params.dryRun) {
|
|
1106
|
+
return previewResult;
|
|
1107
|
+
}
|
|
1108
|
+
const { bundlePath, headCommitHash: bundledHeadCommitHash } = await createGitBundle(repoRoot, "reconcile-local.bundle");
|
|
1109
|
+
const bundleTempDir = import_node_path8.default.dirname(bundlePath);
|
|
1110
|
+
try {
|
|
1111
|
+
const bundleStat = await import_promises8.default.stat(bundlePath);
|
|
1112
|
+
const checksumSha256 = await sha256FileHex(bundlePath);
|
|
1113
|
+
const presignResp = await params.api.presignImportUploadFirstParty({
|
|
1114
|
+
file: {
|
|
1115
|
+
name: import_node_path8.default.basename(bundlePath),
|
|
1116
|
+
mimeType: "application/x-git-bundle",
|
|
1117
|
+
size: bundleStat.size,
|
|
1118
|
+
checksumSha256
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
const uploadTarget = unwrapResponseObject(presignResp, "reconcile upload target");
|
|
1122
|
+
await uploadPresigned({
|
|
1123
|
+
uploadUrl: String(uploadTarget.uploadUrl),
|
|
1124
|
+
filePath: bundlePath,
|
|
1125
|
+
headers: uploadTarget.headers ?? {}
|
|
1126
|
+
});
|
|
1127
|
+
const startResp = await params.api.startAppReconcile(binding.currentAppId, {
|
|
1128
|
+
uploadId: String(uploadTarget.uploadId),
|
|
1129
|
+
localHeadCommitHash: bundledHeadCommitHash,
|
|
1130
|
+
repoFingerprint: binding.repoFingerprint ?? void 0,
|
|
1131
|
+
remoteUrl: binding.remoteUrl ?? void 0,
|
|
1132
|
+
defaultBranch: binding.defaultBranch ?? void 0,
|
|
1133
|
+
idempotencyKey: buildDeterministicIdempotencyKey({
|
|
1134
|
+
appId: binding.currentAppId,
|
|
1135
|
+
localHeadCommitHash: bundledHeadCommitHash,
|
|
1136
|
+
targetHeadCommitHash: preflight.targetHeadCommitHash
|
|
1137
|
+
})
|
|
1138
|
+
});
|
|
1139
|
+
const started = unwrapResponseObject(startResp, "reconcile");
|
|
1140
|
+
const reconcile = await pollReconcile(params.api, binding.currentAppId, started.id);
|
|
1141
|
+
if (!reconcile.mergeBaseCommitHash || !reconcile.reconciledHeadCommitHash || !reconcile.resultBundleRef) {
|
|
1142
|
+
throw new CliError("Reconcile completed without enough result metadata to update the local repository.", {
|
|
1143
|
+
exitCode: 1
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
const backup = await createBackupBranch(repoRoot, {
|
|
1147
|
+
branchName: branch,
|
|
1148
|
+
sourceCommitHash: headCommitHash,
|
|
1149
|
+
prefix: "comerge/reconcile-backup"
|
|
1150
|
+
});
|
|
1151
|
+
await hardResetToCommit(repoRoot, reconcile.mergeBaseCommitHash, "`comerge collab reconcile`");
|
|
1152
|
+
const bundleResp = await params.api.downloadAppReconcileBundle(binding.currentAppId, reconcile.id);
|
|
1153
|
+
const resultTempDir = await import_promises8.default.mkdtemp(import_node_path8.default.join(import_node_os4.default.tmpdir(), "comerge-reconcile-"));
|
|
1154
|
+
const resultBundlePath = import_node_path8.default.join(resultTempDir, bundleResp.fileName ?? "reconcile-result.bundle");
|
|
1155
|
+
try {
|
|
1156
|
+
await import_promises8.default.writeFile(resultBundlePath, bundleResp.data);
|
|
1157
|
+
await importGitBundle(repoRoot, resultBundlePath, reconcile.resultBundleRef);
|
|
1158
|
+
await ensureCommitExists(repoRoot, reconcile.reconciledHeadCommitHash);
|
|
1159
|
+
const localCommitHash = await fastForwardToCommit(repoRoot, reconcile.reconciledHeadCommitHash);
|
|
1160
|
+
if (localCommitHash !== reconcile.reconciledHeadCommitHash) {
|
|
1161
|
+
throw new CliError("Local reconcile completed but final HEAD does not match the server result.", { exitCode: 1 });
|
|
1162
|
+
}
|
|
1163
|
+
return {
|
|
1164
|
+
...previewResult,
|
|
1165
|
+
status: reconcile.status,
|
|
1166
|
+
reconcileId: reconcile.id,
|
|
1167
|
+
mergeBaseCommitHash: reconcile.mergeBaseCommitHash,
|
|
1168
|
+
reconciledHeadCommitId: reconcile.reconciledHeadCommitId,
|
|
1169
|
+
reconciledHeadCommitHash: reconcile.reconciledHeadCommitHash,
|
|
1170
|
+
backupBranchName: backup.branchName,
|
|
1171
|
+
localCommitHash,
|
|
1172
|
+
applied: true,
|
|
1173
|
+
dryRun: false
|
|
1174
|
+
};
|
|
1175
|
+
} finally {
|
|
1176
|
+
await import_promises8.default.rm(resultTempDir, { recursive: true, force: true });
|
|
1177
|
+
}
|
|
1178
|
+
} finally {
|
|
1179
|
+
await import_promises8.default.rm(bundleTempDir, { recursive: true, force: true });
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// src/application/collab/collabRequestMerge.ts
|
|
1184
|
+
async function collabRequestMerge(params) {
|
|
1185
|
+
const repoRoot = await findGitRoot(params.cwd);
|
|
1186
|
+
const binding = await readCollabBinding(repoRoot);
|
|
1187
|
+
if (!binding) throw new CliError("Repository is not bound to Comerge.", { exitCode: 2 });
|
|
1188
|
+
const resp = await params.api.openMergeRequest(binding.currentAppId);
|
|
1189
|
+
return unwrapResponseObject(resp, "merge request");
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// src/application/collab/collabInbox.ts
|
|
1193
|
+
async function collabInbox(params) {
|
|
1194
|
+
const resp = await params.api.listMergeRequests({ status: "open", kind: "merge" });
|
|
1195
|
+
const mergeRequests = unwrapResponseObject(resp, "merge requests");
|
|
1196
|
+
return { mergeRequests };
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// src/application/collab/collabView.ts
|
|
1200
|
+
async function collabView(params) {
|
|
1201
|
+
const resp = await params.api.getMergeRequestReview(params.mrId);
|
|
1202
|
+
return unwrapResponseObject(resp, "merge request review");
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/application/collab/collabApprove.ts
|
|
1206
|
+
async function collabApprove(params) {
|
|
1207
|
+
const resp = await params.api.updateMergeRequest(params.mrId, { status: "approved" });
|
|
1208
|
+
return unwrapResponseObject(resp, "merge request");
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// src/application/collab/collabReject.ts
|
|
1212
|
+
async function collabReject(params) {
|
|
1213
|
+
const resp = await params.api.updateMergeRequest(params.mrId, { status: "rejected" });
|
|
1214
|
+
return unwrapResponseObject(resp, "merge request");
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/application/collab/collabInvite.ts
|
|
1218
|
+
async function collabInvite(params) {
|
|
1219
|
+
const repoRoot = await findGitRoot(params.cwd);
|
|
1220
|
+
const binding = await readCollabBinding(repoRoot);
|
|
1221
|
+
if (!binding) throw new CliError("Repository is not bound to Comerge.", { exitCode: 2 });
|
|
1222
|
+
const resp = await params.api.createProjectInvite(binding.projectId, {
|
|
1223
|
+
email: params.email,
|
|
1224
|
+
role: params.role?.trim() || "viewer"
|
|
1225
|
+
});
|
|
1226
|
+
return unwrapResponseObject(resp, "invite");
|
|
1227
|
+
}
|
|
1228
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1229
|
+
0 && (module.exports = {
|
|
1230
|
+
CliError,
|
|
1231
|
+
buildRepoFingerprint,
|
|
1232
|
+
cloneGitBundleToDirectory,
|
|
1233
|
+
collabAdd,
|
|
1234
|
+
collabApprove,
|
|
1235
|
+
collabInbox,
|
|
1236
|
+
collabInit,
|
|
1237
|
+
collabInvite,
|
|
1238
|
+
collabList,
|
|
1239
|
+
collabReconcile,
|
|
1240
|
+
collabReject,
|
|
1241
|
+
collabRemix,
|
|
1242
|
+
collabRequestMerge,
|
|
1243
|
+
collabSync,
|
|
1244
|
+
collabSyncUpstream,
|
|
1245
|
+
collabView,
|
|
1246
|
+
createBackupBranch,
|
|
1247
|
+
createGitBundle,
|
|
1248
|
+
discardTrackedChanges,
|
|
1249
|
+
ensureCleanWorktree,
|
|
1250
|
+
ensureCommitExists,
|
|
1251
|
+
ensureGitInfoExcludeEntries,
|
|
1252
|
+
fastForwardToCommit,
|
|
1253
|
+
findGitRoot,
|
|
1254
|
+
getCollabBindingPath,
|
|
1255
|
+
getCurrentBranch,
|
|
1256
|
+
getDefaultBranch,
|
|
1257
|
+
getHeadCommitHash,
|
|
1258
|
+
getRemoteOriginUrl,
|
|
1259
|
+
getWorkingTreeDiff,
|
|
1260
|
+
hardResetToCommit,
|
|
1261
|
+
importGitBundle,
|
|
1262
|
+
listUntrackedFiles,
|
|
1263
|
+
normalizeGitRemote,
|
|
1264
|
+
readCollabBinding,
|
|
1265
|
+
requireCurrentBranch,
|
|
1266
|
+
summarizeUnifiedDiff,
|
|
1267
|
+
writeCollabBinding,
|
|
1268
|
+
writeTempUnifiedDiffBackup
|
|
1269
|
+
});
|