zarz 0.3.1-alpha → 0.3.5-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +193 -22
- package/bin/zarz.js +10 -31
- package/package.json +6 -10
- package/scripts/postinstall.js +162 -40
- package/Cargo.lock +0 -2815
- package/Cargo.toml +0 -30
- package/QUICKSTART.md +0 -326
- package/src/cli.rs +0 -201
- package/src/config.rs +0 -249
- package/src/conversation_store.rs +0 -183
- package/src/executor.rs +0 -164
- package/src/fs_ops.rs +0 -117
- package/src/intelligence/context.rs +0 -143
- package/src/intelligence/mod.rs +0 -60
- package/src/intelligence/rust_parser.rs +0 -141
- package/src/intelligence/symbol_search.rs +0 -97
- package/src/main.rs +0 -867
- package/src/mcp/client.rs +0 -316
- package/src/mcp/config.rs +0 -133
- package/src/mcp/manager.rs +0 -186
- package/src/mcp/mod.rs +0 -12
- package/src/mcp/types.rs +0 -170
- package/src/providers/anthropic.rs +0 -214
- package/src/providers/glm.rs +0 -209
- package/src/providers/mod.rs +0 -90
- package/src/providers/openai.rs +0 -197
- package/src/repl.rs +0 -1910
- package/src/session.rs +0 -173
package/src/config.rs
DELETED
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
use anyhow::{Context, Result};
|
|
2
|
-
use crossterm::style::{Color, Stylize};
|
|
3
|
-
use dialoguer::{theme::ColorfulTheme, Select};
|
|
4
|
-
use serde::{Deserialize, Serialize};
|
|
5
|
-
use std::fs;
|
|
6
|
-
use std::io::{self, Write};
|
|
7
|
-
use std::path::PathBuf;
|
|
8
|
-
|
|
9
|
-
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
10
|
-
pub struct Config {
|
|
11
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
12
|
-
pub anthropic_api_key: Option<String>,
|
|
13
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
14
|
-
pub openai_api_key: Option<String>,
|
|
15
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
16
|
-
pub glm_api_key: Option<String>,
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
impl Config {
|
|
20
|
-
/// Get the path to the config file (~/.zarz/config.toml)
|
|
21
|
-
pub fn config_path() -> Result<PathBuf> {
|
|
22
|
-
let home = dirs::home_dir()
|
|
23
|
-
.context("Could not determine home directory")?;
|
|
24
|
-
Ok(home.join(".zarz").join("config.toml"))
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/// Load config from file, or return default if file doesn't exist
|
|
28
|
-
pub fn load() -> Result<Self> {
|
|
29
|
-
let path = Self::config_path()?;
|
|
30
|
-
|
|
31
|
-
if !path.exists() {
|
|
32
|
-
return Ok(Self::default());
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let content = fs::read_to_string(&path)
|
|
36
|
-
.context("Failed to read config file")?;
|
|
37
|
-
|
|
38
|
-
let config: Config = toml::from_str(&content)
|
|
39
|
-
.context("Failed to parse config file")?;
|
|
40
|
-
|
|
41
|
-
Ok(config)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/// Save config to file
|
|
45
|
-
pub fn save(&self) -> Result<()> {
|
|
46
|
-
let path = Self::config_path()?;
|
|
47
|
-
|
|
48
|
-
// Create parent directory if it doesn't exist
|
|
49
|
-
if let Some(parent) = path.parent() {
|
|
50
|
-
fs::create_dir_all(parent)
|
|
51
|
-
.context("Failed to create config directory")?;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
let content = toml::to_string_pretty(self)
|
|
55
|
-
.context("Failed to serialize config")?;
|
|
56
|
-
|
|
57
|
-
fs::write(&path, content)
|
|
58
|
-
.context("Failed to write config file")?;
|
|
59
|
-
|
|
60
|
-
Ok(())
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/// Check if at least one API key is configured
|
|
64
|
-
pub fn has_api_key(&self) -> bool {
|
|
65
|
-
self.anthropic_api_key.is_some() || self.openai_api_key.is_some() || self.glm_api_key.is_some()
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/// Interactive setup to get API keys from user
|
|
69
|
-
pub fn interactive_setup() -> Result<Self> {
|
|
70
|
-
let theme = ColorfulTheme::default();
|
|
71
|
-
|
|
72
|
-
println!(
|
|
73
|
-
"\n{}\n",
|
|
74
|
-
"ZarzCLI Setup".bold().with(Color::Cyan)
|
|
75
|
-
);
|
|
76
|
-
println!(
|
|
77
|
-
"{}",
|
|
78
|
-
"Choose a provider to configure. Use the arrow keys and press Enter.".with(Color::DarkGrey)
|
|
79
|
-
);
|
|
80
|
-
println!(
|
|
81
|
-
"{}",
|
|
82
|
-
"API keys are displayed while typing so you can verify them, then hidden before storing.".with(Color::DarkGrey)
|
|
83
|
-
);
|
|
84
|
-
println!(
|
|
85
|
-
"{}\n",
|
|
86
|
-
"You can configure additional providers later with `zarz config --reset`.".with(Color::DarkGrey)
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
let options = vec![
|
|
90
|
-
"Anthropic Claude (recommended for coding)".bold().with(Color::Yellow).to_string(),
|
|
91
|
-
"OpenAI GPT".bold().with(Color::Yellow).to_string(),
|
|
92
|
-
"GLM (Z.AI - International GLM-4.6)".bold().with(Color::Yellow).to_string(),
|
|
93
|
-
];
|
|
94
|
-
|
|
95
|
-
let selection = Select::with_theme(&theme)
|
|
96
|
-
.with_prompt("Select a provider to set up")
|
|
97
|
-
.items(&options)
|
|
98
|
-
.default(0)
|
|
99
|
-
.interact()?;
|
|
100
|
-
|
|
101
|
-
let mut config = Self::default();
|
|
102
|
-
let mut enabled = Vec::new();
|
|
103
|
-
|
|
104
|
-
match selection {
|
|
105
|
-
0 => {
|
|
106
|
-
let key = Self::prompt_for_key("Anthropic API key")?;
|
|
107
|
-
config.anthropic_api_key = Some(key);
|
|
108
|
-
enabled.push("Anthropic Claude");
|
|
109
|
-
println!("{}\n", "✓ Anthropic ready".with(Color::Green));
|
|
110
|
-
}
|
|
111
|
-
1 => {
|
|
112
|
-
let key = Self::prompt_for_key("OpenAI API key")?;
|
|
113
|
-
config.openai_api_key = Some(key);
|
|
114
|
-
enabled.push("OpenAI GPT");
|
|
115
|
-
println!("{}\n", "✓ OpenAI ready".with(Color::Green));
|
|
116
|
-
}
|
|
117
|
-
_ => {
|
|
118
|
-
let key = Self::prompt_for_key("GLM API key")?;
|
|
119
|
-
config.glm_api_key = Some(key);
|
|
120
|
-
enabled.push("GLM 4.6");
|
|
121
|
-
println!("{}\n", "✓ GLM ready".with(Color::Green));
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if !config.has_api_key() {
|
|
126
|
-
anyhow::bail!("At least one API key is required to use ZarzCLI");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
config.save()?;
|
|
130
|
-
println!(
|
|
131
|
-
"{} {}\n",
|
|
132
|
-
"✅".with(Color::Green),
|
|
133
|
-
format!(
|
|
134
|
-
"Configuration saved to {}",
|
|
135
|
-
Self::config_path()?.display()
|
|
136
|
-
)
|
|
137
|
-
.bold()
|
|
138
|
-
);
|
|
139
|
-
println!(
|
|
140
|
-
"{}",
|
|
141
|
-
format!("Enabled providers: {}", enabled.join(", ")).with(Color::Green)
|
|
142
|
-
);
|
|
143
|
-
println!(
|
|
144
|
-
"{}\n",
|
|
145
|
-
"Run `zarz` any time to start chatting.".with(Color::DarkGrey)
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
Ok(config)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
fn prompt_for_key(label: &str) -> Result<String> {
|
|
152
|
-
loop {
|
|
153
|
-
print!("Enter your {}: ", label);
|
|
154
|
-
io::stdout().flush().ok();
|
|
155
|
-
|
|
156
|
-
let mut key = String::new();
|
|
157
|
-
io::stdin()
|
|
158
|
-
.read_line(&mut key)
|
|
159
|
-
.context("Failed to read API key from stdin")?;
|
|
160
|
-
|
|
161
|
-
let trimmed = key.trim();
|
|
162
|
-
if trimmed.is_empty() {
|
|
163
|
-
println!("{}", "Key cannot be empty. Please try again.".with(Color::Red));
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
println!("{}", "Key captured ✔".with(Color::Green));
|
|
168
|
-
println!(
|
|
169
|
-
"{}",
|
|
170
|
-
"(The key is now stored securely and will no longer be displayed.)".with(Color::DarkGrey)
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
return Ok(trimmed.to_string());
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/// Get Anthropic API key from config or environment
|
|
178
|
-
pub fn get_anthropic_key(&self) -> Option<String> {
|
|
179
|
-
std::env::var("ANTHROPIC_API_KEY")
|
|
180
|
-
.ok()
|
|
181
|
-
.or_else(|| self.anthropic_api_key.clone())
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/// Get OpenAI API key from config or environment
|
|
185
|
-
pub fn get_openai_key(&self) -> Option<String> {
|
|
186
|
-
std::env::var("OPENAI_API_KEY")
|
|
187
|
-
.ok()
|
|
188
|
-
.or_else(|| self.openai_api_key.clone())
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/// Get GLM API key from config or environment
|
|
192
|
-
pub fn get_glm_key(&self) -> Option<String> {
|
|
193
|
-
std::env::var("GLM_API_KEY")
|
|
194
|
-
.ok()
|
|
195
|
-
.or_else(|| self.glm_api_key.clone())
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/// Get default provider based on available API keys
|
|
199
|
-
/// Priority: Anthropic > OpenAI > GLM
|
|
200
|
-
pub fn get_default_provider(&self) -> Option<crate::cli::Provider> {
|
|
201
|
-
if self.get_anthropic_key().is_some() {
|
|
202
|
-
Some(crate::cli::Provider::Anthropic)
|
|
203
|
-
} else if self.get_openai_key().is_some() {
|
|
204
|
-
Some(crate::cli::Provider::OpenAi)
|
|
205
|
-
} else if self.get_glm_key().is_some() {
|
|
206
|
-
Some(crate::cli::Provider::Glm)
|
|
207
|
-
} else {
|
|
208
|
-
None
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/// Apply config to environment variables
|
|
213
|
-
pub fn apply_to_env(&self) {
|
|
214
|
-
if let Some(key) = &self.anthropic_api_key {
|
|
215
|
-
if std::env::var("ANTHROPIC_API_KEY").is_err() {
|
|
216
|
-
unsafe { std::env::set_var("ANTHROPIC_API_KEY", key); }
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
if let Some(key) = &self.openai_api_key {
|
|
220
|
-
if std::env::var("OPENAI_API_KEY").is_err() {
|
|
221
|
-
unsafe { std::env::set_var("OPENAI_API_KEY", key); }
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
if let Some(key) = &self.glm_api_key {
|
|
225
|
-
if std::env::var("GLM_API_KEY").is_err() {
|
|
226
|
-
unsafe { std::env::set_var("GLM_API_KEY", key); }
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/// Remove all stored API keys from disk
|
|
232
|
-
pub fn clear_api_keys(&mut self) -> Result<bool> {
|
|
233
|
-
let mut removed = false;
|
|
234
|
-
|
|
235
|
-
if self.anthropic_api_key.take().is_some() {
|
|
236
|
-
removed = true;
|
|
237
|
-
}
|
|
238
|
-
if self.openai_api_key.take().is_some() {
|
|
239
|
-
removed = true;
|
|
240
|
-
}
|
|
241
|
-
if self.glm_api_key.take().is_some() {
|
|
242
|
-
removed = true;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
self.save()?;
|
|
246
|
-
|
|
247
|
-
Ok(removed)
|
|
248
|
-
}
|
|
249
|
-
}
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
use std::fs;
|
|
2
|
-
use std::path::{PathBuf};
|
|
3
|
-
use std::time::{SystemTime, UNIX_EPOCH};
|
|
4
|
-
|
|
5
|
-
use anyhow::{Context, Result};
|
|
6
|
-
use chrono::{DateTime, Utc};
|
|
7
|
-
use serde::{Deserialize, Serialize};
|
|
8
|
-
|
|
9
|
-
use crate::cli::Provider;
|
|
10
|
-
use crate::session::{Message, MessageRole, Session};
|
|
11
|
-
use crate::config::Config;
|
|
12
|
-
|
|
13
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
14
|
-
pub struct ConversationSnapshot {
|
|
15
|
-
pub id: String,
|
|
16
|
-
pub title: String,
|
|
17
|
-
pub created_at: DateTime<Utc>,
|
|
18
|
-
pub updated_at: DateTime<Utc>,
|
|
19
|
-
pub provider: String,
|
|
20
|
-
pub model: String,
|
|
21
|
-
pub working_directory: PathBuf,
|
|
22
|
-
pub message_count: usize,
|
|
23
|
-
pub messages: Vec<Message>,
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
#[derive(Debug, Clone)]
|
|
27
|
-
pub struct ConversationSummary {
|
|
28
|
-
pub id: String,
|
|
29
|
-
pub title: String,
|
|
30
|
-
pub updated_at: DateTime<Utc>,
|
|
31
|
-
pub provider: String,
|
|
32
|
-
pub model: String,
|
|
33
|
-
pub message_count: usize,
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
pub struct ConversationStore;
|
|
37
|
-
|
|
38
|
-
impl ConversationStore {
|
|
39
|
-
fn storage_dir() -> Result<PathBuf> {
|
|
40
|
-
let config_path = Config::config_path()?;
|
|
41
|
-
let dir = config_path
|
|
42
|
-
.parent()
|
|
43
|
-
.map(|p| p.join("sessions"))
|
|
44
|
-
.unwrap_or_else(|| PathBuf::from(".zarz/sessions"));
|
|
45
|
-
fs::create_dir_all(&dir)
|
|
46
|
-
.with_context(|| format!("Failed to create session storage at {}", dir.display()))?;
|
|
47
|
-
Ok(dir)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
fn generate_id() -> String {
|
|
51
|
-
let now = Utc::now();
|
|
52
|
-
let nanos = now.timestamp_subsec_nanos();
|
|
53
|
-
let since_epoch = SystemTime::now()
|
|
54
|
-
.duration_since(UNIX_EPOCH)
|
|
55
|
-
.map(|d| d.as_nanos())
|
|
56
|
-
.unwrap_or(0);
|
|
57
|
-
format!("{}-{:09}-{:x}", now.format("%Y%m%d-%H%M%S"), nanos, since_epoch)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
fn derive_title(messages: &[Message]) -> String {
|
|
61
|
-
const DEFAULT_TITLE: &str = "Untitled session";
|
|
62
|
-
let candidate = messages
|
|
63
|
-
.iter()
|
|
64
|
-
.find_map(|msg| match msg.role {
|
|
65
|
-
MessageRole::User => {
|
|
66
|
-
msg.content
|
|
67
|
-
.lines()
|
|
68
|
-
.find(|line| !line.trim().is_empty())
|
|
69
|
-
.map(|line| line.trim())
|
|
70
|
-
.map(str::to_string)
|
|
71
|
-
}
|
|
72
|
-
_ => None,
|
|
73
|
-
})
|
|
74
|
-
.unwrap_or_else(|| DEFAULT_TITLE.to_string());
|
|
75
|
-
|
|
76
|
-
let trimmed = candidate.trim();
|
|
77
|
-
let title = if trimmed.is_empty() {
|
|
78
|
-
DEFAULT_TITLE.to_string()
|
|
79
|
-
} else {
|
|
80
|
-
trimmed.to_string()
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
if title.len() > 80 {
|
|
84
|
-
format!("{}…", &title[..80])
|
|
85
|
-
} else {
|
|
86
|
-
title
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
pub fn save_session(session: &mut Session, provider: Provider, model: &str) -> Result<()> {
|
|
91
|
-
if session.conversation_history.is_empty() {
|
|
92
|
-
return Ok(());
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
let now = Utc::now();
|
|
96
|
-
let id = session
|
|
97
|
-
.storage_id
|
|
98
|
-
.clone()
|
|
99
|
-
.unwrap_or_else(Self::generate_id);
|
|
100
|
-
let created_at = session
|
|
101
|
-
.created_at
|
|
102
|
-
.unwrap_or_else(|| {
|
|
103
|
-
let ts = now;
|
|
104
|
-
session.created_at = Some(ts);
|
|
105
|
-
ts
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
let title = session
|
|
109
|
-
.title
|
|
110
|
-
.clone()
|
|
111
|
-
.filter(|t| !t.trim().is_empty())
|
|
112
|
-
.unwrap_or_else(|| Self::derive_title(&session.conversation_history));
|
|
113
|
-
|
|
114
|
-
session.storage_id = Some(id.clone());
|
|
115
|
-
session.title = Some(title.clone());
|
|
116
|
-
session.updated_at = Some(now);
|
|
117
|
-
|
|
118
|
-
let snapshot = ConversationSnapshot {
|
|
119
|
-
id: id.clone(),
|
|
120
|
-
title,
|
|
121
|
-
created_at,
|
|
122
|
-
updated_at: now,
|
|
123
|
-
provider: provider.as_str().to_string(),
|
|
124
|
-
model: model.to_string(),
|
|
125
|
-
working_directory: session.working_directory.clone(),
|
|
126
|
-
message_count: session.conversation_history.len(),
|
|
127
|
-
messages: session.conversation_history.clone(),
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
let dir = Self::storage_dir()?;
|
|
131
|
-
let path = dir.join(format!("{id}.json"));
|
|
132
|
-
let data = serde_json::to_string_pretty(&snapshot)
|
|
133
|
-
.context("Failed to serialize conversation snapshot")?;
|
|
134
|
-
fs::write(&path, data)
|
|
135
|
-
.with_context(|| format!("Failed to write conversation snapshot to {}", path.display()))?;
|
|
136
|
-
|
|
137
|
-
Ok(())
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
pub fn list_summaries() -> Result<Vec<ConversationSummary>> {
|
|
141
|
-
let dir = Self::storage_dir()?;
|
|
142
|
-
if !dir.exists() {
|
|
143
|
-
return Ok(Vec::new());
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
let mut summaries = Vec::new();
|
|
147
|
-
for entry in fs::read_dir(&dir).with_context(|| format!("Failed to read {}", dir.display()))? {
|
|
148
|
-
let entry = entry?;
|
|
149
|
-
if !entry.file_type()?.is_file() {
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
let content = fs::read_to_string(entry.path());
|
|
153
|
-
let Ok(content) = content else {
|
|
154
|
-
continue;
|
|
155
|
-
};
|
|
156
|
-
let snapshot: Result<ConversationSnapshot, _> = serde_json::from_str(&content);
|
|
157
|
-
let Ok(snapshot) = snapshot else {
|
|
158
|
-
continue;
|
|
159
|
-
};
|
|
160
|
-
summaries.push(ConversationSummary {
|
|
161
|
-
id: snapshot.id,
|
|
162
|
-
title: snapshot.title,
|
|
163
|
-
updated_at: snapshot.updated_at,
|
|
164
|
-
provider: snapshot.provider,
|
|
165
|
-
model: snapshot.model,
|
|
166
|
-
message_count: snapshot.message_count,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
summaries.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
|
|
171
|
-
Ok(summaries)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
pub fn load_snapshot(id: &str) -> Result<ConversationSnapshot> {
|
|
175
|
-
let dir = Self::storage_dir()?;
|
|
176
|
-
let path = dir.join(format!("{id}.json"));
|
|
177
|
-
let data = fs::read_to_string(&path)
|
|
178
|
-
.with_context(|| format!("Failed to read session file {}", path.display()))?;
|
|
179
|
-
let snapshot: ConversationSnapshot =
|
|
180
|
-
serde_json::from_str(&data).context("Failed to parse stored session data")?;
|
|
181
|
-
Ok(snapshot)
|
|
182
|
-
}
|
|
183
|
-
}
|
package/src/executor.rs
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
use anyhow::{Context, Result};
|
|
2
|
-
use std::process::Stdio;
|
|
3
|
-
use tokio::io::{AsyncBufReadExt, BufReader};
|
|
4
|
-
use tokio::process::Command;
|
|
5
|
-
|
|
6
|
-
pub struct CommandExecutor;
|
|
7
|
-
|
|
8
|
-
#[derive(Debug)]
|
|
9
|
-
pub struct CommandResult {
|
|
10
|
-
pub stdout: String,
|
|
11
|
-
pub stderr: String,
|
|
12
|
-
pub exit_code: i32,
|
|
13
|
-
pub success: bool,
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
impl CommandExecutor {
|
|
17
|
-
pub async fn execute(command: &str) -> Result<CommandResult> {
|
|
18
|
-
let (shell, flag) = if cfg!(target_os = "windows") {
|
|
19
|
-
("cmd", "/C")
|
|
20
|
-
} else {
|
|
21
|
-
("sh", "-c")
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
let mut child = Command::new(shell)
|
|
25
|
-
.arg(flag)
|
|
26
|
-
.arg(command)
|
|
27
|
-
.stdout(Stdio::piped())
|
|
28
|
-
.stderr(Stdio::piped())
|
|
29
|
-
.spawn()
|
|
30
|
-
.with_context(|| format!("Failed to execute command: {}", command))?;
|
|
31
|
-
|
|
32
|
-
let stdout = child
|
|
33
|
-
.stdout
|
|
34
|
-
.take()
|
|
35
|
-
.context("Failed to capture stdout")?;
|
|
36
|
-
|
|
37
|
-
let stderr = child
|
|
38
|
-
.stderr
|
|
39
|
-
.take()
|
|
40
|
-
.context("Failed to capture stderr")?;
|
|
41
|
-
|
|
42
|
-
let mut stdout_lines = BufReader::new(stdout).lines();
|
|
43
|
-
let mut stderr_lines = BufReader::new(stderr).lines();
|
|
44
|
-
|
|
45
|
-
let stdout_handle = tokio::spawn(async move {
|
|
46
|
-
let mut output = String::new();
|
|
47
|
-
while let Ok(Some(line)) = stdout_lines.next_line().await {
|
|
48
|
-
output.push_str(&line);
|
|
49
|
-
output.push('\n');
|
|
50
|
-
}
|
|
51
|
-
output
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
let stderr_handle = tokio::spawn(async move {
|
|
55
|
-
let mut output = String::new();
|
|
56
|
-
while let Ok(Some(line)) = stderr_lines.next_line().await {
|
|
57
|
-
output.push_str(&line);
|
|
58
|
-
output.push('\n');
|
|
59
|
-
}
|
|
60
|
-
output
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
let stdout_output = stdout_handle
|
|
64
|
-
.await
|
|
65
|
-
.context("Failed to join stdout task")?;
|
|
66
|
-
|
|
67
|
-
let stderr_output = stderr_handle
|
|
68
|
-
.await
|
|
69
|
-
.context("Failed to join stderr task")?;
|
|
70
|
-
|
|
71
|
-
let status = child
|
|
72
|
-
.wait()
|
|
73
|
-
.await
|
|
74
|
-
.context("Failed to wait for command")?;
|
|
75
|
-
|
|
76
|
-
let exit_code = status.code().unwrap_or(-1);
|
|
77
|
-
let success = status.success();
|
|
78
|
-
|
|
79
|
-
Ok(CommandResult {
|
|
80
|
-
stdout: stdout_output,
|
|
81
|
-
stderr: stderr_output,
|
|
82
|
-
exit_code,
|
|
83
|
-
success,
|
|
84
|
-
})
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
#[allow(dead_code)]
|
|
88
|
-
pub async fn execute_streaming<F>(command: &str, mut on_output: F) -> Result<CommandResult>
|
|
89
|
-
where
|
|
90
|
-
F: FnMut(&str) + Send,
|
|
91
|
-
{
|
|
92
|
-
let (shell, flag) = if cfg!(target_os = "windows") {
|
|
93
|
-
("cmd", "/C")
|
|
94
|
-
} else {
|
|
95
|
-
("sh", "-c")
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
let mut child = Command::new(shell)
|
|
99
|
-
.arg(flag)
|
|
100
|
-
.arg(command)
|
|
101
|
-
.stdout(Stdio::piped())
|
|
102
|
-
.stderr(Stdio::piped())
|
|
103
|
-
.spawn()
|
|
104
|
-
.with_context(|| format!("Failed to execute command: {}", command))?;
|
|
105
|
-
|
|
106
|
-
let stdout = child
|
|
107
|
-
.stdout
|
|
108
|
-
.take()
|
|
109
|
-
.context("Failed to capture stdout")?;
|
|
110
|
-
|
|
111
|
-
let stderr = child
|
|
112
|
-
.stderr
|
|
113
|
-
.take()
|
|
114
|
-
.context("Failed to capture stderr")?;
|
|
115
|
-
|
|
116
|
-
let mut stdout_lines = BufReader::new(stdout).lines();
|
|
117
|
-
let mut stderr_lines = BufReader::new(stderr).lines();
|
|
118
|
-
|
|
119
|
-
let mut stdout_output = String::new();
|
|
120
|
-
let mut stderr_output = String::new();
|
|
121
|
-
|
|
122
|
-
loop {
|
|
123
|
-
tokio::select! {
|
|
124
|
-
result = stdout_lines.next_line() => {
|
|
125
|
-
match result {
|
|
126
|
-
Ok(Some(line)) => {
|
|
127
|
-
on_output(&line);
|
|
128
|
-
stdout_output.push_str(&line);
|
|
129
|
-
stdout_output.push('\n');
|
|
130
|
-
}
|
|
131
|
-
Ok(None) => break,
|
|
132
|
-
Err(_) => break,
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
result = stderr_lines.next_line() => {
|
|
136
|
-
match result {
|
|
137
|
-
Ok(Some(line)) => {
|
|
138
|
-
on_output(&line);
|
|
139
|
-
stderr_output.push_str(&line);
|
|
140
|
-
stderr_output.push('\n');
|
|
141
|
-
}
|
|
142
|
-
Ok(None) => {},
|
|
143
|
-
Err(_) => {},
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
let status = child
|
|
150
|
-
.wait()
|
|
151
|
-
.await
|
|
152
|
-
.context("Failed to wait for command")?;
|
|
153
|
-
|
|
154
|
-
let exit_code = status.code().unwrap_or(-1);
|
|
155
|
-
let success = status.success();
|
|
156
|
-
|
|
157
|
-
Ok(CommandResult {
|
|
158
|
-
stdout: stdout_output,
|
|
159
|
-
stderr: stderr_output,
|
|
160
|
-
exit_code,
|
|
161
|
-
success,
|
|
162
|
-
})
|
|
163
|
-
}
|
|
164
|
-
}
|
package/src/fs_ops.rs
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
use anyhow::{Context, Result};
|
|
2
|
-
use std::path::{Path, PathBuf};
|
|
3
|
-
use tokio::fs;
|
|
4
|
-
use walkdir::WalkDir;
|
|
5
|
-
|
|
6
|
-
pub struct FileSystemOps;
|
|
7
|
-
|
|
8
|
-
impl FileSystemOps {
|
|
9
|
-
pub async fn create_file(path: &Path, content: &str) -> Result<()> {
|
|
10
|
-
if let Some(parent) = path.parent() {
|
|
11
|
-
fs::create_dir_all(parent)
|
|
12
|
-
.await
|
|
13
|
-
.with_context(|| format!("Failed to create parent directories for {}", path.display()))?;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
fs::write(path, content)
|
|
17
|
-
.await
|
|
18
|
-
.with_context(|| format!("Failed to write file {}", path.display()))?;
|
|
19
|
-
|
|
20
|
-
Ok(())
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
#[allow(dead_code)]
|
|
24
|
-
pub async fn delete_file(path: &Path) -> Result<()> {
|
|
25
|
-
fs::remove_file(path)
|
|
26
|
-
.await
|
|
27
|
-
.with_context(|| format!("Failed to delete file {}", path.display()))?;
|
|
28
|
-
|
|
29
|
-
Ok(())
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
#[allow(dead_code)]
|
|
33
|
-
pub async fn rename_file(from: &Path, to: &Path) -> Result<()> {
|
|
34
|
-
if let Some(parent) = to.parent() {
|
|
35
|
-
fs::create_dir_all(parent)
|
|
36
|
-
.await
|
|
37
|
-
.with_context(|| format!("Failed to create parent directories for {}", to.display()))?;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
fs::rename(from, to)
|
|
41
|
-
.await
|
|
42
|
-
.with_context(|| format!("Failed to rename {} to {}", from.display(), to.display()))?;
|
|
43
|
-
|
|
44
|
-
Ok(())
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
#[allow(dead_code)]
|
|
48
|
-
pub async fn create_directory(path: &Path) -> Result<()> {
|
|
49
|
-
fs::create_dir_all(path)
|
|
50
|
-
.await
|
|
51
|
-
.with_context(|| format!("Failed to create directory {}", path.display()))?;
|
|
52
|
-
|
|
53
|
-
Ok(())
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
pub async fn read_file(path: &Path) -> Result<String> {
|
|
57
|
-
fs::read_to_string(path)
|
|
58
|
-
.await
|
|
59
|
-
.with_context(|| format!("Failed to read file {}", path.display()))
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
pub async fn file_exists(path: &Path) -> bool {
|
|
63
|
-
fs::metadata(path).await.is_ok()
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
#[allow(dead_code)]
|
|
67
|
-
pub fn list_files(root: &Path, pattern: Option<&str>) -> Result<Vec<PathBuf>> {
|
|
68
|
-
let mut files = Vec::new();
|
|
69
|
-
|
|
70
|
-
for entry in WalkDir::new(root)
|
|
71
|
-
.follow_links(false)
|
|
72
|
-
.into_iter()
|
|
73
|
-
.filter_map(|e| e.ok())
|
|
74
|
-
{
|
|
75
|
-
if entry.file_type().is_file() {
|
|
76
|
-
let path = entry.path();
|
|
77
|
-
|
|
78
|
-
if let Some(pattern) = pattern {
|
|
79
|
-
if let Some(file_name) = path.file_name() {
|
|
80
|
-
if file_name.to_string_lossy().contains(pattern) {
|
|
81
|
-
files.push(path.to_path_buf());
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
} else {
|
|
85
|
-
files.push(path.to_path_buf());
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
Ok(files)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
#[allow(dead_code)]
|
|
94
|
-
pub fn get_directory_structure(root: &Path, max_depth: Option<usize>) -> Result<String> {
|
|
95
|
-
let mut output = String::new();
|
|
96
|
-
let max_depth = max_depth.unwrap_or(3);
|
|
97
|
-
|
|
98
|
-
for entry in WalkDir::new(root)
|
|
99
|
-
.max_depth(max_depth)
|
|
100
|
-
.follow_links(false)
|
|
101
|
-
.into_iter()
|
|
102
|
-
.filter_map(|e| e.ok())
|
|
103
|
-
{
|
|
104
|
-
let depth = entry.depth();
|
|
105
|
-
let indent = " ".repeat(depth);
|
|
106
|
-
let name = entry.file_name().to_string_lossy();
|
|
107
|
-
|
|
108
|
-
if entry.file_type().is_dir() {
|
|
109
|
-
output.push_str(&format!("{}{}/\n", indent, name));
|
|
110
|
-
} else {
|
|
111
|
-
output.push_str(&format!("{}{}\n", indent, name));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
Ok(output)
|
|
116
|
-
}
|
|
117
|
-
}
|