use serde_json::Value;
use std::path::Path;
use super::types::{Check, Status};
use crate::install::cursor_config::plan::{
classify_remem_mcp, managed_cursor_mcp_entry, parse_receipt_from_config, CursorInstallReceipt,
McpOwnership,
};
use crate::install::cursor_config::schema::{validate_hooks_document, validate_mcp_document};
use crate::install::cursor_config::{
cursor_detected, cursor_hooks_path, cursor_mcp_path, CURSOR_HOOK_FAILURE_POLICY_LINE,
CURSOR_SESSION_INIT_LINE,
};
const EFFECTIVE_CAPABILITY_LINES: &str =
"effective: unknown (evidence: GH-822 PR #914, Cursor 3.12.17); \
postToolUse_delivery: proven; postToolUse_managed_context: not_configured; \
sessionStart: blocked; stop: unknown; preCompact: unknown";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ConfiguredMode {
Full,
HooksOnly,
None,
Unknown,
}
impl ConfiguredMode {
fn label(self) -> &'static str {
match self {
ConfiguredMode::Full => "full",
ConfiguredMode::HooksOnly => "hooks_only",
ConfiguredMode::None => "none",
ConfiguredMode::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone)]
struct CursorDoctorState {
detected: bool,
configured: bool,
configured_mode: ConfiguredMode,
malformed: Option<String>,
partial_state: Option<&'static str>,
drift: bool,
collision: bool,
}
pub(super) fn check_cursor_install() -> Check {
let state = classify(&cursor_hooks_path(), &cursor_mcp_path());
render(state)
}
fn classify(hooks_path: &Path, mcp_path: &Path) -> CursorDoctorState {
let mut state = CursorDoctorState {
detected: cursor_detected(),
configured: false,
configured_mode: ConfiguredMode::None,
malformed: None,
partial_state: None,
drift: false,
collision: false,
};
if !state.detected {
return state;
}
let hooks_doc = match read_json(hooks_path) {
Ok(doc) => doc,
Err(detail) => {
state.malformed = Some(detail);
state.configured_mode = ConfiguredMode::Unknown;
return state;
}
};
if let Some(doc) = hooks_doc.as_ref() {
if let Err(error) = validate_hooks_document(doc) {
state.malformed = Some(format!("{}: {error}", hooks_path.display()));
state.configured_mode = ConfiguredMode::Unknown;
return state;
}
}
let mcp_doc = match read_json(mcp_path) {
Ok(doc) => doc,
Err(detail) => {
state.malformed = Some(detail);
state.configured_mode = ConfiguredMode::Unknown;
return state;
}
};
if let Some(doc) = mcp_doc.as_ref() {
if let Err(error) = validate_mcp_document(doc) {
state.malformed = Some(format!("{}: {error}", mcp_path.display()));
state.configured_mode = ConfiguredMode::Unknown;
return state;
}
}
let receipt = match load_receipt() {
Ok(receipt) => receipt,
Err(detail) => {
state.malformed = Some(detail);
state.configured_mode = ConfiguredMode::Unknown;
return state;
}
};
let remem_entry = mcp_doc
.as_ref()
.and_then(|doc| doc.get("mcpServers"))
.and_then(|servers| servers.get("remem"));
let mcp_present = remem_entry.is_some();
match &receipt {
None => {
if mcp_present {
state.configured_mode = ConfiguredMode::Unknown;
state.partial_state =
Some("mcpServers.remem exists but no install receipt records it");
} else {
state.configured_mode = ConfiguredMode::None;
}
}
Some(receipt) => {
let ownership = classify_remem_mcp(remem_entry, &receipt.binary_path, Some(receipt));
let current_expected = remem_entry
.is_some_and(|entry| *entry == managed_cursor_mcp_entry(&receipt.binary_path));
match (receipt.mode.as_str(), mcp_present) {
("full", true) => match ownership {
McpOwnership::OwnedCurrent | McpOwnership::OwnedReceipt if current_expected => {
state.configured = true;
state.configured_mode = ConfiguredMode::Full;
}
McpOwnership::OwnedCurrent | McpOwnership::OwnedReceipt => {
state.drift = true;
state.configured_mode = ConfiguredMode::Unknown;
}
_ => {
state.collision = true;
state.configured_mode = ConfiguredMode::Unknown;
}
},
("full", false) => {
state.configured_mode = ConfiguredMode::Unknown;
state.partial_state =
Some("receipt records mode=full but mcpServers.remem is missing");
}
("hooks_only", false) => {
state.configured = true;
state.configured_mode = ConfiguredMode::HooksOnly;
}
("hooks_only", true) => match ownership {
McpOwnership::Collision => {
state.collision = true;
state.configured_mode = ConfiguredMode::Unknown;
}
_ => {
state.configured_mode = ConfiguredMode::Unknown;
state.partial_state =
Some("receipt records mode=hooks_only but a remem MCP entry exists");
}
},
_ => {
state.configured_mode = ConfiguredMode::Unknown;
state.partial_state = Some("receipt mode is not recognized");
}
}
}
}
state
}
fn render(state: CursorDoctorState) -> Check {
let status = if state.malformed.is_some() || state.collision || state.partial_state.is_some() {
Status::Fail
} else if state.drift || (state.detected && !state.configured) {
Status::Warn
} else {
Status::Ok
};
let mut detail = format!(
"detected={} configured={} configured_mode={}",
state.detected,
state.configured,
state.configured_mode.label()
);
if let Some(malformed) = &state.malformed {
detail.push_str(&format!(
"\nmalformed: {malformed} (files were left unchanged)"
));
}
if let Some(partial) = state.partial_state {
detail.push_str(&format!(
"\npartial_state: {partial}; rerun `remem install --target cursor` or `remem uninstall --target cursor` to converge"
));
}
if state.collision {
detail.push_str(
"\ncollision: an entry remem cannot prove it owns occupies a managed key; remem will not modify or delete it",
);
}
if state.drift {
detail.push_str(
"\ndrift: managed entry differs from the current builder shape; rerun `remem install --target cursor`",
);
}
detail.push_str(&format!("\n{CURSOR_HOOK_FAILURE_POLICY_LINE}"));
detail.push_str(&format!("\n{EFFECTIVE_CAPABILITY_LINES}"));
detail.push_str(&format!(
"\nsession_init: unsupported\n{CURSOR_SESSION_INIT_LINE}"
));
Check::new("Cursor install", status, detail)
}
fn read_json(path: &Path) -> Result<Option<Value>, String> {
let content = match std::fs::read_to_string(path) {
Ok(content) => content,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(format!("cannot read {}: {error}", path.display())),
};
match serde_json::from_str(&content) {
Ok(doc) => Ok(Some(doc)),
Err(error) => Err(format!("cannot parse {}: {error}", path.display())),
}
}
fn load_receipt() -> Result<Option<CursorInstallReceipt>, String> {
let path = crate::runtime_config::config_path();
let content = match std::fs::read_to_string(&path) {
Ok(content) => content,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(format!("cannot read {}: {error}", path.display())),
};
let doc = content
.parse::<toml_edit::DocumentMut>()
.map_err(|error| format!("cannot parse {}: {error}", path.display()))?;
parse_receipt_from_config(&doc).map_err(|error| format!("{}: {error:#}", path.display()))
}
#[cfg(test)]
#[path = "cursor_install/tests.rs"]
mod tests;