From a2b99c0d8d594506483f8cc95d7d74291a254652 Mon Sep 17 00:00:00 2001 From: kipavy Date: Fri, 24 Jul 2026 09:43:49 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(plugins):=20update=20flow=20=E2=80=94?= =?UTF-8?q?=20detection,=20one-click=20update,=20permission=20re-consent?= =?UTF-8?q?=20(#52)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plugin manager had no update mechanism (uninstall+reinstall only) and no re-consent when a plugin's permissions or code change. installPlugin already overwrites + hash-verifies + reloads, so this adds the missing detection, affordance, and consent around it. - updates.ts: pure detection (compareSemver, availableUpdate by version OR versionless hash-diff, addedPermissions) + unit tests. - Update affordance in BOTH tabs: Installed shows v{old} → {new} + Update button; Browse swaps the badge for Update; Installed tab header shows a '• N updates' count. Catalog is fetched once on section mount. - Consent: first installs show a permission disclosure (gated by a new default-on 'Review permissions before installing plugins' setting); updates apply silently unless they request NEW permissions, which always prompts (non-skippable). Shared PluginPermissionModal for both. - marketplaceStore.fetchManifest for the pre-consent permission preview. - i18n en/fr/ru parity. --- .../sections/PluginPermissionModal.tsx | 103 +++++ .../settings/sections/PluginsSection.tsx | 209 +++++++-- src/i18n/locales/en/settings.json | 423 ++++++++++++++--- src/i18n/locales/fr/settings.json | 423 ++++++++++++++--- src/i18n/locales/ru/settings.json | 425 +++++++++++++++--- src/plugins/updates.test.ts | 111 +++++ src/plugins/updates.ts | 48 ++ src/stores/marketplaceStore.ts | 11 + src/stores/toggleSettingsStore.ts | 7 + 9 files changed, 1502 insertions(+), 258 deletions(-) create mode 100644 src/components/settings/sections/PluginPermissionModal.tsx create mode 100644 src/plugins/updates.test.ts create mode 100644 src/plugins/updates.ts diff --git a/src/components/settings/sections/PluginPermissionModal.tsx b/src/components/settings/sections/PluginPermissionModal.tsx new file mode 100644 index 00000000..0a646534 --- /dev/null +++ b/src/components/settings/sections/PluginPermissionModal.tsx @@ -0,0 +1,103 @@ +import { Icon } from "@iconify/react"; +import { useTranslation } from "react-i18next"; +import { Modal, ModalCard } from "@/components/shared/Modal"; + +interface Props { + mode: "install" | "update"; + pluginName: string; + /** All permissions the plugin will hold after this action. */ + permissions: string[]; + /** For updates: permissions newly requested by this version (subset of `permissions`). */ + addedPermissions?: string[]; + onConfirm: () => void; + onCancel: () => void; +} + +/** + * Review dialog shown before executing a plugin's code: + * - install: discloses all declared permissions (gated by the plugin-install-review setting). + * - update: a non-skippable gate shown only when a version requests NEW permissions; the added + * ones are emphasized, the rest listed for context. + */ +export function PluginPermissionModal({ + mode, + pluginName, + permissions, + addedPermissions = [], + onConfirm, + onCancel, +}: Props) { + const { t } = useTranslation(); + const added = new Set(addedPermissions); + const isUpdate = mode === "update"; + + return ( + + +
+
+ +
+

+ {isUpdate + ? t("settings.plugins.permissionModal.updateTitle", { name: pluginName }) + : t("settings.plugins.permissionModal.installTitle", { name: pluginName })} +

+
+ +

+ {isUpdate + ? t("settings.plugins.permissionModal.updateBody") + : t("settings.plugins.permissionModal.installBody")} +

+ + {permissions.length > 0 ? ( +
+ {isUpdate && ( +

+ {t("settings.plugins.permissionModal.newPermissions")} +

+ )} +
+ {permissions.map((perm) => { + const isNew = added.has(perm); + return ( + + {isNew && } + {perm} + + ); + })} +
+
+ ) : ( +

+ {t("settings.plugins.permissionModal.noPermissions")} +

+ )} + +
+ + +
+
+
+ ); +} diff --git a/src/components/settings/sections/PluginsSection.tsx b/src/components/settings/sections/PluginsSection.tsx index 440064d1..15b5c74d 100644 --- a/src/components/settings/sections/PluginsSection.tsx +++ b/src/components/settings/sections/PluginsSection.tsx @@ -8,6 +8,9 @@ import { useMarketplaceStore, type MarketplacePlugin } from "@/stores/marketplac import { useUIStore } from "@/stores/uiStore"; import { useNotificationStore } from "@/stores/notificationStore"; import { PluginHashMismatchError } from "@/plugins/integrity"; +import { availableUpdate, addedPermissions } from "@/plugins/updates"; +import { getToggle, useToggle } from "@/stores/toggleSettingsStore"; +import { PluginPermissionModal } from "./PluginPermissionModal"; import { BUNDLED_PLUGINS } from "@/plugins/bundled"; import { useFilterShortcut } from "@/components/shared/ToolbarViewControls"; import { setPluginActive, getLoadedPlugins, pluginStorageGet, pluginStorageSet } from "@/plugins/runtime"; @@ -145,13 +148,107 @@ function PluginConfigForm({ manifest }: { manifest: PluginManifest }) { ); } +// ─── Shared install/update flow ──────────────────────────────────────────── + +interface PendingReview { + mode: "install" | "update"; + plugin: MarketplacePlugin; + permissions: string[]; + addedPermissions: string[]; +} + +/** + * install/update with permission consent. First installs show a disclosure when the + * `plugin-install-review` setting is on; updates apply silently unless they request NEW + * permissions, in which case a non-skippable review modal is shown. Both paths run the + * authoritative, hash-verified `installPlugin`. + */ +function usePluginInstaller() { + const { t } = useTranslation(); + const installing = useMarketplaceStore((s) => s.installing); + const installPlugin = useMarketplaceStore((s) => s.installPlugin); + const fetchManifest = useMarketplaceStore((s) => s.fetchManifest); + const [preparing, setPreparing] = useState>(new Set()); + const [pending, setPending] = useState(null); + + const busy = new Set([...installing, ...preparing]); + + const notifyError = (e: unknown) => { + useNotificationStore.getState().addToast({ + pluginId: "system", + pluginName: "Voltius", + type: "toast", + severity: "error", + message: e instanceof PluginHashMismatchError + ? t("settings.plugins.install.integrityFailed") + : t("settings.plugins.install.failed"), + duration: 0, + }); + }; + + const runInstall = async (plugin: MarketplacePlugin) => { + try { await installPlugin(plugin); } catch (e) { notifyError(e); } + }; + + const withPreparing = async (id: string, fn: () => Promise) => { + setPreparing((s) => new Set([...s, id])); + try { await fn(); } finally { + setPreparing((s) => { const n = new Set(s); n.delete(id); return n; }); + } + }; + + const startInstall = (plugin: MarketplacePlugin) => { + if (!getToggle("plugin-install-review")) { void runInstall(plugin); return; } + void withPreparing(plugin.id, async () => { + try { + const manifest = await fetchManifest(plugin); + setPending({ mode: "install", plugin, permissions: manifest.permissions ?? [], addedPermissions: [] }); + } catch (e) { notifyError(e); } + }); + }; + + const startUpdate = (plugin: MarketplacePlugin, currentPermissions: string[]) => { + void withPreparing(plugin.id, async () => { + try { + const manifest = await fetchManifest(plugin); + const next = manifest.permissions ?? []; + const added = addedPermissions(currentPermissions, next); + if (added.length === 0) { await runInstall(plugin); return; } + setPending({ mode: "update", plugin, permissions: next, addedPermissions: added }); + } catch (e) { notifyError(e); } + }); + }; + + const confirm = () => { + if (!pending) return; + const { plugin } = pending; + setPending(null); + void runInstall(plugin); + }; + const cancel = () => setPending(null); + + const modal = pending ? ( + + ) : null; + + return { busy, startInstall, startUpdate, modal }; +} + // ─── Installed tab ───────────────────────────────────────────────────────── function InstalledTab() { const { t } = useTranslation(); const settingsPages = usePluginStore((s) => s.settingsPages); const { setEnabled, isEnabled } = usePluginRegistryStore(); - const { installedMeta, uninstallPlugin, reloadPlugin, scanLocal } = useMarketplaceStore(); + const { installedMeta, catalog, uninstallPlugin, reloadPlugin, scanLocal } = useMarketplaceStore(); + const { busy: updateBusy, startUpdate, modal: updateModal } = usePluginInstaller(); const [loadedIds, setLoadedIds] = useState>( () => new Set(getLoadedPlugins().map((m) => m.id)), ); @@ -366,6 +463,8 @@ function InstalledTab() { const isLoaded = loadedIds.has(meta.id); const isReloading = reloading.has(meta.id); const isUninstalling = uninstalling.has(meta.id); + const update = availableUpdate(meta, catalog); + const isUpdating = updateBusy.has(meta.id); return (
- v{meta.version} + {update ? `v${meta.version} → ${update.version}` : `v${meta.version}`} {meta.hash === null && meta.sourceId !== "local" && ( {manifest.description}

)}
+ {update && ( + + )} + ) : update ? ( + ) : ( )} @@ -730,6 +846,7 @@ function BrowseTab() { })} + {modal} ); } @@ -742,13 +859,27 @@ export default function PluginsSection() { const { t } = useTranslation(); const [tab, setTab] = useState("installed"); const installedMeta = useMarketplaceStore((s) => s.installedMeta); + const catalog = useMarketplaceStore((s) => s.catalog); + const catalogLoading = useMarketplaceStore((s) => s.catalogLoading); + const fetchCatalog = useMarketplaceStore((s) => s.fetchCatalog); const isAndroid = useIsAndroid(); const totalCount = visiblePlugins(BUNDLED_PLUGINS, isAndroid).length + installedMeta.length; - const tabLabel = (tabKey: Tab) => - tabKey === "installed" - ? t("settings.plugins.tabs.installed", { count: totalCount }) - : t("settings.plugins.tabs.browse"); + // Fetch the catalog once on mount so update detection works before visiting Browse. + useEffect(() => { + if (catalog.length === 0 && !catalogLoading) void fetchCatalog(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const updateCount = installedMeta.filter((m) => availableUpdate(m, catalog)).length; + + const tabLabel = (tabKey: Tab) => { + if (tabKey === "browse") return t("settings.plugins.tabs.browse"); + const base = t("settings.plugins.tabs.installed", { count: totalCount }); + return updateCount > 0 + ? `${base} ${t("settings.plugins.tabs.updateCount", { count: updateCount })}` + : base; + }; return (
diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index b9dba0f8..e7fe5437 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -22,7 +22,9 @@ }, "appearance": { "interface": "Interface", - "language": { "title": "Language" }, + "language": { + "title": "Language" + }, "colorTheme": "Color Theme", "import": "Import", "importTitle": "Import theme(s)", @@ -61,18 +63,119 @@ } }, "nav": { - "appearance": { "label": "Appearance", "keywords": ["theme", "color", "font", "ui"] }, - "account": { "label": "Account", "keywords": ["profile", "login", "auth"] }, - "sync": { "label": "Sync", "keywords": ["backup", "cloud", "gist", "sync", "pro"] }, - "vaults": { "label": "Vaults", "keywords": ["secret", "password", "storage", "keyring"] }, - "plugins": { "label": "Plugins", "keywords": ["extensions", "addons", "marketplace"] }, - "terminal": { "label": "Terminal", "keywords": ["scrollback", "paste", "bracketed", "copy", "minimap", "buffer"] }, - "sftp": { "label": "SFTP", "keywords": ["file", "transfer", "ftp", "files"] }, - "portForwarding": { "label": "Port Forwarding", "keywords": ["tunnel", "forward", "ports", "ssh", "auto"] }, - "hosts": { "label": "Hosts", "keywords": ["ping", "reachability", "connectivity", "status"] }, - "shortcuts": { "label": "Shortcuts", "keywords": ["keybind", "hotkey", "shortcut", "rebind", "keyboard"] }, - "diagnostics": { "label": "Diagnostics", "keywords": ["diagnostics", "bug", "report", "log", "debug", "issue"] }, - "about": { "label": "About", "keywords": ["version", "update", "release", "changelog"] } + "appearance": { + "label": "Appearance", + "keywords": [ + "theme", + "color", + "font", + "ui" + ] + }, + "account": { + "label": "Account", + "keywords": [ + "profile", + "login", + "auth" + ] + }, + "sync": { + "label": "Sync", + "keywords": [ + "backup", + "cloud", + "gist", + "sync", + "pro" + ] + }, + "vaults": { + "label": "Vaults", + "keywords": [ + "secret", + "password", + "storage", + "keyring" + ] + }, + "plugins": { + "label": "Plugins", + "keywords": [ + "extensions", + "addons", + "marketplace" + ] + }, + "terminal": { + "label": "Terminal", + "keywords": [ + "scrollback", + "paste", + "bracketed", + "copy", + "minimap", + "buffer" + ] + }, + "sftp": { + "label": "SFTP", + "keywords": [ + "file", + "transfer", + "ftp", + "files" + ] + }, + "portForwarding": { + "label": "Port Forwarding", + "keywords": [ + "tunnel", + "forward", + "ports", + "ssh", + "auto" + ] + }, + "hosts": { + "label": "Hosts", + "keywords": [ + "ping", + "reachability", + "connectivity", + "status" + ] + }, + "shortcuts": { + "label": "Shortcuts", + "keywords": [ + "keybind", + "hotkey", + "shortcut", + "rebind", + "keyboard" + ] + }, + "diagnostics": { + "label": "Diagnostics", + "keywords": [ + "diagnostics", + "bug", + "report", + "log", + "debug", + "issue" + ] + }, + "about": { + "label": "About", + "keywords": [ + "version", + "update", + "release", + "changelog" + ] + } }, "chrome": { "title": "Settings", @@ -316,11 +419,26 @@ "prefsFooter": "Disabled types won't trigger automatic syncs when changed. Individual objects can also be excluded via their edit panel.", "quickToggleLabel": "Sync {{label}}", "objectType": { - "connection": { "label": "Hosts", "sub": "SSH connections" }, - "identity": { "label": "Identities", "sub": "Usernames and credentials" }, - "key": { "label": "SSH Keys", "sub": "Key pairs stored in keychain" }, - "folder": { "label": "Folders", "sub": "Folder structure for organizing objects" }, - "port-forwarding-rule": { "label": "Port Forwarding", "sub": "Saved tunnel rules" } + "connection": { + "label": "Hosts", + "sub": "SSH connections" + }, + "identity": { + "label": "Identities", + "sub": "Usernames and credentials" + }, + "key": { + "label": "SSH Keys", + "sub": "Key pairs stored in keychain" + }, + "folder": { + "label": "Folders", + "sub": "Folder structure for organizing objects" + }, + "port-forwarding-rule": { + "label": "Port Forwarding", + "sub": "Saved tunnel rules" + } } }, "vaults": { @@ -432,23 +550,74 @@ "terminalSessions": "Terminal Sessions" }, "perm": { - "VIEW_SECRETS": { "label": "View secrets", "desc": "See passwords and private keys in plain text" }, - "COPY_SECRETS": { "label": "Copy secrets", "desc": "Copy passwords and keys to clipboard" }, - "CONNECT": { "label": "Connect", "desc": "Launch SSH connections" }, - "EDIT_CONNECTIONS": { "label": "Edit connections", "desc": "Create, modify, and delete connections" }, - "EDIT_IDENTITIES": { "label": "Edit identities", "desc": "Create, modify, and delete SSH identities" }, - "EDIT_KEYS": { "label": "Edit keys", "desc": "Create, modify, and delete SSH keys" }, - "EDIT_SNIPPETS": { "label": "Edit snippets", "desc": "Create, modify, and delete command snippets" }, - "EDIT_FOLDERS": { "label": "Edit folders", "desc": "Manage folder structure" }, - "VIEW_AUDIT_LOG": { "label": "View audit log", "desc": "Read the activity audit log" }, - "INVITE_MEMBERS": { "label": "Invite members", "desc": "Invite new members to the vault" }, - "MANAGE_MEMBERS": { "label": "Manage members", "desc": "Assign roles and remove members" }, - "CREATE_CUSTOM_ROLES": { "label": "Manage roles (legacy)", "desc": "Retired permission — kept for compatibility" }, - "MANAGE_ROLES": { "label": "Manage roles", "desc": "Create, edit, and delete roles" }, - "MANAGE_VAULT": { "label": "Manage vault", "desc": "Rename vault and manage vault settings" }, - "START_TERMINAL_SESSION": { "label": "Start sessions", "desc": "Start multiplayer terminal sessions" }, - "JOIN_TERMINAL_SESSION": { "label": "Join sessions", "desc": "Join existing terminal sessions" }, - "VIEW_TERMINAL_SESSIONS": { "label": "View sessions", "desc": "See active terminal sessions" } + "VIEW_SECRETS": { + "label": "View secrets", + "desc": "See passwords and private keys in plain text" + }, + "COPY_SECRETS": { + "label": "Copy secrets", + "desc": "Copy passwords and keys to clipboard" + }, + "CONNECT": { + "label": "Connect", + "desc": "Launch SSH connections" + }, + "EDIT_CONNECTIONS": { + "label": "Edit connections", + "desc": "Create, modify, and delete connections" + }, + "EDIT_IDENTITIES": { + "label": "Edit identities", + "desc": "Create, modify, and delete SSH identities" + }, + "EDIT_KEYS": { + "label": "Edit keys", + "desc": "Create, modify, and delete SSH keys" + }, + "EDIT_SNIPPETS": { + "label": "Edit snippets", + "desc": "Create, modify, and delete command snippets" + }, + "EDIT_FOLDERS": { + "label": "Edit folders", + "desc": "Manage folder structure" + }, + "VIEW_AUDIT_LOG": { + "label": "View audit log", + "desc": "Read the activity audit log" + }, + "INVITE_MEMBERS": { + "label": "Invite members", + "desc": "Invite new members to the vault" + }, + "MANAGE_MEMBERS": { + "label": "Manage members", + "desc": "Assign roles and remove members" + }, + "CREATE_CUSTOM_ROLES": { + "label": "Manage roles (legacy)", + "desc": "Retired permission — kept for compatibility" + }, + "MANAGE_ROLES": { + "label": "Manage roles", + "desc": "Create, edit, and delete roles" + }, + "MANAGE_VAULT": { + "label": "Manage vault", + "desc": "Rename vault and manage vault settings" + }, + "START_TERMINAL_SESSION": { + "label": "Start sessions", + "desc": "Start multiplayer terminal sessions" + }, + "JOIN_TERMINAL_SESSION": { + "label": "Join sessions", + "desc": "Join existing terminal sessions" + }, + "VIEW_TERMINAL_SESSIONS": { + "label": "View sessions", + "desc": "See active terminal sessions" + } }, "builtinBadge": "Built-in", "dragToReorder": "Drag to reorder", @@ -483,7 +652,9 @@ "categoryLabel": "Plugin", "tabs": { "installed": "Installed ({{count}})", - "browse": "Browse" + "browse": "Browse", + "updateCount_one": "• {{count}} update", + "updateCount_other": "• {{count}} updates" }, "installed": { "filterPlaceholder": "Filter plugins…", @@ -500,7 +671,10 @@ "sourceLocal": "Local", "sourceUrl": "URL", "sourceInstalled": "Installed", - "unverified": "Unverified" + "unverified": "Unverified", + "update": "Update", + "updating": "Updating…", + "updateTitle": "Update to v{{version}}" }, "browse": { "searchPlaceholder": "Search plugins…", @@ -517,11 +691,22 @@ "removing": "Removing…", "uninstall": "Uninstall", "installing": "Installing…", - "install": "Install" + "install": "Install", + "updateBadge": "update available" }, "install": { "integrityFailed": "Plugin bundle failed its integrity check — install blocked.", "failed": "Failed to install plugin." + }, + "permissionModal": { + "installTitle": "Install {{name}}?", + "installBody": "This plugin runs with the app's full privileges. It requests these permissions:", + "updateTitle": "Update {{name}}?", + "updateBody": "This update requests new permissions. Review them before continuing:", + "newPermissions": "New permissions", + "noPermissions": "This plugin requests no special permissions.", + "installConfirm": "Install", + "updateConfirm": "Update" } }, "sftp": { @@ -590,10 +775,22 @@ "desc": "How quickly a session is declared lost when the server stops responding. {{detail}}. Can be overridden per host." }, "keepalivePresets": { - "fast": { "label": "Fast", "detail": "Detects a dropped connection in ~4s" }, - "balanced": { "label": "Balanced", "detail": "~9s; tolerates a brief network blip" }, - "tolerant": { "label": "Tolerant", "detail": "~20s; for flaky networks" }, - "off": { "label": "Off", "detail": "No keepalive probes" } + "fast": { + "label": "Fast", + "detail": "Detects a dropped connection in ~4s" + }, + "balanced": { + "label": "Balanced", + "detail": "~9s; tolerates a brief network blip" + }, + "tolerant": { + "label": "Tolerant", + "detail": "~20s; for flaky networks" + }, + "off": { + "label": "Off", + "detail": "No keepalive probes" + } }, "persistentSessions": { "title": "Persistent sessions", @@ -611,26 +808,58 @@ } }, "toggleDefs": { - "scrollMinimap": { "label": "Scroll Minimap" }, - "selectToCopy": { "label": "Select to Copy" }, - "ignoreBracketedPaste": { "label": "Plain Paste (No Bracketed Paste)" }, - "autoForward": { "label": "Automatic Port Forwarding" }, - "forwardingNotifications": { "label": "Port Forwarding Notifications" }, - "sftpTar": { "label": "SFTP Tar Acceleration" }, - "sftpAutoRefresh": { "label": "SFTP Auto-Refresh" }, - "reachability": { "label": "Reachability Check" }, - "teamPresence": { "label": "Team Presence" }, - "shellIntegration": { "label": "Shell Integration" }, - "persistentSessions": { "label": "Persistent Sessions" }, - "restoreWorkspace": { "label": "Restore Workspace on Launch" }, - "crossDeviceSessions": { "label": "Cross-Device Sessions" }, - "changelogPopup": { "label": "Show What's New After Updates" }, + "scrollMinimap": { + "label": "Scroll Minimap" + }, + "selectToCopy": { + "label": "Select to Copy" + }, + "ignoreBracketedPaste": { + "label": "Plain Paste (No Bracketed Paste)" + }, + "autoForward": { + "label": "Automatic Port Forwarding" + }, + "forwardingNotifications": { + "label": "Port Forwarding Notifications" + }, + "sftpTar": { + "label": "SFTP Tar Acceleration" + }, + "sftpAutoRefresh": { + "label": "SFTP Auto-Refresh" + }, + "reachability": { + "label": "Reachability Check" + }, + "teamPresence": { + "label": "Team Presence" + }, + "shellIntegration": { + "label": "Shell Integration" + }, + "persistentSessions": { + "label": "Persistent Sessions" + }, + "restoreWorkspace": { + "label": "Restore Workspace on Launch" + }, + "crossDeviceSessions": { + "label": "Cross-Device Sessions" + }, + "changelogPopup": { + "label": "Show What's New After Updates" + }, "category": { "appearance": "Appearance", "portForwarding": "Port Forwarding", "sftp": "SFTP", "hosts": "Hosts", - "updates": "Updates" + "updates": "Updates", + "plugins": "Plugins" + }, + "pluginInstallReview": { + "label": "Review permissions before installing plugins" } }, "shortcuts": { @@ -656,22 +885,70 @@ "other": "Other" }, "items": { - "omni": { "label": "Omni Search", "desc": "Access everything from one place" }, - "shortcuts": { "label": "Keyboard Shortcuts", "desc": "Open shortcut settings" }, - "themes": { "label": "Settings", "desc": "Open settings" }, - "newTab": { "label": "New Tab", "desc": "Go to hosts view" }, - "closeTab": { "label": "Close Tab", "desc": "Close active session" }, - "nextTab": { "label": "Next Tab", "desc": "Switch to next tab" }, - "prevTab": { "label": "Previous Tab", "desc": "Switch to prev tab" }, - "sidebar": { "label": "Toggle Sidebar", "desc": "Show/hide sidebar" }, - "delete": { "label": "Delete Selected", "desc": "Delete selected items" }, - "undo": { "label": "Undo", "desc": "Undo last action" }, - "redo": { "label": "Redo", "desc": "Redo last undone action" }, - "filter": { "label": "Focus Filter", "desc": "Focus the search filter" }, - "terminalSearch": { "label": "Find in Terminal", "desc": "Search the terminal scrollback" }, - "history": { "label": "History Panel", "desc": "Open command history in right panel" }, - "snippets": { "label": "Snippets Panel", "desc": "Open snippets in right panel" }, - "panelThemes": { "label": "Themes Panel", "desc": "Open themes in right panel" } + "omni": { + "label": "Omni Search", + "desc": "Access everything from one place" + }, + "shortcuts": { + "label": "Keyboard Shortcuts", + "desc": "Open shortcut settings" + }, + "themes": { + "label": "Settings", + "desc": "Open settings" + }, + "newTab": { + "label": "New Tab", + "desc": "Go to hosts view" + }, + "closeTab": { + "label": "Close Tab", + "desc": "Close active session" + }, + "nextTab": { + "label": "Next Tab", + "desc": "Switch to next tab" + }, + "prevTab": { + "label": "Previous Tab", + "desc": "Switch to prev tab" + }, + "sidebar": { + "label": "Toggle Sidebar", + "desc": "Show/hide sidebar" + }, + "delete": { + "label": "Delete Selected", + "desc": "Delete selected items" + }, + "undo": { + "label": "Undo", + "desc": "Undo last action" + }, + "redo": { + "label": "Redo", + "desc": "Redo last undone action" + }, + "filter": { + "label": "Focus Filter", + "desc": "Focus the search filter" + }, + "terminalSearch": { + "label": "Find in Terminal", + "desc": "Search the terminal scrollback" + }, + "history": { + "label": "History Panel", + "desc": "Open command history in right panel" + }, + "snippets": { + "label": "Snippets Panel", + "desc": "Open snippets in right panel" + }, + "panelThemes": { + "label": "Themes Panel", + "desc": "Open themes in right panel" + } } } } diff --git a/src/i18n/locales/fr/settings.json b/src/i18n/locales/fr/settings.json index 63ea8f0f..c532c62b 100644 --- a/src/i18n/locales/fr/settings.json +++ b/src/i18n/locales/fr/settings.json @@ -22,7 +22,9 @@ }, "appearance": { "interface": "Interface", - "language": { "title": "Langue" }, + "language": { + "title": "Langue" + }, "colorTheme": "Thème de couleurs", "import": "Importer", "importTitle": "Importer un ou plusieurs thèmes", @@ -61,18 +63,119 @@ } }, "nav": { - "appearance": { "label": "Apparence", "keywords": ["thème", "couleur", "police", "interface"] }, - "account": { "label": "Compte", "keywords": ["profil", "connexion", "authentification"] }, - "sync": { "label": "Synchronisation", "keywords": ["sauvegarde", "nuage", "gist", "synchronisation", "pro"] }, - "vaults": { "label": "Coffres-forts", "keywords": ["secret", "mot de passe", "stockage", "trousseau"] }, - "plugins": { "label": "Extensions", "keywords": ["extensions", "modules", "boutique"] }, - "terminal": { "label": "Terminal", "keywords": ["historique", "collage", "crochets", "copier", "mini-carte", "tampon"] }, - "sftp": { "label": "SFTP", "keywords": ["fichier", "transfert", "ftp", "fichiers"] }, - "portForwarding": { "label": "Transfert de ports", "keywords": ["tunnel", "redirection", "ports", "ssh", "auto"] }, - "hosts": { "label": "Hôtes", "keywords": ["ping", "accessibilité", "connectivité", "statut"] }, - "shortcuts": { "label": "Raccourcis", "keywords": ["raccourci", "touche", "clavier", "rebind", "keyboard"] }, - "diagnostics": { "label": "Diagnostics", "keywords": ["diagnostics", "bug", "rapport", "journal", "débogage", "problème"] }, - "about": { "label": "À propos", "keywords": ["version", "mise à jour", "publication", "changelog"] } + "appearance": { + "label": "Apparence", + "keywords": [ + "thème", + "couleur", + "police", + "interface" + ] + }, + "account": { + "label": "Compte", + "keywords": [ + "profil", + "connexion", + "authentification" + ] + }, + "sync": { + "label": "Synchronisation", + "keywords": [ + "sauvegarde", + "nuage", + "gist", + "synchronisation", + "pro" + ] + }, + "vaults": { + "label": "Coffres-forts", + "keywords": [ + "secret", + "mot de passe", + "stockage", + "trousseau" + ] + }, + "plugins": { + "label": "Extensions", + "keywords": [ + "extensions", + "modules", + "boutique" + ] + }, + "terminal": { + "label": "Terminal", + "keywords": [ + "historique", + "collage", + "crochets", + "copier", + "mini-carte", + "tampon" + ] + }, + "sftp": { + "label": "SFTP", + "keywords": [ + "fichier", + "transfert", + "ftp", + "fichiers" + ] + }, + "portForwarding": { + "label": "Transfert de ports", + "keywords": [ + "tunnel", + "redirection", + "ports", + "ssh", + "auto" + ] + }, + "hosts": { + "label": "Hôtes", + "keywords": [ + "ping", + "accessibilité", + "connectivité", + "statut" + ] + }, + "shortcuts": { + "label": "Raccourcis", + "keywords": [ + "raccourci", + "touche", + "clavier", + "rebind", + "keyboard" + ] + }, + "diagnostics": { + "label": "Diagnostics", + "keywords": [ + "diagnostics", + "bug", + "rapport", + "journal", + "débogage", + "problème" + ] + }, + "about": { + "label": "À propos", + "keywords": [ + "version", + "mise à jour", + "publication", + "changelog" + ] + } }, "chrome": { "title": "Paramètres", @@ -316,11 +419,26 @@ "prefsFooter": "Les types désactivés ne déclenchent pas de synchronisation automatique lors de modifications. Les objets individuels peuvent également être exclus via leur panneau d'édition.", "quickToggleLabel": "Synchroniser {{label}}", "objectType": { - "connection": { "label": "Hôtes", "sub": "Connexions SSH" }, - "identity": { "label": "Identités", "sub": "Noms d'utilisateur et informations d'identification" }, - "key": { "label": "Clés SSH", "sub": "Paires de clés stockées dans le trousseau" }, - "folder": { "label": "Dossiers", "sub": "Structure de dossiers pour organiser les objets" }, - "port-forwarding-rule": { "label": "Transfert de ports", "sub": "Règles de tunnel enregistrées" } + "connection": { + "label": "Hôtes", + "sub": "Connexions SSH" + }, + "identity": { + "label": "Identités", + "sub": "Noms d'utilisateur et informations d'identification" + }, + "key": { + "label": "Clés SSH", + "sub": "Paires de clés stockées dans le trousseau" + }, + "folder": { + "label": "Dossiers", + "sub": "Structure de dossiers pour organiser les objets" + }, + "port-forwarding-rule": { + "label": "Transfert de ports", + "sub": "Règles de tunnel enregistrées" + } } }, "vaults": { @@ -432,23 +550,74 @@ "terminalSessions": "Sessions de terminal" }, "perm": { - "VIEW_SECRETS": { "label": "Voir les secrets", "desc": "Voir les mots de passe et clés privées en clair" }, - "COPY_SECRETS": { "label": "Copier les secrets", "desc": "Copier les mots de passe et clés dans le presse-papiers" }, - "CONNECT": { "label": "Se connecter", "desc": "Lancer des connexions SSH" }, - "EDIT_CONNECTIONS": { "label": "Modifier les connexions", "desc": "Créer, modifier et supprimer des connexions" }, - "EDIT_IDENTITIES": { "label": "Modifier les identités", "desc": "Créer, modifier et supprimer des identités SSH" }, - "EDIT_KEYS": { "label": "Modifier les clés", "desc": "Créer, modifier et supprimer des clés SSH" }, - "EDIT_SNIPPETS": { "label": "Modifier les snippets", "desc": "Créer, modifier et supprimer des extraits de commandes" }, - "EDIT_FOLDERS": { "label": "Modifier les dossiers", "desc": "Gérer la structure des dossiers" }, - "VIEW_AUDIT_LOG": { "label": "Voir le journal d'audit", "desc": "Lire le journal d'activité" }, - "INVITE_MEMBERS": { "label": "Inviter des membres", "desc": "Inviter de nouveaux membres dans le coffre-fort" }, - "MANAGE_MEMBERS": { "label": "Gérer les membres", "desc": "Attribuer des rôles et retirer des membres" }, - "CREATE_CUSTOM_ROLES": { "label": "Gérer les rôles (hérité)", "desc": "Permission retirée — conservée pour compatibilité" }, - "MANAGE_ROLES": { "label": "Gérer les rôles", "desc": "Créer, modifier et supprimer des rôles" }, - "MANAGE_VAULT": { "label": "Gérer le coffre-fort", "desc": "Renommer le coffre-fort et gérer ses paramètres" }, - "START_TERMINAL_SESSION": { "label": "Démarrer des sessions", "desc": "Démarrer des sessions de terminal multijoueur" }, - "JOIN_TERMINAL_SESSION": { "label": "Rejoindre des sessions", "desc": "Rejoindre des sessions de terminal existantes" }, - "VIEW_TERMINAL_SESSIONS": { "label": "Voir les sessions", "desc": "Voir les sessions de terminal actives" } + "VIEW_SECRETS": { + "label": "Voir les secrets", + "desc": "Voir les mots de passe et clés privées en clair" + }, + "COPY_SECRETS": { + "label": "Copier les secrets", + "desc": "Copier les mots de passe et clés dans le presse-papiers" + }, + "CONNECT": { + "label": "Se connecter", + "desc": "Lancer des connexions SSH" + }, + "EDIT_CONNECTIONS": { + "label": "Modifier les connexions", + "desc": "Créer, modifier et supprimer des connexions" + }, + "EDIT_IDENTITIES": { + "label": "Modifier les identités", + "desc": "Créer, modifier et supprimer des identités SSH" + }, + "EDIT_KEYS": { + "label": "Modifier les clés", + "desc": "Créer, modifier et supprimer des clés SSH" + }, + "EDIT_SNIPPETS": { + "label": "Modifier les snippets", + "desc": "Créer, modifier et supprimer des extraits de commandes" + }, + "EDIT_FOLDERS": { + "label": "Modifier les dossiers", + "desc": "Gérer la structure des dossiers" + }, + "VIEW_AUDIT_LOG": { + "label": "Voir le journal d'audit", + "desc": "Lire le journal d'activité" + }, + "INVITE_MEMBERS": { + "label": "Inviter des membres", + "desc": "Inviter de nouveaux membres dans le coffre-fort" + }, + "MANAGE_MEMBERS": { + "label": "Gérer les membres", + "desc": "Attribuer des rôles et retirer des membres" + }, + "CREATE_CUSTOM_ROLES": { + "label": "Gérer les rôles (hérité)", + "desc": "Permission retirée — conservée pour compatibilité" + }, + "MANAGE_ROLES": { + "label": "Gérer les rôles", + "desc": "Créer, modifier et supprimer des rôles" + }, + "MANAGE_VAULT": { + "label": "Gérer le coffre-fort", + "desc": "Renommer le coffre-fort et gérer ses paramètres" + }, + "START_TERMINAL_SESSION": { + "label": "Démarrer des sessions", + "desc": "Démarrer des sessions de terminal multijoueur" + }, + "JOIN_TERMINAL_SESSION": { + "label": "Rejoindre des sessions", + "desc": "Rejoindre des sessions de terminal existantes" + }, + "VIEW_TERMINAL_SESSIONS": { + "label": "Voir les sessions", + "desc": "Voir les sessions de terminal actives" + } }, "builtinBadge": "Intégré", "dragToReorder": "Glisser pour réordonner", @@ -483,7 +652,9 @@ "categoryLabel": "Extension", "tabs": { "installed": "Installées ({{count}})", - "browse": "Parcourir" + "browse": "Parcourir", + "updateCount_one": "• {{count}} mise à jour", + "updateCount_other": "• {{count}} mises à jour" }, "installed": { "filterPlaceholder": "Filtrer les extensions…", @@ -500,7 +671,10 @@ "sourceLocal": "Locale", "sourceUrl": "URL", "sourceInstalled": "Installée", - "unverified": "Non vérifié" + "unverified": "Non vérifié", + "update": "Mettre à jour", + "updating": "Mise à jour…", + "updateTitle": "Mettre à jour vers la v{{version}}" }, "browse": { "searchPlaceholder": "Rechercher des extensions…", @@ -517,11 +691,22 @@ "removing": "Suppression…", "uninstall": "Désinstaller", "installing": "Installation…", - "install": "Installer" + "install": "Installer", + "updateBadge": "mise à jour disponible" }, "install": { "integrityFailed": "L'intégrité du module a échoué — installation bloquée.", "failed": "Échec de l'installation du module." + }, + "permissionModal": { + "installTitle": "Installer {{name}} ?", + "installBody": "Ce plugin s'exécute avec tous les privilèges de l'application. Il demande ces autorisations :", + "updateTitle": "Mettre à jour {{name}} ?", + "updateBody": "Cette mise à jour demande de nouvelles autorisations. Vérifiez-les avant de continuer :", + "newPermissions": "Nouvelles autorisations", + "noPermissions": "Ce plugin ne demande aucune autorisation particulière.", + "installConfirm": "Installer", + "updateConfirm": "Mettre à jour" } }, "sftp": { @@ -590,10 +775,22 @@ "desc": "Délai avant qu'une session soit considérée comme perdue lorsque le serveur cesse de répondre. {{detail}}. Peut être remplacé par hôte." }, "keepalivePresets": { - "fast": { "label": "Rapide", "detail": "Détecte une connexion perdue en ~4s" }, - "balanced": { "label": "Équilibré", "detail": "~9s ; tolère une brève coupure réseau" }, - "tolerant": { "label": "Tolérant", "detail": "~20s ; pour les réseaux instables" }, - "off": { "label": "Désactivé", "detail": "Aucune sonde de keepalive" } + "fast": { + "label": "Rapide", + "detail": "Détecte une connexion perdue en ~4s" + }, + "balanced": { + "label": "Équilibré", + "detail": "~9s ; tolère une brève coupure réseau" + }, + "tolerant": { + "label": "Tolérant", + "detail": "~20s ; pour les réseaux instables" + }, + "off": { + "label": "Désactivé", + "detail": "Aucune sonde de keepalive" + } }, "persistentSessions": { "title": "Sessions persistantes", @@ -611,26 +808,58 @@ } }, "toggleDefs": { - "scrollMinimap": { "label": "Minimap de défilement" }, - "selectToCopy": { "label": "Sélectionner pour copier" }, - "ignoreBracketedPaste": { "label": "Collage simple (sans crochets)" }, - "autoForward": { "label": "Transfert de ports automatique" }, - "forwardingNotifications": { "label": "Notifications de transfert de ports" }, - "sftpTar": { "label": "Accélération Tar SFTP" }, - "sftpAutoRefresh": { "label": "Actualisation automatique SFTP" }, - "reachability": { "label": "Vérification d'accessibilité" }, - "teamPresence": { "label": "Présence d'équipe" }, - "shellIntegration": { "label": "Intégration du shell" }, - "persistentSessions": { "label": "Sessions persistantes" }, - "restoreWorkspace": { "label": "Restaurer l'espace de travail au lancement" }, - "crossDeviceSessions": { "label": "Sessions multi-appareils" }, - "changelogPopup": { "label": "Afficher les nouveautés après les mises à jour" }, + "scrollMinimap": { + "label": "Minimap de défilement" + }, + "selectToCopy": { + "label": "Sélectionner pour copier" + }, + "ignoreBracketedPaste": { + "label": "Collage simple (sans crochets)" + }, + "autoForward": { + "label": "Transfert de ports automatique" + }, + "forwardingNotifications": { + "label": "Notifications de transfert de ports" + }, + "sftpTar": { + "label": "Accélération Tar SFTP" + }, + "sftpAutoRefresh": { + "label": "Actualisation automatique SFTP" + }, + "reachability": { + "label": "Vérification d'accessibilité" + }, + "teamPresence": { + "label": "Présence d'équipe" + }, + "shellIntegration": { + "label": "Intégration du shell" + }, + "persistentSessions": { + "label": "Sessions persistantes" + }, + "restoreWorkspace": { + "label": "Restaurer l'espace de travail au lancement" + }, + "crossDeviceSessions": { + "label": "Sessions multi-appareils" + }, + "changelogPopup": { + "label": "Afficher les nouveautés après les mises à jour" + }, "category": { "appearance": "Apparence", "portForwarding": "Transfert de ports", "sftp": "SFTP", "hosts": "Hôtes", - "updates": "Mises à jour" + "updates": "Mises à jour", + "plugins": "Plugins" + }, + "pluginInstallReview": { + "label": "Vérifier les autorisations avant d'installer des plugins" } }, "shortcuts": { @@ -656,22 +885,70 @@ "other": "Autre" }, "items": { - "omni": { "label": "Recherche universelle", "desc": "Accéder à tout depuis un seul endroit" }, - "shortcuts": { "label": "Raccourcis clavier", "desc": "Ouvrir les paramètres des raccourcis" }, - "themes": { "label": "Paramètres", "desc": "Ouvrir les paramètres" }, - "newTab": { "label": "Nouvel onglet", "desc": "Aller à la vue des hôtes" }, - "closeTab": { "label": "Fermer l'onglet", "desc": "Fermer la session active" }, - "nextTab": { "label": "Onglet suivant", "desc": "Passer à l'onglet suivant" }, - "prevTab": { "label": "Onglet précédent", "desc": "Passer à l'onglet précédent" }, - "sidebar": { "label": "Basculer la barre latérale", "desc": "Afficher/masquer la barre latérale" }, - "delete": { "label": "Supprimer la sélection", "desc": "Supprimer les éléments sélectionnés" }, - "undo": { "label": "Annuler", "desc": "Annuler la dernière action" }, - "redo": { "label": "Rétablir", "desc": "Rétablir la dernière action annulée" }, - "filter": { "label": "Focus sur le filtre", "desc": "Mettre le focus sur le filtre de recherche" }, - "terminalSearch": { "label": "Rechercher dans le terminal", "desc": "Rechercher dans l'historique du terminal" }, - "history": { "label": "Panneau d'historique", "desc": "Ouvrir l'historique des commandes dans le panneau latéral" }, - "snippets": { "label": "Panneau des extraits", "desc": "Ouvrir les extraits dans le panneau latéral" }, - "panelThemes": { "label": "Panneau des thèmes", "desc": "Ouvrir les thèmes dans le panneau latéral" } + "omni": { + "label": "Recherche universelle", + "desc": "Accéder à tout depuis un seul endroit" + }, + "shortcuts": { + "label": "Raccourcis clavier", + "desc": "Ouvrir les paramètres des raccourcis" + }, + "themes": { + "label": "Paramètres", + "desc": "Ouvrir les paramètres" + }, + "newTab": { + "label": "Nouvel onglet", + "desc": "Aller à la vue des hôtes" + }, + "closeTab": { + "label": "Fermer l'onglet", + "desc": "Fermer la session active" + }, + "nextTab": { + "label": "Onglet suivant", + "desc": "Passer à l'onglet suivant" + }, + "prevTab": { + "label": "Onglet précédent", + "desc": "Passer à l'onglet précédent" + }, + "sidebar": { + "label": "Basculer la barre latérale", + "desc": "Afficher/masquer la barre latérale" + }, + "delete": { + "label": "Supprimer la sélection", + "desc": "Supprimer les éléments sélectionnés" + }, + "undo": { + "label": "Annuler", + "desc": "Annuler la dernière action" + }, + "redo": { + "label": "Rétablir", + "desc": "Rétablir la dernière action annulée" + }, + "filter": { + "label": "Focus sur le filtre", + "desc": "Mettre le focus sur le filtre de recherche" + }, + "terminalSearch": { + "label": "Rechercher dans le terminal", + "desc": "Rechercher dans l'historique du terminal" + }, + "history": { + "label": "Panneau d'historique", + "desc": "Ouvrir l'historique des commandes dans le panneau latéral" + }, + "snippets": { + "label": "Panneau des extraits", + "desc": "Ouvrir les extraits dans le panneau latéral" + }, + "panelThemes": { + "label": "Panneau des thèmes", + "desc": "Ouvrir les thèmes dans le panneau latéral" + } } } } diff --git a/src/i18n/locales/ru/settings.json b/src/i18n/locales/ru/settings.json index d3eba3b2..9b39ccb3 100644 --- a/src/i18n/locales/ru/settings.json +++ b/src/i18n/locales/ru/settings.json @@ -22,7 +22,9 @@ }, "appearance": { "interface": "Интерфейс", - "language": { "title": "Язык" }, + "language": { + "title": "Язык" + }, "colorTheme": "Цветовая тема", "import": "Импорт", "importTitle": "Импортировать тему(-ы)", @@ -61,18 +63,119 @@ } }, "nav": { - "appearance": { "label": "Оформление", "keywords": ["тема", "цвет", "шрифт", "интерфейс"] }, - "account": { "label": "Аккаунт", "keywords": ["профиль", "вход", "аутентификация"] }, - "sync": { "label": "Синхронизация", "keywords": ["резервная копия", "облако", "gist", "синхронизация", "pro"] }, - "vaults": { "label": "Хранилища", "keywords": ["секрет", "пароль", "хранилище", "связка ключей"] }, - "plugins": { "label": "Плагины", "keywords": ["расширения", "дополнения", "маркетплейс"] }, - "terminal": { "label": "Терминал", "keywords": ["буфер", "вставка", "скобочная", "копирование", "мини-карта", "прокрутка"] }, - "sftp": { "label": "SFTP", "keywords": ["файл", "передача", "ftp", "файлы"] }, - "portForwarding": { "label": "Проброс портов", "keywords": ["туннель", "проброс", "порты", "ssh", "авто"] }, - "hosts": { "label": "Хосты", "keywords": ["ping", "доступность", "подключение", "статус"] }, - "shortcuts": { "label": "Сочетания клавиш", "keywords": ["сочетание клавиш", "горячая клавиша", "шорткат", "переназначить", "клавиатура"] }, - "diagnostics": { "label": "Диагностика", "keywords": ["диагностика", "баг", "отчёт", "журнал", "отладка", "проблема"] }, - "about": { "label": "О программе", "keywords": ["версия", "обновление", "релиз", "список изменений"] } + "appearance": { + "label": "Оформление", + "keywords": [ + "тема", + "цвет", + "шрифт", + "интерфейс" + ] + }, + "account": { + "label": "Аккаунт", + "keywords": [ + "профиль", + "вход", + "аутентификация" + ] + }, + "sync": { + "label": "Синхронизация", + "keywords": [ + "резервная копия", + "облако", + "gist", + "синхронизация", + "pro" + ] + }, + "vaults": { + "label": "Хранилища", + "keywords": [ + "секрет", + "пароль", + "хранилище", + "связка ключей" + ] + }, + "plugins": { + "label": "Плагины", + "keywords": [ + "расширения", + "дополнения", + "маркетплейс" + ] + }, + "terminal": { + "label": "Терминал", + "keywords": [ + "буфер", + "вставка", + "скобочная", + "копирование", + "мини-карта", + "прокрутка" + ] + }, + "sftp": { + "label": "SFTP", + "keywords": [ + "файл", + "передача", + "ftp", + "файлы" + ] + }, + "portForwarding": { + "label": "Проброс портов", + "keywords": [ + "туннель", + "проброс", + "порты", + "ssh", + "авто" + ] + }, + "hosts": { + "label": "Хосты", + "keywords": [ + "ping", + "доступность", + "подключение", + "статус" + ] + }, + "shortcuts": { + "label": "Сочетания клавиш", + "keywords": [ + "сочетание клавиш", + "горячая клавиша", + "шорткат", + "переназначить", + "клавиатура" + ] + }, + "diagnostics": { + "label": "Диагностика", + "keywords": [ + "диагностика", + "баг", + "отчёт", + "журнал", + "отладка", + "проблема" + ] + }, + "about": { + "label": "О программе", + "keywords": [ + "версия", + "обновление", + "релиз", + "список изменений" + ] + } }, "chrome": { "title": "Настройки", @@ -320,11 +423,26 @@ "prefsFooter": "Отключённые типы не будут запускать автоматическую синхронизацию при изменении. Отдельные объекты также можно исключить в панели редактирования.", "quickToggleLabel": "Синхронизировать {{label}}", "objectType": { - "connection": { "label": "Хосты", "sub": "SSH-подключения" }, - "identity": { "label": "Личности", "sub": "Имена пользователей и учётные данные" }, - "key": { "label": "SSH-ключи", "sub": "Пары ключей, хранящиеся в связке ключей" }, - "folder": { "label": "Папки", "sub": "Структура папок для организации объектов" }, - "port-forwarding-rule": { "label": "Проброс портов", "sub": "Сохранённые правила туннелей" } + "connection": { + "label": "Хосты", + "sub": "SSH-подключения" + }, + "identity": { + "label": "Личности", + "sub": "Имена пользователей и учётные данные" + }, + "key": { + "label": "SSH-ключи", + "sub": "Пары ключей, хранящиеся в связке ключей" + }, + "folder": { + "label": "Папки", + "sub": "Структура папок для организации объектов" + }, + "port-forwarding-rule": { + "label": "Проброс портов", + "sub": "Сохранённые правила туннелей" + } } }, "vaults": { @@ -444,23 +562,74 @@ "terminalSessions": "Сессии терминала" }, "perm": { - "VIEW_SECRETS": { "label": "Просмотр секретов", "desc": "Просматривать пароли и приватные ключи в открытом виде" }, - "COPY_SECRETS": { "label": "Копирование секретов", "desc": "Копировать пароли и ключи в буфер обмена" }, - "CONNECT": { "label": "Подключение", "desc": "Запускать SSH-подключения" }, - "EDIT_CONNECTIONS": { "label": "Изменение подключений", "desc": "Создавать, изменять и удалять подключения" }, - "EDIT_IDENTITIES": { "label": "Изменение личностей", "desc": "Создавать, изменять и удалять SSH-личности" }, - "EDIT_KEYS": { "label": "Изменение ключей", "desc": "Создавать, изменять и удалять SSH-ключи" }, - "EDIT_SNIPPETS": { "label": "Изменение сниппетов", "desc": "Создавать, изменять и удалять сниппеты команд" }, - "EDIT_FOLDERS": { "label": "Изменение папок", "desc": "Управлять структурой папок" }, - "VIEW_AUDIT_LOG": { "label": "Просмотр журнала аудита", "desc": "Читать журнал аудита действий" }, - "INVITE_MEMBERS": { "label": "Приглашение участников", "desc": "Приглашать новых участников в хранилище" }, - "MANAGE_MEMBERS": { "label": "Управление участниками", "desc": "Назначать роли и удалять участников" }, - "CREATE_CUSTOM_ROLES": { "label": "Управление ролями (устар.)", "desc": "Устаревшее право — сохранено для совместимости" }, - "MANAGE_ROLES": { "label": "Управление ролями", "desc": "Создавать, изменять и удалять роли" }, - "MANAGE_VAULT": { "label": "Управление хранилищем", "desc": "Переименовывать хранилище и управлять его настройками" }, - "START_TERMINAL_SESSION": { "label": "Запуск сессий", "desc": "Запускать совместные сессии терминала" }, - "JOIN_TERMINAL_SESSION": { "label": "Присоединение к сессиям", "desc": "Присоединяться к существующим сессиям терминала" }, - "VIEW_TERMINAL_SESSIONS": { "label": "Просмотр сессий", "desc": "Видеть активные сессии терминала" } + "VIEW_SECRETS": { + "label": "Просмотр секретов", + "desc": "Просматривать пароли и приватные ключи в открытом виде" + }, + "COPY_SECRETS": { + "label": "Копирование секретов", + "desc": "Копировать пароли и ключи в буфер обмена" + }, + "CONNECT": { + "label": "Подключение", + "desc": "Запускать SSH-подключения" + }, + "EDIT_CONNECTIONS": { + "label": "Изменение подключений", + "desc": "Создавать, изменять и удалять подключения" + }, + "EDIT_IDENTITIES": { + "label": "Изменение личностей", + "desc": "Создавать, изменять и удалять SSH-личности" + }, + "EDIT_KEYS": { + "label": "Изменение ключей", + "desc": "Создавать, изменять и удалять SSH-ключи" + }, + "EDIT_SNIPPETS": { + "label": "Изменение сниппетов", + "desc": "Создавать, изменять и удалять сниппеты команд" + }, + "EDIT_FOLDERS": { + "label": "Изменение папок", + "desc": "Управлять структурой папок" + }, + "VIEW_AUDIT_LOG": { + "label": "Просмотр журнала аудита", + "desc": "Читать журнал аудита действий" + }, + "INVITE_MEMBERS": { + "label": "Приглашение участников", + "desc": "Приглашать новых участников в хранилище" + }, + "MANAGE_MEMBERS": { + "label": "Управление участниками", + "desc": "Назначать роли и удалять участников" + }, + "CREATE_CUSTOM_ROLES": { + "label": "Управление ролями (устар.)", + "desc": "Устаревшее право — сохранено для совместимости" + }, + "MANAGE_ROLES": { + "label": "Управление ролями", + "desc": "Создавать, изменять и удалять роли" + }, + "MANAGE_VAULT": { + "label": "Управление хранилищем", + "desc": "Переименовывать хранилище и управлять его настройками" + }, + "START_TERMINAL_SESSION": { + "label": "Запуск сессий", + "desc": "Запускать совместные сессии терминала" + }, + "JOIN_TERMINAL_SESSION": { + "label": "Присоединение к сессиям", + "desc": "Присоединяться к существующим сессиям терминала" + }, + "VIEW_TERMINAL_SESSIONS": { + "label": "Просмотр сессий", + "desc": "Видеть активные сессии терминала" + } }, "builtinBadge": "Встроенная", "dragToReorder": "Перетащите для изменения порядка", @@ -495,7 +664,11 @@ "categoryLabel": "Плагин", "tabs": { "installed": "Установлено ({{count}})", - "browse": "Обзор" + "browse": "Обзор", + "updateCount_one": "• {{count}} обновление", + "updateCount_few": "• {{count}} обновления", + "updateCount_many": "• {{count}} обновлений", + "updateCount_other": "• {{count}} обновлений" }, "installed": { "filterPlaceholder": "Фильтр плагинов…", @@ -511,7 +684,10 @@ "noneInstalled": "Нет установленных плагинов.", "sourceLocal": "Локальный", "sourceUrl": "URL", - "sourceInstalled": "Установлен" + "sourceInstalled": "Установлен", + "update": "Обновить", + "updating": "Обновление…", + "updateTitle": "Обновить до v{{version}}" }, "browse": { "searchPlaceholder": "Поиск плагинов…", @@ -528,7 +704,18 @@ "removing": "Удаление…", "uninstall": "Удалить", "installing": "Установка…", - "install": "Установить" + "install": "Установить", + "updateBadge": "доступно обновление" + }, + "permissionModal": { + "installTitle": "Установить {{name}}?", + "installBody": "Этот плагин выполняется с полными правами приложения. Он запрашивает следующие разрешения:", + "updateTitle": "Обновить {{name}}?", + "updateBody": "Это обновление запрашивает новые разрешения. Проверьте их перед продолжением:", + "newPermissions": "Новые разрешения", + "noPermissions": "Этот плагин не запрашивает особых разрешений.", + "installConfirm": "Установить", + "updateConfirm": "Обновить" } }, "sftp": { @@ -597,10 +784,22 @@ "desc": "Как быстро сессия считается потерянной, если сервер перестаёт отвечать. {{detail}}. Можно переопределить для отдельного хоста." }, "keepalivePresets": { - "fast": { "label": "Быстрый", "detail": "Обнаруживает разрыв соединения примерно за 4 с" }, - "balanced": { "label": "Сбалансированный", "detail": "~9 с; выдерживает кратковременный сбой сети" }, - "tolerant": { "label": "Устойчивый", "detail": "~20 с; для нестабильных сетей" }, - "off": { "label": "Выкл", "detail": "Без проверок keepalive" } + "fast": { + "label": "Быстрый", + "detail": "Обнаруживает разрыв соединения примерно за 4 с" + }, + "balanced": { + "label": "Сбалансированный", + "detail": "~9 с; выдерживает кратковременный сбой сети" + }, + "tolerant": { + "label": "Устойчивый", + "detail": "~20 с; для нестабильных сетей" + }, + "off": { + "label": "Выкл", + "detail": "Без проверок keepalive" + } }, "persistentSessions": { "title": "Постоянные сессии", @@ -618,26 +817,58 @@ } }, "toggleDefs": { - "scrollMinimap": { "label": "Мини-карта прокрутки" }, - "selectToCopy": { "label": "Копирование по выделению" }, - "ignoreBracketedPaste": { "label": "Простая вставка (без «скобок»)" }, - "autoForward": { "label": "Автоматический проброс портов" }, - "forwardingNotifications": { "label": "Уведомления о пробросе портов" }, - "sftpTar": { "label": "Ускорение SFTP через tar" }, - "sftpAutoRefresh": { "label": "Автообновление SFTP" }, - "reachability": { "label": "Проверка доступности" }, - "teamPresence": { "label": "Присутствие команды" }, - "shellIntegration": { "label": "Интеграция с оболочкой" }, - "persistentSessions": { "label": "Постоянные сессии" }, - "restoreWorkspace": { "label": "Восстанавливать рабочее пространство при запуске" }, - "crossDeviceSessions": { "label": "Сессии между устройствами" }, - "changelogPopup": { "label": "Показывать список изменений после обновления" }, + "scrollMinimap": { + "label": "Мини-карта прокрутки" + }, + "selectToCopy": { + "label": "Копирование по выделению" + }, + "ignoreBracketedPaste": { + "label": "Простая вставка (без «скобок»)" + }, + "autoForward": { + "label": "Автоматический проброс портов" + }, + "forwardingNotifications": { + "label": "Уведомления о пробросе портов" + }, + "sftpTar": { + "label": "Ускорение SFTP через tar" + }, + "sftpAutoRefresh": { + "label": "Автообновление SFTP" + }, + "reachability": { + "label": "Проверка доступности" + }, + "teamPresence": { + "label": "Присутствие команды" + }, + "shellIntegration": { + "label": "Интеграция с оболочкой" + }, + "persistentSessions": { + "label": "Постоянные сессии" + }, + "restoreWorkspace": { + "label": "Восстанавливать рабочее пространство при запуске" + }, + "crossDeviceSessions": { + "label": "Сессии между устройствами" + }, + "changelogPopup": { + "label": "Показывать список изменений после обновления" + }, "category": { "appearance": "Оформление", "portForwarding": "Проброс портов", "sftp": "SFTP", "hosts": "Хосты", - "updates": "Обновления" + "updates": "Обновления", + "plugins": "Плагины" + }, + "pluginInstallReview": { + "label": "Проверять разрешения перед установкой плагинов" } }, "shortcuts": { @@ -665,22 +896,70 @@ "other": "Другое" }, "items": { - "omni": { "label": "Универсальный поиск", "desc": "Доступ ко всему из одного места" }, - "shortcuts": { "label": "Сочетания клавиш", "desc": "Открыть настройки сочетаний клавиш" }, - "themes": { "label": "Настройки", "desc": "Открыть настройки" }, - "newTab": { "label": "Новая вкладка", "desc": "Перейти к списку хостов" }, - "closeTab": { "label": "Закрыть вкладку", "desc": "Закрыть активную сессию" }, - "nextTab": { "label": "Следующая вкладка", "desc": "Переключиться на следующую вкладку" }, - "prevTab": { "label": "Предыдущая вкладка", "desc": "Переключиться на предыдущую вкладку" }, - "sidebar": { "label": "Показать/скрыть боковую панель", "desc": "Показать или скрыть боковую панель" }, - "delete": { "label": "Удалить выбранное", "desc": "Удалить выбранные элементы" }, - "undo": { "label": "Отменить", "desc": "Отменить последнее действие" }, - "redo": { "label": "Повторить", "desc": "Повторить последнее отменённое действие" }, - "filter": { "label": "Фокус на фильтре", "desc": "Установить фокус на поле фильтра" }, - "terminalSearch": { "label": "Поиск в терминале", "desc": "Искать в истории терминала" }, - "history": { "label": "Панель истории", "desc": "Открыть историю команд в правой панели" }, - "snippets": { "label": "Панель сниппетов", "desc": "Открыть сниппеты в правой панели" }, - "panelThemes": { "label": "Панель тем", "desc": "Открыть темы в правой панели" } + "omni": { + "label": "Универсальный поиск", + "desc": "Доступ ко всему из одного места" + }, + "shortcuts": { + "label": "Сочетания клавиш", + "desc": "Открыть настройки сочетаний клавиш" + }, + "themes": { + "label": "Настройки", + "desc": "Открыть настройки" + }, + "newTab": { + "label": "Новая вкладка", + "desc": "Перейти к списку хостов" + }, + "closeTab": { + "label": "Закрыть вкладку", + "desc": "Закрыть активную сессию" + }, + "nextTab": { + "label": "Следующая вкладка", + "desc": "Переключиться на следующую вкладку" + }, + "prevTab": { + "label": "Предыдущая вкладка", + "desc": "Переключиться на предыдущую вкладку" + }, + "sidebar": { + "label": "Показать/скрыть боковую панель", + "desc": "Показать или скрыть боковую панель" + }, + "delete": { + "label": "Удалить выбранное", + "desc": "Удалить выбранные элементы" + }, + "undo": { + "label": "Отменить", + "desc": "Отменить последнее действие" + }, + "redo": { + "label": "Повторить", + "desc": "Повторить последнее отменённое действие" + }, + "filter": { + "label": "Фокус на фильтре", + "desc": "Установить фокус на поле фильтра" + }, + "terminalSearch": { + "label": "Поиск в терминале", + "desc": "Искать в истории терминала" + }, + "history": { + "label": "Панель истории", + "desc": "Открыть историю команд в правой панели" + }, + "snippets": { + "label": "Панель сниппетов", + "desc": "Открыть сниппеты в правой панели" + }, + "panelThemes": { + "label": "Панель тем", + "desc": "Открыть темы в правой панели" + } } } } diff --git a/src/plugins/updates.test.ts b/src/plugins/updates.test.ts new file mode 100644 index 00000000..50f8d112 --- /dev/null +++ b/src/plugins/updates.test.ts @@ -0,0 +1,111 @@ +import { test, expect } from "vitest"; +import { compareSemver, availableUpdate, addedPermissions } from "./updates"; +import type { InstalledPluginMeta, MarketplacePlugin } from "@/stores/marketplaceStore"; + +// ─── compareSemver ───────────────────────────────────────────────────────── + +test("compareSemver orders major/minor/patch", () => { + expect(compareSemver("1.1.0", "1.0.0")).toBe(1); + expect(compareSemver("1.0.0", "1.1.0")).toBe(-1); + expect(compareSemver("2.0.0", "1.9.9")).toBe(1); + expect(compareSemver("1.0.1", "1.0.0")).toBe(1); +}); + +test("compareSemver treats equal versions as 0", () => { + expect(compareSemver("1.2.3", "1.2.3")).toBe(0); +}); + +test("compareSemver handles unequal segment counts", () => { + expect(compareSemver("1.2", "1.2.0")).toBe(0); + expect(compareSemver("1.2.1", "1.2")).toBe(1); + expect(compareSemver("1", "1.0.1")).toBe(-1); +}); + +test("compareSemver ignores pre-release/build suffixes (loose)", () => { + expect(compareSemver("1.2.3-beta", "1.2.3")).toBe(0); + expect(compareSemver("1.3.0-rc.1", "1.2.0")).toBe(1); +}); + +// ─── availableUpdate ─────────────────────────────────────────────────────── + +const plugin = (over: Partial): MarketplacePlugin => ({ + id: "p", + name: "P", + author: "a", + description: "", + repo: "https://example.com/p", + version: "1.0.0", + tags: [], + theme: false, + sourceId: "voltius", + ...over, +}); + +const meta = (over: Partial): InstalledPluginMeta => ({ + id: "p", + version: "1.0.0", + sourceId: "voltius", + hash: null, + ...over, +}); + +test("availableUpdate: newer catalog version is an update", () => { + const got = availableUpdate(meta({ version: "1.0.0" }), [plugin({ version: "1.1.0" })]); + expect(got?.version).toBe("1.1.0"); +}); + +test("availableUpdate: same version, no hash signal -> no update", () => { + expect(availableUpdate(meta({ version: "1.0.0" }), [plugin({ version: "1.0.0" })])).toBeNull(); +}); + +test("availableUpdate: same version but differing hashes (both present) -> update", () => { + const got = availableUpdate( + meta({ version: "1.0.0", hash: "aaa" }), + [plugin({ version: "1.0.0", hash: "bbb" })], + ); + expect(got?.hash).toBe("bbb"); +}); + +test("availableUpdate: same version, same hash -> no update", () => { + expect( + availableUpdate(meta({ version: "1.0.0", hash: "aaa" }), [plugin({ version: "1.0.0", hash: "aaa" })]), + ).toBeNull(); +}); + +test("availableUpdate: null installed hash ignores hash signal", () => { + expect( + availableUpdate(meta({ version: "1.0.0", hash: null }), [plugin({ version: "1.0.0", hash: "bbb" })]), + ).toBeNull(); +}); + +test("availableUpdate: older catalog version is not an update", () => { + expect(availableUpdate(meta({ version: "2.0.0" }), [plugin({ version: "1.0.0" })])).toBeNull(); +}); + +test("availableUpdate: no matching catalog entry -> null", () => { + expect(availableUpdate(meta({ id: "p" }), [plugin({ id: "other", version: "9.0.0" })])).toBeNull(); +}); + +test("availableUpdate: prefers the entry matching sourceId", () => { + const got = availableUpdate(meta({ sourceId: "voltius", version: "1.0.0" }), [ + plugin({ sourceId: "other", version: "5.0.0" }), + plugin({ sourceId: "voltius", version: "1.1.0" }), + ]); + expect(got?.sourceId).toBe("voltius"); + expect(got?.version).toBe("1.1.0"); +}); + +// ─── addedPermissions ────────────────────────────────────────────────────── + +test("addedPermissions returns only newly-declared permissions", () => { + expect(addedPermissions(["themes"], ["themes", "notifications"])).toEqual(["notifications"]); +}); + +test("addedPermissions is empty when nothing new", () => { + expect(addedPermissions(["themes", "fs"], ["fs", "themes"])).toEqual([]); + expect(addedPermissions(["a", "b"], ["a"])).toEqual([]); +}); + +test("addedPermissions treats all as new against an empty current set", () => { + expect(addedPermissions([], ["a", "b"])).toEqual(["a", "b"]); +}); diff --git a/src/plugins/updates.ts b/src/plugins/updates.ts new file mode 100644 index 00000000..c0f7c967 --- /dev/null +++ b/src/plugins/updates.ts @@ -0,0 +1,48 @@ +import type { InstalledPluginMeta, MarketplacePlugin } from "@/stores/marketplaceStore"; + +/** + * Loose numeric-dotted semver compare (no dependency). Pre-release/build suffixes are ignored, + * missing segments count as 0. Returns -1 | 0 | 1. + */ +export function compareSemver(a: string, b: string): number { + const parse = (v: string) => + v + .split(/[-+]/, 1)[0] + .split(".") + .map((n) => parseInt(n, 10) || 0); + const pa = parse(a); + const pb = parse(b); + const len = Math.max(pa.length, pb.length); + for (let i = 0; i < len; i++) { + const d = (pa[i] ?? 0) - (pb[i] ?? 0); + if (d !== 0) return d > 0 ? 1 : -1; + } + return 0; +} + +/** + * The catalog entry that represents an update for an installed plugin, or null. + * + * An update exists when the catalog `version` is newer, OR the version is unchanged but both the + * installed and catalog hashes are present and differ (catches versionless re-stamps of an in-repo + * bundle served from a mutable ref). When the installed hash is unknown (unverified/local), only + * the version signal is used. + */ +export function availableUpdate( + meta: InstalledPluginMeta, + catalog: MarketplacePlugin[], +): MarketplacePlugin | null { + const candidates = catalog.filter((p) => p.id === meta.id); + if (candidates.length === 0) return null; + const entry = candidates.find((p) => p.sourceId === meta.sourceId) ?? candidates[0]; + + if (compareSemver(entry.version, meta.version) > 0) return entry; + if (meta.hash && entry.hash && entry.hash.toLowerCase() !== meta.hash.toLowerCase()) return entry; + return null; +} + +/** Permissions declared in `next` that are not in `current`. */ +export function addedPermissions(current: string[], next: string[]): string[] { + const have = new Set(current); + return next.filter((p) => !have.has(p)); +} diff --git a/src/stores/marketplaceStore.ts b/src/stores/marketplaceStore.ts index b0d6426b..cf926c99 100644 --- a/src/stores/marketplaceStore.ts +++ b/src/stores/marketplaceStore.ts @@ -90,6 +90,7 @@ interface MarketplaceState { // Install / uninstall installing: Set; + fetchManifest: (plugin: MarketplacePlugin) => Promise; installPlugin: (plugin: MarketplacePlugin) => Promise; uninstallPlugin: (id: string) => Promise; reloadPlugin: (id: string) => Promise; @@ -160,6 +161,16 @@ export const useMarketplaceStore = create((set, get) => ({ // ── Install ─────────────────────────────────────────────────────────── installing: new Set(), + // Fetch just the manifest (no side effects) to preview declared permissions before + // installing/updating. The executed index.js is still hash-verified in installPlugin. + async fetchManifest(plugin: MarketplacePlugin) { + const base = plugin.repo.startsWith("http") + ? plugin.repo + : `https://github.com/${plugin.repo}/releases/latest/download`; + const manifestText = await invoke("plugin_fetch_url", { url: `${base}/manifest.json` }); + return JSON.parse(manifestText) as PluginManifest; + }, + async installPlugin(plugin: MarketplacePlugin) { const { installing, installedMeta } = get(); if (installing.has(plugin.id)) return; diff --git a/src/stores/toggleSettingsStore.ts b/src/stores/toggleSettingsStore.ts index 154f6598..69debd0a 100644 --- a/src/stores/toggleSettingsStore.ts +++ b/src/stores/toggleSettingsStore.ts @@ -122,6 +122,13 @@ export const TOGGLE_DEFS = { keywords: ["changelog", "popup", "release", "notes", "whats new", "update", "version"], default: true, }, + "plugin-install-review": { + labelKey: "settings.toggleDefs.pluginInstallReview.label", + icon: "lucide:shield-check", + descriptionKey: "settings.toggleDefs.category.plugins", + keywords: ["plugin", "permission", "install", "review", "consent", "disclosure", "security"], + default: true, + }, } as const satisfies Record; export type ToggleId = keyof typeof TOGGLE_DEFS; From f92cd8120a24c3d7e447ab5df7d109fe87617fcf Mon Sep 17 00:00:00 2001 From: kipavy Date: Fri, 24 Jul 2026 10:00:18 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(plugins):=20address=20code=20review=20?= =?UTF-8?q?=E2=80=94=20close=20consent=20loop,=20guard=20local=20updates?= =?UTF-8?q?=20(#52)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Security: pass the reviewed manifest text into installPlugin so the executed permission set is exactly the consented one, closing the fetch→consent→load TOCTOU (manifest.json isn't hash-pinned; index.js still is). Covers install disclosure, silent updates, and permission-grow updates. - availableUpdate now requires an exact sourceId match — a local/url plugin is never 'updated' from an id-colliding catalog entry (which would overwrite the local bundle). +2 regression tests. - Installed-tab update perm baseline reads getLoadedPlugins() at click, matching the Browse tab. - Backfill 3 ru keys missing since #44 (install.failed/integrityFailed, installed.unverified). --- .../settings/sections/PluginsSection.tsx | 22 ++++++++++--------- src/i18n/locales/ru/settings.json | 7 +++++- src/plugins/updates.test.ts | 18 ++++++++++++++- src/plugins/updates.ts | 8 ++++--- src/stores/marketplaceStore.ts | 19 +++++++++++----- 5 files changed, 53 insertions(+), 21 deletions(-) diff --git a/src/components/settings/sections/PluginsSection.tsx b/src/components/settings/sections/PluginsSection.tsx index 15b5c74d..75b2b788 100644 --- a/src/components/settings/sections/PluginsSection.tsx +++ b/src/components/settings/sections/PluginsSection.tsx @@ -155,6 +155,8 @@ interface PendingReview { plugin: MarketplacePlugin; permissions: string[]; addedPermissions: string[]; + /** The exact reviewed manifest text, passed to installPlugin so the loaded perms == consented. */ + manifestText: string; } /** @@ -186,8 +188,8 @@ function usePluginInstaller() { }); }; - const runInstall = async (plugin: MarketplacePlugin) => { - try { await installPlugin(plugin); } catch (e) { notifyError(e); } + const runInstall = async (plugin: MarketplacePlugin, reviewedManifestText?: string) => { + try { await installPlugin(plugin, reviewedManifestText); } catch (e) { notifyError(e); } }; const withPreparing = async (id: string, fn: () => Promise) => { @@ -201,8 +203,8 @@ function usePluginInstaller() { if (!getToggle("plugin-install-review")) { void runInstall(plugin); return; } void withPreparing(plugin.id, async () => { try { - const manifest = await fetchManifest(plugin); - setPending({ mode: "install", plugin, permissions: manifest.permissions ?? [], addedPermissions: [] }); + const { manifest, manifestText } = await fetchManifest(plugin); + setPending({ mode: "install", plugin, permissions: manifest.permissions ?? [], addedPermissions: [], manifestText }); } catch (e) { notifyError(e); } }); }; @@ -210,20 +212,20 @@ function usePluginInstaller() { const startUpdate = (plugin: MarketplacePlugin, currentPermissions: string[]) => { void withPreparing(plugin.id, async () => { try { - const manifest = await fetchManifest(plugin); + const { manifest, manifestText } = await fetchManifest(plugin); const next = manifest.permissions ?? []; const added = addedPermissions(currentPermissions, next); - if (added.length === 0) { await runInstall(plugin); return; } - setPending({ mode: "update", plugin, permissions: next, addedPermissions: added }); + if (added.length === 0) { await runInstall(plugin, manifestText); return; } + setPending({ mode: "update", plugin, permissions: next, addedPermissions: added, manifestText }); } catch (e) { notifyError(e); } }); }; const confirm = () => { if (!pending) return; - const { plugin } = pending; + const { plugin, manifestText } = pending; setPending(null); - void runInstall(plugin); + void runInstall(plugin, manifestText); }; const cancel = () => setPending(null); @@ -507,7 +509,7 @@ function InstalledTab() {
{update && (