diff --git a/crates-tauri/yaak-app/src/lib.rs b/crates-tauri/yaak-app/src/lib.rs index bb463b7e..97227784 100644 --- a/crates-tauri/yaak-app/src/lib.rs +++ b/crates-tauri/yaak-app/src/lib.rs @@ -31,6 +31,7 @@ use tauri_plugin_window_state::{AppHandleExt, StateFlags}; use tokio::sync::Mutex; use tokio::task::block_in_place; use tokio::time; +use yaak_common::command::new_checked_command; use yaak_crypto::manager::EncryptionManager; use yaak_grpc::manager::{GrpcConfig, GrpcHandle}; use yaak_grpc::{Code, ServiceDefinition, serialize_message}; @@ -97,6 +98,7 @@ impl PluginContextExt for WebviewWindow { struct AppMetaData { is_dev: bool, version: String, + cli_version: Option, name: String, app_data_dir: String, app_log_dir: String, @@ -113,9 +115,11 @@ async fn cmd_metadata(app_handle: AppHandle) -> YaakResult { let vendored_plugin_dir = app_handle.path().resolve("vendored/plugins", BaseDirectory::Resource)?; let default_project_dir = app_handle.path().home_dir()?.join("YaakProjects"); + let cli_version = detect_cli_version().await; Ok(AppMetaData { is_dev: is_dev(), version: app_handle.package_info().version.to_string(), + cli_version, name: app_handle.package_info().name.to_string(), app_data_dir: app_data_dir.to_string_lossy().to_string(), app_log_dir: app_log_dir.to_string_lossy().to_string(), @@ -126,6 +130,28 @@ async fn cmd_metadata(app_handle: AppHandle) -> YaakResult { }) } +async fn detect_cli_version() -> Option { + // Prefer `yaak`, but support the legacy `yaakcli` alias if present. + if let Some(version) = detect_cli_version_for_binary("yaak").await { + return Some(version); + } + detect_cli_version_for_binary("yaakcli").await +} + +async fn detect_cli_version_for_binary(program: &str) -> Option { + let mut cmd = new_checked_command(program, "--version").await.ok()?; + let out = cmd.arg("--version").output().await.ok()?; + if !out.status.success() { + return None; + } + + let line = String::from_utf8(out.stdout).ok()?; + let line = line.lines().find(|l| !l.trim().is_empty())?.trim(); + let mut parts = line.split_whitespace(); + let _name = parts.next(); + Some(parts.next().unwrap_or(line).to_string()) +} + #[tauri::command] async fn cmd_template_tokens_to_string( window: WebviewWindow, diff --git a/crates/yaak-common/src/command.rs b/crates/yaak-common/src/command.rs index ee324c89..dbaaf9e9 100644 --- a/crates/yaak-common/src/command.rs +++ b/crates/yaak-common/src/command.rs @@ -1,4 +1,6 @@ -use std::ffi::OsStr; +use std::ffi::{OsStr, OsString}; +use std::io::{self, ErrorKind}; +use std::process::Stdio; #[cfg(target_os = "windows")] const CREATE_NO_WINDOW: u32 = 0x0800_0000; @@ -14,3 +16,27 @@ pub fn new_xplatform_command>(program: S) -> tokio::process::Com } cmd } + +/// Creates a command only if the binary exists and can be invoked with the given probe argument. +pub async fn new_checked_command>( + program: S, + probe_arg: &str, +) -> io::Result { + let program: OsString = program.as_ref().to_os_string(); + + let mut probe = new_xplatform_command(&program); + probe.arg(probe_arg).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()); + + let status = probe.status().await?; + if !status.success() { + return Err(io::Error::new( + ErrorKind::NotFound, + format!( + "'{}' is not available on PATH or failed to execute", + program.to_string_lossy() + ), + )); + } + + Ok(new_xplatform_command(&program)) +} diff --git a/crates/yaak-git/src/binary.rs b/crates/yaak-git/src/binary.rs index 270e6dbe..3fb79233 100644 --- a/crates/yaak-git/src/binary.rs +++ b/crates/yaak-git/src/binary.rs @@ -1,9 +1,8 @@ use crate::error::Error::GitNotFound; use crate::error::Result; use std::path::Path; -use std::process::Stdio; use tokio::process::Command; -use yaak_common::command::new_xplatform_command; +use yaak_common::command::new_checked_command; /// Create a git command that runs in the specified directory pub(crate) async fn new_binary_command(dir: &Path) -> Result { @@ -14,17 +13,5 @@ pub(crate) async fn new_binary_command(dir: &Path) -> Result { /// Create a git command without a specific directory (for global operations) pub(crate) async fn new_binary_command_global() -> Result { - // 1. Probe that `git` exists and is runnable - let mut probe = new_xplatform_command("git"); - probe.arg("--version").stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()); - - let status = probe.status().await.map_err(|_| GitNotFound)?; - - if !status.success() { - return Err(GitNotFound); - } - - // 2. Build the reusable git command - let cmd = new_xplatform_command("git"); - Ok(cmd) + new_checked_command("git", "--version").await.map_err(|_| GitNotFound) } diff --git a/src-web/components/CommandPaletteDialog.tsx b/src-web/components/CommandPaletteDialog.tsx index a2c04d36..94b79ebb 100644 --- a/src-web/components/CommandPaletteDialog.tsx +++ b/src-web/components/CommandPaletteDialog.tsx @@ -32,6 +32,8 @@ import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces'; import { useScrollIntoView } from '../hooks/useScrollIntoView'; import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest'; import { useSidebarHidden } from '../hooks/useSidebarHidden'; +import { appInfo } from '../lib/appInfo'; +import { copyToClipboard } from '../lib/copy'; import { createRequestAndNavigate } from '../lib/createRequestAndNavigate'; import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm'; import { showDialog } from '../lib/dialog'; @@ -162,6 +164,14 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) { label: 'Send Request', onSelect: () => sendRequest(activeRequest.id), }); + if (appInfo.cliVersion != null) { + commands.push({ + key: 'request.copy_cli_send', + searchText: `copy cli send yaak request send ${activeRequest.id}`, + label: 'Copy CLI Send Command', + onSelect: () => copyToClipboard(`yaak request send ${activeRequest.id}`), + }); + } httpRequestActions.forEach((a, i) => { commands.push({ key: `http_request_action.${i}`, diff --git a/src-web/lib/appInfo.ts b/src-web/lib/appInfo.ts index 08c70274..e5812b9d 100644 --- a/src-web/lib/appInfo.ts +++ b/src-web/lib/appInfo.ts @@ -4,6 +4,7 @@ import { invokeCmd } from './tauri'; export interface AppInfo { isDev: boolean; version: string; + cliVersion: string | null; name: string; appDataDir: string; appLogDir: string;