use std::fs;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::error::{Error, IoContext, Result};
use crate::host::{Plugin, Scope, Source, data_root};
use crate::util::hex;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Marker {
pub binary_version: String,
pub plugin_version: String,
pub source_mode: String,
pub scope: String,
pub agent: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub statusline_original: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub statusline_command: Option<String>,
}
fn source_mode(source: &Source) -> &'static str {
match source {
Source::Embedded => "embedded",
Source::GitHub { .. } => "github",
Source::Path(_) => "path",
}
}
fn marker_path(plugin: &Plugin, scope: &Scope, agent: &str) -> Result<PathBuf> {
let root = data_root(plugin)?;
let mut hasher = Sha256::new();
hasher.update(plugin.name.as_bytes());
hasher.update([0]);
hasher.update(scope.key().as_bytes());
hasher.update([0]);
hasher.update(agent.as_bytes());
Ok(root.join("markers").join(hex(&hasher.finalize())))
}
pub(crate) fn read(plugin: &Plugin, scope: &Scope, agent: &str) -> Result<Option<Marker>> {
let path = marker_path(plugin, scope, agent)?;
match fs::read(&path) {
Ok(bytes) => Ok(serde_json::from_slice(&bytes).ok()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(Error::Io { context: format!("reading marker {}", path.display()), source: e }),
}
}
fn base_marker(plugin: &Plugin, scope: &Scope, source: &Source, agent: &str) -> Marker {
Marker {
binary_version: plugin.version.to_string(),
plugin_version: plugin.version.to_string(),
source_mode: source_mode(source).to_string(),
scope: scope.as_cli().to_string(),
agent: agent.to_string(),
project_path: scope.cwd().map(|p| p.display().to_string()),
source_path: match source {
Source::Path(p) => Some(p.display().to_string()),
Source::Embedded | Source::GitHub { .. } => None,
},
statusline_original: None,
statusline_command: None,
}
}
fn write_marker(path: &std::path::Path, marker: &Marker) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).io_ctx(|| format!("creating {}", parent.display()))?;
}
let bytes = serde_json::to_vec_pretty(marker).map_err(|source| Error::Json { what: "stamp marker".into(), source })?;
fs::write(path, bytes).io_ctx(|| format!("writing marker {}", path.display()))
}
pub(crate) fn write(plugin: &Plugin, scope: &Scope, source: &Source, agent: &str) -> Result<()> {
let path = marker_path(plugin, scope, agent)?;
let mut marker = base_marker(plugin, scope, source, agent);
let previous = read(plugin, scope, agent)?;
marker.statusline_original = previous.as_ref().and_then(|m| m.statusline_original.clone());
marker.statusline_command = previous.and_then(|m| m.statusline_command);
write_marker(&path, &marker)
}
crate::agents::cfg_statusline_backends! {
pub(crate) fn stash_statusline(plugin: &Plugin, scope: &Scope, source: &Source, agent: &str, original: serde_json::Value) -> Result<()> {
amend(plugin, scope, source, agent, |marker| marker.statusline_original = Some(original))
}
}
crate::agents::cfg_statusline_backends! {
pub(crate) fn record_statusline_command(plugin: &Plugin, scope: &Scope, source: &Source, agent: &str, command: &str) -> Result<()> {
amend(plugin, scope, source, agent, |marker| marker.statusline_command = Some(command.to_string()))
}
}
crate::agents::cfg_statusline_backends! {
fn amend(plugin: &Plugin, scope: &Scope, source: &Source, agent: &str, edit: impl FnOnce(&mut Marker)) -> Result<()> {
let path = marker_path(plugin, scope, agent)?;
let mut marker = read(plugin, scope, agent)?.unwrap_or_else(|| base_marker(plugin, scope, source, agent));
edit(&mut marker);
write_marker(&path, &marker)
}
}
pub(crate) fn clear(plugin: &Plugin, scope: &Scope, agent: &str) -> Result<()> {
let path = marker_path(plugin, scope, agent)?;
match fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(Error::Io { context: format!("clearing marker {}", path.display()), source: e }),
}
}
pub(crate) fn source_from_marker(marker: Option<&Marker>, default: Source) -> Source {
match marker {
Some(m) if m.source_mode == "path" => m.source_path.clone().map(|p| Source::Path(PathBuf::from(p))).unwrap_or(default),
Some(m) if m.source_mode == "embedded" => Source::Embedded,
_ => default,
}
}
pub(crate) fn resolve_source(plugin: &Plugin, scope: &Scope, agent: &str, default: Source) -> Source {
let marker = read(plugin, scope, agent).ok().flatten();
source_from_marker(marker.as_ref(), default)
}
#[cfg(test)]
#[path = "../tests/unit/stamp.rs"]
mod stamp_tests;