@superhq/shuru 0.2.0 → 0.4.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/README.md +123 -5
- package/package.json +1 -1
- package/src/index.ts +10 -0
- package/src/process-handle.ts +54 -0
- package/src/process.ts +62 -6
- package/src/sandbox.ts +129 -5
- package/src/types.ts +52 -0
package/README.md
CHANGED
|
@@ -15,14 +15,37 @@ import { Sandbox } from "@superhq/shuru";
|
|
|
15
15
|
|
|
16
16
|
const sb = await Sandbox.start();
|
|
17
17
|
|
|
18
|
+
// Buffered exec — run a command and get the full result
|
|
18
19
|
const result = await sb.exec("echo hello");
|
|
19
20
|
console.log(result.stdout); // "hello\n"
|
|
20
21
|
|
|
22
|
+
// Streaming spawn — real-time stdout/stderr
|
|
23
|
+
const proc = await sb.spawn("npm run dev");
|
|
24
|
+
proc.on("stdout", (data) => process.stdout.write(data));
|
|
25
|
+
proc.on("stderr", (data) => process.stderr.write(data));
|
|
26
|
+
proc.on("exit", (code) => console.log("exited:", code));
|
|
27
|
+
|
|
28
|
+
// File watching — guest-side inotify events
|
|
29
|
+
await sb.watch("/workspace", (event) => {
|
|
30
|
+
console.log(event.event, event.path); // "modify" "/workspace/src/main.ts"
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// File I/O
|
|
21
34
|
await sb.writeFile("/tmp/app.ts", "console.log('hi')");
|
|
22
35
|
const data = await sb.readFile("/tmp/app.ts"); // Uint8Array
|
|
23
|
-
const text = new TextDecoder().decode(data);
|
|
24
36
|
|
|
25
|
-
|
|
37
|
+
// Filesystem operations
|
|
38
|
+
await sb.mkdir("/workspace/src");
|
|
39
|
+
const entries = await sb.readDir("/workspace"); // { name, type, size }[]
|
|
40
|
+
const info = await sb.stat("/tmp/app.ts"); // { size, mode, mtime, isDir, ... }
|
|
41
|
+
await sb.copy("/tmp/app.ts", "/tmp/backup.ts");
|
|
42
|
+
await sb.rename("/tmp/backup.ts", "/tmp/old.ts");
|
|
43
|
+
await sb.chmod("/tmp/app.ts", 0o755);
|
|
44
|
+
await sb.remove("/tmp/old.ts");
|
|
45
|
+
if (await sb.exists("/tmp/app.ts")) { /* ... */ }
|
|
46
|
+
|
|
47
|
+
// Checkpoint — save disk state and stop
|
|
48
|
+
await sb.checkpoint("my-env");
|
|
26
49
|
```
|
|
27
50
|
|
|
28
51
|
### Start from a checkpoint
|
|
@@ -58,8 +81,8 @@ const sb = await Sandbox.start({
|
|
|
58
81
|
| `allowNet` | `boolean` | Enable network access |
|
|
59
82
|
| `ports` | `string[]` | Port forwards (`"host:guest"`) |
|
|
60
83
|
| `mounts` | `Record<string, string>` | Directory mounts (`{ hostPath: guestPath }`) |
|
|
61
|
-
| `secrets` | `Record<string, SecretConfig>` | Secrets to inject via proxy
|
|
62
|
-
| `network` | `NetworkConfig` | Network access policy
|
|
84
|
+
| `secrets` | `Record<string, SecretConfig>` | Secrets to inject via proxy |
|
|
85
|
+
| `network` | `NetworkConfig` | Network access policy |
|
|
63
86
|
| `shuruBin` | `string` | Path to shuru binary (default: `"shuru"`) |
|
|
64
87
|
|
|
65
88
|
## API
|
|
@@ -70,7 +93,47 @@ Boot a new microVM. Returns when the VM is ready.
|
|
|
70
93
|
|
|
71
94
|
### `sandbox.exec(command): Promise<ExecResult>`
|
|
72
95
|
|
|
73
|
-
Run a shell command in the VM. Returns `{ stdout, stderr, exitCode }`.
|
|
96
|
+
Run a shell command in the VM. Returns `{ stdout, stderr, exitCode }`. Stdout and stderr are buffered — the promise resolves when the command finishes.
|
|
97
|
+
|
|
98
|
+
### `sandbox.spawn(command, opts?): Promise<SandboxProcess>`
|
|
99
|
+
|
|
100
|
+
Spawn a long-running command in the VM. Returns a `SandboxProcess` handle immediately, streaming output in real-time.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const proc = await sb.spawn("npm run dev", { cwd: "/workspace" });
|
|
104
|
+
|
|
105
|
+
proc.on("stdout", (data: Buffer) => { /* real-time chunks */ });
|
|
106
|
+
proc.on("stderr", (data: Buffer) => { /* real-time chunks */ });
|
|
107
|
+
proc.on("exit", (code: number) => { /* process exited */ });
|
|
108
|
+
|
|
109
|
+
proc.write("input to stdin\n"); // write to stdin
|
|
110
|
+
await proc.kill(); // send SIGTERM
|
|
111
|
+
const exitCode = await proc.exited; // await completion
|
|
112
|
+
console.log(proc.pid); // process ID
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**`SpawnOptions`:**
|
|
116
|
+
| Option | Type | Description |
|
|
117
|
+
|--------|------|-------------|
|
|
118
|
+
| `cwd` | `string` | Working directory for the command |
|
|
119
|
+
| `env` | `Record<string, string>` | Environment variables |
|
|
120
|
+
|
|
121
|
+
### `sandbox.watch(path, handler, opts?): Promise<void>`
|
|
122
|
+
|
|
123
|
+
Watch a directory for file changes inside the guest VM. Uses guest-side inotify, so it detects writes to tmpfs overlays that host-side watchers cannot see.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
await sb.watch("/workspace", (event) => {
|
|
127
|
+
console.log(event.event, event.path);
|
|
128
|
+
// event.event: "create" | "modify" | "delete" | "rename"
|
|
129
|
+
// event.path: full path of the changed file
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**`WatchOptions`:**
|
|
134
|
+
| Option | Type | Default | Description |
|
|
135
|
+
|--------|------|---------|-------------|
|
|
136
|
+
| `recursive` | `boolean` | `true` | Watch subdirectories recursively |
|
|
74
137
|
|
|
75
138
|
### `sandbox.readFile(path): Promise<Uint8Array>`
|
|
76
139
|
|
|
@@ -80,6 +143,50 @@ Read a file from the VM. Returns raw bytes. Use `new TextDecoder().decode(data)`
|
|
|
80
143
|
|
|
81
144
|
Write a file to the VM. Accepts raw bytes or a string.
|
|
82
145
|
|
|
146
|
+
### `sandbox.mkdir(path, opts?): Promise<void>`
|
|
147
|
+
|
|
148
|
+
Create a directory. Creates parent directories by default.
|
|
149
|
+
|
|
150
|
+
| Option | Type | Default | Description |
|
|
151
|
+
|--------|------|---------|-------------|
|
|
152
|
+
| `recursive` | `boolean` | `true` | Create parent directories |
|
|
153
|
+
|
|
154
|
+
### `sandbox.readDir(path): Promise<DirEntry[]>`
|
|
155
|
+
|
|
156
|
+
List the contents of a directory. Each entry has `name`, `type` (`"file"`, `"dir"`, or `"symlink"`), and `size` in bytes.
|
|
157
|
+
|
|
158
|
+
### `sandbox.stat(path): Promise<StatResult>`
|
|
159
|
+
|
|
160
|
+
Get file metadata. Returns `{ size, mode, mtime, isDir, isFile, isSymlink }`. `mtime` is seconds since the Unix epoch. `mode` includes the file type bits (e.g. `0o100644` for a regular file with 644 permissions).
|
|
161
|
+
|
|
162
|
+
### `sandbox.remove(path, opts?): Promise<void>`
|
|
163
|
+
|
|
164
|
+
Delete a file or empty directory. To remove a non-empty directory, pass `{ recursive: true }`.
|
|
165
|
+
|
|
166
|
+
| Option | Type | Default | Description |
|
|
167
|
+
|--------|------|---------|-------------|
|
|
168
|
+
| `recursive` | `boolean` | `false` | Remove directories and their contents |
|
|
169
|
+
|
|
170
|
+
### `sandbox.rename(oldPath, newPath): Promise<void>`
|
|
171
|
+
|
|
172
|
+
Move or rename a file or directory within the guest filesystem. Atomic on the same filesystem.
|
|
173
|
+
|
|
174
|
+
### `sandbox.copy(src, dst, opts?): Promise<void>`
|
|
175
|
+
|
|
176
|
+
Copy a file. To copy a directory tree, pass `{ recursive: true }`.
|
|
177
|
+
|
|
178
|
+
| Option | Type | Default | Description |
|
|
179
|
+
|--------|------|---------|-------------|
|
|
180
|
+
| `recursive` | `boolean` | `false` | Copy directories recursively |
|
|
181
|
+
|
|
182
|
+
### `sandbox.chmod(path, mode): Promise<void>`
|
|
183
|
+
|
|
184
|
+
Change file permissions. `mode` is a numeric permission value (e.g. `0o755`).
|
|
185
|
+
|
|
186
|
+
### `sandbox.exists(path): Promise<boolean>`
|
|
187
|
+
|
|
188
|
+
Check if a path exists. Returns `true` if it does, `false` otherwise.
|
|
189
|
+
|
|
83
190
|
### `sandbox.checkpoint(name): Promise<void>`
|
|
84
191
|
|
|
85
192
|
Save the VM's disk state and stop the VM. To continue working, call `Sandbox.start({ from: name })`.
|
|
@@ -116,6 +223,17 @@ const sb = await Sandbox.start({
|
|
|
116
223
|
|
|
117
224
|
Omit `network.allow` to allow all domains.
|
|
118
225
|
|
|
226
|
+
## Concurrency
|
|
227
|
+
|
|
228
|
+
Multiple `spawn()` calls run concurrently in the same VM. Each gets a unique pid and independent stdout/stderr streams. You can mix `spawn()`, `exec()`, and `watch()` freely:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
// Start a dev server, run tests, and watch for changes — all at once
|
|
232
|
+
const server = await sb.spawn("npm run dev", { cwd: "/workspace" });
|
|
233
|
+
const watcher = sb.watch("/workspace", (e) => console.log(e));
|
|
234
|
+
const tests = await sb.exec("npm test");
|
|
235
|
+
```
|
|
236
|
+
|
|
119
237
|
## Requirements
|
|
120
238
|
|
|
121
239
|
- macOS 14+ on Apple Silicon
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
export { SandboxProcess } from "./process-handle";
|
|
1
2
|
export { Sandbox } from "./sandbox";
|
|
2
3
|
export type {
|
|
4
|
+
CopyOptions,
|
|
5
|
+
DirEntry,
|
|
6
|
+
ExecOptions,
|
|
3
7
|
ExecResult,
|
|
8
|
+
FileChangeEvent,
|
|
9
|
+
MkdirOptions,
|
|
4
10
|
NetworkConfig,
|
|
11
|
+
RemoveOptions,
|
|
5
12
|
SecretConfig,
|
|
13
|
+
SpawnOptions,
|
|
6
14
|
StartOptions,
|
|
15
|
+
StatResult,
|
|
16
|
+
WatchOptions,
|
|
7
17
|
} from "./types";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ShuruProcess } from "./process";
|
|
2
|
+
|
|
3
|
+
type EventMap = {
|
|
4
|
+
stdout: (data: Buffer) => void;
|
|
5
|
+
stderr: (data: Buffer) => void;
|
|
6
|
+
exit: (code: number) => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export class SandboxProcess {
|
|
10
|
+
readonly pid: string;
|
|
11
|
+
readonly exited: Promise<number>;
|
|
12
|
+
private proc: ShuruProcess;
|
|
13
|
+
private listeners: {
|
|
14
|
+
stdout: ((data: Buffer) => void)[];
|
|
15
|
+
stderr: ((data: Buffer) => void)[];
|
|
16
|
+
exit: ((code: number) => void)[];
|
|
17
|
+
} = { stdout: [], stderr: [], exit: [] };
|
|
18
|
+
|
|
19
|
+
constructor(proc: ShuruProcess, pid: string) {
|
|
20
|
+
this.proc = proc;
|
|
21
|
+
this.pid = pid;
|
|
22
|
+
|
|
23
|
+
this.exited = new Promise<number>((resolve) => {
|
|
24
|
+
proc.processHandlers.set(pid, {
|
|
25
|
+
onStdout: (data) => {
|
|
26
|
+
for (const h of this.listeners.stdout) h(data);
|
|
27
|
+
},
|
|
28
|
+
onStderr: (data) => {
|
|
29
|
+
for (const h of this.listeners.stderr) h(data);
|
|
30
|
+
},
|
|
31
|
+
onExit: (code) => {
|
|
32
|
+
for (const h of this.listeners.exit) h(code);
|
|
33
|
+
resolve(code);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
on<K extends keyof EventMap>(event: K, handler: EventMap[K]): this {
|
|
40
|
+
this.listeners[event].push(handler);
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
write(data: Buffer | string): void {
|
|
45
|
+
this.proc.sendNotification("input", {
|
|
46
|
+
pid: this.pid,
|
|
47
|
+
data: Buffer.from(data).toString("base64"),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async kill(): Promise<void> {
|
|
52
|
+
await this.proc.send("kill", { pid: this.pid });
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/process.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { Subprocess } from "bun";
|
|
2
|
-
import type { JsonRpcResponse, JsonRpcResult } from "./types";
|
|
2
|
+
import type { FileChangeEvent, JsonRpcResponse, JsonRpcResult } from "./types";
|
|
3
3
|
|
|
4
4
|
interface PendingRequest {
|
|
5
5
|
resolve: (value: JsonRpcResult) => void;
|
|
6
6
|
reject: (reason: Error) => void;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export interface ProcessEventHandlers {
|
|
10
|
+
onStdout: (data: Buffer) => void;
|
|
11
|
+
onStderr: (data: Buffer) => void;
|
|
12
|
+
onExit: (code: number) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
export class ShuruProcess {
|
|
10
16
|
private proc: Subprocess<"pipe", "pipe", "inherit"> | null = null;
|
|
11
17
|
private pending = new Map<number, PendingRequest>();
|
|
@@ -13,6 +19,12 @@ export class ShuruProcess {
|
|
|
13
19
|
private onReady: (() => void) | null = null;
|
|
14
20
|
private onReadyError: ((err: Error) => void) | null = null;
|
|
15
21
|
|
|
22
|
+
/** Handlers for spawned process output/exit, keyed by pid. */
|
|
23
|
+
readonly processHandlers = new Map<string, ProcessEventHandlers>();
|
|
24
|
+
|
|
25
|
+
/** Handler for file change events from the guest watcher. */
|
|
26
|
+
fileChangeHandler: ((event: FileChangeEvent) => void) | null = null;
|
|
27
|
+
|
|
16
28
|
async start(args: string[]): Promise<void> {
|
|
17
29
|
this.proc = Bun.spawn(args, {
|
|
18
30
|
stdin: "pipe",
|
|
@@ -55,6 +67,14 @@ export class ShuruProcess {
|
|
|
55
67
|
});
|
|
56
68
|
}
|
|
57
69
|
|
|
70
|
+
/** Send a fire-and-forget notification (no id, no response expected). */
|
|
71
|
+
sendNotification(method: string, params: Record<string, unknown>): void {
|
|
72
|
+
if (!this.proc) throw new Error("shuru process not started");
|
|
73
|
+
const line = `${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`;
|
|
74
|
+
this.proc.stdin.write(line);
|
|
75
|
+
this.proc.stdin.flush();
|
|
76
|
+
}
|
|
77
|
+
|
|
58
78
|
async stop(): Promise<void> {
|
|
59
79
|
if (!this.proc) return;
|
|
60
80
|
|
|
@@ -132,11 +152,47 @@ export class ShuruProcess {
|
|
|
132
152
|
}
|
|
133
153
|
|
|
134
154
|
private dispatch(msg: JsonRpcResponse): void {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
155
|
+
// Handle notifications (no id)
|
|
156
|
+
if ("method" in msg && !("id" in msg)) {
|
|
157
|
+
const params = msg.params as Record<string, unknown> | undefined;
|
|
158
|
+
|
|
159
|
+
switch (msg.method) {
|
|
160
|
+
case "ready": {
|
|
161
|
+
if (this.onReady) {
|
|
162
|
+
this.onReady();
|
|
163
|
+
this.onReady = null;
|
|
164
|
+
this.onReadyError = null;
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
case "output": {
|
|
169
|
+
if (!params) return;
|
|
170
|
+
const pid = params.pid as string;
|
|
171
|
+
const h = this.processHandlers.get(pid);
|
|
172
|
+
if (!h) return;
|
|
173
|
+
const buf = Buffer.from(params.data as string, "base64");
|
|
174
|
+
if (params.stream === "stdout") h.onStdout(buf);
|
|
175
|
+
else if (params.stream === "stderr") h.onStderr(buf);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
case "exit": {
|
|
179
|
+
if (!params) return;
|
|
180
|
+
const pid = params.pid as string;
|
|
181
|
+
const h = this.processHandlers.get(pid);
|
|
182
|
+
if (h) {
|
|
183
|
+
h.onExit(params.code as number);
|
|
184
|
+
this.processHandlers.delete(pid);
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
case "file_change": {
|
|
189
|
+
if (!params) return;
|
|
190
|
+
this.fileChangeHandler?.({
|
|
191
|
+
path: params.path as string,
|
|
192
|
+
event: params.event as FileChangeEvent["event"],
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
140
196
|
}
|
|
141
197
|
return;
|
|
142
198
|
}
|
package/src/sandbox.ts
CHANGED
|
@@ -1,11 +1,33 @@
|
|
|
1
1
|
import { ShuruProcess } from "./process";
|
|
2
|
-
import
|
|
2
|
+
import { SandboxProcess } from "./process-handle";
|
|
3
|
+
import type {
|
|
4
|
+
CopyOptions,
|
|
5
|
+
DirEntry,
|
|
6
|
+
ExecOptions,
|
|
7
|
+
ExecResult,
|
|
8
|
+
FileChangeEvent,
|
|
9
|
+
MkdirOptions,
|
|
10
|
+
RemoveOptions,
|
|
11
|
+
SpawnOptions,
|
|
12
|
+
StartOptions,
|
|
13
|
+
StatResult,
|
|
14
|
+
WatchOptions,
|
|
15
|
+
} from "./types";
|
|
3
16
|
|
|
4
17
|
const Method = {
|
|
5
18
|
EXEC: "exec",
|
|
19
|
+
SPAWN: "spawn",
|
|
6
20
|
READ_FILE: "read_file",
|
|
7
21
|
WRITE_FILE: "write_file",
|
|
8
22
|
CHECKPOINT: "checkpoint",
|
|
23
|
+
WATCH: "watch",
|
|
24
|
+
MKDIR: "mkdir",
|
|
25
|
+
READ_DIR: "read_dir",
|
|
26
|
+
STAT: "stat",
|
|
27
|
+
REMOVE: "remove",
|
|
28
|
+
RENAME: "rename",
|
|
29
|
+
COPY: "copy",
|
|
30
|
+
CHMOD: "chmod",
|
|
9
31
|
} as const;
|
|
10
32
|
|
|
11
33
|
export class Sandbox {
|
|
@@ -26,10 +48,15 @@ export class Sandbox {
|
|
|
26
48
|
return new Sandbox(proc);
|
|
27
49
|
}
|
|
28
50
|
|
|
29
|
-
async exec(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
51
|
+
async exec(
|
|
52
|
+
command: string | string[],
|
|
53
|
+
opts?: ExecOptions,
|
|
54
|
+
): Promise<ExecResult> {
|
|
55
|
+
const argv =
|
|
56
|
+
typeof command === "string"
|
|
57
|
+
? [opts?.shell ?? "sh", "-c", command]
|
|
58
|
+
: command;
|
|
59
|
+
const resp = await this.proc.send(Method.EXEC, { argv });
|
|
33
60
|
const r = resp.result as {
|
|
34
61
|
stdout: string;
|
|
35
62
|
stderr: string;
|
|
@@ -42,6 +69,35 @@ export class Sandbox {
|
|
|
42
69
|
};
|
|
43
70
|
}
|
|
44
71
|
|
|
72
|
+
async spawn(
|
|
73
|
+
command: string | string[],
|
|
74
|
+
opts?: SpawnOptions,
|
|
75
|
+
): Promise<SandboxProcess> {
|
|
76
|
+
const argv =
|
|
77
|
+
typeof command === "string"
|
|
78
|
+
? [opts?.shell ?? "sh", "-c", command]
|
|
79
|
+
: command;
|
|
80
|
+
const resp = await this.proc.send(Method.SPAWN, {
|
|
81
|
+
argv,
|
|
82
|
+
cwd: opts?.cwd,
|
|
83
|
+
env: opts?.env,
|
|
84
|
+
});
|
|
85
|
+
const { pid } = resp.result as { pid: string };
|
|
86
|
+
return new SandboxProcess(this.proc, pid);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async watch(
|
|
90
|
+
path: string,
|
|
91
|
+
handler: (event: FileChangeEvent) => void,
|
|
92
|
+
opts?: WatchOptions,
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
this.proc.fileChangeHandler = handler;
|
|
95
|
+
await this.proc.send(Method.WATCH, {
|
|
96
|
+
path,
|
|
97
|
+
recursive: opts?.recursive ?? true,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
45
101
|
async readFile(path: string): Promise<Uint8Array> {
|
|
46
102
|
const resp = await this.proc.send(Method.READ_FILE, { path });
|
|
47
103
|
const r = resp.result as { content: string };
|
|
@@ -53,6 +109,74 @@ export class Sandbox {
|
|
|
53
109
|
await this.proc.send(Method.WRITE_FILE, { path, content: b64 });
|
|
54
110
|
}
|
|
55
111
|
|
|
112
|
+
async mkdir(path: string, opts?: MkdirOptions): Promise<void> {
|
|
113
|
+
await this.proc.send(Method.MKDIR, {
|
|
114
|
+
path,
|
|
115
|
+
recursive: opts?.recursive ?? true,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async readDir(path: string): Promise<DirEntry[]> {
|
|
120
|
+
const resp = await this.proc.send(Method.READ_DIR, { path });
|
|
121
|
+
const r = resp.result as { entries: DirEntry[] };
|
|
122
|
+
return r.entries;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async stat(path: string): Promise<StatResult> {
|
|
126
|
+
const resp = await this.proc.send(Method.STAT, { path });
|
|
127
|
+
const r = resp.result as {
|
|
128
|
+
size: number;
|
|
129
|
+
mode: number;
|
|
130
|
+
mtime: number;
|
|
131
|
+
is_dir: boolean;
|
|
132
|
+
is_file: boolean;
|
|
133
|
+
is_symlink: boolean;
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
size: r.size,
|
|
137
|
+
mode: r.mode,
|
|
138
|
+
mtime: r.mtime,
|
|
139
|
+
isDir: r.is_dir,
|
|
140
|
+
isFile: r.is_file,
|
|
141
|
+
isSymlink: r.is_symlink,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async remove(path: string, opts?: RemoveOptions): Promise<void> {
|
|
146
|
+
await this.proc.send(Method.REMOVE, {
|
|
147
|
+
path,
|
|
148
|
+
recursive: opts?.recursive ?? false,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async rename(oldPath: string, newPath: string): Promise<void> {
|
|
153
|
+
await this.proc.send(Method.RENAME, {
|
|
154
|
+
old_path: oldPath,
|
|
155
|
+
new_path: newPath,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async copy(src: string, dst: string, opts?: CopyOptions): Promise<void> {
|
|
160
|
+
await this.proc.send(Method.COPY, {
|
|
161
|
+
src,
|
|
162
|
+
dst,
|
|
163
|
+
recursive: opts?.recursive ?? false,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async chmod(path: string, mode: number): Promise<void> {
|
|
168
|
+
await this.proc.send(Method.CHMOD, { path, mode });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async exists(path: string): Promise<boolean> {
|
|
172
|
+
try {
|
|
173
|
+
await this.stat(path);
|
|
174
|
+
return true;
|
|
175
|
+
} catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
56
180
|
async checkpoint(name: string): Promise<void> {
|
|
57
181
|
await this.proc.send(Method.CHECKPOINT, { name });
|
|
58
182
|
this.stopped = true;
|
package/src/types.ts
CHANGED
|
@@ -29,6 +29,57 @@ export interface ExecResult {
|
|
|
29
29
|
exitCode: number;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export interface ExecOptions {
|
|
33
|
+
/** Shell to use when command is a string. Defaults to "sh". Ignored when command is an array. */
|
|
34
|
+
shell?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SpawnOptions {
|
|
38
|
+
cwd?: string;
|
|
39
|
+
env?: Record<string, string>;
|
|
40
|
+
/** Shell to use when command is a string. Defaults to "sh". Ignored when command is an array. */
|
|
41
|
+
shell?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface WatchOptions {
|
|
45
|
+
recursive?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface FileChangeEvent {
|
|
49
|
+
path: string;
|
|
50
|
+
event: "create" | "modify" | "delete" | "rename";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --- Filesystem types ---
|
|
54
|
+
|
|
55
|
+
export interface DirEntry {
|
|
56
|
+
name: string;
|
|
57
|
+
type: "file" | "dir" | "symlink";
|
|
58
|
+
size: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface StatResult {
|
|
62
|
+
size: number;
|
|
63
|
+
mode: number;
|
|
64
|
+
/** Seconds since Unix epoch */
|
|
65
|
+
mtime: number;
|
|
66
|
+
isDir: boolean;
|
|
67
|
+
isFile: boolean;
|
|
68
|
+
isSymlink: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface MkdirOptions {
|
|
72
|
+
recursive?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface RemoveOptions {
|
|
76
|
+
recursive?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CopyOptions {
|
|
80
|
+
recursive?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
32
83
|
// --- JSON-RPC 2.0 wire types (internal) ---
|
|
33
84
|
|
|
34
85
|
export interface JsonRpcResult {
|
|
@@ -46,6 +97,7 @@ export interface JsonRpcError {
|
|
|
46
97
|
export interface JsonRpcNotification {
|
|
47
98
|
jsonrpc: "2.0";
|
|
48
99
|
method: string;
|
|
100
|
+
params?: Record<string, unknown>;
|
|
49
101
|
}
|
|
50
102
|
|
|
51
103
|
export type JsonRpcResponse =
|