@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.
Files changed (57) hide show
  1. package/README.md +58 -23
  2. package/bin/webagent.mjs +119 -8
  3. package/config.toml +96 -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 +123 -9
  31. package/lib/daemon.js +175 -41
  32. package/lib/event-handler.js +209 -91
  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 +1202 -144
  40. package/lib/server.js +149 -33
  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 +624 -30
  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.2562YGRO.js +0 -10
  56. package/dist/styles.008ve1hx.css +0 -669
  57. package/lib/shared/constants.js +0 -17
package/lib/config.js CHANGED
@@ -1,30 +1,137 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { parse as parseTOML } from "smol-toml";
3
3
  import { z } from "zod";
4
- const ConfigSchema = z.object({
4
+ export const ConfigSchema = z.object({
5
5
  port: z.number().int().positive().default(6800),
6
6
  data_dir: z.string().default("data"),
7
7
  default_cwd: z.string().default(process.cwd()),
8
8
  public_dir: z.string().default("dist"),
9
- agent_cmd: z.string().default("copilot --acp"),
10
- limits: z.object({
9
+ agent_cmd: z.string().default("auto"),
10
+ limits: z
11
+ .object({
11
12
  bash_output: z.number().int().positive().default(1_048_576), // 1 MB
12
13
  image_upload: z.number().int().positive().default(10_485_760), // 10 MB
14
+ file_upload: z.number().int().positive().default(52_428_800), // 50 MB — non-image attachments
13
15
  cancel_timeout: z.number().int().nonnegative().default(10_000), // 10s; 0 disables
14
16
  recent_paths: z.number().int().nonnegative().default(10), // /new menu display limit; 0 = show all
15
17
  recent_paths_ttl: z.number().int().nonnegative().default(30), // days before auto-cleanup; 0 = keep forever
16
- }).default({
18
+ })
19
+ .default({
17
20
  bash_output: 1_048_576,
18
21
  image_upload: 10_485_760,
22
+ file_upload: 52_428_800,
19
23
  cancel_timeout: 10_000,
20
24
  recent_paths: 10,
21
25
  recent_paths_ttl: 30,
22
26
  }),
23
- push: z.object({
24
- vapid_subject: z.string().default("mailto:webagent@localhost"),
25
- }).default({
26
- vapid_subject: "mailto:webagent@localhost",
27
+ push: z
28
+ .object({
29
+ vapid_subject: z.string().default("mailto:noreply@example.com"),
30
+ global_visibility_suppression: z.boolean().default(true),
31
+ })
32
+ .default({
33
+ vapid_subject: "mailto:noreply@example.com",
34
+ global_visibility_suppression: true,
27
35
  }),
36
+ // [title] — title generation sub-session configuration.
37
+ //
38
+ // `model` is an array of case-insensitive substring patterns. When the
39
+ // title sub-session is created, we look at the model list the agent
40
+ // reports (ACP `availableModels`) and pick the first model whose id
41
+ // matches any pattern in order. Match → call `setConfigOption` with
42
+ // that model id; no match → skip the call and inherit the agent's
43
+ // default model (`currentModelId`).
44
+ //
45
+ // Default list targets the cheap/fast tier across major providers:
46
+ // - "haiku" → Anthropic (claude-haiku-*)
47
+ // - "flash-lite" → Google Gemini (gemini-*-flash-lite) [must precede "flash"]
48
+ // - "nano" → OpenAI (gpt-*-nano), Gemini Nano
49
+ // - "mini" → OpenAI (gpt-*-mini, 4o-mini), Mistral
50
+ // - "flash" → Google Gemini (gemini-*-flash)
51
+ // - "lite" → Cohere, generic
52
+ //
53
+ // Set `model = []` to disable substring matching entirely and always
54
+ // inherit the agent's default model. To pin one specific model, pass a
55
+ // single-element array: `model = ["claude-haiku-4.5"]`.
56
+ title: z
57
+ .object({
58
+ model: z
59
+ .array(z.string())
60
+ .default(["haiku", "flash-lite", "nano", "mini", "flash", "lite"]),
61
+ })
62
+ .default({
63
+ model: ["haiku", "flash-lite", "nano", "mini", "flash", "lite"],
64
+ }),
65
+ // [debug] — frontend log level.
66
+ // level ∈ off | debug | info | warn | error. Default "off".
67
+ // Users can override per page-load via `?debug=<level>` in the URL,
68
+ // or at runtime via the /log slash command.
69
+ debug: z
70
+ .object({
71
+ level: z.enum(["off", "debug", "info", "warn", "error"]).default("off"),
72
+ })
73
+ .default({ level: "off" }),
74
+ // [messages] — external notifications primitive.
75
+ // `unprocessed_ttl_days` caps how long an unbound message stays in the
76
+ // inbox before TTL cleanup removes it. 0 = keep forever.
77
+ messages: z
78
+ .object({
79
+ unprocessed_ttl_days: z.number().int().nonnegative().default(30),
80
+ })
81
+ .default({ unprocessed_ttl_days: 30 }),
82
+ // [share] — public read-only session share links.
83
+ // Default: disabled. Dogfood manually flips `enabled = true` after
84
+ // CF Access bypass + Rate Limiting are configured. See docs/share.md.
85
+ // enabled — master kill switch; when false, all share routes
86
+ // return 410 and slash commands are hidden.
87
+ // ttl_hours — global default TTL for public share links. 0 =
88
+ // never expire (default). >0 is clamped to 168 (7d).
89
+ // Per-share override via `shares.ttl_hours` column.
90
+ // csp_enforce — true (default) emits Content-Security-Policy on
91
+ // /s/* and /api/v1/shared/* routes. false emits
92
+ // Content-Security-Policy-Report-Only for rollback.
93
+ // viewer_origin — public viewer URL host; empty string = same as
94
+ // webagent host (default). Useful if viewer is
95
+ // behind a different CF Worker route (e.g.
96
+ // "https://share.example.com").
97
+ // internal_hosts — sanitizer internal-domain allowlist; any token
98
+ // matching these substrings gets rewritten to
99
+ // `<internal-host>` before publishing.
100
+ share: z
101
+ .object({
102
+ enabled: z.boolean().default(false),
103
+ ttl_hours: z.number().int().nonnegative().default(0),
104
+ csp_enforce: z.boolean().default(true),
105
+ viewer_origin: z.string().default(""),
106
+ internal_hosts: z.array(z.string()).default([]),
107
+ })
108
+ .default({
109
+ enabled: false,
110
+ ttl_hours: 0,
111
+ csp_enforce: true,
112
+ viewer_origin: "",
113
+ internal_hosts: [],
114
+ }),
115
+ // [auth] — bearer-token auth knobs.
116
+ // first_run_bootstrap controls the zero-config first-run UX:
117
+ // true (default) — when auth.json file does NOT exist AND stdin
118
+ // is a TTY, server auto-mints a one-time admin
119
+ // token and prints it as part of the startup-
120
+ // doctor stream. Operator copies the token from
121
+ // terminal scrollback and pastes it into the
122
+ // /login form.
123
+ // false — refuse to serve, print `--create-token` hint,
124
+ // exit 78. Use this when deploying behind a
125
+ // supervisor that provisions auth.json
126
+ // out-of-band (CI, Ansible, k8s init container).
127
+ // Either way: if auth.json exists but list is empty (deleted/parse-
128
+ // error), server still exits 78 — that's a config anomaly, not a
129
+ // fresh install.
130
+ auth: z
131
+ .object({
132
+ first_run_bootstrap: z.boolean().default(true),
133
+ })
134
+ .default({ first_run_bootstrap: true }),
28
135
  });
29
136
  let _config = null;
30
137
  function parseArgs() {
@@ -35,7 +142,14 @@ function parseArgs() {
35
142
  return null;
36
143
  }
37
144
  export function loadConfig() {
38
- const configPath = parseArgs();
145
+ return loadConfigFromPath(parseArgs());
146
+ }
147
+ /**
148
+ * Load + validate config from an explicit path (or defaults if null).
149
+ * Lets non-CLI callers (daemon parent) load the same effective config
150
+ * the server would, without needing to mutate process.argv.
151
+ */
152
+ export function loadConfigFromPath(configPath) {
39
153
  let raw = {};
40
154
  if (configPath) {
41
155
  try {
package/lib/daemon.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import { spawn } from "node:child_process";
2
- import { closeSync, existsSync, openSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
2
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
3
3
  import { dirname, isAbsolute, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { atomicWriteFileSync } from "./atomic-write.js";
6
+ import { loadConfigFromPath } from "./config.js";
7
+ import { runStartupChecks, STARTUP_CHECKED_ENV } from "./startup-checks.js";
5
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
6
9
  // ---------------------------------------------------------------------------
7
10
  // Constants
@@ -30,13 +33,15 @@ export function readPidInfo(filePath) {
30
33
  try {
31
34
  unlinkSync(filePath);
32
35
  }
33
- catch { /* ignore */ }
36
+ catch {
37
+ /* ignore */
38
+ }
34
39
  return null;
35
40
  }
36
41
  }
37
42
  /** Write PID info to `filePath`. */
38
43
  export function writePidInfo(filePath, info) {
39
- writeFileSync(filePath, JSON.stringify(info) + "\n");
44
+ atomicWriteFileSync(filePath, JSON.stringify(info) + "\n");
40
45
  }
41
46
  // ---------------------------------------------------------------------------
42
47
  // Arg helpers
@@ -48,35 +53,116 @@ export function isSubcommand(arg) {
48
53
  export function resolveArgs(args) {
49
54
  const result = [...args];
50
55
  for (let i = 0; i < result.length; i++) {
51
- if (result[i] === "--config" && i + 1 < result.length && !isAbsolute(result[i + 1])) {
56
+ if (result[i] === "--config" &&
57
+ i + 1 < result.length &&
58
+ !isAbsolute(result[i + 1])) {
52
59
  result[i + 1] = resolve(result[i + 1]);
53
60
  }
54
61
  }
55
62
  return result;
56
63
  }
57
64
  // ---------------------------------------------------------------------------
65
+ // Config-path extraction for parent-side checks
66
+ // ---------------------------------------------------------------------------
67
+ /**
68
+ * Pull the `--config <path>` value out of the daemon's argv (resolving
69
+ * relative paths against cwd). Used by `cmdStart` to load the same
70
+ * config the server child would, so the parent process can run the
71
+ * unified startup checks (preflight + auth bootstrap) in the operator's
72
+ * TTY before forking. Returns null if no `--config` was passed.
73
+ */
74
+ export function extractConfigPath(args, cwd) {
75
+ const idx = args.indexOf("--config");
76
+ if (idx < 0 || idx + 1 >= args.length)
77
+ return null;
78
+ const v = args[idx + 1];
79
+ return isAbsolute(v) ? v : resolve(cwd, v);
80
+ }
81
+ // ---------------------------------------------------------------------------
82
+ // Restart decision (pure)
83
+ // ---------------------------------------------------------------------------
84
+ /**
85
+ * Sysexits.h EX_CONFIG. Server exits 78 when configuration is bad
86
+ * (missing auth.json + non-TTY, preflight failures, etc.). Restarting
87
+ * cannot fix configuration — supervisor must surface and stop.
88
+ */
89
+ const EX_CONFIG = 78;
90
+ /**
91
+ * Pure decision: should the supervisor restart the child, and with what
92
+ * delay? Side effects (logging, scheduling) live in `runSupervisor`.
93
+ */
94
+ export function decideRestart(code, _signal, ctx) {
95
+ if (ctx.stopping) {
96
+ return { kind: "stop", reason: "supervisor shutting down" };
97
+ }
98
+ if (code === EX_CONFIG) {
99
+ return {
100
+ kind: "stop",
101
+ reason: `child exited with EX_CONFIG (${EX_CONFIG}) — not restarting`,
102
+ };
103
+ }
104
+ const stable = ctx.now - ctx.lastStart > STABLE_THRESHOLD_MS;
105
+ const delay = stable
106
+ ? RESTART_DELAY_INITIAL
107
+ : Math.min(ctx.currentDelay * 2, RESTART_DELAY_MAX);
108
+ return { kind: "restart", delayMs: delay };
109
+ }
110
+ // ---------------------------------------------------------------------------
111
+ // PID file location
112
+ // ---------------------------------------------------------------------------
113
+ //
114
+ // PID file lives in `data_dir`, NOT cwd. This lets multiple instances
115
+ // (e.g. one per agent / port) coexist on the same machine launched from
116
+ // the same shell — each `--config` points at its own data_dir, so each
117
+ // gets its own pid file. The log file stays in cwd (operator-facing).
118
+ function loadDaemonContext(args, cwd) {
119
+ const cfgPath = extractConfigPath(args, cwd);
120
+ const config = loadConfigFromPath(cfgPath);
121
+ const dataDir = isAbsolute(config.data_dir)
122
+ ? config.data_dir
123
+ : resolve(cwd, config.data_dir);
124
+ try {
125
+ mkdirSync(dataDir, { recursive: true });
126
+ }
127
+ catch {
128
+ /* best-effort — start will surface real errors via startup checks */
129
+ }
130
+ return { config, pidFile: join(dataDir, PID_FILE) };
131
+ }
132
+ // ---------------------------------------------------------------------------
58
133
  // Command dispatch
59
134
  // ---------------------------------------------------------------------------
60
135
  export async function run(command, args) {
61
- const pidFile = join(process.cwd(), PID_FILE);
136
+ const { config, pidFile } = loadDaemonContext(args, process.cwd());
62
137
  const logFile = join(process.cwd(), LOG_FILE);
63
138
  switch (command) {
64
- case "start": return cmdStart(pidFile, logFile, args);
65
- case "stop": return cmdStop(pidFile);
66
- case "status": return cmdStatus(pidFile, logFile);
67
- case "restart": return cmdRestart(pidFile, logFile);
139
+ case "start":
140
+ return cmdStart(pidFile, logFile, args, config);
141
+ case "stop":
142
+ return cmdStop(pidFile);
143
+ case "status":
144
+ return cmdStatus(pidFile, logFile);
145
+ case "restart":
146
+ return cmdRestart(pidFile, logFile);
68
147
  }
69
148
  }
70
149
  // ---------------------------------------------------------------------------
71
150
  // Commands
72
151
  // ---------------------------------------------------------------------------
73
- async function cmdStart(pidFile, logFile, args) {
152
+ async function cmdStart(pidFile, logFile, args, config) {
74
153
  const existing = readPidInfo(pidFile);
75
154
  if (existing) {
76
155
  console.log(`webagent is already running (pid ${existing.pid})`);
77
156
  process.exitCode = 1;
78
157
  return;
79
158
  }
159
+ // Run the unified startup gate in this (foreground) process before
160
+ // forking. This is the operator's TTY, so first-run mint banners
161
+ // and `[check]` failures land where they can actually be seen and
162
+ // copy-pasted, instead of being entombed in the daemon log file.
163
+ // Pass WEBAGENT_STARTUP_CHECKED=1 to the supervisor child so it (and
164
+ // the server it spawns) skip re-running the gate.
165
+ await runStartupChecks(config);
80
166
  const serverJs = join(__dirname, "server.js");
81
167
  if (!existsSync(serverJs)) {
82
168
  console.error(`server not found: ${serverJs}`);
@@ -94,10 +180,18 @@ async function cmdStart(pidFile, logFile, args) {
94
180
  writeFileSync(logFile, lines.slice(-LOG_MAX_LINES).join("\n"));
95
181
  }
96
182
  }
97
- catch { /* best-effort */ }
183
+ catch {
184
+ /* best-effort */
185
+ }
98
186
  }
99
187
  const log = openSync(logFile, "a");
100
- const child = spawn(process.execPath, [daemonJs, "__supervisor", ...resolved], { detached: true, stdio: ["ignore", log, log], cwd: process.cwd(), windowsHide: true });
188
+ const child = spawn(process.execPath, [daemonJs, "__supervisor", ...resolved], {
189
+ detached: true,
190
+ stdio: ["ignore", log, log],
191
+ cwd: process.cwd(),
192
+ windowsHide: true,
193
+ env: { ...process.env, [STARTUP_CHECKED_ENV]: "1" },
194
+ });
101
195
  child.unref();
102
196
  closeSync(log);
103
197
  // Poll for PID file (supervisor writes it on startup)
@@ -128,7 +222,9 @@ async function cmdStop(pidFile) {
128
222
  try {
129
223
  unlinkSync(pidFile);
130
224
  }
131
- catch { /* ignore */ }
225
+ catch {
226
+ /* ignore */
227
+ }
132
228
  return;
133
229
  }
134
230
  // Wait for exit
@@ -143,7 +239,9 @@ async function cmdStop(pidFile) {
143
239
  try {
144
240
  unlinkSync(pidFile);
145
241
  }
146
- catch { /* ignore */ }
242
+ catch {
243
+ /* ignore */
244
+ }
147
245
  console.log("webagent stopped");
148
246
  return;
149
247
  }
@@ -177,7 +275,8 @@ async function cmdRestart(pidFile, logFile) {
177
275
  if (process.platform === "win32") {
178
276
  // No SIGHUP on Windows — fall back to stop + start (non-atomic)
179
277
  await cmdStop(pidFile);
180
- await cmdStart(pidFile, logFile, info.args);
278
+ const { config } = loadDaemonContext(info.args, process.cwd());
279
+ await cmdStart(pidFile, logFile, info.args, config);
181
280
  return;
182
281
  }
183
282
  // Unix: atomic restart via SIGHUP to supervisor
@@ -206,8 +305,12 @@ async function cmdRestart(pidFile, logFile) {
206
305
  // ---------------------------------------------------------------------------
207
306
  function runSupervisor(serverArgs) {
208
307
  const serverJs = join(__dirname, "server.js");
209
- const pidFile = join(process.cwd(), PID_FILE);
210
- writePidInfo(pidFile, { pid: process.pid, args: serverArgs, started: new Date().toISOString() });
308
+ const { pidFile } = loadDaemonContext(serverArgs, process.cwd());
309
+ writePidInfo(pidFile, {
310
+ pid: process.pid,
311
+ args: serverArgs,
312
+ started: new Date().toISOString(),
313
+ });
211
314
  let child = null;
212
315
  let stopping = false;
213
316
  let lastStart = 0;
@@ -215,19 +318,33 @@ function runSupervisor(serverArgs) {
215
318
  let timer = null;
216
319
  function spawnServer() {
217
320
  lastStart = Date.now();
218
- child = spawn(process.execPath, [serverJs, ...serverArgs], { stdio: "inherit", windowsHide: true });
321
+ child = spawn(process.execPath, [serverJs, ...serverArgs], {
322
+ stdio: "inherit",
323
+ windowsHide: true,
324
+ });
219
325
  child.on("exit", onChildExit);
220
326
  }
221
327
  function onChildExit(code, signal) {
222
328
  child = null;
223
- if (stopping)
224
- return;
225
- if (Date.now() - lastStart > STABLE_THRESHOLD_MS) {
226
- delay = RESTART_DELAY_INITIAL;
227
- }
228
- else {
229
- delay = Math.min(delay * 2, RESTART_DELAY_MAX);
329
+ const action = decideRestart(code, signal, {
330
+ stopping,
331
+ lastStart,
332
+ now: Date.now(),
333
+ currentDelay: delay,
334
+ });
335
+ if (action.kind === "stop") {
336
+ if (stopping)
337
+ return;
338
+ console.error(`[supervisor] ${action.reason} (code=${code} signal=${signal})`);
339
+ try {
340
+ unlinkSync(pidFile);
341
+ }
342
+ catch {
343
+ /* ignore */
344
+ }
345
+ process.exit(code ?? 1);
230
346
  }
347
+ delay = action.delayMs;
231
348
  console.log(`[supervisor] server exited (code=${code} signal=${signal}), restarting in ${delay}ms`);
232
349
  timer = setTimeout(spawnServer, delay);
233
350
  }
@@ -236,18 +353,27 @@ function runSupervisor(serverArgs) {
236
353
  clearTimeout(timer);
237
354
  timer = null;
238
355
  }
239
- return new Promise((resolve) => {
356
+ return new Promise((innerResolve) => {
240
357
  if (!child) {
241
- resolve();
358
+ innerResolve();
242
359
  return;
243
360
  }
244
361
  const c = child;
245
- c.once("exit", () => resolve());
362
+ // Take ownership of this exit don't let the auto-restart listener
363
+ // race with the explicit respawn (SIGHUP path) or shutdown.
364
+ c.removeListener("exit", onChildExit);
365
+ c.once("exit", () => {
366
+ innerResolve();
367
+ });
246
368
  c.kill("SIGTERM");
247
- setTimeout(() => { try {
248
- c.kill("SIGKILL");
249
- }
250
- catch { /* ignore */ } }, KILL_GRACE_MS);
369
+ setTimeout(() => {
370
+ try {
371
+ c.kill("SIGKILL");
372
+ }
373
+ catch {
374
+ /* ignore */
375
+ }
376
+ }, KILL_GRACE_MS);
251
377
  });
252
378
  }
253
379
  async function shutdown() {
@@ -258,18 +384,26 @@ function runSupervisor(serverArgs) {
258
384
  try {
259
385
  unlinkSync(pidFile);
260
386
  }
261
- catch { /* ignore */ }
387
+ catch {
388
+ /* ignore */
389
+ }
262
390
  process.exit(0);
263
391
  }
264
- process.on("SIGTERM", () => { shutdown(); });
265
- process.on("SIGINT", () => { shutdown(); });
392
+ process.on("SIGTERM", () => {
393
+ void shutdown();
394
+ });
395
+ process.on("SIGINT", () => {
396
+ void shutdown();
397
+ });
266
398
  if (process.platform !== "win32") {
267
- process.on("SIGHUP", async () => {
268
- console.log("[supervisor] SIGHUP received, restarting server");
269
- delay = RESTART_DELAY_INITIAL;
270
- await killChild();
271
- if (!stopping)
272
- spawnServer();
399
+ process.on("SIGHUP", () => {
400
+ void (async () => {
401
+ console.log("[supervisor] SIGHUP received, restarting server");
402
+ delay = RESTART_DELAY_INITIAL;
403
+ await killChild();
404
+ if (!stopping)
405
+ spawnServer();
406
+ })();
273
407
  });
274
408
  }
275
409
  console.log(`[supervisor] started (pid ${process.pid})`);