wiki-viewer 2.11.0 → 2.11.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.
Files changed (71) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/api/agent/activity/route.js.nft.json +1 -1
  13. package/.next/standalone/.next/server/app/api/agent/events/[...path]/route.js.nft.json +1 -1
  14. package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js.nft.json +1 -1
  16. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/api/agent/fs/move/route.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/api/agent/fs/search/route.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/api/agent/internal/span/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/agent/settings/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/agent/sidecar/[...path]/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/assets/[...path]/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/pdf/save/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/share/[token]/asset/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/share/[token]/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/share/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/system/reveal/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/branch/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/open/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/refresh/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/system/workspaces/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/upload/[...path]/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/wiki/app/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/wiki/backlinks/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/wiki/content/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/wiki/download/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/wiki/folder/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/wiki/git-branches/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/wiki/git-checkout/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/wiki/git-diff/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/wiki/git-file-info/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/wiki/git-history/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/api/wiki/git-pull/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/wiki/move/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/wiki/new-file/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/api/wiki/outlinks/route.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/api/wiki/page/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/api/wiki/presence/route.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/api/wiki/route.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/api/wiki/search/route.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/api/wiki/slugs/route.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/api/wiki/upload/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/api/wiki/watch/route.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/chunks/ssr/_0oq8kwv._.js +1 -1
  58. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  59. package/.next/standalone/.next/server/pages/500.html +1 -1
  60. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  62. package/.next/standalone/.next/static/chunks/{0qs5wsoz9l~.r.js → 0zx8l56mczo3g.js} +1 -1
  63. package/.next/standalone/package.json +1 -1
  64. package/.next/standalone/server.js +1 -1
  65. package/bin/cli/serve.js +33 -6
  66. package/bin/cli/service.js +22 -2
  67. package/bin/cli/update.js +43 -4
  68. package/package.json +1 -1
  69. /package/.next/standalone/.next/static/{QSas9vMwm9uMJRwezOW9O → CciQ9yC2bDmmOcjYpnJoI}/_buildManifest.js +0 -0
  70. /package/.next/standalone/.next/static/{QSas9vMwm9uMJRwezOW9O → CciQ9yC2bDmmOcjYpnJoI}/_clientMiddlewareManifest.js +0 -0
  71. /package/.next/standalone/.next/static/{QSas9vMwm9uMJRwezOW9O → CciQ9yC2bDmmOcjYpnJoI}/_ssgManifest.js +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-viewer",
3
- "version": "2.11.0",
3
+ "version": "2.11.2",
4
4
  "description": "Local file viewer and wiki — view markdown, HTML, PDFs, notebooks, office docs, and more from any directory",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -15,7 +15,7 @@ const currentPort = parseInt(process.env.PORT, 10) || 3000
15
15
  const hostname = process.env.HOSTNAME || '0.0.0.0'
16
16
 
17
17
  let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
18
- const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"2.11.0"},"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":true,"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/runner/work/wiki-viewer/wiki-viewer","allowedDevOrigins":["localhost"],"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/runner/work/wiki-viewer/wiki-viewer"},"distDirRoot":".next"}
18
+ const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"2.11.2"},"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":true,"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/runner/work/wiki-viewer/wiki-viewer","allowedDevOrigins":["localhost"],"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/runner/work/wiki-viewer/wiki-viewer"},"distDirRoot":".next"}
19
19
 
20
20
  process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
21
21
 
package/bin/cli/serve.js CHANGED
@@ -81,11 +81,11 @@ function unmountSync(mp) {
81
81
  }
82
82
 
83
83
  function registerMountCleanup() {
84
- const cleanup = () => { if (activeMount) { const mp = activeMount; activeMount = null; unmountSync(mp); } };
85
- process.on("exit", cleanup);
86
- for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
87
- process.on(sig, () => { cleanup(); process.exit(0); });
88
- }
84
+ // Signal handling (SIGINT/SIGTERM/SIGHUP) is centralized in start() so
85
+ // mount cleanup and child-process shutdown happen as one coordinated
86
+ // sequence instead of racing independent listeners. This only wires the
87
+ // unconditional exit-time safety net for the sshfs mount.
88
+ process.on("exit", () => { if (activeMount) { const mp = activeMount; activeMount = null; unmountSync(mp); } });
89
89
  }
90
90
 
