buninu 0.1.5 → 0.1.7

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,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.7 - 2026-08-17
4
+
5
+ ### Added
6
+
7
+ - Add `buninu --export [output.tgz]` to create a gzip-compressed tar archive of
8
+ the current Buninu installation. The archive retains exactly one top-level
9
+ directory and defaults to `./buninu.tgz`. An existing output is backed up and
10
+ restored if archive creation or replacement fails.
11
+ - Add `apps/cmdlist` as the validated source of POSIX multicall commands.
12
+ Normal startup now recreates every listed command as a `shloader` symlink,
13
+ including after npm installs that omit package symlinks.
14
+ - Add the Buninu-only `# syntax: markdown` first-line marker for Markdown
15
+ highlighting in extensionless files such as `apps/cmdlist`.
16
+
17
+ ### Changed
18
+
19
+ - Update the default and Android startup greetings to point users to
20
+ `glow apps/cmdlist`.
21
+ - Update `musl-la` to prefer Android's native `libld-musl.so`, fall back to the
22
+ bundled loader, add the target ELF directory and existing `LD_LIBRARY_PATH`
23
+ to its library search path, and support `-e` for the environment-variable
24
+ launch mode.
25
+ - Document how to add POSIX multicall commands and their separate Windows
26
+ batch launchers.
27
+
28
+ ### Fixed
29
+
30
+ - Keep the POSIX startup wrapper and Android greeting command on one line so
31
+ process listings do not expose embedded LF (`Ctrl+J`) characters.
32
+
3
33
  ## 0.1.5 - 2026-08-16
4
34
 
5
35
  ### Fixed
package/README.md CHANGED
@@ -4,14 +4,13 @@
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.
7
- > The current early release implements and supports Linux first. Android and
8
- > Windows support are planned, but are not ready yet.
7
+ > Linux, Android, and Windows can run the current early implementation, but
8
+ > their support and portability behavior are still being tested and refined.
9
9
 
10
10
  - Buninu is a portable, self-bootstrapping Unix-like userspace built on Bun that runs across operating systems
11
11
 
12
12
  - It starts a browser-accessible terminal through jsgotty. The architecture is
13
- designed for Android, Linux, and Windows, but the current implementation is
14
- Linux-only.
13
+ designed for Android, Linux, and Windows.
15
14
 
16
15
  - Name expansions:
17
16
  * English: **BUNinu Is Not Unix** 🐮
@@ -28,10 +27,17 @@ and self-bootstrapping design.
28
27
 
29
28
  ## Differences from upstream
30
29
 
31
- - The bundled jsmdcui is configured editor-first by MDCUI_DEFAULT_EDIT, so starting it with a Markdown file opens the normal terminal editor.
32
- - The bundled jsmdcui adds a launcher script to restore its original behavior: `apps/jsmdcui/jsmdcui.sh`, which starts its `tui` entry point with `--mdcui`,
33
- and forwards all command-line arguments.
34
- - The bundled jsgotty package no longer depends on or ships `node-pty`. Its PTY is provided by Bun's terminal API.
30
+ ### The bundled jsmdcui
31
+
32
+ * Is configured editor-first by MDCUI_DEFAULT_EDIT
33
+ * The command `jmi` with a Markdown file opens the normal terminal editor.(js micro editor)
34
+ * The command `jsmdcui` preserves its original behavior: `apps/jsmdcui/jsmdcui.sh`, which starts its `tui` entry point with `--mdcui` when running markdown Apps, and forwards all command-line arguments.
35
+ * Adds the Buninu-only `# syntax: markdown` marker for Markdown highlighting in extensionless files; upstreaming may be considered later.
36
+
37
+ ### The bundled jsgotty
38
+
39
+ * No longer depends on or ships `node-pty`.
40
+ * Its PTY is provided by Bun's terminal API.
35
41
 
36
42
  ## Install Bun
37
43
 
@@ -66,14 +72,114 @@ npx buninu --credential user:pass
66
72
  npx buninu --port 9000
