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(forge): Devx CLI command WIP Feature Branch #69

Draft
wants to merge 27 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
88f00ca
feat: initial command registration
apskhem Oct 3, 2024
f0bff4f
feat: extractor function
apskhem Oct 3, 2024
bd7ffe3
feat: complete parsing
apskhem Oct 3, 2024
4dd7024
feat: id formatter
apskhem Oct 3, 2024
4a9e746
feat: process cmd
apskhem Oct 4, 2024
7319379
feat: executable
apskhem Oct 4, 2024
983dd76
Merge branch 'master' into feat/initial-devx
apskhem Oct 4, 2024
1ea1931
feat: stream output
apskhem Oct 4, 2024
2f2b52a
chore: msg
apskhem Oct 4, 2024
add3272
Merge branch 'master' into feat/initial-devx
apskhem Oct 7, 2024
65cf45b
chore: sync main
apskhem Oct 7, 2024
118c7e0
feat: check available command
apskhem Oct 7, 2024
8448731
chore: move test devx to testdata
apskhem Oct 7, 2024
9999874
refactor: move utils
apskhem Oct 8, 2024
73e5ea5
refactor: restructure
apskhem Oct 8, 2024
2b3322c
chore: remove comments
apskhem Oct 8, 2024
75f0b52
refactor: using executor
apskhem Oct 8, 2024
d9d9b2b
refactor: command executor matching
apskhem Oct 8, 2024
8de6e57
test: for devx
apskhem Oct 8, 2024
e88faa3
Merge pull request #63 from input-output-hk/feat/initial-devx
apskhem Oct 9, 2024
5b1a747
Merge branch 'master' into feat/devx
stevenj Oct 9, 2024
7678bf6
Merge branch 'master' into feat/devx
stevenj Oct 15, 2024
78ddb00
Merge branch 'master' into feat/devx
stevenj Oct 21, 2024
1f51099
docs: Add example Developer.md for further work
stevenj Oct 21, 2024
6fabc15
Merge branch 'master' into feat/devx
apskhem Oct 22, 2024
3668e0a
fix: sync interface
apskhem Oct 22, 2024
8b9b041
Merge pull request #80 from input-output-hk/feat/devx-example-develop…
stevenj Oct 22, 2024
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
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 @@ -26,6 +26,7 @@ var cli struct {

Deploy cmds.DeployCmd `kong:"cmd" help:"Deploy a project."`
Dump cmds.DumpCmd `kong:"cmd" help:"Dumps a project's blueprint to JSON."`
Devx cmds.DevX `kong:"cmd" help:"Reads a forge markdown file and executes a command."`
CI cmds.CICmd `kong:"cmd" help:"Simulate a CI run."`
Release cmds.ReleaseCmd `kong:"cmd" help:"Release a project."`
Run cmds.RunCmd `kong:"cmd" help:"Run an Earthly target."`
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 @@ -80,6 +80,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 @@ -208,6 +208,8 @@ github.com/willabides/kongplete v0.4.0/go.mod h1:0P0jtWD9aTsqPSUAl4de35DLghrr57X
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
}
Loading
Loading