tlc-claude-code 1.3.0 → 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/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,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Scanner - Discover and index repos in workspace
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const SOURCE_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'];
|
|
9
|
+
|
|
10
|
+
class WorkspaceScanner {
|
|
11
|
+
constructor(workspaceConfig) {
|
|
12
|
+
this.workspaceConfig = workspaceConfig;
|
|
13
|
+
this.rootDir = workspaceConfig.rootDir;
|
|
14
|
+
this.cachedResult = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Scan all repos in the workspace
|
|
19
|
+
* @param {Object} options - Scan options
|
|
20
|
+
* @param {boolean} options.force - Force rescan (ignore cache)
|
|
21
|
+
* @param {boolean} options.scanImports - Scan source files for import references
|
|
22
|
+
* @returns {Object} Scan result with repos, graphs, and stats
|
|
23
|
+
*/
|
|
24
|
+
scan(options = {}) {
|
|
25
|
+
const { force = false, scanImports = false } = options;
|
|
26
|
+
|
|
27
|
+
// Return cached result if available and not forcing
|
|
28
|
+
if (this.cachedResult && !force) {
|
|
29
|
+
return this.cachedResult;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const config = this.workspaceConfig.getConfig();
|
|
33
|
+
const repos = [];
|
|
34
|
+
const byPath = {};
|
|
35
|
+
const byName = {};
|
|
36
|
+
const dependencyGraph = {};
|
|
37
|
+
|
|
38
|
+
// Scan each repo
|
|
39
|
+
for (const repoPath of config.repos) {
|
|
40
|
+
const repoInfo = this.scanRepo(repoPath, scanImports);
|
|
41
|
+
repos.push(repoInfo);
|
|
42
|
+
byPath[repoPath] = repoInfo;
|
|
43
|
+
if (repoInfo.name) {
|
|
44
|
+
byName[repoInfo.name] = repoInfo;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Build dependency graph after all repos are scanned
|
|
49
|
+
for (const repo of repos) {
|
|
50
|
+
dependencyGraph[repo.path] = this.findDependencyPaths(repo, byName);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Detect circular dependencies
|
|
54
|
+
const { hasCircular, cycles } = this.detectCircularDeps(dependencyGraph);
|
|
55
|
+
|
|
56
|
+
// Calculate dependency order (topological sort)
|
|
57
|
+
const dependencyOrder = this.calculateDependencyOrder(dependencyGraph);
|
|
58
|
+
|
|
59
|
+
// Build stats
|
|
60
|
+
const stats = {
|
|
61
|
+
totalRepos: repos.length,
|
|
62
|
+
reposWithPackageJson: repos.filter(r => r.hasPackageJson).length,
|
|
63
|
+
reposWithCircularDeps: hasCircular ? cycles.length : 0,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const result = {
|
|
67
|
+
repos,
|
|
68
|
+
byPath,
|
|
69
|
+
byName,
|
|
70
|
+
dependencyGraph,
|
|
71
|
+
dependencyOrder,
|
|
72
|
+
hasCircularDeps: hasCircular,
|
|
73
|
+
circularDeps: cycles,
|
|
74
|
+
stats,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
this.cachedResult = result;
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Scan a single repo for project info
|
|
83
|
+
* @param {string} repoPath - Relative path to repo
|
|
84
|
+
* @param {boolean} scanImports - Whether to scan source files for imports
|
|
85
|
+
* @returns {Object} Repo info
|
|
86
|
+
*/
|
|
87
|
+
scanRepo(repoPath, scanImports = false) {
|
|
88
|
+
const absolutePath = path.join(this.rootDir, repoPath);
|
|
89
|
+
const packageJsonPath = path.join(absolutePath, 'package.json');
|
|
90
|
+
|
|
91
|
+
const info = {
|
|
92
|
+
path: repoPath,
|
|
93
|
+
name: repoPath, // Default to path if no package.json
|
|
94
|
+
version: null,
|
|
95
|
+
description: null,
|
|
96
|
+
main: null,
|
|
97
|
+
module: null,
|
|
98
|
+
scripts: {},
|
|
99
|
+
dependencies: [],
|
|
100
|
+
devDependencies: [],
|
|
101
|
+
workspaceDeps: [],
|
|
102
|
+
importedRepos: [],
|
|
103
|
+
hasPackageJson: false,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Try to read package.json
|
|
107
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
108
|
+
try {
|
|
109
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
110
|
+
info.hasPackageJson = true;
|
|
111
|
+
info.name = pkg.name || repoPath;
|
|
112
|
+
info.version = pkg.version || null;
|
|
113
|
+
info.description = pkg.description || null;
|
|
114
|
+
info.main = pkg.main || null;
|
|
115
|
+
info.module = pkg.module || null;
|
|
116
|
+
info.scripts = pkg.scripts || {};
|
|
117
|
+
|
|
118
|
+
// Extract dependencies
|
|
119
|
+
info.dependencies = Object.keys(pkg.dependencies || {});
|
|
120
|
+
info.devDependencies = Object.keys(pkg.devDependencies || {});
|
|
121
|
+
|
|
122
|
+
// Detect workspace dependencies
|
|
123
|
+
info.workspaceDeps = this.extractWorkspaceDeps(pkg);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
// Ignore parse errors, use defaults
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Scan source files for import references if requested
|
|
130
|
+
if (scanImports) {
|
|
131
|
+
info.importedRepos = this.scanImportReferences(absolutePath);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return info;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Extract workspace dependencies from package.json
|
|
139
|
+
* @param {Object} pkg - Parsed package.json
|
|
140
|
+
* @returns {string[]} Array of workspace dependency names
|
|
141
|
+
*/
|
|
142
|
+
extractWorkspaceDeps(pkg) {
|
|
143
|
+
const workspaceDeps = [];
|
|
144
|
+
const allDeps = {
|
|
145
|
+
...pkg.dependencies,
|
|
146
|
+
...pkg.devDependencies,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
for (const [name, version] of Object.entries(allDeps)) {
|
|
150
|
+
// Detect workspace: protocol
|
|
151
|
+
if (typeof version === 'string' && version.startsWith('workspace:')) {
|
|
152
|
+
workspaceDeps.push(name);
|
|
153
|
+
}
|
|
154
|
+
// Detect file: protocol (relative path)
|
|
155
|
+
else if (typeof version === 'string' && version.startsWith('file:')) {
|
|
156
|
+
workspaceDeps.push(name);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return workspaceDeps;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Scan source files for import references to workspace packages
|
|
165
|
+
* @param {string} repoDir - Absolute path to repo
|
|
166
|
+
* @returns {string[]} Array of imported workspace package names
|
|
167
|
+
*/
|
|
168
|
+
scanImportReferences(repoDir) {
|
|
169
|
+
const importedRepos = new Set();
|
|
170
|
+
const workspacePackages = this.getWorkspacePackageNames();
|
|
171
|
+
|
|
172
|
+
const scanDir = (dir) => {
|
|
173
|
+
if (!fs.existsSync(dir)) return;
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
177
|
+
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
const fullPath = path.join(dir, entry.name);
|
|
180
|
+
|
|
181
|
+
// Skip ignored directories
|
|
182
|
+
if (entry.isDirectory()) {
|
|
183
|
+
if (['node_modules', '.git', 'dist', 'build', 'coverage'].includes(entry.name)) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
scanDir(fullPath);
|
|
187
|
+
} else if (SOURCE_EXTENSIONS.some(ext => entry.name.endsWith(ext))) {
|
|
188
|
+
// Scan source file for imports
|
|
189
|
+
const imports = this.extractImportsFromFile(fullPath);
|
|
190
|
+
for (const imp of imports) {
|
|
191
|
+
if (workspacePackages.has(imp)) {
|
|
192
|
+
importedRepos.add(imp);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch (err) {
|
|
198
|
+
// Ignore read errors
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
scanDir(repoDir);
|
|
203
|
+
return Array.from(importedRepos);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get all workspace package names
|
|
208
|
+
* @returns {Set<string>} Set of package names
|
|
209
|
+
*/
|
|
210
|
+
getWorkspacePackageNames() {
|
|
211
|
+
const names = new Set();
|
|
212
|
+
const config = this.workspaceConfig.getConfig();
|
|
213
|
+
|
|
214
|
+
for (const repoPath of config.repos) {
|
|
215
|
+
const packageJsonPath = path.join(this.rootDir, repoPath, 'package.json');
|
|
216
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
217
|
+
try {
|
|
218
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
219
|
+
if (pkg.name) {
|
|
220
|
+
names.add(pkg.name);
|
|
221
|
+
}
|
|
222
|
+
} catch (err) {
|
|
223
|
+
// Ignore parse errors
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return names;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Extract import/require statements from a file
|
|
233
|
+
* @param {string} filePath - Absolute path to file
|
|
234
|
+
* @returns {string[]} Array of imported module names
|
|
235
|
+
*/
|
|
236
|
+
extractImportsFromFile(filePath) {
|
|
237
|
+
const imports = [];
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
241
|
+
|
|
242
|
+
// ES6 imports: import x from 'y', import { x } from 'y'
|
|
243
|
+
const es6Regex = /import\s+(?:(?:[\w*{}\s,]+)\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
244
|
+
let match;
|
|
245
|
+
while ((match = es6Regex.exec(content)) !== null) {
|
|
246
|
+
imports.push(this.getPackageName(match[1]));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// CommonJS: require('x')
|
|
250
|
+
const cjsRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
251
|
+
while ((match = cjsRegex.exec(content)) !== null) {
|
|
252
|
+
imports.push(this.getPackageName(match[1]));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Dynamic imports: import('x')
|
|
256
|
+
const dynamicRegex = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
257
|
+
while ((match = dynamicRegex.exec(content)) !== null) {
|
|
258
|
+
imports.push(this.getPackageName(match[1]));
|
|
259
|
+
}
|
|
260
|
+
} catch (err) {
|
|
261
|
+
// Ignore read errors
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return imports;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get package name from import path
|
|
269
|
+
* @param {string} importPath - Import path
|
|
270
|
+
* @returns {string} Package name (handles scoped packages)
|
|
271
|
+
*/
|
|
272
|
+
getPackageName(importPath) {
|
|
273
|
+
// Handle relative paths
|
|
274
|
+
if (importPath.startsWith('.') || importPath.startsWith('/')) {
|
|
275
|
+
return importPath;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Handle scoped packages (@scope/package)
|
|
279
|
+
if (importPath.startsWith('@')) {
|
|
280
|
+
const parts = importPath.split('/');
|
|
281
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : importPath;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Regular package
|
|
285
|
+
return importPath.split('/')[0];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Find repo paths that a repo depends on
|
|
290
|
+
* @param {Object} repoInfo - Repo info object
|
|
291
|
+
* @param {Object} byName - Repos indexed by name
|
|
292
|
+
* @returns {string[]} Array of dependency repo paths
|
|
293
|
+
*/
|
|
294
|
+
findDependencyPaths(repoInfo, byName) {
|
|
295
|
+
const depPaths = [];
|
|
296
|
+
|
|
297
|
+
// Check workspace deps (include self-references for circular dep detection)
|
|
298
|
+
for (const depName of repoInfo.workspaceDeps) {
|
|
299
|
+
const depRepo = byName[depName];
|
|
300
|
+
if (depRepo && !depPaths.includes(depRepo.path)) {
|
|
301
|
+
depPaths.push(depRepo.path);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Check imported repos
|
|
306
|
+
for (const importedName of repoInfo.importedRepos) {
|
|
307
|
+
const depRepo = byName[importedName];
|
|
308
|
+
if (depRepo && !depPaths.includes(depRepo.path)) {
|
|
309
|
+
depPaths.push(depRepo.path);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return depPaths;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Detect circular dependencies in the graph
|
|
318
|
+
* @param {Object} graph - Dependency graph { repoPath: [depPaths] }
|
|
319
|
+
* @returns {Object} { hasCircular: boolean, cycles: string[][] }
|
|
320
|
+
*/
|
|
321
|
+
detectCircularDeps(graph) {
|
|
322
|
+
const cycles = [];
|
|
323
|
+
const visited = new Set();
|
|
324
|
+
const stack = new Set();
|
|
325
|
+
const path = [];
|
|
326
|
+
|
|
327
|
+
const dfs = (node) => {
|
|
328
|
+
if (stack.has(node)) {
|
|
329
|
+
// Found a cycle
|
|
330
|
+
const cycleStart = path.indexOf(node);
|
|
331
|
+
const cycle = path.slice(cycleStart);
|
|
332
|
+
cycle.push(node);
|
|
333
|
+
cycles.push(cycle);
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (visited.has(node)) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
visited.add(node);
|
|
342
|
+
stack.add(node);
|
|
343
|
+
path.push(node);
|
|
344
|
+
|
|
345
|
+
const deps = graph[node] || [];
|
|
346
|
+
for (const dep of deps) {
|
|
347
|
+
dfs(dep);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
path.pop();
|
|
351
|
+
stack.delete(node);
|
|
352
|
+
return false;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
for (const node of Object.keys(graph)) {
|
|
356
|
+
if (!visited.has(node)) {
|
|
357
|
+
dfs(node);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
hasCircular: cycles.length > 0,
|
|
363
|
+
cycles,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Calculate topological order for dependencies
|
|
369
|
+
* @param {Object} graph - Dependency graph { repoPath: [depPaths] }
|
|
370
|
+
* @returns {string[]} Ordered array of repo paths
|
|
371
|
+
*/
|
|
372
|
+
calculateDependencyOrder(graph) {
|
|
373
|
+
const nodes = Object.keys(graph);
|
|
374
|
+
const visited = new Set();
|
|
375
|
+
const order = [];
|
|
376
|
+
|
|
377
|
+
const visit = (node) => {
|
|
378
|
+
if (visited.has(node)) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
visited.add(node);
|
|
382
|
+
|
|
383
|
+
// Visit dependencies first
|
|
384
|
+
const deps = graph[node] || [];
|
|
385
|
+
for (const dep of deps) {
|
|
386
|
+
if (graph.hasOwnProperty(dep)) {
|
|
387
|
+
visit(dep);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
order.push(node);
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// Visit all nodes
|
|
395
|
+
for (const node of nodes) {
|
|
396
|
+
visit(node);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return order;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Get repos affected by changes to a given repo
|
|
404
|
+
* @param {string} repoPath - Path of changed repo
|
|
405
|
+
* @returns {string[]} Array of affected repo paths
|
|
406
|
+
*/
|
|
407
|
+
getAffectedRepos(repoPath) {
|
|
408
|
+
// Make sure we have scanned
|
|
409
|
+
if (!this.cachedResult) {
|
|
410
|
+
this.scan();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const affected = new Set();
|
|
414
|
+
const graph = this.cachedResult.dependencyGraph;
|
|
415
|
+
|
|
416
|
+
// Build reverse graph (dependents -> dependencies)
|
|
417
|
+
const reverseGraph = {};
|
|
418
|
+
for (const [repo, deps] of Object.entries(graph)) {
|
|
419
|
+
for (const dep of deps) {
|
|
420
|
+
if (!reverseGraph[dep]) {
|
|
421
|
+
reverseGraph[dep] = [];
|
|
422
|
+
}
|
|
423
|
+
reverseGraph[dep].push(repo);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// BFS to find all affected repos
|
|
428
|
+
const queue = [repoPath];
|
|
429
|
+
while (queue.length > 0) {
|
|
430
|
+
const current = queue.shift();
|
|
431
|
+
const dependents = reverseGraph[current] || [];
|
|
432
|
+
|
|
433
|
+
for (const dependent of dependents) {
|
|
434
|
+
if (!affected.has(dependent)) {
|
|
435
|
+
affected.add(dependent);
|
|
436
|
+
queue.push(dependent);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return Array.from(affected);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Clear cached results
|
|
446
|
+
*/
|
|
447
|
+
clearCache() {
|
|
448
|
+
this.cachedResult = null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
module.exports = { WorkspaceScanner };
|