fix: improve auto-updater for beta releases and DMG installs (#1681)

* fix: improve auto-updater for beta releases and DMG installs

- Add allowPrerelease flag when beta channel selected, enabling
  electron-updater to find pre-releases on GitHub
- Detect read-only volumes (DMG) on macOS and show user-friendly
  warning instead of silent failure
- Load dotenv in electron.vite.config.ts for Sentry DSN embedding
- Replace unsafe dangerouslySetInnerHTML with ReactMarkdown +
  rehype-sanitize for secure HTML release notes rendering
- Use ES module imports and platform abstraction in app-updater.ts
- Add i18n translations for read-only volume warning (en/fr)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address PR review findings — deduplicate channel-setting logic, add consistent error handling

Issue 1: Code duplication in channel-setting logic
- setUpdateChannelWithDowngradeCheck() now calls setUpdateChannel() internally
  instead of duplicating the channel-setting code

Issue 2: Inconsistent error handling across update components
- Added AppUpdateErrorEvent type
- Exposed onAppUpdateError event listener in preload API
- Added error listeners to AppUpdateNotification.tsx, UpdateBanner.tsx,
  and AdvancedSettings.tsx for consistent error feedback

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add read-only volume warning to AppUpdateNotification and UpdateBanner

Added onAppUpdateReadOnlyVolume event listeners to both components
to show appropriate DMG warning when install fails due to read-only
volume, matching the behavior in AdvancedSettings.tsx.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address PR review findings for read-only volume warnings

- Add missing i18n keys to en/fr dialogs.json and navigation.json
- Add optional chaining guards for IPC listeners in AppUpdateNotification
- Use setUpdateChannel() in downloadStableVersion() to reset allowPrerelease
- Hide success message when read-only volume warning is active

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address follow-up PR review findings

- Add optional chaining guards for IPC listeners in AdvancedSettings
- Distinguish EROFS from EACCES in read-only volume detection
- Remove unused stack field from AppUpdateErrorEvent and IPC payload
- Return correct success:false from install IPC when blocked by read-only volume

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: disable install button on read-only warning and reset stale state

- Disable install button when showReadOnlyWarning is active in all three
  components (UpdateBanner, AppUpdateNotification, AdvancedSettings)
- Reset showReadOnlyWarning in onAppUpdateAvailable handlers across all
  three components to clear stale warnings on new update cycles
- Revert AdvancedSettings IPC listeners to direct-call pattern matching
  the existing listeners in the same useEffect block

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: handle unhandled promise, add optional chaining, reset stale error state

- Add .catch() to installAppUpdate preload to prevent unhandled rejection
- Add optional chaining guards for onAppUpdateReadOnlyVolume and
  onAppUpdateError in AdvancedSettings useEffect block
- Reset appUpdateError in onAppUpdateAvailable and onAppUpdateDownloaded
  handlers in AdvancedSettings
- Remove dismiss button from read-only warning in AdvancedSettings to
  prevent warning-install-warning cycle
- Fix misleading variable name and comment in isRunningFromReadOnlyVolume

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: show download errors to user, reset stale state, unify listener pattern

- Show download failure errors to user in AdvancedSettings instead of
  only logging to console
- Reset downloadError and showReadOnlyWarning in onAppUpdateDownloaded
  handlers in AppUpdateNotification and UpdateBanner
- Add releaseNotes and releaseDate to AppUpdateDownloadedEvent type to
  match the actual IPC payload from main process
- Remove unnecessary optional chaining guards from IPC listeners — all
  methods are required in ElectronAPI interface, use direct calls
  consistently across all three components

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: remove /Volumes/ false positive and unify UpdateBanner listener guards

- Remove /Volumes/ prefix early return in isRunningFromReadOnlyVolume()
  since writable external drives also mount under /Volumes/ on macOS;
  rely solely on accessSync EROFS check which handles all cases correctly
- Remove pre-existing optional chaining guards from UpdateBanner IPC
  listeners to match the direct-call pattern in AppUpdateNotification
  and AdvancedSettings — all methods are required in ElectronAPI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: simplify install IPC handler, reset stale state in poll check, unify API access pattern

- Simplify APP_UPDATE_INSTALL handler to fire-and-forget since failure is
  communicated via APP_UPDATE_READONLY_VOLUME event, not IPC return value
- Reset showReadOnlyWarning and downloadError in checkForUpdate poll
  callback when a new version is detected
- Remove unnecessary optional chaining from UpdateBanner electronAPI
  calls — all methods are required in ElectronAPI interface

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use quitAndInstall() return value in IPC handler

Return success:false when quitAndInstall() returns false (read-only
volume) instead of unconditionally reporting success.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add missing downloadError i18n key, document fire-and-forget IPC, remove stale optional chaining

- Add updates.downloadError key to en/fr settings.json so AdvancedSettings
  shows translated error text instead of raw key path
- Document that APP_UPDATE_INSTALL handler is fire-and-forget with failure
  communicated via APP_UPDATE_READONLY_VOLUME event, not IPC return
- Remove unnecessary optional chaining on electronAPI.openExternal in
  AppUpdateNotification to match direct-call pattern used elsewhere

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add safe link handler to ReleaseNotesRenderer, clamp progress, clear stale state

- Add safe link components to ReleaseNotesRenderer in AdvancedSettings
  so external links open in default browser instead of navigating the
  Electron window
- Clamp download progress percent to [0, 100] in UpdateBanner CSS width
- Reset downloadProgress to null in onAppUpdateError handlers across all
  three components to match onAppUpdateDownloaded pattern

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Test User <test@example.com>
This commit is contained in:
Andy
2026-02-05 14:15:42 +01:00
committed by GitHub
parent 6d0222fa9c
commit ff91a1af0f
19 changed files with 491 additions and 128 deletions
+4
View File
@@ -1,6 +1,10 @@
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
import { config as dotenvConfig } from 'dotenv';
// Load .env file for build-time constants (Sentry DSN, etc.)
dotenvConfig({ path: resolve(__dirname, '.env') });
/**
* Sentry configuration embedded at build time.
+2
View File
@@ -96,6 +96,8 @@
"react-i18next": "^16.5.0",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^4.2.0",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"semver": "^7.7.3",
"tailwind-merge": "^3.4.0",
+57 -11
View File
@@ -17,6 +17,8 @@
* - APP_UPDATE_ERROR: Error during update process
*/
import { accessSync, constants as fsConstants } from 'fs';
import path from 'path';
import { autoUpdater } from 'electron-updater';
import type { UpdateInfo } from 'electron-updater';
import { app, net } from 'electron';
@@ -24,6 +26,7 @@ import type { BrowserWindow } from 'electron';
import { IPC_CHANNELS } from '../shared/constants';
import type { AppUpdateInfo } from '../shared/types';
import { compareVersions } from './updater/version-manager';
import { isMacOS } from './platform';
// GitHub repo info for API calls
const GITHUB_OWNER = 'AndyMik90';
@@ -91,10 +94,13 @@ function formatReleaseNotes(releaseNotes: UpdateInfo['releaseNotes']): string |
*/
export function setUpdateChannel(channel: UpdateChannel): void {
autoUpdater.channel = channel;
// Enable pre-release scanning when beta channel is selected
// This allows electron-updater to find beta releases on GitHub
autoUpdater.allowPrerelease = channel === 'beta';
// Clear any downloaded update info when channel changes to prevent showing
// an Install button for an update from a different channel
downloadedUpdateInfo = null;
console.warn(`[app-updater] Update channel set to: ${channel}`);
console.warn(`[app-updater] Update channel set to: ${channel}, allowPrerelease: ${autoUpdater.allowPrerelease}`);
}
// Enable more verbose logging in debug mode
@@ -187,8 +193,7 @@ export function initializeAppUpdater(window: BrowserWindow, betaUpdates = false)
console.error('[app-updater] Update error:', error);
if (mainWindow) {
mainWindow.webContents.send(IPC_CHANNELS.APP_UPDATE_ERROR, {
message: error.message,
stack: error.stack
message: error.message
});
}
});
@@ -293,13 +298,57 @@ export async function downloadUpdate(): Promise<void> {
}
}
/**
* Check if the app is running from a read-only volume (e.g., DMG on macOS)
* Returns true if the app cannot be updated in place
*/
function isRunningFromReadOnlyVolume(): boolean {
if (!isMacOS()) {
return false;
}
const appPath = app.getAppPath();
// Check if the filesystem is read-only by testing write access.
// We don't use a /Volumes/ prefix check because writable external drives
// (USB, external SSDs) are also mounted under /Volumes/ on macOS.
try {
// Navigate from app.asar to the Contents/ directory (app.asar -> Resources -> Contents)
const contentsPath = path.resolve(appPath, '..', '..');
// Try to check if we can write to the app bundle's parent directory
accessSync(path.dirname(contentsPath), fsConstants.W_OK);
return false;
} catch (error: unknown) {
// Only treat as read-only if the filesystem itself is read-only (EROFS).
// Permission errors (EACCES) in managed/enterprise environments should not
// block updates — the updater may still have elevated access.
const code = error instanceof Error ? (error as NodeJS.ErrnoException).code : undefined;
return code === 'EROFS';
}
}
/**
* Quit and install update
* Called from IPC handler when user confirms installation
* Returns false if running from a read-only volume (update cannot proceed)
*/
export function quitAndInstall(): void {
export function quitAndInstall(): boolean {
// Check if running from read-only volume before attempting install
if (isRunningFromReadOnlyVolume()) {
console.warn('[app-updater] Cannot install: running from read-only volume');
if (mainWindow) {
mainWindow.webContents.send(IPC_CHANNELS.APP_UPDATE_READONLY_VOLUME, {
appPath: app.getAppPath()
});
}
return false;
}
console.warn('[app-updater] Quitting and installing update');
autoUpdater.quitAndInstall(false, true);
return true;
}
/**
@@ -483,11 +532,8 @@ export async function setUpdateChannelWithDowngradeCheck(
channel: UpdateChannel,
triggerDowngradeCheck = false
): Promise<AppUpdateInfo | null> {
autoUpdater.channel = channel;
// Clear any downloaded update info when channel changes to prevent showing
// an Install button for an update from a different channel
downloadedUpdateInfo = null;
console.warn(`[app-updater] Update channel set to: ${channel}`);
// Use the shared channel-setting function to avoid code duplication
setUpdateChannel(channel);
// If switching to stable and downgrade check requested, look for stable version
if (channel === 'latest' && triggerDowngradeCheck) {
@@ -509,8 +555,8 @@ export async function setUpdateChannelWithDowngradeCheck(
* Uses electron-updater with allowDowngrade enabled to download older stable versions
*/
export async function downloadStableVersion(): Promise<void> {
// Switch to stable channel
autoUpdater.channel = 'latest';
// Switch to stable channel (resets allowPrerelease and clears downloadedUpdateInfo)
setUpdateChannel('latest');
// Enable downgrade to allow downloading older versions (e.g., stable when on beta)
autoUpdater.allowDowngrade = true;
console.warn('[app-updater] Downloading stable version (allowDowngrade=true)...');
@@ -95,6 +95,10 @@ export function registerAppUpdateHandlers(): void {
IPC_CHANNELS.APP_UPDATE_INSTALL,
async (): Promise<IPCResult> => {
try {
// quitAndInstall() returns false if blocked by read-only volume,
// but the user is notified via APP_UPDATE_READONLY_VOLUME event instead.
// The preload fires this as fire-and-forget, so the return value is
// only consumed by the .catch() handler for unexpected errors.
quitAndInstall();
return { success: true };
} catch (error) {
@@ -4,6 +4,7 @@ import type {
AppUpdateProgress,
AppUpdateAvailableEvent,
AppUpdateDownloadedEvent,
AppUpdateErrorEvent,
IPCResult
} from '../../shared/types';
import { createIpcListener, invokeIpc, IpcListenerCleanup } from './modules/ipc-utils';
@@ -34,6 +35,12 @@ export interface AppUpdateAPI {
onAppUpdateStableDowngrade: (
callback: (info: AppUpdateInfo) => void
) => IpcListenerCleanup;
onAppUpdateReadOnlyVolume: (
callback: (info: { appPath: string }) => void
) => IpcListenerCleanup;
onAppUpdateError: (
callback: (error: AppUpdateErrorEvent) => void
) => IpcListenerCleanup;
}
/**
@@ -51,7 +58,9 @@ export const createAppUpdateAPI = (): AppUpdateAPI => ({
invokeIpc(IPC_CHANNELS.APP_UPDATE_DOWNLOAD_STABLE),
installAppUpdate: (): void => {
invokeIpc(IPC_CHANNELS.APP_UPDATE_INSTALL);
invokeIpc(IPC_CHANNELS.APP_UPDATE_INSTALL).catch((err) =>
console.error('[app-update] Install failed:', err)
);
},
getAppVersion: (): Promise<string> =>
@@ -79,5 +88,15 @@ export const createAppUpdateAPI = (): AppUpdateAPI => ({
onAppUpdateStableDowngrade: (
callback: (info: AppUpdateInfo) => void
): IpcListenerCleanup =>
createIpcListener(IPC_CHANNELS.APP_UPDATE_STABLE_DOWNGRADE, callback)
createIpcListener(IPC_CHANNELS.APP_UPDATE_STABLE_DOWNGRADE, callback),
onAppUpdateReadOnlyVolume: (
callback: (info: { appPath: string }) => void
): IpcListenerCleanup =>
createIpcListener(IPC_CHANNELS.APP_UPDATE_READONLY_VOLUME, callback),
onAppUpdateError: (
callback: (error: AppUpdateErrorEvent) => void
): IpcListenerCleanup =>
createIpcListener(IPC_CHANNELS.APP_UPDATE_ERROR, callback)
});
@@ -1,8 +1,10 @@
import { useState, useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Download, RefreshCw, CheckCircle2, AlertCircle, ExternalLink } from "lucide-react";
import { Download, RefreshCw, CheckCircle2, AlertCircle, AlertTriangle, ExternalLink } from "lucide-react";
import ReactMarkdown, { type Components } from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
import { Button } from "./ui/button";
import { Progress } from "./ui/progress";
import {
@@ -70,6 +72,7 @@ export function AppUpdateNotification() {
const [isDownloading, setIsDownloading] = useState(false);
const [isDownloaded, setIsDownloaded] = useState(false);
const [downloadError, setDownloadError] = useState<string | null>(null);
const [showReadOnlyWarning, setShowReadOnlyWarning] = useState(false);
// Create markdown components with translated accessibility text
const markdownComponents: Components = useMemo(
@@ -88,6 +91,7 @@ export function AppUpdateNotification() {
setIsDownloaded(false);
setDownloadProgress(null);
setDownloadError(null);
setShowReadOnlyWarning(false);
});
return cleanup;
@@ -99,6 +103,8 @@ export function AppUpdateNotification() {
setIsDownloading(false);
setIsDownloaded(true);
setDownloadProgress(null);
setDownloadError(null);
setShowReadOnlyWarning(false);
});
return cleanup;
@@ -113,6 +119,26 @@ export function AppUpdateNotification() {
return cleanup;
}, []);
// Listen for update errors (e.g., install failures)
useEffect(() => {
const cleanup = window.electronAPI.onAppUpdateError((error) => {
setDownloadError(error.message);
setIsDownloading(false);
setDownloadProgress(null);
});
return cleanup;
}, []);
// Listen for read-only volume warning (when trying to install from DMG on macOS)
useEffect(() => {
const cleanup = window.electronAPI.onAppUpdateReadOnlyVolume(() => {
setShowReadOnlyWarning(true);
});
return cleanup;
}, []);
const handleDownload = async () => {
setIsDownloading(true);
setDownloadError(null);
@@ -189,7 +215,11 @@ export function AppUpdateNotification() {
{updateInfo.releaseNotes && (
<div className="bg-background rounded-lg p-4 max-h-64 overflow-y-auto border border-border/50">
<div className="prose prose-sm dark:prose-invert max-w-none">
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeSanitize]}
components={markdownComponents}
>
{updateInfo.releaseNotes}
</ReactMarkdown>
</div>
@@ -201,7 +231,7 @@ export function AppUpdateNotification() {
variant="link"
size="sm"
className="w-full text-xs text-muted-foreground gap-1"
onClick={() => window.electronAPI?.openExternal?.(CLAUDE_CODE_CHANGELOG_URL)}
onClick={() => window.electronAPI.openExternal(CLAUDE_CODE_CHANGELOG_URL)}
aria-label={t(
"dialogs:appUpdate.claudeCodeChangelogAriaLabel",
"View Claude Code Changelog (opens in new window)"
@@ -238,8 +268,23 @@ export function AppUpdateNotification() {
</div>
)}
{/* Read-Only Volume Warning (DMG install on macOS) */}
{showReadOnlyWarning && (
<div className="flex items-start gap-3 text-sm text-warning bg-warning/10 border border-warning/30 rounded-lg p-3">
<AlertTriangle className="h-5 w-5 shrink-0 mt-0.5" />
<div className="space-y-1">
<p className="font-medium">
{t("dialogs:appUpdate.readOnlyVolumeTitle", "Cannot install from disk image")}
</p>
<p className="text-muted-foreground">
{t("dialogs:appUpdate.readOnlyVolumeDescription", "Please move Auto Claude to your Applications folder before updating.")}
</p>
</div>
</div>
)}
{/* Downloaded Success */}
{isDownloaded && (
{isDownloaded && !showReadOnlyWarning && (
<div className="flex items-center gap-3 text-sm text-success bg-success/10 border border-success/30 rounded-lg p-3">
<CheckCircle2 className="h-5 w-5 shrink-0" />
<span>
@@ -260,7 +305,7 @@ export function AppUpdateNotification() {
</Button>
{isDownloaded ? (
<Button onClick={handleInstall}>
<Button onClick={handleInstall} disabled={showReadOnlyWarning}>
<RefreshCw className="mr-2 h-4 w-4" />
{t("dialogs:appUpdate.installAndRestart", "Install and Restart")}
</Button>
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Download, X, RefreshCw } from "lucide-react";
import { Download, X, RefreshCw, AlertTriangle } from "lucide-react";
import { Button } from "./ui/button";
import { cn } from "../lib/utils";
import type { AppUpdateAvailableEvent, AppUpdateProgress } from "../../shared/types";
@@ -25,6 +25,7 @@ export function UpdateBanner({ className }: UpdateBannerProps) {
const [downloadProgress, setDownloadProgress] = useState<AppUpdateProgress | null>(null);
const [isDownloaded, setIsDownloaded] = useState(false);
const [downloadError, setDownloadError] = useState<string | null>(null);
const [showReadOnlyWarning, setShowReadOnlyWarning] = useState(false);
// Ref to track current version for stable callbacks
const currentVersionRef = useRef<string | null>(null);
@@ -32,18 +33,16 @@ export function UpdateBanner({ className }: UpdateBannerProps) {
// Check for updates
const checkForUpdate = useCallback(async () => {
try {
if (!window.electronAPI?.checkAppUpdate) {
return;
}
const result = await window.electronAPI.checkAppUpdate();
if (result.success && result.data) {
const newVersion = result.data.version;
// New update available - show banner (unless same version already dismissed)
if (currentVersionRef.current !== newVersion) {
setIsDismissed(false);
// Reset downloaded state when a newer version is found
// Reset stale state when a newer version is found
setIsDownloaded(false);
setShowReadOnlyWarning(false);
setDownloadError(null);
currentVersionRef.current = newVersion;
}
setUpdateInfo({
@@ -61,9 +60,6 @@ export function UpdateBanner({ className }: UpdateBannerProps) {
useEffect(() => {
const checkDownloaded = async () => {
try {
if (!window.electronAPI?.getDownloadedAppUpdate) {
return;
}
const result = await window.electronAPI.getDownloadedAppUpdate();
if (result.success && result.data) {
currentVersionRef.current = result.data.version;
@@ -94,10 +90,6 @@ export function UpdateBanner({ className }: UpdateBannerProps) {
// Listen for push notifications about updates
useEffect(() => {
if (!window.electronAPI?.onAppUpdateAvailable) {
return;
}
const cleanup = window.electronAPI.onAppUpdateAvailable((info) => {
// New update notification - reset dismiss state if new version
if (currentVersionRef.current !== info.version) {
@@ -109,6 +101,7 @@ export function UpdateBanner({ className }: UpdateBannerProps) {
setIsDownloaded(false);
setDownloadProgress(null);
setDownloadError(null);
setShowReadOnlyWarning(false);
});
return cleanup;
@@ -116,10 +109,6 @@ export function UpdateBanner({ className }: UpdateBannerProps) {
// Listen for download progress
useEffect(() => {
if (!window.electronAPI?.onAppUpdateProgress) {
return;
}
const cleanup = window.electronAPI.onAppUpdateProgress((progress) => {
setDownloadProgress(progress);
});
@@ -129,14 +118,32 @@ export function UpdateBanner({ className }: UpdateBannerProps) {
// Listen for download completed
useEffect(() => {
if (!window.electronAPI?.onAppUpdateDownloaded) {
return;
}
const cleanup = window.electronAPI.onAppUpdateDownloaded(() => {
setIsDownloading(false);
setIsDownloaded(true);
setDownloadProgress(null);
setDownloadError(null);
setShowReadOnlyWarning(false);
});
return cleanup;
}, []);
// Listen for update errors (e.g., install failures)
useEffect(() => {
const cleanup = window.electronAPI.onAppUpdateError((error) => {
setDownloadError(error.message);
setIsDownloading(false);
setDownloadProgress(null);
});
return cleanup;
}, []);
// Listen for read-only volume warning (when trying to install from DMG on macOS)
useEffect(() => {
const cleanup = window.electronAPI.onAppUpdateReadOnlyVolume(() => {
setShowReadOnlyWarning(true);
});
return cleanup;
@@ -146,7 +153,7 @@ export function UpdateBanner({ className }: UpdateBannerProps) {
const handleUpdate = async () => {
if (isDownloaded) {
// Already downloaded - just install
window.electronAPI?.installAppUpdate?.();
window.electronAPI.installAppUpdate();
return;
}
@@ -155,11 +162,6 @@ export function UpdateBanner({ className }: UpdateBannerProps) {
setDownloadError(null);
try {
if (!window.electronAPI?.downloadAppUpdate) {
setDownloadError(t("navigation:updateBanner.downloadError"));
setIsDownloading(false);
return;
}
const result = await window.electronAPI.downloadAppUpdate();
if (!result.success) {
setDownloadError(result.error || t("navigation:updateBanner.downloadError"));
@@ -221,7 +223,7 @@ export function UpdateBanner({ className }: UpdateBannerProps) {
<div className="h-1 w-full bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-info transition-all duration-300"
style={{ width: `${downloadProgress.percent}%` }}
style={{ width: `${Math.min(100, Math.max(0, downloadProgress.percent))}%` }}
/>
</div>
</div>
@@ -232,12 +234,20 @@ export function UpdateBanner({ className }: UpdateBannerProps) {
<p className="text-[10px] text-destructive mb-2">{downloadError}</p>
)}
{/* Read-only volume warning (DMG install on macOS) */}
{showReadOnlyWarning && (
<div className="flex items-start gap-2 text-[10px] text-warning bg-warning/10 border border-warning/30 rounded p-2 mb-2">
<AlertTriangle className="h-3 w-3 shrink-0 mt-0.5" />
<span>{t("navigation:updateBanner.readOnlyVolumeWarning", "Move to Applications folder to update")}</span>
</div>
)}
{/* Action button */}
<Button
size="sm"
className="w-full h-7 text-xs gap-1.5"
onClick={handleUpdate}
disabled={isDownloading}
disabled={isDownloading || showReadOnlyWarning}
>
{isDownloading ? (
<>
@@ -1,4 +1,4 @@
import { useState, useEffect, useMemo } from 'react';
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
RefreshCw,
@@ -6,8 +6,14 @@ import {
Download,
Sparkles,
ArrowDownToLine,
X
X,
AlertTriangle,
AlertCircle
} from 'lucide-react';
import ReactMarkdown, { type Components } from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
import { Button } from '../ui/button';
import { Label } from '../ui/label';
import { Switch } from '../ui/switch';
@@ -22,39 +28,38 @@ import type {
} from '../../../shared/types';
/**
* Simple markdown renderer for release notes
* Handles: headers, bold, lists, line breaks
* Release notes renderer that handles both HTML and markdown input.
* GitHub release notes come as HTML, so we detect and handle both formats.
* Uses ReactMarkdown with rehype-sanitize to prevent XSS attacks.
*/
function ReleaseNotesRenderer({ markdown }: { markdown: string }) {
const html = useMemo(() => {
const result = markdown
// Escape HTML
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// Headers (### Header -> <h3>)
.replace(/^### (.+)$/gm, '<h4 class="text-sm font-semibold text-foreground mt-3 mb-1.5 first:mt-0">$1</h4>')
.replace(/^## (.+)$/gm, '<h3 class="text-sm font-semibold text-foreground mt-3 mb-1.5 first:mt-0">$1</h3>')
// Bold (**text** -> <strong>)
.replace(/\*\*([^*]+)\*\*/g, '<strong class="text-foreground font-medium">$1</strong>')
// Inline code (`code` -> <code>)
.replace(/`([^`]+)`/g, '<code class="px-1 py-0.5 bg-muted rounded text-xs">$1</code>')
// List items (- item -> <li>)
.replace(/^- (.+)$/gm, '<li class="ml-4 text-muted-foreground before:content-[\'•\'] before:mr-2 before:text-muted-foreground/60">$1</li>')
// Wrap consecutive list items
.replace(/(<li[^>]*>.*?<\/li>\n?)+/g, '<ul class="space-y-1 my-2">$&</ul>')
// Line breaks for remaining lines
.replace(/\n\n/g, '<div class="h-2"></div>')
.replace(/\n/g, '<br/>');
return result;
}, [markdown]);
/** Safe link component that opens external URLs in the default browser */
const safeMarkdownComponents: Components = {
a: ({ href, children, ...props }) => {
const isExternal = href?.startsWith('http://') || href?.startsWith('https://');
return (
<a
href={href}
{...props}
{...(isExternal && { target: '_blank', rel: 'noopener noreferrer' })}
className="text-primary hover:underline"
>
{children}
</a>
);
}
};
function ReleaseNotesRenderer({ content }: { content: string }) {
return (
<div
className="text-sm text-muted-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: html }}
/>
<div className="text-sm text-muted-foreground leading-relaxed prose prose-sm dark:prose-invert max-w-none [&_ul]:ml-4 [&_ol]:ml-4">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeSanitize]}
components={safeMarkdownComponents}
>
{content}
</ReactMarkdown>
</div>
);
}
@@ -79,6 +84,10 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version
const [isAppUpdateDownloaded, setIsAppUpdateDownloaded] = useState(false);
// Stable downgrade state (shown when user turns off beta while on prerelease)
const [stableDowngradeInfo, setStableDowngradeInfo] = useState<AppUpdateInfo | null>(null);
// Read-only volume warning (shown when trying to install from DMG)
const [showReadOnlyWarning, setShowReadOnlyWarning] = useState(false);
// General update error state
const [appUpdateError, setAppUpdateError] = useState<string | null>(null);
// Check for updates on mount, including any already-downloaded updates
useEffect(() => {
@@ -141,6 +150,8 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version
const cleanupAvailable = window.electronAPI.onAppUpdateAvailable((info) => {
setAppUpdateInfo(info);
setIsCheckingAppUpdate(false);
setShowReadOnlyWarning(false);
setAppUpdateError(null);
});
const cleanupDownloaded = window.electronAPI.onAppUpdateDownloaded((info) => {
@@ -150,6 +161,9 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version
setAppDownloadProgress(null);
// Clear downgrade info if any update downloaded
setStableDowngradeInfo(null);
// Reset read-only warning when a new update is downloaded
setShowReadOnlyWarning(false);
setAppUpdateError(null);
});
const cleanupProgress = window.electronAPI.onAppUpdateProgress((progress) => {
@@ -161,11 +175,25 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version
setStableDowngradeInfo(info);
});
// Listen for read-only volume warning (when trying to install from DMG)
const cleanupReadOnlyVolume = window.electronAPI.onAppUpdateReadOnlyVolume(() => {
setShowReadOnlyWarning(true);
});
// Listen for update errors (e.g., install failures)
const cleanupError = window.electronAPI.onAppUpdateError((error) => {
setAppUpdateError(error.message);
setIsDownloadingAppUpdate(false);
setAppDownloadProgress(null);
});
return () => {
cleanupAvailable();
cleanupDownloaded();
cleanupProgress();
cleanupStableDowngrade();
cleanupReadOnlyVolume();
cleanupError();
};
}, []);
@@ -188,15 +216,17 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version
const handleDownloadAppUpdate = async () => {
setIsDownloadingAppUpdate(true);
setAppUpdateError(null);
try {
const result = await window.electronAPI.downloadAppUpdate();
if (!result.success) {
console.error('Failed to download app update:', result.error);
setAppUpdateError(result.error || t('updates.downloadError'));
setIsDownloadingAppUpdate(false);
}
// Note: Success case is handled by the onAppUpdateDownloaded event listener
} catch (err) {
console.error('Failed to download app update:', err);
setAppUpdateError(t('updates.downloadError'));
setIsDownloadingAppUpdate(false);
}
};
@@ -207,16 +237,18 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version
const handleDownloadStableVersion = async () => {
setIsDownloadingAppUpdate(true);
setAppUpdateError(null);
try {
// Use dedicated stable download API with allowDowngrade enabled
const result = await window.electronAPI.downloadStableUpdate();
if (!result.success) {
console.error('Failed to download stable version:', result.error);
setAppUpdateError(result.error || t('updates.downloadError'));
setIsDownloadingAppUpdate(false);
}
// Note: Success case is handled by the onAppUpdateDownloaded event listener
} catch (err) {
console.error('Failed to download stable version:', err);
setAppUpdateError(t('updates.downloadError'));
setIsDownloadingAppUpdate(false);
}
};
@@ -304,7 +336,7 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version
{/* Release Notes */}
{appUpdateInfo?.releaseNotes && (
<div className="bg-background rounded-lg p-4 max-h-48 overflow-y-auto border border-border/50">
<ReleaseNotesRenderer markdown={appUpdateInfo.releaseNotes} />
<ReleaseNotesRenderer content={appUpdateInfo.releaseNotes} />
</div>
)}
@@ -324,18 +356,37 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version
</div>
)}
{/* Update Error */}
{appUpdateError && (
<div className="flex items-center gap-3 text-sm text-destructive bg-destructive/10 border border-destructive/30 rounded-lg p-3">
<AlertCircle className="h-5 w-5 shrink-0" />
<span>{appUpdateError}</span>
</div>
)}
{/* Downloaded Success */}
{isAppUpdateDownloaded && (
{isAppUpdateDownloaded && !showReadOnlyWarning && (
<div className="flex items-center gap-3 text-sm text-success bg-success/10 border border-success/30 rounded-lg p-3">
<CheckCircle2 className="h-5 w-5 shrink-0" />
<span>{t('updates.updateDownloaded')}</span>
</div>
)}
{/* Read-Only Volume Warning */}
{showReadOnlyWarning && (
<div className="flex items-start gap-3 text-sm text-warning bg-warning/10 border border-warning/30 rounded-lg p-3">
<AlertTriangle className="h-5 w-5 shrink-0 mt-0.5" />
<div className="space-y-1">
<p className="font-medium text-warning">{t('updates.readOnlyVolumeTitle')}</p>
<p className="text-muted-foreground">{t('updates.readOnlyVolumeDescription')}</p>
</div>
</div>
)}
{/* Action Buttons */}
<div className="flex gap-3">
{isAppUpdateDownloaded ? (
<Button onClick={handleInstallAppUpdate}>
<Button onClick={handleInstallAppUpdate} disabled={showReadOnlyWarning}>
<RefreshCw className="mr-2 h-4 w-4" />
{t('updates.installAndRestart')}
</Button>
@@ -445,7 +496,7 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version
{/* Release Notes */}
{stableDowngradeInfo.releaseNotes && (
<div className="bg-background rounded-lg p-4 max-h-48 overflow-y-auto border border-border/50">
<ReleaseNotesRenderer markdown={stableDowngradeInfo.releaseNotes} />
<ReleaseNotesRenderer content={stableDowngradeInfo.releaseNotes} />
</div>
)}
@@ -54,5 +54,7 @@ export const settingsMock = {
onAppUpdateAvailable: () => () => {},
onAppUpdateDownloaded: () => () => {},
onAppUpdateProgress: () => () => {},
onAppUpdateStableDowngrade: () => () => {}
onAppUpdateStableDowngrade: () => () => {},
onAppUpdateReadOnlyVolume: () => () => {},
onAppUpdateError: () => () => {}
};
@@ -524,6 +524,7 @@ export const IPC_CHANNELS = {
APP_UPDATE_PROGRESS: 'app-update:progress',
APP_UPDATE_ERROR: 'app-update:error',
APP_UPDATE_STABLE_DOWNGRADE: 'app-update:stable-downgrade', // Stable version available for downgrade from beta
APP_UPDATE_READONLY_VOLUME: 'app-update:readonly-volume', // App running from read-only volume (DMG), needs to be moved
// Release operations
RELEASE_SUGGEST_VERSION: 'release:suggestVersion',
@@ -162,7 +162,9 @@
"updateDownloaded": "Update downloaded successfully! Click Install to restart and apply the update.",
"downloadError": "Failed to download update",
"claudeCodeChangelog": "View Claude Code Changelog",
"claudeCodeChangelogAriaLabel": "View Claude Code Changelog (opens in new window)"
"claudeCodeChangelogAriaLabel": "View Claude Code Changelog (opens in new window)",
"readOnlyVolumeTitle": "Cannot install from disk image",
"readOnlyVolumeDescription": "Please move Auto Claude to your Applications folder before updating."
},
"versionWarning": {
"title": "Action Required",
@@ -39,7 +39,8 @@
"installAndRestart": "Install and Restart",
"downloading": "Downloading...",
"dismiss": "Dismiss",
"downloadError": "Failed to download update"
"downloadError": "Failed to download update",
"readOnlyVolumeWarning": "Move to Applications folder to update"
},
"claudeCode": {
"checking": "Checking Claude Code...",
@@ -302,7 +302,10 @@
"stableDowngradeAvailable": "Stable Version Available",
"stableDowngradeDescription": "You're currently on a beta version. Since you've disabled beta updates, you can switch to the latest stable release.",
"stableVersion": "Stable Version",
"downloadStableVersion": "Download Stable Version"
"downloadStableVersion": "Download Stable Version",
"readOnlyVolumeTitle": "Cannot Install from Disk Image",
"readOnlyVolumeDescription": "Auto Claude is running from a read-only disk image (DMG). Please drag the app to your Applications folder and relaunch it from there to install updates.",
"downloadError": "Failed to download update"
},
"notifications": {
"title": "Notifications",
@@ -162,7 +162,9 @@
"updateDownloaded": "Mise à jour téléchargée avec succès ! Cliquez sur Installer pour redémarrer et appliquer la mise à jour.",
"downloadError": "Échec du téléchargement de la mise à jour",
"claudeCodeChangelog": "Voir le journal des modifications Claude Code",
"claudeCodeChangelogAriaLabel": "Voir le journal des modifications Claude Code (s'ouvre dans une nouvelle fenêtre)"
"claudeCodeChangelogAriaLabel": "Voir le journal des modifications Claude Code (s'ouvre dans une nouvelle fenêtre)",
"readOnlyVolumeTitle": "Impossible d'installer depuis une image disque",
"readOnlyVolumeDescription": "Veuillez déplacer Auto Claude dans votre dossier Applications avant de mettre à jour."
},
"versionWarning": {
"title": "Action requise",
@@ -39,7 +39,8 @@
"installAndRestart": "Installer et redémarrer",
"downloading": "Téléchargement...",
"dismiss": "Ignorer",
"downloadError": "Échec du téléchargement de la mise à jour"
"downloadError": "Échec du téléchargement de la mise à jour",
"readOnlyVolumeWarning": "Déplacez l'app dans le dossier Applications pour mettre à jour"
},
"claudeCode": {
"checking": "Vérification de Claude Code...",
@@ -302,7 +302,10 @@
"stableDowngradeAvailable": "Version stable disponible",
"stableDowngradeDescription": "Vous êtes actuellement sur une version bêta. Comme vous avez désactivé les mises à jour bêta, vous pouvez passer à la dernière version stable.",
"stableVersion": "Version stable",
"downloadStableVersion": "Télécharger la version stable"
"downloadStableVersion": "Télécharger la version stable",
"readOnlyVolumeTitle": "Impossible d'installer depuis l'image disque",
"readOnlyVolumeDescription": "Auto Claude s'exécute depuis une image disque en lecture seule (DMG). Veuillez glisser l'application dans votre dossier Applications et la relancer depuis cet emplacement pour installer les mises à jour.",
"downloadError": "Échec du téléchargement de la mise à jour"
},
"notifications": {
"title": "Notifications",
@@ -22,4 +22,10 @@ export interface AppUpdateAvailableEvent {
export interface AppUpdateDownloadedEvent {
version: string;
releaseNotes?: string;
releaseDate?: string;
}
export interface AppUpdateErrorEvent {
message: string;
}
+7 -1
View File
@@ -74,7 +74,7 @@ import type {
TerminalProfileChangedEvent
} from './agent';
import type { AppSettings, SourceEnvConfig, SourceEnvCheckResult } from './settings';
import type { AppUpdateInfo, AppUpdateProgress, AppUpdateAvailableEvent, AppUpdateDownloadedEvent } from './app-update';
import type { AppUpdateInfo, AppUpdateProgress, AppUpdateAvailableEvent, AppUpdateDownloadedEvent, AppUpdateErrorEvent } from './app-update';
import type {
ChangelogTask,
TaskSpecContent,
@@ -710,6 +710,12 @@ export interface ElectronAPI {
onAppUpdateStableDowngrade: (
callback: (info: AppUpdateInfo) => void
) => () => void;
onAppUpdateReadOnlyVolume: (
callback: (info: { appPath: string }) => void
) => () => void;
onAppUpdateError: (
callback: (error: AppUpdateErrorEvent) => void
) => () => void;
// Shell operations
openExternal: (url: string) => Promise<void>;
+190 -35
View File
@@ -1,12 +1,12 @@
{
"name": "auto-claude",
"version": "2.7.6-beta.1",
"version": "2.7.6-beta.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "auto-claude",
"version": "2.7.6-beta.1",
"version": "2.7.6-beta.2",
"license": "AGPL-3.0",
"workspaces": [
"apps/*",
@@ -25,7 +25,7 @@
},
"apps/frontend": {
"name": "auto-claude-ui",
"version": "2.7.6-beta.1",
"version": "2.7.4",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@@ -74,6 +74,8 @@
"react-i18next": "^16.5.0",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^4.2.0",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"semver": "^7.7.3",
"tailwind-merge": "^3.4.0",
@@ -260,7 +262,6 @@
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.6",
@@ -825,7 +826,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -869,7 +869,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -909,7 +908,6 @@
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
@@ -2196,7 +2194,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -2218,7 +2215,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.4.0.tgz",
"integrity": "sha512-jn0phJ+hU7ZuvaoZE/8/Euw3gvHJrn2yi+kXrymwObEPVPjtwCmkvXDRQCWli+fCTTF/aSOtXaLr7CLIvv3LQg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
@@ -2231,7 +2227,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz",
"integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
@@ -2247,7 +2242,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.208.0.tgz",
"integrity": "sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/api-logs": "0.208.0",
"import-in-the-middle": "^2.0.0",
@@ -2650,7 +2644,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz",
"integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.4.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
@@ -2667,7 +2660,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.4.0.tgz",
"integrity": "sha512-WH0xXkz/OHORDLKqaxcUZS0X+t1s7gGlumr2ebiEgNZQl2b0upK2cdoD0tatf7l8iP74woGJ/Kmxe82jdvcWRw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.4.0",
"@opentelemetry/resources": "2.4.0",
@@ -2685,7 +2677,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.39.0.tgz",
"integrity": "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=14"
}
@@ -4880,7 +4871,6 @@
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -5183,7 +5173,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz",
"integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -5194,7 +5183,6 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -5464,7 +5452,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5497,7 +5484,6 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -5968,7 +5954,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -6939,7 +6924,6 @@
"integrity": "sha512-ce4Ogns4VMeisIuCSK0C62umG0lFy012jd8LMZ6w/veHUeX4fqfDrGe+HTWALAEwK6JwKP+dhPvizhArSOsFbg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"app-builder-lib": "26.4.0",
"builder-util": "26.3.4",
@@ -7097,7 +7081,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@electron/get": "^2.0.0",
"@types/node": "^24.9.0",
@@ -7382,7 +7365,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
@@ -8137,6 +8119,91 @@
"node": ">= 0.4"
}
},
"node_modules/hast-util-from-parse5": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz",
"integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
"devlop": "^1.0.0",
"hastscript": "^9.0.0",
"property-information": "^7.0.0",
"vfile": "^6.0.0",
"vfile-location": "^5.0.0",
"web-namespaces": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-parse-selector": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
"integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-raw": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz",
"integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
"@ungap/structured-clone": "^1.0.0",
"hast-util-from-parse5": "^8.0.0",
"hast-util-to-parse5": "^8.0.0",
"html-void-elements": "^3.0.0",
"mdast-util-to-hast": "^13.0.0",
"parse5": "^7.0.0",
"unist-util-position": "^5.0.0",
"unist-util-visit": "^5.0.0",
"vfile": "^6.0.0",
"web-namespaces": "^2.0.0",
"zwitch": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-raw/node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/hast-util-sanitize": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz",
"integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@ungap/structured-clone": "^1.0.0",
"unist-util-position": "^5.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-to-jsx-runtime": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
@@ -8164,6 +8231,25 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-to-parse5": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz",
"integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"comma-separated-tokens": "^2.0.0",
"devlop": "^1.0.0",
"property-information": "^7.0.0",
"space-separated-tokens": "^2.0.0",
"web-namespaces": "^2.0.0",
"zwitch": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-whitespace": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
@@ -8177,6 +8263,23 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/hastscript": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
"integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"comma-separated-tokens": "^2.0.0",
"hast-util-parse-selector": "^4.0.0",
"property-information": "^7.0.0",
"space-separated-tokens": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hosted-git-info": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
@@ -8242,6 +8345,16 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/html-void-elements": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
"integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
@@ -8326,7 +8439,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4"
},
@@ -8653,7 +8765,6 @@
"integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@acemir/cssom": "^0.9.28",
"@asamuzakjp/dom-selector": "^6.7.6",
@@ -11140,7 +11251,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -11331,7 +11441,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -11341,7 +11450,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -11554,6 +11662,35 @@
"node": ">=8"
}
},
"node_modules/rehype-raw": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz",
"integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"hast-util-raw": "^9.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/rehype-sanitize": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz",
"integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"hast-util-sanitize": "^5.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-gfm": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
@@ -12308,8 +12445,7 @@
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.0",
@@ -12488,7 +12624,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -12647,7 +12782,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -12934,6 +13068,20 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/vfile-location": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz",
"integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/vfile-message": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
@@ -12954,7 +13102,6 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -13547,7 +13694,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -13678,6 +13824,16 @@
"defaults": "^1.0.3"
}
},
"node_modules/web-namespaces": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
"integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/webidl-conversions": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
@@ -14002,7 +14158,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}