@fern-api/replay 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -290,6 +290,15 @@ var LockfileManager = class {
290
290
  this.ensureLoaded();
291
291
  this.lock.patches = [];
292
292
  }
293
+ addForgottenHash(hash) {
294
+ this.ensureLoaded();
295
+ if (!this.lock.forgotten_hashes) {
296
+ this.lock.forgotten_hashes = [];
297
+ }
298
+ if (!this.lock.forgotten_hashes.includes(hash)) {
299
+ this.lock.forgotten_hashes.push(hash);
300
+ }
301
+ }
293
302
  getUnresolvedPatches() {
294
303
  this.ensureLoaded();
295
304
  return this.lock.patches.filter((p) => p.status === "unresolved");
@@ -386,6 +395,7 @@ var ReplayDetector = class {
386
395
  }
387
396
  const commits = this.parseGitLog(log);
388
397
  const newPatches = [];
398
+ const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
389
399
  for (const commit of commits) {
390
400
  if (isGenerationCommit(commit)) {
391
401
  continue;
@@ -399,7 +409,7 @@ var ReplayDetector = class {
399
409
  }
400
410
  const patchContent = await this.git.formatPatch(commit.sha);
401
411
  const contentHash = this.computeContentHash(patchContent);
402
- if (lock.patches.find((p) => p.content_hash === contentHash)) {
412
+ if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
403
413
  continue;
404
414
  }
405
415
  const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
@@ -498,7 +508,7 @@ var ReplayDetector = class {
498
508
  if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
499
509
  const contentHash = this.computeContentHash(diff);
500
510
  const lock = this.lockManager.read();
501
- if (lock.patches.some((p) => p.content_hash === contentHash)) {
511
+ if (lock.patches.some((p) => p.content_hash === contentHash) || (lock.forgotten_hashes ?? []).includes(contentHash)) {
502
512
  return { patches: [], revertedPatchIds: [] };
503
513
  }
504
514
  const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
@@ -568,6 +578,36 @@ var import_promises = require("fs/promises");
568
578
  var import_node_os = require("os");
569
579
  var import_node_path2 = require("path");
570
580
  var import_minimatch = require("minimatch");
581
+
582
+ // src/conflict-utils.ts
583
+ function stripConflictMarkers(content) {
584
+ const lines = content.split("\n");
585
+ const result = [];
586
+ let inConflict = false;
587
+ let inOurs = false;
588
+ for (const line of lines) {
589
+ if (line.startsWith("<<<<<<< ")) {
590
+ inConflict = true;
591
+ inOurs = true;
592
+ continue;
593
+ }
594
+ if (inConflict && line === "=======") {
595
+ inOurs = false;
596
+ continue;
597
+ }
598
+ if (inConflict && line.startsWith(">>>>>>> ")) {
599
+ inConflict = false;
600
+ inOurs = false;
601
+ continue;
602
+ }
603
+ if (!inConflict || inOurs) {
604
+ result.push(line);
605
+ }
606
+ }
607
+ return result.join("\n");
608
+ }
609
+
610
+ // src/ReplayApplicator.ts
571
611
  var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
572
612
  ".png",
573
613
  ".jpg",
@@ -639,7 +679,8 @@ var ReplayApplicator = class {
639
679
  async applyPatches(patches) {
640
680
  this.resetAccumulator();
641
681
  const results = [];
642
- for (const patch of patches) {
682
+ for (let i = 0; i < patches.length; i++) {
683
+ const patch = patches[i];
643
684
  if (this.isExcluded(patch)) {
644
685
  results.push({
645
686
  patch,
@@ -650,6 +691,33 @@ var ReplayApplicator = class {
650
691
  }
651
692
  const result = await this.applyPatchWithFallback(patch);
652
693
  results.push(result);
694
+ if (result.status === "conflict" && result.fileResults) {
695
+ const laterFiles = /* @__PURE__ */ new Set();
696
+ for (let j = i + 1; j < patches.length; j++) {
697
+ for (const f of patches[j].files) {
698
+ laterFiles.add(f);
699
+ }
700
+ }
701
+ const resolvedToOriginal = /* @__PURE__ */ new Map();
702
+ if (result.resolvedFiles) {
703
+ for (const [orig, resolved] of Object.entries(result.resolvedFiles)) {
704
+ resolvedToOriginal.set(resolved, orig);
705
+ }
706
+ }
707
+ for (const fileResult of result.fileResults) {
708
+ if (fileResult.status !== "conflict") continue;
709
+ const originalPath = resolvedToOriginal.get(fileResult.file) ?? fileResult.file;
710
+ if (laterFiles.has(fileResult.file) || laterFiles.has(originalPath)) {
711
+ const filePath = (0, import_node_path2.join)(this.outputDir, fileResult.file);
712
+ try {
713
+ const content = await (0, import_promises.readFile)(filePath, "utf-8");
714
+ const stripped = stripConflictMarkers(content);
715
+ await (0, import_promises.writeFile)(filePath, stripped);
716
+ } catch {
717
+ }
718
+ }
719
+ }
720
+ }
653
721
  }
654
722
  return results;
655
723
  }
@@ -668,7 +736,7 @@ var ReplayApplicator = class {
668
736
  const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
669
737
  const base = await this.git.showFile(baseGen.tree_hash, filePath);
670
738
  const theirs = await this.applyPatchToContent(base, patch.patch_content, filePath, tempGit, tempDir);
671
- if (theirs && base) {
739
+ if (theirs) {
672
740
  this.fileTheirsAccumulator.set(resolvedPath, {
673
741
  content: theirs,
674
742
  baseGeneration: patch.base_generation
@@ -800,7 +868,7 @@ var ReplayApplicator = class {
800
868
  );
801
869
  let useAccumulatorAsMergeBase = false;
802
870
  const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
803
- if (!theirs && base && accumulatorEntry) {
871
+ if (!theirs && accumulatorEntry) {
804
872
  theirs = await this.applyPatchToContent(
805
873
  accumulatorEntry.content,
806
874
  patch.patch_content,
@@ -859,7 +927,7 @@ var ReplayApplicator = class {
859
927
  reason: "missing-content"
860
928
  };
861
929
  }
862
- if (!base || !ours) {
930
+ if (!base && !useAccumulatorAsMergeBase || !ours) {
863
931
  return {
864
932
  file: resolvedPath,
865
933
  status: "skipped",
@@ -867,11 +935,18 @@ var ReplayApplicator = class {
867
935
  };
868
936
  }
869
937
  const mergeBase = useAccumulatorAsMergeBase && accumulatorEntry ? accumulatorEntry.content : base;
938
+ if (mergeBase == null) {
939
+ return {
940
+ file: resolvedPath,
941
+ status: "skipped",
942
+ reason: "missing-content"
943
+ };
944
+ }
870
945
  const merged = threeWayMerge(mergeBase, ours, effective_theirs);
871
946
  const outDir = (0, import_node_path2.dirname)(oursPath);
872
947
  await (0, import_promises.mkdir)(outDir, { recursive: true });
873
948
  await (0, import_promises.writeFile)(oursPath, merged.content);
874
- if (effective_theirs && base) {
949
+ if (effective_theirs) {
875
950
  this.fileTheirsAccumulator.set(resolvedPath, {
876
951
  content: effective_theirs,
877
952
  baseGeneration: patch.base_generation
@@ -1114,36 +1189,6 @@ var import_node_fs2 = require("fs");
1114
1189
  var import_node_path3 = require("path");
1115
1190
  var import_minimatch2 = require("minimatch");
1116
1191
  init_GitClient();
1117
-
1118
- // src/conflict-utils.ts
1119
- function stripConflictMarkers(content) {
1120
- const lines = content.split("\n");
1121
- const result = [];
1122
- let inConflict = false;
1123
- let inOurs = false;
1124
- for (const line of lines) {
1125
- if (line.startsWith("<<<<<<< ")) {
1126
- inConflict = true;
1127
- inOurs = true;
1128
- continue;
1129
- }
1130
- if (inConflict && line === "=======") {
1131
- inOurs = false;
1132
- continue;
1133
- }
1134
- if (inConflict && line.startsWith(">>>>>>> ")) {
1135
- inConflict = false;
1136
- inOurs = false;
1137
- continue;
1138
- }
1139
- if (!inConflict || inOurs) {
1140
- result.push(line);
1141
- }
1142
- }
1143
- return result.join("\n");
1144
- }
1145
-
1146
- // src/ReplayService.ts
1147
1192
  var ReplayService = class {
1148
1193
  git;
1149
1194
  detector;
@@ -2111,30 +2156,145 @@ function computeContentHash(patchContent) {
2111
2156
 
2112
2157
  // src/commands/forget.ts
2113
2158
  var import_minimatch4 = require("minimatch");
2114
- function forget(outputDir, filePattern, options) {
2159
+ function parseDiffStat(patchContent) {
2160
+ let additions = 0;
2161
+ let deletions = 0;
2162
+ let inDiffHunk = false;
2163
+ for (const line of patchContent.split("\n")) {
2164
+ if (line.startsWith("diff --git ")) {
2165
+ inDiffHunk = true;
2166
+ continue;
2167
+ }
2168
+ if (!inDiffHunk) continue;
2169
+ if (line.startsWith("+") && !line.startsWith("+++")) {
2170
+ additions++;
2171
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
2172
+ deletions++;
2173
+ }
2174
+ }
2175
+ return { additions, deletions };
2176
+ }
2177
+ function toMatchedPatch(patch) {
2178
+ return {
2179
+ id: patch.id,
2180
+ message: patch.original_message,
2181
+ files: patch.files,
2182
+ diffstat: parseDiffStat(patch.patch_content),
2183
+ ...patch.status ? { status: patch.status } : {}
2184
+ };
2185
+ }
2186
+ function matchesPatch(patch, pattern) {
2187
+ const fileMatch = patch.files.some(
2188
+ (file) => file === pattern || (0, import_minimatch4.minimatch)(file, pattern)
2189
+ );
2190
+ if (fileMatch) return true;
2191
+ return patch.original_message.toLowerCase().includes(pattern.toLowerCase());
2192
+ }
2193
+ function buildWarnings(patches) {
2194
+ const warnings = [];
2195
+ for (const patch of patches) {
2196
+ if (patch.status === "resolving") {
2197
+ warnings.push(
2198
+ `patch ${patch.id} has conflict markers in these files: ${patch.files.join(", ")}. Run \`git checkout -- <files>\` to restore the generated versions.`
2199
+ );
2200
+ } else if (patch.status === "unresolved") {
2201
+ warnings.push(
2202
+ `patch ${patch.id} had unresolved conflicts (files: ${patch.files.join(", ")}).`
2203
+ );
2204
+ }
2205
+ }
2206
+ return warnings;
2207
+ }
2208
+ var EMPTY_RESULT = {
2209
+ initialized: false,
2210
+ removed: [],
2211
+ remaining: 0,
2212
+ notFound: false,
2213
+ alreadyForgotten: [],
2214
+ totalPatches: 0,
2215
+ warnings: []
2216
+ };
2217
+ function forget(outputDir, options) {
2115
2218
  const lockManager = new LockfileManager(outputDir);
2116
2219
  if (!lockManager.exists()) {
2117
- return { removed: [], notFound: true };
2220
+ return { ...EMPTY_RESULT };
2118
2221
  }
2119
2222
  const lock = lockManager.read();
2120
- const matchingPatches = lock.patches.filter(
2121
- (patch) => patch.files.some((file) => file === filePattern || (0, import_minimatch4.minimatch)(file, filePattern))
2122
- );
2123
- if (matchingPatches.length === 0) {
2124
- return { removed: [], notFound: true };
2223
+ const totalPatches = lock.patches.length;
2224
+ if (options?.all) {
2225
+ const removed = lock.patches.map(toMatchedPatch);
2226
+ const warnings = buildWarnings(lock.patches);
2227
+ if (!options.dryRun) {
2228
+ for (const patch of lock.patches) {
2229
+ lockManager.addForgottenHash(patch.content_hash);
2230
+ }
2231
+ lockManager.clearPatches();
2232
+ lockManager.save();
2233
+ }
2234
+ return {
2235
+ initialized: true,
2236
+ removed,
2237
+ remaining: 0,
2238
+ notFound: false,
2239
+ alreadyForgotten: [],
2240
+ totalPatches,
2241
+ warnings
2242
+ };
2125
2243
  }
2126
- const removed = matchingPatches.map((p) => ({
2127
- id: p.id,
2128
- message: p.original_message,
2129
- files: p.files
2130
- }));
2131
- if (!options?.dryRun) {
2132
- for (const patch of matchingPatches) {
2133
- lockManager.removePatch(patch.id);
2244
+ if (options?.patchIds && options.patchIds.length > 0) {
2245
+ const removed = [];
2246
+ const alreadyForgotten = [];
2247
+ const patchesToRemove = [];
2248
+ for (const id of options.patchIds) {
2249
+ const patch = lock.patches.find((p) => p.id === id);
2250
+ if (patch) {
2251
+ removed.push(toMatchedPatch(patch));
2252
+ patchesToRemove.push(patch);
2253
+ } else {
2254
+ alreadyForgotten.push(id);
2255
+ }
2134
2256
  }
2135
- lockManager.save();
2257
+ const warnings = buildWarnings(patchesToRemove);
2258
+ if (!options.dryRun) {
2259
+ for (const patch of patchesToRemove) {
2260
+ lockManager.addForgottenHash(patch.content_hash);
2261
+ lockManager.removePatch(patch.id);
2262
+ }
2263
+ lockManager.save();
2264
+ }
2265
+ return {
2266
+ initialized: true,
2267
+ removed,
2268
+ remaining: totalPatches - removed.length,
2269
+ notFound: removed.length === 0 && alreadyForgotten.length > 0,
2270
+ alreadyForgotten,
2271
+ totalPatches,
2272
+ warnings
2273
+ };
2274
+ }
2275
+ if (options?.pattern) {
2276
+ const matched = lock.patches.filter((p) => matchesPatch(p, options.pattern)).map(toMatchedPatch);
2277
+ return {
2278
+ initialized: true,
2279
+ removed: [],
2280
+ remaining: totalPatches,
2281
+ notFound: matched.length === 0,
2282
+ alreadyForgotten: [],
2283
+ totalPatches,
2284
+ warnings: [],
2285
+ matched
2286
+ };
2136
2287
  }
2137
- return { removed, notFound: false };
2288
+ return {
2289
+ initialized: true,
2290
+ removed: [],
2291
+ remaining: totalPatches,
2292
+ notFound: totalPatches === 0,
2293
+ alreadyForgotten: [],
2294
+ totalPatches,
2295
+ warnings: [],
2296
+ matched: lock.patches.map(toMatchedPatch)
2297
+ };
2138
2298
  }
2139
2299
 
2140
2300
  // src/commands/reset.ts
@@ -2256,24 +2416,49 @@ async function getChangedFiles(git, currentGen, files) {
2256
2416
  function status(outputDir) {
2257
2417
  const lockManager = new LockfileManager(outputDir);
2258
2418
  if (!lockManager.exists()) {
2259
- return { initialized: false, patches: [], lastGeneration: void 0 };
2419
+ return {
2420
+ initialized: false,
2421
+ generationCount: 0,
2422
+ lastGeneration: void 0,
2423
+ patches: [],
2424
+ unresolvedCount: 0,
2425
+ excludePatterns: []
2426
+ };
2260
2427
  }
2261
2428
  const lock = lockManager.read();
2262
2429
  const patches = lock.patches.map((patch) => ({
2263
- sha: patch.original_commit.slice(0, 7),
2264
- author: patch.original_author.split("<")[0]?.trim() ?? "unknown",
2430
+ id: patch.id,
2431
+ type: patch.patch_content.includes("new file mode") ? "added" : "modified",
2265
2432
  message: patch.original_message,
2266
- files: patch.files
2433
+ author: patch.original_author.split("<")[0]?.trim() || "unknown",
2434
+ sha: patch.original_commit.slice(0, 7),
2435
+ files: patch.files,
2436
+ fileCount: patch.files.length,
2437
+ ...patch.status ? { status: patch.status } : {}
2267
2438
  }));
2439
+ const unresolvedCount = lock.patches.filter(
2440
+ (p) => p.status === "unresolved" || p.status === "resolving"
2441
+ ).length;
2268
2442
  let lastGeneration;
2269
2443
  const lastGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
2270
2444
  if (lastGen) {
2271
2445
  lastGeneration = {
2272
- sha: lastGen.commit_sha,
2273
- timestamp: lastGen.timestamp
2446
+ sha: lastGen.commit_sha.slice(0, 7),
2447
+ timestamp: lastGen.timestamp,
2448
+ cliVersion: lastGen.cli_version,
2449
+ generatorVersions: lastGen.generator_versions
2274
2450
  };
2275
2451
  }
2276
- return { initialized: true, patches, lastGeneration };
2452
+ const config = lockManager.getCustomizationsConfig();
2453
+ const excludePatterns = config.exclude ?? [];
2454
+ return {
2455
+ initialized: true,
2456
+ generationCount: lock.generations.length,
2457
+ lastGeneration,
2458
+ patches,
2459
+ unresolvedCount,
2460
+ excludePatterns
2461
+ };
2277
2462
  }
2278
2463
  // Annotate the CommonJS export names for ESM import in node:
2279
2464
  0 && (module.exports = {