repro-cli 0.1.0__tar.gz → 0.2.1__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.
Files changed (25) hide show
  1. repro_cli-0.2.1/LICENSE +21 -0
  2. repro_cli-0.2.1/PKG-INFO +145 -0
  3. repro_cli-0.2.1/README.md +134 -0
  4. {repro_cli-0.1.0 → repro_cli-0.2.1}/pyproject.toml +1 -1
  5. repro_cli-0.2.1/repro/docker/runner.py +290 -0
  6. repro_cli-0.2.1/repro_cli.egg-info/PKG-INFO +145 -0
  7. {repro_cli-0.1.0 → repro_cli-0.2.1}/repro_cli.egg-info/SOURCES.txt +1 -0
  8. repro_cli-0.1.0/PKG-INFO +0 -8
  9. repro_cli-0.1.0/README.md +0 -0
  10. repro_cli-0.1.0/repro/docker/runner.py +0 -182
  11. repro_cli-0.1.0/repro_cli.egg-info/PKG-INFO +0 -8
  12. {repro_cli-0.1.0 → repro_cli-0.2.1}/repro/__init__.py +0 -0
  13. {repro_cli-0.1.0 → repro_cli-0.2.1}/repro/cli.py +0 -0
  14. {repro_cli-0.1.0 → repro_cli-0.2.1}/repro/detector/__init__.py +0 -0
  15. {repro_cli-0.1.0 → repro_cli-0.2.1}/repro/detector/detector.py +0 -0
  16. {repro_cli-0.1.0 → repro_cli-0.2.1}/repro/docker/__init__.py +0 -0
  17. {repro_cli-0.1.0 → repro_cli-0.2.1}/repro/github/__init__.py +0 -0
  18. {repro_cli-0.1.0 → repro_cli-0.2.1}/repro/github/issue.py +0 -0
  19. {repro_cli-0.1.0 → repro_cli-0.2.1}/repro_cli.egg-info/dependency_links.txt +0 -0
  20. {repro_cli-0.1.0 → repro_cli-0.2.1}/repro_cli.egg-info/entry_points.txt +0 -0
  21. {repro_cli-0.1.0 → repro_cli-0.2.1}/repro_cli.egg-info/top_level.txt +0 -0
  22. {repro_cli-0.1.0 → repro_cli-0.2.1}/setup.cfg +0 -0
  23. {repro_cli-0.1.0 → repro_cli-0.2.1}/tests/test_port_parsing.py +0 -0
  24. {repro_cli-0.1.0 → repro_cli-0.2.1}/tests/test_repo_validation.py +0 -0
  25. {repro_cli-0.1.0 → repro_cli-0.2.1}/tests/test_runtime_detection.py +0 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sukhdev Thukral
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: repro-cli
3
+ Version: 0.2.1
4
+ Summary: Disposable Docker environments for any GitHub issue
5
+ Author: Sukhdev Thukral
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Dynamic: license-file
11
+
12
+ # REPRO
13
+ <img width="1279" height="238" alt="Screenshot 2026-08-09 203800" src="https://github.com/user-attachments/assets/f6c12a93-be02-45f3-a237-d310c24ac395" />
14
+
15
+ > Disposable Docker environments for any GitHub issue.
16
+ One command. No setup.
17
+ <br>
18
+ <img width="800" height="456" alt="ezgif com-video-to-gif-converter" src="https://github.com/user-attachments/assets/635990fc-ae46-4995-aded-c74b965c0c1d" />
19
+ <br>
20
+
21
+ ```bash
22
+ pip install repro-cli
23
+ repro https://github.com/org/repo/issues/42
24
+ ```
25
+
26
+ That's it, your terminal boots an isolated sandbox - repo cloned, dependencies installed and IF you have VS Code, it opens automatically **attached directly to the container** so you get a real editor and a single unified terminal, not just a shell. Type `exit` and the container disappears. Your files stay on disk, exactly where they were.
27
+
28
+ ---
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install repro-cli
34
+ ```
35
+
36
+ **Requirements:**
37
+ - Python 3.9+
38
+ - [Docker](https://docs.docker.com/get-docker/) (INSTALLED AND RUNNING)
39
+ - VS Code (optional, but recommended - enables the unified editor/terminal experience below)
40
+
41
+ ---
42
+
43
+ ## Usage
44
+
45
+ ```bash
46
+ # Open a sandbox for any publish GitHub issue
47
+ repro https://github.com/org/repo/issues/123
48
+ ```
49
+
50
+ ```bash
51
+ #Forward a port (e.g. to view a web app running inside the sandbox)
52
+ repro https://github.com/org/repo/issues/123 -p 3000
53
+ ```
54
+
55
+ ```bash
56
+ # Map a different host port to a container port
57
+ repro https://github.com/org/repo/issues/123 -p 8080:3000
58
+ ```
59
+
60
+ ```bash
61
+ # Private repos - pass a GitHub token
62
+ repro https://github.com/org/private-repo/issues/7 --token ghp_xxx
63
+ ```
64
+
65
+ ```bash
66
+ #Or set it once as an env var instead of typing it every single time!
67
+ export DEVSANDBOX_GITHUB_TOKEN=ghp_xxx
68
+ repro https://github.com/org/private-repo/issues/7
69
+ ```
70
+
71
+ ```bash
72
+ # Skip auto-opening an editor
73
+ repro https://github.com/org/repo/issues/123 --no-editor
74
+ ```
75
+
76
+ ```bash
77
+ #Help
78
+ repro --help
79
+ ```
80
+
81
+ ## What it does
82
+
83
+ 1. **Processes** the GitHub issue URL
84
+ 2. **Detects** the repo's runtime i.e Node, Python, Go, Rust, Ruby, PHP, Java, or a devcontainer based on whats actually in the repo.
85
+ 3. **Spins up** a disposable, isolated Docker container with the right base image.
86
+ 4. **Bind-mounts** a real folder on your machine into the container, so nothing is trapped and files persist after you exit
87
+ 5. **Clones** the repo directly into that shared folder and install dependencies
88
+ 6. **Attaches VS Code directly to the running container** (auto-installing the Dev Containers extension if needed) - one window, one terminal, running *inside* the sandbox.
89
+ 7. *Destroys* the container the moment you exit. Your edited files remain exactly where they were.
90
+
91
+ ---
92
+
93
+ ## The unified editor experience
94
+
95
+ - **Real editor**: VS Code attaches directly into the container, so its built-in terminal *is* the sandbox shell
96
+ - **Real persistence**: because the folder is bind-mounted, not just cloned inside the container, your work survives after `exit`.
97
+ If VS Code isn't installed, `repro` falls back gracefully, you still get a real folder path printed at the end, ready to open in whatever editor you use.
98
+
99
+ ---
100
+
101
+ ## Supported runtimes
102
+ | File detected | Runtime | Base image |
103
+ |---|---|---|
104
+ | `package.json` | Node.js | `node:20-alpine` |
105
+ | `yarn.lock` | Node.js (yarn) | `node:20-alpine` |
106
+ | `pnpm-lock.yaml` | Node.js (pnpm) | `node:20-alpine` |
107
+ | `requirements.txt` | Python | `python:3.12-slim` |
108
+ | `pyproject.toml` | Python | `python:3.12-slim` |
109
+ | `Pipfile` | Python (pipenv) | `python:3.12-slim` |
110
+ | `go.mod` | Go | `golang:1.21-alpine` |
111
+ | `Cargo.toml` | Rust | `rust:1.75-slim` |
112
+ | `Gemfile` | Ruby | `ruby:3.3-slim` |
113
+ | `composer.json` | PHP | `php:8.3-cli` |
114
+ | `pom.xml` | Java (Maven) | `maven:3.9-eclipse-temurin-21` |
115
+ | `build.gradle` | Java (Gradle) | `gradle:8-jdk21` |
116
+ | `.devcontainer` | devcontainer | MS universal image |
117
+ | *(none matched)* | Universal fallback | `ubuntu:22.04` |
118
+
119
+ ---
120
+
121
+ ## The GitHub App
122
+
123
+ Install [**repro-sandbox-bot**](https://github.com/apps/repro-sandbox-bot) on any repo and it automatically comments the exact `repro` command on every new issue, so that contributors never have to think about setup.
124
+
125
+ ```
126
+ 🏖 Open this issue in DevSandbox
127
+
128
+ Spin up a disposable Docker environment with this repo already checked out:
129
+
130
+ repro https://github.com/org/repo/issues/42
131
+
132
+ Don't have repro yet? Install it here.
133
+
134
+ Comment /sandbox on any issue to get this again.
135
+ ```
136
+
137
+ You can also trigger it on demand by commenting `/sandbox` on any issue. The bot runs 24/7, independent of your machine.
138
+
139
+ ---
140
+ ## Contributing
141
+ PRs welcome. Open an issue first for anything beyond small fixes - and yes, you can use `repro` itself to work on `repro`.
142
+
143
+ ## License
144
+
145
+ MIT
@@ -0,0 +1,134 @@
1
+ # REPRO
2
+ <img width="1279" height="238" alt="Screenshot 2026-08-09 203800" src="https://github.com/user-attachments/assets/f6c12a93-be02-45f3-a237-d310c24ac395" />
3
+
4
+ > Disposable Docker environments for any GitHub issue.
5
+ One command. No setup.
6
+ <br>
7
+ <img width="800" height="456" alt="ezgif com-video-to-gif-converter" src="https://github.com/user-attachments/assets/635990fc-ae46-4995-aded-c74b965c0c1d" />
8
+ <br>
9
+
10
+ ```bash
11
+ pip install repro-cli
12
+ repro https://github.com/org/repo/issues/42
13
+ ```
14
+
15
+ That's it, your terminal boots an isolated sandbox - repo cloned, dependencies installed and IF you have VS Code, it opens automatically **attached directly to the container** so you get a real editor and a single unified terminal, not just a shell. Type `exit` and the container disappears. Your files stay on disk, exactly where they were.
16
+
17
+ ---
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install repro-cli
23
+ ```
24
+
25
+ **Requirements:**
26
+ - Python 3.9+
27
+ - [Docker](https://docs.docker.com/get-docker/) (INSTALLED AND RUNNING)
28
+ - VS Code (optional, but recommended - enables the unified editor/terminal experience below)
29
+
30
+ ---
31
+
32
+ ## Usage
33
+
34
+ ```bash
35
+ # Open a sandbox for any publish GitHub issue
36
+ repro https://github.com/org/repo/issues/123
37
+ ```
38
+
39
+ ```bash
40
+ #Forward a port (e.g. to view a web app running inside the sandbox)
41
+ repro https://github.com/org/repo/issues/123 -p 3000
42
+ ```
43
+
44
+ ```bash
45
+ # Map a different host port to a container port
46
+ repro https://github.com/org/repo/issues/123 -p 8080:3000
47
+ ```
48
+
49
+ ```bash
50
+ # Private repos - pass a GitHub token
51
+ repro https://github.com/org/private-repo/issues/7 --token ghp_xxx
52
+ ```
53
+
54
+ ```bash
55
+ #Or set it once as an env var instead of typing it every single time!
56
+ export DEVSANDBOX_GITHUB_TOKEN=ghp_xxx
57
+ repro https://github.com/org/private-repo/issues/7
58
+ ```
59
+
60
+ ```bash
61
+ # Skip auto-opening an editor
62
+ repro https://github.com/org/repo/issues/123 --no-editor
63
+ ```
64
+
65
+ ```bash
66
+ #Help
67
+ repro --help
68
+ ```
69
+
70
+ ## What it does
71
+
72
+ 1. **Processes** the GitHub issue URL
73
+ 2. **Detects** the repo's runtime i.e Node, Python, Go, Rust, Ruby, PHP, Java, or a devcontainer based on whats actually in the repo.
74
+ 3. **Spins up** a disposable, isolated Docker container with the right base image.
75
+ 4. **Bind-mounts** a real folder on your machine into the container, so nothing is trapped and files persist after you exit
76
+ 5. **Clones** the repo directly into that shared folder and install dependencies
77
+ 6. **Attaches VS Code directly to the running container** (auto-installing the Dev Containers extension if needed) - one window, one terminal, running *inside* the sandbox.
78
+ 7. *Destroys* the container the moment you exit. Your edited files remain exactly where they were.
79
+
80
+ ---
81
+
82
+ ## The unified editor experience
83
+
84
+ - **Real editor**: VS Code attaches directly into the container, so its built-in terminal *is* the sandbox shell
85
+ - **Real persistence**: because the folder is bind-mounted, not just cloned inside the container, your work survives after `exit`.
86
+ If VS Code isn't installed, `repro` falls back gracefully, you still get a real folder path printed at the end, ready to open in whatever editor you use.
87
+
88
+ ---
89
+
90
+ ## Supported runtimes
91
+ | File detected | Runtime | Base image |
92
+ |---|---|---|
93
+ | `package.json` | Node.js | `node:20-alpine` |
94
+ | `yarn.lock` | Node.js (yarn) | `node:20-alpine` |
95
+ | `pnpm-lock.yaml` | Node.js (pnpm) | `node:20-alpine` |
96
+ | `requirements.txt` | Python | `python:3.12-slim` |
97
+ | `pyproject.toml` | Python | `python:3.12-slim` |
98
+ | `Pipfile` | Python (pipenv) | `python:3.12-slim` |
99
+ | `go.mod` | Go | `golang:1.21-alpine` |
100
+ | `Cargo.toml` | Rust | `rust:1.75-slim` |
101
+ | `Gemfile` | Ruby | `ruby:3.3-slim` |
102
+ | `composer.json` | PHP | `php:8.3-cli` |
103
+ | `pom.xml` | Java (Maven) | `maven:3.9-eclipse-temurin-21` |
104
+ | `build.gradle` | Java (Gradle) | `gradle:8-jdk21` |
105
+ | `.devcontainer` | devcontainer | MS universal image |
106
+ | *(none matched)* | Universal fallback | `ubuntu:22.04` |
107
+
108
+ ---
109
+
110
+ ## The GitHub App
111
+
112
+ Install [**repro-sandbox-bot**](https://github.com/apps/repro-sandbox-bot) on any repo and it automatically comments the exact `repro` command on every new issue, so that contributors never have to think about setup.
113
+
114
+ ```
115
+ 🏖 Open this issue in DevSandbox
116
+
117
+ Spin up a disposable Docker environment with this repo already checked out:
118
+
119
+ repro https://github.com/org/repo/issues/42
120
+
121
+ Don't have repro yet? Install it here.
122
+
123
+ Comment /sandbox on any issue to get this again.
124
+ ```
125
+
126
+ You can also trigger it on demand by commenting `/sandbox` on any issue. The bot runs 24/7, independent of your machine.
127
+
128
+ ---
129
+ ## Contributing
130
+ PRs welcome. Open an issue first for anything beyond small fixes - and yes, you can use `repro` itself to work on `repro`.
131
+
132
+ ## License
133
+
134
+ MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name="repro-cli"
7
- version="0.1.0"
7
+ version="0.2.1"
8
8
  description = "Disposable Docker environments for any GitHub issue"
9
9
  readme="README.md"
10
10
  requires-python=">=3.9"
@@ -0,0 +1,290 @@
1
+ import os
2
+ import shutil
3
+ import re
4
+ import subprocess
5
+ import tempfile
6
+ import threading
7
+ import time
8
+
9
+ class DockerNotFoundError(Exception):
10
+ pass
11
+
12
+ class DockerNotRunningError(Exception):
13
+ pass
14
+
15
+ class InvalidPortError(Exception):
16
+ pass
17
+
18
+ class InvalidRepoError(Exception):
19
+ pass
20
+
21
+ SAFE_NAME = re.compile(r"^[A-Za-z0-9._-]+$")
22
+
23
+ def check_docker_available():
24
+ """Verify Docker CLI exists and the daemon is reachable before trying anythg"""
25
+ if shutil.which("docker") is None:
26
+ raise DockerNotFoundError(
27
+ "Docker is not installed or not on your PATH.\n"
28
+ " Install it from: https://docs.docker.com/get-docker/"
29
+ )
30
+
31
+ result = subprocess.run(["docker", "info"], capture_output=True, text=True)
32
+ if result.returncode != 0:
33
+ raise DockerNotRunningError(
34
+ "Docker is installed but the daemon isnt running.\n"
35
+ " Start Docker Desktop and wait for the icon to go steady, then try again."
36
+ )
37
+
38
+
39
+ def validate_repo_identifiers(owner: str, repo:str):
40
+ """ Guard against shell injection -> owner/repo gets interpolated into a shell command, so anything outside GitHub's allowed charst is rejected
41
+ before it even reaches da subprocess"""
42
+ for label, value in (("owner", owner), ("repo", repo)):
43
+ if not SAFE_NAME.match(value):
44
+ raise InvalidRepoError(f'invalid {label} "{value}" - contains disallowed characters')
45
+ if value in (".", ".."):
46
+ raise InvalidRepoError(f'invalid {label} "{value}"')
47
+ if value.startswith("."):
48
+ raise InvalidRepoError(f'invalid {label} "{value}" - cannot start with a dot')
49
+
50
+
51
+ def parse_port_spec(spec: str) -> tuple[int, int]:
52
+ """ Parse a --port value into (host_port, container_port).
53
+ Accepts "3000" (same both sides) or "8080:3000" (host:container).
54
+ """
55
+ parts = spec.split(":")
56
+
57
+ if len(parts) == 1:
58
+ host, container = parts[0], parts[0]
59
+ elif len(parts) == 2:
60
+ host, container = parts
61
+ else:
62
+ raise InvalidPortError(f'invalid port spec "{spec}" - use PORT or HOST:CONTAINER')
63
+
64
+ for label, value in (("host", host), ("container", container)):
65
+ if not value.isdigit():
66
+ raise InvalidPortError(f'invalid {label} port "{value}" in "{spec}" - must be a number')
67
+ port_num = int(value)
68
+ if not(1 <= port_num <= 65535):
69
+ raise InvalidPortError(f'{label} port {port_num} out of range (1-65535)')
70
+
71
+ return int(host), int(container)
72
+
73
+ DEVCONTAINERS_EXT_ID = "ms-vscode-remote.remote-containers"
74
+
75
+ def has_devcontainers_extension() -> bool:
76
+ """Check if VS Code's DEV containers extension is installed"""
77
+ try:
78
+ result = subprocess.run(
79
+ ["code", "--list-extensions"],
80
+ capture_output=True, text=True, timeout=5,
81
+ shell=(os.name == "nt"),
82
+ )
83
+ return DEVCONTAINERS_EXT_ID in result.stdout
84
+ except Exception:
85
+ return False
86
+
87
+ def install_devcontainers_extension() -> bool:
88
+ print("📦 Installing VS Code's 'Dev Containers' extension (one-time setup)...")
89
+ try:
90
+ result = subprocess.run(
91
+ ["code", "--install-extension", DEVCONTAINERS_EXT_ID],
92
+ capture_output=True, text=True, timeout=60,
93
+ shell=(os.name=="nt")
94
+ )
95
+ if result.returncode == 0:
96
+ print("✅ Dev Containers extension installed.\n")
97
+ return True
98
+ print("⚠️ Couldn't auto-install the extension. Falling back to folder mode.\n")
99
+ return False
100
+ except Exception as e:
101
+ print(f"⚠️ Couldn't auto-install the extension ({e}). Falling back to folder mode.\n")
102
+ return False
103
+
104
+ def attach_vscode_to_container(container_name: str, work_dir:str):
105
+ for _ in range(20):
106
+ result = subprocess.run(
107
+ ["docker", "inspect", "--format", "{{.State.Running}}", container_name],
108
+ capture_output=True, text=True,
109
+ )
110
+ if result.returncode == 0 and result.stdout.strip() == "true":
111
+ break
112
+ time.sleep(0.5)
113
+ else:
114
+ return
115
+
116
+ id_result = subprocess.run(
117
+ ["docker", "inspect", "--format", "{{.Id}}", container_name],
118
+ capture_output=True, text=True,
119
+ )
120
+ if id_result.returncode != 0:
121
+ return
122
+
123
+ container_id = id_result.stdout.strip()
124
+ hex_id = container_id.encode().hex()
125
+ uri = f"vscode-remote://attached-container+{hex_id}{work_dir}"
126
+
127
+ try:
128
+ subprocess.Popen(["code", "--folder-uri", uri], shell=(os.name=="nt"))
129
+ except Exception:
130
+ pass
131
+
132
+ def run_sandbox(issue: dict, runtime: dict, ports: list =None, token: str = None, no_editor: bool = False):
133
+ check_docker_available()
134
+ ports = ports or []
135
+
136
+ owner = issue["owner"]
137
+ repo = issue["repo"]
138
+ number = issue["number"]
139
+
140
+ validate_repo_identifiers(owner, repo)
141
+
142
+ if not isinstance(number, int):
143
+ raise InvalidRepoError("issue number must be an integer")
144
+
145
+ clone_url_public = f"https://github.com/{owner}/{repo}.git"
146
+ work_dir = f"/sandbox/{repo}"
147
+ container_name = f"repro-{owner}-{repo}-{number}"
148
+
149
+ subprocess.run(["docker", "rm", "-f", container_name], capture_output=True)
150
+
151
+ host_dir = tempfile.mkdtemp(prefix=f"repro-{owner}-{repo}-{number}-")
152
+
153
+ print(f"📁 Sandbox files will live at: {host_dir}")
154
+ print(" (this folder survives after you exit - edit here with your own editor)\n")
155
+
156
+ editor_mode = None
157
+ if not no_editor and shutil.which("code"):
158
+ if not has_devcontainers_extension():
159
+ install_devcontainers_extension()
160
+
161
+ if has_devcontainers_extension():
162
+ editor_mode="attached"
163
+ print("🖊️ VS Code will attach directly to the container once it's up...")
164
+ print(" (its built-in terminal will run INSIDE the sandbox — one window, no split)\n")
165
+ else:
166
+ editor_mode= "folder"
167
+ print("🖊️ Opening the folder in VS Code (editing only)...")
168
+ print(" 💡 Install the 'Dev Containers' extension for a fully unified terminal.\n")
169
+ elif not no_editor:
170
+ print("💡 No auto-attach editor detected (currently supports VS Code :( .\n")
171
+ print(" Open the folder above in whatever editor you use - its a real folder on your machine!! .\n")
172
+
173
+ startup_script = build_startup_script(clone_url_public, work_dir, runtime["install"], number, ports, has_token=bool(token))
174
+
175
+ args = [
176
+ "docker", "run",
177
+ "--rm",
178
+ "-i",
179
+ "-t",
180
+ "--name", container_name,
181
+ "--hostname", f"sandbox-issue-{number}",
182
+ "-v", f"{host_dir}:{work_dir}",
183
+ "-e", f"ISSUE_NUMBER={number}",
184
+ "-e", f"REPO={owner}/{repo}",
185
+ "-e", "TERM=xterm-256color",
186
+ ]
187
+
188
+ for host_port, container_port in ports:
189
+ args += ["-p", f"127.0.0.1:{host_port}:{container_port}"]
190
+
191
+ env_file_path = None
192
+ if token:
193
+ fd, env_file_path = tempfile.mkstemp(prefix="repro-", suffix=".env")
194
+ try:
195
+ os.chmod(env_file_path, 0o600)
196
+ with os.fdopen(fd, "w") as f:
197
+ f.write(f"GIT_TOKEN={token}\n")
198
+ args+=["--env-file", env_file_path]
199
+ except Exception:
200
+ if os.path.exists(env_file_path):
201
+ os.remove(env_file_path)
202
+ raise
203
+
204
+ args += [runtime["image"], "sh", "-c", startup_script]
205
+
206
+ print(f"🚀 Starting sandbox for {owner}/{repo} issue #{number}")
207
+ print(" (container will be deleted automatically when you exit)\n")
208
+
209
+ env = os.environ.copy()
210
+ env["DOCKER_CLI_HINTS"] = "false"
211
+
212
+ if editor_mode == "attached":
213
+ threading.Thread(
214
+ target=attach_vscode_to_container,
215
+ args=(container_name, work_dir),
216
+ daemon=True,
217
+ ).start()
218
+ elif editor_mode == "folder":
219
+ try:
220
+ subprocess.Popen(["code", host_dir], shell=(os.name=="nt"))
221
+ except Exception as e:
222
+ print(f"⚠️ Couldn't auto-open VS Code: {e}\n")
223
+
224
+ try:
225
+ try:
226
+ result = subprocess.run(args, env=env)
227
+ except KeyboardInterrupt:
228
+ print("Interrupted - cleaning up the sandbox...")
229
+ return
230
+
231
+ if result.returncode not in (0,130):
232
+ if result.returncode == 125:
233
+ print(f"\n⚠️ Docker couldn't start the container (exit 125) — this often means"
234
+ f"the image failed to pull. Check your internet connection and try again.")
235
+ else:
236
+ print(f"Sandbox exited with code {result.returncode} - check the output above.")
237
+ return
238
+
239
+ print("\n Sandbox destroyed. Back to reality.")
240
+
241
+ finally:
242
+ if env_file_path and os.path.exists(env_file_path):
243
+ os.remove(env_file_path)
244
+ print(f"📁 Your files are still here: {host_dir}")
245
+ print(" (delete manually whenever you're done)")
246
+
247
+ def build_startup_script(clone_url_public, work_dir, install_cmd, issue_number, ports, has_token: bool = False) -> str:
248
+ install_step = (
249
+ f'echo "Installing dependencies....." && {install_cmd}'
250
+ if install_cmd
251
+ else 'echo "No install step, dropping into shell... "'
252
+ )
253
+
254
+ host_and_path = clone_url_public[len("https://"):]
255
+ clone_cmd = (
256
+ f'if [ -n "$GIT_TOKEN" ]; then '
257
+ f'CLONE_URL="https://${{GIT_TOKEN}}@{host_and_path}";'
258
+ f'else CLONE_URL="{clone_url_public}"; fi && '
259
+ f'git clone --depth=1 "$CLONE_URL" {work_dir}'
260
+ f'|| (echo "❌ Clone failed — check the repo exists, the URL is correct, and your token has access." && exit 1)'
261
+ )
262
+
263
+ lines = [
264
+ "set -e",
265
+ "which git > /dev/null 2>&1 || (apk add --no-cache git curl 2>/dev/null || (apt-get update -qq && apt-get install -y -qq git curl))",
266
+ 'echo ""',
267
+ 'echo "=================================="',
268
+ f'echo " Repro - Issue #{issue_number}"',
269
+ 'echo "=================================="',
270
+ 'echo ""',
271
+ f'echo "Cloning repo..."',
272
+ clone_cmd,
273
+ f"cd {work_dir} && git remote set-url origin {clone_url_public} && unset GIT_TOKEN",
274
+ install_step,
275
+ 'echo ""',
276
+ f'echo "Ready! You are in: {work_dir}"',
277
+ f'echo "Working on issue #{issue_number}"',
278
+ ]
279
+
280
+ if ports:
281
+ lines.append('echo ""')
282
+ for host_port, container_port in ports:
283
+ lines.append(f'echo "Port {container_port} forwarded -> http://localhost:{host_port}"')
284
+ lines.append('echo " Start your app inside this shell (e.g. npm start / node server.js) to use it."')
285
+
286
+ lines.append('echo ""')
287
+ lines.append(f"cd {work_dir} && exec sh")
288
+
289
+
290
+ return " && ".join(lines)
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: repro-cli
3
+ Version: 0.2.1
4
+ Summary: Disposable Docker environments for any GitHub issue
5
+ Author: Sukhdev Thukral
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Dynamic: license-file
11
+
12
+ # REPRO
13
+ <img width="1279" height="238" alt="Screenshot 2026-08-09 203800" src="https://github.com/user-attachments/assets/f6c12a93-be02-45f3-a237-d310c24ac395" />
14
+
15
+ > Disposable Docker environments for any GitHub issue.
16
+ One command. No setup.
17
+ <br>
18
+ <img width="800" height="456" alt="ezgif com-video-to-gif-converter" src="https://github.com/user-attachments/assets/635990fc-ae46-4995-aded-c74b965c0c1d" />
19
+ <br>
20
+
21
+ ```bash
22
+ pip install repro-cli
23
+ repro https://github.com/org/repo/issues/42
24
+ ```
25
+
26
+ That's it, your terminal boots an isolated sandbox - repo cloned, dependencies installed and IF you have VS Code, it opens automatically **attached directly to the container** so you get a real editor and a single unified terminal, not just a shell. Type `exit` and the container disappears. Your files stay on disk, exactly where they were.
27
+
28
+ ---
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install repro-cli
34
+ ```
35
+
36
+ **Requirements:**
37
+ - Python 3.9+
38
+ - [Docker](https://docs.docker.com/get-docker/) (INSTALLED AND RUNNING)
39
+ - VS Code (optional, but recommended - enables the unified editor/terminal experience below)
40
+
41
+ ---
42
+
43
+ ## Usage
44
+
45
+ ```bash
46
+ # Open a sandbox for any publish GitHub issue
47
+ repro https://github.com/org/repo/issues/123
48
+ ```
49
+
50
+ ```bash
51
+ #Forward a port (e.g. to view a web app running inside the sandbox)
52
+ repro https://github.com/org/repo/issues/123 -p 3000
53
+ ```
54
+
55
+ ```bash
56
+ # Map a different host port to a container port
57
+ repro https://github.com/org/repo/issues/123 -p 8080:3000
58
+ ```
59
+
60
+ ```bash
61
+ # Private repos - pass a GitHub token
62
+ repro https://github.com/org/private-repo/issues/7 --token ghp_xxx
63
+ ```
64
+
65
+ ```bash
66
+ #Or set it once as an env var instead of typing it every single time!
67
+ export DEVSANDBOX_GITHUB_TOKEN=ghp_xxx
68
+ repro https://github.com/org/private-repo/issues/7
69
+ ```
70
+
71
+ ```bash
72
+ # Skip auto-opening an editor
73
+ repro https://github.com/org/repo/issues/123 --no-editor
74
+ ```
75
+
76
+ ```bash
77
+ #Help
78
+ repro --help
79
+ ```
80
+
81
+ ## What it does
82
+
83
+ 1. **Processes** the GitHub issue URL
84
+ 2. **Detects** the repo's runtime i.e Node, Python, Go, Rust, Ruby, PHP, Java, or a devcontainer based on whats actually in the repo.
85
+ 3. **Spins up** a disposable, isolated Docker container with the right base image.
86
+ 4. **Bind-mounts** a real folder on your machine into the container, so nothing is trapped and files persist after you exit
87
+ 5. **Clones** the repo directly into that shared folder and install dependencies
88
+ 6. **Attaches VS Code directly to the running container** (auto-installing the Dev Containers extension if needed) - one window, one terminal, running *inside* the sandbox.
89
+ 7. *Destroys* the container the moment you exit. Your edited files remain exactly where they were.
90
+
91
+ ---
92
+
93
+ ## The unified editor experience
94
+
95
+ - **Real editor**: VS Code attaches directly into the container, so its built-in terminal *is* the sandbox shell
96
+ - **Real persistence**: because the folder is bind-mounted, not just cloned inside the container, your work survives after `exit`.
97
+ If VS Code isn't installed, `repro` falls back gracefully, you still get a real folder path printed at the end, ready to open in whatever editor you use.
98
+
99
+ ---
100
+
101
+ ## Supported runtimes
102
+ | File detected | Runtime | Base image |
103
+ |---|---|---|
104
+ | `package.json` | Node.js | `node:20-alpine` |
105
+ | `yarn.lock` | Node.js (yarn) | `node:20-alpine` |
106
+ | `pnpm-lock.yaml` | Node.js (pnpm) | `node:20-alpine` |
107
+ | `requirements.txt` | Python | `python:3.12-slim` |
108
+ | `pyproject.toml` | Python | `python:3.12-slim` |
109
+ | `Pipfile` | Python (pipenv) | `python:3.12-slim` |
110
+ | `go.mod` | Go | `golang:1.21-alpine` |
111
+ | `Cargo.toml` | Rust | `rust:1.75-slim` |
112
+ | `Gemfile` | Ruby | `ruby:3.3-slim` |
113
+ | `composer.json` | PHP | `php:8.3-cli` |
114
+ | `pom.xml` | Java (Maven) | `maven:3.9-eclipse-temurin-21` |
115
+ | `build.gradle` | Java (Gradle) | `gradle:8-jdk21` |
116
+ | `.devcontainer` | devcontainer | MS universal image |
117
+ | *(none matched)* | Universal fallback | `ubuntu:22.04` |
118
+
119
+ ---
120
+
121
+ ## The GitHub App
122
+
123
+ Install [**repro-sandbox-bot**](https://github.com/apps/repro-sandbox-bot) on any repo and it automatically comments the exact `repro` command on every new issue, so that contributors never have to think about setup.
124
+
125
+ ```
126
+ 🏖 Open this issue in DevSandbox
127
+
128
+ Spin up a disposable Docker environment with this repo already checked out:
129
+
130
+ repro https://github.com/org/repo/issues/42
131
+
132
+ Don't have repro yet? Install it here.
133
+
134
+ Comment /sandbox on any issue to get this again.
135
+ ```
136
+
137
+ You can also trigger it on demand by commenting `/sandbox` on any issue. The bot runs 24/7, independent of your machine.
138
+
139
+ ---
140
+ ## Contributing
141
+ PRs welcome. Open an issue first for anything beyond small fixes - and yes, you can use `repro` itself to work on `repro`.
142
+
143
+ ## License
144
+
145
+ MIT
@@ -1,3 +1,4 @@
1
+ LICENSE
1
2
  README.md
2
3
  pyproject.toml
3
4
  repro/__init__.py
repro_cli-0.1.0/PKG-INFO DELETED
@@ -1,8 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: repro-cli
3
- Version: 0.1.0
4
- Summary: Disposable Docker environments for any GitHub issue
5
- Author: Sukhdev Thukral
6
- License-Expression: MIT
7
- Requires-Python: >=3.9
8
- Description-Content-Type: text/markdown
repro_cli-0.1.0/README.md DELETED
File without changes
@@ -1,182 +0,0 @@
1
- import os
2
- import shutil
3
- import re
4
- import subprocess
5
- import tempfile
6
-
7
- class DockerNotFoundError(Exception):
8
- pass
9
-
10
- class DockerNotRunningError(Exception):
11
- pass
12
-
13
- class InvalidPortError(Exception):
14
- pass
15
-
16
- class InvalidRepoError(Exception):
17
- pass
18
-
19
- SAFE_NAME = re.compile(r"^[A-Za-z0-9._-]+$")
20
-
21
- def check_docker_available():
22
- """Verify Docker CLI exists and the daemon is reachable before trying anythg"""
23
- if shutil.which("docker") is None:
24
- raise DockerNotFoundError(
25
- "Docker is not installed or not on your PATH.\n"
26
- " Install it from: https://docs.docker.com/get-docker/"
27
- )
28
-
29
- result = subprocess.run(["docker", "info"], capture_output=True, text=True)
30
- if result.returncode != 0:
31
- raise DockerNotRunningError(
32
- "Docker is installed but the daemon isnt running.\n"
33
- " Start Docker Desktop and wait for the icon to go steady, then try again."
34
- )
35
-
36
- def validate_repo_identifiers(owner: str, repo:str):
37
- """ Guard against shell injection -> owner/repo gets interpolated into a shell command, so anything outside GitHub's allowed charst is rejected
38
- before it even reaches da subprocess"""
39
- for label, value in (("owner", owner), ("repo", repo)):
40
- if not SAFE_NAME.match(value):
41
- raise InvalidRepoError(f'invalid {label} "{value}" - contains disallowed characters')
42
- if value in (".", ".."):
43
- raise InvalidRepoError(f'invalid {label} "{value}"')
44
- if value.startswith("."):
45
- raise InvalidRepoError(f'invalid {label} "{value}" - cannot start with a dot')
46
-
47
-
48
- def parse_port_spec(spec: str) -> tuple[int, int]:
49
- """ Parse a --port value into (host_port, container_port).
50
- Accepts "3000" (same both sides) or "8080:3000" (host:container).
51
- """
52
- parts = spec.split(":")
53
-
54
- if len(parts) == 1:
55
- host, container = parts[0], parts[0]
56
- elif len(parts) == 2:
57
- host, container = parts
58
- else:
59
- raise InvalidPortError(f'invalid port spec "{spec}" - use PORT or HOST:CONTAINER')
60
-
61
- for label, value in (("host", host), ("container", container)):
62
- if not value.isdigit():
63
- raise InvalidPortError(f'invalid {label} port "{value}" in "{spec}" - must be a number')
64
- port_num = int(value)
65
- if not(1 <= port_num <= 65535):
66
- raise InvalidPortError(f'{label} port {port_num} out of range (1-65535)')
67
-
68
- return int(host), int(container)
69
-
70
- def run_sandbox(issue: dict, runtime: dict, ports: list =None, token: str = None):
71
- check_docker_available()
72
- ports = ports or []
73
-
74
- owner = issue["owner"]
75
- repo = issue["repo"]
76
- number = issue["number"]
77
-
78
- validate_repo_identifiers(owner, repo)
79
-
80
- if not isinstance(number, int):
81
- raise InvalidRepoError("issue number must be an integer")
82
-
83
- clone_url_public = f"https://github.com/{owner}/{repo}.git"
84
- clone_url_auth = f"https://{token}@github.com/{owner}/{repo}.git" if token else clone_url_public
85
-
86
- work_dir = f"/sandbox/{repo}"
87
- container_name = f"repro-{owner}-{repo}-{number}"
88
-
89
- startup_script = build_startup_script(clone_url_auth, clone_url_public, work_dir, runtime["install"], number, ports, has_token=bool(token))
90
-
91
- args = [
92
- "docker", "run",
93
- "--rm",
94
- "-i",
95
- "-t",
96
- "--name", container_name,
97
- "--hostname", f"sandbox-issue-{number}",
98
- "-e", f"ISSUE_NUMBER={number}",
99
- "-e", f"REPO={owner}/{repo}",
100
- "-e", "TERM=xterm-256color",
101
- ]
102
-
103
- for host_port, container_port in ports:
104
- args += ["-p", f"127.0.0.1:{host_port}:{container_port}"]
105
-
106
- env_file_path = None
107
- if token:
108
- #writing the toekn to a temp file passed via --env-fle instead of -e/embedded-in-command
109
- fd, env_file_path = tempfile.mkstemp(prefix="repro-", suffix=".env")
110
- try:
111
- os.chmod(env_file_path, 0o600)
112
- with os.fdopen(fd, "w") as f:
113
- f.write(f"GIT_TOKEN={token}\n")
114
- except Exception:
115
- if os.path.exists(env_file_path):
116
- os.remove(env_file_path)
117
- raise
118
-
119
- args += [runtime["image"], "sh", "-c", startup_script]
120
-
121
- print(f"Starting sandbox for {owner}/{repo} issue #{number}")
122
- print(" (container will be deleted automatically when you exit)\n")
123
-
124
- env = os.environ.copy()
125
- env["DOCKER_CLI_HINTS"] = "false"
126
-
127
- try:
128
- result = subprocess.run(args, env=env)
129
- except KeyboardInterrupt:
130
- print("Interrupted - cleaning up the sandbox...")
131
- return
132
-
133
- if result.returncode not in (0,130):
134
- print(f"Sandbox exited with code {result.returncode} - check the output above.")
135
- return
136
-
137
- print("\n Sandbox destroyed. Back to reality.")
138
-
139
- def build_startup_script(clone_url_auth, clone_url_public, work_dir, install_cmd, issue_number, ports) -> str:
140
- install_step = (
141
- f'echo "Installing dependencies....." && {install_cmd}'
142
- if install_cmd
143
- else 'echo "No install step, dropping into shell... "'
144
- )
145
-
146
- host_and_path = clone_url_public[len("https://"):]
147
- clone_cmd = (
148
- f'if [ -n "$GIT_TOEKN" ]; then '
149
- f'CLONE_URL="https://${{GIT_TOKEN}}@{host_and_path}";'
150
- f'else CLONE_URL="{clone_url_public}"; fi && '
151
- f'git clone --depth=1 "$CLONE_URL" {work_dir} 2>&1 | tail -5'
152
- )
153
-
154
- lines = [
155
- "set -e",
156
- "which git > /dev/null 2>&1 || (apk add --no-cache git curl)",
157
- 'echo ""',
158
- 'echo "=================================="',
159
- f'echo " DevSandbox - Issue #{issue_number}"',
160
- 'echo "=================================="',
161
- 'echo ""',
162
- f'echo "Cloning repo..."',
163
- clone_cmd,
164
- f"git clone --depth=1 {clone_url_auth} {work_dir} 2>&1 | tail -5",
165
- f"cd {work_dir} && git remote set-url origin {clone_url_public} && unset GIT_TOKEN",
166
- install_step,
167
- 'echo ""',
168
- f'echo "Ready! You are in: {work_dir}"',
169
- f'echo "Working on issue #{issue_number}"',
170
- ]
171
-
172
- if ports:
173
- lines.append('echo ""')
174
- for host_port, container_port in ports:
175
- lines.append(f'echo "Port {container_port} forwarded -> http://localhost:{host_port}"')
176
- lines.append('echo " Start your app inside this shell (e.g. npm start / node server.js) to use it."')
177
-
178
- lines.append('echo ""')
179
- lines.append(f"cd {work_dir} && exec sh")
180
-
181
-
182
- return " && ".join(lines)
@@ -1,8 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: repro-cli
3
- Version: 0.1.0
4
- Summary: Disposable Docker environments for any GitHub issue
5
- Author: Sukhdev Thukral
6
- License-Expression: MIT
7
- Requires-Python: >=3.9
8
- Description-Content-Type: text/markdown
File without changes
File without changes
File without changes