wiki-viewer 1.0.0 → 1.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.
Files changed (128) hide show
  1. package/.next/standalone/README.md +365 -99
  2. package/.next/standalone/agents/bootstrap-prompt.md +1 -0
  3. package/.next/standalone/agents/wiki-viewer-skill/SKILL.md +236 -0
  4. package/.next/standalone/bin/wiki-viewer.js +431 -33
  5. package/.next/standalone/docs/agent-collab-plan.md +1615 -0
  6. package/.next/standalone/docs/agent-collab-v2-plan.md +771 -0
  7. package/.next/standalone/package.json +19 -2
  8. package/.next/standalone/pnpm-lock.yaml +1368 -325
  9. package/.next/standalone/pnpm-workspace.yaml +7 -1
  10. package/.next/standalone/src/app/api/agent/activity/route.ts +58 -0
  11. package/.next/standalone/src/app/api/agent/admin/agents/[agentId]/revoke/route.ts +40 -0
  12. package/.next/standalone/src/app/api/agent/admin/agents/route.ts +33 -0
  13. package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/approve/route.ts +83 -0
  14. package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/deny/route.ts +36 -0
  15. package/.next/standalone/src/app/api/agent/admin/registrations/route.ts +27 -0
  16. package/.next/standalone/src/app/api/agent/events/[...path]/route.ts +136 -0
  17. package/.next/standalone/src/app/api/agent/files/[...path]/route.ts +202 -0
  18. package/.next/standalone/src/app/api/agent/internal/span/route.ts +117 -0
  19. package/.next/standalone/src/app/api/agent/register/[regId]/route.ts +50 -0
  20. package/.next/standalone/src/app/api/agent/register/route.ts +110 -0
  21. package/.next/standalone/src/app/api/agent/settings/route.ts +30 -0
  22. package/.next/standalone/src/app/api/agent/settings/token/regenerate/route.ts +20 -0
  23. package/.next/standalone/src/app/api/agent/sidecar/[...path]/route.ts +49 -0
  24. package/.next/standalone/src/app/api/agents/install/route.ts +83 -0
  25. package/.next/standalone/src/app/api/agents/skill/route.ts +20 -0
  26. package/.next/standalone/src/app/api/agents/skill.tar.gz/route.ts +87 -0
  27. package/.next/standalone/src/app/api/auth/[...all]/route.ts +7 -0
  28. package/.next/standalone/src/app/api/owner/init/route.ts +14 -0
  29. package/.next/standalone/src/app/api/system/auth-settings/route.ts +85 -0
  30. package/.next/standalone/src/app/api/system/browse/route.ts +4 -0
  31. package/.next/standalone/src/app/api/system/clear-root/route.ts +8 -1
  32. package/.next/standalone/src/app/api/system/config/route.ts +5 -1
  33. package/.next/standalone/src/app/api/system/pins/route.ts +7 -0
  34. package/.next/standalone/src/app/api/system/reveal/route.ts +7 -0
  35. package/.next/standalone/src/app/api/system/root-status/route.ts +5 -1
  36. package/.next/standalone/src/app/api/system/set-root/route.ts +7 -0
  37. package/.next/standalone/src/app/api/wiki/app/route.ts +15 -0
  38. package/.next/standalone/src/app/api/wiki/content/route.ts +110 -11
  39. package/.next/standalone/src/app/api/wiki/folder/route.ts +7 -0
  40. package/.next/standalone/src/app/api/wiki/move/route.ts +7 -0
  41. package/.next/standalone/src/app/api/wiki/new-file/route.ts +55 -0
  42. package/.next/standalone/src/app/api/wiki/page/route.ts +7 -0
  43. package/.next/standalone/src/app/api/wiki/route.ts +10 -0
  44. package/.next/standalone/src/app/api/wiki/slugs/route.ts +5 -1
  45. package/.next/standalone/src/app/api/wiki/upload/route.ts +7 -0
  46. package/.next/standalone/src/app/api/wiki/watch/route.ts +6 -1
  47. package/.next/standalone/src/app/globals.css +70 -0
  48. package/.next/standalone/src/app/page.tsx +461 -217
  49. package/.next/standalone/src/app/signin/page.tsx +217 -0
  50. package/.next/standalone/src/components/ai-panel/activity-row.tsx +64 -0
  51. package/.next/standalone/src/components/ai-panel/ai-panel.tsx +322 -0
  52. package/.next/standalone/src/components/ai-panel/token-section.tsx +312 -0
  53. package/.next/standalone/src/components/auth-settings-sheet.tsx +209 -0
  54. package/.next/standalone/src/components/editor/bubble-menu.tsx +41 -2
  55. package/.next/standalone/src/components/editor/comment-pip.tsx +56 -0
  56. package/.next/standalone/src/components/editor/comment-thread.tsx +252 -0
  57. package/.next/standalone/src/components/editor/editor.tsx +513 -10
  58. package/.next/standalone/src/components/editor/extensions/proof-span.ts +60 -0
  59. package/.next/standalone/src/components/editor/extensions.ts +2 -0
  60. package/.next/standalone/src/components/editor/proof-span-popover.tsx +161 -0
  61. package/.next/standalone/src/components/editor/suggest-edit-popover.tsx +228 -0
  62. package/.next/standalone/src/components/editor/suggestion-card.tsx +201 -0
  63. package/.next/standalone/src/components/view-width-toggle.tsx +47 -0
  64. package/.next/standalone/src/components/wiki/markdown-preview.tsx +120 -0
  65. package/.next/standalone/src/lib/auth/allowlist.ts +50 -0
  66. package/.next/standalone/src/lib/auth/client.ts +4 -0
  67. package/.next/standalone/src/lib/auth/csrf.ts +68 -0
  68. package/.next/standalone/src/lib/auth/server.ts +164 -0
  69. package/.next/standalone/src/lib/config.ts +4 -0
  70. package/.next/standalone/src/lib/markdown/parse-frontmatter.ts +20 -0
  71. package/.next/standalone/src/lib/markdown/sanitize-schema.ts +106 -0
  72. package/.next/standalone/src/lib/markdown/to-html.ts +46 -8
  73. package/.next/standalone/src/lib/markdown/to-markdown.ts +14 -0
  74. package/.next/standalone/src/lib/proof/activity-shared.ts +31 -0
  75. package/.next/standalone/src/lib/proof/activity.ts +74 -0
  76. package/.next/standalone/src/lib/proof/auth.ts +132 -0
  77. package/.next/standalone/src/lib/proof/block-refs.ts +133 -0
  78. package/.next/standalone/src/lib/proof/blocks.ts +73 -0
  79. package/.next/standalone/src/lib/proof/client-auth.ts +23 -0
  80. package/.next/standalone/src/lib/proof/event-bus.ts +47 -0
  81. package/.next/standalone/src/lib/proof/file-lock.ts +38 -0
  82. package/.next/standalone/src/lib/proof/glob.ts +51 -0
  83. package/.next/standalone/src/lib/proof/idempotency.ts +32 -0
  84. package/.next/standalone/src/lib/proof/mutex.ts +32 -0
  85. package/.next/standalone/src/lib/proof/ops-applier.ts +678 -0
  86. package/.next/standalone/src/lib/proof/owner-auth.ts +2 -0
  87. package/.next/standalone/src/lib/proof/pending.ts +97 -0
  88. package/.next/standalone/src/lib/proof/proof-span.ts +141 -0
  89. package/.next/standalone/src/lib/proof/rate-limit.ts +53 -0
  90. package/.next/standalone/src/lib/proof/register-rate-limit.ts +53 -0
  91. package/.next/standalone/src/lib/proof/registry.ts +190 -0
  92. package/.next/standalone/src/lib/proof/sidecar.ts +66 -0
  93. package/.next/standalone/src/lib/proof/suggest-capture.ts +69 -0
  94. package/.next/standalone/src/lib/proof/types.ts +170 -0
  95. package/.next/standalone/src/lib/proof-config.ts +14 -0
  96. package/.next/standalone/src/middleware.ts +38 -0
  97. package/.next/standalone/src/stores/ai-panel-store.ts +58 -3
  98. package/.next/standalone/src/stores/editor-store.ts +115 -9
  99. package/.next/standalone/src/stores/proof-store.ts +123 -0
  100. package/.next/standalone/src/stores/view-width-store.ts +62 -0
  101. package/.next/standalone/src/tests/proof/activity-aggregator.test.ts +146 -0
  102. package/.next/standalone/src/tests/proof/agent-ownership.test.ts +140 -0
  103. package/.next/standalone/src/tests/proof/agents-install.test.ts +57 -0
  104. package/.next/standalone/src/tests/proof/better-auth.test.ts +68 -0
  105. package/.next/standalone/src/tests/proof/block-refs.test.ts +108 -0
  106. package/.next/standalone/src/tests/proof/blocks.test.ts +92 -0
  107. package/.next/standalone/src/tests/proof/comments-ops.test.ts +177 -0
  108. package/.next/standalone/src/tests/proof/editor-roundtrip.test.ts +85 -0
  109. package/.next/standalone/src/tests/proof/external-edit.test.ts +58 -0
  110. package/.next/standalone/src/tests/proof/file-lock.test.ts +80 -0
  111. package/.next/standalone/src/tests/proof/glob.test.ts +82 -0
  112. package/.next/standalone/src/tests/proof/helpers/auth-session.ts +27 -0
  113. package/.next/standalone/src/tests/proof/markdown-to-html.test.ts +46 -0
  114. package/.next/standalone/src/tests/proof/ops-applier.test.ts +368 -0
  115. package/.next/standalone/src/tests/proof/owner-init.test.ts +2 -0
  116. package/.next/standalone/src/tests/proof/parse-frontmatter.test.ts +60 -0
  117. package/.next/standalone/src/tests/proof/proof-span.test.ts +98 -0
  118. package/.next/standalone/src/tests/proof/rate-limit.test.ts +54 -0
  119. package/.next/standalone/src/tests/proof/registration-flow.test.ts +330 -0
  120. package/.next/standalone/src/tests/proof/registry.test.ts +169 -0
  121. package/.next/standalone/src/tests/proof/routes.test.ts +516 -0
  122. package/.next/standalone/src/tests/proof/sidecar-trim.test.ts +58 -0
  123. package/.next/standalone/src/tests/proof/suggestion-ops.test.ts +280 -0
  124. package/.next/standalone/src/tests/proof/wiki-content-put.test.ts +140 -0
  125. package/.next/standalone/src/tests/proof/wiki-routes-auth.test.ts +208 -0
  126. package/README.md +365 -99
  127. package/bin/wiki-viewer.js +431 -33
  128. package/package.json +19 -2
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { spawn, execSync } from "node:child_process";
2
+ import { spawn, execSync, execFileSync } from "node:child_process";
3
3
  import { createServer as createHttpsServer } from "node:https";
