repro-cli 0.1.0__tar.gz → 0.2.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.
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: repro-cli
3
+ Version: 0.2.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
9
+
10
+ # REPRO
11
+
12
+ > Disposable Docker environments for any GitHub issue.
13
+ One command. No setup.
14
+
15
+ ```bash
16
+ pip install repro-cli
17
+ repro https://github.com/org/repo/issues/42
18
+ ```
19
+
20
+ 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.
21
+
22
+ ---
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install repro-cli
28
+ ```
29
+
30
+ **Requirements:**
31
+ - Python 3.9+
32
+ - [Docker](https://docs.docker.com/get-docker/) (INSTALLED AND RUNNING)
33
+ - VS Code (optional, but recommended - enables the unified editor/terminal experience below)
34
+
35
+ ---
36
+
37
+ ## Usage
38
+
39
+ ```bash
40
+ # Open a sandbox for any publish GitHub issue
41
+ repro https://github.com/org/repo/issues/123
42
+ ```
43
+
44
+ ```bash
45
+ #Forward a port (e.g. to view a web app running inside the sandbox)
46
+ repro https://github.com/org/repo/issues/123 -p 3000
47
+ ```
48
+
49
+ ```bash
50
+ # Map a different host port to a container port
51
+ repro https://github.com/org/repo/issues/123 -p 8080:3000
52
+ ```
53
+
54
+ ```bash
55
+ # Private repos - pass a GitHub token
56
+ repro https://github.com/org/private-repo/issues/7 --token ghp_xxx
57
+ ```
58
+
59
+ ```bash
60
+ #Or set it once as an env var instead of typing it every single time!
61
+ export DEVSANDBOX_GITHUB_TOKEN=ghp_xxx
62
+ repro https://github.com/org/private-repo/issues/7
63
+ ```
64
+
65
+ ```bash
66
+ # Skip auto-opening an editor
67
+ repro https://github.com/org/repo/issues/123 --no-editor
68
+ ```
69
+
70
+ ```bash
71
+ #Help
72
+ repro --help
73
+ ```
74
+
75
+ ## What it does
76
+
77
+ 1. **Parses** the GitHub issue URL
78
+ 2. **Detects** the repo's runtime - Node, Python, Go, Rust, Ruby, PHP, Java, or a devcontainer, based on whats actually in the repo.
79
+ 3. **Spins up** a disposable, isolated Docker container with the right base image.
80
+ 4. **Bind-mounts** a real folder on your machine into the container, so nothing is trapped - files persist after you exit
81
+ 5. **Clones** the repo directly into that shared folder and install dependencies
82
+ 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.
83
+ 7. *Destroys* the container the moment you exit. Your edited files remain exactly where they were.
84
+
85
+ ---
86
+
87
+ ## The unified editor experience
88
+
89
+ - **Real editor** — VS Code attaches directly into the container, so its built-in terminal *is* the sandbox shell. No second window to juggle.
90
+ - **Real persistence** — because the folder is bind-mounted, not just cloned inside the container, your work survives after `exit`. Nothing is lost.
91
+ 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.
92
+
93
+ ---
94
+
95
+ ## Supported runtimes
96
+ | File detected | Runtime | Base image |
97
+ |---|---|---|
98
+ | `package.json` | Node.js | `node:20-alpine` |
99
+ | `yarn.lock` | Node.js (yarn) | `node:20-alpine` |
100
+ | `pnpm-lock.yaml` | Node.js (pnpm) | `node:20-alpine` |
101
+ | `requirements.txt` | Python | `python:3.12-slim` |
102
+ | `pyproject.toml` | Python | `python:3.12-slim` |
103
+ | `Pipfile` | Python (pipenv) | `python:3.12-slim` |
104
+ | `go.mod` | Go | `golang:1.21-alpine` |
105
+ | `Cargo.toml` | Rust | `rust:1.75-slim` |
106
+ | `Gemfile` | Ruby | `ruby:3.3-slim` |
107
+ | `composer.json` | PHP | `php:8.3-cli` |
108
+ | `pom.xml` | Java (Maven) | `maven:3.9-eclipse-temurin-21` |
109
+ | `build.gradle` | Java (Gradle) | `gradle:8-jdk21` |
110
+ | `.devcontainer` | devcontainer | MS universal image |
111
+ | *(none matched)* | Universal fallback | `ubuntu:22.04` |
112
+
113
+ ---
114
+
115
+ ## The GitHub App
116
+
117
+ 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 contributors never have to think about setup.
118
+
119
+ ```
120
+ 🏖 Open this issue in DevSandbox
121
+
122
+ Spin up a disposable Docker environment with this repo already checked out:
123
+
124
+ repro https://github.com/org/repo/issues/42
125
+
126
+ Don't have repro yet? Install it here.
127
+
128
+ Comment /sandbox on any issue to get this again.
129
+ ```
130
+
131
+ You can also trigger it on demand by commenting `/sandbox` on any issue. The bot runs 24/7, independent of your machine.
132
+
133
+ ---
134
+ ## Contributing
135
+ PRs welcome. Open an issue first for anything beyond small fixes - and yes, you can use `repro` itself to work on `repro`.
136
+
137
+ ## License
138
+
139
+ MIT
@@ -0,0 +1,130 @@
1
+ # REPRO
2
+
3
+ > Disposable Docker environments for any GitHub issue.
4
+ One command. No setup.
5
+
6
+ ```bash
7
+ pip install repro-cli
8
+ repro https://github.com/org/repo/issues/42
9
+ ```
10
+
11
+ 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.
12
+
13
+ ---
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install repro-cli
19
+ ```
20
+
21
+ **Requirements:**
22
+ - Python 3.9+
23
+ - [Docker](https://docs.docker.com/get-docker/) (INSTALLED AND RUNNING)
24
+ - VS Code (optional, but recommended - enables the unified editor/terminal experience below)
25
+
26
+ ---
27
+
28
+ ## Usage
29
+
30
+ ```bash
31
+ # Open a sandbox for any publish GitHub issue
32
+ repro https://github.com/org/repo/issues/123
33
+ ```
34
+
35
+ ```bash
36
+ #Forward a port (e.g. to view a web app running inside the sandbox)
37
+ repro https://github.com/org/repo/issues/123 -p 3000
38
+ ```
39
+
40
+ ```bash
41
+ # Map a different host port to a container port
42
+ repro https://github.com/org/repo/issues/123 -p 8080:3000
43
+ ```
44
+
45
+ ```bash
46
+ # Private repos - pass a GitHub token
47
+ repro https://github.com/org/private-repo/issues/7 --token ghp_xxx
48
+ ```
49
+
50
+ ```bash
51
+ #Or set it once as an env var instead of typing it every single time!
52
+ export DEVSANDBOX_GITHUB_TOKEN=ghp_xxx
53
+ repro https://github.com/org/private-repo/issues/7
54
+ ```
55
+
56
+ ```bash
57
+ # Skip auto-opening an editor
58
+ repro https://github.com/org/repo/issues/123 --no-editor
59
+ ```
60
+
61
+ ```bash
62
+ #Help
63
+ repro --help
64
+ ```
65
+
66
+ ## What it does
67
+
68
+ 1. **Parses** the GitHub issue URL
69
+ 2. **Detects** the repo's runtime - Node, Python, Go, Rust, Ruby, PHP, Java, or a devcontainer, based on whats actually in the repo.
70
+ 3. **Spins up** a disposable, isolated Docker container with the right base image.
71
+ 4. **Bind-mounts** a real folder on your machine into the container, so nothing is trapped - files persist after you exit
72
+ 5. **Clones** the repo directly into that shared folder and install dependencies
73
+ 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.
74
+ 7. *Destroys* the container the moment you exit. Your edited files remain exactly where they were.
75
+
76
+ ---
77
+
78
+ ## The unified editor experience
79
+
80
+ - **Real editor** — VS Code attaches directly into the container, so its built-in terminal *is* the sandbox shell. No second window to juggle.
81
+ - **Real persistence** — because the folder is bind-mounted, not just cloned inside the container, your work survives after `exit`. Nothing is lost.
82
+ 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.
83
+
84
+ ---
85
+
86
+ ## Supported runtimes
87
+ | File detected | Runtime | Base image |
88
+ |---|---|---|
89
+ | `package.json` | Node.js | `node:20-alpine` |
90
+ | `yarn.lock` | Node.js (yarn) | `node:20-alpine` |
91
+ | `pnpm-lock.yaml` | Node.js (pnpm) | `node:20-alpine` |
92
+ | `requirements.txt` | Python | `python:3.12-slim` |
93
+ | `pyproject.toml` | Python | `python:3.12-slim` |
94
+ | `Pipfile` | Python (pipenv) | `python:3.12-slim` |
95
+ | `go.mod` | Go | `golang:1.21-alpine` |
96
+ | `Cargo.toml` | Rust | `rust:1.75-slim` |
97
+ | `Gemfile` | Ruby | `ruby:3.3-slim` |
98
+ | `composer.json` | PHP | `php:8.3-cli` |
99
+ | `pom.xml` | Java (Maven) | `maven:3.9-eclipse-temurin-21` |
100
+ | `build.gradle` | Java (Gradle) | `gradle:8-jdk21` |
101
+ | `.devcontainer` | devcontainer | MS universal image |
102
+ | *(none matched)* | Universal fallback | `ubuntu:22.04` |
103
+
104
+ ---
105
+
106
+ ## The GitHub App
107
+
108
+ 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 contributors never have to think about setup.
109
+
110
+ ```
111
+ 🏖 Open this issue in DevSandbox
112
+
113
+ Spin up a disposable Docker environment with this repo already checked out:
114
+
115
+ repro https://github.com/org/repo/issues/42
116
+
117
+ Don't have repro yet? Install it here.
118
+
119
+ Comment /sandbox on any issue to get this again.
120
+ ```
121
+
122
+ You can also trigger it on demand by commenting `/sandbox` on any issue. The bot runs 24/7, independent of your machine.
123
+
124
+ ---
125
+ ## Contributing
126
+ PRs welcome. Open an issue first for anything beyond small fixes - and yes, you can use `repro` itself to work on `repro`.
127
+
128
+ ## License
129
+
130
+ 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.0"
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,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: repro-cli
3
+ Version: 0.2.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
9
+
10
+ # REPRO
11
+
12
+ > Disposable Docker environments for any GitHub issue.
13
+ One command. No setup.
14
+
15
+ ```bash
16
+ pip install repro-cli
17
+ repro https://github.com/org/repo/issues/42
18
+ ```
19
+
20
+ 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.
21
+
22
+ ---
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install repro-cli
28
+ ```
29
+
30
+ **Requirements:**
31
+ - Python 3.9+
32
+ - [Docker](https://docs.docker.com/get-docker/) (INSTALLED AND RUNNING)
33
+ - VS Code (optional, but recommended - enables the unified editor/terminal experience below)
34
+
35
+ ---
36
+
37
+ ## Usage
38
+
39
+ ```bash
40
+ # Open a sandbox for any publish GitHub issue
41
+ repro https://github.com/org/repo/issues/123
42
+ ```
43
+
44
+ ```bash
45
+ #Forward a port (e.g. to view a web app running inside the sandbox)
46
+ repro https://github.com/org/repo/issues/123 -p 3000
47
+ ```
48
+
49
+ ```bash
50
+ # Map a different host port to a container port
51
+ repro https://github.com/org/repo/issues/123 -p 8080:3000
52
+ ```
53
+
54
+ ```bash
55
+ # Private repos - pass a GitHub token
56
+ repro https://github.com/org/private-repo/issues/7 --token ghp_xxx
57
+ ```
58
+
59
+ ```bash
60
+ #Or set it once as an env var instead of typing it every single time!
61
+ export DEVSANDBOX_GITHUB_TOKEN=ghp_xxx
62
+ repro https://github.com/org/private-repo/issues/7
63
+ ```
64
+
65
+ ```bash
66
+ # Skip auto-opening an editor
67
+ repro https://github.com/org/repo/issues/123 --no-editor
68
+ ```
69
+
70
+ ```bash
71
+ #Help
72
+ repro --help
73
+ ```
74
+
75
+ ## What it does
76
+
77
+ 1. **Parses** the GitHub issue URL
78
+ 2. **Detects** the repo's runtime - Node, Python, Go, Rust, Ruby, PHP, Java, or a devcontainer, based on whats actually in the repo.
79
+ 3. **Spins up** a disposable, isolated Docker container with the right base image.
80
+ 4. **Bind-mounts** a real folder on your machine into the container, so nothing is trapped - files persist after you exit
81
+ 5. **Clones** the repo directly into that shared folder and install dependencies
82
+ 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.
83
+ 7. *Destroys* the container the moment you exit. Your edited files remain exactly where they were.
84
+
85
+ ---
86
+
87
+ ## The unified editor experience
88
+
89
+ - **Real editor** — VS Code attaches directly into the container, so its built-in terminal *is* the sandbox shell. No second window to juggle.
90
+ - **Real persistence** — because the folder is bind-mounted, not just cloned inside the container, your work survives after `exit`. Nothing is lost.
91
+ 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.
92
+
93
+ ---
94
+
95
+ ## Supported runtimes
96
+ | File detected | Runtime | Base image |
97
+ |---|---|---|
98
+ | `package.json` | Node.js | `node:20-alpine` |
99
+ | `yarn.lock` | Node.js (yarn) | `node:20-alpine` |
100
+ | `pnpm-lock.yaml` | Node.js (pnpm) | `node:20-alpine` |
101
+ | `requirements.txt` | Python | `python:3.12-slim` |
102
+ | `pyproject.toml` | Python | `python:3.12-slim` |
103
+ | `Pipfile` | Python (pipenv) | `python:3.12-slim` |
104
+ | `go.mod` | Go | `golang:1.21-alpine` |
105
+ | `Cargo.toml` | Rust | `rust:1.75-slim` |
106
+ | `Gemfile` | Ruby | `ruby:3.3-slim` |
107
+ | `composer.json` | PHP | `php:8.3-cli` |
108
+ | `pom.xml` | Java (Maven) | `maven:3.9-eclipse-temurin-21` |
109
+ | `build.gradle` | Java (Gradle) | `gradle:8-jdk21` |
110
+ | `.devcontainer` | devcontainer | MS universal image |
111
+ | *(none matched)* | Universal fallback | `ubuntu:22.04` |
112
+
113
+ ---
114
+
115
+ ## The GitHub App
116
+
117
+ 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 contributors never have to think about setup.
118
+
119
+ ```
120
+ 🏖 Open this issue in DevSandbox
121
+
122
+ Spin up a disposable Docker environment with this repo already checked out:
123
+
124
+ repro https://github.com/org/repo/issues/42
125
+
126
+ Don't have repro yet? Install it here.
127
+
128
+ Comment /sandbox on any issue to get this again.
129
+ ```
130
+
131
+ You can also trigger it on demand by commenting `/sandbox` on any issue. The bot runs 24/7, independent of your machine.
132
+
133
+ ---
134
+ ## Contributing
135
+ PRs welcome. Open an issue first for anything beyond small fixes - and yes, you can use `repro` itself to work on `repro`.
136
+
137
+ ## License
138
+
139
+ MIT
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