Skip to main content

git_worktree_manager/operations/setup_claude/
mod.rs

1//! Plugin installer for Claude Code integration.
2//!
3//! Installs gw as a Claude Code *plugin* at `~/.claude/plugins/gw/` with
4//! two skills (`delegate`, `manage`). Removes legacy single-skill installs
5//! at `~/.claude/skills/gw/` and `~/.claude/skills/gw-delegate/`.
6
7use std::path::{Path, PathBuf};
8
9use console::style;
10
11use crate::constants::home_dir_or_fallback;
12use crate::error::Result;
13
14mod legacy;
15mod manifest;
16mod skill_delegate;
17mod skill_manage;
18
19const PLUGIN_NAME: &str = "gw";
20
21fn plugin_dir_under(home: &Path) -> PathBuf {
22    home.join(".claude").join("plugins").join(PLUGIN_NAME)
23}
24
25fn manifest_path_under(home: &Path) -> PathBuf {
26    plugin_dir_under(home).join("plugin.json")
27}
28fn delegate_skill_path_under(home: &Path) -> PathBuf {
29    plugin_dir_under(home)
30        .join("skills")
31        .join("delegate")
32        .join("SKILL.md")
33}
34fn manage_skill_path_under(home: &Path) -> PathBuf {
35    plugin_dir_under(home)
36        .join("skills")
37        .join("manage")
38        .join("SKILL.md")
39}
40fn manage_reference_path_under(home: &Path) -> PathBuf {
41    plugin_dir_under(home)
42        .join("skills")
43        .join("manage")
44        .join("references")
45        .join("gw-commands.md")
46}
47
48/// True if the plugin manifest exists at the canonical path.
49pub fn is_plugin_installed() -> bool {
50    manifest_path_under(&home_dir_or_fallback()).exists()
51}
52
53#[doc(hidden)]
54pub fn manage_skill_content_for_test() -> &'static str {
55    skill_manage::content()
56}
57
58#[doc(hidden)]
59pub fn manage_reference_content_for_test() -> &'static str {
60    skill_manage::reference_content()
61}
62
63/// Backward-compatible alias used by `gw doctor`. Returns true if either the
64/// new plugin OR a legacy skill install is present.
65pub fn is_skill_installed() -> bool {
66    is_plugin_installed() || legacy::any_legacy_present()
67}
68
69fn write_if_changed(
70    path: &PathBuf,
71    new_content: &str,
72) -> std::result::Result<bool, std::io::Error> {
73    if path.exists() {
74        let existing = std::fs::read_to_string(path).unwrap_or_default();
75        if existing == new_content {
76            return Ok(false);
77        }
78    }
79    if let Some(parent) = path.parent() {
80        std::fs::create_dir_all(parent)?;
81    }
82    std::fs::write(path, new_content)?;
83    Ok(true)
84}
85
86pub fn setup_claude() -> Result<()> {
87    setup_claude_under(&home_dir_or_fallback())
88}
89
90/// Test-friendly variant: install into an arbitrary `home` root. Avoids
91/// reliance on platform env-var lookup (e.g. dirs::home_dir reads
92/// USERPROFILE on Windows via SHGetKnownFolderPath, which ignores
93/// process-set env vars).
94pub fn setup_claude_under(home: &Path) -> Result<()> {
95    legacy::remove_legacy_installs_under(home);
96
97    let manifest = manifest_path_under(home);
98    let delegate = delegate_skill_path_under(home);
99    let manage = manage_skill_path_under(home);
100    let reference = manage_reference_path_under(home);
101
102    let mut any_changed = false;
103    any_changed |= write_if_changed(&manifest, manifest::content())?;
104    any_changed |= write_if_changed(&delegate, skill_delegate::content())?;
105    any_changed |= write_if_changed(&manage, skill_manage::content())?;
106    any_changed |= write_if_changed(&reference, skill_manage::reference_content())?;
107
108    let location = plugin_dir_under(home);
109    if !any_changed {
110        println!("{} gw plugin already up to date.\n", style("*").green());
111        println!("  Location: {}", style(location.display()).dim());
112        return Ok(());
113    }
114
115    println!(
116        "{} gw plugin installed at {}.\n",
117        style("*").green().bold(),
118        style(location.display()).dim()
119    );
120    println!(
121        "  Use {} in Claude Code to delegate tasks to worktrees.",
122        style("/gw").cyan()
123    );
124    println!(
125        "  The bundled '{}' skill will recommend hooks (e.g. SessionStart sanity)",
126        style("manage").cyan()
127    );
128    println!("  in-session when relevant. It edits your project's .claude/settings.json");
129    println!("  on your consent — gw itself never modifies any settings file.\n");
130
131    Ok(())
132}