om-memory-system 4.10.0 → 4.11.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/dist/cli/index.js CHANGED
@@ -15,6 +15,7 @@ Commands:
15
15
  import-claude-history Import Claude Code history into omms
16
16
  web Start the web app in the foreground
17
17
  web install|uninstall|status Manage the web app login item
18
+ web update Update the global install and restart the web app
18
19
  memory <mode> [options] Search, add, list, or forget memories (run with --help)
19
20
  profile-catch-up Analyse every prompt waiting for profile learning (run with --help)
20
21
  claude-hook <event> Run a Claude Code hook (used by the Claude Code plugin)
@@ -7,4 +7,4 @@ export interface WebHandoverDeps {
7
7
  version?: string;
8
8
  }
9
9
  /** Run the standalone web app or manage its per-user login item. */
10
- export declare function runWebCommand(args: string[], options?: WebAutostartOptions, online?: () => Promise<boolean>, handover?: WebHandoverDeps): Promise<number>;
10
+ export declare function runWebCommand(args: string[], options?: WebAutostartOptions, online?: () => Promise<boolean>, handover?: WebHandoverDeps, update?: Partial<import("./web-update-command.js").WebUpdateCommandDeps>): Promise<number>;
@@ -31,23 +31,30 @@ async function announceHandover(ctx, handover) {
31
31
  }
32
32
  }
33
33
  /** Run the standalone web app or manage its per-user login item. */
