use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::auth::{get_api_base, AuthConfig};
use crate::error::{map_http_error, map_network_error, Error, Result};
use crate::spike::Spike;
pub struct UnshareOptions {
pub slug: String,
pub force: bool,
pub json: bool,
}
#[derive(Debug, Deserialize)]
struct ShareInfo {
id: String,
#[allow(dead_code)]
slug: String,
#[serde(default)]
exported_spikes: Vec<Spike>,
}
#[derive(Debug, Serialize)]
struct UnshareResult {
success: bool,
slug: String,
spikes_saved: usize,
backup_path: Option<String>,
}
pub fn run(options: UnshareOptions) -> Result<()> {
let token = AuthConfig::token()?
.ok_or_else(|| {
Error::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Not logged in. Run 'spikes login' first.",
))
})?;
let share_info = fetch_share_info(&token, &options.slug)?;
if !options.force && !options.json {
print!(
"Delete share '{}'? This cannot be undone. [y/N] ",
options.slug
);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
if !input.trim().eq_ignore_ascii_case("y") {
println!("Cancelled.");
return Ok(());
}
}
delete_share(&token, &share_info.id)?;
let backup_path = save_exported_spikes(&options.slug, &share_info.exported_spikes)?;
let result = UnshareResult {
success: true,
slug: options.slug.clone(),
spikes_saved: share_info.exported_spikes.len(),
backup_path: backup_path.clone(),
};
if options.json {
println!(
"{}",
serde_json::to_string_pretty(&result).expect("Failed to serialize to JSON")
);
} else {
println!();
println!(" 🗡️ Share deleted");
println!();
println!(" Slug: {}", options.slug);
println!(" Spikes saved: {}", share_info.exported_spikes.len());
if let Some(path) = backup_path {
println!(" Backup: {}", path);
}
println!();
}
Ok(())
}
fn fetch_share_info(token: &str, slug: &str) -> Result<ShareInfo> {
let api_base = get_api_base();
let url = format!("{}/shares/{}", api_base.trim_end_matches('/'), slug);
let response = match ureq::get(&url)
.set("Authorization", &format!("Bearer {}", token))
.call()
{
Ok(resp) => resp,
Err(ureq::Error::Status(status, response)) => {
let body = response.into_string().ok();
return Err(map_http_error(status, body.as_deref()));
}
Err(e) => return Err(map_network_error(&e.to_string())),
};
let status = response.status();
if status != 200 {
let body = response.into_string().ok();
return Err(map_http_error(status, body.as_deref()));
}
let body = response
.into_string()
.map_err(|e| Error::RequestFailed(format!("Failed to read response: {}", e)))?;
let share_info: ShareInfo = serde_json::from_str(&body)?;
Ok(share_info)
}
fn delete_share(token: &str, id: &str) -> Result<()> {
let api_base = get_api_base();
let url = format!("{}/shares/{}", api_base.trim_end_matches('/'), id);
let response = match ureq::delete(&url)
.set("Authorization", &format!("Bearer {}", token))
.call()
{
Ok(resp) => resp,
Err(ureq::Error::Status(status, response)) => {
let body = response.into_string().ok();
return Err(map_http_error(status, body.as_deref()));
}
Err(e) => return Err(map_network_error(&e.to_string())),
};
let status = response.status();
if status != 200 && status != 204 {
let body = response.into_string().ok();
return Err(map_http_error(status, body.as_deref()));
}
Ok(())
}
fn save_exported_spikes(slug: &str, spikes: &[Spike]) -> Result<Option<String>> {
if spikes.is_empty() {
return Ok(None);
}
let spikes_dir = Path::new(".spikes");
if !spikes_dir.exists() {
fs::create_dir_all(spikes_dir)?;
}
let backup_path = spikes_dir.join(format!("{}.jsonl", slug));
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&backup_path)?;
for spike in spikes {
let mut json = serde_json::to_string(spike)?;
json.push('\n');
file.write_all(json.as_bytes())?;
}
Ok(Some(backup_path.to_string_lossy().to_string()))
}