Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add initial DevX #63

Merged
merged 19 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from 13 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
207 changes: 207 additions & 0 deletions cli/cmd/cmds/devx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package cmds

import (
"bufio"
"bytes"
"errors"
"fmt"
"log/slog"
"os"
"os/exec"
"regexp"
"strings"
"unicode"

"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text"

"github.com/input-output-hk/catalyst-forge/cli/pkg/run"
)

type DevX struct {
MarkdownPath string `arg:"" help:"Path to the markdown file."`
CommandName string `arg:"" help:"Command to be executed."`
}

func (c *DevX) Run(ctx run.RunContext, logger *slog.Logger) error {
// read the file from the specified path
raw, err := os.ReadFile(c.MarkdownPath)
if err != nil {
return fmt.Errorf("could not read file at %s: %v", c.MarkdownPath, err)
}

// parse the file with prepared options
commandGroups, err := extractCommandGroups(raw)
if err != nil {
return err
}

// exec the command
return processCmd(commandGroups, c.CommandName)
}

type command struct {
content string
lang *string
platform *string
}

func (cmd *command) Exec() error {
executorCmd, executorArgs := getLangExecutor(cmd.lang)
if executorCmd == "" {
return fmt.Errorf("only commands running with `sh` can be executed")
}

// check if the command is available
if _, err := exec.LookPath(executorCmd); err != nil {
return fmt.Errorf("command '%s' not found in PATH", executorCmd)
}

// start executing the command
execCmd := exec.Command(executorCmd, formatArgs(executorArgs, cmd.content)...)
apskhem marked this conversation as resolved.
Show resolved Hide resolved

stdout, err := execCmd.StdoutPipe()
if err != nil {
return err
}

if err := execCmd.Start(); err != nil {
return err
}

scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
fmt.Println(scanner.Text())
}

if err := scanner.Err(); err != nil {
fmt.Println("error reading output:", err)
}

if err := execCmd.Wait(); err != nil {
fmt.Println("error waiting for command:", err)
}

return nil
}

type commandGroup struct {
apskhem marked this conversation as resolved.
Show resolved Hide resolved
name string
commands []command
}

func (cg *commandGroup) GetId() string {
var result []rune

for _, char := range cg.name {
if unicode.IsLetter(char) || unicode.IsDigit(char) {
result = append(result, unicode.ToLower(char))
} else if unicode.IsSpace(char) {
result = append(result, '-')
}
}

joined := string(result)

re := regexp.MustCompile(`-+`)
joined = re.ReplaceAllString(joined, "-")

return strings.Trim(joined, "-")
}

func extractCommandGroups(data []byte) ([]commandGroup, error) {
md := goldmark.New()
reader := text.NewReader(data)
doc := md.Parser().Parse(reader)

// store the command groups and commands
groups := []commandGroup{}
var currentPlatform *string

// walk through the ast nodes
ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
// look up for headers
if heading, ok := n.(*ast.Heading); ok && entering {
if heading.Level == 3 {
currentPlatform = nil
commandName := string(heading.Text(data))

groups = append(groups, commandGroup{
name: commandName,
commands: []command{},
})
}

/* if heading.Level == 4 && len(groups) > 0 {
platform := string(heading.Text(data))
currentPlatform = &platform
} */
}

// look up for code blocks
if block, ok := n.(*ast.FencedCodeBlock); ok && entering && len(groups) > 0 {
i := len(groups) - 1
lang := string(block.Language(data))

var buf bytes.Buffer
for i := 0; i < block.Lines().Len(); i++ {
line := block.Lines().At(i)
buf.Write(line.Value(data))
}

groups[i].commands = append(groups[i].commands, command{
content: buf.String(),
lang: &lang,
platform: currentPlatform,
})
}

return ast.WalkContinue, nil
})

if len(groups) == 0 {
return nil, errors.New("no command groups found in the markdown")
}

return groups, nil
}

func processCmd(list []commandGroup, cmd string) error {
var foundCmd *command
for _, v := range list {
if v.GetId() == cmd {
// TODO: should get the fisrt (most specified) command corresponding to the current host platform
foundCmd = &v.commands[0]
}
}

if foundCmd == nil {
return fmt.Errorf("command not found")
}

return foundCmd.Exec()
}

func getLangExecutor(lang *string) (string, []string) {
apskhem marked this conversation as resolved.
Show resolved Hide resolved
if lang == nil {
return "", nil
}

// TODO: get more supported commands
if *lang == "sh" {
return "sh", []string{"-c", "$"}
} else {
return "", nil
}
}

func formatArgs(base []string, replacement string) []string {
replaced := make([]string, len(base))

for i, str := range base {
replaced[i] = strings.ReplaceAll(str, "$", replacement)
}

return replaced
}
1 change: 1 addition & 0 deletions cli/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var cli struct {

Deploy cmds.DeployCmd `cmd:"" help:"Deploy a project."`
Dump cmds.DumpCmd `cmd:"" help:"Dumps a project's blueprint to JSON."`
Devx cmds.DevX `cmd:"" help:"Reads a forge markdown file and executes a command."`
CI cmds.CICmd `cmd:"" help:"Simulate a CI run."`
Run cmds.RunCmd `cmd:"" help:"Run an Earthly target."`
Scan cmds.ScanCmd `cmd:"" help:"Scan for Earthfiles."`
Expand Down
22 changes: 22 additions & 0 deletions cli/cmd/testdata/devx/Developer.md
apskhem marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# My cool dev docs

### Run Some Command !!

``` sh
echo "should run this command (1)"
```

``` sh
echo "should not run this command (1)"
```

### Extra $Cool$ Command

``` sh
echo "should run this command (2)"
echo "should run another command (2)"
```

``` sh
echo "should not run this command (2)"
```
1 change: 1 addition & 0 deletions cli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ require (
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.2.2 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/yuin/goldmark v1.7.4 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.28.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions cli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
Expand Down