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.
Files changed (122) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +100 -2
  3. package/apps/bunmsh/CHANGELOG.md +262 -0
  4. package/apps/bunmsh/LICENSE +21 -0
  5. package/apps/bunmsh/LICENSE-MKSH +295 -0
  6. package/apps/bunmsh/PORTING.md +192 -0
  7. package/apps/bunmsh/README.md +801 -0
  8. package/apps/bunmsh/bunmsh +7 -0
  9. package/apps/bunmsh/bunshell.md +61 -0
  10. package/apps/bunmsh/help/README.md +1945 -0
  11. package/apps/bunmsh/help/alias.md +22 -0
  12. package/apps/bunmsh/help/basename.md +38 -0
  13. package/apps/bunmsh/help/builtin.md +25 -0
  14. package/apps/bunmsh/help/bun.md +27 -0
  15. package/apps/bunmsh/help/bunmsh.md +47 -0
  16. package/apps/bunmsh/help/cat.md +31 -0
  17. package/apps/bunmsh/help/catfancy.md +35 -0
  18. package/apps/bunmsh/help/cd.md +22 -0
  19. package/apps/bunmsh/help/chmod.md +27 -0
  20. package/apps/bunmsh/help/clear.md +21 -0
  21. package/apps/bunmsh/help/colon.md +22 -0
  22. package/apps/bunmsh/help/command.md +27 -0
  23. package/apps/bunmsh/help/cp.md +28 -0
  24. package/apps/bunmsh/help/cut.md +25 -0
  25. package/apps/bunmsh/help/date.md +25 -0
  26. package/apps/bunmsh/help/dirname.md +25 -0
  27. package/apps/bunmsh/help/echo.md +25 -0
  28. package/apps/bunmsh/help/env.md +27 -0
  29. package/apps/bunmsh/help/eval.md +22 -0
  30. package/apps/bunmsh/help/exec.md +22 -0
  31. package/apps/bunmsh/help/exit.md +21 -0
  32. package/apps/bunmsh/help/export.md +22 -0
  33. package/apps/bunmsh/help/false.md +22 -0
  34. package/apps/bunmsh/help/find.md +33 -0
  35. package/apps/bunmsh/help/getopts.md +28 -0
  36. package/apps/bunmsh/help/grep.md +34 -0
  37. package/apps/bunmsh/help/head.md +27 -0
  38. package/apps/bunmsh/help/kill.md +27 -0
  39. package/apps/bunmsh/help/ln.md +28 -0
  40. package/apps/bunmsh/help/ls.md +37 -0
  41. package/apps/bunmsh/help/lsbun.md +29 -0
  42. package/apps/bunmsh/help/lsfancy.md +37 -0
  43. package/apps/bunmsh/help/md5sum.md +21 -0
  44. package/apps/bunmsh/help/mkdir.md +27 -0
  45. package/apps/bunmsh/help/mktemp.md +26 -0
  46. package/apps/bunmsh/help/mv.md +26 -0
  47. package/apps/bunmsh/help/print.md +30 -0
  48. package/apps/bunmsh/help/printf.md +25 -0
  49. package/apps/bunmsh/help/pwd.md +21 -0
  50. package/apps/bunmsh/help/read.md +27 -0
  51. package/apps/bunmsh/help/readonly.md +27 -0
  52. package/apps/bunmsh/help/realpath.md +21 -0
  53. package/apps/bunmsh/help/rm.md +33 -0
  54. package/apps/bunmsh/help/rmdir.md +27 -0
  55. package/apps/bunmsh/help/sed.md +29 -0
  56. package/apps/bunmsh/help/seq.md +29 -0
  57. package/apps/bunmsh/help/serve.md +50 -0
  58. package/apps/bunmsh/help/set.md +26 -0
  59. package/apps/bunmsh/help/sha256sum.md +21 -0
  60. package/apps/bunmsh/help/shift.md +23 -0
  61. package/apps/bunmsh/help/sleep.md +25 -0
  62. package/apps/bunmsh/help/sort.md +29 -0
  63. package/apps/bunmsh/help/source.md +23 -0
  64. package/apps/bunmsh/help/tab.md +30 -0
  65. package/apps/bunmsh/help/tac.md +29 -0
  66. package/apps/bunmsh/help/tail.md +27 -0
  67. package/apps/bunmsh/help/tee.md +25 -0
  68. package/apps/bunmsh/help/test.md +30 -0
  69. package/apps/bunmsh/help/time.md +29 -0
  70. package/apps/bunmsh/help/touch.md +26 -0
  71. package/apps/bunmsh/help/tr.md +26 -0
  72. package/apps/bunmsh/help/true.md +22 -0
  73. package/apps/bunmsh/help/type.md +21 -0
  74. package/apps/bunmsh/help/umask.md +22 -0
  75. package/apps/bunmsh/help/unalias.md +28 -0
  76. package/apps/bunmsh/help/uname.md +31 -0
  77. package/apps/bunmsh/help/unset.md +28 -0
  78. package/apps/bunmsh/help/wc.md +27 -0
  79. package/apps/bunmsh/help/whence.md +26 -0
  80. package/apps/bunmsh/help/which.md +41 -0
  81. package/apps/bunmsh/help/yes.md +23 -0
  82. package/apps/bunmsh/package.json +26 -0
  83. package/apps/bunmsh/serve.js +468 -0
  84. package/apps/bunmsh/single-exe/README.md +402 -0
  85. package/apps/bunmsh/single-exe/assetsHelper.js +371 -0
  86. package/apps/bunmsh/single-exe/assetsLoader.mjs +85 -0
  87. package/apps/bunmsh/single-exe/assetsPacker.js +218 -0
  88. package/apps/bunmsh/single-exe/compiled.js +527 -0
  89. package/apps/bunmsh/single-exe/entry.mjs +17 -0
  90. package/apps/bunmsh/src/cat-fancy.js +117 -0
  91. package/apps/bunmsh/src/cat-operands.js +31 -0
  92. package/apps/bunmsh/src/completion.js +247 -0
  93. package/apps/bunmsh/src/environment.js +61 -0
  94. package/apps/bunmsh/src/fancy-ls.js +186 -0
  95. package/apps/bunmsh/src/find.js +119 -0
  96. package/apps/bunmsh/src/history.js +198 -0
  97. package/apps/bunmsh/src/main.js +776 -0
  98. package/apps/bunmsh/src/mouse.js +103 -0
  99. package/apps/bunmsh/src/shell.js +3557 -0
  100. package/apps/bunmsh/test/catfancy.test.js +102 -0
  101. package/apps/bunmsh/test/reference-source.sh +1 -0
  102. package/apps/bunmsh/test/reference.test.js +677 -0
  103. package/apps/bunmsh/test/serve/README.md +3 -0
  104. package/apps/bunmsh/test/serve/bad.json +1 -0
  105. package/apps/bunmsh/test/serve/data.json +1 -0
  106. package/apps/bunmsh/test/serve/data.json5 +1 -0
  107. package/apps/bunmsh/test/serve/data.jsonc +2 -0
  108. package/apps/bunmsh/test/serve/data.jsonl +2 -0
  109. package/apps/bunmsh/test/serve/data.ndjson +1 -0
  110. package/apps/bunmsh/test/serve/data.toml +3 -0
  111. package/apps/bunmsh/test/serve/data.ts +15 -0
  112. package/apps/bunmsh/test/serve/data.xml +1 -0
  113. package/apps/bunmsh/test/serve/data.yaml +3 -0
  114. package/apps/bunmsh/test/serve/data.yml +1 -0
  115. package/apps/bunmsh/test/serve.test.js +241 -0
  116. package/apps/bunmsh/test/shell.test.js +1778 -0
  117. package/apps/bunmsh/test/t.sh +12 -0
  118. package/apps/native-bridge/native-bridge.js +45 -0
  119. package/apps/xdg-open/xdg-open.js +74 -2
  120. package/bin/init.js +11 -3
  121. package/demo-server.js +64 -0
  122. 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
+ }