@fern-api/replay 0.6.1 → 0.7.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.cjs CHANGED
@@ -273,6 +273,26 @@ var LockfileManager = class {
273
273
  this.ensureLoaded();
274
274
  this.lock.patches = [];
275
275
  }
276
+ getUnresolvedPatches() {
277
+ this.ensureLoaded();
278
+ return this.lock.patches.filter((p) => p.status === "unresolved");
279
+ }
280
+ getResolvingPatches() {
281
+ this.ensureLoaded();
282
+ return this.lock.patches.filter((p) => p.status === "resolving");
283
+ }
284
+ markPatchUnresolved(patchId) {
285
+ this.updatePatch(patchId, { status: "unresolved" });
286
+ }
287
+ markPatchResolved(patchId, updates) {
288
+ this.ensureLoaded();
289
+ const patch = this.lock.patches.find((p) => p.id === patchId);
290
+ if (!patch) {
291
+ throw new Error(`Patch not found: ${patchId}`);
292
+ }
293
+ delete patch.status;
294
+ Object.assign(patch, updates);
295
+ }
276
296
  getPatches() {
277
297
  this.ensureLoaded();
278
298
  return this.lock.patches;
@@ -973,12 +993,12 @@ CLI Version: ${options.cliVersion}`;
973
993
  await this.git.exec(["commit", "-m", fullMessage]);
974
994
  return (await this.git.exec(["rev-parse", "HEAD"])).trim();
975
995
  }
976
- async commitReplay(patchCount, patches) {
996
+ async commitReplay(_patchCount, patches, message) {
977
997
  await this.stageAll();
978
998
  if (!await this.hasStagedChanges()) {
979
999
  return (await this.git.exec(["rev-parse", "HEAD"])).trim();
980
1000
  }
981
- let fullMessage = `[fern-replay] Applied ${patchCount} customization(s)`;
1001
+ let fullMessage = message ?? `[fern-replay] Applied customizations`;
982
1002
  if (patches && patches.length > 0) {
983
1003
  fullMessage += "\n\nPatches replayed:";
984
1004
  for (const patch of patches) {
@@ -1073,13 +1093,27 @@ var ReplayService = class {
1073
1093
  this.lockManager.initializeInMemory(record);
1074
1094
  } else {
1075
1095
  this.lockManager.read();
1096
+ const unresolvedPatches = [
1097
+ ...this.lockManager.getUnresolvedPatches(),
1098
+ ...this.lockManager.getResolvingPatches()
1099
+ ];
1076
1100
  this.lockManager.addGeneration(record);
1077
1101
  this.lockManager.clearPatches();
1102
+ for (const patch of unresolvedPatches) {
1103
+ this.lockManager.addPatch(patch);
1104
+ }
1078
1105
  }
1079
1106
  this.lockManager.save();
1080
1107
  try {
1081
1108
  const redetectedPatches = await this.detector.detectNewPatches();
1082
1109
  if (redetectedPatches.length > 0) {
1110
+ const redetectedFiles = new Set(redetectedPatches.flatMap((p) => p.files));
1111
+ const currentPatches = this.lockManager.getPatches();
1112
+ for (const patch of currentPatches) {
1113
+ if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
1114
+ this.lockManager.removePatch(patch.id);
1115
+ }
1116
+ }
1083
1117
  for (const patch of redetectedPatches) {
1084
1118
  this.lockManager.addPatch(patch);
1085
1119
  }
@@ -1187,6 +1221,12 @@ var ReplayService = class {
1187
1221
  let results = [];
1188
1222
  if (newPatches.length > 0) {
1189
1223
  results = await this.applicator.applyPatches(newPatches);
1224
+ this.revertConflictingFiles(results);
1225
+ for (const result of results) {
1226
+ if (result.status === "conflict") {
1227
+ result.patch.status = "unresolved";
1228
+ }
1229
+ }
1190
1230
  }
1191
1231
  const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
1192
1232
  for (const patch of newPatches) {
@@ -1197,10 +1237,8 @@ var ReplayService = class {
1197
1237
  this.lockManager.save();
1198
1238
  if (newPatches.length > 0) {
1199
1239
  if (!options?.stageOnly) {
1200
- const appliedCount = results.filter((r) => r.status === "applied" || r.status === "conflict").length;
1201
- if (appliedCount > 0) {
1202
- await this.committer.commitReplay(appliedCount, newPatches);
1203
- }
1240
+ const appliedCount = results.filter((r) => r.status === "applied").length;
1241
+ await this.committer.commitReplay(appliedCount, newPatches);
1204
1242
  } else {
1205
1243
  await this.committer.stageAll();
1206
1244
  }
@@ -1248,20 +1286,32 @@ var ReplayService = class {
1248
1286
  const genRecord = await this.committer.createGenerationRecord(commitOpts);
1249
1287
  this.lockManager.addGeneration(genRecord);
1250
1288
  const results = await this.applicator.applyPatches(allPatches);
1289
+ this.revertConflictingFiles(results);
1290
+ for (const result of results) {
1291
+ if (result.status === "conflict") {
1292
+ result.patch.status = "unresolved";
1293
+ }
1294
+ }
1251
1295
  const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
1252
1296
  for (const patch of newPatches) {
1253
1297
  if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
1254
1298
  this.lockManager.addPatch(patch);
1255
1299
  }
1256
1300
  }
1301
+ for (const result of results) {
1302
+ if (result.status === "conflict") {
1303
+ try {
1304
+ this.lockManager.markPatchUnresolved(result.patch.id);
1305
+ } catch {
1306
+ }
1307
+ }
1308
+ }
1257
1309
  this.lockManager.save();
1258
1310
  if (options?.stageOnly) {
1259
1311
  await this.committer.stageAll();
1260
1312
  } else {
1261
- const appliedCount = results.filter((r) => r.status === "applied" || r.status === "conflict").length;
1262
- if (appliedCount > 0) {
1263
- await this.committer.commitReplay(appliedCount, allPatches);
1264
- }
1313
+ const appliedCount = results.filter((r) => r.status === "applied").length;
1314
+ await this.committer.commitReplay(appliedCount, allPatches);
1265
1315
  }
1266
1316
  return this.buildReport(
1267
1317
  "normal-regeneration",
@@ -1409,6 +1459,10 @@ var ReplayService = class {
1409
1459
  let conflictAbsorbed = 0;
1410
1460
  let contentRefreshed = 0;
1411
1461
  for (const patch of patches) {
1462
+ if (patch.status != null) {
1463
+ delete patch.status;
1464
+ continue;
1465
+ }
1412
1466
  if (patch.base_generation === currentGen) {
1413
1467
  try {
1414
1468
  const diff = await this.git.exec(["diff", currentGen, "HEAD", "--", ...patch.files]).catch(() => null);
@@ -1459,6 +1513,55 @@ var ReplayService = class {
1459
1513
  }
1460
1514
  return { conflictResolved, conflictAbsorbed, contentRefreshed };
1461
1515
  }
1516
+ /**
1517
+ * Strip conflict markers from file content, keeping the OURS (Generated) side.
1518
+ * Preserves clean patches' non-conflicting changes on shared files.
1519
+ */
1520
+ stripConflictMarkers(content) {
1521
+ const lines = content.split("\n");
1522
+ const result = [];
1523
+ let inConflict = false;
1524
+ let inOurs = false;
1525
+ for (const line of lines) {
1526
+ if (line.startsWith("<<<<<<< ")) {
1527
+ inConflict = true;
1528
+ inOurs = true;
1529
+ continue;
1530
+ }
1531
+ if (inConflict && line === "=======") {
1532
+ inOurs = false;
1533
+ continue;
1534
+ }
1535
+ if (inConflict && line.startsWith(">>>>>>> ")) {
1536
+ inConflict = false;
1537
+ inOurs = false;
1538
+ continue;
1539
+ }
1540
+ if (!inConflict || inOurs) {
1541
+ result.push(line);
1542
+ }
1543
+ }
1544
+ return result.join("\n");
1545
+ }
1546
+ /**
1547
+ * After applyPatches(), strip conflict markers from conflicting files
1548
+ * so only clean content is committed. Keeps the Generated (OURS) side.
1549
+ */
1550
+ revertConflictingFiles(results) {
1551
+ for (const result of results) {
1552
+ if (result.status !== "conflict" || !result.fileResults) continue;
1553
+ for (const fileResult of result.fileResults) {
1554
+ if (fileResult.status !== "conflict") continue;
1555
+ const filePath = (0, import_node_path3.join)(this.outputDir, fileResult.file);
1556
+ try {
1557
+ const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
1558
+ const stripped = this.stripConflictMarkers(content);
1559
+ (0, import_node_fs2.writeFileSync)(filePath, stripped);
1560
+ } catch {
1561
+ }
1562
+ }
1563
+ }
1564
+ }
1462
1565
  readFernignorePatterns() {
1463
1566
  const fernignorePath = (0, import_node_path3.join)(this.outputDir, ".fernignore");
1464
1567
  if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
@@ -1493,6 +1596,12 @@ var ReplayService = class {
1493
1596
  patchesRefreshed: preRebaseCounts && preRebaseCounts.contentRefreshed > 0 ? preRebaseCounts.contentRefreshed : void 0,
1494
1597
  conflicts: conflictResults.flatMap((r) => r.fileResults?.filter((f) => f.status === "conflict") ?? []),
1495
1598
  conflictDetails: conflictDetails.length > 0 ? conflictDetails : void 0,
1599
+ unresolvedPatches: conflictResults.length > 0 ? conflictResults.map((r) => ({
1600
+ patchId: r.patch.id,
1601
+ patchMessage: r.patch.original_message,
1602
+ files: r.patch.files,
1603
+ conflictDetails: r.fileResults?.filter((f) => f.status === "conflict") ?? []
1604
+ })) : void 0,
1496
1605
  wouldApply: options?.dryRun ? patches : void 0,
1497
1606
  warnings: warnings && warnings.length > 0 ? warnings : void 0
1498
1607
  };
@@ -1954,17 +2063,82 @@ async function resolve(outputDir, options) {
1954
2063
  return { success: false, reason: "no-patches" };
1955
2064
  }
1956
2065
  const git = new GitClient(outputDir);
2066
+ const unresolvedPatches = lockManager.getUnresolvedPatches();
2067
+ const resolvingPatches = lockManager.getResolvingPatches();
2068
+ if (unresolvedPatches.length > 0) {
2069
+ const applicator = new ReplayApplicator(git, lockManager, outputDir);
2070
+ await applicator.applyPatches(unresolvedPatches);
2071
+ const markerFiles = await findConflictMarkerFiles(git);
2072
+ if (markerFiles.length > 0) {
2073
+ for (const patch of unresolvedPatches) {
2074
+ lockManager.updatePatch(patch.id, { status: "resolving" });
2075
+ }
2076
+ lockManager.save();
2077
+ return {
2078
+ success: false,
2079
+ reason: "conflicts-applied",
2080
+ unresolvedFiles: markerFiles,
2081
+ phase: "applied",
2082
+ patchesApplied: unresolvedPatches.length
2083
+ };
2084
+ }
2085
+ }
1957
2086
  if (options?.checkMarkers !== false) {
1958
- const markerFiles = await git.exec(["grep", "-l", "<<<<<<<", "--", "."]).catch(() => "");
1959
- if (markerFiles.trim()) {
1960
- const files = markerFiles.trim().split("\n").filter(Boolean);
1961
- return { success: false, reason: "unresolved-conflicts", unresolvedFiles: files };
2087
+ const currentMarkerFiles = await findConflictMarkerFiles(git);
2088
+ if (currentMarkerFiles.length > 0) {
2089
+ return { success: false, reason: "unresolved-conflicts", unresolvedFiles: currentMarkerFiles };
2090
+ }
2091
+ }
2092
+ const patchesToCommit = [...resolvingPatches, ...unresolvedPatches];
2093
+ if (patchesToCommit.length > 0) {
2094
+ const currentGen = lock.current_generation;
2095
+ const detector = new ReplayDetector(git, lockManager, outputDir);
2096
+ let patchesResolved = 0;
2097
+ for (const patch of patchesToCommit) {
2098
+ const diff = await git.exec(["diff", currentGen, "--", ...patch.files]).catch(() => null);
2099
+ if (!diff || !diff.trim()) {
2100
+ lockManager.removePatch(patch.id);
2101
+ continue;
2102
+ }
2103
+ const newContentHash = detector.computeContentHash(diff);
2104
+ const changedFiles = await getChangedFiles(git, currentGen, patch.files);
2105
+ lockManager.markPatchResolved(patch.id, {
2106
+ patch_content: diff,
2107
+ content_hash: newContentHash,
2108
+ base_generation: currentGen,
2109
+ files: changedFiles
2110
+ });
2111
+ patchesResolved++;
1962
2112
  }
2113
+ lockManager.save();
2114
+ const committer2 = new ReplayCommitter(git, outputDir);
2115
+ await committer2.stageAll();
2116
+ const commitSha2 = await committer2.commitReplay(
2117
+ lock.patches.length,
2118
+ lock.patches,
2119
+ "[fern-replay] Resolved conflicts"
2120
+ );
2121
+ return {
2122
+ success: true,
2123
+ commitSha: commitSha2,
2124
+ phase: "committed",
2125
+ patchesResolved
2126
+ };
1963
2127
  }
1964
2128
  const committer = new ReplayCommitter(git, outputDir);
1965
2129
  await committer.stageAll();
1966
2130
  const commitSha = await committer.commitReplay(lock.patches.length, lock.patches);
1967
- return { success: true, commitSha };
2131
+ return { success: true, commitSha, phase: "committed" };
2132
+ }
2133
+ async function findConflictMarkerFiles(git) {
2134
+ const output = await git.exec(["grep", "-l", "<<<<<<<", "--", "."]).catch(() => "");
2135
+ return output.trim() ? output.trim().split("\n").filter(Boolean) : [];
2136
+ }
2137
+ async function getChangedFiles(git, currentGen, files) {
2138
+ const filesOutput = await git.exec(["diff", "--name-only", currentGen, "--", ...files]).catch(() => null);
2139
+ if (!filesOutput || !filesOutput.trim()) return files;
2140
+ const changed = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !f.startsWith(".fern/"));
2141
+ return changed.length > 0 ? changed : files;
1968
2142
  }
1969
2143
 
1970
2144
  // src/commands/status.ts