Skip to content

Commit 2a29589

Browse files
javoireclaude
andauthored
feat(skill): support Codex and Cursor in stack skill install (#68)
* feat(skill): support Codex and Cursor in `stack skill install` `stack skill install` now auto-detects which AI coding tools are installed (Claude Code, Codex, Cursor) and installs the skill for all of them. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(skill): keep manual install section in README Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 392f2b6 commit 2a29589

5 files changed

Lines changed: 198 additions & 32 deletions

File tree

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,19 +105,19 @@ Or use the interactive helper:
105105
stack config set
106106
```
107107

108-
## Claude Code Skill
108+
## AI Coding Tools
109109

110-
Stackinator includes a [Claude Code](https://claude.ai/code) skill that teaches Claude how to manage stacked branches.
111-
112-
### Quick install
110+
Stackinator includes a skill that teaches AI coding tools how to manage stacked branches. Supported tools: [Claude Code](https://claude.ai/code), [Codex](https://openai.com/index/introducing-codex/), and [Cursor](https://cursor.com/).
113111

114112
```bash
115113
stack skill install
116114
```
117115

116+
This auto-detects which tools are installed and installs the skill for all of them.
117+
118118
### Manual install
119119

120-
In Claude Code, run:
120+
For Claude Code, run:
121121

122122
```
123123
/plugin marketplace add javoire/stackinator

cmd/skill.go

Lines changed: 124 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,36 @@ import (
44
"fmt"
55
"os"
66
"os/exec"
7+
"path/filepath"
8+
"strings"
79

810
"github.com/javoire/stackinator/internal/ui"
11+
skillcontent "github.com/javoire/stackinator/plugins/stack/skills/stack"
912
"github.com/spf13/cobra"
1013
)
1114

15+
type aiTool struct {
16+
Name string
17+
Detect func() bool
18+
Install func() error
19+
}
20+
1221
var skillCmd = &cobra.Command{
1322
Use: "skill",
14-
Short: "Manage Claude Code skills",
15-
Long: `Manage Claude Code skills for the stack CLI.`,
23+
Short: "Manage AI coding tool skills",
24+
Long: `Manage AI coding tool skills for the stack CLI.`,
1625
Annotations: map[string]string{
1726
"skipGitValidation": "true",
1827
},
1928
}
2029

2130
var skillInstallCmd = &cobra.Command{
2231
Use: "install",
23-
Short: "Install the stack skill for Claude Code",
24-
Long: `Install the stack skill so Claude Code knows how to use the stack CLI.
32+
Short: "Install the stack skill for AI coding tools",
33+
Long: `Install the stack skill so AI coding tools know how to use the stack CLI.
2534
26-
This requires the Claude Code CLI (claude) to be installed.
27-
See https://claude.ai/code for installation instructions.`,
35+
Supported tools: Claude Code, Codex, Cursor.
36+
Automatically detects which tools are installed and installs for all of them.`,
2837
Annotations: map[string]string{
2938
"skipGitValidation": "true",
3039
},
@@ -38,11 +47,48 @@ func init() {
3847
rootCmd.AddCommand(skillCmd)
3948
}
4049

41-
func runSkillInstall() error {
42-
if _, err := exec.LookPath("claude"); err != nil {
43-
return fmt.Errorf("claude CLI not found in PATH. Install it from https://claude.ai/code")
50+
func getAITools() []aiTool {
51+
return []aiTool{
52+
{Name: "Claude Code", Detect: detectClaude, Install: installClaude},
53+
{Name: "Codex", Detect: detectCodex, Install: installCodex},
54+
{Name: "Cursor", Detect: detectCursor, Install: installCursor},
55+
}
56+
}
57+
58+
func detectClaude() bool {
59+
_, err := exec.LookPath("claude")
60+
return err == nil
61+
}
62+
63+
func detectCodex() bool {
64+
_, err := exec.LookPath("codex")
65+
return err == nil
66+
}
67+
68+
func detectCursor() bool {
69+
if _, err := exec.LookPath("cursor"); err == nil {
70+
return true
71+
}
72+
home, err := os.UserHomeDir()
73+
if err != nil {
74+
return false
4475
}
76+
_, err = os.Stat(filepath.Join(home, ".cursor"))
77+
return err == nil
78+
}
79+
80+
// skillBody returns the SKILL.md content with YAML frontmatter stripped.
81+
func skillBody() string {
82+
content := skillcontent.SkillMD
83+
if strings.HasPrefix(content, "---") {
84+
if idx := strings.Index(content[3:], "---"); idx != -1 {
85+
content = strings.TrimLeft(content[3+idx+3:], "\n")
86+
}
87+
}
88+
return content
89+
}
4590

91+
func installClaude() error {
4692
fmt.Println("Adding stackinator marketplace...")
4793
addCmd := exec.Command("claude", "plugin", "marketplace", "add", "javoire/stackinator")
4894
addCmd.Stdout = os.Stdout
@@ -58,7 +104,75 @@ func runSkillInstall() error {
58104
if err := installCmd.Run(); err != nil {
59105
return fmt.Errorf("failed to install skill: %w", err)
60106
}
107+
return nil
108+
}
109+
110+
func installCodex() error {
111+
home, err := os.UserHomeDir()
112+
if err != nil {
113+
return fmt.Errorf("failed to get home directory: %w", err)
114+
}
115+
116+
dir := filepath.Join(home, ".agents", "skills", "stack")
117+
if err := os.MkdirAll(dir, 0755); err != nil {
118+
return fmt.Errorf("failed to create directory %s: %w", dir, err)
119+
}
120+
121+
dest := filepath.Join(dir, "SKILL.md")
122+
if err := os.WriteFile(dest, []byte(skillcontent.SkillMD), 0644); err != nil {
123+
return fmt.Errorf("failed to write %s: %w", dest, err)
124+
}
125+
126+
fmt.Printf("Wrote %s\n", dest)
127+
return nil
128+
}
129+
130+
func installCursor() error {
131+
home, err := os.UserHomeDir()
132+
if err != nil {
133+
return fmt.Errorf("failed to get home directory: %w", err)
134+
}
135+
136+
dir := filepath.Join(home, ".cursor", "rules")
137+
if err := os.MkdirAll(dir, 0755); err != nil {
138+
return fmt.Errorf("failed to create directory %s: %w", dir, err)
139+
}
140+
141+
body := skillBody()
142+
mdc := "---\ndescription: Manage stacked branches with the stack CLI. Covers branch creation, navigation, syncing, and PR management.\nalwaysApply: true\n---\n\n" + body
143+
144+
dest := filepath.Join(dir, "stack.mdc")
145+
if err := os.WriteFile(dest, []byte(mdc), 0644); err != nil {
146+
return fmt.Errorf("failed to write %s: %w", dest, err)
147+
}
148+
149+
fmt.Printf("Wrote %s\n", dest)
150+
return nil
151+
}
152+
153+
func runSkillInstall() error {
154+
tools := getAITools()
155+
156+
var detected []aiTool
157+
for _, t := range tools {
158+
if t.Detect() {
159+
detected = append(detected, t)
160+
}
161+
}
162+
163+
if len(detected) == 0 {
164+
return fmt.Errorf("no supported AI coding tools found. Supported tools: Claude Code (claude), Codex (codex), Cursor (cursor or ~/.cursor/)")
165+
}
166+
167+
var installed []string
168+
for _, t := range detected {
169+
fmt.Printf("Installing for %s...\n", t.Name)
170+
if err := t.Install(); err != nil {
171+
return fmt.Errorf("failed to install for %s: %w", t.Name, err)
172+
}
173+
installed = append(installed, t.Name)
174+
}
61175

62-
fmt.Println(ui.Success("Stack skill installed! Claude Code now knows how to use the stack CLI."))
176+
fmt.Println(ui.Success(fmt.Sprintf("Stack skill installed for: %s", strings.Join(installed, ", "))))
63177
return nil
64178
}

cmd/skill_test.go

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,73 @@ package cmd
22

33
import (
44
"os"
5-
"os/exec"
5+
"path/filepath"
6+
"strings"
67
"testing"
78

89
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
911
)
1012

11-
func TestRunSkillInstall_ClaudeNotFound(t *testing.T) {
12-
// Set PATH to empty so claude can't be found
13-
originalPath := os.Getenv("PATH")
13+
func TestRunSkillInstall_NoToolsFound(t *testing.T) {
1414
t.Setenv("PATH", "")
15-
defer func() { os.Setenv("PATH", originalPath) }()
15+
t.Setenv("HOME", t.TempDir())
1616

17-
// Clear the exec.LookPath cache by ensuring fresh lookup
1817
err := runSkillInstall()
1918
assert.Error(t, err)
20-
assert.Contains(t, err.Error(), "claude CLI not found")
19+
assert.Contains(t, err.Error(), "no supported AI coding tools found")
2120
}
2221

23-
func TestRunSkillInstall_ClaudeFound(t *testing.T) {
24-
// Skip if claude is not installed
25-
if _, err := exec.LookPath("claude"); err != nil {
26-
t.Skip("claude CLI not installed, skipping integration test")
27-
}
22+
func TestInstallCodex(t *testing.T) {
23+
home := t.TempDir()
24+
t.Setenv("HOME", home)
2825

29-
// This would actually run the commands, so we just verify claude is found
30-
// A full integration test would require mocking exec.Command
31-
t.Log("claude CLI found in PATH")
26+
err := installCodex()
27+
require.NoError(t, err)
28+
29+
dest := filepath.Join(home, ".agents", "skills", "stack", "SKILL.md")
30+
content, err := os.ReadFile(dest)
31+
require.NoError(t, err)
32+
assert.Contains(t, string(content), "name: stack")
33+
assert.Contains(t, string(content), "## Common Commands")
34+
}
35+
36+
func TestInstallCursor(t *testing.T) {
37+
home := t.TempDir()
38+
t.Setenv("HOME", home)
39+
40+
err := installCursor()
41+
require.NoError(t, err)
42+
43+
dest := filepath.Join(home, ".cursor", "rules", "stack.mdc")
44+
content, err := os.ReadFile(dest)
45+
require.NoError(t, err)
46+
assert.Contains(t, string(content), "alwaysApply: true")
47+
assert.Contains(t, string(content), "description:")
48+
assert.Contains(t, string(content), "## Common Commands")
49+
// Should not contain SKILL.md frontmatter
50+
assert.NotContains(t, string(content), "name: stack")
51+
}
52+
53+
func TestSkillBody(t *testing.T) {
54+
body := skillBody()
55+
assert.False(t, strings.HasPrefix(body, "---"))
56+
assert.Contains(t, body, "## Common Commands")
57+
}
58+
59+
func TestDetectCursor_WithDir(t *testing.T) {
60+
home := t.TempDir()
61+
t.Setenv("HOME", home)
62+
t.Setenv("PATH", "")
63+
64+
require.NoError(t, os.MkdirAll(filepath.Join(home, ".cursor"), 0755))
65+
assert.True(t, detectCursor())
66+
}
67+
68+
func TestDetectCursor_NothingFound(t *testing.T) {
69+
home := t.TempDir()
70+
t.Setenv("HOME", home)
71+
t.Setenv("PATH", "")
72+
73+
assert.False(t, detectCursor())
3274
}

docs/commands.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,13 @@ Flags:
156156

157157
## `stack skill install`
158158

159-
Install the Claude Code skill so Claude knows how to use the stack CLI.
159+
Install the stack skill for AI coding tools so they know how to use the stack CLI.
160160

161-
Requires the [Claude Code CLI](https://claude.ai/code) to be installed.
161+
Automatically detects which supported tools are installed and installs for all of them:
162+
163+
- **Claude Code** - via plugin marketplace (`claude` CLI required)
164+
- **Codex** - writes `SKILL.md` to `~/.agents/skills/stack/` (`codex` CLI required)
165+
- **Cursor** - writes `stack.mdc` to `~/.cursor/rules/` (`cursor` CLI or `~/.cursor/` directory required)
162166

163167
```bash
164168
stack skill install
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package skillcontent
2+
3+
import _ "embed"
4+
5+
//go:embed SKILL.md
6+
var SkillMD string

0 commit comments

Comments
 (0)