@fern-api/replay 0.11.0 → 0.12.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
@@ -493,7 +493,7 @@ var FERN_SUPPORT_NAMES = ["fern-support", "Fern Support"];
493
493
  function isGenerationCommit(commit) {
494
494
  const isFernSupport = FERN_SUPPORT_NAMES.includes(commit.authorName);
495
495
  const isBotAuthor = !isFernSupport && (commit.authorLogin === FERN_BOT_LOGIN || commit.authorEmail === FERN_BOT_EMAIL || commit.authorName === FERN_BOT_NAME);
496
- const hasGenerationMarker = commit.message.startsWith("[fern-generated]") || commit.message.startsWith("[fern-replay]") || commit.message.includes("Generated by Fern") || commit.message.includes("\u{1F916} Generated with Fern") || // Squash merge of a Fern-generated PR uses the PR title as commit message.
496
+ const hasGenerationMarker = commit.message.startsWith("[fern-generated]") || commit.message.startsWith("[fern-replay]") || commit.message.startsWith("[fern-autoversion]") || commit.message.includes("Generated by Fern") || commit.message.includes("\u{1F916} Generated with Fern") || // Squash merge of a Fern-generated PR uses the PR title as commit message.
497
497
  // The default PR title is "SDK Generation" (from GithubStep's commitMessage default).
498
498
  // GitHub appends "(#N)" for the PR number, e.g. "SDK Generation (#70)".
499
499
  commit.message.startsWith("SDK Generation");
@@ -2180,21 +2180,66 @@ var ReplayService = class {
2180
2180
  this.applicator = new ReplayApplicator(git, this.lockManager, outputDir);
2181
2181
  this.committer = new ReplayCommitter(git, outputDir);
2182
2182
  }
2183
- async runReplay(options) {
2183
+ /**
2184
+ * Phase 1 of the two-phase replay flow. Runs preGenerationRebase (when applicable),
2185
+ * detects new patches, and creates the `[fern-generated]` commit. Leaves HEAD at
2186
+ * the generation commit (or unchanged for dry-run).
2187
+ *
2188
+ * Does NOT apply patches — the returned handle must be passed to
2189
+ * `applyPreparedReplay()` to complete the run. No disk writes to the lockfile
2190
+ * happen here; lockfile persistence is deferred to phase 2.
2191
+ *
2192
+ * Callers may land additional commits on HEAD between `prepareReplay` and
2193
+ * `applyPreparedReplay` (e.g., `[fern-autoversion]`).
2194
+ */
2195
+ async prepareReplay(options) {
2184
2196
  this.warnings = [];
2197
+ this.detector.warnings.length = 0;
2185
2198
  if (options?.skipApplication) {
2186
- return this.handleSkipApplication(options);
2199
+ return this._prepareSkipApplication(options);
2187
2200
  }
2188
2201
  const flow = this.determineFlow();
2189
2202
  switch (flow) {
2190
2203
  case "first-generation":
2191
- return this.handleFirstGeneration(options);
2204
+ return this._prepareFirstGeneration(options);
2192
2205
  case "no-patches":
2193
- return this.handleNoPatchesRegeneration(options);
2206
+ return this._prepareNoPatchesRegeneration(options);
2194
2207
  case "normal-regeneration":
2195
- return this.handleNormalRegeneration(options);
2208
+ return this._prepareNormalRegeneration(options);
2196
2209
  }
2197
2210
  }
2211
+ /**
2212
+ * Phase 2 of the two-phase replay flow. Applies patches, rebases them against
2213
+ * the generation, saves the lockfile, and commits `[fern-replay]` (or stages
2214
+ * under `stageOnly`). Operates on HEAD at call time — mid-phase commits are
2215
+ * respected.
2216
+ *
2217
+ * For terminal handles (dry-run, first-gen has no patch work, skip-app does
2218
+ * its own post-commit logic), this returns the precomputed report.
2219
+ */
2220
+ async applyPreparedReplay(prep, options) {
2221
+ if (prep._prepared.terminal) {
2222
+ return prep._prepared.preparedReport;
2223
+ }
2224
+ switch (prep._prepared.flow) {
2225
+ case "first-generation":
2226
+ return this._applyFirstGeneration(prep);
2227
+ case "skip-application":
2228
+ return this._applySkipApplication(prep, options);
2229
+ case "no-patches":
2230
+ return this._applyNoPatchesRegeneration(prep, options);
2231
+ case "normal-regeneration":
2232
+ return this._applyNormalRegeneration(prep, options);
2233
+ }
2234
+ }
2235
+ /**
2236
+ * Backwards-compatible atomic entry point. Composes `prepareReplay` + `applyPreparedReplay`.
2237
+ * Behavior is byte-identical to the pre-split implementation for all existing callers.
2238
+ */
2239
+ async runReplay(options) {
2240
+ const prep = await this.prepareReplay(options);
2241
+ return this.applyPreparedReplay(prep, options);
2242
+ }
2198
2243
  /**
2199
2244
  * Sync the lockfile after a divergent PR was squash-merged.
2200
2245
  * Call this BEFORE runReplay() when the CLI detects a merged divergent PR.
@@ -2268,15 +2313,35 @@ var ReplayService = class {
2268
2313
  throw error;
2269
2314
  }
2270
2315
  }
2271
- async handleFirstGeneration(options) {
2316
+ firstGenerationReport() {
2317
+ return {
2318
+ flow: "first-generation",
2319
+ patchesDetected: 0,
2320
+ patchesApplied: 0,
2321
+ patchesWithConflicts: 0,
2322
+ patchesSkipped: 0,
2323
+ conflicts: []
2324
+ };
2325
+ }
2326
+ async _prepareFirstGeneration(options) {
2272
2327
  if (options?.dryRun) {
2328
+ const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
2273
2329
  return {
2274
2330
  flow: "first-generation",
2275
- patchesDetected: 0,
2276
- patchesApplied: 0,
2277
- patchesWithConflicts: 0,
2278
- patchesSkipped: 0,
2279
- conflicts: []
2331
+ previousGenerationSha: null,
2332
+ currentGenerationSha: headSha,
2333
+ baseBranchHead: options.baseBranchHead ?? null,
2334
+ _prepared: {
2335
+ flow: "first-generation",
2336
+ terminal: true,
2337
+ preparedReport: this.firstGenerationReport(),
2338
+ patchesToApply: [],
2339
+ newPatchIds: /* @__PURE__ */ new Set(),
2340
+ detectorWarnings: [],
2341
+ revertedCount: 0,
2342
+ genSha: headSha,
2343
+ optionsSnapshot: options
2344
+ }
2280
2345
  };
