@fern-api/generator-cli 0.9.37 → 0.9.39

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 +114 -33
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -22134,7 +22134,7 @@ var init_diff3 = __esm({
22134
22134
  }
22135
22135
  });
22136
22136
 
22137
- // ../../node_modules/.pnpm/@fern-api+replay@0.16.2/node_modules/@fern-api/replay/dist/index.js
22137
+ // ../../node_modules/.pnpm/@fern-api+replay@0.18.0/node_modules/@fern-api/replay/dist/index.js
22138
22138
  var dist_exports = {};
22139
22139
  __export(dist_exports, {
22140
22140
  CREDENTIAL_PATTERNS: () => CREDENTIAL_PATTERNS,
@@ -22495,7 +22495,8 @@ function isBinaryFile(filePath) {
22495
22495
  }
22496
22496
  function matchesFernignorePattern(filePath, patterns) {
22497
22497
  if (patterns.length === 0) return false;
22498
- return patterns.some((p2) => {
22498
+ return patterns.some((rawPattern) => {
22499
+ const p2 = rawPattern.endsWith("/") ? rawPattern.slice(0, -1) : rawPattern;
22499
22500
  if (filePath === p2) return true;
22500
22501
  if (minimatch(filePath, p2)) return true;
22501
22502
  if (!p2.includes("*") && !p2.includes("?") && filePath.startsWith(p2 + "/")) return true;
@@ -22739,16 +22740,20 @@ async function resolveInputs(args) {
22739
22740
  const oursPath = (0, import_path11.join)(outputDir, resolvedPath);
22740
22741
  const ours = await (0, import_promises5.readFile)(oursPath, "utf-8").catch(() => null);
22741
22742
  let ghostTheirs = null;
22742
- if (!base && ours && !renameSourcePath) {
22743
+ let baseTreeUnreachable = false;
22744
+ if (!base && !renameSourcePath) {
22743
22745
  const treeReachable = await isTreeReachable(baseGen.tree_hash);
22744
22746
  if (!treeReachable) {
22745
- const fileDiff = extractFileDiff2(patch.patch_content, filePath);
22746
- if (fileDiff) {
22747
- const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
22748
- const result = reconstructFromGhostPatch2(fileDiff, ours);
22749
- if (result) {
22750
- base = result.base;
22751
- ghostTheirs = result.theirs;
22747
+ baseTreeUnreachable = true;
22748
+ if (ours) {
22749
+ const fileDiff = extractFileDiff2(patch.patch_content, filePath);
22750
+ if (fileDiff) {
22751
+ const { reconstructFromGhostPatch: reconstructFromGhostPatch2 } = await Promise.resolve().then(() => (init_HybridReconstruction(), HybridReconstruction_exports));
22752
+ const result = reconstructFromGhostPatch2(fileDiff, ours);
22753
+ if (result) {
22754
+ base = result.base;
22755
+ ghostTheirs = result.theirs;
22756
+ }
22752
22757
  }
22753
22758
  }
22754
22759
  }
@@ -22760,7 +22765,11 @@ async function resolveInputs(args) {
22760
22765
  ours,
22761
22766
  oursPath,
22762
22767
  renameSourcePath,
22763
- ghostTheirs
22768
+ ghostTheirs,
22769
+ // Only meaningful when ghost reconstruction did NOT recover a base; if it
22770
+ // did, `base` is non-null and the merge proceeds normally. We still set
22771
+ // the flag truthfully (the tree IS unreachable) so Phase 3 can decide.
22772
+ baseTreeUnreachable
22764
22773
  };
22765
22774
  }
22766
22775
  function trimCR(line) {
@@ -22936,7 +22945,11 @@ async function buildMergePlan(args) {
22936
22945
  return { kind: "new-file-only-user", theirs: effective_theirs };
22937
22946
  }
22938
22947
  if (base == null && ours != null && effective_theirs != null && !useAccumulatorAsMergeBase) {
22939
- return { kind: "new-file-both", theirs: effective_theirs };
22948
+ return {
22949
+ kind: "new-file-both",
22950
+ theirs: effective_theirs,
22951
+ ...inputs.baseTreeUnreachable ? { baseTreeUnreachable: true } : {}
22952
+ };
22940
22953
  }
22941
22954
  if (effective_theirs == null) {
22942
22955
  return { kind: "skipped", reason: "missing-content" };
@@ -22980,17 +22993,19 @@ async function runMergeAndRecord(args) {
22980
22993
  const merged2 = threeWayMerge("", ours, plan.theirs);
22981
22994
  await (0, import_promises6.mkdir)((0, import_path12.dirname)(oursPath), { recursive: true });
22982
22995
  await (0, import_promises6.writeFile)(oursPath, merged2.content);
22996
+ const unreachableFlag = plan.baseTreeUnreachable ? { baseTreeUnreachable: true } : {};
22983
22997
  if (merged2.hasConflicts) {
22984
22998
  return {
22985
22999
  file: resolvedPath,
22986
23000
  status: "conflict",
22987
23001
  conflicts: merged2.conflicts,
22988
23002
  conflictReason: "new-file-both",
22989
- conflictMetadata: metadata
23003
+ conflictMetadata: metadata,
23004
+ ...unreachableFlag
22990
23005
  };
22991
23006
  }
22992
23007
  accumulator.recordMerge(resolvedPath, merged2.content, patch.base_generation);
22993
- return { file: resolvedPath, status: "merged" };
23008
+ return { file: resolvedPath, status: "merged", ...unreachableFlag };
22994
23009
  }
22995
23010
  const merged = threeWayMerge(plan.mergeBase, ours, plan.theirs);
22996
23011
  await (0, import_promises6.mkdir)((0, import_path12.dirname)(oursPath), { recursive: true });
@@ -24064,7 +24079,7 @@ function status(outputDir) {
24064
24079
  }
24065
24080
  var import_promises3, import_os, import_path7, import_fs6, import_path8, import_yaml, import_crypto, import_path9, import_promises4, import_os2, import_path10, import_promises5, import_path11, import_promises6, import_path12, import_fs7, import_promises7, import_path13, import_promises8, import_os3, import_path14, import_child_process3, import_crypto2, import_fs8, import_path15, import_yaml2, import_crypto3, import_fs9, import_path16, import_fs10, import_promises9, import_path17, __defProp3, __getOwnPropNames3, __esm3, __export3, GitClient_exports, GitClient, init_GitClient, HybridReconstruction_exports, init_HybridReconstruction, PatchApplyTheirs_exports, init_PatchApplyTheirs, CREDENTIAL_PATTERNS, SENSITIVE_FILE_PATTERNS, FERN_BOT_NAME, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_SUPPORT_NAMES, LOCKFILE_HEADER, LockfileNotFoundError, LockfileManager, BINARY_EXTENSIONS, INFRASTRUCTURE_FILES, ReplayDetector, MergeAccumulator, CONFLICT_OPENER, CONFLICT_SEPARATOR, CONFLICT_CLOSER, ReplayApplicator, ReplayCommitter, FileOwnership, FirstGenerationFlow, SkipApplicationFlow, NoPatchesFlow, NormalRegenerationFlow, ReplayService, FernignoreMigrator, REPLAY_FERNIGNORE_ENTRIES, GITATTRIBUTES_ENTRIES, EMPTY_RESULT;
24066
24081
  var init_dist3 = __esm({
24067
- "../../node_modules/.pnpm/@fern-api+replay@0.16.2/node_modules/@fern-api/replay/dist/index.js"() {
24082
+ "../../node_modules/.pnpm/@fern-api+replay@0.18.0/node_modules/@fern-api/replay/dist/index.js"() {
24068
24083
  "use strict";
24069
24084
  init_esm4();
24070
24085
  import_promises3 = require("fs/promises");
@@ -24145,6 +24160,11 @@ var init_dist3 = __esm({
24145
24160
  proc.stderr.on("data", (data) => {
24146
24161
  stderr += data.toString();
24147
24162
  });
24163
+ proc.on("error", (err) => {
24164
+ reject(new Error(`git ${args.join(" ")} failed to spawn: ${err.message}`));
24165
+ });
24166
+ proc.stdin.on("error", () => {
24167
+ });
24148
24168
  proc.on("close", (code) => {
24149
24169
  if (code === 0) {
24150
24170
  resolve22(stdout);
@@ -24599,6 +24619,22 @@ var init_dist3 = __esm({
24599
24619
  return commit.sha;
24600
24620
  }
24601
24621
  }
24622
+ let fullLog;
24623
+ try {
24624
+ fullLog = await this.git.exec([
24625
+ "log",
24626
+ "--no-merges",
24627
+ "--format=%H%x00%an%x00%ae%x00%s",
24628
+ "HEAD"
24629
+ ]);
24630
+ } catch {
24631
+ return null;
24632
+ }
24633
+ for (const commit of this.parseGitLog(fullLog)) {
24634
+ if (isGenerationBoundary(commit)) {
24635
+ return commit.sha;
24636
+ }
24637
+ }
24602
24638
  return null;
24603
24639
  }
24604
24640
  async detectNewPatches() {
@@ -24610,6 +24646,11 @@ var init_dist3 = __esm({
24610
24646
  }
24611
24647
  const lastGen = this.getLastGeneration(lock);
24612
24648
  if (!lastGen) {
24649
+ if (lock.current_generation) {
24650
+ this.warnings.push(
24651
+ `No generation boundary found in git history and the lockfile's recorded generation (${lock.current_generation.slice(0, 7)}) matches no entry in its generation records. Customer customizations may differ from the last known generation and could be lost on the next regeneration. Run \`fern-replay bootstrap --force\` to re-initialize tracking.`
24652
+ );
24653
+ }
24613
24654
  return { patches: [], revertedPatchIds: [] };
24614
24655
  }
24615
24656
  const exists2 = await this.git.commitExists(lastGen.commit_sha);
@@ -24657,20 +24698,15 @@ var init_dist3 = __esm({
24657
24698
  continue;
24658
24699
  }
24659
24700
  const parents = await this.git.getCommitParents(commit.sha);
24660
- let patchContent;
24701
+ let filesOutput;
24661
24702
  try {
24662
- patchContent = await this.git.formatPatch(commit.sha);
24703
+ filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
24663
24704
  } catch {
24664
24705
  this.warnings.push(
24665
24706
  `Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
24666
24707
  );
24667
24708
  continue;
24668
24709
  }
24669
- let contentHash = this.computeContentHash(patchContent);
24670
- if (lock.patches.find((p2) => p2.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
24671
- continue;
24672
- }
24673
- const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
24674
24710
  const { files, sensitiveFiles, fernignoredFiles } = filterInfrastructureAndSensitiveFiles(
24675
24711
  filesOutput,
24676
24712
  this.fernignorePatterns
@@ -24685,19 +24721,31 @@ var init_dist3 = __esm({
24685
24721
  `.fernignore-protected file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${fernignoredFiles.join(", ")}. These files are managed by .fernignore, not Replay.`
24686
24722
  );
24687
24723
  }
24688
- if ((sensitiveFiles.length > 0 || fernignoredFiles.length > 0) && files.length > 0) {
24689
- const parentSha = parents[0];
24690
- if (parentSha) {
24691
- try {
24692
- patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
24693
- } catch {
24694
- continue;
24695
- }
24696
- if (!patchContent.trim()) continue;
24697
- contentHash = this.computeContentHash(patchContent);
24724
+ if (files.length === 0) {
24725
+ continue;
24726
+ }
24727
+ const wasFiltered = sensitiveFiles.length > 0 || fernignoredFiles.length > 0;
24728
+ const parentSha = parents[0];
24729
+ let patchContent;
24730
+ if (wasFiltered && parentSha) {
24731
+ try {
24732
+ patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
24733
+ } catch {
24734
+ continue;
24735
+ }
24736
+ if (!patchContent.trim()) continue;
24737
+ } else {
24738
+ try {
24739
+ patchContent = await this.git.formatPatch(commit.sha);
24740
+ } catch {
24741
+ this.warnings.push(
24742
+ `Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
24743
+ );
24744
+ continue;
24698
24745
  }
24699
24746
  }
24700
- if (files.length === 0) {
24747
+ const contentHash = this.computeContentHash(patchContent);
24748
+ if (lock.patches.find((p2) => p2.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
24701
24749
  continue;
24702
24750
  }
24703
24751
  const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
@@ -25984,6 +26032,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
25984
26032
  results = await this.service.applicator.applyPatches(newPatches);
25985
26033
  this.service._lastApplyResults = results;
25986
26034
  this.service.recordDroppedContextLineWarnings(results);
26035
+ this.service.recordUnreachableBaseWarnings(results);
25987
26036
  this.service.revertConflictingFiles(results);
25988
26037
  for (const result of results) {
25989
26038
  if (result.status === "conflict") {
@@ -26137,6 +26186,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
26137
26186
  const results = await this.service.applicator.applyPatches(allPatches);
26138
26187
  this.service._lastApplyResults = results;
26139
26188
  this.service.recordDroppedContextLineWarnings(results);
26189
+ this.service.recordUnreachableBaseWarnings(results);
26140
26190
  this.service.revertConflictingFiles(results);
26141
26191
  for (const result of results) {
26142
26192
  if (result.status === "conflict") {
@@ -27047,6 +27097,37 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
27047
27097
  }
27048
27098
  }
27049
27099
  }
27100
+ /**
27101
+ * After applyPatches(), surface a warning for each (patch × file) where a
27102
+ * customization's BASE could not be read because the patch's
27103
+ * base-generation tree is unreachable in this clone (shallow-clone window
27104
+ * too small / pruned history). In that state the merge pipeline falls
27105
+ * through to `new-file-both` and the customer's edit can be dropped with NO
27106
+ * conflict and NO error — the INC-866 silent-loss class. This converts that
27107
+ * silence into an explicit, actionable warning naming the file and the
27108
+ * likely cause (insufficient clone window).
27109
+ *
27110
+ * Mirrors `recordDroppedContextLineWarnings`'s "surface, never silently
27111
+ * drop" contract. De-duplicated per file path so a single insufficient
27112
+ * window doesn't spam one warning per touched file repeatedly.
27113
+ */
27114
+ /** @internal Used by Flow implementations in `src/flows/`. */
27115
+ recordUnreachableBaseWarnings(results) {
27116
+ const warned = /* @__PURE__ */ new Set();
27117
+ for (const result of results) {
27118
+ if (!result.fileResults) continue;
27119
+ for (const fr of result.fileResults) {
27120
+ if (!fr.baseTreeUnreachable) continue;
27121
+ if (warned.has(fr.file)) continue;
27122
+ warned.add(fr.file);
27123
+ const base = fr.conflictMetadata?.baseGeneration;
27124
+ const baseRef = base ? ` (base generation ${base.slice(0, 7)})` : "";
27125
+ this.warnings.push(
27126
+ `${fr.file}: could not read the prior generation's version of this file${baseRef} because that generation's tree is outside the clone window (insufficient shallow-clone depth or pruned history). The customization was treated as a new file, which can drop your edits silently. Re-run with the prior generation in the clone window \u2014 e.g. \`git fetch --shallow-since=<date just before the prior [fern-generated] commit>\` or \`git fetch --unshallow\` \u2014 then re-run replay.`
27127
+ );
27128
+ }
27129
+ }
27130
+ }
27050
27131
  /**
27051
27132
  * After applyPatches(), strip conflict markers from conflicting files
27052
27133
  * so only clean content is committed. Keeps the Generated (OURS) side.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fern-api/generator-cli",
3
- "version": "0.9.37",
3
+ "version": "0.9.39",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/fern-api/fern.git",
@@ -23,7 +23,7 @@
23
23
  ],
24
24
  "dependencies": {
25
25
  "@boundaryml/baml": "^0.219.0",
26
- "@fern-api/replay": "0.16.2",
26
+ "@fern-api/replay": "0.18.0",
27
27
  "@octokit/rest": "^22.0.1",
28
28
  "es-toolkit": "^1.45.1",
29
29
  "semver": "^7.6.3",