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.

@@ -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