feat: manual check for updates menu item

This commit is contained in:
kemalai committed 2026-05-04 00:31:22 +03:00
1 parent 32b3c57edc
commit ca919feba3
10 files changed
+182 -15

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@freecrawl/cli",
"version": "0.2.9",
"version": "0.3.0",
"private": true,
"type": "module",
"bin": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@freecrawl/desktop",
"version": "0.2.9",
"version": "0.3.0",
"private": true,
"description": "FreeCrawl SEO — desktop application",
"main": "./out/main/index.js",
+154
View File
@@ -579,6 +579,7 @@ function rebuildMenu(): void {
recentProjects: getRecentProjects(),
onResetDiagnosticDialogs: () => resetDiagnosticDialogs(),
onOpenLogsFolder: () => openLogsFolder(),
onCheckForUpdates: () => void checkForUpdates(),
}),
);
}
@@ -640,6 +641,159 @@ function resetDiagnosticDialogs(): void {
}
}
/**
* Compare two semver strings (X.Y.Z, optional `v` prefix). Returns
* -1 if `a < b`, 0 if equal, +1 if `a > b`.
* Pre-release suffixes are ignored — the user's installed build is
* compared against the release tag's release-line only. Sufficient for
* a "should I show an update prompt?" decision; full semver semantics
* would need a dependency we'd rather not pull in for one menu item.
*/
function compareSemver(a: string, b: string): number {
const norm = (s: string) =>
s
.trim()
.replace(/^v/, '')
.split('-')[0]!
.split('.')
.map((p) => parseInt(p, 10) || 0);
const pa = norm(a);
const pb = norm(b);
for (let i = 0; i < 3; i++) {
const av = pa[i] ?? 0;
const bv = pb[i] ?? 0;
if (av < bv) return -1;
if (av > bv) return 1;
}
return 0;
}
interface GitHubLatestRelease {
tag_name?: string;
name?: string;
html_url?: string;
body?: string;
draft?: boolean;
prerelease?: boolean;
published_at?: string;
}
/**
* Manual update check. Hits the GitHub Releases API for the latest
* non-draft, non-prerelease tag, compares it with `app.getVersion()`,
* and surfaces a native dialog. Three outcomes:
* - up-to-date → "You're on the latest version (vX.Y.Z)"
* - update available → "v0.X.Y is available" + "Open release page" / "Later"
* - network error / rate → "Couldn't check" with the underlying error
*
* No background polling — only runs when the user clicks the menu item.
* No `electron-updater` dependency, no auto-install (that would need
* macOS notarisation + Windows code-signing first).
*/
async function checkForUpdates(): Promise<void> {
const win = mainWindow;
if (!win) return;
const installed = app.getVersion();
const apiUrl =
'https://api.github.com/repos/kemalai/FreeCrawl-SEO-Tool/releases/latest';
let release: GitHubLatestRelease | null = null;
let fetchError: string | null = null;
try {
const ac = new AbortController();
const timeout = setTimeout(() => ac.abort(), 10_000);
try {
const res = await fetch(apiUrl, {
headers: {
Accept: 'application/vnd.github+json',
'User-Agent': `FreeCrawl-SEO-Tool/${installed}`,
},
signal: ac.signal,
});
if (!res.ok) {
fetchError = `GitHub API returned ${res.status} ${res.statusText}`;
} else {
release = (await res.json()) as GitHubLatestRelease;
}
} finally {
clearTimeout(timeout);
}
} catch (err) {
fetchError =
err instanceof Error ? err.message : 'Unknown error contacting GitHub';
}
if (fetchError || !release || !release.tag_name) {
void dialog.showMessageBox(win, {
type: 'warning',
title: 'Update Check Failed',
message: "Couldn't reach the GitHub Releases API.",
detail:
(fetchError ?? 'No release tag in response.') +
'\n\nYou can browse releases manually at:\nhttps://github.com/kemalai/FreeCrawl-SEO-Tool/releases',
buttons: ['Open Releases Page', 'Close'],
defaultId: 0,
cancelId: 1,
noLink: true,
}).then((r) => {
if (r.response === 0) {
void shell.openExternal(
'https://github.com/kemalai/FreeCrawl-SEO-Tool/releases',
);
}
});
return;
}
const latest = release.tag_name;
const cmp = compareSemver(installed, latest);
if (cmp >= 0) {
void dialog.showMessageBox(win, {
type: 'info',
title: 'Up to Date',
message: `You're on the latest version (v${installed}).`,
detail: release.published_at
? `Latest GitHub release: ${latest}\nPublished: ${new Date(release.published_at).toLocaleString()}`
: `Latest GitHub release: ${latest}`,
buttons: ['OK'],
noLink: true,
});
return;
}
// Newer version available — show release notes preview + open page.
// Trim release body to keep the dialog readable; full notes are on the
// GitHub page the user opens.
const notes = (release.body ?? '').trim();
const notesPreview =
notes.length > 600 ? notes.slice(0, 600).trimEnd() + '\n…' : notes;
const detail =
`Installed: v${installed}\nLatest: ${latest}\n\n` +
(notesPreview
? `Release notes:\n${notesPreview}`
: 'See the release page for the changelog.');
void dialog
.showMessageBox(win, {
type: 'info',
title: 'Update Available',
message: `${latest} is available.`,
detail,
buttons: ['Open Release Page', 'Later'],
defaultId: 0,
cancelId: 1,
noLink: true,
})
.then((r) => {
if (r.response === 0) {
const url =
release?.html_url ??
'https://github.com/kemalai/FreeCrawl-SEO-Tool/releases/latest';
void shell.openExternal(url);
}
});
}
async function promptOpenProject(): Promise<void> {
if (!mainWindow) return;
const res = await dialog.showOpenDialog(mainWindow, {
+13
View File
@@ -21,6 +21,12 @@ export interface AppMenuHandlers {
onResetDiagnosticDialogs: () => void;
/** Reveal the on-disk logs directory in the OS file manager. */
onOpenLogsFolder: () => void;
/**
* Manual update check — fetches the latest GitHub release, compares
* with `app.getVersion()`, and surfaces a native dialog (up-to-date /
* update available with "Open release page" button / network error).
*/
onCheckForUpdates: () => void;
}
export function buildAppMenu(handlers: AppMenuHandlers): Menu {
@@ -199,6 +205,13 @@ export function buildAppMenu(handlers: AppMenuHandlers): Menu {
click: () => send('clear-all-data'),
},
{ type: 'separator' },
{
label: 'Check for Updates…',
toolTip:
'Fetch the latest GitHub release and compare it with your installed version. No background polling — runs only when you click.',
click: () => handlers.onCheckForUpdates(),
},
{ type: 'separator' },
{ label: 'About FreeCrawl SEO', click: () => send('about') },
],
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@freecrawl/mcp-server",
"version": "0.2.9",
"version": "0.3.0",
"private": true,
"type": "module",
"bin": {
+8 -8
View File
@@ -1,12 +1,12 @@
{
"name": "freecrawl-seo",
"version": "0.2.9",
"version": "0.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "freecrawl-seo",
"version": "0.2.9",
"version": "0.3.0",
"license": "MIT",
"workspaces": [
"apps/*",
@@ -25,7 +25,7 @@
},
"apps/cli": {
"name": "@freecrawl/cli",
"version": "0.2.9",
"version": "0.3.0",
"dependencies": {
"@freecrawl/core": "*",
"@freecrawl/db": "*",
@@ -42,7 +42,7 @@
},
"apps/desktop": {
"name": "@freecrawl/desktop",
"version": "0.2.9",
"version": "0.3.0",
"dependencies": {
"@freecrawl/core": "*",
"@freecrawl/db": "*",
@@ -84,7 +84,7 @@
},
"apps/mcp-server": {
"name": "@freecrawl/mcp-server",
"version": "0.2.9",
"version": "0.3.0",
"dependencies": {
"@freecrawl/db": "*",
"@freecrawl/shared-types": "*"
@@ -8732,7 +8732,7 @@
},
"packages/core": {
"name": "@freecrawl/core",
"version": "0.2.9",
"version": "0.3.0",
"dependencies": {
"@freecrawl/db": "*",
"@freecrawl/shared-types": "*",
@@ -8758,7 +8758,7 @@
},
"packages/db": {
"name": "@freecrawl/db",
"version": "0.2.9",
"version": "0.3.0",
"dependencies": {
"@freecrawl/shared-types": "*"
},
@@ -8770,7 +8770,7 @@
},
"packages/shared-types": {
"name": "@freecrawl/shared-types",
"version": "0.2.9",
"version": "0.3.0",
"devDependencies": {
"rimraf": "^6.0.1",
"typescript": "^5.7.0"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "freecrawl-seo",
"version": "0.2.9",
"version": "0.3.0",
"private": true,
"description": "Open-source desktop SEO crawler — a free alternative to Screaming Frog",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@freecrawl/core",
"version": "0.2.9",
"version": "0.3.0",
"private": true,
"type": "module",
"main": "./dist/index.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@freecrawl/db",
"version": "0.2.9",
"version": "0.3.0",
"private": true,
"type": "module",
"main": "./dist/index.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@freecrawl/shared-types",
"version": "0.2.9",
"version": "0.3.0",
"private": true,
"type": "module",
"main": "./dist/index.js",