@fern-api/generator-cli 0.9.28 → 0.9.29

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 +1614 -1365
  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.1/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.1/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
  *
@@ -24910,9 +24527,7 @@ var init_dist3 = __esm({
24910
24527
  continue;
24911
24528
  }
24912
24529
  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));
24530
+ const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
24916
24531
  if (sensitiveFiles.length > 0) {
24917
24532
  this.warnings.push(
24918
24533
  `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 +24548,7 @@ var init_dist3 = __esm({
24933
24548
  if (files.length === 0) {
24934
24549
  continue;
24935
24550
  }
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
- }
24551
+ const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
24942
24552
  newPatches.push({
24943
24553
  id: `patch-${commit.sha.slice(0, 8)}`,
24944
24554
  content_hash: contentHash,
@@ -25026,7 +24636,7 @@ var init_dist3 = __esm({
25026
24636
  *
25027
24637
  * This ensures content hashes match across the no-patches and normal-
25028
24638
  * regeneration flows, which store formatPatch and git-diff formats
25029
- * respectively (FER-9850, D5-D9).
24639
+ * respectively.
25030
24640
  */
25031
24641
  computeContentHash(patchContent) {
25032
24642
  const lines = patchContent.split("\n");
@@ -25036,8 +24646,8 @@ var init_dist3 = __esm({
25036
24646
  return `sha256:${(0, import_crypto.createHash)("sha256").update(normalized).digest("hex")}`;
25037
24647
  }
25038
24648
  /**
25039
- * FER-9805 — Drop patches whose files were transiently created and then
25040
- * deleted within the current linear detection window, and are absent from
24649
+ * Drop patches whose files were transiently created and then deleted
24650
+ * within the current linear detection window, and are absent from
25041
24651
  * HEAD at the end of the window.
25042
24652
  *
25043
24653
  * Rationale: on a merge commit, createMergeCompositePatch already diffs
@@ -25062,8 +24672,8 @@ var init_dist3 = __esm({
25062
24672
  * single files from a multi-file patch would require a follow-up that
25063
24673
  * regenerates the patch content via `git format-patch -- <files>`. No
25064
24674
  * 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.
24675
+ * preserves today's behaviour. Revisit if a future adversarial test
24676
+ * demands per-file stripping.
25067
24677
  */
25068
24678
  async collapseNetZeroFiles(patches) {
25069
24679
  if (patches.length === 0) return patches;
@@ -25124,12 +24734,10 @@ var init_dist3 = __esm({
25124
24734
  firstParent,
25125
24735
  mergeCommit.sha
25126
24736
  ]);
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) {
24737
+ const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
24738
+ if (sensitiveFiles.length > 0) {
25131
24739
  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.`
24740
+ `Sensitive file(s) excluded from merge patch for commit ${mergeCommit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
25133
24741
  );
25134
24742
  }
25135
24743
  if (files.length === 0) return null;
@@ -25145,12 +24753,7 @@ var init_dist3 = __esm({
25145
24753
  if (lock.patches.some((p2) => p2.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
25146
24754
  return null;
25147
24755
  }
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
- }
24756
+ const theirsSnapshot = await capturePatchSnapshot(this.git, files, mergeCommit.sha);
25154
24757
  return {
25155
24758
  id: `patch-merge-${mergeCommit.sha.slice(0, 8)}`,
25156
24759
  content_hash: contentHash,
@@ -25174,13 +24777,24 @@ var init_dist3 = __esm({
25174
24777
  if (!diffBase) {
25175
24778
  return this.detectPatchesViaCommitScan();
25176
24779
  }
24780
+ const lockForGuard = this.lockManager.read();
24781
+ const knownGenerations = new Set(lockForGuard.generations.map((g) => g.commit_sha));
24782
+ let resolvedBaseGeneration = commitKnownMissing ? diffBase : lastGen.commit_sha;
24783
+ if (commitKnownMissing && !knownGenerations.has(diffBase)) {
24784
+ const fallback = lockForGuard.current_generation ?? lockForGuard.generations[lockForGuard.generations.length - 1]?.commit_sha;
24785
+ if (!fallback) {
24786
+ this.warnings.push(
24787
+ `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.`
24788
+ );
24789
+ return { patches: [], revertedPatchIds: [] };
24790
+ }
24791
+ resolvedBaseGeneration = fallback;
24792
+ }
25177
24793
  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) {
24794
+ const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
24795
+ if (sensitiveFiles.length > 0) {
25182
24796
  this.warnings.push(
25183
- `Sensitive file(s) excluded from composite patch: ${sensitiveTreeFiles.join(", ")}. These files will not be tracked by replay.`
24797
+ `Sensitive file(s) excluded from composite patch: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
25184
24798
  );
25185
24799
  }
25186
24800
  if (files.length === 0) return { patches: [], revertedPatchIds: [] };
