@fern-api/replay 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -111,6 +111,9 @@ var init_GitClient = __esm({
111
111
  return false;
112
112
  }
113
113
  }
114
+ async getCommitBody(commitSha) {
115
+ return this.exec(["log", "-1", "--format=%B", commitSha]);
116
+ }
114
117
  getRepoPath() {
115
118
  return this.repoPath;
116
119
  }
@@ -138,6 +141,17 @@ function isGenerationCommit(commit) {
138
141
  function isReplayCommit(commit) {
139
142
  return commit.message.startsWith("[fern-replay]");
140
143
  }
144
+ function isRevertCommit(message) {
145
+ return message.startsWith('Revert "');
146
+ }
147
+ function parseRevertedSha(fullBody) {
148
+ const match = fullBody.match(/This reverts commit ([0-9a-f]{40})\./);
149
+ return match?.[1];
150
+ }
151
+ function parseRevertedMessage(subject) {
152
+ const match = subject.match(/^Revert "(.+)"$/);
153
+ return match?.[1];
154
+ }
141
155
 
142
156
  // src/LockfileManager.ts
143
157
  import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from "fs";
@@ -229,6 +243,26 @@ var LockfileManager = class {
229
243
  this.ensureLoaded();
230
244
  this.lock.patches = [];
231
245
  }
246
+ getUnresolvedPatches() {
247
+ this.ensureLoaded();
248
+ return this.lock.patches.filter((p) => p.status === "unresolved");
249
+ }
250
+ getResolvingPatches() {
251
+ this.ensureLoaded();
252
+ return this.lock.patches.filter((p) => p.status === "resolving");
253
+ }
254
+ markPatchUnresolved(patchId) {
255
+ this.updatePatch(patchId, { status: "unresolved" });
256
+ }
257
+ markPatchResolved(patchId, updates) {
258
+ this.ensureLoaded();
259
+ const patch = this.lock.patches.find((p) => p.id === patchId);
260
+ if (!patch) {
261
+ throw new Error(`Patch not found: ${patchId}`);
262
+ }
263
+ delete patch.status;
264
+ Object.assign(patch, updates);
265
+ }
232
266
  getPatches() {
233
267
  this.ensureLoaded();
234
268
  return this.lock.patches;
@@ -280,14 +314,14 @@ var ReplayDetector = class {
280
314
  const lock = this.lockManager.read();
281
315
  const lastGen = this.getLastGeneration(lock);
282
316
  if (!lastGen) {
283
- return [];
317
+ return { patches: [], revertedPatchIds: [] };
284
318
  }
285
319
  const exists = await this.git.commitExists(lastGen.commit_sha);
286
320
  if (!exists) {
287
321
  this.warnings.push(
288
322
  `Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Skipping new patch detection. Existing lockfile patches will still be applied.`
289
323
  );
290
- return [];
324
+ return { patches: [], revertedPatchIds: [] };
291
325
  }
292
326
  const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
293
327
  if (!isAncestor) {
@@ -301,7 +335,7 @@ var ReplayDetector = class {
301
335
  this.sdkOutputDir
302
336
  ]);
303
337
  if (!log.trim()) {
304
- return [];
338
+ return { patches: [], revertedPatchIds: [] };
305
339
  }
306
340
  const commits = this.parseGitLog(log);
307
341
  const newPatches = [];
@@ -337,7 +371,60 @@ var ReplayDetector = class {
337
371
  patch_content: patchContent
338
372
  });
339
373
  }
340
- return newPatches.reverse();
374
+ newPatches.reverse();
375
+ const revertedPatchIdSet = /* @__PURE__ */ new Set();
376
+ const revertIndicesToRemove = /* @__PURE__ */ new Set();
377
+ for (let i = 0; i < newPatches.length; i++) {
378
+ const patch = newPatches[i];
379
+ if (!isRevertCommit(patch.original_message)) continue;
380
+ let body = "";
381
+ try {
382
+ body = await this.git.getCommitBody(patch.original_commit);
383
+ } catch {
384
+ }
385
+ const revertedSha = parseRevertedSha(body);
386
+ const revertedMessage = parseRevertedMessage(patch.original_message);
387
+ let matchedExisting = false;
388
+ if (revertedSha) {
389
+ const existing = lock.patches.find((p) => p.original_commit === revertedSha);
390
+ if (existing) {
391
+ revertedPatchIdSet.add(existing.id);
392
+ revertIndicesToRemove.add(i);
393
+ matchedExisting = true;
394
+ }
395
+ }
396
+ if (!matchedExisting && revertedMessage) {
397
+ const existing = lock.patches.find((p) => p.original_message === revertedMessage);
398
+ if (existing) {
399
+ revertedPatchIdSet.add(existing.id);
400
+ revertIndicesToRemove.add(i);
401
+ matchedExisting = true;
402
+ }
403
+ }
404
+ if (matchedExisting) continue;
405
+ let matchedNew = false;
406
+ if (revertedSha) {
407
+ const idx = newPatches.findIndex(
408
+ (p, j) => j !== i && !revertIndicesToRemove.has(j) && p.original_commit === revertedSha
409
+ );
410
+ if (idx !== -1) {
411
+ revertIndicesToRemove.add(i);
412
+ revertIndicesToRemove.add(idx);
413
+ matchedNew = true;
414
+ }
415
+ }
416
+ if (!matchedNew && revertedMessage) {
417
+ const idx = newPatches.findIndex(
418
+ (p, j) => j !== i && !revertIndicesToRemove.has(j) && p.original_message === revertedMessage
419
+ );
420
+ if (idx !== -1) {
421
+ revertIndicesToRemove.add(i);
422
+ revertIndicesToRemove.add(idx);
423
+ }
424
+ }
425
+ }
426
+ const filteredPatches = newPatches.filter((_, i) => !revertIndicesToRemove.has(i));
427
+ return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
341
428
  }
