prunify 0.1.2 → 0.1.4
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/dist/cli.cjs +202 -101
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +202 -101
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/core/parser.ts","../src/utils/file.ts","../src/core/graph.ts","../src/modules/dead-code.ts","../src/core/reporter.ts","../src/modules/dupe-finder.ts","../src/utils/ast.ts","../src/modules/dep-check.ts","../src/modules/health-report.ts","../src/modules/circular.ts","../src/modules/assets.ts"],"sourcesContent":["import { Command } from 'commander'\nimport chalk from 'chalk'\nimport Table from 'cli-table3'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport readline from 'node:readline'\n\n// Read version from package.json at runtime so it is always in sync.\n// Works in both ESM (import.meta.url defined) and CJS (tsup injects __dirname).\nfunction readPkgVersion(): string {\n try {\n // ESM path\n if (typeof import.meta !== 'undefined' && import.meta.url) {\n const dir = path.dirname(fileURLToPath(import.meta.url))\n const pkgPath = path.resolve(dir, '..', 'package.json')\n return (JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version: string }).version\n }\n } catch {\n // fall through\n }\n try {\n // CJS path — __dirname is the dist/ folder\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const dir: string = (globalThis as any).__dirname ?? __dirname\n const pkgPath = path.resolve(dir, '..', 'package.json')\n return (JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version: string }).version\n } catch {\n return '0.0.0'\n }\n}\n\nconst PKG_VERSION = readPkgVersion()\nimport { discoverFiles, buildProject, getImportsForFile } from '@/core/parser.js'\nimport { buildGraph, findEntryPoints, detectCycles, type ImportGraph } from '@/core/graph.js'\nimport { runDeadCodeModule } from '@/modules/dead-code.js'\nimport { runDupeFinder } from '@/modules/dupe-finder.js'\nimport { runDepCheck } from '@/modules/dep-check.js'\nimport { runHealthReport } from '@/modules/health-report.js'\nimport { runAssetCheck } from '@/modules/assets.js'\nimport {\n createSpinner,\n ensureReportsDir,\n appendToGitignore,\n writeReport,\n} from '@/core/reporter.js'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\ntype Module = 'dead-code' | 'dupes' | 'circular' | 'deps' | 'assets' | 'health'\n\nconst ALL_MODULES: Module[] = ['dead-code', 'dupes', 'circular', 'deps', 'assets']\n\ninterface CliOptions {\n dir: string\n entry?: string\n only?: string\n ignore: string[]\n out?: string\n html?: boolean\n delete?: boolean\n ci?: boolean\n}\n\n// ─── CLI definition ───────────────────────────────────────────────────────────\n\nconst program = new Command()\n\nprogram\n .name('prunify')\n .description('npm run clean. ship with confidence.')\n .version(PKG_VERSION, '-v, --version')\n .option('--dir <path>', 'Root directory to analyze', process.cwd())\n .option('--entry <path>', 'Override entry point')\n .option('--only <modules>', 'Comma-separated: dead-code,dupes,circular,deps,health')\n .option(\n '--ignore <pattern>',\n 'Glob pattern to ignore (repeatable)',\n (val: string, acc: string[]) => [...acc, val],\n [] as string[],\n )\n .option('--out <path>', 'Output directory for reports')\n .option('--html', 'Also generate code_health.html')\n .option('--delete', 'Prompt to delete dead files after analysis')\n .option('--ci', 'CI mode: exit 1 if issues found, no interactive prompts')\n .action(main)\n\nprogram.parse()\n\n// ─── Main ─────────────────────────────────────────────────────────────────────\n\nasync function main(opts: CliOptions): Promise<void> {\n const rootDir = path.resolve(opts.dir)\n\n if (!fs.existsSync(path.join(rootDir, 'package.json'))) {\n console.error(chalk.red(`✗ No package.json found in ${rootDir}`))\n console.error(chalk.dim(' Use --dir <path> to point to your project root.'))\n process.exit(1)\n }\n\n const modules = resolveModules(opts.only)\n\n // Banner\n console.log()\n console.log(chalk.bold.cyan('🧹 prunify — npm run clean. ship with confidence.'))\n console.log()\n\n // Step 3: Parse codebase\n const parseSpinner = createSpinner(chalk.cyan('Parsing codebase…'))\n const files = discoverFiles(rootDir, opts.ignore)\n parseSpinner.succeed(chalk.green(`Parsed codebase — ${files.length} file(s) found`))\n\n // Step 4: Build import graph\n const graphSpinner = createSpinner(chalk.cyan('Building import graph…'))\n const project = buildProject(files)\n const graph: ImportGraph = buildGraph(files, (f) => {\n const sf = project.getSourceFile(f)\n return sf ? getImportsForFile(sf) : []\n })\n const edgeCount = [...graph.values()].reduce((n, s) => n + s.size, 0)\n graphSpinner.succeed(chalk.green(`Import graph built — ${edgeCount} edge(s)`))\n\n const packageJson = loadPackageJson(rootDir)\n const entryPoints = opts.entry ? [path.resolve(opts.entry)] : findEntryPoints(rootDir, packageJson)\n\n // Set up reports dir\n const reportsDir = ensureReportsDir(rootDir, opts.out)\n appendToGitignore(rootDir)\n\n console.log()\n\n // ─── Run modules ─────────────────────────────────────────────────────────\n\n let deadFileCount = 0\n let dupeCount = 0\n let unusedPkgCount = 0\n let circularCount = 0\n let unusedAssetCount = 0\n\n let deadReportFile = ''\n let dupesReportFile = ''\n let depsReportFile = ''\n let circularReportFile = ''\n let assetsReportFile = ''\n\n const deadFilePaths: string[] = []\n\n if (modules.includes('dead-code')) {\n const spinner = createSpinner(chalk.cyan('Analysing dead code…'))\n const result = runDeadCodeModule(project, graph, entryPoints, rootDir)\n deadFileCount = result.deadFiles.length + result.deadExports.length\n deadFilePaths.push(...result.deadFiles)\n spinner.succeed(chalk.green(`Dead code analysis complete — ${deadFileCount} item(s) found`))\n\n if (result.report) {\n deadReportFile = 'dead-code.txt'\n writeReport(reportsDir, deadReportFile, result.report)\n }\n }\n\n if (modules.includes('dupes')) {\n const outputPath = path.join(reportsDir, 'dupes.md')\n const dupes = await runDupeFinder(rootDir, { output: outputPath })\n dupeCount = dupes.length\n if (dupeCount > 0) dupesReportFile = 'dupes.md'\n }\n\n if (modules.includes('circular')) {\n const spinner = createSpinner(chalk.cyan('Analysing circular imports…'))\n const cycles = detectCycles(graph)\n circularCount = cycles.length\n spinner.succeed(chalk.green(`Circular import analysis complete — ${circularCount} cycle(s) found`))\n\n if (circularCount > 0) {\n circularReportFile = 'circular.txt'\n const cycleText = cycles\n .map((c, i) => `Cycle ${i + 1}: ${c.join(' → ')}`)\n .join('\\n')\n writeReport(reportsDir, circularReportFile, cycleText)\n }\n }\n\n if (modules.includes('deps')) {\n const outputPath = path.join(reportsDir, 'deps.md')\n const issues = await runDepCheck({ cwd: rootDir, output: outputPath })\n unusedPkgCount = issues.filter((i) => i.type === 'unused').length\n if (issues.length > 0) depsReportFile = 'deps.md'\n }\n\n if (modules.includes('assets')) {\n const outputPath = path.join(reportsDir, 'assets.md')\n const unusedAssets = await runAssetCheck(rootDir, { output: outputPath })\n unusedAssetCount = unusedAssets.length\n if (unusedAssetCount > 0) assetsReportFile = 'assets.md'\n }\n\n if (modules.includes('health')) {\n const outputPath = path.join(reportsDir, 'health-report.md')\n await runHealthReport(rootDir, { output: outputPath })\n }\n\n if (opts.html) {\n const htmlPath = path.join(reportsDir, 'code_health.html')\n writeHtmlReport(htmlPath, rootDir, deadFilePaths, circularCount, dupeCount, unusedPkgCount)\n console.log(chalk.cyan(` HTML report written to ${htmlPath}`))\n }\n\n // ─── Summary table ────────────────────────────────────────────────────────\n\n console.log()\n console.log(chalk.bold('Summary'))\n console.log()\n\n const table = new Table({\n head: [chalk.bold('Check'), chalk.bold('Found'), chalk.bold('Output File')],\n style: { head: [], border: [] },\n })\n\n const fmt = (n: number) => (n > 0 ? chalk.yellow(String(n)) : chalk.green('0'))\n\n table.push(\n ['Dead Files / Exports', fmt(deadFileCount), deadReportFile || '—'],\n ['Duplicate Clusters', fmt(dupeCount), dupesReportFile || '—'],\n ['Unused Packages', fmt(unusedPkgCount), depsReportFile || '—'],\n ['Circular Deps', fmt(circularCount), circularReportFile || '—'],\n ['Unused Assets', fmt(unusedAssetCount), assetsReportFile || '—'],\n )\n\n console.log(table.toString())\n console.log()\n\n // ─── --delete ─────────────────────────────────────────────────────────────\n\n if (opts.delete && deadFilePaths.length > 0) {\n console.log(chalk.yellow(`Dead files (${deadFilePaths.length}):`))\n for (const f of deadFilePaths) {\n console.log(chalk.dim(` ${path.relative(rootDir, f)}`))\n }\n console.log()\n\n if (!opts.ci) {\n const confirmed = await confirmPrompt('Delete these files? (y/N) ')\n if (confirmed) {\n for (const f of deadFilePaths) {\n fs.rmSync(f, { force: true })\n }\n console.log(chalk.green(` Deleted ${deadFilePaths.length} file(s).`))\n } else {\n console.log(chalk.dim(' Skipped.'))\n }\n }\n }\n\n // ─── --ci ─────────────────────────────────────────────────────────────────\n\n if (opts.ci) {\n const hasIssues = deadFileCount > 0 || dupeCount > 0 || unusedPkgCount > 0 || circularCount > 0 || unusedAssetCount > 0\n if (hasIssues) process.exit(1)\n }\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction resolveModules(only: string | undefined): Module[] {\n if (!only) return ALL_MODULES\n const valid = new Set<Module>([...ALL_MODULES, 'health'])\n return only\n .split(',')\n .map((s) => s.trim() as Module)\n .filter((m) => valid.has(m))\n}\n\nfunction loadPackageJson(dir: string): unknown {\n try {\n return JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'))\n } catch {\n return null\n }\n}\n\nfunction confirmPrompt(question: string): Promise<boolean> {\n return new Promise((resolve) => {\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout })\n rl.question(question, (answer) => {\n rl.close()\n resolve(answer.trim().toLowerCase() === 'y')\n })\n })\n}\n\nfunction writeHtmlReport(\n outputPath: string,\n rootDir: string,\n deadFiles: string[],\n circularCount: number,\n dupeCount: number,\n unusedPkgCount: number,\n): void {\n const rows = [\n ['Dead Files / Exports', String(deadFiles.length)],\n ['Duplicate Clusters', String(dupeCount)],\n ['Circular Dependencies', String(circularCount)],\n ['Unused Packages', String(unusedPkgCount)],\n ]\n .map(([label, val]) => ` <tr><td>${label}</td><td>${val}</td></tr>`)\n .join('\\n')\n\n const deadList =\n deadFiles.length > 0\n ? `<ul>${deadFiles.map((f) => `<li>${path.relative(rootDir, f)}</li>`).join('')}</ul>`\n : '<p>None</p>'\n\n const html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>prunify — Code Health Report</title>\n <style>\n body { font-family: system-ui, sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }\n h1 { color: #0ea5e9; }\n table { border-collapse: collapse; width: 100%; margin-bottom: 2rem; }\n th, td { border: 1px solid #e2e8f0; padding: .5rem 1rem; text-align: left; }\n th { background: #f8fafc; }\n small { color: #94a3b8; }\n </style>\n</head>\n<body>\n <h1>🧹 prunify — Code Health Report</h1>\n <small>Generated ${new Date().toISOString()}</small>\n <h2>Summary</h2>\n <table>\n <tr><th>Check</th><th>Found</th></tr>\n${rows}\n </table>\n <h2>Dead Files</h2>\n ${deadList}\n</body>\n</html>`\n\n fs.mkdirSync(path.dirname(outputPath), { recursive: true })\n fs.writeFileSync(outputPath, html, 'utf-8')\n}\n\n","import fs from 'node:fs'\nimport path from 'node:path'\nimport { Project, SourceFile, Node, SyntaxKind } from 'ts-morph'\nimport { glob } from '@/utils/file.js'\n\n// ─── Interfaces ──────────────────────────────────────────────────────────────\n\nexport interface ParsedFile {\n path: string\n sourceFile: SourceFile\n exports: string[]\n imports: ImportEntry[]\n}\n\nexport interface ImportEntry {\n moduleSpecifier: string\n namedImports: string[]\n defaultImport: string | undefined\n isTypeOnly: boolean\n}\n\n// ─── Constants ────────────────────────────────────────────────────────────────\n\nconst DEFAULT_IGNORE = [\n 'node_modules',\n 'node_modules/**',\n 'dist',\n 'dist/**',\n '.next',\n '.next/**',\n 'coverage',\n 'coverage/**',\n '**/*.test.ts',\n '**/*.test.tsx',\n '**/*.test.js',\n '**/*.test.jsx',\n '**/*.spec.ts',\n '**/*.spec.tsx',\n '**/*.spec.js',\n '**/*.spec.jsx',\n '**/*.stories.ts',\n '**/*.stories.tsx',\n '**/*.d.ts',\n]\n\nconst SOURCE_PATTERNS = ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx']\n\nconst RESOLVE_EXTENSIONS = [\n '',\n '.ts',\n '.tsx',\n '.js',\n '.jsx',\n] as const\n\nconst INDEX_FILES = ['index.ts', 'index.tsx', 'index.js', 'index.jsx'] as const\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Recursively discovers all .ts/.tsx/.js/.jsx files under `rootDir`.\n *\n * Default exclusions: node_modules, dist, .next, coverage,\n * *.test.ts, *.spec.ts, *.stories.tsx, *.d.ts\n *\n * Additional glob-style patterns can be passed via `ignore`.\n */\nexport function discoverFiles(rootDir: string, ignore: string[] = []): string[] {\n return glob(rootDir, SOURCE_PATTERNS, [...DEFAULT_IGNORE, ...ignore])\n}\n\n/**\n * Creates a ts-morph Project loaded with the given source files.\n *\n * If `tsconfigPath` is not provided, the function walks up the directory tree\n * from the first file in the list to locate a `tsconfig.json`. When found, the\n * project is initialised with it so that path aliases are resolved correctly.\n */\nexport function buildProject(files: string[], tsconfigPath?: string): Project {\n const resolved = tsconfigPath ?? (files.length > 0 ? findTsconfig(files[0]) : undefined)\n\n const project = resolved\n ? new Project({ tsConfigFilePath: resolved, skipAddingFilesFromTsConfig: true })\n : new Project({\n compilerOptions: {\n allowJs: true,\n resolveJsonModule: true,\n },\n })\n\n project.addSourceFilesAtPaths(files)\n return project\n}\n\n/**\n * Returns the resolved absolute file paths of every local module imported by\n * `sourceFile`.\n *\n * Handles:\n * - Named imports: `import { X } from './foo'`\n * - Default imports: `import Foo from './foo'`\n * - Namespace imports: `import * as Foo from './foo'`\n * - Re-exports: `export { X } from './foo'`\n * - Dynamic imports: `const m = await import('./foo')`\n * - Index resolution: `import './components'` → `./components/index.ts`\n * - Path aliases: `import '@/utils/foo'` (via tsconfig paths)\n * - node_modules filtered: bare specifiers like `'chalk'` are excluded\n */\nexport function getImportsForFile(sourceFile: SourceFile): string[] {\n const result = new Set<string>()\n const fileDir = path.dirname(sourceFile.getFilePath())\n const project = sourceFile.getProject()\n const compilerOptions = project.getCompilerOptions()\n const pathAliases = (compilerOptions.paths ?? {}) as Record<string, string[]>\n const baseUrl = compilerOptions.baseUrl\n\n function addResolved(sf: SourceFile | undefined): void {\n if (!sf) return\n const p = path.normalize(sf.getFilePath())\n if (\n !p.includes(`${path.sep}node_modules${path.sep}`) &&\n !p.includes('/node_modules/') &&\n path.normalize(sourceFile.getFilePath()) !== p\n ) {\n result.add(p)\n }\n }\n\n function resolveAndAdd(specifier: string): void {\n if (!specifier) return\n const isRelative = specifier.startsWith('./') || specifier.startsWith('../')\n\n if (isRelative) {\n const p = resolveRelativePath(fileDir, specifier, project)\n if (p) result.add(p)\n } else if (!specifier.startsWith('node:')) {\n const p = resolvePathAlias(specifier, pathAliases, baseUrl, project)\n if (p) result.add(p)\n }\n }\n\n // 1. Static import declarations\n for (const decl of sourceFile.getImportDeclarations()) {\n const sf = decl.getModuleSpecifierSourceFile()\n sf ? addResolved(sf) : resolveAndAdd(decl.getModuleSpecifierValue())\n }\n\n // 2. Re-export declarations: export { X } from '...'\n for (const decl of sourceFile.getExportDeclarations()) {\n const specifier = decl.getModuleSpecifierValue()\n if (!specifier) continue\n const sf = decl.getModuleSpecifierSourceFile()\n sf ? addResolved(sf) : resolveAndAdd(specifier)\n }\n\n // 3. Dynamic imports: import('...')\n for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {\n if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue\n const args = call.getArguments()\n if (args.length > 0 && Node.isStringLiteral(args[0])) {\n resolveAndAdd(args[0].getLiteralValue())\n }\n }\n\n return [...result]\n}\n\n// ─── Legacy helpers (used by other modules) ───────────────────────────────────\n\nexport function createProject(dir: string): Project {\n const files = glob(dir, SOURCE_PATTERNS, [\n 'node_modules',\n 'node_modules/**',\n 'dist',\n 'dist/**',\n '**/*.d.ts',\n ])\n return buildProject(files)\n}\n\nexport function parseFile(sourceFile: SourceFile): ParsedFile {\n const exports: string[] = []\n const imports: ImportEntry[] = []\n\n for (const exportDecl of sourceFile.getExportDeclarations()) {\n if (exportDecl.isTypeOnly()) continue\n for (const named of exportDecl.getNamedExports()) {\n exports.push(named.getName())\n }\n }\n\n for (const exportSymbol of sourceFile.getExportedDeclarations()) {\n exports.push(exportSymbol[0])\n }\n\n for (const importDecl of sourceFile.getImportDeclarations()) {\n imports.push({\n moduleSpecifier: importDecl.getModuleSpecifierValue(),\n namedImports: importDecl.getNamedImports().map((n) => n.getName()),\n defaultImport: importDecl.getDefaultImport()?.getText(),\n isTypeOnly: importDecl.isTypeOnly(),\n })\n }\n\n return {\n path: sourceFile.getFilePath(),\n sourceFile,\n exports: [...new Set(exports)],\n imports,\n }\n}\n\nexport function parseProject(dir: string): Map<string, ParsedFile> {\n const project = createProject(dir)\n const result = new Map<string, ParsedFile>()\n for (const sourceFile of project.getSourceFiles()) {\n const parsed = parseFile(sourceFile)\n result.set(parsed.path, parsed)\n }\n return result\n}\n\nexport function isNodeReferenced(node: Node, sourceFile: SourceFile): boolean {\n if (!Node.isReferenceFindable(node)) return false\n return node\n .findReferences()\n .some((ref) =>\n ref\n .getReferences()\n .some((r) => r.getSourceFile().getFilePath() !== sourceFile.getFilePath()),\n )\n}\n\n// ─── Internal resolution helpers ─────────────────────────────────────────────\n\n/**\n * Resolves a relative specifier (e.g. `'./utils'`) from `fromDir` to an\n * absolute path that exists inside the project, trying all source extensions\n * and index file variants.\n */\nfunction resolveRelativePath(\n fromDir: string,\n specifier: string,\n project: Project,\n): string | null {\n const base = path.resolve(fromDir, specifier)\n\n for (const ext of RESOLVE_EXTENSIONS) {\n const sf = project.getSourceFile(base + ext)\n if (sf) return path.normalize(sf.getFilePath())\n }\n\n for (const index of INDEX_FILES) {\n const sf = project.getSourceFile(path.join(base, index))\n if (sf) return path.normalize(sf.getFilePath())\n }\n\n return null\n}\n\n/**\n * Resolves a path-alias specifier (e.g. `'@/utils/file'`) using the\n * `paths` map from tsconfig. Returns the absolute file path or null.\n */\nfunction resolvePathAlias(\n specifier: string,\n pathAliases: Record<string, string[]>,\n baseUrl: string | undefined,\n project: Project,\n): string | null {\n for (const [alias, targets] of Object.entries(pathAliases)) {\n const match = matchAlias(alias, specifier)\n if (!match) continue\n\n const capture = match[1] ?? ''\n\n for (const target of targets) {\n const resolved = target.replaceAll('*', capture)\n const absolute = baseUrl ? path.resolve(baseUrl, resolved) : path.resolve(resolved)\n const hit = tryResolveAbsolute(absolute, project)\n if (hit) return hit\n }\n }\n\n return null\n}\n\n/** Converts an alias pattern (e.g. `@/*`) to a RegExp and tests `specifier`. */\nfunction matchAlias(alias: string, specifier: string): RegExpExecArray | null {\n // Escape all regex metacharacters except `*`, then replace `*` with a capture group.\n const escaped = alias.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&')\n const pattern = escaped.replaceAll('*', '(.*)')\n return new RegExp(`^${pattern}$`).exec(specifier)\n}\n\n/** Tries all source extensions and index file variants for an absolute base path. */\nfunction tryResolveAbsolute(absolute: string, project: Project): string | null {\n for (const ext of RESOLVE_EXTENSIONS) {\n const sf = project.getSourceFile(absolute + ext)\n if (sf) return path.normalize(sf.getFilePath())\n }\n for (const index of INDEX_FILES) {\n const sf = project.getSourceFile(path.join(absolute, index))\n if (sf) return path.normalize(sf.getFilePath())\n }\n return null\n}\n\n/**\n * Walks up the directory tree from `fromFile` to find the nearest\n * `tsconfig.json`.\n */\nfunction findTsconfig(fromFile: string): string | undefined {\n let dir = path.dirname(fromFile)\n const root = path.parse(dir).root\n\n while (dir !== root) {\n const candidate = path.join(dir, 'tsconfig.json')\n if (fs.existsSync(candidate)) return candidate\n const parent = path.dirname(dir)\n if (parent === dir) break\n dir = parent\n }\n\n return undefined\n}\n","import fs from 'node:fs'\nimport path from 'node:path'\nimport { minimatch } from 'minimatch'\n\n/**\n * Recursively collects files under `dir` that match any of `patterns`\n * and do not match any of `ignore` patterns.\n */\nexport function glob(\n dir: string,\n patterns: string[],\n ignore: string[] = [],\n): string[] {\n const results: string[] = []\n collect(dir, dir, patterns, ignore, results)\n return results\n}\n\nfunction collect(\n base: string,\n current: string,\n patterns: string[],\n ignore: string[],\n results: string[],\n): void {\n let entries: fs.Dirent[]\n\n try {\n entries = fs.readdirSync(current, { withFileTypes: true })\n } catch {\n return\n }\n\n for (const entry of entries) {\n const fullPath = path.join(current, entry.name)\n const relativePath = path.relative(base, fullPath).replace(/\\\\/g, '/')\n\n if (entry.isDirectory()) {\n // For directories: exact matching only — `partial: true` would cause\n // patterns like `**/*.test.ts` to spuriously match directory names.\n const isIgnored = ignore.some((pattern) => minimatch(relativePath, pattern))\n if (!isIgnored) collect(base, fullPath, patterns, ignore, results)\n } else if (entry.isFile()) {\n const isIgnored = ignore.some((pattern) => minimatch(relativePath, pattern))\n if (!isIgnored) {\n const matches = patterns.some((pattern) => minimatch(relativePath, pattern))\n if (matches) results.push(fullPath)\n }\n }\n }\n}\n\n/**\n * Ensures a directory exists, creating it (and parents) if needed.\n */\nexport function ensureDir(dirPath: string): void {\n if (!fs.existsSync(dirPath)) {\n fs.mkdirSync(dirPath, { recursive: true })\n }\n}\n\n/**\n * Reads a file as UTF-8 text, returning `null` if it does not exist.\n */\nexport function readFileSafe(filePath: string): string | null {\n try {\n return fs.readFileSync(filePath, 'utf-8')\n } catch {\n return null\n }\n}\n","import fs from 'node:fs'\nimport path from 'node:path'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\n/**\n * Directed adjacency list: file path → set of file paths it imports.\n */\nexport type ImportGraph = Map<string, Set<string>>\n\nexport interface GraphAnalysis {\n deadFiles: string[]\n liveFiles: string[]\n cycles: string[][]\n}\n\n// ─── Core functions ───────────────────────────────────────────────────────────\n\n/**\n * Builds a directed `ImportGraph` from a list of file paths.\n *\n * @param files - All files that should be nodes in the graph.\n * @param getImports - Returns the resolved absolute import paths for a file.\n */\nexport function buildGraph(\n files: string[],\n getImports: (file: string) => string[],\n): ImportGraph {\n const graph: ImportGraph = new Map()\n\n for (const file of files) {\n graph.set(file, new Set())\n }\n\n for (const file of files) {\n for (const imported of getImports(file)) {\n graph.get(file)?.add(imported)\n // Ensure imported node exists even if it wasn't in the initial list\n if (!graph.has(imported)) graph.set(imported, new Set())\n }\n }\n\n return graph\n}\n\n/**\n * Resolves the entry points for a project.\n *\n * Resolution order:\n * 1. Next.js: all files inside `pages/` and `app/` directories.\n * 2. package.json `\"main\"` and `\"module\"` fields.\n * 3. Common fallbacks: src/main, src/index, src/App, index (all extensions).\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function findEntryPoints(rootDir: string, packageJson: any): string[] {\n const entries = [\n ...resolveNextJsEntries(rootDir),\n ...resolvePkgFieldEntries(rootDir, packageJson),\n ...resolveFallbackEntries(rootDir),\n ]\n\n return [...new Set(entries)]\n}\n\n/**\n * Returns files in the graph that are not imported by any other file —\n * i.e. natural roots of the dependency tree.\n *\n * If every file is imported by at least one other (e.g. a full circular graph),\n * returns all files so nothing is incorrectly flagged as dead.\n */\nexport function findRootFiles(graph: ImportGraph): string[] {\n const imported = new Set<string>()\n for (const deps of graph.values()) {\n for (const dep of deps) imported.add(dep)\n }\n const roots = [...graph.keys()].filter((f) => !imported.has(f))\n return roots.length > 0 ? roots : [...graph.keys()]\n}\n\n/**\n * Iterative DFS from all `entryPoints`, returning every reachable file.\n * Safe against circular references.\n */\nexport function runDFS(graph: ImportGraph, entryPoints: string[]): Set<string> {\n const visited = new Set<string>()\n const stack = [...entryPoints]\n let node: string | undefined\n\n while ((node = stack.pop()) !== undefined) {\n if (visited.has(node)) continue\n visited.add(node)\n for (const neighbor of graph.get(node) ?? []) {\n if (!visited.has(neighbor)) stack.push(neighbor)\n }\n }\n\n return visited\n}\n\n/**\n * For each dead file, finds its full dead chain: files that are transitively\n * imported only by other dead files (i.e. removing the dead root would also\n * make them unreachable from any live entry point).\n *\n * Returns `Map<deadFile, chainMembers[]>`.\n */\nexport function findDeadChains(\n graph: ImportGraph,\n deadFiles: Set<string>,\n): Map<string, string[]> {\n const reverseGraph = buildReverseGraph(graph)\n const result = new Map<string, string[]>()\n\n for (const deadRoot of deadFiles) {\n result.set(deadRoot, dfsDeadChain(deadRoot, graph, deadFiles, reverseGraph))\n }\n\n return result\n}\n\n/**\n * Detects all circular dependency chains using iterative DFS with an explicit\n * path stack. Duplicate cycles (the same cycle starting at a different node)\n * are deduplicated by normalising to the lexicographically smallest rotation.\n */\nexport function detectCycles(graph: ImportGraph): string[][] {\n const cycles: string[][] = []\n const seenKeys = new Set<string>()\n const visited = new Set<string>()\n const inStack = new Set<string>()\n const path: string[] = []\n\n const acc: CycleAccumulator = { seenKeys, cycles }\n\n for (const start of graph.keys()) {\n if (!visited.has(start)) {\n dfsForCycles(start, graph, visited, inStack, path, acc)\n }\n }\n\n return cycles\n}\n\n// ─── Legacy aliases (used by existing modules) ────────────────────────────────\n\n/** @deprecated Use `runDFS` instead. */\nexport function dfs(start: string, graph: ImportGraph): Set<string> {\n return runDFS(graph, [start])\n}\n\n/** @deprecated Use `detectCycles` instead. */\nexport function findCircularGroups(graph: ImportGraph): string[][] {\n return detectCycles(graph)\n}\n\n// ─── Entry-point resolution helpers ──────────────────────────────────────────\n\nfunction resolveNextJsEntries(rootDir: string): string[] {\n const isNext =\n fs.existsSync(path.join(rootDir, 'next.config.js')) ||\n fs.existsSync(path.join(rootDir, 'next.config.ts')) ||\n fs.existsSync(path.join(rootDir, 'next.config.mjs'))\n\n if (!isNext) return []\n\n const entries: string[] = []\n for (const dir of ['pages', 'app'] as const) {\n const dirPath = path.join(rootDir, dir)\n if (fs.existsSync(dirPath)) entries.push(...collectSourceFiles(dirPath))\n }\n return entries\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction resolvePkgFieldEntries(rootDir: string, packageJson: any): string[] {\n const entries: string[] = []\n for (const field of ['main', 'module'] as const) {\n const value = packageJson?.[field]\n if (typeof value !== 'string') continue\n const abs = path.resolve(rootDir, value)\n if (fs.existsSync(abs)) entries.push(abs)\n }\n return entries\n}\n\nfunction resolveFallbackEntries(rootDir: string): string[] {\n const candidates = [\n 'src/main.ts', 'src/main.tsx', 'src/main.js', 'src/main.jsx',\n 'src/index.ts', 'src/index.tsx', 'src/index.js', 'src/index.jsx',\n 'src/App.ts', 'src/App.tsx', 'src/App.js', 'src/App.jsx',\n 'index.ts', 'index.tsx', 'index.js', 'index.jsx',\n ]\n return candidates\n .map((rel) => path.join(rootDir, rel))\n .filter((abs) => fs.existsSync(abs))\n}\n\n// ─── Cycle detection helpers ─────────────────────────────────────────────────\n\ntype CycleFrame = { node: string; neighbors: Iterator<string>; entered: boolean }\ntype CycleAccumulator = { seenKeys: Set<string>; cycles: string[][] }\n\nfunction mkFrame(node: string, graph: ImportGraph): CycleFrame {\n return { node, neighbors: (graph.get(node) ?? new Set()).values(), entered: false }\n}\n\nfunction dfsForCycles(\n start: string,\n graph: ImportGraph,\n visited: Set<string>,\n inStack: Set<string>,\n path: string[],\n acc: CycleAccumulator,\n): void {\n const stack: CycleFrame[] = [mkFrame(start, graph)]\n\n while (stack.length > 0) {\n const frame = stack.at(-1)\n if (!frame) break\n\n if (!frame.entered) {\n if (visited.has(frame.node)) { stack.pop(); continue }\n frame.entered = true\n inStack.add(frame.node)\n path.push(frame.node)\n }\n\n const { done, value: neighbor } = frame.neighbors.next()\n\n if (done) {\n stack.pop()\n path.pop()\n inStack.delete(frame.node)\n visited.add(frame.node)\n } else {\n handleCycleNeighbor(neighbor, stack, path, inStack, visited, acc, graph)\n }\n }\n}\n\nfunction handleCycleNeighbor(\n neighbor: string,\n stack: CycleFrame[],\n path: string[],\n inStack: Set<string>,\n visited: Set<string>,\n acc: CycleAccumulator,\n graph: ImportGraph,\n): void {\n if (inStack.has(neighbor)) {\n recordCycle(neighbor, path, acc)\n } else if (!visited.has(neighbor)) {\n stack.push(mkFrame(neighbor, graph))\n }\n}\n\nfunction recordCycle(\n cycleStart: string,\n path: string[],\n acc: CycleAccumulator,\n): void {\n const idx = path.indexOf(cycleStart)\n if (idx === -1) return\n const cycle = normalizeCycle(path.slice(idx))\n const key = cycle.join('\\0')\n if (!acc.seenKeys.has(key)) {\n acc.seenKeys.add(key)\n acc.cycles.push(cycle)\n }\n}\n\n// ─── Dead-chain helpers ───────────────────────────────────────────────────────\n\nfunction dfsDeadChain(\n deadRoot: string,\n graph: ImportGraph,\n deadFiles: Set<string>,\n reverseGraph: Map<string, Set<string>>,\n): string[] {\n const chain: string[] = []\n const visited = new Set<string>()\n const stack = [...(graph.get(deadRoot) ?? [])]\n let node: string | undefined\n\n while ((node = stack.pop()) !== undefined) {\n if (visited.has(node) || node === deadRoot) continue\n visited.add(node)\n\n if (deadFiles.has(node) || isOnlyImportedByDead(node, deadFiles, reverseGraph)) {\n chain.push(node)\n for (const next of graph.get(node) ?? []) {\n if (!visited.has(next)) stack.push(next)\n }\n }\n }\n\n return chain\n}\n\nfunction isOnlyImportedByDead(\n file: string,\n deadFiles: Set<string>,\n reverseGraph: Map<string, Set<string>>,\n): boolean {\n const importers = reverseGraph.get(file) ?? new Set<string>()\n return importers.size === 0 || [...importers].every((imp) => deadFiles.has(imp))\n}\n\n// ─── General helpers ──────────────────────────────────────────────────────────\n\n/** Reverses all edges in the graph. */\nfunction buildReverseGraph(graph: ImportGraph): Map<string, Set<string>> {\n const rev = new Map<string, Set<string>>()\n\n for (const [file] of graph) {\n if (!rev.has(file)) rev.set(file, new Set())\n }\n\n for (const [file, imports] of graph) {\n for (const imp of imports) {\n if (!rev.has(imp)) rev.set(imp, new Set())\n rev.get(imp)?.add(file)\n }\n }\n\n return rev\n}\n\n/**\n * Rotates `cycle` so it starts at the lexicographically smallest element,\n * producing a canonical form for deduplication.\n */\nfunction normalizeCycle(cycle: string[]): string[] {\n if (cycle.length === 0) return cycle\n const minIdx = cycle.reduce(\n (best, cur, i) => (cur < cycle[best] ? i : best),\n 0,\n )\n return [...cycle.slice(minIdx), ...cycle.slice(0, minIdx)]\n}\n\n/** Collects all .ts/.tsx/.js/.jsx files under a directory recursively. */\nfunction collectSourceFiles(dir: string): string[] {\n const results: string[] = []\n const SOURCE_RE = /\\.(tsx?|jsx?)$/\n\n function walk(current: string): void {\n let entries: fs.Dirent[]\n try {\n entries = fs.readdirSync(current, { withFileTypes: true })\n } catch {\n return\n }\n for (const entry of entries) {\n const full = path.join(current, entry.name)\n if (entry.isDirectory()) {\n walk(full)\n } else if (entry.isFile() && SOURCE_RE.test(entry.name)) {\n results.push(full)\n }\n }\n }\n\n walk(dir)\n return results\n}\n","import ora from 'ora'\nimport chalk from 'chalk'\nimport Table from 'cli-table3'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { Project, Node } from 'ts-morph'\nimport { discoverFiles, buildProject, getImportsForFile } from '@/core/parser.js'\nimport {\n buildGraph,\n findEntryPoints,\n findDeadChains,\n findRootFiles,\n runDFS,\n type ImportGraph,\n} from '@/core/graph.js'\nimport { writeJson, writeMarkdown } from '@/core/reporter.js'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface DeadExport {\n filePath: string\n exportName: string\n line: number\n}\n\nexport interface DeadCodeResult {\n deadFiles: string[]\n liveFiles: Set<string>\n chains: Map<string, string[]>\n deadExports: DeadExport[]\n report: string\n}\n\nexport interface DeadCodeOptions {\n output?: string\n json?: boolean\n}\n\n// ─── Public module API ────────────────────────────────────────────────────────\n\n/**\n * Orchestrates the full dead-code analysis pipeline.\n *\n * @param project - A ts-morph Project containing all source files.\n * @param graph - Pre-built import graph.\n * @param entryPoints - Files to treat as reachable roots.\n * @param rootDir - Project root (used for relative path display).\n */\nexport function runDeadCodeModule(\n project: Project,\n graph: ImportGraph,\n entryPoints: string[],\n rootDir: string,\n): DeadCodeResult {\n const allFiles = [...graph.keys()]\n const effectiveEntries = entryPoints.length > 0 ? entryPoints : findRootFiles(graph)\n\n const liveFiles = runDFS(graph, effectiveEntries)\n const deadFiles = allFiles.filter((f) => !liveFiles.has(f))\n const deadSet = new Set(deadFiles)\n\n const chains = findDeadChains(graph, deadSet)\n const deadExports = findDeadExports(project, liveFiles)\n const report = buildDeadCodeReport(deadFiles, chains, deadExports, rootDir)\n\n return { deadFiles, liveFiles, chains, deadExports, report }\n}\n\n/**\n * For every live file, finds named exports that are never used (imported by\n * name) by any other live file in the project.\n *\n * Handles: named exports, re-exports, default exports.\n * Skips exports that are pulled in via a namespace import (`import * as X`).\n */\nexport function findDeadExports(\n project: Project,\n liveFiles: Set<string>,\n): DeadExport[] {\n const importedNames = buildImportedNameMap(project, liveFiles)\n const dead: DeadExport[] = []\n\n for (const filePath of liveFiles) {\n collectFileDeadExports(filePath, project, importedNames, dead)\n }\n\n return dead\n}\n\nfunction collectFileDeadExports(\n filePath: string,\n project: Project,\n importedNames: Map<string, Set<string>>,\n dead: DeadExport[],\n): void {\n const sf = project.getSourceFile(filePath)\n if (!sf) return\n\n const usedNames = importedNames.get(filePath) ?? new Set<string>()\n if (usedNames.has('*')) return\n\n for (const [exportName, declarations] of sf.getExportedDeclarations()) {\n if (usedNames.has(exportName)) continue\n const line = getExportLine(declarations[0])\n dead.push({ filePath, exportName, line })\n }\n}\n\nfunction getExportLine(decl: import('ts-morph').ExportedDeclarations): number {\n if (!decl) return 0\n if (!Node.isNode(decl)) return 0\n return decl.getStartLineNumber()\n}\n\n/**\n * Returns the size of `filePath` in bytes, or 0 if the file cannot be read.\n */\nexport function getFileSize(filePath: string): number {\n try {\n return fs.statSync(filePath).size\n } catch {\n return 0\n }\n}\n\n/**\n * Builds a human-readable plain-text report suitable for writing to\n * `code_cleaning.txt`.\n */\nexport function buildDeadCodeReport(\n deadFiles: string[],\n chains: Map<string, string[]>,\n deadExports: DeadExport[],\n rootDir: string,\n): string {\n const rel = (p: string) => path.relative(rootDir, p).replaceAll('\\\\', '/')\n\n const totalBytes = deadFiles.reduce((sum, f) => {\n const chain = chains.get(f) ?? []\n return sum + getFileSize(f) + chain.reduce((s, c) => s + getFileSize(c), 0)\n }, 0)\n const totalKb = (totalBytes / 1024).toFixed(1)\n\n const lines: string[] = [\n '========================================',\n ' DEAD CODE REPORT',\n ` Dead files : ${deadFiles.length}`,\n ` Dead exports: ${deadExports.length}`,\n ` Recoverable : ~${totalKb} KB`,\n '========================================',\n '',\n ]\n\n if (deadFiles.length > 0) {\n lines.push('── DEAD FILES ──', '')\n for (const filePath of deadFiles) {\n const chain = chains.get(filePath) ?? []\n const allFiles = [filePath, ...chain]\n const sizeBytes = allFiles.reduce((s, f) => s + getFileSize(f), 0)\n const sizeKb = (sizeBytes / 1024).toFixed(1)\n const chainStr =\n chain.length > 0\n ? [rel(filePath), ...chain.map(rel)].join(' → ')\n : rel(filePath)\n\n const plural = allFiles.length === 1 ? '' : 's'\n\n lines.push(\n `DEAD FILE — ${rel(filePath)}`,\n `Reason: Not imported anywhere in the codebase`,\n `Chain: ${chainStr}`,\n `Size: ~${sizeKb} KB removable across ${allFiles.length} file${plural}`,\n `Action: Safe to delete all ${allFiles.length} file${plural}`,\n '',\n )\n }\n }\n\n if (deadExports.length > 0) {\n lines.push('── DEAD EXPORTS ──', '')\n for (const entry of deadExports) {\n lines.push(\n `DEAD EXPORT — ${rel(entry.filePath)} → ${entry.exportName}() [line ${entry.line}]`,\n `Reason: Exported but never imported`,\n `Action: Remove the export (file itself is still live)`,\n '',\n )\n }\n }\n\n return lines.join('\\n')\n}\n\n// ─── CLI command (backward-compatible) ───────────────────────────────────────\n\n/** Legacy interface used by the CLI table printer. */\ninterface DeadExportRow {\n file: string\n exportName: string\n}\n\nexport async function runDeadCode(dir: string, opts: DeadCodeOptions): Promise<DeadExportRow[]> {\n const spinner = ora(chalk.cyan('Scanning for dead code…')).start()\n\n try {\n const fileList = discoverFiles(dir, [])\n const project = buildProject(fileList)\n const graph = buildGraph(fileList, (f) => {\n const sf = project.getSourceFile(f)\n return sf ? getImportsForFile(sf) : []\n })\n\n const packageJson = loadPackageJson(dir)\n const entries = findEntryPoints(dir, packageJson)\n\n const result = runDeadCodeModule(project, graph, entries, dir)\n\n // Flatten dead files → rows\n const dead: DeadExportRow[] = [\n ...result.deadFiles.map((f) => ({ file: f, exportName: '(entire file)' })),\n ...result.deadExports.map((e) => ({ file: e.filePath, exportName: e.exportName })),\n ]\n\n spinner.succeed(chalk.green(`Dead code scan complete — ${dead.length} item(s) found`))\n\n if (dead.length === 0) {\n console.log(chalk.green(' No dead code detected.'))\n return dead\n }\n\n printDeadTable(dead)\n writeDeadOutput(result, opts)\n\n return dead\n } catch (err) {\n spinner.fail(chalk.red('Dead code scan failed'))\n throw err\n }\n}\n\n// ─── Internal helpers ─────────────────────────────────────────────────────────\n\n/**\n * Builds a map of `filePath → Set<exportName>` for every name that is\n * imported from that file by any live file. A `*` entry means the file is\n * namespace-imported and all its exports are considered used.\n */\nfunction buildImportedNameMap(\n project: Project,\n liveFiles: Set<string>,\n): Map<string, Set<string>> {\n const importedNames = new Map<string, Set<string>>()\n\n const touch = (file: string, name: string) => {\n if (!importedNames.has(file)) importedNames.set(file, new Set())\n importedNames.get(file)!.add(name)\n }\n\n for (const filePath of liveFiles) {\n const sf = project.getSourceFile(filePath)\n if (!sf) continue\n processImports(sf, touch)\n processReExports(sf, touch)\n }\n\n return importedNames\n}\n\nfunction processImports(\n sf: ReturnType<Project['getSourceFiles']>[number],\n touch: (file: string, name: string) => void,\n): void {\n for (const decl of sf.getImportDeclarations()) {\n const resolved = decl.getModuleSpecifierSourceFile()\n if (!resolved) continue\n const target = resolved.getFilePath()\n\n if (decl.getNamespaceImport()) {\n touch(target, '*')\n continue\n }\n\n const defaultImport = decl.getDefaultImport()\n if (defaultImport) touch(target, 'default')\n\n for (const named of decl.getNamedImports()) {\n touch(target, named.getAliasNode()?.getText() ?? named.getName())\n }\n }\n}\n\nfunction processReExports(\n sf: ReturnType<Project['getSourceFiles']>[number],\n touch: (file: string, name: string) => void,\n): void {\n for (const decl of sf.getExportDeclarations()) {\n const resolved = decl.getModuleSpecifierSourceFile()\n if (!resolved) continue\n const target = resolved.getFilePath()\n\n if (decl.isNamespaceExport()) {\n touch(target, '*')\n continue\n }\n\n for (const named of decl.getNamedExports()) {\n touch(target, named.getName())\n }\n }\n}\n\nfunction loadPackageJson(dir: string): unknown {\n const pkgPath = path.join(dir, 'package.json')\n if (!fs.existsSync(pkgPath)) return null\n return JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))\n}\n\nfunction printDeadTable(dead: Array<{ file: string; exportName: string }>): void {\n const table = new Table({ head: ['File', 'Export'] })\n for (const row of dead) {\n table.push([row.file, row.exportName])\n }\n console.log(table.toString())\n}\n\nfunction writeDeadOutput(result: DeadCodeResult, opts: DeadCodeOptions): void {\n if (!opts.output) return\n\n if (opts.json) {\n writeJson(\n { deadFiles: result.deadFiles, deadExports: result.deadExports },\n opts.output,\n )\n console.log(chalk.cyan(` JSON written to ${opts.output}`))\n return\n }\n\n writeMarkdown(\n {\n title: 'Dead Code Report',\n summary: `${result.deadFiles.length} dead file(s), ${result.deadExports.length} dead export(s)`,\n sections: [\n {\n title: 'Dead Files',\n headers: ['File', 'Chain'],\n rows: result.deadFiles.map((f) => [\n f,\n (result.chains.get(f) ?? []).join(' → ') || '—',\n ]),\n },\n {\n title: 'Dead Exports',\n headers: ['File', 'Export', 'Line'],\n rows: result.deadExports.map((e) => [e.filePath, e.exportName, String(e.line)]),\n },\n ],\n generatedAt: new Date(),\n },\n opts.output,\n )\n console.log(chalk.cyan(` Report written to ${opts.output}`))\n}\n","import fs from 'node:fs'\nimport path from 'node:path'\nimport ora, { type Ora } from 'ora'\n\n// ─── Spinner ──────────────────────────────────────────────────────────────────\n\nexport type Spinner = Ora\n\n/**\n * Creates an ora spinner and starts it immediately.\n */\nexport function createSpinner(text: string): Spinner {\n return ora(text).start()\n}\n\n// ─── Reports dir ─────────────────────────────────────────────────────────────\n\nconst REPORTS_DIR_NAME = 'prunify-reports'\n\n/**\n * Creates (if needed) a `prunify-reports/` folder inside `outDir ?? rootDir`\n * and returns its absolute path.\n */\nexport function ensureReportsDir(rootDir: string, outDir?: string): string {\n const base = outDir ? path.resolve(outDir) : path.resolve(rootDir)\n const reportsDir = path.join(base, REPORTS_DIR_NAME)\n if (!fs.existsSync(reportsDir)) {\n fs.mkdirSync(reportsDir, { recursive: true })\n }\n return reportsDir\n}\n\n/**\n * Writes `content` to `reportsDir/filename` and logs the path.\n */\nexport function writeReport(reportsDir: string, filename: string, content: string): void {\n ensureDir(reportsDir)\n const filePath = path.join(reportsDir, filename)\n fs.writeFileSync(filePath, content, 'utf-8')\n console.log(` Report saved → ${filePath}`)\n}\n\n/**\n * Appends `prunify-reports/` to `.gitignore` if not already present.\n */\nexport function appendToGitignore(rootDir: string): void {\n const gitignorePath = path.join(rootDir, '.gitignore')\n const entry = `${REPORTS_DIR_NAME}/`\n\n if (fs.existsSync(gitignorePath)) {\n const contents = fs.readFileSync(gitignorePath, 'utf-8')\n if (contents.split('\\n').some((line) => line.trim() === entry)) return\n fs.appendFileSync(gitignorePath, `\\n${entry}\\n`, 'utf-8')\n } else {\n fs.writeFileSync(gitignorePath, `${entry}\\n`, 'utf-8')\n }\n}\n\n/**\n * Converts a byte count to a human-readable string: \"4.2 KB\", \"1.1 MB\", etc.\n */\nexport function formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`\n if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`\n}\n\n// ─── Markdown / JSON writers ──────────────────────────────────────────────────\n\nexport interface ReportSection {\n title: string\n rows: string[][]\n headers?: string[]\n}\n\nexport interface Report {\n title: string\n summary: string\n sections: ReportSection[]\n generatedAt: Date\n}\n\n/**\n * Writes a Markdown report to `outputPath`.\n */\nexport function writeMarkdown(report: Report, outputPath: string): void {\n const lines: string[] = [\n `# ${report.title}`,\n '',\n `> ${report.summary}`,\n '',\n `_Generated: ${report.generatedAt.toISOString()}_`,\n '',\n ]\n\n for (const section of report.sections) {\n lines.push(`## ${section.title}`, '')\n\n if (section.rows.length === 0) {\n lines.push('_Nothing found._', '')\n continue\n }\n\n if (section.headers && section.headers.length > 0) {\n lines.push(`| ${section.headers.join(' | ')} |`)\n lines.push(`| ${section.headers.map(() => '---').join(' | ')} |`)\n }\n\n for (const row of section.rows) {\n lines.push(`| ${row.join(' | ')} |`)\n }\n\n lines.push('')\n }\n\n ensureDir(path.dirname(outputPath))\n fs.writeFileSync(outputPath, lines.join('\\n'), 'utf-8')\n}\n\n/**\n * Writes a JSON report to `outputPath`.\n */\nexport function writeJson(data: unknown, outputPath: string): void {\n ensureDir(path.dirname(outputPath))\n fs.writeFileSync(outputPath, JSON.stringify(data, null, 2), 'utf-8')\n}\n\nfunction ensureDir(dir: string): void {\n if (dir && !fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true })\n }\n}\n","import ora from 'ora'\nimport chalk from 'chalk'\nimport Table from 'cli-table3'\nimport fs from 'node:fs'\nimport { Project, SyntaxKind, VariableDeclarationKind } from 'ts-morph'\nimport { glob } from '@/utils/file.js'\nimport { hashFunctionBody, isExported } from '@/utils/ast.js'\nimport { writeMarkdown } from '@/core/reporter.js'\nimport { dupeClusterPrompt } from '@/prompt-gen/templates.js'\n\nexport interface DupeFinderOptions {\n output?: string\n minLines?: string\n}\n\nexport interface DuplicateBlock {\n hash: string\n lines: number\n occurrences: Array<{ file: string; startLine: number }>\n}\n\n/**\n * Detects duplicate code blocks across files using a rolling line-hash approach.\n */\nexport async function runDupeFinder(dir: string, opts: DupeFinderOptions): Promise<DuplicateBlock[]> {\n const minLines = parseInt(opts.minLines ?? '5', 10)\n const spinner = ora(chalk.cyan(`Scanning for duplicate blocks (≥${minLines} lines)…`)).start()\n\n try {\n const files = glob(dir, ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], ['node_modules', 'dist'])\n const blockMap = new Map<string, Array<{ file: string; startLine: number }>>()\n\n for (const filePath of files) {\n const content = fs.readFileSync(filePath, 'utf-8')\n const lines = content.split('\\n').map((l) => l.trim()).filter(Boolean)\n\n for (let i = 0; i <= lines.length - minLines; i++) {\n const block = lines.slice(i, i + minLines).join('\\n')\n if (!blockMap.has(block)) blockMap.set(block, [])\n blockMap.get(block)!.push({ file: filePath, startLine: i + 1 })\n }\n }\n\n const dupes: DuplicateBlock[] = []\n\n for (const [block, occurrences] of blockMap) {\n if (occurrences.length > 1) {\n dupes.push({\n hash: hashString(block),\n lines: minLines,\n occurrences,\n })\n }\n }\n\n spinner.succeed(chalk.green(`Duplicate scan complete — ${dupes.length} duplicate block(s) found`))\n\n if (dupes.length === 0) {\n console.log(chalk.green(' No duplicate blocks detected.'))\n return dupes\n }\n\n const table = new Table({ head: ['Hash', 'Lines', 'Count', 'First Occurrence'] })\n for (const d of dupes) {\n table.push([\n chalk.gray(d.hash.slice(0, 8)),\n String(d.lines),\n chalk.yellow(String(d.occurrences.length)),\n `${d.occurrences[0].file}:${d.occurrences[0].startLine}`,\n ])\n }\n console.log(table.toString())\n\n if (opts.output) {\n const rows = dupes.flatMap((d) =>\n d.occurrences.map((o) => [d.hash.slice(0, 8), String(d.lines), o.file, String(o.startLine)]),\n )\n writeMarkdown(\n {\n title: 'Duplicate Code Report',\n summary: `${dupes.length} duplicate block(s) found (min lines: ${minLines})`,\n sections: [{ title: 'Duplicates', headers: ['Hash', 'Lines', 'File', 'Start Line'], rows }],\n generatedAt: new Date(),\n },\n opts.output,\n )\n console.log(chalk.cyan(` Report written to ${opts.output}`))\n }\n\n return dupes\n } catch (err) {\n spinner.fail(chalk.red('Duplicate scan failed'))\n throw err\n }\n}\n\nfunction hashString(str: string): string {\n let hash = 5381\n for (let i = 0; i < str.length; i++) {\n hash = ((hash << 5) + hash) ^ str.charCodeAt(i)\n }\n return (hash >>> 0).toString(16).padStart(8, '0')\n}\n\n// ─── AST-based duplicate detection ───────────────────────────────────────────\n\nexport interface FunctionRecord {\n name: string\n filePath: string\n line: number\n bodyHash: string\n paramCount: number\n}\n\nexport interface DupeCluster {\n type: 'name' | 'structure'\n functions: FunctionRecord[]\n}\n\nexport interface ConstDupe {\n name: string\n value: string\n occurrences: Array<{ filePath: string; line: number }>\n}\n\n/**\n * Extracts named function declarations and exported const arrow functions from\n * all live files in the project, computing an AST body hash for each.\n */\nexport function extractFunctions(project: Project, liveFiles: Set<string>): FunctionRecord[] {\n const records: FunctionRecord[] = []\n\n for (const filePath of liveFiles) {\n const sf = project.getSourceFile(filePath)\n if (!sf) continue\n\n // Named function declarations\n for (const fn of sf.getFunctions()) {\n const name = fn.getName()\n if (!name) continue\n records.push({\n name,\n filePath,\n line: fn.getStartLineNumber(),\n bodyHash: hashFunctionBody(fn),\n paramCount: fn.getParameters().length,\n })\n }\n\n // Exported const arrow functions\n for (const stmt of sf.getVariableStatements()) {\n if (stmt.getDeclarationKind() !== VariableDeclarationKind.Const) continue\n if (!isExported(stmt)) continue\n\n for (const decl of stmt.getDeclarations()) {\n const arrowFn = decl.getInitializer()?.asKind(SyntaxKind.ArrowFunction)\n if (!arrowFn) continue\n records.push({\n name: decl.getName(),\n filePath,\n line: decl.getStartLineNumber(),\n bodyHash: hashFunctionBody(arrowFn),\n paramCount: arrowFn.getParameters().length,\n })\n }\n }\n }\n\n return records\n}\n\n/**\n * Groups functions whose names have a Levenshtein distance ≤ 2 using\n * union-find (transitive closure). Returns only clusters of ≥ 2 functions.\n */\nexport function clusterByName(functions: FunctionRecord[]): DupeCluster[] {\n const n = functions.length\n const parent = Array.from({ length: n }, (_, i) => i)\n\n function find(i: number): number {\n while (parent[i] !== i) {\n parent[i] = parent[parent[i]!]!\n i = parent[i]!\n }\n return i\n }\n\n for (let i = 0; i < n; i++) {\n for (let j = i + 1; j < n; j++) {\n if (levenshtein(functions[i]!.name, functions[j]!.name) <= 2) {\n parent[find(i)] = find(j)\n }\n }\n }\n\n const groups = new Map<number, FunctionRecord[]>()\n for (let i = 0; i < n; i++) {\n const root = find(i)\n const group = groups.get(root) ?? []\n group.push(functions[i]!)\n groups.set(root, group)\n }\n\n return [...groups.values()]\n .filter((g) => g.length >= 2)\n .map((g) => ({ type: 'name' as const, functions: g }))\n}\n\n/**\n * Groups functions with identical body hashes.\n * Returns only clusters of ≥ 2 functions.\n */\nexport function clusterByStructure(functions: FunctionRecord[]): DupeCluster[] {\n const groups = new Map<string, FunctionRecord[]>()\n\n for (const fn of functions) {\n if (!fn.bodyHash) continue\n const group = groups.get(fn.bodyHash) ?? []\n group.push(fn)\n groups.set(fn.bodyHash, group)\n }\n\n return [...groups.values()]\n .filter((g) => g.length >= 2)\n .map((g) => ({ type: 'structure' as const, functions: g }))\n}\n\n/**\n * Finds `const` declarations that share the same name AND literal value\n * across multiple live files.\n */\nexport function findDuplicateConstants(\n project: Project,\n liveFiles: Set<string>,\n): ConstDupe[] {\n const map = new Map<string, Array<{ filePath: string; line: number }>>()\n\n for (const filePath of liveFiles) {\n const sf = project.getSourceFile(filePath)\n if (!sf) continue\n\n for (const stmt of sf.getVariableStatements()) {\n if (stmt.getDeclarationKind() !== VariableDeclarationKind.Const) continue\n\n for (const decl of stmt.getDeclarations()) {\n const name = decl.getName()\n const value = getLiteralValue(decl.getInitializer())\n if (!value) continue\n\n const key = JSON.stringify({ name, value })\n const existing = map.get(key) ?? []\n existing.push({ filePath, line: decl.getStartLineNumber() })\n map.set(key, existing)\n }\n }\n }\n\n const result: ConstDupe[] = []\n for (const [key, occurrences] of map) {\n if (occurrences.length < 2) continue\n const { name, value } = JSON.parse(key) as { name: string; value: string }\n result.push({ name, value, occurrences })\n }\n return result\n}\n\n/**\n * Builds a human-readable plain-text report for code_suggest.txt, including\n * an AI prompt for each cluster.\n */\nexport function buildDupeReport(clusters: DupeCluster[], constDupes: ConstDupe[]): string {\n const lines: string[] = [\n '========================================',\n ' DUPLICATE CODE REPORT',\n ` Function clusters : ${clusters.length}`,\n ` Const duplicates : ${constDupes.length}`,\n '========================================',\n '',\n ]\n\n if (clusters.length > 0) {\n lines.push('── FUNCTION CLUSTERS ──', '')\n for (const cluster of clusters) {\n const label = cluster.type === 'name' ? 'Similar Name' : 'Identical Structure'\n lines.push(`CLUSTER [${label}]`)\n for (const fn of cluster.functions) {\n lines.push(` ${fn.name} ${fn.filePath}:${fn.line} (params: ${fn.paramCount})`)\n }\n lines.push('')\n lines.push('AI SUGGESTION:', dupeClusterPrompt([cluster]), '')\n }\n }\n\n if (constDupes.length > 0) {\n lines.push('── DUPLICATE CONSTANTS ──', '')\n for (const d of constDupes) {\n lines.push(`CONST ${d.name} = ${d.value}`)\n for (const o of d.occurrences) {\n lines.push(` ${o.filePath}:${o.line}`)\n }\n lines.push('')\n }\n }\n\n return lines.join('\\n')\n}\n\n// ─── Private helpers ─────────────────────────────────────────────────────────\n\nfunction levenshtein(a: string, b: string): number {\n const ma = a.length\n const mb = b.length\n let prev = Array.from({ length: mb + 1 }, (_, j) => j)\n\n for (let i = 1; i <= ma; i++) {\n const curr: number[] = [i]\n for (let j = 1; j <= mb; j++) {\n curr[j] =\n a[i - 1] === b[j - 1]\n ? prev[j - 1]!\n : 1 + Math.min(prev[j]!, curr[j - 1]!, prev[j - 1]!)\n }\n prev = curr\n }\n\n return prev[mb]!\n}\n\nfunction getLiteralValue(node: ReturnType<import('ts-morph').VariableDeclaration['getInitializer']>): string | undefined {\n if (!node) return undefined\n const kind = node.getKind()\n if (\n kind === SyntaxKind.StringLiteral ||\n kind === SyntaxKind.NumericLiteral ||\n kind === SyntaxKind.TrueKeyword ||\n kind === SyntaxKind.FalseKeyword ||\n kind === SyntaxKind.NoSubstitutionTemplateLiteral\n ) {\n return node.getText()\n }\n return undefined\n}\n","import crypto from 'node:crypto'\nimport {\n Node,\n SyntaxKind,\n FunctionDeclaration,\n ArrowFunction,\n VariableDeclaration,\n ClassDeclaration,\n InterfaceDeclaration,\n TypeAliasDeclaration,\n SourceFile,\n} from 'ts-morph'\n\nexport type TopLevelDeclaration =\n | FunctionDeclaration\n | VariableDeclaration\n | ClassDeclaration\n | InterfaceDeclaration\n | TypeAliasDeclaration\n\n/**\n * Returns all top-level named declarations in a SourceFile.\n */\nexport function getTopLevelDeclarations(sourceFile: SourceFile): TopLevelDeclaration[] {\n const decls: TopLevelDeclaration[] = []\n\n decls.push(...sourceFile.getFunctions())\n decls.push(...sourceFile.getClasses())\n decls.push(...sourceFile.getInterfaces())\n decls.push(...sourceFile.getTypeAliases())\n\n for (const varStmt of sourceFile.getVariableStatements()) {\n decls.push(...varStmt.getDeclarations())\n }\n\n return decls\n}\n\n/**\n * Returns true if the given node has a JSDoc `@internal` or `@private` tag.\n */\nexport function hasInternalTag(node: Node): boolean {\n if (!Node.isJSDocable(node)) return false\n return node.getJsDocs().some((doc) =>\n doc.getTags().some((tag) => {\n const name = tag.getTagName()\n return name === 'internal' || name === 'private'\n }),\n )\n}\n\n/**\n * Returns the leading comment text for a node, if any.\n */\nexport function getLeadingComment(node: Node): string | undefined {\n const fullText = node.getFullText()\n const match = fullText.match(/^(\\s*\\/\\/[^\\n]*\\n|\\s*\\/\\*[\\s\\S]*?\\*\\/\\s*)+/)\n return match?.[0]?.trim()\n}\n\n/**\n * Checks whether a node is exported (has the `export` keyword).\n */\nexport function isExported(node: Node): boolean {\n return node.getFirstDescendantByKind(SyntaxKind.ExportKeyword) !== undefined\n}\n\n/**\n * Returns the name of a declaration node, or undefined if it is anonymous.\n */\nexport function getDeclarationName(node: TopLevelDeclaration): string | undefined {\n if ('getName' in node && typeof node.getName === 'function') {\n return node.getName() ?? undefined\n }\n return undefined\n}\n\n/**\n * Hashes the body of a function by walking its AST and replacing all\n * identifier names with sequential placeholders, so structurally identical\n * functions produce the same hash regardless of variable names.\n */\nexport function hashFunctionBody(func: FunctionDeclaration | ArrowFunction): string {\n const body = func.getBody()\n if (!body) return ''\n const identMap = new Map<string, string>()\n const normalized = normalizeNode(body, identMap)\n return crypto.createHash('sha256').update(normalized).digest('hex')\n}\n\nfunction normalizeNode(node: Node, identMap: Map<string, string>): string {\n if (node.getKind() === SyntaxKind.Identifier) {\n const text = node.getText()\n if (!identMap.has(text)) identMap.set(text, `$${identMap.size}`)\n return identMap.get(text)!\n }\n const children = node.getChildren()\n if (children.length === 0) return node.getText()\n return `[${node.getKindName()}:${children.map((c) => normalizeNode(c, identMap)).join(',')}]`\n}\n","import ora from 'ora'\nimport chalk from 'chalk'\nimport Table from 'cli-table3'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { Project, SyntaxKind } from 'ts-morph'\nimport { glob } from '@/utils/file.js'\nimport { writeMarkdown } from '@/core/reporter.js'\n\nexport interface DepCheckOptions {\n cwd: string\n output?: string\n}\n\nexport interface DepIssue {\n name: string\n type: 'unused' | 'missing' | 'unlisted-dev'\n detail?: string\n}\n\n/**\n * Audits package.json against actual import usage in source files.\n */\nexport async function runDepCheck(opts: DepCheckOptions): Promise<DepIssue[]> {\n const spinner = ora(chalk.cyan('Auditing dependencies…')).start()\n\n try {\n const pkgPath = path.join(opts.cwd, 'package.json')\n if (!fs.existsSync(pkgPath)) {\n spinner.fail(chalk.red(`No package.json found at ${opts.cwd}`))\n return []\n }\n\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as {\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n }\n\n const declared = new Set([\n ...Object.keys(pkg.dependencies ?? {}),\n ...Object.keys(pkg.devDependencies ?? {}),\n ])\n\n const srcDir = path.join(opts.cwd, 'src')\n const files = glob(srcDir, ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], ['node_modules', 'dist'])\n\n const usedPackages = new Set<string>()\n\n for (const filePath of files) {\n const content = fs.readFileSync(filePath, 'utf-8')\n const importRegex = /from\\s+['\"]([^'\"./][^'\"]*)['\"]/g\n let match: RegExpExecArray | null\n\n while ((match = importRegex.exec(content)) !== null) {\n const specifier = match[1]\n // Normalise scoped packages: @org/pkg → @org/pkg\n const pkgName = specifier.startsWith('@')\n ? specifier.split('/').slice(0, 2).join('/')\n : specifier.split('/')[0]\n usedPackages.add(pkgName)\n }\n }\n\n const issues: DepIssue[] = []\n\n // Unused declared dependencies\n for (const dep of declared) {\n if (!usedPackages.has(dep)) {\n issues.push({ name: dep, type: 'unused' })\n }\n }\n\n // Undeclared (missing) dependencies\n for (const pkg of usedPackages) {\n if (!declared.has(pkg) && !isBuiltin(pkg)) {\n issues.push({ name: pkg, type: 'missing' })\n }\n }\n\n spinner.succeed(chalk.green(`Dependency audit complete — ${issues.length} issue(s) found`))\n\n if (issues.length === 0) {\n console.log(chalk.green(' All dependencies look healthy.'))\n return issues\n }\n\n const table = new Table({ head: ['Package', 'Issue'] })\n for (const issue of issues) {\n const label =\n issue.type === 'unused'\n ? chalk.yellow('unused')\n : issue.type === 'missing'\n ? chalk.red('missing from package.json')\n : chalk.magenta('unlisted dev dep')\n table.push([chalk.gray(issue.name), label])\n }\n console.log(table.toString())\n\n if (opts.output) {\n writeMarkdown(\n {\n title: 'Dependency Audit Report',\n summary: `${issues.length} dependency issue(s) found`,\n sections: [\n {\n title: 'Issues',\n headers: ['Package', 'Type'],\n rows: issues.map((i) => [i.name, i.type]),\n },\n ],\n generatedAt: new Date(),\n },\n opts.output,\n )\n console.log(chalk.cyan(` Report written to ${opts.output}`))\n }\n\n return issues\n } catch (err) {\n spinner.fail(chalk.red('Dependency audit failed'))\n throw err\n }\n}\n\nconst NODE_BUILTINS = new Set([\n 'node:fs', 'node:path', 'node:os', 'node:url', 'node:crypto', 'node:util',\n 'node:stream', 'node:events', 'node:child_process', 'node:process',\n 'fs', 'path', 'os', 'url', 'crypto', 'util', 'stream', 'events', 'child_process',\n])\n\nfunction isBuiltin(name: string): boolean {\n return NODE_BUILTINS.has(name) || name.startsWith('node:')\n}\n\n// ─── Module API ───────────────────────────────────────────────────────────────\n\nexport interface DepCheckResult {\n unusedPackages: string[]\n missingPackages: string[]\n report: string\n}\n\n/**\n * Packages that live exclusively in config/tooling files and are never\n * imported in application source code. Prunify skips these so it doesn't\n * raise false-positive \"unused\" warnings.\n */\nconst CONFIG_ONLY_PACKAGES = new Set([\n // linters / formatters\n 'eslint', '@eslint/js', 'eslint-config-prettier', 'eslint-plugin-react',\n 'eslint-plugin-import', 'eslint-plugin-node', 'eslint-plugin-jsx-a11y',\n 'eslint-plugin-unicorn', '@typescript-eslint/eslint-plugin', '@typescript-eslint/parser',\n 'prettier',\n // type-only packages\n // (handled separately via @types/ prefix check)\n // TypeScript & transpilers\n 'typescript', 'ts-node', 'tsx',\n // test runners\n 'jest', 'ts-jest', '@jest/globals', 'vitest', '@vitest/ui', '@vitest/coverage-v8',\n // bundlers / build tools\n 'rollup', 'webpack', 'vite', 'esbuild', 'tsup', 'parcel',\n '@rollup/plugin-node-resolve', '@rollup/plugin-commonjs', '@rollup/plugin-typescript',\n 'babel', '@babel/core', '@babel/preset-env', '@babel/preset-typescript',\n // task runners / release\n 'nodemon', 'concurrently', 'npm-run-all', 'cross-env', 'rimraf',\n 'husky', 'lint-staged', 'commitizen', 'semantic-release',\n // monorepo\n 'turbo', 'nx', 'lerna',\n // CSS tooling\n 'postcss', 'autoprefixer', 'tailwindcss',\n])\n\n/**\n * Synchronous module-level dependency check backed by a pre-built ts-morph\n * `Project`. Flags packages declared in `package.json` that are never\n * imported in any source file (excluding peer deps, `@types/*`, and\n * config-only tooling listed in `CONFIG_ONLY_PACKAGES`).\n */\nexport function runDepCheckModule(rootDir: string, project: Project): DepCheckResult {\n const pkgPath = path.join(rootDir, 'package.json')\n if (!fs.existsSync(pkgPath)) {\n return { unusedPackages: [], missingPackages: [], report: buildDepReport([], []) }\n }\n\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as {\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n peerDependencies?: Record<string, string>\n }\n\n // peerDependencies are intentionally excluded — they're provided by the host\n const declared = new Set([\n ...Object.keys(pkg.dependencies ?? {}),\n ...Object.keys(pkg.devDependencies ?? {}),\n ])\n\n // Collect all package names actually referenced in source\n const usedPackages = new Set<string>()\n for (const sf of project.getSourceFiles()) {\n if (sf.getFilePath().endsWith('.d.ts')) continue\n\n // Static imports: import { X } from 'pkg'\n for (const importDecl of sf.getImportDeclarations()) {\n const name = extractPackageName(importDecl.getModuleSpecifierValue())\n if (name) usedPackages.add(name)\n }\n\n // Re-exports: export { X } from 'pkg'\n for (const exportDecl of sf.getExportDeclarations()) {\n const spec = exportDecl.getModuleSpecifierValue()\n if (!spec) continue\n const name = extractPackageName(spec)\n if (name) usedPackages.add(name)\n }\n\n // Dynamic imports: import('pkg') and require('pkg')\n for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {\n const expr = call.getExpression().getText()\n if (expr !== 'require' && expr !== 'import') continue\n const args = call.getArguments()\n if (args.length === 0) continue\n const first = args[0]\n if (!first?.isKind(SyntaxKind.StringLiteral)) continue\n const name = extractPackageName(first.getLiteralValue())\n if (name) usedPackages.add(name)\n }\n }\n\n const unusedPackages: string[] = []\n for (const dep of declared) {\n if (\n !usedPackages.has(dep) &&\n !CONFIG_ONLY_PACKAGES.has(dep) &&\n !dep.startsWith('@types/')\n ) {\n unusedPackages.push(dep)\n }\n }\n\n const report = buildDepReport(unusedPackages, [])\n return { unusedPackages, missingPackages: [], report }\n}\n\nfunction extractPackageName(specifier: string): string | null {\n if (!specifier) return null\n // Relative / absolute paths are not packages\n if (specifier.startsWith('.') || specifier.startsWith('/')) return null\n // Node builtins\n if (isBuiltin(specifier)) return null\n // Scoped packages: @org/pkg[/sub/path]\n if (specifier.startsWith('@')) {\n const parts = specifier.split('/')\n return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null\n }\n // Regular packages: pkg[/sub/path]\n return specifier.split('/')[0]!\n}\n\nfunction buildDepReport(unusedPackages: string[], missingPackages: string[]): string {\n const lines: string[] = [\n '========================================',\n ' DEPENDENCY CHECK',\n ` Unused packages : ${unusedPackages.length}`,\n ` Missing from package.json : ${missingPackages.length}`,\n '========================================',\n '',\n ]\n\n if (unusedPackages.length > 0) {\n lines.push('── UNUSED PACKAGES ──', '')\n for (const pkg of unusedPackages) {\n lines.push(` ${pkg}`, ` Declared in package.json but never imported in source.`, '')\n }\n }\n\n if (missingPackages.length > 0) {\n lines.push('── MISSING FROM package.json ──', '')\n for (const pkg of missingPackages) {\n lines.push(` ${pkg}`, ` Imported in source but not listed in package.json.`, '')\n }\n }\n\n if (unusedPackages.length === 0 && missingPackages.length === 0) {\n lines.push(' All dependencies look healthy.', '')\n }\n\n return lines.join('\\n')\n}\n","import chalk from 'chalk'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { Project } from 'ts-morph'\nimport { runDeadCode } from '@/modules/dead-code.js'\nimport { runDupeFinder } from '@/modules/dupe-finder.js'\nimport { runCircular } from '@/modules/circular.js'\nimport { runDepCheck } from '@/modules/dep-check.js'\nimport { writeMarkdown } from '@/core/reporter.js'\nimport type { Report, ReportSection } from '@/core/reporter.js'\nimport type { DeadCodeResult } from '@/modules/dead-code.js'\nimport type { DupeCluster, ConstDupe } from '@/modules/dupe-finder.js'\nimport type { CircularResult } from '@/modules/circular.js'\nimport type { DepCheckResult } from '@/modules/dep-check.js'\n\nexport interface HealthReportOptions {\n output: string\n}\n\n/**\n * Orchestrates all analysis modules and writes a combined Markdown health report.\n */\nexport async function runHealthReport(dir: string, opts: HealthReportOptions): Promise<void> {\n console.log(chalk.bold.cyan('\\n prunify — Codebase Health Report\\n'))\n\n const [deadExports, dupes, cycles, depIssues] = await Promise.all([\n runDeadCode(dir, {}).catch(() => []),\n runDupeFinder(dir, {}).catch(() => []),\n runCircular(dir, {}).catch(() => []),\n runDepCheck({ cwd: path.resolve(dir, '..'), output: undefined }).catch(() => []),\n ])\n\n const sections: ReportSection[] = [\n {\n title: '🚨 Dead Exports',\n headers: ['File', 'Export'],\n rows: deadExports.map((d) => [d.file, d.exportName]),\n },\n {\n title: '🔁 Duplicate Blocks',\n headers: ['Hash', 'Lines', 'Occurrences'],\n rows: dupes.map((d) => [d.hash.slice(0, 8), String(d.lines), String(d.occurrences.length)]),\n },\n {\n title: '♻️ Circular Imports',\n headers: ['Cycle #', 'Chain'],\n rows: cycles.map((cycle, i) => [String(i + 1), cycle.join(' → ')]),\n },\n {\n title: '📦 Dependency Issues',\n headers: ['Package', 'Issue'],\n rows: depIssues.map((i) => [i.name, i.type]),\n },\n ]\n\n const totalIssues =\n deadExports.length + dupes.length + cycles.length + depIssues.length\n\n const report: Report = {\n title: 'Codebase Health Report',\n summary: `Analysed: ${path.resolve(dir)} | Total issues: ${totalIssues}`,\n sections,\n generatedAt: new Date(),\n }\n\n writeMarkdown(report, opts.output)\n\n console.log(\n chalk.bold(`\\n Health report written to `) + chalk.cyan(opts.output),\n )\n console.log(\n chalk.dim(` Total issues found: `) +\n (totalIssues > 0 ? chalk.red(String(totalIssues)) : chalk.green('0')),\n )\n}\n\n// ─── Module API ───────────────────────────────────────────────────────────────\n\nexport interface DupeModuleResult {\n clusters: DupeCluster[]\n constDupes: ConstDupe[]\n}\n\nexport interface AllModuleResults {\n dead: DeadCodeResult\n circular: CircularResult\n deps: DepCheckResult\n dupes: DupeModuleResult\n}\n\nexport interface HealthResult {\n score: number\n report: string\n html?: string\n}\n\n/**\n * Computes a codebase health score (0–100) from pre-computed module results,\n * builds a `code_health.txt` report, and optionally generates a self-contained\n * `code_health.html` with a colour-coded gauge.\n *\n * Scoring deductions:\n * -2 per dead file (max -20)\n * -3 per dupe cluster (max -15)\n * -5 per circular dep (max -20)\n * -2 per unused package (max -10)\n * -1 per barrel ≥ 15 exports (max -10)\n * -1 per file > 300 lines (max -10)\n */\nexport function runHealthReportModule(\n allResults: AllModuleResults,\n project: Project,\n html?: boolean,\n): HealthResult {\n const { dead, circular, deps, dupes } = allResults\n\n // ── Metrics ──────────────────────────────────────────────────────────────\n const deadFileCount = dead.deadFiles.length\n const dupeClusterCount = dupes.clusters.length\n const circularCount = circular.cycles.length\n const unusedPkgCount = deps.unusedPackages.length\n\n let barrelFileCount = 0\n let longFileCount = 0\n for (const sf of project.getSourceFiles()) {\n if (sf.getFilePath().endsWith('.d.ts')) continue\n // Barrel: file with 15+ distinct exported names\n const exportedCount = [...sf.getExportedDeclarations().keys()].length\n if (exportedCount >= 15) barrelFileCount++\n // Long file: > 300 lines\n if (sf.getEndLineNumber() > 300) longFileCount++\n }\n\n // ── Score ─────────────────────────────────────────────────────────────────\n let score = 100\n score -= Math.min(deadFileCount * 2, 20)\n score -= Math.min(dupeClusterCount * 3, 15)\n score -= Math.min(circularCount * 5, 20)\n score -= Math.min(unusedPkgCount * 2, 10)\n score -= Math.min(barrelFileCount * 1, 10)\n score -= Math.min(longFileCount * 1, 10)\n score = Math.max(0, score)\n\n // ── Text report ───────────────────────────────────────────────────────────\n const report = buildHealthReport(\n score,\n dead,\n circular,\n deps,\n dupes,\n barrelFileCount,\n longFileCount,\n )\n\n // ── HTML report ───────────────────────────────────────────────────────────\n const htmlContent = html\n ? buildHealthHtml(score, dead, circular, deps, dupes, barrelFileCount, longFileCount)\n : undefined\n\n return { score, report, html: htmlContent }\n}\n\n// ─── Text report builder ──────────────────────────────────────────────────────\n\nfunction buildHealthReport(\n score: number,\n dead: DeadCodeResult,\n circular: CircularResult,\n deps: DepCheckResult,\n dupes: DupeModuleResult,\n barrelFileCount: number,\n longFileCount: number,\n): string {\n const grade = score >= 80 ? 'GOOD' : score >= 50 ? 'FAIR' : 'POOR'\n const lines: string[] = [\n '========================================',\n ' CODE HEALTH REPORT',\n ` Score : ${score}/100 (${grade})`,\n ' Generated: ' + new Date().toISOString(),\n '========================================',\n '',\n ]\n\n // Dead code section\n lines.push('── DEAD CODE ──', '')\n if (dead.deadFiles.length === 0 && dead.deadExports.length === 0) {\n lines.push(' No dead files or exports detected.', '')\n } else {\n if (dead.deadFiles.length > 0) {\n lines.push(` Dead files: ${dead.deadFiles.length}`)\n for (const f of dead.deadFiles) lines.push(` • ${f}`)\n lines.push('')\n }\n if (dead.deadExports.length > 0) {\n lines.push(` Dead exports: ${dead.deadExports.length}`)\n for (const e of dead.deadExports)\n lines.push(` • ${e.filePath} → ${e.exportName} (line ${e.line})`)\n lines.push('')\n }\n }\n\n // Circular imports section\n lines.push('── CIRCULAR IMPORTS ──', '')\n if (circular.cycles.length === 0) {\n lines.push(' No circular imports detected.', '')\n } else {\n lines.push(` Cycles: ${circular.cycles.length}`)\n for (let i = 0; i < circular.cycles.length; i++) {\n const cycle = circular.cycles[i]!\n lines.push(` Cycle ${i + 1}: ${[...cycle, cycle[0]].join(' → ')}`)\n }\n lines.push('')\n }\n\n // Dependency check section\n lines.push('── DEPENDENCIES ──', '')\n if (deps.unusedPackages.length === 0) {\n lines.push(' All declared packages are used.', '')\n } else {\n lines.push(` Unused packages: ${deps.unusedPackages.length}`)\n for (const p of deps.unusedPackages) lines.push(` • ${p}`)\n lines.push('')\n }\n\n // Duplicate code section\n lines.push('── DUPLICATE CODE ──', '')\n if (dupes.clusters.length === 0 && dupes.constDupes.length === 0) {\n lines.push(' No duplicate functions or constants detected.', '')\n } else {\n if (dupes.clusters.length > 0) {\n lines.push(` Function clusters: ${dupes.clusters.length}`)\n for (const c of dupes.clusters) {\n const label = c.type === 'name' ? 'similar-name' : 'identical-body'\n lines.push(` [${label}] ${c.functions.map((f) => f.name).join(', ')}`)\n }\n lines.push('')\n }\n if (dupes.constDupes.length > 0) {\n lines.push(` Duplicate constants: ${dupes.constDupes.length}`)\n for (const d of dupes.constDupes) lines.push(` • ${d.name} = ${d.value}`)\n lines.push('')\n }\n }\n\n // Code quality section\n lines.push('── CODE QUALITY ──', '')\n lines.push(` Barrel files (≥15 exports) : ${barrelFileCount}`)\n lines.push(` Long files (>300 lines) : ${longFileCount}`)\n lines.push('')\n\n // Deductions summary\n lines.push('── SCORE BREAKDOWN ──', '')\n lines.push(` Starting score : 100`)\n lines.push(` Dead files (-${Math.min(dead.deadFiles.length * 2, 20)})`)\n lines.push(` Dupe clusters (-${Math.min(dupes.clusters.length * 3, 15)})`)\n lines.push(` Circular deps (-${Math.min(circular.cycles.length * 5, 20)})`)\n lines.push(` Unused packages (-${Math.min(deps.unusedPackages.length * 2, 10)})`)\n lines.push(` Barrel files (-${Math.min(barrelFileCount, 10)})`)\n lines.push(` Long files (-${Math.min(longFileCount, 10)})`)\n lines.push(` ─────────────────────────────────`)\n lines.push(` Final score : ${score}/100`)\n lines.push('')\n\n return lines.join('\\n')\n}\n\n// ─── HTML report builder ──────────────────────────────────────────────────────\n\nfunction buildHealthHtml(\n score: number,\n dead: DeadCodeResult,\n circular: CircularResult,\n deps: DepCheckResult,\n dupes: DupeModuleResult,\n barrelFileCount: number,\n longFileCount: number,\n): string {\n const gaugeColor = score >= 80 ? '#22c55e' : score >= 50 ? '#f59e0b' : '#ef4444'\n const grade = score >= 80 ? 'Good' : score >= 50 ? 'Fair' : 'Poor'\n\n // SVG donut gauge — full circle, filled proportionally\n const r = 54\n const circ = 2 * Math.PI * r\n const filled = (score / 100) * circ\n\n function section(emoji: string, title: string, count: number, rows: [string, string][]): string {\n const color = count === 0 ? '#22c55e' : '#f59e0b'\n const badge = `<span style=\"background:${color};color:#fff;border-radius:9999px;padding:1px 8px;font-size:0.75rem;margin-left:6px;\">${count}</span>`\n const body =\n rows.length === 0\n ? `<p style=\"color:#6b7280;margin:0\">None found ✓</p>`\n : `<table style=\"width:100%;border-collapse:collapse;font-size:0.85rem\">\n <thead><tr style=\"background:#f3f4f6\">${Object.keys(rows[0]!).map(() => '').join('')}</tr></thead>\n <tbody>\n ${rows.map(([a, b]) => `<tr style=\"border-bottom:1px solid #e5e7eb\"><td style=\"padding:4px 8px\">${esc(a)}</td><td style=\"padding:4px 8px;color:#4b5563\">${esc(b)}</td></tr>`).join('')}\n </tbody>\n </table>`\n return `\n <details style=\"margin-bottom:12px;border:1px solid #e5e7eb;border-radius:8px;overflow:hidden\">\n <summary style=\"cursor:pointer;padding:12px 16px;font-weight:600;background:#f9fafb;user-select:none\">\n ${emoji} ${esc(title)}${badge}\n </summary>\n <div style=\"padding:12px 16px\">${body}</div>\n </details>`\n }\n\n const deadRows: [string, string][] = [\n ...dead.deadFiles.map<[string, string]>((f) => [f, 'Dead file']),\n ...dead.deadExports.map<[string, string]>((e) => [`${e.filePath}:${e.line}`, `export ${e.exportName}`]),\n ]\n\n const circularRows: [string, string][] = circular.cycles.map<[string, string]>((c, i) => [\n `Cycle ${i + 1}`,\n [...c, c[0]].join(' → '),\n ])\n\n const depsRows: [string, string][] = deps.unusedPackages.map<[string, string]>((p) => [p, 'declared but never imported'])\n\n const dupeRows: [string, string][] = [\n ...dupes.clusters.map<[string, string]>((c) => [\n c.functions.map((f) => f.name).join(', '),\n c.type === 'name' ? 'similar name' : 'identical body',\n ]),\n ...dupes.constDupes.map<[string, string]>((d) => [`${d.name} = ${d.value}`, `${d.occurrences.length} occurrences`]),\n ]\n\n const qualityRows: [string, string][] = [\n [`Barrel files (≥15 exports)`, String(barrelFileCount)],\n [`Long files (>300 lines)`, String(longFileCount)],\n ]\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\"/>\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>\n<title>prunify — Code Health Report</title>\n<style>\n *{box-sizing:border-box;margin:0;padding:0}\n body{font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;background:#f9fafb;color:#111827;padding:32px}\n h1{font-size:1.5rem;font-weight:700;margin-bottom:4px}\n .subtitle{color:#6b7280;font-size:0.875rem;margin-bottom:32px}\n .gauge-wrap{display:flex;flex-direction:column;align-items:center;margin-bottom:32px}\n .gauge-label{font-size:2.5rem;font-weight:800;margin-top:8px;color:${gaugeColor}}\n .grade{font-size:1rem;color:#6b7280;margin-top:2px}\n details summary::-webkit-details-marker{display:none}\n details summary::marker{display:none}\n</style>\n</head>\n<body>\n<h1>🧹 prunify — Code Health Report</h1>\n<p class=\"subtitle\">Generated ${new Date().toISOString()}</p>\n\n<div class=\"gauge-wrap\">\n <svg width=\"140\" height=\"140\" viewBox=\"0 0 140 140\">\n <circle cx=\"70\" cy=\"70\" r=\"${r}\" fill=\"none\" stroke=\"#e5e7eb\" stroke-width=\"16\"/>\n <circle cx=\"70\" cy=\"70\" r=\"${r}\" fill=\"none\" stroke=\"${gaugeColor}\" stroke-width=\"16\"\n stroke-dasharray=\"${filled.toFixed(2)} ${circ.toFixed(2)}\"\n stroke-linecap=\"round\"\n transform=\"rotate(-90 70 70)\"/>\n <text x=\"70\" y=\"75\" text-anchor=\"middle\" font-size=\"24\" font-weight=\"700\" fill=\"${gaugeColor}\">${score}</text>\n </svg>\n <div class=\"gauge-label\">${score}/100</div>\n <div class=\"grade\">${grade}</div>\n</div>\n\n${section('🗑️', 'Dead Code', deadRows.length, deadRows)}\n${section('♻️', 'Circular Imports', circular.cycles.length, circularRows)}\n${section('📦', 'Unused Dependencies', deps.unusedPackages.length, depsRows)}\n${section('🔁', 'Duplicate Code', dupes.clusters.length + dupes.constDupes.length, dupeRows)}\n${section('📐', 'Code Quality', barrelFileCount + longFileCount, qualityRows)}\n\n<p style=\"margin-top:24px;font-size:0.75rem;color:#9ca3af\">Score formula: 100 − 2×dead_files (max 20) − 3×dupe_clusters (max 15) − 5×circular (max 20) − 2×unused_pkgs (max 10) − barrel_files (max 10) − long_files (max 10)</p>\n</body>\n</html>`\n}\n\nfunction esc(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n}\n","import ora from 'ora'\nimport chalk from 'chalk'\nimport Table from 'cli-table3'\nimport { discoverFiles, buildProject, getImportsForFile } from '@/core/parser.js'\nimport { buildGraph, detectCycles, type ImportGraph } from '@/core/graph.js'\nimport { writeMarkdown } from '@/core/reporter.js'\n\n// ─── Module API ───────────────────────────────────────────────────────────────\n\nexport interface CircularResult {\n cycles: string[][]\n report: string\n}\n\n/**\n * Synchronous, graph-only entry point used by the health-report orchestrator.\n * The import graph must already be built by the caller.\n */\nexport function runCircularModule(graph: ImportGraph): CircularResult {\n const cycles = detectCycles(graph)\n return { cycles, report: buildCircularReport(cycles) }\n}\n\nfunction buildCircularReport(cycles: string[][]): string {\n const lines: string[] = [\n '========================================',\n ' CIRCULAR IMPORTS',\n ` Cycles: ${cycles.length}`,\n '========================================',\n '',\n ]\n\n if (cycles.length === 0) {\n lines.push(' No circular imports detected.', '')\n return lines.join('\\n')\n }\n\n lines.push('── CYCLES ──', '')\n for (let i = 0; i < cycles.length; i++) {\n const cycle = cycles[i]!\n lines.push(\n `Cycle ${i + 1}:`,\n ` ${[...cycle, cycle[0]].join(' → ')}`,\n ` Files involved: ${cycle.length}`,\n '',\n )\n }\n\n return lines.join('\\n')\n}\n\nexport interface CircularOptions {\n output?: string\n}\n\n/**\n * Detects circular import chains using DFS with a visited stack.\n */\nexport async function runCircular(dir: string, opts: CircularOptions): Promise<string[][]> {\n const spinner = ora(chalk.cyan('Scanning for circular imports…')).start()\n\n try {\n const fileList = discoverFiles(dir, [])\n const project = buildProject(fileList)\n const graph = buildGraph(fileList, (f) => {\n const sf = project.getSourceFile(f)\n return sf ? getImportsForFile(sf) : []\n })\n const cycles = detectCycles(graph)\n\n spinner.succeed(\n chalk.green(`Circular import scan complete — ${cycles.length} cycle(s) found`),\n )\n\n if (cycles.length === 0) {\n console.log(chalk.green(' No circular imports detected.'))\n return cycles\n }\n\n const table = new Table({ head: ['Cycle #', 'Files involved'] })\n cycles.forEach((cycle, i) => {\n table.push([chalk.yellow(String(i + 1)), cycle.map((f) => chalk.gray(f)).join('\\n → ')])\n })\n console.log(table.toString())\n\n if (opts.output) {\n writeMarkdown(\n {\n title: 'Circular Imports Report',\n summary: `${cycles.length} circular import chain(s) found`,\n sections: [\n {\n title: 'Circular Chains',\n headers: ['Cycle #', 'Files'],\n rows: cycles.map((cycle, i) => [String(i + 1), cycle.join(' → ')]),\n },\n ],\n generatedAt: new Date(),\n },\n opts.output,\n )\n console.log(chalk.cyan(` Report written to ${opts.output}`))\n }\n\n return cycles\n } catch (err) {\n spinner.fail(chalk.red('Circular import scan failed'))\n throw err\n }\n}\n","import fs from 'node:fs'\nimport path from 'node:path'\nimport ora from 'ora'\nimport chalk from 'chalk'\nimport Table from 'cli-table3'\nimport { glob } from '@/utils/file.js'\nimport { writeMarkdown } from '@/core/reporter.js'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface UnusedAsset {\n filePath: string\n relativePath: string\n sizeBytes: number\n}\n\nexport interface AssetCheckResult {\n unusedAssets: UnusedAsset[]\n totalAssets: number\n report: string\n}\n\nexport interface AssetCheckOptions {\n output?: string\n}\n\n// ─── Constants ────────────────────────────────────────────────────────────────\n\nconst ASSET_EXTENSIONS = new Set([\n '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif', '.ico', '.bmp',\n '.woff', '.woff2', '.ttf', '.eot', '.otf',\n '.mp4', '.webm', '.ogg', '.mp3', '.wav',\n '.pdf',\n])\n\nconst SOURCE_PATTERNS = [\n '**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx',\n '**/*.css', '**/*.scss', '**/*.sass', '**/*.less',\n '**/*.html', '**/*.json',\n]\n\nconst SOURCE_IGNORE = [\n 'node_modules', 'node_modules/**',\n 'dist', 'dist/**',\n '.next', '.next/**',\n 'coverage', 'coverage/**',\n 'public', 'public/**',\n]\n\n// ─── Public module API ────────────────────────────────────────────────────────\n\n/**\n * Scans `public/` for assets and checks whether each filename appears in\n * any source file (TS/JS/CSS/HTML/JSON). Checks both the bare filename and\n * the public-root-relative path (e.g. `/logo.png`).\n */\nexport function runAssetCheckModule(rootDir: string): AssetCheckResult {\n const publicDir = path.join(rootDir, 'public')\n\n if (!fs.existsSync(publicDir)) {\n return { unusedAssets: [], totalAssets: 0, report: buildAssetReport([], 0, rootDir) }\n }\n\n const assets = collectAssets(publicDir)\n if (assets.length === 0) {\n return { unusedAssets: [], totalAssets: 0, report: buildAssetReport([], 0, rootDir) }\n }\n\n // Read all source content once\n const sourceFiles = glob(rootDir, SOURCE_PATTERNS, SOURCE_IGNORE)\n const sourceContent = sourceFiles.reduce((acc, f) => {\n try { return acc + fs.readFileSync(f, 'utf-8') + '\\n' } catch { return acc }\n }, '')\n\n const unused: UnusedAsset[] = []\n\n for (const assetAbs of assets) {\n const fileName = path.basename(assetAbs)\n // Public-root-relative path e.g. \"/images/logo.png\"\n const relFromPublic = '/' + path.relative(publicDir, assetAbs).replaceAll('\\\\', '/')\n\n const referenced =\n sourceContent.includes(fileName) ||\n sourceContent.includes(relFromPublic)\n\n if (!referenced) {\n unused.push({\n filePath: assetAbs,\n relativePath: path.relative(rootDir, assetAbs).replaceAll('\\\\', '/'),\n sizeBytes: getFileSize(assetAbs),\n })\n }\n }\n\n const report = buildAssetReport(unused, assets.length, rootDir)\n return { unusedAssets: unused, totalAssets: assets.length, report }\n}\n\n// ─── CLI command ──────────────────────────────────────────────────────────────\n\nexport async function runAssetCheck(rootDir: string, opts: AssetCheckOptions): Promise<UnusedAsset[]> {\n const publicDir = path.join(rootDir, 'public')\n\n if (!fs.existsSync(publicDir)) {\n console.log(chalk.dim(' No public/ folder found — skipping asset check'))\n return []\n }\n\n const spinner = ora(chalk.cyan('Scanning public/ for unused assets…')).start()\n\n try {\n const result = runAssetCheckModule(rootDir)\n\n spinner.succeed(\n chalk.green(\n `Asset scan complete — ${result.unusedAssets.length} unused / ${result.totalAssets} total`,\n ),\n )\n\n if (result.unusedAssets.length === 0) {\n console.log(chalk.green(' All public assets are referenced in source.'))\n return []\n }\n\n const table = new Table({ head: ['Asset', 'Size'] })\n for (const asset of result.unusedAssets) {\n const kb = (asset.sizeBytes / 1024).toFixed(1)\n table.push([chalk.gray(asset.relativePath), `${kb} KB`])\n }\n console.log(table.toString())\n\n if (opts.output) {\n writeMarkdown(\n {\n title: 'Unused Assets Report',\n summary: `${result.unusedAssets.length} unused asset(s) found in public/`,\n sections: [\n {\n title: 'Unused Assets',\n headers: ['Asset', 'Size (KB)'],\n rows: result.unusedAssets.map((a) => [\n a.relativePath,\n (a.sizeBytes / 1024).toFixed(1),\n ]),\n },\n ],\n generatedAt: new Date(),\n },\n opts.output,\n )\n console.log(chalk.cyan(` Report written to ${opts.output}`))\n }\n\n return result.unusedAssets\n } catch (err) {\n spinner.fail(chalk.red('Asset scan failed'))\n throw err\n }\n}\n\n// ─── Internal helpers ─────────────────────────────────────────────────────────\n\nfunction collectAssets(dir: string): string[] {\n const results: string[] = []\n\n function walk(current: string): void {\n let entries: fs.Dirent[]\n try {\n entries = fs.readdirSync(current, { withFileTypes: true })\n } catch {\n return\n }\n for (const entry of entries) {\n const full = path.join(current, entry.name)\n if (entry.isDirectory()) {\n walk(full)\n } else if (entry.isFile() && ASSET_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {\n results.push(full)\n }\n }\n }\n\n walk(dir)\n return results\n}\n\nfunction getFileSize(filePath: string): number {\n try { return fs.statSync(filePath).size } catch { return 0 }\n}\n\nfunction buildAssetReport(unused: UnusedAsset[], totalAssets: number, rootDir: string): string {\n const totalBytes = unused.reduce((s, a) => s + a.sizeBytes, 0)\n const totalKb = (totalBytes / 1024).toFixed(1)\n\n const lines: string[] = [\n '========================================',\n ' UNUSED ASSETS REPORT',\n ` Total assets : ${totalAssets}`,\n ` Unused assets : ${unused.length}`,\n ` Recoverable : ~${totalKb} KB`,\n '========================================',\n '',\n ]\n\n if (unused.length === 0) {\n lines.push(' All public assets are referenced in source.', '')\n return lines.join('\\n')\n }\n\n lines.push('── UNUSED ASSETS ──', '')\n for (const asset of unused) {\n lines.push(\n `UNUSED — ${asset.relativePath}`,\n `Size: ~${(asset.sizeBytes / 1024).toFixed(1)} KB`,\n `Action: Safe to delete if not served directly via URL`,\n '',\n )\n }\n\n return lines.join('\\n')\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,uBAAwB;AACxB,IAAAA,gBAAkB;AAClB,IAAAC,qBAAkB;AAClB,IAAAC,kBAAe;AACf,IAAAC,oBAAiB;AACjB,sBAA8B;AAC9B,2BAAqB;;;ACNrB,IAAAC,kBAAe;AACf,IAAAC,oBAAiB;AACjB,sBAAsD;;;ACFtD,qBAAe;AACf,uBAAiB;AACjB,uBAA0B;AAMnB,SAAS,KACd,KACA,UACA,SAAmB,CAAC,GACV;AACV,QAAM,UAAoB,CAAC;AAC3B,UAAQ,KAAK,KAAK,UAAU,QAAQ,OAAO;AAC3C,SAAO;AACT;AAEA,SAAS,QACP,MACA,SACA,UACA,QACA,SACM;AACN,MAAI;AAEJ,MAAI;AACF,cAAU,eAAAC,QAAG,YAAY,SAAS,EAAE,eAAe,KAAK,CAAC;AAAA,EAC3D,QAAQ;AACN;AAAA,EACF;AAEA,aAAW,SAAS,SAAS;AAC3B,UAAM,WAAW,iBAAAC,QAAK,KAAK,SAAS,MAAM,IAAI;AAC9C,UAAM,eAAe,iBAAAA,QAAK,SAAS,MAAM,QAAQ,EAAE,QAAQ,OAAO,GAAG;AAErE,QAAI,MAAM,YAAY,GAAG;AAGvB,YAAM,YAAY,OAAO,KAAK,CAAC,gBAAY,4BAAU,cAAc,OAAO,CAAC;AAC3E,UAAI,CAAC,UAAW,SAAQ,MAAM,UAAU,UAAU,QAAQ,OAAO;AAAA,IACnE,WAAW,MAAM,OAAO,GAAG;AACzB,YAAM,YAAY,OAAO,KAAK,CAAC,gBAAY,4BAAU,cAAc,OAAO,CAAC;AAC3E,UAAI,CAAC,WAAW;AACd,cAAM,UAAU,SAAS,KAAK,CAAC,gBAAY,4BAAU,cAAc,OAAO,CAAC;AAC3E,YAAI,QAAS,SAAQ,KAAK,QAAQ;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AACF;;;AD3BA,IAAM,iBAAiB;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,kBAAkB,CAAC,WAAW,YAAY,WAAW,UAAU;AAErE,IAAM,qBAAqB;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,cAAc,CAAC,YAAY,aAAa,YAAY,WAAW;AAY9D,SAAS,cAAc,SAAiB,SAAmB,CAAC,GAAa;AAC9E,SAAO,KAAK,SAAS,iBAAiB,CAAC,GAAG,gBAAgB,GAAG,MAAM,CAAC;AACtE;AASO,SAAS,aAAa,OAAiB,cAAgC;AAC5E,QAAM,WAAW,iBAAiB,MAAM,SAAS,IAAI,aAAa,MAAM,CAAC,CAAC,IAAI;AAE9E,QAAM,UAAU,WACZ,IAAI,wBAAQ,EAAE,kBAAkB,UAAU,6BAA6B,KAAK,CAAC,IAC7E,IAAI,wBAAQ;AAAA,IACV,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,mBAAmB;AAAA,IACrB;AAAA,EACF,CAAC;AAEL,UAAQ,sBAAsB,KAAK;AACnC,SAAO;AACT;AAgBO,SAAS,kBAAkB,YAAkC;AAClE,QAAM,SAAS,oBAAI,IAAY;AAC/B,QAAM,UAAU,kBAAAC,QAAK,QAAQ,WAAW,YAAY,CAAC;AACrD,QAAM,UAAU,WAAW,WAAW;AACtC,QAAM,kBAAkB,QAAQ,mBAAmB;AACnD,QAAM,cAAe,gBAAgB,SAAS,CAAC;AAC/C,QAAM,UAAU,gBAAgB;AAEhC,WAAS,YAAY,IAAkC;AACrD,QAAI,CAAC,GAAI;AACT,UAAM,IAAI,kBAAAA,QAAK,UAAU,GAAG,YAAY,CAAC;AACzC,QACE,CAAC,EAAE,SAAS,GAAG,kBAAAA,QAAK,GAAG,eAAe,kBAAAA,QAAK,GAAG,EAAE,KAChD,CAAC,EAAE,SAAS,gBAAgB,KAC5B,kBAAAA,QAAK,UAAU,WAAW,YAAY,CAAC,MAAM,GAC7C;AACA,aAAO,IAAI,CAAC;AAAA,IACd;AAAA,EACF;AAEA,WAAS,cAAc,WAAyB;AAC9C,QAAI,CAAC,UAAW;AAChB,UAAM,aAAa,UAAU,WAAW,IAAI,KAAK,UAAU,WAAW,KAAK;AAE3E,QAAI,YAAY;AACd,YAAM,IAAI,oBAAoB,SAAS,WAAW,OAAO;AACzD,UAAI,EAAG,QAAO,IAAI,CAAC;AAAA,IACrB,WAAW,CAAC,UAAU,WAAW,OAAO,GAAG;AACzC,YAAM,IAAI,iBAAiB,WAAW,aAAa,SAAS,OAAO;AACnE,UAAI,EAAG,QAAO,IAAI,CAAC;AAAA,IACrB;AAAA,EACF;AAGA,aAAW,QAAQ,WAAW,sBAAsB,GAAG;AACrD,UAAM,KAAK,KAAK,6BAA6B;AAC7C,SAAK,YAAY,EAAE,IAAI,cAAc,KAAK,wBAAwB,CAAC;AAAA,EACrE;AAGA,aAAW,QAAQ,WAAW,sBAAsB,GAAG;AACrD,UAAM,YAAY,KAAK,wBAAwB;AAC/C,QAAI,CAAC,UAAW;AAChB,UAAM,KAAK,KAAK,6BAA6B;AAC7C,SAAK,YAAY,EAAE,IAAI,cAAc,SAAS;AAAA,EAChD;AAGA,aAAW,QAAQ,WAAW,qBAAqB,2BAAW,cAAc,GAAG;AAC7E,QAAI,KAAK,cAAc,EAAE,QAAQ,MAAM,2BAAW,cAAe;AACjE,UAAM,OAAO,KAAK,aAAa;AAC/B,QAAI,KAAK,SAAS,KAAK,qBAAK,gBAAgB,KAAK,CAAC,CAAC,GAAG;AACpD,oBAAc,KAAK,CAAC,EAAE,gBAAgB,CAAC;AAAA,IACzC;AAAA,EACF;AAEA,SAAO,CAAC,GAAG,MAAM;AACnB;AA2EA,SAAS,oBACP,SACA,WACA,SACe;AACf,QAAM,OAAO,kBAAAC,QAAK,QAAQ,SAAS,SAAS;AAE5C,aAAW,OAAO,oBAAoB;AACpC,UAAM,KAAK,QAAQ,cAAc,OAAO,GAAG;AAC3C,QAAI,GAAI,QAAO,kBAAAA,QAAK,UAAU,GAAG,YAAY,CAAC;AAAA,EAChD;AAEA,aAAW,SAAS,aAAa;AAC/B,UAAM,KAAK,QAAQ,cAAc,kBAAAA,QAAK,KAAK,MAAM,KAAK,CAAC;AACvD,QAAI,GAAI,QAAO,kBAAAA,QAAK,UAAU,GAAG,YAAY,CAAC;AAAA,EAChD;AAEA,SAAO;AACT;AAMA,SAAS,iBACP,WACA,aACA,SACA,SACe;AACf,aAAW,CAAC,OAAO,OAAO,KAAK,OAAO,QAAQ,WAAW,GAAG;AAC1D,UAAM,QAAQ,WAAW,OAAO,SAAS;AACzC,QAAI,CAAC,MAAO;AAEZ,UAAM,UAAU,MAAM,CAAC,KAAK;AAE5B,eAAW,UAAU,SAAS;AAC5B,YAAM,WAAW,OAAO,WAAW,KAAK,OAAO;AAC/C,YAAM,WAAW,UAAU,kBAAAA,QAAK,QAAQ,SAAS,QAAQ,IAAI,kBAAAA,QAAK,QAAQ,QAAQ;AAClF,YAAM,MAAM,mBAAmB,UAAU,OAAO;AAChD,UAAI,IAAK,QAAO;AAAA,IAClB;AAAA,EACF;AAEA,SAAO;AACT;AAGA,SAAS,WAAW,OAAe,WAA2C;AAE5E,QAAM,UAAU,MAAM,QAAQ,qBAAqB,MAAM;AACzD,QAAM,UAAU,QAAQ,WAAW,KAAK,MAAM;AAC9C,SAAO,IAAI,OAAO,IAAI,OAAO,GAAG,EAAE,KAAK,SAAS;AAClD;AAGA,SAAS,mBAAmB,UAAkB,SAAiC;AAC7E,aAAW,OAAO,oBAAoB;AACpC,UAAM,KAAK,QAAQ,cAAc,WAAW,GAAG;AAC/C,QAAI,GAAI,QAAO,kBAAAA,QAAK,UAAU,GAAG,YAAY,CAAC;AAAA,EAChD;AACA,aAAW,SAAS,aAAa;AAC/B,UAAM,KAAK,QAAQ,cAAc,kBAAAA,QAAK,KAAK,UAAU,KAAK,CAAC;AAC3D,QAAI,GAAI,QAAO,kBAAAA,QAAK,UAAU,GAAG,YAAY,CAAC;AAAA,EAChD;AACA,SAAO;AACT;AAMA,SAAS,aAAa,UAAsC;AAC1D,MAAI,MAAM,kBAAAA,QAAK,QAAQ,QAAQ;AAC/B,QAAM,OAAO,kBAAAA,QAAK,MAAM,GAAG,EAAE;AAE7B,SAAO,QAAQ,MAAM;AACnB,UAAM,YAAY,kBAAAA,QAAK,KAAK,KAAK,eAAe;AAChD,QAAI,gBAAAC,QAAG,WAAW,SAAS,EAAG,QAAO;AACrC,UAAM,SAAS,kBAAAD,QAAK,QAAQ,GAAG;AAC/B,QAAI,WAAW,IAAK;AACpB,UAAM;AAAA,EACR;AAEA,SAAO;AACT;;;AErUC,IAAAE,kBAAe;AAChB,IAAAC,oBAAiB;AAuBV,SAAS,WACd,OACA,YACa;AACb,QAAM,QAAqB,oBAAI,IAAI;AAEnC,aAAW,QAAQ,OAAO;AACxB,UAAM,IAAI,MAAM,oBAAI,IAAI,CAAC;AAAA,EAC3B;AAEA,aAAW,QAAQ,OAAO;AACxB,eAAW,YAAY,WAAW,IAAI,GAAG;AACvC,YAAM,IAAI,IAAI,GAAG,IAAI,QAAQ;AAE7B,UAAI,CAAC,MAAM,IAAI,QAAQ,EAAG,OAAM,IAAI,UAAU,oBAAI,IAAI,CAAC;AAAA,IACzD;AAAA,EACF;AAEA,SAAO;AACT;AAWO,SAAS,gBAAgB,SAAiB,aAA4B;AAC3E,QAAM,UAAU;AAAA,IACd,GAAG,qBAAqB,OAAO;AAAA,IAC/B,GAAG,uBAAuB,SAAS,WAAW;AAAA,IAC9C,GAAG,uBAAuB,OAAO;AAAA,EACnC;AAEA,SAAO,CAAC,GAAG,IAAI,IAAI,OAAO,CAAC;AAC7B;AASO,SAAS,cAAc,OAA8B;AAC1D,QAAM,WAAW,oBAAI,IAAY;AACjC,aAAW,QAAQ,MAAM,OAAO,GAAG;AACjC,eAAW,OAAO,KAAM,UAAS,IAAI,GAAG;AAAA,EAC1C;AACA,QAAM,QAAQ,CAAC,GAAG,MAAM,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,CAAC;AAC9D,SAAO,MAAM,SAAS,IAAI,QAAQ,CAAC,GAAG,MAAM,KAAK,CAAC;AACpD;AAMO,SAAS,OAAO,OAAoB,aAAoC;AAC7E,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,QAAQ,CAAC,GAAG,WAAW;AAC7B,MAAI;AAEJ,UAAQ,OAAO,MAAM,IAAI,OAAO,QAAW;AACzC,QAAI,QAAQ,IAAI,IAAI,EAAG;AACvB,YAAQ,IAAI,IAAI;AAChB,eAAW,YAAY,MAAM,IAAI,IAAI,KAAK,CAAC,GAAG;AAC5C,UAAI,CAAC,QAAQ,IAAI,QAAQ,EAAG,OAAM,KAAK,QAAQ;AAAA,IACjD;AAAA,EACF;AAEA,SAAO;AACT;AASO,SAAS,eACd,OACA,WACuB;AACvB,QAAM,eAAe,kBAAkB,KAAK;AAC5C,QAAM,SAAS,oBAAI,IAAsB;AAEzC,aAAW,YAAY,WAAW;AAChC,WAAO,IAAI,UAAU,aAAa,UAAU,OAAO,WAAW,YAAY,CAAC;AAAA,EAC7E;AAEA,SAAO;AACT;AAOO,SAAS,aAAa,OAAgC;AAC3D,QAAM,SAAqB,CAAC;AAC5B,QAAM,WAAW,oBAAI,IAAY;AACjC,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAMC,SAAiB,CAAC;AAExB,QAAM,MAAwB,EAAE,UAAU,OAAO;AAEjD,aAAW,SAAS,MAAM,KAAK,GAAG;AAChC,QAAI,CAAC,QAAQ,IAAI,KAAK,GAAG;AACvB,mBAAa,OAAO,OAAO,SAAS,SAASA,QAAM,GAAG;AAAA,IACxD;AAAA,EACF;AAEA,SAAO;AACT;AAgBA,SAAS,qBAAqB,SAA2B;AACvD,QAAM,SACJ,gBAAAC,QAAG,WAAW,kBAAAC,QAAK,KAAK,SAAS,gBAAgB,CAAC,KAClD,gBAAAD,QAAG,WAAW,kBAAAC,QAAK,KAAK,SAAS,gBAAgB,CAAC,KAClD,gBAAAD,QAAG,WAAW,kBAAAC,QAAK,KAAK,SAAS,iBAAiB,CAAC;AAErD,MAAI,CAAC,OAAQ,QAAO,CAAC;AAErB,QAAM,UAAoB,CAAC;AAC3B,aAAW,OAAO,CAAC,SAAS,KAAK,GAAY;AAC3C,UAAM,UAAU,kBAAAA,QAAK,KAAK,SAAS,GAAG;AACtC,QAAI,gBAAAD,QAAG,WAAW,OAAO,EAAG,SAAQ,KAAK,GAAG,mBAAmB,OAAO,CAAC;AAAA,EACzE;AACA,SAAO;AACT;AAGA,SAAS,uBAAuB,SAAiB,aAA4B;AAC3E,QAAM,UAAoB,CAAC;AAC3B,aAAW,SAAS,CAAC,QAAQ,QAAQ,GAAY;AAC/C,UAAM,QAAQ,cAAc,KAAK;AACjC,QAAI,OAAO,UAAU,SAAU;AAC/B,UAAM,MAAM,kBAAAC,QAAK,QAAQ,SAAS,KAAK;AACvC,QAAI,gBAAAD,QAAG,WAAW,GAAG,EAAG,SAAQ,KAAK,GAAG;AAAA,EAC1C;AACA,SAAO;AACT;AAEA,SAAS,uBAAuB,SAA2B;AACzD,QAAM,aAAa;AAAA,IACjB;AAAA,IAAe;AAAA,IAAgB;AAAA,IAAe;AAAA,IAC9C;AAAA,IAAgB;AAAA,IAAiB;AAAA,IAAgB;AAAA,IACjD;AAAA,IAAc;AAAA,IAAe;AAAA,IAAc;AAAA,IAC3C;AAAA,IAAY;AAAA,IAAa;AAAA,IAAY;AAAA,EACvC;AACA,SAAO,WACJ,IAAI,CAAC,QAAQ,kBAAAC,QAAK,KAAK,SAAS,GAAG,CAAC,EACpC,OAAO,CAAC,QAAQ,gBAAAD,QAAG,WAAW,GAAG,CAAC;AACvC;AAOA,SAAS,QAAQ,MAAc,OAAgC;AAC7D,SAAO,EAAE,MAAM,YAAY,MAAM,IAAI,IAAI,KAAK,oBAAI,IAAI,GAAG,OAAO,GAAG,SAAS,MAAM;AACpF;AAEA,SAAS,aACP,OACA,OACA,SACA,SACAC,QACA,KACM;AACN,QAAM,QAAsB,CAAC,QAAQ,OAAO,KAAK,CAAC;AAElD,SAAO,MAAM,SAAS,GAAG;AACvB,UAAM,QAAQ,MAAM,GAAG,EAAE;AACzB,QAAI,CAAC,MAAO;AAEZ,QAAI,CAAC,MAAM,SAAS;AAClB,UAAI,QAAQ,IAAI,MAAM,IAAI,GAAG;AAAE,cAAM,IAAI;AAAG;AAAA,MAAS;AACrD,YAAM,UAAU;AAChB,cAAQ,IAAI,MAAM,IAAI;AACtB,MAAAA,OAAK,KAAK,MAAM,IAAI;AAAA,IACtB;AAEA,UAAM,EAAE,MAAM,OAAO,SAAS,IAAI,MAAM,UAAU,KAAK;AAEvD,QAAI,MAAM;AACR,YAAM,IAAI;AACV,MAAAA,OAAK,IAAI;AACT,cAAQ,OAAO,MAAM,IAAI;AACzB,cAAQ,IAAI,MAAM,IAAI;AAAA,IACxB,OAAO;AACL,0BAAoB,UAAU,OAAOA,QAAM,SAAS,SAAS,KAAK,KAAK;AAAA,IACzE;AAAA,EACF;AACF;AAEA,SAAS,oBACP,UACA,OACAA,QACA,SACA,SACA,KACA,OACM;AACN,MAAI,QAAQ,IAAI,QAAQ,GAAG;AACzB,gBAAY,UAAUA,QAAM,GAAG;AAAA,EACjC,WAAW,CAAC,QAAQ,IAAI,QAAQ,GAAG;AACjC,UAAM,KAAK,QAAQ,UAAU,KAAK,CAAC;AAAA,EACrC;AACF;AAEA,SAAS,YACP,YACAA,QACA,KACM;AACN,QAAM,MAAMA,OAAK,QAAQ,UAAU;AACnC,MAAI,QAAQ,GAAI;AAChB,QAAM,QAAQ,eAAeA,OAAK,MAAM,GAAG,CAAC;AAC5C,QAAM,MAAM,MAAM,KAAK,IAAI;AAC3B,MAAI,CAAC,IAAI,SAAS,IAAI,GAAG,GAAG;AAC1B,QAAI,SAAS,IAAI,GAAG;AACpB,QAAI,OAAO,KAAK,KAAK;AAAA,EACvB;AACF;AAIA,SAAS,aACP,UACA,OACA,WACA,cACU;AACV,QAAM,QAAkB,CAAC;AACzB,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,QAAQ,CAAC,GAAI,MAAM,IAAI,QAAQ,KAAK,CAAC,CAAE;AAC7C,MAAI;AAEJ,UAAQ,OAAO,MAAM,IAAI,OAAO,QAAW;AACzC,QAAI,QAAQ,IAAI,IAAI,KAAK,SAAS,SAAU;AAC5C,YAAQ,IAAI,IAAI;AAEhB,QAAI,UAAU,IAAI,IAAI,KAAK,qBAAqB,MAAM,WAAW,YAAY,GAAG;AAC9E,YAAM,KAAK,IAAI;AACf,iBAAW,QAAQ,MAAM,IAAI,IAAI,KAAK,CAAC,GAAG;AACxC,YAAI,CAAC,QAAQ,IAAI,IAAI,EAAG,OAAM,KAAK,IAAI;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,qBACP,MACA,WACA,cACS;AACT,QAAM,YAAY,aAAa,IAAI,IAAI,KAAK,oBAAI,IAAY;AAC5D,SAAO,UAAU,SAAS,KAAK,CAAC,GAAG,SAAS,EAAE,MAAM,CAAC,QAAQ,UAAU,IAAI,GAAG,CAAC;AACjF;AAKA,SAAS,kBAAkB,OAA8C;AACvE,QAAM,MAAM,oBAAI,IAAyB;AAEzC,aAAW,CAAC,IAAI,KAAK,OAAO;AAC1B,QAAI,CAAC,IAAI,IAAI,IAAI,EAAG,KAAI,IAAI,MAAM,oBAAI,IAAI,CAAC;AAAA,EAC7C;AAEA,aAAW,CAAC,MAAM,OAAO,KAAK,OAAO;AACnC,eAAW,OAAO,SAAS;AACzB,UAAI,CAAC,IAAI,IAAI,GAAG,EAAG,KAAI,IAAI,KAAK,oBAAI,IAAI,CAAC;AACzC,UAAI,IAAI,GAAG,GAAG,IAAI,IAAI;AAAA,IACxB;AAAA,EACF;AAEA,SAAO;AACT;AAMA,SAAS,eAAe,OAA2B;AACjD,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,SAAS,MAAM;AAAA,IACnB,CAAC,MAAM,KAAK,MAAO,MAAM,MAAM,IAAI,IAAI,IAAI;AAAA,IAC3C;AAAA,EACF;AACA,SAAO,CAAC,GAAG,MAAM,MAAM,MAAM,GAAG,GAAG,MAAM,MAAM,GAAG,MAAM,CAAC;AAC3D;AAGA,SAAS,mBAAmB,KAAuB;AACjD,QAAM,UAAoB,CAAC;AAC3B,QAAM,YAAY;AAElB,WAAS,KAAK,SAAuB;AACnC,QAAI;AACJ,QAAI;AACF,gBAAU,gBAAAD,QAAG,YAAY,SAAS,EAAE,eAAe,KAAK,CAAC;AAAA,IAC3D,QAAQ;AACN;AAAA,IACF;AACA,eAAW,SAAS,SAAS;AAC3B,YAAM,OAAO,kBAAAC,QAAK,KAAK,SAAS,MAAM,IAAI;AAC1C,UAAI,MAAM,YAAY,GAAG;AACvB,aAAK,IAAI;AAAA,MACX,WAAW,MAAM,OAAO,KAAK,UAAU,KAAK,MAAM,IAAI,GAAG;AACvD,gBAAQ,KAAK,IAAI;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AAEA,OAAK,GAAG;AACR,SAAO;AACT;;;AC9WC,IAAAC,cAAgB;AACjB,mBAAkB;AAClB,wBAAkB;AAClB,IAAAC,kBAAe;AACf,IAAAC,oBAAiB;AACjB,IAAAC,mBAA8B;;;ACL9B,IAAAC,kBAAe;AACf,IAAAC,oBAAiB;AACjB,iBAA8B;AASvB,SAAS,cAAc,MAAuB;AACnD,aAAO,WAAAC,SAAI,IAAI,EAAE,MAAM;AACzB;AAIA,IAAM,mBAAmB;AAMlB,SAAS,iBAAiB,SAAiB,QAAyB;AACzE,QAAM,OAAO,SAAS,kBAAAC,QAAK,QAAQ,MAAM,IAAI,kBAAAA,QAAK,QAAQ,OAAO;AACjE,QAAM,aAAa,kBAAAA,QAAK,KAAK,MAAM,gBAAgB;AACnD,MAAI,CAAC,gBAAAC,QAAG,WAAW,UAAU,GAAG;AAC9B,oBAAAA,QAAG,UAAU,YAAY,EAAE,WAAW,KAAK,CAAC;AAAA,EAC9C;AACA,SAAO;AACT;AAKO,SAAS,YAAY,YAAoB,UAAkB,SAAuB;AACvF,YAAU,UAAU;AACpB,QAAM,WAAW,kBAAAD,QAAK,KAAK,YAAY,QAAQ;AAC/C,kBAAAC,QAAG,cAAc,UAAU,SAAS,OAAO;AAC3C,UAAQ,IAAI,yBAAoB,QAAQ,EAAE;AAC5C;AAKO,SAAS,kBAAkB,SAAuB;AACvD,QAAM,gBAAgB,kBAAAD,QAAK,KAAK,SAAS,YAAY;AACrD,QAAM,QAAQ,GAAG,gBAAgB;AAEjC,MAAI,gBAAAC,QAAG,WAAW,aAAa,GAAG;AAChC,UAAM,WAAW,gBAAAA,QAAG,aAAa,eAAe,OAAO;AACvD,QAAI,SAAS,MAAM,IAAI,EAAE,KAAK,CAAC,SAAS,KAAK,KAAK,MAAM,KAAK,EAAG;AAChE,oBAAAA,QAAG,eAAe,eAAe;AAAA,EAAK,KAAK;AAAA,GAAM,OAAO;AAAA,EAC1D,OAAO;AACL,oBAAAA,QAAG,cAAc,eAAe,GAAG,KAAK;AAAA,GAAM,OAAO;AAAA,EACvD;AACF;AA8BO,SAAS,cAAc,QAAgB,YAA0B;AACtE,QAAM,QAAkB;AAAA,IACtB,KAAK,OAAO,KAAK;AAAA,IACjB;AAAA,IACA,KAAK,OAAO,OAAO;AAAA,IACnB;AAAA,IACA,eAAe,OAAO,YAAY,YAAY,CAAC;AAAA,IAC/C;AAAA,EACF;AAEA,aAAW,WAAW,OAAO,UAAU;AACrC,UAAM,KAAK,MAAM,QAAQ,KAAK,IAAI,EAAE;AAEpC,QAAI,QAAQ,KAAK,WAAW,GAAG;AAC7B,YAAM,KAAK,oBAAoB,EAAE;AACjC;AAAA,IACF;AAEA,QAAI,QAAQ,WAAW,QAAQ,QAAQ,SAAS,GAAG;AACjD,YAAM,KAAK,KAAK,QAAQ,QAAQ,KAAK,KAAK,CAAC,IAAI;AAC/C,YAAM,KAAK,KAAK,QAAQ,QAAQ,IAAI,MAAM,KAAK,EAAE,KAAK,KAAK,CAAC,IAAI;AAAA,IAClE;AAEA,eAAW,OAAO,QAAQ,MAAM;AAC9B,YAAM,KAAK,KAAK,IAAI,KAAK,KAAK,CAAC,IAAI;AAAA,IACrC;AAEA,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,YAAU,kBAAAC,QAAK,QAAQ,UAAU,CAAC;AAClC,kBAAAC,QAAG,cAAc,YAAY,MAAM,KAAK,IAAI,GAAG,OAAO;AACxD;AAKO,SAAS,UAAU,MAAe,YAA0B;AACjE,YAAU,kBAAAD,QAAK,QAAQ,UAAU,CAAC;AAClC,kBAAAC,QAAG,cAAc,YAAY,KAAK,UAAU,MAAM,MAAM,CAAC,GAAG,OAAO;AACrE;AAEA,SAAS,UAAU,KAAmB;AACpC,MAAI,OAAO,CAAC,gBAAAA,QAAG,WAAW,GAAG,GAAG;AAC9B,oBAAAA,QAAG,UAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,EACvC;AACF;;;ADpFO,SAAS,kBACd,SACA,OACA,aACA,SACgB;AAChB,QAAM,WAAW,CAAC,GAAG,MAAM,KAAK,CAAC;AACjC,QAAM,mBAAmB,YAAY,SAAS,IAAI,cAAc,cAAc,KAAK;AAEnF,QAAM,YAAY,OAAO,OAAO,gBAAgB;AAChD,QAAM,YAAY,SAAS,OAAO,CAAC,MAAM,CAAC,UAAU,IAAI,CAAC,CAAC;AAC1D,QAAM,UAAU,IAAI,IAAI,SAAS;AAEjC,QAAM,SAAS,eAAe,OAAO,OAAO;AAC5C,QAAM,cAAc,gBAAgB,SAAS,SAAS;AACtD,QAAM,SAAS,oBAAoB,WAAW,QAAQ,aAAa,OAAO;AAE1E,SAAO,EAAE,WAAW,WAAW,QAAQ,aAAa,OAAO;AAC7D;AASO,SAAS,gBACd,SACA,WACc;AACd,QAAM,gBAAgB,qBAAqB,SAAS,SAAS;AAC7D,QAAM,OAAqB,CAAC;AAE5B,aAAW,YAAY,WAAW;AAChC,2BAAuB,UAAU,SAAS,eAAe,IAAI;AAAA,EAC/D;AAEA,SAAO;AACT;AAEA,SAAS,uBACP,UACA,SACA,eACA,MACM;AACN,QAAM,KAAK,QAAQ,cAAc,QAAQ;AACzC,MAAI,CAAC,GAAI;AAET,QAAM,YAAY,cAAc,IAAI,QAAQ,KAAK,oBAAI,IAAY;AACjE,MAAI,UAAU,IAAI,GAAG,EAAG;AAExB,aAAW,CAAC,YAAY,YAAY,KAAK,GAAG,wBAAwB,GAAG;AACrE,QAAI,UAAU,IAAI,UAAU,EAAG;AAC/B,UAAM,OAAO,cAAc,aAAa,CAAC,CAAC;AAC1C,SAAK,KAAK,EAAE,UAAU,YAAY,KAAK,CAAC;AAAA,EAC1C;AACF;AAEA,SAAS,cAAc,MAAuD;AAC5E,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,CAAC,sBAAK,OAAO,IAAI,EAAG,QAAO;AAC/B,SAAO,KAAK,mBAAmB;AACjC;AAKO,SAAS,YAAY,UAA0B;AACpD,MAAI;AACF,WAAO,gBAAAC,QAAG,SAAS,QAAQ,EAAE;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMO,SAAS,oBACd,WACA,QACA,aACA,SACQ;AACR,QAAM,MAAM,CAAC,MAAc,kBAAAC,QAAK,SAAS,SAAS,CAAC,EAAE,WAAW,MAAM,GAAG;AAEzE,QAAM,aAAa,UAAU,OAAO,CAAC,KAAK,MAAM;AAC9C,UAAM,QAAQ,OAAO,IAAI,CAAC,KAAK,CAAC;AAChC,WAAO,MAAM,YAAY,CAAC,IAAI,MAAM,OAAO,CAAC,GAAG,MAAM,IAAI,YAAY,CAAC,GAAG,CAAC;AAAA,EAC5E,GAAG,CAAC;AACJ,QAAM,WAAW,aAAa,MAAM,QAAQ,CAAC;AAE7C,QAAM,QAAkB;AAAA,IACtB;AAAA,IACA;AAAA,IACA,kBAAkB,UAAU,MAAM;AAAA,IAClC,mBAAmB,YAAY,MAAM;AAAA,IACrC,oBAAoB,OAAO;AAAA,IAC3B;AAAA,IACA;AAAA,EACF;AAEA,MAAI,UAAU,SAAS,GAAG;AACxB,UAAM,KAAK,wCAAoB,EAAE;AACjC,eAAW,YAAY,WAAW;AAChC,YAAM,QAAQ,OAAO,IAAI,QAAQ,KAAK,CAAC;AACvC,YAAM,WAAW,CAAC,UAAU,GAAG,KAAK;AACpC,YAAM,YAAY,SAAS,OAAO,CAAC,GAAG,MAAM,IAAI,YAAY,CAAC,GAAG,CAAC;AACjE,YAAM,UAAU,YAAY,MAAM,QAAQ,CAAC;AAC3C,YAAM,WACJ,MAAM,SAAS,IACX,CAAC,IAAI,QAAQ,GAAG,GAAG,MAAM,IAAI,GAAG,CAAC,EAAE,KAAK,UAAK,IAC7C,IAAI,QAAQ;AAElB,YAAM,SAAS,SAAS,WAAW,IAAI,KAAK;AAE5C,YAAM;AAAA,QACJ,oBAAe,IAAI,QAAQ,CAAC;AAAA,QAC5B;AAAA,QACA,UAAU,QAAQ;AAAA,QAClB,UAAU,MAAM,wBAAwB,SAAS,MAAM,QAAQ,MAAM;AAAA,QACrE,8BAA8B,SAAS,MAAM,QAAQ,MAAM;AAAA,QAC3D;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI,YAAY,SAAS,GAAG;AAC1B,UAAM,KAAK,0CAAsB,EAAE;AACnC,eAAW,SAAS,aAAa;AAC/B,YAAM;AAAA,QACJ,sBAAiB,IAAI,MAAM,QAAQ,CAAC,WAAM,MAAM,UAAU,YAAY,MAAM,IAAI;AAAA,QAChF;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAUA,eAAsB,YAAY,KAAa,MAAiD;AAC9F,QAAM,cAAU,YAAAC,SAAI,aAAAC,QAAM,KAAK,8BAAyB,CAAC,EAAE,MAAM;AAEjE,MAAI;AACF,UAAM,WAAW,cAAc,KAAK,CAAC,CAAC;AACtC,UAAM,UAAU,aAAa,QAAQ;AACrC,UAAM,QAAQ,WAAW,UAAU,CAAC,MAAM;AACxC,YAAM,KAAK,QAAQ,cAAc,CAAC;AAClC,aAAO,KAAK,kBAAkB,EAAE,IAAI,CAAC;AAAA,IACvC,CAAC;AAED,UAAM,cAAc,gBAAgB,GAAG;AACvC,UAAM,UAAU,gBAAgB,KAAK,WAAW;AAEhD,UAAM,SAAS,kBAAkB,SAAS,OAAO,SAAS,GAAG;AAG7D,UAAM,OAAwB;AAAA,MAC5B,GAAG,OAAO,UAAU,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,gBAAgB,EAAE;AAAA,MACzE,GAAG,OAAO,YAAY,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,YAAY,EAAE,WAAW,EAAE;AAAA,IACnF;AAEA,YAAQ,QAAQ,aAAAA,QAAM,MAAM,kCAA6B,KAAK,MAAM,gBAAgB,CAAC;AAErF,QAAI,KAAK,WAAW,GAAG;AACrB,cAAQ,IAAI,aAAAA,QAAM,MAAM,0BAA0B,CAAC;AACnD,aAAO;AAAA,IACT;AAEA,mBAAe,IAAI;AACnB,oBAAgB,QAAQ,IAAI;AAE5B,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,YAAQ,KAAK,aAAAA,QAAM,IAAI,uBAAuB,CAAC;AAC/C,UAAM;AAAA,EACR;AACF;AASA,SAAS,qBACP,SACA,WAC0B;AAC1B,QAAM,gBAAgB,oBAAI,IAAyB;AAEnD,QAAM,QAAQ,CAAC,MAAc,SAAiB;AAC5C,QAAI,CAAC,cAAc,IAAI,IAAI,EAAG,eAAc,IAAI,MAAM,oBAAI,IAAI,CAAC;AAC/D,kBAAc,IAAI,IAAI,EAAG,IAAI,IAAI;AAAA,EACnC;AAEA,aAAW,YAAY,WAAW;AAChC,UAAM,KAAK,QAAQ,cAAc,QAAQ;AACzC,QAAI,CAAC,GAAI;AACT,mBAAe,IAAI,KAAK;AACxB,qBAAiB,IAAI,KAAK;AAAA,EAC5B;AAEA,SAAO;AACT;AAEA,SAAS,eACP,IACA,OACM;AACN,aAAW,QAAQ,GAAG,sBAAsB,GAAG;AAC7C,UAAM,WAAW,KAAK,6BAA6B;AACnD,QAAI,CAAC,SAAU;AACf,UAAM,SAAS,SAAS,YAAY;AAEpC,QAAI,KAAK,mBAAmB,GAAG;AAC7B,YAAM,QAAQ,GAAG;AACjB;AAAA,IACF;AAEA,UAAM,gBAAgB,KAAK,iBAAiB;AAC5C,QAAI,cAAe,OAAM,QAAQ,SAAS;AAE1C,eAAW,SAAS,KAAK,gBAAgB,GAAG;AAC1C,YAAM,QAAQ,MAAM,aAAa,GAAG,QAAQ,KAAK,MAAM,QAAQ,CAAC;AAAA,IAClE;AAAA,EACF;AACF;AAEA,SAAS,iBACP,IACA,OACM;AACN,aAAW,QAAQ,GAAG,sBAAsB,GAAG;AAC7C,UAAM,WAAW,KAAK,6BAA6B;AACnD,QAAI,CAAC,SAAU;AACf,UAAM,SAAS,SAAS,YAAY;AAEpC,QAAI,KAAK,kBAAkB,GAAG;AAC5B,YAAM,QAAQ,GAAG;AACjB;AAAA,IACF;AAEA,eAAW,SAAS,KAAK,gBAAgB,GAAG;AAC1C,YAAM,QAAQ,MAAM,QAAQ,CAAC;AAAA,IAC/B;AAAA,EACF;AACF;AAEA,SAAS,gBAAgB,KAAsB;AAC7C,QAAM,UAAU,kBAAAF,QAAK,KAAK,KAAK,cAAc;AAC7C,MAAI,CAAC,gBAAAD,QAAG,WAAW,OAAO,EAAG,QAAO;AACpC,SAAO,KAAK,MAAM,gBAAAA,QAAG,aAAa,SAAS,OAAO,CAAC;AACrD;AAEA,SAAS,eAAe,MAAyD;AAC/E,QAAM,QAAQ,IAAI,kBAAAI,QAAM,EAAE,MAAM,CAAC,QAAQ,QAAQ,EAAE,CAAC;AACpD,aAAW,OAAO,MAAM;AACtB,UAAM,KAAK,CAAC,IAAI,MAAM,IAAI,UAAU,CAAC;AAAA,EACvC;AACA,UAAQ,IAAI,MAAM,SAAS,CAAC;AAC9B;AAEA,SAAS,gBAAgB,QAAwB,MAA6B;AAC5E,MAAI,CAAC,KAAK,OAAQ;AAElB,MAAI,KAAK,MAAM;AACb;AAAA,MACE,EAAE,WAAW,OAAO,WAAW,aAAa,OAAO,YAAY;AAAA,MAC/D,KAAK;AAAA,IACP;AACA,YAAQ,IAAI,aAAAD,QAAM,KAAK,qBAAqB,KAAK,MAAM,EAAE,CAAC;AAC1D;AAAA,EACF;AAEA;AAAA,IACE;AAAA,MACE,OAAO;AAAA,MACP,SAAS,GAAG,OAAO,UAAU,MAAM,kBAAkB,OAAO,YAAY,MAAM;AAAA,MAC9E,UAAU;AAAA,QACR;AAAA,UACE,OAAO;AAAA,UACP,SAAS,CAAC,QAAQ,OAAO;AAAA,UACzB,MAAM,OAAO,UAAU,IAAI,CAAC,MAAM;AAAA,YAChC;AAAA,aACC,OAAO,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,UAAK,KAAK;AAAA,UAC9C,CAAC;AAAA,QACH;AAAA,QACA;AAAA,UACE,OAAO;AAAA,UACP,SAAS,CAAC,QAAQ,UAAU,MAAM;AAAA,UAClC,MAAM,OAAO,YAAY,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,YAAY,OAAO,EAAE,IAAI,CAAC,CAAC;AAAA,QAChF;AAAA,MACF;AAAA,MACA,aAAa,oBAAI,KAAK;AAAA,IACxB;AAAA,IACA,KAAK;AAAA,EACP;AACA,UAAQ,IAAI,aAAAA,QAAM,KAAK,uBAAuB,KAAK,MAAM,EAAE,CAAC;AAC9D;;;AEzWA,IAAAE,cAAgB;AAChB,IAAAC,gBAAkB;AAClB,IAAAC,qBAAkB;AAClB,IAAAC,kBAAe;AACf,IAAAC,mBAA6D;;;ACJ7D,yBAAmB;AACnB,IAAAC,mBAUO;;;ADaP,eAAsB,cAAc,KAAa,MAAoD;AACnG,QAAM,WAAW,SAAS,KAAK,YAAY,KAAK,EAAE;AAClD,QAAM,cAAU,YAAAC,SAAI,cAAAC,QAAM,KAAK,wCAAmC,QAAQ,eAAU,CAAC,EAAE,MAAM;AAE7F,MAAI;AACF,UAAM,QAAQ,KAAK,KAAK,CAAC,WAAW,YAAY,WAAW,UAAU,GAAG,CAAC,gBAAgB,MAAM,CAAC;AAChG,UAAM,WAAW,oBAAI,IAAwD;AAE7E,eAAW,YAAY,OAAO;AAC5B,YAAM,UAAU,gBAAAC,QAAG,aAAa,UAAU,OAAO;AACjD,YAAM,QAAQ,QAAQ,MAAM,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO;AAErE,eAAS,IAAI,GAAG,KAAK,MAAM,SAAS,UAAU,KAAK;AACjD,cAAM,QAAQ,MAAM,MAAM,GAAG,IAAI,QAAQ,EAAE,KAAK,IAAI;AACpD,YAAI,CAAC,SAAS,IAAI,KAAK,EAAG,UAAS,IAAI,OAAO,CAAC,CAAC;AAChD,iBAAS,IAAI,KAAK,EAAG,KAAK,EAAE,MAAM,UAAU,WAAW,IAAI,EAAE,CAAC;AAAA,MAChE;AAAA,IACF;AAEA,UAAM,QAA0B,CAAC;AAEjC,eAAW,CAAC,OAAO,WAAW,KAAK,UAAU;AAC3C,UAAI,YAAY,SAAS,GAAG;AAC1B,cAAM,KAAK;AAAA,UACT,MAAM,WAAW,KAAK;AAAA,UACtB,OAAO;AAAA,UACP;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAEA,YAAQ,QAAQ,cAAAD,QAAM,MAAM,kCAA6B,MAAM,MAAM,2BAA2B,CAAC;AAEjG,QAAI,MAAM,WAAW,GAAG;AACtB,cAAQ,IAAI,cAAAA,QAAM,MAAM,iCAAiC,CAAC;AAC1D,aAAO;AAAA,IACT;AAEA,UAAM,QAAQ,IAAI,mBAAAE,QAAM,EAAE,MAAM,CAAC,QAAQ,SAAS,SAAS,kBAAkB,EAAE,CAAC;AAChF,eAAW,KAAK,OAAO;AACrB,YAAM,KAAK;AAAA,QACT,cAAAF,QAAM,KAAK,EAAE,KAAK,MAAM,GAAG,CAAC,CAAC;AAAA,QAC7B,OAAO,EAAE,KAAK;AAAA,QACd,cAAAA,QAAM,OAAO,OAAO,EAAE,YAAY,MAAM,CAAC;AAAA,QACzC,GAAG,EAAE,YAAY,CAAC,EAAE,IAAI,IAAI,EAAE,YAAY,CAAC,EAAE,SAAS;AAAA,MACxD,CAAC;AAAA,IACH;AACA,YAAQ,IAAI,MAAM,SAAS,CAAC;AAE5B,QAAI,KAAK,QAAQ;AACf,YAAM,OAAO,MAAM;AAAA,QAAQ,CAAC,MAC1B,EAAE,YAAY,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,MAAM,GAAG,CAAC,GAAG,OAAO,EAAE,KAAK,GAAG,EAAE,MAAM,OAAO,EAAE,SAAS,CAAC,CAAC;AAAA,MAC7F;AACA;AAAA,QACE;AAAA,UACE,OAAO;AAAA,UACP,SAAS,GAAG,MAAM,MAAM,yCAAyC,QAAQ;AAAA,UACzE,UAAU,CAAC,EAAE,OAAO,cAAc,SAAS,CAAC,QAAQ,SAAS,QAAQ,YAAY,GAAG,KAAK,CAAC;AAAA,UAC1F,aAAa,oBAAI,KAAK;AAAA,QACxB;AAAA,QACA,KAAK;AAAA,MACP;AACA,cAAQ,IAAI,cAAAA,QAAM,KAAK,uBAAuB,KAAK,MAAM,EAAE,CAAC;AAAA,IAC9D;AAEA,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,YAAQ,KAAK,cAAAA,QAAM,IAAI,uBAAuB,CAAC;AAC/C,UAAM;AAAA,EACR;AACF;AAEA,SAAS,WAAW,KAAqB;AACvC,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,YAAS,QAAQ,KAAK,OAAQ,IAAI,WAAW,CAAC;AAAA,EAChD;AACA,UAAQ,SAAS,GAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAClD;;;AEtGA,IAAAG,cAAgB;AAChB,IAAAC,gBAAkB;AAClB,IAAAC,qBAAkB;AAClB,IAAAC,kBAAe;AACf,IAAAC,oBAAiB;AACjB,IAAAC,mBAAoC;AAkBpC,eAAsB,YAAY,MAA4C;AAC5E,QAAM,cAAU,YAAAC,SAAI,cAAAC,QAAM,KAAK,6BAAwB,CAAC,EAAE,MAAM;AAEhE,MAAI;AACF,UAAM,UAAU,kBAAAC,QAAK,KAAK,KAAK,KAAK,cAAc;AAClD,QAAI,CAAC,gBAAAC,QAAG,WAAW,OAAO,GAAG;AAC3B,cAAQ,KAAK,cAAAF,QAAM,IAAI,4BAA4B,KAAK,GAAG,EAAE,CAAC;AAC9D,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,MAAM,KAAK,MAAM,gBAAAE,QAAG,aAAa,SAAS,OAAO,CAAC;AAKxD,UAAM,WAAW,oBAAI,IAAI;AAAA,MACvB,GAAG,OAAO,KAAK,IAAI,gBAAgB,CAAC,CAAC;AAAA,MACrC,GAAG,OAAO,KAAK,IAAI,mBAAmB,CAAC,CAAC;AAAA,IAC1C,CAAC;AAED,UAAM,SAAS,kBAAAD,QAAK,KAAK,KAAK,KAAK,KAAK;AACxC,UAAM,QAAQ,KAAK,QAAQ,CAAC,WAAW,YAAY,WAAW,UAAU,GAAG,CAAC,gBAAgB,MAAM,CAAC;AAEnG,UAAM,eAAe,oBAAI,IAAY;AAErC,eAAW,YAAY,OAAO;AAC5B,YAAM,UAAU,gBAAAC,QAAG,aAAa,UAAU,OAAO;AACjD,YAAM,cAAc;AACpB,UAAI;AAEJ,cAAQ,QAAQ,YAAY,KAAK,OAAO,OAAO,MAAM;AACnD,cAAM,YAAY,MAAM,CAAC;AAEzB,cAAM,UAAU,UAAU,WAAW,GAAG,IACpC,UAAU,MAAM,GAAG,EAAE,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG,IACzC,UAAU,MAAM,GAAG,EAAE,CAAC;AAC1B,qBAAa,IAAI,OAAO;AAAA,MAC1B;AAAA,IACF;AAEA,UAAM,SAAqB,CAAC;AAG5B,eAAW,OAAO,UAAU;AAC1B,UAAI,CAAC,aAAa,IAAI,GAAG,GAAG;AAC1B,eAAO,KAAK,EAAE,MAAM,KAAK,MAAM,SAAS,CAAC;AAAA,MAC3C;AAAA,IACF;AAGA,eAAWC,QAAO,cAAc;AAC9B,UAAI,CAAC,SAAS,IAAIA,IAAG,KAAK,CAAC,UAAUA,IAAG,GAAG;AACzC,eAAO,KAAK,EAAE,MAAMA,MAAK,MAAM,UAAU,CAAC;AAAA,MAC5C;AAAA,IACF;AAEA,YAAQ,QAAQ,cAAAH,QAAM,MAAM,oCAA+B,OAAO,MAAM,iBAAiB,CAAC;AAE1F,QAAI,OAAO,WAAW,GAAG;AACvB,cAAQ,IAAI,cAAAA,QAAM,MAAM,kCAAkC,CAAC;AAC3D,aAAO;AAAA,IACT;AAEA,UAAM,QAAQ,IAAI,mBAAAI,QAAM,EAAE,MAAM,CAAC,WAAW,OAAO,EAAE,CAAC;AACtD,eAAW,SAAS,QAAQ;AAC1B,YAAM,QACJ,MAAM,SAAS,WACX,cAAAJ,QAAM,OAAO,QAAQ,IACrB,MAAM,SAAS,YACb,cAAAA,QAAM,IAAI,2BAA2B,IACrC,cAAAA,QAAM,QAAQ,kBAAkB;AACxC,YAAM,KAAK,CAAC,cAAAA,QAAM,KAAK,MAAM,IAAI,GAAG,KAAK,CAAC;AAAA,IAC5C;AACA,YAAQ,IAAI,MAAM,SAAS,CAAC;AAE5B,QAAI,KAAK,QAAQ;AACf;AAAA,QACE;AAAA,UACE,OAAO;AAAA,UACP,SAAS,GAAG,OAAO,MAAM;AAAA,UACzB,UAAU;AAAA,YACR;AAAA,cACE,OAAO;AAAA,cACP,SAAS,CAAC,WAAW,MAAM;AAAA,cAC3B,MAAM,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC;AAAA,YAC1C;AAAA,UACF;AAAA,UACA,aAAa,oBAAI,KAAK;AAAA,QACxB;AAAA,QACA,KAAK;AAAA,MACP;AACA,cAAQ,IAAI,cAAAA,QAAM,KAAK,uBAAuB,KAAK,MAAM,EAAE,CAAC;AAAA,IAC9D;AAEA,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,YAAQ,KAAK,cAAAA,QAAM,IAAI,yBAAyB,CAAC;AACjD,UAAM;AAAA,EACR;AACF;AAEA,IAAM,gBAAgB,oBAAI,IAAI;AAAA,EAC5B;AAAA,EAAW;AAAA,EAAa;AAAA,EAAW;AAAA,EAAY;AAAA,EAAe;AAAA,EAC9D;AAAA,EAAe;AAAA,EAAe;AAAA,EAAsB;AAAA,EACpD;AAAA,EAAM;AAAA,EAAQ;AAAA,EAAM;AAAA,EAAO;AAAA,EAAU;AAAA,EAAQ;AAAA,EAAU;AAAA,EAAU;AACnE,CAAC;AAED,SAAS,UAAU,MAAuB;AACxC,SAAO,cAAc,IAAI,IAAI,KAAK,KAAK,WAAW,OAAO;AAC3D;;;ACpIA,IAAAK,gBAAkB;AAElB,IAAAC,oBAAiB;;;ACFjB,IAAAC,cAAgB;AAChB,IAAAC,gBAAkB;AAClB,IAAAC,qBAAkB;AAwDlB,eAAsB,YAAY,KAAa,MAA4C;AACzF,QAAM,cAAU,YAAAC,SAAI,cAAAC,QAAM,KAAK,qCAAgC,CAAC,EAAE,MAAM;AAExE,MAAI;AACF,UAAM,WAAW,cAAc,KAAK,CAAC,CAAC;AACtC,UAAM,UAAU,aAAa,QAAQ;AACrC,UAAM,QAAQ,WAAW,UAAU,CAAC,MAAM;AACxC,YAAM,KAAK,QAAQ,cAAc,CAAC;AAClC,aAAO,KAAK,kBAAkB,EAAE,IAAI,CAAC;AAAA,IACvC,CAAC;AACD,UAAM,SAAS,aAAa,KAAK;AAEjC,YAAQ;AAAA,MACN,cAAAA,QAAM,MAAM,wCAAmC,OAAO,MAAM,iBAAiB;AAAA,IAC/E;AAEA,QAAI,OAAO,WAAW,GAAG;AACvB,cAAQ,IAAI,cAAAA,QAAM,MAAM,iCAAiC,CAAC;AAC1D,aAAO;AAAA,IACT;AAEA,UAAM,QAAQ,IAAI,mBAAAC,QAAM,EAAE,MAAM,CAAC,WAAW,gBAAgB,EAAE,CAAC;AAC/D,WAAO,QAAQ,CAAC,OAAO,MAAM;AAC3B,YAAM,KAAK,CAAC,cAAAD,QAAM,OAAO,OAAO,IAAI,CAAC,CAAC,GAAG,MAAM,IAAI,CAAC,MAAM,cAAAA,QAAM,KAAK,CAAC,CAAC,EAAE,KAAK,aAAQ,CAAC,CAAC;AAAA,IAC1F,CAAC;AACD,YAAQ,IAAI,MAAM,SAAS,CAAC;AAE5B,QAAI,KAAK,QAAQ;AACf;AAAA,QACE;AAAA,UACE,OAAO;AAAA,UACP,SAAS,GAAG,OAAO,MAAM;AAAA,UACzB,UAAU;AAAA,YACR;AAAA,cACE,OAAO;AAAA,cACP,SAAS,CAAC,WAAW,OAAO;AAAA,cAC5B,MAAM,OAAO,IAAI,CAAC,OAAO,MAAM,CAAC,OAAO,IAAI,CAAC,GAAG,MAAM,KAAK,UAAK,CAAC,CAAC;AAAA,YACnE;AAAA,UACF;AAAA,UACA,aAAa,oBAAI,KAAK;AAAA,QACxB;AAAA,QACA,KAAK;AAAA,MACP;AACA,cAAQ,IAAI,cAAAA,QAAM,KAAK,uBAAuB,KAAK,MAAM,EAAE,CAAC;AAAA,IAC9D;AAEA,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,YAAQ,KAAK,cAAAA,QAAM,IAAI,6BAA6B,CAAC;AACrD,UAAM;AAAA,EACR;AACF;;;ADvFA,eAAsB,gBAAgB,KAAa,MAA0C;AAC3F,UAAQ,IAAI,cAAAE,QAAM,KAAK,KAAK,6CAAwC,CAAC;AAErE,QAAM,CAAC,aAAa,OAAO,QAAQ,SAAS,IAAI,MAAM,QAAQ,IAAI;AAAA,IAChE,YAAY,KAAK,CAAC,CAAC,EAAE,MAAM,MAAM,CAAC,CAAC;AAAA,IACnC,cAAc,KAAK,CAAC,CAAC,EAAE,MAAM,MAAM,CAAC,CAAC;AAAA,IACrC,YAAY,KAAK,CAAC,CAAC,EAAE,MAAM,MAAM,CAAC,CAAC;AAAA,IACnC,YAAY,EAAE,KAAK,kBAAAC,QAAK,QAAQ,KAAK,IAAI,GAAG,QAAQ,OAAU,CAAC,EAAE,MAAM,MAAM,CAAC,CAAC;AAAA,EACjF,CAAC;AAED,QAAM,WAA4B;AAAA,IAChC;AAAA,MACE,OAAO;AAAA,MACP,SAAS,CAAC,QAAQ,QAAQ;AAAA,MAC1B,MAAM,YAAY,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC;AAAA,IACrD;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,SAAS,CAAC,QAAQ,SAAS,aAAa;AAAA,MACxC,MAAM,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,MAAM,GAAG,CAAC,GAAG,OAAO,EAAE,KAAK,GAAG,OAAO,EAAE,YAAY,MAAM,CAAC,CAAC;AAAA,IAC5F;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,SAAS,CAAC,WAAW,OAAO;AAAA,MAC5B,MAAM,OAAO,IAAI,CAAC,OAAO,MAAM,CAAC,OAAO,IAAI,CAAC,GAAG,MAAM,KAAK,UAAK,CAAC,CAAC;AAAA,IACnE;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,SAAS,CAAC,WAAW,OAAO;AAAA,MAC5B,MAAM,UAAU,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC;AAAA,IAC7C;AAAA,EACF;AAEA,QAAM,cACJ,YAAY,SAAS,MAAM,SAAS,OAAO,SAAS,UAAU;AAEhE,QAAM,SAAiB;AAAA,IACrB,OAAO;AAAA,IACP,SAAS,aAAa,kBAAAA,QAAK,QAAQ,GAAG,CAAC,oBAAoB,WAAW;AAAA,IACtE;AAAA,IACA,aAAa,oBAAI,KAAK;AAAA,EACxB;AAEA,gBAAc,QAAQ,KAAK,MAAM;AAEjC,UAAQ;AAAA,IACN,cAAAD,QAAM,KAAK;AAAA,4BAA+B,IAAI,cAAAA,QAAM,KAAK,KAAK,MAAM;AAAA,EACtE;AACA,UAAQ;AAAA,IACN,cAAAA,QAAM,IAAI,wBAAwB,KAC/B,cAAc,IAAI,cAAAA,QAAM,IAAI,OAAO,WAAW,CAAC,IAAI,cAAAA,QAAM,MAAM,GAAG;AAAA,EACvE;AACF;;;AE1EA,IAAAE,kBAAe;AACf,IAAAC,oBAAiB;AACjB,IAAAC,cAAgB;AAChB,IAAAC,gBAAkB;AAClB,IAAAC,qBAAkB;AAwBlB,IAAM,mBAAmB,oBAAI,IAAI;AAAA,EAC/B;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAS;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAS;AAAA,EAAS;AAAA,EAAQ;AAAA,EACnE;AAAA,EAAS;AAAA,EAAU;AAAA,EAAQ;AAAA,EAAQ;AAAA,EACnC;AAAA,EAAQ;AAAA,EAAS;AAAA,EAAQ;AAAA,EAAQ;AAAA,EACjC;AACF,CAAC;AAED,IAAMC,mBAAkB;AAAA,EACtB;AAAA,EAAW;AAAA,EAAY;AAAA,EAAW;AAAA,EAClC;AAAA,EAAY;AAAA,EAAa;AAAA,EAAa;AAAA,EACtC;AAAA,EAAa;AACf;AAEA,IAAM,gBAAgB;AAAA,EACpB;AAAA,EAAgB;AAAA,EAChB;AAAA,EAAQ;AAAA,EACR;AAAA,EAAS;AAAA,EACT;AAAA,EAAY;AAAA,EACZ;AAAA,EAAU;AACZ;AASO,SAAS,oBAAoB,SAAmC;AACrE,QAAM,YAAY,kBAAAC,QAAK,KAAK,SAAS,QAAQ;AAE7C,MAAI,CAAC,gBAAAC,QAAG,WAAW,SAAS,GAAG;AAC7B,WAAO,EAAE,cAAc,CAAC,GAAG,aAAa,GAAG,QAAQ,iBAAiB,CAAC,GAAG,GAAG,OAAO,EAAE;AAAA,EACtF;AAEA,QAAM,SAAS,cAAc,SAAS;AACtC,MAAI,OAAO,WAAW,GAAG;AACvB,WAAO,EAAE,cAAc,CAAC,GAAG,aAAa,GAAG,QAAQ,iBAAiB,CAAC,GAAG,GAAG,OAAO,EAAE;AAAA,EACtF;AAGA,QAAM,cAAc,KAAK,SAASF,kBAAiB,aAAa;AAChE,QAAM,gBAAgB,YAAY,OAAO,CAAC,KAAK,MAAM;AACnD,QAAI;AAAE,aAAO,MAAM,gBAAAE,QAAG,aAAa,GAAG,OAAO,IAAI;AAAA,IAAK,QAAQ;AAAE,aAAO;AAAA,IAAI;AAAA,EAC7E,GAAG,EAAE;AAEL,QAAM,SAAwB,CAAC;AAE/B,aAAW,YAAY,QAAQ;AAC7B,UAAM,WAAW,kBAAAD,QAAK,SAAS,QAAQ;AAEvC,UAAM,gBAAgB,MAAM,kBAAAA,QAAK,SAAS,WAAW,QAAQ,EAAE,WAAW,MAAM,GAAG;AAEnF,UAAM,aACJ,cAAc,SAAS,QAAQ,KAC/B,cAAc,SAAS,aAAa;AAEtC,QAAI,CAAC,YAAY;AACf,aAAO,KAAK;AAAA,QACV,UAAU;AAAA,QACV,cAAc,kBAAAA,QAAK,SAAS,SAAS,QAAQ,EAAE,WAAW,MAAM,GAAG;AAAA,QACnE,WAAWE,aAAY,QAAQ;AAAA,MACjC,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,SAAS,iBAAiB,QAAQ,OAAO,QAAQ,OAAO;AAC9D,SAAO,EAAE,cAAc,QAAQ,aAAa,OAAO,QAAQ,OAAO;AACpE;AAIA,eAAsB,cAAc,SAAiB,MAAiD;AACpG,QAAM,YAAY,kBAAAF,QAAK,KAAK,SAAS,QAAQ;AAE7C,MAAI,CAAC,gBAAAC,QAAG,WAAW,SAAS,GAAG;AAC7B,YAAQ,IAAI,cAAAE,QAAM,IAAI,uDAAkD,CAAC;AACzE,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,cAAU,YAAAC,SAAI,cAAAD,QAAM,KAAK,0CAAqC,CAAC,EAAE,MAAM;AAE7E,MAAI;AACF,UAAM,SAAS,oBAAoB,OAAO;AAE1C,YAAQ;AAAA,MACN,cAAAA,QAAM;AAAA,QACJ,8BAAyB,OAAO,aAAa,MAAM,aAAa,OAAO,WAAW;AAAA,MACpF;AAAA,IACF;AAEA,QAAI,OAAO,aAAa,WAAW,GAAG;AACpC,cAAQ,IAAI,cAAAA,QAAM,MAAM,+CAA+C,CAAC;AACxE,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,QAAQ,IAAI,mBAAAE,QAAM,EAAE,MAAM,CAAC,SAAS,MAAM,EAAE,CAAC;AACnD,eAAW,SAAS,OAAO,cAAc;AACvC,YAAM,MAAM,MAAM,YAAY,MAAM,QAAQ,CAAC;AAC7C,YAAM,KAAK,CAAC,cAAAF,QAAM,KAAK,MAAM,YAAY,GAAG,GAAG,EAAE,KAAK,CAAC;AAAA,IACzD;AACA,YAAQ,IAAI,MAAM,SAAS,CAAC;AAE5B,QAAI,KAAK,QAAQ;AACf;AAAA,QACE;AAAA,UACE,OAAO;AAAA,UACP,SAAS,GAAG,OAAO,aAAa,MAAM;AAAA,UACtC,UAAU;AAAA,YACR;AAAA,cACE,OAAO;AAAA,cACP,SAAS,CAAC,SAAS,WAAW;AAAA,cAC9B,MAAM,OAAO,aAAa,IAAI,CAAC,MAAM;AAAA,gBACnC,EAAE;AAAA,iBACD,EAAE,YAAY,MAAM,QAAQ,CAAC;AAAA,cAChC,CAAC;AAAA,YACH;AAAA,UACF;AAAA,UACA,aAAa,oBAAI,KAAK;AAAA,QACxB;AAAA,QACA,KAAK;AAAA,MACP;AACA,cAAQ,IAAI,cAAAA,QAAM,KAAK,uBAAuB,KAAK,MAAM,EAAE,CAAC;AAAA,IAC9D;AAEA,WAAO,OAAO;AAAA,EAChB,SAAS,KAAK;AACZ,YAAQ,KAAK,cAAAA,QAAM,IAAI,mBAAmB,CAAC;AAC3C,UAAM;AAAA,EACR;AACF;AAIA,SAAS,cAAc,KAAuB;AAC5C,QAAM,UAAoB,CAAC;AAE3B,WAAS,KAAK,SAAuB;AACnC,QAAI;AACJ,QAAI;AACF,gBAAU,gBAAAF,QAAG,YAAY,SAAS,EAAE,eAAe,KAAK,CAAC;AAAA,IAC3D,QAAQ;AACN;AAAA,IACF;AACA,eAAW,SAAS,SAAS;AAC3B,YAAM,OAAO,kBAAAD,QAAK,KAAK,SAAS,MAAM,IAAI;AAC1C,UAAI,MAAM,YAAY,GAAG;AACvB,aAAK,IAAI;AAAA,MACX,WAAW,MAAM,OAAO,KAAK,iBAAiB,IAAI,kBAAAA,QAAK,QAAQ,MAAM,IAAI,EAAE,YAAY,CAAC,GAAG;AACzF,gBAAQ,KAAK,IAAI;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AAEA,OAAK,GAAG;AACR,SAAO;AACT;AAEA,SAASE,aAAY,UAA0B;AAC7C,MAAI;AAAE,WAAO,gBAAAD,QAAG,SAAS,QAAQ,EAAE;AAAA,EAAK,QAAQ;AAAE,WAAO;AAAA,EAAE;AAC7D;AAEA,SAAS,iBAAiB,QAAuB,aAAqB,SAAyB;AAC7F,QAAM,aAAa,OAAO,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,WAAW,CAAC;AAC7D,QAAM,WAAW,aAAa,MAAM,QAAQ,CAAC;AAE7C,QAAM,QAAkB;AAAA,IACtB;AAAA,IACA;AAAA,IACA,sBAAsB,WAAW;AAAA,IACjC,sBAAsB,OAAO,MAAM;AAAA,IACnC,uBAAuB,OAAO;AAAA,IAC9B;AAAA,IACA;AAAA,EACF;AAEA,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,KAAK,iDAAiD,EAAE;AAC9D,WAAO,MAAM,KAAK,IAAI;AAAA,EACxB;AAEA,QAAM,KAAK,2CAAuB,EAAE;AACpC,aAAW,SAAS,QAAQ;AAC1B,UAAM;AAAA,MACJ,iBAAY,MAAM,YAAY;AAAA,MAC9B,WAAW,MAAM,YAAY,MAAM,QAAQ,CAAC,CAAC;AAAA,MAC7C;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;;;AX5NA;AAUA,SAAS,iBAAyB;AAChC,MAAI;AAEF,QAAI,OAAO,gBAAgB,eAAe,YAAY,KAAK;AACzD,YAAM,MAAM,kBAAAK,QAAK,YAAQ,+BAAc,YAAY,GAAG,CAAC;AACvD,YAAM,UAAU,kBAAAA,QAAK,QAAQ,KAAK,MAAM,cAAc;AACtD,aAAQ,KAAK,MAAM,gBAAAC,QAAG,aAAa,SAAS,OAAO,CAAC,EAA0B;AAAA,IAChF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,MAAI;AAGF,UAAM,MAAe,WAAmB,aAAa;AACrD,UAAM,UAAU,kBAAAD,QAAK,QAAQ,KAAK,MAAM,cAAc;AACtD,WAAQ,KAAK,MAAM,gBAAAC,QAAG,aAAa,SAAS,OAAO,CAAC,EAA0B;AAAA,EAChF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,IAAM,cAAc,eAAe;AAmBnC,IAAM,cAAwB,CAAC,aAAa,SAAS,YAAY,QAAQ,QAAQ;AAejF,IAAM,UAAU,IAAI,yBAAQ;AAE5B,QACG,KAAK,SAAS,EACd,YAAY,sCAAsC,EAClD,QAAQ,aAAa,eAAe,EACpC,OAAO,gBAAgB,6BAA6B,QAAQ,IAAI,CAAC,EACjE,OAAO,kBAAkB,sBAAsB,EAC/C,OAAO,oBAAoB,uDAAuD,EAClF;AAAA,EACC;AAAA,EACA;AAAA,EACA,CAAC,KAAa,QAAkB,CAAC,GAAG,KAAK,GAAG;AAAA,EAC5C,CAAC;AACH,EACC,OAAO,gBAAgB,8BAA8B,EACrD,OAAO,UAAU,gCAAgC,EACjD,OAAO,YAAY,4CAA4C,EAC/D,OAAO,QAAQ,yDAAyD,EACxE,OAAO,IAAI;AAEd,QAAQ,MAAM;AAId,eAAe,KAAK,MAAiC;AACnD,QAAM,UAAU,kBAAAD,QAAK,QAAQ,KAAK,GAAG;AAErC,MAAI,CAAC,gBAAAC,QAAG,WAAW,kBAAAD,QAAK,KAAK,SAAS,cAAc,CAAC,GAAG;AACtD,YAAQ,MAAM,cAAAE,QAAM,IAAI,oCAA+B,OAAO,EAAE,CAAC;AACjE,YAAQ,MAAM,cAAAA,QAAM,IAAI,oDAAoD,CAAC;AAC7E,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,UAAU,eAAe,KAAK,IAAI;AAGxC,UAAQ,IAAI;AACZ,UAAQ,IAAI,cAAAA,QAAM,KAAK,KAAK,+DAAmD,CAAC;AAChF,UAAQ,IAAI;AAGZ,QAAM,eAAe,cAAc,cAAAA,QAAM,KAAK,wBAAmB,CAAC;AAClE,QAAM,QAAQ,cAAc,SAAS,KAAK,MAAM;AAChD,eAAa,QAAQ,cAAAA,QAAM,MAAM,0BAAqB,MAAM,MAAM,gBAAgB,CAAC;AAGnF,QAAM,eAAe,cAAc,cAAAA,QAAM,KAAK,6BAAwB,CAAC;AACvE,QAAM,UAAU,aAAa,KAAK;AAClC,QAAM,QAAqB,WAAW,OAAO,CAAC,MAAM;AAClD,UAAM,KAAK,QAAQ,cAAc,CAAC;AAClC,WAAO,KAAK,kBAAkB,EAAE,IAAI,CAAC;AAAA,EACvC,CAAC;AACD,QAAM,YAAY,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,MAAM,CAAC;AACpE,eAAa,QAAQ,cAAAA,QAAM,MAAM,6BAAwB,SAAS,UAAU,CAAC;AAE7E,QAAM,cAAcC,iBAAgB,OAAO;AAC3C,QAAM,cAAc,KAAK,QAAQ,CAAC,kBAAAH,QAAK,QAAQ,KAAK,KAAK,CAAC,IAAI,gBAAgB,SAAS,WAAW;AAGlG,QAAM,aAAa,iBAAiB,SAAS,KAAK,GAAG;AACrD,oBAAkB,OAAO;AAEzB,UAAQ,IAAI;AAIZ,MAAI,gBAAgB;AACpB,MAAI,YAAY;AAChB,MAAI,iBAAiB;AACrB,MAAI,gBAAgB;AACpB,MAAI,mBAAmB;AAEvB,MAAI,iBAAiB;AACrB,MAAI,kBAAkB;AACtB,MAAI,iBAAiB;AACrB,MAAI,qBAAqB;AACzB,MAAI,mBAAmB;AAEvB,QAAM,gBAA0B,CAAC;AAEjC,MAAI,QAAQ,SAAS,WAAW,GAAG;AACjC,UAAM,UAAU,cAAc,cAAAE,QAAM,KAAK,2BAAsB,CAAC;AAChE,UAAM,SAAS,kBAAkB,SAAS,OAAO,aAAa,OAAO;AACrE,oBAAgB,OAAO,UAAU,SAAS,OAAO,YAAY;AAC7D,kBAAc,KAAK,GAAG,OAAO,SAAS;AACtC,YAAQ,QAAQ,cAAAA,QAAM,MAAM,sCAAiC,aAAa,gBAAgB,CAAC;AAE3F,QAAI,OAAO,QAAQ;AACjB,uBAAiB;AACjB,kBAAY,YAAY,gBAAgB,OAAO,MAAM;AAAA,IACvD;AAAA,EACF;AAEA,MAAI,QAAQ,SAAS,OAAO,GAAG;AAC7B,UAAM,aAAa,kBAAAF,QAAK,KAAK,YAAY,UAAU;AACnD,UAAM,QAAQ,MAAM,cAAc,SAAS,EAAE,QAAQ,WAAW,CAAC;AACjE,gBAAY,MAAM;AAClB,QAAI,YAAY,EAAG,mBAAkB;AAAA,EACvC;AAEA,MAAI,QAAQ,SAAS,UAAU,GAAG;AAChC,UAAM,UAAU,cAAc,cAAAE,QAAM,KAAK,kCAA6B,CAAC;AACvE,UAAM,SAAS,aAAa,KAAK;AACjC,oBAAgB,OAAO;AACvB,YAAQ,QAAQ,cAAAA,QAAM,MAAM,4CAAuC,aAAa,iBAAiB,CAAC;AAElG,QAAI,gBAAgB,GAAG;AACrB,2BAAqB;AACrB,YAAM,YAAY,OACf,IAAI,CAAC,GAAG,MAAM,SAAS,IAAI,CAAC,KAAK,EAAE,KAAK,UAAK,CAAC,EAAE,EAChD,KAAK,IAAI;AACZ,kBAAY,YAAY,oBAAoB,SAAS;AAAA,IACvD;AAAA,EACF;AAEA,MAAI,QAAQ,SAAS,MAAM,GAAG;AAC5B,UAAM,aAAa,kBAAAF,QAAK,KAAK,YAAY,SAAS;AAClD,UAAM,SAAS,MAAM,YAAY,EAAE,KAAK,SAAS,QAAQ,WAAW,CAAC;AACrE,qBAAiB,OAAO,OAAO,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE;AAC3D,QAAI,OAAO,SAAS,EAAG,kBAAiB;AAAA,EAC1C;AAEA,MAAI,QAAQ,SAAS,QAAQ,GAAG;AAC9B,UAAM,aAAa,kBAAAA,QAAK,KAAK,YAAY,WAAW;AACpD,UAAM,eAAe,MAAM,cAAc,SAAS,EAAE,QAAQ,WAAW,CAAC;AACxE,uBAAmB,aAAa;AAChC,QAAI,mBAAmB,EAAG,oBAAmB;AAAA,EAC/C;AAEA,MAAI,QAAQ,SAAS,QAAQ,GAAG;AAC9B,UAAM,aAAa,kBAAAA,QAAK,KAAK,YAAY,kBAAkB;AAC3D,UAAM,gBAAgB,SAAS,EAAE,QAAQ,WAAW,CAAC;AAAA,EACvD;AAEA,MAAI,KAAK,MAAM;AACb,UAAM,WAAW,kBAAAA,QAAK,KAAK,YAAY,kBAAkB;AACzD,oBAAgB,UAAU,SAAS,eAAe,eAAe,WAAW,cAAc;AAC1F,YAAQ,IAAI,cAAAE,QAAM,KAAK,4BAA4B,QAAQ,EAAE,CAAC;AAAA,EAChE;AAIA,UAAQ,IAAI;AACZ,UAAQ,IAAI,cAAAA,QAAM,KAAK,SAAS,CAAC;AACjC,UAAQ,IAAI;AAEZ,QAAM,QAAQ,IAAI,mBAAAE,QAAM;AAAA,IACtB,MAAM,CAAC,cAAAF,QAAM,KAAK,OAAO,GAAG,cAAAA,QAAM,KAAK,OAAO,GAAG,cAAAA,QAAM,KAAK,aAAa,CAAC;AAAA,IAC1E,OAAO,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,EAAE;AAAA,EAChC,CAAC;AAED,QAAM,MAAM,CAAC,MAAe,IAAI,IAAI,cAAAA,QAAM,OAAO,OAAO,CAAC,CAAC,IAAI,cAAAA,QAAM,MAAM,GAAG;AAE7E,QAAM;AAAA,IACJ,CAAC,wBAAwB,IAAI,aAAa,GAAG,kBAAkB,QAAG;AAAA,IAClE,CAAC,sBAAsB,IAAI,SAAS,GAAG,mBAAmB,QAAG;AAAA,IAC7D,CAAC,mBAAmB,IAAI,cAAc,GAAG,kBAAkB,QAAG;AAAA,IAC9D,CAAC,iBAAiB,IAAI,aAAa,GAAG,sBAAsB,QAAG;AAAA,IAC/D,CAAC,iBAAiB,IAAI,gBAAgB,GAAG,oBAAoB,QAAG;AAAA,EAClE;AAEA,UAAQ,IAAI,MAAM,SAAS,CAAC;AAC5B,UAAQ,IAAI;AAIZ,MAAI,KAAK,UAAU,cAAc,SAAS,GAAG;AAC3C,YAAQ,IAAI,cAAAA,QAAM,OAAO,eAAe,cAAc,MAAM,IAAI,CAAC;AACjE,eAAW,KAAK,eAAe;AAC7B,cAAQ,IAAI,cAAAA,QAAM,IAAI,KAAK,kBAAAF,QAAK,SAAS,SAAS,CAAC,CAAC,EAAE,CAAC;AAAA,IACzD;AACA,YAAQ,IAAI;AAEZ,QAAI,CAAC,KAAK,IAAI;AACZ,YAAM,YAAY,MAAM,cAAc,4BAA4B;AAClE,UAAI,WAAW;AACb,mBAAW,KAAK,eAAe;AAC7B,0BAAAC,QAAG,OAAO,GAAG,EAAE,OAAO,KAAK,CAAC;AAAA,QAC9B;AACA,gBAAQ,IAAI,cAAAC,QAAM,MAAM,aAAa,cAAc,MAAM,WAAW,CAAC;AAAA,MACvE,OAAO;AACL,gBAAQ,IAAI,cAAAA,QAAM,IAAI,YAAY,CAAC;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AAIA,MAAI,KAAK,IAAI;AACX,UAAM,YAAY,gBAAgB,KAAK,YAAY,KAAK,iBAAiB,KAAK,gBAAgB,KAAK,mBAAmB;AACtH,QAAI,UAAW,SAAQ,KAAK,CAAC;AAAA,EAC/B;AACF;AAIA,SAAS,eAAe,MAAoC;AAC1D,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,QAAQ,oBAAI,IAAY,CAAC,GAAG,aAAa,QAAQ,CAAC;AACxD,SAAO,KACJ,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,CAAW,EAC7B,OAAO,CAAC,MAAM,MAAM,IAAI,CAAC,CAAC;AAC/B;AAEA,SAASC,iBAAgB,KAAsB;AAC7C,MAAI;AACF,WAAO,KAAK,MAAM,gBAAAF,QAAG,aAAa,kBAAAD,QAAK,KAAK,KAAK,cAAc,GAAG,OAAO,CAAC;AAAA,EAC5E,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,cAAc,UAAoC;AACzD,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,KAAK,qBAAAK,QAAS,gBAAgB,EAAE,OAAO,QAAQ,OAAO,QAAQ,QAAQ,OAAO,CAAC;AACpF,OAAG,SAAS,UAAU,CAAC,WAAW;AAChC,SAAG,MAAM;AACT,cAAQ,OAAO,KAAK,EAAE,YAAY,MAAM,GAAG;AAAA,IAC7C,CAAC;AAAA,EACH,CAAC;AACH;AAEA,SAAS,gBACP,YACA,SACA,WACA,eACA,WACA,gBACM;AACN,QAAM,OAAO;AAAA,IACX,CAAC,wBAAwB,OAAO,UAAU,MAAM,CAAC;AAAA,IACjD,CAAC,sBAAsB,OAAO,SAAS,CAAC;AAAA,IACxC,CAAC,yBAAyB,OAAO,aAAa,CAAC;AAAA,IAC/C,CAAC,mBAAmB,OAAO,cAAc,CAAC;AAAA,EAC5C,EACG,IAAI,CAAC,CAAC,OAAO,GAAG,MAAM,eAAe,KAAK,YAAY,GAAG,YAAY,EACrE,KAAK,IAAI;AAEZ,QAAM,WACJ,UAAU,SAAS,IACf,OAAO,UAAU,IAAI,CAAC,MAAM,OAAO,kBAAAL,QAAK,SAAS,SAAS,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC,UAC7E;AAEN,QAAM,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAgBM,oBAAI,KAAK,GAAE,YAAY,CAAC;AAAA;AAAA;AAAA;AAAA,EAI3C,IAAI;AAAA;AAAA;AAAA,IAGF,QAAQ;AAAA;AAAA;AAIV,kBAAAC,QAAG,UAAU,kBAAAD,QAAK,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;AAC1D,kBAAAC,QAAG,cAAc,YAAY,MAAM,OAAO;AAC5C;","names":["import_chalk","import_cli_table3","import_node_fs","import_node_path","import_node_fs","import_node_path","fs","path","path","path","fs","import_node_fs","import_node_path","path","fs","path","import_ora","import_node_fs","import_node_path","import_ts_morph","import_node_fs","import_node_path","ora","path","fs","path","fs","fs","path","ora","chalk","Table","import_ora","import_chalk","import_cli_table3","import_node_fs","import_ts_morph","import_ts_morph","ora","chalk","fs","Table","import_ora","import_chalk","import_cli_table3","import_node_fs","import_node_path","import_ts_morph","ora","chalk","path","fs","pkg","Table","import_chalk","import_node_path","import_ora","import_chalk","import_cli_table3","ora","chalk","Table","chalk","path","import_node_fs","import_node_path","import_ora","import_chalk","import_cli_table3","SOURCE_PATTERNS","path","fs","getFileSize","chalk","ora","Table","path","fs","chalk","loadPackageJson","Table","readline"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/core/parser.ts","../src/utils/file.ts","../src/core/graph.ts","../src/modules/dead-code.ts","../src/core/reporter.ts","../src/modules/dupe-finder.ts","../src/utils/ast.ts","../src/modules/dep-check.ts","../src/modules/health-report.ts","../src/modules/circular.ts","../src/modules/assets.ts"],"sourcesContent":["import { Command } from 'commander'\nimport chalk from 'chalk'\nimport Table from 'cli-table3'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport readline from 'node:readline'\n\n// Read version from package.json at runtime so it is always in sync.\n// Works in both ESM (import.meta.url defined) and CJS (tsup injects __dirname).\nfunction readPkgVersion(): string {\n try {\n // ESM path\n if (typeof import.meta !== 'undefined' && import.meta.url) {\n const dir = path.dirname(fileURLToPath(import.meta.url))\n const pkgPath = path.resolve(dir, '..', 'package.json')\n return (JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version: string }).version\n }\n } catch {\n // fall through\n }\n try {\n // CJS path — __dirname is the dist/ folder\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const dir: string = (globalThis as any).__dirname ?? __dirname\n const pkgPath = path.resolve(dir, '..', 'package.json')\n return (JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version: string }).version\n } catch {\n return '0.0.0'\n }\n}\n\nconst PKG_VERSION = readPkgVersion()\nimport { discoverFiles, buildProject, getImportsForFile } from '@/core/parser.js'\nimport { buildGraph, findEntryPoints, detectCycles, type ImportGraph } from '@/core/graph.js'\nimport { runDeadCodeModule } from '@/modules/dead-code.js'\nimport { runDupeFinder } from '@/modules/dupe-finder.js'\nimport { runDepCheck } from '@/modules/dep-check.js'\nimport { runHealthReport } from '@/modules/health-report.js'\nimport { runAssetCheck } from '@/modules/assets.js'\nimport {\n createSpinner,\n ensureReportsDir,\n appendToGitignore,\n writeReport,\n} from '@/core/reporter.js'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\ntype Module = 'dead-code' | 'dupes' | 'circular' | 'deps' | 'assets' | 'health'\n\nconst ALL_MODULES: Module[] = ['dead-code', 'dupes', 'circular', 'deps', 'assets']\n\ninterface CliOptions {\n dir: string\n entry?: string\n only?: string\n ignore: string[]\n out?: string\n html?: boolean\n delete?: boolean\n ci?: boolean\n}\n\n// ─── CLI definition ───────────────────────────────────────────────────────────\n\nconst program = new Command()\n\nprogram\n .name('prunify')\n .description('npm run clean. ship with confidence.')\n .version(PKG_VERSION, '-v, --version')\n .option('--dir <path>', 'Root directory to analyze', process.cwd())\n .option('--entry <path>', 'Override entry point')\n .option('--only <modules>', 'Comma-separated: dead-code,dupes,circular,deps,health')\n .option(\n '--ignore <pattern>',\n 'Glob pattern to ignore (repeatable)',\n (val: string, acc: string[]) => [...acc, val],\n [] as string[],\n )\n .option('--out <path>', 'Output directory for reports')\n .option('--html', 'Also generate code_health.html')\n .option('--delete', 'Prompt to delete dead files after analysis')\n .option('--ci', 'CI mode: exit 1 if issues found, no interactive prompts')\n .action(main)\n\nprogram.parse()\n\n// ─── Main ─────────────────────────────────────────────────────────────────────\n\nasync function main(opts: CliOptions): Promise<void> {\n const rootDir = path.resolve(opts.dir)\n\n if (!fs.existsSync(path.join(rootDir, 'package.json'))) {\n console.error(chalk.red(`✗ No package.json found in ${rootDir}`))\n console.error(chalk.dim(' Use --dir <path> to point to your project root.'))\n process.exit(1)\n }\n\n const modules = resolveModules(opts.only)\n\n // Banner\n console.log()\n console.log(chalk.bold.cyan('🧹 prunify — npm run clean. ship with confidence.'))\n console.log()\n\n // Step 3: Parse codebase\n const parseSpinner = createSpinner(chalk.cyan('Parsing codebase…'))\n const files = discoverFiles(rootDir, opts.ignore)\n parseSpinner.succeed(chalk.green(`Parsed codebase — ${files.length} file(s) found`))\n\n // Step 4: Build import graph\n const graphSpinner = createSpinner(chalk.cyan('Building import graph…'))\n const project = buildProject(files)\n const graph: ImportGraph = buildGraph(files, (f) => {\n const sf = project.getSourceFile(f)\n return sf ? getImportsForFile(sf) : []\n })\n const edgeCount = [...graph.values()].reduce((n, s) => n + s.size, 0)\n graphSpinner.succeed(chalk.green(`Import graph built — ${edgeCount} edge(s)`))\n\n const packageJson = loadPackageJson(rootDir)\n const entryPoints = opts.entry ? [path.resolve(opts.entry)] : findEntryPoints(rootDir, packageJson)\n\n // Set up reports dir\n const reportsDir = ensureReportsDir(rootDir, opts.out)\n appendToGitignore(rootDir)\n\n console.log()\n\n // ─── Run modules ─────────────────────────────────────────────────────────\n\n let deadFileCount = 0\n let dupeCount = 0\n let unusedPkgCount = 0\n let circularCount = 0\n let unusedAssetCount = 0\n\n let deadReportFile = ''\n let dupesReportFile = ''\n let depsReportFile = ''\n let circularReportFile = ''\n let assetsReportFile = ''\n\n const deadFilePaths: string[] = []\n\n if (modules.includes('dead-code')) {\n const spinner = createSpinner(chalk.cyan('Analysing dead code…'))\n const result = runDeadCodeModule(project, graph, entryPoints, rootDir)\n deadFileCount = result.safeToDelete.length + result.transitivelyDead.length + result.deadExports.length\n deadFilePaths.push(...result.safeToDelete, ...result.transitivelyDead)\n spinner.succeed(\n chalk.green(\n `Dead code analysis complete — ${result.safeToDelete.length} safe to delete, ` +\n `${result.transitivelyDead.length} transitively dead, ` +\n `${result.deadExports.length} dead export(s)`,\n ),\n )\n\n if (result.report) {\n deadReportFile = 'dead-code.txt'\n writeReport(reportsDir, deadReportFile, result.report)\n }\n }\n\n if (modules.includes('dupes')) {\n const outputPath = path.join(reportsDir, 'dupes.md')\n const dupes = await runDupeFinder(rootDir, { output: outputPath })\n dupeCount = dupes.length\n if (dupeCount > 0) dupesReportFile = 'dupes.md'\n }\n\n if (modules.includes('circular')) {\n const spinner = createSpinner(chalk.cyan('Analysing circular imports…'))\n const cycles = detectCycles(graph)\n circularCount = cycles.length\n spinner.succeed(chalk.green(`Circular import analysis complete — ${circularCount} cycle(s) found`))\n\n if (circularCount > 0) {\n circularReportFile = 'circular.txt'\n const rel = (p: string) => path.relative(rootDir, p).replaceAll('\\\\', '/')\n const cycleLines: string[] = [\n '========================================',\n ' CIRCULAR DEPENDENCIES',\n ` Cycles found: ${cycles.length}`,\n '========================================',\n '',\n ]\n for (let i = 0; i < cycles.length; i++) {\n const cycle = cycles[i]!\n cycleLines.push(`Cycle ${i + 1} (${cycle.length} files):`)\n for (let j = 0; j < cycle.length; j++) {\n const arrow = j < cycle.length - 1 ? ' → ' : ' → '\n cycleLines.push(` ${rel(cycle[j])}`)\n }\n cycleLines.push(` ↻ ${rel(cycle[0])} (back to start)`)\n cycleLines.push('')\n }\n writeReport(reportsDir, circularReportFile, cycleLines.join('\\n'))\n }\n }\n\n if (modules.includes('deps')) {\n const outputPath = path.join(reportsDir, 'deps.md')\n const issues = await runDepCheck({ cwd: rootDir, output: outputPath })\n unusedPkgCount = issues.filter((i) => i.type === 'unused').length\n if (issues.length > 0) depsReportFile = 'deps.md'\n }\n\n if (modules.includes('assets')) {\n const outputPath = path.join(reportsDir, 'assets.md')\n const unusedAssets = await runAssetCheck(rootDir, { output: outputPath })\n unusedAssetCount = unusedAssets.length\n if (unusedAssetCount > 0) assetsReportFile = 'assets.md'\n }\n\n if (modules.includes('health')) {\n const outputPath = path.join(reportsDir, 'health-report.md')\n await runHealthReport(rootDir, { output: outputPath })\n }\n\n if (opts.html) {\n const htmlPath = path.join(reportsDir, 'code_health.html')\n writeHtmlReport(htmlPath, rootDir, deadFilePaths, circularCount, dupeCount, unusedPkgCount)\n console.log(chalk.cyan(` HTML report written to ${htmlPath}`))\n }\n\n // ─── Summary table ────────────────────────────────────────────────────────\n\n console.log()\n console.log(chalk.bold('Summary'))\n console.log()\n\n const table = new Table({\n head: [chalk.bold('Check'), chalk.bold('Found'), chalk.bold('Output File')],\n style: { head: [], border: [] },\n })\n\n const fmt = (n: number) => (n > 0 ? chalk.yellow(String(n)) : chalk.green('0'))\n\n table.push(\n ['Dead Code (files + exports)', fmt(deadFileCount), deadReportFile || '—'],\n ['Circular Dependencies', fmt(circularCount), circularReportFile || '—'],\n ['Duplicate Clusters', fmt(dupeCount), dupesReportFile || '—'],\n ['Unused Packages', fmt(unusedPkgCount), depsReportFile || '—'],\n ['Unused Assets', fmt(unusedAssetCount), assetsReportFile || '—'],\n )\n\n console.log(table.toString())\n console.log()\n\n // ─── --delete ─────────────────────────────────────────────────────────────\n\n if (opts.delete && deadFilePaths.length > 0) {\n console.log(chalk.yellow(`Dead files (${deadFilePaths.length}):`))\n for (const f of deadFilePaths) {\n console.log(chalk.dim(` ${path.relative(rootDir, f)}`))\n }\n console.log()\n\n if (!opts.ci) {\n const confirmed = await confirmPrompt('Delete these files? (y/N) ')\n if (confirmed) {\n for (const f of deadFilePaths) {\n fs.rmSync(f, { force: true })\n }\n console.log(chalk.green(` Deleted ${deadFilePaths.length} file(s).`))\n } else {\n console.log(chalk.dim(' Skipped.'))\n }\n }\n }\n\n // ─── --ci ─────────────────────────────────────────────────────────────────\n\n if (opts.ci) {\n const hasIssues = deadFileCount > 0 || dupeCount > 0 || unusedPkgCount > 0 || circularCount > 0 || unusedAssetCount > 0\n if (hasIssues) process.exit(1)\n }\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction resolveModules(only: string | undefined): Module[] {\n if (!only) return ALL_MODULES\n const valid = new Set<Module>([...ALL_MODULES, 'health'])\n return only\n .split(',')\n .map((s) => s.trim() as Module)\n .filter((m) => valid.has(m))\n}\n\nfunction loadPackageJson(dir: string): unknown {\n try {\n return JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'))\n } catch {\n return null\n }\n}\n\nfunction confirmPrompt(question: string): Promise<boolean> {\n return new Promise((resolve) => {\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout })\n rl.question(question, (answer) => {\n rl.close()\n resolve(answer.trim().toLowerCase() === 'y')\n })\n })\n}\n\nfunction writeHtmlReport(\n outputPath: string,\n rootDir: string,\n deadFiles: string[],\n circularCount: number,\n dupeCount: number,\n unusedPkgCount: number,\n): void {\n const rows = [\n ['Dead Files / Exports', String(deadFiles.length)],\n ['Duplicate Clusters', String(dupeCount)],\n ['Circular Dependencies', String(circularCount)],\n ['Unused Packages', String(unusedPkgCount)],\n ]\n .map(([label, val]) => ` <tr><td>${label}</td><td>${val}</td></tr>`)\n .join('\\n')\n\n const deadList =\n deadFiles.length > 0\n ? `<ul>${deadFiles.map((f) => `<li>${path.relative(rootDir, f)}</li>`).join('')}</ul>`\n : '<p>None</p>'\n\n const html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>prunify — Code Health Report</title>\n <style>\n body { font-family: system-ui, sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }\n h1 { color: #0ea5e9; }\n table { border-collapse: collapse; width: 100%; margin-bottom: 2rem; }\n th, td { border: 1px solid #e2e8f0; padding: .5rem 1rem; text-align: left; }\n th { background: #f8fafc; }\n small { color: #94a3b8; }\n </style>\n</head>\n<body>\n <h1>🧹 prunify — Code Health Report</h1>\n <small>Generated ${new Date().toISOString()}</small>\n <h2>Summary</h2>\n <table>\n <tr><th>Check</th><th>Found</th></tr>\n${rows}\n </table>\n <h2>Dead Files</h2>\n ${deadList}\n</body>\n</html>`\n\n fs.mkdirSync(path.dirname(outputPath), { recursive: true })\n fs.writeFileSync(outputPath, html, 'utf-8')\n}\n\n","import fs from 'node:fs'\nimport path from 'node:path'\nimport { Project, SourceFile, Node, SyntaxKind } from 'ts-morph'\nimport { glob } from '@/utils/file.js'\n\n// ─── Interfaces ──────────────────────────────────────────────────────────────\n\nexport interface ParsedFile {\n path: string\n sourceFile: SourceFile\n exports: string[]\n imports: ImportEntry[]\n}\n\nexport interface ImportEntry {\n moduleSpecifier: string\n namedImports: string[]\n defaultImport: string | undefined\n isTypeOnly: boolean\n}\n\n// ─── Constants ────────────────────────────────────────────────────────────────\n\nconst DEFAULT_IGNORE = [\n 'node_modules',\n 'node_modules/**',\n 'dist',\n 'dist/**',\n '.next',\n '.next/**',\n 'coverage',\n 'coverage/**',\n '**/*.test.ts',\n '**/*.test.tsx',\n '**/*.test.js',\n '**/*.test.jsx',\n '**/*.spec.ts',\n '**/*.spec.tsx',\n '**/*.spec.js',\n '**/*.spec.jsx',\n '**/*.stories.ts',\n '**/*.stories.tsx',\n '**/*.d.ts',\n]\n\nconst SOURCE_PATTERNS = ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx']\n\nconst RESOLVE_EXTENSIONS = [\n '',\n '.ts',\n '.tsx',\n '.js',\n '.jsx',\n] as const\n\nconst INDEX_FILES = ['index.ts', 'index.tsx', 'index.js', 'index.jsx'] as const\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Recursively discovers all .ts/.tsx/.js/.jsx files under `rootDir`.\n *\n * Default exclusions: node_modules, dist, .next, coverage,\n * *.test.ts, *.spec.ts, *.stories.tsx, *.d.ts\n *\n * Additional glob-style patterns can be passed via `ignore`.\n */\nexport function discoverFiles(rootDir: string, ignore: string[] = []): string[] {\n return glob(rootDir, SOURCE_PATTERNS, [...DEFAULT_IGNORE, ...ignore])\n}\n\n/**\n * Creates a ts-morph Project loaded with the given source files.\n *\n * If `tsconfigPath` is not provided, the function walks up the directory tree\n * from the first file in the list to locate a `tsconfig.json`. When found, the\n * project is initialised with it so that path aliases are resolved correctly.\n */\nexport function buildProject(files: string[], tsconfigPath?: string): Project {\n const resolved = tsconfigPath ?? (files.length > 0 ? findTsconfig(files[0]) : undefined)\n\n const project = resolved\n ? new Project({ tsConfigFilePath: resolved, skipAddingFilesFromTsConfig: true })\n : new Project({\n compilerOptions: {\n allowJs: true,\n resolveJsonModule: true,\n },\n })\n\n project.addSourceFilesAtPaths(files)\n return project\n}\n\n/**\n * Returns the resolved absolute file paths of every local module imported by\n * `sourceFile`.\n *\n * Handles:\n * - Named imports: `import { X } from './foo'`\n * - Default imports: `import Foo from './foo'`\n * - Namespace imports: `import * as Foo from './foo'`\n * - Re-exports: `export { X } from './foo'`\n * - Dynamic imports: `const m = await import('./foo')`\n * - Index resolution: `import './components'` → `./components/index.ts`\n * - Path aliases: `import '@/utils/foo'` (via tsconfig paths)\n * - node_modules filtered: bare specifiers like `'chalk'` are excluded\n */\nexport function getImportsForFile(sourceFile: SourceFile): string[] {\n const result = new Set<string>()\n const fileDir = path.dirname(sourceFile.getFilePath())\n const project = sourceFile.getProject()\n const compilerOptions = project.getCompilerOptions()\n const pathAliases = (compilerOptions.paths ?? {}) as Record<string, string[]>\n const baseUrl = compilerOptions.baseUrl\n\n function addResolved(sf: SourceFile | undefined): void {\n if (!sf) return\n const p = path.normalize(sf.getFilePath())\n if (\n !p.includes(`${path.sep}node_modules${path.sep}`) &&\n !p.includes('/node_modules/') &&\n path.normalize(sourceFile.getFilePath()) !== p\n ) {\n result.add(p)\n }\n }\n\n function resolveAndAdd(specifier: string): void {\n if (!specifier) return\n const isRelative = specifier.startsWith('./') || specifier.startsWith('../')\n\n if (isRelative) {\n const p = resolveRelativePath(fileDir, specifier, project)\n if (p) result.add(p)\n } else if (!specifier.startsWith('node:')) {\n const p = resolvePathAlias(specifier, pathAliases, baseUrl, project)\n if (p) result.add(p)\n }\n }\n\n // 1. Static import declarations\n for (const decl of sourceFile.getImportDeclarations()) {\n const sf = decl.getModuleSpecifierSourceFile()\n sf ? addResolved(sf) : resolveAndAdd(decl.getModuleSpecifierValue())\n }\n\n // 2. Re-export declarations: export { X } from '...'\n for (const decl of sourceFile.getExportDeclarations()) {\n const specifier = decl.getModuleSpecifierValue()\n if (!specifier) continue\n const sf = decl.getModuleSpecifierSourceFile()\n sf ? addResolved(sf) : resolveAndAdd(specifier)\n }\n\n // 3. Dynamic imports: import('...')\n for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {\n if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue\n const args = call.getArguments()\n if (args.length > 0 && Node.isStringLiteral(args[0])) {\n resolveAndAdd(args[0].getLiteralValue())\n }\n }\n\n return [...result]\n}\n\n// ─── Legacy helpers (used by other modules) ───────────────────────────────────\n\nexport function createProject(dir: string): Project {\n const files = glob(dir, SOURCE_PATTERNS, [\n 'node_modules',\n 'node_modules/**',\n 'dist',\n 'dist/**',\n '**/*.d.ts',\n ])\n return buildProject(files)\n}\n\nexport function parseFile(sourceFile: SourceFile): ParsedFile {\n const exports: string[] = []\n const imports: ImportEntry[] = []\n\n for (const exportDecl of sourceFile.getExportDeclarations()) {\n if (exportDecl.isTypeOnly()) continue\n for (const named of exportDecl.getNamedExports()) {\n exports.push(named.getName())\n }\n }\n\n for (const exportSymbol of sourceFile.getExportedDeclarations()) {\n exports.push(exportSymbol[0])\n }\n\n for (const importDecl of sourceFile.getImportDeclarations()) {\n imports.push({\n moduleSpecifier: importDecl.getModuleSpecifierValue(),\n namedImports: importDecl.getNamedImports().map((n) => n.getName()),\n defaultImport: importDecl.getDefaultImport()?.getText(),\n isTypeOnly: importDecl.isTypeOnly(),\n })\n }\n\n return {\n path: sourceFile.getFilePath(),\n sourceFile,\n exports: [...new Set(exports)],\n imports,\n }\n}\n\nexport function parseProject(dir: string): Map<string, ParsedFile> {\n const project = createProject(dir)\n const result = new Map<string, ParsedFile>()\n for (const sourceFile of project.getSourceFiles()) {\n const parsed = parseFile(sourceFile)\n result.set(parsed.path, parsed)\n }\n return result\n}\n\nexport function isNodeReferenced(node: Node, sourceFile: SourceFile): boolean {\n if (!Node.isReferenceFindable(node)) return false\n return node\n .findReferences()\n .some((ref) =>\n ref\n .getReferences()\n .some((r) => r.getSourceFile().getFilePath() !== sourceFile.getFilePath()),\n )\n}\n\n// ─── Internal resolution helpers ─────────────────────────────────────────────\n\n/**\n * Resolves a relative specifier (e.g. `'./utils'`) from `fromDir` to an\n * absolute path that exists inside the project, trying all source extensions\n * and index file variants.\n */\nfunction resolveRelativePath(\n fromDir: string,\n specifier: string,\n project: Project,\n): string | null {\n const base = path.resolve(fromDir, specifier)\n\n for (const ext of RESOLVE_EXTENSIONS) {\n const sf = project.getSourceFile(base + ext)\n if (sf) return path.normalize(sf.getFilePath())\n }\n\n for (const index of INDEX_FILES) {\n const sf = project.getSourceFile(path.join(base, index))\n if (sf) return path.normalize(sf.getFilePath())\n }\n\n return null\n}\n\n/**\n * Resolves a path-alias specifier (e.g. `'@/utils/file'`) using the\n * `paths` map from tsconfig. Returns the absolute file path or null.\n */\nfunction resolvePathAlias(\n specifier: string,\n pathAliases: Record<string, string[]>,\n baseUrl: string | undefined,\n project: Project,\n): string | null {\n for (const [alias, targets] of Object.entries(pathAliases)) {\n const match = matchAlias(alias, specifier)\n if (!match) continue\n\n const capture = match[1] ?? ''\n\n for (const target of targets) {\n const resolved = target.replaceAll('*', capture)\n const absolute = baseUrl ? path.resolve(baseUrl, resolved) : path.resolve(resolved)\n const hit = tryResolveAbsolute(absolute, project)\n if (hit) return hit\n }\n }\n\n return null\n}\n\n/** Converts an alias pattern (e.g. `@/*`) to a RegExp and tests `specifier`. */\nfunction matchAlias(alias: string, specifier: string): RegExpExecArray | null {\n // Escape all regex metacharacters except `*`, then replace `*` with a capture group.\n const escaped = alias.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&')\n const pattern = escaped.replaceAll('*', '(.*)')\n return new RegExp(`^${pattern}$`).exec(specifier)\n}\n\n/** Tries all source extensions and index file variants for an absolute base path. */\nfunction tryResolveAbsolute(absolute: string, project: Project): string | null {\n for (const ext of RESOLVE_EXTENSIONS) {\n const sf = project.getSourceFile(absolute + ext)\n if (sf) return path.normalize(sf.getFilePath())\n }\n for (const index of INDEX_FILES) {\n const sf = project.getSourceFile(path.join(absolute, index))\n if (sf) return path.normalize(sf.getFilePath())\n }\n return null\n}\n\n/**\n * Walks up the directory tree from `fromFile` to find the nearest\n * `tsconfig.json`.\n */\nfunction findTsconfig(fromFile: string): string | undefined {\n let dir = path.dirname(fromFile)\n const root = path.parse(dir).root\n\n while (dir !== root) {\n const candidate = path.join(dir, 'tsconfig.json')\n if (fs.existsSync(candidate)) return candidate\n const parent = path.dirname(dir)\n if (parent === dir) break\n dir = parent\n }\n\n return undefined\n}\n","import fs from 'node:fs'\nimport path from 'node:path'\nimport { minimatch } from 'minimatch'\n\n/**\n * Recursively collects files under `dir` that match any of `patterns`\n * and do not match any of `ignore` patterns.\n */\nexport function glob(\n dir: string,\n patterns: string[],\n ignore: string[] = [],\n): string[] {\n const results: string[] = []\n collect(dir, dir, patterns, ignore, results)\n return results\n}\n\nfunction collect(\n base: string,\n current: string,\n patterns: string[],\n ignore: string[],\n results: string[],\n): void {\n let entries: fs.Dirent[]\n\n try {\n entries = fs.readdirSync(current, { withFileTypes: true })\n } catch {\n return\n }\n\n for (const entry of entries) {\n const fullPath = path.join(current, entry.name)\n const relativePath = path.relative(base, fullPath).replace(/\\\\/g, '/')\n\n if (entry.isDirectory()) {\n // For directories: exact matching only — `partial: true` would cause\n // patterns like `**/*.test.ts` to spuriously match directory names.\n const isIgnored = ignore.some((pattern) => minimatch(relativePath, pattern))\n if (!isIgnored) collect(base, fullPath, patterns, ignore, results)\n } else if (entry.isFile()) {\n const isIgnored = ignore.some((pattern) => minimatch(relativePath, pattern))\n if (!isIgnored) {\n const matches = patterns.some((pattern) => minimatch(relativePath, pattern))\n if (matches) results.push(fullPath)\n }\n }\n }\n}\n\n/**\n * Ensures a directory exists, creating it (and parents) if needed.\n */\nexport function ensureDir(dirPath: string): void {\n if (!fs.existsSync(dirPath)) {\n fs.mkdirSync(dirPath, { recursive: true })\n }\n}\n\n/**\n * Reads a file as UTF-8 text, returning `null` if it does not exist.\n */\nexport function readFileSafe(filePath: string): string | null {\n try {\n return fs.readFileSync(filePath, 'utf-8')\n } catch {\n return null\n }\n}\n","import fs from 'node:fs'\nimport path from 'node:path'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\n/**\n * Directed adjacency list: file path → set of file paths it imports.\n */\nexport type ImportGraph = Map<string, Set<string>>\n\nexport interface GraphAnalysis {\n deadFiles: string[]\n liveFiles: string[]\n cycles: string[][]\n}\n\n// ─── Core functions ───────────────────────────────────────────────────────────\n\n/**\n * Builds a directed `ImportGraph` from a list of file paths.\n *\n * @param files - All files that should be nodes in the graph.\n * @param getImports - Returns the resolved absolute import paths for a file.\n */\nexport function buildGraph(\n files: string[],\n getImports: (file: string) => string[],\n): ImportGraph {\n const graph: ImportGraph = new Map()\n\n for (const file of files) {\n graph.set(file, new Set())\n }\n\n for (const file of files) {\n for (const imported of getImports(file)) {\n graph.get(file)?.add(imported)\n // Ensure imported node exists even if it wasn't in the initial list\n if (!graph.has(imported)) graph.set(imported, new Set())\n }\n }\n\n return graph\n}\n\n/**\n * Resolves the entry points for a project.\n *\n * Resolution order:\n * 1. Next.js: all files inside `pages/` and `app/` directories.\n * 2. File-based routing: src/pages/, src/routes/, src/views/ (Vite, React Router, etc.)\n * 3. package.json `\"main\"` and `\"module\"` fields.\n * 4. Common fallbacks: src/main, src/index, src/App, index (all extensions).\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function findEntryPoints(rootDir: string, packageJson: any): string[] {\n const entries = [\n ...resolveNextJsEntries(rootDir),\n ...resolveFileBrowserEntries(rootDir),\n ...resolvePkgFieldEntries(rootDir, packageJson),\n ...resolveFallbackEntries(rootDir),\n ]\n\n return [...new Set(entries)]\n}\n\n/**\n * Returns files in the graph that are not imported by any other file —\n * i.e. natural roots of the dependency tree.\n *\n * If every file is imported by at least one other (e.g. a full circular graph),\n * returns all files so nothing is incorrectly flagged as dead.\n */\nexport function findRootFiles(graph: ImportGraph): string[] {\n const imported = new Set<string>()\n for (const deps of graph.values()) {\n for (const dep of deps) imported.add(dep)\n }\n const roots = [...graph.keys()].filter((f) => !imported.has(f))\n return roots.length > 0 ? roots : [...graph.keys()]\n}\n\n/**\n * Iterative DFS from all `entryPoints`, returning every reachable file.\n * Safe against circular references.\n */\nexport function runDFS(graph: ImportGraph, entryPoints: string[]): Set<string> {\n const visited = new Set<string>()\n const stack = [...entryPoints]\n let node: string | undefined\n\n while ((node = stack.pop()) !== undefined) {\n if (visited.has(node)) continue\n visited.add(node)\n for (const neighbor of graph.get(node) ?? []) {\n if (!visited.has(neighbor)) stack.push(neighbor)\n }\n }\n\n return visited\n}\n\n/**\n * For each dead file, finds its full dead chain: files that are transitively\n * imported only by other dead files (i.e. removing the dead root would also\n * make them unreachable from any live entry point).\n *\n * Returns `Map<deadFile, chainMembers[]>`.\n */\nexport function findDeadChains(\n graph: ImportGraph,\n deadFiles: Set<string>,\n): Map<string, string[]> {\n const reverseGraph = buildReverseGraph(graph)\n const result = new Map<string, string[]>()\n\n for (const deadRoot of deadFiles) {\n result.set(deadRoot, dfsDeadChain(deadRoot, graph, deadFiles, reverseGraph))\n }\n\n return result\n}\n\n/**\n * Detects all circular dependency chains using iterative DFS with an explicit\n * path stack. Duplicate cycles (the same cycle starting at a different node)\n * are deduplicated by normalising to the lexicographically smallest rotation.\n */\nexport function detectCycles(graph: ImportGraph): string[][] {\n const cycles: string[][] = []\n const seenKeys = new Set<string>()\n const visited = new Set<string>()\n const inStack = new Set<string>()\n const path: string[] = []\n\n const acc: CycleAccumulator = { seenKeys, cycles }\n\n for (const start of graph.keys()) {\n if (!visited.has(start)) {\n dfsForCycles(start, graph, visited, inStack, path, acc)\n }\n }\n\n return cycles\n}\n\n// ─── Legacy aliases (used by existing modules) ────────────────────────────────\n\n/** @deprecated Use `runDFS` instead. */\nexport function dfs(start: string, graph: ImportGraph): Set<string> {\n return runDFS(graph, [start])\n}\n\n/** @deprecated Use `detectCycles` instead. */\nexport function findCircularGroups(graph: ImportGraph): string[][] {\n return detectCycles(graph)\n}\n\n// ─── Entry-point resolution helpers ──────────────────────────────────────────\n\nfunction resolveNextJsEntries(rootDir: string): string[] {\n const isNext =\n fs.existsSync(path.join(rootDir, 'next.config.js')) ||\n fs.existsSync(path.join(rootDir, 'next.config.ts')) ||\n fs.existsSync(path.join(rootDir, 'next.config.mjs'))\n\n if (!isNext) return []\n\n const entries: string[] = []\n for (const dir of ['pages', 'app', 'src/pages', 'src/app'] as const) {\n const dirPath = path.join(rootDir, dir)\n if (fs.existsSync(dirPath)) entries.push(...collectSourceFiles(dirPath))\n }\n return entries\n}\n\n/**\n * Treats files inside common page/route directories as entry points.\n * Covers Vite + React Router, TanStack Router, Expo, and similar setups\n * where pages live in src/pages/, src/routes/, or src/views/.\n */\nfunction resolveFileBrowserEntries(rootDir: string): string[] {\n const PAGE_DIRS = ['src/pages', 'src/routes', 'src/views', 'src/screens']\n const entries: string[] = []\n for (const rel of PAGE_DIRS) {\n const dirPath = path.join(rootDir, rel)\n if (fs.existsSync(dirPath)) entries.push(...collectSourceFiles(dirPath))\n }\n return entries\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction resolvePkgFieldEntries(rootDir: string, packageJson: any): string[] {\n const entries: string[] = []\n for (const field of ['main', 'module'] as const) {\n const value = packageJson?.[field]\n if (typeof value !== 'string') continue\n const abs = path.resolve(rootDir, value)\n if (fs.existsSync(abs)) entries.push(abs)\n }\n return entries\n}\n\nfunction resolveFallbackEntries(rootDir: string): string[] {\n const candidates = [\n 'src/main.ts', 'src/main.tsx', 'src/main.js', 'src/main.jsx',\n 'src/index.ts', 'src/index.tsx', 'src/index.js', 'src/index.jsx',\n 'src/App.ts', 'src/App.tsx', 'src/App.js', 'src/App.jsx',\n 'index.ts', 'index.tsx', 'index.js', 'index.jsx',\n ]\n return candidates\n .map((rel) => path.join(rootDir, rel))\n .filter((abs) => fs.existsSync(abs))\n}\n\n// ─── Cycle detection helpers ─────────────────────────────────────────────────\n\ntype CycleFrame = { node: string; neighbors: Iterator<string>; entered: boolean }\ntype CycleAccumulator = { seenKeys: Set<string>; cycles: string[][] }\n\nfunction mkFrame(node: string, graph: ImportGraph): CycleFrame {\n return { node, neighbors: (graph.get(node) ?? new Set()).values(), entered: false }\n}\n\nfunction dfsForCycles(\n start: string,\n graph: ImportGraph,\n visited: Set<string>,\n inStack: Set<string>,\n path: string[],\n acc: CycleAccumulator,\n): void {\n const stack: CycleFrame[] = [mkFrame(start, graph)]\n\n while (stack.length > 0) {\n const frame = stack.at(-1)\n if (!frame) break\n\n if (!frame.entered) {\n if (visited.has(frame.node)) { stack.pop(); continue }\n frame.entered = true\n inStack.add(frame.node)\n path.push(frame.node)\n }\n\n const { done, value: neighbor } = frame.neighbors.next()\n\n if (done) {\n stack.pop()\n path.pop()\n inStack.delete(frame.node)\n visited.add(frame.node)\n } else {\n handleCycleNeighbor(neighbor, stack, path, inStack, visited, acc, graph)\n }\n }\n}\n\nfunction handleCycleNeighbor(\n neighbor: string,\n stack: CycleFrame[],\n path: string[],\n inStack: Set<string>,\n visited: Set<string>,\n acc: CycleAccumulator,\n graph: ImportGraph,\n): void {\n if (inStack.has(neighbor)) {\n recordCycle(neighbor, path, acc)\n } else if (!visited.has(neighbor)) {\n stack.push(mkFrame(neighbor, graph))\n }\n}\n\nfunction recordCycle(\n cycleStart: string,\n path: string[],\n acc: CycleAccumulator,\n): void {\n const idx = path.indexOf(cycleStart)\n if (idx === -1) return\n const cycle = normalizeCycle(path.slice(idx))\n const key = cycle.join('\\0')\n if (!acc.seenKeys.has(key)) {\n acc.seenKeys.add(key)\n acc.cycles.push(cycle)\n }\n}\n\n// ─── Dead-chain helpers ───────────────────────────────────────────────────────\n\nfunction dfsDeadChain(\n deadRoot: string,\n graph: ImportGraph,\n deadFiles: Set<string>,\n reverseGraph: Map<string, Set<string>>,\n): string[] {\n const chain: string[] = []\n const visited = new Set<string>()\n const stack = [...(graph.get(deadRoot) ?? [])]\n let node: string | undefined\n\n while ((node = stack.pop()) !== undefined) {\n if (visited.has(node) || node === deadRoot) continue\n visited.add(node)\n\n if (deadFiles.has(node) || isOnlyImportedByDead(node, deadFiles, reverseGraph)) {\n chain.push(node)\n for (const next of graph.get(node) ?? []) {\n if (!visited.has(next)) stack.push(next)\n }\n }\n }\n\n return chain\n}\n\nfunction isOnlyImportedByDead(\n file: string,\n deadFiles: Set<string>,\n reverseGraph: Map<string, Set<string>>,\n): boolean {\n const importers = reverseGraph.get(file) ?? new Set<string>()\n return importers.size === 0 || [...importers].every((imp) => deadFiles.has(imp))\n}\n\n// ─── General helpers ──────────────────────────────────────────────────────────\n\n/** Reverses all edges in the graph: for each edge A→B, creates B→A. */\nexport function buildReverseGraph(graph: ImportGraph): Map<string, Set<string>> {\n const rev = new Map<string, Set<string>>()\n\n for (const [file] of graph) {\n if (!rev.has(file)) rev.set(file, new Set())\n }\n\n for (const [file, imports] of graph) {\n for (const imp of imports) {\n if (!rev.has(imp)) rev.set(imp, new Set())\n rev.get(imp)?.add(file)\n }\n }\n\n return rev\n}\n\n/**\n * Rotates `cycle` so it starts at the lexicographically smallest element,\n * producing a canonical form for deduplication.\n */\nfunction normalizeCycle(cycle: string[]): string[] {\n if (cycle.length === 0) return cycle\n const minIdx = cycle.reduce(\n (best, cur, i) => (cur < cycle[best] ? i : best),\n 0,\n )\n return [...cycle.slice(minIdx), ...cycle.slice(0, minIdx)]\n}\n\n/** Collects all .ts/.tsx/.js/.jsx files under a directory recursively. */\nfunction collectSourceFiles(dir: string): string[] {\n const results: string[] = []\n const SOURCE_RE = /\\.(tsx?|jsx?)$/\n\n function walk(current: string): void {\n let entries: fs.Dirent[]\n try {\n entries = fs.readdirSync(current, { withFileTypes: true })\n } catch {\n return\n }\n for (const entry of entries) {\n const full = path.join(current, entry.name)\n if (entry.isDirectory()) {\n walk(full)\n } else if (entry.isFile() && SOURCE_RE.test(entry.name)) {\n results.push(full)\n }\n }\n }\n\n walk(dir)\n return results\n}\n","import ora from 'ora'\nimport chalk from 'chalk'\nimport Table from 'cli-table3'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { Project, Node } from 'ts-morph'\nimport { discoverFiles, buildProject, getImportsForFile } from '@/core/parser.js'\nimport {\n buildGraph,\n findEntryPoints,\n buildReverseGraph,\n type ImportGraph,\n} from '@/core/graph.js'\nimport { writeJson, writeMarkdown } from '@/core/reporter.js'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface DeadExport {\n filePath: string\n exportName: string\n line: number\n}\n\nexport interface DeadCodeResult {\n safeToDelete: string[]\n transitivelyDead: string[]\n deadFiles: string[]\n liveFiles: Set<string>\n chains: Map<string, string[]>\n deadExports: DeadExport[]\n report: string\n}\n\nexport interface DeadCodeOptions {\n output?: string\n json?: boolean\n}\n\n// ─── Framework / config file patterns ─────────────────────────────────────────\n\nconst FRAMEWORK_FILE_PATTERNS = [\n /^next\\.config\\.(js|ts|mjs|cjs)$/,\n /^middleware\\.(ts|js)$/,\n /^instrumentation\\.(ts|js)$/,\n /^tailwind\\.config\\.(js|ts|mjs|cjs)$/,\n /^postcss\\.config\\.(js|ts|mjs|cjs)$/,\n /^jest\\.config\\.(js|ts|mjs|cjs)$/,\n /^vitest\\.config\\.(js|ts|mjs|cjs)$/,\n /^vite\\.config\\.(js|ts|mjs|cjs)$/,\n /^webpack\\.config\\.(js|ts|mjs|cjs)$/,\n /^babel\\.config\\.(js|ts|cjs|mjs|json)$/,\n /^\\.babelrc\\.(js|cjs)$/,\n /^\\.eslintrc\\.(js|cjs)$/,\n /^eslint\\.config\\.(js|ts|mjs|cjs)$/,\n /^prettier\\.config\\.(js|ts|mjs|cjs)$/,\n /^tsup\\.config\\.(ts|js)$/,\n /^rollup\\.config\\.(js|ts|mjs)$/,\n /^esbuild\\.config\\.(js|ts|mjs)$/,\n /^commitlint\\.config\\.(js|ts)$/,\n /^lint-staged\\.config\\.(js|ts|mjs|cjs)$/,\n /^sentry\\.(client|server|edge)\\.config\\.(ts|js)$/,\n]\n\nfunction isFrameworkFile(filePath: string, rootDir: string): boolean {\n const rel = path.relative(rootDir, filePath)\n const basename = path.basename(rel)\n return FRAMEWORK_FILE_PATTERNS.some((re) => re.test(basename))\n}\n\n// ─── Public module API ────────────────────────────────────────────────────────\n\n/**\n * Orchestrates the full dead-code analysis pipeline.\n *\n * Uses a reverse-graph approach: a file is \"safe to delete\" only when NO\n * other file in the codebase imports it AND it is not a known entry point\n * or framework configuration file.\n *\n * After identifying root orphans, iteratively finds files whose EVERY\n * importer is already dead (\"transitively dead\").\n */\nexport function runDeadCodeModule(\n project: Project,\n graph: ImportGraph,\n entryPoints: string[],\n rootDir: string,\n): DeadCodeResult {\n const allFiles = [...graph.keys()]\n const entrySet = new Set(entryPoints)\n const reverseGraph = buildReverseGraph(graph)\n\n const excludedFiles = new Set<string>()\n for (const file of allFiles) {\n if (isFrameworkFile(file, rootDir)) excludedFiles.add(file)\n }\n\n // 1. Safe to Delete: files with zero importers, not entry points, not config\n const safeToDelete = allFiles.filter((f) => {\n if (entrySet.has(f) || excludedFiles.has(f)) return false\n const importers = reverseGraph.get(f)\n return !importers || importers.size === 0\n })\n\n // 2. Transitively dead: files whose ALL importers are already dead\n const deadSet = new Set(safeToDelete)\n let changed = true\n while (changed) {\n changed = false\n for (const file of allFiles) {\n if (deadSet.has(file) || entrySet.has(file) || excludedFiles.has(file)) continue\n const importers = reverseGraph.get(file)\n if (!importers || importers.size === 0) continue\n if ([...importers].every((imp) => deadSet.has(imp))) {\n deadSet.add(file)\n changed = true\n }\n }\n }\n\n const transitivelyDead = [...deadSet].filter((f) => !safeToDelete.includes(f))\n\n // Build chains: for each safe-to-delete root, list transitively dead dependants\n const chains = new Map<string, string[]>()\n for (const root of safeToDelete) {\n const chain = collectTransitiveChain(root, graph, deadSet)\n chains.set(root, chain)\n }\n\n // Dead exports from live files\n const liveFiles = new Set(allFiles.filter((f) => !deadSet.has(f)))\n const deadExports = findDeadExports(project, liveFiles)\n\n const report = buildDeadCodeReport(\n safeToDelete,\n transitivelyDead,\n chains,\n deadExports,\n rootDir,\n )\n\n return {\n safeToDelete,\n transitivelyDead,\n deadFiles: [...deadSet],\n liveFiles,\n chains,\n deadExports,\n report,\n }\n}\n\n/**\n * Walks forward from a dead root along its imports, collecting only files\n * that are also dead (i.e. would become orphaned if the root is deleted).\n */\nfunction collectTransitiveChain(\n root: string,\n graph: ImportGraph,\n deadSet: Set<string>,\n): string[] {\n const chain: string[] = []\n const visited = new Set<string>()\n const stack = [...(graph.get(root) ?? [])]\n\n let node: string | undefined\n while ((node = stack.pop()) !== undefined) {\n if (visited.has(node) || node === root) continue\n visited.add(node)\n\n if (deadSet.has(node)) {\n chain.push(node)\n for (const next of graph.get(node) ?? []) {\n if (!visited.has(next)) stack.push(next)\n }\n }\n }\n\n return chain\n}\n\n// ─── Dead exports ─────────────────────────────────────────────────────────────\n\n/**\n * For every live file, finds named exports that are never used (imported by\n * name) by any other live file in the project.\n *\n * Handles: named exports, re-exports, default exports.\n * Skips exports that are pulled in via a namespace import (`import * as X`).\n */\nexport function findDeadExports(\n project: Project,\n liveFiles: Set<string>,\n): DeadExport[] {\n const importedNames = buildImportedNameMap(project, liveFiles)\n const dead: DeadExport[] = []\n\n for (const filePath of liveFiles) {\n collectFileDeadExports(filePath, project, importedNames, dead)\n }\n\n return dead\n}\n\nfunction collectFileDeadExports(\n filePath: string,\n project: Project,\n importedNames: Map<string, Set<string>>,\n dead: DeadExport[],\n): void {\n const sf = project.getSourceFile(filePath)\n if (!sf) return\n\n const usedNames = importedNames.get(filePath) ?? new Set<string>()\n if (usedNames.has('*')) return\n\n for (const [exportName, declarations] of sf.getExportedDeclarations()) {\n if (usedNames.has(exportName)) continue\n const line = getExportLine(declarations[0])\n dead.push({ filePath, exportName, line })\n }\n}\n\nfunction getExportLine(decl: import('ts-morph').ExportedDeclarations): number {\n if (!decl) return 0\n if (!Node.isNode(decl)) return 0\n return decl.getStartLineNumber()\n}\n\n/**\n * Returns the size of `filePath` in bytes, or 0 if the file cannot be read.\n */\nexport function getFileSize(filePath: string): number {\n try {\n return fs.statSync(filePath).size\n } catch {\n return 0\n }\n}\n\n// ─── Report builder ───────────────────────────────────────────────────────────\n\nexport function buildDeadCodeReport(\n safeToDelete: string[],\n transitivelyDead: string[],\n chains: Map<string, string[]>,\n deadExports: DeadExport[],\n rootDir: string,\n): string {\n const rel = (p: string) => path.relative(rootDir, p).replaceAll('\\\\', '/')\n\n const allDeadFiles = [...safeToDelete, ...transitivelyDead]\n const totalBytes = allDeadFiles.reduce((sum, f) => sum + getFileSize(f), 0)\n const totalKb = (totalBytes / 1024).toFixed(1)\n\n const lines: string[] = [\n '========================================',\n ' DEAD CODE REPORT',\n ` Safe to delete : ${safeToDelete.length}`,\n ` Transitively dead : ${transitivelyDead.length}`,\n ` Dead exports : ${deadExports.length}`,\n ` Recoverable : ~${totalKb} KB`,\n '========================================',\n '',\n ]\n\n // ── Safe to Delete ──\n if (safeToDelete.length > 0) {\n lines.push(\n '── SAFE TO DELETE ──',\n '(These files are not imported by any other file in the codebase)',\n '',\n )\n for (const filePath of safeToDelete) {\n const chain = chains.get(filePath) ?? []\n const sizeKb = (getFileSize(filePath) / 1024).toFixed(1)\n\n lines.push(` ${rel(filePath)} (~${sizeKb} KB)`)\n\n if (chain.length > 0) {\n lines.push(` └─ also makes dead: ${chain.map(rel).join(', ')}`)\n }\n }\n lines.push('')\n }\n\n // ── Transitively Dead ──\n if (transitivelyDead.length > 0) {\n lines.push(\n '── TRANSITIVELY DEAD ──',\n '(These files are only imported by dead files — they become orphaned too)',\n '',\n )\n for (const filePath of transitivelyDead) {\n const sizeKb = (getFileSize(filePath) / 1024).toFixed(1)\n lines.push(` ${rel(filePath)} (~${sizeKb} KB)`)\n }\n lines.push('')\n }\n\n // ── Dead Exports ──\n if (deadExports.length > 0) {\n lines.push(\n '── DEAD EXPORTS ──',\n '(Exported but never imported by any other file)',\n '',\n )\n for (const entry of deadExports) {\n lines.push(\n ` ${rel(entry.filePath)} → ${entry.exportName} [line ${entry.line}]`,\n )\n }\n lines.push('')\n }\n\n return lines.join('\\n')\n}\n\n// ─── CLI command (backward-compatible) ───────────────────────────────────────\n\ninterface DeadExportRow {\n file: string\n exportName: string\n}\n\nexport async function runDeadCode(dir: string, opts: DeadCodeOptions): Promise<DeadExportRow[]> {\n const spinner = ora(chalk.cyan('Scanning for dead code…')).start()\n\n try {\n const fileList = discoverFiles(dir, [])\n const project = buildProject(fileList)\n const graph = buildGraph(fileList, (f) => {\n const sf = project.getSourceFile(f)\n return sf ? getImportsForFile(sf) : []\n })\n\n const packageJson = loadPackageJson(dir)\n const entries = findEntryPoints(dir, packageJson)\n\n const result = runDeadCodeModule(project, graph, entries, dir)\n\n const dead: DeadExportRow[] = [\n ...result.safeToDelete.map((f) => ({ file: f, exportName: '(entire file — safe to delete)' })),\n ...result.transitivelyDead.map((f) => ({ file: f, exportName: '(entire file — transitively dead)' })),\n ...result.deadExports.map((e) => ({ file: e.filePath, exportName: e.exportName })),\n ]\n\n spinner.succeed(chalk.green(`Dead code scan complete — ${dead.length} item(s) found`))\n\n if (dead.length === 0) {\n console.log(chalk.green(' No dead code detected.'))\n return dead\n }\n\n printDeadTable(dead)\n writeDeadOutput(result, opts)\n\n return dead\n } catch (err) {\n spinner.fail(chalk.red('Dead code scan failed'))\n throw err\n }\n}\n\n// ─── Internal helpers ─────────────────────────────────────────────────────────\n\nfunction buildImportedNameMap(\n project: Project,\n liveFiles: Set<string>,\n): Map<string, Set<string>> {\n const importedNames = new Map<string, Set<string>>()\n\n const touch = (file: string, name: string) => {\n if (!importedNames.has(file)) importedNames.set(file, new Set())\n importedNames.get(file)!.add(name)\n }\n\n for (const filePath of liveFiles) {\n const sf = project.getSourceFile(filePath)\n if (!sf) continue\n processImports(sf, touch)\n processReExports(sf, touch)\n }\n\n return importedNames\n}\n\nfunction processImports(\n sf: ReturnType<Project['getSourceFiles']>[number],\n touch: (file: string, name: string) => void,\n): void {\n for (const decl of sf.getImportDeclarations()) {\n const resolved = decl.getModuleSpecifierSourceFile()\n if (!resolved) continue\n const target = resolved.getFilePath()\n\n if (decl.getNamespaceImport()) {\n touch(target, '*')\n continue\n }\n\n const defaultImport = decl.getDefaultImport()\n if (defaultImport) touch(target, 'default')\n\n for (const named of decl.getNamedImports()) {\n touch(target, named.getAliasNode()?.getText() ?? named.getName())\n }\n }\n}\n\nfunction processReExports(\n sf: ReturnType<Project['getSourceFiles']>[number],\n touch: (file: string, name: string) => void,\n): void {\n for (const decl of sf.getExportDeclarations()) {\n const resolved = decl.getModuleSpecifierSourceFile()\n if (!resolved) continue\n const target = resolved.getFilePath()\n\n if (decl.isNamespaceExport()) {\n touch(target, '*')\n continue\n }\n\n for (const named of decl.getNamedExports()) {\n touch(target, named.getName())\n }\n }\n}\n\nfunction loadPackageJson(dir: string): unknown {\n const pkgPath = path.join(dir, 'package.json')\n if (!fs.existsSync(pkgPath)) return null\n return JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))\n}\n\nfunction printDeadTable(dead: Array<{ file: string; exportName: string }>): void {\n const table = new Table({ head: ['File', 'Export'] })\n for (const row of dead) {\n table.push([row.file, row.exportName])\n }\n console.log(table.toString())\n}\n\nfunction writeDeadOutput(result: DeadCodeResult, opts: DeadCodeOptions): void {\n if (!opts.output) return\n\n if (opts.json) {\n writeJson(\n {\n safeToDelete: result.safeToDelete,\n transitivelyDead: result.transitivelyDead,\n deadExports: result.deadExports,\n },\n opts.output,\n )\n console.log(chalk.cyan(` JSON written to ${opts.output}`))\n return\n }\n\n writeMarkdown(\n {\n title: 'Dead Code Report',\n summary: `${result.safeToDelete.length} safe to delete, ${result.transitivelyDead.length} transitively dead, ${result.deadExports.length} dead export(s)`,\n sections: [\n {\n title: 'Safe to Delete',\n headers: ['File', 'Chain'],\n rows: result.safeToDelete.map((f) => [\n f,\n (result.chains.get(f) ?? []).join(' → ') || '—',\n ]),\n },\n {\n title: 'Transitively Dead',\n headers: ['File'],\n rows: result.transitivelyDead.map((f) => [f]),\n },\n {\n title: 'Dead Exports',\n headers: ['File', 'Export', 'Line'],\n rows: result.deadExports.map((e) => [e.filePath, e.exportName, String(e.line)]),\n },\n ],\n generatedAt: new Date(),\n },\n opts.output,\n )\n console.log(chalk.cyan(` Report written to ${opts.output}`))\n}\n","import fs from 'node:fs'\nimport path from 'node:path'\nimport ora, { type Ora } from 'ora'\n\n// ─── Spinner ──────────────────────────────────────────────────────────────────\n\nexport type Spinner = Ora\n\n/**\n * Creates an ora spinner and starts it immediately.\n */\nexport function createSpinner(text: string): Spinner {\n return ora(text).start()\n}\n\n// ─── Reports dir ─────────────────────────────────────────────────────────────\n\nconst REPORTS_DIR_NAME = 'prunify-reports'\n\n/**\n * Creates (if needed) a `prunify-reports/` folder inside `outDir ?? rootDir`\n * and returns its absolute path.\n */\nexport function ensureReportsDir(rootDir: string, outDir?: string): string {\n const base = outDir ? path.resolve(outDir) : path.resolve(rootDir)\n const reportsDir = path.join(base, REPORTS_DIR_NAME)\n if (!fs.existsSync(reportsDir)) {\n fs.mkdirSync(reportsDir, { recursive: true })\n }\n return reportsDir\n}\n\n/**\n * Writes `content` to `reportsDir/filename` and logs the path.\n */\nexport function writeReport(reportsDir: string, filename: string, content: string): void {\n ensureDir(reportsDir)\n const filePath = path.join(reportsDir, filename)\n fs.writeFileSync(filePath, content, 'utf-8')\n console.log(` Report saved → ${filePath}`)\n}\n\n/**\n * Appends `prunify-reports/` to `.gitignore` if not already present.\n */\nexport function appendToGitignore(rootDir: string): void {\n const gitignorePath = path.join(rootDir, '.gitignore')\n const entry = `${REPORTS_DIR_NAME}/`\n\n if (fs.existsSync(gitignorePath)) {\n const contents = fs.readFileSync(gitignorePath, 'utf-8')\n if (contents.split('\\n').some((line) => line.trim() === entry)) return\n fs.appendFileSync(gitignorePath, `\\n${entry}\\n`, 'utf-8')\n } else {\n fs.writeFileSync(gitignorePath, `${entry}\\n`, 'utf-8')\n }\n}\n\n/**\n * Converts a byte count to a human-readable string: \"4.2 KB\", \"1.1 MB\", etc.\n */\nexport function formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`\n if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`\n}\n\n// ─── Markdown / JSON writers ──────────────────────────────────────────────────\n\nexport interface ReportSection {\n title: string\n rows: string[][]\n headers?: string[]\n}\n\nexport interface Report {\n title: string\n summary: string\n sections: ReportSection[]\n generatedAt: Date\n}\n\n/**\n * Writes a Markdown report to `outputPath`.\n */\nexport function writeMarkdown(report: Report, outputPath: string): void {\n const lines: string[] = [\n `# ${report.title}`,\n '',\n `> ${report.summary}`,\n '',\n `_Generated: ${report.generatedAt.toISOString()}_`,\n '',\n ]\n\n for (const section of report.sections) {\n lines.push(`## ${section.title}`, '')\n\n if (section.rows.length === 0) {\n lines.push('_Nothing found._', '')\n continue\n }\n\n if (section.headers && section.headers.length > 0) {\n lines.push(`| ${section.headers.join(' | ')} |`)\n lines.push(`| ${section.headers.map(() => '---').join(' | ')} |`)\n }\n\n for (const row of section.rows) {\n lines.push(`| ${row.join(' | ')} |`)\n }\n\n lines.push('')\n }\n\n ensureDir(path.dirname(outputPath))\n fs.writeFileSync(outputPath, lines.join('\\n'), 'utf-8')\n}\n\n/**\n * Writes a JSON report to `outputPath`.\n */\nexport function writeJson(data: unknown, outputPath: string): void {\n ensureDir(path.dirname(outputPath))\n fs.writeFileSync(outputPath, JSON.stringify(data, null, 2), 'utf-8')\n}\n\nfunction ensureDir(dir: string): void {\n if (dir && !fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true })\n }\n}\n","import ora from 'ora'\nimport chalk from 'chalk'\nimport Table from 'cli-table3'\nimport fs from 'node:fs'\nimport { Project, SyntaxKind, VariableDeclarationKind } from 'ts-morph'\nimport { glob } from '@/utils/file.js'\nimport { hashFunctionBody, isExported } from '@/utils/ast.js'\nimport { writeMarkdown } from '@/core/reporter.js'\nimport { dupeClusterPrompt } from '@/prompt-gen/templates.js'\n\nexport interface DupeFinderOptions {\n output?: string\n minLines?: string\n}\n\nexport interface DuplicateBlock {\n hash: string\n lines: number\n occurrences: Array<{ file: string; startLine: number }>\n}\n\n// Minimum number of meaningful (non-whitespace) characters a block must have\n// to be worth reporting. Filters out blocks that are all braces/imports/short lines.\nconst MIN_BLOCK_CHARS = 120\n\n/**\n * Detects duplicate code blocks across files using a rolling line-hash approach.\n */\nexport async function runDupeFinder(dir: string, opts: DupeFinderOptions): Promise<DuplicateBlock[]> {\n const minLines = parseInt(opts.minLines ?? '15', 10)\n const spinner = ora(chalk.cyan(`Scanning for duplicate blocks (≥${minLines} lines)…`)).start()\n\n try {\n const files = glob(dir, ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], [\n 'node_modules', 'dist', '.next', 'coverage',\n '**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx',\n '**/*.d.ts',\n ])\n const blockMap = new Map<string, Array<{ file: string; startLine: number }>>()\n\n for (const filePath of files) {\n const content = fs.readFileSync(filePath, 'utf-8')\n const lines = content.split('\\n').map((l) => l.trim()).filter(Boolean)\n\n for (let i = 0; i <= lines.length - minLines; i++) {\n const block = lines.slice(i, i + minLines).join('\\n')\n // Skip blocks that are mostly boilerplate / too short to be meaningful\n const meaningfulChars = block.replace(/[\\s{}();,]/g, '').length\n if (meaningfulChars < MIN_BLOCK_CHARS) continue\n if (!blockMap.has(block)) blockMap.set(block, [])\n blockMap.get(block)!.push({ file: filePath, startLine: i + 1 })\n }\n }\n\n const dupes: DuplicateBlock[] = []\n\n for (const [block, occurrences] of blockMap) {\n if (occurrences.length > 1) {\n dupes.push({\n hash: hashString(block),\n lines: minLines,\n occurrences,\n })\n }\n }\n\n // Sort by most occurrences first, cap at 200 to keep reports readable\n dupes.sort((a, b) => b.occurrences.length - a.occurrences.length)\n if (dupes.length > 200) dupes.splice(200)\n\n spinner.succeed(chalk.green(`Duplicate scan complete — ${dupes.length} duplicate block(s) found`))\n\n if (dupes.length === 0) {\n console.log(chalk.green(' No duplicate blocks detected.'))\n return dupes\n }\n\n const table = new Table({ head: ['Hash', 'Lines', 'Count', 'First Occurrence'] })\n for (const d of dupes) {\n table.push([\n chalk.gray(d.hash.slice(0, 8)),\n String(d.lines),\n chalk.yellow(String(d.occurrences.length)),\n `${d.occurrences[0].file}:${d.occurrences[0].startLine}`,\n ])\n }\n console.log(table.toString())\n\n if (opts.output) {\n const rows = dupes.flatMap((d) =>\n d.occurrences.map((o) => [d.hash.slice(0, 8), String(d.lines), o.file, String(o.startLine)]),\n )\n writeMarkdown(\n {\n title: 'Duplicate Code Report',\n summary: `${dupes.length} duplicate block(s) found (min lines: ${minLines})`,\n sections: [{ title: 'Duplicates', headers: ['Hash', 'Lines', 'File', 'Start Line'], rows }],\n generatedAt: new Date(),\n },\n opts.output,\n )\n console.log(chalk.cyan(` Report written to ${opts.output}`))\n }\n\n return dupes\n } catch (err) {\n spinner.fail(chalk.red('Duplicate scan failed'))\n throw err\n }\n}\n\nfunction hashString(str: string): string {\n let hash = 5381\n for (let i = 0; i < str.length; i++) {\n hash = ((hash << 5) + hash) ^ str.charCodeAt(i)\n }\n return (hash >>> 0).toString(16).padStart(8, '0')\n}\n\n// ─── AST-based duplicate detection ───────────────────────────────────────────\n\nexport interface FunctionRecord {\n name: string\n filePath: string\n line: number\n bodyHash: string\n paramCount: number\n}\n\nexport interface DupeCluster {\n type: 'name' | 'structure'\n functions: FunctionRecord[]\n}\n\nexport interface ConstDupe {\n name: string\n value: string\n occurrences: Array<{ filePath: string; line: number }>\n}\n\n/**\n * Extracts named function declarations and exported const arrow functions from\n * all live files in the project, computing an AST body hash for each.\n */\nexport function extractFunctions(project: Project, liveFiles: Set<string>): FunctionRecord[] {\n const records: FunctionRecord[] = []\n\n for (const filePath of liveFiles) {\n const sf = project.getSourceFile(filePath)\n if (!sf) continue\n\n // Named function declarations\n for (const fn of sf.getFunctions()) {\n const name = fn.getName()\n if (!name) continue\n records.push({\n name,\n filePath,\n line: fn.getStartLineNumber(),\n bodyHash: hashFunctionBody(fn),\n paramCount: fn.getParameters().length,\n })\n }\n\n // Exported const arrow functions\n for (const stmt of sf.getVariableStatements()) {\n if (stmt.getDeclarationKind() !== VariableDeclarationKind.Const) continue\n if (!isExported(stmt)) continue\n\n for (const decl of stmt.getDeclarations()) {\n const arrowFn = decl.getInitializer()?.asKind(SyntaxKind.ArrowFunction)\n if (!arrowFn) continue\n records.push({\n name: decl.getName(),\n filePath,\n line: decl.getStartLineNumber(),\n bodyHash: hashFunctionBody(arrowFn),\n paramCount: arrowFn.getParameters().length,\n })\n }\n }\n }\n\n return records\n}\n\n/**\n * Groups functions whose names have a Levenshtein distance ≤ 2 using\n * union-find (transitive closure). Returns only clusters of ≥ 2 functions.\n */\nexport function clusterByName(functions: FunctionRecord[]): DupeCluster[] {\n const n = functions.length\n const parent = Array.from({ length: n }, (_, i) => i)\n\n function find(i: number): number {\n while (parent[i] !== i) {\n parent[i] = parent[parent[i]!]!\n i = parent[i]!\n }\n return i\n }\n\n for (let i = 0; i < n; i++) {\n for (let j = i + 1; j < n; j++) {\n if (levenshtein(functions[i]!.name, functions[j]!.name) <= 2) {\n parent[find(i)] = find(j)\n }\n }\n }\n\n const groups = new Map<number, FunctionRecord[]>()\n for (let i = 0; i < n; i++) {\n const root = find(i)\n const group = groups.get(root) ?? []\n group.push(functions[i]!)\n groups.set(root, group)\n }\n\n return [...groups.values()]\n .filter((g) => g.length >= 2)\n .map((g) => ({ type: 'name' as const, functions: g }))\n}\n\n/**\n * Groups functions with identical body hashes.\n * Returns only clusters of ≥ 2 functions.\n */\nexport function clusterByStructure(functions: FunctionRecord[]): DupeCluster[] {\n const groups = new Map<string, FunctionRecord[]>()\n\n for (const fn of functions) {\n if (!fn.bodyHash) continue\n const group = groups.get(fn.bodyHash) ?? []\n group.push(fn)\n groups.set(fn.bodyHash, group)\n }\n\n return [...groups.values()]\n .filter((g) => g.length >= 2)\n .map((g) => ({ type: 'structure' as const, functions: g }))\n}\n\n/**\n * Finds `const` declarations that share the same name AND literal value\n * across multiple live files.\n */\nexport function findDuplicateConstants(\n project: Project,\n liveFiles: Set<string>,\n): ConstDupe[] {\n const map = new Map<string, Array<{ filePath: string; line: number }>>()\n\n for (const filePath of liveFiles) {\n const sf = project.getSourceFile(filePath)\n if (!sf) continue\n\n for (const stmt of sf.getVariableStatements()) {\n if (stmt.getDeclarationKind() !== VariableDeclarationKind.Const) continue\n\n for (const decl of stmt.getDeclarations()) {\n const name = decl.getName()\n const value = getLiteralValue(decl.getInitializer())\n if (!value) continue\n\n const key = JSON.stringify({ name, value })\n const existing = map.get(key) ?? []\n existing.push({ filePath, line: decl.getStartLineNumber() })\n map.set(key, existing)\n }\n }\n }\n\n const result: ConstDupe[] = []\n for (const [key, occurrences] of map) {\n if (occurrences.length < 2) continue\n const { name, value } = JSON.parse(key) as { name: string; value: string }\n result.push({ name, value, occurrences })\n }\n return result\n}\n\n/**\n * Builds a human-readable plain-text report for code_suggest.txt, including\n * an AI prompt for each cluster.\n */\nexport function buildDupeReport(clusters: DupeCluster[], constDupes: ConstDupe[]): string {\n const lines: string[] = [\n '========================================',\n ' DUPLICATE CODE REPORT',\n ` Function clusters : ${clusters.length}`,\n ` Const duplicates : ${constDupes.length}`,\n '========================================',\n '',\n ]\n\n if (clusters.length > 0) {\n lines.push('── FUNCTION CLUSTERS ──', '')\n for (const cluster of clusters) {\n const label = cluster.type === 'name' ? 'Similar Name' : 'Identical Structure'\n lines.push(`CLUSTER [${label}]`)\n for (const fn of cluster.functions) {\n lines.push(` ${fn.name} ${fn.filePath}:${fn.line} (params: ${fn.paramCount})`)\n }\n lines.push('')\n lines.push('AI SUGGESTION:', dupeClusterPrompt([cluster]), '')\n }\n }\n\n if (constDupes.length > 0) {\n lines.push('── DUPLICATE CONSTANTS ──', '')\n for (const d of constDupes) {\n lines.push(`CONST ${d.name} = ${d.value}`)\n for (const o of d.occurrences) {\n lines.push(` ${o.filePath}:${o.line}`)\n }\n lines.push('')\n }\n }\n\n return lines.join('\\n')\n}\n\n// ─── Private helpers ─────────────────────────────────────────────────────────\n\nfunction levenshtein(a: string, b: string): number {\n const ma = a.length\n const mb = b.length\n let prev = Array.from({ length: mb + 1 }, (_, j) => j)\n\n for (let i = 1; i <= ma; i++) {\n const curr: number[] = [i]\n for (let j = 1; j <= mb; j++) {\n curr[j] =\n a[i - 1] === b[j - 1]\n ? prev[j - 1]!\n : 1 + Math.min(prev[j]!, curr[j - 1]!, prev[j - 1]!)\n }\n prev = curr\n }\n\n return prev[mb]!\n}\n\nfunction getLiteralValue(node: ReturnType<import('ts-morph').VariableDeclaration['getInitializer']>): string | undefined {\n if (!node) return undefined\n const kind = node.getKind()\n if (\n kind === SyntaxKind.StringLiteral ||\n kind === SyntaxKind.NumericLiteral ||\n kind === SyntaxKind.TrueKeyword ||\n kind === SyntaxKind.FalseKeyword ||\n kind === SyntaxKind.NoSubstitutionTemplateLiteral\n ) {\n return node.getText()\n }\n return undefined\n}\n","import crypto from 'node:crypto'\nimport {\n Node,\n SyntaxKind,\n FunctionDeclaration,\n ArrowFunction,\n VariableDeclaration,\n ClassDeclaration,\n InterfaceDeclaration,\n TypeAliasDeclaration,\n SourceFile,\n} from 'ts-morph'\n\nexport type TopLevelDeclaration =\n | FunctionDeclaration\n | VariableDeclaration\n | ClassDeclaration\n | InterfaceDeclaration\n | TypeAliasDeclaration\n\n/**\n * Returns all top-level named declarations in a SourceFile.\n */\nexport function getTopLevelDeclarations(sourceFile: SourceFile): TopLevelDeclaration[] {\n const decls: TopLevelDeclaration[] = []\n\n decls.push(...sourceFile.getFunctions())\n decls.push(...sourceFile.getClasses())\n decls.push(...sourceFile.getInterfaces())\n decls.push(...sourceFile.getTypeAliases())\n\n for (const varStmt of sourceFile.getVariableStatements()) {\n decls.push(...varStmt.getDeclarations())\n }\n\n return decls\n}\n\n/**\n * Returns true if the given node has a JSDoc `@internal` or `@private` tag.\n */\nexport function hasInternalTag(node: Node): boolean {\n if (!Node.isJSDocable(node)) return false\n return node.getJsDocs().some((doc) =>\n doc.getTags().some((tag) => {\n const name = tag.getTagName()\n return name === 'internal' || name === 'private'\n }),\n )\n}\n\n/**\n * Returns the leading comment text for a node, if any.\n */\nexport function getLeadingComment(node: Node): string | undefined {\n const fullText = node.getFullText()\n const match = fullText.match(/^(\\s*\\/\\/[^\\n]*\\n|\\s*\\/\\*[\\s\\S]*?\\*\\/\\s*)+/)\n return match?.[0]?.trim()\n}\n\n/**\n * Checks whether a node is exported (has the `export` keyword).\n */\nexport function isExported(node: Node): boolean {\n return node.getFirstDescendantByKind(SyntaxKind.ExportKeyword) !== undefined\n}\n\n/**\n * Returns the name of a declaration node, or undefined if it is anonymous.\n */\nexport function getDeclarationName(node: TopLevelDeclaration): string | undefined {\n if ('getName' in node && typeof node.getName === 'function') {\n return node.getName() ?? undefined\n }\n return undefined\n}\n\n/**\n * Hashes the body of a function by walking its AST and replacing all\n * identifier names with sequential placeholders, so structurally identical\n * functions produce the same hash regardless of variable names.\n */\nexport function hashFunctionBody(func: FunctionDeclaration | ArrowFunction): string {\n const body = func.getBody()\n if (!body) return ''\n const identMap = new Map<string, string>()\n const normalized = normalizeNode(body, identMap)\n return crypto.createHash('sha256').update(normalized).digest('hex')\n}\n\nfunction normalizeNode(node: Node, identMap: Map<string, string>): string {\n if (node.getKind() === SyntaxKind.Identifier) {\n const text = node.getText()\n if (!identMap.has(text)) identMap.set(text, `$${identMap.size}`)\n return identMap.get(text)!\n }\n const children = node.getChildren()\n if (children.length === 0) return node.getText()\n return `[${node.getKindName()}:${children.map((c) => normalizeNode(c, identMap)).join(',')}]`\n}\n","import ora from 'ora'\nimport chalk from 'chalk'\nimport Table from 'cli-table3'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { Project, SyntaxKind } from 'ts-morph'\nimport { glob } from '@/utils/file.js'\nimport { writeMarkdown } from '@/core/reporter.js'\n\nexport interface DepCheckOptions {\n cwd: string\n output?: string\n}\n\nexport interface DepIssue {\n name: string\n type: 'unused' | 'missing' | 'unlisted-dev'\n detail?: string\n}\n\n/**\n * Audits package.json against actual import usage in source files.\n */\nexport async function runDepCheck(opts: DepCheckOptions): Promise<DepIssue[]> {\n const spinner = ora(chalk.cyan('Auditing dependencies…')).start()\n\n try {\n const pkgPath = path.join(opts.cwd, 'package.json')\n if (!fs.existsSync(pkgPath)) {\n spinner.fail(chalk.red(`No package.json found at ${opts.cwd}`))\n return []\n }\n\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as {\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n }\n\n const declared = new Set([\n ...Object.keys(pkg.dependencies ?? {}),\n ...Object.keys(pkg.devDependencies ?? {}),\n ])\n\n const srcDir = path.join(opts.cwd, 'src')\n const files = glob(srcDir, ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], ['node_modules', 'dist'])\n\n const usedPackages = new Set<string>()\n\n for (const filePath of files) {\n const content = fs.readFileSync(filePath, 'utf-8')\n const importRegex = /from\\s+['\"]([^'\"./][^'\"]*)['\"]/g\n let match: RegExpExecArray | null\n\n while ((match = importRegex.exec(content)) !== null) {\n const specifier = match[1]\n // Normalise scoped packages: @org/pkg → @org/pkg\n const pkgName = specifier.startsWith('@')\n ? specifier.split('/').slice(0, 2).join('/')\n : specifier.split('/')[0]\n usedPackages.add(pkgName)\n }\n }\n\n const issues: DepIssue[] = []\n\n // Unused declared dependencies\n for (const dep of declared) {\n if (!usedPackages.has(dep)) {\n issues.push({ name: dep, type: 'unused' })\n }\n }\n\n // Undeclared (missing) dependencies\n for (const pkg of usedPackages) {\n if (!declared.has(pkg) && !isBuiltin(pkg)) {\n issues.push({ name: pkg, type: 'missing' })\n }\n }\n\n spinner.succeed(chalk.green(`Dependency audit complete — ${issues.length} issue(s) found`))\n\n if (issues.length === 0) {\n console.log(chalk.green(' All dependencies look healthy.'))\n return issues\n }\n\n const table = new Table({ head: ['Package', 'Issue'] })\n for (const issue of issues) {\n const label =\n issue.type === 'unused'\n ? chalk.yellow('unused')\n : issue.type === 'missing'\n ? chalk.red('missing from package.json')\n : chalk.magenta('unlisted dev dep')\n table.push([chalk.gray(issue.name), label])\n }\n console.log(table.toString())\n\n if (opts.output) {\n writeMarkdown(\n {\n title: 'Dependency Audit Report',\n summary: `${issues.length} dependency issue(s) found`,\n sections: [\n {\n title: 'Issues',\n headers: ['Package', 'Type'],\n rows: issues.map((i) => [i.name, i.type]),\n },\n ],\n generatedAt: new Date(),\n },\n opts.output,\n )\n console.log(chalk.cyan(` Report written to ${opts.output}`))\n }\n\n return issues\n } catch (err) {\n spinner.fail(chalk.red('Dependency audit failed'))\n throw err\n }\n}\n\nconst NODE_BUILTINS = new Set([\n 'node:fs', 'node:path', 'node:os', 'node:url', 'node:crypto', 'node:util',\n 'node:stream', 'node:events', 'node:child_process', 'node:process',\n 'fs', 'path', 'os', 'url', 'crypto', 'util', 'stream', 'events', 'child_process',\n])\n\nfunction isBuiltin(name: string): boolean {\n return NODE_BUILTINS.has(name) || name.startsWith('node:')\n}\n\n// ─── Module API ───────────────────────────────────────────────────────────────\n\nexport interface DepCheckResult {\n unusedPackages: string[]\n missingPackages: string[]\n report: string\n}\n\n/**\n * Packages that live exclusively in config/tooling files and are never\n * imported in application source code. Prunify skips these so it doesn't\n * raise false-positive \"unused\" warnings.\n */\nconst CONFIG_ONLY_PACKAGES = new Set([\n // linters / formatters\n 'eslint', '@eslint/js', 'eslint-config-prettier', 'eslint-plugin-react',\n 'eslint-plugin-import', 'eslint-plugin-node', 'eslint-plugin-jsx-a11y',\n 'eslint-plugin-unicorn', '@typescript-eslint/eslint-plugin', '@typescript-eslint/parser',\n 'prettier',\n // type-only packages\n // (handled separately via @types/ prefix check)\n // TypeScript & transpilers\n 'typescript', 'ts-node', 'tsx',\n // test runners\n 'jest', 'ts-jest', '@jest/globals', 'vitest', '@vitest/ui', '@vitest/coverage-v8',\n // bundlers / build tools\n 'rollup', 'webpack', 'vite', 'esbuild', 'tsup', 'parcel',\n '@rollup/plugin-node-resolve', '@rollup/plugin-commonjs', '@rollup/plugin-typescript',\n 'babel', '@babel/core', '@babel/preset-env', '@babel/preset-typescript',\n // task runners / release\n 'nodemon', 'concurrently', 'npm-run-all', 'cross-env', 'rimraf',\n 'husky', 'lint-staged', 'commitizen', 'semantic-release',\n // monorepo\n 'turbo', 'nx', 'lerna',\n // CSS tooling\n 'postcss', 'autoprefixer', 'tailwindcss',\n])\n\n/**\n * Synchronous module-level dependency check backed by a pre-built ts-morph\n * `Project`. Flags packages declared in `package.json` that are never\n * imported in any source file (excluding peer deps, `@types/*`, and\n * config-only tooling listed in `CONFIG_ONLY_PACKAGES`).\n */\nexport function runDepCheckModule(rootDir: string, project: Project): DepCheckResult {\n const pkgPath = path.join(rootDir, 'package.json')\n if (!fs.existsSync(pkgPath)) {\n return { unusedPackages: [], missingPackages: [], report: buildDepReport([], []) }\n }\n\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as {\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n peerDependencies?: Record<string, string>\n }\n\n // peerDependencies are intentionally excluded — they're provided by the host\n const declared = new Set([\n ...Object.keys(pkg.dependencies ?? {}),\n ...Object.keys(pkg.devDependencies ?? {}),\n ])\n\n // Collect all package names actually referenced in source\n const usedPackages = new Set<string>()\n for (const sf of project.getSourceFiles()) {\n if (sf.getFilePath().endsWith('.d.ts')) continue\n\n // Static imports: import { X } from 'pkg'\n for (const importDecl of sf.getImportDeclarations()) {\n const name = extractPackageName(importDecl.getModuleSpecifierValue())\n if (name) usedPackages.add(name)\n }\n\n // Re-exports: export { X } from 'pkg'\n for (const exportDecl of sf.getExportDeclarations()) {\n const spec = exportDecl.getModuleSpecifierValue()\n if (!spec) continue\n const name = extractPackageName(spec)\n if (name) usedPackages.add(name)\n }\n\n // Dynamic imports: import('pkg') and require('pkg')\n for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {\n const expr = call.getExpression().getText()\n if (expr !== 'require' && expr !== 'import') continue\n const args = call.getArguments()\n if (args.length === 0) continue\n const first = args[0]\n if (!first?.isKind(SyntaxKind.StringLiteral)) continue\n const name = extractPackageName(first.getLiteralValue())\n if (name) usedPackages.add(name)\n }\n }\n\n const unusedPackages: string[] = []\n for (const dep of declared) {\n if (\n !usedPackages.has(dep) &&\n !CONFIG_ONLY_PACKAGES.has(dep) &&\n !dep.startsWith('@types/')\n ) {\n unusedPackages.push(dep)\n }\n }\n\n const report = buildDepReport(unusedPackages, [])\n return { unusedPackages, missingPackages: [], report }\n}\n\nfunction extractPackageName(specifier: string): string | null {\n if (!specifier) return null\n // Relative / absolute paths are not packages\n if (specifier.startsWith('.') || specifier.startsWith('/')) return null\n // Node builtins\n if (isBuiltin(specifier)) return null\n // Scoped packages: @org/pkg[/sub/path]\n if (specifier.startsWith('@')) {\n const parts = specifier.split('/')\n return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null\n }\n // Regular packages: pkg[/sub/path]\n return specifier.split('/')[0]!\n}\n\nfunction buildDepReport(unusedPackages: string[], missingPackages: string[]): string {\n const lines: string[] = [\n '========================================',\n ' DEPENDENCY CHECK',\n ` Unused packages : ${unusedPackages.length}`,\n ` Missing from package.json : ${missingPackages.length}`,\n '========================================',\n '',\n ]\n\n if (unusedPackages.length > 0) {\n lines.push('── UNUSED PACKAGES ──', '')\n for (const pkg of unusedPackages) {\n lines.push(` ${pkg}`, ` Declared in package.json but never imported in source.`, '')\n }\n }\n\n if (missingPackages.length > 0) {\n lines.push('── MISSING FROM package.json ──', '')\n for (const pkg of missingPackages) {\n lines.push(` ${pkg}`, ` Imported in source but not listed in package.json.`, '')\n }\n }\n\n if (unusedPackages.length === 0 && missingPackages.length === 0) {\n lines.push(' All dependencies look healthy.', '')\n }\n\n return lines.join('\\n')\n}\n","import chalk from 'chalk'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { Project } from 'ts-morph'\nimport { runDeadCode } from '@/modules/dead-code.js'\nimport { runDupeFinder } from '@/modules/dupe-finder.js'\nimport { runCircular } from '@/modules/circular.js'\nimport { runDepCheck } from '@/modules/dep-check.js'\nimport { writeMarkdown } from '@/core/reporter.js'\nimport type { Report, ReportSection } from '@/core/reporter.js'\nimport type { DeadCodeResult } from '@/modules/dead-code.js'\nimport type { DupeCluster, ConstDupe } from '@/modules/dupe-finder.js'\nimport type { CircularResult } from '@/modules/circular.js'\nimport type { DepCheckResult } from '@/modules/dep-check.js'\n\nexport interface HealthReportOptions {\n output: string\n}\n\n/**\n * Orchestrates all analysis modules and writes a combined Markdown health report.\n */\nexport async function runHealthReport(dir: string, opts: HealthReportOptions): Promise<void> {\n console.log(chalk.bold.cyan('\\n prunify — Codebase Health Report\\n'))\n\n const [deadExports, dupes, cycles, depIssues] = await Promise.all([\n runDeadCode(dir, {}).catch(() => []),\n runDupeFinder(dir, {}).catch(() => []),\n runCircular(dir, {}).catch(() => []),\n runDepCheck({ cwd: path.resolve(dir, '..'), output: undefined }).catch(() => []),\n ])\n\n const sections: ReportSection[] = [\n {\n title: '🚨 Dead Exports',\n headers: ['File', 'Export'],\n rows: deadExports.map((d) => [d.file, d.exportName]),\n },\n {\n title: '🔁 Duplicate Blocks',\n headers: ['Hash', 'Lines', 'Occurrences'],\n rows: dupes.map((d) => [d.hash.slice(0, 8), String(d.lines), String(d.occurrences.length)]),\n },\n {\n title: '♻️ Circular Imports',\n headers: ['Cycle #', 'Chain'],\n rows: cycles.map((cycle, i) => [String(i + 1), cycle.join(' → ')]),\n },\n {\n title: '📦 Dependency Issues',\n headers: ['Package', 'Issue'],\n rows: depIssues.map((i) => [i.name, i.type]),\n },\n ]\n\n const totalIssues =\n deadExports.length + dupes.length + cycles.length + depIssues.length\n\n const report: Report = {\n title: 'Codebase Health Report',\n summary: `Analysed: ${path.resolve(dir)} | Total issues: ${totalIssues}`,\n sections,\n generatedAt: new Date(),\n }\n\n writeMarkdown(report, opts.output)\n\n console.log(\n chalk.bold(`\\n Health report written to `) + chalk.cyan(opts.output),\n )\n console.log(\n chalk.dim(` Total issues found: `) +\n (totalIssues > 0 ? chalk.red(String(totalIssues)) : chalk.green('0')),\n )\n}\n\n// ─── Module API ───────────────────────────────────────────────────────────────\n\nexport interface DupeModuleResult {\n clusters: DupeCluster[]\n constDupes: ConstDupe[]\n}\n\nexport interface AllModuleResults {\n dead: DeadCodeResult\n circular: CircularResult\n deps: DepCheckResult\n dupes: DupeModuleResult\n}\n\nexport interface HealthResult {\n score: number\n report: string\n html?: string\n}\n\n/**\n * Computes a codebase health score (0–100) from pre-computed module results,\n * builds a `code_health.txt` report, and optionally generates a self-contained\n * `code_health.html` with a colour-coded gauge.\n *\n * Scoring deductions:\n * -2 per dead file (max -20)\n * -3 per dupe cluster (max -15)\n * -5 per circular dep (max -20)\n * -2 per unused package (max -10)\n * -1 per barrel ≥ 15 exports (max -10)\n * -1 per file > 300 lines (max -10)\n */\nexport function runHealthReportModule(\n allResults: AllModuleResults,\n project: Project,\n html?: boolean,\n): HealthResult {\n const { dead, circular, deps, dupes } = allResults\n\n // ── Metrics ──────────────────────────────────────────────────────────────\n const deadFileCount = dead.safeToDelete.length + dead.transitivelyDead.length\n const dupeClusterCount = dupes.clusters.length\n const circularCount = circular.cycles.length\n const unusedPkgCount = deps.unusedPackages.length\n\n let barrelFileCount = 0\n let longFileCount = 0\n for (const sf of project.getSourceFiles()) {\n if (sf.getFilePath().endsWith('.d.ts')) continue\n // Barrel: file with 15+ distinct exported names\n const exportedCount = [...sf.getExportedDeclarations().keys()].length\n if (exportedCount >= 15) barrelFileCount++\n // Long file: > 300 lines\n if (sf.getEndLineNumber() > 300) longFileCount++\n }\n\n // ── Score ─────────────────────────────────────────────────────────────────\n let score = 100\n score -= Math.min(deadFileCount * 2, 20)\n score -= Math.min(dupeClusterCount * 3, 15)\n score -= Math.min(circularCount * 5, 20)\n score -= Math.min(unusedPkgCount * 2, 10)\n score -= Math.min(barrelFileCount * 1, 10)\n score -= Math.min(longFileCount * 1, 10)\n score = Math.max(0, score)\n\n // ── Text report ───────────────────────────────────────────────────────────\n const report = buildHealthReport(\n score,\n dead,\n circular,\n deps,\n dupes,\n barrelFileCount,\n longFileCount,\n )\n\n // ── HTML report ───────────────────────────────────────────────────────────\n const htmlContent = html\n ? buildHealthHtml(score, dead, circular, deps, dupes, barrelFileCount, longFileCount)\n : undefined\n\n return { score, report, html: htmlContent }\n}\n\n// ─── Text report builder ──────────────────────────────────────────────────────\n\nfunction buildHealthReport(\n score: number,\n dead: DeadCodeResult,\n circular: CircularResult,\n deps: DepCheckResult,\n dupes: DupeModuleResult,\n barrelFileCount: number,\n longFileCount: number,\n): string {\n const grade = score >= 80 ? 'GOOD' : score >= 50 ? 'FAIR' : 'POOR'\n const lines: string[] = [\n '========================================',\n ' CODE HEALTH REPORT',\n ` Score : ${score}/100 (${grade})`,\n ' Generated: ' + new Date().toISOString(),\n '========================================',\n '',\n ]\n\n // Dead code section\n lines.push('── DEAD CODE ──', '')\n const totalDeadFiles = dead.safeToDelete.length + dead.transitivelyDead.length\n if (totalDeadFiles === 0 && dead.deadExports.length === 0) {\n lines.push(' No dead files or exports detected.', '')\n } else {\n if (dead.safeToDelete.length > 0) {\n lines.push(` Safe to delete: ${dead.safeToDelete.length}`)\n for (const f of dead.safeToDelete) lines.push(` • ${f}`)\n lines.push('')\n }\n if (dead.transitivelyDead.length > 0) {\n lines.push(` Transitively dead: ${dead.transitivelyDead.length}`)\n for (const f of dead.transitivelyDead) lines.push(` • ${f}`)\n lines.push('')\n }\n if (dead.deadExports.length > 0) {\n lines.push(` Dead exports: ${dead.deadExports.length}`)\n for (const e of dead.deadExports)\n lines.push(` • ${e.filePath} → ${e.exportName} (line ${e.line})`)\n lines.push('')\n }\n }\n\n // Circular imports section\n lines.push('── CIRCULAR IMPORTS ──', '')\n if (circular.cycles.length === 0) {\n lines.push(' No circular imports detected.', '')\n } else {\n lines.push(` Cycles: ${circular.cycles.length}`)\n for (let i = 0; i < circular.cycles.length; i++) {\n const cycle = circular.cycles[i]!\n lines.push(` Cycle ${i + 1}: ${[...cycle, cycle[0]].join(' → ')}`)\n }\n lines.push('')\n }\n\n // Dependency check section\n lines.push('── DEPENDENCIES ──', '')\n if (deps.unusedPackages.length === 0) {\n lines.push(' All declared packages are used.', '')\n } else {\n lines.push(` Unused packages: ${deps.unusedPackages.length}`)\n for (const p of deps.unusedPackages) lines.push(` • ${p}`)\n lines.push('')\n }\n\n // Duplicate code section\n lines.push('── DUPLICATE CODE ──', '')\n if (dupes.clusters.length === 0 && dupes.constDupes.length === 0) {\n lines.push(' No duplicate functions or constants detected.', '')\n } else {\n if (dupes.clusters.length > 0) {\n lines.push(` Function clusters: ${dupes.clusters.length}`)\n for (const c of dupes.clusters) {\n const label = c.type === 'name' ? 'similar-name' : 'identical-body'\n lines.push(` [${label}] ${c.functions.map((f) => f.name).join(', ')}`)\n }\n lines.push('')\n }\n if (dupes.constDupes.length > 0) {\n lines.push(` Duplicate constants: ${dupes.constDupes.length}`)\n for (const d of dupes.constDupes) lines.push(` • ${d.name} = ${d.value}`)\n lines.push('')\n }\n }\n\n // Code quality section\n lines.push('── CODE QUALITY ──', '')\n lines.push(` Barrel files (≥15 exports) : ${barrelFileCount}`)\n lines.push(` Long files (>300 lines) : ${longFileCount}`)\n lines.push('')\n\n // Deductions summary\n lines.push('── SCORE BREAKDOWN ──', '')\n lines.push(` Starting score : 100`)\n lines.push(` Dead files (-${Math.min((dead.safeToDelete.length + dead.transitivelyDead.length) * 2, 20)})`)\n lines.push(` Dupe clusters (-${Math.min(dupes.clusters.length * 3, 15)})`)\n lines.push(` Circular deps (-${Math.min(circular.cycles.length * 5, 20)})`)\n lines.push(` Unused packages (-${Math.min(deps.unusedPackages.length * 2, 10)})`)\n lines.push(` Barrel files (-${Math.min(barrelFileCount, 10)})`)\n lines.push(` Long files (-${Math.min(longFileCount, 10)})`)\n lines.push(` ─────────────────────────────────`)\n lines.push(` Final score : ${score}/100`)\n lines.push('')\n\n return lines.join('\\n')\n}\n\n// ─── HTML report builder ──────────────────────────────────────────────────────\n\nfunction buildHealthHtml(\n score: number,\n dead: DeadCodeResult,\n circular: CircularResult,\n deps: DepCheckResult,\n dupes: DupeModuleResult,\n barrelFileCount: number,\n longFileCount: number,\n): string {\n const gaugeColor = score >= 80 ? '#22c55e' : score >= 50 ? '#f59e0b' : '#ef4444'\n const grade = score >= 80 ? 'Good' : score >= 50 ? 'Fair' : 'Poor'\n\n // SVG donut gauge — full circle, filled proportionally\n const r = 54\n const circ = 2 * Math.PI * r\n const filled = (score / 100) * circ\n\n function section(emoji: string, title: string, count: number, rows: [string, string][]): string {\n const color = count === 0 ? '#22c55e' : '#f59e0b'\n const badge = `<span style=\"background:${color};color:#fff;border-radius:9999px;padding:1px 8px;font-size:0.75rem;margin-left:6px;\">${count}</span>`\n const body =\n rows.length === 0\n ? `<p style=\"color:#6b7280;margin:0\">None found ✓</p>`\n : `<table style=\"width:100%;border-collapse:collapse;font-size:0.85rem\">\n <thead><tr style=\"background:#f3f4f6\">${Object.keys(rows[0]!).map(() => '').join('')}</tr></thead>\n <tbody>\n ${rows.map(([a, b]) => `<tr style=\"border-bottom:1px solid #e5e7eb\"><td style=\"padding:4px 8px\">${esc(a)}</td><td style=\"padding:4px 8px;color:#4b5563\">${esc(b)}</td></tr>`).join('')}\n </tbody>\n </table>`\n return `\n <details style=\"margin-bottom:12px;border:1px solid #e5e7eb;border-radius:8px;overflow:hidden\">\n <summary style=\"cursor:pointer;padding:12px 16px;font-weight:600;background:#f9fafb;user-select:none\">\n ${emoji} ${esc(title)}${badge}\n </summary>\n <div style=\"padding:12px 16px\">${body}</div>\n </details>`\n }\n\n const deadRows: [string, string][] = [\n ...dead.safeToDelete.map<[string, string]>((f) => [f, 'Safe to delete']),\n ...dead.transitivelyDead.map<[string, string]>((f) => [f, 'Transitively dead']),\n ...dead.deadExports.map<[string, string]>((e) => [`${e.filePath}:${e.line}`, `export ${e.exportName}`]),\n ]\n\n const circularRows: [string, string][] = circular.cycles.map<[string, string]>((c, i) => [\n `Cycle ${i + 1}`,\n [...c, c[0]].join(' → '),\n ])\n\n const depsRows: [string, string][] = deps.unusedPackages.map<[string, string]>((p) => [p, 'declared but never imported'])\n\n const dupeRows: [string, string][] = [\n ...dupes.clusters.map<[string, string]>((c) => [\n c.functions.map((f) => f.name).join(', '),\n c.type === 'name' ? 'similar name' : 'identical body',\n ]),\n ...dupes.constDupes.map<[string, string]>((d) => [`${d.name} = ${d.value}`, `${d.occurrences.length} occurrences`]),\n ]\n\n const qualityRows: [string, string][] = [\n [`Barrel files (≥15 exports)`, String(barrelFileCount)],\n [`Long files (>300 lines)`, String(longFileCount)],\n ]\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\"/>\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>\n<title>prunify — Code Health Report</title>\n<style>\n *{box-sizing:border-box;margin:0;padding:0}\n body{font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;background:#f9fafb;color:#111827;padding:32px}\n h1{font-size:1.5rem;font-weight:700;margin-bottom:4px}\n .subtitle{color:#6b7280;font-size:0.875rem;margin-bottom:32px}\n .gauge-wrap{display:flex;flex-direction:column;align-items:center;margin-bottom:32px}\n .gauge-label{font-size:2.5rem;font-weight:800;margin-top:8px;color:${gaugeColor}}\n .grade{font-size:1rem;color:#6b7280;margin-top:2px}\n details summary::-webkit-details-marker{display:none}\n details summary::marker{display:none}\n</style>\n</head>\n<body>\n<h1>🧹 prunify — Code Health Report</h1>\n<p class=\"subtitle\">Generated ${new Date().toISOString()}</p>\n\n<div class=\"gauge-wrap\">\n <svg width=\"140\" height=\"140\" viewBox=\"0 0 140 140\">\n <circle cx=\"70\" cy=\"70\" r=\"${r}\" fill=\"none\" stroke=\"#e5e7eb\" stroke-width=\"16\"/>\n <circle cx=\"70\" cy=\"70\" r=\"${r}\" fill=\"none\" stroke=\"${gaugeColor}\" stroke-width=\"16\"\n stroke-dasharray=\"${filled.toFixed(2)} ${circ.toFixed(2)}\"\n stroke-linecap=\"round\"\n transform=\"rotate(-90 70 70)\"/>\n <text x=\"70\" y=\"75\" text-anchor=\"middle\" font-size=\"24\" font-weight=\"700\" fill=\"${gaugeColor}\">${score}</text>\n </svg>\n <div class=\"gauge-label\">${score}/100</div>\n <div class=\"grade\">${grade}</div>\n</div>\n\n${section('🗑️', 'Dead Code', deadRows.length, deadRows)}\n${section('♻️', 'Circular Imports', circular.cycles.length, circularRows)}\n${section('📦', 'Unused Dependencies', deps.unusedPackages.length, depsRows)}\n${section('🔁', 'Duplicate Code', dupes.clusters.length + dupes.constDupes.length, dupeRows)}\n${section('📐', 'Code Quality', barrelFileCount + longFileCount, qualityRows)}\n\n<p style=\"margin-top:24px;font-size:0.75rem;color:#9ca3af\">Score formula: 100 − 2×dead_files (max 20) − 3×dupe_clusters (max 15) − 5×circular (max 20) − 2×unused_pkgs (max 10) − barrel_files (max 10) − long_files (max 10)</p>\n</body>\n</html>`\n}\n\nfunction esc(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n}\n","import ora from 'ora'\nimport chalk from 'chalk'\nimport Table from 'cli-table3'\nimport { discoverFiles, buildProject, getImportsForFile } from '@/core/parser.js'\nimport { buildGraph, detectCycles, type ImportGraph } from '@/core/graph.js'\nimport { writeMarkdown } from '@/core/reporter.js'\n\n// ─── Module API ───────────────────────────────────────────────────────────────\n\nexport interface CircularResult {\n cycles: string[][]\n report: string\n}\n\n/**\n * Synchronous, graph-only entry point used by the health-report orchestrator.\n * The import graph must already be built by the caller.\n */\nexport function runCircularModule(graph: ImportGraph): CircularResult {\n const cycles = detectCycles(graph)\n return { cycles, report: buildCircularReport(cycles) }\n}\n\nfunction buildCircularReport(cycles: string[][]): string {\n const lines: string[] = [\n '========================================',\n ' CIRCULAR IMPORTS',\n ` Cycles: ${cycles.length}`,\n '========================================',\n '',\n ]\n\n if (cycles.length === 0) {\n lines.push(' No circular imports detected.', '')\n return lines.join('\\n')\n }\n\n lines.push('── CYCLES ──', '')\n for (let i = 0; i < cycles.length; i++) {\n const cycle = cycles[i]!\n lines.push(\n `Cycle ${i + 1}:`,\n ` ${[...cycle, cycle[0]].join(' → ')}`,\n ` Files involved: ${cycle.length}`,\n '',\n )\n }\n\n return lines.join('\\n')\n}\n\nexport interface CircularOptions {\n output?: string\n}\n\n/**\n * Detects circular import chains using DFS with a visited stack.\n */\nexport async function runCircular(dir: string, opts: CircularOptions): Promise<string[][]> {\n const spinner = ora(chalk.cyan('Scanning for circular imports…')).start()\n\n try {\n const fileList = discoverFiles(dir, [])\n const project = buildProject(fileList)\n const graph = buildGraph(fileList, (f) => {\n const sf = project.getSourceFile(f)\n return sf ? getImportsForFile(sf) : []\n })\n const cycles = detectCycles(graph)\n\n spinner.succeed(\n chalk.green(`Circular import scan complete — ${cycles.length} cycle(s) found`),\n )\n\n if (cycles.length === 0) {\n console.log(chalk.green(' No circular imports detected.'))\n return cycles\n }\n\n const table = new Table({ head: ['Cycle #', 'Files involved'] })\n cycles.forEach((cycle, i) => {\n table.push([chalk.yellow(String(i + 1)), cycle.map((f) => chalk.gray(f)).join('\\n → ')])\n })\n console.log(table.toString())\n\n if (opts.output) {\n writeMarkdown(\n {\n title: 'Circular Imports Report',\n summary: `${cycles.length} circular import chain(s) found`,\n sections: [\n {\n title: 'Circular Chains',\n headers: ['Cycle #', 'Files'],\n rows: cycles.map((cycle, i) => [String(i + 1), cycle.join(' → ')]),\n },\n ],\n generatedAt: new Date(),\n },\n opts.output,\n )\n console.log(chalk.cyan(` Report written to ${opts.output}`))\n }\n\n return cycles\n } catch (err) {\n spinner.fail(chalk.red('Circular import scan failed'))\n throw err\n }\n}\n","import fs from 'node:fs'\nimport path from 'node:path'\nimport ora from 'ora'\nimport chalk from 'chalk'\nimport Table from 'cli-table3'\nimport { glob } from '@/utils/file.js'\nimport { writeMarkdown } from '@/core/reporter.js'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface UnusedAsset {\n filePath: string\n relativePath: string\n sizeBytes: number\n}\n\nexport interface AssetCheckResult {\n unusedAssets: UnusedAsset[]\n totalAssets: number\n report: string\n}\n\nexport interface AssetCheckOptions {\n output?: string\n}\n\n// ─── Constants ────────────────────────────────────────────────────────────────\n\nconst ASSET_EXTENSIONS = new Set([\n '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif', '.ico', '.bmp',\n '.woff', '.woff2', '.ttf', '.eot', '.otf',\n '.mp4', '.webm', '.ogg', '.mp3', '.wav',\n '.pdf',\n])\n\nconst SOURCE_PATTERNS = [\n '**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx',\n '**/*.css', '**/*.scss', '**/*.sass', '**/*.less',\n '**/*.html', '**/*.json',\n]\n\nconst SOURCE_IGNORE = [\n 'node_modules', 'node_modules/**',\n 'dist', 'dist/**',\n '.next', '.next/**',\n 'coverage', 'coverage/**',\n 'public', 'public/**',\n]\n\n// ─── Public module API ────────────────────────────────────────────────────────\n\n/**\n * Scans `public/` for assets and checks whether each filename appears in\n * any source file (TS/JS/CSS/HTML/JSON). Checks both the bare filename and\n * the public-root-relative path (e.g. `/logo.png`).\n */\nexport function runAssetCheckModule(rootDir: string): AssetCheckResult {\n const publicDir = path.join(rootDir, 'public')\n\n if (!fs.existsSync(publicDir)) {\n return { unusedAssets: [], totalAssets: 0, report: buildAssetReport([], 0, rootDir) }\n }\n\n const assets = collectAssets(publicDir)\n if (assets.length === 0) {\n return { unusedAssets: [], totalAssets: 0, report: buildAssetReport([], 0, rootDir) }\n }\n\n // Read all source content once\n const sourceFiles = glob(rootDir, SOURCE_PATTERNS, SOURCE_IGNORE)\n const sourceContent = sourceFiles.reduce((acc, f) => {\n try { return acc + fs.readFileSync(f, 'utf-8') + '\\n' } catch { return acc }\n }, '')\n\n const unused: UnusedAsset[] = []\n\n for (const assetAbs of assets) {\n const fileName = path.basename(assetAbs)\n // Public-root-relative path e.g. \"/images/logo.png\"\n const relFromPublic = '/' + path.relative(publicDir, assetAbs).replaceAll('\\\\', '/')\n\n const referenced =\n sourceContent.includes(fileName) ||\n sourceContent.includes(relFromPublic)\n\n if (!referenced) {\n unused.push({\n filePath: assetAbs,\n relativePath: path.relative(rootDir, assetAbs).replaceAll('\\\\', '/'),\n sizeBytes: getFileSize(assetAbs),\n })\n }\n }\n\n const report = buildAssetReport(unused, assets.length, rootDir)\n return { unusedAssets: unused, totalAssets: assets.length, report }\n}\n\n// ─── CLI command ──────────────────────────────────────────────────────────────\n\nexport async function runAssetCheck(rootDir: string, opts: AssetCheckOptions): Promise<UnusedAsset[]> {\n const publicDir = path.join(rootDir, 'public')\n\n if (!fs.existsSync(publicDir)) {\n console.log(chalk.dim(' No public/ folder found — skipping asset check'))\n return []\n }\n\n const spinner = ora(chalk.cyan('Scanning public/ for unused assets…')).start()\n\n try {\n const result = runAssetCheckModule(rootDir)\n\n spinner.succeed(\n chalk.green(\n `Asset scan complete — ${result.unusedAssets.length} unused / ${result.totalAssets} total`,\n ),\n )\n\n if (result.unusedAssets.length === 0) {\n console.log(chalk.green(' All public assets are referenced in source.'))\n return []\n }\n\n const table = new Table({ head: ['Asset', 'Size'] })\n for (const asset of result.unusedAssets) {\n const kb = (asset.sizeBytes / 1024).toFixed(1)\n table.push([chalk.gray(asset.relativePath), `${kb} KB`])\n }\n console.log(table.toString())\n\n if (opts.output) {\n writeMarkdown(\n {\n title: 'Unused Assets Report',\n summary: `${result.unusedAssets.length} unused asset(s) found in public/`,\n sections: [\n {\n title: 'Unused Assets',\n headers: ['Asset', 'Size (KB)'],\n rows: result.unusedAssets.map((a) => [\n a.relativePath,\n (a.sizeBytes / 1024).toFixed(1),\n ]),\n },\n ],\n generatedAt: new Date(),\n },\n opts.output,\n )\n console.log(chalk.cyan(` Report written to ${opts.output}`))\n }\n\n return result.unusedAssets\n } catch (err) {\n spinner.fail(chalk.red('Asset scan failed'))\n throw err\n }\n}\n\n// ─── Internal helpers ─────────────────────────────────────────────────────────\n\nfunction collectAssets(dir: string): string[] {\n const results: string[] = []\n\n function walk(current: string): void {\n let entries: fs.Dirent[]\n try {\n entries = fs.readdirSync(current, { withFileTypes: true })\n } catch {\n return\n }\n for (const entry of entries) {\n const full = path.join(current, entry.name)\n if (entry.isDirectory()) {\n walk(full)\n } else if (entry.isFile() && ASSET_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {\n results.push(full)\n }\n }\n }\n\n walk(dir)\n return results\n}\n\nfunction getFileSize(filePath: string): number {\n try { return fs.statSync(filePath).size } catch { return 0 }\n}\n\nfunction buildAssetReport(unused: UnusedAsset[], totalAssets: number, rootDir: string): string {\n const totalBytes = unused.reduce((s, a) => s + a.sizeBytes, 0)\n const totalKb = (totalBytes / 1024).toFixed(1)\n\n const lines: string[] = [\n '========================================',\n ' UNUSED ASSETS REPORT',\n ` Total assets : ${totalAssets}`,\n ` Unused assets : ${unused.length}`,\n ` Recoverable : ~${totalKb} KB`,\n '========================================',\n '',\n ]\n\n if (unused.length === 0) {\n lines.push(' All public assets are referenced in source.', '')\n return lines.join('\\n')\n }\n\n lines.push('── UNUSED ASSETS ──', '')\n for (const asset of unused) {\n lines.push(\n `UNUSED — ${asset.relativePath}`,\n `Size: ~${(asset.sizeBytes / 1024).toFixed(1)} KB`,\n `Action: Safe to delete if not served directly via URL`,\n '',\n )\n }\n\n return lines.join('\\n')\n}\n"],"mappings":"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","names":["import_chalk","import_cli_table3","import_node_fs","import_node_path","import_node_fs","import_node_path","fs","path","path","path","fs","import_node_fs","import_node_path","path","fs","path","fs","path","import_ora","import_node_fs","import_node_path","import_ts_morph","import_node_fs","import_node_path","ora","path","fs","path","fs","path","fs","ora","chalk","Table","import_ora","import_chalk","import_cli_table3","import_node_fs","import_ts_morph","import_ts_morph","ora","chalk","fs","Table","import_ora","import_chalk","import_cli_table3","import_node_fs","import_node_path","import_ts_morph","ora","chalk","path","fs","pkg","Table","import_chalk","import_node_path","import_ora","import_chalk","import_cli_table3","ora","chalk","Table","chalk","path","import_node_fs","import_node_path","import_ora","import_chalk","import_cli_table3","SOURCE_PATTERNS","path","fs","getFileSize","chalk","ora","Table","path","fs","chalk","loadPackageJson","Table","readline"]}
|