1use crate::{SKILL, VERSION, read, write};
2use serde_json::{Value, json};
3use std::path::Path;
4use toml_edit::{DocumentMut, value};
5
6const START: &str = "<!-- ravenrs:begin -->";
7const END: &str = "<!-- ravenrs:end -->";
8const TOML_START: &str = "# ravenrs:begin";
9const TOML_END: &str = "# ravenrs:end";
10
11fn marked(previous: &str, body: &str, start: &str, end: &str) -> Result<String, String> {
12 let block = format!("{start}\n{body}\n{end}");
13 match (previous.find(start), previous.find(end)) {
14 (Some(a), Some(b))
15 if b > a
16 && !previous[a + start.len()..].contains(start)
17 && !previous[b + end.len()..].contains(end) =>
18 {
19 Ok(format!("{}{}{}", &previous[..a], block, &previous[b + end.len()..]))
20 }
21 (None, None) => Ok(format!(
22 "{previous}{}{block}\n",
23 if previous.is_empty() || previous.ends_with('\n') { "" } else { "\n" }
24 )),
25 _ => Err("Bloco RavenRS incompleto ou duplicado; corrija os marcadores antes de atualizar.".into()),
26 }
27}
28
29fn previous(root: &Path, path: &str) -> Result<String, String> {
30 if root.join(path).exists() { read(&root.join(path)) } else { Ok(String::new()) }
31}
32
33fn json_file(root: &Path, path: &str) -> Result<Value, String> {
34 let old = previous(root, path)?;
35 if old.is_empty() {
36 return Ok(json!({}));
37 }
38 let parsed: Value = serde_json::from_str(&old).map_err(|e| format!("{path}: {e}"))?;
39 if !parsed.is_object() {
40 return Err(format!("{path}: objeto JSON esperado"));
41 }
42 Ok(parsed)
43}
44
45pub fn configure_ai(root: &Path, plugin: bool) -> Result<(), String> {
48 let files = ai_files(root, plugin)?;
49 let skill = root.join(".claude/skills/ravenrs/SKILL.md");
50 let remove_skill = plugin && skill.is_file();
51 if remove_skill && read(&skill)? != SKILL {
52 return Err("A skill local tem alterações. Mova suas notas antes de trocar para --plugin.".into());
53 }
54 for (path, contents) in files {
55 write(root, &path, &contents)?;
56 }
57 if remove_skill {
58 std::fs::remove_file(skill).map_err(|e| e.to_string())?;
59 }
60 println!("Arquivos de IA atualizados para RavenRS {VERSION}.");
61 Ok(())
62}
63
64pub(crate) fn ai_files(root: &Path, plugin: bool) -> Result<Vec<(String, String)>, String> {
65 let _ = read(&root.join("Cargo.toml"))?;
66 let mut files = Vec::new();
67 for (path, key, vscode) in [
68 (".mcp.json", "mcpServers", false),
69 (".cursor/mcp.json", "mcpServers", false),
70 (".vscode/mcp.json", "servers", true),
71 ] {
72 let mut config = json_file(root, path)?;
73 let mut server = json!({"command":"cargo", "args":["ravenrs", "mcp"]});
74 if vscode {
75 server["type"] = json!("stdio");
76 }
77 if config.get(key).is_some_and(|v| !v.is_object()) {
78 return Err(format!("{path}: {key} deve ser um objeto"));
79 }
80 if config.get(key).is_none() {
81 config[key] = json!({});
82 }
83 config[key]["ravenrs"] = server;
84 config["$comment"] = json!(format!("cargo-ravenrs {VERSION}; managed entry: {key}.ravenrs"));
85 files.push((
86 path.into(),
87 format!("{}\n", serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?),
88 ));
89 }
90 let path = ".codex/config.toml";
91 let old = previous(root, path)?;
92 let mut doc: DocumentMut =
93 if old.is_empty() { DocumentMut::new() } else { old.parse().map_err(|e| format!("{path}: {e}"))? };
94 if !old.contains(TOML_START) {
95 if let Some(servers) = doc.get_mut("mcp_servers").and_then(toml_edit::Item::as_table_mut) {
97 servers.remove("ravenrs");
98 }
99 }
100 let old = if old.contains(TOML_START) { old } else { doc.to_string() };
101 let body = format!(
102 "# cargo-ravenrs {VERSION}\n[mcp_servers.ravenrs]\ncommand = \"cargo\"\nargs = [\"ravenrs\", \"mcp\"]"
103 );
104 let contents = marked(&old, &body, TOML_START, TOML_END)?;
105 let _: DocumentMut = contents.parse().map_err(|e| format!("{path}: {e}"))?;
106 files.push((path.into(), contents));
107
108 let manifest: DocumentMut =
109 read(&root.join("Cargo.toml"))?.parse().map_err(|e| format!("Cargo.toml: {e}"))?;
110 let name = manifest["package"]["name"].as_str().unwrap_or("app").replace('-', "_");
111 let web = root.join("web/index.html").is_file();
112 let body = format!(
113 r#"Generated by cargo-ravenrs {VERSION}.
114
115Run: `cargo run`. Test: `cargo test`. Check: `cargo check`.
116{web_commands}
117The main screen is `src/lib.rs` (`app()`); the desktop entry point is `src/main.rs`.
118Before writing API calls, consult the RavenRS MCP (`component_api`, `component_examples`).
119Use semantic design tokens, library components and `ravenrs-app` for record lists and forms.
120Never invent a method without checking the MCP. Keep project-specific instructions outside this block.
121"#,
122 web_commands = if web {
123 format!(
124 "Web: `cargo check --target wasm32-unknown-unknown`.\nBuild: `cargo build --release --lib --target wasm32-unknown-unknown`.\nInstall `wasm-bindgen-cli --version {}` with cargo, then run `wasm-bindgen --target web --out-dir web/pkg target/wasm32-unknown-unknown/release/{name}.wasm` and serve `web/` over HTTP.",
125 ravenrs_project::WASM_BINDGEN_VERSION
126 )
127 } else {
128 String::new()
129 }
130 );
131 files.push(("AGENTS.md".into(), marked(&previous(root, "AGENTS.md")?, &body, START, END)?));
132 files.push((
133 "CLAUDE.md".into(),
134 marked(&previous(root, "CLAUDE.md")?, &format!("cargo-ravenrs {VERSION}\n\n@AGENTS.md"), START, END)?,
135 ));
136
137 let path = ".claude/settings.json";
138 let mut settings = json_file(root, path)?;
139 settings["$comment"] = json!(format!("cargo-ravenrs {VERSION}"));
140 if settings.get("enabledMcpjsonServers").is_none() {
141 settings["enabledMcpjsonServers"] = json!([]);
142 }
143 let servers =
144 settings["enabledMcpjsonServers"].as_array_mut().ok_or("enabledMcpjsonServers deve ser uma lista")?;
145 if !servers.iter().any(|v| v.as_str() == Some("ravenrs")) {
146 servers.push(json!("ravenrs"));
147 }
148 if settings.get("permissions").is_none() {
149 settings["permissions"] = json!({});
150 }
151 if !settings["permissions"].is_object() {
152 return Err("permissions deve ser um objeto".into());
153 }
154 if settings["permissions"].get("allow").is_none() {
155 settings["permissions"]["allow"] = json!([]);
156 }
157 let allow =
158 settings["permissions"]["allow"].as_array_mut().ok_or("permissions.allow deve ser uma lista")?;
159 for rule in ["Bash(cargo check *)", "Bash(cargo test *)", "Bash(cargo run *)", "mcp__ravenrs__*"] {
160 if !allow.iter().any(|v| v.as_str() == Some(rule)) {
161 allow.push(json!(rule));
162 }
163 }
164 if plugin {
165 if settings.get("extraKnownMarketplaces").is_none() {
166 settings["extraKnownMarketplaces"] = json!({});
167 }
168 if settings.get("enabledPlugins").is_none() {
169 settings["enabledPlugins"] = json!({});
170 }
171 if !settings["extraKnownMarketplaces"].is_object() || !settings["enabledPlugins"].is_object() {
172 return Err("Configuração de plugins inválida".into());
173 }
174 settings["extraKnownMarketplaces"]["ravenrs"] =
175 json!({"source":{"source":"github","repo":"alexkads/RavenRS","ref":format!("v{VERSION}")}});
176 settings["enabledPlugins"]["ravenrs@ravenrs"] = json!(true);
177 } else {
178 let path = ".claude/skills/ravenrs/SKILL.md";
180 let old = previous(root, path)?;
181 let skill = if old.is_empty() {
182 SKILL.into()
183 } else {
184 let start = "<!-- ravenrs:skill:begin -->";
185 let end = "<!-- ravenrs:skill:end -->";
186 if !old.contains(start) {
187 return Err(format!(
188 "{path}: skill sem marcadores; preserve seu conteúdo antes de adotar a skill gerenciada."
189 ));
190 }
191 let a = SKILL.find(start).ok_or("Skill sem marcador inicial")? + start.len();
192 let b = SKILL.find(end).ok_or("Skill sem marcador final")?;
193 marked(&old, SKILL[a..b].trim_matches('\n'), start, end)?
194 };
195 files.push((path.into(), skill));
196 if let Some(plugins) = settings.get_mut("enabledPlugins").and_then(Value::as_object_mut) {
197 plugins.remove("ravenrs@ravenrs");
198 }
199 }
200 files.push((
201 path.into(),
202 format!("{}\n", serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?),
203 ));
204 let mut state = DocumentMut::new();
206 state["version"] = value(VERSION);
207 state["plugin"] = value(plugin);
208 files.push((".ravenrs-ai.toml".into(), format!("# cargo-ravenrs {VERSION}\n{state}")));
209 Ok(files)
210}