Compare commits

..

11 Commits

Author SHA1 Message Date
Gregory Schier
f302dc39a2 Move local plugin install command into plugins_ext 2026-03-01 16:42:13 -08:00
Gregory Schier
2ca51125a4 Improve plugin source modeling and runtime dedup (#414) 2026-03-01 16:30:43 -08:00
Gregory Schier
2d99e26f19 plugin-events: route model/find requests through shared handler (#409) 2026-02-28 14:16:32 -08:00
Gregory Schier
da1e04d99e Fix Copy as gRPCurl with template-tag payloads (#413) 2026-02-28 07:39:44 -08:00
dependabot[bot]
d875eaa5bf Bump minimatch from 3.1.2 to 3.1.5 (#411)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-28 07:29:04 -08:00
Gregory Schier
5fa2469cd6 Some cleanup 2026-02-28 07:24:30 -08:00
Gregory Schier
49053cb423 Fix [object Object] request descriptions after OpenAPI import (#412) 2026-02-27 15:36:46 -08:00
Gregory Schier
37d0cabb22 fix: preserve drive letter in Windows plugin paths (#410)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 16:23:24 -08:00
Gregory Schier
435ee54140 docs: clarify app vs cli tag naming 2026-02-26 09:40:58 -08:00
dependabot[bot]
407f2c9921 Bump rollup from 4.55.1 to 4.59.0 (#406)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 09:31:25 -08:00
Gregory Schier
3a6630a14d tests(cli): prevent request send test teardown hang (#408) 2026-02-26 08:58:13 -08:00
39 changed files with 865 additions and 580 deletions

View File

@@ -1,62 +0,0 @@
---
description: Review a PR in a new worktree
allowed-tools: Bash(git worktree:*), Bash(gh pr:*), Bash(git branch:*)
---
Check out a GitHub pull request for review.
## Usage
```
/check-out-pr <PR_NUMBER>
```
## What to do
1. If no PR number is provided, list all open pull requests and ask the user to select one
2. Get PR information using `gh pr view <PR_NUMBER> --json number,headRefName`
3. **Ask the user** whether they want to:
- **A) Check out in current directory** — simple `gh pr checkout <PR_NUMBER>`
- **B) Create a new worktree** — isolated copy at `../yaak-worktrees/pr-<PR_NUMBER>`
4. Follow the appropriate path below
## Option A: Check out in current directory
1. Run `gh pr checkout <PR_NUMBER>`
2. Inform the user which branch they're now on
## Option B: Create a new worktree
1. Create a new worktree at `../yaak-worktrees/pr-<PR_NUMBER>` using `git worktree add` with a timeout of at least 300000ms (5 minutes) since the post-checkout hook runs a bootstrap script
2. Checkout the PR branch in the new worktree using `gh pr checkout <PR_NUMBER>`
3. The post-checkout hook will automatically:
- Create `.env.local` with unique ports
- Copy editor config folders
- Run `npm install && npm run bootstrap`
4. Inform the user:
- Where the worktree was created
- What ports were assigned
- How to access it (cd command)
- How to run the dev server
- How to remove the worktree when done
### Example worktree output
```
Created worktree for PR #123 at ../yaak-worktrees/pr-123
Branch: feature-auth
Ports: Vite (1421), MCP (64344)
To start working:
cd ../yaak-worktrees/pr-123
npm run app-dev
To remove when done:
git worktree remove ../yaak-worktrees/pr-123
```
## Error Handling
- If the PR doesn't exist, show a helpful error
- If the worktree already exists, inform the user and ask if they want to remove and recreate it
- If `gh` CLI is not available, inform the user to install it

View File

@@ -1,35 +0,0 @@
# Worktree Management Skill
## Creating Worktrees
When creating git worktrees for this project, ALWAYS use the path format:
```
../yaak-worktrees/<NAME>
```
For example:
- `git worktree add ../yaak-worktrees/feature-auth`
- `git worktree add ../yaak-worktrees/bugfix-login`
- `git worktree add ../yaak-worktrees/refactor-api`
## What Happens Automatically
The post-checkout hook will automatically:
1. Create `.env.local` with unique ports (YAAK_DEV_PORT and YAAK_PLUGIN_MCP_SERVER_PORT)
2. Copy gitignored editor config folders (.zed, .idea, etc.)
3. Run `npm install && npm run bootstrap`
## Deleting Worktrees
```bash
git worktree remove ../yaak-worktrees/<NAME>
```
## Port Assignments
- Main worktree: 1420 (Vite), 64343 (MCP)
- First worktree: 1421, 64344
- Second worktree: 1422, 64345
- etc.
Each worktree can run `npm run app-dev` simultaneously without conflicts.

View File

@@ -1,46 +0,0 @@
---
name: release-check-out-pr
description: Check out a GitHub pull request for review in this repo, either in the current directory or in a new isolated worktree at ../yaak-worktrees/pr-<PR_NUMBER>. Use when asked to run or replace the old Claude check-out-pr command.
---
# Check Out PR
Check out a PR by number and let the user choose between current-directory checkout and isolated worktree checkout.
## Workflow
1. Confirm `gh` CLI is available.
2. If no PR number is provided, list open PRs (`gh pr list`) and ask the user to choose one.
3. Read PR metadata:
- `gh pr view <PR_NUMBER> --json number,headRefName`
4. Ask the user to choose:
- Option A: check out in the current directory
- Option B: create a new worktree at `../yaak-worktrees/pr-<PR_NUMBER>`
## Option A: Current Directory
1. Run:
- `gh pr checkout <PR_NUMBER>`
2. Report the checked-out branch.
## Option B: New Worktree
1. Use path:
- `../yaak-worktrees/pr-<PR_NUMBER>`
2. Create the worktree with a timeout of at least 5 minutes because checkout hooks run bootstrap.
3. In the new worktree, run:
- `gh pr checkout <PR_NUMBER>`
4. Report:
- Worktree path
- Assigned ports from `.env.local` if present
- How to start work:
- `cd ../yaak-worktrees/pr-<PR_NUMBER>`
- `npm run app-dev`
- How to remove when done:
- `git worktree remove ../yaak-worktrees/pr-<PR_NUMBER>`
## Error Handling
- If PR does not exist, show a clear error.
- If worktree already exists, ask whether to reuse it or remove/recreate it.
- If `gh` is missing, instruct the user to install/authenticate it.

View File

@@ -1,37 +0,0 @@
---
name: worktree-management
description: Manage Yaak git worktrees using the standard ../yaak-worktrees/<NAME> layout, including creation, removal, and expected automatic setup behavior and port assignments.
---
# Worktree Management
Use the Yaak-standard worktree path layout and lifecycle commands.
## Path Convention
Always create worktrees under:
`../yaak-worktrees/<NAME>`
Examples:
- `git worktree add ../yaak-worktrees/feature-auth`
- `git worktree add ../yaak-worktrees/bugfix-login`
- `git worktree add ../yaak-worktrees/refactor-api`
## Automatic Setup After Checkout
Project git hooks automatically:
1. Create `.env.local` with unique `YAAK_DEV_PORT` and `YAAK_PLUGIN_MCP_SERVER_PORT`
2. Copy gitignored editor config folders
3. Run `npm install && npm run bootstrap`
## Remove Worktree
`git worktree remove ../yaak-worktrees/<NAME>`
## Port Pattern
- Main worktree: Vite `1420`, MCP `64343`
- First extra worktree: `1421`, `64344`
- Second extra worktree: `1422`, `64345`
- Continue incrementally for additional worktrees

3
.gitignore vendored
View File

@@ -54,3 +54,6 @@ flatpak/node-sources.json
# Local Codex desktop env state # Local Codex desktop env state
.codex/environments/environment.toml .codex/environments/environment.toml
# Claude Code local settings
.claude/settings.local.json

2
AGENTS.md Normal file
View File

@@ -0,0 +1,2 @@
- 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

1
Cargo.lock generated
View File

@@ -10359,7 +10359,6 @@ dependencies = [
"md5 0.7.0", "md5 0.7.0",
"path-slash", "path-slash",
"rand 0.9.1", "rand 0.9.1",
"regex 1.11.1",
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",

View File

@@ -38,16 +38,12 @@ impl CliContext {
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id)); let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
let plugin_manager = if with_plugins { let plugin_manager = if with_plugins {
let embedded_vendored_plugin_dir = data_dir.join("vendored-plugins"); let vendored_plugin_dir = data_dir.join("vendored-plugins");
let bundled_plugin_dir =
resolve_bundled_plugin_dir_for_cli(&embedded_vendored_plugin_dir);
let installed_plugin_dir = data_dir.join("installed-plugins"); let installed_plugin_dir = data_dir.join("installed-plugins");
let node_bin_path = PathBuf::from("node"); let node_bin_path = PathBuf::from("node");
if bundled_plugin_dir == embedded_vendored_plugin_dir { prepare_embedded_vendored_plugins(&vendored_plugin_dir)
prepare_embedded_vendored_plugins(&embedded_vendored_plugin_dir)
.expect("Failed to prepare bundled plugins"); .expect("Failed to prepare bundled plugins");
}
let plugin_runtime_main = let plugin_runtime_main =
std::env::var("YAAK_PLUGIN_RUNTIME").map(PathBuf::from).unwrap_or_else(|_| { std::env::var("YAAK_PLUGIN_RUNTIME").map(PathBuf::from).unwrap_or_else(|_| {
@@ -56,13 +52,13 @@ impl CliContext {
}); });
match PluginManager::new( match PluginManager::new(
bundled_plugin_dir, vendored_plugin_dir,
embedded_vendored_plugin_dir,
installed_plugin_dir, installed_plugin_dir,
node_bin_path, node_bin_path,
plugin_runtime_main, plugin_runtime_main,
&query_manager, &query_manager,
&PluginContext::new_empty(), &PluginContext::new_empty(),
false,
) )
.await .await
{ {
@@ -135,20 +131,3 @@ fn prepare_embedded_vendored_plugins(vendored_plugin_dir: &Path) -> std::io::Res
EMBEDDED_VENDORED_PLUGINS.extract(vendored_plugin_dir)?; EMBEDDED_VENDORED_PLUGINS.extract(vendored_plugin_dir)?;
Ok(()) Ok(())
} }
fn resolve_bundled_plugin_dir_for_cli(embedded_vendored_plugin_dir: &Path) -> PathBuf {
if !cfg!(debug_assertions) {
return embedded_vendored_plugin_dir.to_path_buf();
}
let plugins_dir = match std::env::current_dir() {
Ok(cwd) => cwd.join("plugins"),
Err(_) => return embedded_vendored_plugin_dir.to_path_buf(),
};
if !plugins_dir.is_dir() {
return embedded_vendored_plugin_dir.to_path_buf();
}
plugins_dir.canonicalize().unwrap_or(plugins_dir)
}

View File

@@ -1,9 +1,14 @@
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::net::TcpListener; use std::net::{SocketAddr, TcpListener, TcpStream};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread; use std::thread;
use std::time::Duration;
pub struct TestHttpServer { pub struct TestHttpServer {
pub url: String, pub url: String,
addr: SocketAddr,
shutdown: Arc<AtomicBool>,
handle: Option<thread::JoinHandle<()>>, handle: Option<thread::JoinHandle<()>>,
} }
@@ -12,10 +17,17 @@ impl TestHttpServer {
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind test HTTP server"); let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind test HTTP server");
let addr = listener.local_addr().expect("Failed to get local addr"); let addr = listener.local_addr().expect("Failed to get local addr");
let url = format!("http://{addr}/test"); let url = format!("http://{addr}/test");
listener.set_nonblocking(true).expect("Failed to set test server listener nonblocking");
let shutdown = Arc::new(AtomicBool::new(false));
let shutdown_signal = Arc::clone(&shutdown);
let body_bytes = body.as_bytes().to_vec(); let body_bytes = body.as_bytes().to_vec();
let handle = thread::spawn(move || { let handle = thread::spawn(move || {
if let Ok((mut stream, _)) = listener.accept() { while !shutdown_signal.load(Ordering::Relaxed) {
match listener.accept() {
Ok((mut stream, _)) => {
let _ = stream.set_read_timeout(Some(Duration::from_secs(1)));
let mut request_buf = [0u8; 4096]; let mut request_buf = [0u8; 4096];
let _ = stream.read(&mut request_buf); let _ = stream.read(&mut request_buf);
@@ -26,15 +38,25 @@ impl TestHttpServer {
let _ = stream.write_all(response.as_bytes()); let _ = stream.write_all(response.as_bytes());
let _ = stream.write_all(&body_bytes); let _ = stream.write_all(&body_bytes);
let _ = stream.flush(); let _ = stream.flush();
break;
}
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(10));
}
Err(_) => break,
}
} }
}); });
Self { url, handle: Some(handle) } Self { url, addr, shutdown, handle: Some(handle) }
} }
} }
impl Drop for TestHttpServer { impl Drop for TestHttpServer {
fn drop(&mut self) { fn drop(&mut self) {
self.shutdown.store(true, Ordering::Relaxed);
let _ = TcpStream::connect(self.addr);
if let Some(handle) = self.handle.take() { if let Some(handle) = self.handle.take() {
let _ = handle.join(); let _ = handle.join();
} }

View File

@@ -37,8 +37,8 @@ use yaak_grpc::{Code, ServiceDefinition, serialize_message};
use yaak_mac_window::AppHandleMacWindowExt; 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, Workspace,
Workspace, WorkspaceMeta, 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::{
@@ -1345,29 +1345,6 @@ async fn cmd_send_http_request<R: Runtime>(
Ok(r) Ok(r)
} }
#[tauri::command]
async fn cmd_install_plugin<R: Runtime>(
directory: &str,
url: Option<String>,
plugin_manager: State<'_, PluginManager>,
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
) -> YaakResult<Plugin> {
let plugin = app_handle.db().upsert_plugin(
&Plugin { directory: directory.into(), url, enabled: true, ..Default::default() },
&UpdateSource::from_window_label(window.label()),
)?;
plugin_manager
.add_plugin(
&PluginContext::new(Some(window.label().to_string()), window.workspace_id()),
&plugin,
)
.await?;
Ok(plugin)
}
#[tauri::command] #[tauri::command]
async fn cmd_reload_plugins<R: Runtime>( async fn cmd_reload_plugins<R: Runtime>(
app_handle: AppHandle<R>, app_handle: AppHandle<R>,
@@ -1652,7 +1629,6 @@ pub fn run() {
cmd_workspace_actions, cmd_workspace_actions,
cmd_folder_actions, cmd_folder_actions,
cmd_import_data, cmd_import_data,
cmd_install_plugin,
cmd_metadata, cmd_metadata,
cmd_new_child_window, cmd_new_child_window,
cmd_new_main_window, cmd_new_main_window,
@@ -1721,6 +1697,7 @@ pub fn run() {
git_ext::cmd_git_rm_remote, git_ext::cmd_git_rm_remote,
// //
// Plugin commands // Plugin commands
plugins_ext::cmd_plugins_install_from_directory,
plugins_ext::cmd_plugins_search, plugins_ext::cmd_plugins_search,
plugins_ext::cmd_plugins_install, plugins_ext::cmd_plugins_install,
plugins_ext::cmd_plugins_uninstall, plugins_ext::cmd_plugins_uninstall,

View File

@@ -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
{
let db = window.db();
l.push(db.get_settings().into()); l.push(db.get_settings().into());
// Add global models
l.append(&mut db.list_workspaces()?.into_iter().map(Into::into).collect()); 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()); l.append(&mut db.list_key_values()?.into_iter().map(Into::into).collect());
l.append(&mut db.list_plugins()?.into_iter().map(Into::into).collect()); }
let plugins = {
let db = window.db();
db.list_plugins()?
};
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());

View File

@@ -19,13 +19,13 @@ use yaak::plugin_events::{
GroupedPluginEvent, HostRequest, SharedPluginEventContext, handle_shared_plugin_event, GroupedPluginEvent, HostRequest, SharedPluginEventContext, handle_shared_plugin_event,
}; };
use yaak_crypto::manager::EncryptionManager; use yaak_crypto::manager::EncryptionManager;
use yaak_models::models::{AnyModel, HttpResponse, Plugin}; use yaak_models::models::{HttpResponse, Plugin};
use yaak_models::queries::any_request::AnyRequest; use yaak_models::queries::any_request::AnyRequest;
use yaak_models::util::UpdateSource; use yaak_models::util::UpdateSource;
use yaak_plugins::error::Error::PluginErr; use yaak_plugins::error::Error::PluginErr;
use yaak_plugins::events::{ use yaak_plugins::events::{
Color, EmptyPayload, ErrorResponse, FindHttpResponsesResponse, GetCookieValueResponse, Icon, Color, EmptyPayload, ErrorResponse, GetCookieValueResponse, Icon, InternalEvent,
InternalEvent, InternalEventPayload, ListCookieNamesResponse, ListOpenWorkspacesResponse, InternalEventPayload, ListCookieNamesResponse, ListOpenWorkspacesResponse,
RenderGrpcRequestResponse, RenderHttpRequestResponse, SendHttpRequestResponse, RenderGrpcRequestResponse, RenderHttpRequestResponse, SendHttpRequestResponse,
ShowToastRequest, TemplateRenderResponse, WindowInfoResponse, WindowNavigateEvent, ShowToastRequest, TemplateRenderResponse, WindowInfoResponse, WindowNavigateEvent,
WorkspaceInfo, WorkspaceInfo,
@@ -190,71 +190,6 @@ async fn handle_host_plugin_request<R: Runtime>(
Ok(None) Ok(None)
} }
} }
HostRequest::FindHttpResponses(req) => {
let http_responses = app_handle
.db()
.list_http_responses_for_request(&req.request_id, req.limit.map(|l| l as u64))
.unwrap_or_default();
Ok(Some(InternalEventPayload::FindHttpResponsesResponse(FindHttpResponsesResponse {
http_responses,
})))
}
HostRequest::UpsertModel(req) => {
use AnyModel::*;
let model = match &req.model {
HttpRequest(m) => {
HttpRequest(app_handle.db().upsert_http_request(m, &UpdateSource::Plugin)?)
}
GrpcRequest(m) => {
GrpcRequest(app_handle.db().upsert_grpc_request(m, &UpdateSource::Plugin)?)
}
WebsocketRequest(m) => WebsocketRequest(
app_handle.db().upsert_websocket_request(m, &UpdateSource::Plugin)?,
),
Folder(m) => Folder(app_handle.db().upsert_folder(m, &UpdateSource::Plugin)?),
Environment(m) => {
Environment(app_handle.db().upsert_environment(m, &UpdateSource::Plugin)?)
}
Workspace(m) => {
Workspace(app_handle.db().upsert_workspace(m, &UpdateSource::Plugin)?)
}
_ => {
return Err(PluginErr("Upsert not supported for this model type".into()).into());
}
};
Ok(Some(InternalEventPayload::UpsertModelResponse(
yaak_plugins::events::UpsertModelResponse { model },
)))
}
HostRequest::DeleteModel(req) => {
let model = match req.model.as_str() {
"http_request" => AnyModel::HttpRequest(
app_handle.db().delete_http_request_by_id(&req.id, &UpdateSource::Plugin)?,
),
"grpc_request" => AnyModel::GrpcRequest(
app_handle.db().delete_grpc_request_by_id(&req.id, &UpdateSource::Plugin)?,
),
"websocket_request" => AnyModel::WebsocketRequest(
app_handle
.db()
.delete_websocket_request_by_id(&req.id, &UpdateSource::Plugin)?,
),
"folder" => AnyModel::Folder(
app_handle.db().delete_folder_by_id(&req.id, &UpdateSource::Plugin)?,
),
"environment" => AnyModel::Environment(
app_handle.db().delete_environment_by_id(&req.id, &UpdateSource::Plugin)?,
),
_ => {
return Err(PluginErr("Delete not supported for this model type".into()).into());
}
};
Ok(Some(InternalEventPayload::DeleteModelResponse(
yaak_plugins::events::DeleteModelResponse { model },
)))
}
HostRequest::RenderGrpcRequest(req) => { HostRequest::RenderGrpcRequest(req) => {
let window = get_window_from_plugin_context(app_handle, plugin_context)?; let window = get_window_from_plugin_context(app_handle, plugin_context)?;

View File

@@ -10,7 +10,6 @@ use crate::error::Result;
use crate::models_ext::QueryManagerExt; use crate::models_ext::QueryManagerExt;
use log::{error, info, warn}; use log::{error, info, warn};
use serde::Serialize; use serde::Serialize;
use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -23,7 +22,8 @@ use tauri::{
use tokio::sync::Mutex; use tokio::sync::Mutex;
use ts_rs::TS; use ts_rs::TS;
use yaak_api::yaak_api_client; use yaak_api::yaak_api_client;
use yaak_models::models::Plugin; use yaak_models::models::{Plugin, PluginSource};
use yaak_models::util::UpdateSource;
use yaak_plugins::api::{ use yaak_plugins::api::{
PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse, check_plugin_updates, PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse, check_plugin_updates,
search_plugins, search_plugins,
@@ -165,6 +165,28 @@ pub async fn cmd_plugins_install<R: Runtime>(
Ok(()) Ok(())
} }
#[command]
pub async fn cmd_plugins_install_from_directory<R: Runtime>(
window: WebviewWindow<R>,
directory: &str,
) -> Result<Plugin> {
let plugin = window.db().upsert_plugin(
&Plugin {
directory: directory.into(),
url: None,
enabled: true,
source: PluginSource::Filesystem,
..Default::default()
},
&UpdateSource::from_window_label(window.label()),
)?;
let plugin_manager = Arc::new((*window.state::<PluginManager>()).clone());
plugin_manager.add_plugin(&window.plugin_context(), &plugin).await?;
Ok(plugin)
}
#[command] #[command]
pub async fn cmd_plugins_uninstall<R: Runtime>( pub async fn cmd_plugins_uninstall<R: Runtime>(
plugin_id: &str, plugin_id: &str,
@@ -244,11 +266,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
.path() .path()
.resolve("vendored/plugins", BaseDirectory::Resource) .resolve("vendored/plugins", BaseDirectory::Resource)
.expect("failed to resolve plugin directory resource"); .expect("failed to resolve plugin directory resource");
let bundled_plugin_dir = if is_dev() {
resolve_workspace_plugins_dir().unwrap_or_else(|| vendored_plugin_dir.clone())
} else {
vendored_plugin_dir.clone()
};
let installed_plugin_dir = app_handle let installed_plugin_dir = app_handle
.path() .path()
@@ -272,6 +289,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
.expect("failed to resolve plugin runtime") .expect("failed to resolve plugin runtime")
.join("index.cjs"); .join("index.cjs");
let dev_mode = is_dev();
let query_manager = let query_manager =
app_handle.state::<yaak_models::query_manager::QueryManager>().inner().clone(); app_handle.state::<yaak_models::query_manager::QueryManager>().inner().clone();
@@ -279,13 +297,13 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
let app_handle_clone = app_handle.clone(); let app_handle_clone = app_handle.clone();
tauri::async_runtime::block_on(async move { tauri::async_runtime::block_on(async move {
let manager = PluginManager::new( let manager = PluginManager::new(
bundled_plugin_dir,
vendored_plugin_dir, vendored_plugin_dir,
installed_plugin_dir, installed_plugin_dir,
node_bin_path, node_bin_path,
plugin_runtime_main, plugin_runtime_main,
&query_manager, &query_manager,
&PluginContext::new_empty(), &PluginContext::new_empty(),
dev_mode,
) )
.await .await
.expect("Failed to initialize plugins"); .expect("Failed to initialize plugins");
@@ -327,11 +345,3 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
}) })
.build() .build()
} }
fn resolve_workspace_plugins_dir() -> Option<PathBuf> {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.join("plugins")
.canonicalize()
.ok()
}

View File

@@ -1,7 +1,6 @@
use crate::client::HttpConnectionOptions; use crate::client::HttpConnectionOptions;
use crate::dns::LocalhostResolver; use crate::dns::LocalhostResolver;
use crate::error::Result; use crate::error::Result;
use log::info;
use reqwest::Client; use reqwest::Client;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::sync::Arc; use std::sync::Arc;

View File

@@ -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, };

View File

@@ -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);

View File

@@ -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(),
}) })
} }
} }

View File

@@ -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)
} }
} }

