use std::fs;
use std::path::PathBuf;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
pub const RECEIPT_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Receipt {
pub schema_version: u32,
pub version: String,
pub binary: PathBuf,
pub install_dir: PathBuf,
pub platform: String,
pub source: String,
pub verified: bool,
pub modify_path: bool,
pub installed_at: String,
}
pub fn arete_home() -> Result<PathBuf> {
if let Some(path) = std::env::var_os("ARETE_HOME") {
if !path.is_empty() {
return Ok(PathBuf::from(path));
}
}
let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?;
Ok(home.join(".arete"))
}
pub fn receipt_path() -> Result<PathBuf> {
Ok(arete_home()?.join("receipt.json"))
}
impl Receipt {
pub fn load() -> Result<Option<Receipt>> {
let path = receipt_path()?;
match fs::read_to_string(&path) {
Ok(content) => {
let receipt: Receipt = serde_json::from_str(&content).with_context(|| {
format!("Failed to parse install receipt {}", path.display())
})?;
Ok(Some(receipt))
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error).with_context(|| format!("Failed to read {}", path.display())),
}
}
pub fn save(&self) -> Result<PathBuf> {
let path = receipt_path()?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create {}", parent.display()))?;
}
let content = serde_json::to_string_pretty(self)?;
fs::write(&path, format!("{content}\n"))
.with_context(|| format!("Failed to write install receipt {}", path.display()))?;
Ok(path)
}
pub fn delete() -> Result<()> {
let path = receipt_path()?;
match fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => {
Err(error).with_context(|| format!("Failed to remove {}", path.display()))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn receipt_round_trips_with_camel_case_keys() {
let receipt = Receipt {
schema_version: RECEIPT_SCHEMA_VERSION,
version: "0.13.0".into(),
binary: PathBuf::from("/Users/x/.local/bin/a4"),
install_dir: PathBuf::from("/Users/x/.local/bin"),
platform: "darwin-arm64".into(),
source: "sh".into(),
verified: true,
modify_path: true,
installed_at: "2026-09-10T12:00:00Z".into(),
};
let json = serde_json::to_string(&receipt).unwrap();
assert!(json.contains("\"schemaVersion\":1"));
assert!(json.contains("\"installDir\""));
assert!(json.contains("\"modifyPath\""));
assert!(json.contains("\"installedAt\""));
let parsed: Receipt = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, receipt);
}
}