sandbox-engine 1.0.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.
Files changed (30) hide show
  1. sandbox_engine-1.0.0/MANIFEST.in +6 -0
  2. sandbox_engine-1.0.0/PKG-INFO +110 -0
  3. sandbox_engine-1.0.0/go.mod +3 -0
  4. sandbox_engine-1.0.0/internal/color/color.go +61 -0
  5. sandbox_engine-1.0.0/internal/detector/detector.go +253 -0
  6. sandbox_engine-1.0.0/internal/exec/exec.go +45 -0
  7. sandbox_engine-1.0.0/internal/fsutil/fsutil.go +74 -0
  8. sandbox_engine-1.0.0/internal/pkgjson/pkgjson.go +110 -0
  9. sandbox_engine-1.0.0/internal/runtime/docker.go +29 -0
  10. sandbox_engine-1.0.0/internal/runtime/go_runtime.go +24 -0
  11. sandbox_engine-1.0.0/internal/runtime/java.go +36 -0
  12. sandbox_engine-1.0.0/internal/runtime/node.go +46 -0
  13. sandbox_engine-1.0.0/internal/runtime/python.go +164 -0
  14. sandbox_engine-1.0.0/internal/runtime/runner.go +72 -0
  15. sandbox_engine-1.0.0/internal/runtime/rust.go +24 -0
  16. sandbox_engine-1.0.0/internal/runtime/static.go +29 -0
  17. sandbox_engine-1.0.0/internal/scanner/scanner.go +167 -0
  18. sandbox_engine-1.0.0/internal/types/types.go +66 -0
  19. sandbox_engine-1.0.0/main.go +207 -0
  20. sandbox_engine-1.0.0/readme.md +89 -0
  21. sandbox_engine-1.0.0/sandbox_engine.egg-info/PKG-INFO +110 -0
  22. sandbox_engine-1.0.0/sandbox_engine.egg-info/SOURCES.txt +28 -0
  23. sandbox_engine-1.0.0/sandbox_engine.egg-info/dependency_links.txt +1 -0
  24. sandbox_engine-1.0.0/sandbox_engine.egg-info/entry_points.txt +2 -0
  25. sandbox_engine-1.0.0/sandbox_engine.egg-info/top_level.txt +1 -0
  26. sandbox_engine-1.0.0/sandbox_engine_pkg/__init__.py +0 -0
  27. sandbox_engine-1.0.0/sandbox_engine_pkg/sandbox-engine.exe +0 -0
  28. sandbox_engine-1.0.0/sandbox_engine_pkg/wrapper.py +26 -0
  29. sandbox_engine-1.0.0/setup.cfg +4 -0
  30. sandbox_engine-1.0.0/setup.py +67 -0
