om-memory-system 4.9.0 → 4.10.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.
@@ -123,7 +123,7 @@ export async function runWebCommand(args, options = {}, online, handover = {}) {
123
123
  process.once("SIGTERM", stop);
124
124
  // The page's Stop and Restart buttons reach the same process through this callback.
125
125
  const { createPowerAction } = await import("./web-power.js");
126
- server.setOnPowerAction(createPowerAction({
126
+ const powerAction = createPowerAction({
127
127
  stopServer: async () => {
128
128
  clearInterval(keeper);
129
129
  process.off("SIGINT", stop);
@@ -140,7 +140,16 @@ export async function runWebCommand(args, options = {}, online, handover = {}) {
140
140
  baseUrl: server.getUrl(),
141
141
  loginItem: action === "--login-item",
142
142
  autostart: options,
143
- }));
143
+ });
144
+ server.setOnPowerAction(powerAction);
145
+ // The update button installs the npm release and restarts onto it through the launcher.
146
+ const { startWebUpdate } = await import("../services/web-update-runtime.js");
147
+ const { launcherPath } = await import("../services/runtime-record.js");
148
+ const { ommsDir } = await import("../services/runtime-handoff.js");
149
+ const { existsSync } = await import("node:fs");
150
+ // Every restart onto the new copy runs the launcher, the login item's included.
151
+ const canRestart = () => existsSync(launcherPath(ommsDir()));
152
+ server.setWebUpdate(startWebUpdate(() => powerAction("update"), canRestart));
144
153
  // A newer `web install` asked this web app to give up the port.
145
154
  server.setOnStepAside(async () => {
146
155
  clearInterval(keeper);
@@ -1,6 +1,8 @@
1
1
  import type { WebAutostartOptions } from "../services/web-autostart.js";
2
2
  /** The two choices the page offers; the web server passes them through unchanged. */
3
3
  export type PowerAction = "stop" | "restart";
4
+ /** A restart through the launcher, so the newest copy on the machine serves after an update. */
5
+ export type ProcessAction = PowerAction | "update";
4
6
  interface SpawnedChild {
5
7
  pid?: number;
6
8
  unref(): void;
@@ -16,6 +18,7 @@ export interface PowerDeps {
16
18
  stdio: "ignore";
17
19
  cwd: string;
18
20
  windowsHide: true;
21
+ env: NodeJS.ProcessEnv;
19
22
  }) => SpawnedChild;
20
23
  /** Restart the login item through the service manager. False when it cannot. */
21
24
  restartLoginItem: () => boolean | Promise<boolean>;
@@ -23,14 +26,29 @@ export interface PowerDeps {
23
26
  writeStartLock: (pid: number) => void | Promise<void>;
24
27
  /** Remove the start lock when it names `pid`. */
25
28
  removeStartLock: (pid: number) => void | Promise<void>;
26
- /** True when an OMMS web app answers health at `baseUrl`. */
27
- probe: (baseUrl: string) => Promise<boolean>;
29
+ /**
30
+ * The OMMS web app that answers at `baseUrl`, or null when none does. The
31
+ * version is null when this process may not read it, for example behind basic auth.
32
+ */
33
+ readOwner: (baseUrl: string) => Promise<{
34
+ instance: string;
35
+ version: string | null;
36
+ } | null>;
37
+ /** Ask the web app at `baseUrl` to step aside for `version`. True when it agrees. */
38
+ stepAside: (baseUrl: string, version: string) => Promise<boolean>;
39
+ /** The instance id the copy reports, so the handoff knows its own copy. */
40
+ newInstance: () => string;
41
+ /** The version on disk now; an in-place upgrade changes it after this process started. */
42
+ version: () => string;
43
+ env: NodeJS.ProcessEnv;
28
44
  sleep: (ms: number) => Promise<void>;
29
45
  now: () => number;
30
46
  exit: (code: number) => void;
31
47
  /** The runtime and arguments of this process, for a detached copy. */
32
48
  execPath: string;
33
49
  args: string[];
50
+ /** The launcher at `~/.omms/bin`, or null when it is missing. */
51
+ launcher: string | null;
34
52
  cwd: string;
35
53
  pid: number;
36
54
  log: (message: string, data: Record<string, unknown>) => void | Promise<void>;
@@ -51,5 +69,5 @@ export declare function createPowerAction(options: {
51
69
  loginItem: boolean;
52
70
  autostart?: WebAutostartOptions;
53
71
  deps?: Partial<PowerDeps>;
54
- }): (action: PowerAction) => Promise<void>;
72
+ }): (action: ProcessAction) => Promise<string | undefined>;
55
73
  export {};
@@ -1,10 +1,24 @@
1
1
  import { spawn as nodeSpawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { existsSync } from "node:fs";
2
4
  import { homedir } from "node:os";
5
+ import { packageVersion } from "../services/package-version.js";
6
+ import { ommsDir } from "../services/runtime-handoff.js";
7
+ import { launcherPath } from "../services/runtime-record.js";
8
+ import { compareVersions } from "../services/version-compare.js";
3
9
  /** How long a fresh copy must stay up before the old process stops serving. */
4
10
  const SPAWN_CHECK_MS = 1_000;
5
11
  /** How long the old process waits for the copy to answer before it serves again. */
6
12
  export const HANDOFF_WAIT_MS = 15_000;
7
13
  const HANDOFF_POLL_MS = 250;
14
+ let tokenHeader = null;
15
+ /** The web app runs on this machine, so the local token file is enough. Read once. */
16
+ function localTokenHeader() {
17
+ tokenHeader ??= import("../services/auth-token.js").then(({ AUTH_HEADER, getOrCreateAuthToken }) => ({
18
+ [AUTH_HEADER]: getOrCreateAuthToken(),
19
+ }));
20
+ return tokenHeader;
21
+ }
8
22
  function productionDeps(options) {
9
23
  return {
10
24
  spawn: nodeSpawn,
@@ -14,23 +28,55 @@ function productionDeps(options) {
14
28
  nodeLockFs.replace(startLockPath(), JSON.stringify({ pid, at: Date.now() }));
15
29
  },
16
30
  removeStartLock: async (pid) => (await import("../services/web-ensure.js")).removeStartLockFor(pid),
17
- probe: async (baseUrl) => {
31
+ readOwner: async (baseUrl) => {
32
+ // Health skips basic auth, so a restart can find its copy with basic auth on.
33
+ let instance;
34
+ try {
35
+ const health = await fetch(`${baseUrl}/api/health`, { signal: AbortSignal.timeout(1_000) });
36
+ const body = (await health.json());
37
+ if (!health.ok || typeof body.instance !== "string")
38
+ return null;
39
+ instance = body.instance;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
18
44
  try {
19
- const response = await fetch(`${baseUrl}/api/health`, {
45
+ const response = await fetch(`${baseUrl}/api/web/status`, {
46
+ headers: await localTokenHeader(),
20
47
  signal: AbortSignal.timeout(1_000),
21
48
  });
22
49
  const body = (await response.json());
23
- return response.ok && body.success === true && body.status === "ok";
50
+ const version = response.ok && typeof body.version === "string" ? body.version : null;
51
+ return { instance, version };
52
+ }
53
+ catch {
54
+ return { instance, version: null };
55
+ }
56
+ },
57
+ stepAside: async (baseUrl, version) => {
58
+ try {
59
+ const response = await fetch(`${baseUrl}/api/web/step-aside`, {
60
+ method: "POST",
61
+ headers: { ...(await localTokenHeader()), "content-type": "application/json" },
62
+ body: JSON.stringify({ version }),
63
+ signal: AbortSignal.timeout(1_000),
64
+ });
65
+ return response.status === 202;
24
66
  }
25
67
  catch {
26
68
  return false;
27
69
  }
28
70
  },
71
+ newInstance: () => randomUUID(),
72
+ version: () => packageVersion(),
73
+ env: process.env,
29
74
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
30
75
  now: () => Date.now(),
31
76
  exit: (code) => process.exit(code),
32
77
  execPath: process.execPath,
33
78
  args: process.argv.slice(1),
79
+ launcher: existsSync(launcherPath(ommsDir())) ? launcherPath(ommsDir()) : null,
34
80
  cwd: homedir(),
35
81
  pid: process.pid,
36
82
  log: async (message, data) => (await import("../services/logger.js")).log(message, data),
@@ -49,14 +95,16 @@ export function createPowerAction(options) {
49
95
  const logFailure = (code) => Promise.resolve(deps.log("Web app restart failed", { code })).catch(() => undefined);
50
96
  let stopped = false;
51
97
  /** Start the copy, check it, stop serving, and wait until the copy answers. */
52
- async function handOff() {
98
+ async function handOff(args) {
53
99
  let child;
100
+ const instance = deps.newInstance();
54
101
  try {
55
- child = deps.spawn(deps.execPath, deps.args, {
102
+ child = deps.spawn(deps.execPath, args, {
56
103
  detached: true,
57
104
  stdio: "ignore",
58
105
  cwd: deps.cwd,
59
106
  windowsHide: true,
107
+ env: { ...deps.env, OMMS_WEB_INSTANCE: instance },
60
108
  });
61
109
  }
62
110
  catch {
@@ -82,12 +130,30 @@ export function createPowerAction(options) {
82
130
  stopped = true;
83
131
  }
84
132
  // The copy waits as a non-owner and takes the port within about 7 seconds.
85
- const began = deps.now();
133
+ let began = deps.now();
134
+ // Each older owner is asked once; another older waiter can take the port after it.
135
+ const askedAside = new Set();
86
136
  while (deps.now() - began < HANDOFF_WAIT_MS) {
87
137
  if (exited)
88
138
  return giveUp("copy-exit", copyPid);
89
- if (await deps.probe(options.baseUrl))
90
- return deps.exit(0);
139
+ const owner = await deps.readOwner(options.baseUrl);
140
+ if (owner?.instance === instance) {
141
+ deps.exit(0);
142
+ return undefined;
143
+ }
144
+ // Another web app that waited for the port took it first.
145
+ if (owner) {
146
+ const ownVersion = deps.version();
147
+ const order = owner.version === null ? null : compareVersions(owner.version, ownVersion);
148
+ if (order === null || order >= 0)
149
+ return yieldTo(child, copyPid);
150
+ if (!askedAside.has(owner.instance)) {
151
+ askedAside.add(owner.instance);
152
+ // The copy needs a fresh wait once the older web app leaves.
153
+ if (await deps.stepAside(options.baseUrl, ownVersion))
154
+ began = deps.now();
155
+ }
156
+ }
91
157
  await deps.sleep(HANDOFF_POLL_MS);
92
158
  }
93
159
  if (exited)
@@ -96,7 +162,15 @@ export function createPowerAction(options) {
96
162
  child.kill("SIGTERM");
97
163
  return giveUp("handoff", copyPid);
98
164
  }
99
- /** Keep or resume serving on the old process, and release the lock it wrote. */
165
+ /** A web app of the same or a newer version serves, so the copy would only wait. */
166
+ async function yieldTo(child, copyPid) {
167
+ child.kill("SIGTERM");
168
+ await deps.removeStartLock(copyPid);
169
+ await logFailure("other-owner");
170
+ deps.exit(0);
171
+ return undefined;
172
+ }
173
+ /** Keep or resume serving on the old process, and release the lock it wrote. Returns the code. */
100
174
  async function giveUp(code, copyPid) {
101
175
  if (copyPid !== undefined)
102
176
  await deps.removeStartLock(copyPid);
@@ -104,7 +178,7 @@ export function createPowerAction(options) {
104
178
  await deps.removeStartLock(deps.pid);
105
179
  await logFailure(code);
106
180
  if (!stopped)
107
- return;
181
+ return code;
108
182
  try {
109
183
  await options.resumeServer();
110
184
  stopped = false;
@@ -113,29 +187,39 @@ export function createPowerAction(options) {
113
187
  await logFailure(error instanceof Error ? error.name : "resume-error");
114
188
  deps.exit(1);
115
189
  }
190
+ return code;
116
191
  }
117
192
  return async (action) => {
118
193
  if (action === "stop") {
119
194
  await options.stopServer();
120
195
  deps.exit(0);
121
- return;
196
+ return undefined;
197
+ }
198
+ // After an update the copy of this process is the old version; the launcher picks the newest.
199
+ if (action === "update" && !deps.launcher) {
200
+ await logFailure("no-launcher");
201
+ return "no-launcher";
122
202
  }
203
+ const args = action === "update" ? [deps.launcher, "web"] : deps.args;
123
204
  stopped = false;
124
205
  try {
125
206
  if (options.loginItem) {
207
+ // The login item runs the launcher, so the service manager restart covers an update too.
126
208
  await options.stopServer();
127
209
  stopped = true;
128
- if (await deps.restartLoginItem())
129
- return deps.exit(0);
210
+ if (await deps.restartLoginItem()) {
211
+ deps.exit(0);
212
+ return undefined;
213
+ }
130
214
  }
131
215
  else {
132
216
  // Hold other hosts off until the copy owns the port.
133
217
  await deps.writeStartLock(deps.pid);
134
218
  }
135
- await handOff();
219
+ return await handOff(args);
136
220
  }
137
221
  catch (error) {
138
- await giveUp(error instanceof Error ? error.name : "unknown");
222
+ return giveUp(error instanceof Error ? error.name : "unknown");
139
223
  }
140
224
  };
141
225
  }
@@ -1,3 +1,4 @@
1
+ import type { WebUpdate } from "./web-update.js";
1
2
  import { WebAuth } from "./web-auth.js";
2
3
  type NodeEventSource = {
3
4
  on(event: string, listener: (...args: any[]) => void): unknown;
@@ -37,6 +38,7 @@ export declare class WebServer {
37
38
  private onPortsExhaustedCallback;
38
39
  private onStepAsideCallback;
39
40
  private onPowerActionCallback;
41
+ private webUpdate;
40
42
  private powerActionRunning;
41
43
  private stepAsideTimer;
42
44
  private holdOffTimer;
@@ -59,9 +61,13 @@ export declare class WebServer {
59
61
  /** Standalone web apps register this to exit when a newer OMMS asks them to step aside. */
60
62
  setOnStepAside(callback: () => void | Promise<void>): void;
61
63
  /** Standalone web apps register this to stop or restart from the page. */
62
- setOnPowerAction(callback: (action: PowerAction) => void | Promise<void>): void;
64
+ setOnPowerAction(callback: (action: PowerAction) => unknown): void;
65
+ /** Standalone web apps register this to report and install npm releases. */
66
+ setWebUpdate(update: Pick<WebUpdate, "status" | "requestInstall">): void;
63
67
  /** Only a local caller may stop the web app; the token alone is not enough. */
64
68
  private handlePowerAction;
69
+ /** Install the npm release and restart onto it. Same guards as Restart. */
70
+ private handleUpdate;
65
71
  /** API token routes. Only a caller on this machine with the local token manages tokens. */
66
72
  private handleTokenRoute;
67
73
  /** Profile catch-up: a preview for anyone with API access; runs only from this machine. */
@@ -77,6 +83,7 @@ export declare class WebServer {
77
83
  start(): Promise<void>;
78
84
  private _start;
79
85
  private startHealthCheckLoop;
86
+ private ownerIsNewer;
80
87
  private stopHealthCheckLoop;
81
88
  private attemptTakeover;
82
89
  private notifyPortsExhausted;
@@ -142,10 +142,19 @@ export function nextFallbackPort(currentPort, failedTakeovers, maxFallbackPort,
142
142
  return currentPort + 1;
143
143
  }
144
144
  const STEP_ASIDE_HOLD_OFF_MS = 60_000;
145
+ /** What a web app without an npm check reports, such as one inside a host session. */
146
+ const NO_UPDATE = { available: null, state: "idle", code: null, canInstall: false };
145
147
  /** Lets the 202 reply reach the caller before the server stops. */
146
148
  const STEP_ASIDE_REPLY_GRACE_MS = 100;
147
- /** Differs between processes, so the page can tell a restarted web app from the old one. */
148
- const PROCESS_INSTANCE = randomUUID();
149
+ /**
150
+ * Differs between processes, so the page can tell a restarted web app from the old one.
151
+ * A restart names its copy's instance, so it can tell its copy from another web app.
152
+ */
153
+ const PROCESS_INSTANCE = /^[0-9a-f-]{36}$/.test(process.env.OMMS_WEB_INSTANCE ?? "")
154
+ ? process.env.OMMS_WEB_INSTANCE
155
+ : randomUUID();
156
+ // Child processes, such as an OpenCode model list, must not report the same instance.
157
+ delete process.env.OMMS_WEB_INSTANCE;
149
158
  export class WebServer {
150
159
  server = null;
151
160
  config;
@@ -156,6 +165,7 @@ export class WebServer {
156
165
  onPortsExhaustedCallback = null;
157
166
  onStepAsideCallback = null;
158
167
  onPowerActionCallback = null;
168
+ webUpdate = null;
159
169
  powerActionRunning = false;
160
170
  stepAsideTimer = null;
161
171
  holdOffTimer = null;
@@ -222,6 +232,10 @@ export class WebServer {
222
232
  setOnPowerAction(callback) {
223
233
  this.onPowerActionCallback = callback;
224
234
  }
235
+ /** Standalone web apps register this to report and install npm releases. */
236
+ setWebUpdate(update) {
237
+ this.webUpdate = update;
238
+ }
225
239
  /** Only a local caller may stop the web app; the token alone is not enough. */
226
240
  handlePowerAction(action, remoteAddress) {
227
241
  const record = (outcome) => log("Web server power request", { action, outcome, ownVersion: packageVersion() });
@@ -234,6 +248,12 @@ export class WebServer {
234
248
  record("unsupported");
235
249
  return this.jsonResponse({ success: false, error: "Not supported by this web app" }, 409);
236
250
  }
251
+ // An update restarts through the same callback; a Stop or Restart now would cut npm off.
252
+ const updateState = this.webUpdate?.status().state;
253
+ if (updateState === "installing" || updateState === "restarting") {
254
+ record("refused_update_running");
255
+ return this.jsonResponse({ success: false, error: "An update is running" }, 409);
256
+ }
237
257
  // A repeated request must not start a second stop or restart while one runs.
238
258
  if (this.powerActionRunning) {
239
259
  record("already_running");
@@ -255,6 +275,24 @@ export class WebServer {
255
275
  }, STEP_ASIDE_REPLY_GRACE_MS).unref();
256
276
  return this.jsonResponse({ success: true }, 202);
257
277
  }
278
+ /** Install the npm release and restart onto it. Same guards as Restart. */
279
+ handleUpdate(remoteAddress) {
280
+ const record = (outcome) => log("Web server update request", { outcome, ownVersion: packageVersion() });
281
+ if (!isLoopbackAddress(remoteAddress)) {
282
+ record("refused_not_loopback");
283
+ return this.jsonResponse({ success: false, error: "Loopback caller required" }, 403);
284
+ }
285
+ if (this.powerActionRunning) {
286
+ record("refused_power_running");
287
+ return this.jsonResponse({ success: false, error: "A stop or restart is running" }, 409);
288
+ }
289
+ const reply = this.webUpdate?.requestInstall() ?? "cannot-install";
290
+ record(reply);
291
+ if (reply === "accepted")
292
+ return this.jsonResponse({ success: true }, 202);
293
+ const error = reply === "no-update" ? "No newer release" : "This web app cannot install";
294
+ return this.jsonResponse({ success: false, error }, 409);
295
+ }
258
296
  /** API token routes. Only a caller on this machine with the local token manages tokens. */
259
297
  async handleTokenRoute(req, path, method, remoteAddress) {
260
298
  if (!isLoopbackAddress(remoteAddress)) {
@@ -540,8 +578,34 @@ export class WebServer {
540
578
  this.stopHealthCheckLoop();
541
579
  await this.attemptTakeover();
542
580
  }
581
+ else if (this.onStepAsideCallback && (await this.ownerIsNewer())) {
582
+ // A standalone web app behind a newer one would serve old code after a restart.
583
+ this.stopHealthCheckLoop();
584
+ log("Web server waiter retired", { ownVersion: packageVersion() });
585
+ try {
586
+ await this.onStepAsideCallback();
587
+ }
588
+ catch (error) {
589
+ log("Step-aside callback error", { error: String(error) });
590
+ }
591
+ }
543
592
  }, 5000);
544
593
  }
594
+ async ownerIsNewer() {
595
+ try {
596
+ const response = await fetch(`${this.getUrl()}/api/web/status`, {
597
+ headers: { [AUTH_HEADER]: getOrCreateAuthToken() },
598
+ signal: AbortSignal.timeout(2_000),
599
+ });
600
+ if (!response.ok)
601
+ return false;
602
+ const { version } = (await response.json());
603
+ return typeof version === "string" && isOlderVersion(packageVersion(), version);
604
+ }
605
+ catch {
606
+ return false;
607
+ }
608
+ }
545
609
  stopHealthCheckLoop() {
546
610
  if (this.healthCheckInterval) {
547
611
  clearInterval(this.healthCheckInterval);
@@ -729,6 +793,8 @@ export class WebServer {
729
793
  success: true,
730
794
  status: "ok",
731
795
  authEnabled: auth?.isEnabled() ?? false,
796
+ // Opaque per process; a restart finds its copy by it even behind basic auth.
797
+ instance: PROCESS_INSTANCE,
732
798
  });
733
799
  }
734
800
  if (path === "/" || path === "/index.html" || path === "/settings") {
@@ -813,11 +879,15 @@ export class WebServer {
813
879
  // Keys, tokens, and the embedder can be changed only from this machine.
814
880
  isLocal: isLoopbackAddress(remoteAddress),
815
881
  instance: PROCESS_INSTANCE,
882
+ update: this.webUpdate?.status() ?? NO_UPDATE,
816
883
  });
817
884
  }
818
885
  if ((path === "/api/web/stop" || path === "/api/web/restart") && method === "POST") {
819
886
  return this.handlePowerAction(path === "/api/web/stop" ? "stop" : "restart", remoteAddress);
820
887
  }
888
+ if (path === "/api/web/update" && method === "POST") {
889
+ return this.handleUpdate(remoteAddress);
890
+ }
821
891
  if (path === "/api/web/step-aside" && method === "POST") {
822
892
  const body = await req.json().catch(() => null);
823
893
  return this.handleStepAside(remoteAddress, body);
@@ -0,0 +1,3 @@
1
+ import { WebUpdate } from "./web-update.js";
2
+ /** The npm check and installer of a standalone web app, with the real process, npm, and clock. */
3
+ export declare function startWebUpdate(restart: () => Promise<string | undefined>, canRestart: () => boolean): WebUpdate;
@@ -0,0 +1,47 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { globalCommandVersion } from "./global-version.js";
4
+ import { log } from "./logger.js";
5
+ import { packageVersion } from "./package-version.js";
6
+ import { WebUpdate } from "./web-update.js";
7
+ /** The npm check and installer of a standalone web app, with the real process, npm, and clock. */
8
+ export function startWebUpdate(restart, canRestart) {
9
+ const update = new WebUpdate({
10
+ version: packageVersion(),
11
+ enabled: process.env.OMMS_DISABLE_UPDATE_CHECK !== "1",
12
+ fetch,
13
+ setInterval: (callback, ms) => setInterval(callback, ms),
14
+ log,
15
+ execPath: process.execPath,
16
+ platform: process.platform,
17
+ exists: existsSync,
18
+ spawn: (command, args, options) => spawn(command, args, {
19
+ shell: options.shell,
20
+ stdio: ["ignore", "ignore", "pipe"],
21
+ windowsHide: true,
22
+ }),
23
+ // npm installs beside this Node.js, so read that install, not the first command on PATH.
24
+ globalVersion: () => globalCommandVersion({ find: () => null }).version,
25
+ restart,
26
+ canRestart,
27
+ // Resolves once npm has stopped. `taskkill /T` also stops npm under the cmd.exe
28
+ // wrapper that `shell: true` starts; the child's own exit confirms it.
29
+ killTree: (child) => new Promise((resolve) => {
30
+ child.on("exit", () => resolve());
31
+ if (process.platform === "win32" && child.pid) {
32
+ spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
33
+ stdio: "ignore",
34
+ windowsHide: true,
35
+ }).on("error", () => child.kill("SIGKILL"));
36
+ }
37
+ else {
38
+ child.kill("SIGTERM");
39
+ }
40
+ }),
41
+ setTimeout: (callback, ms) => setTimeout(callback, ms),
42
+ clearTimeout: (handle) => clearTimeout(handle),
43
+ now: () => Date.now(),
44
+ });
45
+ update.start();
46
+ return update;
47
+ }
@@ -0,0 +1,76 @@
1
+ export declare const UPDATE_CHECK_INTERVAL_MS: number;
2
+ /** An install that takes longer is stopped, and the web app keeps serving. */
3
+ export declare const INSTALL_TIMEOUT_MS: number;
4
+ export type UpdateState = "idle" | "installing" | "restarting" | "failed";
5
+ /** The `update` field of `GET /api/web/status`. */
6
+ export interface UpdateStatus {
7
+ /** The newer release on npm, or null. */
8
+ available: string | null;
9
+ state: UpdateState;
10
+ /** The failure code while `state` is `failed`. */
11
+ code: string | null;
12
+ /** True when this web app can install the release itself. */
13
+ canInstall: boolean;
14
+ }
15
+ export interface WebUpdateDeps {
16
+ /** The running version. */
17
+ version: string;
18
+ /** False when `OMMS_DISABLE_UPDATE_CHECK` is `1`. */
19
+ enabled: boolean;
20
+ fetch: typeof fetch;
21
+ setInterval: (callback: () => void, ms: number) => {
22
+ unref?: () => void;
23
+ };
24
+ log: (message: string, data: Record<string, unknown>) => void;
25
+ /** The runtime of this web app; npm must sit beside it. */
26
+ execPath: string;
27
+ platform: NodeJS.Platform;
28
+ exists: (path: string) => boolean;
29
+ spawn: (command: string, args: string[], options: {
30
+ shell: boolean;
31
+ }) => InstallChild;
32
+ /** The version of the global install after npm ran, or null. */
33
+ globalVersion: () => string | null;
34
+ /** False when no restart could serve the new copy, such as a missing launcher. Checked before npm runs. */
35
+ canRestart: () => boolean;
36
+ /** Stop npm and its children. On Windows the shell wrapper alone would leave npm running. */
37
+ killTree: (child: InstallChild) => Promise<void>;
38
+ /** Restart onto the newest copy. Returns a failure code when this web app keeps serving. */
39
+ restart: () => Promise<string | undefined>;
40
+ setTimeout: (callback: () => void, ms: number) => unknown;
41
+ clearTimeout: (handle: unknown) => void;
42
+ now: () => number;
43
+ }
44
+ /** The parts of a child process the install runner uses. */
45
+ export interface InstallChild {
46
+ pid?: number;
47
+ stderr: {
48
+ on(event: "data", listener: (chunk: Buffer | string) => void): unknown;
49
+ } | null;
50
+ on(event: "error", listener: () => void): unknown;
51
+ on(event: "exit", listener: (code: number | null) => void): unknown;
52
+ kill(signal?: NodeJS.Signals): boolean;
53
+ }
54
+ export type InstallRequest = "accepted" | "no-update" | "cannot-install";
55
+ /** A failure code from npm's error stream. The text itself never leaves this function. */
56
+ export declare function npmFailureCode(errorText: string): string;
57
+ export declare class WebUpdate {
58
+ private readonly deps;
59
+ private available;
60
+ private state;
61
+ private code;
62
+ private started;
63
+ constructor(deps: WebUpdateDeps);
64
+ /** Check now, then every 6 hours. A second call does nothing. */
65
+ start(): void;
66
+ /** Read npm `latest`. A failed read keeps the last result. */
67
+ check(): Promise<void>;
68
+ /** The npm that installs into the prefix the launcher reads. */
69
+ private npmPath;
70
+ status(): UpdateStatus;
71
+ private canInstall;
72
+ /** Start the install and the restart. A request while one runs joins it. */
73
+ requestInstall(): InstallRequest;
74
+ private install;
75
+ private runNpm;
76
+ }