| 15 | } |
| 16 | |
| 17 | export function computeDaemonCodeSignature( |
| 18 | entryPath: string, |
| 19 | root: string = findProjectRoot(), |
| 20 | ): string { |
| 21 | try { |
| 22 | const normalizedRoot = path.resolve(root); |
| 23 | const normalizedEntryPath = path.resolve(entryPath); |
| 24 | const queue = [normalizedEntryPath]; |
| 25 | const visited = new Set<string>(); |
| 26 | const fingerprintParts: string[] = []; |
| 27 | |
| 28 | while (queue.length > 0) { |
| 29 | const currentPath = queue.pop(); |
| 30 | if (!currentPath || visited.has(currentPath)) continue; |
| 31 | visited.add(currentPath); |
| 32 | |
| 33 | const stat = fs.statSync(currentPath); |
| 34 | if (!stat.isFile()) continue; |
| 35 | |
| 36 | const relativePath = path.relative(normalizedRoot, currentPath) || currentPath; |
| 37 | fingerprintParts.push(`${relativePath}:${stat.size}:${Math.trunc(stat.mtimeMs)}`); |
| 38 | |
| 39 | const content = fs.readFileSync(currentPath, 'utf8'); |
| 40 | for (const specifier of collectRelativeImportSpecifiers(content)) { |
| 41 | const dependencyPath = resolveRelativeImportPath(currentPath, specifier); |
| 42 | if (dependencyPath) { |
| 43 | queue.push(dependencyPath); |
| 44 | } |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | const fingerprint = fingerprintParts.sort().join('|'); |
| 49 | const hash = crypto.createHash('sha1').update(fingerprint).digest('hex'); |
| 50 | return `graph:${fingerprintParts.length}:${hash}`; |
| 51 | } catch { |
| 52 | return 'unknown'; |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | function collectRelativeImportSpecifiers(content: string): string[] { |
| 57 | const specifiers = new Set<string>(); |