@@ -0,0 +1,6 @@
1
+ include readme.md
2
+ include go.mod
3
+ include go.sum
4
+ include main.go
5
+ recursive-include internal *
6
+ include sandbox_engine_pkg/*.py
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: sandbox-engine
3
+ Version: 1.0.0
4
+ Summary: Automatic Repository Sandbox Runner — detects any repo's runtime and launches it in a sandbox
5
+ Home-page: https://github.com/VivanRajath/sandbox-engine
6
+ Author: Vivan Rajath
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Environment :: Console
11
+ Classifier: Topic :: Software Development :: Build Tools
12
+ Requires-Python: >=3.7
13
+ Description-Content-Type: text/markdown
14
+ Dynamic: author
15
+ Dynamic: classifier
16
+ Dynamic: description
17
+ Dynamic: description-content-type
18
+ Dynamic: home-page
19
+ Dynamic: requires-python
20
+ Dynamic: summary
21
+
22
+ # Sandbox Engine Architecture & Documentation
23
+
24
+ ## Overview
25
+
26
+ **Sandbox Engine** is an Automatic Repository Sandbox Runner built in Go. It provides a CLI interface designed to seamlessly scan any repository, automatically detect the programming language and framework for all contained projects, install required dependencies in isolated environments, and launch the application.
27
+
28
+ This document details the end-to-end architecture, the internal flow, and the robust support system that makes it highly versatile.
29
+
30
+ ---
31
+
32
+ ## High-Level Architecture Flow
33
+
34
+ The execution lifecycle of the Sandbox Engine consists of three main phases, orchestrated by `main.go`.
35
+
36
+ 1. **Scan Phase:** Recursively traverses the file system to find all application roots.
37
+ 2. **Detect Phase:** Analyzes each discovered project to identify its language, framework, dependencies, entry point, and default port.
38
+ 3. **Run Phase:** Prepares the environment (e.g., instantiating virtual environments, downloading packages) and executes the project.
39
+
40
+ ### 1. Scanner (`internal/scanner`)
41
+
42
+ The Scanner is responsible for identifying where projects live within a potentially large monolithic repository structure.
43
+
44
+ - **Concurrency:** It uses a fixed worker pool of 8 goroutines to perform a depth-first traversal of the filesystem concurrently. This ensures high performance without overwhelming the OS file descriptor limits.
45
+ - **Indicator Files:** It identifies a "project root" by the presence of specific files like `package.json`, `requirements.txt`, `go.mod`, `pom.xml`, `dockerfile`, etc.
46
+ - **Skip Directories:** To optimize scanning, it inherently ignores dependency paths and compiled outputs such as `node_modules`, `.git`, `vendor`, `__pycache__`, `target`, `build`, etc.
47
+ - **Output:** Generates a `types.ScanResult` containing distinct `Project` blocks with the files mapped for each root.
48
+
49
+ ### 2. Detector (`internal/detector`)
50
+
51
+ The Detector consumes the output of the Scanner. For each project, it analyzes its filesystem structure and file contents to populate rich runtime metadata (defined in `internal/types/types.go`).
52
+
53
+ - **Language Inference:** Determines the primary language (`LangNode`, `LangPython`, `LangGo`, `LangJava`, `LangRust`, `LangDotNet`, `LangStatic`, `LangDocker`) based on the matching indicator files.
54
+ - **Framework Detection:** Deep-inspects configuration files or dependency lists to detect frameworks:
55
+ - *Node / JS:* Angular, Astro, Gatsby, Vite, Nuxt, Next.js, Svelte, NestJS, Express, Vue, React.
56
+ - *Python:* Django, FastAPI, Flask (reads `requirements.txt` and entry `.py` files).
57
+ - *Java:* Spring Boot (reads `pom.xml` / `build.gradle`).
58
+ - *.NET:* ASP.NET (reads `.csproj`).
59
+ - **EntryPoint & Port Allocation:** Deduces the main file to execute (e.g., `main.go`, `src/index.ts`, `manage.py`, `index.html`) and assigns the conventional port representing that framework (e.g., 3000 for Node/Static, 8000 for FastAPI/Django, 8080 for Spring Boot/Go).
60
+ - **Environment Configuration:** For environments like Python, it checks for existing virtual environments (`venv`, `.venv`, `env`). If missing, it notes where one needs to be created. It also detects package managers like `pnpm`, `yarn`, or `npm` based on lock files.
61
+
62
+ ### 3. Runtime & Exec (`internal/runtime` & `internal/exec`)
63
+
64
+ The Runtime module bridges detection with actual execution via two main steps:
65
+
66
+ #### Dependency Installation (`InstallDependencies`)
67
+ Depending on the resolved language type, it delegates to specialized installers:
68
+ - **Node:** Uses `npm install`, `yarn install`, or `pnpm install` based on detected lock files.
69
+ - **Python:** Sets up an isolated virtual environment (`python -m venv`) and installs requirements via `pip`.
70
+ - **Java / Go / Rust:** Uses their native package resolution (Maven/Gradle, Go modules, Cargo).
71
+ - *Note: If Docker/Docker Compose is detected, host-level dependency installation is skipped in favor of containerized isolation.*
72
+
73
+ #### Application Launch (`RunProject`)
74
+ Executes the project using the corresponding language runner:
75
+ - **Docker Priority:** If a `docker-compose.yml` or `Dockerfile` is present, those take absolute precedence. `RunProject` launches Docker Compose or builds and runs the container, maintaining sandboxed isolation.
76
+ - **Language Runtimes:** Executes specific launch commands (e.g., `node src/index.js`, `python manage.py runserver`, `go run main.go`, or using `serve` for static HTML roots).
77
+ - **Subprocess Management (`internal/exec`):** All processes are hooked to standard inputs and outputs via OS execution wrappers. `RunCmdAttached` and `RunCmdAttachedEnv` ensure that interactive processes (like dev servers) bind directly to the TTY, proxying stdout and stderr visually back to the user seamlessly.
78
+
79
+ ---
80
+
81
+ ## Supported Ecosystems
82
+
83
+ | Language | Support / Runtime | Detected Frameworks | Package Management |
84
+ | :------- | :--- | :--- | :--- |
85
+ | **Node.js** | Supported | React, Next.js, Vue, Nuxt, Angular, Vite, Svelte, Astro, Gatsby, Express, NestJS | `npm`, `yarn`, `pnpm` |
86
+ | **Python** | Supported | Django, FastAPI, Flask | Isolated `venv`, `pip` |
87
+ | **Java** | Supported | Spring Boot | Maven (`pom.xml`), Gradle (`build.gradle`) |
88
+ | **Go** | Supported | Standard Library | Go Modules (`go.mod`) |
89
+ | **Rust** | Supported | Standard Library | Cargo (`Cargo.toml`) |
90
+ | **C# (.NET)** | Detected | ASP.NET | Standard `.csproj` |
91
+ | **PHP** | Detected | None specifically | N/A |
92
+ | **Static HTML** | Supported | Static File Server (`index.html`) | N/A (Launched via static file hosting) |
93
+ | **Docker** | Supported | Any Containerized App | Dockerfile, Docker Compose |
94
+
95
+ ---
96
+
97
+ ## Command Line Interface (CLI) Use-Cases
98
+
99
+ - **`sandbox-engine scan`**: Performs the file system traversal and outputs a formatted list of all root sub-projects discovered.
100
+ - **`sandbox-engine detect`**: Extends the scan operation to analyze and output the language, framework, port, entrypoint, and Docker usage for every project.
101
+ - **`sandbox-engine run`**: The primary command. It installs dependencies and safely sandboxes the *first* detected project in the repository.
102
+ - **`sandbox-engine run <project>`**: Allows sandboxing of a specific project within a monorepo by string matching the project directory or name.
103
+ - **`--path <dir>`**: An overarching flag that allows the CLI tool to process an external repository without having to change the current working directory.
104
+
105
+ ## Summary
106
+
107
+ The **Sandbox Engine** is built strictly emphasizing modularity:
108
+ `Scanner` maps the territory ➔ `Detector` interprets the context ➔ `Runtime` handles the execution.
109
+
110
+ This strict separation of concerns allows adding support for new languages and frameworks just by appending the indicator file constants in `scanner.go`, defining the metadata enums in `types.go`, assigning the logic in `detector.go`, and creating the relevant runner script under `internal/runtime/`.
@@ -0,0 +1,3 @@
1
+ module sandbox-engine
2
+
3
+ go 1.25.0
@@ -0,0 +1,61 @@
1
+ // Package color provides zero-dependency ANSI colour helpers and structured
2
+ // log functions for the sandbox-engine CLI.
3
+ package color
4
+
5
+ import (
6
+ "fmt"
7
+ "os"
8
+ "strings"
9
+ )
10
+
11
+ // ANSI escape codes.
12
+ const (
13
+ Reset = "\033[0m"
14
+ Bold = "\033[1m"
15
+ Cyan = "\033[36m"
16
+ Green = "\033[32m"
17
+ Yellow = "\033[33m"
18
+ Red = "\033[31m"
19
+ Magenta = "\033[35m"
20
+ White = "\033[97m"
21
+ )
22
+
23
+ // Sprintf wraps text with an ANSI colour code and resets afterwards.
24
+ func Sprintf(code, format string, args ...interface{}) string {
25
+ return code + fmt.Sprintf(format, args...) + Reset
26
+ }
27
+
28
+ // Info prints an informational message to stdout.
29
+ func Info(format string, args ...interface{}) {
30
+ fmt.Printf("%s %s\n", Sprintf(Cyan, "[INFO]"), fmt.Sprintf(format, args...))
31
+ }
32
+
33
+ // Success prints a success message to stdout.
34
+ func Success(format string, args ...interface{}) {
35
+ fmt.Printf("%s %s\n", Sprintf(Green, "[OK]"), fmt.Sprintf(format, args...))
36
+ }
37
+
38
+ // Warn prints a warning message to stdout.
39
+ func Warn(format string, args ...interface{}) {
40
+ fmt.Printf("%s %s\n", Sprintf(Yellow, "[WARN]"), fmt.Sprintf(format, args...))
41
+ }
42
+
43
+ // Error prints an error message to stderr.
44
+ func Error(format string, args ...interface{}) {
45
+ fmt.Fprintf(os.Stderr, "%s %s\n", Sprintf(Red, "[ERR]"), fmt.Sprintf(format, args...))
46
+ }
47
+
48
+ // Step prints a step/action message to stdout.
49
+ func Step(format string, args ...interface{}) {
50
+ fmt.Printf("%s %s\n", Sprintf(Magenta, "[-->]"), fmt.Sprintf(format, args...))
51
+ }
52
+
53
+ // Header prints a bold section header with a horizontal rule.
54
+ func Header(title string) {
55
+ line := strings.Repeat("-", 60)
56
+ fmt.Printf("\n%s%s%s\n %s\n%s%s%s\n",
57
+ Bold, line, Reset,
58
+ Sprintf(Bold, title),
59
+ Bold, line, Reset,
60
+ )
61
+ }
@@ -0,0 +1,253 @@
1
+ // Package detector analyses Project metadata to determine language, framework,
2
+ // entry point, and default port.
3
+ package detector
4
+
5
+ import (
6
+ "path/filepath"
7
+ "strings"
8
+
9
+ "sandbox-engine/internal/color"
10
+ "sandbox-engine/internal/fsutil"
11
+ "sandbox-engine/internal/pkgjson"
12
+ "sandbox-engine/internal/types"
13
+ )
14
+
15
+ // Detector analyses Projects and fills Language, Framework, EntryPoint, Port.
16
+ type Detector struct{}
17
+
18
+ // New returns a ready-to-use Detector.
19
+ func New() *Detector { return &Detector{} }
20
+
21
+ // DetectAll runs detection across every project in the scan result.
22
+ func (d *Detector) DetectAll(result *types.ScanResult) {
23
+ color.Header("Runtime & Framework Detector")
24
+ for _, p := range result.Projects {
25
+ d.Detect(p)
26
+ }
27
+ }
28
+
29
+ // Detect fills all detection fields for a single project.
30
+ func (d *Detector) Detect(p *types.Project) {
31
+ fs := fsutil.FileSet(p.Files)
32
+
33
+ // Docker presence
34
+ p.UseDCom = fs["docker-compose.yml"] || fs["docker-compose.yaml"]
35
+ p.UseDocker = fs["dockerfile"]
36
+
37
+ // Language — priority order
38
+ switch {
39
+ case fs["package.json"]:
40
+ p.Language = types.LangNode
41
+ case fs["requirements.txt"] || fs["pyproject.toml"] || fs["pipfile"] || fs["manage.py"]:
42
+ p.Language = types.LangPython
43
+ case fs["go.mod"]:
44
+ p.Language = types.LangGo
45
+ case fs["pom.xml"] || fs["build.gradle"]:
46
+ p.Language = types.LangJava
47
+ case fs["cargo.toml"]:
48
+ p.Language = types.LangRust
49
+ case fsutil.HasCsproj(p.Files):
50
+ p.Language = types.LangDotNet
51
+ p.Framework = types.FWAspNet
52
+ case fs["index.html"] || fs["index.htm"]:
53
+ p.Language = types.LangStatic
54
+ case p.UseDCom || p.UseDocker:
55
+ p.Language = types.LangDocker
56
+ default:
57
+ p.Language = types.LangUnknown
58
+ }
59
+
60
+ // Framework
61
+ switch p.Language {
62
+ case types.LangNode:
63
+ p.Framework = d.detectNodeFramework(p, fs)
64
+ p.LockFile = detectLockFile(fs)
65
+ case types.LangPython:
66
+ p.Framework = d.detectPythonFramework(p, fs)
67
+ case types.LangJava:
68
+ p.Framework = d.detectJavaFramework(p)
69
+ }
70
+
71
+ // Python virtual environment
72
+ if p.Language == types.LangPython {
73
+ for _, name := range []string{"venv", ".venv", "env"} {
74
+ if fsutil.DirExists(filepath.Join(p.Root, name)) {
75
+ p.HasVenv = true
76
+ p.VenvPath = filepath.Join(p.Root, name)
77
+ break
78
+ }
79
+ }
80
+ if !p.HasVenv {
81
+ p.VenvPath = filepath.Join(p.Root, "venv")
82
+ }
83
+ }
84
+
85
+ // Entry point and port
86
+ p.EntryPoint = d.detectEntryPoint(p, fs)
87
+ p.Port = defaultPort(p)
88
+
89
+ color.Success("%-20s lang=%-12s fw=%-12s port=%d entry=%s",
90
+ p.Name,
91
+ color.Sprintf(color.Cyan, string(p.Language)),
92
+ color.Sprintf(color.Yellow, string(p.Framework)),
93
+ p.Port,
94
+ p.EntryPoint,
95
+ )
96
+ }
97
+
98
+ // detectNodeFramework returns the JS/Node framework for the project.
99
+ func (d *Detector) detectNodeFramework(p *types.Project, fs map[string]bool) types.Framework {
100
+ // Config-file signals carry the highest confidence.
101
+ switch {
102
+ case fs["angular.json"]:
103
+ return types.FWAngular
104
+ case fs["astro.config.mjs"] || fs["astro.config.ts"]:
105
+ return types.FWAstro
106
+ case fs["gatsby-config.js"] || fs["gatsby-config.ts"]:
107
+ return types.FWGatsby
108
+ case fs["vite.config.js"] || fs["vite.config.ts"]:
109
+ return types.FWVite
110
+ }
111
+
112
+ // Fall back to scanning package.json dependencies.
113
+ deps := pkgjson.ReadDeps(filepath.Join(p.Root, "package.json"))
114
+ switch {
115
+ case deps["nuxt"]:
116
+ return types.FWNuxt
117
+ case deps["next"]:
118
+ return types.FWNextJS
119
+ case deps["gatsby"]:
120
+ return types.FWGatsby
121
+ case deps["@sveltejs/kit"] || deps["svelte"]:
122
+ return types.FWSvelte
123
+ case deps["@angular/core"]:
124
+ return types.FWAngular
125
+ case deps["@nestjs/core"]:
126
+ return types.FWNestJS
127
+ case deps["express"]:
128
+ return types.FWExpress
129
+ case deps["vue"]:
130
+ return types.FWVue
131
+ case deps["react"]:
132
+ return types.FWReact
133
+ }
134
+ return types.FWNone
135
+ }
136
+
137
+ // detectLockFile returns "pnpm", "yarn", or "npm" based on lock-file presence.
138
+ func detectLockFile(fs map[string]bool) string {
139
+ switch {
140
+ case fs["pnpm-lock.yaml"]:
141
+ return "pnpm"
142
+ case fs["yarn.lock"]:
143
+ return "yarn"
144
+ default:
145
+ return "npm"
146
+ }
147
+ }
148
+
149
+ // detectPythonFramework infers the Python web framework.
150
+ func (d *Detector) detectPythonFramework(p *types.Project, fs map[string]bool) types.Framework {
151
+ if fs["manage.py"] {
152
+ return types.FWDjango
153
+ }
154
+
155
+ // Scan requirements.txt for known package names.
156
+ for _, line := range fsutil.ReadLines(filepath.Join(p.Root, "requirements.txt")) {
157
+ lower := strings.ToLower(strings.TrimSpace(line))
158
+ switch {
159
+ case strings.HasPrefix(lower, "fastapi"):
160
+ return types.FWFastAPI
161
+ case strings.HasPrefix(lower, "flask"):
162
+ return types.FWFlask
163
+ case strings.HasPrefix(lower, "django"):
164
+ return types.FWDjango
165
+ }
166
+ }
167
+
168
+ // Scan common Python entry files for import statements.
169
+ for _, candidate := range []string{"app.py", "main.py", "server.py"} {
170
+ content := fsutil.ReadFileLower(filepath.Join(p.Root, candidate))
171
+ switch {
172
+ case strings.Contains(content, "fastapi"):
173
+ return types.FWFastAPI
174
+ case strings.Contains(content, "flask"):
175
+ return types.FWFlask
176
+ case strings.Contains(content, "django"):
177
+ return types.FWDjango
178
+ }
179
+ }
180
+ return types.FWNone
181
+ }
182
+
183
+ // detectJavaFramework checks pom.xml / build.gradle for Spring Boot markers.
184
+ func (d *Detector) detectJavaFramework(p *types.Project) types.Framework {
185
+ combined := fsutil.ReadFileLower(filepath.Join(p.Root, "pom.xml")) +
186
+ fsutil.ReadFileLower(filepath.Join(p.Root, "build.gradle"))
187
+ if strings.Contains(combined, "spring-boot") || strings.Contains(combined, "spring.boot") {
188
+ return types.FWSpringBoot
189
+ }
190
+ return types.FWNone
191
+ }
192
+
193
+ // detectEntryPoint returns the primary launch file for the project.
194
+ func (d *Detector) detectEntryPoint(p *types.Project, fs map[string]bool) string {
195
+ switch p.Language {
196
+ case types.LangPython:
197
+ for _, ep := range []string{"manage.py", "app.py", "main.py", "server.py"} {
198
+ if fs[strings.ToLower(ep)] {
199
+ return ep
200
+ }
201
+ }
202
+ case types.LangNode:
203
+ if main := pkgjson.ReadMain(filepath.Join(p.Root, "package.json")); main != "" {
204
+ return main
205
+ }
206
+ for _, ep := range []string{"index.js", "server.js", "app.js", "index.ts", "src/index.js", "src/index.ts"} {
207
+ if fsutil.FileExists(filepath.Join(p.Root, ep)) {
208
+ return ep
209
+ }
210
+ }
211
+ case types.LangGo:
212
+ if fs["main.go"] {
213
+ return "main.go"
214
+ }
215
+ case types.LangJava:
216
+ if fs["pom.xml"] {
217
+ return "pom.xml"
218
+ }
219
+ return "build.gradle"
220
+ case types.LangRust:
221
+ return "Cargo.toml"
222
+ case types.LangDotNet:
223
+ for _, f := range p.Files {
224
+ if strings.HasSuffix(strings.ToLower(f), ".csproj") {
225
+ return f
226
+ }
227
+ }
228
+ case types.LangStatic:
229
+ return "index.html"
230
+ }
231
+ return ""
232
+ }
233
+
234
+ // defaultPort returns the conventional port for a project.
235
+ func defaultPort(p *types.Project) int {
236
+ switch p.Framework {
237
+ case types.FWFlask:
238
+ return 5000
239
+ case types.FWFastAPI, types.FWDjango:
240
+ return 8000
241
+ case types.FWSpringBoot, types.FWAspNet:
242
+ return 8080
243
+ }
244
+ switch p.Language {
245
+ case types.LangGo, types.LangJava:
246
+ return 8080
247
+ case types.LangPython:
248
+ return 5000
249
+ case types.LangNode, types.LangStatic:
250
+ return 3000
251
+ }
252
+ return 3000
253
+ }
@@ -0,0 +1,45 @@
1
+ // Package exec provides thin wrappers around os/exec for running subprocesses.
2
+ package exec
3
+
4
+ import (
5
+ "os"
6
+ osexec "os/exec"
7
+ )
8
+
9
+ // RunCmd runs name+args in dir, streaming stdout/stderr. Returns any error.
10
+ func RunCmd(dir, name string, args ...string) error {
11
+ cmd := osexec.Command(name, args...)
12
+ cmd.Dir = dir
13
+ cmd.Stdout = os.Stdout
14
+ cmd.Stderr = os.Stderr
15
+ return cmd.Run()
16
+ }
17
+
18
+ // RunCmdAttached attaches stdin/stdout/stderr for interactive or long-lived
19
+ // processes (dev servers, docker compose, etc.).
20
+ func RunCmdAttached(dir, name string, args ...string) error {
21
+ cmd := osexec.Command(name, args...)
22
+ cmd.Dir = dir
23
+ cmd.Stdin = os.Stdin
24
+ cmd.Stdout = os.Stdout
25
+ cmd.Stderr = os.Stderr
26
+ return cmd.Run()
27
+ }
28
+
29
+ // RunCmdAttachedEnv is like RunCmdAttached but prepends extra environment
30
+ // variables before the inherited process environment.
31
+ func RunCmdAttachedEnv(dir string, extraEnv []string, name string, args ...string) error {
32
+ cmd := osexec.Command(name, args...)
33
+ cmd.Dir = dir
34
+ cmd.Stdin = os.Stdin
35
+ cmd.Stdout = os.Stdout
36
+ cmd.Stderr = os.Stderr
37
+ cmd.Env = append(os.Environ(), extraEnv...)
38
+ return cmd.Run()
39
+ }
40
+
41
+ // CommandExists reports whether the named binary is accessible via PATH.
42
+ func CommandExists(name string) bool {
43
+ _, err := osexec.LookPath(name)
44
+ return err == nil
45
+ }
@@ -0,0 +1,74 @@
1
+ // Package fsutil provides file-system utility helpers used across sandbox-engine.
2
+ package fsutil
3
+
4
+ import (
5
+ "os"
6
+ "path/filepath"
7
+ "strings"
8
+ )
9
+
10
+ // FileSet converts a filename slice into a lowercase presence map for fast
11
+ // membership checks.
12
+ func FileSet(files []string) map[string]bool {
13
+ fs := make(map[string]bool, len(files))
14
+ for _, f := range files {
15
+ fs[strings.ToLower(f)] = true
16
+ }
17
+ return fs
18
+ }
19
+
20
+ // HasCsproj returns true if any element of files ends with ".csproj".
21
+ func HasCsproj(files []string) bool {
22
+ for _, f := range files {
23
+ if strings.HasSuffix(strings.ToLower(f), ".csproj") {
24
+ return true
25
+ }
26
+ }
27
+ return false
28
+ }
29
+
30
+ // FileExists reports whether path is a regular file.
31
+ func FileExists(path string) bool {
32
+ info, err := os.Stat(path)
33
+ return err == nil && !info.IsDir()
34
+ }
35
+
36
+ // DirExists reports whether path is a directory.
37
+ func DirExists(path string) bool {
38
+ info, err := os.Stat(path)
39
+ return err == nil && info.IsDir()
40
+ }
41
+
42
+ // ReadFileLower reads a file and returns its contents lower-cased.
43
+ // Returns an empty string on any error.
44
+ func ReadFileLower(path string) string {
45
+ data, err := os.ReadFile(path)
46
+ if err != nil {
47
+ return ""
48
+ }
49
+ return strings.ToLower(string(data))
50
+ }
51
+
52
+ // ReadLines returns the lines of a file split on newline.
53
+ // Returns nil on any error.
54
+ func ReadLines(path string) []string {
55
+ data, err := os.ReadFile(path)
56
+ if err != nil {
57
+ return nil
58
+ }
59
+ return strings.Split(string(data), "\n")
60
+ }
61
+
62
+ // PythonBin resolves the path to a binary inside a virtual-env.
63
+ // Falls back to the bare binary name (PATH lookup) when not found in the venv.
64
+ func PythonBin(venvPath, bin string) string {
65
+ // Unix layout
66
+ if p := filepath.Join(venvPath, "bin", bin); FileExists(p) {
67
+ return p
68
+ }
69
+ // Windows layout
70
+ if p := filepath.Join(venvPath, "Scripts", bin+".exe"); FileExists(p) {
71
+ return p
72
+ }
73
+ return bin
74
+ }
@@ -0,0 +1,110 @@
1
+ // Package pkgjson provides lightweight, zero-allocation helpers for extracting
2
+ // data from package.json files without importing encoding/json.
3
+ package pkgjson
4
+
5
+ import (
6
+ "os"
7
+ "strings"
8
+ )
9
+
10
+ // ReadDeps returns a presence map of all dependency names declared in the
11
+ // dependencies, devDependencies, and peerDependencies sections.
12
+ func ReadDeps(path string) map[string]bool {
13
+ result := map[string]bool{}
14
+ data, err := os.ReadFile(path)
15
+ if err != nil {
16
+ return result
17
+ }
18
+ content := string(data)
19
+ for _, section := range []string{`"dependencies"`, `"devDependencies"`, `"peerDependencies"`} {
20
+ idx := strings.Index(content, section)
21
+ if idx < 0 {
22
+ continue
23
+ }
24
+ rest := content[idx:]
25
+ start := strings.Index(rest, "{")
26
+ end := strings.Index(rest, "}")
27
+ if start < 0 || end < 0 || end <= start {
28
+ continue
29
+ }
30
+ block := rest[start+1 : end]
31
+ for _, line := range strings.Split(block, "\n") {
32
+ line = strings.TrimSpace(line)
33
+ if len(line) == 0 || line[0] != '"' {
34
+ continue
35
+ }
36
+ closeQ := strings.Index(line[1:], `"`)
37
+ if closeQ < 0 {
38
+ continue
39
+ }
40
+ pkg := line[1 : closeQ+1]
41
+ if pkg != "" {
42
+ result[pkg] = true
43
+ }
44
+ }
45
+ }
46
+ return result
47
+ }
48
+
49
+ // ReadMain extracts the value of the "main" field from package.json.
50
+ // Returns an empty string when not found.
51
+ func ReadMain(path string) string {
52
+ data, err := os.ReadFile(path)
53
+ if err != nil {
54
+ return ""
55
+ }
56
+ content := string(data)
57
+ idx := strings.Index(content, `"main"`)
58
+ if idx < 0 {
59
+ return ""
60
+ }
61
+ rest := strings.TrimSpace(content[idx+6:])
62
+ colon := strings.Index(rest, ":")
63
+ if colon < 0 {
64
+ return ""
65
+ }
66
+ rest = strings.TrimSpace(rest[colon+1:])
67
+ if len(rest) == 0 || rest[0] != '"' {
68
+ return ""
69
+ }
70
+ end := strings.Index(rest[1:], `"`)
71
+ if end < 0 {
72
+ return ""
73
+ }
74
+ return rest[1 : end+1]
75
+ }
76
+
77
+ // ReadScripts returns a map of script-name to command string from package.json.
78
+ func ReadScripts(path string) map[string]string {
79
+ result := map[string]string{}
80
+ data, err := os.ReadFile(path)
81
+ if err != nil {
82
+ return result
83
+ }
84
+ content := string(data)
85
+ idx := strings.Index(content, `"scripts"`)
86
+ if idx < 0 {
87
+ return result
88
+ }
89
+ rest := content[idx:]
90
+ start := strings.Index(rest, "{")
91
+ end := strings.Index(rest, "}")
92
+ if start < 0 || end < 0 || end <= start {
93
+ return result
94
+ }
95
+ block := rest[start+1 : end]
96
+ for _, line := range strings.Split(block, "\n") {
97
+ line = strings.TrimSpace(strings.TrimRight(line, ","))
98
+ if len(line) == 0 || line[0] != '"' {
99
+ continue
100
+ }
101
+ parts := strings.SplitN(line, ":", 2)
102
+ if len(parts) != 2 {
103
+ continue
104
+ }
105
+ key := strings.Trim(strings.TrimSpace(parts[0]), `"`)
106
+ val := strings.Trim(strings.TrimSpace(parts[1]), `"`)
107
+ result[key] = val
108
+ }
109
+ return result
110
+ }
@@ -0,0 +1,29 @@
1
+ // Package runtime — Docker and docker-compose launch logic.
2
+ package runtime
3
+
4
+ import (
5
+ "fmt"
6
+ "strings"
7
+
8
+ "sandbox-engine/internal/color"
9
+ "sandbox-engine/internal/exec"
10
+ "sandbox-engine/internal/types"
11
+ )
12
+
13
+ // LaunchDockerCompose starts the stack defined by docker-compose.yml.
14
+ func LaunchDockerCompose(p *types.Project) error {
15
+ color.Step("docker compose up")
16
+ return exec.RunCmdAttached(p.Root, "docker", "compose", "up")
17
+ }
18
+
19
+ // LaunchDockerfile builds a Docker image then runs a container from it.
20
+ func LaunchDockerfile(p *types.Project) error {
21
+ image := strings.ToLower(strings.ReplaceAll(p.Name, " ", "-")) + "-sandbox"
22
+ color.Step("docker build -t %s .", image)
23
+ if err := exec.RunCmd(p.Root, "docker", "build", "-t", image, "."); err != nil {
24
+ return err
25
+ }
26
+ portMap := fmt.Sprintf("%d:%d", p.Port, p.Port)
27
+ color.Step("docker run --rm -p %s %s", portMap, image)
28
+ return exec.RunCmdAttached(p.Root, "docker", "run", "--rm", "-p", portMap, image)
29
+ }