Skip to content

Commit beaf710

Browse files
committed
Adding Support for skills.sh
1 parent ba62333 commit beaf710

File tree

11 files changed

+761
-19
lines changed

11 files changed

+761
-19
lines changed

commands/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ var AddCommands AddCommandsFN = func(root *cobra.Command) {
3434
AddKoolPreset(root)
3535
AddKoolRestart(root)
3636
AddKoolRun(root)
37+
AddKoolScripts(root)
3738
AddKoolSelfUpdate(root)
3839
AddKoolShare(root)
3940
AddKoolStart(root)

commands/scripts.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package commands
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"kool-dev/kool/core/environment"
7+
"kool-dev/kool/core/parser"
8+
"path"
9+
10+
"github.com/spf13/cobra"
11+
)
12+
13+
// KoolScriptsFlags holds the flags for the scripts command
14+
type KoolScriptsFlags struct {
15+
JSON bool
16+
}
17+
18+
// KoolScripts holds handlers and functions to implement the scripts command logic
19+
type KoolScripts struct {
20+
DefaultKoolService
21+
Flags *KoolScriptsFlags
22+
parser parser.Parser
23+
env environment.EnvStorage
24+
}
25+
26+
func AddKoolScripts(root *cobra.Command) {
27+
var (
28+
scripts = NewKoolScripts()
29+
scriptsCmd = NewScriptsCommand(scripts)
30+
)
31+
32+
root.AddCommand(scriptsCmd)
33+
}
34+
35+
// NewKoolScripts creates a new handler for scripts logic
36+
func NewKoolScripts() *KoolScripts {
37+
return &KoolScripts{
38+
*newDefaultKoolService(),
39+
&KoolScriptsFlags{},
40+
parser.NewParser(),
41+
environment.NewEnvStorage(),
42+
}
43+
}
44+
45+
// Execute runs the scripts logic with incoming arguments.
46+
func (s *KoolScripts) Execute(args []string) (err error) {
47+
var filter string
48+
if len(args) > 0 {
49+
filter = args[0]
50+
}
51+
52+
cwdErr := s.parser.AddLookupPath(s.env.Get("PWD"))
53+
homeErr := s.parser.AddLookupPath(path.Join(s.env.Get("HOME"), "kool"))
54+
55+
if isKoolYmlNotFound(cwdErr) && isKoolYmlNotFound(homeErr) {
56+
if s.Flags.JSON {
57+
return s.printJSON([]parser.ScriptDetail{})
58+
}
59+
s.Shell().Warning("No kool.yml found in current directory or ~/kool.")
60+
return nil
61+
}
62+
63+
if err = firstLookupError(cwdErr, homeErr); err != nil {
64+
return
65+
}
66+
67+
if s.Flags.JSON {
68+
var details []parser.ScriptDetail
69+
if details, err = s.parser.ParseAvailableScriptsDetails(filter); err != nil {
70+
return
71+
}
72+
return s.printJSON(details)
73+
}
74+
75+
var scripts []string
76+
if scripts, err = s.parser.ParseAvailableScripts(filter); err != nil {
77+
return
78+
}
79+
80+
if len(scripts) == 0 {
81+
if filter == "" {
82+
s.Shell().Warning("No scripts found.")
83+
} else {
84+
s.Shell().Warning("No scripts found with prefix:", filter)
85+
}
86+
return nil
87+
}
88+
89+
s.Shell().Info("Available scripts:")
90+
for _, script := range scripts {
91+
s.Shell().Println(" " + script)
92+
}
93+
94+
return
95+
}
96+
97+
// NewScriptsCommand initializes new kool scripts command
98+
func NewScriptsCommand(scripts *KoolScripts) *cobra.Command {
99+
cmd := &cobra.Command{
100+
Use: "scripts [FILTER]",
101+
Short: "List scripts defined in kool.yml",
102+
Long: `List the scripts defined in kool.yml or kool.yaml in the current
103+
working directory and in ~/kool. Use the optional FILTER to show only scripts
104+
that start with a given prefix.`,
105+
Args: cobra.MaximumNArgs(1),
106+
RunE: DefaultCommandRunFunction(scripts),
107+
DisableFlagsInUseLine: true,
108+
}
109+
110+
cmd.Flags().BoolVar(&scripts.Flags.JSON, "json", false, "Output scripts as JSON")
111+
112+
return cmd
113+
}
114+
115+
func isKoolYmlNotFound(err error) bool {
116+
return errors.Is(err, parser.ErrKoolYmlNotFound)
117+
}
118+
119+
func firstLookupError(cwdErr, homeErr error) error {
120+
if cwdErr != nil && !isKoolYmlNotFound(cwdErr) {
121+
return cwdErr
122+
}
123+
124+
if homeErr != nil && !isKoolYmlNotFound(homeErr) {
125+
return homeErr
126+
}
127+
128+
return nil
129+
}
130+
131+
func (s *KoolScripts) printJSON(details []parser.ScriptDetail) (err error) {
132+
if details == nil {
133+
details = []parser.ScriptDetail{}
134+
}
135+
136+
for i := range details {
137+
if details[i].Comments == nil {
138+
details[i].Comments = []string{}
139+
}
140+
if details[i].Commands == nil {
141+
details[i].Commands = []string{}
142+
}
143+
}
144+
145+
var payload []byte
146+
if payload, err = json.Marshal(details); err != nil {
147+
return
148+
}
149+
150+
s.Shell().Println(string(payload))
151+
return nil
152+
}

