@lelouchhe/webagent 0.3.0 → 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.
- package/README.md +58 -23
- package/bin/webagent.mjs +119 -8
- package/config.toml +96 -3
- package/dist/index.html +64 -41
- package/dist/js/app.GSAIYHML.js +4 -0
- package/dist/js/chunk.AJZBJBMO.js +1 -0
- package/dist/js/chunk.CGWFHJI2.js +76 -0
- package/dist/js/chunk.D4ZYHJAM.js +1 -0
- package/dist/js/chunk.VZXGXFNN.js +5 -0
- package/dist/js/login.PYIK52HN.js +1 -0
- package/dist/js/viewer.6DT53STL.js +1 -0
- package/dist/login.html +49 -0
- package/dist/share-viewer.00gubshk.css +114 -0
- package/dist/share-viewer.html +53 -0
- package/dist/styles.012p32dz.css +1443 -0
- package/dist/sw.js +79 -27
- package/dist/theme-init.js +6 -0
- package/lib/agent-detect.js +110 -0
- package/lib/atomic-write.js +50 -0
- package/lib/attachment-dispatch.js +86 -0
- package/lib/attachment-interceptor.js +130 -0
- package/lib/attachment-labels.js +139 -0
- package/lib/attachments.js +154 -0
- package/lib/auth-middleware.js +102 -0
- package/lib/auth-store.js +269 -0
- package/lib/auth.js +89 -0
- package/lib/bootstrap.js +70 -0
- package/lib/bridge.js +244 -93
- package/lib/client-registry.js +60 -0
- package/lib/config.js +123 -9
- package/lib/daemon.js +175 -41
- package/lib/event-handler.js +209 -91
- package/lib/log-fmt.js +67 -0
- package/lib/log.js +83 -0
- package/lib/message-cleanup.js +48 -0
- package/lib/mode-bucket.js +62 -0
- package/lib/preflight.js +195 -0
- package/lib/push-service.js +338 -45
- package/lib/routes.js +1202 -144
- package/lib/server.js +149 -33
- package/lib/session-manager.js +164 -18
- package/lib/session-state.js +160 -0
- package/lib/sessions-anchor.js +28 -0
- package/lib/share/cleanup.js +45 -0
- package/lib/share/routes.js +972 -0
- package/lib/share/sanitize.js +179 -0
- package/lib/sse-manager.js +94 -8
- package/lib/sse-ticket.js +45 -0
- package/lib/startup-checks.js +94 -0
- package/lib/store.js +624 -30
- package/lib/title-service.js +42 -9
- package/lib/tokens.js +50 -0
- package/lib/types.js +23 -0
- package/package.json +38 -4
- package/dist/js/app.2562YGRO.js +0 -10
- package/dist/styles.008ve1hx.css +0 -669
- package/lib/shared/constants.js +0 -17
package/lib/preflight.js
ADDED
|
@@ -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
|
+
}
|