mdbrowse-cli 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/bin/{mdnow.js → mdbrowse-cli.js} +21 -8
- package/package.json +2 -2
- package/public/app.js +9 -9
- package/public/index.html +3 -3
- package/src/server.js +42 -12
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
**Browse and preview markdown files in any directory.**
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/mdbrowse)
|
|
6
|
-
[](LICENSE)
|
|
5
|
+
[](https://www.npmjs.com/package/mdbrowse-cli)
|
|
6
|
+
[](LICENSE)
|
|
7
7
|
|
|
8
8
|
Zero-install CLI that spins up a local web UI with a file tree, rendered markdown, live reload, and optional Cloudflare Tunnel for remote access.
|
|
9
9
|
|
|
@@ -13,9 +13,9 @@ Zero-install CLI that spins up a local web UI with a file tree, rendered markdow
|
|
|
13
13
|
## Quick start
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npx mdbrowse . # serve current directory
|
|
17
|
-
npx mdbrowse ./docs # serve a specific folder
|
|
18
|
-
npx mdbrowse . --tunnel # expose via Cloudflare Tunnel
|
|
16
|
+
npx mdbrowse-cli . # serve current directory
|
|
17
|
+
npx mdbrowse-cli ./docs # serve a specific folder
|
|
18
|
+
npx mdbrowse-cli . --tunnel # expose via Cloudflare Tunnel
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
Opens in your browser automatically. On SSH/headless servers, grab the printed URL.
|
|
@@ -49,7 +49,7 @@ Opens in your browser automatically. On SSH/headless servers, grab the printed U
|
|
|
49
49
|
|
|
50
50
|
## Use cases
|
|
51
51
|
|
|
52
|
-
**Remote / headless servers** — Working on a cloud dev box or VPS? Run `npx mdbrowse . --tunnel` to get a public URL and view rendered markdown from any browser.
|
|
52
|
+
**Remote / headless servers** — Working on a cloud dev box or VPS? Run `npx mdbrowse-cli . --tunnel` to get a public URL and view rendered markdown from any browser.
|
|
53
53
|
|
|
54
54
|
**AI coding tools** — Using Claude Code, Codex, or similar tools that generate lots of markdown? Browse their output rendered, not raw.
|
|
55
55
|
|
|
@@ -6,12 +6,12 @@ import { startServer } from '../src/server.js';
|
|
|
6
6
|
const program = new Command();
|
|
7
7
|
|
|
8
8
|
program
|
|
9
|
-
.name('
|
|
9
|
+
.name('mdbrowse-cli')
|
|
10
10
|
.description('Browse and preview markdown files in any directory')
|
|
11
11
|
.version('0.1.0')
|
|
12
12
|
.argument('[directory]', 'Directory to serve', '.')
|
|
13
13
|
.option('-p, --port <number>', 'Port to listen on', '3000')
|
|
14
|
-
.option('--host <address>', 'Host to bind to', '
|
|
14
|
+
.option('--host <address>', 'Host to bind to', '0.0.0.0')
|
|
15
15
|
.option('--no-ignore', 'Show all files (ignore .gitignore)')
|
|
16
16
|
.option('--auth <credentials>', 'Require basic auth (user:pass)')
|
|
17
17
|
.option('--read-only', 'Disable file editing')
|
|
@@ -32,7 +32,7 @@ program
|
|
|
32
32
|
auth = { username: user, password: rest.join(':') };
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
await startServer({
|
|
35
|
+
const { port: actualPort } = await startServer({
|
|
36
36
|
directory,
|
|
37
37
|
port,
|
|
38
38
|
host,
|
|
@@ -41,15 +41,28 @@ program
|
|
|
41
41
|
readOnly,
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
const
|
|
45
|
-
console.log(`\n
|
|
46
|
-
console.log(` → ${
|
|
44
|
+
const localUrl = `http://localhost:${actualPort}`;
|
|
45
|
+
console.log(`\n mdbrowse-cli serving ${directory}`);
|
|
46
|
+
console.log(` → Local: ${localUrl}`);
|
|
47
|
+
|
|
48
|
+
if (host === '0.0.0.0') {
|
|
49
|
+
const { networkInterfaces } = await import('os');
|
|
50
|
+
const nets = networkInterfaces();
|
|
51
|
+
for (const name of Object.keys(nets)) {
|
|
52
|
+
for (const net of nets[name]) {
|
|
53
|
+
if (net.family === 'IPv4' && !net.internal) {
|
|
54
|
+
console.log(` → Network: http://${net.address}:${actualPort}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
console.log();
|
|
47
60
|
|
|
48
61
|
if (options.tunnel) {
|
|
49
62
|
const { startTunnel, registerCleanup } = await import('../src/tunnel.js');
|
|
50
63
|
try {
|
|
51
64
|
console.log(' Starting Cloudflare Tunnel...');
|
|
52
|
-
const { url: tunnelUrl, child } = await startTunnel(
|
|
65
|
+
const { url: tunnelUrl, child } = await startTunnel(actualPort);
|
|
53
66
|
registerCleanup(child);
|
|
54
67
|
console.log(` → ${tunnelUrl}\n`);
|
|
55
68
|
} catch (err) {
|
|
@@ -73,7 +86,7 @@ program
|
|
|
73
86
|
: process.platform === 'win32'
|
|
74
87
|
? 'start'
|
|
75
88
|
: 'xdg-open';
|
|
76
|
-
exec(`${cmd} ${
|
|
89
|
+
exec(`${cmd} ${localUrl}`);
|
|
77
90
|
}
|
|
78
91
|
});
|
|
79
92
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mdbrowse-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "CLI tool to browse and preview markdown files in any directory",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"
|
|
7
|
+
"mdbrowse-cli": "bin/mdbrowse-cli.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
package/public/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* ──
|
|
1
|
+
/* ── mdbrowse-cli client ── */
|
|
2
2
|
|
|
3
3
|
const fileTreeEl = document.getElementById('file-tree');
|
|
4
4
|
const contentInner = document.getElementById('content-inner');
|
|
@@ -31,7 +31,7 @@ function isImageFile(name) {
|
|
|
31
31
|
// ── Theme ──
|
|
32
32
|
|
|
33
33
|
function getTheme() {
|
|
34
|
-
return localStorage.getItem('
|
|
34
|
+
return localStorage.getItem('mdbrowse-cli-theme') || 'auto';
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function applyTheme(theme) {
|
|
@@ -39,7 +39,7 @@ function applyTheme(theme) {
|
|
|
39
39
|
if (theme !== 'auto') {
|
|
40
40
|
document.documentElement.classList.add(theme);
|
|
41
41
|
}
|
|
42
|
-
localStorage.setItem('
|
|
42
|
+
localStorage.setItem('mdbrowse-cli-theme', theme);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
themeToggle.addEventListener('click', () => {
|
|
@@ -199,17 +199,17 @@ function renderFile(filePath, data) {
|
|
|
199
199
|
const pathHeader = `<div class="file-path-header">${escapeHtml(filePath)}</div>`;
|
|
200
200
|
|
|
201
201
|
if (data.type === 'image') {
|
|
202
|
-
document.title = `${filePath} —
|
|
202
|
+
document.title = `${filePath} — mdbrowse-cli`;
|
|
203
203
|
const name = filePath.split('/').pop();
|
|
204
204
|
contentInner.innerHTML = pathHeader + `<div class="image-preview"><img src="${data.src}" alt="${escapeHtml(name)}"></div>`;
|
|
205
205
|
} else if (data.type === 'notice') {
|
|
206
|
-
document.title = `${filePath} —
|
|
206
|
+
document.title = `${filePath} — mdbrowse-cli`;
|
|
207
207
|
contentInner.innerHTML = pathHeader + `<div class="file-notice">${escapeHtml(data.message)}</div>`;
|
|
208
208
|
} else if (data.type === 'markdown') {
|
|
209
|
-
document.title = `${data.title || filePath} —
|
|
209
|
+
document.title = `${data.title || filePath} — mdbrowse-cli`;
|
|
210
210
|
contentInner.innerHTML = pathHeader + `<div class="markdown-body">${data.html}</div>`;
|
|
211
211
|
} else {
|
|
212
|
-
document.title = `${filePath} —
|
|
212
|
+
document.title = `${filePath} — mdbrowse-cli`;
|
|
213
213
|
contentInner.innerHTML = pathHeader + `<div class="code-view">${data.html}</div>`;
|
|
214
214
|
}
|
|
215
215
|
|
|
@@ -265,12 +265,12 @@ window.addEventListener('popstate', (e) => {
|
|
|
265
265
|
});
|
|
266
266
|
|
|
267
267
|
function showWelcome() {
|
|
268
|
-
document.title = '
|
|
268
|
+
document.title = 'mdbrowse-cli';
|
|
269
269
|
hideAllToolbarButtons();
|
|
270
270
|
editMode = false;
|
|
271
271
|
contentInner.innerHTML = `
|
|
272
272
|
<div id="welcome">
|
|
273
|
-
<h1>
|
|
273
|
+
<h1>mdbrowse-cli</h1>
|
|
274
274
|
<p>Select a file from the sidebar to get started.</p>
|
|
275
275
|
</div>
|
|
276
276
|
`;
|
package/public/index.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>
|
|
6
|
+
<title>mdbrowse-cli</title>
|
|
7
7
|
<link rel="stylesheet" href="/assets/style.css">
|
|
8
8
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
|
|
9
9
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<div id="app">
|
|
13
13
|
<aside id="sidebar">
|
|
14
14
|
<div class="sidebar-header">
|
|
15
|
-
<span class="logo">
|
|
15
|
+
<span class="logo">mdbrowse-cli</span>
|
|
16
16
|
<button id="theme-toggle" title="Toggle theme" aria-label="Toggle theme">
|
|
17
17
|
<svg class="icon-sun" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
|
18
18
|
<svg class="icon-moon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
</div>
|
|
33
33
|
<div id="content-inner">
|
|
34
34
|
<div id="welcome">
|
|
35
|
-
<h1>
|
|
35
|
+
<h1>mdbrowse-cli</h1>
|
|
36
36
|
<p>Select a file from the sidebar to get started.</p>
|
|
37
37
|
</div>
|
|
38
38
|
</div>
|
package/src/server.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import net from 'net';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import { fileURLToPath } from 'url';
|
|
4
5
|
import { Hono } from 'hono';
|
|
@@ -13,6 +14,34 @@ import { search } from './search.js';
|
|
|
13
14
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
const publicDir = path.join(__dirname, '..', 'public');
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Check if a port is available.
|
|
19
|
+
*/
|
|
20
|
+
function isPortAvailable(port, host) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const server = net.createServer();
|
|
23
|
+
server.once('error', () => resolve(false));
|
|
24
|
+
server.once('listening', () => {
|
|
25
|
+
server.close(() => resolve(true));
|
|
26
|
+
});
|
|
27
|
+
server.listen(port, host);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Find the next available port starting from the given port.
|
|
33
|
+
* Tries up to maxAttempts ports.
|
|
34
|
+
*/
|
|
35
|
+
async function findAvailablePort(startPort, host, maxAttempts = 10) {
|
|
36
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
37
|
+
const port = startPort + i;
|
|
38
|
+
if (await isPortAvailable(port, host)) {
|
|
39
|
+
return port;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
16
45
|
const MAX_FILE_SIZE = 1024 * 1024; // 1 MB
|
|
17
46
|
|
|
18
47
|
const BINARY_EXTENSIONS = new Set([
|
|
@@ -256,23 +285,24 @@ export async function startServer({ directory, port, host, respectIgnore, auth,
|
|
|
256
285
|
return c.html(html);
|
|
257
286
|
});
|
|
258
287
|
|
|
288
|
+
// Find an available port
|
|
289
|
+
const availablePort = await findAvailablePort(port, host);
|
|
290
|
+
if (!availablePort) {
|
|
291
|
+
console.error(`\n Error: No available port found (tried ${port}-${port + 9}).`);
|
|
292
|
+
console.error(` Try: mdbrowse-cli --port <number>\n`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
if (availablePort !== port) {
|
|
296
|
+
console.log(` Port ${port} in use, using ${availablePort} instead.`);
|
|
297
|
+
}
|
|
298
|
+
|
|
259
299
|
// Start HTTP server
|
|
260
300
|
const server = serve({
|
|
261
301
|
fetch: app.fetch,
|
|
262
|
-
port,
|
|
302
|
+
port: availablePort,
|
|
263
303
|
hostname: host,
|
|
264
304
|
});
|
|
265
305
|
|
|
266
|
-
// Handle port-in-use errors
|
|
267
|
-
server.on('error', (err) => {
|
|
268
|
-
if (err.code === 'EADDRINUSE') {
|
|
269
|
-
console.error(`\n Error: Port ${port} is already in use.`);
|
|
270
|
-
console.error(` Try: mdnow --port ${port + 1}\n`);
|
|
271
|
-
process.exit(1);
|
|
272
|
-
}
|
|
273
|
-
throw err;
|
|
274
|
-
});
|
|
275
|
-
|
|
276
306
|
// WebSocket server
|
|
277
307
|
const wss = new WebSocketServer({ server });
|
|
278
308
|
|
|
@@ -295,5 +325,5 @@ export async function startServer({ directory, port, host, respectIgnore, auth,
|
|
|
295
325
|
// Start file watcher
|
|
296
326
|
startWatcher(rootDir, broadcast, respectIgnore);
|
|
297
327
|
|
|
298
|
-
return server;
|
|
328
|
+
return { server, port: availablePort };
|
|
299
329
|
}
|