@@ -294,8 +298,11 @@ export default function MarketplacePreviewPage() {
) : (
)}
+ {versionMismatch && (
+
diff --git a/lib/__tests__/version-compare.test.ts b/lib/__tests__/version-compare.test.ts
new file mode 100644
index 00000000..8d937398
--- /dev/null
+++ b/lib/__tests__/version-compare.test.ts
@@ -0,0 +1,46 @@
+import { describe, it, expect } from 'vitest';
+import { compareVersions, isVersionSatisfied } from '@/lib/version-compare';
+
+describe('compareVersions', () => {
+ it('orders by major, minor, patch', () => {
+ expect(compareVersions('1.0.0', '1.0.0')).toBe(0);
+ expect(compareVersions('1.0.1', '1.0.0')).toBeGreaterThan(0);
+ expect(compareVersions('1.0.0', '1.0.1')).toBeLessThan(0);
+ expect(compareVersions('2.0.0', '1.9.9')).toBeGreaterThan(0);
+ expect(compareVersions('1.10.0', '1.9.0')).toBeGreaterThan(0);
+ });
+
+ it('treats missing segments as 0', () => {
+ expect(compareVersions('1', '1.0.0')).toBe(0);
+ expect(compareVersions('1.2', '1.2.0')).toBe(0);
+ });
+
+ it('tolerates a leading v', () => {
+ expect(compareVersions('v1.6.7', '1.6.7')).toBe(0);
+ });
+
+ it('ignores pre-release / build metadata', () => {
+ expect(compareVersions('1.6.7-rc.1', '1.6.7')).toBe(0);
+ expect(compareVersions('1.6.7+build.5', '1.6.7')).toBe(0);
+ });
+});
+
+describe('isVersionSatisfied', () => {
+ it('returns true when current >= required', () => {
+ expect(isVersionSatisfied('1.6.7', '1.6.7')).toBe(true);
+ expect(isVersionSatisfied('1.6.8', '1.6.7')).toBe(true);
+ expect(isVersionSatisfied('2.0.0', '1.9.9')).toBe(true);
+ });
+
+ it('returns false when current < required', () => {
+ expect(isVersionSatisfied('1.6.6', '1.6.7')).toBe(false);
+ expect(isVersionSatisfied('1.5.0', '1.6.0')).toBe(false);
+ expect(isVersionSatisfied('0.0.0', '1.0.0')).toBe(false);
+ });
+
+ it('treats empty / null / undefined required as no requirement', () => {
+ expect(isVersionSatisfied('1.0.0', '')).toBe(true);
+ expect(isVersionSatisfied('1.0.0', null)).toBe(true);
+ expect(isVersionSatisfied('1.0.0', undefined)).toBe(true);
+ });
+});
diff --git a/lib/version-compare.ts b/lib/version-compare.ts
new file mode 100644
index 00000000..560fd99b
--- /dev/null
+++ b/lib/version-compare.ts
@@ -0,0 +1,39 @@
+/**
+ * Lenient semver comparison for the marketplace's `minAppVersion` gate.
+ *
+ * Parses "major.minor.patch" (any segment may be missing — treated as 0)
+ * and ignores pre-release / build metadata. Returns negative, zero or
+ * positive in the same shape as Array.prototype.sort comparators.
+ *
+ * We intentionally do NOT pull in a full semver dependency: plugins
+ * declare minimum app versions as simple "X.Y.Z" strings and we only
+ * need a >= check.
+ */
+export function compareVersions(a: string, b: string): number {
+ const pa = parseVersion(a);
+ const pb = parseVersion(b);
+ for (let i = 0; i < 3; i++) {
+ if (pa[i] !== pb[i]) return pa[i] - pb[i];
+ }
+ return 0;
+}
+
+function parseVersion(v: string): [number, number, number] {
+ const cleaned = String(v || '').trim().replace(/^v/i, '');
+ // Drop pre-release / build metadata.
+ const core = cleaned.split(/[-+]/)[0];
+ const parts = core.split('.').map((p) => {
+ const n = parseInt(p, 10);
+ return Number.isFinite(n) ? n : 0;
+ });
+ return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
+}
+
+/**
+ * True when `current` satisfies `required` (i.e. current >= required).
+ * Empty / null / undefined `required` is treated as no requirement.
+ */
+export function isVersionSatisfied(current: string, required: string | null | undefined): boolean {
+ if (!required) return true;
+ return compareVersions(current, required) >= 0;
+}