2281
2346
  }
2282
2347
  const commitOpts = options ? {
@@ -2286,22 +2351,36 @@ var ReplayService = class {
2286
2351
  } : void 0;
2287
2352
  await this.committer.commitGeneration("Initial SDK generation", commitOpts);
2288
2353
  const genRecord = await this.committer.createGenerationRecord(commitOpts);
2289
- this.lockManager.initialize(genRecord);
2354
+ this.lockManager.initializeInMemory(genRecord);
2290
2355
  return {
2291
2356
  flow: "first-generation",
2292
- patchesDetected: 0,
2293
- patchesApplied: 0,
2294
- patchesWithConflicts: 0,
2295
- patchesSkipped: 0,
2296
- conflicts: []
2357
+ previousGenerationSha: null,
2358
+ currentGenerationSha: genRecord.commit_sha,
2359
+ baseBranchHead: options?.baseBranchHead ?? null,
2360
+ _prepared: {
2361
+ flow: "first-generation",
2362
+ terminal: false,
2363
+ patchesToApply: [],
2364
+ newPatchIds: /* @__PURE__ */ new Set(),
2365
+ detectorWarnings: [],
2366
+ revertedCount: 0,
2367
+ genSha: genRecord.commit_sha,
2368
+ optionsSnapshot: options
2369
+ }
2297
2370
  };
2298
2371
  }
