1use anyhow::Result;
2use async_trait::async_trait;
3use dirs;
4use sha2::{Sha256, Digest};
5use serde::{Serialize, Deserialize};
6use std::path::PathBuf;
7use libsql::Connection;
8use libsql::Builder;
9use std::fs;
10
11#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
13pub struct Prompt {
14 pub content: String,
16 pub tags: Option<Vec<String>>,
18 pub categories: Option<Vec<String>>,
20 pub hash: String,
22}
23
24impl Prompt {
25 pub fn new(content: &str, tags: Option<Vec<String>>, categories: Option<Vec<String>>) -> Self {
29 let hash = Sha256::digest(content.as_bytes());
30 Self {
31 content: content.to_string(),
32 tags,
33 categories,
34 hash: format!("{:x}", hash),
35 }
36 }
37}
38
39#[async_trait]
41pub trait Storage {
42 async fn save_prompt(&self, prompt: &mut Prompt) -> Result<()>;
44 async fn load_prompts(&self) -> Result<Vec<Prompt>>;
46 async fn delete_prompt(&self, hash: &str) -> Result<()>;
48}
49
50pub struct JsonStorage {
54 storage_path: PathBuf,
55}
56
57fn get_default_storage_dir() -> Result<PathBuf> {
61 let mut path = dirs::config_dir().ok_or_else(|| anyhow::anyhow!("Could not find config directory"))?;
62 path.push("prompts-cli");
63 Ok(path)
64}
65
66impl JsonStorage {
67 pub fn new(storage_path: Option<PathBuf>) -> Result<Self> {
71 let path = match storage_path {
72 Some(path) => path,
73 None => {
74 let mut default_path = get_default_storage_dir()?;
75 default_path.push("prompts");
76 fs::create_dir_all(&default_path)?;
77 default_path
78 }
79 };
80 Ok(Self { storage_path: path })
81 }
82}
83
84#[async_trait]
85impl Storage for JsonStorage {
86 async fn save_prompt(&self, prompt: &mut Prompt) -> Result<()> {
87 let file_path = self.storage_path.join(format!("{}.json", prompt.hash));
88 let json = serde_json::to_string_pretty(prompt)?;
89 tokio::fs::write(file_path, json).await?;
90 Ok(())
91 }
92
93 async fn load_prompts(&self) -> Result<Vec<Prompt>> {
94 let mut prompts = Vec::new();
95 let mut read_dir = tokio::fs::read_dir(&self.storage_path).await?;
96 while let Some(entry) = read_dir.next_entry().await? {
97 let path = entry.path();
98 if path.is_file() && path.extension().map_or(false, |ext| ext == "json") {
99 let json = tokio::fs::read_to_string(&path).await?;
100 let prompt: Prompt = serde_json::from_str(&json)?;
101 prompts.push(prompt);
102 }
103 }
104 Ok(prompts)
105 }
106
107 async fn delete_prompt(&self, hash: &str) -> Result<()> {
108 let file_path = self.storage_path.join(format!("{}.json", hash));
109 if file_path.exists() {
110 tokio::fs::remove_file(file_path).await?;
111 }
112 Ok(())
113 }
114}
115
116pub struct LibSQLStorage {
120 conn: Connection,
121}
122
123impl LibSQLStorage {
124 pub async fn new(storage_path: Option<PathBuf>) -> Result<Self> {
129 let db_path = match storage_path {
130 Some(path) => path,
131 None => {
132 let mut path = get_default_storage_dir()?;
133 fs::create_dir_all(&path)?;
134 path.push("prompts.db");
135 path
136 }
137 };
138
139 let db = Builder::new_local(db_path.to_str().unwrap()).build().await?;
140 let conn = db.connect()?;
141
142 conn.execute(
143 "CREATE TABLE IF NOT EXISTS prompts (
144 hash TEXT PRIMARY KEY,
145 content TEXT NOT NULL,
146 tags TEXT,
147 categories TEXT
148 )",
149 (),
150 ).await?;
151
152 Ok(Self { conn })
153 }
154}
155
156#[async_trait]
157impl Storage for LibSQLStorage {
158 async fn save_prompt(&self, prompt: &mut Prompt) -> Result<()> {
159 let tags = serde_json::to_string(&prompt.tags.as_deref().unwrap_or_default())?;
160 let categories = serde_json::to_string(&prompt.categories.as_deref().unwrap_or_default())?;
161
162 self.conn.execute(
163 "INSERT INTO prompts (hash, content, tags, categories) VALUES (?1, ?2, ?3, ?4)",
164 libsql::params![prompt.hash.clone(), prompt.content.clone(), tags, categories],
165 ).await?;
166
167 Ok(())
168 }
169
170 async fn load_prompts(&self) -> Result<Vec<Prompt>> {
171 let mut rows = self.conn.query("SELECT hash, content, tags, categories FROM prompts", ()).await?;
172 let mut prompts = Vec::new();
173
174 while let Some(row) = rows.next().await? {
175 let hash: String = row.get(0)?;
176 let content: String = row.get(1)?;
177 let tags_str: String = row.get(2)?;
178 let categories_str: String = row.get(3)?;
179
180 let tags: Option<Vec<String>> = serde_json::from_str(&tags_str)?;
181 let categories: Option<Vec<String>> = serde_json::from_str(&categories_str)?;
182
183 prompts.push(Prompt {
184 hash,
185 content,
186 tags,
187 categories,
188 });
189 }
190
191 Ok(prompts)
192 }
193
194 async fn delete_prompt(&self, hash: &str) -> Result<()> {
195 self.conn.execute(
196 "DELETE FROM prompts WHERE hash = ?1",
197 libsql::params![hash],
198 ).await?;
199 Ok(())
200 }
201}