Skip to content

Commit 02c168f

Browse files
authored
feat: implement AccountStore persistence and CLI CRUD wiring (#7) (#11)
Merged via automated workflow
1 parent 15253b5 commit 02c168f

File tree

6 files changed

+960
-9
lines changed

6 files changed

+960
-9
lines changed

Cargo.lock

Lines changed: 5 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sigilforge-cli/src/main.rs

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,20 +124,66 @@ async fn main() -> Result<()> {
124124
}
125125

126126
async fn add_account(service: &str, account: &str, scopes: Option<&str>) -> Result<()> {
127-
println!("Adding account {}/{}", service, account);
127+
use sigilforge_core::{Account, AccountId, AccountStore, ServiceId};
128+
129+
let store = AccountStore::load()?;
130+
131+
let service_id = ServiceId::new(service);
132+
let account_id = AccountId::new(account);
133+
134+
let scope_list = if let Some(scopes) = scopes {
135+
scopes.split(',').map(|s| s.trim().to_string()).collect()
136+
} else {
137+
Vec::new()
138+
};
139+
140+
let new_account = Account::new(service_id.clone(), account_id.clone(), scope_list);
141+
142+
store.add_account(new_account)?;
143+
144+
println!("Account {}/{} added successfully", service, account);
128145
if let Some(scopes) = scopes {
129146
println!(" Scopes: {}", scopes);
130147
}
131-
println!(" [stub] Would start OAuth flow here");
148+
println!(" Storage path: {:?}", store.path());
149+
println!(" [stub] Would start OAuth flow to obtain tokens here");
150+
132151
Ok(())
133152
}
134153

135154
async fn list_accounts(service_filter: Option<&str>) -> Result<()> {
155+
use sigilforge_core::{AccountStore, ServiceId};
156+
157+
let store = AccountStore::load()?;
158+
159+
let filter = service_filter.map(ServiceId::new);
160+
let accounts = store.list_accounts(filter.as_ref())?;
161+
162+
if accounts.is_empty() {
163+
println!("No accounts configured");
164+
if let Some(service) = service_filter {
165+
println!(" (filtered by service: {})", service);
166+
}
167+
return Ok(());
168+
}
169+
136170
println!("Configured accounts:");
137171
if let Some(service) = service_filter {
138172
println!(" (filtered by service: {})", service);
139173
}
140-
println!(" [stub] No accounts configured yet");
174+
println!();
175+
176+
for account in accounts {
177+
println!(" {}/{}", account.service, account.id);
178+
if !account.scopes.is_empty() {
179+
println!(" Scopes: {}", account.scopes.join(", "));
180+
}
181+
println!(" Created: {}", account.created_at);
182+
if let Some(last_used) = account.last_used {
183+
println!(" Last used: {}", last_used);
184+
}
185+
}
186+
141187
Ok(())
142188
}
143189

@@ -156,11 +202,67 @@ async fn get_token(service: &str, account: &str, format: &str) -> Result<()> {
156202
}
157203

158204
async fn remove_account(service: &str, account: &str, force: bool) -> Result<()> {
205+
use sigilforge_core::{AccountStore, CredentialType, MemoryStore, SecretStore, ServiceId, AccountId};
206+
use std::io::{self, Write};
207+
208+
let store = AccountStore::load()?;
209+
let service_id = ServiceId::new(service);
210+
let account_id = AccountId::new(account);
211+
212+
// Verify account exists before prompting
213+
let account_entry = store.get_account(&service_id, &account_id)?;
214+
if account_entry.is_none() {
215+
eprintln!("Error: Account {}/{} not found", service, account);
216+
std::process::exit(1);
217+
}
218+
219+
// Prompt for confirmation unless --force is used
159220
if !force {
160-
println!("Remove account {}/{}? [y/N]", service, account);
161-
println!("[stub] Would prompt for confirmation");
221+
print!("Remove account {}/{}? [y/N] ", service, account);
222+
io::stdout().flush()?;
223+
224+
let mut response = String::new();
225+
io::stdin().read_line(&mut response)?;
226+
227+
let confirmed = response.trim().eq_ignore_ascii_case("y")
228+
|| response.trim().eq_ignore_ascii_case("yes");
229+
230+
if !confirmed {
231+
println!("Cancelled");
232+
return Ok(());
233+
}
234+
}
235+
236+
// Remove account from store
237+
store.remove_account(&service_id, &account_id)?;
238+
239+
// Delete associated secrets from secret store
240+
// For now, we use MemoryStore as a placeholder since we don't have
241+
// a global secret store instance yet. In a production implementation,
242+
// this would use the actual secret store backend.
243+
//
244+
// The secret keys follow the pattern: sigilforge/{service}/{account}/{type}
245+
let secret_store = MemoryStore::new();
246+
247+
// Common credential types to clean up
248+
let credential_types = [
249+
CredentialType::AccessToken,
250+
CredentialType::RefreshToken,
251+
CredentialType::TokenExpiry,
252+
CredentialType::ApiKey,
253+
CredentialType::ClientId,
254+
CredentialType::ClientSecret,
255+
];
256+
257+
for cred_type in &credential_types {
258+
let key = format!("sigilforge/{}/{}/{}", service, account, cred_type);
259+
// Ignore errors - the key might not exist
260+
let _ = secret_store.delete(&key).await;
162261
}
163-
println!("[stub] Account removed");
262+
263+
println!("Account {}/{} removed successfully", service, account);
264+
println!(" [Note: Associated secrets have been deleted from the secret store]");
265+
164266
Ok(())
165267
}
166268

sigilforge-core/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ uuid = { workspace = true }
3232
# URL parsing (for auth:// URIs)
3333
url = { workspace = true }
3434

35+
# Platform-specific directories
36+
directories = { workspace = true }
37+
3538
# Secret storage backends (optional features)
3639
keyring = { workspace = true, optional = true }
3740

@@ -47,3 +50,4 @@ full = ["keyring-store", "oauth"]
4750

4851
[dev-dependencies]
4952
tokio = { workspace = true, features = ["test-util", "macros"] }
53+
tempfile = "3.13"

0 commit comments

Comments
 (0)