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,388 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ const { BulkRepoInit } = await import('./bulk-repo-init.js');
7
+
8
+ describe('BulkRepoInit', () => {
9
+ let tempDir;
10
+ let bulkInit;
11
+
12
+ beforeEach(() => {
13
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bulk-repo-init-test-'));
14
+ });
15
+
16
+ afterEach(() => {
17
+ fs.rmSync(tempDir, { recursive: true, force: true });
18
+ });
19
+
20
+ describe('detection', () => {
21
+ it('detects repo without .tlc.json', () => {
22
+ // Create a repo without TLC config
23
+ const repoPath = path.join(tempDir, 'my-repo');
24
+ fs.mkdirSync(repoPath);
25
+ fs.writeFileSync(path.join(repoPath, 'package.json'), '{"name": "my-repo"}');
26
+
27
+ bulkInit = new BulkRepoInit(tempDir);
28
+ const repos = bulkInit.findUninitializedRepos();
29
+
30
+ expect(repos).toContain('my-repo');
31
+ });
32
+
33
+ it('skips repos that already have .tlc.json', () => {
34
+ // Create a repo WITH TLC config
35
+ const repoPath = path.join(tempDir, 'initialized-repo');
36
+ fs.mkdirSync(repoPath);
37
+ fs.writeFileSync(path.join(repoPath, 'package.json'), '{"name": "initialized-repo"}');
38
+ fs.writeFileSync(path.join(repoPath, '.tlc.json'), '{}');
39
+
40
+ bulkInit = new BulkRepoInit(tempDir);
41
+ const repos = bulkInit.findUninitializedRepos();
42
+
43
+ expect(repos).not.toContain('initialized-repo');
44
+ });
45
+ });
46
+
47
+ describe('project type detection', () => {
48
+ it('detects Node.js project (package.json)', () => {
49
+ const repoPath = path.join(tempDir, 'node-repo');
50
+ fs.mkdirSync(repoPath);
51
+ fs.writeFileSync(path.join(repoPath, 'package.json'), '{"name": "node-repo"}');
52
+
53
+ bulkInit = new BulkRepoInit(tempDir);
54
+ const projectType = bulkInit.detectProjectType(repoPath);
55
+
56
+ expect(projectType).toBe('node');
57
+ });
58
+
59
+ it('detects Python project (pyproject.toml)', () => {
60
+ const repoPath = path.join(tempDir, 'python-repo');
61
+ fs.mkdirSync(repoPath);
62
+ fs.writeFileSync(path.join(repoPath, 'pyproject.toml'), '[project]\nname = "python-repo"');
63
+
64
+ bulkInit = new BulkRepoInit(tempDir);
65
+ const projectType = bulkInit.detectProjectType(repoPath);
66
+
67
+ expect(projectType).toBe('python');
68
+ });
69
+
70
+ it('detects Python project (setup.py)', () => {
71
+ const repoPath = path.join(tempDir, 'python-repo-2');
72
+ fs.mkdirSync(repoPath);
73
+ fs.writeFileSync(path.join(repoPath, 'setup.py'), 'from setuptools import setup\nsetup()');
74
+
75
+ bulkInit = new BulkRepoInit(tempDir);
76
+ const projectType = bulkInit.detectProjectType(repoPath);
77
+
78
+ expect(projectType).toBe('python');
79
+ });
80
+
81
+ it('detects Go project (go.mod)', () => {
82
+ const repoPath = path.join(tempDir, 'go-repo');
83
+ fs.mkdirSync(repoPath);
84
+ fs.writeFileSync(path.join(repoPath, 'go.mod'), 'module example.com/go-repo\n\ngo 1.21');
85
+
86
+ bulkInit = new BulkRepoInit(tempDir);
87
+ const projectType = bulkInit.detectProjectType(repoPath);
88
+
89
+ expect(projectType).toBe('go');
90
+ });
91
+
92
+ it('returns unknown for unrecognized project type', () => {
93
+ const repoPath = path.join(tempDir, 'unknown-repo');
94
+ fs.mkdirSync(repoPath);
95
+ fs.writeFileSync(path.join(repoPath, 'README.md'), '# Unknown Project');
96
+
97
+ bulkInit = new BulkRepoInit(tempDir);
98
+ const projectType = bulkInit.detectProjectType(repoPath);
99
+
100
+ expect(projectType).toBe('unknown');
101
+ });
102
+ });
103
+
104
+ describe('test framework detection', () => {
105
+ it('infers vitest from vite.config', () => {
106
+ const repoPath = path.join(tempDir, 'vitest-repo');
107
+ fs.mkdirSync(repoPath);
108
+ fs.writeFileSync(path.join(repoPath, 'package.json'), '{"name": "vitest-repo"}');
109
+ fs.writeFileSync(path.join(repoPath, 'vite.config.js'), 'export default {}');
110
+
111
+ bulkInit = new BulkRepoInit(tempDir);
112
+ const framework = bulkInit.detectTestFramework(repoPath);
113
+
114
+ expect(framework).toBe('vitest');
115
+ });
116
+
117
+ it('infers vitest from vitest.config', () => {
118
+ const repoPath = path.join(tempDir, 'vitest-repo-2');
119
+ fs.mkdirSync(repoPath);
120
+ fs.writeFileSync(path.join(repoPath, 'package.json'), '{"name": "vitest-repo-2"}');
121
+ fs.writeFileSync(path.join(repoPath, 'vitest.config.js'), 'export default {}');
122
+
123
+ bulkInit = new BulkRepoInit(tempDir);
124
+ const framework = bulkInit.detectTestFramework(repoPath);
125
+
126
+ expect(framework).toBe('vitest');
127
+ });
128
+
129
+ it('infers jest from jest.config', () => {
130
+ const repoPath = path.join(tempDir, 'jest-repo');
131
+ fs.mkdirSync(repoPath);
132
+ fs.writeFileSync(path.join(repoPath, 'package.json'), '{"name": "jest-repo"}');
133
+ fs.writeFileSync(path.join(repoPath, 'jest.config.js'), 'module.exports = {}');
134
+
135
+ bulkInit = new BulkRepoInit(tempDir);
136
+ const framework = bulkInit.detectTestFramework(repoPath);
137
+
138
+ expect(framework).toBe('jest');
139
+ });
140
+
141
+ it('infers pytest from pytest.ini', () => {
142
+ const repoPath = path.join(tempDir, 'pytest-repo');
143
+ fs.mkdirSync(repoPath);
144
+ fs.writeFileSync(path.join(repoPath, 'pyproject.toml'), '[project]\nname = "pytest-repo"');
145
+ fs.writeFileSync(path.join(repoPath, 'pytest.ini'), '[pytest]\ntestpaths = tests');
146
+
147
+ bulkInit = new BulkRepoInit(tempDir);
148
+ const framework = bulkInit.detectTestFramework(repoPath);
149
+
150
+ expect(framework).toBe('pytest');
151
+ });
152
+
153
+ it('infers pytest from pyproject.toml with pytest config', () => {
154
+ const repoPath = path.join(tempDir, 'pytest-repo-2');
155
+ fs.mkdirSync(repoPath);
156
+ fs.writeFileSync(
157
+ path.join(repoPath, 'pyproject.toml'),
158
+ '[project]\nname = "pytest-repo-2"\n\n[tool.pytest.ini_options]\ntestpaths = ["tests"]'
159
+ );
160
+
161
+ bulkInit = new BulkRepoInit(tempDir);
162
+ const framework = bulkInit.detectTestFramework(repoPath);
163
+
164
+ expect(framework).toBe('pytest');
165
+ });
166
+
167
+ it('infers mocha from mocha config', () => {
168
+ const repoPath = path.join(tempDir, 'mocha-repo');
169
+ fs.mkdirSync(repoPath);
170
+ fs.writeFileSync(path.join(repoPath, 'package.json'), '{"name": "mocha-repo"}');
171
+ fs.writeFileSync(path.join(repoPath, '.mocharc.json'), '{}');
172
+
173
+ bulkInit = new BulkRepoInit(tempDir);
174
+ const framework = bulkInit.detectTestFramework(repoPath);
175
+
176
+ expect(framework).toBe('mocha');
177
+ });
178
+
179
+ it('infers test framework from package.json devDependencies', () => {
180
+ const repoPath = path.join(tempDir, 'jest-dep-repo');
181
+ fs.mkdirSync(repoPath);
182
+ fs.writeFileSync(
183
+ path.join(repoPath, 'package.json'),
184
+ JSON.stringify({
185
+ name: 'jest-dep-repo',
186
+ devDependencies: { jest: '^29.0.0' },
187
+ })
188
+ );
189
+
190
+ bulkInit = new BulkRepoInit(tempDir);
191
+ const framework = bulkInit.detectTestFramework(repoPath);
192
+
193
+ expect(framework).toBe('jest');
194
+ });
195
+
196
+ it('returns null for unknown test framework', () => {
197
+ const repoPath = path.join(tempDir, 'no-test-repo');
198
+ fs.mkdirSync(repoPath);
199
+ fs.writeFileSync(path.join(repoPath, 'package.json'), '{"name": "no-test-repo"}');
200
+
201
+ bulkInit = new BulkRepoInit(tempDir);
202
+ const framework = bulkInit.detectTestFramework(repoPath);
203
+
204
+ expect(framework).toBeNull();
205
+ });
206
+ });
207
+
208
+ describe('initialization', () => {
209
+ it('creates .tlc.json with inferred settings', () => {
210
+ const repoPath = path.join(tempDir, 'init-repo');
211
+ fs.mkdirSync(repoPath);
212
+ fs.writeFileSync(
213
+ path.join(repoPath, 'package.json'),
214
+ JSON.stringify({ name: 'init-repo' })
215
+ );
216
+ fs.writeFileSync(path.join(repoPath, 'jest.config.js'), 'module.exports = {}');
217
+
218
+ bulkInit = new BulkRepoInit(tempDir);
219
+ bulkInit.initializeRepo('init-repo');
220
+
221
+ const tlcConfigPath = path.join(repoPath, '.tlc.json');
222
+ expect(fs.existsSync(tlcConfigPath)).toBe(true);
223
+
224
+ const config = JSON.parse(fs.readFileSync(tlcConfigPath, 'utf-8'));
225
+ expect(config.project).toBe('init-repo');
226
+ expect(config.testFrameworks.primary).toBe('jest');
227
+ });
228
+
229
+ it('creates minimal .tlc.json for each repo', () => {
230
+ const repoPath = path.join(tempDir, 'minimal-repo');
231
+ fs.mkdirSync(repoPath);
232
+ fs.writeFileSync(path.join(repoPath, 'package.json'), '{"name": "minimal-repo"}');
233
+
234
+ bulkInit = new BulkRepoInit(tempDir);
235
+ bulkInit.initializeRepo('minimal-repo');
236
+
237
+ const tlcConfigPath = path.join(repoPath, '.tlc.json');
238
+ const config = JSON.parse(fs.readFileSync(tlcConfigPath, 'utf-8'));
239
+
240
+ // Should have minimal required fields
241
+ expect(config).toHaveProperty('project');
242
+ expect(config).toHaveProperty('testFrameworks');
243
+ expect(config).toHaveProperty('paths');
244
+ });
245
+
246
+ it('sets project name from package.json', () => {
247
+ const repoPath = path.join(tempDir, 'named-repo');
248
+ fs.mkdirSync(repoPath);
249
+ fs.writeFileSync(
250
+ path.join(repoPath, 'package.json'),
251
+ JSON.stringify({ name: '@scope/my-package' })
252
+ );
253
+
254
+ bulkInit = new BulkRepoInit(tempDir);
255
+ bulkInit.initializeRepo('named-repo');
256
+
257
+ const tlcConfigPath = path.join(repoPath, '.tlc.json');
258
+ const config = JSON.parse(fs.readFileSync(tlcConfigPath, 'utf-8'));
259
+
260
+ expect(config.project).toBe('@scope/my-package');
261
+ });
262
+ });
263
+
264
+ describe('bulk initialization', () => {
265
+ it('initializes multiple repos at once', () => {
266
+ // Create multiple repos
267
+ ['repo-a', 'repo-b', 'repo-c'].forEach(name => {
268
+ const repoPath = path.join(tempDir, name);
269
+ fs.mkdirSync(repoPath);
270
+ fs.writeFileSync(path.join(repoPath, 'package.json'), JSON.stringify({ name }));
271
+ });
272
+
273
+ bulkInit = new BulkRepoInit(tempDir);
274
+ const result = bulkInit.initializeAll();
275
+
276
+ expect(result.initialized).toBe(3);
277
+ expect(fs.existsSync(path.join(tempDir, 'repo-a', '.tlc.json'))).toBe(true);
278
+ expect(fs.existsSync(path.join(tempDir, 'repo-b', '.tlc.json'))).toBe(true);
279
+ expect(fs.existsSync(path.join(tempDir, 'repo-c', '.tlc.json'))).toBe(true);
280
+ });
281
+
282
+ it('handles mixed project types in workspace', () => {
283
+ // Create Node repo
284
+ const nodeRepo = path.join(tempDir, 'node-app');
285
+ fs.mkdirSync(nodeRepo);
286
+ fs.writeFileSync(path.join(nodeRepo, 'package.json'), '{"name": "node-app"}');
287
+
288
+ // Create Python repo
289
+ const pythonRepo = path.join(tempDir, 'python-app');
290
+ fs.mkdirSync(pythonRepo);
291
+ fs.writeFileSync(path.join(pythonRepo, 'pyproject.toml'), '[project]\nname = "python-app"');
292
+
293
+ // Create Go repo
294
+ const goRepo = path.join(tempDir, 'go-app');
295
+ fs.mkdirSync(goRepo);
296
+ fs.writeFileSync(path.join(goRepo, 'go.mod'), 'module example.com/go-app\n\ngo 1.21');
297
+
298
+ bulkInit = new BulkRepoInit(tempDir);
299
+ const result = bulkInit.initializeAll();
300
+
301
+ expect(result.initialized).toBe(3);
302
+
303
+ // Verify each has correct project type
304
+ const nodeConfig = JSON.parse(fs.readFileSync(path.join(nodeRepo, '.tlc.json'), 'utf-8'));
305
+ expect(nodeConfig.projectType).toBe('node');
306
+
307
+ const pythonConfig = JSON.parse(fs.readFileSync(path.join(pythonRepo, '.tlc.json'), 'utf-8'));
308
+ expect(pythonConfig.projectType).toBe('python');
309
+
310
+ const goConfig = JSON.parse(fs.readFileSync(path.join(goRepo, '.tlc.json'), 'utf-8'));
311
+ expect(goConfig.projectType).toBe('go');
312
+ });
313
+
314
+ it('reports summary (X initialized, Y failed)', () => {
315
+ // Create repos - 2 that can be initialized
316
+ ['good-repo-1', 'good-repo-2'].forEach(name => {
317
+ const repoPath = path.join(tempDir, name);
318
+ fs.mkdirSync(repoPath);
319
+ fs.writeFileSync(path.join(repoPath, 'package.json'), JSON.stringify({ name }));
320
+ });
321
+
322
+ // Create a repo that already has .tlc.json (should be skipped, not failed)
323
+ const existingRepo = path.join(tempDir, 'existing-repo');
324
+ fs.mkdirSync(existingRepo);
325
+ fs.writeFileSync(path.join(existingRepo, 'package.json'), '{"name": "existing-repo"}');
326
+ fs.writeFileSync(path.join(existingRepo, '.tlc.json'), '{}');
327
+
328
+ bulkInit = new BulkRepoInit(tempDir);
329
+ const result = bulkInit.initializeAll();
330
+
331
+ expect(result.initialized).toBe(2);
332
+ expect(result.skipped).toBe(1);
333
+ expect(result.failed).toBe(0);
334
+ expect(result.total).toBe(3);
335
+ });
336
+
337
+ it('reports failures per repo', () => {
338
+ // Create a repo with read-only .tlc.json location (simulate failure)
339
+ const repoPath = path.join(tempDir, 'fail-repo');
340
+ fs.mkdirSync(repoPath);
341
+ fs.writeFileSync(path.join(repoPath, 'package.json'), '{"name": "fail-repo"}');
342
+
343
+ // Create a directory named .tlc.json to cause write failure
344
+ fs.mkdirSync(path.join(repoPath, '.tlc.json'));
345
+
346
+ bulkInit = new BulkRepoInit(tempDir);
347
+ const result = bulkInit.initializeAll();
348
+
349
+ expect(result.failed).toBe(1);
350
+ expect(result.errors).toHaveLength(1);
351
+ expect(result.errors[0].repo).toBe('fail-repo');
352
+ expect(result.errors[0].error).toBeDefined();
353
+ });
354
+
355
+ it('provides list of initialized repos in result', () => {
356
+ ['repo-x', 'repo-y'].forEach(name => {
357
+ const repoPath = path.join(tempDir, name);
358
+ fs.mkdirSync(repoPath);
359
+ fs.writeFileSync(path.join(repoPath, 'package.json'), JSON.stringify({ name }));
360
+ });
361
+
362
+ bulkInit = new BulkRepoInit(tempDir);
363
+ const result = bulkInit.initializeAll();
364
+
365
+ expect(result.repos).toContain('repo-x');
366
+ expect(result.repos).toContain('repo-y');
367
+ });
368
+ });
369
+
370
+ describe('selective initialization', () => {
371
+ it('can initialize specific repos only', () => {
372
+ // Create multiple repos
373
+ ['repo-1', 'repo-2', 'repo-3'].forEach(name => {
374
+ const repoPath = path.join(tempDir, name);
375
+ fs.mkdirSync(repoPath);
376
+ fs.writeFileSync(path.join(repoPath, 'package.json'), JSON.stringify({ name }));
377
+ });
378
+
379
+ bulkInit = new BulkRepoInit(tempDir);
380
+ const result = bulkInit.initializeRepos(['repo-1', 'repo-3']);
381
+
382
+ expect(result.initialized).toBe(2);
383
+ expect(fs.existsSync(path.join(tempDir, 'repo-1', '.tlc.json'))).toBe(true);
384
+ expect(fs.existsSync(path.join(tempDir, 'repo-2', '.tlc.json'))).toBe(false);
385
+ expect(fs.existsSync(path.join(tempDir, 'repo-3', '.tlc.json'))).toBe(true);
386
+ });
387
+ });
388
+ });
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Candidates Tracker
3
+ * Maintain REFACTOR-CANDIDATES.md automatically
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ class CandidatesTracker {
10
+ constructor(options = {}) {
11
+ this.filePath = options.filePath || '.planning/REFACTOR-CANDIDATES.md';
12
+ this.readFile = options.readFile || fs.promises.readFile;
13
+ this.writeFile = options.writeFile || fs.promises.writeFile;
14
+ this.mkdir = options.mkdir || fs.promises.mkdir;
15
+ }
16
+
17
+ /**
18
+ * Load existing candidates from file
19
+ */
20
+ async load() {
21
+ try {
22
+ const content = await this.readFile(this.filePath, 'utf-8');
23
+ return this.parse(content);
24
+ } catch {
25
+ return { high: [], medium: [], low: [], notes: '' };
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Parse markdown content into structured data
31
+ */
32
+ parse(content) {
33
+ const result = { high: [], medium: [], low: [], notes: '' };
34
+ let currentSection = null;
35
+ let notesStart = -1;
36
+
37
+ const lines = content.split('\n');
38
+
39
+ for (let i = 0; i < lines.length; i++) {
40
+ const line = lines[i];
41
+
42
+ if (line.includes('High Priority') || line.includes('Impact 80+')) {
43
+ currentSection = 'high';
44
+ } else if (line.includes('Medium Priority') || line.includes('Impact 50-79')) {
45
+ currentSection = 'medium';
46
+ } else if (line.includes('Low Priority') || line.includes('Impact <50')) {
47
+ currentSection = 'low';
48
+ } else if (line.includes('## Notes') || line.includes('## Manual Notes')) {
49
+ notesStart = i;
50
+ break;
51
+ } else if (currentSection && line.startsWith('- [')) {
52
+ const candidate = this.parseLine(line);
53
+ if (candidate) {
54
+ result[currentSection].push(candidate);
55
+ }
56
+ }
57
+ }
58
+
59
+ // Preserve notes section
60
+ if (notesStart >= 0) {
61
+ result.notes = lines.slice(notesStart).join('\n');
62
+ }
63
+
64
+ return result;
65
+ }
66
+
67
+ /**
68
+ * Parse a single candidate line
69
+ */
70
+ parseLine(line) {
71
+ // Format: - [x] file.js:10-20 - Description (Impact: 85)
72
+ const match = line.match(/- \[([ x])\] (.+?):(\d+)(?:-(\d+))? - (.+?) \(Impact: (\d+)\)/);
73
+ if (!match) return null;
74
+
75
+ return {
76
+ completed: match[1] === 'x',
77
+ file: match[2],
78
+ startLine: parseInt(match[3], 10),
79
+ endLine: match[4] ? parseInt(match[4], 10) : parseInt(match[3], 10),
80
+ description: match[5],
81
+ impact: parseInt(match[6], 10),
82
+ key: `${match[2]}:${match[3]}`,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Add candidates from analysis
88
+ */
89
+ async add(candidates) {
90
+ const existing = await this.load();
91
+
92
+ for (const candidate of candidates) {
93
+ const tier = this.getTier(candidate.impact);
94
+ const key = `${candidate.file}:${candidate.startLine}`;
95
+
96
+ // Check for duplicate
97
+ const existingIndex = existing[tier].findIndex(c => c.key === key);
98
+
99
+ if (existingIndex >= 0) {
100
+ // Update impact score
101
+ existing[tier][existingIndex].impact = candidate.impact;
102
+ existing[tier][existingIndex].description = candidate.description;
103
+ } else {
104
+ // Add new
105
+ existing[tier].push({
106
+ ...candidate,
107
+ key,
108
+ completed: false,
109
+ });
110
+ }
111
+ }
112
+
113
+ // Re-sort by impact
114
+ for (const tier of ['high', 'medium', 'low']) {
115
+ existing[tier].sort((a, b) => b.impact - a.impact);
116
+ }
117
+
118
+ await this.save(existing);
119
+ return existing;
120
+ }
121
+
122
+ /**
123
+ * Mark a candidate as complete
124
+ */
125
+ async markComplete(file, line) {
126
+ const existing = await this.load();
127
+ const key = `${file}:${line}`;
128
+
129
+ for (const tier of ['high', 'medium', 'low']) {
130
+ const candidate = existing[tier].find(c => c.key === key);
131
+ if (candidate) {
132
+ candidate.completed = true;
133
+ break;
134
+ }
135
+ }
136
+
137
+ await this.save(existing);
138
+ }
139
+
140
+ /**
141
+ * Get priority tier from impact score
142
+ */
143
+ getTier(impact) {
144
+ if (impact >= 80) return 'high';
145
+ if (impact >= 50) return 'medium';
146
+ return 'low';
147
+ }
148
+
149
+ /**
150
+ * Save candidates to file
151
+ */
152
+ async save(data) {
153
+ const content = this.format(data);
154
+
155
+ // Ensure directory exists
156
+ const dir = path.dirname(this.filePath);
157
+ await this.mkdir(dir, { recursive: true });
158
+
159
+ await this.writeFile(this.filePath, content);
160
+ }
161
+
162
+ /**
163
+ * Format data as markdown
164
+ */
165
+ format(data) {
166
+ let md = '# Refactor Candidates\n\n';
167
+ md += '_Auto-generated by TLC. Run `/tlc:refactor` to process._\n\n';
168
+
169
+ md += '## High Priority (Impact 80+)\n\n';
170
+ for (const c of data.high) {
171
+ md += this.formatCandidate(c);
172
+ }
173
+ if (data.high.length === 0) md += '_None_\n';
174
+ md += '\n';
175
+
176
+ md += '## Medium Priority (Impact 50-79)\n\n';
177
+ for (const c of data.medium) {
178
+ md += this.formatCandidate(c);
179
+ }
180
+ if (data.medium.length === 0) md += '_None_\n';
181
+ md += '\n';
182
+
183
+ md += '## Low Priority (Impact <50)\n\n';
184
+ for (const c of data.low) {
185
+ md += this.formatCandidate(c);
186
+ }
187
+ if (data.low.length === 0) md += '_None_\n';
188
+ md += '\n';
189
+
190
+ // Preserve notes
191
+ if (data.notes) {
192
+ md += data.notes;
193
+ }
194
+
195
+ return md;
196
+ }
197
+
198
+ /**
199
+ * Format a single candidate as markdown line
200
+ */
201
+ formatCandidate(c) {
202
+ const checkbox = c.completed ? 'x' : ' ';
203
+ const lineRange = c.endLine && c.endLine !== c.startLine
204
+ ? `${c.startLine}-${c.endLine}`
205
+ : `${c.startLine}`;
206
+ return `- [${checkbox}] ${c.file}:${lineRange} - ${c.description} (Impact: ${c.impact})\n`;
207
+ }
208
+ }
209
+
210
+ module.exports = { CandidatesTracker };