buninu 0.2.2 → 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 CHANGED
@@ -1,5 +1,19 @@
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
+
3
17
  ## 0.2.2 - 2026-08-17
4
18
 
5
19
  ### Added
package/README.md CHANGED
@@ -142,8 +142,8 @@ rejects a syscall bin-linking uses), so `bunx` can't install anything there yet.
142
142
  Export the current Buninu installation as a gzip-compressed tar archive:
143
143
 
144
144
  ```sh
145
- npx buninu --export
146
- npx buninu --export /path/to/buninu.tgz
145
+ npx buninu@latest --export
146
+ npx buninu@latest --export /path/to/buninu.tgz
147
147
  ```
148
148
 
149
149
  The default output is `./buninu.tgz`. The archive contains exactly one
@@ -151,6 +151,17 @@ top-level directory so consumers can remove one component while extracting.
151
151
  Export requires `tar` in `PATH`; when replacing an existing output,
152
152
  Buninu restores the previous file if archive creation or replacement fails.
153
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
+
154
165
  ## Environment
155
166
 
156
167
  `BUNINU_HOME` is the absolute path to the installed Buninu package root.
@@ -263,6 +274,19 @@ Override it for one run with:
263
274
  bun ./bin/init.js --command "echo temporary command"
264
275
  ```
265
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
+
266
290
  ## Shell selection (optional)
267
291
 
268
292
  Set `buninu.shell.default` or a platform-specific value (`android`, `linux`,
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.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",