@fern-api/replay 0.8.1 → 0.9.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
@@ -3,6 +3,7 @@ interface GenerationLock {
3
3
  generations: GenerationRecord[];
4
4
  current_generation: string;
5
5
  patches: StoredPatch[];
6
+ forgotten_hashes?: string[];
6
7
  replay_skipped_at?: string;
7
8
  }
8
9
  interface GenerationRecord {
@@ -137,6 +138,7 @@ declare class LockfileManager {
137
138
  updatePatch(patchId: string, updates: Partial<Pick<StoredPatch, "base_generation" | "patch_content" | "content_hash" | "files" | "status">>): void;
138
139
  removePatch(patchId: string): void;
139
140
  clearPatches(): void;
141
+ addForgottenHash(hash: string): void;
140
142
  getUnresolvedPatches(): StoredPatch[];
141
143
  getResolvingPatches(): StoredPatch[];
142
144
  markPatchUnresolved(patchId: string): void;
@@ -426,18 +428,43 @@ declare function bootstrap(outputDir: string, options?: BootstrapOptions): Promi
426
428
  interface ForgetOptions {
427
429
  /** Don't actually remove, just show what would be removed */
428
430
  dryRun?: boolean;
431
+ /** Remove all tracked patches (keep lockfile and generation history) */
432
+ all?: boolean;
433
+ /** Specific patch IDs to remove */
434
+ patchIds?: string[];
435
+ /** Search pattern: file path, glob, or commit message substring */
436
+ pattern?: string;
437
+ }
438
+ interface DiffStat {
439
+ additions: number;
440
+ deletions: number;
441
+ }
442
+ interface MatchedPatch {
443
+ id: string;
444
+ message: string;
445
+ files: string[];
446
+ diffstat: DiffStat;
447
+ status?: "unresolved" | "resolving";
429
448
  }
430
449
  interface ForgetResult {
431
- /** Patches that were (or would be) removed */
432
- removed: Array<{
433
- id: string;
434
- message: string;
435
- files: string[];
436
- }>;
437
- /** True if no patches matched the pattern */
450
+ /** Whether replay is initialized (lockfile exists) */
451
+ initialized: boolean;
452
+ /** Patches that were (or would be in dry-run) removed */
453
+ removed: MatchedPatch[];
454
+ /** Number of patches remaining after removal */
455
+ remaining: number;
456
+ /** True if a search/pattern was given but nothing matched */
438
457
  notFound: boolean;
458
+ /** Patch IDs that were specified but don't exist (idempotent mode) */
459
+ alreadyForgotten: string[];
460
+ /** Total patches before removal */
461
+ totalPatches: number;
462
+ /** Warnings (e.g., forgetting patches with conflict markers on disk) */
463
+ warnings: string[];
464
+ /** Matching patches for interactive selection (search/no-arg mode only) */
465
+ matched?: MatchedPatch[];
439
466
  }
440
- declare function forget(outputDir: string, filePattern: string, options?: ForgetOptions): ForgetResult;
467
+ declare function forget(outputDir: string, options?: ForgetOptions): ForgetResult;
441
468
 
442
469
  interface ResetOptions {
443
470
  /** Don't actually delete, just show what would happen */
@@ -480,27 +507,45 @@ declare function resolve(outputDir: string, options?: ResolveOptions): Promise<R
480
507
  interface StatusResult {
481
508
  /** Whether replay is initialized (lockfile exists) */
482
509
  initialized: boolean;
483
- /** Tracked customization patches */
484
- patches: StatusPatch[];
510
+ /** Total number of generations tracked */
511
+ generationCount: number;
485
512
  /** Last generation info, if available */
486
513
  lastGeneration: StatusGeneration | undefined;
514
+ /** Tracked customization patches */
515
+ patches: StatusPatch[];
516
+ /** Count of patches with "unresolved" or "resolving" status */
517
+ unresolvedCount: number;
518
+ /** Exclude patterns from replay.yml */
519
+ excludePatterns: string[];
487
520
  }
488
521
  interface StatusPatch {
489
- /** Short SHA of the original commit */
490
- sha: string;
491
- /** Author name (without email) */
492
- author: string;
522
+ /** Patch ID (e.g. "patch-def45678") */
523
+ id: string;
524
+ /** "added" if patch_content contains "new file mode", otherwise "modified" */
525
+ type: "added" | "modified";
493
526
  /** Original commit message */
494
527
  message: string;
528
+ /** Author name (without email) */
529
+ author: string;
530
+ /** Short SHA of the original commit (7 chars) */
531
+ sha: string;
495
532
  /** Files touched by this patch */
496
533
  files: string[];
534
+ /** Number of files */
535
+ fileCount: number;
536
+ /** Patch resolution status, if any */
537
+ status?: "unresolved" | "resolving";
497
538
  }
498
539
  interface StatusGeneration {
499
- /** Full commit SHA */
540
+ /** Short commit SHA (7 chars) */
500
541
  sha: string;
501
542
  /** Generation timestamp */
502
543
  timestamp: string;
544
+ /** CLI version used for generation */
545
+ cliVersion: string;
546
+ /** Generator versions (e.g. { "fern-java-sdk": "3.35.0" }) */
547
+ generatorVersions: Record<string, string>;
503
548
  }
504
549
  declare function status(outputDir: string): StatusResult;
505
550
 
506
- export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, type DetectionResult, 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, isRevertCommit, parseRevertedMessage, parseRevertedSha, reset, resolve, status, threeWayMerge };
551
+ export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, type DetectionResult, type DiffStat, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, type MatchedPatch, 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, isRevertCommit, parseRevertedMessage, parseRevertedSha, reset, resolve, status, threeWayMerge };
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ interface GenerationLock {
3
3
  generations: GenerationRecord[];
4
4
  current_generation: string;
5
5
  patches: StoredPatch[];
6
+ forgotten_hashes?: string[];
6
7
  replay_skipped_at?: string;
7
8
  }
8
9
  interface GenerationRecord {
@@ -137,6 +138,7 @@ declare class LockfileManager {
137
138
  updatePatch(patchId: string, updates: Partial<Pick<StoredPatch, "base_generation" | "patch_content" | "content_hash" | "files" | "status">>): void;
138
139
  removePatch(patchId: string): void;
139
140
  clearPatches(): void;
141
+ addForgottenHash(hash: string): void;
140
142
  getUnresolvedPatches(): StoredPatch[];
141
143
  getResolvingPatches(): StoredPatch[];
142
144
  markPatchUnresolved(patchId: string): void;
@@ -426,18 +428,43 @@ declare function bootstrap(outputDir: string, options?: BootstrapOptions): Promi
426
428
  interface ForgetOptions {
427
429
  /** Don't actually remove, just show what would be removed */
428
430
  dryRun?: boolean;
431
+ /** Remove all tracked patches (keep lockfile and generation history) */
432
+ all?: boolean;
433
+ /** Specific patch IDs to remove */
434
+ patchIds?: string[];
435
+ /** Search pattern: file path, glob, or commit message substring */
436
+ pattern?: string;
437
+ }
438
+ interface DiffStat {
439
+ additions: number;
440
+ deletions: number;
441
+ }
442
+ interface MatchedPatch {
443
+ id: string;
444
+ message: string;
445
+ files: string[];
446
+ diffstat: DiffStat;
447
+ status?: "unresolved" | "resolving";
429
448
  }
430
449
  interface ForgetResult {
431
- /** Patches that were (or would be) removed */
432
- removed: Array<{
433
- id: string;
434
- message: string;
435
- files: string[];
436
- }>;
437
- /** True if no patches matched the pattern */
450
+ /** Whether replay is initialized (lockfile exists) */
451
+ initialized: boolean;
452
+ /** Patches that were (or would be in dry-run) removed */
453
+ removed: MatchedPatch[];
454
+ /** Number of patches remaining after removal */
455
+ remaining: number;
456
+ /** True if a search/pattern was given but nothing matched */
438
457
  notFound: boolean;
458
+ /** Patch IDs that were specified but don't exist (idempotent mode) */
459
+ alreadyForgotten: string[];
460
+ /** Total patches before removal */
461
+ totalPatches: number;
462
+ /** Warnings (e.g., forgetting patches with conflict markers on disk) */
463
+ warnings: string[];
464
+ /** Matching patches for interactive selection (search/no-arg mode only) */
465
+ matched?: MatchedPatch[];
439
466
  }
440
- declare function forget(outputDir: string, filePattern: string, options?: ForgetOptions): ForgetResult;
467
+ declare function forget(outputDir: string, options?: ForgetOptions): ForgetResult;
441
468
 
442
469
  interface ResetOptions {
443
470
  /** Don't actually delete, just show what would happen */
@@ -480,27 +507,45 @@ declare function resolve(outputDir: string, options?: ResolveOptions): Promise<R
480
507
  interface StatusResult {
481
508
  /** Whether replay is initialized (lockfile exists) */
482
509
  initialized: boolean;
483
- /** Tracked customization patches */
484
- patches: StatusPatch[];
510
+ /** Total number of generations tracked */
511
+ generationCount: number;
485
512
  /** Last generation info, if available */
486
513
  lastGeneration: StatusGeneration | undefined;
514
+ /** Tracked customization patches */
515
+ patches: StatusPatch[];
516
+ /** Count of patches with "unresolved" or "resolving" status */
517
+ unresolvedCount: number;
518
+ /** Exclude patterns from replay.yml */
519
+ excludePatterns: string[];
487
520
  }
488
521
  interface StatusPatch {
489
- /** Short SHA of the original commit */
490
- sha: string;
491
- /** Author name (without email) */
492
- author: string;
522
+ /** Patch ID (e.g. "patch-def45678") */
523
+ id: string;
524
+ /** "added" if patch_content contains "new file mode", otherwise "modified" */
525
+ type: "added" | "modified";
493
526
  /** Original commit message */
494
527
  message: string;
528
+ /** Author name (without email) */
529
+ author: string;
530
+ /** Short SHA of the original commit (7 chars) */
531
+ sha: string;
495
532
  /** Files touched by this patch */
496
533
  files: string[];
534
+ /** Number of files */
535
+ fileCount: number;
536
+ /** Patch resolution status, if any */
537
+ status?: "unresolved" | "resolving";
497
538
  }
498
539
  interface StatusGeneration {
499
- /** Full commit SHA */
540
+ /** Short commit SHA (7 chars) */
500
541
  sha: string;
501
542
  /** Generation timestamp */
502
543
  timestamp: string;
544
+ /** CLI version used for generation */
545
+ cliVersion: string;
546
+ /** Generator versions (e.g. { "fern-java-sdk": "3.35.0" }) */
547
+ generatorVersions: Record<string, string>;
503
548
  }
504
549
  declare function status(outputDir: string): StatusResult;
505
550
 
506
- export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, type DetectionResult, 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, isRevertCommit, parseRevertedMessage, parseRevertedSha, reset, resolve, status, threeWayMerge };
551
+ export { type BootstrapOptions, type BootstrapResult, type CommitInfo, type CommitOptions, type ConflictDetail, type ConflictMetadata, type ConflictReason, type ConflictRegion, type CustomizationsConfig, type DetectionResult, type DiffStat, FERN_BOT_EMAIL, FERN_BOT_LOGIN, FERN_BOT_NAME, FernignoreMigrator, type FileResult, type ForgetOptions, type ForgetResult, type GenerationLock, type GenerationRecord, GitClient, LockfileManager, type MatchedPatch, 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, isRevertCommit, parseRevertedMessage, parseRevertedSha, reset, resolve, status, threeWayMerge };
package/dist/index.js CHANGED
@@ -243,6 +243,15 @@ var LockfileManager = class {
243
243
  this.ensureLoaded();
244
244
  this.lock.patches = [];
245
245
  }
246
+ addForgottenHash(hash) {
247
+ this.ensureLoaded();
248
+ if (!this.lock.forgotten_hashes) {
249
+ this.lock.forgotten_hashes = [];
250
+ }
251
+ if (!this.lock.forgotten_hashes.includes(hash)) {
252
+ this.lock.forgotten_hashes.push(hash);
253
+ }
254
+ }
246
255
  getUnresolvedPatches() {
247
256
  this.ensureLoaded();
248
257
  return this.lock.patches.filter((p) => p.status === "unresolved");
@@ -339,6 +348,7 @@ var ReplayDetector = class {
339
348
  }
340
349
  const commits = this.parseGitLog(log);
341
350
  const newPatches = [];
351
+ const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
342
352
  for (const commit of commits) {
343
353
  if (isGenerationCommit(commit)) {
344
354
  continue;
@@ -352,7 +362,7 @@ var ReplayDetector = class {
352
362
  }
353
363
  const patchContent = await this.git.formatPatch(commit.sha);
354
364
  const contentHash = this.computeContentHash(patchContent);
355
- if (lock.patches.find((p) => p.content_hash === contentHash)) {
365
+ if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
356
366
  continue;
357
367
  }
358
368
  const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
@@ -451,7 +461,7 @@ var ReplayDetector = class {
451
461
  if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
452
462
  const contentHash = this.computeContentHash(diff);
453
463
  const lock = this.lockManager.read();
454
- if (lock.patches.some((p) => p.content_hash === contentHash)) {
464
+ if (lock.patches.some((p) => p.content_hash === contentHash) || (lock.forgotten_hashes ?? []).includes(contentHash)) {
455
465
  return { patches: [], revertedPatchIds: [] };
456
466
  }
457
467
  const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
@@ -521,6 +531,36 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "fs/promises";
521
531
  import { tmpdir } from "os";
522
532
  import { dirname as dirname2, extname, join as join2 } from "path";
523
533
  import { minimatch } from "minimatch";
534
+
535
+ // src/conflict-utils.ts
536
+ function stripConflictMarkers(content) {
537
+ const lines = content.split("\n");
538
+ const result = [];
539
+ let inConflict = false;
540
+ let inOurs = false;
541
+ for (const line of lines) {
542
+ if (line.startsWith("<<<<<<< ")) {
543
+ inConflict = true;
544
+ inOurs = true;
545
+ continue;
546
+ }
547
+ if (inConflict && line === "=======") {
548
+ inOurs = false;
549
+ continue;
550
+ }
551
+ if (inConflict && line.startsWith(">>>>>>> ")) {
552
+ inConflict = false;
553
+ inOurs = false;
554
+ continue;
555
+ }
556
+ if (!inConflict || inOurs) {
557
+ result.push(line);
558
+ }
559
+ }
560
+ return result.join("\n");
561
+ }
562
+
563
+ // src/ReplayApplicator.ts
524
564
  var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
525
565
  ".png",
526
566
  ".jpg",
@@ -592,7 +632,8 @@ var ReplayApplicator = class {
592
632
  async applyPatches(patches) {
593
633
  this.resetAccumulator();
594
634
  const results = [];
595
- for (const patch of patches) {
635
+ for (let i = 0; i < patches.length; i++) {
636
+ const patch = patches[i];
596
637
  if (this.isExcluded(patch)) {
597
638
  results.push({
598
639
  patch,
@@ -603,6 +644,33 @@ var ReplayApplicator = class {
603
644
  }
604
645
  const result = await this.applyPatchWithFallback(patch);
605
646
  results.push(result);
647
+ if (result.status === "conflict" && result.fileResults) {
648
+ const laterFiles = /* @__PURE__ */ new Set();
649
+ for (let j = i + 1; j < patches.length; j++) {
650
+ for (const f of patches[j].files) {
651
+ laterFiles.add(f);
652
+ }
653
+ }
654
+ const resolvedToOriginal = /* @__PURE__ */ new Map();
655
+ if (result.resolvedFiles) {
656
+ for (const [orig, resolved] of Object.entries(result.resolvedFiles)) {
657
+ resolvedToOriginal.set(resolved, orig);
658
+ }
659
+ }
660
+ for (const fileResult of result.fileResults) {
661
+ if (fileResult.status !== "conflict") continue;
662
+ const originalPath = resolvedToOriginal.get(fileResult.file) ?? fileResult.file;
663
+ if (laterFiles.has(fileResult.file) || laterFiles.has(originalPath)) {
664
+ const filePath = join2(this.outputDir, fileResult.file);
665
+ try {
666
+ const content = await readFile(filePath, "utf-8");
667
+ const stripped = stripConflictMarkers(content);
668
+ await writeFile(filePath, stripped);
669
+ } catch {
670
+ }
671
+ }
672
+ }
673
+ }
606
674
  }
607
675
  return results;
608
676
  }
@@ -621,7 +689,7 @@ var ReplayApplicator = class {
621
689
  const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
622
690
  const base = await this.git.showFile(baseGen.tree_hash, filePath);
623
691
  const theirs = await this.applyPatchToContent(base, patch.patch_content, filePath, tempGit, tempDir);
624
- if (theirs && base) {
692
+ if (theirs) {
625
693
  this.fileTheirsAccumulator.set(resolvedPath, {
626
694
  content: theirs,
627
695
  baseGeneration: patch.base_generation
@@ -753,7 +821,7 @@ var ReplayApplicator = class {
753
821
  );
754
822
  let useAccumulatorAsMergeBase = false;
755
823
  const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
756
- if (!theirs && base && accumulatorEntry) {
824
+ if (!theirs && accumulatorEntry) {
757
825
  theirs = await this.applyPatchToContent(
758
826
  accumulatorEntry.content,
759
827
  patch.patch_content,
@@ -812,7 +880,7 @@ var ReplayApplicator = class {
812
880
  reason: "missing-content"
813
881
  };
814
882
  }
815
- if (!base || !ours) {
883
+ if (!base && !useAccumulatorAsMergeBase || !ours) {
816
884
  return {
817
885
  file: resolvedPath,
818
886
  status: "skipped",
@@ -820,11 +888,18 @@ var ReplayApplicator = class {
820
888
  };
821
889
  }
822
890
  const mergeBase = useAccumulatorAsMergeBase && accumulatorEntry ? accumulatorEntry.content : base;
891
+ if (mergeBase == null) {
892
+ return {
893
+ file: resolvedPath,
894
+ status: "skipped",
895
+ reason: "missing-content"
896
+ };
897
+ }
823
898
  const merged = threeWayMerge(mergeBase, ours, effective_theirs);
824
899
  const outDir = dirname2(oursPath);
825
900
  await mkdir(outDir, { recursive: true });
826
901
  await writeFile(oursPath, merged.content);
827
- if (effective_theirs && base) {
902
+ if (effective_theirs) {
828
903
  this.fileTheirsAccumulator.set(resolvedPath, {
829
904
  content: effective_theirs,
830
905
  baseGeneration: patch.base_generation
@@ -1067,36 +1142,6 @@ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync
1067
1142
  import { join as join3 } from "path";
1068
1143
  import { minimatch as minimatch2 } from "minimatch";
1069
1144
  init_GitClient();
1070
-
1071
- // src/conflict-utils.ts
1072
- function stripConflictMarkers(content) {
1073
- const lines = content.split("\n");
1074
- const result = [];
1075
- let inConflict = false;
1076
- let inOurs = false;
1077
- for (const line of lines) {
1078
- if (line.startsWith("<<<<<<< ")) {
1079
- inConflict = true;
1080
- inOurs = true;
1081
- continue;
1082
- }
1083
- if (inConflict && line === "=======") {
1084
- inOurs = false;
1085
- continue;
1086
- }
1087
- if (inConflict && line.startsWith(">>>>>>> ")) {
1088
- inConflict = false;
1089
- inOurs = false;
1090
- continue;
1091
- }
1092
- if (!inConflict || inOurs) {
1093
- result.push(line);
1094
- }
1095
- }
1096
- return result.join("\n");
1097
- }
1098
-
1099
- // src/ReplayService.ts
1100
1145
  var ReplayService = class {
1101
1146
  git;
1102
1147
  detector;
@@ -2064,30 +2109,145 @@ function computeContentHash(patchContent) {
2064
2109
 
2065
2110
  // src/commands/forget.ts
2066
2111
  import { minimatch as minimatch4 } from "minimatch";
2067
- function forget(outputDir, filePattern, options) {
2112
+ function parseDiffStat(patchContent) {
2113
+ let additions = 0;
2114
+ let deletions = 0;
2115
+ let inDiffHunk = false;
2116
+ for (const line of patchContent.split("\n")) {
2117
+ if (line.startsWith("diff --git ")) {
2118
+ inDiffHunk = true;
2119
+ continue;
2120
+ }
2121
+ if (!inDiffHunk) continue;
2122
+ if (line.startsWith("+") && !line.startsWith("+++")) {
2123
+ additions++;
2124
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
2125
+ deletions++;
2126
+ }
2127
+ }
2128
+ return { additions, deletions };
2129
+ }
2130
+ function toMatchedPatch(patch) {
2131
+ return {
2132
+ id: patch.id,
2133
+ message: patch.original_message,
2134
+ files: patch.files,
2135
+ diffstat: parseDiffStat(patch.patch_content),
2136
+ ...patch.status ? { status: patch.status } : {}
2137
+ };
2138
+ }
2139
+ function matchesPatch(patch, pattern) {
2140
+ const fileMatch = patch.files.some(
2141
+ (file) => file === pattern || minimatch4(file, pattern)
2142
+ );
2143
+ if (fileMatch) return true;
2144
+ return patch.original_message.toLowerCase().includes(pattern.toLowerCase());
2145
+ }
2146
+ function buildWarnings(patches) {
2147
+ const warnings = [];
2148
+ for (const patch of patches) {
2149
+ if (patch.status === "resolving") {
2150
+ warnings.push(
2151
+ `patch ${patch.id} has conflict markers in these files: ${patch.files.join(", ")}. Run \`git checkout -- <files>\` to restore the generated versions.`
2152
+ );
2153
+ } else if (patch.status === "unresolved") {
2154
+ warnings.push(
2155
+ `patch ${patch.id} had unresolved conflicts (files: ${patch.files.join(", ")}).`
2156
+ );
2157
+ }
2158
+ }
2159
+ return warnings;
2160
+ }
2161
+ var EMPTY_RESULT = {
2162
+ initialized: false,
2163
+ removed: [],
2164
+ remaining: 0,
2165
+ notFound: false,
2166
+ alreadyForgotten: [],
2167
+ totalPatches: 0,
2168
+ warnings: []
2169
+ };
2170
+ function forget(outputDir, options) {
2068
2171
  const lockManager = new LockfileManager(outputDir);
2069
2172
  if (!lockManager.exists()) {
2070
- return { removed: [], notFound: true };
2173
+ return { ...EMPTY_RESULT };
2071
2174
  }
2072
2175
  const lock = lockManager.read();
2073
- const matchingPatches = lock.patches.filter(
2074
- (patch) => patch.files.some((file) => file === filePattern || minimatch4(file, filePattern))
2075
- );
2076
- if (matchingPatches.length === 0) {
2077
- return { removed: [], notFound: true };
2176
+ const totalPatches = lock.patches.length;
2177
+ if (options?.all) {
2178
+ const removed = lock.patches.map(toMatchedPatch);
2179
+ const warnings = buildWarnings(lock.patches);
2180
+ if (!options.dryRun) {
2181
+ for (const patch of lock.patches) {
2182
+ lockManager.addForgottenHash(patch.content_hash);
2183
+ }
2184
+ lockManager.clearPatches();
2185
+ lockManager.save();
2186
+ }
2187
+ return {
2188
+ initialized: true,
2189
+ removed,
2190
+ remaining: 0,
2191
+ notFound: false,
2192
+ alreadyForgotten: [],
2193
+ totalPatches,
2194
+ warnings
2195
+ };
2078
2196
  }
2079
- const removed = matchingPatches.map((p) => ({
2080
- id: p.id,
2081
- message: p.original_message,
2082
- files: p.files
2083
- }));
2084
- if (!options?.dryRun) {
2085
- for (const patch of matchingPatches) {
2086
- lockManager.removePatch(patch.id);
2197
+ if (options?.patchIds && options.patchIds.length > 0) {
2198
+ const removed = [];
2199
+ const alreadyForgotten = [];
2200
+ const patchesToRemove = [];
2201
+ for (const id of options.patchIds) {
2202
+ const patch = lock.patches.find((p) => p.id === id);
2203
+ if (patch) {
2204
+ removed.push(toMatchedPatch(patch));
2205
+ patchesToRemove.push(patch);
2206
+ } else {
2207
+ alreadyForgotten.push(id);
2208
+ }
2087
2209
  }
2088
- lockManager.save();
2210
+ const warnings = buildWarnings(patchesToRemove);
2211
+ if (!options.dryRun) {
2212
+ for (const patch of patchesToRemove) {
2213
+ lockManager.addForgottenHash(patch.content_hash);
2214
+ lockManager.removePatch(patch.id);
2215
+ }
2216
+ lockManager.save();
2217
+ }
2218
+ return {
2219
+ initialized: true,
2220
+ removed,
2221
+ remaining: totalPatches - removed.length,
2222
+ notFound: removed.length === 0 && alreadyForgotten.length > 0,
2223
+ alreadyForgotten,
2224
+ totalPatches,
2225
+ warnings
2226
+ };
2227
+ }
2228
+ if (options?.pattern) {
2229
+ const matched = lock.patches.filter((p) => matchesPatch(p, options.pattern)).map(toMatchedPatch);
2230
+ return {
2231
+ initialized: true,
2232
+ removed: [],
2233
+ remaining: totalPatches,
2234
+ notFound: matched.length === 0,
2235
+ alreadyForgotten: [],
2236
+ totalPatches,
2237
+ warnings: [],
2238
+ matched
2239
+ };
2089
2240
  }
2090
- return { removed, notFound: false };
2241
+ return {
2242
+ initialized: true,
2243
+ removed: [],
2244
+ remaining: totalPatches,
2245
+ notFound: totalPatches === 0,
2246
+ alreadyForgotten: [],
2247
+ totalPatches,
2248
+ warnings: [],
2249
+ matched: lock.patches.map(toMatchedPatch)
2250
+ };
2091
2251
  }
2092
2252
 
2093
2253
  // src/commands/reset.ts
@@ -2209,24 +2369,49 @@ async function getChangedFiles(git, currentGen, files) {
2209
2369
  function status(outputDir) {
2210
2370
  const lockManager = new LockfileManager(outputDir);
2211
2371
  if (!lockManager.exists()) {
2212
- return { initialized: false, patches: [], lastGeneration: void 0 };
2372
+ return {
2373
+ initialized: false,
2374
+ generationCount: 0,
2375
+ lastGeneration: void 0,
2376
+ patches: [],
2377
+ unresolvedCount: 0,
2378
+ excludePatterns: []
2379
+ };
2213
2380
  }
2214
2381
  const lock = lockManager.read();
2215
2382
  const patches = lock.patches.map((patch) => ({
2216
- sha: patch.original_commit.slice(0, 7),
2217
- author: patch.original_author.split("<")[0]?.trim() ?? "unknown",
2383
+ id: patch.id,
2384
+ type: patch.patch_content.includes("new file mode") ? "added" : "modified",
2218
2385
  message: patch.original_message,
2219
- files: patch.files
2386
+ author: patch.original_author.split("<")[0]?.trim() || "unknown",
2387
+ sha: patch.original_commit.slice(0, 7),
2388
+ files: patch.files,
2389
+ fileCount: patch.files.length,
2390
+ ...patch.status ? { status: patch.status } : {}
2220
2391
  }));
2392
+ const unresolvedCount = lock.patches.filter(
2393
+ (p) => p.status === "unresolved" || p.status === "resolving"
2394
+ ).length;
2221
2395
  let lastGeneration;
2222
2396
  const lastGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
2223
2397
  if (lastGen) {
2224
2398
  lastGeneration = {
2225
- sha: lastGen.commit_sha,
2226
- timestamp: lastGen.timestamp
2399
+ sha: lastGen.commit_sha.slice(0, 7),
2400
+ timestamp: lastGen.timestamp,
2401
+ cliVersion: lastGen.cli_version,
2402
+ generatorVersions: lastGen.generator_versions
2227
2403
  };
2228
2404
  }
2229
- return { initialized: true, patches, lastGeneration };
2405
+ const config = lockManager.getCustomizationsConfig();
2406
+ const excludePatterns = config.exclude ?? [];
2407
+ return {
2408
+ initialized: true,
2409
+ generationCount: lock.generations.length,
2410
+ lastGeneration,
2411
+ patches,
2412
+ unresolvedCount,
2413
+ excludePatterns
2414
+ };
2230
2415
  }
2231
2416
  export {
2232
2417
  FERN_BOT_EMAIL,