fix(updater): update function support alpha (#45)
This commit is contained in:
104
.github/workflows/release.yml
vendored
104
.github/workflows/release.yml
vendored
@@ -206,10 +206,12 @@ jobs:
|
|||||||
# Job: Upload to Alibaba Cloud OSS
|
# Job: Upload to Alibaba Cloud OSS
|
||||||
# Uploads all release artifacts to OSS for:
|
# Uploads all release artifacts to OSS for:
|
||||||
# - Official website downloads (via release-info.json)
|
# - Official website downloads (via release-info.json)
|
||||||
# - electron-updater auto-update (via latest-*.yml)
|
# - electron-updater auto-update (via {channel}-*.yml)
|
||||||
#
|
#
|
||||||
# Directory structure on OSS:
|
# Directory structure on OSS (channel-separated):
|
||||||
# latest/ → always overwritten with the newest version
|
# latest/ → stable releases (latest.yml, latest-mac.yml, …)
|
||||||
|
# alpha/ → alpha releases (alpha.yml, alpha-mac.yml, …)
|
||||||
|
# beta/ → beta releases (beta.yml, beta-mac.yml, …)
|
||||||
# releases/vX.Y.Z/ → permanent archive, never deleted
|
# releases/vX.Y.Z/ → permanent archive, never deleted
|
||||||
# ──────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────
|
||||||
upload-oss:
|
upload-oss:
|
||||||
@@ -225,7 +227,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: release-artifacts
|
path: release-artifacts
|
||||||
|
|
||||||
- name: Extract version
|
- name: Extract version and channel
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||||
@@ -233,43 +235,88 @@ jobs:
|
|||||||
else
|
else
|
||||||
VERSION="${{ github.event.inputs.version }}"
|
VERSION="${{ github.event.inputs.version }}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Detect channel from semver prerelease tag
|
||||||
|
# e.g. 0.1.8-alpha.0 → alpha, 1.0.0-beta.1 → beta, 1.0.0 → latest
|
||||||
|
if [[ "$VERSION" =~ -([a-zA-Z]+) ]]; then
|
||||||
|
CHANNEL="${BASH_REMATCH[1]}"
|
||||||
|
else
|
||||||
|
CHANNEL="latest"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
echo "tag=v${VERSION}" >> $GITHUB_OUTPUT
|
echo "tag=v${VERSION}" >> $GITHUB_OUTPUT
|
||||||
echo "Detected version: ${VERSION}"
|
echo "channel=${CHANNEL}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Detected version: ${VERSION}, channel: ${CHANNEL}"
|
||||||
|
|
||||||
- name: Prepare upload directories
|
- name: Prepare upload directories
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
TAG="${{ steps.version.outputs.tag }}"
|
TAG="${{ steps.version.outputs.tag }}"
|
||||||
|
CHANNEL="${{ steps.version.outputs.channel }}"
|
||||||
|
|
||||||
mkdir -p staging/latest
|
mkdir -p staging/${CHANNEL}
|
||||||
mkdir -p staging/releases/${TAG}
|
mkdir -p staging/releases/${TAG}
|
||||||
|
|
||||||
# Flatten all platform artifacts into staging directories
|
# Flatten all platform artifacts into staging directories
|
||||||
find release-artifacts/ -type f | while read file; do
|
find release-artifacts/ -type f | while read file; do
|
||||||
filename=$(basename "$file")
|
filename=$(basename "$file")
|
||||||
cp "$file" "staging/latest/${filename}"
|
cp "$file" "staging/${CHANNEL}/${filename}"
|
||||||
cp "$file" "staging/releases/${TAG}/${filename}"
|
cp "$file" "staging/releases/${TAG}/${filename}"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "=== staging/latest/ ==="
|
echo "=== staging/${CHANNEL}/ ==="
|
||||||
ls -lh staging/latest/
|
ls -lh staging/${CHANNEL}/
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== staging/releases/${TAG}/ ==="
|
echo "=== staging/releases/${TAG}/ ==="
|
||||||
ls -lh staging/releases/${TAG}/
|
ls -lh staging/releases/${TAG}/
|
||||||
|
|
||||||
|
- name: Rename yml files for channel
|
||||||
|
run: |
|
||||||
|
CHANNEL="${{ steps.version.outputs.channel }}"
|
||||||
|
|
||||||
|
# electron-builder always generates latest*.yml.
|
||||||
|
# For non-stable channels, rename them so electron-updater can find
|
||||||
|
# e.g. alpha.yml, alpha-mac.yml, alpha-linux.yml
|
||||||
|
if [[ "$CHANNEL" != "latest" ]]; then
|
||||||
|
cd staging/${CHANNEL}
|
||||||
|
for f in latest*.yml; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
newname="${f/latest/$CHANNEL}"
|
||||||
|
echo "Renaming $f → $newname"
|
||||||
|
mv "$f" "$newname"
|
||||||
|
done
|
||||||
|
cd -
|
||||||
|
|
||||||
|
# Also rename in the archive directory
|
||||||
|
cd staging/releases/${{ steps.version.outputs.tag }}
|
||||||
|
for f in latest*.yml; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
newname="${f/latest/$CHANNEL}"
|
||||||
|
echo "Renaming (archive) $f → $newname"
|
||||||
|
mv "$f" "$newname"
|
||||||
|
done
|
||||||
|
cd -
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Final staging/$CHANNEL/ ==="
|
||||||
|
ls -lh staging/${CHANNEL}/
|
||||||
|
|
||||||
- name: Generate release-info.json
|
- name: Generate release-info.json
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
BASE_URL="https://oss.intelli-spectrum.com/latest"
|
CHANNEL="${{ steps.version.outputs.channel }}"
|
||||||
|
BASE_URL="https://oss.intelli-spectrum.com/${CHANNEL}"
|
||||||
|
|
||||||
jq -n \
|
jq -n \
|
||||||
--arg version "$VERSION" \
|
--arg version "$VERSION" \
|
||||||
|
--arg channel "$CHANNEL" \
|
||||||
--arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
--arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||||
--arg base "$BASE_URL" \
|
--arg base "$BASE_URL" \
|
||||||
--arg changelog "https://github.com/${{ github.repository }}/releases/tag/v${VERSION}" \
|
--arg changelog "https://github.com/${{ github.repository }}/releases/tag/v${VERSION}" \
|
||||||
'{
|
'{
|
||||||
version: $version,
|
version: $version,
|
||||||
|
channel: $channel,
|
||||||
releaseDate: $date,
|
releaseDate: $date,
|
||||||
downloads: {
|
downloads: {
|
||||||
mac: {
|
mac: {
|
||||||
@@ -289,10 +336,10 @@ jobs:
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
changelog: $changelog
|
changelog: $changelog
|
||||||
}' > staging/latest/release-info.json
|
}' > staging/${CHANNEL}/release-info.json
|
||||||
|
|
||||||
echo "=== release-info.json ==="
|
echo "=== release-info.json ==="
|
||||||
cat staging/latest/release-info.json
|
cat staging/${CHANNEL}/release-info.json
|
||||||
|
|
||||||
- name: Install and configure ossutil
|
- name: Install and configure ossutil
|
||||||
env:
|
env:
|
||||||
@@ -312,18 +359,20 @@ jobs:
|
|||||||
|
|
||||||
ossutil --version
|
ossutil --version
|
||||||
|
|
||||||
- name: "Upload to OSS: latest/ (overwrite)"
|
- name: "Upload to OSS: {channel}/ (overwrite)"
|
||||||
run: |
|
run: |
|
||||||
# Clean old latest/ to remove stale version files
|
CHANNEL="${{ steps.version.outputs.channel }}"
|
||||||
ossutil rm -r -f oss://valuecell-clawx/latest/ || true
|
|
||||||
|
# Only clean the current channel's directory — never touch other channels
|
||||||
|
ossutil rm -r -f oss://valuecell-clawx/${CHANNEL}/ || true
|
||||||
|
|
||||||
# Upload all files with no-cache so clients always get the freshest version
|
# Upload all files with no-cache so clients always get the freshest version
|
||||||
ossutil cp -r -f \
|
ossutil cp -r -f \
|
||||||
--meta="Cache-Control:no-cache,no-store,must-revalidate" \
|
--meta="Cache-Control:no-cache,no-store,must-revalidate" \
|
||||||
staging/latest/ \
|
staging/${CHANNEL}/ \
|
||||||
oss://valuecell-clawx/latest/
|
oss://valuecell-clawx/${CHANNEL}/
|
||||||
|
|
||||||
echo "Uploaded to latest/"
|
echo "Uploaded to ${CHANNEL}/"
|
||||||
|
|
||||||
- name: "Upload to OSS: releases/vX.Y.Z/ (archive)"
|
- name: "Upload to OSS: releases/vX.Y.Z/ (archive)"
|
||||||
run: |
|
run: |
|
||||||
@@ -340,9 +389,10 @@ jobs:
|
|||||||
- name: Verify OSS upload
|
- name: Verify OSS upload
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ steps.version.outputs.tag }}"
|
TAG="${{ steps.version.outputs.tag }}"
|
||||||
|
CHANNEL="${{ steps.version.outputs.channel }}"
|
||||||
|
|
||||||
echo "=== latest/ ==="
|
echo "=== ${CHANNEL}/ ==="
|
||||||
ossutil ls oss://valuecell-clawx/latest/ --short
|
ossutil ls oss://valuecell-clawx/${CHANNEL}/ --short
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== releases/${TAG}/ ==="
|
echo "=== releases/${TAG}/ ==="
|
||||||
@@ -350,8 +400,18 @@ jobs:
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Verify release-info.json ==="
|
echo "=== Verify release-info.json ==="
|
||||||
ossutil cp oss://valuecell-clawx/latest/release-info.json /tmp/release-info.json -f
|
ossutil cp oss://valuecell-clawx/${CHANNEL}/release-info.json /tmp/release-info.json -f
|
||||||
jq . /tmp/release-info.json
|
jq . /tmp/release-info.json
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "✅ All files uploaded and verified successfully!"
|
echo "=== Verify update yml ==="
|
||||||
|
if [[ "$CHANNEL" == "latest" ]]; then
|
||||||
|
YML_PREFIX="latest"
|
||||||
|
else
|
||||||
|
YML_PREFIX="$CHANNEL"
|
||||||
|
fi
|
||||||
|
echo "Looking for ${YML_PREFIX}*.yml files:"
|
||||||
|
ossutil ls oss://valuecell-clawx/${CHANNEL}/ --short | grep "${YML_PREFIX}.*\\.yml" || echo "(none found)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "All files uploaded and verified successfully!"
|
||||||
|
|||||||
@@ -151,14 +151,8 @@ async function initialize(): Promise<void> {
|
|||||||
// Register update handlers
|
// Register update handlers
|
||||||
registerUpdateHandlers(appUpdater, mainWindow);
|
registerUpdateHandlers(appUpdater, mainWindow);
|
||||||
|
|
||||||
// Check for updates after a delay (only in production)
|
// Note: Auto-check for updates is driven by the renderer (update store init)
|
||||||
if (!process.env.VITE_DEV_SERVER_URL) {
|
// so it respects the user's "Auto-check for updates" setting.
|
||||||
setTimeout(() => {
|
|
||||||
appUpdater.checkForUpdates().catch((err) => {
|
|
||||||
console.error('Failed to check for updates:', err);
|
|
||||||
});
|
|
||||||
}, 10000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle window close
|
// Handle window close
|
||||||
mainWindow.on('closed', () => {
|
mainWindow.on('closed', () => {
|
||||||
|
|||||||
@@ -3,12 +3,16 @@
|
|||||||
* Handles automatic application updates using electron-updater
|
* Handles automatic application updates using electron-updater
|
||||||
*
|
*
|
||||||
* Update providers are configured in electron-builder.yml (OSS primary, GitHub fallback).
|
* Update providers are configured in electron-builder.yml (OSS primary, GitHub fallback).
|
||||||
* electron-updater handles provider resolution automatically.
|
* For prerelease channels (alpha, beta), the feed URL is overridden at runtime
|
||||||
|
* to point at the channel-specific OSS directory (e.g. /alpha/, /beta/).
|
||||||
*/
|
*/
|
||||||
import { autoUpdater, UpdateInfo, ProgressInfo, UpdateDownloadedEvent } from 'electron-updater';
|
import { autoUpdater, UpdateInfo, ProgressInfo, UpdateDownloadedEvent } from 'electron-updater';
|
||||||
import { BrowserWindow, app, ipcMain } from 'electron';
|
import { BrowserWindow, app, ipcMain } from 'electron';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
/** Base CDN URL (without trailing channel path) */
|
||||||
|
const OSS_BASE_URL = 'https://oss.intelli-spectrum.com';
|
||||||
|
|
||||||
export interface UpdateStatus {
|
export interface UpdateStatus {
|
||||||
status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
|
status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
|
||||||
info?: UpdateInfo;
|
info?: UpdateInfo;
|
||||||
@@ -26,6 +30,15 @@ export interface UpdaterEvents {
|
|||||||
'error': (error: Error) => void;
|
'error': (error: Error) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect the update channel from a semver version string.
|
||||||
|
* e.g. "0.1.8-alpha.0" → "alpha", "1.0.0-beta.1" → "beta", "1.0.0" → "latest"
|
||||||
|
*/
|
||||||
|
function detectChannel(version: string): string {
|
||||||
|
const match = version.match(/-([a-zA-Z]+)/);
|
||||||
|
return match ? match[1] : 'latest';
|
||||||
|
}
|
||||||
|
|
||||||
export class AppUpdater extends EventEmitter {
|
export class AppUpdater extends EventEmitter {
|
||||||
private mainWindow: BrowserWindow | null = null;
|
private mainWindow: BrowserWindow | null = null;
|
||||||
private status: UpdateStatus = { status: 'idle' };
|
private status: UpdateStatus = { status: 'idle' };
|
||||||
@@ -45,6 +58,20 @@ export class AppUpdater extends EventEmitter {
|
|||||||
debug: (msg: string) => console.debug('[Updater]', msg),
|
debug: (msg: string) => console.debug('[Updater]', msg),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Override feed URL for prerelease channels so that
|
||||||
|
// alpha -> /alpha/alpha-mac.yml, beta -> /beta/beta-mac.yml, etc.
|
||||||
|
const version = app.getVersion();
|
||||||
|
const channel = detectChannel(version);
|
||||||
|
const feedUrl = `${OSS_BASE_URL}/${channel}`;
|
||||||
|
|
||||||
|
console.log(`[Updater] Version: ${version}, channel: ${channel}, feedUrl: ${feedUrl}`);
|
||||||
|
|
||||||
|
autoUpdater.setFeedURL({
|
||||||
|
provider: 'generic',
|
||||||
|
url: feedUrl,
|
||||||
|
useMultipleRangeRequest: false,
|
||||||
|
});
|
||||||
|
|
||||||
this.setupListeners();
|
this.setupListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,13 +142,34 @@ export class AppUpdater extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for updates
|
* Check for updates.
|
||||||
* electron-updater automatically tries providers defined in electron-builder.yml in order
|
* electron-updater automatically tries providers defined in electron-builder.yml in order.
|
||||||
|
*
|
||||||
|
* In dev mode (not packed), autoUpdater.checkForUpdates() silently returns
|
||||||
|
* null without emitting any events, so we must detect this and force a
|
||||||
|
* final status so the UI never gets stuck in 'checking'.
|
||||||
*/
|
*/
|
||||||
async checkForUpdates(): Promise<UpdateInfo | null> {
|
async checkForUpdates(): Promise<UpdateInfo | null> {
|
||||||
try {
|
try {
|
||||||
const result = await autoUpdater.checkForUpdates();
|
const result = await autoUpdater.checkForUpdates();
|
||||||
return result?.updateInfo || null;
|
|
||||||
|
// In dev mode (app not packaged), autoUpdater silently returns null
|
||||||
|
// without emitting ANY events (not even checking-for-update).
|
||||||
|
// Detect this and force an error so the UI never stays silent.
|
||||||
|
if (result == null) {
|
||||||
|
this.updateStatus({
|
||||||
|
status: 'error',
|
||||||
|
error: 'Update check skipped (dev mode – app is not packaged)',
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety net: if events somehow didn't fire, force a final state.
|
||||||
|
if (this.status.status === 'checking' || this.status.status === 'idle') {
|
||||||
|
this.updateStatus({ status: 'not-available' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.updateInfo || null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Updater] Check for updates failed:', error);
|
console.error('[Updater] Check for updates failed:', error);
|
||||||
this.updateStatus({ status: 'error', error: (error as Error).message || String(error) });
|
this.updateStatus({ status: 'error', error: (error as Error).message || String(error) });
|
||||||
@@ -189,13 +237,14 @@ export function registerUpdateHandlers(
|
|||||||
return updater.getCurrentVersion();
|
return updater.getCurrentVersion();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check for updates
|
// Check for updates – always return final status so the renderer
|
||||||
|
// never gets stuck in 'checking' waiting for a push event.
|
||||||
ipcMain.handle('update:check', async () => {
|
ipcMain.handle('update:check', async () => {
|
||||||
try {
|
try {
|
||||||
const info = await updater.checkForUpdates();
|
await updater.checkForUpdates();
|
||||||
return { success: true, info };
|
return { success: true, status: updater.getStatus() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: String(error) };
|
return { success: false, error: String(error), status: updater.getStatus() };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -227,42 +276,6 @@ export function registerUpdateHandlers(
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
// Forward update events to renderer
|
|
||||||
updater.on('checking-for-update', () => {
|
|
||||||
if (!mainWindow.isDestroyed()) {
|
|
||||||
mainWindow.webContents.send('update:checking');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
updater.on('update-available', (info) => {
|
|
||||||
if (!mainWindow.isDestroyed()) {
|
|
||||||
mainWindow.webContents.send('update:available', info);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
updater.on('update-not-available', (info) => {
|
|
||||||
if (!mainWindow.isDestroyed()) {
|
|
||||||
mainWindow.webContents.send('update:not-available', info);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
updater.on('download-progress', (progress) => {
|
|
||||||
if (!mainWindow.isDestroyed()) {
|
|
||||||
mainWindow.webContents.send('update:progress', progress);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
updater.on('update-downloaded', (event) => {
|
|
||||||
if (!mainWindow.isDestroyed()) {
|
|
||||||
mainWindow.webContents.send('update:downloaded', event);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
updater.on('error', (error) => {
|
|
||||||
if (!mainWindow.isDestroyed()) {
|
|
||||||
mainWindow.webContents.send('update:error', error.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export singleton instance
|
// Export singleton instance
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Displays update status and allows manual update checking/installation
|
* Displays update status and allows manual update checking/installation
|
||||||
*/
|
*/
|
||||||
import { useEffect, useCallback } from 'react';
|
import { useEffect, useCallback } from 'react';
|
||||||
import { Download, RefreshCw, CheckCircle2, AlertCircle, Loader2, Rocket } from 'lucide-react';
|
import { Download, RefreshCw, Loader2, Rocket } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
import { useUpdateStore } from '@/stores/update';
|
import { useUpdateStore } from '@/stores/update';
|
||||||
@@ -41,25 +41,6 @@ export function UpdateSettings() {
|
|||||||
await checkForUpdates();
|
await checkForUpdates();
|
||||||
}, [checkForUpdates, clearError]);
|
}, [checkForUpdates, clearError]);
|
||||||
|
|
||||||
const renderStatusIcon = () => {
|
|
||||||
switch (status) {
|
|
||||||
case 'checking':
|
|
||||||
return <Loader2 className="h-5 w-5 animate-spin text-blue-500" />;
|
|
||||||
case 'downloading':
|
|
||||||
return <Download className="h-5 w-5 text-blue-500 animate-pulse" />;
|
|
||||||
case 'available':
|
|
||||||
return <Download className="h-5 w-5 text-green-500" />;
|
|
||||||
case 'downloaded':
|
|
||||||
return <CheckCircle2 className="h-5 w-5 text-green-500" />;
|
|
||||||
case 'error':
|
|
||||||
return <AlertCircle className="h-5 w-5 text-red-500" />;
|
|
||||||
case 'not-available':
|
|
||||||
return <CheckCircle2 className="h-5 w-5 text-green-500" />;
|
|
||||||
default:
|
|
||||||
return <RefreshCw className="h-5 w-5 text-muted-foreground" />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderStatusText = () => {
|
const renderStatusText = () => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'checking':
|
case 'checking':
|
||||||
@@ -71,7 +52,7 @@ export function UpdateSettings() {
|
|||||||
case 'downloaded':
|
case 'downloaded':
|
||||||
return `Ready to install: v${updateInfo?.version}`;
|
return `Ready to install: v${updateInfo?.version}`;
|
||||||
case 'error':
|
case 'error':
|
||||||
return error || 'Update check failed';
|
return 'Update check failed';
|
||||||
case 'not-available':
|
case 'not-available':
|
||||||
return 'You have the latest version';
|
return 'You have the latest version';
|
||||||
default:
|
default:
|
||||||
@@ -138,13 +119,10 @@ export function UpdateSettings() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Current Version */}
|
{/* Current Version */}
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm font-medium">Current Version</p>
|
<p className="text-sm font-medium">Current Version</p>
|
||||||
<p className="text-2xl font-bold">v{currentVersion}</p>
|
<p className="text-2xl font-bold">v{currentVersion}</p>
|
||||||
</div>
|
</div>
|
||||||
{renderStatusIcon()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Status */}
|
{/* Status */}
|
||||||
<div className="flex items-center justify-between py-3 border-t border-b">
|
<div className="flex items-center justify-between py-3 border-t border-b">
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export function Settings() {
|
|||||||
|
|
||||||
const { status: gatewayStatus, restart: restartGateway } = useGatewayStore();
|
const { status: gatewayStatus, restart: restartGateway } = useGatewayStore();
|
||||||
const currentVersion = useUpdateStore((state) => state.currentVersion);
|
const currentVersion = useUpdateStore((state) => state.currentVersion);
|
||||||
|
const updateSetAutoDownload = useUpdateStore((state) => state.setAutoDownload);
|
||||||
const [controlUiInfo, setControlUiInfo] = useState<ControlUiInfo | null>(null);
|
const [controlUiInfo, setControlUiInfo] = useState<ControlUiInfo | null>(null);
|
||||||
const [openclawCliCommand, setOpenclawCliCommand] = useState('');
|
const [openclawCliCommand, setOpenclawCliCommand] = useState('');
|
||||||
const [openclawCliError, setOpenclawCliError] = useState<string | null>(null);
|
const [openclawCliError, setOpenclawCliError] = useState<string | null>(null);
|
||||||
@@ -380,7 +381,10 @@ export function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={autoDownloadUpdate}
|
checked={autoDownloadUpdate}
|
||||||
onCheckedChange={setAutoDownloadUpdate}
|
onCheckedChange={(value) => {
|
||||||
|
setAutoDownloadUpdate(value);
|
||||||
|
updateSetAutoDownload(value);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
* Manages application update state
|
* Manages application update state
|
||||||
*/
|
*/
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
import { useSettingsStore } from './settings';
|
||||||
|
|
||||||
export interface UpdateInfo {
|
export interface UpdateInfo {
|
||||||
version: string;
|
version: string;
|
||||||
@@ -83,6 +84,8 @@ export const useUpdateStore = create<UpdateState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Listen for update events
|
// Listen for update events
|
||||||
|
// Single source of truth: listen only to update:status-changed
|
||||||
|
// (sent by AppUpdater.updateStatus() in the main process)
|
||||||
window.electron.ipcRenderer.on('update:status-changed', (data) => {
|
window.electron.ipcRenderer.on('update:status-changed', (data) => {
|
||||||
const status = data as {
|
const status = data as {
|
||||||
status: UpdateStatus;
|
status: UpdateStatus;
|
||||||
@@ -98,31 +101,22 @@ export const useUpdateStore = create<UpdateState>((set, get) => ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
window.electron.ipcRenderer.on('update:checking', () => {
|
|
||||||
set({ status: 'checking', error: null });
|
|
||||||
});
|
|
||||||
|
|
||||||
window.electron.ipcRenderer.on('update:available', (info) => {
|
|
||||||
set({ status: 'available', updateInfo: info as UpdateInfo });
|
|
||||||
});
|
|
||||||
|
|
||||||
window.electron.ipcRenderer.on('update:not-available', () => {
|
|
||||||
set({ status: 'not-available' });
|
|
||||||
});
|
|
||||||
|
|
||||||
window.electron.ipcRenderer.on('update:progress', (progress) => {
|
|
||||||
set({ status: 'downloading', progress: progress as ProgressInfo });
|
|
||||||
});
|
|
||||||
|
|
||||||
window.electron.ipcRenderer.on('update:downloaded', (info) => {
|
|
||||||
set({ status: 'downloaded', updateInfo: info as UpdateInfo, progress: null });
|
|
||||||
});
|
|
||||||
|
|
||||||
window.electron.ipcRenderer.on('update:error', (error) => {
|
|
||||||
set({ status: 'error', error: error as string, progress: null });
|
|
||||||
});
|
|
||||||
|
|
||||||
set({ isInitialized: true });
|
set({ isInitialized: true });
|
||||||
|
|
||||||
|
// Apply persisted settings from the settings store
|
||||||
|
const { autoCheckUpdate, autoDownloadUpdate } = useSettingsStore.getState();
|
||||||
|
|
||||||
|
// Sync auto-download preference to the main process
|
||||||
|
if (autoDownloadUpdate) {
|
||||||
|
window.electron.ipcRenderer.invoke('update:setAutoDownload', true).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-check for updates on startup (respects user toggle)
|
||||||
|
if (autoCheckUpdate) {
|
||||||
|
setTimeout(() => {
|
||||||
|
get().checkForUpdates().catch(() => {});
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
checkForUpdates: async () => {
|
checkForUpdates: async () => {
|
||||||
@@ -134,15 +128,34 @@ export const useUpdateStore = create<UpdateState>((set, get) => ({
|
|||||||
new Promise((_, reject) => setTimeout(() => reject(new Error('Update check timed out')), 30000))
|
new Promise((_, reject) => setTimeout(() => reject(new Error('Update check timed out')), 30000))
|
||||||
]) as {
|
]) as {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
status?: {
|
||||||
|
status: UpdateStatus;
|
||||||
info?: UpdateInfo;
|
info?: UpdateInfo;
|
||||||
|
progress?: ProgressInfo;
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
if (!result.success) {
|
if (result.status) {
|
||||||
|
set({
|
||||||
|
status: result.status.status,
|
||||||
|
updateInfo: result.status.info || null,
|
||||||
|
progress: result.status.progress || null,
|
||||||
|
error: result.status.error || null,
|
||||||
|
});
|
||||||
|
} else if (!result.success) {
|
||||||
set({ status: 'error', error: result.error || 'Failed to check for updates' });
|
set({ status: 'error', error: result.error || 'Failed to check for updates' });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({ status: 'error', error: String(error) });
|
set({ status: 'error', error: String(error) });
|
||||||
|
} finally {
|
||||||
|
// In dev mode autoUpdater skips without emitting events, so the
|
||||||
|
// status may still be 'checking' or even 'idle'. Catch both.
|
||||||
|
const currentStatus = get().status;
|
||||||
|
if (currentStatus === 'checking' || currentStatus === 'idle') {
|
||||||
|
set({ status: 'error', error: 'Update check completed without a result. This usually means the app is running in dev mode.' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user