commands/scripts_test.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package commands
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"kool-dev/kool/core/environment"
8+
"kool-dev/kool/core/parser"
9+
"kool-dev/kool/core/shell"
10+
"strings"
11+
"testing"
12+
)
13+
14+
func newFakeKoolScripts(mockScripts []string, mockParseErr error) *KoolScripts {
15+
var details []parser.ScriptDetail
16+
for _, script := range mockScripts {
17+
details = append(details, parser.ScriptDetail{Name: script, Comments: []string{}, Commands: []string{}})
18+
}
19+
return &KoolScripts{
20+
*(newDefaultKoolService().Fake()),
21+
&KoolScriptsFlags{},
22+
&parser.FakeParser{
23+
MockScripts: mockScripts,
24+
MockScriptDetails: details,
25+
MockParseAvailableScriptsError: mockParseErr,
26+
},
27+
environment.NewFakeEnvStorage(),
28+
}
29+
}
30+
31+
func TestScriptsCommandListsScripts(t *testing.T) {
32+
f := newFakeKoolScripts([]string{"setup", "lint"}, nil)
33+
cmd := NewScriptsCommand(f)
34+
cmd.SetArgs([]string{})
35+
36+
if err := cmd.Execute(); err != nil {
37+
t.Errorf("unexpected error executing scripts command; error: %v", err)
38+
}
39+
40+
if !f.parser.(*parser.FakeParser).CalledParseAvailableScripts {
41+
t.Errorf("did not call ParseAvailableScripts")
42+
}
43+
44+
fakeShell := f.shell.(*shell.FakeShell)
45+
46+
if !containsLine(fakeShell.OutLines, "setup") || !containsLine(fakeShell.OutLines, "lint") {
47+
t.Errorf("missing scripts on output: %v", fakeShell.OutLines)
48+
}
49+
}
50+
51+
func TestScriptsCommandFiltersScripts(t *testing.T) {
52+
f := newFakeKoolScripts([]string{"setup", "lint"}, nil)
53+
cmd := NewScriptsCommand(f)
54+
55+
cmd.SetArgs([]string{"se"})
56+
57+
if err := cmd.Execute(); err != nil {
58+
t.Errorf("unexpected error executing scripts command; error: %v", err)
59+
}
60+
61+
fakeShell := f.shell.(*shell.FakeShell)
62+
63+
if containsLine(fakeShell.OutLines, "lint") {
64+
t.Errorf("unexpected script on output: %v", fakeShell.OutLines)
65+
}
66+
67+
if !containsLine(fakeShell.OutLines, "setup") {
68+
t.Errorf("missing filtered script on output: %v", fakeShell.OutLines)
69+
}
70+
}
71+
72+
func TestScriptsCommandNoScripts(t *testing.T) {
73+
f := newFakeKoolScripts([]string{}, nil)
74+
cmd := NewScriptsCommand(f)
75+
cmd.SetArgs([]string{})
76+
77+
if err := cmd.Execute(); err != nil {
78+
t.Errorf("unexpected error executing scripts command; error: %v", err)
79+
}
80+
81+
fakeShell := f.shell.(*shell.FakeShell)
82+
83+
if !fakeShell.CalledWarning {
84+
t.Errorf("did not warn about missing scripts")
85+
}
86+
87+
if !strings.Contains(fmt.Sprint(fakeShell.WarningOutput...), "No scripts found") {
88+
t.Errorf("unexpected warning output: %v", fakeShell.WarningOutput)
89+
}
90+
}
91+
92+
func TestScriptsCommandParseError(t *testing.T) {
93+
f := newFakeKoolScripts([]string{"setup"}, errors.New("parse error"))
94+
cmd := NewScriptsCommand(f)
95+
cmd.SetArgs([]string{})
96+
97+
assertExecGotError(t, cmd, "parse error")
98+
}
99+
100+
func TestScriptsCommandJsonOutput(t *testing.T) {
101+
parserMock := &parser.FakeParser{
102+
MockScriptDetails: []parser.ScriptDetail{
103+
{
104+
Name: "setup",
105+
Comments: []string{"Sets up dependencies"},
106+
Commands: []string{"kool run composer install"},
107+
},
108+
{
109+
Name: "lint",
110+
Comments: []string{},
111+
Commands: []string{"kool run go:linux fmt ./..."},
112+
},
113+
},
114+
}
115+
116+
f := newFakeKoolScripts([]string{}, nil)
117+
f.parser = parserMock
118+
cmd := NewScriptsCommand(f)
119+
cmd.SetArgs([]string{"--json"})
120+
121+
if err := cmd.Execute(); err != nil {
122+
t.Errorf("unexpected error executing scripts command; error: %v", err)
123+
}
124+
125+
fakeShell := f.shell.(*shell.FakeShell)
126+
127+
if len(fakeShell.OutLines) == 0 {
128+
t.Errorf("expected JSON output")
129+
return
130+
}
131+
132+
var output []parser.ScriptDetail
133+
if err := json.Unmarshal([]byte(fakeShell.OutLines[0]), &output); err != nil {
134+
t.Fatalf("failed to parse json output: %v", err)
135+
}
136+
137+
if len(output) != 2 {
138+
t.Fatalf("expected 2 script entries, got %d", len(output))
139+
}
140+
141+
if output[0].Name != "lint" || output[1].Name != "setup" {
142+
t.Errorf("unexpected scripts order or names: %v", output)
143+
}
144+
}
145+
146+
func containsLine(lines []string, match string) bool {
147+
for _, line := range lines {
148+
if strings.Contains(line, match) {
149+
return true
150+
}
151+
}
152+
153+
return false
154+
}

