buninu 0.1.9 → 0.2.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,84 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2 - 2026-08-17
4
+
5
+ ### Added
6
+
7
+ - Add a "Commands inside the shell" section to README.md, split out of
8
+ Command-line information (which is only `bin/init.js`'s own flags, resolved
9
+ before Buninu starts). Lists everything in `apps/cmdlist` and moves the
10
+ `buninu-help`/`bunx` write-ups there, including a note that `bunx` can't
11
+ install anything on Android until
12
+ [oven-sh/bun#39084](https://github.com/oven-sh/bun/pull/39084) merges
13
+ upstream (Android's seccomp policy kills `bun i -g` with SIGSYS during bin
14
+ linking until then).
15
+
16
+ ## 0.2.1 - 2026-08-17
17
+
18
+ ### Added
19
+
20
+ - Add `bunx`, a POSIX and Windows multicall command that ensures a package is
21
+ installed via `bun i -g` and then runs its matching binary, forwarding the
22
+ remaining arguments. Reinstall timing is adapted from real bunx's own rules
23
+ (an explicit dist-tag like `@latest` always reinstalls; otherwise a cached
24
+ binary is reused until it's older than 24h, or stat fails, or bun's own
25
+ `install/global/package.json` shows a different pinned version than the one
26
+ requested — this script has one shared install location instead of bunx's
27
+ per-version cache, so this last check substitutes for that). The global bin
28
+ directory is resolved via `bun pm bin -g`, falling back to
29
+ `$BUN_INSTALL`/`$HOME/.bun` on the very first install before that project
30
+ exists (which `bun pm bin -g` requires).
31
+ - Runs the target through `bun <target>` instead of exec'ing it directly:
32
+ on Android the resolved binary typically sits under storage mounted
33
+ noexec, so a direct exec is refused even though the file is readable and
34
+ executable-bit set. Trade-off: a package whose bin is a real native
35
+ executable rather than a JS/bun script won't run this way.
36
+ - On Android (detected the same way as `bin/init.js`, via
37
+ `/system/bin/linker64`), installs with `--backend=copyfile` since
38
+ hardlinks/symlinks frequently fail across Android's storage, and sets
39
+ `$PREFIX=/data/data/com.termux/files/usr` for the target process (not the
40
+ install step) so CLIs that check for a Termux environment still find one,
41
+ unless `$PREFIX` is already set.
42
+
43
+ ## 0.2.0 - 2026-08-17
44
+
45
+ ### Added
46
+
47
+ - Add `--jsgotty`, `--jsmdcui`, and `--musl-la` as the first argument to
48
+ `bin/init.js` to spawn that app directly, forwarding every remaining
49
+ argument to it and exiting with its exit code. This bypasses the shell and
50
+ startup-command flow entirely, and lets `--jsgotty --help` (and the same for
51
+ the other two) show the app's own option reference instead of Buninu's.
52
+ - Add `buninu-help`, a POSIX and Windows multicall command that renders
53
+ README.md with jsmdcui's `--cat` mode and then displays `icon.png` with
54
+ jsgotty's `--viu`. The default startup greeting now points to it instead of
55
+ separately mentioning `glow`/`README.md`.
56
+
57
+ ### Security
58
+
59
+ - Bind jsgotty to `127.0.0.1` by default instead of `0.0.0.0` in
60
+ `scripts.start`. Previously the terminal server accepted connections from
61
+ any device able to reach the host on its network, relying only on the
62
+ random port and `--random-url` path segment for protection. Pass
63
+ `--address <value>` through to `bin/init.js` (forwarded arguments override
64
+ the flags baked into `scripts.start`) to opt back into listening on other
65
+ interfaces.
66
+
67
+ ## 0.1.9 - 2026-08-17
68
+
69
+ ### Fixed
70
+
71
+ - Correct `apps/musl-la/NOTICE`: it listed 11 third-party components
72
+ (llama.cpp, nghttp2/nghttp3, OpenSSL, libssh2, curl, zlib, libidn2,
73
+ libunistring, brotli, c-ares, libpsl) with no corresponding files anywhere
74
+ in the package — leftover from an unrelated bundle. The only files actually
75
+ shipped in that directory are `ld-musl-aarch64.so.1`, `libgcc_s.so.1`, and
76
+ `libstdc++.so.6`; the notice now covers only those, identifying the exact
77
+ build (GCC 14.2.0, packaged as Alpine Linux 3.22's `libgcc`/`libstdc++`
78
+ 14.2.0-r6, aarch64) with matching SHA-256 hashes and a link to Alpine's
79
+ build recipe pinned to the commit for that package revision, satisfying the
80
+ GCC Runtime Library Exception's corresponding-source requirement.
81
+
3
82
  ## 0.1.8 - 2026-08-17
4
83
 
5
84
  ### Changed
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Buninu
2
2
 
3
- ![icon](icon.png)
3
+ ![icon](https://raw.githubusercontent.com/jjtseng93/buninu/main/icon.png)
4
4
 
5
5
  > **Early development:** Buninu is still at an early stage of development.
6
6
  > Features, configuration, and command-line behavior may change between releases.
@@ -70,8 +70,33 @@ shell for the current platform. Other arguments are forwarded to jsgotty:
70
70
  ```sh
71
71
  npx buninu --credential user:pass
72
72
  npx buninu --port 9000
73
+ npx buninu --jsgotty --help
74
+
75
+ # public: listen on every interface, reachable from other devices on the network, be cautious!
76
+ npx buninu -a 0.0.0.0
73
77
  ```
74
78
 
79
+ As the first argument, `--jsgotty`, `--jsmdcui`, or `--musl-la` bypasses the
80
+ shell/command startup flow entirely: it spawns that app directly with every
81
+ remaining argument forwarded to it, and exits with its exit code. Use this to
82
+ reach an app's own options directly, for example `npx buninu --jsgotty --help`
83
+ to see jsgotty's actual flag reference instead of Buninu's.
84
+
85
+ ## Security
86
+
87
+ `scripts.start` binds jsgotty to `127.0.0.1` by default, so the terminal
88
+ server only accepts connections from the same machine. It is not reachable
89
+ from other devices on the network unless you explicitly opt in.
90
+
91
+ The terminal itself is writable (`-w`) and unauthenticated by default; only
92
+ the loopback binding and jsgotty's random URL path (`-r`) stand between a
93
+ local process and a shell with Buninu's permissions. To listen on another
94
+ interface, forward `--address <value>` (see [Start](#start)); forwarded
95
+ arguments override the flags baked into `scripts.start`. Also pass
96
+ `--credential user:pass` when doing so, since the random port and URL path
97
+ are not a substitute for authentication once the server is reachable from
98
+ outside the machine.
99
+
75
100
  ## Command-line information
76
101
 
77
102
  ```text
@@ -79,8 +104,39 @@ npx buninu --port 9000
79
104
  -V, --version Show the Buninu, Bun, platform, and architecture versions
80
105
  --readme Render README.md in the terminal
81
106
  --changelog Render CHANGELOG.md in the terminal
107
+ --jsgotty [args...] Spawn jsgotty directly and exit with its exit code
108
+ --jsmdcui [args...] Spawn jsmdcui directly and exit with its exit code
109
+ --musl-la [args...] Spawn musl-la directly and exit with its exit code
82
110
  ```
83
111
 
112
+ These are flags to `bin/init.js` itself, resolved before Buninu starts.
113
+
114
+ ## Commands inside the shell
115
+
116
+ Once you are inside a running Buninu shell, these are available (the
117
+ validated source list is `apps/cmdlist`; see [Add a command](#add-a-command)
118
+ for how it works):
119
+
120
+ ```text
121
+ glow Render files with jsmdcui syntax highlighting
122
+ jmi Open files in the js micro editor
123
+ jsgotty Run a browser-accessible terminal
124
+ jsmdcui Edit and run interactive Markdown applications
125
+ musl-la Launch AArch64 ELF programs with the bundled musl loader
126
+ buninu-help Render README.md with glow, then show icon.png with jsgotty --viu
127
+ bunx Globally install a package with bun, then exec its matching binary
128
+ ```
129
+
130
+ `buninu-help` renders README.md with jsmdcui's `--cat` mode and then shows
131
+ `icon.png` with jsgotty's `--viu`. It is the command the default startup
132
+ greeting points to.
133
+
134
+ `bunx <package>[@version] [args...]` installs with `bun i -g` and runs the
135
+ matching binary. On Android, the underlying `bun i -g` currently needs
136
+ [oven-sh/bun#39084](https://github.com/oven-sh/bun/pull/39084) merged
137
+ upstream — without it, install is killed by SIGSYS (Android's seccomp policy
138
+ rejects a syscall bin-linking uses), so `bunx` can't install anything there yet.
139
+
84
140
  ## Export
85
141
 
86
142
  Export the current Buninu installation as a gzip-compressed tar archive:
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env bun
2
+
3
+ // bunx <package>[@version] [args...]
4
+ //
5
+ // Buninu's own bunx: ensures <package> is globally installed via `bun i -g`,
6
+ // then execs the matching ~/.bun/bin/<binary> directly, forwarding the
7
+ // remaining arguments. Registered in apps/cmdlist like any other Buninu
8
+ // command; whether it actually shadows the real `bunx` (Bun's own
9
+ // ephemeral-run subcommand) depends on whether Buninu's bin/ comes before it
10
+ // in PATH -- Buninu appends its bin/ to the inherited PATH rather than
11
+ // prepending it, so an already-installed bun's own bunx will normally still
12
+ // win unless this one is invoked some other way.
13
+ //
14
+ // The "when do we reinstall" rules below are ported from real bunx's own
15
+ // decision tree (src/runtime/cli/bunx_command.rs in the Bun source tree),
16
+ // adapted from its private per-version cache to this script's single shared
17
+ // `bun i -g` install:
18
+ // - an explicit dist-tag (e.g. "latest", "next") never reuses a cached
19
+ // binary -- real bunx sets look_for_existing_bin = false for that case
20
+ // and always resolves fresh.
21
+ // - a missing binary always installs.
22
+ // - an explicit pinned/ranged version reinstalls if bun's own global
23
+ // install record (install/global/package.json) doesn't already list
24
+ // that same version string for this package -- there is no equivalent
25
+ // check in real bunx because its cache is already keyed by version; this
26
+ // is this script's substitute, since `bun i -g` overwrites one shared
27
+ // path regardless of version.
28
+ // - otherwise, reinstall once the binary is older than 24h
29
+ // (SECONDS_CACHE_VALID in bunx_command.rs), or if it can't be stat'd at
30
+ // all -- real bunx also treats a stat failure as stale.
31
+
32
+ const fs = require("fs");
33
+ const path = require("path");
34
+
35
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // matches real bunx's SECONDS_CACHE_VALID
36
+
37
+ function fail(message) {
38
+ console.error(`bunx: ${message}`);
39
+ process.exit(1);
40
+ }
41
+
42
+ // Split "@scope/name@version" (or unscoped variants) into name/version/pkgName.
43
+ // version is "" when the spec didn't include one.
44
+ function parseSpec(spec) {
45
+ let rest = spec;
46
+ let scope = "";
47
+ if (rest.startsWith("@")) {
48
+ const slash = rest.indexOf("/");
49
+ if (slash === -1) fail(`invalid package spec: ${spec}`);
50
+ scope = rest.slice(0, slash);
51
+ rest = rest.slice(slash + 1);
52
+ }
53
+ const at = rest.lastIndexOf("@");
54
+ const name = at === -1 ? rest : rest.slice(0, at);
55
+ const version = at === -1 ? "" : rest.slice(at + 1);
56
+ return { name, version, pkgName: scope ? `${scope}/${name}` : name };
57
+ }
58
+
59
+ // A bare word like "latest"/"next"/"beta"/"canary" is an npm dist-tag.
60
+ // Anything starting with a digit, "v", or a range operator is treated as a
61
+ // real pinned version/range instead.
62
+ function isDistTag(version) {
63
+ return !/^[vV]?\d|^[\^~<>=]/.test(version);
64
+ }
65
+
66
+ // Ask bun itself where its global bin dir is, rather than re-deriving
67
+ // $BUN_INSTALL/$HOME ourselves -- this is the same resolution bun's own
68
+ // installer used, so it can't disagree with where `bun i -g` actually put
69
+ // things (config file settings included, not just env vars).
70
+ //
71
+ // `bun pm bin -g` only works once something has been globally installed at
72
+ // least once (it errors with "no package.json" for install/global before
73
+ // that project exists). On a fresh $BUN_INSTALL/$HOME this script hasn't
74
+ // installed anything yet either, so fall back to the plain env-var
75
+ // computation for that first call -- needsInstall() below will see nothing
76
+ // at that path and install, which creates the project bun pm needs, so every
77
+ // call after the first one resolves through bun pm bin -g instead.
78
+ function globalBinDir() {
79
+ const result = Bun.spawnSync(["bun", "pm", "bin", "-g"], {
80
+ stdio: ["ignore", "pipe", "ignore"],
81
+ });
82
+ if (result.exitCode === 0) return result.stdout.toString().trim();
83
+ return path.join(process.env.BUN_INSTALL || path.join(process.env.HOME, ".bun"), "bin");
84
+ }
85
+
86
+ // `bun i -g` produces bin/<name> on POSIX (a relative symlink resolved via
87
+ // shebang) but bin/<name>.exe on Windows (a compiled shim + a companion
88
+ // .bunx metadata file -- Windows has neither reliable symlinks nor shebang
89
+ // support). Missing this suffix means every Windows lookup below finds
90
+ // nothing, which is harmless: needsInstall() just treats it as "not
91
+ // installed on this platform yet" and installs it here.
92
+ const EXE_SUFFIX = process.platform === "win32" ? ".exe" : "";
93
+
94
+ // Buninu on Android isn't actually running inside Termux, so nothing sets
95
+ // $PREFIX -- but some installed CLIs check it at runtime to find themselves
96
+ // relative to a Termux/Android system. Fake it only for the actual target
97
+ // execution below (not the `bun i -g` install step); an existing $PREFIX
98
+ // (e.g. genuinely running under Termux) is left alone.
99
+ function childEnv() {
100
+ if (process.platform !== "android" || process.env.PREFIX) return process.env;
101
+ return { ...process.env, PREFIX: "/data/data/com.termux/files/usr" };
102
+ }
103
+
104
+ function recordedVersion(bunInstallRoot, pkgName) {
105
+ const manifestPath = path.join(bunInstallRoot, "install", "global", "package.json");
106
+ try {
107
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
108
+ return manifest.dependencies && manifest.dependencies[pkgName];
109
+ } catch {
110
+ return undefined;
111
+ }
112
+ }
113
+
114
+ function needsInstall(bunInstallRoot, target, version, pkgName) {
115
+ if (version && isDistTag(version)) return true;
116
+ if (!fs.existsSync(target)) return true;
117
+ if (version && recordedVersion(bunInstallRoot, pkgName) !== version) return true;
118
+ try {
119
+ const age = Date.now() - fs.statSync(target).mtimeMs;
120
+ return age > CACHE_TTL_MS;
121
+ } catch {
122
+ return true;
123
+ }
124
+ }
125
+
126
+ const args = process.argv.slice(2);
127
+ if (args.length < 1) fail("usage: bunx <package>[@version] [args...]");
128
+
129
+ const spec = args[0];
130
+ const forwarded = args.slice(1);
131
+ const { name: bin, version, pkgName } = parseSpec(spec);
132
+ const binDir = globalBinDir();
133
+ const bunInstallRoot = path.dirname(binDir);
134
+ const target = path.join(binDir, bin + EXE_SUFFIX);
135
+
136
+ if (needsInstall(bunInstallRoot, target, version, pkgName)) {
137
+ // Same Android check bin/init.js's detectEnvironment() uses. Android's
138
+ // storage (FUSE-backed app-private paths) frequently rejects hardlinks
139
+ // and symlinks across the directories bun links between, so fall back to
140
+ // plain file copies there; hardlink (bun's default) is fine elsewhere.
141
+ const installArgs = ["bun", "i", "-g", spec];
142
+ if (fs.existsSync("/system/bin/linker64")) installArgs.push("--backend=copyfile");
143
+
144
+ const install = Bun.spawnSync(installArgs, {
145
+ stdio: ["inherit", "inherit", "inherit"],
146
+ });
147
+ if (install.exitCode !== 0) process.exit(install.exitCode ?? 1);
148
+ }
149
+
150
+ if (!fs.existsSync(target)) {
151
+ fail(`${spec} installed but ${target} not found (binary name may differ from package name)`);
152
+ }
153
+
154
+ // Run through `bun <target>` instead of exec'ing `target` directly. Real
155
+ // bunx does the latter (relies on the OS reading the shebang), which is fine
156
+ // on desktop but not reliable here: on Android the resolved bin usually
157
+ // lives under a path mounted noexec, so the kernel refuses to exec it even
158
+ // though it's readable and chmod +x. `bun` itself is the one binary Android
159
+ // does let us execute (from nativeLibraryDir), so hand it the target as an
160
+ // argument to read+run rather than exec directly. Trade-off: a package whose
161
+ // "bin" is a real native executable (not a JS/bun script) won't run this
162
+ // way -- accepted, since that's the rarer case and this is what makes the
163
+ // common (JS-script bin) case work on Android at all.
164
+ const run = Bun.spawnSync(["bun", target, ...forwarded], {
165
+ env: childEnv(),
166
+ stdio: ["inherit", "inherit", "inherit"],
167
+ });
168
+ process.exit(run.exitCode ?? 1);
package/apps/cmdlist CHANGED
@@ -14,3 +14,9 @@ jsmdcui
14
14
 
15
15
  ### musl-la — Launch AArch64 ELF programs with the bundled musl loader
16
16
  musl-la
17
+
18
+ ### buninu-help — Render README.md with glow, then show icon.png with jsgotty --viu
19
+ buninu-help
20
+
21
+ ### bunx — Globally install a package with bun, then exec its matching binary
22
+ bunx
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ call "%~dp0bun.bat" "%~dp0..\apps\jsmdcui\src\index.js" --cat "%~dp0..\README.md"
3
+ call "%~dp0bun.bat" "%~dp0..\apps\jsgotty\gotty.js" --viu "%~dp0..\icon.png"
4
+ exit /b %ERRORLEVEL%
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+
3
+ sd=$(dirname "$(realpath "$0")")
4
+ root=$sd/..
5
+
6
+ bun "$root"/apps/jsmdcui/src/index.js --cat "$root"/README.md
7
+ exec bun "$root"/apps/jsgotty/gotty.js --viu "$root"/icon.png
package/bin/bunx.bat ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+ call "%~dp0bun.bat" "%~dp0..\apps\bunx\bunx.js" %*
3
+ exit /b %ERRORLEVEL%
package/bin/init.js CHANGED
@@ -30,6 +30,14 @@ Init options:
30
30
  --shell <path|name> Override buninu.shell for this run
31
31
  --command <command> Override buninu.command for this run
32
32
 
33
+ Direct app launch (must be the first argument; skips the shell/command
34
+ startup flow entirely):
35
+ --jsgotty [args...] Spawn jsgotty directly, forwarding remaining
36
+ arguments, and exit with its exit code
37
+ (--jsgotty --help shows jsgotty's own options)
38
+ --jsmdcui [args...] Spawn jsmdcui directly, same forwarding behavior
39
+ --musl-la [args...] Spawn musl-la directly, same forwarding behavior
40
+
33
41
  Package configuration:
34
42
  package.json contains a buninu section for default and platform settings.
35
43
  buninu.shell Select the shell to start
@@ -395,6 +403,34 @@ async function createChildEnvironment(environment) {
395
403
  };
396
404
  }
397
405
 
406
+ const directAppTargets = {
407
+ "--jsgotty": "jsgotty",
408
+ "--jsmdcui": "jsmdcui",
409
+ "--musl-la": "musl-la",
410
+ };
411
+
412
+ async function runDirectApp(name, forwardedArgs, environment) {
413
+ const isWindows = environment.name === "windows";
414
+ const launcherPath = resolve(binDir, isWindows ? `${name}.bat` : name);
415
+ if (!await pathExists(launcherPath)) {
416
+ fail(`${name} is not available on ${environment.name}: ${launcherPath}`);
417
+ }
418
+
419
+ const childEnvironment = await createChildEnvironment(environment);
420
+ const argv = isWindows
421
+ ? [process.env.ComSpec || "cmd.exe", "/c", launcherPath, ...forwardedArgs]
422
+ : [launcherPath, ...forwardedArgs];
423
+
424
+ const child = Bun.spawn(argv, {
425
+ cwd: rootDir,
426
+ env: childEnvironment,
427
+ stdin: "inherit",
428
+ stdout: "inherit",
429
+ stderr: "inherit",
430
+ });
431
+ process.exit(await child.exited);
432
+ }
433
+
398
434
  function readInitArguments(arguments_) {
399
435
  const forwarded = [];
400
436
  let shell = null;
@@ -459,6 +495,14 @@ if (typeof startScript !== "string" || !startScript.trim()) {
459
495
  fail("package.json does not define scripts.start");
460
496
  }
461
497
 
498
+ const directAppTarget = directAppTargets[process.argv[2]];
499
+ if (directAppTarget) {
500
+ const directEnvironment = await detectEnvironment();
501
+ await runPlatformSwitch(directEnvironment);
502
+ await rebuildNativeSymlinks(directEnvironment);
503
+ await runDirectApp(directAppTarget, process.argv.slice(3), directEnvironment);
504
+ }
505
+
462
506
  if (await handleInformationArguments(process.argv.slice(2))) process.exit(0);
463
507
 
464
508
  const command = splitCommand(startScript);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu",
3
- "version": "0.1.9",
3
+ "version": "0.2.2",
4
4
  "description": "Buninu is a portable, self-bootstrapping Unix-like userspace built on Bun that runs across operating systems",
5
5
  "type": "module",
6
6
  "main": "./bin/init.js",
@@ -17,15 +17,15 @@
17
17
  "windows": null
18
18
  },
19
19
  "command": {
20
- "default": "echo Welcome to Buninu, Try: glow apps/cmdlist",
21
- "android": "if command -v libmain.so >/dev/null 2>&1; then libmain.so; else printf '%s\\n' '# Welcome to Buninu Android' '- Try: glow apps/cmdlist'; fi",
20
+ "default": "echo Welcome to Buninu, Try: glow apps/cmdlist, Help: buninu-help",
21
+ "android": "if command -v libmain.so >/dev/null 2>&1; then libmain.so; else printf '%s\\n' '# Welcome to Buninu Android' '- Try: glow apps/cmdlist' '- Help: buninu-help'; fi",
22
22
  "linux": null,
23
23
  "macos": null,
24
24
  "windows": null
25
25
  }
26
26
  },
27
27
  "scripts": {
28
- "start": "bun ./apps/jsgotty/gotty.js --reconnect -r -w --webgl",
28
+ "start": "bun ./apps/jsgotty/gotty.js --reconnect -r -w --webgl -a 127.0.0.1",
29
29
  "check": "node --check ./bin/init.js"
30
30
  }
31
31
  }