33from __future__ import annotations
44
55import json
6+ import os
67import sys
78from pathlib import Path
89
1415from .config import DeadCodeConfig
1516from .scanner import DeadCodeScanner , Finding
1617
18+ try :
19+ from revenueholdings_license import require_license as _rh_require_license
20+
21+ _HAS_RH_LICENSE = True
22+ except ImportError :
23+ _HAS_RH_LICENSE = False
24+
25+ def _rh_require_license (product : str ) -> None : # type: ignore[misc]
26+ pass
27+
28+
1729console = Console ()
1830err_console = Console (stderr = True )
1931
3749 multiple = True ,
3850 help = "Include only matching files (gitignore-style whitelist)" ,
3951)
52+ @click .option (
53+ "--require-license" ,
54+ is_flag = True ,
55+ envvar = "REVENUEHOLDINGS_REQUIRE_LICENSE" ,
56+ help = (
57+ "Exit with an error if revenueholdings-license is not installed "
58+ "or if the license check fails. "
59+ "Also enabled via REVENUEHOLDINGS_REQUIRE_LICENSE=1."
60+ ),
61+ )
4062@click .version_option (__version__ , prog_name = "deadcode" )
4163@click .pass_context
4264def cli (
43- ctx : click .Context , project : str , ignore : tuple [str , ...], include : tuple [str , ...]
65+ ctx : click .Context , project : str , ignore : tuple [str , ...], include : tuple [str , ...],
66+ require_license : bool ,
4467) -> None :
4568 """DeadCode — Find and remove dead code in TS/React/Next.js projects.
4669
@@ -51,9 +74,23 @@ def cli(
5174 ctx .obj ["project" ] = project
5275 ctx .obj ["ignore" ] = list (ignore ) if ignore else None
5376 ctx .obj ["include" ] = list (include ) if include else None
77+ ctx .obj ["require_license" ] = require_license or bool (
78+ os .environ .get ("REVENUEHOLDINGS_REQUIRE_LICENSE" )
79+ )
5480 # Load .deadcode.yml config
5581 ctx .obj ["config" ] = DeadCodeConfig .load (project )
5682
83+ # License check
84+ strict = ctx .obj ["require_license" ]
85+ if _HAS_RH_LICENSE :
86+ _rh_require_license ("deadcode" )
87+ elif strict :
88+ err_console .print (
89+ "[bold red]Error:[/bold red] revenueholdings-license is not installed. "
90+ "Install it with: pip install revenueholdings-license"
91+ )
92+ sys .exit (1 )
93+
5794
5895def _merge_config_ignore (ctx : click .Context ) -> list [str ] | None :
5996 """Merge CLI --ignore flags with .deadcode.yml ignore patterns."""
@@ -155,7 +192,7 @@ def scan(
155192 console .print ("OK — 0 findings" )
156193 else :
157194 for f in findings :
158- console .print (f"{ f .file } :{ f .line } \u2014 { f .category } : { f .name } " )
195+ console .print (f"{ f .file } :{ f .line } — { f .category } : { f .name } " )
159196 console .print (f"\n { len (findings )} findings" )
160197 elif effective_format == "github" :
161198 # GitHub Actions annotation syntax
@@ -177,7 +214,7 @@ def scan(
177214 )
178215
179216 if not findings :
180- console .print ("[green]✓ No dead code found![/green]" )
217+ console .print ("[green]\u2713 No dead code found![/green]" )
181218 else :
182219 # Group by category
183220 by_category : dict [str , list [Finding ]] = {}
@@ -291,7 +328,7 @@ def remove(ctx: click.Context, dry_run: bool, category: str | None) -> None:
291328 removable = [f for f in findings if f .removable ]
292329
293330 if not removable :
294- console .print ("[green]✓ Nothing removable found.[/green]" )
331+ console .print ("[green]\u2713 Nothing removable found.[/green]" )
295332 return
296333
297334 # Group by file
@@ -332,7 +369,7 @@ def remove(ctx: click.Context, dry_run: bool, category: str | None) -> None:
332369 filepath .write_text ("" .join (lines ), encoding = "utf-8" )
333370 removed_count += len (lines_to_remove )
334371 console .print (
335- f"[green]✓ [/green] Cleaned { rel_file } ({ len (lines_to_remove )} lines)"
372+ f"[green]\u2713 [/green] Cleaned { rel_file } ({ len (lines_to_remove )} lines)"
336373 )
337374
338375 action = "Would remove" if dry_run else "Removed"
0 commit comments