diff --git a/apps/desktop/src/main/index.mjs b/apps/desktop/src/main/index.mjs index 1499d00e..37983b12 100644 --- a/apps/desktop/src/main/index.mjs +++ b/apps/desktop/src/main/index.mjs @@ -23,7 +23,7 @@ import { isAllowedPrintPreviewUrl } from "./window-open-policy.mjs"; import { showWindow } from "./window-visibility.mjs"; import { trayIconPath } from "./tray-icon.mjs"; import { writeRichClipboard } from "./clipboard-write.mjs"; -import { scheduleMacLocalDataReset } from "./local-data-reset.mjs"; +import { LocalDataResetError, scheduleMacLocalDataReset } from "./local-data-reset.mjs"; import { buildDesktopDiagnosticIssueUrl, normalizeDesktopDiagnostic } from "./desktop-diagnostics.mjs"; import electronUpdater from "electron-updater"; @@ -828,9 +828,14 @@ app.whenReady().then(async () => { }); } catch (error) { await writeDiagnostic("local-data-reset.schedule-failed", { + code: error instanceof LocalDataResetError ? error.code : "unexpected", message: error instanceof Error ? error.message : String(error), + cause: error instanceof LocalDataResetError && error.cause instanceof Error ? error.cause.message : undefined, }); - throw error; + return { + scheduled: false, + errorCode: error instanceof LocalDataResetError ? error.code : "unexpected", + }; } // Only begin shutting down once the detached reset helper has definitely diff --git a/apps/desktop/src/main/local-data-reset.mjs b/apps/desktop/src/main/local-data-reset.mjs index 1dc689d5..a50cc1b6 100644 --- a/apps/desktop/src/main/local-data-reset.mjs +++ b/apps/desktop/src/main/local-data-reset.mjs @@ -22,6 +22,14 @@ if /bin/rm -rf -- "$user_data_dir"; then fi `; +export class LocalDataResetError extends Error { + constructor(code, message, options) { + super(message, options); + this.name = "LocalDataResetError"; + this.code = code; + } +} + export const managedUserDataDirectory = (userDataDirectory, appDataDirectory) => { const target = resolve(userDataDirectory); const appData = resolve(appDataDirectory); @@ -33,7 +41,10 @@ export const managedUserDataDirectory = (userDataDirectory, appDataDirectory) => isAbsolute(relativeTarget) || relativeTarget.includes(sep) ) { - throw new Error("EdgeEver local data must be a direct child of the application-data directory"); + throw new LocalDataResetError( + "unsafe-data-directory", + "EdgeEver local data must be a direct child of the application-data directory", + ); } return target; }; @@ -46,7 +57,10 @@ export const macApplicationBundlePath = (executablePath) => { if (parent === candidate) break; candidate = parent; } - throw new Error("EdgeEver must be running from a macOS application bundle"); + throw new LocalDataResetError( + "application-bundle-not-found", + "EdgeEver must be running from a macOS application bundle", + ); }; export const scheduleMacLocalDataReset = async ({ @@ -73,10 +87,18 @@ export const scheduleMacLocalDataReset = async ({ stdio: "ignore", }, ); - await new Promise((resolveSpawn, rejectSpawn) => { - helper.once("spawn", resolveSpawn); - helper.once("error", rejectSpawn); - }); + try { + await new Promise((resolveSpawn, rejectSpawn) => { + helper.once("spawn", resolveSpawn); + helper.once("error", rejectSpawn); + }); + } catch (error) { + throw new LocalDataResetError( + "helper-start-failed", + "EdgeEver could not start the local-data reset helper", + { cause: error }, + ); + } helper.unref(); return { applicationPath, target }; }; diff --git a/apps/desktop/src/main/local-data-reset.test.mjs b/apps/desktop/src/main/local-data-reset.test.mjs index 07e15de6..a61c7c80 100644 --- a/apps/desktop/src/main/local-data-reset.test.mjs +++ b/apps/desktop/src/main/local-data-reset.test.mjs @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test"; import { EventEmitter } from "node:events"; import { + LocalDataResetError, macApplicationBundlePath, managedUserDataDirectory, scheduleMacLocalDataReset, @@ -21,16 +22,28 @@ describe("desktop local data reset", () => { "/Users/example/Library/Application Support/EdgeEver/accounts", "/Users/example/Library/Application Support", )).toThrow(); - expect(() => managedUserDataDirectory( - "/Users/example/Documents", - "/Users/example/Library/Application Support", - )).toThrow(); + try { + managedUserDataDirectory( + "/Users/example/Documents", + "/Users/example/Library/Application Support", + ); + throw new Error("expected path validation to fail"); + } catch (error) { + expect(error).toBeInstanceOf(LocalDataResetError); + expect(error.code).toBe("unsafe-data-directory"); + } }); test("finds the enclosing macOS application bundle", () => { expect(macApplicationBundlePath("/Applications/EdgeEver.app/Contents/MacOS/EdgeEver")) .toBe("/Applications/EdgeEver.app"); - expect(() => macApplicationBundlePath("/usr/local/bin/edgeever")).toThrow(); + try { + macApplicationBundlePath("/usr/local/bin/edgeever"); + throw new Error("expected bundle validation to fail"); + } catch (error) { + expect(error).toBeInstanceOf(LocalDataResetError); + expect(error.code).toBe("application-bundle-not-found"); + } }); test("passes paths as shell arguments instead of interpolating them", async () => { @@ -81,6 +94,9 @@ describe("desktop local data reset", () => { userDataDirectory: "/Users/example/Library/Application Support/EdgeEver", }); - await expect(resultPromise).rejects.toThrow("spawn failed"); + const error = await resultPromise.catch((reason) => reason); + expect(error).toBeInstanceOf(LocalDataResetError); + expect(error.code).toBe("helper-start-failed"); + expect(error.cause?.message).toBe("spawn failed"); }); }); diff --git a/apps/web/src/components/settings/DesktopLocalDataCard.tsx b/apps/web/src/components/settings/DesktopLocalDataCard.tsx index 43447fd8..fa849b59 100644 --- a/apps/web/src/components/settings/DesktopLocalDataCard.tsx +++ b/apps/web/src/components/settings/DesktopLocalDataCard.tsx @@ -12,23 +12,34 @@ import { DialogTitle, } from "@/components/ui/dialog"; +const localDataResetErrorKeys: Record = { + "unsafe-data-directory": "localDataReset.errors.unsafeDataDirectory", + "application-bundle-not-found": "localDataReset.errors.applicationBundleNotFound", + "helper-start-failed": "localDataReset.errors.helperStartFailed", + unexpected: "localDataReset.errors.unexpected", +}; + export const DesktopLocalDataCard = () => { const { t } = useTranslation(); const bridge = window.edgeeverDesktop; const [confirmationOpen, setConfirmationOpen] = useState(false); const [isClearing, setIsClearing] = useState(false); - const [error, setError] = useState(false); + const [errorCode, setErrorCode] = useState(null); if (!bridge?.canClearLocalData) return null; const handleClear = async () => { setIsClearing(true); - setError(false); + setErrorCode(null); try { - await bridge.clearLocalData(); + const result = await bridge.clearLocalData(); + if (!result.scheduled) { + setIsClearing(false); + setErrorCode(result.errorCode); + } } catch { setIsClearing(false); - setError(true); + setErrorCode("unexpected"); } }; @@ -43,10 +54,10 @@ export const DesktopLocalDataCard = () => { {t("localDataReset.description")} - {error ? ( + {errorCode ? (