4
4
  import { createServer as createHttpServer, request as httpRequest } from "node:http";
5
5
  import { createServer as createNetServer } from "node:net";
6
- import { readFileSync, mkdirSync, existsSync } from "node:fs";
6
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from "node:fs";
7
7
  import path from "node:path";
8
8
  import os from "node:os";
9
9
  import { fileURLToPath } from "node:url";
@@ -11,9 +11,18 @@ import { fileURLToPath } from "node:url";
11
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
12
  const appRoot = path.resolve(__dirname, "..");
13
13
  const serverJs = path.join(appRoot, ".next", "standalone", "server.js");
14
+ const selfScript = fileURLToPath(import.meta.url);
15
+
16
+ const configDir = path.join(os.homedir(), ".wiki-viewer");
17
+ const configPath = path.join(configDir, "config.json");
18
+ const logDir = path.join(configDir, "logs");
19
+
20
+ const SERVICE_NAME = "wiki-viewer";
21
+ const LAUNCHD_LABEL = "com.wiki-viewer";
14
22
 
15
23
  function printUsage() {
16
24
  console.error("Usage: wiki-viewer [directory] [options]");
25
+ console.error(" wiki-viewer <command> [args]");
17
26
  console.error("");
18
27
  console.error(" directory Directory to serve (optional — pick in browser if omitted)");
19
28
  console.error("");
@@ -22,43 +31,99 @@ function printUsage() {
22
31
  console.error(" -H, --host <host> Host to bind to (default: localhost)");
23
32
  console.error(" --https Enable HTTPS (self-signed cert, enables service workers)");
24
33
  console.error("");
34
+ console.error("Commands:");
35
+ console.error(" service install [dir] [options] Install as a user service (persists across reboot)");
36
+ console.error(" service uninstall Remove the user service");
37
+ console.error(" service status Show service status");
38
+ console.error(" service logs Tail service logs");
39
+ console.error(" service run Run from saved config (used internally by the service)");
40
+ console.error(" update Update wiki-viewer to the latest version and restart");
41
+ console.error("");
25
42
  console.error("Examples:");
26
43
  console.error(" wiki-viewer ~/notes");
27
44
  console.error(" wiki-viewer ~/notes --https");
28
45
  console.error(" wiki-viewer ~/notes -p 8080 -H 0.0.0.0");
46
+ console.error(" wiki-viewer service install ~/notes -H 0.0.0.0 -p 3003 --https");
47
+ console.error(" wiki-viewer update");
29
48
  }
30
49
 
31
- if (!existsSync(serverJs)) {
32
- console.error("Error: pre-built server not found at", serverJs);
33
- console.error("This is a bug – please report it at https://github.com/anh-chu/wiki-viewer/issues");
34
- process.exit(1);
50
+ // ── arg parsing ──────────────────────────────────────────────────────────────
51
+
52
+ function parseServeArgs(args) {
53
+ let port = process.env.PORT;
54
+ let host = process.env.HOSTNAME;
55
+ let useHttps;
56
+ let userSpecifiedPort = false;
57
+ let rootDir;
58
+
59
+ for (let i = 0; i < args.length; i++) {
60
+ const a = args[i];
61
+ if (a === "-p" || a === "--port") { port = args[++i] ?? port; userSpecifiedPort = true; }
62
+ else if (a === "-H" || a === "--host") host = args[++i] ?? host;
63
+ else if (a === "--https") useHttps = true;
64
+ else if (!a.startsWith("-") && rootDir === undefined) rootDir = a;
65
+ }
66
+
67
+ return { rootDir, port, host, useHttps, userSpecifiedPort };
35
68
  }
36
69
 
37
- const args = process.argv.slice(2);
70
+ // ── config file ────────────────────────────────────────────────────────────
38
71
 
39
- if (args.includes("--help") || args.includes("-h")) {
40
- printUsage();
41
- process.exit(0);
72
+ function loadConfig() {
73
+ if (!existsSync(configPath)) return {};
74
+ try {
75
+ return JSON.parse(readFileSync(configPath, "utf8"));
76
+ } catch {
77
+ console.error(`Warning: could not parse ${configPath}, ignoring it`);
78
+ return {};
79
+ }
42
80
  }
43
81
 
44
- const rootDir = args.find((a) => !a.startsWith("-"));
45
- let port = process.env.PORT ?? "3000";
46
- let host = process.env.HOSTNAME ?? "localhost";
47
- let useHttps = false;
82
+ function saveConfig(cfg) {
83
+ mkdirSync(configDir, { recursive: true });
84
+ writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n");
85
+ }
48
86
 
49
- for (let i = 0; i < args.length; i++) {
50
- const a = args[i];
51
- if (a === "-p" || a === "--port") port = args[++i] ?? port;
52
- else if (a === "-H" || a === "--host") host = args[++i] ?? host;
53
- else if (a === "--https") useHttps = true;
87
+ // Ad-hoc serve: pure CLI flags + built-in defaults. Does NOT read the config
88
+ // file, so an installed service never silently alters a one-off invocation.
89
+ function resolveServeOptions(args) {
90
+ const cli = parseServeArgs(args);
91
+ return {
92
+ rootDir: cli.rootDir ? path.resolve(cli.rootDir) : null,
93
+ port: String(cli.port ?? "3000"),
94
+ host: cli.host ?? "localhost",
95
+ useHttps: Boolean(cli.useHttps),
96
+ userSpecifiedPort: cli.userSpecifiedPort,
97
+ };
54
98
  }
55
99
 
56
- const resolvedRoot = rootDir ? path.resolve(rootDir) : null;
100
+ // Service run: config file is the source of truth. CLI flags (if any) still win
101
+ // so the unit/plist could pass overrides, but normally there are none.
102
+ // Precedence: explicit CLI flags > config file > built-in defaults.
103
+ function resolveRunOptions(args) {
104
+ const cli = parseServeArgs(args);
105
+ const cfg = loadConfig();
106
+
107
+ const rootDir = cli.rootDir ?? cfg.rootDir ?? null;
108
+ const port = cli.port ?? cfg.port ?? "3000";
109
+ const host = cli.host ?? cfg.host ?? "localhost";
110
+ const useHttps = cli.useHttps ?? cfg.https ?? false;
111
+ // A config-pinned port is explicit too (don't auto-bump to next free port).
112
+ const userSpecifiedPort = cli.userSpecifiedPort || cfg.port != null;
113
+
114
+ return {
115
+ rootDir: rootDir ? path.resolve(rootDir) : null,
116
+ port: String(port),
117
+ host,
118
+ useHttps: Boolean(useHttps),
119
+ userSpecifiedPort,
120
+ };
121
+ }
57
122
 
58
123
  // ── HTTPS cert generation ──────────────────────────────────────────────────
59
124
 
60
- function ensureCerts() {
61
- const dir = path.join(os.homedir(), ".wiki-viewer", "certs");
125
+ function ensureCerts(host) {
126
+ const dir = path.join(configDir, "certs");
62
127
  mkdirSync(dir, { recursive: true });
63
128
  const keyPath = path.join(dir, "key.pem");
64
129
  const certPath = path.join(dir, "cert.pem");
@@ -69,14 +134,14 @@ function ensureCerts() {
69
134
  try {
70
135
  execSync("mkcert -version", { stdio: "ignore" });
71
136
  execSync(`mkcert -install 2>/dev/null; mkcert -key-file "${keyPath}" -cert-file "${certPath}" localhost 127.0.0.1 "${host}"`, { stdio: "pipe" });
72
- console.log("🔒 Trusted cert via mkcert");
137
+ console.log("�� Trusted cert via mkcert");
73
138
  } catch {
74
139
  try {
75
140
  execSync(
76
141
  `openssl req -x509 -newkey rsa:2048 -keyout "${keyPath}" -out "${certPath}" -days 825 -nodes -subj "/CN=localhost"`,
77
142
  { stdio: "ignore" },
78
143
  );
79
- console.log("🔒 Self-signed cert (browser will warn once — click through)");
144
+ console.log("�� Self-signed cert (browser will warn once — click through)");
80
145
  } catch {
81
146
  console.error("Error: --https requires mkcert or openssl");
82
147
  process.exit(1);
@@ -85,7 +150,22 @@ function ensureCerts() {
85
150
  return { key: readFileSync(keyPath), cert: readFileSync(certPath) };
86
151
  }
87
152
 
88
- // ── free port helper ───────────────────────────────────────────────────────
153
+ // ── port availability helpers ──────────────────────────────────────────────
154
+
155
+ function isPortAvailable(p, h) {
156
+ return new Promise((resolve) => {
157
+ const s = createNetServer();
158
+ s.once("error", () => resolve(false));
159
+ s.once("listening", () => s.close(() => resolve(true)));
160
+ s.listen(Number(p), h);
161
+ });
162
+ }
163
+
164
+ async function findNextAvailablePort(startPort, h) {
165
+ let p = Number(startPort);
166
+ while (!(await isPortAvailable(p, h))) p++;
167
+ return String(p);
168
+ }
89
169
 
90
170
  function freePort() {
91
171
  return new Promise((resolve) => {
@@ -97,19 +177,49 @@ function freePort() {
97
177
  });
98
178
  }
99
179
 
180
+ function getNetworkAddress() {
181
+ for (const ifaces of Object.values(os.networkInterfaces())) {
182
+ for (const iface of ifaces ?? []) {
183
+ if (iface.family === "IPv4" && !iface.internal) return iface.address;
184
+ }
185
+ }
186
+ return null;
187
+ }
188
+
100
189
  // ── start ──────────────────────────────────────────────────────────────────
101
190
 
102
- async function start() {
191
+ async function start(opts) {
192
+ const { rootDir: resolvedRoot, useHttps } = opts;
193
+ let { port, host, userSpecifiedPort } = opts;
194
+
195
+ if (!existsSync(serverJs)) {
196
+ console.error("Error: pre-built server not found at", serverJs);
197
+ console.error("This is a bug – please report it at https://github.com/anh-chu/wiki-viewer/issues");
198
+ process.exit(1);
199
+ }
200
+
103
201
  if (resolvedRoot) {
104
- console.log(`📂 ${resolvedRoot}`);
202
+ console.log(`�� ${resolvedRoot}`);
105
203
  } else {
106
- console.log("📂 No directory specified — open the browser to choose one");
204
+ console.log("�� No directory specified — open the browser to choose one");
205
+ }
206
+
207
+ // Auto-select next free port when user didn't specify one
208
+ if (!userSpecifiedPort) {
209
+ const available = await isPortAvailable(Number(port), host);
210
+ if (!available) {
211
+ const original = port;
212
+ port = await findNextAvailablePort(Number(port) + 1, host);
213
+ console.log(`⚠️ Port ${original} in use → using ${port} (pass -p <port> to override)`);
214
+ }
107
215
  }
108
216
 
109
217
  // When HTTPS is requested, run the standalone server on a random internal
110
218
  // HTTP port and stand up an HTTPS reverse-proxy on the user-facing port.
111
219
  const internalPort = useHttps ? String(await freePort()) : port;
112
- const internalHost = "127.0.0.1";
220
+ // In HTTPS mode the standalone server sits behind the proxy on loopback.
221
+ // Otherwise it must bind to the user-requested host directly.
222
+ const internalHost = useHttps ? "127.0.0.1" : host;
113
223
 
114
224
  const child = spawn(process.execPath, [serverJs], {
115
225
  cwd: path.join(appRoot, ".next", "standalone"),
@@ -125,7 +235,7 @@ async function start() {
125
235
  child.on("exit", (code) => process.exit(code ?? 0));
126
236
 
127
237
  if (useHttps) {
128
- const { key, cert } = ensureCerts();
238
+ const { key, cert } = ensureCerts(host);
129
239
 
130
240
  // Simple HTTPS → HTTP proxy
131
241
  const proxy = createHttpsServer({ key, cert }, (req, res) => {
@@ -147,12 +257,300 @@ async function start() {
147
257
  // Wait a moment for the standalone server to bind before starting proxy
148
258
  setTimeout(() => {
149
259
  proxy.listen(Number(port), host, () => {
150
- console.log(`🌐 https://${host}:${port}`);
260
+ const scheme = "https";
261
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
262
+ console.log(`\n ➜ Local: ${scheme}://${displayHost}:${port}`);
263
+ const netAddr = getNetworkAddress();
264
+ if (netAddr && host !== "localhost" && host !== "127.0.0.1") {
265
+ console.log(` ➜ Network: ${scheme}://${netAddr}:${port}`);
266
+ }
267
+ console.log(`\n Listening on ${host}:${port} (--host / -H, --port / -p to rebind)\n`);
151
268
  });
152
269
  }, 1_000);
153
270
  } else {
154
- console.log(`🌐 http://${host}:${port}`);
271
+ const scheme = "http";
272
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
273
+ console.log(`\n ➜ Local: ${scheme}://${displayHost}:${port}`);
274
+ const netAddr = getNetworkAddress();
275
+ if (netAddr && host !== "localhost" && host !== "127.0.0.1") {
276
+ console.log(` ➜ Network: ${scheme}://${netAddr}:${port}`);
277
+ }
278
+ console.log(`\n Listening on ${host}:${port} (--host / -H, --port / -p to rebind)\n`);
279
+ }
280
+ }
281
+
282
+ // ── service: shared ──────────────────────────────────────────────────────────
283
+
284
+ function platform() {
285
+ if (process.platform === "linux") return "linux";
286
+ if (process.platform === "darwin") return "macos";
287
+ return null;
288
+ }
289
+
290
+ function requireSupportedPlatform() {
291
+ const p = platform();
292
+ if (!p) {
293
+ console.error(`Error: service management is only supported on Linux (systemd) and macOS (launchd), not ${process.platform}.`);
294
+ process.exit(1);
155
295
  }
296
+ return p;
297
+ }
298
+
299
+ function run(cmd, args, opts = {}) {
300
+ return execFileSync(cmd, args, { stdio: "inherit", ...opts });
156
301
  }
157
302
 
158
- start();
303
+ function runQuiet(cmd, args) {
304
+ try {
305
+ return execFileSync(cmd, args, { stdio: ["ignore", "pipe", "pipe"] }).toString();
306
+ } catch (e) {
307
+ return null;
308
+ }
309
+ }
310
+
311
+ // ── service: install ──────────────────────────────────────────────────────────
312
+
313
+ function serviceInstall(args) {
314
+ const p = requireSupportedPlatform();
315
+
316
+ // Capture the run config from flags (falling back to existing config), persist it.
317
+ const cli = parseServeArgs(args);
318
+ const existing = loadConfig();
319
+ const cfg = {
320
+ rootDir: cli.rootDir != null ? path.resolve(cli.rootDir) : existing.rootDir ?? null,
321
+ host: cli.host ?? existing.host ?? "localhost",
322
+ port: cli.port ?? existing.port ?? "3000",
323
+ https: cli.useHttps ?? existing.https ?? false,
324
+ };
325
+ saveConfig(cfg);
326
+ console.log(`Saved config to ${configPath}`);
327
+ console.log(` dir: ${cfg.rootDir ?? "(choose in browser)"}`);
328
+ console.log(` host: ${cfg.host}`);
329
+ console.log(` port: ${cfg.port}`);
330
+ console.log(` https: ${cfg.https}`);
331
+ console.log("");
332
+
333
+ if (p === "linux") installSystemd();
334
+ else installLaunchd();
335
+ }
336
+
337
+ function installSystemd() {
338
+ const unitDir = path.join(os.homedir(), ".config", "systemd", "user");
339
+ mkdirSync(unitDir, { recursive: true });
340
+ const unitPath = path.join(unitDir, `${SERVICE_NAME}.service`);
341
+
342
+ const unit = `[Unit]
343
+ Description=wiki-viewer local file viewer
344
+ After=network.target
345
+
346
+ [Service]
347
+ Type=simple
348
+ ExecStart=${process.execPath} ${selfScript} service run
349
+ Restart=on-failure
350
+ RestartSec=3
351
+ Environment=NODE_ENV=production
352
+
353
+ [Install]
354
+ WantedBy=default.target
355
+ `;
356
+ writeFileSync(unitPath, unit);
357
+ console.log(`Wrote unit ${unitPath}`);
358
+
359
+ // Enable lingering so the service survives logout and starts at boot.
360
+ const user = os.userInfo().username;
361
+ try {
362
+ execFileSync("loginctl", ["enable-linger", user], { stdio: "ignore" });
363
+ console.log(`Enabled linger for ${user} (starts at boot)`);
364
+ } catch {
365
+ console.log(`Note: could not enable linger automatically. For boot persistence run:`);
366
+ console.log(` sudo loginctl enable-linger ${user}`);
367
+ }
368
+
369
+ run("systemctl", ["--user", "daemon-reload"]);
370
+ run("systemctl", ["--user", "enable", "--now", `${SERVICE_NAME}.service`]);
371
+ console.log("\nService installed and started.");
372
+ console.log(" Status: wiki-viewer service status");
373
+ console.log(" Logs: wiki-viewer service logs");
374
+ }
375
+
376
+ function installLaunchd() {
377
+ const agentsDir = path.join(os.homedir(), "Library", "LaunchAgents");
378
+ mkdirSync(agentsDir, { recursive: true });
379
+ mkdirSync(logDir, { recursive: true });
380
+ const plistPath = path.join(agentsDir, `${LAUNCHD_LABEL}.plist`);
381
+ const outLog = path.join(logDir, "out.log");
382
+ const errLog = path.join(logDir, "err.log");
383
+
384
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
385
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
386
+ <plist version="1.0">
387
+ <dict>
388
+ <key>Label</key>
389
+ <string>${LAUNCHD_LABEL}</string>
390
+ <key>ProgramArguments</key>
391
+ <array>
392
+ <string>${process.execPath}</string>
393
+ <string>${selfScript}</string>
394
+ <string>service</string>
395
+ <string>run</string>
396
+ </array>
397
+ <key>RunAtLoad</key>
398
+ <true/>
399
+ <key>KeepAlive</key>
400
+ <true/>
401
+ <key>StandardOutPath</key>
402
+ <string>${outLog}</string>
403
+ <key>StandardErrorPath</key>
404
+ <string>${errLog}</string>
405
+ </dict>
406
+ </plist>
407
+ `;
408
+ writeFileSync(plistPath, plist);
409
+ console.log(`Wrote plist ${plistPath}`);
410
+
411
+ // Reload if already loaded, then load with -w to persist across reboot.
412
+ runQuiet("launchctl", ["unload", plistPath]);
413
+ run("launchctl", ["load", "-w", plistPath]);
414
+ console.log("\nService installed and started.");
415
+ console.log(" Status: wiki-viewer service status");
416
+ console.log(" Logs: wiki-viewer service logs");
417
+ }
418
+
419
+ // ── service: uninstall ──────────────────────────────────────────────────────
420
+
421
+ function serviceUninstall() {
422
+ const p = requireSupportedPlatform();
423
+ if (p === "linux") {
424
+ runQuiet("systemctl", ["--user", "disable", "--now", `${SERVICE_NAME}.service`]);
425
+ const unitPath = path.join(os.homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`);
426
+ if (existsSync(unitPath)) { rmSync(unitPath); console.log(`Removed ${unitPath}`); }
427
+ runQuiet("systemctl", ["--user", "daemon-reload"]);
428
+ } else {
429
+ const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
430
+ runQuiet("launchctl", ["unload", "-w", plistPath]);
431
+ if (existsSync(plistPath)) { rmSync(plistPath); console.log(`Removed ${plistPath}`); }
432
+ }
433
+ console.log("Service uninstalled.");
434
+ }
435
+
436
+ // ── service: status / logs ──────────────────────────────────────────────────
437
+
438
+ function serviceStatus() {
439
+ const p = requireSupportedPlatform();
440
+ if (p === "linux") {
441
+ try { run("systemctl", ["--user", "status", `${SERVICE_NAME}.service`, "--no-pager"]); }
442
+ catch { /* systemctl exits non-zero when inactive; output already shown */ }
443
+ } else {
444
+ const out = runQuiet("launchctl", ["list"]);
445
+ if (out) {
446
+ const line = out.split("\n").find((l) => l.includes(LAUNCHD_LABEL));
447
+ console.log(line ? line.trim() : `${LAUNCHD_LABEL}: not loaded`);
448
+ }
449
+ }
450
+ }
451
+
452
+ function serviceLogs() {
453
+ const p = requireSupportedPlatform();
454
+ if (p === "linux") {
455
+ run("journalctl", ["--user", "-u", `${SERVICE_NAME}.service`, "-n", "100", "-f"]);
456
+ } else {
457
+ const outLog = path.join(logDir, "out.log");
458
+ const errLog = path.join(logDir, "err.log");
459
+ run("tail", ["-n", "100", "-f", outLog, errLog]);
460
+ }
461
+ }
462
+
463
+ function serviceIsInstalled() {
464
+ const p = platform();
465
+ if (p === "linux") {
466
+ return existsSync(path.join(os.homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`));
467
+ }
468
+ if (p === "macos") {
469
+ return existsSync(path.join(os.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`));
470
+ }
471
+ return false;
472
+ }
473
+
474
+ function serviceRestart() {
475
+ const p = platform();
476
+ if (p === "linux") {
477
+ run("systemctl", ["--user", "restart", `${SERVICE_NAME}.service`]);
478
+ } else if (p === "macos") {
479
+ const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
480
+ runQuiet("launchctl", ["unload", plistPath]);
481
+ run("launchctl", ["load", "-w", plistPath]);
482
+ }
483
+ }
484
+
485
+ // ── update ────────────────────────────────────────────────────────────────
486
+
487
+ function detectPackageManager() {
488
+ // Prefer the manager whose global root contains this install.
489
+ const ua = process.env.npm_config_user_agent ?? "";
490
+ if (ua.startsWith("pnpm")) return "pnpm";
491
+ if (ua.startsWith("yarn")) return "yarn";
492
+ if (selfScript.includes(`${path.sep}pnpm${path.sep}`)) return "pnpm";
493
+ return "npm";
494
+ }
495
+
496
+ function update() {
497
+ const pkg = JSON.parse(readFileSync(path.join(appRoot, "package.json"), "utf8"));
498
+ const name = pkg.name;
499
+ console.log(`Current ${name}: v${pkg.version}`);
500
+
501
+ const pm = detectPackageManager();
502
+ const cmd = pm === "pnpm" ? ["pnpm", ["add", "-g", `${name}@latest`]]
503
+ : pm === "yarn" ? ["yarn", ["global", "add", `${name}@latest`]]
504
+ : ["npm", ["install", "-g", `${name}@latest`]];
505
+
506
+ console.log(`Updating via ${pm}…`);
507
+ try {
508
+ run(cmd[0], cmd[1]);
509
+ } catch {
510
+ console.error(`Error: update failed. Try manually: ${cmd[0]} ${cmd[1].join(" ")}`);
511
+ process.exit(1);
512
+ }
513
+
514
+ if (serviceIsInstalled()) {
515
+ console.log("Restarting service…");
516
+ try { serviceRestart(); console.log("Service restarted."); }
517
+ catch { console.log("Note: could not restart service automatically. Run: wiki-viewer service restart"); }
518
+ }
519
+ console.log("Update complete.");
520
+ }
521
+
522
+ // ── dispatch ──────────────────────────────────────────────────────────────
523
+
524
+ const argv = process.argv.slice(2);
525
+
526
+ if (argv.includes("--help") || argv.includes("-h")) {
527
+ printUsage();
528
+ process.exit(0);
529
+ }
530
+
531
+ const [cmd, ...rest] = argv;
532
+
533
+ switch (cmd) {
534
+ case "service": {
535
+ const [sub, ...subArgs] = rest;
536
+ switch (sub) {
537
+ case "install": serviceInstall(subArgs); break;
538
+ case "uninstall": serviceUninstall(); break;
539
+ case "status": serviceStatus(); break;
540
+ case "logs": serviceLogs(); break;
541
+ case "restart": serviceRestart(); break;
542
+ case "run": start(resolveRunOptions(subArgs)); break;
543
+ default:
544
+ console.error(`Unknown service command: ${sub ?? "(none)"}`);
545
+ console.error("Try: install | uninstall | status | logs | restart | run");
546
+ process.exit(1);
547
+ }
548
+ break;
549
+ }
550
+ case "update":
551
+ update();
552
+ break;
553
+ default:
554
+ // No recognized command → ad-hoc serve (directory + flags only).
555
+ start(resolveServeOptions(argv));
556
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-viewer",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
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",
@@ -17,7 +17,8 @@
17
17
  "build": "next build",
18
18
  "start": "next start",
19
19
  "wiki": "node bin/wiki-viewer.js",
20
- "prepublishOnly": "npm run build && cp -r .next/static .next/standalone/.next/static && cp -r public .next/standalone/public"
20
+ "prepublishOnly": "npm run build && cp -r .next/static .next/standalone/.next/static && cp -r public .next/standalone/public",
21
+ "test": "tsx --test src/tests/proof/*.test.ts"
21
22
  },
22
23
  "engines": {
23
24
  "node": ">=18.0.0"
@@ -35,7 +36,14 @@
35
36
  "url": "https://github.com/anh-chu/wiki-viewer.git"
36
37
  },
37
38
  "dependencies": {
39
+ "better-auth": "^1.6.12",
40
+ "better-sqlite3": "^12.10.0",
38
41
  "chokidar": "^5.0.0",
42
+ "kysely": "0.28.5",
43
+ "proper-lockfile": "^4.1.2",
44
+ "rehype-parse": "^9.0.1",
45
+ "rehype-raw": "^7.0.0",
46
+ "rehype-sanitize": "^6.0.0",
39
47
  "undici": "^8.3.0"
40
48
  },
41
49
  "devDependencies": {
@@ -75,9 +83,11 @@
75
83
  "@tiptap/react": "^3.22.5",
76
84
  "@tiptap/starter-kit": "^3.22.5",
77
85
  "@tiptap/suggestion": "^3.22.5",
86
+ "@types/better-sqlite3": "^7.6.13",
78
87
  "@types/mdast": "^4.0.4",
79
88
  "@types/node": "^22.15.0",
80
89
  "@types/papaparse": "^5.5.2",
90
+ "@types/proper-lockfile": "^4.1.4",
81
91
  "@types/react": "19.2.14",
82
92
  "@types/react-dom": "19.2.3",
83
93
  "@types/turndown": "^5.0.6",
@@ -107,14 +117,21 @@
107
117
  "remark-gfm": "^4.0.1",
108
118
  "remark-parse": "^11.0.0",
109
119
  "remark-rehype": "^11.1.2",
120
+ "remark-stringify": "^11.0.0",
110
121
  "sonner": "^2.0.7",
111
122
  "tailwind-merge": "^3.3.0",
112
123
  "tailwindcss": "^3.4.17",
124
+ "tsx": "^4.22.3",
113
125
  "turndown": "^7.2.4",
114
126
  "turndown-plugin-gfm": "^1.0.2",
115
127
  "typescript": "^5.8.0",
116
128
  "unified": "^11.0.5",
117
129
  "xlsx": "^0.18.5",
118
130
  "zustand": "^5.0.12"
131
+ },
132
+ "pnpm": {
133
+ "overrides": {
134
+ "kysely": "0.28.5"
135
+ }
119
136
  }
120
137
  }