@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/index.d.cts CHANGED
@@ -23,6 +23,10 @@ interface StoredPatch {
23
23
  base_generation: string;
24
24
  files: string[];
25
25
  patch_content: string;
26
+ /** When "unresolved", the patch conflicted during CI and needs local resolution.
27
+ * When "resolving", the resolve command has applied patches to the working tree
28
+ * and is waiting for the customer to resolve conflicts and run resolve again. */
29
+ status?: "unresolved" | "resolving";
26
30
  }
27
31
  interface CustomizationsConfig {
28
32
  exclude?: string[];
@@ -123,9 +127,13 @@ declare class LockfileManager {
123
127
  save(): void;
124
128
  addGeneration(record: GenerationRecord): void;
125
129
  addPatch(patch: StoredPatch): void;
126
- updatePatch(patchId: string, updates: Partial<Pick<StoredPatch, "base_generation" | "patch_content" | "content_hash" | "files">>): void;
130
+ updatePatch(patchId: string, updates: Partial<Pick<StoredPatch, "base_generation" | "patch_content" | "content_hash" | "files" | "status">>): void;
127
131
  removePatch(patchId: string): void;
128
132
  clearPatches(): void;
133
+ getUnresolvedPatches(): StoredPatch[];
134
+ getResolvingPatches(): StoredPatch[];
135
+ markPatchUnresolved(patchId: string): void;
136
+ markPatchResolved(patchId: string, updates: Pick<StoredPatch, "patch_content" | "content_hash" | "base_generation" | "files">): void;
129
137
  getPatches(): StoredPatch[];
130
138
  setReplaySkippedAt(timestamp: string): void;
131
139
  clearReplaySkippedAt(): void;
@@ -201,7 +209,7 @@ declare class ReplayCommitter {
201
209
  private outputDir;
202
210
  constructor(git: GitClient, outputDir: string);
203
211
  commitGeneration(message: string, options?: CommitOptions): Promise<string>;
204
- commitReplay(patchCount: number, patches?: StoredPatch[]): Promise<string>;
212
+ commitReplay(_patchCount: number, patches?: StoredPatch[], message?: string): Promise<string>;
205
213
  createGenerationRecord(options?: CommitOptions): Promise<GenerationRecord>;
206
214
  stageAll(): Promise<void>;
207
215
  hasStagedChanges(): Promise<boolean>;
@@ -216,6 +224,12 @@ interface ConflictDetail {
216
224
  /** Files that applied cleanly in a patch that also had conflicts. */
217
225
  cleanFiles?: string[];
218
226
  }
227
+ interface UnresolvedPatchInfo {
228
+ patchId: string;
229
+ patchMessage: string;
230
+ files: string[];
231
+ conflictDetails: FileResult[];
232
+ }
219
233
  interface ReplayReport {
220
234
  flow: "first-generation" | "no-patches" | "normal-regeneration" | "skip-application";
221
235
  patchesDetected: number;
@@ -231,6 +245,8 @@ interface ReplayReport {
231
245
  patchesRefreshed?: number;
232
246
  conflicts: FileResult[];
233
247
  conflictDetails?: ConflictDetail[];
248
+ /** Patches that conflicted and need local resolution via `fern-replay resolve` */
249
+ unresolvedPatches?: UnresolvedPatchInfo[];
234
250
  wouldApply?: StoredPatch[];
235
251
  warnings?: string[];
236
252
  }
@@ -303,6 +319,16 @@ declare class ReplayService {
303
319
  * Called BEFORE commitGeneration() while HEAD has customer code.
304
320
  */
305
321
  private preGenerationRebase;
322
+ /**
323
+ * Strip conflict markers from file content, keeping the OURS (Generated) side.
324
+ * Preserves clean patches' non-conflicting changes on shared files.
325
+ */
326
+ private stripConflictMarkers;
327
+ /**
328
+ * After applyPatches(), strip conflict markers from conflicting files
329
+ * so only clean content is committed. Keeps the Generated (OURS) side.
330
+ */
331
+ private revertConflictingFiles;
306
332
  private readFernignorePatterns;
307
333
  private buildReport;
308
334
  }
@@ -427,10 +453,16 @@ interface ResolveResult {
427
453
  success: boolean;
428
454
  /** Commit SHA of the [fern-replay] commit, if created */
429
455
  commitSha?: string;
430
- /** Reason for failure */
456
+ /** Reason for failure or current state */
431
457
  reason?: string;
432
- /** Files that still have conflict markers */
458
+ /** Files that have conflict markers */
433
459
  unresolvedFiles?: string[];
460
+ /** Phase the command executed */
461
+ phase?: "applied" | "committed" | "nothing-to-resolve";
462
+ /** Number of patches applied to working tree (phase 1) */
463
+ patchesApplied?: number;
464
+ /** Number of patches resolved and committed (phase 2) */
465
+ patchesResolved?: number;
434
466
  }
435
467
  declare function resolve(outputDir: string, options?: ResolveOptions): Promise<ResolveResult>;
436
468
 
@@ -460,4 +492,4 @@ interface StatusGeneration {
460
492
  }
461
493
  declare function status(outputDir: string): StatusResult;
462
494
 
463
- export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, type MergeResult, type MigrationAnalysis, type MigrationResult, type MoveDeclaration, ReplayApplicator, ReplayCommitter, type ReplayConfig, ReplayDetector, type ReplayOptions, type ReplayReport, type ReplayResult, ReplayService, type ResetOptions, type ResetResult, type ResolveOptions, type ResolveResult, type StatusGeneration, type StatusPatch, type StatusResult, type StoredPatch, bootstrap, forget, isGenerationCommit, isReplayCommit, reset, resolve, status, threeWayMerge };
495
+ export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, type MergeResult, type MigrationAnalysis, type MigrationResult, type MoveDeclaration, ReplayApplicator, ReplayCommitter, type ReplayConfig, ReplayDetector, type ReplayOptions, type ReplayReport, type ReplayResult, ReplayService, type ResetOptions, type ResetResult, type ResolveOptions, type ResolveResult, type StatusGeneration, type StatusPatch, type StatusResult, type StoredPatch, type UnresolvedPatchInfo, bootstrap, forget, isGenerationCommit, isReplayCommit, reset, resolve, status, threeWayMerge };
package/dist/index.d.ts CHANGED
@@ -23,6 +23,10 @@ interface StoredPatch {
23
23
  base_generation: string;
24
24
  files: string[];
25
25
  patch_content: string;
26
+ /** When "unresolved", the patch conflicted during CI and needs local resolution.
27
+ * When "resolving", the resolve command has applied patches to the working tree
28
+ * and is waiting for the customer to resolve conflicts and run resolve again. */
29
+ status?: "unresolved" | "resolving";
26
30
  }
27
31
  interface CustomizationsConfig {
28
32
  exclude?: string[];
@@ -123,9 +127,13 @@ declare class LockfileManager {
123
127
  save(): void;
124
128
  addGeneration(record: GenerationRecord): void;
125
129
  addPatch(patch: StoredPatch): void;
126
- updatePatch(patchId: string, updates: Partial<Pick<StoredPatch, "base_generation" | "patch_content" | "content_hash" | "files">>): void;
130
+ updatePatch(patchId: string, updates: Partial<Pick<StoredPatch, "base_generation" | "patch_content" | "content_hash" | "files" | "status">>): void;
127
131
  removePatch(patchId: string): void;
128
132
  clearPatches(): void;
133
+ getUnresolvedPatches(): StoredPatch[];
134
+ getResolvingPatches(): StoredPatch[];
135
+ markPatchUnresolved(patchId: string): void;
136
+ markPatchResolved(patchId: string, updates: Pick<StoredPatch, "patch_content" | "content_hash" | "base_generation" | "files">): void;
129
137
  getPatches(): StoredPatch[];
130
138
  setReplaySkippedAt(timestamp: string): void;
131
139
  clearReplaySkippedAt(): void;
@@ -201,7 +209,7 @@ declare class ReplayCommitter {
201
209
  private outputDir;
202
210
  constructor(git: GitClient, outputDir: string);
203
211
  commitGeneration(message: string, options?: CommitOptions): Promise<string>;
204
- commitReplay(patchCount: number, patches?: StoredPatch[]): Promise<string>;
212
+ commitReplay(_patchCount: number, patches?: StoredPatch[], message?: string): Promise<string>;
205
213
  createGenerationRecord(options?: CommitOptions): Promise<GenerationRecord>;
206
214
  stageAll(): Promise<void>;
207
215
  hasStagedChanges(): Promise<boolean>;
@@ -216,6 +224,12 @@ interface ConflictDetail {
216
224
  /** Files that applied cleanly in a patch that also had conflicts. */
217
225
  cleanFiles?: string[];
218
226
  }
227
+ interface UnresolvedPatchInfo {
228
+ patchId: string;
229
+ patchMessage: string;
230
+ files: string[];
231
+ conflictDetails: FileResult[];
232
+ }
219
233
  interface ReplayReport {
220
234
  flow: "first-generation" | "no-patches" | "normal-regeneration" | "skip-application";
221
235
  patchesDetected: number;
@@ -231,6 +245,8 @@ interface ReplayReport {
231
245
  patchesRefreshed?: number;
232
246
  conflicts: FileResult[];
233
247
  conflictDetails?: ConflictDetail[];
248
+ /** Patches that conflicted and need local resolution via `fern-replay resolve` */
249
+ unresolvedPatches?: UnresolvedPatchInfo[];
234
250
  wouldApply?: StoredPatch[];
235
251
  warnings?: string[];
236
252
  }
@@ -303,6 +319,16 @@ declare class ReplayService {
303
319
  * Called BEFORE commitGeneration() while HEAD has customer code.
304
320
  */
305
321
  private preGenerationRebase;
322
+ /**
323
+ * Strip conflict markers from file content, keeping the OURS (Generated) side.
324
+ * Preserves clean patches' non-conflicting changes on shared files.
325
+ */
326
+ private stripConflictMarkers;
327
+ /**
328
+ * After applyPatches(), strip conflict markers from conflicting files
329
+ * so only clean content is committed. Keeps the Generated (OURS) side.
330
+ */
331
+ private revertConflictingFiles;
306
332
  private readFernignorePatterns;
307
333
  private buildReport;
308
334
  }
@@ -427,10 +453,16 @@ interface ResolveResult {
427
453
  success: boolean;
428
454
  /** Commit SHA of the [fern-replay] commit, if created */
429
455
  commitSha?: string;
430
- /** Reason for failure */
456
+ /** Reason for failure or current state */
431
457
  reason?: string;
432
- /** Files that still have conflict markers */
458
+ /** Files that have conflict markers */
433
459
  unresolvedFiles?: string[];
460
+ /** Phase the command executed */
461
+ phase?: "applied" | "committed" | "nothing-to-resolve";
462
+ /** Number of patches applied to working tree (phase 1) */
463
+ patchesApplied?: number;
464
+ /** Number of patches resolved and committed (phase 2) */
465
+ patchesResolved?: number;
434
466
  }
435
467
  declare function resolve(outputDir: string, options?: ResolveOptions): Promise<ResolveResult>;
436
468
 
@@ -460,4 +492,4 @@ interface StatusGeneration {
460
492
  }
461
493
  declare function status(outputDir: string): StatusResult;
462
494
 
463
- export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, type MergeResult, type MigrationAnalysis, type MigrationResult, type MoveDeclaration, ReplayApplicator, ReplayCommitter, type ReplayConfig, ReplayDetector, type ReplayOptions, type ReplayReport, type ReplayResult, ReplayService, type ResetOptions, type ResetResult, type ResolveOptions, type ResolveResult, type StatusGeneration, type StatusPatch, type StatusResult, type StoredPatch, bootstrap, forget, isGenerationCommit, isReplayCommit, reset, resolve, status, threeWayMerge };
495
+ export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, type MergeResult, type MigrationAnalysis, type MigrationResult, type MoveDeclaration, ReplayApplicator, ReplayCommitter, type ReplayConfig, ReplayDetector, type ReplayOptions, type ReplayReport, type ReplayResult, ReplayService, type ResetOptions, type ResetResult, type ResolveOptions, type ResolveResult, type StatusGeneration, type StatusPatch, type StatusResult, type StoredPatch, type UnresolvedPatchInfo, bootstrap, forget, isGenerationCommit, isReplayCommit, reset, resolve, status, threeWayMerge };
package/dist/index.js CHANGED
@@ -229,6 +229,26 @@ var LockfileManager = class {
229
229
  this.ensureLoaded();
230
230
  this.lock.patches = [];
231
231
  }
232
+ getUnresolvedPatches() {
233
+ this.ensureLoaded();
234
+ return this.lock.patches.filter((p) => p.status === "unresolved");
235
+ }
236
+ getResolvingPatches() {
237
+ this.ensureLoaded();
238
+ return this.lock.patches.filter((p) => p.status === "resolving");
239
+ }
240
+ markPatchUnresolved(patchId) {
241
+ this.updatePatch(patchId, { status: "unresolved" });
242
+ }
243
+ markPatchResolved(patchId, updates) {
244
+ this.ensureLoaded();
245
+ const patch = this.lock.patches.find((p) => p.id === patchId);
246
+ if (!patch) {
247
+ throw new Error(`Patch not found: ${patchId}`);
248
+ }
249
+ delete patch.status;
250
+ Object.assign(patch, updates);
251
+ }
232
252
  getPatches() {
233
253
  this.ensureLoaded();
234
254
  return this.lock.patches;
@@ -929,12 +949,12 @@ CLI Version: ${options.cliVersion}`;
929
949
  await this.git.exec(["commit", "-m", fullMessage]);
930
950
  return (await this.git.exec(["rev-parse", "HEAD"])).trim();
931
951
  }
932
- async commitReplay(patchCount, patches) {
952
+ async commitReplay(_patchCount, patches, message) {
933
953
  await this.stageAll();
934
954
  if (!await this.hasStagedChanges()) {
935
955
  return (await this.git.exec(["rev-parse", "HEAD"])).trim();
936
956
  }
937
- let fullMessage = `[fern-replay] Applied ${patchCount} customization(s)`;
957
+ let fullMessage = message ?? `[fern-replay] Applied customizations`;
938
958
  if (patches && patches.length > 0) {
939
959
  fullMessage += "\n\nPatches replayed:";
940
960
  for (const patch of patches) {
@@ -971,7 +991,7 @@ CLI Version: ${options.cliVersion}`;
971
991
 
972
992
  // src/ReplayService.ts
973
993
  init_GitClient();
974
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
994
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
975
995
  import { join as join3 } from "path";
976
996
  import { minimatch as minimatch2 } from "minimatch";
977
997
  var ReplayService = class {
@@ -1029,13 +1049,27 @@ var ReplayService = class {
1029
1049
  this.lockManager.initializeInMemory(record);
1030
1050
  } else {
1031
1051
  this.lockManager.read();
1052
+ const unresolvedPatches = [
1053
+ ...this.lockManager.getUnresolvedPatches(),
1054
+ ...this.lockManager.getResolvingPatches()
1055
+ ];
1032
1056
  this.lockManager.addGeneration(record);
1033
1057
  this.lockManager.clearPatches();
1058
+ for (const patch of unresolvedPatches) {
1059
+ this.lockManager.addPatch(patch);
1060
+ }
1034
1061
  }
1035
1062
  this.lockManager.save();
1036
1063
  try {
1037
1064
  const redetectedPatches = await this.detector.detectNewPatches();
1038
1065
  if (redetectedPatches.length > 0) {
1066
+ const redetectedFiles = new Set(redetectedPatches.flatMap((p) => p.files));
1067
+ const currentPatches = this.lockManager.getPatches();
1068
+ for (const patch of currentPatches) {
1069
+ if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
1070
+ this.lockManager.removePatch(patch.id);
1071
+ }
1072
+ }
1039
1073
  for (const patch of redetectedPatches) {
1040
1074
  this.lockManager.addPatch(patch);
1041
1075
  }
@@ -1143,6 +1177,12 @@ var ReplayService = class {
1143
1177
  let results = [];
1144
1178
  if (newPatches.length > 0) {
1145
1179
  results = await this.applicator.applyPatches(newPatches);
1180
+ this.revertConflictingFiles(results);
1181
+ for (const result of results) {
1182
+ if (result.status === "conflict") {
1183
+ result.patch.status = "unresolved";
1184
+ }
1185
+ }
1146
1186
  }
1147
1187
  const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
1148
1188
  for (const patch of newPatches) {
@@ -1153,10 +1193,8 @@ var ReplayService = class {
1153
1193
  this.lockManager.save();
1154
1194
  if (newPatches.length > 0) {
1155
1195
  if (!options?.stageOnly) {
1156
- const appliedCount = results.filter((r) => r.status === "applied" || r.status === "conflict").length;
1157
- if (appliedCount > 0) {
1158
- await this.committer.commitReplay(appliedCount, newPatches);
1159
- }
1196
+ const appliedCount = results.filter((r) => r.status === "applied").length;
1197
+ await this.committer.commitReplay(appliedCount, newPatches);
1160
1198
  } else {
1161
1199
  await this.committer.stageAll();
1162
1200
  }
@@ -1204,20 +1242,32 @@ var ReplayService = class {
1204
1242
  const genRecord = await this.committer.createGenerationRecord(commitOpts);
1205
1243
  this.lockManager.addGeneration(genRecord);
1206
1244
  const results = await this.applicator.applyPatches(allPatches);
1245
+ this.revertConflictingFiles(results);
1246
+ for (const result of results) {
1247
+ if (result.status === "conflict") {
1248
+ result.patch.status = "unresolved";
1249
+ }
1250
+ }
1207
1251
  const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
1208
1252
  for (const patch of newPatches) {
1209
1253
  if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
1210
1254
  this.lockManager.addPatch(patch);
1211
1255
  }
1212
1256
  }
1257
+ for (const result of results) {
1258
+ if (result.status === "conflict") {
1259
+ try {
1260
+ this.lockManager.markPatchUnresolved(result.patch.id);
1261
+ } catch {
1262
+ }
1263
+ }
1264
+ }
1213
1265
  this.lockManager.save();
1214
1266
  if (options?.stageOnly) {
1215
1267
  await this.committer.stageAll();
1216
1268
  } else {
1217
- const appliedCount = results.filter((r) => r.status === "applied" || r.status === "conflict").length;
1218
- if (appliedCount > 0) {
1219
- await this.committer.commitReplay(appliedCount, allPatches);
1220
- }
1269
+ const appliedCount = results.filter((r) => r.status === "applied").length;
1270
+ await this.committer.commitReplay(appliedCount, allPatches);
1221
1271
  }
1222
1272
  return this.buildReport(
1223
1273
  "normal-regeneration",
@@ -1365,6 +1415,10 @@ var ReplayService = class {
1365
1415
  let conflictAbsorbed = 0;
1366
1416
  let contentRefreshed = 0;
1367
1417
  for (const patch of patches) {
1418
+ if (patch.status != null) {
1419
+ delete patch.status;
1420
+ continue;
1421
+ }
1368
1422
  if (patch.base_generation === currentGen) {
1369
1423
  try {
1370
1424
  const diff = await this.git.exec(["diff", currentGen, "HEAD", "--", ...patch.files]).catch(() => null);
@@ -1415,6 +1469,55 @@ var ReplayService = class {
1415
1469
  }
1416
1470
  return { conflictResolved, conflictAbsorbed, contentRefreshed };
1417
1471
  }
1472
+ /**
1473
+ * Strip conflict markers from file content, keeping the OURS (Generated) side.
1474
+ * Preserves clean patches' non-conflicting changes on shared files.
1475
+ */
1476
+ stripConflictMarkers(content) {
1477
+ const lines = content.split("\n");
1478
+ const result = [];
1479
+ let inConflict = false;
1480
+ let inOurs = false;
1481
+ for (const line of lines) {
1482
+ if (line.startsWith("<<<<<<< ")) {
1483
+ inConflict = true;
1484
+ inOurs = true;
1485
+ continue;
1486
+ }
1487
+ if (inConflict && line === "=======") {
1488
+ inOurs = false;
1489
+ continue;
1490
+ }
1491
+ if (inConflict && line.startsWith(">>>>>>> ")) {
1492
+ inConflict = false;
1493
+ inOurs = false;
1494
+ continue;
1495
+ }
1496
+ if (!inConflict || inOurs) {
1497
+ result.push(line);
1498
+ }
1499
+ }
1500
+ return result.join("\n");
1501
+ }
1502
+ /**
1503
+ * After applyPatches(), strip conflict markers from conflicting files
1504
+ * so only clean content is committed. Keeps the Generated (OURS) side.
1505
+ */
1506
+ revertConflictingFiles(results) {
1507
+ for (const result of results) {
1508
+ if (result.status !== "conflict" || !result.fileResults) continue;
1509
+ for (const fileResult of result.fileResults) {
1510
+ if (fileResult.status !== "conflict") continue;
1511
+ const filePath = join3(this.outputDir, fileResult.file);
1512
+ try {
1513
+ const content = readFileSync2(filePath, "utf-8");
1514
+ const stripped = this.stripConflictMarkers(content);
1515
+ writeFileSync2(filePath, stripped);
1516
+ } catch {
1517
+ }
1518
+ }
1519
+ }
1520
+ }
1418
1521
  readFernignorePatterns() {
1419
1522
  const fernignorePath = join3(this.outputDir, ".fernignore");
1420
1523
  if (!existsSync2(fernignorePath)) return [];
@@ -1449,6 +1552,12 @@ var ReplayService = class {
1449
1552
  patchesRefreshed: preRebaseCounts && preRebaseCounts.contentRefreshed > 0 ? preRebaseCounts.contentRefreshed : void 0,
1450
1553
  conflicts: conflictResults.flatMap((r) => r.fileResults?.filter((f) => f.status === "conflict") ?? []),
1451
1554
  conflictDetails: conflictDetails.length > 0 ? conflictDetails : void 0,
1555
+ unresolvedPatches: conflictResults.length > 0 ? conflictResults.map((r) => ({
1556
+ patchId: r.patch.id,
1557
+ patchMessage: r.patch.original_message,
1558
+ files: r.patch.files,
1559
+ conflictDetails: r.fileResults?.filter((f) => f.status === "conflict") ?? []
1560
+ })) : void 0,
1452
1561
  wouldApply: options?.dryRun ? patches : void 0,
1453
1562
  warnings: warnings && warnings.length > 0 ? warnings : void 0
1454
1563
  };
@@ -1457,7 +1566,7 @@ var ReplayService = class {
1457
1566
 
1458
1567
  // src/FernignoreMigrator.ts
1459
1568
  import { createHash as createHash2 } from "crypto";
1460
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync2 } from "fs";
1569
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync3 } from "fs";
1461
1570
  import { dirname as dirname3, join as join4 } from "path";
1462
1571
  import { minimatch as minimatch3 } from "minimatch";
1463
1572
  import { parse as parse2, stringify as stringify2 } from "yaml";
@@ -1645,13 +1754,13 @@ var FernignoreMigrator = class {
1645
1754
  if (!existsSync3(dir)) {
1646
1755
  mkdirSync2(dir, { recursive: true });
1647
1756
  }
1648
- writeFileSync2(replayYmlPath, stringify2(config, { lineWidth: 0 }), "utf-8");
1757
+ writeFileSync3(replayYmlPath, stringify2(config, { lineWidth: 0 }), "utf-8");
1649
1758
  }
1650
1759
  };
1651
1760
 
1652
1761
  // src/commands/bootstrap.ts
1653
1762
  import { createHash as createHash3 } from "crypto";
1654
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1763
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
1655
1764
  import { join as join5 } from "path";
1656
1765
  init_GitClient();
1657
1766
  async function bootstrap(outputDir, options) {
@@ -1837,7 +1946,7 @@ function ensureFernignoreEntries(outputDir) {
1837
1946
  content += "\n";
1838
1947
  }
1839
1948
  content += toAdd.join("\n") + "\n";
1840
- writeFileSync3(fernignorePath, content, "utf-8");
1949
+ writeFileSync4(fernignorePath, content, "utf-8");
1841
1950
  return true;
1842
1951
  }
1843
1952
  function computeContentHash(patchContent) {
@@ -1910,17 +2019,82 @@ async function resolve(outputDir, options) {
1910
2019
  return { success: false, reason: "no-patches" };
1911
2020
  }
1912
2021
  const git = new GitClient(outputDir);
2022
+ const unresolvedPatches = lockManager.getUnresolvedPatches();
2023
+ const resolvingPatches = lockManager.getResolvingPatches();
2024
+ if (unresolvedPatches.length > 0) {
2025
+ const applicator = new ReplayApplicator(git, lockManager, outputDir);
2026
+ await applicator.applyPatches(unresolvedPatches);
2027
+ const markerFiles = await findConflictMarkerFiles(git);
2028
+ if (markerFiles.length > 0) {
2029
+ for (const patch of unresolvedPatches) {
2030
+ lockManager.updatePatch(patch.id, { status: "resolving" });
2031
+ }
2032
+ lockManager.save();
2033
+ return {
2034
+ success: false,
2035
+ reason: "conflicts-applied",
2036
+ unresolvedFiles: markerFiles,
2037
+ phase: "applied",
2038
+ patchesApplied: unresolvedPatches.length
2039
+ };
2040
+ }
2041
+ }
1913
2042
  if (options?.checkMarkers !== false) {
1914
- const markerFiles = await git.exec(["grep", "-l", "<<<<<<<", "--", "."]).catch(() => "");
1915
- if (markerFiles.trim()) {
1916
- const files = markerFiles.trim().split("\n").filter(Boolean);
1917
- return { success: false, reason: "unresolved-conflicts", unresolvedFiles: files };
2043
+ const currentMarkerFiles = await findConflictMarkerFiles(git);
2044
+ if (currentMarkerFiles.length > 0) {
2045
+ return { success: false, reason: "unresolved-conflicts", unresolvedFiles: currentMarkerFiles };
2046
+ }
2047
+ }
2048
+ const patchesToCommit = [...resolvingPatches, ...unresolvedPatches];
2049
+ if (patchesToCommit.length > 0) {
2050
+ const currentGen = lock.current_generation;
2051
+ const detector = new ReplayDetector(git, lockManager, outputDir);
2052
+ let patchesResolved = 0;
2053
+ for (const patch of patchesToCommit) {
2054
+ const diff = await git.exec(["diff", currentGen, "--", ...patch.files]).catch(() => null);
2055
+ if (!diff || !diff.trim()) {
2056
+ lockManager.removePatch(patch.id);
2057
+ continue;
2058
+ }
2059
+ const newContentHash = detector.computeContentHash(diff);
2060
+ const changedFiles = await getChangedFiles(git, currentGen, patch.files);
2061
+ lockManager.markPatchResolved(patch.id, {
2062
+ patch_content: diff,
2063
+ content_hash: newContentHash,
2064
+ base_generation: currentGen,
2065
+ files: changedFiles
2066
+ });
2067
+ patchesResolved++;
1918
2068
  }
2069
+ lockManager.save();
2070
+ const committer2 = new ReplayCommitter(git, outputDir);
2071
+ await committer2.stageAll();
2072
+ const commitSha2 = await committer2.commitReplay(
2073
+ lock.patches.length,
2074
+ lock.patches,
2075
+ "[fern-replay] Resolved conflicts"
2076
+ );
2077
+ return {
2078
+ success: true,
2079
+ commitSha: commitSha2,
2080
+ phase: "committed",
2081
+ patchesResolved
2082
+ };
1919
2083
  }
1920
2084
  const committer = new ReplayCommitter(git, outputDir);
1921
2085
  await committer.stageAll();
1922
2086
  const commitSha = await committer.commitReplay(lock.patches.length, lock.patches);
1923
- return { success: true, commitSha };
2087
+ return { success: true, commitSha, phase: "committed" };
2088
+ }
2089
+ async function findConflictMarkerFiles(git) {
2090
+ const output = await git.exec(["grep", "-l", "<<<<<<<", "--", "."]).catch(() => "");
2091
+ return output.trim() ? output.trim().split("\n").filter(Boolean) : [];
2092
+ }
2093
+ async function getChangedFiles(git, currentGen, files) {
2094
+ const filesOutput = await git.exec(["diff", "--name-only", currentGen, "--", ...files]).catch(() => null);
2095
+ if (!filesOutput || !filesOutput.trim()) return files;
2096
+ const changed = filesOutput.trim().split("\n").filter(Boolean).filter((f) => !f.startsWith(".fern/"));
2097
+ return changed.length > 0 ? changed : files;
1924
2098
  }
1925
2099
 
1926
2100
  // src/commands/status.ts