mcp-ticketer 0.1.11__py3-none-any.whl → 0.1.13__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.
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/adapters/__init__.py +8 -1
- mcp_ticketer/adapters/aitrackdown.py +9 -5
- mcp_ticketer/adapters/hybrid.py +505 -0
- mcp_ticketer/adapters/linear.py +427 -3
- mcp_ticketer/cli/configure.py +532 -0
- mcp_ticketer/cli/discover.py +402 -0
- mcp_ticketer/cli/main.py +85 -0
- mcp_ticketer/cli/migrate_config.py +204 -0
- mcp_ticketer/core/__init__.py +2 -1
- mcp_ticketer/core/adapter.py +155 -2
- mcp_ticketer/core/env_discovery.py +555 -0
- mcp_ticketer/core/models.py +58 -6
- mcp_ticketer/core/project_config.py +606 -0
- mcp_ticketer/queue/queue.py +4 -1
- {mcp_ticketer-0.1.11.dist-info → mcp_ticketer-0.1.13.dist-info}/METADATA +1 -1
- {mcp_ticketer-0.1.11.dist-info → mcp_ticketer-0.1.13.dist-info}/RECORD +21 -15
- {mcp_ticketer-0.1.11.dist-info → mcp_ticketer-0.1.13.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.11.dist-info → mcp_ticketer-0.1.13.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.11.dist-info → mcp_ticketer-0.1.13.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.11.dist-info → mcp_ticketer-0.1.13.dist-info}/top_level.txt +0 -0
|
@@ -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()
|
mcp_ticketer/cli/main.py
CHANGED
|
@@ -19,6 +19,9 @@ from ..adapters import AITrackdownAdapter
|
|
|
19
19
|
from ..queue import Queue, QueueStatus, WorkerManager
|
|
20
20
|
from .queue_commands import app as queue_app
|
|
21
21
|
from ..__version__ import __version__
|
|
22
|
+
from .configure import configure_wizard, show_current_config, set_adapter_config
|
|
23
|
+
from .migrate_config import migrate_config_command
|
|
24
|
+
from .discover import app as discover_app
|
|
22
25
|
|
|
23
26
|
# Load environment variables
|
|
24
27
|
load_dotenv()
|
|
@@ -424,6 +427,85 @@ def set_config(
|
|
|
424
427
|
console.print(f"[dim]Configuration saved to {CONFIG_FILE}[/dim]")
|
|
425
428
|
|
|
426
429
|
|
|
430
|
+
@app.command("configure")
|
|
431
|
+
def configure_command(
|
|
432
|
+
show: bool = typer.Option(
|
|
433
|
+
False,
|
|
434
|
+
"--show",
|
|
435
|
+
help="Show current configuration"
|
|
436
|
+
),
|
|
437
|
+
adapter: Optional[str] = typer.Option(
|
|
438
|
+
None,
|
|
439
|
+
"--adapter",
|
|
440
|
+
help="Set default adapter type"
|
|
441
|
+
),
|
|
442
|
+
api_key: Optional[str] = typer.Option(
|
|
443
|
+
None,
|
|
444
|
+
"--api-key",
|
|
445
|
+
help="Set API key/token"
|
|
446
|
+
),
|
|
447
|
+
project_id: Optional[str] = typer.Option(
|
|
448
|
+
None,
|
|
449
|
+
"--project-id",
|
|
450
|
+
help="Set project ID"
|
|
451
|
+
),
|
|
452
|
+
team_id: Optional[str] = typer.Option(
|
|
453
|
+
None,
|
|
454
|
+
"--team-id",
|
|
455
|
+
help="Set team ID (Linear)"
|
|
456
|
+
),
|
|
457
|
+
global_scope: bool = typer.Option(
|
|
458
|
+
False,
|
|
459
|
+
"--global",
|
|
460
|
+
"-g",
|
|
461
|
+
help="Save to global config instead of project-specific"
|
|
462
|
+
),
|
|
463
|
+
) -> None:
|
|
464
|
+
"""Configure MCP Ticketer integration.
|
|
465
|
+
|
|
466
|
+
Run without arguments to launch interactive wizard.
|
|
467
|
+
Use --show to display current configuration.
|
|
468
|
+
Use options to set specific values directly.
|
|
469
|
+
"""
|
|
470
|
+
# Show configuration
|
|
471
|
+
if show:
|
|
472
|
+
show_current_config()
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
# Direct configuration
|
|
476
|
+
if any([adapter, api_key, project_id, team_id]):
|
|
477
|
+
set_adapter_config(
|
|
478
|
+
adapter=adapter,
|
|
479
|
+
api_key=api_key,
|
|
480
|
+
project_id=project_id,
|
|
481
|
+
team_id=team_id,
|
|
482
|
+
global_scope=global_scope
|
|
483
|
+
)
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
# Run interactive wizard
|
|
487
|
+
configure_wizard()
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
@app.command("migrate-config")
|
|
491
|
+
def migrate_config(
|
|
492
|
+
dry_run: bool = typer.Option(
|
|
493
|
+
False,
|
|
494
|
+
"--dry-run",
|
|
495
|
+
help="Show what would be done without making changes"
|
|
496
|
+
),
|
|
497
|
+
) -> None:
|
|
498
|
+
"""Migrate configuration from old format to new format.
|
|
499
|
+
|
|
500
|
+
This command will:
|
|
501
|
+
1. Detect old configuration format
|
|
502
|
+
2. Convert to new schema
|
|
503
|
+
3. Backup old config
|
|
504
|
+
4. Apply new config
|
|
505
|
+
"""
|
|
506
|
+
migrate_config_command(dry_run=dry_run)
|
|
507
|
+
|
|
508
|
+
|
|
427
509
|
@app.command("status")
|
|
428
510
|
def status_command():
|
|
429
511
|
"""Show queue and worker status."""
|
|
@@ -784,6 +866,9 @@ def search(
|
|
|
784
866
|
# Add queue command to main app
|
|
785
867
|
app.add_typer(queue_app, name="queue")
|
|
786
868
|
|
|
869
|
+
# Add discover command to main app
|
|
870
|
+
app.add_typer(discover_app, name="discover")
|
|
871
|
+
|
|
787
872
|
|
|
788
873
|
@app.command()
|
|
789
874
|
def check(
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Configuration migration utilities."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Any
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.prompt import Confirm
|
|
11
|
+
|
|
12
|
+
from ..core.project_config import (
|
|
13
|
+
ConfigResolver,
|
|
14
|
+
TicketerConfig,
|
|
15
|
+
AdapterConfig,
|
|
16
|
+
AdapterType
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def migrate_config_command(dry_run: bool = False) -> None:
|
|
23
|
+
"""Migrate from old config format to new format.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
dry_run: If True, show what would be done without making changes
|
|
27
|
+
"""
|
|
28
|
+
resolver = ConfigResolver()
|
|
29
|
+
|
|
30
|
+
# Check if old config exists
|
|
31
|
+
if not resolver.GLOBAL_CONFIG_PATH.exists():
|
|
32
|
+
console.print("[yellow]No configuration found to migrate[/yellow]")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
# Load old config
|
|
36
|
+
try:
|
|
37
|
+
with open(resolver.GLOBAL_CONFIG_PATH, 'r') as f:
|
|
38
|
+
old_config = json.load(f)
|
|
39
|
+
except Exception as e:
|
|
40
|
+
console.print(f"[red]Failed to load config: {e}[/red]")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
# Check if already in new format
|
|
44
|
+
if "adapters" in old_config and isinstance(old_config.get("adapters"), dict):
|
|
45
|
+
# Check if it looks like new format
|
|
46
|
+
if any("adapter" in v for v in old_config["adapters"].values()):
|
|
47
|
+
console.print("[green]Configuration already in new format[/green]")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
console.print("[bold]Configuration Migration[/bold]\n")
|
|
51
|
+
console.print("Old format detected. This will migrate to the new schema.\n")
|
|
52
|
+
|
|
53
|
+
if dry_run:
|
|
54
|
+
console.print("[yellow]DRY RUN - No changes will be made[/yellow]\n")
|
|
55
|
+
|
|
56
|
+
# Show current config
|
|
57
|
+
console.print("[bold]Current Configuration:[/bold]")
|
|
58
|
+
console.print(json.dumps(old_config, indent=2))
|
|
59
|
+
console.print()
|
|
60
|
+
|
|
61
|
+
# Migrate
|
|
62
|
+
new_config = _migrate_old_to_new(old_config)
|
|
63
|
+
|
|
64
|
+
# Show new config
|
|
65
|
+
console.print("[bold]Migrated Configuration:[/bold]")
|
|
66
|
+
console.print(json.dumps(new_config.to_dict(), indent=2))
|
|
67
|
+
console.print()
|
|
68
|
+
|
|
69
|
+
if dry_run:
|
|
70
|
+
console.print("[yellow]This was a dry run. No changes were made.[/yellow]")
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
# Confirm migration
|
|
74
|
+
if not Confirm.ask("Apply migration?", default=True):
|
|
75
|
+
console.print("[yellow]Migration cancelled[/yellow]")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
# Backup old config
|
|
79
|
+
backup_path = resolver.GLOBAL_CONFIG_PATH.with_suffix('.json.bak')
|
|
80
|
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
81
|
+
backup_path = resolver.GLOBAL_CONFIG_PATH.parent / f"config.{timestamp}.bak"
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
shutil.copy(resolver.GLOBAL_CONFIG_PATH, backup_path)
|
|
85
|
+
console.print(f"[green]✓[/green] Backed up old config to: {backup_path}")
|
|
86
|
+
except Exception as e:
|
|
87
|
+
console.print(f"[red]Failed to backup config: {e}[/red]")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# Save new config
|
|
91
|
+
try:
|
|
92
|
+
resolver.save_global_config(new_config)
|
|
93
|
+
console.print(f"[green]✓[/green] Migration complete!")
|
|
94
|
+
console.print(f"[dim]New config saved to: {resolver.GLOBAL_CONFIG_PATH}[/dim]")
|
|
95
|
+
except Exception as e:
|
|
96
|
+
console.print(f"[red]Failed to save new config: {e}[/red]")
|
|
97
|
+
console.print(f"[yellow]Old config backed up at: {backup_path}[/yellow]")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _migrate_old_to_new(old_config: Dict[str, Any]) -> TicketerConfig:
|
|
101
|
+
"""Migrate old configuration format to new format.
|
|
102
|
+
|
|
103
|
+
Old format examples:
|
|
104
|
+
{
|
|
105
|
+
"adapter": "linear",
|
|
106
|
+
"config": {"api_key": "...", "team_id": "..."}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
or
|
|
110
|
+
|
|
111
|
+
{
|
|
112
|
+
"default_adapter": "linear",
|
|
113
|
+
"adapters": {
|
|
114
|
+
"linear": {"api_key": "...", "team_id": "..."}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
New format:
|
|
119
|
+
{
|
|
120
|
+
"default_adapter": "linear",
|
|
121
|
+
"adapters": {
|
|
122
|
+
"linear": {
|
|
123
|
+
"adapter": "linear",
|
|
124
|
+
"api_key": "...",
|
|
125
|
+
"team_id": "..."
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
old_config: Old configuration dictionary
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
New TicketerConfig object
|
|
135
|
+
"""
|
|
136
|
+
adapters = {}
|
|
137
|
+
default_adapter = "aitrackdown"
|
|
138
|
+
|
|
139
|
+
# Case 1: Single adapter with "adapter" and "config" fields (legacy format)
|
|
140
|
+
if "adapter" in old_config and "config" in old_config:
|
|
141
|
+
adapter_type = old_config["adapter"]
|
|
142
|
+
adapter_config = old_config["config"]
|
|
143
|
+
|
|
144
|
+
# Merge type into config
|
|
145
|
+
adapter_config["adapter"] = adapter_type
|
|
146
|
+
|
|
147
|
+
# Create AdapterConfig
|
|
148
|
+
adapters[adapter_type] = AdapterConfig.from_dict(adapter_config)
|
|
149
|
+
default_adapter = adapter_type
|
|
150
|
+
|
|
151
|
+
# Case 2: New-ish format with "adapters" dict but missing "adapter" field
|
|
152
|
+
elif "adapters" in old_config:
|
|
153
|
+
default_adapter = old_config.get("default_adapter", "aitrackdown")
|
|
154
|
+
|
|
155
|
+
for name, config in old_config["adapters"].items():
|
|
156
|
+
# If config doesn't have "adapter" field, infer from name
|
|
157
|
+
if "adapter" not in config:
|
|
158
|
+
config["adapter"] = name
|
|
159
|
+
|
|
160
|
+
adapters[name] = AdapterConfig.from_dict(config)
|
|
161
|
+
|
|
162
|
+
# Case 3: Already in new format (shouldn't happen but handle it)
|
|
163
|
+
else:
|
|
164
|
+
default_adapter = old_config.get("default_adapter", "aitrackdown")
|
|
165
|
+
|
|
166
|
+
# Create new config
|
|
167
|
+
new_config = TicketerConfig(
|
|
168
|
+
default_adapter=default_adapter,
|
|
169
|
+
adapters=adapters
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return new_config
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def validate_migrated_config(config: TicketerConfig) -> bool:
|
|
176
|
+
"""Validate migrated configuration.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
config: Migrated configuration
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
True if valid, False otherwise
|
|
183
|
+
"""
|
|
184
|
+
from ..core.project_config import ConfigValidator
|
|
185
|
+
|
|
186
|
+
if not config.adapters:
|
|
187
|
+
console.print("[yellow]Warning: No adapters configured[/yellow]")
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
all_valid = True
|
|
191
|
+
|
|
192
|
+
for name, adapter_config in config.adapters.items():
|
|
193
|
+
adapter_dict = adapter_config.to_dict()
|
|
194
|
+
adapter_type = adapter_dict.get("adapter")
|
|
195
|
+
|
|
196
|
+
is_valid, error = ConfigValidator.validate(adapter_type, adapter_dict)
|
|
197
|
+
|
|
198
|
+
if not is_valid:
|
|
199
|
+
console.print(f"[red]✗[/red] {name}: {error}")
|
|
200
|
+
all_valid = False
|
|
201
|
+
else:
|
|
202
|
+
console.print(f"[green]✓[/green] {name}: Valid")
|
|
203
|
+
|
|
204
|
+
return all_valid
|