@fern-api/replay 0.19.0 → 0.19.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.
- package/dist/cli.cjs +132 -7
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +132 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +132 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -16449,6 +16449,51 @@ async function buildMergePlan(args) {
|
|
|
16449
16449
|
// src/applicator/runMergeAndRecord.ts
|
|
16450
16450
|
var import_promises2 = require("fs/promises");
|
|
16451
16451
|
var import_node_path5 = require("path");
|
|
16452
|
+
|
|
16453
|
+
// src/autoversion.ts
|
|
16454
|
+
var AUTOVERSION_PLACEHOLDERS = [
|
|
16455
|
+
"v0.0.0-fern-placeholder",
|
|
16456
|
+
// Go -- a superstring of the default; check first
|
|
16457
|
+
"0.0.0-fern-placeholder",
|
|
16458
|
+
// TS/Java/C#/Ruby/PHP/Rust/default
|
|
16459
|
+
"0.0.0.dev0"
|
|
16460
|
+
// Python (PEP 440 -- Poetry rejects hyphens)
|
|
16461
|
+
];
|
|
16462
|
+
var VERSION_TOKEN = /(?:v)?\d+\.\d+\.\d+[0-9A-Za-z.+-]*/g;
|
|
16463
|
+
var VERSION_MASK = "@@FERN_VERSION@@";
|
|
16464
|
+
function maskVersions(line) {
|
|
16465
|
+
return line.replace(VERSION_TOKEN, VERSION_MASK);
|
|
16466
|
+
}
|
|
16467
|
+
function containsPlaceholder(content) {
|
|
16468
|
+
if (!content) return false;
|
|
16469
|
+
return AUTOVERSION_PLACEHOLDERS.some((p) => content.includes(p));
|
|
16470
|
+
}
|
|
16471
|
+
function remapManagedLines(genBase, target, theirs) {
|
|
16472
|
+
if (!containsPlaceholder(genBase)) return theirs;
|
|
16473
|
+
const managedMasks = /* @__PURE__ */ new Set();
|
|
16474
|
+
for (const line of genBase.split("\n")) {
|
|
16475
|
+
if (AUTOVERSION_PLACEHOLDERS.some((p) => line.includes(p))) managedMasks.add(maskVersions(line));
|
|
16476
|
+
}
|
|
16477
|
+
if (managedMasks.size === 0) return theirs;
|
|
16478
|
+
const targetByMask = /* @__PURE__ */ new Map();
|
|
16479
|
+
for (const line of target.split("\n")) {
|
|
16480
|
+
const mask = maskVersions(line);
|
|
16481
|
+
if (managedMasks.has(mask) && !targetByMask.has(mask)) targetByMask.set(mask, line);
|
|
16482
|
+
}
|
|
16483
|
+
return theirs.split("\n").map((line) => {
|
|
16484
|
+
const mask = maskVersions(line);
|
|
16485
|
+
const replacement = managedMasks.has(mask) ? targetByMask.get(mask) : void 0;
|
|
16486
|
+
return replacement !== void 0 && replacement !== line ? replacement : line;
|
|
16487
|
+
}).join("\n");
|
|
16488
|
+
}
|
|
16489
|
+
function neutralizeAutoversionTheirs(genBase, theirs) {
|
|
16490
|
+
return remapManagedLines(genBase, genBase, theirs);
|
|
16491
|
+
}
|
|
16492
|
+
function alignAutoversionToOurs(genBase, ours, theirs) {
|
|
16493
|
+
return remapManagedLines(genBase, ours, theirs);
|
|
16494
|
+
}
|
|
16495
|
+
|
|
16496
|
+
// src/applicator/runMergeAndRecord.ts
|
|
16452
16497
|
async function runMergeAndRecord(args) {
|
|
16453
16498
|
const { plan, inputs, patch, accumulator } = args;
|
|
16454
16499
|
const { resolvedPath, metadata, oursPath, ours } = inputs;
|
|
@@ -16479,11 +16524,13 @@ async function runMergeAndRecord(args) {
|
|
|
16479
16524
|
accumulator.recordMerge(resolvedPath, merged2.content, patch.base_generation);
|
|
16480
16525
|
return { file: resolvedPath, status: "merged", ...unreachableFlag };
|
|
16481
16526
|
}
|
|
16482
|
-
const
|
|
16527
|
+
const mergeBase = inputs.base != null ? alignAutoversionToOurs(inputs.base, ours, plan.mergeBase) : plan.mergeBase;
|
|
16528
|
+
const theirs = inputs.base != null ? alignAutoversionToOurs(inputs.base, ours, plan.theirs) : plan.theirs;
|
|
16529
|
+
const merged = threeWayMerge(mergeBase, ours, theirs);
|
|
16483
16530
|
await (0, import_promises2.mkdir)((0, import_node_path5.dirname)(oursPath), { recursive: true });
|
|
16484
16531
|
await (0, import_promises2.writeFile)(oursPath, merged.content);
|
|
16485
16532
|
if (merged.hasConflicts) {
|
|
16486
|
-
accumulator.recordConflict(resolvedPath,
|
|
16533
|
+
accumulator.recordConflict(resolvedPath, theirs, patch.base_generation);
|
|
16487
16534
|
return {
|
|
16488
16535
|
file: resolvedPath,
|
|
16489
16536
|
status: "conflict",
|
|
@@ -16492,7 +16539,7 @@ async function runMergeAndRecord(args) {
|
|
|
16492
16539
|
conflictMetadata: metadata
|
|
16493
16540
|
};
|
|
16494
16541
|
}
|
|
16495
|
-
accumulator.recordMerge(resolvedPath,
|
|
16542
|
+
accumulator.recordMerge(resolvedPath, theirs, patch.base_generation);
|
|
16496
16543
|
return {
|
|
16497
16544
|
file: resolvedPath,
|
|
16498
16545
|
status: "merged",
|
|
@@ -17612,7 +17659,9 @@ var NoPatchesFlow = class {
|
|
|
17612
17659
|
}
|
|
17613
17660
|
async prepare(options) {
|
|
17614
17661
|
const preLock = this.service.lockManager.read();
|
|
17615
|
-
const previousGenerationSha =
|
|
17662
|
+
const previousGenerationSha = await this.service.resolveReachablePreviousGeneration(
|
|
17663
|
+
preLock.current_generation ?? null
|
|
17664
|
+
);
|
|
17616
17665
|
let { patches: newPatches, revertedPatchIds } = await this.service.detector.detectNewPatches();
|
|
17617
17666
|
const detectorWarnings = [...this.service.detector.warnings];
|
|
17618
17667
|
const detectorDegraded = [...this.service.detector.degradedReasons];
|
|
@@ -17750,7 +17799,9 @@ var NormalRegenerationFlow = class {
|
|
|
17750
17799
|
}
|
|
17751
17800
|
async prepare(options) {
|
|
17752
17801
|
const preLock = this.service.lockManager.read();
|
|
17753
|
-
const previousGenerationSha =
|
|
17802
|
+
const previousGenerationSha = await this.service.resolveReachablePreviousGeneration(
|
|
17803
|
+
preLock.current_generation ?? null
|
|
17804
|
+
);
|
|
17754
17805
|
if (options?.dryRun) {
|
|
17755
17806
|
const existingPatches2 = this.service.lockManager.getPatches();
|
|
17756
17807
|
const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.service.detector.detectNewPatches();
|
|
@@ -17988,6 +18039,37 @@ var ReplayService = class {
|
|
|
17988
18039
|
this.committer = new ReplayCommitter(git, outputDir);
|
|
17989
18040
|
this.fileOwnership = new FileOwnership(git);
|
|
17990
18041
|
}
|
|
18042
|
+
/**
|
|
18043
|
+
* Resolve the previous-generation SHA to export on
|
|
18044
|
+
* `ReplayPreparation.previousGenerationSha`.
|
|
18045
|
+
*
|
|
18046
|
+
* `current_generation` is the SHA of a LOCALLY-created `[fern-generated]` commit.
|
|
18047
|
+
* generator-cli's signed-commit push (`pushSignedCommit`) recreates that commit under a
|
|
18048
|
+
* new remote SHA, and a squash-merge collapses it away, so the recorded value can be
|
|
18049
|
+
* unreachable in a later clone (org "Natural": autoVersion then diffed against a phantom
|
|
18050
|
+
* baseline and silently reported NO_CHANGE — or, pre-a38bbc6, shipped
|
|
18051
|
+
* `0.0.0-fern-placeholder`). Consumers (generator-cli's autoVersion) diff against this
|
|
18052
|
+
* baseline, so it MUST resolve to a real commit.
|
|
18053
|
+
*
|
|
18054
|
+
* Prefer the recorded SHA while it still resolves — the healthy case, and the pristine
|
|
18055
|
+
* prior `[fern-generated]` tree. Otherwise re-anchor on the most recent reachable
|
|
18056
|
+
* generation boundary in first-parent history: the SAME value the detector already
|
|
18057
|
+
* derives for its own scan boundary (`findPreviousGenerationFromHistory`, whose
|
|
18058
|
+
* `isGenerationBoundary` matches squash-merge subjects like `SDK Generation (#N)`). Fall
|
|
18059
|
+
* back to the recorded SHA only when no boundary is reachable at all — no worse than the
|
|
18060
|
+
* prior behavior. ADR 0002 (generator-cli) allows replay to expose this reconciled value;
|
|
18061
|
+
* the consumer stays independently resilient.
|
|
18062
|
+
*
|
|
18063
|
+
* MUST be called before the new `[fern-generated]` commit is created, while HEAD is still
|
|
18064
|
+
* the pre-generation (customer) state, so the walk anchors on the PREVIOUS generation.
|
|
18065
|
+
*/
|
|
18066
|
+
async resolveReachablePreviousGeneration(recordedSha) {
|
|
18067
|
+
if (recordedSha != null && await this.git.commitExists(recordedSha)) {
|
|
18068
|
+
return recordedSha;
|
|
18069
|
+
}
|
|
18070
|
+
const derived = await this.detector.findPreviousGenerationFromHistory();
|
|
18071
|
+
return derived ?? recordedSha;
|
|
18072
|
+
}
|
|
17991
18073
|
/**
|
|
17992
18074
|
* Phase 1 of the two-phase replay flow. Runs preGenerationRebase (when applicable),
|
|
17993
18075
|
* detects new patches, and creates the `[fern-generated]` commit. Leaves HEAD at
|
|
@@ -18270,7 +18352,9 @@ var ReplayService = class {
|
|
|
18270
18352
|
if (baseChanged) repointed++;
|
|
18271
18353
|
continue;
|
|
18272
18354
|
}
|
|
18273
|
-
const
|
|
18355
|
+
const rawDiff = await this.git.exec(["diff", currentGenSha, "--", ...patch.files]).catch(() => null);
|
|
18356
|
+
const neutralizedRebase = rawDiff != null && containsPlaceholder(rawDiff) ? await this.computeAutoversionNeutralizedRebase(patch.files, currentGenSha) : null;
|
|
18357
|
+
const diff = neutralizedRebase ? neutralizedRebase.diff : rawDiff;
|
|
18274
18358
|
if (diff === null) continue;
|
|
18275
18359
|
if (!diff.trim()) {
|
|
18276
18360
|
if (hasProtectedFiles) {
|
|
@@ -18326,7 +18410,7 @@ var ReplayService = class {
|
|
|
18326
18410
|
patch.base_generation = currentGenSha;
|
|
18327
18411
|
patch.patch_content = diff;
|
|
18328
18412
|
patch.content_hash = newContentHash;
|
|
18329
|
-
const snapshot = await this.readSnapshotFromDisk(patch.files);
|
|
18413
|
+
const snapshot = neutralizedRebase ? neutralizedRebase.snapshot : await this.readSnapshotFromDisk(patch.files);
|
|
18330
18414
|
const snapshotIsNonEmpty = Object.keys(snapshot).length > 0;
|
|
18331
18415
|
if (snapshotIsNonEmpty) {
|
|
18332
18416
|
patch.theirs_snapshot = snapshot;
|
|
@@ -18366,6 +18450,47 @@ var ReplayService = class {
|
|
|
18366
18450
|
}
|
|
18367
18451
|
return snapshot;
|
|
18368
18452
|
}
|
|
18453
|
+
/**
|
|
18454
|
+
* Autoversion-aware replacement for the naive `git diff currentGen -- files`
|
|
18455
|
+
* + on-disk snapshot used when rebasing a cleanly-applied patch.
|
|
18456
|
+
*
|
|
18457
|
+
* The generation tree (BASE) still carries the placeholder on version lines
|
|
18458
|
+
* (autoversion runs after `[fern-generated]`), but the working tree holds
|
|
18459
|
+
* autoversion's resolved semver. A raw diff/snapshot would fold the
|
|
18460
|
+
* placeholder->semver swap into the customer's patch_content/theirs_snapshot,
|
|
18461
|
+
* pinning a stale version. This re-derives both against a version-neutralized
|
|
18462
|
+
* THEIRS so the pipeline-owned version line never enters the patch.
|
|
18463
|
+
*
|
|
18464
|
+
* Returns null when no file is under autoversion (fixed `--version`, i.e. no
|
|
18465
|
+
* placeholder in the generation tree) — callers keep their existing behavior.
|
|
18466
|
+
*/
|
|
18467
|
+
async computeAutoversionNeutralizedRebase(files, currentGenSha) {
|
|
18468
|
+
const genByFile = /* @__PURE__ */ new Map();
|
|
18469
|
+
let anyPlaceholder = false;
|
|
18470
|
+
for (const file of files) {
|
|
18471
|
+
const gen = await this.git.showFile(currentGenSha, file).catch(() => null);
|
|
18472
|
+
genByFile.set(file, gen);
|
|
18473
|
+
if (containsPlaceholder(gen)) anyPlaceholder = true;
|
|
18474
|
+
}
|
|
18475
|
+
if (!anyPlaceholder) return null;
|
|
18476
|
+
const snapshot = {};
|
|
18477
|
+
const fragments = [];
|
|
18478
|
+
for (const file of files) {
|
|
18479
|
+
if (isBinaryFile(file)) continue;
|
|
18480
|
+
const disk = await (0, import_promises6.readFile)((0, import_node_path10.join)(this.outputDir, file), "utf-8").catch(() => null);
|
|
18481
|
+
if (disk == null) continue;
|
|
18482
|
+
const gen = genByFile.get(file) ?? null;
|
|
18483
|
+
const neutralized = gen != null ? neutralizeAutoversionTheirs(gen, disk) : disk;
|
|
18484
|
+
snapshot[file] = neutralized;
|
|
18485
|
+
if (gen == null) {
|
|
18486
|
+
fragments.push(synthesizeNewFileDiff(file, neutralized));
|
|
18487
|
+
} else if (gen !== neutralized) {
|
|
18488
|
+
const fileDiff = await unifiedDiff(gen, neutralized, file);
|
|
18489
|
+
if (fileDiff.trim()) fragments.push(fileDiff);
|
|
18490
|
+
}
|
|
18491
|
+
}
|
|
18492
|
+
return { diff: fragments.join(""), snapshot };
|
|
18493
|
+
}
|
|
18369
18494
|
/**
|
|
18370
18495
|
* Detects autoversion contamination — returns true when a
|
|
18371
18496
|
* `[fern-autoversion]` commit between `fromSha` and `toSha` touched any
|