@lelouchhe/webagent 0.2.6 → 0.4.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 (57) hide show
  1. package/README.md +58 -23
  2. package/bin/webagent.mjs +119 -8
  3. package/config.toml +102 -3
  4. package/dist/index.html +64 -41
  5. package/dist/js/app.GSAIYHML.js +4 -0
  6. package/dist/js/chunk.AJZBJBMO.js +1 -0
  7. package/dist/js/chunk.CGWFHJI2.js +76 -0
  8. package/dist/js/chunk.D4ZYHJAM.js +1 -0
  9. package/dist/js/chunk.VZXGXFNN.js +5 -0
  10. package/dist/js/login.PYIK52HN.js +1 -0
  11. package/dist/js/viewer.6DT53STL.js +1 -0
  12. package/dist/login.html +49 -0
  13. package/dist/share-viewer.00gubshk.css +114 -0
  14. package/dist/share-viewer.html +53 -0
  15. package/dist/styles.012p32dz.css +1443 -0
  16. package/dist/sw.js +79 -27
  17. package/dist/theme-init.js +6 -0
  18. package/lib/agent-detect.js +110 -0
  19. package/lib/atomic-write.js +50 -0
  20. package/lib/attachment-dispatch.js +86 -0
  21. package/lib/attachment-interceptor.js +130 -0
  22. package/lib/attachment-labels.js +139 -0
  23. package/lib/attachments.js +154 -0
  24. package/lib/auth-middleware.js +102 -0
  25. package/lib/auth-store.js +269 -0
  26. package/lib/auth.js +89 -0
  27. package/lib/bootstrap.js +70 -0
  28. package/lib/bridge.js +244 -93
  29. package/lib/client-registry.js +60 -0
  30. package/lib/config.js +127 -9
  31. package/lib/daemon.js +185 -40
  32. package/lib/event-handler.js +209 -90
  33. package/lib/log-fmt.js +67 -0
  34. package/lib/log.js +83 -0
  35. package/lib/message-cleanup.js +48 -0
  36. package/lib/mode-bucket.js +62 -0
  37. package/lib/preflight.js +195 -0
  38. package/lib/push-service.js +338 -45
  39. package/lib/routes.js +1218 -144
  40. package/lib/server.js +159 -32
  41. package/lib/session-manager.js +164 -18
  42. package/lib/session-state.js +160 -0
  43. package/lib/sessions-anchor.js +28 -0
  44. package/lib/share/cleanup.js +45 -0
  45. package/lib/share/routes.js +972 -0
  46. package/lib/share/sanitize.js +179 -0
  47. package/lib/sse-manager.js +94 -8
  48. package/lib/sse-ticket.js +45 -0
  49. package/lib/startup-checks.js +94 -0
  50. package/lib/store.js +654 -24
  51. package/lib/title-service.js +42 -9
  52. package/lib/tokens.js +50 -0
  53. package/lib/types.js +23 -0
  54. package/package.json +38 -4
  55. package/dist/js/app.4FZ67UW4.js +0 -10
  56. package/dist/styles.008ve1hx.css +0 -669
  57. package/lib/shared/constants.js +0 -17
