@fern-api/generator-cli 0.9.11 → 0.9.13

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 (4) hide show
  1. package/dist/api.d.ts +101 -31
  2. package/dist/api.js +1692 -276
  3. package/dist/cli.js +17471 -11396
  4. package/package.json +4 -2
package/dist/api.js CHANGED
@@ -793,12 +793,12 @@ var require_src2 = __commonJS({
793
793
  function check(path3, isFile, isDirectory) {
794
794
  log(`checking %s`, path3);
795
795
  try {
796
- const stat = fs_1.statSync(path3);
797
- if (stat.isFile() && isFile) {
796
+ const stat2 = fs_1.statSync(path3);
797
+ if (stat2.isFile() && isFile) {
798
798
  log(`[OK] path represents a file`);
799
799
  return true;
800
800
  }
801
- if (stat.isDirectory() && isDirectory) {
801
+ if (stat2.isDirectory() && isDirectory) {
802
802
  log(`[OK] path represents a directory`);
803
803
  return true;
804
804
  }
@@ -10060,46 +10060,1662 @@ var consolePipelineLogger = {
10060
10060
  };
10061
10061
 
10062
10062
  // src/pipeline/PostGenerationPipeline.ts
10063
- var import_child_process4 = require("child_process");
10063
+ var import_child_process5 = require("child_process");
10064
10064
 
10065
10065
  // src/pipeline/github/constants.ts
10066
10066
  var FERN_BOT_NAME = "fern-api";
10067
10067
  var FERN_BOT_EMAIL = "115122769+fern-api[bot]@users.noreply.github.com";
10068
10068
  var FERN_BOT_LOGIN = "fern-api[bot]";
10069
10069
 
10070
- // src/pipeline/steps/BaseStep.ts
10071
- var BaseStep = class {
10072
- constructor(outputDir, logger) {
10073
- this.outputDir = outputDir;
10074
- this.logger = logger;
10070
+ // src/pipeline/steps/AutoVersionStep.ts
10071
+ var import_child_process3 = require("child_process");
10072
+ var import_fs3 = require("fs");
10073
+ var import_promises3 = require("fs/promises");
10074
+ var import_path4 = require("path");
10075
+
10076
+ // src/autoversion/AutoVersioningService.ts
10077
+ var import_logging_execa = require("@fern-api/logging-execa");
10078
+ var import_fs2 = require("fs");
10079
+ var import_promises2 = require("fs/promises");
10080
+ var import_path3 = require("path");
10081
+ var import_semver = __toESM(require("semver"));
10082
+ var AutoVersioningException = class extends Error {
10083
+ /**
10084
+ * True when the magic version was not found at all in the diff,
10085
+ * indicating a generator that doesn't embed versions in source files
10086
+ * (e.g., Swift uses git tags via SPM). Only this case should fall back
10087
+ * to reading git tags.
10088
+ */
10089
+ magicVersionAbsent;
10090
+ constructor(message, options) {
10091
+ super(message);
10092
+ this.name = "AutoVersioningException";
10093
+ this.magicVersionAbsent = options?.magicVersionAbsent ?? false;
10094
+ if (options?.cause) {
10095
+ this.cause = options.cause;
10096
+ }
10075
10097
  }
10076
- outputDir;
10077
- logger;
10078
10098
  };
10079
-
10080
- // src/pipeline/steps/AutoVersionStep.ts
10081
- var AutoVersionStep = class extends BaseStep {
10082
- constructor(outputDir, logger, config) {
10099
+ function countFilesInDiff(diffContent) {
10100
+ const marker = "diff --git ";
10101
+ let count = 0;
10102
+ let pos = 0;
10103
+ while (true) {
10104
+ const idx = diffContent.indexOf(marker, pos);
10105
+ if (idx === -1) {
10106
+ break;
10107
+ }
10108
+ if (idx === 0 || diffContent[idx - 1] === "\n") {
10109
+ count++;
10110
+ }
10111
+ pos = idx + marker.length;
10112
+ }
10113
+ return count;
10114
+ }
10115
+ function formatSizeKB(charLength) {
10116
+ return (charLength / 1024).toFixed(1);
10117
+ }
10118
+ var EXCLUDED_FILE_PATTERNS = [
10119
+ // Documentation / generated reference
10120
+ /(?:^|\/)reference\.md$/i,
10121
+ /(?:^|\/)changelog(?:\.[^/]*)?$/i,
10122
+ /(?:^|\/)readme(?:\.[^/]*)?$/i,
10123
+ /(?:^|\/)license(?:\.[^/]*)?$/i,
10124
+ // Lock files (dependency resolution, zero semantic value)
10125
+ /(?:^|\/)pnpm-lock\.yaml$/i,
10126
+ /(?:^|\/)yarn\.lock$/i,
10127
+ /(?:^|\/)package-lock\.json$/i,
10128
+ /(?:^|\/)poetry\.lock$/i,
10129
+ /(?:^|\/)gemfile\.lock$/i,
10130
+ /(?:^|\/)go\.sum$/i,
10131
+ /(?:^|\/)cargo\.lock$/i,
10132
+ /(?:^|\/)composer\.lock$/i,
10133
+ /\.lock$/i,
10134
+ // catch-all for any other lockfiles
10135
+ // Test files (by naming convention only — directory patterns like tests/ and
10136
+ // test/ are intentionally omitted because a customer's API domain could use
10137
+ // those names, e.g. a QA platform SDK with a "tests" resource)
10138
+ /\.test\.ts$/i,
10139
+ /\.test\.js$/i,
10140
+ /\.test\.py$/i,
10141
+ /\.spec\.ts$/i,
10142
+ /\.spec\.js$/i,
10143
+ /_test\.go$/i,
10144
+ /_test\.py$/i,
10145
+ /Test\.java$/,
10146
+ /(?:^|\/)__tests__\//i,
10147
+ // __tests__/ is unambiguously a test directory
10148
+ /(?:^|\/)wiremock\//i,
10149
+ // Fern-generated WireMock test fixtures
10150
+ // Snapshot files
10151
+ /(?:^|\/)__snapshots__\//i,
10152
+ /\.snap$/i,
10153
+ // CI / editor config (no API surface relevance)
10154
+ /(?:^|\/)\.github\//i,
10155
+ /(?:^|\/)\.circleci\//i,
10156
+ /(?:^|\/)\.editorconfig$/i,
10157
+ /(?:^|\/)\.prettierrc/i,
10158
+ /(?:^|\/)biome\.json$/i,
10159
+ // Linting / static analysis config
10160
+ /(?:^|\/)\.eslintrc/i,
10161
+ /(?:^|\/)eslint\.config\./i,
10162
+ /(?:^|\/)\.rubocop/i,
10163
+ // Ruby linter
10164
+ /(?:^|\/)phpstan\.neon$/i,
10165
+ // PHP static analysis
10166
+ /(?:^|\/)phpunit\.xml$/i,
10167
+ // PHP test runner config
10168
+ /(?:^|\/)\.pylintrc$/i,
10169
+ // Python linter
10170
+ /(?:^|\/)pylintrc$/i,
10171
+ /(?:^|\/)\.flake8$/i,
10172
+ // Python linter
10173
+ /(?:^|\/)\.mypy\.ini$/i,
10174
+ // Python type checker
10175
+ /(?:^|\/)mypy\.ini$/i,
10176
+ /(?:^|\/)\.swiftlint\.yml$/i,
10177
+ // Swift linter
10178
+ /(?:^|\/)\.scalafmt\.conf$/i,
10179
+ // Scala formatter
10180
+ /(?:^|\/)rustfmt\.toml$/i,
10181
+ // Rust formatter
10182
+ /(?:^|\/)\.stylelintrc/i,
10183
+ // CSS linter
10184
+ // Build / devtool config (no API surface)
10185
+ /(?:^|\/)tsconfig[^/]*\.json$/i,
10186
+ // TypeScript compiler config
10187
+ /(?:^|\/)vitest\.config\./i,
10188
+ // Test runner config
10189
+ /(?:^|\/)jest\.config\./i,
10190
+ // Test runner config
10191
+ /(?:^|\/)pnpm-workspace\.yaml$/i,
10192
+ // Workspace config
10193
+ /(?:^|\/)\.npmrc$/i,
10194
+ // npm config
10195
+ /(?:^|\/)\.yarnrc/i,
10196
+ // Yarn config
10197
+ /(?:^|\/)tox\.ini$/i,
10198
+ // Python test runner
10199
+ /(?:^|\/)Makefile$/i,
10200
+ // Build automation
10201
+ /(?:^|\/)Rakefile$/i,
10202
+ // Ruby build tasks
10203
+ /(?:^|\/)contributing(?:\.[^/]*)?$/i,
10204
+ // Contributor docs
10205
+ /(?:^|\/)snippet\.json$/i,
10206
+ // Fern-generated snippet metadata
10207
+ /(?:^|\/)\.gitignore$/i,
10208
+ /(?:^|\/)\.gitattributes$/i,
10209
+ /\.slnx$/i
10210
+ // .NET solution files
10211
+ ];
10212
+ var AutoVersioningService = class _AutoVersioningService {
10213
+ logger;
10214
+ constructor({ logger }) {
10215
+ this.logger = logger;
10216
+ }
10217
+ /**
10218
+ * Extracts the previous version from a git diff by finding lines with the magic version.
10219
+ * The diff should contain lines like: -version = '1.2.3' +version = '0.0.0-fern-placeholder'
10220
+ *
10221
+ * @param diffContent The git diff content
10222
+ * @param mappedMagicVersion The magic version after language transformations (e.g., "v0.0.0-fern-placeholder" for Go)
10223
+ * @return The previous version if found, or undefined if all magic version occurrences are in new files
10224
+ * @throws AutoVersioningException if the magic version is not found at all in the diff
10225
+ */
10226
+ extractPreviousVersion(diffContent, mappedMagicVersion) {
10227
+ const lines = diffContent.split("\n");
10228
+ let currentFile = "unknown";
10229
+ let magicVersionOccurrences = 0;
10230
+ for (let i = 0; i < lines.length; i++) {
10231
+ const line = lines[i];
10232
+ if (!line) {
10233
+ continue;
10234
+ }
10235
+ if (line.startsWith("+++ b/")) {
10236
+ currentFile = line.substring(6);
10237
+ }
10238
+ if (line.startsWith("+") && !line.startsWith("+++") && line.includes(mappedMagicVersion)) {
10239
+ magicVersionOccurrences++;
10240
+ const sanitizedPlusLine = line.replace(mappedMagicVersion, "<MAGIC>");
10241
+ this.logger.debug(
10242
+ `Found placeholder version in added line (file: ${currentFile}): ${sanitizedPlusLine}`
10243
+ );
10244
+ const matchingMinusLine = this.findMatchingMinusLine(lines, i, mappedMagicVersion);
10245
+ if (matchingMinusLine == void 0) {
10246
+ this.logger.debug(
10247
+ `No matching minus line in hunk; continuing search. file=${currentFile}, plusLine=${sanitizedPlusLine}`
10248
+ );
10249
+ continue;
10250
+ }
10251
+ const extracted = this.extractPreviousVersionFromDiffLine(matchingMinusLine);
10252
+ if (extracted != void 0) {
10253
+ this.logger.debug(`Extracted previous version from diff (file: ${currentFile}): ${extracted}`);
10254
+ return extracted;
10255
+ }
10256
+ this.logger.debug(
10257
+ `Could not parse version from matching minus line (file: ${currentFile}); continuing search.`
10258
+ );
10259
+ continue;
10260
+ }
10261
+ }
10262
+ if (magicVersionOccurrences > 0) {
10263
+ this.logger.info(
10264
+ `Found placeholder version in the diff but no matching previous version lines were found in any hunk. This indicates a new SDK repository with no previous version. occurrences=${magicVersionOccurrences}`
10265
+ );
10266
+ return void 0;
10267
+ }
10268
+ throw new AutoVersioningException(
10269
+ "Failed to extract version from diff. This may indicate the version file format is not supported for auto-versioning, or the placeholder version was not found in any added lines.",
10270
+ { magicVersionAbsent: true }
10271
+ );
10272
+ }
10273
+ /**
10274
+ * Scans backwards from the given index to find a '-' line that forms a version-change pair
10275
+ * with the '+' line at the given index. Stops at hunk boundaries.
10276
+ *
10277
+ * @param lines All diff lines
10278
+ * @param plusLineIndex Index of the '+' line containing the magic version
10279
+ * @param mappedMagicVersion The magic version to match
10280
+ * @return The matching '-' line, or null if not found
10281
+ */
10282
+ findMatchingMinusLine(lines, plusLineIndex, mappedMagicVersion) {
10283
+ const plusLine = lines[plusLineIndex];
10284
+ if (!plusLine) {
10285
+ return void 0;
10286
+ }
10287
+ let linesScanned = 0;
10288
+ for (let i = plusLineIndex - 1; i >= 0; i--) {
10289
+ const line = lines[i];
10290
+ if (!line) {
10291
+ continue;
10292
+ }
10293
+ linesScanned++;
10294
+ if (this.shouldStopSearching(line)) {
10295
+ this.logger.debug(`Stopped backward scan at hunk boundary after ${linesScanned} lines`);
10296
+ break;
10297
+ }
10298
+ if (this.isDeletionLine(line)) {
10299
+ if (this.isVersionChangePair(line, plusLine, mappedMagicVersion)) {
10300
+ this.logger.debug(`Found matching minus line after scanning ${linesScanned} lines backwards`);
10301
+ return line;
10302
+ }
10303
+ }
10304
+ }
10305
+ this.logger.warn(`No matching minus line found after scanning ${linesScanned} lines backwards`);
10306
+ return void 0;
10307
+ }
10308
+ /**
10309
+ * Cleans the git diff by removing version-related changes.
10310
+ * This prevents the AI from seeing our placeholder version and the version changes themselves.
10311
+ *
10312
+ * Algorithm:
10313
+ * 1. Parse diff into file sections (each starting with "diff --git")
10314
+ * 2. For each file section, identify and remove paired - and + lines where the only difference is the magic version
10315
+ * 3. Remove entire file sections that have no remaining content changes (no + or - lines)
10316
+ *
10317
+ * @param diffContent The git diff content
10318
+ * @param mappedMagicVersion The magic version after language transformations (e.g., "v0.0.0-fern-placeholder" for Go)
10319
+ * @return Cleaned diff content
10320
+ */
10321
+ cleanDiffForAI(diffContent, mappedMagicVersion) {
10322
+ const lines = diffContent.split("\n");
10323
+ const fileSections = this.parseFileSections(lines);
10324
+ const cleanedSections = [];
10325
+ for (const section of fileSections) {
10326
+ const filePath = this.extractFilePathFromSection(section);
10327
+ if (filePath != null && this.shouldExcludeFile(filePath)) {
10328
+ this.logger.debug(`Excluding file from diff analysis: ${filePath}`);
10329
+ continue;
10330
+ }
10331
+ const cleaned = this.cleanFileSection(section, mappedMagicVersion);
10332
+ if (cleaned != void 0) {
10333
+ cleanedSections.push(cleaned);
10334
+ }
10335
+ }
10336
+ const result = [];
10337
+ for (const section of cleanedSections) {
10338
+ for (const line of section.lines) {
10339
+ result.push(line);
10340
+ }
10341
+ }
10342
+ this.logger.debug(
10343
+ `Cleaned diff: removed ${diffContent.length - result.join("\n").length} chars containing version changes`
10344
+ );
10345
+ return result.join("\n");
10346
+ }
10347
+ /**
10348
+ * Known patterns for API-surface signatures (function/method/class declarations).
10349
+ * Used by truncateDiff to prioritise sections with signature-like changes.
10350
+ *
10351
+ * Covers all languages Fern generates SDKs for:
10352
+ * - TypeScript/JavaScript: export function, export class, export interface, export type
10353
+ * - Java/C#: public class, public interface, public void, protected, internal
10354
+ * - Python: def method_name, class ClassName
10355
+ * - Go: func ExportedName (uppercase = exported)
10356
+ * - Ruby: def method_name, class ClassName, module ModuleName
10357
+ * - Swift: public func, public class, open class
10358
+ * - Rust: pub fn, pub struct, pub enum, pub trait
10359
+ * - PHP: public function, class ClassName
10360
+ */
10361
+ static SIGNATURE_PATTERNS = [
10362
+ // TypeScript / JavaScript
10363
+ /^[+-]\s*export\s+/,
10364
+ // Java / C# / Swift / PHP — access modifiers
10365
+ /^[+-]\s*public\s+/,
10366
+ /^[+-]\s*protected\s+/,
10367
+ /^[+-]\s*internal\s+/,
10368
+ /^[+-]\s*open\s+(class|func)\s+/,
10369
+ // Python / Ruby
10370
+ /^[+-]\s*def\s+/,
10371
+ /^[+-]\s*class\s+[A-Z]/,
10372
+ /^[+-]\s*module\s+[A-Z]/,
10373
+ // Go — exported identifiers start with uppercase
10374
+ /^[+-]\s*func\s+[A-Z]/,
10375
+ /^[+-]\s*func\s+\([^)]+\)\s+[A-Z]/,
10376
+ /^[+-]\s*type\s+[A-Z]/,
10377
+ // Rust
10378
+ /^[+-]\s*pub\s+(fn|struct|enum|trait|type|mod)\s+/,
10379
+ // PHP
10380
+ /^[+-]\s*function\s+/
10381
+ ];
10382
+ /**
10383
+ * Splits a cleaned diff into chunks that each fit within a byte budget.
10384
+ * File sections are ranked by semantic priority (same 5-tier system as
10385
+ * classifySection) so the first chunk always contains the highest-signal
10386
+ * sections. Each chunk contains only complete file sections — no partial
10387
+ * files.
10388
+ *
10389
+ * If a single file section exceeds `maxBytesPerChunk`, it is placed alone
10390
+ * in its own chunk so no information is lost.
10391
+ *
10392
+ * @param diff The cleaned diff string (output of cleanDiffForAI)
10393
+ * @param maxBytesPerChunk Maximum byte size for each chunk
10394
+ * @return Array of diff chunk strings (at least one element for non-empty diffs)
10395
+ */
10396
+ chunkDiff(diff, maxBytesPerChunk) {
10397
+ const lines = diff.split("\n");
10398
+ const fileSections = this.parseFileSections(lines);
10399
+ if (fileSections.length === 0) {
10400
+ return [diff];
10401
+ }
10402
+ const ranked = fileSections.map((section) => ({
10403
+ text: section.lines.join("\n"),
10404
+ priority: this.classifySection(section)
10405
+ })).sort((a, b2) => a.priority - b2.priority);
10406
+ const chunks = [];
10407
+ let currentChunkTexts = [];
10408
+ let currentChunkBytes = 0;
10409
+ for (const entry of ranked) {
10410
+ const sectionBytes = Buffer.byteLength(entry.text, "utf-8");
10411
+ const newlineBytes = currentChunkTexts.length > 0 ? 1 : 0;
10412
+ if (sectionBytes > maxBytesPerChunk) {
10413
+ if (currentChunkTexts.length > 0) {
10414
+ chunks.push(currentChunkTexts.join("\n"));
10415
+ currentChunkTexts = [];
10416
+ }
10417
+ currentChunkBytes = 0;
10418
+ chunks.push(entry.text);
10419
+ continue;
10420
+ }
10421
+ if (currentChunkBytes + sectionBytes + newlineBytes > maxBytesPerChunk && currentChunkTexts.length > 0) {
10422
+ chunks.push(currentChunkTexts.join("\n"));
10423
+ currentChunkTexts = [];
10424
+ currentChunkBytes = 0;
10425
+ }
10426
+ currentChunkTexts.push(entry.text);
10427
+ currentChunkBytes += sectionBytes + (currentChunkTexts.length > 1 ? 1 : 0);
10428
+ }
10429
+ if (currentChunkTexts.length > 0) {
10430
+ chunks.push(currentChunkTexts.join("\n"));
10431
+ }
10432
+ return chunks;
10433
+ }
10434
+ /**
10435
+ * Classifies a file section into a priority bucket for truncation ranking.
10436
+ * Lower number = higher priority (included first).
10437
+ *
10438
+ * 1 = deletion-only, 2 = mixed with signatures, 3 = mixed,
10439
+ * 4 = addition-only, 5 = context-only
10440
+ */
10441
+ static isDiffHeader(line) {
10442
+ return line.startsWith("--- a/") || line.startsWith("--- /dev/null") || line.startsWith("+++ b/") || line.startsWith("+++ /dev/null");
10443
+ }
10444
+ classifySection(section) {
10445
+ let hasAdditions = false;
10446
+ let hasDeletions = false;
10447
+ let hasSignature = false;
10448
+ for (const line of section.lines) {
10449
+ const isAddition = line.startsWith("+") && !_AutoVersioningService.isDiffHeader(line);
10450
+ const isDeletion = line.startsWith("-") && !_AutoVersioningService.isDiffHeader(line);
10451
+ if (isAddition) {
10452
+ hasAdditions = true;
10453
+ }
10454
+ if (isDeletion) {
10455
+ hasDeletions = true;
10456
+ }
10457
+ if (isAddition || isDeletion) {
10458
+ for (const pattern of _AutoVersioningService.SIGNATURE_PATTERNS) {
10459
+ if (pattern.test(line)) {
10460
+ hasSignature = true;
10461
+ break;
10462
+ }
10463
+ }
10464
+ }
10465
+ if (hasAdditions && hasDeletions && hasSignature) {
10466
+ break;
10467
+ }
10468
+ }
10469
+ if (hasDeletions && !hasAdditions) {
10470
+ return 1;
10471
+ }
10472
+ if (hasDeletions && hasAdditions && hasSignature) {
10473
+ return 2;
10474
+ }
10475
+ if (hasDeletions && hasAdditions) {
10476
+ return 3;
10477
+ }
10478
+ if (hasAdditions) {
10479
+ return 4;
10480
+ }
10481
+ return 5;
10482
+ }
10483
+ /**
10484
+ * Parses a diff into file sections, where each section starts with "diff --git".
10485
+ */
10486
+ parseFileSections(lines) {
10487
+ const sections = [];
10488
+ let currentSection = void 0;
10489
+ for (const line of lines) {
10490
+ if (line.startsWith("diff --git")) {
10491
+ if (currentSection != void 0) {
10492
+ sections.push(currentSection);
10493
+ }
10494
+ currentSection = { lines: [line] };
10495
+ } else if (currentSection != void 0) {
10496
+ currentSection.lines.push(line);
10497
+ }
10498
+ }
10499
+ if (currentSection != void 0) {
10500
+ sections.push(currentSection);
10501
+ }
10502
+ return sections;
10503
+ }
10504
+ /**
10505
+ * Cleans a single file section by removing version-related changes.
10506
+ * Returns null if the file section should be completely removed.
10507
+ */
10508
+ cleanFileSection(section, mappedMagicVersion) {
10509
+ const headerLines = [];
10510
+ const contentLines = [];
10511
+ let inContent = false;
10512
+ for (const line of section.lines) {
10513
+ if (line.startsWith("@@")) {
10514
+ inContent = true;
10515
+ headerLines.push(line);
10516
+ } else if (!inContent) {
10517
+ headerLines.push(line);
10518
+ } else {
10519
+ contentLines.push(line);
10520
+ }
10521
+ }
10522
+ let processedContent = this.removeVersionChangePairs(contentLines, mappedMagicVersion);
10523
+ processedContent = this.removeRemainingMagicVersionLines(processedContent, mappedMagicVersion);
10524
+ let hasChanges = false;
10525
+ for (const line of processedContent) {
10526
+ const isAddition = line.startsWith("+") && !_AutoVersioningService.isDiffHeader(line);
10527
+ const isDeletion = line.startsWith("-") && !_AutoVersioningService.isDiffHeader(line);
10528
+ if (isAddition || isDeletion) {
10529
+ hasChanges = true;
10530
+ break;
10531
+ }
10532
+ }
10533
+ if (!hasChanges) {
10534
+ return void 0;
10535
+ }
10536
+ const cleaned = { lines: [] };
10537
+ cleaned.lines.push(...headerLines);
10538
+ cleaned.lines.push(...processedContent);
10539
+ return cleaned;
10540
+ }
10541
+ /**
10542
+ * Removes any remaining lines (additions, deletions, or context) that contain the magic version.
10543
+ * This is a secondary pass to catch any lines that the pairing logic missed.
10544
+ */
10545
+ removeRemainingMagicVersionLines(lines, mappedMagicVersion) {
10546
+ const result = [];
10547
+ for (const line of lines) {
10548
+ if (line.includes(mappedMagicVersion)) {
10549
+ continue;
10550
+ }
10551
+ result.push(line);
10552
+ }
10553
+ return result;
10554
+ }
10555
+ /**
10556
+ * Removes paired - and + lines where the only difference is the magic version.
10557
+ */
10558
+ removeVersionChangePairs(lines, mappedMagicVersion) {
10559
+ const result = [];
10560
+ let lineIndex = 0;
10561
+ while (lineIndex < lines.length) {
10562
+ const currentLine = lines[lineIndex];
10563
+ if (!currentLine) {
10564
+ lineIndex++;
10565
+ continue;
10566
+ }
10567
+ if (this.isDeletionLine(currentLine)) {
10568
+ const pairIndex = this.findMatchingAdditionLine(lines, lineIndex, mappedMagicVersion);
10569
+ if (pairIndex !== -1) {
10570
+ result.push(...lines.slice(lineIndex + 1, pairIndex));
10571
+ lineIndex = pairIndex + 1;
10572
+ } else {
10573
+ result.push(currentLine);
10574
+ lineIndex++;
10575
+ }
10576
+ } else {
10577
+ result.push(currentLine);
10578
+ lineIndex++;
10579
+ }
10580
+ }
10581
+ return result;
10582
+ }
10583
+ isDeletionLine(line) {
10584
+ return line.startsWith("-") && !_AutoVersioningService.isDiffHeader(line);
10585
+ }
10586
+ findMatchingAdditionLine(lines, startIndex, mappedMagicVersion) {
10587
+ const startLine = lines[startIndex];
10588
+ if (!startLine) {
10589
+ return -1;
10590
+ }
10591
+ for (let nextIndex = startIndex + 1; nextIndex < lines.length; nextIndex++) {
10592
+ const nextLine = lines[nextIndex];
10593
+ if (!nextLine) {
10594
+ continue;
10595
+ }
10596
+ if (this.shouldStopSearching(nextLine)) {
10597
+ break;
10598
+ }
10599
+ if (this.isAdditionLine(nextLine)) {
10600
+ if (this.isVersionChangePair(startLine, nextLine, mappedMagicVersion)) {
10601
+ return nextIndex;
10602
+ }
10603
+ }
10604
+ }
10605
+ return -1;
10606
+ }
10607
+ isAdditionLine(line) {
10608
+ return line.startsWith("+") && !_AutoVersioningService.isDiffHeader(line);
10609
+ }
10610
+ shouldStopSearching(line) {
10611
+ return line.startsWith("@@") || line.startsWith("diff --git") || line.startsWith("index ") || _AutoVersioningService.isDiffHeader(line);
10612
+ }
10613
+ /**
10614
+ * Checks if two lines form a version change pair where the only difference is the magic version,
10615
+ * or where the only difference is a Go module path /vN suffix.
10616
+ */
10617
+ isVersionChangePair(minusLine, plusLine, mappedMagicVersion) {
10618
+ if (this.isGoModulePathSuffixChange(minusLine, plusLine)) {
10619
+ return true;
10620
+ }
10621
+ if (!plusLine.includes(mappedMagicVersion)) {
10622
+ return false;
10623
+ }
10624
+ const minusContent = minusLine.substring(1);
10625
+ const plusContent = plusLine.substring(1);
10626
+ const magicIndex = plusContent.indexOf(mappedMagicVersion);
10627
+ if (magicIndex === -1) {
10628
+ return false;
10629
+ }
10630
+ const plusPrefix = plusContent.substring(0, magicIndex);
10631
+ const plusSuffix = plusContent.substring(magicIndex + mappedMagicVersion.length);
10632
+ if (!minusContent.startsWith(plusPrefix)) {
10633
+ return false;
10634
+ }
10635
+ const versionStart = plusPrefix.length;
10636
+ const versionEnd = minusContent.length - plusSuffix.length;
10637
+ if (versionEnd <= versionStart || versionEnd > minusContent.length) {
10638
+ return false;
10639
+ }
10640
+ const minusVersion = minusContent.substring(versionStart, versionEnd);
10641
+ const minusSuffix = minusContent.substring(versionEnd);
10642
+ if (minusSuffix !== plusSuffix) {
10643
+ return false;
10644
+ }
10645
+ return /.*\d+.*/.test(minusVersion);
10646
+ }
10647
+ /**
10648
+ * Detects when two diff lines differ only by a Go module path /vN suffix.
10649
+ * For example:
10650
+ * - core "github.com/org/repo/core"
10651
+ * + core "github.com/org/repo/v2/core"
10652
+ * These are artifacts of the major version bump and should be filtered from AI analysis.
10653
+ *
10654
+ * Only matches lines that contain a quoted Go-style module path (e.g., "github.com/..."),
10655
+ * so non-Go diffs like API URL version changes are not affected.
10656
+ */
10657
+ isGoModulePathSuffixChange(minusLine, plusLine) {
10658
+ const minusContent = minusLine.substring(1);
10659
+ const plusContent = plusLine.substring(1);
10660
+ const goModulePathPattern = /"[^"]*(?:github\.com|golang\.org|google\.golang\.org|gopkg\.in)\/[^"]*"/;
10661
+ const goModuleDirectivePattern = /^\s*module\s+(?:github\.com|golang\.org|google\.golang\.org|gopkg\.in)\/\S+/;
10662
+ const hasGoModulePath = goModulePathPattern.test(minusContent) || goModulePathPattern.test(plusContent) || goModuleDirectivePattern.test(minusContent.trim()) || goModuleDirectivePattern.test(plusContent.trim());
10663
+ if (!hasGoModulePath) {
10664
+ return false;
10665
+ }
10666
+ const suffixPattern = /\/v\d+(?=\/|"\s*$|$)/g;
10667
+ const minusWithoutSuffix = minusContent.replace(suffixPattern, "");
10668
+ const plusWithoutSuffix = plusContent.replace(suffixPattern, "");
10669
+ if (minusWithoutSuffix !== plusWithoutSuffix) {
10670
+ return false;
10671
+ }
10672
+ return /\/v\d+(?=\/|"\s*$|$)/.test(minusContent) || /\/v\d+(?=\/|"\s*$|$)/.test(plusContent);
10673
+ }
10674
+ /**
10675
+ * Gets the latest semantic version from git tags in the given repository.
10676
+ * Used as a fallback when the magic version is not embedded in any generated file
10677
+ * (e.g., Swift SDKs which use git tags for versioning via SPM, not a version file).
10678
+ *
10679
+ * @param workingDirectory The git repository directory
10680
+ * @return The latest semver version from git tags, or undefined if no valid tags found
10681
+ */
10682
+ async getLatestVersionFromGitTags(workingDirectory) {
10683
+ try {
10684
+ const result = await (0, import_logging_execa.loggingExeca)(this.logger, "git", ["ls-remote", "--tags", "origin"], {
10685
+ cwd: workingDirectory,
10686
+ doNotPipeOutput: true
10687
+ });
10688
+ const output = result.stdout;
10689
+ if (!output || output.trim().length === 0) {
10690
+ this.logger.info("No git tags found in repository");
10691
+ return void 0;
10692
+ }
10693
+ const tags = [];
10694
+ for (const line of output.split("\n")) {
10695
+ const trimmed2 = line.trim();
10696
+ if (trimmed2.length === 0 || trimmed2.includes("^{}")) {
10697
+ continue;
10698
+ }
10699
+ const match = trimmed2.match(/refs\/tags\/(.+)$/);
10700
+ if (match?.[1]) {
10701
+ tags.push(match[1]);
10702
+ }
10703
+ }
10704
+ const validTags = tags.filter((tag) => import_semver.default.valid(tag) != null).sort((a, b2) => import_semver.default.rcompare(a, b2));
10705
+ if (validTags.length > 0) {
10706
+ const latest = validTags[0];
10707
+ this.logger.info(`Found latest version from git tags: ${latest}`);
10708
+ return latest;
10709
+ }
10710
+ this.logger.info("No valid semver tags found in repository");
10711
+ return void 0;
10712
+ } catch (e) {
10713
+ this.logger.warn(`Failed to read git tags: ${e}`);
10714
+ return void 0;
10715
+ }
10716
+ }
10717
+ /**
10718
+ * Replaces all occurrences of the magic version with the final version in generated files.
10719
+ *
10720
+ * @param workingDirectory The directory containing generated files
10721
+ * @param mappedMagicVersion The magic version after language transformations (e.g., "v0.0.0-fern-placeholder" for Go)
10722
+ * @param finalVersion The final version to use
10723
+ * @throws Error if file operations fail or find/sed command fails
10724
+ */
10725
+ async replaceMagicVersion(workingDirectory, mappedMagicVersion, finalVersion) {
10726
+ this.logger.debug(`Replacing placeholder version ${mappedMagicVersion} with final version: ${finalVersion}`);
10727
+ const sedCommand = `s/${this.escapeForSed(mappedMagicVersion)}/${this.escapeForSed(finalVersion)}/g`;
10728
+ let isGNUSed = false;
10729
+ try {
10730
+ await (0, import_logging_execa.loggingExeca)(this.logger, "sed", ["--version"], {
10731
+ doNotPipeOutput: true
10732
+ });
10733
+ isGNUSed = true;
10734
+ } catch {
10735
+ isGNUSed = false;
10736
+ }
10737
+ let command;
10738
+ if (isGNUSed) {
10739
+ command = `find "${workingDirectory}" -type f -not -path "*/.git/*" -exec sed -i '${sedCommand}' {} +`;
10740
+ } else {
10741
+ command = `find "${workingDirectory}" -type f -not -path "*/.git/*" -exec sed -i '' '${sedCommand}' {} +`;
10742
+ }
10743
+ await (0, import_logging_execa.loggingExeca)(this.logger, "bash", ["-c", command], {
10744
+ cwd: workingDirectory,
10745
+ doNotPipeOutput: true
10746
+ });
10747
+ this.logger.debug("Placeholder version replaced successfully");
10748
+ }
10749
+ /**
10750
+ * Adds /vN suffix to Go module paths and import statements when the computed
10751
+ * version is v2+. Go requires a /vN suffix on module paths for major versions >= 2.
10752
+ *
10753
+ * When using the safe magic version (major 0), the generated code has no /vN suffix.
10754
+ * After auto-versioning determines the real version, this method adds the correct suffix.
10755
+ *
10756
+ * @param workingDirectory The directory containing generated Go files
10757
+ * @param finalVersion The final computed version (e.g., "v2.0.0", "v3.1.0")
10758
+ */
10759
+ async addGoMajorVersionSuffix(workingDirectory, finalVersion) {
10760
+ const major = this.extractMajorVersion(finalVersion);
10761
+ if (major == null || major < 2) {
10762
+ this.logger.debug(
10763
+ `Version ${finalVersion} has major ${major ?? "unknown"}, no Go module path suffix needed`
10764
+ );
10765
+ return;
10766
+ }
10767
+ const goModPath = (0, import_path3.join)(workingDirectory, "go.mod");
10768
+ if (!(0, import_fs2.existsSync)(goModPath)) {
10769
+ this.logger.debug(`No go.mod found in ${workingDirectory}, skipping Go module path update`);
10770
+ return;
10771
+ }
10772
+ const modulePath = await this.readGoModulePath(goModPath);
10773
+ if (modulePath == null) {
10774
+ this.logger.warn(`Could not read module path from go.mod in ${workingDirectory}`);
10775
+ return;
10776
+ }
10777
+ const suffix = `/v${major}`;
10778
+ if (/\/v\d+$/.test(modulePath)) {
10779
+ this.logger.debug(`Module path ${modulePath} already has a version suffix, skipping`);
10780
+ return;
10781
+ }
10782
+ await this.addSuffixToGoMod(goModPath, modulePath, suffix);
10783
+ await this.addSuffixToGoFiles(workingDirectory, modulePath, suffix);
10784
+ this.logger.info(`Added ${suffix} to Go module path ${modulePath} and all import paths`);
10785
+ }
10786
+ /**
10787
+ * Extracts the major version number from a version string.
10788
+ * Handles versions with or without 'v' prefix.
10789
+ */
10790
+ extractMajorVersion(version) {
10791
+ const match = version.match(/^v?(\d+)\./);
10792
+ if (match?.[1] != null) {
10793
+ return parseInt(match[1], 10);
10794
+ }
10795
+ return void 0;
10796
+ }
10797
+ /**
10798
+ * Reads the module path from a go.mod file.
10799
+ */
10800
+ async readGoModulePath(goModPath) {
10801
+ const content = await (0, import_promises2.readFile)(goModPath, "utf-8");
10802
+ for (const line of content.split("\n")) {
10803
+ const trimmed2 = line.trim();
10804
+ if (trimmed2.startsWith("module ")) {
10805
+ return trimmed2.substring("module ".length).trim();
10806
+ }
10807
+ }
10808
+ return void 0;
10809
+ }
10810
+ /**
10811
+ * Adds a version suffix to the module line in go.mod.
10812
+ */
10813
+ async addSuffixToGoMod(goModPath, modulePath, suffix) {
10814
+ const content = await (0, import_promises2.readFile)(goModPath, "utf-8");
10815
+ const lines = content.split("\n");
10816
+ let changed = false;
10817
+ for (let i = 0; i < lines.length; i++) {
10818
+ const line = lines[i];
10819
+ if (line != null && line.trim().startsWith("module ") && line.includes(modulePath)) {
10820
+ lines[i] = line.replace(modulePath, modulePath + suffix);
10821
+ changed = true;
10822
+ break;
10823
+ }
10824
+ }
10825
+ if (changed) {
10826
+ await (0, import_promises2.writeFile)(goModPath, lines.join("\n"), "utf-8");
10827
+ this.logger.debug(`Updated go.mod: added ${suffix} to module path`);
10828
+ }
10829
+ }
10830
+ /**
10831
+ * Recursively walks a directory and returns all file paths matching a filter.
10832
+ */
10833
+ async walkDirectory(dir, filter) {
10834
+ const results = [];
10835
+ const entries2 = await (0, import_promises2.readdir)(dir);
10836
+ for (const entry of entries2) {
10837
+ const fullPath = (0, import_path3.join)(dir, entry);
10838
+ const entryStat = await (0, import_promises2.stat)(fullPath);
10839
+ if (entryStat.isDirectory()) {
10840
+ if (entry === ".git" || entry === "vendor") {
10841
+ continue;
10842
+ }
10843
+ const subResults = await this.walkDirectory(fullPath, filter);
10844
+ results.push(...subResults);
10845
+ } else if (filter(fullPath)) {
10846
+ results.push(fullPath);
10847
+ }
10848
+ }
10849
+ return results;
10850
+ }
10851
+ /**
10852
+ * Adds a version suffix to import paths in all .go files in the repository.
10853
+ */
10854
+ async addSuffixToGoFiles(repoRoot, modulePath, suffix) {
10855
+ const goFiles = await this.walkDirectory(repoRoot, (filePath) => (0, import_path3.extname)(filePath) === ".go");
10856
+ for (const filePath of goFiles) {
10857
+ await this.addSuffixToGoFile(filePath, modulePath, suffix);
10858
+ }
10859
+ }
10860
+ /**
10861
+ * Adds a version suffix to import paths in a single .go file.
10862
+ * Replaces occurrences of the module path with modulePath + suffix in import statements.
10863
+ */
10864
+ async addSuffixToGoFile(filePath, modulePath, suffix) {
10865
+ const content = await (0, import_promises2.readFile)(filePath, "utf-8");
10866
+ const lines = content.split("\n");
10867
+ let changed = false;
10868
+ let inImportBlock = false;
10869
+ for (let i = 0; i < lines.length; i++) {
10870
+ const line = lines[i];
10871
+ if (line == null) {
10872
+ continue;
10873
+ }
10874
+ const trimmed2 = line.trim();
10875
+ if (trimmed2.startsWith("import (")) {
10876
+ inImportBlock = true;
10877
+ } else if (inImportBlock && trimmed2 === ")") {
10878
+ inImportBlock = false;
10879
+ }
10880
+ const isImportLine = inImportBlock || trimmed2.startsWith('import "') || trimmed2.startsWith('import "') || /^import\s+\w+\s+"/.test(trimmed2);
10881
+ if (!isImportLine) {
10882
+ continue;
10883
+ }
10884
+ if (!line.includes(modulePath)) {
10885
+ continue;
10886
+ }
10887
+ const newLine = line.replace(`"${modulePath}/`, `"${modulePath}${suffix}/`).replace(`"${modulePath}"`, `"${modulePath}${suffix}"`);
10888
+ if (newLine !== line) {
10889
+ lines[i] = newLine;
10890
+ changed = true;
10891
+ }
10892
+ }
10893
+ if (changed) {
10894
+ await (0, import_promises2.writeFile)(filePath, lines.join("\n"), "utf-8");
10895
+ this.logger.debug(`Updated Go imports in ${filePath}`);
10896
+ }
10897
+ }
10898
+ /**
10899
+ * Escapes special characters for use in sed command.
10900
+ */
10901
+ escapeForSed(str) {
10902
+ return str.replace(/[/&]/g, "\\$&");
10903
+ }
10904
+ /**
10905
+ * Extracts the file path from a diff file section.
10906
+ * Parses the "diff --git a/path b/path" header line.
10907
+ */
10908
+ extractFilePathFromSection(section) {
10909
+ const firstLine = section.lines[0];
10910
+ if (firstLine == null) {
10911
+ return void 0;
10912
+ }
10913
+ const match = firstLine.match(/^diff --git "?a\/(.+?)"? "?b\/(.+?)"?$/);
10914
+ if (match?.[2] != null) {
10915
+ return match[2];
10916
+ }
10917
+ return void 0;
10918
+ }
10919
+ /**
10920
+ * Checks whether a file path matches any of the exclusion patterns.
10921
+ * Excluded files are noise for AI-based semantic version analysis.
10922
+ */
10923
+ shouldExcludeFile(filePath) {
10924
+ return EXCLUDED_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
10925
+ }
10926
+ /**
10927
+ * Extracts the previous version from a line containing the magic version.
10928
+ * Assumes the line format is like: "version = '0.0.0-fern-placeholder'" or "version: 0.0.0-fern-placeholder"
10929
+ *
10930
+ * @param lineWithMagicVersion A line from git diff containing the magic version
10931
+ * @return The inferred previous version if found, or undefined if the version cannot be parsed
10932
+ */
10933
+ extractPreviousVersionFromDiffLine(lineWithMagicVersion) {
10934
+ const prevVersionPattern = /[-].*?([v]?\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?)/;
10935
+ const matcher = lineWithMagicVersion.match(prevVersionPattern);
10936
+ if (matcher && matcher[1]) {
10937
+ const version = matcher[1];
10938
+ this.logger.info(`Extracted previous version from diff: ${version}`);
10939
+ return version;
10940
+ }
10941
+ this.logger.warn("Could not extract previous version from diff line: " + lineWithMagicVersion);
10942
+ return void 0;
10943
+ }
10944
+ };
10945
+
10946
+ // src/autoversion/VersionUtils.ts
10947
+ var MAGIC_VERSION = "0.0.0-fern-placeholder";
10948
+ var MAGIC_VERSION_PYTHON = "0.0.0.dev0";
10949
+ function mapMagicVersionForLanguage(magicVersion, language) {
10950
+ if (magicVersion !== MAGIC_VERSION) {
10951
+ return magicVersion;
10952
+ }
10953
+ if (language === "go") {
10954
+ return `v${magicVersion}`;
10955
+ }
10956
+ if (language === "python") {
10957
+ return MAGIC_VERSION_PYTHON;
10958
+ }
10959
+ return magicVersion;
10960
+ }
10961
+ var MAX_AI_DIFF_BYTES = 4e4;
10962
+ var MAX_CHUNKS = 40;
10963
+ var MAX_RAW_DIFF_BYTES = 1e7;
10964
+ var VERSION_BUMP_RANK = {
10965
+ ["MAJOR" /* MAJOR */]: 3,
10966
+ ["MINOR" /* MINOR */]: 2,
10967
+ ["PATCH" /* PATCH */]: 1,
10968
+ ["NO_CHANGE" /* NO_CHANGE */]: 0
10969
+ };
10970
+ function maxVersionBump(a, b2) {
10971
+ return (VERSION_BUMP_RANK[a] ?? 0) >= (VERSION_BUMP_RANK[b2] ?? 0) ? a : b2;
10972
+ }
10973
+ var SEMVER_PATTERN = /^(v)?(\d+)\.(\d+)\.(\d+)(?:-([\w.-]+))?(?:\+([\w.-]+))?$/;
10974
+ function isValidSemver(version) {
10975
+ return SEMVER_PATTERN.test(version);
10976
+ }
10977
+ function incrementVersion(currentVersion, versionBump) {
10978
+ const matcher = currentVersion.match(SEMVER_PATTERN);
10979
+ if (!matcher) {
10980
+ throw new AutoVersioningException("Invalid semantic version format: " + currentVersion);
10981
+ }
10982
+ const prefix = matcher[1] || "";
10983
+ let major = parseInt(matcher[2] ?? "0", 10);
10984
+ let minor = parseInt(matcher[3] ?? "0", 10);
10985
+ let patch = parseInt(matcher[4] ?? "0", 10);
10986
+ const preRelease = matcher[5];
10987
+ const buildMetadata = matcher[6];
10988
+ let preserveMetadata = false;
10989
+ if (versionBump === "MAJOR" /* MAJOR */) {
10990
+ major++;
10991
+ minor = 0;
10992
+ patch = 0;
10993
+ } else if (versionBump === "MINOR" /* MINOR */) {
10994
+ minor++;
10995
+ patch = 0;
10996
+ } else if (versionBump === "PATCH" /* PATCH */) {
10997
+ patch++;
10998
+ } else if (versionBump === "NO_CHANGE" /* NO_CHANGE */) {
10999
+ preserveMetadata = true;
11000
+ } else {
11001
+ throw new AutoVersioningException("Unknown version bump type: " + versionBump);
11002
+ }
11003
+ let newVersion = `${prefix}${major}.${minor}.${patch}`;
11004
+ if (preserveMetadata) {
11005
+ if (preRelease) {
11006
+ newVersion += `-${preRelease}`;
11007
+ }
11008
+ if (buildMetadata) {
11009
+ newVersion += `+${buildMetadata}`;
11010
+ }
11011
+ }
11012
+ return newVersion;
11013
+ }
11014
+
11015
+ // src/pipeline/steps/BaseStep.ts
11016
+ var BaseStep = class {
11017
+ constructor(outputDir, logger) {
11018
+ this.outputDir = outputDir;
11019
+ this.logger = logger;
11020
+ }
11021
+ outputDir;
11022
+ logger;
11023
+ };
11024
+
11025
+ // src/pipeline/steps/AutoVersionStep.ts
11026
+ var COMMIT_MARKER = "[fern-autoversion]";
11027
+ var FERN_TRAILER = "\n\n\u{1F33F} Generated with Fern";
11028
+ var AutoVersionStep = class extends BaseStep {
11029
+ constructor(outputDir, logger, config) {
11030
+ super(outputDir, logger);
11031
+ this.config = config;
11032
+ }
11033
+ config;
11034
+ name = "autoVersion";
11035
+ async execute(context) {
11036
+ const prepared = context.previousStepResults.generationCommit?.preparedReplay;
11037
+ if (!prepared) {
11038
+ this.logger.info(
11039
+ "AutoVersionStep: no replay preparation available (replay uninitialized or prepare failed); skipping."
11040
+ );
11041
+ return { executed: true, success: true };
11042
+ }
11043
+ if (prepared.flow === "skip-application") {
11044
+ this.logger.info("AutoVersionStep: replay flow is skip-application; skipping autoversion.");
11045
+ return { executed: true, success: true };
11046
+ }
11047
+ const language = this.config.language;
11048
+ const mappedMagicVersion = mapMagicVersionForLanguage(MAGIC_VERSION, language);
11049
+ const service = new AutoVersioningService({ logger: this.toTaskLogger() });
11050
+ if (prepared.previousGenerationSha == null) {
11051
+ return await this.handleFirstGeneration({ service, language, mappedMagicVersion });
11052
+ }
11053
+ return await this.handleNormalFlow({
11054
+ prepared,
11055
+ service,
11056
+ language,
11057
+ mappedMagicVersion,
11058
+ previousGenerationSha: prepared.previousGenerationSha
11059
+ });
11060
+ }
11061
+ async handleFirstGeneration(params) {
11062
+ const { service, language, mappedMagicVersion } = params;
11063
+ const initialVersion = this.config.baseVersion ?? (mappedMagicVersion.startsWith("v") ? "v0.0.1" : "0.0.1");
11064
+ if (!isValidSemver(initialVersion)) {
11065
+ const errorMessage = `AutoVersionStep: baseVersion ${JSON.stringify(initialVersion)} is not a valid semver string (expected e.g. "1.2.3" or "v1.2.3"). Refusing to run to avoid shell injection into the placeholder-rewrite step.`;
11066
+ this.logger.error(errorMessage);
11067
+ return {
11068
+ executed: true,
11069
+ success: false,
11070
+ errorMessage
11071
+ };
11072
+ }
11073
+ this.logger.info(`AutoVersionStep: first generation \u2014 using initial version ${initialVersion}`);
11074
+ await service.replaceMagicVersion(this.outputDir, mappedMagicVersion, initialVersion);
11075
+ if (language === "go") {
11076
+ await service.addGoMajorVersionSuffix(this.outputDir, initialVersion);
11077
+ }
11078
+ const commitMessage = this.brandMessage("Initial SDK generation");
11079
+ const commitSha = this.commitAutoversion(commitMessage);
11080
+ return {
11081
+ executed: true,
11082
+ success: true,
11083
+ version: initialVersion,
11084
+ commitMessage,
11085
+ commitSha
11086
+ };
11087
+ }
11088
+ async handleNormalFlow(params) {
11089
+ const { prepared, service, language, mappedMagicVersion, previousGenerationSha } = params;
11090
+ const rawDiff = this.gitDiff(previousGenerationSha, prepared.currentGenerationSha);
11091
+ if (rawDiff.trim().length === 0) {
11092
+ this.logger.info("AutoVersionStep: empty diff between generations; skipping autoversion.");
11093
+ return { executed: true, success: true };
11094
+ }
11095
+ const previousVersion = await this.resolvePreviousVersion({ service, rawDiff, mappedMagicVersion });
11096
+ if (previousVersion == null) {
11097
+ return await this.handleFirstGeneration({ service, language, mappedMagicVersion });
11098
+ }
11099
+ const cleanedDiff = service.cleanDiffForAI(rawDiff, mappedMagicVersion);
11100
+ const rawBytes = Buffer.byteLength(rawDiff, "utf-8");
11101
+ const cleanedBytes = Buffer.byteLength(cleanedDiff, "utf-8");
11102
+ this.logger.debug(
11103
+ `AutoVersionStep: raw=${formatSizeKB(rawBytes)}KB (${countFilesInDiff(rawDiff)} files), cleaned=${formatSizeKB(cleanedBytes)}KB (${countFilesInDiff(cleanedDiff)} files)`
11104
+ );
11105
+ if (cleanedDiff.trim().length === 0) {
11106
+ this.logger.info("AutoVersionStep: cleaned diff is empty; no semantic changes.");
11107
+ return { executed: true, success: true };
11108
+ }
11109
+ if (cleanedBytes > MAX_RAW_DIFF_BYTES) {
11110
+ this.logger.warn(
11111
+ `AutoVersionStep: diff too large for FAI (${formatSizeKB(cleanedBytes)}KB, limit ${formatSizeKB(MAX_RAW_DIFF_BYTES)}KB). Falling back to PATCH.`
11112
+ );
11113
+ return await this.finalizeWithBump({
11114
+ service,
11115
+ language,
11116
+ mappedMagicVersion,
11117
+ previousVersion,
11118
+ analysis: { versionBump: "PATCH", message: this.brandMessage("SDK regeneration") }
11119
+ });
11120
+ }
11121
+ const chunks = service.chunkDiff(cleanedDiff, MAX_AI_DIFF_BYTES);
11122
+ const cappedChunks = chunks.slice(0, MAX_CHUNKS);
11123
+ const skippedChunks = chunks.length - cappedChunks.length;
11124
+ if (chunks.length > 1) {
11125
+ this.logger.info(
11126
+ `AutoVersionStep: split diff into ${chunks.length} chunks` + (skippedChunks > 0 ? ` (capped at ${MAX_CHUNKS}, skipping ${skippedChunks})` : "")
11127
+ );
11128
+ }
11129
+ let analysis;
11130
+ try {
11131
+ analysis = cappedChunks.length <= 1 ? await this.analyzeSingle(cleanedDiff, language, previousVersion) : await this.analyzeChunks(cappedChunks, language, previousVersion);
11132
+ } catch (error) {
11133
+ this.logger.warn(`AutoVersionStep: FAI analysis failed (${String(error)}); falling back to PATCH bump.`);
11134
+ analysis = { versionBump: "PATCH", message: this.brandMessage("SDK regeneration") };
11135
+ }
11136
+ if (analysis == null) {
11137
+ this.logger.info("AutoVersionStep: FAI returned NO_CHANGE; skipping autoversion commit.");
11138
+ return {
11139
+ executed: true,
11140
+ success: true,
11141
+ previousVersion,
11142
+ versionBump: "NO_CHANGE"
11143
+ };
11144
+ }
11145
+ return await this.finalizeWithBump({
11146
+ service,
11147
+ language,
11148
+ mappedMagicVersion,
11149
+ previousVersion,
11150
+ analysis
11151
+ });
11152
+ }
11153
+ async finalizeWithBump(params) {
11154
+ const { service, language, mappedMagicVersion, previousVersion, analysis } = params;
11155
+ const newVersion = incrementVersion(previousVersion, analysis.versionBump);
11156
+ this.logger.info(`AutoVersionStep: ${analysis.versionBump} bump: ${previousVersion} \u2192 ${newVersion}`);
11157
+ await service.replaceMagicVersion(this.outputDir, mappedMagicVersion, newVersion);
11158
+ if (language === "go") {
11159
+ await service.addGoMajorVersionSuffix(this.outputDir, newVersion);
11160
+ }
11161
+ if (analysis.changelogEntry && analysis.changelogEntry.trim().length > 0) {
11162
+ await this.prependChangelogEntry({ version: newVersion, entry: analysis.changelogEntry });
11163
+ }
11164
+ const commitSha = this.commitAutoversion(analysis.message);
11165
+ return {
11166
+ executed: true,
11167
+ success: true,
11168
+ version: newVersion,
11169
+ commitMessage: analysis.message,
11170
+ changelogEntry: analysis.changelogEntry,
11171
+ previousVersion,
11172
+ versionBump: analysis.versionBump,
11173
+ prDescription: analysis.prDescription,
11174
+ versionBumpReason: analysis.versionBumpReason,
11175
+ commitSha
11176
+ };
11177
+ }
11178
+ async resolvePreviousVersion(params) {
11179
+ const { service, rawDiff, mappedMagicVersion } = params;
11180
+ try {
11181
+ const extracted = service.extractPreviousVersion(rawDiff, mappedMagicVersion);
11182
+ if (extracted != null) {
11183
+ this.logger.debug(`AutoVersionStep: previous version from diff: ${extracted}`);
11184
+ return extracted;
11185
+ }
11186
+ } catch (error) {
11187
+ if (!(error instanceof AutoVersioningException) || !error.magicVersionAbsent) {
11188
+ throw error;
11189
+ }
11190
+ this.logger.info("AutoVersionStep: magic version not found in diff; falling back to metadata + git tags.");
11191
+ }
11192
+ const metadataVersion = await this.readVersionFromMetadata();
11193
+ if (metadataVersion != null) {
11194
+ return this.normalizeVersionPrefix(metadataVersion, mappedMagicVersion);
11195
+ }
11196
+ try {
11197
+ const tagVersion = await service.getLatestVersionFromGitTags(this.outputDir);
11198
+ if (tagVersion != null) {
11199
+ return this.normalizeVersionPrefix(tagVersion, mappedMagicVersion);
11200
+ }
11201
+ } catch (error) {
11202
+ this.logger.debug(`AutoVersionStep: git-tags fallback failed (${String(error)}); ignoring.`);
11203
+ }
11204
+ return null;
11205
+ }
11206
+ async readVersionFromMetadata() {
11207
+ try {
11208
+ const output = (0, import_child_process3.execFileSync)("git", ["show", "HEAD~1:.fern/metadata.json"], {
11209
+ cwd: this.outputDir,
11210
+ encoding: "utf-8",
11211
+ stdio: "pipe"
11212
+ });
11213
+ const parsed = JSON.parse(output);
11214
+ return parsed.sdkVersion ?? void 0;
11215
+ } catch {
11216
+ return void 0;
11217
+ }
11218
+ }
11219
+ normalizeVersionPrefix(version, mappedMagicVersion) {
11220
+ const stripped = version.startsWith("v") ? version.slice(1) : version;
11221
+ return mappedMagicVersion.startsWith("v") ? `v${stripped}` : stripped;
11222
+ }
11223
+ gitDiff(from, to) {
11224
+ return (0, import_child_process3.execFileSync)("git", ["diff", from, to, "--", ".", ":(exclude).fern/metadata.json"], {
11225
+ cwd: this.outputDir,
11226
+ encoding: "utf-8",
11227
+ stdio: "pipe",
11228
+ maxBuffer: 256 * 1024 * 1024
11229
+ });
11230
+ }
11231
+ commitAutoversion(message) {
11232
+ const subject = message.split("\n")[0] ?? message;
11233
+ const body = message.slice(subject.length).trimStart();
11234
+ const fullMessage = body.length > 0 ? `${COMMIT_MARKER} ${subject}
11235
+
11236
+ ${body}` : `${COMMIT_MARKER} ${subject}`;
11237
+ (0, import_child_process3.execFileSync)("git", ["add", "-A"], { cwd: this.outputDir, stdio: "pipe" });
11238
+ (0, import_child_process3.execFileSync)("git", ["commit", "--allow-empty", "-m", fullMessage], {
11239
+ cwd: this.outputDir,
11240
+ stdio: "pipe"
11241
+ });
11242
+ return (0, import_child_process3.execFileSync)("git", ["rev-parse", "HEAD"], {
11243
+ cwd: this.outputDir,
11244
+ encoding: "utf-8",
11245
+ stdio: "pipe"
11246
+ }).trim();
11247
+ }
11248
+ async prependChangelogEntry(params) {
11249
+ const { version, entry } = params;
11250
+ const changelogPath = (0, import_path4.join)(this.outputDir, "changelog.md");
11251
+ const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
11252
+ const header = `## [${version}] - ${now}
11253
+ `;
11254
+ const newBlock = `${header}${entry.trim()}
11255
+
11256
+ `;
11257
+ let existing = "";
11258
+ if ((0, import_fs3.existsSync)(changelogPath)) {
11259
+ existing = await (0, import_promises3.readFile)(changelogPath, "utf-8");
11260
+ }
11261
+ let output;
11262
+ if (existing.trim().length === 0) {
11263
+ output = `# Changelog
11264
+
11265
+ ${newBlock}`;
11266
+ } else if (existing.startsWith("# Changelog")) {
11267
+ const newlineIdx = existing.indexOf("\n");
11268
+ const headerLine = newlineIdx >= 0 ? existing.slice(0, newlineIdx) : existing;
11269
+ const remainder = (newlineIdx >= 0 ? existing.slice(newlineIdx + 1) : "").replace(/^\s*\n/, "");
11270
+ output = `${headerLine}
11271
+
11272
+ ${newBlock}${remainder}`;
11273
+ } else {
11274
+ output = `${newBlock}${existing}`;
11275
+ }
11276
+ await (0, import_promises3.writeFile)(changelogPath, output, "utf-8");
11277
+ }
11278
+ brandMessage(message) {
11279
+ if (this.config.isWhitelabel) {
11280
+ return message;
11281
+ }
11282
+ if (message.includes("\u{1F33F} Generated with Fern")) {
11283
+ return message;
11284
+ }
11285
+ return `${message.trimEnd()}${FERN_TRAILER}`;
11286
+ }
11287
+ async analyzeSingle(cleanedDiff, language, previousVersion) {
11288
+ const { client, VersionBump: VersionBump2 } = await this.loadBaml();
11289
+ const result = await client.AnalyzeSdkDiff(
11290
+ cleanedDiff,
11291
+ language,
11292
+ previousVersion,
11293
+ this.config.priorChangelog ?? "",
11294
+ this.config.specCommitMessage ?? ""
11295
+ );
11296
+ if (result.version_bump === VersionBump2.NO_CHANGE) {
11297
+ return null;
11298
+ }
11299
+ return {
11300
+ versionBump: result.version_bump,
11301
+ message: result.message,
11302
+ changelogEntry: result.changelog_entry || void 0,
11303
+ versionBumpReason: result.version_bump_reason || void 0
11304
+ };
11305
+ }
11306
+ async analyzeChunks(chunks, language, previousVersion) {
11307
+ const { client, VersionBump: VersionBump2 } = await this.loadBaml();
11308
+ let bestBump = VersionBump2.NO_CHANGE;
11309
+ let bestMessage = "";
11310
+ let bestVersionBumpReason;
11311
+ const changelogEntries = [];
11312
+ for (let i = 0; i < chunks.length; i++) {
11313
+ const chunk = chunks[i];
11314
+ if (!chunk) {
11315
+ continue;
11316
+ }
11317
+ const analysis = await client.AnalyzeSdkDiff(
11318
+ chunk,
11319
+ language,
11320
+ previousVersion,
11321
+ this.config.priorChangelog ?? "",
11322
+ this.config.specCommitMessage ?? ""
11323
+ );
11324
+ if (analysis.version_bump === VersionBump2.NO_CHANGE) {
11325
+ continue;
11326
+ }
11327
+ const prev = bestBump;
11328
+ bestBump = maxVersionBump(bestBump, analysis.version_bump);
11329
+ if (bestBump !== prev) {
11330
+ bestMessage = analysis.message;
11331
+ bestVersionBumpReason = analysis.version_bump_reason;
11332
+ }
11333
+ const entry = analysis.changelog_entry?.trim();
11334
+ if (entry) {
11335
+ changelogEntries.push(entry);
11336
+ }
11337
+ }
11338
+ if (bestBump === VersionBump2.NO_CHANGE) {
11339
+ return null;
11340
+ }
11341
+ if (changelogEntries.length <= 1) {
11342
+ return {
11343
+ versionBump: bestBump,
11344
+ message: bestMessage,
11345
+ changelogEntry: changelogEntries[0],
11346
+ versionBumpReason: bestVersionBumpReason
11347
+ };
11348
+ }
11349
+ try {
11350
+ const consolidated = await client.ConsolidateChangelog(
11351
+ changelogEntries.join("\n\n"),
11352
+ bestBump,
11353
+ language,
11354
+ previousVersion,
11355
+ incrementVersion(previousVersion, bestBump)
11356
+ );
11357
+ return {
11358
+ versionBump: bestBump,
11359
+ message: bestMessage,
11360
+ changelogEntry: consolidated.consolidated_changelog,
11361
+ prDescription: consolidated.pr_description || void 0,
11362
+ versionBumpReason: consolidated.version_bump_reason || bestVersionBumpReason
11363
+ };
11364
+ } catch (error) {
11365
+ this.logger.warn(`AutoVersionStep: ConsolidateChangelog failed (${String(error)}); using joined entries.`);
11366
+ return {
11367
+ versionBump: bestBump,
11368
+ message: bestMessage,
11369
+ changelogEntry: changelogEntries.join("\n\n"),
11370
+ versionBumpReason: bestVersionBumpReason
11371
+ };
11372
+ }
11373
+ }
11374
+ /**
11375
+ * Dynamically imports @fern-api/cli-ai only when autoversion actually needs to
11376
+ * call FAI. The package is ESM-only with extensionless internal imports, so
11377
+ * unbundled CJS consumers (like generator-cli's `bin/cli` used by the README /
11378
+ * reference commands) would fail at load time with a static import — but those
11379
+ * commands never reach this method, so dynamic import keeps them unaffected.
11380
+ */
11381
+ async loadBaml() {
11382
+ if (this.config.ai == null) {
11383
+ throw new Error("AutoVersionStep: ai config is missing. Set autoVersion.ai to a BAML provider+model pair.");
11384
+ }
11385
+ const cliAi = await import("@fern-api/cli-ai");
11386
+ const registry = cliAi.configureBamlClient(this.config.ai);
11387
+ return {
11388
+ client: cliAi.b.withOptions({ clientRegistry: registry }),
11389
+ VersionBump: cliAi.VersionBump
11390
+ };
11391
+ }
11392
+ /**
11393
+ * AutoVersioningService was built against the CLI's TaskContext logger — a richer
11394
+ * shape with `trace`, `log`, `enable`, `disable`. Our pipeline only has the four
11395
+ * core levels; stub the rest as no-ops and adapt the method signatures. Cast to
11396
+ * the service's expected Logger type because the surface is structurally wider
11397
+ * than what PipelineLogger exposes.
11398
+ */
11399
+ toTaskLogger() {
11400
+ const noop = () => void 0;
11401
+ const adapter = {
11402
+ debug: (msg) => this.logger.debug(msg),
11403
+ info: (msg) => this.logger.info(msg),
11404
+ warn: (msg) => this.logger.warn(msg),
11405
+ error: (msg) => this.logger.error(msg),
11406
+ trace: (msg) => this.logger.debug(msg),
11407
+ log: (msg) => this.logger.info(msg),
11408
+ enable: noop,
11409
+ disable: noop
11410
+ };
11411
+ return adapter;
11412
+ }
11413
+ };
11414
+
11415
+ // src/replay/replay-run.ts
11416
+ var import_replay = require("@fern-api/replay");
11417
+ var import_child_process4 = require("child_process");
11418
+ var import_fs5 = require("fs");
11419
+ var import_path6 = require("path");
11420
+
11421
+ // src/replay/fernignore.ts
11422
+ var import_fs4 = require("fs");
11423
+ var import_promises4 = require("fs/promises");
11424
+ var import_path5 = require("path");
11425
+ var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
11426
+ var GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
11427
+ async function ensureReplayFernignoreEntries(outputDir) {
11428
+ const fernignorePath = (0, import_path5.join)(outputDir, ".fernignore");
11429
+ let content = "";
11430
+ try {
11431
+ content = await (0, import_promises4.readFile)(fernignorePath, "utf-8");
11432
+ } catch {
11433
+ }
11434
+ const lines = content.split("\n");
11435
+ const toAdd = REPLAY_FERNIGNORE_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
11436
+ if (toAdd.length > 0) {
11437
+ const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
11438
+ await (0, import_promises4.writeFile)(fernignorePath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
11439
+ return true;
11440
+ }
11441
+ return false;
11442
+ }
11443
+ async function ensureGitattributesEntries(outputDir) {
11444
+ const gitattributesPath = (0, import_path5.join)(outputDir, ".gitattributes");
11445
+ let content = "";
11446
+ try {
11447
+ content = await (0, import_promises4.readFile)(gitattributesPath, "utf-8");
11448
+ } catch {
11449
+ }
11450
+ const lines = content.split("\n");
11451
+ const toAdd = GITATTRIBUTES_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
11452
+ if (toAdd.length > 0) {
11453
+ const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
11454
+ await (0, import_promises4.writeFile)(gitattributesPath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
11455
+ }
11456
+ }
11457
+ function ensureReplayFernignoreEntriesSync(outputDir) {
11458
+ const fernignorePath = (0, import_path5.join)(outputDir, ".fernignore");
11459
+ let content = "";
11460
+ if ((0, import_fs4.existsSync)(fernignorePath)) {
11461
+ content = (0, import_fs4.readFileSync)(fernignorePath, "utf-8");
11462
+ }
11463
+ const lines = content.split("\n");
11464
+ const toAdd = REPLAY_FERNIGNORE_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
11465
+ if (toAdd.length > 0) {
11466
+ const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
11467
+ (0, import_fs4.writeFileSync)(fernignorePath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
11468
+ }
11469
+ }
11470
+ function ensureGitattributesEntriesSync(outputDir) {
11471
+ const gitattributesPath = (0, import_path5.join)(outputDir, ".gitattributes");
11472
+ let content = "";
11473
+ if ((0, import_fs4.existsSync)(gitattributesPath)) {
11474
+ content = (0, import_fs4.readFileSync)(gitattributesPath, "utf-8");
11475
+ }
11476
+ const lines = content.split("\n");
11477
+ const toAdd = GITATTRIBUTES_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
11478
+ if (toAdd.length > 0) {
11479
+ const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
11480
+ (0, import_fs4.writeFileSync)(gitattributesPath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
11481
+ }
11482
+ }
11483
+
11484
+ // src/replay/replay-run.ts
11485
+ async function replayPrepare(params) {
11486
+ const {
11487
+ outputDir,
11488
+ cliVersion,
11489
+ generatorVersions,
11490
+ stageOnly = false,
11491
+ generatorName,
11492
+ skipApplication,
11493
+ logger
11494
+ } = params;
11495
+ const lockfilePath = (0, import_path6.join)(outputDir, ".fern", "replay.lock");
11496
+ if (!(0, import_fs5.existsSync)(lockfilePath)) {
11497
+ return null;
11498
+ }
11499
+ const baseBranchHead = gitRevParse(outputDir, "HEAD");
11500
+ let previousGenerationSha = null;
11501
+ let prevBaseBranchHead = null;
11502
+ try {
11503
+ const lockManager = new import_replay.LockfileManager(outputDir);
11504
+ if (lockManager.exists()) {
11505
+ const lock = lockManager.read();
11506
+ previousGenerationSha = lock.current_generation;
11507
+ const latestGen = lock.generations.find((g2) => g2.commit_sha === lock.current_generation);
11508
+ prevBaseBranchHead = latestGen?.base_branch_head ?? null;
11509
+ }
11510
+ } catch {
11511
+ }
11512
+ if (previousGenerationSha != null) {
11513
+ const sanitizedName = generatorName?.replace(/\//g, "--");
11514
+ const namespacedTag = sanitizedName != null ? `fern-generation-base--${sanitizedName}` : null;
11515
+ const tagSha = (namespacedTag != null ? gitRevParse(outputDir, namespacedTag) : null) ?? gitRevParse(outputDir, "fern-generation-base");
11516
+ let shouldSync = false;
11517
+ if (tagSha != null) {
11518
+ const tagParent = gitRevParse(outputDir, `${tagSha}^`);
11519
+ if (tagParent === prevBaseBranchHead || tagParent === previousGenerationSha) {
11520
+ const tagDistance = gitDiffNameOnly(outputDir, tagSha, "HEAD").length;
11521
+ const lockDistance = gitDiffNameOnly(
11522
+ outputDir,
11523
+ prevBaseBranchHead ?? previousGenerationSha,
11524
+ "HEAD"
11525
+ ).length;
11526
+ shouldSync = tagDistance < lockDistance;
11527
+ }
11528
+ }
11529
+ if (shouldSync && tagSha != null) {
11530
+ if (!isTagMergedIntoHead(outputDir, tagSha)) {
11531
+ logger?.warn(
11532
+ `fern-generation-base tag ${tagSha} is not reachable from HEAD \u2014 skipping divergent-merge sync. The tag likely points at an unmerged generation (PR closed without merge).`
11533
+ );
11534
+ } else {
11535
+ const syncService = new import_replay.ReplayService(outputDir, { enabled: true });
11536
+ await syncService.syncFromDivergentMerge(tagSha, {
11537
+ cliVersion,
11538
+ generatorVersions,
11539
+ baseBranchHead: baseBranchHead ?? void 0
11540
+ });
11541
+ try {
11542
+ const freshLockManager = new import_replay.LockfileManager(outputDir);
11543
+ if (freshLockManager.exists()) {
11544
+ previousGenerationSha = freshLockManager.read().current_generation;
11545
+ }
11546
+ } catch {
11547
+ }
11548
+ }
11549
+ }
11550
+ }
11551
+ const service = new import_replay.ReplayService(outputDir, { enabled: true });
11552
+ let preparation;
11553
+ try {
11554
+ preparation = await service.prepareReplay({
11555
+ cliVersion,
11556
+ generatorVersions,
11557
+ stageOnly,
11558
+ skipApplication,
11559
+ baseBranchHead: baseBranchHead ?? void 0
11560
+ });
11561
+ } catch (error) {
11562
+ logger?.warn("Replay failed, continuing without replay: " + String(error));
11563
+ return null;
11564
+ }
11565
+ return {
11566
+ _service: service,
11567
+ _preparation: preparation,
11568
+ outputDir,
11569
+ flow: preparation.flow,
11570
+ previousGenerationSha,
11571
+ currentGenerationSha: preparation.currentGenerationSha,
11572
+ baseBranchHead
11573
+ };
11574
+ }
11575
+ async function replayApply(prepared, params = {}) {
11576
+ const { stageOnly = false, logger } = params;
11577
+ let report;
11578
+ try {
11579
+ report = await prepared._service.applyPreparedReplay(prepared._preparation, { stageOnly });
11580
+ } catch (error) {
11581
+ logger?.warn("Replay failed, continuing without replay: " + String(error));
11582
+ return {
11583
+ report: null,
11584
+ fernignoreUpdated: false,
11585
+ previousGenerationSha: prepared.previousGenerationSha,
11586
+ currentGenerationSha: prepared.currentGenerationSha,
11587
+ baseBranchHead: prepared.baseBranchHead
11588
+ };
11589
+ }
11590
+ let resolvedBaseBranchHead = prepared.baseBranchHead;
11591
+ try {
11592
+ const freshLockManager = new import_replay.LockfileManager(prepared.outputDir);
11593
+ if (freshLockManager.exists()) {
11594
+ const freshLock = freshLockManager.read();
11595
+ const latestGen = freshLock.generations.find((g2) => g2.commit_sha === freshLock.current_generation);
11596
+ if (latestGen?.base_branch_head) {
11597
+ resolvedBaseBranchHead = latestGen.base_branch_head;
11598
+ }
11599
+ }
11600
+ } catch {
11601
+ }
11602
+ const fernignoreUpdated = await ensureReplayFernignoreEntries(prepared.outputDir);
11603
+ await ensureGitattributesEntries(prepared.outputDir);
11604
+ return {
11605
+ report,
11606
+ fernignoreUpdated,
11607
+ previousGenerationSha: prepared.previousGenerationSha,
11608
+ currentGenerationSha: prepared.currentGenerationSha,
11609
+ baseBranchHead: resolvedBaseBranchHead
11610
+ };
11611
+ }
11612
+ async function replayRun(params) {
11613
+ const prepared = await replayPrepare(params);
11614
+ if (prepared == null) {
11615
+ return {
11616
+ report: null,
11617
+ fernignoreUpdated: false,
11618
+ previousGenerationSha: null,
11619
+ currentGenerationSha: null,
11620
+ baseBranchHead: null
11621
+ };
11622
+ }
11623
+ return replayApply(prepared, { stageOnly: params.stageOnly, logger: params.logger });
11624
+ }
11625
+ function isTagMergedIntoHead(cwd2, tagSha) {
11626
+ try {
11627
+ (0, import_child_process4.execFileSync)("git", ["merge-base", "--is-ancestor", tagSha, "HEAD"], {
11628
+ cwd: cwd2,
11629
+ stdio: "pipe"
11630
+ });
11631
+ return true;
11632
+ } catch {
11633
+ }
11634
+ const tagTree = gitRevParse(cwd2, `${tagSha}^{tree}`);
11635
+ if (tagTree == null) {
11636
+ return false;
11637
+ }
11638
+ try {
11639
+ const trees = (0, import_child_process4.execFileSync)("git", ["log", "HEAD", "--format=%T", "--max-count=500"], {
11640
+ cwd: cwd2,
11641
+ encoding: "utf-8",
11642
+ stdio: "pipe"
11643
+ });
11644
+ return trees.split("\n").some((t2) => t2.trim() === tagTree);
11645
+ } catch {
11646
+ return false;
11647
+ }
11648
+ }
11649
+ function gitRevParse(cwd2, rev) {
11650
+ try {
11651
+ return (0, import_child_process4.execFileSync)("git", ["rev-parse", "--verify", rev], {
11652
+ cwd: cwd2,
11653
+ encoding: "utf-8",
11654
+ stdio: "pipe"
11655
+ }).trim();
11656
+ } catch {
11657
+ return null;
11658
+ }
11659
+ }
11660
+ function gitDiffNameOnly(cwd2, from, to) {
11661
+ try {
11662
+ const output = (0, import_child_process4.execFileSync)("git", ["diff", "--name-only", from, to], {
11663
+ cwd: cwd2,
11664
+ encoding: "utf-8",
11665
+ stdio: "pipe"
11666
+ });
11667
+ return output.trim().split("\n").filter(Boolean);
11668
+ } catch {
11669
+ return [];
11670
+ }
11671
+ }
11672
+
11673
+ // src/pipeline/steps/GenerationCommitStep.ts
11674
+ var GenerationCommitStep = class extends BaseStep {
11675
+ constructor(outputDir, logger, config, cliVersion, generatorVersions, generatorName) {
10083
11676
  super(outputDir, logger);
10084
11677
  this.config = config;
11678
+ this.cliVersion = cliVersion;
11679
+ this.generatorVersions = generatorVersions;
11680
+ this.generatorName = generatorName;
10085
11681
  }
10086
11682
  config;
10087
- name = "autoVersion";
11683
+ cliVersion;
11684
+ generatorVersions;
11685
+ generatorName;
11686
+ name = "generationCommit";
10088
11687
  async execute(_context) {
10089
- this.logger.info(
10090
- `AutoVersionStep: no-op scaffold for language=${this.config.language} (logic lands in FER-9980)`
10091
- );
11688
+ const prepared = await replayPrepare({
11689
+ outputDir: this.outputDir,
11690
+ cliVersion: this.cliVersion,
11691
+ generatorVersions: this.generatorVersions,
11692
+ generatorName: this.generatorName,
11693
+ skipApplication: this.config.skipApplication,
11694
+ logger: this.logger
11695
+ });
11696
+ if (prepared == null) {
11697
+ return {
11698
+ executed: true,
11699
+ success: true,
11700
+ preparedReplay: null
11701
+ };
11702
+ }
10092
11703
  return {
10093
11704
  executed: true,
10094
- success: true
11705
+ success: true,
11706
+ preparedReplay: prepared,
11707
+ previousGenerationSha: prepared.previousGenerationSha ?? void 0,
11708
+ currentGenerationSha: prepared.currentGenerationSha,
11709
+ baseBranchHead: prepared.baseBranchHead ?? void 0,
11710
+ flow: prepared.flow
10095
11711
  };
10096
11712
  }
10097
11713
  };
10098
11714
 
10099
11715
  // src/pipeline/steps/GithubStep.ts
10100
11716
  var import_rest2 = require("@octokit/rest");
10101
- var import_promises2 = require("fs/promises");
10102
- var import_path3 = require("path");
11717
+ var import_promises5 = require("fs/promises");
11718
+ var import_path7 = require("path");
10103
11719
 
10104
11720
  // src/pipeline/github/createReplayBranch.ts
10105
11721
  async function createReplayBranch(repository, branchName, commitMessage, replayConflictInfo, logger) {
@@ -10745,13 +12361,13 @@ var GithubStep = class extends BaseStep {
10745
12361
  }
10746
12362
  async ensureFernignore() {
10747
12363
  this.logger.debug("Checking for .fernignore file...");
10748
- const fernignorePath = (0, import_path3.join)(this.outputDir, ".fernignore");
12364
+ const fernignorePath = (0, import_path7.join)(this.outputDir, ".fernignore");
10749
12365
  try {
10750
- await (0, import_promises2.access)(fernignorePath);
12366
+ await (0, import_promises5.access)(fernignorePath);
10751
12367
  this.logger.debug(".fernignore already exists");
10752
12368
  } catch {
10753
12369
  this.logger.debug("Creating .fernignore file...");
10754
- await (0, import_promises2.writeFile)(fernignorePath, "# Specify files that shouldn't be modified by Fern\n", "utf-8");
12370
+ await (0, import_promises5.writeFile)(fernignorePath, "# Specify files that shouldn't be modified by Fern\n", "utf-8");
10755
12371
  }
10756
12372
  }
10757
12373
  deriveSkipCommit(replayResult) {
@@ -10818,242 +12434,6 @@ function resolveBranchAction({
10818
12434
  return "create-from-head";
10819
12435
  }
10820
12436
 
10821
- // src/replay/replay-run.ts
10822
- var import_replay = require("@fern-api/replay");
10823
- var import_child_process3 = require("child_process");
10824
- var import_fs3 = require("fs");
10825
- var import_path5 = require("path");
10826
-
10827
- // src/replay/fernignore.ts
10828
- var import_fs2 = require("fs");
10829
- var import_promises3 = require("fs/promises");
10830
- var import_path4 = require("path");
10831
- var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
10832
- var GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
10833
- async function ensureReplayFernignoreEntries(outputDir) {
10834
- const fernignorePath = (0, import_path4.join)(outputDir, ".fernignore");
10835
- let content = "";
10836
- try {
10837
- content = await (0, import_promises3.readFile)(fernignorePath, "utf-8");
10838
- } catch {
10839
- }
10840
- const lines = content.split("\n");
10841
- const toAdd = REPLAY_FERNIGNORE_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
10842
- if (toAdd.length > 0) {
10843
- const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
10844
- await (0, import_promises3.writeFile)(fernignorePath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
10845
- return true;
10846
- }
10847
- return false;
10848
- }
10849
- async function ensureGitattributesEntries(outputDir) {
10850
- const gitattributesPath = (0, import_path4.join)(outputDir, ".gitattributes");
10851
- let content = "";
10852
- try {
10853
- content = await (0, import_promises3.readFile)(gitattributesPath, "utf-8");
10854
- } catch {
10855
- }
10856
- const lines = content.split("\n");
10857
- const toAdd = GITATTRIBUTES_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
10858
- if (toAdd.length > 0) {
10859
- const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
10860
- await (0, import_promises3.writeFile)(gitattributesPath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
10861
- }
10862
- }
10863
- function ensureReplayFernignoreEntriesSync(outputDir) {
10864
- const fernignorePath = (0, import_path4.join)(outputDir, ".fernignore");
10865
- let content = "";
10866
- if ((0, import_fs2.existsSync)(fernignorePath)) {
10867
- content = (0, import_fs2.readFileSync)(fernignorePath, "utf-8");
10868
- }
10869
- const lines = content.split("\n");
10870
- const toAdd = REPLAY_FERNIGNORE_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
10871
- if (toAdd.length > 0) {
10872
- const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
10873
- (0, import_fs2.writeFileSync)(fernignorePath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
10874
- }
10875
- }
10876
- function ensureGitattributesEntriesSync(outputDir) {
10877
- const gitattributesPath = (0, import_path4.join)(outputDir, ".gitattributes");
10878
- let content = "";
10879
- if ((0, import_fs2.existsSync)(gitattributesPath)) {
10880
- content = (0, import_fs2.readFileSync)(gitattributesPath, "utf-8");
10881
- }
10882
- const lines = content.split("\n");
10883
- const toAdd = GITATTRIBUTES_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
10884
- if (toAdd.length > 0) {
10885
- const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
10886
- (0, import_fs2.writeFileSync)(gitattributesPath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
10887
- }
10888
- }
10889
-
10890
- // src/replay/replay-run.ts
10891
- async function replayRun(params) {
10892
- const {
10893
- outputDir,
10894
- cliVersion,
10895
- generatorVersions,
10896
- stageOnly = false,
10897
- generatorName,
10898
- skipApplication,
10899
- logger
10900
- } = params;
10901
- const lockfilePath = (0, import_path5.join)(outputDir, ".fern", "replay.lock");
10902
- if (!(0, import_fs3.existsSync)(lockfilePath)) {
10903
- return {
10904
- report: null,
10905
- fernignoreUpdated: false,
10906
- previousGenerationSha: null,
10907
- currentGenerationSha: null,
10908
- baseBranchHead: null
10909
- };
10910
- }
10911
- const baseBranchHead = gitRevParse(outputDir, "HEAD");
10912
- let previousGenerationSha = null;
10913
- let prevBaseBranchHead = null;
10914
- try {
10915
- const lockManager = new import_replay.LockfileManager(outputDir);
10916
- if (lockManager.exists()) {
10917
- const lock = lockManager.read();
10918
- previousGenerationSha = lock.current_generation;
10919
- const latestGen = lock.generations.find((g2) => g2.commit_sha === lock.current_generation);
10920
- prevBaseBranchHead = latestGen?.base_branch_head ?? null;
10921
- }
10922
- } catch {
10923
- }
10924
- if (previousGenerationSha != null) {
10925
- const sanitizedName = generatorName?.replace(/\//g, "--");
10926
- const namespacedTag = sanitizedName != null ? `fern-generation-base--${sanitizedName}` : null;
10927
- const tagSha = (namespacedTag != null ? gitRevParse(outputDir, namespacedTag) : null) ?? gitRevParse(outputDir, "fern-generation-base");
10928
- let shouldSync = false;
10929
- if (tagSha != null) {
10930
- const tagParent = gitRevParse(outputDir, `${tagSha}^`);
10931
- if (tagParent === prevBaseBranchHead || tagParent === previousGenerationSha) {
10932
- const tagDistance = gitDiffNameOnly(outputDir, tagSha, "HEAD").length;
10933
- const lockDistance = gitDiffNameOnly(
10934
- outputDir,
10935
- prevBaseBranchHead ?? previousGenerationSha,
10936
- "HEAD"
10937
- ).length;
10938
- shouldSync = tagDistance < lockDistance;
10939
- }
10940
- }
10941
- if (shouldSync && tagSha != null) {
10942
- if (!isTagMergedIntoHead(outputDir, tagSha)) {
10943
- logger?.warn(
10944
- `fern-generation-base tag ${tagSha} is not reachable from HEAD \u2014 skipping divergent-merge sync. The tag likely points at an unmerged generation (PR closed without merge).`
10945
- );
10946
- } else {
10947
- const syncService = new import_replay.ReplayService(outputDir, { enabled: true });
10948
- await syncService.syncFromDivergentMerge(tagSha, {
10949
- cliVersion,
10950
- generatorVersions,
10951
- baseBranchHead: baseBranchHead ?? void 0
10952
- });
10953
- try {
10954
- const freshLockManager = new import_replay.LockfileManager(outputDir);
10955
- if (freshLockManager.exists()) {
10956
- previousGenerationSha = freshLockManager.read().current_generation;
10957
- }
10958
- } catch {
10959
- }
10960
- }
10961
- }
10962
- }
10963
- const service = new import_replay.ReplayService(outputDir, {
10964
- enabled: true
10965
- });
10966
- let report;
10967
- try {
10968
- report = await service.runReplay({
10969
- cliVersion,
10970
- generatorVersions,
10971
- stageOnly,
10972
- skipApplication,
10973
- baseBranchHead: baseBranchHead ?? void 0
10974
- });
10975
- } catch (error) {
10976
- logger?.warn("Replay failed, continuing without replay: " + String(error));
10977
- return {
10978
- report: null,
10979
- fernignoreUpdated: false,
10980
- previousGenerationSha,
10981
- currentGenerationSha: null,
10982
- baseBranchHead
10983
- };
10984
- }
10985
- let currentGenerationSha = null;
10986
- let resolvedBaseBranchHead = baseBranchHead;
10987
- try {
10988
- const freshLockManager = new import_replay.LockfileManager(outputDir);
10989
- if (freshLockManager.exists()) {
10990
- const freshLock = freshLockManager.read();
10991
- currentGenerationSha = freshLock.current_generation;
10992
- const latestGen = freshLock.generations.find((g2) => g2.commit_sha === freshLock.current_generation);
10993
- if (latestGen?.base_branch_head) {
10994
- resolvedBaseBranchHead = latestGen.base_branch_head;
10995
- }
10996
- }
10997
- } catch {
10998
- }
10999
- const fernignoreUpdated = await ensureReplayFernignoreEntries(outputDir);
11000
- await ensureGitattributesEntries(outputDir);
11001
- return {
11002
- report,
11003
- fernignoreUpdated,
11004
- previousGenerationSha,
11005
- currentGenerationSha,
11006
- baseBranchHead: resolvedBaseBranchHead
11007
- };
11008
- }
11009
- function isTagMergedIntoHead(cwd2, tagSha) {
11010
- try {
11011
- (0, import_child_process3.execFileSync)("git", ["merge-base", "--is-ancestor", tagSha, "HEAD"], {
11012
- cwd: cwd2,
11013
- stdio: "pipe"
11014
- });
11015
- return true;
11016
- } catch {
11017
- }
11018
- const tagTree = gitRevParse(cwd2, `${tagSha}^{tree}`);
11019
- if (tagTree == null) {
11020
- return false;
11021
- }
11022
- try {
11023
- const trees = (0, import_child_process3.execFileSync)("git", ["log", "HEAD", "--format=%T", "--max-count=500"], {
11024
- cwd: cwd2,
11025
- encoding: "utf-8",
11026
- stdio: "pipe"
11027
- });
11028
- return trees.split("\n").some((t2) => t2.trim() === tagTree);
11029
- } catch {
11030
- return false;
11031
- }
11032
- }
11033
- function gitRevParse(cwd2, rev) {
11034
- try {
11035
- return (0, import_child_process3.execFileSync)("git", ["rev-parse", "--verify", rev], {
11036
- cwd: cwd2,
11037
- encoding: "utf-8",
11038
- stdio: "pipe"
11039
- }).trim();
11040
- } catch {
11041
- return null;
11042
- }
11043
- }
11044
- function gitDiffNameOnly(cwd2, from, to) {
11045
- try {
11046
- const output = (0, import_child_process3.execFileSync)("git", ["diff", "--name-only", from, to], {
11047
- cwd: cwd2,
11048
- encoding: "utf-8",
11049
- stdio: "pipe"
11050
- });
11051
- return output.trim().split("\n").filter(Boolean);
11052
- } catch {
11053
- return [];
11054
- }
11055
- }
11056
-
11057
12437
  // src/pipeline/steps/ReplayStep.ts
11058
12438
  var ReplayStep = class extends BaseStep {
11059
12439
  constructor(outputDir, logger, config, cliVersion, generatorVersions, generatorName) {
@@ -11068,8 +12448,23 @@ var ReplayStep = class extends BaseStep {
11068
12448
  generatorVersions;
11069
12449
  generatorName;
11070
12450
  name = "replay";
11071
- async execute(_context) {
11072
- const result = await replayRun({
12451
+ async execute(context) {
12452
+ const generationCommit = context.previousStepResults.generationCommit;
12453
+ const prepared = generationCommit?.preparedReplay;
12454
+ if (generationCommit != null && prepared == null) {
12455
+ return {
12456
+ executed: true,
12457
+ success: true,
12458
+ flow: "first-generation",
12459
+ patchesDetected: 0,
12460
+ patchesApplied: 0,
12461
+ patchesWithConflicts: 0
12462
+ };
12463
+ }
12464
+ const result = prepared != null ? await replayApply(prepared, {
12465
+ stageOnly: this.config.stageOnly ?? false,
12466
+ logger: this.logger
12467
+ }) : await replayRun({
11073
12468
  outputDir: this.outputDir,
11074
12469
  cliVersion: this.cliVersion,
11075
12470
  generatorVersions: this.generatorVersions,
@@ -11125,16 +12520,34 @@ var PostGenerationPipeline = class {
11125
12520
  constructor(config, logger = consolePipelineLogger) {
11126
12521
  this.config = config;
11127
12522
  this.logger = logger;
11128
- if (config.replay?.enabled && config.github?.mode === "push") {
12523
+ let replayEnabled = config.replay?.enabled ?? false;
12524
+ let autoVersionEnabled = config.autoVersion?.enabled ?? false;
12525
+ if (replayEnabled && config.github?.mode === "push") {
11129
12526
  this.logger.warn(
11130
12527
  "Replay is not supported with GitHub push mode. Disabling replay to prevent force push to base branch."
11131
12528
  );
11132
- config.replay.enabled = false;
12529
+ replayEnabled = false;
12530
+ }
12531
+ if (autoVersionEnabled && !replayEnabled) {
12532
+ this.logger.warn("AutoVersion requires Replay to be enabled. Disabling AutoVersion for this run.");
12533
+ autoVersionEnabled = false;
12534
+ }
12535
+ if (replayEnabled && config.replay != null) {
12536
+ this.steps.push(
12537
+ new GenerationCommitStep(
12538
+ config.outputDir,
12539
+ this.logger,
12540
+ { enabled: true, skipApplication: config.replay.skipApplication },
12541
+ config.cliVersion,
12542
+ config.generatorVersions,
12543
+ config.generatorName
12544
+ )
12545
+ );
11133
12546
  }
11134
- if (config.autoVersion?.enabled) {
12547
+ if (autoVersionEnabled && config.autoVersion != null) {
11135
12548
  this.steps.push(new AutoVersionStep(config.outputDir, this.logger, config.autoVersion));
11136
12549
  }
11137
- if (config.replay?.enabled) {
12550
+ if (replayEnabled && config.replay != null) {
11138
12551
  this.steps.push(
11139
12552
  new ReplayStep(
11140
12553
  config.outputDir,
@@ -11156,8 +12569,8 @@ var PostGenerationPipeline = class {
11156
12569
  async run() {
11157
12570
  if (this.config.github?.enabled) {
11158
12571
  try {
11159
- (0, import_child_process4.execFileSync)("git", ["config", "user.name", FERN_BOT_NAME], { cwd: this.config.outputDir });
11160
- (0, import_child_process4.execFileSync)("git", ["config", "user.email", FERN_BOT_EMAIL], { cwd: this.config.outputDir });
12572
+ (0, import_child_process5.execFileSync)("git", ["config", "user.name", FERN_BOT_NAME], { cwd: this.config.outputDir });
12573
+ (0, import_child_process5.execFileSync)("git", ["config", "user.email", FERN_BOT_EMAIL], { cwd: this.config.outputDir });
11161
12574
  } catch {
11162
12575
  }
11163
12576
  }
@@ -11171,7 +12584,10 @@ var PostGenerationPipeline = class {
11171
12584
  for (const step of this.steps) {
11172
12585
  try {
11173
12586
  const stepResult = await step.execute(pipelineContext);
11174
- if (step.name === "replay") {
12587
+ if (step.name === "generationCommit") {
12588
+ result.steps.generationCommit = stepResult;
12589
+ pipelineContext.previousStepResults.generationCommit = stepResult;
12590
+ } else if (step.name === "replay") {
11175
12591
  result.steps.replay = stepResult;
11176
12592
  pipelineContext.previousStepResults.replay = stepResult;
11177
12593
  } else if (step.name === "autoVersion") {
@@ -11206,8 +12622,8 @@ function replayForget(params) {
11206
12622
 
11207
12623
  // src/replay/replay-init.ts
11208
12624
  var import_replay3 = require("@fern-api/replay");
11209
- var import_fs4 = require("fs");
11210
- var import_path6 = require("path");
12625
+ var import_fs6 = require("fs");
12626
+ var import_path8 = require("path");
11211
12627
  var import_tmp_promise2 = __toESM(require("tmp-promise"));
11212
12628
  async function replayInit(params) {
11213
12629
  const { githubRepo, token, dryRun } = params;
@@ -11233,10 +12649,10 @@ async function replayInit(params) {
11233
12649
  }
11234
12650
  ensureReplayFernignoreEntriesSync(repoPath);
11235
12651
  ensureGitattributesEntriesSync(repoPath);
11236
- const lockfilePath = (0, import_path6.join)(repoPath, ".fern", "replay.lock");
11237
- const lockfileContent = (0, import_fs4.existsSync)(lockfilePath) ? (0, import_fs4.readFileSync)(lockfilePath, "utf-8") : void 0;
11238
- const replayYmlPath = (0, import_path6.join)(repoPath, ".fern", "replay.yml");
11239
- const replayYmlContent = (0, import_fs4.existsSync)(replayYmlPath) ? (0, import_fs4.readFileSync)(replayYmlPath, "utf-8") : void 0;
12652
+ const lockfilePath = (0, import_path8.join)(repoPath, ".fern", "replay.lock");
12653
+ const lockfileContent = (0, import_fs6.existsSync)(lockfilePath) ? (0, import_fs6.readFileSync)(lockfilePath, "utf-8") : void 0;
12654
+ const replayYmlPath = (0, import_path8.join)(repoPath, ".fern", "replay.yml");
12655
+ const replayYmlContent = (0, import_fs6.existsSync)(replayYmlPath) ? (0, import_fs6.readFileSync)(replayYmlPath, "utf-8") : void 0;
11240
12656
  return {
11241
12657
  bootstrap: bootstrapResult,
11242
12658
  lockfileContent,