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.
- package/README.md +8 -4
- package/bin/omms-launch.mjs +198 -0
- package/dist/adapters/claude-code/hook-client.d.ts +2 -0
- package/dist/adapters/claude-code/hook-client.js +27 -2
- package/dist/adapters/claude-code/hook-command.d.ts +2 -1
- package/dist/adapters/claude-code/hook-command.js +11 -0
- package/dist/adapters/claude-code/status.d.ts +25 -0
- package/dist/adapters/claude-code/status.js +33 -0
- package/dist/adapters/opencode/tui-status.d.ts +0 -4
- package/dist/adapters/opencode/tui-status.js +1 -23
- package/dist/adapters/pi/extension.js +18 -8
- package/dist/cli/index.js +5 -0
- package/dist/cli/web-command.js +1 -59
- package/dist/index.js +15 -2
- package/dist/services/global-version.d.ts +11 -20
- package/dist/services/global-version.js +74 -44
- package/dist/services/runtime-handoff.d.ts +60 -0
- package/dist/services/runtime-handoff.js +134 -0
- package/dist/services/runtime-record.d.ts +47 -0
- package/dist/services/runtime-record.js +143 -0
- package/dist/services/update-check.d.ts +4 -0
- package/dist/services/update-check.js +23 -0
- package/dist/services/web-autostart.d.ts +9 -7
- package/dist/services/web-autostart.js +33 -54
- package/dist/services/web-ensure.d.ts +11 -0
- package/dist/services/web-ensure.js +37 -5
- package/dist/services/web-handover.d.ts +22 -0
- package/dist/services/web-handover.js +62 -0
- package/dist/services/web-server.js +3 -2
- package/dist/web/assets/{index-45F5ouFN.js → index-D_e39BA6.js} +2 -2
- package/dist/web/index.html +1 -1
- package/package.json +2 -1
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
-
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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. */
|