thailint 0.5.0__py3-none-any.whl → 0.15.3__py3-none-any.whl

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.
Files changed (204) hide show
  1. src/__init__.py +1 -0
  2. src/analyzers/__init__.py +4 -3
  3. src/analyzers/ast_utils.py +54 -0
  4. src/analyzers/rust_base.py +155 -0
  5. src/analyzers/rust_context.py +141 -0
  6. src/analyzers/typescript_base.py +4 -0
  7. src/cli/__init__.py +30 -0
  8. src/cli/__main__.py +22 -0
  9. src/cli/config.py +480 -0
  10. src/cli/config_merge.py +241 -0
  11. src/cli/linters/__init__.py +67 -0
  12. src/cli/linters/code_patterns.py +270 -0
  13. src/cli/linters/code_smells.py +342 -0
  14. src/cli/linters/documentation.py +83 -0
  15. src/cli/linters/performance.py +287 -0
  16. src/cli/linters/shared.py +331 -0
  17. src/cli/linters/structure.py +327 -0
  18. src/cli/linters/structure_quality.py +328 -0
  19. src/cli/main.py +120 -0
  20. src/cli/utils.py +395 -0
  21. src/cli_main.py +37 -0
  22. src/config.py +38 -25
  23. src/core/base.py +7 -2
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +5 -2
  26. src/core/constants.py +54 -0
  27. src/core/linter_utils.py +95 -6
  28. src/core/python_lint_rule.py +101 -0
  29. src/core/registry.py +1 -1
  30. src/core/rule_discovery.py +147 -84
  31. src/core/types.py +13 -0
  32. src/core/violation_builder.py +78 -15
  33. src/core/violation_utils.py +69 -0
  34. src/formatters/__init__.py +22 -0
  35. src/formatters/sarif.py +202 -0
  36. src/linter_config/directive_markers.py +109 -0
  37. src/linter_config/ignore.py +254 -395
  38. src/linter_config/loader.py +45 -12
  39. src/linter_config/pattern_utils.py +65 -0
  40. src/linter_config/rule_matcher.py +89 -0
  41. src/linters/collection_pipeline/__init__.py +90 -0
  42. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  43. src/linters/collection_pipeline/ast_utils.py +40 -0
  44. src/linters/collection_pipeline/config.py +75 -0
  45. src/linters/collection_pipeline/continue_analyzer.py +94 -0
  46. src/linters/collection_pipeline/detector.py +360 -0
  47. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  48. src/linters/collection_pipeline/linter.py +420 -0
  49. src/linters/collection_pipeline/suggestion_builder.py +130 -0
  50. src/linters/cqs/__init__.py +54 -0
  51. src/linters/cqs/config.py +55 -0
  52. src/linters/cqs/function_analyzer.py +201 -0
  53. src/linters/cqs/input_detector.py +139 -0
  54. src/linters/cqs/linter.py +159 -0
  55. src/linters/cqs/output_detector.py +84 -0
  56. src/linters/cqs/python_analyzer.py +54 -0
  57. src/linters/cqs/types.py +82 -0
  58. src/linters/cqs/typescript_cqs_analyzer.py +61 -0
  59. src/linters/cqs/typescript_function_analyzer.py +192 -0
  60. src/linters/cqs/typescript_input_detector.py +203 -0
  61. src/linters/cqs/typescript_output_detector.py +117 -0
  62. src/linters/cqs/violation_builder.py +94 -0
  63. src/linters/dry/base_token_analyzer.py +16 -9
  64. src/linters/dry/block_filter.py +120 -20
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +104 -10
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +54 -11
  69. src/linters/dry/constant.py +92 -0
  70. src/linters/dry/constant_matcher.py +223 -0
  71. src/linters/dry/constant_violation_builder.py +98 -0
  72. src/linters/dry/duplicate_storage.py +5 -4
  73. src/linters/dry/file_analyzer.py +4 -2
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +183 -48
  76. src/linters/dry/python_analyzer.py +60 -439
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/token_hasher.py +116 -112
  80. src/linters/dry/typescript_analyzer.py +68 -382
  81. src/linters/dry/typescript_constant_extractor.py +138 -0
  82. src/linters/dry/typescript_statement_detector.py +255 -0
  83. src/linters/dry/typescript_value_extractor.py +70 -0
  84. src/linters/dry/violation_builder.py +4 -0
  85. src/linters/dry/violation_filter.py +5 -4
  86. src/linters/dry/violation_generator.py +71 -14
  87. src/linters/file_header/atemporal_detector.py +68 -50
  88. src/linters/file_header/base_parser.py +93 -0
  89. src/linters/file_header/bash_parser.py +66 -0
  90. src/linters/file_header/config.py +90 -16
  91. src/linters/file_header/css_parser.py +70 -0
  92. src/linters/file_header/field_validator.py +36 -33
  93. src/linters/file_header/linter.py +140 -144
  94. src/linters/file_header/markdown_parser.py +130 -0
  95. src/linters/file_header/python_parser.py +14 -58
  96. src/linters/file_header/typescript_parser.py +73 -0
  97. src/linters/file_header/violation_builder.py +13 -12
  98. src/linters/file_placement/config_loader.py +3 -1
  99. src/linters/file_placement/directory_matcher.py +4 -0
  100. src/linters/file_placement/linter.py +66 -34
  101. src/linters/file_placement/pattern_matcher.py +41 -6
  102. src/linters/file_placement/pattern_validator.py +31 -12
  103. src/linters/file_placement/rule_checker.py +12 -7
  104. src/linters/lazy_ignores/__init__.py +43 -0
  105. src/linters/lazy_ignores/config.py +74 -0
  106. src/linters/lazy_ignores/directive_utils.py +164 -0
  107. src/linters/lazy_ignores/header_parser.py +177 -0
  108. src/linters/lazy_ignores/linter.py +158 -0
  109. src/linters/lazy_ignores/matcher.py +168 -0
  110. src/linters/lazy_ignores/python_analyzer.py +209 -0
  111. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  112. src/linters/lazy_ignores/skip_detector.py +298 -0
  113. src/linters/lazy_ignores/types.py +71 -0
  114. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  115. src/linters/lazy_ignores/violation_builder.py +135 -0
  116. src/linters/lbyl/__init__.py +31 -0
  117. src/linters/lbyl/config.py +63 -0
  118. src/linters/lbyl/linter.py +67 -0
  119. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  120. src/linters/lbyl/pattern_detectors/base.py +63 -0
  121. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  122. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  123. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  124. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  125. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  126. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  127. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  128. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  129. src/linters/lbyl/python_analyzer.py +215 -0
  130. src/linters/lbyl/violation_builder.py +354 -0
  131. src/linters/magic_numbers/context_analyzer.py +227 -225
  132. src/linters/magic_numbers/linter.py +28 -82
  133. src/linters/magic_numbers/python_analyzer.py +4 -16
  134. src/linters/magic_numbers/typescript_analyzer.py +9 -12
  135. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  136. src/linters/method_property/__init__.py +49 -0
  137. src/linters/method_property/config.py +138 -0
  138. src/linters/method_property/linter.py +414 -0
  139. src/linters/method_property/python_analyzer.py +473 -0
  140. src/linters/method_property/violation_builder.py +119 -0
  141. src/linters/nesting/linter.py +24 -16
  142. src/linters/nesting/python_analyzer.py +4 -0
  143. src/linters/nesting/typescript_analyzer.py +6 -12
  144. src/linters/nesting/violation_builder.py +1 -0
  145. src/linters/performance/__init__.py +91 -0
  146. src/linters/performance/config.py +43 -0
  147. src/linters/performance/constants.py +49 -0
  148. src/linters/performance/linter.py +149 -0
  149. src/linters/performance/python_analyzer.py +365 -0
  150. src/linters/performance/regex_analyzer.py +312 -0
  151. src/linters/performance/regex_linter.py +139 -0
  152. src/linters/performance/typescript_analyzer.py +236 -0
  153. src/linters/performance/violation_builder.py +160 -0
  154. src/linters/print_statements/config.py +7 -12
  155. src/linters/print_statements/linter.py +26 -43
  156. src/linters/print_statements/python_analyzer.py +91 -93
  157. src/linters/print_statements/typescript_analyzer.py +15 -25
  158. src/linters/print_statements/violation_builder.py +12 -14
  159. src/linters/srp/class_analyzer.py +11 -7
  160. src/linters/srp/heuristics.py +56 -22
  161. src/linters/srp/linter.py +15 -16
  162. src/linters/srp/python_analyzer.py +55 -20
  163. src/linters/srp/typescript_metrics_calculator.py +110 -50
  164. src/linters/stateless_class/__init__.py +25 -0
  165. src/linters/stateless_class/config.py +58 -0
  166. src/linters/stateless_class/linter.py +349 -0
  167. src/linters/stateless_class/python_analyzer.py +290 -0
  168. src/linters/stringly_typed/__init__.py +36 -0
  169. src/linters/stringly_typed/config.py +189 -0
  170. src/linters/stringly_typed/context_filter.py +451 -0
  171. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  172. src/linters/stringly_typed/ignore_checker.py +100 -0
  173. src/linters/stringly_typed/ignore_utils.py +51 -0
  174. src/linters/stringly_typed/linter.py +376 -0
  175. src/linters/stringly_typed/python/__init__.py +33 -0
  176. src/linters/stringly_typed/python/analyzer.py +348 -0
  177. src/linters/stringly_typed/python/call_tracker.py +175 -0
  178. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  179. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  180. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  181. src/linters/stringly_typed/python/constants.py +21 -0
  182. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  183. src/linters/stringly_typed/python/validation_detector.py +189 -0
  184. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  185. src/linters/stringly_typed/storage.py +620 -0
  186. src/linters/stringly_typed/storage_initializer.py +45 -0
  187. src/linters/stringly_typed/typescript/__init__.py +28 -0
  188. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  189. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  190. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  191. src/linters/stringly_typed/violation_generator.py +419 -0
  192. src/orchestrator/core.py +252 -14
  193. src/orchestrator/language_detector.py +5 -3
  194. src/templates/thailint_config_template.yaml +196 -0
  195. src/utils/project_root.py +3 -0
  196. thailint-0.15.3.dist-info/METADATA +187 -0
  197. thailint-0.15.3.dist-info/RECORD +226 -0
  198. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  199. src/cli.py +0 -1665
  200. thailint-0.5.0.dist-info/METADATA +0 -1286
  201. thailint-0.5.0.dist-info/RECORD +0 -96
  202. thailint-0.5.0.dist-info/entry_points.txt +0 -4
  203. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
  204. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
