@roamcode.ai/server 1.0.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/LICENSE +21 -0
- package/dist/auth.d.ts +63 -0
- package/dist/auth.js +133 -0
- package/dist/claude-auth-service.d.ts +76 -0
- package/dist/claude-auth-service.js +217 -0
- package/dist/claude-latest-service.d.ts +34 -0
- package/dist/claude-latest-service.js +61 -0
- package/dist/config.d.ts +78 -0
- package/dist/config.js +59 -0
- package/dist/data-dir.d.ts +42 -0
- package/dist/data-dir.js +70 -0
- package/dist/diag.d.ts +43 -0
- package/dist/diag.js +83 -0
- package/dist/fs-service.d.ts +90 -0
- package/dist/fs-service.js +290 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +44 -0
- package/dist/managed-runtime.d.ts +51 -0
- package/dist/managed-runtime.js +411 -0
- package/dist/managed-update-helper.d.ts +2 -0
- package/dist/managed-update-helper.js +34 -0
- package/dist/mcp-send.d.ts +33 -0
- package/dist/mcp-send.js +107 -0
- package/dist/origin-check.d.ts +37 -0
- package/dist/origin-check.js +101 -0
- package/dist/pane-status.d.ts +61 -0
- package/dist/pane-status.js +145 -0
- package/dist/providers/claude-metadata-service.d.ts +58 -0
- package/dist/providers/claude-metadata-service.js +352 -0
- package/dist/providers/claude-provider.d.ts +11 -0
- package/dist/providers/claude-provider.js +166 -0
- package/dist/providers/codex-activity.d.ts +21 -0
- package/dist/providers/codex-activity.js +122 -0
- package/dist/providers/codex-app-server-client.d.ts +90 -0
- package/dist/providers/codex-app-server-client.js +485 -0
- package/dist/providers/codex-latest-service.d.ts +50 -0
- package/dist/providers/codex-latest-service.js +174 -0
- package/dist/providers/codex-metadata-service.d.ts +161 -0
- package/dist/providers/codex-metadata-service.js +686 -0
- package/dist/providers/codex-profile-client.d.ts +16 -0
- package/dist/providers/codex-profile-client.js +52 -0
- package/dist/providers/codex-profile-security.d.ts +23 -0
- package/dist/providers/codex-profile-security.js +161 -0
- package/dist/providers/codex-provider.d.ts +15 -0
- package/dist/providers/codex-provider.js +174 -0
- package/dist/providers/codex-thread-coordinator.d.ts +18 -0
- package/dist/providers/codex-thread-coordinator.js +93 -0
- package/dist/providers/codex-thread-persistence.d.ts +9 -0
- package/dist/providers/codex-thread-persistence.js +45 -0
- package/dist/providers/codex-thread-resolver.d.ts +59 -0
- package/dist/providers/codex-thread-resolver.js +322 -0
- package/dist/providers/options.d.ts +7 -0
- package/dist/providers/options.js +155 -0
- package/dist/providers/provider-artifacts.d.ts +3 -0
- package/dist/providers/provider-artifacts.js +30 -0
- package/dist/providers/registry.d.ts +7 -0
- package/dist/providers/registry.js +23 -0
- package/dist/providers/types.d.ts +95 -0
- package/dist/providers/types.js +8 -0
- package/dist/push-dispatch.d.ts +81 -0
- package/dist/push-dispatch.js +100 -0
- package/dist/push-store.d.ts +25 -0
- package/dist/push-store.js +79 -0
- package/dist/rate-limit.d.ts +52 -0
- package/dist/rate-limit.js +72 -0
- package/dist/server-config.d.ts +60 -0
- package/dist/server-config.js +77 -0
- package/dist/service-install.d.ts +60 -0
- package/dist/service-install.js +221 -0
- package/dist/session-defaults.d.ts +26 -0
- package/dist/session-defaults.js +60 -0
- package/dist/session-store.d.ts +81 -0
- package/dist/session-store.js +654 -0
- package/dist/start.d.ts +31 -0
- package/dist/start.js +372 -0
- package/dist/static-routes.d.ts +101 -0
- package/dist/static-routes.js +188 -0
- package/dist/terminal-capability.d.ts +5 -0
- package/dist/terminal-capability.js +27 -0
- package/dist/terminal-manager.d.ts +224 -0
- package/dist/terminal-manager.js +917 -0
- package/dist/terminal-process.d.ts +85 -0
- package/dist/terminal-process.js +238 -0
- package/dist/terminal-shared.d.ts +36 -0
- package/dist/terminal-shared.js +43 -0
- package/dist/tmux-list.d.ts +11 -0
- package/dist/tmux-list.js +39 -0
- package/dist/transport.d.ts +123 -0
- package/dist/transport.js +1559 -0
- package/dist/updater.d.ts +161 -0
- package/dist/updater.js +451 -0
- package/dist/usage-service.d.ts +118 -0
- package/dist/usage-service.js +173 -0
- package/dist/vapid.d.ts +17 -0
- package/dist/vapid.js +31 -0
- package/dist/web-push-send.d.ts +20 -0
- package/dist/web-push-send.js +21 -0
- package/dist/ws-ticket.d.ts +47 -0
- package/dist/ws-ticket.js +62 -0
- package/package.json +55 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import { type ManagedInstallStatus } from "./managed-runtime.js";
|
|
3
|
+
declare global {
|
|
4
|
+
/** Stable package version injected by tsup. Source/test builds fall back to package.json. */
|
|
5
|
+
const __SERVER_VERSION__: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
export declare const RUNNING_VERSION: string;
|
|
8
|
+
/** One-release compatibility export for diagnostics/tests that used the old SHA name. */
|
|
9
|
+
export declare const RUNNING_BUILD: string;
|
|
10
|
+
export declare const RELEASES_API = "https://api.github.com/repos/burakgon/roamcode/releases?per_page=100";
|
|
11
|
+
export declare const RELEASE_MANIFEST_ASSET = "roamcode-release.json";
|
|
12
|
+
export declare const CHECK_CACHE_MS: number;
|
|
13
|
+
export declare const FAILED_CHECK_TTL_MS = 30000;
|
|
14
|
+
export declare const FETCH_TIMEOUT_MS = 20000;
|
|
15
|
+
export declare const UPDATE_STALE_MS: number;
|
|
16
|
+
export interface UpdaterFs {
|
|
17
|
+
existsSync: (path: string) => boolean;
|
|
18
|
+
readFileSync: (path: string) => string;
|
|
19
|
+
writeFileSync: (path: string, data: string, mode?: number) => void;
|
|
20
|
+
mkdirSync: (path: string) => void;
|
|
21
|
+
chmodSync: (path: string, mode: number) => void;
|
|
22
|
+
renameSync?: (from: string, to: string) => void;
|
|
23
|
+
}
|
|
24
|
+
export declare const defaultUpdaterFs: UpdaterFs;
|
|
25
|
+
export interface GitHubReleaseAsset {
|
|
26
|
+
name: string;
|
|
27
|
+
browser_download_url: string;
|
|
28
|
+
}
|
|
29
|
+
export interface GitHubRelease {
|
|
30
|
+
tag_name: string;
|
|
31
|
+
name?: string;
|
|
32
|
+
body?: string;
|
|
33
|
+
published_at?: string;
|
|
34
|
+
draft: boolean;
|
|
35
|
+
prerelease: boolean;
|
|
36
|
+
assets?: GitHubReleaseAsset[];
|
|
37
|
+
}
|
|
38
|
+
export interface ReleaseRecord {
|
|
39
|
+
version: string;
|
|
40
|
+
tag: string;
|
|
41
|
+
name: string;
|
|
42
|
+
body: string;
|
|
43
|
+
publishedAt: string;
|
|
44
|
+
manifestUrl?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface ReleaseFetchResult {
|
|
47
|
+
releases?: GitHubRelease[];
|
|
48
|
+
etag?: string;
|
|
49
|
+
notModified?: boolean;
|
|
50
|
+
}
|
|
51
|
+
export type FetchReleases = (etag?: string) => Promise<ReleaseFetchResult>;
|
|
52
|
+
export type FetchManifest = (url: string) => Promise<unknown>;
|
|
53
|
+
export interface ChangelogEntry {
|
|
54
|
+
id: string;
|
|
55
|
+
version: string;
|
|
56
|
+
subject: string;
|
|
57
|
+
group: "new" | "fixes" | "improvements" | "other";
|
|
58
|
+
when: string;
|
|
59
|
+
date: string;
|
|
60
|
+
}
|
|
61
|
+
export type UpdateAction = "none" | "migrate" | "update" | "restart";
|
|
62
|
+
export type InstallationKind = "managed" | "legacy-git" | "unmanaged";
|
|
63
|
+
export interface VersionInfo {
|
|
64
|
+
current: string;
|
|
65
|
+
latest: string;
|
|
66
|
+
/** Compatibility alias: number of stable releases behind, never commit count. */
|
|
67
|
+
behind: number;
|
|
68
|
+
releaseCount: number;
|
|
69
|
+
updatable: boolean;
|
|
70
|
+
updateAvailable: boolean;
|
|
71
|
+
updateAction: UpdateAction;
|
|
72
|
+
installation: InstallationKind;
|
|
73
|
+
rollbackAvailable: boolean;
|
|
74
|
+
changelog: ChangelogEntry[];
|
|
75
|
+
runningVersion: string;
|
|
76
|
+
activeVersion?: string;
|
|
77
|
+
installDrift: boolean;
|
|
78
|
+
checkStatus: "fresh" | "stale" | "error";
|
|
79
|
+
checkedAt?: number;
|
|
80
|
+
error?: string;
|
|
81
|
+
/** Deprecated aliases retained until old precached clients have crossed the v1 bridge. */
|
|
82
|
+
runningBuild: string;
|
|
83
|
+
buildDrift: boolean;
|
|
84
|
+
}
|
|
85
|
+
export type UpdateState = ManagedInstallStatus["state"] | "idle";
|
|
86
|
+
export interface UpdateStatus {
|
|
87
|
+
operationId?: string;
|
|
88
|
+
state: UpdateState;
|
|
89
|
+
phase?: string;
|
|
90
|
+
target?: string;
|
|
91
|
+
fromVersion?: string;
|
|
92
|
+
error?: string;
|
|
93
|
+
log?: string;
|
|
94
|
+
updatedAt?: number;
|
|
95
|
+
}
|
|
96
|
+
export interface UpdaterDeps {
|
|
97
|
+
fs?: UpdaterFs;
|
|
98
|
+
spawn?: typeof nodeSpawn;
|
|
99
|
+
now?: () => number;
|
|
100
|
+
dataDir: string;
|
|
101
|
+
env?: NodeJS.ProcessEnv;
|
|
102
|
+
repoRoot?: string;
|
|
103
|
+
helperPath?: string;
|
|
104
|
+
fetchReleases?: FetchReleases;
|
|
105
|
+
fetchManifest?: FetchManifest;
|
|
106
|
+
runningVersion?: string;
|
|
107
|
+
}
|
|
108
|
+
export declare function normalizeRelease(raw: GitHubRelease): ReleaseRecord | undefined;
|
|
109
|
+
export declare function stableReleases(raw: GitHubRelease[]): ReleaseRecord[];
|
|
110
|
+
export declare function relativeWhen(iso: string, now: number): string;
|
|
111
|
+
export declare function parseReleaseNotes(release: ReleaseRecord, now: number): ChangelogEntry[];
|
|
112
|
+
export declare function computeInstallDrift(runningVersion: string, activeVersion: string | undefined): boolean;
|
|
113
|
+
/** Deprecated name retained as a version comparison alias. */
|
|
114
|
+
export declare const computeBuildDrift: (running: string, active: string) => boolean;
|
|
115
|
+
export declare function defaultFetchReleases(etag?: string): Promise<ReleaseFetchResult>;
|
|
116
|
+
export declare function defaultFetchManifest(url: string): Promise<unknown>;
|
|
117
|
+
export declare class Updater {
|
|
118
|
+
private readonly fs;
|
|
119
|
+
private readonly spawn;
|
|
120
|
+
private readonly now;
|
|
121
|
+
private readonly dataDir;
|
|
122
|
+
private readonly env;
|
|
123
|
+
private readonly repoRoot;
|
|
124
|
+
private readonly helperPath;
|
|
125
|
+
private readonly fetchReleases;
|
|
126
|
+
private readonly fetchManifest;
|
|
127
|
+
private readonly runningVersion;
|
|
128
|
+
private cache?;
|
|
129
|
+
private inFlight;
|
|
130
|
+
constructor(deps: UpdaterDeps);
|
|
131
|
+
private installRoot;
|
|
132
|
+
private installation;
|
|
133
|
+
private hasRestartableService;
|
|
134
|
+
private cachePath;
|
|
135
|
+
private loadPersistedReleaseCache;
|
|
136
|
+
private persistReleaseCache;
|
|
137
|
+
private refresh;
|
|
138
|
+
getVersion(force?: boolean): Promise<VersionInfo>;
|
|
139
|
+
readStatus(): UpdateStatus;
|
|
140
|
+
private writeStatus;
|
|
141
|
+
readLastGoodVersion(): string | undefined;
|
|
142
|
+
/** Deprecated compatibility method: rollback is now version/pointer based. */
|
|
143
|
+
readLastGoodSha(): string | undefined;
|
|
144
|
+
private updateIsRunning;
|
|
145
|
+
private finalizeRestartIfHealthy;
|
|
146
|
+
startUpdate(opts?: {
|
|
147
|
+
targetVersion?: string;
|
|
148
|
+
rollback?: boolean;
|
|
149
|
+
}): Promise<{
|
|
150
|
+
started: boolean;
|
|
151
|
+
reason?: string;
|
|
152
|
+
operationId?: string;
|
|
153
|
+
target?: string;
|
|
154
|
+
}>;
|
|
155
|
+
}
|
|
156
|
+
export declare function createUpdater(opts: {
|
|
157
|
+
dataDir: string;
|
|
158
|
+
env?: NodeJS.ProcessEnv;
|
|
159
|
+
repoRoot?: string;
|
|
160
|
+
runningVersion?: string;
|
|
161
|
+
}): Updater;
|
package/dist/updater.js
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { chmodSync as nodeChmodSync, existsSync as nodeExistsSync, mkdirSync as nodeMkdirSync, readFileSync as nodeReadFileSync, renameSync as nodeRenameSync, writeFileSync as nodeWriteFileSync, } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { compareVersions, isStableVersion, readActiveVersion, readPreviousVersion, resolveInstallRoot, } from "./managed-runtime.js";
|
|
7
|
+
function packageVersion() {
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(nodeReadFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"));
|
|
10
|
+
return typeof parsed.version === "string" ? parsed.version : "dev";
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return "dev";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export const RUNNING_VERSION = typeof __SERVER_VERSION__ === "string" && __SERVER_VERSION__
|
|
17
|
+
? __SERVER_VERSION__.replace(/^v/, "")
|
|
18
|
+
: packageVersion();
|
|
19
|
+
/** One-release compatibility export for diagnostics/tests that used the old SHA name. */
|
|
20
|
+
export const RUNNING_BUILD = RUNNING_VERSION;
|
|
21
|
+
export const RELEASES_API = "https://api.github.com/repos/burakgon/roamcode/releases?per_page=100";
|
|
22
|
+
export const RELEASE_MANIFEST_ASSET = "roamcode-release.json";
|
|
23
|
+
export const CHECK_CACHE_MS = 15 * 60_000;
|
|
24
|
+
export const FAILED_CHECK_TTL_MS = 30_000;
|
|
25
|
+
export const FETCH_TIMEOUT_MS = 20_000;
|
|
26
|
+
export const UPDATE_STALE_MS = 30 * 60_000;
|
|
27
|
+
export const defaultUpdaterFs = {
|
|
28
|
+
existsSync: nodeExistsSync,
|
|
29
|
+
readFileSync: (path) => nodeReadFileSync(path, "utf8"),
|
|
30
|
+
writeFileSync: (path, data, mode) => nodeWriteFileSync(path, data, mode === undefined ? undefined : { mode }),
|
|
31
|
+
mkdirSync: (path) => nodeMkdirSync(path, { recursive: true, mode: 0o700 }),
|
|
32
|
+
chmodSync: nodeChmodSync,
|
|
33
|
+
renameSync: nodeRenameSync,
|
|
34
|
+
};
|
|
35
|
+
export function normalizeRelease(raw) {
|
|
36
|
+
if (raw.draft || raw.prerelease)
|
|
37
|
+
return undefined;
|
|
38
|
+
const version = raw.tag_name.trim().replace(/^v/, "");
|
|
39
|
+
if (!isStableVersion(version))
|
|
40
|
+
return undefined;
|
|
41
|
+
const manifestUrl = raw.assets?.find((asset) => asset.name === RELEASE_MANIFEST_ASSET)?.browser_download_url;
|
|
42
|
+
return {
|
|
43
|
+
version,
|
|
44
|
+
tag: `v${version}`,
|
|
45
|
+
name: raw.name?.trim() || `v${version}`,
|
|
46
|
+
body: raw.body ?? "",
|
|
47
|
+
publishedAt: raw.published_at ?? "",
|
|
48
|
+
...(manifestUrl ? { manifestUrl } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function stableReleases(raw) {
|
|
52
|
+
return raw
|
|
53
|
+
.map(normalizeRelease)
|
|
54
|
+
.filter((release) => release !== undefined)
|
|
55
|
+
.sort((a, b) => compareVersions(b.version, a.version));
|
|
56
|
+
}
|
|
57
|
+
export function relativeWhen(iso, now) {
|
|
58
|
+
const then = Date.parse(iso);
|
|
59
|
+
if (Number.isNaN(then))
|
|
60
|
+
return "";
|
|
61
|
+
const days = Math.max(0, Math.floor((now - then) / 86_400_000));
|
|
62
|
+
if (days === 0)
|
|
63
|
+
return "now";
|
|
64
|
+
if (days < 7)
|
|
65
|
+
return `${days}d`;
|
|
66
|
+
if (days < 35)
|
|
67
|
+
return `${Math.floor(days / 7)}w`;
|
|
68
|
+
if (days < 365)
|
|
69
|
+
return `${Math.floor(days / 30)}mo`;
|
|
70
|
+
return `${Math.floor(days / 365)}y`;
|
|
71
|
+
}
|
|
72
|
+
function groupForHeading(heading) {
|
|
73
|
+
const normalized = heading.toLowerCase();
|
|
74
|
+
if (/new|added|feature/.test(normalized))
|
|
75
|
+
return "new";
|
|
76
|
+
if (/fix|security/.test(normalized))
|
|
77
|
+
return "fixes";
|
|
78
|
+
if (/improvement|changed|performance/.test(normalized))
|
|
79
|
+
return "improvements";
|
|
80
|
+
return "other";
|
|
81
|
+
}
|
|
82
|
+
export function parseReleaseNotes(release, now) {
|
|
83
|
+
const entries = [];
|
|
84
|
+
let group = "improvements";
|
|
85
|
+
for (const rawLine of release.body.split("\n")) {
|
|
86
|
+
const heading = /^#{2,4}\s+(.+)$/.exec(rawLine.trim());
|
|
87
|
+
if (heading?.[1]) {
|
|
88
|
+
group = groupForHeading(heading[1]);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const bullet = /^[-*]\s+(.+)$/.exec(rawLine.trim());
|
|
92
|
+
if (!bullet?.[1])
|
|
93
|
+
continue;
|
|
94
|
+
const subject = bullet[1].replace(/\s+by\s+@\S+\s+in\s+https?:\/\/\S+$/i, "").trim();
|
|
95
|
+
if (!subject)
|
|
96
|
+
continue;
|
|
97
|
+
entries.push({
|
|
98
|
+
id: `${release.version}:${entries.length}`,
|
|
99
|
+
version: release.version,
|
|
100
|
+
subject,
|
|
101
|
+
group,
|
|
102
|
+
when: relativeWhen(release.publishedAt, now),
|
|
103
|
+
date: release.publishedAt,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return entries;
|
|
107
|
+
}
|
|
108
|
+
export function computeInstallDrift(runningVersion, activeVersion) {
|
|
109
|
+
if (!activeVersion || !isStableVersion(runningVersion))
|
|
110
|
+
return false;
|
|
111
|
+
return runningVersion !== activeVersion;
|
|
112
|
+
}
|
|
113
|
+
/** Deprecated name retained as a version comparison alias. */
|
|
114
|
+
export const computeBuildDrift = (running, active) => computeInstallDrift(running, active);
|
|
115
|
+
export async function defaultFetchReleases(etag) {
|
|
116
|
+
const response = await fetch(RELEASES_API, {
|
|
117
|
+
headers: {
|
|
118
|
+
accept: "application/vnd.github+json",
|
|
119
|
+
"user-agent": "roamcode-updater",
|
|
120
|
+
...(etag ? { "if-none-match": etag } : {}),
|
|
121
|
+
},
|
|
122
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
123
|
+
});
|
|
124
|
+
if (response.status === 304)
|
|
125
|
+
return { notModified: true, etag };
|
|
126
|
+
if (!response.ok)
|
|
127
|
+
throw new Error(`GitHub Releases returned ${response.status}`);
|
|
128
|
+
const releases = (await response.json());
|
|
129
|
+
return { releases, etag: response.headers.get("etag") ?? undefined };
|
|
130
|
+
}
|
|
131
|
+
export async function defaultFetchManifest(url) {
|
|
132
|
+
const response = await fetch(url, {
|
|
133
|
+
headers: { accept: "application/octet-stream", "user-agent": "roamcode-updater" },
|
|
134
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
135
|
+
});
|
|
136
|
+
if (!response.ok)
|
|
137
|
+
throw new Error(`release manifest returned ${response.status}`);
|
|
138
|
+
return response.json();
|
|
139
|
+
}
|
|
140
|
+
function manifestIntegrities(value, version) {
|
|
141
|
+
if (!value || typeof value !== "object")
|
|
142
|
+
throw new Error("release manifest is invalid");
|
|
143
|
+
const manifest = value;
|
|
144
|
+
if (manifest.version !== version)
|
|
145
|
+
throw new Error("release manifest version does not match its tag");
|
|
146
|
+
const integrities = {};
|
|
147
|
+
for (const packageName of ["roamcode", "@roamcode.ai/server", "@roamcode.ai/web"]) {
|
|
148
|
+
const integrity = manifest.packages?.[packageName]?.integrity;
|
|
149
|
+
if (typeof integrity !== "string" || !integrity.startsWith("sha512-")) {
|
|
150
|
+
throw new Error(`release manifest has no ${packageName} npm integrity`);
|
|
151
|
+
}
|
|
152
|
+
integrities[packageName] = integrity;
|
|
153
|
+
}
|
|
154
|
+
return integrities;
|
|
155
|
+
}
|
|
156
|
+
function moduleDir() {
|
|
157
|
+
return dirname(fileURLToPath(import.meta.url));
|
|
158
|
+
}
|
|
159
|
+
export class Updater {
|
|
160
|
+
fs;
|
|
161
|
+
spawn;
|
|
162
|
+
now;
|
|
163
|
+
dataDir;
|
|
164
|
+
env;
|
|
165
|
+
repoRoot;
|
|
166
|
+
helperPath;
|
|
167
|
+
fetchReleases;
|
|
168
|
+
fetchManifest;
|
|
169
|
+
runningVersion;
|
|
170
|
+
cache;
|
|
171
|
+
inFlight = false;
|
|
172
|
+
constructor(deps) {
|
|
173
|
+
this.fs = deps.fs ?? defaultUpdaterFs;
|
|
174
|
+
this.spawn = deps.spawn ?? nodeSpawn;
|
|
175
|
+
this.now = deps.now ?? Date.now;
|
|
176
|
+
this.dataDir = deps.dataDir;
|
|
177
|
+
this.env = deps.env ?? process.env;
|
|
178
|
+
this.repoRoot = deps.repoRoot ?? join(moduleDir(), "..", "..", "..");
|
|
179
|
+
this.helperPath = deps.helperPath ?? join(moduleDir(), "managed-update-helper.js");
|
|
180
|
+
this.fetchReleases = deps.fetchReleases ?? defaultFetchReleases;
|
|
181
|
+
this.fetchManifest = deps.fetchManifest ?? defaultFetchManifest;
|
|
182
|
+
this.runningVersion = (deps.runningVersion ?? RUNNING_VERSION).replace(/^v/, "");
|
|
183
|
+
this.loadPersistedReleaseCache();
|
|
184
|
+
this.finalizeRestartIfHealthy();
|
|
185
|
+
}
|
|
186
|
+
installRoot() {
|
|
187
|
+
return resolveInstallRoot(this.env);
|
|
188
|
+
}
|
|
189
|
+
installation(activeVersion) {
|
|
190
|
+
if (activeVersion)
|
|
191
|
+
return "managed";
|
|
192
|
+
return this.fs.existsSync(join(this.repoRoot, ".git")) ? "legacy-git" : "unmanaged";
|
|
193
|
+
}
|
|
194
|
+
hasRestartableService() {
|
|
195
|
+
try {
|
|
196
|
+
const value = JSON.parse(this.fs.readFileSync(join(this.dataDir, "service.json")));
|
|
197
|
+
return ((value.manager === "launchd" || value.manager === "systemd") &&
|
|
198
|
+
typeof value.label === "string" &&
|
|
199
|
+
value.label.length > 0);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
cachePath() {
|
|
206
|
+
return join(this.dataDir, "release-cache.json");
|
|
207
|
+
}
|
|
208
|
+
loadPersistedReleaseCache() {
|
|
209
|
+
try {
|
|
210
|
+
const parsed = JSON.parse(this.fs.readFileSync(this.cachePath()));
|
|
211
|
+
if (typeof parsed.at === "number" && Array.isArray(parsed.releases)) {
|
|
212
|
+
this.cache = {
|
|
213
|
+
at: parsed.at,
|
|
214
|
+
releases: parsed.releases.filter((release) => isStableVersion(release.version)),
|
|
215
|
+
...(typeof parsed.etag === "string" ? { etag: parsed.etag } : {}),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// no last-known release feed yet
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
persistReleaseCache() {
|
|
224
|
+
if (!this.cache || this.cache.failed)
|
|
225
|
+
return;
|
|
226
|
+
this.fs.mkdirSync(this.dataDir);
|
|
227
|
+
this.fs.writeFileSync(this.cachePath(), JSON.stringify({ at: this.cache.at, etag: this.cache.etag, releases: this.cache.releases }, null, 2) + "\n", 0o600);
|
|
228
|
+
}
|
|
229
|
+
async refresh(force) {
|
|
230
|
+
const ttl = this.cache?.failed ? FAILED_CHECK_TTL_MS : CHECK_CACHE_MS;
|
|
231
|
+
if (!force && this.cache && this.now() - this.cache.at < ttl)
|
|
232
|
+
return;
|
|
233
|
+
try {
|
|
234
|
+
const result = await this.fetchReleases(this.cache?.etag);
|
|
235
|
+
if (result.notModified && this.cache) {
|
|
236
|
+
this.cache = { ...this.cache, at: this.now(), failed: false, error: undefined };
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
this.cache = {
|
|
240
|
+
at: this.now(),
|
|
241
|
+
releases: stableReleases(result.releases ?? []),
|
|
242
|
+
...(result.etag ? { etag: result.etag } : {}),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
this.persistReleaseCache();
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
249
|
+
if (this.cache?.releases.length)
|
|
250
|
+
this.cache = { ...this.cache, failed: true, error: message };
|
|
251
|
+
else
|
|
252
|
+
this.cache = { at: this.now(), releases: [], failed: true, error: message };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async getVersion(force = false) {
|
|
256
|
+
await this.refresh(force);
|
|
257
|
+
const activeVersion = readActiveVersion(this.installRoot());
|
|
258
|
+
const installation = this.installation(activeVersion);
|
|
259
|
+
const currentVersion = isStableVersion(this.runningVersion) ? this.runningVersion : activeVersion;
|
|
260
|
+
const latestRelease = this.cache?.releases[0];
|
|
261
|
+
const latestVersion = latestRelease?.version ?? currentVersion ?? "dev";
|
|
262
|
+
const newer = currentVersion && isStableVersion(currentVersion) && isStableVersion(latestVersion)
|
|
263
|
+
? (this.cache?.releases ?? []).filter((release) => compareVersions(release.version, currentVersion) > 0)
|
|
264
|
+
: latestRelease
|
|
265
|
+
? [latestRelease]
|
|
266
|
+
: [];
|
|
267
|
+
const installDrift = computeInstallDrift(this.runningVersion, activeVersion);
|
|
268
|
+
let updateAction = "none";
|
|
269
|
+
if (installDrift)
|
|
270
|
+
updateAction = "restart";
|
|
271
|
+
else if (newer.length > 0)
|
|
272
|
+
updateAction = "update";
|
|
273
|
+
else if (installation === "legacy-git" && latestRelease && currentVersion === latestVersion)
|
|
274
|
+
updateAction = "migrate";
|
|
275
|
+
// A foreground process cannot safely replace/restart itself. Both the v0 checkout installer and the
|
|
276
|
+
// managed installer persist service.json, so require that explicit supervisor contract before OTA.
|
|
277
|
+
const updatable = installation !== "unmanaged" && this.hasRestartableService();
|
|
278
|
+
const changelog = newer.flatMap((release) => parseReleaseNotes(release, this.now()));
|
|
279
|
+
const checkStatus = this.cache?.failed ? (this.cache.releases.length ? "stale" : "error") : "fresh";
|
|
280
|
+
return {
|
|
281
|
+
current: currentVersion && isStableVersion(currentVersion) ? `v${currentVersion}` : (currentVersion ?? "dev"),
|
|
282
|
+
latest: isStableVersion(latestVersion) ? `v${latestVersion}` : latestVersion,
|
|
283
|
+
behind: newer.length,
|
|
284
|
+
releaseCount: newer.length,
|
|
285
|
+
updatable,
|
|
286
|
+
updateAvailable: updatable && updateAction !== "none",
|
|
287
|
+
updateAction,
|
|
288
|
+
installation,
|
|
289
|
+
rollbackAvailable: readPreviousVersion(this.installRoot()) !== undefined,
|
|
290
|
+
changelog,
|
|
291
|
+
runningVersion: this.runningVersion,
|
|
292
|
+
...(activeVersion ? { activeVersion } : {}),
|
|
293
|
+
installDrift,
|
|
294
|
+
checkStatus,
|
|
295
|
+
...(this.cache ? { checkedAt: this.cache.at } : {}),
|
|
296
|
+
...(this.cache?.error ? { error: this.cache.error } : {}),
|
|
297
|
+
runningBuild: this.runningVersion,
|
|
298
|
+
buildDrift: installDrift,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
readStatus() {
|
|
302
|
+
try {
|
|
303
|
+
const parsed = JSON.parse(this.fs.readFileSync(join(this.dataDir, "update-status.json")));
|
|
304
|
+
return parsed && typeof parsed.state === "string" ? parsed : { state: "idle" };
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return { state: "idle" };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
writeStatus(status) {
|
|
311
|
+
this.fs.mkdirSync(this.dataDir);
|
|
312
|
+
const path = join(this.dataDir, "update-status.json");
|
|
313
|
+
const value = JSON.stringify(status, null, 2) + "\n";
|
|
314
|
+
if (this.fs.renameSync) {
|
|
315
|
+
const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
316
|
+
this.fs.writeFileSync(temp, value, 0o600);
|
|
317
|
+
this.fs.renameSync(temp, path);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
this.fs.writeFileSync(path, value, 0o600);
|
|
321
|
+
}
|
|
322
|
+
this.fs.chmodSync(path, 0o600);
|
|
323
|
+
}
|
|
324
|
+
readLastGoodVersion() {
|
|
325
|
+
return readPreviousVersion(this.installRoot());
|
|
326
|
+
}
|
|
327
|
+
/** Deprecated compatibility method: rollback is now version/pointer based. */
|
|
328
|
+
readLastGoodSha() {
|
|
329
|
+
return this.readLastGoodVersion();
|
|
330
|
+
}
|
|
331
|
+
updateIsRunning() {
|
|
332
|
+
const status = this.readStatus();
|
|
333
|
+
if (status.state === "idle" || status.state === "done" || status.state === "failed")
|
|
334
|
+
return false;
|
|
335
|
+
return this.now() - (status.updatedAt ?? 0) < UPDATE_STALE_MS;
|
|
336
|
+
}
|
|
337
|
+
finalizeRestartIfHealthy() {
|
|
338
|
+
const status = this.readStatus();
|
|
339
|
+
if (status.state !== "restarting" || !status.target || !status.operationId)
|
|
340
|
+
return;
|
|
341
|
+
const active = readActiveVersion(this.installRoot());
|
|
342
|
+
if (active !== status.target || this.runningVersion !== status.target)
|
|
343
|
+
return;
|
|
344
|
+
this.writeStatus({
|
|
345
|
+
operationId: status.operationId,
|
|
346
|
+
state: "done",
|
|
347
|
+
phase: "done",
|
|
348
|
+
target: status.target,
|
|
349
|
+
...(status.fromVersion ? { fromVersion: status.fromVersion } : {}),
|
|
350
|
+
updatedAt: this.now(),
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
async startUpdate(opts = {}) {
|
|
354
|
+
if (this.inFlight && !this.updateIsRunning())
|
|
355
|
+
this.inFlight = false;
|
|
356
|
+
if (this.inFlight || this.updateIsRunning())
|
|
357
|
+
return { started: false, reason: "an update is already in progress" };
|
|
358
|
+
const versionInfo = await this.getVersion(true);
|
|
359
|
+
if (!versionInfo.updatable)
|
|
360
|
+
return { started: false, reason: "run 'roamcode install' to enable managed OTA" };
|
|
361
|
+
let target;
|
|
362
|
+
let integrities;
|
|
363
|
+
if (opts.rollback) {
|
|
364
|
+
const previous = this.readLastGoodVersion();
|
|
365
|
+
if (!previous)
|
|
366
|
+
return { started: false, reason: "no previous managed version is available" };
|
|
367
|
+
target = previous;
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
target = (opts.targetVersion ?? versionInfo.latest).replace(/^v/, "");
|
|
371
|
+
if (!isStableVersion(target))
|
|
372
|
+
return { started: false, reason: "invalid target version" };
|
|
373
|
+
if (isStableVersion(this.runningVersion) && compareVersions(target, this.runningVersion) < 0) {
|
|
374
|
+
return { started: false, reason: "use the rollback action to activate an older version" };
|
|
375
|
+
}
|
|
376
|
+
if (target === this.runningVersion && versionInfo.updateAction === "none") {
|
|
377
|
+
return { started: false, reason: `v${target} is already active` };
|
|
378
|
+
}
|
|
379
|
+
const release = this.cache?.releases.find((candidate) => candidate.version === target);
|
|
380
|
+
if (!release)
|
|
381
|
+
return { started: false, reason: `v${target} is not a published stable GitHub Release` };
|
|
382
|
+
if (!release.manifestUrl)
|
|
383
|
+
return { started: false, reason: `v${target} is missing ${RELEASE_MANIFEST_ASSET}` };
|
|
384
|
+
try {
|
|
385
|
+
integrities = manifestIntegrities(await this.fetchManifest(release.manifestUrl), target);
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
return { started: false, reason: error instanceof Error ? error.message : String(error) };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const operationId = randomUUID();
|
|
392
|
+
const status = {
|
|
393
|
+
operationId,
|
|
394
|
+
state: "starting",
|
|
395
|
+
phase: opts.rollback
|
|
396
|
+
? "preparing rollback"
|
|
397
|
+
: versionInfo.updateAction === "migrate"
|
|
398
|
+
? "preparing migration"
|
|
399
|
+
: "starting",
|
|
400
|
+
target,
|
|
401
|
+
...(isStableVersion(this.runningVersion) ? { fromVersion: this.runningVersion } : {}),
|
|
402
|
+
updatedAt: this.now(),
|
|
403
|
+
};
|
|
404
|
+
this.writeStatus(status);
|
|
405
|
+
this.inFlight = true;
|
|
406
|
+
try {
|
|
407
|
+
const configPath = join(this.dataDir, `update-${operationId}.json`);
|
|
408
|
+
this.fs.mkdirSync(this.dataDir);
|
|
409
|
+
this.fs.writeFileSync(configPath, JSON.stringify({
|
|
410
|
+
operationId,
|
|
411
|
+
version: target,
|
|
412
|
+
installRoot: this.installRoot(),
|
|
413
|
+
dataDir: this.dataDir,
|
|
414
|
+
nodePath: process.execPath,
|
|
415
|
+
...(integrities ? { expectedIntegrities: integrities } : {}),
|
|
416
|
+
restart: true,
|
|
417
|
+
}, null, 2) + "\n", 0o600);
|
|
418
|
+
const child = this.spawn(process.execPath, [this.helperPath, configPath], {
|
|
419
|
+
detached: true,
|
|
420
|
+
stdio: "ignore",
|
|
421
|
+
env: { ...this.env, ROAMCODE_INSTALL_ROOT: this.installRoot() },
|
|
422
|
+
});
|
|
423
|
+
child.on("error", (error) => {
|
|
424
|
+
this.inFlight = false;
|
|
425
|
+
this.writeStatus({
|
|
426
|
+
...status,
|
|
427
|
+
state: "failed",
|
|
428
|
+
phase: "starting",
|
|
429
|
+
error: error.message,
|
|
430
|
+
updatedAt: this.now(),
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
child.unref();
|
|
434
|
+
return { started: true, operationId, target };
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
this.inFlight = false;
|
|
438
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
439
|
+
this.writeStatus({ ...status, state: "failed", phase: "starting", error: message, updatedAt: this.now() });
|
|
440
|
+
return { started: false, reason: message };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
export function createUpdater(opts) {
|
|
445
|
+
return new Updater({
|
|
446
|
+
dataDir: opts.dataDir,
|
|
447
|
+
env: opts.env,
|
|
448
|
+
repoRoot: opts.repoRoot,
|
|
449
|
+
runningVersion: opts.runningVersion,
|
|
450
|
+
});
|
|
451
|
+
}
|