@fern-api/replay 0.11.0 → 0.12.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
@@ -13469,7 +13469,7 @@ var FERN_SUPPORT_NAMES = ["fern-support", "Fern Support"];
13469
13469
  function isGenerationCommit(commit) {
13470
13470
  const isFernSupport = FERN_SUPPORT_NAMES.includes(commit.authorName);
13471
13471
  const isBotAuthor = !isFernSupport && (commit.authorLogin === FERN_BOT_LOGIN || commit.authorEmail === FERN_BOT_EMAIL || commit.authorName === FERN_BOT_NAME);
13472
- const hasGenerationMarker = commit.message.startsWith("[fern-generated]") || commit.message.startsWith("[fern-replay]") || commit.message.includes("Generated by Fern") || commit.message.includes("\u{1F916} Generated with Fern") || // Squash merge of a Fern-generated PR uses the PR title as commit message.
13472
+ const hasGenerationMarker = commit.message.startsWith("[fern-generated]") || commit.message.startsWith("[fern-replay]") || commit.message.startsWith("[fern-autoversion]") || commit.message.includes("Generated by Fern") || commit.message.includes("\u{1F916} Generated with Fern") || // Squash merge of a Fern-generated PR uses the PR title as commit message.
13473
13473
  // The default PR title is "SDK Generation" (from GithubStep's commitMessage default).
13474
13474
  // GitHub appends "(#N)" for the PR number, e.g. "SDK Generation (#70)".
13475
13475
  commit.message.startsWith("SDK Generation");
@@ -16546,21 +16546,66 @@ var ReplayService = class {
16546
16546
  this.applicator = new ReplayApplicator(git, this.lockManager, outputDir);
16547
16547
  this.committer = new ReplayCommitter(git, outputDir);
16548
16548
  }
16549
- async runReplay(options) {
16549
+ /**
16550
+ * Phase 1 of the two-phase replay flow. Runs preGenerationRebase (when applicable),
16551
+ * detects new patches, and creates the `[fern-generated]` commit. Leaves HEAD at
16552
+ * the generation commit (or unchanged for dry-run).
16553
+ *
16554
+ * Does NOT apply patches — the returned handle must be passed to
16555
+ * `applyPreparedReplay()` to complete the run. No disk writes to the lockfile
16556
+ * happen here; lockfile persistence is deferred to phase 2.
16557
+ *
16558
+ * Callers may land additional commits on HEAD between `prepareReplay` and
16559
+ * `applyPreparedReplay` (e.g., `[fern-autoversion]`).
16560
+ */
16561
+ async prepareReplay(options) {
16550
16562
  this.warnings = [];
16563
+ this.detector.warnings.length = 0;
16551
16564
  if (options?.skipApplication) {
16552
- return this.handleSkipApplication(options);
16565
+ return this._prepareSkipApplication(options);
16553
16566
  }
16554
16567
  const flow = this.determineFlow();
16555
16568
  switch (flow) {
16556
16569
  case "first-generation":
16557
- return this.handleFirstGeneration(options);
16570
+ return this._prepareFirstGeneration(options);
16558
16571
  case "no-patches":
16559
- return this.handleNoPatchesRegeneration(options);
16572
+ return this._prepareNoPatchesRegeneration(options);
16560
16573
  case "normal-regeneration":
16561
- return this.handleNormalRegeneration(options);
16574
+ return this._prepareNormalRegeneration(options);
16562
16575
  }
16563
16576
  }
16577
+ /**
16578
+ * Phase 2 of the two-phase replay flow. Applies patches, rebases them against
16579
+ * the generation, saves the lockfile, and commits `[fern-replay]` (or stages
16580
+ * under `stageOnly`). Operates on HEAD at call time — mid-phase commits are
16581
+ * respected.
16582
+ *
16583
+ * For terminal handles (dry-run, first-gen has no patch work, skip-app does
16584
+ * its own post-commit logic), this returns the precomputed report.
16585
+ */
16586
+ async applyPreparedReplay(prep, options) {
16587
+ if (prep._prepared.terminal) {
16588
+ return prep._prepared.preparedReport;
16589
+ }
16590
+ switch (prep._prepared.flow) {
16591
+ case "first-generation":
16592
+ return this._applyFirstGeneration(prep);
16593
+ case "skip-application":
16594
+ return this._applySkipApplication(prep, options);
16595
+ case "no-patches":
16596
+ return this._applyNoPatchesRegeneration(prep, options);
16597
+ case "normal-regeneration":
16598
+ return this._applyNormalRegeneration(prep, options);
16599
+ }
16600
+ }
16601
+ /**
16602
+ * Backwards-compatible atomic entry point. Composes `prepareReplay` + `applyPreparedReplay`.
16603
+ * Behavior is byte-identical to the pre-split implementation for all existing callers.
16604
+ */
16605
+ async runReplay(options) {
16606
+ const prep = await this.prepareReplay(options);
16607
+ return this.applyPreparedReplay(prep, options);
16608
+ }
16564
16609
  /**
16565
16610
  * Sync the lockfile after a divergent PR was squash-merged.
16566
16611
  * Call this BEFORE runReplay() when the CLI detects a merged divergent PR.
@@ -16634,15 +16679,35 @@ var ReplayService = class {
16634
16679
  throw error;
16635
16680
  }
16636
16681
  }
16637
- async handleFirstGeneration(options) {
16682
+ firstGenerationReport() {
16683
+ return {
16684
+ flow: "first-generation",
16685
+ patchesDetected: 0,
16686
+ patchesApplied: 0,
16687
+ patchesWithConflicts: 0,
16688
+ patchesSkipped: 0,
16689
+ conflicts: []
16690
+ };
16691
+ }
16692
+ async _prepareFirstGeneration(options) {
16638
16693
  if (options?.dryRun) {
16694
+ const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
16639
16695
  return {
16640
16696
  flow: "first-generation",
16641
- patchesDetected: 0,
16642
- patchesApplied: 0,
16643
- patchesWithConflicts: 0,
16644
- patchesSkipped: 0,
16645
- conflicts: []
16697
+ previousGenerationSha: null,
16698
+ currentGenerationSha: headSha,
16699
+ baseBranchHead: options.baseBranchHead ?? null,
16700
+ _prepared: {
16701
+ flow: "first-generation",
16702
+ terminal: true,
16703
+ preparedReport: this.firstGenerationReport(),
16704
+ patchesToApply: [],
16705
+ newPatchIds: /* @__PURE__ */ new Set(),
16706
+ detectorWarnings: [],
16707
+ revertedCount: 0,
16708
+ genSha: headSha,
16709
+ optionsSnapshot: options
16710
+ }
16646
16711
  };