2372
+ async _applyFirstGeneration(_prep) {
2373
+ this.lockManager.save();
2374
+ return this.firstGenerationReport();
2375
+ }
2299
2376
  /**
2300
- * Skip-application mode: commit the generation and update the lockfile
2301
- * but don't detect or apply patches. Sets a marker so the next normal
2302
- * run skips revert detection in preGenerationRebase().
2377
+ * Skip-application mode phase 1: commit the generation and update the
2378
+ * in-memory lockfile, but don't detect or apply patches. Sets a marker
2379
+ * so the next normal run skips revert detection in preGenerationRebase().
2380
+ *
2381
+ * Disk save is deferred to `_applySkipApplication`.
2303
2382
  */
2304
- async handleSkipApplication(options) {
2383
+ async _prepareSkipApplication(options) {
2305
2384
  const commitOpts = options ? {
2306
2385
  cliVersion: options.cliVersion ?? "unknown",
2307
2386
  generatorVersions: options.generatorVersions ?? {},
@@ -2310,8 +2389,10 @@ var ReplayService = class {
2310
2389
  await this.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
2311
2390
  await this.cleanupStaleConflictMarkers();
2312
2391
  const genRecord = await this.committer.createGenerationRecord(commitOpts);
2392
+ let previousGenerationSha = null;
2313
2393
  try {
2314
- this.lockManager.read();
2394
+ const lock = this.lockManager.read();
2395
+ previousGenerationSha = lock.current_generation ?? null;
2315
2396
  this.lockManager.addGeneration(genRecord);
2316
2397
  } catch (error) {
2317
2398
  if (error instanceof LockfileNotFoundError) {
@@ -2321,6 +2402,24 @@ var ReplayService = class {
2321
2402
  }
2322
2403
  }
2323
2404
  this.lockManager.setReplaySkippedAt((/* @__PURE__ */ new Date()).toISOString());
2405
+ return {
2406
+ flow: "skip-application",
2407
+ previousGenerationSha,
2408
+ currentGenerationSha: genRecord.commit_sha,
2409
+ baseBranchHead: options?.baseBranchHead ?? null,
2410
+ _prepared: {
2411
+ flow: "skip-application",
2412
+ terminal: false,
2413
+ patchesToApply: [],
2414
+ newPatchIds: /* @__PURE__ */ new Set(),
2415
+ detectorWarnings: [],
2416
+ revertedCount: 0,
2417
+ genSha: genRecord.commit_sha,
2418
+ optionsSnapshot: options
2419
+ }
2420
+ };
2421
+ }
2422
+ async _applySkipApplication(_prep, options) {
2324
2423
  this.lockManager.save();
2325
2424
  const credentialWarnings = [...this.lockManager.warnings];
2326
2425
  if (!options?.stageOnly) {
@@ -2338,12 +2437,14 @@ var ReplayService = class {
2338
2437
  warnings: credentialWarnings.length > 0 ? credentialWarnings : void 0
2339
2438
  };
2340
2439
  }
2341
- async handleNoPatchesRegeneration(options) {
2440
+ async _prepareNoPatchesRegeneration(options) {
2441
+ const preLock = this.lockManager.read();
2442
+ const previousGenerationSha = preLock.current_generation ?? null;
2342
2443
  let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
2343
2444
  const detectorWarnings = [...this.detector.warnings];
2344
2445
  if (options?.dryRun) {
2345
2446
  const dryRunWarnings = [...detectorWarnings, ...this.warnings];
2346
- return {
2447
+ const preparedReport = {
2347
2448
  flow: "no-patches",
2348
2449
  patchesDetected: newPatches.length,
2349
2450
  patchesApplied: 0,
@@ -2354,9 +2455,26 @@ var ReplayService = class {
2354
2455
  wouldApply: newPatches,
2355
2456
  warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0
2356
2457
  };
2458
+ const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
2459
+ return {
2460
+ flow: "no-patches",
2461
+ previousGenerationSha,
2462
+ currentGenerationSha: headSha,
2463
+ baseBranchHead: options.baseBranchHead ?? null,
2464
+ _prepared: {
2465
+ flow: "no-patches",
2466
+ terminal: true,
2467
+ preparedReport,
2468
+ patchesToApply: [],
2469
+ newPatchIds: /* @__PURE__ */ new Set(),
2470
+ detectorWarnings,
2471
+ revertedCount: revertedPatchIds.length,
2472
+ genSha: headSha,
2473
+ optionsSnapshot: options
2474
+ }
2475
+ };
2357
2476
  }
2358
2477
  {
2359
- const preLock = this.lockManager.read();
2360
2478
  const preCurrentGen = preLock.current_generation;
2361
2479
  const uniqueFiles = [...new Set(newPatches.flatMap((p) => p.files))];
2362
2480
  const absorbedFiles = /* @__PURE__ */ new Set();
@@ -2387,6 +2505,27 @@ var ReplayService = class {
2387
2505
  await this.cleanupStaleConflictMarkers();
2388
2506
  const genRecord = await this.committer.createGenerationRecord(commitOpts);
2389
2507
  this.lockManager.addGeneration(genRecord);
2508
+ return {
2509
+ flow: "no-patches",
2510
+ previousGenerationSha,
2511
+ currentGenerationSha: genRecord.commit_sha,
2512
+ baseBranchHead: options?.baseBranchHead ?? null,
2513
+ _prepared: {
2514
+ flow: "no-patches",
2515
+ terminal: false,
2516
+ patchesToApply: newPatches,
2517
+ newPatchIds: new Set(newPatches.map((p) => p.id)),
2518
+ detectorWarnings,
2519
+ revertedCount: revertedPatchIds.length,
2520
+ genSha: genRecord.commit_sha,
2521
+ optionsSnapshot: options
2522
+ }
2523
+ };
2524
+ }
2525
+ async _applyNoPatchesRegeneration(prep, options) {
2526
+ const newPatches = prep._prepared.patchesToApply;
2527
+ const detectorWarnings = prep._prepared.detectorWarnings;
2528
+ const genSha = prep._prepared.genSha;
2390
2529
  let results = [];
2391
2530
  if (newPatches.length > 0) {
2392
2531
  results = await this.applicator.applyPatches(newPatches);
@@ -2398,7 +2537,7 @@ var ReplayService = class {
2398
2537
  }
2399
2538
  }
2400
2539
  }
2401
- const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
2540
+ const rebaseCounts = await this.rebasePatches(results, genSha);
2402
2541
  for (const patch of newPatches) {
2403
2542
  if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
2404
2543
  this.lockManager.addPatch(patch);
@@ -2415,15 +2554,26 @@ var ReplayService = class {
2415
2554
  }
2416
2555
  }
2417
2556
  const warnings = [...detectorWarnings, ...this.warnings];
2418
- return this.buildReport("no-patches", newPatches, results, options, warnings, rebaseCounts, void 0, revertedPatchIds.length);
2557
+ return this.buildReport(
2558
+ "no-patches",
2559
+ newPatches,
2560
+ results,
2561
+ prep._prepared.optionsSnapshot,
2562
+ warnings,
2563
+ rebaseCounts,
2564
+ void 0,
2565
+ prep._prepared.revertedCount
2566
+ );
2419
2567
  }
2420
- async handleNormalRegeneration(options) {
2568
+ async _prepareNormalRegeneration(options) {
2569
+ const preLock = this.lockManager.read();
2570
+ const previousGenerationSha = preLock.current_generation ?? null;
2421
2571
  if (options?.dryRun) {
2422
2572
  const existingPatches2 = this.lockManager.getPatches();
2423
2573
  const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.detector.detectNewPatches();
2424
- const warnings2 = [...this.detector.warnings, ...this.warnings];
2574
+ const warnings = [...this.detector.warnings, ...this.warnings];
2425
2575
  const allPatches2 = [...existingPatches2, ...newPatches2];
2426
- return {
2576
+ const preparedReport = {
2427
2577
  flow: "normal-regeneration",
2428
2578
  patchesDetected: allPatches2.length,
2429
2579
  patchesApplied: 0,
@@ -2432,7 +2582,25 @@ var ReplayService = class {
2432
2582
  patchesReverted: dryRunReverted.length,
2433
2583
  conflicts: [],
2434
2584
  wouldApply: allPatches2,
2435
- warnings: warnings2.length > 0 ? warnings2 : void 0
2585
+ warnings: warnings.length > 0 ? warnings : void 0
2586
+ };
2587
+ const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
2588
+ return {
2589
+ flow: "normal-regeneration",
2590
+ previousGenerationSha,
2591
+ currentGenerationSha: headSha,
2592
+ baseBranchHead: options.baseBranchHead ?? null,
2593
+ _prepared: {
2594
+ flow: "normal-regeneration",
2595
+ terminal: true,
2596
+ preparedReport,
2597
+ patchesToApply: [],
2598
+ newPatchIds: /* @__PURE__ */ new Set(),
2599
+ detectorWarnings: [...this.detector.warnings],
2600
+ revertedCount: dryRunReverted.length,
2601
+ genSha: headSha,
2602
+ optionsSnapshot: options
2603
+ }
2436
2604
  };
2437
2605
  }
2438
2606
  let existingPatches = this.lockManager.getPatches();
@@ -2484,6 +2652,29 @@ var ReplayService = class {
2484
2652
  await this.cleanupStaleConflictMarkers();
2485
2653
  const genRecord = await this.committer.createGenerationRecord(commitOpts);
2486
2654
  this.lockManager.addGeneration(genRecord);
2655
+ return {
2656
+ flow: "normal-regeneration",
2657
+ previousGenerationSha,
2658
+ currentGenerationSha: genRecord.commit_sha,
2659
+ baseBranchHead: options?.baseBranchHead ?? null,
2660
+ _prepared: {
2661
+ flow: "normal-regeneration",
2662
+ terminal: false,
2663
+ patchesToApply: allPatches,
2664
+ newPatchIds: new Set(newPatches.map((p) => p.id)),
2665
+ preRebaseCounts,
2666
+ detectorWarnings,
2667
+ revertedCount: revertedPatchIds.length,
2668
+ genSha: genRecord.commit_sha,
2669
+ optionsSnapshot: options
2670
+ }
2671
+ };
2672
+ }
2673
+ async _applyNormalRegeneration(prep, options) {
2674
+ const allPatches = prep._prepared.patchesToApply;
2675
+ const newPatchIds = prep._prepared.newPatchIds;
2676
+ const detectorWarnings = prep._prepared.detectorWarnings;
2677
+ const genSha = prep._prepared.genSha;
2487
2678
  const results = await this.applicator.applyPatches(allPatches);
2488
2679
  this._lastApplyResults = results;
2489
2680
  this.revertConflictingFiles(results);
@@ -2492,7 +2683,8 @@ var ReplayService = class {
2492
2683
  result.patch.status = "unresolved";
2493
2684
  }
2494
2685
  }
2495
- const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
2686
+ const rebaseCounts = await this.rebasePatches(results, genSha);
2687
+ const newPatches = allPatches.filter((p) => newPatchIds.has(p.id));
2496
2688
  for (const patch of newPatches) {
2497
2689
  if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
2498
2690
  this.lockManager.addPatch(patch);
@@ -2519,11 +2711,11 @@ var ReplayService = class {
2519
2711
  "normal-regeneration",
2520
2712
  allPatches,
2521
2713
  results,
2522
- options,
2714
+ prep._prepared.optionsSnapshot,
2523
2715
  warnings,
2524
2716
  rebaseCounts,
2525
- preRebaseCounts,
2526
- revertedPatchIds.length
2717
+ prep._prepared.preRebaseCounts,
2718
+ prep._prepared.revertedCount
2527
2719
  );
2528
2720
  }
2529
2721
  /**