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.
- package/LICENSE +21 -0
- package/README.md +175 -0
- package/SOURCE_OFFER.md +22 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/compliance/README.md +38 -0
- package/compliance/build-definition.tar.xz +0 -0
- package/compliance/buildroot.config +4970 -0
- package/compliance/linux.config +3827 -0
- package/features/repl.js +303 -0
- package/index.cjs +70 -0
- package/index.d.ts +66 -0
- package/index.js +465 -0
- package/package.json +69 -0
package/index.js
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { V86 } from "v86";
|
|
8
|
+
|
|
9
|
+
const ASSETS_URL = new URL("./assets/", import.meta.url);
|
|
10
|
+
const CACHE_ROOT = path.join(tmpdir(), "sharjeenux", "decoded-assets-v1");
|
|
11
|
+
const ANSI_PATTERN = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
|
|
12
|
+
const DEFAULTS = Object.freeze({
|
|
13
|
+
memoryMB: 2048,
|
|
14
|
+
bootTimeoutMs: 180_000,
|
|
15
|
+
commandTimeoutMs: 1_800_000,
|
|
16
|
+
networking: true,
|
|
17
|
+
networkRelayUrl: "fetch",
|
|
18
|
+
verifyAssets: true,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
let defaultInstance;
|
|
22
|
+
let manifestPromise;
|
|
23
|
+
|
|
24
|
+
export class CommandTimeoutError extends Error {
|
|
25
|
+
constructor(command, output, timeoutMs) {
|
|
26
|
+
super(`Command timed out after ${timeoutMs} ms: ${command}`);
|
|
27
|
+
this.name = "CommandTimeoutError";
|
|
28
|
+
this.command = command;
|
|
29
|
+
this.output = output;
|
|
30
|
+
this.timeoutMs = timeoutMs;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function deferred() {
|
|
35
|
+
let resolve;
|
|
36
|
+
let reject;
|
|
37
|
+
const promise = new Promise((res, rej) => {
|
|
38
|
+
resolve = res;
|
|
39
|
+
reject = rej;
|
|
40
|
+
});
|
|
41
|
+
return { promise, resolve, reject };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function cleanOutput(value) {
|
|
45
|
+
return value
|
|
46
|
+
.replace(/\r/g, "")
|
|
47
|
+
.replace(ANSI_PATTERN, "")
|
|
48
|
+
.replace(/[\t ]+\n/g, "\n")
|
|
49
|
+
.replace(/^\n+|\n+$/g, "");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function shellQuote(value) {
|
|
53
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toArrayBuffer(buffer) {
|
|
57
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function getManifest() {
|
|
61
|
+
manifestPromise ||= readFile(new URL("manifest.json", ASSETS_URL), "utf8").then(JSON.parse);
|
|
62
|
+
return manifestPromise;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function loadImage(name, verify) {
|
|
66
|
+
const manifest = await getManifest();
|
|
67
|
+
const image = manifest.images[name];
|
|
68
|
+
if (!image) throw new Error(`Image is missing from asset manifest: ${name}`);
|
|
69
|
+
|
|
70
|
+
const cachePath = path.join(CACHE_ROOT, `${name}-${image.sha256}.bin`);
|
|
71
|
+
try {
|
|
72
|
+
const cached = await readFile(cachePath);
|
|
73
|
+
if (cached.length === image.bytes) {
|
|
74
|
+
if (!verify || createHash("sha256").update(cached).digest("hex") === image.sha256) {
|
|
75
|
+
return toArrayBuffer(cached);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// A cold or read-only cache is valid; chunks remain the source of truth.
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const output = Buffer.allocUnsafe(image.bytes);
|
|
83
|
+
let cursor = 0;
|
|
84
|
+
const concurrency = 256;
|
|
85
|
+
|
|
86
|
+
for (let start = 0; start < image.chunks; start += concurrency) {
|
|
87
|
+
const end = Math.min(image.chunks, start + concurrency);
|
|
88
|
+
const parts = await Promise.all(Array.from({ length: end - start }, (_, offset) => {
|
|
89
|
+
const serial = String(start + offset).padStart(6, "0");
|
|
90
|
+
return readFile(new URL(`chunks/${name}/${serial.slice(0, 3)}/${serial}.b64`, ASSETS_URL), "ascii");
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
for (const part of parts) {
|
|
94
|
+
const decoded = Buffer.from(part, "base64");
|
|
95
|
+
decoded.copy(output, cursor);
|
|
96
|
+
cursor += decoded.length;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (cursor !== image.bytes) {
|
|
101
|
+
throw new Error(`Decoded ${name} size mismatch: expected ${image.bytes}, received ${cursor}`);
|
|
102
|
+
}
|
|
103
|
+
if (verify) {
|
|
104
|
+
const digest = createHash("sha256").update(output).digest("hex");
|
|
105
|
+
if (digest !== image.sha256) throw new Error(`SHA-256 mismatch for ${name}`);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
await mkdir(CACHE_ROOT, { recursive: true });
|
|
109
|
+
await writeFile(cachePath, output);
|
|
110
|
+
} catch {
|
|
111
|
+
// Cache writes are an optimization and must not prevent booting.
|
|
112
|
+
}
|
|
113
|
+
return toArrayBuffer(output);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function validateOptions(options) {
|
|
117
|
+
const value = { ...DEFAULTS, ...options };
|
|
118
|
+
if (!Number.isInteger(value.memoryMB) || value.memoryMB < 128 || (value.memoryMB & (value.memoryMB - 1)) !== 0) {
|
|
119
|
+
throw new TypeError("memoryMB must be a power-of-two integer of at least 128");
|
|
120
|
+
}
|
|
121
|
+
for (const name of ["bootTimeoutMs", "commandTimeoutMs"]) {
|
|
122
|
+
if (!Number.isFinite(value[name]) || value[name] < 0) throw new TypeError(`${name} must be a non-negative number`);
|
|
123
|
+
}
|
|
124
|
+
if (typeof value.networkRelayUrl !== "string" || !/^(?:fetch|wisps?:\/\/)/.test(value.networkRelayUrl)) {
|
|
125
|
+
throw new TypeError('networkRelayUrl must be "fetch" or a wisp:// / wisps:// URL');
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export class Sharjeenux extends EventEmitter {
|
|
131
|
+
constructor(options = {}) {
|
|
132
|
+
super();
|
|
133
|
+
this.options = validateOptions(options);
|
|
134
|
+
this.ready = false;
|
|
135
|
+
this._emulator = null;
|
|
136
|
+
this._initializePromise = null;
|
|
137
|
+
this._phase = "idle";
|
|
138
|
+
this._serial = "";
|
|
139
|
+
this._active = null;
|
|
140
|
+
this._queue = Promise.resolve();
|
|
141
|
+
this._serialListener = byte => this._onSerialByte(byte);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
initialize() {
|
|
145
|
+
this._initializePromise ||= this._initialize();
|
|
146
|
+
return this._initializePromise;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async _initialize() {
|
|
150
|
+
this._phase = "loading";
|
|
151
|
+
if (this.options.networkRelayUrl !== "fetch" && typeof globalThis.WebSocket === "undefined") {
|
|
152
|
+
const { WebSocket } = await import("ws");
|
|
153
|
+
globalThis.WebSocket = WebSocket;
|
|
154
|
+
}
|
|
155
|
+
const manifest = await getManifest();
|
|
156
|
+
const [bios, vgabios, kernel, initrd] = await Promise.all([
|
|
157
|
+
loadImage("bios", this.options.verifyAssets),
|
|
158
|
+
loadImage("vgabios", this.options.verifyAssets),
|
|
159
|
+
loadImage("kernel", this.options.verifyAssets),
|
|
160
|
+
manifest.images.initrd ? loadImage("initrd", this.options.verifyAssets) : undefined,
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
const wasmPath = fileURLToPath(import.meta.resolve("v86/build/v86.wasm"));
|
|
164
|
+
const boot = deferred();
|
|
165
|
+
this._boot = boot;
|
|
166
|
+
this._phase = "booting";
|
|
167
|
+
|
|
168
|
+
const emulatorOptions = {
|
|
169
|
+
wasm_path: wasmPath,
|
|
170
|
+
bios: { buffer: bios },
|
|
171
|
+
vga_bios: { buffer: vgabios },
|
|
172
|
+
bzimage: { buffer: kernel },
|
|
173
|
+
memory_size: this.options.memoryMB * 1024 * 1024,
|
|
174
|
+
filesystem: {},
|
|
175
|
+
net_device: this.options.networking ? {
|
|
176
|
+
type: "virtio",
|
|
177
|
+
relay_url: this.options.networkRelayUrl,
|
|
178
|
+
mtu: 1500,
|
|
179
|
+
} : undefined,
|
|
180
|
+
cmdline: "console=ttyS0 rootfstype=ramfs tsc=reliable mitigations=off random.trust_cpu=on",
|
|
181
|
+
autostart: true,
|
|
182
|
+
disable_keyboard: true,
|
|
183
|
+
disable_mouse: true,
|
|
184
|
+
disable_speaker: true,
|
|
185
|
+
};
|
|
186
|
+
if (initrd) emulatorOptions.initrd = { buffer: initrd };
|
|
187
|
+
this._emulator = new V86(emulatorOptions);
|
|
188
|
+
this._emulator.add_listener("serial0-output-byte", this._serialListener);
|
|
189
|
+
|
|
190
|
+
const timer = setTimeout(() => {
|
|
191
|
+
boot.reject(new Error(`Sharjeenux did not boot within ${this.options.bootTimeoutMs} ms`));
|
|
192
|
+
}, this.options.bootTimeoutMs);
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
await boot.promise;
|
|
196
|
+
this.ready = true;
|
|
197
|
+
this._phase = "ready";
|
|
198
|
+
this._serial = "";
|
|
199
|
+
this.emit("ready");
|
|
200
|
+
return this;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
await this.shutdown();
|
|
203
|
+
throw error;
|
|
204
|
+
} finally {
|
|
205
|
+
clearTimeout(timer);
|
|
206
|
+
this._boot = null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
_onSerialByte(byte) {
|
|
211
|
+
const character = String.fromCharCode(byte);
|
|
212
|
+
this._serial = (this._serial + character).slice(-1_000_000);
|
|
213
|
+
|
|
214
|
+
if (this._phase === "booting" && /(?:^|\n)[^\n]*%\s*$/.test(this._serial.replace(/\r/g, ""))) {
|
|
215
|
+
this._phase = "setup";
|
|
216
|
+
const network = this.options.networking ? "udhcpc -q >/dev/null 2>&1 || true;" : "";
|
|
217
|
+
this._emulator.serial0_send(
|
|
218
|
+
`stty -echo rows 24 cols 80; export PATH=/usr/local/bin:/opt/maven/bin:/usr/bin:/bin:/usr/sbin:/sbin HOME=/mnt/home TMPDIR=/mnt/tmp TERM=xterm NO_COLOR=1 CI=1 ` +
|
|
219
|
+
`NODE_OPTIONS=--max-old-space-size=512 JAVA_HOME=/usr/lib/jvm MAVEN_HOME=/opt/maven ` +
|
|
220
|
+
`MAVEN_ARGS='--settings /etc/maven-settings.xml' PIP_CACHE_DIR=/mnt/pip-cache; ` +
|
|
221
|
+
`export npm_config_progress=false npm_config_color=false npm_config_update_notifier=false ` +
|
|
222
|
+
`npm_config_yes=true npm_config_audit=false npm_config_fund=false ` +
|
|
223
|
+
`npm_config_cache=/mnt/npm-cache npm_config_registry=http://registry.npmjs.org/ npm_config_replace_registry_host=always ` +
|
|
224
|
+
`npm_config_fetch_retries=5 npm_config_fetch_retry_mintimeout=10000 npm_config_fetch_retry_maxtimeout=120000 ` +
|
|
225
|
+
`npm_config_fetch_timeout=600000 npm_config_maxsockets=6; ` +
|
|
226
|
+
`mkdir -p /mnt/workspace /mnt/home /mnt/tmp /mnt/npm-cache /mnt/pip-cache /mnt/home/.m2/repository; cd /mnt/workspace; ` +
|
|
227
|
+
`PS1='__SHARJEENUX_PROMPT__ '; ${network} printf '\n__SHARJEENUX_READY__\n'\n`,
|
|
228
|
+
);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (this._phase === "setup" && /\n__SHARJEENUX_READY__\r?\n__SHARJEENUX_PROMPT__ $/.test(this._serial)) {
|
|
233
|
+
this._boot.resolve();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (this._phase !== "ready" || !this._active) return;
|
|
238
|
+
this._consumeActiveOutput();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
_consumeActiveOutput() {
|
|
242
|
+
const active = this._active;
|
|
243
|
+
if (!active) return;
|
|
244
|
+
const text = this._serial.replace(/\r/g, "");
|
|
245
|
+
const begin = `${active.begin}\n`;
|
|
246
|
+
const beginIndex = text.indexOf(begin);
|
|
247
|
+
if (beginIndex < 0) return;
|
|
248
|
+
|
|
249
|
+
const bodyStart = beginIndex + begin.length;
|
|
250
|
+
const endPrefix = `\n${active.end}:`;
|
|
251
|
+
const endIndex = text.indexOf(endPrefix, bodyStart);
|
|
252
|
+
let streamEnd = endIndex >= 0 ? endIndex : text.length;
|
|
253
|
+
if (endIndex < 0) {
|
|
254
|
+
// Hold only a suffix that could actually become the private end marker.
|
|
255
|
+
// Holding the marker's full length would hide short interactive prompts
|
|
256
|
+
// (for example Node's `> `) until more output happened to arrive.
|
|
257
|
+
const body = text.slice(bodyStart);
|
|
258
|
+
const maximum = Math.min(endPrefix.length - 1, body.length);
|
|
259
|
+
for (let length = maximum; length > 0; length -= 1) {
|
|
260
|
+
if (body.endsWith(endPrefix.slice(0, length))) {
|
|
261
|
+
streamEnd -= length;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const available = text.slice(bodyStart, streamEnd);
|
|
267
|
+
|
|
268
|
+
if (available.length > active.streamed) {
|
|
269
|
+
const chunk = available.slice(active.streamed);
|
|
270
|
+
active.streamed = available.length;
|
|
271
|
+
active.options.onOutput?.(chunk);
|
|
272
|
+
this.emit("output", chunk);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (endIndex < 0) return;
|
|
276
|
+
const codeStart = endIndex + endPrefix.length;
|
|
277
|
+
const codeEnd = text.indexOf("\n", codeStart);
|
|
278
|
+
if (codeEnd < 0 || !text.slice(codeEnd + 1).includes("__SHARJEENUX_PROMPT__ ")) return;
|
|
279
|
+
|
|
280
|
+
const exitCode = Number.parseInt(text.slice(codeStart, codeEnd), 10);
|
|
281
|
+
const output = cleanOutput(text.slice(bodyStart, endIndex));
|
|
282
|
+
const result = {
|
|
283
|
+
command: active.command,
|
|
284
|
+
output,
|
|
285
|
+
exitCode: Number.isNaN(exitCode) ? 255 : exitCode,
|
|
286
|
+
durationMs: Date.now() - active.startedAt,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
clearTimeout(active.timer);
|
|
290
|
+
clearTimeout(active.cancelTimer);
|
|
291
|
+
active.options.signal?.removeEventListener("abort", active.abortListener);
|
|
292
|
+
this._active = null;
|
|
293
|
+
this._serial = "";
|
|
294
|
+
if (active.cancelError) {
|
|
295
|
+
active.cancelError.output = output;
|
|
296
|
+
active.reject(active.cancelError);
|
|
297
|
+
} else {
|
|
298
|
+
active.resolve(result);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async exec(command, options = {}) {
|
|
303
|
+
if (typeof command !== "string" || command.includes("\0")) throw new TypeError("command must be a string without NUL bytes");
|
|
304
|
+
if (!command.trim()) return { command, output: "", exitCode: 0, durationMs: 0 };
|
|
305
|
+
await this.initialize();
|
|
306
|
+
|
|
307
|
+
const task = this._queue.then(() => this._execute(command, options));
|
|
308
|
+
this._queue = task.catch(() => undefined);
|
|
309
|
+
return task;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async send(command, options = {}) {
|
|
313
|
+
return (await this.exec(command, options)).output;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
spawn(command, options = {}) {
|
|
317
|
+
return new RunningCommand(this, command, options);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
write(data) {
|
|
321
|
+
if (!this._emulator || !this.ready) throw new Error("Sharjeenux is not ready");
|
|
322
|
+
if (typeof data !== "string") throw new TypeError("input must be a string");
|
|
323
|
+
this._emulator.serial0_send(data);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
_execute(command, options) {
|
|
327
|
+
if (options.signal?.aborted) {
|
|
328
|
+
const error = new Error(`Command aborted: ${command}`);
|
|
329
|
+
error.name = "AbortError";
|
|
330
|
+
return Promise.reject(error);
|
|
331
|
+
}
|
|
332
|
+
const request = deferred();
|
|
333
|
+
const id = randomBytes(12).toString("hex");
|
|
334
|
+
const timeoutMs = options.timeoutMs ?? this.options.commandTimeoutMs;
|
|
335
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new TypeError("timeoutMs must be a non-negative number");
|
|
336
|
+
|
|
337
|
+
const active = {
|
|
338
|
+
command,
|
|
339
|
+
options,
|
|
340
|
+
resolve: request.resolve,
|
|
341
|
+
reject: request.reject,
|
|
342
|
+
begin: `__SHAZINUX_BEGIN_${id}__`,
|
|
343
|
+
end: `__SHAZINUX_END_${id}__`,
|
|
344
|
+
streamed: 0,
|
|
345
|
+
startedAt: Date.now(),
|
|
346
|
+
timer: null,
|
|
347
|
+
cancelTimer: null,
|
|
348
|
+
abortListener: null,
|
|
349
|
+
cancelError: null,
|
|
350
|
+
};
|
|
351
|
+
this._active = active;
|
|
352
|
+
this._serial = "";
|
|
353
|
+
|
|
354
|
+
const cancel = error => {
|
|
355
|
+
if (this._active !== active || active.cancelError) return;
|
|
356
|
+
active.cancelError = error;
|
|
357
|
+
this.interrupt();
|
|
358
|
+
active.cancelTimer = setTimeout(() => {
|
|
359
|
+
if (this._active !== active) return;
|
|
360
|
+
this._active = null;
|
|
361
|
+
this._serial = "";
|
|
362
|
+
this._emulator.serial0_send("\x03\n");
|
|
363
|
+
error.output = "";
|
|
364
|
+
active.reject(error);
|
|
365
|
+
}, 5_000);
|
|
366
|
+
};
|
|
367
|
+
active.abortListener = () => {
|
|
368
|
+
const error = new Error(`Command aborted: ${command}`);
|
|
369
|
+
error.name = "AbortError";
|
|
370
|
+
cancel(error);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
options.signal?.addEventListener("abort", active.abortListener, { once: true });
|
|
374
|
+
if (timeoutMs > 0) {
|
|
375
|
+
active.timer = setTimeout(() => cancel(new CommandTimeoutError(command, "", timeoutMs)), timeoutMs);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const encoded = shellQuote(command);
|
|
379
|
+
const interactiveStart = options.interactive ? "stty echo; " : "";
|
|
380
|
+
const interactiveEnd = options.interactive ? "stty -echo; " : "";
|
|
381
|
+
this._emulator.serial0_send(
|
|
382
|
+
`__shazinux_cmd=${encoded}; printf '\n${active.begin}\n'; ${interactiveStart}eval "$__shazinux_cmd"; ` +
|
|
383
|
+
`__shazinux_code=$?; ${interactiveEnd}printf '\n${active.end}:%s\n' "$__shazinux_code"\n`,
|
|
384
|
+
);
|
|
385
|
+
return request.promise;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
interrupt() {
|
|
389
|
+
this._emulator?.serial0_send("\x03");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async shutdown() {
|
|
393
|
+
this.ready = false;
|
|
394
|
+
this._phase = "shutdown";
|
|
395
|
+
if (this._active) {
|
|
396
|
+
clearTimeout(this._active.timer);
|
|
397
|
+
clearTimeout(this._active.cancelTimer);
|
|
398
|
+
this._active.reject(new Error("Sharjeenux shut down while a command was running"));
|
|
399
|
+
this._active = null;
|
|
400
|
+
}
|
|
401
|
+
if (this._emulator) {
|
|
402
|
+
this._emulator.remove_listener("serial0-output-byte", this._serialListener);
|
|
403
|
+
await this._emulator.destroy();
|
|
404
|
+
this._emulator = null;
|
|
405
|
+
}
|
|
406
|
+
this.emit("shutdown");
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export class RunningCommand {
|
|
411
|
+
constructor(vm, command, options) {
|
|
412
|
+
this._vm = vm;
|
|
413
|
+
this._controller = new AbortController();
|
|
414
|
+
if (options.signal) {
|
|
415
|
+
if (options.signal.aborted) this._controller.abort();
|
|
416
|
+
else options.signal.addEventListener("abort", () => this._controller.abort(), { once: true });
|
|
417
|
+
}
|
|
418
|
+
this.result = vm.exec(command, { ...options, signal: this._controller.signal });
|
|
419
|
+
this.exit = this.result.then(
|
|
420
|
+
result => result.exitCode,
|
|
421
|
+
error => error?.name === "AbortError" ? 130 : error?.name === "CommandTimeoutError" ? 124 : 255,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
write(data) {
|
|
426
|
+
this._vm.write(data);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
kill() {
|
|
430
|
+
this._controller.abort();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function create(options) {
|
|
435
|
+
return new Sharjeenux(options);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export async function initialize(options) {
|
|
439
|
+
defaultInstance ||= create(options);
|
|
440
|
+
await defaultInstance.initialize();
|
|
441
|
+
return defaultInstance;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export async function exec(command, options) {
|
|
445
|
+
return (await initialize()).exec(command, options);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export async function send(command, options) {
|
|
449
|
+
return (await initialize()).send(command, options);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export async function spawn(command, options) {
|
|
453
|
+
return (await initialize()).spawn(command, options);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export async function write(data) {
|
|
457
|
+
(await initialize()).write(data);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export async function shutdown() {
|
|
461
|
+
if (!defaultInstance) return;
|
|
462
|
+
const instance = defaultInstance;
|
|
463
|
+
defaultInstance = undefined;
|
|
464
|
+
await instance.shutdown();
|
|
465
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sharjeenux",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "A headless Buildroot Linux VM with Node.js, Python, Java, package managers, and development tools, controlled from JavaScript.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.cjs",
|
|
7
|
+
"module": "./index.js",
|
|
8
|
+
"types": "./index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./index.d.ts",
|
|
12
|
+
"import": "./index.js",
|
|
13
|
+
"require": "./index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./features/*": "./features/*.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"features/",
|
|
19
|
+
"index.cjs",
|
|
20
|
+
"index.d.ts",
|
|
21
|
+
"index.js",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"README.md",
|
|
24
|
+
"SOURCE_OFFER.md",
|
|
25
|
+
"compliance/",
|
|
26
|
+
"THIRD_PARTY_NOTICES.md"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "node test/smoke.mjs",
|
|
30
|
+
"test:toolchain": "node test/toolchain.mjs",
|
|
31
|
+
"test:network": "node test/network.mjs",
|
|
32
|
+
"test:vite": "node test/vite.mjs",
|
|
33
|
+
"pack:check": "npm pack --dry-run"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"buildroot",
|
|
37
|
+
"linux",
|
|
38
|
+
"virtual-machine",
|
|
39
|
+
"v86",
|
|
40
|
+
"nodejs",
|
|
41
|
+
"npm",
|
|
42
|
+
"typescript",
|
|
43
|
+
"python",
|
|
44
|
+
"java",
|
|
45
|
+
"git",
|
|
46
|
+
"curl"
|
|
47
|
+
],
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=20.6"
|
|
50
|
+
},
|
|
51
|
+
"os": [
|
|
52
|
+
"darwin",
|
|
53
|
+
"linux",
|
|
54
|
+
"win32"
|
|
55
|
+
],
|
|
56
|
+
"license": "MIT",
|
|
57
|
+
"author": {
|
|
58
|
+
"name": "Sharjeel Baig",
|
|
59
|
+
"email": "dr.sharjeel.6@gmail.com"
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public",
|
|
63
|
+
"registry": "https://registry.npmjs.org/"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"v86": "0.5.420",
|
|
67
|
+
"ws": "8.21.0"
|
|
68
|
+
}
|
|
69
|
+
}
|