mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-04 12:01:52 +02:00
feat(cli): add plugin install and complete prompt/render host events
This commit is contained in:
Generated
+118
-7
@@ -1200,7 +1200,7 @@ dependencies = [
|
|||||||
"encode_unicode",
|
"encode_unicode",
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"unicode-width",
|
"unicode-width 0.2.2",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1405,6 +1405,31 @@ version = "0.8.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm"
|
||||||
|
version = "0.25.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"crossterm_winapi",
|
||||||
|
"libc",
|
||||||
|
"mio 0.8.11",
|
||||||
|
"parking_lot",
|
||||||
|
"signal-hook",
|
||||||
|
"signal-hook-mio",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm_winapi"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crunchy"
|
name = "crunchy"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@@ -2294,6 +2319,15 @@ dependencies = [
|
|||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fuzzy-matcher"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
|
||||||
|
dependencies = [
|
||||||
|
"thread_local 1.1.9",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fxhash"
|
name = "fxhash"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -3164,6 +3198,24 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inquire"
|
||||||
|
version = "0.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
|
"crossterm",
|
||||||
|
"dyn-clone",
|
||||||
|
"fuzzy-matcher",
|
||||||
|
"fxhash",
|
||||||
|
"newline-converter",
|
||||||
|
"once_cell",
|
||||||
|
"tempfile",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-width 0.1.14",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "interfaces"
|
name = "interfaces"
|
||||||
version = "0.0.8"
|
version = "0.0.8"
|
||||||
@@ -3756,6 +3808,18 @@ dependencies = [
|
|||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "0.8.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"log 0.4.29",
|
||||||
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -3851,6 +3915,15 @@ version = "1.0.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "newline-converter"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nibble_vec"
|
name = "nibble_vec"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -3942,7 +4015,7 @@ dependencies = [
|
|||||||
"kqueue",
|
"kqueue",
|
||||||
"libc",
|
"libc",
|
||||||
"log 0.4.29",
|
"log 0.4.29",
|
||||||
"mio",
|
"mio 1.0.4",
|
||||||
"notify-types",
|
"notify-types",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
@@ -4501,7 +4574,7 @@ dependencies = [
|
|||||||
"textwrap",
|
"textwrap",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unicode-width",
|
"unicode-width 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6171,7 +6244,7 @@ version = "0.5.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77dff57c9de498bb1eb5b1ce682c2e3a0ae956b266fa0933c3e151b87b078967"
|
checksum = "77dff57c9de498bb1eb5b1ce682c2e3a0ae956b266fa0933c3e151b87b078967"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-width",
|
"unicode-width 0.2.2",
|
||||||
"yansi",
|
"yansi",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -6196,7 +6269,7 @@ dependencies = [
|
|||||||
"kqueue",
|
"kqueue",
|
||||||
"libc",
|
"libc",
|
||||||
"log 0.4.29",
|
"log 0.4.29",
|
||||||
"mio",
|
"mio 1.0.4",
|
||||||
"rolldown-notify-types",
|
"rolldown-notify-types",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
@@ -7173,6 +7246,27 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook"
|
||||||
|
version = "0.3.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-mio"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"mio 0.8.11",
|
||||||
|
"signal-hook",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.5"
|
version = "1.4.5"
|
||||||
@@ -8068,7 +8162,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"smawk",
|
"smawk",
|
||||||
"unicode-linebreak",
|
"unicode-linebreak",
|
||||||
"unicode-width",
|
"unicode-width 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8215,7 +8309,7 @@ checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio 1.0.4",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2 0.6.1",
|
"socket2 0.6.1",
|
||||||
@@ -8785,6 +8879,12 @@ version = "1.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -9562,6 +9662,15 @@ dependencies = [
|
|||||||
"windows-targets 0.42.2",
|
"windows-targets 0.42.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.48.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.48.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
@@ -10141,6 +10250,7 @@ dependencies = [
|
|||||||
name = "yaak-cli"
|
name = "yaak-cli"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"arboard",
|
||||||
"assert_cmd",
|
"assert_cmd",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -10150,6 +10260,7 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"hex",
|
"hex",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
|
"inquire",
|
||||||
"keyring",
|
"keyring",
|
||||||
"log 0.4.29",
|
"log 0.4.29",
|
||||||
"oxc_resolver",
|
"oxc_resolver",
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ name = "yaak"
|
|||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
arboard = "3"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
console = "0.15"
|
console = "0.15"
|
||||||
dirs = "6"
|
dirs = "6"
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
inquire = { version = "0.7", features = ["editor"] }
|
||||||
hex = { workspace = true }
|
hex = { workspace = true }
|
||||||
include_dir = "0.7"
|
include_dir = "0.7"
|
||||||
keyring = { workspace = true, features = ["apple-native", "windows-native", "sync-secret-service"] }
|
keyring = { workspace = true, features = ["apple-native", "windows-native", "sync-secret-service"] }
|
||||||
|
|||||||
@@ -444,6 +444,9 @@ pub enum PluginCommands {
|
|||||||
/// Generate a "Hello World" Yaak plugin
|
/// Generate a "Hello World" Yaak plugin
|
||||||
Generate(GenerateArgs),
|
Generate(GenerateArgs),
|
||||||
|
|
||||||
|
/// Install a plugin from a local directory or from the registry
|
||||||
|
Install(InstallPluginArgs),
|
||||||
|
|
||||||
/// Publish a Yaak plugin version to the plugin registry
|
/// Publish a Yaak plugin version to the plugin registry
|
||||||
Publish(PluginPathArg),
|
Publish(PluginPathArg),
|
||||||
}
|
}
|
||||||
@@ -464,3 +467,9 @@ pub struct GenerateArgs {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub dir: Option<PathBuf>,
|
pub dir: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Clone)]
|
||||||
|
pub struct InstallPluginArgs {
|
||||||
|
/// Local plugin directory path, or registry plugin spec (@org/plugin[@version])
|
||||||
|
pub source: String,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::cli::{GenerateArgs, PluginArgs, PluginCommands, PluginPathArg};
|
use crate::cli::{GenerateArgs, InstallPluginArgs, PluginPathArg};
|
||||||
|
use crate::context::CliContext;
|
||||||
use crate::ui;
|
use crate::ui;
|
||||||
use crate::utils::http;
|
use crate::utils::http;
|
||||||
use keyring::Entry;
|
use keyring::Entry;
|
||||||
@@ -15,6 +16,11 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
use yaak_api::{ApiClientKind, yaak_api_client};
|
||||||
|
use yaak_models::models::{Plugin, PluginSource};
|
||||||
|
use yaak_models::util::UpdateSource;
|
||||||
|
use yaak_plugins::events::PluginContext;
|
||||||
|
use yaak_plugins::install::download_and_install;
|
||||||
use zip::CompressionMethod;
|
use zip::CompressionMethod;
|
||||||
use zip::write::SimpleFileOptions;
|
use zip::write::SimpleFileOptions;
|
||||||
|
|
||||||
@@ -57,12 +63,13 @@ pub async fn run_build(args: PluginPathArg) -> i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(args: PluginArgs) -> i32 {
|
pub async fn run_install(context: &CliContext, args: InstallPluginArgs) -> i32 {
|
||||||
match args.command {
|
match install(context, args).await {
|
||||||
PluginCommands::Build(args) => run_build(args).await,
|
Ok(()) => 0,
|
||||||
PluginCommands::Dev(args) => run_dev(args).await,
|
Err(error) => {
|
||||||
PluginCommands::Generate(args) => run_generate(args).await,
|
ui::error(&error);
|
||||||
PluginCommands::Publish(args) => run_publish(args).await,
|
1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,6 +257,113 @@ async fn publish(args: PluginPathArg) -> CommandResult {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn install(context: &CliContext, args: InstallPluginArgs) -> CommandResult {
|
||||||
|
if args.source.starts_with('@') {
|
||||||
|
let (name, version) =
|
||||||
|
parse_registry_install_spec(args.source.as_str()).ok_or_else(|| {
|
||||||
|
"Invalid registry plugin spec. Expected format: @org/plugin or @org/plugin@version"
|
||||||
|
.to_string()
|
||||||
|
})?;
|
||||||
|
return install_from_registry(context, name, version).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
install_from_directory(context, args.source.as_str()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn install_from_registry(
|
||||||
|
context: &CliContext,
|
||||||
|
name: String,
|
||||||
|
version: Option<String>,
|
||||||
|
) -> CommandResult {
|
||||||
|
let current_version = crate::version::cli_version();
|
||||||
|
let http_client = yaak_api_client(ApiClientKind::Cli, current_version)
|
||||||
|
.map_err(|err| format!("Failed to initialize API client: {err}"))?;
|
||||||
|
let installing_version = version.clone().unwrap_or_else(|| "latest".to_string());
|
||||||
|
ui::info(&format!("Installing registry plugin {name}@{installing_version}"));
|
||||||
|
|
||||||
|
let plugin_context = PluginContext::new(Some("cli".to_string()), None);
|
||||||
|
let installed = download_and_install(
|
||||||
|
context.plugin_manager(),
|
||||||
|
context.query_manager(),
|
||||||
|
&http_client,
|
||||||
|
&plugin_context,
|
||||||
|
name.as_str(),
|
||||||
|
version,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("Failed to install plugin: {err}"))?;
|
||||||
|
|
||||||
|
ui::success(&format!("Installed plugin {}@{}", installed.name, installed.version));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn install_from_directory(context: &CliContext, source: &str) -> CommandResult {
|
||||||
|
let plugin_dir = resolve_plugin_dir(Some(PathBuf::from(source)))?;
|
||||||
|
let plugin_dir_str = plugin_dir
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!("Plugin directory path is not valid UTF-8: {}", plugin_dir.display())
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
ui::info(&format!("Installing plugin from directory {}", plugin_dir.display()));
|
||||||
|
|
||||||
|
let plugin = context
|
||||||
|
.db()
|
||||||
|
.upsert_plugin(
|
||||||
|
&Plugin {
|
||||||
|
directory: plugin_dir_str,
|
||||||
|
url: None,
|
||||||
|
enabled: true,
|
||||||
|
source: PluginSource::Filesystem,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&UpdateSource::Background,
|
||||||
|
)
|
||||||
|
.map_err(|err| format!("Failed to save plugin in database: {err}"))?;
|
||||||
|
|
||||||
|
let plugin_context = PluginContext::new(Some("cli".to_string()), None);
|
||||||
|
context
|
||||||
|
.plugin_manager()
|
||||||
|
.add_plugin(&plugin_context, &plugin)
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("Failed to load plugin runtime: {err}"))?;
|
||||||
|
|
||||||
|
ui::success(&format!("Installed plugin from {}", plugin.directory));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_registry_install_spec(source: &str) -> Option<(String, Option<String>)> {
|
||||||
|
if !source.starts_with('@') || !source.contains('/') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rest = source.get(1..)?;
|
||||||
|
let version_split = rest.rfind('@').map(|idx| idx + 1);
|
||||||
|
let (name, version) = match version_split {
|
||||||
|
Some(at_idx) => {
|
||||||
|
let (name, version) = source.split_at(at_idx);
|
||||||
|
let version = version.strip_prefix('@').unwrap_or_default();
|
||||||
|
if version.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
(name.to_string(), Some(version.to_string()))
|
||||||
|
}
|
||||||
|
None => (source.to_string(), None),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !name.starts_with('@') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let without_scope = name.get(1..)?;
|
||||||
|
let (scope, plugin_name) = without_scope.split_once('/')?;
|
||||||
|
if scope.is_empty() || plugin_name.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((name, version))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct PublishResponse {
|
struct PublishResponse {
|
||||||
version: String,
|
version: String,
|
||||||
|
|||||||
@@ -481,7 +481,8 @@ async fn send_http_request_by_id(
|
|||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let cookie_jar_id = resolve_cookie_jar_id(ctx, workspace_id, cookie_jar_id)?;
|
let cookie_jar_id = resolve_cookie_jar_id(ctx, workspace_id, cookie_jar_id)?;
|
||||||
|
|
||||||
let plugin_context = PluginContext::new(None, Some(workspace_id.to_string()));
|
let plugin_context =
|
||||||
|
PluginContext::new(Some("cli".to_string()), Some(workspace_id.to_string()));
|
||||||
|
|
||||||
let (event_tx, mut event_rx) = mpsc::channel::<SenderHttpResponseEvent>(100);
|
let (event_tx, mut event_rx) = mpsc::channel::<SenderHttpResponseEvent>(100);
|
||||||
let (body_chunk_tx, mut body_chunk_rx) = mpsc::unbounded_channel::<Vec<u8>>();
|
let (body_chunk_tx, mut body_chunk_rx) = mpsc::unbounded_channel::<Vec<u8>>();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ mod version;
|
|||||||
mod version_check;
|
mod version_check;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::{Cli, Commands, RequestCommands};
|
use cli::{Cli, Commands, PluginCommands, RequestCommands};
|
||||||
use context::{CliContext, CliExecutionContext};
|
use context::{CliContext, CliExecutionContext};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use yaak_crypto::manager::EncryptionManager;
|
use yaak_crypto::manager::EncryptionManager;
|
||||||
@@ -52,7 +52,29 @@ async fn main() {
|
|||||||
|
|
||||||
let exit_code = match command {
|
let exit_code = match command {
|
||||||
Commands::Auth(args) => commands::auth::run(args).await,
|
Commands::Auth(args) => commands::auth::run(args).await,
|
||||||
Commands::Plugin(args) => commands::plugin::run(args).await,
|
Commands::Plugin(args) => match args.command {
|
||||||
|
PluginCommands::Build(args) => commands::plugin::run_build(args).await,
|
||||||
|
PluginCommands::Dev(args) => commands::plugin::run_dev(args).await,
|
||||||
|
PluginCommands::Generate(args) => commands::plugin::run_generate(args).await,
|
||||||
|
PluginCommands::Publish(args) => commands::plugin::run_publish(args).await,
|
||||||
|
PluginCommands::Install(install_args) => {
|
||||||
|
let query_manager = query_manager.clone();
|
||||||
|
let blob_manager = blob_manager.clone();
|
||||||
|
let execution_context = CliExecutionContext::default();
|
||||||
|
let context = CliContext::new(
|
||||||
|
data_dir.clone(),
|
||||||
|
query_manager.clone(),
|
||||||
|
blob_manager,
|
||||||
|
Arc::new(EncryptionManager::new(query_manager, app_id)),
|
||||||
|
true,
|
||||||
|
execution_context,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let exit_code = commands::plugin::run_install(&context, install_args).await;
|
||||||
|
context.shutdown().await;
|
||||||
|
exit_code
|
||||||
|
}
|
||||||
|
},
|
||||||
Commands::Build(args) => commands::plugin::run_build(args).await,
|
Commands::Build(args) => commands::plugin::run_build(args).await,
|
||||||
Commands::Dev(args) => commands::plugin::run_dev(args).await,
|
Commands::Dev(args) => commands::plugin::run_dev(args).await,
|
||||||
Commands::Generate(args) => commands::plugin::run_generate(args).await,
|
Commands::Generate(args) => commands::plugin::run_generate(args).await,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
use crate::context::CliExecutionContext;
|
use crate::context::CliExecutionContext;
|
||||||
|
use arboard::Clipboard;
|
||||||
|
use console::Term;
|
||||||
|
use inquire::{Confirm, Editor, Password, PasswordDisplayMode, Select, Text};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
use std::io::IsTerminal;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
@@ -16,10 +20,11 @@ use yaak_models::query_manager::QueryManager;
|
|||||||
use yaak_models::render::make_vars_hashmap;
|
use yaak_models::render::make_vars_hashmap;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
use yaak_plugins::events::{
|
use yaak_plugins::events::{
|
||||||
EmptyPayload, ErrorResponse, GetCookieValueResponse, InternalEvent, InternalEventPayload,
|
EmptyPayload, ErrorResponse, FormInput, GetCookieValueResponse, InternalEvent,
|
||||||
ListCookieNamesResponse, ListOpenWorkspacesResponse, PluginContext, RenderGrpcRequestResponse,
|
InternalEventPayload, JsonPrimitive, ListCookieNamesResponse, ListOpenWorkspacesResponse,
|
||||||
RenderHttpRequestResponse, SendHttpRequestResponse, TemplateRenderResponse, WindowInfoResponse,
|
PluginContext, PromptFormRequest, PromptFormResponse, PromptTextRequest, PromptTextResponse,
|
||||||
WorkspaceInfo,
|
RenderGrpcRequestResponse, RenderHttpRequestResponse, SendHttpRequestResponse,
|
||||||
|
TemplateRenderResponse, WindowInfoResponse, WorkspaceInfo,
|
||||||
};
|
};
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
use yaak_plugins::template_callback::PluginTemplateCallback;
|
use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||||
@@ -404,19 +409,29 @@ async fn build_plugin_reply(
|
|||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HostRequest::CopyText(_) => Some(InternalEventPayload::ErrorResponse(ErrorResponse {
|
HostRequest::CopyText(req) => match copy_text_to_clipboard(req.text.as_str()) {
|
||||||
error: "Unsupported plugin request in CLI: copy_text_request".to_string(),
|
Ok(()) => Some(InternalEventPayload::CopyTextResponse(EmptyPayload {})),
|
||||||
})),
|
Err(error) => Some(InternalEventPayload::ErrorResponse(ErrorResponse {
|
||||||
HostRequest::PromptText(_) => {
|
error: format!("Failed to copy text in CLI: {error}"),
|
||||||
Some(InternalEventPayload::ErrorResponse(ErrorResponse {
|
})),
|
||||||
error: "Unsupported plugin request in CLI: prompt_text_request".to_string(),
|
},
|
||||||
}))
|
HostRequest::PromptText(req) => match prompt_text_for_cli(req) {
|
||||||
}
|
Ok(value) => {
|
||||||
HostRequest::PromptForm(_) => {
|
Some(InternalEventPayload::PromptTextResponse(PromptTextResponse { value }))
|
||||||
Some(InternalEventPayload::ErrorResponse(ErrorResponse {
|
}
|
||||||
error: "Unsupported plugin request in CLI: prompt_form_request".to_string(),
|
Err(error) => Some(InternalEventPayload::ErrorResponse(ErrorResponse {
|
||||||
}))
|
error: format!("Failed to prompt text in CLI: {error}"),
|
||||||
}
|
})),
|
||||||
|
},
|
||||||
|
HostRequest::PromptForm(req) => match prompt_form_for_cli(req) {
|
||||||
|
Ok(values) => Some(InternalEventPayload::PromptFormResponse(PromptFormResponse {
|
||||||
|
values,
|
||||||
|
done: Some(true),
|
||||||
|
})),
|
||||||
|
Err(error) => Some(InternalEventPayload::ErrorResponse(ErrorResponse {
|
||||||
|
error: format!("Failed to prompt form in CLI: {error}"),
|
||||||
|
})),
|
||||||
|
},
|
||||||
HostRequest::OpenWindow(_) => {
|
HostRequest::OpenWindow(_) => {
|
||||||
Some(InternalEventPayload::ErrorResponse(ErrorResponse {
|
Some(InternalEventPayload::ErrorResponse(ErrorResponse {
|
||||||
error: "Unsupported plugin request in CLI: open_window_request".to_string(),
|
error: "Unsupported plugin request in CLI: open_window_request".to_string(),
|
||||||
@@ -567,3 +582,464 @@ fn parse_cookie_name_value(raw_cookie: &str) -> Option<(String, String)> {
|
|||||||
let (name, value) = first_part.split_once('=')?;
|
let (name, value) = first_part.split_once('=')?;
|
||||||
Some((name.trim().to_string(), value.to_string()))
|
Some((name.trim().to_string(), value.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn copy_text_to_clipboard(text: &str) -> Result<(), String> {
|
||||||
|
let mut clipboard = Clipboard::new().map_err(|e| e.to_string())?;
|
||||||
|
clipboard.set_text(text.to_string()).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_text_for_cli(req: &PromptTextRequest) -> Result<Option<String>, String> {
|
||||||
|
if !std::io::stdin().is_terminal() {
|
||||||
|
return Err("cannot prompt in non-interactive mode".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let term = Term::stdout();
|
||||||
|
if let Some(description) = &req.description {
|
||||||
|
if !description.is_empty() {
|
||||||
|
term.write_line(description.as_str()).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let label = if req.label.is_empty() { req.id.as_str() } else { req.label.as_str() };
|
||||||
|
let value = if req.password.unwrap_or(false) {
|
||||||
|
prompt_password_with_inquire(
|
||||||
|
label,
|
||||||
|
req.default_value.clone(),
|
||||||
|
req.required.unwrap_or(false),
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
prompt_text_with_inquire(
|
||||||
|
label,
|
||||||
|
req.default_value.clone(),
|
||||||
|
req.placeholder.clone(),
|
||||||
|
req.required.unwrap_or(false),
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
|
||||||
|
match value {
|
||||||
|
PromptValue::Cancelled => Ok(None),
|
||||||
|
PromptValue::Value(v) => Ok(v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_form_for_cli(
|
||||||
|
req: &PromptFormRequest,
|
||||||
|
) -> Result<Option<HashMap<String, JsonPrimitive>>, String> {
|
||||||
|
if !std::io::stdin().is_terminal() {
|
||||||
|
return Err("cannot prompt in non-interactive mode".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let term = Term::stdout();
|
||||||
|
if let Some(description) = &req.description {
|
||||||
|
if !description.is_empty() {
|
||||||
|
term.write_line(description.as_str()).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut values = HashMap::new();
|
||||||
|
for input in &req.inputs {
|
||||||
|
if prompt_form_input_for_cli(input, &mut values)? == PromptOutcome::Cancelled {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(values))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||||
|
enum PromptOutcome {
|
||||||
|
Continue,
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_form_input_for_cli(
|
||||||
|
input: &FormInput,
|
||||||
|
values: &mut HashMap<String, JsonPrimitive>,
|
||||||
|
) -> Result<PromptOutcome, String> {
|
||||||
|
match input {
|
||||||
|
FormInput::Text(input) => {
|
||||||
|
if input.base.hidden.unwrap_or(false) || input.base.disabled.unwrap_or(false) {
|
||||||
|
return Ok(PromptOutcome::Continue);
|
||||||
|
}
|
||||||
|
|
||||||
|
let label = prompt_label_for_base(&input.base);
|
||||||
|
let required = !input.base.optional.unwrap_or(false);
|
||||||
|
let value = if input.password.unwrap_or(false) {
|
||||||
|
prompt_password_with_inquire(
|
||||||
|
label.as_str(),
|
||||||
|
input.base.default_value.clone(),
|
||||||
|
required,
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
prompt_text_with_inquire(
|
||||||
|
label.as_str(),
|
||||||
|
input.base.default_value.clone(),
|
||||||
|
input.placeholder.clone(),
|
||||||
|
required,
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
|
||||||
|
match value {
|
||||||
|
PromptValue::Cancelled => Ok(PromptOutcome::Cancelled),
|
||||||
|
PromptValue::Value(Some(v)) => {
|
||||||
|
values.insert(input.base.name.clone(), JsonPrimitive::String(v));
|
||||||
|
Ok(PromptOutcome::Continue)
|
||||||
|
}
|
||||||
|
PromptValue::Value(None) => Ok(PromptOutcome::Continue),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FormInput::Editor(input) => {
|
||||||
|
if input.base.hidden.unwrap_or(false) || input.base.disabled.unwrap_or(false) {
|
||||||
|
return Ok(PromptOutcome::Continue);
|
||||||
|
}
|
||||||
|
|
||||||
|
let label = prompt_label_for_base(&input.base);
|
||||||
|
let required = !input.base.optional.unwrap_or(false);
|
||||||
|
let value = prompt_editor_with_inquire(
|
||||||
|
label.as_str(),
|
||||||
|
input.base.default_value.clone(),
|
||||||
|
required,
|
||||||
|
)?;
|
||||||
|
match value {
|
||||||
|
PromptValue::Cancelled => Ok(PromptOutcome::Cancelled),
|
||||||
|
PromptValue::Value(Some(v)) => {
|
||||||
|
values.insert(input.base.name.clone(), JsonPrimitive::String(v));
|
||||||
|
Ok(PromptOutcome::Continue)
|
||||||
|
}
|
||||||
|
PromptValue::Value(None) => Ok(PromptOutcome::Continue),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FormInput::Select(input) => {
|
||||||
|
if input.base.hidden.unwrap_or(false) || input.base.disabled.unwrap_or(false) {
|
||||||
|
return Ok(PromptOutcome::Continue);
|
||||||
|
}
|
||||||
|
|
||||||
|
let label = prompt_label_for_base(&input.base);
|
||||||
|
let options = input.options.iter().map(|o| o.value.clone()).collect::<Vec<_>>();
|
||||||
|
let value = prompt_select_with_inquire(
|
||||||
|
label.as_str(),
|
||||||
|
options,
|
||||||
|
input.base.default_value.clone(),
|
||||||
|
!input.base.optional.unwrap_or(false),
|
||||||
|
)?;
|
||||||
|
match value {
|
||||||
|
PromptValue::Cancelled => Ok(PromptOutcome::Cancelled),
|
||||||
|
PromptValue::Value(Some(v)) => {
|
||||||
|
values.insert(input.base.name.clone(), JsonPrimitive::String(v));
|
||||||
|
Ok(PromptOutcome::Continue)
|
||||||
|
}
|
||||||
|
PromptValue::Value(None) => Ok(PromptOutcome::Continue),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FormInput::Checkbox(input) => {
|
||||||
|
if input.base.hidden.unwrap_or(false) || input.base.disabled.unwrap_or(false) {
|
||||||
|
return Ok(PromptOutcome::Continue);
|
||||||
|
}
|
||||||
|
|
||||||
|
let label = prompt_label_for_base(&input.base);
|
||||||
|
let default = input
|
||||||
|
.base
|
||||||
|
.default_value
|
||||||
|
.as_deref()
|
||||||
|
.map(|v| matches!(v, "1" | "true" | "yes" | "on"))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
match prompt_confirm_with_inquire(label.as_str(), default)? {
|
||||||
|
PromptValue::Cancelled => Ok(PromptOutcome::Cancelled),
|
||||||
|
PromptValue::Value(Some(v)) => {
|
||||||
|
values.insert(input.base.name.clone(), JsonPrimitive::Boolean(v == "true"));
|
||||||
|
Ok(PromptOutcome::Continue)
|
||||||
|
}
|
||||||
|
PromptValue::Value(None) => Ok(PromptOutcome::Continue),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FormInput::File(input) => {
|
||||||
|
if input.base.hidden.unwrap_or(false) || input.base.disabled.unwrap_or(false) {
|
||||||
|
return Ok(PromptOutcome::Continue);
|
||||||
|
}
|
||||||
|
|
||||||
|
let label = prompt_label_for_base(&input.base);
|
||||||
|
let value = prompt_text_with_inquire(
|
||||||
|
label.as_str(),
|
||||||
|
input.base.default_value.clone(),
|
||||||
|
Some("Path".to_string()),
|
||||||
|
!input.base.optional.unwrap_or(false),
|
||||||
|
)?;
|
||||||
|
match value {
|
||||||
|
PromptValue::Cancelled => Ok(PromptOutcome::Cancelled),
|
||||||
|
PromptValue::Value(Some(v)) => {
|
||||||
|
values.insert(input.base.name.clone(), JsonPrimitive::String(v));
|
||||||
|
Ok(PromptOutcome::Continue)
|
||||||
|
}
|
||||||
|
PromptValue::Value(None) => Ok(PromptOutcome::Continue),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FormInput::HttpRequest(input) => {
|
||||||
|
if input.base.hidden.unwrap_or(false) || input.base.disabled.unwrap_or(false) {
|
||||||
|
return Ok(PromptOutcome::Continue);
|
||||||
|
}
|
||||||
|
let label = prompt_label_for_base(&input.base);
|
||||||
|
let value = prompt_text_with_inquire(
|
||||||
|
label.as_str(),
|
||||||
|
input.base.default_value.clone(),
|
||||||
|
Some("Request ID".to_string()),
|
||||||
|
!input.base.optional.unwrap_or(false),
|
||||||
|
)?;
|
||||||
|
match value {
|
||||||
|
PromptValue::Cancelled => Ok(PromptOutcome::Cancelled),
|
||||||
|
PromptValue::Value(Some(v)) => {
|
||||||
|
values.insert(input.base.name.clone(), JsonPrimitive::String(v));
|
||||||
|
Ok(PromptOutcome::Continue)
|
||||||
|
}
|
||||||
|
PromptValue::Value(None) => Ok(PromptOutcome::Continue),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FormInput::KeyValue(input) => {
|
||||||
|
if input.base.hidden.unwrap_or(false) || input.base.disabled.unwrap_or(false) {
|
||||||
|
return Ok(PromptOutcome::Continue);
|
||||||
|
}
|
||||||
|
let label = prompt_label_for_base(&input.base);
|
||||||
|
let value = prompt_text_with_inquire(
|
||||||
|
label.as_str(),
|
||||||
|
input.base.default_value.clone(),
|
||||||
|
Some("JSON string".to_string()),
|
||||||
|
!input.base.optional.unwrap_or(false),
|
||||||
|
)?;
|
||||||
|
match value {
|
||||||
|
PromptValue::Cancelled => Ok(PromptOutcome::Cancelled),
|
||||||
|
PromptValue::Value(Some(v)) => {
|
||||||
|
values.insert(input.base.name.clone(), JsonPrimitive::String(v));
|
||||||
|
Ok(PromptOutcome::Continue)
|
||||||
|
}
|
||||||
|
PromptValue::Value(None) => Ok(PromptOutcome::Continue),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FormInput::Accordion(input) => {
|
||||||
|
if input.hidden.unwrap_or(false) {
|
||||||
|
return Ok(PromptOutcome::Continue);
|
||||||
|
}
|
||||||
|
if let Some(inputs) = &input.inputs {
|
||||||
|
for nested in inputs {
|
||||||
|
if prompt_form_input_for_cli(nested, values)? == PromptOutcome::Cancelled {
|
||||||
|
return Ok(PromptOutcome::Cancelled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(PromptOutcome::Continue)
|
||||||
|
}
|
||||||
|
FormInput::HStack(input) => {
|
||||||
|
if input.hidden.unwrap_or(false) {
|
||||||
|
return Ok(PromptOutcome::Continue);
|
||||||
|
}
|
||||||
|
if let Some(inputs) = &input.inputs {
|
||||||
|
for nested in inputs {
|
||||||
|
if prompt_form_input_for_cli(nested, values)? == PromptOutcome::Cancelled {
|
||||||
|
return Ok(PromptOutcome::Cancelled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(PromptOutcome::Continue)
|
||||||
|
}
|
||||||
|
FormInput::Banner(input) => {
|
||||||
|
if input.hidden.unwrap_or(false) {
|
||||||
|
return Ok(PromptOutcome::Continue);
|
||||||
|
}
|
||||||
|
if let Some(inputs) = &input.inputs {
|
||||||
|
for nested in inputs {
|
||||||
|
if prompt_form_input_for_cli(nested, values)? == PromptOutcome::Cancelled {
|
||||||
|
return Ok(PromptOutcome::Cancelled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(PromptOutcome::Continue)
|
||||||
|
}
|
||||||
|
FormInput::Markdown(input) => {
|
||||||
|
if !input.hidden.unwrap_or(false) && !input.content.trim().is_empty() {
|
||||||
|
let term = Term::stdout();
|
||||||
|
term.write_line(input.content.as_str()).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
Ok(PromptOutcome::Continue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PromptValue {
|
||||||
|
Value(Option<String>),
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_text_with_inquire(
|
||||||
|
label: &str,
|
||||||
|
default_value: Option<String>,
|
||||||
|
placeholder: Option<String>,
|
||||||
|
required: bool,
|
||||||
|
) -> Result<PromptValue, String> {
|
||||||
|
let default_value = default_value.and_then(|v| {
|
||||||
|
let trimmed = v.trim();
|
||||||
|
if trimmed.is_empty() { None } else { Some(v) }
|
||||||
|
});
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let message = prompt_message(label);
|
||||||
|
let mut prompt = Text::new(message.as_str());
|
||||||
|
if let Some(v) = default_value.as_deref() {
|
||||||
|
prompt = prompt.with_default(v);
|
||||||
|
}
|
||||||
|
if let Some(v) = placeholder.as_deref() {
|
||||||
|
if !v.trim().is_empty() {
|
||||||
|
prompt = prompt.with_placeholder(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let result = prompt.prompt();
|
||||||
|
match result {
|
||||||
|
Ok(v) => {
|
||||||
|
let v = v.trim().to_string();
|
||||||
|
if v.is_empty() {
|
||||||
|
if let Some(default) = default_value.clone() {
|
||||||
|
if !default.trim().is_empty() {
|
||||||
|
return Ok(PromptValue::Value(Some(default)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if required {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return Ok(PromptValue::Value(None));
|
||||||
|
}
|
||||||
|
return Ok(PromptValue::Value(Some(v)));
|
||||||
|
}
|
||||||
|
Err(inquire::InquireError::OperationCanceled)
|
||||||
|
| Err(inquire::InquireError::OperationInterrupted) => {
|
||||||
|
return Ok(PromptValue::Cancelled);
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_password_with_inquire(
|
||||||
|
label: &str,
|
||||||
|
default_value: Option<String>,
|
||||||
|
required: bool,
|
||||||
|
) -> Result<PromptValue, String> {
|
||||||
|
let default_value = default_value.and_then(|v| {
|
||||||
|
let trimmed = v.trim();
|
||||||
|
if trimmed.is_empty() { None } else { Some(v) }
|
||||||
|
});
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let message = prompt_message(label);
|
||||||
|
let mut prompt = Password::new(message.as_str()).without_confirmation();
|
||||||
|
prompt = prompt.with_display_mode(PasswordDisplayMode::Masked);
|
||||||
|
if default_value.as_ref().is_some_and(|v| !v.trim().is_empty()) {
|
||||||
|
prompt = prompt.with_help_message("Leave blank to use the default value");
|
||||||
|
}
|
||||||
|
let result = prompt.prompt();
|
||||||
|
match result {
|
||||||
|
Ok(v) => {
|
||||||
|
let v = v.trim().to_string();
|
||||||
|
if v.is_empty() {
|
||||||
|
if let Some(default) = default_value.clone() {
|
||||||
|
if !default.trim().is_empty() {
|
||||||
|
return Ok(PromptValue::Value(Some(default)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if required {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return Ok(PromptValue::Value(None));
|
||||||
|
}
|
||||||
|
return Ok(PromptValue::Value(Some(v)));
|
||||||
|
}
|
||||||
|
Err(inquire::InquireError::OperationCanceled)
|
||||||
|
| Err(inquire::InquireError::OperationInterrupted) => {
|
||||||
|
return Ok(PromptValue::Cancelled);
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_editor_with_inquire(
|
||||||
|
label: &str,
|
||||||
|
default_value: Option<String>,
|
||||||
|
required: bool,
|
||||||
|
) -> Result<PromptValue, String> {
|
||||||
|
loop {
|
||||||
|
let message = prompt_message(label);
|
||||||
|
let mut prompt = Editor::new(message.as_str());
|
||||||
|
if let Some(v) = default_value.as_deref() {
|
||||||
|
prompt = prompt.with_predefined_text(v);
|
||||||
|
}
|
||||||
|
let result = prompt.prompt();
|
||||||
|
match result {
|
||||||
|
Ok(v) => {
|
||||||
|
let v = v.trim().to_string();
|
||||||
|
if v.is_empty() {
|
||||||
|
if required {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return Ok(PromptValue::Value(None));
|
||||||
|
}
|
||||||
|
return Ok(PromptValue::Value(Some(v)));
|
||||||
|
}
|
||||||
|
Err(inquire::InquireError::OperationCanceled)
|
||||||
|
| Err(inquire::InquireError::OperationInterrupted) => {
|
||||||
|
return Ok(PromptValue::Cancelled);
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_select_with_inquire(
|
||||||
|
label: &str,
|
||||||
|
options: Vec<String>,
|
||||||
|
default_value: Option<String>,
|
||||||
|
required: bool,
|
||||||
|
) -> Result<PromptValue, String> {
|
||||||
|
if options.is_empty() {
|
||||||
|
if required {
|
||||||
|
return Err(format!("Select input '{label}' has no options"));
|
||||||
|
}
|
||||||
|
return Ok(PromptValue::Value(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = default_value
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|d| options.iter().position(|o| o == d))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let message = prompt_message(label);
|
||||||
|
let mut prompt = Select::new(message.as_str(), options);
|
||||||
|
if default_value.is_some() {
|
||||||
|
prompt = prompt.with_starting_cursor(index);
|
||||||
|
}
|
||||||
|
match prompt.prompt() {
|
||||||
|
Ok(v) => Ok(PromptValue::Value(Some(v))),
|
||||||
|
Err(inquire::InquireError::OperationCanceled)
|
||||||
|
| Err(inquire::InquireError::OperationInterrupted) => Ok(PromptValue::Cancelled),
|
||||||
|
Err(err) => Err(err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_confirm_with_inquire(label: &str, default: bool) -> Result<PromptValue, String> {
|
||||||
|
let message = prompt_message(label);
|
||||||
|
match Confirm::new(message.as_str()).with_default(default).prompt() {
|
||||||
|
Ok(v) => Ok(PromptValue::Value(Some(if v { "true" } else { "false" }.to_string()))),
|
||||||
|
Err(inquire::InquireError::OperationCanceled)
|
||||||
|
| Err(inquire::InquireError::OperationInterrupted) => Ok(PromptValue::Cancelled),
|
||||||
|
Err(err) => Err(err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_message(label: &str) -> String {
|
||||||
|
format!("{label}:")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_label_for_base(base: &yaak_plugins::events::FormInputBase) -> String {
|
||||||
|
if let Some(label) = &base.label {
|
||||||
|
if !label.is_empty() {
|
||||||
|
return label.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.name.clone()
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "@gschier/prompt-form-cli-test",
|
||||||
|
"displayName": "Prompt Form CLI Test",
|
||||||
|
"description": "Tiny plugin to test prompt.form in the CLI host",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"build": "yaak plugin build"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
export const plugin = {
|
||||||
|
templateFunctions: [
|
||||||
|
{
|
||||||
|
name: 'prompt.form.demo',
|
||||||
|
description: 'Prompt for a few values using prompt.form and return a JSON string',
|
||||||
|
args: [],
|
||||||
|
async onRender(ctx, args) {
|
||||||
|
if (args.purpose !== 'send') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = await ctx.prompt.form({
|
||||||
|
id: 'prompt-form-demo',
|
||||||
|
title: 'CLI Prompt Form Demo',
|
||||||
|
description: 'Fill out the fields to test prompt.form in the CLI.',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'username',
|
||||||
|
label: 'Username',
|
||||||
|
defaultValue: 'alice'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'password',
|
||||||
|
label: 'Password',
|
||||||
|
password: true,
|
||||||
|
optional: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
name: 'region',
|
||||||
|
label: 'Region',
|
||||||
|
defaultValue: 'us',
|
||||||
|
options: [
|
||||||
|
{ label: 'US', value: 'us' },
|
||||||
|
{ label: 'EU', value: 'eu' },
|
||||||
|
{ label: 'APAC', value: 'apac' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'checkbox',
|
||||||
|
name: 'remember',
|
||||||
|
label: 'Remember',
|
||||||
|
defaultValue: 'true',
|
||||||
|
optional: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (values == null) {
|
||||||
|
throw new Error('Prompt form cancelled');
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user