mcp-ticketer 2.0.1__py3-none-any.whl → 2.2.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/_version_scm.py +1 -0
- mcp_ticketer/adapters/aitrackdown.py +122 -0
- mcp_ticketer/adapters/asana/adapter.py +121 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/{github.py → github/adapter.py} +1506 -365
- mcp_ticketer/adapters/github/client.py +335 -0
- mcp_ticketer/adapters/github/mappers.py +797 -0
- mcp_ticketer/adapters/github/queries.py +692 -0
- mcp_ticketer/adapters/github/types.py +460 -0
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/{jira.py → jira/adapter.py} +250 -678
- mcp_ticketer/adapters/jira/client.py +271 -0
- mcp_ticketer/adapters/jira/mappers.py +246 -0
- mcp_ticketer/adapters/jira/queries.py +216 -0
- mcp_ticketer/adapters/jira/types.py +304 -0
- mcp_ticketer/adapters/linear/adapter.py +1000 -92
- mcp_ticketer/adapters/linear/client.py +91 -1
- mcp_ticketer/adapters/linear/mappers.py +107 -0
- mcp_ticketer/adapters/linear/queries.py +112 -2
- mcp_ticketer/adapters/linear/types.py +50 -10
- mcp_ticketer/cli/configure.py +524 -89
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/main.py +10 -0
- mcp_ticketer/cli/mcp_configure.py +177 -49
- mcp_ticketer/cli/platform_installer.py +9 -0
- mcp_ticketer/cli/setup_command.py +157 -1
- mcp_ticketer/cli/ticket_commands.py +443 -81
- mcp_ticketer/cli/utils.py +113 -0
- mcp_ticketer/core/__init__.py +28 -0
- mcp_ticketer/core/adapter.py +367 -1
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +345 -0
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/session_state.py +6 -1
- mcp_ticketer/core/state_matcher.py +36 -3
- mcp_ticketer/mcp/server/__main__.py +2 -1
- mcp_ticketer/mcp/server/routing.py +68 -0
- mcp_ticketer/mcp/server/tools/__init__.py +7 -4
- mcp_ticketer/mcp/server/tools/attachment_tools.py +3 -1
- mcp_ticketer/mcp/server/tools/config_tools.py +233 -35
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +30 -1
- mcp_ticketer/mcp/server/tools/ticket_tools.py +37 -1
- mcp_ticketer/queue/queue.py +68 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/METADATA +33 -3
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/RECORD +72 -36
- mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
- py_mcp_installer/examples/phase3_demo.py +178 -0
- py_mcp_installer/scripts/manage_version.py +54 -0
- py_mcp_installer/setup.py +6 -0
- py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
- py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
- py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
- py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
- py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
- py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
- py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
- py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
- py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
- py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
- py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
- py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
- py_mcp_installer/src/py_mcp_installer/types.py +222 -0
- py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
- py_mcp_installer/tests/__init__.py +0 -0
- py_mcp_installer/tests/platforms/__init__.py +0 -0
- py_mcp_installer/tests/test_platform_detector.py +17 -0
- mcp_ticketer-2.0.1.dist-info/top_level.txt +0 -1
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
mcp_ticketer/cli/configure.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"""Interactive configuration wizard for MCP Ticketer."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import os
|
|
4
5
|
from collections.abc import Callable
|
|
6
|
+
from pathlib import Path
|
|
5
7
|
from typing import Any
|
|
6
8
|
|
|
9
|
+
import httpx
|
|
7
10
|
import typer
|
|
8
11
|
from rich.console import Console
|
|
9
12
|
from rich.panel import Panel
|
|
@@ -23,6 +26,205 @@ from ..core.project_config import (
|
|
|
23
26
|
console = Console()
|
|
24
27
|
|
|
25
28
|
|
|
29
|
+
def _load_existing_adapter_config(adapter_type: str) -> dict[str, Any] | None:
|
|
30
|
+
"""Load existing adapter configuration from project config if available.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
----
|
|
34
|
+
adapter_type: Type of adapter (linear, jira, github, aitrackdown)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
-------
|
|
38
|
+
Dictionary with existing configuration if available, None otherwise
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
config_path = Path.cwd() / ".mcp-ticketer" / "config.json"
|
|
42
|
+
if not config_path.exists():
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
with open(config_path) as f:
|
|
47
|
+
config = json.load(f)
|
|
48
|
+
return config.get("adapters", {}).get(adapter_type)
|
|
49
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
50
|
+
console.print(f"[yellow]Warning: Could not load existing config: {e}[/yellow]")
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _mask_sensitive_value(value: str, show_chars: int = 8) -> str:
|
|
55
|
+
"""Mask sensitive value for display.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
----
|
|
59
|
+
value: The sensitive value to mask
|
|
60
|
+
show_chars: Number of characters to show at start (default: 8)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
-------
|
|
64
|
+
Masked string like "ghp_1234****" or "****" for short values
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
if not value:
|
|
68
|
+
return "****"
|
|
69
|
+
if len(value) <= show_chars:
|
|
70
|
+
return "****"
|
|
71
|
+
return f"{value[:show_chars]}****"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _validate_api_credentials(
|
|
75
|
+
adapter_type: str,
|
|
76
|
+
credentials: dict[str, str],
|
|
77
|
+
max_retries: int = 3,
|
|
78
|
+
) -> bool:
|
|
79
|
+
"""Validate API credentials with real API calls.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
----
|
|
83
|
+
adapter_type: Type of adapter (linear, github, jira)
|
|
84
|
+
credentials: Dictionary with adapter-specific credentials
|
|
85
|
+
max_retries: Maximum retry attempts on validation failure
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
-------
|
|
89
|
+
True if validation succeeds
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
------
|
|
93
|
+
typer.Exit: If user gives up after max retries
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
for attempt in range(1, max_retries + 1):
|
|
97
|
+
try:
|
|
98
|
+
if adapter_type == "linear":
|
|
99
|
+
# Test Linear API with viewer query
|
|
100
|
+
response = httpx.post(
|
|
101
|
+
"https://api.linear.app/graphql",
|
|
102
|
+
headers={"Authorization": credentials["api_key"]},
|
|
103
|
+
json={"query": "{ viewer { id name email } }"},
|
|
104
|
+
timeout=10.0,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if response.status_code == 200:
|
|
108
|
+
data = response.json()
|
|
109
|
+
if "data" in data and "viewer" in data["data"]:
|
|
110
|
+
viewer = data["data"]["viewer"]
|
|
111
|
+
console.print(
|
|
112
|
+
f"[green]✓ Linear API key verified (user: {viewer.get('name', viewer.get('email', 'Unknown'))})[/green]"
|
|
113
|
+
)
|
|
114
|
+
return True
|
|
115
|
+
else:
|
|
116
|
+
errors = data.get("errors", [])
|
|
117
|
+
if errors:
|
|
118
|
+
error_msg = errors[0].get("message", "Unknown error")
|
|
119
|
+
raise ValueError(f"API returned error: {error_msg}")
|
|
120
|
+
raise ValueError("Invalid API response format")
|
|
121
|
+
else:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
f"API returned status {response.status_code}: {response.text}"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
elif adapter_type == "github":
|
|
127
|
+
# Test GitHub API with user endpoint
|
|
128
|
+
response = httpx.get(
|
|
129
|
+
"https://api.github.com/user",
|
|
130
|
+
headers={
|
|
131
|
+
"Authorization": f"Bearer {credentials['token']}",
|
|
132
|
+
"Accept": "application/vnd.github+json",
|
|
133
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
134
|
+
},
|
|
135
|
+
timeout=10.0,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if response.status_code == 200:
|
|
139
|
+
user_data = response.json()
|
|
140
|
+
login = user_data.get("login", "Unknown")
|
|
141
|
+
console.print(
|
|
142
|
+
f"[green]✓ GitHub token verified (user: {login})[/green]"
|
|
143
|
+
)
|
|
144
|
+
return True
|
|
145
|
+
elif response.status_code == 401:
|
|
146
|
+
raise ValueError("Invalid token or token has expired")
|
|
147
|
+
elif response.status_code == 403:
|
|
148
|
+
raise ValueError(
|
|
149
|
+
"Token lacks required permissions (need 'repo' scope)"
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
f"GitHub API returned {response.status_code}: {response.text}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
elif adapter_type == "jira":
|
|
157
|
+
# Test JIRA API with myself endpoint
|
|
158
|
+
response = httpx.get(
|
|
159
|
+
f"{credentials['server'].rstrip('/')}/rest/api/2/myself",
|
|
160
|
+
auth=(credentials["email"], credentials["api_token"]),
|
|
161
|
+
headers={"Accept": "application/json"},
|
|
162
|
+
timeout=10.0,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if response.status_code == 200:
|
|
166
|
+
user_data = response.json()
|
|
167
|
+
name = user_data.get("displayName", credentials["email"])
|
|
168
|
+
console.print(
|
|
169
|
+
f"[green]✓ JIRA credentials verified (user: {name})[/green]"
|
|
170
|
+
)
|
|
171
|
+
return True
|
|
172
|
+
elif response.status_code == 401:
|
|
173
|
+
raise ValueError("Invalid credentials or API token has expired")
|
|
174
|
+
elif response.status_code == 404:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"Invalid JIRA server URL: {credentials['server']}"
|
|
177
|
+
)
|
|
178
|
+
else:
|
|
179
|
+
raise ValueError(
|
|
180
|
+
f"JIRA API returned {response.status_code}: {response.text}"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
else:
|
|
184
|
+
# Unknown adapter type, skip validation
|
|
185
|
+
console.print(
|
|
186
|
+
f"[yellow]Warning: No validation available for adapter '{adapter_type}'[/yellow]"
|
|
187
|
+
)
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
except httpx.TimeoutException:
|
|
191
|
+
console.print(
|
|
192
|
+
f"[red]✗ API validation timed out (attempt {attempt}/{max_retries})[/red]"
|
|
193
|
+
)
|
|
194
|
+
except httpx.NetworkError as e:
|
|
195
|
+
console.print(
|
|
196
|
+
f"[red]✗ Network error during validation: {e} (attempt {attempt}/{max_retries})[/red]"
|
|
197
|
+
)
|
|
198
|
+
except ValueError as e:
|
|
199
|
+
console.print(
|
|
200
|
+
f"[red]✗ API validation failed: {e} (attempt {attempt}/{max_retries})[/red]"
|
|
201
|
+
)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
console.print(
|
|
204
|
+
f"[red]✗ Unexpected error during validation: {e} (attempt {attempt}/{max_retries})[/red]"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Ask user if they want to retry
|
|
208
|
+
if attempt < max_retries:
|
|
209
|
+
retry = Confirm.ask("Re-enter credentials and try again?", default=True)
|
|
210
|
+
if not retry:
|
|
211
|
+
console.print(
|
|
212
|
+
"[yellow]Skipping validation. Configuration saved but may not work.[/yellow]"
|
|
213
|
+
)
|
|
214
|
+
return True # Allow saving unvalidated config
|
|
215
|
+
else:
|
|
216
|
+
console.print("[red]Max retries exceeded[/red]")
|
|
217
|
+
final_choice = Confirm.ask(
|
|
218
|
+
"Save configuration anyway? (it may not work)", default=False
|
|
219
|
+
)
|
|
220
|
+
if final_choice:
|
|
221
|
+
console.print("[yellow]Configuration saved without validation[/yellow]")
|
|
222
|
+
return True
|
|
223
|
+
raise typer.Exit(1) from None
|
|
224
|
+
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
|
|
26
228
|
def _retry_setting(
|
|
27
229
|
setting_name: str,
|
|
28
230
|
prompt_func: Callable[[], Any],
|
|
@@ -207,6 +409,10 @@ def _configure_linear(
|
|
|
207
409
|
if interactive:
|
|
208
410
|
console.print("\n[bold cyan]Linear Configuration[/bold cyan]")
|
|
209
411
|
|
|
412
|
+
# Load existing configuration if available
|
|
413
|
+
if existing_config is None and interactive:
|
|
414
|
+
existing_config = _load_existing_adapter_config("linear")
|
|
415
|
+
|
|
210
416
|
# Check if we have existing config
|
|
211
417
|
has_existing = (
|
|
212
418
|
existing_config is not None and existing_config.get("adapter") == "linear"
|
|
@@ -244,7 +450,7 @@ def _configure_linear(
|
|
|
244
450
|
"[yellow]Enter new values or press Enter to keep current[/yellow]"
|
|
245
451
|
)
|
|
246
452
|
|
|
247
|
-
# API Key
|
|
453
|
+
# API Key with validation loop
|
|
248
454
|
current_key = config_dict.get("api_key", "") if has_existing else ""
|
|
249
455
|
final_api_key = api_key or os.getenv("LINEAR_API_KEY") or ""
|
|
250
456
|
|
|
@@ -256,18 +462,40 @@ def _configure_linear(
|
|
|
256
462
|
if use_env:
|
|
257
463
|
current_key = final_api_key
|
|
258
464
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return Prompt.ask(api_key_prompt, password=True, default=current_key)
|
|
263
|
-
return Prompt.ask("Linear API Key", password=True)
|
|
465
|
+
# Validation loop for API key
|
|
466
|
+
api_key_validated = False
|
|
467
|
+
while not api_key_validated:
|
|
264
468
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
469
|
+
def prompt_api_key() -> str:
|
|
470
|
+
if current_key:
|
|
471
|
+
masked = _mask_sensitive_value(current_key)
|
|
472
|
+
api_key_prompt = f"Linear API Key [current: {masked}]"
|
|
473
|
+
return Prompt.ask(
|
|
474
|
+
api_key_prompt, password=True, default=current_key
|
|
475
|
+
)
|
|
476
|
+
return Prompt.ask("Linear API Key", password=True)
|
|
477
|
+
|
|
478
|
+
def validate_api_key(key: str) -> tuple[bool, str | None]:
|
|
479
|
+
if not key or len(key) < 10:
|
|
480
|
+
return False, "API key must be at least 10 characters"
|
|
481
|
+
return True, None
|
|
482
|
+
|
|
483
|
+
final_api_key = _retry_setting("API Key", prompt_api_key, validate_api_key)
|
|
484
|
+
|
|
485
|
+
# Validate API key with real API call
|
|
486
|
+
try:
|
|
487
|
+
api_key_validated = _validate_api_credentials(
|
|
488
|
+
"linear", {"api_key": final_api_key}
|
|
489
|
+
)
|
|
490
|
+
except typer.Exit:
|
|
491
|
+
# User cancelled, propagate the exit
|
|
492
|
+
raise
|
|
493
|
+
except Exception as e:
|
|
494
|
+
console.print(f"[red]Validation error: {e}[/red]")
|
|
495
|
+
retry = Confirm.ask("Re-enter API key?", default=True)
|
|
496
|
+
if not retry:
|
|
497
|
+
raise typer.Exit(1) from None
|
|
269
498
|
|
|
270
|
-
final_api_key = _retry_setting("API Key", prompt_api_key, validate_api_key)
|
|
271
499
|
elif not final_api_key:
|
|
272
500
|
raise ValueError(
|
|
273
501
|
"Linear API key is required (provide api_key parameter or set LINEAR_API_KEY environment variable)"
|
|
@@ -513,44 +741,112 @@ def _configure_jira(
|
|
|
513
741
|
if interactive:
|
|
514
742
|
console.print("\n[bold]Configure JIRA Integration:[/bold]")
|
|
515
743
|
|
|
516
|
-
#
|
|
744
|
+
# Load existing configuration if available
|
|
745
|
+
existing_config = _load_existing_adapter_config("jira") if interactive else None
|
|
746
|
+
has_existing = existing_config is not None
|
|
747
|
+
|
|
748
|
+
# Server URL (with existing value as default)
|
|
749
|
+
existing_server = existing_config.get("server", "") if has_existing else ""
|
|
517
750
|
final_server = server or os.getenv("JIRA_SERVER") or ""
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
751
|
+
|
|
752
|
+
if interactive:
|
|
753
|
+
if not final_server and existing_server:
|
|
754
|
+
final_server = Prompt.ask(
|
|
755
|
+
f"JIRA Server URL [current: {existing_server}]",
|
|
756
|
+
default=existing_server,
|
|
757
|
+
)
|
|
758
|
+
elif not final_server:
|
|
759
|
+
final_server = Prompt.ask(
|
|
760
|
+
"JIRA Server URL (e.g., https://company.atlassian.net)"
|
|
761
|
+
)
|
|
522
762
|
elif not interactive and not final_server:
|
|
523
763
|
raise ValueError(
|
|
524
764
|
"JIRA server URL is required (provide server parameter or set JIRA_SERVER environment variable)"
|
|
525
765
|
)
|
|
526
766
|
|
|
527
|
-
# Email (
|
|
767
|
+
# Email (with existing value as default)
|
|
768
|
+
existing_email = existing_config.get("email", "") if has_existing else ""
|
|
528
769
|
final_email = email or os.getenv("JIRA_EMAIL") or ""
|
|
529
|
-
|
|
530
|
-
|
|
770
|
+
|
|
771
|
+
if interactive:
|
|
772
|
+
if not final_email and existing_email:
|
|
773
|
+
final_email = Prompt.ask(
|
|
774
|
+
f"JIRA User Email [current: {existing_email}]",
|
|
775
|
+
default=existing_email,
|
|
776
|
+
)
|
|
777
|
+
elif not final_email:
|
|
778
|
+
final_email = Prompt.ask("JIRA User Email")
|
|
531
779
|
elif not interactive and not final_email:
|
|
532
780
|
raise ValueError(
|
|
533
781
|
"JIRA email is required (provide email parameter or set JIRA_EMAIL environment variable)"
|
|
534
782
|
)
|
|
535
783
|
|
|
536
|
-
# API Token
|
|
784
|
+
# API Token with validation loop
|
|
785
|
+
existing_token = existing_config.get("api_token", "") if has_existing else ""
|
|
537
786
|
final_api_token = api_token or os.getenv("JIRA_API_TOKEN") or ""
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
787
|
+
|
|
788
|
+
if interactive:
|
|
789
|
+
# Validation loop for JIRA credentials
|
|
790
|
+
jira_validated = False
|
|
791
|
+
while not jira_validated:
|
|
792
|
+
if not final_api_token and existing_token:
|
|
793
|
+
masked = _mask_sensitive_value(existing_token)
|
|
794
|
+
console.print(
|
|
795
|
+
"[dim]Generate token at: https://id.atlassian.com/manage/api-tokens[/dim]"
|
|
796
|
+
)
|
|
797
|
+
final_api_token = Prompt.ask(
|
|
798
|
+
f"JIRA API Token [current: {masked}]",
|
|
799
|
+
password=True,
|
|
800
|
+
default=existing_token,
|
|
801
|
+
)
|
|
802
|
+
elif not final_api_token:
|
|
803
|
+
console.print(
|
|
804
|
+
"[dim]Generate token at: https://id.atlassian.com/manage/api-tokens[/dim]"
|
|
805
|
+
)
|
|
806
|
+
final_api_token = Prompt.ask("JIRA API Token", password=True)
|
|
807
|
+
|
|
808
|
+
# Validate JIRA credentials with real API call
|
|
809
|
+
try:
|
|
810
|
+
jira_validated = _validate_api_credentials(
|
|
811
|
+
"jira",
|
|
812
|
+
{
|
|
813
|
+
"server": final_server,
|
|
814
|
+
"email": final_email,
|
|
815
|
+
"api_token": final_api_token,
|
|
816
|
+
},
|
|
817
|
+
)
|
|
818
|
+
except typer.Exit:
|
|
819
|
+
# User cancelled, propagate the exit
|
|
820
|
+
raise
|
|
821
|
+
except Exception as e:
|
|
822
|
+
console.print(f"[red]Validation error: {e}[/red]")
|
|
823
|
+
retry = Confirm.ask("Re-enter credentials?", default=True)
|
|
824
|
+
if not retry:
|
|
825
|
+
raise typer.Exit(1) from None
|
|
826
|
+
# Reset to prompt again
|
|
827
|
+
final_api_token = ""
|
|
828
|
+
|
|
543
829
|
elif not interactive and not final_api_token:
|
|
544
830
|
raise ValueError(
|
|
545
831
|
"JIRA API token is required (provide api_token parameter or set JIRA_API_TOKEN environment variable)"
|
|
546
832
|
)
|
|
547
833
|
|
|
548
|
-
# Project Key (optional)
|
|
834
|
+
# Project Key (optional, with existing value as default)
|
|
835
|
+
existing_project_key = (
|
|
836
|
+
existing_config.get("project_key", "") if has_existing else ""
|
|
837
|
+
)
|
|
549
838
|
final_project_key = project_key or os.getenv("JIRA_PROJECT_KEY") or ""
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
839
|
+
|
|
840
|
+
if interactive:
|
|
841
|
+
if not final_project_key and existing_project_key:
|
|
842
|
+
final_project_key = Prompt.ask(
|
|
843
|
+
f"Default Project Key (optional, e.g., PROJ) [current: {existing_project_key}]",
|
|
844
|
+
default=existing_project_key,
|
|
845
|
+
)
|
|
846
|
+
elif not final_project_key:
|
|
847
|
+
final_project_key = Prompt.ask(
|
|
848
|
+
"Default Project Key (optional, e.g., PROJ)", default=""
|
|
849
|
+
)
|
|
554
850
|
|
|
555
851
|
config_dict = {
|
|
556
852
|
"adapter": AdapterType.JIRA.value,
|
|
@@ -580,11 +876,18 @@ def _configure_jira(
|
|
|
580
876
|
console.print("Configure default values for ticket creation:")
|
|
581
877
|
|
|
582
878
|
# Default user/assignee
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
879
|
+
existing_user = existing_config.get("user_email", "") if has_existing else ""
|
|
880
|
+
if existing_user:
|
|
881
|
+
user_input = Prompt.ask(
|
|
882
|
+
f"Default assignee/user (optional, JIRA username or email) [current: {existing_user}]",
|
|
883
|
+
default=existing_user,
|
|
884
|
+
)
|
|
885
|
+
else:
|
|
886
|
+
user_input = Prompt.ask(
|
|
887
|
+
"Default assignee/user (optional, JIRA username or email)",
|
|
888
|
+
default="",
|
|
889
|
+
show_default=False,
|
|
890
|
+
)
|
|
588
891
|
if user_input:
|
|
589
892
|
default_values["default_user"] = user_input
|
|
590
893
|
console.print(
|
|
@@ -592,11 +895,18 @@ def _configure_jira(
|
|
|
592
895
|
)
|
|
593
896
|
|
|
594
897
|
# Default epic/project
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
898
|
+
existing_epic = existing_config.get("default_epic", "") if has_existing else ""
|
|
899
|
+
if existing_epic:
|
|
900
|
+
epic_input = Prompt.ask(
|
|
901
|
+
f"Default epic/project ID (optional, e.g., 'PROJ-123') [current: {existing_epic}]",
|
|
902
|
+
default=existing_epic,
|
|
903
|
+
)
|
|
904
|
+
else:
|
|
905
|
+
epic_input = Prompt.ask(
|
|
906
|
+
"Default epic/project ID (optional, e.g., 'PROJ-123')",
|
|
907
|
+
default="",
|
|
908
|
+
show_default=False,
|
|
909
|
+
)
|
|
600
910
|
if epic_input:
|
|
601
911
|
default_values["default_epic"] = epic_input
|
|
602
912
|
default_values["default_project"] = epic_input # Compatibility
|
|
@@ -605,11 +915,19 @@ def _configure_jira(
|
|
|
605
915
|
)
|
|
606
916
|
|
|
607
917
|
# Default tags
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
918
|
+
existing_tags = existing_config.get("default_tags", []) if has_existing else []
|
|
919
|
+
existing_tags_str = ", ".join(existing_tags) if existing_tags else ""
|
|
920
|
+
if existing_tags_str:
|
|
921
|
+
tags_input = Prompt.ask(
|
|
922
|
+
f"Default tags/labels (optional, comma-separated, e.g., 'bug,urgent') [current: {existing_tags_str}]",
|
|
923
|
+
default=existing_tags_str,
|
|
924
|
+
)
|
|
925
|
+
else:
|
|
926
|
+
tags_input = Prompt.ask(
|
|
927
|
+
"Default tags/labels (optional, comma-separated, e.g., 'bug,urgent')",
|
|
928
|
+
default="",
|
|
929
|
+
show_default=False,
|
|
930
|
+
)
|
|
613
931
|
if tags_input:
|
|
614
932
|
tags_list = [t.strip() for t in tags_input.split(",") if t.strip()]
|
|
615
933
|
if tags_list:
|
|
@@ -650,30 +968,74 @@ def _configure_github(
|
|
|
650
968
|
if interactive:
|
|
651
969
|
console.print("\n[bold]Configure GitHub Integration:[/bold]")
|
|
652
970
|
|
|
653
|
-
#
|
|
971
|
+
# Load existing configuration if available
|
|
972
|
+
existing_config = _load_existing_adapter_config("github") if interactive else None
|
|
973
|
+
has_existing = existing_config is not None
|
|
974
|
+
|
|
975
|
+
# Token with validation loop
|
|
976
|
+
existing_token = existing_config.get("token", "") if has_existing else ""
|
|
654
977
|
final_token = token or os.getenv("GITHUB_TOKEN") or ""
|
|
978
|
+
|
|
655
979
|
if interactive:
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
980
|
+
github_validated = False
|
|
981
|
+
while not github_validated:
|
|
982
|
+
if final_token and not existing_token:
|
|
983
|
+
console.print("[dim]Found GITHUB_TOKEN in environment[/dim]")
|
|
984
|
+
use_env = Confirm.ask("Use this token?", default=True)
|
|
985
|
+
if not use_env:
|
|
986
|
+
final_token = ""
|
|
987
|
+
|
|
988
|
+
if not final_token:
|
|
989
|
+
if existing_token:
|
|
990
|
+
# Show masked existing token as default
|
|
991
|
+
masked = _mask_sensitive_value(existing_token)
|
|
992
|
+
console.print(
|
|
993
|
+
"[dim]Create token at: https://github.com/settings/tokens/new[/dim]"
|
|
994
|
+
)
|
|
995
|
+
console.print(
|
|
996
|
+
"[dim]Required scopes: repo (or public_repo for public repos)[/dim]"
|
|
997
|
+
)
|
|
998
|
+
final_token = Prompt.ask(
|
|
999
|
+
f"GitHub Personal Access Token [current: {masked}]",
|
|
1000
|
+
password=True,
|
|
1001
|
+
default=existing_token,
|
|
1002
|
+
)
|
|
1003
|
+
else:
|
|
1004
|
+
console.print(
|
|
1005
|
+
"[dim]Create token at: https://github.com/settings/tokens/new[/dim]"
|
|
1006
|
+
)
|
|
1007
|
+
console.print(
|
|
1008
|
+
"[dim]Required scopes: repo (or public_repo for public repos)[/dim]"
|
|
1009
|
+
)
|
|
1010
|
+
final_token = Prompt.ask(
|
|
1011
|
+
"GitHub Personal Access Token", password=True
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
# Validate GitHub token with real API call
|
|
1015
|
+
try:
|
|
1016
|
+
github_validated = _validate_api_credentials(
|
|
1017
|
+
"github", {"token": final_token}
|
|
1018
|
+
)
|
|
1019
|
+
except typer.Exit:
|
|
1020
|
+
# User cancelled, propagate the exit
|
|
1021
|
+
raise
|
|
1022
|
+
except Exception as e:
|
|
1023
|
+
console.print(f"[red]Validation error: {e}[/red]")
|
|
1024
|
+
retry = Confirm.ask("Re-enter token?", default=True)
|
|
1025
|
+
if not retry:
|
|
1026
|
+
raise typer.Exit(1) from None
|
|
1027
|
+
# Reset to prompt again
|
|
660
1028
|
final_token = ""
|
|
661
1029
|
|
|
662
|
-
if not final_token:
|
|
663
|
-
console.print(
|
|
664
|
-
"[dim]Create token at: https://github.com/settings/tokens/new[/dim]"
|
|
665
|
-
)
|
|
666
|
-
console.print(
|
|
667
|
-
"[dim]Required scopes: repo (or public_repo for public repos)[/dim]"
|
|
668
|
-
)
|
|
669
|
-
final_token = Prompt.ask("GitHub Personal Access Token", password=True)
|
|
670
1030
|
elif not final_token:
|
|
671
1031
|
raise ValueError(
|
|
672
1032
|
"GitHub token is required (provide token parameter or set GITHUB_TOKEN environment variable)"
|
|
673
1033
|
)
|
|
674
1034
|
|
|
675
1035
|
# Repository URL/Owner/Repo - Prioritize repo_url, fallback to owner/repo
|
|
676
|
-
|
|
1036
|
+
existing_owner = existing_config.get("owner", "") if has_existing else ""
|
|
1037
|
+
existing_repo = existing_config.get("repo", "") if has_existing else ""
|
|
1038
|
+
|
|
677
1039
|
final_owner = ""
|
|
678
1040
|
final_repo = ""
|
|
679
1041
|
|
|
@@ -705,13 +1067,24 @@ def _configure_github(
|
|
|
705
1067
|
"[dim]Enter your GitHub repository URL (e.g., https://github.com/owner/repo)[/dim]"
|
|
706
1068
|
)
|
|
707
1069
|
|
|
1070
|
+
# Show existing as default if available
|
|
1071
|
+
if existing_owner and existing_repo:
|
|
1072
|
+
existing_url = f"https://github.com/{existing_owner}/{existing_repo}"
|
|
1073
|
+
console.print(f"[dim]Current repository: {existing_url}[/dim]")
|
|
1074
|
+
|
|
708
1075
|
# Keep prompting until we get a valid URL
|
|
709
1076
|
while not final_owner or not final_repo:
|
|
710
1077
|
from ..core.url_parser import parse_github_repo_url
|
|
711
1078
|
|
|
1079
|
+
default_prompt = (
|
|
1080
|
+
f"https://github.com/{existing_owner}/{existing_repo}"
|
|
1081
|
+
if existing_owner and existing_repo
|
|
1082
|
+
else "https://github.com/"
|
|
1083
|
+
)
|
|
1084
|
+
|
|
712
1085
|
url_prompt = Prompt.ask(
|
|
713
1086
|
"GitHub Repository URL",
|
|
714
|
-
default=
|
|
1087
|
+
default=default_prompt,
|
|
715
1088
|
)
|
|
716
1089
|
|
|
717
1090
|
parsed_owner, parsed_repo, error = parse_github_repo_url(url_prompt)
|
|
@@ -774,11 +1147,18 @@ def _configure_github(
|
|
|
774
1147
|
console.print("Configure default values for ticket creation:")
|
|
775
1148
|
|
|
776
1149
|
# Default user/assignee
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1150
|
+
existing_user = existing_config.get("user_email", "") if has_existing else ""
|
|
1151
|
+
if existing_user:
|
|
1152
|
+
user_input = Prompt.ask(
|
|
1153
|
+
f"Default assignee/user (optional, GitHub username) [current: {existing_user}]",
|
|
1154
|
+
default=existing_user,
|
|
1155
|
+
)
|
|
1156
|
+
else:
|
|
1157
|
+
user_input = Prompt.ask(
|
|
1158
|
+
"Default assignee/user (optional, GitHub username)",
|
|
1159
|
+
default="",
|
|
1160
|
+
show_default=False,
|
|
1161
|
+
)
|
|
782
1162
|
if user_input:
|
|
783
1163
|
default_values["default_user"] = user_input
|
|
784
1164
|
console.print(
|
|
@@ -786,11 +1166,18 @@ def _configure_github(
|
|
|
786
1166
|
)
|
|
787
1167
|
|
|
788
1168
|
# Default epic/project (milestone for GitHub)
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1169
|
+
existing_epic = existing_config.get("default_epic", "") if has_existing else ""
|
|
1170
|
+
if existing_epic:
|
|
1171
|
+
epic_input = Prompt.ask(
|
|
1172
|
+
f"Default milestone/project (optional, e.g., 'v1.0' or milestone number) [current: {existing_epic}]",
|
|
1173
|
+
default=existing_epic,
|
|
1174
|
+
)
|
|
1175
|
+
else:
|
|
1176
|
+
epic_input = Prompt.ask(
|
|
1177
|
+
"Default milestone/project (optional, e.g., 'v1.0' or milestone number)",
|
|
1178
|
+
default="",
|
|
1179
|
+
show_default=False,
|
|
1180
|
+
)
|
|
794
1181
|
if epic_input:
|
|
795
1182
|
default_values["default_epic"] = epic_input
|
|
796
1183
|
default_values["default_project"] = epic_input # Compatibility
|
|
@@ -799,11 +1186,19 @@ def _configure_github(
|
|
|
799
1186
|
)
|
|
800
1187
|
|
|
801
1188
|
# Default tags (labels for GitHub)
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1189
|
+
existing_tags = existing_config.get("default_tags", []) if has_existing else []
|
|
1190
|
+
existing_tags_str = ", ".join(existing_tags) if existing_tags else ""
|
|
1191
|
+
if existing_tags_str:
|
|
1192
|
+
tags_input = Prompt.ask(
|
|
1193
|
+
f"Default labels (optional, comma-separated, e.g., 'bug,enhancement') [current: {existing_tags_str}]",
|
|
1194
|
+
default=existing_tags_str,
|
|
1195
|
+
)
|
|
1196
|
+
else:
|
|
1197
|
+
tags_input = Prompt.ask(
|
|
1198
|
+
"Default labels (optional, comma-separated, e.g., 'bug,enhancement')",
|
|
1199
|
+
default="",
|
|
1200
|
+
show_default=False,
|
|
1201
|
+
)
|
|
807
1202
|
if tags_input:
|
|
808
1203
|
tags_list = [t.strip() for t in tags_input.split(",") if t.strip()]
|
|
809
1204
|
if tags_list:
|
|
@@ -838,12 +1233,28 @@ def _configure_aitrackdown(
|
|
|
838
1233
|
if interactive:
|
|
839
1234
|
console.print("\n[bold]Configure AITrackdown (File-based):[/bold]")
|
|
840
1235
|
|
|
841
|
-
#
|
|
842
|
-
|
|
1236
|
+
# Load existing configuration if available
|
|
1237
|
+
existing_config = (
|
|
1238
|
+
_load_existing_adapter_config("aitrackdown") if interactive else None
|
|
1239
|
+
)
|
|
1240
|
+
has_existing = existing_config is not None
|
|
1241
|
+
|
|
1242
|
+
# Base path with existing value as default
|
|
1243
|
+
existing_base_path = (
|
|
1244
|
+
existing_config.get("base_path", ".aitrackdown") if has_existing else ""
|
|
1245
|
+
)
|
|
1246
|
+
final_base_path = base_path or existing_base_path or ".aitrackdown"
|
|
1247
|
+
|
|
843
1248
|
if interactive:
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1249
|
+
if existing_base_path:
|
|
1250
|
+
final_base_path = Prompt.ask(
|
|
1251
|
+
f"Base path for ticket storage [current: {existing_base_path}]",
|
|
1252
|
+
default=existing_base_path,
|
|
1253
|
+
)
|
|
1254
|
+
else:
|
|
1255
|
+
final_base_path = Prompt.ask(
|
|
1256
|
+
"Base path for ticket storage", default=".aitrackdown"
|
|
1257
|
+
)
|
|
847
1258
|
|
|
848
1259
|
config_dict = {
|
|
849
1260
|
"adapter": AdapterType.AITRACKDOWN.value,
|
|
@@ -860,9 +1271,16 @@ def _configure_aitrackdown(
|
|
|
860
1271
|
console.print("Configure default values for ticket creation:")
|
|
861
1272
|
|
|
862
1273
|
# Default user/assignee
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1274
|
+
existing_user = existing_config.get("user_email", "") if has_existing else ""
|
|
1275
|
+
if existing_user:
|
|
1276
|
+
user_input = Prompt.ask(
|
|
1277
|
+
f"Default assignee/user (optional) [current: {existing_user}]",
|
|
1278
|
+
default=existing_user,
|
|
1279
|
+
)
|
|
1280
|
+
else:
|
|
1281
|
+
user_input = Prompt.ask(
|
|
1282
|
+
"Default assignee/user (optional)", default="", show_default=False
|
|
1283
|
+
)
|
|
866
1284
|
if user_input:
|
|
867
1285
|
default_values["default_user"] = user_input
|
|
868
1286
|
console.print(
|
|
@@ -870,9 +1288,16 @@ def _configure_aitrackdown(
|
|
|
870
1288
|
)
|
|
871
1289
|
|
|
872
1290
|
# Default epic/project
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1291
|
+
existing_epic = existing_config.get("default_epic", "") if has_existing else ""
|
|
1292
|
+
if existing_epic:
|
|
1293
|
+
epic_input = Prompt.ask(
|
|
1294
|
+
f"Default epic/project ID (optional) [current: {existing_epic}]",
|
|
1295
|
+
default=existing_epic,
|
|
1296
|
+
)
|
|
1297
|
+
else:
|
|
1298
|
+
epic_input = Prompt.ask(
|
|
1299
|
+
"Default epic/project ID (optional)", default="", show_default=False
|
|
1300
|
+
)
|
|
876
1301
|
if epic_input:
|
|
877
1302
|
default_values["default_epic"] = epic_input
|
|
878
1303
|
default_values["default_project"] = epic_input # Compatibility
|
|
@@ -881,9 +1306,19 @@ def _configure_aitrackdown(
|
|
|
881
1306
|
)
|
|
882
1307
|
|
|
883
1308
|
# Default tags
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1309
|
+
existing_tags = existing_config.get("default_tags", []) if has_existing else []
|
|
1310
|
+
existing_tags_str = ", ".join(existing_tags) if existing_tags else ""
|
|
1311
|
+
if existing_tags_str:
|
|
1312
|
+
tags_input = Prompt.ask(
|
|
1313
|
+
f"Default tags (optional, comma-separated) [current: {existing_tags_str}]",
|
|
1314
|
+
default=existing_tags_str,
|
|
1315
|
+
)
|
|
1316
|
+
else:
|
|
1317
|
+
tags_input = Prompt.ask(
|
|
1318
|
+
"Default tags (optional, comma-separated)",
|
|
1319
|
+
default="",
|
|
1320
|
+
show_default=False,
|
|
1321
|
+
)
|
|
887
1322
|
if tags_input:
|
|
888
1323
|
tags_list = [t.strip() for t in tags_input.split(",") if t.strip()]
|
|
889
1324
|
if tags_list:
|