34
- export async function runWebCommand(args, options = {}, online, handover = {}) {
34
+ export async function runWebCommand(args, options = {}, online, handover = {}, update = {}) {
35
35
  const home = options.home ?? homedir();
36
36
  const config = await import("../config.js");
37
37
  config.initConfig(home);
38
38
  const { installWebAutostart, removeWebAutostart, webAutostartStatus } = await import("../services/web-autostart.js");
39
39
  const [action, ...rest] = args;
40
40
  if (rest.length ||
41
- (action && !["install", "uninstall", "status", "--login-item"].includes(action))) {
42
- console.error("Usage: om-memory-system web [install|uninstall|status]");
41
+ (action && !["install", "uninstall", "status", "update", "--login-item"].includes(action))) {
42
+ console.error("Usage: om-memory-system web [install|uninstall|status|update]");
43
43
  return 1;
44
44
  }
45
- if (action === "install" && !config.CONFIG.webServerEnabled) {
45
+ if ((action === "install" || action === "update") && !config.CONFIG.webServerEnabled) {
46
46
  console.error("OMMS web server is disabled (webServerEnabled is false)");
47
47
  return 1;
48
48
  }
49
49
  const { webServerUrl } = await import("../services/web-api-auth.js");
50
50
  const url = webServerUrl(config.CONFIG.webServerHost, config.CONFIG.webServerPort);
51
+ if (action === "update") {
52
+ const { productionWebUpdateDeps, runWebUpdate } = await import("./web-update-command.js");
53
+ return runWebUpdate({
54
+ ...(await productionWebUpdateDeps({ url, home, autostart: options })),
55
+ ...update,
56
+ });
57
+ }
51
58
  if (action === "install" || action === "uninstall") {
52
59
  const { readGlobalConfigRevision, writeGlobalConfigKeys } = await import("../services/global-config-writer.js");
53
60
  await writeGlobalConfigKeys({ webServerAutoStart: action === "install" }, readGlobalConfigRevision());
@@ -0,0 +1,38 @@
1
+ import type { InstallResult } from "../services/web-update.js";
2
+ /** How long the command waits for the fresh web app to answer. */
3
+ export declare const FRESH_WAIT_MS = 15000;
4
+ export interface WebUpdateCommandDeps {
5
+ url: string;
6
+ /** The local token header; the web app runs on this machine. */
7
+ headers: Record<string, string>;
8
+ /** The version this command runs as, the newest copy after the hand-off. */
9
+ version: string;
10
+ fetchFn: typeof fetch;
11
+ sleep: (ms: number) => Promise<void>;
12
+ now: () => number;
13
+ /** npm `latest`, or null when the registry does not answer. */
14
+ latest: () => Promise<string | null>;
15
+ /** The global install's version from its `package.json`, or null. */
16
+ globalVersion: () => string | null;
17
+ install: (target: string) => Promise<InstallResult>;
18
+ loginItemInstalled: () => boolean;
19
+ /** Restart the login item through the service manager. False when it cannot. */
20
+ restartLoginItem: () => boolean;
21
+ /** Start a detached web app through the launcher. False when there is nothing to start. */
22
+ startDetached: () => boolean;
23
+ /** Take the start lock. False when another caller holds a fresh one. */
24
+ takeStartLock: () => boolean;
25
+ /** Remove the start lock, only when this process holds it. */
26
+ removeStartLock: () => void;
27
+ writeRetireMarker: (before: number) => void;
28
+ print: (line: string) => void;
29
+ log: (message: string, data: Record<string, unknown>) => void;
30
+ }
31
+ /** Run `web update`. Returns the exit code. */
32
+ export declare function runWebUpdate(deps: WebUpdateCommandDeps): Promise<number>;
33
+ /** The real network, npm, login item, and launcher. */
34
+ export declare function productionWebUpdateDeps(options: {
35
+ url: string;
36
+ home: string;
37
+ autostart: import("../services/web-autostart.js").WebAutostartOptions;
38
+ }): Promise<WebUpdateCommandDeps>;
@@ -0,0 +1,211 @@
1
+ import { compareVersions } from "../services/version-compare.js";
2
+ // `om-memory-system web update`: bring the global install to npm `latest`, then
3
+ // replace every standalone web app with one fresh web app on the newest copy.
4
+ // No process is killed: each web app stops itself after a request.
5
+ /** How long the command waits for the fresh web app to answer. */
6
+ export const FRESH_WAIT_MS = 15_000;
7
+ const POLL_MS = 250;
8
+ const REQUEST_TIMEOUT_MS = 2_000;
9
+ /** The web app on the port, or null when none answers. */
10
+ async function readOwner(deps) {
11
+ let instance;
12
+ try {
13
+ // Health skips basic auth, so the command finds a web app with basic auth on.
14
+ const health = await deps.fetchFn(`${deps.url}/api/health`, {
15
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
16
+ });
17
+ const body = (await health.json());
18
+ if (!health.ok || typeof body.instance !== "string")
19
+ return null;
20
+ instance = body.instance;
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ try {
26
+ const response = await deps.fetchFn(`${deps.url}/api/web/status`, {
27
+ headers: deps.headers,
28
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
29
+ });
30
+ const body = (await response.json());
31
+ return {
32
+ instance,
33
+ version: response.ok && typeof body.version === "string" ? body.version : null,
34
+ };
35
+ }
36
+ catch {
37
+ return { instance, version: null };
38
+ }
39
+ }
40
+ /** Ask the web app on the port to step aside for a replace. True when it agrees. */
41
+ async function stepAside(deps) {
42
+ try {
43
+ const reply = await deps.fetchFn(`${deps.url}/api/web/step-aside`, {
44
+ method: "POST",
45
+ headers: { ...deps.headers, "content-type": "application/json" },
46
+ body: JSON.stringify({ version: deps.version, replace: true }),
47
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
48
+ });
49
+ return reply.status === 202;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ /** Run `web update`. Returns the exit code. */
56
+ export async function runWebUpdate(deps) {
57
+ const began = deps.now();
58
+ const record = { ownVersion: deps.version };
59
+ const finish = (exitCode, outcome, code = null) => {
60
+ deps.log("Web app update command", {
61
+ ...record,
62
+ outcome,
63
+ code,
64
+ durationMs: deps.now() - began,
65
+ });
66
+ return exitCode;
67
+ };
68
+ // 1. The global install.
69
+ const latest = await deps.latest();
70
+ const before = deps.globalVersion();
71
+ Object.assign(record, { latest, globalBefore: before });
72
+ if (latest === null) {
73
+ record.npm = "unreachable";
74
+ deps.print("Could not check npm for a release. Replacing the web app anyway.");
75
+ }
76
+ else if (before === null || (compareVersions(before, latest) ?? 0) < 0) {
77
+ record.npm = "ran";
78
+ deps.print(`Installing om-memory-system@${latest} globally (was ${before ?? "not installed"})`);
79
+ const { code } = await deps.install(latest);
80
+ const after = deps.globalVersion();
81
+ record.globalAfter = after;
82
+ const failure = code ?? (after === latest ? null : "version-mismatch");
83
+ if (failure) {
84
+ deps.print(`Global install failed (${failure}). The web app keeps running. Run: npm i -g om-memory-system@${latest}`);
85
+ return finish(1, "install-failed", failure);
86
+ }
87
+ deps.print(`Global install: ${latest}`);
88
+ }
89
+ else {
90
+ record.npm = "skipped";
91
+ deps.print(`Global install is up to date (${before}).`);
92
+ }
93
+ // 2. Replace every standalone web app. Hosts wait while no web app serves.
94
+ // A fresh lock of another caller is left alone, and so is that caller's start.
95
+ if (!deps.takeStartLock()) {
96
+ deps.print("Another start is running (a host start or another web update). Wait a few seconds, then run om-memory-system web update again.");
97
+ return finish(1, "lock-held", "lock-held");
98
+ }
99
+ try {
100
+ deps.writeRetireMarker(deps.now());
101
+ const owner = await readOwner(deps);
102
+ record.oldOwner = owner?.version ?? null;
103
+ // A web app before `replace` existed ignores the flag and steps aside only for a newer caller.
104
+ if (owner && !(await stepAside(deps))) {
105
+ const name = owner.version ? `OMMS ${owner.version}` : "Another web app";
106
+ deps.print(`${name} holds ${deps.url} and cannot hand it over. Stop that web app (Stop in its power menu, Ctrl+C in its terminal, or quit the session that runs it), then run om-memory-system web update again.`);
107
+ return finish(1, "stuck", "stuck");
108
+ }
109
+ // 3. Start one fresh web app on the newest copy.
110
+ const viaLoginItem = deps.loginItemInstalled() && deps.restartLoginItem();
111
+ record.start = viaLoginItem ? "login-item" : "launcher";
112
+ if (!viaLoginItem && !deps.startDetached()) {
113
+ deps.print("No OMMS copy to start. Run om-memory-system web to start the web app.");
114
+ return finish(1, "no-copy", "no-copy");
115
+ }
116
+ // 4. Wait for a web app that is not the old one.
117
+ const askedAside = new Set(owner ? [owner.instance] : []);
118
+ const deadline = deps.now() + FRESH_WAIT_MS;
119
+ while (deps.now() < deadline) {
120
+ const current = await readOwner(deps);
121
+ // A web app that hides its version cannot be told from an old waiter, so keep waiting.
122
+ if (current?.version && !askedAside.has(current.instance)) {
123
+ if ((compareVersions(current.version, deps.version) ?? 0) >= 0) {
124
+ record.served = current.version;
125
+ deps.print(`OMMS web app: ${deps.url} (version ${current.version})`);
126
+ return finish(0, "replaced");
127
+ }
128
+ // An older web app that waited for the port took it before the fresh one.
129
+ askedAside.add(current.instance);
130
+ await stepAside(deps);
131
+ }
132
+ await deps.sleep(POLL_MS);
133
+ }
134
+ deps.print(`No web app answered on ${deps.url} within ${FRESH_WAIT_MS / 1000} seconds. Run om-memory-system web status.`);
135
+ return finish(1, "no-answer", "no-answer");
136
+ }
137
+ finally {
138
+ deps.removeStartLock();
139
+ }
140
+ }
141
+ /** The real network, npm, login item, and launcher. */
142
+ export async function productionWebUpdateDeps(options) {
143
+ const [{ spawn }, { existsSync }, { join }, { AUTH_HEADER, getOrCreateAuthToken }, { packageVersion }, { latestNpmVersion }, { globalCommandVersion }, { runGlobalInstall }, { nodeInstallDeps }, { restartWebAutostart, webAutostartStatus }, { launcherPath, recordedCopy }, { ommsDir }, { nodeLockFs, processAlive, removeStartLockFor, startLockPath, takeStartLock }, { writeRetireMarker }, { log },] = await Promise.all([
144
+ import("node:child_process"),
145
+ import("node:fs"),
146
+ import("node:path"),
147
+ import("../services/auth-token.js"),
148
+ import("../services/package-version.js"),
149
+ import("../services/update-check.js"),
150
+ import("../services/global-version.js"),
151
+ import("../services/web-update.js"),
152
+ import("../services/web-update-runtime.js"),
153
+ import("../services/web-autostart.js"),
154
+ import("../services/runtime-record.js"),
155
+ import("../services/runtime-handoff.js"),
156
+ import("../services/web-ensure.js"),
157
+ import("../services/web-retire.js"),
158
+ import("../services/logger.js"),
159
+ ]);
160
+ const dir = ommsDir(options.home);
161
+ const lockPath = startLockPath(options.home);
162
+ return {
163
+ url: options.url,
164
+ headers: { [AUTH_HEADER]: getOrCreateAuthToken() },
165
+ version: packageVersion(),
166
+ fetchFn: fetch,
167
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
168
+ now: () => Date.now(),
169
+ latest: () => latestNpmVersion(fetch),
170
+ // npm installs beside this Node.js, so read that install, not the first command on PATH.
171
+ globalVersion: () => globalCommandVersion({ find: () => null }).version,
172
+ install: (target) => runGlobalInstall(nodeInstallDeps(), target),
173
+ loginItemInstalled: () => webAutostartStatus(options.autostart).state === "installed",
174
+ restartLoginItem: () => restartWebAutostart(options.autostart),
175
+ startDetached: () => {
176
+ // The launcher picks the newest copy, the global install beside this Node.js included.
177
+ const launcher = launcherPath(dir);
178
+ const copy = recordedCopy(dir);
179
+ const script = existsSync(launcher)
180
+ ? launcher
181
+ : copy
182
+ ? join(copy.root, "dist", "cli", "index.js")
183
+ : null;
184
+ if (!script)
185
+ return false;
186
+ try {
187
+ spawn(process.execPath, [script, "web"], {
188
+ detached: true,
189
+ stdio: "ignore",
190
+ cwd: options.home,
191
+ windowsHide: true,
192
+ }).unref();
193
+ return true;
194
+ }
195
+ catch {
196
+ return false;
197
+ }
198
+ },
199
+ takeStartLock: () => takeStartLock({
200
+ lockPath,
201
+ lockFs: nodeLockFs,
202
+ pid: process.pid,
203
+ now: () => Date.now(),
204
+ pidAlive: processAlive,
205
+ }),
206
+ removeStartLock: () => removeStartLockFor(process.pid, lockPath),
207
+ writeRetireMarker: (before) => writeRetireMarker(before, { home: options.home }),
208
+ print: (line) => console.log(line),
209
+ log,
210
+ };
211
+ }
@@ -74,6 +74,7 @@ export declare const nodeLockFs: LockFs;
74
74
  export declare function renameRetrying(from: string, to: string, rename?: (from: string, to: string) => void, platform?: NodeJS.Platform, wait?: (ms: number) => void): void;
75
75
  /** Remove the start lock when it names `pid`. A restart copy calls this once it owns the port. */
76
76
  export declare function removeStartLockFor(pid: number, path?: string, lockFs?: LockFs): void;
77
+ export declare function processAlive(pid: number): boolean;
77
78
  /** What taking the start lock needs; a test drives two callers through it step by step. */
78
79
  export type LockDeps = Pick<EnsureDeps, "lockPath" | "lockFs" | "pid" | "now" | "pidAlive">;
79
80
  /**
@@ -111,7 +111,7 @@ export function removeStartLockFor(pid, path = startLockPath(), lockFs = nodeLoc
111
111
  /* An unreadable lock is stale and the next caller replaces it. */
112
112
  }
113
113
  }
114
- function processAlive(pid) {
114
+ export function processAlive(pid) {
115
115
  try {
116
116
  process.kill(pid, 0);
117
117
  return true;
@@ -0,0 +1,12 @@
1
+ import { type LockFs } from "./web-ensure.js";
2
+ export declare function retireMarkerPath(home?: string): string;
3
+ /** Ask every waiting web app started before `before` (epoch ms) to exit. */
4
+ export declare function writeRetireMarker(before: number, options?: {
5
+ home?: string;
6
+ fs?: Pick<LockFs, "replace">;
7
+ }): void;
8
+ /** The marker's `before` time, or null when the marker is missing or damaged. */
9
+ export declare function readRetireMarker(options?: {
10
+ home?: string;
11
+ fs?: Pick<LockFs, "read">;
12
+ }): number | null;
@@ -0,0 +1,26 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { nodeLockFs } from "./web-ensure.js";
4
+ // The retire marker that `web update` writes. A standalone web app that waits for
5
+ // the port and started before `before` exits. A waiting web app holds no port, so
6
+ // a file is the only way to reach it. The marker stays; the next update overwrites it.
7
+ export function retireMarkerPath(home = homedir()) {
8
+ return join(home, ".omms", "web-retire.json");
9
+ }
10
+ /** Ask every waiting web app started before `before` (epoch ms) to exit. */
11
+ export function writeRetireMarker(before, options = {}) {
12
+ (options.fs ?? nodeLockFs).replace(retireMarkerPath(options.home), `${JSON.stringify({ before })}\n`);
13
+ }
14
+ /** The marker's `before` time, or null when the marker is missing or damaged. */
15
+ export function readRetireMarker(options = {}) {
16
+ const text = (options.fs ?? nodeLockFs).read(retireMarkerPath(options.home));
17
+ if (text === null)
18
+ return null;
19
+ try {
20
+ const { before } = JSON.parse(text);
21
+ return typeof before === "number" && Number.isFinite(before) ? before : null;
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
@@ -26,6 +26,8 @@ interface WebServerConfig {
26
26
  apiToken?: string;
27
27
  /** How long a stepped-aside server waits before it may take the port back. */
28
28
  stepAsideHoldOffMs?: number;
29
+ /** Test seam: the retire marker's time, or null. Defaults to `~/.omms/web-retire.json`. */
30
+ readRetireMarker?: () => number | null;
29
31
  }
30
32
  type PowerAction = "stop" | "restart";
31
33
  export declare class WebServer {
@@ -37,6 +39,8 @@ export declare class WebServer {
37
39
  private onTakeoverCallback;
38
40
  private onPortsExhaustedCallback;
39
41
  private onStepAsideCallback;
42
+ /** `web update` retires waiting web apps that started before its marker. */
43
+ private readonly startedAt;
40
44
  private onPowerActionCallback;
41
45
  private webUpdate;
42
46
  private powerActionRunning;
@@ -83,6 +87,10 @@ export declare class WebServer {
83
87
  start(): Promise<void>;
84
88
  private _start;
85
89
  private startHealthCheckLoop;
90
+ /** One check of a web app that waits for the port. */
91
+ private waiterTick;
92
+ /** True when `web update` asked web apps started before this one to retire. */
93
+ private retiredByUpdate;
86
94
  private ownerIsNewer;
87
95
  private stopHealthCheckLoop;
88
96
  private attemptTakeover;
@@ -164,6 +164,8 @@ export class WebServer {
164
164
  onTakeoverCallback = null;
165
165
  onPortsExhaustedCallback = null;
166
166
  onStepAsideCallback = null;
167
+ /** `web update` retires waiting web apps that started before its marker. */
168
+ startedAt = Date.now();
167
169
  onPowerActionCallback = null;
168
170
  webUpdate = null;
169
171
  powerActionRunning = false;
@@ -490,16 +492,19 @@ export class WebServer {
490
492
  const callerVersion = body && typeof body.version === "string"
491
493
  ? body.version.slice(0, 40)
492
494
  : "";
495
+ // `web update` replaces a web app of any version; other callers must be newer.
496
+ const replace = Boolean(body && body.replace === true);
493
497
  const record = (outcome) => log("Web server step-aside request", {
494
498
  outcome,
495
499
  ownVersion: packageVersion(),
496
500
  callerVersion,
501
+ replace,
497
502
  });
498
503
  if (!isLoopbackAddress(remoteAddress)) {
499
504
  record("refused_auth");
500
505
  return this.jsonResponse({ success: false, error: "Loopback caller required" }, 403);
501
506
  }
502
- if (!callerVersion || !isOlderVersion(packageVersion(), callerVersion)) {
507
+ if (!callerVersion || (!replace && !isOlderVersion(packageVersion(), callerVersion))) {
503
508
  record("refused_not_newer");
504
509
  return this.jsonResponse({ success: false, error: "Caller is not newer" }, 409);
505
510
  }
@@ -572,24 +577,45 @@ export class WebServer {
572
577
  if (this.healthCheckInterval) {
573
578
  return;
574
579
  }
575
- this.healthCheckInterval = setInterval(async () => {
576
- const isAvailable = await this.checkServerAvailable();
577
- if (!isAvailable) {
578
- this.stopHealthCheckLoop();
579
- await this.attemptTakeover();
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
- }
592
- }, 5000);
580
+ this.healthCheckInterval = setInterval(() => void this.waiterTick(), 5000);
581
+ }
582
+ /** One check of a web app that waits for the port. */
583
+ async waiterTick() {
584
+ const isAvailable = await this.checkServerAvailable();
585
+ if (!isAvailable) {
586
+ this.stopHealthCheckLoop();
587
+ await this.attemptTakeover();
588
+ return;
589
+ }
590
+ if (!this.onStepAsideCallback)
591
+ return;
592
+ // A standalone web app behind a newer one would serve old code after a restart.
593
+ const reason = (await this.retiredByUpdate())
594
+ ? "update"
595
+ : (await this.ownerIsNewer())
596
+ ? "newer-owner"
597
+ : null;
598
+ if (!reason)
599
+ return;
600
+ this.stopHealthCheckLoop();
601
+ log("Web server waiter retired", { ownVersion: packageVersion(), reason });
602
+ try {
603
+ await this.onStepAsideCallback();
604
+ }
605
+ catch (error) {
606
+ log("Step-aside callback error", { error: String(error) });
607
+ }
608
+ }
609
+ /** True when `web update` asked web apps started before this one to retire. */
610
+ async retiredByUpdate() {
611
+ try {
612
+ const read = this.config.readRetireMarker ?? (await import("./web-retire.js")).readRetireMarker;
613
+ const before = read();
614
+ return before !== null && this.startedAt < before;
615
+ }
616
+ catch {
617
+ return false;
618
+ }
593
619
  }
594
620
  async ownerIsNewer() {
595
621
  try {
@@ -1,3 +1,5 @@
1
- import { WebUpdate } from "./web-update.js";
1
+ import { WebUpdate, type InstallDeps } from "./web-update.js";
2
2
  /** The npm check and installer of a standalone web app, with the real process, npm, and clock. */
3
3
  export declare function startWebUpdate(restart: () => Promise<string | undefined>, canRestart: () => boolean): WebUpdate;
4
+ /** The real npm process, shared by the update button and `web update`. */
5
+ export declare function nodeInstallDeps(): InstallDeps;
@@ -12,18 +12,27 @@ export function startWebUpdate(restart, canRestart) {
12
12
  fetch,
13
13
  setInterval: (callback, ms) => setInterval(callback, ms),
14
14
  log,
15
+ ...nodeInstallDeps(),
16
+ exists: existsSync,
17
+ // npm installs beside this Node.js, so read that install, not the first command on PATH.
18
+ globalVersion: () => globalCommandVersion({ find: () => null }).version,
19
+ restart,
20
+ canRestart,
21
+ now: () => Date.now(),
22
+ });
23
+ update.start();
24
+ return update;
25
+ }
26
+ /** The real npm process, shared by the update button and `web update`. */
27
+ export function nodeInstallDeps() {
28
+ return {
15
29
  execPath: process.execPath,
16
30
  platform: process.platform,
17
- exists: existsSync,
18
31
  spawn: (command, args, options) => spawn(command, args, {
19
32
  shell: options.shell,
20
33
  stdio: ["ignore", "ignore", "pipe"],
21
34
  windowsHide: true,
22
35
  }),
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
36
  // Resolves once npm has stopped. `taskkill /T` also stops npm under the cmd.exe
28
37
  // wrapper that `shell: true` starts; the child's own exit confirms it.
29
38
  killTree: (child) => new Promise((resolve) => {
@@ -40,8 +49,5 @@ export function startWebUpdate(restart, canRestart) {
40
49
  }),
41
50
  setTimeout: (callback, ms) => setTimeout(callback, ms),
42
51
  clearTimeout: (handle) => clearTimeout(handle),
43
- now: () => Date.now(),
44
- });
45
- update.start();
46
- return update;
52
+ };
47
53
  }
@@ -65,12 +65,23 @@ export declare class WebUpdate {
65
65
  start(): void;
66
66
  /** Read npm `latest`. A failed read keeps the last result. */
67
67
  check(): Promise<void>;
68
- /** The npm that installs into the prefix the launcher reads. */
69
- private npmPath;
70
68
  status(): UpdateStatus;
71
69
  private canInstall;
72
70
  /** Start the install and the restart. A request while one runs joins it. */
73
71
  requestInstall(): InstallRequest;
74
72
  private install;
75
- private runNpm;
76
73
  }
74
+ /** The parts of the update deps that run npm. */
75
+ export type InstallDeps = Pick<WebUpdateDeps, "execPath" | "platform" | "spawn" | "killTree" | "setTimeout" | "clearTimeout">;
76
+ /** A failure code, or null when npm exited with code 0. */
77
+ export interface InstallResult {
78
+ code: string | null;
79
+ exitCode?: number | null;
80
+ }
81
+ /** The npm that installs into the prefix the launcher reads. */
82
+ export declare function npmBesideRuntime(deps: Pick<WebUpdateDeps, "execPath" | "platform">): string;
83
+ /**
84
+ * Run `npm install -g om-memory-system@<target>` and stop it after 5 minutes.
85
+ * The web app's update button and `web update` share it.
86
+ */
87
+ export declare function runGlobalInstall(deps: InstallDeps, target: string): Promise<InstallResult>;