@fern-api/replay 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +33 -0
  2. package/dist/ConflictPrompt.d.ts +13 -0
  3. package/dist/ConflictPrompt.d.ts.map +1 -0
  4. package/dist/ConflictPrompt.js +38 -0
  5. package/dist/ConflictPrompt.js.map +1 -0
  6. package/dist/ConflictResolver.d.ts +11 -0
  7. package/dist/ConflictResolver.d.ts.map +1 -0
  8. package/dist/ConflictResolver.js +53 -0
  9. package/dist/ConflictResolver.js.map +1 -0
  10. package/dist/FernignoreMigrator.d.ts +71 -0
  11. package/dist/FernignoreMigrator.d.ts.map +1 -0
  12. package/dist/FernignoreMigrator.js +243 -0
  13. package/dist/FernignoreMigrator.js.map +1 -0
  14. package/dist/LockfileManager.d.ts +27 -0
  15. package/dist/LockfileManager.d.ts.map +1 -0
  16. package/dist/LockfileManager.js +108 -0
  17. package/dist/LockfileManager.js.map +1 -0
  18. package/dist/ReplayApplicator.d.ts +68 -0
  19. package/dist/ReplayApplicator.d.ts.map +1 -0
  20. package/dist/ReplayApplicator.js +492 -0
  21. package/dist/ReplayApplicator.js.map +1 -0
  22. package/dist/ReplayCommitter.d.ts +39 -0
  23. package/dist/ReplayCommitter.d.ts.map +1 -0
  24. package/dist/ReplayCommitter.js +84 -0
  25. package/dist/ReplayCommitter.js.map +1 -0
  26. package/dist/ReplayDetector.d.ts +25 -0
  27. package/dist/ReplayDetector.d.ts.map +1 -0
  28. package/dist/ReplayDetector.js +110 -0
  29. package/dist/ReplayDetector.js.map +1 -0
  30. package/dist/ReplayService.d.ts +103 -0
  31. package/dist/ReplayService.d.ts.map +1 -0
  32. package/dist/ReplayService.js +457 -0
  33. package/dist/ReplayService.js.map +1 -0
  34. package/dist/ThreeWayMerge.d.ts +11 -0
  35. package/dist/ThreeWayMerge.d.ts.map +1 -0
  36. package/dist/ThreeWayMerge.js +48 -0
  37. package/dist/ThreeWayMerge.js.map +1 -0
  38. package/dist/cli.d.ts +3 -0
  39. package/dist/cli.d.ts.map +1 -0
  40. package/dist/cli.js +464 -0
  41. package/dist/cli.js.map +1 -0
  42. package/dist/commands/bootstrap.d.ts +44 -0
  43. package/dist/commands/bootstrap.d.ts.map +1 -0
  44. package/dist/commands/bootstrap.js +268 -0
  45. package/dist/commands/bootstrap.js.map +1 -0
  46. package/dist/commands/forget.d.ts +26 -0
  47. package/dist/commands/forget.d.ts.map +1 -0
  48. package/dist/commands/forget.js +37 -0
  49. package/dist/commands/forget.js.map +1 -0
  50. package/dist/commands/index.d.ts +6 -0
  51. package/dist/commands/index.d.ts.map +1 -0
  52. package/dist/commands/index.js +6 -0
  53. package/dist/commands/index.js.map +1 -0
  54. package/dist/commands/reset.d.ts +28 -0
  55. package/dist/commands/reset.d.ts.map +1 -0
  56. package/dist/commands/reset.js +37 -0
  57. package/dist/commands/reset.js.map +1 -0
  58. package/dist/commands/resolve.d.ts +16 -0
  59. package/dist/commands/resolve.d.ts.map +1 -0
  60. package/dist/commands/resolve.js +28 -0
  61. package/dist/commands/resolve.js.map +1 -0
  62. package/dist/commands/status.d.ts +34 -0
  63. package/dist/commands/status.d.ts.map +1 -0
  64. package/dist/commands/status.js +32 -0
  65. package/dist/commands/status.js.map +1 -0
  66. package/dist/environment.d.ts +5 -0
  67. package/dist/environment.d.ts.map +1 -0
  68. package/dist/environment.js +14 -0
  69. package/dist/environment.js.map +1 -0
  70. package/dist/git/CommitDetection.d.ts +17 -0
  71. package/dist/git/CommitDetection.d.ts.map +1 -0
  72. package/dist/git/CommitDetection.js +33 -0
  73. package/dist/git/CommitDetection.js.map +1 -0
  74. package/dist/git/GitClient.d.ts +28 -0
  75. package/dist/git/GitClient.d.ts.map +1 -0
  76. package/dist/git/GitClient.js +104 -0
  77. package/dist/git/GitClient.js.map +1 -0
  78. package/dist/index.d.ts +15 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +14 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/types.d.ts +80 -0
  83. package/dist/types.d.ts.map +1 -0
  84. package/dist/types.js +9 -0
  85. package/dist/types.js.map +1 -0
  86. package/package.json +45 -0
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @fern-api/replay
2
+
3
+ Automatically preserves SDK customizations across Fern regenerations.
4
+
5
+ ## What it does
6
+
7
+ Fern Replay detects user edits to generated SDK code, stores them as patches, and re-applies them after each regeneration using 3-way merge. This means your customizations survive `fern generate` without manual intervention.
8
+
9
+ ## Pipeline
10
+
11
+ 1. **Detect** — Find commits since last generation
12
+ 2. **Store** — Save patches to `replay.lock`
13
+ 3. **Generate** — Run generator (overwrites all)
14
+ 4. **Replay** — Apply patches via 3-way merge
15
+ 5. **Commit** — Commit the merged result (or surface conflicts)
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install @fern-api/replay
21
+ ```
22
+
23
+ ## Development
24
+
25
+ ```bash
26
+ npm install
27
+ npm run build
28
+ npm test
29
+ ```
30
+
31
+ ## License
32
+
33
+ MIT
@@ -0,0 +1,13 @@
1
+ import type { FileResult } from "./types.js";
2
+ export type PromptResult = "ours" | "theirs" | "manual" | "abort";
3
+ /**
4
+ * Prompt the user to resolve a single conflicting file.
5
+ * Uses readline for zero-dependency interactive input.
6
+ * Output goes to stderr so stdout stays clean for piping.
7
+ */
8
+ export declare function promptForConflictResolution(options: {
9
+ patchId: string;
10
+ patchMessage: string;
11
+ file: FileResult;
12
+ }): Promise<PromptResult>;
13
+ //# sourceMappingURL=ConflictPrompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConflictPrompt.d.ts","sourceRoot":"","sources":["../src/ConflictPrompt.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAElE;;;;GAIG;AACH,wBAAsB,2BAA2B,CAAC,OAAO,EAAE;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,UAAU,CAAC;CACpB,GAAG,OAAO,CAAC,YAAY,CAAC,CAqBxB"}
@@ -0,0 +1,38 @@
1
+ import { createInterface } from "node:readline";
2
+ /**
3
+ * Prompt the user to resolve a single conflicting file.
4
+ * Uses readline for zero-dependency interactive input.
5
+ * Output goes to stderr so stdout stays clean for piping.
6
+ */
7
+ export async function promptForConflictResolution(options) {
8
+ const { patchId, patchMessage, file } = options;
9
+ const regionCount = file.conflicts?.length ?? 0;
10
+ process.stderr.write(`\nConflict in: ${file.file}\n`);
11
+ process.stderr.write(` Patch: ${patchId} - "${patchMessage}"\n`);
12
+ process.stderr.write(` ${regionCount} conflict region(s)\n\n`);
13
+ process.stderr.write(" [g] Keep generated (discard your customization for this file)\n");
14
+ process.stderr.write(" [m] Keep mine (discard generated changes for this file)\n");
15
+ process.stderr.write(" [s] Skip (leave conflict markers for manual resolution)\n");
16
+ process.stderr.write(" [a] Abort (stop replay, leave working tree as-is)\n\n");
17
+ const answer = await askSingleChar("Choice [g/m/s/a]: ");
18
+ switch (answer.toLowerCase()) {
19
+ case "g": return "ours";
20
+ case "m": return "theirs";
21
+ case "a": return "abort";
22
+ case "s":
23
+ default: return "manual";
24
+ }
25
+ }
26
+ function askSingleChar(prompt) {
27
+ return new Promise((resolve) => {
28
+ const rl = createInterface({
29
+ input: process.stdin,
30
+ output: process.stderr,
31
+ });
32
+ rl.question(prompt, (answer) => {
33
+ rl.close();
34
+ resolve(answer.trim());
35
+ });
36
+ });
37
+ }
38
+ //# sourceMappingURL=ConflictPrompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConflictPrompt.js","sourceRoot":"","sources":["../src/ConflictPrompt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAKhD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAAC,OAIjD;IACG,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;IAChD,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,EAAE,MAAM,IAAI,CAAC,CAAC;IAEhD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,kBAAkB,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC;IACtD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,OAAO,OAAO,YAAY,KAAK,CAAC,CAAC;IAClE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,WAAW,yBAAyB,CAAC,CAAC;IAChE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,mEAAmE,CAAC,CAAC;IAC1F,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,6DAA6D,CAAC,CAAC;IACpF,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,6DAA6D,CAAC,CAAC;IACpF,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,yDAAyD,CAAC,CAAC;IAEhF,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,oBAAoB,CAAC,CAAC;IAEzD,QAAQ,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC;QAC3B,KAAK,GAAG,CAAC,CAAC,OAAO,MAAM,CAAC;QACxB,KAAK,GAAG,CAAC,CAAC,OAAO,QAAQ,CAAC;QAC1B,KAAK,GAAG,CAAC,CAAC,OAAO,OAAO,CAAC;QACzB,KAAK,GAAG,CAAC;QACT,OAAO,CAAC,CAAC,OAAO,QAAQ,CAAC;IAC7B,CAAC;AACL,CAAC;AAED,SAAS,aAAa,CAAC,MAAc;IACjC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC3B,MAAM,EAAE,GAAG,eAAe,CAAC;YACvB,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,MAAM,EAAE,OAAO,CAAC,MAAM;SACzB,CAAC,CAAC;QAEH,EAAE,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE;YAC3B,EAAE,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QAC3B,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -0,0 +1,11 @@
1
+ export type ResolutionStrategy = "ours" | "theirs";
2
+ /**
3
+ * Resolve conflict markers in a string by keeping the chosen side.
4
+ * If the content has no conflict markers, it is returned unchanged.
5
+ */
6
+ export declare function resolveContent(content: string, strategy: ResolutionStrategy): string;
7
+ /**
8
+ * Read a file, resolve all conflict markers using the given strategy, and write it back.
9
+ */
10
+ export declare function resolveConflictMarkers(filePath: string, strategy: ResolutionStrategy): Promise<void>;
11
+ //# sourceMappingURL=ConflictResolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConflictResolver.d.ts","sourceRoot":"","sources":["../src/ConflictResolver.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAMnD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,MAAM,CAuCpF;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAI1G"}
@@ -0,0 +1,53 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ const OURS_START = "<<<<<<< OURS (generated)";
3
+ const SEPARATOR = "=======";
4
+ const THEIRS_END = ">>>>>>> THEIRS (your customization)";
5
+ /**
6
+ * Resolve conflict markers in a string by keeping the chosen side.
7
+ * If the content has no conflict markers, it is returned unchanged.
8
+ */
9
+ export function resolveContent(content, strategy) {
10
+ const lines = content.split("\n");
11
+ const result = [];
12
+ let inConflict = false;
13
+ let inOurs = false;
14
+ let inTheirs = false;
15
+ for (const line of lines) {
16
+ if (line === OURS_START) {
17
+ inConflict = true;
18
+ inOurs = true;
19
+ inTheirs = false;
20
+ continue;
21
+ }
22
+ if (inConflict && line === SEPARATOR) {
23
+ inOurs = false;
24
+ inTheirs = true;
25
+ continue;
26
+ }
27
+ if (inConflict && line === THEIRS_END) {
28
+ inConflict = false;
29
+ inOurs = false;
30
+ inTheirs = false;
31
+ continue;
32
+ }
33
+ if (!inConflict) {
34
+ result.push(line);
35
+ }
36
+ else if (inOurs && strategy === "ours") {
37
+ result.push(line);
38
+ }
39
+ else if (inTheirs && strategy === "theirs") {
40
+ result.push(line);
41
+ }
42
+ }
43
+ return result.join("\n");
44
+ }
45
+ /**
46
+ * Read a file, resolve all conflict markers using the given strategy, and write it back.
47
+ */
48
+ export async function resolveConflictMarkers(filePath, strategy) {
49
+ const content = readFileSync(filePath, "utf-8");
50
+ const resolved = resolveContent(content, strategy);
51
+ writeFileSync(filePath, resolved, "utf-8");
52
+ }
53
+ //# sourceMappingURL=ConflictResolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConflictResolver.js","sourceRoot":"","sources":["../src/ConflictResolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAItD,MAAM,UAAU,GAAG,0BAA0B,CAAC;AAC9C,MAAM,SAAS,GAAG,SAAS,CAAC;AAC5B,MAAM,UAAU,GAAG,qCAAqC,CAAC;AAEzD;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,QAA4B;IACxE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,IAAI,UAAU,GAAG,KAAK,CAAC;IACvB,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;YACtB,UAAU,GAAG,IAAI,CAAC;YAClB,MAAM,GAAG,IAAI,CAAC;YACd,QAAQ,GAAG,KAAK,CAAC;YACjB,SAAS;QACb,CAAC;QAED,IAAI,UAAU,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACnC,MAAM,GAAG,KAAK,CAAC;YACf,QAAQ,GAAG,IAAI,CAAC;YAChB,SAAS;QACb,CAAC;QAED,IAAI,UAAU,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;YACpC,UAAU,GAAG,KAAK,CAAC;YACnB,MAAM,GAAG,KAAK,CAAC;YACf,QAAQ,GAAG,KAAK,CAAC;YACjB,SAAS;QACb,CAAC;QAED,IAAI,CAAC,UAAU,EAAE,CAAC;YACd,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC;aAAM,IAAI,MAAM,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;YACvC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC;aAAM,IAAI,QAAQ,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAC3C,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC;IACL,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC7B,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,QAAgB,EAAE,QAA4B;IACvF,MAAM,OAAO,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,cAAc,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACnD,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;AAC/C,CAAC"}
@@ -0,0 +1,71 @@
1
+ import type { GitClient } from "./git/GitClient.js";
2
+ import type { LockfileManager } from "./LockfileManager.js";
3
+ import type { StoredPatch } from "./types.js";
4
+ export interface MigrationAnalysis {
5
+ /** Files in .fernignore that have git commit history (can be tracked by Replay) */
6
+ trackedByBoth: Array<{
7
+ file: string;
8
+ commit: string;
9
+ }>;
10
+ /** Files in .fernignore but NO recent commit history found after last generation */
11
+ fernignoreOnly: string[];
12
+ /** Commits found that AREN'T in .fernignore (inline edits to generated files) */
13
+ commitsOnly: StoredPatch[];
14
+ /** Synthetic patches created for .fernignore files that differ from pristine generation */
15
+ syntheticPatches: StoredPatch[];
16
+ }
17
+ export interface MigrationResult {
18
+ patchesCreated: number;
19
+ filesSkipped: string[];
20
+ warnings: string[];
21
+ }
22
+ export declare class FernignoreMigrator {
23
+ private git;
24
+ private lockManager;
25
+ private outputDir;
26
+ constructor(git: GitClient, lockManager: LockfileManager, outputDir: string);
27
+ /**
28
+ * Check if .fernignore exists in the output directory.
29
+ */
30
+ fernignoreExists(): boolean;
31
+ /**
32
+ * Read and parse .fernignore patterns.
33
+ * Filters out comments and empty lines.
34
+ */
35
+ readFernignorePatterns(): string[];
36
+ /**
37
+ * Analyze correlation between .fernignore patterns and git history.
38
+ *
39
+ * For patterns without recent commits (committed before last generation),
40
+ * creates synthetic patches by diffing the current file state against
41
+ * the pristine generated state at the generation's tree hash.
42
+ */
43
+ analyzeMigration(): Promise<MigrationAnalysis>;
44
+ /**
45
+ * Resolve glob patterns to actual file paths on disk.
46
+ */
47
+ private resolvePatterns;
48
+ /**
49
+ * Read file content from the output directory.
50
+ */
51
+ private readFileContent;
52
+ /**
53
+ * Create a unified diff for a new file (not in generation tree).
54
+ */
55
+ private createNewFileDiff;
56
+ /**
57
+ * Create a unified diff between pristine and current content.
58
+ */
59
+ private createFileDiff;
60
+ /**
61
+ * Perform the migration - create patches for .fernignore files that have history.
62
+ * Adds detected patches to the lockfile.
63
+ */
64
+ migrate(): Promise<MigrationResult>;
65
+ /**
66
+ * Move .fernignore patterns to replay.yml exclude list.
67
+ * This lets Replay skip these files during application.
68
+ */
69
+ movePatternsToReplayYml(patterns: string[]): void;
70
+ }
71
+ //# sourceMappingURL=FernignoreMigrator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FernignoreMigrator.d.ts","sourceRoot":"","sources":["../src/FernignoreMigrator.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE5D,OAAO,KAAK,EAAE,WAAW,EAAwB,MAAM,YAAY,CAAC;AAEpE,MAAM,WAAW,iBAAiB;IAC9B,mFAAmF;IACnF,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvD,oFAAoF;IACpF,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,iFAAiF;IACjF,WAAW,EAAE,WAAW,EAAE,CAAC;IAC3B,2FAA2F;IAC3F,gBAAgB,EAAE,WAAW,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,eAAe;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,qBAAa,kBAAkB;IAC3B,OAAO,CAAC,GAAG,CAAY;IACvB,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,SAAS,CAAS;gBAEd,GAAG,EAAE,SAAS,EAAE,WAAW,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM;IAM3E;;OAEG;IACH,gBAAgB,IAAI,OAAO;IAI3B;;;OAGG;IACH,sBAAsB,IAAI,MAAM,EAAE;IAYlC;;;;;;OAMG;IACG,gBAAgB,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAwGpD;;OAEG;YACW,eAAe;IAgB7B;;OAEG;IACH,OAAO,CAAC,eAAe;IAgBvB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAazB;;OAEG;IACH,OAAO,CAAC,cAAc;IAgBtB;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,eAAe,CAAC;IA8BzC;;;OAGG;IACH,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI;CAmBpD"}
@@ -0,0 +1,243 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { minimatch } from "minimatch";
5
+ import { stringify, parse } from "yaml";
6
+ import { ReplayDetector } from "./ReplayDetector.js";
7
+ export class FernignoreMigrator {
8
+ git;
9
+ lockManager;
10
+ outputDir;
11
+ constructor(git, lockManager, outputDir) {
12
+ this.git = git;
13
+ this.lockManager = lockManager;
14
+ this.outputDir = outputDir;
15
+ }
16
+ /**
17
+ * Check if .fernignore exists in the output directory.
18
+ */
19
+ fernignoreExists() {
20
+ return existsSync(join(this.outputDir, ".fernignore"));
21
+ }
22
+ /**
23
+ * Read and parse .fernignore patterns.
24
+ * Filters out comments and empty lines.
25
+ */
26
+ readFernignorePatterns() {
27
+ const fernignorePath = join(this.outputDir, ".fernignore");
28
+ if (!existsSync(fernignorePath)) {
29
+ return [];
30
+ }
31
+ const content = readFileSync(fernignorePath, "utf-8");
32
+ return content
33
+ .split("\n")
34
+ .map((line) => line.trim())
35
+ .filter((line) => line && !line.startsWith("#"));
36
+ }
37
+ /**
38
+ * Analyze correlation between .fernignore patterns and git history.
39
+ *
40
+ * For patterns without recent commits (committed before last generation),
41
+ * creates synthetic patches by diffing the current file state against
42
+ * the pristine generated state at the generation's tree hash.
43
+ */
44
+ async analyzeMigration() {
45
+ const patterns = this.readFernignorePatterns();
46
+ const detector = new ReplayDetector(this.git, this.lockManager, this.outputDir);
47
+ const patches = await detector.detectNewPatches();
48
+ const trackedByBoth = [];
49
+ const fernignoreOnly = [];
50
+ const commitsOnly = [];
51
+ const syntheticPatches = [];
52
+ // Check each fernignore pattern against recent patches
53
+ for (const pattern of patterns) {
54
+ const matchingPatch = patches.find((p) => p.files.some((f) => minimatch(f, pattern) || f === pattern));
55
+ if (matchingPatch) {
56
+ trackedByBoth.push({
57
+ file: pattern,
58
+ commit: matchingPatch.original_commit,
59
+ });
60
+ }
61
+ else {
62
+ fernignoreOnly.push(pattern);
63
+ }
64
+ }
65
+ // For fernignore-only patterns, try to create synthetic patches
66
+ // by diffing current file state against pristine generation tree
67
+ const lock = this.lockManager.read();
68
+ const currentGen = lock.generations.find((g) => g.commit_sha === lock.current_generation);
69
+ if (currentGen && fernignoreOnly.length > 0) {
70
+ const resolvedFiles = await this.resolvePatterns(fernignoreOnly);
71
+ const remainingFernignoreOnly = [];
72
+ for (const { pattern, files } of resolvedFiles) {
73
+ const patchFiles = [];
74
+ const diffParts = [];
75
+ for (const filePath of files) {
76
+ const pristine = await this.git.showFile(currentGen.tree_hash, filePath);
77
+ const currentContent = this.readFileContent(filePath);
78
+ if (currentContent === null) {
79
+ // File in .fernignore but not on disk — nothing to track
80
+ continue;
81
+ }
82
+ if (pristine === null) {
83
+ // File doesn't exist in generation tree — it's a wholly new file
84
+ patchFiles.push(filePath);
85
+ diffParts.push(this.createNewFileDiff(filePath, currentContent));
86
+ }
87
+ else if (pristine !== currentContent) {
88
+ // File differs from pristine — user has customized it
89
+ patchFiles.push(filePath);
90
+ diffParts.push(this.createFileDiff(filePath, pristine, currentContent));
91
+ }
92
+ // If pristine === currentContent, file is unchanged — skip
93
+ }
94
+ if (patchFiles.length > 0) {
95
+ const patchContent = diffParts.join("\n");
96
+ const contentHash = `sha256:${createHash("sha256").update(patchContent).digest("hex")}`;
97
+ syntheticPatches.push({
98
+ id: `patch-fernignore-${createHash("sha256").update(pattern).digest("hex").slice(0, 8)}`,
99
+ content_hash: contentHash,
100
+ original_commit: currentGen.commit_sha,
101
+ original_message: `[fernignore-migration] Customizations for ${pattern}`,
102
+ original_author: "Fern Replay <replay@buildwithfern.com>",
103
+ base_generation: currentGen.commit_sha,
104
+ files: patchFiles,
105
+ patch_content: patchContent,
106
+ });
107
+ trackedByBoth.push({
108
+ file: pattern,
109
+ commit: `synthetic (differs from generated)`,
110
+ });
111
+ }
112
+ else {
113
+ // No diff found — file matches generated output or doesn't exist
114
+ remainingFernignoreOnly.push(pattern);
115
+ }
116
+ }
117
+ // Replace fernignoreOnly with only truly untrackable patterns
118
+ fernignoreOnly.length = 0;
119
+ fernignoreOnly.push(...remainingFernignoreOnly);
120
+ }
121
+ // Find patches that touch files NOT in .fernignore
122
+ for (const patch of patches) {
123
+ const hasUnprotectedFiles = patch.files.some((f) => !patterns.some((p) => minimatch(f, p) || f === p));
124
+ if (hasUnprotectedFiles) {
125
+ commitsOnly.push(patch);
126
+ }
127
+ }
128
+ return { trackedByBoth, fernignoreOnly, commitsOnly, syntheticPatches };
129
+ }
130
+ /**
131
+ * Resolve glob patterns to actual file paths on disk.
132
+ */
133
+ async resolvePatterns(patterns) {
134
+ const allFiles = (await this.git.exec(["ls-files"])).trim().split("\n").filter(Boolean);
135
+ const results = [];
136
+ for (const pattern of patterns) {
137
+ const matching = allFiles.filter((f) => minimatch(f, pattern) || f === pattern || f.startsWith(pattern + "/"));
138
+ results.push({ pattern, files: matching.length > 0 ? matching : [pattern] });
139
+ }
140
+ return results;
141
+ }
142
+ /**
143
+ * Read file content from the output directory.
144
+ */
145
+ readFileContent(filePath) {
146
+ const fullPath = join(this.outputDir, filePath);
147
+ if (!existsSync(fullPath)) {
148
+ return null;
149
+ }
150
+ try {
151
+ const stat = statSync(fullPath);
152
+ if (stat.isDirectory()) {
153
+ return null;
154
+ }
155
+ return readFileSync(fullPath, "utf-8");
156
+ }
157
+ catch {
158
+ return null;
159
+ }
160
+ }
161
+ /**
162
+ * Create a unified diff for a new file (not in generation tree).
163
+ */
164
+ createNewFileDiff(filePath, content) {
165
+ const lines = content.split("\n");
166
+ const hunks = lines.map((l) => `+${l}`).join("\n");
167
+ return [
168
+ `diff --git a/${filePath} b/${filePath}`,
169
+ "new file mode 100644",
170
+ `--- /dev/null`,
171
+ `+++ b/${filePath}`,
172
+ `@@ -0,0 +1,${lines.length} @@`,
173
+ hunks,
174
+ ].join("\n");
175
+ }
176
+ /**
177
+ * Create a unified diff between pristine and current content.
178
+ */
179
+ createFileDiff(filePath, pristine, current) {
180
+ const oldLines = pristine.split("\n");
181
+ const newLines = current.split("\n");
182
+ // Simple full-file replacement diff — not optimal but correct
183
+ const removals = oldLines.map((l) => `-${l}`).join("\n");
184
+ const additions = newLines.map((l) => `+${l}`).join("\n");
185
+ return [
186
+ `diff --git a/${filePath} b/${filePath}`,
187
+ `--- a/${filePath}`,
188
+ `+++ b/${filePath}`,
189
+ `@@ -1,${oldLines.length} +1,${newLines.length} @@`,
190
+ removals,
191
+ additions,
192
+ ].join("\n");
193
+ }
194
+ /**
195
+ * Perform the migration - create patches for .fernignore files that have history.
196
+ * Adds detected patches to the lockfile.
197
+ */
198
+ async migrate() {
199
+ const analysis = await this.analyzeMigration();
200
+ const detector = new ReplayDetector(this.git, this.lockManager, this.outputDir);
201
+ const patches = await detector.detectNewPatches();
202
+ const warnings = [];
203
+ let patchesCreated = 0;
204
+ // Add all detected patches to lockfile
205
+ for (const patch of patches) {
206
+ this.lockManager.addPatch(patch);
207
+ patchesCreated++;
208
+ }
209
+ if (patchesCreated > 0) {
210
+ this.lockManager.save();
211
+ }
212
+ // Warn about fernignore-only files
213
+ for (const file of analysis.fernignoreOnly) {
214
+ warnings.push(`${file}: in .fernignore but no commit history found. Commit this file or keep in .fernignore as fallback.`);
215
+ }
216
+ return {
217
+ patchesCreated,
218
+ filesSkipped: analysis.fernignoreOnly,
219
+ warnings,
220
+ };
221
+ }
222
+ /**
223
+ * Move .fernignore patterns to replay.yml exclude list.
224
+ * This lets Replay skip these files during application.
225
+ */
226
+ movePatternsToReplayYml(patterns) {
227
+ const replayYmlPath = join(this.outputDir, ".fern", "replay.yml");
228
+ let config = {};
229
+ if (existsSync(replayYmlPath)) {
230
+ const content = readFileSync(replayYmlPath, "utf-8");
231
+ config = parse(content) ?? {};
232
+ }
233
+ const existing = config.exclude ?? [];
234
+ const merged = [...new Set([...existing, ...patterns])];
235
+ config.exclude = merged;
236
+ const dir = dirname(replayYmlPath);
237
+ if (!existsSync(dir)) {
238
+ mkdirSync(dir, { recursive: true });
239
+ }
240
+ writeFileSync(replayYmlPath, stringify(config, { lineWidth: 0 }), "utf-8");
241
+ }
242
+ }
243
+ //# sourceMappingURL=FernignoreMigrator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FernignoreMigrator.js","sourceRoot":"","sources":["../src/FernignoreMigrator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import type { GenerationLock, GenerationRecord, StoredPatch, CustomizationsConfig } from "./types.js";
2
+ export declare class LockfileManager {
3
+ private outputDir;
4
+ private lock;
5
+ constructor(outputDir: string);
6
+ get lockfilePath(): string;
7
+ get customizationsPath(): string;
8
+ exists(): boolean;
9
+ read(): GenerationLock;
10
+ initialize(firstGeneration: GenerationRecord): void;
11
+ /**
12
+ * Set up in-memory lock state without writing to disk.
13
+ * Useful for bootstrap dry-run where we need state for detection
14
+ * but don't want to persist anything.
15
+ */
16
+ initializeInMemory(firstGeneration: GenerationRecord): void;
17
+ save(): void;
18
+ addGeneration(record: GenerationRecord): void;
19
+ addPatch(patch: StoredPatch): void;
20
+ updatePatch(patchId: string, updates: Partial<Pick<StoredPatch, "base_generation" | "patch_content" | "content_hash" | "files">>): void;
21
+ removePatch(patchId: string): void;
22
+ getPatches(): StoredPatch[];
23
+ getGeneration(commitSha: string): GenerationRecord | undefined;
24
+ getCustomizationsConfig(): CustomizationsConfig;
25
+ private ensureLoaded;
26
+ }
27
+ //# sourceMappingURL=LockfileManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LockfileManager.d.ts","sourceRoot":"","sources":["../src/LockfileManager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACR,cAAc,EACd,gBAAgB,EAChB,WAAW,EACX,oBAAoB,EACvB,MAAM,YAAY,CAAC;AAIpB,qBAAa,eAAe;IACxB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,IAAI,CAA+B;gBAE/B,SAAS,EAAE,MAAM;IAI7B,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,IAAI,kBAAkB,IAAI,MAAM,CAE/B;IAED,MAAM,IAAI,OAAO;IAIjB,IAAI,IAAI,cAAc;IAYtB,UAAU,CAAC,eAAe,EAAE,gBAAgB,GAAG,IAAI;IAKnD;;;;OAIG;IACH,kBAAkB,CAAC,eAAe,EAAE,gBAAgB,GAAG,IAAI;IAS3D,IAAI,IAAI,IAAI;IAmBZ,aAAa,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI;IAM7C,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IAKlC,WAAW,CACP,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,iBAAiB,GAAG,eAAe,GAAG,cAAc,GAAG,OAAO,CAAC,CAAC,GACpG,IAAI;IASP,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAKlC,UAAU,IAAI,WAAW,EAAE;IAK3B,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;IAK9D,uBAAuB,IAAI,oBAAoB;IAQ/C,OAAO,CAAC,YAAY;CAKvB"}
@@ -0,0 +1,108 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { stringify, parse } from "yaml";
4
+ const LOCKFILE_HEADER = "# DO NOT EDIT MANUALLY - Managed by Fern Replay\n";
5
+ export class LockfileManager {
6
+ outputDir;
7
+ lock = null;
8
+ constructor(outputDir) {
9
+ this.outputDir = outputDir;
10
+ }
11
+ get lockfilePath() {
12
+ return join(this.outputDir, ".fern", "replay.lock");
13
+ }
14
+ get customizationsPath() {
15
+ return join(this.outputDir, ".fern", "replay.yml");
16
+ }
17
+ exists() {
18
+ return existsSync(this.lockfilePath);
19
+ }
20
+ read() {
21
+ if (this.lock) {
22
+ return this.lock;
23
+ }
24
+ if (!this.exists()) {
25
+ throw new Error(`Lockfile not found: ${this.lockfilePath}`);
26
+ }
27
+ const content = readFileSync(this.lockfilePath, "utf-8");
28
+ this.lock = parse(content);
29
+ return this.lock;
30
+ }
31
+ initialize(firstGeneration) {
32
+ this.initializeInMemory(firstGeneration);
33
+ this.save();
34
+ }
35
+ /**
36
+ * Set up in-memory lock state without writing to disk.
37
+ * Useful for bootstrap dry-run where we need state for detection
38
+ * but don't want to persist anything.
39
+ */
40
+ initializeInMemory(firstGeneration) {
41
+ this.lock = {
42
+ version: "1.0",
43
+ generations: [firstGeneration],
44
+ current_generation: firstGeneration.commit_sha,
45
+ patches: [],
46
+ };
47
+ }
48
+ save() {
49
+ if (!this.lock) {
50
+ throw new Error("No lockfile data to save. Call read() or initialize() first.");
51
+ }
52
+ const dir = dirname(this.lockfilePath);
53
+ if (!existsSync(dir)) {
54
+ mkdirSync(dir, { recursive: true });
55
+ }
56
+ const yaml = stringify(this.lock, {
57
+ lineWidth: 0,
58
+ blockQuote: "literal",
59
+ });
60
+ const content = LOCKFILE_HEADER + yaml;
61
+ // Atomic write: write to temp file then rename (rename is atomic on most filesystems)
62
+ const tmpPath = this.lockfilePath + ".tmp";
63
+ writeFileSync(tmpPath, content, "utf-8");
64
+ renameSync(tmpPath, this.lockfilePath);
65
+ }
66
+ addGeneration(record) {
67
+ this.ensureLoaded();
68
+ this.lock.generations.push(record);
69
+ this.lock.current_generation = record.commit_sha;
70
+ }
71
+ addPatch(patch) {
72
+ this.ensureLoaded();
73
+ this.lock.patches.push(patch);
74
+ }
75
+ updatePatch(patchId, updates) {
76
+ this.ensureLoaded();
77
+ const patch = this.lock.patches.find((p) => p.id === patchId);
78
+ if (!patch) {
79
+ throw new Error(`Patch not found: ${patchId}`);
80
+ }
81
+ Object.assign(patch, updates);
82
+ }
83
+ removePatch(patchId) {
84
+ this.ensureLoaded();
85
+ this.lock.patches = this.lock.patches.filter((p) => p.id !== patchId);
86
+ }
87
+ getPatches() {
88
+ this.ensureLoaded();
89
+ return this.lock.patches;
90
+ }
91
+ getGeneration(commitSha) {
92
+ this.ensureLoaded();
93
+ return this.lock.generations.find((g) => g.commit_sha === commitSha);
94
+ }
95
+ getCustomizationsConfig() {
96
+ if (!existsSync(this.customizationsPath)) {
97
+ return {};
98
+ }
99
+ const content = readFileSync(this.customizationsPath, "utf-8");
100
+ return parse(content) ?? {};
101
+ }
102
+ ensureLoaded() {
103
+ if (!this.lock) {
104
+ throw new Error("No lockfile loaded. Call read() or initialize() first.");
105
+ }
106
+ }
107
+ }
108
+ //# sourceMappingURL=LockfileManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LockfileManager.js","sourceRoot":"","sources":["../src/LockfileManager.ts"],"names":[],"mappings":"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"}