Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions cli/cmd/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,36 @@ var credentialsPath string
func addAuthCmd(root *cobra.Command) {
authCmd := &cobra.Command{
Use: "auth",
Short: "Manage authentication",
Short: "Manage authentication with the registry",
}

loginCmd := &cobra.Command{
Use: "login",
Short: "Authenticate with the registry",
RunE: runAuthLogin,
Short: "Authenticate with the registry via OIDC device flow",
Long: `Start an OIDC device authorization flow against the registry's configured
identity provider. The CLI prints a URL and a user code; open the URL in any
browser, enter the code, and approve. The resulting tokens are cached at
~/.config/skillsctl/credentials.json (or the path passed to --credentials-path)
and refreshed automatically on subsequent commands.

If the server is running without OIDC configured, this command prints a notice
and exits successfully without writing credentials.`,
Example: ` skillsctl auth login`,
RunE: runAuthLogin,
}

statusCmd := &cobra.Command{
Use: "status",
Short: "Show authentication status",
RunE: runAuthStatus,
Use: "status",
Short: "Show the active identity and token expiry",
Example: ` skillsctl auth status`,
RunE: runAuthStatus,
}

logoutCmd := &cobra.Command{
Use: "logout",
Short: "Remove cached credentials",
RunE: runAuthLogout,
Use: "logout",
Short: "Remove cached credentials",
Example: ` skillsctl auth logout`,
RunE: runAuthLogout,
}

authCmd.AddCommand(loginCmd, statusCmd, logoutCmd)
Expand Down
39 changes: 23 additions & 16 deletions cli/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,34 +21,41 @@ var validConfigKeys = []string{"api_url", "skills_dir"}
func addConfigCmd(root *cobra.Command) {
configCmd := &cobra.Command{
Use: "config",
Short: "Manage skillsctl configuration",
Short: "Manage the local skillsctl configuration file",
Long: `Read and write ~/.config/skillsctl/config.yaml. Recognized keys are
api_url (the registry URL the CLI talks to) and skills_dir (where 'install'
writes downloaded skills).`,
}

initCmd := &cobra.Command{
Use: "init",
Short: "Interactive first-time setup",
RunE: runConfigInit,
Use: "init",
Short: "Interactive first-time setup; prompts for api_url and skills_dir",
Example: ` skillsctl config init`,
RunE: runConfigInit,
}
initCmd.Flags().Bool("force", false, "Overwrite existing config")
initCmd.Flags().Bool("force", false, "Overwrite an existing config file")

setCmd := &cobra.Command{
Use: "set <key> <value>",
Short: "Set a config value",
Args: cobra.ExactArgs(2),
RunE: runConfigSet,
Use: "set <key> <value>",
Short: "Set a config value (api_url or skills_dir)",
Example: ` skillsctl config set api_url https://skillsctl.example.com`,
Args: cobra.ExactArgs(2),
RunE: runConfigSet,
}

getCmd := &cobra.Command{
Use: "get <key>",
Short: "Get a config value",
Args: cobra.ExactArgs(1),
RunE: runConfigGet,
Use: "get <key>",
Short: "Print a single config value",
Example: ` skillsctl config get api_url`,
Args: cobra.ExactArgs(1),
RunE: runConfigGet,
}

listCmd := &cobra.Command{
Use: "list",
Short: "List all config values",
RunE: runConfigList,
Use: "list",
Short: "Print every recognized config value",
Example: ` skillsctl config list`,
RunE: runConfigList,
}

configCmd.PersistentFlags().StringVar(&configPath, "config-path", "", "Config file path (for testing)")
Expand Down
23 changes: 18 additions & 5 deletions cli/cmd/explore.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ func addExploreCmd(root *cobra.Command) {
exploreCmd := &cobra.Command{
Use: "explore",
Short: "Browse available skills",
Example: ` # List every skill in the registry
skillsctl explore

# Filter by tag (repeatable; matches any)
skillsctl explore --tag go --tag testing

# Only show skills published directly to this registry
skillsctl explore --source internal`,
RunE: func(cmd *cobra.Command, _ []string) error {
client := getClientCtx(cmd.Context())

Expand All @@ -41,13 +49,18 @@ func addExploreCmd(root *cobra.Command) {
},
}

exploreCmd.Flags().StringSliceVar(&tags, "tag", nil, "Filter by tag (repeatable)")
exploreCmd.Flags().StringVar(&source, "source", "all", "Filter: internal, external, all")
exploreCmd.Flags().StringSliceVar(&tags, "tag", nil, "Filter by tag; repeat to match any of several tags")
exploreCmd.Flags().StringVar(&source, "source", "all", "Filter by source: internal, external, or all")

showCmd := &cobra.Command{
Use: "show <name>",
Short: "Show skill details",
Args: cobra.ExactArgs(1),
Short: "Show skill details and version history",
Example: ` # Metadata and version history
skillsctl explore show git-commit

# Include the rendered SKILL.md content
skillsctl explore show git-commit --verbose`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client := getClientCtx(cmd.Context())
skill, versions, err := client.GetSkill(context.Background(), args[0])
Expand Down Expand Up @@ -78,7 +91,7 @@ func addExploreCmd(root *cobra.Command) {
return nil
},
}
showCmd.Flags().BoolP("verbose", "v", false, "include skill content in output")
showCmd.Flags().BoolP("verbose", "v", false, "Include the SKILL.md content (multi-file skills are extracted first)")

exploreCmd.AddCommand(showCmd)
root.AddCommand(exploreCmd)
Expand Down
23 changes: 20 additions & 3 deletions cli/cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,24 @@ func addInstallCmd(root *cobra.Command) {
installCmd := &cobra.Command{
Use: "install <name[@version]>",
Short: "Install a skill from the registry",
Args: cobra.ExactArgs(1),
Long: `Download a skill from the registry and write it to the local skills directory.

By default skills install to the directory configured as skills_dir
(typically ~/.claude/skills). Use --project to install into a specific
project's .claude/skills/ instead, or --skills-dir to override the
target for a single invocation.`,
Example: ` # Install the latest version
skillsctl install git-commit

# Pin a version and verify the digest
skillsctl install git-commit@1.2.0 --digest sha256:abc123...

# Install into the current project (writes to ./.claude/skills/)
skillsctl install git-commit --project

# Install into a specific project directory
skillsctl install git-commit --project /path/to/repo`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name, version := parseNameVersion(args[0])
if err := validateSkillName(name); err != nil {
Expand Down Expand Up @@ -90,8 +107,8 @@ func addInstallCmd(root *cobra.Command) {
},
}

installCmd.Flags().StringVar(&digest, "digest", "", "Expected content digest for verification")
installCmd.Flags().StringVar(&skillsDir, "skills-dir", "", "Override skills directory")
installCmd.Flags().StringVar(&digest, "digest", "", "Expected content digest in sha256:... form; install aborts if the downloaded content does not match")
installCmd.Flags().StringVar(&skillsDir, "skills-dir", "", "Override the configured skills directory for this install")
installCmd.Flags().StringVar(&projectDir, "project", "", "Install to <path>/.claude/skills; when used without a value, installs into the current directory")
// Passing --project without a value resolves to the current directory.
installCmd.Flags().Lookup("project").NoOptDefVal = "."
Expand Down
33 changes: 27 additions & 6 deletions cli/cmd/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ func addPublishCmd(root *cobra.Command) {
publishCmd := &cobra.Command{
Use: "publish",
Short: "Publish a skill to the registry",
Long: `Package a skill directory and upload it to the registry.

The directory passed via --dir must contain SKILL.md at its root;
subdirectories (scripts/, references/, assets/) are packaged and
preserved on install. Versions are immutable once published, so each
publish must use a new --version.`,
Example: ` # Publish the first version of a skill
skillsctl publish \
--name git-conventional \
--version 1.0.0 \
--description "Enforce conventional commit messages" \
--dir ./git-conventional

# Publish with tags and a changelog
skillsctl publish \
--name git-conventional \
--version 1.1.0 \
--description "Enforce conventional commit messages" \
--dir ./git-conventional \
--tag git --tag commits \
--changelog "Add breaking-change detection"`,
RunE: func(cmd *cobra.Command, _ []string) error {
content, err := skillpkg.Pack(dirPath)
if err != nil {
Expand Down Expand Up @@ -53,12 +74,12 @@ func addPublishCmd(root *cobra.Command) {
},
}

publishCmd.Flags().StringVar(&name, "name", "", "Skill name")
publishCmd.Flags().StringVar(&version, "version", "", "Skill version (semver)")
publishCmd.Flags().StringVar(&description, "description", "", "Skill description")
publishCmd.Flags().StringVar(&dirPath, "dir", "", "Path to skill directory containing SKILL.md")
publishCmd.Flags().StringSliceVar(&tags, "tag", nil, "Tags (repeatable)")
publishCmd.Flags().StringVar(&changelog, "changelog", "", "Version changelog")
publishCmd.Flags().StringVar(&name, "name", "", "Skill name (lowercase, 2-64 chars, alphanumeric and hyphens)")
publishCmd.Flags().StringVar(&version, "version", "", "Semantic version, e.g. 1.0.0; must be unique for this skill")
publishCmd.Flags().StringVar(&description, "description", "", "Short description shown in 'skillsctl explore'")
publishCmd.Flags().StringVar(&dirPath, "dir", "", "Path to the skill directory; must contain SKILL.md at its root")
publishCmd.Flags().StringSliceVar(&tags, "tag", nil, "Tag for filtering in 'explore' (repeatable)")
publishCmd.Flags().StringVar(&changelog, "changelog", "", "Release notes for this version")

_ = publishCmd.MarkFlagRequired("name")
_ = publishCmd.MarkFlagRequired("version")
Expand Down
6 changes: 5 additions & 1 deletion cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ func NewRootCmd() *cobra.Command {
rootCmd := &cobra.Command{
Use: "skillsctl",
Short: "Discover, install, and publish Claude Code skills",
Long: `skillsctl is the CLI for the SkillsCtl skill registry. Use it to browse
and install skills published by your team or organization, and to publish
your own. Skills install into ~/.claude/skills/ by default and are picked
up automatically by Claude Code at session start.`,
}
rootCmd.Version = version

rootCmd.PersistentFlags().StringVar(&apiURL, "api-url", "", "Backend API URL")
rootCmd.PersistentFlags().StringVar(&apiURL, "api-url", "", "Registry URL; overrides the api_url setting from config and SKILLCTL_API_URL")
rootCmd.PersistentFlags().StringVar(&credentialsPath, "credentials-path", "", "Credentials file path (for testing)")

cobra.OnInitialize(func() {
Expand Down
Loading