@fern-api/replay 0.16.0 → 0.16.1

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
@@ -5485,6 +5485,215 @@ var init_GitClient = __esm({
5485
5485
  }
5486
5486
  });
5487
5487
 
5488
+ // node_modules/balanced-match/index.js
5489
+ var require_balanced_match = __commonJS({
5490
+ "node_modules/balanced-match/index.js"(exports2, module2) {
5491
+ "use strict";
5492
+ module2.exports = balanced;
5493
+ function balanced(a, b, str) {
5494
+ if (a instanceof RegExp) a = maybeMatch(a, str);
5495
+ if (b instanceof RegExp) b = maybeMatch(b, str);
5496
+ var r = range(a, b, str);
5497
+ return r && {
5498
+ start: r[0],
5499
+ end: r[1],
5500
+ pre: str.slice(0, r[0]),
5501
+ body: str.slice(r[0] + a.length, r[1]),
5502
+ post: str.slice(r[1] + b.length)
5503
+ };
5504
+ }
5505
+ function maybeMatch(reg, str) {
5506
+ var m = str.match(reg);
5507
+ return m ? m[0] : null;
5508
+ }
5509
+ balanced.range = range;
5510
+ function range(a, b, str) {
5511
+ var begs, beg, left, right, result;
5512
+ var ai = str.indexOf(a);
5513
+ var bi = str.indexOf(b, ai + 1);
5514
+ var i = ai;
5515
+ if (ai >= 0 && bi > 0) {
5516
+ if (a === b) {
5517
+ return [ai, bi];
5518
+ }
5519
+ begs = [];
5520
+ left = str.length;
5521
+ while (i >= 0 && !result) {
5522
+ if (i == ai) {
5523
+ begs.push(i);
5524
+ ai = str.indexOf(a, i + 1);
5525
+ } else if (begs.length == 1) {
5526
+ result = [begs.pop(), bi];
5527
+ } else {
5528
+ beg = begs.pop();
5529
+ if (beg < left) {
5530
+ left = beg;
5531
+ right = bi;
5532
+ }
5533
+ bi = str.indexOf(b, i + 1);
5534
+ }
5535
+ i = ai < bi && ai >= 0 ? ai : bi;
5536
+ }
5537
+ if (begs.length) {
5538
+ result = [left, right];
5539
+ }
5540
+ }
5541
+ return result;
5542
+ }
5543
+ }
5544
+ });
5545
+
5546
+ // node_modules/brace-expansion/index.js
5547
+ var require_brace_expansion = __commonJS({
5548
+ "node_modules/brace-expansion/index.js"(exports2, module2) {
5549
+ "use strict";
5550
+ var balanced = require_balanced_match();
5551
+ module2.exports = expandTop;
5552
+ var escSlash = "\0SLASH" + Math.random() + "\0";
5553
+ var escOpen = "\0OPEN" + Math.random() + "\0";
5554
+ var escClose = "\0CLOSE" + Math.random() + "\0";
5555
+ var escComma = "\0COMMA" + Math.random() + "\0";
5556
+ var escPeriod = "\0PERIOD" + Math.random() + "\0";
5557
+ function numeric(str) {
5558
+ return parseInt(str, 10) == str ? parseInt(str, 10) : str.charCodeAt(0);
5559
+ }
5560
+ function escapeBraces(str) {
5561
+ return str.split("\\\\").join(escSlash).split("\\{").join(escOpen).split("\\}").join(escClose).split("\\,").join(escComma).split("\\.").join(escPeriod);
5562
+ }
5563
+ function unescapeBraces(str) {
5564
+ return str.split(escSlash).join("\\").split(escOpen).join("{").split(escClose).join("}").split(escComma).join(",").split(escPeriod).join(".");
5565
+ }
5566
+ function parseCommaParts(str) {
5567
+ if (!str)
5568
+ return [""];
5569
+ var parts = [];
5570
+ var m = balanced("{", "}", str);
5571
+ if (!m)
5572
+ return str.split(",");
5573
+ var pre = m.pre;
5574
+ var body = m.body;
5575
+ var post = m.post;
5576
+ var p = pre.split(",");
5577
+ p[p.length - 1] += "{" + body + "}";
5578
+ var postParts = parseCommaParts(post);
5579
+ if (post.length) {
5580
+ p[p.length - 1] += postParts.shift();
5581
+ p.push.apply(p, postParts);
5582
+ }
5583
+ parts.push.apply(parts, p);
5584
+ return parts;
5585
+ }
5586
+ function expandTop(str) {
5587
+ if (!str)
5588
+ return [];
5589
+ if (str.substr(0, 2) === "{}") {
5590
+ str = "\\{\\}" + str.substr(2);
5591
+ }
5592
+ return expand2(escapeBraces(str), true).map(unescapeBraces);
5593
+ }
5594
+ function embrace(str) {
5595
+ return "{" + str + "}";
5596
+ }
5597
+ function isPadded(el) {
5598
+ return /^-?0\d/.test(el);
5599
+ }
5600
+ function lte(i, y) {
5601
+ return i <= y;
5602
+ }
5603
+ function gte(i, y) {
5604
+ return i >= y;
5605
+ }
5606
+ function expand2(str, isTop) {
5607
+ var expansions = [];
5608
+ var m = balanced("{", "}", str);
5609
+ if (!m) return [str];
5610
+ var pre = m.pre;
5611
+ var post = m.post.length ? expand2(m.post, false) : [""];
5612
+ if (/\$$/.test(m.pre)) {
5613
+ for (var k = 0; k < post.length; k++) {
5614
+ var expansion = pre + "{" + m.body + "}" + post[k];
5615
+ expansions.push(expansion);
5616
+ }
5617
+ } else {
5618
+ var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
5619
+ var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
5620
+ var isSequence = isNumericSequence || isAlphaSequence;
5621
+ var isOptions = m.body.indexOf(",") >= 0;
5622
+ if (!isSequence && !isOptions) {
5623
+ if (m.post.match(/,(?!,).*\}/)) {
5624
+ str = m.pre + "{" + m.body + escClose + m.post;
5625
+ return expand2(str);
5626
+ }
5627
+ return [str];
5628
+ }
5629
+ var n;
5630
+ if (isSequence) {
5631
+ n = m.body.split(/\.\./);
5632
+ } else {
5633
+ n = parseCommaParts(m.body);
5634
+ if (n.length === 1) {
5635
+ n = expand2(n[0], false).map(embrace);
5636
+ if (n.length === 1) {
5637
+ return post.map(function(p) {
5638
+ return m.pre + n[0] + p;
5639
+ });
5640
+ }
5641
+ }
5642
+ }
5643
+ var N;
5644
+ if (isSequence) {
5645
+ var x = numeric(n[0]);
5646
+ var y = numeric(n[1]);
5647
+ var width = Math.max(n[0].length, n[1].length);
5648
+ var incr = n.length == 3 ? Math.abs(numeric(n[2])) : 1;
5649
+ var test = lte;
5650
+ var reverse = y < x;
5651
+ if (reverse) {
5652
+ incr *= -1;
5653
+ test = gte;
5654
+ }
5655
+ var pad = n.some(isPadded);
5656
+ N = [];
5657
+ for (var i = x; test(i, y); i += incr) {
5658
+ var c;
5659
+ if (isAlphaSequence) {
5660
+ c = String.fromCharCode(i);
5661
+ if (c === "\\")
5662
+ c = "";
5663
+ } else {
5664
+ c = String(i);
5665
+ if (pad) {
5666
+ var need = width - c.length;
5667
+ if (need > 0) {
5668
+ var z = new Array(need + 1).join("0");
5669
+ if (i < 0)
5670
+ c = "-" + z + c.slice(1);
5671
+ else
5672
+ c = z + c;
5673
+ }
5674
+ }
5675
+ }
5676
+ N.push(c);
5677
+ }
5678
+ } else {
5679
+ N = [];
5680
+ for (var j = 0; j < n.length; j++) {
5681
+ N.push.apply(N, expand2(n[j], false));
5682
+ }
5683
+ }
5684
+ for (var j = 0; j < N.length; j++) {
5685
+ for (var k = 0; k < post.length; k++) {
5686
+ var expansion = pre + N[j] + post[k];
5687
+ if (!isTop || isSequence || expansion)
5688
+ expansions.push(expansion);
5689
+ }
5690
+ }
5691
+ }
5692
+ return expansions;
5693
+ }
5694
+ }
5695
+ });
5696
+
5488
5697
  // node_modules/yaml/dist/nodes/identity.js
