1515from rich .text import Text
1616
1717from datajunction import DJBuilder , Project
18- from datajunction .deployment import DeploymentService
18+ from datajunction .deployment import DeploymentService , display_impact_analysis
1919from datajunction .exceptions import DJClientException , DJDeploymentFailure
2020
2121logging .basicConfig (level = logging .INFO )
2222logger = logging .getLogger (__name__ )
2323
2424
25- def display_impact_analysis (impact : dict , console : Console | None = None ) -> None :
26- """
27- Display deployment impact analysis with rich formatting.
28-
29- Args:
30- impact: The impact analysis response from the server
31- console: Optional Rich console for output
32- """
33- console = console or Console ()
34- namespace = impact .get ("namespace" , "unknown" )
35-
36- # Header
37- console .print ()
38- console .print (
39- f"[bold blue]📊 Impact Analysis for namespace:[/bold blue] [bold green]{ namespace } [/bold green]" ,
40- )
41- console .print ("━" * 60 )
42- console .print ()
43-
44- # Direct Changes Table
45- changes = impact .get ("changes" , [])
46- if changes :
47- changes_table = Table (
48- title = "[bold]📝 Direct Changes[/bold]" ,
49- box = box .ROUNDED ,
50- show_header = True ,
51- header_style = "bold cyan" ,
52- )
53- changes_table .add_column ("Operation" , style = "bold" , width = 10 )
54- changes_table .add_column ("Node" , style = "magenta" )
55- changes_table .add_column ("Type" , style = "dim" , width = 12 )
56- changes_table .add_column ("Changed Fields" , style = "white" )
57-
58- operation_styles = {
59- "create" : ("🟢 Create" , "green" ),
60- "update" : ("🟡 Update" , "yellow" ),
61- "delete" : ("🔴 Delete" , "red" ),
62- "noop" : ("⚪ Skip" , "dim" ),
63- }
64-
65- for change in changes :
66- op = change .get ("operation" , "unknown" )
67- op_display , op_color = operation_styles .get (op , (op .upper (), "white" ))
68- changed_fields = ", " .join (change .get ("changed_fields" , [])) or "-"
69- changes_table .add_row (
70- f"[{ op_color } ]{ op_display } [/{ op_color } ]" ,
71- change .get ("name" , "" ),
72- change .get ("node_type" , "" ),
73- changed_fields ,
74- )
75-
76- console .print (changes_table )
77- console .print ()
78-
79- # Summary for direct changes
80- create_count = impact .get ("create_count" , 0 )
81- update_count = impact .get ("update_count" , 0 )
82- delete_count = impact .get ("delete_count" , 0 )
83- skip_count = impact .get ("skip_count" , 0 )
84-
85- summary_parts = []
86- if create_count :
87- summary_parts .append (
88- f"[green]{ create_count } create{ 's' if create_count != 1 else '' } [/green]" ,
89- )
90- if update_count :
91- summary_parts .append (
92- f"[yellow]{ update_count } update{ 's' if update_count != 1 else '' } [/yellow]" ,
93- )
94- if delete_count :
95- summary_parts .append (
96- f"[red]{ delete_count } delete{ 's' if delete_count != 1 else '' } [/red]" ,
97- )
98- if skip_count :
99- summary_parts .append (f"[dim]{ skip_count } skipped[/dim]" )
100-
101- if summary_parts :
102- console .print (f"[bold]Summary:[/bold] { ', ' .join (summary_parts )} " )
103- console .print ()
104-
105- # Column Changes (if any)
106- column_changes_found = []
107- for change in changes :
108- for col_change in change .get ("column_changes" , []):
109- column_changes_found .append (
110- {
111- "node" : change .get ("name" ),
112- ** col_change ,
113- },
114- )
115-
116- if column_changes_found :
117- col_table = Table (
118- title = "[bold]⚡ Column Changes[/bold]" ,
119- box = box .ROUNDED ,
120- show_header = True ,
121- header_style = "bold cyan" ,
122- )
123- col_table .add_column ("Node" , style = "magenta" )
124- col_table .add_column ("Change" , style = "bold" , width = 10 )
125- col_table .add_column ("Details" , style = "white" )
126-
127- change_type_styles = {
128- "added" : ("🟢 Added" , "green" ),
129- "removed" : ("🔴 Removed" , "red" ),
130- "type_changed" : ("🟡 Type Changed" , "yellow" ),
131- }
132-
133- for col in column_changes_found :
134- change_type = col .get ("change_type" , "unknown" )
135- display , color = change_type_styles .get (
136- change_type ,
137- (change_type .upper (), "white" ),
138- )
139-
140- if change_type == "type_changed" :
141- details = f"'{ col .get ('column' )} ': { col .get ('old_type' )} → { col .get ('new_type' )} "
142- elif change_type == "removed" :
143- details = f"Column '{ col .get ('column' )} ' removed"
144- else :
145- details = f"Column '{ col .get ('column' )} ' added"
146-
147- col_table .add_row (
148- col .get ("node" , "" ),
149- f"[{ color } ]{ display } [/{ color } ]" ,
150- details ,
151- )
152-
153- console .print (col_table )
154- console .print ()
155-
156- # Downstream Impact
157- downstream_impacts = impact .get ("downstream_impacts" , [])
158- if downstream_impacts :
159- impact_table = Table (
160- title = "[bold]🔗 Downstream Impact[/bold]" ,
161- box = box .ROUNDED ,
162- show_header = True ,
163- header_style = "bold cyan" ,
164- )
165- impact_table .add_column ("Node" , style = "magenta" )
166- impact_table .add_column ("Impact" , style = "bold" , width = 18 )
167- impact_table .add_column ("Reason" , style = "white" )
168-
169- impact_styles = {
170- "will_invalidate" : ("❌ Will Invalidate" , "bold red" ),
171- "may_affect" : ("⚠️ May Affect" , "yellow" ),
172- "unchanged" : ("✓ Unchanged" , "dim" ),
173- }
174-
175- for downstream in downstream_impacts :
176- impact_type = downstream .get ("impact_type" , "unknown" )
177- display , style = impact_styles .get (
178- impact_type ,
179- (impact_type .upper (), "white" ),
180- )
181- impact_table .add_row (
182- downstream .get ("name" , "" ),
183- f"[{ style } ]{ display } [/{ style } ]" ,
184- downstream .get ("impact_reason" , "" ),
185- )
186-
187- console .print (impact_table )
188- console .print ()
189-
190- # Downstream summary
191- will_invalidate = impact .get ("will_invalidate_count" , 0 )
192- may_affect = impact .get ("may_affect_count" , 0 )
193-
194- impact_summary = []
195- if will_invalidate :
196- impact_summary .append (f"[red]{ will_invalidate } will invalidate[/red]" )
197- if may_affect :
198- impact_summary .append (f"[yellow]{ may_affect } may be affected[/yellow]" )
199-
200- if impact_summary :
201- console .print (
202- f"[bold]Downstream Summary:[/bold] { ', ' .join (impact_summary )} " ,
203- )
204- console .print ()
205- else :
206- console .print ("[green]✅ No downstream impact detected.[/green]" )
207- console .print ()
208-
209- # Warnings
210- warnings = impact .get ("warnings" , [])
211- if warnings :
212- console .print ("[bold red]⚠️ Warnings:[/bold red]" )
213- for warning in warnings :
214- console .print (f" [yellow]• { warning } [/yellow]" )
215- console .print ()
216- else :
217- console .print ("[green]✅ No warnings.[/green]" )
218- console .print ()
219-
220- # Final verdict
221- has_issues = (
222- impact .get ("will_invalidate_count" , 0 ) > 0
223- or len (warnings ) > 0
224- or delete_count > 0
225- )
226-
227- if has_issues :
228- console .print (
229- "[yellow bold]⚠️ Review the warnings and downstream impacts before deploying.[/yellow bold]" ,
230- )
231- else :
232- console .print ("[green bold]✅ Ready to deploy![/green bold]" )
233-
234- console .print ()
235-
236-
23725class DJCLI :
23826 """DJ command-line tool"""
23927
@@ -281,14 +69,24 @@ def dryrun(
28169 console = Console ()
28270
28371 try :
284- impact = self .deployment_service .get_impact (directory , namespace = namespace )
72+ with console .status (
73+ "[dim]Analyzing deployment impact...[/dim]" ,
74+ spinner = "dots" ,
75+ ):
76+ impact = self .deployment_service .get_impact (
77+ directory ,
78+ namespace = namespace ,
79+ )
28580
28681 if format == "json" :
28782 print (json .dumps (impact , indent = 2 ))
28883 else :
289- console .print (f"[bold]Analyzing deployment from:[/bold] { directory } " )
290- console .print ()
291- display_impact_analysis (impact , console = console )
84+ display_impact_analysis (
85+ impact ,
86+ console = console ,
87+ server_url = self .builder_client .uri ,
88+ source = DeploymentService ._build_deployment_source (cwd = directory ),
89+ )
29290 except DJClientException as exc :
29391 error_data = exc .args [0 ] if exc .args else str (exc )
29492 message = (
@@ -1357,8 +1155,7 @@ def dispatch_command(self, args, parser):
13571155 return
13581156 try :
13591157 self .push (args .directory , verbose = args .verbose , force = args .force )
1360- except DJDeploymentFailure as exc :
1361- logger .error ("Deployment failed: %s" , exc )
1158+ except DJDeploymentFailure :
13621159 raise SystemExit (1 )
13631160 elif args .command == "push" :
13641161 # Handle dry run first
@@ -1387,8 +1184,7 @@ def dispatch_command(self, args, parser):
13871184 verbose = args .verbose ,
13881185 force = args .force ,
13891186 )
1390- except DJDeploymentFailure as exc :
1391- logger .error ("Deployment failed: %s" , exc )
1187+ except DJDeploymentFailure :
13921188 raise SystemExit (1 )
13931189 elif args .command == "generate-codeowners" :
13941190 count = DeploymentService .build_codeowners (
@@ -1649,7 +1445,7 @@ def _setup_mcp_server(self, console: Console):
16491445
16501446 if not config_path :
16511447 console .print (
1652- "\n [yellow]⚠️ Could not find Claude config directory[/yellow]" ,
1448+ "\n [yellow]▲ Could not find Claude config directory[/yellow]" ,
16531449 )
16541450 console .print (
16551451 "[dim]Skipping MCP setup. You can manually add DJ MCP to your Claude config.[/dim]" ,
@@ -1662,7 +1458,7 @@ def _setup_mcp_server(self, console: Console):
16621458 # Find dj-mcp command
16631459 dj_mcp_path = shutil .which ("dj-mcp" )
16641460 if not dj_mcp_path :
1665- console .print ("[yellow]⚠️ dj-mcp command not found in PATH[/yellow]" )
1461+ console .print ("[yellow]▲ dj-mcp command not found in PATH[/yellow]" )
16661462 console .print (
16671463 "[dim]Make sure datajunction client is installed with MCP extras:[/dim]" ,
16681464 )
@@ -1680,7 +1476,7 @@ def _setup_mcp_server(self, console: Console):
16801476 config = json .load (f )
16811477 except json .JSONDecodeError :
16821478 console .print (
1683- "[yellow]⚠️ Invalid JSON in Claude config, creating backup[/yellow]" ,
1479+ "[yellow]▲ Invalid JSON in Claude config, creating backup[/yellow]" ,
16841480 )
16851481 backup_path = config_path .with_suffix (".json.backup" )
16861482 shutil .copy (config_path , backup_path )
@@ -1716,7 +1512,7 @@ def _setup_mcp_server(self, console: Console):
17161512 console .print (" [dim]DJ_USERNAME: dj[/dim]" )
17171513 console .print (" [dim]DJ_PASSWORD: *** (default)[/dim]\n " )
17181514 console .print (
1719- "[bold yellow]⚠️ Restart Claude Desktop/Code to load the MCP server[/bold yellow]" ,
1515+ "[bold yellow]▲ Restart Claude Desktop/Code to load the MCP server[/bold yellow]" ,
17201516 )
17211517
17221518 def seed (self , type : str = "nodes" ):
0 commit comments