mcp-ticketer 0.1.11__py3-none-any.whl → 0.1.12__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/cli/configure.py +532 -0
- mcp_ticketer/cli/main.py +81 -0
- mcp_ticketer/cli/migrate_config.py +204 -0
- mcp_ticketer/core/project_config.py +553 -0
- mcp_ticketer/queue/queue.py +4 -1
- {mcp_ticketer-0.1.11.dist-info → mcp_ticketer-0.1.12.dist-info}/METADATA +1 -1
- {mcp_ticketer-0.1.11.dist-info → mcp_ticketer-0.1.12.dist-info}/RECORD +15 -11
- {mcp_ticketer-0.1.11.dist-info → mcp_ticketer-0.1.12.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.11.dist-info → mcp_ticketer-0.1.12.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.11.dist-info → mcp_ticketer-0.1.12.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.11.dist-info → mcp_ticketer-0.1.12.dist-info}/top_level.txt +0 -0
|
@@ -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
|