16647
16712
  }
16648
16713
  const commitOpts = options ? {
@@ -16652,22 +16717,36 @@ var ReplayService = class {
16652
16717
  } : void 0;
16653
16718
  await this.committer.commitGeneration("Initial SDK generation", commitOpts);
16654
16719
  const genRecord = await this.committer.createGenerationRecord(commitOpts);
16655
- this.lockManager.initialize(genRecord);
16720
+ this.lockManager.initializeInMemory(genRecord);
16656
16721
  return {
16657
16722
  flow: "first-generation",
16658
- patchesDetected: 0,
16659
- patchesApplied: 0,
16660
- patchesWithConflicts: 0,
16661
- patchesSkipped: 0,
16662
- conflicts: []
16723
+ previousGenerationSha: null,
16724
+ currentGenerationSha: genRecord.commit_sha,
16725
+ baseBranchHead: options?.baseBranchHead ?? null,
16726
+ _prepared: {
16727
+ flow: "first-generation",
16728
+ terminal: false,
16729
+ patchesToApply: [],
16730
+ newPatchIds: /* @__PURE__ */ new Set(),
16731
+ detectorWarnings: [],
16732
+ revertedCount: 0,
16733
+ genSha: genRecord.commit_sha,
16734
+ optionsSnapshot: options
16735
+ }
16663
16736
  };
16664
16737
  }
