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.
- package/.next/standalone/README.md +365 -99
- package/.next/standalone/agents/bootstrap-prompt.md +1 -0
- package/.next/standalone/agents/wiki-viewer-skill/SKILL.md +236 -0
- package/.next/standalone/bin/wiki-viewer.js +431 -33
- package/.next/standalone/docs/agent-collab-plan.md +1615 -0
- package/.next/standalone/docs/agent-collab-v2-plan.md +771 -0
- package/.next/standalone/package.json +19 -2
- package/.next/standalone/pnpm-lock.yaml +1368 -325
- package/.next/standalone/pnpm-workspace.yaml +7 -1
- package/.next/standalone/src/app/api/agent/activity/route.ts +58 -0
- package/.next/standalone/src/app/api/agent/admin/agents/[agentId]/revoke/route.ts +40 -0
- package/.next/standalone/src/app/api/agent/admin/agents/route.ts +33 -0
- package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/approve/route.ts +83 -0
- package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/deny/route.ts +36 -0
- package/.next/standalone/src/app/api/agent/admin/registrations/route.ts +27 -0
- package/.next/standalone/src/app/api/agent/events/[...path]/route.ts +136 -0
- package/.next/standalone/src/app/api/agent/files/[...path]/route.ts +202 -0
- package/.next/standalone/src/app/api/agent/internal/span/route.ts +117 -0
- package/.next/standalone/src/app/api/agent/register/[regId]/route.ts +50 -0
- package/.next/standalone/src/app/api/agent/register/route.ts +110 -0
- package/.next/standalone/src/app/api/agent/settings/route.ts +30 -0
- package/.next/standalone/src/app/api/agent/settings/token/regenerate/route.ts +20 -0
- package/.next/standalone/src/app/api/agent/sidecar/[...path]/route.ts +49 -0
- package/.next/standalone/src/app/api/agents/install/route.ts +83 -0
- package/.next/standalone/src/app/api/agents/skill/route.ts +20 -0
- package/.next/standalone/src/app/api/agents/skill.tar.gz/route.ts +87 -0
- package/.next/standalone/src/app/api/auth/[...all]/route.ts +7 -0
- package/.next/standalone/src/app/api/owner/init/route.ts +14 -0
- package/.next/standalone/src/app/api/system/auth-settings/route.ts +85 -0
- package/.next/standalone/src/app/api/system/browse/route.ts +4 -0
- package/.next/standalone/src/app/api/system/clear-root/route.ts +8 -1
- package/.next/standalone/src/app/api/system/config/route.ts +5 -1
- package/.next/standalone/src/app/api/system/pins/route.ts +7 -0
- package/.next/standalone/src/app/api/system/reveal/route.ts +7 -0
- package/.next/standalone/src/app/api/system/root-status/route.ts +5 -1
- package/.next/standalone/src/app/api/system/set-root/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/app/route.ts +15 -0
- package/.next/standalone/src/app/api/wiki/content/route.ts +110 -11
- package/.next/standalone/src/app/api/wiki/folder/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/move/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/new-file/route.ts +55 -0
- package/.next/standalone/src/app/api/wiki/page/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/route.ts +10 -0
- package/.next/standalone/src/app/api/wiki/slugs/route.ts +5 -1
- package/.next/standalone/src/app/api/wiki/upload/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/watch/route.ts +6 -1
- package/.next/standalone/src/app/globals.css +70 -0
- package/.next/standalone/src/app/page.tsx +461 -217
- package/.next/standalone/src/app/signin/page.tsx +217 -0
- package/.next/standalone/src/components/ai-panel/activity-row.tsx +64 -0
- package/.next/standalone/src/components/ai-panel/ai-panel.tsx +322 -0
- package/.next/standalone/src/components/ai-panel/token-section.tsx +312 -0
- package/.next/standalone/src/components/auth-settings-sheet.tsx +209 -0
- package/.next/standalone/src/components/editor/bubble-menu.tsx +41 -2
- package/.next/standalone/src/components/editor/comment-pip.tsx +56 -0
- package/.next/standalone/src/components/editor/comment-thread.tsx +252 -0
- package/.next/standalone/src/components/editor/editor.tsx +513 -10
- package/.next/standalone/src/components/editor/extensions/proof-span.ts +60 -0
- package/.next/standalone/src/components/editor/extensions.ts +2 -0
- package/.next/standalone/src/components/editor/proof-span-popover.tsx +161 -0
- package/.next/standalone/src/components/editor/suggest-edit-popover.tsx +228 -0
- package/.next/standalone/src/components/editor/suggestion-card.tsx +201 -0
- package/.next/standalone/src/components/view-width-toggle.tsx +47 -0
- package/.next/standalone/src/components/wiki/markdown-preview.tsx +120 -0
- package/.next/standalone/src/lib/auth/allowlist.ts +50 -0
- package/.next/standalone/src/lib/auth/client.ts +4 -0
- package/.next/standalone/src/lib/auth/csrf.ts +68 -0
- package/.next/standalone/src/lib/auth/server.ts +164 -0
- package/.next/standalone/src/lib/config.ts +4 -0
- package/.next/standalone/src/lib/markdown/parse-frontmatter.ts +20 -0
- package/.next/standalone/src/lib/markdown/sanitize-schema.ts +106 -0
- package/.next/standalone/src/lib/markdown/to-html.ts +46 -8
- package/.next/standalone/src/lib/markdown/to-markdown.ts +14 -0
- package/.next/standalone/src/lib/proof/activity-shared.ts +31 -0
- package/.next/standalone/src/lib/proof/activity.ts +74 -0
- package/.next/standalone/src/lib/proof/auth.ts +132 -0
- package/.next/standalone/src/lib/proof/block-refs.ts +133 -0
- package/.next/standalone/src/lib/proof/blocks.ts +73 -0
- package/.next/standalone/src/lib/proof/client-auth.ts +23 -0
- package/.next/standalone/src/lib/proof/event-bus.ts +47 -0
- package/.next/standalone/src/lib/proof/file-lock.ts +38 -0
- package/.next/standalone/src/lib/proof/glob.ts +51 -0
- package/.next/standalone/src/lib/proof/idempotency.ts +32 -0
- package/.next/standalone/src/lib/proof/mutex.ts +32 -0
- package/.next/standalone/src/lib/proof/ops-applier.ts +678 -0
- package/.next/standalone/src/lib/proof/owner-auth.ts +2 -0
- package/.next/standalone/src/lib/proof/pending.ts +97 -0
- package/.next/standalone/src/lib/proof/proof-span.ts +141 -0
- package/.next/standalone/src/lib/proof/rate-limit.ts +53 -0
- package/.next/standalone/src/lib/proof/register-rate-limit.ts +53 -0
- package/.next/standalone/src/lib/proof/registry.ts +190 -0
- package/.next/standalone/src/lib/proof/sidecar.ts +66 -0
- package/.next/standalone/src/lib/proof/suggest-capture.ts +69 -0
- package/.next/standalone/src/lib/proof/types.ts +170 -0
- package/.next/standalone/src/lib/proof-config.ts +14 -0
- package/.next/standalone/src/middleware.ts +38 -0
- package/.next/standalone/src/stores/ai-panel-store.ts +58 -3
- package/.next/standalone/src/stores/editor-store.ts +115 -9
- package/.next/standalone/src/stores/proof-store.ts +123 -0
- package/.next/standalone/src/stores/view-width-store.ts +62 -0
- package/.next/standalone/src/tests/proof/activity-aggregator.test.ts +146 -0
- package/.next/standalone/src/tests/proof/agent-ownership.test.ts +140 -0
- package/.next/standalone/src/tests/proof/agents-install.test.ts +57 -0
- package/.next/standalone/src/tests/proof/better-auth.test.ts +68 -0
- package/.next/standalone/src/tests/proof/block-refs.test.ts +108 -0
- package/.next/standalone/src/tests/proof/blocks.test.ts +92 -0
- package/.next/standalone/src/tests/proof/comments-ops.test.ts +177 -0
- package/.next/standalone/src/tests/proof/editor-roundtrip.test.ts +85 -0
- package/.next/standalone/src/tests/proof/external-edit.test.ts +58 -0
- package/.next/standalone/src/tests/proof/file-lock.test.ts +80 -0
- package/.next/standalone/src/tests/proof/glob.test.ts +82 -0
- package/.next/standalone/src/tests/proof/helpers/auth-session.ts +27 -0
- package/.next/standalone/src/tests/proof/markdown-to-html.test.ts +46 -0
- package/.next/standalone/src/tests/proof/ops-applier.test.ts +368 -0
- package/.next/standalone/src/tests/proof/owner-init.test.ts +2 -0
- package/.next/standalone/src/tests/proof/parse-frontmatter.test.ts +60 -0
- package/.next/standalone/src/tests/proof/proof-span.test.ts +98 -0
- package/.next/standalone/src/tests/proof/rate-limit.test.ts +54 -0
- package/.next/standalone/src/tests/proof/registration-flow.test.ts +330 -0
- package/.next/standalone/src/tests/proof/registry.test.ts +169 -0
- package/.next/standalone/src/tests/proof/routes.test.ts +516 -0
- package/.next/standalone/src/tests/proof/sidecar-trim.test.ts +58 -0
- package/.next/standalone/src/tests/proof/suggestion-ops.test.ts +280 -0
- package/.next/standalone/src/tests/proof/wiki-content-put.test.ts +140 -0
- package/.next/standalone/src/tests/proof/wiki-routes-auth.test.ts +208 -0
- package/README.md +365 -99
- package/bin/wiki-viewer.js +431 -33
- 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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
process.
|
|
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
|
-
|
|
70
|
+
// ── config file ────────────────────────────────────────────────────────────
|
|
38
71
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
82
|
+
function saveConfig(cfg) {
|
|
83
|
+
mkdirSync(configDir, { recursive: true });
|
|
84
|
+
writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n");
|
|
85
|
+
}
|
|
48
86
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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(
|
|
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("
|
|
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("
|
|
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
|
-
// ──
|
|
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(
|
|
202
|
+
console.log(`�� ${resolvedRoot}`);
|
|
105
203
|
} else {
|
|
106
|
-
console.log("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|