From ff91a1af0f0277611033d34b1f33deae6f96d29a Mon Sep 17 00:00:00 2001 From: Andy <119136210+AndyMik90@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:15:42 +0100 Subject: [PATCH] fix: improve auto-updater for beta releases and DMG installs (#1681) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 --------- Co-authored-by: Claude Opus 4.5 Co-authored-by: Test User --- apps/frontend/electron.vite.config.ts | 4 + apps/frontend/package.json | 2 + apps/frontend/src/main/app-updater.ts | 68 +++++- .../main/ipc-handlers/app-update-handlers.ts | 4 + .../src/preload/api/app-update-api.ts | 23 +- .../components/AppUpdateNotification.tsx | 55 ++++- .../src/renderer/components/UpdateBanner.tsx | 68 +++--- .../components/settings/AdvancedSettings.tsx | 127 +++++++--- .../src/renderer/lib/mocks/settings-mock.ts | 4 +- apps/frontend/src/shared/constants/ipc.ts | 1 + .../src/shared/i18n/locales/en/dialogs.json | 4 +- .../shared/i18n/locales/en/navigation.json | 3 +- .../src/shared/i18n/locales/en/settings.json | 5 +- .../src/shared/i18n/locales/fr/dialogs.json | 4 +- .../shared/i18n/locales/fr/navigation.json | 3 +- .../src/shared/i18n/locales/fr/settings.json | 5 +- apps/frontend/src/shared/types/app-update.ts | 6 + apps/frontend/src/shared/types/ipc.ts | 8 +- package-lock.json | 225 +++++++++++++++--- 19 files changed, 491 insertions(+), 128 deletions(-) diff --git a/apps/frontend/electron.vite.config.ts b/apps/frontend/electron.vite.config.ts index 0ff642bd..31919d9a 100644 --- a/apps/frontend/electron.vite.config.ts +++ b/apps/frontend/electron.vite.config.ts @@ -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. diff --git a/apps/frontend/package.json b/apps/frontend/package.json index af6dc068..6dfa1074 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -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", diff --git a/apps/frontend/src/main/app-updater.ts b/apps/frontend/src/main/app-updater.ts index e9f7d63b..7c172363 100644 --- a/apps/frontend/src/main/app-updater.ts +++ b/apps/frontend/src/main/app-updater.ts @@ -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 { } } +/** + * 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 { - 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 { - // 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)...'); diff --git a/apps/frontend/src/main/ipc-handlers/app-update-handlers.ts b/apps/frontend/src/main/ipc-handlers/app-update-handlers.ts index 33f4d6b1..04bd3949 100644 --- a/apps/frontend/src/main/ipc-handlers/app-update-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/app-update-handlers.ts @@ -95,6 +95,10 @@ export function registerAppUpdateHandlers(): void { IPC_CHANNELS.APP_UPDATE_INSTALL, async (): Promise => { 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) { diff --git a/apps/frontend/src/preload/api/app-update-api.ts b/apps/frontend/src/preload/api/app-update-api.ts index 489fa98b..23cebac8 100644 --- a/apps/frontend/src/preload/api/app-update-api.ts +++ b/apps/frontend/src/preload/api/app-update-api.ts @@ -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 => @@ -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) }); diff --git a/apps/frontend/src/renderer/components/AppUpdateNotification.tsx b/apps/frontend/src/renderer/components/AppUpdateNotification.tsx index 535f919e..6f007a25 100644 --- a/apps/frontend/src/renderer/components/AppUpdateNotification.tsx +++ b/apps/frontend/src/renderer/components/AppUpdateNotification.tsx @@ -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(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 && (
- + {updateInfo.releaseNotes}
@@ -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() {
)} + {/* Read-Only Volume Warning (DMG install on macOS) */} + {showReadOnlyWarning && ( +
+ +
+

+ {t("dialogs:appUpdate.readOnlyVolumeTitle", "Cannot install from disk image")} +

+

+ {t("dialogs:appUpdate.readOnlyVolumeDescription", "Please move Auto Claude to your Applications folder before updating.")} +

+
+
+ )} + {/* Downloaded Success */} - {isDownloaded && ( + {isDownloaded && !showReadOnlyWarning && (
@@ -260,7 +305,7 @@ export function AppUpdateNotification() { {isDownloaded ? ( - diff --git a/apps/frontend/src/renderer/components/UpdateBanner.tsx b/apps/frontend/src/renderer/components/UpdateBanner.tsx index 8323b464..973d370e 100644 --- a/apps/frontend/src/renderer/components/UpdateBanner.tsx +++ b/apps/frontend/src/renderer/components/UpdateBanner.tsx @@ -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(null); const [isDownloaded, setIsDownloaded] = useState(false); const [downloadError, setDownloadError] = useState(null); + const [showReadOnlyWarning, setShowReadOnlyWarning] = useState(false); // Ref to track current version for stable callbacks const currentVersionRef = useRef(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) {
@@ -232,12 +234,20 @@ export function UpdateBanner({ className }: UpdateBannerProps) {

{downloadError}

)} + {/* Read-only volume warning (DMG install on macOS) */} + {showReadOnlyWarning && ( +
+ + {t("navigation:updateBanner.readOnlyVolumeWarning", "Move to Applications folder to update")} +
+ )} + {/* Action button */} @@ -445,7 +496,7 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version {/* Release Notes */} {stableDowngradeInfo.releaseNotes && (
- +
)} diff --git a/apps/frontend/src/renderer/lib/mocks/settings-mock.ts b/apps/frontend/src/renderer/lib/mocks/settings-mock.ts index 35441190..e37a7669 100644 --- a/apps/frontend/src/renderer/lib/mocks/settings-mock.ts +++ b/apps/frontend/src/renderer/lib/mocks/settings-mock.ts @@ -54,5 +54,7 @@ export const settingsMock = { onAppUpdateAvailable: () => () => {}, onAppUpdateDownloaded: () => () => {}, onAppUpdateProgress: () => () => {}, - onAppUpdateStableDowngrade: () => () => {} + onAppUpdateStableDowngrade: () => () => {}, + onAppUpdateReadOnlyVolume: () => () => {}, + onAppUpdateError: () => () => {} }; diff --git a/apps/frontend/src/shared/constants/ipc.ts b/apps/frontend/src/shared/constants/ipc.ts index 19226749..a6487244 100644 --- a/apps/frontend/src/shared/constants/ipc.ts +++ b/apps/frontend/src/shared/constants/ipc.ts @@ -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', diff --git a/apps/frontend/src/shared/i18n/locales/en/dialogs.json b/apps/frontend/src/shared/i18n/locales/en/dialogs.json index a1c8b998..df7e4121 100644 --- a/apps/frontend/src/shared/i18n/locales/en/dialogs.json +++ b/apps/frontend/src/shared/i18n/locales/en/dialogs.json @@ -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", diff --git a/apps/frontend/src/shared/i18n/locales/en/navigation.json b/apps/frontend/src/shared/i18n/locales/en/navigation.json index 50483700..24d45f49 100644 --- a/apps/frontend/src/shared/i18n/locales/en/navigation.json +++ b/apps/frontend/src/shared/i18n/locales/en/navigation.json @@ -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...", diff --git a/apps/frontend/src/shared/i18n/locales/en/settings.json b/apps/frontend/src/shared/i18n/locales/en/settings.json index 6b86a52a..e3d608f6 100644 --- a/apps/frontend/src/shared/i18n/locales/en/settings.json +++ b/apps/frontend/src/shared/i18n/locales/en/settings.json @@ -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", diff --git a/apps/frontend/src/shared/i18n/locales/fr/dialogs.json b/apps/frontend/src/shared/i18n/locales/fr/dialogs.json index e3c15a84..7ca4115e 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/dialogs.json +++ b/apps/frontend/src/shared/i18n/locales/fr/dialogs.json @@ -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", diff --git a/apps/frontend/src/shared/i18n/locales/fr/navigation.json b/apps/frontend/src/shared/i18n/locales/fr/navigation.json index db779e10..653cb525 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/navigation.json +++ b/apps/frontend/src/shared/i18n/locales/fr/navigation.json @@ -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...", diff --git a/apps/frontend/src/shared/i18n/locales/fr/settings.json b/apps/frontend/src/shared/i18n/locales/fr/settings.json index fab4ab71..60505657 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/settings.json +++ b/apps/frontend/src/shared/i18n/locales/fr/settings.json @@ -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", diff --git a/apps/frontend/src/shared/types/app-update.ts b/apps/frontend/src/shared/types/app-update.ts index ccee20de..dcc01d80 100644 --- a/apps/frontend/src/shared/types/app-update.ts +++ b/apps/frontend/src/shared/types/app-update.ts @@ -22,4 +22,10 @@ export interface AppUpdateAvailableEvent { export interface AppUpdateDownloadedEvent { version: string; + releaseNotes?: string; + releaseDate?: string; +} + +export interface AppUpdateErrorEvent { + message: string; } diff --git a/apps/frontend/src/shared/types/ipc.ts b/apps/frontend/src/shared/types/ipc.ts index da3af2c4..990eec99 100644 --- a/apps/frontend/src/shared/types/ipc.ts +++ b/apps/frontend/src/shared/types/ipc.ts @@ -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; diff --git a/package-lock.json b/package-lock.json index 59e0f33a..ae263006 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" }