@yolo-labs/core-sandbox 1.0.0
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.
- package/dist/index.d.ts +263 -0
- package/dist/index.js +386 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { SandboxProvider, CommandExecutionOptions, CommandResult } from '@yolo-labs/core-types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-memory sandbox using a Map-based virtual file system.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* All file operations are performed against an in-memory Map. Parent
|
|
8
|
+
* directories are auto-created on write. Use `exportState()` and
|
|
9
|
+
* `importState()` for serialization round-trips. Command execution
|
|
10
|
+
* is a no-op in this implementation.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const sandbox = new InMemorySandbox();
|
|
15
|
+
* await sandbox.writeFile('hello.txt', 'world');
|
|
16
|
+
* const content = await sandbox.readFile('hello.txt'); // 'world'
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
declare class InMemorySandbox implements SandboxProvider {
|
|
20
|
+
private files;
|
|
21
|
+
private directories;
|
|
22
|
+
/** Reads a file from the virtual file system by path. */
|
|
23
|
+
readFile(path: string): Promise<string>;
|
|
24
|
+
/** Writes content to a file, auto-creating parent directories. */
|
|
25
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
26
|
+
/** Deletes a file from the virtual file system. */
|
|
27
|
+
deleteFile(path: string): Promise<void>;
|
|
28
|
+
/** Lists immediate children (files and directories) under the given path. */
|
|
29
|
+
listFiles(path: string): Promise<string[]>;
|
|
30
|
+
/** Creates a directory and all intermediate parent directories. */
|
|
31
|
+
mkdir(path: string): Promise<void>;
|
|
32
|
+
/** Returns a no-op result; command execution is not supported in-memory. */
|
|
33
|
+
executeCommand(_command: string, _options?: CommandExecutionOptions): Promise<CommandResult>;
|
|
34
|
+
/** Initializes the sandbox (no-op for in-memory). */
|
|
35
|
+
create(): Promise<void>;
|
|
36
|
+
/** Clears all files and directories, resetting the sandbox. */
|
|
37
|
+
destroy(): Promise<void>;
|
|
38
|
+
/** Resets the sandbox to its initial empty state. */
|
|
39
|
+
reset(): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Exports the entire virtual file system as a path-to-content record.
|
|
42
|
+
*
|
|
43
|
+
* @returns A plain object mapping file paths to their string contents.
|
|
44
|
+
*/
|
|
45
|
+
exportState(): Record<string, string>;
|
|
46
|
+
/**
|
|
47
|
+
* Replaces the virtual file system with the given state.
|
|
48
|
+
*
|
|
49
|
+
* @param state - A plain object mapping file paths to their string contents.
|
|
50
|
+
*/
|
|
51
|
+
importState(state: Record<string, string>): void;
|
|
52
|
+
private normalizePath;
|
|
53
|
+
private dirname;
|
|
54
|
+
private ensureDir;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Configuration options for the Node.js VM sandbox. */
|
|
58
|
+
interface NodeVMSandboxOptions {
|
|
59
|
+
/** Maximum execution time in milliseconds (default: 5000). */
|
|
60
|
+
timeout?: number;
|
|
61
|
+
/** Custom global variables injected into the VM context. */
|
|
62
|
+
globals?: Record<string, unknown>;
|
|
63
|
+
/** Memory limit in bytes for the VM context. */
|
|
64
|
+
memoryLimit?: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Server-side sandbox that executes code in an isolated Node.js VM context.
|
|
68
|
+
*
|
|
69
|
+
* @remarks
|
|
70
|
+
* Uses Node.js `vm.runInContext` for code execution with configurable timeout.
|
|
71
|
+
* File operations are delegated to an internal {@link InMemorySandbox}.
|
|
72
|
+
* stdout/stderr are captured via a custom console object injected into the
|
|
73
|
+
* VM context. For stronger isolation, `isolated-vm` is available as an
|
|
74
|
+
* optional peer dependency.
|
|
75
|
+
*/
|
|
76
|
+
declare class NodeVMSandbox implements SandboxProvider {
|
|
77
|
+
private fileSystem;
|
|
78
|
+
private timeout;
|
|
79
|
+
private customGlobals;
|
|
80
|
+
private context;
|
|
81
|
+
constructor(options?: NodeVMSandboxOptions);
|
|
82
|
+
/** Reads a file from the backing in-memory file system. */
|
|
83
|
+
readFile(path: string): Promise<string>;
|
|
84
|
+
/** Writes a file to the backing in-memory file system. */
|
|
85
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
86
|
+
/** Deletes a file from the backing in-memory file system. */
|
|
87
|
+
deleteFile(path: string): Promise<void>;
|
|
88
|
+
/** Lists files under the given path in the backing file system. */
|
|
89
|
+
listFiles(path: string): Promise<string[]>;
|
|
90
|
+
/** Creates a directory in the backing in-memory file system. */
|
|
91
|
+
mkdir(path: string): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Executes JavaScript code in an isolated VM context with timeout and output capture.
|
|
94
|
+
*
|
|
95
|
+
* @param command - The JavaScript code string to execute.
|
|
96
|
+
* @param options - Optional timeout override and stdout/stderr streaming callbacks.
|
|
97
|
+
* @returns A {@link CommandResult} with exit code and captured output.
|
|
98
|
+
*/
|
|
99
|
+
executeCommand(command: string, options?: CommandExecutionOptions): Promise<CommandResult>;
|
|
100
|
+
/** Pre-creates the isolated VM context with configured globals. */
|
|
101
|
+
create(): Promise<void>;
|
|
102
|
+
/** Destroys the VM context and clears the backing file system. */
|
|
103
|
+
destroy(): Promise<void>;
|
|
104
|
+
/** Resets the sandbox by destroying and re-creating the VM context. */
|
|
105
|
+
reset(): Promise<void>;
|
|
106
|
+
/** Exports the backing file system state as a path-to-content record. */
|
|
107
|
+
exportState(): Record<string, string>;
|
|
108
|
+
/** Replaces the backing file system with the given state. */
|
|
109
|
+
importState(state: Record<string, string>): void;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Input for a compilation pass through the esbuild bundling pipeline. */
|
|
113
|
+
interface CompileRequest {
|
|
114
|
+
/** Entry point file path for the bundle. */
|
|
115
|
+
entryPoint: string;
|
|
116
|
+
/** Virtual file system mapping paths to source contents. */
|
|
117
|
+
files: Record<string, string>;
|
|
118
|
+
/** NPM dependency name-to-version map. */
|
|
119
|
+
dependencies?: Record<string, string>;
|
|
120
|
+
/** Target runtime environment. */
|
|
121
|
+
target?: 'browser' | 'node';
|
|
122
|
+
/** Output module format. */
|
|
123
|
+
format?: 'esm' | 'iife';
|
|
124
|
+
/** Whether to minify the output. */
|
|
125
|
+
minify?: boolean;
|
|
126
|
+
}
|
|
127
|
+
/** Output produced by a compilation pass. */
|
|
128
|
+
interface BuildArtifact {
|
|
129
|
+
/** Bundled JavaScript output. */
|
|
130
|
+
code: string;
|
|
131
|
+
/** Optional source map. */
|
|
132
|
+
map?: string;
|
|
133
|
+
/** Optional extracted CSS. */
|
|
134
|
+
css?: string;
|
|
135
|
+
/** Errors encountered during compilation. */
|
|
136
|
+
errors: BuildError[];
|
|
137
|
+
/** Warnings encountered during compilation. */
|
|
138
|
+
warnings: BuildWarning[];
|
|
139
|
+
}
|
|
140
|
+
/** A compilation error with an optional source location. */
|
|
141
|
+
interface BuildError {
|
|
142
|
+
message: string;
|
|
143
|
+
location?: {
|
|
144
|
+
file: string;
|
|
145
|
+
line: number;
|
|
146
|
+
column: number;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** A compilation warning with an optional source location. */
|
|
150
|
+
interface BuildWarning {
|
|
151
|
+
message: string;
|
|
152
|
+
location?: {
|
|
153
|
+
file: string;
|
|
154
|
+
line: number;
|
|
155
|
+
column: number;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** Configuration for splitting vendor dependencies into named chunks. */
|
|
159
|
+
interface VendorChunkConfig {
|
|
160
|
+
chunks: Record<string, string[]>;
|
|
161
|
+
cdnBaseUrl?: string;
|
|
162
|
+
}
|
|
163
|
+
/** Options for generating an HTML preview page from a build artifact. */
|
|
164
|
+
interface PreviewConfig {
|
|
165
|
+
template?: string;
|
|
166
|
+
title?: string;
|
|
167
|
+
scripts?: string[];
|
|
168
|
+
styles?: string[];
|
|
169
|
+
}
|
|
170
|
+
/** Generated preview HTML and its associated static assets. */
|
|
171
|
+
interface PreviewResult {
|
|
172
|
+
html: string;
|
|
173
|
+
assets: Record<string, string>;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Interface for a Whateverpack-based sandbox adapter.
|
|
177
|
+
*
|
|
178
|
+
* @remarks
|
|
179
|
+
* Consumers implement this to integrate Whatever's esbuild bundling pipeline.
|
|
180
|
+
* This is a contract definition only; the actual implementation lives in
|
|
181
|
+
* the consuming application. See {@link CompileRequest} and {@link BuildArtifact}
|
|
182
|
+
* for the compilation I/O types.
|
|
183
|
+
*/
|
|
184
|
+
interface WhateverpackSandboxAdapter {
|
|
185
|
+
/** Compiles the given source files into a bundled build artifact. */
|
|
186
|
+
compile(request: CompileRequest): Promise<BuildArtifact>;
|
|
187
|
+
/** Resolves vendor chunk configuration into chunk-name-to-URL mappings. */
|
|
188
|
+
resolveVendorChunks(config: VendorChunkConfig): Record<string, string>;
|
|
189
|
+
/** Generates an HTML preview page from a build artifact. */
|
|
190
|
+
generatePreview(artifact: BuildArtifact, config?: PreviewConfig): Promise<PreviewResult>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Minimal duck-type for the subset of the WebContainer API we use.
|
|
195
|
+
* Consumers pass a real `WebContainer` from `@webcontainer/api`;
|
|
196
|
+
* this interface avoids a hard dependency on that package.
|
|
197
|
+
*/
|
|
198
|
+
interface WebContainerLike {
|
|
199
|
+
fs: {
|
|
200
|
+
readFile(path: string, encoding: string): Promise<string>;
|
|
201
|
+
writeFile(path: string, data: string): Promise<void>;
|
|
202
|
+
readdir(path: string): Promise<string[]>;
|
|
203
|
+
mkdir(path: string, options?: {
|
|
204
|
+
recursive?: boolean;
|
|
205
|
+
}): Promise<void>;
|
|
206
|
+
rm(path: string, options?: {
|
|
207
|
+
force?: boolean;
|
|
208
|
+
recursive?: boolean;
|
|
209
|
+
}): Promise<void>;
|
|
210
|
+
};
|
|
211
|
+
spawn(command: string, args?: string[], options?: {
|
|
212
|
+
cwd?: string;
|
|
213
|
+
env?: Record<string, string>;
|
|
214
|
+
}): Promise<WebContainerProcessLike>;
|
|
215
|
+
}
|
|
216
|
+
/** Minimal duck-type for the process returned by `WebContainer.spawn()`. */
|
|
217
|
+
interface WebContainerProcessLike {
|
|
218
|
+
output: ReadableStream<string>;
|
|
219
|
+
exit: Promise<number>;
|
|
220
|
+
}
|
|
221
|
+
/** Configuration options for {@link WebContainerSandbox}. */
|
|
222
|
+
interface WebContainerSandboxOptions {
|
|
223
|
+
/** Root directory within the container to scope all operations to. Defaults to `'/'`. */
|
|
224
|
+
rootDir?: string;
|
|
225
|
+
/** Default timeout for command execution in milliseconds. Defaults to 30 000. */
|
|
226
|
+
timeout?: number;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* SandboxProvider backed by a WebContainer instance.
|
|
230
|
+
*
|
|
231
|
+
* @remarks
|
|
232
|
+
* The consumer boots the WebContainer externally and passes it to the
|
|
233
|
+
* constructor. All file operations delegate to `container.fs` and all
|
|
234
|
+
* command execution delegates to `container.spawn()`.
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```ts
|
|
238
|
+
* import { WebContainer } from '@webcontainer/api';
|
|
239
|
+
* import { WebContainerSandbox } from '@yolo-labs/core-sandbox';
|
|
240
|
+
*
|
|
241
|
+
* const wc = await WebContainer.boot();
|
|
242
|
+
* const sandbox = new WebContainerSandbox(wc, { rootDir: '/project' });
|
|
243
|
+
* await sandbox.writeFile('index.ts', 'console.log("hi")');
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
declare class WebContainerSandbox implements SandboxProvider {
|
|
247
|
+
private container;
|
|
248
|
+
private rootDir;
|
|
249
|
+
private timeout;
|
|
250
|
+
constructor(container: WebContainerLike, options?: WebContainerSandboxOptions);
|
|
251
|
+
readFile(path: string): Promise<string>;
|
|
252
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
253
|
+
deleteFile(path: string): Promise<void>;
|
|
254
|
+
listFiles(path: string): Promise<string[]>;
|
|
255
|
+
mkdir(path: string): Promise<void>;
|
|
256
|
+
executeCommand(command: string, options?: CommandExecutionOptions): Promise<CommandResult>;
|
|
257
|
+
create(): Promise<void>;
|
|
258
|
+
destroy(): Promise<void>;
|
|
259
|
+
reset(): Promise<void>;
|
|
260
|
+
private resolvePath;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export { type BuildArtifact, type BuildError, type BuildWarning, type CompileRequest, InMemorySandbox, NodeVMSandbox, type NodeVMSandboxOptions, type PreviewConfig, type PreviewResult, type VendorChunkConfig, type WebContainerLike, type WebContainerProcessLike, WebContainerSandbox, type WebContainerSandboxOptions, type WhateverpackSandboxAdapter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// src/in-memory-sandbox.ts
|
|
2
|
+
var InMemorySandbox = class {
|
|
3
|
+
// 39a: In-memory Map for file storage
|
|
4
|
+
files = /* @__PURE__ */ new Map();
|
|
5
|
+
directories = /* @__PURE__ */ new Set([".", "/"]);
|
|
6
|
+
/** Reads a file from the virtual file system by path. */
|
|
7
|
+
async readFile(path) {
|
|
8
|
+
const normalized = this.normalizePath(path);
|
|
9
|
+
const content = this.files.get(normalized);
|
|
10
|
+
if (content === void 0) {
|
|
11
|
+
throw new Error(`ENOENT: no such file: ${normalized}`);
|
|
12
|
+
}
|
|
13
|
+
return content;
|
|
14
|
+
}
|
|
15
|
+
/** Writes content to a file, auto-creating parent directories. */
|
|
16
|
+
async writeFile(path, content) {
|
|
17
|
+
const normalized = this.normalizePath(path);
|
|
18
|
+
const dir = this.dirname(normalized);
|
|
19
|
+
if (dir) {
|
|
20
|
+
this.ensureDir(dir);
|
|
21
|
+
}
|
|
22
|
+
this.files.set(normalized, content);
|
|
23
|
+
}
|
|
24
|
+
/** Deletes a file from the virtual file system. */
|
|
25
|
+
async deleteFile(path) {
|
|
26
|
+
const normalized = this.normalizePath(path);
|
|
27
|
+
if (!this.files.has(normalized)) {
|
|
28
|
+
throw new Error(`ENOENT: no such file: ${normalized}`);
|
|
29
|
+
}
|
|
30
|
+
this.files.delete(normalized);
|
|
31
|
+
}
|
|
32
|
+
/** Lists immediate children (files and directories) under the given path. */
|
|
33
|
+
async listFiles(path) {
|
|
34
|
+
const normalized = this.normalizePath(path || ".");
|
|
35
|
+
const prefix = normalized === "." ? "" : normalized + "/";
|
|
36
|
+
const entries = /* @__PURE__ */ new Set();
|
|
37
|
+
for (const filePath of this.files.keys()) {
|
|
38
|
+
if (prefix && !filePath.startsWith(prefix)) continue;
|
|
39
|
+
if (!prefix || filePath.startsWith(prefix)) {
|
|
40
|
+
const relative = prefix ? filePath.slice(prefix.length) : filePath;
|
|
41
|
+
const firstSegment = relative.split("/")[0];
|
|
42
|
+
if (firstSegment) {
|
|
43
|
+
entries.add(firstSegment);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return Array.from(entries).sort();
|
|
48
|
+
}
|
|
49
|
+
/** Creates a directory and all intermediate parent directories. */
|
|
50
|
+
async mkdir(path) {
|
|
51
|
+
const normalized = this.normalizePath(path);
|
|
52
|
+
this.ensureDir(normalized);
|
|
53
|
+
}
|
|
54
|
+
/** Returns a no-op result; command execution is not supported in-memory. */
|
|
55
|
+
async executeCommand(_command, _options) {
|
|
56
|
+
return {
|
|
57
|
+
exitCode: 0,
|
|
58
|
+
stdout: "",
|
|
59
|
+
stderr: "Command execution not supported in InMemorySandbox"
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Initializes the sandbox (no-op for in-memory). */
|
|
63
|
+
async create() {
|
|
64
|
+
}
|
|
65
|
+
/** Clears all files and directories, resetting the sandbox. */
|
|
66
|
+
async destroy() {
|
|
67
|
+
this.files.clear();
|
|
68
|
+
this.directories.clear();
|
|
69
|
+
this.directories.add(".");
|
|
70
|
+
this.directories.add("/");
|
|
71
|
+
}
|
|
72
|
+
/** Resets the sandbox to its initial empty state. */
|
|
73
|
+
async reset() {
|
|
74
|
+
await this.destroy();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Exports the entire virtual file system as a path-to-content record.
|
|
78
|
+
*
|
|
79
|
+
* @returns A plain object mapping file paths to their string contents.
|
|
80
|
+
*/
|
|
81
|
+
exportState() {
|
|
82
|
+
const state = {};
|
|
83
|
+
for (const [path, content] of this.files) {
|
|
84
|
+
state[path] = content;
|
|
85
|
+
}
|
|
86
|
+
return state;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Replaces the virtual file system with the given state.
|
|
90
|
+
*
|
|
91
|
+
* @param state - A plain object mapping file paths to their string contents.
|
|
92
|
+
*/
|
|
93
|
+
importState(state) {
|
|
94
|
+
this.files.clear();
|
|
95
|
+
this.directories.clear();
|
|
96
|
+
this.directories.add(".");
|
|
97
|
+
this.directories.add("/");
|
|
98
|
+
for (const [path, content] of Object.entries(state)) {
|
|
99
|
+
this.files.set(path, content);
|
|
100
|
+
const dir = this.dirname(path);
|
|
101
|
+
if (dir) {
|
|
102
|
+
this.ensureDir(dir);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Helpers
|
|
107
|
+
normalizePath(path) {
|
|
108
|
+
return path.replace(/^\.\//, "").replace(/\/+/g, "/").replace(/\/$/, "");
|
|
109
|
+
}
|
|
110
|
+
dirname(path) {
|
|
111
|
+
const lastSlash = path.lastIndexOf("/");
|
|
112
|
+
return lastSlash > 0 ? path.slice(0, lastSlash) : "";
|
|
113
|
+
}
|
|
114
|
+
ensureDir(path) {
|
|
115
|
+
const parts = path.split("/");
|
|
116
|
+
let current = "";
|
|
117
|
+
for (const part of parts) {
|
|
118
|
+
current = current ? `${current}/${part}` : part;
|
|
119
|
+
this.directories.add(current);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// src/node-vm-sandbox.ts
|
|
125
|
+
import { createContext, runInContext } from "vm";
|
|
126
|
+
var NodeVMSandbox = class {
|
|
127
|
+
fileSystem;
|
|
128
|
+
timeout;
|
|
129
|
+
customGlobals;
|
|
130
|
+
context = null;
|
|
131
|
+
constructor(options = {}) {
|
|
132
|
+
this.fileSystem = new InMemorySandbox();
|
|
133
|
+
this.timeout = options.timeout ?? 5e3;
|
|
134
|
+
this.customGlobals = options.globals ?? {};
|
|
135
|
+
}
|
|
136
|
+
/** Reads a file from the backing in-memory file system. */
|
|
137
|
+
async readFile(path) {
|
|
138
|
+
return this.fileSystem.readFile(path);
|
|
139
|
+
}
|
|
140
|
+
/** Writes a file to the backing in-memory file system. */
|
|
141
|
+
async writeFile(path, content) {
|
|
142
|
+
return this.fileSystem.writeFile(path, content);
|
|
143
|
+
}
|
|
144
|
+
/** Deletes a file from the backing in-memory file system. */
|
|
145
|
+
async deleteFile(path) {
|
|
146
|
+
return this.fileSystem.deleteFile(path);
|
|
147
|
+
}
|
|
148
|
+
/** Lists files under the given path in the backing file system. */
|
|
149
|
+
async listFiles(path) {
|
|
150
|
+
return this.fileSystem.listFiles(path);
|
|
151
|
+
}
|
|
152
|
+
/** Creates a directory in the backing in-memory file system. */
|
|
153
|
+
async mkdir(path) {
|
|
154
|
+
return this.fileSystem.mkdir(path);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Executes JavaScript code in an isolated VM context with timeout and output capture.
|
|
158
|
+
*
|
|
159
|
+
* @param command - The JavaScript code string to execute.
|
|
160
|
+
* @param options - Optional timeout override and stdout/stderr streaming callbacks.
|
|
161
|
+
* @returns A {@link CommandResult} with exit code and captured output.
|
|
162
|
+
*/
|
|
163
|
+
async executeCommand(command, options) {
|
|
164
|
+
const stdout = [];
|
|
165
|
+
const stderr = [];
|
|
166
|
+
const sandbox = {
|
|
167
|
+
...this.customGlobals,
|
|
168
|
+
console: {
|
|
169
|
+
log: (...args) => {
|
|
170
|
+
const line = args.map(String).join(" ");
|
|
171
|
+
stdout.push(line);
|
|
172
|
+
options?.onStdout?.(line + "\n");
|
|
173
|
+
},
|
|
174
|
+
error: (...args) => {
|
|
175
|
+
const line = args.map(String).join(" ");
|
|
176
|
+
stderr.push(line);
|
|
177
|
+
options?.onStderr?.(line + "\n");
|
|
178
|
+
},
|
|
179
|
+
warn: (...args) => {
|
|
180
|
+
const line = args.map(String).join(" ");
|
|
181
|
+
stderr.push(line);
|
|
182
|
+
options?.onStderr?.(line + "\n");
|
|
183
|
+
},
|
|
184
|
+
info: (...args) => {
|
|
185
|
+
const line = args.map(String).join(" ");
|
|
186
|
+
stdout.push(line);
|
|
187
|
+
options?.onStdout?.(line + "\n");
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
setTimeout: globalThis.setTimeout,
|
|
191
|
+
clearTimeout: globalThis.clearTimeout,
|
|
192
|
+
__result: void 0
|
|
193
|
+
};
|
|
194
|
+
const ctx = this.context ?? createContext(sandbox);
|
|
195
|
+
const timeout = options?.timeout ?? this.timeout;
|
|
196
|
+
try {
|
|
197
|
+
runInContext(command, ctx, {
|
|
198
|
+
timeout,
|
|
199
|
+
filename: "sandbox.js"
|
|
200
|
+
});
|
|
201
|
+
return {
|
|
202
|
+
exitCode: 0,
|
|
203
|
+
stdout: stdout.join("\n"),
|
|
204
|
+
stderr: stderr.join("\n")
|
|
205
|
+
};
|
|
206
|
+
} catch (err) {
|
|
207
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
208
|
+
stderr.push(errorMsg);
|
|
209
|
+
options?.onStderr?.(errorMsg + "\n");
|
|
210
|
+
return {
|
|
211
|
+
exitCode: 1,
|
|
212
|
+
stdout: stdout.join("\n"),
|
|
213
|
+
stderr: stderr.join("\n")
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** Pre-creates the isolated VM context with configured globals. */
|
|
218
|
+
async create() {
|
|
219
|
+
const sandbox = {
|
|
220
|
+
...this.customGlobals,
|
|
221
|
+
console: globalThis.console
|
|
222
|
+
};
|
|
223
|
+
this.context = createContext(sandbox);
|
|
224
|
+
}
|
|
225
|
+
/** Destroys the VM context and clears the backing file system. */
|
|
226
|
+
async destroy() {
|
|
227
|
+
this.context = null;
|
|
228
|
+
await this.fileSystem.destroy();
|
|
229
|
+
}
|
|
230
|
+
/** Resets the sandbox by destroying and re-creating the VM context. */
|
|
231
|
+
async reset() {
|
|
232
|
+
await this.destroy();
|
|
233
|
+
await this.create();
|
|
234
|
+
}
|
|
235
|
+
/** Exports the backing file system state as a path-to-content record. */
|
|
236
|
+
exportState() {
|
|
237
|
+
return this.fileSystem.exportState();
|
|
238
|
+
}
|
|
239
|
+
/** Replaces the backing file system with the given state. */
|
|
240
|
+
importState(state) {
|
|
241
|
+
this.fileSystem.importState(state);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// src/webcontainer-sandbox.ts
|
|
246
|
+
var WebContainerSandbox = class {
|
|
247
|
+
container;
|
|
248
|
+
rootDir;
|
|
249
|
+
timeout;
|
|
250
|
+
constructor(container, options) {
|
|
251
|
+
this.container = container;
|
|
252
|
+
this.rootDir = (options?.rootDir ?? "/").replace(/\/+$/, "") || "/";
|
|
253
|
+
this.timeout = options?.timeout ?? 3e4;
|
|
254
|
+
}
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// File operations
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
async readFile(path) {
|
|
259
|
+
return this.container.fs.readFile(this.resolvePath(path), "utf-8");
|
|
260
|
+
}
|
|
261
|
+
async writeFile(path, content) {
|
|
262
|
+
const resolved = this.resolvePath(path);
|
|
263
|
+
const lastSlash = resolved.lastIndexOf("/");
|
|
264
|
+
if (lastSlash > 0) {
|
|
265
|
+
await this.container.fs.mkdir(resolved.substring(0, lastSlash), { recursive: true });
|
|
266
|
+
}
|
|
267
|
+
await this.container.fs.writeFile(resolved, content);
|
|
268
|
+
}
|
|
269
|
+
async deleteFile(path) {
|
|
270
|
+
await this.container.fs.rm(this.resolvePath(path));
|
|
271
|
+
}
|
|
272
|
+
async listFiles(path) {
|
|
273
|
+
return this.container.fs.readdir(this.resolvePath(path));
|
|
274
|
+
}
|
|
275
|
+
async mkdir(path) {
|
|
276
|
+
await this.container.fs.mkdir(this.resolvePath(path), { recursive: true });
|
|
277
|
+
}
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// Command execution
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
async executeCommand(command, options) {
|
|
282
|
+
const parts = parseCommand(command);
|
|
283
|
+
const cmd = parts[0];
|
|
284
|
+
const args = parts.slice(1);
|
|
285
|
+
const spawnOpts = {};
|
|
286
|
+
if (options?.cwd) {
|
|
287
|
+
spawnOpts.cwd = this.resolvePath(options.cwd);
|
|
288
|
+
}
|
|
289
|
+
if (options?.env) {
|
|
290
|
+
spawnOpts.env = options.env;
|
|
291
|
+
}
|
|
292
|
+
const proc = await this.container.spawn(cmd, args, spawnOpts);
|
|
293
|
+
let output = "";
|
|
294
|
+
const timeout = options?.timeout ?? this.timeout;
|
|
295
|
+
const collectOutput = async () => {
|
|
296
|
+
const reader = proc.output.getReader();
|
|
297
|
+
try {
|
|
298
|
+
for (; ; ) {
|
|
299
|
+
const { done, value } = await reader.read();
|
|
300
|
+
if (done) break;
|
|
301
|
+
output += value;
|
|
302
|
+
options?.onStdout?.(value);
|
|
303
|
+
}
|
|
304
|
+
} finally {
|
|
305
|
+
reader.releaseLock();
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
let exitCode;
|
|
309
|
+
try {
|
|
310
|
+
const results = await Promise.race([
|
|
311
|
+
Promise.all([proc.exit, collectOutput()]),
|
|
312
|
+
new Promise(
|
|
313
|
+
(_, reject) => setTimeout(() => reject(new Error("Command timed out")), timeout)
|
|
314
|
+
)
|
|
315
|
+
]);
|
|
316
|
+
exitCode = results[0];
|
|
317
|
+
} catch {
|
|
318
|
+
exitCode = 1;
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
exitCode,
|
|
322
|
+
stdout: output,
|
|
323
|
+
// WebContainer spawn() has a single combined output stream;
|
|
324
|
+
// separate stderr is not available.
|
|
325
|
+
stderr: ""
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
// Lifecycle
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
async create() {
|
|
332
|
+
if (this.rootDir !== "/") {
|
|
333
|
+
await this.container.fs.mkdir(this.rootDir, { recursive: true });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async destroy() {
|
|
337
|
+
}
|
|
338
|
+
async reset() {
|
|
339
|
+
}
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// Internals
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
resolvePath(path) {
|
|
344
|
+
let normalized = path.replace(/^\.\//, "").replace(/\/+/g, "/").replace(/\/+$/, "");
|
|
345
|
+
if (normalized === "." || normalized === "") {
|
|
346
|
+
return this.rootDir;
|
|
347
|
+
}
|
|
348
|
+
if (this.rootDir === "/") {
|
|
349
|
+
return normalized.startsWith("/") ? normalized : "/" + normalized;
|
|
350
|
+
}
|
|
351
|
+
const full = normalized.startsWith("/") ? normalized : this.rootDir + "/" + normalized;
|
|
352
|
+
if (!full.startsWith(this.rootDir)) {
|
|
353
|
+
throw new Error(`Path '${path}' is outside the root directory`);
|
|
354
|
+
}
|
|
355
|
+
return full;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
function parseCommand(command) {
|
|
359
|
+
const parts = [];
|
|
360
|
+
let current = "";
|
|
361
|
+
let inSingle = false;
|
|
362
|
+
let inDouble = false;
|
|
363
|
+
for (let i = 0; i < command.length; i++) {
|
|
364
|
+
const ch = command[i];
|
|
365
|
+
if (ch === "'" && !inDouble) {
|
|
366
|
+
inSingle = !inSingle;
|
|
367
|
+
} else if (ch === '"' && !inSingle) {
|
|
368
|
+
inDouble = !inDouble;
|
|
369
|
+
} else if (ch === " " && !inSingle && !inDouble) {
|
|
370
|
+
if (current) {
|
|
371
|
+
parts.push(current);
|
|
372
|
+
current = "";
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
current += ch;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (current) parts.push(current);
|
|
379
|
+
return parts;
|
|
380
|
+
}
|
|
381
|
+
export {
|
|
382
|
+
InMemorySandbox,
|
|
383
|
+
NodeVMSandbox,
|
|
384
|
+
WebContainerSandbox
|
|
385
|
+
};
|
|
386
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/in-memory-sandbox.ts","../src/node-vm-sandbox.ts","../src/webcontainer-sandbox.ts"],"sourcesContent":["// Task 39: InMemorySandbox — baseline provider using a plain JS object as virtual file system\n\nimport type {\n SandboxProvider,\n CommandResult,\n CommandExecutionOptions,\n} from '@yolo-labs/core-types';\n\n/**\n * In-memory sandbox using a Map-based virtual file system.\n *\n * @remarks\n * All file operations are performed against an in-memory Map. Parent\n * directories are auto-created on write. Use `exportState()` and\n * `importState()` for serialization round-trips. Command execution\n * is a no-op in this implementation.\n *\n * @example\n * ```ts\n * const sandbox = new InMemorySandbox();\n * await sandbox.writeFile('hello.txt', 'world');\n * const content = await sandbox.readFile('hello.txt'); // 'world'\n * ```\n */\nexport class InMemorySandbox implements SandboxProvider {\n // 39a: In-memory Map for file storage\n private files = new Map<string, string>();\n private directories = new Set<string>(['.', '/']);\n\n /** Reads a file from the virtual file system by path. */\n async readFile(path: string): Promise<string> {\n const normalized = this.normalizePath(path);\n const content = this.files.get(normalized);\n if (content === undefined) {\n throw new Error(`ENOENT: no such file: ${normalized}`);\n }\n return content;\n }\n\n /** Writes content to a file, auto-creating parent directories. */\n async writeFile(path: string, content: string): Promise<void> {\n const normalized = this.normalizePath(path);\n // Auto-create parent directories\n const dir = this.dirname(normalized);\n if (dir) {\n this.ensureDir(dir);\n }\n this.files.set(normalized, content);\n }\n\n /** Deletes a file from the virtual file system. */\n async deleteFile(path: string): Promise<void> {\n const normalized = this.normalizePath(path);\n if (!this.files.has(normalized)) {\n throw new Error(`ENOENT: no such file: ${normalized}`);\n }\n this.files.delete(normalized);\n }\n\n /** Lists immediate children (files and directories) under the given path. */\n async listFiles(path: string): Promise<string[]> {\n const normalized = this.normalizePath(path || '.');\n const prefix = normalized === '.' ? '' : normalized + '/';\n const entries = new Set<string>();\n\n for (const filePath of this.files.keys()) {\n if (prefix && !filePath.startsWith(prefix)) continue;\n if (!prefix || filePath.startsWith(prefix)) {\n const relative = prefix ? filePath.slice(prefix.length) : filePath;\n const firstSegment = relative.split('/')[0];\n if (firstSegment) {\n entries.add(firstSegment);\n }\n }\n }\n\n return Array.from(entries).sort();\n }\n\n /** Creates a directory and all intermediate parent directories. */\n async mkdir(path: string): Promise<void> {\n const normalized = this.normalizePath(path);\n this.ensureDir(normalized);\n }\n\n /** Returns a no-op result; command execution is not supported in-memory. */\n async executeCommand(\n _command: string,\n _options?: CommandExecutionOptions,\n ): Promise<CommandResult> {\n return {\n exitCode: 0,\n stdout: '',\n stderr: 'Command execution not supported in InMemorySandbox',\n };\n }\n\n /** Initializes the sandbox (no-op for in-memory). */\n async create(): Promise<void> {\n // No-op\n }\n\n /** Clears all files and directories, resetting the sandbox. */\n async destroy(): Promise<void> {\n this.files.clear();\n this.directories.clear();\n this.directories.add('.');\n this.directories.add('/');\n }\n\n /** Resets the sandbox to its initial empty state. */\n async reset(): Promise<void> {\n await this.destroy();\n }\n\n /**\n * Exports the entire virtual file system as a path-to-content record.\n *\n * @returns A plain object mapping file paths to their string contents.\n */\n exportState(): Record<string, string> {\n const state: Record<string, string> = {};\n for (const [path, content] of this.files) {\n state[path] = content;\n }\n return state;\n }\n\n /**\n * Replaces the virtual file system with the given state.\n *\n * @param state - A plain object mapping file paths to their string contents.\n */\n importState(state: Record<string, string>): void {\n this.files.clear();\n this.directories.clear();\n this.directories.add('.');\n this.directories.add('/');\n for (const [path, content] of Object.entries(state)) {\n this.files.set(path, content);\n const dir = this.dirname(path);\n if (dir) {\n this.ensureDir(dir);\n }\n }\n }\n\n // Helpers\n private normalizePath(path: string): string {\n return path.replace(/^\\.\\//, '').replace(/\\/+/g, '/').replace(/\\/$/, '');\n }\n\n private dirname(path: string): string {\n const lastSlash = path.lastIndexOf('/');\n return lastSlash > 0 ? path.slice(0, lastSlash) : '';\n }\n\n private ensureDir(path: string): void {\n const parts = path.split('/');\n let current = '';\n for (const part of parts) {\n current = current ? `${current}/${part}` : part;\n this.directories.add(current);\n }\n }\n}\n","// Task 40: NodeVMSandbox — server-side sandbox using Node.js vm module\n\nimport { createContext, runInContext, type Context } from 'node:vm';\nimport type {\n SandboxProvider,\n CommandResult,\n CommandExecutionOptions,\n} from '@yolo-labs/core-types';\nimport { InMemorySandbox } from './in-memory-sandbox.js';\n\n/** Configuration options for the Node.js VM sandbox. */\nexport interface NodeVMSandboxOptions {\n /** Maximum execution time in milliseconds (default: 5000). */\n timeout?: number;\n /** Custom global variables injected into the VM context. */\n globals?: Record<string, unknown>;\n /** Memory limit in bytes for the VM context. */\n memoryLimit?: number;\n}\n\n/**\n * Server-side sandbox that executes code in an isolated Node.js VM context.\n *\n * @remarks\n * Uses Node.js `vm.runInContext` for code execution with configurable timeout.\n * File operations are delegated to an internal {@link InMemorySandbox}.\n * stdout/stderr are captured via a custom console object injected into the\n * VM context. For stronger isolation, `isolated-vm` is available as an\n * optional peer dependency.\n */\nexport class NodeVMSandbox implements SandboxProvider {\n private fileSystem: InMemorySandbox;\n private timeout: number;\n private customGlobals: Record<string, unknown>;\n private context: Context | null = null;\n\n constructor(options: NodeVMSandboxOptions = {}) {\n this.fileSystem = new InMemorySandbox();\n this.timeout = options.timeout ?? 5000;\n this.customGlobals = options.globals ?? {};\n }\n\n /** Reads a file from the backing in-memory file system. */\n async readFile(path: string): Promise<string> {\n return this.fileSystem.readFile(path);\n }\n\n /** Writes a file to the backing in-memory file system. */\n async writeFile(path: string, content: string): Promise<void> {\n return this.fileSystem.writeFile(path, content);\n }\n\n /** Deletes a file from the backing in-memory file system. */\n async deleteFile(path: string): Promise<void> {\n return this.fileSystem.deleteFile(path);\n }\n\n /** Lists files under the given path in the backing file system. */\n async listFiles(path: string): Promise<string[]> {\n return this.fileSystem.listFiles(path);\n }\n\n /** Creates a directory in the backing in-memory file system. */\n async mkdir(path: string): Promise<void> {\n return this.fileSystem.mkdir(path);\n }\n\n /**\n * Executes JavaScript code in an isolated VM context with timeout and output capture.\n *\n * @param command - The JavaScript code string to execute.\n * @param options - Optional timeout override and stdout/stderr streaming callbacks.\n * @returns A {@link CommandResult} with exit code and captured output.\n */\n async executeCommand(\n command: string,\n options?: CommandExecutionOptions,\n ): Promise<CommandResult> {\n const stdout: string[] = [];\n const stderr: string[] = [];\n\n // 40a: Create isolated context with configurable globals\n const sandbox: Record<string, unknown> = {\n ...this.customGlobals,\n console: {\n log: (...args: unknown[]) => {\n const line = args.map(String).join(' ');\n stdout.push(line);\n options?.onStdout?.(line + '\\n');\n },\n error: (...args: unknown[]) => {\n const line = args.map(String).join(' ');\n stderr.push(line);\n options?.onStderr?.(line + '\\n');\n },\n warn: (...args: unknown[]) => {\n const line = args.map(String).join(' ');\n stderr.push(line);\n options?.onStderr?.(line + '\\n');\n },\n info: (...args: unknown[]) => {\n const line = args.map(String).join(' ');\n stdout.push(line);\n options?.onStdout?.(line + '\\n');\n },\n },\n setTimeout: globalThis.setTimeout,\n clearTimeout: globalThis.clearTimeout,\n __result: undefined,\n };\n\n const ctx = this.context ?? createContext(sandbox);\n const timeout = options?.timeout ?? this.timeout;\n\n try {\n // 40b: Execute with timeout\n runInContext(command, ctx, {\n timeout,\n filename: 'sandbox.js',\n });\n\n return {\n exitCode: 0,\n stdout: stdout.join('\\n'),\n stderr: stderr.join('\\n'),\n };\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n stderr.push(errorMsg);\n options?.onStderr?.(errorMsg + '\\n');\n\n return {\n exitCode: 1,\n stdout: stdout.join('\\n'),\n stderr: stderr.join('\\n'),\n };\n }\n }\n\n /** Pre-creates the isolated VM context with configured globals. */\n async create(): Promise<void> {\n // 40a: Pre-create the context\n const sandbox: Record<string, unknown> = {\n ...this.customGlobals,\n console: globalThis.console,\n };\n this.context = createContext(sandbox);\n }\n\n /** Destroys the VM context and clears the backing file system. */\n async destroy(): Promise<void> {\n this.context = null;\n await this.fileSystem.destroy();\n }\n\n /** Resets the sandbox by destroying and re-creating the VM context. */\n async reset(): Promise<void> {\n await this.destroy();\n await this.create();\n }\n\n /** Exports the backing file system state as a path-to-content record. */\n exportState(): Record<string, string> {\n return this.fileSystem.exportState();\n }\n\n /** Replaces the backing file system with the given state. */\n importState(state: Record<string, string>): void {\n this.fileSystem.importState(state);\n }\n}\n","// WebContainerSandbox — SandboxProvider backed by a WebContainer instance\n\nimport type {\n SandboxProvider,\n CommandResult,\n CommandExecutionOptions,\n} from '@yolo-labs/core-types';\n\n/**\n * Minimal duck-type for the subset of the WebContainer API we use.\n * Consumers pass a real `WebContainer` from `@webcontainer/api`;\n * this interface avoids a hard dependency on that package.\n */\nexport interface WebContainerLike {\n fs: {\n readFile(path: string, encoding: string): Promise<string>;\n writeFile(path: string, data: string): Promise<void>;\n readdir(path: string): Promise<string[]>;\n mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;\n rm(path: string, options?: { force?: boolean; recursive?: boolean }): Promise<void>;\n };\n spawn(\n command: string,\n args?: string[],\n options?: { cwd?: string; env?: Record<string, string> },\n ): Promise<WebContainerProcessLike>;\n}\n\n/** Minimal duck-type for the process returned by `WebContainer.spawn()`. */\nexport interface WebContainerProcessLike {\n output: ReadableStream<string>;\n exit: Promise<number>;\n}\n\n/** Configuration options for {@link WebContainerSandbox}. */\nexport interface WebContainerSandboxOptions {\n /** Root directory within the container to scope all operations to. Defaults to `'/'`. */\n rootDir?: string;\n /** Default timeout for command execution in milliseconds. Defaults to 30 000. */\n timeout?: number;\n}\n\n/**\n * SandboxProvider backed by a WebContainer instance.\n *\n * @remarks\n * The consumer boots the WebContainer externally and passes it to the\n * constructor. All file operations delegate to `container.fs` and all\n * command execution delegates to `container.spawn()`.\n *\n * @example\n * ```ts\n * import { WebContainer } from '@webcontainer/api';\n * import { WebContainerSandbox } from '@yolo-labs/core-sandbox';\n *\n * const wc = await WebContainer.boot();\n * const sandbox = new WebContainerSandbox(wc, { rootDir: '/project' });\n * await sandbox.writeFile('index.ts', 'console.log(\"hi\")');\n * ```\n */\nexport class WebContainerSandbox implements SandboxProvider {\n private container: WebContainerLike;\n private rootDir: string;\n private timeout: number;\n\n constructor(container: WebContainerLike, options?: WebContainerSandboxOptions) {\n this.container = container;\n this.rootDir = (options?.rootDir ?? '/').replace(/\\/+$/, '') || '/';\n this.timeout = options?.timeout ?? 30_000;\n }\n\n // ---------------------------------------------------------------------------\n // File operations\n // ---------------------------------------------------------------------------\n\n async readFile(path: string): Promise<string> {\n return this.container.fs.readFile(this.resolvePath(path), 'utf-8');\n }\n\n async writeFile(path: string, content: string): Promise<void> {\n const resolved = this.resolvePath(path);\n const lastSlash = resolved.lastIndexOf('/');\n if (lastSlash > 0) {\n await this.container.fs.mkdir(resolved.substring(0, lastSlash), { recursive: true });\n }\n await this.container.fs.writeFile(resolved, content);\n }\n\n async deleteFile(path: string): Promise<void> {\n await this.container.fs.rm(this.resolvePath(path));\n }\n\n async listFiles(path: string): Promise<string[]> {\n return this.container.fs.readdir(this.resolvePath(path));\n }\n\n async mkdir(path: string): Promise<void> {\n await this.container.fs.mkdir(this.resolvePath(path), { recursive: true });\n }\n\n // ---------------------------------------------------------------------------\n // Command execution\n // ---------------------------------------------------------------------------\n\n async executeCommand(\n command: string,\n options?: CommandExecutionOptions,\n ): Promise<CommandResult> {\n const parts = parseCommand(command);\n const cmd = parts[0];\n const args = parts.slice(1);\n\n const spawnOpts: { cwd?: string; env?: Record<string, string> } = {};\n if (options?.cwd) {\n spawnOpts.cwd = this.resolvePath(options.cwd);\n }\n if (options?.env) {\n spawnOpts.env = options.env;\n }\n\n const proc = await this.container.spawn(cmd, args, spawnOpts);\n\n let output = '';\n const timeout = options?.timeout ?? this.timeout;\n\n const collectOutput = async (): Promise<void> => {\n const reader = proc.output.getReader();\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n output += value;\n options?.onStdout?.(value);\n }\n } finally {\n reader.releaseLock();\n }\n };\n\n let exitCode: number;\n try {\n const results = await Promise.race([\n Promise.all([proc.exit, collectOutput()]),\n new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error('Command timed out')), timeout),\n ),\n ]);\n exitCode = results[0];\n } catch {\n exitCode = 1;\n }\n\n return {\n exitCode,\n stdout: output,\n // WebContainer spawn() has a single combined output stream;\n // separate stderr is not available.\n stderr: '',\n };\n }\n\n // ---------------------------------------------------------------------------\n // Lifecycle\n // ---------------------------------------------------------------------------\n\n async create(): Promise<void> {\n if (this.rootDir !== '/') {\n await this.container.fs.mkdir(this.rootDir, { recursive: true });\n }\n }\n\n async destroy(): Promise<void> {\n // No-op: the consumer owns the WebContainer lifecycle.\n }\n\n async reset(): Promise<void> {\n // No-op: resetting a WebContainer's filesystem requires remounting.\n }\n\n // ---------------------------------------------------------------------------\n // Internals\n // ---------------------------------------------------------------------------\n\n private resolvePath(path: string): string {\n // Strip leading ./ and collapse slashes\n let normalized = path\n .replace(/^\\.\\//, '')\n .replace(/\\/+/g, '/')\n .replace(/\\/+$/, '');\n\n // '.' means the root directory itself\n if (normalized === '.' || normalized === '') {\n return this.rootDir;\n }\n\n if (this.rootDir === '/') {\n return normalized.startsWith('/') ? normalized : '/' + normalized;\n }\n\n const full = normalized.startsWith('/')\n ? normalized\n : this.rootDir + '/' + normalized;\n\n if (!full.startsWith(this.rootDir)) {\n throw new Error(`Path '${path}' is outside the root directory`);\n }\n return full;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Splits a command string into [command, ...args] with basic quote awareness.\n * Handles single and double quotes. Does not handle escapes or shell operators.\n */\nexport function parseCommand(command: string): string[] {\n const parts: string[] = [];\n let current = '';\n let inSingle = false;\n let inDouble = false;\n\n for (let i = 0; i < command.length; i++) {\n const ch = command[i];\n if (ch === \"'\" && !inDouble) {\n inSingle = !inSingle;\n } else if (ch === '\"' && !inSingle) {\n inDouble = !inDouble;\n } else if (ch === ' ' && !inSingle && !inDouble) {\n if (current) {\n parts.push(current);\n current = '';\n }\n } else {\n current += ch;\n }\n }\n if (current) parts.push(current);\n return parts;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yolo-labs/core-sandbox",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"import": "./dist/index.js"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@yolo-labs/core-types": "1.0.0"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"isolated-vm": ">=5.0.0",
|
|
21
|
+
"@webcontainer/api": ">=1.0.0"
|
|
22
|
+
},
|
|
23
|
+
"peerDependenciesMeta": {
|
|
24
|
+
"isolated-vm": {
|
|
25
|
+
"optional": true
|
|
26
|
+
},
|
|
27
|
+
"@webcontainer/api": {
|
|
28
|
+
"optional": true
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"tsup": "^8.0.0",
|
|
33
|
+
"typescript": "^5.5.0"
|
|
34
|
+
},
|
|
35
|
+
"description": "Sandbox environments (in-memory, Node VM) for safe code execution",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/yolo-labs-hq/monorepo.git",
|
|
40
|
+
"directory": "yolo-core/packages/sandbox"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public",
|
|
44
|
+
"registry": "https://registry.npmjs.org/"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/yolo-labs-hq/monorepo/tree/main/yolo-core#readme",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/yolo-labs-hq/monorepo/issues"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsup",
|
|
52
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
53
|
+
}
|
|
54
|
+
}
|