buninu 0.2.5 → 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/CHANGELOG.md +40 -0
- package/README.md +100 -2
- package/apps/bunmsh/CHANGELOG.md +262 -0
- package/apps/bunmsh/LICENSE +21 -0
- package/apps/bunmsh/LICENSE-MKSH +295 -0
- package/apps/bunmsh/PORTING.md +192 -0
- package/apps/bunmsh/README.md +801 -0
- package/apps/bunmsh/bunmsh +7 -0
- package/apps/bunmsh/bunshell.md +61 -0
- package/apps/bunmsh/help/README.md +1945 -0
- package/apps/bunmsh/help/alias.md +22 -0
- package/apps/bunmsh/help/basename.md +38 -0
- package/apps/bunmsh/help/builtin.md +25 -0
- package/apps/bunmsh/help/bun.md +27 -0
- package/apps/bunmsh/help/bunmsh.md +47 -0
- package/apps/bunmsh/help/cat.md +31 -0
- package/apps/bunmsh/help/catfancy.md +35 -0
- package/apps/bunmsh/help/cd.md +22 -0
- package/apps/bunmsh/help/chmod.md +27 -0
- package/apps/bunmsh/help/clear.md +21 -0
- package/apps/bunmsh/help/colon.md +22 -0
- package/apps/bunmsh/help/command.md +27 -0
- package/apps/bunmsh/help/cp.md +28 -0
- package/apps/bunmsh/help/cut.md +25 -0
- package/apps/bunmsh/help/date.md +25 -0
- package/apps/bunmsh/help/dirname.md +25 -0
- package/apps/bunmsh/help/echo.md +25 -0
- package/apps/bunmsh/help/env.md +27 -0
- package/apps/bunmsh/help/eval.md +22 -0
- package/apps/bunmsh/help/exec.md +22 -0
- package/apps/bunmsh/help/exit.md +21 -0
- package/apps/bunmsh/help/export.md +22 -0
- package/apps/bunmsh/help/false.md +22 -0
- package/apps/bunmsh/help/find.md +33 -0
- package/apps/bunmsh/help/getopts.md +28 -0
- package/apps/bunmsh/help/grep.md +34 -0
- package/apps/bunmsh/help/head.md +27 -0
- package/apps/bunmsh/help/kill.md +27 -0
- package/apps/bunmsh/help/ln.md +28 -0
- package/apps/bunmsh/help/ls.md +37 -0
- package/apps/bunmsh/help/lsbun.md +29 -0
- package/apps/bunmsh/help/lsfancy.md +37 -0
- package/apps/bunmsh/help/md5sum.md +21 -0
- package/apps/bunmsh/help/mkdir.md +27 -0
- package/apps/bunmsh/help/mktemp.md +26 -0
- package/apps/bunmsh/help/mv.md +26 -0
- package/apps/bunmsh/help/print.md +30 -0
- package/apps/bunmsh/help/printf.md +25 -0
- package/apps/bunmsh/help/pwd.md +21 -0
- package/apps/bunmsh/help/read.md +27 -0
- package/apps/bunmsh/help/readonly.md +27 -0
- package/apps/bunmsh/help/realpath.md +21 -0
- package/apps/bunmsh/help/rm.md +33 -0
- package/apps/bunmsh/help/rmdir.md +27 -0
- package/apps/bunmsh/help/sed.md +29 -0
- package/apps/bunmsh/help/seq.md +29 -0
- package/apps/bunmsh/help/serve.md +50 -0
- package/apps/bunmsh/help/set.md +26 -0
- package/apps/bunmsh/help/sha256sum.md +21 -0
- package/apps/bunmsh/help/shift.md +23 -0
- package/apps/bunmsh/help/sleep.md +25 -0
- package/apps/bunmsh/help/sort.md +29 -0
- package/apps/bunmsh/help/source.md +23 -0
- package/apps/bunmsh/help/tab.md +30 -0
- package/apps/bunmsh/help/tac.md +29 -0
- package/apps/bunmsh/help/tail.md +27 -0
- package/apps/bunmsh/help/tee.md +25 -0
- package/apps/bunmsh/help/test.md +30 -0
- package/apps/bunmsh/help/time.md +29 -0
- package/apps/bunmsh/help/touch.md +26 -0
- package/apps/bunmsh/help/tr.md +26 -0
- package/apps/bunmsh/help/true.md +22 -0
- package/apps/bunmsh/help/type.md +21 -0
- package/apps/bunmsh/help/umask.md +22 -0
- package/apps/bunmsh/help/unalias.md +28 -0
- package/apps/bunmsh/help/uname.md +31 -0
- package/apps/bunmsh/help/unset.md +28 -0
- package/apps/bunmsh/help/wc.md +27 -0
- package/apps/bunmsh/help/whence.md +26 -0
- package/apps/bunmsh/help/which.md +41 -0
- package/apps/bunmsh/help/yes.md +23 -0
- package/apps/bunmsh/package.json +26 -0
- package/apps/bunmsh/serve.js +468 -0
- package/apps/bunmsh/single-exe/README.md +402 -0
- package/apps/bunmsh/single-exe/assetsHelper.js +371 -0
- package/apps/bunmsh/single-exe/assetsLoader.mjs +85 -0
- package/apps/bunmsh/single-exe/assetsPacker.js +218 -0
- package/apps/bunmsh/single-exe/compiled.js +527 -0
- package/apps/bunmsh/single-exe/entry.mjs +17 -0
- package/apps/bunmsh/src/cat-fancy.js +117 -0
- package/apps/bunmsh/src/cat-operands.js +31 -0
- package/apps/bunmsh/src/completion.js +247 -0
- package/apps/bunmsh/src/environment.js +61 -0
- package/apps/bunmsh/src/fancy-ls.js +186 -0
- package/apps/bunmsh/src/find.js +119 -0
- package/apps/bunmsh/src/history.js +198 -0
- package/apps/bunmsh/src/main.js +776 -0
- package/apps/bunmsh/src/mouse.js +103 -0
- package/apps/bunmsh/src/shell.js +3557 -0
- package/apps/bunmsh/test/catfancy.test.js +102 -0
- package/apps/bunmsh/test/reference-source.sh +1 -0
- package/apps/bunmsh/test/reference.test.js +677 -0
- package/apps/bunmsh/test/serve/README.md +3 -0
- package/apps/bunmsh/test/serve/bad.json +1 -0
- package/apps/bunmsh/test/serve/data.json +1 -0
- package/apps/bunmsh/test/serve/data.json5 +1 -0
- package/apps/bunmsh/test/serve/data.jsonc +2 -0
- package/apps/bunmsh/test/serve/data.jsonl +2 -0
- package/apps/bunmsh/test/serve/data.ndjson +1 -0
- package/apps/bunmsh/test/serve/data.toml +3 -0
- package/apps/bunmsh/test/serve/data.ts +15 -0
- package/apps/bunmsh/test/serve/data.xml +1 -0
- package/apps/bunmsh/test/serve/data.yaml +3 -0
- package/apps/bunmsh/test/serve/data.yml +1 -0
- package/apps/bunmsh/test/serve.test.js +241 -0
- package/apps/bunmsh/test/shell.test.js +1778 -0
- package/apps/bunmsh/test/t.sh +12 -0
- package/apps/native-bridge/native-bridge.js +45 -0
- package/apps/xdg-open/xdg-open.js +74 -2
- package/bin/init.js +11 -3
- package/demo-server.js +64 -0
- package/package.json +4 -2
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { appendFile, rename, stat } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
|
|
5
|
+
function importEnabled(env) {
|
|
6
|
+
return !/^(?:0|false|off|no)$/i.test(env.BUNMSH_IMPORT_HISTORY ?? "");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// History is rendered directly beside the prompt. Reject the entire entry
|
|
10
|
+
// instead of stripping bytes and accidentally turning it into another command.
|
|
11
|
+
export function safeHistoryEntry(entry) {
|
|
12
|
+
return typeof entry === "string" && entry.length > 0 &&
|
|
13
|
+
!/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028-\u202e\u2066-\u2069\ufeff]/u.test(entry);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// node:readline stores its history newest-first, while bunmsh keeps history in
|
|
17
|
+
// chronological order for saving and ghost completion.
|
|
18
|
+
export function readlineHistory(history) {
|
|
19
|
+
return history.filter(safeHistoryEntry).toReversed();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function bunmshHistoryPath(env = process.env, platform = process.platform) {
|
|
23
|
+
const trim = (value) => value?.replace(/[\\/]+$/, "");
|
|
24
|
+
if (platform === "win32") {
|
|
25
|
+
const base = trim(env.LOCALAPPDATA || env.APPDATA ||
|
|
26
|
+
(env.USERPROFILE ? `${trim(env.USERPROFILE)}/AppData/Local` : ""));
|
|
27
|
+
return base ? `${base}/bunmsh/history` : null;
|
|
28
|
+
}
|
|
29
|
+
const base = trim(env.XDG_DATA_HOME || (env.HOME ? `${trim(env.HOME)}/.local/share` : ""));
|
|
30
|
+
return base ? `${base}/bunmsh/history` : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function readableText(path) {
|
|
34
|
+
const file = Bun.file(path);
|
|
35
|
+
return await file.exists() ? file.text() : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function statSafe(path) {
|
|
39
|
+
try { return await stat(path); } catch { return null; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function parseBashHistory(source) {
|
|
43
|
+
return source
|
|
44
|
+
.split(/\r?\n/)
|
|
45
|
+
.filter((line) => !/^#[0-9]+$/.test(line) && safeHistoryEntry(line));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function parseFishHistory(source) {
|
|
49
|
+
const records = source.split(/\r?\n/)
|
|
50
|
+
.map((line) => /^- cmd:\s?(.*)$/.exec(line)?.[1] ?? null)
|
|
51
|
+
.filter((entry) => entry !== null);
|
|
52
|
+
// Fish documents this as "YAML-style", not YAML. Command scalars use Fish's
|
|
53
|
+
// own escaping and can be rejected or only partially consumed by YAML
|
|
54
|
+
// parsers. The stable record boundary is the unindented `- cmd:` prefix.
|
|
55
|
+
return records
|
|
56
|
+
.map((entry) => entry.replace(/\\(\\|n)/g, (_match, escaped) => escaped === "n" ? "\n" : "\\"))
|
|
57
|
+
.filter(safeHistoryEntry);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// bunmsh saved its own history as a single JSON array before switching to
|
|
61
|
+
// one-JSON-value-per-line (JSONL). Detecting and reading that old format
|
|
62
|
+
// keeps existing history files working; see saveBunmshHistory for where the
|
|
63
|
+
// on-disk file actually gets converted.
|
|
64
|
+
function isLegacyArrayFormat(source) {
|
|
65
|
+
return source.trimStart().startsWith("[");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseLegacyArrayFormat(source) {
|
|
69
|
+
const parsed = JSON.parse(source);
|
|
70
|
+
return Array.isArray(parsed) ? parsed.filter(safeHistoryEntry) : [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Bun.JSONL.parse stops at the first line it can't parse instead of skipping
|
|
74
|
+
// it, which would silently discard every entry after one corrupted line (for
|
|
75
|
+
// example from a write torn by a crash). Detect that by comparing against the
|
|
76
|
+
// non-empty line count, and fall back to a per-line parse that only skips the
|
|
77
|
+
// bad lines when it looks like that happened.
|
|
78
|
+
function parseJsonlFormat(source) {
|
|
79
|
+
const lines = source.split(/\r?\n/).filter((line) => line.trim() !== "");
|
|
80
|
+
let parsed;
|
|
81
|
+
try { parsed = Bun.JSONL.parse(source); } catch { parsed = []; }
|
|
82
|
+
if (parsed.length < lines.length) {
|
|
83
|
+
parsed = [];
|
|
84
|
+
for (const line of lines) {
|
|
85
|
+
try { parsed.push(JSON.parse(line)); } catch {}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return parsed.filter(safeHistoryEntry);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseBunmshHistoryContent(source) {
|
|
92
|
+
if (source.trim() === "") return [];
|
|
93
|
+
return isLegacyArrayFormat(source) ? parseLegacyArrayFormat(source) : parseJsonlFormat(source);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function encodeJsonlLines(entries) {
|
|
97
|
+
return entries.map((entry) => `${JSON.stringify(entry)}\n`).join("");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Writes the whole file in one shot via a temp file plus atomic rename, so a
|
|
101
|
+
// crash or interruption mid-write can never leave a half-written, unreadable
|
|
102
|
+
// history file behind — only ever the old content or the new content.
|
|
103
|
+
async function writeHistoryFileAtomic(path, entries) {
|
|
104
|
+
const tmpPath = `${path}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
105
|
+
await Bun.write(tmpPath, encodeJsonlLines(entries));
|
|
106
|
+
await rename(tmpPath, path);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function dedupeEntries(entries) {
|
|
110
|
+
const seen = new Set();
|
|
111
|
+
const unique = [];
|
|
112
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
113
|
+
const entry = entries[index];
|
|
114
|
+
if (!safeHistoryEntry(entry) || seen.has(entry)) continue;
|
|
115
|
+
seen.add(entry);
|
|
116
|
+
unique.push(entry);
|
|
117
|
+
}
|
|
118
|
+
return unique.reverse();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// The routine, frequent save path (manual `tab s`/`tab save`, the periodic
|
|
122
|
+
// autosave, and the on-exit flush all call this). It only appends the
|
|
123
|
+
// entries this session hasn't persisted yet — tracked on `state` via
|
|
124
|
+
// historySaved, a count into state.history — instead of rewriting the whole
|
|
125
|
+
// file, so it stays safe to call from multiple bunmsh sessions at once:
|
|
126
|
+
// each one only ever adds its own new lines to the end, never touching (or
|
|
127
|
+
// needing to know about) what any other session already wrote.
|
|
128
|
+
export async function saveBunmshHistory(state, env = process.env, platform = process.platform) {
|
|
129
|
+
const path = bunmshHistoryPath(env, platform);
|
|
130
|
+
if (!path) throw new Error("cannot determine bunmsh history path");
|
|
131
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
132
|
+
if (!state.historyMigrated) {
|
|
133
|
+
// One-time upgrade for a pre-existing legacy (JSON-array) file. Like the
|
|
134
|
+
// explicit dedupe below, this rewrites the whole file, so it carries the
|
|
135
|
+
// same small risk of racing another session's concurrent write — but it
|
|
136
|
+
// happens at most once per history file, ever.
|
|
137
|
+
const source = await readableText(path);
|
|
138
|
+
if (source !== null && isLegacyArrayFormat(source))
|
|
139
|
+
await writeHistoryFileAtomic(path, parseLegacyArrayFormat(source));
|
|
140
|
+
state.historyMigrated = true;
|
|
141
|
+
}
|
|
142
|
+
const saved = state.historySaved ?? 0;
|
|
143
|
+
const pending = (state.history ?? []).slice(saved).filter(safeHistoryEntry);
|
|
144
|
+
if (pending.length) await appendFile(path, encodeJsonlLines(pending));
|
|
145
|
+
state.historySaved = (state.history ?? []).length;
|
|
146
|
+
return path;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The explicit, occasional maintenance path (`tab s d` / `tab save dedupe`):
|
|
150
|
+
// rewrites the whole file with only the most recent occurrence of each
|
|
151
|
+
// command kept. Unlike saveBunmshHistory, this can race a concurrent save
|
|
152
|
+
// from another session — that's the trade-off for being able to shrink the
|
|
153
|
+
// file at all, since dropping duplicates means looking at (and replacing)
|
|
154
|
+
// entries this session didn't itself write. `raced` reports whether the
|
|
155
|
+
// file's mtime/size changed while this ran, as a best-effort heads-up (not a
|
|
156
|
+
// guarantee) that another session's write may have been overwritten by it.
|
|
157
|
+
export async function dedupeBunmshHistory(state, env = process.env, platform = process.platform) {
|
|
158
|
+
// Flush this session's own pending entries through the safe path first, so
|
|
159
|
+
// the dedupe/rewrite below is never the reason they'd be lost.
|
|
160
|
+
const path = await saveBunmshHistory(state, env, platform);
|
|
161
|
+
const before = await statSafe(path);
|
|
162
|
+
const source = await readableText(path);
|
|
163
|
+
const deduped = dedupeEntries(source === null ? [] : parseBunmshHistoryContent(source));
|
|
164
|
+
const after = await statSafe(path);
|
|
165
|
+
const raced = Boolean(before && after &&
|
|
166
|
+
(before.mtimeMs !== after.mtimeMs || before.size !== after.size));
|
|
167
|
+
await writeHistoryFileAtomic(path, deduped);
|
|
168
|
+
return { path, raced, count: deduped.length };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function importedHistory(env = process.env, platform = process.platform) {
|
|
172
|
+
const home = env.HOME?.replace(/\/+$/, "");
|
|
173
|
+
const paths = [];
|
|
174
|
+
if (importEnabled(env) && home) paths.push(
|
|
175
|
+
{ type: "bash", path: env.HISTFILE || `${home}/.bash_history` },
|
|
176
|
+
{ type: "fish", path: `${home}/.local/share/fish/fish_history` },
|
|
177
|
+
);
|
|
178
|
+
const ownPath = bunmshHistoryPath(env, platform);
|
|
179
|
+
if (ownPath) paths.push({ type: "bunmsh", path: ownPath });
|
|
180
|
+
const history = [];
|
|
181
|
+
for (const item of paths) {
|
|
182
|
+
try {
|
|
183
|
+
const source = await readableText(item.path);
|
|
184
|
+
if (source === null) continue;
|
|
185
|
+
history.push(...(item.type === "fish" ? parseFishHistory(source) :
|
|
186
|
+
item.type === "bunmsh" ? parseBunmshHistoryContent(source) : parseBashHistory(source)));
|
|
187
|
+
} catch {}
|
|
188
|
+
}
|
|
189
|
+
// Keep the most recent occurrence, matching historyGhost's reverse lookup.
|
|
190
|
+
const seen = new Set();
|
|
191
|
+
const unique = [];
|
|
192
|
+
for (let index = history.length - 1; index >= 0; index--) {
|
|
193
|
+
if (seen.has(history[index])) continue;
|
|
194
|
+
seen.add(history[index]);
|
|
195
|
+
unique.push(history[index]);
|
|
196
|
+
}
|
|
197
|
+
return unique.reverse();
|
|
198
|
+
}
|