buninu 0.3.0 → 0.4.0

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 (142) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +334 -37
  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/cmdlist +6 -1
  119. package/apps/jsgotty/gotty.js +65 -76
  120. package/apps/jsgotty/package.json +6 -1
  121. package/apps/jsgotty/single-exe/README.md +358 -54
  122. package/apps/jsgotty/single-exe/assetsHelper.js +293 -12
  123. package/apps/jsgotty/single-exe/assetsLoader.mjs +1 -1
  124. package/apps/jsgotty/single-exe/assetsPacker.js +218 -0
  125. package/apps/jsgotty/single-exe/compiled.js +379 -19
  126. package/apps/jsgotty/single-exe/entry.mjs +16 -2
  127. package/apps/jsgotty/zmodem-node.js +14 -12
  128. package/apps/jsmdcui/CHANGELOG.md +56 -0
  129. package/apps/jsmdcui/package.json +12 -2
  130. package/apps/jsmdcui/single-exe/README.md +298 -375
  131. package/apps/jsmdcui/single-exe/assetsHelper.js +198 -18
  132. package/apps/jsmdcui/single-exe/assetsLoader.mjs +1 -1
  133. package/apps/jsmdcui/single-exe/assetsPacker.js +218 -0
  134. package/apps/jsmdcui/single-exe/compiled.js +291 -5
  135. package/apps/jsmdcui/single-exe/entry.mjs +7 -0
  136. package/apps/jsmdcui/src/index.js +11 -8
  137. package/apps/jsmdcui/tests/jmigt.js +16 -0
  138. package/bin/bunmsh.bat +3 -0
  139. package/bin/bunmsh.sh +5 -0
  140. package/bin/init.js +48 -5
  141. package/bin/install.js +541 -0
  142. package/package.json +4 -2
