@fern-api/generator-cli 0.9.10 → 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 +178 -30
  2. package/dist/api.js +2106 -423
  3. package/dist/cli.js +10916 -4636
  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
  }
@@ -881,6 +881,7 @@ var require_dist2 = __commonJS({
881
881
  var api_exports2 = {};
882
882
  __export(api_exports2, {
883
883
  PostGenerationPipeline: () => PostGenerationPipeline,
884
+ ReplaySubmitError: () => ReplaySubmitError,
884
885
  consolePipelineLogger: () => consolePipelineLogger,
885
886
  formatBootstrapSummary: () => formatBootstrapSummary,
886
887
  formatReplayPrBody: () => formatReplayPrBody,
@@ -892,11 +893,13 @@ __export(api_exports2, {
892
893
  githubPush: () => githubPush,
893
894
  githubRelease: () => githubRelease,
894
895
  logReplaySummary: () => logReplaySummary,
896
+ parseOwnerRepo: () => parseOwnerRepo,
895
897
  replayForget: () => replayForget,
896
898
  replayInit: () => replayInit,
897
899
  replayResolve: () => replayResolve,
898
900
  replayRun: () => replayRun,
899
- replayStatus: () => replayStatus
901
+ replayStatus: () => replayStatus,
902
+ submitReplayInit: () => submitReplayInit
900
903
  });
901
904
  module.exports = __toCommonJS(api_exports2);
902
905
 
@@ -6017,6 +6020,46 @@ var ClonedRepository = class _ClonedRepository {
6017
6020
  const result = await this.git.raw(["rev-parse", "HEAD^{tree}"]);
6018
6021
  return result.trim();
6019
6022
  }
6023
+ async getHeadCommitMessage() {
6024
+ await this.git.cwd(this.clonePath);
6025
+ const result = await this.git.raw(["log", "-1", "--format=%B", "HEAD"]);
6026
+ return result.replace(/\n$/, "");
6027
+ }
6028
+ async getHeadParents() {
6029
+ await this.git.cwd(this.clonePath);
6030
+ const result = await this.git.raw(["rev-list", "--parents", "-n", "1", "HEAD"]);
6031
+ const parts = result.trim().split(/\s+/);
6032
+ return parts.slice(1);
6033
+ }
6034
+ /**
6035
+ * Pushes a specific object (commit SHA) to a remote ref.
6036
+ * Used to upload tree and blob objects to the remote without moving a branch.
6037
+ *
6038
+ * Pass `force: true` when overwriting a ref that may already point to an
6039
+ * unrelated commit (e.g. retrying a temp ref after a local rebase rewrites
6040
+ * history so the new SHA is not a descendant of the previous one).
6041
+ */
6042
+ async pushObjectToRef(localSha, remoteRef, { force = false } = {}) {
6043
+ await this.git.cwd(this.clonePath);
6044
+ const spec = force ? `+${localSha}:${remoteRef}` : `${localSha}:${remoteRef}`;
6045
+ await this.git.raw(["push", "origin", spec]);
6046
+ }
6047
+ /**
6048
+ * Resets the current branch HEAD to the given SHA. After fetching a newly-created remote
6049
+ * signed commit, use this to align the local branch so subsequent operations (tag push,
6050
+ * getHeadSha(), etc.) see the signed SHA.
6051
+ */
6052
+ async resetHardToSha(sha) {
6053
+ await this.git.cwd(this.clonePath);
6054
+ await this.git.raw(["reset", "--hard", sha]);
6055
+ }
6056
+ /**
6057
+ * Rebases the current branch onto the given remote branch (`git pull --rebase origin <branch>`).
6058
+ */
6059
+ async pullWithRebase(branch) {
6060
+ await this.git.cwd(this.clonePath);
6061
+ await this.git.raw(["pull", "origin", branch, "--rebase"]);
6062
+ }
6020
6063
  async getCommitTreeHash(commitSha) {
6021
6064
  await this.git.cwd(this.clonePath);
6022
6065
  const result = await this.git.raw(["rev-parse", `${commitSha}^{tree}`]);
@@ -10017,177 +10060,1944 @@ var consolePipelineLogger = {
10017
10060
  };
10018
10061
 
10019
10062
  // src/pipeline/PostGenerationPipeline.ts
10020
- var import_child_process4 = require("child_process");
10063
+ var import_child_process5 = require("child_process");
10021
10064
 
10022
10065
  // src/pipeline/github/constants.ts
10023
10066
  var FERN_BOT_NAME = "fern-api";
10024
10067
  var FERN_BOT_EMAIL = "115122769+fern-api[bot]@users.noreply.github.com";
10025
10068
  var FERN_BOT_LOGIN = "fern-api[bot]";
10026
10069
 
10027
- // src/pipeline/steps/GithubStep.ts
10028
- var import_rest2 = require("@octokit/rest");
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");
10029
10079
  var import_promises2 = require("fs/promises");
10030
10080
  var import_path3 = require("path");
10031
-
10032
- // src/pipeline/github/createReplayBranch.ts
10033
- async function createReplayBranch(repository, branchName, commitMessage, replayConflictInfo, logger) {
10034
- if (replayConflictInfo == null) {
10035
- await repository.createBranchFromHead(branchName);
10036
- return void 0;
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
+ }
10037
10097
  }
10038
- const parentSha = replayConflictInfo.previousGenerationSha;
10039
- await repository.createBranchFromHead(branchName);
10040
- const genTreeHash = await repository.getCommitTreeHash(replayConflictInfo.currentGenerationSha);
10041
- const tagCommitSha = await repository.commitTree(
10042
- genTreeHash,
10043
- parentSha,
10044
- `[fern-generated] ${commitMessage ?? "Update SDK"}`
10045
- );
10046
- return tagCommitSha;
10047
- }
10048
-
10049
- // src/pipeline/github/findExistingUpdatablePR.ts
10050
- async function findExistingUpdatablePR(octokit, owner, repo, baseBranch, logger) {
10051
- try {
10052
- const { data: pulls } = await octokit.pulls.list({
10053
- owner,
10054
- repo,
10055
- state: "open",
10056
- base: baseBranch,
10057
- sort: "updated",
10058
- direction: "desc",
10059
- per_page: 20
10060
- });
10061
- for (const pr of pulls) {
10062
- const prAuthor = pr.user?.login;
10063
- if (prAuthor !== FERN_BOT_LOGIN) {
10064
- logger.debug(`PR #${pr.number}: author ${prAuthor} is not ${FERN_BOT_LOGIN}, checking commits anyway`);
10098
+ };
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;
10065
10234
  }
10066
- if (!pr.head.ref.startsWith("fern-bot/")) {
10067
- logger.debug(`PR #${pr.number} skipped: branch ${pr.head.ref} does not start with fern-bot/`);
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
+ );
10068
10259
  continue;
10069
10260
  }
10070
- const hasOnlyGenerationCommits = await checkPRHasOnlyGenerationCommits(
10071
- octokit,
10072
- owner,
10073
- repo,
10074
- pr.number,
10075
- logger
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}`
10076
10265
  );
10077
- if (hasOnlyGenerationCommits) {
10078
- logger.debug(`Found existing updatable PR #${pr.number} with branch ${pr.head.ref}`);
10079
- return {
10080
- number: pr.number,
10081
- headBranch: pr.head.ref,
10082
- htmlUrl: pr.html_url,
10083
- isDraft: pr.draft ?? false,
10084
- nodeId: pr.node_id
10085
- };
10086
- } else {
10087
- logger.debug(`PR #${pr.number} skipped: contains non-generation commits`);
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
+ }
10088
10303
  }
10089
10304
  }
10090
- return void 0;
10091
- } catch (error) {
10092
- logger.debug(`Error finding existing PRs: ${extractErrorMessage(error)}`);
10305
+ this.logger.warn(`No matching minus line found after scanning ${linesScanned} lines backwards`);
10093
10306
  return void 0;
10094
10307
  }
10095
- }
10096
- async function checkPRHasOnlyGenerationCommits(octokit, owner, repo, pullNumber, logger) {
10097
- try {
10098
- const { data: commits } = await octokit.pulls.listCommits({
10099
- owner,
10100
- repo,
10101
- pull_number: pullNumber,
10102
- per_page: 100
10103
- });
10104
- for (const commit of commits) {
10105
- const authorLogin = commit.author?.login;
10106
- const authorEmail = commit.commit.author?.email;
10107
- const commitMessage = commit.commit.message ?? "";
10108
- const isFernAuthor = authorLogin === FERN_BOT_LOGIN || authorEmail === "115122769+fern-api[bot]@users.noreply.github.com" || commit.commit.author?.name === "fern-api";
10109
- const isFernCommitMessage = commitMessage.startsWith("[fern-generated]") || commitMessage.startsWith("[fern-replay]");
10110
- if (!isFernAuthor && !isFernCommitMessage) {
10111
- logger.debug(
10112
- `Commit ${commit.sha.substring(0, 7)} is not a generation commit: author=${authorLogin}, email=${authorEmail}, message="${commitMessage.slice(0, 40)}"`
10113
- );
10114
- return false;
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);
10115
10334
  }
10116
10335
  }
10117
- return true;
10118
- } catch (error) {
10119
- logger.debug(`Error checking PR commits: ${extractErrorMessage(error)}`);
10120
- return false;
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");
10121
10346
  }