src/cli/config.py ADDED
@@ -0,0 +1,480 @@
1
+ """
2
+ Purpose: Configuration management commands for thai-lint CLI
3
+
4
+ Scope: Commands for viewing, modifying, and initializing thai-lint configuration
5
+
6
+ Overview: Provides CLI commands for managing thai-lint configuration including show (display
7
+ configuration in text/json/yaml), get (retrieve specific value), set (modify value with
8
+ validation), reset (restore defaults), and init-config (generate new .thailint.yaml with
9
+ presets). Supports both interactive and non-interactive modes for human and AI agent
10
+ workflows. Integrates with the config module for loading, saving, and validation.
11
+
12
+ Dependencies: click for CLI framework, src.config for config operations, pathlib for file paths,
13
+ json and yaml for output formatting
14
+
15
+ Exports: config_group (Click command group), init_config command, show_config, get_config,
16
+ set_config, reset_config commands
17
+
18
+ Interfaces: Click commands registered to main CLI group, config presets (strict/standard/lenient)
19
+
20
+ Implementation: Uses Click decorators for command definition, supports multiple output formats,
21
+ validates configuration changes before saving, uses template file for init-config generation
22
+ """
23
+
24
+ import logging
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ import click
29
+ import yaml
30
+
31
+ from src.config import ConfigError, save_config, validate_config
32
+
33
+ from .config_merge import perform_merge
34
+ from .main import cli
35
+
36
+ # Configure module logger
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ # =============================================================================
41
+ # Config Command Group
42
+ # =============================================================================
43
+
44
+
45
+ @cli.group()
46
+ def config() -> None:
47
+ """Configuration management commands."""
48
+ pass
49
+
50
+
51
+ # =============================================================================
52
+ # Config Show Command
53
+ # =============================================================================
54
+
55
+
56
+ @config.command("show")
57
+ @click.option(
58
+ "--format",
59
+ "-f",
60
+ type=click.Choice(["text", "json", "yaml"]),
61
+ default="text",
62
+ help="Output format",
63
+ )
64
+ @click.pass_context
65
+ def config_show(ctx: click.Context, format: str) -> None:
66
+ """Display current configuration.
67
+
68
+ Shows all configuration values in the specified format.
69
+
70
+ Examples:
71
+
72
+ \b
73
+ # Show as text
74
+ thai-lint config show
75
+
76
+ \b
77
+ # Show as JSON
78
+ thai-lint config show --format json
79
+
80
+ \b
81
+ # Show as YAML
82
+ thai-lint config show --format yaml
83
+ """
84
+ cfg = ctx.obj["config"]
85
+
86
+ formatters = {
87
+ "json": _format_config_json,
88
+ "yaml": _format_config_yaml,
89
+ "text": _format_config_text,
90
+ }
91
+ formatters[format](cfg)
92
+
93
+
94
+ def _format_config_json(cfg: dict) -> None:
95
+ """Format configuration as JSON."""
96
+ import json
97
+
98
+ click.echo(json.dumps(cfg, indent=2))
99
+
100
+
101
+ def _format_config_yaml(cfg: dict) -> None:
102
+ """Format configuration as YAML."""
103
+ click.echo(yaml.dump(cfg, default_flow_style=False, sort_keys=False))
104
+
105
+
106
+ def _format_config_text(cfg: dict) -> None:
107
+ """Format configuration as text."""
108
+ click.echo("Current Configuration:")
109
+ click.echo("-" * 40)
110
+ for key, value in cfg.items():
111
+ click.echo(f"{key:20} : {value}")
112
+
113
+
114
+ # =============================================================================
115
+ # Config Get Command
116
+ # =============================================================================
117
+
118
+
119
+ @config.command("get")
120
+ @click.argument("key")
121
+ @click.pass_context
122
+ def config_get(ctx: click.Context, key: str) -> None:
123
+ """Get specific configuration value.
124
+
125
+ KEY: Configuration key to retrieve
126
+
127
+ Examples:
128
+
129
+ \b
130
+ # Get log level
131
+ thai-lint config get log_level
132
+
133
+ \b
134
+ # Get greeting template
135
+ thai-lint config get greeting
136
+ """
137
+ cfg = ctx.obj["config"]
138
+
139
+ if key not in cfg:
140
+ click.echo(f"Configuration key not found: {key}", err=True)
141
+ sys.exit(1)
142
+
143
+ click.echo(cfg[key])
144
+
145
+
146
+ # =============================================================================
147
+ # Config Set Command
148
+ # =============================================================================
149
+
150
+
151
+ def _convert_value_type(value: str) -> bool | int | float | str:
152
+ """Convert string value to appropriate type."""
153
+ from contextlib import suppress
154
+
155
+ if value.lower() in ["true", "false"]:
156
+ return value.lower() == "true"
157
+ # Use EAFP pattern for numeric conversion
158
+ for converter in (int, float):
159
+ with suppress(ValueError):
160
+ return converter(value)
161
+ return value
162
+
163
+
164
+ def _validate_and_report_errors(cfg: dict) -> None:
165
+ """Validate configuration and report errors."""
166
+ is_valid, errors = validate_config(cfg)
167
+ if not is_valid:
168
+ click.echo("Invalid configuration:", err=True)
169
+ for error in errors:
170
+ click.echo(f" - {error}", err=True)
171
+ sys.exit(1)
172
+
173
+
174
+ def _save_and_report_success(
175
+ cfg: dict, key: str, value: bool | int | float | str, config_path: Path | None, verbose: bool
176
+ ) -> None:
177
+ """Save configuration and report success."""
178
+ save_config(cfg, config_path)
179
+ click.echo(f"Set {key} = {value}")
180
+ if verbose:
181
+ logger.info(f"Configuration updated: {key}={value}")
182
+
183
+
184
+ @config.command("set")
185
+ @click.argument("key")
186
+ @click.argument("value")
187
+ @click.pass_context
188
+ def config_set(ctx: click.Context, key: str, value: str) -> None:
189
+ """Set configuration value.
190
+
191
+ KEY: Configuration key to set
192
+
193
+ VALUE: New value for the key
194
+
195
+ Examples:
196
+
197
+ \b
198
+ # Set log level
199
+ thai-lint config set log_level DEBUG
200
+
201
+ \b
202
+ # Set greeting template
203
+ thai-lint config set greeting "Hi"
204
+
205
+ \b
206
+ # Set numeric value
207
+ thai-lint config set max_retries 5
208
+ """
209
+ cfg = ctx.obj["config"]
210
+ converted_value = _convert_value_type(value)
211
+ cfg[key] = converted_value
212
+
213
+ try:
214
+ _validate_and_report_errors(cfg)
215
+ except Exception as e:
216
+ click.echo(f"Validation error: {e}", err=True)
217
+ sys.exit(1)
218
+
219
+ try:
220
+ config_path = ctx.obj.get("config_path")
221
+ verbose = ctx.obj.get("verbose", False)
222
+ _save_and_report_success(cfg, key, converted_value, config_path, verbose)
223
+ except ConfigError as e:
224
+ click.echo(f"Error saving configuration: {e}", err=True)
225
+ sys.exit(1)
226
+
227
+
228
+ # =============================================================================
229
+ # Config Reset Command
230
+ # =============================================================================
231
+
232
+
233
+ @config.command("reset")
234
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
235
+ @click.pass_context
236
+ def config_reset(ctx: click.Context, yes: bool) -> None:
237
+ """Reset configuration to defaults.
238
+
239
+ Examples:
240
+
241
+ \b
242
+ # Reset with confirmation
243
+ thai-lint config reset
244
+
245
+ \b
246
+ # Reset without confirmation
247
+ thai-lint config reset --yes
248
+ """
249
+ if not yes:
250
+ click.confirm("Reset configuration to defaults?", abort=True)
251
+
252
+ from src.config import DEFAULT_CONFIG
253
+
254
+ try:
255
+ config_path = ctx.obj.get("config_path")
256
+ save_config(DEFAULT_CONFIG.copy(), config_path)
257
+ click.echo("Configuration reset to defaults")
258
+
259
+ if ctx.obj.get("verbose"):
260
+ logger.info("Configuration reset to defaults")
261
+ except ConfigError as e:
262
+ click.echo(f"Error resetting configuration: {e}", err=True)
263
+ sys.exit(1)
264
+
265
+
266
+ # =============================================================================
267
+ # Init Config Command
268
+ # =============================================================================
269
+
270
+
271
+ @cli.command("init-config")
272
+ @click.option(
273
+ "--preset",
274
+ "-p",
275
+ type=click.Choice(["strict", "standard", "lenient"]),
276
+ default="standard",
277
+ help="Configuration preset",
278
+ )
279
+ @click.option("--non-interactive", is_flag=True, help="Skip interactive prompts (for AI agents)")
280
+ @click.option("--force", is_flag=True, help="Overwrite existing .thailint.yaml file")
281
+ @click.option(
282
+ "--output", "-o", type=click.Path(), default=".thailint.yaml", help="Output file path"
283
+ )
284
+ def init_config(preset: str, non_interactive: bool, force: bool, output: str) -> None:
285
+ """Generate a .thailint.yaml configuration file with preset values.
286
+
287
+ Creates a richly-commented configuration file with sensible defaults
288
+ and optional customizations for different strictness levels.
289
+
290
+ If a config file already exists, missing linter sections will be added
291
+ without modifying existing settings. Use --force to completely overwrite.
292
+
293
+ For AI agents, use --non-interactive mode:
294
+ thailint init-config --non-interactive --preset lenient
295
+
296
+ Presets:
297
+ strict: Minimal allowed numbers (only -1, 0, 1)
298
+ standard: Balanced defaults (includes 2, 3, 4, 5, 10, 100, 1000)
299
+ lenient: Includes time conversions (adds 60, 3600)
300
+
301
+ Examples:
302
+
303
+ \\b
304
+ # Interactive mode (default, for humans)
305
+ thailint init-config
306
+
307
+ \\b
308
+ # Non-interactive mode (for AI agents)
309
+ thailint init-config --non-interactive
310
+
311
+ \\b
312
+ # Generate with lenient preset
313
+ thailint init-config --preset lenient
314
+
315
+ \\b
316
+ # Overwrite existing config (replaces entire file)
317
+ thailint init-config --force
318
+
319
+ \\b
320
+ # Custom output path
321
+ thailint init-config --output my-config.yaml
322
+ """
323
+ output_path = Path(output)
324
+
325
+ # Interactive mode: Ask user for preferences
326
+ if not non_interactive:
327
+ preset = _run_interactive_preset_selection(preset)
328
+
329
+ # If file exists and not forcing overwrite, merge missing sections
330
+ if output_path.exists() and not force:
331
+ perform_merge(output_path, preset, output, _generate_config_content)
332
+ return
333
+
334
+ # Generate full config based on preset
335
+ config_content = _generate_config_content(preset)
336
+
337
+ # Write config file
338
+ _write_config_file(output_path, config_content, preset, output)
339
+
340
+
341
+ def _run_interactive_preset_selection(default_preset: str) -> str:
342
+ """Run interactive preset selection.
343
+
344
+ Args:
345
+ default_preset: Default preset to use if user accepts default
346
+
347
+ Returns:
348
+ Selected preset name
349
+ """
350
+ click.echo("thai-lint Configuration Generator")
351
+ click.echo("=" * 50)
352
+ click.echo("")
353
+ click.echo("This will create a .thailint.yaml configuration file.")
354
+ click.echo("For non-interactive mode (AI agents), use:")
355
+ click.echo(" thailint init-config --non-interactive")
356
+ click.echo("")
357
+
358
+ # Show preset options
359
+ click.echo("Available presets:")
360
+ click.echo(" strict: Only -1, 0, 1 allowed (strictest)")
361
+ click.echo(" standard: -1, 0, 1, 2, 3, 4, 5, 10, 100, 1000 (balanced)")
362
+ click.echo(" lenient: Includes time conversions 60, 3600 (most permissive)")
363
+ click.echo("")
364
+
365
+ preset_choices = click.Choice(["strict", "standard", "lenient"])
366
+ result: str = click.prompt("Choose preset", type=preset_choices, default=default_preset)
367
+ return result
368
+
369
+
370
+ def _generate_config_content(preset: str) -> str:
371
+ """Generate config file content based on preset.
372
+
373
+ Args:
374
+ preset: Preset name (strict, standard, or lenient)
375
+
376
+ Returns:
377
+ Generated configuration file content
378
+ """
379
+ # Preset configurations
380
+ presets = {
381
+ "strict": {
382
+ "allowed_numbers": "[-1, 0, 1]",
383
+ "max_small_integer": "3",
384
+ "description": "Strict (only universal values)",
385
+ },
386
+ "standard": {
387
+ "allowed_numbers": "[-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000]",
388
+ "max_small_integer": "10",
389
+ "description": "Standard (balanced defaults)",
390
+ },
391
+ "lenient": {
392
+ "allowed_numbers": "[-1, 0, 1, 2, 3, 4, 5, 10, 60, 100, 1000, 3600]",
393
+ "max_small_integer": "10",
394
+ "description": "Lenient (includes time conversions)",
395
+ },
396
+ }
397
+
398
+ config = presets[preset]
399
+
400
+ # Read template - use parent of parent since we're in src/cli/
401
+ template_path = Path(__file__).parent.parent / "templates" / "thailint_config_template.yaml"
402
+ template = template_path.read_text(encoding="utf-8")
403
+
404
+ # Replace placeholders
405
+ content = template.replace("{{PRESET}}", config["description"])
406
+ content = content.replace("{{ALLOWED_NUMBERS}}", config["allowed_numbers"])
407
+ content = content.replace("{{MAX_SMALL_INTEGER}}", config["max_small_integer"])
408
+
409
+ return content
410
+
411
+
412
+ def _write_config_file(output_path: Path, content: str, preset: str, output: str) -> None:
413
+ """Write configuration file and show success message.
414
+
415
+ Args:
416
+ output_path: Path to write file to
417
+ content: File content to write
418
+ preset: Selected preset name
419
+ output: Output filename for display
420
+ """
421
+ try:
422
+ output_path.write_text(content, encoding="utf-8")
423
+ click.echo("")
424
+ click.echo(f"Created {output}")
425
+ click.echo(f"Preset: {preset}")
426
+ click.echo("")
427
+ click.echo("Next steps:")
428
+ click.echo(f" 1. Review and customize {output}")
429
+ click.echo(" 2. Run: thailint magic-numbers .")
430
+ click.echo(" 3. See docs: https://github.com/your-org/thai-lint")
431
+ except OSError as e:
432
+ click.echo(f"Error writing config file: {e}", err=True)
433
+ sys.exit(1)
434
+
435
+
436
+ # =============================================================================
437
+ # Hello Command (Example Command)
438
+ # =============================================================================
439
+
440
+
441
+ @cli.command()
442
+ @click.option("--name", "-n", default="World", help="Name to greet")
443
+ @click.option("--uppercase", "-u", is_flag=True, help="Convert greeting to uppercase")
444
+ @click.pass_context
445
+ def hello(ctx: click.Context, name: str, uppercase: bool) -> None:
446
+ """Print a greeting message.
447
+
448
+ This is a simple example command demonstrating CLI basics.
449
+
450
+ Examples:
451
+
452
+ \b
453
+ # Basic greeting
454
+ thai-lint hello
455
+
456
+ \b
457
+ # Custom name
458
+ thai-lint hello --name Alice
459
+
460
+ \b
461
+ # Uppercase output
462
+ thai-lint hello --name Bob --uppercase
463
+ """
464
+ config = ctx.obj["config"]
465
+ verbose = ctx.obj.get("verbose", False)
466
+
467
+ # Get greeting from config or use default
468
+ greeting_template = config.get("greeting", "Hello")
469
+
470
+ # Build greeting message
471
+ message = f"{greeting_template}, {name}!"
472
+
473
+ if uppercase:
474
+ message = message.upper()
475
+
476
+ # Output greeting
477
+ click.echo(message)
478
+
479
+ if verbose:
480
+ logger.info(f"Greeted {name} with template '{greeting_template}'")