diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..d629601 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,50 @@ +version: 2 + +project_name: foundry + +builds: + - main: ./main.go + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm64 + flags: + - -ldflags="-s -w -X main.version={{ .Version }} -X main.commit={{ .Commit }}" + +archives: + - formats: [tar.gz] + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + format_overrides: + - goos: windows + formats: [zip] + files: + - ./* + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + +release: + github: + owner: Nykenik24 + name: foundry + footer: >- + --- + Released by [GoReleaser](https://github.com/goreleaser/goreleaser). + +source: + enabled: true + +sboms: + - artifacts: archive diff --git a/cmd/runs.go b/cmd/runs.go new file mode 100644 index 0000000..10b7e31 --- /dev/null +++ b/cmd/runs.go @@ -0,0 +1,95 @@ +/* +Copyright © 2026 Luca A. nykenik24@proton.me +*/ + +package cmd + +import ( + "fmt" + "time" + + "github.com/Nykenik24/foundry/core/runs" + "github.com/Nykenik24/foundry/core/utils" + "github.com/spf13/cobra" +) + +var runsCmd = &cobra.Command{ + Use: "runs", + Short: "Manage runs", + Run: func(cmd *cobra.Command, args []string) { + cmd.Usage() + }, +} + +var runsAllCmd = &cobra.Command{ + Use: "all", + Short: "List all runs", + Run: func(cmd *cobra.Command, args []string) { + list := runs.RetrieveRuns() + runs.PrintRuns(list, fmt.Sprintf("Found %d run(s)", len(list))) + }, +} + +var runsSuccessfulCmd = &cobra.Command{ + Use: "success", + Short: "List successful runs", + Run: func(cmd *cobra.Command, args []string) { + list := runs.QuerySuccessful(runs.RetrieveRuns()).Result + runs.PrintRuns(list, fmt.Sprintf("Found %d succesful run(s)", len(list))) + }, +} + +var runsFailedCmd = &cobra.Command{ + Use: "failed", + Short: "List failed runs", + Run: func(cmd *cobra.Command, args []string) { + list := runs.QueryFailed(runs.RetrieveRuns()).Result + runs.PrintRuns(list, fmt.Sprintf("Found %d failed run(s)", len(list))) + }, +} + +var runsBeforeCmd = &cobra.Command{ + Use: "before ", + Args: cobra.ExactArgs(1), + + Short: "List runs before a timestamp.", + Long: `List runs before a timestamp. + + Format for timestamp is YEAR-MONTH-DAY HOUR:MINUTE:SECOND`, + Run: func(cmd *cobra.Command, args []string) { + ts, err := time.Parse(time.DateTime, args[0]) + if err != nil { + utils.PrintFatal("Error when parsing timestamp: %v", err) + } + list := runs.QueryBefore(runs.RetrieveRuns(), ts).Result + runs.PrintRuns(list, fmt.Sprintf("Found %d run(s) before %s", len(list), args[0])) + }, +} + +var runsAfterCmd = &cobra.Command{ + Use: "after ", + Args: cobra.ExactArgs(1), + + Short: "List runs after a timestamp.", + Long: `List runs after a timestamp. + + Format for timestamp is YEAR-MONTH-DAY HOUR:MINUTE:SECOND`, + Run: func(cmd *cobra.Command, args []string) { + ts, err := time.Parse(time.DateTime, args[0]) + if err != nil { + utils.PrintFatal("Error when parsing timestamp: %v", err) + } + list := runs.QueryAfter(runs.RetrieveRuns(), ts).Result + runs.PrintRuns(list, fmt.Sprintf("Found %d run(s) after %s", len(list), args[0])) + }, +} + +func init() { + runsCmd.AddCommand(runsAllCmd) + runsCmd.AddCommand(runsSuccessfulCmd) + runsCmd.AddCommand(runsFailedCmd) + runsCmd.AddCommand(runsBeforeCmd) + runsCmd.AddCommand(runsAfterCmd) + + rootCmd.AddCommand(runsCmd) +} diff --git a/cmd/task.go b/cmd/task.go index f53577c..77b5933 100644 --- a/cmd/task.go +++ b/cmd/task.go @@ -5,11 +5,15 @@ Copyright © 2026 Luca A. package cmd import ( + "bytes" "fmt" "log" "os" "os/exec" + "strings" + "time" + "github.com/Nykenik24/foundry/core/runs" "github.com/Nykenik24/foundry/core/task" "github.com/Nykenik24/foundry/core/utils" "github.com/pelletier/go-toml/v2" @@ -25,6 +29,7 @@ var taskCmd = &cobra.Command{ } var newTaskCommand string +var newTaskStoreRuns bool var taskNewCmd = &cobra.Command{ Use: "new ", @@ -62,6 +67,7 @@ var taskNewCmd = &cobra.Command{ } tasks.Tasks[name] = task.NewTask(name, newTaskCommand) + tasks.Tasks[name].StoreRuns = newTaskStoreRuns newSrc, err := toml.Marshal(tasks) if err != nil { @@ -123,18 +129,18 @@ var taskRunCmd = &cobra.Command{ Short: "Run a task", Run: func(cmd *cobra.Command, args []string) { if !utils.PathExists(".foundry") { - log.Fatalf("Not a foundry project") + utils.PrintFatal("Not a foundry project") } if !utils.PathExists(".foundry/tasks.toml") { if err := os.WriteFile(".foundry/tasks.toml", []byte(""), 0644); err != nil { - log.Fatalf("Error when writing tasks document: %v", err) + utils.PrintFatal("Error when writing tasks document: %v", err) } } taskSrc, err := os.ReadFile(".foundry/tasks.toml") if err != nil { - log.Fatalf("Error when reading .foundry/tasks.toml: %v", err) + utils.PrintFatal("Error when reading .foundry/tasks.toml: %v", err) } name := args[0] @@ -148,19 +154,54 @@ var taskRunCmd = &cobra.Command{ } if _, exists := tasks.Tasks[name]; !exists { - log.Fatalf("Task '%s' doesn't exist", name) + utils.PrintFatal("Task '%s' doesn't exist", name) } - cmdStr := exec.Command("sh", "-c", tasks.Tasks[name].Cmd) + targetTask := tasks.Tasks[name] + cmdStr := exec.Command("sh", "-c", targetTask.Cmd) + + var stdout bytes.Buffer + var stderr bytes.Buffer - cmdStr.Stdout = os.Stdout - cmdStr.Stderr = os.Stderr + cmdStr.Stdout = &stdout + cmdStr.Stderr = &stderr cmdStr.Stdin = os.Stdin err = cmdStr.Run() + os.Stdout.Write([]byte(stdout.String())) + os.Stderr.Write([]byte(stderr.String())) + if err != nil { log.Printf("Error when running task: %v\n", err) - log.Fatalf("Command was '%s'", tasks.Tasks[name].Cmd) + utils.PrintFatal("Command was '%s'", tasks.Tasks[name].Cmd) + } + + if targetTask.StoreRuns { + utils.GlobalLogger.Log(fmt.Sprintf("Storing %s's run", targetTask.Name), utils.LogInfo) + if !utils.PathExists(".foundry/runs") { + os.Mkdir(".foundry/runs", 0755) + } + + taskLogs := strings.Split(stdout.String()+"\n"+stderr.String(), "\n") + + runInfo := runs.RunInfo{ + RanBy: "task/" + targetTask.Name, + Logs: taskLogs, + Success: true, + } + + runToml, err := toml.Marshal(runInfo) + if err != nil { + utils.PrintFatal("Error when marshaling run: %v", err) + } + + utils.GlobalLogger.Log(fmt.Sprintf("TOML for run: %s", string(runToml)), utils.LogInfo) + utils.GlobalLogger.Log(fmt.Sprintf("Name for run: %s.run", time.Now().Format(time.RFC3339)), utils.LogInfo) + + err = os.WriteFile(fmt.Sprintf(".foundry/runs/%s.toml", time.Now().Format(time.RFC3339)), runToml, 0644) + if err != nil { + utils.PrintFatal("Error when creating run file: %v", err) + } } }, } @@ -170,18 +211,18 @@ var taskListCmd = &cobra.Command{ Short: "List all tasks", Run: func(cmd *cobra.Command, args []string) { if !utils.PathExists(".foundry") { - log.Fatalf("Not a foundry project") + utils.PrintFatal("Not a foundry project") } if !utils.PathExists(".foundry/tasks.toml") { if err := os.WriteFile(".foundry/tasks.toml", []byte(""), 0644); err != nil { - log.Fatalf("Error when writing tasks document: %v", err) + utils.PrintFatal("Error when writing tasks document: %v", err) } } taskSrc, err := os.ReadFile(".foundry/tasks.toml") if err != nil { - log.Fatalf("Error when reading .foundry/tasks.toml: %v", err) + utils.PrintFatal("Error when reading .foundry/tasks.toml: %v", err) } tasks, err := task.RetrieveTasks(string(taskSrc)) @@ -202,6 +243,7 @@ var taskListCmd = &cobra.Command{ func init() { taskNewCmd.Flags().StringVar(&newTaskCommand, "cmd", "", "The command the task will have") + taskNewCmd.Flags().BoolVar(&newTaskStoreRuns, "store-runs", false, "Store runs of the task") taskCmd.AddCommand(taskNewCmd) taskCmd.AddCommand(taskRemoveCmd) diff --git a/core/runs/query.go b/core/runs/query.go new file mode 100644 index 0000000..42a6876 --- /dev/null +++ b/core/runs/query.go @@ -0,0 +1,64 @@ +package runs + +import "time" + +type QueryType int + +const ( + QSuccesful QueryType = iota + QFailed + QBefore + QAfter +) + +type RunQuery struct { + Result []*Run + Type QueryType +} + +func newRunQuery(runs []*Run, kind QueryType) *RunQuery { + return &RunQuery{ + Result: runs, + Type: kind, + } +} + +func QuerySuccessful(runs []*Run) *RunQuery { + var queried []*Run + for _, run := range runs { + if run.Info.Success { + queried = append(queried, run) + } + } + return newRunQuery(queried, QSuccesful) +} + +func QueryFailed(runs []*Run) *RunQuery { + var queried []*Run + for _, run := range runs { + if !run.Info.Success { + queried = append(queried, run) + } + } + return newRunQuery(queried, QFailed) +} + +func QueryBefore(runs []*Run, timestamp time.Time) *RunQuery { + var queried []*Run + for _, run := range runs { + if timestamp.After(run.Timestamp) { + queried = append(queried, run) + } + } + return newRunQuery(queried, QBefore) +} + +func QueryAfter(runs []*Run, timestamp time.Time) *RunQuery { + var queried []*Run + for _, run := range runs { + if timestamp.Before(run.Timestamp) { + queried = append(queried, run) + } + } + return newRunQuery(queried, QAfter) +} diff --git a/core/runs/runs.go b/core/runs/runs.go new file mode 100644 index 0000000..37b638d --- /dev/null +++ b/core/runs/runs.go @@ -0,0 +1,107 @@ +package runs + +import ( + "fmt" + "os" + "strings" + "time" + + "github.com/Nykenik24/foundry/core/utils" + "github.com/pelletier/go-toml/v2" +) + +type RunInfo struct { + RanBy string `toml:"ran_by"` + Logs []string `toml:"logs"` + Success bool `toml:"success"` +} + +type Run struct { + Timestamp time.Time + Info RunInfo +} + +func NewRun(timestamp time.Time, info RunInfo) *Run { + return &Run{ + Timestamp: timestamp, + Info: info, + } +} + +func RetrieveRuns() []*Run { + if !utils.PathExists(".foundry") { + utils.PrintFatal("Not in a foundry project") + } + + if !utils.PathExists(".foundry/runs") { + if err := os.Mkdir(".foundry/runs", 0755); err != nil { + utils.PrintFatal("Error creating runs directory: %v", err) + } + } + + entries, err := os.ReadDir(".foundry/runs") + if err != nil { + utils.PrintFatal("Error when reading runs directory: %v", err) + } + + var runs []*Run + + for _, entry := range entries { + name, ok := strings.CutSuffix(entry.Name(), ".toml") + if !ok { + continue + } + + timestamp, err := time.Parse(time.RFC3339, name) + if err != nil { + continue + } + + rawSrc, err := os.ReadFile(".foundry/runs/" + entry.Name()) + if err != nil { + utils.PrintFatal("Error reading %s: %v", entry.Name(), err) + } + + var info RunInfo + if err := toml.Unmarshal(rawSrc, &info); err != nil { + utils.PrintFatal("Error parsing %s: %v", entry.Name(), err) + } + + runs = append(runs, NewRun(timestamp, info)) + } + + return runs +} + +func PrintRuns(runs []*Run, firstLine string) { + fmt.Println(firstLine) + + for i, run := range runs { + prefix := "├──" + if i == len(runs)-1 { + prefix = "└──" + } + + var logN int + for _, log := range run.Info.Logs { + if log != "" { + logN++ + } + } + + var status string + switch run.Info.Success { + case true: + status = "\033[32mSuccessful" + case false: + status = "\033[31mFailed" + } + fmt.Printf("%s Run \033[34m%s\033[0m (%s\033[0m): by \033[33m%s\033[0m (has \033[35m%d\033[0m logs)\n", + prefix, + run.Timestamp.Format(time.DateTime), + status, + run.Info.RanBy, + logN, + ) + } +} diff --git a/core/task/task.go b/core/task/task.go index 3574000..6d7f9ff 100644 --- a/core/task/task.go +++ b/core/task/task.go @@ -7,8 +7,9 @@ import ( ) type Task struct { - Name string `toml:"name"` - Cmd string `toml:"cmd"` + Name string `toml:"name"` + Cmd string `toml:"cmd"` + StoreRuns bool `toml:"store_runs"` } type TasksDocument struct { @@ -17,8 +18,9 @@ type TasksDocument struct { func NewTask(name, cmd string) *Task { return &Task{ - Name: name, - Cmd: cmd, + Name: name, + Cmd: cmd, + StoreRuns: false, } }