tlc-claude-code 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/WorkspaceDocsPane.js +0 -16
- package/dashboard/dist/components/WorkspacePane.d.ts +1 -1
- 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/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/bulk-repo-init.js +342 -0
- package/server/lib/bulk-repo-init.test.js +388 -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/data-flow-doc.js +665 -0
- package/server/lib/data-flow-doc.test.js +659 -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/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/memory-exclusion.js +326 -0
- package/server/lib/memory-exclusion.test.js +241 -0
- package/server/lib/mfa-handler.js +452 -0
- package/server/lib/mfa-handler.test.js +490 -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/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/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-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/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/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
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo Dependency Tracker - Track dependencies between repos in a workspace
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
class RepoDependencyTracker {
|
|
9
|
+
constructor(workspaceRoot, repos) {
|
|
10
|
+
this.workspaceRoot = workspaceRoot;
|
|
11
|
+
this.repos = repos;
|
|
12
|
+
this.repoPackages = new Map(); // repo -> package.json data
|
|
13
|
+
this.repoNames = new Map(); // package name -> repo directory
|
|
14
|
+
this.dependencyGraph = {}; // repo -> [dependencies]
|
|
15
|
+
|
|
16
|
+
this.loadRepoData();
|
|
17
|
+
this.buildDependencyGraph();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load package.json for all repos
|
|
22
|
+
*/
|
|
23
|
+
loadRepoData() {
|
|
24
|
+
for (const repo of this.repos) {
|
|
25
|
+
const pkgPath = path.join(this.workspaceRoot, repo, 'package.json');
|
|
26
|
+
|
|
27
|
+
if (fs.existsSync(pkgPath)) {
|
|
28
|
+
try {
|
|
29
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
30
|
+
this.repoPackages.set(repo, pkg);
|
|
31
|
+
if (pkg.name) {
|
|
32
|
+
this.repoNames.set(pkg.name, repo);
|
|
33
|
+
}
|
|
34
|
+
} catch (err) {
|
|
35
|
+
// Invalid JSON, skip
|
|
36
|
+
this.repoPackages.set(repo, {});
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
this.repoPackages.set(repo, {});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build dependency graph from repo data
|
|
46
|
+
*/
|
|
47
|
+
buildDependencyGraph() {
|
|
48
|
+
for (const repo of this.repos) {
|
|
49
|
+
this.dependencyGraph[repo] = [];
|
|
50
|
+
const pkg = this.repoPackages.get(repo) || {};
|
|
51
|
+
|
|
52
|
+
const allDeps = {
|
|
53
|
+
...pkg.dependencies,
|
|
54
|
+
...pkg.devDependencies,
|
|
55
|
+
...pkg.peerDependencies,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
for (const [depName, depVersion] of Object.entries(allDeps || {})) {
|
|
59
|
+
// Check for workspace: protocol
|
|
60
|
+
if (typeof depVersion === 'string' && depVersion.startsWith('workspace:')) {
|
|
61
|
+
const depRepo = this.repoNames.get(depName);
|
|
62
|
+
if (depRepo && depRepo !== repo) {
|
|
63
|
+
this.dependencyGraph[repo].push(depRepo);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check for file: protocol pointing to another repo
|
|
68
|
+
if (typeof depVersion === 'string' && depVersion.startsWith('file:')) {
|
|
69
|
+
const filePath = depVersion.slice(5); // Remove 'file:'
|
|
70
|
+
const resolvedPath = path.resolve(path.join(this.workspaceRoot, repo), filePath);
|
|
71
|
+
const relativePath = path.relative(this.workspaceRoot, resolvedPath);
|
|
72
|
+
|
|
73
|
+
// Check if it points to one of our repos
|
|
74
|
+
for (const otherRepo of this.repos) {
|
|
75
|
+
if (relativePath === otherRepo || relativePath.startsWith(otherRepo + path.sep)) {
|
|
76
|
+
if (otherRepo !== repo) {
|
|
77
|
+
this.dependencyGraph[repo].push(otherRepo);
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get dependencies of a repo
|
|
89
|
+
* @param {string} repo - Repo directory name
|
|
90
|
+
* @returns {string[]} Array of repo names this repo depends on
|
|
91
|
+
*/
|
|
92
|
+
getDependencies(repo) {
|
|
93
|
+
return this.dependencyGraph[repo] || [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if repoA depends on repoB
|
|
98
|
+
* @param {string} repoA
|
|
99
|
+
* @param {string} repoB
|
|
100
|
+
* @returns {boolean}
|
|
101
|
+
*/
|
|
102
|
+
dependsOn(repoA, repoB) {
|
|
103
|
+
const deps = this.getDependencies(repoA);
|
|
104
|
+
return deps.includes(repoB);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get repos that depend on the given repo
|
|
109
|
+
* @param {string} repo
|
|
110
|
+
* @returns {string[]}
|
|
111
|
+
*/
|
|
112
|
+
getDependents(repo) {
|
|
113
|
+
const dependents = [];
|
|
114
|
+
|
|
115
|
+
for (const [r, deps] of Object.entries(this.dependencyGraph)) {
|
|
116
|
+
if (deps.includes(repo)) {
|
|
117
|
+
dependents.push(r);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return dependents;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get all repos affected when the given repo changes (transitive dependents)
|
|
126
|
+
* @param {string} repo
|
|
127
|
+
* @returns {string[]}
|
|
128
|
+
*/
|
|
129
|
+
getAffectedRepos(repo) {
|
|
130
|
+
const affected = new Set();
|
|
131
|
+
const queue = [repo];
|
|
132
|
+
const visited = new Set([repo]);
|
|
133
|
+
|
|
134
|
+
while (queue.length > 0) {
|
|
135
|
+
const current = queue.shift();
|
|
136
|
+
const dependents = this.getDependents(current);
|
|
137
|
+
|
|
138
|
+
for (const dep of dependents) {
|
|
139
|
+
if (!visited.has(dep)) {
|
|
140
|
+
visited.add(dep);
|
|
141
|
+
affected.add(dep);
|
|
142
|
+
queue.push(dep);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return Array.from(affected);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get topological order for build/test runs
|
|
152
|
+
* @returns {string[]}
|
|
153
|
+
*/
|
|
154
|
+
getTopologicalOrder() {
|
|
155
|
+
const visited = new Set();
|
|
156
|
+
const result = [];
|
|
157
|
+
|
|
158
|
+
const visit = (repo) => {
|
|
159
|
+
if (visited.has(repo)) return;
|
|
160
|
+
visited.add(repo);
|
|
161
|
+
|
|
162
|
+
const deps = this.getDependencies(repo);
|
|
163
|
+
for (const dep of deps) {
|
|
164
|
+
visit(dep);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
result.push(repo);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
for (const repo of this.repos) {
|
|
171
|
+
visit(repo);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Detect circular dependencies
|
|
179
|
+
* @returns {string[][]} Array of cycles found
|
|
180
|
+
*/
|
|
181
|
+
detectCircularDependencies() {
|
|
182
|
+
const cycles = [];
|
|
183
|
+
const visited = new Set();
|
|
184
|
+
const recursionStack = new Set();
|
|
185
|
+
const path = [];
|
|
186
|
+
|
|
187
|
+
const dfs = (repo) => {
|
|
188
|
+
visited.add(repo);
|
|
189
|
+
recursionStack.add(repo);
|
|
190
|
+
path.push(repo);
|
|
191
|
+
|
|
192
|
+
const deps = this.getDependencies(repo);
|
|
193
|
+
for (const dep of deps) {
|
|
194
|
+
if (!visited.has(dep)) {
|
|
195
|
+
const cycle = dfs(dep);
|
|
196
|
+
if (cycle) return cycle;
|
|
197
|
+
} else if (recursionStack.has(dep)) {
|
|
198
|
+
// Found cycle
|
|
199
|
+
const cycleStart = path.indexOf(dep);
|
|
200
|
+
const cycle = path.slice(cycleStart);
|
|
201
|
+
cycle.push(dep); // Complete the cycle
|
|
202
|
+
cycles.push(cycle);
|
|
203
|
+
return cycle;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
path.pop();
|
|
208
|
+
recursionStack.delete(repo);
|
|
209
|
+
return null;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
for (const repo of this.repos) {
|
|
213
|
+
if (!visited.has(repo)) {
|
|
214
|
+
dfs(repo);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return cycles;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Generate Mermaid diagram of dependencies
|
|
223
|
+
* @returns {string}
|
|
224
|
+
*/
|
|
225
|
+
generateMermaidDiagram() {
|
|
226
|
+
const lines = ['graph TD'];
|
|
227
|
+
|
|
228
|
+
// Add all nodes
|
|
229
|
+
for (const repo of this.repos) {
|
|
230
|
+
lines.push(` ${this.sanitizeId(repo)}[${repo}]`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Add edges
|
|
234
|
+
for (const [repo, deps] of Object.entries(this.dependencyGraph)) {
|
|
235
|
+
for (const dep of deps) {
|
|
236
|
+
lines.push(` ${this.sanitizeId(repo)} --> ${this.sanitizeId(dep)}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return lines.join('\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Sanitize repo name for Mermaid ID
|
|
245
|
+
*/
|
|
246
|
+
sanitizeId(name) {
|
|
247
|
+
return name.replace(/[^a-zA-Z0-9]/g, '_');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get the full dependency graph
|
|
252
|
+
* @returns {Object}
|
|
253
|
+
*/
|
|
254
|
+
getDependencyGraph() {
|
|
255
|
+
return { ...this.dependencyGraph };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
RepoDependencyTracker,
|
|
261
|
+
};
|
|
@@ -0,0 +1,350 @@
|
|
|
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 { RepoDependencyTracker } = await import('./repo-dependency-tracker.js');
|
|
7
|
+
|
|
8
|
+
describe('RepoDependencyTracker', () => {
|
|
9
|
+
let tempDir;
|
|
10
|
+
let tracker;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'repo-dep-test-'));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function createRepo(name, packageJson) {
|
|
21
|
+
const repoPath = path.join(tempDir, name);
|
|
22
|
+
fs.mkdirSync(repoPath, { recursive: true });
|
|
23
|
+
fs.writeFileSync(
|
|
24
|
+
path.join(repoPath, 'package.json'),
|
|
25
|
+
JSON.stringify(packageJson, null, 2)
|
|
26
|
+
);
|
|
27
|
+
return repoPath;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('dependency detection', () => {
|
|
31
|
+
it('detects workspace:* dependencies', () => {
|
|
32
|
+
createRepo('core', { name: '@myorg/core', version: '1.0.0' });
|
|
33
|
+
createRepo('api', {
|
|
34
|
+
name: '@myorg/api',
|
|
35
|
+
version: '1.0.0',
|
|
36
|
+
dependencies: {
|
|
37
|
+
'@myorg/core': 'workspace:*',
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
tracker = new RepoDependencyTracker(tempDir, ['core', 'api']);
|
|
42
|
+
const deps = tracker.getDependencies('api');
|
|
43
|
+
|
|
44
|
+
expect(deps).toContain('core');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('detects file:../other-repo dependencies', () => {
|
|
48
|
+
createRepo('shared', { name: 'shared-lib', version: '1.0.0' });
|
|
49
|
+
createRepo('app', {
|
|
50
|
+
name: 'my-app',
|
|
51
|
+
version: '1.0.0',
|
|
52
|
+
dependencies: {
|
|
53
|
+
'shared-lib': 'file:../shared',
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
tracker = new RepoDependencyTracker(tempDir, ['shared', 'app']);
|
|
58
|
+
const deps = tracker.getDependencies('app');
|
|
59
|
+
|
|
60
|
+
expect(deps).toContain('shared');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('detects workspace:^ dependencies', () => {
|
|
64
|
+
createRepo('utils', { name: '@myorg/utils', version: '2.0.0' });
|
|
65
|
+
createRepo('web', {
|
|
66
|
+
name: '@myorg/web',
|
|
67
|
+
version: '1.0.0',
|
|
68
|
+
dependencies: {
|
|
69
|
+
'@myorg/utils': 'workspace:^',
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
tracker = new RepoDependencyTracker(tempDir, ['utils', 'web']);
|
|
74
|
+
const deps = tracker.getDependencies('web');
|
|
75
|
+
|
|
76
|
+
expect(deps).toContain('utils');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('detects dependencies in devDependencies', () => {
|
|
80
|
+
createRepo('test-utils', { name: '@myorg/test-utils', version: '1.0.0' });
|
|
81
|
+
createRepo('service', {
|
|
82
|
+
name: '@myorg/service',
|
|
83
|
+
version: '1.0.0',
|
|
84
|
+
devDependencies: {
|
|
85
|
+
'@myorg/test-utils': 'workspace:*',
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
tracker = new RepoDependencyTracker(tempDir, ['test-utils', 'service']);
|
|
90
|
+
const deps = tracker.getDependencies('service');
|
|
91
|
+
|
|
92
|
+
expect(deps).toContain('test-utils');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('dependency direction', () => {
|
|
97
|
+
it('identifies dependency direction (A depends on B)', () => {
|
|
98
|
+
createRepo('base', { name: 'base', version: '1.0.0' });
|
|
99
|
+
createRepo('derived', {
|
|
100
|
+
name: 'derived',
|
|
101
|
+
version: '1.0.0',
|
|
102
|
+
dependencies: { base: 'workspace:*' },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
tracker = new RepoDependencyTracker(tempDir, ['base', 'derived']);
|
|
106
|
+
|
|
107
|
+
expect(tracker.dependsOn('derived', 'base')).toBe(true);
|
|
108
|
+
expect(tracker.dependsOn('base', 'derived')).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('gets dependents of a repo', () => {
|
|
112
|
+
createRepo('core', { name: 'core', version: '1.0.0' });
|
|
113
|
+
createRepo('api', {
|
|
114
|
+
name: 'api',
|
|
115
|
+
version: '1.0.0',
|
|
116
|
+
dependencies: { core: 'workspace:*' },
|
|
117
|
+
});
|
|
118
|
+
createRepo('web', {
|
|
119
|
+
name: 'web',
|
|
120
|
+
version: '1.0.0',
|
|
121
|
+
dependencies: { core: 'workspace:*' },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
tracker = new RepoDependencyTracker(tempDir, ['core', 'api', 'web']);
|
|
125
|
+
const dependents = tracker.getDependents('core');
|
|
126
|
+
|
|
127
|
+
expect(dependents).toContain('api');
|
|
128
|
+
expect(dependents).toContain('web');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('affected repos', () => {
|
|
133
|
+
it('calculates affected repos when one changes', () => {
|
|
134
|
+
createRepo('core', { name: 'core', version: '1.0.0' });
|
|
135
|
+
createRepo('utils', {
|
|
136
|
+
name: 'utils',
|
|
137
|
+
version: '1.0.0',
|
|
138
|
+
dependencies: { core: 'workspace:*' },
|
|
139
|
+
});
|
|
140
|
+
createRepo('api', {
|
|
141
|
+
name: 'api',
|
|
142
|
+
version: '1.0.0',
|
|
143
|
+
dependencies: { utils: 'workspace:*' },
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
tracker = new RepoDependencyTracker(tempDir, ['core', 'utils', 'api']);
|
|
147
|
+
const affected = tracker.getAffectedRepos('core');
|
|
148
|
+
|
|
149
|
+
expect(affected).toContain('utils');
|
|
150
|
+
expect(affected).toContain('api');
|
|
151
|
+
expect(affected).not.toContain('core'); // Changed repo not in affected
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('returns empty array for repo with no dependents', () => {
|
|
155
|
+
createRepo('standalone', { name: 'standalone', version: '1.0.0' });
|
|
156
|
+
|
|
157
|
+
tracker = new RepoDependencyTracker(tempDir, ['standalone']);
|
|
158
|
+
const affected = tracker.getAffectedRepos('standalone');
|
|
159
|
+
|
|
160
|
+
expect(affected).toEqual([]);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('topological sort', () => {
|
|
165
|
+
it('generates topological sort for build order', () => {
|
|
166
|
+
createRepo('a', { name: 'a', version: '1.0.0' });
|
|
167
|
+
createRepo('b', {
|
|
168
|
+
name: 'b',
|
|
169
|
+
version: '1.0.0',
|
|
170
|
+
dependencies: { a: 'workspace:*' },
|
|
171
|
+
});
|
|
172
|
+
createRepo('c', {
|
|
173
|
+
name: 'c',
|
|
174
|
+
version: '1.0.0',
|
|
175
|
+
dependencies: { b: 'workspace:*' },
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
tracker = new RepoDependencyTracker(tempDir, ['a', 'b', 'c']);
|
|
179
|
+
const order = tracker.getTopologicalOrder();
|
|
180
|
+
|
|
181
|
+
const aIndex = order.indexOf('a');
|
|
182
|
+
const bIndex = order.indexOf('b');
|
|
183
|
+
const cIndex = order.indexOf('c');
|
|
184
|
+
|
|
185
|
+
expect(aIndex).toBeLessThan(bIndex);
|
|
186
|
+
expect(bIndex).toBeLessThan(cIndex);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('handles independent repos in topological sort', () => {
|
|
190
|
+
createRepo('x', { name: 'x', version: '1.0.0' });
|
|
191
|
+
createRepo('y', { name: 'y', version: '1.0.0' });
|
|
192
|
+
createRepo('z', { name: 'z', version: '1.0.0' });
|
|
193
|
+
|
|
194
|
+
tracker = new RepoDependencyTracker(tempDir, ['x', 'y', 'z']);
|
|
195
|
+
const order = tracker.getTopologicalOrder();
|
|
196
|
+
|
|
197
|
+
expect(order).toHaveLength(3);
|
|
198
|
+
expect(order).toContain('x');
|
|
199
|
+
expect(order).toContain('y');
|
|
200
|
+
expect(order).toContain('z');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('circular dependencies', () => {
|
|
205
|
+
it('detects circular dependencies', () => {
|
|
206
|
+
createRepo('a', {
|
|
207
|
+
name: 'a',
|
|
208
|
+
version: '1.0.0',
|
|
209
|
+
dependencies: { b: 'workspace:*' },
|
|
210
|
+
});
|
|
211
|
+
createRepo('b', {
|
|
212
|
+
name: 'b',
|
|
213
|
+
version: '1.0.0',
|
|
214
|
+
dependencies: { a: 'workspace:*' },
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
tracker = new RepoDependencyTracker(tempDir, ['a', 'b']);
|
|
218
|
+
const cycles = tracker.detectCircularDependencies();
|
|
219
|
+
|
|
220
|
+
expect(cycles.length).toBeGreaterThan(0);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('returns empty array when no circular dependencies', () => {
|
|
224
|
+
createRepo('base', { name: 'base', version: '1.0.0' });
|
|
225
|
+
createRepo('app', {
|
|
226
|
+
name: 'app',
|
|
227
|
+
version: '1.0.0',
|
|
228
|
+
dependencies: { base: 'workspace:*' },
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
tracker = new RepoDependencyTracker(tempDir, ['base', 'app']);
|
|
232
|
+
const cycles = tracker.detectCircularDependencies();
|
|
233
|
+
|
|
234
|
+
expect(cycles).toEqual([]);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('detects transitive circular dependencies', () => {
|
|
238
|
+
createRepo('a', {
|
|
239
|
+
name: 'a',
|
|
240
|
+
version: '1.0.0',
|
|
241
|
+
dependencies: { b: 'workspace:*' },
|
|
242
|
+
});
|
|
243
|
+
createRepo('b', {
|
|
244
|
+
name: 'b',
|
|
245
|
+
version: '1.0.0',
|
|
246
|
+
dependencies: { c: 'workspace:*' },
|
|
247
|
+
});
|
|
248
|
+
createRepo('c', {
|
|
249
|
+
name: 'c',
|
|
250
|
+
version: '1.0.0',
|
|
251
|
+
dependencies: { a: 'workspace:*' },
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
tracker = new RepoDependencyTracker(tempDir, ['a', 'b', 'c']);
|
|
255
|
+
const cycles = tracker.detectCircularDependencies();
|
|
256
|
+
|
|
257
|
+
expect(cycles.length).toBeGreaterThan(0);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('Mermaid diagram generation', () => {
|
|
262
|
+
it('generates Mermaid dependency diagram', () => {
|
|
263
|
+
createRepo('core', { name: 'core', version: '1.0.0' });
|
|
264
|
+
createRepo('api', {
|
|
265
|
+
name: 'api',
|
|
266
|
+
version: '1.0.0',
|
|
267
|
+
dependencies: { core: 'workspace:*' },
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
tracker = new RepoDependencyTracker(tempDir, ['core', 'api']);
|
|
271
|
+
const diagram = tracker.generateMermaidDiagram();
|
|
272
|
+
|
|
273
|
+
expect(diagram).toContain('graph');
|
|
274
|
+
expect(diagram).toContain('api');
|
|
275
|
+
expect(diagram).toContain('core');
|
|
276
|
+
expect(diagram).toContain('-->');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('generates empty diagram for no dependencies', () => {
|
|
280
|
+
createRepo('standalone', { name: 'standalone', version: '1.0.0' });
|
|
281
|
+
|
|
282
|
+
tracker = new RepoDependencyTracker(tempDir, ['standalone']);
|
|
283
|
+
const diagram = tracker.generateMermaidDiagram();
|
|
284
|
+
|
|
285
|
+
expect(diagram).toContain('graph');
|
|
286
|
+
expect(diagram).toContain('standalone');
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('error handling', () => {
|
|
291
|
+
it('handles missing dependencies gracefully', () => {
|
|
292
|
+
createRepo('app', {
|
|
293
|
+
name: 'app',
|
|
294
|
+
version: '1.0.0',
|
|
295
|
+
dependencies: { 'non-existent': 'workspace:*' },
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
tracker = new RepoDependencyTracker(tempDir, ['app']);
|
|
299
|
+
const deps = tracker.getDependencies('app');
|
|
300
|
+
|
|
301
|
+
// Should not throw, non-workspace deps are ignored
|
|
302
|
+
expect(deps).toEqual([]);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('handles missing package.json', () => {
|
|
306
|
+
const repoPath = path.join(tempDir, 'no-pkg');
|
|
307
|
+
fs.mkdirSync(repoPath);
|
|
308
|
+
|
|
309
|
+
tracker = new RepoDependencyTracker(tempDir, ['no-pkg']);
|
|
310
|
+
const deps = tracker.getDependencies('no-pkg');
|
|
311
|
+
|
|
312
|
+
expect(deps).toEqual([]);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('handles malformed package.json', () => {
|
|
316
|
+
const repoPath = path.join(tempDir, 'bad-pkg');
|
|
317
|
+
fs.mkdirSync(repoPath);
|
|
318
|
+
fs.writeFileSync(path.join(repoPath, 'package.json'), 'not valid json');
|
|
319
|
+
|
|
320
|
+
tracker = new RepoDependencyTracker(tempDir, ['bad-pkg']);
|
|
321
|
+
const deps = tracker.getDependencies('bad-pkg');
|
|
322
|
+
|
|
323
|
+
expect(deps).toEqual([]);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe('dependency graph', () => {
|
|
328
|
+
it('builds complete dependency graph', () => {
|
|
329
|
+
createRepo('a', { name: 'a', version: '1.0.0' });
|
|
330
|
+
createRepo('b', {
|
|
331
|
+
name: 'b',
|
|
332
|
+
version: '1.0.0',
|
|
333
|
+
dependencies: { a: 'workspace:*' },
|
|
334
|
+
});
|
|
335
|
+
createRepo('c', {
|
|
336
|
+
name: 'c',
|
|
337
|
+
version: '1.0.0',
|
|
338
|
+
dependencies: { a: 'workspace:*', b: 'workspace:*' },
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
tracker = new RepoDependencyTracker(tempDir, ['a', 'b', 'c']);
|
|
342
|
+
const graph = tracker.getDependencyGraph();
|
|
343
|
+
|
|
344
|
+
expect(graph.a).toEqual([]);
|
|
345
|
+
expect(graph.b).toContain('a');
|
|
346
|
+
expect(graph.c).toContain('a');
|
|
347
|
+
expect(graph.c).toContain('b');
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
});
|