angular-doctor 1.2.0 → 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/dist/cli.mjs CHANGED
@@ -130,12 +130,26 @@ const detectFramework = (dependencies) => {
130
130
  return "unknown";
131
131
  };
132
132
  const detectAngularVersion = (dependencies) => dependencies["@angular/core"] ?? null;
133
+ const detectAngularMajorVersion = (version) => {
134
+ if (!version) return null;
135
+ const major = parseInt(version.match(/\d+/)?.[0] ?? "", 10);
136
+ return isNaN(major) ? null : major;
137
+ };
133
138
  const detectStandaloneComponents = (packageJson) => {
134
139
  const angularVersion = collectAllDependencies(packageJson)["@angular/core"];
135
140
  if (!angularVersion) return false;
136
141
  const majorVersion = parseInt(angularVersion.match(/\d+/)?.[0] ?? "", 10);
137
142
  return !isNaN(majorVersion) && majorVersion >= 14;
138
143
  };
144
+ const detectNgRxPackages = (dependencies) => {
145
+ return Object.keys(dependencies).some((dep) => dep.startsWith("@ngrx/") || dep === "@ngrx/store");
146
+ };
147
+ const detectAngularMaterial = (dependencies) => {
148
+ return Object.keys(dependencies).includes("@angular/material");
149
+ };
150
+ const detectSignals = (angularMajorVersion) => {
151
+ return angularMajorVersion !== null && angularMajorVersion >= 17;
152
+ };
139
153
  const countSourceFiles = (rootDirectory) => {
140
154
  const result = spawnSync("git", [
141
155
  "ls-files",
@@ -173,9 +187,13 @@ const discoverProject = (directory) => {
173
187
  const packageJson = readPackageJson(path.join(packageJsonDir, "package.json"));
174
188
  const allDeps = collectAllDependencies(packageJson);
175
189
  const angularVersion = detectAngularVersion(allDeps);
190
+ const angularMajorVersion = detectAngularMajorVersion(angularVersion);
176
191
  const framework = detectFramework(allDeps);
177
192
  const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json")) || fs.existsSync(path.join(packageJsonDir, "tsconfig.json"));
178
193
  const hasStandaloneComponents = detectStandaloneComponents(packageJson);
194
+ const hasNgRx = detectNgRxPackages(allDeps);
195
+ const hasAngularMaterial = detectAngularMaterial(allDeps);
196
+ const hasSignals = detectSignals(angularMajorVersion);
179
197
  const sourceFileCount = countSourceFiles(directory);
180
198
  const angularJsonPath = path.join(packageJsonDir, "angular.json");
181
199
  let projectName = packageJson.name ?? path.basename(directory);
@@ -184,9 +202,13 @@ const discoverProject = (directory) => {
184
202
  rootDirectory: directory,
185
203
  projectName,
186
204
  angularVersion,
205
+ angularMajorVersion,
187
206
  framework,
188
207
  hasTypeScript,
189
208
  hasStandaloneComponents,
209
+ hasNgRx,
210
+ hasAngularMaterial,
211
+ hasSignals,
190
212
  sourceFileCount
191
213
  };
192
214
  };
@@ -369,30 +391,88 @@ const loadConfig = (rootDirectory) => {
369
391
  //#region src/utils/run-eslint.ts
370
392
  const RULE_CATEGORY_MAP = {
371
393
  "@angular-eslint/component-class-suffix": "Components",
394
+ "@angular-eslint/component-max-inline-declarations": "Performance",
395
+ "@angular-eslint/component-selector": "Components",
372
396
  "@angular-eslint/directive-class-suffix": "Components",
397
+ "@angular-eslint/directive-selector": "Components",
373
398
  "@angular-eslint/pipe-prefix": "Components",
374
399
  "@angular-eslint/use-pipe-transform-interface": "Components",
375
400
  "@angular-eslint/no-empty-lifecycle-method": "Components",
376
401
  "@angular-eslint/use-lifecycle-interface": "Components",
377
402
  "@angular-eslint/consistent-component-styles": "Components",
403
+ "@angular-eslint/sort-lifecycle-methods": "Components",
404
+ "@angular-eslint/use-component-selector": "Components",
378
405
  "@angular-eslint/prefer-on-push-component-change-detection": "Performance",
379
406
  "@angular-eslint/no-output-native": "Performance",
407
+ "@angular-eslint/no-pipe-impure": "Performance",
380
408
  "@angular-eslint/no-conflicting-lifecycle": "Correctness",
381
409
  "@angular-eslint/contextual-lifecycle": "Correctness",
410
+ "@angular-eslint/contextual-decorator": "Correctness",
411
+ "@angular-eslint/no-async-lifecycle-method": "Correctness",
412
+ "@angular-eslint/no-duplicates-in-metadata-arrays": "Correctness",
413
+ "@angular-eslint/no-lifecycle-call": "Correctness",
414
+ "@angular-eslint/require-lifecycle-on-prototype": "Correctness",
415
+ "@angular-eslint/no-attribute-decorator": "Architecture",
382
416
  "@angular-eslint/no-forward-ref": "Architecture",
383
417
  "@angular-eslint/no-input-rename": "Architecture",
384
418
  "@angular-eslint/no-output-rename": "Architecture",
385
419
  "@angular-eslint/no-inputs-metadata-property": "Architecture",
386
420
  "@angular-eslint/no-outputs-metadata-property": "Architecture",
421
+ "@angular-eslint/no-queries-metadata-property": "Architecture",
387
422
  "@angular-eslint/prefer-standalone": "Architecture",
423
+ "@angular-eslint/prefer-host-metadata-property": "Architecture",
424
+ "@angular-eslint/prefer-inject": "Architecture",
425
+ "@angular-eslint/prefer-output-emitter-ref": "Architecture",
426
+ "@angular-eslint/prefer-output-readonly": "Architecture",
427
+ "@angular-eslint/use-component-view-encapsulation": "Architecture",
428
+ "@angular-eslint/use-injectable-provided-in": "Architecture",
429
+ "@angular-eslint/no-input-prefix": "Architecture",
430
+ "@angular-eslint/no-output-on-prefix": "Architecture",
431
+ "@angular-eslint/relative-url-prefix": "Security",
432
+ "@angular-eslint/prefer-signals": "Signals",
433
+ "@angular-eslint/prefer-signal-model": "Signals",
434
+ "@angular-eslint/no-uncalled-signals": "Signals",
435
+ "@angular-eslint/template/accessibility": "Accessibility",
436
+ "@angular-eslint/template/alt-text": "Accessibility",
437
+ "@angular-eslint/template/click-events-have-key-events": "Accessibility",
438
+ "@angular-eslint/template/control-events-have-key-events": "Accessibility",
439
+ "@angular-eslint/template/elements-have-content": "Accessibility",
440
+ "@angular-eslint/template/interactive-supports-focus": "Accessibility",
441
+ "@angular-eslint/template/mouse-events-have-key-events": "Accessibility",
442
+ "@angular-eslint/template/no-any": "Accessibility",
443
+ "@angular-eslint/template/table-scope": "Accessibility",
444
+ "@angular-eslint/template/valid-aria": "Accessibility",
445
+ "@ngrx/contextual-action-creator": "NgRx",
446
+ "@ngrx/no-cyclic-action-creators": "NgRx",
447
+ "@ngrx/no-discrete-actions": "NgRx",
448
+ "@ngrx/no-effect-decorator": "NgRx",
449
+ "@ngrx/no-effect-decorator-and-creator": "NgRx",
450
+ "@ngrx/no-multiple-actions-in-effects": "NgRx",
451
+ "@ngrx/no-reordering-in-effect-reducers": "NgRx",
452
+ "@ngrx/no-typed-global-store": "NgRx",
453
+ "@ngrx/on-function-explicit-return-type": "NgRx",
454
+ "@ngrx/prefix-selectors-with-namespace": "NgRx",
455
+ "@ngrx/require-middleware-selector": "NgRx",
456
+ "@ngrx/select-style": "NgRx",
457
+ "@ngrx/use-consumer-selector": "NgRx",
458
+ "@angular/material/prefix-selector": "Material",
459
+ "@angular/material/no-conflicting-mixins": "Material",
388
460
  "@typescript-eslint/no-explicit-any": "TypeScript",
389
- "@typescript-eslint/no-unused-vars": "Dead Code"
461
+ "@typescript-eslint/no-unused-vars": "Dead Code",
462
+ "@typescript-eslint/sort-keys": "TypeScript"
390
463
  };
391
464
  const RULE_SEVERITY_MAP = {
392
465
  "@angular-eslint/no-conflicting-lifecycle": "error",
393
466
  "@angular-eslint/contextual-lifecycle": "error",
394
467
  "@angular-eslint/use-pipe-transform-interface": "error",
395
468
  "@angular-eslint/no-output-native": "error",
469
+ "@angular-eslint/no-async-lifecycle-method": "error",
470
+ "@angular-eslint/no-lifecycle-call": "error",
471
+ "@angular-eslint/no-duplicates-in-metadata-arrays": "error",
472
+ "@angular-eslint/require-lifecycle-on-prototype": "error",
473
+ "@angular-eslint/relative-url-prefix": "error",
474
+ "@angular-eslint/contextual-decorator": "error",
475
+ "@angular-eslint/no-uncalled-signals": "error",
396
476
  "@angular-eslint/component-class-suffix": "warning",
397
477
  "@angular-eslint/directive-class-suffix": "warning",
398
478
  "@angular-eslint/pipe-prefix": "warning",
@@ -406,8 +486,42 @@ const RULE_SEVERITY_MAP = {
406
486
  "@angular-eslint/no-inputs-metadata-property": "warning",
407
487
  "@angular-eslint/no-outputs-metadata-property": "warning",
408
488
  "@angular-eslint/prefer-standalone": "warning",
489
+ "@angular-eslint/component-selector": "warning",
490
+ "@angular-eslint/directive-selector": "warning",
491
+ "@angular-eslint/no-pipe-impure": "warning",
492
+ "@angular-eslint/no-attribute-decorator": "warning",
493
+ "@angular-eslint/no-queries-metadata-property": "warning",
494
+ "@angular-eslint/prefer-host-metadata-property": "warning",
495
+ "@angular-eslint/prefer-inject": "warning",
496
+ "@angular-eslint/prefer-output-emitter-ref": "warning",
497
+ "@angular-eslint/prefer-output-readonly": "warning",
498
+ "@angular-eslint/use-component-selector": "warning",
499
+ "@angular-eslint/use-component-view-encapsulation": "warning",
500
+ "@angular-eslint/use-injectable-provided-in": "warning",
501
+ "@angular-eslint/component-max-inline-declarations": "warning",
502
+ "@angular-eslint/sort-lifecycle-methods": "warning",
503
+ "@angular-eslint/no-input-prefix": "warning",
504
+ "@angular-eslint/no-output-on-prefix": "warning",
505
+ "@angular-eslint/prefer-signals": "warning",
506
+ "@angular-eslint/prefer-signal-model": "warning",
409
507
  "@typescript-eslint/no-explicit-any": "warning",
410
- "@typescript-eslint/no-unused-vars": "warning"
508
+ "@typescript-eslint/no-unused-vars": "warning",
509
+ "@typescript-eslint/sort-keys": "warning",
510
+ "@ngrx/contextual-action-creator": "warning",
511
+ "@ngrx/no-cyclic-action-creators": "error",
512
+ "@ngrx/no-discrete-actions": "warning",
513
+ "@ngrx/no-effect-decorator": "warning",
514
+ "@ngrx/no-effect-decorator-and-creator": "error",
515
+ "@ngrx/no-multiple-actions-in-effects": "error",
516
+ "@ngrx/no-reordering-in-effect-reducers": "error",
517
+ "@ngrx/no-typed-global-store": "error",
518
+ "@ngrx/on-function-explicit-return-type": "warning",
519
+ "@ngrx/prefix-selectors-with-namespace": "warning",
520
+ "@ngrx/require-middleware-selector": "error",
521
+ "@ngrx/select-style": "warning",
522
+ "@ngrx/use-consumer-selector": "warning",
523
+ "@angular/material/prefix-selector": "warning",
524
+ "@angular/material/no-conflicting-mixins": "error"
411
525
  };
412
526
  const RULE_MESSAGE_MAP = {
413
527
  "@angular-eslint/component-class-suffix": "Component class should end with 'Component'",
@@ -421,14 +535,55 @@ const RULE_MESSAGE_MAP = {
421
535
  "@angular-eslint/no-output-native": "Avoid shadowing native DOM events in output names",
422
536
  "@angular-eslint/no-conflicting-lifecycle": "Lifecycle hooks DoCheck and OnChanges cannot be used together",
423
537
  "@angular-eslint/contextual-lifecycle": "Lifecycle hook is not available in this context",
538
+ "@angular-eslint/contextual-decorator": "Use contextual decorator to specify injection context",
424
539
  "@angular-eslint/no-forward-ref": "Avoid using forwardRef — restructure to avoid circular dependency",
425
540
  "@angular-eslint/no-input-rename": "Avoid renaming directive inputs — use the property name as the binding name",
426
541
  "@angular-eslint/no-output-rename": "Avoid renaming directive outputs — use the property name as the binding name",
427
542
  "@angular-eslint/no-inputs-metadata-property": "Use @Input() decorator instead of inputs metadata property",
428
543
  "@angular-eslint/no-outputs-metadata-property": "Use @Output() decorator instead of outputs metadata property",
429
544
  "@angular-eslint/prefer-standalone": "Prefer standalone components over NgModule-based components",
545
+ "@angular-eslint/component-selector": "Component selector should follow naming convention",
546
+ "@angular-eslint/directive-selector": "Directive selector should follow naming convention",
547
+ "@angular-eslint/no-pipe-impure": "Avoid impure pipes — they run on every change detection cycle",
548
+ "@angular-eslint/no-async-lifecycle-method": "Avoid async lifecycle methods — use signals instead",
549
+ "@angular-eslint/no-duplicates-in-metadata-arrays": "Remove duplicate entries in decorator metadata arrays",
550
+ "@angular-eslint/no-lifecycle-call": "Don't call lifecycle method directly — let Angular call them",
551
+ "@angular-eslint/require-lifecycle-on-prototype": "Lifecycle methods should be declared on prototype",
552
+ "@angular-eslint/no-attribute-decorator": "Avoid @Attribute() — use @Input() for property bindings",
553
+ "@angular-eslint/no-queries-metadata-property": "Use decorator-based queries instead of metadata properties",
554
+ "@angular-eslint/prefer-host-metadata-property": "Prefer host metadata property over @Host decorator",
555
+ "@angular-eslint/prefer-inject": "Prefer inject() function over constructor dependency injection",
556
+ "@angular-eslint/prefer-output-emitter-ref": "Use EventEmitter with Output instead of Subject",
557
+ "@angular-eslint/prefer-output-readonly": "Mark outputs as readonly when possible",
558
+ "@angular-eslint/use-component-selector": "Components must have selector for proper encapsulation",
559
+ "@angular-eslint/use-component-view-encapsulation": "Specify view encapsulation strategy explicitly",
560
+ "@angular-eslint/use-injectable-provided-in": "Specify providedIn scope for @Injectable()",
561
+ "@angular-eslint/component-max-inline-declarations": "Too many inline declarations in component — extract to separate files",
562
+ "@angular-eslint/sort-lifecycle-methods": "Lifecycle methods should be declared in correct order",
563
+ "@angular-eslint/no-input-prefix": "Avoid prefix for input property names",
564
+ "@angular-eslint/no-output-on-prefix": "Avoid 'on' prefix for output event names",
565
+ "@angular-eslint/relative-url-prefix": "Use relative URL prefixes for better security",
566
+ "@angular-eslint/prefer-signals": "Prefer Angular signals over other reactive patterns",
567
+ "@angular-eslint/prefer-signal-model": "Prefer signal-based model for component state",
568
+ "@angular-eslint/no-uncalled-signals": "Signal getters must be called to access value",
430
569
  "@typescript-eslint/no-explicit-any": "Avoid 'any' type — use specific types for better type safety",
431
- "@typescript-eslint/no-unused-vars": "Remove unused variable declaration"
570
+ "@typescript-eslint/no-unused-vars": "Remove unused variable declaration",
571
+ "@typescript-eslint/sort-keys": "Sort object keys consistently",
572
+ "@ngrx/contextual-action-creator": "Use contextual action creators for typed actions",
573
+ "@ngrx/no-cyclic-action-creators": "Action creators should not reference each other cyclically",
574
+ "@ngrx/no-discrete-actions": "Use discrete actions instead of broad action types",
575
+ "@ngrx/no-effect-decorator": "Consider using functional effects instead of @Effect decorator",
576
+ "@ngrx/no-effect-decorator-and-creator": "Don't use both @Effect decorator and createEffect function",
577
+ "@ngrx/no-multiple-actions-in-effects": "Effects should dispatch a single action or none",
578
+ "@ngrx/no-reordering-in-effect-reducers": "Don't reorder actions in effect reducers",
579
+ "@ngrx/no-typed-global-store": "Use typed GlobalStore for better type safety",
580
+ "@ngrx/on-function-explicit-return-type": "Specify explicit return type for on() reducer functions",
581
+ "@ngrx/prefix-selectors-with-namespace": "Prefix selectors with feature namespace",
582
+ "@ngrx/require-middleware-selector": "Middleware must have selector for proper scoping",
583
+ "@ngrx/select-style": "Prefer selector functions over props in select",
584
+ "@ngrx/use-consumer-selector": "Use useSelector with selector function for proper memoization",
585
+ "@angular/material/prefix-selector": "Material components should use proper selector prefix",
586
+ "@angular/material/no-conflicting-mixins": "Avoid conflicting mixins in Material components"
432
587
  };
433
588
  const RULE_HELP_MAP = {
434
589
  "@angular-eslint/component-class-suffix": "Add 'Component' suffix: `export class UserProfileComponent { }`",
@@ -445,9 +600,31 @@ const RULE_HELP_MAP = {
445
600
  "@angular-eslint/no-outputs-metadata-property": "Use `@Output() myEvent = new EventEmitter()` instead of `outputs: ['myEvent']` in the decorator metadata",
446
601
  "@angular-eslint/prefer-standalone": "Add `standalone: true` to component: `@Component({ standalone: true, ... })`",
447
602
  "@typescript-eslint/no-explicit-any": "Replace `any` with a specific type or `unknown` if the type is truly unknown",
448
- "@typescript-eslint/no-unused-vars": "Remove the unused variable or prefix with `_` to indicate it's intentionally unused"
603
+ "@typescript-eslint/no-unused-vars": "Remove the unused variable or prefix with `_` to indicate it's intentionally unused",
604
+ "@typescript-eslint/sort-keys": "Sort object keys alphabetically or by a consistent pattern",
605
+ "@angular-eslint/prefer-inject": "Use `inject(MyService)` instead of constructor injection: `constructor(private myService: MyService) {}`",
606
+ "@angular-eslint/no-pipe-impure": "Remove `pure: false` from pipe decorator or refactor to use a service",
607
+ "@angular-eslint/prefer-signals": "Replace observables with signals for simpler reactive state: `count = signal(0)`",
608
+ "@angular-eslint/prefer-signal-model": "Use signal-based input/output model: `count = model(0)` instead of `@Input() count: number`",
609
+ "@angular-eslint/no-uncalled-signals": "Call the signal getter to access its value: `this.count()` not `this.count`",
610
+ "@angular-eslint/component-max-inline-declarations": "Move templates/styles to separate files or use inline with caution — consider extracting when > 3 inline declarations",
611
+ "@angular-eslint/relative-url-prefix": "Use relative URLs (no leading slash) or ensure absolute URLs are intentional for security",
612
+ "@ngrx/contextual-action-creator": "Use `createActionGroup` or `createAction` with props for type-safe actions",
613
+ "@ngrx/no-multiple-actions-in-effects": "Split effect into multiple effects or use `mergeMap` with individual actions"
614
+ };
615
+ const detectPackagePresence = (packageJson) => {
616
+ const deps = {
617
+ ...packageJson.dependencies,
618
+ ...packageJson.devDependencies,
619
+ ...packageJson.peerDependencies
620
+ };
621
+ return {
622
+ hasNgRx: Object.keys(deps).some((dep) => dep.startsWith("@ngrx/") && !dep.includes("store")) || Object.keys(deps).includes("@ngrx/store"),
623
+ hasAngularMaterial: Object.keys(deps).includes("@angular/material"),
624
+ hasSignals: true
625
+ };
449
626
  };
450
- const buildEslintConfig = (hasTypeScript, tsconfigPath, useTypeAware) => {
627
+ const buildEslintConfig = (hasTypeScript, tsconfigPath, useTypeAware, packagePresence) => {
451
628
  const languageOptions = {
452
629
  parser: tsEslint.parser,
453
630
  parserOptions: {
@@ -459,20 +636,73 @@ const buildEslintConfig = (hasTypeScript, tsconfigPath, useTypeAware) => {
459
636
  const angularRules = {
460
637
  "@angular-eslint/component-class-suffix": "warn",
461
638
  "@angular-eslint/directive-class-suffix": "warn",
639
+ "@angular-eslint/pipe-prefix": "warn",
462
640
  "@angular-eslint/no-empty-lifecycle-method": "warn",
463
641
  "@angular-eslint/use-lifecycle-interface": "warn",
464
- "@angular-eslint/use-pipe-transform-interface": "error",
642
+ "@angular-eslint/consistent-component-styles": "warn",
643
+ "@angular-eslint/sort-lifecycle-methods": "warn",
644
+ "@angular-eslint/use-component-selector": "warn",
645
+ "@angular-eslint/component-selector": "warn",
646
+ "@angular-eslint/directive-selector": "warn",
465
647
  "@angular-eslint/prefer-on-push-component-change-detection": "warn",
466
648
  "@angular-eslint/no-output-native": "error",
649
+ "@angular-eslint/no-pipe-impure": "warn",
650
+ "@angular-eslint/component-max-inline-declarations": "warn",
651
+ "@angular-eslint/use-pipe-transform-interface": "error",
467
652
  "@angular-eslint/no-conflicting-lifecycle": "error",
468
653
  "@angular-eslint/contextual-lifecycle": "error",
654
+ "@angular-eslint/contextual-decorator": "error",
655
+ "@angular-eslint/no-async-lifecycle-method": "error",
656
+ "@angular-eslint/no-duplicates-in-metadata-arrays": "error",
657
+ "@angular-eslint/no-lifecycle-call": "error",
658
+ "@angular-eslint/require-lifecycle-on-prototype": "error",
469
659
  "@angular-eslint/no-forward-ref": "warn",
470
660
  "@angular-eslint/no-input-rename": "warn",
471
661
  "@angular-eslint/no-output-rename": "warn",
472
662
  "@angular-eslint/no-inputs-metadata-property": "warn",
473
- "@angular-eslint/no-outputs-metadata-property": "warn"
663
+ "@angular-eslint/no-outputs-metadata-property": "warn",
664
+ "@angular-eslint/no-queries-metadata-property": "warn",
665
+ "@angular-eslint/prefer-standalone": "warn",
666
+ "@angular-eslint/prefer-host-metadata-property": "warn",
667
+ "@angular-eslint/prefer-inject": "warn",
668
+ "@angular-eslint/prefer-output-emitter-ref": "warn",
669
+ "@angular-eslint/prefer-output-readonly": "warn",
670
+ "@angular-eslint/use-component-view-encapsulation": "warn",
671
+ "@angular-eslint/use-injectable-provided-in": "warn",
672
+ "@angular-eslint/no-attribute-decorator": "warn",
673
+ "@angular-eslint/no-input-prefix": "warn",
674
+ "@angular-eslint/no-output-on-prefix": "warn",
675
+ "@angular-eslint/relative-url-prefix": "error"
676
+ };
677
+ const signalsRules = packagePresence.hasSignals ? {
678
+ "@angular-eslint/prefer-signals": "warn",
679
+ "@angular-eslint/prefer-signal-model": "warn",
680
+ "@angular-eslint/no-uncalled-signals": "error"
681
+ } : {};
682
+ const tsRules = {
683
+ "@typescript-eslint/no-explicit-any": "warn",
684
+ "@typescript-eslint/no-unused-vars": "warn",
685
+ "@typescript-eslint/sort-keys": "warn"
474
686
  };
475
- const tsRules = { "@typescript-eslint/no-explicit-any": "warn" };
687
+ const ngrxRules = packagePresence.hasNgRx ? {
688
+ "@ngrx/contextual-action-creator": "warn",
689
+ "@ngrx/no-cyclic-action-creators": "error",
690
+ "@ngrx/no-discrete-actions": "warn",
691
+ "@ngrx/no-effect-decorator": "warn",
692
+ "@ngrx/no-effect-decorator-and-creator": "error",
693
+ "@ngrx/no-multiple-actions-in-effects": "error",
694
+ "@ngrx/no-reordering-in-effect-reducers": "error",
695
+ "@ngrx/no-typed-global-store": "error",
696
+ "@ngrx/on-function-explicit-return-type": "warn",
697
+ "@ngrx/prefix-selectors-with-namespace": "warn",
698
+ "@ngrx/require-middleware-selector": "error",
699
+ "@ngrx/select-style": "warn",
700
+ "@ngrx/use-consumer-selector": "warn"
701
+ } : {};
702
+ const materialRules = packagePresence.hasAngularMaterial ? {
703
+ "@angular/material/prefix-selector": "warn",
704
+ "@angular/material/no-conflicting-mixins": "error"
705
+ } : {};
476
706
  return [{
477
707
  files: ["**/*.ts"],
478
708
  plugins: {
@@ -482,7 +712,10 @@ const buildEslintConfig = (hasTypeScript, tsconfigPath, useTypeAware) => {
482
712
  languageOptions,
483
713
  rules: {
484
714
  ...angularRules,
485
- ...tsRules
715
+ ...tsRules,
716
+ ...signalsRules,
717
+ ...ngrxRules,
718
+ ...materialRules
486
719
  }
487
720
  }];
488
721
  };
@@ -505,24 +738,70 @@ const parsePluginAndRule = (ruleId) => {
505
738
  };
506
739
  };
507
740
  const runEslint = async (rootDirectory, hasTypeScript, includePaths, options) => {
508
- if (includePaths !== void 0 && includePaths.length === 0) return [];
741
+ if (includePaths !== void 0 && includePaths.length === 0) return {
742
+ diagnostics: [],
743
+ errors: []
744
+ };
509
745
  const tsconfigPath = hasTypeScript ? path.join(rootDirectory, "tsconfig.json") : null;
746
+ let packagePresence;
747
+ if (options?.frameworkInfo) packagePresence = {
748
+ hasNgRx: options.frameworkInfo.hasNgRx,
749
+ hasAngularMaterial: options.frameworkInfo.hasAngularMaterial,
750
+ hasSignals: options.frameworkInfo.hasSignals
751
+ };
752
+ else {
753
+ packagePresence = {
754
+ hasNgRx: false,
755
+ hasAngularMaterial: false,
756
+ hasSignals: true
757
+ };
758
+ try {
759
+ const packageJsonPath = path.join(rootDirectory, "package.json");
760
+ if (fs.existsSync(packageJsonPath)) {
761
+ const packageJsonContent = fs.readFileSync(packageJsonPath, "utf-8");
762
+ packagePresence = detectPackagePresence(JSON.parse(packageJsonContent));
763
+ }
764
+ } catch {}
765
+ }
510
766
  const cacheRoot = path.join(rootDirectory, "node_modules", ".cache", "angular-doctor");
511
767
  fs.mkdirSync(cacheRoot, { recursive: true });
512
768
  const eslint = new ESLint({
513
769
  cwd: rootDirectory,
514
770
  overrideConfigFile: null,
515
- overrideConfig: buildEslintConfig(hasTypeScript, tsconfigPath && fs.existsSync(tsconfigPath) ? tsconfigPath : null, options?.useTypeAware ?? true),
771
+ overrideConfig: buildEslintConfig(hasTypeScript, tsconfigPath && fs.existsSync(tsconfigPath) ? tsconfigPath : null, options?.useTypeAware ?? true, packagePresence),
516
772
  ignore: true,
517
773
  cache: true,
518
774
  cacheLocation: path.join(cacheRoot, ".eslintcache")
519
775
  });
520
776
  const patterns = includePaths ?? ["**/*.ts"];
521
777
  let results;
778
+ const errors = [];
522
779
  try {
523
780
  results = await eslint.lintFiles(patterns);
524
- } catch {
525
- return [];
781
+ } catch (error) {
782
+ const errorMessage = error instanceof Error ? error.message : String(error);
783
+ const errorStack = error instanceof Error ? error.stack : void 0;
784
+ const filePathMatch = errorMessage.match(/^(.+?):\s* /);
785
+ const errorFilePath = filePathMatch ? path.relative(rootDirectory, filePathMatch[1]) : void 0;
786
+ errors.push({
787
+ message: errorMessage,
788
+ stack: errorStack,
789
+ filePath: errorFilePath
790
+ });
791
+ return {
792
+ diagnostics: [{
793
+ filePath: errorFilePath ?? "unknown",
794
+ plugin: "eslint",
795
+ rule: "parse-error",
796
+ severity: "error",
797
+ message: errorMessage,
798
+ help: "Fix the syntax error in this file. ESLint could not parse the file.",
799
+ line: 0,
800
+ column: 0,
801
+ category: "Parse Error"
802
+ }],
803
+ errors
804
+ };
526
805
  }
527
806
  const diagnostics = [];
528
807
  for (const result of results) for (const message of result.messages) {
@@ -541,7 +820,10 @@ const runEslint = async (rootDirectory, hasTypeScript, includePaths, options) =>
541
820
  category: resolveDiagnosticCategory(ruleKey)
542
821
  });
543
822
  }
544
- return diagnostics;
823
+ return {
824
+ diagnostics,
825
+ errors
826
+ };
545
827
  };
546
828
 
547
829
  //#endregion
@@ -696,24 +978,106 @@ const buildFileLineMap = (diagnostics) => {
696
978
  }
697
979
  return fileLines;
698
980
  };
981
+ /**
982
+ * Format file:line:column location string
983
+ */
984
+ const formatLocation = (diagnostic) => {
985
+ const { filePath, line, column } = diagnostic;
986
+ return `${filePath}:${line}${column > 0 ? `:${column}` : ""}`;
987
+ };
988
+ /**
989
+ * Print summary breakdown showing:
990
+ * - Count by severity
991
+ * - Count by category
992
+ * - Top rules by frequency
993
+ */
994
+ const printSummaryBreakdown = (diagnostics) => {
995
+ const errorCount = diagnostics.filter((d) => d.severity === "error").length;
996
+ const warningCount = diagnostics.filter((d) => d.severity === "warning").length;
997
+ const sortedCategories = [...groupBy(diagnostics, (d) => d.category).entries()].sort(([, a], [, b]) => b.length - a.length);
998
+ const sortedRules = [...groupBy(diagnostics, (d) => `${d.plugin}/${d.rule}`).entries()].sort(([, a], [, b]) => b.length - a.length).slice(0, 5);
999
+ logger.break();
1000
+ logger.log(" Summary Breakdown");
1001
+ logger.log(" ─────────────────");
1002
+ const severityParts = [];
1003
+ if (errorCount > 0) severityParts.push(highlighter.error(`✗ ${errorCount} error${errorCount === 1 ? "" : "s"}`));
1004
+ if (warningCount > 0) severityParts.push(highlighter.warn(`⚠ ${warningCount} warning${warningCount === 1 ? "" : "s"}`));
1005
+ if (severityParts.length > 0) {
1006
+ logger.log(` ${severityParts.join(", ")}`);
1007
+ logger.break();
1008
+ }
1009
+ if (sortedCategories.length > 0) {
1010
+ logger.dim(" Categories:");
1011
+ for (const [category, categoryDiags] of sortedCategories) {
1012
+ const count = categoryDiags.length;
1013
+ logger.dim(` ${category}: ${count}`);
1014
+ }
1015
+ logger.break();
1016
+ }
1017
+ if (sortedRules.length > 0) {
1018
+ logger.dim(" Top rules:");
1019
+ for (const [rule, ruleDiags] of sortedRules) {
1020
+ const count = ruleDiags.length;
1021
+ const severity = ruleDiags[0].severity === "error" ? highlighter.error("✗") : highlighter.warn("⚠");
1022
+ logger.dim(` ${severity} ${rule}: ${count}`);
1023
+ }
1024
+ logger.break();
1025
+ }
1026
+ };
699
1027
  const printDiagnostics = (diagnostics, isVerbose) => {
700
1028
  const sortedRuleGroups = sortBySeverity([...groupBy(diagnostics, (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`).entries()]);
701
- for (const [, ruleDiagnostics] of sortedRuleGroups) {
1029
+ const errorGroups = sortedRuleGroups.filter(([, diags]) => diags[0]?.severity === "error");
1030
+ const warningGroups = sortedRuleGroups.filter(([, diags]) => diags[0]?.severity === "warning");
1031
+ if (errorGroups.length > 0) {
1032
+ logger.error(` ${errorGroups.length} error${errorGroups.length === 1 ? "" : "s"}:`);
1033
+ logger.log(highlighter.error(" ─────────────────────────────────────────"));
1034
+ }
1035
+ for (const [ruleKey, ruleDiagnostics] of errorGroups) {
702
1036
  const firstDiagnostic = ruleDiagnostics[0];
703
- const icon = colorizeBySeverity(firstDiagnostic.severity === "error" ? "✗" : "⚠", firstDiagnostic.severity);
1037
+ const icon = colorizeBySeverity("✗", firstDiagnostic.severity);
704
1038
  const count = ruleDiagnostics.length;
705
1039
  const countLabel = count > 1 ? colorizeBySeverity(` (${count})`, firstDiagnostic.severity) : "";
706
- logger.log(` ${icon} ${firstDiagnostic.message}${countLabel}`);
1040
+ logger.log(" ┌────────────────────────────────────────────────");
1041
+ logger.log(` │ ${icon} ${firstDiagnostic.message}${countLabel}`);
707
1042
  if (firstDiagnostic.help) logger.dim(indentMultilineText(firstDiagnostic.help, " "));
708
1043
  if (isVerbose) {
709
- const fileLines = buildFileLineMap(ruleDiagnostics);
710
- for (const [filePath, lines] of fileLines) {
711
- const lineLabel = lines.length > 0 ? `: ${lines.join(", ")}` : "";
712
- logger.dim(` ${filePath}${lineLabel}`);
1044
+ logger.dim(` ${highlighter.info("category:")} ${firstDiagnostic.category}`);
1045
+ logger.dim(` ${highlighter.info("rule:")} ${ruleKey}`);
1046
+ for (const diagnostic of ruleDiagnostics) {
1047
+ const location = formatLocation(diagnostic);
1048
+ logger.dim(` at ${location}`);
713
1049
  }
1050
+ if (ruleDiagnostics.length > 5) logger.dim(` ... and ${ruleDiagnostics.length - 5} more occurrences`);
714
1051
  }
1052
+ logger.log(" └────────────────────────────────────────────────");
715
1053
  logger.break();
716
1054
  }
1055
+ if (warningGroups.length > 0) {
1056
+ if (errorGroups.length > 0) logger.break();
1057
+ logger.warn(` ${warningGroups.length} warning${warningGroups.length === 1 ? "" : "s"}:`);
1058
+ logger.log(highlighter.warn(" ─────────────────────────────────────────"));
1059
+ }
1060
+ for (const [ruleKey, ruleDiagnostics] of warningGroups) {
1061
+ const firstDiagnostic = ruleDiagnostics[0];
1062
+ const icon = colorizeBySeverity("⚠", firstDiagnostic.severity);
1063
+ const count = ruleDiagnostics.length;
1064
+ const countLabel = count > 1 ? colorizeBySeverity(` (${count})`, firstDiagnostic.severity) : "";
1065
+ logger.log(" ┌────────────────────────────────────────────────");
1066
+ logger.log(` │ ${icon} ${firstDiagnostic.message}${countLabel}`);
1067
+ if (firstDiagnostic.help) logger.dim(indentMultilineText(firstDiagnostic.help, " "));
1068
+ if (isVerbose) {
1069
+ logger.dim(` ${highlighter.info("category:")} ${firstDiagnostic.category}`);
1070
+ logger.dim(` ${highlighter.info("rule:")} ${ruleKey}`);
1071
+ for (const diagnostic of ruleDiagnostics) {
1072
+ const location = formatLocation(diagnostic);
1073
+ logger.dim(` at ${location}`);
1074
+ }
1075
+ if (ruleDiagnostics.length > 5) logger.dim(` ... and ${ruleDiagnostics.length - 5} more occurrences`);
1076
+ }
1077
+ logger.log(" └────────────────────────────────────────────────");
1078
+ logger.break();
1079
+ }
1080
+ if (isVerbose && diagnostics.length > 0) printSummaryBreakdown(diagnostics);
717
1081
  };
718
1082
  const formatElapsedTime = (elapsedMilliseconds) => {
719
1083
  if (elapsedMilliseconds < MILLISECONDS_PER_SECOND) return `${Math.round(elapsedMilliseconds)}ms`;
@@ -738,7 +1102,7 @@ const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
738
1102
  }
739
1103
  return sections.join("\n") + "\n";
740
1104
  };
741
- const buildMarkdownReport = (diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount) => {
1105
+ const buildMarkdownReport = (diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount, isDiffMode = false) => {
742
1106
  const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
743
1107
  const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
744
1108
  const affectedFileCount = collectAffectedFiles(diagnostics).size;
@@ -749,8 +1113,11 @@ const buildMarkdownReport = (diagnostics, elapsedMilliseconds, scoreResult, tota
749
1113
  `Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`,
750
1114
  ""
751
1115
  ];
752
- if (scoreResult) lines.push("## Score", "", `**${scoreResult.score} / ${PERFECT_SCORE}** — ${scoreResult.label}`, "");
753
- lines.push("## Summary", "", `- Errors: **${errorCount}**`, `- Warnings: **${warningCount}**`, totalSourceFileCount > 0 ? `- Affected files: **${affectedFileCount}/${totalSourceFileCount}**` : `- Affected files: **${affectedFileCount}**`, `- Elapsed: **${elapsed}**`, "");
1116
+ if (scoreResult) {
1117
+ const labelSuffix = isDiffMode ? " (partial)" : "";
1118
+ lines.push("## Score", "", `**${scoreResult.score} / ${PERFECT_SCORE}** — ${scoreResult.label}${labelSuffix}`, "");
1119
+ }
1120
+ lines.push("## Summary", "", `- Errors: **${errorCount}**`, `- Warnings: **${warningCount}**`, totalSourceFileCount > 0 ? `- Affected files: **${affectedFileCount}/${totalSourceFileCount}**` : `- Affected files: **${affectedFileCount}**`, `- Elapsed: **${elapsed}**`, isDiffMode ? `- Mode: **diff (dead code detection skipped)**` : "", "");
754
1121
  if (diagnostics.length === 0) {
755
1122
  lines.push("## Diagnostics", "", "No issues found.", "");
756
1123
  return lines.join("\n");
@@ -781,7 +1148,7 @@ const resolveReportPath = (report, outputDirectory, baseDirectory) => {
781
1148
  }
782
1149
  return join(outputDirectory, "report.md");
783
1150
  };
784
- const writeDiagnosticsDirectory = (diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount, report, baseDirectory) => {
1151
+ const writeDiagnosticsDirectory = (diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount, report, baseDirectory, isDiffMode = false) => {
785
1152
  const outputDirectory = join(tmpdir(), `angular-doctor-${randomUUID()}`);
786
1153
  mkdirSync(outputDirectory);
787
1154
  const sortedRuleGroups = sortBySeverity([...groupBy(diagnostics, (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`).entries()]);
@@ -790,7 +1157,7 @@ const writeDiagnosticsDirectory = (diagnostics, elapsedMilliseconds, scoreResult
790
1157
  const markdownPath = resolveReportPath(report, outputDirectory, baseDirectory);
791
1158
  if (markdownPath) {
792
1159
  mkdirSync(dirname(markdownPath), { recursive: true });
793
- writeFileSync(markdownPath, buildMarkdownReport(diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount));
1160
+ writeFileSync(markdownPath, buildMarkdownReport(diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount, isDiffMode));
794
1161
  }
795
1162
  return {
796
1163
  outputDirectory,
@@ -886,10 +1253,10 @@ const buildCountsSummaryLine = (diagnostics, totalSourceFileCount, elapsedMillis
886
1253
  renderedParts.push(highlighter.dim(fileCountText), highlighter.dim(elapsedTimeText));
887
1254
  return createFramedLine(plainParts.join(" "), renderedParts.join(" "));
888
1255
  };
889
- const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount, report, baseDirectory) => {
1256
+ const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount, report, baseDirectory, isDiffMode = false) => {
890
1257
  printFramedBox([...buildBrandingLines(scoreResult), buildCountsSummaryLine(diagnostics, totalSourceFileCount, elapsedMilliseconds)]);
891
1258
  try {
892
- const { outputDirectory, markdownPath } = writeDiagnosticsDirectory(diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount, report, baseDirectory);
1259
+ const { outputDirectory, markdownPath } = writeDiagnosticsDirectory(diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount, report, baseDirectory, isDiffMode);
893
1260
  logger.break();
894
1261
  logger.dim(` Full diagnostics written to ${outputDirectory}`);
895
1262
  if (markdownPath) logger.dim(` Markdown report written to ${markdownPath}`);
@@ -906,9 +1273,24 @@ const mergeScanOptions = (inputOptions, userConfig) => {
906
1273
  scoreOnly: inputOptions.scoreOnly ?? false,
907
1274
  report: inputOptions.report ?? false,
908
1275
  useTypeAwareLint: !fastMode,
909
- includePaths: inputOptions.includePaths ?? []
1276
+ includePaths: inputOptions.includePaths ?? [],
1277
+ rules: inputOptions.rules
910
1278
  };
911
1279
  };
1280
+ /**
1281
+ * Parses the --rules CLI flag and returns a FrameworkInfo override.
1282
+ * Categories: signals, ngrx, material
1283
+ * Example: "signals,ngrx" force-enables both signals and ngrx rules.
1284
+ */
1285
+ const parseRulesOverride = (rules) => {
1286
+ if (!rules || rules === "all") return void 0;
1287
+ const override = {};
1288
+ const categories = rules.split(",").map((c) => c.trim().toLowerCase());
1289
+ for (const category of categories) if (category === "signals") override.hasSignals = true;
1290
+ else if (category === "ngrx") override.hasNgRx = true;
1291
+ else if (category === "material") override.hasAngularMaterial = true;
1292
+ return Object.keys(override).length > 0 ? override : void 0;
1293
+ };
912
1294
  const printProjectDetection = (projectInfo, userConfig, isDiffMode, includePaths) => {
913
1295
  const frameworkLabel = formatFrameworkName(projectInfo.framework);
914
1296
  const languageLabel = "TypeScript";
@@ -940,9 +1322,34 @@ const scan = async (directory, inputOptions = {}) => {
940
1322
  if (!options.lint) return [];
941
1323
  const lintSpinner = options.scoreOnly ? null : spinner("Running lint checks...").start();
942
1324
  try {
943
- const lintDiagnostics = await runEslint(directory, projectInfo.hasTypeScript, computedIncludePaths, { useTypeAware: options.useTypeAwareLint });
1325
+ const detectedFrameworkInfo = {
1326
+ hasNgRx: projectInfo.hasNgRx,
1327
+ hasAngularMaterial: projectInfo.hasAngularMaterial,
1328
+ hasSignals: projectInfo.hasSignals
1329
+ };
1330
+ const rulesOverride = parseRulesOverride(options.rules);
1331
+ const frameworkInfo = rulesOverride ? {
1332
+ ...detectedFrameworkInfo,
1333
+ ...rulesOverride
1334
+ } : detectedFrameworkInfo;
1335
+ const result = await runEslint(directory, projectInfo.hasTypeScript, computedIncludePaths, {
1336
+ useTypeAware: options.useTypeAwareLint,
1337
+ frameworkInfo
1338
+ });
944
1339
  lintSpinner?.succeed("Running lint checks.");
945
- return lintDiagnostics;
1340
+ if (result.errors.length > 0) {
1341
+ result.errors;
1342
+ if (options.verbose) {
1343
+ logger.break();
1344
+ logger.error("ESLint encountered the following errors:");
1345
+ for (const error of result.errors) {
1346
+ logger.error(` ${error.message}`);
1347
+ if (error.stack) logger.dim(error.stack);
1348
+ }
1349
+ logger.break();
1350
+ }
1351
+ }
1352
+ return result.diagnostics;
946
1353
  } catch (error) {
947
1354
  didLintFail = true;
948
1355
  lintSpinner?.fail("Lint checks failed (non-fatal, skipping).");
@@ -964,7 +1371,11 @@ const scan = async (directory, inputOptions = {}) => {
964
1371
  return [];
965
1372
  }
966
1373
  };
967
- const [lintDiagnostics, deadCodeDiagnostics] = options.scoreOnly ? await Promise.all([runLint(), runDeadCode()]) : [await runLint(), await runDeadCode()];
1374
+ const parallelStartTime = performance.now();
1375
+ const [lintDiagnostics, deadCodeDiagnostics] = await Promise.all([runLint(), runDeadCode()]);
1376
+ const parallelElapsed = performance.now() - parallelStartTime;
1377
+ const sequentialTime = (options.lint ? parallelElapsed * .6 : 0) + (options.deadCode && !isDiffMode ? parallelElapsed * .4 : 0);
1378
+ if (parallelElapsed > 1e3) logger.dim(` Parallel scan: ${formatElapsedTime(parallelElapsed)} (sequential would be ~${formatElapsedTime(sequentialTime)})`);
968
1379
  const diagnostics = combineDiagnostics(lintDiagnostics, deadCodeDiagnostics, userConfig);
969
1380
  const elapsedMilliseconds = performance.now() - startTime;
970
1381
  const skippedChecks = [];
@@ -972,12 +1383,16 @@ const scan = async (directory, inputOptions = {}) => {
972
1383
  if (didDeadCodeFail) skippedChecks.push("dead code");
973
1384
  const hasSkippedChecks = skippedChecks.length > 0;
974
1385
  const scoreResult = calculateScore(diagnostics);
1386
+ const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
1387
+ const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
975
1388
  if (options.scoreOnly) {
976
1389
  logger.log(`${scoreResult.score}`);
977
1390
  return {
978
1391
  diagnostics,
979
1392
  scoreResult,
980
- skippedChecks
1393
+ skippedChecks,
1394
+ errorCount,
1395
+ warningCount
981
1396
  };
982
1397
  }
983
1398
  if (diagnostics.length === 0) {
@@ -996,11 +1411,13 @@ const scan = async (directory, inputOptions = {}) => {
996
1411
  return {
997
1412
  diagnostics,
998
1413
  scoreResult,
999
- skippedChecks
1414
+ skippedChecks,
1415
+ errorCount,
1416
+ warningCount
1000
1417
  };
1001
1418
  }
1002
1419
  printDiagnostics(diagnostics, options.verbose);
1003
- printSummary(diagnostics, elapsedMilliseconds, scoreResult, isDiffMode ? includePaths.length : projectInfo.sourceFileCount, options.report, directory);
1420
+ printSummary(diagnostics, elapsedMilliseconds, scoreResult, isDiffMode ? includePaths.length : projectInfo.sourceFileCount, options.report, directory, isDiffMode);
1004
1421
  if (hasSkippedChecks) {
1005
1422
  const skippedLabel = skippedChecks.join(" and ");
1006
1423
  logger.break();
@@ -1009,7 +1426,9 @@ const scan = async (directory, inputOptions = {}) => {
1009
1426
  return {
1010
1427
  diagnostics,
1011
1428
  scoreResult,
1012
- skippedChecks
1429
+ skippedChecks,
1430
+ errorCount,
1431
+ warningCount
1013
1432
  };
1014
1433
  };
1015
1434
 
@@ -1160,7 +1579,7 @@ const selectProjects = async (rootDirectory, projectFlag, skipPrompts) => {
1160
1579
 
1161
1580
  //#endregion
1162
1581
  //#region src/cli.ts
1163
- const VERSION = "1.2.0";
1582
+ const VERSION = "1.3.0";
1164
1583
  const exitWithHint = () => {
1165
1584
  logger.break();
1166
1585
  logger.log("Cancelled.");
@@ -1186,7 +1605,8 @@ const resolveCliScanOptions = (flags, userConfig, programInstance) => {
1186
1605
  verbose: isCliOverride("verbose") ? Boolean(flags.verbose) : userConfig?.verbose ?? false,
1187
1606
  scoreOnly: flags.score,
1188
1607
  report: flags.report,
1189
- fast: isCliOverride("fast") ? Boolean(flags.fast) : userConfig?.fast ?? false
1608
+ fast: isCliOverride("fast") ? Boolean(flags.fast) : userConfig?.fast ?? false,
1609
+ rules: flags.rules
1190
1610
  };
1191
1611
  };
1192
1612
  const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isScoreOnly) => {
@@ -1204,7 +1624,7 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isSco
1204
1624
  if (isScoreOnly) return false;
1205
1625
  return false;
1206
1626
  };
1207
- const program = new Command().name("angular-doctor").description("Diagnose Angular codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--no-lint", "skip linting").option("--no-dead-code", "skip dead code detection").option("--verbose", "show file details per rule").option("--score", "output only the score").option("--report [path]", "write a markdown report (optional output path)").option("--fast", "speed up by skipping dead code and type-aware lint").option("-y, --yes", "skip prompts, scan all workspace projects").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch").action(async (directory, flags) => {
1627
+ const program = new Command().name("angular-doctor").description("Diagnose Angular codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--no-lint", "skip linting").option("--no-dead-code", "skip dead code detection").option("--verbose", "show file details per rule").option("--score", "output only the score").option("--report [path]", "write a markdown report (optional output path)").option("--fast", "speed up by skipping dead code and type-aware lint").option("-y, --yes", "skip prompts, scan all workspace projects").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch (Note: dead code detection is skipped in diff mode)").option("--rules <categories>", "force-enable specific rule categories (signals,ngrx,material) or 'all'").option("--exit-code", "exit with non-zero code when ESLint errors are found (for CI integration)").action(async (directory, flags) => {
1208
1628
  const isScoreOnly = flags.score;
1209
1629
  try {
1210
1630
  const resolvedDirectory = path.resolve(directory);
@@ -1223,6 +1643,7 @@ const program = new Command().name("angular-doctor").description("Diagnose Angul
1223
1643
  if (isDiffMode && diffInfo && !isScoreOnly) {
1224
1644
  if (diffInfo.isCurrentChanges) logger.log("Scanning uncommitted changes");
1225
1645
  else logger.log(`Scanning changes: ${highlighter.info(diffInfo.currentBranch)} → ${highlighter.info(diffInfo.baseBranch)}`);
1646
+ logger.warn("Note: Dead code detection skipped in diff mode. Run without --diff for full scan.");
1226
1647
  logger.break();
1227
1648
  }
1228
1649
  for (const projectDirectory of projectDirectories) {
@@ -1245,10 +1666,11 @@ const program = new Command().name("angular-doctor").description("Diagnose Angul
1245
1666
  logger.dim(`Scanning ${projectDirectory}...`);
1246
1667
  logger.break();
1247
1668
  }
1248
- await scan(projectDirectory, {
1669
+ const scanResult = await scan(projectDirectory, {
1249
1670
  ...scanOptions,
1250
1671
  includePaths
1251
1672
  });
1673
+ if ((flags.exitCode || isAutomatedEnvironment()) && scanResult.errorCount > 0) process.exitCode = 1;
1252
1674
  if (!isScoreOnly) logger.break();
1253
1675
  }
1254
1676
  } catch (error) {