@fern-api/generator-cli 0.9.0 → 0.9.2

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 (2) hide show
  1. package/dist/cli.js +322 -15
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -19759,7 +19759,7 @@ var init_diff3 = __esm({
19759
19759
  }
19760
19760
  });
19761
19761
 
19762
- // ../../node_modules/.pnpm/@fern-api+replay@0.9.0/node_modules/@fern-api/replay/dist/index.js
19762
+ // ../../node_modules/.pnpm/@fern-api+replay@0.9.1/node_modules/@fern-api/replay/dist/index.js
19763
19763
  var dist_exports = {};
19764
19764
  __export(dist_exports, {
19765
19765
  FERN_BOT_EMAIL: () => FERN_BOT_EMAIL2,
@@ -19784,6 +19784,241 @@ __export(dist_exports, {
19784
19784
  status: () => status,
19785
19785
  threeWayMerge: () => threeWayMerge
19786
19786
  });
19787
+ function parseHunks(fileDiff) {
19788
+ const lines = fileDiff.split("\n");
19789
+ const hunks = [];
19790
+ let currentHunk = null;
19791
+ for (const line of lines) {
19792
+ const headerMatch = line.match(
19793
+ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/
19794
+ );
19795
+ if (headerMatch) {
19796
+ if (currentHunk) {
19797
+ hunks.push(currentHunk);
19798
+ }
19799
+ currentHunk = {
19800
+ oldStart: parseInt(headerMatch[1], 10),
19801
+ oldCount: headerMatch[2] != null ? parseInt(headerMatch[2], 10) : 1,
19802
+ newStart: parseInt(headerMatch[3], 10),
19803
+ newCount: headerMatch[4] != null ? parseInt(headerMatch[4], 10) : 1,
19804
+ lines: []
19805
+ };
19806
+ continue;
19807
+ }
19808
+ if (!currentHunk) continue;
19809
+ if (line.startsWith("diff --git") || line.startsWith("index ") || line.startsWith("---") || line.startsWith("+++") || line.startsWith("old mode") || line.startsWith("new mode") || line.startsWith("similarity index") || line.startsWith("rename from") || line.startsWith("rename to") || line.startsWith("new file mode") || line.startsWith("deleted file mode")) {
19810
+ continue;
19811
+ }
19812
+ if (line === "\") {
19813
+ continue;
19814
+ }
19815
+ if (line.startsWith("-")) {
19816
+ currentHunk.lines.push({ type: "remove", content: line.slice(1) });
19817
+ } else if (line.startsWith("+")) {
19818
+ currentHunk.lines.push({ type: "add", content: line.slice(1) });
19819
+ } else if (line.startsWith(" ") || line === "") {
19820
+ currentHunk.lines.push({
19821
+ type: "context",
19822
+ content: line.startsWith(" ") ? line.slice(1) : line
19823
+ });
19824
+ }
19825
+ }
19826
+ if (currentHunk) {
19827
+ hunks.push(currentHunk);
19828
+ }
19829
+ return hunks;
19830
+ }
19831
+ function extractLeadingContext(hunk) {
19832
+ const result = [];
19833
+ for (const line of hunk.lines) {
19834
+ if (line.type !== "context") break;
19835
+ result.push(line.content);
19836
+ }
19837
+ return result;
19838
+ }
19839
+ function extractTrailingContext(hunk) {
19840
+ const result = [];
19841
+ for (let i = hunk.lines.length - 1; i >= 0; i--) {
19842
+ if (hunk.lines[i].type !== "context") break;
19843
+ result.unshift(hunk.lines[i].content);
19844
+ }
19845
+ return result;
19846
+ }
19847
+ function countOursLinesBeforeTrailing(hunk) {
19848
+ let count = 0;
19849
+ const trailingStart = findTrailingContextStart(hunk);
19850
+ for (let i = 0; i < trailingStart; i++) {
19851
+ if (hunk.lines[i].type === "context") count++;
19852
+ }
19853
+ return count;
19854
+ }
19855
+ function findTrailingContextStart(hunk) {
19856
+ let i = hunk.lines.length - 1;
19857
+ while (i >= 0 && hunk.lines[i].type === "context") {
19858
+ i--;
19859
+ }
19860
+ return i + 1;
19861
+ }
19862
+ function matchesAt(needle, haystack, offset) {
19863
+ for (let i = 0; i < needle.length; i++) {
19864
+ if (haystack[offset + i] !== needle[i]) return false;
19865
+ }
19866
+ return true;
19867
+ }
19868
+ function findContextInOurs(contextLines, oursLines, minIndex, hint) {
19869
+ const SEARCH_WINDOW = 200;
19870
+ const maxStart = oursLines.length - contextLines.length;
19871
+ const clampedHint = Math.max(minIndex, Math.min(hint, maxStart));
19872
+ if (clampedHint >= minIndex && clampedHint <= maxStart) {
19873
+ if (matchesAt(contextLines, oursLines, clampedHint)) {
19874
+ return clampedHint;
19875
+ }
19876
+ }
19877
+ for (let delta = 1; delta <= SEARCH_WINDOW; delta++) {
19878
+ for (const sign of [1, -1]) {
19879
+ const idx = clampedHint + delta * sign;
19880
+ if (idx < minIndex || idx > maxStart) continue;
19881
+ if (matchesAt(contextLines, oursLines, idx)) {
19882
+ return idx;
19883
+ }
19884
+ }
19885
+ }
19886
+ return -1;
19887
+ }
19888
+ function computeOursSpan(hunk, oursLines, oursOffset) {
19889
+ const leading = extractLeadingContext(hunk);
19890
+ const trailing = extractTrailingContext(hunk);
19891
+ if (trailing.length === 0) {
19892
+ const contextCount2 = hunk.lines.filter(
19893
+ (l) => l.type === "context"
19894
+ ).length;
19895
+ return Math.min(contextCount2, oursLines.length - oursOffset);
19896
+ }
19897
+ const searchStart = oursOffset + leading.length;
19898
+ for (let i = searchStart; i <= oursLines.length - trailing.length; i++) {
19899
+ if (matchesAt(trailing, oursLines, i)) {
19900
+ return i + trailing.length - oursOffset;
19901
+ }
19902
+ }
19903
+ const contextCount = hunk.lines.filter(
19904
+ (l) => l.type === "context"
19905
+ ).length;
19906
+ return Math.min(contextCount, oursLines.length - oursOffset);
19907
+ }
19908
+ function locateHunksInOurs(hunks, oursLines) {
19909
+ const located = [];
19910
+ let minOursIndex = 0;
19911
+ for (const hunk of hunks) {
19912
+ const contextLines = extractLeadingContext(hunk);
19913
+ let oursOffset;
19914
+ if (contextLines.length > 0) {
19915
+ const found = findContextInOurs(
19916
+ contextLines,
19917
+ oursLines,
19918
+ minOursIndex,
19919
+ hunk.newStart - 1
19920
+ );
19921
+ if (found === -1) {
19922
+ const trailingContext = extractTrailingContext(hunk);
19923
+ if (trailingContext.length > 0) {
19924
+ const trailingFound = findContextInOurs(
19925
+ trailingContext,
19926
+ oursLines,
19927
+ minOursIndex,
19928
+ hunk.newStart - 1
19929
+ );
19930
+ if (trailingFound === -1) return null;
19931
+ const nonTrailingCount = countOursLinesBeforeTrailing(hunk);
19932
+ oursOffset = trailingFound - nonTrailingCount;
19933
+ if (oursOffset < minOursIndex) return null;
19934
+ } else {
19935
+ return null;
19936
+ }
19937
+ } else {
19938
+ oursOffset = found;
19939
+ }
19940
+ } else if (hunk.oldStart === 1 && hunk.oldCount === 0) {
19941
+ oursOffset = 0;
19942
+ } else {
19943
+ oursOffset = Math.max(hunk.newStart - 1, minOursIndex);
19944
+ }
19945
+ const oursSpan = computeOursSpan(hunk, oursLines, oursOffset);
19946
+ located.push({ hunk, oursOffset, oursSpan });
19947
+ minOursIndex = oursOffset + oursSpan;
19948
+ }
19949
+ return located;
19950
+ }
19951
+ function assembleHybrid(locatedHunks, oursLines) {
19952
+ const baseLines = [];
19953
+ const theirsLines = [];
19954
+ let oursCursor = 0;
19955
+ for (const { hunk, oursOffset, oursSpan } of locatedHunks) {
19956
+ if (oursOffset > oursCursor) {
19957
+ const gapLines = oursLines.slice(oursCursor, oursOffset);
19958
+ baseLines.push(...gapLines);
19959
+ theirsLines.push(...gapLines);
19960
+ }
19961
+ for (const line of hunk.lines) {
19962
+ switch (line.type) {
19963
+ case "context":
19964
+ baseLines.push(line.content);
19965
+ theirsLines.push(line.content);
19966
+ break;
19967
+ case "remove":
19968
+ baseLines.push(line.content);
19969
+ break;
19970
+ case "add":
19971
+ theirsLines.push(line.content);
19972
+ break;
19973
+ }
19974
+ }
19975
+ oursCursor = oursOffset + oursSpan;
19976
+ }
19977
+ if (oursCursor < oursLines.length) {
19978
+ const gapLines = oursLines.slice(oursCursor);
19979
+ baseLines.push(...gapLines);
19980
+ theirsLines.push(...gapLines);
19981
+ }
19982
+ return {
19983
+ base: baseLines.join("\n"),
19984
+ theirs: theirsLines.join("\n")
19985
+ };
19986
+ }
19987
+ function reconstructFromGhostPatch(fileDiff, ours) {
19988
+ const hunks = parseHunks(fileDiff);
19989
+ if (hunks.length === 0) {
19990
+ return null;
19991
+ }
19992
+ const isPureAddition = hunks.every(
19993
+ (h) => h.oldCount === 0 && h.lines.every((l) => l.type !== "remove")
19994
+ );
19995
+ if (isPureAddition) {
19996
+ return null;
19997
+ }
19998
+ const isPureDeletion = hunks.every(
19999
+ (h) => h.newCount === 0 && h.lines.every((l) => l.type !== "add")
20000
+ );
20001
+ if (isPureDeletion) {
20002
+ const baseLines = [];
20003
+ for (const hunk of hunks) {
20004
+ for (const line of hunk.lines) {
20005
+ if (line.type === "context" || line.type === "remove") {
20006
+ baseLines.push(line.content);
20007
+ }
20008
+ }
20009
+ }
20010
+ return {
20011
+ base: baseLines.join("\n"),
20012
+ theirs: ""
20013
+ };
20014
+ }
20015
+ const oursLines = ours.split("\n");
20016
+ const located = locateHunksInOurs(hunks, oursLines);
20017
+ if (!located) {
20018
+ return null;
20019
+ }
20020
+ return assembleHybrid(located, oursLines);
20021
+ }
19787
20022
  function isGenerationCommit(commit) {
19788
20023
  const isFernSupport = FERN_SUPPORT_NAMES.includes(commit.authorName);
19789
20024
  const isBotAuthor = !isFernSupport && (commit.authorLogin === FERN_BOT_LOGIN2 || commit.authorEmail === FERN_BOT_EMAIL2 || commit.authorName === FERN_BOT_NAME2);
@@ -19957,6 +20192,7 @@ async function bootstrap(outputDir, options) {
19957
20192
  }
19958
20193
  lockManager.save();
19959
20194
  const fernignoreUpdated = ensureFernignoreEntries(outputDir);
20195
+ ensureGitattributesEntries(outputDir);
19960
20196
  if (migrator.fernignoreExists() && fernignorePatterns.length > 0) {
19961
20197
  const action = options?.fernignoreAction ?? "skip";
19962
20198
  if (action === "migrate") {
@@ -20061,6 +20297,28 @@ function ensureFernignoreEntries(outputDir) {
20061
20297
  (0, import_fs9.writeFileSync)(fernignorePath, content, "utf-8");
20062
20298
  return true;
20063
20299
  }
20300
+ function ensureGitattributesEntries(outputDir) {
20301
+ const gitattributesPath = (0, import_path12.join)(outputDir, ".gitattributes");
20302
+ let content = "";
20303
+ if ((0, import_fs9.existsSync)(gitattributesPath)) {
20304
+ content = (0, import_fs9.readFileSync)(gitattributesPath, "utf-8");
20305
+ }
20306
+ const lines = content.split("\n");
20307
+ const toAdd = [];
20308
+ for (const entry of GITATTRIBUTES_ENTRIES) {
20309
+ if (!lines.some((line) => line.trim() === entry)) {
20310
+ toAdd.push(entry);
20311
+ }
20312
+ }
20313
+ if (toAdd.length === 0) {
20314
+ return;
20315
+ }
20316
+ if (content && !content.endsWith("\n")) {
20317
+ content += "\n";
20318
+ }
20319
+ content += toAdd.join("\n") + "\n";
20320
+ (0, import_fs9.writeFileSync)(gitattributesPath, content, "utf-8");
20321
+ }
20064
20322
  function computeContentHash(patchContent) {
20065
20323
  const normalized = patchContent.split("\n").filter((line) => !line.startsWith("From ") && !line.startsWith("index ") && !line.startsWith("Date: ")).join("\n");
20066
20324
  return `sha256:${(0, import_crypto3.createHash)("sha256").update(normalized).digest("hex")}`;
@@ -20352,9 +20610,9 @@ function status(outputDir) {
20352
20610
  excludePatterns
20353
20611
  };
20354
20612
  }
20355
- 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, EMPTY_RESULT;
20613
+ 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, HybridReconstruction_exports, init_HybridReconstruction, 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, GITATTRIBUTES_ENTRIES, EMPTY_RESULT;
20356
20614
  var init_dist = __esm({
20357
- "../../node_modules/.pnpm/@fern-api+replay@0.9.0/node_modules/@fern-api/replay/dist/index.js"() {
20615
+ "../../node_modules/.pnpm/@fern-api+replay@0.9.1/node_modules/@fern-api/replay/dist/index.js"() {
20358
20616
  "use strict";
20359
20617
  init_esm4();
20360
20618
  import_fs6 = require("fs");
@@ -20488,6 +20746,14 @@ var init_dist = __esm({
20488
20746
  return false;
20489
20747
  }
20490
20748
  }
20749
+ async treeExists(treeHash) {
20750
+ try {
20751
+ const type = await this.exec(["cat-file", "-t", treeHash]);
20752
+ return type.trim() === "tree";
20753
+ } catch {
20754
+ return false;
20755
+ }
20756
+ }
20491
20757
  async getCommitBody(commitSha) {
20492
20758
  return this.exec(["log", "-1", "--format=%B", commitSha]);
20493
20759
  }
@@ -20497,6 +20763,18 @@ var init_dist = __esm({
20497
20763
  };
20498
20764
  }
20499
20765
  });
20766
+ HybridReconstruction_exports = {};
20767
+ __export3(HybridReconstruction_exports, {
20768
+ assembleHybrid: () => assembleHybrid,
20769
+ locateHunksInOurs: () => locateHunksInOurs,
20770
+ parseHunks: () => parseHunks,
20771
+ reconstructFromGhostPatch: () => reconstructFromGhostPatch
20772
+ });
20773
+ init_HybridReconstruction = __esm3({
20774
+ "src/HybridReconstruction.ts"() {
20775
+ "use strict";
20776
+ }
20777
+ });
20500
20778
  init_GitClient();
20501
20779
  FERN_BOT_NAME2 = "fern-api";
20502
20780
  FERN_BOT_EMAIL2 = "115122769+fern-api[bot]@users.noreply.github.com";
@@ -20883,6 +21161,7 @@ var init_dist = __esm({
20883
21161
  lockManager;
20884
21162
  outputDir;
20885
21163
  renameCache = /* @__PURE__ */ new Map();
21164
+ treeExistsCache = /* @__PURE__ */ new Map();
20886
21165
  fileTheirsAccumulator = /* @__PURE__ */ new Map();
20887
21166
  constructor(git, lockManager, outputDir) {
20888
21167
  this.git = git;
@@ -21079,14 +21358,33 @@ var init_dist = __esm({
21079
21358
  }
21080
21359
  const oursPath = (0, import_path9.join)(this.outputDir, resolvedPath);
21081
21360
  const ours = await (0, import_promises4.readFile)(oursPath, "utf-8").catch(() => null);
21082
- let theirs = await this.applyPatchToContent(
21083
- base,
21084
- patch.patch_content,
21085
- filePath,
21086
- tempGit,
21087
- tempDir,
21088
- renameSourcePath
21089
- );
21361
+ let ghostReconstructed = false;
21362
+ let theirs = null;
21363
+ if (!base && ours && !renameSourcePath) {
21364
+ const treeReachable = await this.isTreeReachable(baseGen.tree_hash);
21365
+ if (!treeReachable) {
21366
+ const fileDiff = this.extractFileDiff(patch.patch_content, filePath);
21367
+ if (fileDiff) {
21368
+ const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
21369
+ const result = reconstructFromGhostPatch2(fileDiff, ours);
21370
+ if (result) {
21371
+ base = result.base;
21372
+ theirs = result.theirs;
21373
+ ghostReconstructed = true;
21374
+ }
21375
+ }
21376
+ }
21377
+ }
21378
+ if (!ghostReconstructed) {
21379
+ theirs = await this.applyPatchToContent(
21380
+ base,
21381
+ patch.patch_content,
21382
+ filePath,
21383
+ tempGit,
21384
+ tempDir,
21385
+ renameSourcePath
21386
+ );
21387
+ }
21090
21388
  let useAccumulatorAsMergeBase = false;
21091
21389
  const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
21092
21390
  if (!theirs && accumulatorEntry) {
@@ -21119,13 +21417,13 @@ var init_dist = __esm({
21119
21417
  baseMismatchSkipped = true;
21120
21418
  }
21121
21419
  }
21122
- if (!base && !ours && effective_theirs) {
21420
+ if (base == null && !ours && effective_theirs) {
21123
21421
  const outDir2 = (0, import_path9.dirname)(oursPath);
21124
21422
  await (0, import_promises4.mkdir)(outDir2, { recursive: true });
21125
21423
  await (0, import_promises4.writeFile)(oursPath, effective_theirs);
21126
21424
  return { file: resolvedPath, status: "merged", reason: "new-file" };
21127
21425
  }
21128
- if (!base && ours && effective_theirs) {
21426
+ if (base == null && ours && effective_theirs) {
21129
21427
  const merged2 = threeWayMerge("", ours, effective_theirs);
21130
21428
  const outDir2 = (0, import_path9.dirname)(oursPath);
21131
21429
  await (0, import_promises4.mkdir)(outDir2, { recursive: true });
@@ -21148,7 +21446,7 @@ var init_dist = __esm({
21148
21446
  reason: "missing-content"
21149
21447
  };
21150
21448
  }
21151
- if (!base && !useAccumulatorAsMergeBase || !ours) {
21449
+ if (base == null && !useAccumulatorAsMergeBase || !ours) {
21152
21450
  return {
21153
21451
  file: resolvedPath,
21154
21452
  status: "skipped",
@@ -21191,6 +21489,14 @@ var init_dist = __esm({
21191
21489
  };
21192
21490
  }
21193
21491
  }
21492
+ async isTreeReachable(treeHash) {
21493
+ let result = this.treeExistsCache.get(treeHash);
21494
+ if (result === void 0) {
21495
+ result = await this.git.treeExists(treeHash);
21496
+ this.treeExistsCache.set(treeHash, result);
21497
+ }
21498
+ return result;
21499
+ }
21194
21500
  isExcluded(patch) {
21195
21501
  const config = this.lockManager.getCustomizationsConfig();
21196
21502
  if (!config.exclude) return false;
@@ -22157,7 +22463,8 @@ CLI Version: ${options.cliVersion}`;
22157
22463
  }
22158
22464
  };
22159
22465
  init_GitClient();
22160
- REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml"];
22466
+ REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
22467
+ GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
22161
22468
  EMPTY_RESULT = {
22162
22469
  initialized: false,
22163
22470
  removed: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fern-api/generator-cli",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/fern-api/fern.git",
@@ -22,7 +22,7 @@
22
22
  "**"
23
23
  ],
24
24
  "dependencies": {
25
- "@fern-api/replay": "^0.9.0",
25
+ "@fern-api/replay": "^0.9.1",
26
26
  "@octokit/rest": "^22.0.1",
27
27
  "es-toolkit": "^1.45.1",
28
28
  "tmp-promise": "^3.0.3"