- {t("localDataReset.error")} + {t(localDataResetErrorKeys[errorCode])}

) : null}
@@ -55,7 +66,7 @@ export const DesktopLocalDataCard = () => { variant="danger" className="bg-rose-600 font-semibold text-white shadow-sm hover:bg-rose-700" onClick={() => { - setError(false); + setErrorCode(null); setConfirmationOpen(true); }} > diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts index d056be61..96c03a95 100644 --- a/apps/web/src/vite-env.d.ts +++ b/apps/web/src/vite-env.d.ts @@ -19,7 +19,10 @@ interface EdgeEverDesktopBridge { copyHtml(html: string, plainText: string): Promise; setSessionToken(value: string): Promise<{ stored: boolean }>; clearSessionToken(): Promise<{ stored: false }>; - clearLocalData(): Promise<{ scheduled: true }>; + clearLocalData(): Promise< + { scheduled: true } + | { scheduled: false; errorCode: DesktopLocalDataResetErrorCode } + >; recordRendererError(details: DesktopRendererErrorDetails): Promise<{ recorded: true }>; openRendererIssue(details: DesktopRendererErrorDetails): Promise<{ opened: true }>; sidecarStatus(): Promise<{ available: boolean; path: string; scope: string }>; @@ -37,6 +40,12 @@ interface EdgeEverDesktopBridge { onImportMarkdown(callback: (payload: { name: string; content: string }) => void): () => void; } +type DesktopLocalDataResetErrorCode = + | "unsafe-data-directory" + | "application-bundle-not-found" + | "helper-start-failed" + | "unexpected"; + interface DesktopRendererErrorDetails { kind: string; message?: string; diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts index 61d26044..577a1cdd 100644 --- a/packages/shared/src/i18n/en-US.ts +++ b/packages/shared/src/i18n/en-US.ts @@ -1206,7 +1206,12 @@ export const enUS = { warning: "This action cannot be undone. Any notes or changes that have not been synced will be permanently lost.", confirm: "Clear local data", clearing: "Clearing...", - error: "Local data could not be cleared. Please try again.", + errors: { + unsafeDataDirectory: "EdgeEver could not verify that the local data folder is safe to remove. Your data was not changed.", + applicationBundleNotFound: "EdgeEver could not identify the installed app needed to restart after clearing. Reinstall EdgeEver in Applications and try again.", + helperStartFailed: "EdgeEver could not start the macOS data-clearing helper. Your data was not changed. Restart the app and try again.", + unexpected: "An unexpected error occurred before local data could be cleared. Your data was not changed. Restart the app and try again.", + }, }, loginDevices: { title: "Signed-in devices", diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts index 443c68fe..7d1f7466 100644 --- a/packages/shared/src/i18n/zh-CN.ts +++ b/packages/shared/src/i18n/zh-CN.ts @@ -1206,7 +1206,12 @@ export const zhCN = { warning: "此操作无法撤销。尚未同步的笔记和更改将永久丢失。", confirm: "清除本地数据", clearing: "正在清除...", - error: "无法清除本地数据,请重试。", + errors: { + unsafeDataDirectory: "无法确认本地数据目录可以安全删除,尚未更改任何数据。", + applicationBundleNotFound: "无法识别清除后需要重新启动的已安装应用。请将 EdgeEver 重新安装到“应用程序”后再试。", + helperStartFailed: "无法启动 macOS 数据清理助手,尚未更改任何数据。请重新启动应用后再试。", + unexpected: "清除本地数据前发生意外错误,尚未更改任何数据。请重新启动应用后再试。", + }, }, loginDevices: { title: "登录设备",