91
91
  async function mountSshTarget({ targetStr, port, keyPath, password, readOnly }) {
@@ -367,7 +367,34 @@ export async function start(opts) {
367
367
  },
368
368
  });
369
369
 
370
- child.on("exit", (code) => process.exit(code ?? 0));
370
+ // Next's production server (startServer from next/dist/server/lib/start-server)
371
+ // does a graceful shutdown on SIGTERM/SIGINT: it waits for in-flight requests
372
+ // and idle keep-alive sockets to close naturally before actually exiting. With
373
+ // keep-alive connections held open by browser tabs, that wait can stretch out
374
+ // far longer than expected — and under systemd (Type=simple, default
375
+ // TimeoutStopSec=90s) "systemctl restart" then blocks for up to 90 seconds
376
+ // before systemd gives up and SIGKILLs it. Bound that here directly: forward
377
+ // the signal immediately, then force-kill after a short grace period so
378
+ // restarts stay fast regardless of how long Next's own drain logic wants to wait.
379
+ let childExited = false;
380
+ child.on("exit", (code) => { childExited = true; process.exit(code ?? 0); });
381
+
382
+ let shuttingDown = false;
383
+ const SHUTDOWN_GRACE_MS = 5_000;
384
+ function shutdown(signal) {
385
+ if (shuttingDown) return;
386
+ shuttingDown = true;
387
+ if (activeMount) { const mp = activeMount; activeMount = null; unmountSync(mp); }
388
+ if (childExited) return;
389
+ try { child.kill(signal); } catch { /* already gone */ }
390
+ const killTimer = setTimeout(() => {
391
+ if (!childExited) { try { child.kill("SIGKILL"); } catch { /* already gone */ } }
392
+ }, SHUTDOWN_GRACE_MS);
393
+ child.once("exit", () => clearTimeout(killTimer));
394
+ }
395
+ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
396
+ process.on(sig, () => shutdown(sig));
397
+ }
371
398
 
372
399
  if (useHttps) {
373
400
  const { key, cert } = ensureCerts(host);
@@ -2,12 +2,17 @@ import { execFileSync, execSync } from "node:child_process";
2
2
  import path from "node:path";
3
3
  import os from "node:os";
4
4
  import { fileURLToPath } from "node:url";
5
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
5
+ import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from "node:fs";
6
6
  import { configPath, logDir, loadConfig, saveConfig, parseEnvFlags } from "./config.js";
7
7
  import { computeServerEnv, parseServeArgs } from "./serve.js";
8
8
  import { looksLikeSshTarget } from "../shared/ssh-target.js";
9
9
 
10
- const selfScript = fileURLToPath(import.meta.url);
10
+ // The actual CLI entrypoint (bin/wiki-viewer.js), not this module's own file
11
+ // (bin/cli/service.js). resolveServiceScript() below needs the entrypoint
12
+ // path to build a correct systemd ExecStart / launchd ProgramArguments —
13
+ // pointing at this module itself would produce a unit that starts a script
14
+ // with no CLI dispatch, exiting immediately without ever serving anything.
15
+ const selfScript = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "wiki-viewer.js");
11
16
 
12
17
  export const SERVICE_NAME = "wiki-viewer";
13
18
  export const LAUNCHD_LABEL = "com.wiki-viewer";
@@ -58,6 +63,10 @@ Type=simple
58
63
  ExecStart=${nodeBin} ${scriptPath} service run
59
64
  Restart=on-failure
60
65
  RestartSec=3
66
+ # Our own SIGTERM/SIGKILL escalation in serve.js bounds shutdown to ~5s, but
67
+ # cap systemd's own wait too (default TimeoutStopSec is 90s) so "systemctl
68
+ # restart" can never hang longer than that regardless of process behavior.
69
+ TimeoutStopSec=10
61
70
  Environment=NODE_ENV=production
62
71
  Environment=PATH=${sysPath}${rgEnv}
63
72
 
