aicodinggym-cli 0.2.0__tar.gz → 0.4.0__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.
- {aicodinggym_cli-0.2.0 → aicodinggym_cli-0.4.0}/PKG-INFO +39 -2
- {aicodinggym_cli-0.2.0 → aicodinggym_cli-0.4.0}/README.md +38 -1
- {aicodinggym_cli-0.2.0 → aicodinggym_cli-0.4.0}/__init__.py +1 -1
- {aicodinggym_cli-0.2.0 → aicodinggym_cli-0.4.0}/aicodinggym_cli.egg-info/PKG-INFO +39 -2
- {aicodinggym_cli-0.2.0 → aicodinggym_cli-0.4.0}/api.py +17 -1
- {aicodinggym_cli-0.2.0 → aicodinggym_cli-0.4.0}/cli.py +203 -5
- {aicodinggym_cli-0.2.0 → aicodinggym_cli-0.4.0}/config.py +19 -2
- aicodinggym_cli-0.4.0/git_ops.py +309 -0
- {aicodinggym_cli-0.2.0 → aicodinggym_cli-0.4.0}/pyproject.toml +1 -1
- aicodinggym_cli-0.2.0/git_ops.py +0 -162
- {aicodinggym_cli-0.2.0 → aicodinggym_cli-0.4.0}/aicodinggym_cli.egg-info/SOURCES.txt +0 -0
- {aicodinggym_cli-0.2.0 → aicodinggym_cli-0.4.0}/aicodinggym_cli.egg-info/dependency_links.txt +0 -0
- {aicodinggym_cli-0.2.0 → aicodinggym_cli-0.4.0}/aicodinggym_cli.egg-info/entry_points.txt +0 -0
- {aicodinggym_cli-0.2.0 → aicodinggym_cli-0.4.0}/aicodinggym_cli.egg-info/requires.txt +0 -0
- {aicodinggym_cli-0.2.0 → aicodinggym_cli-0.4.0}/aicodinggym_cli.egg-info/top_level.txt +0 -0
- {aicodinggym_cli-0.2.0 → aicodinggym_cli-0.4.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aicodinggym-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: CLI tool for AI Coding Gym platform
|
|
5
5
|
Author-email: AICodingGym Team <datasmithlab@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -20,7 +20,7 @@ Requires-Dist: requests>=2.31.0
|
|
|
20
20
|
# aicodinggym-cli
|
|
21
21
|
|
|
22
22
|
CLI tool for the [AI Coding Gym](https://aicodinggym.com) platform.
|
|
23
|
-
Supports
|
|
23
|
+
Supports three benchmarks: **SWE-bench** (code bug fixes), **MLE-bench** (ML competitions), and **Code Review** challenges.
|
|
24
24
|
|
|
25
25
|
**Install:** `pip install aicodinggym-cli`
|
|
26
26
|
**Entry point:** `aicodinggym`
|
|
@@ -43,6 +43,11 @@ aicodinggym swe submit django__django-10097
|
|
|
43
43
|
aicodinggym mle download spaceship-titanic
|
|
44
44
|
# ... train model, generate predictions ...
|
|
45
45
|
aicodinggym mle submit spaceship-titanic -F predictions.csv
|
|
46
|
+
|
|
47
|
+
# 4. Code Review: fetch, review, submit
|
|
48
|
+
aicodinggym cr fetch keycloak-0008
|
|
49
|
+
# ... read diff.patch, write your review in review.md ...
|
|
50
|
+
aicodinggym cr submit keycloak-0008 -f review.md
|
|
46
51
|
```
|
|
47
52
|
|
|
48
53
|
---
|
|
@@ -154,6 +159,38 @@ aicodinggym mle submit COMPETITION_ID -F FILE [--user-id ID] [--message MSG]
|
|
|
154
159
|
|
|
155
160
|
---
|
|
156
161
|
|
|
162
|
+
### `aicodinggym cr` — Code Review Commands
|
|
163
|
+
|
|
164
|
+
#### `aicodinggym cr fetch PROBLEM_ID`
|
|
165
|
+
|
|
166
|
+
Download the PR diff and create a `review.md` template.
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
aicodinggym cr fetch PROBLEM_ID [--user-id ID] [--workspace-dir DIR]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Creates in `<workspace>/<problem_id>/`:
|
|
173
|
+
- `diff.patch` — the full diff between base and head branches
|
|
174
|
+
- `review.md` — template to fill in your review (only created if not already present)
|
|
175
|
+
|
|
176
|
+
#### `aicodinggym cr submit PROBLEM_ID`
|
|
177
|
+
|
|
178
|
+
Submit your code review.
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
aicodinggym cr submit PROBLEM_ID -f review.md [--user-id ID]
|
|
182
|
+
aicodinggym cr submit PROBLEM_ID -m "Inline review text"
|
|
183
|
+
echo "My review" | aicodinggym cr submit PROBLEM_ID
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
| Option | Description |
|
|
187
|
+
|---|---|
|
|
188
|
+
| `-f, --file` | Path to a file containing your review (e.g. `review.md`) |
|
|
189
|
+
| `-m, --message` | Inline review text |
|
|
190
|
+
| stdin | Pipe review text from stdin |
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
157
194
|
## File Structure
|
|
158
195
|
|
|
159
196
|
```
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# aicodinggym-cli
|
|
2
2
|
|
|
3
3
|
CLI tool for the [AI Coding Gym](https://aicodinggym.com) platform.
|
|
4
|
-
Supports
|
|
4
|
+
Supports three benchmarks: **SWE-bench** (code bug fixes), **MLE-bench** (ML competitions), and **Code Review** challenges.
|
|
5
5
|
|
|
6
6
|
**Install:** `pip install aicodinggym-cli`
|
|
7
7
|
**Entry point:** `aicodinggym`
|
|
@@ -24,6 +24,11 @@ aicodinggym swe submit django__django-10097
|
|
|
24
24
|
aicodinggym mle download spaceship-titanic
|
|
25
25
|
# ... train model, generate predictions ...
|
|
26
26
|
aicodinggym mle submit spaceship-titanic -F predictions.csv
|
|
27
|
+
|
|
28
|
+
# 4. Code Review: fetch, review, submit
|
|
29
|
+
aicodinggym cr fetch keycloak-0008
|
|
30
|
+
# ... read diff.patch, write your review in review.md ...
|
|
31
|
+
aicodinggym cr submit keycloak-0008 -f review.md
|
|
27
32
|
```
|
|
28
33
|
|
|
29
34
|
---
|
|
@@ -135,6 +140,38 @@ aicodinggym mle submit COMPETITION_ID -F FILE [--user-id ID] [--message MSG]
|
|
|
135
140
|
|
|
136
141
|
---
|
|
137
142
|
|
|
143
|
+
### `aicodinggym cr` — Code Review Commands
|
|
144
|
+
|
|
145
|
+
#### `aicodinggym cr fetch PROBLEM_ID`
|
|
146
|
+
|
|
147
|
+
Download the PR diff and create a `review.md` template.
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
aicodinggym cr fetch PROBLEM_ID [--user-id ID] [--workspace-dir DIR]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Creates in `<workspace>/<problem_id>/`:
|
|
154
|
+
- `diff.patch` — the full diff between base and head branches
|
|
155
|
+
- `review.md` — template to fill in your review (only created if not already present)
|
|
156
|
+
|
|
157
|
+
#### `aicodinggym cr submit PROBLEM_ID`
|
|
158
|
+
|
|
159
|
+
Submit your code review.
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
aicodinggym cr submit PROBLEM_ID -f review.md [--user-id ID]
|
|
163
|
+
aicodinggym cr submit PROBLEM_ID -m "Inline review text"
|
|
164
|
+
echo "My review" | aicodinggym cr submit PROBLEM_ID
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
| Option | Description |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `-f, --file` | Path to a file containing your review (e.g. `review.md`) |
|
|
170
|
+
| `-m, --message` | Inline review text |
|
|
171
|
+
| stdin | Pipe review text from stdin |
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
138
175
|
## File Structure
|
|
139
176
|
|
|
140
177
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aicodinggym-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: CLI tool for AI Coding Gym platform
|
|
5
5
|
Author-email: AICodingGym Team <datasmithlab@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -20,7 +20,7 @@ Requires-Dist: requests>=2.31.0
|
|
|
20
20
|
# aicodinggym-cli
|
|
21
21
|
|
|
22
22
|
CLI tool for the [AI Coding Gym](https://aicodinggym.com) platform.
|
|
23
|
-
Supports
|
|
23
|
+
Supports three benchmarks: **SWE-bench** (code bug fixes), **MLE-bench** (ML competitions), and **Code Review** challenges.
|
|
24
24
|
|
|
25
25
|
**Install:** `pip install aicodinggym-cli`
|
|
26
26
|
**Entry point:** `aicodinggym`
|
|
@@ -43,6 +43,11 @@ aicodinggym swe submit django__django-10097
|
|
|
43
43
|
aicodinggym mle download spaceship-titanic
|
|
44
44
|
# ... train model, generate predictions ...
|
|
45
45
|
aicodinggym mle submit spaceship-titanic -F predictions.csv
|
|
46
|
+
|
|
47
|
+
# 4. Code Review: fetch, review, submit
|
|
48
|
+
aicodinggym cr fetch keycloak-0008
|
|
49
|
+
# ... read diff.patch, write your review in review.md ...
|
|
50
|
+
aicodinggym cr submit keycloak-0008 -f review.md
|
|
46
51
|
```
|
|
47
52
|
|
|
48
53
|
---
|
|
@@ -154,6 +159,38 @@ aicodinggym mle submit COMPETITION_ID -F FILE [--user-id ID] [--message MSG]
|
|
|
154
159
|
|
|
155
160
|
---
|
|
156
161
|
|
|
162
|
+
### `aicodinggym cr` — Code Review Commands
|
|
163
|
+
|
|
164
|
+
#### `aicodinggym cr fetch PROBLEM_ID`
|
|
165
|
+
|
|
166
|
+
Download the PR diff and create a `review.md` template.
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
aicodinggym cr fetch PROBLEM_ID [--user-id ID] [--workspace-dir DIR]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Creates in `<workspace>/<problem_id>/`:
|
|
173
|
+
- `diff.patch` — the full diff between base and head branches
|
|
174
|
+
- `review.md` — template to fill in your review (only created if not already present)
|
|
175
|
+
|
|
176
|
+
#### `aicodinggym cr submit PROBLEM_ID`
|
|
177
|
+
|
|
178
|
+
Submit your code review.
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
aicodinggym cr submit PROBLEM_ID -f review.md [--user-id ID]
|
|
182
|
+
aicodinggym cr submit PROBLEM_ID -m "Inline review text"
|
|
183
|
+
echo "My review" | aicodinggym cr submit PROBLEM_ID
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
| Option | Description |
|
|
187
|
+
|---|---|
|
|
188
|
+
| `-f, --file` | Path to a file containing your review (e.g. `review.md`) |
|
|
189
|
+
| `-m, --message` | Inline review text |
|
|
190
|
+
| stdin | Pipe review text from stdin |
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
157
194
|
## File Structure
|
|
158
195
|
|
|
159
196
|
```
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"""HTTP API client for the AI Coding Gym backend at aicodinggym.com."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
4
|
+
|
|
3
5
|
import requests
|
|
4
6
|
|
|
5
|
-
API_BASE = "https://aicodinggym.com/api"
|
|
7
|
+
API_BASE = os.environ.get("AICODINGGYM_API_BASE", "https://aicodinggym.com/api")
|
|
6
8
|
TIMEOUT = 30
|
|
7
9
|
|
|
8
10
|
|
|
@@ -84,6 +86,20 @@ def submit_notification(problem_id: str, user_id: str, commit_hash: str,
|
|
|
84
86
|
})
|
|
85
87
|
|
|
86
88
|
|
|
89
|
+
def fetch_pr(user_id: str, problem_id: str) -> dict:
|
|
90
|
+
"""Fetch CR problem info. Returns {'base_branch': ..., 'head_branch': ..., 'repo_url': ...}."""
|
|
91
|
+
return _post("code-review-fetch", {"user_id": user_id, "problem_id": problem_id})
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cr_submit_review(user_id: str, problem_id: str, review: str) -> dict:
|
|
95
|
+
"""Submit a code review."""
|
|
96
|
+
return _post("code-review-submit", {
|
|
97
|
+
"user_id": user_id,
|
|
98
|
+
"problem_id": problem_id,
|
|
99
|
+
"review": review,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
|
|
87
103
|
def mlebench_download_info(user_id: str, competition_id: str, dest_path: str) -> None:
|
|
88
104
|
"""Download dataset for an MLE-bench competition directly to dest_path."""
|
|
89
105
|
resp = _get(f"competitions/{competition_id}/download", stream=True)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""AI Coding Gym CLI - main entry point.
|
|
2
2
|
|
|
3
3
|
A command-line tool for the AI Coding Gym platform (https://aicodinggym.com).
|
|
4
|
-
Supports SWE-bench
|
|
4
|
+
Supports SWE-bench, MLE-bench, and Code Review challenges.
|
|
5
5
|
|
|
6
6
|
SETUP (required before any other command):
|
|
7
7
|
aicodinggym configure --user-id YOUR_USER_ID
|
|
@@ -15,6 +15,11 @@ MLE-BENCH WORKFLOW:
|
|
|
15
15
|
aicodinggym mle download spaceship-titanic
|
|
16
16
|
# ... train model, generate predictions ...
|
|
17
17
|
aicodinggym mle submit spaceship-titanic -F submission.csv
|
|
18
|
+
|
|
19
|
+
CODE REVIEW WORKFLOW:
|
|
20
|
+
aicodinggym cr fetch sentry-0001
|
|
21
|
+
# ... review the diff and write your review ...
|
|
22
|
+
aicodinggym cr submit sentry-0001 -f review.md
|
|
18
23
|
"""
|
|
19
24
|
|
|
20
25
|
import os
|
|
@@ -31,6 +36,8 @@ from . import __version__
|
|
|
31
36
|
from .api import (
|
|
32
37
|
APIError,
|
|
33
38
|
configure as api_configure,
|
|
39
|
+
cr_submit_review,
|
|
40
|
+
fetch_pr as api_fetch_pr,
|
|
34
41
|
fetch_problem as api_fetch_problem,
|
|
35
42
|
mlebench_download_file,
|
|
36
43
|
mlebench_download_info,
|
|
@@ -47,11 +54,19 @@ from .git_ops import (
|
|
|
47
54
|
add_commit_push,
|
|
48
55
|
check_tool_installed,
|
|
49
56
|
clone_repo,
|
|
57
|
+
clone_repo_cr,
|
|
50
58
|
generate_ssh_key_pair,
|
|
51
59
|
reset_to_setup_commit,
|
|
60
|
+
run_git_command,
|
|
52
61
|
)
|
|
53
62
|
|
|
54
63
|
|
|
64
|
+
def _hyperlink(url: str, text: str | None = None) -> str:
|
|
65
|
+
"""Return an OSC 8 terminal hyperlink. Falls back to plain URL on unsupported terminals."""
|
|
66
|
+
label = text or url
|
|
67
|
+
return f"\033]8;;{url}\033\\{label}\033]8;;\033\\"
|
|
68
|
+
|
|
69
|
+
|
|
55
70
|
def _error(msg: str) -> None:
|
|
56
71
|
"""Print an error message to stderr and exit."""
|
|
57
72
|
click.echo(f"Error: {msg}", err=True)
|
|
@@ -228,7 +243,9 @@ def _resolve_key_path(config: dict, creds: dict | None = None) -> Path:
|
|
|
228
243
|
" aicodinggym swe fetch django__django-10097\n"
|
|
229
244
|
" aicodinggym swe submit django__django-10097 --message 'Fix auth bug'\n"
|
|
230
245
|
" aicodinggym mle download spaceship-titanic\n"
|
|
231
|
-
" aicodinggym mle submit spaceship-titanic -F predictions.csv\n
|
|
246
|
+
" aicodinggym mle submit spaceship-titanic -F predictions.csv\n"
|
|
247
|
+
" aicodinggym cr fetch sentry-0001\n"
|
|
248
|
+
" aicodinggym cr submit sentry-0001 -f review.md\n\n"
|
|
232
249
|
"\b\n"
|
|
233
250
|
"WEBSITE:\n"
|
|
234
251
|
" https://aicodinggym.com\n"
|
|
@@ -326,7 +343,7 @@ def configure(user_id: str, workspace_dir: str | None):
|
|
|
326
343
|
f" SSH Key: {private_key_path}\n"
|
|
327
344
|
f" Config: ~/.aicodinggym/config.json\n"
|
|
328
345
|
f"\n"
|
|
329
|
-
f"You can now use 'aicodinggym swe' and 'aicodinggym
|
|
346
|
+
f"You can now use 'aicodinggym swe', 'aicodinggym mle', and 'aicodinggym cr' commands."
|
|
330
347
|
)
|
|
331
348
|
except APIError as e:
|
|
332
349
|
_error(str(e))
|
|
@@ -548,7 +565,7 @@ def swe_submit(problem_id: str, user_id: str | None, message: str | None,
|
|
|
548
565
|
f" Branch: {branch}\n"
|
|
549
566
|
f" Status: Pushed and backend notified\n"
|
|
550
567
|
f"\n"
|
|
551
|
-
f"
|
|
568
|
+
f"View results at: {_hyperlink(f'https://aicodinggym.com/challenges/swe/{problem_id}')}"
|
|
552
569
|
)
|
|
553
570
|
|
|
554
571
|
|
|
@@ -845,6 +862,187 @@ def swe_test(problem_id: str, user_id: str | None, workspace_dir: str | None,
|
|
|
845
862
|
sys.exit(proc.returncode)
|
|
846
863
|
|
|
847
864
|
|
|
865
|
+
# ── cr group ──────────────────────────────────────────────────────────────────
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
@main.group()
|
|
869
|
+
def cr():
|
|
870
|
+
"""Code Review challenges - submit reviews for code diffs.
|
|
871
|
+
|
|
872
|
+
\b
|
|
873
|
+
PREREQUISITE:
|
|
874
|
+
Run 'aicodinggym configure --user-id YOUR_USER_ID' before using these commands.
|
|
875
|
+
|
|
876
|
+
\b
|
|
877
|
+
WORKFLOW:
|
|
878
|
+
1. aicodinggym cr fetch CR_PROBLEM_ID # Download diff + create review.md
|
|
879
|
+
2. (edit review.md with your findings)
|
|
880
|
+
3. aicodinggym cr submit CR_PROBLEM_ID -f review.md # Submit your review
|
|
881
|
+
"""
|
|
882
|
+
pass
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
@cr.command("fetch")
|
|
886
|
+
@click.argument("problem_id")
|
|
887
|
+
@click.option("--user-id", default=None, help="Override configured user ID.")
|
|
888
|
+
@click.option("--workspace-dir", default=None, type=click.Path(),
|
|
889
|
+
help="Directory to clone into. Overrides configured workspace.")
|
|
890
|
+
def cr_fetch(problem_id: str, user_id: str | None, workspace_dir: str | None):
|
|
891
|
+
"""Fetch a Code Review problem: downloads the PR diff and creates a review template.
|
|
892
|
+
|
|
893
|
+
Clones the repository, generates diff.patch from base→head, and creates
|
|
894
|
+
review.md as a template to fill in your review.
|
|
895
|
+
|
|
896
|
+
\b
|
|
897
|
+
ARGUMENTS:
|
|
898
|
+
PROBLEM_ID The code review problem identifier (e.g., 'keycloak-0008').
|
|
899
|
+
|
|
900
|
+
\b
|
|
901
|
+
EXAMPLE:
|
|
902
|
+
aicodinggym cr fetch keycloak-0008
|
|
903
|
+
# Edit review.md in the problem directory, then:
|
|
904
|
+
aicodinggym cr submit keycloak-0008 -f review.md
|
|
905
|
+
"""
|
|
906
|
+
config = load_config()
|
|
907
|
+
uid = _resolve_user_id(config, user_id)
|
|
908
|
+
workspace = _resolve_workspace(config, workspace_dir)
|
|
909
|
+
|
|
910
|
+
try:
|
|
911
|
+
click.echo(f"Fetching problem '{problem_id}' from server...")
|
|
912
|
+
data = api_fetch_pr(uid, problem_id)
|
|
913
|
+
except APIError as e:
|
|
914
|
+
_error(str(e))
|
|
915
|
+
|
|
916
|
+
base_branch = data.get("base_branch")
|
|
917
|
+
head_branch = data.get("head_branch")
|
|
918
|
+
repo_url = data.get("repo_url")
|
|
919
|
+
|
|
920
|
+
if not (repo_url and repo_url.strip()) or not (base_branch and base_branch.strip()) or not (head_branch and head_branch.strip()):
|
|
921
|
+
_error("Server did not return required fields (repo_url, base_branch, head_branch).")
|
|
922
|
+
|
|
923
|
+
# Save credentials for later submit
|
|
924
|
+
credentials = load_credentials()
|
|
925
|
+
credentials[problem_id] = {
|
|
926
|
+
"repo_url": repo_url,
|
|
927
|
+
"base_branch": base_branch,
|
|
928
|
+
"head_branch": head_branch,
|
|
929
|
+
"user_id": uid,
|
|
930
|
+
"workspace_dir": str(workspace),
|
|
931
|
+
"benchmark": "cr",
|
|
932
|
+
}
|
|
933
|
+
save_credentials(credentials)
|
|
934
|
+
|
|
935
|
+
workspace.mkdir(parents=True, exist_ok=True)
|
|
936
|
+
|
|
937
|
+
click.echo(f"Cloning into {workspace / problem_id}...")
|
|
938
|
+
success, msg = clone_repo_cr(repo_url, base_branch, head_branch,
|
|
939
|
+
problem_id, str(workspace))
|
|
940
|
+
if not success:
|
|
941
|
+
_error(msg)
|
|
942
|
+
|
|
943
|
+
problem_dir = workspace / problem_id
|
|
944
|
+
|
|
945
|
+
# Generate diff.patch
|
|
946
|
+
diff_result = run_git_command(
|
|
947
|
+
["git", "diff", f"{base_branch}..{head_branch}"], str(problem_dir)
|
|
948
|
+
)
|
|
949
|
+
diff_path = problem_dir / "diff.patch"
|
|
950
|
+
diff_path.write_text(diff_result.stdout)
|
|
951
|
+
|
|
952
|
+
# Create review.md template if it doesn't exist yet
|
|
953
|
+
review_path = problem_dir / "review.md"
|
|
954
|
+
if not review_path.exists():
|
|
955
|
+
review_path.write_text(
|
|
956
|
+
f"# Code Review: {problem_id}\n\n"
|
|
957
|
+
"## Summary\n\n"
|
|
958
|
+
"<!-- Brief summary of what this PR does -->\n\n"
|
|
959
|
+
"## Issues Found\n\n"
|
|
960
|
+
"<!-- List bugs, logic errors, security issues, etc. -->\n\n"
|
|
961
|
+
"## Suggestions\n\n"
|
|
962
|
+
"<!-- Optional improvements, style notes, etc. -->\n\n"
|
|
963
|
+
"## Verdict\n\n"
|
|
964
|
+
"<!-- Approve / Request Changes / Comment -->\n"
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
cat_cmd = "type" if sys.platform == "win32" else "cat"
|
|
968
|
+
click.echo(
|
|
969
|
+
f"\nSuccessfully fetched: {problem_id}\n"
|
|
970
|
+
f"\n"
|
|
971
|
+
f" Diff saved to: {diff_path}\n"
|
|
972
|
+
f" Review template: {review_path}\n"
|
|
973
|
+
f"\n"
|
|
974
|
+
f"Next steps:\n"
|
|
975
|
+
f" 1. Review the diff: {cat_cmd} {diff_path}\n"
|
|
976
|
+
f" 2. Write your review in {review_path}\n"
|
|
977
|
+
f" 3. Submit: aicodinggym cr submit {problem_id} -f review.md\n"
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
@cr.command("submit")
|
|
982
|
+
@click.argument("problem_id")
|
|
983
|
+
@click.option("--user-id", default=None, help="Override configured user ID.")
|
|
984
|
+
@click.option(
|
|
985
|
+
"-f", "--file", "review_file", type=click.Path(exists=True),
|
|
986
|
+
help="Path to a file containing your review.",
|
|
987
|
+
)
|
|
988
|
+
@click.option(
|
|
989
|
+
"-m", "--message", "review_text",
|
|
990
|
+
help="Inline review text.",
|
|
991
|
+
)
|
|
992
|
+
def cr_submit(problem_id: str, user_id: str | None, review_file: str | None,
|
|
993
|
+
review_text: str | None):
|
|
994
|
+
"""Submit a code review for a Code Review challenge.
|
|
995
|
+
|
|
996
|
+
Reads your review from a file (-f), inline text (-m), or piped stdin,
|
|
997
|
+
and submits it to the AI Coding Gym server.
|
|
998
|
+
|
|
999
|
+
\b
|
|
1000
|
+
ARGUMENTS:
|
|
1001
|
+
PROBLEM_ID The code review problem identifier (e.g., 'sentry-0001').
|
|
1002
|
+
|
|
1003
|
+
\b
|
|
1004
|
+
EXAMPLE:
|
|
1005
|
+
aicodinggym cr submit sentry-0001 -f review.md
|
|
1006
|
+
aicodinggym cr submit sentry-0001 -m "Found a null pointer bug on line 42"
|
|
1007
|
+
echo "My review" | aicodinggym cr submit sentry-0001
|
|
1008
|
+
"""
|
|
1009
|
+
config = load_config()
|
|
1010
|
+
uid = _resolve_user_id(config, user_id)
|
|
1011
|
+
|
|
1012
|
+
# Collect review text (priority: -f > -m > stdin)
|
|
1013
|
+
review = None
|
|
1014
|
+
if review_file:
|
|
1015
|
+
review = Path(review_file).read_text()
|
|
1016
|
+
elif review_text:
|
|
1017
|
+
review = review_text
|
|
1018
|
+
elif not sys.stdin.isatty():
|
|
1019
|
+
review = sys.stdin.read()
|
|
1020
|
+
|
|
1021
|
+
if not review or not review.strip():
|
|
1022
|
+
_error(
|
|
1023
|
+
"No review text provided.\n\n"
|
|
1024
|
+
"Provide your review using one of:\n"
|
|
1025
|
+
" -f <file> Read review from a file\n"
|
|
1026
|
+
" -m \"text\" Inline review text\n"
|
|
1027
|
+
" echo ... | ... Pipe from stdin\n\n"
|
|
1028
|
+
"Example:\n"
|
|
1029
|
+
f" aicodinggym cr submit {problem_id} -f review.md"
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
try:
|
|
1033
|
+
result = cr_submit_review(uid, problem_id, review.strip())
|
|
1034
|
+
except APIError as e:
|
|
1035
|
+
_error(str(e))
|
|
1036
|
+
|
|
1037
|
+
click.echo(
|
|
1038
|
+
f"\nSuccessfully submitted code review for {problem_id}\n"
|
|
1039
|
+
f"\n"
|
|
1040
|
+
f" Status: {result.get('status', 'COMPLETED')}\n"
|
|
1041
|
+
f"\n"
|
|
1042
|
+
f"View results at: {_hyperlink(f'https://aicodinggym.com/challenges/cr/{problem_id}')}"
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
|
|
848
1046
|
# ── mle group ────────────────────────────────────────────────────────────────
|
|
849
1047
|
|
|
850
1048
|
|
|
@@ -974,4 +1172,4 @@ def mle_submit(competition_id: str, csv_path: str, user_id: str | None,
|
|
|
974
1172
|
)
|
|
975
1173
|
if score is not None:
|
|
976
1174
|
click.echo(f" Score: {score}\n")
|
|
977
|
-
click.echo("
|
|
1175
|
+
click.echo(f"View results at: {_hyperlink(f'https://aicodinggym.com/challenges/mle/{competition_id}')}")
|
|
@@ -6,6 +6,9 @@ SSH keys are stored in ~/.aicodinggym/{user_id}_id_rsa.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
9
12
|
from pathlib import Path
|
|
10
13
|
from typing import Any
|
|
11
14
|
|
|
@@ -19,8 +22,22 @@ _CONFIG_FIELDS = ("user_id", "repo_name", "private_key_path", "workspace_dir")
|
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
def ensure_config_dir() -> Path:
|
|
22
|
-
"""Create the config directory with secure permissions if it doesn't exist.
|
|
23
|
-
|
|
25
|
+
"""Create the config directory with secure permissions if it doesn't exist.
|
|
26
|
+
|
|
27
|
+
On Unix/macOS: mode 0o700 (owner-only access).
|
|
28
|
+
On Windows: removes inherited ACLs and grants full control only to the
|
|
29
|
+
current user via icacls.
|
|
30
|
+
"""
|
|
31
|
+
created = not CONFIG_DIR.exists()
|
|
32
|
+
CONFIG_DIR.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
33
|
+
if created and sys.platform == "win32":
|
|
34
|
+
username = os.environ.get("USERNAME", "")
|
|
35
|
+
if username:
|
|
36
|
+
subprocess.run(
|
|
37
|
+
["icacls", str(CONFIG_DIR), "/inheritance:r",
|
|
38
|
+
"/grant:r", f"{username}:(OI)(CI)(F)"],
|
|
39
|
+
capture_output=True,
|
|
40
|
+
)
|
|
24
41
|
return CONFIG_DIR
|
|
25
42
|
|
|
26
43
|
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Git and SSH key operations for AI Coding Gym CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from .config import ensure_config_dir
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _find_git_ssh() -> str | None:
|
|
15
|
+
"""On Windows, find Git for Windows' bundled ssh.exe.
|
|
16
|
+
|
|
17
|
+
Windows may have two SSH binaries on PATH: the built-in OpenSSH
|
|
18
|
+
(C:\\Windows\\System32\\OpenSSH\\ssh.exe) and Git for Windows' MSYS2
|
|
19
|
+
ssh (C:\\Program Files\\Git\\usr\\bin\\ssh.exe). System32 is usually
|
|
20
|
+
first on PATH, so an unqualified 'ssh' resolves to Windows OpenSSH,
|
|
21
|
+
which can trigger GUI credential dialogs or deadlock when stdout is
|
|
22
|
+
captured. This function returns the full path to Git's bundled ssh
|
|
23
|
+
so we can reference it explicitly in GIT_SSH_COMMAND.
|
|
24
|
+
"""
|
|
25
|
+
if sys.platform != "win32":
|
|
26
|
+
return None
|
|
27
|
+
git_path = shutil.which("git")
|
|
28
|
+
if not git_path:
|
|
29
|
+
return None
|
|
30
|
+
# Walk up from git.exe to find the Git root containing usr/bin/ssh.exe.
|
|
31
|
+
# Handles cmd/, bin/, and mingw64/bin/ layouts.
|
|
32
|
+
candidate = Path(git_path).resolve().parent
|
|
33
|
+
for _ in range(4):
|
|
34
|
+
ssh = candidate / "usr" / "bin" / "ssh.exe"
|
|
35
|
+
if ssh.exists():
|
|
36
|
+
return str(ssh).replace("\\", "/")
|
|
37
|
+
candidate = candidate.parent
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _validate_git_ref(name: str, label: str) -> None:
|
|
42
|
+
"""Raise ValueError if name contains suspicious shell metacharacters."""
|
|
43
|
+
if re.search(r'[;&|`$(){}]', name):
|
|
44
|
+
raise ValueError(f"Invalid {label}: {name!r}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _restrict_key_permissions(key_path: Path) -> None:
|
|
48
|
+
"""Restrict an SSH private key file to owner-only access.
|
|
49
|
+
|
|
50
|
+
On Unix/macOS: chmod 600 (read/write owner only).
|
|
51
|
+
On Windows: uses icacls to remove inherited permissions and grant
|
|
52
|
+
full control only to the current user. SSH clients on both platforms
|
|
53
|
+
refuse to use a key whose permissions are too open.
|
|
54
|
+
"""
|
|
55
|
+
if sys.platform == "win32":
|
|
56
|
+
# Remove inherited ACLs, then grant only the current user full control.
|
|
57
|
+
# (F) = Full control, matching chmod 0o600 (owner read+write).
|
|
58
|
+
key_str = str(key_path)
|
|
59
|
+
username = os.environ.get("USERNAME", "")
|
|
60
|
+
if username:
|
|
61
|
+
subprocess.run(
|
|
62
|
+
["icacls", key_str, "/inheritance:r",
|
|
63
|
+
"/grant:r", f"{username}:(F)"],
|
|
64
|
+
capture_output=True,
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
key_path.chmod(0o600)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def generate_ssh_key_pair(user_id: str) -> tuple[Path, str]:
|
|
71
|
+
"""Generate an SSH key pair for the user.
|
|
72
|
+
|
|
73
|
+
First checks ~/.mcp-keys/ for existing keys matching the user_id.
|
|
74
|
+
If found, copies them to ~/.aicodinggym/ and reuses them.
|
|
75
|
+
Otherwise generates new keys in ~/.aicodinggym/{user_id}_id_rsa.
|
|
76
|
+
Returns (private_key_path, public_key_content).
|
|
77
|
+
"""
|
|
78
|
+
key_dir = ensure_config_dir()
|
|
79
|
+
key_path = key_dir / f"{user_id}_id_rsa"
|
|
80
|
+
|
|
81
|
+
if not key_path.exists():
|
|
82
|
+
# Check ~/.mcp-keys/ for existing keys matching user_id
|
|
83
|
+
mcp_keys_dir = Path.home() / ".mcp-keys"
|
|
84
|
+
mcp_private = mcp_keys_dir / f"{user_id}_id_rsa"
|
|
85
|
+
mcp_public = mcp_keys_dir / f"{user_id}_id_rsa.pub"
|
|
86
|
+
|
|
87
|
+
if mcp_private.exists() and mcp_public.exists():
|
|
88
|
+
shutil.copy2(mcp_private, key_path)
|
|
89
|
+
shutil.copy2(mcp_public, Path(f"{key_path}.pub"))
|
|
90
|
+
_restrict_key_permissions(key_path)
|
|
91
|
+
else:
|
|
92
|
+
if not shutil.which("ssh-keygen"):
|
|
93
|
+
raise RuntimeError(
|
|
94
|
+
"ssh-keygen is not installed or not on PATH.\n"
|
|
95
|
+
"On Windows, install Git for Windows (https://git-scm.com) "
|
|
96
|
+
"which includes ssh-keygen, or use the OpenSSH optional feature."
|
|
97
|
+
)
|
|
98
|
+
result = subprocess.run(
|
|
99
|
+
["ssh-keygen", "-t", "rsa", "-b", "4096", "-f", str(key_path),
|
|
100
|
+
"-N", "", "-C", f"aicodinggym-{user_id}"],
|
|
101
|
+
capture_output=True, text=True,
|
|
102
|
+
)
|
|
103
|
+
if result.returncode != 0:
|
|
104
|
+
raise RuntimeError(f"Failed to generate SSH key: {result.stderr}")
|
|
105
|
+
|
|
106
|
+
pub_key_path = Path(f"{key_path}.pub")
|
|
107
|
+
public_key = pub_key_path.read_text().strip()
|
|
108
|
+
return key_path, public_key
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def run_git_command(cmd: list[str], cwd: str, key_path: Optional[Path] = None) -> subprocess.CompletedProcess:
|
|
112
|
+
"""Execute a git command with optional SSH key configuration.
|
|
113
|
+
|
|
114
|
+
cmd must be a list of arguments (e.g. ["git", "status"]).
|
|
115
|
+
"""
|
|
116
|
+
env = os.environ.copy()
|
|
117
|
+
if key_path:
|
|
118
|
+
# Quote the key path in case it contains spaces (common on Windows).
|
|
119
|
+
# Use forward slashes — works on all platforms and avoids backslash escaping.
|
|
120
|
+
quoted_key = str(key_path).replace("\\", "/")
|
|
121
|
+
# On Windows, use Git for Windows' bundled ssh to avoid Windows native
|
|
122
|
+
# OpenSSH which can trigger GUI credential dialogs or deadlock when
|
|
123
|
+
# stdout is captured. Falls back to bare "ssh" if not found.
|
|
124
|
+
ssh_bin = _find_git_ssh() or "ssh"
|
|
125
|
+
# Always use /dev/null for UserKnownHostsFile. On macOS/Linux this is
|
|
126
|
+
# the native null device. On Windows, Git for Windows bundles MSYS2's
|
|
127
|
+
# ssh which translates /dev/null correctly. Using os.devnull ("nul")
|
|
128
|
+
# would break MSYS2's ssh which treats "nul" as a literal filename.
|
|
129
|
+
# BatchMode=yes prevents any interactive prompts (password, passphrase)
|
|
130
|
+
# that would cause a hang when stdout/stderr are captured.
|
|
131
|
+
env["GIT_SSH_COMMAND"] = (
|
|
132
|
+
f'"{ssh_bin}" -i "{quoted_key}" '
|
|
133
|
+
f"-o StrictHostKeyChecking=no "
|
|
134
|
+
f"-o UserKnownHostsFile=/dev/null "
|
|
135
|
+
f"-o BatchMode=yes"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, env=env)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def clone_repo(repo_url: str, branch: str, dest_name: str,
|
|
142
|
+
workspace: str, key_path: Path) -> tuple[bool, str]:
|
|
143
|
+
"""Clone a repo branch into workspace/dest_name.
|
|
144
|
+
|
|
145
|
+
Returns (success, message).
|
|
146
|
+
"""
|
|
147
|
+
problem_dir = Path(workspace) / dest_name
|
|
148
|
+
|
|
149
|
+
if problem_dir.exists():
|
|
150
|
+
# Check if already on the correct branch
|
|
151
|
+
result = run_git_command(["git", "rev-parse", "--abbrev-ref", "HEAD"], str(problem_dir))
|
|
152
|
+
if result.returncode == 0 and result.stdout.strip() == branch:
|
|
153
|
+
pull = run_git_command(["git", "pull", "origin", branch], str(problem_dir), key_path)
|
|
154
|
+
if pull.returncode != 0:
|
|
155
|
+
return False, f"Git pull failed:\n{pull.stderr}"
|
|
156
|
+
return True, f"Already exists. Updated to latest version.\nRepository: {problem_dir}\nBranch: {branch}"
|
|
157
|
+
return False, (
|
|
158
|
+
f"Directory {problem_dir} already exists with different content.\n"
|
|
159
|
+
"Remove it first or use --workspace-dir to specify a different location."
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
cmd = ["git", "clone", "--single-branch", "--branch", branch, "--depth", "1", repo_url, dest_name]
|
|
163
|
+
result = run_git_command(cmd, workspace, key_path)
|
|
164
|
+
|
|
165
|
+
if result.returncode != 0:
|
|
166
|
+
return False, f"Git clone failed:\n{result.stderr}\nMake sure the branch '{branch}' exists in the repository."
|
|
167
|
+
|
|
168
|
+
return True, f"Cloned to: {problem_dir}\nBranch: {branch}"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def clone_repo_cr(repo_url: str, base_branch: str, head_branch: str,
|
|
172
|
+
dest_name: str, workspace: str,
|
|
173
|
+
key_path: Optional[Path] = None) -> tuple[bool, str]:
|
|
174
|
+
"""Clone a code review repo with both base and head branches.
|
|
175
|
+
|
|
176
|
+
Clones the base branch first (shallow), then fetches the head branch.
|
|
177
|
+
Returns (success, message).
|
|
178
|
+
"""
|
|
179
|
+
_validate_git_ref(base_branch, "base_branch")
|
|
180
|
+
_validate_git_ref(head_branch, "head_branch")
|
|
181
|
+
_validate_git_ref(repo_url, "repo_url")
|
|
182
|
+
_validate_git_ref(dest_name, "dest_name")
|
|
183
|
+
|
|
184
|
+
problem_dir = Path(workspace) / dest_name
|
|
185
|
+
|
|
186
|
+
if problem_dir.exists():
|
|
187
|
+
# Already cloned — fetch latest for both branches
|
|
188
|
+
for branch in (base_branch, head_branch):
|
|
189
|
+
result = run_git_command(["git", "fetch", "origin", branch], str(problem_dir), key_path)
|
|
190
|
+
if result.returncode != 0:
|
|
191
|
+
return False, f"Git fetch failed for {branch}:\n{result.stderr}"
|
|
192
|
+
result = run_git_command(["git", "branch", "-f", branch, "FETCH_HEAD"], str(problem_dir))
|
|
193
|
+
if result.returncode != 0:
|
|
194
|
+
return False, f"Failed to update branch {branch}:\n{result.stderr}"
|
|
195
|
+
result = run_git_command(["git", "checkout", head_branch], str(problem_dir))
|
|
196
|
+
if result.returncode != 0:
|
|
197
|
+
return False, f"Failed to checkout head branch '{head_branch}':\n{result.stderr}"
|
|
198
|
+
return True, (
|
|
199
|
+
f"Already exists. Updated both branches.\n"
|
|
200
|
+
f"Repository: {problem_dir}\n"
|
|
201
|
+
f"Branches: {base_branch}, {head_branch}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Clone base branch (shallow); depth 50 needed for diffing between branches
|
|
205
|
+
cmd = ["git", "clone", "--single-branch", "--branch", base_branch, "--depth", "50", repo_url, dest_name]
|
|
206
|
+
result = run_git_command(cmd, workspace, key_path)
|
|
207
|
+
if result.returncode != 0:
|
|
208
|
+
return False, f"Git clone failed:\n{result.stderr}"
|
|
209
|
+
|
|
210
|
+
# Fetch head branch
|
|
211
|
+
result = run_git_command(["git", "fetch", "origin", head_branch], str(problem_dir), key_path)
|
|
212
|
+
if result.returncode != 0:
|
|
213
|
+
return False, f"Failed to fetch head branch '{head_branch}':\n{result.stderr}"
|
|
214
|
+
|
|
215
|
+
# Create local head branch tracking the fetched ref
|
|
216
|
+
result = run_git_command(["git", "branch", "-f", head_branch, "FETCH_HEAD"], str(problem_dir))
|
|
217
|
+
if result.returncode != 0:
|
|
218
|
+
return False, f"Failed to create branch {head_branch}:\n{result.stderr}"
|
|
219
|
+
|
|
220
|
+
# Check out head branch so the user starts on the code being reviewed
|
|
221
|
+
result = run_git_command(["git", "checkout", head_branch], str(problem_dir))
|
|
222
|
+
if result.returncode != 0:
|
|
223
|
+
return False, f"Failed to checkout head branch '{head_branch}':\n{result.stderr}"
|
|
224
|
+
|
|
225
|
+
return True, (
|
|
226
|
+
f"Cloned to: {problem_dir}\n"
|
|
227
|
+
f"Branches: {base_branch}, {head_branch}"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def add_commit_push(problem_dir: str, branch: str, key_path: Path,
|
|
232
|
+
message: str, force: bool = False) -> tuple[bool, str, str]:
|
|
233
|
+
"""Stage, commit, and push changes.
|
|
234
|
+
|
|
235
|
+
Returns (success, message, commit_hash).
|
|
236
|
+
"""
|
|
237
|
+
pdir = Path(problem_dir)
|
|
238
|
+
|
|
239
|
+
# Stage all changes except .github
|
|
240
|
+
result = run_git_command(["git", "add", "-A", "--", ".", ":(exclude).github"], str(pdir))
|
|
241
|
+
if result.returncode != 0:
|
|
242
|
+
return False, f"Git add failed:\n{result.stderr}", ""
|
|
243
|
+
|
|
244
|
+
# Check for staged changes
|
|
245
|
+
status = run_git_command(["git", "diff", "--cached", "--name-only"], str(pdir))
|
|
246
|
+
if not status.stdout.strip():
|
|
247
|
+
return False, "No changes to commit. Your working directory is clean.", ""
|
|
248
|
+
|
|
249
|
+
# Commit — pass message directly as a list arg; no shell escaping needed
|
|
250
|
+
result = run_git_command(["git", "commit", "-m", message], str(pdir))
|
|
251
|
+
if result.returncode != 0:
|
|
252
|
+
return False, f"Git commit failed:\n{result.stderr}", ""
|
|
253
|
+
|
|
254
|
+
# Get commit hash
|
|
255
|
+
hash_result = run_git_command(["git", "rev-parse", "HEAD"], str(pdir))
|
|
256
|
+
commit_hash = hash_result.stdout.strip()
|
|
257
|
+
|
|
258
|
+
# Push
|
|
259
|
+
push_cmd = ["git", "push"]
|
|
260
|
+
if force:
|
|
261
|
+
push_cmd.append("--force-with-lease")
|
|
262
|
+
push_cmd += ["origin", branch]
|
|
263
|
+
result = run_git_command(push_cmd, str(pdir), key_path)
|
|
264
|
+
if result.returncode != 0:
|
|
265
|
+
return False, f"Git push failed:\n{result.stderr}", commit_hash
|
|
266
|
+
|
|
267
|
+
return True, "Committed and pushed successfully.", commit_hash
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def reset_to_setup_commit(problem_dir: str) -> tuple[bool, str]:
|
|
271
|
+
"""Reset repo to the original 'Setup SWE-bench instance:' commit.
|
|
272
|
+
|
|
273
|
+
Returns (success, message).
|
|
274
|
+
"""
|
|
275
|
+
log_result = run_git_command(["git", "log", "--format=%H:%s", "--reverse"], problem_dir)
|
|
276
|
+
if log_result.returncode != 0:
|
|
277
|
+
return False, f"Git log failed:\n{log_result.stderr}"
|
|
278
|
+
|
|
279
|
+
setup_prefix = "Setup SWE-bench instance:"
|
|
280
|
+
setup_commit = None
|
|
281
|
+
for line in log_result.stdout.splitlines():
|
|
282
|
+
parts = line.split(":", 1)
|
|
283
|
+
if len(parts) != 2:
|
|
284
|
+
continue
|
|
285
|
+
commit_hash, subject = parts[0].strip(), parts[1].strip()
|
|
286
|
+
if subject.startswith(setup_prefix):
|
|
287
|
+
setup_commit = commit_hash
|
|
288
|
+
break
|
|
289
|
+
|
|
290
|
+
if not setup_commit:
|
|
291
|
+
return False, (
|
|
292
|
+
f"Could not find the original setup commit.\n"
|
|
293
|
+
f"Expected a commit message starting with '{setup_prefix}'."
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
reset = run_git_command(["git", "reset", "--hard", setup_commit], problem_dir)
|
|
297
|
+
if reset.returncode != 0:
|
|
298
|
+
return False, f"Git reset failed:\n{reset.stderr}"
|
|
299
|
+
|
|
300
|
+
clean = run_git_command(["git", "clean", "-fd"], problem_dir)
|
|
301
|
+
if clean.returncode != 0:
|
|
302
|
+
return False, f"Git clean failed:\n{clean.stderr}"
|
|
303
|
+
|
|
304
|
+
return True, f"Reset to setup commit {setup_commit[:8]}.\nLocal changes discarded and untracked files removed."
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def check_tool_installed(tool_name: str) -> bool:
|
|
308
|
+
"""Check if a CLI tool is available on PATH."""
|
|
309
|
+
return shutil.which(tool_name) is not None
|
aicodinggym_cli-0.2.0/git_ops.py
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
"""Git and SSH key operations for AI Coding Gym CLI."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import shutil
|
|
5
|
-
import subprocess
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Optional
|
|
8
|
-
|
|
9
|
-
from .config import ensure_config_dir
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def generate_ssh_key_pair(user_id: str) -> tuple[Path, str]:
|
|
13
|
-
"""Generate an SSH key pair for the user.
|
|
14
|
-
|
|
15
|
-
First checks ~/.mcp-keys/ for existing keys matching the user_id.
|
|
16
|
-
If found, copies them to ~/.aicodinggym/ and reuses them.
|
|
17
|
-
Otherwise generates new keys in ~/.aicodinggym/{user_id}_id_rsa.
|
|
18
|
-
Returns (private_key_path, public_key_content).
|
|
19
|
-
"""
|
|
20
|
-
key_dir = ensure_config_dir()
|
|
21
|
-
key_path = key_dir / f"{user_id}_id_rsa"
|
|
22
|
-
|
|
23
|
-
if not key_path.exists():
|
|
24
|
-
# Check ~/.mcp-keys/ for existing keys matching user_id
|
|
25
|
-
mcp_keys_dir = Path.home() / ".mcp-keys"
|
|
26
|
-
mcp_private = mcp_keys_dir / f"{user_id}_id_rsa"
|
|
27
|
-
mcp_public = mcp_keys_dir / f"{user_id}_id_rsa.pub"
|
|
28
|
-
|
|
29
|
-
if mcp_private.exists() and mcp_public.exists():
|
|
30
|
-
shutil.copy2(mcp_private, key_path)
|
|
31
|
-
shutil.copy2(mcp_public, Path(f"{key_path}.pub"))
|
|
32
|
-
key_path.chmod(0o600)
|
|
33
|
-
else:
|
|
34
|
-
result = subprocess.run(
|
|
35
|
-
["ssh-keygen", "-t", "rsa", "-b", "4096", "-f", str(key_path),
|
|
36
|
-
"-N", "", "-C", f"aicodinggym-{user_id}"],
|
|
37
|
-
capture_output=True, text=True,
|
|
38
|
-
)
|
|
39
|
-
if result.returncode != 0:
|
|
40
|
-
raise RuntimeError(f"Failed to generate SSH key: {result.stderr}")
|
|
41
|
-
|
|
42
|
-
pub_key_path = Path(f"{key_path}.pub")
|
|
43
|
-
public_key = pub_key_path.read_text().strip()
|
|
44
|
-
return key_path, public_key
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def run_git_command(cmd: str, cwd: str, key_path: Optional[Path] = None) -> subprocess.CompletedProcess:
|
|
48
|
-
"""Execute a git command with optional SSH key configuration."""
|
|
49
|
-
env = os.environ.copy()
|
|
50
|
-
if key_path:
|
|
51
|
-
env["GIT_SSH_COMMAND"] = f"ssh -i {key_path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
|
52
|
-
|
|
53
|
-
return subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True, env=env)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def clone_repo(repo_url: str, branch: str, dest_name: str,
|
|
57
|
-
workspace: str, key_path: Path) -> tuple[bool, str]:
|
|
58
|
-
"""Clone a repo branch into workspace/dest_name.
|
|
59
|
-
|
|
60
|
-
Returns (success, message).
|
|
61
|
-
"""
|
|
62
|
-
problem_dir = Path(workspace) / dest_name
|
|
63
|
-
|
|
64
|
-
if problem_dir.exists():
|
|
65
|
-
# Check if already on the correct branch
|
|
66
|
-
result = run_git_command("git rev-parse --abbrev-ref HEAD", str(problem_dir))
|
|
67
|
-
if result.returncode == 0 and result.stdout.strip() == branch:
|
|
68
|
-
pull = run_git_command(f"git pull origin {branch}", str(problem_dir), key_path)
|
|
69
|
-
if pull.returncode != 0:
|
|
70
|
-
return False, f"Git pull failed:\n{pull.stderr}"
|
|
71
|
-
return True, f"Already exists. Updated to latest version.\nRepository: {problem_dir}\nBranch: {branch}"
|
|
72
|
-
return False, (
|
|
73
|
-
f"Directory {problem_dir} already exists with different content.\n"
|
|
74
|
-
"Remove it first or use --workspace-dir to specify a different location."
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
cmd = f"git clone --single-branch --branch {branch} --depth 1 {repo_url} {dest_name}"
|
|
78
|
-
result = run_git_command(cmd, workspace, key_path)
|
|
79
|
-
|
|
80
|
-
if result.returncode != 0:
|
|
81
|
-
return False, f"Git clone failed:\n{result.stderr}\nMake sure the branch '{branch}' exists in the repository."
|
|
82
|
-
|
|
83
|
-
return True, f"Cloned to: {problem_dir}\nBranch: {branch}"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def add_commit_push(problem_dir: str, branch: str, key_path: Path,
|
|
87
|
-
message: str, force: bool = False) -> tuple[bool, str, str]:
|
|
88
|
-
"""Stage, commit, and push changes.
|
|
89
|
-
|
|
90
|
-
Returns (success, message, commit_hash).
|
|
91
|
-
"""
|
|
92
|
-
pdir = Path(problem_dir)
|
|
93
|
-
|
|
94
|
-
# Stage all changes except .github
|
|
95
|
-
result = run_git_command("git add -A -- . ':(exclude).github'", str(pdir))
|
|
96
|
-
if result.returncode != 0:
|
|
97
|
-
return False, f"Git add failed:\n{result.stderr}", ""
|
|
98
|
-
|
|
99
|
-
# Check for staged changes
|
|
100
|
-
status = run_git_command("git diff --cached --name-only", str(pdir))
|
|
101
|
-
if not status.stdout.strip():
|
|
102
|
-
return False, "No changes to commit. Your working directory is clean.", ""
|
|
103
|
-
|
|
104
|
-
# Commit
|
|
105
|
-
safe_msg = message.replace('"', '\\"')
|
|
106
|
-
result = run_git_command(f'git commit -m "{safe_msg}"', str(pdir))
|
|
107
|
-
if result.returncode != 0:
|
|
108
|
-
return False, f"Git commit failed:\n{result.stderr}", ""
|
|
109
|
-
|
|
110
|
-
# Get commit hash
|
|
111
|
-
hash_result = run_git_command("git rev-parse HEAD", str(pdir))
|
|
112
|
-
commit_hash = hash_result.stdout.strip()
|
|
113
|
-
|
|
114
|
-
# Push
|
|
115
|
-
push_flag = "--force-with-lease " if force else ""
|
|
116
|
-
result = run_git_command(f"git push {push_flag}origin {branch}", str(pdir), key_path)
|
|
117
|
-
if result.returncode != 0:
|
|
118
|
-
return False, f"Git push failed:\n{result.stderr}", commit_hash
|
|
119
|
-
|
|
120
|
-
return True, "Committed and pushed successfully.", commit_hash
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def reset_to_setup_commit(problem_dir: str) -> tuple[bool, str]:
|
|
124
|
-
"""Reset repo to the original 'Setup SWE-bench instance:' commit.
|
|
125
|
-
|
|
126
|
-
Returns (success, message).
|
|
127
|
-
"""
|
|
128
|
-
log_result = run_git_command("git log --format=%H:%s --reverse", problem_dir)
|
|
129
|
-
if log_result.returncode != 0:
|
|
130
|
-
return False, f"Git log failed:\n{log_result.stderr}"
|
|
131
|
-
|
|
132
|
-
setup_prefix = "Setup SWE-bench instance:"
|
|
133
|
-
setup_commit = None
|
|
134
|
-
for line in log_result.stdout.splitlines():
|
|
135
|
-
parts = line.split(":", 1)
|
|
136
|
-
if len(parts) != 2:
|
|
137
|
-
continue
|
|
138
|
-
commit_hash, subject = parts[0].strip(), parts[1].strip()
|
|
139
|
-
if subject.startswith(setup_prefix):
|
|
140
|
-
setup_commit = commit_hash
|
|
141
|
-
break
|
|
142
|
-
|
|
143
|
-
if not setup_commit:
|
|
144
|
-
return False, (
|
|
145
|
-
f"Could not find the original setup commit.\n"
|
|
146
|
-
f"Expected a commit message starting with '{setup_prefix}'."
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
reset = run_git_command(f"git reset --hard {setup_commit}", problem_dir)
|
|
150
|
-
if reset.returncode != 0:
|
|
151
|
-
return False, f"Git reset failed:\n{reset.stderr}"
|
|
152
|
-
|
|
153
|
-
clean = run_git_command("git clean -fd", problem_dir)
|
|
154
|
-
if clean.returncode != 0:
|
|
155
|
-
return False, f"Git clean failed:\n{clean.stderr}"
|
|
156
|
-
|
|
157
|
-
return True, f"Reset to setup commit {setup_commit[:8]}.\nLocal changes discarded and untracked files removed."
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def check_tool_installed(tool_name: str) -> bool:
|
|
161
|
-
"""Check if a CLI tool is available on PATH."""
|
|
162
|
-
return shutil.which(tool_name) is not None
|
|
File without changes
|
{aicodinggym_cli-0.2.0 → aicodinggym_cli-0.4.0}/aicodinggym_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|