wiki-viewer 1.0.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.
Files changed (119) hide show
  1. package/.next/standalone/.github/workflows/publish.yml +37 -0
  2. package/.next/standalone/DESIGN.md +521 -0
  3. package/.next/standalone/README.md +283 -0
  4. package/.next/standalone/bin/wiki-viewer.js +158 -0
  5. package/.next/standalone/next.config.ts +11 -0
  6. package/.next/standalone/package.json +120 -0
  7. package/.next/standalone/pnpm-lock.yaml +5654 -0
  8. package/.next/standalone/pnpm-workspace.yaml +2 -0
  9. package/.next/standalone/postcss.config.mjs +2 -0
  10. package/.next/standalone/public/logo.svg +11 -0
  11. package/.next/standalone/public/public/logo.svg +11 -0
  12. package/.next/standalone/server.js +44 -0
  13. package/.next/standalone/src/app/api/app-proxy/[...path]/route.ts +247 -0
  14. package/.next/standalone/src/app/api/assets/[...path]/route.ts +70 -0
  15. package/.next/standalone/src/app/api/system/browse/route.ts +78 -0
  16. package/.next/standalone/src/app/api/system/clear-root/route.ts +9 -0
  17. package/.next/standalone/src/app/api/system/config/route.ts +10 -0
  18. package/.next/standalone/src/app/api/system/pins/route.ts +31 -0
  19. package/.next/standalone/src/app/api/system/reveal/route.ts +28 -0
  20. package/.next/standalone/src/app/api/system/root-status/route.ts +9 -0
  21. package/.next/standalone/src/app/api/system/set-root/route.ts +23 -0
  22. package/.next/standalone/src/app/api/upload/[...path]/route.ts +80 -0
  23. package/.next/standalone/src/app/api/wiki/app/route.ts +41 -0
  24. package/.next/standalone/src/app/api/wiki/content/route.ts +60 -0
  25. package/.next/standalone/src/app/api/wiki/file/route.ts +52 -0
  26. package/.next/standalone/src/app/api/wiki/folder/route.ts +26 -0
  27. package/.next/standalone/src/app/api/wiki/move/route.ts +45 -0
  28. package/.next/standalone/src/app/api/wiki/page/route.ts +84 -0
  29. package/.next/standalone/src/app/api/wiki/route.ts +98 -0
  30. package/.next/standalone/src/app/api/wiki/slugs/route.ts +50 -0
  31. package/.next/standalone/src/app/api/wiki/upload/route.ts +87 -0
  32. package/.next/standalone/src/app/api/wiki/watch/route.ts +86 -0
  33. package/.next/standalone/src/app/globals.css +716 -0
  34. package/.next/standalone/src/app/icon.svg +11 -0
  35. package/.next/standalone/src/app/layout.tsx +35 -0
  36. package/.next/standalone/src/app/page.tsx +1509 -0
  37. package/.next/standalone/src/components/confirm-dialog.tsx +56 -0
  38. package/.next/standalone/src/components/dir-picker.tsx +393 -0
  39. package/.next/standalone/src/components/editor/bubble-menu.tsx +286 -0
  40. package/.next/standalone/src/components/editor/callout-extension.ts +36 -0
  41. package/.next/standalone/src/components/editor/csv-viewer.tsx +335 -0
  42. package/.next/standalone/src/components/editor/editor-toolbar.tsx +564 -0
  43. package/.next/standalone/src/components/editor/editor.tsx +633 -0
  44. package/.next/standalone/src/components/editor/extensions/drag-handle.ts +325 -0
  45. package/.next/standalone/src/components/editor/extensions/heading-anchors.ts +55 -0
  46. package/.next/standalone/src/components/editor/extensions/resizable-image.tsx +159 -0
  47. package/.next/standalone/src/components/editor/extensions.ts +148 -0
  48. package/.next/standalone/src/components/editor/file-fallback-viewer.tsx +74 -0
  49. package/.next/standalone/src/components/editor/folder-index.tsx +235 -0
  50. package/.next/standalone/src/components/editor/google-doc-viewer.tsx +73 -0
  51. package/.next/standalone/src/components/editor/image-viewer.tsx +56 -0
  52. package/.next/standalone/src/components/editor/link-popover.tsx +111 -0
  53. package/.next/standalone/src/components/editor/media-popover.tsx +211 -0
  54. package/.next/standalone/src/components/editor/media-viewer.tsx +69 -0
  55. package/.next/standalone/src/components/editor/mermaid-viewer.tsx +285 -0
  56. package/.next/standalone/src/components/editor/node-app-viewer.tsx +269 -0
  57. package/.next/standalone/src/components/editor/notebook-viewer.tsx +370 -0
  58. package/.next/standalone/src/components/editor/office/docx-viewer.tsx +92 -0
  59. package/.next/standalone/src/components/editor/office/office-chrome.tsx +82 -0
  60. package/.next/standalone/src/components/editor/office/pptx-viewer.tsx +103 -0
  61. package/.next/standalone/src/components/editor/office/xlsx-viewer.tsx +116 -0
  62. package/.next/standalone/src/components/editor/pdf-viewer.tsx +31 -0
  63. package/.next/standalone/src/components/editor/slash-commands.tsx +454 -0
  64. package/.next/standalone/src/components/editor/source-viewer.tsx +212 -0
  65. package/.next/standalone/src/components/editor/table-menu.tsx +232 -0
  66. package/.next/standalone/src/components/editor/website-viewer.tsx +74 -0
  67. package/.next/standalone/src/components/editor/wiki-link-create-dialog.tsx +188 -0
  68. package/.next/standalone/src/components/editor/wiki-link-decorator.ts +79 -0
  69. package/.next/standalone/src/components/editor/wiki-link-extension.ts +120 -0
  70. package/.next/standalone/src/components/editor/wiki-link-picker.tsx +250 -0
  71. package/.next/standalone/src/components/layout/viewer-toolbar.tsx +54 -0
  72. package/.next/standalone/src/components/theme-provider.tsx +16 -0
  73. package/.next/standalone/src/components/theme-toggle.tsx +24 -0
  74. package/.next/standalone/src/components/ui/badge.tsx +35 -0
  75. package/.next/standalone/src/components/ui/button.tsx +56 -0
  76. package/.next/standalone/src/components/ui/card.tsx +63 -0
  77. package/.next/standalone/src/components/ui/collapsible.tsx +9 -0
  78. package/.next/standalone/src/components/ui/command.tsx +150 -0
  79. package/.next/standalone/src/components/ui/context-menu.tsx +191 -0
  80. package/.next/standalone/src/components/ui/dialog.tsx +119 -0
  81. package/.next/standalone/src/components/ui/dropdown-menu.tsx +194 -0
  82. package/.next/standalone/src/components/ui/input.tsx +22 -0
  83. package/.next/standalone/src/components/ui/label.tsx +25 -0
  84. package/.next/standalone/src/components/ui/popover.tsx +33 -0
  85. package/.next/standalone/src/components/ui/scroll-area.tsx +47 -0
  86. package/.next/standalone/src/components/ui/select.tsx +156 -0
  87. package/.next/standalone/src/components/ui/separator.tsx +30 -0
  88. package/.next/standalone/src/components/ui/sheet.tsx +140 -0
  89. package/.next/standalone/src/components/ui/skeleton.tsx +15 -0
  90. package/.next/standalone/src/components/ui/switch.tsx +28 -0
  91. package/.next/standalone/src/components/ui/tabs.tsx +54 -0
  92. package/.next/standalone/src/components/ui/textarea.tsx +21 -0
  93. package/.next/standalone/src/components/ui/tip.tsx +23 -0
  94. package/.next/standalone/src/components/ui/toggle-group.tsx +41 -0
  95. package/.next/standalone/src/components/ui/tooltip.tsx +27 -0
  96. package/.next/standalone/src/components/wiki/frontmatter-header.tsx +164 -0
  97. package/.next/standalone/src/lib/app-runner.ts +231 -0
  98. package/.next/standalone/src/lib/cabinets/tree.ts +75 -0
  99. package/.next/standalone/src/lib/config.ts +32 -0
  100. package/.next/standalone/src/lib/embeds/detect.ts +142 -0
  101. package/.next/standalone/src/lib/google/detect.ts +107 -0
  102. package/.next/standalone/src/lib/markdown/parse-frontmatter.ts +84 -0
  103. package/.next/standalone/src/lib/markdown/remark-wikilinks.ts +100 -0
  104. package/.next/standalone/src/lib/markdown/to-html.ts +168 -0
  105. package/.next/standalone/src/lib/markdown/to-markdown.ts +238 -0
  106. package/.next/standalone/src/lib/root-dir.ts +35 -0
  107. package/.next/standalone/src/lib/toast.ts +9 -0
  108. package/.next/standalone/src/lib/utils.ts +6 -0
  109. package/.next/standalone/src/lib/wiki-helpers.ts +36 -0
  110. package/.next/standalone/src/stores/ai-panel-store.ts +23 -0
  111. package/.next/standalone/src/stores/editor-store.ts +244 -0
  112. package/.next/standalone/src/stores/tree-store.ts +52 -0
  113. package/.next/standalone/src/stores/wiki-slugs-store.ts +74 -0
  114. package/.next/standalone/src/types/index.ts +47 -0
  115. package/.next/standalone/tailwind.config.ts +201 -0
  116. package/.next/standalone/tsconfig.json +41 -0
  117. package/README.md +283 -0
  118. package/bin/wiki-viewer.js +158 -0
  119. package/package.json +120 -0
