mcp-ticketer 0.1.12__py3-none-any.whl → 0.1.14__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.

Potentially problematic release.


This version of mcp-ticketer might be problematic. Click here for more details.

@@ -0,0 +1,402 @@
1
+ """CLI command for auto-discovering configuration from .env files."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+ from rich.panel import Panel
11
+ from rich import print as rprint
12
+
13
+ from ..core.env_discovery import EnvDiscovery, DiscoveredAdapter
14
+ from ..core.project_config import (
15
+ ConfigResolver,
16
+ TicketerConfig,
17
+ AdapterConfig,
18
+ ConfigValidator,
19
+ )
20
+
21
+ console = Console()
22
+ app = typer.Typer(help="Auto-discover configuration from .env files")
23
+
24
+
25
+ def _mask_sensitive(value: str, key: str) -> str:
26
+ """Mask sensitive values for display.
27
+
28
+ Args:
29
+ value: Value to potentially mask
30
+ key: Key name to determine if masking needed
31
+
32
+ Returns:
33
+ Masked or original value
34
+ """
35
+ sensitive_keys = ["token", "key", "password", "secret", "api_token"]
36
+
37
+ # Check if key contains any sensitive pattern
38
+ key_lower = key.lower()
39
+ is_sensitive = any(pattern in key_lower for pattern in sensitive_keys)
40
+
41
+ # Don't mask team_id, team_key, project_key, etc.
42
+ if "team" in key_lower or "project" in key_lower:
43
+ is_sensitive = False
44
+
45
+ if is_sensitive and value:
46
+ # Show first 4 and last 4 characters
47
+ if len(value) > 12:
48
+ return f"{value[:4]}...{value[-4:]}"
49
+ else:
50
+ return "***"
51
+
52
+ return value
53
+
54
+
55
+ def _display_discovered_adapter(adapter: DiscoveredAdapter, discovery: EnvDiscovery) -> None:
56
+ """Display information about a discovered adapter.
57
+
58
+ Args:
59
+ adapter: Discovered adapter to display
60
+ discovery: EnvDiscovery instance for validation
61
+ """
62
+ # Header
63
+ completeness = "✅ Complete" if adapter.is_complete() else "⚠️ Incomplete"
64
+ confidence_percent = int(adapter.confidence * 100)
65
+
66
+ console.print(
67
+ f"\n[bold cyan]{adapter.adapter_type.upper()}[/bold cyan] "
68
+ f"({completeness}, {confidence_percent}% confidence)"
69
+ )
70
+
71
+ # Configuration details
72
+ console.print(f" [dim]Found in: {adapter.found_in}[/dim]")
73
+
74
+ for key, value in adapter.config.items():
75
+ if key == "adapter":
76
+ continue
77
+
78
+ display_value = _mask_sensitive(str(value), key)
79
+ console.print(f" {key}: [green]{display_value}[/green]")
80
+
81
+ # Missing fields
82
+ if adapter.missing_fields:
83
+ console.print(f" [yellow]Missing:[/yellow] {', '.join(adapter.missing_fields)}")
84
+
85
+ # Validation warnings
86
+ warnings = discovery.validate_discovered_config(adapter)
87
+ if warnings:
88
+ for warning in warnings:
89
+ console.print(f" {warning}")
90
+
91
+
92
+ @app.command()
93
+ def show(
94
+ project_path: Optional[Path] = typer.Option(
95
+ None,
96
+ "--path",
97
+ "-p",
98
+ help="Project path to scan (defaults to current directory)"
99
+ ),
100
+ ) -> None:
101
+ """Show discovered configuration without saving."""
102
+ proj_path = project_path or Path.cwd()
103
+
104
+ console.print(f"\n[bold]🔍 Auto-discovering configuration in:[/bold] {proj_path}\n")
105
+
106
+ # Discover
107
+ discovery = EnvDiscovery(proj_path)
108
+ result = discovery.discover()
109
+
110
+ # Show env files found
111
+ if result.env_files_found:
112
+ console.print("[bold]Environment files found:[/bold]")
113
+ for env_file in result.env_files_found:
114
+ console.print(f" ✅ {env_file}")
115
+ else:
116
+ console.print("[yellow]No .env files found[/yellow]")
117
+ return
118
+
119
+ # Show discovered adapters
120
+ if result.adapters:
121
+ console.print("\n[bold]Detected adapter configurations:[/bold]")
122
+ for adapter in sorted(result.adapters, key=lambda a: a.confidence, reverse=True):
123
+ _display_discovered_adapter(adapter, discovery)
124
+
125
+ # Show recommended adapter
126
+ primary = result.get_primary_adapter()
127
+ if primary:
128
+ console.print(
129
+ f"\n[bold green]Recommended adapter:[/bold green] {primary.adapter_type} "
130
+ f"(most complete configuration)"
131
+ )
132
+ else:
133
+ console.print("\n[yellow]No adapter configurations detected[/yellow]")
134
+ console.print("[dim]Make sure your .env file contains adapter credentials[/dim]")
135
+
136
+ # Show warnings
137
+ if result.warnings:
138
+ console.print("\n[bold yellow]Warnings:[/bold yellow]")
139
+ for warning in result.warnings:
140
+ console.print(f" {warning}")
141
+
142
+
143
+ @app.command()
144
+ def save(
145
+ adapter: Optional[str] = typer.Option(
146
+ None,
147
+ "--adapter",
148
+ "-a",
149
+ help="Which adapter to save (defaults to recommended)"
150
+ ),
151
+ global_config: bool = typer.Option(
152
+ False,
153
+ "--global",
154
+ "-g",
155
+ help="Save to global config instead of project config"
156
+ ),
157
+ dry_run: bool = typer.Option(
158
+ False,
159
+ "--dry-run",
160
+ help="Show what would be saved without saving"
161
+ ),
162
+ project_path: Optional[Path] = typer.Option(
163
+ None,
164
+ "--path",
165
+ "-p",
166
+ help="Project path to scan (defaults to current directory)"
167
+ ),
168
+ ) -> None:
169
+ """Discover configuration and save to config file.
170
+
171
+ By default, saves to project-specific config (.mcp-ticketer/config.json).
172
+ Use --global to save to global config (~/.mcp-ticketer/config.json).
173
+ """
174
+ proj_path = project_path or Path.cwd()
175
+
176
+ console.print(f"\n[bold]🔍 Auto-discovering configuration in:[/bold] {proj_path}\n")
177
+
178
+ # Discover
179
+ discovery = EnvDiscovery(proj_path)
180
+ result = discovery.discover()
181
+
182
+ if not result.adapters:
183
+ console.print("[red]No adapter configurations detected[/red]")
184
+ console.print("[dim]Make sure your .env file contains adapter credentials[/dim]")
185
+ raise typer.Exit(1)
186
+
187
+ # Determine which adapter to save
188
+ if adapter:
189
+ discovered_adapter = result.get_adapter_by_type(adapter)
190
+ if not discovered_adapter:
191
+ console.print(f"[red]No configuration found for adapter: {adapter}[/red]")
192
+ console.print(f"[dim]Available: {', '.join(a.adapter_type for a in result.adapters)}[/dim]")
193
+ raise typer.Exit(1)
194
+ else:
195
+ # Use recommended adapter
196
+ discovered_adapter = result.get_primary_adapter()
197
+ if not discovered_adapter:
198
+ console.print("[red]Could not determine recommended adapter[/red]")
199
+ raise typer.Exit(1)
200
+
201
+ console.print(
202
+ f"[bold]Using recommended adapter:[/bold] {discovered_adapter.adapter_type}"
203
+ )
204
+
205
+ # Display what will be saved
206
+ _display_discovered_adapter(discovered_adapter, discovery)
207
+
208
+ # Validate configuration
209
+ is_valid, error_msg = ConfigValidator.validate(
210
+ discovered_adapter.adapter_type,
211
+ discovered_adapter.config
212
+ )
213
+
214
+ if not is_valid:
215
+ console.print(f"\n[red]Configuration validation failed:[/red] {error_msg}")
216
+ console.print("[dim]Fix the configuration in your .env file and try again[/dim]")
217
+ raise typer.Exit(1)
218
+
219
+ if dry_run:
220
+ console.print("\n[yellow]Dry run - no changes made[/yellow]")
221
+ return
222
+
223
+ # Load or create config
224
+ resolver = ConfigResolver(proj_path)
225
+
226
+ if global_config:
227
+ config = resolver.load_global_config()
228
+ else:
229
+ config = resolver.load_project_config() or TicketerConfig()
230
+
231
+ # Set default adapter
232
+ config.default_adapter = discovered_adapter.adapter_type
233
+
234
+ # Create adapter config
235
+ adapter_config = AdapterConfig.from_dict(discovered_adapter.config)
236
+
237
+ # Add to config
238
+ config.adapters[discovered_adapter.adapter_type] = adapter_config
239
+
240
+ # Save
241
+ try:
242
+ if global_config:
243
+ resolver.save_global_config(config)
244
+ config_location = resolver.GLOBAL_CONFIG_PATH
245
+ else:
246
+ resolver.save_project_config(config, proj_path)
247
+ config_location = proj_path / resolver.PROJECT_CONFIG_SUBPATH
248
+
249
+ console.print(f"\n[green]✅ Configuration saved to:[/green] {config_location}")
250
+ console.print(f"[green]✅ Default adapter set to:[/green] {discovered_adapter.adapter_type}")
251
+
252
+ except Exception as e:
253
+ console.print(f"\n[red]Failed to save configuration:[/red] {e}")
254
+ raise typer.Exit(1)
255
+
256
+
257
+ @app.command()
258
+ def interactive(
259
+ project_path: Optional[Path] = typer.Option(
260
+ None,
261
+ "--path",
262
+ "-p",
263
+ help="Project path to scan (defaults to current directory)"
264
+ ),
265
+ ) -> None:
266
+ """Interactive mode for discovering and saving configuration."""
267
+ proj_path = project_path or Path.cwd()
268
+
269
+ console.print(f"\n[bold]🔍 Auto-discovering configuration in:[/bold] {proj_path}\n")
270
+
271
+ # Discover
272
+ discovery = EnvDiscovery(proj_path)
273
+ result = discovery.discover()
274
+
275
+ # Show env files
276
+ if result.env_files_found:
277
+ console.print("[bold]Environment files found:[/bold]")
278
+ for env_file in result.env_files_found:
279
+ console.print(f" ✅ {env_file}")
280
+ else:
281
+ console.print("[red]No .env files found[/red]")
282
+ raise typer.Exit(1)
283
+
284
+ # Show discovered adapters
285
+ if not result.adapters:
286
+ console.print("\n[red]No adapter configurations detected[/red]")
287
+ console.print("[dim]Make sure your .env file contains adapter credentials[/dim]")
288
+ raise typer.Exit(1)
289
+
290
+ console.print("\n[bold]Detected adapter configurations:[/bold]")
291
+ for i, adapter in enumerate(result.adapters, 1):
292
+ completeness = "✅" if adapter.is_complete() else "⚠️ "
293
+ console.print(
294
+ f" {i}. {completeness} [cyan]{adapter.adapter_type}[/cyan] "
295
+ f"({int(adapter.confidence * 100)}% confidence)"
296
+ )
297
+
298
+ # Show warnings
299
+ if result.warnings:
300
+ console.print("\n[bold yellow]Warnings:[/bold yellow]")
301
+ for warning in result.warnings:
302
+ console.print(f" {warning}")
303
+
304
+ # Ask user which adapter to save
305
+ primary = result.get_primary_adapter()
306
+ console.print(
307
+ f"\n[bold]Recommended:[/bold] {primary.adapter_type if primary else 'None'}"
308
+ )
309
+
310
+ # Prompt for selection
311
+ console.print("\n[bold]Select an option:[/bold]")
312
+ console.print(" 1. Save recommended adapter to project config")
313
+ console.print(" 2. Save recommended adapter to global config")
314
+ console.print(" 3. Choose different adapter")
315
+ console.print(" 4. Save all adapters")
316
+ console.print(" 5. Cancel")
317
+
318
+ choice = typer.prompt("Enter choice", type=int, default=1)
319
+
320
+ if choice == 5:
321
+ console.print("[yellow]Cancelled[/yellow]")
322
+ return
323
+
324
+ # Determine adapters to save
325
+ if choice in [1, 2]:
326
+ if not primary:
327
+ console.print("[red]No recommended adapter found[/red]")
328
+ raise typer.Exit(1)
329
+ adapters_to_save = [primary]
330
+ default_adapter = primary.adapter_type
331
+ elif choice == 3:
332
+ # Let user choose
333
+ console.print("\n[bold]Available adapters:[/bold]")
334
+ for i, adapter in enumerate(result.adapters, 1):
335
+ console.print(f" {i}. {adapter.adapter_type}")
336
+
337
+ adapter_choice = typer.prompt("Select adapter", type=int, default=1)
338
+ if 1 <= adapter_choice <= len(result.adapters):
339
+ selected = result.adapters[adapter_choice - 1]
340
+ adapters_to_save = [selected]
341
+ default_adapter = selected.adapter_type
342
+ else:
343
+ console.print("[red]Invalid choice[/red]")
344
+ raise typer.Exit(1)
345
+ else: # choice == 4
346
+ adapters_to_save = result.adapters
347
+ default_adapter = primary.adapter_type if primary else result.adapters[0].adapter_type
348
+
349
+ # Determine save location
350
+ save_global = choice == 2
351
+
352
+ # Load or create config
353
+ resolver = ConfigResolver(proj_path)
354
+
355
+ if save_global:
356
+ config = resolver.load_global_config()
357
+ else:
358
+ config = resolver.load_project_config() or TicketerConfig()
359
+
360
+ # Set default adapter
361
+ config.default_adapter = default_adapter
362
+
363
+ # Add adapters
364
+ for discovered_adapter in adapters_to_save:
365
+ # Validate
366
+ is_valid, error_msg = ConfigValidator.validate(
367
+ discovered_adapter.adapter_type,
368
+ discovered_adapter.config
369
+ )
370
+
371
+ if not is_valid:
372
+ console.print(
373
+ f"\n[yellow]Warning:[/yellow] {discovered_adapter.adapter_type} "
374
+ f"validation failed: {error_msg}"
375
+ )
376
+ continue
377
+
378
+ # Create adapter config
379
+ adapter_config = AdapterConfig.from_dict(discovered_adapter.config)
380
+ config.adapters[discovered_adapter.adapter_type] = adapter_config
381
+
382
+ console.print(f" ✅ Added {discovered_adapter.adapter_type}")
383
+
384
+ # Save
385
+ try:
386
+ if save_global:
387
+ resolver.save_global_config(config)
388
+ config_location = resolver.GLOBAL_CONFIG_PATH
389
+ else:
390
+ resolver.save_project_config(config, proj_path)
391
+ config_location = proj_path / resolver.PROJECT_CONFIG_SUBPATH
392
+
393
+ console.print(f"\n[green]✅ Configuration saved to:[/green] {config_location}")
394
+ console.print(f"[green]✅ Default adapter:[/green] {config.default_adapter}")
395
+
396
+ except Exception as e:
397
+ console.print(f"\n[red]Failed to save configuration:[/red] {e}")
398
+ raise typer.Exit(1)
399
+
400
+
401
+ if __name__ == "__main__":
402
+ app()