pagedex 0.0.0-stage → 0.1.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.
Binary file
Binary file
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
2
+ <rect x="2" y="2" width="60" height="60" rx="16" fill="#181a20" stroke="#8b5cf6" stroke-opacity="0.5" stroke-width="2.5" />
3
+ <path d="M15 18H25L32 37L26.5 48L15 18Z" fill="#a78bfa" />
4
+ <path d="M49 18H39L32 37V48H37.5L49 18Z" fill="#7c3aed" />
5
+ <path d="M26.5 48L32 37L37.5 48H26.5Z" fill="#ede9fe" />
6
+ <circle cx="32" cy="23.5" r="3.2" fill="#eceef2" />
7
+ </svg>
Binary file
Binary file
@@ -0,0 +1,22 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="color-scheme" content="dark light" />
7
+ <meta name="theme-color" content="#111216" />
8
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
9
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
10
+ <link rel="icon" href="/favicon.ico" sizes="any" />
11
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
12
+ <title>Vault</title>
13
+ <script type="module" crossorigin src="/assets/index-B9GrzXPf.js"></script>
14
+ <link rel="modulepreload" crossorigin href="/assets/vendor-react-E4kvVwMA.js">
15
+ <link rel="modulepreload" crossorigin href="/assets/vendor-markdown-C94ubdL7.js">
16
+ <link rel="modulepreload" crossorigin href="/assets/vendor-hljs-B8we2J8a.js">
17
+ <link rel="stylesheet" crossorigin href="/assets/index-D21lvsqQ.css">
18
+ </head>
19
+ <body>
20
+ <div id="root"></div>
21
+ </body>
22
+ </html>
@@ -0,0 +1,15 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
2
+ <!-- App squircle -->
3
+ <rect x="2" y="2" width="60" height="60" rx="15" fill="#181a20" stroke="#8b5cf6" stroke-opacity="0.45" stroke-width="2" />
4
+
5
+ <!-- Subtle inner vault ring -->
6
+ <circle cx="32" cy="32" r="21" stroke="#8b5cf6" stroke-opacity="0.18" stroke-width="1.5" />
7
+
8
+ <!-- Geometric Vault 'V' facets -->
9
+ <path d="M16 19H25.5L32 36.5L26.5 47L16 19Z" fill="#a78bfa" />
10
+ <path d="M48 19H38.5L32 36.5V47H37.5L48 19Z" fill="#7c3aed" />
11
+ <path d="M26.5 47L32 36.5L37.5 47H26.5Z" fill="#ddd6fe" />
12
+
13
+ <!-- Keyhole / lock pin -->
14
+ <circle cx="32" cy="23.5" r="3" fill="#eceef2" />
15
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
2
+ <rect x="2.5" y="2.5" width="19" height="19" rx="5" stroke="currentColor" stroke-width="1.75" />
3
+ <path d="M7.2 7.5L12 17L16.8 7.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
4
+ <circle cx="12" cy="8.5" r="1.35" fill="currentColor" />
5
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "pagedex",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Local-first Markdown knowledge viewer, editor, Git bridge, and MCP v2 server for any directory.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=22.5.0"
9
+ },
10
+ "bin": {
11
+ "pagedex": "./bin/pagedex.js"
12
+ },
13
+ "files": [
14
+ "bin/",
15
+ "src/server/",
16
+ "dist/",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "dev": "node src/server/dev.js",
21
+ "dev:server": "node --no-warnings --watch src/server/index.js",
22
+ "dev:client": "vite",
23
+ "build": "vite build",
24
+ "start": "node --no-warnings src/server/index.js",
25
+ "prepublishOnly": "vite build",
26
+ "test": "node --no-warnings --test test/*.test.js",
27
+ "smoke": "node --no-warnings test/smoke.js all",
28
+ "smoke:api": "node --no-warnings test/smoke.js api",
29
+ "smoke:mcp": "node --no-warnings test/smoke.js mcp"
30
+ },
31
+ "dependencies": {
32
+ "@hono/node-server": "^1.14.0",
33
+ "@modelcontextprotocol/client": "^2.4.0",
34
+ "@modelcontextprotocol/server": "^2.4.0",
35
+ "@tiptap/pm": "^3.34.0",
36
+ "@tiptap/react": "^3.34.0",
37
+ "@tiptap/starter-kit": "^3.34.0",
38
+ "highlight.js": "^11.12.0",
39
+ "hono": "^4.7.0",
40
+ "marked": "^15.0.0",
41
+ "react": "^19.1.0",
42
+ "react-dom": "^19.1.0",
43
+ "tiptap-markdown": "^0.9.0",
44
+ "zod": "^4.6.7"
45
+ },
46
+ "devDependencies": {
47
+ "@vitejs/plugin-react": "^4.4.0",
48
+ "vite": "^6.3.0"
49
+ }
50
+ }
@@ -0,0 +1,14 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ const procs = [
4
+ spawn('node', ['--watch', 'src/server/index.js'], { stdio: 'inherit' }),
5
+ spawn('pnpm', ['exec', 'vite'], { stdio: 'inherit' })
6
+ ];
7
+
8
+ function shutdown() {
9
+ for (const p of procs) p.kill('SIGTERM');
10
+ process.exit(0);
11
+ }
12
+
13
+ process.on('SIGINT', shutdown);
14
+ process.on('SIGTERM', shutdown);
@@ -0,0 +1,120 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { VAULT_ROOT, isProtectedPath, resolveSafe } from './paths.js';
4
+ import { invalidateVaultCache, removeVaultCacheFile, updateVaultCacheFile } from './vault-cache.js';
5
+
6
+ export function writeNote(relPath, content, { append = false } = {}) {
7
+ if (!relPath || isProtectedPath(relPath)) return null;
8
+ const full = resolveSafe(relPath);
9
+ if (!full || full === VAULT_ROOT) return null;
10
+ fs.mkdirSync(path.dirname(full), { recursive: true });
11
+ if (append && fs.existsSync(full)) {
12
+ fs.appendFileSync(full, String(content ?? ''), 'utf8');
13
+ } else {
14
+ fs.writeFileSync(full, String(content ?? ''), 'utf8');
15
+ }
16
+ const clean = relPath.replace(/^\/+/, '');
17
+ updateVaultCacheFile(clean);
18
+ return { path: clean, bytes: Buffer.byteLength(String(content ?? ''), 'utf8') };
19
+ }
20
+
21
+ export function editNote(relPath, target, replacement, { replaceAll = false } = {}) {
22
+ if (!relPath || isProtectedPath(relPath) || typeof target !== 'string' || !target) {
23
+ return { error: 'Invalid path or empty target' };
24
+ }
25
+ const full = resolveSafe(relPath);
26
+ if (!full || !fs.existsSync(full) || !fs.statSync(full).isFile()) {
27
+ return { error: 'File not found' };
28
+ }
29
+ const current = fs.readFileSync(full, 'utf8');
30
+ if (!current.includes(target)) {
31
+ return { error: 'Target not found in file' };
32
+ }
33
+ const count = current.split(target).length - 1;
34
+ if (count > 1 && !replaceAll) {
35
+ return { error: `Target matched ${count} times; pass replaceAll: true or more context` };
36
+ }
37
+ const next = replaceAll ? current.split(target).join(String(replacement ?? '')) : current.replace(target, String(replacement ?? ''));
38
+ const clean = relPath.replace(/^\/+/, '');
39
+ fs.writeFileSync(full, next, 'utf8');
40
+ updateVaultCacheFile(clean);
41
+ return {
42
+ path: clean,
43
+ replacements: replaceAll ? count : 1,
44
+ bytes: Buffer.byteLength(next, 'utf8')
45
+ };
46
+ }
47
+
48
+ export function deleteNote(relPath) {
49
+ if (!relPath || isProtectedPath(relPath)) return null;
50
+ const full = resolveSafe(relPath);
51
+ if (!full || !fs.existsSync(full) || !fs.statSync(full).isFile()) return null;
52
+ fs.unlinkSync(full);
53
+ const clean = relPath.replace(/^\/+/, '');
54
+ removeVaultCacheFile(clean);
55
+ return { deleted: true, path: clean };
56
+ }
57
+
58
+ export function renameNote(oldPath, newPath) {
59
+ if (!oldPath || !newPath || isProtectedPath(oldPath) || isProtectedPath(newPath)) {
60
+ return { error: 'Invalid or protected path' };
61
+ }
62
+ const cleanOld = oldPath.replace(/^\/+/, '');
63
+ const cleanNew = newPath.replace(/^\/+/, '');
64
+ const srcFull = resolveSafe(cleanOld);
65
+ const dstFull = resolveSafe(cleanNew);
66
+ if (!srcFull || !dstFull || srcFull === VAULT_ROOT || dstFull === VAULT_ROOT) {
67
+ return { error: 'Invalid path' };
68
+ }
69
+ if (!fs.existsSync(srcFull) || !fs.statSync(srcFull).isFile()) {
70
+ return { error: 'Source file not found' };
71
+ }
72
+ if (fs.existsSync(dstFull)) {
73
+ return { error: 'Destination already exists' };
74
+ }
75
+ fs.mkdirSync(path.dirname(dstFull), { recursive: true });
76
+ fs.renameSync(srcFull, dstFull);
77
+ removeVaultCacheFile(cleanOld);
78
+ updateVaultCacheFile(cleanNew);
79
+ return { ok: true, oldPath: cleanOld, newPath: cleanNew };
80
+ }
81
+
82
+ export function renameDir(oldPath, newPath) {
83
+ if (!oldPath || !newPath || isProtectedPath(oldPath) || isProtectedPath(newPath)) {
84
+ return { error: 'Invalid or protected path' };
85
+ }
86
+ const cleanOld = oldPath.replace(/^\/+|\/+$/g, '');
87
+ const cleanNew = newPath.replace(/^\/+|\/+$/g, '');
88
+ if (!cleanOld || !cleanNew) {
89
+ return { error: 'Invalid path' };
90
+ }
91
+ const srcFull = resolveSafe(cleanOld);
92
+ const dstFull = resolveSafe(cleanNew);
93
+ if (!srcFull || !dstFull || srcFull === VAULT_ROOT || dstFull === VAULT_ROOT) {
94
+ return { error: 'Invalid path' };
95
+ }
96
+ if (!fs.existsSync(srcFull) || !fs.statSync(srcFull).isDirectory()) {
97
+ return { error: 'Source folder not found' };
98
+ }
99
+ if (dstFull === srcFull || dstFull.startsWith(srcFull + path.sep)) {
100
+ return { error: 'Cannot move a folder into itself' };
101
+ }
102
+ if (fs.existsSync(dstFull)) {
103
+ return { error: 'Destination already exists' };
104
+ }
105
+ fs.mkdirSync(path.dirname(dstFull), { recursive: true });
106
+ fs.renameSync(srcFull, dstFull);
107
+ invalidateVaultCache();
108
+ return { ok: true, oldPath: cleanOld, newPath: cleanNew };
109
+ }
110
+
111
+ export function writeBinaryFile(relPath, buffer) {
112
+ if (!relPath || isProtectedPath(relPath)) return null;
113
+ const clean = relPath.replace(/^\/+/, '');
114
+ const full = resolveSafe(clean);
115
+ if (!full || full === VAULT_ROOT) return null;
116
+ fs.mkdirSync(path.dirname(full), { recursive: true });
117
+ fs.writeFileSync(full, buffer);
118
+ updateVaultCacheFile(clean);
119
+ return { path: clean, bytes: buffer.length };
120
+ }
@@ -0,0 +1,217 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { VAULT_ROOT, isImagePath, isProtectedPath, resolveSafe } from './paths.js';
5
+ import { removeVaultCacheFile, updateVaultCacheFile } from './vault-cache.js';
6
+
7
+ export function getGitChanges() {
8
+ try {
9
+ const out = execFileSync('git', ['status', '--porcelain', '-uall'], {
10
+ cwd: VAULT_ROOT,
11
+ encoding: 'utf8',
12
+ stdio: ['ignore', 'pipe', 'ignore']
13
+ });
14
+ const changes = out
15
+ .split('\n')
16
+ .filter(Boolean)
17
+ .map((line) => {
18
+ const rawCode = line.slice(0, 2).trim() || 'M';
19
+ let filePath = line.slice(3).trim();
20
+ if (filePath.includes(' -> ')) filePath = filePath.split(' -> ').pop();
21
+ if (filePath.startsWith('"') && filePath.endsWith('"')) {
22
+ filePath = filePath.slice(1, -1);
23
+ }
24
+ const status = rawCode === '??' ? 'U' : rawCode[0];
25
+ let mtimeMs = 0;
26
+ if (status !== 'D') {
27
+ try {
28
+ mtimeMs = fs.statSync(path.join(VAULT_ROOT, filePath)).mtimeMs;
29
+ } catch {
30
+ mtimeMs = 0;
31
+ }
32
+ }
33
+ return { status, path: filePath, mtimeMs };
34
+ })
35
+ .filter((c) => c.path && !isProtectedPath(c.path));
36
+ return { isGit: true, changes };
37
+ } catch {
38
+ return { isGit: false, changes: [] };
39
+ }
40
+ }
41
+
42
+ export function gitCommitAndPush({ message, files = [], push = false }) {
43
+ const msg = String(message || '').trim();
44
+ if (!msg) return { error: 'Commit message is required' };
45
+ try {
46
+ const safeFiles = Array.isArray(files)
47
+ ? files.filter((f) => typeof f === 'string' && f && !isProtectedPath(f) && resolveSafe(f))
48
+ : [];
49
+ if (safeFiles.length > 0) {
50
+ execFileSync('git', ['add', '--', ...safeFiles], { cwd: VAULT_ROOT, encoding: 'utf8' });
51
+ } else {
52
+ execFileSync('git', ['add', '-A'], { cwd: VAULT_ROOT, encoding: 'utf8' });
53
+ }
54
+ const commitOut = execFileSync('git', ['commit', '-m', msg], { cwd: VAULT_ROOT, encoding: 'utf8' });
55
+ let pushOut = '';
56
+ if (push) {
57
+ pushOut = execFileSync('git', ['push'], { cwd: VAULT_ROOT, encoding: 'utf8' });
58
+ }
59
+ return { ok: true, output: [commitOut, pushOut].filter(Boolean).join('\n').trim() };
60
+ } catch (err) {
61
+ const detail = err?.stderr?.toString?.() || err?.stdout?.toString?.() || err.message;
62
+ return { error: detail.trim() || 'Git command failed' };
63
+ }
64
+ }
65
+
66
+ function parseUnifiedDiff(raw) {
67
+ const lines = raw.split('\n');
68
+ const fileHeaders = [];
69
+ const hunks = [];
70
+ let current = null;
71
+ let oldLn = 0;
72
+ let newLn = 0;
73
+
74
+ for (const line of lines) {
75
+ if (line.startsWith('diff --git') || line.startsWith('index ') || line.startsWith('--- ') || line.startsWith('+++ ')) {
76
+ fileHeaders.push(line);
77
+ continue;
78
+ }
79
+ const hm = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/);
80
+ if (hm) {
81
+ oldLn = Number(hm[1]);
82
+ newLn = Number(hm[3]);
83
+ current = {
84
+ header: line,
85
+ oldStart: oldLn,
86
+ newStart: newLn,
87
+ lines: []
88
+ };
89
+ hunks.push(current);
90
+ continue;
91
+ }
92
+ if (!current) continue;
93
+ if (line.startsWith('+')) {
94
+ current.lines.push({ type: 'add', text: line.slice(1), oldLine: null, newLine: newLn++ });
95
+ } else if (line.startsWith('-')) {
96
+ current.lines.push({ type: 'del', text: line.slice(1), oldLine: oldLn++, newLine: null });
97
+ } else if (line.startsWith(' ')) {
98
+ current.lines.push({ type: 'context', text: line.slice(1), oldLine: oldLn++, newLine: newLn++ });
99
+ }
100
+ }
101
+ return { fileHeaders, hunks };
102
+ }
103
+
104
+ export function getGitDiff(relPath) {
105
+ if (!relPath || isProtectedPath(relPath) || !resolveSafe(relPath)) {
106
+ return { error: 'Invalid or protected path' };
107
+ }
108
+ const cleanPath = relPath.replace(/^\/+/, '');
109
+ try {
110
+ let raw = execFileSync('git', ['diff', '--no-color', '-U3', '--', cleanPath], {
111
+ cwd: VAULT_ROOT,
112
+ encoding: 'utf8',
113
+ stdio: ['ignore', 'pipe', 'pipe']
114
+ });
115
+ let untracked = false;
116
+ if (!raw.trim()) {
117
+ const st = execFileSync('git', ['status', '--porcelain', '--', cleanPath], {
118
+ cwd: VAULT_ROOT,
119
+ encoding: 'utf8',
120
+ stdio: ['ignore', 'pipe', 'ignore']
121
+ });
122
+ if (st.startsWith('??')) {
123
+ untracked = true;
124
+ const full = resolveSafe(cleanPath);
125
+ if (full && fs.existsSync(full) && !isImagePath(cleanPath)) {
126
+ const textLines = fs.readFileSync(full, 'utf8').split('\n');
127
+ if (textLines.at(-1) === '') textLines.pop();
128
+ raw = [
129
+ `--- /dev/null`,
130
+ `+++ b/${cleanPath}`,
131
+ `@@ -0,0 +1,${textLines.length} @@`,
132
+ ...textLines.map((l) => `+${l}`)
133
+ ].join('\n');
134
+ }
135
+ }
136
+ }
137
+ const { fileHeaders, hunks } = parseUnifiedDiff(raw);
138
+ return { path: cleanPath, untracked, fileHeaders, hunks, raw };
139
+ } catch (err) {
140
+ return { error: err?.stderr?.toString?.() || err.message };
141
+ }
142
+ }
143
+
144
+ export function discardGitChange({ path: relPath, hunkIndex, lineIndex }) {
145
+ if (!relPath || isProtectedPath(relPath)) return { error: 'Invalid or protected path' };
146
+ const cleanPath = relPath.replace(/^\/+/, '');
147
+ const full = resolveSafe(cleanPath);
148
+ if (!full) return { error: 'Invalid path' };
149
+
150
+ try {
151
+ if (hunkIndex === undefined || hunkIndex === null) {
152
+ const st = execFileSync('git', ['status', '--porcelain', '--', cleanPath], {
153
+ cwd: VAULT_ROOT,
154
+ encoding: 'utf8'
155
+ });
156
+ if (st.startsWith('??')) {
157
+ if (fs.existsSync(full)) fs.unlinkSync(full);
158
+ removeVaultCacheFile(cleanPath);
159
+ return { ok: true, path: cleanPath, discarded: 'untracked' };
160
+ }
161
+ execFileSync('git', ['checkout', '--', cleanPath], { cwd: VAULT_ROOT, encoding: 'utf8' });
162
+ updateVaultCacheFile(cleanPath);
163
+ return { ok: true, path: cleanPath, discarded: 'file' };
164
+ }
165
+
166
+ const diff = getGitDiff(cleanPath);
167
+ if (diff.error) return diff;
168
+ if (diff.untracked) return { error: 'Cannot discard single lines on an untracked file; discard whole file instead' };
169
+
170
+ const hunk = diff.hunks[Number(hunkIndex)];
171
+ if (!hunk) return { error: 'Hunk not found' };
172
+
173
+ const rawText = fs.readFileSync(full, 'utf8');
174
+ const hadTrailingNewline = rawText.endsWith('\n');
175
+ const fileLines = rawText.split('\n');
176
+ if (hadTrailingNewline) fileLines.pop();
177
+
178
+ if (lineIndex === undefined || lineIndex === null) {
179
+ const startIdx = hunk.newStart === 0 ? 0 : hunk.newStart - 1;
180
+ const deleteCount = hunk.lines.filter((l) => l.type === 'context' || l.type === 'add').length;
181
+ const restoreLines = hunk.lines
182
+ .filter((l) => l.type === 'context' || l.type === 'del')
183
+ .map((l) => l.text);
184
+ fileLines.splice(startIdx, deleteCount, ...restoreLines);
185
+ const out = fileLines.join('\n') + (hadTrailingNewline ? '\n' : '');
186
+ fs.writeFileSync(full, out, 'utf8');
187
+ updateVaultCacheFile(cleanPath);
188
+ return { ok: true, path: cleanPath, discarded: 'hunk' };
189
+ }
190
+
191
+ const targetIdx = Number(lineIndex);
192
+ const targetLine = hunk.lines[targetIdx];
193
+ if (!targetLine || targetLine.type === 'context') {
194
+ return { error: 'Line is not a change' };
195
+ }
196
+
197
+ if (targetLine.type === 'add') {
198
+ fileLines.splice(targetLine.newLine - 1, 1);
199
+ } else {
200
+ let insertAt = hunk.newStart === 0 ? 0 : hunk.newStart - 1;
201
+ for (let i = targetIdx - 1; i >= 0; i--) {
202
+ if (hunk.lines[i].newLine !== null) {
203
+ insertAt = hunk.lines[i].newLine;
204
+ break;
205
+ }
206
+ }
207
+ fileLines.splice(insertAt, 0, targetLine.text);
208
+ }
209
+
210
+ const out = fileLines.join('\n') + (hadTrailingNewline ? '\n' : '');
211
+ fs.writeFileSync(full, out, 'utf8');
212
+ updateVaultCacheFile(cleanPath);
213
+ return { ok: true, path: cleanPath, discarded: 'line' };
214
+ } catch (err) {
215
+ return { error: err?.stderr?.toString?.() || err.message };
216
+ }
217
+ }
@@ -0,0 +1,43 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { Hono } from 'hono';
4
+ import { serve } from '@hono/node-server';
5
+ import { serveStatic } from '@hono/node-server/serve-static';
6
+ import { getCachedVault } from './vault.js';
7
+ import { syncIndex } from './search-index.js';
8
+ import { handleMcpRequest } from './mcp.js';
9
+ import { registerFileRoutes } from './routes/files.js';
10
+ import { registerGitRoutes } from './routes/git.js';
11
+ import { registerEventRoutes, startVaultWatcher } from './routes/events.js';
12
+
13
+ const HOST = process.env.HOST || '127.0.0.1';
14
+ const PORT = Number(process.env.PORT || 4321);
15
+
16
+ try {
17
+ syncIndex();
18
+ getCachedVault();
19
+ } catch (err) {
20
+ console.warn('search index init warning:', err.message);
21
+ }
22
+
23
+ startVaultWatcher();
24
+
25
+ const app = new Hono();
26
+
27
+ registerFileRoutes(app);
28
+ registerGitRoutes(app);
29
+ registerEventRoutes(app);
30
+
31
+ // MCP endpoint (Streamable HTTP)
32
+ app.all('/mcp', (c) => handleMcpRequest(c.req.raw));
33
+
34
+ // Serve built React app in production (dist/)
35
+ const distDir = path.resolve(import.meta.dirname, '../../dist');
36
+ if (fs.existsSync(distDir)) {
37
+ const relDist = path.relative(process.cwd(), distDir) || '.';
38
+ app.use('/*', serveStatic({ root: relDist }));
39
+ }
40
+
41
+ serve({ fetch: app.fetch, hostname: HOST, port: PORT }, () => {
42
+ console.log(`Pagedex listening on http://${HOST}:${PORT} (root: ${process.env.PAGEDEX_ROOT || process.env.VAULT_ROOT || process.cwd()})`);
43
+ });