innate_core/install/
mod.rs1use std::io::{self, BufRead, Write};
7use std::path::{Path, PathBuf};
8
9use chrono::Utc;
10use serde_json::{json, Value};
11
12const SKILL_MD: &str = include_str!("../../assets/SKILL.md");
13
14mod agents;
15mod path;
16mod settings;
17mod skills;
18mod ui;
19mod uninstall;
20mod wizard;
21
22pub use uninstall::run_uninstall;
23pub use wizard::run_install;
24
25const INNATE_TOOLS: &[&str] = &[
26 "innate_recall",
27 "innate_record",
28 "innate_add",
29 "innate_spark",
30 "innate_evolve",
31 "innate_inspect",
32 "innate_approve",
33 "innate_archive",
34 "innate_invalidate",
35 "innate_restore",
36 "innate_mature_spark",
37 "innate_promote_spark",
38 "innate_drop_spark",
39];
40
41fn home_dir() -> PathBuf {
46 dirs_next::home_dir().unwrap_or_else(|| PathBuf::from("."))
47}
48
49fn tilde_path(p: &Path) -> String {
50 let home = home_dir();
51 if let Ok(rel) = p.strip_prefix(&home) {
52 format!("~/{}", rel.display())
53 } else {
54 p.display().to_string()
55 }
56}
57
58fn read_json(path: &Path) -> Option<Value> {
59 let txt = std::fs::read_to_string(path).ok()?;
60 serde_json::from_str(&txt).ok()
61}
62
63fn read_json_object(path: &Path) -> Result<Value, String> {
68 match std::fs::read_to_string(path) {
69 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(json!({})),
70 Err(e) => Err(format!("cannot read {}: {e}", path.display())),
71 Ok(txt) => match serde_json::from_str::<Value>(&txt) {
72 Err(e) => Err(format!(
73 "cannot parse {}: {e} — fix the file and re-run",
74 path.display()
75 )),
76 Ok(v) if !v.is_object() => {
77 Err(format!("{}: root is not a JSON object", path.display()))
78 }
79 Ok(v) => Ok(v),
80 },
81 }
82}
83
84fn write_json(path: &Path, value: &Value) -> anyhow::Result<()> {
85 if let Some(parent) = path.parent() {
86 std::fs::create_dir_all(parent)?;
87 }
88 let txt = serde_json::to_string_pretty(value)? + "\n";
89 let tmp = path.with_extension("tmp");
95 std::fs::write(&tmp, txt.as_bytes())?;
96 if let Err(e) = std::fs::rename(&tmp, path) {
97 let _ = std::fs::remove_file(&tmp);
98 return Err(e.into());
99 }
100 Ok(())
101}
102
103fn strip_jsonc_comments(s: &str) -> String {
105 let mut out = String::with_capacity(s.len());
106 let mut chars = s.chars().peekable();
107 let mut in_str = false;
108 let mut escape = false;
109
110 while let Some(c) = chars.next() {
111 if escape {
112 out.push(c);
113 escape = false;
114 continue;
115 }
116 if in_str {
117 if c == '\\' {
118 escape = true;
119 out.push(c);
120 continue;
121 }
122 if c == '"' {
123 in_str = false;
124 }
125 out.push(c);
126 continue;
127 }
128 if c == '"' {
129 in_str = true;
130 out.push(c);
131 continue;
132 }
133 if c == '/' {
134 match chars.peek() {
135 Some('/') => {
136 for nc in chars.by_ref() {
137 if nc == '\n' {
138 out.push('\n');
139 break;
140 }
141 }
142 continue;
143 }
144 Some('*') => {
145 chars.next();
146 while let Some(nc) = chars.next() {
147 if nc == '*' && chars.peek() == Some(&'/') {
148 chars.next();
149 break;
150 }
151 }
152 continue;
153 }
154 _ => {}
155 }
156 }
157 out.push(c);
158 }
159 out
160}
161
162fn strip_toml_section(toml: &str, section_prefix: &str) -> String {
165 let mut out = String::new();
166 let mut skip = false;
167 for line in toml.lines() {
168 let trimmed = line.trim();
169 if trimmed.starts_with('[') {
170 let header = trimmed.trim_start_matches('[').trim_end_matches(']');
172 skip = header == section_prefix || header.starts_with(&format!("{section_prefix}."));
173 }
174 if !skip {
175 out.push_str(line);
176 out.push('\n');
177 }
178 }
179 out
180}