gitstow 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,247 @@
1
+ """gitstow setup-ai — detect and configure AI tool integrations.
2
+
3
+ This is the AI-first onboarding path. gitstow's primary interface is through
4
+ AI tools (Claude Code, Claude Desktop, Cursor, etc.), so this setup should
5
+ be part of the default installation flow, not an afterthought.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import shutil
12
+ from pathlib import Path
13
+
14
+ import typer
15
+ from beaupy import confirm as bconfirm
16
+ from rich.console import Console
17
+ from rich.panel import Panel
18
+
19
+ console = Console()
20
+ err_console = Console(stderr=True)
21
+
22
+ # Known AI tool config locations
23
+ CLAUDE_CODE_DIR = Path.home() / ".claude"
24
+ CLAUDE_CODE_SKILLS = CLAUDE_CODE_DIR / "skills"
25
+
26
+ # Claude Desktop config paths (macOS, Linux, Windows)
27
+ CLAUDE_DESKTOP_CONFIGS = [
28
+ Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json", # macOS
29
+ Path.home() / ".config" / "claude" / "claude_desktop_config.json", # Linux
30
+ Path.home() / "AppData" / "Roaming" / "Claude" / "claude_desktop_config.json", # Windows
31
+ ]
32
+
33
+ # Cursor MCP config
34
+ CURSOR_CONFIG = Path.home() / ".cursor" / "mcp.json"
35
+
36
+ # Generic .mcp.json in home (some tools use this)
37
+ HOME_MCP_CONFIG = Path.home() / ".mcp.json"
38
+
39
+
40
+ def setup_ai(
41
+ auto: bool = typer.Option(
42
+ False, "--auto", "-a", help="Auto-configure everything without prompts.",
43
+ ),
44
+ quiet: bool = typer.Option(
45
+ False, "--quiet", "-q", help="Suppress output.",
46
+ ),
47
+ ) -> None:
48
+ """[bold]Set up AI integrations[/bold] — Claude Code skill, MCP server for Claude Desktop, Cursor, etc.
49
+
50
+ Detects which AI tools are installed and configures gitstow for each.
51
+ This is the recommended post-install step — gitstow is designed to be
52
+ used primarily through AI tools.
53
+
54
+ \b
55
+ Examples:
56
+ gitstow setup-ai # Interactive setup
57
+ gitstow setup-ai --auto # Auto-configure everything detected
58
+ """
59
+ if not quiet:
60
+ console.print(Panel(
61
+ "[bold]AI Integration Setup[/bold]\n\n"
62
+ "gitstow is designed to be used primarily through AI tools.\n"
63
+ "The Claude Code skill is the recommended integration (zero\n"
64
+ "context overhead, auto-updates). MCP is optional for tools\n"
65
+ "that don't support Claude Code skills.",
66
+ border_style="cyan",
67
+ padding=(1, 2),
68
+ ))
69
+ console.print()
70
+
71
+ detected = _detect_ai_tools()
72
+
73
+ if not detected and not quiet:
74
+ console.print(" [dim]No AI tools detected. You can still use gitstow from the terminal.[/dim]")
75
+ console.print(" [dim]Re-run this command after installing an AI tool.[/dim]\n")
76
+ return
77
+
78
+ if not quiet:
79
+ console.print(f" [bold]Detected {len(detected)} AI tool{'s' if len(detected) != 1 else ''}:[/bold]\n")
80
+ for tool in detected:
81
+ console.print(f" [green]✓[/green] {tool['name']}")
82
+ console.print()
83
+
84
+ # Always configure Claude Code skill first (primary integration)
85
+ claude_code = [t for t in detected if t["type"] == "claude_code"]
86
+ mcp_tools = [t for t in detected if t["type"] == "mcp_config"]
87
+
88
+ for tool in claude_code:
89
+ _setup_claude_code(auto=auto, quiet=quiet)
90
+
91
+ # Offer MCP for non-Claude-Code tools with clear context warning
92
+ if mcp_tools:
93
+ if not quiet:
94
+ console.print()
95
+ console.print(" [bold]MCP Server[/bold] (optional, for non-Claude-Code tools)")
96
+ console.print(" [dim]Warning: MCP tools are always loaded into context,")
97
+ console.print(" costing tokens even when you're not managing repos.[/dim]")
98
+ console.print(" [dim]The Claude Code skill has zero overhead when inactive.[/dim]")
99
+ console.print()
100
+ for tool in mcp_tools:
101
+ _setup_mcp_config(tool["path"], tool["name"], auto=auto, quiet=quiet)
102
+
103
+ if not quiet:
104
+ console.print(" [bold green]AI setup complete.[/bold green]\n")
105
+
106
+
107
+ def _detect_ai_tools() -> list[dict]:
108
+ """Detect which AI tools are installed on this machine."""
109
+ detected = []
110
+
111
+ # Claude Code
112
+ if CLAUDE_CODE_DIR.exists():
113
+ detected.append({
114
+ "name": "Claude Code",
115
+ "type": "claude_code",
116
+ "path": str(CLAUDE_CODE_DIR),
117
+ })
118
+
119
+ # Claude Desktop
120
+ for config_path in CLAUDE_DESKTOP_CONFIGS:
121
+ if config_path.parent.exists():
122
+ detected.append({
123
+ "name": "Claude Desktop",
124
+ "type": "mcp_config",
125
+ "path": str(config_path),
126
+ })
127
+ break # Only one Claude Desktop per machine
128
+
129
+ # Cursor
130
+ if CURSOR_CONFIG.parent.exists():
131
+ detected.append({
132
+ "name": "Cursor",
133
+ "type": "mcp_config",
134
+ "path": str(CURSOR_CONFIG),
135
+ })
136
+
137
+ return detected
138
+
139
+
140
+ def _setup_claude_code(auto: bool = False, quiet: bool = False) -> None:
141
+ """Install the Claude Code skill."""
142
+ from gitstow.cli.skill_cmd import _do_install_skill
143
+
144
+ skill_path = CLAUDE_CODE_SKILLS / "gitstow" / "SKILL.md"
145
+ already_installed = skill_path.exists()
146
+
147
+ if already_installed and not quiet:
148
+ console.print(" [dim]Claude Code skill already installed.[/dim]")
149
+
150
+ if not already_installed or auto:
151
+ if auto or bconfirm(" Install Claude Code skill?", default_is_yes=True):
152
+ _do_install_skill(quiet=quiet)
153
+ elif not quiet:
154
+ reinstall = bconfirm(" Reinstall Claude Code skill (update)?", default_is_yes=False)
155
+ if reinstall:
156
+ _do_install_skill(quiet=quiet)
157
+
158
+
159
+ def _setup_mcp_config(config_path: str, tool_name: str, auto: bool = False, quiet: bool = False) -> None:
160
+ """Add gitstow MCP server to an AI tool's config."""
161
+ path = Path(config_path)
162
+
163
+ # Check if gitstow-mcp is available
164
+ mcp_bin = shutil.which("gitstow-mcp")
165
+ if not mcp_bin:
166
+ if not quiet:
167
+ console.print(" [yellow]⚠[/yellow] gitstow-mcp not found on PATH.")
168
+ console.print(" Install with: [bold]pip install gitstow[mcp][/bold]")
169
+ return
170
+
171
+ # Check if already configured
172
+ already_configured = False
173
+ existing_config = {}
174
+ if path.exists():
175
+ try:
176
+ existing_config = json.loads(path.read_text())
177
+ if "mcpServers" in existing_config and "gitstow" in existing_config["mcpServers"]:
178
+ already_configured = True
179
+ except (json.JSONDecodeError, KeyError):
180
+ pass
181
+
182
+ if already_configured:
183
+ if not quiet:
184
+ console.print(f" [dim]{tool_name} MCP already configured.[/dim]")
185
+ return
186
+
187
+ # Offer to configure
188
+ if not auto:
189
+ if not quiet:
190
+ console.print(f"\n [bold]{tool_name}[/bold] — configure MCP server?")
191
+ console.print(f" Config: {config_path}")
192
+ console.print(f" Command: {mcp_bin}")
193
+ proceed = bconfirm(f" Add gitstow to {tool_name}?", default_is_yes=True)
194
+ if not proceed:
195
+ return
196
+
197
+ # Add to config
198
+ if "mcpServers" not in existing_config:
199
+ existing_config["mcpServers"] = {}
200
+
201
+ existing_config["mcpServers"]["gitstow"] = {
202
+ "command": mcp_bin,
203
+ }
204
+
205
+ path.parent.mkdir(parents=True, exist_ok=True)
206
+ path.write_text(json.dumps(existing_config, indent=2) + "\n")
207
+
208
+ if not quiet:
209
+ console.print(f" [green]✓[/green] {tool_name} MCP configured ({config_path})")
210
+
211
+
212
+ # Also make this callable from onboard.py
213
+ def _setup_ai_integrations(auto: bool = False, quiet: bool = False) -> None:
214
+ """Run AI setup as part of onboarding.
215
+
216
+ During onboarding, only the Claude Code skill is installed by default.
217
+ MCP setup is offered but explained as opt-in, since MCP tools are always
218
+ loaded into context and cost tokens even when not in use.
219
+ """
220
+ console.print(" [bold]5. AI Integration[/bold]\n")
221
+
222
+ detected = _detect_ai_tools()
223
+
224
+ if not detected:
225
+ console.print(" [dim]No AI tools detected. You can set up later with 'gitstow setup-ai'.[/dim]\n")
226
+ return
227
+
228
+ # Always install Claude Code skill (zero context cost when not in use)
229
+ claude_code = [t for t in detected if t["type"] == "claude_code"]
230
+ if claude_code:
231
+ _setup_claude_code(auto=True, quiet=quiet)
232
+
233
+ # Offer MCP setup but explain the tradeoff
234
+ mcp_tools = [t for t in detected if t["type"] == "mcp_config"]
235
+ if mcp_tools:
236
+ console.print()
237
+ console.print(" [bold]MCP server[/bold] (optional)")
238
+ console.print(" The MCP server lets Claude Desktop, Cursor, etc. manage repos.")
239
+ console.print(" [dim]Note: MCP tools are always loaded into context and use tokens")
240
+ console.print(" even when you're not managing repos. Best for dedicated setups.[/dim]")
241
+ console.print(" [dim]You can set this up later with: gitstow setup-ai[/dim]")
242
+ console.print()
243
+
244
+ for tool in mcp_tools:
245
+ _setup_mcp_config(tool["path"], tool["name"], auto=False, quiet=quiet)
246
+
247
+ console.print()
gitstow/cli/shell.py ADDED
@@ -0,0 +1,317 @@
1
+ """gitstow shell — shell integration helpers (fzf, cd, completions)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+
11
+ from gitstow.core.config import load_config
12
+ from gitstow.core.repo import RepoStore
13
+
14
+ shell_app = typer.Typer(
15
+ help="Shell integration — fzf picker, cd helper, shell functions.",
16
+ no_args_is_help=True,
17
+ )
18
+
19
+ console = Console()
20
+
21
+
22
+ @shell_app.command("pick")
23
+ def pick(
24
+ tag: str | None = typer.Option(None, "--tag", "-t", help="Filter by tag."),
25
+ owner: str | None = typer.Option(None, "--owner", help="Filter by owner."),
26
+ path_only: bool = typer.Option(True, "--path/--key", help="Output path (default) or key."),
27
+ ) -> None:
28
+ """Pick a repo interactively using fzf.
29
+
30
+ Outputs the selected repo's path (or key) to stdout.
31
+ Designed for shell integration:
32
+
33
+ \b
34
+ cd "$(gitstow shell pick)"
35
+ code "$(gitstow shell pick)"
36
+ """
37
+ import shutil
38
+ import subprocess
39
+
40
+ from gitstow.cli.helpers import iter_repos_with_workspace
41
+
42
+ settings = load_config()
43
+ store = RepoStore()
44
+
45
+ repo_ws_pairs = iter_repos_with_workspace(store, settings)
46
+ if tag:
47
+ repo_ws_pairs = [(r, ws) for r, ws in repo_ws_pairs if tag in r.tags]
48
+ if owner:
49
+ repo_ws_pairs = [(r, ws) for r, ws in repo_ws_pairs if r.owner == owner]
50
+
51
+ if not repo_ws_pairs:
52
+ sys.exit(1)
53
+
54
+ ws_map = {r.global_key: ws for r, ws in repo_ws_pairs}
55
+ repos = [r for r, _ in repo_ws_pairs]
56
+ multi_ws = len({r.workspace for r in repos}) > 1
57
+
58
+ # Check for fzf
59
+ if not shutil.which("fzf"):
60
+ try:
61
+ from beaupy import select as bselect
62
+ options = [f"[{r.workspace}] {r.key}" if multi_ws else r.key for r in repos]
63
+ choice = bselect(options, cursor=">>>", cursor_style="bold cyan")
64
+ if choice:
65
+ idx = options.index(choice)
66
+ r = repos[idx]
67
+ ws = ws_map[r.global_key]
68
+ print(str(r.get_path(ws.get_path())) if path_only else r.key)
69
+ else:
70
+ sys.exit(1)
71
+ except ImportError:
72
+ for r in repos:
73
+ print(r.key)
74
+ sys.exit(1)
75
+ return
76
+
77
+ # Build fzf input
78
+ lines = []
79
+ for r in repos:
80
+ prefix = f"[{r.workspace}] " if multi_ws else ""
81
+ frozen = " ❄" if r.frozen else ""
82
+ tags_str = f" [{', '.join(r.tags)}]" if r.tags else ""
83
+ lines.append(f"{prefix}{r.key}{frozen}{tags_str}")
84
+
85
+ fzf_input = "\n".join(lines)
86
+ try:
87
+ result = subprocess.run(
88
+ ["fzf", "--reverse", "--height=40%", "--prompt=repo> "],
89
+ input=fzf_input,
90
+ capture_output=True,
91
+ text=True,
92
+ )
93
+ if result.returncode != 0 or not result.stdout.strip():
94
+ sys.exit(1)
95
+
96
+ selected_line = result.stdout.strip()
97
+ # Find the matching repo by comparing fzf lines
98
+ idx = lines.index(selected_line) if selected_line in lines else -1
99
+ if idx >= 0:
100
+ r = repos[idx]
101
+ ws = ws_map[r.global_key]
102
+ print(str(r.get_path(ws.get_path())) if path_only else r.key)
103
+ else:
104
+ sys.exit(1)
105
+ except (FileNotFoundError, ValueError):
106
+ sys.exit(1)
107
+
108
+
109
+ @shell_app.command("init")
110
+ def shell_init(
111
+ shell_type: str = typer.Argument(
112
+ default="auto",
113
+ help="Shell type: bash, zsh, fish, or auto.",
114
+ ),
115
+ ) -> None:
116
+ """Print shell functions to source in your shell rc file.
117
+
118
+ \b
119
+ Usage:
120
+ # Add to your ~/.zshrc or ~/.bashrc:
121
+ eval "$(gitstow shell init)"
122
+
123
+ # Or for fish (~/.config/fish/config.fish):
124
+ gitstow shell init fish | source
125
+ """
126
+ import os
127
+
128
+ if shell_type == "auto":
129
+ shell_env = os.environ.get("SHELL", "")
130
+ if "zsh" in shell_env:
131
+ shell_type = "zsh"
132
+ elif "fish" in shell_env:
133
+ shell_type = "fish"
134
+ else:
135
+ shell_type = "bash"
136
+
137
+ if shell_type == "fish":
138
+ print(_FISH_FUNCTIONS)
139
+ else:
140
+ print(_BASH_ZSH_FUNCTIONS)
141
+
142
+
143
+ @shell_app.command("completions")
144
+ def shell_completions(
145
+ shell_type: str = typer.Argument(
146
+ default="auto",
147
+ help="Shell type: bash, zsh, fish, or auto.",
148
+ ),
149
+ ) -> None:
150
+ """Print shell completion script for repo names, workspaces, and tags.
151
+
152
+ \b
153
+ Usage:
154
+ # Add to your ~/.zshrc or ~/.bashrc (after shell init):
155
+ eval "$(gitstow shell completions)"
156
+ """
157
+ import os
158
+
159
+ if shell_type == "auto":
160
+ shell_env = os.environ.get("SHELL", "")
161
+ if "zsh" in shell_env:
162
+ shell_type = "zsh"
163
+ elif "fish" in shell_env:
164
+ shell_type = "fish"
165
+ else:
166
+ shell_type = "bash"
167
+
168
+ if shell_type == "zsh":
169
+ print(_ZSH_COMPLETIONS)
170
+ elif shell_type == "fish":
171
+ print(_FISH_COMPLETIONS)
172
+ else:
173
+ print(_BASH_COMPLETIONS)
174
+
175
+
176
+ @shell_app.command("setup")
177
+ def shell_setup() -> None:
178
+ """Show instructions for setting up shell integration."""
179
+ console.print(Panel(
180
+ "[bold]Shell Integration Setup[/bold]\n\n"
181
+ "Add one of these to your shell config file:\n\n"
182
+ "[bold cyan]Bash/Zsh[/bold cyan] (~/.bashrc or ~/.zshrc):\n"
183
+ ' [green]eval "$(gitstow shell init)"[/green]\n\n'
184
+ "[bold cyan]Fish[/bold cyan] (~/.config/fish/config.fish):\n"
185
+ " [green]gitstow shell init fish | source[/green]\n\n"
186
+ "This gives you:\n"
187
+ " [cyan]gs[/cyan] — cd into a repo (fzf picker)\n"
188
+ " [cyan]gse[/cyan] — open a repo in your editor (fzf picker)\n"
189
+ " [cyan]gsp[/cyan] — gitstow pull (shorthand)\n"
190
+ " [cyan]gss[/cyan] — gitstow status (shorthand)\n"
191
+ " [cyan]gsl[/cyan] — gitstow list (shorthand)\n\n"
192
+ "[bold cyan]Tab Completion[/bold cyan] (optional):\n"
193
+ ' [green]eval "$(gitstow shell completions)"[/green]\n\n'
194
+ "Completes repo names, workspace labels, and tags.",
195
+ border_style="cyan",
196
+ padding=(1, 2),
197
+ ))
198
+
199
+
200
+ # Shell function templates
201
+ _BASH_ZSH_FUNCTIONS = r'''# gitstow shell integration
202
+ # cd into a repo via fzf picker
203
+ gs() {
204
+ local dir
205
+ dir="$(gitstow shell pick 2>/dev/null)"
206
+ if [ -n "$dir" ]; then
207
+ cd "$dir" || return
208
+ fi
209
+ }
210
+
211
+ # Open a repo in editor via fzf picker
212
+ gse() {
213
+ local dir
214
+ dir="$(gitstow shell pick 2>/dev/null)"
215
+ if [ -n "$dir" ]; then
216
+ ${EDITOR:-code} "$dir"
217
+ fi
218
+ }
219
+
220
+ # Shortcuts
221
+ alias gsp="gitstow pull"
222
+ alias gss="gitstow status"
223
+ alias gsl="gitstow list"
224
+ alias gsa="gitstow add"
225
+ '''
226
+
227
+ _FISH_FUNCTIONS = r'''# gitstow shell integration for fish
228
+ function gs --description "cd into a gitstow repo via fzf"
229
+ set -l dir (gitstow shell pick 2>/dev/null)
230
+ if test -n "$dir"
231
+ cd "$dir"
232
+ end
233
+ end
234
+
235
+ function gse --description "Open a gitstow repo in editor via fzf"
236
+ set -l dir (gitstow shell pick 2>/dev/null)
237
+ if test -n "$dir"
238
+ eval $EDITOR "$dir"
239
+ end
240
+ end
241
+
242
+ alias gsp "gitstow pull"
243
+ alias gss "gitstow status"
244
+ alias gsl "gitstow list"
245
+ alias gsa "gitstow add"
246
+ '''
247
+
248
+ # Completion templates
249
+ _BASH_COMPLETIONS = r'''# gitstow bash completions
250
+ _gitstow_repos() {
251
+ COMPREPLY=($(compgen -W "$(gitstow list --quiet 2>/dev/null)" -- "${COMP_WORDS[COMP_CWORD]}"))
252
+ }
253
+ _gitstow_workspaces() {
254
+ COMPREPLY=($(compgen -W "$(gitstow workspace list --quiet 2>/dev/null)" -- "${COMP_WORDS[COMP_CWORD]}"))
255
+ }
256
+ _gitstow_tags() {
257
+ COMPREPLY=($(compgen -W "$(gitstow repo tags --quiet 2>/dev/null)" -- "${COMP_WORDS[COMP_CWORD]}"))
258
+ }
259
+ _gitstow_complete() {
260
+ local cur prev cmd
261
+ cur="${COMP_WORDS[COMP_CWORD]}"
262
+ prev="${COMP_WORDS[COMP_CWORD-1]}"
263
+ if [[ "$prev" == "-w" || "$prev" == "--workspace" ]]; then
264
+ _gitstow_workspaces; return
265
+ fi
266
+ if [[ "$prev" == "-t" || "$prev" == "--tag" ]]; then
267
+ _gitstow_tags; return
268
+ fi
269
+ case "${COMP_WORDS[1]}" in
270
+ remove|open|exec) _gitstow_repos ;;
271
+ repo)
272
+ case "${COMP_WORDS[2]}" in
273
+ freeze|unfreeze|tag|untag|info) _gitstow_repos ;;
274
+ esac ;;
275
+ esac
276
+ }
277
+ complete -F _gitstow_complete gitstow
278
+ '''
279
+
280
+ _ZSH_COMPLETIONS = r'''# gitstow zsh completions
281
+ _gitstow_repos() {
282
+ local repos=(${(f)"$(gitstow list --quiet 2>/dev/null)"})
283
+ compadd -a repos
284
+ }
285
+ _gitstow_workspaces() {
286
+ local workspaces=(${(f)"$(gitstow workspace list --quiet 2>/dev/null)"})
287
+ compadd -a workspaces
288
+ }
289
+ _gitstow_tags() {
290
+ local tags=(${(f)"$(gitstow repo tags --quiet 2>/dev/null)"})
291
+ compadd -a tags
292
+ }
293
+ _gitstow() {
294
+ local cur="${words[CURRENT]}" prev="${words[CURRENT-1]}"
295
+ if [[ "$prev" == "-w" || "$prev" == "--workspace" ]]; then
296
+ _gitstow_workspaces; return
297
+ fi
298
+ if [[ "$prev" == "-t" || "$prev" == "--tag" ]]; then
299
+ _gitstow_tags; return
300
+ fi
301
+ case "${words[2]}" in
302
+ remove|open|exec) _gitstow_repos ;;
303
+ repo)
304
+ case "${words[3]}" in
305
+ freeze|unfreeze|tag|untag|info) _gitstow_repos ;;
306
+ esac ;;
307
+ esac
308
+ }
309
+ compdef _gitstow gitstow
310
+ '''
311
+
312
+ _FISH_COMPLETIONS = r'''# gitstow fish completions
313
+ complete -c gitstow -n '__fish_seen_subcommand_from remove open' -xa '(gitstow list --quiet 2>/dev/null)'
314
+ complete -c gitstow -n '__fish_seen_subcommand_from repo; and __fish_seen_subcommand_from freeze unfreeze tag untag info' -xa '(gitstow list --quiet 2>/dev/null)'
315
+ complete -c gitstow -s w -l workspace -xa '(gitstow workspace list --quiet 2>/dev/null)'
316
+ complete -c gitstow -s t -l tag -xa '(gitstow repo tags --quiet 2>/dev/null)'
317
+ '''
@@ -0,0 +1,63 @@
1
+ """gitstow install-skill — install the Claude Code skill."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ from gitstow.core.paths import SKILL_TARGET, CLAUDE_SKILLS_DIR, get_skill_source_dir
12
+
13
+ console = Console()
14
+
15
+
16
+ def _do_install_skill(quiet: bool = False) -> bool:
17
+ """Install the skill. Returns True on success."""
18
+ source = get_skill_source_dir()
19
+
20
+ # Ensure skills directory exists
21
+ CLAUDE_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
22
+
23
+ # Copy skill files
24
+ if SKILL_TARGET.exists():
25
+ shutil.rmtree(SKILL_TARGET)
26
+
27
+ # Copy the skill directory
28
+ SKILL_TARGET.mkdir(parents=True, exist_ok=True)
29
+
30
+ # Copy SKILL.md
31
+ source_skill = Path(str(source)) / "SKILL.md"
32
+ if source_skill.exists():
33
+ shutil.copy2(source_skill, SKILL_TARGET / "SKILL.md")
34
+ else:
35
+ # Fallback: try importlib.resources traversable
36
+ try:
37
+ skill_content = (source / "SKILL.md").read_text()
38
+ (SKILL_TARGET / "SKILL.md").write_text(skill_content)
39
+ except Exception:
40
+ if not quiet:
41
+ console.print(" [red]✗[/red] Could not find bundled SKILL.md")
42
+ return False
43
+
44
+ # Write version marker for auto-update detection
45
+ from gitstow import __version__
46
+ (SKILL_TARGET / ".version").write_text(__version__)
47
+
48
+ if not quiet:
49
+ console.print(f" [green]✓[/green] Skill installed to {SKILL_TARGET}")
50
+ return True
51
+
52
+
53
+ def install_skill() -> None:
54
+ """[bold]Install[/bold] the Claude Code skill for AI-assisted repo management.
55
+
56
+ Copies the gitstow skill to ~/.claude/skills/gitstow/ so Claude Code
57
+ can manage your repos conversationally.
58
+ """
59
+ success = _do_install_skill(quiet=False)
60
+ if not success:
61
+ raise typer.Exit(code=1)
62
+ console.print("\n You can now use gitstow from Claude Code!")
63
+ console.print(" Try saying: [cyan]\"add this repo\"[/cyan] or [cyan]\"update my repos\"[/cyan]\n")