View File

@@ -15,7 +15,6 @@ log = { workspace = true }
md5 = "0.7.0" md5 = "0.7.0"
path-slash = "0.2.1" path-slash = "0.2.1"
rand = "0.9.0" rand = "0.9.0"
regex = "1.10.6"
reqwest = { workspace = true, features = ["json"] } reqwest = { workspace = true, features = ["json"] }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }

View File

@@ -24,3 +24,7 @@ export async function checkPluginUpdates() {
export async function updateAllPlugins() { export async function updateAllPlugins() {
return invoke<PluginNameVersion[]>('cmd_plugins_update_all', {}); return invoke<PluginNameVersion[]>('cmd_plugins_update_all', {});
} }
export async function installPluginFromDirectory(directory: string) {
return invoke<void>('cmd_plugins_install_from_directory', { directory });
}

View File

@@ -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) => {

View File

@@ -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,

View File

@@ -21,9 +21,11 @@ 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::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@@ -32,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;
@@ -45,9 +47,9 @@ pub struct PluginManager {
kill_tx: tokio::sync::watch::Sender<bool>, kill_tx: tokio::sync::watch::Sender<bool>,
killed_rx: Arc<Mutex<Option<oneshot::Receiver<()>>>>, killed_rx: Arc<Mutex<Option<oneshot::Receiver<()>>>>,
ws_service: Arc<PluginRuntimeServerWebsocket>, ws_service: Arc<PluginRuntimeServerWebsocket>,
bundled_plugin_dir: PathBuf,
vendored_plugin_dir: PathBuf, vendored_plugin_dir: PathBuf,
pub(crate) installed_plugin_dir: PathBuf, pub(crate) installed_plugin_dir: PathBuf,
dev_mode: bool,
} }
/// Callback for plugin initialization events (e.g., toast notifications) /// Callback for plugin initialization events (e.g., toast notifications)
@@ -57,21 +59,21 @@ impl PluginManager {
/// Create a new PluginManager with the given paths. /// Create a new PluginManager with the given paths.
/// ///
/// # Arguments /// # Arguments
/// * `bundled_plugin_dir` - Directory to scan for bundled plugins
/// * `vendored_plugin_dir` - Path to vendored plugins directory /// * `vendored_plugin_dir` - Path to vendored plugins directory
/// * `installed_plugin_dir` - Path to installed plugins directory /// * `installed_plugin_dir` - Path to installed plugins directory
/// * `node_bin_path` - Path to the yaaknode binary /// * `node_bin_path` - Path to the yaaknode binary
/// * `plugin_runtime_main` - Path to the plugin runtime index.cjs /// * `plugin_runtime_main` - Path to the plugin runtime index.cjs
/// * `query_manager` - Query manager for bundled plugin registration and loading /// * `query_manager` - Query manager for bundled plugin registration and loading
/// * `plugin_context` - Context to use while initializing plugins /// * `plugin_context` - Context to use while initializing plugins
/// * `dev_mode` - Whether the app is in dev mode (affects plugin loading)
pub async fn new( pub async fn new(
bundled_plugin_dir: PathBuf,
vendored_plugin_dir: PathBuf, vendored_plugin_dir: PathBuf,
installed_plugin_dir: PathBuf, installed_plugin_dir: PathBuf,
node_bin_path: PathBuf, node_bin_path: PathBuf,
plugin_runtime_main: PathBuf, plugin_runtime_main: PathBuf,
query_manager: &QueryManager, query_manager: &QueryManager,
plugin_context: &PluginContext, plugin_context: &PluginContext,
dev_mode: bool,
) -> Result<PluginManager> { ) -> Result<PluginManager> {
let (events_tx, mut events_rx) = mpsc::channel(2048); let (events_tx, mut events_rx) = mpsc::channel(2048);
let (kill_server_tx, kill_server_rx) = tokio::sync::watch::channel(false); let (kill_server_tx, kill_server_rx) = tokio::sync::watch::channel(false);
@@ -88,9 +90,9 @@ impl PluginManager {
ws_service: Arc::new(ws_service.clone()), ws_service: Arc::new(ws_service.clone()),
kill_tx: kill_server_tx, kill_tx: kill_server_tx,
killed_rx: Arc::new(Mutex::new(Some(killed_rx))), killed_rx: Arc::new(Mutex::new(Some(killed_rx))),
bundled_plugin_dir,
vendored_plugin_dir, vendored_plugin_dir,
installed_plugin_dir, installed_plugin_dir,
dev_mode,
}; };
// Forward events to subscribers // Forward events to subscribers
@@ -161,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,
@@ -191,11 +194,76 @@ impl PluginManager {
Ok(plugin_manager) Ok(plugin_manager)
} }
/// Get the vendored plugin directory path (resolves dev mode path if applicable)
pub fn get_plugins_dir(&self) -> PathBuf {
if self.dev_mode {
// Use plugins directly for easy development
// Tauri runs from crates-tauri/yaak-app/, so go up two levels to reach project root
env::current_dir()
.map(|cwd| cwd.join("../../plugins").canonicalize().unwrap())
.unwrap_or_else(|_| self.vendored_plugin_dir.clone())
} else {
self.vendored_plugin_dir.clone()
}
}
/// Read plugin directories from disk and return their paths. /// Read plugin directories from disk and return their paths.
/// This is useful for discovering bundled plugins. /// This is useful for discovering bundled plugins.
pub async fn list_bundled_plugin_dirs(&self) -> Result<Vec<String>> { pub async fn list_bundled_plugin_dirs(&self) -> Result<Vec<String>> {
info!("Loading bundled plugins from {:?}", self.bundled_plugin_dir); let plugins_dir = self.get_plugins_dir();
read_plugins_dir(&self.bundled_plugin_dir).await info!("Loading bundled plugins from {plugins_dir:?}");
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<()> {
@@ -272,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,
@@ -282,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()));
@@ -1048,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![];
@@ -1066,16 +1156,10 @@ async fn read_plugins_dir(dir: &PathBuf) -> Result<Vec<String>> {
fn fix_windows_paths(p: &PathBuf) -> String { fn fix_windows_paths(p: &PathBuf) -> String {
use dunce; use dunce;
use path_slash::PathBufExt; use path_slash::PathBufExt;
use regex::Regex;
// 1. Remove UNC prefix for Windows paths to pass to sidecar // 1. Remove UNC prefix for Windows paths
let safe_path = dunce::simplified(p.as_path()).to_string_lossy().to_string(); let safe_path = dunce::simplified(p.as_path());
// 2. Remove the drive letter // 2. Convert backslashes to forward slashes for Node.js compatibility
let safe_path = Regex::new("^[a-zA-Z]:").unwrap().replace(safe_path.as_str(), ""); PathBuf::from(safe_path).to_slash_lossy().to_string()
// 3. Convert backslashes to forward
let safe_path = PathBuf::from(safe_path.to_string()).to_slash_lossy().to_string();
safe_path
} }

View File

@@ -1,5 +1,5 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export function unescape_template(template: string): any;
export function parse_template(template: string): any;
export function escape_template(template: string): any; export function escape_template(template: string): any;
export function parse_template(template: string): any;
export function unescape_template(template: string): any;

View File

@@ -165,10 +165,10 @@ function takeFromExternrefTable0(idx) {
* @param {string} template * @param {string} template
* @returns {any} * @returns {any}
*/ */
export function unescape_template(template) { export function escape_template(template) {
const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN; const len0 = WASM_VECTOR_LEN;
const ret = wasm.unescape_template(ptr0, len0); const ret = wasm.escape_template(ptr0, len0);
if (ret[2]) { if (ret[2]) {
throw takeFromExternrefTable0(ret[1]); throw takeFromExternrefTable0(ret[1]);
} }
@@ -193,10 +193,10 @@ export function parse_template(template) {
* @param {string} template * @param {string} template
* @returns {any} * @returns {any}
*/ */
export function escape_template(template) { export function unescape_template(template) {
const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN; const len0 = WASM_VECTOR_LEN;
const ret = wasm.escape_template(ptr0, len0); const ret = wasm.unescape_template(ptr0, len0);
if (ret[2]) { if (ret[2]) {
throw takeFromExternrefTable0(ret[1]); throw takeFromExternrefTable0(ret[1]);
} }

Binary file not shown.

View File

@@ -1,14 +1,16 @@
use yaak_models::models::AnyModel;
use yaak_models::query_manager::QueryManager; use yaak_models::query_manager::QueryManager;
use yaak_models::util::UpdateSource;
use yaak_plugins::events::{ use yaak_plugins::events::{
CloseWindowRequest, CopyTextRequest, DeleteKeyValueRequest, DeleteKeyValueResponse, CloseWindowRequest, CopyTextRequest, DeleteKeyValueRequest, DeleteKeyValueResponse,
DeleteModelRequest, ErrorResponse, FindHttpResponsesRequest, GetCookieValueRequest, DeleteModelRequest, DeleteModelResponse, ErrorResponse, FindHttpResponsesRequest,
GetHttpRequestByIdRequest, GetHttpRequestByIdResponse, GetKeyValueRequest, GetKeyValueResponse, FindHttpResponsesResponse, GetCookieValueRequest, GetHttpRequestByIdRequest,
InternalEventPayload, ListCookieNamesRequest, ListFoldersRequest, ListFoldersResponse, GetHttpRequestByIdResponse, GetKeyValueRequest, GetKeyValueResponse, InternalEventPayload,
ListHttpRequestsRequest, ListHttpRequestsResponse, ListOpenWorkspacesRequest, ListCookieNamesRequest, ListFoldersRequest, ListFoldersResponse, ListHttpRequestsRequest,
OpenExternalUrlRequest, OpenWindowRequest, PromptFormRequest, PromptTextRequest, ListHttpRequestsResponse, ListOpenWorkspacesRequest, OpenExternalUrlRequest, OpenWindowRequest,
ReloadResponse, RenderGrpcRequestRequest, RenderHttpRequestRequest, SendHttpRequestRequest, PromptFormRequest, PromptTextRequest, ReloadResponse, RenderGrpcRequestRequest,
SetKeyValueRequest, ShowToastRequest, TemplateRenderRequest, UpsertModelRequest, RenderHttpRequestRequest, SendHttpRequestRequest, SetKeyValueRequest, ShowToastRequest,
WindowInfoRequest, TemplateRenderRequest, UpsertModelRequest, UpsertModelResponse, WindowInfoRequest,
}; };
pub struct SharedPluginEventContext<'a> { pub struct SharedPluginEventContext<'a> {
@@ -37,6 +39,9 @@ pub enum SharedRequest<'a> {
GetHttpRequestById(&'a GetHttpRequestByIdRequest), GetHttpRequestById(&'a GetHttpRequestByIdRequest),
ListFolders(&'a ListFoldersRequest), ListFolders(&'a ListFoldersRequest),
ListHttpRequests(&'a ListHttpRequestsRequest), ListHttpRequests(&'a ListHttpRequestsRequest),
FindHttpResponses(&'a FindHttpResponsesRequest),
UpsertModel(&'a UpsertModelRequest),
DeleteModel(&'a DeleteModelRequest),
} }
#[derive(Debug)] #[derive(Debug)]
@@ -45,9 +50,6 @@ pub enum HostRequest<'a> {
CopyText(&'a CopyTextRequest), CopyText(&'a CopyTextRequest),
PromptText(&'a PromptTextRequest), PromptText(&'a PromptTextRequest),
PromptForm(&'a PromptFormRequest), PromptForm(&'a PromptFormRequest),
FindHttpResponses(&'a FindHttpResponsesRequest),
UpsertModel(&'a UpsertModelRequest),
DeleteModel(&'a DeleteModelRequest),
RenderGrpcRequest(&'a RenderGrpcRequestRequest), RenderGrpcRequest(&'a RenderGrpcRequestRequest),
RenderHttpRequest(&'a RenderHttpRequestRequest), RenderHttpRequest(&'a RenderHttpRequestRequest),
TemplateRender(&'a TemplateRenderRequest), TemplateRender(&'a TemplateRenderRequest),
@@ -71,9 +73,6 @@ impl HostRequest<'_> {
HostRequest::CopyText(_) => "copy_text_request".to_string(), HostRequest::CopyText(_) => "copy_text_request".to_string(),
HostRequest::PromptText(_) => "prompt_text_request".to_string(), HostRequest::PromptText(_) => "prompt_text_request".to_string(),
HostRequest::PromptForm(_) => "prompt_form_request".to_string(), HostRequest::PromptForm(_) => "prompt_form_request".to_string(),
HostRequest::FindHttpResponses(_) => "find_http_responses_request".to_string(),
HostRequest::UpsertModel(_) => "upsert_model_request".to_string(),
HostRequest::DeleteModel(_) => "delete_model_request".to_string(),
HostRequest::RenderGrpcRequest(_) => "render_grpc_request_request".to_string(), HostRequest::RenderGrpcRequest(_) => "render_grpc_request_request".to_string(),
HostRequest::RenderHttpRequest(_) => "render_http_request_request".to_string(), HostRequest::RenderHttpRequest(_) => "render_http_request_request".to_string(),
HostRequest::TemplateRender(_) => "template_render_request".to_string(), HostRequest::TemplateRender(_) => "template_render_request".to_string(),
@@ -135,13 +134,13 @@ impl<'a> From<&'a InternalEventPayload> for GroupedPluginRequest<'a> {
GroupedPluginRequest::Host(HostRequest::PromptForm(req)) GroupedPluginRequest::Host(HostRequest::PromptForm(req))
} }
InternalEventPayload::FindHttpResponsesRequest(req) => { InternalEventPayload::FindHttpResponsesRequest(req) => {
GroupedPluginRequest::Host(HostRequest::FindHttpResponses(req)) GroupedPluginRequest::Shared(SharedRequest::FindHttpResponses(req))
} }
InternalEventPayload::UpsertModelRequest(req) => { InternalEventPayload::UpsertModelRequest(req) => {
GroupedPluginRequest::Host(HostRequest::UpsertModel(req)) GroupedPluginRequest::Shared(SharedRequest::UpsertModel(req))
} }
InternalEventPayload::DeleteModelRequest(req) => { InternalEventPayload::DeleteModelRequest(req) => {
GroupedPluginRequest::Host(HostRequest::DeleteModel(req)) GroupedPluginRequest::Shared(SharedRequest::DeleteModel(req))
} }
InternalEventPayload::RenderGrpcRequestRequest(req) => { InternalEventPayload::RenderGrpcRequestRequest(req) => {
GroupedPluginRequest::Host(HostRequest::RenderGrpcRequest(req)) GroupedPluginRequest::Host(HostRequest::RenderGrpcRequest(req))
@@ -275,17 +274,175 @@ fn build_shared_reply(
http_requests, http_requests,
}) })
} }
SharedRequest::FindHttpResponses(req) => {
let http_responses = query_manager
.connect()
.list_http_responses_for_request(&req.request_id, req.limit.map(|l| l as u64))
.unwrap_or_default();
InternalEventPayload::FindHttpResponsesResponse(FindHttpResponsesResponse {
http_responses,
})
}
SharedRequest::UpsertModel(req) => {
use AnyModel::*;
let model = match &req.model {
HttpRequest(m) => {
match query_manager.connect().upsert_http_request(m, &UpdateSource::Plugin) {
Ok(model) => HttpRequest(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to upsert HTTP request: {err}"),
});
}
}
}
GrpcRequest(m) => {
match query_manager.connect().upsert_grpc_request(m, &UpdateSource::Plugin) {
Ok(model) => GrpcRequest(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to upsert gRPC request: {err}"),
});
}
}
}
WebsocketRequest(m) => {
match query_manager.connect().upsert_websocket_request(m, &UpdateSource::Plugin)
{
Ok(model) => WebsocketRequest(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to upsert WebSocket request: {err}"),
});
}
}
}
Folder(m) => {
match query_manager.connect().upsert_folder(m, &UpdateSource::Plugin) {
Ok(model) => Folder(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to upsert folder: {err}"),
});
}
}
}
Environment(m) => {
match query_manager.connect().upsert_environment(m, &UpdateSource::Plugin) {
Ok(model) => Environment(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to upsert environment: {err}"),
});
}
}
}
Workspace(m) => {
match query_manager.connect().upsert_workspace(m, &UpdateSource::Plugin) {
Ok(model) => Workspace(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to upsert workspace: {err}"),
});
}
}
}
_ => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: "Upsert not supported for this model type".to_string(),
});
}
};
InternalEventPayload::UpsertModelResponse(UpsertModelResponse { model })
}
SharedRequest::DeleteModel(req) => {
let model = match req.model.as_str() {
"http_request" => {
match query_manager
.connect()
.delete_http_request_by_id(&req.id, &UpdateSource::Plugin)
{
Ok(model) => AnyModel::HttpRequest(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to delete HTTP request: {err}"),
});
}
}
}
"grpc_request" => {
match query_manager
.connect()
.delete_grpc_request_by_id(&req.id, &UpdateSource::Plugin)
{
Ok(model) => AnyModel::GrpcRequest(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to delete gRPC request: {err}"),
});
}
}
}
"websocket_request" => {
match query_manager
.connect()
.delete_websocket_request_by_id(&req.id, &UpdateSource::Plugin)
{
Ok(model) => AnyModel::WebsocketRequest(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to delete WebSocket request: {err}"),
});
}
}
}
"folder" => match query_manager
.connect()
.delete_folder_by_id(&req.id, &UpdateSource::Plugin)
{
Ok(model) => AnyModel::Folder(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to delete folder: {err}"),
});
}
},
"environment" => {
match query_manager
.connect()
.delete_environment_by_id(&req.id, &UpdateSource::Plugin)
{
Ok(model) => AnyModel::Environment(model),
Err(err) => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to delete environment: {err}"),
});
}
}
}
_ => {
return InternalEventPayload::ErrorResponse(ErrorResponse {
error: "Delete not supported for this model type".to_string(),
});
}
};
InternalEventPayload::DeleteModelResponse(DeleteModelResponse { model })
}
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use yaak_models::models::{Folder, HttpRequest, Workspace}; use tempfile::TempDir;
use yaak_models::models::{AnyModel, Folder, HttpRequest, Workspace};
use yaak_models::util::UpdateSource; use yaak_models::util::UpdateSource;
fn seed_query_manager() -> QueryManager { fn seed_query_manager() -> (QueryManager, TempDir) {
let temp_dir = tempfile::TempDir::new().expect("Failed to create temp dir"); let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("db.sqlite"); let db_path = temp_dir.path().join("db.sqlite");
let blob_path = temp_dir.path().join("blobs.sqlite"); let blob_path = temp_dir.path().join("blobs.sqlite");
let (query_manager, _blob_manager, _rx) = let (query_manager, _blob_manager, _rx) =
@@ -332,12 +489,12 @@ mod tests {
) )
.expect("Failed to seed request"); .expect("Failed to seed request");
query_manager (query_manager, temp_dir)
} }
#[test] #[test]
fn list_requests_requires_workspace_when_folder_missing() { fn list_requests_requires_workspace_when_folder_missing() {
let query_manager = seed_query_manager(); let (query_manager, _temp_dir) = seed_query_manager();
let payload = InternalEventPayload::ListHttpRequestsRequest( let payload = InternalEventPayload::ListHttpRequestsRequest(
yaak_plugins::events::ListHttpRequestsRequest { folder_id: None }, yaak_plugins::events::ListHttpRequestsRequest { folder_id: None },
); );
@@ -355,7 +512,7 @@ mod tests {
#[test] #[test]
fn list_requests_by_workspace_and_folder() { fn list_requests_by_workspace_and_folder() {
let query_manager = seed_query_manager(); let (query_manager, _temp_dir) = seed_query_manager();
let by_workspace_payload = InternalEventPayload::ListHttpRequestsRequest( let by_workspace_payload = InternalEventPayload::ListHttpRequestsRequest(
yaak_plugins::events::ListHttpRequestsRequest { folder_id: None }, yaak_plugins::events::ListHttpRequestsRequest { folder_id: None },
@@ -394,9 +551,83 @@ mod tests {
} }
} }
#[test]
fn find_http_responses_is_shared_handled() {
let (query_manager, _temp_dir) = seed_query_manager();
let payload = InternalEventPayload::FindHttpResponsesRequest(FindHttpResponsesRequest {
request_id: "rq_test".to_string(),
limit: Some(1),
});
let result = handle_shared_plugin_event(
&query_manager,
&payload,
SharedPluginEventContext { plugin_name: "@yaak/test", workspace_id: Some("wk_test") },
);
match result {
GroupedPluginEvent::Handled(Some(InternalEventPayload::FindHttpResponsesResponse(
resp,
))) => {
assert!(resp.http_responses.is_empty());
}
other => panic!("unexpected find responses result: {other:?}"),
}
}
#[test]
fn upsert_and_delete_model_are_shared_handled() {
let (query_manager, _temp_dir) = seed_query_manager();
let existing = query_manager
.connect()
.get_http_request("rq_test")
.expect("Failed to load seeded request");
let upsert_payload = InternalEventPayload::UpsertModelRequest(UpsertModelRequest {
model: AnyModel::HttpRequest(HttpRequest {
name: "Request Updated".to_string(),
..existing
}),
});
let upsert_result = handle_shared_plugin_event(
&query_manager,
&upsert_payload,
SharedPluginEventContext { plugin_name: "@yaak/test", workspace_id: Some("wk_test") },
);
match upsert_result {
GroupedPluginEvent::Handled(Some(InternalEventPayload::UpsertModelResponse(resp))) => {
match resp.model {
AnyModel::HttpRequest(r) => assert_eq!(r.name, "Request Updated"),
other => panic!("unexpected upsert model type: {other:?}"),
}
}
other => panic!("unexpected upsert result: {other:?}"),
}
let delete_payload = InternalEventPayload::DeleteModelRequest(DeleteModelRequest {
model: "http_request".to_string(),
id: "rq_test".to_string(),
});
let delete_result = handle_shared_plugin_event(
&query_manager,
&delete_payload,
SharedPluginEventContext { plugin_name: "@yaak/test", workspace_id: Some("wk_test") },
);
match delete_result {
GroupedPluginEvent::Handled(Some(InternalEventPayload::DeleteModelResponse(resp))) => {
match resp.model {
AnyModel::HttpRequest(r) => assert_eq!(r.id, "rq_test"),
other => panic!("unexpected delete model type: {other:?}"),
}
}
other => panic!("unexpected delete result: {other:?}"),
}
}
#[test] #[test]
fn host_request_classification_works() { fn host_request_classification_works() {
let query_manager = seed_query_manager(); let (query_manager, _temp_dir) = seed_query_manager();
let payload = InternalEventPayload::WindowInfoRequest(WindowInfoRequest { let payload = InternalEventPayload::WindowInfoRequest(WindowInfoRequest {
label: "main".to_string(), label: "main".to_string(),
}); });

334
package-lock.json generated
View File

@@ -1491,29 +1491,6 @@
"hono": "^4" "hono": "^4"
} }
}, },
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -2242,9 +2219,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
"integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -2256,9 +2233,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
"integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2270,9 +2247,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
"integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2284,9 +2261,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
"integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2298,9 +2275,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
"integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2312,9 +2289,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
"integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2326,9 +2303,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
"integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -2340,9 +2317,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
"integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -2354,9 +2331,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
"integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2368,9 +2345,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
"integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2382,9 +2359,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loong64-gnu": { "node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
"integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -2396,9 +2373,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loong64-musl": { "node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
"integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -2410,9 +2387,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-ppc64-gnu": { "node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
"integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -2424,9 +2401,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-ppc64-musl": { "node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
"integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -2438,9 +2415,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
"integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -2452,9 +2429,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-musl": { "node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
"integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -2466,9 +2443,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
"integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -2480,9 +2457,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
"integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2494,9 +2471,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
"integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2508,9 +2485,9 @@
] ]
}, },
"node_modules/@rollup/rollup-openbsd-x64": { "node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
"integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2522,9 +2499,9 @@
] ]
}, },
"node_modules/@rollup/rollup-openharmony-arm64": { "node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
"integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2536,9 +2513,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
"integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2550,9 +2527,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
"integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -2564,9 +2541,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-gnu": { "node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
"integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2578,9 +2555,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
"integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -5799,17 +5776,40 @@
"npm": ">=10" "npm": ">=10"
} }
}, },
"node_modules/cpx2/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/cpx2/node_modules/brace-expansion": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/cpx2/node_modules/minimatch": { "node_modules/cpx2/node_modules/minimatch": {
"version": "10.1.1", "version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"@isaacs/brace-expansion": "^5.0.0" "brace-expansion": "^5.0.2"
}, },
"engines": { "engines": {
"node": "20 || >=22" "node": "18 || 20 || >=22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
@@ -7636,17 +7636,40 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/glob/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/glob/node_modules/minimatch": { "node_modules/glob/node_modules/minimatch": {
"version": "10.1.1", "version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"@isaacs/brace-expansion": "^5.0.0" "brace-expansion": "^5.0.2"
}, },
"engines": { "engines": {
"node": "20 || >=22" "node": "18 || 20 || >=22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
@@ -10524,9 +10547,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
@@ -12951,6 +12974,29 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/rimraf/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/rimraf/node_modules/brace-expansion": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/rimraf/node_modules/glob": { "node_modules/rimraf/node_modules/glob": {
"version": "13.0.0", "version": "13.0.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz",
@@ -12970,25 +13016,25 @@
} }
}, },
"node_modules/rimraf/node_modules/minimatch": { "node_modules/rimraf/node_modules/minimatch": {
"version": "10.1.1", "version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"@isaacs/brace-expansion": "^5.0.0" "brace-expansion": "^5.0.2"
}, },
"engines": { "engines": {
"node": "20 || >=22" "node": "18 || 20 || >=22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.55.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -13002,31 +13048,31 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm-eabi": "4.59.0",
"@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-android-arm64": "4.59.0",
"@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.59.0",
"@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-darwin-x64": "4.59.0",
"@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.59.0",
"@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.59.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
"@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
"@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.59.0",
"@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.59.0",
"@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.59.0",
"@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.59.0",
"@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
"@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.59.0",
"@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
"@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.59.0",
"@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.59.0",
"@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.59.0",
"@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.59.0",
"@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openbsd-x64": "4.59.0",
"@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.59.0",
"@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.59.0",
"@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.59.0",
"@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.59.0",
"@rollup/rollup-win32-x64-msvc": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.59.0",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },

View File

@@ -11,7 +11,7 @@ export const plugin: PluginDefinition = {
async onSelect(ctx, args) { async onSelect(ctx, args) {
const rendered_request = await ctx.grpcRequest.render({ const rendered_request = await ctx.grpcRequest.render({
grpcRequest: args.grpcRequest, grpcRequest: args.grpcRequest,
purpose: 'preview', purpose: 'send',
}); });
const data = await convert(rendered_request, args.protoFiles); const data = await convert(rendered_request, args.protoFiles);
await ctx.clipboard.copyText(data); await ctx.clipboard.copyText(data);
@@ -103,7 +103,7 @@ export async function convert(request: Partial<GrpcRequest>, allProtoFiles: stri
// Add form params // Add form params
if (request.message) { if (request.message) {
xs.push('-d', `${quote(JSON.stringify(JSON.parse(request.message)))}`); xs.push('-d', quote(request.message));
xs.push(NEWLINE); xs.push(NEWLINE);
} }

View File

@@ -151,7 +151,26 @@ describe('exporter-curl', () => {
[ [
`grpcurl -import-path '/'`, `grpcurl -import-path '/'`,
`-proto '/foo.proto'`, `-proto '/foo.proto'`,
`-d '{"foo":"bar","baz":1}'`, `-d '{\n "foo": "bar",\n "baz": 1\n}'`,
'yaak.app',
].join(' \\\n '),
);
});
test('Sends data with unresolved template tags', async () => {
expect(
await convert(
{
url: 'https://yaak.app',
message: '{"timestamp": ${[ faker "timestamp" ]}, "foo": "bar"}',
},
['/foo.proto'],
),
).toEqual(
[
`grpcurl -import-path '/'`,
`-proto '/foo.proto'`,
`-d '{"timestamp": \${[ faker "timestamp" ]}, "foo": "bar"}'`,
'yaak.app', 'yaak.app',
].join(' \\\n '), ].join(' \\\n '),
); );

View File

@@ -7,6 +7,29 @@ describe('importer-openapi', () => {
const p = path.join(__dirname, 'fixtures'); const p = path.join(__dirname, 'fixtures');
const fixtures = fs.readdirSync(p); const fixtures = fs.readdirSync(p);
test('Maps operation description to request description', async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: '3.0.0',
info: { title: 'Description Test', version: '1.0.0' },
paths: {
'/klanten': {
get: {
description: 'Lijst van klanten',
responses: { '200': { description: 'ok' } },
},
},
},
}),
);
expect(imported?.resources.httpRequests).toEqual([
expect.objectContaining({
description: 'Lijst van klanten',
}),
]);
});
test('Skips invalid file', async () => { test('Skips invalid file', async () => {
const imported = await convertOpenApi('{}'); const imported = await convertOpenApi('{}');
expect(imported).toBeUndefined(); expect(imported).toBeUndefined();

View File

@@ -55,19 +55,11 @@ export function convertPostman(contents: string): ImportPluginResponse | undefin
folders: [], folders: [],
}; };
const rawDescription = info.description;
const description =
typeof rawDescription === 'object' && rawDescription != null && 'content' in rawDescription
? String(rawDescription.content)
: rawDescription == null
? undefined
: String(rawDescription);
const workspace: ExportResources['workspaces'][0] = { const workspace: ExportResources['workspaces'][0] = {
model: 'workspace', model: 'workspace',
id: generateId('workspace'), id: generateId('workspace'),
name: info.name ? String(info.name) : 'Postman Import', name: info.name ? String(info.name) : 'Postman Import',
description, description: importDescription(info.description),
...globalAuth, ...globalAuth,
}; };
exportResources.workspaces.push(workspace); exportResources.workspaces.push(workspace);
@@ -139,7 +131,7 @@ export function convertPostman(contents: string): ImportPluginResponse | undefin
workspaceId: workspace.id, workspaceId: workspace.id,
folderId, folderId,
name: v.name, name: v.name,
description: r.description ? String(r.description) : undefined, description: importDescription(r.description),
method: typeof r.method === 'string' ? r.method : 'GET', method: typeof r.method === 'string' ? r.method : 'GET',
url, url,
urlParameters, urlParameters,
@@ -509,6 +501,26 @@ function toArray<T>(value: unknown): T[] {
return []; return [];
} }
function importDescription(rawDescription: unknown): string | undefined {
if (rawDescription == null) {
return undefined;
}
if (typeof rawDescription === 'string') {
return rawDescription;
}
if (typeof rawDescription === 'object' && !Array.isArray(rawDescription)) {
const description = toRecord(rawDescription);
if ('content' in description && description.content != null) {
return String(description.content);
}
return undefined;
}
return String(rawDescription);
}
/** Recursively render all nested object properties */ /** Recursively render all nested object properties */
function convertTemplateSyntax<T>(obj: T): T { function convertTemplateSyntax<T>(obj: T): T {
if (typeof obj === 'string') { if (typeof obj === 'string') {

View File

@@ -22,4 +22,39 @@ describe('importer-postman', () => {
); );
}); });
} }
test('Imports object descriptions without [object Object]', () => {
const result = convertPostman(
JSON.stringify({
info: {
name: 'Description Test',
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
},
item: [
{
name: 'Request 1',
request: {
method: 'GET',
description: {
content: 'Lijst van klanten',
type: 'text/plain',
},
},
},
],
}),
);
expect(result?.resources.workspaces).toEqual([
expect.objectContaining({
name: 'Description Test',
}),
]);
expect(result?.resources.httpRequests).toEqual([
expect.objectContaining({
name: 'Request 1',
description: 'Lijst van klanten',
}),
]);
});
}); });

