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)?;
89 std::fs::write(path, txt + "\n")?;
90 Ok(())
91}
92
93fn strip_jsonc_comments(s: &str) -> String {
95 let mut out = String::with_capacity(s.len());
96 let mut chars = s.chars().peekable();
97 let mut in_str = false;
98 let mut escape = false;
99
100 while let Some(c) = chars.next() {
101 if escape {
102 out.push(c);
103 escape = false;
104 continue;
105 }
106 if in_str {
107 if c == '\\' {
108 escape = true;
109 out.push(c);
110 continue;
111 }
112 if c == '"' {
113 in_str = false;
114 }
115 out.push(c);
116 continue;
117 }
118 if c == '"' {
119 in_str = true;
120 out.push(c);
121 continue;
122 }
123 if c == '/' {
124 match chars.peek() {
125 Some('/') => {
126 for nc in chars.by_ref() {
127 if nc == '\n' {
128 out.push('\n');
129 break;
130 }
131 }
132 continue;
133 }
134 Some('*') => {
135 chars.next();
136 while let Some(nc) = chars.next() {
137 if nc == '*' && chars.peek() == Some(&'/') {
138 chars.next();
139 break;
140 }
141 }
142 continue;
143 }
144 _ => {}
145 }
146 }
147 out.push(c);
148 }
149 out
150}
151
152fn strip_toml_section(toml: &str, section_prefix: &str) -> String {
155 let mut out = String::new();
156 let mut skip = false;
157 for line in toml.lines() {
158 let trimmed = line.trim();
159 if trimmed.starts_with('[') {
160 let header = trimmed.trim_start_matches('[').trim_end_matches(']');
162 skip = header == section_prefix || header.starts_with(&format!("{section_prefix}."));
163 }
164 if !skip {
165 out.push_str(line);
166 out.push('\n');
167 }
168 }
169 out
170}