sharjeenux 0.3.0 → 1.0.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 CHANGED
@@ -4,36 +4,57 @@ Sharjeenux is a headless, in-memory Buildroot Linux VM controlled from Node.js.
4
4
  It boots an i686 Linux image through v86 and exposes one persistent serial shell
5
5
  as an async JavaScript API.
6
6
 
7
- The guest includes:
7
+ ## Modular Architecture
8
8
 
9
- - Node.js 20.20, npm, npx, TypeScript (`tsc` and `tsserver`)
10
- - Python 3 and pip
11
- - OpenJDK 21 (`java`, `javac`) and Apache Maven
9
+ Sharjeenux uses a plugin-based architecture. The core package ships a minimal
10
+ Linux base system, and language runtimes are installed as separate packages:
11
+
12
+ | Package | What it adds | Install |
13
+ |---|---|---|
14
+ | `sharjeenux` | Base Linux (BusyBox, networking, archive tools) | `npm install sharjeenux` |
15
+ | `@sharjeenux/node` | Node.js 20, npm, npx, TypeScript | `npm install @sharjeenux/node` |
16
+ | `@sharjeenux/python` | Python 3, pip | `npm install @sharjeenux/python` |
17
+ | `@sharjeenux/java` | OpenJDK 21, javac, Apache Maven | `npm install @sharjeenux/java` |
18
+
19
+ The base system always includes:
20
+
21
+ - BusyBox coreutils, shell, and init
12
22
  - Git, curl, GNU wget, OpenSSL, and CA certificates
13
23
  - Common archive tools: tar, gzip, bzip2, xz, zip, and unzip
24
+ - Outbound networking via v86's fetch transport or a Wisp relay
14
25
 
15
26
  It has no desktop, display server, SSH server, or host port-forwarding layer.
16
27
 
17
- ## Install
28
+ ## Quick Start
29
+
30
+ ### Minimal (base Linux only)
18
31
 
19
32
  ```sh
20
33
  npm install sharjeenux
21
34
  ```
22
35
 
23
- To test the generated package before registry publication:
36
+ ```js
37
+ import { initialize, send, shutdown } from "sharjeenux";
24
38
 
25
- ```sh
26
- npm install ./sharjeenux-0.3.0.tgz
39
+ await initialize();
40
+ console.log(await send("uname -a"));
41
+ console.log(await send("ls -la"));
42
+ console.log(await send("curl --version"));
43
+ await shutdown();
27
44
  ```
28
45
 
29
- ## Use
46
+ ### Full Stack (Node.js + Python + Java)
47
+
48
+ ```sh
49
+ npm install sharjeenux @sharjeenux/node @sharjeenux/python @sharjeenux/java
50
+ ```
30
51
 
31
52
  ```js
32
53
  import { initialize, send, exec, shutdown } from "sharjeenux";
33
54
 
34
55
  await initialize();
35
56
 
36
- console.log(await send("ls -la"));
57
+ // All runtimes are available automatically
37
58
  console.log(await send("node -e 'console.log(6 * 7)'"));
38
59
  console.log(await send("python3 -c 'print(6 * 7)'"));
39
60
  console.log(await send("java --version"));
@@ -45,7 +66,23 @@ console.log(result.output, result.exitCode, result.durationMs);
45
66
  await shutdown();
46
67
  ```
47
68
 
48
- CommonJS is supported:
69
+ ### Pick Only What You Need
70
+
71
+ ```sh
72
+ # Just Node.js
73
+ npm install sharjeenux @sharjeenux/node
74
+
75
+ # Just Python
76
+ npm install sharjeenux @sharjeenux/python
77
+
78
+ # Node.js + Java (no Python)
79
+ npm install sharjeenux @sharjeenux/node @sharjeenux/java
80
+ ```
81
+
82
+ Plugins are detected automatically from your `package.json` dependencies. No
83
+ extra configuration is needed — just install and go.
84
+
85
+ CommonJS is also supported:
49
86
 
50
87
  ```js
51
88
  const { initialize, send, shutdown } = require("sharjeenux");
@@ -55,18 +92,93 @@ console.log(await send("uname -a"));
55
92
  await shutdown();