@@ -0,0 +1,195 @@
1
+ // Startup preflight checks. Each check prints a uniform `[check] <name> ✓`
2
+ // line on success, or `✗` with an actionable hint and exits 78 (sysexits
3
+ // EX_CONFIG) on failure. Runs synchronously before any heavy init so
4
+ // failures land on the operator's terminal first thing, not buried under
5
+ // noise.
6
+ //
7
+ // Scope is intentionally narrow: things we can answer before binding the
8
+ // port. Network reachability, agent login state, etc. are runtime
9
+ // concerns and surface as warnings via the bridge / UI later.
10
+ import { accessSync, constants, mkdirSync, statSync } from "node:fs";
11
+ import { resolve } from "node:path";
12
+ import { spawnSync } from "node:child_process";
13
+ import { createServer } from "node:net";
14
+ import { detectAgent, formatDetectionFailure } from "./agent-detect.js";
15
+ const PAD = 36;
16
+ function pad(s) {
17
+ return s.length >= PAD ? s + " " : s + " ".repeat(PAD - s.length);
18
+ }
19
+ function printOk(c) {
20
+ console.log(`[check] ${pad(`${c.name}: ${c.detail}`)} ✓`);
21
+ }
22
+ function printFail(c) {
23
+ console.error(`[check] ${pad(`${c.name}: ${c.detail}`)} ✗`);
24
+ for (const line of c.hint.split("\n"))
25
+ console.error(` ${line}`);
26
+ }
27
+ function checkNodeVersion() {
28
+ const v = process.versions.node;
29
+ const [maj, min] = v.split(".").map((n) => parseInt(n, 10));
30
+ // package.json declares engines.node >= 22.6.0; mirror that here so the
31
+ // diagnostic is friendly rather than a stack trace from a missing API.
32
+ const ok = maj > 22 || (maj === 22 && min >= 6);
33
+ if (ok)
34
+ return { ok: true, name: "node", detail: `v${v}` };
35
+ return {
36
+ ok: false,
37
+ name: "node",
38
+ detail: `v${v}`,
39
+ hint: "webagent requires Node.js v22.6.0 or newer (for --experimental-strip-types).\nInstall via: nvm install 22 && nvm use 22",
40
+ };
41
+ }
42
+ function checkDataDir(dir) {
43
+ const abs = resolve(dir);
44
+ // Create the directory if it doesn't exist (Store does this lazily but
45
+ // we want the failure surface here, before SQLite tries to open).
46
+ try {
47
+ if (!safeStat(abs))
48
+ mkdirSync(abs, { recursive: true });
49
+ accessSync(abs, constants.W_OK);
50
+ return { ok: true, name: "data_dir", detail: abs };
51
+ }
52
+ catch (err) {
53
+ const code = err.code ?? "unknown";
54
+ return {
55
+ ok: false,
56
+ name: "data_dir",
57
+ detail: abs,
58
+ hint: `cannot create or write to ${abs}: ${code}\nfix permissions or set data_dir in config.toml to a writable path.`,
59
+ };
60
+ }
61
+ function safeStat(p) {
62
+ try {
63
+ return statSync(p);
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ }
70
+ /**
71
+ * Resolve `agent_cmd`. If it's the "auto" sentinel, run PATH detection;
72
+ * otherwise verify the first token (the binary) exists in PATH so we
73
+ * fail at preflight instead of after `server.listen` with a cryptic
74
+ * ENOENT in the bridge stderr.
75
+ */
76
+ function checkAgent(agentCmd) {
77
+ if (agentCmd === "auto") {
78
+ const r = detectAgent();
79
+ if (r.ok) {
80
+ return {
81
+ ok: true,
82
+ name: "acp agent",
83
+ detail: `${r.label} (${r.cmd})`,
84
+ resolved: r.cmd,
85
+ };
86
+ }
87
+ return {
88
+ ok: false,
89
+ name: "acp agent",
90
+ detail: "no ACP-ready binary in PATH",
91
+ hint: formatDetectionFailure(r).replace(/^\[bridge\] [^\n]*\n\n?/, ""),
92
+ };
93
+ }
94
+ // Explicit agent_cmd. Best-effort sanity: check first token's binary.
95
+ const bin = agentCmd.trim().split(/\s+/)[0];
96
+ if (!bin) {
97
+ return {
98
+ ok: false,
99
+ name: "acp agent",
100
+ detail: agentCmd,
101
+ hint: "agent_cmd is empty.",
102
+ };
103
+ }
104
+ // Use the same detection helper logic via a one-off PATH probe.
105
+ const which = process.platform === "win32" ? "where" : "which";
106
+ const r = spawnSync(which, [bin], { stdio: "ignore" });
107
+ if (r.status === 0) {
108
+ return {
109
+ ok: true,
110
+ name: "acp agent",
111
+ detail: agentCmd,
112
+ resolved: agentCmd,
113
+ };
114
+ }
115
+ return {
116
+ ok: false,
117
+ name: "acp agent",
118
+ detail: agentCmd,
119
+ hint: `'${bin}' not found in PATH.\nverify the binary is installed, or set agent_cmd to "auto" for automatic detection.`,
120
+ };
121
+ }
122
+ /**
123
+ * Probe whether `port` can be bound on 0.0.0.0 (matching what
124
+ * `server.listen` actually uses). Listens, then closes immediately.
125
+ * There's a tiny race window between close and the real
126
+ * server.listen() — that's fine for diagnostics: the goal is a friendly
127
+ * "port already in use" hint, not a hard guarantee.
128
+ *
129
+ * Port 0 means "let the OS pick"; we treat it as always-free.
130
+ */
131
+ async function checkPort(port) {
132
+ if (port === 0) {
133
+ return { ok: true, name: "port", detail: "0 (OS-assigned)" };
134
+ }
135
+ // Probe must bind to the same address family as the real server
136
+ // (server.ts uses "0.0.0.0"). Probing 127.0.0.1 lets a foreign
137
+ // listener on 0.0.0.0:PORT slip past preflight and only surface as
138
+ // EADDRINUSE during the real server.listen() — exactly the case
139
+ // we're trying to catch.
140
+ const result = await new Promise((settle) => {
141
+ const probe = createServer();
142
+ probe.once("error", (err) => {
143
+ settle({ code: err.code ?? "unknown" });
144
+ });
145
+ probe.listen(port, "0.0.0.0", () => {
146
+ probe.close(() => {
147
+ settle({});
148
+ });
149
+ });
150
+ });
151
+ if (!result.code) {
152
+ return { ok: true, name: "port", detail: String(port) };
153
+ }
154
+ if (result.code === "EADDRINUSE") {
155
+ return {
156
+ ok: false,
157
+ name: "port",
158
+ detail: `${port} (in use)`,
159
+ hint: `port ${port} is already in use (EADDRINUSE).\nfind the owner: ${process.platform === "win32"
160
+ ? `netstat -ano | findstr :${port}`
161
+ : `lsof -nP -iTCP:${port} -sTCP:LISTEN`}\nor change \`port\` in config.toml to a free port.`,
162
+ };
163
+ }
164
+ return {
165
+ ok: false,
166
+ name: "port",
167
+ detail: `${port} (${result.code})`,
168
+ hint: `cannot bind port ${port}: ${result.code}\ncheck firewall / permissions, or change \`port\` in config.toml.`,
169
+ };
170
+ }
171
+ /**
172
+ * Run all preflight checks in order. Prints each result; exits process
173
+ * (78) on first failure. On success, returns the resolved agent command
174
+ * so the caller doesn't need to re-detect.
175
+ */
176
+ export async function runPreflight(opts) {
177
+ const checks = [];
178
+ checks.push(checkNodeVersion());
179
+ checks.push(checkDataDir(opts.data_dir));
180
+ const agent = checkAgent(opts.agent_cmd);
181
+ checks.push(agent);
182
+ checks.push(await checkPort(opts.port));
183
+ for (const c of checks) {
184
+ if (c.ok)
185
+ printOk(c);
186
+ else {
187
+ printFail(c);
188
+ process.exit(78);
189
+ }
190
+ }
191
+ // After the loop above all checks are ok — narrow the agent result.
192
+ return {
193
+ agentCmd: agent.resolved,
194
+ };
195
+ }