@fern-api/replay 0.16.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -214,8 +214,19 @@ declare class LockfileNotFoundError extends Error {
214
214
  declare class LockfileManager {
215
215
  private outputDir;
216
216
  private lock;
217
+ private fernignorePatterns;
217
218
  readonly warnings: string[];
218
219
  constructor(outputDir: string);
220
+ /**
221
+ * Inform the lockfile of the customer's `.fernignore` patterns so that
222
+ * `save()` can suppress its "Patch X removed from lockfile" warning when
223
+ * every file in the stripped patch is already `.fernignore`-protected.
224
+ * The strip itself still happens (credentials never persist); only the
225
+ * customer-visible warning is suppressed in that case, because the
226
+ * protected files survive the generator wipe regardless of whether the
227
+ * lockfile tracks them, so there's nothing for the customer to action.
228
+ */
229
+ setFernignorePatterns(patterns: string[]): void;
219
230
  get lockfilePath(): string;
220
231
  get customizationsPath(): string;
221
232
  exists(): boolean;
@@ -874,7 +885,8 @@ declare class ReplayService {
874
885
  */
875
886
  /** @internal Used by Flow implementations in `src/flows/`. */
876
887
  cleanupStaleConflictMarkers(): Promise<void>;
877
- private readFernignorePatterns;
888
+ /** @internal Used by Flow implementations in `src/flows/`. */
889
+ readFernignorePatterns(): string[];
878
890
  /** @internal Used by Flow implementations in `src/flows/`. */
879
891
  buildReport(flow: "first-generation" | "no-patches" | "normal-regeneration" | "skip-application", patches: StoredPatch[], results: ReplayResult[], options?: ReplayOptions, warnings?: string[], rebaseCounts?: {
880
892
  absorbed: number;
package/dist/index.d.ts CHANGED
@@ -214,8 +214,19 @@ declare class LockfileNotFoundError extends Error {
214
214
  declare class LockfileManager {
215
215
  private outputDir;
216
216
  private lock;
217
+ private fernignorePatterns;
217
218
  readonly warnings: string[];
218
219
  constructor(outputDir: string);
220
+ /**
221
+ * Inform the lockfile of the customer's `.fernignore` patterns so that
222
+ * `save()` can suppress its "Patch X removed from lockfile" warning when
223
+ * every file in the stripped patch is already `.fernignore`-protected.
224
+ * The strip itself still happens (credentials never persist); only the
225
+ * customer-visible warning is suppressed in that case, because the
226
+ * protected files survive the generator wipe regardless of whether the
227
+ * lockfile tracks them, so there's nothing for the customer to action.
228
+ */
229
+ setFernignorePatterns(patterns: string[]): void;
219
230
  get lockfilePath(): string;
220
231
  get customizationsPath(): string;
221
232
  exists(): boolean;
@@ -874,7 +885,8 @@ declare class ReplayService {
874
885
  */
875
886
  /** @internal Used by Flow implementations in `src/flows/`. */
876
887
  cleanupStaleConflictMarkers(): Promise<void>;
877
- private readFernignorePatterns;
888
+ /** @internal Used by Flow implementations in `src/flows/`. */
889
+ readFernignorePatterns(): string[];
878
890
  /** @internal Used by Flow implementations in `src/flows/`. */
879
891
  buildReport(flow: "first-generation" | "no-patches" | "normal-regeneration" | "skip-application", patches: StoredPatch[], results: ReplayResult[], options?: ReplayOptions, warnings?: string[], rebaseCounts?: {
880
892
  absorbed: number;
package/dist/index.js CHANGED
@@ -533,6 +533,7 @@ function parseRevertedMessage(subject) {
533
533
  // src/LockfileManager.ts
534
534
  import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from "fs";
535
535
  import { join, dirname } from "path";
536
+ import { minimatch } from "minimatch";
536
537
  import { stringify, parse } from "yaml";
537
538
  var LOCKFILE_HEADER = "# DO NOT EDIT MANUALLY - Managed by Fern Replay\n";
538
539
  var LockfileNotFoundError = class extends Error {
@@ -544,10 +545,23 @@ var LockfileNotFoundError = class extends Error {
544
545
  var LockfileManager = class {
545
546
  outputDir;
546
547
  lock = null;
548
+ fernignorePatterns = [];
547
549
  warnings = [];
548
550
  constructor(outputDir) {
549
551
  this.outputDir = outputDir;
550
552
  }
553
+ /**
554
+ * Inform the lockfile of the customer's `.fernignore` patterns so that
555
+ * `save()` can suppress its "Patch X removed from lockfile" warning when
556
+ * every file in the stripped patch is already `.fernignore`-protected.
557
+ * The strip itself still happens (credentials never persist); only the
558
+ * customer-visible warning is suppressed in that case, because the
559
+ * protected files survive the generator wipe regardless of whether the
560
+ * lockfile tracks them, so there's nothing for the customer to action.
561
+ */
562
+ setFernignorePatterns(patterns) {
563
+ this.fernignorePatterns = patterns;
564
+ }
551
565
  get lockfilePath() {
552
566
  return join(this.outputDir, ".fern", "replay.lock");
553
567
  }
@@ -616,9 +630,14 @@ var LockfileManager = class {
616
630
  if (sensitiveFiles.length > 0) {
617
631
  reasons.push(`sensitive files: ${sensitiveFiles.join(", ")}`);
618
632
  }
619
- this.warnings.push(
620
- `Patch ${patch.id} removed from lockfile: ${reasons.join("; ")}. This prevents credential exposure in committed lockfiles.`
633
+ const allProtected = patch.files.length > 0 && patch.files.every(
634
+ (f) => this.fernignorePatterns.some((p) => f === p || minimatch(f, p))
621
635
  );
636
+ if (!allProtected) {
637
+ this.warnings.push(
638
+ `Patch ${patch.id} removed from lockfile: ${reasons.join("; ")}. This prevents credential exposure in committed lockfiles.`
639
+ );
640
+ }
622
641
  } else {
623
642
  cleanPatches.push(patch);
624
643
  }
@@ -1501,7 +1520,7 @@ function applyBothPatches(baseLines, oursPatches, theirsPatches) {
1501
1520
  import { mkdir as mkdir2, mkdtemp, readFile as readFile2, rm, unlink, writeFile as writeFile2 } from "fs/promises";
1502
1521
  import { tmpdir } from "os";
1503
1522
  import { dirname as dirname3, join as join3 } from "path";
1504
- import { minimatch } from "minimatch";
1523
+ import { minimatch as minimatch2 } from "minimatch";
1505
1524
 
1506
1525
  // src/accumulator/MergeAccumulator.ts
1507
1526
  var MergeAccumulator = class {
@@ -2217,13 +2236,13 @@ var ReplayApplicator = class {
2217
2236
  isExcluded(patch) {
2218
2237
  const config = this.lockManager.getCustomizationsConfig();
2219
2238
  if (!config.exclude) return false;
2220
- return patch.files.some((file) => config.exclude.some((pattern) => minimatch(file, pattern)));
2239
+ return patch.files.some((file) => config.exclude.some((pattern) => minimatch2(file, pattern)));
2221
2240
  }
2222
2241
  async resolveFilePath(filePath, baseTreeHash, currentTreeHash) {
2223
2242
  const config = this.lockManager.getCustomizationsConfig();
2224
2243
  if (config.moves) {
2225
2244
  for (const move of config.moves) {
2226
- if (minimatch(filePath, move.from) || filePath === move.from) {
2245
+ if (minimatch2(filePath, move.from) || filePath === move.from) {
2227
2246
  if (filePath === move.from) {
2228
2247
  return move.to;
2229
2248
  }
@@ -2514,7 +2533,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
2514
2533
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
2515
2534
  import { readFile as readFileAsync } from "fs/promises";
2516
2535
  import { join as join6 } from "path";
2517
- import { minimatch as minimatch3 } from "minimatch";
2536
+ import { minimatch as minimatch4 } from "minimatch";
2518
2537
  init_GitClient();
2519
2538
 
2520
2539
  // src/PatchRegionDiff.ts
@@ -2625,12 +2644,23 @@ async function computePerPatchDiff(patch, getCurrentGenContent, getWorkingTreeCo
2625
2644
  unlocatableFiles.push(file);
2626
2645
  continue;
2627
2646
  }
2628
- const locInCurrentGen = locInCurrentGenRaw.map(
2629
- (l) => resolveSpan(l, currentGenLines, "old")
2630
- );
2631
- const locInWorkingTree = locInWorkingTreeRaw.map(
2632
- (l) => resolveSpan(l, workingTreeLines, "new")
2633
- );
2647
+ const locInCurrentGen = [];
2648
+ const locInWorkingTree = [];
2649
+ let spanResolutionFailed = false;
2650
+ for (let i = 0; i < locInCurrentGenRaw.length; i++) {
2651
+ const cg = resolveSpan(locInCurrentGenRaw[i], currentGenLines, "old");
2652
+ const wt = resolveSpan(locInWorkingTreeRaw[i], workingTreeLines, "new");
2653
+ if (cg === null || wt === null) {
2654
+ spanResolutionFailed = true;
2655
+ break;
2656
+ }
2657
+ locInCurrentGen.push(cg);
2658
+ locInWorkingTree.push(wt);
2659
+ }
2660
+ if (spanResolutionFailed) {
2661
+ unlocatableFiles.push(file);
2662
+ continue;
2663
+ }
2634
2664
  const overlay = overlayRegions(
2635
2665
  currentGenLines,
2636
2666
  locInCurrentGen,
@@ -2692,11 +2722,11 @@ function resolveSpan(loc, oursLines, prefer) {
2692
2722
  if (prefer === "old") {
2693
2723
  if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
2694
2724
  if (newFits) return { ...loc, oursSpan: hunk.newCount };
2695
- } else {
2696
- if (newFits) return { ...loc, oursSpan: hunk.newCount };
2697
- if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
2725
+ return loc;
2698
2726
  }
2699
- return loc;
2727
+ if (newFits) return { ...loc, oursSpan: hunk.newCount };
2728
+ if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
2729
+ return null;
2700
2730
  }
2701
2731
  function sequenceMatches(needle, haystack, offset) {
2702
2732
  if (offset + needle.length > haystack.length) return false;
@@ -2810,7 +2840,7 @@ function synthesizeDeleteFileDiff(file, content) {
2810
2840
  }
2811
2841
 
2812
2842
  // src/classifier/FileOwnership.ts
2813
- import { minimatch as minimatch2 } from "minimatch";
2843
+ import { minimatch as minimatch3 } from "minimatch";
2814
2844
  var FileOwnership = class {
2815
2845
  constructor(git) {
2816
2846
  this.git = git;
@@ -2834,7 +2864,7 @@ var FileOwnership = class {
2834
2864
  const { file, originalFileName, patch, currentGenSha, fernignorePatterns, warnedGens, warnings } = ctx;
2835
2865
  if (patch.user_owned) return true;
2836
2866
  if (file === ".fernignore") return true;
2837
- if (fernignorePatterns.some((p) => file === p || minimatch2(file, p))) return true;
2867
+ if (fernignorePatterns.some((p) => file === p || minimatch3(file, p))) return true;
2838
2868
  if (fileIsCreationInPatchContent(patch.patch_content, originalFileName ?? file)) {
2839
2869
  return true;
2840
2870
  }
@@ -2913,6 +2943,7 @@ var FirstGenerationFlow = class {
2913
2943
  };
2914
2944
  }
2915
2945
  async apply(_prep) {
2946
+ this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
2916
2947
  this.service.lockManager.save();
2917
2948
  return firstGenerationReport();
2918
2949
  }
@@ -2971,6 +3002,7 @@ var SkipApplicationFlow = class {
2971
3002
  };
2972
3003
  }
2973
3004
  async apply(_prep, options) {
3005
+ this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
2974
3006
  this.service.lockManager.save();
2975
3007
  const credentialWarnings = [...this.service.lockManager.warnings];
2976
3008
  if (!options?.stageOnly) {
@@ -3094,6 +3126,7 @@ var NoPatchesFlow = class {
3094
3126
  this.service.lockManager.addPatch(patch);
3095
3127
  }
3096
3128
  }
3129
+ this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
3097
3130
  this.service.lockManager.save();
3098
3131
  this.service.warnings.push(...this.service.lockManager.warnings);
3099
3132
  if (newPatches.length > 0) {
@@ -3256,6 +3289,7 @@ var NormalRegenerationFlow = class {
3256
3289
  }
3257
3290
  }
3258
3291
  }
3292
+ this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
3259
3293
  this.service.lockManager.save();
3260
3294
  this.service.warnings.push(...this.service.lockManager.warnings);
3261
3295
  if (options?.stageOnly) {
@@ -3489,7 +3523,7 @@ var ReplayService = class {
3489
3523
  )
3490
3524
  );
3491
3525
  const hasProtectedFiles = patch.files.some(
3492
- (file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || minimatch3(file, p))
3526
+ (file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || minimatch4(file, p))
3493
3527
  );
3494
3528
  const allUserOwned = isUserOwnedPerFile.every(Boolean);
3495
3529
  if (allUserOwned && patch.files.length > 0) {
@@ -3929,7 +3963,7 @@ var ReplayService = class {
3929
3963
  );
3930
3964
  if (snapHasStaleMarkers) continue;
3931
3965
  const isProtectedSnap = patch.files.some(
3932
- (file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || minimatch3(file, p))
3966
+ (file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || minimatch4(file, p))
3933
3967
  );
3934
3968
  if (isProtectedSnap) continue;
3935
3969
  const snapHash = this.detector.computeContentHash(snapshotDiff);
@@ -3949,6 +3983,11 @@ var ReplayService = class {
3949
3983
  (files) => this.git.exec(["diff", currentGen, "HEAD", "--", ...files]).catch(() => null)
3950
3984
  );
3951
3985
  if (ppResult === null) continue;
3986
+ if (ppResult.hadFallback) {
3987
+ this.warnings.push(
3988
+ `Patch ${patch.id}: couldn't fully isolate ${ppResult.fallbackFiles.join(", ")} \u2014 customization preserved, attribution may have merged with another patch.`
3989
+ );
3990
+ }
3952
3991
  const diff = ppResult.diff;
3953
3992
  if (!diff.trim()) {
3954
3993
  if (await allPatchFilesUserOwned(patch)) continue;
@@ -3964,7 +4003,7 @@ var ReplayService = class {
3964
4003
  continue;
3965
4004
  }
3966
4005
  const isProtected = patch.files.some(
3967
- (file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || minimatch3(file, p))
4006
+ (file) => file === ".fernignore" || fernignorePatterns.some((p) => file === p || minimatch4(file, p))
3968
4007
  );
3969
4008
  if (isProtected) {
3970
4009
  continue;
@@ -4036,6 +4075,11 @@ var ReplayService = class {
4036
4075
  (files) => this.git.exec(["diff", currentGen, "HEAD", "--", ...files]).catch(() => null)
4037
4076
  );
4038
4077
  if (ppResult === null) continue;
4078
+ if (ppResult.hadFallback) {
4079
+ this.warnings.push(
4080
+ `Patch ${patch.id}: couldn't fully isolate ${ppResult.fallbackFiles.join(", ")} \u2014 customization preserved, attribution may have merged with another patch.`
4081
+ );
4082
+ }
4039
4083
  const diff = ppResult.diff;
4040
4084
  if (!diff.trim()) {
4041
4085
  if (await allPatchFilesUserOwned(patch)) {
@@ -4123,13 +4167,14 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
4123
4167
  if (files.length === 0) return;
4124
4168
  const fernignorePatterns = this.readFernignorePatterns();
4125
4169
  for (const file of files) {
4126
- if (fernignorePatterns.some((pattern) => minimatch3(file, pattern))) continue;
4170
+ if (fernignorePatterns.some((pattern) => minimatch4(file, pattern))) continue;
4127
4171
  try {
4128
4172
  await this.git.exec(["checkout", "HEAD", "--", file]);
4129
4173
  } catch {
4130
4174
  }
4131
4175
  }
4132
4176
  }
4177
+ /** @internal Used by Flow implementations in `src/flows/`. */
4133
4178
  readFernignorePatterns() {
4134
4179
  const fernignorePath = join6(this.outputDir, ".fernignore");
4135
4180
  if (!existsSync2(fernignorePath)) return [];
@@ -4212,7 +4257,7 @@ function computeCommitBuckets(results, absorbedPatchIds, patchesPassedToCommit)
4212
4257
  import { createHash as createHash2 } from "crypto";
4213
4258
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync3 } from "fs";
4214
4259
  import { dirname as dirname5, join as join7 } from "path";
4215
- import { minimatch as minimatch4 } from "minimatch";
4260
+ import { minimatch as minimatch5 } from "minimatch";
4216
4261
  import { parse as parse2, stringify as stringify2 } from "yaml";
4217
4262
  var FernignoreMigrator = class {
4218
4263
  git;
@@ -4244,7 +4289,7 @@ var FernignoreMigrator = class {
4244
4289
  const commitsOnly = [];
4245
4290
  const syntheticPatches = [];
4246
4291
  for (const pattern of patterns) {
4247
- const matchingPatch = patches.find((p) => p.files.some((f) => minimatch4(f, pattern) || f === pattern));
4292
+ const matchingPatch = patches.find((p) => p.files.some((f) => minimatch5(f, pattern) || f === pattern));
4248
4293
  if (matchingPatch) {
4249
4294
  trackedByBoth.push({
4250
4295
  file: pattern,
@@ -4317,7 +4362,7 @@ var FernignoreMigrator = class {
4317
4362
  fernignoreOnly.push(...remainingFernignoreOnly);
4318
4363
  }
4319
4364
  for (const patch of patches) {
4320
- const hasUnprotectedFiles = patch.files.some((f) => !patterns.some((p) => minimatch4(f, p) || f === p));
4365
+ const hasUnprotectedFiles = patch.files.some((f) => !patterns.some((p) => minimatch5(f, p) || f === p));
4321
4366
  if (hasUnprotectedFiles) {
4322
4367
  commitsOnly.push(patch);
4323
4368
  }
@@ -4329,7 +4374,7 @@ var FernignoreMigrator = class {
4329
4374
  const results = [];
4330
4375
  for (const pattern of patterns) {
4331
4376
  const matching = allFiles.filter(
4332
- (f) => minimatch4(f, pattern) || f === pattern || f.startsWith(pattern + "/")
4377
+ (f) => minimatch5(f, pattern) || f === pattern || f.startsWith(pattern + "/")
4333
4378
  );
4334
4379
  results.push({ pattern, files: matching.length > 0 ? matching : [pattern] });
4335
4380
  }
@@ -4642,7 +4687,7 @@ function computeContentHash(patchContent) {
4642
4687
  }
4643
4688
 
4644
4689
  // src/commands/forget.ts
4645
- import { minimatch as minimatch5 } from "minimatch";
4690
+ import { minimatch as minimatch6 } from "minimatch";
4646
4691
  function parseDiffStat(patchContent) {
4647
4692
  let additions = 0;
4648
4693
  let deletions = 0;
@@ -4672,7 +4717,7 @@ function toMatchedPatch(patch) {
4672
4717
  }
4673
4718
  function matchesPatch(patch, pattern) {
4674
4719
  const fileMatch = patch.files.some(
4675
- (file) => file === pattern || minimatch5(file, pattern)
4720
+ (file) => file === pattern || minimatch6(file, pattern)
4676
4721
  );
4677
4722
  if (fileMatch) return true;
4678
4723
  return patch.original_message.toLowerCase().includes(pattern.toLowerCase());