342
429
  /**
343
430
  * Compute content hash for deduplication.
@@ -348,31 +435,34 @@ var ReplayDetector = class {
348
435
  const normalized = patchContent.split("\n").filter((line) => !line.startsWith("From ") && !line.startsWith("index ") && !line.startsWith("Date: ")).join("\n");
349
436
  return `sha256:${createHash("sha256").update(normalized).digest("hex")}`;
350
437
  }
351
- /** Detect patches via tree diff for non-linear history. Returns a composite patch. */
438
+ /**
439
+ * Detect patches via tree diff for non-linear history. Returns a composite patch.
440
+ * Revert reconciliation is skipped here because tree-diff produces a single composite
441
+ * patch from the aggregate diff — individual revert commits are not distinguishable.
442
+ */
352
443
  async detectPatchesViaTreeDiff(lastGen) {
353
444
  const filesOutput = await this.git.exec(["diff", "--name-only", lastGen.commit_sha, "HEAD"]);
354
445
  const files = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
355
- if (files.length === 0) return [];
446
+ if (files.length === 0) return { patches: [], revertedPatchIds: [] };
356
447
  const diff = await this.git.exec(["diff", lastGen.commit_sha, "HEAD", "--", ...files]);
357
- if (!diff.trim()) return [];
448
+ if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
358
449
  const contentHash = this.computeContentHash(diff);
359
450
  const lock = this.lockManager.read();
360
451
  if (lock.patches.some((p) => p.content_hash === contentHash)) {
361
- return [];
452
+ return { patches: [], revertedPatchIds: [] };
362
453
  }
363
454
  const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
364
- return [
365
- {
366
- id: `patch-composite-${headSha.slice(0, 8)}`,
367
- content_hash: contentHash,
368
- original_commit: headSha,
369
- original_message: "Customer customizations (composite)",
370
- original_author: "composite",
371
- base_generation: lastGen.commit_sha,
372
- files,
373
- patch_content: diff
374
- }
375
- ];
455
+ const compositePatch = {
456
+ id: `patch-composite-${headSha.slice(0, 8)}`,
457
+ content_hash: contentHash,
458
+ original_commit: headSha,
459
+ original_message: "Customer customizations (composite)",
460
+ original_author: "composite",
461
+ base_generation: lastGen.commit_sha,
462
+ files,
463
+ patch_content: diff
464
+ };
465
+ return { patches: [compositePatch], revertedPatchIds: [] };
376
466
  }
