nc1709 1.15.4__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.
- nc1709/__init__.py +13 -0
- nc1709/agent/__init__.py +36 -0
- nc1709/agent/core.py +505 -0
- nc1709/agent/mcp_bridge.py +245 -0
- nc1709/agent/permissions.py +298 -0
- nc1709/agent/tools/__init__.py +21 -0
- nc1709/agent/tools/base.py +440 -0
- nc1709/agent/tools/bash_tool.py +367 -0
- nc1709/agent/tools/file_tools.py +454 -0
- nc1709/agent/tools/notebook_tools.py +516 -0
- nc1709/agent/tools/search_tools.py +322 -0
- nc1709/agent/tools/task_tool.py +284 -0
- nc1709/agent/tools/web_tools.py +555 -0
- nc1709/agents/__init__.py +17 -0
- nc1709/agents/auto_fix.py +506 -0
- nc1709/agents/test_generator.py +507 -0
- nc1709/checkpoints.py +372 -0
- nc1709/cli.py +3380 -0
- nc1709/cli_ui.py +1080 -0
- nc1709/cognitive/__init__.py +149 -0
- nc1709/cognitive/anticipation.py +594 -0
- nc1709/cognitive/context_engine.py +1046 -0
- nc1709/cognitive/council.py +824 -0
- nc1709/cognitive/learning.py +761 -0
- nc1709/cognitive/router.py +583 -0
- nc1709/cognitive/system.py +519 -0
- nc1709/config.py +155 -0
- nc1709/custom_commands.py +300 -0
- nc1709/executor.py +333 -0
- nc1709/file_controller.py +354 -0
- nc1709/git_integration.py +308 -0
- nc1709/github_integration.py +477 -0
- nc1709/image_input.py +446 -0
- nc1709/linting.py +519 -0
- nc1709/llm_adapter.py +667 -0
- nc1709/logger.py +192 -0
- nc1709/mcp/__init__.py +18 -0
- nc1709/mcp/client.py +370 -0
- nc1709/mcp/manager.py +407 -0
- nc1709/mcp/protocol.py +210 -0
- nc1709/mcp/server.py +473 -0
- nc1709/memory/__init__.py +20 -0
- nc1709/memory/embeddings.py +325 -0
- nc1709/memory/indexer.py +474 -0
- nc1709/memory/sessions.py +432 -0
- nc1709/memory/vector_store.py +451 -0
- nc1709/models/__init__.py +86 -0
- nc1709/models/detector.py +377 -0
- nc1709/models/formats.py +315 -0
- nc1709/models/manager.py +438 -0
- nc1709/models/registry.py +497 -0
- nc1709/performance/__init__.py +343 -0
- nc1709/performance/cache.py +705 -0
- nc1709/performance/pipeline.py +611 -0
- nc1709/performance/tiering.py +543 -0
- nc1709/plan_mode.py +362 -0
- nc1709/plugins/__init__.py +17 -0
- nc1709/plugins/agents/__init__.py +18 -0
- nc1709/plugins/agents/django_agent.py +912 -0
- nc1709/plugins/agents/docker_agent.py +623 -0
- nc1709/plugins/agents/fastapi_agent.py +887 -0
- nc1709/plugins/agents/git_agent.py +731 -0
- nc1709/plugins/agents/nextjs_agent.py +867 -0
- nc1709/plugins/base.py +359 -0
- nc1709/plugins/manager.py +411 -0
- nc1709/plugins/registry.py +337 -0
- nc1709/progress.py +443 -0
- nc1709/prompts/__init__.py +22 -0
- nc1709/prompts/agent_system.py +180 -0
- nc1709/prompts/task_prompts.py +340 -0
- nc1709/prompts/unified_prompt.py +133 -0
- nc1709/reasoning_engine.py +541 -0
- nc1709/remote_client.py +266 -0
- nc1709/shell_completions.py +349 -0
- nc1709/slash_commands.py +649 -0
- nc1709/task_classifier.py +408 -0
- nc1709/version_check.py +177 -0
- nc1709/web/__init__.py +8 -0
- nc1709/web/server.py +950 -0
- nc1709/web/templates/index.html +1127 -0
- nc1709-1.15.4.dist-info/METADATA +858 -0
- nc1709-1.15.4.dist-info/RECORD +86 -0
- nc1709-1.15.4.dist-info/WHEEL +5 -0
- nc1709-1.15.4.dist-info/entry_points.txt +2 -0
- nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
- nc1709-1.15.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GitHub Integration for NC1709
|
|
3
|
+
|
|
4
|
+
Provides GitHub-related functionality:
|
|
5
|
+
- PR creation and management
|
|
6
|
+
- Issue tracking
|
|
7
|
+
- GitHub CLI (gh) integration
|
|
8
|
+
- Repository information
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional, List, Dict, Any
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class PullRequest:
|
|
22
|
+
"""Represents a GitHub Pull Request"""
|
|
23
|
+
number: int
|
|
24
|
+
title: str
|
|
25
|
+
url: str
|
|
26
|
+
state: str # open, closed, merged
|
|
27
|
+
author: str
|
|
28
|
+
branch: str
|
|
29
|
+
base: str
|
|
30
|
+
created_at: str
|
|
31
|
+
updated_at: str
|
|
32
|
+
body: Optional[str] = None
|
|
33
|
+
labels: List[str] = field(default_factory=list)
|
|
34
|
+
reviewers: List[str] = field(default_factory=list)
|
|
35
|
+
checks_status: Optional[str] = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Issue:
|
|
40
|
+
"""Represents a GitHub Issue"""
|
|
41
|
+
number: int
|
|
42
|
+
title: str
|
|
43
|
+
url: str
|
|
44
|
+
state: str # open, closed
|
|
45
|
+
author: str
|
|
46
|
+
created_at: str
|
|
47
|
+
body: Optional[str] = None
|
|
48
|
+
labels: List[str] = field(default_factory=list)
|
|
49
|
+
assignees: List[str] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class GitHubIntegration:
|
|
53
|
+
"""
|
|
54
|
+
GitHub integration using the gh CLI.
|
|
55
|
+
|
|
56
|
+
Features:
|
|
57
|
+
- Check gh CLI availability
|
|
58
|
+
- Create and manage PRs
|
|
59
|
+
- View and manage issues
|
|
60
|
+
- Get repository info
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, repo_path: Optional[str] = None):
|
|
64
|
+
"""
|
|
65
|
+
Initialize GitHub integration.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
repo_path: Path to git repository (defaults to cwd)
|
|
69
|
+
"""
|
|
70
|
+
self.repo_path = Path(repo_path) if repo_path else Path.cwd()
|
|
71
|
+
self._gh_available = self._check_gh_cli()
|
|
72
|
+
self._authenticated = False
|
|
73
|
+
|
|
74
|
+
if self._gh_available:
|
|
75
|
+
self._authenticated = self._check_auth()
|
|
76
|
+
|
|
77
|
+
def _check_gh_cli(self) -> bool:
|
|
78
|
+
"""Check if gh CLI is available"""
|
|
79
|
+
try:
|
|
80
|
+
result = subprocess.run(
|
|
81
|
+
["gh", "--version"],
|
|
82
|
+
capture_output=True,
|
|
83
|
+
text=True,
|
|
84
|
+
timeout=5
|
|
85
|
+
)
|
|
86
|
+
return result.returncode == 0
|
|
87
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
def _check_auth(self) -> bool:
|
|
91
|
+
"""Check if gh is authenticated"""
|
|
92
|
+
try:
|
|
93
|
+
result = subprocess.run(
|
|
94
|
+
["gh", "auth", "status"],
|
|
95
|
+
capture_output=True,
|
|
96
|
+
text=True,
|
|
97
|
+
cwd=str(self.repo_path),
|
|
98
|
+
timeout=10
|
|
99
|
+
)
|
|
100
|
+
return result.returncode == 0
|
|
101
|
+
except subprocess.SubprocessError:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def is_available(self) -> bool:
|
|
106
|
+
"""Check if GitHub integration is available"""
|
|
107
|
+
return self._gh_available
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def is_authenticated(self) -> bool:
|
|
111
|
+
"""Check if gh is authenticated"""
|
|
112
|
+
return self._authenticated
|
|
113
|
+
|
|
114
|
+
def _run_gh(self, *args, json_output: bool = False) -> subprocess.CompletedProcess:
|
|
115
|
+
"""Run a gh command"""
|
|
116
|
+
cmd = ["gh"] + list(args)
|
|
117
|
+
if json_output:
|
|
118
|
+
cmd.append("--json")
|
|
119
|
+
return subprocess.run(
|
|
120
|
+
cmd,
|
|
121
|
+
cwd=str(self.repo_path),
|
|
122
|
+
capture_output=True,
|
|
123
|
+
text=True,
|
|
124
|
+
timeout=60
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def get_repo_info(self) -> Optional[Dict[str, Any]]:
|
|
128
|
+
"""Get current repository information"""
|
|
129
|
+
if not self._gh_available:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
result = self._run_gh(
|
|
134
|
+
"repo", "view", "--json",
|
|
135
|
+
"name,owner,description,url,defaultBranchRef,isPrivate"
|
|
136
|
+
)
|
|
137
|
+
if result.returncode == 0:
|
|
138
|
+
return json.loads(result.stdout)
|
|
139
|
+
except (subprocess.SubprocessError, json.JSONDecodeError):
|
|
140
|
+
pass
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
def create_pr(
|
|
144
|
+
self,
|
|
145
|
+
title: str,
|
|
146
|
+
body: str,
|
|
147
|
+
base: str = "main",
|
|
148
|
+
draft: bool = False,
|
|
149
|
+
labels: Optional[List[str]] = None,
|
|
150
|
+
reviewers: Optional[List[str]] = None
|
|
151
|
+
) -> Optional[PullRequest]:
|
|
152
|
+
"""
|
|
153
|
+
Create a new Pull Request.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
title: PR title
|
|
157
|
+
body: PR description
|
|
158
|
+
base: Base branch to merge into
|
|
159
|
+
draft: Create as draft PR
|
|
160
|
+
labels: List of labels to apply
|
|
161
|
+
reviewers: List of reviewers to request
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
PullRequest object if successful, None otherwise
|
|
165
|
+
"""
|
|
166
|
+
if not self._gh_available or not self._authenticated:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
cmd = ["pr", "create", "--title", title, "--body", body, "--base", base]
|
|
171
|
+
|
|
172
|
+
if draft:
|
|
173
|
+
cmd.append("--draft")
|
|
174
|
+
|
|
175
|
+
if labels:
|
|
176
|
+
for label in labels:
|
|
177
|
+
cmd.extend(["--label", label])
|
|
178
|
+
|
|
179
|
+
if reviewers:
|
|
180
|
+
for reviewer in reviewers:
|
|
181
|
+
cmd.extend(["--reviewer", reviewer])
|
|
182
|
+
|
|
183
|
+
result = self._run_gh(*cmd)
|
|
184
|
+
|
|
185
|
+
if result.returncode == 0:
|
|
186
|
+
# Parse the output URL to get PR info
|
|
187
|
+
pr_url = result.stdout.strip()
|
|
188
|
+
|
|
189
|
+
# Get PR details
|
|
190
|
+
pr_info = self.get_pr_by_url(pr_url)
|
|
191
|
+
if pr_info:
|
|
192
|
+
return pr_info
|
|
193
|
+
|
|
194
|
+
# Fallback: create minimal PR object
|
|
195
|
+
return PullRequest(
|
|
196
|
+
number=0,
|
|
197
|
+
title=title,
|
|
198
|
+
url=pr_url,
|
|
199
|
+
state="open",
|
|
200
|
+
author="",
|
|
201
|
+
branch="",
|
|
202
|
+
base=base,
|
|
203
|
+
created_at=datetime.now().isoformat(),
|
|
204
|
+
updated_at=datetime.now().isoformat(),
|
|
205
|
+
body=body
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
except subprocess.SubprocessError:
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def get_pr_by_url(self, url: str) -> Optional[PullRequest]:
|
|
214
|
+
"""Get PR details from URL"""
|
|
215
|
+
try:
|
|
216
|
+
# Extract PR number from URL
|
|
217
|
+
parts = url.rstrip('/').split('/')
|
|
218
|
+
pr_number = parts[-1] if parts else None
|
|
219
|
+
|
|
220
|
+
if pr_number and pr_number.isdigit():
|
|
221
|
+
return self.get_pr(int(pr_number))
|
|
222
|
+
except Exception:
|
|
223
|
+
pass
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
def get_pr(self, number: int) -> Optional[PullRequest]:
|
|
227
|
+
"""Get Pull Request by number"""
|
|
228
|
+
if not self._gh_available:
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
result = self._run_gh(
|
|
233
|
+
"pr", "view", str(number), "--json",
|
|
234
|
+
"number,title,url,state,author,headRefName,baseRefName,createdAt,updatedAt,body,labels,reviewRequests"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if result.returncode == 0:
|
|
238
|
+
data = json.loads(result.stdout)
|
|
239
|
+
return PullRequest(
|
|
240
|
+
number=data.get("number", 0),
|
|
241
|
+
title=data.get("title", ""),
|
|
242
|
+
url=data.get("url", ""),
|
|
243
|
+
state=data.get("state", "").lower(),
|
|
244
|
+
author=data.get("author", {}).get("login", ""),
|
|
245
|
+
branch=data.get("headRefName", ""),
|
|
246
|
+
base=data.get("baseRefName", ""),
|
|
247
|
+
created_at=data.get("createdAt", ""),
|
|
248
|
+
updated_at=data.get("updatedAt", ""),
|
|
249
|
+
body=data.get("body"),
|
|
250
|
+
labels=[l.get("name", "") for l in data.get("labels", [])],
|
|
251
|
+
reviewers=[r.get("login", "") for r in data.get("reviewRequests", [])]
|
|
252
|
+
)
|
|
253
|
+
except (subprocess.SubprocessError, json.JSONDecodeError):
|
|
254
|
+
pass
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
def list_prs(self, state: str = "open", limit: int = 10) -> List[PullRequest]:
|
|
258
|
+
"""List Pull Requests"""
|
|
259
|
+
if not self._gh_available:
|
|
260
|
+
return []
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
result = self._run_gh(
|
|
264
|
+
"pr", "list", "--state", state, "--limit", str(limit), "--json",
|
|
265
|
+
"number,title,url,state,author,headRefName,baseRefName,createdAt,updatedAt,labels"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if result.returncode == 0:
|
|
269
|
+
data = json.loads(result.stdout)
|
|
270
|
+
return [
|
|
271
|
+
PullRequest(
|
|
272
|
+
number=pr.get("number", 0),
|
|
273
|
+
title=pr.get("title", ""),
|
|
274
|
+
url=pr.get("url", ""),
|
|
275
|
+
state=pr.get("state", "").lower(),
|
|
276
|
+
author=pr.get("author", {}).get("login", ""),
|
|
277
|
+
branch=pr.get("headRefName", ""),
|
|
278
|
+
base=pr.get("baseRefName", ""),
|
|
279
|
+
created_at=pr.get("createdAt", ""),
|
|
280
|
+
updated_at=pr.get("updatedAt", ""),
|
|
281
|
+
labels=[l.get("name", "") for l in pr.get("labels", [])]
|
|
282
|
+
)
|
|
283
|
+
for pr in data
|
|
284
|
+
]
|
|
285
|
+
except (subprocess.SubprocessError, json.JSONDecodeError):
|
|
286
|
+
pass
|
|
287
|
+
return []
|
|
288
|
+
|
|
289
|
+
def get_pr_checks(self, number: int) -> Optional[Dict[str, Any]]:
|
|
290
|
+
"""Get CI check status for a PR"""
|
|
291
|
+
if not self._gh_available:
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
result = self._run_gh(
|
|
296
|
+
"pr", "checks", str(number), "--json",
|
|
297
|
+
"name,status,conclusion,startedAt,completedAt"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if result.returncode == 0:
|
|
301
|
+
return json.loads(result.stdout)
|
|
302
|
+
except (subprocess.SubprocessError, json.JSONDecodeError):
|
|
303
|
+
pass
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
def merge_pr(
|
|
307
|
+
self,
|
|
308
|
+
number: int,
|
|
309
|
+
method: str = "merge", # merge, squash, rebase
|
|
310
|
+
delete_branch: bool = True
|
|
311
|
+
) -> bool:
|
|
312
|
+
"""Merge a Pull Request"""
|
|
313
|
+
if not self._gh_available or not self._authenticated:
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
cmd = ["pr", "merge", str(number), f"--{method}"]
|
|
318
|
+
if delete_branch:
|
|
319
|
+
cmd.append("--delete-branch")
|
|
320
|
+
|
|
321
|
+
result = self._run_gh(*cmd)
|
|
322
|
+
return result.returncode == 0
|
|
323
|
+
except subprocess.SubprocessError:
|
|
324
|
+
return False
|
|
325
|
+
|
|
326
|
+
def list_issues(self, state: str = "open", limit: int = 10) -> List[Issue]:
|
|
327
|
+
"""List Issues"""
|
|
328
|
+
if not self._gh_available:
|
|
329
|
+
return []
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
result = self._run_gh(
|
|
333
|
+
"issue", "list", "--state", state, "--limit", str(limit), "--json",
|
|
334
|
+
"number,title,url,state,author,createdAt,body,labels,assignees"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if result.returncode == 0:
|
|
338
|
+
data = json.loads(result.stdout)
|
|
339
|
+
return [
|
|
340
|
+
Issue(
|
|
341
|
+
number=issue.get("number", 0),
|
|
342
|
+
title=issue.get("title", ""),
|
|
343
|
+
url=issue.get("url", ""),
|
|
344
|
+
state=issue.get("state", "").lower(),
|
|
345
|
+
author=issue.get("author", {}).get("login", ""),
|
|
346
|
+
created_at=issue.get("createdAt", ""),
|
|
347
|
+
body=issue.get("body"),
|
|
348
|
+
labels=[l.get("name", "") for l in issue.get("labels", [])],
|
|
349
|
+
assignees=[a.get("login", "") for a in issue.get("assignees", [])]
|
|
350
|
+
)
|
|
351
|
+
for issue in data
|
|
352
|
+
]
|
|
353
|
+
except (subprocess.SubprocessError, json.JSONDecodeError):
|
|
354
|
+
pass
|
|
355
|
+
return []
|
|
356
|
+
|
|
357
|
+
def create_issue(
|
|
358
|
+
self,
|
|
359
|
+
title: str,
|
|
360
|
+
body: str,
|
|
361
|
+
labels: Optional[List[str]] = None,
|
|
362
|
+
assignees: Optional[List[str]] = None
|
|
363
|
+
) -> Optional[Issue]:
|
|
364
|
+
"""Create a new Issue"""
|
|
365
|
+
if not self._gh_available or not self._authenticated:
|
|
366
|
+
return None
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
cmd = ["issue", "create", "--title", title, "--body", body]
|
|
370
|
+
|
|
371
|
+
if labels:
|
|
372
|
+
for label in labels:
|
|
373
|
+
cmd.extend(["--label", label])
|
|
374
|
+
|
|
375
|
+
if assignees:
|
|
376
|
+
for assignee in assignees:
|
|
377
|
+
cmd.extend(["--assignee", assignee])
|
|
378
|
+
|
|
379
|
+
result = self._run_gh(*cmd)
|
|
380
|
+
|
|
381
|
+
if result.returncode == 0:
|
|
382
|
+
issue_url = result.stdout.strip()
|
|
383
|
+
# Extract issue number and return
|
|
384
|
+
parts = issue_url.rstrip('/').split('/')
|
|
385
|
+
issue_number = int(parts[-1]) if parts and parts[-1].isdigit() else 0
|
|
386
|
+
|
|
387
|
+
return Issue(
|
|
388
|
+
number=issue_number,
|
|
389
|
+
title=title,
|
|
390
|
+
url=issue_url,
|
|
391
|
+
state="open",
|
|
392
|
+
author="",
|
|
393
|
+
created_at=datetime.now().isoformat(),
|
|
394
|
+
body=body,
|
|
395
|
+
labels=labels or [],
|
|
396
|
+
assignees=assignees or []
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
except subprocess.SubprocessError:
|
|
400
|
+
pass
|
|
401
|
+
|
|
402
|
+
return None
|
|
403
|
+
|
|
404
|
+
def get_current_branch(self) -> Optional[str]:
|
|
405
|
+
"""Get current git branch"""
|
|
406
|
+
try:
|
|
407
|
+
result = subprocess.run(
|
|
408
|
+
["git", "branch", "--show-current"],
|
|
409
|
+
cwd=str(self.repo_path),
|
|
410
|
+
capture_output=True,
|
|
411
|
+
text=True
|
|
412
|
+
)
|
|
413
|
+
if result.returncode == 0:
|
|
414
|
+
return result.stdout.strip()
|
|
415
|
+
except subprocess.SubprocessError:
|
|
416
|
+
pass
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
def push_branch(self, set_upstream: bool = True) -> bool:
|
|
420
|
+
"""Push current branch to remote"""
|
|
421
|
+
try:
|
|
422
|
+
branch = self.get_current_branch()
|
|
423
|
+
if not branch:
|
|
424
|
+
return False
|
|
425
|
+
|
|
426
|
+
cmd = ["git", "push"]
|
|
427
|
+
if set_upstream:
|
|
428
|
+
cmd.extend(["-u", "origin", branch])
|
|
429
|
+
|
|
430
|
+
result = subprocess.run(
|
|
431
|
+
cmd,
|
|
432
|
+
cwd=str(self.repo_path),
|
|
433
|
+
capture_output=True,
|
|
434
|
+
text=True
|
|
435
|
+
)
|
|
436
|
+
return result.returncode == 0
|
|
437
|
+
except subprocess.SubprocessError:
|
|
438
|
+
return False
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# Global GitHub integration
|
|
442
|
+
_github_integration: Optional[GitHubIntegration] = None
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def get_github_integration() -> GitHubIntegration:
|
|
446
|
+
"""Get or create the global GitHub integration"""
|
|
447
|
+
global _github_integration
|
|
448
|
+
if _github_integration is None:
|
|
449
|
+
_github_integration = GitHubIntegration()
|
|
450
|
+
return _github_integration
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def format_pr_summary(pr: PullRequest) -> str:
|
|
454
|
+
"""Format a PR for display"""
|
|
455
|
+
lines = []
|
|
456
|
+
lines.append(f"\033[1m#{pr.number}: {pr.title}\033[0m")
|
|
457
|
+
lines.append(f" URL: {pr.url}")
|
|
458
|
+
lines.append(f" State: {pr.state}")
|
|
459
|
+
lines.append(f" Branch: {pr.branch} → {pr.base}")
|
|
460
|
+
lines.append(f" Author: {pr.author}")
|
|
461
|
+
if pr.labels:
|
|
462
|
+
lines.append(f" Labels: {', '.join(pr.labels)}")
|
|
463
|
+
return "\n".join(lines)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def format_issue_summary(issue: Issue) -> str:
|
|
467
|
+
"""Format an issue for display"""
|
|
468
|
+
lines = []
|
|
469
|
+
lines.append(f"\033[1m#{issue.number}: {issue.title}\033[0m")
|
|
470
|
+
lines.append(f" URL: {issue.url}")
|
|
471
|
+
lines.append(f" State: {issue.state}")
|
|
472
|
+
lines.append(f" Author: {issue.author}")
|
|
473
|
+
if issue.labels:
|
|
474
|
+
lines.append(f" Labels: {', '.join(issue.labels)}")
|
|
475
|
+
if issue.assignees:
|
|
476
|
+
lines.append(f" Assignees: {', '.join(issue.assignees)}")
|
|
477
|
+
return "\n".join(lines)
|