67
73
  ```
68
74
 
75
+ ## Command-line information
76
+
77
+ ```text
78
+ -h, --help Show command-line help
79
+ -V, --version Show the Buninu, Bun, platform, and architecture versions
80
+ --readme Render README.md in the terminal
81
+ --changelog Render CHANGELOG.md in the terminal
82
+ ```
83
+
84
+ ## Export
85
+
86
+ Export the current Buninu installation as a gzip-compressed tar archive:
87
+
88
+ ```sh
89
+ npx buninu --export
90
+ npx buninu --export /path/to/buninu.tgz
91
+ ```
92
+
93
+ The default output is `./buninu.tgz`. The archive contains exactly one
94
+ top-level directory so consumers can remove one component while extracting.
95
+ Export requires `tar` in `PATH`; when replacing an existing output,
96
+ Buninu restores the previous file if archive creation or replacement fails.
97
+
69
98
  ## Environment
70
99
 
71
100
  `BUNINU_HOME` is the absolute path to the installed Buninu package root.
72
101
  The jsgotty process, startup command, and interactive shell start with this
73
102
  directory as their working directory. Buninu also appends
74
- `$BUNINU_HOME/bin` to `PATH`.
103
+ `$BUNINU_HOME/bin` to `PATH` (`Path` on Windows).
104
+
105
+ Buninu preserves inherited environment variables and supplies these fallbacks:
106
+
107
+ - `HOME`: inherited value, or `BUNINU_HOME` when unset.
108
+ - `TMPDIR`: inherited value; on Android, use the app cache directory when it
109
+ exists or `$BUNINU_HOME/tmp` otherwise; on other platforms, use the system
110
+ temporary directory.
111
+ - `SHELL`: inherited value, or a detected platform-appropriate shell.
112
+ - `TERM`: `xterm-256color` when unset.
113
+ - `COLORTERM`: `truecolor` when unset.
114
+
115
+ The Buninu Android APK launcher also supplies user-aware external-storage
116
+ paths obtained from Android APIs:
117
+
118
+ - `PKG_DDIR`: `/storage/emulated/<user-id>/Android/data/<package>`.
119
+ - `PKG_MDIR`: `/storage/emulated/<user-id>/Android/media/<package>`.
120
+
121
+ ## Add a command
122
+
123
+ On POSIX systems, Buninu exposes its commands through the multicall
124
+ `shloader`. The command names are listed one per line in `apps/cmdlist`:
125
+
126
+ ```text
127
+ # syntax: markdown
128
+
129
+ # Lines beginning with # are comments
130
+ glow
131
+ jsgotty
132
+ jsmdcui
133
+ ```
134
+
135
+ Blank lines and comment lines are ignored. Keep comments on their own lines;
136
+ inline comments are not supported. Command names may contain ASCII letters,
137
+ digits, `.`, `_`, and `-`, and must begin with a letter or digit.
138
+
139
+ For example, to add a command named `hello`, create
140
+ `apps/hello/hello.js`:
141
+
142
+ ```js
143
+ console.log("Hello from Buninu");
144
+ ```
145
+
146
+ Then add its name to `apps/cmdlist`:
147
+
148
+ ```text
149
+ hello
150
+ ```
151
+
152
+ Restart Buninu normally. During startup, `bin/init.js` reads the list and
153
+ rebuilds `bin/hello` as a symbolic link to `shloader`. The command is then
154
+ available from the Buninu shell:
155
+
156
+ ```sh
157
+ hello
158
+ ```
159
+
160
+ The multicall loader looks for an implementation in this order:
161
+
162
+ ```text
163
+ bin/hello.sh
164
+ apps/hello/hello.sh
165
+ apps/hello/hello.js
166
+ apps/hello/hello.mjs
167
+ apps/hello/hello.ts
168
+ apps/hello/hello.mts
169
+ ```
75
170
 
76
- ## Startup command
171
+ Running only an information option such as `--help` or `--version` does not
172
+ rebuild links; start Buninu normally at least once after changing the list.
173
+ Windows does not use these POSIX symbolic links. To expose the same command
174
+ on Windows, also provide `bin/hello.bat`, for example:
175
+
176
+ ```bat
177
+ @echo off
178
+ call "%~dp0bun.bat" "%~dp0..\apps\hello\hello.js" %*
179
+ exit /b %ERRORLEVEL%
180
+ ```
181
+
182
+ ## Startup command (optional)
77
183
 
78
184
  Set `buninu.command.default` or a platform-specific value (`android`, `linux`,
79
185
  or `windows`) in `package.json` to run a complete shell command from
@@ -101,7 +207,7 @@ Override it for one run with:
101
207
  bun ./bin/init.js --command "echo temporary command"
102
208
  ```
