om-memory-system 4.4.0-next.67 → 4.4.0-next.70

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 (32) hide show
  1. package/README.md +8 -4
  2. package/bin/omms-launch.mjs +198 -0
  3. package/dist/adapters/claude-code/hook-client.d.ts +2 -0
  4. package/dist/adapters/claude-code/hook-client.js +27 -2
  5. package/dist/adapters/claude-code/hook-command.d.ts +2 -1
  6. package/dist/adapters/claude-code/hook-command.js +11 -0
  7. package/dist/adapters/claude-code/status.d.ts +25 -0
  8. package/dist/adapters/claude-code/status.js +33 -0
  9. package/dist/adapters/opencode/tui-status.d.ts +0 -4
  10. package/dist/adapters/opencode/tui-status.js +1 -23
  11. package/dist/adapters/pi/extension.js +18 -8
  12. package/dist/cli/index.js +5 -0
  13. package/dist/cli/web-command.js +1 -59
  14. package/dist/index.js +15 -2
  15. package/dist/services/global-version.d.ts +11 -20
  16. package/dist/services/global-version.js +74 -44
  17. package/dist/services/runtime-handoff.d.ts +60 -0
  18. package/dist/services/runtime-handoff.js +134 -0
  19. package/dist/services/runtime-record.d.ts +47 -0
  20. package/dist/services/runtime-record.js +143 -0
  21. package/dist/services/update-check.d.ts +4 -0
  22. package/dist/services/update-check.js +23 -0
  23. package/dist/services/web-autostart.d.ts +9 -7
  24. package/dist/services/web-autostart.js +33 -54
  25. package/dist/services/web-ensure.d.ts +11 -0
  26. package/dist/services/web-ensure.js +37 -5
  27. package/dist/services/web-handover.d.ts +22 -0
  28. package/dist/services/web-handover.js +62 -0
  29. package/dist/services/web-server.js +3 -2
  30. package/dist/web/assets/{index-45F5ouFN.js → index-D_e39BA6.js} +2 -2
  31. package/dist/web/index.html +1 -1
  32. package/package.json +2 -1
