@fern-api/replay 0.6.1 → 0.7.0
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 +215 -18
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +189 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -5
- package/dist/index.d.ts +37 -5
- package/dist/index.js +194 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -12935,6 +12935,26 @@ var LockfileManager = class {
|
|
|
12935
12935
|
this.ensureLoaded();
|
|
12936
12936
|
this.lock.patches = [];
|
|
12937
12937
|
}
|
|
12938
|
+
getUnresolvedPatches() {
|
|
12939
|
+
this.ensureLoaded();
|
|
12940
|
+
return this.lock.patches.filter((p) => p.status === "unresolved");
|
|
12941
|
+
}
|
|
12942
|
+
getResolvingPatches() {
|
|
12943
|
+
this.ensureLoaded();
|
|
12944
|
+
return this.lock.patches.filter((p) => p.status === "resolving");
|
|
12945
|
+
}
|
|
12946
|
+
markPatchUnresolved(patchId) {
|
|
12947
|
+
this.updatePatch(patchId, { status: "unresolved" });
|
|
12948
|
+
}
|
|
12949
|
+
markPatchResolved(patchId, updates) {
|
|
12950
|
+
this.ensureLoaded();
|
|
12951
|
+
const patch = this.lock.patches.find((p) => p.id === patchId);
|
|
12952
|
+
if (!patch) {
|
|
12953
|
+
throw new Error(`Patch not found: ${patchId}`);
|
|
12954
|
+
}
|
|
12955
|
+
delete patch.status;
|
|
12956
|
+
Object.assign(patch, updates);
|
|
12957
|
+
}
|
|
12938
12958
|
getPatches() {
|
|
12939
12959
|
this.ensureLoaded();
|
|
12940
12960
|
return this.lock.patches;
|
|
@@ -15206,12 +15226,12 @@ CLI Version: ${options.cliVersion}`;
|
|
|
15206
15226
|
await this.git.exec(["commit", "-m", fullMessage]);
|
|
15207
15227
|
return (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
15208
15228
|
}
|
|
15209
|
-
async commitReplay(
|
|
15229
|
+
async commitReplay(_patchCount, patches, message) {
|
|
15210
15230
|
await this.stageAll();
|
|
15211
15231
|
if (!await this.hasStagedChanges()) {
|
|
15212
15232
|
return (await this.git.exec(["rev-parse", "HEAD"])).trim();
|
|
15213
15233
|
}
|
|
15214
|
-
let fullMessage = `[fern-replay] Applied
|
|
15234
|
+
let fullMessage = message ?? `[fern-replay] Applied customizations`;
|
|
15215
15235
|
if (patches && patches.length > 0) {
|
|
15216
15236
|
fullMessage += "\n\nPatches replayed:";
|
|
15217
15237
|
for (const patch of patches) {
|
|
@@ -15302,13 +15322,27 @@ var ReplayService = class {
|
|
|
15302
15322
|
this.lockManager.initializeInMemory(record);
|
|
15303
15323
|
} else {
|
|
15304
15324
|
this.lockManager.read();
|
|
15325
|
+
const unresolvedPatches = [
|
|
15326
|
+
...this.lockManager.getUnresolvedPatches(),
|
|
15327
|
+
...this.lockManager.getResolvingPatches()
|
|
15328
|
+
];
|
|
15305
15329
|
this.lockManager.addGeneration(record);
|
|
15306
15330
|
this.lockManager.clearPatches();
|
|
15331
|
+
for (const patch of unresolvedPatches) {
|
|
15332
|
+
this.lockManager.addPatch(patch);
|
|
15333
|
+
}
|
|
15307
15334
|
}
|
|
15308
15335
|
this.lockManager.save();
|
|
15309
15336
|
try {
|
|
15310
15337
|
const redetectedPatches = await this.detector.detectNewPatches();
|
|
15311
15338
|
if (redetectedPatches.length > 0) {
|
|
15339
|
+
const redetectedFiles = new Set(redetectedPatches.flatMap((p) => p.files));
|
|
15340
|
+
const currentPatches = this.lockManager.getPatches();
|
|
15341
|
+
for (const patch of currentPatches) {
|
|
15342
|
+
if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
|
|
15343
|
+
this.lockManager.removePatch(patch.id);
|
|
15344
|
+
}
|
|
15345
|
+
}
|
|
15312
15346
|
for (const patch of redetectedPatches) {
|
|
15313
15347
|
this.lockManager.addPatch(patch);
|
|
15314
15348
|
}
|
|
@@ -15416,6 +15450,12 @@ var ReplayService = class {
|
|
|
15416
15450
|
let results = [];
|
|
15417
15451
|
if (newPatches.length > 0) {
|
|
15418
15452
|
results = await this.applicator.applyPatches(newPatches);
|
|
15453
|
+
this.revertConflictingFiles(results);
|
|
15454
|
+
for (const result of results) {
|
|
15455
|
+
if (result.status === "conflict") {
|
|
15456
|
+
result.patch.status = "unresolved";
|
|
15457
|
+
}
|
|
15458
|
+
}
|
|
15419
15459
|
}
|
|
15420
15460
|
const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
|
|
15421
15461
|
for (const patch of newPatches) {
|
|
@@ -15426,10 +15466,8 @@ var ReplayService = class {
|
|
|
15426
15466
|
this.lockManager.save();
|
|
15427
15467
|
if (newPatches.length > 0) {
|
|
15428
15468
|
if (!options?.stageOnly) {
|
|
15429
|
-
const appliedCount = results.filter((r) => r.status === "applied"
|
|
15430
|
-
|
|
15431
|
-
await this.committer.commitReplay(appliedCount, newPatches);
|
|
15432
|
-
}
|
|
15469
|
+
const appliedCount = results.filter((r) => r.status === "applied").length;
|
|
15470
|
+
await this.committer.commitReplay(appliedCount, newPatches);
|
|
15433
15471
|
} else {
|
|
15434
15472
|
await this.committer.stageAll();
|
|
15435
15473
|
}
|
|
@@ -15477,20 +15515,32 @@ var ReplayService = class {
|
|
|
15477
15515
|
const genRecord = await this.committer.createGenerationRecord(commitOpts);
|
|
15478
15516
|
this.lockManager.addGeneration(genRecord);
|
|
15479
15517
|
const results = await this.applicator.applyPatches(allPatches);
|
|
15518
|
+
this.revertConflictingFiles(results);
|
|
15519
|
+
for (const result of results) {
|
|
15520
|
+
if (result.status === "conflict") {
|
|
15521
|
+
result.patch.status = "unresolved";
|
|
15522
|
+
}
|
|
15523
|
+
}
|
|
15480
15524
|
const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
|
|
15481
15525
|
for (const patch of newPatches) {
|
|
15482
15526
|
if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
|
|
15483
15527
|
this.lockManager.addPatch(patch);
|
|
15484
15528
|
}
|
|
15485
15529
|
}
|
|
15530
|
+
for (const result of results) {
|
|
15531
|
+
if (result.status === "conflict") {
|
|
15532
|
+
try {
|
|
15533
|
+
this.lockManager.markPatchUnresolved(result.patch.id);
|
|
15534
|
+
} catch {
|
|
15535
|
+
}
|
|
15536
|
+
}
|
|
15537
|
+
}
|
|
15486
15538
|
this.lockManager.save();
|
|
15487
15539
|
if (options?.stageOnly) {
|
|
15488
15540
|
await this.committer.stageAll();
|
|
15489
15541
|
} else {
|
|
15490
|
-
const appliedCount = results.filter((r) => r.status === "applied"
|
|
15491
|
-
|
|
15492
|
-
await this.committer.commitReplay(appliedCount, allPatches);
|
|
15493
|
-
}
|
|
15542
|
+
const appliedCount = results.filter((r) => r.status === "applied").length;
|
|
15543
|
+
await this.committer.commitReplay(appliedCount, allPatches);
|
|
15494
15544
|
}
|
|
15495
15545
|
return this.buildReport(
|
|
15496
15546
|
"normal-regeneration",
|
|
@@ -15638,6 +15688,10 @@ var ReplayService = class {
|
|
|
15638
15688
|
let conflictAbsorbed = 0;
|
|
15639
15689
|
let contentRefreshed = 0;
|
|
15640
15690
|
for (const patch of patches) {
|
|
15691
|
+
if (patch.status != null) {
|
|
15692
|
+
delete patch.status;
|
|
15693
|
+
continue;
|
|
15694
|
+
}
|
|
15641
15695
|
if (patch.base_generation === currentGen) {
|
|
15642
15696
|
try {
|
|
15643
15697
|
const diff = await this.git.exec(["diff", currentGen, "HEAD", "--", ...patch.files]).catch(() => null);
|
|
@@ -15688,6 +15742,55 @@ var ReplayService = class {
|
|
|
15688
15742
|
}
|
|
15689
15743
|
return { conflictResolved, conflictAbsorbed, contentRefreshed };
|
|
15690
15744
|
}
|
|
15745
|
+
/**
|
|
15746
|
+
* Strip conflict markers from file content, keeping the OURS (Generated) side.
|
|
15747
|
+
* Preserves clean patches' non-conflicting changes on shared files.
|
|
15748
|
+
*/
|
|
15749
|
+
stripConflictMarkers(content) {
|
|
15750
|
+
const lines = content.split("\n");
|
|
15751
|
+
const result = [];
|
|
15752
|
+
let inConflict = false;
|
|
15753
|
+
let inOurs = false;
|
|
15754
|
+
for (const line of lines) {
|
|
15755
|
+
if (line.startsWith("<<<<<<< ")) {
|
|
15756
|
+
inConflict = true;
|
|
15757
|
+
inOurs = true;
|
|
15758
|
+
continue;
|
|
15759
|
+
}
|
|
15760
|
+
if (inConflict && line === "=======") {
|
|
15761
|
+
inOurs = false;
|
|
15762
|
+
continue;
|
|
15763
|
+
}
|
|
15764
|
+
if (inConflict && line.startsWith(">>>>>>> ")) {
|
|
15765
|
+
inConflict = false;
|
|
15766
|
+
inOurs = false;
|
|
15767
|
+
continue;
|
|
15768
|
+
}
|
|
15769
|
+
if (!inConflict || inOurs) {
|
|
15770
|
+
result.push(line);
|
|
15771
|
+
}
|
|
15772
|
+
}
|
|
15773
|
+
return result.join("\n");
|
|
15774
|
+
}
|
|
15775
|
+
/**
|
|
15776
|
+
* After applyPatches(), strip conflict markers from conflicting files
|
|
15777
|
+
* so only clean content is committed. Keeps the Generated (OURS) side.
|
|
15778
|
+
*/
|
|
15779
|
+
revertConflictingFiles(results) {
|
|
15780
|
+
for (const result of results) {
|
|
15781
|
+
if (result.status !== "conflict" || !result.fileResults) continue;
|
|
15782
|
+
for (const fileResult of result.fileResults) {
|
|
15783
|
+
if (fileResult.status !== "conflict") continue;
|
|
15784
|
+
const filePath = (0, import_node_path4.join)(this.outputDir, fileResult.file);
|
|
15785
|
+
try {
|
|
15786
|
+
const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
|
|
15787
|
+
const stripped = this.stripConflictMarkers(content);
|
|
15788
|
+
(0, import_node_fs2.writeFileSync)(filePath, stripped);
|
|
15789
|
+
} catch {
|
|
15790
|
+
}
|
|
15791
|
+
}
|
|
15792
|
+
}
|
|
15793
|
+
}
|
|
15691
15794
|
readFernignorePatterns() {
|
|
15692
15795
|
const fernignorePath = (0, import_node_path4.join)(this.outputDir, ".fernignore");
|
|
15693
15796
|
if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];
|
|
@@ -15722,6 +15825,12 @@ var ReplayService = class {
|
|
|
15722
15825
|
patchesRefreshed: preRebaseCounts && preRebaseCounts.contentRefreshed > 0 ? preRebaseCounts.contentRefreshed : void 0,
|
|
15723
15826
|
conflicts: conflictResults.flatMap((r) => r.fileResults?.filter((f) => f.status === "conflict") ?? []),
|
|
15724
15827
|
conflictDetails: conflictDetails.length > 0 ? conflictDetails : void 0,
|
|
15828
|
+
unresolvedPatches: conflictResults.length > 0 ? conflictResults.map((r) => ({
|
|
15829
|
+
patchId: r.patch.id,
|
|
15830
|
+
patchMessage: r.patch.original_message,
|
|
15831
|
+
files: r.patch.files,
|
|
15832
|
+
conflictDetails: r.fileResults?.filter((f) => f.status === "conflict") ?? []
|
|
15833
|
+
})) : void 0,
|
|
15725
15834
|
wouldApply: options?.dryRun ? patches : void 0,
|
|
15726
15835
|
warnings: warnings && warnings.length > 0 ? warnings : void 0
|
|
15727
15836
|
};
|
|
@@ -16181,17 +16290,82 @@ async function resolve(outputDir, options) {
|
|
|
16181
16290
|
return { success: false, reason: "no-patches" };
|
|
16182
16291
|
}
|
|
16183
16292
|
const git = new GitClient(outputDir);
|
|
16293
|
+
const unresolvedPatches = lockManager.getUnresolvedPatches();
|
|
16294
|
+
const resolvingPatches = lockManager.getResolvingPatches();
|
|
16295
|
+
if (unresolvedPatches.length > 0) {
|
|
16296
|
+
const applicator = new ReplayApplicator(git, lockManager, outputDir);
|
|
16297
|
+
await applicator.applyPatches(unresolvedPatches);
|
|
16298
|
+
const markerFiles = await findConflictMarkerFiles(git);
|
|
16299
|
+
if (markerFiles.length > 0) {
|
|
16300
|
+
for (const patch of unresolvedPatches) {
|
|
16301
|
+
lockManager.updatePatch(patch.id, { status: "resolving" });
|
|
16302
|
+
}
|
|
16303
|
+
lockManager.save();
|
|
16304
|
+
return {
|
|
16305
|
+
success: false,
|
|
16306
|
+
reason: "conflicts-applied",
|
|
16307
|
+
unresolvedFiles: markerFiles,
|
|
16308
|
+
phase: "applied",
|
|
16309
|
+
patchesApplied: unresolvedPatches.length
|
|
16310
|
+
};
|
|
16311
|
+
}
|
|
16312
|
+
}
|
|
16184
16313
|
if (options?.checkMarkers !== false) {
|
|
16185
|
-
const
|
|
16186
|
-
if (
|
|
16187
|
-
|
|
16188
|
-
return { success: false, reason: "unresolved-conflicts", unresolvedFiles: files };
|
|
16314
|
+
const currentMarkerFiles = await findConflictMarkerFiles(git);
|
|
16315
|
+
if (currentMarkerFiles.length > 0) {
|
|
16316
|
+
return { success: false, reason: "unresolved-conflicts", unresolvedFiles: currentMarkerFiles };
|
|
16189
16317
|
}
|
|
16190
16318
|
}
|
|
16319
|
+
const patchesToCommit = [...resolvingPatches, ...unresolvedPatches];
|
|
16320
|
+
if (patchesToCommit.length > 0) {
|
|
16321
|
+
const currentGen = lock.current_generation;
|
|
16322
|
+
const detector = new ReplayDetector(git, lockManager, outputDir);
|
|
16323
|
+
let patchesResolved = 0;
|
|
16324
|
+
for (const patch of patchesToCommit) {
|
|
16325
|
+
const diff = await git.exec(["diff", currentGen, "--", ...patch.files]).catch(() => null);
|
|
16326
|
+
if (!diff || !diff.trim()) {
|
|
16327
|
+
lockManager.removePatch(patch.id);
|
|
16328
|
+
continue;
|
|
16329
|
+
}
|
|
16330
|
+
const newContentHash = detector.computeContentHash(diff);
|
|
16331
|
+
const changedFiles = await getChangedFiles(git, currentGen, patch.files);
|
|
16332
|
+
lockManager.markPatchResolved(patch.id, {
|
|
16333
|
+
patch_content: diff,
|
|
16334
|
+
content_hash: newContentHash,
|
|
16335
|
+
base_generation: currentGen,
|
|
16336
|
+
files: changedFiles
|
|
16337
|
+
});
|
|
16338
|
+
patchesResolved++;
|
|
16339
|
+
}
|
|
16340
|
+
lockManager.save();
|
|
16341
|
+
const committer2 = new ReplayCommitter(git, outputDir);
|
|
16342
|
+
await committer2.stageAll();
|
|
16343
|
+
const commitSha2 = await committer2.commitReplay(
|
|
16344
|
+
lock.patches.length,
|
|
16345
|
+
lock.patches,
|
|
16346
|
+
"[fern-replay] Resolved conflicts"
|
|
16347
|
+
);
|
|
16348
|
+
return {
|
|
16349
|
+
success: true,
|
|
16350
|
+
commitSha: commitSha2,
|
|
16351
|
+
phase: "committed",
|
|
16352
|
+
patchesResolved
|
|
16353
|
+
};
|
|
16354
|
+
}
|
|
16191
16355
|
const committer = new ReplayCommitter(git, outputDir);
|
|
16192
16356
|
await committer.stageAll();
|
|
16193
16357
|
const commitSha = await committer.commitReplay(lock.patches.length, lock.patches);
|
|
16194
|
-
return { success: true, commitSha };
|
|
16358
|
+
return { success: true, commitSha, phase: "committed" };
|
|
16359
|
+
}
|
|
16360
|
+
async function findConflictMarkerFiles(git) {
|
|
16361
|
+
const output = await git.exec(["grep", "-l", "<<<<<<<", "--", "."]).catch(() => "");
|
|
16362
|
+
return output.trim() ? output.trim().split("\n").filter(Boolean) : [];
|
|
16363
|
+
}
|
|
16364
|
+
async function getChangedFiles(git, currentGen, files) {
|
|
16365
|
+
const filesOutput = await git.exec(["diff", "--name-only", currentGen, "--", ...files]).catch(() => null);
|
|
16366
|
+
if (!filesOutput || !filesOutput.trim()) return files;
|
|
16367
|
+
const changed = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !f.startsWith(".fern/"));
|
|
16368
|
+
return changed.length > 0 ? changed : files;
|
|
16195
16369
|
}
|
|
16196
16370
|
|
|
16197
16371
|
// src/cli.ts
|
|
@@ -16449,14 +16623,23 @@ function printReport(report) {
|
|
|
16449
16623
|
}
|
|
16450
16624
|
}
|
|
16451
16625
|
}
|
|
16452
|
-
console.log("\
|
|
16453
|
-
console.log("Or run: fern-replay forget <file-pattern> to remove problematic patches");
|
|
16626
|
+
console.log("\nOr run: fern-replay forget <file-pattern> to remove problematic patches");
|
|
16454
16627
|
} else if (report.conflicts.length > 0) {
|
|
16455
16628
|
console.log("\nConflicts:");
|
|
16456
16629
|
for (const conflict of report.conflicts) {
|
|
16457
16630
|
console.log(` ${conflict.file}: ${conflict.reason ?? "merge conflict"}`);
|
|
16458
16631
|
}
|
|
16459
16632
|
}
|
|
16633
|
+
if (report.unresolvedPatches && report.unresolvedPatches.length > 0) {
|
|
16634
|
+
console.log("\nUnresolved Patches (need local resolution):");
|
|
16635
|
+
for (const patch of report.unresolvedPatches) {
|
|
16636
|
+
console.log(` Patch: ${patch.patchId} - "${patch.patchMessage}"`);
|
|
16637
|
+
for (const file of patch.files) {
|
|
16638
|
+
console.log(` ${file}`);
|
|
16639
|
+
}
|
|
16640
|
+
}
|
|
16641
|
+
console.log("\nRun 'fern-replay resolve <dir>' on this branch to resolve conflicts.");
|
|
16642
|
+
}
|
|
16460
16643
|
if (report.warnings && report.warnings.length > 0) {
|
|
16461
16644
|
console.log("\nWarnings:");
|
|
16462
16645
|
for (const w of report.warnings) console.log(` ${w}`);
|
|
@@ -16572,6 +16755,17 @@ async function runResolve(dir, flags) {
|
|
|
16572
16755
|
case "no-patches":
|
|
16573
16756
|
console.error("No patches in lockfile. Nothing to resolve.");
|
|
16574
16757
|
break;
|
|
16758
|
+
case "conflicts-applied":
|
|
16759
|
+
console.log(`Applied ${result.patchesApplied} unresolved patch(es) to working tree.`);
|
|
16760
|
+
console.log(`
|
|
16761
|
+
${result.unresolvedFiles.length} file(s) have conflicts:`);
|
|
16762
|
+
for (const f of result.unresolvedFiles) {
|
|
16763
|
+
console.log(` ${f}`);
|
|
16764
|
+
}
|
|
16765
|
+
console.log(`
|
|
16766
|
+
Resolve the conflicts in your editor, then run 'fern-replay resolve ${dir}' again.`);
|
|
16767
|
+
return;
|
|
16768
|
+
// Normal flow, don't exit(1)
|
|
16575
16769
|
case "unresolved-conflicts":
|
|
16576
16770
|
console.error("Conflict markers still present in:");
|
|
16577
16771
|
for (const f of result.unresolvedFiles ?? []) {
|
|
@@ -16585,8 +16779,11 @@ async function runResolve(dir, flags) {
|
|
|
16585
16779
|
}
|
|
16586
16780
|
process.exit(1);
|
|
16587
16781
|
}
|
|
16782
|
+
if (result.patchesResolved) {
|
|
16783
|
+
console.log(`Resolution complete. Updated ${result.patchesResolved} patch(es) in lockfile.`);
|
|
16784
|
+
}
|
|
16588
16785
|
console.log(`Created [fern-replay] commit: ${result.commitSha?.slice(0, 7)}`);
|
|
16589
|
-
console.log("
|
|
16786
|
+
console.log("Push to update the PR.");
|
|
16590
16787
|
}
|
|
16591
16788
|
async function main() {
|
|
16592
16789
|
const { command, dir, flags, pattern } = parseArgs(process.argv);
|