1use std::borrow::Cow;
10use std::path::{Path, PathBuf};
11use thiserror::Error;
12
13pub const MARKER_KEY: &str = "_sentinel";
16
17pub const STATE_DIR: &str = ".sentinel";
20
21pub const WAIVER_DIR: &str = "sentinel";
24
25pub const WAIVERS_FILE: &str = "sentinel/waivers.toml";
27
28pub const RULE_PREFIX: &str = "sentinel.";
32
33const MODERN_RULE_PREFIX: &str = "pushkin.";
35
36pub const CODEX_RULES: &str = ".codex/rules/sentinel.rules";
39pub const AUGGIE_SCRIPT: &str = ".augment/hooks/sentinel.sh";
40pub const OPENCODE_PLUGIN: &str = ".opencode/plugin/sentinel.ts";
41pub const HERMES_HOOK: &str = ".hermes/hooks/sentinel.json";
42
43pub const HERMES_PLUGIN_DIR: &str = "sentinel-gate";
46
47pub const HERMES_DRAFT_DIR: &str = "sentinel_gate";
50
51#[must_use]
55pub fn modern_rule_id(rule: &str) -> Cow<'_, str> {
56 match rule.strip_prefix(RULE_PREFIX) {
57 Some(tail) => Cow::Owned(format!("{MODERN_RULE_PREFIX}{tail}")),
58 None => Cow::Borrowed(rule),
59 }
60}
61
62#[must_use]
65pub fn legacy_rule_id(rule: &str) -> Cow<'_, str> {
66 match rule.strip_prefix(MODERN_RULE_PREFIX) {
67 Some(tail) => Cow::Owned(format!("{RULE_PREFIX}{tail}")),
68 None => Cow::Borrowed(rule),
69 }
70}
71
72#[derive(Debug, Error)]
73pub enum MigrationError {
74 #[error(
75 "both {old} and {new} exist — refusing to guess which is current. \
76 A human must reconcile them (keep one, remove or merge the other), \
77 then re-run."
78 )]
79 BothExist { old: String, new: String },
80 #[error("cannot rename {old} to {new}: {source}")]
81 Rename {
82 old: String,
83 new: String,
84 source: std::io::Error,
85 },
86}
87
88#[derive(Debug)]
90pub struct MigratedDir {
91 pub old: PathBuf,
92 pub new: PathBuf,
93}
94
95pub fn migrate_dirs(root: &Path) -> Result<Vec<MigratedDir>, MigrationError> {
104 let mut renamed = Vec::new();
105 let state_pair = (root.join(STATE_DIR), root.join(".pushkin"));
106 if state_pair.0.is_dir() {
107 rename_pair(&state_pair.0, &state_pair.1, &mut renamed)?;
108 }
109 let waiver_pair = (root.join(WAIVER_DIR), root.join("pushkin"));
110 if root.join(WAIVERS_FILE).is_file() {
111 rename_pair(&waiver_pair.0, &waiver_pair.1, &mut renamed)?;
112 }
113 Ok(renamed)
114}
115
116fn rename_pair(
117 old: &Path,
118 new: &Path,
119 renamed: &mut Vec<MigratedDir>,
120) -> Result<(), MigrationError> {
121 if new.exists() {
122 return Err(MigrationError::BothExist {
125 old: format!("{}/", old.display()),
126 new: format!("{}/", new.display()),
127 });
128 }
129 std::fs::rename(old, new).map_err(|source| MigrationError::Rename {
130 old: old.display().to_string(),
131 new: new.display().to_string(),
132 source,
133 })?;
134 renamed.push(MigratedDir {
135 old: old.to_path_buf(),
136 new: new.to_path_buf(),
137 });
138 Ok(())
139}