om-memory-system 4.4.0-next.67 → 4.4.0-next.69
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/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/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
|
@@ -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
|
+
}
|
|
@@ -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. */
|
|
@@ -6,6 +6,7 @@ import { basename, dirname, join, delimiter, resolve } from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { CONFIG } from "../config.js";
|
|
8
8
|
import { log } from "./logger.js";
|
|
9
|
+
import { copyVersion, ensureLauncher, launcherPath, recordedCopy, } from "./runtime-record.js";
|
|
9
10
|
import { compareVersions } from "./version-compare.js";
|
|
10
11
|
const MARKER = "OMMS login item";
|
|
11
12
|
const NAME = "io.github.cmdaltctr.omms.web";
|
|
@@ -70,53 +71,18 @@ function packageRoot() {
|
|
|
70
71
|
dir = parent;
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
|
-
function packageVersionAt(root) {
|
|
74
|
-
try {
|
|
75
|
-
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
76
|
-
if (pkg.name !== "om-memory-system" || !existsSync(join(root, "dist", "cli", "index.js"))) {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
return typeof pkg.version === "string" ? pkg.version : null;
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
/** The package folder a written login item runs, read back from its command line. */
|
|
86
|
-
export function itemPackageRoot(file) {
|
|
87
|
-
try {
|
|
88
|
-
const content = readFileSync(file, "utf8");
|
|
89
|
-
const match = /([^"<>\r\n]*?)[\\/]dist[\\/]cli[\\/]index\.js/.exec(content);
|
|
90
|
-
const path = match?.[1]?.replace(/^.*?<string>/, "").trim();
|
|
91
|
-
if (!path)
|
|
92
|
-
return null;
|
|
93
|
-
// Only plists escape XML. Decode ampersands last to preserve literal entity text.
|
|
94
|
-
return content.includes("<plist")
|
|
95
|
-
? path
|
|
96
|
-
.replaceAll(""", '"')
|
|
97
|
-
.replaceAll("'", "'")
|
|
98
|
-
.replaceAll("<", "<")
|
|
99
|
-
.replaceAll(">", ">")
|
|
100
|
-
.replaceAll("&", "&")
|
|
101
|
-
: path;
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
74
|
/**
|
|
108
|
-
* The newest OMMS copy among
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* the login item.
|
|
75
|
+
* The newest valid OMMS copy among the candidates. On equal versions the first
|
|
76
|
+
* wins, so a host's cached copy goes last. This is the copy the launcher starts,
|
|
77
|
+
* and it matches the launcher's own choice.
|
|
112
78
|
*/
|
|
113
79
|
export function preferredPackageRoot(candidates) {
|
|
114
80
|
let best = null;
|
|
115
81
|
for (const root of candidates) {
|
|
116
82
|
if (!root)
|
|
117
83
|
continue;
|
|
118
|
-
const version =
|
|
119
|
-
if (!version
|
|
84
|
+
const version = copyVersion(root);
|
|
85
|
+
if (!version)
|
|
120
86
|
continue;
|
|
121
87
|
if (!best || (compareVersions(version, best.version) ?? 0) > 0)
|
|
122
88
|
best = { root, version };
|
|
@@ -145,15 +111,20 @@ function details(options) {
|
|
|
145
111
|
const platform = options.platform ?? process.platform;
|
|
146
112
|
const path = itemPath(home, platform);
|
|
147
113
|
const runtime = options.runtime === undefined ? resolveWebRuntime() : options.runtime;
|
|
148
|
-
const
|
|
149
|
-
const
|
|
114
|
+
const dir = join(home, ".omms");
|
|
115
|
+
const own = options.ownRoot !== undefined
|
|
116
|
+
? options.ownRoot
|
|
117
|
+
: options.packageRoot !== undefined
|
|
118
|
+
? options.packageRoot
|
|
119
|
+
: packageRoot();
|
|
120
|
+
const recorded = recordedCopy(dir, logCode)?.root ?? null;
|
|
150
121
|
const root = options.packageRoot === undefined
|
|
151
|
-
?
|
|
152
|
-
|
|
153
|
-
: own;
|
|
122
|
+
? preferredPackageRoot([recorded, globalPackageRoot(runtime ?? null, platform), own])
|
|
123
|
+
: options.packageRoot;
|
|
154
124
|
const supported = platform !== "linux" || (options.systemctlAvailable ?? Boolean(executable("systemctl")));
|
|
155
|
-
return { home, platform, path, runtime, root, supported };
|
|
125
|
+
return { home, platform, path, runtime, root, own, recorded, dir, supported };
|
|
156
126
|
}
|
|
127
|
+
const logCode = (code, data) => log("Runtime record", { code, ...data });
|
|
157
128
|
const xml = (value) => value
|
|
158
129
|
.replaceAll("&", "&")
|
|
159
130
|
.replaceAll("<", "<")
|
|
@@ -161,21 +132,21 @@ const xml = (value) => value
|
|
|
161
132
|
.replaceAll('"', """);
|
|
162
133
|
const systemd = (value) => `"${value.replaceAll("%", "%%").replaceAll('"', '\\"')}"`;
|
|
163
134
|
const windows = (value) => `"${value.replaceAll("%", "%%")}"`;
|
|
164
|
-
function itemContent(platform, runtime,
|
|
135
|
+
function itemContent(platform, runtime, launcher) {
|
|
165
136
|
if (platform === "darwin")
|
|
166
137
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
167
138
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
168
139
|
<plist version="1.0"><dict>
|
|
169
140
|
<!-- ${MARKER} -->
|
|
170
141
|
<key>Label</key><string>${NAME}</string>
|
|
171
|
-
<key>ProgramArguments</key><array><string>${xml(runtime)}</string><string>${xml(
|
|
142
|
+
<key>ProgramArguments</key><array><string>${xml(runtime)}</string><string>${xml(launcher)}</string><string>web</string><string>--login-item</string></array>
|
|
172
143
|
<key>RunAtLoad</key><true/><key>KeepAlive</key><false/>
|
|
173
144
|
<key>StandardOutPath</key><string>/dev/null</string><key>StandardErrorPath</key><string>/dev/null</string>
|
|
174
145
|
</dict></plist>\n`;
|
|
175
146
|
if (platform === "linux")
|
|
176
147
|
return `# ${MARKER}
|
|
177
|
-
[Unit]\nDescription=OMMS web app\n[Service]\nType=simple\nExecStart=${systemd(runtime)} ${systemd(
|
|
178
|
-
return `@echo off\r\nrem ${MARKER}\r\nstart "" /min ${windows(runtime)} ${windows(
|
|
148
|
+
[Unit]\nDescription=OMMS web app\n[Service]\nType=simple\nExecStart=${systemd(runtime)} ${systemd(launcher)} web --login-item\n[Install]\nWantedBy=default.target\n`;
|
|
149
|
+
return `@echo off\r\nrem ${MARKER}\r\nstart "" /min ${windows(runtime)} ${windows(launcher)} web --login-item\r\n`;
|
|
179
150
|
}
|
|
180
151
|
function owned(path) {
|
|
181
152
|
return readFileSync(path, "utf8").includes(MARKER);
|
|
@@ -188,15 +159,16 @@ function runCommand(options, home, command, args) {
|
|
|
188
159
|
}
|
|
189
160
|
/** Read the login item's state without changing any system files. */
|
|
190
161
|
export function webAutostartStatus(options = {}) {
|
|
191
|
-
const { path, runtime, root, supported } = details(options);
|
|
162
|
+
const { path, runtime, root, dir, supported } = details(options);
|
|
192
163
|
if (!path || !supported)
|
|
193
164
|
return { state: "unsupported" };
|
|
194
165
|
if (!runtime)
|
|
195
166
|
return { state: "no-runtime", path };
|
|
196
167
|
if (!root)
|
|
197
168
|
return { state: "no-package", path, runtime };
|
|
169
|
+
const launcher = launcherPath(dir);
|
|
198
170
|
if (!existsSync(path))
|
|
199
|
-
return { state: "not-installed", path, runtime, packagePath: root };
|
|
171
|
+
return { state: "not-installed", path, runtime, packagePath: root, launcher };
|
|
200
172
|
if (!owned(path))
|
|
201
173
|
throw new Error("Login item at the OMMS path is not owned by OMMS");
|
|
202
174
|
return {
|
|
@@ -204,6 +176,7 @@ export function webAutostartStatus(options = {}) {
|
|
|
204
176
|
path,
|
|
205
177
|
runtime,
|
|
206
178
|
packagePath: root,
|
|
179
|
+
launcher,
|
|
207
180
|
command: readFileSync(path, "utf8"),
|
|
208
181
|
};
|
|
209
182
|
}
|
|
@@ -214,9 +187,15 @@ export function installWebAutostart(options = {}) {
|
|
|
214
187
|
status.state === "no-runtime" ||
|
|
215
188
|
status.state === "no-package")
|
|
216
189
|
return status;
|
|
217
|
-
const { home, platform, path, runtime, root } = details(options);
|
|
190
|
+
const { home, platform, path, runtime, root, own, recorded, dir } = details(options);
|
|
191
|
+
// The item runs a fixed launcher, so the launcher must exist before the item does.
|
|
192
|
+
if (!ensureLauncher({ dir, sources: [root, own, recorded], log: logCode })) {
|
|
193
|
+
// A valid package exists. Only the launcher is missing, so say that.
|
|
194
|
+
logCode("launcher-missing", {});
|
|
195
|
+
return { state: "no-launcher", path: path, runtime: runtime };
|
|
196
|
+
}
|
|
218
197
|
const file = path;
|
|
219
|
-
const content = itemContent(platform, runtime,
|
|
198
|
+
const content = itemContent(platform, runtime, launcherPath(dir));
|
|
220
199
|
mkdirSync(dirname(file), { recursive: true });
|
|
221
200
|
const changed = !existsSync(file) || readFileSync(file, "utf8") !== content;
|
|
222
201
|
if (changed) {
|
|
@@ -37,6 +37,8 @@ export interface EnsureDeps {
|
|
|
37
37
|
resolveRuntime: () => Promise<string | null> | string | null;
|
|
38
38
|
/** The `om-memory-system` script that `web` is run from. */
|
|
39
39
|
cliScript: string;
|
|
40
|
+
/** `~/.omms/bin/omms-launch.mjs` when it exists. `web` then starts through it, on the newest recorded copy. */
|
|
41
|
+
launcherScript: string | null;
|
|
40
42
|
lockPath: string;
|
|
41
43
|
lockFs: LockFs;
|
|
42
44
|
pidAlive: (pid: number) => boolean;
|
|
@@ -51,6 +53,15 @@ export interface EnsureOptions {
|
|
|
51
53
|
budgetMs: number;
|
|
52
54
|
/** `false` starts the web app and returns at once (session starts). Default `true`. */
|
|
53
55
|
wait?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Replace a running web app older than `version`: ask it to step aside, then
|
|
58
|
+
* start as for an empty port. `headers` are the local token headers the
|
|
59
|
+
* step-aside route needs. Without it, any running OMMS web app is used.
|
|
60
|
+
*/
|
|
61
|
+
replaceOlder?: {
|
|
62
|
+
version: string;
|
|
63
|
+
headers: Record<string, string>;
|
|
64
|
+
};
|
|
54
65
|
deps?: Partial<EnsureDeps>;
|
|
55
66
|
}
|
|
56
67
|
/** A lock older than this is stale. Also the longest a background start holds it. */
|