mcp-ticketer 0.3.0__py3-none-any.whl → 2.2.9__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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +930 -52
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1537 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/github/adapter.py +3229 -0
- 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/hybrid.py +58 -16
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/jira/adapter.py +1351 -0
- 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/__init__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +3810 -462
- mcp_ticketer/adapters/linear/client.py +312 -69
- mcp_ticketer/adapters/linear/mappers.py +305 -85
- mcp_ticketer/adapters/linear/queries.py +317 -17
- mcp_ticketer/adapters/linear/types.py +187 -64
- mcp_ticketer/adapters/linear.py +2 -2
- 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/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +91 -54
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +1323 -151
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +209 -114
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +256 -130
- mcp_ticketer/cli/main.py +140 -1544
- mcp_ticketer/cli/mcp_configure.py +1013 -100
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +545 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +794 -0
- mcp_ticketer/cli/simple_health.py +84 -59
- mcp_ticketer/cli/ticket_commands.py +1375 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +195 -72
- mcp_ticketer/core/__init__.py +64 -1
- mcp_ticketer/core/adapter.py +618 -18
- mcp_ticketer/core/config.py +77 -68
- mcp_ticketer/core/env_discovery.py +75 -16
- mcp_ticketer/core/env_loader.py +121 -97
- mcp_ticketer/core/exceptions.py +32 -24
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +566 -19
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +189 -49
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +176 -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/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +723 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +69 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
- 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 +318 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +78 -63
- mcp_ticketer/queue/queue.py +108 -21
- mcp_ticketer/queue/run_worker.py +2 -2
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +96 -58
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
- mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
- mcp_ticketer-2.2.9.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/adapters/github.py +0 -1354
- mcp_ticketer/adapters/jira.py +0 -1011
- mcp_ticketer/mcp/server.py +0 -2030
- mcp_ticketer-0.3.0.dist-info/METADATA +0 -414
- mcp_ticketer-0.3.0.dist-info/RECORD +0 -59
- mcp_ticketer-0.3.0.dist-info/top_level.txt +0 -1
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,40 +1,123 @@
|
|
|
1
1
|
"""Linear-specific CLI commands for workspace and team management."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
|
|
4
|
+
import re
|
|
5
5
|
|
|
6
6
|
import typer
|
|
7
|
+
from gql import Client, gql
|
|
8
|
+
from gql.transport.httpx import HTTPXTransport
|
|
7
9
|
from rich.console import Console
|
|
8
10
|
from rich.table import Table
|
|
9
|
-
from gql import gql, Client
|
|
10
|
-
from gql.transport.httpx import HTTPXTransport
|
|
11
11
|
|
|
12
12
|
app = typer.Typer(name="linear", help="Linear workspace and team management")
|
|
13
13
|
console = Console()
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
async def derive_team_from_url(
|
|
17
|
+
api_key: str, team_url: str
|
|
18
|
+
) -> tuple[str | None, str | None]:
|
|
19
|
+
"""Derive team ID from Linear team issues URL.
|
|
20
|
+
|
|
21
|
+
Accepts URLs like:
|
|
22
|
+
- https://linear.app/1m-hyperdev/team/1M/active
|
|
23
|
+
- https://linear.app/1m-hyperdev/team/1M/
|
|
24
|
+
- https://linear.app/1m-hyperdev/team/1M
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
api_key: Linear API key
|
|
28
|
+
team_url: URL to Linear team issues page
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Tuple of (team_id, error_message). If successful, team_id is set and error_message is None.
|
|
32
|
+
If failed, team_id is None and error_message contains the error.
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
# Extract team key from URL using regex
|
|
36
|
+
# Pattern: https://linear.app/<workspace>/team/<TEAM_KEY>/...
|
|
37
|
+
pattern = r"https://linear\.app/[\w-]+/team/([\w-]+)"
|
|
38
|
+
match = re.search(pattern, team_url)
|
|
39
|
+
|
|
40
|
+
if not match:
|
|
41
|
+
return (
|
|
42
|
+
None,
|
|
43
|
+
"Invalid Linear team URL format. Expected: https://linear.app/<workspace>/team/<TEAM_KEY>",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
team_key = match.group(1)
|
|
47
|
+
console.print(f"[dim]Extracted team key: {team_key}[/dim]")
|
|
48
|
+
|
|
49
|
+
# Query Linear API to resolve team key to team ID
|
|
50
|
+
query = gql(
|
|
51
|
+
"""
|
|
52
|
+
query GetTeamByKey($key: String!) {
|
|
53
|
+
teams(filter: { key: { eq: $key } }) {
|
|
54
|
+
nodes {
|
|
55
|
+
id
|
|
56
|
+
key
|
|
57
|
+
name
|
|
58
|
+
organization {
|
|
59
|
+
name
|
|
60
|
+
urlKey
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
"""
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
# Create client
|
|
70
|
+
transport = HTTPXTransport(
|
|
71
|
+
url="https://api.linear.app/graphql", headers={"Authorization": api_key}
|
|
72
|
+
)
|
|
73
|
+
client = Client(transport=transport, fetch_schema_from_transport=False)
|
|
74
|
+
|
|
75
|
+
# Execute query
|
|
76
|
+
result = client.execute(query, variable_values={"key": team_key})
|
|
77
|
+
teams = result.get("teams", {}).get("nodes", [])
|
|
78
|
+
|
|
79
|
+
if not teams:
|
|
80
|
+
return (
|
|
81
|
+
None,
|
|
82
|
+
f"Team with key '{team_key}' not found. Please check your team URL and API key.",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
team = teams[0]
|
|
86
|
+
team_id = team["id"]
|
|
87
|
+
team_name = team["name"]
|
|
88
|
+
|
|
89
|
+
console.print(
|
|
90
|
+
f"[green]✓[/green] Resolved team: {team_name} (Key: {team_key}, ID: {team_id})"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return team_id, None
|
|
94
|
+
|
|
95
|
+
except Exception as e:
|
|
96
|
+
return None, f"Failed to query Linear API: {str(e)}"
|
|
97
|
+
|
|
98
|
+
|
|
16
99
|
def _create_linear_client() -> Client:
|
|
17
100
|
"""Create a Linear GraphQL client."""
|
|
18
101
|
api_key = os.getenv("LINEAR_API_KEY")
|
|
19
102
|
if not api_key:
|
|
20
103
|
console.print("[red]❌ LINEAR_API_KEY not found in environment[/red]")
|
|
21
104
|
console.print("Set it in .env.local or environment variables")
|
|
22
|
-
raise typer.Exit(1)
|
|
23
|
-
|
|
105
|
+
raise typer.Exit(1) from None
|
|
106
|
+
|
|
24
107
|
transport = HTTPXTransport(
|
|
25
|
-
url="https://api.linear.app/graphql",
|
|
26
|
-
headers={"Authorization": api_key}
|
|
108
|
+
url="https://api.linear.app/graphql", headers={"Authorization": api_key}
|
|
27
109
|
)
|
|
28
110
|
return Client(transport=transport, fetch_schema_from_transport=False)
|
|
29
111
|
|
|
30
112
|
|
|
31
113
|
@app.command("workspaces")
|
|
32
|
-
def list_workspaces():
|
|
114
|
+
def list_workspaces() -> None:
|
|
33
115
|
"""List all accessible Linear workspaces."""
|
|
34
116
|
console.print("🔍 Discovering Linear workspaces...")
|
|
35
|
-
|
|
117
|
+
|
|
36
118
|
# Query for current organization and user info
|
|
37
|
-
query = gql(
|
|
119
|
+
query = gql(
|
|
120
|
+
"""
|
|
38
121
|
query GetWorkspaceInfo {
|
|
39
122
|
viewer {
|
|
40
123
|
id
|
|
@@ -48,44 +131,54 @@ def list_workspaces():
|
|
|
48
131
|
}
|
|
49
132
|
}
|
|
50
133
|
}
|
|
51
|
-
"""
|
|
52
|
-
|
|
134
|
+
"""
|
|
135
|
+
)
|
|
136
|
+
|
|
53
137
|
try:
|
|
54
138
|
client = _create_linear_client()
|
|
55
139
|
result = client.execute(query)
|
|
56
|
-
|
|
57
|
-
viewer = result.get(
|
|
58
|
-
organization = viewer.get(
|
|
59
|
-
|
|
140
|
+
|
|
141
|
+
viewer = result.get("viewer", {})
|
|
142
|
+
organization = viewer.get("organization", {})
|
|
143
|
+
|
|
60
144
|
console.print(f"\n👤 User: {viewer.get('name')} ({viewer.get('email')})")
|
|
61
|
-
console.print(
|
|
145
|
+
console.print("🏢 Current Workspace:")
|
|
62
146
|
console.print(f" Name: {organization.get('name')}")
|
|
63
147
|
console.print(f" URL Key: {organization.get('urlKey')}")
|
|
64
148
|
console.print(f" ID: {organization.get('id')}")
|
|
65
|
-
if organization.get(
|
|
149
|
+
if organization.get("createdAt"):
|
|
66
150
|
console.print(f" Created: {organization.get('createdAt')}")
|
|
67
|
-
|
|
68
|
-
console.print(
|
|
69
|
-
|
|
70
|
-
|
|
151
|
+
|
|
152
|
+
console.print(
|
|
153
|
+
f"\n✅ API key has access to: {organization.get('name')} workspace"
|
|
154
|
+
)
|
|
155
|
+
console.print(
|
|
156
|
+
f"🌐 Workspace URL: https://linear.app/{organization.get('urlKey')}"
|
|
157
|
+
)
|
|
158
|
+
|
|
71
159
|
except Exception as e:
|
|
72
160
|
console.print(f"[red]❌ Error fetching workspace info: {e}[/red]")
|
|
73
|
-
raise typer.Exit(1)
|
|
161
|
+
raise typer.Exit(1) from e
|
|
74
162
|
|
|
75
163
|
|
|
76
164
|
@app.command("teams")
|
|
77
165
|
def list_teams(
|
|
78
|
-
workspace:
|
|
79
|
-
|
|
80
|
-
)
|
|
166
|
+
workspace: str | None = typer.Option(
|
|
167
|
+
None, "--workspace", "-w", help="Workspace URL key (optional)"
|
|
168
|
+
),
|
|
169
|
+
all_teams: bool = typer.Option(
|
|
170
|
+
False, "--all", "-a", help="Show all teams across all workspaces"
|
|
171
|
+
),
|
|
172
|
+
) -> None:
|
|
81
173
|
"""List all teams in the current workspace or all accessible teams."""
|
|
82
174
|
if all_teams:
|
|
83
175
|
console.print("🔍 Discovering ALL accessible Linear teams across workspaces...")
|
|
84
176
|
else:
|
|
85
177
|
console.print("🔍 Discovering Linear teams...")
|
|
86
|
-
|
|
178
|
+
|
|
87
179
|
# Query for all teams with pagination
|
|
88
|
-
query = gql(
|
|
180
|
+
query = gql(
|
|
181
|
+
"""
|
|
89
182
|
query GetTeams($first: Int, $after: String) {
|
|
90
183
|
viewer {
|
|
91
184
|
organization {
|
|
@@ -128,58 +221,70 @@ def list_teams(
|
|
|
128
221
|
}
|
|
129
222
|
}
|
|
130
223
|
}
|
|
131
|
-
"""
|
|
132
|
-
|
|
224
|
+
"""
|
|
225
|
+
)
|
|
226
|
+
|
|
133
227
|
try:
|
|
134
228
|
client = _create_linear_client()
|
|
135
|
-
|
|
229
|
+
|
|
136
230
|
# Fetch all teams with pagination
|
|
137
231
|
all_teams = []
|
|
138
232
|
has_next_page = True
|
|
139
233
|
after_cursor = None
|
|
140
234
|
current_workspace = None
|
|
141
|
-
|
|
235
|
+
|
|
142
236
|
while has_next_page:
|
|
143
237
|
variables = {"first": 50}
|
|
144
238
|
if after_cursor:
|
|
145
239
|
variables["after"] = after_cursor
|
|
146
|
-
|
|
240
|
+
|
|
147
241
|
result = client.execute(query, variable_values=variables)
|
|
148
|
-
|
|
242
|
+
|
|
149
243
|
# Get workspace info from first page
|
|
150
244
|
if current_workspace is None:
|
|
151
|
-
viewer = result.get(
|
|
152
|
-
current_workspace = viewer.get(
|
|
153
|
-
|
|
154
|
-
teams_data = result.get(
|
|
155
|
-
page_teams = teams_data.get(
|
|
156
|
-
page_info = teams_data.get(
|
|
157
|
-
|
|
245
|
+
viewer = result.get("viewer", {})
|
|
246
|
+
current_workspace = viewer.get("organization", {})
|
|
247
|
+
|
|
248
|
+
teams_data = result.get("teams", {})
|
|
249
|
+
page_teams = teams_data.get("nodes", [])
|
|
250
|
+
page_info = teams_data.get("pageInfo", {})
|
|
251
|
+
|
|
158
252
|
all_teams.extend(page_teams)
|
|
159
|
-
has_next_page = page_info.get(
|
|
160
|
-
after_cursor = page_info.get(
|
|
161
|
-
|
|
253
|
+
has_next_page = page_info.get("hasNextPage", False)
|
|
254
|
+
after_cursor = page_info.get("endCursor")
|
|
255
|
+
|
|
162
256
|
# Display workspace info
|
|
163
|
-
console.print(
|
|
164
|
-
|
|
257
|
+
console.print(
|
|
258
|
+
f"\n🏢 Workspace: {current_workspace.get('name')} ({current_workspace.get('urlKey')})"
|
|
259
|
+
)
|
|
260
|
+
|
|
165
261
|
# Filter teams by workspace if specified
|
|
166
262
|
if workspace:
|
|
167
|
-
filtered_teams = [
|
|
168
|
-
|
|
263
|
+
filtered_teams = [
|
|
264
|
+
team
|
|
265
|
+
for team in all_teams
|
|
266
|
+
if team.get("organization", {}).get("urlKey") == workspace
|
|
267
|
+
]
|
|
169
268
|
if not filtered_teams:
|
|
170
|
-
console.print(
|
|
269
|
+
console.print(
|
|
270
|
+
f"[yellow]No teams found in workspace '{workspace}'[/yellow]"
|
|
271
|
+
)
|
|
171
272
|
return
|
|
172
273
|
all_teams = filtered_teams
|
|
173
274
|
elif not all_teams and current_workspace:
|
|
174
275
|
# If not showing all teams, filter to current workspace only
|
|
175
|
-
filtered_teams = [
|
|
176
|
-
|
|
276
|
+
filtered_teams = [
|
|
277
|
+
team
|
|
278
|
+
for team in all_teams
|
|
279
|
+
if team.get("organization", {}).get("urlKey")
|
|
280
|
+
== current_workspace.get("urlKey")
|
|
281
|
+
]
|
|
177
282
|
all_teams = filtered_teams
|
|
178
|
-
|
|
283
|
+
|
|
179
284
|
if not all_teams:
|
|
180
285
|
console.print("[yellow]No teams found[/yellow]")
|
|
181
286
|
return
|
|
182
|
-
|
|
287
|
+
|
|
183
288
|
# Create table
|
|
184
289
|
title_suffix = " (all workspaces)" if all_teams else ""
|
|
185
290
|
table = Table(title=f"Linear Teams ({len(all_teams)} found){title_suffix}")
|
|
@@ -191,60 +296,65 @@ def list_teams(
|
|
|
191
296
|
table.add_column("Issues", justify="center")
|
|
192
297
|
table.add_column("Projects", justify="center")
|
|
193
298
|
table.add_column("Private", justify="center")
|
|
194
|
-
|
|
299
|
+
|
|
195
300
|
for team in all_teams:
|
|
196
|
-
member_count = len(team.get(
|
|
197
|
-
issue_count = len(team.get(
|
|
198
|
-
project_count = len(team.get(
|
|
199
|
-
is_private = "🔒" if team.get(
|
|
200
|
-
workspace_key = team.get(
|
|
301
|
+
member_count = len(team.get("members", {}).get("nodes", []))
|
|
302
|
+
issue_count = len(team.get("issues", {}).get("nodes", []))
|
|
303
|
+
project_count = len(team.get("projects", {}).get("nodes", []))
|
|
304
|
+
is_private = "🔒" if team.get("private") else "🌐"
|
|
305
|
+
workspace_key = team.get("organization", {}).get("urlKey", "")
|
|
201
306
|
|
|
202
307
|
table.add_row(
|
|
203
|
-
team.get(
|
|
204
|
-
team.get(
|
|
308
|
+
team.get("key", ""),
|
|
309
|
+
team.get("name", ""),
|
|
205
310
|
workspace_key,
|
|
206
|
-
team.get(
|
|
311
|
+
team.get("id", ""),
|
|
207
312
|
str(member_count),
|
|
208
313
|
str(issue_count),
|
|
209
314
|
str(project_count),
|
|
210
|
-
is_private
|
|
315
|
+
is_private,
|
|
211
316
|
)
|
|
212
|
-
|
|
317
|
+
|
|
213
318
|
console.print(table)
|
|
214
|
-
|
|
319
|
+
|
|
215
320
|
# Show configuration suggestions
|
|
216
321
|
if all_teams:
|
|
217
|
-
console.print(
|
|
322
|
+
console.print("\n💡 Configuration suggestions:")
|
|
218
323
|
for team in all_teams[:3]: # Show first 3 teams
|
|
219
324
|
console.print(f" Team '{team.get('name')}':")
|
|
220
325
|
console.print(f" team_key: '{team.get('key')}'")
|
|
221
326
|
console.print(f" team_id: '{team.get('id')}'")
|
|
222
327
|
console.print()
|
|
223
|
-
|
|
328
|
+
|
|
224
329
|
except Exception as e:
|
|
225
330
|
console.print(f"[red]❌ Error fetching teams: {e}[/red]")
|
|
226
|
-
raise typer.Exit(1)
|
|
331
|
+
raise typer.Exit(1) from e
|
|
227
332
|
|
|
228
333
|
|
|
229
334
|
@app.command("configure")
|
|
230
335
|
def configure_team(
|
|
231
|
-
team_key:
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
)
|
|
336
|
+
team_key: str | None = typer.Option(
|
|
337
|
+
None, "--team-key", "-k", help="Team key (e.g., '1M')"
|
|
338
|
+
),
|
|
339
|
+
team_id: str | None = typer.Option(None, "--team-id", "-i", help="Team UUID"),
|
|
340
|
+
workspace: str | None = typer.Option(
|
|
341
|
+
None, "--workspace", "-w", help="Workspace URL key"
|
|
342
|
+
),
|
|
343
|
+
) -> None:
|
|
235
344
|
"""Configure Linear adapter with a specific team."""
|
|
236
345
|
from ..cli.main import load_config, save_config
|
|
237
346
|
|
|
238
347
|
if not team_key and not team_id:
|
|
239
348
|
console.print("[red]❌ Either --team-key or --team-id is required[/red]")
|
|
240
|
-
raise typer.Exit(1)
|
|
349
|
+
raise typer.Exit(1) from None
|
|
241
350
|
|
|
242
351
|
console.print("🔧 Configuring Linear adapter...")
|
|
243
352
|
|
|
244
353
|
# Validate team exists
|
|
245
354
|
if team_id:
|
|
246
355
|
# Validate team by ID
|
|
247
|
-
query = gql(
|
|
356
|
+
query = gql(
|
|
357
|
+
"""
|
|
248
358
|
query GetTeamById($id: String!) {
|
|
249
359
|
team(id: $id) {
|
|
250
360
|
id
|
|
@@ -256,24 +366,26 @@ def configure_team(
|
|
|
256
366
|
}
|
|
257
367
|
}
|
|
258
368
|
}
|
|
259
|
-
"""
|
|
369
|
+
"""
|
|
370
|
+
)
|
|
260
371
|
|
|
261
372
|
try:
|
|
262
373
|
client = _create_linear_client()
|
|
263
374
|
result = client.execute(query, variable_values={"id": team_id})
|
|
264
|
-
team = result.get(
|
|
375
|
+
team = result.get("team")
|
|
265
376
|
|
|
266
377
|
if not team:
|
|
267
378
|
console.print(f"[red]❌ Team with ID '{team_id}' not found[/red]")
|
|
268
|
-
raise typer.Exit(1)
|
|
379
|
+
raise typer.Exit(1) from None
|
|
269
380
|
|
|
270
381
|
except Exception as e:
|
|
271
382
|
console.print(f"[red]❌ Error validating team: {e}[/red]")
|
|
272
|
-
raise typer.Exit(1)
|
|
383
|
+
raise typer.Exit(1) from e
|
|
273
384
|
|
|
274
385
|
elif team_key:
|
|
275
386
|
# Validate team by key
|
|
276
|
-
query = gql(
|
|
387
|
+
query = gql(
|
|
388
|
+
"""
|
|
277
389
|
query GetTeamByKey($key: String!) {
|
|
278
390
|
teams(filter: { key: { eq: $key } }) {
|
|
279
391
|
nodes {
|
|
@@ -287,23 +399,24 @@ def configure_team(
|
|
|
287
399
|
}
|
|
288
400
|
}
|
|
289
401
|
}
|
|
290
|
-
"""
|
|
402
|
+
"""
|
|
403
|
+
)
|
|
291
404
|
|
|
292
405
|
try:
|
|
293
406
|
client = _create_linear_client()
|
|
294
407
|
result = client.execute(query, variable_values={"key": team_key})
|
|
295
|
-
teams = result.get(
|
|
408
|
+
teams = result.get("teams", {}).get("nodes", [])
|
|
296
409
|
|
|
297
410
|
if not teams:
|
|
298
411
|
console.print(f"[red]❌ Team with key '{team_key}' not found[/red]")
|
|
299
|
-
raise typer.Exit(1)
|
|
412
|
+
raise typer.Exit(1) from None
|
|
300
413
|
|
|
301
414
|
team = teams[0]
|
|
302
|
-
team_id = team[
|
|
415
|
+
team_id = team["id"] # Use the found team ID
|
|
303
416
|
|
|
304
417
|
except Exception as e:
|
|
305
418
|
console.print(f"[red]❌ Error validating team: {e}[/red]")
|
|
306
|
-
raise typer.Exit(1)
|
|
419
|
+
raise typer.Exit(1) from e
|
|
307
420
|
|
|
308
421
|
# Update configuration
|
|
309
422
|
config = load_config()
|
|
@@ -313,10 +426,7 @@ def configure_team(
|
|
|
313
426
|
config["adapters"] = {}
|
|
314
427
|
|
|
315
428
|
# Update Linear adapter configuration
|
|
316
|
-
linear_config = {
|
|
317
|
-
"type": "linear",
|
|
318
|
-
"team_id": team_id
|
|
319
|
-
}
|
|
429
|
+
linear_config = {"type": "linear", "team_id": team_id}
|
|
320
430
|
|
|
321
431
|
if team_key:
|
|
322
432
|
linear_config["team_key"] = team_key
|
|
@@ -327,23 +437,27 @@ def configure_team(
|
|
|
327
437
|
|
|
328
438
|
# Save configuration
|
|
329
439
|
save_config(config)
|
|
330
|
-
|
|
331
|
-
console.print(
|
|
440
|
+
|
|
441
|
+
console.print("✅ Linear adapter configured successfully!")
|
|
332
442
|
console.print(f" Team: {team.get('name')} ({team.get('key')})")
|
|
333
443
|
console.print(f" Team ID: {team_id}")
|
|
334
444
|
console.print(f" Workspace: {team.get('organization', {}).get('name')}")
|
|
335
|
-
|
|
445
|
+
|
|
336
446
|
# Test the configuration
|
|
337
447
|
console.print("\n🧪 Testing configuration...")
|
|
338
448
|
try:
|
|
339
449
|
from ..adapters.linear import LinearAdapter
|
|
450
|
+
|
|
340
451
|
adapter = LinearAdapter(linear_config)
|
|
341
|
-
|
|
452
|
+
|
|
342
453
|
# Test by listing a few tickets
|
|
343
454
|
import asyncio
|
|
455
|
+
|
|
344
456
|
tickets = asyncio.run(adapter.list(limit=1))
|
|
345
|
-
console.print(
|
|
346
|
-
|
|
457
|
+
console.print(
|
|
458
|
+
f"✅ Configuration test successful! Found {len(tickets)} ticket(s)"
|
|
459
|
+
)
|
|
460
|
+
|
|
347
461
|
except Exception as e:
|
|
348
462
|
console.print(f"[yellow]⚠️ Configuration saved but test failed: {e}[/yellow]")
|
|
349
463
|
console.print("You may need to check your API key or team permissions")
|
|
@@ -351,17 +465,22 @@ def configure_team(
|
|
|
351
465
|
|
|
352
466
|
@app.command("info")
|
|
353
467
|
def show_info(
|
|
354
|
-
team_key:
|
|
355
|
-
|
|
356
|
-
)
|
|
468
|
+
team_key: str | None = typer.Option(
|
|
469
|
+
None, "--team-key", "-k", help="Team key to show info for"
|
|
470
|
+
),
|
|
471
|
+
team_id: str | None = typer.Option(
|
|
472
|
+
None, "--team-id", "-i", help="Team UUID to show info for"
|
|
473
|
+
),
|
|
474
|
+
) -> None:
|
|
357
475
|
"""Show detailed information about a specific team."""
|
|
358
476
|
if not team_key and not team_id:
|
|
359
477
|
console.print("[red]❌ Either --team-key or --team-id is required[/red]")
|
|
360
|
-
raise typer.Exit(1)
|
|
361
|
-
|
|
478
|
+
raise typer.Exit(1) from None
|
|
479
|
+
|
|
362
480
|
# Query for detailed team information
|
|
363
481
|
if team_id:
|
|
364
|
-
query = gql(
|
|
482
|
+
query = gql(
|
|
483
|
+
"""
|
|
365
484
|
query GetTeamInfo($id: String!) {
|
|
366
485
|
team(id: $id) {
|
|
367
486
|
id
|
|
@@ -392,10 +511,12 @@ def show_info(
|
|
|
392
511
|
}
|
|
393
512
|
}
|
|
394
513
|
}
|
|
395
|
-
"""
|
|
514
|
+
"""
|
|
515
|
+
)
|
|
396
516
|
variables = {"id": team_id}
|
|
397
517
|
else:
|
|
398
|
-
query = gql(
|
|
518
|
+
query = gql(
|
|
519
|
+
"""
|
|
399
520
|
query GetTeamInfoByKey($key: String!) {
|
|
400
521
|
teams(filter: { key: { eq: $key } }) {
|
|
401
522
|
nodes {
|
|
@@ -428,63 +549,68 @@ def show_info(
|
|
|
428
549
|
}
|
|
429
550
|
}
|
|
430
551
|
}
|
|
431
|
-
"""
|
|
552
|
+
"""
|
|
553
|
+
)
|
|
432
554
|
variables = {"key": team_key}
|
|
433
|
-
|
|
555
|
+
|
|
434
556
|
try:
|
|
435
557
|
client = _create_linear_client()
|
|
436
558
|
result = client.execute(query, variable_values=variables)
|
|
437
|
-
|
|
559
|
+
|
|
438
560
|
if team_id:
|
|
439
|
-
team = result.get(
|
|
561
|
+
team = result.get("team")
|
|
440
562
|
else:
|
|
441
|
-
teams = result.get(
|
|
563
|
+
teams = result.get("teams", {}).get("nodes", [])
|
|
442
564
|
team = teams[0] if teams else None
|
|
443
|
-
|
|
565
|
+
|
|
444
566
|
if not team:
|
|
445
567
|
identifier = team_id or team_key
|
|
446
568
|
console.print(f"[red]❌ Team '{identifier}' not found[/red]")
|
|
447
|
-
raise typer.Exit(1)
|
|
448
|
-
|
|
569
|
+
raise typer.Exit(1) from None
|
|
570
|
+
|
|
449
571
|
# Display team information
|
|
450
572
|
console.print(f"\n🏷️ Team: {team.get('name')}")
|
|
451
573
|
console.print(f" Key: {team.get('key')}")
|
|
452
574
|
console.print(f" ID: {team.get('id')}")
|
|
453
|
-
console.print(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
575
|
+
console.print(
|
|
576
|
+
f" Workspace: {team.get('organization', {}).get('name')} ({team.get('organization', {}).get('urlKey')})"
|
|
577
|
+
)
|
|
578
|
+
console.print(
|
|
579
|
+
f" Privacy: {'🔒 Private' if team.get('private') else '🌐 Public'}"
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
if team.get("description"):
|
|
457
583
|
console.print(f" Description: {team.get('description')}")
|
|
458
|
-
|
|
584
|
+
|
|
459
585
|
console.print(f" Created: {team.get('createdAt')}")
|
|
460
|
-
|
|
586
|
+
|
|
461
587
|
# Statistics
|
|
462
|
-
member_count = len(team.get(
|
|
463
|
-
state_count = len(team.get(
|
|
464
|
-
|
|
465
|
-
console.print(
|
|
588
|
+
member_count = len(team.get("members", {}).get("nodes", []))
|
|
589
|
+
state_count = len(team.get("states", {}).get("nodes", []))
|
|
590
|
+
|
|
591
|
+
console.print("\n📊 Statistics:")
|
|
466
592
|
console.print(f" Members: {member_count}")
|
|
467
593
|
console.print(f" Workflow States: {state_count}")
|
|
468
|
-
|
|
594
|
+
|
|
469
595
|
# Show members
|
|
470
|
-
members = team.get(
|
|
596
|
+
members = team.get("members", {}).get("nodes", [])
|
|
471
597
|
if members:
|
|
472
598
|
console.print(f"\n👥 Members ({len(members)}):")
|
|
473
599
|
for member in members:
|
|
474
|
-
status = "✅" if member.get(
|
|
600
|
+
status = "✅" if member.get("active") else "❌"
|
|
475
601
|
console.print(f" {status} {member.get('name')}")
|
|
476
602
|
if len(members) == 10:
|
|
477
|
-
console.print(
|
|
478
|
-
|
|
603
|
+
console.print(" ... (showing first 10 members)")
|
|
604
|
+
|
|
479
605
|
# Show workflow states
|
|
480
|
-
states = team.get(
|
|
606
|
+
states = team.get("states", {}).get("nodes", [])
|
|
481
607
|
if states:
|
|
482
608
|
console.print(f"\n🔄 Workflow States ({len(states)}):")
|
|
483
|
-
for state in sorted(states, key=lambda s: s.get(
|
|
609
|
+
for state in sorted(states, key=lambda s: s.get("position", 0)):
|
|
484
610
|
console.print(f" {state.get('name')} ({state.get('type')})")
|
|
485
611
|
if len(states) == 20:
|
|
486
|
-
console.print(
|
|
487
|
-
|
|
612
|
+
console.print(" ... (showing first 20 states)")
|
|
613
|
+
|
|
488
614
|
except Exception as e:
|
|
489
615
|
console.print(f"[red]❌ Error fetching team info: {e}[/red]")
|
|
490
|
-
raise typer.Exit(1)
|
|
616
|
+
raise typer.Exit(1) from e
|