377
467
  parseGitLog(log) {
378
468
  return log.trim().split("\n").map((line) => {
@@ -929,12 +1019,12 @@ CLI Version: ${options.cliVersion}`;
929
1019
  await this.git.exec(["commit", "-m", fullMessage]);
930
1020
  return (await this.git.exec(["rev-parse", "HEAD"])).trim();
931
1021
  }
932
- async commitReplay(_patchCount, patches) {
1022
+ async commitReplay(_patchCount, patches, message) {
933
1023
  await this.stageAll();
934
1024
  if (!await this.hasStagedChanges()) {
935
1025
  return (await this.git.exec(["rev-parse", "HEAD"])).trim();
936
1026
  }
937
- let fullMessage = `[fern-replay] Applied customizations`;
1027
+ let fullMessage = message ?? `[fern-replay] Applied customizations`;
938
1028
  if (patches && patches.length > 0) {
939
1029
  fullMessage += "\n\nPatches replayed:";
940
1030
  for (const patch of patches) {
@@ -970,10 +1060,40 @@ CLI Version: ${options.cliVersion}`;
970
1060
  };
971
1061
 
972
1062
  // src/ReplayService.ts
973
- init_GitClient();
974
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
1063
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
975
1064
  import { join as join3 } from "path";
976
1065
  import { minimatch as minimatch2 } from "minimatch";
1066
+ init_GitClient();
1067
+
1068
+ // src/conflict-utils.ts
1069
+ function stripConflictMarkers(content) {
1070
+ const lines = content.split("\n");
1071
+ const result = [];
1072
+ let inConflict = false;
1073
+ let inOurs = false;
1074
+ for (const line of lines) {
1075
+ if (line.startsWith("<<<<<<< ")) {
1076
+ inConflict = true;
1077
+ inOurs = true;
1078
+ continue;
1079
+ }
1080
+ if (inConflict && line === "=======") {
1081
+ inOurs = false;
1082
+ continue;
1083
+ }
1084
+ if (inConflict && line.startsWith(">>>>>>> ")) {
1085
+ inConflict = false;
1086
+ inOurs = false;
1087
+ continue;
1088
+ }
1089
+ if (!inConflict || inOurs) {
1090
+ result.push(line);
1091
+ }
1092
+ }
1093
+ return result.join("\n");
1094
+ }
1095
+
1096
+ // src/ReplayService.ts
977
1097
  var ReplayService = class {
978
1098
  git;
979
1099
  detector;
@@ -1029,13 +1149,27 @@ var ReplayService = class {
1029
1149
  this.lockManager.initializeInMemory(record);
1030
1150
  } else {
1031
1151
  this.lockManager.read();
1152
+ const unresolvedPatches = [
1153
+ ...this.lockManager.getUnresolvedPatches(),
1154
+ ...this.lockManager.getResolvingPatches()
1155
+ ];
1032
1156
  this.lockManager.addGeneration(record);
1033
1157
  this.lockManager.clearPatches();
1158
+ for (const patch of unresolvedPatches) {
1159
+ this.lockManager.addPatch(patch);
1160
+ }
1034
1161
  }
1035
1162
  this.lockManager.save();
1036
1163
  try {
1037
- const redetectedPatches = await this.detector.detectNewPatches();
1164
+ const { patches: redetectedPatches } = await this.detector.detectNewPatches();
1038
1165
  if (redetectedPatches.length > 0) {
1166
+ const redetectedFiles = new Set(redetectedPatches.flatMap((p) => p.files));
1167
+ const currentPatches = this.lockManager.getPatches();
1168
+ for (const patch of currentPatches) {
1169
+ if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
1170
+ this.lockManager.removePatch(patch.id);
1171
+ }
1172
+ }
1039
1173
  for (const patch of redetectedPatches) {
1040
1174
  this.lockManager.addPatch(patch);
1041
1175
  }
@@ -1118,7 +1252,7 @@ var ReplayService = class {
1118
1252
  };
1119
1253
  }
1120
1254
  async handleNoPatchesRegeneration(options) {
1121
- const newPatches = await this.detector.detectNewPatches();
1255
+ const { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
1122
1256
  const warnings = [...this.detector.warnings];
1123
1257
  if (options?.dryRun) {
1124
1258
  return {
@@ -1127,11 +1261,18 @@ var ReplayService = class {
1127
1261
  patchesApplied: 0,
1128
1262
  patchesWithConflicts: 0,
1129
1263
  patchesSkipped: 0,
1264
+ patchesReverted: revertedPatchIds.length,
1130
1265
  conflicts: [],
1131
1266
  wouldApply: newPatches,
1132
1267
  warnings: warnings.length > 0 ? warnings : void 0
1133
1268
  };
1134
1269
  }
1270
+ for (const id of revertedPatchIds) {
1271
+ try {
1272
+ this.lockManager.removePatch(id);
1273
+ } catch {
1274
+ }
1275
+ }
1135
1276
  const commitOpts = options ? {
1136
1277
  cliVersion: options.cliVersion ?? "unknown",
1137
1278
  generatorVersions: options.generatorVersions ?? {},
@@ -1143,6 +1284,12 @@ var ReplayService = class {
1143
1284
  let results = [];
1144
1285
  if (newPatches.length > 0) {
1145
1286
  results = await this.applicator.applyPatches(newPatches);
1287
+ this.revertConflictingFiles(results);
1288
+ for (const result of results) {
1289
+ if (result.status === "conflict") {
1290
+ result.patch.status = "unresolved";
1291
+ }
1292
+ }
1146
1293
  }
1147
1294
  const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
1148
1295
  for (const patch of newPatches) {
@@ -1153,20 +1300,18 @@ var ReplayService = class {
1153
1300
  this.lockManager.save();
1154
1301
  if (newPatches.length > 0) {
1155
1302
  if (!options?.stageOnly) {
1156
- const appliedCount = results.filter((r) => r.status === "applied" || r.status === "conflict").length;
1157
- if (appliedCount > 0) {
1158
- await this.committer.commitReplay(appliedCount, newPatches);
1159
- }
1303
+ const appliedCount = results.filter((r) => r.status === "applied").length;
1304
+ await this.committer.commitReplay(appliedCount, newPatches);
1160
1305
  } else {
1161
1306
  await this.committer.stageAll();
1162
1307
  }
1163
1308
  }
1164
- return this.buildReport("no-patches", newPatches, results, options, warnings, rebaseCounts);
1309
+ return this.buildReport("no-patches", newPatches, results, options, warnings, rebaseCounts, void 0, revertedPatchIds.length);
1165
1310
  }
1166
1311
  async handleNormalRegeneration(options) {
1167
1312
  if (options?.dryRun) {
1168
1313
  const existingPatches2 = this.lockManager.getPatches();
1169
- const newPatches2 = await this.detector.detectNewPatches();
1314
+ const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.detector.detectNewPatches();
1170
1315
  const warnings2 = [...this.detector.warnings];
1171
1316
  const allPatches2 = [...existingPatches2, ...newPatches2];
1172
1317
  return {
@@ -1175,13 +1320,19 @@ var ReplayService = class {
1175
1320
  patchesApplied: 0,
1176
1321
  patchesWithConflicts: 0,
1177
1322
  patchesSkipped: 0,
1323
+ patchesReverted: dryRunReverted.length,
1178
1324
  conflicts: [],
1179
1325
  wouldApply: allPatches2,
1180
1326
  warnings: warnings2.length > 0 ? warnings2 : void 0
1181
1327
  };
1182
1328
  }
1183
1329
  let existingPatches = this.lockManager.getPatches();
1330
+ const preRebasePatchIds = new Set(existingPatches.map((p) => p.id));
1184
1331
  const preRebaseCounts = await this.preGenerationRebase(existingPatches);
1332
+ const postRebasePatchIds = new Set(this.lockManager.getPatches().map((p) => p.id));
1333
+ const removedByPreRebase = existingPatches.filter(
1334
+ (p) => preRebasePatchIds.has(p.id) && !postRebasePatchIds.has(p.id)
1335
+ );
1185
1336
  existingPatches = this.lockManager.getPatches();
1186
1337
  const seenHashes = /* @__PURE__ */ new Set();
1187
1338
  for (const p of existingPatches) {
@@ -1192,8 +1343,28 @@ var ReplayService = class {
1192
1343
  }
1193
1344
  }
1194
1345
  existingPatches = this.lockManager.getPatches();
1195
- const newPatches = await this.detector.detectNewPatches();
1346
+ let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
1196
1347
  const warnings = [...this.detector.warnings];
1348
+ if (removedByPreRebase.length > 0) {
1349
+ const removedOriginalCommits = new Set(removedByPreRebase.map((p) => p.original_commit));
1350
+ const removedOriginalMessages = new Set(removedByPreRebase.map((p) => p.original_message));
1351
+ newPatches = newPatches.filter((p) => {
1352
+ if (removedOriginalCommits.has(p.original_commit)) return false;
1353
+ if (isRevertCommit(p.original_message)) {
1354
+ const revertedMsg = parseRevertedMessage(p.original_message);
1355
+ if (revertedMsg && removedOriginalMessages.has(revertedMsg)) return false;
1356
+ }
1357
+ return true;
1358
+ });
1359
+ }
1360
+ for (const id of revertedPatchIds) {
1361
+ try {
1362
+ this.lockManager.removePatch(id);
1363
+ } catch {
1364
+ }
1365
+ }
1366
+ const revertedSet = new Set(revertedPatchIds);
1367
+ existingPatches = existingPatches.filter((p) => !revertedSet.has(p.id));
1197
1368
  const allPatches = [...existingPatches, ...newPatches];
1198
1369
  const commitOpts = options ? {
1199
1370
  cliVersion: options.cliVersion ?? "unknown",
@@ -1204,20 +1375,32 @@ var ReplayService = class {
1204
1375
  const genRecord = await this.committer.createGenerationRecord(commitOpts);
1205
1376
  this.lockManager.addGeneration(genRecord);
1206
1377
  const results = await this.applicator.applyPatches(allPatches);
1378
+ this.revertConflictingFiles(results);
1379
+ for (const result of results) {
1380
+ if (result.status === "conflict") {
1381
+ result.patch.status = "unresolved";
1382
+ }
1383
+ }
1207
1384
  const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
1208
1385
  for (const patch of newPatches) {
1209
1386
  if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
1210
1387
  this.lockManager.addPatch(patch);
1211
1388
  }
1212
1389
  }
1390
+ for (const result of results) {
1391
+ if (result.status === "conflict") {
1392
+ try {
1393
+ this.lockManager.markPatchUnresolved(result.patch.id);
1394
+ } catch {
1395
+ }
1396
+ }
1397
+ }
1213
1398
  this.lockManager.save();
1214
1399
  if (options?.stageOnly) {
1215
1400
  await this.committer.stageAll();
1216
1401
  } else {
1217
- const appliedCount = results.filter((r) => r.status === "applied" || r.status === "conflict").length;
1218
- if (appliedCount > 0) {
1219
- await this.committer.commitReplay(appliedCount, allPatches);
1220
- }
1402
+ const appliedCount = results.filter((r) => r.status === "applied").length;
1403
+ await this.committer.commitReplay(appliedCount, allPatches);
1221
1404
  }
1222
1405
  return this.buildReport(
1223
1406
  "normal-regeneration",
@@ -1226,7 +1409,8 @@ var ReplayService = class {
1226
1409
  options,
1227
1410
  warnings,
1228
1411
  rebaseCounts,
1229
- preRebaseCounts
1412
+ preRebaseCounts,
1413
+ revertedPatchIds.length
1230
1414
  );
1231
1415
  }
1232
1416
  /**
@@ -1365,6 +1549,10 @@ var ReplayService = class {
1365
1549
  let conflictAbsorbed = 0;
1366
1550
  let contentRefreshed = 0;
1367
1551
  for (const patch of patches) {
1552
+ if (patch.status != null) {
1553
+ delete patch.status;
1554
+ continue;
1555
+ }
1368
1556
  if (patch.base_generation === currentGen) {
1369
1557
  try {
1370
1558
  const diff = await this.git.exec(["diff", currentGen, "HEAD", "--", ...patch.files]).catch(() => null);
@@ -1415,12 +1603,31 @@ var ReplayService = class {
1415
1603
  }
1416
1604
  return { conflictResolved, conflictAbsorbed, contentRefreshed };
1417
1605
  }
1606
+ /**
1607
+ * After applyPatches(), strip conflict markers from conflicting files
1608
+ * so only clean content is committed. Keeps the Generated (OURS) side.
1609
+ */
1610
+ revertConflictingFiles(results) {
1611
+ for (const result of results) {
1612
+ if (result.status !== "conflict" || !result.fileResults) continue;
1613
+ for (const fileResult of result.fileResults) {
1614
+ if (fileResult.status !== "conflict") continue;
1615
+ const filePath = join3(this.outputDir, fileResult.file);
1616
+ try {
1617
+ const content = readFileSync2(filePath, "utf-8");
1618
+ const stripped = stripConflictMarkers(content);
1619
+ writeFileSync2(filePath, stripped);
1620
+ } catch {
1621
+ }
1622
+ }
1623
+ }
1624
+ }
1418
1625
  readFernignorePatterns() {
1419
1626
  const fernignorePath = join3(this.outputDir, ".fernignore");
1420
1627
  if (!existsSync2(fernignorePath)) return [];
1421
1628
  return readFileSync2(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
1422
1629
  }
1423
- buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts) {
1630
+ buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted) {
1424
1631
  const conflictResults = results.filter((r) => r.status === "conflict");
1425
1632
  const conflictDetails = conflictResults.map((r) => {
1426
1633
  const conflictFiles = r.fileResults?.filter((f) => f.status === "conflict") ?? [];
@@ -1445,10 +1652,17 @@ var ReplayService = class {
1445
1652
  patchesRepointed: rebaseCounts && rebaseCounts.repointed > 0 ? rebaseCounts.repointed : void 0,
1446
1653
  patchesContentRebased: rebaseCounts && rebaseCounts.contentRebased > 0 ? rebaseCounts.contentRebased : void 0,
1447
1654
  patchesKeptAsUserOwned: rebaseCounts && rebaseCounts.keptAsUserOwned > 0 ? rebaseCounts.keptAsUserOwned : void 0,
1655
+ patchesReverted: patchesReverted && patchesReverted > 0 ? patchesReverted : void 0,
1448
1656
  patchesConflictResolved: preRebaseCounts && preRebaseCounts.conflictResolved + preRebaseCounts.conflictAbsorbed > 0 ? preRebaseCounts.conflictResolved + preRebaseCounts.conflictAbsorbed : void 0,
1449
1657
  patchesRefreshed: preRebaseCounts && preRebaseCounts.contentRefreshed > 0 ? preRebaseCounts.contentRefreshed : void 0,
1450
1658
  conflicts: conflictResults.flatMap((r) => r.fileResults?.filter((f) => f.status === "conflict") ?? []),
1451
1659
  conflictDetails: conflictDetails.length > 0 ? conflictDetails : void 0,
1660
+ unresolvedPatches: conflictResults.length > 0 ? conflictResults.map((r) => ({
1661
+ patchId: r.patch.id,
1662
+ patchMessage: r.patch.original_message,
1663
+ files: r.patch.files,
1664
+ conflictDetails: r.fileResults?.filter((f) => f.status === "conflict") ?? []
1665
+ })) : void 0,
1452
1666
  wouldApply: options?.dryRun ? patches : void 0,
1453
1667
  warnings: warnings && warnings.length > 0 ? warnings : void 0
1454
1668
  };
@@ -1457,7 +1671,7 @@ var ReplayService = class {
1457
1671
 
1458
1672
  // src/FernignoreMigrator.ts
1459
1673
  import { createHash as createHash2 } from "crypto";
1460
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync2 } from "fs";
1674
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync3 } from "fs";
1461
1675
  import { dirname as dirname3, join as join4 } from "path";
1462
1676
  import { minimatch as minimatch3 } from "minimatch";
1463
1677
  import { parse as parse2, stringify as stringify2 } from "yaml";
@@ -1485,7 +1699,7 @@ var FernignoreMigrator = class {
1485
1699
  async analyzeMigration() {
1486
1700
  const patterns = this.readFernignorePatterns();
1487
1701
  const detector = new ReplayDetector(this.git, this.lockManager, this.outputDir);
1488
- const patches = await detector.detectNewPatches();
1702
+ const { patches } = await detector.detectNewPatches();
1489
1703
  const trackedByBoth = [];
1490
1704
  const fernignoreOnly = [];
1491
1705
  const commitsOnly = [];
@@ -1610,7 +1824,7 @@ var FernignoreMigrator = class {
1610
1824
  async migrate() {
1611
1825
  const analysis = await this.analyzeMigration();
1612
1826
  const detector = new ReplayDetector(this.git, this.lockManager, this.outputDir);
1613
- const patches = await detector.detectNewPatches();
1827
+ const { patches } = await detector.detectNewPatches();
1614
1828
  const warnings = [];
1615
1829
  let patchesCreated = 0;
1616
1830
  for (const patch of patches) {
@@ -1645,13 +1859,13 @@ var FernignoreMigrator = class {
1645
1859
  if (!existsSync3(dir)) {
1646
1860
  mkdirSync2(dir, { recursive: true });
1647
1861
  }
1648
- writeFileSync2(replayYmlPath, stringify2(config, { lineWidth: 0 }), "utf-8");
1862
+ writeFileSync3(replayYmlPath, stringify2(config, { lineWidth: 0 }), "utf-8");
1649
1863
  }
1650
1864
  };
1651
1865
 
1652
1866
  // src/commands/bootstrap.ts
1653
1867
  import { createHash as createHash3 } from "crypto";
1654
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1868
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
1655
1869
  import { join as join5 } from "path";
1656
1870
  init_GitClient();
1657
1871
  async function bootstrap(outputDir, options) {
@@ -1837,7 +2051,7 @@ function ensureFernignoreEntries(outputDir) {
1837
2051
  content += "\n";
1838
2052
  }
1839
2053
  content += toAdd.join("\n") + "\n";
1840
- writeFileSync3(fernignorePath, content, "utf-8");
2054
+ writeFileSync4(fernignorePath, content, "utf-8");
1841
2055
  return true;
1842
2056
  }
1843
2057
  function computeContentHash(patchContent) {
@@ -1910,17 +2124,82 @@ async function resolve(outputDir, options) {
1910
2124
  return { success: false, reason: "no-patches" };
1911
2125
  }
1912
2126
  const git = new GitClient(outputDir);
2127
+ const unresolvedPatches = lockManager.getUnresolvedPatches();
2128
+ const resolvingPatches = lockManager.getResolvingPatches();
2129
+ if (unresolvedPatches.length > 0) {
2130
+ const applicator = new ReplayApplicator(git, lockManager, outputDir);
2131
+ await applicator.applyPatches(unresolvedPatches);
2132
+ const markerFiles = await findConflictMarkerFiles(git);
2133
+ if (markerFiles.length > 0) {
2134
+ for (const patch of unresolvedPatches) {
2135
+ lockManager.updatePatch(patch.id, { status: "resolving" });
2136
+ }
2137
+ lockManager.save();
2138
+ return {
2139
+ success: false,
2140
+ reason: "conflicts-applied",
2141
+ unresolvedFiles: markerFiles,
2142
+ phase: "applied",
2143
+ patchesApplied: unresolvedPatches.length
2144
+ };
2145
+ }
2146
+ }
1913
2147
  if (options?.checkMarkers !== false) {
1914
- const markerFiles = await git.exec(["grep", "-l", "<<<<<<<", "--", "."]).catch(() => "");
1915
- if (markerFiles.trim()) {
1916
- const files = markerFiles.trim().split("\n").filter(Boolean);
1917
- return { success: false, reason: "unresolved-conflicts", unresolvedFiles: files };
2148
+ const currentMarkerFiles = await findConflictMarkerFiles(git);
2149
+ if (currentMarkerFiles.length > 0) {
2150
+ return { success: false, reason: "unresolved-conflicts", unresolvedFiles: currentMarkerFiles };
2151
+ }
2152
+ }
2153
+ const patchesToCommit = [...resolvingPatches, ...unresolvedPatches];
2154
+ if (patchesToCommit.length > 0) {
2155
+ const currentGen = lock.current_generation;
2156
+ const detector = new ReplayDetector(git, lockManager, outputDir);
2157
+ let patchesResolved = 0;
2158
+ for (const patch of patchesToCommit) {
2159
+ const diff = await git.exec(["diff", currentGen, "--", ...patch.files]).catch(() => null);
2160
+ if (!diff || !diff.trim()) {
2161
+ lockManager.removePatch(patch.id);
2162
+ continue;
2163
+ }
2164
+ const newContentHash = detector.computeContentHash(diff);
2165
+ const changedFiles = await getChangedFiles(git, currentGen, patch.files);
2166
+ lockManager.markPatchResolved(patch.id, {
2167
+ patch_content: diff,
2168
+ content_hash: newContentHash,
2169
+ base_generation: currentGen,
2170
+ files: changedFiles
2171
+ });
2172
+ patchesResolved++;
1918
2173
  }
2174
+ lockManager.save();
2175
+ const committer2 = new ReplayCommitter(git, outputDir);
2176
+ await committer2.stageAll();
2177
+ const commitSha2 = await committer2.commitReplay(
2178
+ lock.patches.length,
2179
+ lock.patches,
2180
+ "[fern-replay] Resolved conflicts"
2181
+ );
2182
+ return {
2183
+ success: true,
2184
+ commitSha: commitSha2,
2185
+ phase: "committed",
2186
+ patchesResolved
2187
+ };
1919
2188
  }
1920
2189
  const committer = new ReplayCommitter(git, outputDir);
1921
2190
  await committer.stageAll();
1922
2191
  const commitSha = await committer.commitReplay(lock.patches.length, lock.patches);
1923
- return { success: true, commitSha };
2192
+ return { success: true, commitSha, phase: "committed" };
2193
+ }
2194
+ async function findConflictMarkerFiles(git) {
2195
+ const output = await git.exec(["grep", "-l", "<<<<<<<", "--", "."]).catch(() => "");
2196
+ return output.trim() ? output.trim().split("\n").filter(Boolean) : [];
2197
+ }
2198
+ async function getChangedFiles(git, currentGen, files) {
2199
+ const filesOutput = await git.exec(["diff", "--name-only", currentGen, "--", ...files]).catch(() => null);
2200
+ if (!filesOutput || !filesOutput.trim()) return files;
2201
+ const changed = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !f.startsWith(".fern/"));
2202
+ return changed.length > 0 ? changed : files;
1924
2203
  }
1925
2204
 
1926
2205
  // src/commands/status.ts
@@ -1961,6 +2240,9 @@ export {
1961
2240
  forget,
1962
2241
  isGenerationCommit,
1963
2242
  isReplayCommit,
2243
+ isRevertCommit,
2244
+ parseRevertedMessage,
2245
+ parseRevertedSha,
1964
2246
  reset,
1965
2247
  resolve,
1966
2248
  status,