@@ -0,0 +1,776 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { createInterface } from "node:readline/promises";
4
+ import {
5
+ builtinNames,
6
+ createState,
7
+ execute,
8
+ executeArgv,
9
+ executeBunShellFallback,
10
+ needsMoreInput,
11
+ pipelineChildState,
12
+ } from "./shell.js";
13
+ import {
14
+ CommandIndex,
15
+ FileIndex,
16
+ completionContext,
17
+ fitGhost,
18
+ historyGhost,
19
+ nextGhostChunk,
20
+ } from "./completion.js";
21
+ import { readAssetText } from "../single-exe/assetsHelper.js";
22
+ import { buildEarlyExit, stringifyNonPrimitiveDefineValues } from "../single-exe/compiled.js";
23
+ import { importedHistory, readlineHistory, saveBunmshHistory } from "./history.js";
24
+ import pkg from "../package.json" with { type:"json" }
25
+ import { MOUSE_OFF, MOUSE_ON, PASTE_OFF, PASTE_ON, mouseInput } from "./mouse.js";
26
+ import { homeRelativePath } from "./environment.js";
27
+
28
+ const VERSION = `
29
+ ${pkg.name}: ${pkg.description}
30
+ (mksh-inspired Bun Modern Shell)
31
+ Made by: Dr. John (醫者小智)
32
+
33
+ Version: ${pkg.version}
34
+ `
35
+
36
+ async function printReadme() {
37
+ const source = await readAssetText("README.md");
38
+ console.log(
39
+ Bun.markdown?.ansi(source, { hyperlinks: true }) || source,
40
+ );
41
+ }
42
+
43
+ async function printChangelog() {
44
+ const source = await readAssetText("CHANGELOG.md");
45
+ console.log(
46
+ Bun.markdown?.ansi(source, { hyperlinks: true }) || source,
47
+ );
48
+ }
49
+
50
+ function usage() {
51
+ console.log(`
52
+ Usage:
53
+ bunmsh [options]
54
+ bunmsh <script_path> [argv Array]
55
+ bunmsh -c <script_text> [argv Array]
56
+ bunmsh -cc <command> [argv Array]
57
+
58
+ Options:
59
+ -c <script_text> [argv Array]
60
+ Command text as shell script
61
+ → Parse & Execute
62
+
63
+ -c 'echo "$0" "$1"' Hello world
64
+ → Hello world
65
+
66
+ -cc <command> [argv Array]
67
+ Call Command with argv as is
68
+ Can be alias/builtin or from PATH
69
+ Without shell parsing
70
+ Without shell expansions
71
+
72
+ -cc echo '$HOME' '*.js'
73
+ → $HOME *.js
74
+ -cc lsfancy -lh
75
+ → List cwd with emojis
76
+
77
+ --mouse Enable mouse events
78
+ --builtin-only
79
+ Skip $PATH lookup
80
+
81
+ -i Enter interactive mode
82
+
83
+ -V, --version
84
+ Show version
85
+ -h, --help Show this help
86
+ --readme Show bunmsh's README.md
87
+ --changelog Show bunmsh's CHANGELOG.md
88
+
89
+ --build-exe
90
+ Build ./bmsh for the current platform
91
+ --build-for TARGET
92
+ Cross-compile ./bmsh for TARGET
93
+ `);
94
+ }
95
+
96
+ function renderCwd(state, cwd) {
97
+ const relative = homeRelativePath(cwd, state.env.HOME);
98
+ return relative === null ? cwd : `~${relative}`;
99
+ }
100
+
101
+ function renderPrompt(state, withRegions = false) {
102
+ const tabParts = state.tabs.length > 1
103
+ ? state.tabs.map((path, index) =>
104
+ index === state.activeTab
105
+ ? `\x1b[38;5;81m📂 ${renderCwd(state, path)}\x1b[0m`
106
+ : `📁 ${renderCwd(state, path)}`)
107
+ : null;
108
+ const cwd = tabParts ? tabParts.join(" ") : renderCwd(state, state.cwd);
109
+ const defaultPrompt = state.tabs.length > 1
110
+ ? `\\w\n[${state.activeTab + 1}]$ `
111
+ : "📁 \\w\n$ ";
112
+ const template = state.env.PS1 ?? defaultPrompt;
113
+ const styledTemplate = state.lastStatus === 0
114
+ ? template
115
+ : template.replaceAll("$", "\x1b[31m$\x1b[0m");
116
+ const text = styledTemplate.replaceAll("\\w", cwd);
117
+ if (!withRegions) return { text, regions: [], newTabRegion: [], inputStart: null };
118
+ const columns = process.stdout.columns ?? 80;
119
+ let row = 0, column = 0;
120
+ const advance = (source, cells = null) => {
121
+ for (let i = 0; i < source.length;) {
122
+ const ansi = /^\x1b\[[0-9;]*m/.exec(source.slice(i));
123
+ if (ansi) { i += ansi[0].length; continue; }
124
+ const character = String.fromCodePoint(source.codePointAt(i));
125
+ i += character.length;
126
+ if (character === "\n") { row++; column = 0; continue; }
127
+ const width = Bun.stringWidth(character);
128
+ for (let n = 0; n < width; n++) {
129
+ if (column >= columns) { row++; column = 0; }
130
+ cells?.push({ row, column });
131
+ column++;
132
+ }
133
+ }
134
+ };
135
+ let regions = [];
136
+ const newTabRegion = [];
137
+ if (tabParts) {
138
+ const prefix = styledTemplate.slice(0, styledTemplate.indexOf("\\w"));
139
+ advance(prefix);
140
+ regions = tabParts.map((part, index) => {
141
+ const cells = [];
142
+ advance(part, cells);
143
+ if (index + 1 < tabParts.length) advance(" ");
144
+ return cells;
145
+ });
146
+ const marker = `[${state.activeTab + 1}]$`;
147
+ const markerIndex = text.lastIndexOf(marker);
148
+ if (markerIndex >= 0) {
149
+ row = 0;
150
+ column = 0;
151
+ advance(text.slice(0, markerIndex + 1));
152
+ advance(String(state.activeTab + 1), newTabRegion);
153
+ }
154
+ }
155
+ // Where the editable input line begins, so a click past the prompt itself
156
+ // can be mapped back to a character offset in the line (see
157
+ // moveCursorToClick below) regardless of tab count or prompt wrapping.
158
+ row = 0;
159
+ column = 0;
160
+ advance(text);
161
+ return { text, regions, newTabRegion, inputStart: { row, column } };
162
+ }
163
+
164
+ async function interactive(state) {
165
+ const terminal = Boolean(process.stdin.isTTY && process.stdout.isTTY);
166
+ let mouseTracking = terminal && state.mouseTracking;
167
+ const commandIndex = new CommandIndex(builtinNames());
168
+ const fileIndex = new FileIndex();
169
+ state.history = await importedHistory(state.env);
170
+ const history = state.history;
171
+ // Marks everything just loaded (including whatever another session has
172
+ // already saved) as already persisted, so this session's own history
173
+ // saves only ever append commands actually typed here — never the
174
+ // imported bash/fish/bunmsh history it started from. See saveBunmshHistory.
175
+ state.historySaved = state.history.length;
176
+ if (terminal) await commandIndex.refresh(state);
177
+ let readline;
178
+ let promptRegions = [];
179
+ let newTabRegion = [];
180
+ let inputStart = null;
181
+ let pendingClick = null;
182
+ let pendingCursorQuery = null;
183
+ let lastTabClick = null;
184
+ let mouseCommandRunning = false;
185
+ let foregroundCommand = false;
186
+ let promptAbort = null;
187
+ // Set by onPaste (below) when a bracketed paste contains a newline, and
188
+ // consumed by the main loop's readline.question() catch block right after
189
+ // promptAbort.abort() rejects it -- see the comment there.
190
+ let pastedLine = null;
191
+ // A single-line paste behaves exactly like ordinary typed/pasted text
192
+ // already did: insert it at the cursor. A multi-line one (the common case
193
+ // this exists for -- pasting a whole multi-command tutorial block, some of
194
+ // whose commands span several `\`-continued physical lines) cannot be fed
195
+ // through readline a keypress at a time, since every embedded "\n" would
196
+ // be read as its own Enter and submit each physical line separately
197
+ // (breaking any `\` continuation, since the interrupted line wouldn't yet
198
+ // have the next line to join with). Instead, whatever was already typed on
199
+ // this line plus the pasted text becomes the resolved `line` value for the
200
+ // in-flight readline.question() -- exactly as if the user had typed all of
201
+ // it and pressed Enter once -- so it goes through the exact same
202
+ // pending/needsMoreInput/execute path an ordinary submitted line does.
203
+ const onPaste = (text) => {
204
+ if (!readline) return;
205
+ // A terminal's own line-ending convention for pasted content isn't
206
+ // guaranteed to be "\n" -- observed in the wild: a bare "\r" (the same
207
+ // byte Enter sends) between lines, which bunmsh's parser otherwise
208
+ // treats as whitespace, not a statement separator (see shell.js's
209
+ // isSpace). Bracketed-paste content specifically always means "these
210
+ // are separate lines", so normalize every convention to "\n" here
211
+ // before anything downstream looks at it.
212
+ const normalized = text.replace(/\r\n?/g, "\n");
213
+ if (!normalized.includes("\n")) {
214
+ readline.write(normalized);
215
+ return;
216
+ }
217
+ if (foregroundCommand || !promptAbort) return;
218
+ const line = readline.line ?? "";
219
+ const cursor = readline.cursor ?? line.length;
220
+ pastedLine = line.slice(0, cursor) + normalized + line.slice(cursor);
221
+ readline.line = "";
222
+ readline.cursor = 0;
223
+ promptAbort.abort();
224
+ };
225
+ const runTabShortcut = (args) => {
226
+ if (!readline || mouseCommandRunning) return;
227
+ mouseCommandRunning = true;
228
+ void executeArgv(["builtin", "tab", ...args], state, { capture: true }).finally(() => {
229
+ mouseCommandRunning = false;
230
+ const next = renderPrompt(state, true);
231
+ promptRegions = next.regions;
232
+ newTabRegion = next.newTabRegion;
233
+ inputStart = next.inputStart;
234
+ readline.setPrompt(next.text);
235
+ readline.prompt(true);
236
+ });
237
+ };
238
+ const runFancyShortcut = (args = []) => {
239
+ if (!readline || mouseCommandRunning) return;
240
+ mouseCommandRunning = true;
241
+ // readline.prompt(true) moves upward by the current prompt/edit buffer's
242
+ // row count before repainting. Reserve those rows below the listing so it
243
+ // clears the reserved space instead of overwriting a short lsfancy result.
244
+ const repaintRows = readline.getCursorPos().rows;
245
+ process.stdout.write("\r\x1b[0J\n");
246
+ void executeArgv(["builtin", "lsfancy", ...args], state).finally(() => {
247
+ mouseCommandRunning = false;
248
+ if (repaintRows > 0) process.stdout.write("\n".repeat(repaintRows));
249
+ const next = renderPrompt(state, true);
250
+ promptRegions = next.regions;
251
+ newTabRegion = next.newTabRegion;
252
+ inputStart = next.inputStart;
253
+ readline.setPrompt(next.text);
254
+ readline.prompt(true);
255
+ });
256
+ };
257
+ const queryCursor = () => new Promise((resolve) => {
258
+ const timer = setTimeout(() => {
259
+ if (pendingCursorQuery?.resolve === resolve) pendingCursorQuery = null;
260
+ resolve(null);
261
+ }, 60);
262
+ pendingCursorQuery = {
263
+ resolve(position) {
264
+ clearTimeout(timer);
265
+ pendingCursorQuery = null;
266
+ resolve(position);
267
+ },
268
+ };
269
+ process.stdout.write("\x1b[6n");
270
+ });
271
+ // Maps a click's cell position (relative to the prompt's own first row,
272
+ // same coordinate space as promptRegions/newTabRegion) to a character
273
+ // offset in the currently typed input line, then moves the cursor there
274
+ // by simulating that many left/right arrow presses — the only way to
275
+ // relocate readline's cursor and have it redraw correctly without reaching
276
+ // into its private internals.
277
+ const moveCursorToClick = (targetRow, targetColumn) => {
278
+ if (!readline || !inputStart) return;
279
+ const line = readline.line ?? "";
280
+ const columns = process.stdout.columns ?? 80;
281
+ let row = inputStart.row, column = inputStart.column;
282
+ let index = line.length;
283
+ for (let i = 0; i < line.length; i++) {
284
+ if (row > targetRow || (row === targetRow && column >= targetColumn)) { index = i; break; }
285
+ const width = Bun.stringWidth(line[i]);
286
+ for (let n = 0; n < width; n++) {
287
+ if (column >= columns) { row++; column = 0; }
288
+ column++;
289
+ }
290
+ }
291
+ const delta = index - readline.cursor;
292
+ const key = { name: delta > 0 ? "right" : "left" };
293
+ for (let n = 0; n < Math.abs(delta); n++) readline.write(null, key);
294
+ };
295
+ const filteredInput = terminal ? mouseInput((mouse) => {
296
+ if (!mouse.press || (mouse.button & 3) !== 0 || (mouse.button & 32)) return;
297
+ pendingClick = { ...mouse, at: Date.now() };
298
+ process.stdout.write("\x1b[6n");
299
+ }, (position) => {
300
+ if (pendingCursorQuery) {
301
+ pendingCursorQuery.resolve(position);
302
+ return;
303
+ }
304
+ const { row } = position;
305
+ if (!pendingClick || !readline) return;
306
+ const click = pendingClick;
307
+ pendingClick = null;
308
+ const cursor = readline.getCursorPos();
309
+ const relativeRow = click.y - (row - cursor.rows);
310
+ const relativeColumn = click.x - 1;
311
+ if (newTabRegion.some((cell) =>
312
+ cell.row === relativeRow && cell.column === relativeColumn)) {
313
+ lastTabClick = null;
314
+ runTabShortcut(["n"]);
315
+ return;
316
+ }
317
+ const index = promptRegions.findIndex((cells) =>
318
+ cells.some((cell) => cell.row === relativeRow && cell.column === relativeColumn));
319
+ if (index < 0) {
320
+ moveCursorToClick(relativeRow, relativeColumn);
321
+ return;
322
+ }
323
+ const doubleClick = lastTabClick?.index === index &&
324
+ click.at - lastTabClick.at <= 400;
325
+ lastTabClick = doubleClick ? null : { index, at: click.at };
326
+ if (index !== state.activeTab) {
327
+ state.activeTab = index;
328
+ state.cwd = state.tabs[index];
329
+ state.env.PWD = state.cwd;
330
+ }
331
+ const rendered = renderPrompt(state, true);
332
+ promptRegions = rendered.regions;
333
+ newTabRegion = rendered.newTabRegion;
334
+ inputStart = rendered.inputStart;
335
+ readline.setPrompt(rendered.text);
336
+ if (!doubleClick || mouseCommandRunning) {
337
+ readline.prompt(true);
338
+ return;
339
+ }
340
+ runFancyShortcut();
341
+ }, (shortcut) => {
342
+ if (shortcut === "tab") runTabShortcut([]);
343
+ else if (shortcut === "tab-left") runTabShortcut(["l"]);
344
+ else if (shortcut === "lsfancy") runFancyShortcut();
345
+ else if (shortcut === "lsfancy-parent") runFancyShortcut([".."]);
346
+ else if (shortcut === "tab-close") runTabShortcut(["x"]);
347
+ }, onPaste) : process.stdin;
348
+ const completer = (line) => {
349
+ commandIndex.refreshIfChanged(state);
350
+ const context = completionContext(line);
351
+ if (context.command) {
352
+ return [
353
+ context.prefix.includes("/")
354
+ ? fileIndex.matches(context.prefix, state)
355
+ : commandIndex.matches(context.prefix),
356
+ context.prefix,
357
+ ];
358
+ }
359
+ if (!context.prefix)
360
+ return [fileIndex.matches("", state), ""];
361
+ const fromHistory = historyGhost(history, line);
362
+ const fileMatch = fileIndex.first(context.prefix, state);
363
+ const ghost = fromHistory ?? (fileMatch ? fileMatch.slice(context.prefix.length) : null);
364
+ const chunk = nextGhostChunk(ghost);
365
+ return chunk ? [[`${line}${chunk}`], line] : [[], line];
366
+ };
367
+ let readlineResizeListeners = [];
368
+ const setReadlineResizeEnabled = (enabled) => {
369
+ for (const listener of readlineResizeListeners) {
370
+ if (enabled) {
371
+ if (!process.stdout.listeners("resize").includes(listener))
372
+ process.stdout.on("resize", listener);
373
+ } else {
374
+ process.stdout.off("resize", listener);
375
+ }
376
+ }
377
+ };
378
+ const interrupt = () => {
379
+ state.lastStatus = 130;
380
+ if (foregroundCommand || promptAbort?.signal.aborted) return;
381
+ process.stdout.write("\n");
382
+ promptAbort?.abort();
383
+ };
384
+ // Unlike Ctrl-C, SIGTERM/SIGHUP (e.g. `kill <pid>`, or some terminal
385
+ // emulators on window close) mean the process itself is being asked to
386
+ // go away, not just the current edit. There is no general job-control
387
+ // layer yet to also stop a foreground command's own process, so this
388
+ // only makes sure history isn't lost: flush it immediately if one is
389
+ // running (its own async work continues independently either way), or
390
+ // otherwise abort the pending prompt so the main loop's own exit path —
391
+ // and its usual cleanup — runs right away instead of only via the next
392
+ // periodic autosave.
393
+ const SIGNAL_EXIT_STATUS = { SIGHUP: 129, SIGTERM: 143 };
394
+ const terminate = (signal) => {
395
+ state.exitRequested = true;
396
+ state.exitStatus = SIGNAL_EXIT_STATUS[signal] ?? state.exitStatus;
397
+ if (foregroundCommand) {
398
+ void saveBunmshHistory(state, state.env).catch(() => {});
399
+ return;
400
+ }
401
+ promptAbort?.abort();
402
+ };
403
+ let closed;
404
+ // Node's readline closes itself (and cannot be reused) the moment Ctrl-D
405
+ // is pressed on an empty line — including an empty PS2 continuation line
406
+ // mid here-document. That should only end the current construct, like
407
+ // mksh, not the whole shell, so this rebuilds a fresh interface on the
408
+ // same input instead of exiting. Used at startup and again after such a
409
+ // close; it re-wires everything tied to the interface itself (the SIGINT
410
+ // handler and the resize-repaint listeners readline registers) since a new
411
+ // instance does not carry those over.
412
+ const setupReadline = () => {
413
+ const resizeListenersBeforeReadline = terminal
414
+ ? new Set(process.stdout.listeners("resize"))
415
+ : null;
416
+ readline = createInterface({
417
+ input: filteredInput,
418
+ output: process.stdout,
419
+ terminal,
420
+ completer,
421
+ history: readlineHistory(history),
422
+ historySize: Math.max(1000, history.length + 1000),
423
+ });
424
+ // readline refreshes its prompt whenever stdout emits "resize", even while
425
+ // the interface is paused. Termux emits a resize when its app returns to
426
+ // the foreground, which otherwise paints the shell's cwd prompt over a
427
+ // running server or other foreground command.
428
+ readlineResizeListeners = terminal
429
+ ? process.stdout.listeners("resize").filter(
430
+ (listener) => !resizeListenersBeforeReadline.has(listener))
431
+ : [];
432
+ readline.on("SIGINT", interrupt);
433
+ closed = new Promise((resolve) => readline.once("close", () => resolve(null)));
434
+ };
435
+ setupReadline();
436
+ process.on("SIGINT", interrupt);
437
+ process.on("SIGTERM", terminate);
438
+ process.on("SIGHUP", terminate);
439
+ // Append-only (see saveBunmshHistory), so this is cheap and safe to run
440
+ // unconditionally in the background — it only ever adds this session's
441
+ // own new commands to the end of the file, never touching anything a
442
+ // concurrent bunmsh session elsewhere has written. Failures (e.g. a full
443
+ // disk) are swallowed rather than interrupting whatever the user is
444
+ // typing; `tab s` still reports a real error if the user asks directly.
445
+ const historySaveTimer = setInterval(() => {
446
+ void saveBunmshHistory(state, state.env).catch(() => {});
447
+ }, 60_000);
448
+ historySaveTimer.unref?.();
449
+ let refreshTimer = null;
450
+ let removeGhostHooks = () => {};
451
+ if (terminal) {
452
+ process.stdin.pipe(filteredInput);
453
+ if (mouseTracking) process.stdout.write(MOUSE_ON);
454
+ process.stdout.write(PASTE_ON);
455
+ refreshTimer = setInterval(() => void commandIndex.refresh(state), 10_000);
456
+ refreshTimer.unref?.();
457
+
458
+ let ghostWidth = 0;
459
+ let startedAtEnd = false;
460
+ let renderTask = null;
461
+ const currentGhost = () => {
462
+ if (readline.cursor !== readline.line.length) return null;
463
+ const context = completionContext(readline.line, readline.cursor);
464
+ if (context.command) {
465
+ const match = context.prefix.includes("/")
466
+ ? fileIndex.first(context.prefix, state)
467
+ : commandIndex.first(context.prefix);
468
+ return match ? match.slice(context.prefix.length) : null;
469
+ }
470
+ const fromHistory = historyGhost(history, readline.line);
471
+ if (fromHistory) return fromHistory;
472
+ const match = fileIndex.first(context.prefix, state);
473
+ return match ? match.slice(context.prefix.length) : null;
474
+ };
475
+ const clearGhost = () => {
476
+ if (!ghostWidth) return;
477
+ process.stdout.write("\x1b[0K");
478
+ ghostWidth = 0;
479
+ };
480
+ const visibleGhost = (ghost) => {
481
+ const columns = process.stdout.columns ?? 80;
482
+ const cursor = readline.getCursorPos?.();
483
+ const available = Math.max(0, columns - (cursor?.cols ?? 0) - 1);
484
+ return fitGhost(ghost, available);
485
+ };
486
+ const renderGhost = () => {
487
+ renderTask = null;
488
+ clearGhost();
489
+ const ghost = currentGhost();
490
+ if (!ghost) return;
491
+ const visible = visibleGhost(ghost);
492
+ ghostWidth = visible.width;
493
+ if (!ghostWidth) return;
494
+ process.stdout.write(`\x1b[2m${visible.output}\x1b[0m`);
495
+ if (ghostWidth) process.stdout.write(`\x1b[${ghostWidth}D`);
496
+ };
497
+ const beforeKey = () => {
498
+ startedAtEnd = readline.cursor === readline.line.length;
499
+ clearGhost();
500
+ };
501
+ const afterKey = (_text, key = {}) => {
502
+ if (key.name === "right" && startedAtEnd) {
503
+ const ghost = currentGhost();
504
+ if (ghost) readline.write(ghost);
505
+ }
506
+ if (renderTask !== null) clearTimeout(renderTask);
507
+ renderTask = setTimeout(renderGhost, 0);
508
+ };
509
+ filteredInput.prependListener("keypress", beforeKey);
510
+ filteredInput.on("keypress", afterKey);
511
+ removeGhostHooks = () => {
512
+ if (renderTask !== null) clearTimeout(renderTask);
513
+ clearGhost();
514
+ filteredInput.off("keypress", beforeKey);
515
+ filteredInput.off("keypress", afterKey);
516
+ };
517
+ }
518
+ // A here-document, an open quote/substitution, or an unfinished if/while/
519
+ // for/case body all need more lines before they can run. `pending` holds
520
+ // what has been typed so far across such a continuation, and mksh's PS2
521
+ // prompt (default "> ") is shown instead of the normal prompt while it is
522
+ // non-empty.
523
+ let pending = "";
524
+ try {
525
+ while (!state.exitRequested) {
526
+ const continuing = pending !== "";
527
+ const rendered = continuing
528
+ ? { text: state.env.PS2 ?? "> ", regions: [], newTabRegion: [],
529
+ inputStart: { row: 0, column: Bun.stringWidth(state.env.PS2 ?? "> ") } }
530
+ : renderPrompt(state, true);
531
+ promptRegions = rendered.regions;
532
+ newTabRegion = rendered.newTabRegion;
533
+ inputStart = rendered.inputStart;
534
+ readline.setPrompt(rendered.text);
535
+ promptAbort = new AbortController();
536
+ let line;
537
+ try {
538
+ line = await Promise.race([
539
+ readline.question(rendered.text, { signal: promptAbort.signal }),
540
+ closed,
541
+ ]);
542
+ } catch (error) {
543
+ if (error?.name !== "AbortError") throw error;
544
+ // onPaste aborts the same way Ctrl-C's interrupt() does, but leaves
545
+ // pastedLine set first. Falling through to the normal resolution
546
+ // path below with that as `line` runs it through the exact same
547
+ // pending/needsMoreInput/execute handling a typed-and-Entered line
548
+ // gets -- Ctrl-C itself never sets pastedLine, so that case is
549
+ // unaffected: pending is cleared and the loop just reprompts.
550
+ if (pastedLine === null) { pending = ""; continue; }
551
+ line = pastedLine;
552
+ pastedLine = null;
553
+ } finally {
554
+ promptAbort = null;
555
+ }
556
+ if (line === null && !continuing) break;
557
+ // Ctrl-D on an empty line closes readline itself, whether that empty
558
+ // line was the primary prompt (handled above) or a PS2 continuation
559
+ // line. In the latter case the interface is now gone, but the
560
+ // terminal is still there, so this is recoverable: run whatever was
561
+ // typed so far — mksh does the same for a here-document ended this
562
+ // way — and rebuild a fresh interface afterward instead of exiting.
563
+ const eof = line === null;
564
+ if (!eof) {
565
+ // Stdin is still open: fold the new line in and, if the command is
566
+ // still unterminated, loop back for another one under the PS2
567
+ // prompt instead of running it.
568
+ pending = continuing ? `${pending}\n${line}` : line;
569
+ if (needsMoreInput(pending)) continue;
570
+ }
571
+ line = pending;
572
+ pending = "";
573
+ if (line && history.at(-1) !== line) history.push(line);
574
+ // readline must not keep reading from the terminal while a foreground
575
+ // program owns it. Otherwise both processes race for input and the
576
+ // first key (notably Ctrl-Q in full-screen editors) can be consumed by
577
+ // the shell. Also give the child a sane terminal mode to start from.
578
+ // A closed-by-EOF readline can't be paused (or later resumed).
579
+ if (!eof) {
580
+ readline.pause();
581
+ setReadlineResizeEnabled(false);
582
+ }
583
+ if (terminal) {
584
+ if (mouseTracking) process.stdout.write(MOUSE_OFF);
585
+ process.stdout.write(PASTE_OFF);
586
+ process.stdin.unpipe(filteredInput);
587
+ }
588
+ if (terminal) process.stdin.setRawMode(false);
589
+ foregroundCommand = true;
590
+ let endSession = false;
591
+ try {
592
+ if (line) await execute(line, state);
593
+ } finally {
594
+ foregroundCommand = false;
595
+ if (!state.exitRequested) {
596
+ if (terminal) process.stdin.setRawMode(true);
597
+ if (terminal) {
598
+ process.stdin.pipe(filteredInput);
599
+ // External commands stream directly to the terminal, so their
600
+ // final byte cannot be inferred from an execution result. Ask the
601
+ // terminal where the cursor is and terminate an unfinished line
602
+ // before readline paints a multi-line prompt over it.
603
+ const cursor = await queryCursor();
604
+ if (cursor && cursor.column > 1) process.stdout.write("↩️\n");
605
+ mouseTracking = Boolean(state.mouseTracking);
606
+ if (mouseTracking) process.stdout.write(MOUSE_ON);
607
+ process.stdout.write(PASTE_ON);
608
+ }
609
+ if (eof) {
610
+ if (terminal) setupReadline();
611
+ else endSession = true;
612
+ } else {
613
+ setReadlineResizeEnabled(true);
614
+ readline.resume();
615
+ }
616
+ } else if (eof) {
617
+ endSession = true;
618
+ }
619
+ }
620
+ if (endSession) break;
621
+ }
622
+ } finally {
623
+ if (terminal) {
624
+ if (mouseTracking) process.stdout.write(MOUSE_OFF);
625
+ process.stdout.write(PASTE_OFF);
626
+ process.stdin.unpipe(filteredInput);
627
+ }
628
+ clearInterval(historySaveTimer);
629
+ // Catches whatever the last autosave (up to 60s ago) missed, so a normal
630
+ // exit never loses the tail of the session's history.
631
+ await saveBunmshHistory(state, state.env).catch(() => {});
632
+ if (refreshTimer !== null) clearInterval(refreshTimer);
633
+ removeGhostHooks();
634
+ readline.off("SIGINT", interrupt);
635
+ process.off("SIGINT", interrupt);
636
+ process.off("SIGTERM", terminate);
637
+ process.off("SIGHUP", terminate);
638
+ readline.close();
639
+ }
640
+ return state.exitRequested ? state.exitStatus : state.lastStatus;
641
+ }
642
+
643
+ async function main(argv) {
644
+ if (argv[0] === "--bun-shell-fallback")
645
+ return executeBunShellFallback(argv.slice(1));
646
+ let command = null;
647
+ let forwardedArgv = null;
648
+ let forceInteractive = false;
649
+ let forceMouse = false;
650
+ let builtinOnly = false;
651
+ let i = 0;
652
+ for (; i < argv.length; i++) {
653
+ const arg = argv[i];
654
+ if (arg === "--") {
655
+ i++;
656
+ break;
657
+ }
658
+ if (arg === "-c") {
659
+ if (i + 1 >= argv.length) {
660
+ console.error("bunmsh: -c requires an argument");
661
+ return 2;
662
+ }
663
+ command = argv[++i];
664
+ i++;
665
+ break;
666
+ }
667
+ if (arg === "-cc") {
668
+ forwardedArgv = argv.slice(i + 1);
669
+ i = argv.length;
670
+ break;
671
+ }
672
+ if (arg === "-i") {
673
+ forceInteractive = true;
674
+ continue;
675
+ }
676
+ if (arg === "--mouse") {
677
+ forceMouse = true;
678
+ continue;
679
+ }
680
+ if (arg === "--builtin-only") {
681
+ builtinOnly = true;
682
+ continue;
683
+ }
684
+ if (arg === "-h" || arg === "--help") {
685
+ usage();
686
+ return 0;
687
+ }
688
+ if (arg === "-V" || arg === "--version") {
689
+ console.log(VERSION);
690
+ return 0;
691
+ }
692
+ if (arg === "--readme") {
693
+ await printReadme();
694
+ return 0;
695
+ }
696
+ if (arg === "--changelog") {
697
+ await printChangelog();
698
+ return 0;
699
+ }
700
+ if (arg.startsWith("-")) {
701
+ console.error(`bunmsh: ${arg}: unknown option`);
702
+ return 2;
703
+ }
704
+ break;
705
+ }
706
+
707
+ if (forwardedArgv !== null) {
708
+ if (forwardedArgv.length === 0) {
709
+ console.error("bunmsh: -cc requires a command");
710
+ return 2;
711
+ }
712
+ const state = pipelineChildState();
713
+ if (forceMouse) state.mouseTracking = true;
714
+ if (builtinOnly) state.pathSearch = false;
715
+ const result = await executeArgv(forwardedArgv, state);
716
+ return state.exitRequested ? state.exitStatus : result.status;
717
+ }
718
+
719
+ if (command !== null) {
720
+ const name = argv[i] ?? "bunmsh";
721
+ const state = createState({
722
+ args: [name, ...argv.slice(i + 1)],
723
+ mouseTracking: forceMouse || undefined,
724
+ pathSearch: builtinOnly ? false : undefined,
725
+ });
726
+ const result = await execute(command, state);
727
+ return state.exitRequested ? state.exitStatus : result.status;
728
+ }
729
+
730
+ if (i < argv.length) {
731
+ const script = argv[i];
732
+ try {
733
+ const source = await Bun.file(script).text();
734
+ const state = createState({
735
+ args: [script, ...argv.slice(i + 1)],
736
+ mouseTracking: forceMouse || undefined,
737
+ pathSearch: builtinOnly ? false : undefined,
738
+ });
739
+ const result = await execute(source, state);
740
+ if (forceInteractive && !state.exitRequested) return interactive(state);
741
+ return state.exitRequested ? state.exitStatus : result.status;
742
+ } catch (error) {
743
+ console.error(`bunmsh: ${script}: ${error.message}`);
744
+ return 1;
745
+ }
746
+ }
747
+
748
+ const state = createState({
749
+ mouseTracking: forceMouse || undefined,
750
+ pathSearch: builtinOnly ? false : undefined,
751
+ });
752
+ if (forceInteractive || process.stdin.isTTY) return interactive(state);
753
+ const source = await Bun.stdin.text();
754
+ const result = await execute(source, state);
755
+ return state.exitRequested ? state.exitStatus : result.status;
756
+ }
757
+
758
+ // Lets --define process.env.SERVE_AUTO_OPEN=/index.html, =off, or =yes
759
+ // arrive unquoted from a shell: string-shaped values that aren't already a
760
+ // valid bare literal (number/boolean/null/undefined, e.g. =1) get quoted
761
+ // here, so `bun ./src/main.js --build-exe --define process.env.SERVE_AUTO_OPEN=/index.html`
762
+ // works without the caller hand-quoting the value themselves.
763
+ stringifyNonPrimitiveDefineValues(process.argv, "process.env.SERVE_AUTO_OPEN");
764
+ stringifyNonPrimitiveDefineValues(process.argv, "process.env.SERVE_MINAPK_WEBVIEW");
765
+ stringifyNonPrimitiveDefineValues(process.argv, "process.env.SERVE_RANDOM_URL");
766
+ await buildEarlyExit(process.argv, "bmsh");
767
+ process.exitCode = await main(process.argv.slice(2));
768
+ // By this point every await in main() (including interactive()'s own
769
+ // cleanup and every writeStream() call for command output) has already
770
+ // finished, so there is no legitimate reason left for the process to stay
771
+ // alive. Force the exit rather than letting the event loop decide: a killed
772
+ // pipeline stage (see runExternal's stopUpstream handling) can leave behind
773
+ // a stream operation Bun never lets finish or be canceled, which otherwise
774
+ // hangs the shell forever even though the pipeline itself already produced
775
+ // its result correctly.
776
+ process.exit(process.exitCode);