buninu 0.2.0 → 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 +40 -0
- package/README.md +27 -4
- package/apps/bunx/bunx.js +168 -0
- package/apps/cmdlist +3 -0
- package/bin/bunx.bat +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
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
|
+
|
|
3
43
|
## 0.2.0 - 2026-08-17
|
|
4
44
|
|
|
5
45
|
### Added
|
package/README.md
CHANGED
|
@@ -109,10 +109,33 @@ outside the machine.
|
|
|
109
109
|
--musl-la [args...] Spawn musl-la directly and exit with its exit code
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
These are flags to `bin/init.js` itself, resolved before Buninu starts.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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.
|
|
116
139
|
|
|
117
140
|
## Export
|
|
118
141
|
|
|
@@ -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
package/bin/bunx.bat
ADDED
package/package.json
CHANGED