mirror of
https://github.com/lWolvesl/claw-code.git
synced 2026-04-02 07:41:52 +08:00
Merge remote-tracking branch 'origin/rcc/cli' into dev/rust
This commit is contained in:
@@ -305,48 +305,40 @@ struct AnthropicErrorBody {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ALT_REQUEST_ID_HEADER, REQUEST_ID_HEADER};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::types::{ContentBlockDelta, MessageRequest};
|
||||
|
||||
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
|
||||
static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
ENV_LOCK
|
||||
.get_or_init(|| Mutex::new(()))
|
||||
.lock()
|
||||
.expect("env lock should not be poisoned")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_api_key_requires_presence() {
|
||||
let previous_auth = std::env::var("ANTHROPIC_AUTH_TOKEN").ok();
|
||||
let previous_key = std::env::var("ANTHROPIC_API_KEY").ok();
|
||||
let _guard = env_lock();
|
||||
std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
|
||||
std::env::remove_var("ANTHROPIC_API_KEY");
|
||||
let error = super::read_api_key().expect_err("missing key should error");
|
||||
assert!(matches!(error, crate::error::ApiError::MissingApiKey));
|
||||
match previous_auth {
|
||||
Some(value) => std::env::set_var("ANTHROPIC_AUTH_TOKEN", value),
|
||||
None => std::env::remove_var("ANTHROPIC_AUTH_TOKEN"),
|
||||
}
|
||||
match previous_key {
|
||||
Some(value) => std::env::set_var("ANTHROPIC_API_KEY", value),
|
||||
None => std::env::remove_var("ANTHROPIC_API_KEY"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_api_key_requires_non_empty_value() {
|
||||
let previous_auth = std::env::var("ANTHROPIC_AUTH_TOKEN").ok();
|
||||
let previous_key = std::env::var("ANTHROPIC_API_KEY").ok();
|
||||
let _guard = env_lock();
|
||||
std::env::set_var("ANTHROPIC_AUTH_TOKEN", "");
|
||||
std::env::remove_var("ANTHROPIC_API_KEY");
|
||||
let error = super::read_api_key().expect_err("empty key should error");
|
||||
assert!(matches!(error, crate::error::ApiError::MissingApiKey));
|
||||
match previous_auth {
|
||||
Some(value) => std::env::set_var("ANTHROPIC_AUTH_TOKEN", value),
|
||||
None => std::env::remove_var("ANTHROPIC_AUTH_TOKEN"),
|
||||
}
|
||||
match previous_key {
|
||||
Some(value) => std::env::set_var("ANTHROPIC_API_KEY", value),
|
||||
None => std::env::remove_var("ANTHROPIC_API_KEY"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_api_key_prefers_api_key_env() {
|
||||
let _guard = env_lock();
|
||||
std::env::set_var("ANTHROPIC_AUTH_TOKEN", "auth-token");
|
||||
std::env::set_var("ANTHROPIC_API_KEY", "legacy-key");
|
||||
assert_eq!(
|
||||
@@ -359,6 +351,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn read_auth_token_reads_auth_token_env() {
|
||||
let _guard = env_lock();
|
||||
std::env::set_var("ANTHROPIC_AUTH_TOKEN", "auth-token");
|
||||
assert_eq!(super::read_auth_token().as_deref(), Some("auth-token"));
|
||||
std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
|
||||
|
||||
@@ -30,6 +30,168 @@ impl CommandRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct SlashCommandSpec {
|
||||
pub name: &'static str,
|
||||
pub summary: &'static str,
|
||||
pub argument_hint: Option<&'static str>,
|
||||
pub resume_supported: bool,
|
||||
}
|
||||
|
||||
const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
SlashCommandSpec {
|
||||
name: "help",
|
||||
summary: "Show available slash commands",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "status",
|
||||
summary: "Show current session status",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "compact",
|
||||
summary: "Compact local session history",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "model",
|
||||
summary: "Show or switch the active model",
|
||||
argument_hint: Some("[model]"),
|
||||
resume_supported: false,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "permissions",
|
||||
summary: "Show or switch the active permission mode",
|
||||
argument_hint: Some("[read-only|workspace-write|danger-full-access]"),
|
||||
resume_supported: false,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "clear",
|
||||
summary: "Start a fresh local session",
|
||||
argument_hint: Some("[--confirm]"),
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "cost",
|
||||
summary: "Show cumulative token usage for this session",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "resume",
|
||||
summary: "Load a saved session into the REPL",
|
||||
argument_hint: Some("<session-path>"),
|
||||
resume_supported: false,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "config",
|
||||
summary: "Inspect discovered Claude config files",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "memory",
|
||||
summary: "Inspect loaded Claude instruction memory files",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "init",
|
||||
summary: "Create a starter CLAUDE.md for this repo",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SlashCommand {
|
||||
Help,
|
||||
Status,
|
||||
Compact,
|
||||
Model { model: Option<String> },
|
||||
Permissions { mode: Option<String> },
|
||||
Clear { confirm: bool },
|
||||
Cost,
|
||||
Resume { session_path: Option<String> },
|
||||
Config,
|
||||
Memory,
|
||||
Init,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl SlashCommand {
|
||||
#[must_use]
|
||||
pub fn parse(input: &str) -> Option<Self> {
|
||||
let trimmed = input.trim();
|
||||
if !trimmed.starts_with('/') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut parts = trimmed.trim_start_matches('/').split_whitespace();
|
||||
let command = parts.next().unwrap_or_default();
|
||||
Some(match command {
|
||||
"help" => Self::Help,
|
||||
"status" => Self::Status,
|
||||
"compact" => Self::Compact,
|
||||
"model" => Self::Model {
|
||||
model: parts.next().map(ToOwned::to_owned),
|
||||
},
|
||||
"permissions" => Self::Permissions {
|
||||
mode: parts.next().map(ToOwned::to_owned),
|
||||
},
|
||||
"clear" => Self::Clear {
|
||||
confirm: parts.next() == Some("--confirm"),
|
||||
},
|
||||
"cost" => Self::Cost,
|
||||
"resume" => Self::Resume {
|
||||
session_path: parts.next().map(ToOwned::to_owned),
|
||||
},
|
||||
"config" => Self::Config,
|
||||
"memory" => Self::Memory,
|
||||
"init" => Self::Init,
|
||||
other => Self::Unknown(other.to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn slash_command_specs() -> &'static [SlashCommandSpec] {
|
||||
SLASH_COMMAND_SPECS
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn resume_supported_slash_commands() -> Vec<&'static SlashCommandSpec> {
|
||||
slash_command_specs()
|
||||
.iter()
|
||||
.filter(|spec| spec.resume_supported)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn render_slash_command_help() -> String {
|
||||
let mut lines = vec![
|
||||
"Available commands:".to_string(),
|
||||
" (resume-safe commands are marked with [resume])".to_string(),
|
||||
];
|
||||
for spec in slash_command_specs() {
|
||||
let name = match spec.argument_hint {
|
||||
Some(argument_hint) => format!("/{} {}", spec.name, argument_hint),
|
||||
None => format!("/{}", spec.name),
|
||||
};
|
||||
let resume = if spec.resume_supported {
|
||||
" [resume]"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
lines.push(format!(" {name:<20} {}{}", spec.summary, resume));
|
||||
}
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SlashCommandResult {
|
||||
pub message: String,
|
||||
@@ -42,13 +204,8 @@ pub fn handle_slash_command(
|
||||
session: &Session,
|
||||
compaction: CompactionConfig,
|
||||
) -> Option<SlashCommandResult> {
|
||||
let trimmed = input.trim();
|
||||
if !trimmed.starts_with('/') {
|
||||
return None;
|
||||
}
|
||||
|
||||
match trimmed.split_whitespace().next() {
|
||||
Some("/compact") => {
|
||||
match SlashCommand::parse(input)? {
|
||||
SlashCommand::Compact => {
|
||||
let result = compact_session(session, compaction);
|
||||
let message = if result.removed_message_count == 0 {
|
||||
"Compaction skipped: session is below the compaction threshold.".to_string()
|
||||
@@ -63,15 +220,90 @@ pub fn handle_slash_command(
|
||||
session: result.compacted_session,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
SlashCommand::Help => Some(SlashCommandResult {
|
||||
message: render_slash_command_help(),
|
||||
session: session.clone(),
|
||||
}),
|
||||
SlashCommand::Status
|
||||
| SlashCommand::Model { .. }
|
||||
| SlashCommand::Permissions { .. }
|
||||
| SlashCommand::Clear { .. }
|
||||
| SlashCommand::Cost
|
||||
| SlashCommand::Resume { .. }
|
||||
| SlashCommand::Config
|
||||
| SlashCommand::Memory
|
||||
| SlashCommand::Init
|
||||
| SlashCommand::Unknown(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::handle_slash_command;
|
||||
use super::{
|
||||
handle_slash_command, render_slash_command_help, resume_supported_slash_commands,
|
||||
slash_command_specs, SlashCommand,
|
||||
};
|
||||
use runtime::{CompactionConfig, ContentBlock, ConversationMessage, MessageRole, Session};
|
||||
|
||||
#[test]
|
||||
fn parses_supported_slash_commands() {
|
||||
assert_eq!(SlashCommand::parse("/help"), Some(SlashCommand::Help));
|
||||
assert_eq!(SlashCommand::parse(" /status "), Some(SlashCommand::Status));
|
||||
assert_eq!(
|
||||
SlashCommand::parse("/model claude-opus"),
|
||||
Some(SlashCommand::Model {
|
||||
model: Some("claude-opus".to_string()),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
SlashCommand::parse("/model"),
|
||||
Some(SlashCommand::Model { model: None })
|
||||
);
|
||||
assert_eq!(
|
||||
SlashCommand::parse("/permissions read-only"),
|
||||
Some(SlashCommand::Permissions {
|
||||
mode: Some("read-only".to_string()),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
SlashCommand::parse("/clear"),
|
||||
Some(SlashCommand::Clear { confirm: false })
|
||||
);
|
||||
assert_eq!(
|
||||
SlashCommand::parse("/clear --confirm"),
|
||||
Some(SlashCommand::Clear { confirm: true })
|
||||
);
|
||||
assert_eq!(SlashCommand::parse("/cost"), Some(SlashCommand::Cost));
|
||||
assert_eq!(
|
||||
SlashCommand::parse("/resume session.json"),
|
||||
Some(SlashCommand::Resume {
|
||||
session_path: Some("session.json".to_string()),
|
||||
})
|
||||
);
|
||||
assert_eq!(SlashCommand::parse("/config"), Some(SlashCommand::Config));
|
||||
assert_eq!(SlashCommand::parse("/memory"), Some(SlashCommand::Memory));
|
||||
assert_eq!(SlashCommand::parse("/init"), Some(SlashCommand::Init));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_help_from_shared_specs() {
|
||||
let help = render_slash_command_help();
|
||||
assert!(help.contains("resume-safe commands"));
|
||||
assert!(help.contains("/help"));
|
||||
assert!(help.contains("/status"));
|
||||
assert!(help.contains("/compact"));
|
||||
assert!(help.contains("/model [model]"));
|
||||
assert!(help.contains("/permissions [read-only|workspace-write|danger-full-access]"));
|
||||
assert!(help.contains("/clear [--confirm]"));
|
||||
assert!(help.contains("/cost"));
|
||||
assert!(help.contains("/resume <session-path>"));
|
||||
assert!(help.contains("/config"));
|
||||
assert!(help.contains("/memory"));
|
||||
assert!(help.contains("/init"));
|
||||
assert_eq!(slash_command_specs().len(), 11);
|
||||
assert_eq!(resume_supported_slash_commands().len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compacts_sessions_via_slash_command() {
|
||||
let session = Session {
|
||||
@@ -103,8 +335,40 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_unknown_slash_commands() {
|
||||
fn help_command_is_non_mutating() {
|
||||
let session = Session::new();
|
||||
let result = handle_slash_command("/help", &session, CompactionConfig::default())
|
||||
.expect("help command should be handled");
|
||||
assert_eq!(result.session, session);
|
||||
assert!(result.message.contains("Available commands:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_unknown_or_runtime_bound_slash_commands() {
|
||||
let session = Session::new();
|
||||
assert!(handle_slash_command("/unknown", &session, CompactionConfig::default()).is_none());
|
||||
assert!(handle_slash_command("/status", &session, CompactionConfig::default()).is_none());
|
||||
assert!(
|
||||
handle_slash_command("/model claude", &session, CompactionConfig::default()).is_none()
|
||||
);
|
||||
assert!(handle_slash_command(
|
||||
"/permissions read-only",
|
||||
&session,
|
||||
CompactionConfig::default()
|
||||
)
|
||||
.is_none());
|
||||
assert!(handle_slash_command("/clear", &session, CompactionConfig::default()).is_none());
|
||||
assert!(
|
||||
handle_slash_command("/clear --confirm", &session, CompactionConfig::default())
|
||||
.is_none()
|
||||
);
|
||||
assert!(handle_slash_command("/cost", &session, CompactionConfig::default()).is_none());
|
||||
assert!(handle_slash_command(
|
||||
"/resume session.json",
|
||||
&session,
|
||||
CompactionConfig::default()
|
||||
)
|
||||
.is_none());
|
||||
assert!(handle_slash_command("/config", &session, CompactionConfig::default()).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,10 @@ impl UpstreamPaths {
|
||||
.as_ref()
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| workspace_dir.as_ref().to_path_buf());
|
||||
let repo_root = workspace_dir
|
||||
let primary_repo_root = workspace_dir
|
||||
.parent()
|
||||
.map_or_else(|| PathBuf::from(".."), Path::to_path_buf);
|
||||
let repo_root = resolve_upstream_repo_root(&primary_repo_root);
|
||||
Self { repo_root }
|
||||
}
|
||||
|
||||
@@ -53,6 +54,42 @@ pub struct ExtractedManifest {
|
||||
pub bootstrap: BootstrapPlan,
|
||||
}
|
||||
|
||||
fn resolve_upstream_repo_root(primary_repo_root: &Path) -> PathBuf {
|
||||
let candidates = upstream_repo_candidates(primary_repo_root);
|
||||
candidates
|
||||
.into_iter()
|
||||
.find(|candidate| candidate.join("src/commands.ts").is_file())
|
||||
.unwrap_or_else(|| primary_repo_root.to_path_buf())
|
||||
}
|
||||
|
||||
fn upstream_repo_candidates(primary_repo_root: &Path) -> Vec<PathBuf> {
|
||||
let mut candidates = vec![primary_repo_root.to_path_buf()];
|
||||
|
||||
if let Some(explicit) = std::env::var_os("CLAUDE_CODE_UPSTREAM") {
|
||||
candidates.push(PathBuf::from(explicit));
|
||||
}
|
||||
|
||||
for ancestor in primary_repo_root.ancestors().take(4) {
|
||||
candidates.push(ancestor.join("claude-code"));
|
||||
candidates.push(ancestor.join("clawd-code"));
|
||||
}
|
||||
|
||||
candidates.push(
|
||||
primary_repo_root
|
||||
.join("reference-source")
|
||||
.join("claude-code"),
|
||||
);
|
||||
candidates.push(primary_repo_root.join("vendor").join("claude-code"));
|
||||
|
||||
let mut deduped = Vec::new();
|
||||
for candidate in candidates {
|
||||
if !deduped.iter().any(|seen: &PathBuf| seen == &candidate) {
|
||||
deduped.push(candidate);
|
||||
}
|
||||
}
|
||||
deduped
|
||||
}
|
||||
|
||||
pub fn extract_manifest(paths: &UpstreamPaths) -> std::io::Result<ExtractedManifest> {
|
||||
let commands_source = fs::read_to_string(paths.commands_path())?;
|
||||
let tools_source = fs::read_to_string(paths.tools_path())?;
|
||||
|
||||
@@ -285,7 +285,7 @@ pub fn grep_search(input: &GrepSearchInput) -> io::Result<GrepSearchOutput> {
|
||||
.output_mode
|
||||
.clone()
|
||||
.unwrap_or_else(|| String::from("files_with_matches"));
|
||||
let context = input.context.or(input.context_short).unwrap_or(0);
|
||||
let context_window = input.context.or(input.context_short).unwrap_or(0);
|
||||
|
||||
let mut filenames = Vec::new();
|
||||
let mut content_lines = Vec::new();
|
||||
@@ -296,12 +296,12 @@ pub fn grep_search(input: &GrepSearchInput) -> io::Result<GrepSearchOutput> {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(file_text) = fs::read_to_string(&file_path) else {
|
||||
let Ok(file_content) = fs::read_to_string(&file_path) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if output_mode == "count" {
|
||||
let count = regex.find_iter(&file_text).count();
|
||||
let count = regex.find_iter(&file_content).count();
|
||||
if count > 0 {
|
||||
filenames.push(file_path.to_string_lossy().into_owned());
|
||||
total_matches += count;
|
||||
@@ -309,7 +309,7 @@ pub fn grep_search(input: &GrepSearchInput) -> io::Result<GrepSearchOutput> {
|
||||
continue;
|
||||
}
|
||||
|
||||
let lines: Vec<&str> = file_text.lines().collect();
|
||||
let lines: Vec<&str> = file_content.lines().collect();
|
||||
let mut matched_lines = Vec::new();
|
||||
for (index, line) in lines.iter().enumerate() {
|
||||
if regex.is_match(line) {
|
||||
@@ -325,15 +325,15 @@ pub fn grep_search(input: &GrepSearchInput) -> io::Result<GrepSearchOutput> {
|
||||
filenames.push(file_path.to_string_lossy().into_owned());
|
||||
if output_mode == "content" {
|
||||
for index in matched_lines {
|
||||
let start = index.saturating_sub(input.before.unwrap_or(context));
|
||||
let end = (index + input.after.unwrap_or(context) + 1).min(lines.len());
|
||||
for (current, line_text) in lines.iter().enumerate().take(end).skip(start) {
|
||||
let start = index.saturating_sub(input.before.unwrap_or(context_window));
|
||||
let end = (index + input.after.unwrap_or(context_window) + 1).min(lines.len());
|
||||
for (current, line_content) in lines.iter().enumerate().take(end).skip(start) {
|
||||
let prefix = if input.line_numbers.unwrap_or(true) {
|
||||
format!("{}:{}:", file_path.to_string_lossy(), current + 1)
|
||||
} else {
|
||||
format!("{}:", file_path.to_string_lossy())
|
||||
};
|
||||
content_lines.push(format!("{prefix}{line_text}"));
|
||||
content_lines.push(format!("{prefix}{line_content}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -341,7 +341,7 @@ pub fn grep_search(input: &GrepSearchInput) -> io::Result<GrepSearchOutput> {
|
||||
|
||||
let (filenames, applied_limit, applied_offset) =
|
||||
apply_limit(filenames, input.head_limit, input.offset);
|
||||
let content_output = if output_mode == "content" {
|
||||
let content = if output_mode == "content" {
|
||||
let (lines, limit, offset) = apply_limit(content_lines, input.head_limit, input.offset);
|
||||
return Ok(GrepSearchOutput {
|
||||
mode: Some(output_mode),
|
||||
@@ -361,7 +361,7 @@ pub fn grep_search(input: &GrepSearchInput) -> io::Result<GrepSearchOutput> {
|
||||
mode: Some(output_mode.clone()),
|
||||
num_files: filenames.len(),
|
||||
filenames,
|
||||
content: content_output,
|
||||
content,
|
||||
num_lines: None,
|
||||
num_matches: (output_mode == "count").then_some(total_matches),
|
||||
applied_limit,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -172,12 +172,12 @@ fn run_bash(input: BashCommandInput) -> Result<String, String> {
|
||||
|
||||
fn run_read_file(input: &ReadFileInput) -> Result<String, String> {
|
||||
to_pretty_json(
|
||||
read_file(&input.path, input.offset, input.limit).map_err(|error| error.to_string())?,
|
||||
read_file(&input.path, input.offset, input.limit).map_err(|error| io_to_string(&error))?,
|
||||
)
|
||||
}
|
||||
|
||||
fn run_write_file(input: &WriteFileInput) -> Result<String, String> {
|
||||
to_pretty_json(write_file(&input.path, &input.content).map_err(|error| error.to_string())?)
|
||||
to_pretty_json(write_file(&input.path, &input.content).map_err(|error| io_to_string(&error))?)
|
||||
}
|
||||
|
||||
fn run_edit_file(input: &EditFileInput) -> Result<String, String> {
|
||||
@@ -188,24 +188,28 @@ fn run_edit_file(input: &EditFileInput) -> Result<String, String> {
|
||||
&input.new_string,
|
||||
input.replace_all.unwrap_or(false),
|
||||
)
|
||||
.map_err(|error| error.to_string())?,
|
||||
.map_err(|error| io_to_string(&error))?,
|
||||
)
|
||||
}
|
||||
|
||||
fn run_glob_search(input: &GlobSearchInputValue) -> Result<String, String> {
|
||||
to_pretty_json(
|
||||
glob_search(&input.pattern, input.path.as_deref()).map_err(|error| error.to_string())?,
|
||||
glob_search(&input.pattern, input.path.as_deref()).map_err(|error| io_to_string(&error))?,
|
||||
)
|
||||
}
|
||||
|
||||
fn run_grep_search(input: &GrepSearchInput) -> Result<String, String> {
|
||||
to_pretty_json(grep_search(input).map_err(|error| error.to_string())?)
|
||||
to_pretty_json(grep_search(input).map_err(|error| io_to_string(&error))?)
|
||||
}
|
||||
|
||||
fn to_pretty_json<T: serde::Serialize>(value: T) -> Result<String, String> {
|
||||
serde_json::to_string_pretty(&value).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn io_to_string(error: &std::io::Error) -> String {
|
||||
error.to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ReadFileInput {
|
||||
path: String,
|
||||
|
||||
Reference in New Issue
Block a user