1use crate::cards::{Card, CardConfig, CardCommand};
2use crate::utils;
3use anyhow::{Result, Context, anyhow};
4use colored::Colorize;
5use std::path::PathBuf;
6use std::fs;
7use std::io::{Read, Write};
8use std::process::Command;
9use dirs;
10
11pub struct BlendCard {
13 name: String,
15
16 version: String,
18
19 description: String,
21
22 config: BlendCardConfig,
24
25 data_dir: PathBuf,
27}
28
29#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
31pub struct BlendCardConfig {
32 pub hook_dir: String,
34
35 pub bin_dir: String,
37}
38
39impl Default for BlendCardConfig {
40 fn default() -> Self {
41 Self {
42 hook_dir: "~/.pocket/hooks".to_string(),
43 bin_dir: "~/.pocket/bin".to_string(),
44 }
45 }
46}
47
48impl BlendCard {
49 pub fn new(data_dir: impl AsRef<std::path::Path>) -> Self {
51 Self {
52 name: "blend".to_string(),
53 version: env!("CARGO_PKG_VERSION").to_string(),
54 description: "Shell integration and executable hooks".to_string(),
55 config: BlendCardConfig::default(),
56 data_dir: data_dir.as_ref().to_path_buf(),
57 }
58 }
59
60 pub fn add_hook(&self, script_path: &str, executable: bool) -> Result<()> {
62 let hook_dir = utils::expand_path(&self.config.hook_dir)?;
64
65 if !hook_dir.exists() {
67 fs::create_dir_all(&hook_dir)
68 .with_context(|| format!("Failed to create hook directory at {}", hook_dir.display()))?;
69 }
70
71 let script_content = fs::read_to_string(script_path)
73 .with_context(|| format!("Failed to read script at {}", script_path))?;
74
75 let script_path = std::path::Path::new(script_path);
77 let hook_name = script_path.file_stem()
78 .and_then(|stem| stem.to_str())
79 .ok_or_else(|| anyhow!("Invalid script filename"))?;
80
81 let hook_script_path = hook_dir.join(format!("{}.sh", hook_name));
83
84 fs::write(&hook_script_path, script_content)
86 .with_context(|| format!("Failed to write hook script to {}", hook_script_path.display()))?;
87
88 if executable {
89 #[cfg(unix)]
91 {
92 use std::os::unix::fs::PermissionsExt;
93 let mut perms = fs::metadata(&hook_script_path)?.permissions();
94 perms.set_mode(0o755);
95 fs::set_permissions(&hook_script_path, perms)?;
96 }
97
98 let bin_dir = utils::expand_path(&self.config.bin_dir)?;
100 if !bin_dir.exists() {
101 fs::create_dir_all(&bin_dir)
102 .with_context(|| format!("Failed to create bin directory at {}", bin_dir.display()))?;
103
104 self.add_bin_to_path(&bin_dir)?;
106 }
107
108 let wrapper_path = bin_dir.join(format!("@{}", hook_name));
110 let wrapper_content = format!(
111 "#!/bin/bash\n\
112 # Wrapper for Pocket hook: {}\n\
113 exec \"{}\" \"$@\"\n",
114 hook_name,
115 hook_script_path.display()
116 );
117
118 fs::write(&wrapper_path, wrapper_content)
119 .with_context(|| format!("Failed to write wrapper script to {}", wrapper_path.display()))?;
120
121 #[cfg(unix)]
123 {
124 use std::os::unix::fs::PermissionsExt;
125 let mut perms = fs::metadata(&wrapper_path)?.permissions();
126 perms.set_mode(0o755);
127 fs::set_permissions(&wrapper_path, perms)?;
128 }
129
130 println!("Successfully added executable hook '{}' from {}", hook_name, script_path.display());
131 println!("You can run it with '@{}' or 'pocket blend run {}'", hook_name, hook_name);
132 } else {
133 self.add_hook_to_shell_config(hook_name, &hook_script_path)?;
135 println!("Successfully added hook '{}' from {}", hook_name, script_path.display());
136 println!("Restart your shell or run 'source {}' to apply changes", self.get_shell_config_path()?.display());
137 }
138
139 Ok(())
140 }
141
142 pub fn list_hooks(&self) -> Result<()> {
144 let hook_dir = utils::expand_path(&self.config.hook_dir)?;
146
147 if !hook_dir.exists() {
148 println!("No hooks installed yet");
149 return Ok(());
150 }
151
152 let mut hooks = Vec::new();
153
154 for entry in fs::read_dir(hook_dir)? {
156 let entry = entry?;
157 let path = entry.path();
158
159 if path.is_file() && path.extension().and_then(|e| e.to_str()) == Some("sh") {
160 let name = path.file_stem()
161 .and_then(|stem| stem.to_str())
162 .unwrap_or("unknown")
163 .to_string();
164
165 let bin_dir = utils::expand_path(&self.config.bin_dir)?;
167 let wrapper_path = bin_dir.join(format!("@{}", name));
168 let is_executable = wrapper_path.exists();
169
170 hooks.push((name, path, is_executable));
171 }
172 }
173
174 if hooks.is_empty() {
175 println!("No hooks installed yet");
176 return Ok(());
177 }
178
179 println!("Installed hooks:");
180 for (name, path, is_executable) in hooks {
181 let hook_type = if is_executable {
182 "[executable]"
183 } else {
184 "[shell extension]"
185 };
186
187 println!(" @{} ({}) {}", name, path.display(), hook_type);
188 }
189
190 Ok(())
191 }
192
193 pub fn edit_hook(&self, hook_name: &str) -> Result<()> {
195 let hook_name = hook_name.trim_start_matches('@');
197
198 let hook_dir = utils::expand_path(&self.config.hook_dir)?;
200 let hook_path = hook_dir.join(format!("{}.sh", hook_name));
201
202 if !hook_path.exists() {
203 return Err(anyhow!("Hook '{}' not found", hook_name));
204 }
205
206 let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
208
209 let status = Command::new(&editor)
211 .arg(&hook_path)
212 .status()
213 .with_context(|| format!("Failed to open editor {}", editor))?;
214
215 if !status.success() {
216 return Err(anyhow!("Editor exited with non-zero status"));
217 }
218
219 println!("Hook '{}' edited successfully", hook_name);
220 Ok(())
221 }
222
223 pub fn run_hook(&self, hook_name: &str, args: &[String]) -> Result<()> {
225 let hook_name = hook_name.trim_start_matches('@');
227
228 let hook_dir = utils::expand_path(&self.config.hook_dir)?;
230 let hook_path = hook_dir.join(format!("{}.sh", hook_name));
231
232 if !hook_path.exists() {
233 return Err(anyhow!("Hook '{}' not found", hook_name));
234 }
235
236 println!("Running hook '{}'...", hook_name);
237
238 #[cfg(unix)]
240 {
241 use std::os::unix::fs::PermissionsExt;
242 let mut perms = fs::metadata(&hook_path)?.permissions();
243 perms.set_mode(0o755);
244 fs::set_permissions(&hook_path, perms)?;
245 }
246
247 let mut command = Command::new(&hook_path);
249 if !args.is_empty() {
250 command.args(args);
251 }
252
253 let status = command
254 .status()
255 .with_context(|| format!("Failed to execute hook '{}'", hook_name))?;
256
257 if !status.success() {
258 return Err(anyhow!("Hook '{}' exited with non-zero status", hook_name));
259 }
260
261 Ok(())
262 }
263
264 fn get_shell_config_path(&self) -> Result<PathBuf> {
266 let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string());
268 let home = utils::expand_path("~")?;
269
270 let config_path = if shell.contains("zsh") {
272 home.join(".zshrc")
273 } else if shell.contains("bash") {
274 let bash_profile = home.join(".bash_profile");
276 if bash_profile.exists() {
277 bash_profile
278 } else {
279 home.join(".bashrc")
280 }
281 } else {
282 home.join(".profile")
284 };
285
286 Ok(config_path)
287 }
288
289 fn add_hook_to_shell_config(&self, hook_name: &str, hook_path: &PathBuf) -> Result<()> {
291 let config_path = self.get_shell_config_path()?;
292
293 let mut config_content = String::new();
295 if config_path.exists() {
296 let mut file = fs::File::open(&config_path)?;
297 file.read_to_string(&mut config_content)?;
298 }
299
300 let source_line = format!("source \"{}\"", hook_path.display());
302 if config_content.contains(&source_line) {
303 println!("Hook '{}' is already sourced in {}", hook_name, config_path.display());
304 return Ok(());
305 }
306
307 let mut file = fs::OpenOptions::new()
309 .write(true)
310 .append(true)
311 .create(true)
312 .open(&config_path)?;
313
314 writeln!(file, "\n# Pocket CLI hook: {}", hook_name)?;
315 writeln!(file, "{}", source_line)?;
316
317 println!("Added hook '{}' to {}", hook_name, config_path.display());
318 Ok(())
319 }
320
321 fn add_bin_to_path(&self, bin_dir: &PathBuf) -> Result<()> {
323 let config_path = self.get_shell_config_path()?;
324
325 let mut config_content = String::new();
327 if config_path.exists() {
328 let mut file = fs::File::open(&config_path)?;
329 file.read_to_string(&mut config_content)?;
330 }
331
332 let path_line = format!("export PATH=\"{}:$PATH\"", bin_dir.display());
334 if config_content.contains(&path_line) {
335 return Ok(());
336 }
337
338 let mut file = fs::OpenOptions::new()
340 .write(true)
341 .append(true)
342 .create(true)
343 .open(&config_path)?;
344
345 writeln!(file, "\n# Pocket hook bin directory")?;
346 writeln!(file, "{}", path_line)?;
347
348 println!("Added Pocket hook bin directory to your PATH");
349 Ok(())
350 }
351}
352
353impl Card for BlendCard {
354 fn name(&self) -> &str {
355 &self.name
356 }
357
358 fn version(&self) -> &str {
359 &self.version
360 }
361
362 fn description(&self) -> &str {
363 &self.description
364 }
365
366 fn initialize(&mut self, config: &CardConfig) -> Result<()> {
367 if let Some(options_value) = config.options.get("blend") {
369 if let Ok(options) = serde_json::from_value::<BlendCardConfig>(options_value.clone()) {
370 self.config = options;
371 }
372 }
373
374 Ok(())
375 }
376
377 fn execute(&self, command: &str, args: &[String]) -> Result<()> {
378 match command {
379 "add" => {
380 if args.is_empty() {
381 return Err(anyhow!("Missing script path"));
382 }
383
384 let script_path = &args[0];
385
386 let mut executable = false;
387
388 let mut i = 1;
390 while i < args.len() {
391 match args[i].as_str() {
392 "--executable" | "-e" => {
393 executable = true;
394 }
395 _ => { }
396 }
397 i += 1;
398 }
399
400 self.add_hook(script_path, executable)?;
401 }
402 "list" => {
403 self.list_hooks()?;
404 }
405 "edit" => {
406 if args.is_empty() {
407 return Err(anyhow!("Missing hook name"));
408 }
409
410 let hook_name = &args[0];
411 self.edit_hook(hook_name)?;
412 }
413 "run" => {
414 if args.is_empty() {
415 return Err(anyhow!("Missing hook name"));
416 }
417
418 let hook_name = &args[0];
419 let hook_args = if args.len() > 1 {
420 &args[1..]
421 } else {
422 &[]
423 };
424
425 self.run_hook(hook_name, hook_args)?;
426 }
427 _ => {
428 return Err(anyhow!("Unknown command: {}", command));
429 }
430 }
431
432 Ok(())
433 }
434
435 fn commands(&self) -> Vec<CardCommand> {
436 vec![
437 CardCommand {
438 name: "add".to_string(),
439 description: "Add a shell script as a hook".to_string(),
440 usage: "add <script_path> [--executable]".to_string(),
441 },
442 CardCommand {
443 name: "list".to_string(),
444 description: "List all installed hooks".to_string(),
445 usage: "list".to_string(),
446 },
447 CardCommand {
448 name: "edit".to_string(),
449 description: "Edit an existing hook".to_string(),
450 usage: "edit <hook_name>".to_string(),
451 },
452 CardCommand {
453 name: "run".to_string(),
454 description: "Run a hook command directly".to_string(),
455 usage: "run <hook_name> [args...]".to_string(),
456 },
457 ]
458 }
459
460 fn cleanup(&mut self) -> Result<()> {
461 Ok(())
462 }
463}