56
93
  ```
57
94
 
58
- Commands execute sequentially in one persistent shell rooted at
59
- `/mnt/workspace`. Directory changes, environment variables, and installed
60
- language packages remain available until `shutdown()`.
95
+ ## How It Works
96
+
97
+ Under the hood, Sharjeenux leverages Linux's **composite initramfs** feature.
98
+ Each plugin package ships a compressed CPIO archive containing its runtime
99
+ binaries. At boot, the base initrd and all plugin initrds are concatenated
100
+ into a single buffer. The Linux kernel extracts them sequentially, overlaying
101
+ each archive onto the root filesystem. The result is a unified system with all
102
+ your chosen runtimes available in `$PATH`.
103
+
104
+ ```
105
+ sharjeenux (base) → /bin, /sbin, /usr (BusyBox, git, curl…)
106
+ @sharjeenux/node (plugin) → /usr/local/bin/node, /usr/local/bin/npm…
107
+ @sharjeenux/python (plugin)→ /usr/bin/python3, /usr/bin/pip3…
108
+ @sharjeenux/java (plugin) → /usr/lib/jvm/…, /opt/maven/…
109
+ ```
110
+
111
+ ## API
61
112
 
62
- ## Package managers
113
+ ### `initialize(options?)`
114
+
115
+ Boots the VM and returns the `Sharjeenux` instance. Subsequent calls return the
116
+ same instance.
117
+
118
+ ```js
119
+ const vm = await initialize({
120
+ memoryMB: 2048, // Guest RAM (power of 2, min 128)
121
+ bootTimeoutMs: 180_000, // Max time to wait for boot
122
+ commandTimeoutMs: 1_800_000, // Default per-command timeout
123
+ networking: true, // Enable outbound networking
124
+ networkRelayUrl: "fetch", // Or "wisps://your-relay.example/"
125
+ plugins: [], // Explicit plugin package names (auto-detected by default)
126
+ });
127
+ ```
128
+
129
+ ### `send(command, options?)`
130
+
131
+ Runs a shell command and returns its stdout as a string.
132
+
133
+ ```js
134
+ const output = await send("ls -la");
135
+ ```
136
+
137
+ ### `exec(command, options?)`
138
+
139
+ Runs a shell command and returns a result object:
140
+
141
+ ```js
142
+ const { output, exitCode, durationMs } = await exec("npm install lodash");
143
+ ```
144
+
145
+ ### `spawn(command, options?)`
146
+
147
+ Starts a long-running command without waiting for it to exit:
148
+
149
+ ```js
150
+ const dev = vm.spawn("npm run dev -- --host 0.0.0.0", {
151
+ timeoutMs: 0,
152
+ onOutput: chunk => process.stdout.write(chunk),
153
+ });
154
+
155
+ // Later...
156
+ dev.kill();
157
+ await dev.exit;
158
+ ```
159
+
160
+ ### `shutdown()`
161
+
162
+ Destroys the VM and releases all resources.
163
+
164
+ ### Streaming Output
165
+
166
+ Both `exec()` and `send()` accept an `onOutput` callback for real-time output:
167
+
168
+ ```js
169
+ await send("npm install express", {
170
+ onOutput: chunk => process.stdout.write(chunk),
171
+ });
172
+ ```
173
+
174
+ ## Package Managers
63
175
 
64
176
  Use npm/npx for JavaScript and TypeScript, pip for Python, and Maven for Java:
65
177
 
66
178
  ```js
