From 5a97ba1bbb8e7d3d8a113a447cfd5709ab28f40a Mon Sep 17 00:00:00 2001 From: Nate Meyer <672246+notnmeyer@users.noreply.github.com> Date: Mon, 11 May 2026 13:45:44 -0700 Subject: [PATCH 1/3] add skill install command --- cmd/skill.go | 12 +++++++ cmd/skill_install.go | 70 +++++++++++++++++++++++++++++++++++++++ cmd/skill_install_test.go | 58 ++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 cmd/skill.go create mode 100644 cmd/skill_install.go create mode 100644 cmd/skill_install_test.go diff --git a/cmd/skill.go b/cmd/skill.go new file mode 100644 index 0000000..720bf0c --- /dev/null +++ b/cmd/skill.go @@ -0,0 +1,12 @@ +package cmd + +import "github.com/spf13/cobra" + +var skillCmd = &cobra.Command{ + Use: "skill", + Short: "Manage the Loops CLI skill for AI agents", +} + +func init() { + rootCmd.AddCommand(skillCmd) +} diff --git a/cmd/skill_install.go b/cmd/skill_install.go new file mode 100644 index 0000000..9158929 --- /dev/null +++ b/cmd/skill_install.go @@ -0,0 +1,70 @@ +package cmd + +import ( + "fmt" + "io" + "os" + "os/exec" + "strings" + + "github.com/spf13/cobra" +) + +const ( + skillsRepoURL = "https://github.com/loops-so/skills" + skillName = "loops-cli" +) + +var ( + skillInstallGlobal bool + skillInstallYes bool +) + +func skillInstallArgs(global, yes bool) []string { + args := []string{"skills", "add", skillsRepoURL, "--skill", skillName} + if global { + args = append(args, "--global") + } + if yes { + args = append(args, "--yes") + } + return args +} + +func runSkillInstall(stderr io.Writer, global, yes bool) error { + if _, err := exec.LookPath("npx"); err != nil { + return fmt.Errorf("npx not found on PATH") + } + + args := skillInstallArgs(global, yes) + fmt.Fprintf(stderr, "Running: npx %s\n", strings.Join(args, " ")) + + c := exec.Command("npx", args...) + c.Stdin = os.Stdin + c.Stdout = stderr + c.Stderr = stderr + if err := c.Run(); err != nil { + return fmt.Errorf("npx: %w", err) + } + return nil +} + +var skillInstallCmd = &cobra.Command{ + Use: "install", + Short: "Install the Loops CLI skill via npx", + RunE: func(cmd *cobra.Command, args []string) error { + if err := runSkillInstall(os.Stderr, skillInstallGlobal, skillInstallYes); err != nil { + return err + } + if isJSONOutput() { + return printJSON(cmd.OutOrStdout(), Result{Success: true, Message: "skill installed"}) + } + return nil + }, +} + +func init() { + skillInstallCmd.Flags().BoolVarP(&skillInstallGlobal, "global", "g", false, "Install the skill globally") + skillInstallCmd.Flags().BoolVarP(&skillInstallYes, "yes", "y", false, "Skip confirmation prompts") + skillCmd.AddCommand(skillInstallCmd) +} diff --git a/cmd/skill_install_test.go b/cmd/skill_install_test.go new file mode 100644 index 0000000..5876f40 --- /dev/null +++ b/cmd/skill_install_test.go @@ -0,0 +1,58 @@ +package cmd + +import ( + "bytes" + "reflect" + "strings" + "testing" +) + +func TestSkillInstallArgs(t *testing.T) { + tests := []struct { + name string + global bool + yes bool + want []string + }{ + { + name: "no flags", + want: []string{"skills", "add", "https://github.com/loops-so/skills", "--skill", "loops-cli"}, + }, + { + name: "global", + global: true, + want: []string{"skills", "add", "https://github.com/loops-so/skills", "--skill", "loops-cli", "--global"}, + }, + { + name: "yes", + yes: true, + want: []string{"skills", "add", "https://github.com/loops-so/skills", "--skill", "loops-cli", "--yes"}, + }, + { + name: "global and yes", + global: true, + yes: true, + want: []string{"skills", "add", "https://github.com/loops-so/skills", "--skill", "loops-cli", "--global", "--yes"}, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := skillInstallArgs(tc.global, tc.yes) + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("got %v, want %v", got, tc.want) + } + }) + } +} + +func TestRunSkillInstallErrorsWhenNpxMissing(t *testing.T) { + t.Setenv("PATH", "") + var buf bytes.Buffer + err := runSkillInstall(&buf, false, false) + if err == nil { + t.Fatal("expected error when npx is missing, got nil") + } + if !strings.Contains(err.Error(), "npx not found") { + t.Errorf("expected error to mention 'npx not found', got %v", err) + } +} From 0d25c9636cdcf2ef5b4b8d233a4797ae1746cb61 Mon Sep 17 00:00:00 2001 From: Nate Meyer <672246+notnmeyer@users.noreply.github.com> Date: Wed, 27 May 2026 14:40:27 -0700 Subject: [PATCH 2/3] also pass --yes to npx to supress its prompt --- cmd/skill_install.go | 10 ++++++++-- cmd/skill_install_test.go | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/cmd/skill_install.go b/cmd/skill_install.go index 9158929..983abbe 100644 --- a/cmd/skill_install.go +++ b/cmd/skill_install.go @@ -21,10 +21,16 @@ var ( ) func skillInstallArgs(global, yes bool) []string { - args := []string{"skills", "add", skillsRepoURL, "--skill", skillName} + args := []string{} + // --yes for npx + if yes { + args = append(args, "--yes") + } + args = append(args, "skills", "add", skillsRepoURL, "--skill", skillName) if global { args = append(args, "--global") } + // --yes for skills if yes { args = append(args, "--yes") } @@ -51,7 +57,7 @@ func runSkillInstall(stderr io.Writer, global, yes bool) error { var skillInstallCmd = &cobra.Command{ Use: "install", - Short: "Install the Loops CLI skill via npx", + Short: "Install the Loops CLI skill via 'skills add'", RunE: func(cmd *cobra.Command, args []string) error { if err := runSkillInstall(os.Stderr, skillInstallGlobal, skillInstallYes); err != nil { return err diff --git a/cmd/skill_install_test.go b/cmd/skill_install_test.go index 5876f40..1e3672b 100644 --- a/cmd/skill_install_test.go +++ b/cmd/skill_install_test.go @@ -26,13 +26,13 @@ func TestSkillInstallArgs(t *testing.T) { { name: "yes", yes: true, - want: []string{"skills", "add", "https://github.com/loops-so/skills", "--skill", "loops-cli", "--yes"}, + want: []string{"--yes", "skills", "add", "https://github.com/loops-so/skills", "--skill", "loops-cli", "--yes"}, }, { name: "global and yes", global: true, yes: true, - want: []string{"skills", "add", "https://github.com/loops-so/skills", "--skill", "loops-cli", "--global", "--yes"}, + want: []string{"--yes", "skills", "add", "https://github.com/loops-so/skills", "--skill", "loops-cli", "--global", "--yes"}, }, } for _, tc := range tests { From 2d94567658dda190a993aee20b019ac59867b42e Mon Sep 17 00:00:00 2001 From: Nate Meyer <672246+notnmeyer@users.noreply.github.com> Date: Wed, 27 May 2026 15:08:15 -0700 Subject: [PATCH 3/3] add an --all flag --- cmd/skill_install.go | 25 ++++++++++++++++--------- cmd/skill_install_test.go | 22 ++++++++++++++++++++-- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/cmd/skill_install.go b/cmd/skill_install.go index 983abbe..44d7b3a 100644 --- a/cmd/skill_install.go +++ b/cmd/skill_install.go @@ -18,31 +18,37 @@ const ( var ( skillInstallGlobal bool skillInstallYes bool + skillInstallAll bool ) -func skillInstallArgs(global, yes bool) []string { +func skillInstallArgs(global, yes, all bool) []string { args := []string{} - // --yes for npx - if yes { + // --yes for npx (--all implies non-interactive intent) + if yes || all { args = append(args, "--yes") } - args = append(args, "skills", "add", skillsRepoURL, "--skill", skillName) + args = append(args, "skills", "add", skillsRepoURL) + if all { + args = append(args, "--all") + } else { + args = append(args, "--skill", skillName) + } if global { args = append(args, "--global") } - // --yes for skills - if yes { + // --yes for skills (--all already implies -y to skills) + if yes && !all { args = append(args, "--yes") } return args } -func runSkillInstall(stderr io.Writer, global, yes bool) error { +func runSkillInstall(stderr io.Writer, global, yes, all bool) error { if _, err := exec.LookPath("npx"); err != nil { return fmt.Errorf("npx not found on PATH") } - args := skillInstallArgs(global, yes) + args := skillInstallArgs(global, yes, all) fmt.Fprintf(stderr, "Running: npx %s\n", strings.Join(args, " ")) c := exec.Command("npx", args...) @@ -59,7 +65,7 @@ var skillInstallCmd = &cobra.Command{ Use: "install", Short: "Install the Loops CLI skill via 'skills add'", RunE: func(cmd *cobra.Command, args []string) error { - if err := runSkillInstall(os.Stderr, skillInstallGlobal, skillInstallYes); err != nil { + if err := runSkillInstall(os.Stderr, skillInstallGlobal, skillInstallYes, skillInstallAll); err != nil { return err } if isJSONOutput() { @@ -72,5 +78,6 @@ var skillInstallCmd = &cobra.Command{ func init() { skillInstallCmd.Flags().BoolVarP(&skillInstallGlobal, "global", "g", false, "Install the skill globally") skillInstallCmd.Flags().BoolVarP(&skillInstallYes, "yes", "y", false, "Skip confirmation prompts") + skillInstallCmd.Flags().BoolVarP(&skillInstallAll, "all", "a", false, "Install all Loops skills (not just the CLI skill)") skillCmd.AddCommand(skillInstallCmd) } diff --git a/cmd/skill_install_test.go b/cmd/skill_install_test.go index 1e3672b..5359566 100644 --- a/cmd/skill_install_test.go +++ b/cmd/skill_install_test.go @@ -12,6 +12,7 @@ func TestSkillInstallArgs(t *testing.T) { name string global bool yes bool + all bool want []string }{ { @@ -34,10 +35,27 @@ func TestSkillInstallArgs(t *testing.T) { yes: true, want: []string{"--yes", "skills", "add", "https://github.com/loops-so/skills", "--skill", "loops-cli", "--global", "--yes"}, }, + { + name: "all", + all: true, + want: []string{"--yes", "skills", "add", "https://github.com/loops-so/skills", "--all"}, + }, + { + name: "all and global", + all: true, + global: true, + want: []string{"--yes", "skills", "add", "https://github.com/loops-so/skills", "--all", "--global"}, + }, + { + name: "all and yes", + all: true, + yes: true, + want: []string{"--yes", "skills", "add", "https://github.com/loops-so/skills", "--all"}, + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - got := skillInstallArgs(tc.global, tc.yes) + got := skillInstallArgs(tc.global, tc.yes, tc.all) if !reflect.DeepEqual(got, tc.want) { t.Errorf("got %v, want %v", got, tc.want) } @@ -48,7 +66,7 @@ func TestSkillInstallArgs(t *testing.T) { func TestRunSkillInstallErrorsWhenNpxMissing(t *testing.T) { t.Setenv("PATH", "") var buf bytes.Buffer - err := runSkillInstall(&buf, false, false) + err := runSkillInstall(&buf, false, false, false) if err == nil { t.Fatal("expected error when npx is missing, got nil") }