Skip to content

Commit

Permalink
Merge pull request #63 from input-output-hk/feat/initial-devx
Browse files Browse the repository at this point in the history
feat: add initial DevX
  • Loading branch information
apskhem authored Oct 9, 2024
2 parents 7de57df + 8de6e57 commit e88faa3
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 0 deletions.
29 changes: 29 additions & 0 deletions cli/cmd/cmds/devx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package cmds

import (
"fmt"
"log/slog"
"os"

"github.com/input-output-hk/catalyst-forge/cli/pkg/command"
"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 {
raw, err := os.ReadFile(c.MarkdownPath)
if err != nil {
return fmt.Errorf("could not read file at %s: %v", c.MarkdownPath, err)
}

prog, err := command.ExtractDevXMarkdown(raw)
if err != nil {
return err
}

return prog.ProcessCmd(c.CommandName, logger)
}
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
6 changes: 6 additions & 0 deletions cli/cmd/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ func TestScan(t *testing.T) {
})
}

func TestDevX(t *testing.T) {
testscript.Run(t, testscript.Params{
Dir: "testdata/devx",
})
}

func mockEarthly() int {
for _, arg := range os.Args {
fmt.Println(arg)
Expand Down
19 changes: 19 additions & 0 deletions cli/cmd/testdata/devx/1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
exec git init .

exec forge devx ./Developer.md run-some-command
cmp stdout expect.txt

-- Developer.md --
# My cool dev docs

### Run Some Command !!

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

``` sh
echo "should not run this command (1)"
```
-- expect.txt --
should run this command (1)
22 changes: 22 additions & 0 deletions cli/cmd/testdata/devx/Developer.md
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
29 changes: 29 additions & 0 deletions cli/pkg/command/executor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package command

type LanguageExecutor struct {
executor map[string]Executor
}

func NewDefaultLanguageExecutor() LanguageExecutor {
return LanguageExecutor{
executor: map[string]Executor{
"sh": ShellLanguageExecutor{},
},
}
}

type Executor interface {
GetExecutorCommand() string
GetExecutorArgs(content string) []string
}

// shell
type ShellLanguageExecutor struct{}

func (e ShellLanguageExecutor) GetExecutorCommand() string {
return "sh"
}

func (e ShellLanguageExecutor) GetExecutorArgs(content string) []string {
return formatArgs([]string{"-c", "$"}, content)
}
85 changes: 85 additions & 0 deletions cli/pkg/command/program.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package command

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

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

type Program struct {
name string
groups []CommandGroup
}

type CommandGroup struct {
name string
commands []Command
}

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

func (prog *Program) ProcessCmd(cmd string, logger *slog.Logger) error {
var foundCmd *Command
for _, v := range prog.groups {
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 '%s' not found in markdown", cmd)
}

return foundCmd.exec(logger)
}

func (cmd *Command) exec(logger *slog.Logger) error {
if cmd.lang == nil {
return fmt.Errorf("command block without specified language")
}

lang, ok := NewDefaultLanguageExecutor().executor[*cmd.lang]
if !ok {
return fmt.Errorf("only commands running with `sh` can be executed")
}
if _, err := exec.LookPath(lang.GetExecutorCommand()); err != nil {
return fmt.Errorf("command '%s' is unavailable", lang.GetExecutorCommand())
}

localExec := executor.NewLocalExecutor(
logger,
executor.WithRedirect(),
)
_, err := localExec.Execute(lang.GetExecutorCommand(), lang.GetExecutorArgs(cmd.content))

return err
}

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, "-")
}
93 changes: 93 additions & 0 deletions cli/pkg/command/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package command

import (
"bytes"
"errors"
"strings"

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

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

// store the command groups and commands
groups := []CommandGroup{}
var progName *string
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 == 1 {
title := string(heading.Text(data))

progName = &title
}

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")
}
if progName == nil {
return nil, errors.New("no title found in the markdown")
}

prog := Program{
name: *progName,
groups: groups,
}

return &prog, 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
}

0 comments on commit e88faa3

Please sign in to comment.