angular-doctor 1.1.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -0
- package/dist/cli.mjs +462 -40
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +4 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +306 -17
- package/dist/index.mjs.map +1 -1
- package/install-skill.sh +181 -0
- package/package.json +4 -2
- package/skills/angular-doctor/SKILL.md +19 -0
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/utils/calculate-score.ts","../src/utils/filter-diagnostics.ts","../src/utils/combine-diagnostics.ts","../src/utils/discover-project.ts","../src/utils/load-config.ts","../src/utils/run-eslint.ts","../src/utils/run-knip.ts","../src/utils/get-diff-files.ts","../src/index.ts"],"sourcesContent":["export const SOURCE_FILE_PATTERN = /\\.ts$/;\n\nexport const MILLISECONDS_PER_SECOND = 1000;\n\nexport const PERFECT_SCORE = 100;\n\nexport const SCORE_GOOD_THRESHOLD = 75;\n\nexport const SCORE_OK_THRESHOLD = 50;\n\nexport const SCORE_BAR_WIDTH_CHARS = 50;\n\nexport const SUMMARY_BOX_HORIZONTAL_PADDING_CHARS = 1;\n\nexport const SUMMARY_BOX_OUTER_INDENT_CHARS = 2;\n\nexport const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;\n\nexport const MAX_KNIP_RETRIES = 5;\n\nexport const ERROR_RULE_PENALTY = 1.5;\n\nexport const WARNING_RULE_PENALTY = 0.75;\n\nexport const DEFAULT_BRANCH_CANDIDATES = [\"main\", \"master\"];\n","import {\n ERROR_RULE_PENALTY,\n PERFECT_SCORE,\n SCORE_GOOD_THRESHOLD,\n SCORE_OK_THRESHOLD,\n WARNING_RULE_PENALTY,\n} from \"../constants.js\";\nimport type { Diagnostic, ScoreResult } from \"../types.js\";\n\nexport const getScoreLabel = (score: number): string => {\n if (score >= SCORE_GOOD_THRESHOLD) return \"Great\";\n if (score >= SCORE_OK_THRESHOLD) return \"Needs work\";\n return \"Critical\";\n};\n\nconst countUniqueRules = (\n diagnostics: Diagnostic[],\n): { errorRuleCount: number; warningRuleCount: number } => {\n const errorRules = new Set<string>();\n const warningRules = new Set<string>();\n\n for (const diagnostic of diagnostics) {\n const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;\n if (diagnostic.severity === \"error\") {\n errorRules.add(ruleKey);\n } else {\n warningRules.add(ruleKey);\n }\n }\n\n return { errorRuleCount: errorRules.size, warningRuleCount: warningRules.size };\n};\n\nexport const calculateScore = (diagnostics: Diagnostic[]): ScoreResult => {\n const { errorRuleCount, warningRuleCount } = countUniqueRules(diagnostics);\n const penalty = errorRuleCount * ERROR_RULE_PENALTY + warningRuleCount * WARNING_RULE_PENALTY;\n const score = Math.max(0, Math.round(PERFECT_SCORE - penalty));\n return { score, label: getScoreLabel(score) };\n};\n","import type { AngularDoctorConfig, Diagnostic } from \"../types.js\";\n\nconst matchesGlob = (filePath: string, pattern: string): boolean => {\n const escapedPattern = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*\\*/g, \".*\")\n .replace(/\\*/g, \"[^/]*\");\n return new RegExp(`^${escapedPattern}$`).test(filePath);\n};\n\nexport const filterIgnoredDiagnostics = (\n diagnostics: Diagnostic[],\n config: AngularDoctorConfig,\n): Diagnostic[] => {\n const ignoredRules = new Set(config.ignore?.rules ?? []);\n const ignoredFilePatterns = config.ignore?.files ?? [];\n\n return diagnostics.filter((diagnostic) => {\n const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;\n if (ignoredRules.has(ruleKey)) return false;\n\n if (ignoredFilePatterns.some((pattern) => matchesGlob(diagnostic.filePath, pattern))) {\n return false;\n }\n\n return true;\n });\n};\n","import { SOURCE_FILE_PATTERN } from \"../constants.js\";\nimport type { AngularDoctorConfig, Diagnostic } from \"../types.js\";\nimport { filterIgnoredDiagnostics } from \"./filter-diagnostics.js\";\n\nexport const computeIncludePaths = (includePaths: string[]): string[] | undefined =>\n includePaths.length > 0\n ? includePaths.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath))\n : undefined;\n\nexport const combineDiagnostics = (\n lintDiagnostics: Diagnostic[],\n deadCodeDiagnostics: Diagnostic[],\n userConfig: AngularDoctorConfig | null,\n): Diagnostic[] => {\n const allDiagnostics = [...lintDiagnostics, ...deadCodeDiagnostics];\n return userConfig ? filterIgnoredDiagnostics(allDiagnostics, userConfig) : allDiagnostics;\n};\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport { GIT_LS_FILES_MAX_BUFFER_BYTES, SOURCE_FILE_PATTERN } from \"../constants.js\";\nimport type { AngularFramework, PackageJson, ProjectInfo, WorkspacePackage } from \"../types.js\";\n\nconst ANGULAR_FRAMEWORK_PACKAGES: Record<string, AngularFramework> = {\n \"@nrwl/angular\": \"nx\",\n \"@nx/angular\": \"nx\",\n \"@analogjs/platform\": \"analog\",\n \"@ionic/angular\": \"ionic\",\n \"@angular/ssr\": \"universal\",\n \"@nguniversal/express-engine\": \"universal\",\n};\n\nconst ANGULAR_FRAMEWORK_DISPLAY_NAMES: Record<AngularFramework, string> = {\n \"angular-cli\": \"Angular CLI\",\n nx: \"Nx\",\n analog: \"AnalogJS\",\n ionic: \"Ionic\",\n universal: \"Angular SSR\",\n unknown: \"Angular\",\n};\n\nexport const formatFrameworkName = (framework: AngularFramework): string =>\n ANGULAR_FRAMEWORK_DISPLAY_NAMES[framework];\n\nconst readPackageJson = (filePath: string): PackageJson => {\n try {\n const content = fs.readFileSync(filePath, \"utf-8\");\n return JSON.parse(content) as PackageJson;\n } catch {\n return {};\n }\n};\n\nconst collectAllDependencies = (packageJson: PackageJson): Record<string, string> => ({\n ...packageJson.peerDependencies,\n ...packageJson.dependencies,\n ...packageJson.devDependencies,\n});\n\nconst detectFramework = (dependencies: Record<string, string>): AngularFramework => {\n for (const [packageName, frameworkName] of Object.entries(ANGULAR_FRAMEWORK_PACKAGES)) {\n if (dependencies[packageName]) {\n return frameworkName;\n }\n }\n if (dependencies[\"@angular/cli\"] || dependencies[\"@angular-devkit/build-angular\"] || dependencies[\"@angular-devkit/core\"]) {\n return \"angular-cli\";\n }\n return \"unknown\";\n};\n\nconst detectAngularVersion = (dependencies: Record<string, string>): string | null =>\n dependencies[\"@angular/core\"] ?? null;\n\nconst detectStandaloneComponents = (packageJson: PackageJson): boolean => {\n const deps = collectAllDependencies(packageJson);\n const angularVersion = deps[\"@angular/core\"];\n if (!angularVersion) return false;\n // Angular 14+ supports standalone components (standalone: true flag)\n // Angular 17+ makes standalone the default\n const majorVersion = parseInt(angularVersion.match(/\\d+/)?.[0] ?? \"\", 10);\n return !isNaN(majorVersion) && majorVersion >= 14;\n};\n\nconst countSourceFiles = (rootDirectory: string): number => {\n const result = spawnSync(\"git\", [\"ls-files\", \"--cached\", \"--others\", \"--exclude-standard\"], {\n cwd: rootDirectory,\n encoding: \"utf-8\",\n maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES,\n });\n\n if (result.error || result.status !== 0) {\n return 0;\n }\n\n return result.stdout\n .split(\"\\n\")\n .filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath)).length;\n};\n\nconst hasAngularDependency = (packageJson: PackageJson): boolean => {\n const allDependencies = collectAllDependencies(packageJson);\n return Boolean(allDependencies[\"@angular/core\"]);\n};\n\n/**\n * Walks up the directory tree from `directory` until it finds a `package.json`.\n * Returns the directory containing the package.json, or null if not found.\n */\nconst findNearestPackageJsonDir = (directory: string): string | null => {\n let current = directory;\n while (true) {\n if (fs.existsSync(path.join(current, \"package.json\"))) return current;\n const parent = path.dirname(current);\n if (parent === current) return null;\n current = parent;\n }\n};\n\nexport const discoverProject = (directory: string): ProjectInfo => {\n // First try the given directory, then walk up to find the nearest package.json\n const packageJsonDir = fs.existsSync(path.join(directory, \"package.json\"))\n ? directory\n : findNearestPackageJsonDir(directory);\n\n if (!packageJsonDir) {\n throw new Error(`No package.json found in ${directory} or any parent directory`);\n }\n\n const packageJson = readPackageJson(path.join(packageJsonDir, \"package.json\"));\n const allDeps = collectAllDependencies(packageJson);\n const angularVersion = detectAngularVersion(allDeps);\n const framework = detectFramework(allDeps);\n\n // tsconfig.json — check the project directory first, then the package.json directory\n const hasTypeScript =\n fs.existsSync(path.join(directory, \"tsconfig.json\")) ||\n fs.existsSync(path.join(packageJsonDir, \"tsconfig.json\"));\n\n const hasStandaloneComponents = detectStandaloneComponents(packageJson);\n const sourceFileCount = countSourceFiles(directory);\n\n // Use the Angular project name from angular.json if possible, otherwise from package.json\n const angularJsonPath = path.join(packageJsonDir, \"angular.json\");\n let projectName = packageJson.name ?? path.basename(directory);\n if (packageJsonDir !== directory && fs.existsSync(angularJsonPath)) {\n // Use the directory name as a more meaningful project name for workspace sub-projects\n projectName = path.basename(directory);\n }\n\n return {\n rootDirectory: directory,\n projectName,\n angularVersion,\n framework,\n hasTypeScript,\n hasStandaloneComponents,\n sourceFileCount,\n };\n};\n\nconst parsePnpmWorkspacePatterns = (rootDirectory: string): string[] => {\n const workspacePath = path.join(rootDirectory, \"pnpm-workspace.yaml\");\n if (!fs.existsSync(workspacePath)) return [];\n\n const content = fs.readFileSync(workspacePath, \"utf-8\");\n const patterns: string[] = [];\n let isInsidePackagesBlock = false;\n\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (trimmed === \"packages:\") {\n isInsidePackagesBlock = true;\n continue;\n }\n if (isInsidePackagesBlock && trimmed.startsWith(\"-\")) {\n patterns.push(trimmed.replace(/^-\\s*/, \"\").replace(/[\"']/g, \"\"));\n } else if (isInsidePackagesBlock && trimmed.length > 0 && !trimmed.startsWith(\"#\")) {\n isInsidePackagesBlock = false;\n }\n }\n\n return patterns;\n};\n\nconst getWorkspacePatterns = (rootDirectory: string, packageJson: PackageJson): string[] => {\n const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);\n if (pnpmPatterns.length > 0) return pnpmPatterns;\n\n if (Array.isArray(packageJson.workspaces)) {\n return packageJson.workspaces;\n }\n\n if (packageJson.workspaces?.packages) {\n return packageJson.workspaces.packages;\n }\n\n return [];\n};\n\nconst resolveWorkspaceDirectories = (rootDirectory: string, pattern: string): string[] => {\n const cleanPattern = pattern.replace(/[\"']/g, \"\").replace(/\\/\\*\\*$/, \"/*\");\n\n if (!cleanPattern.includes(\"*\")) {\n const directoryPath = path.join(rootDirectory, cleanPattern);\n if (fs.existsSync(directoryPath) && fs.existsSync(path.join(directoryPath, \"package.json\"))) {\n return [directoryPath];\n }\n return [];\n }\n\n const wildcardIndex = cleanPattern.indexOf(\"*\");\n const baseDirectory = path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));\n const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);\n\n if (!fs.existsSync(baseDirectory) || !fs.statSync(baseDirectory).isDirectory()) {\n return [];\n }\n\n return fs\n .readdirSync(baseDirectory)\n .map((entry) => path.join(baseDirectory, entry, suffixAfterWildcard))\n .filter(\n (entryPath) =>\n fs.existsSync(entryPath) &&\n fs.statSync(entryPath).isDirectory() &&\n fs.existsSync(path.join(entryPath, \"package.json\")),\n );\n};\n\nexport const listWorkspacePackages = (rootDirectory: string): WorkspacePackage[] => {\n const packageJsonPath = path.join(rootDirectory, \"package.json\");\n if (!fs.existsSync(packageJsonPath)) return [];\n\n const packageJson = readPackageJson(packageJsonPath);\n const patterns = getWorkspacePatterns(rootDirectory, packageJson);\n if (patterns.length === 0) return [];\n\n const packages: WorkspacePackage[] = [];\n\n for (const pattern of patterns) {\n const directories = resolveWorkspaceDirectories(rootDirectory, pattern);\n for (const workspaceDirectory of directories) {\n const workspacePackageJson = readPackageJson(path.join(workspaceDirectory, \"package.json\"));\n\n if (!hasAngularDependency(workspacePackageJson)) continue;\n\n const name = workspacePackageJson.name ?? path.basename(workspaceDirectory);\n packages.push({ name, directory: workspaceDirectory });\n }\n }\n\n return packages;\n};\n\ninterface AngularWorkspaceProject {\n projectType?: string;\n root?: string;\n}\n\ninterface AngularWorkspace {\n version?: number;\n projects?: Record<string, AngularWorkspaceProject | string>;\n}\n\n/**\n * Reads `angular.json` (Angular CLI workspace config) and returns one\n * WorkspacePackage per project whose root directory contains an Angular dependency.\n */\nexport const listAngularWorkspaceProjects = (rootDirectory: string): WorkspacePackage[] => {\n const angularJsonPath = path.join(rootDirectory, \"angular.json\");\n if (!fs.existsSync(angularJsonPath)) return [];\n\n let workspace: AngularWorkspace;\n try {\n workspace = JSON.parse(fs.readFileSync(angularJsonPath, \"utf-8\")) as AngularWorkspace;\n } catch {\n return [];\n }\n\n if (!workspace.projects || typeof workspace.projects !== \"object\") return [];\n\n const packages: WorkspacePackage[] = [];\n\n for (const [name, projectConfig] of Object.entries(workspace.projects)) {\n // Older angular.json formats store the root as a plain string\n const root =\n typeof projectConfig === \"string\"\n ? projectConfig\n : (projectConfig.root ?? \"\");\n\n const projectDirectory = root ? path.resolve(rootDirectory, root) : rootDirectory;\n\n // Only include directories that actually exist\n if (!fs.existsSync(projectDirectory) || !fs.statSync(projectDirectory).isDirectory()) {\n continue;\n }\n\n packages.push({ name, directory: projectDirectory });\n }\n\n return packages;\n};\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { AngularDoctorConfig } from \"../types.js\";\n\nconst CONFIG_FILENAME = \"angular-doctor.config.json\";\nconst PACKAGE_JSON_CONFIG_KEY = \"angularDoctor\";\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nexport const loadConfig = (rootDirectory: string): AngularDoctorConfig | null => {\n const configFilePath = path.join(rootDirectory, CONFIG_FILENAME);\n\n if (fs.existsSync(configFilePath)) {\n try {\n const fileContent = fs.readFileSync(configFilePath, \"utf-8\");\n const parsed: unknown = JSON.parse(fileContent);\n if (!isPlainObject(parsed)) {\n console.warn(`Warning: ${CONFIG_FILENAME} must be a JSON object, ignoring.`);\n return null;\n }\n return parsed as AngularDoctorConfig;\n } catch (error) {\n console.warn(\n `Warning: Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`,\n );\n return null;\n }\n }\n\n const packageJsonPath = path.join(rootDirectory, \"package.json\");\n if (fs.existsSync(packageJsonPath)) {\n try {\n const fileContent = fs.readFileSync(packageJsonPath, \"utf-8\");\n const packageJson = JSON.parse(fileContent) as Record<string, unknown>;\n const embeddedConfig = packageJson[PACKAGE_JSON_CONFIG_KEY];\n if (isPlainObject(embeddedConfig)) {\n return embeddedConfig as AngularDoctorConfig;\n }\n } catch {\n return null;\n }\n }\n\n return null;\n};\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { ESLint, type Linter } from \"eslint\";\nimport angularEslintPlugin from \"@angular-eslint/eslint-plugin\";\nimport tsEslint from \"typescript-eslint\";\nimport type { Diagnostic } from \"../types.js\";\n\n// Rule category mapping\nconst RULE_CATEGORY_MAP: Record<string, string> = {\n // Angular component best practices\n \"@angular-eslint/component-class-suffix\": \"Components\",\n \"@angular-eslint/directive-class-suffix\": \"Components\",\n \"@angular-eslint/pipe-prefix\": \"Components\",\n \"@angular-eslint/use-pipe-transform-interface\": \"Components\",\n \"@angular-eslint/no-empty-lifecycle-method\": \"Components\",\n \"@angular-eslint/use-lifecycle-interface\": \"Components\",\n \"@angular-eslint/consistent-component-styles\": \"Components\",\n\n // Angular performance\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"Performance\",\n \"@angular-eslint/no-output-native\": \"Performance\",\n\n // Angular architecture / correctness\n \"@angular-eslint/no-conflicting-lifecycle\": \"Correctness\",\n \"@angular-eslint/contextual-lifecycle\": \"Correctness\",\n \"@angular-eslint/no-forward-ref\": \"Architecture\",\n \"@angular-eslint/no-input-rename\": \"Architecture\",\n \"@angular-eslint/no-output-rename\": \"Architecture\",\n \"@angular-eslint/no-inputs-metadata-property\": \"Architecture\",\n \"@angular-eslint/no-outputs-metadata-property\": \"Architecture\",\n \"@angular-eslint/prefer-standalone\": \"Architecture\",\n\n // TypeScript quality\n \"@typescript-eslint/no-explicit-any\": \"TypeScript\",\n \"@typescript-eslint/no-unused-vars\": \"Dead Code\",\n};\n\n// Rule severity mapping\nconst RULE_SEVERITY_MAP: Record<string, \"error\" | \"warning\"> = {\n \"@angular-eslint/no-conflicting-lifecycle\": \"error\",\n \"@angular-eslint/contextual-lifecycle\": \"error\",\n \"@angular-eslint/use-pipe-transform-interface\": \"error\",\n \"@angular-eslint/no-output-native\": \"error\",\n \"@angular-eslint/component-class-suffix\": \"warning\",\n \"@angular-eslint/directive-class-suffix\": \"warning\",\n \"@angular-eslint/pipe-prefix\": \"warning\",\n \"@angular-eslint/no-empty-lifecycle-method\": \"warning\",\n \"@angular-eslint/use-lifecycle-interface\": \"warning\",\n \"@angular-eslint/consistent-component-styles\": \"warning\",\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"warning\",\n \"@angular-eslint/no-forward-ref\": \"warning\",\n \"@angular-eslint/no-input-rename\": \"warning\",\n \"@angular-eslint/no-output-rename\": \"warning\",\n \"@angular-eslint/no-inputs-metadata-property\": \"warning\",\n \"@angular-eslint/no-outputs-metadata-property\": \"warning\",\n \"@angular-eslint/prefer-standalone\": \"warning\",\n \"@typescript-eslint/no-explicit-any\": \"warning\",\n \"@typescript-eslint/no-unused-vars\": \"warning\",\n};\n\n// Human-readable messages and help text\nconst RULE_MESSAGE_MAP: Record<string, string> = {\n \"@angular-eslint/component-class-suffix\":\n \"Component class should end with 'Component'\",\n \"@angular-eslint/directive-class-suffix\":\n \"Directive class should end with 'Directive'\",\n \"@angular-eslint/pipe-prefix\": \"Pipe name should have a consistent prefix\",\n \"@angular-eslint/use-pipe-transform-interface\":\n \"Pipe class must implement PipeTransform interface\",\n \"@angular-eslint/no-empty-lifecycle-method\": \"Remove empty lifecycle methods\",\n \"@angular-eslint/use-lifecycle-interface\":\n \"Implement the lifecycle interface for lifecycle hooks\",\n \"@angular-eslint/consistent-component-styles\":\n \"Use consistent styles type in component decorator\",\n \"@angular-eslint/prefer-on-push-component-change-detection\":\n \"Use OnPush change detection for better performance\",\n \"@angular-eslint/no-output-native\":\n \"Avoid shadowing native DOM events in output names\",\n \"@angular-eslint/no-conflicting-lifecycle\":\n \"Lifecycle hooks DoCheck and OnChanges cannot be used together\",\n \"@angular-eslint/contextual-lifecycle\":\n \"Lifecycle hook is not available in this context\",\n \"@angular-eslint/no-forward-ref\":\n \"Avoid using forwardRef — restructure to avoid circular dependency\",\n \"@angular-eslint/no-input-rename\":\n \"Avoid renaming directive inputs — use the property name as the binding name\",\n \"@angular-eslint/no-output-rename\":\n \"Avoid renaming directive outputs — use the property name as the binding name\",\n \"@angular-eslint/no-inputs-metadata-property\":\n \"Use @Input() decorator instead of inputs metadata property\",\n \"@angular-eslint/no-outputs-metadata-property\":\n \"Use @Output() decorator instead of outputs metadata property\",\n \"@angular-eslint/prefer-standalone\":\n \"Prefer standalone components over NgModule-based components\",\n \"@typescript-eslint/no-explicit-any\":\n \"Avoid 'any' type — use specific types for better type safety\",\n \"@typescript-eslint/no-unused-vars\": \"Remove unused variable declaration\",\n};\n\nconst RULE_HELP_MAP: Record<string, string> = {\n \"@angular-eslint/component-class-suffix\":\n \"Add 'Component' suffix: `export class UserProfileComponent { }`\",\n \"@angular-eslint/directive-class-suffix\":\n \"Add 'Directive' suffix: `export class HighlightDirective { }`\",\n \"@angular-eslint/use-pipe-transform-interface\":\n \"Implement PipeTransform: `export class MyPipe implements PipeTransform { transform(value: unknown) { } }`\",\n \"@angular-eslint/no-empty-lifecycle-method\":\n \"Remove the empty lifecycle method or add logic to it\",\n \"@angular-eslint/use-lifecycle-interface\":\n \"Add the interface: `export class MyComponent implements OnInit, OnDestroy { }`\",\n \"@angular-eslint/prefer-on-push-component-change-detection\":\n \"Add to decorator: `@Component({ changeDetection: ChangeDetectionStrategy.OnPush })`\",\n \"@angular-eslint/no-output-native\":\n \"Rename the output: use a descriptive name like `(valueChange)` instead of `(click)` or `(change)`\",\n \"@angular-eslint/no-forward-ref\":\n \"Restructure your code to avoid circular dependencies, or use `inject()` with a lazy function\",\n \"@angular-eslint/no-input-rename\":\n \"Remove the alias: `@Input() myProp: string` instead of `@Input('myAlias') myProp: string`\",\n \"@angular-eslint/no-output-rename\":\n \"Remove the alias: `@Output() myEvent = new EventEmitter()` instead of aliased version\",\n \"@angular-eslint/no-inputs-metadata-property\":\n \"Use `@Input() myProp: string` decorator on the property instead of `inputs: ['myProp']` in the decorator metadata\",\n \"@angular-eslint/no-outputs-metadata-property\":\n \"Use `@Output() myEvent = new EventEmitter()` instead of `outputs: ['myEvent']` in the decorator metadata\",\n \"@angular-eslint/prefer-standalone\":\n \"Add `standalone: true` to component: `@Component({ standalone: true, ... })`\",\n \"@typescript-eslint/no-explicit-any\":\n \"Replace `any` with a specific type or `unknown` if the type is truly unknown\",\n \"@typescript-eslint/no-unused-vars\":\n \"Remove the unused variable or prefix with `_` to indicate it's intentionally unused\",\n};\n\nconst buildEslintConfig = (\n hasTypeScript: boolean,\n tsconfigPath: string | null,\n useTypeAware: boolean,\n): Linter.Config[] => {\n const languageOptions: Linter.Config[\"languageOptions\"] = {\n parser: tsEslint.parser as Linter.Parser,\n parserOptions: {\n ecmaVersion: \"latest\",\n sourceType: \"module\",\n ...(hasTypeScript && tsconfigPath && useTypeAware\n ? { project: tsconfigPath }\n : {}),\n },\n };\n\n const angularRules: Linter.RulesRecord = {\n \"@angular-eslint/component-class-suffix\": \"warn\",\n \"@angular-eslint/directive-class-suffix\": \"warn\",\n \"@angular-eslint/no-empty-lifecycle-method\": \"warn\",\n \"@angular-eslint/use-lifecycle-interface\": \"warn\",\n \"@angular-eslint/use-pipe-transform-interface\": \"error\",\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"warn\",\n \"@angular-eslint/no-output-native\": \"error\",\n \"@angular-eslint/no-conflicting-lifecycle\": \"error\",\n \"@angular-eslint/contextual-lifecycle\": \"error\",\n \"@angular-eslint/no-forward-ref\": \"warn\",\n \"@angular-eslint/no-input-rename\": \"warn\",\n \"@angular-eslint/no-output-rename\": \"warn\",\n \"@angular-eslint/no-inputs-metadata-property\": \"warn\",\n \"@angular-eslint/no-outputs-metadata-property\": \"warn\",\n };\n\n const tsRules: Linter.RulesRecord = {\n \"@typescript-eslint/no-explicit-any\": \"warn\",\n };\n\n return [\n {\n files: [\"**/*.ts\"],\n plugins: {\n \"@angular-eslint\": angularEslintPlugin as unknown as ESLint.Plugin,\n \"@typescript-eslint\": tsEslint.plugin as unknown as ESLint.Plugin,\n },\n languageOptions,\n rules: {\n ...angularRules,\n ...tsRules,\n },\n },\n ];\n};\n\nconst mapEslintSeverity = (\n severity: number,\n ruleId: string | null,\n): \"error\" | \"warning\" => {\n if (ruleId && RULE_SEVERITY_MAP[ruleId]) {\n return RULE_SEVERITY_MAP[ruleId];\n }\n return severity === 2 ? \"error\" : \"warning\";\n};\n\nconst resolveDiagnosticCategory = (ruleId: string): string =>\n RULE_CATEGORY_MAP[ruleId] ?? \"Other\";\n\nconst resolveMessage = (ruleId: string, defaultMessage: string): string =>\n RULE_MESSAGE_MAP[ruleId] ?? defaultMessage;\n\nconst resolveHelp = (ruleId: string): string => RULE_HELP_MAP[ruleId] ?? \"\";\n\nconst parsePluginAndRule = (\n ruleId: string,\n): { plugin: string; rule: string } => {\n // e.g. \"@angular-eslint/component-class-suffix\" -> plugin: \"@angular-eslint\", rule: \"component-class-suffix\"\n // e.g. \"@typescript-eslint/no-explicit-any\" -> plugin: \"@typescript-eslint\", rule: \"no-explicit-any\"\n const match = ruleId.match(/^(@[^/]+\\/[^/]+|[^/]+)\\/(.+)$/);\n if (match) {\n return { plugin: match[1], rule: match[2] };\n }\n return { plugin: \"eslint\", rule: ruleId };\n};\n\nexport const runEslint = async (\n rootDirectory: string,\n hasTypeScript: boolean,\n includePaths?: string[],\n options?: { useTypeAware?: boolean },\n): Promise<Diagnostic[]> => {\n if (includePaths !== undefined && includePaths.length === 0) {\n return [];\n }\n\n const tsconfigPath = hasTypeScript\n ? path.join(rootDirectory, \"tsconfig.json\")\n : null;\n\n const cacheRoot = path.join(\n rootDirectory,\n \"node_modules\",\n \".cache\",\n \"angular-doctor\",\n );\n fs.mkdirSync(cacheRoot, { recursive: true });\n const eslint = new ESLint({\n cwd: rootDirectory,\n overrideConfigFile: null,\n overrideConfig: buildEslintConfig(\n hasTypeScript,\n tsconfigPath && fs.existsSync(tsconfigPath) ? tsconfigPath : null,\n options?.useTypeAware ?? true,\n ),\n ignore: true,\n cache: true,\n cacheLocation: path.join(cacheRoot, \".eslintcache\"),\n });\n\n const patterns = includePaths ?? [\"**/*.ts\"];\n\n let results: ESLint.LintResult[];\n try {\n results = await eslint.lintFiles(patterns);\n } catch {\n return [];\n }\n\n const diagnostics: Diagnostic[] = [];\n\n for (const result of results) {\n for (const message of result.messages) {\n if (!message.ruleId) continue;\n const { plugin, rule } = parsePluginAndRule(message.ruleId);\n const ruleKey = message.ruleId;\n\n diagnostics.push({\n filePath: path.relative(rootDirectory, result.filePath),\n plugin,\n rule,\n severity: mapEslintSeverity(message.severity, ruleKey),\n message: resolveMessage(ruleKey, message.message),\n help: resolveHelp(ruleKey),\n line: message.line ?? 0,\n column: message.column ?? 0,\n category: resolveDiagnosticCategory(ruleKey),\n });\n }\n }\n\n return diagnostics;\n};\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { main } from \"knip\";\nimport { createOptions } from \"knip/session\";\nimport { MAX_KNIP_RETRIES } from \"../constants.js\";\nimport type { Diagnostic, KnipIssueRecords, KnipResults } from \"../types.js\";\n\nconst KNIP_CATEGORY_MAP: Record<string, string> = {\n files: \"Dead Code\",\n exports: \"Dead Code\",\n types: \"Dead Code\",\n duplicates: \"Dead Code\",\n};\n\nconst KNIP_MESSAGE_MAP: Record<string, string> = {\n files: \"Unused file\",\n exports: \"Unused export\",\n types: \"Unused type\",\n duplicates: \"Duplicate export\",\n};\n\nconst KNIP_SEVERITY_MAP: Record<string, \"error\" | \"warning\"> = {\n files: \"warning\",\n exports: \"warning\",\n types: \"warning\",\n duplicates: \"warning\",\n};\n\nconst collectIssueRecords = (\n records: KnipIssueRecords,\n issueType: string,\n rootDirectory: string,\n): Diagnostic[] => {\n const diagnostics: Diagnostic[] = [];\n\n for (const issues of Object.values(records)) {\n for (const issue of Object.values(issues)) {\n const filePath = path.relative(rootDirectory, issue.filePath);\n // Skip issues for files outside the rootDirectory scope\n if (filePath.startsWith(\"..\")) continue;\n diagnostics.push({\n filePath,\n plugin: \"knip\",\n rule: issueType,\n severity: KNIP_SEVERITY_MAP[issueType] ?? \"warning\",\n message: `${KNIP_MESSAGE_MAP[issueType]}: ${issue.symbol}`,\n help: \"\",\n line: 0,\n column: 0,\n category: KNIP_CATEGORY_MAP[issueType] ?? \"Dead Code\",\n weight: 1,\n });\n }\n }\n\n return diagnostics;\n};\n\n// HACK: knip triggers dotenv which logs to stdout/stderr via console methods\nconst silenced = async <T>(fn: () => Promise<T>): Promise<T> => {\n const originalLog = console.log;\n const originalInfo = console.info;\n const originalWarn = console.warn;\n const originalError = console.error;\n console.log = () => {};\n console.info = () => {};\n console.warn = () => {};\n console.error = () => {};\n try {\n return await fn();\n } finally {\n console.log = originalLog;\n console.info = originalInfo;\n console.warn = originalWarn;\n console.error = originalError;\n }\n};\n\nconst CONFIG_LOADING_ERROR_PATTERN = /Error loading .*\\/([a-z-]+)\\.config\\./;\n\nconst extractFailedPluginName = (error: unknown): string | null => {\n const match = String(error).match(CONFIG_LOADING_ERROR_PATTERN);\n return match?.[1] ?? null;\n};\n\nconst ANGULAR_ENTRY_PATTERNS = [\"**/*.module.ts\", \"**/*.routes.ts\"];\n\nconst runKnipWithOptions = async (\n knipCwd: string,\n workspaceName?: string,\n): Promise<KnipResults> => {\n const options = await silenced(() =>\n createOptions({\n cwd: knipCwd,\n isShowProgress: false,\n ...(workspaceName ? { workspace: workspaceName } : {}),\n }),\n );\n\n const parsedConfig = options.parsedConfig as Record<string, unknown>;\n\n // For Angular projects without user-defined entry patterns, add Angular module\n // and routes files as entry points. This prevents false positives from\n // lazy-loaded modules whose import chains cannot be statically traced.\n if (\n fs.existsSync(path.join(knipCwd, \"angular.json\")) &&\n parsedConfig[\"entry\"] === undefined\n ) {\n parsedConfig[\"entry\"] = ANGULAR_ENTRY_PATTERNS;\n }\n\n for (let attempt = 0; attempt <= MAX_KNIP_RETRIES; attempt++) {\n try {\n return (await silenced(() => main(options))) as KnipResults;\n } catch (error) {\n const failedPlugin = extractFailedPluginName(error);\n if (!failedPlugin || attempt === MAX_KNIP_RETRIES) {\n throw error;\n }\n parsedConfig[failedPlugin] = false;\n }\n }\n\n throw new Error(\"Unreachable\");\n};\n\nconst hasNodeModules = (directory: string): boolean => {\n const nodeModulesPath = path.join(directory, \"node_modules\");\n return fs.existsSync(nodeModulesPath) && fs.statSync(nodeModulesPath).isDirectory();\n};\n\n/**\n * Searches upward from `directory` for an `angular.json` file and returns\n * the directory that contains it, or `null` if none is found.\n */\nexport const findAngularWorkspaceRoot = (directory: string): string | null => {\n let current = directory;\n while (true) {\n if (fs.existsSync(path.join(current, \"angular.json\"))) return current;\n const parent = path.dirname(current);\n if (parent === current) return null;\n current = parent;\n }\n};\n\nexport const runKnip = async (rootDirectory: string): Promise<Diagnostic[]> => {\n if (!hasNodeModules(rootDirectory)) {\n return [];\n }\n\n // Use the Angular workspace root (where angular.json lives) as the knip cwd\n // so that the Angular plugin can find its configuration and correctly identify\n // entry points for the whole workspace.\n const angularWorkspaceRoot = findAngularWorkspaceRoot(rootDirectory);\n const knipCwd = angularWorkspaceRoot ?? rootDirectory;\n\n const knipResult = await runKnipWithOptions(knipCwd);\n\n const { issues } = knipResult;\n const diagnostics: Diagnostic[] = [];\n\n for (const unusedFile of issues.files) {\n const filePath = path.relative(rootDirectory, unusedFile);\n // Skip files outside the rootDirectory scope (e.g., from other workspace projects)\n if (filePath.startsWith(\"..\")) continue;\n diagnostics.push({\n filePath,\n plugin: \"knip\",\n rule: \"files\",\n severity: KNIP_SEVERITY_MAP[\"files\"],\n message: KNIP_MESSAGE_MAP[\"files\"],\n help: \"This file is not imported by any other file in the project.\",\n line: 0,\n column: 0,\n category: KNIP_CATEGORY_MAP[\"files\"],\n weight: 1,\n });\n }\n\n const recordTypes = [\"exports\", \"types\", \"duplicates\"] as const;\n\n for (const issueType of recordTypes) {\n diagnostics.push(...collectIssueRecords(issues[issueType], issueType, rootDirectory));\n }\n\n return diagnostics;\n};\n","import { spawnSync } from \"node:child_process\";\nimport { SOURCE_FILE_PATTERN, DEFAULT_BRANCH_CANDIDATES } from \"../constants.js\";\nimport type { DiffInfo } from \"../types.js\";\n\nconst getCurrentBranch = (directory: string): string | null => {\n const result = spawnSync(\"git\", [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"], {\n cwd: directory,\n encoding: \"utf-8\",\n });\n return result.status === 0 ? result.stdout.trim() : null;\n};\n\nconst getBranchChangedFiles = (directory: string, baseBranch: string): string[] => {\n const result = spawnSync(\"git\", [\"diff\", \"--name-only\", baseBranch], {\n cwd: directory,\n encoding: \"utf-8\",\n });\n if (result.status !== 0) return [];\n return result.stdout.split(\"\\n\").filter(Boolean);\n};\n\nconst getUncommittedChangedFiles = (directory: string): string[] => {\n const result = spawnSync(\"git\", [\"status\", \"--porcelain\"], {\n cwd: directory,\n encoding: \"utf-8\",\n });\n if (result.status !== 0) return [];\n return result.stdout\n .split(\"\\n\")\n .filter(Boolean)\n .map((line) => line.slice(3).trim())\n .filter(Boolean);\n};\n\nconst branchExists = (directory: string, branch: string): boolean => {\n const result = spawnSync(\"git\", [\"rev-parse\", \"--verify\", branch], {\n cwd: directory,\n encoding: \"utf-8\",\n });\n return result.status === 0;\n};\n\nexport const filterSourceFiles = (files: string[]): string[] =>\n files.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath));\n\nexport const getDiffInfo = (directory: string, explicitBaseBranch?: string): DiffInfo | null => {\n const currentBranch = getCurrentBranch(directory);\n if (!currentBranch) return null;\n\n if (explicitBaseBranch) {\n if (!branchExists(directory, explicitBaseBranch)) return null;\n const changedFiles = getBranchChangedFiles(directory, explicitBaseBranch);\n return { currentBranch, baseBranch: explicitBaseBranch, changedFiles };\n }\n\n const uncommittedFiles = getUncommittedChangedFiles(directory);\n if (uncommittedFiles.length > 0) {\n return {\n currentBranch,\n baseBranch: currentBranch,\n changedFiles: uncommittedFiles,\n isCurrentChanges: true,\n };\n }\n\n for (const candidate of DEFAULT_BRANCH_CANDIDATES) {\n if (branchExists(directory, candidate) && currentBranch !== candidate) {\n const changedFiles = getBranchChangedFiles(directory, candidate);\n if (changedFiles.length > 0) {\n return { currentBranch, baseBranch: candidate, changedFiles };\n }\n }\n }\n\n return null;\n};\n","import path from \"node:path\";\nimport { performance } from \"node:perf_hooks\";\nimport type { AngularDoctorConfig, Diagnostic, DiffInfo, ProjectInfo, ScoreResult } from \"./types.js\";\nimport { calculateScore } from \"./utils/calculate-score.js\";\nimport { combineDiagnostics, computeIncludePaths } from \"./utils/combine-diagnostics.js\";\nimport { discoverProject } from \"./utils/discover-project.js\";\nimport { loadConfig } from \"./utils/load-config.js\";\nimport { runEslint } from \"./utils/run-eslint.js\";\nimport { runKnip } from \"./utils/run-knip.js\";\n\nexport type { Diagnostic, DiffInfo, ProjectInfo, AngularDoctorConfig, ScoreResult };\nexport { getDiffInfo, filterSourceFiles } from \"./utils/get-diff-files.js\";\n\nexport interface DiagnoseOptions {\n lint?: boolean;\n deadCode?: boolean;\n includePaths?: string[];\n}\n\nexport interface DiagnoseResult {\n diagnostics: Diagnostic[];\n score: ScoreResult;\n project: ProjectInfo;\n elapsedMilliseconds: number;\n}\n\nexport const diagnose = async (\n directory: string,\n options: DiagnoseOptions = {},\n): Promise<DiagnoseResult> => {\n const { includePaths = [] } = options;\n const isDiffMode = includePaths.length > 0;\n\n const startTime = performance.now();\n const resolvedDirectory = path.resolve(directory);\n const projectInfo = discoverProject(resolvedDirectory);\n const userConfig = loadConfig(resolvedDirectory);\n\n const effectiveLint = options.lint ?? userConfig?.lint ?? true;\n const effectiveDeadCode = options.deadCode ?? userConfig?.deadCode ?? true;\n\n if (!projectInfo.angularVersion) {\n throw new Error(\"No Angular dependency found in package.json\");\n }\n\n const computedIncludePaths = computeIncludePaths(includePaths);\n\n const emptyDiagnostics: Diagnostic[] = [];\n\n const lintPromise = effectiveLint\n ? runEslint(\n resolvedDirectory,\n projectInfo.hasTypeScript,\n computedIncludePaths,\n ).catch((error: unknown) => {\n console.error(\"Lint failed:\", error);\n return emptyDiagnostics;\n })\n : Promise.resolve(emptyDiagnostics);\n\n const deadCodePromise =\n effectiveDeadCode && !isDiffMode\n ? runKnip(resolvedDirectory).catch((error: unknown) => {\n console.error(\"Dead code analysis failed:\", error);\n return emptyDiagnostics;\n })\n : Promise.resolve(emptyDiagnostics);\n\n const [lintDiagnostics, deadCodeDiagnostics] = await Promise.all([lintPromise, deadCodePromise]);\n const diagnostics = combineDiagnostics(lintDiagnostics, deadCodeDiagnostics, userConfig);\n\n const elapsedMilliseconds = performance.now() - startTime;\n const score = calculateScore(diagnostics);\n\n return {\n diagnostics,\n score,\n project: projectInfo,\n elapsedMilliseconds,\n };\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/utils/calculate-score.ts","../src/utils/filter-diagnostics.ts","../src/utils/combine-diagnostics.ts","../src/utils/discover-project.ts","../src/utils/load-config.ts","../src/utils/run-eslint.ts","../src/utils/run-knip.ts","../src/utils/get-diff-files.ts","../src/index.ts"],"sourcesContent":["export const SOURCE_FILE_PATTERN = /\\.ts$/;\n\nexport const MILLISECONDS_PER_SECOND = 1000;\n\nexport const PERFECT_SCORE = 100;\n\nexport const SCORE_GOOD_THRESHOLD = 75;\n\nexport const SCORE_OK_THRESHOLD = 50;\n\nexport const SCORE_BAR_WIDTH_CHARS = 50;\n\nexport const SUMMARY_BOX_HORIZONTAL_PADDING_CHARS = 1;\n\nexport const SUMMARY_BOX_OUTER_INDENT_CHARS = 2;\n\nexport const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;\n\nexport const MAX_KNIP_RETRIES = 5;\n\nexport const ERROR_RULE_PENALTY = 1.5;\n\nexport const WARNING_RULE_PENALTY = 0.75;\n\nexport const DEFAULT_BRANCH_CANDIDATES = [\"main\", \"master\"];\n","import {\n ERROR_RULE_PENALTY,\n PERFECT_SCORE,\n SCORE_GOOD_THRESHOLD,\n SCORE_OK_THRESHOLD,\n WARNING_RULE_PENALTY,\n} from \"../constants.js\";\nimport type { Diagnostic, ScoreResult } from \"../types.js\";\n\nexport const getScoreLabel = (score: number): string => {\n if (score >= SCORE_GOOD_THRESHOLD) return \"Great\";\n if (score >= SCORE_OK_THRESHOLD) return \"Needs work\";\n return \"Critical\";\n};\n\nconst countUniqueRules = (\n diagnostics: Diagnostic[],\n): { errorRuleCount: number; warningRuleCount: number } => {\n const errorRules = new Set<string>();\n const warningRules = new Set<string>();\n\n for (const diagnostic of diagnostics) {\n const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;\n if (diagnostic.severity === \"error\") {\n errorRules.add(ruleKey);\n } else {\n warningRules.add(ruleKey);\n }\n }\n\n return { errorRuleCount: errorRules.size, warningRuleCount: warningRules.size };\n};\n\nexport const calculateScore = (diagnostics: Diagnostic[]): ScoreResult => {\n const { errorRuleCount, warningRuleCount } = countUniqueRules(diagnostics);\n const penalty = errorRuleCount * ERROR_RULE_PENALTY + warningRuleCount * WARNING_RULE_PENALTY;\n const score = Math.max(0, Math.round(PERFECT_SCORE - penalty));\n return { score, label: getScoreLabel(score) };\n};\n","import type { AngularDoctorConfig, Diagnostic } from \"../types.js\";\n\nconst matchesGlob = (filePath: string, pattern: string): boolean => {\n const escapedPattern = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*\\*/g, \".*\")\n .replace(/\\*/g, \"[^/]*\");\n return new RegExp(`^${escapedPattern}$`).test(filePath);\n};\n\nexport const filterIgnoredDiagnostics = (\n diagnostics: Diagnostic[],\n config: AngularDoctorConfig,\n): Diagnostic[] => {\n const ignoredRules = new Set(config.ignore?.rules ?? []);\n const ignoredFilePatterns = config.ignore?.files ?? [];\n\n return diagnostics.filter((diagnostic) => {\n const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;\n if (ignoredRules.has(ruleKey)) return false;\n\n if (ignoredFilePatterns.some((pattern) => matchesGlob(diagnostic.filePath, pattern))) {\n return false;\n }\n\n return true;\n });\n};\n","import { SOURCE_FILE_PATTERN } from \"../constants.js\";\nimport type { AngularDoctorConfig, Diagnostic } from \"../types.js\";\nimport { filterIgnoredDiagnostics } from \"./filter-diagnostics.js\";\n\nexport const computeIncludePaths = (includePaths: string[]): string[] | undefined =>\n includePaths.length > 0\n ? includePaths.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath))\n : undefined;\n\nexport const combineDiagnostics = (\n lintDiagnostics: Diagnostic[],\n deadCodeDiagnostics: Diagnostic[],\n userConfig: AngularDoctorConfig | null,\n): Diagnostic[] => {\n const allDiagnostics = [...lintDiagnostics, ...deadCodeDiagnostics];\n return userConfig ? filterIgnoredDiagnostics(allDiagnostics, userConfig) : allDiagnostics;\n};\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport { GIT_LS_FILES_MAX_BUFFER_BYTES, SOURCE_FILE_PATTERN } from \"../constants.js\";\nimport type { AngularFramework, PackageJson, ProjectInfo, WorkspacePackage } from \"../types.js\";\n\nconst ANGULAR_FRAMEWORK_PACKAGES: Record<string, AngularFramework> = {\n \"@nrwl/angular\": \"nx\",\n \"@nx/angular\": \"nx\",\n \"@analogjs/platform\": \"analog\",\n \"@ionic/angular\": \"ionic\",\n \"@angular/ssr\": \"universal\",\n \"@nguniversal/express-engine\": \"universal\",\n};\n\nconst ANGULAR_FRAMEWORK_DISPLAY_NAMES: Record<AngularFramework, string> = {\n \"angular-cli\": \"Angular CLI\",\n nx: \"Nx\",\n analog: \"AnalogJS\",\n ionic: \"Ionic\",\n universal: \"Angular SSR\",\n unknown: \"Angular\",\n};\n\nexport const formatFrameworkName = (framework: AngularFramework): string =>\n ANGULAR_FRAMEWORK_DISPLAY_NAMES[framework];\n\nconst readPackageJson = (filePath: string): PackageJson => {\n try {\n const content = fs.readFileSync(filePath, \"utf-8\");\n return JSON.parse(content) as PackageJson;\n } catch {\n return {};\n }\n};\n\nconst collectAllDependencies = (packageJson: PackageJson): Record<string, string> => ({\n ...packageJson.peerDependencies,\n ...packageJson.dependencies,\n ...packageJson.devDependencies,\n});\n\nconst detectFramework = (dependencies: Record<string, string>): AngularFramework => {\n for (const [packageName, frameworkName] of Object.entries(ANGULAR_FRAMEWORK_PACKAGES)) {\n if (dependencies[packageName]) {\n return frameworkName;\n }\n }\n if (dependencies[\"@angular/cli\"] || dependencies[\"@angular-devkit/build-angular\"] || dependencies[\"@angular-devkit/core\"]) {\n return \"angular-cli\";\n }\n return \"unknown\";\n};\n\nconst detectAngularVersion = (dependencies: Record<string, string>): string | null =>\n dependencies[\"@angular/core\"] ?? null;\n\nconst detectAngularMajorVersion = (version: string | null): number | null => {\n if (!version) return null;\n const major = parseInt(version.match(/\\d+/)?.[0] ?? \"\", 10);\n return isNaN(major) ? null : major;\n};\n\nconst detectStandaloneComponents = (packageJson: PackageJson): boolean => {\n const deps = collectAllDependencies(packageJson);\n const angularVersion = deps[\"@angular/core\"];\n if (!angularVersion) return false;\n // Angular 14+ supports standalone components (standalone: true flag)\n // Angular 17+ makes standalone the default\n const majorVersion = parseInt(angularVersion.match(/\\d+/)?.[0] ?? \"\", 10);\n return !isNaN(majorVersion) && majorVersion >= 14;\n};\n\nconst detectNgRxPackages = (dependencies: Record<string, string>): boolean => {\n return Object.keys(dependencies).some(\n (dep) => dep.startsWith(\"@ngrx/\") || dep === \"@ngrx/store\",\n );\n};\n\nconst detectAngularMaterial = (dependencies: Record<string, string>): boolean => {\n return Object.keys(dependencies).includes(\"@angular/material\");\n};\n\nconst detectSignals = (angularMajorVersion: number | null): boolean => {\n // Angular Signals are available starting from Angular 17\n return angularMajorVersion !== null && angularMajorVersion >= 17;\n};\n\nconst countSourceFiles = (rootDirectory: string): number => {\n const result = spawnSync(\"git\", [\"ls-files\", \"--cached\", \"--others\", \"--exclude-standard\"], {\n cwd: rootDirectory,\n encoding: \"utf-8\",\n maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES,\n });\n\n if (result.error || result.status !== 0) {\n return 0;\n }\n\n return result.stdout\n .split(\"\\n\")\n .filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath)).length;\n};\n\nconst hasAngularDependency = (packageJson: PackageJson): boolean => {\n const allDependencies = collectAllDependencies(packageJson);\n return Boolean(allDependencies[\"@angular/core\"]);\n};\n\n/**\n * Walks up the directory tree from `directory` until it finds a `package.json`.\n * Returns the directory containing the package.json, or null if not found.\n */\nconst findNearestPackageJsonDir = (directory: string): string | null => {\n let current = directory;\n while (true) {\n if (fs.existsSync(path.join(current, \"package.json\"))) return current;\n const parent = path.dirname(current);\n if (parent === current) return null;\n current = parent;\n }\n};\n\nexport const discoverProject = (directory: string): ProjectInfo => {\n // First try the given directory, then walk up to find the nearest package.json\n const packageJsonDir = fs.existsSync(path.join(directory, \"package.json\"))\n ? directory\n : findNearestPackageJsonDir(directory);\n\n if (!packageJsonDir) {\n throw new Error(`No package.json found in ${directory} or any parent directory`);\n }\n\n const packageJson = readPackageJson(path.join(packageJsonDir, \"package.json\"));\n const allDeps = collectAllDependencies(packageJson);\n const angularVersion = detectAngularVersion(allDeps);\n const angularMajorVersion = detectAngularMajorVersion(angularVersion);\n const framework = detectFramework(allDeps);\n\n // tsconfig.json — check the project directory first, then the package.json directory\n const hasTypeScript =\n fs.existsSync(path.join(directory, \"tsconfig.json\")) ||\n fs.existsSync(path.join(packageJsonDir, \"tsconfig.json\"));\n\n const hasStandaloneComponents = detectStandaloneComponents(packageJson);\n const hasNgRx = detectNgRxPackages(allDeps);\n const hasAngularMaterial = detectAngularMaterial(allDeps);\n const hasSignals = detectSignals(angularMajorVersion);\n const sourceFileCount = countSourceFiles(directory);\n\n // Use the Angular project name from angular.json if possible, otherwise from package.json\n const angularJsonPath = path.join(packageJsonDir, \"angular.json\");\n let projectName = packageJson.name ?? path.basename(directory);\n if (packageJsonDir !== directory && fs.existsSync(angularJsonPath)) {\n // Use the directory name as a more meaningful project name for workspace sub-projects\n projectName = path.basename(directory);\n }\n\n return {\n rootDirectory: directory,\n projectName,\n angularVersion,\n angularMajorVersion,\n framework,\n hasTypeScript,\n hasStandaloneComponents,\n hasNgRx,\n hasAngularMaterial,\n hasSignals,\n sourceFileCount,\n };\n};\n\nconst parsePnpmWorkspacePatterns = (rootDirectory: string): string[] => {\n const workspacePath = path.join(rootDirectory, \"pnpm-workspace.yaml\");\n if (!fs.existsSync(workspacePath)) return [];\n\n const content = fs.readFileSync(workspacePath, \"utf-8\");\n const patterns: string[] = [];\n let isInsidePackagesBlock = false;\n\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (trimmed === \"packages:\") {\n isInsidePackagesBlock = true;\n continue;\n }\n if (isInsidePackagesBlock && trimmed.startsWith(\"-\")) {\n patterns.push(trimmed.replace(/^-\\s*/, \"\").replace(/[\"']/g, \"\"));\n } else if (isInsidePackagesBlock && trimmed.length > 0 && !trimmed.startsWith(\"#\")) {\n isInsidePackagesBlock = false;\n }\n }\n\n return patterns;\n};\n\nconst getWorkspacePatterns = (rootDirectory: string, packageJson: PackageJson): string[] => {\n const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);\n if (pnpmPatterns.length > 0) return pnpmPatterns;\n\n if (Array.isArray(packageJson.workspaces)) {\n return packageJson.workspaces;\n }\n\n if (packageJson.workspaces?.packages) {\n return packageJson.workspaces.packages;\n }\n\n return [];\n};\n\nconst resolveWorkspaceDirectories = (rootDirectory: string, pattern: string): string[] => {\n const cleanPattern = pattern.replace(/[\"']/g, \"\").replace(/\\/\\*\\*$/, \"/*\");\n\n if (!cleanPattern.includes(\"*\")) {\n const directoryPath = path.join(rootDirectory, cleanPattern);\n if (fs.existsSync(directoryPath) && fs.existsSync(path.join(directoryPath, \"package.json\"))) {\n return [directoryPath];\n }\n return [];\n }\n\n const wildcardIndex = cleanPattern.indexOf(\"*\");\n const baseDirectory = path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));\n const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);\n\n if (!fs.existsSync(baseDirectory) || !fs.statSync(baseDirectory).isDirectory()) {\n return [];\n }\n\n return fs\n .readdirSync(baseDirectory)\n .map((entry) => path.join(baseDirectory, entry, suffixAfterWildcard))\n .filter(\n (entryPath) =>\n fs.existsSync(entryPath) &&\n fs.statSync(entryPath).isDirectory() &&\n fs.existsSync(path.join(entryPath, \"package.json\")),\n );\n};\n\nexport const listWorkspacePackages = (rootDirectory: string): WorkspacePackage[] => {\n const packageJsonPath = path.join(rootDirectory, \"package.json\");\n if (!fs.existsSync(packageJsonPath)) return [];\n\n const packageJson = readPackageJson(packageJsonPath);\n const patterns = getWorkspacePatterns(rootDirectory, packageJson);\n if (patterns.length === 0) return [];\n\n const packages: WorkspacePackage[] = [];\n\n for (const pattern of patterns) {\n const directories = resolveWorkspaceDirectories(rootDirectory, pattern);\n for (const workspaceDirectory of directories) {\n const workspacePackageJson = readPackageJson(path.join(workspaceDirectory, \"package.json\"));\n\n if (!hasAngularDependency(workspacePackageJson)) continue;\n\n const name = workspacePackageJson.name ?? path.basename(workspaceDirectory);\n packages.push({ name, directory: workspaceDirectory });\n }\n }\n\n return packages;\n};\n\ninterface AngularWorkspaceProject {\n projectType?: string;\n root?: string;\n}\n\ninterface AngularWorkspace {\n version?: number;\n projects?: Record<string, AngularWorkspaceProject | string>;\n}\n\n/**\n * Reads `angular.json` (Angular CLI workspace config) and returns one\n * WorkspacePackage per project whose root directory contains an Angular dependency.\n */\nexport const listAngularWorkspaceProjects = (rootDirectory: string): WorkspacePackage[] => {\n const angularJsonPath = path.join(rootDirectory, \"angular.json\");\n if (!fs.existsSync(angularJsonPath)) return [];\n\n let workspace: AngularWorkspace;\n try {\n workspace = JSON.parse(fs.readFileSync(angularJsonPath, \"utf-8\")) as AngularWorkspace;\n } catch {\n return [];\n }\n\n if (!workspace.projects || typeof workspace.projects !== \"object\") return [];\n\n const packages: WorkspacePackage[] = [];\n\n for (const [name, projectConfig] of Object.entries(workspace.projects)) {\n // Older angular.json formats store the root as a plain string\n const root =\n typeof projectConfig === \"string\"\n ? projectConfig\n : (projectConfig.root ?? \"\");\n\n const projectDirectory = root ? path.resolve(rootDirectory, root) : rootDirectory;\n\n // Only include directories that actually exist\n if (!fs.existsSync(projectDirectory) || !fs.statSync(projectDirectory).isDirectory()) {\n continue;\n }\n\n packages.push({ name, directory: projectDirectory });\n }\n\n return packages;\n};\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { AngularDoctorConfig } from \"../types.js\";\n\nconst CONFIG_FILENAME = \"angular-doctor.config.json\";\nconst PACKAGE_JSON_CONFIG_KEY = \"angularDoctor\";\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nexport const loadConfig = (rootDirectory: string): AngularDoctorConfig | null => {\n const configFilePath = path.join(rootDirectory, CONFIG_FILENAME);\n\n if (fs.existsSync(configFilePath)) {\n try {\n const fileContent = fs.readFileSync(configFilePath, \"utf-8\");\n const parsed: unknown = JSON.parse(fileContent);\n if (!isPlainObject(parsed)) {\n console.warn(`Warning: ${CONFIG_FILENAME} must be a JSON object, ignoring.`);\n return null;\n }\n return parsed as AngularDoctorConfig;\n } catch (error) {\n console.warn(\n `Warning: Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`,\n );\n return null;\n }\n }\n\n const packageJsonPath = path.join(rootDirectory, \"package.json\");\n if (fs.existsSync(packageJsonPath)) {\n try {\n const fileContent = fs.readFileSync(packageJsonPath, \"utf-8\");\n const packageJson = JSON.parse(fileContent) as Record<string, unknown>;\n const embeddedConfig = packageJson[PACKAGE_JSON_CONFIG_KEY];\n if (isPlainObject(embeddedConfig)) {\n return embeddedConfig as AngularDoctorConfig;\n }\n } catch {\n return null;\n }\n }\n\n return null;\n};\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { ESLint, type Linter } from \"eslint\";\nimport angularEslintPlugin from \"@angular-eslint/eslint-plugin\";\nimport tsEslint from \"typescript-eslint\";\nimport type { Diagnostic, PackageJson } from \"../types.js\";\n\n// Extended rule category mapping (45+ rules)\nconst RULE_CATEGORY_MAP: Record<string, string> = {\n // Angular component best practices\n \"@angular-eslint/component-class-suffix\": \"Components\",\n \"@angular-eslint/component-max-inline-declarations\": \"Performance\",\n \"@angular-eslint/component-selector\": \"Components\",\n \"@angular-eslint/directive-class-suffix\": \"Components\",\n \"@angular-eslint/directive-selector\": \"Components\",\n \"@angular-eslint/pipe-prefix\": \"Components\",\n \"@angular-eslint/use-pipe-transform-interface\": \"Components\",\n \"@angular-eslint/no-empty-lifecycle-method\": \"Components\",\n \"@angular-eslint/use-lifecycle-interface\": \"Components\",\n \"@angular-eslint/consistent-component-styles\": \"Components\",\n \"@angular-eslint/sort-lifecycle-methods\": \"Components\",\n \"@angular-eslint/use-component-selector\": \"Components\",\n\n // Angular performance\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"Performance\",\n \"@angular-eslint/no-output-native\": \"Performance\",\n \"@angular-eslint/no-pipe-impure\": \"Performance\",\n\n // Angular architecture / correctness\n \"@angular-eslint/no-conflicting-lifecycle\": \"Correctness\",\n \"@angular-eslint/contextual-lifecycle\": \"Correctness\",\n \"@angular-eslint/contextual-decorator\": \"Correctness\",\n \"@angular-eslint/no-async-lifecycle-method\": \"Correctness\",\n \"@angular-eslint/no-duplicates-in-metadata-arrays\": \"Correctness\",\n \"@angular-eslint/no-lifecycle-call\": \"Correctness\",\n \"@angular-eslint/require-lifecycle-on-prototype\": \"Correctness\",\n \"@angular-eslint/no-attribute-decorator\": \"Architecture\",\n \"@angular-eslint/no-forward-ref\": \"Architecture\",\n \"@angular-eslint/no-input-rename\": \"Architecture\",\n \"@angular-eslint/no-output-rename\": \"Architecture\",\n \"@angular-eslint/no-inputs-metadata-property\": \"Architecture\",\n \"@angular-eslint/no-outputs-metadata-property\": \"Architecture\",\n \"@angular-eslint/no-queries-metadata-property\": \"Architecture\",\n \"@angular-eslint/prefer-standalone\": \"Architecture\",\n \"@angular-eslint/prefer-host-metadata-property\": \"Architecture\",\n \"@angular-eslint/prefer-inject\": \"Architecture\",\n \"@angular-eslint/prefer-output-emitter-ref\": \"Architecture\",\n \"@angular-eslint/prefer-output-readonly\": \"Architecture\",\n \"@angular-eslint/use-component-view-encapsulation\": \"Architecture\",\n \"@angular-eslint/use-injectable-provided-in\": \"Architecture\",\n \"@angular-eslint/no-input-prefix\": \"Architecture\",\n \"@angular-eslint/no-output-on-prefix\": \"Architecture\",\n\n // Angular security\n \"@angular-eslint/relative-url-prefix\": \"Security\",\n\n // Angular signals (Angular 17+)\n \"@angular-eslint/prefer-signals\": \"Signals\",\n \"@angular-eslint/prefer-signal-model\": \"Signals\",\n \"@angular-eslint/no-uncalled-signals\": \"Signals\",\n\n // Angular templates/accessibility (when @angular-eslint/eslint-plugin-template available)\n \"@angular-eslint/template/accessibility\": \"Accessibility\",\n \"@angular-eslint/template/alt-text\": \"Accessibility\",\n \"@angular-eslint/template/click-events-have-key-events\": \"Accessibility\",\n \"@angular-eslint/template/control-events-have-key-events\": \"Accessibility\",\n \"@angular-eslint/template/elements-have-content\": \"Accessibility\",\n \"@angular-eslint/template/interactive-supports-focus\": \"Accessibility\",\n \"@angular-eslint/template/mouse-events-have-key-events\": \"Accessibility\",\n \"@angular-eslint/template/no-any\": \"Accessibility\",\n \"@angular-eslint/template/table-scope\": \"Accessibility\",\n \"@angular-eslint/template/valid-aria\": \"Accessibility\",\n\n // NgRx patterns (conditional on @ngrx packages)\n \"@ngrx/contextual-action-creator\": \"NgRx\",\n \"@ngrx/no-cyclic-action-creators\": \"NgRx\",\n \"@ngrx/no-discrete-actions\": \"NgRx\",\n \"@ngrx/no-effect-decorator\": \"NgRx\",\n \"@ngrx/no-effect-decorator-and-creator\": \"NgRx\",\n \"@ngrx/no-multiple-actions-in-effects\": \"NgRx\",\n \"@ngrx/no-reordering-in-effect-reducers\": \"NgRx\",\n \"@ngrx/no-typed-global-store\": \"NgRx\",\n \"@ngrx/on-function-explicit-return-type\": \"NgRx\",\n \"@ngrx/prefix-selectors-with-namespace\": \"NgRx\",\n \"@ngrx/require-middleware-selector\": \"NgRx\",\n \"@ngrx/select-style\": \"NgRx\",\n \"@ngrx/use-consumer-selector\": \"NgRx\",\n\n // Angular Material patterns (conditional on @angular/material)\n \"@angular/material/prefix-selector\": \"Material\",\n \"@angular/material/no-conflicting-mixins\": \"Material\",\n\n // TypeScript quality\n \"@typescript-eslint/no-explicit-any\": \"TypeScript\",\n \"@typescript-eslint/no-unused-vars\": \"Dead Code\",\n \"@typescript-eslint/sort-keys\": \"TypeScript\",\n};\n\n// Rule severity mapping\nconst RULE_SEVERITY_MAP: Record<string, \"error\" | \"warning\"> = {\n // Errors (serious issues)\n \"@angular-eslint/no-conflicting-lifecycle\": \"error\",\n \"@angular-eslint/contextual-lifecycle\": \"error\",\n \"@angular-eslint/use-pipe-transform-interface\": \"error\",\n \"@angular-eslint/no-output-native\": \"error\",\n \"@angular-eslint/no-async-lifecycle-method\": \"error\",\n \"@angular-eslint/no-lifecycle-call\": \"error\",\n \"@angular-eslint/no-duplicates-in-metadata-arrays\": \"error\",\n \"@angular-eslint/require-lifecycle-on-prototype\": \"error\",\n \"@angular-eslint/relative-url-prefix\": \"error\",\n \"@angular-eslint/contextual-decorator\": \"error\",\n \"@angular-eslint/no-uncalled-signals\": \"error\",\n\n // Warnings\n \"@angular-eslint/component-class-suffix\": \"warning\",\n \"@angular-eslint/directive-class-suffix\": \"warning\",\n \"@angular-eslint/pipe-prefix\": \"warning\",\n \"@angular-eslint/no-empty-lifecycle-method\": \"warning\",\n \"@angular-eslint/use-lifecycle-interface\": \"warning\",\n \"@angular-eslint/consistent-component-styles\": \"warning\",\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"warning\",\n \"@angular-eslint/no-forward-ref\": \"warning\",\n \"@angular-eslint/no-input-rename\": \"warning\",\n \"@angular-eslint/no-output-rename\": \"warning\",\n \"@angular-eslint/no-inputs-metadata-property\": \"warning\",\n \"@angular-eslint/no-outputs-metadata-property\": \"warning\",\n \"@angular-eslint/prefer-standalone\": \"warning\",\n \"@angular-eslint/component-selector\": \"warning\",\n \"@angular-eslint/directive-selector\": \"warning\",\n \"@angular-eslint/no-pipe-impure\": \"warning\",\n \"@angular-eslint/no-attribute-decorator\": \"warning\",\n \"@angular-eslint/no-queries-metadata-property\": \"warning\",\n \"@angular-eslint/prefer-host-metadata-property\": \"warning\",\n \"@angular-eslint/prefer-inject\": \"warning\",\n \"@angular-eslint/prefer-output-emitter-ref\": \"warning\",\n \"@angular-eslint/prefer-output-readonly\": \"warning\",\n \"@angular-eslint/use-component-selector\": \"warning\",\n \"@angular-eslint/use-component-view-encapsulation\": \"warning\",\n \"@angular-eslint/use-injectable-provided-in\": \"warning\",\n \"@angular-eslint/component-max-inline-declarations\": \"warning\",\n \"@angular-eslint/sort-lifecycle-methods\": \"warning\",\n \"@angular-eslint/no-input-prefix\": \"warning\",\n \"@angular-eslint/no-output-on-prefix\": \"warning\",\n \"@angular-eslint/prefer-signals\": \"warning\",\n \"@angular-eslint/prefer-signal-model\": \"warning\",\n\n // TypeScript\n \"@typescript-eslint/no-explicit-any\": \"warning\",\n \"@typescript-eslint/no-unused-vars\": \"warning\",\n \"@typescript-eslint/sort-keys\": \"warning\",\n\n // NgRx (conditional - set to ignore if @ngrx not present)\n \"@ngrx/contextual-action-creator\": \"warning\",\n \"@ngrx/no-cyclic-action-creators\": \"error\",\n \"@ngrx/no-discrete-actions\": \"warning\",\n \"@ngrx/no-effect-decorator\": \"warning\",\n \"@ngrx/no-effect-decorator-and-creator\": \"error\",\n \"@ngrx/no-multiple-actions-in-effects\": \"error\",\n \"@ngrx/no-reordering-in-effect-reducers\": \"error\",\n \"@ngrx/no-typed-global-store\": \"error\",\n \"@ngrx/on-function-explicit-return-type\": \"warning\",\n \"@ngrx/prefix-selectors-with-namespace\": \"warning\",\n \"@ngrx/require-middleware-selector\": \"error\",\n \"@ngrx/select-style\": \"warning\",\n \"@ngrx/use-consumer-selector\": \"warning\",\n\n // Angular Material\n \"@angular/material/prefix-selector\": \"warning\",\n \"@angular/material/no-conflicting-mixins\": \"error\",\n};\n\n// Human-readable messages and help text\nconst RULE_MESSAGE_MAP: Record<string, string> = {\n \"@angular-eslint/component-class-suffix\":\n \"Component class should end with 'Component'\",\n \"@angular-eslint/directive-class-suffix\":\n \"Directive class should end with 'Directive'\",\n \"@angular-eslint/pipe-prefix\": \"Pipe name should have a consistent prefix\",\n \"@angular-eslint/use-pipe-transform-interface\":\n \"Pipe class must implement PipeTransform interface\",\n \"@angular-eslint/no-empty-lifecycle-method\": \"Remove empty lifecycle methods\",\n \"@angular-eslint/use-lifecycle-interface\":\n \"Implement the lifecycle interface for lifecycle hooks\",\n \"@angular-eslint/consistent-component-styles\":\n \"Use consistent styles type in component decorator\",\n \"@angular-eslint/prefer-on-push-component-change-detection\":\n \"Use OnPush change detection for better performance\",\n \"@angular-eslint/no-output-native\":\n \"Avoid shadowing native DOM events in output names\",\n \"@angular-eslint/no-conflicting-lifecycle\":\n \"Lifecycle hooks DoCheck and OnChanges cannot be used together\",\n \"@angular-eslint/contextual-lifecycle\":\n \"Lifecycle hook is not available in this context\",\n \"@angular-eslint/contextual-decorator\":\n \"Use contextual decorator to specify injection context\",\n \"@angular-eslint/no-forward-ref\":\n \"Avoid using forwardRef — restructure to avoid circular dependency\",\n \"@angular-eslint/no-input-rename\":\n \"Avoid renaming directive inputs — use the property name as the binding name\",\n \"@angular-eslint/no-output-rename\":\n \"Avoid renaming directive outputs — use the property name as the binding name\",\n \"@angular-eslint/no-inputs-metadata-property\":\n \"Use @Input() decorator instead of inputs metadata property\",\n \"@angular-eslint/no-outputs-metadata-property\":\n \"Use @Output() decorator instead of outputs metadata property\",\n \"@angular-eslint/prefer-standalone\":\n \"Prefer standalone components over NgModule-based components\",\n \"@angular-eslint/component-selector\":\n \"Component selector should follow naming convention\",\n \"@angular-eslint/directive-selector\":\n \"Directive selector should follow naming convention\",\n \"@angular-eslint/no-pipe-impure\":\n \"Avoid impure pipes — they run on every change detection cycle\",\n \"@angular-eslint/no-async-lifecycle-method\":\n \"Avoid async lifecycle methods — use signals instead\",\n \"@angular-eslint/no-duplicates-in-metadata-arrays\":\n \"Remove duplicate entries in decorator metadata arrays\",\n \"@angular-eslint/no-lifecycle-call\":\n \"Don't call lifecycle method directly — let Angular call them\",\n \"@angular-eslint/require-lifecycle-on-prototype\":\n \"Lifecycle methods should be declared on prototype\",\n \"@angular-eslint/no-attribute-decorator\":\n \"Avoid @Attribute() — use @Input() for property bindings\",\n \"@angular-eslint/no-queries-metadata-property\":\n \"Use decorator-based queries instead of metadata properties\",\n \"@angular-eslint/prefer-host-metadata-property\":\n \"Prefer host metadata property over @Host decorator\",\n \"@angular-eslint/prefer-inject\":\n \"Prefer inject() function over constructor dependency injection\",\n \"@angular-eslint/prefer-output-emitter-ref\":\n \"Use EventEmitter with Output instead of Subject\",\n \"@angular-eslint/prefer-output-readonly\":\n \"Mark outputs as readonly when possible\",\n \"@angular-eslint/use-component-selector\":\n \"Components must have selector for proper encapsulation\",\n \"@angular-eslint/use-component-view-encapsulation\":\n \"Specify view encapsulation strategy explicitly\",\n \"@angular-eslint/use-injectable-provided-in\":\n \"Specify providedIn scope for @Injectable()\",\n \"@angular-eslint/component-max-inline-declarations\":\n \"Too many inline declarations in component — extract to separate files\",\n \"@angular-eslint/sort-lifecycle-methods\":\n \"Lifecycle methods should be declared in correct order\",\n \"@angular-eslint/no-input-prefix\":\n \"Avoid prefix for input property names\",\n \"@angular-eslint/no-output-on-prefix\":\n \"Avoid 'on' prefix for output event names\",\n \"@angular-eslint/relative-url-prefix\":\n \"Use relative URL prefixes for better security\",\n \"@angular-eslint/prefer-signals\":\n \"Prefer Angular signals over other reactive patterns\",\n \"@angular-eslint/prefer-signal-model\":\n \"Prefer signal-based model for component state\",\n \"@angular-eslint/no-uncalled-signals\":\n \"Signal getters must be called to access value\",\n \"@typescript-eslint/no-explicit-any\":\n \"Avoid 'any' type — use specific types for better type safety\",\n \"@typescript-eslint/no-unused-vars\": \"Remove unused variable declaration\",\n \"@typescript-eslint/sort-keys\": \"Sort object keys consistently\",\n // NgRx rules\n \"@ngrx/contextual-action-creator\":\n \"Use contextual action creators for typed actions\",\n \"@ngrx/no-cyclic-action-creators\":\n \"Action creators should not reference each other cyclically\",\n \"@ngrx/no-discrete-actions\":\n \"Use discrete actions instead of broad action types\",\n \"@ngrx/no-effect-decorator\":\n \"Consider using functional effects instead of @Effect decorator\",\n \"@ngrx/no-effect-decorator-and-creator\":\n \"Don't use both @Effect decorator and createEffect function\",\n \"@ngrx/no-multiple-actions-in-effects\":\n \"Effects should dispatch a single action or none\",\n \"@ngrx/no-reordering-in-effect-reducers\":\n \"Don't reorder actions in effect reducers\",\n \"@ngrx/no-typed-global-store\":\n \"Use typed GlobalStore for better type safety\",\n \"@ngrx/on-function-explicit-return-type\":\n \"Specify explicit return type for on() reducer functions\",\n \"@ngrx/prefix-selectors-with-namespace\":\n \"Prefix selectors with feature namespace\",\n \"@ngrx/require-middleware-selector\":\n \"Middleware must have selector for proper scoping\",\n \"@ngrx/select-style\":\n \"Prefer selector functions over props in select\",\n \"@ngrx/use-consumer-selector\":\n \"Use useSelector with selector function for proper memoization\",\n // Angular Material rules\n \"@angular/material/prefix-selector\":\n \"Material components should use proper selector prefix\",\n \"@angular/material/no-conflicting-mixins\":\n \"Avoid conflicting mixins in Material components\",\n};\n\nconst RULE_HELP_MAP: Record<string, string> = {\n \"@angular-eslint/component-class-suffix\":\n \"Add 'Component' suffix: `export class UserProfileComponent { }`\",\n \"@angular-eslint/directive-class-suffix\":\n \"Add 'Directive' suffix: `export class HighlightDirective { }`\",\n \"@angular-eslint/use-pipe-transform-interface\":\n \"Implement PipeTransform: `export class MyPipe implements PipeTransform { transform(value: unknown) { } }`\",\n \"@angular-eslint/no-empty-lifecycle-method\":\n \"Remove the empty lifecycle method or add logic to it\",\n \"@angular-eslint/use-lifecycle-interface\":\n \"Add the interface: `export class MyComponent implements OnInit, OnDestroy { }`\",\n \"@angular-eslint/prefer-on-push-component-change-detection\":\n \"Add to decorator: `@Component({ changeDetection: ChangeDetectionStrategy.OnPush })`\",\n \"@angular-eslint/no-output-native\":\n \"Rename the output: use a descriptive name like `(valueChange)` instead of `(click)` or `(change)`\",\n \"@angular-eslint/no-forward-ref\":\n \"Restructure your code to avoid circular dependencies, or use `inject()` with a lazy function\",\n \"@angular-eslint/no-input-rename\":\n \"Remove the alias: `@Input() myProp: string` instead of `@Input('myAlias') myProp: string`\",\n \"@angular-eslint/no-output-rename\":\n \"Remove the alias: `@Output() myEvent = new EventEmitter()` instead of aliased version\",\n \"@angular-eslint/no-inputs-metadata-property\":\n \"Use `@Input() myProp: string` decorator on the property instead of `inputs: ['myProp']` in the decorator metadata\",\n \"@angular-eslint/no-outputs-metadata-property\":\n \"Use `@Output() myEvent = new EventEmitter()` instead of `outputs: ['myEvent']` in the decorator metadata\",\n \"@angular-eslint/prefer-standalone\":\n \"Add `standalone: true` to component: `@Component({ standalone: true, ... })`\",\n \"@typescript-eslint/no-explicit-any\":\n \"Replace `any` with a specific type or `unknown` if the type is truly unknown\",\n \"@typescript-eslint/no-unused-vars\":\n \"Remove the unused variable or prefix with `_` to indicate it's intentionally unused\",\n \"@typescript-eslint/sort-keys\":\n \"Sort object keys alphabetically or by a consistent pattern\",\n \"@angular-eslint/prefer-inject\":\n \"Use `inject(MyService)` instead of constructor injection: `constructor(private myService: MyService) {}`\",\n \"@angular-eslint/no-pipe-impure\":\n \"Remove `pure: false` from pipe decorator or refactor to use a service\",\n \"@angular-eslint/prefer-signals\":\n \"Replace observables with signals for simpler reactive state: `count = signal(0)`\",\n \"@angular-eslint/prefer-signal-model\":\n \"Use signal-based input/output model: `count = model(0)` instead of `@Input() count: number`\",\n \"@angular-eslint/no-uncalled-signals\":\n \"Call the signal getter to access its value: `this.count()` not `this.count`\",\n \"@angular-eslint/component-max-inline-declarations\":\n \"Move templates/styles to separate files or use inline with caution — consider extracting when > 3 inline declarations\",\n \"@angular-eslint/relative-url-prefix\":\n \"Use relative URLs (no leading slash) or ensure absolute URLs are intentional for security\",\n \"@ngrx/contextual-action-creator\":\n \"Use `createActionGroup` or `createAction` with props for type-safe actions\",\n \"@ngrx/no-multiple-actions-in-effects\":\n \"Split effect into multiple effects or use `mergeMap` with individual actions\",\n};\n\ninterface PackagePresence {\n hasNgRx: boolean;\n hasAngularMaterial: boolean;\n hasSignals: boolean;\n}\n\nconst detectPackagePresence = (packageJson: PackageJson): PackagePresence => {\n const deps = {\n ...packageJson.dependencies,\n ...packageJson.devDependencies,\n ...packageJson.peerDependencies,\n };\n\n return {\n hasNgRx: Object.keys(deps).some(\n (dep) => dep.startsWith(\"@ngrx/\") && !dep.includes(\"store\"),\n ) || Object.keys(deps).includes(\"@ngrx/store\"),\n hasAngularMaterial: Object.keys(deps).includes(\"@angular/material\"),\n hasSignals: true, // Angular 17+ signals are built-in, always available\n };\n};\n\nconst buildEslintConfig = (\n hasTypeScript: boolean,\n tsconfigPath: string | null,\n useTypeAware: boolean,\n packagePresence: PackagePresence,\n): Linter.Config[] => {\n const languageOptions: Linter.Config[\"languageOptions\"] = {\n parser: tsEslint.parser as Linter.Parser,\n parserOptions: {\n ecmaVersion: \"latest\",\n sourceType: \"module\",\n ...(hasTypeScript && tsconfigPath && useTypeAware\n ? { project: tsconfigPath }\n : {}),\n },\n };\n\n // Core Angular rules (always enabled)\n const angularRules: Linter.RulesRecord = {\n // Component best practices\n \"@angular-eslint/component-class-suffix\": \"warn\",\n \"@angular-eslint/directive-class-suffix\": \"warn\",\n \"@angular-eslint/pipe-prefix\": \"warn\",\n \"@angular-eslint/no-empty-lifecycle-method\": \"warn\",\n \"@angular-eslint/use-lifecycle-interface\": \"warn\",\n \"@angular-eslint/consistent-component-styles\": \"warn\",\n \"@angular-eslint/sort-lifecycle-methods\": \"warn\",\n \"@angular-eslint/use-component-selector\": \"warn\",\n \"@angular-eslint/component-selector\": \"warn\",\n \"@angular-eslint/directive-selector\": \"warn\",\n\n // Performance\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"warn\",\n \"@angular-eslint/no-output-native\": \"error\",\n \"@angular-eslint/no-pipe-impure\": \"warn\",\n \"@angular-eslint/component-max-inline-declarations\": \"warn\",\n\n // Correctness\n \"@angular-eslint/use-pipe-transform-interface\": \"error\",\n \"@angular-eslint/no-conflicting-lifecycle\": \"error\",\n \"@angular-eslint/contextual-lifecycle\": \"error\",\n \"@angular-eslint/contextual-decorator\": \"error\",\n \"@angular-eslint/no-async-lifecycle-method\": \"error\",\n \"@angular-eslint/no-duplicates-in-metadata-arrays\": \"error\",\n \"@angular-eslint/no-lifecycle-call\": \"error\",\n \"@angular-eslint/require-lifecycle-on-prototype\": \"error\",\n\n // Architecture\n \"@angular-eslint/no-forward-ref\": \"warn\",\n \"@angular-eslint/no-input-rename\": \"warn\",\n \"@angular-eslint/no-output-rename\": \"warn\",\n \"@angular-eslint/no-inputs-metadata-property\": \"warn\",\n \"@angular-eslint/no-outputs-metadata-property\": \"warn\",\n \"@angular-eslint/no-queries-metadata-property\": \"warn\",\n \"@angular-eslint/prefer-standalone\": \"warn\",\n \"@angular-eslint/prefer-host-metadata-property\": \"warn\",\n \"@angular-eslint/prefer-inject\": \"warn\",\n \"@angular-eslint/prefer-output-emitter-ref\": \"warn\",\n \"@angular-eslint/prefer-output-readonly\": \"warn\",\n \"@angular-eslint/use-component-view-encapsulation\": \"warn\",\n \"@angular-eslint/use-injectable-provided-in\": \"warn\",\n \"@angular-eslint/no-attribute-decorator\": \"warn\",\n \"@angular-eslint/no-input-prefix\": \"warn\",\n \"@angular-eslint/no-output-on-prefix\": \"warn\",\n\n // Security\n \"@angular-eslint/relative-url-prefix\": \"error\",\n };\n\n // Signals rules (Angular 17+ only - conditional)\n const signalsRules: Linter.RulesRecord = packagePresence.hasSignals\n ? {\n \"@angular-eslint/prefer-signals\": \"warn\",\n \"@angular-eslint/prefer-signal-model\": \"warn\",\n \"@angular-eslint/no-uncalled-signals\": \"error\",\n }\n : {};\n\n // TypeScript rules\n const tsRules: Linter.RulesRecord = {\n \"@typescript-eslint/no-explicit-any\": \"warn\",\n \"@typescript-eslint/no-unused-vars\": \"warn\",\n \"@typescript-eslint/sort-keys\": \"warn\",\n };\n\n // NgRx rules (conditional - only if @ngrx packages present)\n const ngrxRules: Linter.RulesRecord = packagePresence.hasNgRx\n ? {\n \"@ngrx/contextual-action-creator\": \"warn\",\n \"@ngrx/no-cyclic-action-creators\": \"error\",\n \"@ngrx/no-discrete-actions\": \"warn\",\n \"@ngrx/no-effect-decorator\": \"warn\",\n \"@ngrx/no-effect-decorator-and-creator\": \"error\",\n \"@ngrx/no-multiple-actions-in-effects\": \"error\",\n \"@ngrx/no-reordering-in-effect-reducers\": \"error\",\n \"@ngrx/no-typed-global-store\": \"error\",\n \"@ngrx/on-function-explicit-return-type\": \"warn\",\n \"@ngrx/prefix-selectors-with-namespace\": \"warn\",\n \"@ngrx/require-middleware-selector\": \"error\",\n \"@ngrx/select-style\": \"warn\",\n \"@ngrx/use-consumer-selector\": \"warn\",\n }\n : {};\n\n // Angular Material rules (conditional - only if @angular/material present)\n const materialRules: Linter.RulesRecord = packagePresence.hasAngularMaterial\n ? {\n \"@angular/material/prefix-selector\": \"warn\",\n \"@angular/material/no-conflicting-mixins\": \"error\",\n }\n : {};\n\n return [\n {\n files: [\"**/*.ts\"],\n plugins: {\n \"@angular-eslint\": angularEslintPlugin as unknown as ESLint.Plugin,\n \"@typescript-eslint\": tsEslint.plugin as unknown as ESLint.Plugin,\n },\n languageOptions,\n rules: {\n ...angularRules,\n ...tsRules,\n ...signalsRules,\n ...ngrxRules,\n ...materialRules,\n },\n },\n ];\n};\n\nconst mapEslintSeverity = (\n severity: number,\n ruleId: string | null,\n): \"error\" | \"warning\" => {\n if (ruleId && RULE_SEVERITY_MAP[ruleId]) {\n return RULE_SEVERITY_MAP[ruleId];\n }\n return severity === 2 ? \"error\" : \"warning\";\n};\n\nconst resolveDiagnosticCategory = (ruleId: string): string =>\n RULE_CATEGORY_MAP[ruleId] ?? \"Other\";\n\nconst resolveMessage = (ruleId: string, defaultMessage: string): string =>\n RULE_MESSAGE_MAP[ruleId] ?? defaultMessage;\n\nconst resolveHelp = (ruleId: string): string => RULE_HELP_MAP[ruleId] ?? \"\";\n\nconst parsePluginAndRule = (\n ruleId: string,\n): { plugin: string; rule: string } => {\n // e.g. \"@angular-eslint/component-class-suffix\" -> plugin: \"@angular-eslint\", rule: \"component-class-suffix\"\n // e.g. \"@typescript-eslint/no-explicit-any\" -> plugin: \"@typescript-eslint\", rule: \"no-explicit-any\"\n const match = ruleId.match(/^(@[^/]+\\/[^/]+|[^/]+)\\/(.+)$/);\n if (match) {\n return { plugin: match[1], rule: match[2] };\n }\n return { plugin: \"eslint\", rule: ruleId };\n};\n\nexport interface FrameworkInfo {\n hasNgRx: boolean;\n hasAngularMaterial: boolean;\n hasSignals: boolean;\n}\n\nexport interface LintError {\n message: string;\n stack?: string;\n filePath?: string;\n}\n\nexport interface RunEslintResult {\n diagnostics: Diagnostic[];\n errors: LintError[];\n}\n\nexport const runEslint = async (\n rootDirectory: string,\n hasTypeScript: boolean,\n includePaths?: string[],\n options?: { useTypeAware?: boolean; frameworkInfo?: FrameworkInfo },\n): Promise<RunEslintResult> => {\n if (includePaths !== undefined && includePaths.length === 0) {\n return { diagnostics: [], errors: [] };\n }\n\n const tsconfigPath = hasTypeScript\n ? path.join(rootDirectory, \"tsconfig.json\")\n : null;\n\n // Use provided framework info or detect if not provided\n let packagePresence: PackagePresence;\n if (options?.frameworkInfo) {\n // Use the framework detection info passed from scan.ts (via discover-project)\n packagePresence = {\n hasNgRx: options.frameworkInfo.hasNgRx,\n hasAngularMaterial: options.frameworkInfo.hasAngularMaterial,\n hasSignals: options.frameworkInfo.hasSignals,\n };\n } else {\n // Fallback: detect package presence from package.json\n packagePresence = {\n hasNgRx: false,\n hasAngularMaterial: false,\n hasSignals: true,\n };\n try {\n const packageJsonPath = path.join(rootDirectory, \"package.json\");\n if (fs.existsSync(packageJsonPath)) {\n const packageJsonContent = fs.readFileSync(packageJsonPath, \"utf-8\");\n const packageJson = JSON.parse(packageJsonContent) as PackageJson;\n packagePresence = detectPackagePresence(packageJson);\n }\n } catch {\n // If we can't read package.json, default to no conditional rules\n }\n }\n\n const cacheRoot = path.join(\n rootDirectory,\n \"node_modules\",\n \".cache\",\n \"angular-doctor\",\n );\n fs.mkdirSync(cacheRoot, { recursive: true });\n const eslint = new ESLint({\n cwd: rootDirectory,\n overrideConfigFile: null,\n overrideConfig: buildEslintConfig(\n hasTypeScript,\n tsconfigPath && fs.existsSync(tsconfigPath) ? tsconfigPath : null,\n options?.useTypeAware ?? true,\n packagePresence,\n ),\n ignore: true,\n cache: true,\n cacheLocation: path.join(cacheRoot, \".eslintcache\"),\n });\n\n const patterns = includePaths ?? [\"**/*.ts\"];\n\n let results: ESLint.LintResult[];\n const errors: LintError[] = [];\n try {\n results = await eslint.lintFiles(patterns);\n } catch (error) {\n // Capture parse errors and other ESLint failures as diagnostics\n const errorMessage = error instanceof Error ? error.message : String(error);\n const errorStack = error instanceof Error ? error.stack : undefined;\n\n // Try to extract file path from error message\n const filePathMatch = errorMessage.match(/^(.+?):\\s* /);\n const errorFilePath = filePathMatch ? path.relative(rootDirectory, filePathMatch[1]) : undefined;\n\n errors.push({\n message: errorMessage,\n stack: errorStack,\n filePath: errorFilePath,\n });\n\n // Create a diagnostic for the parse error\n const parseErrorDiagnostic: Diagnostic = {\n filePath: errorFilePath ?? \"unknown\",\n plugin: \"eslint\",\n rule: \"parse-error\",\n severity: \"error\",\n message: errorMessage,\n help: \"Fix the syntax error in this file. ESLint could not parse the file.\",\n line: 0,\n column: 0,\n category: \"Parse Error\",\n };\n\n return { diagnostics: [parseErrorDiagnostic], errors };\n }\n\n const diagnostics: Diagnostic[] = [];\n\n for (const result of results) {\n for (const message of result.messages) {\n if (!message.ruleId) continue;\n const { plugin, rule } = parsePluginAndRule(message.ruleId);\n const ruleKey = message.ruleId;\n\n diagnostics.push({\n filePath: path.relative(rootDirectory, result.filePath),\n plugin,\n rule,\n severity: mapEslintSeverity(message.severity, ruleKey),\n message: resolveMessage(ruleKey, message.message),\n help: resolveHelp(ruleKey),\n line: message.line ?? 0,\n column: message.column ?? 0,\n category: resolveDiagnosticCategory(ruleKey),\n });\n }\n }\n\n return { diagnostics, errors };\n};\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { main } from \"knip\";\nimport { createOptions } from \"knip/session\";\nimport { MAX_KNIP_RETRIES } from \"../constants.js\";\nimport type { Diagnostic, KnipIssueRecords, KnipResults } from \"../types.js\";\n\nconst KNIP_CATEGORY_MAP: Record<string, string> = {\n files: \"Dead Code\",\n exports: \"Dead Code\",\n types: \"Dead Code\",\n duplicates: \"Dead Code\",\n};\n\nconst KNIP_MESSAGE_MAP: Record<string, string> = {\n files: \"Unused file\",\n exports: \"Unused export\",\n types: \"Unused type\",\n duplicates: \"Duplicate export\",\n};\n\nconst KNIP_SEVERITY_MAP: Record<string, \"error\" | \"warning\"> = {\n files: \"warning\",\n exports: \"warning\",\n types: \"warning\",\n duplicates: \"warning\",\n};\n\nconst collectIssueRecords = (\n records: KnipIssueRecords,\n issueType: string,\n rootDirectory: string,\n): Diagnostic[] => {\n const diagnostics: Diagnostic[] = [];\n\n for (const issues of Object.values(records)) {\n for (const issue of Object.values(issues)) {\n const filePath = path.relative(rootDirectory, issue.filePath);\n // Skip issues for files outside the rootDirectory scope\n if (filePath.startsWith(\"..\")) continue;\n diagnostics.push({\n filePath,\n plugin: \"knip\",\n rule: issueType,\n severity: KNIP_SEVERITY_MAP[issueType] ?? \"warning\",\n message: `${KNIP_MESSAGE_MAP[issueType]}: ${issue.symbol}`,\n help: \"\",\n line: 0,\n column: 0,\n category: KNIP_CATEGORY_MAP[issueType] ?? \"Dead Code\",\n weight: 1,\n });\n }\n }\n\n return diagnostics;\n};\n\n// HACK: knip triggers dotenv which logs to stdout/stderr via console methods\nconst silenced = async <T>(fn: () => Promise<T>): Promise<T> => {\n const originalLog = console.log;\n const originalInfo = console.info;\n const originalWarn = console.warn;\n const originalError = console.error;\n console.log = () => {};\n console.info = () => {};\n console.warn = () => {};\n console.error = () => {};\n try {\n return await fn();\n } finally {\n console.log = originalLog;\n console.info = originalInfo;\n console.warn = originalWarn;\n console.error = originalError;\n }\n};\n\nconst CONFIG_LOADING_ERROR_PATTERN = /Error loading .*\\/([a-z-]+)\\.config\\./;\n\nconst extractFailedPluginName = (error: unknown): string | null => {\n const match = String(error).match(CONFIG_LOADING_ERROR_PATTERN);\n return match?.[1] ?? null;\n};\n\nconst ANGULAR_ENTRY_PATTERNS = [\"**/*.module.ts\", \"**/*.routes.ts\"];\n\nconst runKnipWithOptions = async (\n knipCwd: string,\n workspaceName?: string,\n): Promise<KnipResults> => {\n const options = await silenced(() =>\n createOptions({\n cwd: knipCwd,\n isShowProgress: false,\n ...(workspaceName ? { workspace: workspaceName } : {}),\n }),\n );\n\n const parsedConfig = options.parsedConfig as Record<string, unknown>;\n\n // For Angular projects without user-defined entry patterns, add Angular module\n // and routes files as entry points. This prevents false positives from\n // lazy-loaded modules whose import chains cannot be statically traced.\n if (\n fs.existsSync(path.join(knipCwd, \"angular.json\")) &&\n parsedConfig[\"entry\"] === undefined\n ) {\n parsedConfig[\"entry\"] = ANGULAR_ENTRY_PATTERNS;\n }\n\n for (let attempt = 0; attempt <= MAX_KNIP_RETRIES; attempt++) {\n try {\n return (await silenced(() => main(options))) as KnipResults;\n } catch (error) {\n const failedPlugin = extractFailedPluginName(error);\n if (!failedPlugin || attempt === MAX_KNIP_RETRIES) {\n throw error;\n }\n parsedConfig[failedPlugin] = false;\n }\n }\n\n throw new Error(\"Unreachable\");\n};\n\nconst hasNodeModules = (directory: string): boolean => {\n const nodeModulesPath = path.join(directory, \"node_modules\");\n return fs.existsSync(nodeModulesPath) && fs.statSync(nodeModulesPath).isDirectory();\n};\n\n/**\n * Searches upward from `directory` for an `angular.json` file and returns\n * the directory that contains it, or `null` if none is found.\n */\nexport const findAngularWorkspaceRoot = (directory: string): string | null => {\n let current = directory;\n while (true) {\n if (fs.existsSync(path.join(current, \"angular.json\"))) return current;\n const parent = path.dirname(current);\n if (parent === current) return null;\n current = parent;\n }\n};\n\nexport const runKnip = async (rootDirectory: string): Promise<Diagnostic[]> => {\n if (!hasNodeModules(rootDirectory)) {\n return [];\n }\n\n // Use the Angular workspace root (where angular.json lives) as the knip cwd\n // so that the Angular plugin can find its configuration and correctly identify\n // entry points for the whole workspace.\n const angularWorkspaceRoot = findAngularWorkspaceRoot(rootDirectory);\n const knipCwd = angularWorkspaceRoot ?? rootDirectory;\n\n const knipResult = await runKnipWithOptions(knipCwd);\n\n const { issues } = knipResult;\n const diagnostics: Diagnostic[] = [];\n\n for (const unusedFile of issues.files) {\n const filePath = path.relative(rootDirectory, unusedFile);\n // Skip files outside the rootDirectory scope (e.g., from other workspace projects)\n if (filePath.startsWith(\"..\")) continue;\n diagnostics.push({\n filePath,\n plugin: \"knip\",\n rule: \"files\",\n severity: KNIP_SEVERITY_MAP[\"files\"],\n message: KNIP_MESSAGE_MAP[\"files\"],\n help: \"This file is not imported by any other file in the project.\",\n line: 0,\n column: 0,\n category: KNIP_CATEGORY_MAP[\"files\"],\n weight: 1,\n });\n }\n\n const recordTypes = [\"exports\", \"types\", \"duplicates\"] as const;\n\n for (const issueType of recordTypes) {\n diagnostics.push(...collectIssueRecords(issues[issueType], issueType, rootDirectory));\n }\n\n return diagnostics;\n};\n","import { spawnSync } from \"node:child_process\";\nimport { SOURCE_FILE_PATTERN, DEFAULT_BRANCH_CANDIDATES } from \"../constants.js\";\nimport type { DiffInfo } from \"../types.js\";\n\nconst getCurrentBranch = (directory: string): string | null => {\n const result = spawnSync(\"git\", [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"], {\n cwd: directory,\n encoding: \"utf-8\",\n });\n return result.status === 0 ? result.stdout.trim() : null;\n};\n\nconst getBranchChangedFiles = (directory: string, baseBranch: string): string[] => {\n const result = spawnSync(\"git\", [\"diff\", \"--name-only\", baseBranch], {\n cwd: directory,\n encoding: \"utf-8\",\n });\n if (result.status !== 0) return [];\n return result.stdout.split(\"\\n\").filter(Boolean);\n};\n\nconst getUncommittedChangedFiles = (directory: string): string[] => {\n const result = spawnSync(\"git\", [\"status\", \"--porcelain\"], {\n cwd: directory,\n encoding: \"utf-8\",\n });\n if (result.status !== 0) return [];\n return result.stdout\n .split(\"\\n\")\n .filter(Boolean)\n .map((line) => line.slice(3).trim())\n .filter(Boolean);\n};\n\nconst branchExists = (directory: string, branch: string): boolean => {\n const result = spawnSync(\"git\", [\"rev-parse\", \"--verify\", branch], {\n cwd: directory,\n encoding: \"utf-8\",\n });\n return result.status === 0;\n};\n\nexport const filterSourceFiles = (files: string[]): string[] =>\n files.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath));\n\nexport const getDiffInfo = (directory: string, explicitBaseBranch?: string): DiffInfo | null => {\n const currentBranch = getCurrentBranch(directory);\n if (!currentBranch) return null;\n\n if (explicitBaseBranch) {\n if (!branchExists(directory, explicitBaseBranch)) return null;\n const changedFiles = getBranchChangedFiles(directory, explicitBaseBranch);\n return { currentBranch, baseBranch: explicitBaseBranch, changedFiles };\n }\n\n const uncommittedFiles = getUncommittedChangedFiles(directory);\n if (uncommittedFiles.length > 0) {\n return {\n currentBranch,\n baseBranch: currentBranch,\n changedFiles: uncommittedFiles,\n isCurrentChanges: true,\n };\n }\n\n for (const candidate of DEFAULT_BRANCH_CANDIDATES) {\n if (branchExists(directory, candidate) && currentBranch !== candidate) {\n const changedFiles = getBranchChangedFiles(directory, candidate);\n if (changedFiles.length > 0) {\n return { currentBranch, baseBranch: candidate, changedFiles };\n }\n }\n }\n\n return null;\n};\n","import path from \"node:path\";\nimport { performance } from \"node:perf_hooks\";\nimport type { AngularDoctorConfig, Diagnostic, DiffInfo, ProjectInfo, ScoreResult } from \"./types.js\";\nimport { calculateScore } from \"./utils/calculate-score.js\";\nimport { combineDiagnostics, computeIncludePaths } from \"./utils/combine-diagnostics.js\";\nimport { discoverProject } from \"./utils/discover-project.js\";\nimport { loadConfig } from \"./utils/load-config.js\";\nimport { runEslint } from \"./utils/run-eslint.js\";\nimport { runKnip } from \"./utils/run-knip.js\";\n\nexport type { Diagnostic, DiffInfo, ProjectInfo, AngularDoctorConfig, ScoreResult };\nexport { getDiffInfo, filterSourceFiles } from \"./utils/get-diff-files.js\";\n\nexport interface DiagnoseOptions {\n lint?: boolean;\n deadCode?: boolean;\n includePaths?: string[];\n}\n\nexport interface DiagnoseResult {\n diagnostics: Diagnostic[];\n score: ScoreResult;\n project: ProjectInfo;\n elapsedMilliseconds: number;\n}\n\nexport const diagnose = async (\n directory: string,\n options: DiagnoseOptions = {},\n): Promise<DiagnoseResult> => {\n const { includePaths = [] } = options;\n const isDiffMode = includePaths.length > 0;\n\n const startTime = performance.now();\n const resolvedDirectory = path.resolve(directory);\n const projectInfo = discoverProject(resolvedDirectory);\n const userConfig = loadConfig(resolvedDirectory);\n\n const effectiveLint = options.lint ?? userConfig?.lint ?? true;\n const effectiveDeadCode = options.deadCode ?? userConfig?.deadCode ?? true;\n\n if (!projectInfo.angularVersion) {\n throw new Error(\"No Angular dependency found in package.json\");\n }\n\n const computedIncludePaths = computeIncludePaths(includePaths);\n\n const emptyDiagnostics: Diagnostic[] = [];\n\n const lintPromise = effectiveLint\n ? runEslint(\n resolvedDirectory,\n projectInfo.hasTypeScript,\n computedIncludePaths,\n ).catch((error: unknown) => {\n console.error(\"Lint failed:\", error);\n return { diagnostics: emptyDiagnostics, errors: [] };\n })\n : Promise.resolve({ diagnostics: emptyDiagnostics, errors: [] });\n\n const deadCodePromise =\n effectiveDeadCode && !isDiffMode\n ? runKnip(resolvedDirectory).catch((error: unknown) => {\n console.error(\"Dead code analysis failed:\", error);\n return emptyDiagnostics;\n })\n : Promise.resolve(emptyDiagnostics);\n\n const [lintResult, deadCodeDiagnostics] = await Promise.all([lintPromise, deadCodePromise]);\n const lintDiagnostics = lintResult.diagnostics;\n const diagnostics = combineDiagnostics(lintDiagnostics, deadCodeDiagnostics, userConfig);\n\n const elapsedMilliseconds = performance.now() - startTime;\n const score = calculateScore(diagnostics);\n\n return {\n diagnostics,\n score,\n project: projectInfo,\n elapsedMilliseconds,\n };\n};\n"],"mappings":"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+B;CAC/B,0CAA0C;CAC1C,yCAAyC;CACzC,qCAAqC;CACrC,sBAAsB;CACtB,+BAA+B;CAG/B,qCAAqC;CACrC,2CAA2C;CAC5C;AAGD,MAAM,mBAA2C;CAC/C,0CACE;CACF,0CACE;CACF,+BAA+B;CAC/B,gDACE;CACF,6CAA6C;CAC7C,2CACE;CACF,+CACE;CACF,6DACE;CACF,oCACE;CACF,4CACE;CACF,wCACE;CACF,wCACE;CACF,kCACE;CACF,mCACE;CACF,oCACE;CACF,+CACE;CACF,gDACE;CACF,qCACE;CACF,sCACE;CACF,sCACE;CACF,kCACE;CACF,6CACE;CACF,oDACE;CACF,qCACE;CACF,kDACE;CACF,0CACE;CACF,gDACE;CACF,iDACE;CACF,iCACE;CACF,6CACE;CACF,0CACE;CACF,0CACE;CACF,oDACE;CACF,8CACE;CACF,qDACE;CACF,0CACE;CACF,mCACE;CACF,uCACE;CACF,uCACE;CACF,kCACE;CACF,uCACE;CACF,uCACE;CACF,sCACE;CACF,qCAAqC;CACrC,gCAAgC;CAEhC,mCACE;CACF,mCACE;CACF,6BACE;CACF,6BACE;CACF,yCACE;CACF,wCACE;CACF,0CACE;CACF,+BACE;CACF,0CACE;CACF,yCACE;CACF,qCACE;CACF,sBACE;CACF,+BACE;CAEF,qCACE;CACF,2CACE;CACH;AAED,MAAM,gBAAwC;CAC5C,0CACE;CACF,0CACE;CACF,gDACE;CACF,6CACE;CACF,2CACE;CACF,6DACE;CACF,oCACE;CACF,kCACE;CACF,mCACE;CACF,oCACE;CACF,+CACE;CACF,gDACE;CACF,qCACE;CACF,sCACE;CACF,qCACE;CACF,gCACE;CACF,iCACE;CACF,kCACE;CACF,kCACE;CACF,uCACE;CACF,uCACE;CACF,qDACE;CACF,uCACE;CACF,mCACE;CACF,wCACE;CACH;AAQD,MAAM,yBAAyB,gBAA8C;CAC3E,MAAM,OAAO;EACX,GAAG,YAAY;EACf,GAAG,YAAY;EACf,GAAG,YAAY;EAChB;AAED,QAAO;EACL,SAAS,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"}
|
package/install-skill.sh
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Installs the angular-doctor skill into your AI coding agent.
|
|
3
|
+
# Supports: Cursor, Claude Code, Windsurf, Amp Code, Codex, Gemini CLI, OpenCode
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# curl -fsSL https://raw.githubusercontent.com/antonygiomarxdev/angular-doctor/main/install-skill.sh | bash
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
SKILL_NAME="angular-doctor"
|
|
11
|
+
SKILL_DESCRIPTION="Run after making Angular changes to catch issues early. Use when reviewing code, finishing a feature, or fixing bugs in an Angular project."
|
|
12
|
+
SKILL_COMMAND="npx -y angular-doctor@latest . --verbose --diff"
|
|
13
|
+
|
|
14
|
+
SKILL_BODY="# Angular Doctor
|
|
15
|
+
|
|
16
|
+
Scans your Angular codebase for performance, correctness, and architecture issues. Outputs a 0-100 score with actionable diagnostics.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
\`\`\`bash
|
|
21
|
+
${SKILL_COMMAND}
|
|
22
|
+
\`\`\`
|
|
23
|
+
|
|
24
|
+
## Workflow
|
|
25
|
+
|
|
26
|
+
Run after making changes to catch issues early. Fix errors first, then re-run to verify the score improved."
|
|
27
|
+
|
|
28
|
+
installed=0
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Cursor — .cursor/rules/angular-doctor.mdc
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
if [ -d ".cursor" ] || command -v cursor &>/dev/null; then
|
|
34
|
+
mkdir -p ".cursor/rules"
|
|
35
|
+
cat > ".cursor/rules/${SKILL_NAME}.mdc" <<EOF
|
|
36
|
+
---
|
|
37
|
+
description: ${SKILL_DESCRIPTION}
|
|
38
|
+
globs:
|
|
39
|
+
alwaysApply: false
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
${SKILL_BODY}
|
|
43
|
+
EOF
|
|
44
|
+
echo "✓ Installed for Cursor (.cursor/rules/${SKILL_NAME}.mdc)"
|
|
45
|
+
installed=$((installed + 1))
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Claude Code — .claude/SKILL.md
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
if command -v claude &>/dev/null || [ -d ".claude" ]; then
|
|
52
|
+
mkdir -p ".claude"
|
|
53
|
+
cat > ".claude/${SKILL_NAME}.md" <<EOF
|
|
54
|
+
---
|
|
55
|
+
name: ${SKILL_NAME}
|
|
56
|
+
description: ${SKILL_DESCRIPTION}
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
${SKILL_BODY}
|
|
60
|
+
EOF
|
|
61
|
+
echo "✓ Installed for Claude Code (.claude/${SKILL_NAME}.md)"
|
|
62
|
+
installed=$((installed + 1))
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# Windsurf — .windsurf/rules/angular-doctor.md
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
if [ -d ".windsurf" ] || command -v windsurf &>/dev/null; then
|
|
69
|
+
mkdir -p ".windsurf/rules"
|
|
70
|
+
cat > ".windsurf/rules/${SKILL_NAME}.md" <<EOF
|
|
71
|
+
---
|
|
72
|
+
description: ${SKILL_DESCRIPTION}
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
${SKILL_BODY}
|
|
76
|
+
EOF
|
|
77
|
+
echo "✓ Installed for Windsurf (.windsurf/rules/${SKILL_NAME}.md)"
|
|
78
|
+
installed=$((installed + 1))
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Amp Code — .amp/skills/angular-doctor.md
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
if command -v amp &>/dev/null || [ -d ".amp" ]; then
|
|
85
|
+
mkdir -p ".amp/skills"
|
|
86
|
+
cat > ".amp/skills/${SKILL_NAME}.md" <<EOF
|
|
87
|
+
---
|
|
88
|
+
name: ${SKILL_NAME}
|
|
89
|
+
description: ${SKILL_DESCRIPTION}
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
${SKILL_BODY}
|
|
93
|
+
EOF
|
|
94
|
+
echo "✓ Installed for Amp Code (.amp/skills/${SKILL_NAME}.md)"
|
|
95
|
+
installed=$((installed + 1))
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# Codex (OpenAI) — appended to AGENTS.md
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
if command -v codex &>/dev/null || [ -f "AGENTS.md" ]; then
|
|
102
|
+
AGENTS_ENTRY="
|
|
103
|
+
## ${SKILL_NAME}
|
|
104
|
+
|
|
105
|
+
${SKILL_DESCRIPTION}
|
|
106
|
+
|
|
107
|
+
Run \`${SKILL_COMMAND}\` after making Angular changes.
|
|
108
|
+
"
|
|
109
|
+
if [ -f "AGENTS.md" ]; then
|
|
110
|
+
if ! grep -q "${SKILL_NAME}" AGENTS.md 2>/dev/null; then
|
|
111
|
+
printf '\n%s' "${AGENTS_ENTRY}" >> AGENTS.md
|
|
112
|
+
echo "✓ Installed for Codex (appended to AGENTS.md)"
|
|
113
|
+
installed=$((installed + 1))
|
|
114
|
+
else
|
|
115
|
+
echo " Codex: ${SKILL_NAME} already present in AGENTS.md, skipping."
|
|
116
|
+
fi
|
|
117
|
+
else
|
|
118
|
+
printf '%s' "${AGENTS_ENTRY}" > AGENTS.md
|
|
119
|
+
echo "✓ Installed for Codex (created AGENTS.md)"
|
|
120
|
+
installed=$((installed + 1))
|
|
121
|
+
fi
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Gemini CLI — appended to GEMINI.md
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
if command -v gemini &>/dev/null || [ -f "GEMINI.md" ]; then
|
|
128
|
+
GEMINI_ENTRY="
|
|
129
|
+
## ${SKILL_NAME}
|
|
130
|
+
|
|
131
|
+
${SKILL_DESCRIPTION}
|
|
132
|
+
|
|
133
|
+
Run \`${SKILL_COMMAND}\` after making Angular changes.
|
|
134
|
+
"
|
|
135
|
+
if [ -f "GEMINI.md" ]; then
|
|
136
|
+
if ! grep -q "${SKILL_NAME}" GEMINI.md 2>/dev/null; then
|
|
137
|
+
printf '\n%s' "${GEMINI_ENTRY}" >> GEMINI.md
|
|
138
|
+
echo "✓ Installed for Gemini CLI (appended to GEMINI.md)"
|
|
139
|
+
installed=$((installed + 1))
|
|
140
|
+
else
|
|
141
|
+
echo " Gemini CLI: ${SKILL_NAME} already present in GEMINI.md, skipping."
|
|
142
|
+
fi
|
|
143
|
+
else
|
|
144
|
+
printf '%s' "${GEMINI_ENTRY}" > GEMINI.md
|
|
145
|
+
echo "✓ Installed for Gemini CLI (created GEMINI.md)"
|
|
146
|
+
installed=$((installed + 1))
|
|
147
|
+
fi
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# OpenCode — .opencode/skills/angular-doctor.md
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
if command -v opencode &>/dev/null || [ -d ".opencode" ]; then
|
|
154
|
+
mkdir -p ".opencode/skills"
|
|
155
|
+
cat > ".opencode/skills/${SKILL_NAME}.md" <<EOF
|
|
156
|
+
---
|
|
157
|
+
name: ${SKILL_NAME}
|
|
158
|
+
description: ${SKILL_DESCRIPTION}
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
${SKILL_BODY}
|
|
162
|
+
EOF
|
|
163
|
+
echo "✓ Installed for OpenCode (.opencode/skills/${SKILL_NAME}.md)"
|
|
164
|
+
installed=$((installed + 1))
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
# Summary
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
echo ""
|
|
171
|
+
if [ "${installed}" -eq 0 ]; then
|
|
172
|
+
echo "No supported AI coding agent detected in this directory."
|
|
173
|
+
echo ""
|
|
174
|
+
echo "Supported agents: Cursor, Claude Code, Windsurf, Amp Code, Codex, Gemini CLI, OpenCode"
|
|
175
|
+
echo ""
|
|
176
|
+
echo "To install manually, copy skills/angular-doctor/SKILL.md into your agent's rules directory."
|
|
177
|
+
else
|
|
178
|
+
echo "angular-doctor skill installed for ${installed} agent(s)."
|
|
179
|
+
echo ""
|
|
180
|
+
echo "Your coding agent will now run angular-doctor automatically after Angular changes."
|
|
181
|
+
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "angular-doctor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"author": "Antony Giomar <antonygiomarx@gmail.com>",
|
|
5
5
|
"description": "Diagnose and fix issues in your Angular app",
|
|
6
6
|
"repository": {
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
"bin": "bin/angular-doctor.js",
|
|
17
17
|
"files": [
|
|
18
18
|
"dist",
|
|
19
|
-
"bin"
|
|
19
|
+
"bin",
|
|
20
|
+
"skills",
|
|
21
|
+
"install-skill.sh"
|
|
20
22
|
],
|
|
21
23
|
"type": "module",
|
|
22
24
|
"exports": {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: angular-doctor
|
|
3
|
+
description: Run after making Angular changes to catch issues early. Use when reviewing code, finishing a feature, or fixing bugs in an Angular project.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Angular Doctor
|
|
8
|
+
|
|
9
|
+
Scans your Angular codebase for performance, correctness, and architecture issues. Outputs a 0-100 score with actionable diagnostics.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx -y angular-doctor@latest . --verbose --diff
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Workflow
|
|
18
|
+
|
|
19
|
+
Run after making changes to catch issues early. Fix errors first, then re-run to verify the score improved.
|