buninu 0.2.0 → 0.2.3
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 +54 -0
- package/README.md +53 -6
- package/apps/bunx/bunx.js +168 -0
- package/apps/cmdlist +3 -0
- package/bin/bunx.bat +3 -0
- package/bin/init.js +60 -4
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.3 - 2026-08-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Add `buninu.exitAfterCmd` package.json setting (default `false`). When
|
|
8
|
+
`true`, the shell/PTY exits with `buninu.command`'s status instead of
|
|
9
|
+
falling back to an interactive shell once the command finishes; `false`
|
|
10
|
+
keeps the existing fall-back-to-shell behavior.
|
|
11
|
+
- Add `--export-config [output.json]`, which writes this entire `package.json`
|
|
12
|
+
to `buninu.json` in the current directory by default (like `--export`, the
|
|
13
|
+
output path can be overridden), instead of exporting the whole installation
|
|
14
|
+
like `--export` does. If the output path already exists, it now asks for
|
|
15
|
+
confirmation (`[y/N]`) before overwriting.
|
|
16
|
+
|
|
17
|
+
## 0.2.2 - 2026-08-17
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Add a "Commands inside the shell" section to README.md, split out of
|
|
22
|
+
Command-line information (which is only `bin/init.js`'s own flags, resolved
|
|
23
|
+
before Buninu starts). Lists everything in `apps/cmdlist` and moves the
|
|
24
|
+
`buninu-help`/`bunx` write-ups there, including a note that `bunx` can't
|
|
25
|
+
install anything on Android until
|
|
26
|
+
[oven-sh/bun#39084](https://github.com/oven-sh/bun/pull/39084) merges
|
|
27
|
+
upstream (Android's seccomp policy kills `bun i -g` with SIGSYS during bin
|
|
28
|
+
linking until then).
|
|
29
|
+
|
|
30
|
+
## 0.2.1 - 2026-08-17
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- Add `bunx`, a POSIX and Windows multicall command that ensures a package is
|
|
35
|
+
installed via `bun i -g` and then runs its matching binary, forwarding the
|
|
36
|
+
remaining arguments. Reinstall timing is adapted from real bunx's own rules
|
|
37
|
+
(an explicit dist-tag like `@latest` always reinstalls; otherwise a cached
|
|
38
|
+
binary is reused until it's older than 24h, or stat fails, or bun's own
|
|
39
|
+
`install/global/package.json` shows a different pinned version than the one
|
|
40
|
+
requested — this script has one shared install location instead of bunx's
|
|
41
|
+
per-version cache, so this last check substitutes for that). The global bin
|
|
42
|
+
directory is resolved via `bun pm bin -g`, falling back to
|
|
43
|
+
`$BUN_INSTALL`/`$HOME/.bun` on the very first install before that project
|
|
44
|
+
exists (which `bun pm bin -g` requires).
|
|
45
|
+
- Runs the target through `bun <target>` instead of exec'ing it directly:
|
|
46
|
+
on Android the resolved binary typically sits under storage mounted
|
|
47
|
+
noexec, so a direct exec is refused even though the file is readable and
|
|
48
|
+
executable-bit set. Trade-off: a package whose bin is a real native
|
|
49
|
+
executable rather than a JS/bun script won't run this way.
|
|
50
|
+
- On Android (detected the same way as `bin/init.js`, via
|
|
51
|
+
`/system/bin/linker64`), installs with `--backend=copyfile` since
|
|
52
|
+
hardlinks/symlinks frequently fail across Android's storage, and sets
|
|
53
|
+
`$PREFIX=/data/data/com.termux/files/usr` for the target process (not the
|
|
54
|
+
install step) so CLIs that check for a Termux environment still find one,
|
|
55
|
+
unless `$PREFIX` is already set.
|
|
56
|
+
|
|
3
57
|
## 0.2.0 - 2026-08-17
|
|
4
58
|
|
|
5
59
|
### Added
|
package/README.md
CHANGED
|
@@ -109,18 +109,41 @@ 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
|
|
|
119
142
|
Export the current Buninu installation as a gzip-compressed tar archive:
|
|
120
143
|
|
|
121
144
|
```sh
|
|
122
|
-
npx buninu --export
|
|
123
|
-
npx buninu --export /path/to/buninu.tgz
|
|
145
|
+
npx buninu@latest --export
|
|
146
|
+
npx buninu@latest --export /path/to/buninu.tgz
|
|
124
147
|
```
|
|
125
148
|
|
|
126
149
|
The default output is `./buninu.tgz`. The archive contains exactly one
|
|
@@ -128,6 +151,17 @@ top-level directory so consumers can remove one component while extracting.
|
|
|
128
151
|
Export requires `tar` in `PATH`; when replacing an existing output,
|
|
129
152
|
Buninu restores the previous file if archive creation or replacement fails.
|
|
130
153
|
|
|
154
|
+
Export this `package.json` on its own, instead of the whole installation:
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
npx buninu@latest --export-config
|
|
158
|
+
npx buninu@latest --export-config /path/to/buninu.json
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The default output is `./buninu.json`. This is the full `package.json` (not
|
|
162
|
+
just the `buninu` section), so the output is ready to use as-is anywhere a
|
|
163
|
+
complete replacement `package.json` is expected.
|
|
164
|
+
|
|
131
165
|
## Environment
|
|
132
166
|
|
|
133
167
|
`BUNINU_HOME` is the absolute path to the installed Buninu package root.
|
|
@@ -240,6 +274,19 @@ Override it for one run with:
|
|
|
240
274
|
bun ./bin/init.js --command "echo temporary command"
|
|
241
275
|
```
|
|
242
276
|
|
|
277
|
+
Set `buninu.exitAfterCmd` to `true` to exit once `buninu.command` finishes
|
|
278
|
+
instead of falling back to an interactive shell. It defaults to `false`,
|
|
279
|
+
which is the current fall-back-to-shell behavior described above.
|
|
280
|
+
|
|
281
|
+
```json
|
|
282
|
+
{
|
|
283
|
+
"buninu": {
|
|
284
|
+
"command": { "default": "echo Welcome to Buninu" },
|
|
285
|
+
"exitAfterCmd": true
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
243
290
|
## Shell selection (optional)
|
|
244
291
|
|
|
245
292
|
Set `buninu.shell.default` or a platform-specific value (`android`, `linux`,
|
|
@@ -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/bin/init.js
CHANGED
|
@@ -4,6 +4,7 @@ import pkg from "../package.json" with { type: "json" };
|
|
|
4
4
|
import { existsSync, lstatSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, unlinkSync } from "node:fs";
|
|
5
5
|
import { tmpdir as systemTmpDir } from "node:os";
|
|
6
6
|
import { basename, delimiter, dirname, isAbsolute, resolve } from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
|
|
9
10
|
const binDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -27,6 +28,7 @@ Init options:
|
|
|
27
28
|
--readme Render README.md in the terminal and exit
|
|
28
29
|
--changelog Render CHANGELOG.md in the terminal and exit
|
|
29
30
|
--export [output.tgz] Export this Buninu installation (default: ./buninu.tgz)
|
|
31
|
+
--export-config [output.json] Export this package.json (default: ./buninu.json)
|
|
30
32
|
--shell <path|name> Override buninu.shell for this run
|
|
31
33
|
--command <command> Override buninu.command for this run
|
|
32
34
|
|
|
@@ -42,10 +44,13 @@ Package configuration:
|
|
|
42
44
|
package.json contains a buninu section for default and platform settings.
|
|
43
45
|
buninu.shell Select the shell to start
|
|
44
46
|
buninu.command Run a startup command before entering the shell
|
|
47
|
+
buninu.exitAfterCmd Exit after buninu.command instead of falling back to
|
|
48
|
+
the shell (default: false)
|
|
45
49
|
|
|
46
50
|
All other options are forwarded to jsgotty. Without buninu.command, the
|
|
47
51
|
selected shell starts directly. With a command, it runs from the package.json
|
|
48
|
-
directory and then returns to the interactive shell regardless of exit status
|
|
52
|
+
directory and then returns to the interactive shell regardless of exit status,
|
|
53
|
+
unless buninu.exitAfterCmd is true, in which case it exits instead.
|
|
49
54
|
`;
|
|
50
55
|
}
|
|
51
56
|
|
|
@@ -69,6 +74,12 @@ async function handleInformationArguments(arguments_) {
|
|
|
69
74
|
return true;
|
|
70
75
|
}
|
|
71
76
|
|
|
77
|
+
const exportConfigOption = readExportConfigArgument(arguments_);
|
|
78
|
+
if (exportConfigOption) {
|
|
79
|
+
await exportConfig(exportConfigOption.output);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
72
83
|
if (arguments_.includes("--readme")) {
|
|
73
84
|
const readmePath = resolve(REPO_ROOT, "README.md");
|
|
74
85
|
if (!await pathExists(readmePath)) fail(`README not found: ${readmePath}`);
|
|
@@ -106,6 +117,22 @@ function readExportArgument(arguments_) {
|
|
|
106
117
|
return null;
|
|
107
118
|
}
|
|
108
119
|
|
|
120
|
+
function readExportConfigArgument(arguments_) {
|
|
121
|
+
for (let index = 0; index < arguments_.length; index += 1) {
|
|
122
|
+
const argument = arguments_[index];
|
|
123
|
+
if (argument === "--export-config") {
|
|
124
|
+
const next = arguments_[index + 1];
|
|
125
|
+
return { output: next || "buninu.json" };
|
|
126
|
+
}
|
|
127
|
+
if (argument.startsWith("--export-config=")) {
|
|
128
|
+
const output = argument.slice("--export-config=".length);
|
|
129
|
+
if (!output) fail("--export-config= requires an output path");
|
|
130
|
+
return { output };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
109
136
|
async function exportInstallation(output) {
|
|
110
137
|
const outputPath = resolve(process.cwd(), output);
|
|
111
138
|
const tar = Bun.which(process.platform === "win32" ? "tar.exe" : "tar") || Bun.which("tar");
|
|
@@ -145,6 +172,25 @@ async function exportInstallation(output) {
|
|
|
145
172
|
console.log(outputPath);
|
|
146
173
|
}
|
|
147
174
|
|
|
175
|
+
async function exportConfig(output) {
|
|
176
|
+
const outputPath = resolve(process.cwd(), output);
|
|
177
|
+
|
|
178
|
+
if (existsSync(outputPath)) {
|
|
179
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
180
|
+
let answer;
|
|
181
|
+
try {
|
|
182
|
+
answer = await rl.question(`${outputPath} already exists and may be overwritten. Continue? [y/N] `);
|
|
183
|
+
} finally {
|
|
184
|
+
rl.close();
|
|
185
|
+
}
|
|
186
|
+
if (!/^[yY]/.test(answer.trim())) fail("Cancelled: output file was not overwritten");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const packageJsonPath = resolve(REPO_ROOT, "package.json");
|
|
190
|
+
await Bun.write(outputPath, Bun.file(packageJsonPath));
|
|
191
|
+
console.log(outputPath);
|
|
192
|
+
}
|
|
193
|
+
|
|
148
194
|
function splitCommand(command) {
|
|
149
195
|
const words = [];
|
|
150
196
|
let word = "";
|
|
@@ -460,16 +506,25 @@ function readInitArguments(arguments_) {
|
|
|
460
506
|
return { shell, command, forwarded };
|
|
461
507
|
}
|
|
462
508
|
|
|
463
|
-
function shellCommandArguments(shell, command) {
|
|
509
|
+
function shellCommandArguments(shell, command, exitAfterCmd) {
|
|
464
510
|
if (!command) return [shell];
|
|
465
511
|
|
|
466
512
|
const shellName = basename(shell).toLowerCase();
|
|
467
513
|
if (shellName === "cmd" || shellName === "cmd.exe") {
|
|
514
|
+
if (exitAfterCmd) return [shell, "/d", "/s", "/c", command];
|
|
468
515
|
const quotedShell = `"${shell.replaceAll('"', '""')}"`;
|
|
469
516
|
return [shell, "/d", "/s", "/c", `${command} & ${quotedShell}`];
|
|
470
517
|
}
|
|
471
518
|
|
|
472
519
|
if (["powershell", "powershell.exe", "pwsh", "pwsh.exe"].includes(shellName)) {
|
|
520
|
+
if (exitAfterCmd) {
|
|
521
|
+
return [
|
|
522
|
+
shell,
|
|
523
|
+
"-NoLogo",
|
|
524
|
+
"-Command",
|
|
525
|
+
`& { ${command}; if (-not $?) { Write-Error 'Startup command failed' } }`,
|
|
526
|
+
];
|
|
527
|
+
}
|
|
473
528
|
const escapedShell = shell.replaceAll("'", "''");
|
|
474
529
|
return [
|
|
475
530
|
shell,
|
|
@@ -483,7 +538,7 @@ function shellCommandArguments(shell, command) {
|
|
|
483
538
|
`buninu_command=$1; shift; ( eval "$buninu_command" ); status=$?; ` +
|
|
484
539
|
`if [ "$status" -ne 0 ]; then ` +
|
|
485
540
|
`echo "buninu: startup command failed with exit $status" >&2; ` +
|
|
486
|
-
`fi; exec "$0"
|
|
541
|
+
(exitAfterCmd ? `fi; exit "$status"` : `fi; exec "$0"`);
|
|
487
542
|
// Pass both the shell and startup command as argv instead of interpolating
|
|
488
543
|
// them into the wrapper. This keeps the wrapper single-line and avoids LF
|
|
489
544
|
// (Ctrl+J) characters in its process-list representation.
|
|
@@ -539,6 +594,7 @@ const configuredCommand =
|
|
|
539
594
|
commandConfiguration?.default ??
|
|
540
595
|
null;
|
|
541
596
|
const startupCommand = initArguments.command ?? configuredCommand;
|
|
597
|
+
const exitAfterCmd = Boolean(pkg.buninu?.exitAfterCmd);
|
|
542
598
|
|
|
543
599
|
const forwardedArguments = initArguments.forwarded;
|
|
544
600
|
const hasExplicitPort = forwardedArguments.some((argument) =>
|
|
@@ -559,7 +615,7 @@ const child = Bun.spawn(
|
|
|
559
615
|
...command,
|
|
560
616
|
...portArguments,
|
|
561
617
|
...forwardedArguments,
|
|
562
|
-
...shellCommandArguments(environment.shell, startupCommand),
|
|
618
|
+
...shellCommandArguments(environment.shell, startupCommand, exitAfterCmd),
|
|
563
619
|
],
|
|
564
620
|
{
|
|
565
621
|
cwd: rootDir,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buninu",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"linux": null,
|
|
23
23
|
"macos": null,
|
|
24
24
|
"windows": null
|
|
25
|
-
}
|
|
25
|
+
},
|
|
26
|
+
"exitAfterCmd": false
|
|
26
27
|
},
|
|
27
28
|
"scripts": {
|
|
28
29
|
"start": "bun ./apps/jsgotty/gotty.js --reconnect -r -w --webgl -a 127.0.0.1",
|