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.
- sandbox_engine-1.0.0/MANIFEST.in +6 -0
- sandbox_engine-1.0.0/PKG-INFO +110 -0
- sandbox_engine-1.0.0/go.mod +3 -0
- sandbox_engine-1.0.0/internal/color/color.go +61 -0
- sandbox_engine-1.0.0/internal/detector/detector.go +253 -0
- sandbox_engine-1.0.0/internal/exec/exec.go +45 -0
- sandbox_engine-1.0.0/internal/fsutil/fsutil.go +74 -0
- sandbox_engine-1.0.0/internal/pkgjson/pkgjson.go +110 -0
- sandbox_engine-1.0.0/internal/runtime/docker.go +29 -0
- sandbox_engine-1.0.0/internal/runtime/go_runtime.go +24 -0
- sandbox_engine-1.0.0/internal/runtime/java.go +36 -0
- sandbox_engine-1.0.0/internal/runtime/node.go +46 -0
- sandbox_engine-1.0.0/internal/runtime/python.go +164 -0
- sandbox_engine-1.0.0/internal/runtime/runner.go +72 -0
- sandbox_engine-1.0.0/internal/runtime/rust.go +24 -0
- sandbox_engine-1.0.0/internal/runtime/static.go +29 -0
- sandbox_engine-1.0.0/internal/scanner/scanner.go +167 -0
- sandbox_engine-1.0.0/internal/types/types.go +66 -0
- sandbox_engine-1.0.0/main.go +207 -0
- sandbox_engine-1.0.0/readme.md +89 -0
- sandbox_engine-1.0.0/sandbox_engine.egg-info/PKG-INFO +110 -0
- sandbox_engine-1.0.0/sandbox_engine.egg-info/SOURCES.txt +28 -0
- sandbox_engine-1.0.0/sandbox_engine.egg-info/dependency_links.txt +1 -0
- sandbox_engine-1.0.0/sandbox_engine.egg-info/entry_points.txt +2 -0
- sandbox_engine-1.0.0/sandbox_engine.egg-info/top_level.txt +1 -0
- sandbox_engine-1.0.0/sandbox_engine_pkg/__init__.py +0 -0
- sandbox_engine-1.0.0/sandbox_engine_pkg/sandbox-engine.exe +0 -0
- sandbox_engine-1.0.0/sandbox_engine_pkg/wrapper.py +26 -0
- sandbox_engine-1.0.0/setup.cfg +4 -0
- sandbox_engine-1.0.0/setup.py +67 -0
|
@@ -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,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
|
+
}
|