mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-22 08:48:26 +02:00
Improve plugin source modeling and runtime dedup (#414)
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
- Tag safety: app releases use `v*` tags and CLI releases use `yaak-cli-*` tags; always confirm which one is requested before retagging.
|
- Tag safety: app releases use `v*` tags and CLI releases use `yaak-cli-*` tags; always confirm which one is requested before retagging.
|
||||||
|
- Do not commit, push, or tag without explicit approval
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ use yaak_mac_window::AppHandleMacWindowExt;
|
|||||||
use yaak_models::models::{
|
use yaak_models::models::{
|
||||||
AnyModel, CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent,
|
AnyModel, CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent,
|
||||||
GrpcEventType, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseState, Plugin,
|
GrpcEventType, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseState, Plugin,
|
||||||
Workspace, WorkspaceMeta,
|
PluginSource, Workspace, WorkspaceMeta,
|
||||||
};
|
};
|
||||||
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
|
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
|
||||||
use yaak_plugins::events::{
|
use yaak_plugins::events::{
|
||||||
@@ -1354,7 +1354,13 @@ async fn cmd_install_plugin<R: Runtime>(
|
|||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
) -> YaakResult<Plugin> {
|
) -> YaakResult<Plugin> {
|
||||||
let plugin = app_handle.db().upsert_plugin(
|
let plugin = app_handle.db().upsert_plugin(
|
||||||
&Plugin { directory: directory.into(), url, enabled: true, ..Default::default() },
|
&Plugin {
|
||||||
|
directory: directory.into(),
|
||||||
|
url,
|
||||||
|
enabled: true,
|
||||||
|
source: PluginSource::Filesystem,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
&UpdateSource::from_window_label(window.label()),
|
&UpdateSource::from_window_label(window.label()),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use yaak_models::error::Result;
|
|||||||
use yaak_models::models::{AnyModel, GraphQlIntrospection, GrpcEvent, Settings, WebsocketEvent};
|
use yaak_models::models::{AnyModel, GraphQlIntrospection, GrpcEvent, Settings, WebsocketEvent};
|
||||||
use yaak_models::query_manager::QueryManager;
|
use yaak_models::query_manager::QueryManager;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
use yaak_plugins::manager::PluginManager;
|
||||||
|
|
||||||
const MODEL_CHANGES_RETENTION_HOURS: i64 = 1;
|
const MODEL_CHANGES_RETENTION_HOURS: i64 = 1;
|
||||||
const MODEL_CHANGES_POLL_INTERVAL_MS: u64 = 1000;
|
const MODEL_CHANGES_POLL_INTERVAL_MS: u64 = 1000;
|
||||||
@@ -255,23 +256,32 @@ pub(crate) fn models_upsert_graphql_introspection<R: Runtime>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub(crate) fn models_workspace_models<R: Runtime>(
|
pub(crate) async fn models_workspace_models<R: Runtime>(
|
||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
workspace_id: Option<&str>,
|
workspace_id: Option<&str>,
|
||||||
|
plugin_manager: State<'_, PluginManager>,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let db = window.db();
|
|
||||||
let mut l: Vec<AnyModel> = Vec::new();
|
let mut l: Vec<AnyModel> = Vec::new();
|
||||||
|
|
||||||
// Add the settings
|
// Add the global models
|
||||||
l.push(db.get_settings().into());
|
{
|
||||||
|
let db = window.db();
|
||||||
|
l.push(db.get_settings().into());
|
||||||
|
l.append(&mut db.list_workspaces()?.into_iter().map(Into::into).collect());
|
||||||
|
l.append(&mut db.list_key_values()?.into_iter().map(Into::into).collect());
|
||||||
|
}
|
||||||
|
|
||||||
// Add global models
|
let plugins = {
|
||||||
l.append(&mut db.list_workspaces()?.into_iter().map(Into::into).collect());
|
let db = window.db();
|
||||||
l.append(&mut db.list_key_values()?.into_iter().map(Into::into).collect());
|
db.list_plugins()?
|
||||||
l.append(&mut db.list_plugins()?.into_iter().map(Into::into).collect());
|
};
|
||||||
|
|
||||||
|
let plugins = plugin_manager.resolve_plugins_for_runtime_from_db(plugins).await;
|
||||||
|
l.append(&mut plugins.into_iter().map(Into::into).collect());
|
||||||
|
|
||||||
// Add the workspace children
|
// Add the workspace children
|
||||||
if let Some(wid) = workspace_id {
|
if let Some(wid) = workspace_id {
|
||||||
|
let db = window.db();
|
||||||
l.append(&mut db.list_cookie_jars(wid)?.into_iter().map(Into::into).collect());
|
l.append(&mut db.list_cookie_jars(wid)?.into_iter().map(Into::into).collect());
|
||||||
l.append(&mut db.list_environments_ensure_base(wid)?.into_iter().map(Into::into).collect());
|
l.append(&mut db.list_environments_ensure_base(wid)?.into_iter().map(Into::into).collect());
|
||||||
l.append(&mut db.list_folders(wid)?.into_iter().map(Into::into).collect());
|
l.append(&mut db.list_folders(wid)?.into_iter().map(Into::into).collect());
|
||||||
|
|||||||
4
crates/yaak-models/bindings/gen_models.ts
generated
4
crates/yaak-models/bindings/gen_models.ts
generated
@@ -67,7 +67,9 @@ export type ParentAuthentication = { authentication: Record<string, any>, authen
|
|||||||
|
|
||||||
export type ParentHeaders = { headers: Array<HttpRequestHeader>, };
|
export type ParentHeaders = { headers: Array<HttpRequestHeader>, };
|
||||||
|
|
||||||
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, };
|
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, source: PluginSource, };
|
||||||
|
|
||||||
|
export type PluginSource = "bundled" | "filesystem" | "registry";
|
||||||
|
|
||||||
export type PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, };
|
export type PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, };
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
ALTER TABLE plugins
|
||||||
|
ADD COLUMN source TEXT DEFAULT 'filesystem' NOT NULL;
|
||||||
|
|
||||||
|
-- Existing registry installs have a URL; classify them first.
|
||||||
|
UPDATE plugins
|
||||||
|
SET source = 'registry'
|
||||||
|
WHERE url IS NOT NULL;
|
||||||
|
|
||||||
|
-- Best-effort bundled backfill for legacy rows.
|
||||||
|
UPDATE plugins
|
||||||
|
SET source = 'bundled'
|
||||||
|
WHERE source = 'filesystem'
|
||||||
|
AND (
|
||||||
|
-- Normalize separators so this also works for Windows paths.
|
||||||
|
replace(directory, '\', '/') LIKE '%/vendored/plugins/%'
|
||||||
|
OR replace(directory, '\', '/') LIKE '%/vendored-plugins/%'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Keep one row per exact directory before adding uniqueness.
|
||||||
|
-- Tie-break by recency.
|
||||||
|
WITH ranked AS (SELECT id,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY directory
|
||||||
|
ORDER BY updated_at DESC,
|
||||||
|
created_at DESC
|
||||||
|
) AS row_num
|
||||||
|
FROM plugins)
|
||||||
|
DELETE
|
||||||
|
FROM plugins
|
||||||
|
WHERE id IN (SELECT id FROM ranked WHERE row_num > 1);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_plugins_directory_unique
|
||||||
|
ON plugins (directory);
|
||||||
@@ -2074,6 +2074,46 @@ pub struct Plugin {
|
|||||||
pub directory: String,
|
pub directory: String,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
|
pub source: PluginSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[ts(export, export_to = "gen_models.ts")]
|
||||||
|
pub enum PluginSource {
|
||||||
|
Bundled,
|
||||||
|
Filesystem,
|
||||||
|
Registry,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for PluginSource {
|
||||||
|
type Err = crate::error::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
match s {
|
||||||
|
"bundled" => Ok(Self::Bundled),
|
||||||
|
"filesystem" => Ok(Self::Filesystem),
|
||||||
|
"registry" => Ok(Self::Registry),
|
||||||
|
_ => Ok(Self::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for PluginSource {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let str = match self {
|
||||||
|
PluginSource::Bundled => "bundled".to_string(),
|
||||||
|
PluginSource::Filesystem => "filesystem".to_string(),
|
||||||
|
PluginSource::Registry => "registry".to_string(),
|
||||||
|
};
|
||||||
|
write!(f, "{}", str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PluginSource {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Filesystem
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpsertModelInfo for Plugin {
|
impl UpsertModelInfo for Plugin {
|
||||||
@@ -2109,6 +2149,7 @@ impl UpsertModelInfo for Plugin {
|
|||||||
(Directory, self.directory.into()),
|
(Directory, self.directory.into()),
|
||||||
(Url, self.url.into()),
|
(Url, self.url.into()),
|
||||||
(Enabled, self.enabled.into()),
|
(Enabled, self.enabled.into()),
|
||||||
|
(Source, self.source.to_string().into()),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2119,6 +2160,7 @@ impl UpsertModelInfo for Plugin {
|
|||||||
PluginIden::Directory,
|
PluginIden::Directory,
|
||||||
PluginIden::Url,
|
PluginIden::Url,
|
||||||
PluginIden::Enabled,
|
PluginIden::Enabled,
|
||||||
|
PluginIden::Source,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2135,6 +2177,7 @@ impl UpsertModelInfo for Plugin {
|
|||||||
url: row.get("url")?,
|
url: row.get("url")?,
|
||||||
directory: row.get("directory")?,
|
directory: row.get("directory")?,
|
||||||
enabled: row.get("enabled")?,
|
enabled: row.get("enabled")?,
|
||||||
|
source: PluginSource::from_str(row.get::<_, String>("source")?.as_str()).unwrap(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ impl<'a> DbContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn upsert_plugin(&self, plugin: &Plugin, source: &UpdateSource) -> Result<Plugin> {
|
pub fn upsert_plugin(&self, plugin: &Plugin, source: &UpdateSource) -> Result<Plugin> {
|
||||||
self.upsert(plugin, source)
|
let mut plugin_to_upsert = plugin.clone();
|
||||||
|
if let Some(existing) = self.get_plugin_by_directory(&plugin.directory) {
|
||||||
|
plugin_to_upsert.id = existing.id;
|
||||||
|
}
|
||||||
|
self.upsert(&plugin_to_upsert, source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use yaak_models::models::Plugin;
|
use yaak_models::models::{Plugin, PluginSource};
|
||||||
|
|
||||||
/// Get plugin info from the registry.
|
/// Get plugin info from the registry.
|
||||||
pub async fn get_plugin(
|
pub async fn get_plugin(
|
||||||
@@ -58,7 +58,7 @@ pub async fn check_plugin_updates(
|
|||||||
) -> Result<PluginUpdatesResponse> {
|
) -> Result<PluginUpdatesResponse> {
|
||||||
let name_versions: Vec<PluginNameVersion> = plugins
|
let name_versions: Vec<PluginNameVersion> = plugins
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|p| p.url.is_some()) // Only check plugins with URLs (from registry)
|
.filter(|p| matches!(p.source, PluginSource::Registry)) // Only check registry-installed plugins
|
||||||
.filter_map(|p| match get_plugin_meta(&Path::new(&p.directory)) {
|
.filter_map(|p| match get_plugin_meta(&Path::new(&p.directory)) {
|
||||||
Ok(m) => Some(PluginNameVersion { name: m.name, version: m.version }),
|
Ok(m) => Some(PluginNameVersion { name: m.name, version: m.version }),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use log::info;
|
|||||||
use std::fs::{create_dir_all, remove_dir_all};
|
use std::fs::{create_dir_all, remove_dir_all};
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use yaak_models::models::Plugin;
|
use yaak_models::models::{Plugin, PluginSource};
|
||||||
use yaak_models::query_manager::QueryManager;
|
use yaak_models::query_manager::QueryManager;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
|
||||||
@@ -78,6 +78,7 @@ pub async fn download_and_install(
|
|||||||
directory: plugin_dir_str.clone(),
|
directory: plugin_dir_str.clone(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
url: Some(plugin_version.url.clone()),
|
url: Some(plugin_version.url.clone()),
|
||||||
|
source: PluginSource::Registry,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
&UpdateSource::Background,
|
&UpdateSource::Background,
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ use crate::events::{
|
|||||||
use crate::native_template_functions::{template_function_keyring, template_function_secure};
|
use crate::native_template_functions::{template_function_keyring, template_function_secure};
|
||||||
use crate::nodejs::start_nodejs_plugin_runtime;
|
use crate::nodejs::start_nodejs_plugin_runtime;
|
||||||
use crate::plugin_handle::PluginHandle;
|
use crate::plugin_handle::PluginHandle;
|
||||||
|
use crate::plugin_meta::get_plugin_meta;
|
||||||
use crate::server_ws::PluginRuntimeServerWebsocket;
|
use crate::server_ws::PluginRuntimeServerWebsocket;
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -33,7 +34,7 @@ use tokio::net::TcpListener;
|
|||||||
use tokio::sync::mpsc::error::TrySendError;
|
use tokio::sync::mpsc::error::TrySendError;
|
||||||
use tokio::sync::{Mutex, mpsc, oneshot};
|
use tokio::sync::{Mutex, mpsc, oneshot};
|
||||||
use tokio::time::{Instant, timeout};
|
use tokio::time::{Instant, timeout};
|
||||||
use yaak_models::models::Plugin;
|
use yaak_models::models::{Plugin, PluginSource};
|
||||||
use yaak_models::query_manager::QueryManager;
|
use yaak_models::query_manager::QueryManager;
|
||||||
use yaak_models::util::{UpdateSource, generate_id};
|
use yaak_models::util::{UpdateSource, generate_id};
|
||||||
use yaak_templates::error::Error::RenderError;
|
use yaak_templates::error::Error::RenderError;
|
||||||
@@ -162,13 +163,14 @@ impl PluginManager {
|
|||||||
|
|
||||||
let bundled_dirs = plugin_manager.list_bundled_plugin_dirs().await?;
|
let bundled_dirs = plugin_manager.list_bundled_plugin_dirs().await?;
|
||||||
let db = query_manager.connect();
|
let db = query_manager.connect();
|
||||||
for dir in bundled_dirs {
|
for dir in &bundled_dirs {
|
||||||
if db.get_plugin_by_directory(&dir).is_none() {
|
if db.get_plugin_by_directory(dir).is_none() {
|
||||||
db.upsert_plugin(
|
db.upsert_plugin(
|
||||||
&Plugin {
|
&Plugin {
|
||||||
directory: dir,
|
directory: dir.clone(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
url: None,
|
url: None,
|
||||||
|
source: PluginSource::Bundled,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
&UpdateSource::Background,
|
&UpdateSource::Background,
|
||||||
@@ -213,6 +215,57 @@ impl PluginManager {
|
|||||||
read_plugins_dir(&plugins_dir).await
|
read_plugins_dir(&plugins_dir).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn resolve_plugins_for_runtime_from_db(&self, plugins: Vec<Plugin>) -> Vec<Plugin> {
|
||||||
|
let bundled_dirs = match self.list_bundled_plugin_dirs().await {
|
||||||
|
Ok(dirs) => dirs,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Failed to read bundled plugin dirs for resolution: {err:?}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.resolve_plugins_for_runtime(plugins, bundled_dirs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the plugin set for the current runtime instance.
|
||||||
|
///
|
||||||
|
/// Rules:
|
||||||
|
/// - Drop bundled rows that are not present in this instance's bundled directory list.
|
||||||
|
/// - Deduplicate by plugin metadata name (fallback to directory key when metadata is unreadable).
|
||||||
|
/// - Prefer sources in this order: filesystem > registry > bundled.
|
||||||
|
/// - For same-source conflicts, prefer the most recently installed row (`created_at`).
|
||||||
|
fn resolve_plugins_for_runtime(
|
||||||
|
&self,
|
||||||
|
plugins: Vec<Plugin>,
|
||||||
|
bundled_dirs: Vec<String>,
|
||||||
|
) -> Vec<Plugin> {
|
||||||
|
let bundled_dir_set: HashSet<String> = bundled_dirs.into_iter().collect();
|
||||||
|
let mut selected: HashMap<String, Plugin> = HashMap::new();
|
||||||
|
|
||||||
|
for plugin in plugins {
|
||||||
|
if matches!(plugin.source, PluginSource::Bundled)
|
||||||
|
&& !bundled_dir_set.contains(&plugin.directory)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = match get_plugin_meta(Path::new(&plugin.directory)) {
|
||||||
|
Ok(meta) => meta.name,
|
||||||
|
Err(_) => format!("__dir__{}", plugin.directory),
|
||||||
|
};
|
||||||
|
|
||||||
|
match selected.get(&key) {
|
||||||
|
Some(existing) if !prefer_plugin(&plugin, existing) => {}
|
||||||
|
_ => {
|
||||||
|
selected.insert(key, plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut resolved = selected.into_values().collect::<Vec<_>>();
|
||||||
|
resolved.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||||
|
resolved
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn uninstall(&self, plugin_context: &PluginContext, dir: &str) -> Result<()> {
|
pub async fn uninstall(&self, plugin_context: &PluginContext, dir: &str) -> Result<()> {
|
||||||
let plugin = self.get_plugin_by_dir(dir).await.ok_or(PluginNotFoundErr(dir.to_string()))?;
|
let plugin = self.get_plugin_by_dir(dir).await.ok_or(PluginNotFoundErr(dir.to_string()))?;
|
||||||
self.remove_plugin(plugin_context, &plugin).await
|
self.remove_plugin(plugin_context, &plugin).await
|
||||||
@@ -287,7 +340,8 @@ impl PluginManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize all plugins from the provided list.
|
/// Initialize all plugins from the provided DB list.
|
||||||
|
/// Plugin candidates are resolved for this runtime instance before initialization.
|
||||||
/// Returns a list of (plugin_directory, error_message) for any plugins that failed to initialize.
|
/// Returns a list of (plugin_directory, error_message) for any plugins that failed to initialize.
|
||||||
pub async fn initialize_all_plugins(
|
pub async fn initialize_all_plugins(
|
||||||
&self,
|
&self,
|
||||||
@@ -297,15 +351,18 @@ impl PluginManager {
|
|||||||
info!("Initializing all plugins");
|
info!("Initializing all plugins");
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
|
let plugins = self.resolve_plugins_for_runtime_from_db(plugins).await;
|
||||||
|
|
||||||
|
// Rebuild runtime handles from scratch to avoid stale/duplicate handles.
|
||||||
|
let existing_handles = { self.plugin_handles.lock().await.clone() };
|
||||||
|
for plugin_handle in existing_handles {
|
||||||
|
if let Err(e) = self.remove_plugin(plugin_context, &plugin_handle).await {
|
||||||
|
error!("Failed to remove plugin {} {e:?}", plugin_handle.dir);
|
||||||
|
errors.push((plugin_handle.dir.clone(), e.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for plugin in plugins {
|
for plugin in plugins {
|
||||||
// First remove the plugin if it exists and is enabled
|
|
||||||
if let Some(plugin_handle) = self.get_plugin_by_dir(&plugin.directory).await {
|
|
||||||
if let Err(e) = self.remove_plugin(plugin_context, &plugin_handle).await {
|
|
||||||
error!("Failed to remove plugin {} {e:?}", plugin.directory);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Err(e) = self.add_plugin(plugin_context, &plugin).await {
|
if let Err(e) = self.add_plugin(plugin_context, &plugin).await {
|
||||||
warn!("Failed to add plugin {} {e:?}", plugin.directory);
|
warn!("Failed to add plugin {} {e:?}", plugin.directory);
|
||||||
errors.push((plugin.directory.clone(), e.to_string()));
|
errors.push((plugin.directory.clone(), e.to_string()));
|
||||||
@@ -1063,6 +1120,24 @@ impl PluginManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn source_priority(source: &PluginSource) -> i32 {
|
||||||
|
match source {
|
||||||
|
PluginSource::Filesystem => 3,
|
||||||
|
PluginSource::Registry => 2,
|
||||||
|
PluginSource::Bundled => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prefer_plugin(candidate: &Plugin, existing: &Plugin) -> bool {
|
||||||
|
let candidate_priority = source_priority(&candidate.source);
|
||||||
|
let existing_priority = source_priority(&existing.source);
|
||||||
|
if candidate_priority != existing_priority {
|
||||||
|
return candidate_priority > existing_priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
candidate.created_at > existing.created_at
|
||||||
|
}
|
||||||
|
|
||||||
async fn read_plugins_dir(dir: &PathBuf) -> Result<Vec<String>> {
|
async fn read_plugins_dir(dir: &PathBuf) -> Result<Vec<String>> {
|
||||||
let mut result = read_dir(dir).await?;
|
let mut result = read_dir(dir).await?;
|
||||||
let mut dirs: Vec<String> = vec![];
|
let mut dirs: Vec<String> = vec![];
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaak/template-function-timestamp",
|
"name": "@yaak/template-function-timestamp",
|
||||||
|
"displayName": "Timestamp Template Functions",
|
||||||
|
"description": "Template functions for dealing with timestamps",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { useDebouncedValue } from '../../hooks/useDebouncedValue';
|
|||||||
import { useInstallPlugin } from '../../hooks/useInstallPlugin';
|
import { useInstallPlugin } from '../../hooks/useInstallPlugin';
|
||||||
import { usePluginInfo } from '../../hooks/usePluginInfo';
|
import { usePluginInfo } from '../../hooks/usePluginInfo';
|
||||||
import { usePluginsKey, useRefreshPlugins } from '../../hooks/usePlugins';
|
import { usePluginsKey, useRefreshPlugins } from '../../hooks/usePlugins';
|
||||||
import { appInfo } from '../../lib/appInfo';
|
|
||||||
import { showConfirmDelete } from '../../lib/confirm';
|
import { showConfirmDelete } from '../../lib/confirm';
|
||||||
import { minPromiseMillis } from '../../lib/minPromiseMillis';
|
import { minPromiseMillis } from '../../lib/minPromiseMillis';
|
||||||
import { Button } from '../core/Button';
|
import { Button } from '../core/Button';
|
||||||
@@ -33,16 +32,6 @@ import { TabContent, Tabs } from '../core/Tabs/Tabs';
|
|||||||
import { EmptyStateText } from '../EmptyStateText';
|
import { EmptyStateText } from '../EmptyStateText';
|
||||||
import { SelectFile } from '../SelectFile';
|
import { SelectFile } from '../SelectFile';
|
||||||
|
|
||||||
function isPluginBundled(plugin: Plugin, vendoredPluginDir: string): boolean {
|
|
||||||
const normalizedDir = plugin.directory.replace(/\\/g, '/');
|
|
||||||
const normalizedVendoredDir = vendoredPluginDir.replace(/\\/g, '/');
|
|
||||||
return (
|
|
||||||
normalizedDir.includes(normalizedVendoredDir) ||
|
|
||||||
normalizedDir.includes('vendored/plugins') ||
|
|
||||||
normalizedDir.includes('/plugins/')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SettingsPluginsProps {
|
interface SettingsPluginsProps {
|
||||||
defaultSubtab?: string;
|
defaultSubtab?: string;
|
||||||
}
|
}
|
||||||
@@ -50,8 +39,8 @@ interface SettingsPluginsProps {
|
|||||||
export function SettingsPlugins({ defaultSubtab }: SettingsPluginsProps) {
|
export function SettingsPlugins({ defaultSubtab }: SettingsPluginsProps) {
|
||||||
const [directory, setDirectory] = useState<string | null>(null);
|
const [directory, setDirectory] = useState<string | null>(null);
|
||||||
const plugins = useAtomValue(pluginsAtom);
|
const plugins = useAtomValue(pluginsAtom);
|
||||||
const bundledPlugins = plugins.filter((p) => isPluginBundled(p, appInfo.vendoredPluginDir));
|
const bundledPlugins = plugins.filter((p) => p.source === 'bundled');
|
||||||
const installedPlugins = plugins.filter((p) => !isPluginBundled(p, appInfo.vendoredPluginDir));
|
const installedPlugins = plugins.filter((p) => p.source !== 'bundled');
|
||||||
const createPlugin = useInstallPlugin();
|
const createPlugin = useInstallPlugin();
|
||||||
const refreshPlugins = useRefreshPlugins();
|
const refreshPlugins = useRefreshPlugins();
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -896,9 +896,9 @@ function MenuItem({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const rightSlot = item.submenu ? (
|
const rightSlot = item.submenu ? (
|
||||||
<Icon icon="chevron_right" color='secondary' />
|
<Icon icon="chevron_right" color="secondary" />
|
||||||
) : (
|
) : (
|
||||||
(item.rightSlot ?? <Hotkey action={item.hotKeyAction ?? null} />)
|
(item.rightSlot ?? <Hotkey variant="text" action={item.hotKeyAction ?? null} />)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -937,7 +937,7 @@ function MenuItem({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className={classNames('truncate')}>{item.label}</div>
|
<div className={classNames('truncate min-w-[5rem]')}>{item.label}</div>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user