@@ -25192,21 +24806,19 @@ var init_dist3 = __esm({
25192
24806
  return { patches: [], revertedPatchIds: [] };
25193
24807
  }
25194
24808
  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
- }
24809
+ const theirsSnapshot = await capturePatchSnapshot(this.git, files, "HEAD");
25201
24810
  const compositePatch = {
25202
24811
  id: `patch-composite-${headSha.slice(0, 8)}`,
25203
24812
  content_hash: contentHash,
25204
24813
  original_commit: headSha,
25205
24814
  original_message: "Customer customizations (composite)",
25206
24815
  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,
24816
+ // Anchor `base_generation` on a SHA the applicator's
24817
+ // `resolveBaseGeneration` can find — always a member of
24818
+ // `lock.generations`. When `diffBase` is a recorded
24819
+ // generation, use it directly; otherwise the guard above
24820
+ // has already mapped it onto `current_generation`.
24821
+ base_generation: resolvedBaseGeneration,
25210
24822
  files,
25211
24823
  patch_content: diff,
25212
24824
  ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
@@ -25224,6 +24836,8 @@ var init_dist3 = __esm({
25224
24836
  */
25225
24837
  async detectPatchesViaCommitScan() {
25226
24838
  const lock = this.lockManager.read();
24839
+ const knownGenerations = new Set(lock.generations.map((g) => g.commit_sha));
24840
+ const fallbackAnchor = lock.current_generation ?? lock.generations[lock.generations.length - 1]?.commit_sha;
25227
24841
  const log = await this.git.exec([
25228
24842
  "log",
25229
24843
  "--max-count=200",
@@ -25265,12 +24879,10 @@ var init_dist3 = __esm({
25265
24879
  continue;
25266
24880
  }
25267
24881
  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) {
24882
+ const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
24883
+ if (sensitiveFiles.length > 0) {
25272
24884
  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.`
24885
+ `Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
25274
24886
  );
25275
24887
  if (files.length > 0) {
25276
24888
  if (parents.length === 0) {
@@ -25292,19 +24904,25 @@ var init_dist3 = __esm({
25292
24904
  continue;
25293
24905
  }
25294
24906
  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;
24907
+ let baseGeneration;
24908
+ if (knownGenerations.has(parentSha)) {
24909
+ baseGeneration = parentSha;
24910
+ } else if (fallbackAnchor) {
24911
+ baseGeneration = fallbackAnchor;
24912
+ } else {
24913
+ this.warnings.push(
24914
+ `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.`
24915
+ );
24916
+ continue;
25300
24917
  }
24918
+ const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
25301
24919
  newPatches.push({
25302
24920
  id: `patch-${commit.sha.slice(0, 8)}`,
25303
24921
  content_hash: contentHash,
25304
24922
  original_commit: commit.sha,
25305
24923
  original_message: commit.message,
25306
24924
  original_author: `${commit.authorName} <${commit.authorEmail}>`,
25307
- base_generation: parentSha,
24925
+ base_generation: baseGeneration,
25308
24926
  files,
25309
24927
  patch_content: patchContent,
25310
24928
  ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {}
@@ -25349,6 +24967,541 @@ var init_dist3 = __esm({
25349
24967
  return lock.generations.find((g) => g.commit_sha === lock.current_generation);
25350
24968
  }
25351
24969
  };
24970
+ MergeAccumulator = class {
24971
+ constructor(mode, sharedFiles) {
24972
+ this.mode = mode;
24973
+ this.sharedFiles = sharedFiles;
24974
+ }
24975
+ entries = /* @__PURE__ */ new Map();
24976
+ /** Was this file touched by 2+ patches in the current `applyPatches` invocation? */
24977
+ isShared(path5) {
24978
+ return this.sharedFiles.has(path5);
24979
+ }
24980
+ /** Has any prior patch already recorded a result for this path? */
24981
+ has(path5) {
24982
+ return this.entries.has(path5);
24983
+ }
24984
+ /** Look up the accumulated content + base generation for this path. */
24985
+ lookup(path5) {
24986
+ return this.entries.get(path5);
24987
+ }
24988
+ /**
24989
+ * True if any of the patch's files have a prior accumulator entry — in
24990
+ * which case the fast path (`git apply --3way`) cannot honor prior
24991
+ * patches' contributions and we must route through the slow path.
24992
+ */
24993
+ shouldSkipFastPath(patch) {
24994
+ return patch.files.some((f) => this.entries.has(f));
24995
+ }
24996
+ /** Record a successful (clean) merge result. */
24997
+ recordMerge(path5, content, baseGeneration) {
24998
+ this.entries.set(path5, { content, baseGeneration });
24999
+ }
25000
+ /**
25001
+ * Record a conflicted merge result. Populates only in `"resolve"` mode
25002
+ * — replay mode keeps the accumulator clean of marker-laden content
25003
+ * since markers will be stripped from disk after the apply loop.
25004
+ */
25005
+ recordConflict(path5, content, baseGeneration) {
25006
+ if (this.mode === "resolve") {
25007
+ this.entries.set(path5, { content, baseGeneration });
25008
+ }
25009
+ }
25010
+ /**
25011
+ * Defensive copy for test introspection. Used by invariant I2 to assert
25012
+ * that after every applied patch, the accumulator has an entry for each
25013
+ * file the patch touched (see `__tests__/invariants/i2-accumulator-correctness.ts`).
25014
+ *
25015
+ * @internal
25016
+ */
25017
+ snapshot() {
25018
+ const copy = /* @__PURE__ */ new Map();
25019
+ for (const [path5, entry] of this.entries) {
25020
+ copy.set(path5, { content: entry.content, baseGeneration: entry.baseGeneration });
25021
+ }
25022
+ return copy;
25023
+ }
25024
+ };
25025
+ CONFLICT_OPENER = "<<<<<<< Generated";
25026
+ CONFLICT_SEPARATOR = "=======";
25027
+ CONFLICT_CLOSER = ">>>>>>> Your customization";
25028
+ ReplayApplicator = class {
25029
+ git;
25030
+ lockManager;
25031
+ outputDir;
25032
+ renameCache = /* @__PURE__ */ new Map();
25033
+ treeExistsCache = /* @__PURE__ */ new Map();
25034
+ /**
25035
+ * Inter-patch THEIRS coordination. Created fresh in each `applyPatches`
25036
+ * invocation with the run's apply mode + precomputed shared-file set.
25037
+ * See `src/accumulator/MergeAccumulator.ts` for the full contract.
25038
+ *
25039
+ * Initialized to an empty default in the constructor so type narrowing
25040
+ * works for callers that read it before any `applyPatches` invocation
25041
+ * (e.g., the `getAccumulatorSnapshot` test accessor).
25042
+ */
25043
+ accumulator = new MergeAccumulator("replay", /* @__PURE__ */ new Set());
25044
+ constructor(git, lockManager, outputDir) {
25045
+ this.git = git;
25046
+ this.lockManager = lockManager;
25047
+ this.outputDir = outputDir;
25048
+ }
25049
+ /**
25050
+ * Resolve the GenerationRecord for a patch's base_generation.
25051
+ * Falls back to constructing an ad-hoc record from the commit's tree
25052
+ * when base_generation isn't a tracked generation (commit-scan patches).
25053
+ */
25054
+ async resolveBaseGeneration(baseGeneration) {
25055
+ const gen = this.lockManager.getGeneration(baseGeneration);
25056
+ if (gen) return gen;
25057
+ try {
25058
+ const treeHash = await this.git.getTreeHash(baseGeneration);
25059
+ return {
25060
+ commit_sha: baseGeneration,
25061
+ tree_hash: treeHash,
25062
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25063
+ cli_version: "unknown",
25064
+ generator_versions: {}
25065
+ };
25066
+ } catch {
25067
+ return void 0;
25068
+ }
25069
+ }
25070
+ /**
25071
+ * @internal Test-only.
25072
+ * Returns a defensive copy of the inter-patch accumulator. Used by
25073
+ * invariant I2 to assert that after every applied patch, the
25074
+ * accumulator has an entry for each file the patch touched, and the
25075
+ * entry's content matches on-disk.
25076
+ */
25077
+ getAccumulatorSnapshot() {
25078
+ return this.accumulator.snapshot();
25079
+ }
25080
+ /**
25081
+ * Apply all patches, returning results for each.
25082
+ * Skips patches that match exclude patterns in replay.yml
25083
+ *
25084
+ * `applyMode` controls the post-conflict marker strategy:
25085
+ * - `"replay"` (default): strip conflict markers from disk between
25086
+ * iterations whenever a later patch touches the same file. Lets the
25087
+ * follow-up patch's `git apply --3way` see clean OURS content,
25088
+ * which is what the silent-loss fix (PR #73) relies on. The replay
25089
+ * pipeline calls `revertConflictingFiles` after the loop to clean
25090
+ * any markers that survive.
25091
+ * - `"resolve"`: keep markers on disk. The resolve command needs the
25092
+ * customer to see them. Slow-path 3-way merge naturally preserves
25093
+ * A's markers and B's clean writes when their regions don't overlap;
25094
+ * when they do overlap, the customer gets nested markers and
25095
+ * resolves manually. Either way they aren't silently dropped.
25096
+ */
25097
+ async applyPatches(patches, opts) {
25098
+ const applyMode = opts?.applyMode ?? "replay";
25099
+ const fileFreq = /* @__PURE__ */ new Map();
25100
+ for (const p2 of patches) {
25101
+ for (const f of p2.files) {
25102
+ fileFreq.set(f, (fileFreq.get(f) ?? 0) + 1);
25103
+ }
25104
+ }
25105
+ const sharedFiles = new Set(
25106
+ Array.from(fileFreq.entries()).filter(([, n]) => n > 1).map(([f]) => f)
25107
+ );
25108
+ this.accumulator = new MergeAccumulator(applyMode, sharedFiles);
25109
+ const results = [];
25110
+ for (let i2 = 0; i2 < patches.length; i2++) {
25111
+ const patch = patches[i2];
25112
+ if (this.isExcluded(patch)) {
25113
+ results.push({
25114
+ patch,
25115
+ status: "skipped",
25116
+ method: "git-am"
25117
+ });
25118
+ continue;
25119
+ }
25120
+ const result = await this.applyPatchWithFallback(patch);
25121
+ results.push(result);
25122
+ if (applyMode !== "resolve" && result.status === "conflict" && result.fileResults) {
25123
+ const laterFiles = /* @__PURE__ */ new Set();
25124
+ for (let j2 = i2 + 1; j2 < patches.length; j2++) {
25125
+ for (const f of patches[j2].files) {
25126
+ laterFiles.add(f);
25127
+ }
25128
+ }
25129
+ const resolvedToOriginal = /* @__PURE__ */ new Map();
25130
+ if (result.resolvedFiles) {
25131
+ for (const [orig, resolved] of Object.entries(result.resolvedFiles)) {
25132
+ resolvedToOriginal.set(resolved, orig);
25133
+ }
25134
+ }
25135
+ for (const fileResult of result.fileResults) {
25136
+ if (fileResult.status !== "conflict") continue;
25137
+ const originalPath = resolvedToOriginal.get(fileResult.file) ?? fileResult.file;
25138
+ if (laterFiles.has(fileResult.file) || laterFiles.has(originalPath)) {
25139
+ const filePath = (0, import_path10.join)(this.outputDir, fileResult.file);
25140
+ try {
25141
+ const content = await (0, import_promises4.readFile)(filePath, "utf-8");
25142
+ const stripped = stripConflictMarkers(content);
25143
+ await (0, import_promises4.writeFile)(filePath, stripped);
25144
+ } catch {
25145
+ }
25146
+ }
25147
+ }
25148
+ }
25149
+ }
25150
+ return results;
25151
+ }
25152
+ /**
25153
+ * Populate accumulator after git apply succeeds, AND collect per-file
25154
+ * results including any "dropped context lines" — lines that were
25155
+ * present in BASE and THEIRS (unchanged context) but absent from the
25156
+ * final on-disk state because OURS deleted them. This is the fast-path
25157
+ * counterpart to ThreeWayMerge.computeDroppedContextLines used by the
25158
+ * 3-way slow path; both must surface the same warning.
25159
+ */
25160
+ async populateAccumulatorForPatch(patch, baseGen, currentTreeHash) {
25161
+ const fileResults = [];
25162
+ if (!baseGen) return fileResults;
25163
+ const tempDir = await (0, import_promises4.mkdtemp)((0, import_path10.join)((0, import_os2.tmpdir)(), "replay-acc-"));
25164
+ const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
25165
+ const tempGit = new GitClient2(tempDir);
25166
+ await tempGit.exec(["init"]);
25167
+ await tempGit.exec(["config", "user.email", "replay@fern.com"]);
25168
+ await tempGit.exec(["config", "user.name", "Fern Replay"]);
25169
+ try {
25170
+ for (const filePath of patch.files) {
25171
+ if (isBinaryFile(filePath)) continue;
25172
+ const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
25173
+ const base = await this.git.showFile(baseGen.tree_hash, filePath);
25174
+ const snapshotForFile = patch.theirs_snapshot?.[resolvedPath] ?? patch.theirs_snapshot?.[filePath];
25175
+ const fileIsShared = this.accumulator.isShared(resolvedPath) || this.accumulator.isShared(filePath);
25176
+ const theirs = !fileIsShared && snapshotForFile != null ? snapshotForFile : await this.applyPatchToContent(base, patch.patch_content, filePath, tempGit, tempDir);
25177
+ const finalOnDisk = await (0, import_promises4.readFile)((0, import_path10.join)(this.outputDir, resolvedPath), "utf-8").catch(() => null);
25178
+ const effectiveTheirs = theirs ?? finalOnDisk;
25179
+ if (effectiveTheirs != null) {
25180
+ this.accumulator.recordMerge(resolvedPath, effectiveTheirs, patch.base_generation);
25181
+ }
25182
+ if (base != null && effectiveTheirs != null && finalOnDisk != null) {
25183
+ const dropped = computeDroppedContextLinesForFile(base, effectiveTheirs, finalOnDisk);
25184
+ if (dropped.length > 0) {
25185
+ fileResults.push({
25186
+ file: resolvedPath,
25187
+ status: "merged",
25188
+ droppedContextLines: dropped
25189
+ });
25190
+ }
25191
+ }
25192
+ }
25193
+ } finally {
25194
+ await (0, import_promises4.rm)(tempDir, { recursive: true }).catch(() => {
25195
+ });
25196
+ }
25197
+ return fileResults;
25198
+ }
25199
+ async applyPatchWithFallback(patch) {
25200
+ const baseGen = await this.resolveBaseGeneration(patch.base_generation);
25201
+ const lock = this.lockManager.read();
25202
+ const currentGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
25203
+ const currentTreeHash = currentGen?.tree_hash ?? baseGen?.tree_hash ?? "";
25204
+ const needsAccumulation = await Promise.all(
25205
+ patch.files.map(async (f) => {
25206
+ if (!baseGen) return false;
25207
+ const resolved = await this.resolveFilePath(f, baseGen.tree_hash, currentTreeHash);
25208
+ return this.accumulator.has(resolved);
25209
+ })
25210
+ ).then((results) => results.some(Boolean));
25211
+ const resolvedFiles = {};
25212
+ for (const filePath of patch.files) {
25213
+ const resolvedPath = baseGen ? await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash) : filePath;
25214
+ if (resolvedPath !== filePath) {
25215
+ resolvedFiles[filePath] = resolvedPath;
25216
+ }
25217
+ }
25218
+ const patchIsRenameAware = /^rename from /m.test(patch.patch_content);
25219
+ const hasGeneratorRename = Object.keys(resolvedFiles).length > 0 && !patchIsRenameAware;
25220
+ if (!needsAccumulation && !hasGeneratorRename) {
25221
+ const snapshots = /* @__PURE__ */ new Map();
25222
+ for (const filePath of patch.files) {
25223
+ const fullPath = (0, import_path10.join)(this.outputDir, filePath);
25224
+ snapshots.set(filePath, await (0, import_promises4.readFile)(fullPath, "utf-8").catch(() => null));
25225
+ }
25226
+ try {
25227
+ await this.git.execWithInput(["apply", "--3way"], patch.patch_content);
25228
+ const fastPathFileResults = await this.populateAccumulatorForPatch(patch, baseGen, currentTreeHash);
25229
+ return {
25230
+ patch,
25231
+ status: "applied",
25232
+ method: "git-am",
25233
+ ...fastPathFileResults.length > 0 && { fileResults: fastPathFileResults },
25234
+ ...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
25235
+ };
25236
+ } catch {
25237
+ for (const [filePath, content] of snapshots) {
25238
+ const fullPath = (0, import_path10.join)(this.outputDir, filePath);
25239
+ if (content != null) {
25240
+ await (0, import_promises4.writeFile)(fullPath, content);
25241
+ } else {
25242
+ await (0, import_promises4.unlink)(fullPath).catch(() => {
25243
+ });
25244
+ }
25245
+ }
25246
+ }
25247
+ }
25248
+ return this.applyWithThreeWayMerge(patch);
25249
+ }
25250
+ async applyWithThreeWayMerge(patch) {
25251
+ const fileResults = [];
25252
+ const resolvedFiles = {};
25253
+ const tempDir = await (0, import_promises4.mkdtemp)((0, import_path10.join)((0, import_os2.tmpdir)(), "replay-"));
25254
+ const { GitClient: GitClient2 } = await Promise.resolve().then(() => (init_GitClient(), GitClient_exports));
25255
+ const tempGit = new GitClient2(tempDir);
25256
+ await tempGit.exec(["init"]);
25257
+ await tempGit.exec(["config", "user.email", "replay@fern.com"]);
25258
+ await tempGit.exec(["config", "user.name", "Fern Replay"]);
25259
+ try {
25260
+ for (const filePath of patch.files) {
25261
+ if (isBinaryFile(filePath)) {
25262
+ fileResults.push({
25263
+ file: filePath,
25264
+ status: "skipped",
25265
+ reason: "binary-file"
25266
+ });
25267
+ continue;
25268
+ }
25269
+ const result = await this.mergeFile(patch, filePath, tempGit, tempDir);
25270
+ if (result.file !== filePath) {
25271
+ resolvedFiles[filePath] = result.file;
25272
+ }
25273
+ fileResults.push(result);
25274
+ }
25275
+ } finally {
25276
+ await (0, import_promises4.rm)(tempDir, { recursive: true }).catch(() => {
25277
+ });
25278
+ }
25279
+ const conflictFiles = fileResults.filter((r2) => r2.status === "conflict");
25280
+ const hasConflicts = conflictFiles.length > 0;
25281
+ const conflictReason = hasConflicts ? conflictFiles.some((f) => f.conflictReason === "base-generation-mismatch") ? "base-generation-mismatch" : conflictFiles[0]?.conflictReason : void 0;
25282
+ return {
25283
+ patch,
25284
+ status: hasConflicts ? "conflict" : "applied",
25285
+ method: "3way-merge",
25286
+ fileResults,
25287
+ conflictReason,
25288
+ ...Object.keys(resolvedFiles).length > 0 && { resolvedFiles }
25289
+ };
25290
+ }
25291
+ async mergeFile(patch, filePath, tempGit, tempDir) {
25292
+ try {
25293
+ const inputs = await resolveInputs({
25294
+ patch,
25295
+ filePath,
25296
+ git: this.git,
25297
+ lockManager: this.lockManager,
25298
+ outputDir: this.outputDir,
25299
+ isTreeReachable: (h2) => this.isTreeReachable(h2),
25300
+ resolveBaseGeneration: (s) => this.resolveBaseGeneration(s),
25301
+ resolveFilePath: (f, b3, c3) => this.resolveFilePath(f, b3, c3),
25302
+ extractRenameSource: (p2, f) => this.extractRenameSource(p2, f),
25303
+ extractFileDiff: (p2, f) => this.extractFileDiff(p2, f)
25304
+ });
25305
+ if (inputs.earlyExit) {
25306
+ return { file: filePath, ...inputs.earlyExit };
25307
+ }
25308
+ const plan = await buildMergePlan({
25309
+ inputs,
25310
+ patch,
25311
+ filePath,
25312
+ accumulator: this.accumulator,
25313
+ tempGit,
25314
+ tempDir,
25315
+ applyPatchToContent: (b3, p2, f, g, d, s) => this.applyPatchToContent(b3, p2, f, g, d, s),
25316
+ isPatchAlreadyApplied: (p2, f, c3, g, d) => this.isPatchAlreadyApplied(p2, f, c3, g, d)
25317
+ });
25318
+ return await runMergeAndRecord({
25319
+ plan,
25320
+ inputs,
25321
+ patch,
25322
+ accumulator: this.accumulator
25323
+ });
25324
+ } catch (error) {
25325
+ return {
25326
+ file: filePath,
25327
+ status: "skipped",
25328
+ reason: `error: ${error instanceof Error ? error.message : String(error)}`
25329
+ };
25330
+ }
25331
+ }
25332
+ async isTreeReachable(treeHash) {
25333
+ let result = this.treeExistsCache.get(treeHash);
25334
+ if (result === void 0) {
25335
+ result = await this.git.treeExists(treeHash);
25336
+ this.treeExistsCache.set(treeHash, result);
25337
+ }
25338
+ return result;
25339
+ }
25340
+ isExcluded(patch) {
25341
+ const config = this.lockManager.getCustomizationsConfig();
25342
+ if (!config.exclude) return false;
25343
+ return patch.files.some((file) => config.exclude.some((pattern) => minimatch(file, pattern)));
25344
+ }
25345
+ async resolveFilePath(filePath, baseTreeHash, currentTreeHash) {
25346
+ const config = this.lockManager.getCustomizationsConfig();
25347
+ if (config.moves) {
25348
+ for (const move of config.moves) {
25349
+ if (minimatch(filePath, move.from) || filePath === move.from) {
25350
+ if (filePath === move.from) {
25351
+ return move.to;
25352
+ }
25353
+ const fromBase = move.from.replace(/\*\*.*$/, "");
25354
+ const toBase = move.to.replace(/\*\*.*$/, "");
25355
+ if (filePath.startsWith(fromBase)) {
25356
+ return toBase + filePath.slice(fromBase.length);
25357
+ }
25358
+ }
25359
+ }
25360
+ }
25361
+ const cacheKey = `${baseTreeHash}:${currentTreeHash}`;
25362
+ let renames = this.renameCache.get(cacheKey);
25363
+ if (!renames) {
25364
+ renames = await this.git.detectRenames(baseTreeHash, currentTreeHash);
25365
+ this.renameCache.set(cacheKey, renames);
25366
+ }
25367
+ const gitRename = renames.find((r2) => r2.from === filePath);
25368
+ if (gitRename) {
25369
+ return gitRename.to;
25370
+ }
25371
+ return filePath;
25372
+ }
25373
+ async applyPatchToContent(base, patchContent, filePath, tempGit, tempDir, sourceFilePath) {
25374
+ if (!base) {
25375
+ return this.extractNewFileFromPatch(patchContent, filePath);
25376
+ }
25377
+ const fileDiff = this.extractFileDiff(patchContent, filePath);
25378
+ if (!fileDiff) return null;
25379
+ try {
25380
+ if (sourceFilePath) {
25381
+ const tempSourcePath = (0, import_path10.join)(tempDir, sourceFilePath);
25382
+ await (0, import_promises4.mkdir)((0, import_path10.dirname)(tempSourcePath), { recursive: true });
25383
+ await (0, import_promises4.writeFile)(tempSourcePath, base);
25384
+ await tempGit.exec(["add", sourceFilePath]);
25385
+ await tempGit.exec([
25386
+ "commit",
25387
+ "-m",
25388
+ `base for rename ${sourceFilePath} -> ${filePath}`,
25389
+ "--allow-empty"
25390
+ ]);
25391
+ await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
25392
+ const tempTargetPath = (0, import_path10.join)(tempDir, filePath);
25393
+ return await (0, import_promises4.readFile)(tempTargetPath, "utf-8");
25394
+ }
25395
+ const tempFilePath = (0, import_path10.join)(tempDir, filePath);
25396
+ await (0, import_promises4.mkdir)((0, import_path10.dirname)(tempFilePath), { recursive: true });
25397
+ await (0, import_promises4.writeFile)(tempFilePath, base);
25398
+ await tempGit.exec(["add", filePath]);
25399
+ await tempGit.exec(["commit", "-m", `base for ${filePath}`, "--allow-empty"]);
25400
+ await tempGit.execWithInput(["apply", "--allow-empty"], fileDiff);
25401
+ return await (0, import_promises4.readFile)(tempFilePath, "utf-8");
25402
+ } catch {
25403
+ return null;
25404
+ }
25405
+ }
25406
+ /**
25407
+ * Detects whether a patch's additions are already present in `content`.
25408
+ * Uses `git apply --reverse --check` — if the reverse patch applies cleanly,
25409
+ * the forward patch is effectively a no-op (its +lines are already there and
25410
+ * its -lines are already gone). Used to distinguish "patch already applied"
25411
+ * from "patch base mismatch" after a forward-apply fails.
25412
+ */
25413
+ async isPatchAlreadyApplied(patchContent, filePath, content, tempGit, tempDir) {
25414
+ const fileDiff = this.extractFileDiff(patchContent, filePath);
25415
+ if (!fileDiff) return false;
25416
+ try {
25417
+ const tempFilePath = (0, import_path10.join)(tempDir, filePath);
25418
+ await (0, import_promises4.mkdir)((0, import_path10.dirname)(tempFilePath), { recursive: true });
25419
+ await (0, import_promises4.writeFile)(tempFilePath, content);
25420
+ await tempGit.exec(["add", filePath]);
25421
+ await tempGit.exec(["commit", "-m", `check already-applied ${filePath}`, "--allow-empty"]);
25422
+ await tempGit.execWithInput(["apply", "--reverse", "--check", "--allow-empty"], fileDiff);
25423
+ return true;
25424
+ } catch {
25425
+ return false;
25426
+ }
25427
+ }
25428
+ extractFileDiff(patchContent, filePath) {
25429
+ const lines = patchContent.split("\n");
25430
+ const diffLines = [];
25431
+ let inTargetFile = false;
25432
+ for (const line of lines) {
25433
+ if (line.startsWith("diff --git")) {
25434
+ if (inTargetFile) {
25435
+ break;
25436
+ }
25437
+ if (isDiffLineForFile(line, filePath)) {
25438
+ inTargetFile = true;
25439
+ diffLines.push(line);
25440
+ }
25441
+ continue;
25442
+ }
25443
+ if (inTargetFile) {
25444
+ diffLines.push(line);
25445
+ }
25446
+ }
25447
+ return diffLines.length > 0 ? diffLines.join("\n") + "\n" : null;
25448
+ }
25449
+ extractRenameSource(patchContent, targetFilePath) {
25450
+ const lines = patchContent.split("\n");
25451
+ let inTargetFile = false;
25452
+ for (const line of lines) {
25453
+ if (line.startsWith("diff --git")) {
25454
+ if (inTargetFile) break;
25455
+ inTargetFile = isDiffLineForFile(line, targetFilePath);
25456
+ continue;
25457
+ }
25458
+ if (!inTargetFile) continue;
25459
+ if (line.startsWith("@@")) break;
25460
+ if (line.startsWith("rename from ")) {
25461
+ return line.slice("rename from ".length);
25462
+ }
25463
+ }
25464
+ return null;
25465
+ }
25466
+ extractNewFileFromPatch(patchContent, filePath) {
25467
+ const lines = patchContent.split("\n");
25468
+ const addedLines = [];
25469
+ let inTargetFile = false;
25470
+ let inHunk = false;
25471
+ let isNewFile = false;
25472
+ let noTrailingNewline = false;
25473
+ for (const line of lines) {
25474
+ if (line.startsWith("diff --git")) {
25475
+ if (inTargetFile) break;
25476
+ inTargetFile = isDiffLineForFile(line, filePath);
25477
+ inHunk = false;
25478
+ isNewFile = false;
25479
+ continue;
25480
+ }
25481
+ if (!inTargetFile) continue;
25482
+ if (!inHunk) {
25483
+ if (line === "--- /dev/null" || line.startsWith("new file mode")) {
25484
+ isNewFile = true;
25485
+ }
25486
+ }
25487
+ if (line.startsWith("@@")) {
25488
+ if (!isNewFile) return null;
25489
+ inHunk = true;
25490
+ continue;
25491
+ }
25492
+ if (!inHunk) continue;
25493
+ if (line === "\") {
25494
+ noTrailingNewline = true;
25495
+ continue;
25496
+ }
25497
+ if (line.startsWith("+") && !line.startsWith("+++")) {
25498
+ addedLines.push(line.slice(1));
25499
+ }
25500
+ }
25501
+ if (addedLines.length === 0) return null;
25502
+ return addedLines.join("\n") + (noTrailingNewline ? "" : "\n");
25503
+ }
25504
+ };
25352
25505
  ReplayCommitter = class {
25353
25506
  git;
25354
25507
  outputDir;
@@ -25452,210 +25605,66 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25452
25605
  };
25453
25606
  init_GitClient();
25454
25607
  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);
25608
+ FileOwnership = class {
25609
+ constructor(git) {
25491
25610
  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
- }
25498
- /**
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.
25506
- *
25507
- * Callers may land additional commits on HEAD between `prepareReplay` and
25508
- * `applyPreparedReplay` (e.g., `[fern-autoversion]`).
25509
- */
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
25611
  }
25526
25612
  /**
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.
25613
+ * Canonical resolution. Returns true if `file` is user-owned per:
25531
25614
  *
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.
25615
+ * 1. `patch.user_owned === true` — authoritative, persisted flag.
25616
+ * 2. `file === ".fernignore"` — always user-owned.
25617
+ * 3. File matches a `.fernignore` pattern (via minimatch).
25618
+ * 4. Patch content shows `--- /dev/null` for this file (legacy-safe
25619
+ * signal for patches predating `user_owned` or whose flag was lost).
25620
+ * 5. Tree-based check against `patch.base_generation` when reachable
25621
+ * AND distinct from `currentGenSha`. Unreachable base → conservative
25622
+ * user-owned (silent-absorption safety net), with a one-time warning
25623
+ * per base SHA.
25624
+ * 6. Final fallback: check `currentGenSha` when no usable base exists
25625
+ * (legacy one-gen lockfile).
25534
25626
  */
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
- }
25627
+ async resolveCanonical(ctx) {
25628
+ const { file, originalFileName, patch, currentGenSha, fernignorePatterns, warnedGens, warnings } = ctx;
25629
+ if (patch.user_owned) return true;
25630
+ if (file === ".fernignore") return true;
25631
+ if (fernignorePatterns.some((p2) => file === p2 || minimatch(file, p2))) return true;
25632
+ if (fileIsCreationInPatchContent(patch.patch_content, originalFileName ?? file)) {
25633
+ return true;
25594
25634
  }
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);
25635
+ const base = patch.base_generation;
25636
+ if (base && base !== currentGenSha) {
25637
+ const reachable = await this.git.commitExists(base) || await this.git.treeExists(base);
25638
+ if (!reachable) {
25639
+ if (!warnedGens.has(base)) {
25640
+ warnedGens.add(base);
25641
+ warnings.push(
25642
+ `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.`
25643
+ );
25607
25644
  }
25645
+ return true;
25608
25646
  }
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;
25647
+ return await this.git.showFile(base, originalFileName ?? file) === null;
25629
25648
  }
25649
+ return await this.git.showFile(currentGenSha, originalFileName ?? file) === null;
25630
25650
  }
25631
- firstGenerationReport() {
25632
- return {
25633
- flow: "first-generation",
25634
- patchesDetected: 0,
25635
- patchesApplied: 0,
25636
- patchesWithConflicts: 0,
25637
- patchesSkipped: 0,
25638
- conflicts: []
25639
- };
25651
+ };
25652
+ FirstGenerationFlow = class {
25653
+ constructor(service) {
25654
+ this.service = service;
25640
25655
  }
25641
- async _prepareFirstGeneration(options) {
25656
+ async prepare(options) {
25642
25657
  if (options?.dryRun) {
25643
- const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
25658
+ const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
25644
25659
  return {
25645
25660
  flow: "first-generation",
25646
25661
  previousGenerationSha: null,
25647
25662
  currentGenerationSha: headSha,
25648
25663
  baseBranchHead: options.baseBranchHead ?? null,
25649
25664
  _prepared: {
25665
+ kind: "terminal",
25650
25666
  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
25667
+ preparedReport: firstGenerationReport()
25659
25668
  }
25660
25669
  };
25661
25670
  }
@@ -25664,17 +25673,17 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25664
25673
  generatorVersions: options.generatorVersions ?? {},
25665
25674
  baseBranchHead: options.baseBranchHead
25666
25675
  } : void 0;
25667
- await this.committer.commitGeneration("Initial SDK generation", commitOpts);
25668
- const genRecord = await this.committer.createGenerationRecord(commitOpts);
25669
- this.lockManager.initializeInMemory(genRecord);
25676
+ await this.service.committer.commitGeneration("Initial SDK generation", commitOpts);
25677
+ const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
25678
+ this.service.lockManager.initializeInMemory(genRecord);
25670
25679
  return {
25671
25680
  flow: "first-generation",
25672
25681
  previousGenerationSha: null,
25673
25682
  currentGenerationSha: genRecord.commit_sha,
25674
25683
  baseBranchHead: options?.baseBranchHead ?? null,
25675
25684
  _prepared: {
25685
+ kind: "active",
25676
25686
  flow: "first-generation",
25677
- terminal: false,
25678
25687
  patchesToApply: [],
25679
25688
  newPatchIds: /* @__PURE__ */ new Set(),
25680
25689
  detectorWarnings: [],
@@ -25684,47 +25693,45 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25684
25693
  }
25685
25694
  };
25686
25695
  }
25687
- async _applyFirstGeneration(_prep) {
25688
- this.lockManager.save();
25689
- return this.firstGenerationReport();
25696
+ async apply(_prep) {
25697
+ this.service.lockManager.save();
25698
+ return firstGenerationReport();
25690
25699
  }
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) {
25700
+ };
25701
+ SkipApplicationFlow = class {
25702
+ constructor(service) {
25703
+ this.service = service;
25704
+ }
25705
+ async prepare(options) {
25699
25706
  const commitOpts = options ? {
25700
25707
  cliVersion: options.cliVersion ?? "unknown",
25701
25708
  generatorVersions: options.generatorVersions ?? {},
25702
25709
  baseBranchHead: options.baseBranchHead
25703
25710
  } : void 0;
25704
- await this.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
25705
- await this.cleanupStaleConflictMarkers();
25706
- const genRecord = await this.committer.createGenerationRecord(commitOpts);
25711
+ await this.service.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
25712
+ await this.service.cleanupStaleConflictMarkers();
25713
+ const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
25707
25714
  let previousGenerationSha = null;
25708
25715
  try {
25709
- const lock = this.lockManager.read();
25716
+ const lock = this.service.lockManager.read();
25710
25717
  previousGenerationSha = lock.current_generation ?? null;
25711
- this.lockManager.addGeneration(genRecord);
25718
+ this.service.lockManager.addGeneration(genRecord);
25712
25719
  } catch (error) {
25713
25720
  if (error instanceof LockfileNotFoundError) {
25714
- this.lockManager.initializeInMemory(genRecord);
25721
+ this.service.lockManager.initializeInMemory(genRecord);
25715
25722
  } else {
25716
25723
  throw error;
25717
25724
  }
25718
25725
  }
25719
- this.lockManager.setReplaySkippedAt((/* @__PURE__ */ new Date()).toISOString());
25726
+ this.service.lockManager.setReplaySkippedAt((/* @__PURE__ */ new Date()).toISOString());
25720
25727
  return {
25721
25728
  flow: "skip-application",
25722
25729
  previousGenerationSha,
25723
25730
  currentGenerationSha: genRecord.commit_sha,
25724
25731
  baseBranchHead: options?.baseBranchHead ?? null,
25725
25732
  _prepared: {
25733
+ kind: "active",
25726
25734
  flow: "skip-application",
25727
- terminal: false,
25728
25735
  patchesToApply: [],
25729
25736
  newPatchIds: /* @__PURE__ */ new Set(),
25730
25737
  detectorWarnings: [],
@@ -25734,13 +25741,13 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25734
25741
  }
25735
25742
  };
25736
25743
  }
25737
- async _applySkipApplication(_prep, options) {
25738
- this.lockManager.save();
25739
- const credentialWarnings = [...this.lockManager.warnings];
25744
+ async apply(_prep, options) {
25745
+ this.service.lockManager.save();
25746
+ const credentialWarnings = [...this.service.lockManager.warnings];
25740
25747
  if (!options?.stageOnly) {
25741
- await this.committer.commitReplay(0);
25748
+ await this.service.committer.commitReplay(0);
25742
25749
  } else {
25743
- await this.committer.stageAll();
25750
+ await this.service.committer.stageAll();
25744
25751
  }
25745
25752
  return {
25746
25753
  flow: "skip-application",
@@ -25752,13 +25759,18 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25752
25759
  warnings: credentialWarnings.length > 0 ? credentialWarnings : void 0
25753
25760
  };
25754
25761
  }
25755
- async _prepareNoPatchesRegeneration(options) {
25756
- const preLock = this.lockManager.read();
25762
+ };
25763
+ NoPatchesFlow = class {
25764
+ constructor(service) {
25765
+ this.service = service;
25766
+ }
25767
+ async prepare(options) {
25768
+ const preLock = this.service.lockManager.read();
25757
25769
  const previousGenerationSha = preLock.current_generation ?? null;
25758
- let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
25759
- const detectorWarnings = [...this.detector.warnings];
25770
+ let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
25771
+ const detectorWarnings = [...this.service.detector.warnings];
25760
25772
  if (options?.dryRun) {
25761
- const dryRunWarnings = [...detectorWarnings, ...this.warnings];
25773
+ const dryRunWarnings = [...detectorWarnings, ...this.service.warnings];
25762
25774
  const preparedReport = {
25763
25775
  flow: "no-patches",
25764
25776
  patchesDetected: newPatches.length,
@@ -25770,22 +25782,16 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25770
25782
  wouldApply: newPatches,
25771
25783
  warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0
25772
25784
  };
25773
- const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
25785
+ const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
25774
25786
  return {
25775
25787
  flow: "no-patches",
25776
25788
  previousGenerationSha,
25777
25789
  currentGenerationSha: headSha,
25778
25790
  baseBranchHead: options.baseBranchHead ?? null,
25779
25791
  _prepared: {
25792
+ kind: "terminal",
25780
25793
  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
25794
+ preparedReport
25789
25795
  }
25790
25796
  };
25791
25797
  }
@@ -25794,7 +25800,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25794
25800
  const uniqueFiles = [...new Set(newPatches.flatMap((p2) => p2.files))];
25795
25801
  const absorbedFiles = /* @__PURE__ */ new Set();
25796
25802
  for (const file of uniqueFiles) {
25797
- const diff = await this.git.exec(["diff", preCurrentGen, "HEAD", "--", file]).catch(() => null);
25803
+ const diff = await this.service.git.exec(["diff", preCurrentGen, "HEAD", "--", file]).catch(() => null);
25798
25804
  if (diff !== null && !diff.trim()) {
25799
25805
  absorbedFiles.add(file);
25800
25806
  }
@@ -25807,7 +25813,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25807
25813
  }
25808
25814
  for (const id of revertedPatchIds) {
25809
25815
  try {
25810
- this.lockManager.removePatch(id);
25816
+ this.service.lockManager.removePatch(id);
25811
25817
  } catch {
25812
25818
  }
25813
25819
  }
@@ -25816,18 +25822,18 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25816
25822
  generatorVersions: options.generatorVersions ?? {},
25817
25823
  baseBranchHead: options.baseBranchHead
25818
25824
  } : 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);
25825
+ await this.service.committer.commitGeneration("Update SDK", commitOpts);
25826
+ await this.service.cleanupStaleConflictMarkers();
25827
+ const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
25828
+ this.service.lockManager.addGeneration(genRecord);
25823
25829
  return {
25824
25830
  flow: "no-patches",
25825
25831
  previousGenerationSha,
25826
25832
  currentGenerationSha: genRecord.commit_sha,
25827
25833
  baseBranchHead: options?.baseBranchHead ?? null,
25828
25834
  _prepared: {
25835
+ kind: "active",
25829
25836
  flow: "no-patches",
25830
- terminal: false,
25831
25837
  patchesToApply: newPatches,
25832
25838
  newPatchIds: new Set(newPatches.map((p2) => p2.id)),
25833
25839
  detectorWarnings,
@@ -25837,41 +25843,42 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25837
25843
  }
25838
25844
  };
25839
25845
  }
25840
- async _applyNoPatchesRegeneration(prep, options) {
25846
+ async apply(prep, options) {
25847
+ assertActive(prep);
25841
25848
  const newPatches = prep._prepared.patchesToApply;
25842
25849
  const detectorWarnings = prep._prepared.detectorWarnings;
25843
25850
  const genSha = prep._prepared.genSha;
25844
25851
  let results = [];
25845
25852
  if (newPatches.length > 0) {
25846
- results = await this.applicator.applyPatches(newPatches);
25847
- this._lastApplyResults = results;
25848
- this.recordDroppedContextLineWarnings(results);
25849
- this.revertConflictingFiles(results);
25853
+ results = await this.service.applicator.applyPatches(newPatches);
25854
+ this.service._lastApplyResults = results;
25855
+ this.service.recordDroppedContextLineWarnings(results);
25856
+ this.service.revertConflictingFiles(results);
25850
25857
  for (const result of results) {
25851
25858
  if (result.status === "conflict") {
25852
25859
  result.patch.status = "unresolved";
25853
25860
  }
25854
25861
  }
25855
25862
  }
25856
- const rebaseCounts = await this.rebasePatches(results, genSha);
25863
+ const rebaseCounts = await this.service.rebasePatches(results, genSha);
25857
25864
  for (const patch of newPatches) {
25858
25865
  if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
25859
- this.lockManager.addPatch(patch);
25866
+ this.service.lockManager.addPatch(patch);
25860
25867
  }
25861
25868
  }
25862
- this.lockManager.save();
25863
- this.warnings.push(...this.lockManager.warnings);
25869
+ this.service.lockManager.save();
25870
+ this.service.warnings.push(...this.service.lockManager.warnings);
25864
25871
  if (newPatches.length > 0) {
25865
25872
  if (!options?.stageOnly) {
25866
25873
  const appliedCount = results.filter((r2) => r2.status === "applied").length;
25867
25874
  const buckets = computeCommitBuckets(results, rebaseCounts.absorbedPatchIds, newPatches);
25868
- await this.committer.commitReplay(appliedCount, newPatches, void 0, { buckets });
25875
+ await this.service.committer.commitReplay(appliedCount, newPatches, void 0, { buckets });
25869
25876
  } else {
25870
- await this.committer.stageAll();
25877
+ await this.service.committer.stageAll();
25871
25878
  }
25872
25879
  }
25873
- const warnings = [...detectorWarnings, ...this.warnings];
25874
- return this.buildReport(
25880
+ const warnings = [...detectorWarnings, ...this.service.warnings];
25881
+ return this.service.buildReport(
25875
25882
  "no-patches",
25876
25883
  newPatches,
25877
25884
  results,
@@ -25882,13 +25889,18 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25882
25889
  prep._prepared.revertedCount
25883
25890
  );
25884
25891
  }
25885
- async _prepareNormalRegeneration(options) {
25886
- const preLock = this.lockManager.read();
25892
+ };
25893
+ NormalRegenerationFlow = class {
25894
+ constructor(service) {
25895
+ this.service = service;
25896
+ }
25897
+ async prepare(options) {
25898
+ const preLock = this.service.lockManager.read();
25887
25899
  const previousGenerationSha = preLock.current_generation ?? null;
25888
25900
  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];
25901
+ const existingPatches2 = this.service.lockManager.getPatches();
25902
+ const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.service.detector.detectNewPatches();
25903
+ const warnings = [...this.service.detector.warnings, ...this.service.warnings];
25892
25904
  const allPatches2 = [...existingPatches2, ...newPatches2];
25893
25905
  const preparedReport = {
25894
25906
  flow: "normal-regeneration",
@@ -25901,48 +25913,42 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25901
25913
  wouldApply: allPatches2,
25902
25914
  warnings: warnings.length > 0 ? warnings : void 0
25903
25915
  };
25904
- const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
25916
+ const headSha = await this.service.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
25905
25917
  return {
25906
25918
  flow: "normal-regeneration",
25907
25919
  previousGenerationSha,
25908
25920
  currentGenerationSha: headSha,
25909
25921
  baseBranchHead: options.baseBranchHead ?? null,
25910
25922
  _prepared: {
25923
+ kind: "terminal",
25911
25924
  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
25925
+ preparedReport
25920
25926
  }
25921
25927
  };
25922
25928
  }
25923
- let existingPatches = this.lockManager.getPatches();
25929
+ let existingPatches = this.service.lockManager.getPatches();
25924
25930
  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));
25931
+ const preRebaseCounts = await this.service.preGenerationRebase(existingPatches);
25932
+ const postRebasePatchIds = new Set(this.service.lockManager.getPatches().map((p2) => p2.id));
25927
25933
  const removedByPreRebase = existingPatches.filter(
25928
25934
  (p2) => preRebasePatchIds.has(p2.id) && !postRebasePatchIds.has(p2.id)
25929
25935
  );
25930
- existingPatches = this.lockManager.getPatches();
25936
+ existingPatches = this.service.lockManager.getPatches();
25931
25937
  const seenHashes = /* @__PURE__ */ new Map();
25932
25938
  for (const p2 of existingPatches) {
25933
25939
  const priorPatchId = seenHashes.get(p2.content_hash);
25934
25940
  if (priorPatchId !== void 0) {
25935
- this.lockManager.removePatch(p2.id);
25936
- this.warnings.push(
25941
+ this.service.lockManager.removePatch(p2.id);
25942
+ this.service.warnings.push(
25937
25943
  `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
25944
  );
25939
25945
  } else {
25940
25946
  seenHashes.set(p2.content_hash, p2.id);
25941
25947
  }
25942
25948
  }
25943
- existingPatches = this.lockManager.getPatches();
25944
- let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
25945
- const detectorWarnings = [...this.detector.warnings];
25949
+ existingPatches = this.service.lockManager.getPatches();
25950
+ let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
25951
+ const detectorWarnings = [...this.service.detector.warnings];
25946
25952
  if (removedByPreRebase.length > 0) {
25947
25953
  const removedOriginalCommits = new Set(removedByPreRebase.map((p2) => p2.original_commit));
25948
25954
  const removedOriginalMessages = new Set(removedByPreRebase.map((p2) => p2.original_message));
@@ -25957,7 +25963,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25957
25963
  }
25958
25964
  for (const id of revertedPatchIds) {
25959
25965
  try {
25960
- this.lockManager.removePatch(id);
25966
+ this.service.lockManager.removePatch(id);
25961
25967
  } catch {
25962
25968
  }
25963
25969
  }
@@ -25969,18 +25975,18 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25969
25975
  generatorVersions: options.generatorVersions ?? {},
25970
25976
  baseBranchHead: options.baseBranchHead
25971
25977
  } : 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);
25978
+ await this.service.committer.commitGeneration("Update SDK", commitOpts);
25979
+ await this.service.cleanupStaleConflictMarkers();
25980
+ const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
25981
+ this.service.lockManager.addGeneration(genRecord);
25976
25982
  return {
25977
25983
  flow: "normal-regeneration",
25978
25984
  previousGenerationSha,
25979
25985
  currentGenerationSha: genRecord.commit_sha,
25980
25986
  baseBranchHead: options?.baseBranchHead ?? null,
25981
25987
  _prepared: {
25988
+ kind: "active",
25982
25989
  flow: "normal-regeneration",
25983
- terminal: false,
25984
25990
  patchesToApply: allPatches,
25985
25991
  newPatchIds: new Set(newPatches.map((p2) => p2.id)),
25986
25992
  preRebaseCounts,
@@ -25991,46 +25997,47 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25991
25997
  }
25992
25998
  };
25993
25999
  }
25994
- async _applyNormalRegeneration(prep, options) {
26000
+ async apply(prep, options) {
26001
+ assertActive(prep);
25995
26002
  const allPatches = prep._prepared.patchesToApply;
25996
26003
  const newPatchIds = prep._prepared.newPatchIds;
25997
26004
  const detectorWarnings = prep._prepared.detectorWarnings;
25998
26005
  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);
26006
+ const results = await this.service.applicator.applyPatches(allPatches);
26007
+ this.service._lastApplyResults = results;
26008
+ this.service.recordDroppedContextLineWarnings(results);
26009
+ this.service.revertConflictingFiles(results);
26003
26010
  for (const result of results) {
26004
26011
  if (result.status === "conflict") {
26005
26012
  result.patch.status = "unresolved";
26006
26013
  }
26007
26014
  }
26008
- const rebaseCounts = await this.rebasePatches(results, genSha);
26015
+ const rebaseCounts = await this.service.rebasePatches(results, genSha);
26009
26016
  const newPatches = allPatches.filter((p2) => newPatchIds.has(p2.id));
26010
26017
  for (const patch of newPatches) {
26011
26018
  if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
26012
- this.lockManager.addPatch(patch);
26019
+ this.service.lockManager.addPatch(patch);
26013
26020
  }
26014
26021
  }
26015
26022
  for (const result of results) {
26016
26023
  if (result.status === "conflict") {
26017
26024
  try {
26018
- this.lockManager.markPatchUnresolved(result.patch.id);
26025
+ this.service.lockManager.markPatchUnresolved(result.patch.id);
26019
26026
  } catch {
26020
26027
  }
26021
26028
  }
26022
26029
  }
26023
- this.lockManager.save();
26024
- this.warnings.push(...this.lockManager.warnings);
26030
+ this.service.lockManager.save();
26031
+ this.service.warnings.push(...this.service.lockManager.warnings);
26025
26032
  if (options?.stageOnly) {
26026
- await this.committer.stageAll();
26033
+ await this.service.committer.stageAll();
26027
26034
  } else {
26028
26035
  const appliedCount = results.filter((r2) => r2.status === "applied").length;
26029
26036
  const buckets = computeCommitBuckets(results, rebaseCounts.absorbedPatchIds, allPatches);
26030
- await this.committer.commitReplay(appliedCount, allPatches, void 0, { buckets });
26037
+ await this.service.committer.commitReplay(appliedCount, allPatches, void 0, { buckets });
26031
26038
  }
26032
- const warnings = [...detectorWarnings, ...this.warnings];
26033
- return this.buildReport(
26039
+ const warnings = [...detectorWarnings, ...this.service.warnings];
26040
+ return this.service.buildReport(
26034
26041
  "normal-regeneration",
26035
26042
  allPatches,
26036
26043
  results,
@@ -26041,10 +26048,204 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26041
26048
  prep._prepared.revertedCount
26042
26049
  );
26043
26050
  }
26051
+ };
26052
+ ReplayService = class {
26053
+ /** @internal Used by Flow implementations in `src/flows/`. */
26054
+ git;
26055
+ /** @internal Used by Flow implementations in `src/flows/`. */
26056
+ detector;
26057
+ /** @internal Used by Flow implementations in `src/flows/`. */
26058
+ applicator;
26059
+ /** @internal Used by Flow implementations in `src/flows/`. */
26060
+ committer;
26061
+ /** @internal Used by Flow implementations in `src/flows/`. */
26062
+ lockManager;
26063
+ /** @internal Used by Flow implementations in `src/flows/`. */
26064
+ fileOwnership;
26065
+ /** @internal Used by Flow implementations in `src/flows/`. */
26066
+ outputDir;
26067
+ /** @internal Test-only. Captures the last ReplayResult[] returned from applicator.applyPatches(). */
26068
+ _lastApplyResults = [];
26069
+ /**
26070
+ * Service-level warnings accumulated during a single runReplay() call.
26071
+ * Merged with `detector.warnings` into `ReplayReport.warnings` by each flow handler.
26072
+ * Populated by `FileOwnership.resolveCanonical` when a patch's base generation tree
26073
+ * is unreachable (shallow clone / aggressive `git gc --prune`) and we fall back to a
26074
+ * conservative heuristic. Cleared at the top of `runReplay()`.
26075
+ *
26076
+ * @internal Used by Flow implementations in `src/flows/`.
26077
+ */
26078
+ warnings = [];
26079
+ /**
26080
+ * @internal Test-only accessor.
26081
+ * Returns the ReplayApplicator instance used for the last run. Tests use this
26082
+ * to inspect the inter-patch accumulator after runReplay() completes (invariant I2).
26083
+ */
26084
+ get applicatorRef() {
26085
+ return this.applicator;
26086
+ }
26087
+ /**
26088
+ * @internal Test-only accessor.
26089
+ * Returns the ReplayResult[] from the most recent applyPatches() call. Tests use
26090
+ * this to filter applied-vs-conflicted-vs-absorbed patches for invariant
26091
+ * assertions. Returns an empty array if no patches have been applied yet.
26092
+ */
26093
+ getLastResults() {
26094
+ return this._lastApplyResults;
26095
+ }
26096
+ constructor(outputDir, _config) {
26097
+ const git = new GitClient(outputDir);
26098
+ this.git = git;
26099
+ this.outputDir = outputDir;
26100
+ this.lockManager = new LockfileManager(outputDir);
26101
+ this.detector = new ReplayDetector(git, this.lockManager, outputDir);
26102
+ this.applicator = new ReplayApplicator(git, this.lockManager, outputDir);
26103
+ this.committer = new ReplayCommitter(git, outputDir);
26104
+ this.fileOwnership = new FileOwnership(git);
26105
+ }
26106
+ /**
26107
+ * Phase 1 of the two-phase replay flow. Runs preGenerationRebase (when applicable),
26108
+ * detects new patches, and creates the `[fern-generated]` commit. Leaves HEAD at
26109
+ * the generation commit (or unchanged for dry-run).
26110
+ *
26111
+ * Does NOT apply patches — the returned handle must be passed to
26112
+ * `applyPreparedReplay()` to complete the run. No disk writes to the lockfile
26113
+ * happen here; lockfile persistence is deferred to phase 2.
26114
+ *
26115
+ * Callers may land additional commits on HEAD between `prepareReplay` and
26116
+ * `applyPreparedReplay` (e.g., `[fern-autoversion]`).
26117
+ */
26118
+ async prepareReplay(options) {
26119
+ this.warnings = [];
26120
+ this.detector.warnings.length = 0;
26121
+ return this.selectFlow(options).prepare(options);
26122
+ }
26123
+ /**
26124
+ * Phase 2 of the two-phase replay flow. Applies patches, rebases them against
26125
+ * the generation, saves the lockfile, and commits `[fern-replay]` (or stages
26126
+ * under `stageOnly`). Operates on HEAD at call time — mid-phase commits are
26127
+ * respected.
26128
+ *
26129
+ * For terminal handles (dry-run, first-gen has no patch work, skip-app does
26130
+ * its own post-commit logic), this returns the precomputed report.
26131
+ */
26132
+ async applyPreparedReplay(prep, options) {
26133
+ if (prep._prepared.kind === "terminal") {
26134
+ return prep._prepared.preparedReport;
26135
+ }
26136
+ return this.flowForKind(prep._prepared.flow).apply(prep, options);
26137
+ }
26138
+ /**
26139
+ * Construct the flow for the current run. `selectFlow` is called from
26140
+ * `prepareReplay` and decides based on options + lockfile state.
26141
+ */
26142
+ selectFlow(options) {
26143
+ if (options?.skipApplication) return new SkipApplicationFlow(this);
26144
+ return this.flowForKind(this.determineFlow());
26145
+ }
26146
+ /**
26147
+ * Construct a flow given its kind. Used by `applyPreparedReplay` to
26148
+ * route to the same kind that was selected in phase 1.
26149
+ */
26150
+ flowForKind(kind) {
26151
+ switch (kind) {
26152
+ case "first-generation":
26153
+ return new FirstGenerationFlow(this);
26154
+ case "no-patches":
26155
+ return new NoPatchesFlow(this);
26156
+ case "normal-regeneration":
26157
+ return new NormalRegenerationFlow(this);
26158
+ case "skip-application":
26159
+ return new SkipApplicationFlow(this);
26160
+ }
26161
+ }
26162
+ /**
26163
+ * Backwards-compatible atomic entry point. Composes `prepareReplay` + `applyPreparedReplay`.
26164
+ * Behavior is byte-identical to the pre-split implementation for all existing callers.
26165
+ */
26166
+ async runReplay(options) {
26167
+ const prep = await this.prepareReplay(options);
26168
+ return this.applyPreparedReplay(prep, options);
26169
+ }
26170
+ /**
26171
+ * Sync the lockfile after a divergent PR was squash-merged.
26172
+ * Call this BEFORE runReplay() when the CLI detects a merged divergent PR.
26173
+ *
26174
+ * After updating the generation record, re-detects customer patches via
26175
+ * tree diff between the generation tag (pure generation tree) and HEAD
26176
+ * (which includes customer customizations after squash merge). This ensures
26177
+ * patches survive the squash merge → regenerate cycle even when the lockfile
26178
+ * was restored from the base branch during conflict PR creation.
26179
+ */
26180
+ async syncFromDivergentMerge(generationCommitSha, options) {
26181
+ const treeHash = await this.git.getTreeHash(generationCommitSha);
26182
+ const timestamp = (await this.git.exec(["log", "-1", "--format=%aI", generationCommitSha])).trim();
26183
+ const record2 = {
26184
+ commit_sha: generationCommitSha,
26185
+ tree_hash: treeHash,
26186
+ timestamp,
26187
+ cli_version: options?.cliVersion ?? "unknown",
26188
+ generator_versions: options?.generatorVersions ?? {},
26189
+ base_branch_head: options?.baseBranchHead
26190
+ };
26191
+ let resolvedPatches;
26192
+ if (!this.lockManager.exists()) {
26193
+ this.lockManager.initializeInMemory(record2);
26194
+ } else {
26195
+ this.lockManager.read();
26196
+ const unresolvedPatches = [
26197
+ ...this.lockManager.getUnresolvedPatches(),
26198
+ ...this.lockManager.getResolvingPatches()
26199
+ ];
26200
+ resolvedPatches = this.lockManager.getPatches().filter((p2) => p2.status == null);
26201
+ this.lockManager.addGeneration(record2);
26202
+ this.lockManager.clearPatches();
26203
+ for (const patch of unresolvedPatches) {
26204
+ this.lockManager.addPatch(patch);
26205
+ }
26206
+ }
26207
+ try {
26208
+ const { patches: redetectedPatches } = await this.detector.detectNewPatches();
26209
+ if (redetectedPatches.length > 0) {
26210
+ const redetectedFiles = new Set(redetectedPatches.flatMap((p2) => p2.files));
26211
+ const currentPatches = this.lockManager.getPatches();
26212
+ for (const patch of currentPatches) {
26213
+ if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
26214
+ this.lockManager.removePatch(patch.id);
26215
+ }
26216
+ }
26217
+ for (const patch of redetectedPatches) {
26218
+ this.lockManager.addPatch(patch);
26219
+ }
26220
+ }
26221
+ } catch (error) {
26222
+ for (const patch of resolvedPatches ?? []) {
26223
+ this.lockManager.addPatch(patch);
26224
+ }
26225
+ this.detector.warnings.push(
26226
+ `Patch re-detection failed after divergent merge sync. ${(resolvedPatches ?? []).length} previously resolved patch(es) preserved. Error: ${error instanceof Error ? error.message : String(error)}`
26227
+ );
26228
+ }
26229
+ this.lockManager.save();
26230
+ this.detector.warnings.push(...this.lockManager.warnings);
26231
+ }
26232
+ determineFlow() {
26233
+ try {
26234
+ const lock = this.lockManager.read();
26235
+ return lock.patches.length === 0 ? "no-patches" : "normal-regeneration";
26236
+ } catch (error) {
26237
+ if (error instanceof LockfileNotFoundError) {
26238
+ return "first-generation";
26239
+ }
26240
+ throw error;
26241
+ }
26242
+ }
26044
26243
  /**
26045
26244
  * Rebase cleanly applied patches so they are relative to the current generation.
26046
26245
  * This prevents recurring conflicts on subsequent regenerations.
26047
26246
  * Returns the number of patches rebased.
26247
+ *
26248
+ * @internal Used by Flow implementations in `src/flows/`.
26048
26249
  */
26049
26250
  async rebasePatches(results, currentGenSha) {
26050
26251
  let absorbed = 0;
@@ -26054,6 +26255,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26054
26255
  const seenContentHashes = /* @__PURE__ */ new Map();
26055
26256
  const absorbedPatchIds = /* @__PURE__ */ new Set();
26056
26257
  const fernignorePatterns = this.readFernignorePatterns();
26258
+ const warnedGens = /* @__PURE__ */ new Set();
26057
26259
  for (const result of results) {
26058
26260
  if (result.resolvedFiles && Object.keys(result.resolvedFiles).length > 0) {
26059
26261
  const patch = result.patch;
@@ -26098,18 +26300,17 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26098
26300
  }
26099
26301
  }
26100
26302
  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
- })
26303
+ patch.files.map(
26304
+ (file) => this.fileOwnership.resolveCanonical({
26305
+ file,
26306
+ originalFileName: originalByResolved[file],
26307
+ patch,
26308
+ currentGenSha,
26309
+ fernignorePatterns,
26310
+ warnedGens,
26311
+ warnings: this.warnings
26312
+ })
26313
+ )
26113
26314
  );
26114
26315
  const hasProtectedFiles = patch.files.some(
26115
26316
  (file) => file === ".fernignore" || fernignorePatterns.some((p2) => file === p2 || minimatch(file, p2))
@@ -26236,7 +26437,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26236
26437
  for (const file of files) {
26237
26438
  if (isBinaryFile(file)) continue;
26238
26439
  try {
26239
- const content = await (0, import_promises5.readFile)((0, import_path10.join)(this.outputDir, file), "utf-8");
26440
+ const content = await (0, import_promises7.readFile)((0, import_path13.join)(this.outputDir, file), "utf-8");
26240
26441
  snapshot[file] = content;
26241
26442
  } catch {
26242
26443
  }
@@ -26244,10 +26445,16 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26244
26445
  return snapshot;
26245
26446
  }
26246
26447
  /**
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.
26448
+ * Detects autoversion contamination — returns true when a
26449
+ * `[fern-autoversion]` commit between `fromSha` and `toSha` touched any
26450
+ * of the patch's `files`. Re-classifies each grep match via
26451
+ * `isGenerationCommit` so a customer commit that merely quotes the
26452
+ * marker in its body doesn't false-positive.
26453
+ *
26454
+ * Used by preGenerationRebase to skip the customer-content rebuild for
26455
+ * patches whose files were last touched by an autoversion step — a
26456
+ * naive `git diff currentGen HEAD` there would capture the
26457
+ * placeholder→semver swap as customer customization.
26251
26458
  */
26252
26459
  async hasAutoversionContamination(fromSha, toSha, files) {
26253
26460
  if (files.length === 0) return false;
@@ -26276,10 +26483,15 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26276
26483
  return false;
26277
26484
  }
26278
26485
  /**
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.
26486
+ * Rebuild patch_content from `theirs_snapshot` (captured at detection
26487
+ * time, predates pipeline content) rather than a HEAD-relative diff.
26488
+ * Returns `null` when snapshot is missing or doesn't cover every file
26489
+ * in `patch.files` — caller leaves the patch unchanged.
26490
+ *
26491
+ * Used as the autoversion-contamination escape hatch: when the
26492
+ * HEAD-relative diff would capture pipeline content (autoversion's
26493
+ * placeholder swap) as customer customization, the snapshot path
26494
+ * preserves the customer's actual intent.
26283
26495
  */
26284
26496
  async computeSnapshotBasedPatchDiff(patch, currentGen) {
26285
26497
  if (!patch.theirs_snapshot) return null;
@@ -26327,7 +26539,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26327
26539
  * **Skips patches sharing files with other patches.** HEAD's content
26328
26540
  * for a file shared by N patches is cumulative across all of them, so
26329
26541
  * a HEAD-fallback would falsely encode other patches' contributions
26330
- * into one patch's snapshot — breaking FER-9525 cross-patch isolation
26542
+ * into one patch's snapshot — breaking cross-patch isolation
26331
26543
  * if the snapshot fallback later fires. Per-patch snapshots only
26332
26544
  * make sense when the patch owns its file. Multi-patch-same-file
26333
26545
  * legacy lockfiles keep using line-anchored reconstruction (the
@@ -26389,72 +26601,6 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26389
26601
  );
26390
26602
  }
26391
26603
  }
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
26604
  /**
26459
26605
  * For conflict patches with mixed results (some files merged, some conflicted),
26460
26606
  * check if the cleanly merged files were absorbed by the generator (empty diff).
@@ -26473,7 +26619,15 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26473
26619
  const warnedGens = /* @__PURE__ */ new Set();
26474
26620
  const generatorCleanFiles = [];
26475
26621
  for (const file of cleanFiles) {
26476
- if (await this.isFileUserOwned(file, patch, currentGenSha, fernignorePatterns, warnedGens)) {
26622
+ const userOwned = await this.fileOwnership.resolveCanonical({
26623
+ file,
26624
+ patch,
26625
+ currentGenSha,
26626
+ fernignorePatterns,
26627
+ warnedGens,
26628
+ warnings: this.warnings
26629
+ });
26630
+ if (userOwned) {
26477
26631
  continue;
26478
26632
  }
26479
26633
  generatorCleanFiles.push(file);
@@ -26502,6 +26656,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26502
26656
  * Pre-generation rebase: update patches using the customer's current state.
26503
26657
  * Called BEFORE commitGeneration() while HEAD has customer code.
26504
26658
  */
26659
+ /** @internal Used by Flow implementations in `src/flows/`. */
26505
26660
  async preGenerationRebase(patches) {
26506
26661
  const lock = this.lockManager.read();
26507
26662
  const currentGen = lock.current_generation;
@@ -26540,7 +26695,15 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26540
26695
  if (patch.user_owned) return true;
26541
26696
  if (patch.files.length === 0) return false;
26542
26697
  for (const file of patch.files) {
26543
- if (!await this.isFileUserOwned(file, patch, currentGen, fernignorePatterns, warnedGens)) {
26698
+ const userOwned = await this.fileOwnership.resolveCanonical({
26699
+ file,
26700
+ patch,
26701
+ currentGenSha: currentGen,
26702
+ fernignorePatterns,
26703
+ warnedGens,
26704
+ warnings: this.warnings
26705
+ });
26706
+ if (!userOwned) {
26544
26707
  return false;
26545
26708
  }
26546
26709
  }
@@ -26732,6 +26895,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26732
26895
  * relied on them. This is the "surface or preserve, never silently drop"
26733
26896
  * contract for legitimate-deletion cases.
26734
26897
  */
26898
+ /** @internal Used by Flow implementations in `src/flows/`. */
26735
26899
  recordDroppedContextLineWarnings(results) {
26736
26900
  const PREVIEW_COUNT = 5;
26737
26901
  for (const result of results) {
@@ -26754,12 +26918,13 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
26754
26918
  * After applyPatches(), strip conflict markers from conflicting files
26755
26919
  * so only clean content is committed. Keeps the Generated (OURS) side.
26756
26920
  */
26921
+ /** @internal Used by Flow implementations in `src/flows/`. */
26757
26922
  revertConflictingFiles(results) {
26758
26923
  for (const result of results) {
26759
26924
  if (result.status !== "conflict" || !result.fileResults) continue;
26760
26925
  for (const fileResult of result.fileResults) {
26761
26926
  if (fileResult.status !== "conflict") continue;
26762
- const filePath = (0, import_path10.join)(this.outputDir, fileResult.file);
26927
+ const filePath = (0, import_path13.join)(this.outputDir, fileResult.file);
26763
26928
  try {
26764
26929
  const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
26765
26930
  const stripped = stripConflictMarkers(content);
@@ -26775,6 +26940,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
26775
26940
  * Restores files to their clean generated state from HEAD.
26776
26941
  * Skips .fernignore-protected files to prevent overwriting user content.
26777
26942
  */
26943
+ /** @internal Used by Flow implementations in `src/flows/`. */
26778
26944
  async cleanupStaleConflictMarkers() {
26779
26945
  const markerFiles = await this.git.exec(["grep", "-l", "<<<<<<< Generated", "--", "."]).catch(() => "");
26780
26946
  const files = markerFiles.trim().split("\n").filter(Boolean);
@@ -26789,10 +26955,11 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
26789
26955
  }
26790
26956
  }
26791
26957
  readFernignorePatterns() {
26792
- const fernignorePath = (0, import_path10.join)(this.outputDir, ".fernignore");
26958
+ const fernignorePath = (0, import_path13.join)(this.outputDir, ".fernignore");
26793
26959
  if (!(0, import_fs7.existsSync)(fernignorePath)) return [];
26794
26960
  return (0, import_fs7.readFileSync)(fernignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
26795
26961
  }
26962
+ /** @internal Used by Flow implementations in `src/flows/`. */
26796
26963
  buildReport(flow, patches, results, options, warnings, rebaseCounts, preRebaseCounts, patchesReverted) {
26797
26964
  const conflictResults = results.filter((r2) => r2.status === "conflict");
26798
26965
  const conflictDetails = conflictResults.map((r2) => {
@@ -26847,10 +27014,10 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
26847
27014
  this.outputDir = outputDir;
26848
27015
  }
26849
27016
  fernignoreExists() {
26850
- return (0, import_fs8.existsSync)((0, import_path12.join)(this.outputDir, ".fernignore"));
27017
+ return (0, import_fs8.existsSync)((0, import_path15.join)(this.outputDir, ".fernignore"));
26851
27018
  }
26852
27019
  readFernignorePatterns() {
26853
- const fernignorePath = (0, import_path12.join)(this.outputDir, ".fernignore");
27020
+ const fernignorePath = (0, import_path15.join)(this.outputDir, ".fernignore");
26854
27021
  if (!(0, import_fs8.existsSync)(fernignorePath)) {
26855
27022
  return [];
26856
27023
  }
@@ -26959,7 +27126,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
26959
27126
  return results;
26960
27127
  }
26961
27128
  readFileContent(filePath) {
26962
- const fullPath = (0, import_path12.join)(this.outputDir, filePath);
27129
+ const fullPath = (0, import_path15.join)(this.outputDir, filePath);
26963
27130
  if (!(0, import_fs8.existsSync)(fullPath)) {
26964
27131
  return null;
26965
27132
  }
@@ -27024,7 +27191,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
27024
27191
  };
27025
27192
  }
27026
27193
  movePatternsToReplayYml(patterns) {
27027
- const replayYmlPath = (0, import_path12.join)(this.outputDir, ".fern", "replay.yml");
27194
+ const replayYmlPath = (0, import_path15.join)(this.outputDir, ".fern", "replay.yml");
27028
27195
  let config = {};
27029
27196
  if ((0, import_fs8.existsSync)(replayYmlPath)) {
27030
27197
  const content = (0, import_fs8.readFileSync)(replayYmlPath, "utf-8");
@@ -27033,7 +27200,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
27033
27200
  const existing = config.exclude ?? [];
27034
27201
  const merged = [.../* @__PURE__ */ new Set([...existing, ...patterns])];
27035
27202
  config.exclude = merged;
27036
- const dir = (0, import_path12.dirname)(replayYmlPath);
27203
+ const dir = (0, import_path15.dirname)(replayYmlPath);
27037
27204
  if (!(0, import_fs8.existsSync)(dir)) {
27038
27205
  (0, import_fs8.mkdirSync)(dir, { recursive: true });
27039
27206
  }
@@ -29639,8 +29806,8 @@ function resolve(...paths) {
29639
29806
 
29640
29807
  // src/cli.ts
29641
29808
  var import_fs16 = __toESM(require("fs"));
29642
- var import_promises16 = require("fs/promises");
29643
- var import_path22 = __toESM(require("path"));
29809
+ var import_promises18 = require("fs/promises");
29810
+ var import_path25 = __toESM(require("path"));
29644
29811
 
29645
29812
  // ../../node_modules/.pnpm/yargs@17.7.2/node_modules/yargs/build/lib/yerror.js
29646
29813
  var YError = class _YError extends Error {
@@ -31607,15 +31774,18 @@ function parseRepository(githubRepository) {
31607
31774
  githubRepository = githubRepository.slice(0, -4);
31608
31775
  }
31609
31776
  const parts = githubRepository.split("/");
31610
- if (parts.length === 2 && parts[0] != null && parts[1] != null) {
31777
+ if (parts.length === 2 && isNonEmptyPart(parts[0]) && isNonEmptyPart(parts[1])) {
31611
31778
  [owner, repo] = parts;
31612
- } else if (parts.length === 3 && parts[0] != null && parts[1] != null && parts[2] != null) {
31779
+ } else if (parts.length === 3 && isNonEmptyPart(parts[0]) && isNonEmptyPart(parts[1]) && isNonEmptyPart(parts[2])) {
31613
31780
  [remote, owner, repo] = parts;
31614
31781
  } else {
31615
31782
  throw new Error(`Failed to parse GitHub repository ${githubRepository}`);
31616
31783
  }
31617
31784
  return newRepositoryReference({ remote, owner, repo });
31618
31785
  }
31786
+ function isNonEmptyPart(part) {
31787
+ return part != null && part.length > 0;
31788
+ }
31619
31789
  function newRepositoryReference({ remote, owner, repo }) {
31620
31790
  const repoUrl = getRepoUrl({ remote, owner, repo });
31621
31791
  const cloneUrl = getCloneUrl({ remote, owner, repo });
@@ -39354,42 +39524,42 @@ function replayForget(params) {
39354
39524
  // src/replay/replay-init.ts
39355
39525
  init_dist3();
39356
39526
  var import_fs11 = require("fs");
39357
- var import_path16 = require("path");
39527
+ var import_path19 = require("path");
39358
39528
  var import_tmp_promise2 = __toESM(require_tmp_promise());
39359
39529
 
39360
39530
  // src/replay/fernignore.ts
39361
- var import_promises8 = require("fs/promises");
39362
- var import_path15 = require("path");
39531
+ var import_promises10 = require("fs/promises");
39532
+ var import_path18 = require("path");
39363
39533
  var REPLAY_FERNIGNORE_ENTRIES2 = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
39364
39534
  var GITATTRIBUTES_ENTRIES2 = [".fern/replay.lock linguist-generated=true"];
39365
39535
  async function ensureReplayFernignoreEntries(outputDir) {
39366
- const fernignorePath = (0, import_path15.join)(outputDir, ".fernignore");
39536
+ const fernignorePath = (0, import_path18.join)(outputDir, ".fernignore");
39367
39537
  let content = "";
39368
39538
  try {
39369
- content = await (0, import_promises8.readFile)(fernignorePath, "utf-8");
39539
+ content = await (0, import_promises10.readFile)(fernignorePath, "utf-8");
39370
39540
  } catch {
39371
39541
  }
39372
39542
  const lines = content.split("\n");
39373
39543
  const toAdd = REPLAY_FERNIGNORE_ENTRIES2.filter((entry) => !lines.some((line) => line.trim() === entry));
39374
39544
  if (toAdd.length > 0) {
39375
39545
  const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
39376
- await (0, import_promises8.writeFile)(fernignorePath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
39546
+ await (0, import_promises10.writeFile)(fernignorePath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
39377
39547
  return true;
39378
39548
  }
39379
39549
  return false;
39380
39550
  }
39381
39551
  async function ensureGitattributesEntries2(outputDir) {
39382
- const gitattributesPath = (0, import_path15.join)(outputDir, ".gitattributes");
39552
+ const gitattributesPath = (0, import_path18.join)(outputDir, ".gitattributes");
39383
39553
  let content = "";
39384
39554
  try {
39385
- content = await (0, import_promises8.readFile)(gitattributesPath, "utf-8");
39555
+ content = await (0, import_promises10.readFile)(gitattributesPath, "utf-8");
39386
39556
  } catch {
39387
39557
  }
39388
39558
  const lines = content.split("\n");
39389
39559
  const toAdd = GITATTRIBUTES_ENTRIES2.filter((entry) => !lines.some((line) => line.trim() === entry));
39390
39560
  if (toAdd.length > 0) {
39391
39561
  const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
39392
- await (0, import_promises8.writeFile)(gitattributesPath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
39562
+ await (0, import_promises10.writeFile)(gitattributesPath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
39393
39563
  }
39394
39564
  }
39395
39565
 
@@ -39416,7 +39586,7 @@ async function replayInit(params) {
39416
39586
  if (dryRun) {
39417
39587
  return { bootstrap: bootstrapResult, fernignoreEntries: [], gitattributesEntries: [] };
39418
39588
  }
39419
- const lockfilePath = (0, import_path16.join)(repoPath, ".fern", "replay.lock");
39589
+ const lockfilePath = (0, import_path19.join)(repoPath, ".fern", "replay.lock");
39420
39590
  const lockfileContent = (0, import_fs11.existsSync)(lockfilePath) ? (0, import_fs11.readFileSync)(lockfilePath, "utf-8") : void 0;
39421
39591
  return {
39422
39592
  bootstrap: bootstrapResult,
@@ -39539,7 +39709,7 @@ init_dist3();
39539
39709
  init_dist3();
39540
39710
  var import_child_process4 = require("child_process");
39541
39711
  var import_fs12 = require("fs");
39542
- var import_path17 = require("path");
39712
+ var import_path20 = require("path");
39543
39713
  async function replayPrepare(params, state) {
39544
39714
  const {
39545
39715
  outputDir,
@@ -39550,7 +39720,7 @@ async function replayPrepare(params, state) {
39550
39720
  skipApplication,
39551
39721
  logger
39552
39722
  } = params;
39553
- const lockfilePath = (0, import_path17.join)(outputDir, ".fern", "replay.lock");
39723
+ const lockfilePath = (0, import_path20.join)(outputDir, ".fern", "replay.lock");
39554
39724
  let autoBootstrapped = false;
39555
39725
  let bootstrapAttempted = false;
39556
39726
  if (!(0, import_fs12.existsSync)(lockfilePath)) {
@@ -39804,29 +39974,29 @@ function replayStatus(params) {
39804
39974
  }
39805
39975
 
39806
39976
  // src/configuration/loadGitHubConfig.ts
39807
- var import_promises9 = require("fs/promises");
39977
+ var import_promises11 = require("fs/promises");
39808
39978
  async function loadGitHubConfig({
39809
39979
  absolutePathToConfig
39810
39980
  }) {
39811
- const rawContents = await (0, import_promises9.readFile)(absolutePathToConfig, "utf8");
39981
+ const rawContents = await (0, import_promises11.readFile)(absolutePathToConfig, "utf8");
39812
39982
  return JSON.parse(rawContents);
39813
39983
  }
39814
39984
 
39815
39985
  // src/configuration/loadReadmeConfig.ts
39816
- var import_promises10 = require("fs/promises");
39986
+ var import_promises12 = require("fs/promises");
39817
39987
  async function loadReadmeConfig({
39818
39988
  absolutePathToConfig
39819
39989
  }) {
39820
- const rawContents = await (0, import_promises10.readFile)(absolutePathToConfig, "utf8");
39990
+ const rawContents = await (0, import_promises12.readFile)(absolutePathToConfig, "utf8");
39821
39991
  return JSON.parse(rawContents);
39822
39992
  }
39823
39993
 
39824
39994
  // src/configuration/loadReferenceConfig.ts
39825
- var import_promises11 = require("fs/promises");
39995
+ var import_promises13 = require("fs/promises");
39826
39996
  async function loadReferenceConfig({
39827
39997
  absolutePathToConfig
39828
39998
  }) {
39829
- const rawContents = await (0, import_promises11.readFile)(absolutePathToConfig, "utf8");
39999
+ const rawContents = await (0, import_promises13.readFile)(absolutePathToConfig, "utf8");
39830
40000
  return JSON.parse(rawContents);
39831
40001
  }
39832
40002
 
@@ -39853,8 +40023,8 @@ var FERN_BOT_LOGIN2 = "fern-api[bot]";
39853
40023
  // src/pipeline/steps/AutoVersionStep.ts
39854
40024
  var import_child_process5 = require("child_process");
39855
40025
  var import_fs14 = require("fs");
39856
- var import_promises13 = require("fs/promises");
39857
- var import_path19 = require("path");
40026
+ var import_promises15 = require("fs/promises");
40027
+ var import_path22 = require("path");
39858
40028
 
39859
40029
  // ../commons/logging-execa/lib/loggingExeca.js
39860
40030
  var import_execa = __toESM(require_execa(), 1);
@@ -39911,10 +40081,136 @@ async function loggingExeca(logger, executable, args = [], { doNotPipeOutput = f
39911
40081
  return result;
39912
40082
  }
39913
40083
 
40084
+ // ../cli/task-context/lib/CliError.js
40085
+ var CliError = class _CliError extends Error {
40086
+ code;
40087
+ docsLink;
40088
+ constructor({ message, code, docsLink }) {
40089
+ super(message);
40090
+ Object.setPrototypeOf(this, new.target.prototype);
40091
+ if (Error.captureStackTrace) {
40092
+ Error.captureStackTrace(this, this.constructor);
40093
+ }
40094
+ this.name = this.constructor.name;
40095
+ this.code = code;
40096
+ this.docsLink = docsLink;
40097
+ }
40098
+ static authRequired(message) {
40099
+ return new _CliError({
40100
+ message: message ?? "Authentication required. Please run 'fern login' or set the FERN_TOKEN environment variable.",
40101
+ code: _CliError.Code.AuthError
40102
+ });
40103
+ }
40104
+ static unauthorized(message) {
40105
+ return new _CliError({
40106
+ message: message ?? "Unauthorized. Please run 'fern auth login' or set the FERN_TOKEN environment variable.",
40107
+ code: _CliError.Code.AuthError
40108
+ });
40109
+ }
40110
+ static notFound(message) {
40111
+ return new _CliError({ message, code: _CliError.Code.ConfigError });
40112
+ }
40113
+ static badRequest(message) {
40114
+ return new _CliError({ message, code: _CliError.Code.NetworkError });
40115
+ }
40116
+ static validationError(message) {
40117
+ return new _CliError({ message, code: _CliError.Code.ValidationError });
40118
+ }
40119
+ static internalError(message) {
40120
+ return new _CliError({ message, code: _CliError.Code.InternalError });
40121
+ }
40122
+ };
40123
+ (function(CliError2) {
40124
+ CliError2.Code = {
40125
+ InternalError: "INTERNAL_ERROR",
40126
+ ResolutionError: "RESOLUTION_ERROR",
40127
+ IrConversionError: "IR_CONVERSION_ERROR",
40128
+ ContainerError: "CONTAINER_ERROR",
40129
+ VersionError: "VERSION_ERROR",
40130
+ ParseError: "PARSE_ERROR",
40131
+ EnvironmentError: "ENVIRONMENT_ERROR",
40132
+ ReferenceError: "REFERENCE_ERROR",
40133
+ ValidationError: "VALIDATION_ERROR",
40134
+ NetworkError: "NETWORK_ERROR",
40135
+ AuthError: "AUTH_ERROR",
40136
+ ConfigError: "CONFIG_ERROR",
40137
+ UserError: "USER_ERROR"
40138
+ };
40139
+ })(CliError || (CliError = {}));
40140
+ var SENTRY_REPORTABLE = {
40141
+ [CliError.Code.InternalError]: true,
40142
+ [CliError.Code.ResolutionError]: true,
40143
+ [CliError.Code.IrConversionError]: true,
40144
+ [CliError.Code.ContainerError]: false,
40145
+ [CliError.Code.VersionError]: false,
40146
+ [CliError.Code.ParseError]: false,
40147
+ [CliError.Code.EnvironmentError]: false,
40148
+ [CliError.Code.ReferenceError]: false,
40149
+ [CliError.Code.ValidationError]: false,
40150
+ [CliError.Code.NetworkError]: false,
40151
+ [CliError.Code.AuthError]: false,
40152
+ [CliError.Code.ConfigError]: false,
40153
+ [CliError.Code.UserError]: false
40154
+ };
40155
+
40156
+ // ../cli/logger/lib/LogLevel.js
40157
+ var LogLevel2 = {
40158
+ Trace: "trace",
40159
+ Debug: "debug",
40160
+ Info: "info",
40161
+ Warn: "warn",
40162
+ Error: "error"
40163
+ };
40164
+ var LOG_LEVELS = Object.values(LogLevel2);
40165
+
40166
+ // ../cli/logger/lib/createLogger.js
40167
+ var LoggerImpl = class {
40168
+ log;
40169
+ enabled;
40170
+ constructor(log, enabled = true) {
40171
+ this.log = log;
40172
+ this.enabled = enabled;
40173
+ }
40174
+ disable() {
40175
+ this.enabled = false;
40176
+ }
40177
+ enable() {
40178
+ this.enabled = true;
40179
+ }
40180
+ debug(...args) {
40181
+ if (this.enabled) {
40182
+ this.log(LogLevel2.Debug, ...args);
40183
+ }
40184
+ }
40185
+ info(...args) {
40186
+ if (this.enabled) {
40187
+ this.log(LogLevel2.Info, ...args);
40188
+ }
40189
+ }
40190
+ warn(...args) {
40191
+ if (this.enabled) {
40192
+ this.log(LogLevel2.Warn, ...args);
40193
+ }
40194
+ }
40195
+ error(...args) {
40196
+ if (this.enabled) {
40197
+ this.log(LogLevel2.Error, ...args);
40198
+ }
40199
+ }
40200
+ trace(...args) {
40201
+ if (this.enabled) {
40202
+ this.log(LogLevel2.Trace, ...args);
40203
+ }
40204
+ }
40205
+ };
40206
+ function createLogger4(log) {
40207
+ return new LoggerImpl(log);
40208
+ }
40209
+
39914
40210
  // src/autoversion/AutoVersioningService.ts
39915
40211
  var import_fs13 = require("fs");
39916
- var import_promises12 = require("fs/promises");
39917
- var import_path18 = require("path");
40212
+ var import_promises14 = require("fs/promises");
40213
+ var import_path21 = require("path");
39918
40214
  var import_semver = __toESM(require_semver2());
39919
40215
  var AutoVersioningException = class extends Error {
39920
40216
  /**
@@ -40577,10 +40873,17 @@ var AutoVersioningService = class _AutoVersioningService {
40577
40873
  } else {
40578
40874
  command = `find "${workingDirectory}" -type f -not -path "*/.git/*" -exec sed -i '' '${sedCommand}' {} +`;
40579
40875
  }
40580
- await loggingExeca(this.logger, "bash", ["-c", command], {
40581
- cwd: workingDirectory,
40582
- doNotPipeOutput: true
40583
- });
40876
+ try {
40877
+ await loggingExeca(this.logger, "bash", ["-c", command], {
40878
+ cwd: workingDirectory,
40879
+ doNotPipeOutput: true
40880
+ });
40881
+ } catch (error) {
40882
+ throw new CliError({
40883
+ message: `Failed to replace placeholder version in generated files: ${extractErrorMessage(error)}`,
40884
+ code: CliError.Code.UserError
40885
+ });
40886
+ }
40584
40887
  this.logger.debug("Placeholder version replaced successfully");
40585
40888
  }
40586
40889
  /**
@@ -40601,7 +40904,7 @@ var AutoVersioningService = class _AutoVersioningService {
40601
40904
  );
40602
40905
  return;
40603
40906
  }
40604
- const goModPath = (0, import_path18.join)(workingDirectory, "go.mod");
40907
+ const goModPath = (0, import_path21.join)(workingDirectory, "go.mod");
40605
40908
  if (!(0, import_fs13.existsSync)(goModPath)) {
40606
40909
  this.logger.debug(`No go.mod found in ${workingDirectory}, skipping Go module path update`);
40607
40910
  return;
@@ -40635,7 +40938,7 @@ var AutoVersioningService = class _AutoVersioningService {
40635
40938
  * Reads the module path from a go.mod file.
40636
40939
  */
40637
40940
  async readGoModulePath(goModPath) {
40638
- const content = await (0, import_promises12.readFile)(goModPath, "utf-8");
40941
+ const content = await (0, import_promises14.readFile)(goModPath, "utf-8");
40639
40942
  for (const line of content.split("\n")) {
40640
40943
  const trimmed2 = line.trim();
40641
40944
  if (trimmed2.startsWith("module ")) {
@@ -40648,7 +40951,7 @@ var AutoVersioningService = class _AutoVersioningService {
40648
40951
  * Adds a version suffix to the module line in go.mod.
40649
40952
  */
40650
40953
  async addSuffixToGoMod(goModPath, modulePath, suffix) {
40651
- const content = await (0, import_promises12.readFile)(goModPath, "utf-8");
40954
+ const content = await (0, import_promises14.readFile)(goModPath, "utf-8");
40652
40955
  const lines = content.split("\n");
40653
40956
  let changed = false;
40654
40957
  for (let i2 = 0; i2 < lines.length; i2++) {
@@ -40660,7 +40963,7 @@ var AutoVersioningService = class _AutoVersioningService {
40660
40963
  }
40661
40964
  }
40662
40965
  if (changed) {
40663
- await (0, import_promises12.writeFile)(goModPath, lines.join("\n"), "utf-8");
40966
+ await (0, import_promises14.writeFile)(goModPath, lines.join("\n"), "utf-8");
40664
40967
  this.logger.debug(`Updated go.mod: added ${suffix} to module path`);
40665
40968
  }
40666
40969
  }
@@ -40669,10 +40972,10 @@ var AutoVersioningService = class _AutoVersioningService {
40669
40972
  */
40670
40973
  async walkDirectory(dir, filter2) {
40671
40974
  const results = [];
40672
- const entries2 = await (0, import_promises12.readdir)(dir);
40975
+ const entries2 = await (0, import_promises14.readdir)(dir);
40673
40976
  for (const entry of entries2) {
40674
- const fullPath = (0, import_path18.join)(dir, entry);
40675
- const entryStat = await (0, import_promises12.stat)(fullPath);
40977
+ const fullPath = (0, import_path21.join)(dir, entry);
40978
+ const entryStat = await (0, import_promises14.stat)(fullPath);
40676
40979
  if (entryStat.isDirectory()) {
40677
40980
  if (entry === ".git" || entry === "vendor") {
40678
40981
  continue;
@@ -40689,7 +40992,7 @@ var AutoVersioningService = class _AutoVersioningService {
40689
40992
  * Adds a version suffix to import paths in all .go files in the repository.
40690
40993
  */
40691
40994
  async addSuffixToGoFiles(repoRoot, modulePath, suffix) {
40692
- const goFiles = await this.walkDirectory(repoRoot, (filePath) => (0, import_path18.extname)(filePath) === ".go");
40995
+ const goFiles = await this.walkDirectory(repoRoot, (filePath) => (0, import_path21.extname)(filePath) === ".go");
40693
40996
  for (const filePath of goFiles) {
40694
40997
  await this.addSuffixToGoFile(filePath, modulePath, suffix);
40695
40998
  }
@@ -40699,7 +41002,7 @@ var AutoVersioningService = class _AutoVersioningService {
40699
41002
  * Replaces occurrences of the module path with modulePath + suffix in import statements.
40700
41003
  */
40701
41004
  async addSuffixToGoFile(filePath, modulePath, suffix) {
40702
- const content = await (0, import_promises12.readFile)(filePath, "utf-8");
41005
+ const content = await (0, import_promises14.readFile)(filePath, "utf-8");
40703
41006
  const lines = content.split("\n");
40704
41007
  let changed = false;
40705
41008
  let inImportBlock = false;
@@ -40728,7 +41031,7 @@ var AutoVersioningService = class _AutoVersioningService {
40728
41031
  }
40729
41032
  }
40730
41033
  if (changed) {
40731
- await (0, import_promises12.writeFile)(filePath, lines.join("\n"), "utf-8");
41034
+ await (0, import_promises14.writeFile)(filePath, lines.join("\n"), "utf-8");
40732
41035
  this.logger.debug(`Updated Go imports in ${filePath}`);
40733
41036
  }
40734
41037
  }
@@ -41134,7 +41437,7 @@ ${body}` : `${COMMIT_MARKER} ${subject}`;
41134
41437
  }
41135
41438
  async prependChangelogEntry(params) {
41136
41439
  const { version: version2, entry } = params;
41137
- const changelogPath = (0, import_path19.join)(this.outputDir, "changelog.md");
41440
+ const changelogPath = (0, import_path22.join)(this.outputDir, "changelog.md");
41138
41441
  const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
41139
41442
  const header = `## [${version2}] - ${now}
41140
41443
  `;
@@ -41143,7 +41446,7 @@ ${body}` : `${COMMIT_MARKER} ${subject}`;
41143
41446
  `;
41144
41447
  let existing = "";
41145
41448
  if ((0, import_fs14.existsSync)(changelogPath)) {
41146
- existing = await (0, import_promises13.readFile)(changelogPath, "utf-8");
41449
+ existing = await (0, import_promises15.readFile)(changelogPath, "utf-8");
41147
41450
  }
41148
41451
  let output;
41149
41452
  if (existing.trim().length === 0) {
@@ -41160,7 +41463,7 @@ ${newBlock}${remainder}`;
41160
41463
  } else {
41161
41464
  output = `${newBlock}${existing}`;
41162
41465
  }
41163
- await (0, import_promises13.writeFile)(changelogPath, output, "utf-8");
41466
+ await (0, import_promises15.writeFile)(changelogPath, output, "utf-8");
41164
41467
  }
41165
41468
  brandMessage(message) {
41166
41469
  if (this.config.isWhitelabel) {
@@ -41383,8 +41686,8 @@ function tryRevParse(cwd2, rev) {
41383
41686
  }
41384
41687
 
41385
41688
  // src/pipeline/steps/GithubStep.ts
41386
- var import_promises14 = require("fs/promises");
41387
- var import_path20 = require("path");
41689
+ var import_promises16 = require("fs/promises");
41690
+ var import_path23 = require("path");
41388
41691
 
41389
41692
  // src/pipeline/github/createReplayBranch.ts
41390
41693
  async function createReplayBranch(repository, branchName, commitMessage, replayConflictInfo, logger) {
@@ -42024,13 +42327,13 @@ var GithubStep = class extends BaseStep {
42024
42327
  }
42025
42328
  async ensureFernignore() {
42026
42329
  this.logger.debug("Checking for .fernignore file...");
42027
- const fernignorePath = (0, import_path20.join)(this.outputDir, ".fernignore");
42330
+ const fernignorePath = (0, import_path23.join)(this.outputDir, ".fernignore");
42028
42331
  try {
42029
- await (0, import_promises14.access)(fernignorePath);
42332
+ await (0, import_promises16.access)(fernignorePath);
42030
42333
  this.logger.debug(".fernignore already exists");
42031
42334
  } catch {
42032
42335
  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");
42336
+ await (0, import_promises16.writeFile)(fernignorePath, "# Specify files that shouldn't be modified by Fern\n", "utf-8");
42034
42337
  }
42035
42338
  }
42036
42339
  deriveSkipCommit(replayResult) {
@@ -42241,7 +42544,7 @@ var ReplayStep = class extends BaseStep {
42241
42544
  };
42242
42545
 
42243
42546
  // ../cli/generation/local-generation/docker-utils/lib/runDocker.js
42244
- var import_promises15 = require("fs/promises");
42547
+ var import_promises17 = require("fs/promises");
42245
42548
  var import_tmp_promise3 = __toESM(require_tmp_promise(), 1);
42246
42549
  async function pullImage(imageName, runner, signal) {
42247
42550
  await loggingExeca(void 0, runner ?? "docker", ["pull", imageName], {
@@ -42288,7 +42591,7 @@ async function execInContainer({ logger, containerId, command, runner, writeLogs
42288
42591
  const logs = stdout + stderr;
42289
42592
  if (writeLogsToFile) {
42290
42593
  const tmpFile = await import_tmp_promise3.default.file();
42291
- await (0, import_promises15.writeFile)(tmpFile.path, logs);
42594
+ await (0, import_promises17.writeFile)(tmpFile.path, logs);
42292
42595
  logger.info(`Generator logs here: ${tmpFile.path}`);
42293
42596
  }
42294
42597
  const resolvedExitCode = exitCode ?? 1;
@@ -42319,63 +42622,9 @@ async function stopContainer({ logger, containerId, runner }) {
42319
42622
  });
42320
42623
  }
42321
42624
 
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
42625
  // src/pipeline/steps/VerificationStep.ts
42377
42626
  var import_fs15 = require("fs");
42378
- var import_path21 = require("path");
42627
+ var import_path24 = require("path");
42379
42628
  var CONTAINER_WORKSPACE_PATH = "/workspace";
42380
42629
  var VERIFY_SCRIPT_RELATIVE_PATH = ".fern/verify.sh";
42381
42630
  var VERIFY_SCRIPT_CONTAINER_PATH = `${CONTAINER_WORKSPACE_PATH}/${VERIFY_SCRIPT_RELATIVE_PATH}`;
@@ -42391,7 +42640,7 @@ var VerificationStep = class extends BaseStep {
42391
42640
  generatorVersions;
42392
42641
  name = "verify";
42393
42642
  async execute(_context) {
42394
- const verifyScriptHostPath = (0, import_path21.join)(this.outputDir, VERIFY_SCRIPT_RELATIVE_PATH);
42643
+ const verifyScriptHostPath = (0, import_path24.join)(this.outputDir, VERIFY_SCRIPT_RELATIVE_PATH);
42395
42644
  if (!(0, import_fs15.existsSync)(verifyScriptHostPath)) {
42396
42645
  return { executed: true, success: true, skipped: true };
42397
42646
  }
@@ -42664,7 +42913,7 @@ void yargs_default(hideBin(process.argv)).scriptName(process.env.CLI_NAME ?? "ge
42664
42913
  const readmeConfig = await loadReadmeConfig({
42665
42914
  absolutePathToConfig: resolve(wd, argv.config)
42666
42915
  });
42667
- const originalReadmeContent = argv.originalReadme != null ? await (0, import_promises16.readFile)(argv.originalReadme, "utf8") : void 0;
42916
+ const originalReadmeContent = argv.originalReadme != null ? await (0, import_promises18.readFile)(argv.originalReadme, "utf8") : void 0;
42668
42917
  await generateReadmeToStream({
42669
42918
  originalReadmeContent,
42670
42919
  readmeConfig,
@@ -42792,7 +43041,7 @@ void yargs_default(hideBin(process.argv)).scriptName(process.env.CLI_NAME ?? "ge
42792
43041
  try {
42793
43042
  const wd = cwd();
42794
43043
  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");
43044
+ const configContent = configValue === STDIN_MARKER ? await readStdin() : configValue.trimStart().startsWith("{") ? configValue : await (0, import_promises18.readFile)(resolve(wd, configValue), "utf-8");
42796
43045
  let config;
42797
43046
  try {
42798
43047
  config = JSON.parse(configContent);
@@ -43088,7 +43337,7 @@ async function createWriteStream(outputPath) {
43088
43337
  }
43089
43338
  async function createWriteStreamFromFile(filepath) {
43090
43339
  if (!await doesPathExist(filepath)) {
43091
- await (0, import_promises16.mkdir)(import_path22.default.dirname(filepath), { recursive: true });
43340
+ await (0, import_promises18.mkdir)(import_path25.default.dirname(filepath), { recursive: true });
43092
43341
  }
43093
43342
  return import_fs16.default.createWriteStream(filepath);
43094
43343
  }