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.
- package/CHANGELOG.md +71 -0
- package/README.md +334 -37
- 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/cmdlist +6 -1
- package/apps/jsgotty/gotty.js +65 -76
- package/apps/jsgotty/package.json +6 -1
- package/apps/jsgotty/single-exe/README.md +358 -54
- package/apps/jsgotty/single-exe/assetsHelper.js +293 -12
- package/apps/jsgotty/single-exe/assetsLoader.mjs +1 -1
- package/apps/jsgotty/single-exe/assetsPacker.js +218 -0
- package/apps/jsgotty/single-exe/compiled.js +379 -19
- package/apps/jsgotty/single-exe/entry.mjs +16 -2
- package/apps/jsgotty/zmodem-node.js +14 -12
- package/apps/jsmdcui/CHANGELOG.md +56 -0
- package/apps/jsmdcui/package.json +12 -2
- package/apps/jsmdcui/single-exe/README.md +298 -375
- package/apps/jsmdcui/single-exe/assetsHelper.js +198 -18
- package/apps/jsmdcui/single-exe/assetsLoader.mjs +1 -1
- package/apps/jsmdcui/single-exe/assetsPacker.js +218 -0
- package/apps/jsmdcui/single-exe/compiled.js +291 -5
- package/apps/jsmdcui/single-exe/entry.mjs +7 -0
- package/apps/jsmdcui/src/index.js +11 -8
- package/apps/jsmdcui/tests/jmigt.js +16 -0
- package/bin/bunmsh.bat +3 -0
- package/bin/bunmsh.sh +5 -0
- package/bin/init.js +48 -5
- package/bin/install.js +541 -0
- package/package.json +4 -2
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { lstatSync, readdirSync } from "node:fs";
|
|
4
|
+
import { basename, isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
6
|
+
import { iconFor } from "./src/fancy-ls.js";
|
|
7
|
+
|
|
8
|
+
let root = process.cwd();
|
|
9
|
+
let randomRoute = "";
|
|
10
|
+
const publicUrls = new WeakMap();
|
|
11
|
+
|
|
12
|
+
const ENV_FLAGS = {
|
|
13
|
+
autoOpen: process.env.SERVE_AUTO_OPEN,
|
|
14
|
+
minapkWebview: process.env.SERVE_MINAPK_WEBVIEW,
|
|
15
|
+
randomUrl: process.env.SERVE_RANDOM_URL,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const OPTION_NAMES = {
|
|
19
|
+
"--auto-open": "autoOpen",
|
|
20
|
+
"--minapk-webview": "minapkWebview",
|
|
21
|
+
"--random-url": "randomUrl",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const envEnabled = (value) => /^(?:1|true|yes|on)$/i.test(String(value ?? "").trim());
|
|
25
|
+
// A bare CLI flag always means "on"; `=off`/`=no`/`=false`/`=` always means an
|
|
26
|
+
// explicit "off" that overrides an environment default turned on inline.
|
|
27
|
+
const isOffValue = (value) => value === "" || /^(?:off|no|false)$/i.test(value);
|
|
28
|
+
|
|
29
|
+
// SERVE_MINAPK_WEBVIEW can hold a boolean-ish word (on/off/...) or a literal
|
|
30
|
+
// number string to pass through as MINAPK_WEBVIEW's default value.
|
|
31
|
+
function minapkWebviewEnvDefault(value) {
|
|
32
|
+
const trimmed = String(value ?? "").trim();
|
|
33
|
+
if (isOffValue(trimmed)) return null;
|
|
34
|
+
if (/^\d+$/.test(trimmed)) return trimmed;
|
|
35
|
+
return envEnabled(trimmed) ? "1" : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// autoOpen is `false` (off), `true` (on, open the server root), or a string
|
|
39
|
+
// starting with "/" -- a path opened relative to the served URL (after any
|
|
40
|
+
// --random-url prefix) instead of the root. SERVE_AUTO_OPEN can set any of
|
|
41
|
+
// the three as the default the same way --auto-open's value does.
|
|
42
|
+
function autoOpenEnvDefault(value) {
|
|
43
|
+
const trimmed = String(value ?? "").trim();
|
|
44
|
+
return trimmed.startsWith("/") ? trimmed : envEnabled(trimmed);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parseServeArguments(args, env = process.env, cwd = process.cwd()) {
|
|
48
|
+
const options = {
|
|
49
|
+
autoOpen: autoOpenEnvDefault(ENV_FLAGS.autoOpen),
|
|
50
|
+
minapkWebview: minapkWebviewEnvDefault(ENV_FLAGS.minapkWebview),
|
|
51
|
+
randomUrl: envEnabled(ENV_FLAGS.randomUrl),
|
|
52
|
+
};
|
|
53
|
+
const operands = [];
|
|
54
|
+
let parseOptions = true;
|
|
55
|
+
for (const argument of args) {
|
|
56
|
+
if (parseOptions && argument === "--") {
|
|
57
|
+
parseOptions = false;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (parseOptions && argument.startsWith("--")) {
|
|
61
|
+
const equals = argument.indexOf("=");
|
|
62
|
+
const flag = equals === -1 ? argument : argument.slice(0, equals);
|
|
63
|
+
const value = equals === -1 ? undefined : argument.slice(equals + 1);
|
|
64
|
+
const name = OPTION_NAMES[flag];
|
|
65
|
+
if (!name) return { error: `unknown option: ${argument}` };
|
|
66
|
+
if (name === "minapkWebview") {
|
|
67
|
+
if (value === undefined) options.minapkWebview = "1";
|
|
68
|
+
else if (isOffValue(value)) options.minapkWebview = null;
|
|
69
|
+
else if (/^\d+$/.test(value)) options.minapkWebview = value;
|
|
70
|
+
else return { error: `invalid value for --minapk-webview: ${value}` };
|
|
71
|
+
} else if (name === "autoOpen") {
|
|
72
|
+
if (value === undefined) options.autoOpen = true;
|
|
73
|
+
else if (isOffValue(value)) options.autoOpen = false;
|
|
74
|
+
else if (value.startsWith("/")) options.autoOpen = value;
|
|
75
|
+
else options.autoOpen = true;
|
|
76
|
+
} else {
|
|
77
|
+
options[name] = value === undefined || !isOffValue(value);
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
operands.push(argument);
|
|
82
|
+
}
|
|
83
|
+
if (operands.length > 1) return { error: "too many directory operands" };
|
|
84
|
+
return { directory: operands[0] ?? cwd, ...options, env };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Each UUIDv7 is 128 bits: a 48-bit ms timestamp (not secret), a 4-bit
|
|
88
|
+
// version + 2-bit variant (fixed), a 12-bit rand_a, and a 62-bit rand_b.
|
|
89
|
+
// Confirmed against Bun's own source (bun/src/jsc/uuid.rs UUID7::next):
|
|
90
|
+
// rand_a is a process-global monotonic counter seeded fresh only on the
|
|
91
|
+
// first call to observe a new millisecond -- every later call in that same
|
|
92
|
+
// millisecond just gets seed+1, seed+2, ..., not a fresh random draw. Four
|
|
93
|
+
// calls made back-to-back like this land in the same millisecond, so their
|
|
94
|
+
// timestamp and rand_a sequence are effectively one shared, low-entropy
|
|
95
|
+
// value. Only rand_b is unconditionally re-drawn from BoringSSL's CSPRNG
|
|
96
|
+
// (bun/src/jsc/rare_data.rs EntropyCache, backed by bun_boringssl::rand_bytes)
|
|
97
|
+
// on every single call, so the four independent 62-bit rand_b fields are
|
|
98
|
+
// the actual guaranteed entropy floor: >= 248 bits, not 4 * 128.
|
|
99
|
+
export const randomServeRoute = () =>
|
|
100
|
+
`${Array.from({ length: 4 }, () => Bun.randomUUIDv7("base64url")).join("")}/`;
|
|
101
|
+
|
|
102
|
+
export const serveUrl = (server) => publicUrls.get(server) ?? server.url;
|
|
103
|
+
|
|
104
|
+
const escapeHtml = (value) => Bun.escapeHTML(value);
|
|
105
|
+
|
|
106
|
+
const PARSERS = new Map([
|
|
107
|
+
["json", { name: "json.parse", parse: JSON.parse }],
|
|
108
|
+
["json5", { name: "bun.json5.parse", parse: Bun.JSON5.parse }],
|
|
109
|
+
["jsonc", { name: "bun.jsonc.parse", parse: Bun.JSONC.parse }],
|
|
110
|
+
["jsonl", { name: "bun.jsonl.parse", parse: Bun.JSONL.parse }],
|
|
111
|
+
["ndjson", { name: "bun.jsonl.parse", parse: Bun.JSONL.parse }],
|
|
112
|
+
["yaml", { name: "bun.yaml.parse", parse: Bun.YAML.parse }],
|
|
113
|
+
["yml", { name: "bun.yaml.parse", parse: Bun.YAML.parse }],
|
|
114
|
+
["toml", { name: "bun.toml.parse", parse: Bun.TOML.parse }],
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
if (typeof Bun.XML?.parse === "function") {
|
|
118
|
+
PARSERS.set("xml", { name: "bun.xml.parse", parse: Bun.XML.parse });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const extension = (name) => {
|
|
122
|
+
const dot = name.lastIndexOf(".");
|
|
123
|
+
return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const previewName = (name) => extension(name) === "md"
|
|
127
|
+
? "bun.markdown.html"
|
|
128
|
+
: PARSERS.get(extension(name))?.name;
|
|
129
|
+
|
|
130
|
+
const page = (title, body) => `<!doctype html>
|
|
131
|
+
<meta charset="utf-8">
|
|
132
|
+
<meta name="viewport" content="width=device-width">
|
|
133
|
+
<title>${escapeHtml(title)}</title>
|
|
134
|
+
<style>
|
|
135
|
+
:root { color-scheme: light dark }
|
|
136
|
+
body { max-width: 72rem; margin: 2rem auto; padding: 0 1rem; font: 16px/1.7 ui-monospace, monospace }
|
|
137
|
+
a { color: inherit; text-decoration: none }
|
|
138
|
+
a:hover { text-decoration: underline }
|
|
139
|
+
pre { padding: 1rem; overflow: auto; white-space: pre-wrap; background: color-mix(in srgb, CanvasText 7%, Canvas) }
|
|
140
|
+
code { font: inherit }
|
|
141
|
+
.json-statement { color: #b0005a }
|
|
142
|
+
.json-string { color: #6b5d00 }
|
|
143
|
+
.json-special { color: #087f5b }
|
|
144
|
+
.json-number, .json-constant { color: #5f3dc4 }
|
|
145
|
+
@media (prefers-color-scheme: dark) {
|
|
146
|
+
.json-statement { color: #f92672 }
|
|
147
|
+
.json-string { color: #e6db74 }
|
|
148
|
+
.json-special { color: #a6e22e }
|
|
149
|
+
.json-number, .json-constant { color: #ae81ff }
|
|
150
|
+
}
|
|
151
|
+
</style>
|
|
152
|
+
${body}`;
|
|
153
|
+
|
|
154
|
+
const pretty = (value) => {
|
|
155
|
+
try {
|
|
156
|
+
return JSON.stringify(value, (_key, item) =>
|
|
157
|
+
typeof item === "bigint" ? `${item}n` : item, 2) ?? String(value);
|
|
158
|
+
} catch {
|
|
159
|
+
return Bun.inspect(value, { colors: false, depth: Infinity });
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Mirrors the useful groups in jsmdcui/runtime/syntax/json.yaml for the
|
|
164
|
+
// strict JSON emitted by pretty(): statement keys, strings and escapes,
|
|
165
|
+
// numbers, and true/false/null constants.
|
|
166
|
+
export const highlightJson = (source) => {
|
|
167
|
+
const input = String(source);
|
|
168
|
+
const token = /"(?:\\(?:u[\da-fA-F]{4}|.)|[^"\\])*"|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|\b(?:true|false|null)\b/g;
|
|
169
|
+
|
|
170
|
+
//" fixes micro syntax highlighting
|
|
171
|
+
|
|
172
|
+
let output = "", cursor = 0, match;
|
|
173
|
+
const stringHtml = (value) => {
|
|
174
|
+
let html = "", offset = 0;
|
|
175
|
+
for (const escape of value.matchAll(/\\(?:u[\da-fA-F]{4}|.)/g)) {
|
|
176
|
+
html += escapeHtml(value.slice(offset, escape.index));
|
|
177
|
+
html += `<span class="json-special">${escapeHtml(escape[0])}</span>`;
|
|
178
|
+
offset = escape.index + escape[0].length;
|
|
179
|
+
}
|
|
180
|
+
return html + escapeHtml(value.slice(offset));
|
|
181
|
+
};
|
|
182
|
+
while ((match = token.exec(input))) {
|
|
183
|
+
output += escapeHtml(input.slice(cursor, match.index));
|
|
184
|
+
const value = match[0];
|
|
185
|
+
let end = token.lastIndex;
|
|
186
|
+
if (value.startsWith('"')) {
|
|
187
|
+
const propertyEnd = /^\s*:/.exec(input.slice(end));
|
|
188
|
+
if (propertyEnd) {
|
|
189
|
+
end += propertyEnd[0].length;
|
|
190
|
+
output += `<span class="json-statement">${escapeHtml(input.slice(match.index, end))}</span>`;
|
|
191
|
+
} else {
|
|
192
|
+
output += `<span class="json-string">${stringHtml(value)}</span>`;
|
|
193
|
+
}
|
|
194
|
+
} else if (value === "true" || value === "false" || value === "null") {
|
|
195
|
+
output += `<span class="json-constant">${value}</span>`;
|
|
196
|
+
} else {
|
|
197
|
+
output += `<span class="json-number">${value}</span>`;
|
|
198
|
+
}
|
|
199
|
+
cursor = end;
|
|
200
|
+
token.lastIndex = end;
|
|
201
|
+
}
|
|
202
|
+
return output + escapeHtml(input.slice(cursor));
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const previewResponse = async (entry) => {
|
|
206
|
+
|
|
207
|
+
const renderHTMLOptions = {
|
|
208
|
+
headings: true
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const kind = extension(entry.name);
|
|
212
|
+
try {
|
|
213
|
+
const source = await Bun.file(entry.path).text();
|
|
214
|
+
if (kind === "md") return new Response(page(entry.name,
|
|
215
|
+
`<p><a href="${encodeURIComponent(entry.name)}">← ${escapeHtml(entry.name)}</a></p>\n${Bun.markdown.html(source,renderHTMLOptions)}`), {
|
|
216
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
217
|
+
});
|
|
218
|
+
const parser = PARSERS.get(kind);
|
|
219
|
+
const output = pretty(parser.parse(source));
|
|
220
|
+
return new Response(page(`${entry.name} — ${parser.name}`,
|
|
221
|
+
`<p><a href="${encodeURIComponent(entry.name)}">← ${escapeHtml(entry.name)}</a></p>\n<h1>${escapeHtml(entry.name)}</h1>\n<pre><code>${highlightJson(output)}</code></pre>`), {
|
|
222
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
223
|
+
});
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return new Response(page(`Cannot preview ${entry.name}`,
|
|
226
|
+
`<h1>Cannot preview ${escapeHtml(entry.name)}</h1>\n<pre>${escapeHtml(error?.stack ?? String(error))}</pre>`), {
|
|
227
|
+
status: 422,
|
|
228
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const directoryHtml = (route, entries) => {
|
|
234
|
+
const parent = route === "/" ? "" : `<a href="../">📦 ../</a>\n`;
|
|
235
|
+
const links = entries
|
|
236
|
+
.filter((entry) => !entry.name.startsWith("."))
|
|
237
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
238
|
+
.map((entry) => {
|
|
239
|
+
const directory = entry.stats.isDirectory();
|
|
240
|
+
const name = `${entry.name}${directory ? "/" : ""}`;
|
|
241
|
+
const encoded = encodeURIComponent(entry.name);
|
|
242
|
+
const preview = !directory && previewName(entry.name);
|
|
243
|
+
return `<a href="${encoded}${directory ? "/" : ""}">${iconFor(entry.name, entry.stats)} ${escapeHtml(name)}</a>${preview ? ` <a href="${encoded}-${preview}" title="Preview with ${preview}">🔍</a>` : ""}`;
|
|
244
|
+
})
|
|
245
|
+
.join("\n");
|
|
246
|
+
const title = `Index of ${route}`;
|
|
247
|
+
return page(title, `<h1>${escapeHtml(title)}</h1>\n<pre>${parent}${links}${links ? "\n" : ""}</pre>`);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const resolveRequestPath = (pathname) => {
|
|
251
|
+
let decoded;
|
|
252
|
+
try {
|
|
253
|
+
decoded = decodeURIComponent(pathname);
|
|
254
|
+
} catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
if (decoded.includes("\0")) return null;
|
|
258
|
+
const path = resolve(root, `.${decoded.replaceAll("\\", "/")}`);
|
|
259
|
+
const fromRoot = relative(root, path);
|
|
260
|
+
if (fromRoot === ".." || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot))
|
|
261
|
+
return null;
|
|
262
|
+
return { path, decoded };
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const previewRequest = (pathname) => {
|
|
266
|
+
const names = ["bun.markdown.html", ...new Set([...PARSERS.values()].map(({ name }) => name))];
|
|
267
|
+
for (const preview of names) {
|
|
268
|
+
const suffix = `-${preview}`;
|
|
269
|
+
if (!pathname.endsWith(suffix)) continue;
|
|
270
|
+
const original = pathname.slice(0, -suffix.length);
|
|
271
|
+
if (previewName(basename(original)) === preview) return original;
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const notFound = () => new Response("Not Found", { status: 404 });
|
|
277
|
+
|
|
278
|
+
export const resolveBunfsPath = (directory, moduleDirectory = import.meta.dirname) => {
|
|
279
|
+
const requested = String(directory).replaceAll("\\", "/");
|
|
280
|
+
const moduleDir = String(moduleDirectory).replaceAll("\\", "/").replace(/\/$/, "");
|
|
281
|
+
const mount = /^(\/\$bunfs|[a-z]:\/~bun)(?:\/|$)/i.exec(moduleDir)?.[1];
|
|
282
|
+
if (!mount) return null;
|
|
283
|
+
const alias = /^(\/\$bunfs|[a-z]:\/~bun)(?=\/|$)/i.exec(requested)?.[1];
|
|
284
|
+
if (!alias) return null;
|
|
285
|
+
let suffix = requested.slice(alias.length);
|
|
286
|
+
// Accept both the logical-root spelling and Bun's displayed physical
|
|
287
|
+
// spelling without turning .../root/... into .../root/root/....
|
|
288
|
+
if (suffix.toLowerCase() === "/root") suffix = "";
|
|
289
|
+
else if (suffix.toLowerCase().startsWith("/root/")) suffix = suffix.slice(5);
|
|
290
|
+
return `${moduleDir}${suffix}`;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const handleRequest = async (request) => {
|
|
294
|
+
if (request.method !== "GET" && request.method !== "HEAD")
|
|
295
|
+
return new Response("Method Not Allowed", { status: 405, headers: { allow: "GET, HEAD" } });
|
|
296
|
+
|
|
297
|
+
const url = new URL(request.url);
|
|
298
|
+
let pathname = url.pathname;
|
|
299
|
+
if (randomRoute) {
|
|
300
|
+
const prefix = `/${randomRoute}`;
|
|
301
|
+
if (!pathname.startsWith(prefix)) return notFound();
|
|
302
|
+
pathname = `/${pathname.slice(prefix.length)}`;
|
|
303
|
+
}
|
|
304
|
+
const originalPreviewPath = previewRequest(pathname);
|
|
305
|
+
const resolved = resolveRequestPath(originalPreviewPath ?? pathname);
|
|
306
|
+
if (!resolved) return new Response("Bad Request", { status: 400 });
|
|
307
|
+
|
|
308
|
+
let stats;
|
|
309
|
+
try {
|
|
310
|
+
stats = lstatSync(resolved.path);
|
|
311
|
+
} catch {
|
|
312
|
+
return notFound();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (originalPreviewPath) {
|
|
316
|
+
if (stats.isDirectory()) return notFound();
|
|
317
|
+
return previewResponse({ name: basename(resolved.path), path: resolved.path, stats });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (stats.isDirectory()) {
|
|
321
|
+
if (!url.pathname.endsWith("/")) {
|
|
322
|
+
url.pathname += "/";
|
|
323
|
+
return Response.redirect(url, 308);
|
|
324
|
+
}
|
|
325
|
+
let entries;
|
|
326
|
+
try {
|
|
327
|
+
entries = readdirSync(resolved.path, { withFileTypes: true }).map((entry) => {
|
|
328
|
+
const path = resolve(resolved.path, entry.name);
|
|
329
|
+
return { name: entry.name, path, stats: lstatSync(path) };
|
|
330
|
+
});
|
|
331
|
+
} catch (error) {
|
|
332
|
+
return new Response(`Cannot list directory: ${error.message}`, { status: 403 });
|
|
333
|
+
}
|
|
334
|
+
return new Response(directoryHtml(resolved.decoded, entries), {
|
|
335
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Keep this as a whole, unsliced Bun.file() response. Bun's normal response
|
|
340
|
+
// path then provides native GET/HEAD Range handling, including 206 and 416.
|
|
341
|
+
return new Response(Bun.file(resolved.path));
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const safeHandleRequest = async (request) => {
|
|
345
|
+
try {
|
|
346
|
+
return await handleRequest(request);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
// Keep an unexpected renderer, filesystem, or runtime exception local to
|
|
349
|
+
// this request. Bun.serve stays alive and the next request is unaffected.
|
|
350
|
+
console.error("bunmsh: serve: request failed:", error);
|
|
351
|
+
return new Response(page("Internal Server Error",
|
|
352
|
+
"<h1>Internal Server Error</h1>\n<p>The server is still running.</p>"), {
|
|
353
|
+
status: 500,
|
|
354
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// Prefer a real xdg-open on PATH regardless of platform -- it's common
|
|
360
|
+
// enough on macOS/Windows dev setups (Cygwin, MSYS2, Homebrew's xdg-utils)
|
|
361
|
+
// and behaves consistently. Only fall back to each OS's own "hand this to
|
|
362
|
+
// the default handler" command when xdg-open isn't present, same relationship
|
|
363
|
+
// as the npm `buninu` package's own xdg-open shim.
|
|
364
|
+
function openServeCommand() {
|
|
365
|
+
const executable = Bun.which("xdg-open");
|
|
366
|
+
if (executable) return [executable];
|
|
367
|
+
if (process.platform === "darwin") return ["open"];
|
|
368
|
+
// start is a cmd builtin, not a real executable, and needs an empty ""
|
|
369
|
+
// title argument first or it treats a quoted target as the title.
|
|
370
|
+
if (process.platform === "win32") return ["cmd", "/c", "start", ""];
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// autoOpen is `true` for the served root as-is, or a "/"-led path opened
|
|
375
|
+
// relative to it -- resolved after any --random-url prefix baked into url,
|
|
376
|
+
// since url already ends in "/" (server.url does, and so does the URL built
|
|
377
|
+
// from randomRoute in main()).
|
|
378
|
+
export const openTargetUrl = (url, autoOpen) =>
|
|
379
|
+
typeof autoOpen === "string" ? new URL(autoOpen.slice(1), url) : url;
|
|
380
|
+
|
|
381
|
+
function openServeUrl(url, minapkWebview, env = process.env) {
|
|
382
|
+
const command = openServeCommand();
|
|
383
|
+
if (!command) {
|
|
384
|
+
console.error("bunmsh: serve: xdg-open not found");
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
Bun.spawn([...command, String(url)], {
|
|
389
|
+
env: minapkWebview ? { ...env, MINAPK_WEBVIEW: String(minapkWebview) } : env,
|
|
390
|
+
stdin: "ignore",
|
|
391
|
+
stdout: "ignore",
|
|
392
|
+
stderr: "ignore",
|
|
393
|
+
}).unref();
|
|
394
|
+
return true;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
console.error(`bunmsh: serve: ${command[0]}: ${error.message}`);
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function main(directory = process.cwd(), options = {}) {
|
|
402
|
+
// The virtual mount itself is not stat-able. import.meta.dirname points at
|
|
403
|
+
// its real root on each platform: /$bunfs/root or B:/~BUN/root.
|
|
404
|
+
root = resolve(resolveBunfsPath(directory) ?? directory);
|
|
405
|
+
randomRoute = options.randomUrl ? randomServeRoute() : "";
|
|
406
|
+
const serverOptions = {
|
|
407
|
+
port: Number((options.env ?? process.env).PORT ?? 3000),
|
|
408
|
+
fetch: safeHandleRequest,
|
|
409
|
+
};
|
|
410
|
+
let server;
|
|
411
|
+
try {
|
|
412
|
+
server = Bun.serve(serverOptions);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
const addressInUse = error?.code === "EADDRINUSE" ||
|
|
415
|
+
/EADDRINUSE|address already in use/i.test(String(error?.message ?? error));
|
|
416
|
+
if (serverOptions.port !== 3000 || !addressInUse) throw error;
|
|
417
|
+
server = Bun.serve({ ...serverOptions, port: 0 });
|
|
418
|
+
}
|
|
419
|
+
const url = randomRoute ? new URL(randomRoute, server.url) : server.url;
|
|
420
|
+
publicUrls.set(server, url);
|
|
421
|
+
console.log(`Serving ${root}\n ${url.href}`);
|
|
422
|
+
if (options.autoOpen) openServeUrl(openTargetUrl(url, options.autoOpen), options.minapkWebview, options.env);
|
|
423
|
+
return server;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function waitForInterrupt(server, options = {}) {
|
|
427
|
+
return new Promise((resolve) => {
|
|
428
|
+
let readline;
|
|
429
|
+
let finished = false;
|
|
430
|
+
const stop = (signal) => {
|
|
431
|
+
if (finished) return;
|
|
432
|
+
finished = true;
|
|
433
|
+
process.off("SIGINT", onSigint);
|
|
434
|
+
process.off("SIGTERM", onSigterm);
|
|
435
|
+
readline?.close();
|
|
436
|
+
server.stop(true);
|
|
437
|
+
resolve(signal);
|
|
438
|
+
};
|
|
439
|
+
const onSigint = () => stop("SIGINT");
|
|
440
|
+
const onSigterm = () => stop("SIGTERM");
|
|
441
|
+
process.once("SIGINT", onSigint);
|
|
442
|
+
process.once("SIGTERM", onSigterm);
|
|
443
|
+
|
|
444
|
+
if (!process.stdin.isTTY) return;
|
|
445
|
+
console.log("q/quit/exit: stop o: open in browser");
|
|
446
|
+
readline = createInterface({ input: process.stdin, terminal: false });
|
|
447
|
+
readline.on("line", (line) => {
|
|
448
|
+
const command = line.trim().toLowerCase();
|
|
449
|
+
if (["q", "quit", "exit"].includes(command)) {
|
|
450
|
+
stop("QUIT");
|
|
451
|
+
} else if (command === "o") {
|
|
452
|
+
openServeUrl(openTargetUrl(serveUrl(server), options.autoOpen), options.minapkWebview, options.env);
|
|
453
|
+
} else if (command) {
|
|
454
|
+
console.error(`bunmsh: serve: unknown control: ${command}`);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
readline.on("close", () => stop("EOF"));
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (import.meta.main) {
|
|
462
|
+
const options = parseServeArguments(process.argv.slice(2));
|
|
463
|
+
if (options.error) {
|
|
464
|
+
console.error(`bunmsh: serve: ${options.error}`);
|
|
465
|
+
process.exit(2);
|
|
466
|
+
}
|
|
467
|
+
await waitForInterrupt(main(options.directory, options), options);
|
|
468
|
+
}
|