@fern-api/generator-cli 0.9.32 → 0.9.34

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 +1877 -64
  2. package/dist/cli.js +364 -62
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -22126,7 +22126,7 @@ var init_diff3 = __esm({
22126
22126
  }
22127
22127
  });
22128
22128
 
22129
- // ../../node_modules/.pnpm/@fern-api+replay@0.16.0/node_modules/@fern-api/replay/dist/index.js
22129
+ // ../../node_modules/.pnpm/@fern-api+replay@0.16.2/node_modules/@fern-api/replay/dist/index.js
22130
22130
  var dist_exports = {};
22131
22131
  __export(dist_exports, {
22132
22132
  CREDENTIAL_PATTERNS: () => CREDENTIAL_PATTERNS,
@@ -22437,13 +22437,13 @@ function isSensitiveFile(filePath) {
22437
22437
  return SENSITIVE_FILE_PATTERNS.some((re) => re.test(filePath));
22438
22438
  }
22439
22439
  function scanForCredentials(content) {
22440
- const matches = [];
22440
+ const matches2 = [];
22441
22441
  for (const { name, re } of CREDENTIAL_PATTERNS) {
22442
22442
  if (re.test(content)) {
22443
- matches.push(name);
22443
+ matches2.push(name);
22444
22444
  }
22445
22445
  }
22446
- return matches;
22446
+ return matches2;
22447
22447
  }
22448
22448
  function isGenerationCommit(commit) {
22449
22449
  const isFernSupport = FERN_SUPPORT_NAMES.includes(commit.authorName);
@@ -22485,6 +22485,15 @@ function isBinaryFile(filePath) {
22485
22485
  const ext2 = (0, import_path9.extname)(filePath).toLowerCase();
22486
22486
  return BINARY_EXTENSIONS.has(ext2);
22487
22487
  }
22488
+ function matchesFernignorePattern(filePath, patterns) {
22489
+ if (patterns.length === 0) return false;
22490
+ return patterns.some((p2) => {
22491
+ if (filePath === p2) return true;
22492
+ if (minimatch(filePath, p2)) return true;
22493
+ if (!p2.includes("*") && !p2.includes("?") && filePath.startsWith(p2 + "/")) return true;
22494
+ return false;
22495
+ });
22496
+ }
22488
22497
  function parsePatchFileHeaders(patchContent) {
22489
22498
  const results = [];
22490
22499
  let currentPath = null;
@@ -22525,11 +22534,13 @@ async function capturePatchSnapshot(git, files, treeish) {
22525
22534
  }
22526
22535
  return snapshot;
22527
22536
  }
22528
- function filterInfrastructureAndSensitiveFiles(rawFilesOutput) {
22537
+ function filterInfrastructureAndSensitiveFiles(rawFilesOutput, fernignorePatterns) {
22529
22538
  const allFiles = rawFilesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
22530
22539
  const sensitiveFiles = allFiles.filter((f) => isSensitiveFile(f));
22531
- const files = allFiles.filter((f) => !isSensitiveFile(f));
22532
- return { files, sensitiveFiles };
22540
+ const nonSensitive = allFiles.filter((f) => !isSensitiveFile(f));
22541
+ const fernignoredFiles = nonSensitive.filter((f) => matchesFernignorePattern(f, fernignorePatterns));
22542
+ const files = nonSensitive.filter((f) => !matchesFernignorePattern(f, fernignorePatterns));
22543
+ return { files, sensitiveFiles, fernignoredFiles };
22533
22544
  }
22534
22545
  function threeWayMerge(base, ours, theirs) {
22535
22546
  const baseLines = base.split("\n");
@@ -23123,12 +23134,23 @@ async function computePerPatchDiff(patch, getCurrentGenContent, getWorkingTreeCo
23123
23134
  unlocatableFiles.push(file);
23124
23135
  continue;
23125
23136
  }
23126
- const locInCurrentGen = locInCurrentGenRaw.map(
23127
- (l) => resolveSpan(l, currentGenLines, "old")
23128
- );
23129
- const locInWorkingTree = locInWorkingTreeRaw.map(
23130
- (l) => resolveSpan(l, workingTreeLines, "new")
23131
- );
23137
+ const locInCurrentGen = [];
23138
+ const locInWorkingTree = [];
23139
+ let spanResolutionFailed = false;
23140
+ for (let i2 = 0; i2 < locInCurrentGenRaw.length; i2++) {
23141
+ const cg = resolveSpan(locInCurrentGenRaw[i2], currentGenLines, "old");
23142
+ const wt = resolveSpan(locInWorkingTreeRaw[i2], workingTreeLines, "new");
23143
+ if (cg === null || wt === null) {
23144
+ spanResolutionFailed = true;
23145
+ break;
23146
+ }
23147
+ locInCurrentGen.push(cg);
23148
+ locInWorkingTree.push(wt);
23149
+ }
23150
+ if (spanResolutionFailed) {
23151
+ unlocatableFiles.push(file);
23152
+ continue;
23153
+ }
23132
23154
  const overlay = overlayRegions(
23133
23155
  currentGenLines,
23134
23156
  locInCurrentGen,
@@ -23190,11 +23212,11 @@ function resolveSpan(loc, oursLines, prefer) {
23190
23212
  if (prefer === "old") {
23191
23213
  if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
23192
23214
  if (newFits) return { ...loc, oursSpan: hunk.newCount };
23193
- } else {
23194
- if (newFits) return { ...loc, oursSpan: hunk.newCount };
23195
- if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
23215
+ return loc;
23196
23216
  }
23197
- return loc;
23217
+ if (newFits) return { ...loc, oursSpan: hunk.newCount };
23218
+ if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
23219
+ return null;
23198
23220
  }
23199
23221
  function sequenceMatches(needle, haystack, offset) {
23200
23222
  if (offset + needle.length > haystack.length) return false;
@@ -23364,6 +23386,93 @@ function computeCommitBuckets(results, absorbedPatchIds, patchesPassedToCommit)
23364
23386
  }
23365
23387
  return { applied, unresolved, absorbed };
23366
23388
  }
23389
+ function computeLegacyFernignoreCleanse(patch, fernignorePatterns, computeContentHash2) {
23390
+ const matchesFernignore = (filePath) => fernignorePatterns.some((p2) => {
23391
+ if (filePath === p2) return true;
23392
+ if (minimatch(filePath, p2)) return true;
23393
+ if (!p2.includes("*") && !p2.includes("?") && filePath.startsWith(p2 + "/")) return true;
23394
+ return false;
23395
+ });
23396
+ const strippedFromFiles = patch.files.filter(matchesFernignore);
23397
+ const survivingFiles = patch.files.filter((f) => !matchesFernignore(f));
23398
+ let survivingSnapshot = void 0;
23399
+ let snapshotChanged = false;
23400
+ if (patch.theirs_snapshot) {
23401
+ survivingSnapshot = {};
23402
+ for (const [key, value] of Object.entries(patch.theirs_snapshot)) {
23403
+ if (matchesFernignore(key)) {
23404
+ snapshotChanged = true;
23405
+ } else {
23406
+ survivingSnapshot[key] = value;
23407
+ }
23408
+ }
23409
+ }
23410
+ const { stripped: patchContentStripped, content: newPatchContent, strippedSectionPaths } = stripFernignoreDiffSections(patch.patch_content, matchesFernignore);
23411
+ const unchanged = strippedFromFiles.length === 0 && !snapshotChanged && !patchContentStripped;
23412
+ if (unchanged) {
23413
+ return {
23414
+ unchanged: true,
23415
+ remove: false,
23416
+ files: patch.files,
23417
+ patch_content: patch.patch_content,
23418
+ content_hash: patch.content_hash,
23419
+ theirs_snapshot: patch.theirs_snapshot,
23420
+ strippedPaths: []
23421
+ };
23422
+ }
23423
+ const strippedPaths = Array.from(/* @__PURE__ */ new Set([...strippedFromFiles, ...strippedSectionPaths]));
23424
+ if (survivingFiles.length === 0 || newPatchContent.trim() === "") {
23425
+ return {
23426
+ unchanged: false,
23427
+ remove: true,
23428
+ files: [],
23429
+ patch_content: "",
23430
+ content_hash: "",
23431
+ strippedPaths
23432
+ };
23433
+ }
23434
+ const newContentHash = patchContentStripped ? computeContentHash2(newPatchContent) : patch.content_hash;
23435
+ return {
23436
+ unchanged: false,
23437
+ remove: false,
23438
+ files: survivingFiles,
23439
+ patch_content: newPatchContent,
23440
+ content_hash: newContentHash,
23441
+ ...survivingSnapshot !== void 0 ? { theirs_snapshot: survivingSnapshot } : {},
23442
+ strippedPaths
23443
+ };
23444
+ }
23445
+ function stripFernignoreDiffSections(patchContent, matchesFernignore) {
23446
+ const lines = patchContent.split("\n");
23447
+ const sectionStarts = [];
23448
+ for (let i2 = 0; i2 < lines.length; i2++) {
23449
+ const line = lines[i2];
23450
+ if (line.startsWith("diff --git ")) {
23451
+ const match2 = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
23452
+ const path5 = match2 ? match2[2] : null;
23453
+ sectionStarts.push({ idx: i2, path: path5 });
23454
+ }
23455
+ }
23456
+ if (sectionStarts.length === 0) {
23457
+ return { stripped: false, content: patchContent, strippedSectionPaths: [] };
23458
+ }
23459
+ const preamble = lines.slice(0, sectionStarts[0].idx);
23460
+ const newLines = [...preamble];
23461
+ const strippedSectionPaths = [];
23462
+ let stripped = false;
23463
+ for (let i2 = 0; i2 < sectionStarts.length; i2++) {
23464
+ const section = sectionStarts[i2];
23465
+ const next = sectionStarts[i2 + 1];
23466
+ const endIdx = next ? next.idx : lines.length;
23467
+ if (section.path !== null && matchesFernignore(section.path)) {
23468
+ stripped = true;
23469
+ strippedSectionPaths.push(section.path);
23470
+ continue;
23471
+ }
23472
+ newLines.push(...lines.slice(section.idx, endIdx));
23473
+ }
23474
+ return { stripped, content: newLines.join("\n"), strippedSectionPaths };
23475
+ }
23367
23476
  async function bootstrap(outputDir, options) {
23368
23477
  const git = new GitClient(outputDir);
23369
23478
  const lockManager = new LockfileManager(outputDir);
@@ -23947,7 +24056,7 @@ function status(outputDir) {
23947
24056
  }
23948
24057
  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;
23949
24058
  var init_dist3 = __esm({
23950
- "../../node_modules/.pnpm/@fern-api+replay@0.16.0/node_modules/@fern-api/replay/dist/index.js"() {
24059
+ "../../node_modules/.pnpm/@fern-api+replay@0.16.2/node_modules/@fern-api/replay/dist/index.js"() {
23951
24060
  "use strict";
23952
24061
  init_esm4();
23953
24062
  import_promises3 = require("fs/promises");
@@ -23955,8 +24064,10 @@ var init_dist3 = __esm({
23955
24064
  import_path7 = require("path");
23956
24065
  import_fs6 = require("fs");
23957
24066
  import_path8 = require("path");
24067
+ init_esm3();
23958
24068
  import_yaml = __toESM(require_dist3(), 1);
23959
24069
  import_crypto = require("crypto");
24070
+ init_esm3();
23960
24071
  import_path9 = require("path");
23961
24072
  init_diff3();
23962
24073
  import_promises4 = require("fs/promises");
@@ -24187,10 +24298,23 @@ var init_dist3 = __esm({
24187
24298
  LockfileManager = class {
24188
24299
  outputDir;
24189
24300
  lock = null;
24301
+ fernignorePatterns = [];
24190
24302
  warnings = [];
24191
24303
  constructor(outputDir) {
24192
24304
  this.outputDir = outputDir;
24193
24305
  }
24306
+ /**
24307
+ * Inform the lockfile of the customer's `.fernignore` patterns so that
24308
+ * `save()` can suppress its "Patch X removed from lockfile" warning when
24309
+ * every file in the stripped patch is already `.fernignore`-protected.
24310
+ * The strip itself still happens (credentials never persist); only the
24311
+ * customer-visible warning is suppressed in that case, because the
24312
+ * protected files survive the generator wipe regardless of whether the
24313
+ * lockfile tracks them, so there's nothing for the customer to action.
24314
+ */
24315
+ setFernignorePatterns(patterns) {
24316
+ this.fernignorePatterns = patterns;
24317
+ }
24194
24318
  get lockfilePath() {
24195
24319
  return (0, import_path8.join)(this.outputDir, ".fern", "replay.lock");
24196
24320
  }
@@ -24259,9 +24383,14 @@ var init_dist3 = __esm({
24259
24383
  if (sensitiveFiles.length > 0) {
24260
24384
  reasons.push(`sensitive files: ${sensitiveFiles.join(", ")}`);
24261
24385
  }
24262
- this.warnings.push(
24263
- `Patch ${patch.id} removed from lockfile: ${reasons.join("; ")}. This prevents credential exposure in committed lockfiles.`
24386
+ const allProtected = patch.files.length > 0 && patch.files.every(
24387
+ (f) => this.fernignorePatterns.some((p2) => f === p2 || minimatch(f, p2))
24264
24388
  );
24389
+ if (!allProtected) {
24390
+ this.warnings.push(
24391
+ `Patch ${patch.id} removed from lockfile: ${reasons.join("; ")}. This prevents credential exposure in committed lockfiles.`
24392
+ );
24393
+ }
24265
24394
  } else {
24266
24395
  cleanPatches.push(patch);
24267
24396
  }
@@ -24421,11 +24550,13 @@ var init_dist3 = __esm({
24421
24550
  git;
24422
24551
  lockManager;
24423
24552
  sdkOutputDir;
24553
+ fernignorePatterns;
24424
24554
  warnings = [];
24425
- constructor(git, lockManager, sdkOutputDir) {
24555
+ constructor(git, lockManager, sdkOutputDir, fernignorePatterns = []) {
24426
24556
  this.git = git;
24427
24557
  this.lockManager = lockManager;
24428
24558
  this.sdkOutputDir = sdkOutputDir;
24559
+ this.fernignorePatterns = fernignorePatterns;
24429
24560
  }
24430
24561
  /**
24431
24562
  * Derive the previous-generation SHA from git history instead of trusting
@@ -24532,22 +24663,30 @@ var init_dist3 = __esm({
24532
24663
  continue;
24533
24664
  }
24534
24665
  const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
24535
- const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
24666
+ const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
24667
+ filesOutput,
24668
+ this.fernignorePatterns
24669
+ );
24536
24670
  if (sensitiveFiles.length > 0) {
24537
24671
  this.warnings.push(
24538
24672
  `Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
24539
24673
  );
24540
- if (files.length > 0) {
24541
- const parentSha = parents[0];
24542
- if (parentSha) {
24543
- try {
24544
- patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
24545
- } catch {
24546
- continue;
24547
- }
24548
- if (!patchContent.trim()) continue;
24549
- contentHash = this.computeContentHash(patchContent);
24674
+ }
24675
+ if (fernignoredFiles.length > 0) {
24676
+ this.warnings.push(
24677
+ `.fernignore-protected file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
24678
+ );
24679
+ }
24680
+ if ((sensitiveFiles.length > 0 || fernignoredFiles.length > 0) && files.length > 0) {
24681
+ const parentSha = parents[0];
24682
+ if (parentSha) {
24683
+ try {
24684
+ patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
24685
+ } catch {
24686
+ continue;
24550
24687
  }
24688
+ if (!patchContent.trim()) continue;
24689
+ contentHash = this.computeContentHash(patchContent);
24551
24690
  }
24552
24691
  }
24553
24692
  if (files.length === 0) {
@@ -24746,12 +24885,20 @@ var init_dist3 = __esm({
24746
24885
  resolvedBaseGeneration = fallback;
24747
24886
  }
24748
24887
  const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
24749
- const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
24888
+ const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
24889
+ filesOutput,
24890
+ this.fernignorePatterns
24891
+ );
24750
24892
  if (sensitiveFiles.length > 0) {
24751
24893
  this.warnings.push(
24752
24894
  `Sensitive file(s) excluded from composite patch: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
24753
24895
  );
24754
24896
  }
24897
+ if (fernignoredFiles.length > 0) {
24898
+ this.warnings.push(
24899
+ `.fernignore-protected file(s) excluded from composite patch: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
24900
+ );
24901
+ }
24755
24902
  if (files.length === 0) return { patches: [], revertedPatchIds: [] };
24756
24903
  const diff = await this.git.exec(["diff", diffBase, "HEAD", "--", ...files]);
24757
24904
  if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
@@ -24834,23 +24981,31 @@ var init_dist3 = __esm({
24834
24981
  continue;
24835
24982
  }
24836
24983
  const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
24837
- const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
24984
+ const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
24985
+ filesOutput,
24986
+ this.fernignorePatterns
24987
+ );
24838
24988
  if (sensitiveFiles.length > 0) {
24839
24989
  this.warnings.push(
24840
24990
  `Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
24841
24991
  );
24842
- if (files.length > 0) {
24843
- if (parents.length === 0) {
24844
- continue;
24845
- }
24846
- try {
24847
- patchContent = await this.git.exec(["diff", parents[0], commit.sha, "--", ...files]);
24848
- } catch {
24849
- continue;
24850
- }
24851
- if (!patchContent.trim()) continue;
24852
- contentHash = this.computeContentHash(patchContent);
24992
+ }
24993
+ if (fernignoredFiles.length > 0) {
24994
+ this.warnings.push(
24995
+ `.fernignore-protected file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
24996
+ );
24997
+ }
24998
+ if ((sensitiveFiles.length > 0 || fernignoredFiles.length > 0) && files.length > 0) {
24999
+ if (parents.length === 0) {
25000
+ continue;
25001
+ }
25002
+ try {
25003
+ patchContent = await this.git.exec(["diff", parents[0], commit.sha, "--", ...files]);
25004
+ } catch {
25005
+ continue;
24853
25006
  }
25007
+ if (!patchContent.trim()) continue;
25008
+ contentHash = this.computeContentHash(patchContent);
24854
25009
  }
24855
25010
  if (files.length === 0) {
24856
25011
  continue;
@@ -25665,6 +25820,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25665
25820
  };
25666
25821
  }
25667
25822
  async apply(_prep) {
25823
+ this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
25668
25824
  this.service.lockManager.save();
25669
25825
  return firstGenerationReport();
25670
25826
  }
@@ -25711,6 +25867,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25711
25867
  };
25712
25868
  }
25713
25869
  async apply(_prep, options) {
25870
+ this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
25714
25871
  this.service.lockManager.save();
25715
25872
  const credentialWarnings = [...this.service.lockManager.warnings];
25716
25873
  if (!options?.stageOnly) {
@@ -25832,6 +25989,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25832
25989
  this.service.lockManager.addPatch(patch);
25833
25990
  }
25834
25991
  }
25992
+ this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
25835
25993
  this.service.lockManager.save();
25836
25994
  this.service.warnings.push(...this.service.lockManager.warnings);
25837
25995
  if (newPatches.length > 0) {
@@ -25992,6 +26150,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25992
26150
  }
25993
26151
  }
25994
26152
  }
26153
+ this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
25995
26154
  this.service.lockManager.save();
25996
26155
  this.service.warnings.push(...this.service.lockManager.warnings);
25997
26156
  if (options?.stageOnly) {
@@ -26063,7 +26222,8 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26063
26222
  this.git = git;
26064
26223
  this.outputDir = outputDir;
26065
26224
  this.lockManager = new LockfileManager(outputDir);
26066
- this.detector = new ReplayDetector(git, this.lockManager, outputDir);
26225
+ const fernignorePatterns = this.readFernignorePatterns();
26226
+ this.detector = new ReplayDetector(git, this.lockManager, outputDir, fernignorePatterns);
26067
26227
  this.applicator = new ReplayApplicator(git, this.lockManager, outputDir);
26068
26228
  this.committer = new ReplayCommitter(git, outputDir);
26069
26229
  this.fileOwnership = new FileOwnership(git);
@@ -26083,8 +26243,60 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26083
26243
  async prepareReplay(options) {
26084
26244
  this.warnings = [];
26085
26245
  this.detector.warnings.length = 0;
26246
+ this.cleanseLegacyFernignoreEntries();
26086
26247
  return this.selectFlow(options).prepare(options);
26087
26248
  }
26249
+ /**
26250
+ * ADR 0002 migration step. Strips `.fernignore`-matched entries from
26251
+ * `patch.files`, `patch.theirs_snapshot` keys, and `diff --git` sections
26252
+ * in `patch.patch_content` for every patch in the lockfile. Patches that
26253
+ * end up with no surviving files are removed entirely.
26254
+ *
26255
+ * Idempotent: a second run is a no-op because the first run already
26256
+ * matches the contract. Per-patch try/catch ensures one bad patch does
26257
+ * not block the rest. Each affected patch emits a warning naming the
26258
+ * stripped paths. `patch_content` is left untouched if no parseable
26259
+ * `diff --git` headers are found (fail-safe for exotic legacy formats).
26260
+ */
26261
+ cleanseLegacyFernignoreEntries() {
26262
+ const patterns = this.readFernignorePatterns();
26263
+ if (patterns.length === 0) return;
26264
+ if (!this.lockManager.exists()) return;
26265
+ this.lockManager.read();
26266
+ let lockfileMutated = false;
26267
+ const patches = this.lockManager.getPatches();
26268
+ for (const patch of patches) {
26269
+ try {
26270
+ const result = computeLegacyFernignoreCleanse(
26271
+ patch,
26272
+ patterns,
26273
+ (content) => this.detector.computeContentHash(content)
26274
+ );
26275
+ if (result.unchanged) continue;
26276
+ if (result.remove) {
26277
+ this.lockManager.removePatch(patch.id);
26278
+ this.warnings.push(
26279
+ `Cleansed legacy patch ${patch.id}: all referenced files match .fernignore \u2014 patch removed. Customer's on-disk content for those files is preserved by .fernignore. (ADR 0002: .fernignore-protected files are not tracked by Replay.)`
26280
+ );
26281
+ } else {
26282
+ this.lockManager.updatePatch(patch.id, {
26283
+ files: result.files,
26284
+ patch_content: result.patch_content,
26285
+ content_hash: result.content_hash,
26286
+ ...result.theirs_snapshot !== void 0 ? { theirs_snapshot: result.theirs_snapshot } : {}
26287
+ });
26288
+ this.warnings.push(
26289
+ `Cleansed legacy patch ${patch.id}: stripped .fernignore-matched entries (${result.strippedPaths.join(", ")}). Customer's on-disk content for those files is preserved by .fernignore. (ADR 0002.)`
26290
+ );
26291
+ }
26292
+ lockfileMutated = true;
26293
+ } catch {
26294
+ }
26295
+ }
26296
+ if (lockfileMutated) {
26297
+ this.lockManager.save();
26298
+ }
26299
+ }
26088
26300
  /**
26089
26301
  * Phase 2 of the two-phase replay flow. Applies patches, rebases them against
26090
26302
  * the generation, saves the lockfile, and commits `[fern-replay]` (or stages
@@ -26676,6 +26888,11 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26676
26888
  (files) => this.git.exec(["diff", currentGen, "HEAD", "--", ...files]).catch(() => null)
26677
26889
  );
26678
26890
  if (ppResult === null) continue;
26891
+ if (ppResult.hadFallback) {
26892
+ this.warnings.push(
26893
+ `Patch ${patch.id}: couldn't fully isolate ${ppResult.fallbackFiles.join(", ")} \u2014 customization preserved, attribution may have merged with another patch.`
26894
+ );
26895
+ }
26679
26896
  const diff = ppResult.diff;
26680
26897
  if (!diff.trim()) {
26681
26898
  if (await allPatchFilesUserOwned(patch)) continue;
@@ -26763,6 +26980,11 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26763
26980
  (files) => this.git.exec(["diff", currentGen, "HEAD", "--", ...files]).catch(() => null)
26764
26981
  );
26765
26982
  if (ppResult === null) continue;
26983
+ if (ppResult.hadFallback) {
26984
+ this.warnings.push(
26985
+ `Patch ${patch.id}: couldn't fully isolate ${ppResult.fallbackFiles.join(", ")} \u2014 customization preserved, attribution may have merged with another patch.`
26986
+ );
26987
+ }
26766
26988
  const diff = ppResult.diff;
26767
26989
  if (!diff.trim()) {
26768
26990
  if (await allPatchFilesUserOwned(patch)) {
@@ -26857,6 +27079,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
26857
27079
  }
26858
27080
  }
26859
27081
  }
27082
+ /** @internal Used by Flow implementations in `src/flows/`. */
26860
27083
  readFernignorePatterns() {
26861
27084
  const fernignorePath = (0, import_path13.join)(this.outputDir, ".fernignore");
26862
27085
  if (!(0, import_fs7.existsSync)(fernignorePath)) return [];
@@ -31368,6 +31591,11 @@ var ClonedRepository = class _ClonedRepository {
31368
31591
  }
31369
31592
  await this.git.raw([...args, ...Array.isArray(files) ? files : [files]]);
31370
31593
  }
31594
+ async listTrackedFiles() {
31595
+ await this.git.cwd(this.clonePath);
31596
+ const result = await this.git.raw(["ls-tree", "-r", "HEAD", "--name-only"]);
31597
+ return result.split("\n").filter((line) => line.length > 0);
31598
+ }
31371
31599
  async restoreFilesFromCommit(commitSha, files) {
31372
31600
  await this.git.cwd(this.clonePath);
31373
31601
  const fileList = Array.isArray(files) ? files : [files];
@@ -31790,6 +32018,20 @@ async function cloneRepository({ githubRepository, installationToken, targetDire
31790
32018
  });
31791
32019
  }
31792
32020
 
32021
+ // ../commons/github/lib/expandFernignorePatterns.js
32022
+ init_esm3();
32023
+ function expandFernignorePatterns(fernignoreContent, candidatePaths) {
32024
+ const patterns = fernignoreContent.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
32025
+ return candidatePaths.filter((candidate) => patterns.some((pattern) => matches(candidate, pattern)));
32026
+ }
32027
+ function matches(candidate, pattern) {
32028
+ const normalized = pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
32029
+ if (minimatch(candidate, normalized, { dot: true })) {
32030
+ return true;
32031
+ }
32032
+ return candidate === normalized || candidate.startsWith(normalized + "/");
32033
+ }
32034
+
31793
32035
  // ../../node_modules/.pnpm/universal-user-agent@7.0.3/node_modules/universal-user-agent/index.js
31794
32036
  function getUserAgent() {
31795
32037
  if (typeof navigator === "object" && "userAgent" in navigator) {
@@ -32006,11 +32248,11 @@ function removeNonChars(variableName) {
32006
32248
  return variableName.replace(/(?:^\W+)|(?:(?<!\W)\W+$)/g, "").split(/,/);
32007
32249
  }
32008
32250
  function extractUrlVariableNames(url) {
32009
- const matches = url.match(urlVariableRegex);
32010
- if (!matches) {
32251
+ const matches2 = url.match(urlVariableRegex);
32252
+ if (!matches2) {
32011
32253
  return [];
32012
32254
  }
32013
- return matches.map(removeNonChars).reduce((a, b3) => a.concat(b3), []);
32255
+ return matches2.map(removeNonChars).reduce((a, b3) => a.concat(b3), []);
32014
32256
  }
32015
32257
  function omit(object2, keysToOmit) {
32016
32258
  const result = { __proto__: null };
@@ -32453,8 +32695,8 @@ async function fetchWrapper(requestOptions) {
32453
32695
  data: ""
32454
32696
  };
32455
32697
  if ("deprecation" in responseHeaders) {
32456
- const matches = responseHeaders.link && responseHeaders.link.match(/<([^<>]+)>; rel="deprecation"/);
32457
- const deprecationLink = matches && matches.pop();
32698
+ const matches2 = responseHeaders.link && responseHeaders.link.match(/<([^<>]+)>; rel="deprecation"/);
32699
+ const deprecationLink = matches2 && matches2.pop();
32458
32700
  log.warn(
32459
32701
  `[@octokit/request] "${requestOptions.method} ${requestOptions.url}" is deprecated. It is scheduled to be removed on ${responseHeaders.sunset}${deprecationLink ? `. See ${deprecationLink}` : ""}`
32460
32702
  );
@@ -35538,15 +35780,8 @@ var GitHub = class {
35538
35780
  if (fernignore === void 0) {
35539
35781
  return [];
35540
35782
  }
35541
- const fernignoreLines = fernignore.split("\n");
35542
- const fernignoreFiles = [];
35543
- for (const line of fernignoreLines) {
35544
- const trimmedLine = line.trim();
35545
- if (!trimmedLine.startsWith("#") && trimmedLine.length > 0 && await repository.fileExists({ relativeFilePath: trimmedLine })) {
35546
- fernignoreFiles.push(trimmedLine);
35547
- }
35548
- }
35549
- return fernignoreFiles;
35783
+ const tracked = await repository.listTrackedFiles();
35784
+ return expandFernignorePatterns(fernignore, tracked);
35550
35785
  }
35551
35786
  async restoreFiles(repository, files) {
35552
35787
  if (files.length === 0) {
@@ -41858,6 +42093,8 @@ var GithubStep = class extends BaseStep {
41858
42093
  );
41859
42094
  case "push":
41860
42095
  return await this.executePushMode(repository, resolvedPrFields);
42096
+ case "commit-and-release":
42097
+ return await this.executeCommitAndReleaseMode(repository, resolvedPrFields);
41861
42098
  default: {
41862
42099
  const exhaustive = mode;
41863
42100
  throw new Error(`Unexpected GitHub mode: ${String(exhaustive)}`);
@@ -42084,6 +42321,71 @@ var GithubStep = class extends BaseStep {
42084
42321
  }
42085
42322
  return result;
42086
42323
  }
42324
+ async executeCommitAndReleaseMode(repository, resolved) {
42325
+ const baseBranch = this.config.branch ?? await repository.getDefaultBranch();
42326
+ if (this.config.branch != null) {
42327
+ this.logger.debug(`Checking out branch ${this.config.branch}`);
42328
+ await repository.checkout(this.config.branch);
42329
+ }
42330
+ await this.ensureFernignore();
42331
+ this.logger.debug("Committing changes...");
42332
+ await repository.commitAllChanges(resolved.commitMessage);
42333
+ this.logger.debug(`Committed changes to local copy of GitHub repository at ${this.outputDir}`);
42334
+ if (shouldCheckNoDiff(this.config)) {
42335
+ const noDiff = await repository.treeHashEquals(`origin/${baseBranch}`);
42336
+ if (noDiff) {
42337
+ this.logger.info("No changes detected after generation \u2014 skipping commit-and-release");
42338
+ return { executed: true, success: true, skippedNoDiff: true };
42339
+ }
42340
+ }
42341
+ const result = {
42342
+ executed: true,
42343
+ success: true
42344
+ };
42345
+ if (!this.config.previewMode) {
42346
+ const octokit = this.createOctokit();
42347
+ const { owner, repo, remote } = parseRepository(this.config.uri);
42348
+ await pushSignedCommit({
42349
+ repository,
42350
+ octokit,
42351
+ owner,
42352
+ repo,
42353
+ branch: baseBranch,
42354
+ force: false,
42355
+ rebaseOnConflict: true,
42356
+ author: this.config.author,
42357
+ logger: this.logger
42358
+ });
42359
+ const pushedBranch = await repository.getCurrentBranch();
42360
+ result.branchUrl = `https://${remote}/${owner}/${repo}/tree/${pushedBranch}`;
42361
+ this.logger.info(`Pushed branch: ${result.branchUrl}`);
42362
+ if (resolved.newVersion == null) {
42363
+ this.logger.warn(
42364
+ "No new version available \u2014 skipping release tag creation. Pass --version <semver> or --version AUTO to fern generate to enable release tagging."
42365
+ );
42366
+ } else {
42367
+ try {
42368
+ const tagName = resolved.newVersion;
42369
+ const headSha = await repository.getHeadSha();
42370
+ await octokit.repos.createRelease({
42371
+ owner,
42372
+ repo,
42373
+ tag_name: tagName,
42374
+ target_commitish: headSha,
42375
+ name: tagName,
42376
+ body: resolved.changelogEntry ?? resolved.commitMessage,
42377
+ draft: false,
42378
+ prerelease: false
42379
+ });
42380
+ this.logger.info(`Created GitHub release ${tagName}`);
42381
+ result.releaseUrl = `https://${remote}/${owner}/${repo}/releases/tag/${tagName}`;
42382
+ } catch (error) {
42383
+ this.logger.warn(`Could not create GitHub release: ${extractErrorMessage(error)}`);
42384
+ }
42385
+ }
42386
+ }
42387
+ return result;
42388
+ }
42087
42389
  createOctokit() {
42088
42390
  const opts = { auth: this.config.token };
42089
42391
  if (this.config.apiBaseUrl != null) {
@@ -42528,9 +42830,9 @@ var PostGenerationPipeline = class {
42528
42830
  this.logger = logger;
42529
42831
  let replayEnabled = config.replay?.enabled ?? false;
42530
42832
  let autoVersionEnabled = config.autoVersion?.enabled ?? false;
42531
- if (replayEnabled && config.github?.mode === "push") {
42833
+ if (replayEnabled && (config.github?.mode === "push" || config.github?.mode === "commit-and-release")) {
42532
42834
  this.logger.warn(
42533
- "Replay is not supported with GitHub push mode. Disabling replay to prevent force push to base branch."
42835
+ `Replay is not supported with GitHub ${config.github.mode} mode. Disabling replay to prevent push to base branch.`
42534
42836
  );
42535
42837
  replayEnabled = false;
42536
42838
  }