sharjeenux 0.3.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.
@@ -0,0 +1,303 @@
1
+ export default async function repl() {
2
+ const [{ create }, readline] = await Promise.all([
3
+ import("../index.js"),
4
+ import("node:readline"),
5
+ ]);
6
+
7
+ if (!process.stdin.isTTY) {
8
+ throw new Error("This REPL requires an interactive terminal.");
9
+ }
10
+
11
+ const vm = create({
12
+ networking: true,
13
+ commandTimeoutMs: 0,
14
+ });
15
+
16
+ let running = null;
17
+ let line = [];
18
+ let cursor = 0;
19
+ let historyIndex = 0;
20
+ let savedLine = [];
21
+ let outputNeedsNewline = false;
22
+ let closing = false;
23
+ const history = [];
24
+
25
+ function redraw() {
26
+ const value = line.join("");
27
+ process.stdout.write(`\r\x1b[2K$ ${value}`);
28
+
29
+ const distanceFromEnd = line.length - cursor;
30
+ if (distanceFromEnd > 0) {
31
+ process.stdout.write(`\x1b[${distanceFromEnd}D`);
32
+ }
33
+ }
34
+
35
+ function prompt() {
36
+ line = [];
37
+ cursor = 0;
38
+ historyIndex = history.length;
39
+ savedLine = [];
40
+ process.stdout.write("$ ");
41
+ }
42
+
43
+ function writeOutput(chunk) {
44
+ process.stdout.write(chunk);
45
+ outputNeedsNewline = chunk.length > 0 && !/[\r\n]$/.test(chunk);
46
+ }
47
+
48
+ function setLine(value) {
49
+ line = Array.from(value);
50
+ cursor = line.length;
51
+ redraw();
52
+ }
53
+
54
+ function previousWord() {
55
+ while (cursor > 0 && /\s/.test(line[cursor - 1])) cursor -= 1;
56
+ while (cursor > 0 && !/\s/.test(line[cursor - 1])) cursor -= 1;
57
+ }
58
+
59
+ function nextWord() {
60
+ while (cursor < line.length && !/\s/.test(line[cursor])) cursor += 1;
61
+ while (cursor < line.length && /\s/.test(line[cursor])) cursor += 1;
62
+ }
63
+
64
+ function historyUp() {
65
+ if (!history.length || historyIndex === 0) return;
66
+ if (historyIndex === history.length) savedLine = [...line];
67
+ historyIndex -= 1;
68
+ setLine(history[historyIndex]);
69
+ }
70
+
71
+ function historyDown() {
72
+ if (historyIndex >= history.length) return;
73
+ historyIndex += 1;
74
+ if (historyIndex === history.length) {
75
+ line = [...savedLine];
76
+ cursor = line.length;
77
+ redraw();
78
+ } else {
79
+ setLine(history[historyIndex]);
80
+ }
81
+ }
82
+
83
+ async function execute(command) {
84
+ if (!command.trim()) {
85
+ prompt();
86
+ return;
87
+ }
88
+
89
+ if (command === "exit" || command === "quit") {
90
+ await close();
91
+ return;
92
+ }
93
+
94
+ running = vm.spawn(command, {
95
+ timeoutMs: 0,
96
+ interactive: true,
97
+ onOutput: writeOutput,
98
+ });
99
+
100
+ try {
101
+ const result = await running.result;
102
+
103
+ if (outputNeedsNewline) process.stdout.write("\n");
104
+ if (result.exitCode !== 0) {
105
+ process.stdout.write(`[exit ${result.exitCode}]\n`);
106
+ }
107
+ } catch (error) {
108
+ if (outputNeedsNewline) process.stdout.write("\n");
109
+ if (error?.name !== "AbortError") {
110
+ process.stderr.write(`${error?.stack ?? error}\n`);
111
+ }
112
+ } finally {
113
+ running = null;
114
+ outputNeedsNewline = false;
115
+ if (!closing) prompt();
116
+ }
117
+ }
118
+
119
+ function submitLine() {
120
+ process.stdout.write("\n");
121
+ const command = line.join("");
122
+
123
+ if (command.trim() && history.at(-1) !== command) {
124
+ history.push(command);
125
+ }
126
+
127
+ line = [];
128
+ cursor = 0;
129
+ historyIndex = history.length;
130
+ void execute(command);
131
+ }
132
+
133
+ function handleIdleKeypress(character, key) {
134
+ const name = key?.name;
135
+
136
+ if (name === "return" || name === "enter") {
137
+ submitLine();
138
+ return;
139
+ }
140
+
141
+ if (name === "up" || (key?.ctrl && name === "p")) {
142
+ historyUp();
143
+ return;
144
+ }
145
+
146
+ if (name === "down" || (key?.ctrl && name === "n")) {
147
+ historyDown();
148
+ return;
149
+ }
150
+
151
+ if (name === "left") {
152
+ if (key?.ctrl || key?.meta) previousWord();
153
+ else if (cursor > 0) cursor -= 1;
154
+ redraw();
155
+ return;
156
+ }
157
+
158
+ if (name === "right") {
159
+ if (key?.ctrl || key?.meta) nextWord();
160
+ else if (cursor < line.length) cursor += 1;
161
+ redraw();
162
+ return;
163
+ }
164
+
165
+ if (name === "home" || (key?.ctrl && name === "a")) {
166
+ cursor = 0;
167
+ redraw();
168
+ return;
169
+ }
170
+
171
+ if (name === "end" || (key?.ctrl && name === "e")) {
172
+ cursor = line.length;
173
+ redraw();
174
+ return;
175
+ }
176
+
177
+ if (name === "backspace") {
178
+ if (cursor > 0) {
179
+ line.splice(cursor - 1, 1);
180
+ cursor -= 1;
181
+ redraw();
182
+ }
183
+ return;
184
+ }
185
+
186
+ if (name === "delete") {
187
+ if (cursor < line.length) {
188
+ line.splice(cursor, 1);
189
+ redraw();
190
+ }
191
+ return;
192
+ }
193
+
194
+ if (key?.ctrl && name === "u") {
195
+ line.splice(0, cursor);
196
+ cursor = 0;
197
+ redraw();
198
+ return;
199
+ }
200
+
201
+ if (key?.ctrl && name === "k") {
202
+ line.splice(cursor);
203
+ redraw();
204
+ return;
205
+ }
206
+
207
+ if (key?.ctrl && name === "w") {
208
+ const end = cursor;
209
+ previousWord();
210
+ line.splice(cursor, end - cursor);
211
+ redraw();
212
+ return;
213
+ }
214
+
215
+ if (key?.meta && name === "b") {
216
+ previousWord();
217
+ redraw();
218
+ return;
219
+ }
220
+
221
+ if (key?.meta && name === "f") {
222
+ nextWord();
223
+ redraw();
224
+ return;
225
+ }
226
+
227
+ if (key?.ctrl && name === "l") {
228
+ process.stdout.write("\x1b[2J\x1b[H");
229
+ redraw();
230
+ return;
231
+ }
232
+
233
+ if (key?.ctrl && name === "c") {
234
+ line = [];
235
+ cursor = 0;
236
+ process.stdout.write("^C\n");
237
+ prompt();
238
+ return;
239
+ }
240
+
241
+ if (key?.ctrl && name === "d") {
242
+ if (!line.length) void close();
243
+ else if (cursor < line.length) {
244
+ line.splice(cursor, 1);
245
+ redraw();
246
+ }
247
+ return;
248
+ }
249
+
250
+ if (name === "tab") {
251
+ process.stdout.write("\x07");
252
+ return;
253
+ }
254
+
255
+ if (character && !key?.ctrl && !key?.meta && character >= " ") {
256
+ const inserted = Array.from(character);
257
+ line.splice(cursor, 0, ...inserted);
258
+ cursor += inserted.length;
259
+ redraw();
260
+ }
261
+ }
262
+
263
+ function handleKeypress(character, key) {
264
+ if (running) {
265
+ // Preserve the exact terminal sequence for guest applications. This
266
+ // gives Node/Python REPLs and other TTY programs their own arrow keys,
267
+ // Ctrl+C, Ctrl+D, Home/End, Delete and function-key handling.
268
+ const sequence = key?.sequence ?? character;
269
+ if (sequence) running.write(sequence);
270
+ return;
271
+ }
272
+
273
+ handleIdleKeypress(character, key);
274
+ }
275
+
276
+ async function close(exitCode = 0) {
277
+ if (closing) return;
278
+ closing = true;
279
+
280
+ process.stdin.off("keypress", handleKeypress);
281
+ process.stdin.setRawMode(false);
282
+ process.stdin.pause();
283
+
284
+ running?.kill();
285
+ await vm.shutdown();
286
+
287
+ process.stdout.write("\n");
288
+ process.exitCode = exitCode;
289
+ }
290
+
291
+ process.once("SIGTERM", () => void close(143));
292
+ process.once("SIGHUP", () => void close(129));
293
+
294
+ process.stdout.write("🔌 Booting Sharjeenux...\n");
295
+ await vm.initialize();
296
+
297
+ readline.emitKeypressEvents(process.stdin);
298
+ process.stdin.setRawMode(true);
299
+ process.stdin.resume();
300
+ process.stdin.on("keypress", handleKeypress);
301
+
302
+ prompt();
303
+ }
package/index.cjs ADDED
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+
3
+ let modulePromise;
4
+ const load = () => modulePromise ||= import("./index.js");
5
+
6
+ class CommandTimeoutError {
7
+ static [Symbol.hasInstance](value) {
8
+ return value?.name === "CommandTimeoutError";
9
+ }
10
+ }
11
+
12
+ class Sharjeenux {
13
+ constructor(options) {
14
+ this.ready = false;
15
+ this._instance = load().then(module => module.create(options));
16
+ }
17
+
18
+ async initialize() {
19
+ const instance = await this._instance;
20
+ await instance.initialize();
21
+ this.ready = true;
22
+ return this;
23
+ }
24
+
25
+ async send(...args) {
26
+ return (await this._instance).send(...args);
27
+ }
28
+
29
+ async exec(...args) {
30
+ return (await this._instance).exec(...args);
31
+ }
32
+
33
+ spawn(...args) {
34
+ const handle = this._instance.then(instance => instance.spawn(...args));
35
+ return {
36
+ result: handle.then(value => value.result),
37
+ exit: handle.then(value => value.exit),
38
+ write: data => void handle.then(value => value.write(data)),
39
+ kill: () => void handle.then(value => value.kill()),
40
+ };
41
+ }
42
+
43
+ write(data) {
44
+ void this._instance.then(instance => instance.write(data));
45
+ }
46
+
47
+ interrupt() {
48
+ void this._instance.then(instance => instance.interrupt());
49
+ }
50
+
51
+ on(...args) {
52
+ void this._instance.then(instance => instance.on(...args));
53
+ return this;
54
+ }
55
+
56
+ async shutdown() {
57
+ this.ready = false;
58
+ return (await this._instance).shutdown();
59
+ }
60
+ }
61
+
62
+ exports.CommandTimeoutError = CommandTimeoutError;
63
+ exports.Sharjeenux = Sharjeenux;
64
+ exports.create = options => new Sharjeenux(options);
65
+ exports.initialize = (...args) => load().then(module => module.initialize(...args));
66
+ exports.send = (...args) => load().then(module => module.send(...args));
67
+ exports.exec = (...args) => load().then(module => module.exec(...args));
68
+ exports.spawn = (...args) => load().then(module => module.spawn(...args));
69
+ exports.write = (...args) => load().then(module => module.write(...args));
70
+ exports.shutdown = (...args) => load().then(module => module.shutdown(...args));
package/index.d.ts ADDED
@@ -0,0 +1,66 @@
1
+ export interface SharjeenuxOptions {
2
+ /** Guest RAM in MiB. Must be a power of two. Default: 2048. */
3
+ memoryMB?: number;
4
+ /** Maximum initialization time. Default: 180000. */
5
+ bootTimeoutMs?: number;
6
+ /** Default command timeout. Default: 1800000. Use 0 to disable. */
7
+ commandTimeoutMs?: number;
8
+ /** Configure the guest network through v86's fetch backend. Default: true. */
9
+ networking?: boolean;
10
+ /** "fetch" for HTTP-only outbound traffic, or a wisp(s) relay URL for arbitrary outbound TCP/HTTPS. Default: "fetch". */
11
+ networkRelayUrl?: string;
12
+ /** Hash-check decoded VM assets during initialization. Default: true. */
13
+ verifyAssets?: boolean;
14
+ }
15
+
16
+ export interface CommandOptions {
17
+ /** Override the instance command timeout. Use 0 to disable. */
18
+ timeoutMs?: number;
19
+ /** Receive command output as it arrives. */
20
+ onOutput?: (chunk: string) => void;
21
+ /** Abort the command. The guest receives Ctrl+C. */
22
+ signal?: AbortSignal;
23
+ }
24
+
25
+ export interface CommandResult {
26
+ command: string;
27
+ output: string;
28
+ exitCode: number;
29
+ durationMs: number;
30
+ }
31
+
32
+ export class RunningCommand {
33
+ readonly result: Promise<CommandResult>;
34
+ readonly exit: Promise<number>;
35
+ write(data: string): void;
36
+ kill(): void;
37
+ }
38
+
39
+ export class CommandTimeoutError extends Error {
40
+ readonly command: string;
41
+ readonly output: string;
42
+ readonly timeoutMs: number;
43
+ }
44
+
45
+ export class Sharjeenux {
46
+ constructor(options?: SharjeenuxOptions);
47
+ readonly ready: boolean;
48
+ initialize(): Promise<this>;
49
+ send(command: string, options?: CommandOptions): Promise<string>;
50
+ exec(command: string, options?: CommandOptions): Promise<CommandResult>;
51
+ spawn(command: string, options?: CommandOptions): RunningCommand;
52
+ write(data: string): void;
53
+ interrupt(): void;
54
+ shutdown(): Promise<void>;
55
+ on(event: "output", listener: (chunk: string) => void): this;
56
+ on(event: "ready", listener: () => void): this;
57
+ on(event: "shutdown", listener: () => void): this;
58
+ }
59
+
60
+ export function create(options?: SharjeenuxOptions): Sharjeenux;
61
+ export function initialize(options?: SharjeenuxOptions): Promise<Sharjeenux>;
62
+ export function send(command: string, options?: CommandOptions): Promise<string>;
63
+ export function exec(command: string, options?: CommandOptions): Promise<CommandResult>;
64
+ export function spawn(command: string, options?: CommandOptions): Promise<RunningCommand>;
65
+ export function write(data: string): Promise<void>;
66
+ export function shutdown(): Promise<void>;