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.
- package/dashboard/dist/components/AuditPane.d.ts +30 -0
- package/dashboard/dist/components/AuditPane.js +127 -0
- package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
- package/dashboard/dist/components/AuditPane.test.js +339 -0
- package/dashboard/dist/components/CompliancePane.d.ts +39 -0
- package/dashboard/dist/components/CompliancePane.js +96 -0
- package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
- package/dashboard/dist/components/CompliancePane.test.js +183 -0
- package/dashboard/dist/components/SSOPane.d.ts +36 -0
- package/dashboard/dist/components/SSOPane.js +71 -0
- package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
- package/dashboard/dist/components/SSOPane.test.js +155 -0
- package/dashboard/dist/components/UsagePane.d.ts +13 -0
- package/dashboard/dist/components/UsagePane.js +51 -0
- package/dashboard/dist/components/UsagePane.test.d.ts +1 -0
- package/dashboard/dist/components/UsagePane.test.js +142 -0
- package/dashboard/dist/components/WorkspaceDocsPane.d.ts +19 -0
- package/dashboard/dist/components/WorkspaceDocsPane.js +130 -0
- package/dashboard/dist/components/WorkspaceDocsPane.test.d.ts +1 -0
- package/dashboard/dist/components/WorkspaceDocsPane.test.js +242 -0
- package/dashboard/dist/components/WorkspacePane.d.ts +18 -0
- package/dashboard/dist/components/WorkspacePane.js +17 -0
- package/dashboard/dist/components/WorkspacePane.test.d.ts +1 -0
- package/dashboard/dist/components/WorkspacePane.test.js +84 -0
- package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
- package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
- package/package.json +1 -1
- package/server/lib/access-control-doc.js +541 -0
- package/server/lib/access-control-doc.test.js +672 -0
- package/server/lib/adr-generator.js +423 -0
- package/server/lib/adr-generator.test.js +586 -0
- package/server/lib/agent-progress-monitor.js +223 -0
- package/server/lib/agent-progress-monitor.test.js +202 -0
- package/server/lib/architecture-command.js +450 -0
- package/server/lib/architecture-command.test.js +754 -0
- package/server/lib/ast-analyzer.js +324 -0
- package/server/lib/ast-analyzer.test.js +437 -0
- package/server/lib/audit-attribution.js +191 -0
- package/server/lib/audit-attribution.test.js +359 -0
- package/server/lib/audit-classifier.js +202 -0
- package/server/lib/audit-classifier.test.js +209 -0
- package/server/lib/audit-command.js +275 -0
- package/server/lib/audit-command.test.js +325 -0
- package/server/lib/audit-exporter.js +380 -0
- package/server/lib/audit-exporter.test.js +464 -0
- package/server/lib/audit-logger.js +236 -0
- package/server/lib/audit-logger.test.js +364 -0
- package/server/lib/audit-query.js +257 -0
- package/server/lib/audit-query.test.js +352 -0
- package/server/lib/audit-storage.js +269 -0
- package/server/lib/audit-storage.test.js +272 -0
- package/server/lib/auth-system.test.js +4 -1
- package/server/lib/boundary-detector.js +427 -0
- package/server/lib/boundary-detector.test.js +320 -0
- package/server/lib/budget-alerts.js +138 -0
- package/server/lib/budget-alerts.test.js +235 -0
- package/server/lib/bulk-repo-init.js +342 -0
- package/server/lib/bulk-repo-init.test.js +388 -0
- package/server/lib/candidates-tracker.js +210 -0
- package/server/lib/candidates-tracker.test.js +300 -0
- package/server/lib/checkpoint-manager.js +251 -0
- package/server/lib/checkpoint-manager.test.js +474 -0
- package/server/lib/circular-detector.js +337 -0
- package/server/lib/circular-detector.test.js +353 -0
- package/server/lib/cohesion-analyzer.js +310 -0
- package/server/lib/cohesion-analyzer.test.js +447 -0
- package/server/lib/compliance-checklist.js +866 -0
- package/server/lib/compliance-checklist.test.js +476 -0
- package/server/lib/compliance-command.js +616 -0
- package/server/lib/compliance-command.test.js +551 -0
- package/server/lib/compliance-reporter.js +692 -0
- package/server/lib/compliance-reporter.test.js +707 -0
- package/server/lib/contract-testing.js +625 -0
- package/server/lib/contract-testing.test.js +342 -0
- package/server/lib/conversion-planner.js +469 -0
- package/server/lib/conversion-planner.test.js +361 -0
- package/server/lib/convert-command.js +351 -0
- package/server/lib/convert-command.test.js +608 -0
- package/server/lib/coupling-calculator.js +189 -0
- package/server/lib/coupling-calculator.test.js +509 -0
- package/server/lib/data-flow-doc.js +665 -0
- package/server/lib/data-flow-doc.test.js +659 -0
- package/server/lib/dependency-graph.js +367 -0
- package/server/lib/dependency-graph.test.js +516 -0
- package/server/lib/duplication-detector.js +349 -0
- package/server/lib/duplication-detector.test.js +401 -0
- package/server/lib/ephemeral-storage.js +249 -0
- package/server/lib/ephemeral-storage.test.js +254 -0
- package/server/lib/evidence-collector.js +627 -0
- package/server/lib/evidence-collector.test.js +901 -0
- package/server/lib/example-service.js +616 -0
- package/server/lib/example-service.test.js +397 -0
- package/server/lib/flow-diagram-generator.js +474 -0
- package/server/lib/flow-diagram-generator.test.js +446 -0
- package/server/lib/idp-manager.js +626 -0
- package/server/lib/idp-manager.test.js +587 -0
- package/server/lib/impact-scorer.js +184 -0
- package/server/lib/impact-scorer.test.js +211 -0
- package/server/lib/memory-exclusion.js +326 -0
- package/server/lib/memory-exclusion.test.js +241 -0
- package/server/lib/mermaid-generator.js +358 -0
- package/server/lib/mermaid-generator.test.js +301 -0
- package/server/lib/messaging-patterns.js +750 -0
- package/server/lib/messaging-patterns.test.js +213 -0
- package/server/lib/mfa-handler.js +452 -0
- package/server/lib/mfa-handler.test.js +490 -0
- package/server/lib/microservice-template.js +386 -0
- package/server/lib/microservice-template.test.js +325 -0
- package/server/lib/new-project-microservice.js +450 -0
- package/server/lib/new-project-microservice.test.js +600 -0
- package/server/lib/oauth-flow.js +375 -0
- package/server/lib/oauth-flow.test.js +487 -0
- package/server/lib/oauth-registry.js +190 -0
- package/server/lib/oauth-registry.test.js +306 -0
- package/server/lib/readme-generator.js +490 -0
- package/server/lib/readme-generator.test.js +493 -0
- package/server/lib/refactor-command.js +326 -0
- package/server/lib/refactor-command.test.js +528 -0
- package/server/lib/refactor-executor.js +254 -0
- package/server/lib/refactor-executor.test.js +305 -0
- package/server/lib/refactor-observer.js +292 -0
- package/server/lib/refactor-observer.test.js +422 -0
- package/server/lib/refactor-progress.js +193 -0
- package/server/lib/refactor-progress.test.js +251 -0
- package/server/lib/refactor-reporter.js +237 -0
- package/server/lib/refactor-reporter.test.js +247 -0
- package/server/lib/repo-dependency-tracker.js +261 -0
- package/server/lib/repo-dependency-tracker.test.js +350 -0
- package/server/lib/retention-policy.js +281 -0
- package/server/lib/retention-policy.test.js +486 -0
- package/server/lib/role-mapper.js +236 -0
- package/server/lib/role-mapper.test.js +395 -0
- package/server/lib/saml-provider.js +765 -0
- package/server/lib/saml-provider.test.js +643 -0
- package/server/lib/security-policy-generator.js +682 -0
- package/server/lib/security-policy-generator.test.js +544 -0
- package/server/lib/semantic-analyzer.js +198 -0
- package/server/lib/semantic-analyzer.test.js +474 -0
- package/server/lib/sensitive-detector.js +112 -0
- package/server/lib/sensitive-detector.test.js +209 -0
- package/server/lib/service-interaction-diagram.js +700 -0
- package/server/lib/service-interaction-diagram.test.js +638 -0
- package/server/lib/service-scaffold.js +486 -0
- package/server/lib/service-scaffold.test.js +373 -0
- package/server/lib/service-summary.js +553 -0
- package/server/lib/service-summary.test.js +619 -0
- package/server/lib/session-purge.js +460 -0
- package/server/lib/session-purge.test.js +312 -0
- package/server/lib/shared-kernel.js +578 -0
- package/server/lib/shared-kernel.test.js +255 -0
- package/server/lib/sso-command.js +544 -0
- package/server/lib/sso-command.test.js +552 -0
- package/server/lib/sso-session.js +492 -0
- package/server/lib/sso-session.test.js +670 -0
- package/server/lib/traefik-config.js +282 -0
- package/server/lib/traefik-config.test.js +312 -0
- package/server/lib/usage-command.js +218 -0
- package/server/lib/usage-command.test.js +391 -0
- package/server/lib/usage-formatter.js +192 -0
- package/server/lib/usage-formatter.test.js +267 -0
- package/server/lib/usage-history.js +122 -0
- package/server/lib/usage-history.test.js +206 -0
- package/server/lib/workspace-command.js +249 -0
- package/server/lib/workspace-command.test.js +264 -0
- package/server/lib/workspace-config.js +270 -0
- package/server/lib/workspace-config.test.js +312 -0
- package/server/lib/workspace-docs-command.js +547 -0
- package/server/lib/workspace-docs-command.test.js +692 -0
- package/server/lib/workspace-memory.js +451 -0
- package/server/lib/workspace-memory.test.js +403 -0
- package/server/lib/workspace-scanner.js +452 -0
- package/server/lib/workspace-scanner.test.js +677 -0
- package/server/lib/workspace-test-runner.js +315 -0
- package/server/lib/workspace-test-runner.test.js +294 -0
- package/server/lib/zero-retention-command.js +439 -0
- package/server/lib/zero-retention-command.test.js +448 -0
- package/server/lib/zero-retention.js +322 -0
- package/server/lib/zero-retention.test.js +258 -0
- package/server/package-lock.json +14 -0
- 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 };
|