feat: macOS Developer ID 签名 + notarization + 原生热更新
签名 & 公证: - build/entitlements.mac.plist, build/entitlements.mac.inherit.plist — Electron hardened runtime 所需权限(JIT、unsigned memory、disable library validation) - electron-builder.yml — 启用 hardenedRuntime、entitlements、notarize: true - scripts/after-sign.js — 环境变量 + codesign -d 双重检测,有 Developer ID 时跳过 ad-hoc;无证书时 fallback ad-hoc - .github/workflows/build.yml — 映射签名 secrets(MAC_CERT_P12_BASE64 等),验证步骤断言 Developer ID Authority + TeamIdentifier,遍历双架构 .app 原生热更新: - electron/updater.ts — 从 no-op 重写为完整 electron-updater 实现(autoDownload:false, IPC handlers, 启动 10s 延迟检查) - electron/preload.ts — 恢复 updater IPC bridge - src/components/layout/AppShell.tsx — 运行时检测 native updater,useEffect 监听 updater:status 事件,native 优先 + browser fallback 其他: - README_CN.md — 更新自动更新功能描述 - package.json — 版本号升至 0.23.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
ddb2c00a8c
commit
0e38e4460e
11 files changed
+301
-76
No files matched your search
@@ -70,9 +70,57 @@ jobs:
|
||||
|
||||
- name: Package for macOS (x64 + arm64)
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||
CSC_LINK: ${{ secrets.MAC_CERT_P12_BASE64 }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERT_PASSWORD }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: npx electron-builder --mac --config electron-builder.yml --publish never
|
||||
|
||||
- name: Verify code signature
|
||||
run: |
|
||||
# Verify every .app bundle produced (arm64 + x64)
|
||||
APPS=$(find release -name "CodePilot.app" -maxdepth 3)
|
||||
if [ -z "$APPS" ]; then
|
||||
echo "::warning::No CodePilot.app found, skipping signature verification"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
FAILED=0
|
||||
while IFS= read -r APP_PATH; do
|
||||
echo "========== Verifying: $APP_PATH =========="
|
||||
|
||||
# Display signature details
|
||||
codesign -dv --verbose=4 "$APP_PATH" 2>&1 | tee /tmp/codesign-info.txt || true
|
||||
|
||||
# Assert Developer ID authority
|
||||
if ! grep -q 'Authority=Developer ID Application' /tmp/codesign-info.txt; then
|
||||
echo "::error::$APP_PATH is NOT signed with Developer ID Application"
|
||||
FAILED=1
|
||||
continue
|
||||
fi
|
||||
|
||||
# Assert correct Team Identifier
|
||||
if ! grep -q 'TeamIdentifier=K9X599X9Q2' /tmp/codesign-info.txt; then
|
||||
echo "::error::$APP_PATH has unexpected TeamIdentifier"
|
||||
FAILED=1
|
||||
continue
|
||||
fi
|
||||
|
||||
# Strict verification (must pass)
|
||||
codesign --verify --deep --strict --verbose=4 "$APP_PATH"
|
||||
|
||||
# Gatekeeper assessment (best-effort, does not fail the build)
|
||||
spctl -a -vvv --type execute "$APP_PATH" || echo "::warning::spctl assessment did not pass for $APP_PATH (may be expected in CI)"
|
||||
|
||||
echo "✓ $APP_PATH passed all checks"
|
||||
done <<< "$APPS"
|
||||
|
||||
if [ "$FAILED" -ne 0 ]; then
|
||||
echo "::error::One or more .app bundles failed signature verification"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@
|
||||
- **自定义技能** -- 定义可复用的提示词技能(全局或项目级别),在聊天中作为斜杠命令调用
|
||||
- **设置编辑器** -- 可视化和 JSON 编辑器管理 `~/.claude/settings.json`,包括权限和环境变量配置
|
||||
- **Token 用量追踪** -- 每次助手回复后查看输入/输出 Token 数量和预估费用
|
||||
- **自动更新检查** -- 应用定期检查新版本并在有更新时通知你
|
||||
- **自动更新** -- 已签名版本支持应用内自动下载和安装更新;未签名本地构建仅支持手动下载
|
||||
- **深色/浅色主题** -- 导航栏一键切换主题
|
||||
- **斜杠命令** -- 内置 `/help`、`/clear`、`/cost`、`/compact`、`/doctor`、`/review` 等命令
|
||||
- **Electron 打包** -- 桌面应用,隐藏标题栏,内置 Next.js 服务器,优雅关闭进程,自动端口分配
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<key>com.apple.security.inherit</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -48,6 +48,11 @@ mac:
|
||||
icon: build/icon.icns
|
||||
category: public.app-category.developer-tools
|
||||
artifactName: "${productName}-${version}-${arch}.${ext}"
|
||||
hardenedRuntime: true
|
||||
gatekeeperAssess: false
|
||||
entitlements: build/entitlements.mac.plist
|
||||
entitlementsInherit: build/entitlements.mac.inherit.plist
|
||||
notarize: true
|
||||
target:
|
||||
- target: dmg
|
||||
arch: [x64, arm64]
|
||||
|
||||
+10
-13
@@ -28,17 +28,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
bridge: {
|
||||
isActive: () => ipcRenderer.invoke('bridge:is-active'),
|
||||
},
|
||||
// Native auto-updater bridge — disabled (code signature issues on macOS).
|
||||
// Users are directed to download from GitHub Releases.
|
||||
// TODO: Re-enable after obtaining Apple Developer certificate.
|
||||
// updater: {
|
||||
// checkForUpdates: () => ipcRenderer.invoke('updater:check'),
|
||||
// downloadUpdate: () => ipcRenderer.invoke('updater:download'),
|
||||
// quitAndInstall: () => ipcRenderer.invoke('updater:quit-and-install'),
|
||||
// onStatus: (callback: (data: unknown) => void) => {
|
||||
// const listener = (_event: unknown, data: unknown) => callback(data);
|
||||
// ipcRenderer.on('updater:status', listener);
|
||||
// return () => { ipcRenderer.removeListener('updater:status', listener); };
|
||||
// },
|
||||
// },
|
||||
updater: {
|
||||
checkForUpdates: () => ipcRenderer.invoke('updater:check'),
|
||||
downloadUpdate: () => ipcRenderer.invoke('updater:download'),
|
||||
quitAndInstall: () => ipcRenderer.invoke('updater:quit-and-install'),
|
||||
onStatus: (callback: (data: unknown) => void) => {
|
||||
const listener = (_event: unknown, data: unknown) => callback(data);
|
||||
ipcRenderer.on('updater:status', listener);
|
||||
return () => { ipcRenderer.removeListener('updater:status', listener); };
|
||||
},
|
||||
},
|
||||
});
|
||||
+63
-18
@@ -1,25 +1,70 @@
|
||||
// =============================================================================
|
||||
// Native auto-updater (electron-updater) — DISABLED
|
||||
//
|
||||
// Temporarily disabled due to macOS code signature validation failures with
|
||||
// ad-hoc signing. Users are directed to download from GitHub Releases instead.
|
||||
// The browser-mode update check (via /api/app/updates) remains active in the
|
||||
// frontend to notify users of new versions.
|
||||
//
|
||||
// TODO: Re-enable after obtaining an Apple Developer certificate for proper
|
||||
// code signing, then uncomment this file and the calls in main.ts / preload.ts.
|
||||
// =============================================================================
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import { BrowserWindow, ipcMain } from 'electron';
|
||||
|
||||
// import { autoUpdater } from 'electron-updater';
|
||||
import type { BrowserWindow } from 'electron';
|
||||
// import { ipcMain, session } from 'electron';
|
||||
let win: BrowserWindow | null = null;
|
||||
|
||||
function sendStatus(data: {
|
||||
status: string;
|
||||
info?: unknown;
|
||||
progress?: unknown;
|
||||
error?: string;
|
||||
}) {
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send('updater:status', data);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function initAutoUpdater(_win: BrowserWindow) {
|
||||
console.log('[updater] Native auto-updater is disabled. Users should download updates from GitHub Releases.');
|
||||
win = _win;
|
||||
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
sendStatus({ status: 'checking' });
|
||||
});
|
||||
|
||||
autoUpdater.on('update-available', (info) => {
|
||||
sendStatus({ status: 'available', info });
|
||||
});
|
||||
|
||||
autoUpdater.on('update-not-available', (info) => {
|
||||
sendStatus({ status: 'not-available', info });
|
||||
});
|
||||
|
||||
autoUpdater.on('download-progress', (progress) => {
|
||||
sendStatus({ status: 'downloading', progress });
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
sendStatus({ status: 'downloaded', info });
|
||||
});
|
||||
|
||||
autoUpdater.on('error', (err) => {
|
||||
sendStatus({ status: 'error', error: err?.message ?? String(err) });
|
||||
});
|
||||
|
||||
// IPC handlers
|
||||
ipcMain.handle('updater:check', async () => {
|
||||
return autoUpdater.checkForUpdates();
|
||||
});
|
||||
|
||||
ipcMain.handle('updater:download', async () => {
|
||||
return autoUpdater.downloadUpdate();
|
||||
});
|
||||
|
||||
ipcMain.handle('updater:quit-and-install', () => {
|
||||
autoUpdater.quitAndInstall();
|
||||
});
|
||||
|
||||
// Delayed initial check (10 seconds after launch)
|
||||
setTimeout(() => {
|
||||
autoUpdater.checkForUpdates().catch((err) => {
|
||||
console.log('[updater] Initial update check failed:', err?.message ?? err);
|
||||
});
|
||||
}, 10_000);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function setUpdaterWindow(_win: BrowserWindow) {
|
||||
// no-op while native updater is disabled
|
||||
win = _win;
|
||||
}
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "codepilot",
|
||||
"version": "0.22.1",
|
||||
"version": "0.23.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "codepilot",
|
||||
"version": "0.22.1",
|
||||
"version": "0.23.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^3.0.47",
|
||||
"@ai-sdk/google": "^3.0.31",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codepilot",
|
||||
"version": "0.22.1",
|
||||
"version": "0.23.0",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "op7418",
|
||||
|
||||
+54
-25
@@ -1,24 +1,20 @@
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
/**
|
||||
* electron-builder afterSign hook — ad-hoc code signing for macOS.
|
||||
* electron-builder afterSign hook — code signing for macOS.
|
||||
*
|
||||
* electron-updater's ShipIt process validates code signatures when applying
|
||||
* updates. Without a valid signature the update fails with:
|
||||
* "Code signature did not pass validation: 代码未能满足指定的代码要求"
|
||||
* When a real Developer ID certificate is available (CSC_LINK or CSC_NAME env
|
||||
* vars are set), electron-builder handles signing automatically. This hook only
|
||||
* runs a strict verification to confirm the signature is intact.
|
||||
*
|
||||
* The previous approach used `codesign --force --deep -s -`, but --deep is
|
||||
* unreliable: it does not guarantee correct signing order and may miss nested
|
||||
* components, causing kSecCSStrictValidate failures.
|
||||
* When no certificate is available (local dev builds), falls back to ad-hoc
|
||||
* signing so that electron-updater's ShipIt process can still validate the
|
||||
* code signature.
|
||||
*
|
||||
* This script signs each component individually from the inside out:
|
||||
* Ad-hoc signing order (inside-out):
|
||||
* 1. All native binaries (.node, .dylib, .so)
|
||||
* 2. All Frameworks (*.framework)
|
||||
* 3. All Helper apps (*.app inside Frameworks/)
|
||||
* 4. The main .app bundle
|
||||
*
|
||||
* Runs in the afterSign hook so it executes AFTER electron-builder's own
|
||||
* signing step (which is a no-op with CSC_IDENTITY_AUTO_DISCOVERY=false)
|
||||
* and right before DMG/ZIP artifact creation.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
@@ -90,19 +86,55 @@ module.exports = async function afterSign(context) {
|
||||
const appPath = path.join(appOutDir, `${appName}.app`);
|
||||
|
||||
if (!fs.existsSync(appPath)) {
|
||||
console.warn(`[afterSign] macOS app not found at ${appPath}, skipping ad-hoc signing`);
|
||||
console.warn(`[afterSign] macOS app not found at ${appPath}, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Detect real (non-ad-hoc) code signature ───────────────────────────
|
||||
// Check env vars first (CI path), then probe the actual signature on the
|
||||
// .app bundle (covers the case where electron-builder auto-discovered a
|
||||
// Developer ID certificate from the local Keychain).
|
||||
let hasRealSignature = !!(process.env.CSC_LINK || process.env.CSC_NAME);
|
||||
|
||||
if (!hasRealSignature) {
|
||||
try {
|
||||
const info = execSync(`codesign -d --verbose=2 "${appPath}" 2>&1`, {
|
||||
stdio: 'pipe',
|
||||
timeout: 15000,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
if (/Authority=Developer ID Application/.test(info)) {
|
||||
hasRealSignature = true;
|
||||
}
|
||||
} catch {
|
||||
// codesign -d fails if the bundle is unsigned — that's fine
|
||||
}
|
||||
}
|
||||
|
||||
if (hasRealSignature) {
|
||||
console.log('[afterSign] Real code signing certificate detected (CSC_LINK/CSC_NAME set or Developer ID signature found).');
|
||||
console.log('[afterSign] Skipping ad-hoc signing to preserve Developer ID signature.');
|
||||
|
||||
try {
|
||||
execSync(`codesign --verify --deep --strict --verbose=4 "${appPath}"`, {
|
||||
stdio: 'pipe',
|
||||
timeout: 60000,
|
||||
});
|
||||
console.log('[afterSign] Developer ID signature verification passed.');
|
||||
} catch (err) {
|
||||
console.error('[afterSign] WARNING: Developer ID signature verification FAILED:', err.stderr?.toString() || err.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ── No certificate — ad-hoc signing fallback ─────────────────────────
|
||||
console.log(`[afterSign] Ad-hoc signing ${appPath} (individual component signing)...`);
|
||||
|
||||
const contentsPath = path.join(appPath, 'Contents');
|
||||
const frameworksPath = path.join(contentsPath, 'Frameworks');
|
||||
let signed = 0;
|
||||
|
||||
// ── Step 1: Sign all native binaries (.node, .dylib, .so) ─────────────
|
||||
// These are the innermost signable items. Must be signed before their
|
||||
// enclosing bundles.
|
||||
// Step 1: Sign all native binaries (.node, .dylib, .so)
|
||||
const nativeBinaries = collectFiles(contentsPath, ['.node', '.dylib', '.so']);
|
||||
for (const bin of nativeBinaries) {
|
||||
codesign(bin);
|
||||
@@ -112,9 +144,7 @@ module.exports = async function afterSign(context) {
|
||||
console.log(`[afterSign] Signed ${nativeBinaries.length} native binaries (.node/.dylib/.so)`);
|
||||
}
|
||||
|
||||
// ── Step 2: Sign all Frameworks ───────────────────────────────────────
|
||||
// Frameworks contain nested code that was already signed in step 1 (if any
|
||||
// .dylib/.so lived outside the framework) or that --sign covers here.
|
||||
// Step 2: Sign all Frameworks
|
||||
const frameworks = collectBundles(frameworksPath, '.framework');
|
||||
for (const fw of frameworks) {
|
||||
codesign(fw);
|
||||
@@ -124,8 +154,7 @@ module.exports = async function afterSign(context) {
|
||||
console.log(`[afterSign] Signed ${frameworks.length} frameworks`);
|
||||
}
|
||||
|
||||
// ── Step 3: Sign all Helper apps ──────────────────────────────────────
|
||||
// Electron ships multiple helper apps (GPU, Plugin, Renderer, etc.)
|
||||
// Step 3: Sign all Helper apps
|
||||
const helperApps = collectBundles(frameworksPath, '.app');
|
||||
for (const helper of helperApps) {
|
||||
codesign(helper);
|
||||
@@ -135,19 +164,19 @@ module.exports = async function afterSign(context) {
|
||||
console.log(`[afterSign] Signed ${helperApps.length} helper apps`);
|
||||
}
|
||||
|
||||
// ── Step 4: Sign the main app bundle ──────────────────────────────────
|
||||
// Step 4: Sign the main app bundle
|
||||
codesign(appPath);
|
||||
signed++;
|
||||
|
||||
console.log(`[afterSign] Ad-hoc signing complete — ${signed} components signed`);
|
||||
|
||||
// ── Verify ────────────────────────────────────────────────────────────
|
||||
// Verify
|
||||
try {
|
||||
execSync(`codesign --verify --strict "${appPath}"`, {
|
||||
execSync(`codesign --verify --deep --strict "${appPath}"`, {
|
||||
stdio: 'pipe',
|
||||
timeout: 30000,
|
||||
});
|
||||
console.log('[afterSign] Signature verification passed (--strict)');
|
||||
console.log('[afterSign] Signature verification passed (--deep --strict)');
|
||||
} catch (err) {
|
||||
console.error('[afterSign] WARNING: Signature verification FAILED:', err.stderr?.toString() || err.message);
|
||||
}
|
||||
|
||||
@@ -378,12 +378,74 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
const [checking, setChecking] = useState(false);
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
|
||||
// Native auto-updater disabled — always use browser-mode (GitHub API) check.
|
||||
// Users are directed to download from GitHub Releases.
|
||||
// TODO: Re-enable native updater after obtaining Apple Developer certificate.
|
||||
const isNativeUpdater = false;
|
||||
// Runtime detection: native updater available when running in Electron with updater bridge
|
||||
const isNativeUpdater = typeof window !== "undefined" && !!window.electronAPI?.updater;
|
||||
|
||||
// --- Browser-mode update check ---
|
||||
// --- Native updater status listener ---
|
||||
useEffect(() => {
|
||||
if (!isNativeUpdater) return;
|
||||
const cleanup = window.electronAPI!.updater!.onStatus((event) => {
|
||||
switch (event.status) {
|
||||
case 'available':
|
||||
setUpdateInfo((prev) => ({
|
||||
updateAvailable: true,
|
||||
latestVersion: event.info?.version ?? prev?.latestVersion ?? '',
|
||||
currentVersion: prev?.currentVersion ?? '',
|
||||
releaseName: event.info?.releaseName ?? prev?.releaseName ?? '',
|
||||
releaseNotes: typeof event.info?.releaseNotes === 'string' ? event.info.releaseNotes : prev?.releaseNotes ?? '',
|
||||
releaseUrl: prev?.releaseUrl ?? '',
|
||||
publishedAt: event.info?.releaseDate ?? prev?.publishedAt ?? '',
|
||||
downloadProgress: null,
|
||||
readyToInstall: false,
|
||||
isNativeUpdate: true,
|
||||
lastError: null,
|
||||
}));
|
||||
{
|
||||
const ver = event.info?.version;
|
||||
const dismissed = localStorage.getItem(DISMISSED_VERSION_KEY);
|
||||
if (ver && dismissed !== ver) {
|
||||
setShowDialog(true);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'not-available':
|
||||
setUpdateInfo((prev) => prev ? { ...prev, updateAvailable: false, isNativeUpdate: true, lastError: null } : prev);
|
||||
break;
|
||||
case 'downloading':
|
||||
setUpdateInfo((prev) => prev ? {
|
||||
...prev,
|
||||
downloadProgress: event.progress?.percent ?? prev.downloadProgress,
|
||||
isNativeUpdate: true,
|
||||
lastError: null,
|
||||
} : prev);
|
||||
break;
|
||||
case 'downloaded':
|
||||
setUpdateInfo((prev) => prev ? {
|
||||
...prev,
|
||||
readyToInstall: true,
|
||||
downloadProgress: 100,
|
||||
isNativeUpdate: true,
|
||||
lastError: null,
|
||||
} : prev);
|
||||
break;
|
||||
case 'error':
|
||||
setUpdateInfo((prev) => prev ? {
|
||||
...prev,
|
||||
lastError: event.error ?? 'Unknown error',
|
||||
isNativeUpdate: true,
|
||||
} : prev);
|
||||
break;
|
||||
}
|
||||
if (event.status === 'checking') {
|
||||
setChecking(true);
|
||||
} else {
|
||||
setChecking(false);
|
||||
}
|
||||
});
|
||||
return cleanup;
|
||||
}, [isNativeUpdater]);
|
||||
|
||||
// --- Browser-mode update check (fallback for non-Electron) ---
|
||||
const checkForUpdatesBrowser = useCallback(async () => {
|
||||
setChecking(true);
|
||||
try {
|
||||
@@ -412,29 +474,42 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Native updater check disabled — always use browser-mode check
|
||||
const checkForUpdates = checkForUpdatesBrowser;
|
||||
// --- Unified check: native first, browser fallback ---
|
||||
const checkForUpdates = useCallback(async () => {
|
||||
if (isNativeUpdater) {
|
||||
try {
|
||||
await window.electronAPI!.updater!.checkForUpdates();
|
||||
return;
|
||||
} catch {
|
||||
// native check failed, fall through to browser mode
|
||||
}
|
||||
}
|
||||
await checkForUpdatesBrowser();
|
||||
}, [isNativeUpdater, checkForUpdatesBrowser]);
|
||||
|
||||
// Browser mode: check on mount + every 8 hours
|
||||
// Browser mode: periodic check (non-Electron or as fallback)
|
||||
useEffect(() => {
|
||||
if (isNativeUpdater) return; // native updater handles its own initial check
|
||||
checkForUpdatesBrowser();
|
||||
const id = setInterval(checkForUpdatesBrowser, CHECK_INTERVAL);
|
||||
return () => clearInterval(id);
|
||||
}, [checkForUpdatesBrowser]);
|
||||
}, [isNativeUpdater, checkForUpdatesBrowser]);
|
||||
|
||||
const dismissUpdate = useCallback(() => {
|
||||
setShowDialog(false);
|
||||
}, []);
|
||||
|
||||
// Native download/install disabled — no-ops while native updater is disabled.
|
||||
// The UI shows "View Release" (opens GitHub) instead.
|
||||
const downloadUpdate = useCallback(async () => {
|
||||
// no-op: native updater disabled
|
||||
}, []);
|
||||
if (isNativeUpdater) {
|
||||
await window.electronAPI!.updater!.downloadUpdate();
|
||||
}
|
||||
}, [isNativeUpdater]);
|
||||
|
||||
const quitAndInstall = useCallback(() => {
|
||||
// no-op: native updater disabled
|
||||
}, []);
|
||||
if (isNativeUpdater) {
|
||||
window.electronAPI!.updater!.quitAndInstall();
|
||||
}
|
||||
}, [isNativeUpdater]);
|
||||
|
||||
const updateContextValue = useMemo(
|
||||
() => ({
|
||||
|
||||
Reference in new issue
Block a user