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
110 changes: 110 additions & 0 deletions cmd/thv/app/skill_upgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

package app

import (
"encoding/json"
"fmt"

"github.com/spf13/cobra"

"github.com/stacklok/toolhive/pkg/skills"
)

var (
skillUpgradeProjectRoot string
skillUpgradeClientsRaw string
skillUpgradePreview bool
skillUpgradeFailOnChanges bool
skillUpgradeAllowRefChange bool
skillUpgradeFormat string
)

var skillUpgradeCmd = &cobra.Command{
Use: "upgrade [skill-name...]",
Short: "Upgrade project skills to newer pinned content",
Long: `Re-resolve a project's lock entries and install newer content where available.

Skills pinned to an immutable reference (an OCI digest or a full git commit
hash) are reported not-upgradable — there is nothing newer to resolve to.
Use --preview to see what would change without persisting anything (OCI
sources are still fetched into the local artifact store to compare digests),
and --allow-ref-change to permit the resolved reference itself changing
(e.g. a registry entry repointed at a different repository).
--fail-on-changes evaluates the same plan and never installs: it is a CI
freshness gate.`,
PreRunE: chainPreRunE(
ValidateFormat(&skillUpgradeFormat),
),
RunE: skillUpgradeCmdFunc,
}

func init() {
skillCmd.AddCommand(skillUpgradeCmd)

skillUpgradeCmd.Flags().StringVar(&skillUpgradeProjectRoot, "project-root", "",
"Project root path (default: auto-detected from the current directory)")
skillUpgradeCmd.Flags().StringVar(&skillUpgradeClientsRaw, "clients", "",
`Comma-separated target client apps (e.g. claude-code,opencode), or "all" for every available client`)
skillUpgradeCmd.Flags().BoolVar(&skillUpgradePreview, "preview", false,
"Report what would change without persisting anything (OCI sources are still fetched to compare digests)")
skillUpgradeCmd.Flags().BoolVar(&skillUpgradeFailOnChanges, "fail-on-changes", false,
"Report what would change without installing anything; a CI freshness gate")
skillUpgradeCmd.Flags().BoolVar(&skillUpgradeAllowRefChange, "allow-ref-change", false,
"Permit the resolved reference itself to change during upgrade")
AddFormatFlag(skillUpgradeCmd, &skillUpgradeFormat)
}

func skillUpgradeCmdFunc(cmd *cobra.Command, args []string) error {
projectRoot, err := resolveProjectRoot(skillUpgradeProjectRoot)
if err != nil {
return err
}

c := newSkillClient(cmd.Context())
result, err := c.Upgrade(cmd.Context(), skills.UpgradeOptions{
ProjectRoot: projectRoot,
Names: args,
Clients: parseSkillInstallClients(skillUpgradeClientsRaw),
Preview: skillUpgradePreview,
FailOnChanges: skillUpgradeFailOnChanges,
AllowRefChange: skillUpgradeAllowRefChange,
})
if err != nil {
return formatSkillError("upgrade skills", err)
}

return printUpgradeResult(result, skillUpgradeFormat)
}

func printUpgradeResult(result *skills.UpgradeResult, format string) error {
if format == FormatJSON {
data, err := json.MarshalIndent(result, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal JSON: %w", err)
}
fmt.Println(string(data))
return nil
}

if len(result.Outcomes) == 0 {
fmt.Println("No skills in the project's lock file")
return nil
}
for _, o := range result.Outcomes {
switch o.Status {
case skills.UpgradeStatusUpgraded:
fmt.Printf("%s: upgraded %s -> %s\n", o.Name, o.OldDigest, o.NewDigest)
case skills.UpgradeStatusUpToDate:
fmt.Printf("%s: up to date\n", o.Name)
case skills.UpgradeStatusNotUpgradable:
fmt.Printf("%s: not upgradable (pinned to an immutable reference)\n", o.Name)
case skills.UpgradeStatusRefChangeBlocked:
fmt.Printf("%s: reference change blocked (would move to %s; use --allow-ref-change)\n", o.Name, o.NewResolvedReference)
case skills.UpgradeStatusFailed:
fmt.Printf("%s: failed [%s]: %s\n", o.Name, o.Reason, o.Error)
}
}
return nil
}
1 change: 1 addition & 0 deletions docs/cli/thv_skill.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 54 additions & 0 deletions docs/cli/thv_skill_upgrade.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

193 changes: 192 additions & 1 deletion docs/server/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading