Skip to content

Commit e3f9d85

Browse files
authored
Add render skills install command (#269)
## Summary Add `render skills` commands for managing Render agent skills across AI coding tools. ## What's included - **`skills install`** — Interactive or CLI-driven installation of skills from the Render skills repo to detected AI coding tools (Codex, OpenCode, Gemini CLI, and all Vercel-registry agents) - **`skills update`** — Content-hash-based update detection (SHA-256) with interactive selection of outdated skills - **`skills remove`** — Remove skills interactively or via `--skill`, `--tool`, and `--all` flags - **`skills list`** — Display installed skills and detected tools from local state (offline) - **Skills hub** — `render skills` opens a TUI palette for quick access to all subcommands ## Key design decisions - **Shared skills directory** (`~/.agents/skills`) used by most tools, with tool-specific paths where needed (e.g. `~/.gemini/skills`) - **Scope support** — Skills can be installed at user or project scope, with scope-aware state management and labeling - **TUI-first** — All commands use Bubble Tea views with state machines; `--tool`/`--skill`/`--scope` flags available for CI - **go-git** for cloning instead of shelling out to `git` - **State persistence** in `~/.render/skills.yaml` tracking versions, content hashes, scopes, and directory names
1 parent dc46a98 commit e3f9d85

19 files changed

Lines changed: 4557 additions & 18 deletions

cmd/skills.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
6+
tea "github.com/charmbracelet/bubbletea"
7+
"github.com/spf13/cobra"
8+
9+
"github.com/render-oss/cli/pkg/tui"
10+
"github.com/render-oss/cli/pkg/tui/views"
11+
)
12+
13+
var skillsCmd = &cobra.Command{
14+
Use: "skills",
15+
Short: "Manage Render agent skills for AI coding tools",
16+
Long: `Install and manage Render agent skills for AI coding tools such as
17+
Claude Code, Codex, OpenCode, and Cursor.
18+
19+
Skills add deployment, debugging, and monitoring capabilities to your
20+
AI coding assistant.`,
21+
RunE: func(cmd *cobra.Command, args []string) error {
22+
ctx := cmd.Context()
23+
stack := tui.GetStackFromContext(ctx)
24+
25+
commands := []views.PaletteCommand{
26+
{
27+
Name: "list",
28+
Description: "List installed skills and detected tools",
29+
Action: func(ctx context.Context, args []string) tea.Cmd {
30+
return pushSkillsView(ctx, views.NewSkillsListView(""), "List Skills")
31+
},
32+
},
33+
{
34+
Name: "install",
35+
Description: "Install skills to AI coding tools",
36+
Action: func(ctx context.Context, args []string) tea.Cmd {
37+
return pushSkillsView(ctx, views.NewSkillsInstallView(views.SkillsInstallViewInput{}), "Install Skills")
38+
},
39+
},
40+
{
41+
Name: "update",
42+
Description: "Update previously installed skills",
43+
Action: func(ctx context.Context, args []string) tea.Cmd {
44+
return pushSkillsView(ctx, views.NewSkillsUpdateView(false, ""), "Update Skills")
45+
},
46+
},
47+
{
48+
Name: "remove",
49+
Description: "Remove installed skills from tools",
50+
Action: func(ctx context.Context, args []string) tea.Cmd {
51+
return pushSkillsView(ctx, views.NewSkillsRemoveView(""), "Remove Skills")
52+
},
53+
},
54+
}
55+
56+
palette := views.NewPaletteView(ctx, commands)
57+
stack.Push(tui.ModelWithCmd{
58+
Model: palette,
59+
Breadcrumb: "Skills",
60+
})
61+
return nil
62+
},
63+
}
64+
65+
func init() {
66+
rootCmd.AddCommand(skillsCmd)
67+
}
68+
69+
// pushSkillsView pushes a skills view onto the TUI stack.
70+
// Skills views are pushed directly (not via AddToStackFunc) because
71+
// they are purely local — there's no CLI command string to copy.
72+
func pushSkillsView(ctx context.Context, model tea.Model, breadcrumb string) tea.Cmd {
73+
stack := tui.GetStackFromContext(ctx)
74+
return stack.Push(tui.ModelWithCmd{
75+
Model: model,
76+
Breadcrumb: breadcrumb,
77+
})
78+
}