core/parser/fake_parser.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package parser
22

33
import (
44
"kool-dev/kool/core/builder"
5+
"sort"
56
"strings"
67
)
78

@@ -11,10 +12,13 @@ type FakeParser struct {
1112
TargetFiles []string
1213
CalledParse bool
1314
CalledParseAvailableScripts bool
15+
CalledParseAvailableDetails bool
1416
MockParsedCommands map[string][]builder.Command
1517
MockParseError map[string]error
1618
MockScripts []string
19+
MockScriptDetails []ScriptDetail
1720
MockParseAvailableScriptsError error
21+
MockParseAvailableDetailsError error
1822
}
1923

2024
// AddLookupPath implements fake AddLookupPath behavior
@@ -49,3 +53,27 @@ func (f *FakeParser) ParseAvailableScripts(filter string) (scripts []string, err
4953
err = f.MockParseAvailableScriptsError
5054
return
5155
}
56+
57+
// ParseAvailableScriptsDetails implements fake ParseAvailableScriptsDetails behavior
58+
func (f *FakeParser) ParseAvailableScriptsDetails(filter string) (details []ScriptDetail, err error) {
59+
f.CalledParseAvailableDetails = true
60+
61+
if filter == "" {
62+
details = append(details, f.MockScriptDetails...)
63+
} else {
64+
for _, detail := range f.MockScriptDetails {
65+
if strings.HasPrefix(detail.Name, filter) {
66+
details = append(details, detail)
67+
}
68+
}
69+
}
70+
71+
if len(details) > 1 {
72+
sort.Slice(details, func(i, j int) bool {
73+
return details[i].Name < details[j].Name
74+
})
75+
}
76+
77+
err = f.MockParseAvailableDetailsError
78+
return
79+
}

0 commit comments

Comments
 (0)