mdv-live 0.3.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 ADDED
@@ -0,0 +1,50 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.3.0] - 2026-01-31
9
+
10
+ ### Added
11
+
12
+ - Initial Node.js version (rewrite from Python mdv-live)
13
+ - Express server with WebSocket for live reload
14
+ - markdown-it for standard Markdown rendering
15
+ - @marp-team/marp-core for Marp slide rendering
16
+ - File tree navigation with lazy loading
17
+ - Edit mode with textarea editor
18
+ - Dark/Light theme support
19
+ - Syntax highlighting with highlight.js
20
+ - Mermaid diagram support
21
+ - PDF output via browser print
22
+ - File operations (create, delete, rename, move, upload)
23
+ - Keyboard shortcuts for common actions
24
+ - Task list (checkbox) support with markdown-it-task-lists
25
+ - Range Request support for video/audio streaming
26
+ - WebSocket tree_update broadcast for multi-client sync
27
+ - Comprehensive security tests (76 tests)
28
+
29
+ ### Security
30
+
31
+ - Path traversal prevention (absolute path, `..`, null byte)
32
+ - Filename sanitization for uploads
33
+ - Unified validatePath() for all API endpoints
34
+
35
+ ### Marp Features
36
+
37
+ - Full compatibility with marp-core
38
+ - Official themes: default, gaia, uncover
39
+ - All directives: paginate, header, footer, backgroundColor, etc.
40
+ - Background images and split backgrounds
41
+ - KaTeX math support
42
+ - Slide navigation with arrow keys
43
+
44
+ ### CLI Features
45
+
46
+ - Port auto-increment when port is in use
47
+ - Server list (`mdv -l`)
48
+ - Server kill (`mdv -k PID` or `mdv -k -a`)
49
+ - PDF conversion (`mdv --pdf file.md`)
50
+ - No-browser mode (`mdv --no-browser`)
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PanHouse
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,240 @@
1
+ # MDV - Markdown Viewer with Marp Support
2
+
3
+ ファイルツリー + ライブプレビュー + Marp完全対応のMarkdownビューア
4
+
5
+ [![npm version](https://badge.fury.io/js/mdv-live.svg)](https://www.npmjs.com/package/mdv-live)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Features
9
+
10
+ - 📁 左側にフォルダツリー表示(遅延読み込み対応)
11
+ - 📄 Markdownをリアルタイムレンダリング
12
+ - 🎬 **Marp完全対応**(公式テーマ・ディレクティブ・数式)
13
+ - 🔄 ファイル更新時に自動リロード(WebSocket)
14
+ - 🎨 シンタックスハイライト(highlight.js)
15
+ - 📊 Mermaid図のレンダリング
16
+ - 🌙 ダーク/ライトテーマ切り替え
17
+ - ✏️ インラインエディタ(Cmd+E)
18
+ - ✅ タスクリスト(チェックボックス)対応
19
+ - 📥 PDF出力(Cmd+P)
20
+ - 🎬 動画/音声ストリーミング再生(Range Request対応)
21
+ - 📤 ファイルアップロード(ドラッグ&ドロップ)
22
+ - 🔒 セキュリティ強化(パストラバーサル防止)
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ # グローバルインストール(推奨)
28
+ npm install -g mdv-live
29
+
30
+ # または npx で直接実行
31
+ npx mdv-live
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```bash
37
+ # カレントディレクトリを表示
38
+ mdv
39
+
40
+ # 特定のディレクトリを表示
41
+ mdv ./docs
42
+
43
+ # 特定のファイルを開く
44
+ mdv README.md
45
+
46
+ # ポート指定(デフォルト: 8642)
47
+ mdv -p 9000
48
+
49
+ # ブラウザを自動で開かない
50
+ mdv --no-browser
51
+
52
+ # 起動中のサーバー一覧
53
+ mdv -l
54
+
55
+ # サーバーを停止(PID指定)
56
+ mdv -k 12345
57
+
58
+ # 全サーバーを停止
59
+ mdv -k -a
60
+
61
+ # PDFに変換
62
+ mdv --pdf slide.md
63
+ mdv --pdf slide.md -o output.pdf
64
+
65
+ # バージョン表示
66
+ mdv -v
67
+ ```
68
+
69
+ ### ポート自動増分
70
+
71
+ ポートが使用中の場合、自動的に次のポート番号を試します。
72
+
73
+ ```
74
+ $ mdv -p 8642
75
+ ポート 8642 は使用中です。8643 を試します...
76
+ MDV server running at http://localhost:8643
77
+ ```
78
+
79
+ ## macOS Finder Integration
80
+
81
+ macOSで`.md`ファイルをダブルクリックしてMDVで開けるようにする設定です。
82
+
83
+ ### セットアップスクリプトを使用(推奨)
84
+
85
+ ```bash
86
+ # mdvがインストールされていることを確認
87
+ which mdv
88
+
89
+ # セットアップスクリプトを実行
90
+ curl -fsSL https://raw.githubusercontent.com/panhouse/mdv/main/scripts/setup-macos-app.sh | bash
91
+ ```
92
+
93
+ または、リポジトリをクローンしている場合:
94
+
95
+ ```bash
96
+ npm run setup-macos
97
+ ```
98
+
99
+ ### デフォルトアプリに設定
100
+
101
+ 1. Finderで任意の`.md`ファイルを右クリック
102
+ 2. 「情報を見る」を選択
103
+ 3. 「このアプリケーションで開く」で「MDV」を選択
104
+ 4. 「すべてを変更...」をクリック
105
+
106
+ ## Marp Support
107
+
108
+ `marp: true` フロントマターを含むMarkdownファイルは自動的にMarpスライドとしてレンダリングされます。
109
+
110
+ ```markdown
111
+ ---
112
+ marp: true
113
+ theme: default
114
+ paginate: true
115
+ ---
116
+
117
+ # スライドタイトル
118
+
119
+ 内容...
120
+
121
+ ---
122
+
123
+ # 次のスライド
124
+
125
+ - 箇条書き
126
+ - 数式: $E = mc^2$
127
+ ```
128
+
129
+ ### サポートされるMarp機能
130
+
131
+ - **テーマ**: default, gaia, uncover
132
+ - **ディレクティブ**: paginate, header, footer, backgroundColor, etc.
133
+ - **画像構文**: `![bg]`, `![w:100px]`, `![bg left]`
134
+ - **数式**: KaTeX対応(インライン `$...$`、ブロック `$$...$$`)
135
+
136
+ ## Keyboard Shortcuts
137
+
138
+ | ショートカット | 機能 |
139
+ |---------------|------|
140
+ | Cmd/Ctrl + B | サイドバー表示切替 |
141
+ | Cmd/Ctrl + E | 編集モード切替 |
142
+ | Cmd/Ctrl + S | 保存(編集モード時) |
143
+ | Cmd/Ctrl + P | PDF出力 |
144
+ | Cmd/Ctrl + W | タブを閉じる |
145
+ | ← / → | スライド移動(Marp時) |
146
+ | F2 | ファイル名変更 |
147
+ | Delete | ファイル削除 |
148
+
149
+ ## API Endpoints
150
+
151
+ | Endpoint | Method | Description |
152
+ |----------|--------|-------------|
153
+ | `/api/file` | GET | ファイル内容取得 |
154
+ | `/api/file` | POST | ファイル保存 |
155
+ | `/api/file` | DELETE | ファイル/ディレクトリ削除 |
156
+ | `/api/tree` | GET | ファイルツリー取得 |
157
+ | `/api/tree/expand` | GET | ディレクトリ展開(遅延読み込み) |
158
+ | `/api/mkdir` | POST | ディレクトリ作成 |
159
+ | `/api/move` | POST | ファイル移動/リネーム |
160
+ | `/api/download` | GET | ファイルダウンロード |
161
+ | `/api/upload` | POST | ファイルアップロード |
162
+ | `/api/info` | GET | サーバー情報 |
163
+
164
+ ## Tech Stack
165
+
166
+ - **Backend**: Node.js + Express
167
+ - **Frontend**: Vanilla JavaScript
168
+ - **Markdown**: markdown-it + markdown-it-task-lists
169
+ - **Marp**: @marp-team/marp-core
170
+ - **WebSocket**: ws
171
+ - **File Watching**: chokidar
172
+ - **Syntax Highlight**: highlight.js
173
+
174
+ ## Development
175
+
176
+ ```bash
177
+ # Clone repository
178
+ git clone https://github.com/panhouse/mdv.git
179
+ cd mdv
180
+
181
+ # Install dependencies
182
+ npm install
183
+
184
+ # Start development server
185
+ npm run dev
186
+
187
+ # Run tests
188
+ npm test
189
+ ```
190
+
191
+ ## Project Structure
192
+
193
+ ```
194
+ mdv/
195
+ ├── bin/mdv.js # CLI entry point
196
+ ├── src/
197
+ │ ├── server.js # Express server setup
198
+ │ ├── watcher.js # File watching (chokidar)
199
+ │ ├── api/
200
+ │ │ ├── file.js # File operations API
201
+ │ │ ├── tree.js # File tree API
202
+ │ │ └── upload.js # Upload API
203
+ │ ├── rendering/
204
+ │ │ ├── index.js # Rendering entry
205
+ │ │ ├── markdown.js # Markdown rendering
206
+ │ │ └── marp.js # Marp rendering
207
+ │ ├── utils/
208
+ │ │ ├── fileTypes.js # File type detection
209
+ │ │ └── path.js # Path security utilities
210
+ │ └── static/ # Frontend files
211
+ │ ├── index.html
212
+ │ ├── app.js
213
+ │ └── styles.css
214
+ ├── scripts/
215
+ │ └── setup-macos-app.sh # macOS app setup
216
+ └── tests/ # Test files
217
+ ```
218
+
219
+ ## Requirements
220
+
221
+ - Node.js 18+
222
+
223
+ ## Migration from Python version
224
+
225
+ 以前のPython版(`pip install mdv-live`)からの移行:
226
+
227
+ ```bash
228
+ # Python版をアンインストール
229
+ pip uninstall mdv-live
230
+
231
+ # Node.js版をインストール
232
+ npm install -g mdv-live
233
+
234
+ # macOSアプリを再設定(必要な場合)
235
+ npm run setup-macos
236
+ ```
237
+
238
+ ## License
239
+
240
+ MIT
package/bin/mdv.js ADDED
@@ -0,0 +1,400 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MDV CLI - Markdown Viewer with Marp support
5
+ * Compatible with the original Python mdv-live CLI
6
+ */
7
+
8
+ import { createMdvServer } from '../src/server.js';
9
+ import { parseArgs } from 'node:util';
10
+ import { execSync, spawn } from 'child_process';
11
+ import { createServer as createNetServer } from 'net';
12
+ import path from 'path';
13
+ import fs from 'fs/promises';
14
+ import open from 'open';
15
+
16
+ const DEFAULT_PORT = 8642;
17
+
18
+ // Parse command line arguments
19
+ const options = {
20
+ port: {
21
+ type: 'string',
22
+ short: 'p',
23
+ },
24
+ 'no-browser': {
25
+ type: 'boolean',
26
+ default: false
27
+ },
28
+ list: {
29
+ type: 'boolean',
30
+ short: 'l',
31
+ default: false
32
+ },
33
+ kill: {
34
+ type: 'boolean',
35
+ short: 'k',
36
+ default: false
37
+ },
38
+ all: {
39
+ type: 'boolean',
40
+ short: 'a',
41
+ default: false
42
+ },
43
+ pdf: {
44
+ type: 'boolean',
45
+ default: false
46
+ },
47
+ output: {
48
+ type: 'string',
49
+ short: 'o',
50
+ },
51
+ help: {
52
+ type: 'boolean',
53
+ short: 'h',
54
+ default: false
55
+ },
56
+ version: {
57
+ type: 'boolean',
58
+ short: 'v',
59
+ default: false
60
+ }
61
+ };
62
+
63
+ function showHelp() {
64
+ console.log(`
65
+ MDV - Markdown Viewer with file tree + live preview + Marp support
66
+
67
+ Usage: mdv [options] [path]
68
+
69
+ Arguments:
70
+ path Directory or file path to view (default: current directory)
71
+
72
+ Server Options:
73
+ -p, --port <n> Server port (default: ${DEFAULT_PORT})
74
+ --no-browser Don't open browser automatically
75
+
76
+ Server Management:
77
+ -l, --list List running MDV servers
78
+ -k, --kill [PID] Stop server (-k -a for all, -k <PID> for specific)
79
+ -a, --all Use with -k to stop all servers
80
+
81
+ PDF Conversion:
82
+ --pdf Convert markdown file to PDF
83
+ -o, --output <file> Output PDF file path
84
+
85
+ Other:
86
+ -h, --help Show this help message
87
+ -v, --version Show version number
88
+
89
+ Examples:
90
+ mdv Start viewer in current directory
91
+ mdv /path/to/dir Start viewer in specified directory
92
+ mdv README.md Open specific file
93
+ mdv --pdf README.md Convert markdown to PDF
94
+ mdv -p 3000 Start on port 3000
95
+ mdv -l List running servers
96
+ mdv -k -a Stop all servers
97
+ `);
98
+ }
99
+
100
+ /**
101
+ * Get running MDV server processes
102
+ */
103
+ function getMdvProcesses() {
104
+ try {
105
+ const result = execSync('lsof -i -P -n 2>/dev/null || true', { encoding: 'utf-8' });
106
+ const processes = [];
107
+
108
+ for (const line of result.split('\n')) {
109
+ if (!line.includes('node') || !line.includes('LISTEN')) continue;
110
+
111
+ const parts = line.split(/\s+/);
112
+ if (parts.length < 9) continue;
113
+
114
+ const pid = parts[1];
115
+
116
+ // Check if this is an MDV process
117
+ try {
118
+ const cmdResult = execSync(`ps -p ${pid} -o command= 2>/dev/null || true`, { encoding: 'utf-8' }).trim();
119
+ if (!cmdResult.toLowerCase().includes('mdv')) continue;
120
+
121
+ // Extract port
122
+ const portInfo = parts[8] || '';
123
+ let port = '';
124
+ if (portInfo.includes(':')) {
125
+ port = portInfo.split(':').pop().split('->')[0];
126
+ }
127
+
128
+ const displayCmd = cmdResult.length > 60 ? cmdResult.slice(0, 60) + '...' : cmdResult;
129
+ processes.push({ pid, port, command: displayCmd });
130
+ } catch {
131
+ continue;
132
+ }
133
+ }
134
+
135
+ return processes;
136
+ } catch {
137
+ return [];
138
+ }
139
+ }
140
+
141
+ /**
142
+ * List running MDV servers
143
+ */
144
+ function listServers() {
145
+ const processes = getMdvProcesses();
146
+
147
+ if (processes.length === 0) {
148
+ console.log('稼働中のMDVサーバーはありません');
149
+ return 0;
150
+ }
151
+
152
+ console.log(`稼働中のMDVサーバー: ${processes.length}件`);
153
+ console.log('-'.repeat(60));
154
+ console.log(`${'PID'.padEnd(8)} ${'Port'.padEnd(8)} Command`);
155
+ console.log('-'.repeat(60));
156
+
157
+ for (const proc of processes) {
158
+ console.log(`${proc.pid.padEnd(8)} ${proc.port.padEnd(8)} ${proc.command}`);
159
+ }
160
+
161
+ console.log('-'.repeat(60));
162
+ console.log('\n停止: mdv -k -a (全停止) / mdv -k <PID> (個別停止)');
163
+ return 0;
164
+ }
165
+
166
+ /**
167
+ * Kill MDV server(s)
168
+ */
169
+ function killServers(target, killAll) {
170
+ if (target) {
171
+ // Kill specific PID
172
+ try {
173
+ execSync(`kill ${target}`, { encoding: 'utf-8' });
174
+ console.log(`PID ${target} を停止しました`);
175
+ return 0;
176
+ } catch {
177
+ console.log(`PID ${target} の停止に失敗しました`);
178
+ return 1;
179
+ }
180
+ }
181
+
182
+ if (!killAll) {
183
+ console.log('全サーバーを停止するには -a オプションが必要です');
184
+ console.log(' mdv -k -a 全サーバーを停止');
185
+ console.log(' mdv -k <PID> 特定のサーバーを停止');
186
+ return 1;
187
+ }
188
+
189
+ // Kill all servers
190
+ const processes = getMdvProcesses();
191
+
192
+ if (processes.length === 0) {
193
+ console.log('稼働中のMDVサーバーはありません');
194
+ return 0;
195
+ }
196
+
197
+ console.log(`${processes.length}件のMDVサーバーを停止します...`);
198
+
199
+ let killed = 0;
200
+ for (const proc of processes) {
201
+ try {
202
+ execSync(`kill ${proc.pid}`, { encoding: 'utf-8' });
203
+ console.log(` PID ${proc.pid} (port ${proc.port}) を停止`);
204
+ killed++;
205
+ } catch {
206
+ console.log(` PID ${proc.pid} の停止に失敗`);
207
+ }
208
+ }
209
+
210
+ console.log(`\n完了: ${killed}/${processes.length} 件を停止しました`);
211
+ return killed === processes.length ? 0 : 1;
212
+ }
213
+
214
+ /**
215
+ * Convert markdown to PDF using marp-cli
216
+ */
217
+ async function convertToPdf(inputPath, outputPath) {
218
+ const resolved = path.resolve(inputPath);
219
+
220
+ try {
221
+ await fs.access(resolved);
222
+ } catch {
223
+ console.error(`Error: File not found: ${inputPath}`);
224
+ return 1;
225
+ }
226
+
227
+ const ext = path.extname(resolved).toLowerCase();
228
+ if (!['.md', '.markdown'].includes(ext)) {
229
+ console.error(`Error: Not a markdown file: ${inputPath}`);
230
+ return 1;
231
+ }
232
+
233
+ const defaultOutput = resolved.replace(/\.(md|markdown)$/i, '.pdf');
234
+ const finalOutput = outputPath ? path.resolve(outputPath) : defaultOutput;
235
+
236
+ console.log(`Converting ${inputPath} to PDF...`);
237
+
238
+ try {
239
+ // Use marp-cli for PDF conversion (supports Marp slides)
240
+ execSync(`npx @marp-team/marp-cli "${resolved}" --pdf -o "${finalOutput}"`, {
241
+ encoding: 'utf-8',
242
+ stdio: 'inherit'
243
+ });
244
+ console.log(`PDF saved: ${finalOutput}`);
245
+ return 0;
246
+ } catch (err) {
247
+ console.error('Error: PDF conversion failed');
248
+ console.error('Make sure Node.js and npx are installed');
249
+ return 1;
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Check if a port is available
255
+ */
256
+ async function isPortAvailable(port) {
257
+ return new Promise((resolve) => {
258
+ const server = createNetServer();
259
+ server.once('error', () => resolve(false));
260
+ server.once('listening', () => {
261
+ server.close(() => resolve(true));
262
+ });
263
+ server.listen(port);
264
+ });
265
+ }
266
+
267
+ /**
268
+ * Find an available port starting from the given port
269
+ */
270
+ async function findAvailablePort(startPort, maxRetries = 100) {
271
+ for (let i = 0; i < maxRetries; i++) {
272
+ const port = startPort + i;
273
+ const available = await isPortAvailable(port);
274
+ if (available) {
275
+ return port;
276
+ }
277
+ if (i > 0) {
278
+ console.log(`ポート ${port - 1} は使用中です。${port} を試します...`);
279
+ }
280
+ }
281
+ return null;
282
+ }
283
+
284
+ /**
285
+ * Start MDV server with auto port increment
286
+ */
287
+ async function startViewer(targetPath, startPort, openBrowser) {
288
+ let rootDir = process.cwd();
289
+ let initialFile = null;
290
+
291
+ if (targetPath && targetPath !== '.') {
292
+ const resolved = path.resolve(targetPath);
293
+ try {
294
+ const stats = await fs.stat(resolved);
295
+ if (stats.isDirectory()) {
296
+ rootDir = resolved;
297
+ } else if (stats.isFile()) {
298
+ rootDir = path.dirname(resolved);
299
+ initialFile = path.basename(resolved);
300
+ }
301
+ } catch {
302
+ console.error(`Error: Path not found: ${targetPath}`);
303
+ process.exit(1);
304
+ }
305
+ }
306
+
307
+ // Find available port
308
+ const port = await findAvailablePort(startPort);
309
+ if (!port) {
310
+ console.error('Error: 利用可能なポートが見つかりませんでした');
311
+ process.exit(1);
312
+ }
313
+
314
+ if (port !== startPort) {
315
+ console.log(`ポート ${startPort} は使用中のため、${port} で起動します`);
316
+ }
317
+
318
+ // Create and start server
319
+ const mdv = createMdvServer({ rootDir, port });
320
+ await mdv.start();
321
+
322
+ const url = initialFile
323
+ ? `http://localhost:${port}?file=${encodeURIComponent(initialFile)}`
324
+ : `http://localhost:${port}`;
325
+
326
+ console.log(`
327
+ MDV - Markdown Viewer with Marp support
328
+
329
+ Server running at: ${url}
330
+ Root directory: ${rootDir}
331
+
332
+ Press Ctrl+C to stop
333
+ `);
334
+
335
+ if (openBrowser) {
336
+ await open(url);
337
+ }
338
+ }
339
+
340
+ async function main() {
341
+ let args;
342
+ try {
343
+ args = parseArgs({
344
+ options,
345
+ allowPositionals: true,
346
+ strict: false
347
+ });
348
+ } catch (err) {
349
+ console.error('Error parsing arguments:', err.message);
350
+ showHelp();
351
+ process.exit(1);
352
+ }
353
+
354
+ const { values, positionals } = args;
355
+
356
+ // Help
357
+ if (values.help) {
358
+ showHelp();
359
+ process.exit(0);
360
+ }
361
+
362
+ // Version
363
+ if (values.version) {
364
+ console.log('mdv v0.3.0');
365
+ process.exit(0);
366
+ }
367
+
368
+ // List servers
369
+ if (values.list) {
370
+ process.exit(listServers());
371
+ }
372
+
373
+ // Kill servers
374
+ if (values.kill) {
375
+ const pid = positionals[0] || null;
376
+ process.exit(killServers(pid, values.all));
377
+ }
378
+
379
+ // PDF conversion
380
+ if (values.pdf) {
381
+ const inputPath = positionals[0];
382
+ if (!inputPath) {
383
+ console.error('Error: --pdf requires a markdown file path');
384
+ process.exit(1);
385
+ }
386
+ process.exit(await convertToPdf(inputPath, values.output));
387
+ }
388
+
389
+ // Default: start viewer
390
+ const targetPath = positionals[0] || '.';
391
+ const port = parseInt(values.port, 10) || DEFAULT_PORT;
392
+ const openBrowser = !values['no-browser'];
393
+
394
+ await startViewer(targetPath, port, openBrowser);
395
+ }
396
+
397
+ main().catch(err => {
398
+ console.error('Error:', err.message);
399
+ process.exit(1);
400
+ });