-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathrepl.ts
More file actions
125 lines (102 loc) · 3.21 KB
/
Copy pathrepl.ts
File metadata and controls
125 lines (102 loc) · 3.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import path from 'node:path';
import * as readline from 'node:readline';
import type { Sandbox } from 'e2b';
/**
* Options parameter for the @see tinyE2bRepl function.
*/
export interface IE2bReplOptions {
/** The current working directory. */
cwd: string | undefined;
}
type CommandResult = {
exitCode: number;
stdout: string;
stderr: string;
};
function expandTilde(filePath: string, homedir: string): string {
return filePath.startsWith('~') ? path.posix.join(homedir, filePath.slice(1)) : filePath;
}
class ShellState {
#homedir: string;
#cwd: string;
#sandbox: Sandbox;
private constructor(homedir: string, cwd: string, sandbox: Sandbox) {
this.#homedir = homedir;
this.#cwd = cwd;
this.#sandbox = sandbox;
}
static async create(sandbox: Sandbox, cwd: string): Promise<ShellState> {
const homedir = await ShellState.probeHome(sandbox);
return new ShellState(homedir, expandTilde(cwd, homedir), sandbox);
}
/** Execute a shell command. Handles `cd` gracefully enough. */
async run(cmd: string): Promise<CommandResult | null> {
const trimmed: string = cmd.trim();
if (!trimmed) {
return null;
}
const splitCommand = trimmed.split(/\s+/);
if (splitCommand[0] === 'cd') {
const target = splitCommand[1] ?? '~';
this.#cwd = path.posix.isAbsolute(target)
? splitCommand[1]
: path.posix.join(this.#cwd, this.expandTilde(target));
return null;
}
const result = await this.#sandbox.commands.run(`cd ${this.#cwd} && ${trimmed}`);
return {
exitCode: result.exitCode,
stdout: result.stdout,
stderr: result.stderr,
};
}
private static async probeHome(sandbox: Sandbox): Promise<string> {
const result = await sandbox.commands.run('echo $HOME');
return result.stdout.trim();
}
private expandTilde(filePath: string): string {
return expandTilde(filePath, this.#homedir);
}
}
function question(rl: readline.Interface, prompt: string): Promise<string | null> {
return new Promise((resolve) => {
rl.once('close', () => resolve(null));
rl.question(prompt, (answer) => resolve(answer));
});
}
/**
* Spawns the REPL so that users can use it.
*
* This is a tiny REPL and is not a full shell, so it has an incredibly limited subset of regular shell commands.
*/
export default async function tinyE2bRepl(sandbox: Sandbox, options: IE2bReplOptions | undefined): Promise<void> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const shell = await ShellState.create(sandbox, options?.cwd ?? '~');
console.log(`Connected to ${options?.cwd ?? '~'}. Type "exit" or Ctrl+C to quit.\n`);
while (true) {
const input = await question(rl, '$ ');
if (input === null) {
break;
}
if (input.trim() === 'exit') {
break;
}
const result = await shell.run(input);
if (result === null) {
continue;
}
const { exitCode, stdout, stderr } = result;
if (stdout) {
process.stdout.write(stdout);
}
if (stderr) {
process.stderr.write(stderr);
}
if (exitCode !== null && exitCode !== 0) {
console.error(`The process exited with a non-zero exit code: ${exitCode}`);
}
}
}