cmd/skillsinstall.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package cmd
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"github.com/render-oss/cli/pkg/command"
7+
"github.com/render-oss/cli/pkg/skills"
8+
"github.com/render-oss/cli/pkg/text"
9+
"github.com/render-oss/cli/pkg/tui"
10+
"github.com/render-oss/cli/pkg/tui/views"
11+
)
12+
13+
// SkillsInstallInput holds the CLI input for skills install.
14+
type SkillsInstallInput struct {
15+
Tool string `cli:"tool"`
16+
Skills []string `cli:"skill"`
17+
DryRun bool `cli:"dry-run"`
18+
Scope string `cli:"scope"`
19+
}
20+
21+
var skillsInstallCmd = &cobra.Command{
22+
Use: "install",
23+
Short: "Install Render skills to AI coding tools",
24+
Long: `Install Render agent skills from https://github.com/render-oss/skills to
25+
detected AI coding tools.
26+
27+
Supported tools: Claude Code, Codex, OpenCode, Cursor.
28+
29+
Skills can be installed at two scopes:
30+
- user: Install to ~/.{tool}/skills/ (default, current user only)
31+
- project: Install to ./.{tool}/skills/ (committed to git, all collaborators)
32+
33+
By default an interactive prompt lets you pick scope, tools, and skills.
34+
Use --scope, --tool, and --skill flags to skip the prompts (useful for CI).`,
35+
SilenceUsage: true,
36+
}
37+
38+
func init() {
39+
skillsCmd.AddCommand(skillsInstallCmd)
40+
skillsInstallCmd.Flags().String("tool", "", "install to a specific tool only (claude, codex, opencode, cursor)")
41+
skillsInstallCmd.Flags().StringSlice("skill", nil, "install specific skills only (e.g. --skill render-deploy --skill render-debug)")
42+
skillsInstallCmd.Flags().Bool("dry-run", false, "show what would be installed without making changes")
43+
skillsInstallCmd.Flags().String("scope", "", "installation scope: user (default) or project")
44+
45+
skillsInstallCmd.RunE = func(cmd *cobra.Command, args []string) error {
46+
var input SkillsInstallInput
47+
if err := command.ParseCommand(cmd, args, &input); err != nil {
48+
return err
49+
}
50+
51+
// Parse scope if provided
52+
var scope skills.Scope
53+
if input.Scope != "" {
54+
var err error
55+
scope, err = skills.ParseScope(input.Scope)
56+
if err != nil {
57+
return err
58+
}
59+
}
60+
61+
// Non-interactive path: use command.NonInteractive
62+
if nonInteractive, err := command.NonInteractive(cmd, func() (*skills.InstallResult, error) {
63+
return skills.Install(skills.InstallInput{
64+
ToolFilter: input.Tool,
65+
SkillFilter: input.Skills,
66+
DryRun: input.DryRun,
67+
Scope: scope,
68+
})
69+
}, func(r *skills.InstallResult) string {
70+
action := "Installed"
71+
if r.DryRun {
72+
action = "Would install"
73+
}
74+
return text.FormatStringF("%s %d skill(s) to %d tool(s)", action, len(r.Skills), len(r.Tools))
75+
}); err != nil {
76+
return err
77+
} else if nonInteractive {
78+
return nil
79+
}
80+
81+
// Interactive path: launch TUI with pre-populated input
82+
interactiveSkillsInstall(cmd, input, scope)
83+
return nil
84+
}
85+
}
86+
87+
// interactiveSkillsInstall launches the TUI, skipping steps based on provided flags.
88+
func interactiveSkillsInstall(cmd *cobra.Command, input SkillsInstallInput, scope skills.Scope) {
89+
ctx := cmd.Context()
90+
stack := tui.GetStackFromContext(ctx)
91+
92+
// Pass input to the view so it can skip steps if flags are provided
93+
stack.Push(tui.ModelWithCmd{
94+
Model: views.NewSkillsInstallView(views.SkillsInstallViewInput{
95+
ToolFilter: input.Tool,
96+
SkillFilter: input.Skills,
97+
Scope: scope,
98+
}),
99+
Breadcrumb: "Install Skills",
100+
})
101+
}

cmd/skillslist.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package cmd
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"github.com/render-oss/cli/pkg/skills"
7+
"github.com/render-oss/cli/pkg/tui"
8+
"github.com/render-oss/cli/pkg/tui/views"
9+
)
10+
11+
var skillsListCmd = &cobra.Command{
12+
Use: "list",
13+
Short: "List installed Render skills and detected tools",
14+
Long: `Show which Render skills are currently installed and which AI coding tools
15+
they are installed to. This reads from local state only — no network
16+
access is required.
17+
18+
Use --scope to filter by installation scope (user or project).`,
19+
SilenceUsage: true,
20+
RunE: func(cmd *cobra.Command, args []string) error {
21+
scopeFilter, _ := cmd.Flags().GetString("scope")
22+
23+
var scope skills.Scope
24+
if scopeFilter != "" {
25+
var err error
26+
scope, err = skills.ParseScope(scopeFilter)
27+
if err != nil {
28+
return err
29+
}
30+
}
31+
32+
// Push TUI view onto the stack.
33+
// We push directly (not via AddToStackFunc) because skills are
34+
// purely local — there's no CLI command string to copy.
35+
ctx := cmd.Context()
36+
stack := tui.GetStackFromContext(ctx)
37+
stack.Push(tui.ModelWithCmd{
38+
Model: views.NewSkillsListView(scope),
39+
Breadcrumb: "List Skills",
40+
})
41+
return nil
42+
},
43+
}
44+
45+
func init() {
46+
skillsCmd.AddCommand(skillsListCmd)
47+
skillsListCmd.Flags().String("scope", "", "filter by scope: user or project")
48+
}

0 commit comments

Comments
 (0)