10122
- }
10123
-
10124
- // src/pipeline/github/parseCommitMessage.ts
10125
- function parseCommitMessageForPR(commitMessage, changelogEntry, prDescription, versionBumpReason, previousVersion, newVersion, versionBump, changelogUrl) {
10126
- const lines = commitMessage.split("\n");
10127
- const prTitle = lines[0]?.trim() || "SDK Generation";
10128
- const bodyFromCommit = lines.slice(1).join("\n").trim() || "Automated SDK generation by Fern";
10129
- let prBody = prDescription?.trim() || changelogEntry?.trim() || bodyFromCommit;
10130
- const isBreaking = versionBump === "MAJOR";
10131
- if (previousVersion?.trim() && newVersion?.trim()) {
10132
- const emoji = isBreaking ? "\u26A0\uFE0F " : "";
10133
- const header = `## ${emoji}${previousVersion.trim()} \u2192 ${newVersion.trim()}`;
10134
- if (isBreaking && versionBumpReason?.trim()) {
10135
- prBody = `${header}
10136
-
10137
- **Breaking:** ${versionBumpReason.trim()}
10138
-
10139
- ${prBody}`;
10140
- } else {
10141
- prBody = `${header}
10142
-
10143
- ${prBody}`;
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);
10144
10428
  }
10145
- } else if (isBreaking && versionBumpReason?.trim()) {
10146
- prBody = `\u26A0\uFE0F **Breaking:** ${versionBumpReason.trim()}
10147
-
10148
- ${prBody}`;
10429
+ if (currentChunkTexts.length > 0) {
10430
+ chunks.push(currentChunkTexts.join("\n"));
10431
+ }
10432
+ return chunks;
10149
10433
  }
10150
- if (changelogUrl?.trim()) {
10151
- prBody += `
10152
-
10153
- ---
10154
-
10155
- [See full changelog](${changelogUrl.trim()})`;
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;
10156
10482
  }
