om-memory-system 5.7.0-next.218 → 5.9.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/adapters/opencode/self-update.d.ts +19 -0
- package/dist/adapters/opencode/self-update.js +42 -0
- package/dist/adapters/opencode/tui-status.d.ts +12 -2
- package/dist/adapters/opencode/tui-status.js +84 -11
- package/dist/adapters/pi/extension.js +19 -3
- package/dist/adapters/pi/self-update.d.ts +25 -0
- package/dist/adapters/pi/self-update.js +146 -0
- package/dist/cli/space-command.js +21 -1
- package/dist/cli/web-command.js +1 -1
- package/dist/config.js +11 -1
- package/dist/services/host-copies.d.ts +52 -0
- package/dist/services/host-copies.js +150 -0
- package/dist/services/host-self-update.d.ts +108 -0
- package/dist/services/host-self-update.js +254 -0
- package/dist/services/space-routes.d.ts +10 -1
- package/dist/services/space-routes.js +63 -20
- package/dist/services/spaces/backup.js +3 -0
- package/dist/services/spaces/operations.js +3 -1
- package/dist/services/spaces/registry-store.d.ts +5 -3
- package/dist/services/spaces/registry-store.js +38 -9
- package/dist/services/spaces/registry.d.ts +11 -5
- package/dist/services/spaces/registry.js +31 -23
- package/dist/services/spaces/restore.js +1 -1
- package/dist/services/spaces/space-label.d.ts +16 -0
- package/dist/services/spaces/space-label.js +69 -0
- package/dist/services/web-route-modules.d.ts +3 -1
- package/dist/services/web-route-modules.js +3 -1
- package/dist/services/web-server.d.ts +1 -1
- package/dist/services/web-server.js +2 -1
- package/dist/services/web-update-runtime.d.ts +1 -1
- package/dist/services/web-update-runtime.js +2 -1
- package/dist/services/web-update.d.ts +8 -0
- package/dist/services/web-update.js +27 -0
- package/dist/v2/plugin.js +60 -1
- package/dist/web/assets/index-Bhk1P5Z7.js +170 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-C6WnDE6h.js +0 -170
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { isOlderVersion } from "./version-compare.js";
|
|
5
|
+
// OpenCode 2.2 installs each update of a plugin to a new folder:
|
|
6
|
+
// `<cache>/om-memory-system@latest/<ms timestamp>/node_modules/om-memory-system/`.
|
|
7
|
+
// It never removes the old ones. This module lists the copies next to the
|
|
8
|
+
// running one, so the update check can see whether the new version reached the
|
|
9
|
+
// disk and the OpenCode service can remove copies that no process needs.
|
|
10
|
+
// It takes paths as arguments and imports no config, so tests need no real home.
|
|
11
|
+
/** The clean-up leaves everything alone until the running copy is this old. */
|
|
12
|
+
export const CACHE_PRUNE_MIN_AGE_MS = 24 * 60 * 60 * 1000;
|
|
13
|
+
const PACKAGE_FOLDER = "/node_modules/om-memory-system";
|
|
14
|
+
/**
|
|
15
|
+
* OpenCode names the cache folder after the package and the version spec, such as
|
|
16
|
+
* `om-memory-system@latest`. The `@` is required: a plain `om-memory-system` folder is
|
|
17
|
+
* a checkout or project, and its numeric folders are not OpenCode installs.
|
|
18
|
+
*/
|
|
19
|
+
const CACHE_FOLDER = /^om-memory-system@.+$/;
|
|
20
|
+
function versionAt(root) {
|
|
21
|
+
try {
|
|
22
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
23
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** The same path with one separator and no trailing one, for comparing roots. */
|
|
30
|
+
const samePath = (path) => path.replaceAll("\\", "/").replace(/\/+$/, "");
|
|
31
|
+
/**
|
|
32
|
+
* A key that is equal for two paths to the same folder. The device and inode
|
|
33
|
+
* match through a symbolic link or a different letter case; a path that cannot
|
|
34
|
+
* be read falls back to its text.
|
|
35
|
+
*/
|
|
36
|
+
function folderKey(path) {
|
|
37
|
+
try {
|
|
38
|
+
const { dev, ino } = statSync(path);
|
|
39
|
+
if (ino !== 0)
|
|
40
|
+
return `inode:${dev}:${ino}`;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Compare the text below.
|
|
44
|
+
}
|
|
45
|
+
return `path:${samePath(path)}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The running copy and, when it sits in a timestamp folder, the other copies in
|
|
49
|
+
* the same cache folder. Null when `modulePath` is not inside
|
|
50
|
+
* `node_modules/om-memory-system/`. Never throws.
|
|
51
|
+
*/
|
|
52
|
+
export function listHostCopies(modulePath) {
|
|
53
|
+
const normal = modulePath.replaceAll("\\", "/");
|
|
54
|
+
const at = normal.lastIndexOf(`${PACKAGE_FOLDER}/`);
|
|
55
|
+
if (at < 0)
|
|
56
|
+
return null;
|
|
57
|
+
// Replacing a backslash with a slash keeps the length, so the index fits the original path.
|
|
58
|
+
const root = modulePath.slice(0, at + PACKAGE_FOLDER.length);
|
|
59
|
+
const stampFolder = modulePath.slice(0, at);
|
|
60
|
+
const parts = stampFolder.replaceAll("\\", "/").split("/");
|
|
61
|
+
const name = parts[parts.length - 1] ?? "";
|
|
62
|
+
const result = { root, version: versionAt(root), stamp: null, siblings: [] };
|
|
63
|
+
if (!/^\d+$/.test(name))
|
|
64
|
+
return result;
|
|
65
|
+
const cache = stampFolder.slice(0, stampFolder.length - name.length);
|
|
66
|
+
// Only OpenCode's `om-memory-system@<spec>` folder holds timestamp copies. A project
|
|
67
|
+
// folder with a number for a name is not one, and the clean-up must never delete its siblings.
|
|
68
|
+
const cacheName = cache.replaceAll("\\", "/").split("/").filter(Boolean).pop() ?? "";
|
|
69
|
+
if (!CACHE_FOLDER.test(cacheName))
|
|
70
|
+
return result;
|
|
71
|
+
result.stamp = Number(name);
|
|
72
|
+
try {
|
|
73
|
+
const names = readdirSync(cache, { withFileTypes: true })
|
|
74
|
+
.filter((entry) => entry.isDirectory() && /^\d+$/.test(entry.name) && entry.name !== name)
|
|
75
|
+
.map((entry) => entry.name)
|
|
76
|
+
.sort((a, b) => Number(a) - Number(b));
|
|
77
|
+
for (const sibling of names) {
|
|
78
|
+
const siblingRoot = join(cache, sibling, "node_modules", "om-memory-system");
|
|
79
|
+
result.siblings.push({
|
|
80
|
+
root: siblingRoot,
|
|
81
|
+
version: versionAt(siblingRoot),
|
|
82
|
+
stamp: Number(sibling),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// An unreadable cache folder lists no siblings.
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The versions on disk: the running copy's and each sibling's. Pi updates in
|
|
93
|
+
* place, so its running copy shows the new version after `pi update`. OpenCode
|
|
94
|
+
* shows it in a new sibling folder. Never throws.
|
|
95
|
+
*/
|
|
96
|
+
export function installedVersions(modulePath) {
|
|
97
|
+
const copies = listHostCopies(modulePath);
|
|
98
|
+
if (!copies)
|
|
99
|
+
return [];
|
|
100
|
+
const versions = [copies.version, ...copies.siblings.map((copy) => copy.version)];
|
|
101
|
+
return versions.filter((version) => version !== null);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Remove old OMMS copies from the running copy's cache folder. A copy goes only
|
|
105
|
+
* when its timestamp and its version are both older than the running copy's,
|
|
106
|
+
* it is not in `keepRoots`, and the running copy is at least
|
|
107
|
+
* `CACHE_PRUNE_MIN_AGE_MS` old. A copy that cannot be removed stays. Logs one
|
|
108
|
+
* `OpenCode cache clean-up` record with counts for each pass in a timestamp
|
|
109
|
+
* folder, also when the counts are zero. Never throws.
|
|
110
|
+
*/
|
|
111
|
+
export async function pruneOldCacheCopies(options) {
|
|
112
|
+
const counts = { removed: 0, kept: 0 };
|
|
113
|
+
try {
|
|
114
|
+
const copies = listHostCopies(options.modulePath);
|
|
115
|
+
if (!copies || copies.stamp === null)
|
|
116
|
+
return counts;
|
|
117
|
+
const older = copies.siblings.filter((copy) => copy.stamp < copies.stamp);
|
|
118
|
+
const keep = new Set(options.keepRoots.map(folderKey));
|
|
119
|
+
const old = options.now - copies.stamp >= CACHE_PRUNE_MIN_AGE_MS;
|
|
120
|
+
const remove = options.remove ?? ((folder) => rm(folder, { recursive: true, force: true }));
|
|
121
|
+
for (const copy of older) {
|
|
122
|
+
const removable = old &&
|
|
123
|
+
copy.version !== null &&
|
|
124
|
+
isOlderVersion(copy.version, options.runningVersion) &&
|
|
125
|
+
!keep.has(folderKey(copy.root));
|
|
126
|
+
if (removable) {
|
|
127
|
+
try {
|
|
128
|
+
// The timestamp folder holds the whole install, not only the package folder.
|
|
129
|
+
await remove(join(copy.root, "..", ".."));
|
|
130
|
+
counts.removed++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// A locked or busy folder stays.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
counts.kept++;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
void Promise.resolve(options.log("OpenCode cache clean-up", { ...counts })).catch(() => { });
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// A failed log write must not change the result.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// The clean-up is housekeeping. It never breaks the service.
|
|
148
|
+
}
|
|
149
|
+
return counts;
|
|
150
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** A second session does not start another update for the same release within this time. */
|
|
2
|
+
export declare const HOST_SELF_UPDATE_RETRY_MS: number;
|
|
3
|
+
/** How often a running plugin reads the marker while an update is pending. */
|
|
4
|
+
export declare const HOST_UPDATE_POLL_MS: number;
|
|
5
|
+
/** How long a running plugin reads the marker before it gives up. */
|
|
6
|
+
export declare const HOST_UPDATE_POLL_LIMIT_MS: number;
|
|
7
|
+
export type HostSelfUpdateCode = "started" | "current" | "recent" | "disabled" | "unreachable" | "local" | "pinned" | "spawn-failed";
|
|
8
|
+
/** How the host installed this copy. Only "npm" (unpinned) is updated. */
|
|
9
|
+
export type HostInstallSource = "npm" | "local" | "pinned";
|
|
10
|
+
export interface HostUpdateMarker {
|
|
11
|
+
version: string;
|
|
12
|
+
startedAt: number;
|
|
13
|
+
outcome?: "installed" | "failed";
|
|
14
|
+
finishedAt?: number;
|
|
15
|
+
}
|
|
16
|
+
export type HostUpdateStatus = "pending" | "installed" | "failed";
|
|
17
|
+
export interface HostSelfUpdateDeps {
|
|
18
|
+
/** False when `OMMS_DISABLE_UPDATE_CHECK` is `1`. */
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
/** Log message, for example `Pi plugin self-update`. */
|
|
21
|
+
logLabel: string;
|
|
22
|
+
currentVersion: string;
|
|
23
|
+
/** The host's own update command, for example `pi` with `["update", "npm:om-memory-system"]`. */
|
|
24
|
+
command: string;
|
|
25
|
+
args: string[];
|
|
26
|
+
/** The marker the detached process writes its result into. */
|
|
27
|
+
markerPath: string;
|
|
28
|
+
installSource: () => HostInstallSource;
|
|
29
|
+
fetch: typeof fetch;
|
|
30
|
+
now: () => number;
|
|
31
|
+
readMarker: () => HostUpdateMarker | null;
|
|
32
|
+
writeMarker: (value: HostUpdateMarker) => void;
|
|
33
|
+
/** The OMMS versions found on disk: the running copy's and, for OpenCode, its cache siblings. */
|
|
34
|
+
installedVersions: () => string[];
|
|
35
|
+
/** Start a process that outlives this session. */
|
|
36
|
+
spawnDetached: (command: string, args: string[]) => void;
|
|
37
|
+
log: (message: string, data: Record<string, unknown>) => void | Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
export interface HostSelfUpdateResult {
|
|
40
|
+
code: HostSelfUpdateCode;
|
|
41
|
+
/** The newer release on npm, or null when there is none to report. */
|
|
42
|
+
update: string | null;
|
|
43
|
+
}
|
|
44
|
+
/** Arguments for the script runner (`node` or Bun) that runs the host command detached. */
|
|
45
|
+
export declare function hostUpdateRunnerArgs(markerPath: string, marker: HostUpdateMarker, command: string, args: string[]): string[];
|
|
46
|
+
/** Start a background host update when npm has a newer release. Never throws. */
|
|
47
|
+
export declare function startHostSelfUpdate(deps: HostSelfUpdateDeps): Promise<HostSelfUpdateResult>;
|
|
48
|
+
/** The state of the update for `version` that the marker records, or null. */
|
|
49
|
+
export declare function markerStatus(marker: HostUpdateMarker | null, version: string): HostUpdateStatus | null;
|
|
50
|
+
/**
|
|
51
|
+
* `installed` holds only when a copy on disk has the release (or a newer one,
|
|
52
|
+
* when a release landed during the update). The host command can exit with 0
|
|
53
|
+
* and install nothing; that outcome reads as `failed`. Other states pass through.
|
|
54
|
+
*/
|
|
55
|
+
export declare function confirmedStatus(status: HostUpdateStatus | null, version: string, installed: string[]): HostUpdateStatus | null;
|
|
56
|
+
/** The marker's state for `version`, with `installed` confirmed against the copies on disk. */
|
|
57
|
+
export declare function hostUpdateStatus(deps: Pick<HostSelfUpdateDeps, "readMarker" | "installedVersions">, version: string): HostUpdateStatus | null;
|
|
58
|
+
/** Read a marker file. A missing, unreadable or malformed file is no marker. */
|
|
59
|
+
export declare function readMarkerFile(path: string): HostUpdateMarker | null;
|
|
60
|
+
/**
|
|
61
|
+
* Read the marker now, then every `intervalMs` until it holds an outcome or
|
|
62
|
+
* `maxMs` passes. Calls `onSettled` once with the outcome. Returns a stop function.
|
|
63
|
+
*/
|
|
64
|
+
export declare function watchHostUpdate(options: {
|
|
65
|
+
read: () => HostUpdateStatus | null;
|
|
66
|
+
onSettled: (status: "installed" | "failed") => void;
|
|
67
|
+
intervalMs?: number;
|
|
68
|
+
maxMs?: number;
|
|
69
|
+
}): () => void;
|
|
70
|
+
/** True when the module file sits in an npm install of om-memory-system. */
|
|
71
|
+
export declare function installedFromNpm(modulePath: string): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* True when the module sits in a host package cache folder named
|
|
74
|
+
* `om-memory-system` or `om-memory-system@latest`. Any other folder, such as
|
|
75
|
+
* one with a version, counts as pinned. OpenCode 2.2 adds a timestamp folder
|
|
76
|
+
* between that folder and `node_modules`.
|
|
77
|
+
*/
|
|
78
|
+
export declare function unpinnedCacheFolder(modulePath: string): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* True when a settings file lists `npm:om-memory-system` in `packages` with no
|
|
81
|
+
* version. A file that cannot be read, or an entry with a version, counts as pinned.
|
|
82
|
+
*/
|
|
83
|
+
export declare function unpinnedPackageSource(settingsText: string | null): boolean;
|
|
84
|
+
/** Footer text after the status glyph: a newer release, or one installed and waiting for a restart. */
|
|
85
|
+
export declare function updateSuffix(update: string, state: "available" | "installed"): string;
|
|
86
|
+
/** The one-time message for each update state. */
|
|
87
|
+
export declare function updateNotice(options: {
|
|
88
|
+
hostName: string;
|
|
89
|
+
command: string;
|
|
90
|
+
/** How to restart the host, in lower case. Defaults to `restart <hostName>`. */
|
|
91
|
+
restart?: string;
|
|
92
|
+
current: string;
|
|
93
|
+
update: string;
|
|
94
|
+
state: "available" | "installing" | "installed";
|
|
95
|
+
}): string;
|
|
96
|
+
/** Production inputs: npm, the marker file, and a detached script runner. */
|
|
97
|
+
export declare function hostSelfUpdateDeps(options: {
|
|
98
|
+
logLabel: string;
|
|
99
|
+
currentVersion: string;
|
|
100
|
+
command: string;
|
|
101
|
+
args: string[];
|
|
102
|
+
markerPath: string;
|
|
103
|
+
/** The file of the running module. The installed copies are found from it. */
|
|
104
|
+
modulePath: string;
|
|
105
|
+
installSource: () => HostInstallSource;
|
|
106
|
+
log: HostSelfUpdateDeps["log"];
|
|
107
|
+
env?: NodeJS.ProcessEnv;
|
|
108
|
+
}): HostSelfUpdateDeps;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { installedVersions } from "./host-copies.js";
|
|
5
|
+
import { availableUpdate, latestNpmVersion } from "./update-check.js";
|
|
6
|
+
import { compareVersions } from "./version-compare.js";
|
|
7
|
+
// Pi and OpenCode install OMMS from npm, but neither updates its plugins by
|
|
8
|
+
// itself. At session start OMMS compares the running copy with npm `latest`
|
|
9
|
+
// and, when it is older, runs the host's own update command in a detached
|
|
10
|
+
// process. That process writes the result into a marker file, which the
|
|
11
|
+
// running plugin reads to tell the user to restart. Claude Code has its own
|
|
12
|
+
// module (`src/adapters/claude-code/plugin-self-update.ts`).
|
|
13
|
+
/** A second session does not start another update for the same release within this time. */
|
|
14
|
+
export const HOST_SELF_UPDATE_RETRY_MS = 30 * 60 * 1000;
|
|
15
|
+
/** How often a running plugin reads the marker while an update is pending. */
|
|
16
|
+
export const HOST_UPDATE_POLL_MS = 60 * 1000;
|
|
17
|
+
/** How long a running plugin reads the marker before it gives up. */
|
|
18
|
+
export const HOST_UPDATE_POLL_LIMIT_MS = 10 * 60 * 1000;
|
|
19
|
+
/** The longest time the detached process lets the host command run. */
|
|
20
|
+
const HOST_COMMAND_TIMEOUT_MS = 120_000;
|
|
21
|
+
/**
|
|
22
|
+
* Node or Bun script that runs the host command, then records the outcome in
|
|
23
|
+
* the marker. Arguments: marker path, marker JSON, command, command args.
|
|
24
|
+
* `BUN_BE_BUN` lets a compiled Bun host (OpenCode) run this script; the host
|
|
25
|
+
* command must not inherit it.
|
|
26
|
+
*/
|
|
27
|
+
const UPDATE_SCRIPT = `
|
|
28
|
+
const { spawnSync } = require("node:child_process");
|
|
29
|
+
const { writeFileSync } = require("node:fs");
|
|
30
|
+
const [markerPath, markerJson, command, ...args] = process.argv.slice(1);
|
|
31
|
+
const env = { ...process.env };
|
|
32
|
+
delete env.BUN_BE_BUN;
|
|
33
|
+
let ok = false;
|
|
34
|
+
try {
|
|
35
|
+
const run = spawnSync(command, args, {
|
|
36
|
+
stdio: "ignore",
|
|
37
|
+
shell: process.platform === "win32",
|
|
38
|
+
windowsHide: true,
|
|
39
|
+
timeout: ${HOST_COMMAND_TIMEOUT_MS},
|
|
40
|
+
env,
|
|
41
|
+
});
|
|
42
|
+
ok = run.status === 0;
|
|
43
|
+
} catch {}
|
|
44
|
+
const marker = JSON.parse(markerJson);
|
|
45
|
+
writeFileSync(markerPath, JSON.stringify({ ...marker, outcome: ok ? "installed" : "failed", finishedAt: Date.now() }));
|
|
46
|
+
`;
|
|
47
|
+
/** Arguments for the script runner (`node` or Bun) that runs the host command detached. */
|
|
48
|
+
export function hostUpdateRunnerArgs(markerPath, marker, command, args) {
|
|
49
|
+
return ["-e", UPDATE_SCRIPT, markerPath, JSON.stringify(marker), command, ...args];
|
|
50
|
+
}
|
|
51
|
+
/** Start a background host update when npm has a newer release. Never throws. */
|
|
52
|
+
export async function startHostSelfUpdate(deps) {
|
|
53
|
+
const result = await decide(deps);
|
|
54
|
+
if (result.code === "started" || result.code === "spawn-failed") {
|
|
55
|
+
try {
|
|
56
|
+
await deps.log(deps.logLabel, {
|
|
57
|
+
code: result.code,
|
|
58
|
+
current: deps.currentVersion,
|
|
59
|
+
latest: result.update,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// A failed log write must not change the update result.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
async function decide(deps) {
|
|
69
|
+
if (!deps.enabled)
|
|
70
|
+
return { code: "disabled", update: null };
|
|
71
|
+
const latest = await latestNpmVersion(deps.fetch);
|
|
72
|
+
if (latest === null)
|
|
73
|
+
return { code: "unreachable", update: null };
|
|
74
|
+
const update = availableUpdate(deps.currentVersion, latest);
|
|
75
|
+
if (!update)
|
|
76
|
+
return { code: "current", update: null };
|
|
77
|
+
let source;
|
|
78
|
+
try {
|
|
79
|
+
source = deps.installSource();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
source = "pinned";
|
|
83
|
+
}
|
|
84
|
+
if (source !== "npm")
|
|
85
|
+
return { code: source, update };
|
|
86
|
+
const marker = deps.readMarker();
|
|
87
|
+
const now = deps.now();
|
|
88
|
+
if (marker?.version === update && now - marker.startedAt <= HOST_SELF_UPDATE_RETRY_MS) {
|
|
89
|
+
return { code: "recent", update };
|
|
90
|
+
}
|
|
91
|
+
const started = { version: update, startedAt: now };
|
|
92
|
+
try {
|
|
93
|
+
// Write first, so a session that starts during the update does not start another.
|
|
94
|
+
deps.writeMarker(started);
|
|
95
|
+
deps.spawnDetached(process.execPath, hostUpdateRunnerArgs(deps.markerPath, started, deps.command, deps.args));
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return { code: "spawn-failed", update };
|
|
99
|
+
}
|
|
100
|
+
return { code: "started", update };
|
|
101
|
+
}
|
|
102
|
+
/** The state of the update for `version` that the marker records, or null. */
|
|
103
|
+
export function markerStatus(marker, version) {
|
|
104
|
+
if (!marker || marker.version !== version)
|
|
105
|
+
return null;
|
|
106
|
+
return marker.outcome ?? "pending";
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* `installed` holds only when a copy on disk has the release (or a newer one,
|
|
110
|
+
* when a release landed during the update). The host command can exit with 0
|
|
111
|
+
* and install nothing; that outcome reads as `failed`. Other states pass through.
|
|
112
|
+
*/
|
|
113
|
+
export function confirmedStatus(status, version, installed) {
|
|
114
|
+
if (status !== "installed")
|
|
115
|
+
return status;
|
|
116
|
+
const found = installed.some((have) => have === version || (compareVersions(have, version) ?? -1) >= 0);
|
|
117
|
+
return found ? "installed" : "failed";
|
|
118
|
+
}
|
|
119
|
+
/** The marker's state for `version`, with `installed` confirmed against the copies on disk. */
|
|
120
|
+
export function hostUpdateStatus(deps, version) {
|
|
121
|
+
return confirmedStatus(markerStatus(deps.readMarker(), version), version, deps.installedVersions());
|
|
122
|
+
}
|
|
123
|
+
/** Read a marker file. A missing, unreadable or malformed file is no marker. */
|
|
124
|
+
export function readMarkerFile(path) {
|
|
125
|
+
try {
|
|
126
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
127
|
+
if (typeof value.version !== "string" || typeof value.startedAt !== "number")
|
|
128
|
+
return null;
|
|
129
|
+
const marker = { version: value.version, startedAt: value.startedAt };
|
|
130
|
+
if (value.outcome === "installed" || value.outcome === "failed") {
|
|
131
|
+
marker.outcome = value.outcome;
|
|
132
|
+
if (typeof value.finishedAt === "number")
|
|
133
|
+
marker.finishedAt = value.finishedAt;
|
|
134
|
+
}
|
|
135
|
+
return marker;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Read the marker now, then every `intervalMs` until it holds an outcome or
|
|
143
|
+
* `maxMs` passes. Calls `onSettled` once with the outcome. Returns a stop function.
|
|
144
|
+
*/
|
|
145
|
+
export function watchHostUpdate(options) {
|
|
146
|
+
const intervalMs = options.intervalMs ?? HOST_UPDATE_POLL_MS;
|
|
147
|
+
const deadline = Date.now() + (options.maxMs ?? HOST_UPDATE_POLL_LIMIT_MS);
|
|
148
|
+
let timer;
|
|
149
|
+
let done = false;
|
|
150
|
+
const stop = () => {
|
|
151
|
+
done = true;
|
|
152
|
+
if (timer)
|
|
153
|
+
clearInterval(timer);
|
|
154
|
+
};
|
|
155
|
+
const check = () => {
|
|
156
|
+
if (done)
|
|
157
|
+
return;
|
|
158
|
+
const status = options.read();
|
|
159
|
+
if (status === "installed" || status === "failed") {
|
|
160
|
+
stop();
|
|
161
|
+
options.onSettled(status);
|
|
162
|
+
}
|
|
163
|
+
else if (Date.now() >= deadline) {
|
|
164
|
+
stop();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
check();
|
|
168
|
+
if (!done) {
|
|
169
|
+
timer = setInterval(check, intervalMs);
|
|
170
|
+
timer.unref?.();
|
|
171
|
+
}
|
|
172
|
+
return stop;
|
|
173
|
+
}
|
|
174
|
+
/** True when the module file sits in an npm install of om-memory-system. */
|
|
175
|
+
export function installedFromNpm(modulePath) {
|
|
176
|
+
return modulePath.replaceAll("\\", "/").includes("/node_modules/om-memory-system/");
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* True when the module sits in a host package cache folder named
|
|
180
|
+
* `om-memory-system` or `om-memory-system@latest`. Any other folder, such as
|
|
181
|
+
* one with a version, counts as pinned. OpenCode 2.2 adds a timestamp folder
|
|
182
|
+
* between that folder and `node_modules`.
|
|
183
|
+
*/
|
|
184
|
+
export function unpinnedCacheFolder(modulePath) {
|
|
185
|
+
const parts = modulePath.replaceAll("\\", "/").split("/");
|
|
186
|
+
const index = parts.lastIndexOf("node_modules");
|
|
187
|
+
if (index < 1 || parts[index + 1] !== "om-memory-system")
|
|
188
|
+
return false;
|
|
189
|
+
const above = parts[index - 1];
|
|
190
|
+
const folder = /^\d+$/.test(above) ? parts[index - 2] : above;
|
|
191
|
+
return folder === "om-memory-system" || folder === "om-memory-system@latest";
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* True when a settings file lists `npm:om-memory-system` in `packages` with no
|
|
195
|
+
* version. A file that cannot be read, or an entry with a version, counts as pinned.
|
|
196
|
+
*/
|
|
197
|
+
export function unpinnedPackageSource(settingsText) {
|
|
198
|
+
if (settingsText === null)
|
|
199
|
+
return false;
|
|
200
|
+
let packages;
|
|
201
|
+
try {
|
|
202
|
+
packages = JSON.parse(settingsText).packages;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
if (!Array.isArray(packages))
|
|
208
|
+
return false;
|
|
209
|
+
return packages.some((entry) => {
|
|
210
|
+
const source = typeof entry === "string" ? entry : entry?.source;
|
|
211
|
+
return source === "npm:om-memory-system";
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/** Footer text after the status glyph: a newer release, or one installed and waiting for a restart. */
|
|
215
|
+
export function updateSuffix(update, state) {
|
|
216
|
+
return state === "installed" ? ` · ${update} installed, restart` : ` · ${update} available`;
|
|
217
|
+
}
|
|
218
|
+
/** The one-time message for each update state. */
|
|
219
|
+
export function updateNotice(options) {
|
|
220
|
+
const { hostName, command, current, update, state } = options;
|
|
221
|
+
const restart = options.restart ?? `restart ${hostName}`;
|
|
222
|
+
if (state === "installing")
|
|
223
|
+
return `om-memory-system ${update} is installing in the background.`;
|
|
224
|
+
if (state === "installed")
|
|
225
|
+
return `om-memory-system ${update} installed. ${restart[0].toUpperCase()}${restart.slice(1)} to use it.`;
|
|
226
|
+
return `om-memory-system ${update} is available (running ${current}). Run: ${command}, then ${restart}.`;
|
|
227
|
+
}
|
|
228
|
+
/** Production inputs: npm, the marker file, and a detached script runner. */
|
|
229
|
+
export function hostSelfUpdateDeps(options) {
|
|
230
|
+
const env = options.env ?? process.env;
|
|
231
|
+
const { modulePath, ...rest } = options;
|
|
232
|
+
return {
|
|
233
|
+
...rest,
|
|
234
|
+
enabled: env.OMMS_DISABLE_UPDATE_CHECK !== "1",
|
|
235
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
236
|
+
now: Date.now,
|
|
237
|
+
readMarker: () => readMarkerFile(options.markerPath),
|
|
238
|
+
installedVersions: () => installedVersions(modulePath),
|
|
239
|
+
writeMarker: (value) => {
|
|
240
|
+
mkdirSync(dirname(options.markerPath), { recursive: true });
|
|
241
|
+
writeFileSync(options.markerPath, JSON.stringify(value));
|
|
242
|
+
},
|
|
243
|
+
spawnDetached: (command, args) => {
|
|
244
|
+
const child = spawn(command, args, {
|
|
245
|
+
detached: true,
|
|
246
|
+
stdio: "ignore",
|
|
247
|
+
windowsHide: true,
|
|
248
|
+
env: { ...env, BUN_BE_BUN: "1" },
|
|
249
|
+
});
|
|
250
|
+
child.on("error", () => { });
|
|
251
|
+
child.unref();
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
@@ -7,6 +7,15 @@ import { type SpaceErrorCode } from "./spaces/registry.js";
|
|
|
7
7
|
*/
|
|
8
8
|
type Body = Record<string, unknown>;
|
|
9
9
|
export declare const STATUS_BY_CODE: Record<SpaceErrorCode | "transfer-busy", number>;
|
|
10
|
+
/** Starts a program with arguments, without a shell. Tests inject a fake. */
|
|
11
|
+
export type FolderOpener = (command: string, args: string[]) => Promise<void>;
|
|
12
|
+
/** What a spaces request carries besides its path. */
|
|
13
|
+
export interface SpaceRouteOptions {
|
|
14
|
+
/** Peer address of the caller. Only a loopback caller may start a file manager. */
|
|
15
|
+
remoteAddress?: string;
|
|
16
|
+
/** Starts the platform file manager. Defaults to the real one; tests inject a recorder. */
|
|
17
|
+
startOpener?: FolderOpener;
|
|
18
|
+
}
|
|
10
19
|
/** What the status call reports: the space this process uses now. */
|
|
11
20
|
export declare function spaceStatus(directory: string): {
|
|
12
21
|
problem?: string | undefined;
|
|
@@ -14,5 +23,5 @@ export declare function spaceStatus(directory: string): {
|
|
|
14
23
|
name: string;
|
|
15
24
|
customStoragePath: boolean;
|
|
16
25
|
};
|
|
17
|
-
export declare function routeSpaces(path: string, method: string, readBody: () => Promise<Body>, cwd: string): Promise<RouteResult | undefined>;
|
|
26
|
+
export declare function routeSpaces(path: string, method: string, readBody: () => Promise<Body>, cwd: string, options?: SpaceRouteOptions): Promise<RouteResult | undefined>;
|
|
18
27
|
export {};
|