mcp-ticketer 0.12.0__py3-none-any.whl → 2.0.1__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/__init__.py +10 -10
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/adapters/aitrackdown.py +385 -6
- mcp_ticketer/adapters/asana/adapter.py +108 -0
- mcp_ticketer/adapters/asana/mappers.py +14 -0
- mcp_ticketer/adapters/github.py +525 -11
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira.py +521 -0
- mcp_ticketer/adapters/linear/adapter.py +1784 -101
- mcp_ticketer/adapters/linear/client.py +85 -3
- mcp_ticketer/adapters/linear/mappers.py +96 -8
- mcp_ticketer/adapters/linear/queries.py +168 -1
- mcp_ticketer/adapters/linear/types.py +80 -4
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cli/adapter_diagnostics.py +3 -1
- mcp_ticketer/cli/auggie_configure.py +17 -5
- mcp_ticketer/cli/codex_configure.py +97 -61
- mcp_ticketer/cli/configure.py +851 -103
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +13 -12
- mcp_ticketer/cli/discover.py +5 -0
- mcp_ticketer/cli/gemini_configure.py +17 -5
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +6 -0
- mcp_ticketer/cli/main.py +233 -3151
- mcp_ticketer/cli/mcp_configure.py +672 -98
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/platform_detection.py +77 -12
- mcp_ticketer/cli/platform_installer.py +536 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +12 -10
- mcp_ticketer/cli/ticket_commands.py +264 -24
- mcp_ticketer/core/__init__.py +28 -6
- mcp_ticketer/core/adapter.py +166 -1
- mcp_ticketer/core/config.py +21 -21
- mcp_ticketer/core/exceptions.py +7 -1
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +31 -19
- mcp_ticketer/core/models.py +135 -0
- mcp_ticketer/core/onepassword_secrets.py +1 -1
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +132 -14
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +106 -25
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +31 -12
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
- mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
- mcp_ticketer/queue/worker.py +1 -1
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
- mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
- mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
- mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
mcp_ticketer/cli/configure.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Interactive configuration wizard for MCP Ticketer."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
from collections.abc import Callable
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
7
|
import typer
|
|
@@ -22,6 +23,61 @@ from ..core.project_config import (
|
|
|
22
23
|
console = Console()
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
def _retry_setting(
|
|
27
|
+
setting_name: str,
|
|
28
|
+
prompt_func: Callable[[], Any],
|
|
29
|
+
validate_func: Callable[[Any], tuple[bool, str | None]],
|
|
30
|
+
max_retries: int = 3,
|
|
31
|
+
) -> Any:
|
|
32
|
+
"""Retry a configuration setting with validation.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
----
|
|
36
|
+
setting_name: Human-readable name of the setting
|
|
37
|
+
prompt_func: Function that prompts for the setting value
|
|
38
|
+
validate_func: Function that validates the value (returns tuple of success, error_msg)
|
|
39
|
+
max_retries: Maximum number of retry attempts
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
-------
|
|
43
|
+
Validated setting value
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
------
|
|
47
|
+
typer.Exit: If max retries exceeded
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
for attempt in range(1, max_retries + 1):
|
|
51
|
+
try:
|
|
52
|
+
value = prompt_func()
|
|
53
|
+
is_valid, error = validate_func(value)
|
|
54
|
+
|
|
55
|
+
if is_valid:
|
|
56
|
+
return value
|
|
57
|
+
console.print(f"[red]✗ {error}[/red]")
|
|
58
|
+
if attempt < max_retries:
|
|
59
|
+
console.print(
|
|
60
|
+
f"[yellow]Attempt {attempt}/{max_retries} - Please try again[/yellow]"
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
console.print(f"[red]Failed after {max_retries} attempts[/red]")
|
|
64
|
+
retry = Confirm.ask("Retry this setting?", default=True)
|
|
65
|
+
if retry:
|
|
66
|
+
# Extend attempts
|
|
67
|
+
max_retries += 3
|
|
68
|
+
console.print(
|
|
69
|
+
f"[yellow]Extending retries (new limit: {max_retries})[/yellow]"
|
|
70
|
+
)
|
|
71
|
+
continue
|
|
72
|
+
raise typer.Exit(1) from None
|
|
73
|
+
except KeyboardInterrupt:
|
|
74
|
+
console.print("\n[yellow]Configuration cancelled[/yellow]")
|
|
75
|
+
raise typer.Exit(0) from None
|
|
76
|
+
|
|
77
|
+
console.print(f"[red]Could not configure {setting_name}[/red]")
|
|
78
|
+
raise typer.Exit(1)
|
|
79
|
+
|
|
80
|
+
|
|
25
81
|
def configure_wizard() -> None:
|
|
26
82
|
"""Run interactive configuration wizard."""
|
|
27
83
|
console.print(
|
|
@@ -97,173 +153,853 @@ def _configure_single_adapter() -> TicketerConfig:
|
|
|
97
153
|
adapter_type = adapter_type_map[adapter_choice]
|
|
98
154
|
|
|
99
155
|
# Configure the selected adapter
|
|
156
|
+
default_values: dict[str, str] = {}
|
|
100
157
|
if adapter_type == AdapterType.LINEAR:
|
|
101
|
-
adapter_config = _configure_linear()
|
|
158
|
+
adapter_config, default_values = _configure_linear(interactive=True)
|
|
102
159
|
elif adapter_type == AdapterType.JIRA:
|
|
103
|
-
adapter_config = _configure_jira()
|
|
160
|
+
adapter_config, default_values = _configure_jira(interactive=True)
|
|
104
161
|
elif adapter_type == AdapterType.GITHUB:
|
|
105
|
-
adapter_config = _configure_github()
|
|
162
|
+
adapter_config, default_values = _configure_github(interactive=True)
|
|
106
163
|
else:
|
|
107
|
-
adapter_config = _configure_aitrackdown()
|
|
164
|
+
adapter_config, default_values = _configure_aitrackdown(interactive=True)
|
|
108
165
|
|
|
109
|
-
# Create config
|
|
166
|
+
# Create config with default values
|
|
110
167
|
config = TicketerConfig(
|
|
111
168
|
default_adapter=adapter_type.value,
|
|
112
169
|
adapters={adapter_type.value: adapter_config},
|
|
170
|
+
default_user=default_values.get("default_user"),
|
|
171
|
+
default_project=default_values.get("default_project"),
|
|
172
|
+
default_epic=default_values.get("default_epic"),
|
|
173
|
+
default_tags=default_values.get("default_tags"),
|
|
113
174
|
)
|
|
114
175
|
|
|
115
176
|
return config
|
|
116
177
|
|
|
117
178
|
|
|
118
|
-
def _configure_linear(
|
|
119
|
-
|
|
120
|
-
|
|
179
|
+
def _configure_linear(
|
|
180
|
+
existing_config: dict[str, Any] | None = None,
|
|
181
|
+
interactive: bool = True,
|
|
182
|
+
api_key: str | None = None,
|
|
183
|
+
team_id: str | None = None,
|
|
184
|
+
team_key: str | None = None,
|
|
185
|
+
**kwargs: Any,
|
|
186
|
+
) -> tuple[AdapterConfig, dict[str, Any]]:
|
|
187
|
+
"""Configure Linear adapter with option to preserve existing settings.
|
|
121
188
|
|
|
122
|
-
|
|
123
|
-
api_key = os.getenv("LINEAR_API_KEY") or ""
|
|
124
|
-
if api_key:
|
|
125
|
-
console.print("[dim]Found LINEAR_API_KEY in environment[/dim]")
|
|
126
|
-
use_env = Confirm.ask("Use this API key?", default=True)
|
|
127
|
-
if not use_env:
|
|
128
|
-
api_key = ""
|
|
189
|
+
Supports both interactive (wizard) and programmatic (init command) modes.
|
|
129
190
|
|
|
130
|
-
|
|
131
|
-
|
|
191
|
+
Args:
|
|
192
|
+
----
|
|
193
|
+
existing_config: Optional existing configuration to preserve/update
|
|
194
|
+
interactive: If True, prompt user for missing values (default: True)
|
|
195
|
+
api_key: Pre-provided API key (optional, for programmatic mode)
|
|
196
|
+
team_id: Pre-provided team ID (optional, for programmatic mode)
|
|
197
|
+
team_key: Pre-provided team key (optional, for programmatic mode)
|
|
198
|
+
**kwargs: Additional configuration parameters
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
-------
|
|
202
|
+
Tuple of (AdapterConfig, default_values_dict)
|
|
203
|
+
- AdapterConfig: Configured Linear adapter configuration
|
|
204
|
+
- default_values_dict: Dictionary containing default_user, default_epic, default_project, default_tags
|
|
132
205
|
|
|
133
|
-
|
|
134
|
-
|
|
206
|
+
"""
|
|
207
|
+
if interactive:
|
|
208
|
+
console.print("\n[bold cyan]Linear Configuration[/bold cyan]")
|
|
135
209
|
|
|
136
|
-
#
|
|
137
|
-
|
|
210
|
+
# Check if we have existing config
|
|
211
|
+
has_existing = (
|
|
212
|
+
existing_config is not None and existing_config.get("adapter") == "linear"
|
|
213
|
+
)
|
|
138
214
|
|
|
139
|
-
|
|
140
|
-
project_id = Prompt.ask("Project ID (optional)", default="")
|
|
215
|
+
config_dict: dict[str, Any] = {"adapter": AdapterType.LINEAR.value}
|
|
141
216
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
217
|
+
if has_existing and interactive:
|
|
218
|
+
preserve = Confirm.ask(
|
|
219
|
+
"Existing Linear configuration found. Preserve current settings?",
|
|
220
|
+
default=True,
|
|
221
|
+
)
|
|
222
|
+
if preserve:
|
|
223
|
+
console.print("[green]✓[/green] Keeping existing configuration")
|
|
224
|
+
config_dict = existing_config.copy()
|
|
225
|
+
|
|
226
|
+
# Allow updating specific fields
|
|
227
|
+
update_fields = Confirm.ask("Update specific fields?", default=False)
|
|
228
|
+
if not update_fields:
|
|
229
|
+
# Extract default values before returning
|
|
230
|
+
default_values = {}
|
|
231
|
+
if "user_email" in config_dict:
|
|
232
|
+
default_values["default_user"] = config_dict.pop("user_email")
|
|
233
|
+
if "default_epic" in config_dict:
|
|
234
|
+
default_values["default_epic"] = config_dict.pop("default_epic")
|
|
235
|
+
if "default_project" in config_dict:
|
|
236
|
+
default_values["default_project"] = config_dict.pop(
|
|
237
|
+
"default_project"
|
|
238
|
+
)
|
|
239
|
+
if "default_tags" in config_dict:
|
|
240
|
+
default_values["default_tags"] = config_dict.pop("default_tags")
|
|
241
|
+
return AdapterConfig.from_dict(config_dict), default_values
|
|
242
|
+
|
|
243
|
+
console.print(
|
|
244
|
+
"[yellow]Enter new values or press Enter to keep current[/yellow]"
|
|
245
|
+
)
|
|
146
246
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if
|
|
152
|
-
|
|
247
|
+
# API Key (programmatic mode: use provided value or env, interactive: prompt)
|
|
248
|
+
current_key = config_dict.get("api_key", "") if has_existing else ""
|
|
249
|
+
final_api_key = api_key or os.getenv("LINEAR_API_KEY") or ""
|
|
250
|
+
|
|
251
|
+
if interactive:
|
|
252
|
+
# Interactive mode: prompt with retry
|
|
253
|
+
if final_api_key and not current_key:
|
|
254
|
+
console.print("[dim]Found LINEAR_API_KEY in environment[/dim]")
|
|
255
|
+
use_env = Confirm.ask("Use this API key?", default=True)
|
|
256
|
+
if use_env:
|
|
257
|
+
current_key = final_api_key
|
|
258
|
+
|
|
259
|
+
def prompt_api_key() -> str:
|
|
260
|
+
if current_key:
|
|
261
|
+
api_key_prompt = f"Linear API Key [current: {'*' * 8}...]"
|
|
262
|
+
return Prompt.ask(api_key_prompt, password=True, default=current_key)
|
|
263
|
+
return Prompt.ask("Linear API Key", password=True)
|
|
264
|
+
|
|
265
|
+
def validate_api_key(key: str) -> tuple[bool, str | None]:
|
|
266
|
+
if not key or len(key) < 10:
|
|
267
|
+
return False, "API key must be at least 10 characters"
|
|
268
|
+
return True, None
|
|
269
|
+
|
|
270
|
+
final_api_key = _retry_setting("API Key", prompt_api_key, validate_api_key)
|
|
271
|
+
elif not final_api_key:
|
|
272
|
+
raise ValueError(
|
|
273
|
+
"Linear API key is required (provide api_key parameter or set LINEAR_API_KEY environment variable)"
|
|
274
|
+
)
|
|
153
275
|
|
|
154
|
-
|
|
276
|
+
config_dict["api_key"] = final_api_key
|
|
277
|
+
|
|
278
|
+
# Team Key/ID (programmatic mode: use provided values, interactive: prompt)
|
|
279
|
+
current_team_key = config_dict.get("team_key", "") if has_existing else ""
|
|
280
|
+
config_dict.get("team_id", "") if has_existing else ""
|
|
281
|
+
final_team_key = team_key or os.getenv("LINEAR_TEAM_KEY") or ""
|
|
282
|
+
final_team_id = team_id or os.getenv("LINEAR_TEAM_ID") or ""
|
|
283
|
+
|
|
284
|
+
if interactive:
|
|
285
|
+
# Interactive mode: prompt for team key (preferred over team_id)
|
|
286
|
+
def prompt_team_key() -> str:
|
|
287
|
+
if current_team_key:
|
|
288
|
+
team_key_prompt = f"Linear Team Key [current: {current_team_key}]"
|
|
289
|
+
return Prompt.ask(team_key_prompt, default=current_team_key)
|
|
290
|
+
return Prompt.ask("Linear Team Key (e.g., 'ENG', 'BTA')")
|
|
291
|
+
|
|
292
|
+
def validate_team_key(key: str) -> tuple[bool, str | None]:
|
|
293
|
+
if not key or len(key) < 2:
|
|
294
|
+
return False, "Team key must be at least 2 characters"
|
|
295
|
+
return True, None
|
|
296
|
+
|
|
297
|
+
final_team_key = _retry_setting("Team Key", prompt_team_key, validate_team_key)
|
|
298
|
+
config_dict["team_key"] = final_team_key
|
|
299
|
+
|
|
300
|
+
# Remove team_id if present (will be resolved from team_key)
|
|
301
|
+
if "team_id" in config_dict:
|
|
302
|
+
del config_dict["team_id"]
|
|
303
|
+
else:
|
|
304
|
+
# Programmatic mode: use whichever was provided
|
|
305
|
+
if final_team_key:
|
|
306
|
+
config_dict["team_key"] = final_team_key
|
|
307
|
+
if final_team_id:
|
|
308
|
+
config_dict["team_id"] = final_team_id
|
|
309
|
+
if not final_team_key and not final_team_id:
|
|
310
|
+
raise ValueError(
|
|
311
|
+
"Linear requires either team_key or team_id (provide parameter or set LINEAR_TEAM_KEY/LINEAR_TEAM_ID environment variable)"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# User email configuration (optional, for default assignee) - only in interactive mode
|
|
315
|
+
if interactive:
|
|
316
|
+
current_user_email = config_dict.get("user_email", "") if has_existing else ""
|
|
317
|
+
|
|
318
|
+
def prompt_user_email() -> str:
|
|
319
|
+
if current_user_email:
|
|
320
|
+
user_email_prompt = (
|
|
321
|
+
f"Your Linear email (optional, for auto-assignment) "
|
|
322
|
+
f"[current: {current_user_email}]"
|
|
323
|
+
)
|
|
324
|
+
return Prompt.ask(user_email_prompt, default=current_user_email)
|
|
325
|
+
return Prompt.ask(
|
|
326
|
+
"Your Linear email (optional, for auto-assignment)", default=""
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def validate_user_email(email: str) -> tuple[bool, str | None]:
|
|
330
|
+
if not email: # Optional field
|
|
331
|
+
return True, None
|
|
332
|
+
import re
|
|
333
|
+
|
|
334
|
+
email_pattern = re.compile(
|
|
335
|
+
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
|
336
|
+
)
|
|
337
|
+
if not email_pattern.match(email):
|
|
338
|
+
return False, f"Invalid email format: {email}"
|
|
339
|
+
return True, None
|
|
340
|
+
|
|
341
|
+
user_email = _retry_setting(
|
|
342
|
+
"User Email", prompt_user_email, validate_user_email
|
|
343
|
+
)
|
|
344
|
+
if user_email:
|
|
345
|
+
config_dict["user_email"] = user_email
|
|
346
|
+
console.print(f"[green]✓[/green] Will use {user_email} as default assignee")
|
|
347
|
+
|
|
348
|
+
# ============================================================
|
|
349
|
+
# DEFAULT VALUES SECTION (for ticket creation)
|
|
350
|
+
# ============================================================
|
|
351
|
+
|
|
352
|
+
console.print("\n[bold cyan]Default Values (Optional)[/bold cyan]")
|
|
353
|
+
console.print("Configure default values for ticket creation:")
|
|
354
|
+
|
|
355
|
+
# Default epic/project
|
|
356
|
+
current_default_epic = (
|
|
357
|
+
config_dict.get("default_epic", "") if has_existing else ""
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
def prompt_default_epic() -> str:
|
|
361
|
+
if current_default_epic:
|
|
362
|
+
return Prompt.ask(
|
|
363
|
+
f"Default epic/project ID (optional) [current: {current_default_epic}]",
|
|
364
|
+
default=current_default_epic,
|
|
365
|
+
)
|
|
366
|
+
return Prompt.ask(
|
|
367
|
+
"Default epic/project ID (optional, accepts project URLs or IDs like 'PROJ-123')",
|
|
368
|
+
default="",
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def validate_default_epic(epic_id: str) -> tuple[bool, str | None]:
|
|
372
|
+
if not epic_id: # Optional field
|
|
373
|
+
return True, None
|
|
374
|
+
# Basic validation - just check it's not empty when provided
|
|
375
|
+
if len(epic_id.strip()) < 2:
|
|
376
|
+
return False, "Epic/project ID must be at least 2 characters"
|
|
377
|
+
return True, None
|
|
378
|
+
|
|
379
|
+
default_epic = _retry_setting(
|
|
380
|
+
"Default Epic/Project", prompt_default_epic, validate_default_epic
|
|
381
|
+
)
|
|
382
|
+
if default_epic:
|
|
383
|
+
config_dict["default_epic"] = default_epic
|
|
384
|
+
config_dict["default_project"] = default_epic # Set both for compatibility
|
|
385
|
+
console.print(
|
|
386
|
+
f"[green]✓[/green] Will use '{default_epic}' as default epic/project"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Default tags
|
|
390
|
+
current_default_tags = (
|
|
391
|
+
config_dict.get("default_tags", []) if has_existing else []
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
def prompt_default_tags() -> str:
|
|
395
|
+
if current_default_tags:
|
|
396
|
+
tags_str = ", ".join(current_default_tags)
|
|
397
|
+
return Prompt.ask(
|
|
398
|
+
f"Default tags (optional, comma-separated) [current: {tags_str}]",
|
|
399
|
+
default=tags_str,
|
|
400
|
+
)
|
|
401
|
+
return Prompt.ask(
|
|
402
|
+
"Default tags (optional, comma-separated, e.g., 'bug,urgent')",
|
|
403
|
+
default="",
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
def validate_default_tags(tags_input: str) -> tuple[bool, str | None]:
|
|
407
|
+
if not tags_input: # Optional field
|
|
408
|
+
return True, None
|
|
409
|
+
# Parse and validate tags
|
|
410
|
+
tags = [tag.strip() for tag in tags_input.split(",") if tag.strip()]
|
|
411
|
+
if not tags:
|
|
412
|
+
return False, "Please provide at least one tag or leave empty"
|
|
413
|
+
# Check each tag is reasonable
|
|
414
|
+
for tag in tags:
|
|
415
|
+
if len(tag) < 2:
|
|
416
|
+
return False, f"Tag '{tag}' must be at least 2 characters"
|
|
417
|
+
if len(tag) > 50:
|
|
418
|
+
return False, f"Tag '{tag}' is too long (max 50 characters)"
|
|
419
|
+
return True, None
|
|
420
|
+
|
|
421
|
+
default_tags_input = _retry_setting(
|
|
422
|
+
"Default Tags", prompt_default_tags, validate_default_tags
|
|
423
|
+
)
|
|
424
|
+
if default_tags_input:
|
|
425
|
+
default_tags = [
|
|
426
|
+
tag.strip() for tag in default_tags_input.split(",") if tag.strip()
|
|
427
|
+
]
|
|
428
|
+
config_dict["default_tags"] = default_tags
|
|
429
|
+
console.print(f"[green]✓[/green] Will use tags: {', '.join(default_tags)}")
|
|
430
|
+
|
|
431
|
+
# Validate with detailed error reporting
|
|
155
432
|
is_valid, error = ConfigValidator.validate_linear_config(config_dict)
|
|
433
|
+
|
|
156
434
|
if not is_valid:
|
|
157
|
-
console.print(
|
|
435
|
+
console.print("\n[red]❌ Configuration Validation Failed[/red]")
|
|
436
|
+
console.print(f"[red]Error: {error}[/red]\n")
|
|
437
|
+
|
|
438
|
+
# Show which settings were problematic
|
|
439
|
+
console.print("[yellow]Problematic settings:[/yellow]")
|
|
440
|
+
if "api_key" not in config_dict or not config_dict["api_key"]:
|
|
441
|
+
console.print(" • [red]API Key[/red] - Missing or empty")
|
|
442
|
+
if "team_key" not in config_dict and "team_id" not in config_dict:
|
|
443
|
+
console.print(
|
|
444
|
+
" • [red]Team Key/ID[/red] - Neither team_key nor team_id provided"
|
|
445
|
+
)
|
|
446
|
+
if "user_email" in config_dict:
|
|
447
|
+
email = config_dict["user_email"]
|
|
448
|
+
import re
|
|
449
|
+
|
|
450
|
+
if not re.match(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", email):
|
|
451
|
+
console.print(f" • [red]User Email[/red] - Invalid format: {email}")
|
|
452
|
+
|
|
453
|
+
# Offer to retry specific settings
|
|
454
|
+
console.print("\n[cyan]Options:[/cyan]")
|
|
455
|
+
console.print(" 1. Retry configuration from scratch")
|
|
456
|
+
console.print(" 2. Fix specific settings")
|
|
457
|
+
console.print(" 3. Exit")
|
|
458
|
+
|
|
459
|
+
choice = Prompt.ask("Choose an option", choices=["1", "2", "3"], default="2")
|
|
460
|
+
|
|
461
|
+
if choice == "1":
|
|
462
|
+
# Recursive retry
|
|
463
|
+
return _configure_linear(existing_config=None)
|
|
464
|
+
if choice == "2":
|
|
465
|
+
# Fix specific settings
|
|
466
|
+
return _configure_linear(existing_config=config_dict)
|
|
158
467
|
raise typer.Exit(1) from None
|
|
159
468
|
|
|
160
|
-
|
|
469
|
+
console.print("[green]✓ Configuration validated successfully[/green]")
|
|
470
|
+
|
|
471
|
+
# Extract default values to return separately (not part of AdapterConfig)
|
|
472
|
+
default_values = {}
|
|
473
|
+
if "user_email" in config_dict:
|
|
474
|
+
default_values["default_user"] = config_dict.pop("user_email")
|
|
475
|
+
if "default_epic" in config_dict:
|
|
476
|
+
default_values["default_epic"] = config_dict.pop("default_epic")
|
|
477
|
+
if "default_project" in config_dict:
|
|
478
|
+
default_values["default_project"] = config_dict.pop("default_project")
|
|
479
|
+
if "default_tags" in config_dict:
|
|
480
|
+
default_values["default_tags"] = config_dict.pop("default_tags")
|
|
161
481
|
|
|
482
|
+
return AdapterConfig.from_dict(config_dict), default_values
|
|
162
483
|
|
|
163
|
-
def _configure_jira() -> AdapterConfig:
|
|
164
|
-
"""Configure JIRA adapter."""
|
|
165
|
-
console.print("\n[bold]Configure JIRA Integration:[/bold]")
|
|
166
484
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
485
|
+
def _configure_jira(
|
|
486
|
+
interactive: bool = True,
|
|
487
|
+
server: str | None = None,
|
|
488
|
+
email: str | None = None,
|
|
489
|
+
api_token: str | None = None,
|
|
490
|
+
project_key: str | None = None,
|
|
491
|
+
**kwargs: Any,
|
|
492
|
+
) -> tuple[AdapterConfig, dict[str, Any]]:
|
|
493
|
+
"""Configure JIRA adapter.
|
|
494
|
+
|
|
495
|
+
Supports both interactive (wizard) and programmatic (init command) modes.
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
----
|
|
499
|
+
interactive: If True, prompt user for missing values (default: True)
|
|
500
|
+
server: Pre-provided JIRA server URL (optional)
|
|
501
|
+
email: Pre-provided JIRA user email (optional)
|
|
502
|
+
api_token: Pre-provided JIRA API token (optional)
|
|
503
|
+
project_key: Pre-provided default project key (optional)
|
|
504
|
+
**kwargs: Additional configuration parameters
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
-------
|
|
508
|
+
Tuple of (AdapterConfig, default_values_dict)
|
|
509
|
+
- AdapterConfig: Configured JIRA adapter configuration
|
|
510
|
+
- default_values_dict: Dictionary containing default_user, default_epic, default_project, default_tags
|
|
511
|
+
|
|
512
|
+
"""
|
|
513
|
+
if interactive:
|
|
514
|
+
console.print("\n[bold]Configure JIRA Integration:[/bold]")
|
|
515
|
+
|
|
516
|
+
# Server URL (programmatic mode: use provided value or env, interactive: prompt)
|
|
517
|
+
final_server = server or os.getenv("JIRA_SERVER") or ""
|
|
518
|
+
if interactive and not final_server:
|
|
519
|
+
final_server = Prompt.ask(
|
|
520
|
+
"JIRA Server URL (e.g., https://company.atlassian.net)"
|
|
521
|
+
)
|
|
522
|
+
elif not interactive and not final_server:
|
|
523
|
+
raise ValueError(
|
|
524
|
+
"JIRA server URL is required (provide server parameter or set JIRA_SERVER environment variable)"
|
|
525
|
+
)
|
|
171
526
|
|
|
172
|
-
# Email
|
|
173
|
-
|
|
174
|
-
if not
|
|
175
|
-
|
|
527
|
+
# Email (programmatic mode: use provided value or env, interactive: prompt)
|
|
528
|
+
final_email = email or os.getenv("JIRA_EMAIL") or ""
|
|
529
|
+
if interactive and not final_email:
|
|
530
|
+
final_email = Prompt.ask("JIRA User Email")
|
|
531
|
+
elif not interactive and not final_email:
|
|
532
|
+
raise ValueError(
|
|
533
|
+
"JIRA email is required (provide email parameter or set JIRA_EMAIL environment variable)"
|
|
534
|
+
)
|
|
176
535
|
|
|
177
|
-
# API Token
|
|
178
|
-
|
|
179
|
-
if not
|
|
536
|
+
# API Token (programmatic mode: use provided value or env, interactive: prompt)
|
|
537
|
+
final_api_token = api_token or os.getenv("JIRA_API_TOKEN") or ""
|
|
538
|
+
if interactive and not final_api_token:
|
|
180
539
|
console.print(
|
|
181
540
|
"[dim]Generate token at: https://id.atlassian.com/manage/api-tokens[/dim]"
|
|
182
541
|
)
|
|
183
|
-
|
|
542
|
+
final_api_token = Prompt.ask("JIRA API Token", password=True)
|
|
543
|
+
elif not interactive and not final_api_token:
|
|
544
|
+
raise ValueError(
|
|
545
|
+
"JIRA API token is required (provide api_token parameter or set JIRA_API_TOKEN environment variable)"
|
|
546
|
+
)
|
|
184
547
|
|
|
185
|
-
# Project Key
|
|
186
|
-
|
|
548
|
+
# Project Key (optional)
|
|
549
|
+
final_project_key = project_key or os.getenv("JIRA_PROJECT_KEY") or ""
|
|
550
|
+
if interactive and not final_project_key:
|
|
551
|
+
final_project_key = Prompt.ask(
|
|
552
|
+
"Default Project Key (optional, e.g., PROJ)", default=""
|
|
553
|
+
)
|
|
187
554
|
|
|
188
555
|
config_dict = {
|
|
189
556
|
"adapter": AdapterType.JIRA.value,
|
|
190
|
-
"server":
|
|
191
|
-
"email":
|
|
192
|
-
"api_token":
|
|
557
|
+
"server": final_server.rstrip("/"),
|
|
558
|
+
"email": final_email,
|
|
559
|
+
"api_token": final_api_token,
|
|
193
560
|
}
|
|
194
561
|
|
|
195
|
-
if
|
|
196
|
-
config_dict["project_key"] =
|
|
562
|
+
if final_project_key:
|
|
563
|
+
config_dict["project_key"] = final_project_key
|
|
197
564
|
|
|
198
565
|
# Validate
|
|
199
566
|
is_valid, error = ConfigValidator.validate_jira_config(config_dict)
|
|
200
567
|
if not is_valid:
|
|
201
|
-
|
|
202
|
-
|
|
568
|
+
if interactive:
|
|
569
|
+
console.print(f"[red]Configuration error: {error}[/red]")
|
|
570
|
+
raise typer.Exit(1) from None
|
|
571
|
+
raise ValueError(f"JIRA configuration validation failed: {error}")
|
|
572
|
+
|
|
573
|
+
# ============================================================
|
|
574
|
+
# DEFAULT VALUES SECTION (for ticket creation)
|
|
575
|
+
# ============================================================
|
|
576
|
+
default_values = {}
|
|
577
|
+
|
|
578
|
+
if interactive:
|
|
579
|
+
console.print("\n[bold cyan]Default Values (Optional)[/bold cyan]")
|
|
580
|
+
console.print("Configure default values for ticket creation:")
|
|
581
|
+
|
|
582
|
+
# Default user/assignee
|
|
583
|
+
user_input = Prompt.ask(
|
|
584
|
+
"Default assignee/user (optional, JIRA username or email)",
|
|
585
|
+
default="",
|
|
586
|
+
show_default=False,
|
|
587
|
+
)
|
|
588
|
+
if user_input:
|
|
589
|
+
default_values["default_user"] = user_input
|
|
590
|
+
console.print(
|
|
591
|
+
f"[green]✓[/green] Will use '{user_input}' as default assignee"
|
|
592
|
+
)
|
|
203
593
|
|
|
204
|
-
|
|
594
|
+
# Default epic/project
|
|
595
|
+
epic_input = Prompt.ask(
|
|
596
|
+
"Default epic/project ID (optional, e.g., 'PROJ-123')",
|
|
597
|
+
default="",
|
|
598
|
+
show_default=False,
|
|
599
|
+
)
|
|
600
|
+
if epic_input:
|
|
601
|
+
default_values["default_epic"] = epic_input
|
|
602
|
+
default_values["default_project"] = epic_input # Compatibility
|
|
603
|
+
console.print(
|
|
604
|
+
f"[green]✓[/green] Will use '{epic_input}' as default epic/project"
|
|
605
|
+
)
|
|
205
606
|
|
|
607
|
+
# Default tags
|
|
608
|
+
tags_input = Prompt.ask(
|
|
609
|
+
"Default tags/labels (optional, comma-separated, e.g., 'bug,urgent')",
|
|
610
|
+
default="",
|
|
611
|
+
show_default=False,
|
|
612
|
+
)
|
|
613
|
+
if tags_input:
|
|
614
|
+
tags_list = [t.strip() for t in tags_input.split(",") if t.strip()]
|
|
615
|
+
if tags_list:
|
|
616
|
+
default_values["default_tags"] = tags_list
|
|
617
|
+
console.print(f"[green]✓[/green] Will use tags: {', '.join(tags_list)}")
|
|
206
618
|
|
|
207
|
-
|
|
208
|
-
"""Configure GitHub adapter."""
|
|
209
|
-
console.print("\n[bold]Configure GitHub Integration:[/bold]")
|
|
619
|
+
return AdapterConfig.from_dict(config_dict), default_values
|
|
210
620
|
|
|
211
|
-
# Token
|
|
212
|
-
token = os.getenv("GITHUB_TOKEN") or ""
|
|
213
|
-
if token:
|
|
214
|
-
console.print("[dim]Found GITHUB_TOKEN in environment[/dim]")
|
|
215
|
-
use_env = Confirm.ask("Use this token?", default=True)
|
|
216
|
-
if not use_env:
|
|
217
|
-
token = ""
|
|
218
621
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
622
|
+
def _configure_github(
|
|
623
|
+
interactive: bool = True,
|
|
624
|
+
token: str | None = None,
|
|
625
|
+
repo_url: str | None = None,
|
|
626
|
+
owner: str | None = None,
|
|
627
|
+
repo: str | None = None,
|
|
628
|
+
**kwargs: Any,
|
|
629
|
+
) -> tuple[AdapterConfig, dict[str, Any]]:
|
|
630
|
+
"""Configure GitHub adapter.
|
|
631
|
+
|
|
632
|
+
Supports both interactive (wizard) and programmatic (init command) modes.
|
|
633
|
+
|
|
634
|
+
Args:
|
|
635
|
+
----
|
|
636
|
+
interactive: If True, prompt user for missing values (default: True)
|
|
637
|
+
token: Pre-provided GitHub Personal Access Token (optional)
|
|
638
|
+
repo_url: Pre-provided GitHub repository URL (optional, preferred)
|
|
639
|
+
owner: Pre-provided repository owner (optional, fallback)
|
|
640
|
+
repo: Pre-provided repository name (optional, fallback)
|
|
641
|
+
**kwargs: Additional configuration parameters
|
|
642
|
+
|
|
643
|
+
Returns:
|
|
644
|
+
-------
|
|
645
|
+
Tuple of (AdapterConfig, default_values_dict)
|
|
646
|
+
- AdapterConfig: Configured GitHub adapter configuration
|
|
647
|
+
- default_values_dict: Dictionary containing default_user, default_epic, default_project, default_tags
|
|
648
|
+
|
|
649
|
+
"""
|
|
650
|
+
if interactive:
|
|
651
|
+
console.print("\n[bold]Configure GitHub Integration:[/bold]")
|
|
652
|
+
|
|
653
|
+
# Token (programmatic mode: use provided value or env, interactive: prompt)
|
|
654
|
+
final_token = token or os.getenv("GITHUB_TOKEN") or ""
|
|
655
|
+
if interactive:
|
|
656
|
+
if final_token:
|
|
657
|
+
console.print("[dim]Found GITHUB_TOKEN in environment[/dim]")
|
|
658
|
+
use_env = Confirm.ask("Use this token?", default=True)
|
|
659
|
+
if not use_env:
|
|
660
|
+
final_token = ""
|
|
661
|
+
|
|
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
|
+
elif not final_token:
|
|
671
|
+
raise ValueError(
|
|
672
|
+
"GitHub token is required (provide token parameter or set GITHUB_TOKEN environment variable)"
|
|
222
673
|
)
|
|
674
|
+
|
|
675
|
+
# Repository URL/Owner/Repo - Prioritize repo_url, fallback to owner/repo
|
|
676
|
+
# Programmatic mode: use repo_url or GITHUB_REPO_URL env, fallback to owner/repo
|
|
677
|
+
final_owner = ""
|
|
678
|
+
final_repo = ""
|
|
679
|
+
|
|
680
|
+
# Step 1: Try to get URL from parameter or environment
|
|
681
|
+
url_input = repo_url or os.getenv("GITHUB_REPO_URL") or ""
|
|
682
|
+
|
|
683
|
+
# Step 2: Parse URL if provided
|
|
684
|
+
if url_input:
|
|
685
|
+
from ..core.url_parser import parse_github_repo_url
|
|
686
|
+
|
|
687
|
+
parsed_owner, parsed_repo, error = parse_github_repo_url(url_input)
|
|
688
|
+
if parsed_owner and parsed_repo:
|
|
689
|
+
final_owner = parsed_owner
|
|
690
|
+
final_repo = parsed_repo
|
|
691
|
+
if interactive:
|
|
692
|
+
console.print(
|
|
693
|
+
f"[dim]✓ Extracted repository: {final_owner}/{final_repo}[/dim]"
|
|
694
|
+
)
|
|
695
|
+
else:
|
|
696
|
+
# URL parsing failed
|
|
697
|
+
if interactive:
|
|
698
|
+
console.print(f"[yellow]Warning: {error}[/yellow]")
|
|
699
|
+
else:
|
|
700
|
+
raise ValueError(f"Failed to parse GitHub repository URL: {error}")
|
|
701
|
+
|
|
702
|
+
# Step 3: Interactive mode - prompt for URL if not provided
|
|
703
|
+
if interactive and not final_owner and not final_repo:
|
|
223
704
|
console.print(
|
|
224
|
-
"[dim]
|
|
705
|
+
"[dim]Enter your GitHub repository URL (e.g., https://github.com/owner/repo)[/dim]"
|
|
225
706
|
)
|
|
226
|
-
token = Prompt.ask("GitHub Personal Access Token", password=True)
|
|
227
707
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
708
|
+
# Keep prompting until we get a valid URL
|
|
709
|
+
while not final_owner or not final_repo:
|
|
710
|
+
from ..core.url_parser import parse_github_repo_url
|
|
711
|
+
|
|
712
|
+
url_prompt = Prompt.ask(
|
|
713
|
+
"GitHub Repository URL",
|
|
714
|
+
default="https://github.com/",
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
parsed_owner, parsed_repo, error = parse_github_repo_url(url_prompt)
|
|
718
|
+
if parsed_owner and parsed_repo:
|
|
719
|
+
final_owner = parsed_owner
|
|
720
|
+
final_repo = parsed_repo
|
|
721
|
+
console.print(f"[dim]✓ Repository: {final_owner}/{final_repo}[/dim]")
|
|
722
|
+
break
|
|
723
|
+
else:
|
|
724
|
+
console.print(f"[red]Error: {error}[/red]")
|
|
725
|
+
console.print(
|
|
726
|
+
"[yellow]Please enter a valid GitHub repository URL[/yellow]"
|
|
727
|
+
)
|
|
232
728
|
|
|
233
|
-
#
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
729
|
+
# Step 4: Non-interactive fallback - use individual owner/repo parameters
|
|
730
|
+
if not final_owner or not final_repo:
|
|
731
|
+
fallback_owner = owner or os.getenv("GITHUB_OWNER") or ""
|
|
732
|
+
fallback_repo = repo or os.getenv("GITHUB_REPO") or ""
|
|
733
|
+
|
|
734
|
+
# In non-interactive mode, both must be provided if URL wasn't
|
|
735
|
+
if not interactive:
|
|
736
|
+
if not fallback_owner or not fallback_repo:
|
|
737
|
+
raise ValueError(
|
|
738
|
+
"GitHub repository is required. Provide either:\n"
|
|
739
|
+
" - repo_url parameter or GITHUB_REPO_URL environment variable, OR\n"
|
|
740
|
+
" - Both owner and repo parameters (or GITHUB_OWNER/GITHUB_REPO environment variables)"
|
|
741
|
+
)
|
|
742
|
+
final_owner = fallback_owner
|
|
743
|
+
final_repo = fallback_repo
|
|
744
|
+
else:
|
|
745
|
+
# Interactive mode with fallback values
|
|
746
|
+
if fallback_owner:
|
|
747
|
+
final_owner = fallback_owner
|
|
748
|
+
if fallback_repo:
|
|
749
|
+
final_repo = fallback_repo
|
|
237
750
|
|
|
238
751
|
config_dict = {
|
|
239
752
|
"adapter": AdapterType.GITHUB.value,
|
|
240
|
-
"token":
|
|
241
|
-
"owner":
|
|
242
|
-
"repo":
|
|
243
|
-
"project_id": f"{
|
|
753
|
+
"token": final_token,
|
|
754
|
+
"owner": final_owner,
|
|
755
|
+
"repo": final_repo,
|
|
756
|
+
"project_id": f"{final_owner}/{final_repo}", # Convenience field
|
|
244
757
|
}
|
|
245
758
|
|
|
246
759
|
# Validate
|
|
247
760
|
is_valid, error = ConfigValidator.validate_github_config(config_dict)
|
|
248
761
|
if not is_valid:
|
|
249
|
-
|
|
250
|
-
|
|
762
|
+
if interactive:
|
|
763
|
+
console.print(f"[red]Configuration error: {error}[/red]")
|
|
764
|
+
raise typer.Exit(1) from None
|
|
765
|
+
raise ValueError(f"GitHub configuration validation failed: {error}")
|
|
766
|
+
|
|
767
|
+
# ============================================================
|
|
768
|
+
# DEFAULT VALUES SECTION (for ticket creation)
|
|
769
|
+
# ============================================================
|
|
770
|
+
default_values = {}
|
|
771
|
+
|
|
772
|
+
if interactive:
|
|
773
|
+
console.print("\n[bold cyan]Default Values (Optional)[/bold cyan]")
|
|
774
|
+
console.print("Configure default values for ticket creation:")
|
|
775
|
+
|
|
776
|
+
# Default user/assignee
|
|
777
|
+
user_input = Prompt.ask(
|
|
778
|
+
"Default assignee/user (optional, GitHub username)",
|
|
779
|
+
default="",
|
|
780
|
+
show_default=False,
|
|
781
|
+
)
|
|
782
|
+
if user_input:
|
|
783
|
+
default_values["default_user"] = user_input
|
|
784
|
+
console.print(
|
|
785
|
+
f"[green]✓[/green] Will use '{user_input}' as default assignee"
|
|
786
|
+
)
|
|
251
787
|
|
|
252
|
-
|
|
788
|
+
# Default epic/project (milestone for GitHub)
|
|
789
|
+
epic_input = Prompt.ask(
|
|
790
|
+
"Default milestone/project (optional, e.g., 'v1.0' or milestone number)",
|
|
791
|
+
default="",
|
|
792
|
+
show_default=False,
|
|
793
|
+
)
|
|
794
|
+
if epic_input:
|
|
795
|
+
default_values["default_epic"] = epic_input
|
|
796
|
+
default_values["default_project"] = epic_input # Compatibility
|
|
797
|
+
console.print(
|
|
798
|
+
f"[green]✓[/green] Will use '{epic_input}' as default milestone/project"
|
|
799
|
+
)
|
|
253
800
|
|
|
801
|
+
# Default tags (labels for GitHub)
|
|
802
|
+
tags_input = Prompt.ask(
|
|
803
|
+
"Default labels (optional, comma-separated, e.g., 'bug,enhancement')",
|
|
804
|
+
default="",
|
|
805
|
+
show_default=False,
|
|
806
|
+
)
|
|
807
|
+
if tags_input:
|
|
808
|
+
tags_list = [t.strip() for t in tags_input.split(",") if t.strip()]
|
|
809
|
+
if tags_list:
|
|
810
|
+
default_values["default_tags"] = tags_list
|
|
811
|
+
console.print(
|
|
812
|
+
f"[green]✓[/green] Will use labels: {', '.join(tags_list)}"
|
|
813
|
+
)
|
|
254
814
|
|
|
255
|
-
|
|
256
|
-
"""Configure AITrackdown adapter."""
|
|
257
|
-
console.print("\n[bold]Configure AITrackdown (File-based):[/bold]")
|
|
815
|
+
return AdapterConfig.from_dict(config_dict), default_values
|
|
258
816
|
|
|
259
|
-
|
|
817
|
+
|
|
818
|
+
def _configure_aitrackdown(
|
|
819
|
+
interactive: bool = True, base_path: str | None = None, **kwargs: Any
|
|
820
|
+
) -> tuple[AdapterConfig, dict[str, Any]]:
|
|
821
|
+
"""Configure AITrackdown adapter.
|
|
822
|
+
|
|
823
|
+
Supports both interactive (wizard) and programmatic (init command) modes.
|
|
824
|
+
|
|
825
|
+
Args:
|
|
826
|
+
----
|
|
827
|
+
interactive: If True, prompt user for missing values (default: True)
|
|
828
|
+
base_path: Pre-provided base path for ticket storage (optional)
|
|
829
|
+
**kwargs: Additional configuration parameters
|
|
830
|
+
|
|
831
|
+
Returns:
|
|
832
|
+
-------
|
|
833
|
+
Tuple of (AdapterConfig, default_values_dict)
|
|
834
|
+
- AdapterConfig: Configured AITrackdown adapter configuration
|
|
835
|
+
- default_values_dict: Dictionary containing default_user, default_epic, default_project, default_tags
|
|
836
|
+
|
|
837
|
+
"""
|
|
838
|
+
if interactive:
|
|
839
|
+
console.print("\n[bold]Configure AITrackdown (File-based):[/bold]")
|
|
840
|
+
|
|
841
|
+
# Base path (programmatic mode: use provided value or default, interactive: prompt)
|
|
842
|
+
final_base_path = base_path or ".aitrackdown"
|
|
843
|
+
if interactive:
|
|
844
|
+
final_base_path = Prompt.ask(
|
|
845
|
+
"Base path for ticket storage", default=".aitrackdown"
|
|
846
|
+
)
|
|
260
847
|
|
|
261
848
|
config_dict = {
|
|
262
849
|
"adapter": AdapterType.AITRACKDOWN.value,
|
|
263
|
-
"base_path":
|
|
850
|
+
"base_path": final_base_path,
|
|
264
851
|
}
|
|
265
852
|
|
|
266
|
-
|
|
853
|
+
# ============================================================
|
|
854
|
+
# DEFAULT VALUES SECTION (for ticket creation)
|
|
855
|
+
# ============================================================
|
|
856
|
+
default_values = {}
|
|
857
|
+
|
|
858
|
+
if interactive:
|
|
859
|
+
console.print("\n[bold cyan]Default Values (Optional)[/bold cyan]")
|
|
860
|
+
console.print("Configure default values for ticket creation:")
|
|
861
|
+
|
|
862
|
+
# Default user/assignee
|
|
863
|
+
user_input = Prompt.ask(
|
|
864
|
+
"Default assignee/user (optional)", default="", show_default=False
|
|
865
|
+
)
|
|
866
|
+
if user_input:
|
|
867
|
+
default_values["default_user"] = user_input
|
|
868
|
+
console.print(
|
|
869
|
+
f"[green]✓[/green] Will use '{user_input}' as default assignee"
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
# Default epic/project
|
|
873
|
+
epic_input = Prompt.ask(
|
|
874
|
+
"Default epic/project ID (optional)", default="", show_default=False
|
|
875
|
+
)
|
|
876
|
+
if epic_input:
|
|
877
|
+
default_values["default_epic"] = epic_input
|
|
878
|
+
default_values["default_project"] = epic_input # Compatibility
|
|
879
|
+
console.print(
|
|
880
|
+
f"[green]✓[/green] Will use '{epic_input}' as default epic/project"
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
# Default tags
|
|
884
|
+
tags_input = Prompt.ask(
|
|
885
|
+
"Default tags (optional, comma-separated)", default="", show_default=False
|
|
886
|
+
)
|
|
887
|
+
if tags_input:
|
|
888
|
+
tags_list = [t.strip() for t in tags_input.split(",") if t.strip()]
|
|
889
|
+
if tags_list:
|
|
890
|
+
default_values["default_tags"] = tags_list
|
|
891
|
+
console.print(f"[green]✓[/green] Will use tags: {', '.join(tags_list)}")
|
|
892
|
+
|
|
893
|
+
return AdapterConfig.from_dict(config_dict), default_values
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def prompt_default_values(
|
|
897
|
+
adapter_type: str,
|
|
898
|
+
existing_values: dict[str, Any] | None = None,
|
|
899
|
+
) -> dict[str, Any]:
|
|
900
|
+
"""Prompt user for default values (for ticket creation).
|
|
901
|
+
|
|
902
|
+
This is a standalone function that can be called independently of adapter configuration.
|
|
903
|
+
Used when adapter credentials exist but default values need to be set or updated.
|
|
904
|
+
|
|
905
|
+
Args:
|
|
906
|
+
----
|
|
907
|
+
adapter_type: Type of adapter (linear, jira, github, aitrackdown)
|
|
908
|
+
existing_values: Optional existing default values to show as current values
|
|
909
|
+
|
|
910
|
+
Returns:
|
|
911
|
+
-------
|
|
912
|
+
Dictionary containing default_user, default_epic, default_project, default_tags
|
|
913
|
+
(only includes keys that were provided by the user)
|
|
914
|
+
|
|
915
|
+
"""
|
|
916
|
+
console.print("\n[bold cyan]Default Values (Optional)[/bold cyan]")
|
|
917
|
+
console.print("Configure default values for ticket creation:")
|
|
918
|
+
|
|
919
|
+
default_values = {}
|
|
920
|
+
existing_values = existing_values or {}
|
|
921
|
+
|
|
922
|
+
# Default user/assignee
|
|
923
|
+
current_user = existing_values.get("default_user", "")
|
|
924
|
+
if current_user:
|
|
925
|
+
user_input = Prompt.ask(
|
|
926
|
+
f"Default assignee/user (optional) [current: {current_user}]",
|
|
927
|
+
default=current_user,
|
|
928
|
+
show_default=False,
|
|
929
|
+
)
|
|
930
|
+
else:
|
|
931
|
+
user_input = Prompt.ask(
|
|
932
|
+
"Default assignee/user (optional)",
|
|
933
|
+
default="",
|
|
934
|
+
show_default=False,
|
|
935
|
+
)
|
|
936
|
+
if user_input:
|
|
937
|
+
default_values["default_user"] = user_input
|
|
938
|
+
console.print(f"[green]✓[/green] Will use '{user_input}' as default assignee")
|
|
939
|
+
|
|
940
|
+
# Default epic/project
|
|
941
|
+
current_epic = existing_values.get("default_epic") or existing_values.get(
|
|
942
|
+
"default_project", ""
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
# Adapter-specific messaging
|
|
946
|
+
if adapter_type == "github":
|
|
947
|
+
epic_label = "milestone/project"
|
|
948
|
+
epic_example = "e.g., 'v1.0' or milestone number"
|
|
949
|
+
elif adapter_type == "linear":
|
|
950
|
+
epic_label = "epic/project ID"
|
|
951
|
+
epic_example = "e.g., 'PROJ-123' or UUID"
|
|
952
|
+
else:
|
|
953
|
+
epic_label = "epic/project ID"
|
|
954
|
+
epic_example = "e.g., 'PROJ-123'"
|
|
955
|
+
|
|
956
|
+
if current_epic:
|
|
957
|
+
epic_input = Prompt.ask(
|
|
958
|
+
f"Default {epic_label} (optional) [current: {current_epic}]",
|
|
959
|
+
default=current_epic,
|
|
960
|
+
show_default=False,
|
|
961
|
+
)
|
|
962
|
+
else:
|
|
963
|
+
epic_input = Prompt.ask(
|
|
964
|
+
f"Default {epic_label} (optional, {epic_example})",
|
|
965
|
+
default="",
|
|
966
|
+
show_default=False,
|
|
967
|
+
)
|
|
968
|
+
if epic_input:
|
|
969
|
+
default_values["default_epic"] = epic_input
|
|
970
|
+
default_values["default_project"] = epic_input # Compatibility
|
|
971
|
+
console.print(
|
|
972
|
+
f"[green]✓[/green] Will use '{epic_input}' as default {epic_label}"
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
# Default tags
|
|
976
|
+
current_tags = existing_values.get("default_tags", [])
|
|
977
|
+
current_tags_str = ", ".join(current_tags) if current_tags else ""
|
|
978
|
+
|
|
979
|
+
# Adapter-specific messaging
|
|
980
|
+
tags_label = "labels" if adapter_type == "github" else "tags/labels"
|
|
981
|
+
|
|
982
|
+
if current_tags_str:
|
|
983
|
+
tags_input = Prompt.ask(
|
|
984
|
+
f"Default {tags_label} (optional, comma-separated) [current: {current_tags_str}]",
|
|
985
|
+
default=current_tags_str,
|
|
986
|
+
show_default=False,
|
|
987
|
+
)
|
|
988
|
+
else:
|
|
989
|
+
tags_input = Prompt.ask(
|
|
990
|
+
f"Default {tags_label} (optional, comma-separated, e.g., 'bug,urgent')",
|
|
991
|
+
default="",
|
|
992
|
+
show_default=False,
|
|
993
|
+
)
|
|
994
|
+
if tags_input:
|
|
995
|
+
tags_list = [t.strip() for t in tags_input.split(",") if t.strip()]
|
|
996
|
+
if tags_list:
|
|
997
|
+
default_values["default_tags"] = tags_list
|
|
998
|
+
console.print(
|
|
999
|
+
f"[green]✓[/green] Will use {tags_label}: {', '.join(tags_list)}"
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
return default_values
|
|
267
1003
|
|
|
268
1004
|
|
|
269
1005
|
def _configure_hybrid_mode() -> TicketerConfig:
|
|
@@ -301,20 +1037,25 @@ def _configure_hybrid_mode() -> TicketerConfig:
|
|
|
301
1037
|
|
|
302
1038
|
# Configure each adapter
|
|
303
1039
|
adapters = {}
|
|
1040
|
+
default_values: dict[str, str] = {}
|
|
304
1041
|
for adapter_type in selected_adapters:
|
|
305
1042
|
console.print(f"\n[cyan]Configuring {adapter_type.value}...[/cyan]")
|
|
306
1043
|
|
|
307
1044
|
if adapter_type == AdapterType.LINEAR:
|
|
308
|
-
adapter_config = _configure_linear()
|
|
1045
|
+
adapter_config, adapter_defaults = _configure_linear(interactive=True)
|
|
309
1046
|
elif adapter_type == AdapterType.JIRA:
|
|
310
|
-
adapter_config = _configure_jira()
|
|
1047
|
+
adapter_config, adapter_defaults = _configure_jira(interactive=True)
|
|
311
1048
|
elif adapter_type == AdapterType.GITHUB:
|
|
312
|
-
adapter_config = _configure_github()
|
|
1049
|
+
adapter_config, adapter_defaults = _configure_github(interactive=True)
|
|
313
1050
|
else:
|
|
314
|
-
adapter_config = _configure_aitrackdown()
|
|
1051
|
+
adapter_config, adapter_defaults = _configure_aitrackdown(interactive=True)
|
|
315
1052
|
|
|
316
1053
|
adapters[adapter_type.value] = adapter_config
|
|
317
1054
|
|
|
1055
|
+
# Only save defaults from the first/primary adapter
|
|
1056
|
+
if not default_values:
|
|
1057
|
+
default_values = adapter_defaults
|
|
1058
|
+
|
|
318
1059
|
# Select primary adapter
|
|
319
1060
|
console.print("\n[bold]Select primary adapter (source of truth):[/bold]")
|
|
320
1061
|
for idx, adapter_type in enumerate(selected_adapters, 1):
|
|
@@ -354,9 +1095,15 @@ def _configure_hybrid_mode() -> TicketerConfig:
|
|
|
354
1095
|
sync_strategy=sync_strategy,
|
|
355
1096
|
)
|
|
356
1097
|
|
|
357
|
-
# Create full config
|
|
1098
|
+
# Create full config with default values
|
|
358
1099
|
config = TicketerConfig(
|
|
359
|
-
default_adapter=primary_adapter,
|
|
1100
|
+
default_adapter=primary_adapter,
|
|
1101
|
+
adapters=adapters,
|
|
1102
|
+
hybrid_mode=hybrid_config,
|
|
1103
|
+
default_user=default_values.get("default_user"),
|
|
1104
|
+
default_project=default_values.get("default_project"),
|
|
1105
|
+
default_epic=default_values.get("default_epic"),
|
|
1106
|
+
default_tags=default_values.get("default_tags"),
|
|
360
1107
|
)
|
|
361
1108
|
|
|
362
1109
|
return config
|
|
@@ -437,6 +1184,7 @@ def set_adapter_config(
|
|
|
437
1184
|
"""Set specific adapter configuration values.
|
|
438
1185
|
|
|
439
1186
|
Args:
|
|
1187
|
+
----
|
|
440
1188
|
adapter: Adapter type to set as default
|
|
441
1189
|
api_key: API key/token
|
|
442
1190
|
project_id: Project ID
|