@fern-api/generator-cli 0.9.28 → 0.9.30

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.
Files changed (3) hide show
  1. package/dist/api.js +5 -2
  2. package/dist/cli.js +1615 -1428
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -7031,18 +7031,18 @@ var require_build3 = __commonJS({
7031
7031
  var require_sync = __commonJS({
7032
7032
  "../../node_modules/.pnpm/escalade@3.2.0/node_modules/escalade/sync/index.js"(exports2, module2) {
7033
7033
  "use strict";
7034
- var { dirname: dirname5, resolve: resolve8 } = require("path");
7034
+ var { dirname: dirname6, resolve: resolve8 } = require("path");
7035
7035
  var { readdirSync: readdirSync2, statSync: statSync4 } = require("fs");
7036
7036
  module2.exports = function(start, callback) {
7037
7037
  let dir = resolve8(".", start);
7038
7038
  let tmp4, stats = statSync4(dir);
7039
7039
  if (!stats.isDirectory()) {
7040
- dir = dirname5(dir);
7040
+ dir = dirname6(dir);
7041
7041
  }
7042
7042
  while (true) {
7043
7043
  tmp4 = callback(dir, readdirSync2(dir));
7044
7044
  if (tmp4) return resolve8(dir, tmp4);
7045
- dir = dirname5(tmp4 = dir);
7045
+ dir = dirname6(tmp4 = dir);
7046
7046
  if (tmp4 === dir) break;
7047
7047
  }
7048
7048
  };
@@ -7078,9 +7078,9 @@ var require_require_directory = __commonJS({
7078
7078
  "../../node_modules/.pnpm/require-directory@2.1.1/node_modules/require-directory/index.js"(exports2, module2) {
7079
7079
  "use strict";
7080
7080
  var fs3 = require("fs");
7081
- var join16 = require("path").join;
7081
+ var join17 = require("path").join;
7082
7082
  var resolve8 = require("path").resolve;
7083
- var dirname5 = require("path").dirname;
7083
+ var dirname6 = require("path").dirname;
7084
7084
  var defaultOptions2 = {
7085
7085
  extensions: ["js", "json", "coffee"],
7086
7086
  recurse: true,
@@ -7113,9 +7113,9 @@ var require_require_directory = __commonJS({
7113
7113
  options[prop] = defaultOptions2[prop];
7114
7114
  }
7115
7115
  }
7116
- path5 = !path5 ? dirname5(m.filename) : resolve8(dirname5(m.filename), path5);
7116
+ path5 = !path5 ? dirname6(m.filename) : resolve8(dirname6(m.filename), path5);
7117
7117
  fs3.readdirSync(path5).forEach(function(filename) {
7118
- var joined = join16(path5, filename), files, key, obj;
7118
+ var joined = join17(path5, filename), files, key, obj;
7119
7119
  if (fs3.statSync(joined).isDirectory() && options.recurse) {
7120
7120
  files = requireDirectory(m, joined, options);
7121
7121
  if (Object.keys(files).length) {
@@ -22126,7 +22126,7 @@ var init_diff3 = __esm({
22126
22126
  }
22127
22127
  });
22128
22128
 
22129
- // ../../node_modules/.pnpm/@fern-api+replay@0.15.0/node_modules/@fern-api/replay/dist/index.js
22129
+ // ../../node_modules/.pnpm/@fern-api+replay@0.15.2/node_modules/@fern-api/replay/dist/index.js
22130
22130
  var dist_exports = {};
22131
22131
  __export(dist_exports, {
22132
22132
  CREDENTIAL_PATTERNS: () => CREDENTIAL_PATTERNS,
@@ -22468,73 +22468,55 @@ function parseRevertedMessage(subject) {
22468
22468
  const match2 = subject.match(/^Revert "(.+)"$/);
22469
22469
  return match2?.[1];
22470
22470
  }
22471
- function trimCR(line) {
22472
- return line.endsWith("\r") ? line.slice(0, -1) : line;
22471
+ function isBinaryFile(filePath) {
22472
+ const ext2 = (0, import_path9.extname)(filePath).toLowerCase();
22473
+ return BINARY_EXTENSIONS.has(ext2);
22473
22474
  }
22474
- function findConflictRanges(lines) {
22475
- const ranges = [];
22476
- let i2 = 0;
22477
- while (i2 < lines.length) {
22478
- if (trimCR(lines[i2]) === CONFLICT_OPENER) {
22479
- let separatorIdx = -1;
22480
- let j2 = i2 + 1;
22481
- let found = false;
22482
- while (j2 < lines.length) {
22483
- const trimmed2 = trimCR(lines[j2]);
22484
- if (trimmed2 === CONFLICT_OPENER) {
22485
- break;
22486
- }
22487
- if (separatorIdx === -1 && trimmed2 === CONFLICT_SEPARATOR) {
22488
- separatorIdx = j2;
22489
- } else if (separatorIdx !== -1 && trimmed2 === CONFLICT_CLOSER) {
22490
- ranges.push({ start: i2, separator: separatorIdx, end: j2 });
22491
- i2 = j2;
22492
- found = true;
22493
- break;
22494
- }
22495
- j2++;
22475
+ function parsePatchFileHeaders(patchContent) {
22476
+ const results = [];
22477
+ let currentPath = null;
22478
+ let currentIsCreate = false;
22479
+ let currentIsDelete = false;
22480
+ const flush2 = () => {
22481
+ if (currentPath !== null) {
22482
+ results.push({ path: currentPath, isCreate: currentIsCreate, isDelete: currentIsDelete });
22483
+ }
22484
+ };
22485
+ for (const line of patchContent.split("\n")) {
22486
+ if (line.startsWith("diff --git ")) {
22487
+ flush2();
22488
+ currentPath = null;
22489
+ currentIsCreate = false;
22490
+ currentIsDelete = false;
22491
+ const match2 = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
22492
+ if (match2) {
22493
+ currentPath = match2[2] ?? null;
22496
22494
  }
22497
- if (!found) {
22498
- i2++;
22499
- continue;
22495
+ } else if (currentPath !== null) {
22496
+ if (line.startsWith("new file mode ")) {
22497
+ currentIsCreate = true;
22498
+ } else if (line.startsWith("deleted file mode ")) {
22499
+ currentIsDelete = true;
22500
22500
  }
22501
22501
  }
22502
- i2++;
22503
22502
  }
22504
- return ranges;
22503
+ flush2();
22504
+ return results;
22505
22505
  }
22506
- function stripConflictMarkers(content) {
22507
- const lines = content.split(/\r?\n/);
22508
- const ranges = findConflictRanges(lines);
22509
- if (ranges.length === 0) {
22510
- return content;
22506
+ async function capturePatchSnapshot(git, files, treeish) {
22507
+ const snapshot = {};
22508
+ for (const file of files) {
22509
+ if (isBinaryFile(file)) continue;
22510
+ const content = await git.showFile(treeish, file).catch(() => null);
22511
+ if (content != null) snapshot[file] = content;
22511
22512
  }
22512
- const result = [];
22513
- let rangeIdx = 0;
22514
- for (let i2 = 0; i2 < lines.length; i2++) {
22515
- if (rangeIdx < ranges.length) {
22516
- const range2 = ranges[rangeIdx];
22517
- if (i2 === range2.start) {
22518
- continue;
22519
- }
22520
- if (i2 > range2.start && i2 < range2.separator) {
22521
- result.push(lines[i2]);
22522
- continue;
22523
- }
22524
- if (i2 === range2.separator) {
22525
- continue;
22526
- }
22527
- if (i2 > range2.separator && i2 < range2.end) {
22528
- continue;
22529
- }
22530
- if (i2 === range2.end) {
22531
- rangeIdx++;
22532
- continue;
22533
- }
22534
- }
22535
- result.push(lines[i2]);
22536
- }
22537
- return result.join("\n");
22513
+ return snapshot;
22514
+ }
22515
+ function filterInfrastructureAndSensitiveFiles(rawFilesOutput) {
22516
+ const allFiles = rawFilesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
22517
+ const sensitiveFiles = allFiles.filter((f) => isSensitiveFile(f));
22518
+ const files = allFiles.filter((f) => !isSensitiveFile(f));
22519
+ return { files, sensitiveFiles };
22538
22520
  }
22539
22521
  function threeWayMerge(base, ours, theirs) {
22540
22522
  const baseLines = base.split("\n");
@@ -22672,9 +22654,331 @@ function applyBothPatches(baseLines, oursPatches, theirsPatches) {
22672
22654
  }
22673
22655
  return result;
22674
22656
  }
22675
- function isBinaryFile(filePath) {
22676
- const ext2 = (0, import_path9.extname)(filePath).toLowerCase();
22677
- return BINARY_EXTENSIONS.has(ext2);
22657
+ async function resolveInputs(args) {
22658
+ const {
22659
+ patch,
22660
+ filePath,
22661
+ git,
22662
+ lockManager,
22663
+ outputDir,
22664
+ isTreeReachable,
22665
+ resolveBaseGeneration,
22666
+ resolveFilePath,
22667
+ extractRenameSource,
22668
+ extractFileDiff: extractFileDiff2
22669
+ } = args;
22670
+ const baseGen = await resolveBaseGeneration(patch.base_generation);
22671
+ if (!baseGen) {
22672
+ return {
22673
+ resolvedPath: filePath,
22674
+ metadata: {
22675
+ patchId: patch.id,
22676
+ patchMessage: patch.original_message,
22677
+ baseGeneration: patch.base_generation,
22678
+ currentGeneration: lockManager.read().current_generation
22679
+ },
22680
+ base: null,
22681
+ ours: null,
22682
+ oursPath: (0, import_path11.join)(outputDir, filePath),
22683
+ renameSourcePath: void 0,
22684
+ ghostTheirs: null,
22685
+ earlyExit: { status: "skipped", reason: "base-generation-not-found" }
22686
+ };
22687
+ }
22688
+ const lock = lockManager.read();
22689
+ const currentGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
22690
+ const currentTreeHash = currentGen?.tree_hash ?? baseGen.tree_hash;
22691
+ const resolvedPath = await resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
22692
+ const metadata = {
22693
+ patchId: patch.id,
22694
+ patchMessage: patch.original_message,
22695
+ baseGeneration: patch.base_generation,
22696
+ currentGeneration: lock.current_generation
22697
+ };
22698
+ let base = await git.showFile(baseGen.tree_hash, filePath);
22699
+ let renameSourcePath;
22700
+ if (!base) {
22701
+ const renameSource = extractRenameSource(patch.patch_content, filePath);
22702
+ if (renameSource) {
22703
+ base = await git.showFile(baseGen.tree_hash, renameSource);
22704
+ renameSourcePath = renameSource;
22705
+ }
22706
+ }
22707
+ const oursPath = (0, import_path11.join)(outputDir, resolvedPath);
22708
+ const ours = await (0, import_promises5.readFile)(oursPath, "utf-8").catch(() => null);
22709
+ let ghostTheirs = null;
22710
+ if (!base && ours && !renameSourcePath) {
22711
+ const treeReachable = await isTreeReachable(baseGen.tree_hash);
22712
+ if (!treeReachable) {
22713
+ const fileDiff = extractFileDiff2(patch.patch_content, filePath);
22714
+ if (fileDiff) {
22715
+ const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
22716
+ const result = reconstructFromGhostPatch2(fileDiff, ours);
22717
+ if (result) {
22718
+ base = result.base;
22719
+ ghostTheirs = result.theirs;
22720
+ }
22721
+ }
22722
+ }
22723
+ }
22724
+ return {
22725
+ resolvedPath,
22726
+ metadata,
22727
+ base,
22728
+ ours,
22729
+ oursPath,
22730
+ renameSourcePath,
22731
+ ghostTheirs
22732
+ };
22733
+ }
22734
+ function trimCR(line) {
22735
+ return line.endsWith("\r") ? line.slice(0, -1) : line;
22736
+ }
22737
+ function findConflictRanges(lines) {
22738
+ const ranges = [];
22739
+ let i2 = 0;
22740
+ while (i2 < lines.length) {
22741
+ if (trimCR(lines[i2]) === CONFLICT_OPENER) {
22742
+ let separatorIdx = -1;
22743
+ let j2 = i2 + 1;
22744
+ let found = false;
22745
+ while (j2 < lines.length) {
22746
+ const trimmed2 = trimCR(lines[j2]);
22747
+ if (trimmed2 === CONFLICT_OPENER) {
22748
+ break;
22749
+ }
22750
+ if (separatorIdx === -1 && trimmed2 === CONFLICT_SEPARATOR) {
22751
+ separatorIdx = j2;
22752
+ } else if (separatorIdx !== -1 && trimmed2 === CONFLICT_CLOSER) {
22753
+ ranges.push({ start: i2, separator: separatorIdx, end: j2 });
22754
+ i2 = j2;
22755
+ found = true;
22756
+ break;
22757
+ }
22758
+ j2++;
22759
+ }
22760
+ if (!found) {
22761
+ i2++;
22762
+ continue;
22763
+ }
22764
+ }
22765
+ i2++;
22766
+ }
22767
+ return ranges;
22768
+ }
22769
+ function stripConflictMarkers(content) {
22770
+ const lines = content.split(/\r?\n/);
22771
+ const ranges = findConflictRanges(lines);
22772
+ if (ranges.length === 0) {
22773
+ return content;
22774
+ }
22775
+ const result = [];
22776
+ let rangeIdx = 0;
22777
+ for (let i2 = 0; i2 < lines.length; i2++) {
22778
+ if (rangeIdx < ranges.length) {
22779
+ const range2 = ranges[rangeIdx];
22780
+ if (i2 === range2.start) {
22781
+ continue;
22782
+ }
22783
+ if (i2 > range2.start && i2 < range2.separator) {
22784
+ result.push(lines[i2]);
22785
+ continue;
22786
+ }
22787
+ if (i2 === range2.separator) {
22788
+ continue;
22789
+ }
22790
+ if (i2 > range2.separator && i2 < range2.end) {
22791
+ continue;
22792
+ }
22793
+ if (i2 === range2.end) {
22794
+ rangeIdx++;
22795
+ continue;
22796
+ }
22797
+ }
22798
+ result.push(lines[i2]);
22799
+ }
22800
+ return result.join("\n");
22801
+ }
22802
+ function hasConflictMarkerStrings(content) {
22803
+ if (content == null) return false;
22804
+ return content.includes(CONFLICT_OPENER) || content.includes(CONFLICT_CLOSER);
22805
+ }
22806
+ async function buildMergePlan(args) {
22807
+ const {
22808
+ inputs,
22809
+ patch,
22810
+ filePath,
22811
+ accumulator,
22812
+ tempGit,
22813
+ tempDir,
22814
+ applyPatchToContent: applyPatchToContent2,
22815
+ isPatchAlreadyApplied
22816
+ } = args;
22817
+ const { base, ours, resolvedPath, renameSourcePath, ghostTheirs } = inputs;
22818
+ let theirs = ghostTheirs;
22819
+ const ghostReconstructed = ghostTheirs != null;
22820
+ const snapshotForFile = patch.theirs_snapshot?.[resolvedPath] ?? patch.theirs_snapshot?.[filePath];
22821
+ const fileIsShared = accumulator.isShared(resolvedPath) || accumulator.isShared(filePath);
22822
+ const useSnapshotAsPrimary = !fileIsShared && snapshotForFile != null;
22823
+ let primarySource = ghostReconstructed ? "ghost" : "reconstructed";
22824
+ if (!ghostReconstructed) {
22825
+ if (useSnapshotAsPrimary) {
22826
+ theirs = snapshotForFile;
22827
+ primarySource = "snapshot";
22828
+ } else {
22829
+ theirs = await applyPatchToContent2(
22830
+ base,
22831
+ patch.patch_content,
22832
+ filePath,
22833
+ tempGit,
22834
+ tempDir,
22835
+ renameSourcePath
22836
+ );
22837
+ const reconstructionHasMarkers = hasConflictMarkerStrings(theirs);
22838
+ const baseHadMarkers = hasConflictMarkerStrings(base);
22839
+ if (reconstructionHasMarkers && !baseHadMarkers && snapshotForFile != null) {
22840
+ theirs = snapshotForFile;
22841
+ }
22842
+ }
22843
+ }
22844
+ const accumulatorEntry = accumulator.lookup(resolvedPath);
22845
+ if (theirs) {
22846
+ const theirsHasMarkers = hasConflictMarkerStrings(theirs);
22847
+ const baseHasMarkers = hasConflictMarkerStrings(base);
22848
+ if (theirsHasMarkers && !baseHasMarkers) {
22849
+ if (accumulatorEntry) {
22850
+ theirs = null;
22851
+ } else {
22852
+ return { kind: "skipped", reason: "stale-conflict-markers" };
22853
+ }
22854
+ }
22855
+ }
22856
+ let useAccumulatorAsMergeBase = false;
22857
+ if (accumulatorEntry && (theirs == null || base == null)) {
22858
+ theirs = await applyPatchToContent2(
22859
+ accumulatorEntry.content,
22860
+ patch.patch_content,
22861
+ filePath,
22862
+ tempGit,
22863
+ tempDir
22864
+ );
22865
+ if (theirs != null) {
22866
+ useAccumulatorAsMergeBase = true;
22867
+ } else if (await isPatchAlreadyApplied(
22868
+ patch.patch_content,
22869
+ filePath,
22870
+ accumulatorEntry.content,
22871
+ tempGit,
22872
+ tempDir
22873
+ )) {
22874
+ theirs = accumulatorEntry.content;
22875
+ useAccumulatorAsMergeBase = true;
22876
+ }
22877
+ }
22878
+ if (theirs) {
22879
+ const theirsHasMarkers = hasConflictMarkerStrings(theirs);
22880
+ const accBaseHasMarkers = accumulatorEntry != null && hasConflictMarkerStrings(accumulatorEntry.content);
22881
+ if (theirsHasMarkers && !accBaseHasMarkers && !hasConflictMarkerStrings(base)) {
22882
+ return { kind: "skipped", reason: "stale-conflict-markers" };
22883
+ }
22884
+ }
22885
+ let effective_theirs = theirs;
22886
+ let conflictReasonHint;
22887
+ if (theirs != null && base != null && !useAccumulatorAsMergeBase) {
22888
+ if (accumulatorEntry && accumulatorEntry.baseGeneration === patch.base_generation) {
22889
+ try {
22890
+ const preMerged = threeWayMerge(base, accumulatorEntry.content, theirs);
22891
+ if (!preMerged.hasConflicts) {
22892
+ effective_theirs = preMerged.content;
22893
+ } else {
22894
+ effective_theirs = theirs;
22895
+ }
22896
+ } catch {
22897
+ effective_theirs = theirs;
22898
+ }
22899
+ } else if (accumulatorEntry) {
22900
+ conflictReasonHint = "base-generation-mismatch";
22901
+ }
22902
+ }
22903
+ if (base == null && ours == null && effective_theirs != null) {
22904
+ return { kind: "new-file-only-user", theirs: effective_theirs };
22905
+ }
22906
+ if (base == null && ours != null && effective_theirs != null && !useAccumulatorAsMergeBase) {
22907
+ return { kind: "new-file-both", theirs: effective_theirs };
22908
+ }
22909
+ if (effective_theirs == null) {
22910
+ return { kind: "skipped", reason: "missing-content" };
22911
+ }
22912
+ if (base == null && !useAccumulatorAsMergeBase || ours == null) {
22913
+ return { kind: "skipped", reason: "missing-content" };
22914
+ }
22915
+ if (useAccumulatorAsMergeBase) {
22916
+ const mergeBase = accumulatorEntry?.content ?? base;
22917
+ if (mergeBase == null) {
22918
+ return { kind: "skipped", reason: "missing-content" };
22919
+ }
22920
+ return {
22921
+ kind: "accumulator-base",
22922
+ theirs: effective_theirs,
22923
+ mergeBase,
22924
+ conflictReasonHint
22925
+ };
22926
+ }
22927
+ return {
22928
+ kind: primarySource,
22929
+ base,
22930
+ theirs: effective_theirs,
22931
+ mergeBase: base,
22932
+ conflictReasonHint
22933
+ };
22934
+ }
22935
+ async function runMergeAndRecord(args) {
22936
+ const { plan, inputs, patch, accumulator } = args;
22937
+ const { resolvedPath, metadata, oursPath, ours } = inputs;
22938
+ if (plan.kind === "skipped") {
22939
+ return { file: resolvedPath, status: "skipped", reason: plan.reason };
22940
+ }
22941
+ if (plan.kind === "new-file-only-user") {
22942
+ accumulator.recordMerge(resolvedPath, plan.theirs, patch.base_generation);
22943
+ await (0, import_promises6.mkdir)((0, import_path12.dirname)(oursPath), { recursive: true });
22944
+ await (0, import_promises6.writeFile)(oursPath, plan.theirs);
22945
+ return { file: resolvedPath, status: "merged", reason: "new-file" };
22946
+ }
22947
+ if (plan.kind === "new-file-both") {
22948
+ const merged2 = threeWayMerge("", ours, plan.theirs);
22949
+ await (0, import_promises6.mkdir)((0, import_path12.dirname)(oursPath), { recursive: true });
22950
+ await (0, import_promises6.writeFile)(oursPath, merged2.content);
22951
+ if (merged2.hasConflicts) {
22952
+ return {
22953
+ file: resolvedPath,
22954
+ status: "conflict",
22955
+ conflicts: merged2.conflicts,
22956
+ conflictReason: "new-file-both",
22957
+ conflictMetadata: metadata
22958
+ };
22959
+ }
22960
+ accumulator.recordMerge(resolvedPath, merged2.content, patch.base_generation);
22961
+ return { file: resolvedPath, status: "merged" };
22962
+ }
22963
+ const merged = threeWayMerge(plan.mergeBase, ours, plan.theirs);
22964
+ await (0, import_promises6.mkdir)((0, import_path12.dirname)(oursPath), { recursive: true });
22965
+ await (0, import_promises6.writeFile)(oursPath, merged.content);
22966
+ if (merged.hasConflicts) {
22967
+ accumulator.recordConflict(resolvedPath, plan.theirs, patch.base_generation);
22968
+ return {
22969
+ file: resolvedPath,
22970
+ status: "conflict",
22971
+ conflicts: merged.conflicts,
22972
+ conflictReason: plan.conflictReasonHint ?? "same-line-edit",
22973
+ conflictMetadata: metadata
22974
+ };
22975
+ }
22976
+ accumulator.recordMerge(resolvedPath, plan.theirs, patch.base_generation);
22977
+ return {
22978
+ file: resolvedPath,
22979
+ status: "merged",
22980
+ ...merged.droppedContextLines && merged.droppedContextLines.length > 0 ? { droppedContextLines: merged.droppedContextLines } : {}
22981
+ };
22678
22982
  }
22679
22983
  function computeDroppedContextLinesForFile(base, theirs, finalContent) {
22680
22984
  const baseLines = base.split("\n");
@@ -22704,37 +23008,6 @@ function isDiffLineForFile(diffLine, filePath) {
22704
23008
  const match2 = diffLine.match(/^diff --git a\/.+ b\/(.+)$/);
22705
23009
  return match2 !== null && match2[1] === filePath;
22706
23010
  }
22707
- function parsePatchFileHeaders(patchContent) {
22708
- const results = [];
22709
- let currentPath = null;
22710
- let currentIsCreate = false;
22711
- let currentIsDelete = false;
22712
- const flush2 = () => {
22713
- if (currentPath !== null) {
22714
- results.push({ path: currentPath, isCreate: currentIsCreate, isDelete: currentIsDelete });
22715
- }
22716
- };
22717
- for (const line of patchContent.split("\n")) {
22718
- if (line.startsWith("diff --git ")) {
22719
- flush2();
22720
- currentPath = null;
22721
- currentIsCreate = false;
22722
- currentIsDelete = false;
22723
- const match2 = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
22724
- if (match2) {
22725
- currentPath = match2[2] ?? null;
22726
- }
22727
- } else if (currentPath !== null) {
22728
- if (line.startsWith("new file mode ")) {
22729
- currentIsCreate = true;
22730
- } else if (line.startsWith("deleted file mode ")) {
22731
- currentIsDelete = true;
22732
- }
22733
- }
22734
- }
22735
- flush2();
22736
- return results;
22737
- }
22738
23011
  function normalizeHunkBody(hunk) {
22739
23012
  let contextC = 0;
22740
23013
  let removeC = 0;
@@ -22939,17 +23212,17 @@ function overlayRegions(currentGenLines, locInCurrentGen, workingTreeLines, locI
22939
23212
  }
22940
23213
  async function unifiedDiff(beforeContent, afterContent, filePath) {
22941
23214
  if (beforeContent === afterContent) return "";
22942
- const tmp4 = await (0, import_promises6.mkdtemp)((0, import_path11.join)((0, import_os3.tmpdir)(), "fern-replay-pp-"));
23215
+ const tmp4 = await (0, import_promises8.mkdtemp)((0, import_path14.join)((0, import_os3.tmpdir)(), "fern-replay-pp-"));
22943
23216
  try {
22944
- const beforePath = (0, import_path11.join)(tmp4, "before");
22945
- const afterPath = (0, import_path11.join)(tmp4, "after");
22946
- await (0, import_promises6.writeFile)(beforePath, beforeContent, "utf-8");
22947
- await (0, import_promises6.writeFile)(afterPath, afterContent, "utf-8");
23217
+ const beforePath = (0, import_path14.join)(tmp4, "before");
23218
+ const afterPath = (0, import_path14.join)(tmp4, "after");
23219
+ await (0, import_promises8.writeFile)(beforePath, beforeContent, "utf-8");
23220
+ await (0, import_promises8.writeFile)(afterPath, afterContent, "utf-8");
22948
23221
  const raw = await runGitDiffNoIndex(beforePath, afterPath);
22949
23222
  if (!raw.trim()) return "";
22950
23223
  return rewriteDiffHeaders(raw, filePath);
22951
23224
  } finally {
22952
- await (0, import_promises6.rm)(tmp4, { recursive: true, force: true });
23225
+ await (0, import_promises8.rm)(tmp4, { recursive: true, force: true });
22953
23226
  }
22954
23227
  }
22955
23228
  function runGitDiffNoIndex(beforePath, afterPath) {
@@ -23020,6 +23293,37 @@ function synthesizeDeleteFileDiff(file, content) {
23020
23293
  const trailer = hasTrailingNewline ? "" : "\n\";
23021
23294
  return header + "\n" + body + trailer + "\n";
23022
23295
  }
23296
+ function fileIsCreationInPatchContent(patchContent, file) {
23297
+ const lines = patchContent.split("\n");
23298
+ let inFileSection = false;
23299
+ for (const line of lines) {
23300
+ if (line.startsWith("diff --git ")) {
23301
+ inFileSection = line.includes(` b/${file}`) || line.endsWith(` b/${file}`);
23302
+ continue;
23303
+ }
23304
+ if (inFileSection && line.startsWith("--- ")) {
23305
+ return line === "--- /dev/null";
23306
+ }
23307
+ }
23308
+ return false;
23309
+ }
23310
+ function firstGenerationReport() {
23311
+ return {
23312
+ flow: "first-generation",
23313
+ patchesDetected: 0,
23314
+ patchesApplied: 0,
23315
+ patchesWithConflicts: 0,
23316
+ patchesSkipped: 0,
23317
+ conflicts: []
23318
+ };
23319
+ }
23320
+ function assertActive(prep) {
23321
+ if (prep._prepared.kind !== "active") {
23322
+ throw new Error(
23323
+ `Flow.apply() called with terminal preparation (kind=${prep._prepared.kind}); terminal cases must be handled by applyPreparedReplay's dispatcher`
23324
+ );
23325
+ }
23326
+ }
23023
23327
  function computeCommitBuckets(results, absorbedPatchIds, patchesPassedToCommit) {
23024
23328
  const resultByPatchId = /* @__PURE__ */ new Map();
23025
23329
  for (const r2 of results) resultByPatchId.set(r2.patch.id, r2);
@@ -23211,7 +23515,7 @@ function parseGitLog(log) {
23211
23515
  });
23212
23516
  }
23213
23517
  function ensureFernignoreEntries(outputDir) {
23214
- const fernignorePath = (0, import_path13.join)(outputDir, ".fernignore");
23518
+ const fernignorePath = (0, import_path16.join)(outputDir, ".fernignore");
23215
23519
  let content = "";
23216
23520
  if ((0, import_fs9.existsSync)(fernignorePath)) {
23217
23521
  content = (0, import_fs9.readFileSync)(fernignorePath, "utf-8");
@@ -23234,7 +23538,7 @@ function ensureFernignoreEntries(outputDir) {
23234
23538
  return true;
23235
23539
  }
23236
23540
  function ensureGitattributesEntries(outputDir) {
23237
- const gitattributesPath = (0, import_path13.join)(outputDir, ".gitattributes");
23541
+ const gitattributesPath = (0, import_path16.join)(outputDir, ".gitattributes");
23238
23542
  let content = "";
23239
23543
  if ((0, import_fs9.existsSync)(gitattributesPath)) {
23240
23544
  content = (0, import_fs9.readFileSync)(gitattributesPath, "utf-8");
@@ -23462,7 +23766,7 @@ async function resolve7(outputDir, options) {
23462
23766
  const result = await computePerPatchDiffWithFallback(
23463
23767
  patch,
23464
23768
  (f) => git.showFile(currentGen, f),
23465
- (f) => (0, import_promises7.readFile)((0, import_path14.join)(outputDir, f), "utf-8").catch(() => null),
23769
+ (f) => (0, import_promises9.readFile)((0, import_path17.join)(outputDir, f), "utf-8").catch(() => null),
23466
23770
  (files) => git.exec(["diff", currentGen, "--", ...files]).catch(() => null)
23467
23771
  );
23468
23772
  if (result === null) {
@@ -23518,7 +23822,7 @@ async function resolve7(outputDir, options) {
23518
23822
  const theirsSnapshot = {};
23519
23823
  for (const file of filesForSnapshot) {
23520
23824
  if (isBinaryFile(file)) continue;
23521
- const content = await (0, import_promises7.readFile)((0, import_path14.join)(outputDir, file), "utf-8").catch(() => null);
23825
+ const content = await (0, import_promises9.readFile)((0, import_path17.join)(outputDir, file), "utf-8").catch(() => null);
23522
23826
  if (content != null) {
23523
23827
  theirsSnapshot[file] = content;
23524
23828
  }
@@ -23628,9 +23932,9 @@ function status(outputDir) {
23628
23932
  excludePatterns
23629
23933
  };
23630
23934
  }
23631
- var import_promises3, import_os, import_path7, import_fs6, import_path8, import_yaml, import_crypto, import_promises4, import_os2, import_path9, import_fs7, import_promises5, import_path10, import_promises6, import_os3, import_path11, import_child_process3, import_crypto2, import_fs8, import_path12, import_yaml2, import_crypto3, import_fs9, import_path13, import_fs10, import_promises7, import_path14, __defProp3, __getOwnPropNames3, __esm3, __export3, GitClient_exports, GitClient, init_GitClient, HybridReconstruction_exports, init_HybridReconstruction, PatchApplyTheirs_exports, init_PatchApplyTheirs, CREDENTIAL_PATTERNS, SENSITIVE_FILE_PATTERNS, FERN_BOT_NAME, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_SUPPORT_NAMES, LOCKFILE_HEADER, LockfileNotFoundError, LockfileManager, CONFLICT_OPENER, CONFLICT_SEPARATOR, CONFLICT_CLOSER, BINARY_EXTENSIONS, ReplayApplicator, INFRASTRUCTURE_FILES, ReplayDetector, ReplayCommitter, ReplayService, FernignoreMigrator, REPLAY_FERNIGNORE_ENTRIES, GITATTRIBUTES_ENTRIES, EMPTY_RESULT;
23935
+ var import_promises3, import_os, import_path7, import_fs6, import_path8, import_yaml, import_crypto, import_path9, import_promises4, import_os2, import_path10, import_promises5, import_path11, import_promises6, import_path12, import_fs7, import_promises7, import_path13, import_promises8, import_os3, import_path14, import_child_process3, import_crypto2, import_fs8, import_path15, import_yaml2, import_crypto3, import_fs9, import_path16, import_fs10, import_promises9, import_path17, __defProp3, __getOwnPropNames3, __esm3, __export3, GitClient_exports, GitClient, init_GitClient, HybridReconstruction_exports, init_HybridReconstruction, PatchApplyTheirs_exports, init_PatchApplyTheirs, CREDENTIAL_PATTERNS, SENSITIVE_FILE_PATTERNS, FERN_BOT_NAME, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_SUPPORT_NAMES, LOCKFILE_HEADER, LockfileNotFoundError, LockfileManager, BINARY_EXTENSIONS, INFRASTRUCTURE_FILES, ReplayDetector, MergeAccumulator, CONFLICT_OPENER, CONFLICT_SEPARATOR, CONFLICT_CLOSER, ReplayApplicator, ReplayCommitter, FileOwnership, FirstGenerationFlow, SkipApplicationFlow, NoPatchesFlow, NormalRegenerationFlow, ReplayService, FernignoreMigrator, REPLAY_FERNIGNORE_ENTRIES, GITATTRIBUTES_ENTRIES, EMPTY_RESULT;
23632
23936
  var init_dist3 = __esm({
23633
- "../../node_modules/.pnpm/@fern-api+replay@0.15.0/node_modules/@fern-api/replay/dist/index.js"() {
23937
+ "../../node_modules/.pnpm/@fern-api+replay@0.15.2/node_modules/@fern-api/replay/dist/index.js"() {
23634
23938
  "use strict";
23635
23939
  init_esm4();
23636
23940
  import_promises3 = require("fs/promises");
@@ -23640,31 +23944,37 @@ var init_dist3 = __esm({
23640
23944
  import_path8 = require("path");
23641
23945
  import_yaml = __toESM(require_dist3(), 1);
23642
23946
  import_crypto = require("crypto");
23643
- import_promises4 = require("fs/promises");
23644
- import_os2 = require("os");
23645
23947
  import_path9 = require("path");
23646
- init_esm3();
23647
23948
  init_diff3();
23648
- import_fs7 = require("fs");
23649
- import_promises5 = require("fs/promises");
23949
+ import_promises4 = require("fs/promises");
23950
+ import_os2 = require("os");
23650
23951
  import_path10 = require("path");
23651
23952
  init_esm3();
23953
+ import_promises5 = require("fs/promises");
23954
+ import_path11 = require("path");
23652
23955
  import_promises6 = require("fs/promises");
23956
+ import_path12 = require("path");
23957
+ import_fs7 = require("fs");
23958
+ import_promises7 = require("fs/promises");
23959
+ import_path13 = require("path");
23960
+ init_esm3();
23961
+ import_promises8 = require("fs/promises");
23653
23962
  import_os3 = require("os");
23654
- import_path11 = require("path");
23963
+ import_path14 = require("path");
23655
23964
  import_child_process3 = require("child_process");
23965
+ init_esm3();
23656
23966
  import_crypto2 = require("crypto");
23657
23967
  import_fs8 = require("fs");
23658
- import_path12 = require("path");
23968
+ import_path15 = require("path");
23659
23969
  init_esm3();
23660
23970
  import_yaml2 = __toESM(require_dist3(), 1);
23661
23971
  import_crypto3 = require("crypto");
23662
23972
  import_fs9 = require("fs");
23663
- import_path13 = require("path");
23973
+ import_path16 = require("path");
23664
23974
  init_esm3();
23665
23975
  import_fs10 = require("fs");
23666
- import_promises7 = require("fs/promises");
23667
- import_path14 = require("path");
23976
+ import_promises9 = require("fs/promises");
23977
+ import_path17 = require("path");
23668
23978
  __defProp3 = Object.defineProperty;
23669
23979
  __getOwnPropNames3 = Object.getOwnPropertyNames;
23670
23980
  __esm3 = (fn, res) => function __init() {
@@ -24044,9 +24354,6 @@ var init_dist3 = __esm({
24044
24354
  }
24045
24355
  }
24046
24356
  };
24047
- CONFLICT_OPENER = "<<<<<<< Generated";
24048
- CONFLICT_SEPARATOR = "=======";
24049
- CONFLICT_CLOSER = ">>>>>>> Your customization";
24050
24357
  BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
24051
24358
  ".png",
24052
24359
  ".jpg",
@@ -24096,696 +24403,6 @@ var init_dist3 = __esm({
24096
24403
  ".pyo",
24097
24404
  ".DS_Store"
24098
24405
  ]);
24099
- ReplayApplicator = class {
24100
- git;
24101
- lockManager;
24102
- outputDir;
24103
- renameCache = /* @__PURE__ */ new Map();
24104
- treeExistsCache = /* @__PURE__ */ new Map();
24105
- fileTheirsAccumulator = /* @__PURE__ */ new Map();
24106
- /**
24107
- * Apply mode for the current `applyPatches` invocation. Set at the start
24108
- * of `applyPatches`, read by `mergeFile` to decide:
24109
- * - whether to skip the intra-loop marker strip (kept in `applyPatches`)
24110
- * - whether to populate the accumulator after a conflicted merge
24111
- * (resolve mode populates so subsequent patches on the same file
24112
- * can use patch A's THEIRS as a structurally-correct merge base
24113
- * when their diff was authored against post-A structure)
24114
- * - whether to retry THEIRS reconstruction against the accumulator
24115
- * when the BASE-relative reconstruction produced markers from a
24116
- * cross-patch context mismatch
24117
- */
24118
- currentApplyMode = "replay";
24119
- /**
24120
- * Set of files that appear in 2+ patches in the current `applyPatches`
24121
- * invocation. Computed at the top of `applyPatches`. Read by `mergeFile`
24122
- * and `populateAccumulatorForPatch` to decide whether to use the patch's
24123
- * `theirs_snapshot` directly as THEIRS (snapshot-as-primary) or fall
24124
- * back to line-anchored reconstruction (FER-9525 cross-patch isolation).
24125
- *
24126
- * Snapshot-as-primary is correct when the patch owns its file (no other
24127
- * patch in this run touches it): the snapshot is what the customer
24128
- * intends for that file post-regen, regardless of generator structural
24129
- * changes that would invalidate the diff's line anchors.
24130
- *
24131
- * When a file IS shared, snapshots are CUMULATIVE across the patches
24132
- * touching it (each one's snapshot includes prior patches' contributions).
24133
- * Using a later patch's cumulative snapshot as its individual THEIRS
24134
- * would propagate prior patches' edits into the merge — surfacing nested
24135
- * conflicts at regions the patch alone never touched. Reconstruction
24136
- * via `applyPatchToContent` is required there.
24137
- */
24138
- sharedFiles = /* @__PURE__ */ new Set();
24139
- constructor(git, lockManager, outputDir) {
24140
- this.git = git;
24141
- this.lockManager = lockManager;
24142
- this.outputDir = outputDir;
24143
- }
24144
- /**
24145
- * Resolve the GenerationRecord for a patch's base_generation.
24146
- * Falls back to constructing an ad-hoc record from the commit's tree
24147
- * when base_generation isn't a tracked generation (commit-scan patches).
24148
- */
24149
- async resolveBaseGeneration(baseGeneration) {
24150
- const gen = this.lockManager.getGeneration(baseGeneration);
24151
- if (gen) return gen;
24152
- try {
24153
- const treeHash = await this.git.getTreeHash(baseGeneration);
24154
- return {
24155
- commit_sha: baseGeneration,
24156
- tree_hash: treeHash,
24157
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
24158
- cli_version: "unknown",
24159
- generator_versions: {}
24160
- };
24161
- } catch {
24162
- return void 0;
24163
- }
24164
- }
24165
- /** Reset inter-patch accumulator for a new cycle. */
24166
- resetAccumulator() {
24167
- this.fileTheirsAccumulator.clear();
24168
- }
24169
- /**
24170
- * @internal Test-only.
24171
- * Returns a defensive copy of the inter-patch accumulator. Used by the
24172
- * adversarial test suite (FER-9791) to assert invariant I2: after every
24173
- * applied patch, the accumulator has an entry for each file the patch
24174
- * touched, and the entry's content matches on-disk.
24175
- */
24176
- getAccumulatorSnapshot() {
24177
- const snapshot = /* @__PURE__ */ new Map();
24178
- for (const [path5, entry] of this.fileTheirsAccumulator) {
24179
- snapshot.set(path5, { content: entry.content, baseGeneration: entry.baseGeneration });
24180
- }
24181
- return snapshot;
24182
- }
24183
- /**
24184
- * Apply all patches, returning results for each.
24185
- * Skips patches that match exclude patterns in replay.yml
24186
- *
24187
- * `applyMode` controls the post-conflict marker strategy:
24188
- * - `"replay"` (default): strip conflict markers from disk between
24189
- * iterations whenever a later patch touches the same file. Lets the
24190
- * follow-up patch's `git apply --3way` see clean OURS content,
24191
- * which is what the silent-loss fix (PR #73) relies on. The replay
24192
- * pipeline calls `revertConflictingFiles` after the loop to clean
24193
- * any markers that survive.
24194
- * - `"resolve"`: keep markers on disk. The resolve command needs the
24195
- * customer to see them. Slow-path 3-way merge naturally preserves
24196
- * A's markers and B's clean writes when their regions don't overlap;
24197
- * when they do overlap, the customer gets nested markers and
24198
- * resolves manually. Either way they aren't silently dropped.
24199
- */
24200
- async applyPatches(patches, opts) {
24201
- const applyMode = opts?.applyMode ?? "replay";
24202
- this.currentApplyMode = applyMode;
24203
- this.resetAccumulator();
24204
- const fileFreq = /* @__PURE__ */ new Map();
24205
- for (const p2 of patches) {
24206
- for (const f of p2.files) {
24207
- fileFreq.set(f, (fileFreq.get(f) ?? 0) + 1);
24208
- }
24209
- }
24210
- this.sharedFiles = new Set(
24211
- Array.from(fileFreq.entries()).filter(([, n]) => n > 1).map(([f]) => f)
24212
- );
24213
- const results = [];
24214
- for (let i2 = 0; i2 < patches.length; i2++) {
24215
- const patch = patches[i2];
24216
- if (this.isExcluded(patch)) {
24217
- results.push({
24218
- patch,
24219
- status: "skipped",
24220
- method: "git-am"
24221
- });
24222
- continue;
24223
- }
24224
- const result = await this.applyPatchWithFallback(patch);
24225
- results.push(result);
24226
- if (applyMode !== "resolve" && result.status === "conflict" && result.fileResults) {
24227
- const laterFiles = /* @__PURE__ */ new Set();
24228
- for (let j2 = i2 + 1; j2 < patches.length; j2++) {
24229
- for (const f of patches[j2].files) {
24230
- laterFiles.add(f);
24231
- }
24232
- }
24233
- const resolvedToOriginal = /* @__PURE__ */ new Map();
24234
- if (result.resolvedFiles) {
24235
- for (const [orig, resolved] of Object.entries(result.resolvedFiles)) {
24236
- resolvedToOriginal.set(resolved, orig);
24237
- }
24238
- }
24239
- for (const fileResult of result.fileResults) {
24240
- if (fileResult.status !== "conflict") continue;
24241
- const originalPath = resolvedToOriginal.get(fileResult.file) ?? fileResult.file;
24242
- if (laterFiles.has(fileResult.file) || laterFiles.has(originalPath)) {
24243
- const filePath = (0, import_path9.join)(this.outputDir, fileResult.file);
24244
- try {
24245
- const content = await (0, import_promises4.readFile)(filePath, "utf-8");
24246
- const stripped = stripConflictMarkers(content);
24247
- await (0, import_promises4.writeFile)(filePath, stripped);
24248
- } catch {
24249
- }
24250
- }
24251
- }
24252
- }
24253
- }
24254
- return results;
24255
- }
24256
- /**
24257
- * Populate accumulator after git apply succeeds, AND collect per-file
24258
- * results including any "dropped context lines" — lines that were
24259
- * present in BASE and THEIRS (unchanged context) but absent from the
24260
- * final on-disk state because OURS deleted them. This is the fast-path
24261
- * counterpart to ThreeWayMerge.computeDroppedContextLines used by the
24262
- * 3-way slow path; both must surface the same warning.
24263
- */
24264
- async populateAccumulatorForPatch(patch, baseGen, currentTreeHash) {
24265
- const fileResults = [];
24266
- if (!baseGen) return fileResults;
24267
- const tempDir = await (0, import_promises4.mkdtemp)((0, import_path9.join)((0, import_os2.tmpdir)(), "replay-acc-"));
24268
- const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
24269
- const tempGit = new GitClient2(tempDir);
24270
- await tempGit.exec(["init"]);
24271
- await tempGit.exec(["config", "user.email", "replay@fern.com"]);
24272
- await tempGit.exec(["config", "user.name", "Fern Replay"]);
24273
- try {
24274
- for (const filePath of patch.files) {
24275
- if (isBinaryFile(filePath)) continue;
24276
- const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
24277
- const base = await this.git.showFile(baseGen.tree_hash, filePath);
24278
- const snapshotForFile = patch.theirs_snapshot?.[resolvedPath] ?? patch.theirs_snapshot?.[filePath];
24279
- const fileIsShared = this.sharedFiles.has(resolvedPath) || this.sharedFiles.has(filePath);
24280
- const theirs = !fileIsShared && snapshotForFile != null ? snapshotForFile : await this.applyPatchToContent(base, patch.patch_content, filePath, tempGit, tempDir);
24281
- const finalOnDisk = await (0, import_promises4.readFile)((0, import_path9.join)(this.outputDir, resolvedPath), "utf-8").catch(() => null);
24282
- const effectiveTheirs = theirs ?? finalOnDisk;
24283
- if (effectiveTheirs != null) {
24284
- this.fileTheirsAccumulator.set(resolvedPath, {
24285
- content: effectiveTheirs,
24286
- baseGeneration: patch.base_generation
24287
- });
24288
- }
24289
- if (base != null && effectiveTheirs != null && finalOnDisk != null) {
24290
- const dropped = computeDroppedContextLinesForFile(base, effectiveTheirs, finalOnDisk);
24291
- if (dropped.length > 0) {
24292
- fileResults.push({
24293
- file: resolvedPath,
24294
- status: "merged",
24295
- droppedContextLines: dropped
24296
- });
24297
- }
24298
- }
24299
- }
24300
- } finally {
24301
- await (0, import_promises4.rm)(tempDir, { recursive: true }).catch(() => {
24302
- });
24303
- }
24304
- return fileResults;
24305
- }
24306
- async applyPatchWithFallback(patch) {
24307
- const baseGen = await this.resolveBaseGeneration(patch.base_generation);
24308
- const lock = this.lockManager.read();
24309
- const currentGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
24310
- const currentTreeHash = currentGen?.tree_hash ?? baseGen?.tree_hash ?? "";
24311
- const needsAccumulation = await Promise.all(
24312
- patch.files.map(async (f) => {
24313
- if (!baseGen) return false;
24314
- const resolved = await this.resolveFilePath(f, baseGen.tree_hash, currentTreeHash);
24315
- return this.fileTheirsAccumulator.has(resolved);
24316
- })
24317
- ).then((results) => results.some(Boolean));
24318
- const resolvedFiles = {};
24319
- for (const filePath of patch.files) {
24320
- const resolvedPath = baseGen ? await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash) : filePath;
24321
- if (resolvedPath !== filePath) {
24322
- resolvedFiles[filePath] = resolvedPath;
24323
- }
24324
- }
24325
- const patchIsRenameAware = /^rename from /m.test(patch.patch_content);
24326
- const hasGeneratorRename = Object.keys(resolvedFiles).length > 0 && !patchIsRenameAware;
24327
- if (!needsAccumulation && !hasGeneratorRename) {
24328
- const snapshots = /* @__PURE__ */ new Map();
24329
- for (const filePath of patch.files) {
24330
- const fullPath = (0, import_path9.join)(this.outputDir, filePath);
24331
- snapshots.set(filePath, await (0, import_promises4.readFile)(fullPath, "utf-8").catch(() => null));
24332
- }
24333
- try {
24334
- await this.git.execWithInput(["apply", "--3way"], patch.patch_content);
24335
- const fastPathFileResults = await this.populateAccumulatorForPatch(patch, baseGen, currentTreeHash);
24336
- return {
24337
- patch,
24338
- status: "applied",
24339
- method: "git-am",
24340
- ...fastPathFileResults.length > 0 && { fileResults: fastPathFileResults },
24341
- ...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
24342
- };
24343
- } catch {
24344
- for (const [filePath, content] of snapshots) {
24345
- const fullPath = (0, import_path9.join)(this.outputDir, filePath);
24346
- if (content != null) {
24347
- await (0, import_promises4.writeFile)(fullPath, content);
24348
- } else {
24349
- await (0, import_promises4.unlink)(fullPath).catch(() => {
24350
- });
24351
- }
24352
- }
24353
- }
24354
- }
24355
- return this.applyWithThreeWayMerge(patch);
24356
- }
24357
- async applyWithThreeWayMerge(patch) {
24358
- const fileResults = [];
24359
- const resolvedFiles = {};
24360
- const tempDir = await (0, import_promises4.mkdtemp)((0, import_path9.join)((0, import_os2.tmpdir)(), "replay-"));
24361
- const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
24362
- const tempGit = new GitClient2(tempDir);
24363
- await tempGit.exec(["init"]);
24364
- await tempGit.exec(["config", "user.email", "replay@fern.com"]);
24365
- await tempGit.exec(["config", "user.name", "Fern Replay"]);
24366
- try {
24367
- for (const filePath of patch.files) {
24368
- if (isBinaryFile(filePath)) {
24369
- fileResults.push({
24370
- file: filePath,
24371
- status: "skipped",
24372
- reason: "binary-file"
24373
- });
24374
- continue;
24375
- }
24376
- const result = await this.mergeFile(patch, filePath, tempGit, tempDir);
24377
- if (result.file !== filePath) {
24378
- resolvedFiles[filePath] = result.file;
24379
- }
24380
- fileResults.push(result);
24381
- }
24382
- } finally {
24383
- await (0, import_promises4.rm)(tempDir, { recursive: true }).catch(() => {
24384
- });
24385
- }
24386
- const conflictFiles = fileResults.filter((r2) => r2.status === "conflict");
24387
- const hasConflicts = conflictFiles.length > 0;
24388
- const conflictReason = hasConflicts ? conflictFiles.some((f) => f.conflictReason === "base-generation-mismatch") ? "base-generation-mismatch" : conflictFiles[0]?.conflictReason : void 0;
24389
- return {
24390
- patch,
24391
- status: hasConflicts ? "conflict" : "applied",
24392
- method: "3way-merge",
24393
- fileResults,
24394
- conflictReason,
24395
- ...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
24396
- };
24397
- }
24398
- async mergeFile(patch, filePath, tempGit, tempDir) {
24399
- try {
24400
- const baseGen = await this.resolveBaseGeneration(patch.base_generation);
24401
- if (!baseGen) {
24402
- return { file: filePath, status: "skipped", reason: "base-generation-not-found" };
24403
- }
24404
- const lock = this.lockManager.read();
24405
- const currentGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
24406
- const currentTreeHash = currentGen?.tree_hash ?? baseGen.tree_hash;
24407
- const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
24408
- const metadata = {
24409
- patchId: patch.id,
24410
- patchMessage: patch.original_message,
24411
- baseGeneration: patch.base_generation,
24412
- currentGeneration: lock.current_generation
24413
- };
24414
- let base = await this.git.showFile(baseGen.tree_hash, filePath);
24415
- let renameSourcePath;
24416
- if (!base) {
24417
- const renameSource = this.extractRenameSource(patch.patch_content, filePath);
24418
- if (renameSource) {
24419
- base = await this.git.showFile(baseGen.tree_hash, renameSource);
24420
- renameSourcePath = renameSource;
24421
- }
24422
- }
24423
- const oursPath = (0, import_path9.join)(this.outputDir, resolvedPath);
24424
- const ours = await (0, import_promises4.readFile)(oursPath, "utf-8").catch(() => null);
24425
- let ghostReconstructed = false;
24426
- let theirs = null;
24427
- if (!base && ours && !renameSourcePath) {
24428
- const treeReachable = await this.isTreeReachable(baseGen.tree_hash);
24429
- if (!treeReachable) {
24430
- const fileDiff = this.extractFileDiff(patch.patch_content, filePath);
24431
- if (fileDiff) {
24432
- const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
24433
- const result = reconstructFromGhostPatch2(fileDiff, ours);
24434
- if (result) {
24435
- base = result.base;
24436
- theirs = result.theirs;
24437
- ghostReconstructed = true;
24438
- }
24439
- }
24440
- }
24441
- }
24442
- const snapshotForFile = patch.theirs_snapshot?.[resolvedPath] ?? patch.theirs_snapshot?.[filePath];
24443
- const fileIsShared = this.sharedFiles.has(resolvedPath) || this.sharedFiles.has(filePath);
24444
- const useSnapshotAsPrimary = !fileIsShared && snapshotForFile != null;
24445
- if (!ghostReconstructed) {
24446
- if (useSnapshotAsPrimary) {
24447
- theirs = snapshotForFile;
24448
- } else {
24449
- theirs = await this.applyPatchToContent(
24450
- base,
24451
- patch.patch_content,
24452
- filePath,
24453
- tempGit,
24454
- tempDir,
24455
- renameSourcePath
24456
- );
24457
- const reconstructionHasMarkers = theirs != null && (theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization"));
24458
- const baseHadMarkers = base != null && (base.includes("<<<<<<< Generated") || base.includes(">>>>>>> Your customization"));
24459
- if (reconstructionHasMarkers && !baseHadMarkers && snapshotForFile != null) {
24460
- theirs = snapshotForFile;
24461
- }
24462
- }
24463
- }
24464
- const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
24465
- if (theirs) {
24466
- const theirsHasMarkers = theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization");
24467
- const baseHasMarkers = base != null && (base.includes("<<<<<<< Generated") || base.includes(">>>>>>> Your customization"));
24468
- if (theirsHasMarkers && !baseHasMarkers) {
24469
- if (accumulatorEntry) {
24470
- theirs = null;
24471
- } else {
24472
- return {
24473
- file: resolvedPath,
24474
- status: "skipped",
24475
- reason: "stale-conflict-markers"
24476
- };
24477
- }
24478
- }
24479
- }
24480
- let useAccumulatorAsMergeBase = false;
24481
- if (accumulatorEntry && (theirs == null || base == null)) {
24482
- theirs = await this.applyPatchToContent(
24483
- accumulatorEntry.content,
24484
- patch.patch_content,
24485
- filePath,
24486
- tempGit,
24487
- tempDir
24488
- );
24489
- if (theirs != null) {
24490
- useAccumulatorAsMergeBase = true;
24491
- } else if (await this.isPatchAlreadyApplied(
24492
- patch.patch_content,
24493
- filePath,
24494
- accumulatorEntry.content,
24495
- tempGit,
24496
- tempDir
24497
- )) {
24498
- theirs = accumulatorEntry.content;
24499
- useAccumulatorAsMergeBase = true;
24500
- }
24501
- }
24502
- if (theirs) {
24503
- const theirsHasMarkers = theirs.includes("<<<<<<< Generated") || theirs.includes(">>>>>>> Your customization");
24504
- const accBaseHasMarkers = accumulatorEntry != null && (accumulatorEntry.content.includes("<<<<<<< Generated") || accumulatorEntry.content.includes(">>>>>>> Your customization"));
24505
- if (theirsHasMarkers && !accBaseHasMarkers && !(base != null && (base.includes("<<<<<<< Generated") || base.includes(">>>>>>> Your customization")))) {
24506
- return {
24507
- file: resolvedPath,
24508
- status: "skipped",
24509
- reason: "stale-conflict-markers"
24510
- };
24511
- }
24512
- }
24513
- let effective_theirs = theirs;
24514
- let baseMismatchSkipped = false;
24515
- if (theirs != null && base != null && !useAccumulatorAsMergeBase) {
24516
- if (accumulatorEntry && accumulatorEntry.baseGeneration === patch.base_generation) {
24517
- try {
24518
- const preMerged = threeWayMerge(base, accumulatorEntry.content, theirs);
24519
- if (!preMerged.hasConflicts) {
24520
- effective_theirs = preMerged.content;
24521
- } else {
24522
- effective_theirs = theirs;
24523
- }
24524
- } catch {
24525
- effective_theirs = theirs;
24526
- }
24527
- } else if (accumulatorEntry) {
24528
- baseMismatchSkipped = true;
24529
- }
24530
- }
24531
- if (base == null && ours == null && effective_theirs != null) {
24532
- this.fileTheirsAccumulator.set(resolvedPath, {
24533
- content: effective_theirs,
24534
- baseGeneration: patch.base_generation
24535
- });
24536
- const outDir2 = (0, import_path9.dirname)(oursPath);
24537
- await (0, import_promises4.mkdir)(outDir2, { recursive: true });
24538
- await (0, import_promises4.writeFile)(oursPath, effective_theirs);
24539
- return { file: resolvedPath, status: "merged", reason: "new-file" };
24540
- }
24541
- if (base == null && ours != null && effective_theirs != null && !useAccumulatorAsMergeBase) {
24542
- const merged2 = threeWayMerge("", ours, effective_theirs);
24543
- const outDir2 = (0, import_path9.dirname)(oursPath);
24544
- await (0, import_promises4.mkdir)(outDir2, { recursive: true });
24545
- await (0, import_promises4.writeFile)(oursPath, merged2.content);
24546
- if (merged2.hasConflicts) {
24547
- return {
24548
- file: resolvedPath,
24549
- status: "conflict",
24550
- conflicts: merged2.conflicts,
24551
- conflictReason: "new-file-both",
24552
- conflictMetadata: metadata
24553
- };
24554
- }
24555
- this.fileTheirsAccumulator.set(resolvedPath, {
24556
- content: merged2.content,
24557
- baseGeneration: patch.base_generation
24558
- });
24559
- return { file: resolvedPath, status: "merged" };
24560
- }
24561
- if (effective_theirs == null) {
24562
- return {
24563
- file: resolvedPath,
24564
- status: "skipped",
24565
- reason: "missing-content"
24566
- };
24567
- }
24568
- if (base == null && !useAccumulatorAsMergeBase || ours == null) {
24569
- return {
24570
- file: resolvedPath,
24571
- status: "skipped",
24572
- reason: "missing-content"
24573
- };
24574
- }
24575
- const mergeBase = useAccumulatorAsMergeBase && accumulatorEntry ? accumulatorEntry.content : base;
24576
- if (mergeBase == null) {
24577
- return {
24578
- file: resolvedPath,
24579
- status: "skipped",
24580
- reason: "missing-content"
24581
- };
24582
- }
24583
- const merged = threeWayMerge(mergeBase, ours, effective_theirs);
24584
- const outDir = (0, import_path9.dirname)(oursPath);
24585
- await (0, import_promises4.mkdir)(outDir, { recursive: true });
24586
- await (0, import_promises4.writeFile)(oursPath, merged.content);
24587
- const populateAccumulator = effective_theirs != null && (!merged.hasConflicts || this.currentApplyMode === "resolve");
24588
- if (populateAccumulator) {
24589
- this.fileTheirsAccumulator.set(resolvedPath, {
24590
- content: effective_theirs,
24591
- baseGeneration: patch.base_generation
24592
- });
24593
- }
24594
- if (merged.hasConflicts) {
24595
- return {
24596
- file: resolvedPath,
24597
- status: "conflict",
24598
- conflicts: merged.conflicts,
24599
- conflictReason: baseMismatchSkipped ? "base-generation-mismatch" : "same-line-edit",
24600
- conflictMetadata: metadata
24601
- };
24602
- }
24603
- return {
24604
- file: resolvedPath,
24605
- status: "merged",
24606
- ...merged.droppedContextLines && merged.droppedContextLines.length > 0 ? { droppedContextLines: merged.droppedContextLines } : {}
24607
- };
24608
- } catch (error) {
24609
- return {
24610
- file: filePath,
24611
- status: "skipped",
24612
- reason: `error: ${error instanceof Error ? error.message : String(error)}`
24613
- };
24614
- }
24615
- }
24616
- async isTreeReachable(treeHash) {
24617
- let result = this.treeExistsCache.get(treeHash);
24618
- if (result === void 0) {
24619
- result = await this.git.treeExists(treeHash);
24620
- this.treeExistsCache.set(treeHash, result);
24621
- }
24622
- return result;
24623
- }
24624
- isExcluded(patch) {
24625
- const config = this.lockManager.getCustomizationsConfig();
24626
- if (!config.exclude) return false;
24627
- return patch.files.some((file) => config.exclude.some((pattern) => minimatch(file, pattern)));
24628
- }
24629
- async resolveFilePath(filePath, baseTreeHash, currentTreeHash) {
24630
- const config = this.lockManager.getCustomizationsConfig();
24631
- if (config.moves) {
24632
- for (const move of config.moves) {
24633
- if (minimatch(filePath, move.from) || filePath === move.from) {
24634
- if (filePath === move.from) {
24635
- return move.to;
24636
- }
24637
- const fromBase = move.from.replace(/\*\*.*$/, "");
24638
- const toBase = move.to.replace(/\*\*.*$/, "");
24639
- if (filePath.startsWith(fromBase)) {
24640
- return toBase + filePath.slice(fromBase.length);
24641
- }
24642
- }
24643
- }
24644
- }
24645
- const cacheKey = `${baseTreeHash}:${currentTreeHash}`;
24646
- let renames = this.renameCache.get(cacheKey);
24647
- if (!renames) {
24648
- renames = await this.git.detectRenames(baseTreeHash, currentTreeHash);
24649
- this.renameCache.set(cacheKey, renames);
24650
- }
24651
- const gitRename = renames.find((r2) => r2.from === filePath);
24652
- if (gitRename) {
24653
- return gitRename.to;
24654
- }
24655
- return filePath;
24656
- }
24657
- async applyPatchToContent(base, patchContent, filePath, tempGit, tempDir, sourceFilePath) {
24658
- if (!base) {
24659
- return this.extractNewFileFromPatch(patchContent, filePath);
24660
- }
24661
- const fileDiff = this.extractFileDiff(patchContent, filePath);
24662
- if (!fileDiff) return null;
24663
- try {
24664
- if (sourceFilePath) {
24665
- const tempSourcePath = (0, import_path9.join)(tempDir, sourceFilePath);
24666
- await (0, import_promises4.mkdir)((0, import_path9.dirname)(tempSourcePath), { recursive: true });
24667
- await (0, import_promises4.writeFile)(tempSourcePath, base);
24668
- await tempGit.exec(["add", sourceFilePath]);
24669
- await tempGit.exec([
24670
- "commit",
24671
- "-m",
24672
- `base for rename ${sourceFilePath} -> ${filePath}`,
24673
- "--allow-empty"
24674
- ]);
24675
- await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
24676
- const tempTargetPath = (0, import_path9.join)(tempDir, filePath);
24677
- return await (0, import_promises4.readFile)(tempTargetPath, "utf-8");
24678
- }
24679
- const tempFilePath = (0, import_path9.join)(tempDir, filePath);
24680
- await (0, import_promises4.mkdir)((0, import_path9.dirname)(tempFilePath), { recursive: true });
24681
- await (0, import_promises4.writeFile)(tempFilePath, base);
24682
- await tempGit.exec(["add", filePath]);
24683
- await tempGit.exec(["commit", "-m", `base for ${filePath}`, "--allow-empty"]);
24684
- await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
24685
- return await (0, import_promises4.readFile)(tempFilePath, "utf-8");
24686
- } catch {
24687
- return null;
24688
- }
24689
- }
24690
- /**
24691
- * Detects whether a patch's additions are already present in `content`.
24692
- * Uses `git apply --reverse --check` — if the reverse patch applies cleanly,
24693
- * the forward patch is effectively a no-op (its +lines are already there and
24694
- * its -lines are already gone). Used to distinguish "patch already applied"
24695
- * from "patch base mismatch" after a forward-apply fails.
24696
- */
24697
- async isPatchAlreadyApplied(patchContent, filePath, content, tempGit, tempDir) {
24698
- const fileDiff = this.extractFileDiff(patchContent, filePath);
24699
- if (!fileDiff) return false;
24700
- try {
24701
- const tempFilePath = (0, import_path9.join)(tempDir, filePath);
24702
- await (0, import_promises4.mkdir)((0, import_path9.dirname)(tempFilePath), { recursive: true });
24703
- await (0, import_promises4.writeFile)(tempFilePath, content);
24704
- await tempGit.exec(["add", filePath]);
24705
- await tempGit.exec(["commit", "-m", `check already-applied ${filePath}`, "--allow-empty"]);
24706
- await tempGit.execWithInput(["apply", "--reverse", "--check", "--allow-empty"], fileDiff);
24707
- return true;
24708
- } catch {
24709
- return false;
24710
- }
24711
- }
24712
- extractFileDiff(patchContent, filePath) {
24713
- const lines = patchContent.split("\n");
24714
- const diffLines = [];
24715
- let inTargetFile = false;
24716
- for (const line of lines) {
24717
- if (line.startsWith("diff --git")) {
24718
- if (inTargetFile) {
24719
- break;
24720
- }
24721
- if (isDiffLineForFile(line, filePath)) {
24722
- inTargetFile = true;
24723
- diffLines.push(line);
24724
- }
24725
- continue;
24726
- }
24727
- if (inTargetFile) {
24728
- diffLines.push(line);
24729
- }
24730
- }
24731
- return diffLines.length > 0 ? diffLines.join("\n") + "\n" : null;
24732
- }
24733
- extractRenameSource(patchContent, targetFilePath) {
24734
- const lines = patchContent.split("\n");
24735
- let inTargetFile = false;
24736
- for (const line of lines) {
24737
- if (line.startsWith("diff --git")) {
24738
- if (inTargetFile) break;
24739
- inTargetFile = isDiffLineForFile(line, targetFilePath);
24740
- continue;
24741
- }
24742
- if (!inTargetFile) continue;
24743
- if (line.startsWith("@@")) break;
24744
- if (line.startsWith("rename from ")) {
24745
- return line.slice("rename from ".length);
24746
- }
24747
- }
24748
- return null;
24749
- }
24750
- extractNewFileFromPatch(patchContent, filePath) {
24751
- const lines = patchContent.split("\n");
24752
- const addedLines = [];
24753
- let inTargetFile = false;
24754
- let inHunk = false;
24755
- let isNewFile = false;
24756
- let noTrailingNewline = false;
24757
- for (const line of lines) {
24758
- if (line.startsWith("diff --git")) {
24759
- if (inTargetFile) break;
24760
- inTargetFile = isDiffLineForFile(line, filePath);
24761
- inHunk = false;
24762
- isNewFile = false;
24763
- continue;
24764
- }
24765
- if (!inTargetFile) continue;
24766
- if (!inHunk) {
24767
- if (line === "--- /dev/null" || line.startsWith("new file mode")) {
24768
- isNewFile = true;
24769
- }
24770
- }
24771
- if (line.startsWith("@@")) {
24772
- if (!isNewFile) return null;
24773
- inHunk = true;
24774
- continue;
24775
- }
24776
- if (!inHunk) continue;
24777
- if (line === "\") {
24778
- noTrailingNewline = true;
24779
- continue;
24780
- }
24781
- if (line.startsWith("+") && !line.startsWith("+++")) {
24782
- addedLines.push(line.slice(1));
24783
- }
24784
- }
24785
- if (addedLines.length === 0) return null;
24786
- return addedLines.join("\n") + (noTrailingNewline ? "" : "\n");
24787
- }
24788
- };
24789
24406
  INFRASTRUCTURE_FILES = /* @__PURE__ */ new Set([".fernignore"]);
24790
24407
  ReplayDetector = class {
24791
24408
  git;
@@ -24821,7 +24438,7 @@ var init_dist3 = __esm({
24821
24438
  return this.detectPatchesInRange(lastGen.commit_sha, lastGen, lock);
24822
24439
  }
24823
24440
  /**
24824
- * FER-10201 — Non-linear-history detection via `merge-base(prevGen, HEAD)`.
24441
+ * Non-linear-history detection via `merge-base(prevGen, HEAD)`.
24825
24442
  *
24826
24443
  * After a squash-merge of a Fern-bot PR, `lastGen.commit_sha` (the previous
24827
24444
  * `[fern-generated]`) is orphaned but still in git's object database. The
@@ -24853,7 +24470,7 @@ var init_dist3 = __esm({
24853
24470
  return this.detectPatchesInRange(mergeBase, lastGen, lock);
24854
24471
  }
24855
24472
  /**
24856
- * FER-10201 — Walk `${rangeStart}..HEAD`, classify each commit, emit one
24473
+ * Walk `${rangeStart}..HEAD`, classify each commit, emit one
24857
24474
  * `StoredPatch` per customer commit. Body shared by the linear path
24858
24475
  * (rangeStart = lastGen.commit_sha) and the merge-base path.
24859
24476
  *
@@ -24864,7 +24481,7 @@ var init_dist3 = __esm({
24864
24481
  async detectPatchesInRange(rangeStart, lastGen, lock) {
24865
24482
  const log = await this.git.exec([
24866
24483
  "log",
24867
- "--first-parent",
24484
+ "--no-merges",
24868
24485
  "--format=%H%x00%an%x00%ae%x00%s",
24869
24486
  `${rangeStart}..HEAD`,
24870
24487
  "--",
@@ -24884,18 +24501,6 @@ var init_dist3 = __esm({
24884
24501
  continue;
24885
24502
  }
24886
24503
  const parents = await this.git.getCommitParents(commit.sha);
24887
- if (parents.length > 1) {
24888
- const compositePatch = await this.createMergeCompositePatch({
24889
- mergeCommit: commit,
24890
- firstParent: parents[0],
24891
- lastGen,
24892
- lock
24893
- });
24894
- if (compositePatch) {
24895
- newPatches.push(compositePatch);
24896
- }
24897
- continue;
24898
- }
24899
24504
  let patchContent;
24900
24505
  try {
24901
24506
  patchContent = await this.git.formatPatch(commit.sha);
@@ -24910,9 +24515,7 @@ var init_dist3 = __esm({
24910
24515
  continue;
24911
24516
  }
24912
24517
  const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
24913
- const allFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
24914
- const sensitiveFiles = allFiles.filter((f) => isSensitiveFile(f));
24915
- const files = allFiles.filter((f) => !isSensitiveFile(f));
24518
+ const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
24916
24519
  if (sensitiveFiles.length > 0) {
24917
24520
  this.warnings.push(
24918
24521
  `Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
@@ -24933,12 +24536,7 @@ var init_dist3 = __esm({
24933
24536
  if (files.length === 0) {
24934
24537
  continue;
24935
24538
  }
24936
- const theirsSnapshot = {};
24937
- for (const file of files) {
24938
- if (isBinaryFile(file)) continue;
24939
- const content = await this.git.showFile(commit.sha, file).catch(() => null);
24940
- if (content != null) theirsSnapshot[file] = content;
24941
- }
24539
+ const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
24942
24540
  newPatches.push({
24943
24541
  id: `patch-${commit.sha.slice(0, 8)}`,
24944
24542
  content_hash: contentHash,
@@ -25026,7 +24624,7 @@ var init_dist3 = __esm({
25026
24624
  *
25027
24625
  * This ensures content hashes match across the no-patches and normal-
25028
24626
  * regeneration flows, which store formatPatch and git-diff formats
25029
- * respectively (FER-9850, D5-D9).
24627
+ * respectively.
25030
24628
  */
25031
24629
  computeContentHash(patchContent) {
25032
24630
  const lines = patchContent.split("\n");
@@ -25036,13 +24634,11 @@ var init_dist3 = __esm({
25036
24634
  return `sha256:${(0, import_crypto.createHash)("sha256").update(normalized).digest("hex")}`;
25037
24635
  }
25038
24636
  /**
25039
- * FER-9805 — Drop patches whose files were transiently created and then
25040
- * deleted within the current linear detection window, and are absent from
24637
+ * Drop patches whose files were transiently created and then deleted
24638
+ * within the current linear detection window, and are absent from
25041
24639
  * HEAD at the end of the window.
25042
24640
  *
25043
- * Rationale: on a merge commit, createMergeCompositePatch already diffs
25044
- * firstParent..mergeCommit so net-zero files never enter the composite. On
25045
- * linear history each commit becomes its own patch, so a file that was
24641
+ * Rationale: each commit becomes its own patch, so a file that was
25046
24642
  * briefly added and later removed survives as a create+delete pair whose
25047
24643
  * cumulative diff is empty. We drop such patches before they reach the
25048
24644
  * lockfile.
@@ -25062,8 +24658,8 @@ var init_dist3 = __esm({
25062
24658
  * single files from a multi-file patch would require a follow-up that
25063
24659
  * regenerates the patch content via `git format-patch -- <files>`. No
25064
24660
  * current failing test exercises this, and leaving the patch intact
25065
- * preserves today's behaviour. TODO(FER-9805): revisit if a future
25066
- * adversarial test demands per-file stripping.
24661
+ * preserves today's behaviour. Revisit if a future adversarial test
24662
+ * demands per-file stripping.
25067
24663
  */
25068
24664
  async collapseNetZeroFiles(patches) {
25069
24665
  if (patches.length === 0) return patches;
@@ -25110,77 +24706,33 @@ var init_dist3 = __esm({
25110
24706
  }
25111
24707
  }
25112
24708
  /**
25113
- * Create a single composite patch for a merge commit by diffing the merge
25114
- * commit against its first parent. This captures the net change of the
25115
- * entire merged branch as one patch, eliminating accumulator chaining and
25116
- * zero-net-change ghost conflicts.
25117
- */
25118
- async createMergeCompositePatch(input) {
25119
- const { mergeCommit, firstParent, lastGen, lock } = input;
25120
- const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
25121
- const filesOutput = await this.git.exec([
25122
- "diff",
25123
- "--name-only",
25124
- firstParent,
25125
- mergeCommit.sha
25126
- ]);
25127
- const allMergeFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
25128
- const sensitiveMergeFiles = allMergeFiles.filter((f) => isSensitiveFile(f));
25129
- const files = allMergeFiles.filter((f) => !isSensitiveFile(f));
25130
- if (sensitiveMergeFiles.length > 0) {
25131
- this.warnings.push(
25132
- `Sensitive file(s) excluded from merge patch for commit ${mergeCommit.sha.slice(0, 7)}: ${sensitiveMergeFiles.join(", ")}. These files will not be tracked by replay.`
25133
- );
25134
- }
25135
- if (files.length === 0) return null;
25136
- const diff = await this.git.exec([
25137
- "diff",
25138
- firstParent,
25139
- mergeCommit.sha,
25140
- "--",
25141
- ...files
25142
- ]);
25143
- if (!diff.trim()) return null;
25144
- const contentHash = this.computeContentHash(diff);
25145
- if (lock.patches.some((p2) => p2.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
25146
- return null;
25147
- }
25148
- const theirsSnapshot = {};
25149
- for (const file of files) {
25150
- if (isBinaryFile(file)) continue;
25151
- const content = await this.git.showFile(mergeCommit.sha, file).catch(() => null);
25152
- if (content != null) theirsSnapshot[file] = content;
25153
- }
25154
- return {
25155
- id: `patch-merge-${mergeCommit.sha.slice(0, 8)}`,
25156
- content_hash: contentHash,
25157
- original_commit: mergeCommit.sha,
25158
- original_message: mergeCommit.message,
25159
- original_author: `${mergeCommit.authorName} <${mergeCommit.authorEmail}>`,
25160
- base_generation: lastGen.commit_sha,
25161
- files,
25162
- patch_content: diff,
25163
- ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
25164
- ...this.isCreationOnlyPatch(diff) ? { user_owned: true } : {}
25165
- };
25166
- }
25167
- /**
25168
- * Detect patches via tree diff for non-linear history. Returns a composite patch.
25169
- * Revert reconciliation is skipped here because tree-diff produces a single composite
25170
- * patch from the aggregate diff — individual revert commits are not distinguishable.
24709
+ * Detect patches via tree diff for non-linear history. Returns a composite patch.
24710
+ * Revert reconciliation is skipped here because tree-diff produces a single composite
24711
+ * patch from the aggregate diff — individual revert commits are not distinguishable.
25171
24712
  */
25172
24713
  async detectPatchesViaTreeDiff(lastGen, commitKnownMissing) {
25173
24714
  const diffBase = await this.resolveDiffBase(lastGen, commitKnownMissing);
25174
24715
  if (!diffBase) {
25175
24716
  return this.detectPatchesViaCommitScan();
25176
24717
  }
24718
+ const lockForGuard = this.lockManager.read();
24719
+ const knownGenerations = new Set(lockForGuard.generations.map((g) => g.commit_sha));
24720
+ let resolvedBaseGeneration = commitKnownMissing ? diffBase : lastGen.commit_sha;
24721
+ if (commitKnownMissing && !knownGenerations.has(diffBase)) {
24722
+ const fallback = lockForGuard.current_generation ?? lockForGuard.generations[lockForGuard.generations.length - 1]?.commit_sha;
24723
+ if (!fallback) {
24724
+ this.warnings.push(
24725
+ `Composite-patch fallback skipped: diff base ${diffBase.slice(0, 7)} is not a recorded generation and the lockfile has no recorded generations to anchor on. Customer customizations on disk are preserved; tracked patches will resume on the next regeneration.`
24726
+ );
24727
+ return { patches: [], revertedPatchIds: [] };
24728
+ }
24729
+ resolvedBaseGeneration = fallback;
24730
+ }
25177
24731
  const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
25178
- const allTreeFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
25179
- const sensitiveTreeFiles = allTreeFiles.filter((f) => isSensitiveFile(f));
25180
- const files = allTreeFiles.filter((f) => !isSensitiveFile(f));
25181
- if (sensitiveTreeFiles.length > 0) {
24732
+ const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
24733
+ if (sensitiveFiles.length > 0) {
25182
24734
  this.warnings.push(
25183
- `Sensitive file(s) excluded from composite patch: ${sensitiveTreeFiles.join(", ")}. These files will not be tracked by replay.`
24735
+ `Sensitive file(s) excluded from composite patch: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
25184
24736
  );
25185
24737
  }
25186
24738
  if (files.length === 0) return { patches: [], revertedPatchIds: [] };
@@ -25192,21 +24744,19 @@ var init_dist3 = __esm({
25192
24744
  return { patches: [], revertedPatchIds: [] };
25193
24745
  }
25194
24746
  const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
25195
- const theirsSnapshot = {};
25196
- for (const file of files) {
25197
- if (isBinaryFile(file)) continue;
25198
- const content = await this.git.showFile("HEAD", file).catch(() => null);
25199
- if (content != null) theirsSnapshot[file] = content;
25200
- }
24747
+ const theirsSnapshot = await capturePatchSnapshot(this.git, files, "HEAD");
25201
24748
  const compositePatch = {
25202
24749
  id: `patch-composite-${headSha.slice(0, 8)}`,
25203
24750
  content_hash: contentHash,
25204
24751
  original_commit: headSha,
25205
24752
  original_message: "Customer customizations (composite)",
25206
24753
  original_author: "composite",
25207
- // Use diffBase when commit is unreachable — the applicator needs a reachable
25208
- // reference to find base file content. diffBase may be the tree_hash.
25209
- base_generation: commitKnownMissing ? diffBase : lastGen.commit_sha,
24754
+ // Anchor `base_generation` on a SHA the applicator's
24755
+ // `resolveBaseGeneration` can find — always a member of
24756
+ // `lock.generations`. When `diffBase` is a recorded
24757
+ // generation, use it directly; otherwise the guard above
24758
+ // has already mapped it onto `current_generation`.
24759
+ base_generation: resolvedBaseGeneration,
25210
24760
  files,
25211
24761
  patch_content: diff,
25212
24762
  ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
@@ -25224,6 +24774,8 @@ var init_dist3 = __esm({
25224
24774
  */
25225
24775
  async detectPatchesViaCommitScan() {
25226
24776
  const lock = this.lockManager.read();
24777
+ const knownGenerations = new Set(lock.generations.map((g) => g.commit_sha));
24778
+ const fallbackAnchor = lock.current_generation ?? lock.generations[lock.generations.length - 1]?.commit_sha;
25227
24779
  const log = await this.git.exec([
25228
24780
  "log",
25229
24781
  "--max-count=200",
@@ -25265,12 +24817,10 @@ var init_dist3 = __esm({
25265
24817
  continue;
25266
24818
  }
25267
24819
  const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
25268
- const allScanFiles = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f));
25269
- const sensitiveScanFiles = allScanFiles.filter((f) => isSensitiveFile(f));
25270
- const files = allScanFiles.filter((f) => !isSensitiveFile(f));
25271
- if (sensitiveScanFiles.length > 0) {
24820
+ const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
24821
+ if (sensitiveFiles.length > 0) {
25272
24822
  this.warnings.push(
25273
- `Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveScanFiles.join(", ")}. These files will not be tracked by replay.`
24823
+ `Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
25274
24824
  );
25275
24825
  if (files.length > 0) {
25276
24826
  if (parents.length === 0) {
@@ -25292,19 +24842,25 @@ var init_dist3 = __esm({
25292
24842
  continue;
25293
24843
  }
25294
24844
  const parentSha = parents[0];
25295
- const theirsSnapshot = {};
25296
- for (const file of files) {
25297
- if (isBinaryFile(file)) continue;
25298
- const content = await this.git.showFile(commit.sha, file).catch(() => null);
25299
- if (content != null) theirsSnapshot[file] = content;
24845
+ let baseGeneration;
24846
+ if (knownGenerations.has(parentSha)) {
24847
+ baseGeneration = parentSha;
24848
+ } else if (fallbackAnchor) {
24849
+ baseGeneration = fallbackAnchor;
24850
+ } else {
24851
+ this.warnings.push(
24852
+ `Skipping commit ${commit.sha.slice(0, 7)} (${commit.message.split("\n")[0]}): no recorded generation in the lockfile to anchor on. The customer's state on disk is preserved; tracked patches will resume on the next regeneration.`
24853
+ );
24854
+ continue;
25300
24855
  }
24856
+ const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
25301
24857
  newPatches.push({
25302
24858
  id: `patch-${commit.sha.slice(0, 8)}`,
25303
24859
  content_hash: contentHash,
25304
24860
  original_commit: commit.sha,
25305
24861
  original_message: commit.message,
25306
24862
  original_author: `${commit.authorName} <${commit.authorEmail}>`,
25307
- base_generation: parentSha,
24863
+ base_generation: baseGeneration,
25308
24864
  files,
25309
24865
  patch_content: patchContent,
25310
24866
  ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {}
@@ -25349,6 +24905,541 @@ var init_dist3 = __esm({
25349
24905
  return lock.generations.find((g) => g.commit_sha === lock.current_generation);
25350
24906
  }
25351
24907
  };
24908
+ MergeAccumulator = class {
24909
+ constructor(mode, sharedFiles) {
24910
+ this.mode = mode;
24911
+ this.sharedFiles = sharedFiles;
24912
+ }
24913
+ entries = /* @__PURE__ */ new Map();
24914
+ /** Was this file touched by 2+ patches in the current `applyPatches` invocation? */
24915
+ isShared(path5) {
24916
+ return this.sharedFiles.has(path5);
24917
+ }
24918
+ /** Has any prior patch already recorded a result for this path? */
24919
+ has(path5) {
24920
+ return this.entries.has(path5);
24921
+ }
24922
+ /** Look up the accumulated content + base generation for this path. */
24923
+ lookup(path5) {
24924
+ return this.entries.get(path5);
24925
+ }
24926
+ /**
24927
+ * True if any of the patch's files have a prior accumulator entry — in
24928
+ * which case the fast path (`git apply --3way`) cannot honor prior
24929
+ * patches' contributions and we must route through the slow path.
24930
+ */
24931
+ shouldSkipFastPath(patch) {
24932
+ return patch.files.some((f) => this.entries.has(f));
24933
+ }
24934
+ /** Record a successful (clean) merge result. */
24935
+ recordMerge(path5, content, baseGeneration) {
24936
+ this.entries.set(path5, { content, baseGeneration });
24937
+ }
24938
+ /**
24939
+ * Record a conflicted merge result. Populates only in `"resolve"` mode
24940
+ * — replay mode keeps the accumulator clean of marker-laden content
24941
+ * since markers will be stripped from disk after the apply loop.
24942
+ */
24943
+ recordConflict(path5, content, baseGeneration) {
24944
+ if (this.mode === "resolve") {
24945
+ this.entries.set(path5, { content, baseGeneration });
24946
+ }
24947
+ }
24948
+ /**
24949
+ * Defensive copy for test introspection. Used by invariant I2 to assert
24950
+ * that after every applied patch, the accumulator has an entry for each
24951
+ * file the patch touched (see `__tests__/invariants/i2-accumulator-correctness.ts`).
24952
+ *
24953
+ * @internal
24954
+ */
24955
+ snapshot() {
24956
+ const copy = /* @__PURE__ */ new Map();
24957
+ for (const [path5, entry] of this.entries) {
24958
+ copy.set(path5, { content: entry.content, baseGeneration: entry.baseGeneration });
24959
+ }
24960
+ return copy;
24961
+ }
24962
+ };
24963
+ CONFLICT_OPENER = "<<<<<<< Generated";
24964
+ CONFLICT_SEPARATOR = "=======";
24965
+ CONFLICT_CLOSER = ">>>>>>> Your customization";
24966
+ ReplayApplicator = class {
24967
+ git;
24968
+ lockManager;
24969
+ outputDir;
24970
+ renameCache = /* @__PURE__ */ new Map();
24971
+ treeExistsCache = /* @__PURE__ */ new Map();
24972
+ /**
24973
+ * Inter-patch THEIRS coordination. Created fresh in each `applyPatches`
24974
+ * invocation with the run's apply mode + precomputed shared-file set.
24975
+ * See `src/accumulator/MergeAccumulator.ts` for the full contract.
24976
+ *
24977
+ * Initialized to an empty default in the constructor so type narrowing
24978
+ * works for callers that read it before any `applyPatches` invocation
24979
+ * (e.g., the `getAccumulatorSnapshot` test accessor).
24980
+ */
24981
+ accumulator = new MergeAccumulator("replay", /* @__PURE__ */ new Set());
24982
+ constructor(git, lockManager, outputDir) {
24983
+ this.git = git;
24984
+ this.lockManager = lockManager;
24985
+ this.outputDir = outputDir;
24986
+ }
24987
+ /**
24988
+ * Resolve the GenerationRecord for a patch's base_generation.
24989
+ * Falls back to constructing an ad-hoc record from the commit's tree
24990
+ * when base_generation isn't a tracked generation (commit-scan patches).
24991
+ */
24992
+ async resolveBaseGeneration(baseGeneration) {
24993
+ const gen = this.lockManager.getGeneration(baseGeneration);
24994
+ if (gen) return gen;
24995
+ try {
24996
+ const treeHash = await this.git.getTreeHash(baseGeneration);
24997
+ return {
24998
+ commit_sha: baseGeneration,
24999
+ tree_hash: treeHash,
25000
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25001
+ cli_version: "unknown",
25002
+ generator_versions: {}
25003
+ };
25004
+ } catch {
25005
+ return void 0;
25006
+ }
25007
+ }
25008
+ /**
25009
+ * @internal Test-only.
25010
+ * Returns a defensive copy of the inter-patch accumulator. Used by
25011
+ * invariant I2 to assert that after every applied patch, the
25012
+ * accumulator has an entry for each file the patch touched, and the
25013
+ * entry's content matches on-disk.
25014
+ */
25015
+ getAccumulatorSnapshot() {
25016
+ return this.accumulator.snapshot();
25017
+ }
25018
+ /**
25019
+ * Apply all patches, returning results for each.
25020
+ * Skips patches that match exclude patterns in replay.yml
25021
+ *
25022
+ * `applyMode` controls the post-conflict marker strategy:
25023
+ * - `"replay"` (default): strip conflict markers from disk between
25024
+ * iterations whenever a later patch touches the same file. Lets the
25025
+ * follow-up patch's `git apply --3way` see clean OURS content,
25026
+ * which is what the silent-loss fix (PR #73) relies on. The replay
25027
+ * pipeline calls `revertConflictingFiles` after the loop to clean
25028
+ * any markers that survive.
25029
+ * - `"resolve"`: keep markers on disk. The resolve command needs the
25030
+ * customer to see them. Slow-path 3-way merge naturally preserves
25031
+ * A's markers and B's clean writes when their regions don't overlap;
25032
+ * when they do overlap, the customer gets nested markers and
25033
+ * resolves manually. Either way they aren't silently dropped.
25034
+ */
25035
+ async applyPatches(patches, opts) {
25036
+ const applyMode = opts?.applyMode ?? "replay";
25037
+ const fileFreq = /* @__PURE__ */ new Map();
25038
+ for (const p2 of patches) {
25039
+ for (const f of p2.files) {
25040
+ fileFreq.set(f, (fileFreq.get(f) ?? 0) + 1);
25041
+ }
25042
+ }
25043
+ const sharedFiles = new Set(
25044
+ Array.from(fileFreq.entries()).filter(([, n]) => n > 1).map(([f]) => f)
25045
+ );
25046
+ this.accumulator = new MergeAccumulator(applyMode, sharedFiles);
25047
+ const results = [];
25048
+ for (let i2 = 0; i2 < patches.length; i2++) {
25049
+ const patch = patches[i2];
25050
+ if (this.isExcluded(patch)) {
25051
+ results.push({
25052
+ patch,
25053
+ status: "skipped",
25054
+ method: "git-am"
25055
+ });
25056
+ continue;
25057
+ }
25058
+ const result = await this.applyPatchWithFallback(patch);
25059
+ results.push(result);
25060
+ if (applyMode !== "resolve" && result.status === "conflict" && result.fileResults) {
25061
+ const laterFiles = /* @__PURE__ */ new Set();
25062
+ for (let j2 = i2 + 1; j2 < patches.length; j2++) {
25063
+ for (const f of patches[j2].files) {
25064
+ laterFiles.add(f);
25065
+ }
25066
+ }
25067
+ const resolvedToOriginal = /* @__PURE__ */ new Map();
25068
+ if (result.resolvedFiles) {
25069
+ for (const [orig, resolved] of Object.entries(result.resolvedFiles)) {
25070
+ resolvedToOriginal.set(resolved, orig);
25071
+ }
25072
+ }
25073
+ for (const fileResult of result.fileResults) {
25074
+ if (fileResult.status !== "conflict") continue;
25075
+ const originalPath = resolvedToOriginal.get(fileResult.file) ?? fileResult.file;
25076
+ if (laterFiles.has(fileResult.file) || laterFiles.has(originalPath)) {
25077
+ const filePath = (0, import_path10.join)(this.outputDir, fileResult.file);
25078
+ try {
25079
+ const content = await (0, import_promises4.readFile)(filePath, "utf-8");
25080
+ const stripped = stripConflictMarkers(content);
25081
+ await (0, import_promises4.writeFile)(filePath, stripped);
25082
+ } catch {
25083
+ }
25084
+ }
25085
+ }
25086
+ }
25087
+ }
25088
+ return results;
25089
+ }
25090
+ /**
25091
+ * Populate accumulator after git apply succeeds, AND collect per-file
25092
+ * results including any "dropped context lines" — lines that were
25093
+ * present in BASE and THEIRS (unchanged context) but absent from the
25094
+ * final on-disk state because OURS deleted them. This is the fast-path
25095
+ * counterpart to ThreeWayMerge.computeDroppedContextLines used by the
25096
+ * 3-way slow path; both must surface the same warning.
25097
+ */
25098
+ async populateAccumulatorForPatch(patch, baseGen, currentTreeHash) {
25099
+ const fileResults = [];
25100
+ if (!baseGen) return fileResults;
25101
+ const tempDir = await (0, import_promises4.mkdtemp)((0, import_path10.join)((0, import_os2.tmpdir)(), "replay-acc-"));
25102
+ const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
25103
+ const tempGit = new GitClient2(tempDir);
25104
+ await tempGit.exec(["init"]);
25105
+ await tempGit.exec(["config", "user.email", "replay@fern.com"]);
25106
+ await tempGit.exec(["config", "user.name", "Fern Replay"]);
25107
+ try {
25108
+ for (const filePath of patch.files) {
25109
+ if (isBinaryFile(filePath)) continue;
25110
+ const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
25111
+ const base = await this.git.showFile(baseGen.tree_hash, filePath);
25112
+ const snapshotForFile = patch.theirs_snapshot?.[resolvedPath] ?? patch.theirs_snapshot?.[filePath];
25113
+ const fileIsShared = this.accumulator.isShared(resolvedPath) || this.accumulator.isShared(filePath);
25114
+ const theirs = !fileIsShared && snapshotForFile != null ? snapshotForFile : await this.applyPatchToContent(base, patch.patch_content, filePath, tempGit, tempDir);
25115
+ const finalOnDisk = await (0, import_promises4.readFile)((0, import_path10.join)(this.outputDir, resolvedPath), "utf-8").catch(() => null);
25116
+ const effectiveTheirs = theirs ?? finalOnDisk;
25117
+ if (effectiveTheirs != null) {
25118
+ this.accumulator.recordMerge(resolvedPath, effectiveTheirs, patch.base_generation);
25119
+ }
25120
+ if (base != null && effectiveTheirs != null && finalOnDisk != null) {
25121
+ const dropped = computeDroppedContextLinesForFile(base, effectiveTheirs, finalOnDisk);
25122
+ if (dropped.length > 0) {
25123
+ fileResults.push({
25124
+ file: resolvedPath,
25125
+ status: "merged",
25126
+ droppedContextLines: dropped
25127
+ });
25128
+ }
25129
+ }
25130
+ }
25131
+ } finally {
25132
+ await (0, import_promises4.rm)(tempDir, { recursive: true }).catch(() => {
25133
+ });
25134
+ }
25135
+ return fileResults;
25136
+ }
25137
+ async applyPatchWithFallback(patch) {
25138
+ const baseGen = await this.resolveBaseGeneration(patch.base_generation);
25139
+ const lock = this.lockManager.read();
25140
+ const currentGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
25141
+ const currentTreeHash = currentGen?.tree_hash ?? baseGen?.tree_hash ?? "";
25142
+ const needsAccumulation = await Promise.all(
25143
+ patch.files.map(async (f) => {
25144
+ if (!baseGen) return false;
25145
+ const resolved = await this.resolveFilePath(f, baseGen.tree_hash, currentTreeHash);
25146
+ return this.accumulator.has(resolved);
25147
+ })
25148
+ ).then((results) => results.some(Boolean));
25149
+ const resolvedFiles = {};
25150
+ for (const filePath of patch.files) {
25151
+ const resolvedPath = baseGen ? await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash) : filePath;
25152
+ if (resolvedPath !== filePath) {
25153
+ resolvedFiles[filePath] = resolvedPath;
25154
+ }
25155
+ }
25156
+ const patchIsRenameAware = /^rename from /m.test(patch.patch_content);
25157
+ const hasGeneratorRename = Object.keys(resolvedFiles).length > 0 && !patchIsRenameAware;
25158
+ if (!needsAccumulation && !hasGeneratorRename) {
25159
+ const snapshots = /* @__PURE__ */ new Map();
25160
+ for (const filePath of patch.files) {
25161
+ const fullPath = (0, import_path10.join)(this.outputDir, filePath);
25162
+ snapshots.set(filePath, await (0, import_promises4.readFile)(fullPath, "utf-8").catch(() => null));
25163
+ }
25164
+ try {
25165
+ await this.git.execWithInput(["apply", "--3way"], patch.patch_content);
25166
+ const fastPathFileResults = await this.populateAccumulatorForPatch(patch, baseGen, currentTreeHash);
25167
+ return {
25168
+ patch,
25169
+ status: "applied",
25170
+ method: "git-am",
25171
+ ...fastPathFileResults.length > 0 && { fileResults: fastPathFileResults },
25172
+ ...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
25173
+ };
25174
+ } catch {
25175
+ for (const [filePath, content] of snapshots) {
25176
+ const fullPath = (0, import_path10.join)(this.outputDir, filePath);
25177
+ if (content != null) {
25178
+ await (0, import_promises4.writeFile)(fullPath, content);
25179
+ } else {
25180
+ await (0, import_promises4.unlink)(fullPath).catch(() => {
25181
+ });
25182
+ }
25183
+ }
25184
+ }
25185
+ }
25186
+ return this.applyWithThreeWayMerge(patch);
25187
+ }
25188
+ async applyWithThreeWayMerge(patch) {
25189
+ const fileResults = [];
25190
+ const resolvedFiles = {};
25191
+ const tempDir = await (0, import_promises4.mkdtemp)((0, import_path10.join)((0, import_os2.tmpdir)(), "replay-"));
25192
+ const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
25193
+ const tempGit = new GitClient2(tempDir);
25194
+ await tempGit.exec(["init"]);
25195
+ await tempGit.exec(["config", "user.email", "replay@fern.com"]);
25196
+ await tempGit.exec(["config", "user.name", "Fern Replay"]);
25197
+ try {
25198
+ for (const filePath of patch.files) {
25199
+ if (isBinaryFile(filePath)) {
25200
+ fileResults.push({
25201
+ file: filePath,
25202
+ status: "skipped",
25203
+ reason: "binary-file"
25204
+ });
25205
+ continue;
25206
+ }
25207
+ const result = await this.mergeFile(patch, filePath, tempGit, tempDir);
25208
+ if (result.file !== filePath) {
25209
+ resolvedFiles[filePath] = result.file;
25210
+ }
25211
+ fileResults.push(result);
25212
+ }
25213
+ } finally {
25214
+ await (0, import_promises4.rm)(tempDir, { recursive: true }).catch(() => {
25215
+ });
25216
+ }
25217
+ const conflictFiles = fileResults.filter((r2) => r2.status === "conflict");
25218
+ const hasConflicts = conflictFiles.length > 0;
25219
+ const conflictReason = hasConflicts ? conflictFiles.some((f) => f.conflictReason === "base-generation-mismatch") ? "base-generation-mismatch" : conflictFiles[0]?.conflictReason : void 0;
25220
+ return {
25221
+ patch,
25222
+ status: hasConflicts ? "conflict" : "applied",
25223
+ method: "3way-merge",
25224
+ fileResults,
25225
+ conflictReason,
25226
+ ...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
25227
+ };
25228
+ }
25229
+ async mergeFile(patch, filePath, tempGit, tempDir) {
25230
+ try {
25231
+ const inputs = await resolveInputs({
25232
+ patch,
25233
+ filePath,
25234
+ git: this.git,
25235
+ lockManager: this.lockManager,
25236
+ outputDir: this.outputDir,
25237
+ isTreeReachable: (h2) => this.isTreeReachable(h2),
25238
+ resolveBaseGeneration: (s) => this.resolveBaseGeneration(s),
25239
+ resolveFilePath: (f, b3, c3) => this.resolveFilePath(f, b3, c3),
25240
+ extractRenameSource: (p2, f) => this.extractRenameSource(p2, f),
25241
+ extractFileDiff: (p2, f) => this.extractFileDiff(p2, f)
25242
+ });
25243
+ if (inputs.earlyExit) {
25244
+ return { file: filePath, ...inputs.earlyExit };
25245
+ }
25246
+ const plan = await buildMergePlan({
25247
+ inputs,
25248
+ patch,
25249
+ filePath,
25250
+ accumulator: this.accumulator,
25251
+ tempGit,
25252
+ tempDir,
25253
+ applyPatchToContent: (b3, p2, f, g, d, s) => this.applyPatchToContent(b3, p2, f, g, d, s),
25254
+ isPatchAlreadyApplied: (p2, f, c3, g, d) => this.isPatchAlreadyApplied(p2, f, c3, g, d)
25255
+ });
25256
+ return await runMergeAndRecord({
25257
+ plan,
25258
+ inputs,
25259
+ patch,
25260
+ accumulator: this.accumulator
25261
+ });
25262
+ } catch (error) {
25263
+ return {
25264
+ file: filePath,
25265
+ status: "skipped",
25266
+ reason: `error: ${error instanceof Error ? error.message : String(error)}`
25267
+ };
25268
+ }
25269
+ }
25270
+ async isTreeReachable(treeHash) {
25271
+ let result = this.treeExistsCache.get(treeHash);
25272
+ if (result === void 0) {
25273
+ result = await this.git.treeExists(treeHash);
25274
+ this.treeExistsCache.set(treeHash, result);
25275
+ }
25276
+ return result;
25277
+ }
25278
+ isExcluded(patch) {
25279
+ const config = this.lockManager.getCustomizationsConfig();
25280
+ if (!config.exclude) return false;
25281
+ return patch.files.some((file) => config.exclude.some((pattern) => minimatch(file, pattern)));
25282
+ }
25283
+ async resolveFilePath(filePath, baseTreeHash, currentTreeHash) {
25284
+ const config = this.lockManager.getCustomizationsConfig();
25285
+ if (config.moves) {
25286
+ for (const move of config.moves) {
25287
+ if (minimatch(filePath, move.from) || filePath === move.from) {
25288
+ if (filePath === move.from) {
25289
+ return move.to;
25290
+ }
25291
+ const fromBase = move.from.replace(/\*\*.*$/, "");
25292
+ const toBase = move.to.replace(/\*\*.*$/, "");
25293
+ if (filePath.startsWith(fromBase)) {
25294
+ return toBase + filePath.slice(fromBase.length);
25295
+ }
25296
+ }
25297
+ }
25298
+ }
25299
+ const cacheKey = `${baseTreeHash}:${currentTreeHash}`;
25300
+ let renames = this.renameCache.get(cacheKey);
25301
+ if (!renames) {
25302
+ renames = await this.git.detectRenames(baseTreeHash, currentTreeHash);
25303
+ this.renameCache.set(cacheKey, renames);
25304
+ }
25305
+ const gitRename = renames.find((r2) => r2.from === filePath);
25306
+ if (gitRename) {
25307
+ return gitRename.to;
25308
+ }
25309
+ return filePath;
25310
+ }
25311
+ async applyPatchToContent(base, patchContent, filePath, tempGit, tempDir, sourceFilePath) {
25312
+ if (!base) {
25313
+ return this.extractNewFileFromPatch(patchContent, filePath);
25314
+ }
25315
+ const fileDiff = this.extractFileDiff(patchContent, filePath);
25316
+ if (!fileDiff) return null;
25317
+ try {
25318
+ if (sourceFilePath) {
25319
+ const tempSourcePath = (0, import_path10.join)(tempDir, sourceFilePath);
25320
+ await (0, import_promises4.mkdir)((0, import_path10.dirname)(tempSourcePath), { recursive: true });
25321
+ await (0, import_promises4.writeFile)(tempSourcePath, base);
25322
+ await tempGit.exec(["add", sourceFilePath]);
25323
+ await tempGit.exec([
25324
+ "commit",
25325
+ "-m",
25326
+ `base for rename ${sourceFilePath} -> ${filePath}`,
25327
+ "--allow-empty"
25328
+ ]);
25329
+ await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
25330
+ const tempTargetPath = (0, import_path10.join)(tempDir, filePath);
25331
+ return await (0, import_promises4.readFile)(tempTargetPath, "utf-8");
25332
+ }
25333
+ const tempFilePath = (0, import_path10.join)(tempDir, filePath);
25334
+ await (0, import_promises4.mkdir)((0, import_path10.dirname)(tempFilePath), { recursive: true });
25335
+ await (0, import_promises4.writeFile)(tempFilePath, base);
25336
+ await tempGit.exec(["add", filePath]);
25337
+ await tempGit.exec(["commit", "-m", `base for ${filePath}`, "--allow-empty"]);
25338
+ await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
25339
+ return await (0, import_promises4.readFile)(tempFilePath, "utf-8");
25340
+ } catch {
25341
+ return null;
25342
+ }
25343
+ }
25344
+ /**
25345
+ * Detects whether a patch's additions are already present in `content`.
25346
+ * Uses `git apply --reverse --check` — if the reverse patch applies cleanly,
25347
+ * the forward patch is effectively a no-op (its +lines are already there and
25348
+ * its -lines are already gone). Used to distinguish "patch already applied"
25349
+ * from "patch base mismatch" after a forward-apply fails.
25350
+ */
25351
+ async isPatchAlreadyApplied(patchContent, filePath, content, tempGit, tempDir) {
25352
+ const fileDiff = this.extractFileDiff(patchContent, filePath);
25353
+ if (!fileDiff) return false;
25354
+ try {
25355
+ const tempFilePath = (0, import_path10.join)(tempDir, filePath);
25356
+ await (0, import_promises4.mkdir)((0, import_path10.dirname)(tempFilePath), { recursive: true });
25357
+ await (0, import_promises4.writeFile)(tempFilePath, content);
25358
+ await tempGit.exec(["add", filePath]);
25359
+ await tempGit.exec(["commit", "-m", `check already-applied ${filePath}`, "--allow-empty"]);
25360
+ await tempGit.execWithInput(["apply", "--reverse", "--check", "--allow-empty"], fileDiff);
25361
+ return true;
25362
+ } catch {
25363
+ return false;
25364
+ }
25365
+ }
25366
+ extractFileDiff(patchContent, filePath) {
25367
+ const lines = patchContent.split("\n");
25368
+ const diffLines = [];
25369
+ let inTargetFile = false;
25370
+ for (const line of lines) {
25371
+ if (line.startsWith("diff --git")) {
25372
+ if (inTargetFile) {
25373
+ break;
25374
+ }
25375
+ if (isDiffLineForFile(line, filePath)) {
25376
+ inTargetFile = true;
25377
+ diffLines.push(line);
25378
+ }
25379
+ continue;
25380
+ }
25381
+ if (inTargetFile) {
25382
+ diffLines.push(line);
25383
+ }
25384
+ }
25385
+ return diffLines.length > 0 ? diffLines.join("\n") + "\n" : null;
25386
+ }
25387
+ extractRenameSource(patchContent, targetFilePath) {
25388
+ const lines = patchContent.split("\n");
25389
+ let inTargetFile = false;
25390
+ for (const line of lines) {
25391
+ if (line.startsWith("diff --git")) {
25392
+ if (inTargetFile) break;
25393
+ inTargetFile = isDiffLineForFile(line, targetFilePath);
25394
+ continue;
25395
+ }
25396
+ if (!inTargetFile) continue;
25397
+ if (line.startsWith("@@")) break;
25398
+ if (line.startsWith("rename from ")) {
25399
+ return line.slice("rename from ".length);
25400
+ }
25401
+ }
25402
+ return null;
25403
+ }
25404
+ extractNewFileFromPatch(patchContent, filePath) {
25405
+ const lines = patchContent.split("\n");
25406
+ const addedLines = [];
25407
+ let inTargetFile = false;
25408
+ let inHunk = false;
25409
+ let isNewFile = false;
25410
+ let noTrailingNewline = false;
25411
+ for (const line of lines) {
25412
+ if (line.startsWith("diff --git")) {
25413
+ if (inTargetFile) break;
25414
+ inTargetFile = isDiffLineForFile(line, filePath);
25415
+ inHunk = false;
25416
+ isNewFile = false;
25417
+ continue;
25418
+ }
25419
+ if (!inTargetFile) continue;
25420
+ if (!inHunk) {
25421
+ if (line === "--- /dev/null" || line.startsWith("new file mode")) {
25422
+ isNewFile = true;
25423
+ }
25424
+ }
25425
+ if (line.startsWith("@@")) {
25426
+ if (!isNewFile) return null;
25427
+ inHunk = true;
25428
+ continue;
25429
+ }
25430
+ if (!inHunk) continue;
25431
+ if (line === "\") {
25432
+ noTrailingNewline = true;
25433
+ continue;
25434
+ }
25435
+ if (line.startsWith("+") && !line.startsWith("+++")) {
25436
+ addedLines.push(line.slice(1));
25437
+ }
25438
+ }
25439
+ if (addedLines.length === 0) return null;
25440
+ return addedLines.join("\n") + (noTrailingNewline ? "" : "\n");
25441
+ }
25442
+ };
25352
25443
  ReplayCommitter = class {
25353
25444
  git;
25354
25445
  outputDir;
@@ -25452,210 +25543,66 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25452
25543
  };
25453
25544
  init_GitClient();
25454
25545
  init_HybridReconstruction();
25455
- ReplayService = class {
25456
- git;
25457
- detector;
25458
- applicator;
25459
- committer;
25460
- lockManager;
25461
- outputDir;
25462
- /** @internal Test-only. Captures the last ReplayResult[] returned from applicator.applyPatches(). */
25463
- _lastApplyResults = [];
25464
- /**
25465
- * Service-level warnings accumulated during a single runReplay() call.
25466
- * Merged with `detector.warnings` into `ReplayReport.warnings` by each flow handler.
25467
- * Populated by `isFileUserOwned` when a patch's base generation tree is unreachable
25468
- * (shallow clone / aggressive `git gc --prune`) and we fall back to a conservative
25469
- * heuristic. Cleared at the top of `runReplay()`.
25470
- */
25471
- warnings = [];
25472
- /**
25473
- * @internal Test-only accessor.
25474
- * Returns the ReplayApplicator instance used for the last run. Tests use this
25475
- * to inspect the inter-patch accumulator after runReplay() completes (FER-9791, I2).
25476
- */
25477
- get applicatorRef() {
25478
- return this.applicator;
25479
- }
25480
- /**
25481
- * @internal Test-only accessor.
25482
- * Returns the ReplayResult[] from the most recent applyPatches() call. Tests use
25483
- * this to filter applied-vs-conflicted-vs-absorbed patches for invariant assertions
25484
- * (FER-9791). Returns an empty array if no patches have been applied yet.
25485
- */
25486
- getLastResults() {
25487
- return this._lastApplyResults;
25488
- }
25489
- constructor(outputDir, _config) {
25490
- const git = new GitClient(outputDir);
25546
+ FileOwnership = class {
25547
+ constructor(git) {
25491
25548
  this.git = git;
25492
- this.outputDir = outputDir;
25493
- this.lockManager = new LockfileManager(outputDir);
25494
- this.detector = new ReplayDetector(git, this.lockManager, outputDir);
25495
- this.applicator = new ReplayApplicator(git, this.lockManager, outputDir);
25496
- this.committer = new ReplayCommitter(git, outputDir);
25497
25549
  }
25498
25550
  /**
25499
- * Phase 1 of the two-phase replay flow. Runs preGenerationRebase (when applicable),
25500
- * detects new patches, and creates the `[fern-generated]` commit. Leaves HEAD at
25501
- * the generation commit (or unchanged for dry-run).
25502
- *
25503
- * Does NOT apply patches — the returned handle must be passed to
25504
- * `applyPreparedReplay()` to complete the run. No disk writes to the lockfile
25505
- * happen here; lockfile persistence is deferred to phase 2.
25551
+ * Canonical resolution. Returns true if `file` is user-owned per:
25506
25552
  *
25507
- * Callers may land additional commits on HEAD between `prepareReplay` and
25508
- * `applyPreparedReplay` (e.g., `[fern-autoversion]`).
25553
+ * 1. `patch.user_owned === true` — authoritative, persisted flag.
25554
+ * 2. `file === ".fernignore"` — always user-owned.
25555
+ * 3. File matches a `.fernignore` pattern (via minimatch).
25556
+ * 4. Patch content shows `--- /dev/null` for this file (legacy-safe
25557
+ * signal for patches predating `user_owned` or whose flag was lost).
25558
+ * 5. Tree-based check against `patch.base_generation` when reachable
25559
+ * AND distinct from `currentGenSha`. Unreachable base → conservative
25560
+ * user-owned (silent-absorption safety net), with a one-time warning
25561
+ * per base SHA.
25562
+ * 6. Final fallback: check `currentGenSha` when no usable base exists
25563
+ * (legacy one-gen lockfile).
25509
25564
  */
25510
- async prepareReplay(options) {
25511
- this.warnings = [];
25512
- this.detector.warnings.length = 0;
25513
- if (options?.skipApplication) {
25514
- return this._prepareSkipApplication(options);
25515
- }
25516
- const flow = this.determineFlow();
25517
- switch (flow) {
25518
- case "first-generation":
25519
- return this._prepareFirstGeneration(options);
25520
- case "no-patches":
25521
- return this._prepareNoPatchesRegeneration(options);
25522
- case "normal-regeneration":
25523
- return this._prepareNormalRegeneration(options);
25524
- }
25525
- }
25526
- /**
25527
- * Phase 2 of the two-phase replay flow. Applies patches, rebases them against
25528
- * the generation, saves the lockfile, and commits `[fern-replay]` (or stages
25529
- * under `stageOnly`). Operates on HEAD at call time — mid-phase commits are
25530
- * respected.
25531
- *
25532
- * For terminal handles (dry-run, first-gen has no patch work, skip-app does
25533
- * its own post-commit logic), this returns the precomputed report.
25534
- */
25535
- async applyPreparedReplay(prep, options) {
25536
- if (prep._prepared.terminal) {
25537
- return prep._prepared.preparedReport;
25538
- }
25539
- switch (prep._prepared.flow) {
25540
- case "first-generation":
25541
- return this._applyFirstGeneration(prep);
25542
- case "skip-application":
25543
- return this._applySkipApplication(prep, options);
25544
- case "no-patches":
25545
- return this._applyNoPatchesRegeneration(prep, options);
25546
- case "normal-regeneration":
25547
- return this._applyNormalRegeneration(prep, options);
25548
- }
25549
- }
25550
- /**
25551
- * Backwards-compatible atomic entry point. Composes `prepareReplay` + `applyPreparedReplay`.
25552
- * Behavior is byte-identical to the pre-split implementation for all existing callers.
25553
- */
25554
- async runReplay(options) {
25555
- const prep = await this.prepareReplay(options);
25556
- return this.applyPreparedReplay(prep, options);
25557
- }
25558
- /**
25559
- * Sync the lockfile after a divergent PR was squash-merged.
25560
- * Call this BEFORE runReplay() when the CLI detects a merged divergent PR.
25561
- *
25562
- * After updating the generation record, re-detects customer patches via
25563
- * tree diff between the generation tag (pure generation tree) and HEAD
25564
- * (which includes customer customizations after squash merge). This ensures
25565
- * patches survive the squash merge → regenerate cycle even when the lockfile
25566
- * was restored from the base branch during conflict PR creation.
25567
- */
25568
- async syncFromDivergentMerge(generationCommitSha, options) {
25569
- const treeHash = await this.git.getTreeHash(generationCommitSha);
25570
- const timestamp = (await this.git.exec(["log", "-1", "--format=%aI", generationCommitSha])).trim();
25571
- const record2 = {
25572
- commit_sha: generationCommitSha,
25573
- tree_hash: treeHash,
25574
- timestamp,
25575
- cli_version: options?.cliVersion ?? "unknown",
25576
- generator_versions: options?.generatorVersions ?? {},
25577
- base_branch_head: options?.baseBranchHead
25578
- };
25579
- let resolvedPatches;
25580
- if (!this.lockManager.exists()) {
25581
- this.lockManager.initializeInMemory(record2);
25582
- } else {
25583
- this.lockManager.read();
25584
- const unresolvedPatches = [
25585
- ...this.lockManager.getUnresolvedPatches(),
25586
- ...this.lockManager.getResolvingPatches()
25587
- ];
25588
- resolvedPatches = this.lockManager.getPatches().filter((p2) => p2.status == null);
25589
- this.lockManager.addGeneration(record2);
25590
- this.lockManager.clearPatches();
25591
- for (const patch of unresolvedPatches) {
25592
- this.lockManager.addPatch(patch);
25593
- }
25565
+ async resolveCanonical(ctx) {
25566
+ const { file, originalFileName, patch, currentGenSha, fernignorePatterns, warnedGens, warnings } = ctx;
25567
+ if (patch.user_owned) return true;
25568
+ if (file === ".fernignore") return true;
25569
+ if (fernignorePatterns.some((p2) => file === p2 || minimatch(file, p2))) return true;
25570
+ if (fileIsCreationInPatchContent(patch.patch_content, originalFileName ?? file)) {
25571
+ return true;
25594
25572
  }
25595
- try {
25596
- const { patches: redetectedPatches } = await this.detector.detectNewPatches();
25597
- if (redetectedPatches.length > 0) {
25598
- const redetectedFiles = new Set(redetectedPatches.flatMap((p2) => p2.files));
25599
- const currentPatches = this.lockManager.getPatches();
25600
- for (const patch of currentPatches) {
25601
- if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
25602
- this.lockManager.removePatch(patch.id);
25603
- }
25604
- }
25605
- for (const patch of redetectedPatches) {
25606
- this.lockManager.addPatch(patch);
25573
+ const base = patch.base_generation;
25574
+ if (base && base !== currentGenSha) {
25575
+ const reachable = await this.git.commitExists(base) || await this.git.treeExists(base);
25576
+ if (!reachable) {
25577
+ if (!warnedGens.has(base)) {
25578
+ warnedGens.add(base);
25579
+ warnings.push(
25580
+ `Base generation ${base.slice(0, 7)} is unreachable (shallow clone or pruned history). Treating affected files as user-owned to avoid silent data loss. Run \`git fetch --unshallow\` or avoid \`git gc --prune\` to restore full history.`
25581
+ );
25607
25582
  }
25583
+ return true;
25608
25584
  }
25609
- } catch (error) {
25610
- for (const patch of resolvedPatches ?? []) {
25611
- this.lockManager.addPatch(patch);
25612
- }
25613
- this.detector.warnings.push(
25614
- `Patch re-detection failed after divergent merge sync. ${(resolvedPatches ?? []).length} previously resolved patch(es) preserved. Error: ${error instanceof Error ? error.message : String(error)}`
25615
- );
25616
- }
25617
- this.lockManager.save();
25618
- this.detector.warnings.push(...this.lockManager.warnings);
25619
- }
25620
- determineFlow() {
25621
- try {
25622
- const lock = this.lockManager.read();
25623
- return lock.patches.length === 0 ? "no-patches" : "normal-regeneration";
25624
- } catch (error) {
25625
- if (error instanceof LockfileNotFoundError) {
25626
- return "first-generation";
25627
- }
25628
- throw error;
25585
+ return await this.git.showFile(base, originalFileName ?? file) === null;
25629
25586
  }
25587
+ return await this.git.showFile(currentGenSha, originalFileName ?? file) === null;
25630
25588
  }
25631
- firstGenerationReport() {
25632
- return {
25633
- flow: "first-generation",
25634
- patchesDetected: 0,
25635
- patchesApplied: 0,
25636
- patchesWithConflicts: 0,
25637
- patchesSkipped: 0,
25638
- conflicts: []
25639
- };
25589
+ };
25590
+ FirstGenerationFlow = class {
25591
+ constructor(service) {
25592
+ this.service = service;
25640
25593
  }
25641
- async _prepareFirstGeneration(options) {
25594
+ async prepare(options) {
25642
25595
  if (options?.dryRun) {
25643
- const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
25596
+ const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
25644
25597
  return {
25645
25598
  flow: "first-generation",
25646
25599
  previousGenerationSha: null,
25647
25600
  currentGenerationSha: headSha,
25648
25601
  baseBranchHead: options.baseBranchHead ?? null,
25649
25602
  _prepared: {
25603
+ kind: "terminal",
25650
25604
  flow: "first-generation",
25651
- terminal: true,
25652
- preparedReport: this.firstGenerationReport(),
25653
- patchesToApply: [],
25654
- newPatchIds: /* @__PURE__ */ new Set(),
25655
- detectorWarnings: [],
25656
- revertedCount: 0,
25657
- genSha: headSha,
25658
- optionsSnapshot: options
25605
+ preparedReport: firstGenerationReport()
25659
25606
  }
25660
25607
  };
25661
25608
  }
@@ -25664,17 +25611,17 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25664
25611
  generatorVersions: options.generatorVersions ?? {},
25665
25612
  baseBranchHead: options.baseBranchHead
25666
25613
  } : void 0;
25667
- await this.committer.commitGeneration("Initial SDK generation", commitOpts);
25668
- const genRecord = await this.committer.createGenerationRecord(commitOpts);
25669
- this.lockManager.initializeInMemory(genRecord);
25614
+ await this.service.committer.commitGeneration("Initial SDK generation", commitOpts);
25615
+ const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
25616
+ this.service.lockManager.initializeInMemory(genRecord);
25670
25617
  return {
25671
25618
  flow: "first-generation",
25672
25619
  previousGenerationSha: null,
25673
25620
  currentGenerationSha: genRecord.commit_sha,
25674
25621
  baseBranchHead: options?.baseBranchHead ?? null,
25675
25622
  _prepared: {
25623
+ kind: "active",
25676
25624
  flow: "first-generation",
25677
- terminal: false,
25678
25625
  patchesToApply: [],
25679
25626
  newPatchIds: /* @__PURE__ */ new Set(),
25680
25627
  detectorWarnings: [],
@@ -25684,47 +25631,45 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25684
25631
  }
25685
25632
  };
25686
25633
  }
25687
- async _applyFirstGeneration(_prep) {
25688
- this.lockManager.save();
25689
- return this.firstGenerationReport();
25634
+ async apply(_prep) {
25635
+ this.service.lockManager.save();
25636
+ return firstGenerationReport();
25690
25637
  }
25691
- /**
25692
- * Skip-application mode phase 1: commit the generation and update the
25693
- * in-memory lockfile, but don't detect or apply patches. Sets a marker
25694
- * so the next normal run skips revert detection in preGenerationRebase().
25695
- *
25696
- * Disk save is deferred to `_applySkipApplication`.
25697
- */
25698
- async _prepareSkipApplication(options) {
25638
+ };
25639
+ SkipApplicationFlow = class {
25640
+ constructor(service) {
25641
+ this.service = service;
25642
+ }
25643
+ async prepare(options) {
25699
25644
  const commitOpts = options ? {
25700
25645
  cliVersion: options.cliVersion ?? "unknown",
25701
25646
  generatorVersions: options.generatorVersions ?? {},
25702
25647
  baseBranchHead: options.baseBranchHead
25703
25648
  } : void 0;
25704
- await this.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
25705
- await this.cleanupStaleConflictMarkers();
25706
- const genRecord = await this.committer.createGenerationRecord(commitOpts);
25649
+ await this.service.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
25650
+ await this.service.cleanupStaleConflictMarkers();
25651
+ const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
25707
25652
  let previousGenerationSha = null;
25708
25653
  try {
25709
- const lock = this.lockManager.read();
25654
+ const lock = this.service.lockManager.read();
25710
25655
  previousGenerationSha = lock.current_generation ?? null;
25711
- this.lockManager.addGeneration(genRecord);
25656
+ this.service.lockManager.addGeneration(genRecord);
25712
25657
  } catch (error) {
25713
25658
  if (error instanceof LockfileNotFoundError) {
25714
- this.lockManager.initializeInMemory(genRecord);
25659
+ this.service.lockManager.initializeInMemory(genRecord);
25715
25660
  } else {
25716
25661
  throw error;
25717
25662
  }
25718
25663
  }
25719
- this.lockManager.setReplaySkippedAt((/* @__PURE__ */ new Date()).toISOString());
25664
+ this.service.lockManager.setReplaySkippedAt((/* @__PURE__ */ new Date()).toISOString());
25720
25665
  return {
25721
25666
  flow: "skip-application",
25722
25667
  previousGenerationSha,
25723
25668
  currentGenerationSha: genRecord.commit_sha,
25724
25669
  baseBranchHead: options?.baseBranchHead ?? null,
25725
25670
  _prepared: {
25671
+ kind: "active",
25726
25672
  flow: "skip-application",
25727
- terminal: false,
25728
25673
  patchesToApply: [],
25729
25674
  newPatchIds: /* @__PURE__ */ new Set(),
25730
25675
  detectorWarnings: [],
@@ -25734,13 +25679,13 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25734
25679
  }
25735
25680
  };
25736
25681
  }
25737
- async _applySkipApplication(_prep, options) {
25738
- this.lockManager.save();
25739
- const credentialWarnings = [...this.lockManager.warnings];
25682
+ async apply(_prep, options) {
25683
+ this.service.lockManager.save();
25684
+ const credentialWarnings = [...this.service.lockManager.warnings];
25740
25685
  if (!options?.stageOnly) {
25741
- await this.committer.commitReplay(0);
25686
+ await this.service.committer.commitReplay(0);
25742
25687
  } else {
25743
- await this.committer.stageAll();
25688
+ await this.service.committer.stageAll();
25744
25689
  }
25745
25690
  return {
25746
25691
  flow: "skip-application",
@@ -25752,13 +25697,18 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25752
25697
  warnings: credentialWarnings.length > 0 ? credentialWarnings : void 0
25753
25698
  };
25754
25699
  }
25755
- async _prepareNoPatchesRegeneration(options) {
25756
- const preLock = this.lockManager.read();
25700
+ };
25701
+ NoPatchesFlow = class {
25702
+ constructor(service) {
25703
+ this.service = service;
25704
+ }
25705
+ async prepare(options) {
25706
+ const preLock = this.service.lockManager.read();
25757
25707
  const previousGenerationSha = preLock.current_generation ?? null;
25758
- let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
25759
- const detectorWarnings = [...this.detector.warnings];
25708
+ let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
25709
+ const detectorWarnings = [...this.service.detector.warnings];
25760
25710
  if (options?.dryRun) {
25761
- const dryRunWarnings = [...detectorWarnings, ...this.warnings];
25711
+ const dryRunWarnings = [...detectorWarnings, ...this.service.warnings];
25762
25712
  const preparedReport = {
25763
25713
  flow: "no-patches",
25764
25714
  patchesDetected: newPatches.length,
@@ -25770,22 +25720,16 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25770
25720
  wouldApply: newPatches,
25771
25721
  warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0
25772
25722
  };
25773
- const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
25723
+ const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
25774
25724
  return {
25775
25725
  flow: "no-patches",
25776
25726
  previousGenerationSha,
25777
25727
  currentGenerationSha: headSha,
25778
25728
  baseBranchHead: options.baseBranchHead ?? null,
25779
25729
  _prepared: {
25730
+ kind: "terminal",
25780
25731
  flow: "no-patches",
25781
- terminal: true,
25782
- preparedReport,
25783
- patchesToApply: [],
25784
- newPatchIds: /* @__PURE__ */ new Set(),
25785
- detectorWarnings,
25786
- revertedCount: revertedPatchIds.length,
25787
- genSha: headSha,
25788
- optionsSnapshot: options
25732
+ preparedReport
25789
25733
  }
25790
25734
  };
25791
25735
  }
@@ -25794,7 +25738,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25794
25738
  const uniqueFiles = [...new Set(newPatches.flatMap((p2) => p2.files))];
25795
25739
  const absorbedFiles = /* @__PURE__ */ new Set();
25796
25740
  for (const file of uniqueFiles) {
25797
- const diff = await this.git.exec(["diff", preCurrentGen, "HEAD", "--", file]).catch(() => null);
25741
+ const diff = await this.service.git.exec(["diff", preCurrentGen, "HEAD", "--", file]).catch(() => null);
25798
25742
  if (diff !== null && !diff.trim()) {
25799
25743
  absorbedFiles.add(file);
25800
25744
  }
@@ -25807,7 +25751,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25807
25751
  }
25808
25752
  for (const id of revertedPatchIds) {
25809
25753
  try {
25810
- this.lockManager.removePatch(id);
25754
+ this.service.lockManager.removePatch(id);
25811
25755
  } catch {
25812
25756
  }
25813
25757
  }
@@ -25816,18 +25760,18 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25816
25760
  generatorVersions: options.generatorVersions ?? {},
25817
25761
  baseBranchHead: options.baseBranchHead
25818
25762
  } : void 0;
25819
- await this.committer.commitGeneration("Update SDK", commitOpts);
25820
- await this.cleanupStaleConflictMarkers();
25821
- const genRecord = await this.committer.createGenerationRecord(commitOpts);
25822
- this.lockManager.addGeneration(genRecord);
25763
+ await this.service.committer.commitGeneration("Update SDK", commitOpts);
25764
+ await this.service.cleanupStaleConflictMarkers();
25765
+ const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
25766
+ this.service.lockManager.addGeneration(genRecord);
25823
25767
  return {
25824
25768
  flow: "no-patches",
25825
25769
  previousGenerationSha,
25826
25770
  currentGenerationSha: genRecord.commit_sha,
25827
25771
  baseBranchHead: options?.baseBranchHead ?? null,
25828
25772
  _prepared: {
25773
+ kind: "active",
25829
25774
  flow: "no-patches",
25830
- terminal: false,
25831
25775
  patchesToApply: newPatches,
25832
25776
  newPatchIds: new Set(newPatches.map((p2) => p2.id)),
25833
25777
  detectorWarnings,
@@ -25837,41 +25781,42 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25837
25781
  }
25838
25782
  };
25839
25783
  }
25840
- async _applyNoPatchesRegeneration(prep, options) {
25784
+ async apply(prep, options) {
25785
+ assertActive(prep);
25841
25786
  const newPatches = prep._prepared.patchesToApply;
25842
25787
  const detectorWarnings = prep._prepared.detectorWarnings;
25843
25788
  const genSha = prep._prepared.genSha;
25844
25789
  let results = [];
25845
25790
  if (newPatches.length > 0) {
25846
- results = await this.applicator.applyPatches(newPatches);
25847
- this._lastApplyResults = results;
25848
- this.recordDroppedContextLineWarnings(results);
25849
- this.revertConflictingFiles(results);
25791
+ results = await this.service.applicator.applyPatches(newPatches);
25792
+ this.service._lastApplyResults = results;
25793
+ this.service.recordDroppedContextLineWarnings(results);
25794
+ this.service.revertConflictingFiles(results);
25850
25795
  for (const result of results) {
25851
25796
  if (result.status === "conflict") {
25852
25797
  result.patch.status = "unresolved";
25853
25798
  }
25854
25799
  }
25855
25800
  }
25856
- const rebaseCounts = await this.rebasePatches(results, genSha);
25801
+ const rebaseCounts = await this.service.rebasePatches(results, genSha);
25857
25802
  for (const patch of newPatches) {
25858
25803
  if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
25859
- this.lockManager.addPatch(patch);
25804
+ this.service.lockManager.addPatch(patch);
25860
25805
  }
25861
25806
  }
25862
- this.lockManager.save();
25863
- this.warnings.push(...this.lockManager.warnings);
25807
+ this.service.lockManager.save();
25808
+ this.service.warnings.push(...this.service.lockManager.warnings);
25864
25809
  if (newPatches.length > 0) {
25865
25810
  if (!options?.stageOnly) {
25866
25811
  const appliedCount = results.filter((r2) => r2.status === "applied").length;
25867
25812
  const buckets = computeCommitBuckets(results, rebaseCounts.absorbedPatchIds, newPatches);
25868
- await this.committer.commitReplay(appliedCount, newPatches, void 0, { buckets });
25813
+ await this.service.committer.commitReplay(appliedCount, newPatches, void 0, { buckets });
25869
25814
  } else {
25870
- await this.committer.stageAll();
25815
+ await this.service.committer.stageAll();
25871
25816
  }
25872
25817
  }
25873
- const warnings = [...detectorWarnings, ...this.warnings];
25874
- return this.buildReport(
25818
+ const warnings = [...detectorWarnings, ...this.service.warnings];
25819
+ return this.service.buildReport(
25875
25820
  "no-patches",
25876
25821
  newPatches,
25877
25822
  results,
@@ -25882,13 +25827,18 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25882
25827
  prep._prepared.revertedCount
25883
25828
  );
25884
25829
  }
25885
- async _prepareNormalRegeneration(options) {
25886
- const preLock = this.lockManager.read();
25830
+ };
25831
+ NormalRegenerationFlow = class {
25832
+ constructor(service) {
25833
+ this.service = service;
25834
+ }
25835
+ async prepare(options) {
25836
+ const preLock = this.service.lockManager.read();
25887
25837
  const previousGenerationSha = preLock.current_generation ?? null;
25888
25838
  if (options?.dryRun) {
25889
- const existingPatches2 = this.lockManager.getPatches();
25890
- const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.detector.detectNewPatches();
25891
- const warnings = [...this.detector.warnings, ...this.warnings];
25839
+ const existingPatches2 = this.service.lockManager.getPatches();
25840
+ const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.service.detector.detectNewPatches();
25841
+ const warnings = [...this.service.detector.warnings, ...this.service.warnings];
25892
25842
  const allPatches2 = [...existingPatches2, ...newPatches2];
25893
25843
  const preparedReport = {
25894
25844
  flow: "normal-regeneration",
@@ -25901,48 +25851,42 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25901
25851
  wouldApply: allPatches2,
25902
25852
  warnings: warnings.length > 0 ? warnings : void 0
25903
25853
  };
25904
- const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
25854
+ const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
25905
25855
  return {
25906
25856
  flow: "normal-regeneration",
25907
25857
  previousGenerationSha,
25908
25858
  currentGenerationSha: headSha,
25909
25859
  baseBranchHead: options.baseBranchHead ?? null,
25910
25860
  _prepared: {
25861
+ kind: "terminal",
25911
25862
  flow: "normal-regeneration",
25912
- terminal: true,
25913
- preparedReport,
25914
- patchesToApply: [],
25915
- newPatchIds: /* @__PURE__ */ new Set(),
25916
- detectorWarnings: [...this.detector.warnings],
25917
- revertedCount: dryRunReverted.length,
25918
- genSha: headSha,
25919
- optionsSnapshot: options
25863
+ preparedReport
25920
25864
  }
25921
25865
  };
25922
25866
  }
25923
- let existingPatches = this.lockManager.getPatches();
25867
+ let existingPatches = this.service.lockManager.getPatches();
25924
25868
  const preRebasePatchIds = new Set(existingPatches.map((p2) => p2.id));
25925
- const preRebaseCounts = await this.preGenerationRebase(existingPatches);
25926
- const postRebasePatchIds = new Set(this.lockManager.getPatches().map((p2) => p2.id));
25869
+ const preRebaseCounts = await this.service.preGenerationRebase(existingPatches);
25870
+ const postRebasePatchIds = new Set(this.service.lockManager.getPatches().map((p2) => p2.id));
25927
25871
  const removedByPreRebase = existingPatches.filter(
25928
25872
  (p2) => preRebasePatchIds.has(p2.id) && !postRebasePatchIds.has(p2.id)
25929
25873
  );
25930
- existingPatches = this.lockManager.getPatches();
25874
+ existingPatches = this.service.lockManager.getPatches();
25931
25875
  const seenHashes = /* @__PURE__ */ new Map();
25932
25876
  for (const p2 of existingPatches) {
25933
25877
  const priorPatchId = seenHashes.get(p2.content_hash);
25934
25878
  if (priorPatchId !== void 0) {
25935
- this.lockManager.removePatch(p2.id);
25936
- this.warnings.push(
25879
+ this.service.lockManager.removePatch(p2.id);
25880
+ this.service.warnings.push(
25937
25881
  `Consolidated patch ${p2.id} (commit ${(p2.original_commit || "<missing>").slice(0, 7)}) into prior patch ${priorPatchId}: byte-identical patch_content. Per-commit attribution preserved in git log; lockfile collapsed to avoid duplicate-apply corruption on next regen.`
25938
25882
  );
25939
25883
  } else {
25940
25884
  seenHashes.set(p2.content_hash, p2.id);
25941
25885
  }
25942
25886
  }
25943
- existingPatches = this.lockManager.getPatches();
25944
- let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
25945
- const detectorWarnings = [...this.detector.warnings];
25887
+ existingPatches = this.service.lockManager.getPatches();
25888
+ let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
25889
+ const detectorWarnings = [...this.service.detector.warnings];
25946
25890
  if (removedByPreRebase.length > 0) {
25947
25891
  const removedOriginalCommits = new Set(removedByPreRebase.map((p2) => p2.original_commit));
25948
25892
  const removedOriginalMessages = new Set(removedByPreRebase.map((p2) => p2.original_message));
@@ -25957,7 +25901,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25957
25901
  }
25958
25902
  for (const id of revertedPatchIds) {
25959
25903
  try {
25960
- this.lockManager.removePatch(id);
25904
+ this.service.lockManager.removePatch(id);
25961
25905
  } catch {
25962
25906
  }
25963
25907
  }
@@ -25969,18 +25913,18 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25969
25913
  generatorVersions: options.generatorVersions ?? {},
25970
25914
  baseBranchHead: options.baseBranchHead
25971
25915
  } : void 0;
25972
- await this.committer.commitGeneration("Update SDK", commitOpts);
25973
- await this.cleanupStaleConflictMarkers();
25974
- const genRecord = await this.committer.createGenerationRecord(commitOpts);
25975
- this.lockManager.addGeneration(genRecord);
25916
+ await this.service.committer.commitGeneration("Update SDK", commitOpts);
25917
+ await this.service.cleanupStaleConflictMarkers();
25918
+ const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
25919
+ this.service.lockManager.addGeneration(genRecord);
25976
25920
  return {
25977
25921
  flow: "normal-regeneration",
25978
25922
  previousGenerationSha,
25979
25923
  currentGenerationSha: genRecord.commit_sha,
25980
25924
  baseBranchHead: options?.baseBranchHead ?? null,
25981
25925
  _prepared: {
25926
+ kind: "active",
25982
25927
  flow: "normal-regeneration",
25983
- terminal: false,
25984
25928
  patchesToApply: allPatches,
25985
25929
  newPatchIds: new Set(newPatches.map((p2) => p2.id)),
25986
25930
  preRebaseCounts,
@@ -25991,46 +25935,47 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25991
25935
  }
25992
25936
  };
25993
25937
  }
25994
- async _applyNormalRegeneration(prep, options) {
25938
+ async apply(prep, options) {
25939
+ assertActive(prep);
25995
25940
  const allPatches = prep._prepared.patchesToApply;
25996
25941
  const newPatchIds = prep._prepared.newPatchIds;
25997
25942
  const detectorWarnings = prep._prepared.detectorWarnings;
25998
25943
  const genSha = prep._prepared.genSha;
25999
- const results = await this.applicator.applyPatches(allPatches);
26000
- this._lastApplyResults = results;
26001
- this.recordDroppedContextLineWarnings(results);
26002
- this.revertConflictingFiles(results);
25944
+ const results = await this.service.applicator.applyPatches(allPatches);
25945
+ this.service._lastApplyResults = results;
25946
+ this.service.recordDroppedContextLineWarnings(results);
25947
+ this.service.revertConflictingFiles(results);
26003
25948
  for (const result of results) {
26004
25949
  if (result.status === "conflict") {
26005
25950
  result.patch.status = "unresolved";
26006
25951
  }
26007
25952
  }
26008
- const rebaseCounts = await this.rebasePatches(results, genSha);
25953
+ const rebaseCounts = await this.service.rebasePatches(results, genSha);
26009
25954
  const newPatches = allPatches.filter((p2) => newPatchIds.has(p2.id));
26010
25955
  for (const patch of newPatches) {
26011
25956
  if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
26012
- this.lockManager.addPatch(patch);
25957
+ this.service.lockManager.addPatch(patch);
26013
25958
  }
26014
25959
  }
26015
25960
  for (const result of results) {
26016
25961
  if (result.status === "conflict") {
26017
25962
  try {
26018
- this.lockManager.markPatchUnresolved(result.patch.id);
25963
+ this.service.lockManager.markPatchUnresolved(result.patch.id);
26019
25964
  } catch {
26020
25965
  }
26021
25966
  }
26022
25967
  }
26023
- this.lockManager.save();
26024
- this.warnings.push(...this.lockManager.warnings);
25968
+ this.service.lockManager.save();
25969
+ this.service.warnings.push(...this.service.lockManager.warnings);
26025
25970
  if (options?.stageOnly) {
26026
- await this.committer.stageAll();
25971
+ await this.service.committer.stageAll();
26027
25972
  } else {
26028
25973
  const appliedCount = results.filter((r2) => r2.status === "applied").length;
26029
25974
  const buckets = computeCommitBuckets(results, rebaseCounts.absorbedPatchIds, allPatches);
26030
- await this.committer.commitReplay(appliedCount, allPatches, void 0, { buckets });
25975
+ await this.service.committer.commitReplay(appliedCount, allPatches, void 0, { buckets });
26031
25976
  }
26032
- const warnings = [...detectorWarnings, ...this.warnings];
26033
- return this.buildReport(
25977
+ const warnings = [...detectorWarnings, ...this.service.warnings];
25978
+ return this.service.buildReport(
26034
25979
  "normal-regeneration",
26035
25980
  allPatches,
26036
25981
  results,
@@ -26041,10 +25986,204 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26041
25986
  prep._prepared.revertedCount
26042
25987
  );
26043
25988
  }
25989
+ };
25990
+ ReplayService = class {
25991
+ /** @internal Used by Flow implementations in `src/flows/`. */
25992
+ git;
25993
+ /** @internal Used by Flow implementations in `src/flows/`. */
25994
+ detector;
25995
+ /** @internal Used by Flow implementations in `src/flows/`. */
25996
+ applicator;
25997
+ /** @internal Used by Flow implementations in `src/flows/`. */
25998
+ committer;
25999
+ /** @internal Used by Flow implementations in `src/flows/`. */
26000
+ lockManager;
26001
+ /** @internal Used by Flow implementations in `src/flows/`. */
26002
+ fileOwnership;
26003
+ /** @internal Used by Flow implementations in `src/flows/`. */
26004
+ outputDir;
26005
+ /** @internal Test-only. Captures the last ReplayResult[] returned from applicator.applyPatches(). */
26006
+ _lastApplyResults = [];
26007
+ /**
26008
+ * Service-level warnings accumulated during a single runReplay() call.
26009
+ * Merged with `detector.warnings` into `ReplayReport.warnings` by each flow handler.
26010
+ * Populated by `FileOwnership.resolveCanonical` when a patch's base generation tree
26011
+ * is unreachable (shallow clone / aggressive `git gc --prune`) and we fall back to a
26012
+ * conservative heuristic. Cleared at the top of `runReplay()`.
26013
+ *
26014
+ * @internal Used by Flow implementations in `src/flows/`.
26015
+ */
26016
+ warnings = [];
26017
+ /**
26018
+ * @internal Test-only accessor.
26019
+ * Returns the ReplayApplicator instance used for the last run. Tests use this
26020
+ * to inspect the inter-patch accumulator after runReplay() completes (invariant I2).
26021
+ */
26022
+ get applicatorRef() {
26023
+ return this.applicator;
26024
+ }
26025
+ /**
26026
+ * @internal Test-only accessor.
26027
+ * Returns the ReplayResult[] from the most recent applyPatches() call. Tests use
26028
+ * this to filter applied-vs-conflicted-vs-absorbed patches for invariant
26029
+ * assertions. Returns an empty array if no patches have been applied yet.
26030
+ */
26031
+ getLastResults() {
26032
+ return this._lastApplyResults;
26033
+ }
26034
+ constructor(outputDir, _config) {
26035
+ const git = new GitClient(outputDir);
26036
+ this.git = git;
26037
+ this.outputDir = outputDir;
26038
+ this.lockManager = new LockfileManager(outputDir);
26039
+ this.detector = new ReplayDetector(git, this.lockManager, outputDir);
26040
+ this.applicator = new ReplayApplicator(git, this.lockManager, outputDir);
26041
+ this.committer = new ReplayCommitter(git, outputDir);
26042
+ this.fileOwnership = new FileOwnership(git);
26043
+ }
26044
+ /**
26045
+ * Phase 1 of the two-phase replay flow. Runs preGenerationRebase (when applicable),
26046
+ * detects new patches, and creates the `[fern-generated]` commit. Leaves HEAD at
26047
+ * the generation commit (or unchanged for dry-run).
26048
+ *
26049
+ * Does NOT apply patches — the returned handle must be passed to
26050
+ * `applyPreparedReplay()` to complete the run. No disk writes to the lockfile
26051
+ * happen here; lockfile persistence is deferred to phase 2.
26052
+ *
26053
+ * Callers may land additional commits on HEAD between `prepareReplay` and
26054
+ * `applyPreparedReplay` (e.g., `[fern-autoversion]`).
26055
+ */
26056
+ async prepareReplay(options) {
26057
+ this.warnings = [];
26058
+ this.detector.warnings.length = 0;
26059
+ return this.selectFlow(options).prepare(options);
26060
+ }
26061
+ /**
26062
+ * Phase 2 of the two-phase replay flow. Applies patches, rebases them against
26063
+ * the generation, saves the lockfile, and commits `[fern-replay]` (or stages
26064
+ * under `stageOnly`). Operates on HEAD at call time — mid-phase commits are
26065
+ * respected.
26066
+ *
26067
+ * For terminal handles (dry-run, first-gen has no patch work, skip-app does
26068
+ * its own post-commit logic), this returns the precomputed report.
26069
+ */
26070
+ async applyPreparedReplay(prep, options) {
26071
+ if (prep._prepared.kind === "terminal") {
26072
+ return prep._prepared.preparedReport;
26073
+ }
26074
+ return this.flowForKind(prep._prepared.flow).apply(prep, options);
26075
+ }
26076
+ /**
26077
+ * Construct the flow for the current run. `selectFlow` is called from
26078
+ * `prepareReplay` and decides based on options + lockfile state.
26079
+ */
26080
+ selectFlow(options) {
26081
+ if (options?.skipApplication) return new SkipApplicationFlow(this);
26082
+ return this.flowForKind(this.determineFlow());
26083
+ }
26084
+ /**
26085
+ * Construct a flow given its kind. Used by `applyPreparedReplay` to
26086
+ * route to the same kind that was selected in phase 1.
26087
+ */
26088
+ flowForKind(kind) {
26089
+ switch (kind) {
26090
+ case "first-generation":
26091
+ return new FirstGenerationFlow(this);
26092
+ case "no-patches":
26093
+ return new NoPatchesFlow(this);
26094
+ case "normal-regeneration":
26095
+ return new NormalRegenerationFlow(this);
26096
+ case "skip-application":
26097
+ return new SkipApplicationFlow(this);
26098
+ }
26099
+ }
26100
+ /**
26101
+ * Backwards-compatible atomic entry point. Composes `prepareReplay` + `applyPreparedReplay`.
26102
+ * Behavior is byte-identical to the pre-split implementation for all existing callers.
26103
+ */
26104
+ async runReplay(options) {
26105
+ const prep = await this.prepareReplay(options);
26106
+ return this.applyPreparedReplay(prep, options);
26107
+ }
26108
+ /**
26109
+ * Sync the lockfile after a divergent PR was squash-merged.
26110
+ * Call this BEFORE runReplay() when the CLI detects a merged divergent PR.
26111
+ *
26112
+ * After updating the generation record, re-detects customer patches via
26113
+ * tree diff between the generation tag (pure generation tree) and HEAD
26114
+ * (which includes customer customizations after squash merge). This ensures
26115
+ * patches survive the squash merge → regenerate cycle even when the lockfile
26116
+ * was restored from the base branch during conflict PR creation.
26117
+ */
26118
+ async syncFromDivergentMerge(generationCommitSha, options) {
26119
+ const treeHash = await this.git.getTreeHash(generationCommitSha);
26120
+ const timestamp = (await this.git.exec(["log", "-1", "--format=%aI", generationCommitSha])).trim();
26121
+ const record2 = {
26122
+ commit_sha: generationCommitSha,
26123
+ tree_hash: treeHash,
26124
+ timestamp,
26125
+ cli_version: options?.cliVersion ?? "unknown",
26126
+ generator_versions: options?.generatorVersions ?? {},
26127
+ base_branch_head: options?.baseBranchHead
26128
+ };
26129
+ let resolvedPatches;
26130
+ if (!this.lockManager.exists()) {
26131
+ this.lockManager.initializeInMemory(record2);
26132
+ } else {
26133
+ this.lockManager.read();
26134
+ const unresolvedPatches = [
26135
+ ...this.lockManager.getUnresolvedPatches(),
26136
+ ...this.lockManager.getResolvingPatches()
26137
+ ];
26138
+ resolvedPatches = this.lockManager.getPatches().filter((p2) => p2.status == null);
26139
+ this.lockManager.addGeneration(record2);
26140
+ this.lockManager.clearPatches();
26141
+ for (const patch of unresolvedPatches) {
26142
+ this.lockManager.addPatch(patch);
26143
+ }
26144
+ }
26145
+ try {
26146
+ const { patches: redetectedPatches } = await this.detector.detectNewPatches();
26147
+ if (redetectedPatches.length > 0) {
26148
+ const redetectedFiles = new Set(redetectedPatches.flatMap((p2) => p2.files));
26149
+ const currentPatches = this.lockManager.getPatches();
26150
+ for (const patch of currentPatches) {
26151
+ if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
26152
+ this.lockManager.removePatch(patch.id);
26153
+ }
26154
+ }
26155
+ for (const patch of redetectedPatches) {
26156
+ this.lockManager.addPatch(patch);
26157
+ }
26158
+ }
26159
+ } catch (error) {
26160
+ for (const patch of resolvedPatches ?? []) {
26161
+ this.lockManager.addPatch(patch);
26162
+ }
26163
+ this.detector.warnings.push(
26164
+ `Patch re-detection failed after divergent merge sync. ${(resolvedPatches ?? []).length} previously resolved patch(es) preserved. Error: ${error instanceof Error ? error.message : String(error)}`
26165
+ );
26166
+ }
26167
+ this.lockManager.save();
26168
+ this.detector.warnings.push(...this.lockManager.warnings);
26169
+ }
26170
+ determineFlow() {
26171
+ try {
26172
+ const lock = this.lockManager.read();
26173
+ return lock.patches.length === 0 ? "no-patches" : "normal-regeneration";
26174
+ } catch (error) {
26175
+ if (error instanceof LockfileNotFoundError) {
26176
+ return "first-generation";
26177
+ }
26178
+ throw error;
26179
+ }
26180
+ }
26044
26181
  /**
26045
26182
  * Rebase cleanly applied patches so they are relative to the current generation.
26046
26183
  * This prevents recurring conflicts on subsequent regenerations.
26047
26184
  * Returns the number of patches rebased.
26185
+ *
26186
+ * @internal Used by Flow implementations in `src/flows/`.
26048
26187
  */
26049
26188
  async rebasePatches(results, currentGenSha) {
26050
26189
  let absorbed = 0;
@@ -26054,6 +26193,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26054
26193
  const seenContentHashes = /* @__PURE__ */ new Map();
26055
26194
  const absorbedPatchIds = /* @__PURE__ */ new Set();
26056
26195
  const fernignorePatterns = this.readFernignorePatterns();
26196
+ const warnedGens = /* @__PURE__ */ new Set();
26057
26197
  for (const result of results) {
26058
26198
  if (result.resolvedFiles && Object.keys(result.resolvedFiles).length > 0) {
26059
26199
  const patch = result.patch;
@@ -26098,18 +26238,17 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26098
26238
  }
26099
26239
  }
26100
26240
  const isUserOwnedPerFile = await Promise.all(
26101
- patch.files.map(async (file) => {
26102
- if (file === ".fernignore") return true;
26103
- if (fernignorePatterns.some((p2) => file === p2 || minimatch(file, p2))) {
26104
- return true;
26105
- }
26106
- if (patch.user_owned) return true;
26107
- if (this.fileIsCreationInPatchContent(patch.patch_content, originalByResolved[file] ?? file)) {
26108
- return true;
26109
- }
26110
- const content = await this.git.showFile(currentGenSha, file);
26111
- return content === null;
26112
- })
26241
+ patch.files.map(
26242
+ (file) => this.fileOwnership.resolveCanonical({
26243
+ file,
26244
+ originalFileName: originalByResolved[file],
26245
+ patch,
26246
+ currentGenSha,
26247
+ fernignorePatterns,
26248
+ warnedGens,
26249
+ warnings: this.warnings
26250
+ })
26251
+ )
26113
26252
  );
26114
26253
  const hasProtectedFiles = patch.files.some(
26115
26254
  (file) => file === ".fernignore" || fernignorePatterns.some((p2) => file === p2 || minimatch(file, p2))
@@ -26236,7 +26375,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26236
26375
  for (const file of files) {
26237
26376
  if (isBinaryFile(file)) continue;
26238
26377
  try {
26239
- const content = await (0, import_promises5.readFile)((0, import_path10.join)(this.outputDir, file), "utf-8");
26378
+ const content = await (0, import_promises7.readFile)((0, import_path13.join)(this.outputDir, file), "utf-8");
26240
26379
  snapshot[file] = content;
26241
26380
  } catch {
26242
26381
  }
@@ -26244,10 +26383,16 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26244
26383
  return snapshot;
26245
26384
  }
26246
26385
  /**
26247
- * FER-10203: returns true when a `[fern-autoversion]` commit between
26248
- * `fromSha` and `toSha` touched any of the patch's `files`. Re-classifies
26249
- * each grep match via `isGenerationCommit` so a customer commit that
26250
- * merely quotes the marker in its body doesn't false-positive.
26386
+ * Detects autoversion contamination — returns true when a
26387
+ * `[fern-autoversion]` commit between `fromSha` and `toSha` touched any
26388
+ * of the patch's `files`. Re-classifies each grep match via
26389
+ * `isGenerationCommit` so a customer commit that merely quotes the
26390
+ * marker in its body doesn't false-positive.
26391
+ *
26392
+ * Used by preGenerationRebase to skip the customer-content rebuild for
26393
+ * patches whose files were last touched by an autoversion step — a
26394
+ * naive `git diff currentGen HEAD` there would capture the
26395
+ * placeholder→semver swap as customer customization.
26251
26396
  */
26252
26397
  async hasAutoversionContamination(fromSha, toSha, files) {
26253
26398
  if (files.length === 0) return false;
@@ -26276,10 +26421,15 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26276
26421
  return false;
26277
26422
  }
26278
26423
  /**
26279
- * FER-10203: rebuild patch_content from `theirs_snapshot` (captured at
26280
- * detection time, predates pipeline content) rather than a HEAD-relative
26281
- * diff. Returns `null` when snapshot is missing or doesn't cover every
26282
- * file in `patch.files` — caller leaves the patch unchanged.
26424
+ * Rebuild patch_content from `theirs_snapshot` (captured at detection
26425
+ * time, predates pipeline content) rather than a HEAD-relative diff.
26426
+ * Returns `null` when snapshot is missing or doesn't cover every file
26427
+ * in `patch.files` — caller leaves the patch unchanged.
26428
+ *
26429
+ * Used as the autoversion-contamination escape hatch: when the
26430
+ * HEAD-relative diff would capture pipeline content (autoversion's
26431
+ * placeholder swap) as customer customization, the snapshot path
26432
+ * preserves the customer's actual intent.
26283
26433
  */
26284
26434
  async computeSnapshotBasedPatchDiff(patch, currentGen) {
26285
26435
  if (!patch.theirs_snapshot) return null;
@@ -26327,7 +26477,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26327
26477
  * **Skips patches sharing files with other patches.** HEAD's content
26328
26478
  * for a file shared by N patches is cumulative across all of them, so
26329
26479
  * a HEAD-fallback would falsely encode other patches' contributions
26330
- * into one patch's snapshot — breaking FER-9525 cross-patch isolation
26480
+ * into one patch's snapshot — breaking cross-patch isolation
26331
26481
  * if the snapshot fallback later fires. Per-patch snapshots only
26332
26482
  * make sense when the patch owns its file. Multi-patch-same-file
26333
26483
  * legacy lockfiles keep using line-anchored reconstruction (the
@@ -26389,72 +26539,6 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26389
26539
  );
26390
26540
  }
26391
26541
  }
26392
- /**
26393
- * Determine whether `file` is user-owned (never produced by the generator).
26394
- *
26395
- * Resolution order (first match wins):
26396
- * 1. `patch.user_owned === true` — authoritative, persisted across cycles.
26397
- * 2. `.fernignore` itself, or file matches a .fernignore pattern.
26398
- * 3. Legacy-safe signal: `patch_content` shows this file as a `--- /dev/null`
26399
- * creation. Works for patches that predate the `user_owned` field or
26400
- * whose flag was lost. Immune to rename tricks — relies on raw patch text.
26401
- * 4. Tree-based check against `patch.base_generation` when it is reachable
26402
- * AND distinct from `currentGenSha`. If the base tree is unreachable
26403
- * (shallow clone / aggressive `git gc --prune`), emit a one-time
26404
- * customer-actionable warning (deduplicated via warnedGens) and
26405
- * conservatively treat as user-owned — strictly safer than silent
26406
- * absorption (FER-9809).
26407
- * 5. Final fallback — check `currentGenSha` when there is no usable base
26408
- * (legacy one-gen lockfile). Preserves pre-fix behavior for that edge.
26409
- *
26410
- * `originalFileName` is the pre-rename path when the generator moved the
26411
- * file between the patch's base and the current generation. Tree lookups
26412
- * at ancestor generations use the original path — that's where the
26413
- * generator produced the content.
26414
- */
26415
- async isFileUserOwned(file, patch, currentGenSha, fernignorePatterns, warnedGens, originalFileName) {
26416
- if (patch.user_owned) return true;
26417
- if (file === ".fernignore") return true;
26418
- if (fernignorePatterns.some((p2) => file === p2 || minimatch(file, p2))) return true;
26419
- if (this.fileIsCreationInPatchContent(patch.patch_content, originalFileName ?? file)) {
26420
- return true;
26421
- }
26422
- const base = patch.base_generation;
26423
- if (base && base !== currentGenSha) {
26424
- const reachable = await this.git.commitExists(base) || await this.git.treeExists(base);
26425
- if (!reachable) {
26426
- if (!warnedGens.has(base)) {
26427
- warnedGens.add(base);
26428
- this.warnings.push(
26429
- `Base generation ${base.slice(0, 7)} is unreachable (shallow clone or pruned history). Treating affected files as user-owned to avoid silent data loss. Run \`git fetch --unshallow\` or avoid \`git gc --prune\` to restore full history.`
26430
- );
26431
- }
26432
- return true;
26433
- }
26434
- return await this.git.showFile(base, originalFileName ?? file) === null;
26435
- }
26436
- return await this.git.showFile(currentGenSha, originalFileName ?? file) === null;
26437
- }
26438
- /**
26439
- * Returns true if `file`'s diff section in `patchContent` starts with
26440
- * `--- /dev/null`, meaning this file was introduced by the patch (user
26441
- * creation). Used as a legacy-safe fallback when `patch.user_owned` is
26442
- * absent or cleared.
26443
- */
26444
- fileIsCreationInPatchContent(patchContent, file) {
26445
- const lines = patchContent.split("\n");
26446
- let inFileSection = false;
26447
- for (const line of lines) {
26448
- if (line.startsWith("diff --git ")) {
26449
- inFileSection = line.includes(` b/${file}`) || line.endsWith(` b/${file}`);
26450
- continue;
26451
- }
26452
- if (inFileSection && line.startsWith("--- ")) {
26453
- return line === "--- /dev/null";
26454
- }
26455
- }
26456
- return false;
26457
- }
26458
26542
  /**
26459
26543
  * For conflict patches with mixed results (some files merged, some conflicted),
26460
26544
  * check if the cleanly merged files were absorbed by the generator (empty diff).
@@ -26473,7 +26557,15 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26473
26557
  const warnedGens = /* @__PURE__ */ new Set();
26474
26558
  const generatorCleanFiles = [];
26475
26559
  for (const file of cleanFiles) {
26476
- if (await this.isFileUserOwned(file, patch, currentGenSha, fernignorePatterns, warnedGens)) {
26560
+ const userOwned = await this.fileOwnership.resolveCanonical({
26561
+ file,
26562
+ patch,
26563
+ currentGenSha,
26564
+ fernignorePatterns,
26565
+ warnedGens,
26566
+ warnings: this.warnings
26567
+ });
26568
+ if (userOwned) {
26477
26569
  continue;
26478
26570
  }
26479
26571
  generatorCleanFiles.push(file);
@@ -26502,6 +26594,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26502
26594
  * Pre-generation rebase: update patches using the customer's current state.
26503
26595
  * Called BEFORE commitGeneration() while HEAD has customer code.
26504
26596
  */
26597
+ /** @internal Used by Flow implementations in `src/flows/`. */
26505
26598
  async preGenerationRebase(patches) {
26506
26599
  const lock = this.lockManager.read();
26507
26600
  const currentGen = lock.current_generation;
@@ -26540,7 +26633,15 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26540
26633
  if (patch.user_owned) return true;
26541
26634
  if (patch.files.length === 0) return false;
26542
26635
  for (const file of patch.files) {
26543
- if (!await this.isFileUserOwned(file, patch, currentGen, fernignorePatterns, warnedGens)) {
26636
+ const userOwned = await this.fileOwnership.resolveCanonical({
26637
+ file,
26638
+ patch,
26639
+ currentGenSha: currentGen,
26640
+ fernignorePatterns,
26641
+ warnedGens,
26642
+ warnings: this.warnings
26643
+ });
26644
+ if (!userOwned) {
26544
26645
  return false;
26545
26646
  }
26546
26647
  }
@@ -26732,6 +26833,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26732
26833
  * relied on them. This is the "surface or preserve, never silently drop"
26733
26834
  * contract for legitimate-deletion cases.
26734
26835
  */
26836
+ /** @internal Used by Flow implementations in `src/flows/`. */
26735
26837
  recordDroppedContextLineWarnings(results) {
26736
26838
  const PREVIEW_COUNT = 5;
26737
26839
  for (const result of results) {
@@ -26754,12 +26856,13 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
26754
26856
  * After applyPatches(), strip conflict markers from conflicting files
26755
26857
  * so only clean content is committed. Keeps the Generated (OURS) side.
26756
26858
  */
26859
+ /** @internal Used by Flow implementations in `src/flows/`. */
26757
26860
  revertConflictingFiles(results) {
26758
26861
  for (const result of results) {
26759
26862
  if (result.status !== "conflict" || !result.fileResults) continue;
26760
26863
  for (const fileResult of result.fileResults) {
26761
26864
  if (fileResult.status !== "conflict") continue;
26762
- const filePath = (0, import_path10.join)(this.outputDir, fileResult.file);
26865
+ const filePath = (0, import_path13.join)(this.outputDir, fileResult.file);
26763
26866
  try {
26764
26867
  const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
26765
26868
  const stripped = stripConflictMarkers(content);
@@ -26775,6 +26878,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
26775
26878
  * Restores files to their clean generated state from HEAD.
26776
26879
  * Skips .fernignore-protected files to prevent overwriting user content.
26777
26880
  */
26881
+ /** @internal Used by Flow implementations in `src/flows/`. */
26778
26882
  async cleanupStaleConflictMarkers() {
26779
26883
  const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<< Generated", "--", "."]).catch(() => "");
26780
26884
  const files = markerFiles.trim().split("\n").filter(Boolean);
@@ -26789,10 +26893,11 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
26789
26893
  }
26790
26894
  }
26791
26895
  readFernignorePatterns() {
26792
- const fernignorePath = (0, import_path10.join)(this.outputDir, ".fernignore");
26896
+ const fernignorePath = (0, import_path13.join)(this.outputDir, ".fernignore");
26793
26897
  if (!(0, import_fs7.existsSync)(fernignorePath)) return [];
26794
26898
  return (0, import_fs7.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
26795
26899
  }
26900
+ /** @internal Used by Flow implementations in `src/flows/`. */
26796
26901
  buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted) {
26797
26902
  const conflictResults = results.filter((r2) => r2.status === "conflict");
26798
26903
  const conflictDetails = conflictResults.map((r2) => {
@@ -26847,10 +26952,10 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
26847
26952
  this.outputDir = outputDir;
26848
26953
  }
26849
26954
  fernignoreExists() {
26850
- return (0, import_fs8.existsSync)((0, import_path12.join)(this.outputDir, ".fernignore"));
26955
+ return (0, import_fs8.existsSync)((0, import_path15.join)(this.outputDir, ".fernignore"));
26851
26956
  }
26852
26957
  readFernignorePatterns() {
26853
- const fernignorePath = (0, import_path12.join)(this.outputDir, ".fernignore");
26958
+ const fernignorePath = (0, import_path15.join)(this.outputDir, ".fernignore");
26854
26959
  if (!(0, import_fs8.existsSync)(fernignorePath)) {
26855
26960
  return [];
26856
26961
  }
@@ -26959,7 +27064,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
26959
27064
  return results;
26960
27065
  }
26961
27066
  readFileContent(filePath) {
26962
- const fullPath = (0, import_path12.join)(this.outputDir, filePath);
27067
+ const fullPath = (0, import_path15.join)(this.outputDir, filePath);
26963
27068
  if (!(0, import_fs8.existsSync)(fullPath)) {
26964
27069
  return null;
26965
27070
  }
@@ -27024,7 +27129,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
27024
27129
  };
27025
27130
  }
27026
27131
  movePatternsToReplayYml(patterns) {
27027
- const replayYmlPath = (0, import_path12.join)(this.outputDir, ".fern", "replay.yml");
27132
+ const replayYmlPath = (0, import_path15.join)(this.outputDir, ".fern", "replay.yml");
27028
27133
  let config = {};
27029
27134
  if ((0, import_fs8.existsSync)(replayYmlPath)) {
27030
27135
  const content = (0, import_fs8.readFileSync)(replayYmlPath, "utf-8");
@@ -27033,7 +27138,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
27033
27138
  const existing = config.exclude ?? [];
27034
27139
  const merged = [.../* @__PURE__ */ new Set([...existing, ...patterns])];
27035
27140
  config.exclude = merged;
27036
- const dir = (0, import_path12.dirname)(replayYmlPath);
27141
+ const dir = (0, import_path15.dirname)(replayYmlPath);
27037
27142
  if (!(0, import_fs8.existsSync)(dir)) {
27038
27143
  (0, import_fs8.mkdirSync)(dir, { recursive: true });
27039
27144
  }
@@ -29639,8 +29744,8 @@ function resolve(...paths) {
29639
29744
 
29640
29745
  // src/cli.ts
29641
29746
  var import_fs16 = __toESM(require("fs"));
29642
- var import_promises16 = require("fs/promises");
29643
- var import_path22 = __toESM(require("path"));
29747
+ var import_promises18 = require("fs/promises");
29748
+ var import_path25 = __toESM(require("path"));
29644
29749
 
29645
29750
  // ../../node_modules/.pnpm/yargs@17.7.2/node_modules/yargs/build/lib/yerror.js
29646
29751
  var YError = class _YError extends Error {
@@ -31607,15 +31712,18 @@ function parseRepository(githubRepository) {
31607
31712
  githubRepository = githubRepository.slice(0, -4);
31608
31713
  }
31609
31714
  const parts = githubRepository.split("/");
31610
- if (parts.length === 2 && parts[0] != null && parts[1] != null) {
31715
+ if (parts.length === 2 && isNonEmptyPart(parts[0]) && isNonEmptyPart(parts[1])) {
31611
31716
  [owner, repo] = parts;
31612
- } else if (parts.length === 3 && parts[0] != null && parts[1] != null && parts[2] != null) {
31717
+ } else if (parts.length === 3 && isNonEmptyPart(parts[0]) && isNonEmptyPart(parts[1]) && isNonEmptyPart(parts[2])) {
31613
31718
  [remote, owner, repo] = parts;
31614
31719
  } else {
31615
31720
  throw new Error(`Failed to parse GitHub repository ${githubRepository}`);
31616
31721
  }
31617
31722
  return newRepositoryReference({ remote, owner, repo });
31618
31723
  }
31724
+ function isNonEmptyPart(part) {
31725
+ return part != null && part.length > 0;
31726
+ }
31619
31727
  function newRepositoryReference({ remote, owner, repo }) {
31620
31728
  const repoUrl = getRepoUrl({ remote, owner, repo });
31621
31729
  const cloneUrl = getCloneUrl({ remote, owner, repo });
@@ -39354,42 +39462,42 @@ function replayForget(params) {
39354
39462
  // src/replay/replay-init.ts
39355
39463
  init_dist3();
39356
39464
  var import_fs11 = require("fs");
39357
- var import_path16 = require("path");
39465
+ var import_path19 = require("path");
39358
39466
  var import_tmp_promise2 = __toESM(require_tmp_promise());
39359
39467
 
39360
39468
  // src/replay/fernignore.ts
39361
- var import_promises8 = require("fs/promises");
39362
- var import_path15 = require("path");
39469
+ var import_promises10 = require("fs/promises");
39470
+ var import_path18 = require("path");
39363
39471
  var REPLAY_FERNIGNORE_ENTRIES2 = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
39364
39472
  var GITATTRIBUTES_ENTRIES2 = [".fern/replay.lock linguist-generated=true"];
39365
39473
  async function ensureReplayFernignoreEntries(outputDir) {
39366
- const fernignorePath = (0, import_path15.join)(outputDir, ".fernignore");
39474
+ const fernignorePath = (0, import_path18.join)(outputDir, ".fernignore");
39367
39475
  let content = "";
39368
39476
  try {
39369
- content = await (0, import_promises8.readFile)(fernignorePath, "utf-8");
39477
+ content = await (0, import_promises10.readFile)(fernignorePath, "utf-8");
39370
39478
  } catch {
39371
39479
  }
39372
39480
  const lines = content.split("\n");
39373
39481
  const toAdd = REPLAY_FERNIGNORE_ENTRIES2.filter((entry) => !lines.some((line) => line.trim() === entry));
39374
39482
  if (toAdd.length > 0) {
39375
39483
  const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
39376
- await (0, import_promises8.writeFile)(fernignorePath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
39484
+ await (0, import_promises10.writeFile)(fernignorePath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
39377
39485
  return true;
39378
39486
  }
39379
39487
  return false;
39380
39488
  }
39381
39489
  async function ensureGitattributesEntries2(outputDir) {
39382
- const gitattributesPath = (0, import_path15.join)(outputDir, ".gitattributes");
39490
+ const gitattributesPath = (0, import_path18.join)(outputDir, ".gitattributes");
39383
39491
  let content = "";
39384
39492
  try {
39385
- content = await (0, import_promises8.readFile)(gitattributesPath, "utf-8");
39493
+ content = await (0, import_promises10.readFile)(gitattributesPath, "utf-8");
39386
39494
  } catch {
39387
39495
  }
39388
39496
  const lines = content.split("\n");
39389
39497
  const toAdd = GITATTRIBUTES_ENTRIES2.filter((entry) => !lines.some((line) => line.trim() === entry));
39390
39498
  if (toAdd.length > 0) {
39391
39499
  const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
39392
- await (0, import_promises8.writeFile)(gitattributesPath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
39500
+ await (0, import_promises10.writeFile)(gitattributesPath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
39393
39501
  }
39394
39502
  }
39395
39503
 
@@ -39416,7 +39524,7 @@ async function replayInit(params) {
39416
39524
  if (dryRun) {
39417
39525
  return { bootstrap: bootstrapResult, fernignoreEntries: [], gitattributesEntries: [] };
39418
39526
  }
39419
- const lockfilePath = (0, import_path16.join)(repoPath, ".fern", "replay.lock");
39527
+ const lockfilePath = (0, import_path19.join)(repoPath, ".fern", "replay.lock");
39420
39528
  const lockfileContent = (0, import_fs11.existsSync)(lockfilePath) ? (0, import_fs11.readFileSync)(lockfilePath, "utf-8") : void 0;
39421
39529
  return {
39422
39530
  bootstrap: bootstrapResult,
@@ -39539,7 +39647,7 @@ init_dist3();
39539
39647
  init_dist3();
39540
39648
  var import_child_process4 = require("child_process");
39541
39649
  var import_fs12 = require("fs");
39542
- var import_path17 = require("path");
39650
+ var import_path20 = require("path");
39543
39651
  async function replayPrepare(params, state) {
39544
39652
  const {
39545
39653
  outputDir,
@@ -39550,7 +39658,7 @@ async function replayPrepare(params, state) {
39550
39658
  skipApplication,
39551
39659
  logger
39552
39660
  } = params;
39553
- const lockfilePath = (0, import_path17.join)(outputDir, ".fern", "replay.lock");
39661
+ const lockfilePath = (0, import_path20.join)(outputDir, ".fern", "replay.lock");
39554
39662
  let autoBootstrapped = false;
39555
39663
  let bootstrapAttempted = false;
39556
39664
  if (!(0, import_fs12.existsSync)(lockfilePath)) {
@@ -39804,29 +39912,29 @@ function replayStatus(params) {
39804
39912
  }
39805
39913
 
39806
39914
  // src/configuration/loadGitHubConfig.ts
39807
- var import_promises9 = require("fs/promises");
39915
+ var import_promises11 = require("fs/promises");
39808
39916
  async function loadGitHubConfig({
39809
39917
  absolutePathToConfig
39810
39918
  }) {
39811
- const rawContents = await (0, import_promises9.readFile)(absolutePathToConfig, "utf8");
39919
+ const rawContents = await (0, import_promises11.readFile)(absolutePathToConfig, "utf8");
39812
39920
  return JSON.parse(rawContents);
39813
39921
  }
39814
39922
 
39815
39923
  // src/configuration/loadReadmeConfig.ts
39816
- var import_promises10 = require("fs/promises");
39924
+ var import_promises12 = require("fs/promises");
39817
39925
  async function loadReadmeConfig({
39818
39926
  absolutePathToConfig
39819
39927
  }) {
39820
- const rawContents = await (0, import_promises10.readFile)(absolutePathToConfig, "utf8");
39928
+ const rawContents = await (0, import_promises12.readFile)(absolutePathToConfig, "utf8");
39821
39929
  return JSON.parse(rawContents);
39822
39930
  }
39823
39931
 
39824
39932
  // src/configuration/loadReferenceConfig.ts
39825
- var import_promises11 = require("fs/promises");
39933
+ var import_promises13 = require("fs/promises");
39826
39934
  async function loadReferenceConfig({
39827
39935
  absolutePathToConfig
39828
39936
  }) {
39829
- const rawContents = await (0, import_promises11.readFile)(absolutePathToConfig, "utf8");
39937
+ const rawContents = await (0, import_promises13.readFile)(absolutePathToConfig, "utf8");
39830
39938
  return JSON.parse(rawContents);
39831
39939
  }
39832
39940
 
@@ -39853,8 +39961,8 @@ var FERN_BOT_LOGIN2 = "fern-api[bot]";
39853
39961
  // src/pipeline/steps/AutoVersionStep.ts
39854
39962
  var import_child_process5 = require("child_process");
39855
39963
  var import_fs14 = require("fs");
39856
- var import_promises13 = require("fs/promises");
39857
- var import_path19 = require("path");
39964
+ var import_promises15 = require("fs/promises");
39965
+ var import_path22 = require("path");
39858
39966
 
39859
39967
  // ../commons/logging-execa/lib/loggingExeca.js
39860
39968
  var import_execa = __toESM(require_execa(), 1);
@@ -39911,10 +40019,136 @@ async function loggingExeca(logger, executable, args = [], { doNotPipeOutput = f
39911
40019
  return result;
39912
40020
  }
39913
40021
 
40022
+ // ../cli/task-context/lib/CliError.js
40023
+ var CliError = class _CliError extends Error {
40024
+ code;
40025
+ docsLink;
40026
+ constructor({ message, code, docsLink }) {
40027
+ super(message);
40028
+ Object.setPrototypeOf(this, new.target.prototype);
40029
+ if (Error.captureStackTrace) {
40030
+ Error.captureStackTrace(this, this.constructor);
40031
+ }
40032
+ this.name = this.constructor.name;
40033
+ this.code = code;
40034
+ this.docsLink = docsLink;
40035
+ }
40036
+ static authRequired(message) {
40037
+ return new _CliError({
40038
+ message: message ?? "Authentication required. Please run 'fern login' or set the FERN_TOKEN environment variable.",
40039
+ code: _CliError.Code.AuthError
40040
+ });
40041
+ }
40042
+ static unauthorized(message) {
40043
+ return new _CliError({
40044
+ message: message ?? "Unauthorized. Please run 'fern auth login' or set the FERN_TOKEN environment variable.",
40045
+ code: _CliError.Code.AuthError
40046
+ });
40047
+ }
40048
+ static notFound(message) {
40049
+ return new _CliError({ message, code: _CliError.Code.ConfigError });
40050
+ }
40051
+ static badRequest(message) {
40052
+ return new _CliError({ message, code: _CliError.Code.NetworkError });
40053
+ }
40054
+ static validationError(message) {
40055
+ return new _CliError({ message, code: _CliError.Code.ValidationError });
40056
+ }
40057
+ static internalError(message) {
40058
+ return new _CliError({ message, code: _CliError.Code.InternalError });
40059
+ }
40060
+ };
40061
+ (function(CliError2) {
40062
+ CliError2.Code = {
40063
+ InternalError: "INTERNAL_ERROR",
40064
+ ResolutionError: "RESOLUTION_ERROR",
40065
+ IrConversionError: "IR_CONVERSION_ERROR",
40066
+ ContainerError: "CONTAINER_ERROR",
40067
+ VersionError: "VERSION_ERROR",
40068
+ ParseError: "PARSE_ERROR",
40069
+ EnvironmentError: "ENVIRONMENT_ERROR",
40070
+ ReferenceError: "REFERENCE_ERROR",
40071
+ ValidationError: "VALIDATION_ERROR",
40072
+ NetworkError: "NETWORK_ERROR",
40073
+ AuthError: "AUTH_ERROR",
40074
+ ConfigError: "CONFIG_ERROR",
40075
+ UserError: "USER_ERROR"
40076
+ };
40077
+ })(CliError || (CliError = {}));
40078
+ var SENTRY_REPORTABLE = {
40079
+ [CliError.Code.InternalError]: true,
40080
+ [CliError.Code.ResolutionError]: true,
40081
+ [CliError.Code.IrConversionError]: true,
40082
+ [CliError.Code.ContainerError]: false,
40083
+ [CliError.Code.VersionError]: false,
40084
+ [CliError.Code.ParseError]: false,
40085
+ [CliError.Code.EnvironmentError]: false,
40086
+ [CliError.Code.ReferenceError]: false,
40087
+ [CliError.Code.ValidationError]: false,
40088
+ [CliError.Code.NetworkError]: false,
40089
+ [CliError.Code.AuthError]: false,
40090
+ [CliError.Code.ConfigError]: false,
40091
+ [CliError.Code.UserError]: false
40092
+ };
40093
+
40094
+ // ../cli/logger/lib/LogLevel.js
40095
+ var LogLevel2 = {
40096
+ Trace: "trace",
40097
+ Debug: "debug",
40098
+ Info: "info",
40099
+ Warn: "warn",
40100
+ Error: "error"
40101
+ };
40102
+ var LOG_LEVELS = Object.values(LogLevel2);
40103
+
40104
+ // ../cli/logger/lib/createLogger.js
40105
+ var LoggerImpl = class {
40106
+ log;
40107
+ enabled;
40108
+ constructor(log, enabled = true) {
40109
+ this.log = log;
40110
+ this.enabled = enabled;
40111
+ }
40112
+ disable() {
40113
+ this.enabled = false;
40114
+ }
40115
+ enable() {
40116
+ this.enabled = true;
40117
+ }
40118
+ debug(...args) {
40119
+ if (this.enabled) {
40120
+ this.log(LogLevel2.Debug, ...args);
40121
+ }
40122
+ }
40123
+ info(...args) {
40124
+ if (this.enabled) {
40125
+ this.log(LogLevel2.Info, ...args);
40126
+ }
40127
+ }
40128
+ warn(...args) {
40129
+ if (this.enabled) {
40130
+ this.log(LogLevel2.Warn, ...args);
40131
+ }
40132
+ }
40133
+ error(...args) {
40134
+ if (this.enabled) {
40135
+ this.log(LogLevel2.Error, ...args);
40136
+ }
40137
+ }
40138
+ trace(...args) {
40139
+ if (this.enabled) {
40140
+ this.log(LogLevel2.Trace, ...args);
40141
+ }
40142
+ }
40143
+ };
40144
+ function createLogger4(log) {
40145
+ return new LoggerImpl(log);
40146
+ }
40147
+
39914
40148
  // src/autoversion/AutoVersioningService.ts
39915
40149
  var import_fs13 = require("fs");
39916
- var import_promises12 = require("fs/promises");
39917
- var import_path18 = require("path");
40150
+ var import_promises14 = require("fs/promises");
40151
+ var import_path21 = require("path");
39918
40152
  var import_semver = __toESM(require_semver2());
39919
40153
  var AutoVersioningException = class extends Error {
39920
40154
  /**
@@ -40577,10 +40811,17 @@ var AutoVersioningService = class _AutoVersioningService {
40577
40811
  } else {
40578
40812
  command = `find "${workingDirectory}" -type f -not -path "*/.git/*" -exec sed -i '' '${sedCommand}' {} +`;
40579
40813
  }
40580
- await loggingExeca(this.logger, "bash", ["-c", command], {
40581
- cwd: workingDirectory,
40582
- doNotPipeOutput: true
40583
- });
40814
+ try {
40815
+ await loggingExeca(this.logger, "bash", ["-c", command], {
40816
+ cwd: workingDirectory,
40817
+ doNotPipeOutput: true
40818
+ });
40819
+ } catch (error) {
40820
+ throw new CliError({
40821
+ message: `Failed to replace placeholder version in generated files: ${extractErrorMessage(error)}`,
40822
+ code: CliError.Code.UserError
40823
+ });
40824
+ }
40584
40825
  this.logger.debug("Placeholder version replaced successfully");
40585
40826
  }
40586
40827
  /**
@@ -40601,7 +40842,7 @@ var AutoVersioningService = class _AutoVersioningService {
40601
40842
  );
40602
40843
  return;
40603
40844
  }
40604
- const goModPath = (0, import_path18.join)(workingDirectory, "go.mod");
40845
+ const goModPath = (0, import_path21.join)(workingDirectory, "go.mod");
40605
40846
  if (!(0, import_fs13.existsSync)(goModPath)) {
40606
40847
  this.logger.debug(`No go.mod found in ${workingDirectory}, skipping Go module path update`);
40607
40848
  return;
@@ -40635,7 +40876,7 @@ var AutoVersioningService = class _AutoVersioningService {
40635
40876
  * Reads the module path from a go.mod file.
40636
40877
  */
40637
40878
  async readGoModulePath(goModPath) {
40638
- const content = await (0, import_promises12.readFile)(goModPath, "utf-8");
40879
+ const content = await (0, import_promises14.readFile)(goModPath, "utf-8");
40639
40880
  for (const line of content.split("\n")) {
40640
40881
  const trimmed2 = line.trim();
40641
40882
  if (trimmed2.startsWith("module ")) {
@@ -40648,7 +40889,7 @@ var AutoVersioningService = class _AutoVersioningService {
40648
40889
  * Adds a version suffix to the module line in go.mod.
40649
40890
  */
40650
40891
  async addSuffixToGoMod(goModPath, modulePath, suffix) {
40651
- const content = await (0, import_promises12.readFile)(goModPath, "utf-8");
40892
+ const content = await (0, import_promises14.readFile)(goModPath, "utf-8");
40652
40893
  const lines = content.split("\n");
40653
40894
  let changed = false;
40654
40895
  for (let i2 = 0; i2 < lines.length; i2++) {
@@ -40660,7 +40901,7 @@ var AutoVersioningService = class _AutoVersioningService {
40660
40901
  }
40661
40902
  }
40662
40903
  if (changed) {
40663
- await (0, import_promises12.writeFile)(goModPath, lines.join("\n"), "utf-8");
40904
+ await (0, import_promises14.writeFile)(goModPath, lines.join("\n"), "utf-8");
40664
40905
  this.logger.debug(`Updated go.mod: added ${suffix} to module path`);
40665
40906
  }
40666
40907
  }
@@ -40669,10 +40910,10 @@ var AutoVersioningService = class _AutoVersioningService {
40669
40910
  */
40670
40911
  async walkDirectory(dir, filter2) {
40671
40912
  const results = [];
40672
- const entries2 = await (0, import_promises12.readdir)(dir);
40913
+ const entries2 = await (0, import_promises14.readdir)(dir);
40673
40914
  for (const entry of entries2) {
40674
- const fullPath = (0, import_path18.join)(dir, entry);
40675
- const entryStat = await (0, import_promises12.stat)(fullPath);
40915
+ const fullPath = (0, import_path21.join)(dir, entry);
40916
+ const entryStat = await (0, import_promises14.stat)(fullPath);
40676
40917
  if (entryStat.isDirectory()) {
40677
40918
  if (entry === ".git" || entry === "vendor") {
40678
40919
  continue;
@@ -40689,7 +40930,7 @@ var AutoVersioningService = class _AutoVersioningService {
40689
40930
  * Adds a version suffix to import paths in all .go files in the repository.
40690
40931
  */
40691
40932
  async addSuffixToGoFiles(repoRoot, modulePath, suffix) {
40692
- const goFiles = await this.walkDirectory(repoRoot, (filePath) => (0, import_path18.extname)(filePath) === ".go");
40933
+ const goFiles = await this.walkDirectory(repoRoot, (filePath) => (0, import_path21.extname)(filePath) === ".go");
40693
40934
  for (const filePath of goFiles) {
40694
40935
  await this.addSuffixToGoFile(filePath, modulePath, suffix);
40695
40936
  }
@@ -40699,7 +40940,7 @@ var AutoVersioningService = class _AutoVersioningService {
40699
40940
  * Replaces occurrences of the module path with modulePath + suffix in import statements.
40700
40941
  */
40701
40942
  async addSuffixToGoFile(filePath, modulePath, suffix) {
40702
- const content = await (0, import_promises12.readFile)(filePath, "utf-8");
40943
+ const content = await (0, import_promises14.readFile)(filePath, "utf-8");
40703
40944
  const lines = content.split("\n");
40704
40945
  let changed = false;
40705
40946
  let inImportBlock = false;
@@ -40728,7 +40969,7 @@ var AutoVersioningService = class _AutoVersioningService {
40728
40969
  }
40729
40970
  }
40730
40971
  if (changed) {
40731
- await (0, import_promises12.writeFile)(filePath, lines.join("\n"), "utf-8");
40972
+ await (0, import_promises14.writeFile)(filePath, lines.join("\n"), "utf-8");
40732
40973
  this.logger.debug(`Updated Go imports in ${filePath}`);
40733
40974
  }
40734
40975
  }
@@ -41134,7 +41375,7 @@ ${body}` : `${COMMIT_MARKER} ${subject}`;
41134
41375
  }
41135
41376
  async prependChangelogEntry(params) {
41136
41377
  const { version: version2, entry } = params;
41137
- const changelogPath = (0, import_path19.join)(this.outputDir, "changelog.md");
41378
+ const changelogPath = (0, import_path22.join)(this.outputDir, "changelog.md");
41138
41379
  const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
41139
41380
  const header = `## [${version2}] - ${now}
41140
41381
  `;
@@ -41143,7 +41384,7 @@ ${body}` : `${COMMIT_MARKER} ${subject}`;
41143
41384
  `;
41144
41385
  let existing = "";
41145
41386
  if ((0, import_fs14.existsSync)(changelogPath)) {
41146
- existing = await (0, import_promises13.readFile)(changelogPath, "utf-8");
41387
+ existing = await (0, import_promises15.readFile)(changelogPath, "utf-8");
41147
41388
  }
41148
41389
  let output;
41149
41390
  if (existing.trim().length === 0) {
@@ -41160,7 +41401,7 @@ ${newBlock}${remainder}`;
41160
41401
  } else {
41161
41402
  output = `${newBlock}${existing}`;
41162
41403
  }
41163
- await (0, import_promises13.writeFile)(changelogPath, output, "utf-8");
41404
+ await (0, import_promises15.writeFile)(changelogPath, output, "utf-8");
41164
41405
  }
41165
41406
  brandMessage(message) {
41166
41407
  if (this.config.isWhitelabel) {
@@ -41383,8 +41624,8 @@ function tryRevParse(cwd2, rev) {
41383
41624
  }
41384
41625
 
41385
41626
  // src/pipeline/steps/GithubStep.ts
41386
- var import_promises14 = require("fs/promises");
41387
- var import_path20 = require("path");
41627
+ var import_promises16 = require("fs/promises");
41628
+ var import_path23 = require("path");
41388
41629
 
41389
41630
  // src/pipeline/github/createReplayBranch.ts
41390
41631
  async function createReplayBranch(repository, branchName, commitMessage, replayConflictInfo, logger) {
@@ -42024,13 +42265,13 @@ var GithubStep = class extends BaseStep {
42024
42265
  }
42025
42266
  async ensureFernignore() {
42026
42267
  this.logger.debug("Checking for .fernignore file...");
42027
- const fernignorePath = (0, import_path20.join)(this.outputDir, ".fernignore");
42268
+ const fernignorePath = (0, import_path23.join)(this.outputDir, ".fernignore");
42028
42269
  try {
42029
- await (0, import_promises14.access)(fernignorePath);
42270
+ await (0, import_promises16.access)(fernignorePath);
42030
42271
  this.logger.debug(".fernignore already exists");
42031
42272
  } catch {
42032
42273
  this.logger.debug("Creating .fernignore file...");
42033
- await (0, import_promises14.writeFile)(fernignorePath, "# Specify files that shouldn't be modified by Fern\n", "utf-8");
42274
+ await (0, import_promises16.writeFile)(fernignorePath, "# Specify files that shouldn't be modified by Fern\n", "utf-8");
42034
42275
  }
42035
42276
  }
42036
42277
  deriveSkipCommit(replayResult) {
@@ -42241,7 +42482,7 @@ var ReplayStep = class extends BaseStep {
42241
42482
  };
42242
42483
 
42243
42484
  // ../cli/generation/local-generation/docker-utils/lib/runDocker.js
42244
- var import_promises15 = require("fs/promises");
42485
+ var import_promises17 = require("fs/promises");
42245
42486
  var import_tmp_promise3 = __toESM(require_tmp_promise(), 1);
42246
42487
  async function pullImage(imageName, runner, signal) {
42247
42488
  await loggingExeca(void 0, runner ?? "docker", ["pull", imageName], {
@@ -42288,7 +42529,7 @@ async function execInContainer({ logger, containerId, command, runner, writeLogs
42288
42529
  const logs = stdout + stderr;
42289
42530
  if (writeLogsToFile) {
42290
42531
  const tmpFile = await import_tmp_promise3.default.file();
42291
- await (0, import_promises15.writeFile)(tmpFile.path, logs);
42532
+ await (0, import_promises17.writeFile)(tmpFile.path, logs);
42292
42533
  logger.info(`Generator logs here: ${tmpFile.path}`);
42293
42534
  }
42294
42535
  const resolvedExitCode = exitCode ?? 1;
@@ -42319,63 +42560,9 @@ async function stopContainer({ logger, containerId, runner }) {
42319
42560
  });
42320
42561
  }
42321
42562
 
42322
- // ../cli/logger/lib/LogLevel.js
42323
- var LogLevel2 = {
42324
- Trace: "trace",
42325
- Debug: "debug",
42326
- Info: "info",
42327
- Warn: "warn",
42328
- Error: "error"
42329
- };
42330
- var LOG_LEVELS = Object.values(LogLevel2);
42331
-
42332
- // ../cli/logger/lib/createLogger.js
42333
- var LoggerImpl = class {
42334
- log;
42335
- enabled;
42336
- constructor(log, enabled = true) {
42337
- this.log = log;
42338
- this.enabled = enabled;
42339
- }
42340
- disable() {
42341
- this.enabled = false;
42342
- }
42343
- enable() {
42344
- this.enabled = true;
42345
- }
42346
- debug(...args) {
42347
- if (this.enabled) {
42348
- this.log(LogLevel2.Debug, ...args);
42349
- }
42350
- }
42351
- info(...args) {
42352
- if (this.enabled) {
42353
- this.log(LogLevel2.Info, ...args);
42354
- }
42355
- }
42356
- warn(...args) {
42357
- if (this.enabled) {
42358
- this.log(LogLevel2.Warn, ...args);
42359
- }
42360
- }
42361
- error(...args) {
42362
- if (this.enabled) {
42363
- this.log(LogLevel2.Error, ...args);
42364
- }
42365
- }
42366
- trace(...args) {
42367
- if (this.enabled) {
42368
- this.log(LogLevel2.Trace, ...args);
42369
- }
42370
- }
42371
- };
42372
- function createLogger4(log) {
42373
- return new LoggerImpl(log);
42374
- }
42375
-
42376
42563
  // src/pipeline/steps/VerificationStep.ts
42377
42564
  var import_fs15 = require("fs");
42378
- var import_path21 = require("path");
42565
+ var import_path24 = require("path");
42379
42566
  var CONTAINER_WORKSPACE_PATH = "/workspace";
42380
42567
  var VERIFY_SCRIPT_RELATIVE_PATH = ".fern/verify.sh";
42381
42568
  var VERIFY_SCRIPT_CONTAINER_PATH = `${CONTAINER_WORKSPACE_PATH}/${VERIFY_SCRIPT_RELATIVE_PATH}`;
@@ -42391,7 +42578,7 @@ var VerificationStep = class extends BaseStep {
42391
42578
  generatorVersions;
42392
42579
  name = "verify";
42393
42580
  async execute(_context) {
42394
- const verifyScriptHostPath = (0, import_path21.join)(this.outputDir, VERIFY_SCRIPT_RELATIVE_PATH);
42581
+ const verifyScriptHostPath = (0, import_path24.join)(this.outputDir, VERIFY_SCRIPT_RELATIVE_PATH);
42395
42582
  if (!(0, import_fs15.existsSync)(verifyScriptHostPath)) {
42396
42583
  return { executed: true, success: true, skipped: true };
42397
42584
  }
@@ -42664,7 +42851,7 @@ void yargs_default(hideBin(process.argv)).scriptName(process.env.CLI_NAME ?? "ge
42664
42851
  const readmeConfig = await loadReadmeConfig({
42665
42852
  absolutePathToConfig: resolve(wd, argv.config)
42666
42853
  });
42667
- const originalReadmeContent = argv.originalReadme != null ? await (0, import_promises16.readFile)(argv.originalReadme, "utf8") : void 0;
42854
+ const originalReadmeContent = argv.originalReadme != null ? await (0, import_promises18.readFile)(argv.originalReadme, "utf8") : void 0;
42668
42855
  await generateReadmeToStream({
42669
42856
  originalReadmeContent,
42670
42857
  readmeConfig,
@@ -42792,7 +42979,7 @@ void yargs_default(hideBin(process.argv)).scriptName(process.env.CLI_NAME ?? "ge
42792
42979
  try {
42793
42980
  const wd = cwd();
42794
42981
  const configValue = argv.config;
42795
- const configContent = configValue === STDIN_MARKER ? await readStdin() : configValue.trimStart().startsWith("{") ? configValue : await (0, import_promises16.readFile)(resolve(wd, configValue), "utf-8");
42982
+ const configContent = configValue === STDIN_MARKER ? await readStdin() : configValue.trimStart().startsWith("{") ? configValue : await (0, import_promises18.readFile)(resolve(wd, configValue), "utf-8");
42796
42983
  let config;
42797
42984
  try {
42798
42985
  config = JSON.parse(configContent);
@@ -43088,7 +43275,7 @@ async function createWriteStream(outputPath) {
43088
43275
  }
43089
43276
  async function createWriteStreamFromFile(filepath) {
43090
43277
  if (!await doesPathExist(filepath)) {
43091
- await (0, import_promises16.mkdir)(import_path22.default.dirname(filepath), { recursive: true });
43278
+ await (0, import_promises18.mkdir)(import_path25.default.dirname(filepath), { recursive: true });
43092
43279
  }
43093
43280
  return import_fs16.default.createWriteStream(filepath);
43094
43281
  }