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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ metadata:
name: author-name
type: human # human | agent
platform: claude-code # only when type=agent
version: "0.1.0"
version: "0.0.1"
modified-by: # append-only provenance list
- name: codex-cli
type: agent
Expand Down
1 change: 1 addition & 0 deletions internal/cli/skill.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func newSkillCmd() *cobra.Command {
cmd.AddCommand(newSkillUninstallCmd())
cmd.AddCommand(newSkillRecommendCmd())
cmd.AddCommand(newSkillDiffCmd())
cmd.AddCommand(newSkillVersionCmd())

return cmd
}
9 changes: 9 additions & 0 deletions internal/cli/skill_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func newSkillCreateCmd() *cobra.Command {
force bool
fromTemplate string
tags []string
version string
)

cmd := &cobra.Command{
Expand Down Expand Up @@ -105,6 +106,13 @@ func newSkillCreateCmd() *cobra.Command {
s := skill.NewSkillWithBody(name, description, author, authorType, authorPlatform, body)
s.Tags = tags

if version != "" {
if _, err := skill.ParseVersion(version); err != nil {
return &ValidationError{Message: err.Error()}
}
s.Metadata.Version = version
}

// Validate on create (warnings only, don't block)
issues := skill.Validate(s)
if len(issues) > 0 {
Expand Down Expand Up @@ -136,6 +144,7 @@ func newSkillCreateCmd() *cobra.Command {
cmd.Flags().BoolVar(&force, "force", false, "bypass overlap detection block")
cmd.Flags().StringVar(&fromTemplate, "from-template", "", "path to a template file for the skill body")
cmd.Flags().StringSliceVar(&tags, "tags", nil, "comma-separated tags for the skill")
cmd.Flags().StringVar(&version, "version", "", "initial version (default: 0.0.1)")

return cmd
}
Expand Down
84 changes: 84 additions & 0 deletions internal/cli/skill_version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package cli

import (
"fmt"
"path/filepath"

"github.com/devrimcavusoglu/skern/internal/output"
"github.com/devrimcavusoglu/skern/internal/skill"
"github.com/spf13/cobra"
)

func newSkillVersionCmd() *cobra.Command {
var (
scope string
bump string
)

cmd := &cobra.Command{
Use: "version <name>",
Short: "Show or bump a skill's version",
Long: "Display the current version of a skill, or bump it with --bump patch|minor|major.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := getContext(cmd)
name := args[0]

reg, err := ctx.NewRegistry()
if err != nil {
return err
}

s, skillDir, foundScope, err := resolveSkill(reg, name, scope)
if err != nil {
return err
}

if bump == "" {
// Show current version
result := output.SkillVersionResult{
Name: name,
Version: s.Metadata.Version,
Scope: string(foundScope),
Bumped: false,
}
text := fmt.Sprintf("%s\n", s.Metadata.Version)
ctx.Printer.PrintResult(result, text)
return nil
}

// Validate bump level
if bump != "patch" && bump != "minor" && bump != "major" {
return &ValidationError{Message: fmt.Sprintf("invalid bump level %q: must be patch, minor, or major", bump)}
}

previousVersion := s.Metadata.Version
newVersion, err := skill.BumpVersion(previousVersion, bump)
if err != nil {
return fmt.Errorf("bumping version: %w", err)
}

s.Metadata.Version = newVersion
manifestPath := filepath.Join(skillDir, "SKILL.md")
if err := skill.WriteManifest(s, manifestPath); err != nil {
return fmt.Errorf("writing manifest: %w", err)
}

result := output.SkillVersionResult{
Name: name,
Version: newVersion,
Scope: string(foundScope),
PreviousVersion: previousVersion,
Bumped: true,
}
text := fmt.Sprintf("Bumped %q from %s to %s (%s)\n", name, previousVersion, newVersion, bump)
ctx.Printer.PrintResult(result, text)
return nil
},
}

cmd.Flags().StringVar(&scope, "scope", "", "skill scope (user or project)")
cmd.Flags().StringVar(&bump, "bump", "", "bump level: patch, minor, or major")

return cmd
}
224 changes: 224 additions & 0 deletions internal/cli/skill_version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package cli

import (
"encoding/json"
"testing"

"github.com/devrimcavusoglu/skern/internal/output"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// --- skill version (show) ---

func TestSkillVersion_Show(t *testing.T) {
cc := testRegistry(t)

_, err := runCmd(t, cc, "skill", "create", "ver-skill", "--description", "A skill")
require.NoError(t, err)

out, err := runCmd(t, cc, "skill", "version", "ver-skill", "--json")
require.NoError(t, err)

var result output.SkillVersionResult
require.NoError(t, json.Unmarshal([]byte(out), &result))
assert.Equal(t, "ver-skill", result.Name)
assert.Equal(t, "0.0.1", result.Version)
assert.Equal(t, "user", result.Scope)
assert.False(t, result.Bumped)
}

func TestSkillVersion_Show_Text(t *testing.T) {
cc := testRegistry(t)

_, err := runCmd(t, cc, "skill", "create", "ver-text", "--description", "A skill")
require.NoError(t, err)

out, err := runCmd(t, cc, "skill", "version", "ver-text")
require.NoError(t, err)
assert.Contains(t, out, "0.0.1")
}

func TestSkillVersion_NotFound(t *testing.T) {
cc := testRegistry(t)

_, err := runCmd(t, cc, "skill", "version", "nonexistent")
assert.Error(t, err)
}

// --- skill version --bump ---

func TestSkillVersion_BumpPatch(t *testing.T) {
cc := testRegistry(t)

_, err := runCmd(t, cc, "skill", "create", "bump-patch", "--description", "A skill")
require.NoError(t, err)

out, err := runCmd(t, cc, "skill", "version", "bump-patch", "--bump", "patch", "--json")
require.NoError(t, err)

var result output.SkillVersionResult
require.NoError(t, json.Unmarshal([]byte(out), &result))
assert.Equal(t, "bump-patch", result.Name)
assert.Equal(t, "0.0.2", result.Version)
assert.Equal(t, "0.0.1", result.PreviousVersion)
assert.True(t, result.Bumped)

// Verify the change persisted
showOut, err := runCmd(t, cc, "skill", "show", "bump-patch", "--json")
require.NoError(t, err)

var showResult output.SkillResult
require.NoError(t, json.Unmarshal([]byte(showOut), &showResult))
assert.Equal(t, "0.0.2", showResult.Version)
}

func TestSkillVersion_BumpMinor(t *testing.T) {
cc := testRegistry(t)

_, err := runCmd(t, cc, "skill", "create", "bump-minor", "--description", "A skill")
require.NoError(t, err)

out, err := runCmd(t, cc, "skill", "version", "bump-minor", "--bump", "minor", "--json")
require.NoError(t, err)

var result output.SkillVersionResult
require.NoError(t, json.Unmarshal([]byte(out), &result))
assert.Equal(t, "0.1.0", result.Version)
assert.Equal(t, "0.0.1", result.PreviousVersion)
assert.True(t, result.Bumped)
}

func TestSkillVersion_BumpMajor(t *testing.T) {
cc := testRegistry(t)

_, err := runCmd(t, cc, "skill", "create", "bump-major", "--description", "A skill")
require.NoError(t, err)

out, err := runCmd(t, cc, "skill", "version", "bump-major", "--bump", "major", "--json")
require.NoError(t, err)

var result output.SkillVersionResult
require.NoError(t, json.Unmarshal([]byte(out), &result))
assert.Equal(t, "1.0.0", result.Version)
assert.Equal(t, "0.0.1", result.PreviousVersion)
assert.True(t, result.Bumped)
}

func TestSkillVersion_BumpInvalidLevel(t *testing.T) {
cc := testRegistry(t)

_, err := runCmd(t, cc, "skill", "create", "bump-bad", "--description", "A skill")
require.NoError(t, err)

_, err = runCmd(t, cc, "skill", "version", "bump-bad", "--bump", "invalid")
assert.Error(t, err)
}

func TestSkillVersion_BumpText(t *testing.T) {
cc := testRegistry(t)

_, err := runCmd(t, cc, "skill", "create", "bump-text", "--description", "A skill")
require.NoError(t, err)

out, err := runCmd(t, cc, "skill", "version", "bump-text", "--bump", "patch")
require.NoError(t, err)
assert.Contains(t, out, "Bumped")
assert.Contains(t, out, "0.0.1")
assert.Contains(t, out, "0.0.2")
}

func TestSkillVersion_MultipleBumps(t *testing.T) {
cc := testRegistry(t)

_, err := runCmd(t, cc, "skill", "create", "multi-bump", "--description", "A skill")
require.NoError(t, err)

// Bump patch twice
_, err = runCmd(t, cc, "skill", "version", "multi-bump", "--bump", "patch")
require.NoError(t, err)
_, err = runCmd(t, cc, "skill", "version", "multi-bump", "--bump", "patch")
require.NoError(t, err)

out, err := runCmd(t, cc, "skill", "version", "multi-bump", "--json")
require.NoError(t, err)

var result output.SkillVersionResult
require.NoError(t, json.Unmarshal([]byte(out), &result))
assert.Equal(t, "0.0.3", result.Version)
}

func TestSkillVersion_Scoped(t *testing.T) {
cc := testRegistry(t)

_, err := runCmd(t, cc, "skill", "create", "scoped-ver", "--scope", "project", "--description", "A skill")
require.NoError(t, err)

out, err := runCmd(t, cc, "skill", "version", "scoped-ver", "--scope", "project", "--json")
require.NoError(t, err)

var result output.SkillVersionResult
require.NoError(t, json.Unmarshal([]byte(out), &result))
assert.Equal(t, "project", result.Scope)
assert.Equal(t, "0.0.1", result.Version)
}

// --- skill create --version ---

func TestSkillCreate_WithVersion(t *testing.T) {
cc := testRegistry(t)

_, err := runCmd(t, cc, "skill", "create", "versioned-skill",
"--description", "A skill", "--version", "0.2.0")
require.NoError(t, err)

out, err := runCmd(t, cc, "skill", "show", "versioned-skill", "--json")
require.NoError(t, err)

var result output.SkillResult
require.NoError(t, json.Unmarshal([]byte(out), &result))
assert.Equal(t, "0.2.0", result.Version)
}

func TestSkillCreate_WithVersion_JSON(t *testing.T) {
cc := testRegistry(t)

out, err := runCmd(t, cc, "skill", "create", "ver-json-skill",
"--description", "A skill", "--version", "1.0.0", "--json")
require.NoError(t, err)

var createResult output.SkillCreateResult
require.NoError(t, json.Unmarshal([]byte(out), &createResult))
assert.Equal(t, "ver-json-skill", createResult.Name)

// Verify version was set
showOut, err := runCmd(t, cc, "skill", "show", "ver-json-skill", "--json")
require.NoError(t, err)

var showResult output.SkillResult
require.NoError(t, json.Unmarshal([]byte(showOut), &showResult))
assert.Equal(t, "1.0.0", showResult.Version)
}

func TestSkillCreate_WithVersion_Invalid(t *testing.T) {
cc := testRegistry(t)

_, err := runCmd(t, cc, "skill", "create", "bad-ver-skill",
"--description", "A skill", "--version", "bad")
assert.Error(t, err)
}

func TestSkillCreate_DefaultVersion(t *testing.T) {
cc := testRegistry(t)

_, err := runCmd(t, cc, "skill", "create", "default-ver",
"--description", "A skill")
require.NoError(t, err)

out, err := runCmd(t, cc, "skill", "show", "default-ver", "--json")
require.NoError(t, err)

var result output.SkillResult
require.NoError(t, json.Unmarshal([]byte(out), &result))
assert.Equal(t, "0.0.1", result.Version)
}
17 changes: 17 additions & 0 deletions internal/output/types_skill.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,20 @@ type SkillDiffResult struct {
LeftBody string `json:"left_body,omitempty"`
RightBody string `json:"right_body,omitempty"`
}

// SkillVersionResult is the JSON envelope for skill version output.
type SkillVersionResult struct {
Name string `json:"name"`
Version string `json:"version"`
Scope string `json:"scope"`
PreviousVersion string `json:"previous_version,omitempty"`
Bumped bool `json:"bumped"`
}

// VersionCompareResult is the JSON envelope for version comparison output.
type VersionCompareResult struct {
Installed string `json:"installed"`
Available string `json:"available"`
Kind string `json:"kind,omitempty"`
Upgrade bool `json:"upgrade"`
}
2 changes: 1 addition & 1 deletion internal/skill/scaffold.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func NewSkillWithBody(name, description, authorName, authorType, authorPlatform,
Description: description,
Metadata: Metadata{
Author: author,
Version: "0.1.0",
Version: "0.0.1",
},
Body: body,
}
Expand Down
2 changes: 1 addition & 1 deletion internal/skill/scaffold_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ func TestNewSkill_NameOnly(t *testing.T) {

assert.Equal(t, "my-skill", s.Name)
assert.Contains(t, s.Description, "TODO")
assert.Equal(t, "0.1.0", s.Metadata.Version)
assert.Equal(t, "0.0.1", s.Metadata.Version)
assert.Contains(t, s.Body, "## Instructions")
}

Expand Down
Loading
Loading