Skip to main content

lean_ctx/core/editor_registry/writers/
mod.rs

1// Auto-split from the former monolithic writers.rs. Grouped by operation
2// (install/uninstall) + shared helpers; behavior is unchanged.
3
4use super::types::{ConfigType, EditorTarget};
5
6mod install;
7mod shared;
8mod uninstall;
9
10pub use install::{COMMANDCODE_MCP_INSTRUCTIONS, validate_claude_binary};
11pub use shared::auto_approve_tools;
12pub use uninstall::remove_lean_ctx_mcp_server;
13// Routers below dispatch to every install/uninstall writer; a glob keeps the
14// dispatch table readable and lets the test module reach them via `super::*`.
15#[allow(clippy::wildcard_imports)]
16use install::*;
17#[allow(clippy::wildcard_imports)]
18use uninstall::*;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum WriteAction {
22    Created,
23    Updated,
24    Already,
25}
26
27#[derive(Debug, Clone, Copy, Default)]
28pub struct WriteOptions {
29    pub overwrite_invalid: bool,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct WriteResult {
34    pub action: WriteAction,
35    pub note: Option<String>,
36}
37
38pub fn write_config(target: &EditorTarget, binary: &str) -> Result<WriteResult, String> {
39    write_config_with_options(target, binary, WriteOptions::default())
40}
41
42pub fn write_config_with_options(
43    target: &EditorTarget,
44    binary: &str,
45    opts: WriteOptions,
46) -> Result<WriteResult, String> {
47    let allow_rule_steering = crate::core::config::Config::load().rules_injection_effective()
48        != crate::core::config::RulesInjection::Off;
49
50    write_config_with_options_and_rule_steering(target, binary, opts, allow_rule_steering)
51}
52
53pub fn write_config_with_options_and_rule_steering(
54    target: &EditorTarget,
55    binary: &str,
56    opts: WriteOptions,
57    allow_rule_steering: bool,
58) -> Result<WriteResult, String> {
59    // A host on the governor setup has no LeanCTX MCP server; setup, doctor
60    // and refresh paths all write through here.
61    if crate::core::governor_hosts::suppresses_mcp(&target.agent_key, &target.config_path) {
62        return Ok(WriteResult {
63            action: WriteAction::Already,
64            note: Some("governor setup: no MCP server".to_string()),
65        });
66    }
67    if let Some(parent) = target.config_path.parent() {
68        std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
69    }
70
71    match target.config_type {
72        ConfigType::McpJson => write_mcp_json(target, binary, opts),
73        ConfigType::Zed => write_zed_config(target, binary, opts),
74        ConfigType::Codex => write_codex_config(target, binary),
75        ConfigType::VsCodeMcp => write_vscode_mcp(target, binary, opts),
76        ConfigType::CopilotCli => write_copilot_cli(target, binary, opts),
77        ConfigType::OpenCode => write_opencode_config(target, binary, opts),
78        ConfigType::Crush => write_crush_config(target, binary, opts),
79        ConfigType::JetBrains => write_jetbrains_config(target, binary, opts),
80        ConfigType::Amp => write_amp_config(target, binary, opts),
81        ConfigType::HermesYaml => write_hermes_yaml(target, binary, opts),
82        ConfigType::GooseYaml => write_goose_yaml(target, binary, opts),
83        ConfigType::GeminiSettings => write_gemini_settings(target, binary, opts),
84        ConfigType::QoderSettings => write_qoder_settings(target, binary, opts),
85        ConfigType::AugmentVsCode => write_augment_vscode(target, binary, opts),
86        ConfigType::OpenClaw => write_openclaw_config(target, binary, opts),
87        ConfigType::VibeToml => write_vibe_toml(target, binary, opts),
88        ConfigType::CommandCode => {
89            write_commandcode_config_with_rule_steering(target, binary, opts, allow_rule_steering)
90        }
91        ConfigType::ClineCli => write_cline_cli_config(target, binary, opts),
92        ConfigType::OmpMcp => write_omp_mcp(target, binary, opts),
93        ConfigType::CodeWhale => write_codewhale_config(target, binary, opts),
94        ConfigType::MuseSettings => write_muse_settings(target, binary, opts),
95    }
96}
97
98pub fn remove_lean_ctx_server(
99    target: &EditorTarget,
100    opts: WriteOptions,
101) -> Result<WriteResult, String> {
102    match target.config_type {
103        ConfigType::McpJson
104        | ConfigType::JetBrains
105        | ConfigType::GeminiSettings
106        | ConfigType::QoderSettings
107        | ConfigType::CommandCode
108        | ConfigType::ClineCli
109        | ConfigType::OmpMcp
110        // Copilot CLI's `mcp-config.json` uses `mcpServers` (`write_copilot_cli`),
111        // not VS Code's `servers`; removing under `servers` left the entry behind.
112        | ConfigType::CopilotCli => remove_lean_ctx_mcp_server(&target.config_path, opts),
113        ConfigType::VsCodeMcp => remove_lean_ctx_vscode_server(&target.config_path, opts),
114        ConfigType::Codex => remove_lean_ctx_codex_server(&target.config_path),
115        ConfigType::OpenCode | ConfigType::Crush => {
116            remove_lean_ctx_named_json_server(&target.config_path, "mcp", opts)
117        }
118        ConfigType::Zed => {
119            remove_lean_ctx_named_json_server(&target.config_path, "context_servers", opts)
120        }
121        ConfigType::Amp => remove_lean_ctx_amp_server(&target.config_path, opts),
122        ConfigType::HermesYaml => remove_lean_ctx_hermes_yaml_server(&target.config_path),
123        ConfigType::GooseYaml => remove_lean_ctx_goose_yaml_server(&target.config_path),
124        ConfigType::AugmentVsCode => {
125            remove_lean_ctx_augment_vscode_server(&target.config_path, opts)
126        }
127        ConfigType::OpenClaw => remove_lean_ctx_openclaw_server(&target.config_path, opts),
128        ConfigType::VibeToml => remove_lean_ctx_vibe_toml_server(&target.config_path, opts),
129        // #1402: CodeWhale accepts either root key, and we merge into whichever
130        // one the user already had — so removal has to visit both, or an entry
131        // written under `servers` would survive `uninstall`.
132        ConfigType::CodeWhale => {
133            let servers = remove_lean_ctx_named_json_server(&target.config_path, "servers", opts)?;
134            let mcp_servers =
135                remove_lean_ctx_named_json_server(&target.config_path, "mcpServers", opts)?;
136            Ok(if servers.action == WriteAction::Updated {
137                servers
138            } else {
139                mcp_servers
140            })
141        }
142        // #2198: Muse Code accepts the canonical `mcpServers` or the legacy
143        // `mcp_servers` root and the writer keeps whichever the file uses, so
144        // removal visits both. Only the `lean-ctx` entry is removed; the file,
145        // `schema_version` and every other server stay.
146        ConfigType::MuseSettings => {
147            let canonical =
148                remove_lean_ctx_named_json_server(&target.config_path, MUSE_MCP_ROOT, opts)?;
149            let legacy =
150                remove_lean_ctx_named_json_server(&target.config_path, MUSE_LEGACY_MCP_ROOT, opts)?;
151            Ok(if canonical.action == WriteAction::Updated {
152                canonical
153            } else {
154                legacy
155            })
156        }
157    }
158}
159
160#[cfg(test)]
161mod tests;