From beaf710949f22bdba4aa3c36b6ef5cff55a52593 Mon Sep 17 00:00:00 2001 From: dbpolito Date: Wed, 28 Jan 2026 14:08:16 -0300 Subject: [PATCH 1/7] Adding Support for skills.sh --- commands/root.go | 1 + commands/scripts.go | 152 ++++++++++++++++ commands/scripts_test.go | 154 ++++++++++++++++ core/parser/fake_parser.go | 28 +++ core/parser/parser.go | 50 +++++- core/parser/parser_test.go | 31 ++++ core/parser/yml.go | 196 ++++++++++++++++++++- core/parser/yml_test.go | 94 ++++++++-- docs/05-Commands-Reference/0-kool.md | 2 +- docs/05-Commands-Reference/kool-scripts.md | 33 ++++ skills/kool/SKILL.md | 39 ++++ 11 files changed, 761 insertions(+), 19 deletions(-) create mode 100644 commands/scripts.go create mode 100644 commands/scripts_test.go create mode 100644 docs/05-Commands-Reference/kool-scripts.md create mode 100644 skills/kool/SKILL.md diff --git a/commands/root.go b/commands/root.go index 7cbd52a2..054eda0c 100644 --- a/commands/root.go +++ b/commands/root.go @@ -34,6 +34,7 @@ var AddCommands AddCommandsFN = func(root *cobra.Command) { AddKoolPreset(root) AddKoolRestart(root) AddKoolRun(root) + AddKoolScripts(root) AddKoolSelfUpdate(root) AddKoolShare(root) AddKoolStart(root) diff --git a/commands/scripts.go b/commands/scripts.go new file mode 100644 index 00000000..23946f98 --- /dev/null +++ b/commands/scripts.go @@ -0,0 +1,152 @@ +package commands + +import ( + "encoding/json" + "errors" + "kool-dev/kool/core/environment" + "kool-dev/kool/core/parser" + "path" + + "github.com/spf13/cobra" +) + +// KoolScriptsFlags holds the flags for the scripts command +type KoolScriptsFlags struct { + JSON bool +} + +// KoolScripts holds handlers and functions to implement the scripts command logic +type KoolScripts struct { + DefaultKoolService + Flags *KoolScriptsFlags + parser parser.Parser + env environment.EnvStorage +} + +func AddKoolScripts(root *cobra.Command) { + var ( + scripts = NewKoolScripts() + scriptsCmd = NewScriptsCommand(scripts) + ) + + root.AddCommand(scriptsCmd) +} + +// NewKoolScripts creates a new handler for scripts logic +func NewKoolScripts() *KoolScripts { + return &KoolScripts{ + *newDefaultKoolService(), + &KoolScriptsFlags{}, + parser.NewParser(), + environment.NewEnvStorage(), + } +} + +// Execute runs the scripts logic with incoming arguments. +func (s *KoolScripts) Execute(args []string) (err error) { + var filter string + if len(args) > 0 { + filter = args[0] + } + + cwdErr := s.parser.AddLookupPath(s.env.Get("PWD")) + homeErr := s.parser.AddLookupPath(path.Join(s.env.Get("HOME"), "kool")) + + if isKoolYmlNotFound(cwdErr) && isKoolYmlNotFound(homeErr) { + if s.Flags.JSON { + return s.printJSON([]parser.ScriptDetail{}) + } + s.Shell().Warning("No kool.yml found in current directory or ~/kool.") + return nil + } + + if err = firstLookupError(cwdErr, homeErr); err != nil { + return + } + + if s.Flags.JSON { + var details []parser.ScriptDetail + if details, err = s.parser.ParseAvailableScriptsDetails(filter); err != nil { + return + } + return s.printJSON(details) + } + + var scripts []string + if scripts, err = s.parser.ParseAvailableScripts(filter); err != nil { + return + } + + if len(scripts) == 0 { + if filter == "" { + s.Shell().Warning("No scripts found.") + } else { + s.Shell().Warning("No scripts found with prefix:", filter) + } + return nil + } + + s.Shell().Info("Available scripts:") + for _, script := range scripts { + s.Shell().Println(" " + script) + } + + return +} + +// NewScriptsCommand initializes new kool scripts command +func NewScriptsCommand(scripts *KoolScripts) *cobra.Command { + cmd := &cobra.Command{ + Use: "scripts [FILTER]", + Short: "List scripts defined in kool.yml", + Long: `List the scripts defined in kool.yml or kool.yaml in the current +working directory and in ~/kool. Use the optional FILTER to show only scripts +that start with a given prefix.`, + Args: cobra.MaximumNArgs(1), + RunE: DefaultCommandRunFunction(scripts), + DisableFlagsInUseLine: true, + } + + cmd.Flags().BoolVar(&scripts.Flags.JSON, "json", false, "Output scripts as JSON") + + return cmd +} + +func isKoolYmlNotFound(err error) bool { + return errors.Is(err, parser.ErrKoolYmlNotFound) +} + +func firstLookupError(cwdErr, homeErr error) error { + if cwdErr != nil && !isKoolYmlNotFound(cwdErr) { + return cwdErr + } + + if homeErr != nil && !isKoolYmlNotFound(homeErr) { + return homeErr + } + + return nil +} + +func (s *KoolScripts) printJSON(details []parser.ScriptDetail) (err error) { + if details == nil { + details = []parser.ScriptDetail{} + } + + for i := range details { + if details[i].Comments == nil { + details[i].Comments = []string{} + } + if details[i].Commands == nil { + details[i].Commands = []string{} + } + } + + var payload []byte + if payload, err = json.Marshal(details); err != nil { + return + } + + s.Shell().Println(string(payload)) + return nil +} diff --git a/commands/scripts_test.go b/commands/scripts_test.go new file mode 100644 index 00000000..94629b33 --- /dev/null +++ b/commands/scripts_test.go @@ -0,0 +1,154 @@ +package commands + +import ( + "encoding/json" + "errors" + "fmt" + "kool-dev/kool/core/environment" + "kool-dev/kool/core/parser" + "kool-dev/kool/core/shell" + "strings" + "testing" +) + +func newFakeKoolScripts(mockScripts []string, mockParseErr error) *KoolScripts { + var details []parser.ScriptDetail + for _, script := range mockScripts { + details = append(details, parser.ScriptDetail{Name: script, Comments: []string{}, Commands: []string{}}) + } + return &KoolScripts{ + *(newDefaultKoolService().Fake()), + &KoolScriptsFlags{}, + &parser.FakeParser{ + MockScripts: mockScripts, + MockScriptDetails: details, + MockParseAvailableScriptsError: mockParseErr, + }, + environment.NewFakeEnvStorage(), + } +} + +func TestScriptsCommandListsScripts(t *testing.T) { + f := newFakeKoolScripts([]string{"setup", "lint"}, nil) + cmd := NewScriptsCommand(f) + cmd.SetArgs([]string{}) + + if err := cmd.Execute(); err != nil { + t.Errorf("unexpected error executing scripts command; error: %v", err) + } + + if !f.parser.(*parser.FakeParser).CalledParseAvailableScripts { + t.Errorf("did not call ParseAvailableScripts") + } + + fakeShell := f.shell.(*shell.FakeShell) + + if !containsLine(fakeShell.OutLines, "setup") || !containsLine(fakeShell.OutLines, "lint") { + t.Errorf("missing scripts on output: %v", fakeShell.OutLines) + } +} + +func TestScriptsCommandFiltersScripts(t *testing.T) { + f := newFakeKoolScripts([]string{"setup", "lint"}, nil) + cmd := NewScriptsCommand(f) + + cmd.SetArgs([]string{"se"}) + + if err := cmd.Execute(); err != nil { + t.Errorf("unexpected error executing scripts command; error: %v", err) + } + + fakeShell := f.shell.(*shell.FakeShell) + + if containsLine(fakeShell.OutLines, "lint") { + t.Errorf("unexpected script on output: %v", fakeShell.OutLines) + } + + if !containsLine(fakeShell.OutLines, "setup") { + t.Errorf("missing filtered script on output: %v", fakeShell.OutLines) + } +} + +func TestScriptsCommandNoScripts(t *testing.T) { + f := newFakeKoolScripts([]string{}, nil) + cmd := NewScriptsCommand(f) + cmd.SetArgs([]string{}) + + if err := cmd.Execute(); err != nil { + t.Errorf("unexpected error executing scripts command; error: %v", err) + } + + fakeShell := f.shell.(*shell.FakeShell) + + if !fakeShell.CalledWarning { + t.Errorf("did not warn about missing scripts") + } + + if !strings.Contains(fmt.Sprint(fakeShell.WarningOutput...), "No scripts found") { + t.Errorf("unexpected warning output: %v", fakeShell.WarningOutput) + } +} + +func TestScriptsCommandParseError(t *testing.T) { + f := newFakeKoolScripts([]string{"setup"}, errors.New("parse error")) + cmd := NewScriptsCommand(f) + cmd.SetArgs([]string{}) + + assertExecGotError(t, cmd, "parse error") +} + +func TestScriptsCommandJsonOutput(t *testing.T) { + parserMock := &parser.FakeParser{ + MockScriptDetails: []parser.ScriptDetail{ + { + Name: "setup", + Comments: []string{"Sets up dependencies"}, + Commands: []string{"kool run composer install"}, + }, + { + Name: "lint", + Comments: []string{}, + Commands: []string{"kool run go:linux fmt ./..."}, + }, + }, + } + + f := newFakeKoolScripts([]string{}, nil) + f.parser = parserMock + cmd := NewScriptsCommand(f) + cmd.SetArgs([]string{"--json"}) + + if err := cmd.Execute(); err != nil { + t.Errorf("unexpected error executing scripts command; error: %v", err) + } + + fakeShell := f.shell.(*shell.FakeShell) + + if len(fakeShell.OutLines) == 0 { + t.Errorf("expected JSON output") + return + } + + var output []parser.ScriptDetail + if err := json.Unmarshal([]byte(fakeShell.OutLines[0]), &output); err != nil { + t.Fatalf("failed to parse json output: %v", err) + } + + if len(output) != 2 { + t.Fatalf("expected 2 script entries, got %d", len(output)) + } + + if output[0].Name != "lint" || output[1].Name != "setup" { + t.Errorf("unexpected scripts order or names: %v", output) + } +} + +func containsLine(lines []string, match string) bool { + for _, line := range lines { + if strings.Contains(line, match) { + return true + } + } + + return false +} diff --git a/core/parser/fake_parser.go b/core/parser/fake_parser.go index a6f8c9c1..ae10bd4d 100644 --- a/core/parser/fake_parser.go +++ b/core/parser/fake_parser.go @@ -2,6 +2,7 @@ package parser import ( "kool-dev/kool/core/builder" + "sort" "strings" ) @@ -11,10 +12,13 @@ type FakeParser struct { TargetFiles []string CalledParse bool CalledParseAvailableScripts bool + CalledParseAvailableDetails bool MockParsedCommands map[string][]builder.Command MockParseError map[string]error MockScripts []string + MockScriptDetails []ScriptDetail MockParseAvailableScriptsError error + MockParseAvailableDetailsError error } // AddLookupPath implements fake AddLookupPath behavior @@ -49,3 +53,27 @@ func (f *FakeParser) ParseAvailableScripts(filter string) (scripts []string, err err = f.MockParseAvailableScriptsError return } + +// ParseAvailableScriptsDetails implements fake ParseAvailableScriptsDetails behavior +func (f *FakeParser) ParseAvailableScriptsDetails(filter string) (details []ScriptDetail, err error) { + f.CalledParseAvailableDetails = true + + if filter == "" { + details = append(details, f.MockScriptDetails...) + } else { + for _, detail := range f.MockScriptDetails { + if strings.HasPrefix(detail.Name, filter) { + details = append(details, detail) + } + } + } + + if len(details) > 1 { + sort.Slice(details, func(i, j int) bool { + return details[i].Name < details[j].Name + }) + } + + err = f.MockParseAvailableDetailsError + return +} diff --git a/core/parser/parser.go b/core/parser/parser.go index 6ef49ef3..b0dbe6de 100644 --- a/core/parser/parser.go +++ b/core/parser/parser.go @@ -15,6 +15,7 @@ type Parser interface { AddLookupPath(string) error Parse(string) ([]builder.Command, error) ParseAvailableScripts(string) ([]string, error) + ParseAvailableScriptsDetails(string) ([]ScriptDetail, error) } // DefaultParser implements all default behavior for using kool.yml files. @@ -77,7 +78,7 @@ func (p *DefaultParser) Parse(script string) (commands []builder.Command, err er } for _, koolFile = range p.targetFiles { - if parsedFile, err = ParseKoolYaml(koolFile); err != nil { + if parsedFile, err = ParseKoolYamlWithDetails(koolFile); err != nil { return } @@ -142,3 +143,50 @@ func (p *DefaultParser) ParseAvailableScripts(filter string) (scripts []string, return } + +// ParseAvailableScriptsDetails parses all available scripts with details +func (p *DefaultParser) ParseAvailableScriptsDetails(filter string) (details []ScriptDetail, err error) { + var ( + koolFile string + parsedFile *KoolYaml + found map[string]ScriptDetail + keys []string + ) + + if len(p.targetFiles) == 0 { + err = errors.New("kool.yml not found") + return + } + + found = make(map[string]ScriptDetail) + + for _, koolFile = range p.targetFiles { + if parsedFile, err = ParseKoolYamlWithDetails(koolFile); err != nil { + return + } + + for name, detail := range parsedFile.ScriptDetails { + if _, exists := found[name]; exists { + continue + } + if filter != "" && !strings.HasPrefix(name, filter) { + continue + } + found[name] = detail + } + } + + for name := range found { + keys = append(keys, name) + } + if len(keys) == 0 { + return + } + + sort.Strings(keys) + for _, name := range keys { + details = append(details, found[name]) + } + + return +} diff --git a/core/parser/parser_test.go b/core/parser/parser_test.go index e1aad525..742a7800 100644 --- a/core/parser/parser_test.go +++ b/core/parser/parser_test.go @@ -150,3 +150,34 @@ func TestParserParseAvailableScriptsFilter(t *testing.T) { t.Error("failed to get filtered scripts from kool.yml") } } + +func TestParserParseAvailableScriptsDetails(t *testing.T) { + var ( + p Parser = NewParser() + details []ScriptDetail + err error + ) + + if _, err = p.ParseAvailableScriptsDetails(""); err == nil { + t.Error("expecting 'kool.yml not found' error, got none") + } + + if err != nil && err.Error() != "kool.yml not found" { + t.Errorf("expecting error 'kool.yml not found', got '%s'", err.Error()) + } + + workDir, _ := os.Getwd() + _ = p.AddLookupPath(path.Join(workDir, "testing_files")) + + if details, err = p.ParseAvailableScriptsDetails(""); err != nil { + t.Errorf("unexpected error; error: %s", err) + } + + if len(details) != 1 || details[0].Name != "testing" { + t.Error("failed to get script details from kool.yml") + } + + if len(details[0].Commands) != 1 || details[0].Commands[0] != "echo testing" { + t.Errorf("unexpected command details: %v", details[0].Commands) + } +} diff --git a/core/parser/yml.go b/core/parser/yml.go index bd27f575..9bbff76d 100644 --- a/core/parser/yml.go +++ b/core/parser/yml.go @@ -5,9 +5,10 @@ import ( "io" "kool-dev/kool/core/builder" "os" + "strings" "github.com/agnivade/levenshtein" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) // SimilarThreshold represents the minimal Levenshteindistance of two @@ -18,7 +19,15 @@ type yamlMarshalFnType func(interface{}) ([]byte, error) // KoolYaml holds the structure for parsing the custom commands file type KoolYaml struct { - Scripts map[string]interface{} `yaml:"scripts"` + Scripts map[string]interface{} `yaml:"scripts"` + ScriptDetails map[string]ScriptDetail `yaml:"-"` +} + +// ScriptDetail describes a kool.yml script with context +type ScriptDetail struct { + Name string `json:"name"` + Comments []string `json:"comments"` + Commands []string `json:"commands"` } // KoolYamlParser holds logic for handling kool yaml @@ -51,19 +60,53 @@ func ParseKoolYaml(filePath string) (parsed *KoolYaml, err error) { } parsed = new(KoolYaml) - err = yaml.Unmarshal(raw, parsed) + if err = yaml.Unmarshal(raw, parsed); err != nil { + return + } return } +// ParseKoolYamlWithDetails decodes the target kool.yml and includes script details. +func ParseKoolYamlWithDetails(filePath string) (parsed *KoolYaml, err error) { + var ( + file *os.File + raw []byte + root yaml.Node + ) + + if file, err = os.OpenFile(filePath, os.O_RDONLY, os.ModePerm); err != nil { + return + } + + defer file.Close() + + if raw, err = io.ReadAll(file); err != nil { + return + } + + parsed = new(KoolYaml) + if err = yaml.Unmarshal(raw, parsed); err != nil { + return + } + + if err = yaml.Unmarshal(raw, &root); err != nil { + return + } + + parsed.ScriptDetails = parseScriptDetails(&root) + return +} + // Parse decodes the target kool.yml func (y *KoolYaml) Parse(filePath string) (err error) { var parsed *KoolYaml - if parsed, err = ParseKoolYaml(filePath); err != nil { + if parsed, err = ParseKoolYamlWithDetails(filePath); err != nil { return } y.Scripts = parsed.Scripts + y.ScriptDetails = parsed.ScriptDetails return } @@ -95,6 +138,7 @@ func (y *KoolYaml) ParseCommands(script string) (commands []builder.Command, err isList bool line string lines []interface{} + linesStr []string command *builder.DefaultCommand ) @@ -104,9 +148,22 @@ func (y *KoolYaml) ParseCommands(script string) (commands []builder.Command, err } commands = append(commands, command) + } else if linesStr, isList = y.Scripts[script].([]string); isList { + for _, line := range linesStr { + if command, err = builder.ParseCommand(line); err != nil { + return + } + + commands = append(commands, command) + } } else if lines, isList = y.Scripts[script].([]interface{}); isList { for _, i := range lines { - if command, err = builder.ParseCommand(i.(string)); err != nil { + var lineStr string + if lineStr, isSingle = i.(string); !isSingle { + err = fmt.Errorf("failed parsing script '%s': expected string or array of strings", script) + return + } + if command, err = builder.ParseCommand(lineStr); err != nil { return } @@ -128,6 +185,18 @@ func (y *KoolYaml) SetScript(key string, commands []string) { y.Scripts = make(map[string]interface{}) } + if y.ScriptDetails == nil { + y.ScriptDetails = make(map[string]ScriptDetail) + } + + currentDetail := y.ScriptDetails[key] + currentDetail.Name = key + currentDetail.Commands = append([]string{}, commands...) + if currentDetail.Comments == nil { + currentDetail.Comments = []string{} + } + y.ScriptDetails[key] = currentDetail + if len(commands) == 1 { y.Scripts[key] = commands[0] } else { @@ -151,3 +220,120 @@ func (y *KoolYaml) String() (content string, err error) { content = string(parsedBytes) return } + +func parseScriptDetails(root *yaml.Node) map[string]ScriptDetail { + result := make(map[string]ScriptDetail) + if root == nil { + return result + } + + scriptsNode := findScriptsNode(root) + if scriptsNode == nil || scriptsNode.Kind != yaml.MappingNode { + return result + } + + for i := 0; i+1 < len(scriptsNode.Content); i += 2 { + keyNode := scriptsNode.Content[i] + valueNode := scriptsNode.Content[i+1] + name := keyNode.Value + detail := ScriptDetail{ + Name: name, + Comments: collectComments(keyNode, valueNode), + Commands: parseCommandsNode(valueNode), + } + if detail.Comments == nil { + detail.Comments = []string{} + } + if detail.Commands == nil { + detail.Commands = []string{} + } + result[name] = detail + } + + return result +} + +func findScriptsNode(root *yaml.Node) *yaml.Node { + current := root + if current.Kind == yaml.DocumentNode && len(current.Content) > 0 { + current = current.Content[0] + } + + if current.Kind != yaml.MappingNode { + return nil + } + + for i := 0; i+1 < len(current.Content); i += 2 { + keyNode := current.Content[i] + valueNode := current.Content[i+1] + if keyNode.Value == "scripts" { + return valueNode + } + } + + return nil +} + +func parseCommandsNode(node *yaml.Node) []string { + if node == nil { + return []string{} + } + + switch node.Kind { + case yaml.ScalarNode: + return []string{node.Value} + case yaml.SequenceNode: + commands := make([]string, 0, len(node.Content)) + for _, item := range node.Content { + if item.Kind == yaml.ScalarNode { + commands = append(commands, item.Value) + } + } + return commands + default: + return []string{} + } +} + +func collectComments(nodes ...*yaml.Node) []string { + var comments []string + for _, node := range nodes { + if node == nil { + continue + } + comments = appendCommentLines(comments, node.HeadComment) + comments = appendCommentLines(comments, node.LineComment) + } + + return comments +} + +func appendCommentLines(comments []string, raw string) []string { + if raw == "" { + return comments + } + + for _, line := range strings.Split(raw, "\n") { + line = strings.TrimSpace(line) + line = strings.TrimPrefix(line, "#") + line = strings.TrimSpace(line) + if line == "" { + continue + } + if !containsString(comments, line) { + comments = append(comments, line) + } + } + + return comments +} + +func containsString(items []string, value string) bool { + for _, item := range items { + if item == value { + return true + } + } + + return false +} diff --git a/core/parser/yml_test.go b/core/parser/yml_test.go index a436c0ee..c1cb149e 100644 --- a/core/parser/yml_test.go +++ b/core/parser/yml_test.go @@ -5,8 +5,9 @@ import ( "kool-dev/kool/core/builder" "os" "path" - "strings" "testing" + + "gopkg.in/yaml.v3" ) const KoolYmlOK = `scripts: @@ -123,17 +124,86 @@ func TestParseKoolYaml(t *testing.T) { return } - expected := `scripts: - multi-line: - - line 1 - - line 2 - new-script: - - new-command 1 - - new-command 2 - single-line: single line script` - - if expected != strings.TrimSpace(koolContent) { - t.Errorf("expecting kool.yml content '%s', got '%s'", expected, strings.TrimSpace(koolContent)) + parsedOutput := new(KoolYaml) + if err = yaml.Unmarshal([]byte(koolContent), parsedOutput); err != nil { + t.Errorf("failed to parse generated kool.yml content; error: %s", err) + return + } + + if len(parsedOutput.Scripts) != 3 { + t.Errorf("expected to parse 3 scripts from generated content; got %d", len(parsedOutput.Scripts)) + return + } + + if !parsedOutput.HasScript("single-line") || !parsedOutput.HasScript("multi-line") || !parsedOutput.HasScript("new-script") { + t.Errorf("expected to have single-line, multi-line and new-script scripts") + return + } + + if cmds, err = parsedOutput.ParseCommands("new-script"); err != nil { + t.Errorf("failed to parse new-script from generated content; error: %s", err) + return + } + + if len(cmds) != 2 { + t.Errorf("expected new-script to parse 2 commands; got %d", len(cmds)) + return + } +} + +func TestParseKoolYamlScriptDetails(t *testing.T) { + const KoolYmlWithComments = `scripts: + # build the app + # and setup + setup: kool run go build + lint: + - kool run go vet + - kool run go fmt +` + + var ( + err error + parsed *KoolYaml + tmp string + ) + + tmp = path.Join(t.TempDir(), "kool.yml") + if err = os.WriteFile(tmp, []byte(KoolYmlWithComments), os.ModePerm); err != nil { + t.Fatal("failed creating temporary file for test", err) + } + + if parsed, err = ParseKoolYamlWithDetails(tmp); err != nil { + t.Fatalf("failed parsing kool.yml with comments; error: %s", err) + } + + setup, ok := parsed.ScriptDetails["setup"] + if !ok { + t.Fatal("expected to find setup script details") + } + + if len(setup.Comments) != 2 { + t.Fatalf("expected 2 comments for setup, got %v", setup.Comments) + } + + if setup.Comments[0] != "build the app" || setup.Comments[1] != "and setup" { + t.Fatalf("unexpected setup comments: %v", setup.Comments) + } + + if setup.Commands == nil || len(setup.Commands) != 1 || setup.Commands[0] != "kool run go build" { + t.Fatalf("unexpected setup commands: %v", setup.Commands) + } + + lint, ok := parsed.ScriptDetails["lint"] + if !ok { + t.Fatal("expected to find lint script details") + } + + if len(lint.Comments) != 0 { + t.Fatalf("expected no comments for lint, got %v", lint.Comments) + } + + if len(lint.Commands) != 2 { + t.Fatalf("expected 2 commands for lint, got %v", lint.Commands) } } diff --git a/docs/05-Commands-Reference/0-kool.md b/docs/05-Commands-Reference/0-kool.md index 1c280d34..596b1eee 100644 --- a/docs/05-Commands-Reference/0-kool.md +++ b/docs/05-Commands-Reference/0-kool.md @@ -34,9 +34,9 @@ kool * [kool recipe](kool-recipe) - Adds configuration for some recipe in the current work directory. * [kool restart](kool-restart) - Restart running service containers (the same as 'kool stop' followed by 'kool start') * [kool run](kool-run) - Execute a script defined in kool.yml +* [kool scripts](kool-scripts) - List scripts defined in kool.yml * [kool self-update](kool-self-update) - Update kool to the latest version * [kool share](kool-share) - Live share your local environment on the Internet using an HTTP tunnel * [kool start](kool-start) - Start service containers defined in docker-compose.yml * [kool status](kool-status) - Show the status of all service containers * [kool stop](kool-stop) - Stop and destroy running service containers - diff --git a/docs/05-Commands-Reference/kool-scripts.md b/docs/05-Commands-Reference/kool-scripts.md new file mode 100644 index 00000000..3abac0fd --- /dev/null +++ b/docs/05-Commands-Reference/kool-scripts.md @@ -0,0 +1,33 @@ +## kool scripts + +List scripts defined in kool.yml + +### Synopsis + +List the scripts defined in kool.yml or kool.yaml in the current working directory +and in ~/kool. Use the optional FILTER to show only scripts that start with a given +prefix. + +``` +kool scripts [FILTER] +``` + +### Options + +``` + -h, --help help for scripts + --json Output scripts as JSON +``` + +When using `--json`, output is an array of objects with `name`, `comments`, and `commands`. + +### Options inherited from parent commands + +``` + --verbose Increases output verbosity + -w, --working_dir string Changes the working directory for the command +``` + +### SEE ALSO + +* [kool](kool) - Cloud native environments made easy diff --git a/skills/kool/SKILL.md b/skills/kool/SKILL.md new file mode 100644 index 00000000..b49b5819 --- /dev/null +++ b/skills/kool/SKILL.md @@ -0,0 +1,39 @@ +--- +name: kool +description: Discovers and runs project scripts with the kool CLI. Use when the user wants to list custom commands from kool.yml, understand project tasks, or run kool scripts. Lists scripts in JSON for agents and reads kool.yml for context. +--- + +# Kool CLI + +Use kool to discover and run project scripts defined in `kool.yml`. + +## Quick Start + +```bash +kool scripts --json +kool run