Skip to content

Commit 63e7d08

Browse files
authored
chore: add skill install command (#81)
1 parent b9748fe commit 63e7d08

3 files changed

Lines changed: 171 additions & 0 deletions

File tree

cmd/skill.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package cmd
2+
3+
import "github.com/spf13/cobra"
4+
5+
var skillCmd = &cobra.Command{
6+
Use: "skill",
7+
Short: "Manage the Loops CLI skill for AI agents",
8+
}
9+
10+
func init() {
11+
rootCmd.AddCommand(skillCmd)
12+
}

cmd/skill_install.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"os/exec"
8+
"strings"
9+
10+
"github.com/spf13/cobra"
11+
)
12+
13+
const (
14+
skillsRepoURL = "https://github.com/loops-so/skills"
15+
skillName = "loops-cli"
16+
)
17+
18+
var (
19+
skillInstallGlobal bool
20+
skillInstallYes bool
21+
skillInstallAll bool
22+
)
23+
24+
func skillInstallArgs(global, yes, all bool) []string {
25+
args := []string{}
26+
// --yes for npx (--all implies non-interactive intent)
27+
if yes || all {
28+
args = append(args, "--yes")
29+
}
30+
args = append(args, "skills", "add", skillsRepoURL)
31+
if all {
32+
args = append(args, "--all")
33+
} else {
34+
args = append(args, "--skill", skillName)
35+
}
36+
if global {
37+
args = append(args, "--global")
38+
}
39+
// --yes for skills (--all already implies -y to skills)
40+
if yes && !all {
41+
args = append(args, "--yes")
42+
}
43+
return args
44+
}
45+
46+
func runSkillInstall(stderr io.Writer, global, yes, all bool) error {
47+
if _, err := exec.LookPath("npx"); err != nil {
48+
return fmt.Errorf("npx not found on PATH")
49+
}
50+
51+
args := skillInstallArgs(global, yes, all)
52+
fmt.Fprintf(stderr, "Running: npx %s\n", strings.Join(args, " "))
53+
54+
c := exec.Command("npx", args...)
55+
c.Stdin = os.Stdin
56+
c.Stdout = stderr
57+
c.Stderr = stderr
58+
if err := c.Run(); err != nil {
59+
return fmt.Errorf("npx: %w", err)
60+
}
61+
return nil
62+
}
63+
64+
var skillInstallCmd = &cobra.Command{
65+
Use: "install",
66+
Short: "Install the Loops CLI skill via 'skills add'",
67+
RunE: func(cmd *cobra.Command, args []string) error {
68+
if err := runSkillInstall(os.Stderr, skillInstallGlobal, skillInstallYes, skillInstallAll); err != nil {
69+
return err
70+
}
71+
if isJSONOutput() {
72+
return printJSON(cmd.OutOrStdout(), Result{Success: true, Message: "skill installed"})
73+
}
74+
return nil
75+
},
76+
}
77+
78+
func init() {
79+
skillInstallCmd.Flags().BoolVarP(&skillInstallGlobal, "global", "g", false, "Install the skill globally")
80+
skillInstallCmd.Flags().BoolVarP(&skillInstallYes, "yes", "y", false, "Skip confirmation prompts")
81+
skillInstallCmd.Flags().BoolVarP(&skillInstallAll, "all", "a", false, "Install all Loops skills (not just the CLI skill)")
82+
skillCmd.AddCommand(skillInstallCmd)
83+
}

cmd/skill_install_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"reflect"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestSkillInstallArgs(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
global bool
14+
yes bool
15+
all bool
16+
want []string
17+
}{
18+
{
19+
name: "no flags",
20+
want: []string{"skills", "add", "https://github.com/loops-so/skills", "--skill", "loops-cli"},
21+
},
22+
{
23+
name: "global",
24+
global: true,
25+
want: []string{"skills", "add", "https://github.com/loops-so/skills", "--skill", "loops-cli", "--global"},
26+
},
27+
{
28+
name: "yes",
29+
yes: true,
30+
want: []string{"--yes", "skills", "add", "https://github.com/loops-so/skills", "--skill", "loops-cli", "--yes"},
31+
},
32+
{
33+
name: "global and yes",
34+
global: true,
35+
yes: true,
36+
want: []string{"--yes", "skills", "add", "https://github.com/loops-so/skills", "--skill", "loops-cli", "--global", "--yes"},
37+
},
38+
{
39+
name: "all",
40+
all: true,
41+
want: []string{"--yes", "skills", "add", "https://github.com/loops-so/skills", "--all"},
42+
},
43+
{
44+
name: "all and global",
45+
all: true,
46+
global: true,
47+
want: []string{"--yes", "skills", "add", "https://github.com/loops-so/skills", "--all", "--global"},
48+
},
49+
{
50+
name: "all and yes",
51+
all: true,
52+
yes: true,
53+
want: []string{"--yes", "skills", "add", "https://github.com/loops-so/skills", "--all"},
54+
},
55+
}
56+
for _, tc := range tests {
57+
t.Run(tc.name, func(t *testing.T) {
58+
got := skillInstallArgs(tc.global, tc.yes, tc.all)
59+
if !reflect.DeepEqual(got, tc.want) {
60+
t.Errorf("got %v, want %v", got, tc.want)
61+
}
62+
})
63+
}
64+
}
65+
66+
func TestRunSkillInstallErrorsWhenNpxMissing(t *testing.T) {
67+
t.Setenv("PATH", "")
68+
var buf bytes.Buffer
69+
err := runSkillInstall(&buf, false, false, false)
70+
if err == nil {
71+
t.Fatal("expected error when npx is missing, got nil")
72+
}
73+
if !strings.Contains(err.Error(), "npx not found") {
74+
t.Errorf("expected error to mention 'npx not found', got %v", err)
75+
}
76+
}

0 commit comments

Comments
 (0)