tlc-claude-code 1.2.29 → 1.4.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 (182) hide show
  1. package/dashboard/dist/components/AuditPane.d.ts +30 -0
  2. package/dashboard/dist/components/AuditPane.js +127 -0
  3. package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
  4. package/dashboard/dist/components/AuditPane.test.js +339 -0
  5. package/dashboard/dist/components/CompliancePane.d.ts +39 -0
  6. package/dashboard/dist/components/CompliancePane.js +96 -0
  7. package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
  8. package/dashboard/dist/components/CompliancePane.test.js +183 -0
  9. package/dashboard/dist/components/SSOPane.d.ts +36 -0
  10. package/dashboard/dist/components/SSOPane.js +71 -0
  11. package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
  12. package/dashboard/dist/components/SSOPane.test.js +155 -0
  13. package/dashboard/dist/components/UsagePane.d.ts +13 -0
  14. package/dashboard/dist/components/UsagePane.js +51 -0
  15. package/dashboard/dist/components/UsagePane.test.d.ts +1 -0
  16. package/dashboard/dist/components/UsagePane.test.js +142 -0
  17. package/dashboard/dist/components/WorkspaceDocsPane.d.ts +19 -0
  18. package/dashboard/dist/components/WorkspaceDocsPane.js +130 -0
  19. package/dashboard/dist/components/WorkspaceDocsPane.test.d.ts +1 -0
  20. package/dashboard/dist/components/WorkspaceDocsPane.test.js +242 -0
  21. package/dashboard/dist/components/WorkspacePane.d.ts +18 -0
  22. package/dashboard/dist/components/WorkspacePane.js +17 -0
  23. package/dashboard/dist/components/WorkspacePane.test.d.ts +1 -0
  24. package/dashboard/dist/components/WorkspacePane.test.js +84 -0
  25. package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
  26. package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
  27. package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
  28. package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
  29. package/package.json +1 -1
  30. package/server/lib/access-control-doc.js +541 -0
  31. package/server/lib/access-control-doc.test.js +672 -0
  32. package/server/lib/adr-generator.js +423 -0
  33. package/server/lib/adr-generator.test.js +586 -0
  34. package/server/lib/agent-progress-monitor.js +223 -0
  35. package/server/lib/agent-progress-monitor.test.js +202 -0
  36. package/server/lib/architecture-command.js +450 -0
  37. package/server/lib/architecture-command.test.js +754 -0
  38. package/server/lib/ast-analyzer.js +324 -0
  39. package/server/lib/ast-analyzer.test.js +437 -0
  40. package/server/lib/audit-attribution.js +191 -0
  41. package/server/lib/audit-attribution.test.js +359 -0
  42. package/server/lib/audit-classifier.js +202 -0
  43. package/server/lib/audit-classifier.test.js +209 -0
  44. package/server/lib/audit-command.js +275 -0
  45. package/server/lib/audit-command.test.js +325 -0
  46. package/server/lib/audit-exporter.js +380 -0
  47. package/server/lib/audit-exporter.test.js +464 -0
  48. package/server/lib/audit-logger.js +236 -0
  49. package/server/lib/audit-logger.test.js +364 -0
  50. package/server/lib/audit-query.js +257 -0
  51. package/server/lib/audit-query.test.js +352 -0
  52. package/server/lib/audit-storage.js +269 -0
  53. package/server/lib/audit-storage.test.js +272 -0
  54. package/server/lib/auth-system.test.js +4 -1
  55. package/server/lib/boundary-detector.js +427 -0
  56. package/server/lib/boundary-detector.test.js +320 -0
  57. package/server/lib/budget-alerts.js +138 -0
  58. package/server/lib/budget-alerts.test.js +235 -0
  59. package/server/lib/bulk-repo-init.js +342 -0
  60. package/server/lib/bulk-repo-init.test.js +388 -0
  61. package/server/lib/candidates-tracker.js +210 -0
  62. package/server/lib/candidates-tracker.test.js +300 -0
  63. package/server/lib/checkpoint-manager.js +251 -0
  64. package/server/lib/checkpoint-manager.test.js +474 -0
  65. package/server/lib/circular-detector.js +337 -0
  66. package/server/lib/circular-detector.test.js +353 -0
  67. package/server/lib/cohesion-analyzer.js +310 -0
  68. package/server/lib/cohesion-analyzer.test.js +447 -0
  69. package/server/lib/compliance-checklist.js +866 -0
  70. package/server/lib/compliance-checklist.test.js +476 -0
  71. package/server/lib/compliance-command.js +616 -0
  72. package/server/lib/compliance-command.test.js +551 -0
  73. package/server/lib/compliance-reporter.js +692 -0
  74. package/server/lib/compliance-reporter.test.js +707 -0
  75. package/server/lib/contract-testing.js +625 -0
  76. package/server/lib/contract-testing.test.js +342 -0
  77. package/server/lib/conversion-planner.js +469 -0
  78. package/server/lib/conversion-planner.test.js +361 -0
  79. package/server/lib/convert-command.js +351 -0
  80. package/server/lib/convert-command.test.js +608 -0
  81. package/server/lib/coupling-calculator.js +189 -0
  82. package/server/lib/coupling-calculator.test.js +509 -0
  83. package/server/lib/data-flow-doc.js +665 -0
  84. package/server/lib/data-flow-doc.test.js +659 -0
  85. package/server/lib/dependency-graph.js +367 -0
  86. package/server/lib/dependency-graph.test.js +516 -0
  87. package/server/lib/duplication-detector.js +349 -0
  88. package/server/lib/duplication-detector.test.js +401 -0
  89. package/server/lib/ephemeral-storage.js +249 -0
  90. package/server/lib/ephemeral-storage.test.js +254 -0
  91. package/server/lib/evidence-collector.js +627 -0
  92. package/server/lib/evidence-collector.test.js +901 -0
  93. package/server/lib/example-service.js +616 -0
  94. package/server/lib/example-service.test.js +397 -0
  95. package/server/lib/flow-diagram-generator.js +474 -0
  96. package/server/lib/flow-diagram-generator.test.js +446 -0
  97. package/server/lib/idp-manager.js +626 -0
  98. package/server/lib/idp-manager.test.js +587 -0
  99. package/server/lib/impact-scorer.js +184 -0
  100. package/server/lib/impact-scorer.test.js +211 -0
  101. package/server/lib/memory-exclusion.js +326 -0
  102. package/server/lib/memory-exclusion.test.js +241 -0
  103. package/server/lib/mermaid-generator.js +358 -0
  104. package/server/lib/mermaid-generator.test.js +301 -0
  105. package/server/lib/messaging-patterns.js +750 -0
  106. package/server/lib/messaging-patterns.test.js +213 -0
  107. package/server/lib/mfa-handler.js +452 -0
  108. package/server/lib/mfa-handler.test.js +490 -0
  109. package/server/lib/microservice-template.js +386 -0
  110. package/server/lib/microservice-template.test.js +325 -0
  111. package/server/lib/new-project-microservice.js +450 -0
  112. package/server/lib/new-project-microservice.test.js +600 -0
  113. package/server/lib/oauth-flow.js +375 -0
  114. package/server/lib/oauth-flow.test.js +487 -0
  115. package/server/lib/oauth-registry.js +190 -0
  116. package/server/lib/oauth-registry.test.js +306 -0
  117. package/server/lib/readme-generator.js +490 -0
  118. package/server/lib/readme-generator.test.js +493 -0
  119. package/server/lib/refactor-command.js +326 -0
  120. package/server/lib/refactor-command.test.js +528 -0
  121. package/server/lib/refactor-executor.js +254 -0
  122. package/server/lib/refactor-executor.test.js +305 -0
  123. package/server/lib/refactor-observer.js +292 -0
  124. package/server/lib/refactor-observer.test.js +422 -0
  125. package/server/lib/refactor-progress.js +193 -0
  126. package/server/lib/refactor-progress.test.js +251 -0
  127. package/server/lib/refactor-reporter.js +237 -0
  128. package/server/lib/refactor-reporter.test.js +247 -0
  129. package/server/lib/repo-dependency-tracker.js +261 -0
  130. package/server/lib/repo-dependency-tracker.test.js +350 -0
  131. package/server/lib/retention-policy.js +281 -0
  132. package/server/lib/retention-policy.test.js +486 -0
  133. package/server/lib/role-mapper.js +236 -0
  134. package/server/lib/role-mapper.test.js +395 -0
  135. package/server/lib/saml-provider.js +765 -0
  136. package/server/lib/saml-provider.test.js +643 -0
  137. package/server/lib/security-policy-generator.js +682 -0
  138. package/server/lib/security-policy-generator.test.js +544 -0
  139. package/server/lib/semantic-analyzer.js +198 -0
  140. package/server/lib/semantic-analyzer.test.js +474 -0
  141. package/server/lib/sensitive-detector.js +112 -0
  142. package/server/lib/sensitive-detector.test.js +209 -0
  143. package/server/lib/service-interaction-diagram.js +700 -0
  144. package/server/lib/service-interaction-diagram.test.js +638 -0
  145. package/server/lib/service-scaffold.js +486 -0
  146. package/server/lib/service-scaffold.test.js +373 -0
  147. package/server/lib/service-summary.js +553 -0
  148. package/server/lib/service-summary.test.js +619 -0
  149. package/server/lib/session-purge.js +460 -0
  150. package/server/lib/session-purge.test.js +312 -0
  151. package/server/lib/shared-kernel.js +578 -0
  152. package/server/lib/shared-kernel.test.js +255 -0
  153. package/server/lib/sso-command.js +544 -0
  154. package/server/lib/sso-command.test.js +552 -0
  155. package/server/lib/sso-session.js +492 -0
  156. package/server/lib/sso-session.test.js +670 -0
  157. package/server/lib/traefik-config.js +282 -0
  158. package/server/lib/traefik-config.test.js +312 -0
  159. package/server/lib/usage-command.js +218 -0
  160. package/server/lib/usage-command.test.js +391 -0
  161. package/server/lib/usage-formatter.js +192 -0
  162. package/server/lib/usage-formatter.test.js +267 -0
  163. package/server/lib/usage-history.js +122 -0
  164. package/server/lib/usage-history.test.js +206 -0
  165. package/server/lib/workspace-command.js +249 -0
  166. package/server/lib/workspace-command.test.js +264 -0
  167. package/server/lib/workspace-config.js +270 -0
  168. package/server/lib/workspace-config.test.js +312 -0
  169. package/server/lib/workspace-docs-command.js +547 -0
  170. package/server/lib/workspace-docs-command.test.js +692 -0
  171. package/server/lib/workspace-memory.js +451 -0
  172. package/server/lib/workspace-memory.test.js +403 -0
  173. package/server/lib/workspace-scanner.js +452 -0
  174. package/server/lib/workspace-scanner.test.js +677 -0
  175. package/server/lib/workspace-test-runner.js +315 -0
  176. package/server/lib/workspace-test-runner.test.js +294 -0
  177. package/server/lib/zero-retention-command.js +439 -0
  178. package/server/lib/zero-retention-command.test.js +448 -0
  179. package/server/lib/zero-retention.js +322 -0
  180. package/server/lib/zero-retention.test.js +258 -0
  181. package/server/package-lock.json +14 -0
  182. package/server/package.json +1 -0
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Candidates Tracker Tests
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+
7
+ describe('CandidatesTracker', () => {
8
+ describe('file creation', () => {
9
+ it('creates file if not exists', async () => {
10
+ const { CandidatesTracker } = await import('./candidates-tracker.js');
11
+
12
+ const writeFileMock = vi.fn().mockResolvedValue();
13
+ const readFileMock = vi.fn().mockRejectedValue(new Error('ENOENT'));
14
+ const mkdirMock = vi.fn().mockResolvedValue();
15
+
16
+ const tracker = new CandidatesTracker({
17
+ readFile: readFileMock,
18
+ writeFile: writeFileMock,
19
+ mkdir: mkdirMock,
20
+ });
21
+
22
+ await tracker.add([
23
+ { file: 'test.js', startLine: 10, description: 'Test issue', impact: 85 },
24
+ ]);
25
+
26
+ expect(writeFileMock).toHaveBeenCalled();
27
+ expect(mkdirMock).toHaveBeenCalled();
28
+ });
29
+ });
30
+
31
+ describe('priority sections', () => {
32
+ it('appends new candidates to correct priority section', async () => {
33
+ const { CandidatesTracker } = await import('./candidates-tracker.js');
34
+
35
+ let savedContent = '';
36
+ const writeFileMock = vi.fn().mockImplementation((path, content) => {
37
+ savedContent = content;
38
+ return Promise.resolve();
39
+ });
40
+ const readFileMock = vi.fn().mockRejectedValue(new Error('ENOENT'));
41
+ const mkdirMock = vi.fn().mockResolvedValue();
42
+
43
+ const tracker = new CandidatesTracker({
44
+ readFile: readFileMock,
45
+ writeFile: writeFileMock,
46
+ mkdir: mkdirMock,
47
+ });
48
+
49
+ await tracker.add([
50
+ { file: 'high.js', startLine: 1, description: 'High priority', impact: 90 },
51
+ { file: 'medium.js', startLine: 1, description: 'Medium priority', impact: 65 },
52
+ { file: 'low.js', startLine: 1, description: 'Low priority', impact: 30 },
53
+ ]);
54
+
55
+ expect(savedContent).toContain('High Priority');
56
+ expect(savedContent).toContain('high.js:1');
57
+ expect(savedContent).toContain('Medium Priority');
58
+ expect(savedContent).toContain('medium.js:1');
59
+ expect(savedContent).toContain('Low Priority');
60
+ expect(savedContent).toContain('low.js:1');
61
+ });
62
+
63
+ it('correctly categorizes by impact score', async () => {
64
+ const { CandidatesTracker } = await import('./candidates-tracker.js');
65
+ const tracker = new CandidatesTracker({});
66
+
67
+ expect(tracker.getTier(90)).toBe('high');
68
+ expect(tracker.getTier(80)).toBe('high');
69
+ expect(tracker.getTier(79)).toBe('medium');
70
+ expect(tracker.getTier(50)).toBe('medium');
71
+ expect(tracker.getTier(49)).toBe('low');
72
+ });
73
+ });
74
+
75
+ describe('deduplication', () => {
76
+ it('deduplicates by file:line key', async () => {
77
+ const { CandidatesTracker } = await import('./candidates-tracker.js');
78
+
79
+ const existingContent = `# Refactor Candidates
80
+
81
+ ## High Priority (Impact 80+)
82
+
83
+ - [ ] test.js:10 - Existing issue (Impact: 85)
84
+
85
+ ## Medium Priority (Impact 50-79)
86
+
87
+ _None_
88
+
89
+ ## Low Priority (Impact <50)
90
+
91
+ _None_
92
+ `;
93
+
94
+ let savedContent = '';
95
+ const writeFileMock = vi.fn().mockImplementation((path, content) => {
96
+ savedContent = content;
97
+ return Promise.resolve();
98
+ });
99
+ const readFileMock = vi.fn().mockResolvedValue(existingContent);
100
+ const mkdirMock = vi.fn().mockResolvedValue();
101
+
102
+ const tracker = new CandidatesTracker({
103
+ readFile: readFileMock,
104
+ writeFile: writeFileMock,
105
+ mkdir: mkdirMock,
106
+ });
107
+
108
+ await tracker.add([
109
+ { file: 'test.js', startLine: 10, description: 'Updated issue', impact: 90 },
110
+ ]);
111
+
112
+ // Should only have one entry for test.js:10
113
+ const matches = savedContent.match(/test\.js:10/g);
114
+ expect(matches).toHaveLength(1);
115
+ expect(savedContent).toContain('Updated issue');
116
+ expect(savedContent).toContain('Impact: 90');
117
+ });
118
+ });
119
+
120
+ describe('impact score updates', () => {
121
+ it('updates impact scores on re-analysis', async () => {
122
+ const { CandidatesTracker } = await import('./candidates-tracker.js');
123
+
124
+ const existingContent = `# Refactor Candidates
125
+
126
+ ## High Priority (Impact 80+)
127
+
128
+ - [ ] test.js:10 - Old description (Impact: 85)
129
+
130
+ ## Medium Priority (Impact 50-79)
131
+
132
+ _None_
133
+
134
+ ## Low Priority (Impact <50)
135
+
136
+ _None_
137
+ `;
138
+
139
+ let savedContent = '';
140
+ const tracker = new CandidatesTracker({
141
+ readFile: vi.fn().mockResolvedValue(existingContent),
142
+ writeFile: vi.fn().mockImplementation((p, c) => { savedContent = c; return Promise.resolve(); }),
143
+ mkdir: vi.fn().mockResolvedValue(),
144
+ });
145
+
146
+ await tracker.add([
147
+ { file: 'test.js', startLine: 10, description: 'New description', impact: 95 },
148
+ ]);
149
+
150
+ expect(savedContent).toContain('Impact: 95');
151
+ expect(savedContent).toContain('New description');
152
+ });
153
+ });
154
+
155
+ describe('completion marking', () => {
156
+ it('marks candidate as complete after refactoring', async () => {
157
+ const { CandidatesTracker } = await import('./candidates-tracker.js');
158
+
159
+ const existingContent = `# Refactor Candidates
160
+
161
+ ## High Priority (Impact 80+)
162
+
163
+ - [ ] test.js:10 - Test issue (Impact: 85)
164
+
165
+ ## Medium Priority (Impact 50-79)
166
+
167
+ _None_
168
+
169
+ ## Low Priority (Impact <50)
170
+
171
+ _None_
172
+ `;
173
+
174
+ let savedContent = '';
175
+ const tracker = new CandidatesTracker({
176
+ readFile: vi.fn().mockResolvedValue(existingContent),
177
+ writeFile: vi.fn().mockImplementation((p, c) => { savedContent = c; return Promise.resolve(); }),
178
+ mkdir: vi.fn().mockResolvedValue(),
179
+ });
180
+
181
+ await tracker.markComplete('test.js', 10);
182
+
183
+ expect(savedContent).toContain('[x] test.js:10');
184
+ });
185
+ });
186
+
187
+ describe('notes preservation', () => {
188
+ it('preserves manual notes in file', async () => {
189
+ const { CandidatesTracker } = await import('./candidates-tracker.js');
190
+
191
+ const existingContent = `# Refactor Candidates
192
+
193
+ ## High Priority (Impact 80+)
194
+
195
+ - [ ] test.js:10 - Test issue (Impact: 85)
196
+
197
+ ## Medium Priority (Impact 50-79)
198
+
199
+ _None_
200
+
201
+ ## Low Priority (Impact <50)
202
+
203
+ _None_
204
+
205
+ ## Notes
206
+
207
+ This is a manual note that should be preserved.
208
+ Another line of notes.
209
+ `;
210
+
211
+ let savedContent = '';
212
+ const tracker = new CandidatesTracker({
213
+ readFile: vi.fn().mockResolvedValue(existingContent),
214
+ writeFile: vi.fn().mockImplementation((p, c) => { savedContent = c; return Promise.resolve(); }),
215
+ mkdir: vi.fn().mockResolvedValue(),
216
+ });
217
+
218
+ await tracker.add([
219
+ { file: 'new.js', startLine: 5, description: 'New issue', impact: 70 },
220
+ ]);
221
+
222
+ expect(savedContent).toContain('## Notes');
223
+ expect(savedContent).toContain('manual note that should be preserved');
224
+ });
225
+ });
226
+
227
+ describe('parsing', () => {
228
+ it('parses existing candidates correctly', async () => {
229
+ const { CandidatesTracker } = await import('./candidates-tracker.js');
230
+
231
+ const content = `# Refactor Candidates
232
+
233
+ ## High Priority (Impact 80+)
234
+
235
+ - [ ] src/api.js:10-25 - Extract validation (Impact: 85)
236
+ - [x] src/utils.js:5 - Rename variable (Impact: 82)
237
+
238
+ ## Medium Priority (Impact 50-79)
239
+
240
+ - [ ] src/helpers.js:30 - Simplify logic (Impact: 65)
241
+
242
+ ## Low Priority (Impact <50)
243
+
244
+ _None_
245
+ `;
246
+
247
+ const tracker = new CandidatesTracker({
248
+ readFile: vi.fn().mockResolvedValue(content),
249
+ writeFile: vi.fn().mockResolvedValue(),
250
+ mkdir: vi.fn().mockResolvedValue(),
251
+ });
252
+
253
+ const data = await tracker.load();
254
+
255
+ expect(data.high).toHaveLength(2);
256
+ expect(data.high[0].file).toBe('src/api.js');
257
+ expect(data.high[0].startLine).toBe(10);
258
+ expect(data.high[0].endLine).toBe(25);
259
+ expect(data.high[0].completed).toBe(false);
260
+ expect(data.high[1].completed).toBe(true);
261
+ expect(data.medium).toHaveLength(1);
262
+ expect(data.low).toHaveLength(0);
263
+ });
264
+ });
265
+
266
+ describe('formatting', () => {
267
+ it('formats line ranges correctly', async () => {
268
+ const { CandidatesTracker } = await import('./candidates-tracker.js');
269
+ const tracker = new CandidatesTracker({});
270
+
271
+ const line = tracker.formatCandidate({
272
+ completed: false,
273
+ file: 'test.js',
274
+ startLine: 10,
275
+ endLine: 20,
276
+ description: 'Test',
277
+ impact: 85,
278
+ });
279
+
280
+ expect(line).toContain('test.js:10-20');
281
+ });
282
+
283
+ it('formats single line correctly', async () => {
284
+ const { CandidatesTracker } = await import('./candidates-tracker.js');
285
+ const tracker = new CandidatesTracker({});
286
+
287
+ const line = tracker.formatCandidate({
288
+ completed: false,
289
+ file: 'test.js',
290
+ startLine: 10,
291
+ endLine: 10,
292
+ description: 'Test',
293
+ impact: 85,
294
+ });
295
+
296
+ expect(line).toContain('test.js:10 -');
297
+ expect(line).not.toContain('10-10');
298
+ });
299
+ });
300
+ });
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Checkpoint Manager
3
+ * Create and manage git-based checkpoints for safe refactoring
4
+ */
5
+
6
+ const { execSync } = require('child_process');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ class CheckpointManager {
11
+ constructor(options = {}) {
12
+ this.exec = options.exec || this.defaultExec.bind(this);
13
+ this.readFile = options.readFile || this.defaultReadFile.bind(this);
14
+ this.writeFile = options.writeFile || this.defaultWriteFile.bind(this);
15
+ this.stateFile = options.stateFile || '.tlc/checkpoint.json';
16
+ }
17
+
18
+ /**
19
+ * Default exec implementation
20
+ */
21
+ async defaultExec(command) {
22
+ return new Promise((resolve, reject) => {
23
+ try {
24
+ const stdout = execSync(command, { encoding: 'utf-8' });
25
+ resolve({ stdout });
26
+ } catch (error) {
27
+ reject(error);
28
+ }
29
+ });
30
+ }
31
+
32
+ /**
33
+ * Default read file implementation
34
+ */
35
+ async defaultReadFile(filePath) {
36
+ return fs.promises.readFile(filePath, 'utf-8');
37
+ }
38
+
39
+ /**
40
+ * Default write file implementation
41
+ */
42
+ async defaultWriteFile(filePath, content) {
43
+ const dir = path.dirname(filePath);
44
+ await fs.promises.mkdir(dir, { recursive: true });
45
+ await fs.promises.writeFile(filePath, content, 'utf-8');
46
+ }
47
+
48
+ /**
49
+ * Create a new checkpoint
50
+ * @param {Object} options - Creation options
51
+ * @returns {Object} Checkpoint info
52
+ */
53
+ async create(options = {}) {
54
+ // Check for existing checkpoint
55
+ if (!options.force) {
56
+ const existing = await this.load();
57
+ if (existing) {
58
+ throw new Error('Checkpoint already exists. Use rollback() first or force: true');
59
+ }
60
+ }
61
+
62
+ // Get current branch
63
+ let originalBranch;
64
+ let wasDetached = false;
65
+
66
+ try {
67
+ const { stdout } = await this.exec('git branch --show-current');
68
+ originalBranch = stdout.trim();
69
+
70
+ if (!originalBranch) {
71
+ // Detached HEAD
72
+ const { stdout: headCommit } = await this.exec('git rev-parse HEAD');
73
+ originalBranch = headCommit.trim();
74
+ wasDetached = true;
75
+ }
76
+ } catch (error) {
77
+ throw new Error('Failed to get current branch: ' + error.message);
78
+ }
79
+
80
+ // Check for uncommitted changes
81
+ const { stdout: status } = await this.exec('git status --porcelain');
82
+ const hasChanges = status.trim().length > 0;
83
+ let hasStash = false;
84
+ let stashRef = null;
85
+
86
+ // Stash changes if any
87
+ if (hasChanges) {
88
+ await this.exec('git stash push -m "TLC checkpoint stash"');
89
+ hasStash = true;
90
+ stashRef = 'stash@{0}';
91
+ }
92
+
93
+ // Create refactor branch
94
+ const timestamp = Date.now();
95
+ let branch = `refactor/${timestamp}`;
96
+ let attempts = 0;
97
+
98
+ while (attempts < 3) {
99
+ try {
100
+ await this.exec(`git checkout -b ${branch}`);
101
+ break;
102
+ } catch (error) {
103
+ if (error.message.includes('already exists')) {
104
+ attempts++;
105
+ branch = `refactor/${timestamp}-${attempts}`;
106
+ } else {
107
+ throw error;
108
+ }
109
+ }
110
+ }
111
+
112
+ // Get commit hash
113
+ const { stdout: commitHash } = await this.exec('git rev-parse HEAD');
114
+
115
+ const checkpoint = {
116
+ id: `checkpoint-${timestamp}`,
117
+ branch,
118
+ originalBranch,
119
+ wasDetached,
120
+ hasStash,
121
+ stashRef,
122
+ commitHash: commitHash.trim(),
123
+ createdAt: new Date(),
124
+ };
125
+
126
+ // Save checkpoint state
127
+ await this.save(checkpoint);
128
+
129
+ return checkpoint;
130
+ }
131
+
132
+ /**
133
+ * Rollback to checkpoint state
134
+ * @param {Object} checkpoint - Checkpoint to rollback to
135
+ */
136
+ async rollback(checkpoint) {
137
+ // Checkout original branch
138
+ await this.exec(`git checkout ${checkpoint.originalBranch}`);
139
+
140
+ // Delete refactor branch
141
+ try {
142
+ await this.exec(`git branch -D ${checkpoint.branch}`);
143
+ } catch (error) {
144
+ // Branch might not exist
145
+ }
146
+
147
+ // Pop stash if we stashed
148
+ if (checkpoint.hasStash) {
149
+ try {
150
+ await this.exec('git stash pop');
151
+ } catch (error) {
152
+ // Stash might be empty or conflict
153
+ }
154
+ }
155
+
156
+ // Clear checkpoint state
157
+ await this.clear();
158
+ }
159
+
160
+ /**
161
+ * Commit current changes
162
+ * @param {string} message - Commit message
163
+ */
164
+ async commit(message) {
165
+ await this.exec('git add -A');
166
+ await this.exec(`git commit -m "${message.replace(/"/g, '\\"')}"`);
167
+ }
168
+
169
+ /**
170
+ * Merge refactor branch back to original
171
+ * @param {Object} checkpoint - Checkpoint info
172
+ * @param {Object} options - Merge options
173
+ */
174
+ async merge(checkpoint, options = {}) {
175
+ // Checkout original branch
176
+ await this.exec(`git checkout ${checkpoint.originalBranch}`);
177
+
178
+ // Merge refactor branch
179
+ await this.exec(`git merge ${checkpoint.branch}`);
180
+
181
+ // Cleanup if requested
182
+ if (options.cleanup) {
183
+ await this.exec(`git branch -d ${checkpoint.branch}`);
184
+ }
185
+
186
+ // Clear checkpoint state
187
+ await this.clear();
188
+ }
189
+
190
+ /**
191
+ * Save checkpoint state to file
192
+ */
193
+ async save(checkpoint) {
194
+ await this.writeFile(this.stateFile, JSON.stringify(checkpoint, null, 2));
195
+ }
196
+
197
+ /**
198
+ * Load existing checkpoint state
199
+ * @returns {Object|null} Checkpoint or null if none exists
200
+ */
201
+ async load() {
202
+ try {
203
+ const content = await this.readFile(this.stateFile);
204
+ return JSON.parse(content);
205
+ } catch (error) {
206
+ return null;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Clear checkpoint state
212
+ */
213
+ async clear() {
214
+ try {
215
+ await fs.promises.unlink(this.stateFile);
216
+ } catch (error) {
217
+ // File might not exist
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Get current checkpoint status
223
+ * @returns {Object} Status info
224
+ */
225
+ async status() {
226
+ const checkpoint = await this.load();
227
+
228
+ if (!checkpoint) {
229
+ return {
230
+ active: false,
231
+ };
232
+ }
233
+
234
+ // Get current branch
235
+ const { stdout: currentBranch } = await this.exec('git branch --show-current');
236
+
237
+ // Check for uncommitted changes
238
+ const { stdout: status } = await this.exec('git status --porcelain');
239
+
240
+ return {
241
+ active: true,
242
+ branch: checkpoint.branch,
243
+ originalBranch: checkpoint.originalBranch,
244
+ isOnRefactorBranch: currentBranch.trim() === checkpoint.branch,
245
+ hasUncommittedChanges: status.trim().length > 0,
246
+ createdAt: checkpoint.createdAt,
247
+ };
248
+ }
249
+ }
250
+
251
+ module.exports = { CheckpointManager };