67
- await send("npm install lodash");
68
- await send("pip3 install requests");
69
- await send("mvn --version");
179
+ await send("npm install lodash"); // requires @sharjeenux/node
180
+ await send("pip3 install requests"); // requires @sharjeenux/python
181
+ await send("mvn --version"); // requires @sharjeenux/java
70
182
  ```
71
183
 
72
184
  Buildroot images are intentionally assembled at build time and do not have a
@@ -79,7 +191,7 @@ compile native C/C++ extensions inside the guest are not guaranteed because
79
191
  this image does not ship an on-target C/C++ compiler toolchain. Java source is
80
192
  supported through `javac`.
81
193
 
82
- ## Long-running commands
194
+ ## Long-Running Commands
83
195
 
84
196
  `send()` resolves only when a command exits. Use `spawn()` for a development
85
197
  server or another long-lived process:
@@ -109,7 +221,7 @@ Sharjeenux deliberately does not map guest listening ports to the host. A Vite
109
221
  server can run inside Linux, but its URL is not exposed to the host, browser,
110
222
  or public network.
111
223
 
112
- ## Outbound networking
224
+ ## Outbound Networking
113
225
 
114
226
  The default is v86's fetch transport:
115
227
 
@@ -136,7 +248,20 @@ The fetch transport is much slower and less reliable for large dependency
136
248
  graphs than normal TCP. Sharjeenux configures npm retries and conservative
137
249
  parallelism, but a trusted Wisp relay is recommended for large installs.
138
250
 
139
- ## Sandbox requirements
251
+ ## Creating a Plugin Package
252
+
253
+ Want to add your own runtime to Sharjeenux? Create an npm package that ships a
254
+ CPIO archive:
255
+
256
+ 1. Build a `plugin.cpio.xz` (or `.cpio.gz` / `.cpio`) containing the files you
257
+ want overlaid onto the root filesystem
258
+ 2. Set `"main": "plugin.cpio.xz"` in your plugin's `package.json`
259
+ 3. Include `plugin.cpio.xz` in the `"files"` array
260
+ 4. Name your package `@sharjeenux/<name>` or `sharjeenux-<name>`
261
+
262
+ The archive will be automatically detected and loaded at boot.
263
+
264
+ ## Sandbox Requirements
140
265
 
141
266
  Sharjeenux works in Node.js sandboxes that permit:
142
267
 
@@ -150,7 +275,7 @@ It is not a browser package: its host wrapper imports Node.js APIs. Serverless
150
275
  and edge runtimes with short CPU limits, no writable temporary storage, or no
151
276
  WebAssembly support are not compatible.
152
277
 
153
- ## Performance and Base64 assets
278
+ ## Performance and Base64 Assets
154
279
 
155
280
  Every embedded binary is stored as Base64 text files of at most 1 KiB each.
156
281
  The first initialization must open and decode those files. Decoded images are
@@ -162,7 +287,7 @@ slower than native Node.js or StackBlitz WebContainers, whose architecture is
162
287
  designed specifically around browser-native execution. Streaming output with
163
288
  `onOutput` makes long installs observable but does not remove emulation cost.
164
289
 
165
- ## Licensing and corresponding source
290
+ ## Licensing and Corresponding Source
166
291
 
167
292
  The JavaScript wrapper is MIT-licensed. The Linux image contains components
168
293
  under their own licenses, including GPLv2 BusyBox and the Linux kernel. See
@@ -446,10 +446,10 @@ BR2_STRIP_EXCLUDE_FILES=""
446
446
  BR2_STRIP_EXCLUDE_DIRS=""
447
447
  # BR2_OPTIMIZE_0 is not set
448
448
  # BR2_OPTIMIZE_1 is not set
449
- BR2_OPTIMIZE_2=y
449
+ # BR2_OPTIMIZE_2 is not set
450
450
  # BR2_OPTIMIZE_3 is not set
451
451
  # BR2_OPTIMIZE_G is not set
452
- # BR2_OPTIMIZE_S is not set
452
+ BR2_OPTIMIZE_S=y
453
453
  # BR2_OPTIMIZE_FAST is not set
454
454
  # BR2_ENABLE_LTO is not set
455
455
  # BR2_GOOGLE_BREAKPAD_ENABLE is not set
@@ -3811,12 +3811,12 @@ BR2_TARGET_ROOTFS_CPIO=y
3811
3811
  BR2_TARGET_ROOTFS_CPIO_FULL=y
3812
3812
  # BR2_TARGET_ROOTFS_CPIO_DRACUT is not set
3813
3813
  # BR2_TARGET_ROOTFS_CPIO_NONE is not set
3814
- BR2_TARGET_ROOTFS_CPIO_GZIP=y
3814
+ # BR2_TARGET_ROOTFS_CPIO_GZIP is not set
3815
3815
  # BR2_TARGET_ROOTFS_CPIO_BZIP2 is not set
3816
3816
  # BR2_TARGET_ROOTFS_CPIO_LZ4 is not set
3817
3817
  # BR2_TARGET_ROOTFS_CPIO_LZMA is not set
3818
3818
  # BR2_TARGET_ROOTFS_CPIO_LZO is not set
3819
- # BR2_TARGET_ROOTFS_CPIO_XZ is not set
3819
+ BR2_TARGET_ROOTFS_CPIO_XZ=y
3820
3820
  # BR2_TARGET_ROOTFS_CPIO_ZSTD is not set
3821
3821
  # BR2_TARGET_ROOTFS_CPIO_UIMAGE is not set
3822
3822
  # BR2_TARGET_ROOTFS_CRAMFS is not set
@@ -186,7 +186,7 @@ CONFIG_NET_NS=y
186
186
  # CONFIG_SCHED_AUTOGROUP is not set
187
187
  # CONFIG_RELAY is not set
188
188
  CONFIG_BLK_DEV_INITRD=y
189
- CONFIG_INITRAMFS_SOURCE="${BR_BINARIES_DIR}/rootfs.cpio"
189
+ CONFIG_INITRAMFS_SOURCE=""
190
190
  CONFIG_INITRAMFS_ROOT_UID=0
191
191
  CONFIG_INITRAMFS_ROOT_GID=0
192
192
  CONFIG_RD_GZIP=y
@@ -196,18 +196,18 @@ CONFIG_RD_XZ=y
196
196
  CONFIG_RD_LZO=y
197
197
  CONFIG_RD_LZ4=y
198
198
  CONFIG_RD_ZSTD=y
199
- CONFIG_INITRAMFS_COMPRESSION_GZIP=y
199
+ # CONFIG_INITRAMFS_COMPRESSION_GZIP is not set
200
200
  # CONFIG_INITRAMFS_COMPRESSION_BZIP2 is not set
201
201
  # CONFIG_INITRAMFS_COMPRESSION_LZMA is not set
202
- # CONFIG_INITRAMFS_COMPRESSION_XZ is not set
202
+ CONFIG_INITRAMFS_COMPRESSION_XZ=y
203
203
  # CONFIG_INITRAMFS_COMPRESSION_LZO is not set
204
204
  # CONFIG_INITRAMFS_COMPRESSION_LZ4 is not set
205
205
  # CONFIG_INITRAMFS_COMPRESSION_ZSTD is not set
206
206
  # CONFIG_INITRAMFS_COMPRESSION_NONE is not set
207
207
  # CONFIG_BOOT_CONFIG is not set
208
208
  CONFIG_INITRAMFS_PRESERVE_MTIME=y
209
- CONFIG_CC_OPTIMIZE_FOR_PERFORMANCE=y
210
- # CONFIG_CC_OPTIMIZE_FOR_SIZE is not set
209
+ # CONFIG_CC_OPTIMIZE_FOR_PERFORMANCE is not set
210
+ CONFIG_CC_OPTIMIZE_FOR_SIZE=y
211
211
  CONFIG_LD_ORPHAN_WARN=y
212
212
  CONFIG_LD_ORPHAN_WARN_LEVEL="warn"
213
213
  CONFIG_SYSCTL=y
package/index.d.ts CHANGED
@@ -11,6 +11,8 @@ export interface SharjeenuxOptions {
11
11
  networkRelayUrl?: string;
12
12
  /** Hash-check decoded VM assets during initialization. Default: true. */
13
13
  verifyAssets?: boolean;
14
+ /** Explicit list of plugin package names to load (e.g. ["@sharjeenux/node"]). By default, plugins are auto-detected from package.json dependencies. */
15
+ plugins?: string[];
14
16
  }
15
17
 
16
18
  export interface CommandOptions {
package/index.js CHANGED
@@ -4,6 +4,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
+ import { createRequire } from "node:module";
7
8
  import { V86 } from "v86";
8
9
 
9
10
  const ASSETS_URL = new URL("./assets/", import.meta.url);
@@ -16,6 +17,7 @@ const DEFAULTS = Object.freeze({
16
17
  networking: true,
17
18
  networkRelayUrl: "fetch",
18
19
  verifyAssets: true,
20
+ plugins: [],
19
21
  });
20
22
 
21
23
  let defaultInstance;
@@ -57,6 +59,56 @@ function toArrayBuffer(buffer) {
57
59
  return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
58
60
  }
59
61
 
62
+
63
+ function concatArrayBuffers(buffers) {
64
+ const totalLength = buffers.reduce((acc, b) => acc + b.byteLength, 0);
65
+ const result = new Uint8Array(totalLength);
66
+ let offset = 0;
67
+ for (const b of buffers) {
68
+ result.set(new Uint8Array(b), offset);
69
+ offset += b.byteLength;
70
+ }
71
+ return result.buffer;
72
+ }
73
+
74
+ async function loadPlugins(explicitPlugins = []) {
75
+ const plugins = [];
76
+ const require = createRequire(process.cwd() + "/");
77
+ const depsToTry = new Set(explicitPlugins);
78
+
79
+ try {
80
+ const pkgPath = path.resolve(process.cwd(), "package.json");
81
+ const pkgText = await readFile(pkgPath, "utf8");
82
+ const pkg = JSON.parse(pkgText);
83
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
84
+ for (const dep of Object.keys(deps)) {
85
+ if ((dep.startsWith("sharjeenux-") || dep.startsWith("@sharjeenux/")) && dep !== "sharjeenux") {
86
+ depsToTry.add(dep);
87
+ }
88
+ }
89
+ } catch (e) {}
90
+
91
+ for (const dep of depsToTry) {
92
+ try {
93
+ let depPath;
94
+ try {
95
+ depPath = require.resolve(`${dep}/plugin.cpio.xz`);
96
+ } catch (e) {
97
+ try {
98
+ depPath = require.resolve(`${dep}/plugin.cpio.gz`);
99
+ } catch (e2) {
100
+ depPath = require.resolve(`${dep}/plugin.cpio`);
101
+ }
102
+ }
103
+ const buffer = await readFile(depPath);
104
+ plugins.push(toArrayBuffer(buffer));
105
+ } catch (e) {
106
+ console.warn(`[Sharjeenux] Failed to load plugin ${dep}: ${e.message}`);
107
+ }
108
+ }
109
+ return plugins;
110
+ }
111
+
60
112
  async function getManifest() {
61
113
  manifestPromise ||= readFile(new URL("manifest.json", ASSETS_URL), "utf8").then(JSON.parse);
62
114
  return manifestPromise;
@@ -153,13 +205,23 @@ export class Sharjeenux extends EventEmitter {
153
205
  globalThis.WebSocket = WebSocket;
154
206
  }
155
207
  const manifest = await getManifest();
156
- const [bios, vgabios, kernel, initrd] = await Promise.all([
208
+ const [bios, vgabios, kernel, baseInitrd, plugins] = await Promise.all([
157
209
  loadImage("bios", this.options.verifyAssets),
158
210
  loadImage("vgabios", this.options.verifyAssets),
159
211
  loadImage("kernel", this.options.verifyAssets),
160
- manifest.images.initrd ? loadImage("initrd", this.options.verifyAssets) : undefined,
212
+ manifest.images.initrd ? loadImage("initrd", this.options.verifyAssets) : Promise.resolve(null),
213
+ loadPlugins(this.options.plugins)
161
214
  ]);
162
215
 
216
+ let finalInitrd;
217
+ const initrdBuffers = [];
218
+ if (baseInitrd) initrdBuffers.push(baseInitrd);
219
+ if (plugins.length > 0) initrdBuffers.push(...plugins);
220
+
221
+ if (initrdBuffers.length > 0) {
222
+ finalInitrd = concatArrayBuffers(initrdBuffers);
223
+ }
224
+
163
225
  const wasmPath = fileURLToPath(import.meta.resolve("v86/build/v86.wasm"));
164
226
  const boot = deferred();
165
227
  this._boot = boot;
@@ -183,7 +245,7 @@ export class Sharjeenux extends EventEmitter {
183
245
  disable_mouse: true,
184
246
  disable_speaker: true,
185
247
  };
186
- if (initrd) emulatorOptions.initrd = { buffer: initrd };
248
+ if (finalInitrd) emulatorOptions.initrd = { buffer: finalInitrd };
187
249
  this._emulator = new V86(emulatorOptions);
188
250
  this._emulator.add_listener("serial0-output-byte", this._serialListener);
189
251
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
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.",
3
+ "version": "1.0.0",
4
+ "description": "A modular, headless Buildroot Linux VM controlled from JavaScript. Install language runtimes as plugins: @sharjeenux/node, @sharjeenux/python, @sharjeenux/java.",
5
5
  "type": "module",
6
6
  "main": "./index.cjs",
7
7
  "module": "./index.js",