103
209
 
104
- ## Shell selection
210
+ ## Shell selection (optional)
105
211
 
106
212
  Set `buninu.shell.default` or a platform-specific value (`android`, `linux`,
107
213
  or `windows`) in `package.json`. A relative shell path is resolved
@@ -119,3 +225,12 @@ from the directory containing `package.json`.
119
225
  ```
120
226
 
121
227
  Use `--shell <path-or-name>` for a one-time override.
228
+
229
+ ## Process-list helpers
230
+
231
+ The bundled `.bashrc` provides `pspa` (`ps -eo pid,args`) and `pspac`, which
232
+ writes that process list to `$HOME/.pspidargs.sh` and displays it with `glow`.
233
+ Buninu preserves an existing `HOME` rather than replacing or modifying the
234
+ user's home; shells that honor `ENV` load the bundled file, while interactive
235
+ Bash may load only `$HOME/.bashrc`, so add `. "$BUNINU_HOME/.bashrc"` to a
236
+ custom Bash configuration when these aliases are not available.
package/apps/cmdlist ADDED
@@ -0,0 +1,16 @@
1
+ # syntax: markdown
2
+
3
+ ### glow — Render files with jsmdcui syntax highlighting
4
+ glow
5
+
6
+ ### jmi — Open files in the js micro editor
7
+ jmi
8
+
9
+ ### jsgotty — Run a browser-accessible terminal
10
+ jsgotty
11
+
12
+ ### jsmdcui — Edit and run interactive Markdown applications
13
+ jsmdcui
14
+
15
+ ### musl-la — Launch AArch64 ELF programs with the bundled musl loader
16
+ musl-la
@@ -2,6 +2,7 @@ filetype: markdown
2
2
 
3
3
  detect:
4
4
  filename: "\\.(livemd|md|mkd|mkdn|markdown)$"
5
+ header: "^# syntax: markdown$"
5
6
 
6
7
  rules:
7
8
  # Tables (Github extension)
@@ -2,4 +2,35 @@
2
2
 
3
3
  sd=$(dirname "$(realpath "$0")")
4
4
 
5
- exec "$sd"/ldmusl --library-path "$sd" "$@"
5
+ muslbin=$sd/../../bin/androidNativeLibs/libld-musl.so
6
+
7
+ if [ ! -f "$muslbin" ] ; then
8
+ muslbin=$sd/ld-musl-aarch64.so.1
9
+ fi
10
+
11
+ use_env=
12
+ if [ "$1" = -e ] ; then
13
+ shift
14
+ use_env=1
15
+ fi
16
+
17
+ elfdir=
18
+ for arg do
19
+ if [ -f "$arg" ] ; then
20
+ elf_magic=$(od -A n -t x1 -N 4 "$arg" 2>/dev/null | tr -d '[:space:]')
21
+ if [ "$elf_magic" = 7f454c46 ] ; then
22
+ elfdir=$(dirname "$(realpath "$arg")")
23
+ break
24
+ fi
25
+ fi
26
+ done
27
+
28
+ library_path=$sd${elfdir:+:$elfdir}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}
29
+
30
+ if [ "$use_env" = 1 ] ; then
31
+ LD_LIBRARY_PATH=$library_path
32
+ export LD_LIBRARY_PATH
33
+ exec "$muslbin" "$@"
34
+ fi
35
+
36
+ exec "$muslbin" --library-path "$library_path" "$@"
package/bin/init.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import pkg from "../package.json" with { type: "json" };
4
- import { existsSync, lstatSync, mkdirSync, symlinkSync, unlinkSync } from "node:fs";
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
7
  import { fileURLToPath } from "node:url";
@@ -26,6 +26,7 @@ Init options:
26
26
  -V, --version Show version and runtime information, then exit
27
27
  --readme Render README.md in the terminal and exit
28
28
  --changelog Render CHANGELOG.md in the terminal and exit
29
+ --export [output.tgz] Export this Buninu installation (default: ./buninu.tgz)
29
30
  --shell <path|name> Override buninu.shell for this run
30
31
  --command <command> Override buninu.command for this run
31
32
 
@@ -54,6 +55,12 @@ async function handleInformationArguments(arguments_) {
54
55
  return true;
55
56
  }
56
57
 
58
+ const exportOption = readExportArgument(arguments_);
59
+ if (exportOption) {
60
+ await exportInstallation(exportOption.output);
61
+ return true;
62
+ }
63
+
57
64
  if (arguments_.includes("--readme")) {
58
65
  const readmePath = resolve(REPO_ROOT, "README.md");
59
66
  if (!await pathExists(readmePath)) fail(`README not found: ${readmePath}`);
@@ -75,6 +82,61 @@ async function handleInformationArguments(arguments_) {
75
82
  return false;
76
83
  }
77
84
 
85
+ function readExportArgument(arguments_) {
86
+ for (let index = 0; index < arguments_.length; index += 1) {
87
+ const argument = arguments_[index];
88
+ if (argument === "--export") {
89
+ const next = arguments_[index + 1];
90
+ return { output: next || "buninu.tgz" };
91
+ }
92
+ if (argument.startsWith("--export=")) {
93
+ const output = argument.slice("--export=".length);
94
+ if (!output) fail("--export= requires an output path");
95
+ return { output };
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+
101
+ async function exportInstallation(output) {
102
+ const outputPath = resolve(process.cwd(), output);
103
+ const tar = Bun.which(process.platform === "win32" ? "tar.exe" : "tar") || Bun.which("tar");
104
+ if (!tar) fail("tar was not found in PATH");
105
+
106
+ mkdirSync(dirname(outputPath), { recursive: true });
107
+ const temporaryDir = mkdtempSync(resolve(systemTmpDir(), "buninu-export-"));
108
+ const temporaryArchive = resolve(temporaryDir, "buninu.tgz");
109
+ const temporaryBackup = resolve(temporaryDir, "previous-output.tgz");
110
+ const hadExistingOutput = existsSync(outputPath);
111
+ let exportError = null;
112
+ try {
113
+ // Keep the previous output recoverable, but remove it from its original
114
+ // path while tar walks REPO_ROOT so an in-tree export cannot include itself.
115
+ if (hadExistingOutput) {
116
+ if (!lstatSync(outputPath).isFile()) throw new Error(`export path is not a file: ${outputPath}`);
117
+ await Bun.write(temporaryBackup, Bun.file(outputPath));
118
+ unlinkSync(outputPath);
119
+ }
120
+ const process_ = Bun.spawn(
121
+ [tar, "-czf", temporaryArchive, "-C", dirname(REPO_ROOT), basename(REPO_ROOT)],
122
+ { env: { ...process.env }, stdin: "inherit", stdout: "inherit", stderr: "inherit" },
123
+ );
124
+ const status = await process_.exited;
125
+ if (status !== 0) throw new Error(`tar failed with exit ${status}`);
126
+ await Bun.write(outputPath, Bun.file(temporaryArchive));
127
+ } catch (error) {
128
+ exportError = error;
129
+ if (hadExistingOutput && await pathExists(temporaryBackup)) {
130
+ await Bun.write(outputPath, Bun.file(temporaryBackup));
131
+ }
132
+ } finally {
133
+ rmSync(temporaryDir, { recursive: true, force: true });
134
+ }
135
+
136
+ if (exportError) fail(exportError?.message || String(exportError));
137
+ console.log(outputPath);
138
+ }
139
+
78
140
  function splitCommand(command) {
79
141
  const words = [];
80
142
  let word = "";
@@ -119,7 +181,7 @@ async function findCommand(command) {
119
181
 
120
182
  async function detectEnvironment() {
121
183
  const windows = process.platform === "win32" || Boolean(process.env.WINDIR);
122
- const termux = !windows && await pathExists("/data/data/com.termux");
184
+ const termux = !windows && await pathExists("/data/data/com.termux/files/usr/bin/bash");
123
185
  const android = !windows && !termux && (
124
186
  process.platform === "android" ||
125
187
  await pathExists("/system/bin/linker64") ||
@@ -222,7 +284,7 @@ async function runPlatformSwitch(environment) {
222
284
  }
223
285
  }
224
286
 
225
- function rebuildNativeSymlinks(environment) {
287
+ async function rebuildNativeSymlinks(environment) {
226
288
  if (environment.name === "windows") return;
227
289
 
228
290
  const platformLinks = environment.name === "android"
@@ -234,14 +296,27 @@ function rebuildNativeSymlinks(environment) {
234
296
  bun: "bun.sh",
235
297
  shloader: "multicall.sh",
236
298
  };
237
- const links = {
238
- ...platformLinks,
239
- glow: "shloader",
240
- jmi: "shloader",
241
- jsgotty: "shloader",
242
- jsmdcui: "shloader",
243
- "musl-la": "shloader",
244
- };
299
+ const cmdlistPath = resolve(rootDir, "apps", "cmdlist");
300
+ if (!await pathExists(cmdlistPath)) {
301
+ fail(`command list does not exist: ${cmdlistPath}`);
302
+ }
303
+
304
+ const commandNames = [...new Set(
305
+ (await Bun.file(cmdlistPath).text())
306
+ .split(/\r?\n/)
307
+ .map((line) => line.trim())
308
+ .filter((line) => line && !line.startsWith("#")),
309
+ )];
310
+ for (const name of commandNames) {
311
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)) {
312
+ fail(`invalid command name in ${cmdlistPath}: ${name}`);
313
+ }
314
+ }
315
+
316
+ const links = Object.fromEntries([
317
+ ...Object.entries(platformLinks),
318
+ ...commandNames.map((name) => [name, "shloader"]),
319
+ ]);
245
320
 
246
321
  for (const [name, target] of Object.entries(links)) {
247
322
  const linkPath = resolve(binDir, name);
@@ -369,16 +444,14 @@ function shellCommandArguments(shell, command) {
369
444
  }
370
445
 
371
446
  const wrapped =
372
- `(\n${command}\n)\n` +
373
- `status=$?\n` +
374
- `if [ "$status" -ne 0 ]; then\n` +
375
- ` echo "buninu: startup command failed with exit $status" >&2\n` +
376
- `fi\n` +
377
- `exec "$0"`;
378
- // The argument after the command string becomes $0. Passing the shell path
379
- // as argv avoids interpolating or quoting an executable path inside the
380
- // user's shell command.
381
- return [shell, "-c", wrapped, shell];
447
+ `buninu_command=$1; shift; ( eval "$buninu_command" ); status=$?; ` +
448
+ `if [ "$status" -ne 0 ]; then ` +
449
+ `echo "buninu: startup command failed with exit $status" >&2; ` +
450
+ `fi; exec "$0"`;
451
+ // Pass both the shell and startup command as argv instead of interpolating
452
+ // them into the wrapper. This keeps the wrapper single-line and avoids LF
453
+ // (Ctrl+J) characters in its process-list representation.
454
+ return [shell, "-c", wrapped, shell, command];
382
455
  }
383
456
 
384
457
  const startScript = pkg.scripts?.start;
@@ -396,7 +469,7 @@ if (command[0] === "bun") command[0] = process.execPath;
396
469
 
397
470
  const environment = await detectEnvironment();
398
471
  await runPlatformSwitch(environment);
399
- rebuildNativeSymlinks(environment);
472
+ await rebuildNativeSymlinks(environment);
400
473
  const initArguments = readInitArguments(process.argv.slice(2));
401
474
  const configuredShell =
402
475
  pkg.buninu?.shell?.[environment.name] ??
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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,8 +17,8 @@
17
17
  "windows": null
18
18
  },
19
19
  "command": {
20
- "default": null,
21
- "android": "echo Welcome to Buninu Android",
20
+ "default": "echo Welcome to Buninu, Try: glow apps/cmdlist",
21
+ "android": "printf '%s\\n' '# Welcome to Buninu Android' '- Try: glow apps/cmdlist'",
22
22
  "linux": null,
23
23
  "macos": null,
24
24
  "windows": null
File without changes