@@ -0,0 +1,2 @@
1
+ allowBuilds:
2
+ sharp: set this to true or false
@@ -0,0 +1,2 @@
1
+ const config = { plugins: { tailwindcss: {}, autoprefixer: {} } };
2
+ export default config;
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
2
+ <rect x="15" y="15" width="70" height="70" rx="8" fill="#292524"/>
3
+
4
+ <rect x="23" y="29" width="46" height="8" rx="2" fill="#ffffff"/>
5
+ <rect x="23" y="45" width="34" height="8" rx="2" fill="#ffffff"/>
6
+
7
+ <rect x="23" y="61" width="16" height="8" rx="2" fill="#ffffff"/>
8
+ <rect x="43" y="61" width="28" height="8" rx="2" fill="#ffffff"/>
9
+
10
+ <path d="M 69 57 L 78 65 L 69 73 Z" fill="#ffffff"/>
11
+ </svg>
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
2
+ <rect x="15" y="15" width="70" height="70" rx="8" fill="#292524"/>
3
+
4
+ <rect x="23" y="29" width="46" height="8" rx="2" fill="#ffffff"/>
5
+ <rect x="23" y="45" width="34" height="8" rx="2" fill="#ffffff"/>
6
+
7
+ <rect x="23" y="61" width="16" height="8" rx="2" fill="#ffffff"/>
8
+ <rect x="43" y="61" width="28" height="8" rx="2" fill="#ffffff"/>
9
+
10
+ <path d="M 69 57 L 78 65 L 69 73 Z" fill="#ffffff"/>
11
+ </svg>
@@ -0,0 +1,44 @@
1
+ performance.mark('next-start');
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import module from 'node:module'
5
+ const require = module.createRequire(import.meta.url)
6
+ const __dirname = fileURLToPath(new URL('.', import.meta.url))
7
+
8
+
9
+ const dir = path.join(__dirname)
10
+
11
+ process.env.NODE_ENV = 'production'
12
+ process.chdir(__dirname)
13
+
14
+ const currentPort = parseInt(process.env.PORT, 10) || 3000
15
+ const hostname = process.env.HOSTNAME || '0.0.0.0'
16
+
17
+ let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
18
+ const nextConfig = {"env":{},"webpack":null,"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":false,"customCacheHandler":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{"serverFunctions":true,"browserToTerminal":"warn"},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/home/sil/wiki-viewer","allowedDevOrigins":["devvm"],"cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"appNewScrollHandler":false,"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"cachedNavigations":false,"partialFallbacks":false,"dynamicOnHover":false,"varyParams":false,"prefetchInlining":false,"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":3,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"imgOptSkipMetadata":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"strictRouteTypes":false,"viewTransition":false,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":false,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":true,"serverComponentsHmrCache":true,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"gestureTransition":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":"warn","lockDistDir":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":false,"turbopackInferModuleSideEffects":true,"turbopackPluginRuntimeStrategy":"childProcesses","optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","turbopack":{"root":"/home/sil/wiki-viewer"},"distDirRoot":".next"}
19
+
20
+ process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
21
+
22
+ require('next')
23
+ const { startServer } = require('next/dist/server/lib/start-server')
24
+
25
+ if (
26
+ Number.isNaN(keepAliveTimeout) ||
27
+ !Number.isFinite(keepAliveTimeout) ||
28
+ keepAliveTimeout < 0
29
+ ) {
30
+ keepAliveTimeout = undefined
31
+ }
32
+
33
+ startServer({
34
+ dir,
35
+ isDev: false,
36
+ config: nextConfig,
37
+ hostname,
38
+ port: currentPort,
39
+ allowRetry: false,
40
+ keepAliveTimeout,
41
+ }).catch((err) => {
42
+ console.error(err);
43
+ process.exit(1);
44
+ });
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Reverse proxy for node-app directories — built on undici.request()
3
+ *
4
+ * Unlike fetch(), undici.request() does NOT auto-decompress, so compressed
5
+ * assets (gzip/br) stream through with their Content-Encoding intact and the
6
+ * browser handles decompression itself. No ERR_CONTENT_DECODING_FAILED.
7
+ *
8
+ * For HTML/CSS (which we rewrite) we force accept-encoding:identity upstream
9
+ * so we always receive plain text we can safely manipulate.
10
+ */
11
+ import { Readable } from "node:stream";
12
+ import { request as undiciRequest } from "undici";
13
+ import type { Dispatcher } from "undici";
14
+ import { NextResponse } from "next/server";
15
+ import { resolveByPrefix } from "@/lib/app-runner";
16
+
17
+ const HOP_BY_HOP = new Set([
18
+ "connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
19
+ "te", "trailers", "transfer-encoding", "upgrade",
20
+ ]);
21
+
22
+ // ── service worker ────────────────────────────────────────────────────────────
23
+
24
+ function makeServiceWorker(proxyBase: string): string {
25
+ return `
26
+ /* wiki-viewer injected service worker */
27
+ const BASE = ${JSON.stringify(proxyBase)};
28
+ self.addEventListener("install", () => self.skipWaiting());
29
+ self.addEventListener("activate", (e) => e.waitUntil(self.clients.claim()));
30
+ self.addEventListener("fetch", (event) => {
31
+ const url = new URL(event.request.url);
32
+ if (url.origin !== self.location.origin) return;
33
+ if (url.pathname.startsWith(BASE + "/")) return;
34
+ if (url.pathname === BASE + "/sw-proxy.js") return;
35
+ event.respondWith(fetch(BASE + url.pathname + url.search, {
36
+ method: event.request.method,
37
+ headers: event.request.headers,
38
+ body: ["GET","HEAD"].includes(event.request.method) ? undefined : event.request.body,
39
+ credentials: event.request.credentials,
40
+ }));
41
+ });`.trim();
42
+ }
43
+
44
+ // ── rewriters ─────────────────────────────────────────────────────────────────
45
+
46
+ function rewriteHtml(html: string, proxyBase: string): string {
47
+ let out = html;
48
+ out = out.replace(/((?:src|href|action|data-src|data-href|content)=")\/(?!\/)/g, `$1${proxyBase}/`);
49
+ out = out.replace(/(srcset="[^"]*)\/(?!\/)/g, `$1${proxyBase}/`);
50
+ out = out.replace(/(<head(?:\s[^>]*)?>)/i, `$1\n<base href="${proxyBase}/">`);
51
+ // Inject before </head>. Works without service workers (non-localhost HTTP):
52
+ //
53
+ // 1. replaceState — strips the proxy prefix from the initial URL so
54
+ // BrowserRouter sees "/" instead of "/api/app-proxy/apps/X"
55
+ // 2. fetch/XHR overrides — rewrite absolute-path calls (/api/...) through
56
+ // the proxy so they reach the upstream app, not wiki-viewer
57
+ // 3. SW — best-effort (only works on localhost/HTTPS), provides navigation
58
+ // interception for hard-refreshes on sub-routes
59
+ const patches = `<script>
60
+ (function(){
61
+ var BASE = ${JSON.stringify(proxyBase)};
62
+ // 1. Normalize initial URL: BrowserRouter will see "/" not "/api/app-proxy/apps/X"
63
+ var _loc = location.pathname;
64
+ if (_loc.startsWith(BASE)) {
65
+ history.replaceState(history.state, '', _loc.slice(BASE.length) || '/');
66
+ }
67
+ // 2. Rewrite fetch() absolute paths through proxy (works on non-localhost HTTP)
68
+ var _fetch = window.fetch;
69
+ window.fetch = function(input, init) {
70
+ if (typeof input === 'string' && input.startsWith('/') && !input.startsWith('/api/app-proxy/')) {
71
+ input = BASE + input;
72
+ }
73
+ return _fetch.call(window, input, init);
74
+ };
75
+ // 3. Rewrite XMLHttpRequest absolute paths
76
+ var _xhrOpen = XMLHttpRequest.prototype.open;
77
+ XMLHttpRequest.prototype.open = function(method, url) {
78
+ if (typeof url === 'string' && url.startsWith('/') && !url.startsWith('/api/app-proxy/')) {
79
+ url = BASE + url;
80
+ }
81
+ return _xhrOpen.apply(this, [method, url].concat(Array.prototype.slice.call(arguments, 2)));
82
+ };
83
+ // 4. SW — best-effort for localhost/HTTPS; reloads once on first activation
84
+ if ('serviceWorker' in navigator) {
85
+ if (!navigator.serviceWorker.controller) {
86
+ navigator.serviceWorker.addEventListener('controllerchange', function() {
87
+ window.location.reload();
88
+ }, { once: true });
89
+ }
90
+ navigator.serviceWorker.register(BASE + '/sw-proxy.js?v=2', { scope: BASE + '/' })
91
+ .catch(function(){});
92
+ }
93
+ })();
94
+ </script>`;
95
+ out = out.replace(/<\/head>/i, `${patches}\n</head>`);
96
+ return out;
97
+ }
98
+
99
+ function rewriteCss(css: string, proxyBase: string): string {
100
+ return css.replace(/url\((['"]?)\/(?!\/)/g, `url($1${proxyBase}/`);
101
+ }
102
+
103
+ // ── header helpers ────────────────────────────────────────────────────────────
104
+
105
+ function upstreamHeaders(
106
+ src: Headers,
107
+ port: number,
108
+ reqUrl: URL,
109
+ forceIdentity = false,
110
+ ): Record<string, string> {
111
+ const out: Record<string, string> = {};
112
+ for (const [k, v] of src.entries()) {
113
+ if (HOP_BY_HOP.has(k.toLowerCase())) continue;
114
+ out[k] = v;
115
+ }
116
+ out["host"] = `localhost:${port}`;
117
+ out["x-forwarded-host"] = reqUrl.host;
118
+ out["x-forwarded-proto"] = reqUrl.protocol.replace(":", "");
119
+ if (forceIdentity) out["accept-encoding"] = "identity";
120
+ return out;
121
+ }
122
+
123
+ function buildResHeaders(raw: Dispatcher.ResponseData["headers"]): Headers {
124
+ const out = new Headers();
125
+ for (const [k, v] of Object.entries(raw)) {
126
+ if (!v || HOP_BY_HOP.has(k.toLowerCase())) continue;
127
+ const vals = Array.isArray(v) ? v : [v];
128
+ for (const val of vals) out.append(k, val);
129
+ }
130
+ return out;
131
+ }
132
+
133
+ // ── handler ───────────────────────────────────────────────────────────────────
134
+
135
+ async function handleProxy(
136
+ request: Request,
137
+ { params }: { params: Promise<{ path: string[] }> },
138
+ ): Promise<Response> {
139
+ const segments = (await params).path ?? [];
140
+ const reqUrl = new URL(request.url);
141
+
142
+ const resolved = resolveByPrefix(segments);
143
+ if (!resolved) {
144
+ return NextResponse.json(
145
+ { error: "App not running — launch it first in wiki-viewer." },
146
+ { status: 503 },
147
+ );
148
+ }
149
+
150
+ const { port, relPath, rest } = resolved;
151
+ const proxyBase = `/api/app-proxy/${relPath}`;
152
+
153
+ if (rest === "/sw-proxy.js") {
154
+ return new Response(makeServiceWorker(proxyBase), {
155
+ status: 200,
156
+ headers: {
157
+ "content-type": "application/javascript; charset=utf-8",
158
+ "service-worker-allowed": proxyBase + "/",
159
+ "cache-control": "no-store",
160
+ },
161
+ });
162
+ }
163
+
164
+ const upstreamUrl = `http://localhost:${port}${rest}${reqUrl.search}`;
165
+ const method = request.method as Dispatcher.HttpMethod;
166
+ const isBodyless = ["GET", "HEAD"].includes(request.method);
167
+
168
+ try {
169
+ // First pass with normal headers to discover content-type
170
+ const first = await undiciRequest(upstreamUrl, {
171
+ method,
172
+ headers: upstreamHeaders(request.headers, port, reqUrl),
173
+ body: isBodyless ? null : (request.body as unknown as Readable),
174
+ });
175
+
176
+ const contentType = String(first.headers["content-type"] ?? "");
177
+ const needsRewrite = contentType.includes("text/html") || contentType.includes("text/css");
178
+
179
+ if (needsRewrite) {
180
+ // Drain first response and re-fetch with identity encoding for plain text
181
+ first.body.resume();
182
+ const second = await undiciRequest(upstreamUrl, {
183
+ method: isBodyless ? method : "GET",
184
+ headers: upstreamHeaders(request.headers, port, reqUrl, true),
185
+ body: null,
186
+ });
187
+ const text = await second.body.text();
188
+ const resHeaders = buildResHeaders(second.headers);
189
+ // Body changed size after rewriting — drop these or client truncates
190
+ resHeaders.delete("content-encoding");
191
+ resHeaders.delete("content-length");
192
+
193
+ if (contentType.includes("text/html")) {
194
+ resHeaders.set("content-type", "text/html; charset=utf-8");
195
+ return new Response(rewriteHtml(text, proxyBase), { status: second.statusCode, headers: resHeaders });
196
+ }
197
+ resHeaders.set("content-type", contentType);
198
+ return new Response(rewriteCss(text, proxyBase), { status: second.statusCode, headers: resHeaders });
199
+ }
200
+
201
+ // SPA fallback: if upstream returns 404 for a path with no file extension
202
+ // (i.e. a client-side route), re-fetch "/" and return index.html so the
203
+ // SPA's router can handle it client-side.
204
+ const hasExt = /\.[a-z0-9]{1,8}$/i.test(rest.split("?")[0]);
205
+ if (first.statusCode === 404 && !hasExt) {
206
+ first.body.resume();
207
+ const fallback = await undiciRequest(`http://localhost:${port}/`, {
208
+ method: "GET",
209
+ headers: upstreamHeaders(request.headers, port, reqUrl, true),
210
+ body: null,
211
+ });
212
+ const fallbackText = await fallback.body.text();
213
+ const fallbackHeaders = buildResHeaders(fallback.headers);
214
+ fallbackHeaders.delete("content-encoding");
215
+ fallbackHeaders.delete("content-length");
216
+ fallbackHeaders.set("content-type", "text/html; charset=utf-8");
217
+ return new Response(rewriteHtml(fallbackText, proxyBase), {
218
+ status: 200,
219
+ headers: fallbackHeaders,
220
+ });
221
+ }
222
+
223
+ // 304/204/205: null-body statuses — Response constructor rejects a stream body
224
+ if (new Set([101, 204, 205, 304]).has(first.statusCode)) {
225
+ first.body.resume();
226
+ return new Response(null, { status: first.statusCode, headers: buildResHeaders(first.headers) });
227
+ }
228
+
229
+ // Stream everything else — compressed bytes + Content-Encoding flow through intact
230
+ return new Response(Readable.toWeb(first.body) as ReadableStream, {
231
+ status: first.statusCode,
232
+ headers: buildResHeaders(first.headers),
233
+ });
234
+
235
+ } catch (e) {
236
+ return NextResponse.json({ error: `Upstream unreachable: ${e}` }, { status: 502 });
237
+ }
238
+ }
239
+
240
+ export const GET = handleProxy;
241
+ export const POST = handleProxy;
242
+ export const PUT = handleProxy;
243
+ export const DELETE = handleProxy;
244
+ export const PATCH = handleProxy;
245
+ export const HEAD = handleProxy;
246
+ export const OPTIONS = handleProxy;
247
+ export const dynamic = "force-dynamic";
@@ -0,0 +1,70 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { NextResponse } from "next/server";
4
+ import { getRootDir } from "@/lib/root-dir";
5
+
6
+ const MIME_MAP: Record<string, string> = {
7
+ jpg: "image/jpeg",
8
+ jpeg: "image/jpeg",
9
+ png: "image/png",
10
+ gif: "image/gif",
11
+ webp: "image/webp",
12
+ svg: "image/svg+xml",
13
+ avif: "image/avif",
14
+ ico: "image/x-icon",
15
+ bmp: "image/bmp",
16
+ pdf: "application/pdf",
17
+ txt: "text/plain; charset=utf-8",
18
+ md: "text/markdown; charset=utf-8",
19
+ mp4: "video/mp4",
20
+ webm: "video/webm",
21
+ mov: "video/quicktime",
22
+ m4v: "video/mp4",
23
+ mp3: "audio/mpeg",
24
+ wav: "audio/wav",
25
+ ogg: "audio/ogg",
26
+ m4a: "audio/mp4",
27
+ aac: "audio/aac",
28
+ ipynb: "application/json",
29
+ json: "application/json",
30
+ js: "text/javascript",
31
+ ts: "text/plain",
32
+ css: "text/css",
33
+ html: "text/html",
34
+ mmd: "text/plain",
35
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
36
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
37
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
38
+ };
39
+
40
+ export async function GET(
41
+ _request: Request,
42
+ { params }: { params: Promise<{ path: string[] }> },
43
+ ) {
44
+ const segments = (await params).path;
45
+ const rel = segments.join("/");
46
+
47
+ // Path traversal guard
48
+ const resolved = path.resolve(getRootDir(), rel);
49
+ if (resolved !== getRootDir() && !resolved.startsWith(getRootDir() + path.sep)) {
50
+ return NextResponse.json({ error: "Invalid path" }, { status: 400 });
51
+ }
52
+
53
+ try {
54
+ const info = await stat(resolved);
55
+ if (info.isDirectory())
56
+ return NextResponse.json({ error: "Not a file" }, { status: 400 });
57
+
58
+ const ext = path.extname(resolved).slice(1).toLowerCase();
59
+ const contentType = MIME_MAP[ext] ?? "application/octet-stream";
60
+ const buffer = await readFile(resolved);
61
+ return new Response(buffer, {
62
+ headers: {
63
+ "Content-Type": contentType,
64
+ "Cache-Control": "private, max-age=60",
65
+ },
66
+ });
67
+ } catch {
68
+ return NextResponse.json({ error: "File not found" }, { status: 404 });
69
+ }
70
+ }
@@ -0,0 +1,78 @@
1
+ import { readdir, stat } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { NextResponse } from "next/server";
5
+
6
+ interface BrowseEntry {
7
+ name: string;
8
+ path: string;
9
+ accessible: boolean;
10
+ }
11
+
12
+ export async function GET(request: Request) {
13
+ const { searchParams } = new URL(request.url);
14
+ const rawPath = searchParams.get("path");
15
+
16
+ // Default to home dir
17
+ const target = rawPath ? path.resolve(rawPath) : os.homedir();
18
+
19
+ // Verify it's a directory
20
+ try {
21
+ const info = await stat(target);
22
+ if (!info.isDirectory()) {
23
+ return NextResponse.json({ error: "Not a directory" }, { status: 400 });
24
+ }
25
+ } catch {
26
+ return NextResponse.json({ error: "Path not found" }, { status: 404 });
27
+ }
28
+
29
+ // List entries — only directories
30
+ let entries: BrowseEntry[] = [];
31
+ try {
32
+ const names = await readdir(target);
33
+ const infos = await Promise.allSettled(
34
+ names.map(async (name) => {
35
+ const full = path.join(target, name);
36
+ const info = await stat(full);
37
+ return { name, full, isDir: info.isDirectory() };
38
+ }),
39
+ );
40
+ entries = infos
41
+ .filter(
42
+ (r): r is PromiseFulfilledResult<{ name: string; full: string; isDir: boolean }> =>
43
+ r.status === "fulfilled" && r.value.isDir,
44
+ )
45
+ .map((r) => ({ name: r.value.name, path: r.value.full, accessible: true }))
46
+ .sort((a, b) => a.name.localeCompare(b.name));
47
+ } catch {
48
+ // Permission denied or similar — return empty list but still allow selection
49
+ }
50
+
51
+ // Parent path (null at filesystem root)
52
+ const parent = target === path.parse(target).root ? null : path.dirname(target);
53
+
54
+ // Quick-access shortcuts
55
+ const home = os.homedir();
56
+ const shortcuts = [
57
+ { label: "Home", path: home },
58
+ { label: "Root", path: "/" },
59
+ ];
60
+ // Add common paths that exist
61
+ for (const [label, p] of [
62
+ ["Desktop", path.join(home, "Desktop")],
63
+ ["Documents", path.join(home, "Documents")],
64
+ ["Downloads", path.join(home, "Downloads")],
65
+ ] as [string, string][]) {
66
+ try {
67
+ await stat(p);
68
+ shortcuts.push({ label, path: p });
69
+ } catch {}
70
+ }
71
+
72
+ return NextResponse.json({
73
+ path: target,
74
+ parent,
75
+ entries,
76
+ shortcuts,
77
+ });
78
+ }
@@ -0,0 +1,9 @@
1
+ import { NextResponse } from "next/server";
2
+ import { writeConfig } from "@/lib/config";
3
+ import { clearRootDir } from "@/lib/root-dir";
4
+
5
+ export async function POST() {
6
+ clearRootDir();
7
+ await writeConfig({ lastOpenedPath: undefined });
8
+ return NextResponse.json({ ok: true });
9
+ }
@@ -0,0 +1,10 @@
1
+ import { NextResponse } from "next/server";
2
+ import { readConfig } from "@/lib/config";
3
+
4
+ export async function GET() {
5
+ const config = await readConfig();
6
+ return NextResponse.json({
7
+ pinnedPaths: config.pinnedPaths ?? [],
8
+ lastOpenedPath: config.lastOpenedPath ?? null,
9
+ });
10
+ }
@@ -0,0 +1,31 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { NextResponse } from "next/server";
3
+ import { readConfig, writeConfig } from "@/lib/config";
4
+
5
+ export async function POST(request: Request) {
6
+ const body: { path?: string; action?: "pin" | "unpin" } = await request.json();
7
+ const p = body.path?.trim();
8
+ const action = body.action ?? "pin";
9
+ if (!p) return NextResponse.json({ error: "Missing path" }, { status: 400 });
10
+
11
+ if (action === "pin") {
12
+ try {
13
+ const info = await stat(p);
14
+ if (!info.isDirectory())
15
+ return NextResponse.json({ error: "Not a directory" }, { status: 400 });
16
+ } catch {
17
+ return NextResponse.json({ error: "Path not found" }, { status: 404 });
18
+ }
19
+ const config = await readConfig();
20
+ const pins = config.pinnedPaths ?? [];
21
+ if (!pins.includes(p)) {
22
+ await writeConfig({ pinnedPaths: [...pins, p] });
23
+ }
24
+ } else {
25
+ const config = await readConfig();
26
+ const pins = (config.pinnedPaths ?? []).filter((x) => x !== p);
27
+ await writeConfig({ pinnedPaths: pins });
28
+ }
29
+
30
+ return NextResponse.json({ ok: true });
31
+ }
@@ -0,0 +1,28 @@
1
+ import { exec } from "node:child_process";
2
+ import path from "node:path";
3
+ import { NextResponse } from "next/server";
4
+ import { getRootDir } from "@/lib/root-dir";
5
+
6
+ export async function POST(request: Request) {
7
+ const body: { path?: string } = await request.json();
8
+ const rel = body.path;
9
+ if (!rel || typeof rel !== "string")
10
+ return NextResponse.json({ error: "Invalid path" }, { status: 400 });
11
+
12
+ // Path traversal guard
13
+ const resolved = path.resolve(getRootDir(), rel);
14
+ if (resolved !== getRootDir() && !resolved.startsWith(getRootDir() + path.sep))
15
+ return NextResponse.json({ error: "Invalid path" }, { status: 400 });
16
+
17
+ // Open in system file manager
18
+ const platform = process.platform;
19
+ const cmd =
20
+ platform === "darwin"
21
+ ? `open -R "${resolved}"`
22
+ : platform === "win32"
23
+ ? `explorer /select,"${resolved}"`
24
+ : `xdg-open "${path.dirname(resolved)}"`;
25
+
26
+ exec(cmd, () => {});
27
+ return NextResponse.json({ ok: true });
28
+ }
@@ -0,0 +1,9 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getRootDir, isRootDirSet } from "@/lib/root-dir";
3
+
4
+ export async function GET() {
5
+ return NextResponse.json({
6
+ configured: isRootDirSet(),
7
+ path: isRootDirSet() ? getRootDir() : null,
8
+ });
9
+ }
@@ -0,0 +1,23 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { NextResponse } from "next/server";
3
+ import { writeConfig } from "@/lib/config";
4
+ import { setRootDir } from "@/lib/root-dir";
5
+
6
+ export async function POST(request: Request) {
7
+ const body: { path?: string } = await request.json();
8
+ const dir = body.path?.trim();
9
+ if (!dir) return NextResponse.json({ error: "Missing path" }, { status: 400 });
10
+
11
+ // Verify it exists and is a directory
12
+ try {
13
+ const info = await stat(dir);
14
+ if (!info.isDirectory())
15
+ return NextResponse.json({ error: "Not a directory" }, { status: 400 });
16
+ } catch {
17
+ return NextResponse.json({ error: "Directory not found" }, { status: 404 });
18
+ }
19
+
20
+ setRootDir(dir);
21
+ await writeConfig({ lastOpenedPath: dir });
22
+ return NextResponse.json({ ok: true, path: dir });
23
+ }