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 CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  **Browse and preview markdown files in any directory.**
4
4
 
5
- [![npm version](https://img.shields.io/npm/v/mdbrowse)](https://www.npmjs.com/package/mdbrowse)
6
- [![license](https://img.shields.io/npm/l/mdbrowse)](LICENSE)
5
+ [![npm version](https://img.shields.io/npm/v/mdbrowse-cli)](https://www.npmjs.com/package/mdbrowse-cli)
6
+ [![license](https://img.shields.io/npm/l/mdbrowse-cli)](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('mdnow')
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', 'localhost')
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 url = `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`;
45
- console.log(`\n mdnow serving ${directory}`);
46
- console.log(` → ${url}\n`);
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(port);
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} ${url}`);
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.0",
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
- "mdnow": "bin/mdnow.js"
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
- /* ── mdnow client ── */
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('mdnow-theme') || 'auto';
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('mdnow-theme', theme);
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} — mdnow`;
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} — mdnow`;
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} — mdnow`;
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} — mdnow`;
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 = 'mdnow';
268
+ document.title = 'mdbrowse-cli';
269
269
  hideAllToolbarButtons();
270
270
  editMode = false;
271
271
  contentInner.innerHTML = `
272
272
  <div id="welcome">
273
- <h1>mdnow</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>mdnow</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">mdnow</span>
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>mdnow</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
  }