git_worktree_manager/operations/setup_claude/
mod.rs1use std::path::Path;
10
11use console::style;
12
13use crate::error::{CwError, Result};
14use crate::git;
15use crate::operations::sync_claude;
16
17mod skill_delegate;
18mod skill_manage;
19
20#[doc(hidden)]
21pub fn manage_skill_content_for_test() -> &'static str {
22 skill_manage::content()
23}
24
25#[doc(hidden)]
26pub fn manage_reference_content_for_test() -> &'static str {
27 skill_manage::reference_content()
28}
29
30#[doc(hidden)]
31pub fn delegate_skill_content_for_test() -> &'static str {
32 skill_delegate::content()
33}
34
35pub fn setup_claude() -> Result<()> {
37 let repo_root = git::get_repo_root(None).map_err(|_| {
38 CwError::Other(
39 "setup-claude: not inside a git repository. Run from within a git repo.".to_string(),
40 )
41 })?;
42
43 let mut wrote_any = false;
44 wrote_any |= write_skill_files(&repo_root)?;
45 wrote_any |= sync_hooks(&repo_root)?;
46
47 print_outcome(&repo_root, wrote_any);
48 Ok(())
49}
50
51fn write_skill_files(repo_root: &Path) -> Result<bool> {
54 let mut changed = false;
55 let triples = [
56 (
57 ".claude/skills/gw-delegate/SKILL.md",
58 skill_delegate::content(),
59 ),
60 (".claude/skills/gw-manage/SKILL.md", skill_manage::content()),
61 (
62 ".claude/skills/gw-manage/references/gw-commands.md",
63 skill_manage::reference_content(),
64 ),
65 ];
66 for (rel, content) in triples {
67 let path = repo_root.join(rel);
68 if let Some(parent) = path.parent() {
69 std::fs::create_dir_all(parent).map_err(|e| {
70 CwError::Other(format!(
71 "setup-claude: failed to create {}: {}",
72 parent.display(),
73 e
74 ))
75 })?;
76 }
77 let current = std::fs::read_to_string(&path).ok();
78 if current.as_deref() != Some(content) {
79 std::fs::write(&path, content).map_err(|e| {
80 CwError::Other(format!(
81 "setup-claude: failed to write {}: {}",
82 path.display(),
83 e
84 ))
85 })?;
86 changed = true;
87 }
88 }
89 Ok(changed)
90}
91
92fn sync_hooks(repo_root: &Path) -> Result<bool> {
95 let claude_dir = repo_root.join(".claude");
96 let settings_path = claude_dir.join("settings.json");
97 std::fs::create_dir_all(&claude_dir).map_err(|e| {
98 CwError::Other(format!(
99 "setup-claude: failed to create {}: {}",
100 claude_dir.display(),
101 e
102 ))
103 })?;
104
105 let mut settings: serde_json::Value = if settings_path.exists() {
106 let raw = std::fs::read_to_string(&settings_path).map_err(|e| {
107 CwError::Other(format!(
108 "setup-claude: failed to read {}: {}",
109 settings_path.display(),
110 e
111 ))
112 })?;
113 serde_json::from_str(&raw).map_err(|e| {
114 CwError::Other(format!(
115 "setup-claude: malformed JSON in {}: {}. Fix the file manually before re-running.",
116 settings_path.display(),
117 e
118 ))
119 })?
120 } else {
121 serde_json::Value::Object(serde_json::Map::new())
122 };
123
124 let changed = sync_claude::merge_hooks_into(&mut settings)?;
125 if changed {
126 let mut out = serde_json::to_string_pretty(&settings).map_err(|e| {
127 CwError::Other(format!("setup-claude: failed to serialize settings: {}", e))
128 })?;
129 out.push('\n');
130 std::fs::write(&settings_path, &out).map_err(|e| {
131 CwError::Other(format!(
132 "setup-claude: failed to write {}: {}",
133 settings_path.display(),
134 e
135 ))
136 })?;
137 }
138 Ok(changed)
139}
140
141pub fn is_installed_in_repo(repo_root: &Path) -> bool {
144 repo_root
145 .join(".claude/skills/gw-delegate/SKILL.md")
146 .exists()
147 && repo_root.join(".claude/skills/gw-manage/SKILL.md").exists()
148 && repo_root
149 .join(".claude/skills/gw-manage/references/gw-commands.md")
150 .exists()
151}
152
153pub fn is_installed() -> bool {
155 if let Ok(root) = git::get_repo_root(None) {
156 is_installed_in_repo(&root)
157 } else {
158 false
159 }
160}
161
162fn print_outcome(repo_root: &Path, wrote_any: bool) {
163 let location = repo_root.join(".claude");
164 if wrote_any {
165 println!(
166 "{} Claude Code integration installed at {}.",
167 style("*").green().bold(),
168 style(location.display()).dim()
169 );
170 } else {
171 println!(
172 "{} Claude Code integration already up to date.",
173 style("*").green()
174 );
175 }
176 println!(
177 " Skills: {} {}",
178 style("gw-delegate").cyan(),
179 style("gw-manage").cyan()
180 );
181 println!(" Hooks: PreToolUse(Bash), WorktreeCreate, WorktreeRemove");
182 println!(" Re-run `gw setup-claude` after upgrading gw to refresh skills/hooks.");
183}