@parel/sandbox-vercel 0.1.1 → 0.2.1
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.js +194 -4
- package/package.json +10 -17
- package/parel.plugin.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,193 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { Buffer } from "buffer";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
|
|
4
|
+
// ../../capabilities/sandbox/dist/index.js
|
|
5
|
+
var PAREL_SANDBOX_CAPABILITY = "parel.sandbox";
|
|
6
|
+
var PROCESS_VIEW_STORE_PREFIX = "sandbox_process:";
|
|
7
|
+
var PORT_VIEW_STORE_PREFIX = "sandbox_port:";
|
|
8
|
+
function viewShellQuote(value) {
|
|
9
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
10
|
+
}
|
|
11
|
+
function viewCreateId(prefix) {
|
|
12
|
+
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
13
|
+
}
|
|
14
|
+
function clampBytes(value, fallback, max) {
|
|
15
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return fallback;
|
|
16
|
+
return Math.min(Math.floor(value), max);
|
|
17
|
+
}
|
|
18
|
+
function createSandboxCapabilityViews(capability, store) {
|
|
19
|
+
const requireFs = () => {
|
|
20
|
+
if (!capability.fs) {
|
|
21
|
+
throw new Error(`Sandbox provider ${capability.provider} does not support filesystem access`);
|
|
22
|
+
}
|
|
23
|
+
return capability.fs;
|
|
24
|
+
};
|
|
25
|
+
const shell = async (command, opts) => {
|
|
26
|
+
const proc = capability.process;
|
|
27
|
+
if (!proc) {
|
|
28
|
+
throw new Error(`Sandbox provider ${capability.provider} does not support process execution`);
|
|
29
|
+
}
|
|
30
|
+
if (proc.shell) return proc.shell(command, opts);
|
|
31
|
+
return proc.exec(["sh", "-lc", command], opts);
|
|
32
|
+
};
|
|
33
|
+
const filesystem = {
|
|
34
|
+
async readFile(path) {
|
|
35
|
+
return requireFs().readFile(path);
|
|
36
|
+
},
|
|
37
|
+
async writeFile(path, content) {
|
|
38
|
+
await requireFs().writeFile(path, content);
|
|
39
|
+
},
|
|
40
|
+
async exists(path) {
|
|
41
|
+
const fs = requireFs();
|
|
42
|
+
if (fs.exists) return fs.exists(path);
|
|
43
|
+
try {
|
|
44
|
+
await fs.readFile(path);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
async listDir(path) {
|
|
51
|
+
return (await requireFs().listDir(path)).map((entry) => entry.name);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const exec = {
|
|
55
|
+
async run(command) {
|
|
56
|
+
const result = await shell(command);
|
|
57
|
+
if (result.exitCode !== 0) {
|
|
58
|
+
const detail = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
59
|
+
return `Exit code: ${result.exitCode}${detail ? `
|
|
60
|
+
${detail}` : ""}`;
|
|
61
|
+
}
|
|
62
|
+
return result.stdout;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const processKey = (id) => `${PROCESS_VIEW_STORE_PREFIX}${id}`;
|
|
66
|
+
const process = {
|
|
67
|
+
async start(command, opts = {}) {
|
|
68
|
+
if (!command.trim()) throw new Error("command must be a non-empty string");
|
|
69
|
+
const id = viewCreateId("proc");
|
|
70
|
+
const dir = `/tmp/parel/processes/${id}`;
|
|
71
|
+
const stdoutPath = `${dir}/stdout.log`;
|
|
72
|
+
const stderrPath = `${dir}/stderr.log`;
|
|
73
|
+
const script = `(${command}) > ${viewShellQuote(stdoutPath)} 2> ${viewShellQuote(stderrPath)}`;
|
|
74
|
+
const runner = `nohup sh -lc ${viewShellQuote(script)} >/dev/null 2>&1 & echo $!`;
|
|
75
|
+
const launch = `mkdir -p ${viewShellQuote(dir)} && if command -v setsid >/dev/null 2>&1; then setsid ${runner}; else sh -lmc ${viewShellQuote(runner)}; fi`;
|
|
76
|
+
const result = await shell(launch, {
|
|
77
|
+
...opts.cwd ? { cwd: opts.cwd } : {},
|
|
78
|
+
...opts.envs ? { env: opts.envs } : {},
|
|
79
|
+
...opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}
|
|
80
|
+
});
|
|
81
|
+
const pid = Number.parseInt(result.stdout.trim().split("\n").pop() ?? "", 10);
|
|
82
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
83
|
+
throw new Error(`Failed to start background process: ${result.stderr || result.stdout}`);
|
|
84
|
+
}
|
|
85
|
+
const record = {
|
|
86
|
+
id,
|
|
87
|
+
pid,
|
|
88
|
+
command,
|
|
89
|
+
...opts.cwd ? { cwd: opts.cwd } : {},
|
|
90
|
+
stdoutPath,
|
|
91
|
+
stderrPath,
|
|
92
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
93
|
+
status: "running"
|
|
94
|
+
};
|
|
95
|
+
await store.set(processKey(id), record);
|
|
96
|
+
return record;
|
|
97
|
+
},
|
|
98
|
+
async list() {
|
|
99
|
+
const keys = await store.list(PROCESS_VIEW_STORE_PREFIX);
|
|
100
|
+
const records = (await Promise.all(keys.map((key) => store.get(key)))).filter((record) => Boolean(record));
|
|
101
|
+
if (records.length === 0) return [];
|
|
102
|
+
const pids = records.map((record) => record.pid).join(" ");
|
|
103
|
+
const probe = await shell(
|
|
104
|
+
`for p in ${pids}; do kill -0 "$p" 2>/dev/null && echo "$p"; done; true`
|
|
105
|
+
);
|
|
106
|
+
const running = new Set(
|
|
107
|
+
probe.stdout.split("\n").map((line) => Number.parseInt(line.trim(), 10)).filter((pid) => Number.isFinite(pid))
|
|
108
|
+
);
|
|
109
|
+
return records.map((record) => ({
|
|
110
|
+
...record,
|
|
111
|
+
status: record.status === "stopped" ? "stopped" : running.has(record.pid) ? "running" : "unknown"
|
|
112
|
+
}));
|
|
113
|
+
},
|
|
114
|
+
async tail(processId, opts = {}) {
|
|
115
|
+
const record = await store.get(processKey(processId));
|
|
116
|
+
if (!record) throw new Error(`unknown process: ${processId}`);
|
|
117
|
+
const maxBytes = clampBytes(opts.maxBytes, 32 * 1024, 1024 * 1024);
|
|
118
|
+
const readTail = async (path) => {
|
|
119
|
+
const result = await shell(
|
|
120
|
+
`tail -c ${maxBytes} ${viewShellQuote(path)} 2>/dev/null || true`
|
|
121
|
+
);
|
|
122
|
+
return result.stdout;
|
|
123
|
+
};
|
|
124
|
+
return {
|
|
125
|
+
stdout: await readTail(record.stdoutPath),
|
|
126
|
+
stderr: await readTail(record.stderrPath),
|
|
127
|
+
stdoutPath: record.stdoutPath,
|
|
128
|
+
stderrPath: record.stderrPath
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
async stop(processId) {
|
|
132
|
+
const record = await store.get(processKey(processId));
|
|
133
|
+
if (!record) throw new Error(`unknown process: ${processId}`);
|
|
134
|
+
const result = await shell(
|
|
135
|
+
`kill -- -${record.pid} 2>/dev/null; kill ${record.pid} 2>/dev/null; sleep 0.2; if kill -0 -- -${record.pid} 2>/dev/null || kill -0 ${record.pid} 2>/dev/null; then echo still-alive; fi`
|
|
136
|
+
);
|
|
137
|
+
const stopped = !result.stdout.includes("still-alive");
|
|
138
|
+
const next = {
|
|
139
|
+
...record,
|
|
140
|
+
status: stopped ? "stopped" : record.status
|
|
141
|
+
};
|
|
142
|
+
await store.set(processKey(processId), next);
|
|
143
|
+
return { stopped, process: next };
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const portKey = (id) => `${PORT_VIEW_STORE_PREFIX}${id}`;
|
|
147
|
+
const ports = {
|
|
148
|
+
async expose(port, opts = {}) {
|
|
149
|
+
if (!capability.ports) {
|
|
150
|
+
throw new Error(`Sandbox provider ${capability.provider} does not support ports`);
|
|
151
|
+
}
|
|
152
|
+
const protocol = opts.protocol ?? "https";
|
|
153
|
+
const exposed = await capability.ports.expose(port, { protocol });
|
|
154
|
+
const url = exposed.url.includes("://") ? exposed.url : `${protocol}://${exposed.url}`;
|
|
155
|
+
const host = url.replace(/^[a-z]+:\/\//, "");
|
|
156
|
+
const record = {
|
|
157
|
+
id: String(exposed.port),
|
|
158
|
+
port: exposed.port,
|
|
159
|
+
host,
|
|
160
|
+
url,
|
|
161
|
+
protocol: exposed.protocol ?? protocol,
|
|
162
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
163
|
+
};
|
|
164
|
+
await store.set(portKey(record.id), record);
|
|
165
|
+
return record;
|
|
166
|
+
},
|
|
167
|
+
async list() {
|
|
168
|
+
const keys = await store.list(PORT_VIEW_STORE_PREFIX);
|
|
169
|
+
return (await Promise.all(keys.map((key) => store.get(key)))).filter(
|
|
170
|
+
(record) => Boolean(record)
|
|
171
|
+
);
|
|
172
|
+
},
|
|
173
|
+
async revoke(port) {
|
|
174
|
+
const key = portKey(String(port));
|
|
175
|
+
const existing = await store.get(key);
|
|
176
|
+
if (!existing) return false;
|
|
177
|
+
if (!capability.ports?.unexpose) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Sandbox provider ${capability.provider} cannot unexpose ports; the exposed URL stays live until the sandbox stops`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
await capability.ports.unexpose(existing.port);
|
|
183
|
+
await store.delete(key);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
return { filesystem, exec, process, ports };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/index.ts
|
|
6
191
|
import { definePlugin, LifecycleEvent } from "@parel/plugin-sdk";
|
|
7
192
|
import { Sandbox as VercelSandboxClient } from "@vercel/sandbox";
|
|
8
193
|
|
|
@@ -12,7 +197,7 @@ var parel_plugin_default = {
|
|
|
12
197
|
version: "0.0.0",
|
|
13
198
|
description: "Provides the standard parel.sandbox capability backed by Vercel Sandbox.",
|
|
14
199
|
provides: {
|
|
15
|
-
capabilities: ["parel.sandbox"]
|
|
200
|
+
capabilities: ["parel.sandbox", "filesystem", "exec", "process", "ports"]
|
|
16
201
|
},
|
|
17
202
|
requires: {
|
|
18
203
|
permissions: {
|
|
@@ -316,6 +501,11 @@ var index_default = definePlugin({
|
|
|
316
501
|
await disposeSandbox();
|
|
317
502
|
});
|
|
318
503
|
ctx.provide(PAREL_SANDBOX_CAPABILITY, capability);
|
|
504
|
+
const views = createSandboxCapabilityViews(capability, ctx.store);
|
|
505
|
+
ctx.provide("filesystem", views.filesystem);
|
|
506
|
+
ctx.provide("exec", views.exec);
|
|
507
|
+
ctx.provide("process", views.process);
|
|
508
|
+
ctx.provide("ports", views.ports);
|
|
319
509
|
}
|
|
320
510
|
});
|
|
321
511
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parel/sandbox-vercel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "PAREL sandbox capability provider plugin for Vercel Sandbox.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
"type": "module",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
|
-
"bun": "./src/index.ts",
|
|
19
18
|
"types": "./dist/index.d.ts",
|
|
20
19
|
"import": "./dist/index.js"
|
|
21
20
|
}
|
|
@@ -26,15 +25,10 @@
|
|
|
26
25
|
"LICENSE",
|
|
27
26
|
"README.md"
|
|
28
27
|
],
|
|
29
|
-
"scripts": {
|
|
30
|
-
"build": "tsup",
|
|
31
|
-
"test": "vitest run --passWithNoTests",
|
|
32
|
-
"lint": "biome check src/"
|
|
33
|
-
},
|
|
34
28
|
"dependencies": {
|
|
35
|
-
"@
|
|
36
|
-
"@parel/
|
|
37
|
-
"@
|
|
29
|
+
"@vercel/sandbox": "^2.1.1",
|
|
30
|
+
"@parel/capability-sandbox": "0.2.0",
|
|
31
|
+
"@parel/plugin-sdk": "0.4.1"
|
|
38
32
|
},
|
|
39
33
|
"devDependencies": {
|
|
40
34
|
"@types/node": "^25.9.1",
|
|
@@ -43,12 +37,11 @@
|
|
|
43
37
|
"vitest": "^4.1.8"
|
|
44
38
|
},
|
|
45
39
|
"publishConfig": {
|
|
46
|
-
"exports": {
|
|
47
|
-
".": {
|
|
48
|
-
"types": "./dist/index.d.ts",
|
|
49
|
-
"import": "./dist/index.js"
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
40
|
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsup",
|
|
44
|
+
"test": "vitest run --passWithNoTests",
|
|
45
|
+
"lint": "biome check src/"
|
|
53
46
|
}
|
|
54
|
-
}
|
|
47
|
+
}
|
package/parel.plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"version": "0.0.0",
|
|
4
4
|
"description": "Provides the standard parel.sandbox capability backed by Vercel Sandbox.",
|
|
5
5
|
"provides": {
|
|
6
|
-
"capabilities": ["parel.sandbox"]
|
|
6
|
+
"capabilities": ["parel.sandbox", "filesystem", "exec", "process", "ports"]
|
|
7
7
|
},
|
|
8
8
|
"requires": {
|
|
9
9
|
"permissions": {
|