lean_ctx/setup/
helpers.rs1#[allow(clippy::wildcard_imports)]
5use super::*;
6
7pub fn install_skill_files(home: &std::path::Path) -> Vec<(String, bool)> {
8 crate::rules_inject::install_all_skills(home)
9}
10
11pub(crate) fn install_kiro_steering(home: &std::path::Path) {
12 let cwd = std::env::current_dir().unwrap_or_else(|_| home.to_path_buf());
13 let steering_dir = cwd.join(".kiro").join("steering");
14 let steering_file = steering_dir.join("lean-ctx.md");
15
16 if steering_file.exists()
17 && std::fs::read_to_string(&steering_file)
18 .unwrap_or_default()
19 .contains("lean-ctx")
20 {
21 println!(" Kiro steering file already exists at .kiro/steering/lean-ctx.md");
22 return;
23 }
24
25 let _ = std::fs::create_dir_all(&steering_dir);
26 let _ = std::fs::write(&steering_file, crate::hooks::kiro_steering_content());
27 println!(
28 " \x1b[32m✓\x1b[0m Created .kiro/steering/lean-ctx.md (Kiro will now prefer lean-ctx tools)"
29 );
30}
31
32pub(crate) fn configure_plan_mode_settings(newly_configured: &[&str], already_configured: &[&str]) {
33 use crate::terminal_ui;
34
35 let all_configured: Vec<&str> = newly_configured
36 .iter()
37 .chain(already_configured.iter())
38 .copied()
39 .collect();
40
41 let has_vscode = all_configured.contains(&"VS Code");
42 let has_claude = all_configured.contains(&"Claude Code");
43 let has_codebuddy = all_configured.contains(&"CodeBuddy");
44
45 if !has_vscode && !has_claude && !has_codebuddy {
46 return;
47 }
48
49 if has_vscode {
50 match crate::core::editor_registry::plan_mode::write_vscode_plan_settings() {
51 Ok(r) if r.action == WriteAction::Already => {
52 terminal_ui::print_status_ok(
53 "VS Code \x1b[2mplan mode already configured\x1b[0m",
54 );
55 }
56 Ok(_) => {
57 terminal_ui::print_status_new(
58 "VS Code \x1b[2mplan mode tools configured\x1b[0m",
59 );
60 }
61 Err(e) => {
62 terminal_ui::print_status_warn(&format!("VS Code plan mode: {e}"));
63 }
64 }
65 }
66
67 if has_claude {
68 match crate::core::editor_registry::plan_mode::write_claude_code_plan_permissions() {
69 Ok(r) if r.action == WriteAction::Already => {
70 terminal_ui::print_status_ok(
71 "Claude Code \x1b[2mplan mode permissions present\x1b[0m",
72 );
73 }
74 Ok(_) => {
75 terminal_ui::print_status_new(
76 "Claude Code \x1b[2mplan mode permissions added\x1b[0m",
77 );
78 }
79 Err(e) => {
80 terminal_ui::print_status_warn(&format!("Claude Code plan mode: {e}"));
81 }
82 }
83 }
84
85 if has_codebuddy {
86 match crate::core::editor_registry::plan_mode::write_claude_code_plan_permissions() {
87 Ok(r) if r.action == WriteAction::Already => {
88 terminal_ui::print_status_ok(
89 "CodeBuddy \x1b[2mplan mode permissions present\x1b[0m",
90 );
91 }
92 Ok(_) => {
93 terminal_ui::print_status_new(
94 "CodeBuddy \x1b[2mplan mode permissions added\x1b[0m",
95 );
96 }
97 Err(e) => {
98 terminal_ui::print_status_warn(&format!("CodeBuddy plan mode: {e}"));
99 }
100 }
101 }
102}
103
104pub(crate) fn shorten_path(path: &str, home: &str) -> String {
105 if let Some(stripped) = path.strip_prefix(home) {
106 format!("~{stripped}")
107 } else {
108 path.to_string()
109 }
110}
111
112fn upsert_toml_key(content: &mut String, key: &str, value: &str) {
113 let pattern = format!("{key} = ");
114 if let Some(start) = content.find(&pattern) {
115 let line_end = content[start..]
116 .find('\n')
117 .map_or(content.len(), |p| start + p);
118 content.replace_range(start..line_end, &format!("{key} = \"{value}\""));
119 } else {
120 if !content.is_empty() && !content.ends_with('\n') {
121 content.push('\n');
122 }
123 content.push_str(&format!("{key} = \"{value}\"\n"));
124 }
125}
126
127fn remove_toml_key(content: &mut String, key: &str) {
128 let pattern = format!("{key} = ");
129 if let Some(start) = content.find(&pattern) {
130 let line_end = content[start..]
131 .find('\n')
132 .map_or(content.len(), |p| start + p + 1);
133 content.replace_range(start..line_end, "");
134 }
135}
136
137pub(crate) fn configure_tool_profile() {
138 use crate::terminal_ui;
139 use std::io::Write;
140
141 let cfg = crate::core::config::Config::load();
142 let current = cfg.tool_profile_effective();
143 let pinned = cfg.tool_profile.is_some() || std::env::var("LEAN_CTX_TOOL_PROFILE").is_ok();
144
145 if pinned && !matches!(current, crate::core::tool_profiles::ToolProfile::Power) {
149 terminal_ui::print_status_ok(&format!(
150 "Tool profile: {} ({} tools)",
151 current.as_str(),
152 current.tool_count()
153 ));
154 return;
155 }
156
157 let dim = "\x1b[2m";
158 let bold = "\x1b[1m";
159 let cyan = "\x1b[36m";
160 let rst = "\x1b[0m";
161
162 let registry_count = crate::server::registry::tool_count();
163 let lazy_count = crate::tool_defs::core_tool_names().len();
164
165 println!(" {dim}Control how many MCP tool schemas your AI agent sees.{rst}");
166 println!(" {dim}Fewer advertised tools = less context overhead. Every tool stays{rst}");
167 println!(" {dim}callable through ctx_call, even when its schema is not advertised.{rst}");
168 println!();
169 println!(
170 " {cyan}lean{rst} — {lazy_count} tools {dim}(lazy core, recommended — lowest token overhead){rst}"
171 );
172 println!(
173 " {cyan}minimal{rst} — 5 tools {dim}(ctx_read, ctx_shell, ctx_search, ctx_glob, ctx_tree){rst}"
174 );
175 println!(" {cyan}standard{rst} — 16 tools {dim}(balanced set for most workflows){rst}");
176 println!(
177 " {cyan}power{rst} — {registry_count} tools {dim}(everything advertised, costs the most context){rst}"
178 );
179 println!();
180 print!(" Tool profile? {bold}[lean/minimal/standard/power]{rst} {dim}(default: lean){rst} ");
181 std::io::stdout().flush().ok();
182
183 let mut profile_input = String::new();
184 let profile_name = if std::io::stdin().read_line(&mut profile_input).is_ok() {
185 let trimmed = profile_input.trim().to_lowercase();
186 match trimmed.as_str() {
187 "minimal" | "min" => "minimal",
188 "standard" | "std" => "standard",
189 "power" | "full" | "all" => "power",
190 _ => "lean",
191 }
192 } else {
193 "lean"
194 };
195
196 if profile_name == "lean" {
197 match crate::core::tool_profiles::clear_profile_in_config() {
198 Ok(()) => terminal_ui::print_status_ok(&format!(
199 "Tool profile: lean ({lazy_count} tools advertised, all reachable via ctx_call)"
200 )),
201 Err(e) => terminal_ui::print_status_warn(&format!("Could not save tool profile: {e}")),
202 }
203 return;
204 }
205
206 match crate::core::tool_profiles::set_profile_in_config(profile_name) {
207 Ok(()) => {
208 let profile = crate::core::tool_profiles::ToolProfile::parse(profile_name)
209 .unwrap_or(crate::core::tool_profiles::ToolProfile::Standard);
210 let count = match &profile {
211 crate::core::tool_profiles::ToolProfile::Power => registry_count,
212 other => other.tool_count(),
213 };
214 terminal_ui::print_status_ok(&format!("Tool profile: {profile_name} ({count} tools)"));
215 }
216 Err(e) => {
217 terminal_ui::print_status_warn(&format!("Could not save tool profile: {e}"));
218 }
219 }
220}
221
222pub(crate) fn configure_premium_features(home: &std::path::Path) {
223 use crate::terminal_ui;
224 use std::io::Write;
225
226 let config_path = crate::core::config::Config::path()
227 .unwrap_or_else(|| home.join(".config/lean-ctx").join("config.toml"));
228 if let Some(dir) = config_path.parent() {
229 let _ = std::fs::create_dir_all(dir);
230 }
231 let mut config_content = std::fs::read_to_string(&config_path).unwrap_or_default();
232
233 let dim = "\x1b[2m";
234 let bold = "\x1b[1m";
235 let cyan = "\x1b[36m";
236 let rst = "\x1b[0m";
237
238 println!("\n {bold}Compression Level{rst} {dim}(controls all token optimization layers){rst}");
240 println!(" {dim}Applies to tool output, agent prompts, and protocol mode.{rst}");
241 println!();
242 println!(" {cyan}off{rst} — No compression (full verbose output)");
243 println!(
244 " {cyan}lite{rst} — Light: concise output, basic terse filtering {dim}(~25% savings){rst}"
245 );
246 println!(
247 " {cyan}standard{rst} — Dense output + compact protocol + pattern-aware {dim}(~45% savings){rst}"
248 );
249 println!(
250 " {cyan}max{rst} — Expert mode: TDD protocol, all layers active {dim}(~65% savings){rst}"
251 );
252 println!();
253 print!(" Compression level? {bold}[off/lite/standard/max]{rst} {dim}(default: off){rst} ");
254 std::io::stdout().flush().ok();
255
256 let mut level_input = String::new();
257 let level = if std::io::stdin().read_line(&mut level_input).is_ok() {
258 match level_input.trim().to_lowercase().as_str() {
259 "lite" => "lite",
260 "standard" | "std" => "standard",
261 "max" => "max",
262 _ => "off",
263 }
264 } else {
265 "off"
266 };
267
268 let (effective_level, compression_status) = if level != "off" {
271 upsert_toml_key(&mut config_content, "compression_level", level);
272 remove_toml_key(&mut config_content, "terse_agent");
273 remove_toml_key(&mut config_content, "output_density");
274 (
275 crate::core::config::CompressionLevel::from_str_label(level),
276 StatusLine::ok(format!("Compression: {level}")),
277 )
278 } else if config_content.contains("compression_level") {
279 upsert_toml_key(&mut config_content, "compression_level", "off");
280 (
281 Some(crate::core::config::CompressionLevel::Off),
282 StatusLine::ok("Compression: off".to_string()),
283 )
284 } else {
285 (
286 Some(crate::core::config::CompressionLevel::Off),
287 StatusLine::skip(
288 "Compression: off (change later with: lean-ctx compression <level>)".to_string(),
289 ),
290 )
291 };
292
293 println!(
295 "\n {bold}Tool Result Archive{rst} {dim}(zero-loss: large outputs archived, retrievable via ctx_expand){rst}"
296 );
297 print!(" Enable auto-archive? {bold}[Y/n]{rst} ");
298 std::io::stdout().flush().ok();
299
300 let mut archive_input = String::new();
301 let archive_on = if std::io::stdin().read_line(&mut archive_input).is_ok() {
302 let a = archive_input.trim().to_lowercase();
303 a.is_empty() || a == "y" || a == "yes"
304 } else {
305 true
306 };
307
308 let archive_status = if archive_on && !config_content.contains("[archive]") {
309 if !config_content.is_empty() && !config_content.ends_with('\n') {
310 config_content.push('\n');
311 }
312 config_content.push_str("\n[archive]\nenabled = true\n");
313 Some(StatusLine::ok("Tool Result Archive: enabled".to_string()))
314 } else if !archive_on {
315 Some(StatusLine::skip(
316 "Archive: off (enable later in config.toml)".to_string(),
317 ))
318 } else {
319 None
320 };
321
322 match crate::config_io::write_atomic_with_backup(&config_path, &config_content) {
326 Ok(()) => {
327 compression_status.emit();
328 if effective_level.is_some() {
329 let home = dirs::home_dir().unwrap_or_default();
330 let result = crate::rules_inject::inject_all_rules(&home);
331 if !result.updated.is_empty() {
332 terminal_ui::print_status_ok(&format!(
333 "Updated {} rules file(s) with compression prompt",
334 result.updated.len()
335 ));
336 }
337 }
338 if let Some(status) = archive_status {
339 status.emit();
340 }
341 }
342 Err(e) => {
343 terminal_ui::print_status_warn(&format!(
344 "Could not save settings to {}: {e}",
345 config_path.display()
346 ));
347 terminal_ui::print_status_warn(
348 "Premium features were not applied — re-run `lean-ctx setup` or edit config.toml manually",
349 );
350 }
351 }
352}
353
354struct StatusLine {
357 skip: bool,
358 msg: String,
359}
360
361impl StatusLine {
362 fn ok(msg: String) -> Self {
363 Self { skip: false, msg }
364 }
365 fn skip(msg: String) -> Self {
366 Self { skip: true, msg }
367 }
368 fn emit(&self) {
369 if self.skip {
370 crate::terminal_ui::print_status_skip(&self.msg);
371 } else {
372 crate::terminal_ui::print_status_ok(&self.msg);
373 }
374 }
375}