diff --git a/apps/web/src/lib/version-check.test.mjs b/apps/web/src/lib/version-check.test.mjs
index ccf13ade..86e41b4a 100644
--- a/apps/web/src/lib/version-check.test.mjs
+++ b/apps/web/src/lib/version-check.test.mjs
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
-import { findDesktopReleaseVersion, getReleaseTagForVersion, isVersionOutdated, resolveLocalizedReleaseChanges } from "./version-check";
+import { findDesktopReleaseVersion, getReleaseTagForVersion, isClientAheadOfInstance, isVersionOutdated, resolveLocalizedReleaseChanges } from "./version-check";
describe("platform release version checks", () => {
test("derives the installed desktop version from the DMG asset", () => {
@@ -31,6 +31,13 @@ describe("platform release version checks", () => {
expect(isVersionOutdated("1.6.50", "1.6.50-beta.10")).toBe(false);
});
+ test("treats the client as ahead only when it is newer than a known instance", () => {
+ expect(isClientAheadOfInstance("1.66.0", "1.65.1")).toBe(true);
+ expect(isClientAheadOfInstance("1.65.1+4", "1.65.1")).toBe(false);
+ expect(isClientAheadOfInstance("1.66.0", null)).toBe(false);
+ expect(isClientAheadOfInstance("1.66.0", "local")).toBe(false);
+ });
+
test("maps deployed builds back to their formal release tag", () => {
expect(getReleaseTagForVersion("1.34.1+18")).toBe("v1.34.1");
expect(getReleaseTagForVersion("v1.35.0-beta.2+4")).toBe("v1.35.0-beta.2");
diff --git a/apps/web/src/lib/version-check.ts b/apps/web/src/lib/version-check.ts
index f3b66a1c..6f045fb0 100644
--- a/apps/web/src/lib/version-check.ts
+++ b/apps/web/src/lib/version-check.ts
@@ -16,13 +16,7 @@ export const findDesktopReleaseVersion = (assetNames: string[]) => {
: null;
};
-const parseVersion = (value: string) => {
- const match = value.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/i);
- return match ? {
- core: match.slice(1, 4).map(Number),
- prerelease: match[4]?.split(".") ?? null,
- } : null;
-};
+export { isClientAheadOfInstance, isVersionOutdated } from "@edgeever/shared";
export const getReleaseTagForVersion = (version: string) => {
const match = version.match(/^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?:\+[0-9A-Za-z.-]+)?$/i);
@@ -49,30 +43,3 @@ export const resolveLocalizedReleaseChanges = (
const fallbackLocale = locales.find((locale) => normalizeLocale(locale) === normalizedFallback);
return fallbackLocale ? changes[fallbackLocale] : [];
};
-
-export const isVersionOutdated = (currentVersion: string, latestVersion: string) => {
- const current = parseVersion(currentVersion);
- const latest = parseVersion(latestVersion);
- if (!current || !latest) return false;
-
- for (let index = 0; index < 3; index += 1) {
- if (current.core[index] !== latest.core[index]) return current.core[index] < latest.core[index];
- }
- if (!current.prerelease) return false;
- if (!latest.prerelease) return true;
- const length = Math.max(current.prerelease.length, latest.prerelease.length);
- for (let index = 0; index < length; index += 1) {
- const currentPart = current.prerelease[index];
- const latestPart = latest.prerelease[index];
- if (currentPart === undefined) return true;
- if (latestPart === undefined) return false;
- if (currentPart === latestPart) continue;
- const currentNumber = /^\d+$/.test(currentPart) ? Number(currentPart) : null;
- const latestNumber = /^\d+$/.test(latestPart) ? Number(latestPart) : null;
- if (currentNumber !== null && latestNumber !== null) return currentNumber < latestNumber;
- if (currentNumber !== null) return true;
- if (latestNumber !== null) return false;
- return currentPart.localeCompare(latestPart) < 0;
- }
- return false;
-};
diff --git a/package.json b/package.json
index 630478d1..8e894db6 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,7 @@
"release": "bun scripts/release.mjs",
"publish:stores": "bun scripts/store-delivery.mjs",
"test:desktop:sidecar": "bun scripts/test-desktop-sidecar.mjs",
- "test:desktop": "bun test apps/desktop/src/main/account-scope.test.mjs apps/desktop/src/main/app-protocol.test.mjs apps/desktop/src/main/child-process-start.test.mjs apps/desktop/src/main/clipboard-write.test.mjs apps/desktop/src/main/desktop-diagnostics.test.mjs apps/desktop/src/main/desktop-menu.test.mjs apps/desktop/src/main/desktop-update-flow.test.mjs apps/desktop/src/main/diagnostic-log.test.mjs apps/desktop/src/main/file-association.test.mjs apps/desktop/src/main/file-permissions.test.mjs apps/desktop/src/main/instance-update-gate.test.mjs apps/desktop/src/main/installation-location.test.mjs apps/desktop/src/main/local-data-reset.test.mjs apps/desktop/src/main/plugin-public-network.test.mjs apps/desktop/src/main/renderer-recovery.test.mjs apps/desktop/src/main/renderer-startup-guard.test.mjs apps/desktop/src/main/renderer-storage-migration.test.mjs apps/desktop/src/main/resource-request.test.mjs apps/desktop/src/main/resource-url.test.mjs apps/desktop/src/main/scheduled-task-scheduler.test.mjs apps/desktop/src/main/staged-resource.test.mjs apps/desktop/src/main/tray-icon.test.mjs apps/desktop/src/main/user-data-directory.test.mjs apps/desktop/src/main/window-open-policy.test.mjs apps/desktop/src/main/window-visibility.test.mjs apps/desktop/src/main/windows-update-trust.test.mjs apps/web/src/lib/api.test.mjs apps/web/src/lib/app-page-path.test.mjs apps/web/src/lib/clipboard.test.mjs apps/web/src/lib/desktop-renderer-ready.test.mjs apps/web/src/lib/desktop-repository.test.mjs apps/web/src/lib/desktop-resources.test.mjs apps/web/src/lib/desktop-sync.test.mjs apps/web/src/lib/local-mirror.test.mjs apps/web/src/lib/repository.test.mjs apps/web/src/lib/runtime.test.mjs apps/web/src/lib/sync-queue.test.mjs && bun run test:desktop:sidecar",
+ "test:desktop": "bun test apps/desktop/src/main/account-scope.test.mjs apps/desktop/src/main/app-protocol.test.mjs apps/desktop/src/main/child-process-start.test.mjs apps/desktop/src/main/clipboard-write.test.mjs apps/desktop/src/main/desktop-diagnostics.test.mjs apps/desktop/src/main/desktop-menu.test.mjs apps/desktop/src/main/desktop-update-flow.test.mjs apps/desktop/src/main/diagnostic-log.test.mjs apps/desktop/src/main/file-association.test.mjs apps/desktop/src/main/file-permissions.test.mjs apps/desktop/src/main/installation-location.test.mjs apps/desktop/src/main/local-data-reset.test.mjs apps/desktop/src/main/plugin-public-network.test.mjs apps/desktop/src/main/renderer-recovery.test.mjs apps/desktop/src/main/renderer-startup-guard.test.mjs apps/desktop/src/main/renderer-storage-migration.test.mjs apps/desktop/src/main/resource-request.test.mjs apps/desktop/src/main/resource-url.test.mjs apps/desktop/src/main/scheduled-task-scheduler.test.mjs apps/desktop/src/main/staged-resource.test.mjs apps/desktop/src/main/tray-icon.test.mjs apps/desktop/src/main/user-data-directory.test.mjs apps/desktop/src/main/window-open-policy.test.mjs apps/desktop/src/main/window-visibility.test.mjs apps/desktop/src/main/windows-update-trust.test.mjs apps/web/src/lib/api.test.mjs apps/web/src/lib/app-page-path.test.mjs apps/web/src/lib/clipboard.test.mjs apps/web/src/lib/desktop-renderer-ready.test.mjs apps/web/src/lib/desktop-repository.test.mjs apps/web/src/lib/desktop-resources.test.mjs apps/web/src/lib/desktop-sync.test.mjs apps/web/src/lib/local-mirror.test.mjs apps/web/src/lib/repository.test.mjs apps/web/src/lib/runtime.test.mjs apps/web/src/lib/sync-queue.test.mjs && bun run test:desktop:sidecar",
"verify:desktop-renderer": "bun scripts/verify-desktop-renderer.mjs",
"verify:renderer-origin-migration": "bun run --cwd apps/desktop verify-origin-migration",
"verify:desktop-cross-version-startup": "bun scripts/verify-desktop-cross-version-startup.mjs",
diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts
index 5cb865fa..8bea401b 100644
--- a/packages/shared/src/i18n/en-US.ts
+++ b/packages/shared/src/i18n/en-US.ts
@@ -1137,6 +1137,7 @@ export const enUS = {
browserMode: "Browser",
unknown: "Unknown",
copy: "Copy info",
+ clientAheadOfInstance: "This client is newer than the connected cloud instance, so some features may not work yet. You can wait for the daily automatic instance update, or update the instance manually.",
deployedUpdateTitle: "Your cloud instance was automatically updated to v{{version}}",
viewReleaseNotes: "Release notes",
desktopCheckForUpdates: "Check for updates",
diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts
index 07c28404..1da92358 100644
--- a/packages/shared/src/i18n/zh-CN.ts
+++ b/packages/shared/src/i18n/zh-CN.ts
@@ -1137,6 +1137,7 @@ export const zhCN = {
browserMode: "浏览器",
unknown: "未知",
copy: "复制信息",
+ clientAheadOfInstance: "当前客户端版本高于云端实例,部分功能可能暂不可用。可等待每天自动更新,也可手动更新实例。",
deployedUpdateTitle: "您的云端实例已自动更新至 v{{version}}",
viewReleaseNotes: "版本说明",
desktopCheckForUpdates: "检查更新",
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
index 0b7748d9..bfe34993 100644
--- a/packages/shared/src/index.ts
+++ b/packages/shared/src/index.ts
@@ -39,6 +39,7 @@ export * from "./file-attachment";
export * from "./note-image-card";
export * from "./pdf-attachment";
export * from "./types";
+export * from "./version";
export * from './plugin-capabilities';
export {
diff --git a/packages/shared/src/version.test.mjs b/packages/shared/src/version.test.mjs
new file mode 100644
index 00000000..802cb47e
--- /dev/null
+++ b/packages/shared/src/version.test.mjs
@@ -0,0 +1,24 @@
+import { describe, expect, test } from "bun:test";
+import { isClientAheadOfInstance, isVersionOutdated } from "./version";
+
+describe("release version comparison", () => {
+ test("compares semantic release components and ignores build metadata", () => {
+ expect(isVersionOutdated("1.6.49+12", "1.6.50")).toBe(true);
+ expect(isVersionOutdated("1.6.50+3", "1.6.50")).toBe(false);
+ expect(isVersionOutdated("1.6.50-beta.2", "1.6.50-beta.10")).toBe(true);
+ expect(isVersionOutdated("1.6.50-beta.10", "1.6.50")).toBe(true);
+ expect(isVersionOutdated("1.6.50", "1.6.50-beta.10")).toBe(false);
+ });
+
+ test("treats the client as ahead only when it is newer than a known instance", () => {
+ expect(isClientAheadOfInstance("1.66.0", "1.65.1")).toBe(true);
+ expect(isClientAheadOfInstance("v1.66.0", "v1.65.1")).toBe(true);
+ expect(isClientAheadOfInstance("1.65.1+4", "1.65.1")).toBe(false);
+ expect(isClientAheadOfInstance("1.65.1", "1.65.1")).toBe(false);
+ expect(isClientAheadOfInstance("1.64.0", "1.65.1")).toBe(false);
+ expect(isClientAheadOfInstance("1.66.0", null)).toBe(false);
+ expect(isClientAheadOfInstance("1.66.0", "")).toBe(false);
+ expect(isClientAheadOfInstance("1.66.0", "local")).toBe(false);
+ expect(isClientAheadOfInstance(null, "1.65.1")).toBe(false);
+ });
+});
diff --git a/apps/desktop/src/main/instance-update-gate.mjs b/packages/shared/src/version.ts
similarity index 62%
rename from apps/desktop/src/main/instance-update-gate.mjs
rename to packages/shared/src/version.ts
index 1413ad86..8800cee2 100644
--- a/apps/desktop/src/main/instance-update-gate.mjs
+++ b/packages/shared/src/version.ts
@@ -1,14 +1,16 @@
-const parseVersion = (value) => {
- const match = String(value ?? "").match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/i);
- return match
- ? { core: match.slice(1, 4).map(Number), prerelease: match[4]?.split(".") ?? null }
- : null;
+const parseVersion = (value: string) => {
+ const match = value.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/i);
+ return match ? {
+ core: match.slice(1, 4).map(Number),
+ prerelease: match[4]?.split(".") ?? null,
+ } : null;
};
-const isVersionOutdated = (currentVersion, latestVersion) => {
+export const isVersionOutdated = (currentVersion: string, latestVersion: string) => {
const current = parseVersion(currentVersion);
const latest = parseVersion(latestVersion);
if (!current || !latest) return false;
+
for (let index = 0; index < 3; index += 1) {
if (current.core[index] !== latest.core[index]) return current.core[index] < latest.core[index];
}
@@ -31,12 +33,7 @@ const isVersionOutdated = (currentVersion, latestVersion) => {
return false;
};
-export const instanceReleaseVersionFromPayload = (payload) => (
- payload && typeof payload.version === "string" && payload.version.trim()
- ? payload.version.trim()
- : null
-);
-
-export const shouldHoldAutoRestartUpdate = (updateVersion, instanceVersion) => (
- Boolean(updateVersion && instanceVersion && isVersionOutdated(instanceVersion, updateVersion))
-);
+export const isClientAheadOfInstance = (
+ clientVersion: string | null | undefined,
+ instanceVersion: string | null | undefined,
+) => Boolean(clientVersion && instanceVersion && isVersionOutdated(instanceVersion, clientVersion));