16738
+ async _applyFirstGeneration(_prep) {
16739
+ this.lockManager.save();
16740
+ return this.firstGenerationReport();
16741
+ }
16665
16742
  /**
16666
- * Skip-application mode: commit the generation and update the lockfile
16667
- * but don't detect or apply patches. Sets a marker so the next normal
16668
- * run skips revert detection in preGenerationRebase().
16743
+ * Skip-application mode phase 1: commit the generation and update the
16744
+ * in-memory lockfile, but don't detect or apply patches. Sets a marker
16745
+ * so the next normal run skips revert detection in preGenerationRebase().
16746
+ *
16747
+ * Disk save is deferred to `_applySkipApplication`.
16669
16748
  */
16670
- async handleSkipApplication(options) {
16749
+ async _prepareSkipApplication(options) {
16671
16750
  const commitOpts = options ? {
16672
16751
  cliVersion: options.cliVersion ?? "unknown",
16673
16752
  generatorVersions: options.generatorVersions ?? {},
@@ -16676,8 +16755,10 @@ var ReplayService = class {
16676
16755
  await this.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
16677
16756
  await this.cleanupStaleConflictMarkers();
16678
16757
  const genRecord = await this.committer.createGenerationRecord(commitOpts);
16758
+ let previousGenerationSha = null;
16679
16759
  try {
16680
- this.lockManager.read();
16760
+ const lock = this.lockManager.read();
16761
+ previousGenerationSha = lock.current_generation ?? null;
16681
16762
  this.lockManager.addGeneration(genRecord);
16682
16763
  } catch (error) {
16683
16764
  if (error instanceof LockfileNotFoundError) {
@@ -16687,6 +16768,24 @@ var ReplayService = class {
16687
16768
  }
16688
16769
  }
16689
16770
  this.lockManager.setReplaySkippedAt((/* @__PURE__ */ new Date()).toISOString());
16771
+ return {
16772
+ flow: "skip-application",
16773
+ previousGenerationSha,
16774
+ currentGenerationSha: genRecord.commit_sha,
16775
+ baseBranchHead: options?.baseBranchHead ?? null,
16776
+ _prepared: {
16777
+ flow: "skip-application",
16778
+ terminal: false,
16779
+ patchesToApply: [],
16780
+ newPatchIds: /* @__PURE__ */ new Set(),
16781
+ detectorWarnings: [],
16782
+ revertedCount: 0,
16783
+ genSha: genRecord.commit_sha,
16784
+ optionsSnapshot: options
16785
+ }
16786
+ };
16787
+ }
16788
+ async _applySkipApplication(_prep, options) {
16690
16789
  this.lockManager.save();
16691
16790
  const credentialWarnings = [...this.lockManager.warnings];
16692
16791
  if (!options?.stageOnly) {
@@ -16704,12 +16803,14 @@ var ReplayService = class {
16704
16803
  warnings: credentialWarnings.length > 0 ? credentialWarnings : void 0
16705
16804
  };
16706
16805
  }
16707
- async handleNoPatchesRegeneration(options) {
16806
+ async _prepareNoPatchesRegeneration(options) {
16807
+ const preLock = this.lockManager.read();
16808
+ const previousGenerationSha = preLock.current_generation ?? null;
16708
16809
  let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
16709
16810
  const detectorWarnings = [...this.detector.warnings];
16710
16811
  if (options?.dryRun) {
16711
16812
  const dryRunWarnings = [...detectorWarnings, ...this.warnings];
16712
- return {
16813
+ const preparedReport = {
16713
16814
  flow: "no-patches",
16714
16815
  patchesDetected: newPatches.length,
16715
16816
  patchesApplied: 0,
@@ -16720,9 +16821,26 @@ var ReplayService = class {
16720
16821
  wouldApply: newPatches,
16721
16822
  warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0
16722
16823
  };
16824
+ const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
16825
+ return {
16826
+ flow: "no-patches",
16827
+ previousGenerationSha,
16828
+ currentGenerationSha: headSha,
16829
+ baseBranchHead: options.baseBranchHead ?? null,
16830
+ _prepared: {
16831
+ flow: "no-patches",
16832
+ terminal: true,
16833
+ preparedReport,
16834
+ patchesToApply: [],
16835
+ newPatchIds: /* @__PURE__ */ new Set(),
16836
+ detectorWarnings,
16837
+ revertedCount: revertedPatchIds.length,
16838
+ genSha: headSha,
16839
+ optionsSnapshot: options
16840
+ }
16841
+ };
16723
16842
  }
16724
16843
  {
16725
- const preLock = this.lockManager.read();
16726
16844
  const preCurrentGen = preLock.current_generation;
16727
16845
  const uniqueFiles = [...new Set(newPatches.flatMap((p) => p.files))];
16728
16846
  const absorbedFiles = /* @__PURE__ */ new Set();
@@ -16753,6 +16871,27 @@ var ReplayService = class {
16753
16871
  await this.cleanupStaleConflictMarkers();
16754
16872
  const genRecord = await this.committer.createGenerationRecord(commitOpts);
16755
16873
  this.lockManager.addGeneration(genRecord);
16874
+ return {
16875
+ flow: "no-patches",
16876
+ previousGenerationSha,
16877
+ currentGenerationSha: genRecord.commit_sha,
16878
+ baseBranchHead: options?.baseBranchHead ?? null,
16879
+ _prepared: {
16880
+ flow: "no-patches",
16881
+ terminal: false,
16882
+ patchesToApply: newPatches,
16883
+ newPatchIds: new Set(newPatches.map((p) => p.id)),
16884
+ detectorWarnings,
16885
+ revertedCount: revertedPatchIds.length,
16886
+ genSha: genRecord.commit_sha,
16887
+ optionsSnapshot: options
16888
+ }
16889
+ };
16890
+ }
16891
+ async _applyNoPatchesRegeneration(prep, options) {
16892
+ const newPatches = prep._prepared.patchesToApply;
16893
+ const detectorWarnings = prep._prepared.detectorWarnings;
16894
+ const genSha = prep._prepared.genSha;
16756
16895
  let results = [];
16757
16896
  if (newPatches.length > 0) {
16758
16897
  results = await this.applicator.applyPatches(newPatches);
@@ -16764,7 +16903,7 @@ var ReplayService = class {
16764
16903
  }
16765
16904
  }
16766
16905
  }
16767
- const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
16906
+ const rebaseCounts = await this.rebasePatches(results, genSha);
16768
16907
  for (const patch of newPatches) {
16769
16908
  if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
16770
16909
  this.lockManager.addPatch(patch);
@@ -16781,15 +16920,26 @@ var ReplayService = class {
16781
16920
  }
16782
16921
  }
16783
16922
  const warnings = [...detectorWarnings, ...this.warnings];
16784
- return this.buildReport("no-patches", newPatches, results, options, warnings, rebaseCounts, void 0, revertedPatchIds.length);
16923
+ return this.buildReport(
16924
+ "no-patches",
16925
+ newPatches,
16926
+ results,
16927
+ prep._prepared.optionsSnapshot,
16928
+ warnings,
16929
+ rebaseCounts,
16930
+ void 0,
16931
+ prep._prepared.revertedCount
16932
+ );
16785
16933
  }
16786
- async handleNormalRegeneration(options) {
16934
+ async _prepareNormalRegeneration(options) {
16935
+ const preLock = this.lockManager.read();
16936
+ const previousGenerationSha = preLock.current_generation ?? null;
16787
16937
  if (options?.dryRun) {
16788
16938
  const existingPatches2 = this.lockManager.getPatches();
16789
16939
  const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.detector.detectNewPatches();
16790
- const warnings2 = [...this.detector.warnings, ...this.warnings];
16940
+ const warnings = [...this.detector.warnings, ...this.warnings];
16791
16941
  const allPatches2 = [...existingPatches2, ...newPatches2];
16792
- return {
16942
+ const preparedReport = {
16793
16943
  flow: "normal-regeneration",
16794
16944
  patchesDetected: allPatches2.length,
16795
16945
  patchesApplied: 0,
@@ -16798,7 +16948,25 @@ var ReplayService = class {
16798
16948
  patchesReverted: dryRunReverted.length,
16799
16949
  conflicts: [],
16800
16950
  wouldApply: allPatches2,
16801
- warnings: warnings2.length > 0 ? warnings2 : void 0
16951
+ warnings: warnings.length > 0 ? warnings : void 0
16952
+ };
16953
+ const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
16954
+ return {
16955
+ flow: "normal-regeneration",
16956
+ previousGenerationSha,
16957
+ currentGenerationSha: headSha,
16958
+ baseBranchHead: options.baseBranchHead ?? null,
16959
+ _prepared: {
16960
+ flow: "normal-regeneration",
16961
+ terminal: true,
16962
+ preparedReport,
16963
+ patchesToApply: [],
16964
+ newPatchIds: /* @__PURE__ */ new Set(),
16965
+ detectorWarnings: [...this.detector.warnings],
16966
+ revertedCount: dryRunReverted.length,
16967
+ genSha: headSha,
16968
+ optionsSnapshot: options
16969
+ }
16802
16970
  };
16803
16971
  }
16804
16972
  let existingPatches = this.lockManager.getPatches();
@@ -16850,6 +17018,29 @@ var ReplayService = class {
16850
17018
  await this.cleanupStaleConflictMarkers();
16851
17019
  const genRecord = await this.committer.createGenerationRecord(commitOpts);
16852
17020
  this.lockManager.addGeneration(genRecord);
17021
+ return {
17022
+ flow: "normal-regeneration",
17023
+ previousGenerationSha,
17024
+ currentGenerationSha: genRecord.commit_sha,
17025
+ baseBranchHead: options?.baseBranchHead ?? null,
17026
+ _prepared: {
17027
+ flow: "normal-regeneration",
17028
+ terminal: false,
17029
+ patchesToApply: allPatches,
17030
+ newPatchIds: new Set(newPatches.map((p) => p.id)),
17031
+ preRebaseCounts,
17032
+ detectorWarnings,
17033
+ revertedCount: revertedPatchIds.length,
17034
+ genSha: genRecord.commit_sha,
17035
+ optionsSnapshot: options
17036
+ }
17037
+ };
17038
+ }
17039
+ async _applyNormalRegeneration(prep, options) {
17040
+ const allPatches = prep._prepared.patchesToApply;
17041
+ const newPatchIds = prep._prepared.newPatchIds;
17042
+ const detectorWarnings = prep._prepared.detectorWarnings;
17043
+ const genSha = prep._prepared.genSha;
16853
17044
  const results = await this.applicator.applyPatches(allPatches);
16854
17045
  this._lastApplyResults = results;
16855
17046
  this.revertConflictingFiles(results);
@@ -16858,7 +17049,8 @@ var ReplayService = class {
16858
17049
  result.patch.status = "unresolved";
16859
17050
  }
16860
17051
  }
16861
- const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
17052
+ const rebaseCounts = await this.rebasePatches(results, genSha);
17053
+ const newPatches = allPatches.filter((p) => newPatchIds.has(p.id));
16862
17054
  for (const patch of newPatches) {
16863
17055
  if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
16864
17056
  this.lockManager.addPatch(patch);
@@ -16885,11 +17077,11 @@ var ReplayService = class {
16885
17077
  "normal-regeneration",
16886
17078
  allPatches,
16887
17079
  results,
16888
- options,
17080
+ prep._prepared.optionsSnapshot,
16889
17081
  warnings,
16890
17082
  rebaseCounts,
16891
- preRebaseCounts,
16892
- revertedPatchIds.length
17083
+ prep._prepared.preRebaseCounts,
17084
+ prep._prepared.revertedCount
16893
17085
  );
16894
17086
  }
16895
17087
  /**