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.
- gitstow/__init__.py +3 -0
- gitstow/__main__.py +11 -0
- gitstow/cli/__init__.py +1 -0
- gitstow/cli/add.py +262 -0
- gitstow/cli/config_cmd.py +235 -0
- gitstow/cli/doctor.py +163 -0
- gitstow/cli/exec_cmd.py +159 -0
- gitstow/cli/export_cmd.py +293 -0
- gitstow/cli/helpers.py +113 -0
- gitstow/cli/list_cmd.py +213 -0
- gitstow/cli/main.py +129 -0
- gitstow/cli/manage.py +283 -0
- gitstow/cli/migrate.py +142 -0
- gitstow/cli/onboard.py +234 -0
- gitstow/cli/open_cmd.py +144 -0
- gitstow/cli/pull.py +265 -0
- gitstow/cli/remove.py +88 -0
- gitstow/cli/search.py +199 -0
- gitstow/cli/setup_ai.py +247 -0
- gitstow/cli/shell.py +317 -0
- gitstow/cli/skill_cmd.py +63 -0
- gitstow/cli/stats.py +126 -0
- gitstow/cli/status.py +213 -0
- gitstow/cli/tui.py +27 -0
- gitstow/cli/workspace_cmd.py +202 -0
- gitstow/core/__init__.py +1 -0
- gitstow/core/config.py +130 -0
- gitstow/core/discovery.py +116 -0
- gitstow/core/git.py +261 -0
- gitstow/core/parallel.py +85 -0
- gitstow/core/paths.py +51 -0
- gitstow/core/repo.py +300 -0
- gitstow/core/url_parser.py +159 -0
- gitstow/mcp/__init__.py +1 -0
- gitstow/mcp/server.py +704 -0
- gitstow/skill/SKILL.md +210 -0
- gitstow/tui/__init__.py +1 -0
- gitstow/tui/app.py +384 -0
- gitstow-0.1.0.dist-info/METADATA +306 -0
- gitstow-0.1.0.dist-info/RECORD +43 -0
- gitstow-0.1.0.dist-info/WHEEL +4 -0
- gitstow-0.1.0.dist-info/entry_points.txt +3 -0
- gitstow-0.1.0.dist-info/licenses/LICENSE +21 -0
gitstow/cli/setup_ai.py
ADDED
|
@@ -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
|
+
'''
|
gitstow/cli/skill_cmd.py
ADDED
|
@@ -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")
|