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.
Files changed (105) 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/WorkspaceDocsPane.js +0 -16
  14. package/dashboard/dist/components/WorkspacePane.d.ts +1 -1
  15. package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
  16. package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
  17. package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
  18. package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
  19. package/package.json +1 -1
  20. package/server/lib/access-control-doc.js +541 -0
  21. package/server/lib/access-control-doc.test.js +672 -0
  22. package/server/lib/adr-generator.js +423 -0
  23. package/server/lib/adr-generator.test.js +586 -0
  24. package/server/lib/agent-progress-monitor.js +223 -0
  25. package/server/lib/agent-progress-monitor.test.js +202 -0
  26. package/server/lib/audit-attribution.js +191 -0
  27. package/server/lib/audit-attribution.test.js +359 -0
  28. package/server/lib/audit-classifier.js +202 -0
  29. package/server/lib/audit-classifier.test.js +209 -0
  30. package/server/lib/audit-command.js +275 -0
  31. package/server/lib/audit-command.test.js +325 -0
  32. package/server/lib/audit-exporter.js +380 -0
  33. package/server/lib/audit-exporter.test.js +464 -0
  34. package/server/lib/audit-logger.js +236 -0
  35. package/server/lib/audit-logger.test.js +364 -0
  36. package/server/lib/audit-query.js +257 -0
  37. package/server/lib/audit-query.test.js +352 -0
  38. package/server/lib/audit-storage.js +269 -0
  39. package/server/lib/audit-storage.test.js +272 -0
  40. package/server/lib/bulk-repo-init.js +342 -0
  41. package/server/lib/bulk-repo-init.test.js +388 -0
  42. package/server/lib/compliance-checklist.js +866 -0
  43. package/server/lib/compliance-checklist.test.js +476 -0
  44. package/server/lib/compliance-command.js +616 -0
  45. package/server/lib/compliance-command.test.js +551 -0
  46. package/server/lib/compliance-reporter.js +692 -0
  47. package/server/lib/compliance-reporter.test.js +707 -0
  48. package/server/lib/data-flow-doc.js +665 -0
  49. package/server/lib/data-flow-doc.test.js +659 -0
  50. package/server/lib/ephemeral-storage.js +249 -0
  51. package/server/lib/ephemeral-storage.test.js +254 -0
  52. package/server/lib/evidence-collector.js +627 -0
  53. package/server/lib/evidence-collector.test.js +901 -0
  54. package/server/lib/flow-diagram-generator.js +474 -0
  55. package/server/lib/flow-diagram-generator.test.js +446 -0
  56. package/server/lib/idp-manager.js +626 -0
  57. package/server/lib/idp-manager.test.js +587 -0
  58. package/server/lib/memory-exclusion.js +326 -0
  59. package/server/lib/memory-exclusion.test.js +241 -0
  60. package/server/lib/mfa-handler.js +452 -0
  61. package/server/lib/mfa-handler.test.js +490 -0
  62. package/server/lib/oauth-flow.js +375 -0
  63. package/server/lib/oauth-flow.test.js +487 -0
  64. package/server/lib/oauth-registry.js +190 -0
  65. package/server/lib/oauth-registry.test.js +306 -0
  66. package/server/lib/readme-generator.js +490 -0
  67. package/server/lib/readme-generator.test.js +493 -0
  68. package/server/lib/repo-dependency-tracker.js +261 -0
  69. package/server/lib/repo-dependency-tracker.test.js +350 -0
  70. package/server/lib/retention-policy.js +281 -0
  71. package/server/lib/retention-policy.test.js +486 -0
  72. package/server/lib/role-mapper.js +236 -0
  73. package/server/lib/role-mapper.test.js +395 -0
  74. package/server/lib/saml-provider.js +765 -0
  75. package/server/lib/saml-provider.test.js +643 -0
  76. package/server/lib/security-policy-generator.js +682 -0
  77. package/server/lib/security-policy-generator.test.js +544 -0
  78. package/server/lib/sensitive-detector.js +112 -0
  79. package/server/lib/sensitive-detector.test.js +209 -0
  80. package/server/lib/service-interaction-diagram.js +700 -0
  81. package/server/lib/service-interaction-diagram.test.js +638 -0
  82. package/server/lib/service-summary.js +553 -0
  83. package/server/lib/service-summary.test.js +619 -0
  84. package/server/lib/session-purge.js +460 -0
  85. package/server/lib/session-purge.test.js +312 -0
  86. package/server/lib/sso-command.js +544 -0
  87. package/server/lib/sso-command.test.js +552 -0
  88. package/server/lib/sso-session.js +492 -0
  89. package/server/lib/sso-session.test.js +670 -0
  90. package/server/lib/workspace-command.js +249 -0
  91. package/server/lib/workspace-command.test.js +264 -0
  92. package/server/lib/workspace-config.js +270 -0
  93. package/server/lib/workspace-config.test.js +312 -0
  94. package/server/lib/workspace-docs-command.js +547 -0
  95. package/server/lib/workspace-docs-command.test.js +692 -0
  96. package/server/lib/workspace-memory.js +451 -0
  97. package/server/lib/workspace-memory.test.js +403 -0
  98. package/server/lib/workspace-scanner.js +452 -0
  99. package/server/lib/workspace-scanner.test.js +677 -0
  100. package/server/lib/workspace-test-runner.js +315 -0
  101. package/server/lib/workspace-test-runner.test.js +294 -0
  102. package/server/lib/zero-retention-command.js +439 -0
  103. package/server/lib/zero-retention-command.test.js +448 -0
  104. package/server/lib/zero-retention.js +322 -0
  105. 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
+ });