View File

@@ -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": {

View File

@@ -45,6 +45,9 @@ const args = [
...additionalArgs ...additionalArgs
]; ];
const result = spawnSync('tauri', args, { stdio: 'inherit', shell: false, env: process.env }); // Invoke the tauri CLI JS entry point directly via node to avoid shell escaping issues on Windows
const tauriJs = path.join(rootDir, 'node_modules', '@tauri-apps', 'cli', 'tauri.js');
const result = spawnSync(process.execPath, [tauriJs, ...args], { stdio: 'inherit', env: process.env });
process.exit(result.status || 0); process.exit(result.status || 0);

View File

@@ -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 (

View File

@@ -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>
); );
} }

View File

@@ -1,11 +1,11 @@
import { invokeCmd } from '../lib/tauri'; import { installPluginFromDirectory } from '@yaakapp-internal/plugins';
import { useFastMutation } from './useFastMutation'; import { useFastMutation } from './useFastMutation';
export function useInstallPlugin() { export function useInstallPlugin() {
return useFastMutation<void, unknown, string>({ return useFastMutation<void, unknown, string>({
mutationKey: ['install_plugin'], mutationKey: ['install_plugin'],
mutationFn: async (directory: string) => { mutationFn: async (directory: string) => {
await invokeCmd('cmd_install_plugin', { directory }); await installPluginFromDirectory(directory);
}, },
}); });
} }

View File

@@ -36,7 +36,6 @@ type TauriCmd =
| 'cmd_http_request_body' | 'cmd_http_request_body'
| 'cmd_http_response_body' | 'cmd_http_response_body'
| 'cmd_import_data' | 'cmd_import_data'
| 'cmd_install_plugin'
| 'cmd_metadata' | 'cmd_metadata'
| 'cmd_restart' | 'cmd_restart'
| 'cmd_new_child_window' | 'cmd_new_child_window'