5489
5698
  var require_identity = __commonJS({
5490
5699
  "node_modules/yaml/dist/nodes/identity.js"(exports2) {
@@ -12774,243 +12983,34 @@ var require_dist3 = __commonJS({
12774
12983
  }
12775
12984
  });
12776
12985
 
12777
- // node_modules/balanced-match/index.js
12778
- var require_balanced_match = __commonJS({
12779
- "node_modules/balanced-match/index.js"(exports2, module2) {
12780
- "use strict";
12781
- module2.exports = balanced;
12782
- function balanced(a, b, str) {
12783
- if (a instanceof RegExp) a = maybeMatch(a, str);
12784
- if (b instanceof RegExp) b = maybeMatch(b, str);
12785
- var r = range(a, b, str);
12786
- return r && {
12787
- start: r[0],
12788
- end: r[1],
12789
- pre: str.slice(0, r[0]),
12790
- body: str.slice(r[0] + a.length, r[1]),
12791
- post: str.slice(r[1] + b.length)
12986
+ // src/HybridReconstruction.ts
12987
+ var HybridReconstruction_exports = {};
12988
+ __export(HybridReconstruction_exports, {
12989
+ assembleHybrid: () => assembleHybrid,
12990
+ locateHunksInOurs: () => locateHunksInOurs,
12991
+ parseHunks: () => parseHunks,
12992
+ reconstructFromGhostPatch: () => reconstructFromGhostPatch
12993
+ });
12994
+ function parseHunks(fileDiff) {
12995
+ const lines = fileDiff.split("\n");
12996
+ const hunks = [];
12997
+ let currentHunk = null;
12998
+ for (const line of lines) {
12999
+ const headerMatch = line.match(
13000
+ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/
13001
+ );
13002
+ if (headerMatch) {
13003
+ if (currentHunk) {
13004
+ hunks.push(currentHunk);
13005
+ }
13006
+ currentHunk = {
13007
+ oldStart: parseInt(headerMatch[1], 10),
13008
+ oldCount: headerMatch[2] != null ? parseInt(headerMatch[2], 10) : 1,
13009
+ newStart: parseInt(headerMatch[3], 10),
13010
+ newCount: headerMatch[4] != null ? parseInt(headerMatch[4], 10) : 1,
13011
+ lines: []
12792
13012
  };
12793
- }
12794
- function maybeMatch(reg, str) {
12795
- var m = str.match(reg);
12796
- return m ? m[0] : null;
12797
- }
12798
- balanced.range = range;
12799
- function range(a, b, str) {
12800
- var begs, beg, left, right, result;
12801
- var ai = str.indexOf(a);
12802
- var bi = str.indexOf(b, ai + 1);
12803
- var i = ai;
12804
- if (ai >= 0 && bi > 0) {
12805
- if (a === b) {
12806
- return [ai, bi];
12807
- }
12808
- begs = [];
12809
- left = str.length;
12810
- while (i >= 0 && !result) {
12811
- if (i == ai) {
12812
- begs.push(i);
12813
- ai = str.indexOf(a, i + 1);
12814
- } else if (begs.length == 1) {
12815
- result = [begs.pop(), bi];
12816
- } else {
12817
- beg = begs.pop();
12818
- if (beg < left) {
12819
- left = beg;
12820
- right = bi;
12821
- }
12822
- bi = str.indexOf(b, i + 1);
12823
- }
12824
- i = ai < bi && ai >= 0 ? ai : bi;
12825
- }
12826
- if (begs.length) {
12827
- result = [left, right];
12828
- }
12829
- }
12830
- return result;
12831
- }
12832
- }
12833
- });
12834
-
12835
- // node_modules/brace-expansion/index.js
12836
- var require_brace_expansion = __commonJS({
12837
- "node_modules/brace-expansion/index.js"(exports2, module2) {
12838
- "use strict";
12839
- var balanced = require_balanced_match();
12840
- module2.exports = expandTop;
12841
- var escSlash = "\0SLASH" + Math.random() + "\0";
12842
- var escOpen = "\0OPEN" + Math.random() + "\0";
12843
- var escClose = "\0CLOSE" + Math.random() + "\0";
12844
- var escComma = "\0COMMA" + Math.random() + "\0";
12845
- var escPeriod = "\0PERIOD" + Math.random() + "\0";
12846
- function numeric(str) {
12847
- return parseInt(str, 10) == str ? parseInt(str, 10) : str.charCodeAt(0);
12848
- }
12849
- function escapeBraces(str) {
12850
- return str.split("\\\\").join(escSlash).split("\\{").join(escOpen).split("\\}").join(escClose).split("\\,").join(escComma).split("\\.").join(escPeriod);
12851
- }
12852
- function unescapeBraces(str) {
12853
- return str.split(escSlash).join("\\").split(escOpen).join("{").split(escClose).join("}").split(escComma).join(",").split(escPeriod).join(".");
12854
- }
12855
- function parseCommaParts(str) {
12856
- if (!str)
12857
- return [""];
12858
- var parts = [];
12859
- var m = balanced("{", "}", str);
12860
- if (!m)
12861
- return str.split(",");
12862
- var pre = m.pre;
12863
- var body = m.body;
12864
- var post = m.post;
12865
- var p = pre.split(",");
12866
- p[p.length - 1] += "{" + body + "}";
12867
- var postParts = parseCommaParts(post);
12868
- if (post.length) {
12869
- p[p.length - 1] += postParts.shift();
12870
- p.push.apply(p, postParts);
12871
- }
12872
- parts.push.apply(parts, p);
12873
- return parts;
12874
- }
12875
- function expandTop(str) {
12876
- if (!str)
12877
- return [];
12878
- if (str.substr(0, 2) === "{}") {
12879
- str = "\\{\\}" + str.substr(2);
12880
- }
12881
- return expand2(escapeBraces(str), true).map(unescapeBraces);
12882
- }
12883
- function embrace(str) {
12884
- return "{" + str + "}";
12885
- }
12886
- function isPadded(el) {
12887
- return /^-?0\d/.test(el);
12888
- }
12889
- function lte(i, y) {
12890
- return i <= y;
12891
- }
12892
- function gte(i, y) {
12893
- return i >= y;
12894
- }
12895
- function expand2(str, isTop) {
12896
- var expansions = [];
12897
- var m = balanced("{", "}", str);
12898
- if (!m) return [str];
12899
- var pre = m.pre;
12900
- var post = m.post.length ? expand2(m.post, false) : [""];
12901
- if (/\$$/.test(m.pre)) {
12902
- for (var k = 0; k < post.length; k++) {
12903
- var expansion = pre + "{" + m.body + "}" + post[k];
12904
- expansions.push(expansion);
12905
- }
12906
- } else {
12907
- var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
12908
- var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
12909
- var isSequence = isNumericSequence || isAlphaSequence;
12910
- var isOptions = m.body.indexOf(",") >= 0;
12911
- if (!isSequence && !isOptions) {
12912
- if (m.post.match(/,(?!,).*\}/)) {
12913
- str = m.pre + "{" + m.body + escClose + m.post;
12914
- return expand2(str);
12915
- }
12916
- return [str];
12917
- }
12918
- var n;
12919
- if (isSequence) {
12920
- n = m.body.split(/\.\./);
12921
- } else {
12922
- n = parseCommaParts(m.body);
12923
- if (n.length === 1) {
12924
- n = expand2(n[0], false).map(embrace);
12925
- if (n.length === 1) {
12926
- return post.map(function(p) {
12927
- return m.pre + n[0] + p;
12928
- });
12929
- }
12930
- }
12931
- }
12932
- var N;
12933
- if (isSequence) {
12934
- var x = numeric(n[0]);
12935
- var y = numeric(n[1]);
12936
- var width = Math.max(n[0].length, n[1].length);
12937
- var incr = n.length == 3 ? Math.abs(numeric(n[2])) : 1;
12938
- var test = lte;
12939
- var reverse = y < x;
12940
- if (reverse) {
12941
- incr *= -1;
12942
- test = gte;
12943
- }
12944
- var pad = n.some(isPadded);
12945
- N = [];
12946
- for (var i = x; test(i, y); i += incr) {
12947
- var c;
12948
- if (isAlphaSequence) {
12949
- c = String.fromCharCode(i);
12950
- if (c === "\\")
12951
- c = "";
12952
- } else {
12953
- c = String(i);
12954
- if (pad) {
12955
- var need = width - c.length;
12956
- if (need > 0) {
12957
- var z = new Array(need + 1).join("0");
12958
- if (i < 0)
12959
- c = "-" + z + c.slice(1);
12960
- else
12961
- c = z + c;
12962
- }
12963
- }
12964
- }
12965
- N.push(c);
12966
- }
12967
- } else {
12968
- N = [];
12969
- for (var j = 0; j < n.length; j++) {
12970
- N.push.apply(N, expand2(n[j], false));
12971
- }
12972
- }
12973
- for (var j = 0; j < N.length; j++) {
12974
- for (var k = 0; k < post.length; k++) {
12975
- var expansion = pre + N[j] + post[k];
12976
- if (!isTop || isSequence || expansion)
12977
- expansions.push(expansion);
12978
- }
12979
- }
12980
- }
12981
- return expansions;
12982
- }
12983
- }
12984
- });
12985
-
12986
- // src/HybridReconstruction.ts
12987
- var HybridReconstruction_exports = {};
12988
- __export(HybridReconstruction_exports, {
12989
- assembleHybrid: () => assembleHybrid,
12990
- locateHunksInOurs: () => locateHunksInOurs,
12991
- parseHunks: () => parseHunks,
12992
- reconstructFromGhostPatch: () => reconstructFromGhostPatch
12993
- });
12994
- function parseHunks(fileDiff) {
12995
- const lines = fileDiff.split("\n");
12996
- const hunks = [];
12997
- let currentHunk = null;
12998
- for (const line of lines) {
12999
- const headerMatch = line.match(
13000
- /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/
13001
- );
13002
- if (headerMatch) {
13003
- if (currentHunk) {
13004
- hunks.push(currentHunk);
13005
- }
13006
- currentHunk = {
13007
- oldStart: parseInt(headerMatch[1], 10),
13008
- oldCount: headerMatch[2] != null ? parseInt(headerMatch[2], 10) : 1,
13009
- newStart: parseInt(headerMatch[3], 10),
13010
- newCount: headerMatch[4] != null ? parseInt(headerMatch[4], 10) : 1,
13011
- lines: []
13012
- };
13013
- continue;
13013
+ continue;
13014
13014
  }
13015
13015
  if (!currentHunk) continue;
13016
13016
  if (line.startsWith("diff --git") || line.startsWith("index ") || line.startsWith("---") || line.startsWith("+++") || line.startsWith("old mode") || line.startsWith("new mode") || line.startsWith("similarity index") || line.startsWith("rename from") || line.startsWith("rename to") || line.startsWith("new file mode") || line.startsWith("deleted file mode")) {
@@ -13299,2244 +13299,2262 @@ init_GitClient();
13299
13299
  // src/LockfileManager.ts
13300
13300
  var import_node_fs = require("fs");
13301
13301
  var import_node_path2 = require("path");
13302
- var import_yaml = __toESM(require_dist3(), 1);
13303
13302
 
13304
- // src/credentials.ts
13305
- var CREDENTIAL_PATTERNS = [
13306
- { name: "PRIVATE KEY block", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
13307
- { name: "OPENSSH key", re: /-----BEGIN OPENSSH PRIVATE KEY-----/ },
13308
- { name: "RSA key", re: /-----BEGIN RSA PRIVATE KEY-----/ },
13309
- { name: "CERTIFICATE", re: /-----BEGIN CERTIFICATE-----/ },
13310
- { name: "TOKEN=", re: /\bTOKEN\s*=\s*["']?[A-Za-z0-9._\-+/=]{10,}/ },
13311
- { name: "api_key=", re: /\bapi[_-]?key\s*=\s*["']?[A-Za-z0-9._\-+/=]{10,}/i },
13312
- { name: "password=", re: /\bpassword\s*=\s*["']?[^\s"']{6,}/i },
13313
- { name: "AWS_SECRET_ACCESS_KEY", re: /AWS_SECRET_ACCESS_KEY/ },
13314
- { name: "JWT (eyJ...)", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ }
13315
- ];
13316
- var SENSITIVE_FILE_PATTERNS = [
13317
- /(^|\/)\.env(\.[^/]+)?$/,
13318
- /\.pem$/,
13319
- /\.key$/,
13320
- /\.pkce_state\.json$/,
13321
- /(^|\/)id_rsa$/,
13322
- /(^|\/)id_ed25519$/
13323
- ];
13324
- function isSensitiveFile(filePath) {
13325
- return SENSITIVE_FILE_PATTERNS.some((re) => re.test(filePath));
13326
- }
13327
- function scanForCredentials(content) {
13328
- const matches = [];
13329
- for (const { name, re } of CREDENTIAL_PATTERNS) {
13330
- if (re.test(content)) {
13331
- matches.push(name);
13332
- }
13333
- }
13334
- return matches;
13335
- }
13303
+ // node_modules/minimatch/dist/esm/index.js
13304
+ var import_brace_expansion = __toESM(require_brace_expansion(), 1);
13336
13305
 
13337
- // src/LockfileManager.ts
13338
- var LOCKFILE_HEADER = "# DO NOT EDIT MANUALLY - Managed by Fern Replay\n";
13339
- var LockfileNotFoundError = class extends Error {
13340
- constructor(path2) {
13341
- super(`Lockfile not found: ${path2}`);
13342
- this.name = "LockfileNotFoundError";
13343
- }
13344
- };
13345
- var LockfileManager = class {
13346
- outputDir;
13347
- lock = null;
13348
- warnings = [];
13349
- constructor(outputDir) {
13350
- this.outputDir = outputDir;
13351
- }
13352
- get lockfilePath() {
13353
- return (0, import_node_path2.join)(this.outputDir, ".fern", "replay.lock");
13306
+ // node_modules/minimatch/dist/esm/assert-valid-pattern.js
13307
+ var MAX_PATTERN_LENGTH = 1024 * 64;
13308
+ var assertValidPattern = (pattern) => {
13309
+ if (typeof pattern !== "string") {
13310
+ throw new TypeError("invalid pattern");
13354
13311
  }
13355
- get customizationsPath() {
13356
- return (0, import_node_path2.join)(this.outputDir, ".fern", "replay.yml");
13312
+ if (pattern.length > MAX_PATTERN_LENGTH) {
13313
+ throw new TypeError("pattern is too long");
13357
13314
  }
13358
- exists() {
13359
- return (0, import_node_fs.existsSync)(this.lockfilePath);
13315
+ };
13316
+
13317
+ // node_modules/minimatch/dist/esm/brace-expressions.js
13318
+ var posixClasses = {
13319
+ "[:alnum:]": ["\\p{L}\\p{Nl}\\p{Nd}", true],
13320
+ "[:alpha:]": ["\\p{L}\\p{Nl}", true],
13321
+ "[:ascii:]": ["\\x00-\\x7f", false],
13322
+ "[:blank:]": ["\\p{Zs}\\t", true],
13323
+ "[:cntrl:]": ["\\p{Cc}", true],
13324
+ "[:digit:]": ["\\p{Nd}", true],
13325
+ "[:graph:]": ["\\p{Z}\\p{C}", true, true],
13326
+ "[:lower:]": ["\\p{Ll}", true],
13327
+ "[:print:]": ["\\p{C}", true],
13328
+ "[:punct:]": ["\\p{P}", true],
13329
+ "[:space:]": ["\\p{Z}\\t\\r\\n\\v\\f", true],
13330
+ "[:upper:]": ["\\p{Lu}", true],
13331
+ "[:word:]": ["\\p{L}\\p{Nl}\\p{Nd}\\p{Pc}", true],
13332
+ "[:xdigit:]": ["A-Fa-f0-9", false]
13333
+ };
13334
+ var braceEscape = (s) => s.replace(/[[\]\\-]/g, "\\$&");
13335
+ var regexpEscape = (s) => s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
13336
+ var rangesToString = (ranges) => ranges.join("");
13337
+ var parseClass = (glob, position) => {
13338
+ const pos = position;
13339
+ if (glob.charAt(pos) !== "[") {
13340
+ throw new Error("not in a brace expression");
13360
13341
  }
13361
- read() {
13362
- if (this.lock) {
13363
- return this.lock;
13342
+ const ranges = [];
13343
+ const negs = [];
13344
+ let i = pos + 1;
13345
+ let sawStart = false;
13346
+ let uflag = false;
13347
+ let escaping = false;
13348
+ let negate = false;
13349
+ let endPos = pos;
13350
+ let rangeStart = "";
13351
+ WHILE: while (i < glob.length) {
13352
+ const c = glob.charAt(i);
13353
+ if ((c === "!" || c === "^") && i === pos + 1) {
13354
+ negate = true;
13355
+ i++;
13356
+ continue;
13364
13357
  }
13365
- try {
13366
- const content = (0, import_node_fs.readFileSync)(this.lockfilePath, "utf-8");
13367
- this.lock = (0, import_yaml.parse)(content);
13368
- return this.lock;
13369
- } catch (error) {
13370
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
13371
- throw new LockfileNotFoundError(this.lockfilePath);
13372
- }
13373
- throw error;
13358
+ if (c === "]" && sawStart && !escaping) {
13359
+ endPos = i + 1;
13360
+ break;
13374
13361
  }
13375
- }
13376
- initialize(firstGeneration) {
13377
- this.initializeInMemory(firstGeneration);
13378
- this.save();
13379
- }
13380
- /**
13381
- * Set up in-memory lock state without writing to disk.
13382
- * Useful for bootstrap dry-run where we need state for detection
13383
- * but don't want to persist anything.
13384
- */
13385
- initializeInMemory(firstGeneration) {
13386
- this.lock = {
13387
- version: "1.0",
13388
- generations: [firstGeneration],
13389
- current_generation: firstGeneration.commit_sha,
13390
- patches: []
13391
- };
13392
- }
13393
- save() {
13394
- if (!this.lock) {
13395
- throw new Error("No lockfile data to save. Call read() or initialize() first.");
13362
+ sawStart = true;
13363
+ if (c === "\\") {
13364
+ if (!escaping) {
13365
+ escaping = true;
13366
+ i++;
13367
+ continue;
13368
+ }
13396
13369
  }
13397
- this.warnings.length = 0;
13398
- const cleanPatches = [];
13399
- for (const patch of this.lock.patches) {
13400
- const diffMatches = scanForCredentials(patch.patch_content);
13401
- const snapshotMatches = [];
13402
- if (patch.theirs_snapshot) {
13403
- for (const content2 of Object.values(patch.theirs_snapshot)) {
13404
- const m = scanForCredentials(content2);
13405
- for (const x of m) {
13406
- if (!snapshotMatches.includes(x)) snapshotMatches.push(x);
13370
+ if (c === "[" && !escaping) {
13371
+ for (const [cls, [unip, u, neg]] of Object.entries(posixClasses)) {
13372
+ if (glob.startsWith(cls, i)) {
13373
+ if (rangeStart) {
13374
+ return ["$.", false, glob.length - pos, true];
13407
13375
  }
13376
+ i += cls.length;
13377
+ if (neg)
13378
+ negs.push(unip);
13379
+ else
13380
+ ranges.push(unip);
13381
+ uflag = uflag || u;
13382
+ continue WHILE;
13408
13383
  }
13409
13384
  }
13410
- const credentialMatches = [...diffMatches, ...snapshotMatches.filter((m) => !diffMatches.includes(m))];
13411
- const sensitiveFiles = patch.files.filter((f) => isSensitiveFile(f));
13412
- if (credentialMatches.length > 0 || sensitiveFiles.length > 0) {
13413
- const reasons = [];
13414
- if (credentialMatches.length > 0) {
13415
- reasons.push(`credential patterns: ${credentialMatches.join(", ")}`);
13416
- }
13417
- if (sensitiveFiles.length > 0) {
13418
- reasons.push(`sensitive files: ${sensitiveFiles.join(", ")}`);
13419
- }
13420
- this.warnings.push(
13421
- `Patch ${patch.id} removed from lockfile: ${reasons.join("; ")}. This prevents credential exposure in committed lockfiles.`
13422
- );
13423
- } else {
13424
- cleanPatches.push(patch);
13425
- }
13426
- }
13427
- this.lock.patches = cleanPatches;
13428
- const dir = (0, import_node_path2.dirname)(this.lockfilePath);
13429
- if (!(0, import_node_fs.existsSync)(dir)) {
13430
- (0, import_node_fs.mkdirSync)(dir, { recursive: true });
13431
- }
13432
- const yaml = (0, import_yaml.stringify)(this.lock, {
13433
- lineWidth: 0,
13434
- blockQuote: "literal"
13435
- });
13436
- const content = LOCKFILE_HEADER + yaml;
13437
- const tmpPath = this.lockfilePath + ".tmp";
13438
- (0, import_node_fs.writeFileSync)(tmpPath, content, "utf-8");
13439
- (0, import_node_fs.renameSync)(tmpPath, this.lockfilePath);
13440
- }
13441
- addGeneration(record) {
13442
- this.ensureLoaded();
13443
- this.lock.generations.push(record);
13444
- this.lock.current_generation = record.commit_sha;
13445
- }
13446
- addPatch(patch) {
13447
- this.ensureLoaded();
13448
- this.lock.patches.push(patch);
13449
- }
13450
- updatePatch(patchId, updates) {
13451
- this.ensureLoaded();
13452
- const patch = this.lock.patches.find((p) => p.id === patchId);
13453
- if (!patch) {
13454
- throw new Error(`Patch not found: ${patchId}`);
13455
13385
  }
13456
- Object.assign(patch, updates);
13457
- }
13458
- removePatch(patchId) {
13459
- this.ensureLoaded();
13460
- this.lock.patches = this.lock.patches.filter((p) => p.id !== patchId);
13461
- }
13462
- clearPatches() {
13463
- this.ensureLoaded();
13464
- this.lock.patches = [];
13465
- }
13466
- addForgottenHash(hash) {
13467
- this.ensureLoaded();
13468
- if (!this.lock.forgotten_hashes) {
13469
- this.lock.forgotten_hashes = [];
13386
+ escaping = false;
13387
+ if (rangeStart) {
13388
+ if (c > rangeStart) {
13389
+ ranges.push(braceEscape(rangeStart) + "-" + braceEscape(c));
13390
+ } else if (c === rangeStart) {
13391
+ ranges.push(braceEscape(c));
13392
+ }
13393
+ rangeStart = "";
13394
+ i++;
13395
+ continue;
13470
13396
  }
13471
- if (!this.lock.forgotten_hashes.includes(hash)) {
13472
- this.lock.forgotten_hashes.push(hash);
13397
+ if (glob.startsWith("-]", i + 1)) {
13398
+ ranges.push(braceEscape(c + "-"));
13399
+ i += 2;
13400
+ continue;
13473
13401
  }
13474
- }
13475
- getUnresolvedPatches() {
13476
- this.ensureLoaded();
13477
- return this.lock.patches.filter((p) => p.status === "unresolved");
13478
- }
13479
- getResolvingPatches() {
13480
- this.ensureLoaded();
13481
- return this.lock.patches.filter((p) => p.status === "resolving");
13482
- }
13483
- markPatchUnresolved(patchId) {
13484
- this.updatePatch(patchId, { status: "unresolved" });
13485
- }
13486
- markPatchResolved(patchId, updates) {
13487
- this.ensureLoaded();
13488
- const patch = this.lock.patches.find((p) => p.id === patchId);
13489
- if (!patch) {
13490
- throw new Error(`Patch not found: ${patchId}`);
13402
+ if (glob.startsWith("-", i + 1)) {
13403
+ rangeStart = c;
13404
+ i += 2;
13405
+ continue;
13491
13406
  }
13492
- delete patch.status;
13493
- Object.assign(patch, updates);
13494
- }
13495
- getPatches() {
13496
- this.ensureLoaded();
13497
- return this.lock.patches;
13498
- }
13499
- setReplaySkippedAt(timestamp) {
13500
- this.ensureLoaded();
13501
- this.lock.replay_skipped_at = timestamp;
13407
+ ranges.push(braceEscape(c));
13408
+ i++;
13502
13409
  }
13503
- clearReplaySkippedAt() {
13504
- this.ensureLoaded();
13505
- delete this.lock.replay_skipped_at;
13410
+ if (endPos < i) {
13411
+ return ["", false, 0, false];
13506
13412
  }
13507
- isReplaySkipped() {
13508
- this.ensureLoaded();
13509
- return this.lock.replay_skipped_at != null;
13413
+ if (!ranges.length && !negs.length) {
13414
+ return ["$.", false, glob.length - pos, true];
13510
13415
  }
13511
- getGeneration(commitSha) {
13512
- this.ensureLoaded();
13513
- return this.lock.generations.find((g) => g.commit_sha === commitSha);
13514
- }
13515
- getCustomizationsConfig() {
13516
- if (!(0, import_node_fs.existsSync)(this.customizationsPath)) {
13517
- return {};
13518
- }
13519
- const content = (0, import_node_fs.readFileSync)(this.customizationsPath, "utf-8");
13520
- return (0, import_yaml.parse)(content) ?? {};
13521
- }
13522
- ensureLoaded() {
13523
- if (!this.lock) {
13524
- throw new Error("No lockfile loaded. Call read() or initialize() first.");
13525
- }
13416
+ if (negs.length === 0 && ranges.length === 1 && /^\\?.$/.test(ranges[0]) && !negate) {
13417
+ const r = ranges[0].length === 2 ? ranges[0].slice(-1) : ranges[0];
13418
+ return [regexpEscape(r), false, endPos - pos, false];
13526
13419
  }
13420
+ const sranges = "[" + (negate ? "^" : "") + rangesToString(ranges) + "]";
13421
+ const snegs = "[" + (negate ? "" : "^") + rangesToString(negs) + "]";
13422
+ const comb = ranges.length && negs.length ? "(" + sranges + "|" + snegs + ")" : ranges.length ? sranges : snegs;
13423
+ return [comb, uflag, endPos - pos, true];
13527
13424
  };
13528
13425
 
13529
- // src/ReplayDetector.ts
13530
- var import_node_crypto = require("crypto");
13531
-
13532
- // src/shared/binary.ts
13533
- var import_node_path3 = require("path");
13534
- var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
13535
- ".png",
13536
- ".jpg",
13537
- ".jpeg",
13538
- ".gif",
13539
- ".bmp",
13540
- ".ico",
13541
- ".webp",
13542
- ".svg",
13543
- ".pdf",
13544
- ".doc",
13545
- ".docx",
13546
- ".xls",
13547
- ".xlsx",
13548
- ".ppt",
13549
- ".pptx",
13550
- ".zip",
13551
- ".gz",
13552
- ".tar",
13553
- ".bz2",
13554
- ".7z",
13555
- ".rar",
13556
- ".jar",
13557
- ".war",
13558
- ".ear",
13559
- ".class",
13560
- ".exe",
13561
- ".dll",
13562
- ".so",
13563
- ".dylib",
13564
- ".o",
13565
- ".a",
13566
- ".woff",
13567
- ".woff2",
13568
- ".ttf",
13569
- ".eot",
13570
- ".otf",
13571
- ".mp3",
13572
- ".mp4",
13573
- ".avi",
13574
- ".mov",
13575
- ".wav",
13576
- ".flac",
13577
- ".sqlite",
13578
- ".db",
13579
- ".pyc",
13580
- ".pyo",
13581
- ".DS_Store"
13582
- ]);
13583
- function isBinaryFile(filePath) {
13584
- const ext2 = (0, import_node_path3.extname)(filePath).toLowerCase();
13585
- return BINARY_EXTENSIONS.has(ext2);
13586
- }
13587
-
13588
- // src/git/CommitDetection.ts
13589
- var FERN_BOT_NAME = "fern-api";
13590
- var FERN_BOT_EMAIL = "115122769+fern-api[bot]@users.noreply.github.com";
13591
- var FERN_BOT_LOGIN = "fern-api[bot]";
13592
- var FERN_SUPPORT_NAMES = ["fern-support", "Fern Support"];
13593
- function isGenerationCommit(commit) {
13594
- const isFernSupport = FERN_SUPPORT_NAMES.includes(commit.authorName);
13595
- const isBotAuthor = !isFernSupport && (commit.authorLogin === FERN_BOT_LOGIN || commit.authorEmail === FERN_BOT_EMAIL || commit.authorName === FERN_BOT_NAME);
13596
- const hasGenerationMarker = commit.message.startsWith("[fern-generated]") || commit.message.startsWith("[fern-replay]") || commit.message.startsWith("[fern-autoversion]") || commit.message.includes("Generated by Fern") || commit.message.includes("\u{1F916} Generated with Fern") || // Squash merge of a Fern-generated PR uses the PR title as commit message.
13597
- // The default PR title is "SDK Generation" (from GithubStep's commitMessage default).
13598
- // GitHub appends "(#N)" for the PR number, e.g. "SDK Generation (#70)".
13599
- commit.message.startsWith("SDK Generation") || // Merge commit (GitHub's "Create a merge commit" option) of a fern-bot
13600
- // regen PR. First-parent walking lands on this commit, not on the
13601
- // generation commit on the side branch, so the classifier must treat
13602
- // it as a generation boundary. GitHub's default subject is
13603
- // "Merge pull request #N from <owner>/<branch>"; fern-bot branches are
13604
- // named "<owner>/fern-bot/<...>".
13605
- /^Merge pull request #\d+ from [^/]+\/fern-bot\//.test(commit.message);
13606
- return isBotAuthor || hasGenerationMarker;
13607
- }
13608
- function isGenerationBoundary(commit) {
13609
- const isFernSupport = FERN_SUPPORT_NAMES.includes(commit.authorName);
13610
- const isBotAuthor = !isFernSupport && (commit.authorLogin === FERN_BOT_LOGIN || commit.authorEmail === FERN_BOT_EMAIL || commit.authorName === FERN_BOT_NAME);
13611
- const isAutoversion = commit.message.startsWith("[fern-autoversion]");
13612
- const hasBoundaryMarker = commit.message.startsWith("[fern-generated]") || commit.message.startsWith("[fern-replay]") || commit.message.includes("Generated by Fern") || commit.message.includes("\u{1F916} Generated with Fern") || commit.message.startsWith("SDK Generation");
13613
- return isBotAuthor && !isAutoversion || hasBoundaryMarker;
13614
- }
13615
- function isReplayCommit(commit) {
13616
- return commit.message.startsWith("[fern-replay]");
13617
- }
13618
- function isRevertCommit(message) {
13619
- return /^Revert ".+"$/.test(message);
13620
- }
13621
- function parseRevertedSha(fullBody) {
13622
- const match2 = fullBody.match(/This reverts commit ([0-9a-f]{40})\./);
13623
- return match2?.[1];
13624
- }
13625
- function parseRevertedMessage(subject) {
13626
- const match2 = subject.match(/^Revert "(.+)"$/);
13627
- return match2?.[1];
13628
- }
13426
+ // node_modules/minimatch/dist/esm/unescape.js
13427
+ var unescape = (s, { windowsPathsNoEscape = false } = {}) => {
13428
+ return windowsPathsNoEscape ? s.replace(/\[([^\/\\])\]/g, "$1") : s.replace(/((?!\\).|^)\[([^\/\\])\]/g, "$1$2").replace(/\\([^\/])/g, "$1");
13429
+ };
13629
13430
 
13630
- // src/ReplayDetector.ts
13631
- var INFRASTRUCTURE_FILES = /* @__PURE__ */ new Set([".fernignore"]);
13632
- function parsePatchFileHeaders(patchContent) {
13633
- const results = [];
13634
- let currentPath = null;
13635
- let currentIsCreate = false;
13636
- let currentIsDelete = false;
13637
- const flush = () => {
13638
- if (currentPath !== null) {
13639
- results.push({ path: currentPath, isCreate: currentIsCreate, isDelete: currentIsDelete });
13431
+ // node_modules/minimatch/dist/esm/ast.js
13432
+ var types = /* @__PURE__ */ new Set(["!", "?", "+", "*", "@"]);
13433
+ var isExtglobType = (c) => types.has(c);
13434
+ var startNoTraversal = "(?!(?:^|/)\\.\\.?(?:$|/))";
13435
+ var startNoDot = "(?!\\.)";
13436
+ var addPatternStart = /* @__PURE__ */ new Set(["[", "."]);
13437
+ var justDots = /* @__PURE__ */ new Set(["..", "."]);
13438
+ var reSpecials = new Set("().*{}+?[]^$\\!");
13439
+ var regExpEscape = (s) => s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
13440
+ var qmark = "[^/]";
13441
+ var star = qmark + "*?";
13442
+ var starNoEmpty = qmark + "+?";
13443
+ var AST = class _AST {
13444
+ type;
13445
+ #root;
13446
+ #hasMagic;
13447
+ #uflag = false;
13448
+ #parts = [];
13449
+ #parent;
13450
+ #parentIndex;
13451
+ #negs;
13452
+ #filledNegs = false;
13453
+ #options;
13454
+ #toString;
13455
+ // set to true if it's an extglob with no children
13456
+ // (which really means one child of '')
13457
+ #emptyExt = false;
13458
+ constructor(type, parent, options = {}) {
13459
+ this.type = type;
13460
+ if (type)
13461
+ this.#hasMagic = true;
13462
+ this.#parent = parent;
13463
+ this.#root = this.#parent ? this.#parent.#root : this;
13464
+ this.#options = this.#root === this ? options : this.#root.#options;
13465
+ this.#negs = this.#root === this ? [] : this.#root.#negs;
13466
+ if (type === "!" && !this.#root.#filledNegs)
13467
+ this.#negs.push(this);
13468
+ this.#parentIndex = this.#parent ? this.#parent.#parts.length : 0;
13469
+ }
13470
+ get hasMagic() {
13471
+ if (this.#hasMagic !== void 0)
13472
+ return this.#hasMagic;
13473
+ for (const p of this.#parts) {
13474
+ if (typeof p === "string")
13475
+ continue;
13476
+ if (p.type || p.hasMagic)
13477
+ return this.#hasMagic = true;
13640
13478
  }
13641
- };
13642
- for (const line of patchContent.split("\n")) {
13643
- if (line.startsWith("diff --git ")) {
13644
- flush();
13645
- currentPath = null;
13646
- currentIsCreate = false;
13647
- currentIsDelete = false;
13648
- const match2 = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
13649
- if (match2) {
13650
- currentPath = match2[2] ?? null;
13479
+ return this.#hasMagic;
13480
+ }
13481
+ // reconstructs the pattern
13482
+ toString() {
13483
+ if (this.#toString !== void 0)
13484
+ return this.#toString;
13485
+ if (!this.type) {
13486
+ return this.#toString = this.#parts.map((p) => String(p)).join("");
13487
+ } else {
13488
+ return this.#toString = this.type + "(" + this.#parts.map((p) => String(p)).join("|") + ")";
13489
+ }
13490
+ }
13491
+ #fillNegs() {
13492
+ if (this !== this.#root)
13493
+ throw new Error("should only call on root");
13494
+ if (this.#filledNegs)
13495
+ return this;
13496
+ this.toString();
13497
+ this.#filledNegs = true;
13498
+ let n;
13499
+ while (n = this.#negs.pop()) {
13500
+ if (n.type !== "!")
13501
+ continue;
13502
+ let p = n;
13503
+ let pp = p.#parent;
13504
+ while (pp) {
13505
+ for (let i = p.#parentIndex + 1; !pp.type && i < pp.#parts.length; i++) {
13506
+ for (const part of n.#parts) {
13507
+ if (typeof part === "string") {
13508
+ throw new Error("string part in extglob AST??");
13509
+ }
13510
+ part.copyIn(pp.#parts[i]);
13511
+ }
13512
+ }
13513
+ p = pp;
13514
+ pp = p.#parent;
13651
13515
  }
13652
- } else if (currentPath !== null) {
13653
- if (line.startsWith("new file mode ")) {
13654
- currentIsCreate = true;
13655
- } else if (line.startsWith("deleted file mode ")) {
13656
- currentIsDelete = true;
13516
+ }
13517
+ return this;
13518
+ }
13519
+ push(...parts) {
13520
+ for (const p of parts) {
13521
+ if (p === "")
13522
+ continue;
13523
+ if (typeof p !== "string" && !(p instanceof _AST && p.#parent === this)) {
13524
+ throw new Error("invalid part: " + p);
13657
13525
  }
13526
+ this.#parts.push(p);
13658
13527
  }
13659
13528
  }
13660
- flush();
13661
- return results;
13662
- }
13663
- async function capturePatchSnapshot(git, files, treeish) {
13664
- const snapshot = {};
13665
- for (const file of files) {
13666
- if (isBinaryFile(file)) continue;
13667
- const content = await git.showFile(treeish, file).catch(() => null);
13668
- if (content != null) snapshot[file] = content;
13529
+ toJSON() {
13530
+ const ret = this.type === null ? this.#parts.slice().map((p) => typeof p === "string" ? p : p.toJSON()) : [this.type, ...this.#parts.map((p) => p.toJSON())];
13531
+ if (this.isStart() && !this.type)
13532
+ ret.unshift([]);
13533
+ if (this.isEnd() && (this === this.#root || this.#root.#filledNegs && this.#parent?.type === "!")) {
13534
+ ret.push({});
13535
+ }
13536
+ return ret;
13669
13537
  }
13670
- return snapshot;
13671
- }
13672
- function filterInfrastructureAndSensitiveFiles(rawFilesOutput) {
13673
- const allFiles = rawFilesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
13674
- const sensitiveFiles = allFiles.filter((f) => isSensitiveFile(f));
13675
- const files = allFiles.filter((f) => !isSensitiveFile(f));
13676
- return { files, sensitiveFiles };
13677
- }
13678
- var ReplayDetector = class {
13679
- git;
13680
- lockManager;
13681
- sdkOutputDir;
13682
- warnings = [];
13683
- constructor(git, lockManager, sdkOutputDir) {
13684
- this.git = git;
13685
- this.lockManager = lockManager;
13686
- this.sdkOutputDir = sdkOutputDir;
13687
- }
13688
- /**
13689
- * Derive the previous-generation SHA from git history instead of trusting
13690
- * `lock.current_generation`.
13691
- *
13692
- * Walks `git log HEAD --first-parent` and returns the SHA of the most recent
13693
- * commit that `isGenerationBoundary` classifies as a customization-baseline
13694
- * boundary, or null if no such commit is reachable from HEAD on the mainline.
13695
- *
13696
- * `isGenerationBoundary` is intentionally narrower than `isGenerationCommit`:
13697
- * it excludes `[fern-autoversion]` (lightweight bumps that don't reset the
13698
- * baseline) and the GitHub merge-commit subject for fern-bot regen PRs
13699
- * (which sits on first-parent but where the customer fix-up commits live
13700
- * on second-parent — stopping at the merge would hide them).
13701
- *
13702
- * See docs/adr/0001-derived-scan-boundary.md.
13703
- */
13704
- async findPreviousGenerationFromHistory() {
13705
- let log;
13706
- try {
13707
- log = await this.git.exec([
13708
- "log",
13709
- "--first-parent",
13710
- "--format=%H%x00%an%x00%ae%x00%s",
13711
- "HEAD"
13712
- ]);
13713
- } catch {
13714
- return null;
13715
- }
13716
- for (const commit of this.parseGitLog(log)) {
13717
- if (isGenerationBoundary(commit)) {
13718
- return commit.sha;
13538
+ isStart() {
13539
+ if (this.#root === this)
13540
+ return true;
13541
+ if (!this.#parent?.isStart())
13542
+ return false;
13543
+ if (this.#parentIndex === 0)
13544
+ return true;
13545
+ const p = this.#parent;
13546
+ for (let i = 0; i < this.#parentIndex; i++) {
13547
+ const pp = p.#parts[i];
13548
+ if (!(pp instanceof _AST && pp.type === "!")) {
13549
+ return false;
13719
13550
  }
13720
13551
  }
13721
- return null;
13552
+ return true;
13722
13553
  }
13723
- async detectNewPatches() {
13724
- const lock = this.lockManager.read();
13725
- const derivedSha = await this.findPreviousGenerationFromHistory();
13726
- if (derivedSha !== null) {
13727
- const derivedRecord = await this.synthesizeGenerationRecord(derivedSha);
13728
- return this.detectPatchesInRange(derivedSha, derivedRecord, lock);
13729
- }
13730
- const lastGen = this.getLastGeneration(lock);
13731
- if (!lastGen) {
13732
- return { patches: [], revertedPatchIds: [] };
13733
- }
13734
- const exists2 = await this.git.commitExists(lastGen.commit_sha);
13735
- if (!exists2) {
13736
- this.warnings.push(
13737
- `Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to tree-diff detection (likely a shallow clone).`
13738
- );
13739
- return this.detectPatchesViaTreeDiff(
13740
- lastGen,
13741
- /* commitKnownMissing */
13742
- true
13743
- );
13554
+ isEnd() {
13555
+ if (this.#root === this)
13556
+ return true;
13557
+ if (this.#parent?.type === "!")
13558
+ return true;
13559
+ if (!this.#parent?.isEnd())
13560
+ return false;
13561
+ if (!this.type)
13562
+ return this.#parent?.isEnd();
13563
+ const pl = this.#parent ? this.#parent.#parts.length : 0;
13564
+ return this.#parentIndex === pl - 1;
13565
+ }
13566
+ copyIn(part) {
13567
+ if (typeof part === "string")
13568
+ this.push(part);
13569
+ else
13570
+ this.push(part.clone(this));
13571
+ }
13572
+ clone(parent) {
13573
+ const c = new _AST(this.type, parent);
13574
+ for (const p of this.#parts) {
13575
+ c.copyIn(p);
13744
13576
  }
13745
- return this.detectPatchesInRange(lastGen.commit_sha, lastGen, lock);
13577
+ return c;
13746
13578
  }
13747
- /**
13748
- * Walk `${rangeStart}..HEAD`, classify each commit, emit one
13749
- * `StoredPatch` per customer commit. Body shared by the linear path
13750
- * (rangeStart = lastGen.commit_sha) and the merge-base path.
13751
- *
13752
- * Patch `base_generation` is always `lastGen.commit_sha` — the
13753
- * lockfile-tracked reference the applicator uses to fetch base file
13754
- * content, regardless of where the walk actually started.
13755
- */
13756
- async detectPatchesInRange(rangeStart, lastGen, lock) {
13757
- const log = await this.git.exec([
13758
- "log",
13759
- "--no-merges",
13760
- "--format=%H%x00%an%x00%ae%x00%s",
13761
- `${rangeStart}..HEAD`,
13762
- "--",
13763
- this.sdkOutputDir
13764
- ]);
13765
- if (!log.trim()) {
13766
- return { patches: [], revertedPatchIds: [] };
13579
+ static #parseAST(str, ast, pos, opt) {
13580
+ let escaping = false;
13581
+ let inBrace = false;
13582
+ let braceStart = -1;
13583
+ let braceNeg = false;
13584
+ if (ast.type === null) {
13585
+ let i2 = pos;
13586
+ let acc2 = "";
13587
+ while (i2 < str.length) {
13588
+ const c = str.charAt(i2++);
13589
+ if (escaping || c === "\\") {
13590
+ escaping = !escaping;
13591
+ acc2 += c;
13592
+ continue;
13593
+ }
13594
+ if (inBrace) {
13595
+ if (i2 === braceStart + 1) {
13596
+ if (c === "^" || c === "!") {
13597
+ braceNeg = true;
13598
+ }
13599
+ } else if (c === "]" && !(i2 === braceStart + 2 && braceNeg)) {
13600
+ inBrace = false;
13601
+ }
13602
+ acc2 += c;
13603
+ continue;
13604
+ } else if (c === "[") {
13605
+ inBrace = true;
13606
+ braceStart = i2;
13607
+ braceNeg = false;
13608
+ acc2 += c;
13609
+ continue;
13610
+ }
13611
+ if (!opt.noext && isExtglobType(c) && str.charAt(i2) === "(") {
13612
+ ast.push(acc2);
13613
+ acc2 = "";
13614
+ const ext2 = new _AST(c, ast);
13615
+ i2 = _AST.#parseAST(str, ext2, i2, opt);
13616
+ ast.push(ext2);
13617
+ continue;
13618
+ }
13619
+ acc2 += c;
13620
+ }
13621
+ ast.push(acc2);
13622
+ return i2;
13767
13623
  }
13768
- const commits = this.parseGitLog(log);
13769
- const newPatches = [];
13770
- const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
13771
- for (const commit of commits) {
13772
- if (isGenerationCommit(commit)) {
13624
+ let i = pos + 1;
13625
+ let part = new _AST(null, ast);
13626
+ const parts = [];
13627
+ let acc = "";
13628
+ while (i < str.length) {
13629
+ const c = str.charAt(i++);
13630
+ if (escaping || c === "\\") {
13631
+ escaping = !escaping;
13632
+ acc += c;
13773
13633
  continue;
13774
13634
  }
13775
- if (lock.patches.find((p) => p.original_commit === commit.sha)) {
13635
+ if (inBrace) {
13636
+ if (i === braceStart + 1) {
13637
+ if (c === "^" || c === "!") {
13638
+ braceNeg = true;
13639
+ }
13640
+ } else if (c === "]" && !(i === braceStart + 2 && braceNeg)) {
13641
+ inBrace = false;
13642
+ }
13643
+ acc += c;
13644
+ continue;
13645
+ } else if (c === "[") {
13646
+ inBrace = true;
13647
+ braceStart = i;
13648
+ braceNeg = false;
13649
+ acc += c;
13776
13650
  continue;
13777
13651
  }
13778
- const parents = await this.git.getCommitParents(commit.sha);
13779
- let patchContent;
13780
- try {
13781
- patchContent = await this.git.formatPatch(commit.sha);
13782
- } catch {
13783
- this.warnings.push(
13784
- `Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
13785
- );
13652
+ if (isExtglobType(c) && str.charAt(i) === "(") {
13653
+ part.push(acc);
13654
+ acc = "";
13655
+ const ext2 = new _AST(c, part);
13656
+ part.push(ext2);
13657
+ i = _AST.#parseAST(str, ext2, i, opt);
13786
13658
  continue;
13787
13659
  }
13788
- let contentHash = this.computeContentHash(patchContent);
13789
- if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
13660
+ if (c === "|") {
13661
+ part.push(acc);
13662
+ acc = "";
13663
+ parts.push(part);
13664
+ part = new _AST(null, ast);
13790
13665
  continue;
13791
13666
  }
13792
- const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
13793
- const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
13794
- if (sensitiveFiles.length > 0) {
13795
- this.warnings.push(
13796
- `Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
13797
- );
13798
- if (files.length > 0) {
13799
- const parentSha = parents[0];
13800
- if (parentSha) {
13801
- try {
13802
- patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
13803
- } catch {
13804
- continue;
13805
- }
13806
- if (!patchContent.trim()) continue;
13807
- contentHash = this.computeContentHash(patchContent);
13808
- }
13667
+ if (c === ")") {
13668
+ if (acc === "" && ast.#parts.length === 0) {
13669
+ ast.#emptyExt = true;
13809
13670
  }
13671
+ part.push(acc);
13672
+ acc = "";
13673
+ ast.push(...parts, part);
13674
+ return i;
13810
13675
  }
13811
- if (files.length === 0) {
13812
- continue;
13813
- }
13814
- const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
13815
- newPatches.push({
13816
- id: `patch-${commit.sha.slice(0, 8)}`,
13817
- content_hash: contentHash,
13818
- original_commit: commit.sha,
13819
- original_message: commit.message,
13820
- original_author: `${commit.authorName} <${commit.authorEmail}>`,
13821
- base_generation: lastGen.commit_sha,
13822
- files,
13823
- patch_content: patchContent,
13824
- ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
13825
- ...this.isCreationOnlyPatch(patchContent) ? { user_owned: true } : {}
13826
- });
13676
+ acc += c;
13827
13677
  }
13828
- const survivingPatches = await this.collapseNetZeroFiles(newPatches);
13829
- survivingPatches.reverse();
13830
- const revertedPatchIdSet = /* @__PURE__ */ new Set();
13831
- const revertIndicesToRemove = /* @__PURE__ */ new Set();
13832
- for (let i = 0; i < survivingPatches.length; i++) {
13833
- const patch = survivingPatches[i];
13834
- if (!isRevertCommit(patch.original_message)) continue;
13835
- let body = "";
13836
- try {
13837
- body = await this.git.getCommitBody(patch.original_commit);
13838
- } catch {
13839
- }
13840
- const revertedSha = parseRevertedSha(body);
13841
- const revertedMessage = parseRevertedMessage(patch.original_message);
13842
- let matchedExisting = false;
13843
- if (revertedSha) {
13844
- const existing = lock.patches.find((p) => p.original_commit === revertedSha);
13845
- if (existing) {
13846
- revertedPatchIdSet.add(existing.id);
13847
- revertIndicesToRemove.add(i);
13848
- matchedExisting = true;
13678
+ ast.type = null;
13679
+ ast.#hasMagic = void 0;
13680
+ ast.#parts = [str.substring(pos - 1)];
13681
+ return i;
13682
+ }
13683
+ static fromGlob(pattern, options = {}) {
13684
+ const ast = new _AST(null, void 0, options);
13685
+ _AST.#parseAST(pattern, ast, 0, options);
13686
+ return ast;
13687
+ }
13688
+ // returns the regular expression if there's magic, or the unescaped
13689
+ // string if not.
13690
+ toMMPattern() {
13691
+ if (this !== this.#root)
13692
+ return this.#root.toMMPattern();
13693
+ const glob = this.toString();
13694
+ const [re, body, hasMagic, uflag] = this.toRegExpSource();
13695
+ const anyMagic = hasMagic || this.#hasMagic || this.#options.nocase && !this.#options.nocaseMagicOnly && glob.toUpperCase() !== glob.toLowerCase();
13696
+ if (!anyMagic) {
13697
+ return body;
13698
+ }
13699
+ const flags = (this.#options.nocase ? "i" : "") + (uflag ? "u" : "");
13700
+ return Object.assign(new RegExp(`^${re}$`, flags), {
13701
+ _src: re,
13702
+ _glob: glob
13703
+ });
13704
+ }
13705
+ get options() {
13706
+ return this.#options;
13707
+ }
13708
+ // returns the string match, the regexp source, whether there's magic
13709
+ // in the regexp (so a regular expression is required) and whether or
13710
+ // not the uflag is needed for the regular expression (for posix classes)
13711
+ // TODO: instead of injecting the start/end at this point, just return
13712
+ // the BODY of the regexp, along with the start/end portions suitable
13713
+ // for binding the start/end in either a joined full-path makeRe context
13714
+ // (where we bind to (^|/), or a standalone matchPart context (where
13715
+ // we bind to ^, and not /). Otherwise slashes get duped!
13716
+ //
13717
+ // In part-matching mode, the start is:
13718
+ // - if not isStart: nothing
13719
+ // - if traversal possible, but not allowed: ^(?!\.\.?$)
13720
+ // - if dots allowed or not possible: ^
13721
+ // - if dots possible and not allowed: ^(?!\.)
13722
+ // end is:
13723
+ // - if not isEnd(): nothing
13724
+ // - else: $
13725
+ //
13726
+ // In full-path matching mode, we put the slash at the START of the
13727
+ // pattern, so start is:
13728
+ // - if first pattern: same as part-matching mode
13729
+ // - if not isStart(): nothing
13730
+ // - if traversal possible, but not allowed: /(?!\.\.?(?:$|/))
13731
+ // - if dots allowed or not possible: /
13732
+ // - if dots possible and not allowed: /(?!\.)
13733
+ // end is:
13734
+ // - if last pattern, same as part-matching mode
13735
+ // - else nothing
13736
+ //
13737
+ // Always put the (?:$|/) on negated tails, though, because that has to be
13738
+ // there to bind the end of the negated pattern portion, and it's easier to
13739
+ // just stick it in now rather than try to inject it later in the middle of
13740
+ // the pattern.
13741
+ //
13742
+ // We can just always return the same end, and leave it up to the caller
13743
+ // to know whether it's going to be used joined or in parts.
13744
+ // And, if the start is adjusted slightly, can do the same there:
13745
+ // - if not isStart: nothing
13746
+ // - if traversal possible, but not allowed: (?:/|^)(?!\.\.?$)
13747
+ // - if dots allowed or not possible: (?:/|^)
13748
+ // - if dots possible and not allowed: (?:/|^)(?!\.)
13749
+ //
13750
+ // But it's better to have a simpler binding without a conditional, for
13751
+ // performance, so probably better to return both start options.
13752
+ //
13753
+ // Then the caller just ignores the end if it's not the first pattern,
13754
+ // and the start always gets applied.
13755
+ //
13756
+ // But that's always going to be $ if it's the ending pattern, or nothing,
13757
+ // so the caller can just attach $ at the end of the pattern when building.
13758
+ //
13759
+ // So the todo is:
13760
+ // - better detect what kind of start is needed
13761
+ // - return both flavors of starting pattern
13762
+ // - attach $ at the end of the pattern when creating the actual RegExp
13763
+ //
13764
+ // Ah, but wait, no, that all only applies to the root when the first pattern
13765
+ // is not an extglob. If the first pattern IS an extglob, then we need all
13766
+ // that dot prevention biz to live in the extglob portions, because eg
13767
+ // +(*|.x*) can match .xy but not .yx.
13768
+ //
13769
+ // So, return the two flavors if it's #root and the first child is not an
13770
+ // AST, otherwise leave it to the child AST to handle it, and there,
13771
+ // use the (?:^|/) style of start binding.
13772
+ //
13773
+ // Even simplified further:
13774
+ // - Since the start for a join is eg /(?!\.) and the start for a part
13775
+ // is ^(?!\.), we can just prepend (?!\.) to the pattern (either root
13776
+ // or start or whatever) and prepend ^ or / at the Regexp construction.
13777
+ toRegExpSource(allowDot) {
13778
+ const dot = allowDot ?? !!this.#options.dot;
13779
+ if (this.#root === this)
13780
+ this.#fillNegs();
13781
+ if (!this.type) {
13782
+ const noEmpty = this.isStart() && this.isEnd();
13783
+ const src = this.#parts.map((p) => {
13784
+ const [re, _, hasMagic, uflag] = typeof p === "string" ? _AST.#parseGlob(p, this.#hasMagic, noEmpty) : p.toRegExpSource(allowDot);
13785
+ this.#hasMagic = this.#hasMagic || hasMagic;
13786
+ this.#uflag = this.#uflag || uflag;
13787
+ return re;
13788
+ }).join("");
13789
+ let start2 = "";
13790
+ if (this.isStart()) {
13791
+ if (typeof this.#parts[0] === "string") {
13792
+ const dotTravAllowed = this.#parts.length === 1 && justDots.has(this.#parts[0]);
13793
+ if (!dotTravAllowed) {
13794
+ const aps = addPatternStart;
13795
+ const needNoTrav = (
13796
+ // dots are allowed, and the pattern starts with [ or .
13797
+ dot && aps.has(src.charAt(0)) || // the pattern starts with \., and then [ or .
13798
+ src.startsWith("\\.") && aps.has(src.charAt(2)) || // the pattern starts with \.\., and then [ or .
13799
+ src.startsWith("\\.\\.") && aps.has(src.charAt(4))
13800
+ );
13801
+ const needNoDot = !dot && !allowDot && aps.has(src.charAt(0));
13802
+ start2 = needNoTrav ? startNoTraversal : needNoDot ? startNoDot : "";
13803
+ }
13849
13804
  }
13850
13805
  }
13851
- if (!matchedExisting && revertedMessage) {
13852
- const existing = lock.patches.find((p) => p.original_message === revertedMessage);
13853
- if (existing) {
13854
- revertedPatchIdSet.add(existing.id);
13855
- revertIndicesToRemove.add(i);
13856
- matchedExisting = true;
13857
- }
13806
+ let end = "";
13807
+ if (this.isEnd() && this.#root.#filledNegs && this.#parent?.type === "!") {
13808
+ end = "(?:$|\\/)";
13858
13809
  }
13859
- if (matchedExisting) continue;
13860
- let matchedNew = false;
13861
- if (revertedSha) {
13862
- const idx = survivingPatches.findIndex(
13863
- (p, j) => j !== i && !revertIndicesToRemove.has(j) && p.original_commit === revertedSha
13864
- );
13865
- if (idx !== -1) {
13866
- revertIndicesToRemove.add(i);
13867
- revertIndicesToRemove.add(idx);
13868
- matchedNew = true;
13810
+ const final2 = start2 + src + end;
13811
+ return [
13812
+ final2,
13813
+ unescape(src),
13814
+ this.#hasMagic = !!this.#hasMagic,
13815
+ this.#uflag
13816
+ ];
13817
+ }
13818
+ const repeated = this.type === "*" || this.type === "+";
13819
+ const start = this.type === "!" ? "(?:(?!(?:" : "(?:";
13820
+ let body = this.#partsToRegExp(dot);
13821
+ if (this.isStart() && this.isEnd() && !body && this.type !== "!") {
13822
+ const s = this.toString();
13823
+ this.#parts = [s];
13824
+ this.type = null;
13825
+ this.#hasMagic = void 0;
13826
+ return [s, unescape(this.toString()), false, false];
13827
+ }
13828
+ let bodyDotAllowed = !repeated || allowDot || dot || !startNoDot ? "" : this.#partsToRegExp(true);
13829
+ if (bodyDotAllowed === body) {
13830
+ bodyDotAllowed = "";
13831
+ }
13832
+ if (bodyDotAllowed) {
13833
+ body = `(?:${body})(?:${bodyDotAllowed})*?`;
13834
+ }
13835
+ let final = "";
13836
+ if (this.type === "!" && this.#emptyExt) {
13837
+ final = (this.isStart() && !dot ? startNoDot : "") + starNoEmpty;
13838
+ } else {
13839
+ const close = this.type === "!" ? (
13840
+ // !() must match something,but !(x) can match ''
13841
+ "))" + (this.isStart() && !dot && !allowDot ? startNoDot : "") + star + ")"
13842
+ ) : this.type === "@" ? ")" : this.type === "?" ? ")?" : this.type === "+" && bodyDotAllowed ? ")" : this.type === "*" && bodyDotAllowed ? `)?` : `)${this.type}`;
13843
+ final = start + body + close;
13844
+ }
13845
+ return [
13846
+ final,
13847
+ unescape(body),
13848
+ this.#hasMagic = !!this.#hasMagic,
13849
+ this.#uflag
13850
+ ];
13851
+ }
13852
+ #partsToRegExp(dot) {
13853
+ return this.#parts.map((p) => {
13854
+ if (typeof p === "string") {
13855
+ throw new Error("string type in extglob ast??");
13856
+ }
13857
+ const [re, _, _hasMagic, uflag] = p.toRegExpSource(dot);
13858
+ this.#uflag = this.#uflag || uflag;
13859
+ return re;
13860
+ }).filter((p) => !(this.isStart() && this.isEnd()) || !!p).join("|");
13861
+ }
13862
+ static #parseGlob(glob, hasMagic, noEmpty = false) {
13863
+ let escaping = false;
13864
+ let re = "";
13865
+ let uflag = false;
13866
+ for (let i = 0; i < glob.length; i++) {
13867
+ const c = glob.charAt(i);
13868
+ if (escaping) {
13869
+ escaping = false;
13870
+ re += (reSpecials.has(c) ? "\\" : "") + c;
13871
+ continue;
13872
+ }
13873
+ if (c === "\\") {
13874
+ if (i === glob.length - 1) {
13875
+ re += "\\\\";
13876
+ } else {
13877
+ escaping = true;
13869
13878
  }
13879
+ continue;
13870
13880
  }
13871
- if (!matchedNew && revertedMessage) {
13872
- const idx = survivingPatches.findIndex(
13873
- (p, j) => j !== i && !revertIndicesToRemove.has(j) && p.original_message === revertedMessage
13874
- );
13875
- if (idx !== -1) {
13876
- revertIndicesToRemove.add(i);
13877
- revertIndicesToRemove.add(idx);
13881
+ if (c === "[") {
13882
+ const [src, needUflag, consumed, magic] = parseClass(glob, i);
13883
+ if (consumed) {
13884
+ re += src;
13885
+ uflag = uflag || needUflag;
13886
+ i += consumed - 1;
13887
+ hasMagic = hasMagic || magic;
13888
+ continue;
13878
13889
  }
13879
13890
  }
13880
- if (!matchedExisting && !matchedNew) {
13881
- revertIndicesToRemove.add(i);
13891
+ if (c === "*") {
13892
+ if (noEmpty && glob === "*")
13893
+ re += starNoEmpty;
13894
+ else
13895
+ re += star;
13896
+ hasMagic = true;
13897
+ continue;
13898
+ }
13899
+ if (c === "?") {
13900
+ re += qmark;
13901
+ hasMagic = true;
13902
+ continue;
13882
13903
  }
13904
+ re += regExpEscape(c);
13883
13905
  }
13884
- const filteredPatches = survivingPatches.filter((_, i) => !revertIndicesToRemove.has(i));
13885
- return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
13906
+ return [re, unescape(glob), !!hasMagic, uflag];
13886
13907
  }
13887
- /**
13888
- * Compute content hash for deduplication.
13889
- *
13890
- * Produces a format-agnostic hash so both `git format-patch` output
13891
- * (email-wrapped) and plain `git diff` output hash to the same value
13892
- * when the underlying diff hunks are identical.
13893
- *
13894
- * Normalization:
13895
- * 1. Strip email wrapper: everything before the first `diff --git` line
13896
- * (From:, Subject:, Date:, diffstat, blank separators).
13897
- * 2. Strip `index` lines (blob SHA pairs change across rebases).
13898
- * 3. Strip trailing git version marker (`-- \n<version>`).
13899
- *
13900
- * This ensures content hashes match across the no-patches and normal-
13901
- * regeneration flows, which store formatPatch and git-diff formats
13902
- * respectively.
13903
- */
13904
- computeContentHash(patchContent) {
13905
- const lines = patchContent.split("\n");
13906
- const diffStart = lines.findIndex((l) => l.startsWith("diff --git "));
13907
- const relevantLines = diffStart > 0 ? lines.slice(diffStart) : lines;
13908
- const normalized = relevantLines.filter((line) => !line.startsWith("index ")).join("\n").replace(/\n-- \n[\d]+\.[\d]+[^\n]*\s*$/, "");
13909
- return `sha256:${(0, import_node_crypto.createHash)("sha256").update(normalized).digest("hex")}`;
13908
+ };
13909
+
13910
+ // node_modules/minimatch/dist/esm/escape.js
13911
+ var escape = (s, { windowsPathsNoEscape = false } = {}) => {
13912
+ return windowsPathsNoEscape ? s.replace(/[?*()[\]]/g, "[$&]") : s.replace(/[?*()[\]\\]/g, "\\$&");
13913
+ };
13914
+
13915
+ // node_modules/minimatch/dist/esm/index.js
13916
+ var minimatch = (p, pattern, options = {}) => {
13917
+ assertValidPattern(pattern);
13918
+ if (!options.nocomment && pattern.charAt(0) === "#") {
13919
+ return false;
13910
13920
  }
13911
- /**
13912
- * Drop patches whose files were transiently created and then deleted
13913
- * within the current linear detection window, and are absent from
13914
- * HEAD at the end of the window.
13915
- *
13916
- * Rationale: each commit becomes its own patch, so a file that was
13917
- * briefly added and later removed survives as a create+delete pair whose
13918
- * cumulative diff is empty. We drop such patches before they reach the
13919
- * lockfile.
13920
- *
13921
- * A file is "transient" when:
13922
- * 1. some patch in the window sets `new file mode` for it (a creation), AND
13923
- * 2. some patch in the window sets `deleted file mode` for it (a deletion), AND
13924
- * 3. it is absent from HEAD's tree.
13925
- *
13926
- * The HEAD guard (#3) prevents false positives when a file is deleted
13927
- * and then recreated with different content — the cumulative diff of
13928
- * such a pair is non-empty and must be preserved.
13929
- *
13930
- * This only drops patches whose `files` are a subset of the transient
13931
- * set. A partial-transient patch (one that touches a transient file
13932
- * alongside a persistent one) is left unmodified; surgical stripping of
13933
- * single files from a multi-file patch would require a follow-up that
13934
- * regenerates the patch content via `git format-patch -- <files>`. No
13935
- * current failing test exercises this, and leaving the patch intact
13936
- * preserves today's behaviour. Revisit if a future adversarial test
13937
- * demands per-file stripping.
13938
- */
13939
- async collapseNetZeroFiles(patches) {
13940
- if (patches.length === 0) return patches;
13941
- const stateByFile = /* @__PURE__ */ new Map();
13942
- for (const patch of patches) {
13943
- for (const header of parsePatchFileHeaders(patch.patch_content)) {
13944
- if (!header.isCreate && !header.isDelete) continue;
13945
- const prior = stateByFile.get(header.path) ?? { created: false, deleted: false };
13946
- if (header.isCreate) prior.created = true;
13947
- if (header.isDelete) prior.deleted = true;
13948
- stateByFile.set(header.path, prior);
13921
+ return new Minimatch(pattern, options).match(p);
13922
+ };
13923
+ var starDotExtRE = /^\*+([^+@!?\*\[\(]*)$/;
13924
+ var starDotExtTest = (ext2) => (f) => !f.startsWith(".") && f.endsWith(ext2);
13925
+ var starDotExtTestDot = (ext2) => (f) => f.endsWith(ext2);
13926
+ var starDotExtTestNocase = (ext2) => {
13927
+ ext2 = ext2.toLowerCase();
13928
+ return (f) => !f.startsWith(".") && f.toLowerCase().endsWith(ext2);
13929
+ };
13930
+ var starDotExtTestNocaseDot = (ext2) => {
13931
+ ext2 = ext2.toLowerCase();
13932
+ return (f) => f.toLowerCase().endsWith(ext2);
13933
+ };
13934
+ var starDotStarRE = /^\*+\.\*+$/;
13935
+ var starDotStarTest = (f) => !f.startsWith(".") && f.includes(".");
13936
+ var starDotStarTestDot = (f) => f !== "." && f !== ".." && f.includes(".");
13937
+ var dotStarRE = /^\.\*+$/;
13938
+ var dotStarTest = (f) => f !== "." && f !== ".." && f.startsWith(".");
13939
+ var starRE = /^\*+$/;
13940
+ var starTest = (f) => f.length !== 0 && !f.startsWith(".");
13941
+ var starTestDot = (f) => f.length !== 0 && f !== "." && f !== "..";
13942
+ var qmarksRE = /^\?+([^+@!?\*\[\(]*)?$/;
13943
+ var qmarksTestNocase = ([$0, ext2 = ""]) => {
13944
+ const noext = qmarksTestNoExt([$0]);
13945
+ if (!ext2)
13946
+ return noext;
13947
+ ext2 = ext2.toLowerCase();
13948
+ return (f) => noext(f) && f.toLowerCase().endsWith(ext2);
13949
+ };
13950
+ var qmarksTestNocaseDot = ([$0, ext2 = ""]) => {
13951
+ const noext = qmarksTestNoExtDot([$0]);
13952
+ if (!ext2)
13953
+ return noext;
13954
+ ext2 = ext2.toLowerCase();
13955
+ return (f) => noext(f) && f.toLowerCase().endsWith(ext2);
13956
+ };
13957
+ var qmarksTestDot = ([$0, ext2 = ""]) => {
13958
+ const noext = qmarksTestNoExtDot([$0]);
13959
+ return !ext2 ? noext : (f) => noext(f) && f.endsWith(ext2);
13960
+ };
13961
+ var qmarksTest = ([$0, ext2 = ""]) => {
13962
+ const noext = qmarksTestNoExt([$0]);
13963
+ return !ext2 ? noext : (f) => noext(f) && f.endsWith(ext2);
13964
+ };
13965
+ var qmarksTestNoExt = ([$0]) => {
13966
+ const len = $0.length;
13967
+ return (f) => f.length === len && !f.startsWith(".");
13968
+ };
13969
+ var qmarksTestNoExtDot = ([$0]) => {
13970
+ const len = $0.length;
13971
+ return (f) => f.length === len && f !== "." && f !== "..";
13972
+ };
13973
+ var defaultPlatform = typeof process === "object" && process ? typeof process.env === "object" && process.env && process.env.__MINIMATCH_TESTING_PLATFORM__ || process.platform : "posix";
13974
+ var path = {
13975
+ win32: { sep: "\\" },
13976
+ posix: { sep: "/" }
13977
+ };
13978
+ var sep = defaultPlatform === "win32" ? path.win32.sep : path.posix.sep;
13979
+ minimatch.sep = sep;
13980
+ var GLOBSTAR = /* @__PURE__ */ Symbol("globstar **");
13981
+ minimatch.GLOBSTAR = GLOBSTAR;
13982
+ var qmark2 = "[^/]";
13983
+ var star2 = qmark2 + "*?";
13984
+ var twoStarDot = "(?:(?!(?:\\/|^)(?:\\.{1,2})($|\\/)).)*?";
13985
+ var twoStarNoDot = "(?:(?!(?:\\/|^)\\.).)*?";
13986
+ var filter = (pattern, options = {}) => (p) => minimatch(p, pattern, options);
13987
+ minimatch.filter = filter;
13988
+ var ext = (a, b = {}) => Object.assign({}, a, b);
13989
+ var defaults = (def) => {
13990
+ if (!def || typeof def !== "object" || !Object.keys(def).length) {
13991
+ return minimatch;
13992
+ }
13993
+ const orig = minimatch;
13994
+ const m = (p, pattern, options = {}) => orig(p, pattern, ext(def, options));
13995
+ return Object.assign(m, {
13996
+ Minimatch: class Minimatch extends orig.Minimatch {
13997
+ constructor(pattern, options = {}) {
13998
+ super(pattern, ext(def, options));
13949
13999
  }
13950
- }
13951
- let anyCandidate = false;
13952
- for (const state of stateByFile.values()) {
13953
- if (state.created && state.deleted) {
13954
- anyCandidate = true;
13955
- break;
14000
+ static defaults(options) {
14001
+ return orig.defaults(ext(def, options)).Minimatch;
13956
14002
  }
13957
- }
13958
- if (!anyCandidate) return patches;
13959
- const headFiles = await this.listHeadFiles();
13960
- const transient = /* @__PURE__ */ new Set();
13961
- for (const [file, state] of stateByFile) {
13962
- if (state.created && state.deleted && !headFiles.has(file)) {
13963
- transient.add(file);
14003
+ },
14004
+ AST: class AST extends orig.AST {
14005
+ /* c8 ignore start */
14006
+ constructor(type, parent, options = {}) {
14007
+ super(type, parent, ext(def, options));
13964
14008
  }
13965
- }
13966
- if (transient.size === 0) return patches;
13967
- return patches.filter((patch) => !patch.files.every((f) => transient.has(f)));
14009
+ /* c8 ignore stop */
14010
+ static fromGlob(pattern, options = {}) {
14011
+ return orig.AST.fromGlob(pattern, ext(def, options));
14012
+ }
14013
+ },
14014
+ unescape: (s, options = {}) => orig.unescape(s, ext(def, options)),
14015
+ escape: (s, options = {}) => orig.escape(s, ext(def, options)),
14016
+ filter: (pattern, options = {}) => orig.filter(pattern, ext(def, options)),
14017
+ defaults: (options) => orig.defaults(ext(def, options)),
14018
+ makeRe: (pattern, options = {}) => orig.makeRe(pattern, ext(def, options)),
14019
+ braceExpand: (pattern, options = {}) => orig.braceExpand(pattern, ext(def, options)),
14020
+ match: (list, pattern, options = {}) => orig.match(list, pattern, ext(def, options)),
14021
+ sep: orig.sep,
14022
+ GLOBSTAR
14023
+ });
14024
+ };
14025
+ minimatch.defaults = defaults;
14026
+ var braceExpand = (pattern, options = {}) => {
14027
+ assertValidPattern(pattern);
14028
+ if (options.nobrace || !/\{(?:(?!\{).)*\}/.test(pattern)) {
14029
+ return [pattern];
13968
14030
  }
13969
- /**
13970
- * List every tracked path at HEAD (one `git ls-tree -r` invocation).
13971
- * Returns an empty Set if HEAD is unborn or the invocation fails — the
13972
- * caller then treats every candidate as "not in HEAD" and may collapse
13973
- * more aggressively. On typical SDK repos this is a single sub-10ms call.
13974
- */
13975
- async listHeadFiles() {
13976
- try {
13977
- const out = await this.git.exec(["ls-tree", "-r", "--name-only", "HEAD"]);
13978
- return new Set(out.split("\n").map((l) => l.trim()).filter(Boolean));
13979
- } catch {
13980
- return /* @__PURE__ */ new Set();
13981
- }
14031
+ return (0, import_brace_expansion.default)(pattern);
14032
+ };
14033
+ minimatch.braceExpand = braceExpand;
14034
+ var makeRe = (pattern, options = {}) => new Minimatch(pattern, options).makeRe();
14035
+ minimatch.makeRe = makeRe;
14036
+ var match = (list, pattern, options = {}) => {
14037
+ const mm = new Minimatch(pattern, options);
14038
+ list = list.filter((f) => mm.match(f));
14039
+ if (mm.options.nonull && !list.length) {
14040
+ list.push(pattern);
13982
14041
  }
13983
- /**
13984
- * Detect patches via tree diff for non-linear history. Returns a composite patch.
13985
- * Revert reconciliation is skipped here because tree-diff produces a single composite
13986
- * patch from the aggregate diff — individual revert commits are not distinguishable.
13987
- */
13988
- async detectPatchesViaTreeDiff(lastGen, commitKnownMissing) {
13989
- const diffBase = await this.resolveDiffBase(lastGen, commitKnownMissing);
13990
- if (!diffBase) {
13991
- return this.detectPatchesViaCommitScan();
13992
- }
13993
- const lockForGuard = this.lockManager.read();
13994
- const knownGenerations = new Set(lockForGuard.generations.map((g) => g.commit_sha));
13995
- let resolvedBaseGeneration = commitKnownMissing ? diffBase : lastGen.commit_sha;
13996
- if (commitKnownMissing && !knownGenerations.has(diffBase)) {
13997
- const fallback = lockForGuard.current_generation ?? lockForGuard.generations[lockForGuard.generations.length - 1]?.commit_sha;
13998
- if (!fallback) {
13999
- this.warnings.push(
14000
- `Composite-patch fallback skipped: diff base ${diffBase.slice(0, 7)} is not a recorded generation and the lockfile has no recorded generations to anchor on. Customer customizations on disk are preserved; tracked patches will resume on the next regeneration.`
14001
- );
14002
- return { patches: [], revertedPatchIds: [] };
14003
- }
14004
- resolvedBaseGeneration = fallback;
14042
+ return list;
14043
+ };
14044
+ minimatch.match = match;
14045
+ var globMagic = /[?*]|[+@!]\(.*?\)|\[|\]/;
14046
+ var regExpEscape2 = (s) => s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
14047
+ var Minimatch = class {
14048
+ options;
14049
+ set;
14050
+ pattern;
14051
+ windowsPathsNoEscape;
14052
+ nonegate;
14053
+ negate;
14054
+ comment;
14055
+ empty;
14056
+ preserveMultipleSlashes;
14057
+ partial;
14058
+ globSet;
14059
+ globParts;
14060
+ nocase;
14061
+ isWindows;
14062
+ platform;
14063
+ windowsNoMagicRoot;
14064
+ regexp;
14065
+ constructor(pattern, options = {}) {
14066
+ assertValidPattern(pattern);
14067
+ options = options || {};
14068
+ this.options = options;
14069
+ this.pattern = pattern;
14070
+ this.platform = options.platform || defaultPlatform;
14071
+ this.isWindows = this.platform === "win32";
14072
+ this.windowsPathsNoEscape = !!options.windowsPathsNoEscape || options.allowWindowsEscape === false;
14073
+ if (this.windowsPathsNoEscape) {
14074
+ this.pattern = this.pattern.replace(/\\/g, "/");
14005
14075
  }
14006
- const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
14007
- const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
14008
- if (sensitiveFiles.length > 0) {
14009
- this.warnings.push(
14010
- `Sensitive file(s) excluded from composite patch: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
14011
- );
14076
+ this.preserveMultipleSlashes = !!options.preserveMultipleSlashes;
14077
+ this.regexp = null;
14078
+ this.negate = false;
14079
+ this.nonegate = !!options.nonegate;
14080
+ this.comment = false;
14081
+ this.empty = false;
14082
+ this.partial = !!options.partial;
14083
+ this.nocase = !!this.options.nocase;
14084
+ this.windowsNoMagicRoot = options.windowsNoMagicRoot !== void 0 ? options.windowsNoMagicRoot : !!(this.isWindows && this.nocase);
14085
+ this.globSet = [];
14086
+ this.globParts = [];
14087
+ this.set = [];
14088
+ this.make();
14089
+ }
14090
+ hasMagic() {
14091
+ if (this.options.magicalBraces && this.set.length > 1) {
14092
+ return true;
14012
14093
  }
14013
- if (files.length === 0) return { patches: [], revertedPatchIds: [] };
14014
- const diff = await this.git.exec(["diff", diffBase, "HEAD", "--", ...files]);
14015
- if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
14016
- const contentHash = this.computeContentHash(diff);
14017
- const lock = this.lockManager.read();
14018
- if (lock.patches.some((p) => p.content_hash === contentHash) || (lock.forgotten_hashes ?? []).includes(contentHash)) {
14019
- return { patches: [], revertedPatchIds: [] };
14094
+ for (const pattern of this.set) {
14095
+ for (const part of pattern) {
14096
+ if (typeof part !== "string")
14097
+ return true;
14098
+ }
14020
14099
  }
14021
- const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
14022
- const theirsSnapshot = await capturePatchSnapshot(this.git, files, "HEAD");
14023
- const compositePatch = {
14024
- id: `patch-composite-${headSha.slice(0, 8)}`,
14025
- content_hash: contentHash,
14026
- original_commit: headSha,
14027
- original_message: "Customer customizations (composite)",
14028
- original_author: "composite",
14029
- // Anchor `base_generation` on a SHA the applicator's
14030
- // `resolveBaseGeneration` can find — always a member of
14031
- // `lock.generations`. When `diffBase` is a recorded
14032
- // generation, use it directly; otherwise the guard above
14033
- // has already mapped it onto `current_generation`.
14034
- base_generation: resolvedBaseGeneration,
14035
- files,
14036
- patch_content: diff,
14037
- ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
14038
- ...this.isCreationOnlyPatch(diff) ? { user_owned: true } : {}
14039
- };
14040
- return { patches: [compositePatch], revertedPatchIds: [] };
14100
+ return false;
14041
14101
  }
14042
- /**
14043
- * Last-resort detection when both generation commit and tree are unreachable.
14044
- * Scans all commits from HEAD, filters against known lockfile patches, and
14045
- * skips creation-only commits (squashed history after force push).
14046
- *
14047
- * Detected patches use the commit's parent as base_generation so the applicator
14048
- * can find base file content from the parent's tree (which IS reachable).
14049
- */
14050
- async detectPatchesViaCommitScan() {
14051
- const lock = this.lockManager.read();
14052
- const knownGenerations = new Set(lock.generations.map((g) => g.commit_sha));
14053
- const fallbackAnchor = lock.current_generation ?? lock.generations[lock.generations.length - 1]?.commit_sha;
14054
- const log = await this.git.exec([
14055
- "log",
14056
- "--max-count=200",
14057
- "--format=%H%x00%an%x00%ae%x00%s",
14058
- "HEAD",
14059
- "--",
14060
- this.sdkOutputDir
14061
- ]);
14062
- if (!log.trim()) {
14063
- return { patches: [], revertedPatchIds: [] };
14102
+ debug(..._) {
14103
+ }
14104
+ make() {
14105
+ const pattern = this.pattern;
14106
+ const options = this.options;
14107
+ if (!options.nocomment && pattern.charAt(0) === "#") {
14108
+ this.comment = true;
14109
+ return;
14064
14110
  }
14065
- const commits = this.parseGitLog(log);
14066
- const newPatches = [];
14067
- const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
14068
- const existingHashes = new Set(lock.patches.map((p) => p.content_hash));
14069
- const existingCommits = new Set(lock.patches.map((p) => p.original_commit));
14070
- for (const commit of commits) {
14071
- if (isGenerationCommit(commit)) {
14072
- continue;
14073
- }
14074
- const parents = await this.git.getCommitParents(commit.sha);
14075
- if (parents.length > 1) {
14076
- continue;
14077
- }
14078
- if (existingCommits.has(commit.sha)) {
14079
- continue;
14080
- }
14081
- let patchContent;
14082
- try {
14083
- patchContent = await this.git.formatPatch(commit.sha);
14084
- } catch {
14085
- continue;
14086
- }
14087
- let contentHash = this.computeContentHash(patchContent);
14088
- if (existingHashes.has(contentHash) || forgottenHashes.has(contentHash)) {
14089
- continue;
14111
+ if (!pattern) {
14112
+ this.empty = true;
14113
+ return;
14114
+ }
14115
+ this.parseNegate();
14116
+ this.globSet = [...new Set(this.braceExpand())];
14117
+ if (options.debug) {
14118
+ this.debug = (...args) => console.error(...args);
14119
+ }
14120
+ this.debug(this.pattern, this.globSet);
14121
+ const rawGlobParts = this.globSet.map((s) => this.slashSplit(s));
14122
+ this.globParts = this.preprocess(rawGlobParts);
14123
+ this.debug(this.pattern, this.globParts);
14124
+ let set = this.globParts.map((s, _, __) => {
14125
+ if (this.isWindows && this.windowsNoMagicRoot) {
14126
+ const isUNC = s[0] === "" && s[1] === "" && (s[2] === "?" || !globMagic.test(s[2])) && !globMagic.test(s[3]);
14127
+ const isDrive = /^[a-z]:/i.test(s[0]);
14128
+ if (isUNC) {
14129
+ return [...s.slice(0, 4), ...s.slice(4).map((ss) => this.parse(ss))];
14130
+ } else if (isDrive) {
14131
+ return [s[0], ...s.slice(1).map((ss) => this.parse(ss))];
14132
+ }
14090
14133
  }
14091
- if (this.isCreationOnlyPatch(patchContent)) {
14092
- continue;
14134
+ return s.map((ss) => this.parse(ss));
14135
+ });
14136
+ this.debug(this.pattern, set);
14137
+ this.set = set.filter((s) => s.indexOf(false) === -1);
14138
+ if (this.isWindows) {
14139
+ for (let i = 0; i < this.set.length; i++) {
14140
+ const p = this.set[i];
14141
+ if (p[0] === "" && p[1] === "" && this.globParts[i][2] === "?" && typeof p[3] === "string" && /^[a-z]:$/i.test(p[3])) {
14142
+ p[2] = "?";
14143
+ }
14093
14144
  }
14094
- const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
14095
- const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
14096
- if (sensitiveFiles.length > 0) {
14097
- this.warnings.push(
14098
- `Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
14099
- );
14100
- if (files.length > 0) {
14101
- if (parents.length === 0) {
14102
- continue;
14103
- }
14104
- try {
14105
- patchContent = await this.git.exec(["diff", parents[0], commit.sha, "--", ...files]);
14106
- } catch {
14107
- continue;
14145
+ }
14146
+ this.debug(this.pattern, this.set);
14147
+ }
14148
+ // various transforms to equivalent pattern sets that are
14149
+ // faster to process in a filesystem walk. The goal is to
14150
+ // eliminate what we can, and push all ** patterns as far
14151
+ // to the right as possible, even if it increases the number
14152
+ // of patterns that we have to process.
14153
+ preprocess(globParts) {
14154
+ if (this.options.noglobstar) {
14155
+ for (let i = 0; i < globParts.length; i++) {
14156
+ for (let j = 0; j < globParts[i].length; j++) {
14157
+ if (globParts[i][j] === "**") {
14158
+ globParts[i][j] = "*";
14108
14159
  }
14109
- if (!patchContent.trim()) continue;
14110
- contentHash = this.computeContentHash(patchContent);
14111
14160
  }
14112
14161
  }
14113
- if (files.length === 0) {
14114
- continue;
14115
- }
14116
- if (parents.length === 0) {
14117
- continue;
14118
- }
14119
- const parentSha = parents[0];
14120
- let baseGeneration;
14121
- if (knownGenerations.has(parentSha)) {
14122
- baseGeneration = parentSha;
14123
- } else if (fallbackAnchor) {
14124
- baseGeneration = fallbackAnchor;
14125
- } else {
14126
- this.warnings.push(
14127
- `Skipping commit ${commit.sha.slice(0, 7)} (${commit.message.split("\n")[0]}): no recorded generation in the lockfile to anchor on. The customer's state on disk is preserved; tracked patches will resume on the next regeneration.`
14128
- );
14129
- continue;
14130
- }
14131
- const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
14132
- newPatches.push({
14133
- id: `patch-${commit.sha.slice(0, 8)}`,
14134
- content_hash: contentHash,
14135
- original_commit: commit.sha,
14136
- original_message: commit.message,
14137
- original_author: `${commit.authorName} <${commit.authorEmail}>`,
14138
- base_generation: baseGeneration,
14139
- files,
14140
- patch_content: patchContent,
14141
- ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {}
14142
- });
14143
14162
  }
14144
- newPatches.reverse();
14145
- return { patches: newPatches, revertedPatchIds: [] };
14163
+ const { optimizationLevel = 1 } = this.options;
14164
+ if (optimizationLevel >= 2) {
14165
+ globParts = this.firstPhasePreProcess(globParts);
14166
+ globParts = this.secondPhasePreProcess(globParts);
14167
+ } else if (optimizationLevel >= 1) {
14168
+ globParts = this.levelOneOptimize(globParts);
14169
+ } else {
14170
+ globParts = this.adjascentGlobstarOptimize(globParts);
14171
+ }
14172
+ return globParts;
14146
14173
  }
14147
- /**
14148
- * Check if a format-patch consists entirely of new-file creations.
14149
- * Used to identify squashed commits after force push, which create all files
14150
- * from scratch (--- /dev/null) rather than modifying existing files.
14151
- */
14152
- isCreationOnlyPatch(patchContent) {
14153
- const diffOldHeaders = patchContent.split("\n").filter((l) => l.startsWith("--- "));
14154
- if (diffOldHeaders.length === 0) {
14155
- return false;
14156
- }
14157
- return diffOldHeaders.every((l) => l === "--- /dev/null");
14158
- }
14159
- /**
14160
- * Resolve the best available diff base for a generation record.
14161
- * Prefers commit_sha, falls back to tree_hash for unreachable commits.
14162
- * When commitKnownMissing is true, skips the redundant commitExists check.
14163
- */
14164
- async resolveDiffBase(gen, commitKnownMissing) {
14165
- if (!commitKnownMissing && await this.git.commitExists(gen.commit_sha)) {
14166
- return gen.commit_sha;
14167
- }
14168
- if (await this.git.treeExists(gen.tree_hash)) {
14169
- return gen.tree_hash;
14170
- }
14171
- return null;
14172
- }
14173
- parseGitLog(log) {
14174
- return log.trim().split("\n").map((line) => {
14175
- const [sha, authorName, authorEmail, message] = line.split("\0");
14176
- return { sha, authorName, authorEmail, message };
14174
+ // just get rid of adjascent ** portions
14175
+ adjascentGlobstarOptimize(globParts) {
14176
+ return globParts.map((parts) => {
14177
+ let gs = -1;
14178
+ while (-1 !== (gs = parts.indexOf("**", gs + 1))) {
14179
+ let i = gs;
14180
+ while (parts[i + 1] === "**") {
14181
+ i++;
14182
+ }
14183
+ if (i !== gs) {
14184
+ parts.splice(gs, i - gs);
14185
+ }
14186
+ }
14187
+ return parts;
14177
14188
  });
14178
14189
  }
14179
- getLastGeneration(lock) {
14180
- return lock.generations.find((g) => g.commit_sha === lock.current_generation);
14181
- }
14182
- /**
14183
- * Build a transient `GenerationRecord` from a SHA derived by walking git
14184
- * history. Only `commit_sha` (and `tree_hash` when needed downstream) is
14185
- * load-bearing here; the rest are filled with defensible placeholders.
14186
- * This record is consumed by `detectPatchesInRange` and is never persisted.
14187
- */
14188
- async synthesizeGenerationRecord(sha) {
14189
- let tree_hash = "";
14190
- try {
14191
- tree_hash = await this.git.getTreeHash(sha);
14192
- } catch {
14193
- }
14194
- return {
14195
- commit_sha: sha,
14196
- tree_hash,
14197
- timestamp: (/* @__PURE__ */ new Date(0)).toISOString(),
14198
- cli_version: "",
14199
- generator_versions: {}
14200
- };
14201
- }
14202
- };
14203
-
14204
- // src/ReplayService.ts
14205
- var import_node_fs2 = require("fs");
14206
- var import_promises6 = require("fs/promises");
14207
- var import_node_path9 = require("path");
14208
-
14209
- // node_modules/minimatch/dist/esm/index.js
14210
- var import_brace_expansion = __toESM(require_brace_expansion(), 1);
14211
-
14212
- // node_modules/minimatch/dist/esm/assert-valid-pattern.js
14213
- var MAX_PATTERN_LENGTH = 1024 * 64;
14214
- var assertValidPattern = (pattern) => {
14215
- if (typeof pattern !== "string") {
14216
- throw new TypeError("invalid pattern");
14217
- }
14218
- if (pattern.length > MAX_PATTERN_LENGTH) {
14219
- throw new TypeError("pattern is too long");
14220
- }
14221
- };
14222
-
14223
- // node_modules/minimatch/dist/esm/brace-expressions.js
14224
- var posixClasses = {
14225
- "[:alnum:]": ["\\p{L}\\p{Nl}\\p{Nd}", true],
14226
- "[:alpha:]": ["\\p{L}\\p{Nl}", true],
14227
- "[:ascii:]": ["\\x00-\\x7f", false],
14228
- "[:blank:]": ["\\p{Zs}\\t", true],
14229
- "[:cntrl:]": ["\\p{Cc}", true],
14230
- "[:digit:]": ["\\p{Nd}", true],
14231
- "[:graph:]": ["\\p{Z}\\p{C}", true, true],
14232
- "[:lower:]": ["\\p{Ll}", true],
14233
- "[:print:]": ["\\p{C}", true],
14234
- "[:punct:]": ["\\p{P}", true],
14235
- "[:space:]": ["\\p{Z}\\t\\r\\n\\v\\f", true],
14236
- "[:upper:]": ["\\p{Lu}", true],
14237
- "[:word:]": ["\\p{L}\\p{Nl}\\p{Nd}\\p{Pc}", true],
14238
- "[:xdigit:]": ["A-Fa-f0-9", false]
14239
- };
14240
- var braceEscape = (s) => s.replace(/[[\]\\-]/g, "\\$&");
14241
- var regexpEscape = (s) => s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
14242
- var rangesToString = (ranges) => ranges.join("");
14243
- var parseClass = (glob, position) => {
14244
- const pos = position;
14245
- if (glob.charAt(pos) !== "[") {
14246
- throw new Error("not in a brace expression");
14190
+ // get rid of adjascent ** and resolve .. portions
14191
+ levelOneOptimize(globParts) {
14192
+ return globParts.map((parts) => {
14193
+ parts = parts.reduce((set, part) => {
14194
+ const prev = set[set.length - 1];
14195
+ if (part === "**" && prev === "**") {
14196
+ return set;
14197
+ }
14198
+ if (part === "..") {
14199
+ if (prev && prev !== ".." && prev !== "." && prev !== "**") {
14200
+ set.pop();
14201
+ return set;
14202
+ }
14203
+ }
14204
+ set.push(part);
14205
+ return set;
14206
+ }, []);
14207
+ return parts.length === 0 ? [""] : parts;
14208
+ });
14247
14209
  }
14248
- const ranges = [];
14249
- const negs = [];
14250
- let i = pos + 1;
14251
- let sawStart = false;
14252
- let uflag = false;
14253
- let escaping = false;
14254
- let negate = false;
14255
- let endPos = pos;
14256
- let rangeStart = "";
14257
- WHILE: while (i < glob.length) {
14258
- const c = glob.charAt(i);
14259
- if ((c === "!" || c === "^") && i === pos + 1) {
14260
- negate = true;
14261
- i++;
14262
- continue;
14263
- }
14264
- if (c === "]" && sawStart && !escaping) {
14265
- endPos = i + 1;
14266
- break;
14267
- }
14268
- sawStart = true;
14269
- if (c === "\\") {
14270
- if (!escaping) {
14271
- escaping = true;
14272
- i++;
14273
- continue;
14274
- }
14210
+ levelTwoFileOptimize(parts) {
14211
+ if (!Array.isArray(parts)) {
14212
+ parts = this.slashSplit(parts);
14275
14213
  }
14276
- if (c === "[" && !escaping) {
14277
- for (const [cls, [unip, u, neg]] of Object.entries(posixClasses)) {
14278
- if (glob.startsWith(cls, i)) {
14279
- if (rangeStart) {
14280
- return ["$.", false, glob.length - pos, true];
14214
+ let didSomething = false;
14215
+ do {
14216
+ didSomething = false;
14217
+ if (!this.preserveMultipleSlashes) {
14218
+ for (let i = 1; i < parts.length - 1; i++) {
14219
+ const p = parts[i];
14220
+ if (i === 1 && p === "" && parts[0] === "")
14221
+ continue;
14222
+ if (p === "." || p === "") {
14223
+ didSomething = true;
14224
+ parts.splice(i, 1);
14225
+ i--;
14281
14226
  }
14282
- i += cls.length;
14283
- if (neg)
14284
- negs.push(unip);
14285
- else
14286
- ranges.push(unip);
14287
- uflag = uflag || u;
14288
- continue WHILE;
14227
+ }
14228
+ if (parts[0] === "." && parts.length === 2 && (parts[1] === "." || parts[1] === "")) {
14229
+ didSomething = true;
14230
+ parts.pop();
14289
14231
  }
14290
14232
  }
14291
- }
14292
- escaping = false;
14293
- if (rangeStart) {
14294
- if (c > rangeStart) {
14295
- ranges.push(braceEscape(rangeStart) + "-" + braceEscape(c));
14296
- } else if (c === rangeStart) {
14297
- ranges.push(braceEscape(c));
14233
+ let dd = 0;
14234
+ while (-1 !== (dd = parts.indexOf("..", dd + 1))) {
14235
+ const p = parts[dd - 1];
14236
+ if (p && p !== "." && p !== ".." && p !== "**") {
14237
+ didSomething = true;
14238
+ parts.splice(dd - 1, 2);
14239
+ dd -= 2;
14240
+ }
14298
14241
  }
14299
- rangeStart = "";
14300
- i++;
14301
- continue;
14302
- }
14303
- if (glob.startsWith("-]", i + 1)) {
14304
- ranges.push(braceEscape(c + "-"));
14305
- i += 2;
14306
- continue;
14307
- }
14308
- if (glob.startsWith("-", i + 1)) {
14309
- rangeStart = c;
14310
- i += 2;
14311
- continue;
14312
- }
14313
- ranges.push(braceEscape(c));
14314
- i++;
14315
- }
14316
- if (endPos < i) {
14317
- return ["", false, 0, false];
14318
- }
14319
- if (!ranges.length && !negs.length) {
14320
- return ["$.", false, glob.length - pos, true];
14321
- }
14322
- if (negs.length === 0 && ranges.length === 1 && /^\\?.$/.test(ranges[0]) && !negate) {
14323
- const r = ranges[0].length === 2 ? ranges[0].slice(-1) : ranges[0];
14324
- return [regexpEscape(r), false, endPos - pos, false];
14242
+ } while (didSomething);
14243
+ return parts.length === 0 ? [""] : parts;
14325
14244
  }
14326
- const sranges = "[" + (negate ? "^" : "") + rangesToString(ranges) + "]";
14327
- const snegs = "[" + (negate ? "" : "^") + rangesToString(negs) + "]";
14328
- const comb = ranges.length && negs.length ? "(" + sranges + "|" + snegs + ")" : ranges.length ? sranges : snegs;
14329
- return [comb, uflag, endPos - pos, true];
14330
- };
14331
-
14332
- // node_modules/minimatch/dist/esm/unescape.js
14333
- var unescape = (s, { windowsPathsNoEscape = false } = {}) => {
14334
- return windowsPathsNoEscape ? s.replace(/\[([^\/\\])\]/g, "$1") : s.replace(/((?!\\).|^)\[([^\/\\])\]/g, "$1$2").replace(/\\([^\/])/g, "$1");
14335
- };
14336
-
14337
- // node_modules/minimatch/dist/esm/ast.js
14338
- var types = /* @__PURE__ */ new Set(["!", "?", "+", "*", "@"]);
14339
- var isExtglobType = (c) => types.has(c);
14340
- var startNoTraversal = "(?!(?:^|/)\\.\\.?(?:$|/))";
14341
- var startNoDot = "(?!\\.)";
14342
- var addPatternStart = /* @__PURE__ */ new Set(["[", "."]);
14343
- var justDots = /* @__PURE__ */ new Set(["..", "."]);
14344
- var reSpecials = new Set("().*{}+?[]^$\\!");
14345
- var regExpEscape = (s) => s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
14346
- var qmark = "[^/]";
14347
- var star = qmark + "*?";
14348
- var starNoEmpty = qmark + "+?";
14349
- var AST = class _AST {
14350
- type;
14351
- #root;
14352
- #hasMagic;
14353
- #uflag = false;
14354
- #parts = [];
14355
- #parent;
14356
- #parentIndex;
14357
- #negs;
14358
- #filledNegs = false;
14359
- #options;
14360
- #toString;
14361
- // set to true if it's an extglob with no children
14362
- // (which really means one child of '')
14363
- #emptyExt = false;
14364
- constructor(type, parent, options = {}) {
14365
- this.type = type;
14366
- if (type)
14367
- this.#hasMagic = true;
14368
- this.#parent = parent;
14369
- this.#root = this.#parent ? this.#parent.#root : this;
14370
- this.#options = this.#root === this ? options : this.#root.#options;
14371
- this.#negs = this.#root === this ? [] : this.#root.#negs;
14372
- if (type === "!" && !this.#root.#filledNegs)
14373
- this.#negs.push(this);
14374
- this.#parentIndex = this.#parent ? this.#parent.#parts.length : 0;
14375
- }
14376
- get hasMagic() {
14377
- if (this.#hasMagic !== void 0)
14378
- return this.#hasMagic;
14379
- for (const p of this.#parts) {
14380
- if (typeof p === "string")
14381
- continue;
14382
- if (p.type || p.hasMagic)
14383
- return this.#hasMagic = true;
14384
- }
14385
- return this.#hasMagic;
14386
- }
14387
- // reconstructs the pattern
14388
- toString() {
14389
- if (this.#toString !== void 0)
14390
- return this.#toString;
14391
- if (!this.type) {
14392
- return this.#toString = this.#parts.map((p) => String(p)).join("");
14393
- } else {
14394
- return this.#toString = this.type + "(" + this.#parts.map((p) => String(p)).join("|") + ")";
14395
- }
14396
- }
14397
- #fillNegs() {
14398
- if (this !== this.#root)
14399
- throw new Error("should only call on root");
14400
- if (this.#filledNegs)
14401
- return this;
14402
- this.toString();
14403
- this.#filledNegs = true;
14404
- let n;
14405
- while (n = this.#negs.pop()) {
14406
- if (n.type !== "!")
14407
- continue;
14408
- let p = n;
14409
- let pp = p.#parent;
14410
- while (pp) {
14411
- for (let i = p.#parentIndex + 1; !pp.type && i < pp.#parts.length; i++) {
14412
- for (const part of n.#parts) {
14413
- if (typeof part === "string") {
14414
- throw new Error("string part in extglob AST??");
14245
+ // First phase: single-pattern processing
14246
+ // <pre> is 1 or more portions
14247
+ // <rest> is 1 or more portions
14248
+ // <p> is any portion other than ., .., '', or **
14249
+ // <e> is . or ''
14250
+ //
14251
+ // **/.. is *brutal* for filesystem walking performance, because
14252
+ // it effectively resets the recursive walk each time it occurs,
14253
+ // and ** cannot be reduced out by a .. pattern part like a regexp
14254
+ // or most strings (other than .., ., and '') can be.
14255
+ //
14256
+ // <pre>/**/../<p>/<p>/<rest> -> {<pre>/../<p>/<p>/<rest>,<pre>/**/<p>/<p>/<rest>}
14257
+ // <pre>/<e>/<rest> -> <pre>/<rest>
14258
+ // <pre>/<p>/../<rest> -> <pre>/<rest>
14259
+ // **/**/<rest> -> **/<rest>
14260
+ //
14261
+ // **/*/<rest> -> */**/<rest> <== not valid because ** doesn't follow
14262
+ // this WOULD be allowed if ** did follow symlinks, or * didn't
14263
+ firstPhasePreProcess(globParts) {
14264
+ let didSomething = false;
14265
+ do {
14266
+ didSomething = false;
14267
+ for (let parts of globParts) {
14268
+ let gs = -1;
14269
+ while (-1 !== (gs = parts.indexOf("**", gs + 1))) {
14270
+ let gss = gs;
14271
+ while (parts[gss + 1] === "**") {
14272
+ gss++;
14273
+ }
14274
+ if (gss > gs) {
14275
+ parts.splice(gs + 1, gss - gs);
14276
+ }
14277
+ let next = parts[gs + 1];
14278
+ const p = parts[gs + 2];
14279
+ const p2 = parts[gs + 3];
14280
+ if (next !== "..")
14281
+ continue;
14282
+ if (!p || p === "." || p === ".." || !p2 || p2 === "." || p2 === "..") {
14283
+ continue;
14284
+ }
14285
+ didSomething = true;
14286
+ parts.splice(gs, 1);
14287
+ const other = parts.slice(0);
14288
+ other[gs] = "**";
14289
+ globParts.push(other);
14290
+ gs--;
14291
+ }
14292
+ if (!this.preserveMultipleSlashes) {
14293
+ for (let i = 1; i < parts.length - 1; i++) {
14294
+ const p = parts[i];
14295
+ if (i === 1 && p === "" && parts[0] === "")
14296
+ continue;
14297
+ if (p === "." || p === "") {
14298
+ didSomething = true;
14299
+ parts.splice(i, 1);
14300
+ i--;
14415
14301
  }
14416
- part.copyIn(pp.#parts[i]);
14302
+ }
14303
+ if (parts[0] === "." && parts.length === 2 && (parts[1] === "." || parts[1] === "")) {
14304
+ didSomething = true;
14305
+ parts.pop();
14306
+ }
14307
+ }
14308
+ let dd = 0;
14309
+ while (-1 !== (dd = parts.indexOf("..", dd + 1))) {
14310
+ const p = parts[dd - 1];
14311
+ if (p && p !== "." && p !== ".." && p !== "**") {
14312
+ didSomething = true;
14313
+ const needDot = dd === 1 && parts[dd + 1] === "**";
14314
+ const splin = needDot ? ["."] : [];
14315
+ parts.splice(dd - 1, 2, ...splin);
14316
+ if (parts.length === 0)
14317
+ parts.push("");
14318
+ dd -= 2;
14417
14319
  }
14418
14320
  }
14419
- p = pp;
14420
- pp = p.#parent;
14421
14321
  }
14422
- }
14423
- return this;
14322
+ } while (didSomething);
14323
+ return globParts;
14424
14324
  }
14425
- push(...parts) {
14426
- for (const p of parts) {
14427
- if (p === "")
14428
- continue;
14429
- if (typeof p !== "string" && !(p instanceof _AST && p.#parent === this)) {
14430
- throw new Error("invalid part: " + p);
14325
+ // second phase: multi-pattern dedupes
14326
+ // {<pre>/*/<rest>,<pre>/<p>/<rest>} -> <pre>/*/<rest>
14327
+ // {<pre>/<rest>,<pre>/<rest>} -> <pre>/<rest>
14328
+ // {<pre>/**/<rest>,<pre>/<rest>} -> <pre>/**/<rest>
14329
+ //
14330
+ // {<pre>/**/<rest>,<pre>/**/<p>/<rest>} -> <pre>/**/<rest>
14331
+ // ^-- not valid because ** doens't follow symlinks
14332
+ secondPhasePreProcess(globParts) {
14333
+ for (let i = 0; i < globParts.length - 1; i++) {
14334
+ for (let j = i + 1; j < globParts.length; j++) {
14335
+ const matched = this.partsMatch(globParts[i], globParts[j], !this.preserveMultipleSlashes);
14336
+ if (matched) {
14337
+ globParts[i] = [];
14338
+ globParts[j] = matched;
14339
+ break;
14340
+ }
14431
14341
  }
14432
- this.#parts.push(p);
14433
- }
14434
- }
14435
- toJSON() {
14436
- const ret = this.type === null ? this.#parts.slice().map((p) => typeof p === "string" ? p : p.toJSON()) : [this.type, ...this.#parts.map((p) => p.toJSON())];
14437
- if (this.isStart() && !this.type)
14438
- ret.unshift([]);
14439
- if (this.isEnd() && (this === this.#root || this.#root.#filledNegs && this.#parent?.type === "!")) {
14440
- ret.push({});
14441
14342
  }
14442
- return ret;
14343
+ return globParts.filter((gs) => gs.length);
14443
14344
  }
14444
- isStart() {
14445
- if (this.#root === this)
14446
- return true;
14447
- if (!this.#parent?.isStart())
14448
- return false;
14449
- if (this.#parentIndex === 0)
14450
- return true;
14451
- const p = this.#parent;
14452
- for (let i = 0; i < this.#parentIndex; i++) {
14453
- const pp = p.#parts[i];
14454
- if (!(pp instanceof _AST && pp.type === "!")) {
14345
+ partsMatch(a, b, emptyGSMatch = false) {
14346
+ let ai = 0;
14347
+ let bi = 0;
14348
+ let result = [];
14349
+ let which = "";
14350
+ while (ai < a.length && bi < b.length) {
14351
+ if (a[ai] === b[bi]) {
14352
+ result.push(which === "b" ? b[bi] : a[ai]);
14353
+ ai++;
14354
+ bi++;
14355
+ } else if (emptyGSMatch && a[ai] === "**" && b[bi] === a[ai + 1]) {
14356
+ result.push(a[ai]);
14357
+ ai++;
14358
+ } else if (emptyGSMatch && b[bi] === "**" && a[ai] === b[bi + 1]) {
14359
+ result.push(b[bi]);
14360
+ bi++;
14361
+ } else if (a[ai] === "*" && b[bi] && (this.options.dot || !b[bi].startsWith(".")) && b[bi] !== "**") {
14362
+ if (which === "b")
14363
+ return false;
14364
+ which = "a";
14365
+ result.push(a[ai]);
14366
+ ai++;
14367
+ bi++;
14368
+ } else if (b[bi] === "*" && a[ai] && (this.options.dot || !a[ai].startsWith(".")) && a[ai] !== "**") {
14369
+ if (which === "a")
14370
+ return false;
14371
+ which = "b";
14372
+ result.push(b[bi]);
14373
+ ai++;
14374
+ bi++;
14375
+ } else {
14455
14376
  return false;
14456
14377
  }
14457
14378
  }
14458
- return true;
14459
- }
14460
- isEnd() {
14461
- if (this.#root === this)
14462
- return true;
14463
- if (this.#parent?.type === "!")
14464
- return true;
14465
- if (!this.#parent?.isEnd())
14466
- return false;
14467
- if (!this.type)
14468
- return this.#parent?.isEnd();
14469
- const pl = this.#parent ? this.#parent.#parts.length : 0;
14470
- return this.#parentIndex === pl - 1;
14471
- }
14472
- copyIn(part) {
14473
- if (typeof part === "string")
14474
- this.push(part);
14475
- else
14476
- this.push(part.clone(this));
14379
+ return a.length === b.length && result;
14477
14380
  }
14478
- clone(parent) {
14479
- const c = new _AST(this.type, parent);
14480
- for (const p of this.#parts) {
14481
- c.copyIn(p);
14381
+ parseNegate() {
14382
+ if (this.nonegate)
14383
+ return;
14384
+ const pattern = this.pattern;
14385
+ let negate = false;
14386
+ let negateOffset = 0;
14387
+ for (let i = 0; i < pattern.length && pattern.charAt(i) === "!"; i++) {
14388
+ negate = !negate;
14389
+ negateOffset++;
14482
14390
  }
14483
- return c;
14391
+ if (negateOffset)
14392
+ this.pattern = pattern.slice(negateOffset);
14393
+ this.negate = negate;
14484
14394
  }
14485
- static #parseAST(str, ast, pos, opt) {
14486
- let escaping = false;
14487
- let inBrace = false;
14488
- let braceStart = -1;
14489
- let braceNeg = false;
14490
- if (ast.type === null) {
14491
- let i2 = pos;
14492
- let acc2 = "";
14493
- while (i2 < str.length) {
14494
- const c = str.charAt(i2++);
14495
- if (escaping || c === "\\") {
14496
- escaping = !escaping;
14497
- acc2 += c;
14498
- continue;
14499
- }
14500
- if (inBrace) {
14501
- if (i2 === braceStart + 1) {
14502
- if (c === "^" || c === "!") {
14503
- braceNeg = true;
14504
- }
14505
- } else if (c === "]" && !(i2 === braceStart + 2 && braceNeg)) {
14506
- inBrace = false;
14395
+ // set partial to true to test if, for example,
14396
+ // "/a/b" matches the start of "/*/b/*/d"
14397
+ // Partial means, if you run out of file before you run
14398
+ // out of pattern, then that's fine, as long as all
14399
+ // the parts match.
14400
+ matchOne(file, pattern, partial = false) {
14401
+ const options = this.options;
14402
+ if (this.isWindows) {
14403
+ const fileDrive = typeof file[0] === "string" && /^[a-z]:$/i.test(file[0]);
14404
+ const fileUNC = !fileDrive && file[0] === "" && file[1] === "" && file[2] === "?" && /^[a-z]:$/i.test(file[3]);
14405
+ const patternDrive = typeof pattern[0] === "string" && /^[a-z]:$/i.test(pattern[0]);
14406
+ const patternUNC = !patternDrive && pattern[0] === "" && pattern[1] === "" && pattern[2] === "?" && typeof pattern[3] === "string" && /^[a-z]:$/i.test(pattern[3]);
14407
+ const fdi = fileUNC ? 3 : fileDrive ? 0 : void 0;
14408
+ const pdi = patternUNC ? 3 : patternDrive ? 0 : void 0;
14409
+ if (typeof fdi === "number" && typeof pdi === "number") {
14410
+ const [fd, pd] = [file[fdi], pattern[pdi]];
14411
+ if (fd.toLowerCase() === pd.toLowerCase()) {
14412
+ pattern[pdi] = fd;
14413
+ if (pdi > fdi) {
14414
+ pattern = pattern.slice(pdi);
14415
+ } else if (fdi > pdi) {
14416
+ file = file.slice(fdi);
14507
14417
  }
14508
- acc2 += c;
14509
- continue;
14510
- } else if (c === "[") {
14511
- inBrace = true;
14512
- braceStart = i2;
14513
- braceNeg = false;
14514
- acc2 += c;
14515
- continue;
14516
- }
14517
- if (!opt.noext && isExtglobType(c) && str.charAt(i2) === "(") {
14518
- ast.push(acc2);
14519
- acc2 = "";
14520
- const ext2 = new _AST(c, ast);
14521
- i2 = _AST.#parseAST(str, ext2, i2, opt);
14522
- ast.push(ext2);
14523
- continue;
14524
14418
  }
14525
- acc2 += c;
14526
14419
  }
14527
- ast.push(acc2);
14528
- return i2;
14529
14420
  }
14530
- let i = pos + 1;
14531
- let part = new _AST(null, ast);
14532
- const parts = [];
14533
- let acc = "";
14534
- while (i < str.length) {
14535
- const c = str.charAt(i++);
14536
- if (escaping || c === "\\") {
14537
- escaping = !escaping;
14538
- acc += c;
14539
- continue;
14421
+ const { optimizationLevel = 1 } = this.options;
14422
+ if (optimizationLevel >= 2) {
14423
+ file = this.levelTwoFileOptimize(file);
14424
+ }
14425
+ this.debug("matchOne", this, { file, pattern });
14426
+ this.debug("matchOne", file.length, pattern.length);
14427
+ for (var fi = 0, pi = 0, fl = file.length, pl = pattern.length; fi < fl && pi < pl; fi++, pi++) {
14428
+ this.debug("matchOne loop");
14429
+ var p = pattern[pi];
14430
+ var f = file[fi];
14431
+ this.debug(pattern, p, f);
14432
+ if (p === false) {
14433
+ return false;
14540
14434
  }
14541
- if (inBrace) {
14542
- if (i === braceStart + 1) {
14543
- if (c === "^" || c === "!") {
14544
- braceNeg = true;
14435
+ if (p === GLOBSTAR) {
14436
+ this.debug("GLOBSTAR", [pattern, p, f]);
14437
+ var fr = fi;
14438
+ var pr = pi + 1;
14439
+ if (pr === pl) {
14440
+ this.debug("** at the end");
14441
+ for (; fi < fl; fi++) {
14442
+ if (file[fi] === "." || file[fi] === ".." || !options.dot && file[fi].charAt(0) === ".")
14443
+ return false;
14545
14444
  }
14546
- } else if (c === "]" && !(i === braceStart + 2 && braceNeg)) {
14547
- inBrace = false;
14445
+ return true;
14548
14446
  }
14549
- acc += c;
14550
- continue;
14551
- } else if (c === "[") {
14552
- inBrace = true;
14553
- braceStart = i;
14554
- braceNeg = false;
14555
- acc += c;
14556
- continue;
14557
- }
14558
- if (isExtglobType(c) && str.charAt(i) === "(") {
14559
- part.push(acc);
14560
- acc = "";
14561
- const ext2 = new _AST(c, part);
14562
- part.push(ext2);
14563
- i = _AST.#parseAST(str, ext2, i, opt);
14564
- continue;
14565
- }
14566
- if (c === "|") {
14567
- part.push(acc);
14568
- acc = "";
14569
- parts.push(part);
14570
- part = new _AST(null, ast);
14571
- continue;
14572
- }
14573
- if (c === ")") {
14574
- if (acc === "" && ast.#parts.length === 0) {
14575
- ast.#emptyExt = true;
14447
+ while (fr < fl) {
14448
+ var swallowee = file[fr];
14449
+ this.debug("\nglobstar while", file, fr, pattern, pr, swallowee);
14450
+ if (this.matchOne(file.slice(fr), pattern.slice(pr), partial)) {
14451
+ this.debug("globstar found match!", fr, fl, swallowee);
14452
+ return true;
14453
+ } else {
14454
+ if (swallowee === "." || swallowee === ".." || !options.dot && swallowee.charAt(0) === ".") {
14455
+ this.debug("dot detected!", file, fr, pattern, pr);
14456
+ break;
14457
+ }
14458
+ this.debug("globstar swallow a segment, and continue");
14459
+ fr++;
14460
+ }
14576
14461
  }
14577
- part.push(acc);
14578
- acc = "";
14579
- ast.push(...parts, part);
14580
- return i;
14462
+ if (partial) {
14463
+ this.debug("\n>>> no match, partial?", file, fr, pattern, pr);
14464
+ if (fr === fl) {
14465
+ return true;
14466
+ }
14467
+ }
14468
+ return false;
14581
14469
  }
14582
- acc += c;
14470
+ let hit;
14471
+ if (typeof p === "string") {
14472
+ hit = f === p;
14473
+ this.debug("string match", p, f, hit);
14474
+ } else {
14475
+ hit = p.test(f);
14476
+ this.debug("pattern match", p, f, hit);
14477
+ }
14478
+ if (!hit)
14479
+ return false;
14480
+ }
14481
+ if (fi === fl && pi === pl) {
14482
+ return true;
14483
+ } else if (fi === fl) {
14484
+ return partial;
14485
+ } else if (pi === pl) {
14486
+ return fi === fl - 1 && file[fi] === "";
14487
+ } else {
14488
+ throw new Error("wtf?");
14583
14489
  }
14584
- ast.type = null;
14585
- ast.#hasMagic = void 0;
14586
- ast.#parts = [str.substring(pos - 1)];
14587
- return i;
14588
14490
  }
14589
- static fromGlob(pattern, options = {}) {
14590
- const ast = new _AST(null, void 0, options);
14591
- _AST.#parseAST(pattern, ast, 0, options);
14592
- return ast;
14491
+ braceExpand() {
14492
+ return braceExpand(this.pattern, this.options);
14593
14493
  }
14594
- // returns the regular expression if there's magic, or the unescaped
14595
- // string if not.
14596
- toMMPattern() {
14597
- if (this !== this.#root)
14598
- return this.#root.toMMPattern();
14599
- const glob = this.toString();
14600
- const [re, body, hasMagic, uflag] = this.toRegExpSource();
14601
- const anyMagic = hasMagic || this.#hasMagic || this.#options.nocase && !this.#options.nocaseMagicOnly && glob.toUpperCase() !== glob.toLowerCase();
14602
- if (!anyMagic) {
14603
- return body;
14494
+ parse(pattern) {
14495
+ assertValidPattern(pattern);
14496
+ const options = this.options;
14497
+ if (pattern === "**")
14498
+ return GLOBSTAR;
14499
+ if (pattern === "")
14500
+ return "";
14501
+ let m;
14502
+ let fastTest = null;
14503
+ if (m = pattern.match(starRE)) {
14504
+ fastTest = options.dot ? starTestDot : starTest;
14505
+ } else if (m = pattern.match(starDotExtRE)) {
14506
+ fastTest = (options.nocase ? options.dot ? starDotExtTestNocaseDot : starDotExtTestNocase : options.dot ? starDotExtTestDot : starDotExtTest)(m[1]);
14507
+ } else if (m = pattern.match(qmarksRE)) {
14508
+ fastTest = (options.nocase ? options.dot ? qmarksTestNocaseDot : qmarksTestNocase : options.dot ? qmarksTestDot : qmarksTest)(m);
14509
+ } else if (m = pattern.match(starDotStarRE)) {
14510
+ fastTest = options.dot ? starDotStarTestDot : starDotStarTest;
14511
+ } else if (m = pattern.match(dotStarRE)) {
14512
+ fastTest = dotStarTest;
14604
14513
  }
14605
- const flags = (this.#options.nocase ? "i" : "") + (uflag ? "u" : "");
14606
- return Object.assign(new RegExp(`^${re}$`, flags), {
14607
- _src: re,
14608
- _glob: glob
14609
- });
14610
- }
14611
- get options() {
14612
- return this.#options;
14514
+ const re = AST.fromGlob(pattern, this.options).toMMPattern();
14515
+ if (fastTest && typeof re === "object") {
14516
+ Reflect.defineProperty(re, "test", { value: fastTest });
14517
+ }
14518
+ return re;
14613
14519
  }
14614
- // returns the string match, the regexp source, whether there's magic
14615
- // in the regexp (so a regular expression is required) and whether or
14616
- // not the uflag is needed for the regular expression (for posix classes)
14617
- // TODO: instead of injecting the start/end at this point, just return
14618
- // the BODY of the regexp, along with the start/end portions suitable
14619
- // for binding the start/end in either a joined full-path makeRe context
14620
- // (where we bind to (^|/), or a standalone matchPart context (where
14621
- // we bind to ^, and not /). Otherwise slashes get duped!
14622
- //
14623
- // In part-matching mode, the start is:
14624
- // - if not isStart: nothing
14625
- // - if traversal possible, but not allowed: ^(?!\.\.?$)
14626
- // - if dots allowed or not possible: ^
14627
- // - if dots possible and not allowed: ^(?!\.)
14628
- // end is:
14629
- // - if not isEnd(): nothing
14630
- // - else: $
14631
- //
14632
- // In full-path matching mode, we put the slash at the START of the
14633
- // pattern, so start is:
14634
- // - if first pattern: same as part-matching mode
14635
- // - if not isStart(): nothing
14636
- // - if traversal possible, but not allowed: /(?!\.\.?(?:$|/))
14637
- // - if dots allowed or not possible: /
14638
- // - if dots possible and not allowed: /(?!\.)
14639
- // end is:
14640
- // - if last pattern, same as part-matching mode
14641
- // - else nothing
14642
- //
14643
- // Always put the (?:$|/) on negated tails, though, because that has to be
14644
- // there to bind the end of the negated pattern portion, and it's easier to
14645
- // just stick it in now rather than try to inject it later in the middle of
14646
- // the pattern.
14647
- //
14648
- // We can just always return the same end, and leave it up to the caller
14649
- // to know whether it's going to be used joined or in parts.
14650
- // And, if the start is adjusted slightly, can do the same there:
14651
- // - if not isStart: nothing
14652
- // - if traversal possible, but not allowed: (?:/|^)(?!\.\.?$)
14653
- // - if dots allowed or not possible: (?:/|^)
14654
- // - if dots possible and not allowed: (?:/|^)(?!\.)
14655
- //
14656
- // But it's better to have a simpler binding without a conditional, for
14657
- // performance, so probably better to return both start options.
14658
- //
14659
- // Then the caller just ignores the end if it's not the first pattern,
14660
- // and the start always gets applied.
14661
- //
14662
- // But that's always going to be $ if it's the ending pattern, or nothing,
14663
- // so the caller can just attach $ at the end of the pattern when building.
14664
- //
14665
- // So the todo is:
14666
- // - better detect what kind of start is needed
14667
- // - return both flavors of starting pattern
14668
- // - attach $ at the end of the pattern when creating the actual RegExp
14669
- //
14670
- // Ah, but wait, no, that all only applies to the root when the first pattern
14671
- // is not an extglob. If the first pattern IS an extglob, then we need all
14672
- // that dot prevention biz to live in the extglob portions, because eg
14673
- // +(*|.x*) can match .xy but not .yx.
14674
- //
14675
- // So, return the two flavors if it's #root and the first child is not an
14676
- // AST, otherwise leave it to the child AST to handle it, and there,
14677
- // use the (?:^|/) style of start binding.
14678
- //
14679
- // Even simplified further:
14680
- // - Since the start for a join is eg /(?!\.) and the start for a part
14681
- // is ^(?!\.), we can just prepend (?!\.) to the pattern (either root
14682
- // or start or whatever) and prepend ^ or / at the Regexp construction.
14683
- toRegExpSource(allowDot) {
14684
- const dot = allowDot ?? !!this.#options.dot;
14685
- if (this.#root === this)
14686
- this.#fillNegs();
14687
- if (!this.type) {
14688
- const noEmpty = this.isStart() && this.isEnd();
14689
- const src = this.#parts.map((p) => {
14690
- const [re, _, hasMagic, uflag] = typeof p === "string" ? _AST.#parseGlob(p, this.#hasMagic, noEmpty) : p.toRegExpSource(allowDot);
14691
- this.#hasMagic = this.#hasMagic || hasMagic;
14692
- this.#uflag = this.#uflag || uflag;
14693
- return re;
14694
- }).join("");
14695
- let start2 = "";
14696
- if (this.isStart()) {
14697
- if (typeof this.#parts[0] === "string") {
14698
- const dotTravAllowed = this.#parts.length === 1 && justDots.has(this.#parts[0]);
14699
- if (!dotTravAllowed) {
14700
- const aps = addPatternStart;
14701
- const needNoTrav = (
14702
- // dots are allowed, and the pattern starts with [ or .
14703
- dot && aps.has(src.charAt(0)) || // the pattern starts with \., and then [ or .
14704
- src.startsWith("\\.") && aps.has(src.charAt(2)) || // the pattern starts with \.\., and then [ or .
14705
- src.startsWith("\\.\\.") && aps.has(src.charAt(4))
14706
- );
14707
- const needNoDot = !dot && !allowDot && aps.has(src.charAt(0));
14708
- start2 = needNoTrav ? startNoTraversal : needNoDot ? startNoDot : "";
14520
+ makeRe() {
14521
+ if (this.regexp || this.regexp === false)
14522
+ return this.regexp;
14523
+ const set = this.set;
14524
+ if (!set.length) {
14525
+ this.regexp = false;
14526
+ return this.regexp;
14527
+ }
14528
+ const options = this.options;
14529
+ const twoStar = options.noglobstar ? star2 : options.dot ? twoStarDot : twoStarNoDot;
14530
+ const flags = new Set(options.nocase ? ["i"] : []);
14531
+ let re = set.map((pattern) => {
14532
+ const pp = pattern.map((p) => {
14533
+ if (p instanceof RegExp) {
14534
+ for (const f of p.flags.split(""))
14535
+ flags.add(f);
14536
+ }
14537
+ return typeof p === "string" ? regExpEscape2(p) : p === GLOBSTAR ? GLOBSTAR : p._src;
14538
+ });
14539
+ pp.forEach((p, i) => {
14540
+ const next = pp[i + 1];
14541
+ const prev = pp[i - 1];
14542
+ if (p !== GLOBSTAR || prev === GLOBSTAR) {
14543
+ return;
14544
+ }
14545
+ if (prev === void 0) {
14546
+ if (next !== void 0 && next !== GLOBSTAR) {
14547
+ pp[i + 1] = "(?:\\/|" + twoStar + "\\/)?" + next;
14548
+ } else {
14549
+ pp[i] = twoStar;
14709
14550
  }
14551
+ } else if (next === void 0) {
14552
+ pp[i - 1] = prev + "(?:\\/|" + twoStar + ")?";
14553
+ } else if (next !== GLOBSTAR) {
14554
+ pp[i - 1] = prev + "(?:\\/|\\/" + twoStar + "\\/)" + next;
14555
+ pp[i + 1] = GLOBSTAR;
14710
14556
  }
14711
- }
14712
- let end = "";
14713
- if (this.isEnd() && this.#root.#filledNegs && this.#parent?.type === "!") {
14714
- end = "(?:$|\\/)";
14715
- }
14716
- const final2 = start2 + src + end;
14717
- return [
14718
- final2,
14719
- unescape(src),
14720
- this.#hasMagic = !!this.#hasMagic,
14721
- this.#uflag
14722
- ];
14557
+ });
14558
+ return pp.filter((p) => p !== GLOBSTAR).join("/");
14559
+ }).join("|");
14560
+ const [open, close] = set.length > 1 ? ["(?:", ")"] : ["", ""];
14561
+ re = "^" + open + re + close + "$";
14562
+ if (this.negate)
14563
+ re = "^(?!" + re + ").+$";
14564
+ try {
14565
+ this.regexp = new RegExp(re, [...flags].join(""));
14566
+ } catch (ex) {
14567
+ this.regexp = false;
14723
14568
  }
14724
- const repeated = this.type === "*" || this.type === "+";
14725
- const start = this.type === "!" ? "(?:(?!(?:" : "(?:";
14726
- let body = this.#partsToRegExp(dot);
14727
- if (this.isStart() && this.isEnd() && !body && this.type !== "!") {
14728
- const s = this.toString();
14729
- this.#parts = [s];
14730
- this.type = null;
14731
- this.#hasMagic = void 0;
14732
- return [s, unescape(this.toString()), false, false];
14569
+ return this.regexp;
14570
+ }
14571
+ slashSplit(p) {
14572
+ if (this.preserveMultipleSlashes) {
14573
+ return p.split("/");
14574
+ } else if (this.isWindows && /^\/\/[^\/]+/.test(p)) {
14575
+ return ["", ...p.split(/\/+/)];
14576
+ } else {
14577
+ return p.split(/\/+/);
14733
14578
  }
14734
- let bodyDotAllowed = !repeated || allowDot || dot || !startNoDot ? "" : this.#partsToRegExp(true);
14735
- if (bodyDotAllowed === body) {
14736
- bodyDotAllowed = "";
14579
+ }
14580
+ match(f, partial = this.partial) {
14581
+ this.debug("match", f, this.pattern);
14582
+ if (this.comment) {
14583
+ return false;
14737
14584
  }
14738
- if (bodyDotAllowed) {
14739
- body = `(?:${body})(?:${bodyDotAllowed})*?`;
14585
+ if (this.empty) {
14586
+ return f === "";
14740
14587
  }
14741
- let final = "";
14742
- if (this.type === "!" && this.#emptyExt) {
14743
- final = (this.isStart() && !dot ? startNoDot : "") + starNoEmpty;
14744
- } else {
14745
- const close = this.type === "!" ? (
14746
- // !() must match something,but !(x) can match ''
14747
- "))" + (this.isStart() && !dot && !allowDot ? startNoDot : "") + star + ")"
14748
- ) : this.type === "@" ? ")" : this.type === "?" ? ")?" : this.type === "+" && bodyDotAllowed ? ")" : this.type === "*" && bodyDotAllowed ? `)?` : `)${this.type}`;
14749
- final = start + body + close;
14588
+ if (f === "/" && partial) {
14589
+ return true;
14750
14590
  }
14751
- return [
14752
- final,
14753
- unescape(body),
14754
- this.#hasMagic = !!this.#hasMagic,
14755
- this.#uflag
14756
- ];
14757
- }
14758
- #partsToRegExp(dot) {
14759
- return this.#parts.map((p) => {
14760
- if (typeof p === "string") {
14761
- throw new Error("string type in extglob ast??");
14762
- }
14763
- const [re, _, _hasMagic, uflag] = p.toRegExpSource(dot);
14764
- this.#uflag = this.#uflag || uflag;
14765
- return re;
14766
- }).filter((p) => !(this.isStart() && this.isEnd()) || !!p).join("|");
14767
- }
14768
- static #parseGlob(glob, hasMagic, noEmpty = false) {
14769
- let escaping = false;
14770
- let re = "";
14771
- let uflag = false;
14772
- for (let i = 0; i < glob.length; i++) {
14773
- const c = glob.charAt(i);
14774
- if (escaping) {
14775
- escaping = false;
14776
- re += (reSpecials.has(c) ? "\\" : "") + c;
14777
- continue;
14591
+ const options = this.options;
14592
+ if (this.isWindows) {
14593
+ f = f.split("\\").join("/");
14594
+ }
14595
+ const ff = this.slashSplit(f);
14596
+ this.debug(this.pattern, "split", ff);
14597
+ const set = this.set;
14598
+ this.debug(this.pattern, "set", set);
14599
+ let filename = ff[ff.length - 1];
14600
+ if (!filename) {
14601
+ for (let i = ff.length - 2; !filename && i >= 0; i--) {
14602
+ filename = ff[i];
14778
14603
  }
14779
- if (c === "\\") {
14780
- if (i === glob.length - 1) {
14781
- re += "\\\\";
14782
- } else {
14783
- escaping = true;
14784
- }
14785
- continue;
14604
+ }
14605
+ for (let i = 0; i < set.length; i++) {
14606
+ const pattern = set[i];
14607
+ let file = ff;
14608
+ if (options.matchBase && pattern.length === 1) {
14609
+ file = [filename];
14786
14610
  }
14787
- if (c === "[") {
14788
- const [src, needUflag, consumed, magic] = parseClass(glob, i);
14789
- if (consumed) {
14790
- re += src;
14791
- uflag = uflag || needUflag;
14792
- i += consumed - 1;
14793
- hasMagic = hasMagic || magic;
14794
- continue;
14611
+ const hit = this.matchOne(file, pattern, partial);
14612
+ if (hit) {
14613
+ if (options.flipNegate) {
14614
+ return true;
14795
14615
  }
14616
+ return !this.negate;
14796
14617
  }
14797
- if (c === "*") {
14798
- if (noEmpty && glob === "*")
14799
- re += starNoEmpty;
14800
- else
14801
- re += star;
14802
- hasMagic = true;
14803
- continue;
14804
- }
14805
- if (c === "?") {
14806
- re += qmark;
14807
- hasMagic = true;
14808
- continue;
14809
- }
14810
- re += regExpEscape(c);
14811
14618
  }
14812
- return [re, unescape(glob), !!hasMagic, uflag];
14619
+ if (options.flipNegate) {
14620
+ return false;
14621
+ }
14622
+ return this.negate;
14623
+ }
14624
+ static defaults(def) {
14625
+ return minimatch.defaults(def).Minimatch;
14813
14626
  }
14814
14627
  };
14628
+ minimatch.AST = AST;
14629
+ minimatch.Minimatch = Minimatch;
14630
+ minimatch.escape = escape;
14631
+ minimatch.unescape = unescape;
14815
14632
 
14816
- // node_modules/minimatch/dist/esm/escape.js
14817
- var escape = (s, { windowsPathsNoEscape = false } = {}) => {
14818
- return windowsPathsNoEscape ? s.replace(/[?*()[\]]/g, "[$&]") : s.replace(/[?*()[\]\\]/g, "\\$&");
14819
- };
14633
+ // src/LockfileManager.ts
14634
+ var import_yaml = __toESM(require_dist3(), 1);
14820
14635
 
14821
- // node_modules/minimatch/dist/esm/index.js
14822
- var minimatch = (p, pattern, options = {}) => {
14823
- assertValidPattern(pattern);
14824
- if (!options.nocomment && pattern.charAt(0) === "#") {
14825
- return false;
14636
+ // src/credentials.ts
14637
+ var CREDENTIAL_PATTERNS = [
14638
+ { name: "PRIVATE KEY block", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
14639
+ { name: "OPENSSH key", re: /-----BEGIN OPENSSH PRIVATE KEY-----/ },
14640
+ { name: "RSA key", re: /-----BEGIN RSA PRIVATE KEY-----/ },
14641
+ { name: "CERTIFICATE", re: /-----BEGIN CERTIFICATE-----/ },
14642
+ { name: "TOKEN=", re: /\bTOKEN\s*=\s*["']?[A-Za-z0-9._\-+/=]{10,}/ },
14643
+ { name: "api_key=", re: /\bapi[_-]?key\s*=\s*["']?[A-Za-z0-9._\-+/=]{10,}/i },
14644
+ { name: "password=", re: /\bpassword\s*=\s*["']?[^\s"']{6,}/i },
14645
+ { name: "AWS_SECRET_ACCESS_KEY", re: /AWS_SECRET_ACCESS_KEY/ },
14646
+ { name: "JWT (eyJ...)", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ }
14647
+ ];
14648
+ var SENSITIVE_FILE_PATTERNS = [
14649
+ /(^|\/)\.env(\.[^/]+)?$/,
14650
+ /\.pem$/,
14651
+ /\.key$/,
14652
+ /\.pkce_state\.json$/,
14653
+ /(^|\/)id_rsa$/,
14654
+ /(^|\/)id_ed25519$/
14655
+ ];
14656
+ function isSensitiveFile(filePath) {
14657
+ return SENSITIVE_FILE_PATTERNS.some((re) => re.test(filePath));
14658
+ }
14659
+ function scanForCredentials(content) {
14660
+ const matches = [];
14661
+ for (const { name, re } of CREDENTIAL_PATTERNS) {
14662
+ if (re.test(content)) {
14663
+ matches.push(name);
14664
+ }
14665
+ }
14666
+ return matches;
14667
+ }
14668
+
14669
+ // src/LockfileManager.ts
14670
+ var LOCKFILE_HEADER = "# DO NOT EDIT MANUALLY - Managed by Fern Replay\n";
14671
+ var LockfileNotFoundError = class extends Error {
14672
+ constructor(path2) {
14673
+ super(`Lockfile not found: ${path2}`);
14674
+ this.name = "LockfileNotFoundError";
14826
14675
  }
14827
- return new Minimatch(pattern, options).match(p);
14828
- };
14829
- var starDotExtRE = /^\*+([^+@!?\*\[\(]*)$/;
14830
- var starDotExtTest = (ext2) => (f) => !f.startsWith(".") && f.endsWith(ext2);
14831
- var starDotExtTestDot = (ext2) => (f) => f.endsWith(ext2);
14832
- var starDotExtTestNocase = (ext2) => {
14833
- ext2 = ext2.toLowerCase();
14834
- return (f) => !f.startsWith(".") && f.toLowerCase().endsWith(ext2);
14835
- };
14836
- var starDotExtTestNocaseDot = (ext2) => {
14837
- ext2 = ext2.toLowerCase();
14838
- return (f) => f.toLowerCase().endsWith(ext2);
14839
- };
14840
- var starDotStarRE = /^\*+\.\*+$/;
14841
- var starDotStarTest = (f) => !f.startsWith(".") && f.includes(".");
14842
- var starDotStarTestDot = (f) => f !== "." && f !== ".." && f.includes(".");
14843
- var dotStarRE = /^\.\*+$/;
14844
- var dotStarTest = (f) => f !== "." && f !== ".." && f.startsWith(".");
14845
- var starRE = /^\*+$/;
14846
- var starTest = (f) => f.length !== 0 && !f.startsWith(".");
14847
- var starTestDot = (f) => f.length !== 0 && f !== "." && f !== "..";
14848
- var qmarksRE = /^\?+([^+@!?\*\[\(]*)?$/;
14849
- var qmarksTestNocase = ([$0, ext2 = ""]) => {
14850
- const noext = qmarksTestNoExt([$0]);
14851
- if (!ext2)
14852
- return noext;
14853
- ext2 = ext2.toLowerCase();
14854
- return (f) => noext(f) && f.toLowerCase().endsWith(ext2);
14855
- };
14856
- var qmarksTestNocaseDot = ([$0, ext2 = ""]) => {
14857
- const noext = qmarksTestNoExtDot([$0]);
14858
- if (!ext2)
14859
- return noext;
14860
- ext2 = ext2.toLowerCase();
14861
- return (f) => noext(f) && f.toLowerCase().endsWith(ext2);
14862
- };
14863
- var qmarksTestDot = ([$0, ext2 = ""]) => {
14864
- const noext = qmarksTestNoExtDot([$0]);
14865
- return !ext2 ? noext : (f) => noext(f) && f.endsWith(ext2);
14866
- };
14867
- var qmarksTest = ([$0, ext2 = ""]) => {
14868
- const noext = qmarksTestNoExt([$0]);
14869
- return !ext2 ? noext : (f) => noext(f) && f.endsWith(ext2);
14870
- };
14871
- var qmarksTestNoExt = ([$0]) => {
14872
- const len = $0.length;
14873
- return (f) => f.length === len && !f.startsWith(".");
14874
- };
14875
- var qmarksTestNoExtDot = ([$0]) => {
14876
- const len = $0.length;
14877
- return (f) => f.length === len && f !== "." && f !== "..";
14878
- };
14879
- var defaultPlatform = typeof process === "object" && process ? typeof process.env === "object" && process.env && process.env.__MINIMATCH_TESTING_PLATFORM__ || process.platform : "posix";
14880
- var path = {
14881
- win32: { sep: "\\" },
14882
- posix: { sep: "/" }
14883
14676
  };
14884
- var sep = defaultPlatform === "win32" ? path.win32.sep : path.posix.sep;
14885
- minimatch.sep = sep;
14886
- var GLOBSTAR = /* @__PURE__ */ Symbol("globstar **");
14887
- minimatch.GLOBSTAR = GLOBSTAR;
14888
- var qmark2 = "[^/]";
14889
- var star2 = qmark2 + "*?";
14890
- var twoStarDot = "(?:(?!(?:\\/|^)(?:\\.{1,2})($|\\/)).)*?";
14891
- var twoStarNoDot = "(?:(?!(?:\\/|^)\\.).)*?";
14892
- var filter = (pattern, options = {}) => (p) => minimatch(p, pattern, options);
14893
- minimatch.filter = filter;
14894
- var ext = (a, b = {}) => Object.assign({}, a, b);
14895
- var defaults = (def) => {
14896
- if (!def || typeof def !== "object" || !Object.keys(def).length) {
14897
- return minimatch;
14677
+ var LockfileManager = class {
14678
+ outputDir;
14679
+ lock = null;
14680
+ fernignorePatterns = [];
14681
+ warnings = [];
14682
+ constructor(outputDir) {
14683
+ this.outputDir = outputDir;
14898
14684
  }
14899
- const orig = minimatch;
14900
- const m = (p, pattern, options = {}) => orig(p, pattern, ext(def, options));
14901
- return Object.assign(m, {
14902
- Minimatch: class Minimatch extends orig.Minimatch {
14903
- constructor(pattern, options = {}) {
14904
- super(pattern, ext(def, options));
14905
- }
14906
- static defaults(options) {
14907
- return orig.defaults(ext(def, options)).Minimatch;
14685
+ /**
14686
+ * Inform the lockfile of the customer's `.fernignore` patterns so that
14687
+ * `save()` can suppress its "Patch X removed from lockfile" warning when
14688
+ * every file in the stripped patch is already `.fernignore`-protected.
14689
+ * The strip itself still happens (credentials never persist); only the
14690
+ * customer-visible warning is suppressed in that case, because the
14691
+ * protected files survive the generator wipe regardless of whether the
14692
+ * lockfile tracks them, so there's nothing for the customer to action.
14693
+ */
14694
+ setFernignorePatterns(patterns) {
14695
+ this.fernignorePatterns = patterns;
14696
+ }
14697
+ get lockfilePath() {
14698
+ return (0, import_node_path2.join)(this.outputDir, ".fern", "replay.lock");
14699
+ }
14700
+ get customizationsPath() {
14701
+ return (0, import_node_path2.join)(this.outputDir, ".fern", "replay.yml");
14702
+ }
14703
+ exists() {
14704
+ return (0, import_node_fs.existsSync)(this.lockfilePath);
14705
+ }
14706
+ read() {
14707
+ if (this.lock) {
14708
+ return this.lock;
14709
+ }
14710
+ try {
14711
+ const content = (0, import_node_fs.readFileSync)(this.lockfilePath, "utf-8");
14712
+ this.lock = (0, import_yaml.parse)(content);
14713
+ return this.lock;
14714
+ } catch (error) {
14715
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
14716
+ throw new LockfileNotFoundError(this.lockfilePath);
14908
14717
  }
14909
- },
14910
- AST: class AST extends orig.AST {
14911
- /* c8 ignore start */
14912
- constructor(type, parent, options = {}) {
14913
- super(type, parent, ext(def, options));
14718
+ throw error;
14719
+ }
14720
+ }
14721
+ initialize(firstGeneration) {
14722
+ this.initializeInMemory(firstGeneration);
14723
+ this.save();
14724
+ }
14725
+ /**
14726
+ * Set up in-memory lock state without writing to disk.
14727
+ * Useful for bootstrap dry-run where we need state for detection
14728
+ * but don't want to persist anything.
14729
+ */
14730
+ initializeInMemory(firstGeneration) {
14731
+ this.lock = {
14732
+ version: "1.0",
14733
+ generations: [firstGeneration],
14734
+ current_generation: firstGeneration.commit_sha,
14735
+ patches: []
14736
+ };
14737
+ }
14738
+ save() {
14739
+ if (!this.lock) {
14740
+ throw new Error("No lockfile data to save. Call read() or initialize() first.");
14741
+ }
14742
+ this.warnings.length = 0;
14743
+ const cleanPatches = [];
14744
+ for (const patch of this.lock.patches) {
14745
+ const diffMatches = scanForCredentials(patch.patch_content);
14746
+ const snapshotMatches = [];
14747
+ if (patch.theirs_snapshot) {
14748
+ for (const content2 of Object.values(patch.theirs_snapshot)) {
14749
+ const m = scanForCredentials(content2);
14750
+ for (const x of m) {
14751
+ if (!snapshotMatches.includes(x)) snapshotMatches.push(x);
14752
+ }
14753
+ }
14914
14754
  }
14915
- /* c8 ignore stop */
14916
- static fromGlob(pattern, options = {}) {
14917
- return orig.AST.fromGlob(pattern, ext(def, options));
14755
+ const credentialMatches = [...diffMatches, ...snapshotMatches.filter((m) => !diffMatches.includes(m))];
14756
+ const sensitiveFiles = patch.files.filter((f) => isSensitiveFile(f));
14757
+ if (credentialMatches.length > 0 || sensitiveFiles.length > 0) {
14758
+ const reasons = [];
14759
+ if (credentialMatches.length > 0) {
14760
+ reasons.push(`credential patterns: ${credentialMatches.join(", ")}`);
14761
+ }
14762
+ if (sensitiveFiles.length > 0) {
14763
+ reasons.push(`sensitive files: ${sensitiveFiles.join(", ")}`);
14764
+ }
14765
+ const allProtected = patch.files.length > 0 && patch.files.every(
14766
+ (f) => this.fernignorePatterns.some((p) => f === p || minimatch(f, p))
14767
+ );
14768
+ if (!allProtected) {
14769
+ this.warnings.push(
14770
+ `Patch ${patch.id} removed from lockfile: ${reasons.join("; ")}. This prevents credential exposure in committed lockfiles.`
14771
+ );
14772
+ }
14773
+ } else {
14774
+ cleanPatches.push(patch);
14918
14775
  }
14919
- },
14920
- unescape: (s, options = {}) => orig.unescape(s, ext(def, options)),
14921
- escape: (s, options = {}) => orig.escape(s, ext(def, options)),
14922
- filter: (pattern, options = {}) => orig.filter(pattern, ext(def, options)),
14923
- defaults: (options) => orig.defaults(ext(def, options)),
14924
- makeRe: (pattern, options = {}) => orig.makeRe(pattern, ext(def, options)),
14925
- braceExpand: (pattern, options = {}) => orig.braceExpand(pattern, ext(def, options)),
14926
- match: (list, pattern, options = {}) => orig.match(list, pattern, ext(def, options)),
14927
- sep: orig.sep,
14928
- GLOBSTAR
14929
- });
14930
- };
14931
- minimatch.defaults = defaults;
14932
- var braceExpand = (pattern, options = {}) => {
14933
- assertValidPattern(pattern);
14934
- if (options.nobrace || !/\{(?:(?!\{).)*\}/.test(pattern)) {
14935
- return [pattern];
14776
+ }
14777
+ this.lock.patches = cleanPatches;
14778
+ const dir = (0, import_node_path2.dirname)(this.lockfilePath);
14779
+ if (!(0, import_node_fs.existsSync)(dir)) {
14780
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true });
14781
+ }
14782
+ const yaml = (0, import_yaml.stringify)(this.lock, {
14783
+ lineWidth: 0,
14784
+ blockQuote: "literal"
14785
+ });
14786
+ const content = LOCKFILE_HEADER + yaml;
14787
+ const tmpPath = this.lockfilePath + ".tmp";
14788
+ (0, import_node_fs.writeFileSync)(tmpPath, content, "utf-8");
14789
+ (0, import_node_fs.renameSync)(tmpPath, this.lockfilePath);
14936
14790
  }
14937
- return (0, import_brace_expansion.default)(pattern);
14938
- };
14939
- minimatch.braceExpand = braceExpand;
14940
- var makeRe = (pattern, options = {}) => new Minimatch(pattern, options).makeRe();
14941
- minimatch.makeRe = makeRe;
14942
- var match = (list, pattern, options = {}) => {
14943
- const mm = new Minimatch(pattern, options);
14944
- list = list.filter((f) => mm.match(f));
14945
- if (mm.options.nonull && !list.length) {
14946
- list.push(pattern);
14791
+ addGeneration(record) {
14792
+ this.ensureLoaded();
14793
+ this.lock.generations.push(record);
14794
+ this.lock.current_generation = record.commit_sha;
14947
14795
  }
14948
- return list;
14949
- };
14950
- minimatch.match = match;
14951
- var globMagic = /[?*]|[+@!]\(.*?\)|\[|\]/;
14952
- var regExpEscape2 = (s) => s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
14953
- var Minimatch = class {
14954
- options;
14955
- set;
14956
- pattern;
14957
- windowsPathsNoEscape;
14958
- nonegate;
14959
- negate;
14960
- comment;
14961
- empty;
14962
- preserveMultipleSlashes;
14963
- partial;
14964
- globSet;
14965
- globParts;
14966
- nocase;
14967
- isWindows;
14968
- platform;
14969
- windowsNoMagicRoot;
14970
- regexp;
14971
- constructor(pattern, options = {}) {
14972
- assertValidPattern(pattern);
14973
- options = options || {};
14974
- this.options = options;
14975
- this.pattern = pattern;
14976
- this.platform = options.platform || defaultPlatform;
14977
- this.isWindows = this.platform === "win32";
14978
- this.windowsPathsNoEscape = !!options.windowsPathsNoEscape || options.allowWindowsEscape === false;
14979
- if (this.windowsPathsNoEscape) {
14980
- this.pattern = this.pattern.replace(/\\/g, "/");
14796
+ addPatch(patch) {
14797
+ this.ensureLoaded();
14798
+ this.lock.patches.push(patch);
14799
+ }
14800
+ updatePatch(patchId, updates) {
14801
+ this.ensureLoaded();
14802
+ const patch = this.lock.patches.find((p) => p.id === patchId);
14803
+ if (!patch) {
14804
+ throw new Error(`Patch not found: ${patchId}`);
14981
14805
  }
14982
- this.preserveMultipleSlashes = !!options.preserveMultipleSlashes;
14983
- this.regexp = null;
14984
- this.negate = false;
14985
- this.nonegate = !!options.nonegate;
14986
- this.comment = false;
14987
- this.empty = false;
14988
- this.partial = !!options.partial;
14989
- this.nocase = !!this.options.nocase;
14990
- this.windowsNoMagicRoot = options.windowsNoMagicRoot !== void 0 ? options.windowsNoMagicRoot : !!(this.isWindows && this.nocase);
14991
- this.globSet = [];
14992
- this.globParts = [];
14993
- this.set = [];
14994
- this.make();
14806
+ Object.assign(patch, updates);
14995
14807
  }
14996
- hasMagic() {
14997
- if (this.options.magicalBraces && this.set.length > 1) {
14998
- return true;
14808
+ removePatch(patchId) {
14809
+ this.ensureLoaded();
14810
+ this.lock.patches = this.lock.patches.filter((p) => p.id !== patchId);
14811
+ }
14812
+ clearPatches() {
14813
+ this.ensureLoaded();
14814
+ this.lock.patches = [];
14815
+ }
14816
+ addForgottenHash(hash) {
14817
+ this.ensureLoaded();
14818
+ if (!this.lock.forgotten_hashes) {
14819
+ this.lock.forgotten_hashes = [];
14820
+ }
14821
+ if (!this.lock.forgotten_hashes.includes(hash)) {
14822
+ this.lock.forgotten_hashes.push(hash);
14823
+ }
14824
+ }
14825
+ getUnresolvedPatches() {
14826
+ this.ensureLoaded();
14827
+ return this.lock.patches.filter((p) => p.status === "unresolved");
14828
+ }
14829
+ getResolvingPatches() {
14830
+ this.ensureLoaded();
14831
+ return this.lock.patches.filter((p) => p.status === "resolving");
14832
+ }
14833
+ markPatchUnresolved(patchId) {
14834
+ this.updatePatch(patchId, { status: "unresolved" });
14835
+ }
14836
+ markPatchResolved(patchId, updates) {
14837
+ this.ensureLoaded();
14838
+ const patch = this.lock.patches.find((p) => p.id === patchId);
14839
+ if (!patch) {
14840
+ throw new Error(`Patch not found: ${patchId}`);
14841
+ }
14842
+ delete patch.status;
14843
+ Object.assign(patch, updates);
14844
+ }
14845
+ getPatches() {
14846
+ this.ensureLoaded();
14847
+ return this.lock.patches;
14848
+ }
14849
+ setReplaySkippedAt(timestamp) {
14850
+ this.ensureLoaded();
14851
+ this.lock.replay_skipped_at = timestamp;
14852
+ }
14853
+ clearReplaySkippedAt() {
14854
+ this.ensureLoaded();
14855
+ delete this.lock.replay_skipped_at;
14856
+ }
14857
+ isReplaySkipped() {
14858
+ this.ensureLoaded();
14859
+ return this.lock.replay_skipped_at != null;
14860
+ }
14861
+ getGeneration(commitSha) {
14862
+ this.ensureLoaded();
14863
+ return this.lock.generations.find((g) => g.commit_sha === commitSha);
14864
+ }
14865
+ getCustomizationsConfig() {
14866
+ if (!(0, import_node_fs.existsSync)(this.customizationsPath)) {
14867
+ return {};
14868
+ }
14869
+ const content = (0, import_node_fs.readFileSync)(this.customizationsPath, "utf-8");
14870
+ return (0, import_yaml.parse)(content) ?? {};
14871
+ }
14872
+ ensureLoaded() {
14873
+ if (!this.lock) {
14874
+ throw new Error("No lockfile loaded. Call read() or initialize() first.");
14875
+ }
14876
+ }
14877
+ };
14878
+
14879
+ // src/ReplayDetector.ts
14880
+ var import_node_crypto = require("crypto");
14881
+
14882
+ // src/shared/binary.ts
14883
+ var import_node_path3 = require("path");
14884
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
14885
+ ".png",
14886
+ ".jpg",
14887
+ ".jpeg",
14888
+ ".gif",
14889
+ ".bmp",
14890
+ ".ico",
14891
+ ".webp",
14892
+ ".svg",
14893
+ ".pdf",
14894
+ ".doc",
14895
+ ".docx",
14896
+ ".xls",
14897
+ ".xlsx",
14898
+ ".ppt",
14899
+ ".pptx",
14900
+ ".zip",
14901
+ ".gz",
14902
+ ".tar",
14903
+ ".bz2",
14904
+ ".7z",
14905
+ ".rar",
14906
+ ".jar",
14907
+ ".war",
14908
+ ".ear",
14909
+ ".class",
14910
+ ".exe",
14911
+ ".dll",
14912
+ ".so",
14913
+ ".dylib",
14914
+ ".o",
14915
+ ".a",
14916
+ ".woff",
14917
+ ".woff2",
14918
+ ".ttf",
14919
+ ".eot",
14920
+ ".otf",
14921
+ ".mp3",
14922
+ ".mp4",
14923
+ ".avi",
14924
+ ".mov",
14925
+ ".wav",
14926
+ ".flac",
14927
+ ".sqlite",
14928
+ ".db",
14929
+ ".pyc",
14930
+ ".pyo",
14931
+ ".DS_Store"
14932
+ ]);
14933
+ function isBinaryFile(filePath) {
14934
+ const ext2 = (0, import_node_path3.extname)(filePath).toLowerCase();
14935
+ return BINARY_EXTENSIONS.has(ext2);
14936
+ }
14937
+
14938
+ // src/git/CommitDetection.ts
14939
+ var FERN_BOT_NAME = "fern-api";
14940
+ var FERN_BOT_EMAIL = "115122769+fern-api[bot]@users.noreply.github.com";
14941
+ var FERN_BOT_LOGIN = "fern-api[bot]";
14942
+ var FERN_SUPPORT_NAMES = ["fern-support", "Fern Support"];
14943
+ function isGenerationCommit(commit) {
14944
+ const isFernSupport = FERN_SUPPORT_NAMES.includes(commit.authorName);
14945
+ const isBotAuthor = !isFernSupport && (commit.authorLogin === FERN_BOT_LOGIN || commit.authorEmail === FERN_BOT_EMAIL || commit.authorName === FERN_BOT_NAME);
14946
+ const hasGenerationMarker = commit.message.startsWith("[fern-generated]") || commit.message.startsWith("[fern-replay]") || commit.message.startsWith("[fern-autoversion]") || commit.message.includes("Generated by Fern") || commit.message.includes("\u{1F916} Generated with Fern") || // Squash merge of a Fern-generated PR uses the PR title as commit message.
14947
+ // The default PR title is "SDK Generation" (from GithubStep's commitMessage default).
14948
+ // GitHub appends "(#N)" for the PR number, e.g. "SDK Generation (#70)".
14949
+ commit.message.startsWith("SDK Generation") || // Merge commit (GitHub's "Create a merge commit" option) of a fern-bot
14950
+ // regen PR. First-parent walking lands on this commit, not on the
14951
+ // generation commit on the side branch, so the classifier must treat
14952
+ // it as a generation boundary. GitHub's default subject is
14953
+ // "Merge pull request #N from <owner>/<branch>"; fern-bot branches are
14954
+ // named "<owner>/fern-bot/<...>".
14955
+ /^Merge pull request #\d+ from [^/]+\/fern-bot\//.test(commit.message);
14956
+ return isBotAuthor || hasGenerationMarker;
14957
+ }
14958
+ function isGenerationBoundary(commit) {
14959
+ const isFernSupport = FERN_SUPPORT_NAMES.includes(commit.authorName);
14960
+ const isBotAuthor = !isFernSupport && (commit.authorLogin === FERN_BOT_LOGIN || commit.authorEmail === FERN_BOT_EMAIL || commit.authorName === FERN_BOT_NAME);
14961
+ const isAutoversion = commit.message.startsWith("[fern-autoversion]");
14962
+ const hasBoundaryMarker = commit.message.startsWith("[fern-generated]") || commit.message.startsWith("[fern-replay]") || commit.message.includes("Generated by Fern") || commit.message.includes("\u{1F916} Generated with Fern") || commit.message.startsWith("SDK Generation");
14963
+ return isBotAuthor && !isAutoversion || hasBoundaryMarker;
14964
+ }
14965
+ function isReplayCommit(commit) {
14966
+ return commit.message.startsWith("[fern-replay]");
14967
+ }
14968
+ function isRevertCommit(message) {
14969
+ return /^Revert ".+"$/.test(message);
14970
+ }
14971
+ function parseRevertedSha(fullBody) {
14972
+ const match2 = fullBody.match(/This reverts commit ([0-9a-f]{40})\./);
14973
+ return match2?.[1];
14974
+ }
14975
+ function parseRevertedMessage(subject) {
14976
+ const match2 = subject.match(/^Revert "(.+)"$/);
14977
+ return match2?.[1];
14978
+ }
14979
+
14980
+ // src/ReplayDetector.ts
14981
+ var INFRASTRUCTURE_FILES = /* @__PURE__ */ new Set([".fernignore"]);
14982
+ function parsePatchFileHeaders(patchContent) {
14983
+ const results = [];
14984
+ let currentPath = null;
14985
+ let currentIsCreate = false;
14986
+ let currentIsDelete = false;
14987
+ const flush = () => {
14988
+ if (currentPath !== null) {
14989
+ results.push({ path: currentPath, isCreate: currentIsCreate, isDelete: currentIsDelete });
14999
14990
  }
15000
- for (const pattern of this.set) {
15001
- for (const part of pattern) {
15002
- if (typeof part !== "string")
15003
- return true;
14991
+ };
14992
+ for (const line of patchContent.split("\n")) {
14993
+ if (line.startsWith("diff --git ")) {
14994
+ flush();
14995
+ currentPath = null;
14996
+ currentIsCreate = false;
14997
+ currentIsDelete = false;
14998
+ const match2 = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
14999
+ if (match2) {
15000
+ currentPath = match2[2] ?? null;
15001
+ }
15002
+ } else if (currentPath !== null) {
15003
+ if (line.startsWith("new file mode ")) {
15004
+ currentIsCreate = true;
15005
+ } else if (line.startsWith("deleted file mode ")) {
15006
+ currentIsDelete = true;
15004
15007
  }
15005
15008
  }
15006
- return false;
15007
15009
  }
15008
- debug(..._) {
15010
+ flush();
15011
+ return results;
15012
+ }
15013
+ async function capturePatchSnapshot(git, files, treeish) {
15014
+ const snapshot = {};
15015
+ for (const file of files) {
15016
+ if (isBinaryFile(file)) continue;
15017
+ const content = await git.showFile(treeish, file).catch(() => null);
15018
+ if (content != null) snapshot[file] = content;
15009
15019
  }
15010
- make() {
15011
- const pattern = this.pattern;
15012
- const options = this.options;
15013
- if (!options.nocomment && pattern.charAt(0) === "#") {
15014
- this.comment = true;
15015
- return;
15016
- }
15017
- if (!pattern) {
15018
- this.empty = true;
15019
- return;
15020
- }
15021
- this.parseNegate();
15022
- this.globSet = [...new Set(this.braceExpand())];
15023
- if (options.debug) {
15024
- this.debug = (...args) => console.error(...args);
15020
+ return snapshot;
15021
+ }
15022
+ function filterInfrastructureAndSensitiveFiles(rawFilesOutput) {
15023
+ const allFiles = rawFilesOutput.trim().split("\n").filter(Boolean).filter((f) => !INFRASTRUCTURE_FILES.has(f)).filter((f) => !f.startsWith(".fern/"));
15024
+ const sensitiveFiles = allFiles.filter((f) => isSensitiveFile(f));
15025
+ const files = allFiles.filter((f) => !isSensitiveFile(f));
15026
+ return { files, sensitiveFiles };
15027
+ }
15028
+ var ReplayDetector = class {
15029
+ git;
15030
+ lockManager;
15031
+ sdkOutputDir;
15032
+ warnings = [];
15033
+ constructor(git, lockManager, sdkOutputDir) {
15034
+ this.git = git;
15035
+ this.lockManager = lockManager;
15036
+ this.sdkOutputDir = sdkOutputDir;
15037
+ }
15038
+ /**
15039
+ * Derive the previous-generation SHA from git history instead of trusting
15040
+ * `lock.current_generation`.
15041
+ *
15042
+ * Walks `git log HEAD --first-parent` and returns the SHA of the most recent
15043
+ * commit that `isGenerationBoundary` classifies as a customization-baseline
15044
+ * boundary, or null if no such commit is reachable from HEAD on the mainline.
15045
+ *
15046
+ * `isGenerationBoundary` is intentionally narrower than `isGenerationCommit`:
15047
+ * it excludes `[fern-autoversion]` (lightweight bumps that don't reset the
15048
+ * baseline) and the GitHub merge-commit subject for fern-bot regen PRs
15049
+ * (which sits on first-parent but where the customer fix-up commits live
15050
+ * on second-parent — stopping at the merge would hide them).
15051
+ *
15052
+ * See docs/adr/0001-derived-scan-boundary.md.
15053
+ */
15054
+ async findPreviousGenerationFromHistory() {
15055
+ let log;
15056
+ try {
15057
+ log = await this.git.exec([
15058
+ "log",
15059
+ "--first-parent",
15060
+ "--format=%H%x00%an%x00%ae%x00%s",
15061
+ "HEAD"
15062
+ ]);
15063
+ } catch {
15064
+ return null;
15025
15065
  }
15026
- this.debug(this.pattern, this.globSet);
15027
- const rawGlobParts = this.globSet.map((s) => this.slashSplit(s));
15028
- this.globParts = this.preprocess(rawGlobParts);
15029
- this.debug(this.pattern, this.globParts);
15030
- let set = this.globParts.map((s, _, __) => {
15031
- if (this.isWindows && this.windowsNoMagicRoot) {
15032
- const isUNC = s[0] === "" && s[1] === "" && (s[2] === "?" || !globMagic.test(s[2])) && !globMagic.test(s[3]);
15033
- const isDrive = /^[a-z]:/i.test(s[0]);
15034
- if (isUNC) {
15035
- return [...s.slice(0, 4), ...s.slice(4).map((ss) => this.parse(ss))];
15036
- } else if (isDrive) {
15037
- return [s[0], ...s.slice(1).map((ss) => this.parse(ss))];
15038
- }
15039
- }
15040
- return s.map((ss) => this.parse(ss));
15041
- });
15042
- this.debug(this.pattern, set);
15043
- this.set = set.filter((s) => s.indexOf(false) === -1);
15044
- if (this.isWindows) {
15045
- for (let i = 0; i < this.set.length; i++) {
15046
- const p = this.set[i];
15047
- if (p[0] === "" && p[1] === "" && this.globParts[i][2] === "?" && typeof p[3] === "string" && /^[a-z]:$/i.test(p[3])) {
15048
- p[2] = "?";
15049
- }
15066
+ for (const commit of this.parseGitLog(log)) {
15067
+ if (isGenerationBoundary(commit)) {
15068
+ return commit.sha;
15050
15069
  }
15051
15070
  }
15052
- this.debug(this.pattern, this.set);
15071
+ return null;
15053
15072
  }
15054
- // various transforms to equivalent pattern sets that are
15055
- // faster to process in a filesystem walk. The goal is to
15056
- // eliminate what we can, and push all ** patterns as far
15057
- // to the right as possible, even if it increases the number
15058
- // of patterns that we have to process.
15059
- preprocess(globParts) {
15060
- if (this.options.noglobstar) {
15061
- for (let i = 0; i < globParts.length; i++) {
15062
- for (let j = 0; j < globParts[i].length; j++) {
15063
- if (globParts[i][j] === "**") {
15064
- globParts[i][j] = "*";
15065
- }
15066
- }
15067
- }
15073
+ async detectNewPatches() {
15074
+ const lock = this.lockManager.read();
15075
+ const derivedSha = await this.findPreviousGenerationFromHistory();
15076
+ if (derivedSha !== null) {
15077
+ const derivedRecord = await this.synthesizeGenerationRecord(derivedSha);
15078
+ return this.detectPatchesInRange(derivedSha, derivedRecord, lock);
15068
15079
  }
15069
- const { optimizationLevel = 1 } = this.options;
15070
- if (optimizationLevel >= 2) {
15071
- globParts = this.firstPhasePreProcess(globParts);
15072
- globParts = this.secondPhasePreProcess(globParts);
15073
- } else if (optimizationLevel >= 1) {
15074
- globParts = this.levelOneOptimize(globParts);
15075
- } else {
15076
- globParts = this.adjascentGlobstarOptimize(globParts);
15080
+ const lastGen = this.getLastGeneration(lock);
15081
+ if (!lastGen) {
15082
+ return { patches: [], revertedPatchIds: [] };
15077
15083
  }
15078
- return globParts;
15079
- }
15080
- // just get rid of adjascent ** portions
15081
- adjascentGlobstarOptimize(globParts) {
15082
- return globParts.map((parts) => {
15083
- let gs = -1;
15084
- while (-1 !== (gs = parts.indexOf("**", gs + 1))) {
15085
- let i = gs;
15086
- while (parts[i + 1] === "**") {
15087
- i++;
15088
- }
15089
- if (i !== gs) {
15090
- parts.splice(gs, i - gs);
15091
- }
15092
- }
15093
- return parts;
15094
- });
15095
- }
15096
- // get rid of adjascent ** and resolve .. portions
15097
- levelOneOptimize(globParts) {
15098
- return globParts.map((parts) => {
15099
- parts = parts.reduce((set, part) => {
15100
- const prev = set[set.length - 1];
15101
- if (part === "**" && prev === "**") {
15102
- return set;
15103
- }
15104
- if (part === "..") {
15105
- if (prev && prev !== ".." && prev !== "." && prev !== "**") {
15106
- set.pop();
15107
- return set;
15108
- }
15109
- }
15110
- set.push(part);
15111
- return set;
15112
- }, []);
15113
- return parts.length === 0 ? [""] : parts;
15114
- });
15084
+ const exists2 = await this.git.commitExists(lastGen.commit_sha);
15085
+ if (!exists2) {
15086
+ this.warnings.push(
15087
+ `Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to tree-diff detection (likely a shallow clone).`
15088
+ );
15089
+ return this.detectPatchesViaTreeDiff(
15090
+ lastGen,
15091
+ /* commitKnownMissing */
15092
+ true
15093
+ );
15094
+ }
15095
+ return this.detectPatchesInRange(lastGen.commit_sha, lastGen, lock);
15115
15096
  }
15116
- levelTwoFileOptimize(parts) {
15117
- if (!Array.isArray(parts)) {
15118
- parts = this.slashSplit(parts);
15097
+ /**
15098
+ * Walk `${rangeStart}..HEAD`, classify each commit, emit one
15099
+ * `StoredPatch` per customer commit. Body shared by the linear path
15100
+ * (rangeStart = lastGen.commit_sha) and the merge-base path.
15101
+ *
15102
+ * Patch `base_generation` is always `lastGen.commit_sha` — the
15103
+ * lockfile-tracked reference the applicator uses to fetch base file
15104
+ * content, regardless of where the walk actually started.
15105
+ */
15106
+ async detectPatchesInRange(rangeStart, lastGen, lock) {
15107
+ const log = await this.git.exec([
15108
+ "log",
15109
+ "--no-merges",
15110
+ "--format=%H%x00%an%x00%ae%x00%s",
15111
+ `${rangeStart}..HEAD`,
15112
+ "--",
15113
+ this.sdkOutputDir
15114
+ ]);
15115
+ if (!log.trim()) {
15116
+ return { patches: [], revertedPatchIds: [] };
15119
15117
  }
15120
- let didSomething = false;
15121
- do {
15122
- didSomething = false;
15123
- if (!this.preserveMultipleSlashes) {
15124
- for (let i = 1; i < parts.length - 1; i++) {
15125
- const p = parts[i];
15126
- if (i === 1 && p === "" && parts[0] === "")
15127
- continue;
15128
- if (p === "." || p === "") {
15129
- didSomething = true;
15130
- parts.splice(i, 1);
15131
- i--;
15118
+ const commits = this.parseGitLog(log);
15119
+ const newPatches = [];
15120
+ const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
15121
+ for (const commit of commits) {
15122
+ if (isGenerationCommit(commit)) {
15123
+ continue;
15124
+ }
15125
+ if (lock.patches.find((p) => p.original_commit === commit.sha)) {
15126
+ continue;
15127
+ }
15128
+ const parents = await this.git.getCommitParents(commit.sha);
15129
+ let patchContent;
15130
+ try {
15131
+ patchContent = await this.git.formatPatch(commit.sha);
15132
+ } catch {
15133
+ this.warnings.push(
15134
+ `Could not generate patch for commit ${commit.sha.slice(0, 7)} \u2014 it may be unreachable in a shallow clone. Skipping.`
15135
+ );
15136
+ continue;
15137
+ }
15138
+ let contentHash = this.computeContentHash(patchContent);
15139
+ if (lock.patches.find((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
15140
+ continue;
15141
+ }
15142
+ const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
15143
+ const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
15144
+ if (sensitiveFiles.length > 0) {
15145
+ this.warnings.push(
15146
+ `Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
15147
+ );
15148
+ if (files.length > 0) {
15149
+ const parentSha = parents[0];
15150
+ if (parentSha) {
15151
+ try {
15152
+ patchContent = await this.git.exec(["diff", parentSha, commit.sha, "--", ...files]);
15153
+ } catch {
15154
+ continue;
15155
+ }
15156
+ if (!patchContent.trim()) continue;
15157
+ contentHash = this.computeContentHash(patchContent);
15132
15158
  }
15133
15159
  }
15134
- if (parts[0] === "." && parts.length === 2 && (parts[1] === "." || parts[1] === "")) {
15135
- didSomething = true;
15136
- parts.pop();
15137
- }
15138
15160
  }
15139
- let dd = 0;
15140
- while (-1 !== (dd = parts.indexOf("..", dd + 1))) {
15141
- const p = parts[dd - 1];
15142
- if (p && p !== "." && p !== ".." && p !== "**") {
15143
- didSomething = true;
15144
- parts.splice(dd - 1, 2);
15145
- dd -= 2;
15161
+ if (files.length === 0) {
15162
+ continue;
15163
+ }
15164
+ const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
15165
+ newPatches.push({
15166
+ id: `patch-${commit.sha.slice(0, 8)}`,
15167
+ content_hash: contentHash,
15168
+ original_commit: commit.sha,
15169
+ original_message: commit.message,
15170
+ original_author: `${commit.authorName} <${commit.authorEmail}>`,
15171
+ base_generation: lastGen.commit_sha,
15172
+ files,
15173
+ patch_content: patchContent,
15174
+ ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
15175
+ ...this.isCreationOnlyPatch(patchContent) ? { user_owned: true } : {}
15176
+ });
15177
+ }
15178
+ const survivingPatches = await this.collapseNetZeroFiles(newPatches);
15179
+ survivingPatches.reverse();
15180
+ const revertedPatchIdSet = /* @__PURE__ */ new Set();
15181
+ const revertIndicesToRemove = /* @__PURE__ */ new Set();
15182
+ for (let i = 0; i < survivingPatches.length; i++) {
15183
+ const patch = survivingPatches[i];
15184
+ if (!isRevertCommit(patch.original_message)) continue;
15185
+ let body = "";
15186
+ try {
15187
+ body = await this.git.getCommitBody(patch.original_commit);
15188
+ } catch {
15189
+ }
15190
+ const revertedSha = parseRevertedSha(body);
15191
+ const revertedMessage = parseRevertedMessage(patch.original_message);
15192
+ let matchedExisting = false;
15193
+ if (revertedSha) {
15194
+ const existing = lock.patches.find((p) => p.original_commit === revertedSha);
15195
+ if (existing) {
15196
+ revertedPatchIdSet.add(existing.id);
15197
+ revertIndicesToRemove.add(i);
15198
+ matchedExisting = true;
15146
15199
  }
15147
15200
  }
15148
- } while (didSomething);
15149
- return parts.length === 0 ? [""] : parts;
15150
- }
15151
- // First phase: single-pattern processing
15152
- // <pre> is 1 or more portions
15153
- // <rest> is 1 or more portions
15154
- // <p> is any portion other than ., .., '', or **
15155
- // <e> is . or ''
15156
- //
15157
- // **/.. is *brutal* for filesystem walking performance, because
15158
- // it effectively resets the recursive walk each time it occurs,
15159
- // and ** cannot be reduced out by a .. pattern part like a regexp
15160
- // or most strings (other than .., ., and '') can be.
15161
- //
15162
- // <pre>/**/../<p>/<p>/<rest> -> {<pre>/../<p>/<p>/<rest>,<pre>/**/<p>/<p>/<rest>}
15163
- // <pre>/<e>/<rest> -> <pre>/<rest>
15164
- // <pre>/<p>/../<rest> -> <pre>/<rest>
15165
- // **/**/<rest> -> **/<rest>
15166
- //
15167
- // **/*/<rest> -> */**/<rest> <== not valid because ** doesn't follow
15168
- // this WOULD be allowed if ** did follow symlinks, or * didn't
15169
- firstPhasePreProcess(globParts) {
15170
- let didSomething = false;
15171
- do {
15172
- didSomething = false;
15173
- for (let parts of globParts) {
15174
- let gs = -1;
15175
- while (-1 !== (gs = parts.indexOf("**", gs + 1))) {
15176
- let gss = gs;
15177
- while (parts[gss + 1] === "**") {
15178
- gss++;
15179
- }
15180
- if (gss > gs) {
15181
- parts.splice(gs + 1, gss - gs);
15182
- }
15183
- let next = parts[gs + 1];
15184
- const p = parts[gs + 2];
15185
- const p2 = parts[gs + 3];
15186
- if (next !== "..")
15187
- continue;
15188
- if (!p || p === "." || p === ".." || !p2 || p2 === "." || p2 === "..") {
15189
- continue;
15190
- }
15191
- didSomething = true;
15192
- parts.splice(gs, 1);
15193
- const other = parts.slice(0);
15194
- other[gs] = "**";
15195
- globParts.push(other);
15196
- gs--;
15201
+ if (!matchedExisting && revertedMessage) {
15202
+ const existing = lock.patches.find((p) => p.original_message === revertedMessage);
15203
+ if (existing) {
15204
+ revertedPatchIdSet.add(existing.id);
15205
+ revertIndicesToRemove.add(i);
15206
+ matchedExisting = true;
15197
15207
  }
15198
- if (!this.preserveMultipleSlashes) {
15199
- for (let i = 1; i < parts.length - 1; i++) {
15200
- const p = parts[i];
15201
- if (i === 1 && p === "" && parts[0] === "")
15202
- continue;
15203
- if (p === "." || p === "") {
15204
- didSomething = true;
15205
- parts.splice(i, 1);
15206
- i--;
15207
- }
15208
- }
15209
- if (parts[0] === "." && parts.length === 2 && (parts[1] === "." || parts[1] === "")) {
15210
- didSomething = true;
15211
- parts.pop();
15212
- }
15208
+ }
15209
+ if (matchedExisting) continue;
15210
+ let matchedNew = false;
15211
+ if (revertedSha) {
15212
+ const idx = survivingPatches.findIndex(
15213
+ (p, j) => j !== i && !revertIndicesToRemove.has(j) && p.original_commit === revertedSha
15214
+ );
15215
+ if (idx !== -1) {
15216
+ revertIndicesToRemove.add(i);
15217
+ revertIndicesToRemove.add(idx);
15218
+ matchedNew = true;
15213
15219
  }
15214
- let dd = 0;
15215
- while (-1 !== (dd = parts.indexOf("..", dd + 1))) {
15216
- const p = parts[dd - 1];
15217
- if (p && p !== "." && p !== ".." && p !== "**") {
15218
- didSomething = true;
15219
- const needDot = dd === 1 && parts[dd + 1] === "**";
15220
- const splin = needDot ? ["."] : [];
15221
- parts.splice(dd - 1, 2, ...splin);
15222
- if (parts.length === 0)
15223
- parts.push("");
15224
- dd -= 2;
15225
- }
15220
+ }
15221
+ if (!matchedNew && revertedMessage) {
15222
+ const idx = survivingPatches.findIndex(
15223
+ (p, j) => j !== i && !revertIndicesToRemove.has(j) && p.original_message === revertedMessage
15224
+ );
15225
+ if (idx !== -1) {
15226
+ revertIndicesToRemove.add(i);
15227
+ revertIndicesToRemove.add(idx);
15226
15228
  }
15227
15229
  }
15228
- } while (didSomething);
15229
- return globParts;
15230
+ if (!matchedExisting && !matchedNew) {
15231
+ revertIndicesToRemove.add(i);
15232
+ }
15233
+ }
15234
+ const filteredPatches = survivingPatches.filter((_, i) => !revertIndicesToRemove.has(i));
15235
+ return { patches: filteredPatches, revertedPatchIds: [...revertedPatchIdSet] };
15230
15236
  }
15231
- // second phase: multi-pattern dedupes
15232
- // {<pre>/*/<rest>,<pre>/<p>/<rest>} -> <pre>/*/<rest>
15233
- // {<pre>/<rest>,<pre>/<rest>} -> <pre>/<rest>
15234
- // {<pre>/**/<rest>,<pre>/<rest>} -> <pre>/**/<rest>
15235
- //
15236
- // {<pre>/**/<rest>,<pre>/**/<p>/<rest>} -> <pre>/**/<rest>
15237
- // ^-- not valid because ** doens't follow symlinks
15238
- secondPhasePreProcess(globParts) {
15239
- for (let i = 0; i < globParts.length - 1; i++) {
15240
- for (let j = i + 1; j < globParts.length; j++) {
15241
- const matched = this.partsMatch(globParts[i], globParts[j], !this.preserveMultipleSlashes);
15242
- if (matched) {
15243
- globParts[i] = [];
15244
- globParts[j] = matched;
15245
- break;
15246
- }
15237
+ /**
15238
+ * Compute content hash for deduplication.
15239
+ *
15240
+ * Produces a format-agnostic hash so both `git format-patch` output
15241
+ * (email-wrapped) and plain `git diff` output hash to the same value
15242
+ * when the underlying diff hunks are identical.
15243
+ *
15244
+ * Normalization:
15245
+ * 1. Strip email wrapper: everything before the first `diff --git` line
15246
+ * (From:, Subject:, Date:, diffstat, blank separators).
15247
+ * 2. Strip `index` lines (blob SHA pairs change across rebases).
15248
+ * 3. Strip trailing git version marker (`-- \n<version>`).
15249
+ *
15250
+ * This ensures content hashes match across the no-patches and normal-
15251
+ * regeneration flows, which store formatPatch and git-diff formats
15252
+ * respectively.
15253
+ */
15254
+ computeContentHash(patchContent) {
15255
+ const lines = patchContent.split("\n");
15256
+ const diffStart = lines.findIndex((l) => l.startsWith("diff --git "));
15257
+ const relevantLines = diffStart > 0 ? lines.slice(diffStart) : lines;
15258
+ const normalized = relevantLines.filter((line) => !line.startsWith("index ")).join("\n").replace(/\n-- \n[\d]+\.[\d]+[^\n]*\s*$/, "");
15259
+ return `sha256:${(0, import_node_crypto.createHash)("sha256").update(normalized).digest("hex")}`;
15260
+ }
15261
+ /**
15262
+ * Drop patches whose files were transiently created and then deleted
15263
+ * within the current linear detection window, and are absent from
15264
+ * HEAD at the end of the window.
15265
+ *
15266
+ * Rationale: each commit becomes its own patch, so a file that was
15267
+ * briefly added and later removed survives as a create+delete pair whose
15268
+ * cumulative diff is empty. We drop such patches before they reach the
15269
+ * lockfile.
15270
+ *
15271
+ * A file is "transient" when:
15272
+ * 1. some patch in the window sets `new file mode` for it (a creation), AND
15273
+ * 2. some patch in the window sets `deleted file mode` for it (a deletion), AND
15274
+ * 3. it is absent from HEAD's tree.
15275
+ *
15276
+ * The HEAD guard (#3) prevents false positives when a file is deleted
15277
+ * and then recreated with different content — the cumulative diff of
15278
+ * such a pair is non-empty and must be preserved.
15279
+ *
15280
+ * This only drops patches whose `files` are a subset of the transient
15281
+ * set. A partial-transient patch (one that touches a transient file
15282
+ * alongside a persistent one) is left unmodified; surgical stripping of
15283
+ * single files from a multi-file patch would require a follow-up that
15284
+ * regenerates the patch content via `git format-patch -- <files>`. No
15285
+ * current failing test exercises this, and leaving the patch intact
15286
+ * preserves today's behaviour. Revisit if a future adversarial test
15287
+ * demands per-file stripping.
15288
+ */
15289
+ async collapseNetZeroFiles(patches) {
15290
+ if (patches.length === 0) return patches;
15291
+ const stateByFile = /* @__PURE__ */ new Map();
15292
+ for (const patch of patches) {
15293
+ for (const header of parsePatchFileHeaders(patch.patch_content)) {
15294
+ if (!header.isCreate && !header.isDelete) continue;
15295
+ const prior = stateByFile.get(header.path) ?? { created: false, deleted: false };
15296
+ if (header.isCreate) prior.created = true;
15297
+ if (header.isDelete) prior.deleted = true;
15298
+ stateByFile.set(header.path, prior);
15247
15299
  }
15248
15300
  }
15249
- return globParts.filter((gs) => gs.length);
15250
- }
15251
- partsMatch(a, b, emptyGSMatch = false) {
15252
- let ai = 0;
15253
- let bi = 0;
15254
- let result = [];
15255
- let which = "";
15256
- while (ai < a.length && bi < b.length) {
15257
- if (a[ai] === b[bi]) {
15258
- result.push(which === "b" ? b[bi] : a[ai]);
15259
- ai++;
15260
- bi++;
15261
- } else if (emptyGSMatch && a[ai] === "**" && b[bi] === a[ai + 1]) {
15262
- result.push(a[ai]);
15263
- ai++;
15264
- } else if (emptyGSMatch && b[bi] === "**" && a[ai] === b[bi + 1]) {
15265
- result.push(b[bi]);
15266
- bi++;
15267
- } else if (a[ai] === "*" && b[bi] && (this.options.dot || !b[bi].startsWith(".")) && b[bi] !== "**") {
15268
- if (which === "b")
15269
- return false;
15270
- which = "a";
15271
- result.push(a[ai]);
15272
- ai++;
15273
- bi++;
15274
- } else if (b[bi] === "*" && a[ai] && (this.options.dot || !a[ai].startsWith(".")) && a[ai] !== "**") {
15275
- if (which === "a")
15276
- return false;
15277
- which = "b";
15278
- result.push(b[bi]);
15279
- ai++;
15280
- bi++;
15281
- } else {
15282
- return false;
15301
+ let anyCandidate = false;
15302
+ for (const state of stateByFile.values()) {
15303
+ if (state.created && state.deleted) {
15304
+ anyCandidate = true;
15305
+ break;
15283
15306
  }
15284
15307
  }
15285
- return a.length === b.length && result;
15308
+ if (!anyCandidate) return patches;
15309
+ const headFiles = await this.listHeadFiles();
15310
+ const transient = /* @__PURE__ */ new Set();
15311
+ for (const [file, state] of stateByFile) {
15312
+ if (state.created && state.deleted && !headFiles.has(file)) {
15313
+ transient.add(file);
15314
+ }
15315
+ }
15316
+ if (transient.size === 0) return patches;
15317
+ return patches.filter((patch) => !patch.files.every((f) => transient.has(f)));
15286
15318
  }
15287
- parseNegate() {
15288
- if (this.nonegate)
15289
- return;
15290
- const pattern = this.pattern;
15291
- let negate = false;
15292
- let negateOffset = 0;
15293
- for (let i = 0; i < pattern.length && pattern.charAt(i) === "!"; i++) {
15294
- negate = !negate;
15295
- negateOffset++;
15319
+ /**
15320
+ * List every tracked path at HEAD (one `git ls-tree -r` invocation).
15321
+ * Returns an empty Set if HEAD is unborn or the invocation fails — the
15322
+ * caller then treats every candidate as "not in HEAD" and may collapse
15323
+ * more aggressively. On typical SDK repos this is a single sub-10ms call.
15324
+ */
15325
+ async listHeadFiles() {
15326
+ try {
15327
+ const out = await this.git.exec(["ls-tree", "-r", "--name-only", "HEAD"]);
15328
+ return new Set(out.split("\n").map((l) => l.trim()).filter(Boolean));
15329
+ } catch {
15330
+ return /* @__PURE__ */ new Set();
15296
15331
  }
15297
- if (negateOffset)
15298
- this.pattern = pattern.slice(negateOffset);
15299
- this.negate = negate;
15300
15332
  }
15301
- // set partial to true to test if, for example,
15302
- // "/a/b" matches the start of "/*/b/*/d"
15303
- // Partial means, if you run out of file before you run
15304
- // out of pattern, then that's fine, as long as all
15305
- // the parts match.
15306
- matchOne(file, pattern, partial = false) {
15307
- const options = this.options;
15308
- if (this.isWindows) {
15309
- const fileDrive = typeof file[0] === "string" && /^[a-z]:$/i.test(file[0]);
15310
- const fileUNC = !fileDrive && file[0] === "" && file[1] === "" && file[2] === "?" && /^[a-z]:$/i.test(file[3]);
15311
- const patternDrive = typeof pattern[0] === "string" && /^[a-z]:$/i.test(pattern[0]);
15312
- const patternUNC = !patternDrive && pattern[0] === "" && pattern[1] === "" && pattern[2] === "?" && typeof pattern[3] === "string" && /^[a-z]:$/i.test(pattern[3]);
15313
- const fdi = fileUNC ? 3 : fileDrive ? 0 : void 0;
15314
- const pdi = patternUNC ? 3 : patternDrive ? 0 : void 0;
15315
- if (typeof fdi === "number" && typeof pdi === "number") {
15316
- const [fd, pd] = [file[fdi], pattern[pdi]];
15317
- if (fd.toLowerCase() === pd.toLowerCase()) {
15318
- pattern[pdi] = fd;
15319
- if (pdi > fdi) {
15320
- pattern = pattern.slice(pdi);
15321
- } else if (fdi > pdi) {
15322
- file = file.slice(fdi);
15323
- }
15324
- }
15333
+ /**
15334
+ * Detect patches via tree diff for non-linear history. Returns a composite patch.
15335
+ * Revert reconciliation is skipped here because tree-diff produces a single composite
15336
+ * patch from the aggregate diff individual revert commits are not distinguishable.
15337
+ */
15338
+ async detectPatchesViaTreeDiff(lastGen, commitKnownMissing) {
15339
+ const diffBase = await this.resolveDiffBase(lastGen, commitKnownMissing);
15340
+ if (!diffBase) {
15341
+ return this.detectPatchesViaCommitScan();
15342
+ }
15343
+ const lockForGuard = this.lockManager.read();
15344
+ const knownGenerations = new Set(lockForGuard.generations.map((g) => g.commit_sha));
15345
+ let resolvedBaseGeneration = commitKnownMissing ? diffBase : lastGen.commit_sha;
15346
+ if (commitKnownMissing && !knownGenerations.has(diffBase)) {
15347
+ const fallback = lockForGuard.current_generation ?? lockForGuard.generations[lockForGuard.generations.length - 1]?.commit_sha;
15348
+ if (!fallback) {
15349
+ this.warnings.push(
15350
+ `Composite-patch fallback skipped: diff base ${diffBase.slice(0, 7)} is not a recorded generation and the lockfile has no recorded generations to anchor on. Customer customizations on disk are preserved; tracked patches will resume on the next regeneration.`
15351
+ );
15352
+ return { patches: [], revertedPatchIds: [] };
15325
15353
  }
15354
+ resolvedBaseGeneration = fallback;
15326
15355
  }
15327
- const { optimizationLevel = 1 } = this.options;
15328
- if (optimizationLevel >= 2) {
15329
- file = this.levelTwoFileOptimize(file);
15356
+ const filesOutput = await this.git.exec(["diff", "--name-only", diffBase, "HEAD"]);
15357
+ const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
15358
+ if (sensitiveFiles.length > 0) {
15359
+ this.warnings.push(
15360
+ `Sensitive file(s) excluded from composite patch: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
15361
+ );
15330
15362
  }
15331
- this.debug("matchOne", this, { file, pattern });
15332
- this.debug("matchOne", file.length, pattern.length);
15333
- for (var fi = 0, pi = 0, fl = file.length, pl = pattern.length; fi < fl && pi < pl; fi++, pi++) {
15334
- this.debug("matchOne loop");
15335
- var p = pattern[pi];
15336
- var f = file[fi];
15337
- this.debug(pattern, p, f);
15338
- if (p === false) {
15339
- return false;
15363
+ if (files.length === 0) return { patches: [], revertedPatchIds: [] };
15364
+ const diff = await this.git.exec(["diff", diffBase, "HEAD", "--", ...files]);
15365
+ if (!diff.trim()) return { patches: [], revertedPatchIds: [] };
15366
+ const contentHash = this.computeContentHash(diff);
15367
+ const lock = this.lockManager.read();
15368
+ if (lock.patches.some((p) => p.content_hash === contentHash) || (lock.forgotten_hashes ?? []).includes(contentHash)) {
15369
+ return { patches: [], revertedPatchIds: [] };
15370
+ }
15371
+ const headSha = (await this.git.exec(["rev-parse", "HEAD"])).trim();
15372
+ const theirsSnapshot = await capturePatchSnapshot(this.git, files, "HEAD");
15373
+ const compositePatch = {
15374
+ id: `patch-composite-${headSha.slice(0, 8)}`,
15375
+ content_hash: contentHash,
15376
+ original_commit: headSha,
15377
+ original_message: "Customer customizations (composite)",
15378
+ original_author: "composite",
15379
+ // Anchor `base_generation` on a SHA the applicator's
15380
+ // `resolveBaseGeneration` can find — always a member of
15381
+ // `lock.generations`. When `diffBase` is a recorded
15382
+ // generation, use it directly; otherwise the guard above
15383
+ // has already mapped it onto `current_generation`.
15384
+ base_generation: resolvedBaseGeneration,
15385
+ files,
15386
+ patch_content: diff,
15387
+ ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {},
15388
+ ...this.isCreationOnlyPatch(diff) ? { user_owned: true } : {}
15389
+ };
15390
+ return { patches: [compositePatch], revertedPatchIds: [] };
15391
+ }
15392
+ /**
15393
+ * Last-resort detection when both generation commit and tree are unreachable.
15394
+ * Scans all commits from HEAD, filters against known lockfile patches, and
15395
+ * skips creation-only commits (squashed history after force push).
15396
+ *
15397
+ * Detected patches use the commit's parent as base_generation so the applicator
15398
+ * can find base file content from the parent's tree (which IS reachable).
15399
+ */
15400
+ async detectPatchesViaCommitScan() {
15401
+ const lock = this.lockManager.read();
15402
+ const knownGenerations = new Set(lock.generations.map((g) => g.commit_sha));
15403
+ const fallbackAnchor = lock.current_generation ?? lock.generations[lock.generations.length - 1]?.commit_sha;
15404
+ const log = await this.git.exec([
15405
+ "log",
15406
+ "--max-count=200",
15407
+ "--format=%H%x00%an%x00%ae%x00%s",
15408
+ "HEAD",
15409
+ "--",
15410
+ this.sdkOutputDir
15411
+ ]);
15412
+ if (!log.trim()) {
15413
+ return { patches: [], revertedPatchIds: [] };
15414
+ }
15415
+ const commits = this.parseGitLog(log);
15416
+ const newPatches = [];
15417
+ const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
15418
+ const existingHashes = new Set(lock.patches.map((p) => p.content_hash));
15419
+ const existingCommits = new Set(lock.patches.map((p) => p.original_commit));
15420
+ for (const commit of commits) {
15421
+ if (isGenerationCommit(commit)) {
15422
+ continue;
15340
15423
  }
15341
- if (p === GLOBSTAR) {
15342
- this.debug("GLOBSTAR", [pattern, p, f]);
15343
- var fr = fi;
15344
- var pr = pi + 1;
15345
- if (pr === pl) {
15346
- this.debug("** at the end");
15347
- for (; fi < fl; fi++) {
15348
- if (file[fi] === "." || file[fi] === ".." || !options.dot && file[fi].charAt(0) === ".")
15349
- return false;
15350
- }
15351
- return true;
15352
- }
15353
- while (fr < fl) {
15354
- var swallowee = file[fr];
15355
- this.debug("\nglobstar while", file, fr, pattern, pr, swallowee);
15356
- if (this.matchOne(file.slice(fr), pattern.slice(pr), partial)) {
15357
- this.debug("globstar found match!", fr, fl, swallowee);
15358
- return true;
15359
- } else {
15360
- if (swallowee === "." || swallowee === ".." || !options.dot && swallowee.charAt(0) === ".") {
15361
- this.debug("dot detected!", file, fr, pattern, pr);
15362
- break;
15363
- }
15364
- this.debug("globstar swallow a segment, and continue");
15365
- fr++;
15366
- }
15367
- }
15368
- if (partial) {
15369
- this.debug("\n>>> no match, partial?", file, fr, pattern, pr);
15370
- if (fr === fl) {
15371
- return true;
15424
+ const parents = await this.git.getCommitParents(commit.sha);
15425
+ if (parents.length > 1) {
15426
+ continue;
15427
+ }
15428
+ if (existingCommits.has(commit.sha)) {
15429
+ continue;
15430
+ }
15431
+ let patchContent;
15432
+ try {
15433
+ patchContent = await this.git.formatPatch(commit.sha);
15434
+ } catch {
15435
+ continue;
15436
+ }
15437
+ let contentHash = this.computeContentHash(patchContent);
15438
+ if (existingHashes.has(contentHash) || forgottenHashes.has(contentHash)) {
15439
+ continue;
15440
+ }
15441
+ if (this.isCreationOnlyPatch(patchContent)) {
15442
+ continue;
15443
+ }
15444
+ const filesOutput = await this.git.exec(["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha]);
15445
+ const { files, sensitiveFiles } = filterInfrastructureAndSensitiveFiles(filesOutput);
15446
+ if (sensitiveFiles.length > 0) {
15447
+ this.warnings.push(
15448
+ `Sensitive file(s) excluded from patch for commit ${commit.sha.slice(0, 7)}: ${sensitiveFiles.join(", ")}. These files will not be tracked by replay.`
15449
+ );
15450
+ if (files.length > 0) {
15451
+ if (parents.length === 0) {
15452
+ continue;
15453
+ }
15454
+ try {
15455
+ patchContent = await this.git.exec(["diff", parents[0], commit.sha, "--", ...files]);
15456
+ } catch {
15457
+ continue;
15372
15458
  }
15459
+ if (!patchContent.trim()) continue;
15460
+ contentHash = this.computeContentHash(patchContent);
15373
15461
  }
15374
- return false;
15375
15462
  }
15376
- let hit;
15377
- if (typeof p === "string") {
15378
- hit = f === p;
15379
- this.debug("string match", p, f, hit);
15463
+ if (files.length === 0) {
15464
+ continue;
15465
+ }
15466
+ if (parents.length === 0) {
15467
+ continue;
15468
+ }
15469
+ const parentSha = parents[0];
15470
+ let baseGeneration;
15471
+ if (knownGenerations.has(parentSha)) {
15472
+ baseGeneration = parentSha;
15473
+ } else if (fallbackAnchor) {
15474
+ baseGeneration = fallbackAnchor;
15380
15475
  } else {
15381
- hit = p.test(f);
15382
- this.debug("pattern match", p, f, hit);
15476
+ this.warnings.push(
15477
+ `Skipping commit ${commit.sha.slice(0, 7)} (${commit.message.split("\n")[0]}): no recorded generation in the lockfile to anchor on. The customer's state on disk is preserved; tracked patches will resume on the next regeneration.`
15478
+ );
15479
+ continue;
15383
15480
  }
15384
- if (!hit)
15385
- return false;
15386
- }
15387
- if (fi === fl && pi === pl) {
15388
- return true;
15389
- } else if (fi === fl) {
15390
- return partial;
15391
- } else if (pi === pl) {
15392
- return fi === fl - 1 && file[fi] === "";
15393
- } else {
15394
- throw new Error("wtf?");
15481
+ const theirsSnapshot = await capturePatchSnapshot(this.git, files, commit.sha);
15482
+ newPatches.push({
15483
+ id: `patch-${commit.sha.slice(0, 8)}`,
15484
+ content_hash: contentHash,
15485
+ original_commit: commit.sha,
15486
+ original_message: commit.message,
15487
+ original_author: `${commit.authorName} <${commit.authorEmail}>`,
15488
+ base_generation: baseGeneration,
15489
+ files,
15490
+ patch_content: patchContent,
15491
+ ...Object.keys(theirsSnapshot).length > 0 ? { theirs_snapshot: theirsSnapshot } : {}
15492
+ });
15395
15493
  }
15494
+ newPatches.reverse();
15495
+ return { patches: newPatches, revertedPatchIds: [] };
15396
15496
  }
15397
- braceExpand() {
15398
- return braceExpand(this.pattern, this.options);
15399
- }
15400
- parse(pattern) {
15401
- assertValidPattern(pattern);
15402
- const options = this.options;
15403
- if (pattern === "**")
15404
- return GLOBSTAR;
15405
- if (pattern === "")
15406
- return "";
15407
- let m;
15408
- let fastTest = null;
15409
- if (m = pattern.match(starRE)) {
15410
- fastTest = options.dot ? starTestDot : starTest;
15411
- } else if (m = pattern.match(starDotExtRE)) {
15412
- fastTest = (options.nocase ? options.dot ? starDotExtTestNocaseDot : starDotExtTestNocase : options.dot ? starDotExtTestDot : starDotExtTest)(m[1]);
15413
- } else if (m = pattern.match(qmarksRE)) {
15414
- fastTest = (options.nocase ? options.dot ? qmarksTestNocaseDot : qmarksTestNocase : options.dot ? qmarksTestDot : qmarksTest)(m);
15415
- } else if (m = pattern.match(starDotStarRE)) {
15416
- fastTest = options.dot ? starDotStarTestDot : starDotStarTest;
15417
- } else if (m = pattern.match(dotStarRE)) {
15418
- fastTest = dotStarTest;
15419
- }
15420
- const re = AST.fromGlob(pattern, this.options).toMMPattern();
15421
- if (fastTest && typeof re === "object") {
15422
- Reflect.defineProperty(re, "test", { value: fastTest });
15497
+ /**
15498
+ * Check if a format-patch consists entirely of new-file creations.
15499
+ * Used to identify squashed commits after force push, which create all files
15500
+ * from scratch (--- /dev/null) rather than modifying existing files.
15501
+ */
15502
+ isCreationOnlyPatch(patchContent) {
15503
+ const diffOldHeaders = patchContent.split("\n").filter((l) => l.startsWith("--- "));
15504
+ if (diffOldHeaders.length === 0) {
15505
+ return false;
15423
15506
  }
15424
- return re;
15507
+ return diffOldHeaders.every((l) => l === "--- /dev/null");
15425
15508
  }
15426
- makeRe() {
15427
- if (this.regexp || this.regexp === false)
15428
- return this.regexp;
15429
- const set = this.set;
15430
- if (!set.length) {
15431
- this.regexp = false;
15432
- return this.regexp;
15509
+ /**
15510
+ * Resolve the best available diff base for a generation record.
15511
+ * Prefers commit_sha, falls back to tree_hash for unreachable commits.
15512
+ * When commitKnownMissing is true, skips the redundant commitExists check.
15513
+ */
15514
+ async resolveDiffBase(gen, commitKnownMissing) {
15515
+ if (!commitKnownMissing && await this.git.commitExists(gen.commit_sha)) {
15516
+ return gen.commit_sha;
15433
15517
  }
15434
- const options = this.options;
15435
- const twoStar = options.noglobstar ? star2 : options.dot ? twoStarDot : twoStarNoDot;
15436
- const flags = new Set(options.nocase ? ["i"] : []);
15437
- let re = set.map((pattern) => {
15438
- const pp = pattern.map((p) => {
15439
- if (p instanceof RegExp) {
15440
- for (const f of p.flags.split(""))
15441
- flags.add(f);
15442
- }
15443
- return typeof p === "string" ? regExpEscape2(p) : p === GLOBSTAR ? GLOBSTAR : p._src;
15444
- });
15445
- pp.forEach((p, i) => {
15446
- const next = pp[i + 1];
15447
- const prev = pp[i - 1];
15448
- if (p !== GLOBSTAR || prev === GLOBSTAR) {
15449
- return;
15450
- }
15451
- if (prev === void 0) {
15452
- if (next !== void 0 && next !== GLOBSTAR) {
15453
- pp[i + 1] = "(?:\\/|" + twoStar + "\\/)?" + next;
15454
- } else {
15455
- pp[i] = twoStar;
15456
- }
15457
- } else if (next === void 0) {
15458
- pp[i - 1] = prev + "(?:\\/|" + twoStar + ")?";
15459
- } else if (next !== GLOBSTAR) {
15460
- pp[i - 1] = prev + "(?:\\/|\\/" + twoStar + "\\/)" + next;
15461
- pp[i + 1] = GLOBSTAR;
15462
- }
15463
- });
15464
- return pp.filter((p) => p !== GLOBSTAR).join("/");
15465
- }).join("|");
15466
- const [open, close] = set.length > 1 ? ["(?:", ")"] : ["", ""];
15467
- re = "^" + open + re + close + "$";
15468
- if (this.negate)
15469
- re = "^(?!" + re + ").+$";
15470
- try {
15471
- this.regexp = new RegExp(re, [...flags].join(""));
15472
- } catch (ex) {
15473
- this.regexp = false;
15518
+ if (await this.git.treeExists(gen.tree_hash)) {
15519
+ return gen.tree_hash;
15474
15520
  }
15475
- return this.regexp;
15521
+ return null;
15476
15522
  }
15477
- slashSplit(p) {
15478
- if (this.preserveMultipleSlashes) {
15479
- return p.split("/");
15480
- } else if (this.isWindows && /^\/\/[^\/]+/.test(p)) {
15481
- return ["", ...p.split(/\/+/)];
15482
- } else {
15483
- return p.split(/\/+/);
15484
- }
15523
+ parseGitLog(log) {
15524
+ return log.trim().split("\n").map((line) => {
15525
+ const [sha, authorName, authorEmail, message] = line.split("\0");
15526
+ return { sha, authorName, authorEmail, message };
15527
+ });
15485
15528
  }
15486
- match(f, partial = this.partial) {
15487
- this.debug("match", f, this.pattern);
15488
- if (this.comment) {
15489
- return false;
15490
- }
15491
- if (this.empty) {
15492
- return f === "";
15493
- }
15494
- if (f === "/" && partial) {
15495
- return true;
15496
- }
15497
- const options = this.options;
15498
- if (this.isWindows) {
15499
- f = f.split("\\").join("/");
15500
- }
15501
- const ff = this.slashSplit(f);
15502
- this.debug(this.pattern, "split", ff);
15503
- const set = this.set;
15504
- this.debug(this.pattern, "set", set);
15505
- let filename = ff[ff.length - 1];
15506
- if (!filename) {
15507
- for (let i = ff.length - 2; !filename && i >= 0; i--) {
15508
- filename = ff[i];
15509
- }
15510
- }
15511
- for (let i = 0; i < set.length; i++) {
15512
- const pattern = set[i];
15513
- let file = ff;
15514
- if (options.matchBase && pattern.length === 1) {
15515
- file = [filename];
15516
- }
15517
- const hit = this.matchOne(file, pattern, partial);
15518
- if (hit) {
15519
- if (options.flipNegate) {
15520
- return true;
15521
- }
15522
- return !this.negate;
15523
- }
15524
- }
15525
- if (options.flipNegate) {
15526
- return false;
15527
- }
15528
- return this.negate;
15529
+ getLastGeneration(lock) {
15530
+ return lock.generations.find((g) => g.commit_sha === lock.current_generation);
15529
15531
  }
15530
- static defaults(def) {
15531
- return minimatch.defaults(def).Minimatch;
15532
+ /**
15533
+ * Build a transient `GenerationRecord` from a SHA derived by walking git
15534
+ * history. Only `commit_sha` (and `tree_hash` when needed downstream) is
15535
+ * load-bearing here; the rest are filled with defensible placeholders.
15536
+ * This record is consumed by `detectPatchesInRange` and is never persisted.
15537
+ */
15538
+ async synthesizeGenerationRecord(sha) {
15539
+ let tree_hash = "";
15540
+ try {
15541
+ tree_hash = await this.git.getTreeHash(sha);
15542
+ } catch {
15543
+ }
15544
+ return {
15545
+ commit_sha: sha,
15546
+ tree_hash,
15547
+ timestamp: (/* @__PURE__ */ new Date(0)).toISOString(),
15548
+ cli_version: "",
15549
+ generator_versions: {}
15550
+ };
15532
15551
  }
15533
15552
  };
15534
- minimatch.AST = AST;
15535
- minimatch.Minimatch = Minimatch;
15536
- minimatch.escape = escape;
15537
- minimatch.unescape = unescape;
15538
15553
 
15539
15554
  // src/ReplayService.ts
15555
+ var import_node_fs2 = require("fs");
15556
+ var import_promises6 = require("fs/promises");
15557
+ var import_node_path9 = require("path");
15540
15558
  init_GitClient();
15541
15559
 
15542
15560
  // src/ReplayApplicator.ts
@@ -17042,12 +17060,23 @@ async function computePerPatchDiff(patch, getCurrentGenContent, getWorkingTreeCo
17042
17060
  unlocatableFiles.push(file);
17043
17061
  continue;
17044
17062
  }
17045
- const locInCurrentGen = locInCurrentGenRaw.map(
17046
- (l) => resolveSpan(l, currentGenLines, "old")
17047
- );
17048
- const locInWorkingTree = locInWorkingTreeRaw.map(
17049
- (l) => resolveSpan(l, workingTreeLines, "new")
17050
- );
17063
+ const locInCurrentGen = [];
17064
+ const locInWorkingTree = [];
17065
+ let spanResolutionFailed = false;
17066
+ for (let i = 0; i < locInCurrentGenRaw.length; i++) {
17067
+ const cg = resolveSpan(locInCurrentGenRaw[i], currentGenLines, "old");
17068
+ const wt = resolveSpan(locInWorkingTreeRaw[i], workingTreeLines, "new");
17069
+ if (cg === null || wt === null) {
17070
+ spanResolutionFailed = true;
17071
+ break;
17072
+ }
17073
+ locInCurrentGen.push(cg);
17074
+ locInWorkingTree.push(wt);
17075
+ }
17076
+ if (spanResolutionFailed) {
17077
+ unlocatableFiles.push(file);
17078
+ continue;
17079
+ }
17051
17080
  const overlay = overlayRegions(
17052
17081
  currentGenLines,
17053
17082
  locInCurrentGen,
@@ -17109,11 +17138,11 @@ function resolveSpan(loc, oursLines, prefer) {
17109
17138
  if (prefer === "old") {
17110
17139
  if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
17111
17140
  if (newFits) return { ...loc, oursSpan: hunk.newCount };
17112
- } else {
17113
- if (newFits) return { ...loc, oursSpan: hunk.newCount };
17114
- if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
17141
+ return loc;
17115
17142
  }
17116
- return loc;
17143
+ if (newFits) return { ...loc, oursSpan: hunk.newCount };
17144
+ if (oldFits) return { ...loc, oursSpan: hunk.oldCount };
17145
+ return null;
17117
17146
  }
17118
17147
  function sequenceMatches(needle, haystack, offset) {
17119
17148
  if (offset + needle.length > haystack.length) return false;
@@ -17329,6 +17358,7 @@ var FirstGenerationFlow = class {
17329
17358
  };
17330
17359
  }
17331
17360
  async apply(_prep) {
17361
+ this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
17332
17362
  this.service.lockManager.save();
17333
17363
  return firstGenerationReport();
17334
17364
  }
@@ -17387,6 +17417,7 @@ var SkipApplicationFlow = class {
17387
17417
  };
17388
17418
  }
17389
17419
  async apply(_prep, options) {
17420
+ this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
17390
17421
  this.service.lockManager.save();
17391
17422
  const credentialWarnings = [...this.service.lockManager.warnings];
17392
17423
  if (!options?.stageOnly) {
@@ -17510,6 +17541,7 @@ var NoPatchesFlow = class {
17510
17541
  this.service.lockManager.addPatch(patch);
17511
17542
  }
17512
17543
  }
17544
+ this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
17513
17545
  this.service.lockManager.save();
17514
17546
  this.service.warnings.push(...this.service.lockManager.warnings);
17515
17547
  if (newPatches.length > 0) {
@@ -17672,6 +17704,7 @@ var NormalRegenerationFlow = class {
17672
17704
  }
17673
17705
  }
17674
17706
  }
17707
+ this.service.lockManager.setFernignorePatterns(this.service.readFernignorePatterns());
17675
17708
  this.service.lockManager.save();
17676
17709
  this.service.warnings.push(...this.service.lockManager.warnings);
17677
17710
  if (options?.stageOnly) {
@@ -18365,6 +18398,11 @@ var ReplayService = class {
18365
18398
  (files) => this.git.exec(["diff", currentGen, "HEAD", "--", ...files]).catch(() => null)
18366
18399
  );
18367
18400
  if (ppResult === null) continue;
18401
+ if (ppResult.hadFallback) {
18402
+ this.warnings.push(
18403
+ `Patch ${patch.id}: couldn't fully isolate ${ppResult.fallbackFiles.join(", ")} \u2014 customization preserved, attribution may have merged with another patch.`
18404
+ );
18405
+ }
18368
18406
  const diff = ppResult.diff;
18369
18407
  if (!diff.trim()) {
18370
18408
  if (await allPatchFilesUserOwned(patch)) continue;
@@ -18452,6 +18490,11 @@ var ReplayService = class {
18452
18490
  (files) => this.git.exec(["diff", currentGen, "HEAD", "--", ...files]).catch(() => null)
18453
18491
  );
18454
18492
  if (ppResult === null) continue;
18493
+ if (ppResult.hadFallback) {
18494
+ this.warnings.push(
18495
+ `Patch ${patch.id}: couldn't fully isolate ${ppResult.fallbackFiles.join(", ")} \u2014 customization preserved, attribution may have merged with another patch.`
18496
+ );
18497
+ }
18455
18498
  const diff = ppResult.diff;
18456
18499
  if (!diff.trim()) {
18457
18500
  if (await allPatchFilesUserOwned(patch)) {
@@ -18546,6 +18589,7 @@ If you want to keep these lines, restore them in a follow-up commit and re-run r
18546
18589
  }
18547
18590
  }
18548
18591
  }
18592
+ /** @internal Used by Flow implementations in `src/flows/`. */
18549
18593
  readFernignorePatterns() {
18550
18594
  const fernignorePath = (0, import_node_path9.join)(this.outputDir, ".fernignore");
18551
18595
  if (!(0, import_node_fs2.existsSync)(fernignorePath)) return [];