import { readdirSync, readFileSync, statSync } from 'fs' import { createHash } from 'crypto' import { join, relative } from 'path' export interface FileInfo { hash: string mtime: string size: number } export interface Manifest { files: Record name: string } const EXCLUDE_PATTERNS = [ 'node_modules', '.DS_Store', '*.log', 'dist', 'build', '.env.local', '.git', 'bun.lockb', ] export function computeHash(content: Buffer | string): string { return createHash('sha256').update(content).digest('hex') } export function generateManifest(appPath: string, appName: string): Manifest { const files: Record = {} function walkDir(dir: string) { const entries = readdirSync(dir, { withFileTypes: true }) for (const entry of entries) { const fullPath = join(dir, entry.name) const relativePath = relative(appPath, fullPath) // Check exclusions if (shouldExclude(relativePath)) continue if (entry.isDirectory()) { walkDir(fullPath) } else if (entry.isFile()) { const content = readFileSync(fullPath) const stats = statSync(fullPath) files[relativePath] = { hash: computeHash(content), mtime: stats.mtime.toISOString(), size: stats.size, } } } } walkDir(appPath) return { name: appName, files, } } function shouldExclude(path: string): boolean { const parts = path.split('/') for (const pattern of EXCLUDE_PATTERNS) { // Exact match or starts with (for directories) if (parts.includes(pattern)) return true // Wildcard patterns if (pattern.startsWith('*')) { const ext = pattern.slice(1) if (path.endsWith(ext)) return true } } return false }