@fern-api/replay 0.6.2 → 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/cli.cjs CHANGED
@@ -12935,6 +12935,26 @@ var LockfileManager = class {
12935
12935
  this.ensureLoaded();
12936
12936
  this.lock.patches = [];
12937
12937
  }
12938
+ getUnresolvedPatches() {
12939
+ this.ensureLoaded();
12940
+ return this.lock.patches.filter((p) => p.status === "unresolved");
12941
+ }
12942
+ getResolvingPatches() {
12943
+ this.ensureLoaded();
12944
+ return this.lock.patches.filter((p) => p.status === "resolving");
12945
+ }
12946
+ markPatchUnresolved(patchId) {
12947
+ this.updatePatch(patchId, { status: "unresolved" });
12948
+ }
12949
+ markPatchResolved(patchId, updates) {
12950
+ this.ensureLoaded();
12951
+ const patch = this.lock.patches.find((p) => p.id === patchId);
12952
+ if (!patch) {
12953
+ throw new Error(`Patch not found: ${patchId}`);
12954
+ }
12955
+ delete patch.status;
12956
+ Object.assign(patch, updates);
12957
+ }
12938
12958
  getPatches() {
12939
12959
  this.ensureLoaded();
12940
12960
  return this.lock.patches;
@@ -15206,12 +15226,12 @@ CLI Version: ${options.cliVersion}`;
15206
15226
  await this.git.exec(["commit", "-m", fullMessage]);
15207
15227
  return (await this.git.exec(["rev-parse", "HEAD"])).trim();
15208
15228
  }
15209
- async commitReplay(_patchCount, patches) {
15229
+ async commitReplay(_patchCount, patches, message) {
15210
15230
  await this.stageAll();
15211
15231
  if (!await this.hasStagedChanges()) {
15212
15232
  return (await this.git.exec(["rev-parse", "HEAD"])).trim();
15213
15233
  }
15214
- let fullMessage = `[fern-replay] Applied customizations`;
15234
+ let fullMessage = message ?? `[fern-replay] Applied customizations`;
15215
15235
  if (patches && patches.length > 0) {
15216
15236
  fullMessage += "\n\nPatches replayed:";
15217
15237
  for (const patch of patches) {
@@ -15302,13 +15322,27 @@ var ReplayService = class {
15302
15322
  this.lockManager.initializeInMemory(record);
15303
15323
  } else {
15304
15324
  this.lockManager.read();
15325
+ const unresolvedPatches = [
15326
+ ...this.lockManager.getUnresolvedPatches(),
15327
+ ...this.lockManager.getResolvingPatches()
15328
+ ];
15305
15329
  this.lockManager.addGeneration(record);
15306
15330
  this.lockManager.clearPatches();
15331
+ for (const patch of unresolvedPatches) {
15332
+ this.lockManager.addPatch(patch);
15333
+ }
15307
15334
  }
15308
15335
  this.lockManager.save();
15309
15336
  try {
15310
15337
  const redetectedPatches = await this.detector.detectNewPatches();
15311
15338
  if (redetectedPatches.length > 0) {
15339
+ const redetectedFiles = new Set(redetectedPatches.flatMap((p) => p.files));
15340
+ const currentPatches = this.lockManager.getPatches();
15341
+ for (const patch of currentPatches) {
15342
+ if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
15343
+ this.lockManager.removePatch(patch.id);
15344
+ }
15345
+ }
15312
15346
  for (const patch of redetectedPatches) {
15313
15347
  this.lockManager.addPatch(patch);
15314
15348
  }
@@ -15416,6 +15450,12 @@ var ReplayService = class {
15416
15450
  let results = [];
15417
15451
  if (newPatches.length > 0) {
15418
15452
  results = await this.applicator.applyPatches(newPatches);
15453
+ this.revertConflictingFiles(results);
15454
+ for (const result of results) {
15455
+ if (result.status === "conflict") {
15456
+ result.patch.status = "unresolved";
15457
+ }
15458
+ }
15419
15459
  }
15420
15460
  const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
15421
15461
  for (const patch of newPatches) {
@@ -15426,10 +15466,8 @@ var ReplayService = class {
15426
15466
  this.lockManager.save();
15427
15467
  if (newPatches.length > 0) {
15428
15468
  if (!options?.stageOnly) {
15429
- const appliedCount = results.filter((r) => r.status === "applied" || r.status === "conflict").length;
15430
- if (appliedCount > 0) {
15431
- await this.committer.commitReplay(appliedCount, newPatches);
15432
- }
15469
+ const appliedCount = results.filter((r) => r.status === "applied").length;
15470
+ await this.committer.commitReplay(appliedCount, newPatches);
15433
15471
  } else {
15434
15472
  await this.committer.stageAll();
15435
15473
  }
@@ -15477,20 +15515,32 @@ var ReplayService = class {
15477
15515
  const genRecord = await this.committer.createGenerationRecord(commitOpts);
15478
15516
  this.lockManager.addGeneration(genRecord);
15479
15517
  const results = await this.applicator.applyPatches(allPatches);
15518
+ this.revertConflictingFiles(results);
15519
+ for (const result of results) {
15520
+ if (result.status === "conflict") {
15521
+ result.patch.status = "unresolved";
15522
+ }
15523
+ }
15480
15524
  const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
15481
15525
  for (const patch of newPatches) {
15482
15526
  if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
15483
15527
  this.lockManager.addPatch(patch);
15484
15528
  }
15485
15529
  }
15530
+ for (const result of results) {
15531
+ if (result.status === "conflict") {
15532
+ try {
15533
+ this.lockManager.markPatchUnresolved(result.patch.id);
15534
+ } catch {
15535
+ }
15536
+ }
15537
+ }
15486
15538
  this.lockManager.save();
15487
15539
  if (options?.stageOnly) {
15488
15540
  await this.committer.stageAll();
15489
15541
  } else {
15490
- const appliedCount = results.filter((r) => r.status === "applied" || r.status === "conflict").length;
15491
- if (appliedCount > 0) {
15492
- await this.committer.commitReplay(appliedCount, allPatches);
15493
- }
15542
+ const appliedCount = results.filter((r) => r.status === "applied").length;
15543
+ await this.committer.commitReplay(appliedCount, allPatches);
15494
15544
  }
15495
15545
  return this.buildReport(
15496
15546
  "normal-regeneration",
@@ -15638,6 +15688,10 @@ var ReplayService = class {
15638
15688
  let conflictAbsorbed = 0;
15639
15689
  let contentRefreshed = 0;
15640
15690
  for (const patch of patches) {
15691
+ if (patch.status != null) {
15692
+ delete patch.status;
15693
+ continue;
15694
+ }
15641
15695
  if (patch.base_generation === currentGen) {
15642
15696
  try {
15643
15697
  const diff = await this.git.exec(["diff", currentGen, "HEAD", "--", ...patch.files]).catch(() => null);
@@ -15688,6 +15742,55 @@ var ReplayService = class {
15688
15742
  }
15689
15743
  return { conflictResolved, conflictAbsorbed, contentRefreshed };
15690
15744
  }
15745
+ /**
15746
+ * Strip conflict markers from file content, keeping the OURS (Generated) side.
15747
+ * Preserves clean patches' non-conflicting changes on shared files.
15748
+ */
15749
+ stripConflictMarkers(content) {
15750
+ const lines = content.split("\n");
15751
+ const result = [];
15752
+ let inConflict = false;
15753
+ let inOurs = false;
15754
+ for (const line of lines) {
15755
+ if (line.startsWith("<<<<<<< ")) {
15756
+ inConflict = true;
15757
+ inOurs = true;
15758
+ continue;
15759
+ }
15760
+ if (inConflict && line === "=======") {
15761
+ inOurs = false;
15762
+ continue;
15763
+ }
15764
+ if (inConflict && line.startsWith(">>>>>>> ")) {
15765
+ inConflict = false;
15766
+ inOurs = false;
15767
+ continue;
15768
+ }
15769
+ if (!inConflict || inOurs) {
15770
+ result.push(line);
15771
+ }
15772
+ }
15773
+ return result.join("\n");
15774
+ }
15775
+ /**
15776
+ * After applyPatches(), strip conflict markers from conflicting files
15777
+ * so only clean content is committed. Keeps the Generated (OURS) side.
15778
+ */
15779
+ revertConflictingFiles(results) {
15780
+ for (const result of results) {
15781
+ if (result.status !== "conflict" || !result.fileResults) continue;
15782
+ for (const fileResult of result.fileResults) {
15783
+ if (fileResult.status !== "conflict") continue;
15784
+ const filePath = (0, import_node_path4.join)(this.outputDir, fileResult.file);
15785
+ try {
15786
+ const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
15787
+ const stripped = this.stripConflictMarkers(content);
15788
+ (0, import_node_fs2.writeFileSync)(filePath, stripped);
15789
+ } catch {
15790
+ }
15791
+ }
15792
+ }
15793
+ }
15691
15794
  readFernignorePatterns() {
15692
15795
  const fernignorePath = (0, import_node_path4.join)(this.outputDir, ".fernignore");
15693
15796
  if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
@@ -15722,6 +15825,12 @@ var ReplayService = class {
15722
15825
  patchesRefreshed: preRebaseCounts && preRebaseCounts.contentRefreshed > 0 ? preRebaseCounts.contentRefreshed : void 0,
15723
15826
  conflicts: conflictResults.flatMap((r) => r.fileResults?.filter((f) => f.status === "conflict") ?? []),
15724
15827
  conflictDetails: conflictDetails.length > 0 ? conflictDetails : void 0,
15828
+ unresolvedPatches: conflictResults.length > 0 ? conflictResults.map((r) => ({
15829
+ patchId: r.patch.id,
15830
+ patchMessage: r.patch.original_message,
15831
+ files: r.patch.files,
15832
+ conflictDetails: r.fileResults?.filter((f) => f.status === "conflict") ?? []
15833
+ })) : void 0,
15725
15834
  wouldApply: options?.dryRun ? patches : void 0,
15726
15835
  warnings: warnings && warnings.length > 0 ? warnings : void 0
15727
15836
  };
@@ -16181,17 +16290,82 @@ async function resolve(outputDir, options) {
16181
16290
  return { success: false, reason: "no-patches" };
16182
16291
  }
16183
16292
  const git = new GitClient(outputDir);
16293
+ const unresolvedPatches = lockManager.getUnresolvedPatches();
16294
+ const resolvingPatches = lockManager.getResolvingPatches();
16295
+ if (unresolvedPatches.length > 0) {
16296
+ const applicator = new ReplayApplicator(git, lockManager, outputDir);
16297
+ await applicator.applyPatches(unresolvedPatches);
16298
+ const markerFiles = await findConflictMarkerFiles(git);
16299
+ if (markerFiles.length > 0) {
16300
+ for (const patch of unresolvedPatches) {
16301
+ lockManager.updatePatch(patch.id, { status: "resolving" });
16302
+ }
16303
+ lockManager.save();
16304
+ return {
16305
+ success: false,
16306
+ reason: "conflicts-applied",
16307
+ unresolvedFiles: markerFiles,
16308
+ phase: "applied",
16309
+ patchesApplied: unresolvedPatches.length
16310
+ };
16311
+ }
16312
+ }
16184
16313
  if (options?.checkMarkers !== false) {
16185
- const markerFiles = await git.exec(["grep", "-l", "<<<<<<<", "--", "."]).catch(() => "");
16186
- if (markerFiles.trim()) {
16187
- const files = markerFiles.trim().split("\n").filter(Boolean);
16188
- return { success: false, reason: "unresolved-conflicts", unresolvedFiles: files };
16314
+ const currentMarkerFiles = await findConflictMarkerFiles(git);
16315
+ if (currentMarkerFiles.length > 0) {
16316
+ return { success: false, reason: "unresolved-conflicts", unresolvedFiles: currentMarkerFiles };
16189
16317
  }
16190
16318
  }
16319
+ const patchesToCommit = [...resolvingPatches, ...unresolvedPatches];
16320
+ if (patchesToCommit.length > 0) {
16321
+ const currentGen = lock.current_generation;
16322
+ const detector = new ReplayDetector(git, lockManager, outputDir);
16323
+ let patchesResolved = 0;
16324
+ for (const patch of patchesToCommit) {
16325
+ const diff = await git.exec(["diff", currentGen, "--", ...patch.files]).catch(() => null);
16326
+ if (!diff || !diff.trim()) {
16327
+ lockManager.removePatch(patch.id);
16328
+ continue;
16329
+ }
16330
+ const newContentHash = detector.computeContentHash(diff);
16331
+ const changedFiles = await getChangedFiles(git, currentGen, patch.files);
16332
+ lockManager.markPatchResolved(patch.id, {
16333
+ patch_content: diff,
16334
+ content_hash: newContentHash,
16335
+ base_generation: currentGen,
16336
+ files: changedFiles
16337
+ });
16338
+ patchesResolved++;
16339
+ }
16340
+ lockManager.save();
16341
+ const committer2 = new ReplayCommitter(git, outputDir);
16342
+ await committer2.stageAll();
16343
+ const commitSha2 = await committer2.commitReplay(
16344
+ lock.patches.length,
16345
+ lock.patches,
16346
+ "[fern-replay] Resolved conflicts"
16347
+ );
16348
+ return {
16349
+ success: true,
16350
+ commitSha: commitSha2,
16351
+ phase: "committed",
16352
+ patchesResolved
16353
+ };
16354
+ }
16191
16355
  const committer = new ReplayCommitter(git, outputDir);
16192
16356
  await committer.stageAll();
16193
16357
  const commitSha = await committer.commitReplay(lock.patches.length, lock.patches);
16194
- return { success: true, commitSha };
16358
+ return { success: true, commitSha, phase: "committed" };
16359
+ }
16360
+ async function findConflictMarkerFiles(git) {
16361
+ const output = await git.exec(["grep", "-l", "<<<<<<<", "--", "."]).catch(() => "");
16362
+ return output.trim() ? output.trim().split("\n").filter(Boolean) : [];
16363
+ }
16364
+ async function getChangedFiles(git, currentGen, files) {
16365
+ const filesOutput = await git.exec(["diff", "--name-only", currentGen, "--", ...files]).catch(() => null);
16366
+ if (!filesOutput || !filesOutput.trim()) return files;
16367
+ const changed = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !f.startsWith(".fern/"));
16368
+ return changed.length > 0 ? changed : files;
16195
16369
  }
16196
16370
 
16197
16371
  // src/cli.ts
@@ -16449,14 +16623,23 @@ function printReport(report) {
16449
16623
  }
16450
16624
  }
16451
16625
  }
16452
- console.log("\nResolve conflicts by editing the marked files, then commit.");
16453
- console.log("Or run: fern-replay forget <file-pattern> to remove problematic patches");
16626
+ console.log("\nOr run: fern-replay forget <file-pattern> to remove problematic patches");
16454
16627
  } else if (report.conflicts.length > 0) {
16455
16628
  console.log("\nConflicts:");
16456
16629
  for (const conflict of report.conflicts) {
16457
16630
  console.log(` ${conflict.file}: ${conflict.reason ?? "merge conflict"}`);
16458
16631
  }
16459
16632
  }
16633
+ if (report.unresolvedPatches && report.unresolvedPatches.length > 0) {
16634
+ console.log("\nUnresolved Patches (need local resolution):");
16635
+ for (const patch of report.unresolvedPatches) {
16636
+ console.log(` Patch: ${patch.patchId} - "${patch.patchMessage}"`);
16637
+ for (const file of patch.files) {
16638
+ console.log(` ${file}`);
16639
+ }
16640
+ }
16641
+ console.log("\nRun 'fern-replay resolve <dir>' on this branch to resolve conflicts.");
16642
+ }
16460
16643
  if (report.warnings && report.warnings.length > 0) {
16461
16644
  console.log("\nWarnings:");
16462
16645
  for (const w of report.warnings) console.log(` ${w}`);
@@ -16572,6 +16755,17 @@ async function runResolve(dir, flags) {
16572
16755
  case "no-patches":
16573
16756
  console.error("No patches in lockfile. Nothing to resolve.");
16574
16757
  break;
16758
+ case "conflicts-applied":
16759
+ console.log(`Applied ${result.patchesApplied} unresolved patch(es) to working tree.`);
16760
+ console.log(`
16761
+ ${result.unresolvedFiles.length} file(s) have conflicts:`);
16762
+ for (const f of result.unresolvedFiles) {
16763
+ console.log(` ${f}`);
16764
+ }
16765
+ console.log(`
16766
+ Resolve the conflicts in your editor, then run 'fern-replay resolve ${dir}' again.`);
16767
+ return;
16768
+ // Normal flow, don't exit(1)
16575
16769
  case "unresolved-conflicts":
16576
16770
  console.error("Conflict markers still present in:");
16577
16771
  for (const f of result.unresolvedFiles ?? []) {
@@ -16585,8 +16779,11 @@ async function runResolve(dir, flags) {
16585
16779
  }
16586
16780
  process.exit(1);
16587
16781
  }
16782
+ if (result.patchesResolved) {
16783
+ console.log(`Resolution complete. Updated ${result.patchesResolved} patch(es) in lockfile.`);
16784
+ }
16588
16785
  console.log(`Created [fern-replay] commit: ${result.commitSha?.slice(0, 7)}`);
16589
- console.log("Conflict resolution complete.");
16786
+ console.log("Push to update the PR.");
16590
16787
  }
16591
16788
  async function main() {
16592
16789
  const { command, dir, flags, pattern } = parseArgs(process.argv);