@@ -295,6 +304,17 @@ export function serviceIsInstalled() {
295
304
  export function serviceRestart() {
296
305
  const p = platform();
297
306
  if (p === "linux") {
307
+ // Self-heal units installed before TimeoutStopSec was added: without it,
308
+ // "systemctl restart" can block up to systemd's default 90s if the
309
+ // service doesn't exit promptly on SIGTERM. Rewrite the unit in place
310
+ // (same content installSystemd would generate) before restarting.
311
+ const unitPath = path.join(os.homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`);
312
+ try {
313
+ const current = existsSync(unitPath) ? readFileSync(unitPath, "utf8") : "";
314
+ if (current && !current.includes("TimeoutStopSec")) {
315
+ installSystemd(resolveServiceNode(), resolveServiceScript());
316
+ }
317
+ } catch { /* best-effort; fall through to restart regardless */ }
298
318
  run("systemctl", ["--user", "restart", `${SERVICE_NAME}.service`]);
299
319
  } else if (p === "macos") {
300
320
  const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
package/bin/cli/update.js CHANGED
@@ -37,18 +37,57 @@ export function update() {
37
37
  if (latest) console.log(`Latest: v${latest}`);
38
38
 
39
39
  const pm = detectPackageManager();
40
- const cmd = pm === "pnpm" ? ["pnpm", ["add", "-g", `${name}@latest`, "--prefer-offline"]]
41
- : pm === "yarn" ? ["yarn", ["global", "add", `${name}@latest`]]
42
- : ["npm", ["install", "-g", `${name}@latest`, "--prefer-offline"]];
40
+ // Pin the exact resolved version (not the @latest dist-tag) and skip
41
+ // --prefer-offline: both npm and pnpm can serve a stale cached packument
42
+ // under offline-preference, which resolves the dependency graph correctly
43
+ // but then skips re-extracting files because the cache believes the
44
+ // requested version is already satisfied — leaving the old binary in
45
+ // place while printing a successful install. Installing an exact pinned
46
+ // version avoids relying on dist-tag/offline resolution altogether.
47
+ const target = latest ? `${name}@${latest}` : `${name}@latest`;
48
+ const cmd = pm === "pnpm" ? ["pnpm", ["add", "-g", target]]
49
+ : pm === "yarn" ? ["yarn", ["global", "add", target]]
50
+ : ["npm", ["install", "-g", target]];
43
51
 
44
52
  console.log(`Updating via ${pm}…`);
53
+ const runInstall = (extraArgs = []) => execFileSync(cmd[0], [...cmd[1], ...extraArgs], { stdio: "inherit" });
45
54
  try {
46
- execFileSync(cmd[0], cmd[1], { stdio: "inherit" });
55
+ runInstall();
47
56
  } catch {
48
57
  console.error(`Error: update failed. Try manually: ${cmd[0]} ${cmd[1].join(" ")}`);
49
58
  process.exit(1);
50
59
  }
51
60
 
61
+ // Verify the install actually landed. If the on-disk version still
62
+ // doesn't match what we asked for, the package manager likely skipped
63
+ // the real file write (stale cache/idempotency short-circuit) despite
64
+ // reporting success — retry once with --force to bypass that.
65
+ const readInstalledVersion = () => {
66
+ try {
67
+ return JSON.parse(readFileSync(path.join(appRoot, "package.json"), "utf8")).version;
68
+ } catch {
69
+ return null;
70
+ }
71
+ };
72
+ let installedVersion = readInstalledVersion();
73
+ if (latest && installedVersion !== latest) {
74
+ console.log("Install did not take effect on first attempt — retrying with --force…");
75
+ try {
76
+ runInstall(["--force"]);
77
+ } catch {
78
+ console.error(`Error: forced update failed. Try manually: ${cmd[0]} ${cmd[1].join(" ")} --force`);
79
+ process.exit(1);
80
+ }
81
+ installedVersion = readInstalledVersion();
82
+ if (latest && installedVersion !== latest) {
83
+ console.error(
84
+ `Error: update reported success but v${installedVersion ?? "unknown"} is installed, not v${latest}. ` +
85
+ `Try manually: ${cmd[0]} ${cmd[1].join(" ")} --force`,
86
+ );
87
+ process.exit(1);
88
+ }
89
+ }
90
+
52
91
  if (serviceIsInstalled()) {
53
92
  console.log("Restarting service…");
54
93
  try { serviceRestart(); console.log("Service restarted."); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-viewer",
3
- "version": "2.11.0",
3
+ "version": "2.11.2",
4
4
  "description": "Local file viewer and wiki — view markdown, HTML, PDFs, notebooks, office docs, and more from any directory",
5
5
  "license": "MIT",
6
6
  "type": "module",