aboutsummaryrefslogtreecommitdiffstats
path: root/main.go
blob: b1674c63603752aedd8b81e967fc6e356136f205 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/signal"
	"runtime/debug"
	"strings"
	"syscall"

	"github.com/spf13/cobra"
)

// version is set at build time; `go install` builds fall back to the module
// version.
var version = "dev"

func init() {
	if bi, ok := debug.ReadBuildInfo(); ok && version == "dev" && bi.Main.Version != "" && bi.Main.Version != "(devel)" {
		version = bi.Main.Version
	}
}

const docsURL = "https://git.simenandre.no/skills/about/"

type usageError struct{ error }

type options struct {
	manifest string
	quiet    bool
}

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	err := newRoot().ExecuteContext(ctx)
	interrupted := ctx.Err() != nil
	stop()
	if err == nil {
		return
	}
	fmt.Fprintln(os.Stderr, "skills:", err)
	var ue usageError
	switch {
	case interrupted:
		os.Exit(130)
	case errors.As(err, &ue), strings.HasPrefix(err.Error(), "unknown command"):
		os.Exit(2)
	}
	os.Exit(1)
}

func newRoot() *cobra.Command {
	opts := &options{}
	root := &cobra.Command{
		Use:   "skills",
		Short: "Install and update agent skills from git repositories",
		Long: `skills installs agent skills (directories holding a SKILL.md) from git
repositories into Claude Code, Codex and OpenCode, and keeps them updated.

What you add is recorded in a manifest, and the exact commit of each source
in a lock file next to it. Commit both to reproduce the same skills on
another machine with ` + "`skills install`" + `.`,
		Example: `  # Add every skill in a repository
  skills add anthropics/skills

  # Add one skill, from a private repository, at a tag
  skills add git@example.com:team/skills.git#v1.2.0 --skill deploy

  # See what is installed, then move everything to the newest commits
  skills list
  skills update`,
		Version:       version,
		SilenceUsage:  true,
		SilenceErrors: true,
	}
	root.SetFlagErrorFunc(func(_ *cobra.Command, err error) error { return usageError{err} })
	root.PersistentFlags().StringVar(&opts.manifest, "manifest", "",
		"manifest to use (default $SKILLS_MANIFEST, else $XDG_CONFIG_HOME/skills/skills.json)")
	root.PersistentFlags().BoolVarP(&opts.quiet, "quiet", "q", false, "print only errors")
	root.SetHelpTemplate(root.HelpTemplate() + "\nDocumentation and source: " + docsURL + "\n")

	root.AddCommand(addCmd(opts), removeCmd(opts), updateCmd(opts), installCmd(opts), listCmd(opts))
	return root
}

func (o *options) workspace() (*workspace, error) {
	path, err := manifestPath(o.manifest)
	if err != nil {
		return nil, err
	}
	return openWorkspace(path)
}

func (o *options) printf(format string, a ...any) {
	if !o.quiet {
		fmt.Printf(format, a...)
	}
}

// progressf reports slow work on stderr, only to a person watching.
func (o *options) progressf(format string, a ...any) {
	if o.quiet {
		return
	}
	if fi, err := os.Stderr.Stat(); err == nil && fi.Mode()&os.ModeCharDevice != 0 {
		fmt.Fprintf(os.Stderr, format, a...)
	}
}

func exactArgs(n int) cobra.PositionalArgs {
	return func(cmd *cobra.Command, args []string) error {
		if len(args) != n {
			return usageError{fmt.Errorf("%s takes %d argument(s), got %d (see `%s --help`)", cmd.Name(), n, len(args), cmd.CommandPath())}
		}
		return nil
	}
}

func minArgs(n int) cobra.PositionalArgs {
	return func(cmd *cobra.Command, args []string) error {
		if len(args) < n {
			return usageError{fmt.Errorf("%s needs at least %d argument(s) (see `%s --help`)", cmd.Name(), n, cmd.CommandPath())}
		}
		return nil
	}
}