@@ -1,11 +1,6 @@
1
- import { execFile } from "node:child_process";
2
- import { accessSync, constants } from "node:fs";
3
- import { delimiter, isAbsolute, join } from "node:path";
4
- const TIMEOUT_MS = 3_000;
5
- const CACHE_MS = 10 * 60_000;
6
- const run = (file, args, options) => new Promise((resolve, reject) => {
7
- execFile(file, args, { ...options, encoding: "utf8" }, (error, stdout) => error ? reject(error) : resolve(String(stdout)));
8
- });
1
+ import { accessSync, constants, readFileSync, realpathSync } from "node:fs";
2
+ import { delimiter, dirname, isAbsolute, join } from "node:path";
3
+ import { compareVersions } from "./version-compare.js";
9
4
  function executable(path) {
10
5
  try {
11
6
  accessSync(path, constants.X_OK);
@@ -31,48 +26,83 @@ export function findGlobalCommand(env = process.env, platform = process.platform
31
26
  }
32
27
  return null;
33
28
  }
29
+ /** The package folder of the install the command belongs to, or null. */
30
+ function packageFolder(command, platform) {
31
+ // npm's Windows layout: the wrapper sits beside node_modules.
32
+ if (platform === "win32")
33
+ return join(dirname(command), "node_modules", "om-memory-system");
34
+ let dir = dirname(realpathSync(command));
35
+ for (;;) {
36
+ try {
37
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
38
+ if (pkg.name === "om-memory-system")
39
+ return dir;
40
+ }
41
+ catch {
42
+ /* Try the parent. */
43
+ }
44
+ const parent = dirname(dir);
45
+ if (parent === dir)
46
+ return null;
47
+ dir = parent;
48
+ }
49
+ }
34
50
  /**
35
- * The command and arguments that print the version without a user-controlled
36
- * shell. A Windows `.cmd` wrapper can only run through cmd.exe, so it runs
37
- * through the system's own cmd.exe with fixed arguments.
51
+ * Where a global npm install can be for a Node.js binary, the same places the
52
+ * launcher looks. Homebrew runs Node from a versioned `Cellar/node/<version>`
53
+ * folder, but npm installs under the prefix above `Cellar`.
38
54
  */
39
- export function versionInvocation(path, platform = process.platform, env = process.env) {
40
- if (platform === "win32" && path.toLowerCase().endsWith(".cmd")) {
41
- const cmd = env.SystemRoot ? `${env.SystemRoot}\\System32\\cmd.exe` : "cmd.exe";
42
- return { file: cmd, args: ["/d", "/s", "/c", `"${path}" --version`] };
43
- }
44
- return { file: path, args: ["--version"] };
55
+ function installsBesideRuntime(execPath, platform) {
56
+ if (!execPath)
57
+ return [];
58
+ if (platform === "win32")
59
+ return [join(dirname(execPath), "node_modules", "om-memory-system")];
60
+ const direct = join(dirname(dirname(execPath)), "lib", "node_modules", "om-memory-system");
61
+ const brew = /^(.*)[\\/]Cellar[\\/][^\\/]+[\\/][^\\/]+[\\/]bin[\\/][^\\/]+$/.exec(execPath);
62
+ return brew ? [direct, join(brew[1], "lib", "node_modules", "om-memory-system")] : [direct];
45
63
  }
46
- let cached = null;
47
- /** Look up the global command's version, at most once every 10 minutes. */
48
- export async function globalCommandVersion(options = {}) {
49
- const now = (options.now ?? Date.now)();
50
- if (cached && now - cached.at < CACHE_MS)
51
- return cached.value;
64
+ /** The version in an install's `package.json`, or null when it is not an OMMS install. */
65
+ function installVersion(folder) {
66
+ const pkg = JSON.parse(readFileSync(join(folder, "package.json"), "utf8"));
67
+ return pkg.name === "om-memory-system" && typeof pkg.version === "string" ? pkg.version : null;
68
+ }
69
+ /**
70
+ * The global command's version, read from its install's `package.json`. The
71
+ * command does not run: after a hand-off it would print the newest copy's version.
72
+ * The login item runs with a minimal PATH, so with no command on PATH the install
73
+ * beside the running Node.js counts too.
74
+ */
75
+ export function globalCommandVersion(options = {}) {
76
+ const platform = options.platform ?? process.platform;
52
77
  const path = (options.find ?? (() => findGlobalCommand()))();
53
- let value;
54
78
  if (!path) {
55
- value = { version: null, path: null };
56
- }
57
- else {
58
- const { file, args } = versionInvocation(path, options.platform);
59
- try {
60
- const output = await (options.runner ?? run)(file, args, {
61
- timeout: TIMEOUT_MS,
62
- windowsHide: true,
63
- shell: false,
64
- });
65
- const version = /\d+\.\d+\.\d+(?:[-+][\w.]+)?/.exec(output)?.[0] ?? null;
66
- value = version ? { version, path } : { version: null, path, error: "failed" };
67
- }
68
- catch (error) {
69
- const killed = error.killed;
70
- value = { version: null, path, error: killed ? "timeout" : "failed" };
79
+ for (const folder of installsBesideRuntime(options.execPath ?? process.execPath, platform)) {
80
+ try {
81
+ const version = installVersion(folder);
82
+ if (version)
83
+ return { version, path: folder };
84
+ }
85
+ catch {
86
+ /* No install there. */
87
+ }
71
88
  }
89
+ return { version: null, path: null };
90
+ }
91
+ try {
92
+ const folder = packageFolder(path, platform);
93
+ const version = folder ? installVersion(folder) : null;
94
+ return version ? { version, path } : { version: null, path, error: "failed" };
95
+ }
96
+ catch {
97
+ return { version: null, path, error: "failed" };
72
98
  }
73
- cached = { at: now, value };
74
- return value;
75
99
  }
76
- export function resetGlobalVersionCache() {
77
- cached = null;
100
+ /** How the global install compares with the running OMMS. */
101
+ export function globalRelation(running, global) {
102
+ if (global === null)
103
+ return "missing";
104
+ const order = compareVersions(global, running);
105
+ if (order === null)
106
+ return "unknown";
107
+ return order === 0 ? "same" : order < 0 ? "older" : "newer";
78
108
  }
@@ -0,0 +1,60 @@
1
+ import { type RegisterResult, type RuntimeCopy, type RuntimeLog } from "./runtime-record.js";
2
+ export declare const ommsDir: (home?: string) => string;
3
+ /** The package folder of the running copy, found from source or `dist/`. */
4
+ export declare function ownPackageRoot(from?: string): string | null;
5
+ /**
6
+ * Record the running copy when it is the newest. A failure is logged with a
7
+ * code and never thrown, so it cannot stop a session or a command.
8
+ */
9
+ export declare function registerOwnCopy(options?: {
10
+ dir?: string;
11
+ root?: string | null;
12
+ log?: RuntimeLog;
13
+ }): RegisterResult;
14
+ export interface HandoffOptions {
15
+ dir?: string;
16
+ ownRoot?: string | null;
17
+ ownVersion?: string;
18
+ env?: NodeJS.ProcessEnv;
19
+ log?: RuntimeLog;
20
+ /** Injected so tests run no process. Resolves to the exit code, or null when the copy cannot start. */
21
+ spawn?: (command: string, args: string[], env: NodeJS.ProcessEnv) => Promise<number | null>;
22
+ }
23
+ /** The recorded copy this command must hand off to, or null. */
24
+ export declare function handoffTarget(options: HandoffOptions & {
25
+ ownVersion: string;
26
+ }): RuntimeCopy | null;
27
+ /**
28
+ * The first step of the `om-memory-system` command. When the record names a
29
+ * newer valid copy, run that copy's command with the same arguments and return
30
+ * its exit code. Otherwise record this copy and return null, and the command
31
+ * continues with its own code. `OMMS_HANDED_OFF` stops a loop when two records disagree.
32
+ */
33
+ export declare function handOffOrRegister(argv: string[], options?: HandoffOptions): Promise<number | null>;
34
+ /** The version of the newest valid recorded copy, or null. This is the version a launcher start runs. */
35
+ export declare function newestRecordedVersion(options?: {
36
+ dir?: string;
37
+ log?: RuntimeLog;
38
+ }): string | null;
39
+ /** What `ensureWebApp` needs to replace an older running web app. */
40
+ export interface ReplaceOlder {
41
+ version: string;
42
+ headers: Record<string, string>;
43
+ }
44
+ /** The browser password settings a web app request needs, as `CONFIG` holds them. */
45
+ export interface BasicAuthSettings {
46
+ webServerAuthPassword?: string;
47
+ webServerAuthUsername?: string;
48
+ }
49
+ /**
50
+ * Headers for the version lookup and the step-aside request. They carry the local
51
+ * token, and Basic Auth when a browser password is set: the web server checks Basic
52
+ * Auth first and refuses every other API request without it.
53
+ */
54
+ export declare function replaceHeaders(token: string, settings?: BasicAuthSettings): Promise<Record<string, string>>;
55
+ /**
56
+ * For a host start: record this copy, then name the version a running web app
57
+ * must reach. A web app older than it is asked to step aside. Without a
58
+ * recorded copy there is no target, so any running web app is used.
59
+ */
60
+ export declare function hostReplaceOlder(settings?: BasicAuthSettings): Promise<ReplaceOlder | undefined>;
@@ -0,0 +1,134 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { recordedCopy, registerCopy, } from "./runtime-record.js";
7
+ import { compareVersions } from "./version-compare.js";
8
+ // The newest-copy rule for the terminal command and the hosts: an old command
9
+ // hands off to a newer recorded copy, and every copy that runs records itself.
10
+ // Like `runtime-record.ts`, it imports no config.
11
+ export const ommsDir = (home = homedir()) => join(home, ".omms");
12
+ /** The package folder of the running copy, found from source or `dist/`. */
13
+ export function ownPackageRoot(from = import.meta.url) {
14
+ let dir = dirname(fileURLToPath(from));
15
+ for (;;) {
16
+ try {
17
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
18
+ if (pkg.name === "om-memory-system")
19
+ return dir;
20
+ }
21
+ catch {
22
+ /* Try the parent. */
23
+ }
24
+ const parent = dirname(dir);
25
+ if (parent === dir)
26
+ return null;
27
+ dir = parent;
28
+ }
29
+ }
30
+ /** Log a code through the shared logger. A host calls this after its config is loaded. */
31
+ const logCode = (code, data) => {
32
+ void import("./logger.js")
33
+ .then(({ log }) => log("Runtime record", { code, ...data }))
34
+ .catch(() => { });
35
+ };
36
+ /**
37
+ * Record the running copy when it is the newest. A failure is logged with a
38
+ * code and never thrown, so it cannot stop a session or a command.
39
+ */
40
+ export function registerOwnCopy(options = {}) {
41
+ const root = options.root === undefined ? ownPackageRoot() : options.root;
42
+ if (!root)
43
+ return "skipped";
44
+ // Tests set this so that starting the real CLI never writes the developer's real record.
45
+ if (options.dir === undefined && process.env.OMMS_DISABLE_RUNTIME_RECORD === "1")
46
+ return "skipped";
47
+ try {
48
+ return registerCopy({ dir: options.dir ?? ommsDir(), root, log: options.log ?? logCode });
49
+ }
50
+ catch {
51
+ return "failed";
52
+ }
53
+ }
54
+ /**
55
+ * Run the child and resolve to its exit code, or null when it could not start.
56
+ * Standard input, output, and error flow through.
57
+ */
58
+ function spawnCopy(command, args, env) {
59
+ return new Promise((resolve) => {
60
+ const child = nodeSpawn(command, args, { stdio: "inherit", env });
61
+ child.on("error", () => resolve(null));
62
+ child.on("close", (code) => resolve(code ?? 1));
63
+ });
64
+ }
65
+ /** The recorded copy this command must hand off to, or null. */
66
+ export function handoffTarget(options) {
67
+ const env = options.env ?? process.env;
68
+ if (env.OMMS_NO_HANDOFF === "1" || env.OMMS_HANDED_OFF)
69
+ return null;
70
+ const copy = recordedCopy(options.dir ?? ommsDir(), options.log ?? logCode);
71
+ if (!copy || copy.root === options.ownRoot)
72
+ return null;
73
+ return (compareVersions(copy.version, options.ownVersion) ?? 0) > 0 ? copy : null;
74
+ }
75
+ /**
76
+ * The first step of the `om-memory-system` command. When the record names a
77
+ * newer valid copy, run that copy's command with the same arguments and return
78
+ * its exit code. Otherwise record this copy and return null, and the command
79
+ * continues with its own code. `OMMS_HANDED_OFF` stops a loop when two records disagree.
80
+ */
81
+ export async function handOffOrRegister(argv, options = {}) {
82
+ const ownRoot = options.ownRoot === undefined ? ownPackageRoot() : options.ownRoot;
83
+ let ownVersion = options.ownVersion;
84
+ if (ownVersion === undefined) {
85
+ const { packageVersion } = await import("./package-version.js");
86
+ ownVersion = packageVersion();
87
+ }
88
+ const target = handoffTarget({ ...options, ownRoot, ownVersion });
89
+ if (target) {
90
+ const run = options.spawn ?? spawnCopy;
91
+ const code = await run(process.execPath, [join(target.root, "dist", "cli", "index.js"), ...argv], { ...(options.env ?? process.env), OMMS_HANDED_OFF: "1" });
92
+ if (code !== null)
93
+ return code;
94
+ }
95
+ registerOwnCopy({ dir: options.dir, root: ownRoot, log: options.log });
96
+ return null;
97
+ }
98
+ /** The version of the newest valid recorded copy, or null. This is the version a launcher start runs. */
99
+ export function newestRecordedVersion(options = {}) {
100
+ return recordedCopy(options.dir ?? ommsDir(), options.log ?? logCode)?.version ?? null;
101
+ }
102
+ /**
103
+ * Headers for the version lookup and the step-aside request. They carry the local
104
+ * token, and Basic Auth when a browser password is set: the web server checks Basic
105
+ * Auth first and refuses every other API request without it.
106
+ */
107
+ export async function replaceHeaders(token, settings = {}) {
108
+ const { AUTH_HEADER } = await import("./auth-token.js");
109
+ const headers = { [AUTH_HEADER]: token };
110
+ const password = (settings.webServerAuthPassword ?? "").trim();
111
+ if (!password)
112
+ return headers;
113
+ const { WebAuth } = await import("./web-auth.js");
114
+ const { username } = new WebAuth({
115
+ password,
116
+ username: settings.webServerAuthUsername,
117
+ }).getConfig();
118
+ headers.authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
119
+ return headers;
120
+ }
121
+ /**
122
+ * For a host start: record this copy, then name the version a running web app
123
+ * must reach. A web app older than it is asked to step aside. Without a
124
+ * recorded copy there is no target, so any running web app is used.
125
+ */
126
+ export async function hostReplaceOlder(settings = {}) {
127
+ registerOwnCopy();
128
+ const version = newestRecordedVersion();
129
+ if (!version)
130
+ return undefined;
131
+ const { getOrCreateAuthToken } = await import("./auth-token.js");
132
+ // The web app runs on this machine, so the local token file is enough.
133
+ return { version, headers: await replaceHeaders(getOrCreateAuthToken(), settings) };
134
+ }
@@ -0,0 +1,47 @@
1
+ export interface RuntimeRecord {
2
+ root: string;
3
+ version: string;
4
+ updatedAt: string;
5
+ }
6
+ /** A valid copy: its package folder and the version in its `package.json`. */
7
+ export interface RuntimeCopy {
8
+ root: string;
9
+ version: string;
10
+ }
11
+ /** A log call carries a code and identifiers only, never a path or file text. */
12
+ export type RuntimeLog = (code: string, data: Record<string, unknown>) => void;
13
+ export type RegisterResult = "written" | "kept" | "skipped" | "failed";
14
+ export declare const recordPath: (dir: string) => string;
15
+ export declare const launcherPath: (dir: string) => string;
16
+ /**
17
+ * The version of a valid OMMS copy, or null. A copy is valid when its
18
+ * `package.json` names `om-memory-system`, its version can be compared, and it
19
+ * has `dist/cli/index.js`.
20
+ */
21
+ export declare function copyVersion(root: string): string | null;
22
+ /** The record as written, or null. A missing record is normal; any other failure logs a code. */
23
+ export declare function readRuntimeRecord(dir: string, log?: RuntimeLog): RuntimeRecord | null;
24
+ /** The copy the record names, when it is still valid. The version is read from its `package.json`. */
25
+ export declare function recordedCopy(dir: string, log?: RuntimeLog): RuntimeCopy | null;
26
+ /**
27
+ * Make sure a launcher exists at `~/.omms/bin`. An existing launcher stays, so an
28
+ * older copy never replaces it. When it is missing, the first source with a
29
+ * `bin/omms-launch.mjs` provides it. Returns whether a launcher exists afterwards.
30
+ */
31
+ export declare function ensureLauncher(options: {
32
+ dir: string;
33
+ sources: readonly (string | null | undefined)[];
34
+ log?: RuntimeLog;
35
+ }): boolean;
36
+ /**
37
+ * Record this copy when the record is missing, names a copy that is no longer
38
+ * valid, or names an older version. An equal version keeps the record. The copy
39
+ * that holds the record also keeps the launcher at `~/.omms/bin`. A failure is
40
+ * logged with a code and never thrown.
41
+ */
42
+ export declare function registerCopy(options: {
43
+ dir: string;
44
+ root: string;
45
+ log?: RuntimeLog;
46
+ now?: () => Date;
47
+ }): RegisterResult;
@@ -0,0 +1,143 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { compareVersions } from "./version-compare.js";
4
+ export const recordPath = (dir) => join(dir, "runtime.json");
5
+ export const launcherPath = (dir) => join(dir, "bin", "omms-launch.mjs");
6
+ /**
7
+ * The version of a valid OMMS copy, or null. A copy is valid when its
8
+ * `package.json` names `om-memory-system`, its version can be compared, and it
9
+ * has `dist/cli/index.js`.
10
+ */
11
+ export function copyVersion(root) {
12
+ try {
13
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
14
+ if (pkg.name !== "om-memory-system" || typeof pkg.version !== "string")
15
+ return null;
16
+ if (compareVersions(pkg.version, pkg.version) === null)
17
+ return null;
18
+ return existsSync(join(root, "dist", "cli", "index.js")) ? pkg.version : null;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ /** The record as written, or null. A missing record is normal; any other failure logs a code. */
25
+ export function readRuntimeRecord(dir, log) {
26
+ let text;
27
+ try {
28
+ text = readFileSync(recordPath(dir), "utf8");
29
+ }
30
+ catch (error) {
31
+ if (error.code !== "ENOENT")
32
+ log?.("runtime-record-read-failed", { reason: "read" });
33
+ return null;
34
+ }
35
+ try {
36
+ const value = JSON.parse(text);
37
+ if (!value ||
38
+ typeof value.root !== "string" ||
39
+ typeof value.version !== "string" ||
40
+ typeof value.updatedAt !== "string")
41
+ throw new Error("shape");
42
+ return { root: value.root, version: value.version, updatedAt: value.updatedAt };
43
+ }
44
+ catch {
45
+ log?.("runtime-record-read-failed", { reason: "parse" });
46
+ return null;
47
+ }
48
+ }
49
+ /** The copy the record names, when it is still valid. The version is read from its `package.json`. */
50
+ export function recordedCopy(dir, log) {
51
+ const record = readRuntimeRecord(dir, log);
52
+ if (!record)
53
+ return null;
54
+ const version = copyVersion(record.root);
55
+ return version ? { root: record.root, version } : null;
56
+ }
57
+ /** Write a file in one step, so a reader never finds a partial file. */
58
+ function writeAtomic(path, write) {
59
+ const temp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
60
+ try {
61
+ write(temp);
62
+ renameSync(temp, path);
63
+ }
64
+ finally {
65
+ rmSync(temp, { force: true });
66
+ }
67
+ }
68
+ function sameFile(a, b) {
69
+ try {
70
+ return readFileSync(a).equals(readFileSync(b));
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ }
76
+ /** Put the copy's launcher at the fixed path when it is missing or different. */
77
+ function placeLauncher(dir, root, log) {
78
+ const source = join(root, "bin", "omms-launch.mjs");
79
+ if (!existsSync(source))
80
+ return;
81
+ const target = launcherPath(dir);
82
+ if (sameFile(source, target))
83
+ return;
84
+ try {
85
+ mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
86
+ writeAtomic(target, (temp) => copyFileSync(source, temp));
87
+ }
88
+ catch {
89
+ log?.("runtime-launcher-write-failed", {});
90
+ }
91
+ }
92
+ /**
93
+ * Make sure a launcher exists at `~/.omms/bin`. An existing launcher stays, so an
94
+ * older copy never replaces it. When it is missing, the first source with a
95
+ * `bin/omms-launch.mjs` provides it. Returns whether a launcher exists afterwards.
96
+ */
97
+ export function ensureLauncher(options) {
98
+ const target = launcherPath(options.dir);
99
+ if (existsSync(target))
100
+ return true;
101
+ for (const root of options.sources) {
102
+ if (root)
103
+ placeLauncher(options.dir, root, options.log);
104
+ if (existsSync(target))
105
+ return true;
106
+ }
107
+ return false;
108
+ }
109
+ /**
110
+ * Record this copy when the record is missing, names a copy that is no longer
111
+ * valid, or names an older version. An equal version keeps the record. The copy
112
+ * that holds the record also keeps the launcher at `~/.omms/bin`. A failure is
113
+ * logged with a code and never thrown.
114
+ */
115
+ export function registerCopy(options) {
116
+ const { dir, root, log } = options;
117
+ const own = copyVersion(root);
118
+ if (!own)
119
+ return "skipped";
120
+ try {
121
+ const current = recordedCopy(dir, log);
122
+ const newer = !current || (compareVersions(own, current.version) ?? 0) > 0;
123
+ if (!newer) {
124
+ // The recorded copy owns the launcher, so it repairs a missing or damaged one.
125
+ if (current?.root === root)
126
+ placeLauncher(dir, root, log);
127
+ return "kept";
128
+ }
129
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
130
+ const record = {
131
+ root,
132
+ version: own,
133
+ updatedAt: (options.now?.() ?? new Date()).toISOString(),
134
+ };
135
+ writeAtomic(recordPath(dir), (temp) => writeFileSync(temp, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 }));
136
+ placeLauncher(dir, root, log);
137
+ return "written";
138
+ }
139
+ catch {
140
+ log?.("runtime-record-write-failed", {});
141
+ return "failed";
142
+ }
143
+ }
@@ -0,0 +1,4 @@
1
+ /** The `latest` version on npm, or null when the registry cannot be reached. */
2
+ export declare function latestNpmVersion(fetchFn: typeof fetch, timeoutMs?: number): Promise<string | null>;
3
+ /** A newer release than `current`, ignoring prereleases; null otherwise. */
4
+ export declare function availableUpdate(current: string, latest: string | null): string | null;
@@ -0,0 +1,23 @@
1
+ import { isOlderVersion } from "./version-compare.js";
2
+ /** The `latest` version on npm, or null when the registry cannot be reached. */
3
+ export async function latestNpmVersion(fetchFn, timeoutMs = 5_000) {
4
+ try {
5
+ const response = await fetchFn("https://registry.npmjs.org/om-memory-system/latest", {
6
+ signal: AbortSignal.timeout(timeoutMs),
7
+ headers: { accept: "application/json" },
8
+ });
9
+ if (!response.ok)
10
+ return null;
11
+ const body = (await response.json());
12
+ return typeof body.version === "string" ? body.version : null;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ /** A newer release than `current`, ignoring prereleases; null otherwise. */
19
+ export function availableUpdate(current, latest) {
20
+ if (!latest || latest.includes("-"))
21
+ return null;
22
+ return isOlderVersion(current, latest) ? latest : null;
23
+ }
@@ -1,10 +1,13 @@
1
1
  import { CONFIG } from "../config.js";
2
- type State = "installed" | "not-installed" | "unsupported" | "no-runtime" | "no-package";
2
+ type State = "installed" | "not-installed" | "unsupported" | "no-runtime" | "no-package" | "no-launcher";
3
3
  export type WebAutostartStatus = {
4
4
  state: State;
5
5
  path?: string;
6
6
  runtime?: string;
7
+ /** The copy the launcher starts now: the newest of the record, the global install, and this copy. */
7
8
  packagePath?: string;
9
+ /** The fixed launcher the item runs, `~/.omms/bin/omms-launch.mjs`. */
10
+ launcher?: string;
8
11
  command?: string;
9
12
  };
10
13
  export interface WebAutostartOptions {
@@ -12,19 +15,18 @@ export interface WebAutostartOptions {
12
15
  platform?: string;
13
16
  runtime?: string | null;
14
17
  packageRoot?: string | null;
18
+ /** The running copy, a launcher source when the chosen copy has none. Defaults to `packageRoot`, then this copy. */
19
+ ownRoot?: string | null;
15
20
  systemctlAvailable?: boolean;
16
21
  run?: (command: string, args: string[]) => void;
17
22
  start?: boolean;
18
23
  }
19
24
  /** OpenCode's executable is not a JavaScript runtime. */
20
25
  export declare function resolveWebRuntime(execPath?: string, platform?: NodeJS.Platform): string | null;
21
- /** The package folder a written login item runs, read back from its command line. */
22
- export declare function itemPackageRoot(file: string): string | null;
23
26
  /**
24
- * The newest OMMS copy among this one, the global install beside the runtime,
25
- * and the copy the item already runs. A host starts OMMS from its own cache,
26
- * which can be older than the global install; that copy must not downgrade
27
- * the login item.
27
+ * The newest valid OMMS copy among the candidates. On equal versions the first
28
+ * wins, so a host's cached copy goes last. This is the copy the launcher starts,
29
+ * and it matches the launcher's own choice.
28
30
  */
29
31
  export declare function preferredPackageRoot(candidates: readonly (string | null | undefined)[]): string | null;
30
32
  /** Read the login item's state without changing any system files. */