22 "argparse" ,
33 "collections" ,
44 "collections.abc" ,
5+ "os" ,
56 "pathlib" ,
6- "rich" ,
7- "rich.columns" ,
8- "rich.panel" ,
97 "sp_repo_review._compat" ,
108 "sp_repo_review.checks" ,
119 "sp_repo_review.checks.ruff" ,
1210 "sys" ,
11+ "typing" ,
1312]
1413
1514import argparse
15+ import importlib
1616import importlib .resources
1717import json
18+ import os
1819import sys
1920from collections .abc import Iterator , Mapping
21+ from importlib .util import find_spec
2022from pathlib import Path
2123
22- from rich import print
23- from rich .columns import Columns
24- from rich .panel import Panel
25-
2624from sp_repo_review ._compat import tomllib
2725from sp_repo_review .checks .ruff import get_rule_selection , ruff
2826
4341 IGNORE_INFO = json .load (f )
4442
4543
46- def print_each (items : Mapping [str , str ]) -> Iterator [str ]:
44+ def _is_agent_environment () -> bool :
45+ """Check if running from an AI coding agent.
46+
47+ Uses environment variables from https://github.com/agentsmd/agents.md/issues/136
48+ """
49+
50+ # Tool-specific agent variables
51+ agent_vars = [
52+ "AGENT" , # Pi, Goose, Amp
53+ "CLAUDECODE" ,
54+ "CURSOR_AGENT" ,
55+ "CLINE_ACTIVE" ,
56+ "GEMINI_CLI" ,
57+ "CODEX_SANDBOX" ,
58+ "AUGMENT_AGENT" ,
59+ "TRAE_AI_SHELL_ID" ,
60+ "OPENCODE_CLIENT" ,
61+ ]
62+
63+ return any (os .environ .get (var ) for var in agent_vars )
64+
65+
66+ def _resolve_format (format_arg : str ) -> str :
67+ """Resolve 'auto' format to either 'rich' or 'plain'."""
68+ if format_arg != "auto" :
69+ return format_arg
70+
71+ if _is_agent_environment ():
72+ return "plain"
73+
74+ return "rich" if _has_rich () else "plain"
75+
76+
77+ def _has_rich () -> bool :
78+ return find_spec ("rich" ) is not None
79+
80+
81+ def _print_each_plain (items : Mapping [str , str ], indent : int = 2 ) -> Iterator [str ]:
82+ """Generate plain text formatted rule lines."""
83+ size = max (len (k ) for k in items ) if items else 0
84+ for k , v in items .items ():
85+ yield f"{ ' ' * indent } \" { k } \" ,{ ' ' * (size - len (k ))} # { v } "
86+
87+
88+ def _print_each_rich (items : Mapping [str , str ]) -> Iterator [str ]:
89+ """Generate rich formatted rule lines."""
4790 size = max (len (k ) for k in items ) if items else 0
4891 for k , v in items .items ():
4992 kk = f'[green]"{ k } "[/green],'
5093 yield f" { kk :{size + 18 }} [dim]# { v } [/dim]"
5194
5295
53- def process_dir (path : Path ) -> None :
96+ def _output_error (fmt : str , message : str ) -> None :
97+ """Output error message in appropriate format."""
98+ if fmt == "rich" :
99+ import rich
100+
101+ rich .print (message , file = sys .stderr )
102+ else :
103+ print (message , file = sys .stderr )
104+
105+
106+ def _print_output_rich (
107+ selected_items : dict [str , str ],
108+ libs_items : dict [str , str ],
109+ spec_items : dict [str , str ],
110+ unselected_items : dict [str , str ],
111+ ) -> None :
112+ """Print rich formatted output."""
113+ import rich .columns
114+ import rich .panel
115+
116+ panel_sel = rich .panel .Panel (
117+ "\n " .join (_print_each_rich (selected_items )),
118+ title = "Selected" ,
119+ border_style = "green" ,
120+ )
121+ panel_lib = rich .panel .Panel (
122+ "\n " .join (_print_each_rich (libs_items )),
123+ title = "Library specific" ,
124+ border_style = "yellow" ,
125+ )
126+ panel_spec = rich .panel .Panel (
127+ "\n " .join (_print_each_rich (spec_items )),
128+ title = "Specialized" ,
129+ border_style = "yellow" ,
130+ )
131+ uns = "\n " .join (_print_each_rich (unselected_items ))
132+
133+ rich .print (rich .columns .Columns ([panel_sel , panel_lib , panel_spec ]))
134+ if uns :
135+ rich .print ("[red]Unselected [dim](copy and paste ready)" )
136+ rich .print (uns )
137+
138+
139+ def _print_output_plain (
140+ selected_items : dict [str , str ],
141+ libs_items : dict [str , str ],
142+ spec_items : dict [str , str ],
143+ unselected_items : dict [str , str ],
144+ ) -> None :
145+ """Print plain formatted output."""
146+ print ("Selected:" )
147+ for item in _print_each_plain (selected_items ):
148+ print (item )
149+
150+ if libs_items :
151+ print ("\n Library specific:" )
152+ for item in _print_each_plain (libs_items ):
153+ print (item )
154+
155+ if spec_items :
156+ print ("\n Specialized:" )
157+ for item in _print_each_plain (spec_items ):
158+ print (item )
159+
160+ if unselected_items :
161+ print ("\n Unselected (copy and paste ready):" )
162+ for item in _print_each_plain (unselected_items ):
163+ print (item )
164+
165+
166+ def _handle_all_selected (fmt : str , ruff_config : dict [str , object ]) -> None :
167+ """Handle the case when ALL rules are selected."""
168+ ignored = get_rule_selection (ruff_config , "ignore" )
169+ missed = [
170+ r
171+ for r in IGNORE_INFO
172+ if not any (
173+ x .startswith ((r .get ("rule" , "." ), r .get ("family" , "." )))
174+ for x in (ignored or [])
175+ )
176+ ]
177+
178+ msg = '[green]"ALL"[/green] selected.' if fmt == "rich" else '"ALL" selected.'
179+ if fmt == "rich" :
180+ import rich
181+
182+ rich .print (msg )
183+ else :
184+ print (msg )
185+
186+ ignores = {v .get ("rule" , v .get ("family" , "" )): v ["reason" ] for v in missed }
187+ if ignores :
188+ msg_header = "Some things that sometimes need ignoring:"
189+ if fmt == "rich" :
190+ import rich
191+
192+ rich .print (msg_header )
193+ for item in _print_each_rich (ignores ):
194+ rich .print (item )
195+ else :
196+ print (msg_header )
197+ for item in _print_each_plain (ignores ):
198+ print (item )
199+
200+
201+ def process_dir (path : Path , format : str = "auto" ) -> None :
202+ """Process a directory and display ruff rules configuration.
203+
204+ Args:
205+ path: Directory to process
206+ format: Output format - 'auto', 'rich', or 'plain'
207+ """
208+ fmt = _resolve_format (format )
209+
54210 try :
55211 with path .joinpath ("pyproject.toml" ).open ("rb" ) as f :
56212 pyproject = tomllib .load (f )
57213 except FileNotFoundError :
58214 pyproject = {}
59215
60216 ruff_config = ruff (pyproject = pyproject , root = path )
217+ if fmt == "rich" and not _has_rich ():
218+ _output_error (
219+ "plain" , "Error: --format rich requested, but rich is not installed"
220+ )
221+ raise SystemExit (3 )
222+
61223 if ruff_config is None :
62- print (
63- "[red]Could not find a ruff config [dim](.ruff.toml, ruff.toml, or pyproject.toml)" ,
64- file = sys .stderr ,
224+ msg = (
225+ "[red]Could not find a ruff config [dim](.ruff.toml, ruff.toml, or pyproject.toml)"
226+ if fmt == "rich"
227+ else "Error: Could not find a ruff config (.ruff.toml, ruff.toml, or pyproject.toml)"
65228 )
229+ _output_error (fmt , msg )
66230 raise SystemExit (1 )
231+
67232 selected = get_rule_selection (ruff_config )
68233 if not selected :
69- print (
70- "[red]No rules selected" ,
71- file = sys .stderr ,
72- )
234+ msg = "[red]No rules selected" if fmt == "rich" else "Error: No rules selected"
235+ _output_error (fmt , msg )
73236 raise SystemExit (2 )
74237
75238 if "ALL" in selected :
76- ignored = get_rule_selection (ruff_config , "ignore" )
77- missed = [
78- r
79- for r in IGNORE_INFO
80- if not any (
81- x .startswith ((r .get ("rule" , "." ), r .get ("family" , "." )))
82- for x in ignored
83- )
84- ]
85-
86- print ('[green]"ALL"[/green] selected.' )
87- ignores = {v .get ("rule" , v .get ("family" , "" )): v ["reason" ] for v in missed }
88- if ignores :
89- print ("Some things that sometimes need ignoring:" )
90- for item in print_each (ignores ):
91- print (item )
239+ _handle_all_selected (fmt , ruff_config )
92240 return
93241
94242 selected_items = {k : v for k , v in LINT_INFO .items () if k in selected }
@@ -99,23 +247,10 @@ def process_dir(path: Path) -> None:
99247 libs_items = {k : v for k , v in all_uns_items .items () if k in LIBS }
100248 spec_items = {k : v for k , v in all_uns_items .items () if k in SPECIALTY }
101249
102- panel_sel = Panel (
103- "\n " .join (print_each (selected_items )), title = "Selected" , border_style = "green"
104- )
105- panel_lib = Panel (
106- "\n " .join (print_each (libs_items )),
107- title = "Library specific" ,
108- border_style = "yellow" ,
109- )
110- panel_spec = Panel (
111- "\n " .join (print_each (spec_items )), title = "Specialized" , border_style = "yellow"
112- )
113- uns = "\n " .join (print_each (unselected_items ))
114-
115- print (Columns ([panel_sel , panel_lib , panel_spec ]))
116- if uns :
117- print ("[red]Unselected [dim](copy and paste ready)" )
118- print (uns )
250+ if fmt == "rich" :
251+ _print_output_rich (selected_items , libs_items , spec_items , unselected_items )
252+ else :
253+ _print_output_plain (selected_items , libs_items , spec_items , unselected_items )
119254
120255
121256def main () -> None :
@@ -127,9 +262,15 @@ def main() -> None:
127262 default = Path .cwd (),
128263 help = "Directory to process (default: current working directory)" ,
129264 )
265+ parser .add_argument (
266+ "--format" ,
267+ choices = ["auto" , "rich" , "plain" ],
268+ default = "auto" ,
269+ help = "Output format (default: auto)" ,
270+ )
130271 args = parser .parse_args ()
131272
132- process_dir (args .path )
273+ process_dir (args .path , format = args . format )
133274
134275
135276if __name__ == "__main__" :
0 commit comments