1515
1616try :
1717 from revenueholdings_license import require_license
18+
1819 _HAS_RH_LICENSE = True
1920except ImportError :
2021 _HAS_RH_LICENSE = False
2526@click .group ()
2627@click .version_option (package_name = "deploydiff" )
2728@click .option ("--no-gate" , is_flag = True , help = "Skip license gating check." )
29+ @click .option (
30+ "--require-license" ,
31+ "require_license_flag" ,
32+ is_flag = True ,
33+ envvar = "REVENUEHOLDINGS_REQUIRE_LICENSE" ,
34+ help = (
35+ "Exit with an error if revenueholdings-license is not installed "
36+ "or if the license check fails. "
37+ "Also enabled via REVENUEHOLDINGS_REQUIRE_LICENSE=1."
38+ ),
39+ )
2840@click .pass_context
29- def main (ctx , no_gate ) -> None :
41+ def main (ctx , no_gate , require_license_flag ) -> None :
3042 """DeployDiff - Preview infrastructure changes with cost impact and rollback."""
3143 ctx .ensure_object (dict )
3244 ctx .obj ["no_gate" ] = no_gate
33- if _HAS_RH_LICENSE and not no_gate :
34- require_license ("deploydiff" )
45+ ctx .obj ["require_license_flag" ] = require_license_flag
46+ if not no_gate :
47+ if _HAS_RH_LICENSE :
48+ require_license ("deploydiff" )
49+ elif require_license_flag :
50+ console .print (
51+ "[bold red]Error:[/bold red] revenueholdings-license is not installed. "
52+ "Install it with: pip install revenueholdings-license"
53+ )
54+ raise SystemExit (1 )
3555
3656
3757@main .command ()
38- @click .option ("--tf" , "terraform_file" , type = click .Path (exists = True ), help = "Terraform plan JSON file" )
39- @click .option ("--cfn" , "cloudformation_file" , type = click .Path (exists = True ), help = "CloudFormation change set JSON file" )
40- @click .option ("--pulumi" , "pulumi_file" , type = click .Path (exists = True ), help = "Pulumi preview JSON file" )
41- @click .option ("-v" , "--verbose" , is_flag = True , help = "Show before/after details for each change" )
58+ @click .option (
59+ "--tf" ,
60+ "terraform_file" ,
61+ type = click .Path (exists = True ),
62+ help = "Terraform plan JSON file" ,
63+ )
64+ @click .option (
65+ "--cfn" ,
66+ "cloudformation_file" ,
67+ type = click .Path (exists = True ),
68+ help = "CloudFormation change set JSON file" ,
69+ )
70+ @click .option (
71+ "--pulumi" ,
72+ "pulumi_file" ,
73+ type = click .Path (exists = True ),
74+ help = "Pulumi preview JSON file" ,
75+ )
76+ @click .option (
77+ "-v" , "--verbose" , is_flag = True , help = "Show before/after details for each change"
78+ )
4279@click .option (
4380 "--exit-on-destroy" ,
4481 is_flag = True ,
4582 help = "Exit with code 1 if the plan contains destructive changes (deletes or replaces)" ,
4683)
47- def preview (terraform_file , cloudformation_file , pulumi_file , verbose , exit_on_destroy ) -> None :
84+ def preview (
85+ terraform_file , cloudformation_file , pulumi_file , verbose , exit_on_destroy
86+ ) -> None :
4887 """Preview infrastructure changes from a plan file."""
4988 plan = _load_plan (terraform_file , cloudformation_file , pulumi_file )
5089 if plan is None :
@@ -62,20 +101,43 @@ def preview(terraform_file, cloudformation_file, pulumi_file, verbose, exit_on_d
62101
63102
64103@main .command ()
65- @click .option ("--tf" , "terraform_file" , type = click .Path (exists = True ), help = "Terraform plan JSON file" )
66- @click .option ("--cfn" , "cloudformation_file" , type = click .Path (exists = True ), help = "CloudFormation change set JSON file" )
67- @click .option ("--pulumi" , "pulumi_file" , type = click .Path (exists = True ), help = "Pulumi preview JSON file" )
68- @click .option ("--pricing" , "pricing_file" , type = click .Path (exists = True ), help = "Custom pricing JSON file" )
104+ @click .option (
105+ "--tf" ,
106+ "terraform_file" ,
107+ type = click .Path (exists = True ),
108+ help = "Terraform plan JSON file" ,
109+ )
110+ @click .option (
111+ "--cfn" ,
112+ "cloudformation_file" ,
113+ type = click .Path (exists = True ),
114+ help = "CloudFormation change set JSON file" ,
115+ )
116+ @click .option (
117+ "--pulumi" ,
118+ "pulumi_file" ,
119+ type = click .Path (exists = True ),
120+ help = "Pulumi preview JSON file" ,
121+ )
122+ @click .option (
123+ "--pricing" ,
124+ "pricing_file" ,
125+ type = click .Path (exists = True ),
126+ help = "Custom pricing JSON file" ,
127+ )
69128@click .option (
70129 "--threshold" ,
71130 type = float ,
72131 default = None ,
73132 help = "Exit with code 1 if total monthly cost delta exceeds this value (e.g. 500 for $500)" ,
74133)
75- def cost (terraform_file , cloudformation_file , pulumi_file , pricing_file , threshold ) -> None :
134+ def cost (
135+ terraform_file , cloudformation_file , pulumi_file , pricing_file , threshold
136+ ) -> None :
76137 """Estimate monthly cost impact of infrastructure changes. (Pro feature)"""
77138 if _HAS_RH_LICENSE :
78139 from revenueholdings_license import require_tier
140+
79141 require_tier ("pro" , "deploydiff cost" )
80142 plan = _load_plan (terraform_file , cloudformation_file , pulumi_file )
81143 if plan is None :
@@ -95,13 +157,29 @@ def cost(terraform_file, cloudformation_file, pulumi_file, pricing_file, thresho
95157
96158
97159@main .command ()
98- @click .option ("--tf" , "terraform_file" , type = click .Path (exists = True ), help = "Terraform plan JSON file" )
99- @click .option ("--cfn" , "cloudformation_file" , type = click .Path (exists = True ), help = "CloudFormation change set JSON file" )
100- @click .option ("--pulumi" , "pulumi_file" , type = click .Path (exists = True ), help = "Pulumi preview JSON file" )
160+ @click .option (
161+ "--tf" ,
162+ "terraform_file" ,
163+ type = click .Path (exists = True ),
164+ help = "Terraform plan JSON file" ,
165+ )
166+ @click .option (
167+ "--cfn" ,
168+ "cloudformation_file" ,
169+ type = click .Path (exists = True ),
170+ help = "CloudFormation change set JSON file" ,
171+ )
172+ @click .option (
173+ "--pulumi" ,
174+ "pulumi_file" ,
175+ type = click .Path (exists = True ),
176+ help = "Pulumi preview JSON file" ,
177+ )
101178def rollback (terraform_file , cloudformation_file , pulumi_file ) -> None :
102179 """Generate rollback commands for infrastructure changes. (Pro feature)"""
103180 if _HAS_RH_LICENSE :
104181 from revenueholdings_license import require_tier
182+
105183 require_tier ("pro" , "deploydiff rollback" )
106184 plan = _load_plan (terraform_file , cloudformation_file , pulumi_file )
107185 if plan is None :
@@ -113,7 +191,6 @@ def rollback(terraform_file, cloudformation_file, pulumi_file) -> None:
113191 console .print (cmd )
114192
115193
116-
117194def _load_plan (
118195 terraform_file : str | None ,
119196 cloudformation_file : str | None ,
@@ -126,7 +203,9 @@ def _load_plan(
126203 if len (provided ) == 0 :
127204 return None
128205 if len (provided ) > 1 :
129- console .print ("[red]Error: Provide only one source file (--tf, --cfn, or --pulumi)[/red]" )
206+ console .print (
207+ "[red]Error: Provide only one source file (--tf, --cfn, or --pulumi)[/red]"
208+ )
130209 raise SystemExit (1 )
131210
132211 if terraform_file :
@@ -139,7 +218,9 @@ def _load_plan(
139218 return None
140219
141220
142- def _render_costs (estimates : list [CostEstimate ], plan : DeployPlan , console : Console ) -> None :
221+ def _render_costs (
222+ estimates : list [CostEstimate ], plan : DeployPlan , console : Console
223+ ) -> None :
143224 """Render cost estimates to the console."""
144225 from rich import box
145226 from rich .table import Table
@@ -172,7 +253,9 @@ def _render_costs(estimates: list[CostEstimate], plan: DeployPlan, console: Cons
172253 if total > 0 :
173254 console .print (f"\n [bold red]Total monthly increase: +${ total :.2f} [/bold red]" )
174255 elif total < 0 :
175- console .print (f"\n [bold green]Total monthly decrease: -${ abs (total ):.2f} [/bold green]" )
256+ console .print (
257+ f"\n [bold green]Total monthly decrease: -${ abs (total ):.2f} [/bold green]"
258+ )
176259 else :
177260 console .print ("\n [bold]Total monthly change: $0.00[/bold]" )
178261
@@ -188,8 +271,7 @@ def mcp() -> None:
188271 from .mcp_server import run_for_app
189272 except ImportError as exc :
190273 console .print (
191- "[red]Error: click-to-mcp is not installed.[/red]\n "
192- "Install it with: [bold]pip install click-to-mcp[/bold]"
274+ "[red]Error: click-to-mcp is not installed.[/red]\n Install it with: [bold]pip install click-to-mcp[/bold]"
193275 )
194276 raise SystemExit (1 ) from exc
195277
0 commit comments