@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/cli.cjs CHANGED
@@ -751,7 +751,7 @@ var require_dist2 = __commonJS({
751
751
  function deferred2() {
752
752
  let done;
753
753
  let fail;
754
- let status = "pending";
754
+ let status2 = "pending";
755
755
  const promise = new Promise((_done, _fail) => {
756
756
  done = _done;
757
757
  fail = _fail;
@@ -759,22 +759,22 @@ var require_dist2 = __commonJS({
759
759
  return {
760
760
  promise,
761
761
  done(result) {
762
- if (status === "pending") {
763
- status = "resolved";
762
+ if (status2 === "pending") {
763
+ status2 = "resolved";
764
764
  done(result);
765
765
  }
766
766
  },
767
767
  fail(error) {
768
- if (status === "pending") {
769
- status = "rejected";
768
+ if (status2 === "pending") {
769
+ status2 = "rejected";
770
770
  fail(error);
771
771
  }
772
772
  },
773
773
  get fulfilled() {
774
- return status !== "pending";
774
+ return status2 !== "pending";
775
775
  },
776
776
  get status() {
777
- return status;
777
+ return status2;
778
778
  }
779
779
  };
780
780
  }
@@ -1817,10 +1817,10 @@ function mergeTask(customArgs) {
1817
1817
  }
1818
1818
  };
1819
1819
  }
1820
- function pushResultPushedItem(local, remote, status) {
1821
- const deleted = status.includes("deleted");
1822
- const tag = status.includes("tag") || /^refs\/tags/.test(local);
1823
- const alreadyUpdated = !status.includes("new");
1820
+ function pushResultPushedItem(local, remote, status2) {
1821
+ const deleted = status2.includes("deleted");
1822
+ const tag = status2.includes("tag") || /^refs\/tags/.test(local);
1823
+ const alreadyUpdated = !status2.includes("new");
1824
1824
  return {
1825
1825
  deleted,
1826
1826
  tag,
@@ -3659,7 +3659,7 @@ var init_esm = __esm({
3659
3659
  nameStatusParser = [
3660
3660
  new LineParser(
3661
3661
  /([ACDMRTUXB])([0-9]{0,3})\t(.[^\t]*)(\t(.[^\t]*))?$/,
3662
- (result, [status, similarity, from, _to, to]) => {
3662
+ (result, [status2, similarity, from, _to, to]) => {
3663
3663
  result.changed++;
3664
3664
  result.files.push({
3665
3665
  file: to ?? from,
@@ -3667,7 +3667,7 @@ var init_esm = __esm({
3667
3667
  insertions: 0,
3668
3668
  deletions: 0,
3669
3669
  binary: false,
3670
- status: orVoid(isDiffNameStatus(status) && status),
3670
+ status: orVoid(isDiffNameStatus(status2) && status2),
3671
3671
  from: orVoid(!!to && from !== to && from),
3672
3672
  similarity: asNumber(similarity)
3673
3673
  });
@@ -4214,7 +4214,7 @@ var init_esm = __esm({
4214
4214
  ]);
4215
4215
  parseStatusSummary = function(text) {
4216
4216
  const lines = text.split(NULL);
4217
- const status = new StatusSummary();
4217
+ const status2 = new StatusSummary();
4218
4218
  for (let i = 0, l = lines.length; i < l; ) {
4219
4219
  let line = lines[i++].trim();
4220
4220
  if (!line) {
@@ -4223,9 +4223,9 @@ var init_esm = __esm({
4223
4223
  if (line.charAt(0) === "R") {
4224
4224
  line += NULL + (lines[i++] || "");
4225
4225
  }
4226
- splitLine(status, line);
4226
+ splitLine(status2, line);
4227
4227
  }
4228
- return status;
4228
+ return status2;
4229
4229
  };
4230
4230
  }
4231
4231
  });
@@ -4514,15 +4514,15 @@ var init_esm = __esm({
4514
4514
  this.current = "";
4515
4515
  this.detached = false;
4516
4516
  }
4517
- push(status, detached, name, commit, label) {
4518
- if (status === "*") {
4517
+ push(status2, detached, name, commit, label) {
4518
+ if (status2 === "*") {
4519
4519
  this.detached = detached;
4520
4520
  this.current = name;
4521
4521
  }
4522
4522
  this.all.push(name);
4523
4523
  this.branches[name] = {
4524
- current: status === "*",
4525
- linkedWorkTree: status === "+",
4524
+ current: status2 === "*",
4525
+ linkedWorkTree: status2 === "+",
4526
4526
  name,
4527
4527
  commit,
4528
4528
  label
@@ -12846,6 +12846,7 @@ var require_brace_expansion = __commonJS({
12846
12846
  // src/cli.ts
12847
12847
  var import_node_path7 = require("path");
12848
12848
  var import_node_fs6 = require("fs");
12849
+ var import_node_readline = require("readline");
12849
12850
  init_GitClient();
12850
12851
 
12851
12852
  // src/LockfileManager.ts
@@ -12938,6 +12939,15 @@ var LockfileManager = class {
12938
12939
  this.ensureLoaded();
12939
12940
  this.lock.patches = [];
12940
12941
  }
12942
+ addForgottenHash(hash) {
12943
+ this.ensureLoaded();
12944
+ if (!this.lock.forgotten_hashes) {
12945
+ this.lock.forgotten_hashes = [];
12946
+ }
12947
+ if (!this.lock.forgotten_hashes.includes(hash)) {
12948
+ this.lock.forgotten_hashes.push(hash);
12949
+ }
12950
+ }
12941
12951
  getUnresolvedPatches() {
12942
12952
  this.ensureLoaded();
12943
12953
  return this.lock.patches.filter((p) => p.status === "unresolved");
@@ -13065,6 +13075,7 @@ var ReplayDetector = class {
13065
13075
  }
13066
13076
  const commits = this.parseGitLog(log);
13067
13077
  const newPatches = [];
13078
+ const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
13068
13079
  for (const commit of commits) {
13069
13080
  if (isGenerationCommit(commit)) {
13070
13081
  continue;
@@ -13078,7 +13089,7 @@ var ReplayDetector = class {
13078
13089
  }
13079
13090
  const patchContent = await this.git.formatPatch(commit.sha);
13080
13091
  const contentHash = this.computeContentHash(patchContent);
13081
- if (lock.patches.find((p) => p.content_hash === contentHash)) {
13092
+ if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
13082
13093
  continue;
13083
13094
  }
13084
13095
  const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
@@ -13177,7 +13188,7 @@ var ReplayDetector = class {
13177
13188
  if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
13178
13189
  const contentHash = this.computeContentHash(diff);
13179
13190
  const lock = this.lockManager.read();
13180
- if (lock.patches.some((p) => p.content_hash === contentHash)) {
13191
+ if (lock.patches.some((p) => p.content_hash === contentHash) || (lock.forgotten_hashes ?? []).includes(contentHash)) {
13181
13192
  return { patches: [], revertedPatchIds: [] };
13182
13193
  }
13183
13194
  const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
@@ -14546,6 +14557,34 @@ var import_promises = require("fs/promises");
14546
14557
  var import_node_os = require("os");
14547
14558
  var import_node_path3 = require("path");
14548
14559
 
14560
+ // src/conflict-utils.ts
14561
+ function stripConflictMarkers(content) {
14562
+ const lines = content.split("\n");
14563
+ const result = [];
14564
+ let inConflict = false;
14565
+ let inOurs = false;
14566
+ for (const line of lines) {
14567
+ if (line.startsWith("<<<<<<< ")) {
14568
+ inConflict = true;
14569
+ inOurs = true;
14570
+ continue;
14571
+ }
14572
+ if (inConflict && line === "=======") {
14573
+ inOurs = false;
14574
+ continue;
14575
+ }
14576
+ if (inConflict && line.startsWith(">>>>>>> ")) {
14577
+ inConflict = false;
14578
+ inOurs = false;
14579
+ continue;
14580
+ }
14581
+ if (!inConflict || inOurs) {
14582
+ result.push(line);
14583
+ }
14584
+ }
14585
+ return result.join("\n");
14586
+ }
14587
+
14549
14588
  // node_modules/node-diff3/dist/diff3.mjs
14550
14589
  function LCS(buffer1, buffer2) {
14551
14590
  let equivalenceClasses = {};
@@ -14869,7 +14908,8 @@ var ReplayApplicator = class {
14869
14908
  async applyPatches(patches) {
14870
14909
  this.resetAccumulator();
14871
14910
  const results = [];
14872
- for (const patch of patches) {
14911
+ for (let i = 0; i < patches.length; i++) {
14912
+ const patch = patches[i];
14873
14913
  if (this.isExcluded(patch)) {
14874
14914
  results.push({
14875
14915
  patch,
@@ -14880,6 +14920,33 @@ var ReplayApplicator = class {
14880
14920
  }
14881
14921
  const result = await this.applyPatchWithFallback(patch);
14882
14922
  results.push(result);
14923
+ if (result.status === "conflict" && result.fileResults) {
14924
+ const laterFiles = /* @__PURE__ */ new Set();
14925
+ for (let j = i + 1; j < patches.length; j++) {
14926
+ for (const f of patches[j].files) {
14927
+ laterFiles.add(f);
14928
+ }
14929
+ }
14930
+ const resolvedToOriginal = /* @__PURE__ */ new Map();
14931
+ if (result.resolvedFiles) {
14932
+ for (const [orig, resolved] of Object.entries(result.resolvedFiles)) {
14933
+ resolvedToOriginal.set(resolved, orig);
14934
+ }
14935
+ }
14936
+ for (const fileResult of result.fileResults) {
14937
+ if (fileResult.status !== "conflict") continue;
14938
+ const originalPath = resolvedToOriginal.get(fileResult.file) ?? fileResult.file;
14939
+ if (laterFiles.has(fileResult.file) || laterFiles.has(originalPath)) {
14940
+ const filePath = (0, import_node_path3.join)(this.outputDir, fileResult.file);
14941
+ try {
14942
+ const content = await (0, import_promises.readFile)(filePath, "utf-8");
14943
+ const stripped = stripConflictMarkers(content);
14944
+ await (0, import_promises.writeFile)(filePath, stripped);
14945
+ } catch {
14946
+ }
14947
+ }
14948
+ }
14949
+ }
14883
14950
  }
14884
14951
  return results;
14885
14952
  }
@@ -14898,7 +14965,7 @@ var ReplayApplicator = class {
14898
14965
  const resolvedPath = await this.resolveFilePath(filePath, baseGen.tree_hash, currentTreeHash);
14899
14966
  const base = await this.git.showFile(baseGen.tree_hash, filePath);
14900
14967
  const theirs = await this.applyPatchToContent(base, patch.patch_content, filePath, tempGit, tempDir);
14901
- if (theirs && base) {
14968
+ if (theirs) {
14902
14969
  this.fileTheirsAccumulator.set(resolvedPath, {
14903
14970
  content: theirs,
14904
14971
  baseGeneration: patch.base_generation
@@ -15030,7 +15097,7 @@ var ReplayApplicator = class {
15030
15097
  );
15031
15098
  let useAccumulatorAsMergeBase = false;
15032
15099
  const accumulatorEntry = this.fileTheirsAccumulator.get(resolvedPath);
15033
- if (!theirs && base && accumulatorEntry) {
15100
+ if (!theirs && accumulatorEntry) {
15034
15101
  theirs = await this.applyPatchToContent(
15035
15102
  accumulatorEntry.content,
15036
15103
  patch.patch_content,
@@ -15089,7 +15156,7 @@ var ReplayApplicator = class {
15089
15156
  reason: "missing-content"
15090
15157
  };
15091
15158
  }
15092
- if (!base || !ours) {
15159
+ if (!base && !useAccumulatorAsMergeBase || !ours) {
15093
15160
  return {
15094
15161
  file: resolvedPath,
15095
15162
  status: "skipped",
@@ -15097,11 +15164,18 @@ var ReplayApplicator = class {
15097
15164
  };
15098
15165
  }
15099
15166
  const mergeBase = useAccumulatorAsMergeBase && accumulatorEntry ? accumulatorEntry.content : base;
15167
+ if (mergeBase == null) {
15168
+ return {
15169
+ file: resolvedPath,
15170
+ status: "skipped",
15171
+ reason: "missing-content"
15172
+ };
15173
+ }
15100
15174
  const merged = threeWayMerge(mergeBase, ours, effective_theirs);
15101
15175
  const outDir = (0, import_node_path3.dirname)(oursPath);
15102
15176
  await (0, import_promises.mkdir)(outDir, { recursive: true });
15103
15177
  await (0, import_promises.writeFile)(oursPath, merged.content);
15104
- if (effective_theirs && base) {
15178
+ if (effective_theirs) {
15105
15179
  this.fileTheirsAccumulator.set(resolvedPath, {
15106
15180
  content: effective_theirs,
15107
15181
  baseGeneration: patch.base_generation
@@ -15339,34 +15413,6 @@ CLI Version: ${options.cliVersion}`;
15339
15413
  }
15340
15414
  };
15341
15415
 
15342
- // src/conflict-utils.ts
15343
- function stripConflictMarkers(content) {
15344
- const lines = content.split("\n");
15345
- const result = [];
15346
- let inConflict = false;
15347
- let inOurs = false;
15348
- for (const line of lines) {
15349
- if (line.startsWith("<<<<<<< ")) {
15350
- inConflict = true;
15351
- inOurs = true;
15352
- continue;
15353
- }
15354
- if (inConflict && line === "=======") {
15355
- inOurs = false;
15356
- continue;
15357
- }
15358
- if (inConflict && line.startsWith(">>>>>>> ")) {
15359
- inConflict = false;
15360
- inOurs = false;
15361
- continue;
15362
- }
15363
- if (!inConflict || inOurs) {
15364
- result.push(line);
15365
- }
15366
- }
15367
- return result.join("\n");
15368
- }
15369
-
15370
15416
  // src/ReplayService.ts
15371
15417
  var ReplayService = class {
15372
15418
  git;
@@ -16333,30 +16379,145 @@ function computeContentHash(patchContent) {
16333
16379
  }
16334
16380
 
16335
16381
  // src/commands/forget.ts
16336
- function forget(outputDir, filePattern, options) {
16382
+ function parseDiffStat(patchContent) {
16383
+ let additions = 0;
16384
+ let deletions = 0;
16385
+ let inDiffHunk = false;
16386
+ for (const line of patchContent.split("\n")) {
16387
+ if (line.startsWith("diff --git ")) {
16388
+ inDiffHunk = true;
16389
+ continue;
16390
+ }
16391
+ if (!inDiffHunk) continue;
16392
+ if (line.startsWith("+") && !line.startsWith("+++")) {
16393
+ additions++;
16394
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
16395
+ deletions++;
16396
+ }
16397
+ }
16398
+ return { additions, deletions };
16399
+ }
16400
+ function toMatchedPatch(patch) {
16401
+ return {
16402
+ id: patch.id,
16403
+ message: patch.original_message,
16404
+ files: patch.files,
16405
+ diffstat: parseDiffStat(patch.patch_content),
16406
+ ...patch.status ? { status: patch.status } : {}
16407
+ };
16408
+ }
16409
+ function matchesPatch(patch, pattern) {
16410
+ const fileMatch = patch.files.some(
16411
+ (file) => file === pattern || minimatch(file, pattern)
16412
+ );
16413
+ if (fileMatch) return true;
16414
+ return patch.original_message.toLowerCase().includes(pattern.toLowerCase());
16415
+ }
16416
+ function buildWarnings(patches) {
16417
+ const warnings = [];
16418
+ for (const patch of patches) {
16419
+ if (patch.status === "resolving") {
16420
+ warnings.push(
16421
+ `patch ${patch.id} has conflict markers in these files: ${patch.files.join(", ")}. Run \`git checkout -- <files>\` to restore the generated versions.`
16422
+ );
16423
+ } else if (patch.status === "unresolved") {
16424
+ warnings.push(
16425
+ `patch ${patch.id} had unresolved conflicts (files: ${patch.files.join(", ")}).`
16426
+ );
16427
+ }
16428
+ }
16429
+ return warnings;
16430
+ }
16431
+ var EMPTY_RESULT = {
16432
+ initialized: false,
16433
+ removed: [],
16434
+ remaining: 0,
16435
+ notFound: false,
16436
+ alreadyForgotten: [],
16437
+ totalPatches: 0,
16438
+ warnings: []
16439
+ };
16440
+ function forget(outputDir, options) {
16337
16441
  const lockManager = new LockfileManager(outputDir);
16338
16442
  if (!lockManager.exists()) {
16339
- return { removed: [], notFound: true };
16443
+ return { ...EMPTY_RESULT };
16340
16444
  }
16341
16445
  const lock = lockManager.read();
16342
- const matchingPatches = lock.patches.filter(
16343
- (patch) => patch.files.some((file) => file === filePattern || minimatch(file, filePattern))
16344
- );
16345
- if (matchingPatches.length === 0) {
16346
- return { removed: [], notFound: true };
16446
+ const totalPatches = lock.patches.length;
16447
+ if (options?.all) {
16448
+ const removed = lock.patches.map(toMatchedPatch);
16449
+ const warnings = buildWarnings(lock.patches);
16450
+ if (!options.dryRun) {
16451
+ for (const patch of lock.patches) {
16452
+ lockManager.addForgottenHash(patch.content_hash);
16453
+ }
16454
+ lockManager.clearPatches();
16455
+ lockManager.save();
16456
+ }
16457
+ return {
16458
+ initialized: true,
16459
+ removed,
16460
+ remaining: 0,
16461
+ notFound: false,
16462
+ alreadyForgotten: [],
16463
+ totalPatches,
16464
+ warnings
16465
+ };
16347
16466
  }
16348
- const removed = matchingPatches.map((p) => ({
16349
- id: p.id,
16350
- message: p.original_message,
16351
- files: p.files
16352
- }));
16353
- if (!options?.dryRun) {
16354
- for (const patch of matchingPatches) {
16355
- lockManager.removePatch(patch.id);
16467
+ if (options?.patchIds && options.patchIds.length > 0) {
16468
+ const removed = [];
16469
+ const alreadyForgotten = [];
16470
+ const patchesToRemove = [];
16471
+ for (const id of options.patchIds) {
16472
+ const patch = lock.patches.find((p) => p.id === id);
16473
+ if (patch) {
16474
+ removed.push(toMatchedPatch(patch));
16475
+ patchesToRemove.push(patch);
16476
+ } else {
16477
+ alreadyForgotten.push(id);
16478
+ }
16356
16479
  }
16357
- lockManager.save();
16480
+ const warnings = buildWarnings(patchesToRemove);
16481
+ if (!options.dryRun) {
16482
+ for (const patch of patchesToRemove) {
16483
+ lockManager.addForgottenHash(patch.content_hash);
16484
+ lockManager.removePatch(patch.id);
16485
+ }
16486
+ lockManager.save();
16487
+ }
16488
+ return {
16489
+ initialized: true,
16490
+ removed,
16491
+ remaining: totalPatches - removed.length,
16492
+ notFound: removed.length === 0 && alreadyForgotten.length > 0,
16493
+ alreadyForgotten,
16494
+ totalPatches,
16495
+ warnings
16496
+ };
16358
16497
  }
16359
- return { removed, notFound: false };
16498
+ if (options?.pattern) {
16499
+ const matched = lock.patches.filter((p) => matchesPatch(p, options.pattern)).map(toMatchedPatch);
16500
+ return {
16501
+ initialized: true,
16502
+ removed: [],
16503
+ remaining: totalPatches,
16504
+ notFound: matched.length === 0,
16505
+ alreadyForgotten: [],
16506
+ totalPatches,
16507
+ warnings: [],
16508
+ matched
16509
+ };
16510
+ }
16511
+ return {
16512
+ initialized: true,
16513
+ removed: [],
16514
+ remaining: totalPatches,
16515
+ notFound: totalPatches === 0,
16516
+ alreadyForgotten: [],
16517
+ totalPatches,
16518
+ warnings: [],
16519
+ matched: lock.patches.map(toMatchedPatch)
16520
+ };
16360
16521
  }
16361
16522
 
16362
16523
  // src/commands/reset.ts
@@ -16474,6 +16635,55 @@ async function getChangedFiles(git, currentGen, files) {
16474
16635
  return changed.length > 0 ? changed : files;
16475
16636
  }
16476
16637
 
16638
+ // src/commands/status.ts
16639
+ function status(outputDir) {
16640
+ const lockManager = new LockfileManager(outputDir);
16641
+ if (!lockManager.exists()) {
16642
+ return {
16643
+ initialized: false,
16644
+ generationCount: 0,
16645
+ lastGeneration: void 0,
16646
+ patches: [],
16647
+ unresolvedCount: 0,
16648
+ excludePatterns: []
16649
+ };
16650
+ }
16651
+ const lock = lockManager.read();
16652
+ const patches = lock.patches.map((patch) => ({
16653
+ id: patch.id,
16654
+ type: patch.patch_content.includes("new file mode") ? "added" : "modified",
16655
+ message: patch.original_message,
16656
+ author: patch.original_author.split("<")[0]?.trim() || "unknown",
16657
+ sha: patch.original_commit.slice(0, 7),
16658
+ files: patch.files,
16659
+ fileCount: patch.files.length,
16660
+ ...patch.status ? { status: patch.status } : {}
16661
+ }));
16662
+ const unresolvedCount = lock.patches.filter(
16663
+ (p) => p.status === "unresolved" || p.status === "resolving"
16664
+ ).length;
16665
+ let lastGeneration;
16666
+ const lastGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
16667
+ if (lastGen) {
16668
+ lastGeneration = {
16669
+ sha: lastGen.commit_sha.slice(0, 7),
16670
+ timestamp: lastGen.timestamp,
16671
+ cliVersion: lastGen.cli_version,
16672
+ generatorVersions: lastGen.generator_versions
16673
+ };
16674
+ }
16675
+ const config = lockManager.getCustomizationsConfig();
16676
+ const excludePatterns = config.exclude ?? [];
16677
+ return {
16678
+ initialized: true,
16679
+ generationCount: lock.generations.length,
16680
+ lastGeneration,
16681
+ patches,
16682
+ unresolvedCount,
16683
+ excludePatterns
16684
+ };
16685
+ }
16686
+
16477
16687
  // src/cli.ts
16478
16688
  var COMMANDS = ["bootstrap", "status", "detect", "replay", "migrate", "forget", "reset", "resolve"];
16479
16689
  function usage() {
@@ -16485,7 +16695,7 @@ Commands:
16485
16695
  detect Detect new customization patches (dry-run)
16486
16696
  replay Run full replay (detect + apply patches)
16487
16697
  migrate Analyze .fernignore migration
16488
- forget Remove specific patches by file pattern
16698
+ forget Remove specific patches (by ID, search, or --all)
16489
16699
  reset Remove all Replay state (nuclear option)
16490
16700
  resolve Finish replay after manually resolving conflicts
16491
16701
 
@@ -16495,6 +16705,8 @@ Options:
16495
16705
  --no-check Skip conflict marker check (resolve command only)
16496
16706
  --migrate-fernignore Move untrackable .fernignore patterns to replay.yml
16497
16707
  --max-commits <n> Max commits to scan (default: 500)
16708
+ --all Remove all tracked patches (forget only)
16709
+ --yes, -y Skip interactive selection/confirmation (forget only)
16498
16710
 
16499
16711
  Examples:
16500
16712
  fern-replay bootstrap ./my-sdk
@@ -16504,8 +16716,9 @@ Examples:
16504
16716
  fern-replay detect ./my-sdk
16505
16717
  fern-replay replay ./my-sdk --dry-run
16506
16718
  fern-replay migrate ./my-sdk
16719
+ fern-replay forget ./my-sdk patch-def45678
16507
16720
  fern-replay forget ./my-sdk "src/utils/retry.ts"
16508
- fern-replay forget ./my-sdk "src/legacy/**" --dry-run
16721
+ fern-replay forget ./my-sdk --all
16509
16722
  fern-replay reset ./my-sdk
16510
16723
  fern-replay reset ./my-sdk --dry-run`);
16511
16724
  }
@@ -16530,7 +16743,7 @@ function parseArgs(argv) {
16530
16743
  process.exit(1);
16531
16744
  }
16532
16745
  const flags = {};
16533
- let pattern;
16746
+ const positionals = [];
16534
16747
  for (let i = 2; i < args.length; i++) {
16535
16748
  const arg = args[i];
16536
16749
  if (arg === "--dry-run") {
@@ -16545,11 +16758,17 @@ function parseArgs(argv) {
16545
16758
  flags.force = true;
16546
16759
  } else if (arg === "--no-check") {
16547
16760
  flags.noCheck = true;
16548
- } else if (!arg.startsWith("--") && !pattern) {
16549
- pattern = arg;
16761
+ } else if (arg === "--verbose" || arg === "-v") {
16762
+ flags.verbose = true;
16763
+ } else if (arg === "--all") {
16764
+ flags.all = true;
16765
+ } else if (arg === "--yes" || arg === "-y") {
16766
+ flags.yes = true;
16767
+ } else if (!arg.startsWith("--")) {
16768
+ positionals.push(arg);
16550
16769
  }
16551
16770
  }
16552
- return { command, dir: (0, import_node_path7.resolve)(dir), flags, pattern };
16771
+ return { command, dir: (0, import_node_path7.resolve)(dir), flags, positionals };
16553
16772
  }
16554
16773
  async function runBootstrap(dir, flags) {
16555
16774
  const dryRun = !!flags.dryRun;
@@ -16602,43 +16821,67 @@ async function runBootstrap(dir, flags) {
16602
16821
  for (const w of result.warnings) console.log(` ${w}`);
16603
16822
  }
16604
16823
  }
16605
- async function runStatus(dir) {
16606
- const lockManager = new LockfileManager(dir);
16607
- if (!lockManager.exists()) {
16608
- console.log("Replay is not initialized. Run 'fern-replay bootstrap <dir>' first.");
16824
+ async function runStatus(dir, flags) {
16825
+ const result = status(dir);
16826
+ const verbose = !!flags.verbose;
16827
+ if (!result.initialized) {
16828
+ console.log("Replay is not initialized. Run 'fern replay init' to get started.");
16609
16829
  return;
16610
16830
  }
16611
- const lock = lockManager.read();
16612
- const lastGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
16613
- console.log(`Replay Status: ${dir}
16614
- `);
16615
- console.log(`Generations: ${lock.generations.length}`);
16616
- if (lastGen) {
16617
- console.log(`Last generation: ${lastGen.commit_sha.slice(0, 7)} (${lastGen.timestamp})`);
16618
- console.log(` CLI: ${lastGen.cli_version}`);
16619
- if (Object.keys(lastGen.generator_versions).length > 0) {
16620
- for (const [name, ver] of Object.entries(lastGen.generator_versions)) {
16831
+ console.log("Replay: initialized");
16832
+ if (result.lastGeneration) {
16833
+ const gen = result.lastGeneration;
16834
+ if (verbose) {
16835
+ console.log(`Last generation: ${gen.sha} (${gen.timestamp})`);
16836
+ console.log(` CLI: ${gen.cliVersion}`);
16837
+ for (const [name, ver] of Object.entries(gen.generatorVersions)) {
16621
16838
  console.log(` ${name}: ${ver}`);
16622
16839
  }
16840
+ console.log(`Generations tracked: ${result.generationCount}`);
16841
+ } else {
16842
+ const genVersions = Object.entries(gen.generatorVersions);
16843
+ const versionSuffix = genVersions.length > 0 ? `, ${genVersions[0][0]}@${genVersions[0][1]}` : "";
16844
+ console.log(`Last generation: ${gen.sha} (${gen.timestamp}${versionSuffix})`);
16623
16845
  }
16624
16846
  }
16847
+ if (result.patches.length === 0) {
16848
+ console.log("\nNo customizations tracked. Edit SDK files and commit \u2014 Replay will detect them on next generation.");
16849
+ return;
16850
+ }
16851
+ const unresolvedSuffix = result.unresolvedCount > 0 ? `, ${result.unresolvedCount} unresolved` : "";
16625
16852
  console.log(`
16626
- Patches: ${lock.patches.length}`);
16627
- if (lock.patches.length > 0) {
16628
- for (const patch of lock.patches) {
16629
- const type = patch.patch_content.includes("new file mode") ? "added" : "modified";
16630
- console.log(` ${patch.id} [${type}] "${patch.original_message.slice(0, 50)}"`);
16631
- console.log(` by ${patch.original_author} (${patch.original_commit.slice(0, 7)})`);
16853
+ Patches: ${result.patches.length} tracked${unresolvedSuffix}`);
16854
+ if (verbose) {
16855
+ for (const patch of result.patches) {
16856
+ const prefix = patch.status ? "\u26A0 " : " ";
16857
+ console.log(`
16858
+ ${prefix}${patch.id} [${patch.type}] "${patch.message}"`);
16859
+ console.log(` by ${patch.author} (${patch.sha})`);
16632
16860
  for (const f of patch.files) {
16633
16861
  console.log(` ${f}`);
16634
16862
  }
16863
+ if (patch.status) {
16864
+ console.log(" Run `fern replay resolve` to fix.");
16865
+ }
16866
+ }
16867
+ if (result.excludePatterns.length > 0) {
16868
+ console.log("\nExclude patterns (from replay.yml):");
16869
+ for (const p of result.excludePatterns) {
16870
+ console.log(` ${p}`);
16871
+ }
16872
+ }
16873
+ } else {
16874
+ console.log("");
16875
+ const maxDisplay = 10;
16876
+ const displayPatches = result.patches.slice(0, maxDisplay);
16877
+ for (const patch of displayPatches) {
16878
+ const prefix = patch.status ? "\u26A0 " : " ";
16879
+ const filesLabel = patch.fileCount === 1 ? "1 file" : `${patch.fileCount} files`;
16880
+ console.log(`${prefix}${patch.id} "${patch.message}" ${filesLabel}`);
16881
+ }
16882
+ if (result.patches.length > maxDisplay) {
16883
+ console.log(` ... and ${result.patches.length - maxDisplay} more`);
16635
16884
  }
16636
- }
16637
- const config = lockManager.getCustomizationsConfig();
16638
- if (config.exclude && config.exclude.length > 0) {
16639
- console.log(`
16640
- Exclude patterns: ${config.exclude.length}`);
16641
- for (const p of config.exclude) console.log(` ${p}`);
16642
16885
  }
16643
16886
  }
16644
16887
  async function runDetect(dir) {
@@ -16807,32 +17050,155 @@ Unprotected customizations (${analysis.commitsOnly.length}):`);
16807
17050
  console.log("Run: fern-replay bootstrap <dir>");
16808
17051
  }
16809
17052
  }
16810
- async function runForget(dir, flags, pattern) {
16811
- if (!pattern) {
16812
- console.error("Usage: fern-replay forget <sdk-dir> <file-pattern>");
16813
- console.error("Example: fern-replay forget ./my-sdk src/utils/retry.ts");
16814
- process.exit(1);
17053
+ function formatDiffStat(patch) {
17054
+ return `+${patch.diffstat.additions} -${patch.diffstat.deletions}`;
17055
+ }
17056
+ function formatPatchLine(index, patch) {
17057
+ const status2 = patch.status ? ` [${patch.status}]` : "";
17058
+ const filesStr = patch.files.join(", ");
17059
+ return ` [${index}] ${patch.id} "${patch.message}" ${formatDiffStat(patch)} ${filesStr}${status2}`;
17060
+ }
17061
+ function printForgetWarnings(warnings) {
17062
+ for (const w of warnings) {
17063
+ console.log(`Warning: ${w}`);
16815
17064
  }
17065
+ }
17066
+ function promptLine(question) {
17067
+ const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
17068
+ return new Promise((resolve2) => {
17069
+ rl.question(question, (answer) => {
17070
+ rl.close();
17071
+ resolve2(answer.trim());
17072
+ });
17073
+ });
17074
+ }
17075
+ function parseSelection(input, max) {
17076
+ const trimmed2 = input.trim().toLowerCase();
17077
+ if (trimmed2 === "all") return "all";
17078
+ if (trimmed2 === "none" || trimmed2 === "") return "none";
17079
+ const nums = [];
17080
+ for (const part of trimmed2.split(",")) {
17081
+ const n = parseInt(part.trim(), 10);
17082
+ if (isNaN(n) || n < 1 || n > max) return null;
17083
+ if (!nums.includes(n)) nums.push(n);
17084
+ }
17085
+ return nums.length > 0 ? nums : null;
17086
+ }
17087
+ async function runForget(dir, flags, positionals) {
16816
17088
  const dryRun = !!flags.dryRun;
16817
- console.log(`Forgetting patches matching: ${pattern}`);
16818
- if (dryRun) console.log("(dry-run mode)");
16819
- const result = forget(dir, pattern, { dryRun });
16820
- if (result.notFound) {
16821
- console.log("No patches found matching that pattern.");
17089
+ const yes = !!flags.yes;
17090
+ const all = !!flags.all;
17091
+ const isPatchIdMode = positionals.length > 0 && positionals.every((a) => a.startsWith("patch-"));
17092
+ const pattern = !isPatchIdMode && positionals.length > 0 ? positionals[0] : void 0;
17093
+ const patchIds = isPatchIdMode ? positionals : void 0;
17094
+ if (all) {
17095
+ const preview = forget(dir, { all: true, dryRun: true });
17096
+ if (!preview.initialized) {
17097
+ console.log("Replay is not initialized. Run 'fern replay init' to get started.");
17098
+ process.exit(1);
17099
+ }
17100
+ if (preview.totalPatches === 0) {
17101
+ console.log("No patches tracked. Nothing to remove.");
17102
+ return;
17103
+ }
17104
+ if (!dryRun && !yes) {
17105
+ console.log(`WARNING: This will remove all ${preview.totalPatches} tracked patches.`);
17106
+ console.log("Affected files will be overwritten on next generation.");
17107
+ console.log("Replay will remain initialized and detect new customizations.\n");
17108
+ const confirm = await promptLine("Proceed? [y/N] ");
17109
+ if (confirm.toLowerCase() !== "y") {
17110
+ console.log("Aborted.");
17111
+ return;
17112
+ }
17113
+ }
17114
+ const finalResult = dryRun ? preview : forget(dir, { all: true });
17115
+ printForgetWarnings(finalResult.warnings);
17116
+ const verb2 = dryRun ? "Would remove" : "Removed";
17117
+ console.log(`${verb2} ${finalResult.removed.length} patches. ${finalResult.remaining} patches remaining.`);
16822
17118
  return;
16823
17119
  }
16824
- console.log(`
16825
- ${dryRun ? "Would remove" : "Removed"} ${result.removed.length} patch(es):
16826
- `);
16827
- for (const patch of result.removed) {
16828
- console.log(` ${patch.id}: "${patch.message.slice(0, 50)}"`);
16829
- for (const f of patch.files) {
16830
- console.log(` ${f}`);
17120
+ if (isPatchIdMode && patchIds) {
17121
+ const result2 = forget(dir, { patchIds, dryRun });
17122
+ if (!result2.initialized) {
17123
+ console.log("Replay is not initialized. Run 'fern replay init' to get started.");
17124
+ process.exit(1);
17125
+ }
17126
+ if (result2.alreadyForgotten.length > 0 && result2.removed.length === 0) {
17127
+ for (const id of result2.alreadyForgotten) {
17128
+ console.log(`${id} is not tracked. Nothing to remove.`);
17129
+ }
17130
+ return;
16831
17131
  }
17132
+ printForgetWarnings(result2.warnings);
17133
+ if (result2.alreadyForgotten.length > 0) {
17134
+ for (const id of result2.alreadyForgotten) {
17135
+ console.log(`${id} is not tracked (skipped).`);
17136
+ }
17137
+ }
17138
+ const verb2 = dryRun ? "Would remove" : "Removed";
17139
+ if (result2.removed.length === 1) {
17140
+ const p = result2.removed[0];
17141
+ console.log(`${verb2} 1 patch: "${p.message}" (${p.files.length} files)`);
17142
+ } else {
17143
+ console.log(`${verb2} ${result2.removed.length} patches.`);
17144
+ }
17145
+ console.log(`${result2.remaining} patches remaining.`);
17146
+ return;
16832
17147
  }
16833
- if (!dryRun) {
16834
- console.log("\nNext generation will restore pure generated code for these files.");
17148
+ const searchResult = pattern ? forget(dir, { pattern }) : forget(dir);
17149
+ if (!searchResult.initialized) {
17150
+ console.log("Replay is not initialized. Run 'fern replay init' to get started.");
17151
+ process.exit(1);
17152
+ }
17153
+ const matches = searchResult.matched ?? [];
17154
+ if (matches.length === 0) {
17155
+ if (pattern) {
17156
+ console.log(`No patches found matching "${pattern}". Run 'fern replay status -v' to see all tracked patches.`);
17157
+ } else {
17158
+ console.log("No patches tracked. Nothing to remove.");
17159
+ }
17160
+ return;
17161
+ }
17162
+ if (pattern) {
17163
+ console.log(`Found ${matches.length} patch(es) matching "${pattern}":
17164
+ `);
17165
+ } else {
17166
+ console.log("All tracked patches:\n");
16835
17167
  }
17168
+ for (let i = 0; i < matches.length; i++) {
17169
+ console.log(formatPatchLine(i + 1, matches[i]));
17170
+ }
17171
+ if (!process.stdin.isTTY && !yes) {
17172
+ console.error("\nError: Interactive selection required. Use patch IDs directly or add --yes to remove all matches.");
17173
+ process.exit(1);
17174
+ }
17175
+ let selectedIds;
17176
+ if (yes) {
17177
+ selectedIds = matches.map((m) => m.id);
17178
+ } else {
17179
+ console.log();
17180
+ const input = await promptLine("Remove which? (comma-separated numbers, 'all', or 'none'): ");
17181
+ const selection = parseSelection(input, matches.length);
17182
+ if (selection === "none" || selection === null) {
17183
+ if (selection === null) console.log("Invalid selection.");
17184
+ console.log("Aborted.");
17185
+ return;
17186
+ }
17187
+ selectedIds = selection === "all" ? matches.map((m) => m.id) : selection.map((i) => matches[i - 1].id);
17188
+ console.log(`
17189
+ This will remove ${selectedIds.length} patch(es). Affected files will be overwritten on next generation.`);
17190
+ const confirm = await promptLine("Proceed? [y/N] ");
17191
+ if (confirm.toLowerCase() !== "y") {
17192
+ console.log("Aborted.");
17193
+ return;
17194
+ }
17195
+ }
17196
+ const result = forget(dir, { patchIds: selectedIds, dryRun });
17197
+ printForgetWarnings(result.warnings);
17198
+ const verb = dryRun ? "Would remove" : "Removed";
17199
+ console.log(`
17200
+ ${verb} ${result.removed.length} patch(es).`);
17201
+ console.log(`${result.remaining} patches remaining.`);
16836
17202
  }
16837
17203
  async function runReset(dir, flags) {
16838
17204
  const dryRun = !!flags.dryRun;
@@ -16898,7 +17264,7 @@ Resolve the conflicts in your editor, then run 'fern-replay resolve ${dir}' agai
16898
17264
  console.log("Push to update the PR.");
16899
17265
  }
16900
17266
  async function main() {
16901
- const { command, dir, flags, pattern } = parseArgs(process.argv);
17267
+ const { command, dir, flags, positionals } = parseArgs(process.argv);
16902
17268
  if (!(0, import_node_fs6.existsSync)(dir)) {
16903
17269
  console.error(`Directory not found: ${dir}`);
16904
17270
  process.exit(1);
@@ -16908,7 +17274,7 @@ async function main() {
16908
17274
  await runBootstrap(dir, flags);
16909
17275
  break;
16910
17276
  case "status":
16911
- await runStatus(dir);
17277
+ await runStatus(dir, flags);
16912
17278
  break;
16913
17279
  case "detect":
16914
17280
  await runDetect(dir);
@@ -16920,7 +17286,7 @@ async function main() {
16920
17286
  await runMigrate(dir);
16921
17287
  break;
16922
17288
  case "forget":
16923
- await runForget(dir, flags, pattern);
17289
+ await runForget(dir, flags, positionals);
16924
17290
  break;
16925
17291
  case "reset":
16926
17292
  await runReset(dir, flags);