buninu-linux 0.2.1 → 0.3.1
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/README.md +547 -190
- package/build-uki.sh +4 -2
- package/fetch-alpine.sh +78 -43
- package/index.js +4 -1
- package/initramfs/bin/ip +630 -0
- package/initramfs/bin/mount +401 -0
- package/initramfs/bin/poweroff +10 -0
- package/initramfs/bin/ps +256 -0
- package/initramfs/bin/reboot +10 -0
- package/initramfs/bin/tar +169 -0
- package/initramfs/bin/top +129 -0
- package/initramfs/bin/umount +71 -0
- package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +36 -0
- package/initramfs/buninu/apps/bunmsh/README.md +7 -0
- package/initramfs/buninu/apps/bunmsh/package.json +1 -1
- package/initramfs/buninu/apps/bunmsh/src/fancy-ls.js +29 -14
- package/initramfs/buninu/apps/bunmsh/src/line-edit.js +43 -0
- package/initramfs/buninu/apps/bunmsh/src/main.js +15 -1
- package/initramfs/buninu/apps/bunmsh/src/mouse.js +29 -4
- package/initramfs/buninu/apps/bunmsh/src/shell.js +9 -1
- package/initramfs/buninu/apps/bunmsh/test/shell1.test.js +70 -0
- package/initramfs/buninu/apps/jsmdcui/CHANGELOG.md +10 -0
- package/initramfs/buninu/apps/jsmdcui/README.md +2 -0
- package/initramfs/buninu/apps/jsmdcui/package.json +1 -1
- package/initramfs/buninu/apps/jsmdcui/runtime/help/commands.md +4 -1
- package/initramfs/buninu/apps/jsmdcui/runtime/help/help.md +2 -0
- package/initramfs/buninu/apps/jsmdcui/src/index.js +40 -13
- package/initramfs/buninu/package.json +1 -1
- package/initramfs/init.js +164 -11
- package/initramfs/lib/dlopen.js +121 -0
- package/initramfs/lib/document.js +6 -0
- package/initramfs/lib/modprobe.js +235 -0
- package/initramfs/lib/power.js +35 -0
- package/initramfs/usr/share/doc/buninu-linux/ip.md +155 -0
- package/initramfs/usr/share/doc/buninu-linux/mount.md +155 -0
- package/initramfs/usr/share/doc/buninu-linux/poweroff.md +15 -0
- package/initramfs/usr/share/doc/buninu-linux/ps.md +94 -0
- package/initramfs/usr/share/doc/buninu-linux/reboot.md +15 -0
- package/initramfs/usr/share/doc/buninu-linux/tar.md +73 -0
- package/initramfs/usr/share/doc/buninu-linux/top.md +23 -0
- package/initramfs/usr/share/doc/buninu-linux/umount.md +48 -0
- package/package.json +2 -2
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/bin/bun
|
|
2
|
+
// reboot — thin wrapper over the shared sync-and-reboot implementation.
|
|
3
|
+
|
|
4
|
+
import { LINUX_REBOOT_CMD_RESTART, runPowerCommand } from "../lib/power.js";
|
|
5
|
+
|
|
6
|
+
await runPowerCommand({
|
|
7
|
+
name: "reboot",
|
|
8
|
+
command: { name: "LINUX_REBOOT_CMD_RESTART", value: LINUX_REBOOT_CMD_RESTART },
|
|
9
|
+
action: "restarting",
|
|
10
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/bin/bun
|
|
2
|
+
// tar — a small tar/gzip/zstd CLI over Bun.Archive.
|
|
3
|
+
|
|
4
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
5
|
+
import { basename, join, resolve } from "node:path";
|
|
6
|
+
import { showDocument } from "../lib/document.js";
|
|
7
|
+
|
|
8
|
+
const fail = (message) => {
|
|
9
|
+
console.error(`tar: ${message}`);
|
|
10
|
+
process.exit(2);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const usage = () => {
|
|
14
|
+
console.error(`Usage:
|
|
15
|
+
tar -cf ARCHIVE FILE...
|
|
16
|
+
tar -xf ARCHIVE [-C DIR] [MEMBER...]
|
|
17
|
+
tar -tf ARCHIVE [MEMBER...]
|
|
18
|
+
tar -h | --help
|
|
19
|
+
|
|
20
|
+
Try 'tar --help' for the full manual.`);
|
|
21
|
+
process.exit(2);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const options = { operation: null, archive: null, directory: ".", compression: null, verbose: false, operands: [] };
|
|
25
|
+
const argv = process.argv.slice(2);
|
|
26
|
+
if (argv.length === 0) usage();
|
|
27
|
+
const take = (index, option) => {
|
|
28
|
+
if (index + 1 >= argv.length) fail(`${option} requires an argument`);
|
|
29
|
+
return argv[index + 1];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
for (let index = 0; index < argv.length; index++) {
|
|
33
|
+
const argument = argv[index];
|
|
34
|
+
const oldStyleOptions = index === 0 && /^[cxtrvfz]+$/.test(argument)
|
|
35
|
+
&& /[cxtr]/.test(argument);
|
|
36
|
+
if (argument === "--") { options.operands.push(...argv.slice(index + 1)); break; }
|
|
37
|
+
if (argument === "-h" || argument === "--help") {
|
|
38
|
+
await showDocument("tar");
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
if (argument === "--zstd") { options.compression = "zstd"; continue; }
|
|
42
|
+
if (argument === "--gzip") { options.compression = "gzip"; continue; }
|
|
43
|
+
if (argument === "-C" || argument === "--directory") {
|
|
44
|
+
options.directory = take(index++, argument);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (argument.startsWith("--directory=")) {
|
|
48
|
+
options.directory = argument.slice(12);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (argument === "-f" || argument === "--file") {
|
|
52
|
+
options.archive = take(index++, argument);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (argument.startsWith("--file=")) {
|
|
56
|
+
options.archive = argument.slice(7);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if ((argument.startsWith("-") && argument !== "-") || oldStyleOptions) {
|
|
60
|
+
const flags = oldStyleOptions ? argument : argument.slice(1);
|
|
61
|
+
for (let offset = 0; offset < flags.length; offset++) {
|
|
62
|
+
const flag = flags[offset];
|
|
63
|
+
if ("cxt".includes(flag)) {
|
|
64
|
+
if (options.operation && options.operation !== flag) fail("only one of -c, -x, or -t may be used");
|
|
65
|
+
options.operation = flag;
|
|
66
|
+
} else if (flag === "r") {
|
|
67
|
+
fail("-r append is not supported by Bun.Archive");
|
|
68
|
+
} else if (flag === "v") options.verbose = true;
|
|
69
|
+
else if (flag === "z") options.compression = "gzip";
|
|
70
|
+
else if (flag === "f") {
|
|
71
|
+
const attached = flags.slice(offset + 1);
|
|
72
|
+
options.archive = attached || take(index++, "-f");
|
|
73
|
+
break;
|
|
74
|
+
} else fail(`unknown option -- ${flag}`);
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
options.operands.push(argument);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!options.operation) fail("one of -c, -x, or -t is required");
|
|
82
|
+
if (!options.archive) fail("an archive must be specified with -f");
|
|
83
|
+
|
|
84
|
+
const archiveBytes = async () => {
|
|
85
|
+
const bytes = options.archive === "-"
|
|
86
|
+
? await Bun.stdin.bytes()
|
|
87
|
+
: await Bun.file(options.archive).bytes();
|
|
88
|
+
const zstd = options.compression === "zstd" || /\.(?:zst|tzst)$/i.test(options.archive);
|
|
89
|
+
return zstd ? Bun.zstdDecompressSync(bytes) : bytes;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const patterns = () => options.operands.flatMap((name) => {
|
|
93
|
+
const clean = name.replace(/^\.\//, "").replace(/\/$/, "");
|
|
94
|
+
return [clean, `${clean}/**`];
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const verboseListing = (name, file) => {
|
|
98
|
+
const modified = Number.isFinite(file.lastModified) && file.lastModified > 0
|
|
99
|
+
? new Date(file.lastModified).toISOString().replace("T", " ").slice(0, 16)
|
|
100
|
+
: "---- -- -- --:--";
|
|
101
|
+
return `${String(file.size).padStart(12)} ${modified} ${name}`;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (options.operation === "x" || options.operation === "t") {
|
|
105
|
+
let archive;
|
|
106
|
+
try {
|
|
107
|
+
archive = new Bun.Archive(await archiveBytes());
|
|
108
|
+
} catch (error) {
|
|
109
|
+
fail(error?.message || String(error));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (options.operation === "t") {
|
|
113
|
+
try {
|
|
114
|
+
const files = await archive.files(options.operands.length ? patterns() : undefined);
|
|
115
|
+
for (const [name, file] of files) {
|
|
116
|
+
console.log(options.verbose ? verboseListing(name, file) : name);
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
fail(error?.message || String(error));
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
try {
|
|
123
|
+
if (options.verbose) {
|
|
124
|
+
const files = await archive.files(options.operands.length ? patterns() : undefined);
|
|
125
|
+
for (const name of files.keys()) console.log(name);
|
|
126
|
+
}
|
|
127
|
+
await archive.extract(options.directory, options.operands.length ? { glob: patterns() } : undefined);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
fail(error?.message || String(error));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (options.operands.length === 0) fail("refusing to create an empty archive");
|
|
136
|
+
const root = resolve(options.directory);
|
|
137
|
+
const inputs = {};
|
|
138
|
+
|
|
139
|
+
const add = async (diskPath, archivePath) => {
|
|
140
|
+
const info = await lstat(diskPath);
|
|
141
|
+
if (info.isSymbolicLink()) fail(`${archivePath}: creating symbolic links is not supported by Bun.Archive`);
|
|
142
|
+
if (info.isDirectory()) {
|
|
143
|
+
for (const entry of await readdir(diskPath)) await add(join(diskPath, entry), `${archivePath}/${entry}`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (!info.isFile()) fail(`${archivePath}: unsupported file type`);
|
|
147
|
+
const name = archivePath.replaceAll("\\", "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
148
|
+
if (!name || name === ".." || name.startsWith("../") || name.includes("/../")) fail(`${archivePath}: unsafe archive path`);
|
|
149
|
+
// Bun 1.4.3 accepts Blob in ArchiveInput, but a lazy BunFile is currently
|
|
150
|
+
// archived as zero bytes. Materialise it until Bun.Archive handles BunFile
|
|
151
|
+
// contents directly.
|
|
152
|
+
inputs[name] = await Bun.file(diskPath).bytes();
|
|
153
|
+
if (options.verbose) console.error(name);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
for (const operand of options.operands) {
|
|
158
|
+
const diskPath = resolve(root, operand);
|
|
159
|
+
const name = operand.startsWith("/") ? basename(operand) : operand.replace(/\/$/, "");
|
|
160
|
+
await add(diskPath, name);
|
|
161
|
+
}
|
|
162
|
+
const archive = new Bun.Archive(inputs, options.compression === "gzip" ? { compress: "gzip" } : undefined);
|
|
163
|
+
const output = options.compression === "zstd"
|
|
164
|
+
? Bun.zstdCompressSync(await archive.bytes())
|
|
165
|
+
: archive;
|
|
166
|
+
await Bun.write(options.archive === "-" ? Bun.stdout : options.archive, output);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
fail(error?.message || String(error));
|
|
169
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/bin/bun
|
|
2
|
+
// top — periodically render snapshots and process lines supplied by ps.
|
|
3
|
+
|
|
4
|
+
import { showDocument } from "../lib/document.js";
|
|
5
|
+
import {
|
|
6
|
+
CLOCK_TICKS,
|
|
7
|
+
formatProcessHeader,
|
|
8
|
+
formatProcessLine,
|
|
9
|
+
prepareProcessFormat,
|
|
10
|
+
readProcessSnapshot,
|
|
11
|
+
} from "./ps";
|
|
12
|
+
|
|
13
|
+
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
14
|
+
const fields = ["pid", "user", "pri", "ni", "vsz", "rss", "state", "%cpu", "%mem", "time", "comm"];
|
|
15
|
+
|
|
16
|
+
const uptimeText = (seconds) => {
|
|
17
|
+
const days = Math.floor(seconds / 86400);
|
|
18
|
+
const hours = Math.floor(seconds % 86400 / 3600);
|
|
19
|
+
const minutes = Math.floor(seconds % 3600 / 60);
|
|
20
|
+
return `${days ? `${days} day${days === 1 ? "" : "s"}, ` : ""}${hours}:${String(minutes).padStart(2, "0")}`;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const memoryText = (memory) => {
|
|
24
|
+
const total = memory.MemTotal ?? 0;
|
|
25
|
+
const free = memory.MemFree ?? 0;
|
|
26
|
+
const cache = (memory.Buffers ?? 0) + (memory.Cached ?? 0) + (memory.SReclaimable ?? 0);
|
|
27
|
+
return `MiB Mem : ${(total / 1024).toFixed(1)} total, ${(free / 1024).toFixed(1)} free, ${((total - free - cache) / 1024).toFixed(1)} used, ${(cache / 1024).toFixed(1)} buff/cache`;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const intervalRows = (snapshot, previous) => {
|
|
31
|
+
if (!previous) return snapshot.processes;
|
|
32
|
+
const elapsed = Math.max(0.001, (snapshot.at - previous.at) / 1000);
|
|
33
|
+
const oldTicks = new Map(previous.processes.map((row) => [row.pid, row.ticks]));
|
|
34
|
+
return snapshot.processes.map((row) => ({
|
|
35
|
+
...row,
|
|
36
|
+
cpu: Math.max(0, row.ticks - (oldTicks.get(row.pid) ?? row.ticks)) / CLOCK_TICKS / elapsed * 100,
|
|
37
|
+
}));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const render = (snapshot, previous, sort, batch) => {
|
|
41
|
+
const rows = intervalRows(snapshot, previous);
|
|
42
|
+
rows.sort(sort === "memory"
|
|
43
|
+
? (a, b) => b.memory - a.memory || a.pid - b.pid
|
|
44
|
+
: sort === "pid" ? (a, b) => a.pid - b.pid : (a, b) => b.cpu - a.cpu || a.pid - b.pid);
|
|
45
|
+
const states = { running: 0, sleeping: 0, stopped: 0, zombie: 0 };
|
|
46
|
+
for (const row of rows) {
|
|
47
|
+
if (row.state[0] === "R") states.running++;
|
|
48
|
+
else if (row.state[0] === "T") states.stopped++;
|
|
49
|
+
else if (row.state[0] === "Z") states.zombie++;
|
|
50
|
+
else states.sleeping++;
|
|
51
|
+
}
|
|
52
|
+
const total = previous ? snapshot.cpu.total - previous.cpu.total : snapshot.cpu.total;
|
|
53
|
+
const idleTicks = previous ? snapshot.cpu.idle - previous.cpu.idle : snapshot.cpu.idle;
|
|
54
|
+
const idle = total > 0 ? idleTicks / total * 100 : 0;
|
|
55
|
+
const clock = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
56
|
+
const lines = [
|
|
57
|
+
`top - ${clock} up ${uptimeText(snapshot.uptime)}, load average: ${snapshot.loadAverage.map((v) => v.toFixed(2)).join(", ")}`,
|
|
58
|
+
`Tasks: ${rows.length} total, ${states.running} running, ${states.sleeping} sleeping, ${states.stopped} stopped, ${states.zombie} zombie`,
|
|
59
|
+
`%Cpu(s): ${(100 - idle).toFixed(1)} used, ${idle.toFixed(1)} id`,
|
|
60
|
+
memoryText(snapshot.memory),
|
|
61
|
+
"",
|
|
62
|
+
];
|
|
63
|
+
const format = prepareProcessFormat(rows, fields);
|
|
64
|
+
lines.push(formatProcessHeader(format));
|
|
65
|
+
// Some early boot consoles report a TTY size of 0x0. Treat that as
|
|
66
|
+
// unknown; otherwise Math.max(1, 0 - headers) displays only one process
|
|
67
|
+
// and makes it look as if the snapshot contained only top itself.
|
|
68
|
+
const terminalRows = Number(process.stdout.rows);
|
|
69
|
+
const screenRows = Number.isFinite(terminalRows) && terminalRows > lines.length
|
|
70
|
+
? terminalRows
|
|
71
|
+
: 24;
|
|
72
|
+
const limit = batch ? rows.length : Math.max(1, screenRows - lines.length);
|
|
73
|
+
for (const row of rows.slice(0, limit)) lines.push(formatProcessLine(row, format));
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
async function main(argv) {
|
|
78
|
+
let delay = 3;
|
|
79
|
+
let iterations = Infinity;
|
|
80
|
+
let batch = false;
|
|
81
|
+
for (let index = 0; index < argv.length; index++) {
|
|
82
|
+
const argument = argv[index];
|
|
83
|
+
if (argument === "-h" || argument === "--help") return showDocument("top");
|
|
84
|
+
if (argument === "-b") { batch = true; continue; }
|
|
85
|
+
if (argument === "-d") { delay = Number(argv[++index]); continue; }
|
|
86
|
+
if (argument === "-n") { iterations = Number(argv[++index]); continue; }
|
|
87
|
+
throw new Error(`top: unsupported option '${argument}'`);
|
|
88
|
+
}
|
|
89
|
+
if (!(delay > 0) || !(iterations > 0)) throw new Error("top: -d and -n require positive numbers");
|
|
90
|
+
let quit = false;
|
|
91
|
+
let sort = "cpu";
|
|
92
|
+
const interactive = !batch && process.stdin.isTTY && process.stdout.isTTY;
|
|
93
|
+
const cleanup = () => {
|
|
94
|
+
if (interactive) {
|
|
95
|
+
process.stdin.setRawMode(false);
|
|
96
|
+
process.stdin.pause();
|
|
97
|
+
process.stdout.write("\x1b[?25h\n");
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
if (interactive) {
|
|
101
|
+
process.stdin.setRawMode(true);
|
|
102
|
+
process.stdin.resume();
|
|
103
|
+
process.stdin.setEncoding("utf8");
|
|
104
|
+
process.stdin.on("data", (key) => {
|
|
105
|
+
if (key === "q" || key === "\u0003") quit = true;
|
|
106
|
+
else if (key === "P") sort = "cpu";
|
|
107
|
+
else if (key === "M") sort = "memory";
|
|
108
|
+
else if (key === "N") sort = "pid";
|
|
109
|
+
});
|
|
110
|
+
process.stdout.write("\x1b[?25l");
|
|
111
|
+
process.on("exit", cleanup);
|
|
112
|
+
} else if (!batch && iterations === Infinity) iterations = 1;
|
|
113
|
+
|
|
114
|
+
let previous = null;
|
|
115
|
+
try {
|
|
116
|
+
for (let count = 0; count < iterations && !quit; count++) {
|
|
117
|
+
const snapshot = readProcessSnapshot();
|
|
118
|
+
const output = render(snapshot, previous, sort, batch);
|
|
119
|
+
process.stdout.write(`${interactive ? "\x1b[H\x1b[2J" : ""}${output}\n`);
|
|
120
|
+
previous = snapshot;
|
|
121
|
+
if (count + 1 < iterations && !quit) await sleep(delay * 1000);
|
|
122
|
+
}
|
|
123
|
+
} finally { cleanup(); }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (import.meta.main) {
|
|
127
|
+
try { await main(process.argv.slice(2)); }
|
|
128
|
+
catch (error) { console.error(error.message); process.exit(1); }
|
|
129
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/bin/bun
|
|
2
|
+
// umount — unmount a filesystem. `umount --help` renders
|
|
3
|
+
// /usr/share/doc/buninu-linux/umount.md.
|
|
4
|
+
|
|
5
|
+
import { libc, cString, ptr, errno, strerror, Errno } from "../lib/dlopen.js";
|
|
6
|
+
import { showDocument } from "../lib/document.js";
|
|
7
|
+
|
|
8
|
+
const MNT = { FORCE: 1, DETACH: 2, EXPIRE: 4, NOFOLLOW: 8 };
|
|
9
|
+
|
|
10
|
+
const usage = () => {
|
|
11
|
+
console.error(`Usage:
|
|
12
|
+
umount [-f] [-l] [-R] [-v] TARGET...
|
|
13
|
+
umount -h | --help`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
let flags = 0;
|
|
18
|
+
let recursive = false;
|
|
19
|
+
let verbose = false;
|
|
20
|
+
let help = false;
|
|
21
|
+
const targets = [];
|
|
22
|
+
for (const argument of process.argv.slice(2)) {
|
|
23
|
+
if (argument === "-h" || argument === "--help") help = true;
|
|
24
|
+
else if (argument === "-f" || argument === "--force") flags |= MNT.FORCE;
|
|
25
|
+
else if (argument === "-l" || argument === "--lazy") flags |= MNT.DETACH;
|
|
26
|
+
else if (argument === "-R" || argument === "--recursive") recursive = true;
|
|
27
|
+
else if (argument === "-v" || argument === "--verbose") verbose = true;
|
|
28
|
+
else if (argument === "-n" || argument === "-d" || argument === "--no-mtab" || argument === "--detach-loop") {}
|
|
29
|
+
else if (argument === "-a" || argument === "--all") { console.error("umount: -a is not supported"); process.exit(1); }
|
|
30
|
+
else if (argument.startsWith("-") && argument.length > 1) { console.error(`umount: unknown option ${argument}`); usage(); }
|
|
31
|
+
else targets.push(argument);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (help) await showDocument("umount");
|
|
35
|
+
else if (targets.length === 0) usage();
|
|
36
|
+
|
|
37
|
+
const mountPoints = () => {
|
|
38
|
+
const table = Bun.file("/proc/self/mounts");
|
|
39
|
+
return table.text().then((text) =>
|
|
40
|
+
text.split("\n").filter(Boolean).map((line) =>
|
|
41
|
+
line.split(" ")[1].replace(/\\([0-7]{3})/g, (_, code) => String.fromCharCode(parseInt(code, 8))),
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const unmount = (target) => {
|
|
47
|
+
const result = libc.symbols.umount2(ptr(cString(target)), flags);
|
|
48
|
+
if (result === 0) {
|
|
49
|
+
if (verbose) console.log(`${target} unmounted`);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
const code = errno();
|
|
53
|
+
const reason = code === Errno.EBUSY ? "target is busy (try -l for a lazy unmount)"
|
|
54
|
+
: code === Errno.EINVAL ? "not mounted"
|
|
55
|
+
: code === Errno.EPERM ? "must be superuser to unmount"
|
|
56
|
+
: strerror(code);
|
|
57
|
+
console.error(`umount: ${target}: ${reason}.`);
|
|
58
|
+
return false;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
let status = 0;
|
|
62
|
+
for (const target of targets) {
|
|
63
|
+
const path = target.length > 1 ? target.replace(/\/+$/, "") : target;
|
|
64
|
+
// -R takes every mount at or below the target, deepest first, so each
|
|
65
|
+
// umount2 sees an empty subtree.
|
|
66
|
+
const list = recursive
|
|
67
|
+
? (await mountPoints()).filter((point) => point === path || point.startsWith(`${path}/`)).sort((a, b) => b.length - a.length)
|
|
68
|
+
: [path];
|
|
69
|
+
for (const point of list) if (!unmount(point)) status = 32;
|
|
70
|
+
}
|
|
71
|
+
process.exit(status);
|
|
@@ -2,6 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
All notable user-visible changes to bunmsh are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.3.9] - 2026-09-22
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Make pathname expansion follow directory symbolic links used as intermediate
|
|
10
|
+
path components, matching traditional shell behaviour. `Bun.Glob` disables
|
|
11
|
+
that traversal by default, so patterns such as `./*/*.js` silently skipped
|
|
12
|
+
a matching directory symlink, and sysfs patterns such as
|
|
13
|
+
`/sys/class/net/*/device` failed to pass through class-device links. bunmsh
|
|
14
|
+
now enables `followSymlinks` while scanning; a symlink matched as the final
|
|
15
|
+
component is still passed to the command as a symlink.
|
|
16
|
+
|
|
17
|
+
- Stop the fallback `ls`/`lsfancy` from printing every glob-expanded file as
|
|
18
|
+
a directory section. `builtin ls L*` previously expanded the glob
|
|
19
|
+
correctly, but multiple operands unconditionally received `filename:`
|
|
20
|
+
headings and blank separators. Only directories whose contents are being
|
|
21
|
+
listed now receive section headings; multiple ordinary files form one
|
|
22
|
+
continuous listing and retain their full operand paths rather than being
|
|
23
|
+
reduced to basenames.
|
|
24
|
+
|
|
25
|
+
- Honor `--` in `ls`/`lsfancy`, so names such as `-odd` after it are operands
|
|
26
|
+
rather than option clusters. Also follow a command-line symlink to a
|
|
27
|
+
directory for ordinary and recursive listings, as traditional `ls` does;
|
|
28
|
+
`-d`, `-l`, and `-F` continue to inspect the symlink itself.
|
|
29
|
+
|
|
30
|
+
## [0.3.8] - 2026-09-21
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- Make `Ctrl-U` and `Ctrl-K` toggle their edits in the interactive line
|
|
35
|
+
editor. `Ctrl-U` saves the text removed before the cursor and restores it
|
|
36
|
+
when pressed again at the beginning of the line; `Ctrl-K` does the same for
|
|
37
|
+
text after the cursor when pressed again at the end. Each shortcut keeps an
|
|
38
|
+
independent saved value, and control bytes inside bracketed paste remain
|
|
39
|
+
literal pasted content rather than triggering either action.
|
|
40
|
+
|
|
5
41
|
## [0.3.7] - 2026-09-20
|
|
6
42
|
|
|
7
43
|
### Fixed
|
|
@@ -476,6 +476,13 @@ Bun.e, Bun.file("/tm # -> a path, completed inside the string
|
|
|
476
476
|
|
|
477
477
|
- `Ctrl-D`: On an empty input line, sends EOF and exits the interactive shell.
|
|
478
478
|
|
|
479
|
+
- `Ctrl-U`: Removes and saves everything before the cursor. Press it again
|
|
480
|
+
while the cursor is at the beginning to restore that saved prefix.
|
|
481
|
+
|
|
482
|
+
- `Ctrl-K`: Removes and saves everything after the cursor. Press it again
|
|
483
|
+
while the cursor is at the end to restore that saved suffix. Its saved text
|
|
484
|
+
is independent from `Ctrl-U`.
|
|
485
|
+
|
|
479
486
|
- `Ctrl-T`: Calls `builtin tab` without adding a command to history. It creates
|
|
480
487
|
a tab when only one exists, or switches to the next tab otherwise. Any
|
|
481
488
|
command text currently being edited is preserved.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, lstatSync, readdirSync, readlinkSync } from "node:fs";
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, readlinkSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, resolve } from "node:path";
|
|
3
3
|
|
|
4
4
|
const IMAGE = new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "ico", "avif", "heic"]);
|
|
5
5
|
const MUSIC = new Set(["mp3", "wav", "flac", "ogg", "m4a", "aac", "opus", "mid", "midi"]);
|
|
@@ -120,9 +120,11 @@ export function fancyLs(argv, state, terminal = Boolean(process.stdout.isTTY)) {
|
|
|
120
120
|
recursive: false, time: false, reverse: false, size: false, one: false, classify: false,
|
|
121
121
|
};
|
|
122
122
|
const operands = [];
|
|
123
|
+
let parseOptions = true;
|
|
123
124
|
for (const argument of argv.slice(1)) {
|
|
124
|
-
if (
|
|
125
|
-
if (
|
|
125
|
+
if (parseOptions && argument === "--") { parseOptions = false; continue; }
|
|
126
|
+
if (parseOptions && (argument === "--color=auto" || argument === "--color")) continue;
|
|
127
|
+
if (parseOptions && /^-[^-]/.test(argument)) {
|
|
126
128
|
for (const flag of argument.slice(1)) {
|
|
127
129
|
if (flag === "a") options.all = true;
|
|
128
130
|
else if (flag === "A") options.almostAll = true;
|
|
@@ -142,15 +144,30 @@ export function fancyLs(argv, state, terminal = Boolean(process.stdout.isTTY)) {
|
|
|
142
144
|
if (!operands.length) operands.push(".");
|
|
143
145
|
let status = 0, stdout = "", stderr = "";
|
|
144
146
|
const visited = new Set();
|
|
145
|
-
|
|
147
|
+
let previousWasDirectory = false;
|
|
148
|
+
const show = (operand, heading = operands.length > 1, separate = true) => {
|
|
146
149
|
const path = isAbsolute(operand) ? operand : resolve(state.cwd, operand);
|
|
147
150
|
try {
|
|
148
151
|
const stats = lstatSync(path);
|
|
152
|
+
let listingDirectory = stats.isDirectory() && !options.directory;
|
|
153
|
+
// GNU/POSIX-style ls follows a command-line symlink to a directory for
|
|
154
|
+
// an ordinary listing and for -R. -d, -l, and -F request information
|
|
155
|
+
// about the link itself instead. A broken link simply cannot qualify.
|
|
156
|
+
if (stats.isSymbolicLink() && !options.directory && !options.long && !options.classify) {
|
|
157
|
+
try { listingDirectory = statSync(path).isDirectory(); } catch {}
|
|
158
|
+
}
|
|
159
|
+
// Multiple file operands are one listing, not a series of directory
|
|
160
|
+
// sections. Only actual directory contents receive `name:` headings
|
|
161
|
+
// and blank section separators.
|
|
162
|
+
if (separate && stdout && (listingDirectory || previousWasDirectory)) stdout += "\n";
|
|
149
163
|
let entries;
|
|
150
|
-
if (!
|
|
151
|
-
|
|
164
|
+
if (!listingDirectory)
|
|
165
|
+
// Preserve the operand spelling just like traditional ls. A glob such
|
|
166
|
+
// as ./*/*.js has already expanded to paths, and reducing each one to
|
|
167
|
+
// basename would lose the information that distinguishes its parent.
|
|
168
|
+
entries = [{ name: operand, stats, path }];
|
|
152
169
|
else entries = listDirectory(path, options);
|
|
153
|
-
if (heading) stdout += `${operand}:\n`;
|
|
170
|
+
if (heading && listingDirectory) stdout += `${operand}:\n`;
|
|
154
171
|
const rendered = entries.map((entry) => displayEntry(entry, options));
|
|
155
172
|
if (options.long) {
|
|
156
173
|
for (let i = 0; i < entries.length; i++) {
|
|
@@ -164,23 +181,21 @@ export function fancyLs(argv, state, terminal = Boolean(process.stdout.isTTY)) {
|
|
|
164
181
|
} else stdout += terminal && !options.one
|
|
165
182
|
? columns(rendered, process.stdout.columns ?? 80)
|
|
166
183
|
: `${rendered.join("\n")}${rendered.length ? "\n" : ""}`;
|
|
167
|
-
if (options.recursive &&
|
|
184
|
+
if (options.recursive && listingDirectory) {
|
|
168
185
|
for (const entry of entries) if (entry.stats.isDirectory() && entry.name !== "." && entry.name !== "..") {
|
|
169
186
|
const child = resolve(path, entry.name);
|
|
170
187
|
if (visited.has(child)) continue;
|
|
171
188
|
visited.add(child);
|
|
172
189
|
stdout += "\n";
|
|
173
|
-
show(`${operand.replace(/\/$/, "")}/${entry.name}`, true);
|
|
190
|
+
show(`${operand.replace(/\/$/, "")}/${entry.name}`, true, false);
|
|
174
191
|
}
|
|
175
192
|
}
|
|
193
|
+
previousWasDirectory = listingDirectory;
|
|
176
194
|
} catch (error) {
|
|
177
195
|
status = 1;
|
|
178
196
|
stderr += `bunmsh: ${argv[0]}: ${operand}: ${error.message}\n`;
|
|
179
197
|
}
|
|
180
198
|
};
|
|
181
|
-
for (
|
|
182
|
-
if (i) stdout += "\n";
|
|
183
|
-
show(operands[i]);
|
|
184
|
-
}
|
|
199
|
+
for (const operand of operands) show(operand);
|
|
185
200
|
return { status, stdout, stderr };
|
|
186
201
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function toggleSavedText(line, cursor, saved, side) {
|
|
2
|
+
line = String(line ?? "");
|
|
3
|
+
cursor = Math.max(0, Math.min(line.length, Number.isFinite(cursor) ? cursor : line.length));
|
|
4
|
+
saved = String(saved ?? "");
|
|
5
|
+
|
|
6
|
+
if (side === "head") {
|
|
7
|
+
if (cursor > 0) {
|
|
8
|
+
return {
|
|
9
|
+
line: line.slice(cursor),
|
|
10
|
+
cursor: 0,
|
|
11
|
+
saved: line.slice(0, cursor),
|
|
12
|
+
changed: true,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (saved) {
|
|
16
|
+
return {
|
|
17
|
+
line: saved + line,
|
|
18
|
+
cursor: saved.length,
|
|
19
|
+
saved: "",
|
|
20
|
+
changed: true,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
} else if (side === "tail") {
|
|
24
|
+
if (cursor < line.length) {
|
|
25
|
+
return {
|
|
26
|
+
line: line.slice(0, cursor),
|
|
27
|
+
cursor,
|
|
28
|
+
saved: line.slice(cursor),
|
|
29
|
+
changed: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (saved) {
|
|
33
|
+
return {
|
|
34
|
+
line: line + saved,
|
|
35
|
+
cursor,
|
|
36
|
+
saved: "",
|
|
37
|
+
changed: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { line, cursor, saved, changed: false };
|
|
43
|
+
}
|
|
@@ -29,6 +29,7 @@ import { importedHistory, readlineHistory, saveBunmshHistory } from "./history.j
|
|
|
29
29
|
import pkg from "../package.json" with { type:"json" }
|
|
30
30
|
import { MOUSE_OFF, MOUSE_ON, PASTE_OFF, PASTE_ON, mouseInput } from "./mouse.js";
|
|
31
31
|
import { homeRelativePath } from "./environment.js";
|
|
32
|
+
import { toggleSavedText } from "./line-edit.js";
|
|
32
33
|
|
|
33
34
|
const VERSION = `
|
|
34
35
|
${pkg.name}: ${pkg.description}
|
|
@@ -223,6 +224,8 @@ async function interactive(state) {
|
|
|
223
224
|
let lastTabClick = null;
|
|
224
225
|
let mouseCommandRunning = false;
|
|
225
226
|
let foregroundCommand = false;
|
|
227
|
+
let savedHead = "";
|
|
228
|
+
let savedTail = "";
|
|
226
229
|
let promptAbort = null;
|
|
227
230
|
// Set by onPaste (below) when a bracketed paste contains a newline, and
|
|
228
231
|
// consumed by the main loop's readline.question() catch block right after
|
|
@@ -332,6 +335,17 @@ async function interactive(state) {
|
|
|
332
335
|
const key = { name: delta > 0 ? "right" : "left" };
|
|
333
336
|
for (let n = 0; n < Math.abs(delta); n++) readline.write(null, key);
|
|
334
337
|
};
|
|
338
|
+
const toggleLineText = (side) => {
|
|
339
|
+
if (!readline || foregroundCommand) return;
|
|
340
|
+
const saved = side === "head" ? savedHead : savedTail;
|
|
341
|
+
const result = toggleSavedText(readline.line, readline.cursor, saved, side);
|
|
342
|
+
if (side === "head") savedHead = result.saved;
|
|
343
|
+
else savedTail = result.saved;
|
|
344
|
+
if (!result.changed) return;
|
|
345
|
+
readline.line = result.line;
|
|
346
|
+
readline.cursor = result.cursor;
|
|
347
|
+
readline.prompt(true);
|
|
348
|
+
};
|
|
335
349
|
const filteredInput = terminal ? mouseInput((mouse) => {
|
|
336
350
|
if (!mouse.press || (mouse.button & 3) !== 0 || (mouse.button & 32)) return;
|
|
337
351
|
pendingClick = { ...mouse, at: Date.now() };
|
|
@@ -384,7 +398,7 @@ async function interactive(state) {
|
|
|
384
398
|
else if (shortcut === "lsfancy") runFancyShortcut();
|
|
385
399
|
else if (shortcut === "lsfancy-parent") runFancyShortcut([".."]);
|
|
386
400
|
else if (shortcut === "tab-close") runTabShortcut(["x"]);
|
|
387
|
-
}, onPaste) : process.stdin;
|
|
401
|
+
}, onPaste, toggleLineText) : process.stdin;
|
|
388
402
|
const completer = (line) => {
|
|
389
403
|
commandIndex.refreshIfChanged(state);
|
|
390
404
|
if (javascriptLine(line)) {
|