@fern-api/generator-cli 0.6.10 → 0.6.11

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 +15 -19
  2. package/dist/api.js +116 -354
  3. package/dist/cli.js +274 -323
  4. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -19568,7 +19568,7 @@ var init_diff3 = __esm({
19568
19568
  }
19569
19569
  });
19570
19570
 
19571
- // ../../node_modules/.pnpm/@fern-api+replay@0.6.2/node_modules/@fern-api/replay/dist/index.js
19571
+ // ../../node_modules/.pnpm/@fern-api+replay@0.7.0/node_modules/@fern-api/replay/dist/index.js
19572
19572
  var dist_exports = {};
19573
19573
  __export(dist_exports, {
19574
19574
  FERN_BOT_EMAIL: () => FERN_BOT_EMAIL2,
@@ -19891,17 +19891,82 @@ async function resolve7(outputDir, options) {
19891
19891
  return { success: false, reason: "no-patches" };
19892
19892
  }
19893
19893
  const git = new GitClient(outputDir);
19894
+ const unresolvedPatches = lockManager.getUnresolvedPatches();
19895
+ const resolvingPatches = lockManager.getResolvingPatches();
19896
+ if (unresolvedPatches.length > 0) {
19897
+ const applicator = new ReplayApplicator(git, lockManager, outputDir);
19898
+ await applicator.applyPatches(unresolvedPatches);
19899
+ const markerFiles = await findConflictMarkerFiles(git);
19900
+ if (markerFiles.length > 0) {
19901
+ for (const patch of unresolvedPatches) {
19902
+ lockManager.updatePatch(patch.id, { status: "resolving" });
19903
+ }
19904
+ lockManager.save();
19905
+ return {
19906
+ success: false,
19907
+ reason: "conflicts-applied",
19908
+ unresolvedFiles: markerFiles,
19909
+ phase: "applied",
19910
+ patchesApplied: unresolvedPatches.length
19911
+ };
19912
+ }
19913
+ }
19894
19914
  if (options?.checkMarkers !== false) {
19895
- const markerFiles = await git.exec(["grep", "-l", "<<<<<<<", "--", "."]).catch(() => "");
19896
- if (markerFiles.trim()) {
19897
- const files = markerFiles.trim().split("\n").filter(Boolean);
19898
- return { success: false, reason: "unresolved-conflicts", unresolvedFiles: files };
19915
+ const currentMarkerFiles = await findConflictMarkerFiles(git);
19916
+ if (currentMarkerFiles.length > 0) {
19917
+ return { success: false, reason: "unresolved-conflicts", unresolvedFiles: currentMarkerFiles };
19918
+ }
19919
+ }
19920
+ const patchesToCommit = [...resolvingPatches, ...unresolvedPatches];
19921
+ if (patchesToCommit.length > 0) {
19922
+ const currentGen = lock.current_generation;
19923
+ const detector = new ReplayDetector(git, lockManager, outputDir);
19924
+ let patchesResolved = 0;
19925
+ for (const patch of patchesToCommit) {
19926
+ const diff = await git.exec(["diff", currentGen, "--", ...patch.files]).catch(() => null);
19927
+ if (!diff || !diff.trim()) {
19928
+ lockManager.removePatch(patch.id);
19929
+ continue;
19930
+ }
19931
+ const newContentHash = detector.computeContentHash(diff);
19932
+ const changedFiles = await getChangedFiles(git, currentGen, patch.files);
19933
+ lockManager.markPatchResolved(patch.id, {
19934
+ patch_content: diff,
19935
+ content_hash: newContentHash,
19936
+ base_generation: currentGen,
19937
+ files: changedFiles
19938
+ });
19939
+ patchesResolved++;
19899
19940
  }
19941
+ lockManager.save();
19942
+ const committer2 = new ReplayCommitter(git, outputDir);
19943
+ await committer2.stageAll();
19944
+ const commitSha2 = await committer2.commitReplay(
19945
+ lock.patches.length,
19946
+ lock.patches,
19947
+ "[fern-replay] Resolved conflicts"
19948
+ );
19949
+ return {
19950
+ success: true,
19951
+ commitSha: commitSha2,
19952
+ phase: "committed",
19953
+ patchesResolved
19954
+ };
19900
19955
  }
19901
19956
  const committer = new ReplayCommitter(git, outputDir);
19902
19957
  await committer.stageAll();
19903
19958
  const commitSha = await committer.commitReplay(lock.patches.length, lock.patches);
19904
- return { success: true, commitSha };
19959
+ return { success: true, commitSha, phase: "committed" };
19960
+ }
19961
+ async function findConflictMarkerFiles(git) {
19962
+ const output = await git.exec(["grep", "-l", "<<<<<<<", "--", "."]).catch(() => "");
19963
+ return output.trim() ? output.trim().split("\n").filter(Boolean) : [];
19964
+ }
19965
+ async function getChangedFiles(git, currentGen, files) {
19966
+ const filesOutput = await git.exec(["diff", "--name-only", currentGen, "--", ...files]).catch(() => null);
19967
+ if (!filesOutput || !filesOutput.trim()) return files;
19968
+ const changed = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !f.startsWith(".fern/"));
19969
+ return changed.length > 0 ? changed : files;
19905
19970
  }
19906
19971
  function status(outputDir) {
19907
19972
  const lockManager = new LockfileManager(outputDir);
@@ -19927,7 +19992,7 @@ function status(outputDir) {
19927
19992
  }
19928
19993
  var import_fs6, import_path8, import_yaml, import_crypto, import_promises4, import_os, import_path9, import_fs7, import_path10, import_crypto2, import_fs8, import_path11, import_yaml2, import_crypto3, import_fs9, import_path12, import_fs10, __defProp3, __getOwnPropNames3, __esm3, __export3, GitClient_exports, GitClient, init_GitClient, FERN_BOT_NAME2, FERN_BOT_EMAIL2, FERN_BOT_LOGIN2, FERN_SUPPORT_NAMES, LOCKFILE_HEADER, LockfileManager, INFRASTRUCTURE_FILES, ReplayDetector, BINARY_EXTENSIONS, ReplayApplicator, ReplayCommitter, ReplayService, FernignoreMigrator, REPLAY_FERNIGNORE_ENTRIES;
19929
19994
  var init_dist = __esm({
19930
- "../../node_modules/.pnpm/@fern-api+replay@0.6.2/node_modules/@fern-api/replay/dist/index.js"() {
19995
+ "../../node_modules/.pnpm/@fern-api+replay@0.7.0/node_modules/@fern-api/replay/dist/index.js"() {
19931
19996
  "use strict";
19932
19997
  init_esm4();
19933
19998
  import_fs6 = require("fs");
@@ -20158,6 +20223,26 @@ var init_dist = __esm({
20158
20223
  this.ensureLoaded();
20159
20224
  this.lock.patches = [];
20160
20225
  }
20226
+ getUnresolvedPatches() {
20227
+ this.ensureLoaded();
20228
+ return this.lock.patches.filter((p) => p.status === "unresolved");
20229
+ }
20230
+ getResolvingPatches() {
20231
+ this.ensureLoaded();
20232
+ return this.lock.patches.filter((p) => p.status === "resolving");
20233
+ }
20234
+ markPatchUnresolved(patchId) {
20235
+ this.updatePatch(patchId, { status: "unresolved" });
20236
+ }
20237
+ markPatchResolved(patchId, updates) {
20238
+ this.ensureLoaded();
20239
+ const patch = this.lock.patches.find((p) => p.id === patchId);
20240
+ if (!patch) {
20241
+ throw new Error(`Patch not found: ${patchId}`);
20242
+ }
20243
+ delete patch.status;
20244
+ Object.assign(patch, updates);
20245
+ }
20161
20246
  getPatches() {
20162
20247
  this.ensureLoaded();
20163
20248
  return this.lock.patches;
@@ -20801,12 +20886,12 @@ CLI Version: ${options.cliVersion}`;
20801
20886
  await this.git.exec(["commit", "-m", fullMessage]);
20802
20887
  return (await this.git.exec(["rev-parse", "HEAD"])).trim();
20803
20888
  }
20804
- async commitReplay(_patchCount, patches) {
20889
+ async commitReplay(_patchCount, patches, message) {
20805
20890
  await this.stageAll();
20806
20891
  if (!await this.hasStagedChanges()) {
20807
20892
  return (await this.git.exec(["rev-parse", "HEAD"])).trim();
20808
20893
  }
20809
- let fullMessage = `[fern-replay] Applied customizations`;
20894
+ let fullMessage = message ?? `[fern-replay] Applied customizations`;
20810
20895
  if (patches && patches.length > 0) {
20811
20896
  fullMessage += "\n\nPatches replayed:";
20812
20897
  for (const patch of patches) {
@@ -20896,13 +20981,27 @@ CLI Version: ${options.cliVersion}`;
20896
20981
  this.lockManager.initializeInMemory(record2);
20897
20982
  } else {
20898
20983
  this.lockManager.read();
20984
+ const unresolvedPatches = [
20985
+ ...this.lockManager.getUnresolvedPatches(),
20986
+ ...this.lockManager.getResolvingPatches()
20987
+ ];
20899
20988
  this.lockManager.addGeneration(record2);
20900
20989
  this.lockManager.clearPatches();
20990
+ for (const patch of unresolvedPatches) {
20991
+ this.lockManager.addPatch(patch);
20992
+ }
20901
20993
  }
20902
20994
  this.lockManager.save();
20903
20995
  try {
20904
20996
  const redetectedPatches = await this.detector.detectNewPatches();
20905
20997
  if (redetectedPatches.length > 0) {
20998
+ const redetectedFiles = new Set(redetectedPatches.flatMap((p) => p.files));
20999
+ const currentPatches = this.lockManager.getPatches();
21000
+ for (const patch of currentPatches) {
21001
+ if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
21002
+ this.lockManager.removePatch(patch.id);
21003
+ }
21004
+ }
20906
21005
  for (const patch of redetectedPatches) {
20907
21006
  this.lockManager.addPatch(patch);
20908
21007
  }
@@ -21010,6 +21109,12 @@ CLI Version: ${options.cliVersion}`;
21010
21109
  let results = [];
21011
21110
  if (newPatches.length > 0) {
21012
21111
  results = await this.applicator.applyPatches(newPatches);
21112
+ this.revertConflictingFiles(results);
21113
+ for (const result of results) {
21114
+ if (result.status === "conflict") {
21115
+ result.patch.status = "unresolved";
21116
+ }
21117
+ }
21013
21118
  }
21014
21119
  const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
21015
21120
  for (const patch of newPatches) {
@@ -21020,10 +21125,8 @@ CLI Version: ${options.cliVersion}`;
21020
21125
  this.lockManager.save();
21021
21126
  if (newPatches.length > 0) {
21022
21127
  if (!options?.stageOnly) {
21023
- const appliedCount = results.filter((r) => r.status === "applied" || r.status === "conflict").length;
21024
- if (appliedCount > 0) {
21025
- await this.committer.commitReplay(appliedCount, newPatches);
21026
- }
21128
+ const appliedCount = results.filter((r) => r.status === "applied").length;
21129
+ await this.committer.commitReplay(appliedCount, newPatches);
21027
21130
  } else {
21028
21131
  await this.committer.stageAll();
21029
21132
  }
@@ -21071,20 +21174,32 @@ CLI Version: ${options.cliVersion}`;
21071
21174
  const genRecord = await this.committer.createGenerationRecord(commitOpts);
21072
21175
  this.lockManager.addGeneration(genRecord);
21073
21176
  const results = await this.applicator.applyPatches(allPatches);
21177
+ this.revertConflictingFiles(results);
21178
+ for (const result of results) {
21179
+ if (result.status === "conflict") {
21180
+ result.patch.status = "unresolved";
21181
+ }
21182
+ }
21074
21183
  const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
21075
21184
  for (const patch of newPatches) {
21076
21185
  if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
21077
21186
  this.lockManager.addPatch(patch);
21078
21187
  }
21079
21188
  }
21189
+ for (const result of results) {
21190
+ if (result.status === "conflict") {
21191
+ try {
21192
+ this.lockManager.markPatchUnresolved(result.patch.id);
21193
+ } catch {
21194
+ }
21195
+ }
21196
+ }
21080
21197
  this.lockManager.save();
21081
21198
  if (options?.stageOnly) {
21082
21199
  await this.committer.stageAll();
21083
21200
  } else {
21084
- const appliedCount = results.filter((r) => r.status === "applied" || r.status === "conflict").length;
21085
- if (appliedCount > 0) {
21086
- await this.committer.commitReplay(appliedCount, allPatches);
21087
- }
21201
+ const appliedCount = results.filter((r) => r.status === "applied").length;
21202
+ await this.committer.commitReplay(appliedCount, allPatches);
21088
21203
  }
21089
21204
  return this.buildReport(
21090
21205
  "normal-regeneration",
@@ -21232,6 +21347,10 @@ CLI Version: ${options.cliVersion}`;
21232
21347
  let conflictAbsorbed = 0;
21233
21348
  let contentRefreshed = 0;
21234
21349
  for (const patch of patches) {
21350
+ if (patch.status != null) {
21351
+ delete patch.status;
21352
+ continue;
21353
+ }
21235
21354
  if (patch.base_generation === currentGen) {
21236
21355
  try {
21237
21356
  const diff = await this.git.exec(["diff", currentGen, "HEAD", "--", ...patch.files]).catch(() => null);
@@ -21282,6 +21401,55 @@ CLI Version: ${options.cliVersion}`;
21282
21401
  }
21283
21402
  return { conflictResolved, conflictAbsorbed, contentRefreshed };
21284
21403
  }
21404
+ /**
21405
+ * Strip conflict markers from file content, keeping the OURS (Generated) side.
21406
+ * Preserves clean patches' non-conflicting changes on shared files.
21407
+ */
21408
+ stripConflictMarkers(content) {
21409
+ const lines = content.split("\n");
21410
+ const result = [];
21411
+ let inConflict = false;
21412
+ let inOurs = false;
21413
+ for (const line of lines) {
21414
+ if (line.startsWith("<<<<<<< ")) {
21415
+ inConflict = true;
21416
+ inOurs = true;
21417
+ continue;
21418
+ }
21419
+ if (inConflict && line === "=======") {
21420
+ inOurs = false;
21421
+ continue;
21422
+ }
21423
+ if (inConflict && line.startsWith(">>>>>>> ")) {
21424
+ inConflict = false;
21425
+ inOurs = false;
21426
+ continue;
21427
+ }
21428
+ if (!inConflict || inOurs) {
21429
+ result.push(line);
21430
+ }
21431
+ }
21432
+ return result.join("\n");
21433
+ }
21434
+ /**
21435
+ * After applyPatches(), strip conflict markers from conflicting files
21436
+ * so only clean content is committed. Keeps the Generated (OURS) side.
21437
+ */
21438
+ revertConflictingFiles(results) {
21439
+ for (const result of results) {
21440
+ if (result.status !== "conflict" || !result.fileResults) continue;
21441
+ for (const fileResult of result.fileResults) {
21442
+ if (fileResult.status !== "conflict") continue;
21443
+ const filePath = (0, import_path10.join)(this.outputDir, fileResult.file);
21444
+ try {
21445
+ const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
21446
+ const stripped = this.stripConflictMarkers(content);
21447
+ (0, import_fs7.writeFileSync)(filePath, stripped);
21448
+ } catch {
21449
+ }
21450
+ }
21451
+ }
21452
+ }
21285
21453
  readFernignorePatterns() {
21286
21454
  const fernignorePath = (0, import_path10.join)(this.outputDir, ".fernignore");
21287
21455
  if (!(0, import_fs7.existsSync)(fernignorePath)) return [];
@@ -21316,6 +21484,12 @@ CLI Version: ${options.cliVersion}`;
21316
21484
  patchesRefreshed: preRebaseCounts && preRebaseCounts.contentRefreshed > 0 ? preRebaseCounts.contentRefreshed : void 0,
21317
21485
  conflicts: conflictResults.flatMap((r) => r.fileResults?.filter((f) => f.status === "conflict") ?? []),
21318
21486
  conflictDetails: conflictDetails.length > 0 ? conflictDetails : void 0,
21487
+ unresolvedPatches: conflictResults.length > 0 ? conflictResults.map((r) => ({
21488
+ patchId: r.patch.id,
21489
+ patchMessage: r.patch.original_message,
21490
+ files: r.patch.files,
21491
+ conflictDetails: r.fileResults?.filter((f) => f.status === "conflict") ?? []
21492
+ })) : void 0,
21319
21493
  wouldApply: options?.dryRun ? patches : void 0,
21320
21494
  warnings: warnings && warnings.length > 0 ? warnings : void 0
21321
21495
  };
@@ -27227,9 +27401,6 @@ async function githubRelease(params) {
27227
27401
  await github.release();
27228
27402
  }
27229
27403
 
27230
- // src/api/generate-readme.ts
27231
- var import_node_stream = require("stream");
27232
-
27233
27404
  // ../../node_modules/.pnpm/es-toolkit@1.44.0/node_modules/es-toolkit/dist/predicate/isLength.mjs
27234
27405
  function isLength(value) {
27235
27406
  return Number.isSafeInteger(value) && value >= 0;
@@ -30048,16 +30219,25 @@ var ReadmeGenerator = class {
30048
30219
  this.apiName = this.readmeConfig.apiName ?? this.organizationPascalCase;
30049
30220
  }
30050
30221
  async generateReadme({ output }) {
30222
+ const writer = new StreamWriter(output);
30223
+ await this.writeReadmeContent({ writer });
30224
+ await writer.end();
30225
+ }
30226
+ async generateReadmeToString() {
30227
+ const writer = new StringWriter();
30228
+ await this.writeReadmeContent({ writer });
30229
+ await writer.end();
30230
+ return writer.toString();
30231
+ }
30232
+ async writeReadmeContent({ writer }) {
30051
30233
  const blocks = await this.generateBlocks();
30052
30234
  const mergedBlocks = await this.mergeBlocks({ blocks });
30053
- const writer = new StreamWriter(output);
30054
30235
  await this.writeHeader({ writer });
30055
30236
  await this.writeTableOfContents({ writer, blocks: mergedBlocks });
30056
30237
  await this.writeBlocks({
30057
30238
  writer,
30058
30239
  blocks: mergedBlocks
30059
30240
  });
30060
- await writer.end();
30061
30241
  }
30062
30242
  async generateBlocks() {
30063
30243
  const blocks = [];
@@ -30927,9 +31107,6 @@ async function generateReadmeToStream(params) {
30927
31107
  });
30928
31108
  }
30929
31109
 
30930
- // src/api/generate-reference.ts
30931
- var import_node_stream2 = require("stream");
30932
-
30933
31110
  // src/reference/ReferenceGenerator.ts
30934
31111
  var ReferenceGenerator = class {
30935
31112
  referenceConfig;
@@ -30940,6 +31117,16 @@ var ReferenceGenerator = class {
30940
31117
  }
30941
31118
  async generate({ output }) {
30942
31119
  const writer = new StreamWriter(output);
31120
+ await this.writeReferenceContent({ writer });
31121
+ await writer.end();
31122
+ }
31123
+ async generateToString() {
31124
+ const writer = new StringWriter();
31125
+ await this.writeReferenceContent({ writer });
31126
+ await writer.end();
31127
+ return writer.toString();
31128
+ }
31129
+ async writeReferenceContent({ writer }) {
30943
31130
  await writer.writeLine("# Reference");
30944
31131
  if (this.referenceConfig.rootSection != null) {
30945
31132
  await this.writeRootSection({
@@ -30950,7 +31137,6 @@ var ReferenceGenerator = class {
30950
31137
  for (const section of this.referenceConfig.sections) {
30951
31138
  await this.writeSection({ section, writer });
30952
31139
  }
30953
- await writer.end();
30954
31140
  }
30955
31141
  async writeRootSection({
30956
31142
  section,
@@ -31090,41 +31276,19 @@ var import_path7 = require("path");
31090
31276
 
31091
31277
  // src/pipeline/github/createReplayBranch.ts
31092
31278
  async function createReplayBranch(repository, branchName, commitMessage, replayConflictInfo, logger) {
31093
- if (replayConflictInfo?.hasConflicts) {
31094
- const parentSha = replayConflictInfo.previousGenerationSha;
31095
- const lockfileRestoreSha = replayConflictInfo.baseBranchHead ?? replayConflictInfo.previousGenerationSha;
31096
- logger.debug(`Creating divergent PR branch with generation tree (parent: ${parentSha.substring(0, 7)})`);
31097
- const genTreeHash = await repository.getCommitTreeHash(replayConflictInfo.currentGenerationSha);
31098
- const syntheticCommitSha = await repository.commitTree(
31099
- genTreeHash,
31100
- parentSha,
31101
- `[fern-generated] ${commitMessage ?? "Update SDK"}`
31102
- );
31103
- await repository.createBranchFromCommit(branchName, syntheticCommitSha);
31104
- try {
31105
- await repository.restoreFilesFromCommit(lockfileRestoreSha, ".fern/replay.lock");
31106
- await repository.commitAllChanges(`[fern-generated] ${commitMessage ?? "Update SDK"}`);
31107
- } catch (error) {
31108
- logger.debug(
31109
- `Could not restore lockfile from base branch: ${error instanceof Error ? error.message : String(error)}`
31110
- );
31111
- }
31112
- return syntheticCommitSha;
31113
- } else if (replayConflictInfo != null) {
31114
- const parentSha = replayConflictInfo.previousGenerationSha;
31115
- logger.debug(`Creating linear PR branch from HEAD (no conflicts)`);
31116
- await repository.createBranchFromHead(branchName);
31117
- const genTreeHash = await repository.getCommitTreeHash(replayConflictInfo.currentGenerationSha);
31118
- const tagCommitSha = await repository.commitTree(
31119
- genTreeHash,
31120
- parentSha,
31121
- `[fern-generated] ${commitMessage ?? "Update SDK"}`
31122
- );
31123
- return tagCommitSha;
31124
- } else {
31279
+ if (replayConflictInfo == null) {
31125
31280
  await repository.createBranchFromHead(branchName);
31126
31281
  return void 0;
31127
31282
  }
31283
+ const parentSha = replayConflictInfo.previousGenerationSha;
31284
+ await repository.createBranchFromHead(branchName);
31285
+ const genTreeHash = await repository.getCommitTreeHash(replayConflictInfo.currentGenerationSha);
31286
+ const tagCommitSha = await repository.commitTree(
31287
+ genTreeHash,
31288
+ parentSha,
31289
+ `[fern-generated] ${commitMessage ?? "Update SDK"}`
31290
+ );
31291
+ return tagCommitSha;
31128
31292
  }
31129
31293
 
31130
31294
  // src/pipeline/github/findExistingUpdatablePR.ts
@@ -31219,11 +31383,11 @@ function formatConflictReason(reason) {
31219
31383
  case "same-line-edit":
31220
31384
  return "The new generation changed the same lines you edited";
31221
31385
  case "new-file-both":
31222
- return "You and the generator both created this file";
31386
+ return "Both the generator and your customization created this file";
31223
31387
  case "base-generation-mismatch":
31224
- return "The generated code changed significantly around your edit";
31388
+ return "This customization was from a previous generation";
31225
31389
  case "patch-apply-failed":
31226
- return "Your change could not be applied to the new generated code";
31390
+ return "The customization could not be applied to the new code";
31227
31391
  default:
31228
31392
  return "Your edit overlaps with changes in the new generation";
31229
31393
  }
@@ -31232,7 +31396,7 @@ function patchDescription(detail) {
31232
31396
  if (detail.patchMessage && detail.patchMessage !== "update") {
31233
31397
  return detail.patchMessage;
31234
31398
  }
31235
- const firstFile = detail.files[0]?.file;
31399
+ const firstFile = detail.files[0];
31236
31400
  return firstFile != null ? `changes in ${firstFile}` : "customization";
31237
31401
  }
31238
31402
  function formatReplayPrBody(result, options) {
@@ -31241,70 +31405,66 @@ function formatReplayPrBody(result, options) {
31241
31405
  }
31242
31406
  const applied = result.patchesApplied ?? 0;
31243
31407
  const absorbed = result.patchesAbsorbed ?? 0;
31244
- const conflicts2 = result.patchesWithConflicts ?? 0;
31408
+ const unresolvedPatches = result.unresolvedPatches ?? [];
31409
+ const hasUnresolved = unresolvedPatches.length > 0;
31245
31410
  const preserved = applied - absorbed;
31246
- if (preserved === 0 && conflicts2 === 0) {
31411
+ if (preserved === 0 && !hasUnresolved) {
31247
31412
  return void 0;
31248
31413
  }
31249
31414
  const parts = [];
31250
- if (preserved > 0 && conflicts2 === 0) {
31415
+ if (preserved > 0 && !hasUnresolved) {
31251
31416
  parts.push(`\u2705 Customizations automatically preserved in this update.`);
31252
31417
  } else if (preserved > 0) {
31418
+ const totalFiles = unresolvedPatches.reduce((sum, patch) => sum + patch.conflictDetails.length, 0);
31253
31419
  parts.push(
31254
- `\u2705 Customizations automatically preserved, but ${plural(conflicts2, "file")} ${conflicts2 === 1 ? "needs" : "need"} your attention.`
31420
+ `\u2705 Customizations automatically preserved, but ${plural(totalFiles, "file")} ${totalFiles === 1 ? "needs" : "need"} your attention.`
31255
31421
  );
31256
31422
  }
31257
- if (conflicts2 > 0) {
31258
- const allFiles = (result.conflictDetails ?? []).flatMap((d) => d.files);
31259
- const totalFiles = allFiles.length;
31423
+ if (hasUnresolved) {
31424
+ const totalFiles = unresolvedPatches.reduce((sum, patch) => sum + patch.conflictDetails.length, 0);
31260
31425
  parts.push(`
31261
- ### \u26A0\uFE0F Action required: ${plural(totalFiles, "file")} with customization conflicts
31426
+ ### Action required: ${plural(totalFiles, "file")} with unresolved customization conflicts
31262
31427
  `);
31263
31428
  parts.push(
31264
- `You previously customized ${totalFiles === 1 ? "a file" : "files"} in this SDK. This generation changed the same code, so your ${totalFiles === 1 ? "edit" : "edits"} couldn't be applied automatically.
31265
- `
31266
- );
31267
- parts.push(
31268
- `> \u26A0\uFE0F **Label guide:** In GitHub's conflict editor, \`<<<<<<< HEAD\` ("Accept current") = new generated code. \`>>>>>>> main\` ("Accept incoming") = your customization.
31429
+ `The new generation changed code you previously customized. Non-conflicting customizations have been applied automatically. The following files need manual resolution:
31269
31430
  `
31270
31431
  );
31271
31432
  parts.push(`| File | Your customization | Why it conflicted |`);
31272
31433
  parts.push(`|------|-------------------|-------------------|`);
31273
- for (const detail of result.conflictDetails ?? []) {
31274
- const description = patchDescription(detail);
31275
- for (const f of detail.files) {
31276
- parts.push(`| \`${f.file}\` | ${description} | ${formatConflictReason(f.conflictReason)} |`);
31434
+ for (const patch of unresolvedPatches) {
31435
+ const description = patchDescription(patch);
31436
+ for (const file of patch.conflictDetails) {
31437
+ parts.push(`| \`${file.file}\` | ${description} | ${formatConflictReason(file.conflictReason)} |`);
31277
31438
  }
31278
31439
  }
31279
31440
  const branch = options?.branchName ?? "<branch-name>";
31441
+ const repoUri = options?.repoUri;
31442
+ const repoName = repoUri?.split("/").pop() ?? "<repo>";
31280
31443
  parts.push(`
31281
- #### How to fix
31282
- `);
31283
- parts.push(`**Option A: Resolve in GitHub (recommended for simple conflicts)**
31284
- `);
31285
- parts.push(`1. Click the **Resolve conflicts** button below`);
31286
- parts.push(`2. For each file, choose which code to keep:`);
31287
- parts.push(` - \`<<<<<<< HEAD\` / "Accept current changes" = **new generated code**`);
31288
- parts.push(` - \`>>>>>>> main\` / "Accept incoming changes" = **your customization**`);
31289
- parts.push(` - Or manually combine both, then delete the conflict markers`);
31290
- parts.push(`3. Click **Mark as resolved** for each file, then **Commit merge**
31444
+ #### How to resolve
31291
31445
  `);
31292
- parts.push(`**Option B: Resolve locally**
31293
- `);
31294
- parts.push(`1. Fetch and check out this branch:`);
31295
- parts.push(` \`\`\`sh`);
31296
- parts.push(` git fetch origin && git checkout ${branch}`);
31297
- parts.push(` \`\`\``);
31298
- parts.push(`2. Merge the base branch and resolve conflicts in your editor:`);
31446
+ parts.push(`1. Check out this branch:`);
31299
31447
  parts.push(` \`\`\`sh`);
31300
- parts.push(` git merge origin/main`);
31448
+ parts.push(` git fetch origin && git checkout -b ${branch} origin/${branch}`);
31301
31449
  parts.push(` \`\`\``);
31302
- parts.push(`3. Commit and push:`);
31303
- parts.push(` \`\`\`sh`);
31304
- parts.push(` git add -A && git commit -m "resolve conflicts" && git push`);
31305
- parts.push(` \`\`\`
31450
+ if (repoUri != null) {
31451
+ parts.push(` Or if you don't have the repo cloned:`);
31452
+ parts.push(` \`\`\`sh`);
31453
+ parts.push(
31454
+ ` git clone https://github.com/${repoUri}.git && cd ${repoName} && git checkout -b ${branch} origin/${branch}`
31455
+ );
31456
+ parts.push(` \`\`\``);
31457
+ }
31458
+ parts.push(`2. Run: \`fern replay resolve\``);
31459
+ parts.push(`3. Open the conflicting files in your editor \u2014 you'll see standard merge conflict markers`);
31460
+ parts.push(`4. Resolve using your editor's merge tools (VS Code, IntelliJ, etc.)`);
31461
+ parts.push(`5. Run: \`fern replay resolve\` again to finalize`);
31462
+ parts.push(`6. Push your changes
31306
31463
  `);
31307
- parts.push(`Your resolved changes will be remembered on future SDK generations.`);
31464
+ parts.push(`Your resolved customizations will be remembered on future SDK generations.`);
31465
+ parts.push(
31466
+ `If you merge this PR without resolving, your unresolved customizations will conflict again on the next generation.`
31467
+ );
31308
31468
  }
31309
31469
  return parts.join("\n");
31310
31470
  }
@@ -31440,16 +31600,9 @@ var GithubStep = class extends BaseStep {
31440
31600
  }
31441
31601
  }
31442
31602
  }
31443
- if (!this.config.previewMode) {
31444
- const headSha = await repository.getHeadSha();
31445
- await this.postReplayConflictStatus(octokit, owner, repo, headSha, replayConflictInfo, replayResult);
31446
- if (isUpdatingExistingPR && existingPR != null) {
31447
- await this.togglePrDraftState(octokit, existingPR, replayConflictInfo);
31448
- }
31449
- }
31450
31603
  const finalCommitMessage = this.config.commitMessage ?? "SDK Generation";
31451
31604
  const { prTitle, prBody } = parseCommitMessageForPR(finalCommitMessage);
31452
- const replaySection = formatReplayPrBody(replayResult, { branchName: prBranch });
31605
+ const replaySection = formatReplayPrBody(replayResult, { branchName: prBranch, repoUri: this.config.uri });
31453
31606
  const enrichedBody = replaySection != null ? prBody + "\n\n---\n\n" + replaySection : prBody;
31454
31607
  if (isUpdatingExistingPR && existingPR != null) {
31455
31608
  this.logger.info(`Updated existing pull request: ${existingPR.htmlUrl}`);
@@ -31471,7 +31624,6 @@ var GithubStep = class extends BaseStep {
31471
31624
  }
31472
31625
  } else {
31473
31626
  const head = `${owner}:${prBranch}`;
31474
- const hasConflicts = replayConflictInfo?.hasConflicts === true;
31475
31627
  try {
31476
31628
  const { data: pullRequest } = await octokit.pulls.create({
31477
31629
  owner,
@@ -31479,8 +31631,7 @@ var GithubStep = class extends BaseStep {
31479
31631
  title: prTitle,
31480
31632
  body: enrichedBody,
31481
31633
  head,
31482
- base: baseBranch,
31483
- draft: hasConflicts
31634
+ base: baseBranch
31484
31635
  });
31485
31636
  this.logger.info(`Created pull request: ${pullRequest.html_url}`);
31486
31637
  result.prUrl = pullRequest.html_url;
@@ -31494,17 +31645,6 @@ var GithubStep = class extends BaseStep {
31494
31645
  }
31495
31646
  }
31496
31647
  }
31497
- if (!this.config.previewMode && result.prNumber != null && replayConflictInfo?.hasConflicts === true && replayResult != null) {
31498
- await this.postWebEditorFallbackComment(
31499
- octokit,
31500
- owner,
31501
- repo,
31502
- result.prNumber,
31503
- prBranch,
31504
- baseBranch,
31505
- replayResult
31506
- );
31507
- }
31508
31648
  return result;
31509
31649
  }
31510
31650
  async executePushMode(repository) {
@@ -31550,198 +31690,17 @@ var GithubStep = class extends BaseStep {
31550
31690
  if (replayResult == null) {
31551
31691
  return void 0;
31552
31692
  }
31553
- const hasConflicts = (replayResult.patchesWithConflicts ?? 0) > 0;
31554
31693
  const previousGenerationSha = replayResult.previousGenerationSha;
31555
31694
  const currentGenerationSha = replayResult.currentGenerationSha;
31556
31695
  if (previousGenerationSha != null && currentGenerationSha != null) {
31557
31696
  return {
31558
31697
  previousGenerationSha,
31559
- currentGenerationSha,
31560
- hasConflicts,
31561
- baseBranchHead: replayResult.baseBranchHead
31698
+ currentGenerationSha
31562
31699
  };
31563
31700
  }
31564
31701
  return void 0;
31565
31702
  }
31566
- async togglePrDraftState(octokit, existingPR, replayConflictInfo) {
31567
- const hasConflicts = replayConflictInfo?.hasConflicts === true;
31568
- if (hasConflicts && !existingPR.isDraft) {
31569
- await this.convertPrToDraft(octokit, existingPR.nodeId, existingPR.number);
31570
- } else if (!hasConflicts && existingPR.isDraft) {
31571
- await this.markPrReady(octokit, existingPR.nodeId, existingPR.number);
31572
- }
31573
- }
31574
- async convertPrToDraft(octokit, nodeId, prNumber) {
31575
- try {
31576
- await octokit.graphql(
31577
- `mutation($id: ID!) {
31578
- convertPullRequestToDraft(input: {pullRequestId: $id}) {
31579
- pullRequest { isDraft }
31580
- }
31581
- }`,
31582
- { id: nodeId }
31583
- );
31584
- this.logger.info(`Converted PR #${prNumber} to draft due to replay conflicts`);
31585
- } catch (error) {
31586
- this.logger.debug(
31587
- `Could not convert PR to draft: ${error instanceof Error ? error.message : String(error)}`
31588
- );
31589
- }
31590
- }
31591
- async markPrReady(octokit, nodeId, prNumber) {
31592
- try {
31593
- await octokit.graphql(
31594
- `mutation($id: ID!) {
31595
- markPullRequestReadyForReview(input: {pullRequestId: $id}) {
31596
- pullRequest { isDraft }
31597
- }
31598
- }`,
31599
- { id: nodeId }
31600
- );
31601
- this.logger.info(`Marked PR #${prNumber} as ready (conflicts resolved)`);
31602
- } catch (error) {
31603
- this.logger.debug(`Could not mark PR as ready: ${error instanceof Error ? error.message : String(error)}`);
31604
- }
31605
- }
31606
- async postReplayConflictStatus(octokit, owner, repo, sha, replayConflictInfo, replayResult) {
31607
- if (replayResult == null || !replayResult.executed) {
31608
- return;
31609
- }
31610
- const hasConflicts = replayConflictInfo?.hasConflicts === true;
31611
- const conflictFileCount = (replayResult.conflictDetails ?? []).reduce(
31612
- (sum, detail) => sum + detail.files.length,
31613
- 0
31614
- );
31615
- const sanitizedName = this.config.generatorName?.replace(/\//g, "--");
31616
- const context = sanitizedName != null ? `fern / sdk customizations / ${sanitizedName}` : "fern / sdk customizations";
31617
- try {
31618
- await octokit.repos.createCommitStatus({
31619
- owner,
31620
- repo,
31621
- sha,
31622
- state: hasConflicts ? "failure" : "success",
31623
- context,
31624
- description: hasConflicts ? `${conflictFileCount} file(s) need manual conflict resolution \u2014 see PR description` : "All customizations applied"
31625
- });
31626
- this.logger.debug(`Posted ${hasConflicts ? "failing" : "passing"} commit status (${context})`);
31627
- } catch (error) {
31628
- this.logger.debug(
31629
- `Could not post commit status: ${error instanceof Error ? error.message : String(error)}`
31630
- );
31631
- }
31632
- }
31633
- async postWebEditorFallbackComment(octokit, owner, repo, prNumber, branchName, baseBranch, replayResult) {
31634
- try {
31635
- const webEditorLikelyDisabled = await this.isWebEditorLikelyDisabled(
31636
- octokit,
31637
- owner,
31638
- repo,
31639
- prNumber,
31640
- replayResult
31641
- );
31642
- if (!webEditorLikelyDisabled) {
31643
- this.logger.debug(`PR #${prNumber}: web conflict editor appears usable, skipping fallback comment`);
31644
- return;
31645
- }
31646
- const comment = buildWebEditorFallbackComment(branchName, baseBranch);
31647
- await octokit.issues.createComment({
31648
- owner,
31649
- repo,
31650
- issue_number: prNumber,
31651
- body: comment
31652
- });
31653
- this.logger.info(`Posted web-editor fallback comment on PR #${prNumber}`);
31654
- } catch (error) {
31655
- this.logger.debug(
31656
- `Could not post web-editor fallback comment: ${error instanceof Error ? error.message : String(error)}`
31657
- );
31658
- }
31659
- }
31660
- async isWebEditorLikelyDisabled(octokit, owner, repo, prNumber, replayResult) {
31661
- const conflictDetails = replayResult.conflictDetails ?? [];
31662
- const allConflictFiles = conflictDetails.flatMap((d) => d.files);
31663
- const totalConflictFiles = allConflictFiles.length;
31664
- const hasFileDeletionConflict = allConflictFiles.some((f) => {
31665
- const status2 = (f.status ?? "").toLowerCase();
31666
- const reason = (f.conflictReason ?? "").toLowerCase();
31667
- return status2 === "skipped" || reason.includes("delete") || reason.includes("removed");
31668
- });
31669
- if (hasFileDeletionConflict) {
31670
- this.logger.debug(`PR #${prNumber}: detected file deletion conflict, web editor likely disabled`);
31671
- return true;
31672
- }
31673
- if (totalConflictFiles >= WEB_EDITOR_FILE_THRESHOLD) {
31674
- this.logger.debug(
31675
- `PR #${prNumber}: ${totalConflictFiles} conflicting files exceeds threshold (${WEB_EDITOR_FILE_THRESHOLD}), web editor likely disabled`
31676
- );
31677
- return true;
31678
- }
31679
- const totalHunks = (replayResult.conflicts ?? []).reduce(
31680
- (sum, fileConflict) => sum + fileConflict.conflicts.length,
31681
- 0
31682
- );
31683
- if (totalHunks >= WEB_EDITOR_HUNK_THRESHOLD) {
31684
- this.logger.debug(
31685
- `PR #${prNumber}: ${totalHunks} conflict hunks exceeds threshold (${WEB_EDITOR_HUNK_THRESHOLD}), web editor likely disabled`
31686
- );
31687
- return true;
31688
- }
31689
- const mergeableState = await this.pollMergeableState(octokit, owner, repo, prNumber);
31690
- if (mergeableState.mergeable === null) {
31691
- this.logger.debug(`PR #${prNumber}: mergeable state still null after polling, web editor likely disabled`);
31692
- return true;
31693
- }
31694
- return false;
31695
- }
31696
- async pollMergeableState(octokit, owner, repo, prNumber) {
31697
- for (let attempt2 = 0; attempt2 < MERGEABLE_POLL_MAX_ATTEMPTS; attempt2++) {
31698
- const { data: pr } = await octokit.pulls.get({
31699
- owner,
31700
- repo,
31701
- pull_number: prNumber
31702
- });
31703
- if (pr.mergeable != null) {
31704
- return {
31705
- mergeable: pr.mergeable,
31706
- mergeableState: pr.mergeable_state
31707
- };
31708
- }
31709
- if (attempt2 < MERGEABLE_POLL_MAX_ATTEMPTS - 1) {
31710
- await sleep(MERGEABLE_POLL_DELAY_MS);
31711
- }
31712
- }
31713
- return { mergeable: null, mergeableState: "unknown" };
31714
- }
31715
31703
  };
31716
- var WEB_EDITOR_FILE_THRESHOLD = 20;
31717
- var WEB_EDITOR_HUNK_THRESHOLD = 50;
31718
- var MERGEABLE_POLL_MAX_ATTEMPTS = 3;
31719
- var MERGEABLE_POLL_DELAY_MS = 1500;
31720
- function buildWebEditorFallbackComment(branchName, baseBranch) {
31721
- const lines = [
31722
- `> **Note:** These conflicts may be too complex for GitHub's web editor. To resolve locally:`,
31723
- `>`,
31724
- `> 1. Check out this branch:`,
31725
- `> \`\`\`sh`,
31726
- `> git fetch origin && git checkout ${branchName}`,
31727
- `> \`\`\``,
31728
- `> 2. Merge the base branch:`,
31729
- `> \`\`\`sh`,
31730
- `> git merge origin/${baseBranch}`,
31731
- `> \`\`\``,
31732
- `> 3. Resolve conflicts in your editor (VS Code, IntelliJ, etc. have built-in merge tools)`,
31733
- `> 4. Commit and push:`,
31734
- `> \`\`\`sh`,
31735
- `> git add -A && git commit -m "resolve conflicts" && git push`,
31736
- `> \`\`\``,
31737
- `>`,
31738
- `> **Label guide:** In your editor's merge view, "current" = new generated code, "incoming" = your customization.`
31739
- ];
31740
- return lines.join("\n");
31741
- }
31742
- function sleep(ms) {
31743
- return new Promise((resolve8) => setTimeout(resolve8, ms));
31744
- }
31745
31704
 
31746
31705
  // src/replay/replay-run.ts
31747
31706
  init_dist();
@@ -31950,8 +31909,7 @@ var ReplayStep = class extends BaseStep {
31950
31909
  flow: "first-generation",
31951
31910
  patchesDetected: 0,
31952
31911
  patchesApplied: 0,
31953
- patchesWithConflicts: 0,
31954
- conflicts: []
31912
+ patchesWithConflicts: 0
31955
31913
  };
31956
31914
  }
31957
31915
  const report = result.report;
@@ -31969,22 +31927,12 @@ var ReplayStep = class extends BaseStep {
31969
31927
  patchesRepointed: report.patchesRepointed,
31970
31928
  patchesContentRebased: report.patchesContentRebased,
31971
31929
  patchesKeptAsUserOwned: report.patchesKeptAsUserOwned,
31972
- conflicts: report.conflicts.map((fileResult) => ({
31973
- filePath: fileResult.file,
31974
- conflicts: fileResult.conflicts?.map((conflict) => ({
31975
- startLine: conflict.startLine,
31976
- endLine: conflict.endLine,
31977
- ours: conflict.ours,
31978
- theirs: conflict.theirs
31979
- })) ?? []
31980
- })),
31981
- conflictDetails: report.conflictDetails?.map((detail) => ({
31982
- patchId: detail.patchId,
31983
- patchMessage: detail.patchMessage,
31984
- reason: detail.reason,
31985
- files: detail.files.map((f) => ({
31930
+ unresolvedPatches: report.unresolvedPatches?.map((info) => ({
31931
+ patchId: info.patchId,
31932
+ patchMessage: info.patchMessage,
31933
+ files: info.files,
31934
+ conflictDetails: info.conflictDetails.map((f) => ({
31986
31935
  file: f.file,
31987
- status: f.status,
31988
31936
  conflictReason: f.conflictReason
31989
31937
  }))
31990
31938
  })),
@@ -32248,6 +32196,9 @@ function buildPrBody(result) {
32248
32196
  return lines.join("\n");
32249
32197
  }
32250
32198
 
32199
+ // src/replay/replay-resolve.ts
32200
+ init_dist();
32201
+
32251
32202
  // src/configuration/loadGitHubConfig.ts
32252
32203
  var import_promises6 = require("fs/promises");
32253
32204
  async function loadGitHubConfig({