git-copilot-commit 0.4.4__tar.gz → 0.4.6__tar.gz
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.
- {git_copilot_commit-0.4.4 → git_copilot_commit-0.4.6}/PKG-INFO +8 -1
- {git_copilot_commit-0.4.4 → git_copilot_commit-0.4.6}/README.md +6 -0
- {git_copilot_commit-0.4.4 → git_copilot_commit-0.4.6}/pyproject.toml +1 -0
- {git_copilot_commit-0.4.4 → git_copilot_commit-0.4.6}/src/git_copilot_commit/cli.py +103 -8
- {git_copilot_commit-0.4.4 → git_copilot_commit-0.4.6}/src/git_copilot_commit/github_copilot.py +165 -33
- {git_copilot_commit-0.4.4 → git_copilot_commit-0.4.6}/uv.lock +11 -0
- {git_copilot_commit-0.4.4 → git_copilot_commit-0.4.6}/.github/workflows/ci.yml +0 -0
- {git_copilot_commit-0.4.4 → git_copilot_commit-0.4.6}/.gitignore +0 -0
- {git_copilot_commit-0.4.4 → git_copilot_commit-0.4.6}/.justfile +0 -0
- {git_copilot_commit-0.4.4 → git_copilot_commit-0.4.6}/.python-version +0 -0
- {git_copilot_commit-0.4.4 → git_copilot_commit-0.4.6}/LICENSE +0 -0
- {git_copilot_commit-0.4.4 → git_copilot_commit-0.4.6}/src/git_copilot_commit/__init__.py +0 -0
- {git_copilot_commit-0.4.4 → git_copilot_commit-0.4.6}/src/git_copilot_commit/git.py +0 -0
- {git_copilot_commit-0.4.4 → git_copilot_commit-0.4.6}/src/git_copilot_commit/prompts/commit-message-generator-prompt.md +0 -0
- {git_copilot_commit-0.4.4 → git_copilot_commit-0.4.6}/src/git_copilot_commit/py.typed +0 -0
- {git_copilot_commit-0.4.4 → git_copilot_commit-0.4.6}/src/git_copilot_commit/settings.py +0 -0
- {git_copilot_commit-0.4.4 → git_copilot_commit-0.4.6}/src/git_copilot_commit/version.py +0 -0
- {git_copilot_commit-0.4.4 → git_copilot_commit-0.4.6}/vhs/demo.vhs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-copilot-commit
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.6
|
|
4
4
|
Summary: Automatically generate and commit changes using copilot
|
|
5
5
|
Author-email: Dheepak Krishnamurthy <1813121+kdheepak@users.noreply.github.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -8,6 +8,7 @@ Requires-Python: >=3.12
|
|
|
8
8
|
Requires-Dist: httpx>=0.28.0
|
|
9
9
|
Requires-Dist: platformdirs>=4.0.0
|
|
10
10
|
Requires-Dist: rich>=14.0.0
|
|
11
|
+
Requires-Dist: truststore>=0.10.4
|
|
11
12
|
Requires-Dist: typer>=0.16.0
|
|
12
13
|
Description-Content-Type: text/markdown
|
|
13
14
|
|
|
@@ -77,6 +78,12 @@ pipx install git-copilot-commit
|
|
|
77
78
|
uvx git-copilot-commit authenticate
|
|
78
79
|
```
|
|
79
80
|
|
|
81
|
+
If your cached GitHub token is revoked or expires, refresh it with:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
uvx git-copilot-commit authenticate --force
|
|
85
|
+
```
|
|
86
|
+
|
|
80
87
|
2. Make changes in your repository.
|
|
81
88
|
|
|
82
89
|
3. Generate and commit:
|
|
@@ -64,6 +64,12 @@ pipx install git-copilot-commit
|
|
|
64
64
|
uvx git-copilot-commit authenticate
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
If your cached GitHub token is revoked or expires, refresh it with:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uvx git-copilot-commit authenticate --force
|
|
71
|
+
```
|
|
72
|
+
|
|
67
73
|
2. Make changes in your repository.
|
|
68
74
|
|
|
69
75
|
3. Generate and commit:
|
|
@@ -3,6 +3,7 @@ git-copilot-commit - AI-powered Git commit assistant
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
6
7
|
|
|
7
8
|
import rich
|
|
8
9
|
import typer
|
|
@@ -18,6 +19,27 @@ from . import github_copilot
|
|
|
18
19
|
console = Console()
|
|
19
20
|
app = typer.Typer(help=__doc__, add_completion=False)
|
|
20
21
|
|
|
22
|
+
CA_BUNDLE_HELP = (
|
|
23
|
+
"Path to a custom CA bundle (PEM). Use this to test internal / company CAs."
|
|
24
|
+
)
|
|
25
|
+
NATIVE_TLS_HELP = (
|
|
26
|
+
"Use the OS's native certificate store via 'truststore' for httpx instead of "
|
|
27
|
+
"the Python bundle. Ignored if --ca-bundle or --insecure is used."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
CaBundleOption = Annotated[
|
|
31
|
+
str | None,
|
|
32
|
+
typer.Option("--ca-bundle", metavar="PATH", help=CA_BUNDLE_HELP),
|
|
33
|
+
]
|
|
34
|
+
InsecureOption = Annotated[
|
|
35
|
+
bool,
|
|
36
|
+
typer.Option("--insecure", help="Disable SSL certificate verification."),
|
|
37
|
+
]
|
|
38
|
+
NativeTlsOption = Annotated[
|
|
39
|
+
bool,
|
|
40
|
+
typer.Option("--native-tls/--no-native-tls", help=NATIVE_TLS_HELP),
|
|
41
|
+
]
|
|
42
|
+
|
|
21
43
|
|
|
22
44
|
def version_callback(value: bool):
|
|
23
45
|
if value:
|
|
@@ -84,8 +106,24 @@ def load_system_prompt() -> str:
|
|
|
84
106
|
raise typer.Exit(1)
|
|
85
107
|
|
|
86
108
|
|
|
109
|
+
def build_http_client_config(
|
|
110
|
+
*,
|
|
111
|
+
ca_bundle: str | None,
|
|
112
|
+
insecure: bool,
|
|
113
|
+
native_tls: bool,
|
|
114
|
+
) -> github_copilot.HttpClientConfig:
|
|
115
|
+
return github_copilot.HttpClientConfig(
|
|
116
|
+
native_tls=native_tls,
|
|
117
|
+
insecure=insecure,
|
|
118
|
+
ca_bundle=ca_bundle,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
87
122
|
def generate_commit_message(
|
|
88
|
-
repo: GitRepository,
|
|
123
|
+
repo: GitRepository,
|
|
124
|
+
model: str | None = None,
|
|
125
|
+
context: str = "",
|
|
126
|
+
http_client_config: github_copilot.HttpClientConfig | None = None,
|
|
89
127
|
) -> str:
|
|
90
128
|
"""Generate a conventional commit message using Copilot API."""
|
|
91
129
|
|
|
@@ -127,6 +165,7 @@ def generate_commit_message(
|
|
|
127
165
|
{prompt}
|
|
128
166
|
""",
|
|
129
167
|
model=model,
|
|
168
|
+
http_client_config=http_client_config,
|
|
130
169
|
)
|
|
131
170
|
|
|
132
171
|
|
|
@@ -151,6 +190,32 @@ def commit_with_retry_no_verify(
|
|
|
151
190
|
raise typer.Exit(1)
|
|
152
191
|
|
|
153
192
|
|
|
193
|
+
@app.command("authenticate")
|
|
194
|
+
@app.command("login", hidden=True)
|
|
195
|
+
def authenticate(
|
|
196
|
+
force: bool = typer.Option(
|
|
197
|
+
False, "--force", help="Replace cached GitHub Copilot credentials"
|
|
198
|
+
),
|
|
199
|
+
ca_bundle: CaBundleOption = None,
|
|
200
|
+
insecure: InsecureOption = False,
|
|
201
|
+
native_tls: NativeTlsOption = False,
|
|
202
|
+
):
|
|
203
|
+
"""Authenticate with GitHub Copilot and cache credentials locally."""
|
|
204
|
+
http_client_config = build_http_client_config(
|
|
205
|
+
ca_bundle=ca_bundle,
|
|
206
|
+
insecure=insecure,
|
|
207
|
+
native_tls=native_tls,
|
|
208
|
+
)
|
|
209
|
+
try:
|
|
210
|
+
github_copilot.login(
|
|
211
|
+
force=force,
|
|
212
|
+
http_client_config=http_client_config,
|
|
213
|
+
)
|
|
214
|
+
except github_copilot.CopilotError as exc:
|
|
215
|
+
console.print(f"[red]Authentication failed: {exc}[/red]")
|
|
216
|
+
raise typer.Exit(1)
|
|
217
|
+
|
|
218
|
+
|
|
154
219
|
@app.command()
|
|
155
220
|
def commit(
|
|
156
221
|
all_files: bool = typer.Option(
|
|
@@ -168,6 +233,9 @@ def commit(
|
|
|
168
233
|
"-c",
|
|
169
234
|
help="Optional user-provided context to guide commit message",
|
|
170
235
|
),
|
|
236
|
+
ca_bundle: CaBundleOption = None,
|
|
237
|
+
insecure: InsecureOption = False,
|
|
238
|
+
native_tls: NativeTlsOption = False,
|
|
171
239
|
):
|
|
172
240
|
"""
|
|
173
241
|
Generate commit message based on changes in the current git repository and commit them.
|
|
@@ -180,11 +248,24 @@ def commit(
|
|
|
180
248
|
|
|
181
249
|
try:
|
|
182
250
|
existing_credentials = github_copilot.load_credentials()
|
|
183
|
-
except
|
|
251
|
+
except github_copilot.CopilotError:
|
|
184
252
|
existing_credentials = None
|
|
185
253
|
|
|
254
|
+
http_client_config = build_http_client_config(
|
|
255
|
+
ca_bundle=ca_bundle,
|
|
256
|
+
insecure=insecure,
|
|
257
|
+
native_tls=native_tls,
|
|
258
|
+
)
|
|
259
|
+
|
|
186
260
|
if existing_credentials is None:
|
|
187
|
-
|
|
261
|
+
try:
|
|
262
|
+
github_copilot.login(
|
|
263
|
+
force=True,
|
|
264
|
+
http_client_config=http_client_config,
|
|
265
|
+
)
|
|
266
|
+
except github_copilot.CopilotError as exc:
|
|
267
|
+
console.print(f"[red]Authentication failed: {exc}[/red]")
|
|
268
|
+
raise typer.Exit(1)
|
|
188
269
|
|
|
189
270
|
# Load settings and use default model if none provided
|
|
190
271
|
settings = Settings()
|
|
@@ -228,11 +309,25 @@ def commit(
|
|
|
228
309
|
Panel(context.strip(), title="User Context", border_style="magenta")
|
|
229
310
|
)
|
|
230
311
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
312
|
+
try:
|
|
313
|
+
github_copilot.ensure_auth_ready(
|
|
314
|
+
model=model,
|
|
315
|
+
http_client_config=http_client_config,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Generate or use provided commit message
|
|
319
|
+
with console.status(
|
|
320
|
+
"[yellow]Generating commit message based on [bold]`git diff --staged`[/] ...[/yellow]"
|
|
321
|
+
):
|
|
322
|
+
commit_message = generate_commit_message(
|
|
323
|
+
repo,
|
|
324
|
+
model,
|
|
325
|
+
context=context,
|
|
326
|
+
http_client_config=http_client_config,
|
|
327
|
+
)
|
|
328
|
+
except github_copilot.CopilotError as exc:
|
|
329
|
+
console.print(f"[red]Could not generate a commit message: {exc}[/red]")
|
|
330
|
+
raise typer.Exit(1)
|
|
236
331
|
|
|
237
332
|
console.print("[yellow]Generated commit message.[/yellow]")
|
|
238
333
|
|
{git_copilot_commit-0.4.4 → git_copilot_commit-0.4.6}/src/git_copilot_commit/github_copilot.py
RENAMED
|
@@ -10,15 +10,15 @@ import uuid
|
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from datetime import datetime
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import Any
|
|
13
|
+
from typing import Any, Callable, TypeVar
|
|
14
14
|
|
|
15
15
|
import httpx
|
|
16
|
-
import typer
|
|
17
16
|
from rich.console import Console
|
|
18
17
|
from rich.panel import Panel
|
|
19
18
|
from rich.table import Table
|
|
20
19
|
|
|
21
20
|
APP_NAME = "github-copilot-commit"
|
|
21
|
+
CLI_AUTH_COMMAND = "git-copilot-commit authenticate"
|
|
22
22
|
DEFAULT_GITHUB_DOMAIN = "github.com"
|
|
23
23
|
USER_AGENT = "GitHubCopilotChat/0.35.0"
|
|
24
24
|
EDITOR_VERSION = "vscode/1.107.0"
|
|
@@ -41,20 +41,25 @@ DEFAULT_MODEL_PREFERENCES = (
|
|
|
41
41
|
"gpt-4o",
|
|
42
42
|
)
|
|
43
43
|
|
|
44
|
-
app = typer.Typer(
|
|
45
|
-
add_completion=False,
|
|
46
|
-
no_args_is_help=True,
|
|
47
|
-
help="General-purpose GitHub Copilot CLI.",
|
|
48
|
-
)
|
|
49
|
-
|
|
50
44
|
console = Console()
|
|
51
|
-
|
|
45
|
+
T = TypeVar("T")
|
|
52
46
|
|
|
53
47
|
|
|
54
48
|
class CopilotError(RuntimeError):
|
|
55
49
|
pass
|
|
56
50
|
|
|
57
51
|
|
|
52
|
+
class CopilotHttpError(CopilotError):
|
|
53
|
+
def __init__(
|
|
54
|
+
self, status_code: int, reason_phrase: str, detail: str | None = None
|
|
55
|
+
) -> None:
|
|
56
|
+
self.status_code = status_code
|
|
57
|
+
self.reason_phrase = reason_phrase
|
|
58
|
+
self.detail = detail
|
|
59
|
+
suffix = f": {detail}" if detail else ""
|
|
60
|
+
super().__init__(f"{status_code} {reason_phrase}{suffix}")
|
|
61
|
+
|
|
62
|
+
|
|
58
63
|
@dataclass(slots=True)
|
|
59
64
|
class DeviceCodeResponse:
|
|
60
65
|
device_code: str
|
|
@@ -151,6 +156,25 @@ class CopilotModel:
|
|
|
151
156
|
)
|
|
152
157
|
|
|
153
158
|
|
|
159
|
+
@dataclass(frozen=True, slots=True)
|
|
160
|
+
class HttpClientConfig:
|
|
161
|
+
native_tls: bool = False
|
|
162
|
+
insecure: bool = False
|
|
163
|
+
ca_bundle: str | None = None
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def use_native_tls(self) -> bool:
|
|
167
|
+
return self.native_tls and not self.insecure and self.ca_bundle is None
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def verify(self) -> bool | str:
|
|
171
|
+
if self.insecure:
|
|
172
|
+
return False
|
|
173
|
+
if self.ca_bundle:
|
|
174
|
+
return self.ca_bundle
|
|
175
|
+
return True
|
|
176
|
+
|
|
177
|
+
|
|
154
178
|
@dataclass(slots=True)
|
|
155
179
|
class GitHubViewer:
|
|
156
180
|
login: str
|
|
@@ -259,8 +283,34 @@ def get_github_copilot_base_url(
|
|
|
259
283
|
return "https://api.individual.githubcopilot.com"
|
|
260
284
|
|
|
261
285
|
|
|
262
|
-
|
|
286
|
+
_NATIVE_TLS_ENABLED = False
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _maybe_enable_native_tls(native_tls: bool) -> None:
|
|
290
|
+
"""
|
|
291
|
+
Globally switch httpx/requests to use the OS certificate store via truststore.
|
|
292
|
+
Safe to call multiple times; it no-ops after the first.
|
|
293
|
+
"""
|
|
294
|
+
global _NATIVE_TLS_ENABLED
|
|
295
|
+
if not native_tls or _NATIVE_TLS_ENABLED:
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
import truststore
|
|
300
|
+
|
|
301
|
+
truststore.inject_into_ssl()
|
|
302
|
+
except Exception as _:
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
_NATIVE_TLS_ENABLED = True
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def make_http_client(http_client_config: HttpClientConfig | None = None) -> httpx.Client:
|
|
309
|
+
config = http_client_config or HttpClientConfig()
|
|
310
|
+
_maybe_enable_native_tls(config.use_native_tls)
|
|
311
|
+
|
|
263
312
|
return httpx.Client(
|
|
313
|
+
verify=config.verify,
|
|
264
314
|
follow_redirects=True,
|
|
265
315
|
timeout=httpx.Timeout(30.0, connect=10.0),
|
|
266
316
|
)
|
|
@@ -290,8 +340,7 @@ def request_json(
|
|
|
290
340
|
detail = response.text.strip()
|
|
291
341
|
if len(detail) > 400:
|
|
292
342
|
detail = f"{detail[:397]}..."
|
|
293
|
-
|
|
294
|
-
raise CopilotError(f"{response.status_code} {response.reason_phrase}{suffix}")
|
|
343
|
+
raise CopilotHttpError(response.status_code, response.reason_phrase, detail)
|
|
295
344
|
|
|
296
345
|
content_type = response.headers.get("content-type", "")
|
|
297
346
|
if "application/json" not in content_type:
|
|
@@ -364,10 +413,9 @@ def iter_sse_events(response: httpx.Response, url: str):
|
|
|
364
413
|
yield payload
|
|
365
414
|
|
|
366
415
|
|
|
367
|
-
def
|
|
368
|
-
path = credentials_path()
|
|
416
|
+
def read_json_object(path: Path) -> dict[str, Any] | None:
|
|
369
417
|
if not path.exists():
|
|
370
|
-
return
|
|
418
|
+
return None
|
|
371
419
|
|
|
372
420
|
try:
|
|
373
421
|
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
@@ -379,11 +427,23 @@ def load_credentials() -> CopilotCredentials | None:
|
|
|
379
427
|
if not isinstance(raw, dict):
|
|
380
428
|
raise CopilotError(f"Cached credentials in {path} are not a JSON object.")
|
|
381
429
|
|
|
430
|
+
return raw
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def load_stored_credentials_from_path(path: Path) -> CopilotCredentials | None:
|
|
434
|
+
raw = read_json_object(path)
|
|
435
|
+
if raw is None:
|
|
436
|
+
return None
|
|
437
|
+
|
|
382
438
|
return CopilotCredentials.from_dict(raw)
|
|
383
439
|
|
|
384
440
|
|
|
441
|
+
def load_credentials() -> CopilotCredentials | None:
|
|
442
|
+
return load_stored_credentials_from_path(credentials_path())
|
|
443
|
+
|
|
444
|
+
|
|
385
445
|
def save_credentials(credentials: CopilotCredentials) -> Path:
|
|
386
|
-
path = credentials_path()
|
|
446
|
+
path = credentials_path().expanduser()
|
|
387
447
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
388
448
|
payload = json.dumps(credentials.to_dict(), indent=2, sort_keys=True)
|
|
389
449
|
path.write_text(f"{payload}\n", encoding="utf-8")
|
|
@@ -573,7 +633,7 @@ def ensure_fresh_credentials(client: httpx.Client) -> CopilotCredentials:
|
|
|
573
633
|
credentials = load_credentials()
|
|
574
634
|
if credentials is None:
|
|
575
635
|
raise CopilotError(
|
|
576
|
-
f"No cached Copilot credentials found. Run `{
|
|
636
|
+
f"No cached Copilot credentials found. Run `{CLI_AUTH_COMMAND}` first."
|
|
577
637
|
)
|
|
578
638
|
|
|
579
639
|
if not credentials.is_expired():
|
|
@@ -588,6 +648,23 @@ def ensure_fresh_credentials(client: httpx.Client) -> CopilotCredentials:
|
|
|
588
648
|
return refreshed
|
|
589
649
|
|
|
590
650
|
|
|
651
|
+
def should_reauthenticate(exc: CopilotError) -> bool:
|
|
652
|
+
if isinstance(exc, CopilotHttpError):
|
|
653
|
+
return exc.status_code == 401
|
|
654
|
+
|
|
655
|
+
message = str(exc)
|
|
656
|
+
retryable_prefixes = (
|
|
657
|
+
"No cached Copilot credentials found.",
|
|
658
|
+
"Cached GitHub access token is missing or invalid.",
|
|
659
|
+
"Cached Copilot token is missing or invalid.",
|
|
660
|
+
"Cached Copilot expiration timestamp is missing or invalid.",
|
|
661
|
+
"Cached enterprise domain is invalid.",
|
|
662
|
+
"Unable to read cached credentials from ",
|
|
663
|
+
"Cached credentials in ",
|
|
664
|
+
)
|
|
665
|
+
return any(message.startswith(prefix) for prefix in retryable_prefixes)
|
|
666
|
+
|
|
667
|
+
|
|
591
668
|
def copilot_request_headers(
|
|
592
669
|
access_token: str,
|
|
593
670
|
*,
|
|
@@ -886,9 +963,8 @@ def responses_completion(
|
|
|
886
963
|
detail = response.read().decode("utf-8", errors="replace").strip()
|
|
887
964
|
if len(detail) > 400:
|
|
888
965
|
detail = f"{detail[:397]}..."
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
f"{response.status_code} {response.reason_phrase}{suffix}"
|
|
966
|
+
raise CopilotHttpError(
|
|
967
|
+
response.status_code, response.reason_phrase, detail
|
|
892
968
|
)
|
|
893
969
|
|
|
894
970
|
content_type = response.headers.get("content-type", "")
|
|
@@ -1101,20 +1177,31 @@ def print_login_summary(
|
|
|
1101
1177
|
console.print(Panel.fit(table, title="Login Summary"))
|
|
1102
1178
|
|
|
1103
1179
|
|
|
1104
|
-
def login(
|
|
1180
|
+
def login(
|
|
1181
|
+
enterprise_domain: str | None = None,
|
|
1182
|
+
force: bool = False,
|
|
1183
|
+
*,
|
|
1184
|
+
http_client_config: HttpClientConfig | None = None,
|
|
1185
|
+
) -> None:
|
|
1105
1186
|
"""Authenticate with GitHub and cache Copilot credentials locally."""
|
|
1106
1187
|
normalized_domain = normalize_domain(enterprise_domain)
|
|
1107
1188
|
if enterprise_domain and not normalized_domain:
|
|
1108
1189
|
raise CopilotError("Invalid GitHub Enterprise hostname.")
|
|
1109
1190
|
|
|
1110
|
-
existing =
|
|
1191
|
+
existing: CopilotCredentials | None = None
|
|
1192
|
+
try:
|
|
1193
|
+
existing = load_credentials()
|
|
1194
|
+
except CopilotError:
|
|
1195
|
+
if not force:
|
|
1196
|
+
raise
|
|
1197
|
+
|
|
1111
1198
|
if existing and not force:
|
|
1112
1199
|
raise CopilotError(
|
|
1113
1200
|
f"Cached credentials already exist at {credentials_path()}. Re-run with --force to replace them."
|
|
1114
1201
|
)
|
|
1115
1202
|
|
|
1116
1203
|
domain = normalized_domain or DEFAULT_GITHUB_DOMAIN
|
|
1117
|
-
with make_http_client() as client:
|
|
1204
|
+
with make_http_client(http_client_config) as client:
|
|
1118
1205
|
device = start_device_flow(client, domain)
|
|
1119
1206
|
|
|
1120
1207
|
console.print(
|
|
@@ -1162,16 +1249,61 @@ def login(enterprise_domain: str | None = None, force: bool = False) -> None:
|
|
|
1162
1249
|
console.print(f"[yellow]Warning:[/yellow] {warning}")
|
|
1163
1250
|
|
|
1164
1251
|
|
|
1165
|
-
def
|
|
1166
|
-
|
|
1167
|
-
|
|
1252
|
+
def _ask_once(client: httpx.Client, prompt: str, model: str | None = None) -> str:
|
|
1253
|
+
credentials = ensure_fresh_credentials(client)
|
|
1254
|
+
all_models = list_models(client, credentials)
|
|
1255
|
+
|
|
1256
|
+
selected_model = pick_model(all_models, model)
|
|
1257
|
+
return complete_text_prompt(
|
|
1258
|
+
client,
|
|
1259
|
+
credentials,
|
|
1260
|
+
model=selected_model,
|
|
1261
|
+
prompt=prompt,
|
|
1262
|
+
)
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
def _with_reauthentication(
|
|
1266
|
+
action: Callable[[httpx.Client], T],
|
|
1267
|
+
*,
|
|
1268
|
+
http_client_config: HttpClientConfig | None = None,
|
|
1269
|
+
) -> T:
|
|
1270
|
+
try:
|
|
1271
|
+
with make_http_client(http_client_config) as client:
|
|
1272
|
+
return action(client)
|
|
1273
|
+
except CopilotError as exc:
|
|
1274
|
+
if not should_reauthenticate(exc):
|
|
1275
|
+
raise
|
|
1276
|
+
|
|
1277
|
+
console.print(
|
|
1278
|
+
"[yellow]Cached GitHub Copilot credentials are missing or no longer valid. Starting authentication...[/yellow]"
|
|
1279
|
+
)
|
|
1280
|
+
login(force=True, http_client_config=http_client_config)
|
|
1281
|
+
|
|
1282
|
+
with make_http_client(http_client_config) as client:
|
|
1283
|
+
return action(client)
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
def ensure_auth_ready(
|
|
1287
|
+
model: str | None = None,
|
|
1288
|
+
*,
|
|
1289
|
+
http_client_config: HttpClientConfig | None = None,
|
|
1290
|
+
) -> None:
|
|
1291
|
+
def validate(client: httpx.Client) -> None:
|
|
1168
1292
|
credentials = ensure_fresh_credentials(client)
|
|
1169
1293
|
all_models = list_models(client, credentials)
|
|
1294
|
+
pick_model(all_models, model)
|
|
1170
1295
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1296
|
+
_with_reauthentication(validate, http_client_config=http_client_config)
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
def ask(
|
|
1300
|
+
prompt: str,
|
|
1301
|
+
model: str | None = None,
|
|
1302
|
+
*,
|
|
1303
|
+
http_client_config: HttpClientConfig | None = None,
|
|
1304
|
+
) -> str:
|
|
1305
|
+
"""Send a prompt to GitHub Copilot and print the reply."""
|
|
1306
|
+
return _with_reauthentication(
|
|
1307
|
+
lambda client: _ask_once(client, prompt, model),
|
|
1308
|
+
http_client_config=http_client_config,
|
|
1309
|
+
)
|
|
@@ -53,6 +53,7 @@ dependencies = [
|
|
|
53
53
|
{ name = "httpx" },
|
|
54
54
|
{ name = "platformdirs" },
|
|
55
55
|
{ name = "rich" },
|
|
56
|
+
{ name = "truststore" },
|
|
56
57
|
{ name = "typer" },
|
|
57
58
|
]
|
|
58
59
|
|
|
@@ -61,6 +62,7 @@ requires-dist = [
|
|
|
61
62
|
{ name = "httpx", specifier = ">=0.28.0" },
|
|
62
63
|
{ name = "platformdirs", specifier = ">=4.0.0" },
|
|
63
64
|
{ name = "rich", specifier = ">=14.0.0" },
|
|
65
|
+
{ name = "truststore", specifier = ">=0.10.4" },
|
|
64
66
|
{ name = "typer", specifier = ">=0.16.0" },
|
|
65
67
|
]
|
|
66
68
|
|
|
@@ -180,6 +182,15 @@ wheels = [
|
|
|
180
182
|
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
|
181
183
|
]
|
|
182
184
|
|
|
185
|
+
[[package]]
|
|
186
|
+
name = "truststore"
|
|
187
|
+
version = "0.10.4"
|
|
188
|
+
source = { registry = "https://pypi.org/simple" }
|
|
189
|
+
sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" }
|
|
190
|
+
wheels = [
|
|
191
|
+
{ url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" },
|
|
192
|
+
]
|
|
193
|
+
|
|
183
194
|
[[package]]
|
|
184
195
|
name = "typer"
|
|
185
196
|
version = "0.16.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|