@superhq/shuru 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -19,7 +19,8 @@ const result = await sb.exec("echo hello");
19
19
  console.log(result.stdout); // "hello\n"
20
20
 
21
21
  await sb.writeFile("/tmp/app.ts", "console.log('hi')");
22
- const content = await sb.readFile("/tmp/app.ts");
22
+ const data = await sb.readFile("/tmp/app.ts"); // Uint8Array
23
+ const text = new TextDecoder().decode(data);
23
24
 
24
25
  await sb.checkpoint("my-env"); // saves disk state and stops the VM
25
26
  ```
@@ -65,13 +66,13 @@ Boot a new microVM. Returns when the VM is ready.
65
66
 
66
67
  Run a shell command in the VM. Returns `{ stdout, stderr, exitCode }`.
67
68
 
68
- ### `sandbox.readFile(path): Promise<string>`
69
+ ### `sandbox.readFile(path): Promise<Uint8Array>`
69
70
 
70
- Read a file from the VM.
71
+ Read a file from the VM. Returns raw bytes. Use `new TextDecoder().decode(data)` for text files.
71
72
 
72
- ### `sandbox.writeFile(path, content): Promise<void>`
73
+ ### `sandbox.writeFile(path, content: Uint8Array | string): Promise<void>`
73
74
 
74
- Write a file to the VM.
75
+ Write a file to the VM. Accepts raw bytes or a string.
75
76
 
76
77
  ### `sandbox.checkpoint(name): Promise<void>`
77
78
 
package/package.json CHANGED
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "name": "@superhq/shuru",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
7
7
  "files": ["src"],
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/superhq-ai/shuru",
11
+ "directory": "packages/sdk"
12
+ },
8
13
  "scripts": {
9
14
  "test": "bun test test/unit/",
10
15
  "test:integration": "bun test test/integration/",
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { Sandbox } from "./sandbox";
2
- export type { ExecResult, StartOptions, StdioResponse } from "./types";
2
+ export type { ExecResult, StartOptions } from "./types";
package/src/process.ts CHANGED
@@ -1,15 +1,17 @@
1
1
  import type { Subprocess } from "bun";
2
- import type { StdioResponse } from "./types";
2
+ import type { JsonRpcResponse, JsonRpcResult } from "./types";
3
3
 
4
4
  interface PendingRequest {
5
- resolve: (value: StdioResponse) => void;
5
+ resolve: (value: JsonRpcResult) => void;
6
6
  reject: (reason: Error) => void;
7
7
  }
8
8
 
9
9
  export class ShuruProcess {
10
10
  private proc: Subprocess<"pipe", "pipe", "inherit"> | null = null;
11
- private pending = new Map<string, PendingRequest>();
11
+ private pending = new Map<number, PendingRequest>();
12
12
  private idCounter = 0;
13
+ private onReady: (() => void) | null = null;
14
+ private onReadyError: ((err: Error) => void) | null = null;
13
15
 
14
16
  async start(args: string[]): Promise<void> {
15
17
  this.proc = Bun.spawn(args, {
@@ -25,27 +27,28 @@ export class ShuruProcess {
25
27
  reject(new Error("shuru: timed out waiting for ready signal (30s)"));
26
28
  }, 30_000);
27
29
 
28
- this.pending.set("__ready__", {
29
- resolve: () => {
30
- clearTimeout(timeout);
31
- resolve();
32
- },
33
- reject: (err: Error) => {
34
- clearTimeout(timeout);
35
- reject(err);
36
- },
37
- });
30
+ this.onReady = () => {
31
+ clearTimeout(timeout);
32
+ resolve();
33
+ };
34
+ this.onReadyError = (err: Error) => {
35
+ clearTimeout(timeout);
36
+ reject(err);
37
+ };
38
38
  });
39
39
  }
40
40
 
41
- async send(msg: Record<string, unknown>): Promise<StdioResponse> {
41
+ send(
42
+ method: string,
43
+ params: Record<string, unknown>,
44
+ ): Promise<JsonRpcResult> {
42
45
  if (!this.proc) throw new Error("shuru process not started");
43
46
 
44
- const id = String(++this.idCounter);
45
- const line = `${JSON.stringify({ id, ...msg })}\n`;
47
+ const id = ++this.idCounter;
48
+ const line = `${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`;
46
49
  const proc = this.proc;
47
50
 
48
- return new Promise<StdioResponse>((resolve, reject) => {
51
+ return new Promise<JsonRpcResult>((resolve, reject) => {
49
52
  this.pending.set(id, { resolve, reject });
50
53
  proc.stdin.write(line);
51
54
  proc.stdin.flush();
@@ -88,21 +91,26 @@ export class ShuruProcess {
88
91
 
89
92
  const reader = this.proc.stdout.getReader();
90
93
  const decoder = new TextDecoder();
91
- let buffer = "";
94
+ let remainder = "";
92
95
 
93
96
  try {
94
97
  while (true) {
95
98
  const { done, value } = await reader.read();
96
99
  if (done) break;
97
100
 
98
- buffer += decoder.decode(value, { stream: true });
99
- const lines = buffer.split("\n");
100
- buffer = lines.pop() ?? "";
101
+ remainder += decoder.decode(value, { stream: true });
102
+
103
+ while (true) {
104
+ const newlineIdx = remainder.indexOf("\n");
105
+ if (newlineIdx === -1) break;
106
+ const line = remainder.slice(0, newlineIdx);
107
+ remainder = remainder.slice(newlineIdx + 1);
101
108
 
102
- for (const line of lines) {
103
109
  if (!line) continue;
110
+
104
111
  try {
105
- this.dispatch(JSON.parse(line) as StdioResponse);
112
+ const msg = JSON.parse(line) as JsonRpcResponse;
113
+ this.dispatch(msg);
106
114
  } catch {
107
115
  // malformed line
108
116
  }
@@ -112,33 +120,38 @@ export class ShuruProcess {
112
120
  // stream closed
113
121
  }
114
122
 
123
+ if (this.onReadyError) {
124
+ this.onReadyError(new Error("shuru process exited unexpectedly"));
125
+ this.onReady = null;
126
+ this.onReadyError = null;
127
+ }
115
128
  for (const [, req] of this.pending) {
116
129
  req.reject(new Error("shuru process exited unexpectedly"));
117
130
  }
118
131
  this.pending.clear();
119
132
  }
120
133
 
121
- private dispatch(msg: StdioResponse): void {
122
- if (msg.type === "ready") {
123
- const req = this.pending.get("__ready__");
124
- if (req) {
125
- this.pending.delete("__ready__");
126
- req.resolve(msg);
134
+ private dispatch(msg: JsonRpcResponse): void {
135
+ if ("method" in msg && msg.method === "ready") {
136
+ if (this.onReady) {
137
+ this.onReady();
138
+ this.onReady = null;
139
+ this.onReadyError = null;
127
140
  }
128
141
  return;
129
142
  }
130
143
 
131
- const { id } = msg;
132
- if (!id) return;
144
+ if (!("id" in msg) || msg.id == null) return;
145
+ const id = msg.id as number;
133
146
 
134
147
  const req = this.pending.get(id);
135
148
  if (!req) return;
136
149
  this.pending.delete(id);
137
150
 
138
- if (msg.type === "error") {
139
- req.reject(new Error(msg.error));
151
+ if ("error" in msg) {
152
+ req.reject(new Error(msg.error.message));
140
153
  } else {
141
- req.resolve(msg);
154
+ req.resolve(msg as JsonRpcResult);
142
155
  }
143
156
  }
144
157
  }
package/src/sandbox.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  import { ShuruProcess } from "./process";
2
2
  import type { ExecResult, StartOptions } from "./types";
3
3
 
4
+ const Method = {
5
+ EXEC: "exec",
6
+ READ_FILE: "read_file",
7
+ WRITE_FILE: "write_file",
8
+ CHECKPOINT: "checkpoint",
9
+ } as const;
10
+
4
11
  export class Sandbox {
5
12
  private proc: ShuruProcess;
6
13
  private stopped = false;
@@ -20,57 +27,34 @@ export class Sandbox {
20
27
  }
21
28
 
22
29
  async exec(command: string): Promise<ExecResult> {
23
- const resp = await this.proc.send({
24
- type: "exec",
30
+ const resp = await this.proc.send(Method.EXEC, {
25
31
  argv: ["sh", "-c", command],
26
32
  });
27
- if (resp.type !== "exec") {
28
- throw new Error(`unexpected response type: ${resp.type}`);
29
- }
33
+ const r = resp.result as {
34
+ stdout: string;
35
+ stderr: string;
36
+ exit_code: number;
37
+ };
30
38
  return {
31
- stdout: resp.stdout,
32
- stderr: resp.stderr,
33
- exitCode: resp.exit_code,
39
+ stdout: r.stdout,
40
+ stderr: r.stderr,
41
+ exitCode: r.exit_code,
34
42
  };
35
43
  }
36
44
 
37
- async readFile(path: string): Promise<string> {
38
- const resp = await this.proc.send({
39
- type: "exec",
40
- argv: ["cat", path],
41
- });
42
- if (resp.type !== "exec") {
43
- throw new Error(`unexpected response type: ${resp.type}`);
44
- }
45
- if (resp.exit_code !== 0) {
46
- throw new Error(
47
- `readFile failed (exit ${resp.exit_code}): ${resp.stderr}`,
48
- );
49
- }
50
- return resp.stdout;
45
+ async readFile(path: string): Promise<Uint8Array> {
46
+ const resp = await this.proc.send(Method.READ_FILE, { path });
47
+ const r = resp.result as { content: string };
48
+ return new Uint8Array(Buffer.from(r.content, "base64"));
51
49
  }
52
50
 
53
- async writeFile(path: string, content: string): Promise<void> {
54
- const b64 = btoa(content);
55
- const resp = await this.proc.send({
56
- type: "exec",
57
- argv: ["sh", "-c", `echo '${b64}' | base64 -d > "$1"`, "--", path],
58
- });
59
- if (resp.type !== "exec") {
60
- throw new Error(`unexpected response type: ${resp.type}`);
61
- }
62
- if (resp.exit_code !== 0) {
63
- throw new Error(
64
- `writeFile failed (exit ${resp.exit_code}): ${resp.stderr}`,
65
- );
66
- }
51
+ async writeFile(path: string, content: Uint8Array | string): Promise<void> {
52
+ const b64 = Buffer.from(content).toString("base64");
53
+ await this.proc.send(Method.WRITE_FILE, { path, content: b64 });
67
54
  }
68
55
 
69
56
  async checkpoint(name: string): Promise<void> {
70
- const resp = await this.proc.send({ type: "checkpoint", name });
71
- if (resp.type !== "checkpoint") {
72
- throw new Error(`unexpected response type: ${resp.type}`);
73
- }
57
+ await this.proc.send(Method.CHECKPOINT, { name });
74
58
  this.stopped = true;
75
59
  await this.proc.stop();
76
60
  }
package/src/types.ts CHANGED
@@ -15,14 +15,26 @@ export interface ExecResult {
15
15
  exitCode: number;
16
16
  }
17
17
 
18
- export type StdioResponse =
19
- | { type: "ready" }
20
- | {
21
- type: "exec";
22
- id: string;
23
- stdout: string;
24
- stderr: string;
25
- exit_code: number;
26
- }
27
- | { type: "checkpoint"; id: string; ok: boolean }
28
- | { type: "error"; id: string; error: string };
18
+ // --- JSON-RPC 2.0 wire types (internal) ---
19
+
20
+ export interface JsonRpcResult {
21
+ jsonrpc: "2.0";
22
+ id: number;
23
+ result: unknown;
24
+ }
25
+
26
+ export interface JsonRpcError {
27
+ jsonrpc: "2.0";
28
+ id: number;
29
+ error: { code: number; message: string };
30
+ }
31
+
32
+ export interface JsonRpcNotification {
33
+ jsonrpc: "2.0";
34
+ method: string;
35
+ }
36
+
37
+ export type JsonRpcResponse =
38
+ | JsonRpcResult
39
+ | JsonRpcError
40
+ | JsonRpcNotification;