10157
- return { prTitle, prBody };
10158
- }
10159
-
10160
- // src/pipeline/replay-summary.ts
10161
- function plural(n, word) {
10162
- return `${n} ${word}${n === 1 ? "" : "s"}`;
10163
- }
10164
- function formatConflictReason(reason) {
10165
- switch (reason) {
10166
- case "same-line-edit":
10167
- return "The new generation changed the same lines you edited";
10168
- case "new-file-both":
10169
- return "Both the generator and your customization created this file";
10170
- case "base-generation-mismatch":
10171
- return "This customization was from a previous generation";
10172
- case "patch-apply-failed":
10173
- return "The customization could not be applied to the new code";
10174
- default:
10175
- return "Your edit overlaps with changes in the new generation";
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;
10176
10503
  }
10177
- }
10178
- function patchDescription(detail) {
10179
- if (detail.patchMessage && detail.patchMessage !== "update") {
10180
- return detail.patchMessage;
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;
10181
10540
  }
10182
- const firstFile = detail.files[0];
10183
- return firstFile != null ? `changes in ${firstFile}` : "customization";
10184
- }
10185
- function logReplaySummary(result, logger) {
10186
- if (!result.executed) {
10187
- return;
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;
10188
10554
  }
10189
- const applied = result.patchesApplied ?? 0;
10190
- const absorbed = result.patchesAbsorbed ?? 0;
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) {
11676
+ super(outputDir, logger);
11677
+ this.config = config;
11678
+ this.cliVersion = cliVersion;
11679
+ this.generatorVersions = generatorVersions;
11680
+ this.generatorName = generatorName;
11681
+ }
11682
+ config;
11683
+ cliVersion;
11684
+ generatorVersions;
11685
+ generatorName;
11686
+ name = "generationCommit";
11687
+ async execute(_context) {
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
+ }
11703
+ return {
11704
+ executed: 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
11711
+ };
11712
+ }
11713
+ };
11714
+
11715
+ // src/pipeline/steps/GithubStep.ts
11716
+ var import_rest2 = require("@octokit/rest");
11717
+ var import_promises5 = require("fs/promises");
11718
+ var import_path7 = require("path");
11719
+
11720
+ // src/pipeline/github/createReplayBranch.ts
11721
+ async function createReplayBranch(repository, branchName, commitMessage, replayConflictInfo, logger) {
11722
+ if (replayConflictInfo == null) {
11723
+ await repository.createBranchFromHead(branchName);
11724
+ return void 0;
11725
+ }
11726
+ const parentSha = replayConflictInfo.previousGenerationSha;
11727
+ await repository.createBranchFromHead(branchName);
11728
+ const genTreeHash = await repository.getCommitTreeHash(replayConflictInfo.currentGenerationSha);
11729
+ const tagCommitSha = await repository.commitTree(
11730
+ genTreeHash,
11731
+ parentSha,
11732
+ `[fern-generated] ${commitMessage ?? "Update SDK"}`
11733
+ );
11734
+ return tagCommitSha;
11735
+ }
11736
+
11737
+ // src/pipeline/github/findExistingUpdatablePR.ts
11738
+ async function findExistingUpdatablePR(octokit, owner, repo, baseBranch, logger) {
11739
+ try {
11740
+ const { data: pulls } = await octokit.pulls.list({
11741
+ owner,
11742
+ repo,
11743
+ state: "open",
11744
+ base: baseBranch,
11745
+ sort: "updated",
11746
+ direction: "desc",
11747
+ per_page: 20
11748
+ });
11749
+ for (const pr of pulls) {
11750
+ const prAuthor = pr.user?.login;
11751
+ if (prAuthor !== FERN_BOT_LOGIN) {
11752
+ logger.debug(`PR #${pr.number}: author ${prAuthor} is not ${FERN_BOT_LOGIN}, checking commits anyway`);
11753
+ }
11754
+ if (!pr.head.ref.startsWith("fern-bot/")) {
11755
+ logger.debug(`PR #${pr.number} skipped: branch ${pr.head.ref} does not start with fern-bot/`);
11756
+ continue;
11757
+ }
11758
+ const hasOnlyGenerationCommits = await checkPRHasOnlyGenerationCommits(
11759
+ octokit,
11760
+ owner,
11761
+ repo,
11762
+ pr.number,
11763
+ logger
11764
+ );
11765
+ if (hasOnlyGenerationCommits) {
11766
+ logger.debug(`Found existing updatable PR #${pr.number} with branch ${pr.head.ref}`);
11767
+ return {
11768
+ number: pr.number,
11769
+ headBranch: pr.head.ref,
11770
+ htmlUrl: pr.html_url,
11771
+ isDraft: pr.draft ?? false,
11772
+ nodeId: pr.node_id
11773
+ };
11774
+ } else {
11775
+ logger.debug(`PR #${pr.number} skipped: contains non-generation commits`);
11776
+ }
11777
+ }
11778
+ return void 0;
11779
+ } catch (error) {
11780
+ logger.debug(`Error finding existing PRs: ${extractErrorMessage(error)}`);
11781
+ return void 0;
11782
+ }
11783
+ }
11784
+ async function checkPRHasOnlyGenerationCommits(octokit, owner, repo, pullNumber, logger) {
11785
+ try {
11786
+ const { data: commits } = await octokit.pulls.listCommits({
11787
+ owner,
11788
+ repo,
11789
+ pull_number: pullNumber,
11790
+ per_page: 100
11791
+ });
11792
+ for (const commit of commits) {
11793
+ const authorLogin = commit.author?.login;
11794
+ const authorEmail = commit.commit.author?.email;
11795
+ const commitMessage = commit.commit.message ?? "";
11796
+ const isFernAuthor = authorLogin === FERN_BOT_LOGIN || authorEmail === "115122769+fern-api[bot]@users.noreply.github.com" || commit.commit.author?.name === "fern-api";
11797
+ const isFernCommitMessage = commitMessage.startsWith("[fern-generated]") || commitMessage.startsWith("[fern-replay]");
11798
+ if (!isFernAuthor && !isFernCommitMessage) {
11799
+ logger.debug(
11800
+ `Commit ${commit.sha.substring(0, 7)} is not a generation commit: author=${authorLogin}, email=${authorEmail}, message="${commitMessage.slice(0, 40)}"`
11801
+ );
11802
+ return false;
11803
+ }
11804
+ }
11805
+ return true;
11806
+ } catch (error) {
11807
+ logger.debug(`Error checking PR commits: ${extractErrorMessage(error)}`);
11808
+ return false;
11809
+ }
11810
+ }
11811
+
11812
+ // src/pipeline/github/parseCommitMessage.ts
11813
+ function parseCommitMessageForPR(commitMessage, changelogEntry, prDescription, versionBumpReason, previousVersion, newVersion, versionBump, changelogUrl) {
11814
+ const lines = commitMessage.split("\n");
11815
+ const prTitle = lines[0]?.trim() || "SDK Generation";
11816
+ const bodyFromCommit = lines.slice(1).join("\n").trim() || "Automated SDK generation by Fern";
11817
+ let prBody = prDescription?.trim() || changelogEntry?.trim() || bodyFromCommit;
11818
+ const isBreaking = versionBump === "MAJOR";
11819
+ if (previousVersion?.trim() && newVersion?.trim()) {
11820
+ const emoji = isBreaking ? "\u26A0\uFE0F " : "";
11821
+ const header = `## ${emoji}${previousVersion.trim()} \u2192 ${newVersion.trim()}`;
11822
+ if (isBreaking && versionBumpReason?.trim()) {
11823
+ prBody = `${header}
11824
+
11825
+ **Breaking:** ${versionBumpReason.trim()}
11826
+
11827
+ ${prBody}`;
11828
+ } else {
11829
+ prBody = `${header}
11830
+
11831
+ ${prBody}`;
11832
+ }
11833
+ } else if (isBreaking && versionBumpReason?.trim()) {
11834
+ prBody = `\u26A0\uFE0F **Breaking:** ${versionBumpReason.trim()}
11835
+
11836
+ ${prBody}`;
11837
+ }
11838
+ if (changelogUrl?.trim()) {
11839
+ prBody += `
11840
+
11841
+ ---
11842
+
11843
+ [See full changelog](${changelogUrl.trim()})`;
11844
+ }
11845
+ return { prTitle, prBody };
11846
+ }
11847
+
11848
+ // src/pipeline/github/pushSignedCommit.ts
11849
+ var MAX_CONCURRENT_PUSH_RETRIES = 3;
11850
+ async function pushSignedCommit({
11851
+ repository,
11852
+ octokit,
11853
+ owner,
11854
+ repo,
11855
+ branch,
11856
+ force,
11857
+ rebaseOnConflict = false,
11858
+ logger
11859
+ }) {
11860
+ const tempRef = `refs/temp/fern-${Date.now()}`;
11861
+ let tempRefPushed = false;
11862
+ try {
11863
+ let [localHeadSha, treeSha, message, parents] = await Promise.all([
11864
+ repository.getHeadSha(),
11865
+ repository.getHeadTreeHash(),
11866
+ repository.getHeadCommitMessage(),
11867
+ repository.getHeadParents()
11868
+ ]);
11869
+ await repository.pushObjectToRef(localHeadSha, tempRef);
11870
+ tempRefPushed = true;
11871
+ for (let attempt = 0; attempt < MAX_CONCURRENT_PUSH_RETRIES; attempt++) {
11872
+ const { data: signedCommit } = await octokit.git.createCommit({
11873
+ owner,
11874
+ repo,
11875
+ message,
11876
+ tree: treeSha,
11877
+ parents
11878
+ });
11879
+ const signedSha = signedCommit.sha;
11880
+ try {
11881
+ await upsertBranchRef({ octokit, owner, repo, branch, sha: signedSha, force });
11882
+ logger.debug(`Updated refs/heads/${branch} to signed commit ${signedSha}`);
11883
+ await syncLocalToSignedCommit({ repository, branch, signedSha });
11884
+ return signedSha;
11885
+ } catch (err) {
11886
+ if (!isNonFastForwardError(err) || force || attempt >= MAX_CONCURRENT_PUSH_RETRIES - 1) {
11887
+ throw err;
11888
+ }
11889
+ if (rebaseOnConflict) {
11890
+ logger.warn(
11891
+ `Non-fast-forward on refs/heads/${branch} (attempt ${attempt + 1}/${MAX_CONCURRENT_PUSH_RETRIES}); rebasing locally and retrying.`
11892
+ );
11893
+ await repository.pullWithRebase(branch);
11894
+ [localHeadSha, treeSha, message, parents] = await Promise.all([
11895
+ repository.getHeadSha(),
11896
+ repository.getHeadTreeHash(),
11897
+ repository.getHeadCommitMessage(),
11898
+ repository.getHeadParents()
11899
+ ]);
11900
+ await repository.pushObjectToRef(localHeadSha, tempRef, { force: true });
11901
+ } else {
11902
+ throw err;
11903
+ }
11904
+ }
11905
+ }
11906
+ throw new Error(
11907
+ `Failed to push signed commit to refs/heads/${branch} after ${MAX_CONCURRENT_PUSH_RETRIES} attempts`
11908
+ );
11909
+ } finally {
11910
+ if (tempRefPushed) {
11911
+ try {
11912
+ await octokit.git.deleteRef({ owner, repo, ref: tempRef.replace(/^refs\//, "") });
11913
+ } catch (err) {
11914
+ logger.debug(`Failed to delete temp ref ${tempRef}: ${extractErrorMessage(err)}`);
11915
+ }
11916
+ }
11917
+ }
11918
+ }
11919
+ async function upsertBranchRef({
11920
+ octokit,
11921
+ owner,
11922
+ repo,
11923
+ branch,
11924
+ sha,
11925
+ force
11926
+ }) {
11927
+ try {
11928
+ await octokit.git.updateRef({
11929
+ owner,
11930
+ repo,
11931
+ ref: `heads/${branch}`,
11932
+ sha,
11933
+ force
11934
+ });
11935
+ } catch (err) {
11936
+ if (isNotFoundError(err)) {
11937
+ await octokit.git.createRef({
11938
+ owner,
11939
+ repo,
11940
+ ref: `refs/heads/${branch}`,
11941
+ sha
11942
+ });
11943
+ return;
11944
+ }
11945
+ throw err;
11946
+ }
11947
+ }
11948
+ async function syncLocalToSignedCommit({
11949
+ repository,
11950
+ branch,
11951
+ signedSha
11952
+ }) {
11953
+ await repository.fetch(["origin", branch]);
11954
+ await repository.resetHardToSha(signedSha);
11955
+ }
11956
+ function hasNumericStatus(err) {
11957
+ return typeof err === "object" && err != null && "status" in err && typeof err.status === "number";
11958
+ }
11959
+ function isNotFoundError(err) {
11960
+ return hasNumericStatus(err) && err.status === 404;
11961
+ }
11962
+ function isNonFastForwardError(err) {
11963
+ if (!hasNumericStatus(err) || err.status !== 422) {
11964
+ return false;
11965
+ }
11966
+ const message = extractErrorMessage(err).toLowerCase();
11967
+ return message.includes("not a fast forward") || message.includes("not a fast-forward") || message.includes("update is not a fast forward");
11968
+ }
11969
+
11970
+ // src/pipeline/replay-summary.ts
11971
+ function plural(n, word) {
11972
+ return `${n} ${word}${n === 1 ? "" : "s"}`;
11973
+ }
11974
+ function formatConflictReason(reason) {
11975
+ switch (reason) {
11976
+ case "same-line-edit":
11977
+ return "The new generation changed the same lines you edited";
11978
+ case "new-file-both":
11979
+ return "Both the generator and your customization created this file";
11980
+ case "base-generation-mismatch":
11981
+ return "This customization was from a previous generation";
11982
+ case "patch-apply-failed":
11983
+ return "The customization could not be applied to the new code";
11984
+ default:
11985
+ return "Your edit overlaps with changes in the new generation";
11986
+ }
11987
+ }
11988
+ function patchDescription(detail) {
11989
+ if (detail.patchMessage && detail.patchMessage !== "update") {
11990
+ return detail.patchMessage;
11991
+ }
11992
+ const firstFile = detail.files[0];
11993
+ return firstFile != null ? `changes in ${firstFile}` : "customization";
11994
+ }
11995
+ function logReplaySummary(result, logger) {
11996
+ if (!result.executed) {
11997
+ return;
11998
+ }
11999
+ const applied = result.patchesApplied ?? 0;
12000
+ const absorbed = result.patchesAbsorbed ?? 0;
10191
12001
  const unresolvedCount = result.unresolvedPatches?.length ?? 0;
10192
12002
  const preserved = applied - absorbed;
10193
12003
  logger.debug(
@@ -10288,16 +12098,6 @@ function formatReplayPrBody(result, options) {
10288
12098
  return parts.join("\n");
10289
12099
  }
10290
12100
 
10291
- // src/pipeline/steps/BaseStep.ts
10292
- var BaseStep = class {
10293
- constructor(outputDir, logger) {
10294
- this.outputDir = outputDir;
10295
- this.logger = logger;
10296
- }
10297
- outputDir;
10298
- logger;
10299
- };
10300
-
10301
12101
  // src/pipeline/steps/GithubStep.ts
10302
12102
  var GithubStep = class extends BaseStep {
10303
12103
  constructor(outputDir, logger, config) {
@@ -10407,7 +12207,7 @@ var GithubStep = class extends BaseStep {
10407
12207
  await repository.commitAllChanges(finalCommitMessage2);
10408
12208
  this.logger.debug(`Committed changes to local copy of GitHub repository at ${this.outputDir}`);
10409
12209
  }
10410
- if (this.config.automationMode) {
12210
+ if (shouldCheckNoDiff(this.config)) {
10411
12211
  const noDiff = await repository.treeHashEquals(`origin/${baseBranch}`);
10412
12212
  if (noDiff) {
10413
12213
  this.logger.info("No changes detected after generation \u2014 skipping PR creation");
@@ -10418,13 +12218,17 @@ var GithubStep = class extends BaseStep {
10418
12218
  executed: true,
10419
12219
  success: true,
10420
12220
  updatedExistingPr: isUpdatingExistingPR
10421
- };
10422
- if (!this.config.previewMode) {
10423
- if (isUpdatingExistingPR) {
10424
- await repository.forcePush();
10425
- } else {
10426
- await repository.push();
10427
- }
12221
+ };
12222
+ if (!this.config.previewMode) {
12223
+ await pushSignedCommit({
12224
+ repository,
12225
+ octokit,
12226
+ owner,
12227
+ repo,
12228
+ branch: prBranch,
12229
+ force: isUpdatingExistingPR,
12230
+ logger: this.logger
12231
+ });
10428
12232
  const pushedBranch = await repository.getCurrentBranch();
10429
12233
  result.branchUrl = `https://github.com/${this.config.uri}/tree/${pushedBranch}`;
10430
12234
  this.logger.info(`Pushed branch: ${result.branchUrl}`);
@@ -10525,7 +12329,7 @@ var GithubStep = class extends BaseStep {
10525
12329
  const finalCommitMessage = this.config.commitMessage ?? "SDK Generation";
10526
12330
  await repository.commitAllChanges(finalCommitMessage);
10527
12331
  this.logger.debug(`Committed changes to local copy of GitHub repository at ${this.outputDir}`);
10528
- if (this.config.automationMode) {
12332
+ if (shouldCheckNoDiff(this.config)) {
10529
12333
  const noDiff = await repository.treeHashEquals(`origin/${baseBranch}`);
10530
12334
  if (noDiff) {
10531
12335
  this.logger.info("No changes detected after generation \u2014 skipping push");
@@ -10537,7 +12341,18 @@ var GithubStep = class extends BaseStep {
10537
12341
  success: true
10538
12342
  };
10539
12343
  if (!this.config.previewMode) {
10540
- await repository.pushWithRebasingRemote();
12344
+ const octokit = new import_rest2.Octokit({ auth: this.config.token });
12345
+ const { owner, repo } = parseRepository(this.config.uri);
12346
+ await pushSignedCommit({
12347
+ repository,
12348
+ octokit,
12349
+ owner,
12350
+ repo,
12351
+ branch: baseBranch,
12352
+ force: false,
12353
+ rebaseOnConflict: true,
12354
+ logger: this.logger
12355
+ });
10541
12356
  const pushedBranch = await repository.getCurrentBranch();
10542
12357
  result.branchUrl = `https://github.com/${this.config.uri}/tree/${pushedBranch}`;
10543
12358
  this.logger.info(`Pushed branch: ${result.branchUrl}`);
@@ -10546,13 +12361,13 @@ var GithubStep = class extends BaseStep {
10546
12361
  }
10547
12362
  async ensureFernignore() {
10548
12363
  this.logger.debug("Checking for .fernignore file...");
10549
- const fernignorePath = (0, import_path3.join)(this.outputDir, ".fernignore");
12364
+ const fernignorePath = (0, import_path7.join)(this.outputDir, ".fernignore");
10550
12365
  try {
10551
- await (0, import_promises2.access)(fernignorePath);
12366
+ await (0, import_promises5.access)(fernignorePath);
10552
12367
  this.logger.debug(".fernignore already exists");
10553
12368
  } catch {
10554
12369
  this.logger.debug("Creating .fernignore file...");
10555
- 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");
10556
12371
  }
10557
12372
  }
10558
12373
  deriveSkipCommit(replayResult) {
@@ -10596,6 +12411,9 @@ _Fern Run ID: \`${config.runId}\`_`;
10596
12411
  function shouldEnableAutomerge(config) {
10597
12412
  return config.automationMode === true && config.autoMerge === true && config.hasBreakingChanges !== true;
10598
12413
  }
12414
+ function shouldCheckNoDiff(config) {
12415
+ return config.skipIfNoDiff === true;
12416
+ }
10599
12417
  function shouldPushGenerationBaseTag(replayResult) {
10600
12418
  return (replayResult?.patchesWithConflicts ?? 0) > 0;
10601
12419
  }
@@ -10616,242 +12434,6 @@ function resolveBranchAction({
10616
12434
  return "create-from-head";
10617
12435
  }
10618
12436
 
10619
- // src/replay/replay-run.ts
10620
- var import_replay = require("@fern-api/replay");
10621
- var import_child_process3 = require("child_process");
10622
- var import_fs3 = require("fs");
10623
- var import_path5 = require("path");
10624
-
10625
- // src/replay/fernignore.ts
10626
- var import_fs2 = require("fs");
10627
- var import_promises3 = require("fs/promises");
10628
- var import_path4 = require("path");
10629
- var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
10630
- var GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
10631
- async function ensureReplayFernignoreEntries(outputDir) {
10632
- const fernignorePath = (0, import_path4.join)(outputDir, ".fernignore");
10633
- let content = "";
10634
- try {
10635
- content = await (0, import_promises3.readFile)(fernignorePath, "utf-8");
10636
- } catch {
10637
- }
10638
- const lines = content.split("\n");
10639
- const toAdd = REPLAY_FERNIGNORE_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
10640
- if (toAdd.length > 0) {
10641
- const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
10642
- await (0, import_promises3.writeFile)(fernignorePath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
10643
- return true;
10644
- }
10645
- return false;
10646
- }
10647
- async function ensureGitattributesEntries(outputDir) {
10648
- const gitattributesPath = (0, import_path4.join)(outputDir, ".gitattributes");
10649
- let content = "";
10650
- try {
10651
- content = await (0, import_promises3.readFile)(gitattributesPath, "utf-8");
10652
- } catch {
10653
- }
10654
- const lines = content.split("\n");
10655
- const toAdd = GITATTRIBUTES_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
10656
- if (toAdd.length > 0) {
10657
- const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
10658
- await (0, import_promises3.writeFile)(gitattributesPath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
10659
- }
10660
- }
10661
- function ensureReplayFernignoreEntriesSync(outputDir) {
10662
- const fernignorePath = (0, import_path4.join)(outputDir, ".fernignore");
10663
- let content = "";
10664
- if ((0, import_fs2.existsSync)(fernignorePath)) {
10665
- content = (0, import_fs2.readFileSync)(fernignorePath, "utf-8");
10666
- }
10667
- const lines = content.split("\n");
10668
- const toAdd = REPLAY_FERNIGNORE_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
10669
- if (toAdd.length > 0) {
10670
- const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
10671
- (0, import_fs2.writeFileSync)(fernignorePath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
10672
- }
10673
- }
10674
- function ensureGitattributesEntriesSync(outputDir) {
10675
- const gitattributesPath = (0, import_path4.join)(outputDir, ".gitattributes");
10676
- let content = "";
10677
- if ((0, import_fs2.existsSync)(gitattributesPath)) {
10678
- content = (0, import_fs2.readFileSync)(gitattributesPath, "utf-8");
10679
- }
10680
- const lines = content.split("\n");
10681
- const toAdd = GITATTRIBUTES_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
10682
- if (toAdd.length > 0) {
10683
- const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
10684
- (0, import_fs2.writeFileSync)(gitattributesPath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
10685
- }
10686
- }
10687
-
10688
- // src/replay/replay-run.ts
10689
- async function replayRun(params) {
10690
- const {
10691
- outputDir,
10692
- cliVersion,
10693
- generatorVersions,
10694
- stageOnly = false,
10695
- generatorName,
10696
- skipApplication,
10697
- logger
10698
- } = params;
10699
- const lockfilePath = (0, import_path5.join)(outputDir, ".fern", "replay.lock");
10700
- if (!(0, import_fs3.existsSync)(lockfilePath)) {
10701
- return {
10702
- report: null,
10703
- fernignoreUpdated: false,
10704
- previousGenerationSha: null,
10705
- currentGenerationSha: null,
10706
- baseBranchHead: null
10707
- };
10708
- }
10709
- const baseBranchHead = gitRevParse(outputDir, "HEAD");
10710
- let previousGenerationSha = null;
10711
- let prevBaseBranchHead = null;
10712
- try {
10713
- const lockManager = new import_replay.LockfileManager(outputDir);
10714
- if (lockManager.exists()) {
10715
- const lock = lockManager.read();
10716
- previousGenerationSha = lock.current_generation;
10717
- const latestGen = lock.generations.find((g2) => g2.commit_sha === lock.current_generation);
10718
- prevBaseBranchHead = latestGen?.base_branch_head ?? null;
10719
- }
10720
- } catch {
10721
- }
10722
- if (previousGenerationSha != null) {
10723
- const sanitizedName = generatorName?.replace(/\//g, "--");
10724
- const namespacedTag = sanitizedName != null ? `fern-generation-base--${sanitizedName}` : null;
10725
- const tagSha = (namespacedTag != null ? gitRevParse(outputDir, namespacedTag) : null) ?? gitRevParse(outputDir, "fern-generation-base");
10726
- let shouldSync = false;
10727
- if (tagSha != null) {
10728
- const tagParent = gitRevParse(outputDir, `${tagSha}^`);
10729
- if (tagParent === prevBaseBranchHead || tagParent === previousGenerationSha) {
10730
- const tagDistance = gitDiffNameOnly(outputDir, tagSha, "HEAD").length;
10731
- const lockDistance = gitDiffNameOnly(
10732
- outputDir,
10733
- prevBaseBranchHead ?? previousGenerationSha,
10734
- "HEAD"
10735
- ).length;
10736
- shouldSync = tagDistance < lockDistance;
10737
- }
10738
- }
10739
- if (shouldSync && tagSha != null) {
10740
- if (!isTagMergedIntoHead(outputDir, tagSha)) {
10741
- logger?.warn(
10742
- `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).`
10743
- );
10744
- } else {
10745
- const syncService = new import_replay.ReplayService(outputDir, { enabled: true });
10746
- await syncService.syncFromDivergentMerge(tagSha, {
10747
- cliVersion,
10748
- generatorVersions,
10749
- baseBranchHead: baseBranchHead ?? void 0
10750
- });
10751
- try {
10752
- const freshLockManager = new import_replay.LockfileManager(outputDir);
10753
- if (freshLockManager.exists()) {
10754
- previousGenerationSha = freshLockManager.read().current_generation;
10755
- }
10756
- } catch {
10757
- }
10758
- }
10759
- }
10760
- }
10761
- const service = new import_replay.ReplayService(outputDir, {
10762
- enabled: true
10763
- });
10764
- let report;
10765
- try {
10766
- report = await service.runReplay({
10767
- cliVersion,
10768
- generatorVersions,
10769
- stageOnly,
10770
- skipApplication,
10771
- baseBranchHead: baseBranchHead ?? void 0
10772
- });
10773
- } catch (error) {
10774
- logger?.warn("Replay failed, continuing without replay: " + String(error));
10775
- return {
10776
- report: null,
10777
- fernignoreUpdated: false,
10778
- previousGenerationSha,
10779
- currentGenerationSha: null,
10780
- baseBranchHead
10781
- };
10782
- }
10783
- let currentGenerationSha = null;
10784
- let resolvedBaseBranchHead = baseBranchHead;
10785
- try {
10786
- const freshLockManager = new import_replay.LockfileManager(outputDir);
10787
- if (freshLockManager.exists()) {
10788
- const freshLock = freshLockManager.read();
10789
- currentGenerationSha = freshLock.current_generation;
10790
- const latestGen = freshLock.generations.find((g2) => g2.commit_sha === freshLock.current_generation);
10791
- if (latestGen?.base_branch_head) {
10792
- resolvedBaseBranchHead = latestGen.base_branch_head;
10793
- }
10794
- }
10795
- } catch {
10796
- }
10797
- const fernignoreUpdated = await ensureReplayFernignoreEntries(outputDir);
10798
- await ensureGitattributesEntries(outputDir);
10799
- return {
10800
- report,
10801
- fernignoreUpdated,
10802
- previousGenerationSha,
10803
- currentGenerationSha,
10804
- baseBranchHead: resolvedBaseBranchHead
10805
- };
10806
- }
10807
- function isTagMergedIntoHead(cwd2, tagSha) {
10808
- try {
10809
- (0, import_child_process3.execFileSync)("git", ["merge-base", "--is-ancestor", tagSha, "HEAD"], {
10810
- cwd: cwd2,
10811
- stdio: "pipe"
10812
- });
10813
- return true;
10814
- } catch {
10815
- }
10816
- const tagTree = gitRevParse(cwd2, `${tagSha}^{tree}`);
10817
- if (tagTree == null) {
10818
- return false;
10819
- }
10820
- try {
10821
- const trees = (0, import_child_process3.execFileSync)("git", ["log", "HEAD", "--format=%T", "--max-count=500"], {
10822
- cwd: cwd2,
10823
- encoding: "utf-8",
10824
- stdio: "pipe"
10825
- });
10826
- return trees.split("\n").some((t2) => t2.trim() === tagTree);
10827
- } catch {
10828
- return false;
10829
- }
10830
- }
10831
- function gitRevParse(cwd2, rev) {
10832
- try {
10833
- return (0, import_child_process3.execFileSync)("git", ["rev-parse", "--verify", rev], {
10834
- cwd: cwd2,
10835
- encoding: "utf-8",
10836
- stdio: "pipe"
10837
- }).trim();
10838
- } catch {
10839
- return null;
10840
- }
10841
- }
10842
- function gitDiffNameOnly(cwd2, from, to) {
10843
- try {
10844
- const output = (0, import_child_process3.execFileSync)("git", ["diff", "--name-only", from, to], {
10845
- cwd: cwd2,
10846
- encoding: "utf-8",
10847
- stdio: "pipe"
10848
- });
10849
- return output.trim().split("\n").filter(Boolean);
10850
- } catch {
10851
- return [];
10852
- }
10853
- }
10854
-
10855
12437
  // src/pipeline/steps/ReplayStep.ts
10856
12438
  var ReplayStep = class extends BaseStep {
10857
12439
  constructor(outputDir, logger, config, cliVersion, generatorVersions, generatorName) {
@@ -10866,8 +12448,23 @@ var ReplayStep = class extends BaseStep {
10866
12448
  generatorVersions;
10867
12449
  generatorName;
10868
12450
  name = "replay";
10869
- async execute(_context) {
10870
- 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({
10871
12468
  outputDir: this.outputDir,
10872
12469
  cliVersion: this.cliVersion,
10873
12470
  generatorVersions: this.generatorVersions,
@@ -10923,13 +12520,34 @@ var PostGenerationPipeline = class {
10923
12520
  constructor(config, logger = consolePipelineLogger) {
10924
12521
  this.config = config;
10925
12522
  this.logger = logger;
10926
- 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") {
10927
12526
  this.logger.warn(
10928
12527
  "Replay is not supported with GitHub push mode. Disabling replay to prevent force push to base branch."
10929
12528
  );
10930
- 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
+ );
10931
12546
  }
10932
- if (config.replay?.enabled) {
12547
+ if (autoVersionEnabled && config.autoVersion != null) {
12548
+ this.steps.push(new AutoVersionStep(config.outputDir, this.logger, config.autoVersion));
12549
+ }
12550
+ if (replayEnabled && config.replay != null) {
10933
12551
  this.steps.push(
10934
12552
  new ReplayStep(
10935
12553
  config.outputDir,
@@ -10951,8 +12569,8 @@ var PostGenerationPipeline = class {
10951
12569
  async run() {
10952
12570
  if (this.config.github?.enabled) {
10953
12571
  try {
10954
- (0, import_child_process4.execFileSync)("git", ["config", "user.name", FERN_BOT_NAME], { cwd: this.config.outputDir });
10955
- (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 });
10956
12574
  } catch {
10957
12575
  }
10958
12576
  }
@@ -10966,9 +12584,15 @@ var PostGenerationPipeline = class {
10966
12584
  for (const step of this.steps) {
10967
12585
  try {
10968
12586
  const stepResult = await step.execute(pipelineContext);
10969
- 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") {
10970
12591
  result.steps.replay = stepResult;
10971
12592
  pipelineContext.previousStepResults.replay = stepResult;
12593
+ } else if (step.name === "autoVersion") {
12594
+ result.steps.autoVersion = stepResult;
12595
+ pipelineContext.previousStepResults.autoVersion = stepResult;
10972
12596
  } else if (step.name === "fernignore") {
10973
12597
  result.steps.fernignore = stepResult;
10974
12598
  } else if (step.name === "github") {
@@ -10998,8 +12622,8 @@ function replayForget(params) {
10998
12622
 
10999
12623
  // src/replay/replay-init.ts
11000
12624
  var import_replay3 = require("@fern-api/replay");
11001
- var import_fs4 = require("fs");
11002
- var import_path6 = require("path");
12625
+ var import_fs6 = require("fs");
12626
+ var import_path8 = require("path");
11003
12627
  var import_tmp_promise2 = __toESM(require("tmp-promise"));
11004
12628
  async function replayInit(params) {
11005
12629
  const { githubRepo, token, dryRun } = params;
@@ -11025,10 +12649,10 @@ async function replayInit(params) {
11025
12649
  }
11026
12650
  ensureReplayFernignoreEntriesSync(repoPath);
11027
12651
  ensureGitattributesEntriesSync(repoPath);
11028
- const lockfilePath = (0, import_path6.join)(repoPath, ".fern", "replay.lock");
11029
- const lockfileContent = (0, import_fs4.existsSync)(lockfilePath) ? (0, import_fs4.readFileSync)(lockfilePath, "utf-8") : void 0;
11030
- const replayYmlPath = (0, import_path6.join)(repoPath, ".fern", "replay.yml");
11031
- 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;
11032
12656
  return {
11033
12657
  bootstrap: bootstrapResult,
11034
12658
  lockfileContent,
@@ -11156,9 +12780,66 @@ var import_replay5 = require("@fern-api/replay");
11156
12780
  function replayStatus(params) {
11157
12781
  return (0, import_replay5.status)(params.outputDir);
11158
12782
  }
12783
+
12784
+ // src/replay/replay-submit-init.ts
12785
+ async function submitReplayInit(params) {
12786
+ const { fiddleOrigin, fernToken, githubRepo, initResult } = params;
12787
+ const { owner, repo } = parseOwnerRepo(githubRepo);
12788
+ const response = await fetch(`${fiddleOrigin}/api/remote-gen/replay/init`, {
12789
+ method: "POST",
12790
+ headers: {
12791
+ "Content-Type": "application/json",
12792
+ Authorization: `Bearer ${fernToken}`
12793
+ },
12794
+ body: JSON.stringify({
12795
+ owner,
12796
+ repo,
12797
+ lockfileContents: initResult.lockfileContent,
12798
+ fernignoreEntries: initResult.fernignoreEntries,
12799
+ prBody: initResult.prBody
12800
+ })
12801
+ });
12802
+ if (!response.ok) {
12803
+ if (response.status === 404) {
12804
+ throw new ReplaySubmitError(
12805
+ "The Fern GitHub App is not installed on this repository. Install it at https://github.com/apps/fern-api to enable server-side PR creation.",
12806
+ response.status
12807
+ );
12808
+ }
12809
+ const body = await response.text();
12810
+ throw new ReplaySubmitError(`Failed to create PR via Fern: ${body}`, response.status);
12811
+ }
12812
+ const data = await response.json();
12813
+ if (!isSubmitResponse(data)) {
12814
+ throw new ReplaySubmitError("Unexpected response from Fern: missing prUrl field.");
12815
+ }
12816
+ return { prUrl: data.prUrl };
12817
+ }
12818
+ function parseOwnerRepo(githubRepo) {
12819
+ const cleaned = githubRepo.replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/^github\.com\//, "");
12820
+ const parts = cleaned.split("/");
12821
+ const owner = parts[parts.length - 2];
12822
+ const repo = parts[parts.length - 1];
12823
+ if (!owner || !repo) {
12824
+ throw new Error(`Could not parse owner/repo from: ${githubRepo}`);
12825
+ }
12826
+ return { owner, repo };
12827
+ }
12828
+ var ReplaySubmitError = class extends Error {
12829
+ statusCode;
12830
+ constructor(message, statusCode) {
12831
+ super(message);
12832
+ this.name = "ReplaySubmitError";
12833
+ this.statusCode = statusCode;
12834
+ }
12835
+ };
12836
+ function isSubmitResponse(data) {
12837
+ return typeof data === "object" && data != null && "prUrl" in data && typeof data.prUrl === "string";
12838
+ }
11159
12839
  // Annotate the CommonJS export names for ESM import in node:
11160
12840
  0 && (module.exports = {
11161
12841
  PostGenerationPipeline,
12842
+ ReplaySubmitError,
11162
12843
  consolePipelineLogger,
11163
12844
  formatBootstrapSummary,
11164
12845
  formatReplayPrBody,
@@ -11170,9 +12851,11 @@ function replayStatus(params) {
11170
12851
  githubPush,
11171
12852
  githubRelease,
11172
12853
  logReplaySummary,
12854
+ parseOwnerRepo,
11173
12855
  replayForget,
11174
12856
  replayInit,
11175
12857
  replayResolve,
11176
12858
  replayRun,
11177
- replayStatus
12859
+ replayStatus,
12860
+ submitReplayInit
11178
12861
  });